Search code examples
pythonpython-3.xglibpyside2dbus

Integrating two Python scripts into one?


I have two Python scripts that i need to communicate with each another. The first is a GUI made in PySide2. The GUI consists of simple controls for controlling a bluetooth audio device (play, pause, next, previous, etc...). These commands operate with a second python script that i found. The second script is a loop that waits for these commands to be entered and responds once those commands are executed. I'm pretty new to programming, i'm guessing this is essentially connecting a front end with a back end, but its something i've never done before.

I've written a simplified version of my GUI to only display the controls i need. The "back-end" is also below but can originally be found here: https://scribles.net/controlling-bluetooth-audio-on-raspberry-pi/

I've previously asked a similar question and was given a solid and working answer by @eyllanesc here: Execute command to a Python script from separate Python script? However, using the QProcess method i could not work out how to get the print outputs from the back-end into the front-end script. The error messages print correctly however. I have tried playing around with sys.stdout in the back-end, variants of process.read, and QByteArrays but can't seem to get anything going.

The other issue i'm having is that the script will only work if a bluetooth device is connected prior to starting the script. If i disconnect while it is running and try to reconnect, it will no longer accept commands. If there is also a way to monitor whether a device is playing/paused so that the play/pause button can update depending on the devices state, that would also be useful, but its not important at this stage.

Theres a number of ways that it can be done, but i feel that ultimately it would be better for me to have both scripts integrated into one, however i'm open to any solution that works. If anyone has any advice or can get me started i'd be very appreciative!

Front-end:

import sys
from PySide2.QtWidgets import *

class MainWindow(QWidget):
    def __init__(self):
        QWidget.__init__(self)

        self.playbtn = QPushButton("Play")
        self.nextbtn = QPushButton("Next")
        self.prevbtn = QPushButton("Prev")

        layout = QVBoxLayout()
        layout.addWidget(self.playbtn)
        layout.addWidget(self.nextbtn)
        layout.addWidget(self.prevbtn)

        self.setLayout(layout)

        self.playbtn.released.connect(self.btnplay)
        self.nextbtn.released.connect(self.btnnext)
        self.prevbtn.released.connect(self.btnprev)

    def btnplay(self): #play button turns into pause button upon being pressed
        status = self.playbtn.text()
        if status == "Play":
            self.playbtn.setText("Pause")
            print("Play Pressed")
        elif status == "Pause":
            self.playbtn.setText("Play")
            print("Pause pressed")

    def btnnext(self):
        print("Next pressed")

    def btnprev(self):
        print("Prev pressed")


app = QApplication(sys.argv)

window = MainWindow()
window.show()
app.exec_()

Back-end:

import dbus, dbus.mainloop.glib, sys
from gi.repository import GLib


def on_property_changed(interface, changed, invalidated):
    if interface != 'org.bluez.MediaPlayer1':
        return
    for prop, value in changed.items():
        if prop == 'Status':
            print('Playback Status: {}'.format(value))
        elif prop == 'Track':
            print('Music Info:')
            for key in ('Title', 'Artist', 'Album'):
                print('   {}: {}'.format(key, value.get(key, '')))


def on_playback_control(fd, condition):
    str = fd.readline()
    if str.startswith('play'):
        player_iface.Play()
    elif str.startswith('pause'):
        player_iface.Pause()
    elif str.startswith('next'):
        player_iface.Next()
    elif str.startswith('prev'):
        player_iface.Previous()
    elif str.startswith('vol'):
        vol = int(str.split()[1])
        if vol not in range(0, 128):
            print('Possible Values: 0-127')
            return True
        transport_prop_iface.Set(
            'org.bluez.MediaTransport1',
            'Volume',
            dbus.UInt16(vol))
    return True


