Mit einem UT8803E Multimeter reden

Posted on So 02 April 2023 in Computer & Electronics

In meiner Sammlung von Messgeräten findet sich unter anderem ein Uni-T UT8803E Tischmultimeter. Aktuell ist es so um die 160€ zu haben und bietet 6000 counts (3 5/6 Stellen) Präzision – also reden wir hier eher vom unteren Ende der Preis-/Qualitätsskala und in online Foren sollte man lieber nicht zugeben, dass man dieses Instrument besitzt, weil die Klugscheißer sonst in Horden über einen herfallen, weil ein gebrauchtes Agilent oder Fluke Gerät natürlich bei weitem die bessere Wahl sei, wie jeder Nicht-Idiot zweifelsfrei erkenne könne.

Ich mag es dennoch ganz gern. Es hat ein schönes großes helles Display, misst so ziemlich alles, was ich in meinen Projekten so brauche und hat eigentlich ganz passable Messbereiche. Für den günstigen Preis gibt es leider keine Logging Funktion, was schon manchmal praktisch wäre. Aber immerhin hat es eine USB-Schnittstelle über die man dann extern loggen kann. Aber die mitgelieferte Software ist, wie so oft, nur für Windows tauglich und auch nicht gerade auf der Höhe des User Experience Designs:

Aber sie tut was sie soll und gibt uns Hinweise darauf, was man an Daten erwarten darf und welche Funktionen man fernsteuern kann. Und es sieht so aus, als könne man alle Daten über USB empfangen und auch alle Funktionen per Software steuern. Sehr gut!

Protokollerkundung

Jetzt müssen wir nur noch das Protokoll herausfinden. Der Form halber habe ich beim UNI-T Support angefragt, aber erwartungsgemäß keine Antwort bekommen. Also selber herausfinden – Gerät einschalten, USB-Kabel rein und mal schauen:

❯ lsusb
[...]
Bus 006 Device 008: ID 10c4:ea80 Silicon Labs CP2110 HID UART Bridge
[...]

Da ist es. Ich hatte eher auf einen normalen USB-UART Chip gehofft – sowas wie ein CP2102 oder PL2303HX oder so. Aber gut – HID also. Wie man sieht findet sich unser Multimeter an Bus 6 und hat die Gerätenummer 8 bekommen. Achtung: das müsst Ihr jedes mal überprüfen wenn das Gerät z.B. neu verbunden wurde, weil sich die Zahlen fröhlich ändern, je nachdem an welchem USB-Port Ihr es einsteckt und welche anderen Geräte zuvor bereits verbunden wurden. Also nicht wundern, wenn auch im Laufe meines Posts immer wieder mal unterschiedliche Bus und Device Numbers vorkommen sollten...

Vielleicht hat ja jemand schon was geeignetes programmiert? Eine Google-Suche nach Begriffen wie "UT8803E protocol", "UT8803E Linux" oder "UT8803E Python" war unergiebig. Also mal direkt auf Github versuchen – da findet sich mit dem Suchbegriff "UT8803E" immerhin dies: hskim7639/UNI-T. Das klingt sehr gut! Leider scheint das eher work in progress zu sein und verwendet zudem pywinusb, das wie der Name schon sagt, nur für Windows ist. Also schauen wir uns das mal selber an und verwenden ggf. Erkenntnisse aus dem gefunden Code..

Als erstes wollen wir mal prüfen, ob das Instrument von selber Daten sendet, oder ob wir dazu erst irgendeinen Befehl senden müssen. Also bauen wir ein kleines Python Script. Aber wie spricht man so ein HID Device an? Grundsätzlich gibt es die hid library. Alternativ existiert auch eine für den spezifischen Chip: pycp2110. Aber am erfreutesten war ich als ich sah, dass das auch von der pyserial library unterstützt wird. Damit das funktioniert müsst Ihr noch sicherstellen, dass auf dem System auch libhidapi-hidraw0 installiert ist. Und so sieht unser Skript aus:

#! /bin/env python3
import serial

with serial.serial_for_url("cp2110://0006:0008:00") as iface:
    while True:
        dat = iface.read(100)
        print(dat.hex())

