Python Startup Sound über USB-Box

Python auf Einplatinencomputer wie Raspberry Pi, Banana Pi / Python für Micro-Controller
Antworten
rob87
User
Beiträge: 45
Registriert: Donnerstag 17. Oktober 2019, 14:24

Hi,
ich habe ein Rpi 3B mit BASTER LITE und USB Soundbar.
Jetzt möchte ich nach dem Start Warten bis die USB Sound Bar da ist und dann ein Sound abspielen.

meine Crontab:

Code: Alles auswählen

 @reboot python3 /home/rfid/python/Control_Vol_by_AC511.py &
Mein Python Script (funktioiniert in der Console):

Code: Alles auswählen

import evdev #import InputDevice, categorize, ecodes
import os
import subprocess
# importing vlc module
import vlc

# importing time module
import time



def play_start():
	
	
	# creating vlc media player object
	player = vlc.MediaPlayer("/home/rfid/music/Startton.mp3")
	player.audio_set_volume(100)
	print("Akutelles VOl-Value :" +str(player.audio_get_volume()))
	player.play()
	time.sleep(4)
	player.stop()
		
		

def main():
	# bei def Volume starten
	command = ["amixer", "sset", "Master", "{}%".format(12)]
	subprocess.Popen(command)
	


	found_Bar =False
	
	while not found_Bar:
		try: 
				# Soundbar suchen
			devices = [evdev.InputDevice(path) for path in evdev.list_devices()]
			for device in devices:
				if device.name =="Dell Dell AC511 USB SoundBar":
					dev= evdev.InputDevice(device.path)
					found_Bar = True
					print(device.path, device.name, device.phys)
		except:
			print ("Event Code Suchcode has error")
	
	time.sleep(2)
	play_start()
	print(dev)
	dev.grab()
    
	for event in dev.read_loop():
		print(event.code)

		
if __name__ == "__main__":
	try: 
		main()
	finally:
		command = ["amixer", "sset", "Master", "{}%".format(0)]
		subprocess.Popen(command)
Die MP3 ist nur 4 Sekunden Lang.

Der Code scheint zu laufen, denn wenn ich ihn in der Console starte Kommt der Fehler das " dev.grab()" bereits Busy ist.

Habt Ihr noch einen Tipp für mich? Ist das vll. ein User Context Thema des VLCs?
__deets__
User
Beiträge: 14545
Registriert: Mittwoch 14. Oktober 2015, 14:29

Das ist ein Problem deines Setups. cron ist fuer sowas nicht geeignet, auch wenn die Seiten da draussen, die einem erzaehlen, etwas in @reboot zu haengen, in die Millionen zu gehen scheinen.

Der richtige Weg ist eine systemd-unit, die auf zwei Arten gestarted werden kann:

- aus einer udev-Regel, die den Service anfordert, wenn das USB device enumeriert und initialisiert worden ist.
- via einer Pfad-Abhaengikeit, also zB PathExists, die auf ein durch das USB-Device erzeugten Pfad zeigt (wahrscheinlich etwas in /dev/snd)

Was dir besser gefaellt, musst du entscheiden.
Benutzeravatar
__blackjack__
User
Beiträge: 13117
Registriert: Samstag 2. Juni 2018, 10:21
Wohnort: 127.0.0.1
Kontaktdaten:

@rob87: Anmerkungen zum Quelltext:

Eingerückt wird per Konvention mit vier Leerzeichen pro Ebene, nicht mit Tabulatorzeichen.

Kommentare sollen dem Leser einen Mehrwert über den Code geben. Faustregel: Kommentare beschreiben nicht *was* der Code macht, denn das steht da bereits als Code, sondern warum er das macht. Sofern das nicht offensichtlich ist. Offensichtlich ist in aller Regel auch was in der Dokumentation von Python und den verwendeten Bibliotheken steht.

Namen werden in Python klein_mit_unterstrichen geschrieben. Ausnahmen sind Konstanten (KOMPLETT_GROSS) und Klassen (PascalCase).

`os` wird importiert aber nirgends verwendet.

`Popen` startet das externe Programm asynchron. Das sollte man nur machen wenn man das *wirklich* machen will/braucht. Ausserdem entsteht ein Zombieprozess wenn man den Rückgabecode nicht vom beendeten Prozess abfragt. Und man erfährt nicht wenn das externe Programm mit einem Fehlercode endet. Und bevor man das alles selber mit dem `Popen`-Objekt macht, verwendet man einfach `subprocess.run()`.

