Forum: PC-Programmierung Python Bluetooth


Announcement: there is an English version of this forum on EmbDev.net. Posts you create there will be displayed on Mikrocontroller.net and EmbDev.net.
von Detlef _. (detlef_a)


Angehängte Dateien:

Lesenswert?

Hallo,

ich hänge zwei Bluetooth Boxen an das Bluetooth device des Laptop und 
bespaße die mit Python. Mit sounddevice erstelle ich einen OutputStream 
und starte den. Die Daten werden in einer audio_callback routine als 
Rechtecksignal erzeugt. Das geht ganz gut, man hört am knackfreien Ton 
dass die callbacks immer rechtzeitig bedient werden.

Jetzt mache ich mit threading das ganze zweimal parallel auf zwei Boxen. 
Dann kommt die Sache schon ins Schwimmen, manchmal knackt es und die 
Zähler beider threads laufen auseinander.

Wie krieg ich hin die beiden Kanäle in Echtzeit störunsfrei auszugeben, 
ich möchte das auf insgesamt 12 Kanäle aufbohren?

Wo ist der Falschenhals?

Ist es der Interpreter, muss ich aus dem .py ein .exe machen?

Der laptop ist ein thinkpad E15, könnte der verbaute Bluetooth chip 
zicken?

Muss/kann ich die beiden callbacks hoch priorisieren?

Vielen Dank
Cheers
Detlef

von Εrnst B. (ernst)


Lesenswert?

Detlef _. schrieb:
> Wo ist der Falschenhals?

Testen und rausfinden.

> Ist es der Interpreter, muss ich aus dem .py ein .exe machen?

Kann ich mir kaum vorstellen.

> Der laptop ist ein thinkpad E15, könnte der verbaute Bluetooth chip
> zicken?

Möglich, aber bei nur zwei Audio-Streams unwahrscheinlich.

Also: Schnapp dir einen möglichst ressourcenschonenden Musikplayer, und 
starte den zweimal für deine beiden Boxen. Schau ob der beide 
Musikstreams ruckelfrei rüberbringt.
Dann teste mit mehr Boxen, bis zu deinen 12.

Wenn das geht, ist zumindest nicht Bluetooth der Flaschenhals, und wir 
schauen weiter.

von Oliver (imonbln)


Lesenswert?

Detlef _. schrieb:
> Wo ist der Falschenhals?

Da hilft nur testen

> Ist es der Interpreter, muss ich aus dem .py ein .exe machen?

Das wird nicht die Lösung sein, auch wenn es vielleicht zufällig dein 
Problem unterdrückt.

Wenn ich Spekulieren sollte, könnte das GIL ein teil deines Problems 
sein. Welche Python Version verwendest du? Ab 3.13 ist es deaktiviert.

Generell globale Variablen sind böse und sollten vermieden werden, 
besonders in nebenläufigen Programmen. Globale Variablen sollte man auch 
in Python großschreiben. Wenn du das Gefühl hast, das du Variablen 
nummerieren musst, kann das ein Hinweis darauf sein das man eigentlich 
ein Array haben will. Deine Variable devid gefällt mir nicht. Wenn die 
nicht zufällig 5 oder 6 ist, hast du hochdrehenden Leerlauf, weil dann 
raus nichts macht.
Außerdem sind die Berechnungen für cnt5 & cnt6 die gleichen, es hängt 
nur vom "index" in devid ab. Das solltest du also Umbauen. Denn ein 
Unterschied gibt es nicht. Die Globalen variablen sind* werden nicht 
verwendet.
Übrigens while ist keine Funktion, sondern ein Keyword, die Klammern 
sind überflüssig zudem schreibt man in Python while True, statt while 1. 
Globale Variablen im Thread zu ändern und im Hauptprogramm zu lesen ist 
undefiniertes verhalten, hierfür solltest du eine Queue verwenden. 
Vielleicht ist auch ein Threadpool etwas für dich, mit seiner map 
Implementierung.

von Detlef _. (detlef_a)


Lesenswert?

Hallo,

thx, was ist GIL? Ich benutze 3.9 . Die Source ist nix Fertiges, die 
genannten Aspekte hab ich soweit aufm Schirm, ich bin wie zu sehen neu 
in Py :|