Achtung: serial_for_url nimmt es mit dem Format sehr genau. D.h. die Anzahl der Stellen muss genau so sein, wie im obigen Code.

Und ausprobieren:

❯ ./ut8803e.py
ababcd120201312b302e303030303030303c30300433abcd120201312b302e303030303030303c30300433abcd120201312b302e303030303030303c30300433abcd120201312b302e303030303030303c30300433abcd120201312b302e30303030303030
3c30300433abcd120201312b302e303030303030303c30300433abcd120201312b302e303030303030303c30300433abcd120201312b302e303030303030303c30300433abcd120201312b302e303030303030303c30300433abcd120201312b302e3030
30303030303c30300433abcd120201312b302e303030303030303c30300433abcd120201312b302e303030303030303c30300433abcd120201312b302e303030303030303c30300433abcd120201312b302e303030303030303c30300433abcd12020131

Sehr gut – es kommen von selber Daten. Das ist doch schon mal eine gute Sache! Bei genauerer Betrachtung fällt auf, dass es bestimmte Dinge gibt, die sich immer wiederholen. Z.B. dieser Block 2e30303030 oder auch die Bytefolge abcd1202. Aber wie groß ist ein einzelner Record? Ausprobieren: Wir lesen einfach mal nicht 100 byte auf einmal, sondern erstmal nur 16:

❯ ./ut8803e.py
ab3c0433abcd120201312b302e303030
303030303c30300433abcd120201312b30
2e303030303030303c30300433abcd1202
01312b302e303030303030303c30300433
abcd120201312b302e30303030303030
3c30300433abcd120201312b302e3030

Nee – das war zu wenig. Also mal 17, 18, 19, und 21 etc. probieren und siehe da – bei Record Länge 21 bekommen wir dies:

❯ ./ut8803e.py
abcd120201312b302e303030303030303c30300433abcd120201312b302e303030303030303c30300433abcd120201312b302e303030303030303c30300433ab
abcd120201312b302e303030303030303c30300433
abcd120201312b302e303030303030303c30300433
abcd120201312b302e303030303030303c30300433
abcd120201312b302e303030303030303c30300433
abcd120201312b302e303030303030303c30300433

Beim ersten read() haben wir mehr bekommen als bestellt (wird da vielleicht erst irgendein Buffer geleert?), aber danach sieht das doch schön regelmäßig aus! Also gehen wir mal davon aus, dass wir die korrekte Länge des Records ermittelt haben. Aber irgendwie ist die Frage, ob wir hier wirklich einen kompletten Record erfassen, oder ob die Grenze vielleicht mitten in der Zeile liegt und wir so quasi an der falschen Stelle schneiden, denn ich sehe hier z.B. kein carriage return oder ähnliches.

Auch stellt sich die Frage, was die ganzen Bytes bedeuten. Zunächst habe ich dazu den Code in dem oben genannten github repo gelesen, aber so richtig erschöpfend war das auch nicht beschrieben oder implementiert. Immerhin konnte ich direkt bei UNI-T ein UT8000E Programming Manual.pdf finden. Darin finden sich zumindest Hinweise wie man bestimmte Bits und Bytes interpretieren muss, auch wenn das dort beschriebene Datenformat offenbar das einer eigenen Library ist und nicht das des eigentlichen Protokolls. Und natürlich konnte ich die im Dokument erwähnte UCI library nirgends finden.

Messwert finden

In unserem obigen Versuch waren alle Records identisch. Das ist auch nicht verwunderlich, denn es ist ja auch nix angeschlossen und das Display zeigt 0.000V. Das wollen wir nun ändern – in der Hoffnung, dass wir identifizieren können, welche Bytes diese Zahl beinhalten. Und es gibt schon einen natürlichen Kandidaten, denn wenn wir die Daten mal als normalen String ausgeben sehen wir dies:

\xab\xcd\x12\x02\x011+0.0000000<00\x043

Und der String '+0.000' riecht doch sehr nach Vorzeichen und Messwert. Also testen wir das mal. Dazu legen wir Spannung von ca 1.5V (Batterie) an:

\xab\xcd\x12\x02\x011+1.4950100<00\x04G
\xab\xcd\x12\x02\x011+1.4950100<00\x04G
\xab\xcd\x12\x02\x011+1.4950100<00\x04G
\xab\xcd\x12\x02\x011+1.4950100<00\x04G
\xab\xcd\x12\x02\x011+1.4950100<00\x04G
\xab\xcd\x12\x02\x011+1.4950100<00\x04G
\xab\xcd\x12\x02\x011+1.4950100<00\x04G
\xab\xcd\x12\x02\x011+1.4950100<00\x04G
\xab\xcd\x12\x02\x011+1.4950100<00\x04G
\xab\xcd\x12\x02\x011+1.4950100<00\x04G
\xab\xcd\x12\x02\x011+1.4950100<00\x04G
\xab\xcd\x12\x02\x011+1.4950100<00\x04G
\xab\xcd\x12\x02\x011+1.4950100<00\x04G
\xab\xcd\x12\x02\x011+1.4950100<00\x04G
\xab\xcd\x12\x02\x011+1.4950100<00\x04G

Und polen diese dann mal um:

\xab\xcd\x12\x02\x011-1.4950080<00\x04P
\xab\xcd\x12\x02\x011-1.4960080<00\x04Q
\xab\xcd\x12\x02\x011-1.4950080<00\x04P
\xab\xcd\x12\x02\x011-1.4950080<00\x04P
\xab\xcd\x12\x02\x011-1.4950080<00\x04P
\xab\xcd\x12\x02\x011-1.4820080<00\x04L
\xab\xcd\x12\x02\x011-1.4540080<00\x04K
\xab\xcd\x12\x02\x011-1.4960080<00\x04Q
\xab\xcd\x12\x02\x011-1.4950080<00\x04P
\xab\xcd\x12\x02\x011-1.4950080<00\x04P
\xab\xcd\x12\x02\x011-1.4980080<00\x04S
\xab\xcd\x12\x02\x011-1.4950080<00\x04P
\xab\xcd\x12\x02\x011-1.4960080<00\x04Q
\xab\xcd\x12\x02\x011-1.4960080<00\x04Q
\xab\xcd\x12\x02\x011-1.4960080<00\x04Q
\xab\xcd\x12\x02\x011-1.4960080<00\x04Q
\xab\xcd\x12\x02\x011-1.4950080<00\x04P
\xab\xcd\x12\x02\x011-1.4960080<00\x04Q

Es ist naheliegend, dass hier vier Stellen angezeigt werden und der Rest eine andere Bedeutung hat, denn das entspricht der Präzision des Instruments (3 5/6 Stellen). Also halten wir mal fest:

value = dat[6:12]   # Messwert als String

Als nächstes wollen wir mal schauen, wo der aktuelle Modus codiert wird, also Strom, oder Spannung oder... Im Programming Manual steht, dass das in einem eigenen Byte codiert ist. Dabei erwarten wir folgendes:

  • 00: AC V
  • 01: DC V
  • 02: AC µA
  • 03: AC mA
  • [...]

Also drehe ich mal am Schalter und schaue, wo in den Daten sich was verändert. Das ist an zwei Stellen der Fall: Ganz am Ende und an Position 4. Letzteres ist der Treffer – hier findet man die richtigen Werte. Also unit = dat[4].

Als nächstes machen wir uns auf die Suche nach dem Hold Flag. Multimeter auf Volt stellen und immer wieder Hold ein- und ausschalten. Und so habe ich es im LSB von dat[14] gefunden.

Und nach dem selben Schema erkunden wir nun min/max OL, rel usw.

Fernsteuerung

Was wir bisher noch nicht adressiert haben ist die Fernsteuerung des Instruments. Die Windows Software hat diverse Knöpfe, die im Wesentlichen denen am Gerät entsprechen: Hold, select, range, etc. Wie kommen wir an diesen Teil des Protokolls?

Indem wir die Software belauschen! Dazu installieren wir sie zunächst in einer virtuellen Windows Maschine unter Virtualbox. Als nächstes müssen wir noch den Zugriff auf USB erlauben. Dazu gibt es in Virtualbox unter Devices > USB die entsprechenden Einträge. Einfach das entsprechende Gerät auswählen und schon kann Windows mit ihm reden. Instrumenten-Software starten und hoffen. Und siehe da – es funktioniert. Ich kann die Messwerte in der Software sehen und über die Buttons das Multimeter fernsteuern.