``"{}%".format(12)`` ist recht umständlich um einfach ``"12%"`` zu schreiben. Wenn man statt eines festen Wertes eine Variable in eine Zeichenkette formatieren wollte, würde man heute eher ein f-Zeichenkettenliteral verwenden, statt die `format()`-Methode aufzurufen.

Da der Code der ``alsamixer`` aufruft zweimal fast identisch im Code steht, würde ich daraus eine Funktion machen.

`found_bar` ist im Grunde überflüssig weil man `dev` dafür verwenden könnte. Am Anfang auf `None` setzen und dann solange die Schleife abarbeiten bis das Gerät gefunden wurde.

Das ``try``/``except`` ohne konkrete Ausnahme ist falsch. Einmal weil das *alles* behandelt, und dann weil es alles sehr schlecht behandelt. Mit der Ausgabe das da ein Fehler war, ohne irgendeinen Anhaltspunkt *was* der Fehler war, kann man sehr schlecht auf Fehlersuche gehen.

Was ist denn dieses komische aus einem `InputDevice` das aus einem Pfad erstellt wurde den Pfad abfragen und daraus ein `InputDevice` zu erstellen? Da kommt doch wieder ein gleichwertiges `InputDevice` bei heraus‽ Falls nicht sollte man *das* tatsächlich kommentieren/begründen, denn so ist der Code ein grosses Fragezeichen an der Stelle.

Ungetestet:

Code: Alles auswählen

#!/usr/bin/env python3
import subprocess
import time

import evdev
import vlc
from more_itertools import first_true


def set_volume(percent):
    subprocess.run(["amixer", "sset", "Master", f"{percent}%"], check=True)


def play():
    player = vlc.MediaPlayer("/home/rfid/music/Startton.mp3")
    player.audio_set_volume(100)
    print("Aktueller Vol-Wert:", player.audio_get_volume())
    player.play()
    time.sleep(4)
    player.stop()


def main():
    set_volume(12)
    try:
        while True:
            device = first_true(
                map(evdev.InputDevice, evdev.list_devices()),
                None,
                lambda device: device.name == "Dell Dell AC511 USB SoundBar",
            )
            if device:
                break

        print(device.path, device.name, device.phys)

        time.sleep(2)
        play()
        print(device)
        device.grab()
        for event in device.read_loop():
            print(event.code)

    finally:
        set_volume(0)


if __name__ == "__main__":
    main()
Edit: Ganz vergessen: Es gibt eine Python-Anbindung an die ALSA-API, man muss da also nicht unbedingt ein externes Programm für starten.
„All religions are the same: religion is basically guilt, with different holidays.” — Cathy Ladman
rob87
User
Beiträge: 45
Registriert: Donnerstag 17. Oktober 2019, 14:24

__deets__ hat geschrieben: Dienstag 2. Januar 2024, 14:14 Der richtige Weg ist eine systemd-unit:

- aus einer udev-Regel, die den Service anfordert, wenn das USB device enumeriert und initialisiert worden ist.
Hallo,
Danke für deine Antwort. Ich habe jetzt ein Wenig zur Udev Regel gelesen, verstehen tu ich es trotzdem nicht.
Aktuell scheitere beim erstellen einer "10-AC511.rules" Datei in "/etc/udev/rules.d"
Mal ganz zu Schweigen das ich nicht weiß was ich dort hineinschreiben sollte.

Mittels

Code: Alles auswählen

udevadm info --attribute-walk --name=/dev/hidraw1
Habe ich schon herausgefunden:
looking at parent device '/devices/platform/soc/3f980000.usb/usb1/1-1/1-1.5/1>
KERNELS=="1-1.5.2"
SUBSYSTEMS=="usb"
DRIVERS=="usb"
ATTRS{authorized}=="1"
ATTRS{avoid_reset_quirk}=="0"
ATTRS{bConfigurationValue}=="1"
ATTRS{bDeviceClass}=="00"
ATTRS{bDeviceProtocol}=="00"
ATTRS{bDeviceSubClass}=="00"
ATTRS{bMaxPacketSize0}=="8"
ATTRS{bMaxPower}=="200mA"
ATTRS{bNumConfigurations}=="1"
ATTRS{bNumInterfaces}==" 4"
ATTRS{bcdDevice}=="0110"
ATTRS{bmAttributes}=="80"
ATTRS{busnum}=="1"
ATTRS{configuration}==""
ATTRS{devnum}=="11"
ATTRS{devpath}=="1.5.2"
ATTRS{devspec}=="(null)"
ATTRS{idProduct}=="a503"
ATTRS{idVendor}=="413c"
ATTRS{ltm_capable}=="no"
ATTRS{manufacturer}=="Dell"
ATTRS{maxchild}=="0"
ATTRS{power/active_duration}=="87890"
ATTRS{power/autosuspend}=="2"
ATTRS{power/autosuspend_delay_ms}=="2000"
ATTRS{power/connected_duration}=="87890"
ATTRS{power/control}=="on"
ATTRS{power/level}=="on"
ATTRS{power/persist}=="1"
ATTRS{power/runtime_active_time}=="87527"
ATTRS{power/runtime_status}=="active"
ATTRS{power/runtime_suspended_time}=="0"
ATTRS{product}=="Dell AC511 USB SoundBar"
Aber ohne das ich die Datei erstellen kann, brauche auch nicht zu suche wie ich ein Python-Script aus dem Verzeichnis
>> /home/rfid/python
starten kann.
__blackjack__ hat geschrieben: Dienstag 2. Januar 2024, 22:42
Eingerückt wird per Konvention mit vier Leerzeichen pro Ebene, nicht mit Tabulatorzeichen.
Darüber diskutiert man öfter und immer wieder. Die Restlichen Hinweise warn, wie immer, sehr hilfreif und dafür danke ich auch dir :)
Benutzeravatar
Dennis89
User
Beiträge: 1156
Registriert: Freitag 11. Dezember 2020, 15:13

