Test Vectors
This page is generated. Do not edit it by hand: run
Every value below is produced by executing the real implementation, so it
cannot disagree with the code. A previous hand-written version specified the
wrong CRC algorithm and decoded 0x46E5B000 as 14710.0 (it is 29400.0);
nothing caught either, because nothing ran it.
1. CRC-16/CCITT
| Polynomial | 0x1021 |
| Initial value | 0xFFFF |
| Reflect in / out | No / No |
| Final XOR | 0xFFFF |
| Covered bytes | frame[1:-2] — after the start byte, before the CRC |
| CRC byte order | Big-endian |
There is one convention, for reads and writes alike. A second helper without the final XOR exists in the source as a building block; no frame the pump accepts uses it.
The frames below were captured from a real pump, which accepted them. Reproducing their CRC is evidence about the protocol, not merely internal consistency.
| Frame | CRC over frame[1:-2] |
What it is |
|---|---|---|
27 07 E7 F8 02 03 94 95 96 EB 47 |
0xEB47 |
Class 2 GET of unit family/type/version |
27 07 E7 F8 0A 03 56 00 06 C5 5A |
0xC55A |
Class 10 GET of Object 86 Sub 6 |
27 05 E7 F8 05 C1 4B C3 82 |
0xC382 |
INFO query, Class 5 item 0x4B |
27 05 E7 F8 0B C1 0F D0 C3 |
0xD0C3 |
INFO query, Class 11 item 0x0F |
27 05 E7 F8 03 81 06 E5 87 |
0xE587 |
Class 3 START |
27 05 E7 F8 03 81 05 D5 E4 |
0xD5E4 |
Class 3 STOP |
assert calc_crc16(bytes.fromhex("07e7f80203949596")) == 0xEB47
assert calc_crc16(bytes.fromhex("07e7f80a03560006")) == 0xC55A
assert calc_crc16(bytes.fromhex("05e7f805c14b")) == 0xC382
assert calc_crc16(bytes.fromhex("05e7f80bc10f")) == 0xD0C3
assert calc_crc16(bytes.fromhex("05e7f8038106")) == 0xE587
assert calc_crc16(bytes.fromhex("05e7f8038105")) == 0xD5E4
2. IEEE 754 Big-Endian Float
| Value | Encoded | Round-trips |
|---|---|---|
1.5 — The canonical IEEE 754 example |
3F C0 00 00 |
1.5 |
14710 — 1.5 m of head, in Pascals |
46 65 D8 00 |
14710 |
1650 — The pump's minimum speed, RPM |
44 CE 40 00 |
1650 |
3671 — The pump's maximum speed, RPM |
45 65 70 00 |
3671 |
0 — Zero |
00 00 00 00 |
0 |
-5.5 — Negative |
C0 B0 00 00 |
-5.5 |
38.9 — A temperature setpoint, degrees C |
42 1B 99 9A |
38.9 |
0x45657000is 3671.0, the pump's maximum speed — not an inert placeholder. It appears verbatim in the pump's own limits block, and sending it as a setpoint "suffix" writes run-at-full-speed over whatever the mode actually held.
3. Uint16 Big-Endian
| Value | Encoded |
|---|---|
1000 (0x03E8) |
03 E8 |
22016 (0x5600) — Control SubID |
56 00 |
65535 (0xFFFF) — Maximum |
FF FF |
4. Uint32 Big-Endian
| Value | Encoded |
|---|---|
5701701 (0x00570045) — Motor state register |
00 57 00 45 |
1234567890 (0x499602D2) |
49 96 02 D2 |
380392 (0x0005CDE8) — Operating time, seconds |
00 05 CD E8 |
5. Frame Building
The opening packets are built from their APDUs and must reproduce the captured constants byte for byte:
| APDU | Frame | Matches capture |
|---|---|---|
02 03 94 95 96 |
27 07 E7 F8 02 03 94 95 96 EB 47 |
yes |
0A 03 56 00 06 |
27 07 E7 F8 0A 03 56 00 06 C5 5A |
yes |
05 C1 4B |
27 05 E7 F8 05 C1 4B C3 82 |
yes |
0B C1 0F |
27 05 E7 F8 0B C1 0F D0 C3 |
yes |
Class 10 object read
| Object / Sub | Frame |
|---|---|
| 86 / 6 | 27 07 E7 F8 0A 03 56 00 06 C5 5A |
| 86 / 7 | 27 07 E7 F8 0A 03 56 00 07 D5 7B |
| 86 / 13 | 27 07 E7 F8 0A 03 56 00 0D 74 31 |
| 84 / 1 | 27 07 E7 F8 0A 03 54 00 01 DB DD |
| 91 / 421 | 27 07 E7 F8 0A 03 5B 01 A5 31 B3 |
| 91 / 430 | 27 07 E7 F8 0A 03 5B 01 AE 80 D8 |
| 93 / 1 | 27 07 E7 F8 0A 03 5D 00 01 45 4C |
The Object/Sub pair and the 24-bit register form are two spellings of one
address, not two addressing modes — build_class10_object_read(0x57, 0x0045)
and build_class10_read(0x570045) emit identical bytes.
Control frames
| Purpose | Frame |
|---|---|
| Start (Class 3) | 27 05 E7 F8 03 81 06 E5 87 |
| Stop (Class 3) | 27 05 E7 F8 03 81 05 D5 E4 |
| Mode -> Constant Speed | 27 14 E7 F8 0A 90 56 00 0A 01 2F 01 00 00 07 00 06 02 7F FF FF FF 0C EC |
| Setpoint 1650 RPM | 27 14 E7 F8 0A 90 56 00 06 01 2F 01 00 00 07 00 00 02 44 CE 40 00 47 63 |
7F FF FF FF is NaN, meaning "keep the stored setpoint". A mode change sends
it so that the fused control object does not overwrite the target mode's
value.
6. MTU Chunking
Every frame is written in 20-byte chunks. The count is not fixed at two — a two-chunk splitter looks correct on every control packet and silently truncates the schedule write.
| Frame length | Chunks |
|---|---|
| 9 bytes | 9 |
| 24 bytes | 20 + 4 |
| 27 bytes | 20 + 7 |
| 43 bytes | 20 + 20 + 3 |
| 59 bytes | 20 + 20 + 19 |
7. Frame Parsing
Measured replies, parsed by the real parser:
| Reply | Class | Byte 5 | Payload length |
|---|---|---|---|
2412f8e70a0e00012f0100000700001b39678ac34fbc |
10 | 0x0E |
14 |
2415f8e70a110000da0100000a02050005010100000000dd89 |
10 | 0x11 |
17 |
2417f8e70a130001420100000c07ea08041019043100020101e229 |
10 | 0x13 |
19 |
Byte 5 of a response is a length, not an operation specifier. Its top two
bits are always 00 and its low six bits are the payload length, so both of
these hold for every measured reply:
Reading it as a type code means filtering replies by size — which is what the
old {0x30, 0x2B, 0x14, 0x09} "register-read opspec" filter really did.
8. Unit Conversions
| Quantity | Wire | API | Conversion |
|---|---|---|---|
| Pressure | Pascals | metres | ÷ 9806.65 |
| Speed | RPM | RPM | none |
| Flow setpoint | SI m³/s | m³/h | ÷ 3600 on the way out |
| Flow telemetry | m³/h | m³/h | none |
| Temperature | °C | °C | none |
The two flow rows differ, and both are correct. A setpoint written in m³/h reaches the pump 3600× too large, is rejected as out of range, and leaves the stored value untouched — which makes the register look frozen rather than wrong.
| Metres | Pascals | Encoded |
|---|---|---|
| 1 | 9806.65 | 46 19 3A 9A |
| 1.5 | 14709.97 | 46 65 D7 E6 |
| 3 | 29419.95 | 46 E5 D7 E6 |
| 10 | 98066.50 | 47 BF 89 40 |
| m³/h | m³/s | Encoded |
|---|---|---|
| 0.5 | 0.00013889 | 39 11 A2 B4 |
| 1 | 0.00027778 | 39 91 A2 B4 |
| 2.5 | 0.00069444 | 3A 36 0B 61 |