Als nächstes brauchen wir ein Werkzeug mit dem wir den Datenverkehr abfangen können: Wireshark. Also installieren, falls noch nicht geschehen.

Nun müssen wir noch den Kernel-USB-Monitor aktivieren:

sudo modprobe usbmon

Dann Wireshark starten:

sudo wireshark

Nun müssen wir eines der zahlreichen usbmon devices auswählen. Eine kurze Überprüfung mit lsusb ergibt immer noch Bus 006 Device 008. Also usbmon6 auswählen.

Leider befindet sich auch mein WLAN Adapter auf Bus 6 und so werde ich nun fröhlich vollgespammt. Also müssen wir das einschränken, damit wir nur unser Multimeter sehen. Dazu setzen wir einen Display Filter: usb.src == "6.8.0" || usb.dst == "6.8.0"

Und schon bekommen wir dies:

Das sieht zwar schon ganz nett aus, aber es kommen ums verrecken keine weiten Pakete an, obwohl das Multimeter fröhlich Daten sendet, wie ich in der Windows-Software zweifelsfrei sehen kann. Komisch. Nach viel rumprobieren ist dann der Knoten geplatzt: Zwar findet das initiale Setup auf interface 0 statt, aber ganz offenbar wird danach ein neues interface 1 geöffnet auf dem dann der eigentliche Datenfluss passiert. Ändern wir den Display Filter auf usb.dst ~ "6\.8\.." || usb.src ~ "6\.8\.." sieht die Welt schon anders aus:

OK – gut. Als nächstes drücken wir mal den Hold Button in der Software und schauen, ob wir herausfinden können, was die Software dann sendet. Nachdem ich mich durch eine gefühlte Million URB_INTERRUPT in Packages gewühlt hatte, fand ich dann endlich ein paar vom Typ URB_INTERRUPT out – die kamen zur Abwechslung auf interface 2:

Und auf das Interface filtere ich nun erstmal:

OK - also wurden nach unserem Tastendruck vier Datenpakete hintereinander gesendet und die schauen wir uns nun genauer an:

Frame 571: 72 bytes on wire (576 bits), 72 bytes captured (576 bits) on interface usbmon6, id 0
USB URB
    [Source: host]
    [Destination: 6.8.2]
    URB id: 0xffff93fe33fc8840
    URB type: URB_SUBMIT ('S')
    URB transfer type: URB_INTERRUPT (0x01)
    Endpoint: 0x02, Direction: OUT
    Device: 8
    URB bus id: 6
    Device setup request: not relevant ('-')
    Data: present (0)
    URB sec: 1680358519
    URB usec: 534491
    URB status: Operation now in progress (-EINPROGRESS) (-115)
    URB length [bytes]: 8
    Data length [bytes]: 8
    [Response in: 572]
    [bInterfaceClass: HID (0x03)]
    Unused Setup Header
    Interval: 1
    Start frame: 0
    Copy of Transfer Flags: 0x00000000
    Number of ISO descriptors: 0
HID Data: 07abcd04460001c2

Frame 572: 64 bytes on wire (512 bits), 64 bytes captured (512 bits) on interface usbmon6, id 0
USB URB
    [Source: 6.8.2]
    [Destination: host]
    URB id: 0xffff93fe33fc8840
    URB type: URB_COMPLETE ('C')
    URB transfer type: URB_INTERRUPT (0x01)
    Endpoint: 0x02, Direction: OUT
    Device: 8
    URB bus id: 6
    Device setup request: not relevant ('-')
    Data: not present ('>')
    URB sec: 1680358519
    URB usec: 535116
    URB status: Success (0)
    URB length [bytes]: 8
    Data length [bytes]: 0
    [Request in: 571]
    [Time from request: 0.000625000 seconds]
    [bInterfaceClass: HID (0x03)]
    Unused Setup Header
    Interval: 1
    Start frame: 0
    Copy of Transfer Flags: 0x00000000
    Number of ISO descriptors: 0