Die Signale sind fest, die kann ich auch aus einer mp3 oder wav 
abspielen. Nur synchron müssen sie bleiben. Welcher Player kann denn 
Signale über 12 verschiedene Kanäle synchron abspielen? Nix gefunden.

THX
Cheers
Detlef

von Oliver (imonbln)


Lesenswert?

Detlef _. schrieb:
> thx, was ist GIL?

GIL ~ Global Interpreter Lock. Hier ist ein schöner Artikel in Englisch 
der den/das GIL unter Python erklärt.

https://realpython.com/python-gil/

von Ein T. (ein_typ)


Lesenswert?

Oliver schrieb:
> Das wird nicht die Lösung sein, auch wenn es vielleicht zufällig dein
> Problem unterdrückt.
>
> Wenn ich Spekulieren sollte, könnte das GIL ein teil deines Problems
> sein. Welche Python Version verwendest du? Ab 3.13 ist es deaktiviert.

Diese Vermutungen teile ich. Allerdings möchte ich darauf hinweisen, daß 
die Deaktivierung des GIL in Python 3.13 noch ein experimentelles 
Feature, und als solches nicht standardmäßig aktiviert ist. Die Version 
ohne GIL verwendet ein anderes Executable (python3.13t anstelle von 
python3.13), und sie muß bei der Kompilierung des Interpreters explizit 
aktiviert werden, siehe dazu auch [1].

[1] https://docs.python.org/3/whatsnew/3.13.html#free-threaded-cpython

> Übrigens while ist keine Funktion, sondern ein Keyword, die Klammern
> sind überflüssig zudem schreibt man in Python while True, statt while 1.
> Globale Variablen im Thread zu ändern und im Hauptprogramm zu lesen ist
> undefiniertes verhalten, hierfür solltest du eine Queue verwenden.
> Vielleicht ist auch ein Threadpool etwas für dich, mit seiner map
> Implementierung.

Ich würde stattdessen eher empfehlen, das Modul "multiprocessing" [2] 
anstelle von "threading" zu verwenden. Die API ist jener von "threading" 
sehr ähnlich, allerdings werden dann Prozesse anstelle von Threads 
verwendet, das vermeidet einerseits das GIL und sorgt andererseits auch 
dafür, daß jeder Prozeß seine eigenen Variablen besitzt, die ohne 
Seiteneffekte überschrieben werden können.

Darüber hinaus würde ich empfehlen, anstelle der diversen 
print()-Funktionen das Builtin-Modul "logging" zu verwenden. Neben 
Zeitstempeln und Loglevels bietet das Modul auch einige Vorzüge 
hinsichtlich der Frage, wohin geloggt wird: auf os.stdout, os.stderr 
wären beispielsweise auch eine Datei oder das systemweite Syslog 
möglich, wenn man in Produktion geht.

Wenn Funktionen zuvor bekannte Variablen benötigen, verwende ich 
anstelle von functools.partial gerne die im Anhang gezeigte Technik mit 
der Magic-Methode "__call__()" an einer eigenen Klasse. Diese 
Möglichkeit ist (aus meiner Sicht) sehr viel flexibler und auch deutlich 
besser lesbar, allerdings kenne ich auch andere Entwickler, die das 
nicht so sehen.

Außerdem ist es möglich, allerdings nicht sonderlich weit verbreitet, 
daß auch die Methode einer Instanz als Callback verwendet werden kann, 
also:
1
class Dings:
2
    def __init__(self, arg):
3
        self.arg = arg
4
    def methode(self, metharg):
5
        print('do sth with self.arg and metharg')
6
d = Dings('foo')
7
p = Process(target=d.methode, args=('bar',)) # oha, d.methode!
8
# ...

Das ist insbesondere im Zusammenhang mit dem Modul multiprocessing ein 
wenig tricky, denn dabei muß man sich in Erinnerung rufen, daß der 
UNIX-Systembefehl fork(2) (heute wird eher clone(2) genutzt, aber das 
funktioniert an dieser Stelle ganz ähnlich) eine haargenaue Kopie des 
aktuellen Prozesses mit einem neuen Speicherraum erzeugt. Dadurch hat 
man mit dieser Technik plötzlich je eine eigene Instanz pro Prozeß, die 
jedoch in jedem der Prozesse haargenau dieselbe Speicheradresse hat... 
aber das nur am Rande.

