Hallo zusammen.
Eigentlich lautet die Frage nicht "wie decodiere ich SUMD?" von Graupner
Hott, sondern "wieso kapiere ich diese verdammten Datenformate nicht?"
Also laut Graupner schickt der Sender:
o Format: 115200 Baud, 8 Bit, 1 Stop-Bit;
o Der Frame sieht wie folgt aus:
o byte 0 = always '168'
o byte 1 = seems to be a kind of error indicator (1 normally..
o byte 2 = count of following channels
o byte 3 = high byte of Servo 0
o byte 4 = low byte of Servo 0; raw min is 8000 max is 16000
o ... repeats over the channel count
o byte n-1 = High byte of CRC
o byte n = Low byte of CRC16, which is CRC-CCITT (XModem) with 0x1021
as polynomial and 0x0000 as starting value.
An UART1 des ESP32 empfängt Micropython :
wert : b'\xa8'
wert : b'\x01'
wert : b'\x06'
wert : b'.'
wert : b'\xd8'
wert : b'.'
wert : b'\xc8'
wert : b'3'
wert : b'('
wert : b'/'
wert : b'\x08'
wert : b'.'
wert : b'\xe0'
wert : b'.'
wert : b'\xe0'
wert : b'\xfb'
wert : b'\xe8'
168, 1, 6 : das passt! Aber was kommt denn nun daher???
Hier mein Code :
Servo 0 hat 11992, das ist zwischen 8000 und 16000. Sieht doch alles gut
aus.
Print() versucht die Bytes als Zeichen zu interpretieren,
wahrscheintlich als UTF-8. Schau mal, ob Du einen Weg findest, die Daten
roh als HEX-Wert zu senden.
Walter T. schrieb:> Servo 0 hat 11992, das ist zwischen 8000 und 16000. Sieht doch> alles gut > aus.
Mein Problem ist viel grundlegender:
Wie rechne ich HB*256 + LB mit den Werten: b'.' und: b'\xd8'.
Oder anders gefragt, wie mach ich da Dezimalwerte draus ??
Reiner D. schrieb:> Walter T. schrieb:>> Servo 0 hat 11992, das ist zwischen 8000 und 16000. Sieht doch>> alles gut > aus.>> Mein Problem ist viel grundlegender:> Wie rechne ich HB*256 + LB mit den Werten: b'.' und: b'\xd8'.> Oder anders gefragt, wie mach ich da Dezimalwerte draus ??
struct ist dein Freund.
pack und unpack basteln alle mögliche Zeug zusammen und wieder
auseinander.
Ich bin mir gerade nicht 100%ig sicher, wie print() unter micropython
funktioniert. Für mich sieht das so aus:
Der Komma-Operator concateniert den String "wert : " und das Byte chari
in einen längeren String. Deswegen wird versucht, letzteres als Zeichen
darzustellen. Für 46 funktioniert das, es ergibt einen Punkt. Für 216
nicht (weil >128 und deswegen in UTF-8 als einzelnes Byte ungültig),
deswegen wird der Hex-Wert direkt zurückgegeben.
Wenn Du mit den Werten etwas anfangen will, solltest Du wohl die Bytes
erst einmal in einen String mit den Hex-Werten umbauen, bevor Du es mit
dem String davor zusammenpfropst.
>> Walter T. schrieb:>> Mein Problem ist viel grundlegender:>> Wie rechne ich HB*256 + LB mit den Werten: b'.' und: b'\xd8'.>> Oder anders gefragt, wie mach ich da Dezimalwerte draus ??
b'...' ist vom Type bytes, d.h. ein gepacktes Read-Only-Array von
Integerwerten im Bereich von 0 bis 255. Auf die einzelnen Array-Elemente
kannst du mit [] zugreifen.
Beispiel:
1
>>> bs = b'\x06'
2
>>> b[0]
3
6
4
>>> bs = b'.\xd8'
5
>>> bs[0]
6
46
7
>>> bs[1]
8
216
Mit uart1.read(1) wird 1 Byte gelesen, d.h. der Rückgabewert ist ein
bytes mit der Länge 1. Wenn du zwei zusammengehörige Bytes (bspw. bei
den Servo-Werten) erwartest, kannst auch gleich beide in einem Aufwasch
mit uart1.read(2) lesen.
Für das Zusammensetzen von 2 Bytes zu einem 16-Bit-Integer gibt es
mehrere Möglichkeiten, von denen zwei (int.from_bytes und struct.unpack)
bereits genannt wurden. Eine dritte Möglichkeit ist die Verwendung von
<< und |.
Das ganze Telegramm kann also folgendermaßen gelesen werden:
1
uart1.read(2) # 0xa8 0x01
2
n = uart1.read(1)[0] # Anzahl Servos
3
for i in range(n):
4
# mit int.from_bytes
5
val = int.from_bytes(uart1.read(2)) # byteorder='big' (default)
6
7
# mit struct. unpack:
8
# val, = struct.unpack('>H', uart1.read(2))
9
10
# ohne int.from_bytes und struct.unpack:
11
# bs = uart1.read(2)
12
# val = bs[0]<<8 | bs[1]
13
14
# dasselbe etwas pythonischer:
15
# high, low = uart1.read(2)
16
# val = high<<8 | low
17
18
print(f'Servo {i+1}: {val}')
Hinzu kommt natürlich noch die Auswertung der ersten beiden Bytes und
der CRC.
Yalu X. schrieb:> Norbert schrieb:>> servopos = struct.unpack('H', struct.pack('2B', ba[4],ba[3]))[0]>> Oder kurz:> servopos, = struct.unpack('>H', ba[3:5])
Na dann aber wenigstens: servopos,_ = …
Irgendein PEP (war's 8?)
Da wird sofort ersichtlich, dass ein Teil des Tupels brachial
weggeworfen wird. ;-)
Norbert schrieb:> Yalu X. schrieb:>> Norbert schrieb:>>> servopos = struct.unpack('H', struct.pack('2B', ba[4],ba[3]))[0]>>>> Oder kurz:>> servopos, = struct.unpack('>H', ba[3:5])>> Na dann aber wenigstens: servopos,_ = …
Nein, struct.unpack liefert im konkreten Fall ein Tupel mit 1 Element,
weswegen man auch nur eines extrahieren kann.
Norbert schrieb:> Irgendein PEP (war's 8?)> Da wird sofort ersichtlich, dass ein Teil des Tupels brachial> weggeworfen wird. ;-)
Es muss auch kein Teil des Tupels brachial weggeworfen werden.
Sind wir schon wieder an dem Punkt wo jeder unbedingt recht haben muss?
Ich formuliere aber gerne um: Durch x,_ = … wird erkennbar dass man nur
am ersten Wert des Tupels interessiert ist. (auch wenn in diesem Fall
das Tupel nur einen Wert enthält)
Da PEP aber sowieso nur proposals liefert, machen wir's doch einfach
beide so wie wir mögen.
Norbert schrieb:> Ich formuliere aber gerne um: Durch x,_ = … wird erkennbar dass man nur> am ersten Wert des Tupels interessiert ist.
Genauer gesagt: ..., dass man nur am ersten Werte eines Tupels mit genau
2 Elementen interessiert ist.
Das Muster auf der linken Seite der Zuweisung muss immer exakt zur
Listen-/Tupel-Struktur auf der rechten Seite passen.
> (auch wenn in diesem Fall das Tupel nur einen Wert enthält)
Das genau ist das Problem. Hat das Tupel nur 1 Element oder mehr als 2
Elemente, gibt es Schelte:
1
>>> x,_ = (11, 22)
2
>>> x
3
11
Soweit ok. Aber:
1
>>> x,_ = (11,)
2
Traceback (most recent call last):
3
File "<stdin>", line 1, in <module>
4
ValueError: not enough values to unpack (expected 2, got 1)
Und:
1
>>> x,_ = (11, 22, 33)
2
Traceback (most recent call last):
3
File "<stdin>", line 1, in <module>
4
ValueError: too many values to unpack (expected 2)
Das ist hier also keine Frage des Stils und hat deswegen nichts mit PEP
8 zu tun.
Norbert schrieb:> Reiner D. schrieb:>> Was macht das [0]?>> Das erzeugt eine Exception wenn innerhalb des Timeouts keine Daten> eintrudeln.
Wenn Reiner beschließt, einen Timeout für uart1 festzulegen (was er
derzeit noch nicht tut), muss er ausgelöste Timeouts natürlich auch
adäquat behandeln. Das ist dann aber wieder ein anderes Thema.
Yalu X. schrieb:> Wenn Reiner beschließt, einen Timeout für uart1 festzulegen (was er> derzeit noch nicht tut), muss er ausgelöste Timeouts natürlich auch> adäquat behandeln. Das ist dann aber wieder ein anderes Thema.
Das trifft es aber nicht.
Wenn man den Rückgabeparameter blindlings als bytes() auswertet und sich
sofort das erste Element nimmt, dann knallt es wenn man nur ein
einfaches ›None‹ zurück bekommt.
Also muss man erst einmal auf ›is not None‹ testen und dann
gegebenenfalls auseinander pflücken.
Vielen Dank für eure Mühe zunächst!
Das mit dem Timeout kommt noch, damit möchte ich die Abwesenheit des
Senders detektieren. das mache ich (wie die gesamte Decodiererei)
aktuell "analog" mit dem Impuls-SummenPPM (SumO). Und weil ich das
unelegant finde, der ganze Kram mit dem seriellen Signal. Vielleicht
kapier ich auch mal ein wenig mehr in der komplizierten Welt der
Datenformate in Python ;-)
Zunächst das harte Tagesgeschäft.
Es läuft schon bissi was, aber so recht versteh ich's nicht :
Produziert den Fehler:
File "<stdin>", line 14, in <module>
ValueError: need more than 1 values to unpack
Wenn ich die Zeile "print(n)" drin lasse, klappt alles. Ok, ohne das ich
verstehe, was da passiert, ist das nix wert. Wie kann der Print-Befehl
diese Auswirkung haben? Ein Timing-Problem? Das gibts doch gar nicht.
Reiner D. schrieb:> Vielen Dank für eure Mühe zunächst!>> Das mit dem Timeout kommt noch, damit möchte ich die Abwesenheit des> Senders detektieren. das mache ich (wie die gesamte Decodiererei)> aktuell "analog" mit dem Impuls-SummenPPM (SumO). Und weil ich das> unelegant finde, der ganze Kram mit dem seriellen Signal. Vielleicht> kapier ich auch mal ein wenig mehr in der komplizierten Welt der> Datenformate in Python ;-)>> Zunächst das harte Tagesgeschäft.> Es läuft schon bissi was, aber so recht versteh ich's nicht :> import machine> uart1 = machine.UART(1, baudrate=115200, tx=12, rx=13)> uart1.init(115200, bits=8, parity=None, stop=1)> while 1:> chari = uart1.read(1)> while(chari != b'\xa8'): # warte auf die 168> chari = uart1.read(1)> uart1.read(1) # 0x01 lesen> n=uart1.read(1) # Anzahl Servos (=6)> if(n is not None):> #print(n)> h,l = uart1.read(2) #> wert=h*256+l> print(wert)> Produziert den Fehler:>> File "<stdin>", line 14, in <module>> ValueError: need more than 1 values to unpack>> Wenn ich die Zeile "print(n)" drin lasse, klappt alles. Ok, ohne das ich> verstehe, was da passiert, ist das nix wert. Wie kann der Print-Befehl> diese Auswirkung haben? Ein Timing-Problem? Das gibts doch gar nicht.
Timeout!
Dein UART ist non-blocking (ohne timeout) initialisiert.
Wenn der print drin ist, hat der UART genug Zeit um schnell noch zwei
oder mehr Werte zu empfangen.
Ohne print geht's zu schnell, da bekommt er einen oder keinen.
Folglich: ValueError: need more than 1 values to unpack
Also solltest du bei der UART Initialisierung einen kleinen Timeout
spezifizieren.
uart1.init(115200, bits=8, parity=None, stop=1, timeout=20) # ms
Das hier: h,l = uart1.read(2)
heißt, lese wenn möglich und vorhanden zwei Bytes, aber nicht mehr.
Es heißt nicht, warte bis du zwei Bytes gelesen hast.
Ich möchte nicht sinnlos massig UART-Daten lesen, die ich in der
Häufigkeit eh nicht brauche. Also versuche ich die "main" mit der Uart
zu "synchronisieren".
In der main haben ich einen 100ms-Zyklustimer, der ein festes Zeitraster
vorgibt (nötig z.b. für die Regler oder Filter usw.) Darin setze ich ein
Flag ("Data new") auf 0, wenn ich die PPM-Daten in der main übernommen
habe. Die UART-Funktion aktiviere ich nur, wenn "Data new" auf ß liegt,
und nach dem ersten Durchlauf des Strings wird das wieder 1.
Habs probiert, das geht, 100ms ist zwar müde für eine Steuerfunktion,
aber für ein Schiff langt das.
Was ich nicht ganz kapiere ist, wieso ein Timeout den UART blockierend
macht.
Reiner D. schrieb:> Also versuche ich die "main" mit der Uart> zu "synchronisieren".
Schlechte Idee. Wenn keine Daten kommen, was dann?
Reiner D. schrieb:> Was ich nicht ganz kapiere ist, wieso ein Timeout den UART blockierend> macht.
Ein timeout von zB. 20ms blockiert den UART für bis zu 20ms während
auf die gewünschte Anzahl Bytes gewartet wird.
Wenn die Daten schneller (oder sofort) da sind, muss nicht blockiert
werden.
Solltest du mehrere Dinge quasi-parallel machen wollen, dann schau dir
die Möglichkeiten von asyncio an. Das ist praktisch (und stark
vereinfacht) kooperatives Multitasking.
Einer dieser Tasks könnte kontinuierlich die UART Daten entgegen nehmen
und in Blöcke packen.
Ein weiterer könnte analysieren und ein dritter zB. eine Steuerung
aktivieren.
Alles in den Docs.
PS.
Reiner D. schrieb:> In der main haben ich einen 100ms-Zyklustimer,
Warum so lahmarschig? Für 100ms brauchst du keinen Timer sondern einen
Abreißkalender.
Norbert schrieb:>>> Schlechte Idee. Wenn keine Daten kommen, was dann?
Das ist quasi das Funktionsprinzip dahinter. Irgendwie (beim SUMD noch
nicht realisiert) überwache ich, ob der Sender noch da ist. Bisher, beim
PPM-Impulssignal, habe ich das in der Interruptstruktur "nebenher"
gemacht. Der weggefallene (ausgeschaltete) Sender ist der Auslöser für
die autonome Fahrt (waypoints oder home)
>>>Das ist praktisch (und stark vereinfacht) kooperatives Multitasking.
Das mach ich mit einem NPRTM-Scheduler. Feste Zeitschlitze für die
verschiedenen Tasks (deren Laufzeiten ich kenne). Vermeidet alle
Probleme von kooperativem MT.
>>>Warum so lahmarschig? Für 100ms brauchst du keinen Timer sondern einen
Abreißkalender.
Das stimmt natürlich. Momentan hab ich wenige Prozent Auslastung, das
geht locker auch viel schneller. Aber das mach ich, wenn überhaupt (es
langt echt für ein Schiff..), erst später, wenn der Rest fertig ist.
Und, sehr schlimm?
Nachgemessen: das Lesen der 12 Kanaldaten-Bytes dauert gemessen rund
1ms.
(Gerechnet bei 120Bit und 115200 Baud: 1.03 ms)
Der Plan wäre jetzt, aus dem timeout der UART das Fehlen des
Sendesignals abzuleiten. Wäre viel eleganter als irgendwelche Messungen
von Interrupts.
Aber : ich krieg's nicht hin. Wie werte ich die Überschreitung der
maximalen Lesezeit aus? "Flush" ist offenbar fürs Schreiben, ein
"try/except" klappt auch nicht. Hat wer 'nen Tipp (einen "Spoiler"
sozusagen?
Zunächst einmal, die CRC16 sollte unbedingt geprüft werden.
Das dauert für einen kompletten Datenblock um die 35µs…45µs **, stellt
also praktisch keinerlei Overhead dar. Gibt aber so ein warmes Gefühl im
Bauch.
sumdbytes = uart1.read(12)
sollte anstelle von 12 die vorher ermittelte Zahl*2 sein.
Ein vorkonfiguriertes Array kann mittels readinto() wiederverwendet
werden.
Ungleich schneller und weniger GC.
Und wenn du testest auf flag = zahl*2 == uart.readinto(sumdbytes,
zahl*2), hast du direkt ein Flag ob's gut ging.
**Korrektur: Das war mit dem RP2350
Mit dem RP2040 dauert's 57.5µs
CRC ist klar, schon der "Eleganz" wegen ;-)
Array auch.
Die Kanalzahl ist eigentlich egal, weil ich nur den einen
Graupner-Sender habe, und der hat immer 6. Prinzipiell hast du aber
natürlich recht.
Die Erkennung, ob der Sender noch da ist, kann auch elegant der
Hott-Empfänger selber. Man kann ihn so einstellen, daß auch bei
Senderausfall ein Summensignal ansteht (sonst wirds schwierig, dann
bleibt z.b. meine Suchschleife nach "168" hängen), und dann wird ein
Fail-Safe Signal abgegeben, oder noch einfacher ist's, die "0x01" im
Kopf von Sumd auswerten : 1 = ok, 81 = Sender weg.
Noch Lust, über Micropython zu plaudern ?
Ich grüble gerade ein wenig über die Taskumschaltung.
Der Vorschlag, asyncio zu benutzen, ist interessant.
Aktuell läuft es so, daß ein Timer einen festen Scheduling-Takt vorgibt.
In 10ms-Raster wird die main "losgelassen". Damit werden die schnellsten
Aufgaben in Funktionsaufrufen bearbeitet. Natürlich muß dazu die
Laufzeit all dieser Aufgaben unter 10ms sein, weil ich eine
Taskunterbrechnug im Sinne eines präemptiven MT nie selber hinkriege.
Als Schicht drüber läuft in diesem Raster ein Zähler, der die 10 ms
durch n teilt (aktuell n=5). In diesem 50ms-Raster werden dann weniger
schnelle Aufgaben erledigt. Dieses System läßt sich beliebig komplex
auslegen. Man muß dazu nur die Laufzeiten der Aufgaben und die nötige
Häufigkeit ihres Aufrufs kennen. Ganz am Anfang der Prozessrechentechnik
wurde das mal so gemacht, die Methode hies:
"non-preemtive-realtime-multiplexing" (oder -multitasking) NPRM.
Alternative asyncio. Wenn ich das richtig verstehe macht es ein nicht
viel anderes timing. Es wird nicht unterbrochen. Das beim "normalen"
kooperativen MT erfolgende Scheduling meist bei i/o-Vorgängen
("blocked") wird hier im Normalfall durch Pause-Anweisungen erzwungen.
(Damit könnte man sogar ein gezieltes Unterbrechen vor Taskende
provozieren). Ist das besser als NPRM ?
Reiner D. schrieb:> Noch Lust, über Micropython zu plaudern ?>> Ich grüble gerade ein wenig über die Taskumschaltung.> Der Vorschlag, asyncio zu benutzen, ist interessant.> *Ganz am Anfang der Prozessrechentechnik*> wurde das mal so gemacht, die Methode hies:> "non-preemtive-realtime-multiplexing" (oder -multitasking) NPRM.> Alternative asyncio. Wenn ich das richtig verstehe macht es ein nicht> viel anderes timing. Es wird nicht unterbrochen. Das beim "normalen"> kooperativen MT erfolgende Scheduling meist bei i/o-Vorgängen> ("blocked") wird hier im Normalfall durch Pause-Anweisungen erzwungen.> (Damit könnte man sogar ein gezieltes Unterbrechen vor Taskende> provozieren). Ist das besser als NPRM ?
Ganz ehrlich? Alles ist besser im Jahre 2024. ;-)
Selbst eine einzelne Zeile innerhalb eines asyncio Tasks:
1
await uasyncio.sleep_ms(0)
gibt die Kontrolle instantan an den asyncio Scheduler zurück und der
lässt den nächsten Task weiter laufen.
Wenn kein anderer etwas zu tun hat (alle im Wartemodus, was fast immer
der Normalzustand ist), kommt der Bumerang schneller zurück als du ›hi‹
sagen kannst. Wir reden von tausenden von Taskswitches pro Sekunde.
1
await uasyncio.sleep_ms(time_in_milliseconds)
erlaubt es natürlich auch, eine verabredete Zeit zu warten.
Und jeder separate Task kann sich ein Timestamp holen und die nächste
Wartezeit für sich jeweils dynamisch berechnen.
asyncio ist aber nicht für Mikrosekunden-genaues Arbeiten. Dafür gibt
es andere Techniken. (und fast immer Hardware welche das besser als jede
CPU macht)