Frame 573: 72 bytes on wire (576 bits), 72 bytes captured (576 bits) on interface usbmon6, id 0
USB URB
    [Source: host]
    [Destination: 6.8.2]
    URB id: 0xffff93fe33fc8840
    URB type: URB_SUBMIT ('S')
    URB transfer type: URB_INTERRUPT (0x01)
    Endpoint: 0x02, Direction: OUT
    Device: 8
    URB bus id: 6
    Device setup request: not relevant ('-')
    Data: present (0)
    URB sec: 1680358519
    URB usec: 750194
    URB status: Operation now in progress (-EINPROGRESS) (-115)
    URB length [bytes]: 8
    Data length [bytes]: 8
    [Response in: 574]
    [bInterfaceClass: HID (0x03)]
    Unused Setup Header
    Interval: 1
    Start frame: 0
    Copy of Transfer Flags: 0x00000000
    Number of ISO descriptors: 0
HID Data: 07abcd045a0001d6

Frame 574: 64 bytes on wire (512 bits), 64 bytes captured (512 bits) on interface usbmon6, id 0
USB URB
    [Source: 6.8.2]
    [Destination: host]
    URB id: 0xffff93fe33fc8840
    URB type: URB_COMPLETE ('C')
    URB transfer type: URB_INTERRUPT (0x01)
    Endpoint: 0x02, Direction: OUT
    Device: 8
    URB bus id: 6
    Device setup request: not relevant ('-')
    Data: not present ('>')
    URB sec: 1680358519
    URB usec: 751136
    URB status: Success (0)
    URB length [bytes]: 8
    Data length [bytes]: 0
    [Request in: 573]
    [Time from request: 0.000942000 seconds]
    [bInterfaceClass: HID (0x03)]
    Unused Setup Header
    Interval: 1
    Start frame: 0
    Copy of Transfer Flags: 0x00000000
    Number of ISO descriptors: 0

D.h. Zuerst wird ein Paket vom Typ URB type: URB_SUBMIT ('S') geschickt und als Antwort schickt das Instrument dann eins vom Typ URB type: URB_COMPLETE ('C'). Das erste enthält HIDdata, das zweite scheint eine Art Empfangsbestätigung zu sein.

Und das machen wir nun systematisch mit allen Buttons und notieren die gesendeten Messages:

# Hold
07abcd04460001c2
07abcd045a0001d6

# Back light
07abcd04470001c3
07abcd045a0001d6

# Select
07abcd04480001c4
07abcd045a0001d6

# Manual Range
07abcd04490001c5
07abcd045a0001d6

# Auto Range
07abcd044a0001c6
07abcd045a0001d6

# Min/Max
07abcd044b0001c7
07abcd045a0001d6

# Exit Min/max
07abcd044c0001c8
07abcd045a0001d6

# Rel
07abcd044d0001c9
07abcd045a0001d6

# D Value
07abcd044e0001ca
07abcd045a0001d6

# Q Value
07abcd044f0001cb
07abcd045a0001d6

# R value
07abcd04510001cd
07abcd045a0001d6

# Exit DQR
07abcd04500001cc
07abcd045a0001d6

Das erste Byte dürfte ankündigen, dass nun 7 Bytes folgen. Analog findet man bei der häppchenweise Übertragung der Messdaten nämlich Pakete, die so aussehen:

01ab
01cd
0112
0102
[...]

Also lassen wir das erste Byte im Folgenden weg. Wer genau hinschaut wird feststellen, dass die erste Message offenbar den eigentlichen Befehl codiert und die Zweite immer die gleiche ist – offenbar eine Art End-of-command (EOC) oder sowas. Und auch die Struktur der verschiedenen Messages ähnelt sich sehr untereinander. Stellen wir mal alle untereinander und sortiert dar, um das auf uns wirken zu lassen:

abcd04460001c2    # Hold          
abcd04470001c3    # Back light    
abcd04480001c4    # Select        
abcd04490001c5    # Manual Range  
abcd044a0001c6    # Auto Range    
abcd044b0001c7    # Min/Max       
abcd044c0001c8    # Exit Min/max  
abcd044d0001c9    # Rel           
abcd044e0001ca    # D Value       
abcd044f0001cb    # Q Value       
abcd04500001cc    # Exit DQR      
abcd04510001cd    # R value       
abcd045a0001d6    # EOC

