diff --git a/src/assets/diagrams/packet-control-empty.svg b/src/assets/diagrams/packet-control-empty.svg new file mode 100644 index 0000000..243cad4 --- /dev/null +++ b/src/assets/diagrams/packet-control-empty.svg @@ -0,0 +1,49 @@ + diff --git a/src/assets/diagrams/packet-controller-ack-new-session.svg b/src/assets/diagrams/packet-controller-ack-new-session.svg new file mode 100644 index 0000000..63cf770 --- /dev/null +++ b/src/assets/diagrams/packet-controller-ack-new-session.svg @@ -0,0 +1,66 @@ + diff --git a/src/assets/diagrams/packet-omnilink-message.svg b/src/assets/diagrams/packet-omnilink-message.svg new file mode 100644 index 0000000..54d1dba --- /dev/null +++ b/src/assets/diagrams/packet-omnilink-message.svg @@ -0,0 +1,77 @@ + diff --git a/src/assets/diagrams/packet-secure-session.svg b/src/assets/diagrams/packet-secure-session.svg new file mode 100644 index 0000000..89fbf9e --- /dev/null +++ b/src/assets/diagrams/packet-secure-session.svg @@ -0,0 +1,81 @@ + diff --git a/src/content/docs/reference/protocol.mdx b/src/content/docs/reference/protocol.mdx index bd9c1eb..767e8be 100644 --- a/src/content/docs/reference/protocol.mdx +++ b/src/content/docs/reference/protocol.mdx @@ -5,6 +5,10 @@ description: Byte-level handshake, packet layouts, key derivation, and steady-st import HandshakeSequence from '../../../assets/diagrams/handshake-sequence.svg?raw'; import PacketStructure from '../../../assets/diagrams/packet-structure.svg?raw'; +import PacketEmpty from '../../../assets/diagrams/packet-control-empty.svg?raw'; +import PacketAckNew from '../../../assets/diagrams/packet-controller-ack-new-session.svg?raw'; +import PacketSecureSession from '../../../assets/diagrams/packet-secure-session.svg?raw'; +import PacketOmniLink from '../../../assets/diagrams/packet-omnilink-message.svg?raw';
@@ -81,14 +85,23 @@ UDP / 1808 TCP). All offsets are **into the packet payload**, i.e., after the 4-byte outer header (`[seq_hi][seq_lo][type][reserved=0]`). -### `ClientRequestNewSession` (type 0x01) +### `NoMessage` (type 0x00) and `ClientRequestNewSession` (type 0x01) + + + +Both have empty payloads. `NoMessage` is the protocol's keepalive — sent +when the client wants to confirm the TCP connection is alive without doing +anything. `ClientRequestNewSession` is step 1 of the handshake. Wire = 4 +bytes total either way (just the header). | Offset | Size | Field | Notes | |-------:|-----:|-------|-------| -| — | 0 | (no payload) | `clsOmniLinkPacket.Data == null` (line 1283/1688). Wire = 4 bytes total: `00 01 01 00`. | +| — | 0 | (no payload) | `clsOmniLinkPacket.Data == null` (line 1283/1688). | ### `ControllerAckNewSession` (type 0x02) + + Payload size **7 bytes** (TCP reader hardcodes `tcpReadBytes(array, 7)` on this type, line 1714). @@ -100,7 +113,19 @@ type, line 1714). Total wire packet: 4-byte header + 7-byte payload = 11 bytes. -### `ClientRequestSecureSession` (type 0x03) +### `ClientRequestSecureSession` (type 0x03) and `ControllerAckSecureSession` (type 0x04) + + + +Both directions carry the same 5-byte SessionID echo, zero-padded to a +16-byte AES block, encrypted, and per-block whitened. The client builds +step 3 with the SessionID it received in step 2; the controller replies in +step 4 with the same SessionID re-encrypted. The implicit "did the AES +round-trip succeed?" is the only proof both sides have the same key — ECB +provides no integrity check, so the wrong key produces 16 bytes of garbage +that the receiver will dutifully accept (see notes below the next table). + +#### `ClientRequestSecureSession` plaintext layout Payload **before encryption** is 5 bytes; **on the wire** it is 16 bytes (one AES block). @@ -118,7 +143,7 @@ Then `EncryptPacket` runs (lines 396-401): controller can only decrypt this if it computed the same key from its own `ControllerKey` and the SessionID it generated. -### `ControllerAckSecureSession` (type 0x04) +#### `ControllerAckSecureSession` (type 0x04) Payload size **16 bytes** on the wire (TCP reader hardcodes `tcpReadBytes(array, 16)`, line 1722-1729). @@ -136,6 +161,37 @@ bytes of garbage (no exception), so the *only* thing that protects the client from accepting a wrong-key ack is the controller pre-validating step 3 and sending `ControllerSessionTerminated` instead. +### `ClientSessionTerminated` (type 0x05), `ControllerSessionTerminated` (type 0x06), `ControllerCannotStartNewSession` (type 0x07) + +All three share the empty-payload layout. `ClientSessionTerminated` is sent +by `OmniConnection.close()` for a graceful shutdown. `ControllerSessionTerminated` +is what the panel sends on a wrong key, a session timeout, or when it kicks +an idle client. `ControllerCannotStartNewSession` is the panel saying "I'm +already talking to someone; the protocol is single-client" — it arrives in +response to a `ClientRequestNewSession` when an existing session is open. + +The four-byte wire form for all three is just `[seq_hi seq_lo TYPE 0x00]`. + +### `OmniLinkMessage` (type 0x10), `OmniLinkUnencryptedMessage` (type 0x11), `OmniLink2Message` (type 0x20), `OmniLink2UnencryptedMessage` (type 0x21) + + + +These four packet types are the actual conversation — every command, +status query, and unsolicited push event flows through one of them. The +shape is the same; the type byte tells you whether the payload is +encrypted (`0x10` / `0x20`) or laid bare on the wire (`0x11` / `0x21`), +and which protocol version (`v1` for `0x1x`, `v2` for `0x2x`). + +The encrypted variants on TCP/v2 are what PC Access actually uses +day-to-day. The unencrypted variants exist for the serial path and for +diagnostics. PC Access never sends `OmniLink2UnencryptedMessage` over +TCP — the panel would accept it but no production deployment uses it. + +The inner Message format is documented at the top of this page (start +byte, length, opcode, data, CRC-16/MODBUS). The opcode tables for v1 and +v2 live in [`omni_pca.opcodes`](https://github.com/rsp2k/omni-pca/blob/main/src/omni_pca/opcodes.py) +— too long to reproduce here. + ### v2 `Login` (inner opcode 42) — *defined but unused on TCP* If the protocol ever calls for it, the layout is (`clsOL2MsgLogin.cs`):