Raspberry Pi Funktion auslösen wenn Button gedrückt wurde

Python auf Einplatinencomputer wie Raspberry Pi, Banana Pi / Python für Micro-Controller
Antworten
mmueller-87
User
Beiträge: 21
Registriert: Sonntag 11. Juni 2023, 07:25

Liebe Forumgemeinde,

Ich bin noch Python Anfänger und gewillt einiges dazu zu lernen. Aber mich beschäftigt ein kleines Problem. Ich nutze einen Raspberry Pi um eine Tonfrequenz über die Konsole zu erzeugen:

Code: Alles auswählen

play -n -c1 synth 0 sine 67
Diese Ton (Beispiel: 67 Hz) soll über die Soundkarte ausgegeben werden. Das funktioniert soweit ganz gut. Nun möchte ich anhand von verschiedenen Buttons die Töne erzeugen. Wenn ich beispielsweise den Button 1 betätige, soll der Ton 67 Hz ausgegeben werden, bis der Button nicht mehr gedrückt wurde.

Da ich den Ton mit Hilfe von:

Code: Alles auswählen

os.system(f"play -n -c1 synth 0 sine {Hz}&")
erzeuge, stehe ich vor der Hürde das er diesen erst aufhört zu spielen, wenn ich mittels Konsole STRG + C drücke, da er diesen Prozess bis zur Beendigung abwartet. Nun möchte ich aber, wenn ich den Button los lasse, da er diesen Prozess beendet.

Ich hatte folgenden Ansatz:

Code: Alles auswählen

import time
import os, subprocess
import RPi.GPIO as GPIO

GPIO.setmode(GPIO.BCM)
GPIO.setup(4, GPIO.IN, pull_up_down=GPIO.PUD_UP)

while True:
    if GPIO.input(4) == False:
        os.system(f"play -n -c1 synth 0 sine {Hz}")
    else:
        os.system("pkill play")
Das funktioniert leider so nicht. Hat jemand einen Tipp oder kann mir evtl sagen, ob es mit Python an sich möglich ist, einen Hertz Ton zu erzeugen?
Benutzeravatar
__blackjack__
User
Beiträge: 14045
Registriert: Samstag 2. Juni 2018, 10:21
Wohnort: 127.0.0.1
Kontaktdaten:

@mmueller-87: `subprocess` wird ja schon importiert. `os.system()` sollte man sowieso nicht benutzen. Dessen Dokumentation verweist ja auch auf das `subprocess`-Modul.

`Popen`-Objekte haben dann eine `terminate()`-Methode um den Prozess, den man da gestartet hat, wieder zu beenden.

Man sollte da übrigens keine „busy waiting“-Schleife machen und die CPU damit unnötig belasten.

Und eventuell mal überlegen auf `gpiozero` umzusteigen.
„A life is like a garden. Perfect moments can be had, but not preserved, except in memory. LLAP” — Leonard Nimoy's last tweet.
Benutzeravatar
DeaD_EyE
User
Beiträge: 1238
Registriert: Sonntag 19. September 2010, 13:45
Wohnort: Hagen
Kontaktdaten:

Code nicht getestet:

Code: Alles auswählen

import signal
import subprocess
import RPi.GPIO as GPIO


class SynthPlayer:
    def __init__(self, frequency):
        self.cmd = ["play", "-n", "-c1", "synth", "0", "sine", str(frequency)]
        self.proc = None

    def start(self):
        self.stop()
        self.proc  = subprocess.Popen(self.cmd)

    def stop(self):
        if self.proc is not None:
            self.proc.terminate()


if __name__ == "__main__":
    player = SynthPlayer(440)
    CHANNEL = 4
    GPIO.setmode(GPIO.BCM)
    GPIO.setup(CHANNEL, GPIO.IN, pull_up_down=GPIO.PUD_UP)
    GPIO.add_event_detect(CHANNEL, GPIO.FALLING, callback=player.start, bouncetime=200)

    signal.pause()
sourceserver.info - sourceserver.info/wiki/ - ausgestorbener Support für HL2-Server
mmueller-87
User
Beiträge: 21
Registriert: Sonntag 11. Juni 2023, 07:25

Danke für Eure Antworten. Das mit dem os.system() habe ich auch chon gelesen, aber hatte gerade keine andere Methode gesehen.