Hallo,

eigentlich muss man nicht viel diskutieren:
https://peps.python.org/pep-0008/#tabs-or-spaces

Grüße
Dennis
"When I got the music, I got a place to go" [Rancid, 1993]
rob87
User
Beiträge: 45
Registriert: Donnerstag 17. Oktober 2019, 14:24

__blackjack__ hat geschrieben: Dienstag 2. Januar 2024, 22:42
Ungetestet:

Code: Alles auswählen

#!/usr/bin/env python3
import subprocess
import time
...
Edit: Ganz vergessen: Es gibt eine Python-Anbindung an die ALSA-API, man muss da also nicht unbedingt ein externes Programm für starten.

Hi, da gibt es mit dem code scheinbar Laufzeitprobleme. Zumindestens Lese ich das aus der Fehler Meldung:
Traceback (most recent call last):
File "/usr/local/lib/python3.11/dist-packages/evdev/device.py", line 125, in __init__
fd = os.open(dev, os.O_RDWR | os.O_NONBLOCK)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
FileNotFoundError: [Errno 2] No such file or directory: '/dev/input/event1'

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
File "/home/rfid/python/Control_Vol_by_AC511_V2.py", line 53, in <module>
main()
File "/home/rfid/python/Control_Vol_by_AC511_V2.py", line 27, in main
device = first_true(
^^^^^^^^^^^
File "/home/rfid/.local/lib/python3.11/site-packages/more_itertools/recipes.py", line 578, in first_true
return next(filter(pred, iterable), default)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/usr/local/lib/python3.11/dist-packages/evdev/device.py", line 127, in __init__
fd = os.open(dev, os.O_RDONLY | os.O_NONBLOCK)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
FileNotFoundError: [Errno 2] No such file or directory: '/dev/input/event1'

Hast du dazu noch eine Idee?
rob87
User
Beiträge: 45
Registriert: Donnerstag 17. Oktober 2019, 14:24

Weiter Test haben nun Ergeben, wenn ich mein Script starte und dann die USB BOX einstecke kommt auch kein ton. egal wie groß das Delay for "play_start()" ist. Wird das Script direkt im Anschluss gestartet. Kommt der Ton... Komisch
rob87
User
Beiträge: 45
Registriert: Donnerstag 17. Oktober 2019, 14:24

Weitere Erkenntnis: Wenn ich das "set_volume(12)" ausführe bevor ich die MP3 abspiele klappt es im händischen Versuch. nur mit der crontab nicht.
__deets__
User
Beiträge: 14545
Registriert: Mittwoch 14. Oktober 2015, 14:29

Koennte daran liegen, dass vlc irgendwie schon die bestehenden Audio-Dvices zum Programmstart ermittelt. Und danach nicht refresht.

Die Loesung von __blackjack__ finde ich persoenlich ziemlich dreckig, weil das Ding dauerhaft schleift, bis das USB-Geraet da ist. Und das aufzaehlen der ganzen Devices zumindest bei mir in anderen Projekten auch durchaus ne Weile gedauert hat, statt mit UDEV-Regel auf den Punkt zu kommen.
Benutzeravatar
__blackjack__
User
Beiträge: 13117
Registriert: Samstag 2. Juni 2018, 10:21
Wohnort: 127.0.0.1
Kontaktdaten:

@__deets__: Das ist ja nicht meine Lösung, ich habe das Original überarbeitet. Ich würde da auch eher mit UDEV arbeiten.
„All religions are the same: religion is basically guilt, with different holidays.” — Cathy Ladman
rob87
User
Beiträge: 45
Registriert: Donnerstag 17. Oktober 2019, 14:24

__deets__ hat geschrieben: Donnerstag 4. Januar 2024, 14:10 ...die bestehenden Audio-Dvices zum Programmstart ermittelt.
-> Dann müsste das Script ja bei händischen Start auch nicht funktionieren oder
__blackjack__ hat geschrieben: Donnerstag 4. Januar 2024, 16:08 Ich würde da auch eher mit UDEV arbeiten.
-> Mein Problem mit der Udev Regel ist:
rob87 hat geschrieben: Mittwoch 3. Januar 2024, 12:46 Aktuell scheitere beim erstellen einer "10-AC511.rules" Datei in "/etc/udev/rules.d"
-> das Script von Blackjack funktioniert ja leider auch nicht.

Vielleich sollte ich ein wenig mehr ausholen.

die Komplette Funktion soll folgendes machen:
RPi Startet und wartet das das USB-Gerät ( AC511 USB-SOUNDBAR) verfügbar ist [Soundbar wird immer per USB-HUB angeschlossen sein.]

Anschließend soll bei zugehörigen Event Code 114 das systemvol um 1% verringert und bei 115 das systemvol um 1% erhöht werden.
Dazu ist mir leider nur der Weg über eine While -Schleife inkl. entsprechender Funktion bekannt.
Benutzeravatar
__blackjack__
User
Beiträge: 13117
Registriert: Samstag 2. Juni 2018, 10:21
Wohnort: 127.0.0.1
Kontaktdaten:

@rob87: Meins funktioniert nicht weil ich das nackte ``except`` rausgenommen habe. Anscheinend gibt es Ausnahmen und da muss man eine *vernünftige* Behandlung einbauen, statt einfach *alles* damit zu behandeln einen generischen festen Text auszugeben.

Aber UDEV-Regel wäre schon sinnvoller als zu pollen.
„All religions are the same: religion is basically guilt, with different holidays.” — Cathy Ladman
Benutzeravatar
noisefloor
User
Beiträge: 3856
Registriert: Mittwoch 17. Oktober 2007, 21:40
Wohnort: WW
Kontaktdaten:

Hallo,

wenn du mit der UDEV-Regel nicht klar kommst gibt es ja noch zwei Möglichkeiten:

* eine systemd Path Unit die ausgeführt wird, wenn das entsprechende Verzeichnis, dass du brauchst, angelegt ist
* mit `systemd-analyze` deine Bootvorgang analysieren, schauen, welche Unit so mit als letztes gestartet ist und dann dein Skript über eine systemd Service Unit danach starten.

Gruß, noisefloor
__deets__
User
Beiträge: 14545
Registriert: Mittwoch 14. Oktober 2015, 14:29

So, ich habe das mal gebastelt, weil ich eh mehr Erfahrung mit systemd brauchen kann.

Das eigentliche Playback-Script:

Code: Alles auswählen

#!/usr/bin/python3
import pathlib
import time

import pygame.mixer


BASE = pathlib.Path(__file__).parent

WAV = BASE / "717412__timbre__trap-ed-in-portishead-stems-from-co-producer.wav"

assert WAV.exists()


def main():
    pygame.init()
    pygame.mixer.init()
    pygame.mixer.music.load(WAV)
    pygame.mixer.music.play()
    time.sleep(10)


if __name__ == '__main__':
    main()
Die playback.service unit, liegt in /etc/systemd/system. Achtung: username und attribute muessen angepasst werden!

Code: Alles auswählen

[Unit]
PartOf=graphical-session.target
After=graphical-session.target

[Install]
WantedBy=graphical-session.target

[Service]
ExecStart=/home/<user>/projects/private/usb-triggered-audio-playback/playback.py
User=<user>
Environment="XDG_RUNTIME_DIR=/run/user/1000"
Environment="PULSE_RUNTIME_PATH=/run/user/1000/pulse/"
Mit

sudo cp playback.service /etc/systemd/system/ && sudo systemctl daemon-reload
sudo systemctl start playback

muss das installiert werden & laufen. Mit

systemctl status playback

sieht man eventuelle Fehler.

Und dann noch eine rules-Datei, wobei ich das Produkt natuerlich nicht habe, und stattdessen einen FTDI USB Serial Converter genommen habe. Deine Rules muss entsprechend anders aussehen.

Code: Alles auswählen

ACTION=="add", ATTRS{product}=="FT232R USB UART", ATTRS{manufacturer}=="FTDI", TAG+="systemd", ENV{SYSTEMD_WANTS}="playback.service"
Das war es, beim einstecken spielt ein sample.
rob87
User
Beiträge: 45
Registriert: Donnerstag 17. Oktober 2019, 14:24

Hi,
wenn ich
__deets__ hat geschrieben: Freitag 5. Januar 2024, 10:49 ...

Code: Alles auswählen

[Service]
ExecStart=/home/<user>/projects/private/usb-triggered-audio-playback/playback.py

mit

Code: Alles auswählen

ExecStart=/bin/bash -c 'python3 -u /home/rfid/python/Control_Vol_by_AC511_V3.py'
ersetze, geht es beim einstecken. Nur beim Start geht es nicht. (Habe Allerdings noch VLC statt pygame im Versuch)
__deets__
User
Beiträge: 14545
Registriert: Mittwoch 14. Oktober 2015, 14:29

Start kann ich erst die Tage testen, ich starte jetzt nicht extra neu waehrend der Arbeit. Ich wuerde den Blick in die Ausgabe des journals empfehlen, Kommando habe ich ja gennant.
__deets__
User
Beiträge: 14545
Registriert: Mittwoch 14. Oktober 2015, 14:29

Ich hatte gerade noch eine Idee: ich habe - weil am Desktop unterwegs - bewusst einen Service gebaut, der in der Desktop session ausgefuehrt wird. Ggf. kannst du darauf verzichten, also Dinge wie die Umgebungsvariablen, den User (bzw root angeben) und das target entfernen, und dann will der ALSA. Das ging wiederum bei mir nicht, aber ist ja auch ein anderer Anwendungsfall.
Benutzeravatar
__blackjack__
User
Beiträge: 13117
Registriert: Samstag 2. Juni 2018, 10:21
Wohnort: 127.0.0.1
Kontaktdaten:

@rob87: Warum der Umweg über eine Bash‽
„All religions are the same: religion is basically guilt, with different holidays.” — Cathy Ladman
Benutzeravatar
noisefloor
User
Beiträge: 3856
Registriert: Mittwoch 17. Oktober 2007, 21:40
Wohnort: WW
Kontaktdaten:

Hallo,

Code: Alles auswählen

ExecStart=/bin/bash -c 'python3 -u /home/rfid/python/Control_Vol_by_AC511_V3.py'
ist unnötig, man muss keine Bash-Shell öffnen. Wenn dann

Code: Alles auswählen

ExecStart=/usr/bin/python3 /home/rfid/python/Control_Vol_by_AC511_V3.py'
Davon ausgehend, dass Python3 in /usr/bin liegt. Wo es normalerweise unter Linux liegt.

Gruß, noisefloor
Benutzeravatar
__blackjack__
User
Beiträge: 13117
Registriert: Samstag 2. Juni 2018, 10:21
Wohnort: 127.0.0.1
Kontaktdaten:

Wenn es passieren kann, das `list_devices()` Pfade liefert, die es dann aber gar nicht gibt wenn man ein `InputDevice` daraus erstellen möchte, muss man den entsprechenden `FileNotFoundError` halt ignorieren:

Code: Alles auswählen

#!/usr/bin/env python3
import subprocess
import time

import evdev
import vlc
from more_itertools import first_true, map_except


def set_volume(percent):
    subprocess.run(["amixer", "sset", "Master", f"{percent}%"], check=True)


def play():
    player = vlc.MediaPlayer("/home/rfid/music/Startton.mp3")
    player.audio_set_volume(100)
    print("Aktueller Vol-Wert:", player.audio_get_volume())
    player.play()
    time.sleep(4)
    player.stop()


def main():
    set_volume(12)
    try:
        while True:
            device = first_true(
                map_except(
                    evdev.InputDevice, evdev.list_devices(), FileNotFoundError
                ),
                None,
                lambda device: device.name == "Dell Dell AC511 USB SoundBar",
            )
            if device:
                break

        print(device.path, device.name, device.phys)

        time.sleep(2)
        play()
        print(device)
        device.grab()
        for event in device.read_loop():
            print(event.code)

    finally:
        set_volume(0)


if __name__ == "__main__":
    main()
Wie gesagt bin ich aber auch der Meinung, das polling keine schöne Lösung ist.

Edit: Und das wird wohl nichts finden weil in dem Namen „Dell“ zweimal steht. Das wurde vom Original übernommen, war dort also schon falsch.
„All religions are the same: religion is basically guilt, with different holidays.” — Cathy Ladman
Antworten