
Genisys: The Rail Signalling Protocol Nobody Documents
If you go looking for documentation on the Genisys protocol, you will not find much. There is no RFC, no IANA-assigned port, no Wikipedia article, no Stack Overflow tag. What you will find is one reverse-engineered network parser, a Wireshark dissector patch that was proposed and never merged, and a handful of obscure repositories. Yet Genisys quietly moves signalling data across a lot of North American railroad, so it is worth writing down how it actually works. This is a protocol explainer built from the Union Switch & Signal vendor manuals — no products, no networks, just the protocol.
What Genisys Is
Genisys is a binary, byte-oriented, master/slave polling protocol created by Union Switch & Signal (the lineage now sits with Alstom / KB Signaling). It was designed for asynchronous serial links and today is just as often tunneled over UDP or TCP. Conceptually it is close to Modbus, with one difference: Modbus polls full frames, while Genisys is event-based — a slave answers a poll with data only when something has changed. There is exactly one master on a channel; it initiates every conversation, and a slave may never speak unless addressed first. If a slave detects an error in a received message, it says nothing — the error is implied by the absence of a reply, and the master retries.
The Frame
Every Genisys message has the same skeleton: a header/control byte, a station address, optional data, a two-byte CRC-16, and a terminator.
| Field | Bytes | Notes |
|---|---|---|
| Header / control | 1 | Function code, always 0xF1–0xFE |
| Station address | 1 | 0x01–0xFF; 0x00 = broadcast |
| Data | 0..n | Optional; (address, value) byte pairs |
| CRC-16 | 2 | Low byte first, then high byte |
| Terminator | 1 | Always 0xF6 (end-of-text) |
The trick that makes the protocol parseable is that every function code has a high nibble of 0xF. The reserved range 0xF0–0xFF is set aside for framing, so a header or terminator can never be confused with data. That guarantee is only possible because of byte-stuffing. Data, when present, is always sent as pairs: a byte address (typically 0x00–0x1F) followed by the byte value.
Function Codes
Direction is baked into the code: 0xF1–0xF3 are slave-to-master, 0xF9–0xFE are master-to-slave.
| Code | Message | Direction |
|---|---|---|
| 0xF0 | Escape prefix | — |
| 0xF1 | Acknowledge (no data to return) | slave → master |
| 0xF2 | Indication data | slave → master |
| 0xF3 | Control checkback | slave → master |
| 0xF6 | Message terminator | — |
| 0xF9 | Common (broadcast) control | master → slave |
| 0xFA | Acknowledge & poll | master → slave |
| 0xFB | Poll | master → slave |
| 0xFC | Control data | master → slave |
| 0xFD | Recall (send full state) | master → slave |
| 0xFE | Execute controls | master → slave |
0xF4, 0xF5, 0xF7, and 0xF8 are reserved. 0xFF is illegal by design — it is a value noisy lines produce all by themselves, so the protocol refuses to assign it meaning.
Byte-Stuffing: The F0 Escape
Real data can legitimately land anywhere in 0x00–0xFF, including the reserved framing range. Genisys solves this the same way HDLC and PPP do: any body byte in 0xF0–0xFF is sent as two bytes — the escape character 0xF0 followed by the low nibble of the original. On receive, when you see 0xF0 you OR it with the next byte to recover the value.
Original byte On the wire
0x3C 0x3C (below 0xF0, sent as-is)
0xF6 0xF0 0x06 (reserved, escaped)
0xFB 0xF0 0x0B
0xFF 0xF0 0x0F
Only two bytes in the whole frame are exempt: the header and the terminator. Everything between them — station address, data, and both CRC bytes — is subject to the escape.
CRC-16
Data security is a two-byte CRC-16 using the generator polynomial x^16 + x^15 + x^2 + 1 — better known as CRC-16/ARC (reflected 0xA001, init 0x0000). Two subtleties account for most broken implementations:
- The CRC is computed before stuffing. It covers header, address, and data as raw bytes and excludes only the terminator. On receive you must de-stuff first, then check.
- It is transmitted little-endian — low byte, then high byte — and either byte may itself need escaping if it lands in the reserved range.
Not every message carries a CRC. The slave acknowledge (0xF1) and the non-secure poll are sent without one; a secure poll adds the CRC for the cases that matter.
# CRC-16/ARC over the de-stuffed header+address+data
def crc16_arc(data: bytes) -> int:
crc = 0x0000
for b in data:
crc ^= b
for _ in range(8):
crc = (crc >> 1) ^ 0xA001 if crc & 1 else crc >> 1
return crc # transmit as: crc & 0xFF, then (crc >> 8) & 0xFF
The Checkback Control Sequence
This is the most interesting part of Genisys, and the part that earns it a place in signalling. Sending a control is not a single message — it is a three-step handshake designed so a garbled command can never be acted on.
- Control (0xFC, master → slave): the master sends the control data.
- Checkback (0xF3, slave → master): the slave echoes back every control byte exactly as received, and holds it, without acting.
- Execute (0xFE, master → slave): only if the checkback matched does the master issue execute, which must immediately follow the checkback. The slave then acts on the held control.
If the checkback does not match, or execute does not arrive next, the held control is discarded and the sequence starts over. The effect is a read-back-before-you-act interlock: the field proves it understood the command before the office commits it. A single dropped or corrupted frame mid-handshake does not produce a wrong output — it produces no output, and the sequence retries. Fail-safe by construction.
Polling, Recall, and the E0 Byte
When a master driver starts, it sends a recall (0xFD) to each slave, asking for its entire indication database; only once a slave returns a complete response is it marked active. Recall is also how the master re-syncs after an outage. From there the master polls round-robin, skipping failed stations during normal polling and probing them with a recall at the end of each cycle. Configuration rides in the 0xE0 control byte: bit 0 database complete, bit 1 enable checkback, bit 2 respond to secure poll only, bit 3 enable common control.
Where Implementations Go Wrong
Genisys is simple, but the corners are sharp, and because the public reference material is so thin, independent implementations quietly disagree. The recurring mistakes: forgetting to stuff the CRC bytes (the escape applies to the checksum too, and because those bytes are effectively random it happens often); computing the CRC after stuffing instead of before; getting the byte order backwards (it is low byte first on the wire, and more than one public parser gets this wrong); assuming every frame has a CRC (acknowledge and non-secure poll do not); and treating any 0xF6 as end-of-frame without accounting for stuffing.
The Tooling Landscape
If you need to look at Genisys on the wire, the options are sparse. CISA publishes a Zeek/Spicy parser (icsnpp-genisys) as part of its ICS Network Protocol Parsers family — useful for framing and presence, though it was reverse-engineered and worth validating against known-good frames first. The original public knowledge traces back to an old Wireshark dissector patch that never landed. Beyond that, it is vendor manuals and a few personal projects.
That scarcity is the reason for this post. Genisys is a 1990s serial protocol that got wrapped in UDP and kept running, the way a surprising amount of railroad infrastructure does. It is not complicated — a header, an address, some pairs, a checksum, a terminator, and one genuinely clever handshake — but it is nearly undocumented in public, and that gap is worth closing one page at a time.
Simple protocol. Sharp corners. Still moving trains.