[2] https://docs.python.org/3/library/multiprocessing.html

Edit: Huch, Link vergessen.

: Bearbeitet durch User
von Detlef _. (detlef_a)


Lesenswert?

Hallo,

vielen Dank für den Rat.

Ich hab mal einfach
raus(5)
raus(6)
aufgerufen, also ohne threading. Das geht zwar, aber die Knacker 
bleiben. Das liegt also sicher daran, dass die callbackroutinen nicht 
rechtzeitig rankommen.

Ich habe auf das aktuelle PyCharm Community upgedatet. Das macht auch 
nur Python 3.9. und mag Multiprocessing überhaupt nicht installieren.

Welche IDE würdet Ihr denn empfehlen?

>> Finger weg von globalen Variablen <<
In der callbackroutine benötige ich eine static Variable, die gibts 
nicht also habe ich die global gemacht.

Wie wäre das denn in Python besser gemacht?

THX
Cheers
Detlef

von Ein T. (ein_typ)


Lesenswert?

Detlef _. schrieb:
> Ich hab mal einfach
> raus(5)
> raus(6)
> aufgerufen, also ohne threading. Das geht zwar, aber die Knacker
> bleiben. Das liegt also sicher daran, dass die callbackroutinen nicht
> rechtzeitig rankommen.

Hmmm... ehrlich gesagt würde ich mir das gerne einmal genauer anschauen, 
allerdings habe ich leider nicht Dein Setup -- und vor allem nicht Deine 
Hardware. Könntest Du die vielleicht etwas genauer beschreiben? 
Eventuell könnte ich hier etwas nachstellen.

Letzten Endes kann es nämlich auch sein, daß das Knacken von 
konkurrierenden Zugriffen auf Dein Bluetooth- oder Endgerät kommt...

> Ich habe auf das aktuelle PyCharm Community upgedatet. Das macht auch
> nur Python 3.9. und mag Multiprocessing überhaupt nicht installieren.
>
> Welche IDE würdet Ihr denn empfehlen?

Ich persönlich verwende den GNU Emacs, aber... das ist eine sehr alte 
UNIX-Software und wird deutlich anders bedient als moderne Editoren bzw. 
IDEs. Im Grunde genommen brauchst Du keine IDE, sondern ein ordentlicher 
Editor wie Geany, Atom, Sublime, VSCode, Vi(m), UltraEdit, Programmers 
Notepad... ein sauberes Syntax-Highlighting für Python sollte er 
natürlich haben, aber den ganzen Rest (Debugger etc.) kann man auch in 
jeder anständigen Kommandozeile (zB. Bash oder Powershell) verwenden.

> In der callbackroutine benötige ich eine static Variable, die gibts
> nicht also habe ich die global gemacht.
>
> Wie wäre das denn in Python besser gemacht?

Was genau meinst Du mit "static variable"? So etwas gibt es in Python 
nicht wirklich, jedenfalls nicht im Sinne dessen, was das Schlüsselwort 
"static" in C macht. Wenn Du eine Variable benötigst, die über mehrere 
Funktionsaufrufe hinweg ihren Wert behalten bzw. ändern soll, dann würde 
ich eine Klasse bzw. deren Instanz als eine Art "Container" für Deine 
Variable und eine Methode dieser Klasse für den Zugriff auf die Variable 
empfehlen, etwa so:
1
class Foo:
2
    def __init__(self, bar):
3
        self.bar = bar
4
    def increment(self):
5
        self.bar += 1
6
    def get_bar(self):
7
        return bar
8
9
if __name__ == '__main__':
10
    foo = Foo(0)
11
    print(foo.get_bar()) # gibt "0" aus
12
    foo.increment()
13
    foo.increment()
14
    foo.increment()
15
    print(foo.get_bar()) # gibt "3" aus

von Detlef _. (detlef_a)


Lesenswert?

Hi,

ok, Python mit einem Editor sollte gehen.

>>> enn Du eine Variable benötigst, die über mehrere
Funktionsaufrufe hinweg ihren Wert behalten bzw. ändern soll
<<<

Das ist Menge Holz für ne schlichte C static variable in der Routine. 
Na, mal sehen.

