Reverse Engineering a Queue Display Protocol: BCD4 Encoding and Carry Arithmetic
An engineer reverse-engineered a proprietary queue display protocol without documentation. The protocol uses BCD4 encoding, where each data bit occupies a full byte for noise immunity on long RS-232 lines at 1200 baud. The packet structure is 33 bytes with a marker, number digits, window number, and checksum fields with carry.
BCD4: Encoding for Reliable Transmission
The protocol employs a hardware method for noise protection: each bit of a number (0–15) is encoded as a byte. 0x7F denotes 1 (seven ones for a clear pulse), 0x00 denotes 0. This stretches a 4-bit value to 4 bytes.
Encoding examples:
| Number | Bits | BCD4 Bytes |
|--------|--------|------------------|
| 0 | 0000 | 00 00 00 00 |
| 1 | 0001 | 7F 00 00 00 |
| 2 | 0010 | 00 7F 00 00 |
| 5 | 0101 | 7F 00 7F 00 |
| 9 | 1001 | 7F 00 00 7F |
| 12 | 1100 | 00 00 7F 7F |
Implementation in Python:
def _bcd4(value: int) -> bytes:
return bytes([0x7F if value & (1 << i) else 0x00 for i in range(4)])
print(_bcd4(5).hex(' ')) # 7f 00 7f 00
print(_bcd4(9).hex(' ')) # 7f 00 00 7f
The 1200 baud rate ensures reliability at the cost of throughput.
Packet Structure for Number Display
A 33-byte packet displays a number on a window:
- Byte 0:
0x55— start marker. - Bytes 1–4: mode header.
- Bytes 5–8: BCD4(units digit).
- Bytes 9–12: BCD4(tens digit).
- Bytes 13–16: BCD4(hundreds digit).
- Bytes 17–20: BCD4(window units digit).
- Bytes 21–24: BCD4(window tens digit).
- Bytes 25–28: BCD4(c1) — lower checksum part.
- Bytes 29–32: BCD4(c2) — upper part with carry.
Example for number 487 on window 19:
55 00 7F 00 00 # header
7F 7F 7F 00 # BCD4(7)
00 00 00 7F # BCD4(8)
00 00 7F 00 # BCD4(4)
7F 00 00 7F # BCD4(9)
7F 00 00 00 # BCD4(1)
[checksum fields]
Checksum Calculation with Carry
Checksum fields c1 and c2 form an 8-bit value with carry mechanics:
- Compute raw_c1 as a linear combination of packet fields.
- If raw_c1 >= 16: carry = 1, c1 = raw_c1 - 16.
- Else: carry = 0, c1 = raw_c1.
- c2 is computed with carry from other fields.
Carry occurs with numbers like 80–90 depending on the window. AI (Claude) identified the pattern from 30 packet examples after refining the hypothesis: two 4-bit nibbles with carry as in arithmetic.
For "all windows" mode (25 bytes), there is no carry, and the formula is simpler.
Window programming mode uses two independent carry chains with multiplication and constants:
- Chain A: raw_A from W_lo, carry_A = raw_A >> 4, field_A1 = raw_A & 0xF, field_A2 from W_hi and carry_A.
- Chain B: similarly for another linear function.
Assembling and Sending the Packet
Packet assembly function:
def build_window_packet(window: int, number: int) -> bytes:
d1 = number % 10
d2 = (number // 10) % 10
d3 = (number // 100) % 10
w_lo = window % 10
w_hi = window // 10
c1, c2 = _checksum(d1, d2, d3, w_lo, w_hi)
packet = bytearray(b'\x55\x00\x7F\x00\x00')
packet += _bcd4(d1) + _bcd4(d2) + _bcd4(d3)
packet += _bcd4(w_lo) + _bcd4(w_hi)
packet += _bcd4(c1) + _bcd4(c2)
assert len(packet) == 33
return bytes(packet)
The packet is sent to a COM port.
Integration into a Web System
A Python server accepts HTTP POST:
POST / {"window": 5, "ticket": "487"}
It parses JSON, builds the packet, and writes to the port. A heartbeat mechanism restores connection:
- 60-second interval when OK.
- 10 seconds on error.
def _heartbeat_loop():
while True:
ok = _try_register(...)
was = _server_state['connected']
_server_state['connected'] = ok
if not was and ok:
print("[OK] Connection restored")
elif was and not ok:
print("[WARN] Connection lost")
time.sleep(60 if ok else 10)
Key Takeaways
- BCD4 encoding stretches 1 bit to a byte for noise immunity at 1200 baud.
- The checksum uses carry between nibbles c1/c2.
- Reverse engineering with AI is effective with precise hypotheses and packet examples.
- The server enables seamless integration of legacy equipment with the web.
- Protocol: 33 bytes, marker 0x55, digits + window + checksum.
— Editorial Team
No comments yet.