@DeaD_EyE bei deinem Beispiel bekomme ich diesen Fehler. Habe auch noch nicht verstanden, wie ich dort dann mehere Buttons mit verschiedenen Freuqenzen erzeugen kann. Ich möchte insgesamt 4 Buttons einfügen, die verschiedene Frequenzen erzeugen sollen.

Code: Alles auswählen

Traceback (most recent call last):
  File "/home/pi/test.py", line 25, in <module>
    GPIO.add_event_detect(CHANNEL, GPIO.FALLING, callback=player.start, bouncetime=200)
RuntimeError: Failed to add edge detection
Edit: Das Problem hat ein veraltetes Paket verursacht. Habe das Paket deinstalliert:

Code: Alles auswählen

python3-rpi.gpio
und dieses nachinstalliert:

Code: Alles auswählen

python3-rpi-lgpio
und es funktioniert ohne diese Fehlermeldung. Nun erhalte ich allerdings folgendes:

Code: Alles auswählen

SynthPlayer.start() takes 1 positional argument but 2 were given
Benutzeravatar
Dennis89
User
Beiträge: 1547
Registriert: Freitag 11. Dezember 2020, 15:13

Beim Callback wird noch ein Pin-Objekt mit übergeben, das muss die Funktion annehmen können.
RPi benötigt noch ein „cleanup“.

Grüße
Dennis
"When I got the music, I got a place to go" [Rancid, 1993]
mmueller-87
User
Beiträge: 21
Registriert: Sonntag 11. Juni 2023, 07:25

Hallo Dennis,

der Pin wird doch übergeben?

Code: Alles auswählen

    CHANNEL = 4
    GPIO.setmode(GPIO.BCM)
    GPIO.setup(CHANNEL, GPIO.IN, pull_up_down=GPIO.PUD_UP)
    GPIO.add_event_detect(CHANNEL, GPIO.FALLING, callback=player.start, bouncetime=200)
Meinst du mit cleanup das heir?

Code: Alles auswählen

GPIO.cleanup()
Sirius3
User
Beiträge: 18270
Registriert: Sonntag 21. Oktober 2012, 17:20

`as` ist dazu da, einen Import umzubenennen, GPIO wird aber gar nicht umbenannt!
Konstanten werden üblicherweise am Anfang der Datei definiert.
Unter if __name__ sollte nur ein Aufruf der main-Funktion stehen.
Das callback erwartet ein Argument, `start` hat aber keins.
In Callbacks sollte möglichst wenig Code stehen, vor allem keine komplexen Systemaufrufe.
Am Ende des Programms sollte immer GPIO.cleanup aufgerufen werden.

Code: Alles auswählen

import subprocess
from queue import Queue
from RPi import GPIO

PIN_TO_FREQUENCY = {
    4: 440,
}

def setup():
    queue = Queue()
    GPIO.setmode(GPIO.BCM)
    for pin in PIN_TO_FREQUENCY:
        GPIO.setup(pin, GPIO.IN, pull_up_down=GPIO.PUD_UP)
        GPIO.add_event_detect(pin, GPIO.FALLING, callback=queue.put, bouncetime=200)
    return queue

def main():
    try:
        queue = setup()
        process = None
        while True:
            channel = queue.get()
            if process is not None:
                process.terminate()
            command = ["play", "-n", "-c1", "synth", "0", "sine", str(PIN_TO_FREQUENCY[channel])]
            process = subprocess.Popen(command)

    finally:
        GPIO.cleanup()
        if process is not None:
            process.terminate()


if __name__ == "__main__":
    main()
nezzcarth
User
Beiträge: 1762
Registriert: Samstag 16. April 2011, 12:47