>>>
Könntest Du die vielleicht etwas genauer beschreiben?
Eventuell könnte ich hier etwas nachstellen.
<<<<
Ich hab mir nen Dutzend von diesen Billigdingern

https://www.amazon.de/LogiLink-SP0051-Bluetooth-Lautsprecher-MP3-Player-Schwarz/dp/B01L6PLKJM?th=1

geschossen, 2,38 das Stück :) . Das Problem tritt schon mit zweien auf. 
Ich habe auch keinerlei Schimmer wie der Bluetooth Chip im Lenovo E15 
das Protokoll bedient, ob der eventuell zwei Kanäle nicht bedienen kann.

Vllt. war das mit den Bluetooth Boxen doch nicht so eine gute Idee.

Vielen Dank
Cheers
Detlef

von Norbert (der_norbert)


Lesenswert?

Detlef _. schrieb:
> Das ist Menge Holz für ne schlichte C static variable in der Routine.
> Na, mal sehen.

Eine sehr deutliche Nummer kleiner lässt sich so etwas auch mit einer 
einfachen ›closure‹ erreichen.

von Ein T. (ein_typ)


Lesenswert?

Detlef _. schrieb:
>>>> enn Du eine Variable benötigst, die über mehrere
> Funktionsaufrufe hinweg ihren Wert behalten bzw. ändern soll
> <<<
>
> Das ist Menge Holz für ne schlichte C static variable in der Routine.
> Na, mal sehen.

Nicht wirklich. :-)

>>>>
> Könntest Du die vielleicht etwas genauer beschreiben?
> Eventuell könnte ich hier etwas nachstellen.
> <<<<
> Ich hab mir nen Dutzend von diesen Billigdingern
>
> 
https://www.amazon.de/LogiLink-SP0051-Bluetooth-Lautsprecher-MP3-Player-Schwarz/dp/B01L6PLKJM?th=1
> geschossen, 2,38 das Stück :) . Das Problem tritt schon mit zweien auf.
> Ich habe auch keinerlei Schimmer wie der Bluetooth Chip im Lenovo E15
> das Protokoll bedient, ob der eventuell zwei Kanäle nicht bedienen kann.

Schade, nicht mehr verfügbar... sonst hätte ich glatt ein paar gekauft.

> Vllt. war das mit den Bluetooth Boxen doch nicht so eine gute Idee.

Möglicherweise... was ist denn das eigentliche Ziel Deines Projekts?

von Ein T. (ein_typ)


Lesenswert?

Norbert schrieb:
> Eine sehr deutliche Nummer kleiner lässt sich so etwas auch mit einer
> einfachen ›closure‹ erreichen.

Eine "sehr deutliche Nummer kleiner" ist das nur dann, wenn man Klassen 
und ihre Instanzen als etwas Riesengroßes und Kompliziertes ansieht, was 
jedoch ziemlicher Humbug wäre. :-)

von Norbert (der_norbert)


Lesenswert?

Ein T. schrieb:
> Norbert schrieb:
>> Eine sehr deutliche Nummer kleiner lässt sich so etwas auch mit einer
>> einfachen ›closure‹ erreichen.
>
> Eine "sehr deutliche Nummer kleiner" ist das nur dann, wenn man Klassen
> und ihre Instanzen als etwas Riesengroßes und Kompliziertes ansieht, was
> jedoch ziemlicher Humbug wäre. :-)

Dir ist aber schon klar, das auch eine ›closure‹ instanziiert werden 
muss. Nur mit deutlich weniger Geplänkel ringsherum.
1
#!/usr/bin/python3
2
# -*- coding: utf-8 -*-
3
4
def closure():
5
    def inner():
6
        nonlocal static_var
7
        print(static_var)
8
        static_var += 1
9
    static_var = 100
10
    return inner
11
12
instance = closure()
13
instance()  # 100
14
instance()  # 101
15
instance()  # 102

von Ein T. (ein_typ)


Lesenswert?

Norbert schrieb:
> Dir ist aber schon klar, das auch eine ›closure‹ instanziiert werden
> muss.

Natürlich. Was möchtest Du mir sagen?

von Detlef _. (detlef_a)


Lesenswert?

Hi,

