Sumd (Graupner) mit Micropython auswerten

OP #7770524
Lesenswert?

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 :

1
import machine
2

3
uart1 = machine.UART(1, baudrate=115200, tx=12, rx=13)
4
uart1.init(115200, bits=8, parity=None, stop=1) 
5

6
while 1:
7
    chari = uart1.read(1)    
8
    if chari is not None:
9
        print("wert : ",chari)

..ich komme einfach nicht weiter. Ich schätze, ich kapier einfach die Codierung nicht ;-(

#7770539
Lesenswert?

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.

#7770540
Lesenswert?

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.

#7770568
Lesenswert?

Ernsthaft?

Welche Kapriolen denn noch?

1
#!/python
2
# Das ist empfangen worden:
3
ba = bytes((b'\xa8\x01\x06.\xd8.\xc83(/\x08.\xe0.\xe0\xfb\xe8'))
4
# Und nu schau'n mer mal:
5
for index,byte in enumerate(ba):
6
    print(f'{index:2}  0x{byte:02x} ({byte})')
7
#  0  0xa8 (168)
8
#  1  0x01 (1)
9
#  2  0x06 (6)
10
#  3  0x2e (46)
11
#  4  0xd8 (216)
12
#  5  0x2e (46)
13
#  6  0xc8 (200)
14
#  7  0x33 (51)
15
#  8  0x28 (40)
16
#  9  0x2f (47)
17
# 10  0x08 (8)
18
# 11  0x2e (46)
19
# 12  0xe0 (224)
20
# 13  0x2e (46)
21
# 14  0xe0 (224)
22
# 15  0xfb (251)
23
# 16  0xe8 (232)
24

25
# Und da das erste Servo bei index 3 & 4 liegt:
26
servopos = struct.unpack('H', struct.pack('2B', ba[4],ba[3]))[0]
27
# oder wie robert vorschlug:
28
servopos = int.from_bytes(ba[3:4+1], 'big')
Moderator #7770578
Lesenswert?

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.

Moderator #7770639
Lesenswert?

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.

#7770665
Lesenswert?

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.

Moderator #7770763
Lesenswert?

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.

Moderator #7770768
Lesenswert?

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.

#7770836
Lesenswert?

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.

OP #7771139
Lesenswert?

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 :

1
import machine
2

3
uart1 = machine.UART(1, baudrate=115200, tx=12, rx=13)
4
uart1.init(115200, bits=8, parity=None, stop=1) 
5

6
while 1:
7
    chari = uart1.read(1)
8
    while(chari != b'\xa8'):    # warte auf die 168
9
        chari = uart1.read(1)        
10
    uart1.read(1)               # 0x01 lesen
11
    n=uart1.read(1)             # Anzahl Servos (=6)
12
    if(n is not None):
13
        #print(n)
14
        h,l = uart1.read(2)         # 
15
        wert=h*256+l
16
        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.

#7771155
Lesenswert?

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.

OP #7771355
Lesenswert?

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.

#7771361
Lesenswert?

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.

OP #7771382
Lesenswert?

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.

OP #7771821
Lesenswert?

Das Decodieren mit der UART klappt prima, dank eurer Hilfe ! So sieht das jetzt aus:

1
import machine
2
import config
3

4
uart1 = machine.UART(1, tx=12, rx=13)
5
uart1.init(115200, bits=8, parity=None, stop=1, timeout=10) 
6

7
oszi = machine.Pin(27, machine.Pin.OUT)  #für testmessungen
8

9
def read_sumd():
10
    
11
    cha = uart1.read(1)
12
    while(cha != b'\xa8'):    # warte auf die 168
13
        cha = uart1.read(1)
14
    
15
    eins=uart1.read(1)        # 0x01 lesen
16
    zahl=uart1.read(1)        # Anzahl Servos (=6)
17

18
    oszi.on()
19
    
20
    sumdbytes=uart1.read(12)
21
    config.kanal[0] = sumdbytes[0]*256 + sumdbytes[1]
22
    config.kanal[1] = sumdbytes[2]*256 + sumdbytes[3]
23
    config.kanal[2] = sumdbytes[4]*256 + sumdbytes[5]
24
    config.kanal[3] = sumdbytes[6]*256 + sumdbytes[7]
25
    config.kanal[4] = sumdbytes[8]*256 + sumdbytes[9]
26
    config.kanal[5] = sumdbytes[10]*256 + sumdbytes[11]        
27

28
    oszi.off()

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?

#7771833
Lesenswert?

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 = zahl2 == uart.readinto(sumdbytes, zahl2), hast du direkt ein Flag ob's gut ging.

**Korrektur: Das war mit dem RP2350 Mit dem RP2040 dauert's 57.5µs

OP #7772171
Lesenswert?

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.

Beitrag #7772223 wurde von einem Moderator gelöscht.
OP #7774129
Lesenswert?

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 ?

#7774150
Lesenswert?

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)

1
#!/python
2
# -*- coding: utf-8 -*-
3
# vim: fileencoding=utf-8: ts=4: sw=4: expandtab:
4

5
import uasyncio
6
from array import array
7
import sys
8

9
async def task(counters, index, wait_ms):
10
    while True:
11
        await uasyncio.sleep_ms(wait_ms)
12
        counters[index] += 1
13

14
async def main(counters):
15
    uasyncio.create_task(task(counters, 0, 0))
16
    uasyncio.create_task(task(counters, 1, 1))
17
    uasyncio.create_task(task(counters, 2, 5))
18
    uasyncio.create_task(task(counters, 3, 10))
19
    await uasyncio.sleep_ms(5_000)
20

21
counters = array('I', (0,0,0,0))
22
uasyncio.run(main(counters))
23
print(counters)
24
#   array('I', [13708, 4999, 999, 499])

Antwort schreiben

Bitte melde dich an, um einen Beitrag zu schreiben.

oder

Mit Google-Account einloggen

Die Registrierung ist kostenlos und dauert nur eine Minute.

Jetzt registrieren