if __name__ == '__main__':
    dbus.mainloop.glib.DBusGMainLoop(set_as_default=True)
    bus = dbus.SystemBus()
    obj = bus.get_object('org.bluez', "/")
    mgr = dbus.Interface(obj, 'org.freedesktop.DBus.ObjectManager')
    player_iface = None
    transport_prop_iface = None
    for path, ifaces in mgr.GetManagedObjects().items():
        if 'org.bluez.MediaPlayer1' in ifaces:
            player_iface = dbus.Interface(
                bus.get_object('org.bluez', path),
                'org.bluez.MediaPlayer1')
        elif 'org.bluez.MediaTransport1' in ifaces:
            transport_prop_iface = dbus.Interface(
                bus.get_object('org.bluez', path),
                'org.freedesktop.DBus.Properties')
    if not player_iface:
        sys.exit('Error: Media Player not found.')
    if not transport_prop_iface:
        sys.exit('Error: DBus.Properties iface not found.')

    bus.add_signal_receiver(
        on_property_changed,
        bus_name='org.bluez',
        signal_name='PropertiesChanged',
        dbus_interface='org.freedesktop.DBus.Properties')
    GLib.io_add_watch(sys.stdin, GLib.IO_IN, on_playback_control)
    GLib.MainLoop().run()

UPDATE 31/10/2020: I've been playing around with the QProcess class suggested in my earlier question linked above. By using it on the button press functions and adding sys.exit after the command has been executed, it eliminates the need for a device to always be connected, but i still can't find a way to recieve the print outputs from back-end script. It also feels like a really dirty way of working. It also retains the issue with the play/pause state not automatically updating. If anyone has any suggestions i would be very grateful!

import sys
import os.path
from PySide2.QtCore import *
from PySide2.QtWidgets import *

CURRENT_DIR = os.path.dirname(os.path.realpath(__file__))

class MainWindow(QWidget):
    def __init__(self):
        QWidget.__init__(self)

        self.playbtn = QPushButton("Play")
        self.nextbtn = QPushButton("Next")
        self.prevbtn = QPushButton("Prev")

        layout = QVBoxLayout()
        layout.addWidget(self.playbtn)
        layout.addWidget(self.nextbtn)
        layout.addWidget(self.prevbtn)

        self.setLayout(layout)

        self.playbtn.released.connect(self.btnplay)
        self.nextbtn.released.connect(self.btnnext)
        self.prevbtn.released.connect(self.btnprev)

    def btnplay(self):
        self.process = QProcess()
        self.process.readyReadStandardError.connect(self.handle_readyReadStandardError)
        self.process.readyReadStandardOutput.connect(self.handle_readyReadStandardOutput)
        self.process.setProgram(sys.executable)
        script_path = os.path.join(CURRENT_DIR, "test2.py")
        self.process.setArguments([script_path])
        self.process.start()

        status = self.playbtn.text()
        if status == "Play":
            command = "play"
            self.playbtn.setText("Pause")
            print("play pressed")
        elif status == "Pause":
            command = "pause"
            self.playbtn.setText("Play")
            print("pause pressed")

        msg = "{}\n".format(command)
        self.process.write(msg.encode())

    def btnnext(self):
        self.process = QProcess()
        self.process.readyReadStandardError.connect(self.handle_readyReadStandardError)
        self.process.readyReadStandardOutput.connect(self.handle_readyReadStandardOutput)
        self.process.setProgram(sys.executable)
        script_path = os.path.join(CURRENT_DIR, "test2.py")
        self.process.setArguments([script_path])
        self.process.start()

        command = "next"
        msg = "{}\n".format(command)
        self.process.write(msg.encode())
        print("next pressed")

    def btnprev(self):
        self.process = QProcess()
        self.process.readyReadStandardError.connect(self.handle_readyReadStandardError)
        self.process.readyReadStandardOutput.connect(self.handle_readyReadStandardOutput)
        self.process.setProgram(sys.executable)
        script_path = os.path.join(CURRENT_DIR, "test2.py")
        self.process.setArguments([script_path])
        self.process.start()

        command = "prev"
        msg = "{}\n".format(command)
        self.process.write(msg.encode())
        print("prev pressed")

    def handle_readyReadStandardError(self):
        print(self.process.readAllStandardError().data().decode())

    def handle_readyReadStandardOutput(self):
        print(self.process.readAllStandardOutput().data().decode())