>>>>>
Schade, nicht mehr verfügbar... sonst hätte ich glatt ein paar gekauft.
<<<<<<

Doch, die gibs noch, hab mir gerade 12 weitere bestellt

https://www.lets-sell.de/smartphone-tablet/lautsprecher/34332/bluetooth-lautsprecher-schwarz-mp3-player-microsd-mikrofon-freisprechfunktion

>>>>>>
Möglicherweise... was ist denn das eigentliche Ziel Deines Projekts?
<<<<<<

Vielen Dank für die Frage. Ich hänge die Bluetooth Lautsprecher in die 
Ecken einer Turnhalle und lasse sie 
https://de.wikipedia.org/wiki/Pseudozufallsrauschen abstrahlen. Dann 
kann ich mit einem 
https://www.mikrocontroller.net/articles/MEMS-Mikrofone die 
Laufzeitdifferenzen des Schalls zu dem Mic bestimmen und daraus die 
Position des Mics im Raum. Das funktioniert mit Kabeln zu Lautsprechern 
schon gut, ohne Kabel ist natürlich schöner, deshalb Bluetooth. Das 
Rauschen geht vorher durch einen Hochpass sodass im Hörbereich nur ein 
Zischeln übrig blaibt.

:))
Cheers
Detlef

von Ein T. (ein_typ)


Lesenswert?

Detlef _. schrieb:
> Ein T. schrieb:
>> Schade, nicht mehr verfügbar... sonst hätte ich glatt ein paar gekauft.
>
> Doch, die gibs noch, hab mir gerade 12 weitere bestellt
>
> 
https://www.lets-sell.de/smartphone-tablet/lautsprecher/34332/bluetooth-lautsprecher-schwarz-mp3-player-microsd-mikrofon-freisprechfunktion

Oh, prima, habe mir gerade vier bestellt... Tipp: wenn Du unter den 
Beiträgen den Link "Markierten Text zitieren" benutzt, werden Deine 
Zitate sehr viel lesbarer und enthalten obendrein auch noch einen Link 
zu dem Beitrag, den Du zitierst, so daß unsere Leser dort ganz einfach 
den genauen Kontext des von Dir Zitierten nachvollziehen können. Das ist 
für alle Beteiligten inklusive Deiner selbst wesentlich komfortabler als 
diese Mergekonflikte.

> Vielen Dank für die Frage. Ich hänge die Bluetooth Lautsprecher in die
> Ecken einer Turnhalle und lasse sie
> https://de.wikipedia.org/wiki/Pseudozufallsrauschen abstrahlen. Dann
> kann ich mit einem
> https://www.mikrocontroller.net/articles/MEMS-Mikrofone die
> Laufzeitdifferenzen des Schalls zu dem Mic bestimmen und daraus die
> Position des Mics im Raum. Das funktioniert mit Kabeln zu Lautsprechern
> schon gut, ohne Kabel ist natürlich schöner, deshalb Bluetooth.

Ah, also Positionsbestimmung... würden dazu nicht drei oder vier 
Lautsprecher reichen, ähnlich wie die Satelliten bei GPS? Davon 
abgesehen bin ich mir nicht sicher, wie konstant die Latenzen bei BT 
sind.

von Detlef _. (detlef_a)


Lesenswert?

Ja, vier Lautsprecher reichen. Das ist wie bei GPS, da reichen auch 4 
Satelliten. Mit fünf ist es einfacher zu rechnen. Und je mehr man hat 
umso besser sieht man die. Die Latenz spielt keine Rolle, es kommt auf 
die Differenz der Ankunftszeiten an. Die wird für einen Bluetooth Chip 
nicht auseinanderlaufen, weil die Audiokanäle dann auch asynchron 
würden. Bei mehreren Chips mit unterschiedlichen clocks muss man 
schauen.
Cheers
Detlef

von Εrnst B. (ernst)


Lesenswert?

Detlef _. schrieb:
> Die wird für einen Bluetooth Chip
> nicht auseinanderlaufen, weil die Audiokanäle dann auch asynchron
> würden.

Find's grade nicht mehr, aber jemand hat da mal eine Messreihe gemacht.


Lautsprecher und Mikrophon am PC, Sound ausgeben, Delay messen bis der 
am Mic ankommt, das als "0-Stellung" festhalten.
Dann gleiches Mic, aber Soundausgabe über diverse Bluetooth-Kopfhörer 
und Boxen.