Die ersten drei Bytes sind bei allen identisch. Vielleicht eine Art magic number, an der man eine valide Message erkennt? Dann folgen drei Bytes, die vermutlich den Key codieren. 460001, 470001 etc. Das letzte Byte könnte natürlich ebenso dazu gehören, aber die Tatsache, dass es schön brav hochzählt kommt mir irgendwie spanisch vor. Irgendwie riecht das nach einer Prüfsumme. Vor allem wenn man bedenkt, dass das eins zu eins mit den aufsteigenden Zahlen am Anfang der Key Codes korrespondiert. Und noch was fällt mir auf: schaut Euch mal im Vergleich die Daten an, die wir mit unserem Skript vom Gerät empfangen (mV Range DC, nix angeschlossen):

abcd12 0201302b3034352e373435303234303004 45
abcd12 0201302b3034312e393431303234303004 3f
abcd12 0201302b3033382e333338303234303004 45
abcd12 0201302b3033352e353335303234303004 41
abcd12 0201302b3033322e373332303234303004 3d
abcd12 0201302b3032372e353237303234303004 43
abcd12 0201302b3032342e303233303234303004 37
abcd12 0201302b3032322e383232303234303004 3c
abcd12 0201302b3031372e313137303234303004 3d
abcd12 0201302b3031332e363133303234303004 3a
abcd12 0201302b3031302e343130303234303004 32

Ich habe mal die ersten vier Byte und das letzte visuell abgesetzt. Hier ist der Start abcd12 gefolgt von 17 Daten Bytes und der vermuteten Prüfziffer. Bei den Knöpfen war es abcd04 gefolgt von drei Datenbytes und der Prüfziffer. Und:

0x12 = 18
0x04 = 4

Ich wette, das 0xabcd die Magic number für eine Message ist, dann kommt ein Byte, das angibt, wieviele Daten nun folgen (18 bzw. 4 Bytes) und das letze Byte dann die Prüfziffer ist. Natürlich könnte die Prüfung mit etwas wie CRC gemacht werden, aber dann kämen nicht so schöne aufsteigende Zahlen heraus. Vermutlich wirklich eine simple Prüfsumme. Und diese Hypothese prüfen wir nun anhand der ersten drei Messages:

hex(sum(bytearray.fromhex("abcd120201302b3034352e373435303234303004")))
'0x449'

hex(sum(bytearray.fromhex("abcd120201302b3034312e393431303234303004")))
'0x443'

hex(sum(bytearray.fromhex("abcd120201302b3033382e333338303234303004")))
'0x449'

# Empfangener Datensatz von oben: abcd12 0201312b302e303030303030303c303004 33
hex(sum(bytearray.fromhex("abcd120201312b302e303030303030303c303004")))
'0x437'

Hm – korrekt wären nach unserer Theorie 45, 3f, 45 und 33. D.h. wir liegen jedesmal um 0x404 daneben. Oder anders gesagt: die Prüfziffer umfasst die letzten zwei Bytes:

hex(sum(bytearray.fromhex("abcd120201302b3034352e3734353032343030")))
'0x445'
hex(sum(bytearray.fromhex("abcd120201302b3034312e3934313032343030")))
'0x43f'
hex(sum(bytearray.fromhex("abcd120201302b3033382e3333383032343030")))
'0x445'
hex(sum(bytearray.fromhex("abcd120201312b302e303030303030303c3030")))
'0x433'

Das muss natürlich noch mit größeren Datenmengen verifiziert werden, aber mein Bauchgefühl sagt, dass wir es geknackt haben.

D.h. nun fangen wir an, das mal vernünftig zu parsen. Solche Sachen machen direkt in Python keinen Spaß. Aber dafür gibt es die construct library, die ich zum Bitschubsen in Python liebe. Und so definieren wir darin erstmal ein Datenpaket:

import serial
form construct import *

with serial.serial_for_url("cp2110://0006:0008:00") as iface:
    iface.reset_input_buffer()
    while True:
        package = bytearray(iface.read(21))
        print ("\nraw: ", package.hex())

       # Decode values
       record = Struct(
                "signature" / Const(b"\xab\xcd"),
                "length"    / Int8ub,
                "data"      / Bytes(this.length)
                )

        payload = Struct(
                "rectype"   / Bytes(1),
                "mode"      / Int8ub,
                "range"     / Bytes(1),
                "value"     / Bytes(6),
                "foo"       / Bytes(7),  # Hier sind die diversen Flags drin
                "checksum"  / Int16ub
                )

        dat = record.parse(package)
        print(dat)
        val = payload.parse(dat["data"])
        print(val)

