home
erste Version am 11.01.2018
letzte Änderung am 11.01.2018

PeakTech 4000 unter Linux auslesen


Zu Weihnachten 2017 habe ich mir das Tischmultimeter PeakTech 4000 gegönnt. Zum Lieferumfang gehört ein RS232-zu-USB-Adapter, über den die aktuellen Messwerte an einen Windows-PC übertragen werden können.
Weil ich die Daten aber eher auf einem Linux-PC protokollieren will, braucht es ein entsprechendes Dekodier-Script.

Unter Linux wird kein spezieller Treiber benötigt.
Nach Anschluss des Adapters erscheint sofort ein neues Device:
dede@i5:~> dmesg | tail
[ 1234.828764] usbserial: USB Serial support registered for generic
[ 1234.830391] usbcore: registered new interface driver ftdi_sio
[ 1234.830413] usbserial: USB Serial support registered for FTDI USB Serial Device
[ 1234.830523] ftdi_sio 1-1.4:1.0: FTDI USB Serial Device converter detected
[ 1234.830616] usb 1-1.4: Detected FT232RL
[ 1234.830618] usb 1-1.4: Number of endpoints 2
[ 1234.830620] usb 1-1.4: Endpoint 1 MaxPacketSize 64
[ 1234.830622] usb 1-1.4: Endpoint 2 MaxPacketSize 64
[ 1234.830624] usb 1-1.4: Setting MaxPacketSize 64
[ 1234.830913] usb 1-1.4: FTDI USB Serial Device converter now attached to ttyUSB0

Der Hersteller bietet auf dieser Seite im Abschnitt "Interface Protocol & Changelog" eine Dokumentation des Datenformats an.
Im Abschnitt für das 4000'er-Modell stehen ganz oben die Parameter der RS232-Schnittstelle: 2400 Baud, Parity=Even, Bits=8 / 1, Frame=14 Byte

Nun muss man erstmal am Multimeter den Port dadurch aktivieren, dass man die HOLD/USB-Taste für zwei Sekunden gedrückt hält.
Danach liefert CuteCom mit obigen Protokoll-Parametern sowas hier:

\0xa5(\0x10\0x00\0x05\0x01\0x08\0x00\0x04\0x00\0x00\0x00\0xfb\0x00\0xa5(\0x10\0x00\0x05\0x01\0x08\0x00\0x04\0x00\0x00\0x00\0xfb\0x00\0xa5(\0x10\0x00\0x05\0x01\0x08\0x00\0x04\0x00\0x00\0x00\0xfb\0x00\0xa5(\0x10\0x00\0x05\0x01\0x08\0x00\0x04\0x00\0x00\0x00\0xfb\0x00\0xa5(\0x10\0x00\0x05\0x01\0x08\0x00\0x04\0x00\0x00\0x00\0xfb\0x00\0xa5(\0x10\0x00\0x05\0x01\0x08\0x00\0x04\0x00\0x00\0x00\0xfb\0x00\0xa5(\0x10\0x00\0x05\0x01\0x08\0x00\0x04\0x00\0x00\0x00\0xfb\0x00\0xa5(\0x10\0x00\0x05\0x01\0x08\0x00\0x04\0x00\0x00\0x00\0xfb\0x00\0xa5(\0x10\0x00\0x05\0x01\0x08\0x00\0x04\0x00\0x00\0x00\0xfb\0x00\0xa5(\0x10\0x00\0x05\0x01\0x08\0x00\0x04\0x00\0x00\0x00\0xfb\0x00\0xa5(\0x10\0x00\0x05\0x01\0x08\0x00\0x04\0x00\0x00\0x00\0xfb\0x00

Das Multimeter war dabei auf Widerstandsmessung eingestellt, der Bereich war "Mega Ohm".
Naja...es kommt was. Aber das ist in dieser Form nur Datensalat.

Daher braucht es zuerst mal ein Python-Script, das die Datensätze einzeln ausgibt.
#!/usr/bin/env python
# -*- coding: utf-8 -*-

import serial

ser=serial.Serial('/dev/ttyUSB0', 2400, parity=serial.PARITY_EVEN)
while True:
  s=ser.read(14)
  for c in s:
    print hex(ord(c)),
  print

Das liefert nun das hier (für "unendlich Mega Ohm"):

0xa5 0x28 0x10 0x0 0x5 0x1 0x8 0x0 0x4 0x0 0x0 0x0 0xfb 0x0
0xa5 0x28 0x10 0x0 0x5 0x1 0x8 0x0 0x4 0x0 0x0 0x0 0xfb 0x0
0xa5 0x28 0x10 0x0 0x5 0x1 0x8 0x0 0x4 0x0 0x0 0x0 0xfb 0x0


Und nach Anklemmen eines 100KΩ-Widerstandes (Anzeige: "098.52 kΩ"):

0xa3 0x8 0x10 0x0 0x0 0x9 0x8 0x5 0x2 0x0 0x0 0x0 0xfb 0x0
0xa3 0x8 0x10 0x0 0x0 0x9 0x8 0x5 0x2 0x0 0x0 0x0 0xfb 0x0
0xa3 0x8 0x10 0x0 0x0 0x9 0x8 0x5 0x2 0x0 0x0 0x0 0xfb 0x0


Laut Doku kennzeichnen die ersten vier Byte irgendwelche Optionen, danach kommen fünf Byte "Primary Digits", dann fünf Byte "Secundary Digits".
Das passt jetzt nicht so ganz zu den Daten mit dem 100KΩ. Mitten drin findet sich jedoch die Sequenz: 0 0 9 8   5 2 0 0
Also scheint der Frame-Start nicht ganz zu passen.
Das "upper nibble" von Byte(0) ist laut Doku immer 1010. Also "a". Das passt zu meinen Daten - hieße aber, dass der Frame-Start korrekt dargestellt ist.

Aber vielleicht meinen die mit "primary" und "secundary" nicht Vorkomma- und Nachkommastellen.... So wird es sein. Laut Anleitung (Seite 8) gibt es eine sog. "LED-Sekundäranzeige".
Somit passen die Daten dann doch zur Anzeige am Gerät: 0 9 8 5 2 = 98.52

Und schlauerweise haben die das offensichtlich so gebaut, dass das Start-Byte das Einzige ist, bei dem das oberste Bit gesetzt ist. Damit lässt sich also sehr einfach der Start eines 14-Byte-Blockes erkennen.

Nun geht es darum, die Optionen zu dekodieren.
0: A3 -> xxx.xx kΩ
1: 08 -> Resistance (Ω)
2: 10 -> Manual
3: 00 -> xxx.xx
Ergebnis: 098.52 kΩ  .... Passt!

Bei der oberen Messung (unendlich Ohm) ergäbe sich:
0: A5 -> xx.xxx MΩ
1: 28 -> "Prim.OV" + Resistance (Ω)
2: 10 -> Manual
3: 00 -> xxx.xx
Also wird "OV" wohl overflow meinen!?

Ein bischen problematisch könnte werden, dass ich das Ding bisher nur zur Messung von Gleichspannung, Gleichstrom, Widerstand und als Durchgangsprüfer genutzt habe. Die Bedeutung der Werte in Byte(3) erschließt sich mir noch nicht.

Hier nun erstmal ein Script, das die von mir genutzen Informationen auswerten und darstellen kann:
#!/usr/bin/env python3
# -*- coding: utf-8 -*-

# ###########################################################
# Ein Script zur Anzeige der Messwerte eines PeakTech 4000.
#
#
# Detlev Ahlgrimm, 11.01.2018
#

import serial

# Stellen, Bereich
byte0bit3210= { 0 : ["x.xxxx V", "xx.xxx V", "xxx.xx V", "xxx.x V", "-", "-", "-" ],
1 : ["x.xxxx V", "xx.xxx V", "xxx.xx V", "xxx.x V", "-", "-", "-" ],
2 : ["x.xxxx V", "xx.xxx V", "xxx.xx V", "xxx.x V", "-", "-", "-" ],
3 : ["xx.xxx mV", "xxx.xx mV", "-", "-", "-", "-", "-" ],
4 : ["xx.xxx mV", "xxx.xx mV", "-", "-", "-", "-", "-" ],
5 : ["xx.xxx mV", "xxx.xx mV", "-", "-", "-", "-", "-" ],
6 : ["xx.xxx Hz", "xxx.xx Hz", "x.xxxx kHz", "xx.xxx kHz", "xxx.xx kHz", "x.xxxx MHz", "xx.xxx MHz" ],
7 : ["x.xxxx V", "-", "-", "-", "-", "-", "-" ],
8 : ["xxx.xx Ω", "x.xxxx kΩ", "xx.xxx kΩ", "xxx.xx kΩ", "x.xxxx MΩ", "xx.xxx MΩ", "-" ],
9 : ["xxx.xx Ω", "-", "-", "-", "-", "-", "-" ],
10 : ["xx.xx nF", "xxx.x nF", "x.xxx μF", "xx.xx μF", "xxx.x μF", "xxxx μF", "-" ],
11 : ["xxx.xx μA", "xxxx.x μA", "-", "-", "-", "-", "-" ],
12 : ["xxx.xx μA", "xxxx.x μA", "-", "-", "-", "-", "-" ],
13 : ["xxx.xx μA", "xxxx.x μA", "-", "-", "-", "-", "-" ],
14 : ["xx.xxx mA", "xxx.xx mA", "-", "-", "-", "-", "-" ],
15 : ["xx.xxx mA", "xxx.xx mA", "-", "-", "-", "-", "-" ],
16 : ["xx.xxx mA", "xxx.xx mA", "-", "-", "-", "-", "-" ],
17 : ["x.xxxx A", "xx.xxx A", "-", "-", "-", "-", "-" ],
18 : ["x.xxxx A", "xx.xxx A", "-", "-", "-", "-", "-" ],
19 : ["x.xxxx A", "xx.xxx A", "-", "-", "-", "-", "-" ] }

# Einheit
byte1bit43210=[ "Volt AC (V)", # 00
"Volt DC (V)", # 01
"Volt DC+AC (V)", # 02
"Millivolt DC (mV)", # 03
"Millivolt AC (mV)", # 04
"Millivolt DC+AC (mV)", # 05
"Frequency (Hz)", # 06
"Diode Volt (V)", # 07
"Resistance (Ω)", # 08
"Continuity (Ω)", # 09 (Durchgangsprüfung)
"Capacitance (F)", # 10 0x0a
"Microampere DC (μA)", # 11 0x0b
"Microampere AC (μA)", # 12 0x0c
"Microampere DC+AC (μA)", # 13 0x0d
"Milliampere DC (mA)", # 14 0x0e
"Milliampere AC (mA)", # 15 0x0f
"Milliampere DC+AC (mA)", # 16 0x10
"Ampere DC (A)", # 17 0x11
"Ampere AC (A)", # 18 0x12
"Ampere DC+AC (A)" ] # 19 0x13

# Quelle: http://peaktech.de/detalle-del-producto/kategorie/software/produkt/dmm-tool-basic.1034.html?file=tl_files/downloads/Diverse/PeakTech%20Multimeter-device%20communication%20protocols%202015-12-23.pdf

acdc=[ "AC", "DC", "DC+AC", "DC", "AC", "DC+AC", "", "", "", "", "",
"DC", "AC", "DC+AC", "DC", "AC", "DC+AC", "DC", "AC", "DC+AC"]

# ###########################################################
# Liefert "val" mit Komma gemäß "fmt" als String.
# val="00001", fmt="xx.xxx" -> "00.001"
# val="09850", fmt="xxx.xx" -> "098.50"
# val="00006", fmt="xx.xx" -> "00.06"
def formatValue(val, fmt):
valr=val[::-1]
fmtr=fmt[::-1]
res=""
vc=0
for i in range(len(fmtr)):
if fmtr[i]=="x":
res+=valr[vc]
vc+=1
elif fmtr[i]==".":
res+="."
return(res[::-1])

# ###########################################################
# Liefert den aufbereiteten Inhalt des 14 Byte langen "block"
# als Tupel im Format:
# (Vorzeichen, Wert, Einheit(kurz), Einheit(voll), Flags)
# Also z.B.:
# ('+', '098.50', 'kΩ', 'Resistance (Ω)', '')
# ('+', '0.0001', 'V', 'Volt DC (V)', 'DC')
# ('+', '00.005', 'mV', 'Millivolt DC (mV)', 'DC,HOLD')
def parseBlock(block):
msr=block[1]&0x1f # Einheit (Index in byte1bit43210[])
rng=block[0]&0x0f # Stellen, Bereich (Key in byte0bit3210[])
rng_f=byte0bit3210[msr][rng].split()
if (block[2]&0x20)==0: sgn="+" # Vorzeichen
else: sgn="-"
ovl=(block[1]&0x20)>0 # Überlauf
hld=(block[1]&0x40)>0 # Hold

val=""
for d in block[4:9]: # Primäranzeige
if 0<=(d&0x0f)<=9:
val+=str(d&0x0f)
else:
val+="?"

flgs=""
if acdc[msr]!="": flgs=acdc[msr]
if ovl: flgs+=",OL"
if hld: flgs+=",HOLD"
if flgs.startswith(","): flgs=flgs[1:] # erstes Komma weg

fval=formatValue(val, rng_f[0])
if ovl: fval="---"

return((sgn, fval, rng_f[1], byte1bit43210[msr], flgs))

# ###########################################################
# main()
if __name__=='__main__':
debug=False
ser=serial.Serial('/dev/ttyUSB0', 2400, parity=serial.PARITY_EVEN)
while True:
s=ser.read(1)
if (ord(s)&0xf0)==0xa0:
s+=ser.read(13)
data=parseBlock(s)
if debug:
for c in s:
print(("0"+hex(c)[2:])[-2:], end=' ')
print(" - ", data, " - ", end=' ')
print(data[0] + data[1], data[2], end=' ')
if len(data[4])>0:
print(" (" + data[4] + ")", end='')
print(flush=True)


Die Ausgabe erfolgt zeilenweise und sieht für die unterschiedlichen Einstellungen dann zum Beispiel so aus:

dede@i5:~> ./pt4000_tst.py | tee -a log.txt
+098.49 kΩ
+098.49 kΩ
+098.49 kΩ
+098.49 kΩ
+098.49 kΩ

[...]
+00.002 mV (DC)
+00.001 mV (DC)

[...]
+106.7 μF
+106.7 μF
+106.8 μF


Im Protokoll-Dokument von PeakTech zum Modell 4000 ist mindestens ein Fehler auf Seite 12 in der Tabelle zu Byte(0), Zeile(Capacitance), Spalte(0100).
Da steht "x.xxx μF". Korrekt ist "xxx.x μF".
Und bei allen drei Zeilen für mA brauchte es ebenfalls eine Verschiebung des Punktes.