Das HESP-Protokoll¶
HESP ist das binäre Anfrage/Antwort-Protokoll auf dem 19200-Baud-Bus (X7). Name und Grundstruktur des Telegramms sind in einem FHEM-Forum-Thread (Topic 96437) zur verwandten Hermes-/P-Serie vorbeschrieben; die hier dokumentierte Frame-Struktur und das Prüfsummenmodell wurden aus eigenen Busmitschnitten bestimmt.
Physische Ebene¶
- 19200 Baud, 8N1, RS485 halbduplex, differentielles A/B-Paar auf X7-4/X7-3.
- Master ist der STM32 des PTC-Moduls; er pollt den LPC1758 etwa alle 5 Sekunden und re-asserted dabei den aktuellen Panel-Zustand (Stufe/Modus/Sollwerte).
Frame-Format¶
Jeder Frame:
<h0> <h1> <h2> <id_lo> <id_hi> 00 00 <len> <data …> <crc_lo> <crc_hi>
| Feld | Bytes | Bedeutung |
|---|---|---|
h0 h1 h2 |
3 | Adresskopf, 24-bit little-endian: Typ, Quell- und Zieladresse (siehe unten) |
id_lo id_hi |
2 | Datenpunkt-ID, 16-bit little-endian |
00 00 |
2 | Reserviert / Sub-Selektor |
len |
1 | Nibble-Anzahl der Daten (0x08=4 B, 0x20=16 B, 0xe0=224 B) |
data |
n | Nutzdaten (float32 LE, int, Array …) |
crc |
2 | Prüfsumme (little-endian) |
Der Adresskopf¶
Die ersten drei Bytes bilden vermutlich ein 24-bit-Wort H = h0 | h1 << 8 | h2 << 16 mit
drei Feldern:
| Bits | Feld | Breite |
|---|---|---|
| 0–3 | Typ (siehe unten) | 4 bit |
| 4–13 | Quelladresse | 10 bit |
| 14–23 | Zieladresse | 10 bit |
Diese Deutung würde die bisher als „fester Kopf-Rest" bzw. „Node-abhängig" beschriebenen Bytes erklären: Anfrage und Antwort tauschen Quell- und Zieladresse.
| Kopf | Typ | Quelle → Ziel |
|---|---|---|
10 80 00 |
QUERY | 1 → 2 |
22 40 00 |
RESPONSE | 2 → 1 |
11 80 00 |
SET | 1 → 2 |
23 40 00 |
CONFIRM | 2 → 1 |
10 80 07 |
QUERY | 1 → 30 |
10 00 0a |
QUERY | 1 → 40 |
Adresse 1 ist der pollende Master. Die Zerlegung ist mit den Mitschnitten einer zweiten Anlage mit weiteren Busteilnehmern (u. a. Adressen 10–15, 30, 40) konsistent: Jede Antwort trägt dort die vertauschten Adressen der vorausgehenden Anfrage. Eine Bestätigung durch Herstellerunterlagen liegt nicht vor.
Der Typ-Nibble¶
Das Low-Nibble des ersten Bytes kodiert die Telegramm-Art — exakt wie im Forum dokumentiert und am Draht bestätigt:
| Nibble | Typ | Header-Beispiel |
|---|---|---|
0 |
QUERY (Anfrage) | 10 80 00 … |
1 |
SET (Schreiben) | 11 80 00 … |
2 |
RESPONSE (Antwort) | 22 40 00 … |
3 |
CONFIRM (Schreibbestätigung) | 23 40 00 … |
Beispiele¶
QUERY 10 80 00 60 01 00 00 04 01 00 49 e1 # frage DP 0x0160
RESPONSE 22 40 00 d2 00 00 00 20 …16 B… 5a cb # Antwort mit 16 B Daten
SET 11 80 00 27 02 00 00 08 00 00 98 41 8a cd # setze DP 0x0227 = 19.0 (float32)
CONFIRM 23 40 00 27 02 00 00 00 39 a1 # Schreiben bestätigt
Nutzdaten sind IEEE-754 float32 little-endian für Temperaturen und Sollwerte, sonst Integer (RPM, Zähler, Flags) oder Arrays (Kennlinien).
Prüfsumme¶
Die letzten zwei Bytes jedes Frames bilden eine CRC-16 über alle vorhergehenden Bytes (Kopf bis einschließlich Nutzdaten), little-endian abgelegt:
| Parameter | Wert |
|---|---|
| Polynom | 0x8048 in reflektierter Darstellung (LSB zuerst; normal: 0x1201) |
| Startwert | 0xFFFF |
| Final-XOR | keines |
| Schiebeschritte je Byte | 16 |
Die Besonderheit ist die Zahl der Schiebeschritte: Nach dem Einrechnen eines Bytes wird
das Register 16-mal statt 8-mal geschoben. Das ist gleichbedeutend damit, dass jedes
Datenbyte als 16-bit-Wort mit High-Byte 0x00 in eine gewöhnliche reflektierte CRC-16
eingeht. Diese Eigenheit erklärt, warum eine Suche über Standard-Parametrierungen (alle
Polynome, Reflect-Varianten, Byte-Reihenfolgen) keine Übereinstimmung liefert.
def hesp_crc(data: bytes) -> int:
crc = 0xFFFF
for b in data:
crc ^= b
for _ in range(16):
crc = (crc >> 1) ^ 0x8048 if crc & 1 else crc >> 1
return crc # im Frame: crc & 0xFF, dann crc >> 8
Geprüft an allen Beispielen dieser Seite sowie an rund 6000 Frames einer zweiten Anlage (alle vier Telegrammtypen, Frame-Längen 10–134 Byte) ohne Abweichung. Die Parameter sind aus Mitschnitten ermittelt; eine Bestätigung durch Herstellerunterlagen liegt nicht vor.
Zusammenhang mit dem affinen GF(2)-Modell¶
Eine frühere Fassung dieser Seite beschrieb die Prüfsumme empirisch als affine Abbildung
über GF(2) mit einer Tabelle von Beitragsvektoren V[e]. Die Linearität ist eine direkte
Folge der CRC. Die Tabelle war jedoch nicht eindeutig bestimmt: Bits, die im
Trainingsmaterial stets gemeinsam bzw. konstant gesetzt waren, lassen sich nicht von der
längenabhängigen Konstante trennen. Tatsächlich ist die Konstante nicht null, sondern
die CRC eines Null-Frames gleicher Länge (z. B. 27d8 für 10 Byte, edc9 für 12 Byte).
Die Tabelle lieferte daher für die beobachteten Bitmuster korrekte Ergebnisse, nicht aber
für beliebige Frames. Die Beitragsvektoren folgen aus dem Algorithmus als
V[e] = crc(Frame mit nur Bit e gesetzt) ⊕ crc(Null-Frame) und hängen nur von e
ab, nicht von der Frame-Länge.
Tabelle der Beitragsvektoren V[e] (aus dem Algorithmus, e = 0–95)
Bitposition vom Frame-Ende: Für Byte i (0-basiert), Bit b (0 = LSB) eines Frames mit
N Bytes ohne CRC gilt e = (N − 1 − i) · 8 + b. Damit ist
CRC = K(N) ⊕ (⊕ V[e] über alle gesetzten Bits) mit K(N) = CRC eines Null-Frames der Länge N.
e |
V[e] |
e |
V[e] |
e |
V[e] |
e |
V[e] |
|---|---|---|---|---|---|---|---|
| 0 | d1f4 |
1 | a379 |
2 | 4663 |
3 | 8cc6 |
| 4 | 191d |
5 | 323a |
6 | 6474 |
7 | c8e8 |
| 8 | 80fc |
9 | 0169 |
10 | 02d2 |
11 | 05a4 |
| 12 | 0b48 |
13 | 1690 |
14 | 2d20 |
15 | 5a40 |
| 16 | cd56 |
17 | 9a3d |
18 | 34eb |
19 | 69d6 |
| 20 | d3ac |
21 | a7c9 |
22 | 4f03 |
23 | 9e06 |
| 24 | 0534 |
25 | 0a68 |
26 | 14d0 |
27 | 29a0 |
| 28 | 5340 |
29 | a680 |
30 | 4d91 |
31 | 9b22 |
| 32 | b823 |
33 | 70d7 |
34 | e1ae |
35 | c3cd |
| 36 | 870b |
37 | 0e87 |
38 | 1d0e |
39 | 3a1c |
| 40 | 78a8 |
41 | f150 |
42 | e231 |
43 | c4f3 |
| 44 | 8977 |
45 | 127f |
46 | 24fe |
47 | 49fc |
| 48 | 8e67 |
49 | 1c5f |
50 | 38be |
51 | 717c |
| 52 | e2f8 |
53 | c561 |
54 | 8a53 |
55 | 1437 |
| 56 | 0c91 |
57 | 1922 |
58 | 3244 |
59 | 6488 |
| 60 | c910 |
61 | 92b1 |
62 | 25f3 |
63 | 4be6 |
| 64 | cc6b |
65 | 9847 |
66 | 301f |
67 | 603e |
| 68 | c07c |
69 | 8069 |
70 | 0043 |
71 | 0086 |
| 72 | a403 |
73 | 4897 |
74 | 912e |
75 | 22cd |
| 76 | 459a |
77 | 8b34 |
78 | 16f9 |
79 | 2df2 |
| 80 | 96f1 |
81 | 2d73 |
82 | 5ae6 |
83 | b5cc |
| 84 | 6b09 |
85 | d612 |
86 | acb5 |
87 | 59fb |
| 88 | a03b |
89 | 40e7 |
90 | 81ce |
91 | 030d |
| 92 | 061a |
93 | 0c34 |
94 | 1868 |
95 | 30d0 |
N |
8 | 9 | 10 | 11 | 12 | 13 | 14 |
|---|---|---|---|---|---|---|---|
K(N) |
81dd |
bfd9 |
27d8 |
ee21 |
edc9 |
4cfb |
f5be |
Rechenbeispiel¶
Anfrage-Frame für Datenpunkt 0x0160 (siehe Beispiele oben):
>>> hex(hesp_crc(bytes.fromhex("10 80 00 60 01 00 00 04 01 00")))
'0xe149'
→ crc_lo = 49, crc_hi = e1; der vollständige Frame auf dem Bus lautet also
10 80 00 60 01 00 00 04 01 00 49 e1
und entspricht exakt dem Mitschnitt.
Meta-Datenpunkte (Selbstauskunft)¶
Der Bus beschreibt sich teilweise selbst:
| DP | Inhalt |
|---|---|
0x0001 |
Klartextname des Nodes (z. B. Knoten 30 = „PTC-Modul") |
0x0033 |
Liste aller Parameter-DP-Nummern (Verzeichnis) |
0x0034 |
parallele Typcode-Liste dazu |
0x0038 |
Zugriffslevel-Maske je DP (siehe unten) |
0x0039 |
Einheiten-Code je DP (8=°C, 5=Sekunden, 11=String …) |
0x0130 |
Fehlerstatus als Klartext („Kein Fehler") |
Knotennummern
Die Knotennummer ist dem Adresskopf zu entnehmen (siehe oben), nicht einem
einzelnen Kopf-Byte: Die Antworten des PTC-Moduls beginnen mit e2 41 00, was
nach der 24-bit-Zerlegung RESPONSE von Knoten 30 an Knoten 1 bedeutet. Eine
frühere Fassung dieser Seite las das mittlere Byte als Knoten-ID („Node 0x41“).
Zugriffsmaske 0x0038¶
Ein Byte pro DP kodiert die Schreibrechte über vier Zugriffsebenen (je 2 Bit R/W):
| Maske | Bedeutung | trifft |
|---|---|---|
0x55 |
read-only | alle Messwerte, Zähler |
0xFF |
überall schreibbar | die vom Panel gesetzten User-Werte (Raumsoll etc.) |
0xD5 |
nur oberste Ebene schreibbar | der Fachmann-Parameterblock |
0xC0 |
nur oberste Ebene sichtbar | Werks-Kennlinien, Lüfter-Arrays |
Die Maske bestätigt unabhängig, welche DPs das (PIN-geschützte) Fachmann-Menü ausmachen — nützlich, um beim Parametrieren nicht blind in geschützte Register zu schreiben.