3

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_()
Welshhobo
  • 115
  • 11

2 Answers2

4

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_()
eyllanesc
  • 235,170
  • 19
  • 170
  • 241
  • 1
    I would agree that avoiding stdin / stdout is almost always the easier answer, though it is sometimes unavoidable when you're working with a binary for the subprocess rather than another python script. – Aaron Nov 03 '20 at 04:02
  • I could kiss you for this! Only just learning about dbus, so this is magnificent help. Working perfectly with my full script, and will be donating a few coffees to you! I've read a bit about there being a specific QT event loop - `dbus.mainloop.qt.DBusQtMainLoop` - is there a reason this wasn't used or is it a case of it doesn't really matter? Also can you recommend any guides/tutorials on learning more?, i'm finding dbus to be really interesting. – Welshhobo Nov 06 '20 at 07:33
1

im not familiar with what you're using on your backend, but it should be as simple as this:

# in your frontend
import NAME_OF_BACKEND_PY_FILE_HERE as backend
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")
            backend.on_playback_control("play")
        elif status == "Pause":
            self.playbtn.setText("Play")
            print("Pause pressed")
            backend.on_playback_control("pause")

    def btnnext(self):
        print("Next pressed")
        backend.on_playback_control("next")

    def btnprev(self):
        print("Prev pressed")
        backend.on_playback_control("prev")

in your backend you should also remove this line: if __name__ == '__main__': and unindent all the code below it. im not sure how that function should be called normally or what the second variable 'condition' is for. but that's what I can come up with

Avi Baruch
  • 112
  • 2
  • 8
  • Don't think this is any good im afraid as the backend runs in its own loop. As soon as it reaches the import statement it stays in its loop until you exit manually. – Welshhobo Oct 30 '20 at 21:20
  • not sure if its loop would still work, you could try do threading in order to run 2 things at the same time. thats the only way I could think of that would make that work – Avi Baruch Oct 30 '20 at 21:23