Müssen das spezifische Frequenzen sein oder reichen auch midi-Noten, die du dann lokal an einen Synth deiner Wahl durchreichst? Für so etwas nehme ich gerne 'mido' (https://mido.readthedocs.io/en/stable/). Im Prinzip wäre die Aufgabe des Skripts dann nur, das Betätigen der Knöpfe in Midi an/aus-Signale umzuwandeln, die du dann nach Belieben weiterverarbeiten/weiterreichen kannst.
Zuletzt geändert von nezzcarth am Donnerstag 24. Juli 2025, 18:49, insgesamt 1-mal geändert.
mmueller-87
User
Beiträge: 21
Registriert: Sonntag 11. Juni 2023, 07:25

nezzcarth hat geschrieben: Donnerstag 24. Juli 2025, 18:37 Müssen das spezifische Frequenzen sein oder reichen auch midi-Noten, die du dann lokal an einen Synth deiner Wahl durchreichst? Für so etwas nehme ich gerne 'mido' (https://mido.readthedocs.io/en/stable/). Im Prinzip wäre die Aufgabe des Skripts dann nur, das Betätigen der Knöpfe in Midi an/aus-Signale umzuwandeln, die dann nach Belieben weiterverarbeiten/weiterreichen kannst.
Nein leider nicht. Habe ich alles probiert. @Sirius3

Bisher macht er alles was ich möchte. Wenn ich den Button 4 drücke, springt die Aufnahme an, wird aber beim loslassen des Button nicht beendet.
Benutzeravatar
Dennis89
User
Beiträge: 1547
Registriert: Freitag 11. Dezember 2020, 15:13

@mmueller-87 Ich war am Handy, deswegen ohne Beispiel. `SynthPlayer.start` bekommt "im Hintergrund" ein Pin-Objekt übergeben, kann das aber nicht annehmen, weil es nur `self` annehmen kann.

@Sirius3 Yeah, das ist mal ne cool Lösung, vor allem dass das übergebene Pin-Objekt genutzt wird. 🤩


Grüße
Dennis

Edit: Habe mal versucht, die Idee von Sirius3 beizubehalten und erweitert um den Zustand zu speichern. Ganz ungetestet:

Code: Alles auswählen

import subprocess
from queue import Queue

from RPi import GPIO

PIN_TO_FREQUENCY = {
    4: 440,
}


class Player:
    def __init__(self):
        self._process = None

    @property
    def process(self):
        return self._process

    @process.setter
    def process(self, value):
        self._process = value

    def stop(self, _):
        if self.process:
            self.process.terminate()


def setup(player):
    queue = Queue()
    GPIO.setmode(GPIO.BCM)
    for pin in PIN_TO_FREQUENCY:
        GPIO.setup(pin, GPIO.IN, pull_up_down=GPIO.PUD_UP)
        GPIO.add_event_detect(pin, GPIO.FALLING, callback=queue.put, bouncetime=200)
        GPIO.add_event_detect(pin, GPIO.RISING, callback=player.stop, bouncetime=200)
    return queue


def main():
    player = Player()
    try:
        queue = setup(player)
        while True:
            channel = queue.get()
            command = [
                "play",
                "-n",
                "-c1",
                "synth",
                "0",
                "sine",
                str(PIN_TO_FREQUENCY[channel]),
            ]
            player.process = subprocess.Popen(command)

    finally:
        GPIO.cleanup()
        if player.process is not None:
            player.stop(None)


if __name__ == "__main__":
    main()
"When I got the music, I got a place to go" [Rancid, 1993]
mmueller-87
User
Beiträge: 21
Registriert: Sonntag 11. Juni 2023, 07:25

Ich habe in der zwischenzeit mich auch etwas belesen. Habe mir erstmal ein paar Grundfunktionen erlernt. Habe zumindest den Codeschnipsel verstanden.
@Dennis89 Dein Vorschlag löst etliche Fehler aus.

Code: Alles auswählen

Traceback (most recent call last):
  File "/home/pi/test.py", line 62, in <module>
    main()
  File "/home/pi/test.py", line 41, in main
    queue = setup(player)
            ^^^^^^^^^^^^^
  File "/home/pi/test.py", line 34, in setup
    GPIO.add_event_detect(pin, GPIO.RISING, callback=player.stop, bouncetime=200)
  File "/usr/lib/python3/dist-packages/RPi/GPIO/__init__.py", line 862, in add_event_detect
    alert = _get_alert(gpio, mode, edge, bouncetime)
            ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/usr/lib/python3/dist-packages/RPi/GPIO/__init__.py", line 297, in _get_alert
    raise RuntimeError(
RuntimeError: Conflicting edge detection already enabled for this GPIO channel
Benutzeravatar
Dennis89
User
Beiträge: 1547
Registriert: Freitag 11. Dezember 2020, 15:13

Okay, wusste nicht, dass man nicht zwei Callbacks registrieren darf.

Andere Idee:

Code: Alles auswählen

import subprocess
from queue import Queue

from RPi import GPIO

PIN_TO_FREQUENCY = {
    4: 440,
}


def setup():
    queue = Queue()
    GPIO.setmode(GPIO.BCM)
    for pin in PIN_TO_FREQUENCY:
        GPIO.setup(pin, GPIO.IN, pull_up_down=GPIO.PUD_UP)
        GPIO.add_event_detect(pin, GPIO.BOTH, callback=queue.put, bouncetime=200)
    return queue


def main():
    try:
        queue = setup()
        process = None
        while True:
            channel = queue.get()
            if GPIO.input(channel):
                command = [
                    "play",
                    "-n",
                    "-c1",
                    "synth",
                    "0",
                    "sine",
                    str(PIN_TO_FREQUENCY[channel]),
                ]
                process = subprocess.Popen(command)
            else:
                if process is not None:
                    process.terminate()

    finally:
        GPIO.cleanup()
        if process is not None:
            process.terminate()


if __name__ == "__main__":
    main()
Das kann funktionieren, wenn beim Drücken des Tasters Strom auf den Pin fließt, wenn der anders rum angeschlossen ist, dann muss man `if not GPIO.input...` verwenden.

Grüße
Dennis
"When I got the music, I got a place to go" [Rancid, 1993]
mmueller-87
User
Beiträge: 21
Registriert: Sonntag 11. Juni 2023, 07:25

Hallo Dennis,

funktioniert super.
mmueller-87
User
Beiträge: 21
Registriert: Sonntag 11. Juni 2023, 07:25

Mahlzeit an alle,
habe mich gerade mal probiert und das OLED was ich hier rumliegen habe, nochmals angeschlossen. Nun bekomme ich die Frequenz angezeigt. Wenn ich jedoch Button 1 mit 67 Hz drücke, erscheint im Display auch die 67 Hz. Wenn ich den Button nun loslasse, bleibt die 67 Hz im Display stehen. Wenn ich dann zusätzlich den anderen Button mit 103.5 Hz (siehe im Code) drücke, überschreibt er die 67 Hz nicht sondern überlappt diese. Wenn keines der Button gedrückt ist, soll eigentlich 0 Hz da stehen.

Habe es mit disp.fill(0) und disp.show() in Zeile 54 und 70 versucht anzufügen, aber es bleibt der alte Wert stehen. Oder stelle ich mich zu doof an ?

Code: Alles auswählen

import subprocess
import time, os
from queue import Queue
from RPi import GPIO
import board
from board import SCL, SDA
import busio
from PIL import Image, ImageDraw, ImageFont
import adafruit_ssd1306

PIN_TO_FREQUENCY = {
    4: 88.5,
    17: 103.5,
}

# Create the I2C interface.
i2c = busio.I2C(SCL, SDA)
disp = adafruit_ssd1306.SSD1306_I2C(128, 32, i2c)

# Get drawing object to draw on image.
image = Image.new("1", (disp.width, disp.height))
draw = ImageDraw.Draw(image)
# font = ImageFont.load_default()
font = ImageFont.truetype('mine.ttf', 22)

def getfontsize(font, text):
    # Calculate the size of the text in pixels
    left, top, right, bottom = font.getbbox(text)
    return right - left, bottom - top

def setup():
    queue = Queue()
    GPIO.setmode(GPIO.BCM)
    for pin in PIN_TO_FREQUENCY:
        GPIO.setup(pin, GPIO.IN, pull_up_down=GPIO.PUD_UP)
        GPIO.add_event_detect(pin, GPIO.BOTH, callback=queue.put, bouncetime=200)
    return queue

def main():
    try:
        queue = setup()
        process = None
        while True:
            channel = queue.get()
            frequence = PIN_TO_FREQUENCY[channel]
            if not GPIO.input(channel):
                command = [
                    "play",
                    "-n",
                    "-c1",
                    "synth",
                    "0",
                    "sine",
                    str(frequence),
                ]
                process = subprocess.Popen(command)
            else:
                if process is not None:
                    process.terminate()

            # Draw a black filled box to clear the image.
            draw.rectangle((30, 1, disp.width, disp.height), outline=0, fill=0)

            frequence_text = str(frequence)
            # Get the width and height of the text in pixels
            (font_width, font_height) = getfontsize(font, frequence_text)
            draw.text((disp.width // 2 - font_width // 2, disp.height // 2 - font_height // 2),  frequence_text, font=font, fill=255)

            # Display image
            disp.image(image)
            disp.show()

    finally:
        GPIO.cleanup()
        if process is not None:
            process.terminate()

if __name__ == "__main__":
    main()
Benutzeravatar
__blackjack__
User
Beiträge: 14045
Registriert: Samstag 2. Juni 2018, 10:21
Wohnort: 127.0.0.1
Kontaktdaten:

@mmueller-87: Wenn da 0 Hertz stehen soll, dann muss `frequenz` auf 0 gesetzt werden wenn der Button losgelassen wird.

Auf Modulebene sollte nur Code stehen der Konstanten, Funktionen, und Klassen definiert.

`getfontsize()` hat einen irreführenden Namen, denn das ermittelt nicht die Grösse von der Schriftart, sondern die Grösse/„bounding box“ von gerendertem Text.

Die erste Zuweisung ``process = None`` muss ausserhalb des ``try`` stehen, weil es sonst im ``finally`` zu einem Folgefehler kommen wird wenn `setup()` eine Ausnahme auslöst.

Das `I2C`-Objekt sollte am Programmende sauber geschlossen werden. Man kann das mit der ``with``-Anweisung verwenden.

Das dürfte auch nicht wirklich sinnvoll funktionieren wenn ein Knopf gedrückt wird, während ein anderer noch nicht wieder losgelassen wurde.
„A life is like a garden. Perfect moments can be had, but not preserved, except in memory. LLAP” — Leonard Nimoy's last tweet.
mmueller-87
User
Beiträge: 21
Registriert: Sonntag 11. Juni 2023, 07:25

__blackjack__ hat geschrieben: Freitag 25. Juli 2025, 11:48 @mmueller-87: Wenn da 0 Hertz stehen soll, dann muss `frequenz` auf 0 gesetzt werden wenn der Button losgelassen wird.
Habe ich umgesetzt. War eben ein Denkfehler von mir.
__blackjack__ hat geschrieben: Freitag 25. Juli 2025, 11:48 Auf Modulebene sollte nur Code stehen der Konstanten, Funktionen, und Klassen definiert.
Ich hoffe ich habe das nun richtig verstanden, das die OLED Sektionen in einer Funktion untergebracht werden sollen? Falls ja, habe ich das mal erledigt.
__blackjack__ hat geschrieben: Freitag 25. Juli 2025, 11:48 `getfontsize()` hat einen irreführenden Namen, denn das ermittelt nicht die Grösse von der Schriftart, sondern die Grösse/„bounding box“ von gerendertem Text.
Gut, das Prinzip ist aber ähnlich, das der Text zentriert werden soll. Ob man nun über den Funktionsnamen eine Disskussion führen muss, ist fraglich.
__blackjack__ hat geschrieben: Freitag 25. Juli 2025, 11:48Die erste Zuweisung ``process = None`` muss ausserhalb des ``try`` stehen, weil es sonst im ``finally`` zu einem Folgefehler kommen wird wenn `setup()` eine Ausnahme auslöst.
Habe ich erledigt. Habe nun auch mal 2 Tasten gleichzeitig gedrückt und da passiert nichts was außer Kontrolle gerät. Zumal ich glaube so oder einen Drehschalter verwenden werde.
__blackjack__ hat geschrieben: Freitag 25. Juli 2025, 11:48Das `I2C`-Objekt sollte am Programmende sauber geschlossen werden. Man kann das mit der ``with``-Anweisung verwenden.
Hast du da ein Beispiel? Verstehe ich nicht so ganz. @__blackjack__ hier mal das angepasste Script:

Code: Alles auswählen

import subprocess
import time, os
from queue import Queue
from RPi import GPIO
import board
from board import SCL, SDA
import busio
from PIL import Image, ImageDraw, ImageFont
import adafruit_ssd1306

PIN_TO_FREQUENCY = {
    4: 88.5,
    17: 103.5,
}

# Create the I2C interface.
oled_i2c = busio.I2C(SCL, SDA)
oled = adafruit_ssd1306.SSD1306_I2C(128, 32, oled_i2c)

# Get drawing object to draw on image.
image = Image.new('1', (oled.width, oled.height))
draw = ImageDraw.Draw(image)

# Define fonts
# font = ImageFont.load_default()
font_freq = ImageFont.truetype('paul.ttf', 30)
font_boot = ImageFont.truetype('paul.ttf', 22)

def getfontsize(font, text):
    # Calculate the size of the text in pixels
    left, top, right, bottom = font.getbbox(text)
    return right - left, bottom - top

def oled_show(font, text):
    # Get the width and height of the text in pixels
    (font_width, font_height) = getfontsize(font, text)

    # Draw a black filled box to clear the image.
    draw.rectangle((0, 0, oled.width, oled.height), outline=0, fill=0)
    draw.text((oled.width // 2 - font_width // 2, oled.height // 6 - font_height // 6),  text, font=font, fill=255)

    # Display image
    oled.image(image)
    oled.show()

def setup():
    queue = Queue()
    GPIO.setmode(GPIO.BCM)
    for pin in PIN_TO_FREQUENCY:
        GPIO.setup(pin, GPIO.IN, pull_up_down=GPIO.PUD_UP)
        GPIO.add_event_detect(pin, GPIO.BOTH, callback=queue.put, bouncetime=200)
    return queue

def main():
    process = None
    try:
        queue = setup()
        while True:
            channel = queue.get()
            frequence = PIN_TO_FREQUENCY[channel]
            if not GPIO.input(channel):
                command = [
                    "play",
                    "-n",
                    "-c1",
                    "synth",
                    "0",
                    "sine",
                    str(frequence),
                ]
                process = subprocess.Popen(command)
            else:
                if process is not None:
                    process.terminate()
                    frequence = 0

            oled_show(font_freq, str(frequence) + "  Hz")

    finally:
        GPIO.cleanup()
        if process is not None:
            process.terminate()

if __name__ == "__main__":
    main()
Wenn ich das Script starte, erscheint erstmal ein schwarzer Bildschirm. Wäre es möglich, den vorhanden Button (Pin) zu ermitteln und zu schauen, welcher bereits gedrückt wurde?
Benutzeravatar
__blackjack__
User
Beiträge: 14045
Registriert: Samstag 2. Juni 2018, 10:21
Wohnort: 127.0.0.1
Kontaktdaten:

@mmueller-87: Modulebene ist der Code der Namen definiert die im ganzen Modul sichtbar sind. Also Importe, Funktionsdefinitionen, und Klassendefinitionen. Und Variablen. *Die* gehören da aber nicht hin, sofern sie nicht Konstanten sind. Wenn es Konstanten sind, dann werden sie auch gross geschrieben, damit man sie als solche erkennen kann.

Die Anzahl der Argumente die `oled_show()` fängt IMHO dann schon an grenzwertig zu werden, dass ich da drüber nachdenken würde alles was da zusätzlich gebraucht wird und bei jedem Aufruf gleich ist, zu einem Objekt zusammenzufassen.

Die Importe könnten aufgeräumt werden — da sind auch drei dabei, die überhaupt nicht benutzt werden.

Also für mich ist die Diskussion von Namen keine Frage: Gute Namen sind wichtig. Wenn man Sachen falsch benennt, ist es im besten Fall schwieriger zu verstehen was da passiert, im schlechten Fall ist der Namen falsch, man denkt man hat verstanden was passiert, aber es passiert eigentlich was anderes. Und wenn man etwas nicht oder falsch versteht, macht man unnötig Fehler.

Ausprobieren reicht nicht. Das mag so aussehen als ob alles funktioniert, muss es aber nicht. Und es kann sogar ”funktionieren”, aber eben nicht garantiert, also nicht immer oder auch unter bestimmten Umständen dann gar nicht. Wenn man zwei Knöpfe drückt ohne einen vorher wieder loszulassen wird der zweite Knopf den Prozess vom ersten nicht beendet. Python garantiert nicht wann (und im Grunde nicht einmal ob überhaupt) Objekte die im Programm nicht mehr zugreifbar sind, abgeräumt werden. Der erste Ton könnte also potentiell weiterlaufen solange das Programm läuft. Wenn man einen neuen Ton abspielt, muss man sicherstellen, das der alte auch wirklich aufhört.

`terminate()` reicht übrigens auch nicht aus, weil das nur den Prozess beendet, aber nicht den Rückgabecode abfragt, was zu Zombie-Prozessen führt. Das Betriebssystem räumt Prozesse nämlich erst ab wenn der Rückgabecode abgefragt wurde, weil erst dann sicher ist, dass der Elternprozess da keinen Zugriff mehr drauf haben will.

Zwischenstand (ungetestet):

Code: Alles auswählen

#!/usr/bin/env python3
import subprocess
from queue import Queue

import adafruit_ssd1306
import board
import busio
from board import SCL, SDA
from PIL import Image, ImageDraw, ImageFont
from RPi import GPIO

PIN_TO_FREQUENCY = {4: 88.5, 17: 103.5}


def get_rendered_size(font, text):
    """
    Calculate the size of the text in pixels.
    """
    left, top, right, bottom = font.getbbox(text)
    return right - left, bottom - top


def show_text(oled, image, draw, font, text):
    font_width, font_height = get_rendered_size(font, text)
    #
    # Draw a black filled box to clear the image.
    #
    draw.rectangle((0, 0, oled.width, oled.height), outline=0, fill=0)
    draw.text(
        (
            oled.width // 2 - font_width // 2,
            oled.height // 6 - font_height // 6,
        ),
        text,
        font=font,
        fill=255,
    )
    oled.image(image)
    oled.show()


def setup_pins():
    queue = Queue()
    GPIO.setmode(GPIO.BCM)
    for pin in PIN_TO_FREQUENCY:
        GPIO.setup(pin, GPIO.IN, pull_up_down=GPIO.PUD_UP)
        GPIO.add_event_detect(
            pin, GPIO.BOTH, callback=queue.put, bouncetime=200
        )
    return queue


def stop_process(process):
    if process:
        process.terminate()
        process.wait()


def main():
    process = None
    try:
        with busio.I2C(SCL, SDA) as oled_i2c:
            oled = adafruit_ssd1306.SSD1306_I2C(128, 32, oled_i2c)
            image = Image.new("1", (oled.width, oled.height))
            draw = ImageDraw.Draw(image)
            font = ImageFont.truetype("paul.ttf", 30)

            queue = setup_pins()
            for channel in iter(queue.get, None):
                if not GPIO.input(channel):
                    frequency = PIN_TO_FREQUENCY[channel]
                    stop_process(process)
                    process = subprocess.Popen(
                        [
                            "play",
                            "-n",
                            "-c1",
                            "synth",
                            "0",
                            "sine",
                            str(frequency),
                        ]
                    )
                else:
                    frequency = 0
                    stop_process(process)

                show_text(oled, image, draw, font, f"{frequency}  Hz")

    finally:
        GPIO.cleanup()
        stop_process(process)


if __name__ == "__main__":
    main()
Das Töne abspielen würde sich auch anbieten in einem Objekt zusammengefasst zu werden.
„A life is like a garden. Perfect moments can be had, but not preserved, except in memory. LLAP” — Leonard Nimoy's last tweet.
mmueller-87
User
Beiträge: 21
Registriert: Sonntag 11. Juni 2023, 07:25

Hallo,
vielen dank für Deine und Eure Hilfe. Klappt soweit ganz gut. Wenn 2 Button gedrückt wurden, führt er das zumindest nicht doppelt aus. Das klappt schon westenlich besser. Nun, da ich Druckknöpfe sind, kann es passieren, das vergessen wurde, den Button rauszudrücken. Beim starten unterscheidet er das aber nicht. Wenn der Button gedrückt wurde, zeigt er nichts an, bevor ich ihn nicht erneut drücke. Kann man den Status vorerst abfragen und anzeigen ?
Benutzeravatar
Dennis89
User
Beiträge: 1547
Registriert: Freitag 11. Dezember 2020, 15:13

Den Zustand des Pins kannst du mit `GPIO.input(<PIN_NUMMER>)` abfragen. Das wird in der `if`-Abfrage in der `main`-Funktion zum Beispiel auch gemacht.

Grüße
Dennis
"When I got the music, I got a place to go" [Rancid, 1993]
Antworten