Unidesa / Cirsa¶
Binary frames, a checksum that's a byte sum wearing a mask, and counters read by numeric id. Cirsa speaks the
same thing, so --protocol cirsa gets the same simulator with a different name tag. Saves money on
translators.
Frame¶
| Field | Size | Meaning |
|---|---|---|
| STX | 1 | Always 0x02. |
| SD | 1 | Destination. Replies use 0xFF, because the readers scan for 02 FF. |
| SO | 1 | Source. Replies use 0x00. |
| NM | 1 | Message number. Replies echo the request's, so clients tracking it stay calm. |
| CMD | 1 | Command. |
| ND | 1 | Number of data bytes. |
| DATA | ND | Payload. |
| CHK | 1 | XOR55 checksum over STX through the end of DATA. |
| EXT | 1 | Always 0x03. |
Total length is always 8 + ND.
XOR55¶
That's the whole algorithm. Add the bytes, XOR with 0x55, keep the low byte. Don't overthink it. The people
who designed it certainly didn't.
Commands¶
| CMD | Name | Request data | Reply data |
|---|---|---|---|
0x0B |
Access | anything | 00 |
0x06 |
Logout | anything | 00 |
0x2E |
Custom selection | (group, counter_id) pairs |
00 |
0x16 |
Read counters | group first last |
group first received (id value[4 LE])* |
| other | Unknown | anything | 01, with the request's CMD echoed back |
Custom selection (0x2E)¶
The payload is a list of (group, counter_id) pairs. The group byte is read and ignored, same as my emails.
An odd trailing byte gets dropped. The counter ids become the new custom selection. An empty list keeps the
old one.
Read counters (0x16)¶
The request has three bytes:
- group:
0xFFreads the custom selection. Anything else (the readers use0x00) reads the default list, whatever0x2Epicked. - first: an offset into that list, not a counter id.
0is the start. The reader works outcnt = counter + first + 1on its own. - last: the last offset to include.
0means "to the end".
The reply carries group, first, the number of counters that follow, then 5 bytes per counter: the id, then
the value as 4 bytes little endian, unsigned. Pages are capped at 32 counters. Nobody wants a 200-byte reply
landing on a 9600 baud line.
Fewer than 3 request bytes gets error 01. The reader didn't say what it wanted, so it doesn't get it.
The default counter list, in order:
1, 2, 17, 3, 31, 32, 33, 34, 154, 4, 7, 8, 11, 12, 24, 35, 84, 97, 160, 161, 162, 163,
130, 131, 132, 133, 6, 80, 10, 81, 73, 114, 115, 116, 117, 122, 123, 124, 125
A real exchange¶
Generated by running the simulator with counter 1 at 123456 and counter 2 at 120000:
access -> 02 00 00 01 0B 00 5B 03
<- 02 FF 00 01 0B 01 00 5B 03
select [1, 2] -> 02 00 00 02 2E 04 00 01 00 02 6C 03
<- 02 FF 00 02 2E 01 00 67 03
read 0xFF 0 0 -> 02 00 00 03 16 03 FF 00 00 48 03
<- 02 FF 00 03 16 0D FF 00 02 01 40 E2 01 00 02 C0 D4 01 00 B6 03
Read the last one right to left and it says: group FF, first 0, 2 counters, counter 01 is
0x0001E240 (123456), counter 02 is 0x0001D4C0 (120000). Little endian. Of course it's little endian.
Streams and bad frames¶
TCP doesn't care where your frames end. One read can hold half a frame, or three frames glued together. The CLI and the pytest fixture both buffer the stream and cut it into whole frames before the simulator sees them. Junk in front of an STX gets thrown away.
A frame with bad delimiters or a bad checksum gets logged and skipped. No reply at all. The client gets to sit in silence and think about what it did, which is what a real machine does with garbage.
The simulator's own handle() is stricter: it takes exactly one frame and raises ValueError on a bad one.
Use that directly in unit tests when you want the error.
Building frames in Python¶
from morpheus_vlt.core.framing import UnidesaMessage, build_unidesa_reply, xor55_checksum
frame = build_unidesa_reply(sd=0x00, so=0x00, nm=0x01, cmd=0x16, data=bytes([0xFF, 0x00, 0x00]))
msg = UnidesaMessage.parse(frame)
msg.validate() # raises ValueError if the delimiters or checksum are off
build_unidesa_reply builds any frame, request or reply. The name is a little optimistic.