Da war von ~100ms (Für aptX-LowLatency Codecs) bis fast 600ms alles 
dabei (SBC Codec)

Hört sich jetzt schlimm an, aber, Lichtblick:
Die Bluetooth-Geräte können dem Host per "Delay Report" mitteilen, wie 
weit sie hinterher-hängen.
(AVDTP Delay Reporting, eigentlich für "Lip Sync" beim Videoschauen 
gedacht)
Der Host kann dann die Audio-Streams zu den einzelnen Lautsprechern 
entsprechend verzögern.
Schattenseite: Viele Geräte melden einfach statische 150ms auch wenn sie 
bei 140 oder 170ms liegen. Für Lip-Sync reicht das, bei dir liegst du 
halt mal 10 Meter daneben..

von Detlef _. (detlef_a)


Lesenswert?

Das delay ist mir insoweit egal als es für alle Lautsprecher gleich sein 
muss. Ich berechne die Position nicht aus der Laufzeit sondern aus der 
Differenz der Laufzeiten.
Cheers
Detlef

von 900ss (900ss)


Lesenswert?

Detlef _. schrieb:
> die Laufzeitdifferenzen des Schalls zu dem Mic bestimmen und daraus die
> Position des Mics im Raum.

Sehr spannend :) Stören die Reflektionen der Wände nicht?

Wer/Was trägt denn die Mikrofone? Also von was möchtest du die Position 
bestimmen?

von Detlef _. (detlef_a)


Lesenswert?

900ss schrieb:
> Detlef _. schrieb:
>> die Laufzeitdifferenzen des Schalls zu dem Mic bestimmen und daraus die
>> Position des Mics im Raum.
>
> Sehr spannend :) Stören die Reflektionen der Wände nicht?
>
> Wer/Was trägt denn die Mikrofone? Also von was möchtest du die Position
> bestimmen?

Ich bestimme mit dem Pseudorauschen den Frequenzgang und damit die 
Impulsantwort der Strecke zwischen Lautsprecher und Mic. Reflexionen 
sehe ich als verspätete Impulse und kann sie unterscheiden. Wenn ich 
einen Lautsprecher nur über eine Wandreflexion sehe ist das natürlich 
nutzlos für die Positionsbestimmung. Ich benötige freie Sicht auf die 
Lautsprecher, deswegen nehme ich viele.

Die Mics haben einen clock Eingang ca. 3MHz und einen Ein-Bit data 
Ausgang. Die lassen sich direkt an einen SPI hängen, ich nehme ein 
Nucleo-eval board.
Wenn man irgendwas mit freier SPI Schnittstelle hat kann man ein mic 
ranhängen und bekommt die Position im Raum.

Cheers
Detlef

von 900ss (900ss)


Lesenswert?

Detlef _. schrieb:
> Reflexionen sehe ich als verspätete Impulse und kann sie unterscheiden

Interessant...

Geht es jetzt erstmal "nur" um das Thema Positionsbestimmung oder gibt 
es schon ein weiteres Ziel von was die Position bestimmen werden soll?

von Detlef _. (detlef_a)


Lesenswert?

900ss schrieb:
> Detlef _. schrieb:
>> Reflexionen sehe ich als verspätete Impulse und kann sie unterscheiden
>
> Interessant...
>
> Geht es jetzt erstmal "nur" um das Thema Positionsbestimmung oder gibt
> es schon ein weiteres Ziel von was die Position bestimmen werden soll?

Nein, ich hab nix von dem ich die Position bestimmen muss.

"Und warum mach ich das? Weil ichs' kann." Alexander Gerst :)

Cheers
Detlef

von 900ss (900ss)


Lesenswert?

Detlef _. schrieb:
> "Und warum mach ich das? Weil ichs' kann." Alexander Gerst :)

Reicht mir auch oft als Grund :)

Bitte melde dich an um einen Beitrag zu schreiben. Anmeldung ist kostenlos und dauert nur eine Minute.
Bestehender Account
Schon ein Account bei Google/GoogleMail? Keine Anmeldung erforderlich!
Mit Google-Account einloggen
Noch kein Account? Hier anmelden.