app = QApplication(sys.argv)

window = MainWindow()
window.show()
app.exec_()

Solution

  • IMHO the OP has an XY problem that adds unnecessary complexity to the application since the dbus eventloop can coexist with the Qt one as I show in the following example:

    import sys
    
    import dbus
    import dbus.mainloop.glib
    
    from PyQt5 import QtCore, QtWidgets
    
    
    class AudioManager(QtCore.QObject):
        statusChanged = QtCore.pyqtSignal(str)
        infoChanged = QtCore.pyqtSignal(dict)
    
        def __init__(self, parent=None):
            super().__init__(parent)
            self._player_iface = None
            self._transport_prop_iface = None
    
        def initialize(self):
            bus = dbus.SystemBus()
            obj = bus.get_object("org.bluez", "/")
            mgr = dbus.Interface(obj, "org.freedesktop.DBus.ObjectManager")
            player_iface = None
            transport_prop_iface = None
            for path, ifaces in mgr.GetManagedObjects().items():
                if "org.bluez.MediaPlayer1" in ifaces:
                    player_iface = dbus.Interface(
                        bus.get_object("org.bluez", path), "org.bluez.MediaPlayer1"
                    )
                elif "org.bluez.MediaTransport1" in ifaces:
                    transport_prop_iface = dbus.Interface(
                        bus.get_object("org.bluez", path), "org.freedesktop.DBus.Properties"
                    )
            if not player_iface:
                raise Exception("Error: Media Player not found.")
            if not transport_prop_iface:
                raise Exception("Error: DBus.Properties iface not found.")
    
            self._player_iface = player_iface
            self._transport_prop_iface = transport_prop_iface
    
            bus.add_signal_receiver(
                self.handle_property_changed,
                bus_name="org.bluez",
                signal_name="PropertiesChanged",
                dbus_interface="org.freedesktop.DBus.Properties",
            )
    
        def play(self):
            self._player_iface.Play()
    
        def pause(self):
            self._player_iface.Pause()
    
        def next(self):
            self._player_iface.Next()
    
        def previous(self):
            self._player_iface.Previous()
    
        def set_volume(self, Volume):
            if Volume not in range(0, 128):
                raise ValueError("Possible Values: 0-127")
            self._transport_prop_iface.Set(
                "org.bluez.MediaTransport1", "Volume", dbus.UInt16(vol)
            )
    
        def handle_property_changed(self, interface, changed, invalidated):
            if interface != "org.bluez.MediaPlayer1":
                return
            for prop, value in changed.items():
                if prop == "Status":
                    self.statusChanged.emit(value)
                elif prop == "Track":
                    info = dict()
                    for key in ("Title", "Artist", "Album"):
                        info[key] = str(value.get(key, ""))
                    self.infoChanged.emit(info)
    
    
    class MainWindow(QtWidgets.QWidget):
        def __init__(self, parent=None):
            super().__init__(parent)
    
            self._manager = AudioManager()
            self._manager.infoChanged.connect(self.handle_info_changed)
            self._manager.initialize()
    
            self.playbtn = QtWidgets.QPushButton("Play")
            self.nextbtn = QtWidgets.QPushButton("Next")
            self.prevbtn = QtWidgets.QPushButton("Prev")
    
            layout = QtWidgets.QVBoxLayout(self)
            layout.addWidget(self.playbtn)
            layout.addWidget(self.nextbtn)
            layout.addWidget(self.prevbtn)
    
            self.playbtn.released.connect(self._manager.play)
            self.nextbtn.released.connect(self._manager.next)
            self.prevbtn.released.connect(self._manager.previous)
    
        def handle_info_changed(self, info):
            print(info)
    
    
    if __name__ == "__main__":
        dbus.mainloop.glib.DBusGMainLoop(set_as_default=True)
    
        app = QtWidgets.QApplication(sys.argv)
    
        w = MainWindow()
        w.show()
    
        app.exec_()