Und, oh Freude: Wir bekommen tatsächlich Werte:

raw:  abcd120200312b302e323036303030303c3030043a
Container:
    signature = b'\xab\xcd' (total 2)
    length = 18
    data = b'\x02\x001+0.2060000<00'... (truncated, total 18)
Container:
    rectype = b'\x02' (total 1)
    mode = 0
    range = b'1' (total 1)
    value = b'+0.206' (total 6)
    foo = b'0000<'... (total 7)
    checksum = 1082

raw:  abcd120200312b302e323036303030303c3030043a
Container:
    signature = b'\xab\xcd' (total 2)
    length = 18
    data = b'\x02\x001+0.2060000<00'... (truncated, total 18)
Container:
    rectype = b'\x02' (total 1)
    mode = 0
    range = b'1' (total 1)
    value = b'+0.206' (total 6)
    foo = b'0000<'... (total 7)
    checksum = 1082

D.h. das Parsen funktioniert – nur die Decodierung der diversen Bits (Hold etc.) fehlt noch in obigem Code.

Auf Sendung!

Weiter oben hatten wir kühne Theorien darüber aufgestellt, mit welchen Befehlen man das Instrument fernsteuern kann. Nun wagen wir uns daran, das zu verifizieren, indem wir einfach die oben entdeckten Sequenzen (jeweiliges Kommando und Confirmation Message) an das Gerät senden:

import serial
iface = serial.serial_for_url("cp2110://0006:0008:00")

set_hold = b'\xab\xcd\x04\x46\x00\x01\xc2'
confirm = b'\xab\xcd\x04\x5a\x00\x01\xd6'
iface.write(set_hold)
iface.write(confirm)

Hurra! Es funktioniert! Sobald die beiden Sequenzen gesendet sind, piepst das Gerät kurz und wechselt in den Hold Modus bzw. verlässt diesen wieder.

Device ID?

Nun haben wir fast alle Geheimnisse des Protokolls gelüftet. Aber wenn man die Originalsoftware anschaut gibt es noch einen offenen Punkt: Da wird eine Device-ID angezeigt. Ich habe zwar keine Ahnung, wozu ich die jemals brauchen könnte, aber der Perfektionist in mir will die auch noch auslesen. Also nochmal Wireshark bemühen und mit der Aufzeichnung beginnen, bevor ich die PC-Software in der Virtuellen Maschine starte. Einstellungen analog zu oben, als wir die Buttons ausgetüftelt hatten. Und tatsächlich sendet die Software ganz am Anfang ein Kommando, ohne dass ein Knopf gedrückt wurde:

# request Device-ID
abcd04580001d4
abcd045a0001d6

Und wenn wir das tun, dann kommt kurz darauf ein Datenpaket zurück, das sich von den normalen Messwert Messages unterscheidet:

abcd17003030353030303236333830303030313933

Und wenn man das als ASCII String interpretiert, findet sich darin unsere ID. Sehr gut. Nun bin ich zufrieden!

Implementierung

Nun werde ich mich mal hinsetzen und die ganzen Codeschnipsel in ein richtiges Programm verwandeln. Außerdem müssen wir noch die ganzen Flags korrekt parsen. Das Ergebnis stelle ich dann auf github und werde hier den Link einfügen, sobald es fertig ist.

Update 2023-07-29: Das Github Repository ist nun verfügbar. Ich habe beim Implementieren dann doch direkt das cp2110 Modul verwendet, weil der zusätzliche Layer von pyserial nur die Komplexität erhöht.

Außerdem habe ich inzwischen herausgefunden, dass der confirm/EOC Befehl eigentlich nichts zu tun scheint. Alle Kommandos scheinen problemlos zu funktionieren ohne dieses "Bestätigungs-Paket" zu senden. Mal sehen, ob ich dieses Rätsel noch lösen kann.