Per-packet diagrams: every PacketType now has a wire-format visual

src/assets/diagrams/packet-control-empty.svg
  Covers the five empty-payload control types: NoMessage (0x00),
  ClientRequestNewSession (0x01), ClientSessionTerminated (0x05),
  ControllerSessionTerminated (0x06), ControllerCannotStartNewSession
  (0x07). Single 4-byte header diagram with the type byte highlighted,
  '(no payload)' shown as a dashed empty box, and all five type values
  enumerated below.

src/assets/diagrams/packet-controller-ack-new-session.svg
  Type 0x02. 11 bytes total — 4-byte header + 2-byte protocol version
  (0x00 0x01) + 5-byte SessionID nonce. SessionID cells in accent
  colour because they're what feed quirk #1's session key XOR mix.
  Bottom annotation explains the proto-version is hard-coded.

src/assets/diagrams/packet-secure-session.svg
  Types 0x03 and 0x04 share the same shape both directions. Two-row
  layout: top shows the plaintext (5-byte SessionID echo + 11 zero-pad
  cells with 'zero pad to 16 bytes'); bottom shows the wire form
  (header + 16-byte AES ciphertext block). Highlights the symmetric
  client-up/controller-down design.

src/assets/diagrams/packet-omnilink-message.svg
  Covers all four conversation packet types in one diagram: 0x10
  (v1 encrypted), 0x11 (v1 plaintext), 0x20 (v2 encrypted), 0x21 (v2
  plaintext). Top row shows the encrypted variant with N x 16-byte
  ciphertext blocks; brace down to bottom row showing the inner
  Message format (start byte + length + opcode + data + CRC u16 LE).
  Same diagram serves all 4 since the layout is identical except
  for the type byte and whether AES is applied.

src/content/docs/reference/protocol.mdx
  Added five new sections so every PacketType is now documented:
  - NoMessage (0x00) folded into the ClientRequestNewSession heading
    (same empty-payload layout)
  - ClientSessionTerminated (0x05) / ControllerSessionTerminated (0x06)
    / ControllerCannotStartNewSession (0x07) get a combined section
    with a one-paragraph explanation of when each fires
  - OmniLinkMessage (0x10), OmniLinkUnencryptedMessage (0x11),
    OmniLink2Message (0x20), OmniLink2UnencryptedMessage (0x21)
    consolidated into one section with the diagram + a note about
    which TCP/PC-Access actually uses

  ClientRequestSecureSession + ControllerAckSecureSession merged into
  one section since they share the diagram; original prose preserved
  as #### subsections beneath.

Build: 23 pages clean. Protocol page now 118 KB (was 92 KB) — six
inline SVG titles confirmed via grep on the rendered HTML. Every
packet type defined in omni_pca.opcodes.PacketType (12 values) is
now visualized in the docs.
This commit is contained in:
Ryan Malloy 2026-05-10 17:44:27 -06:00
parent d5d2ea3d32
commit 0c245b0af5
5 changed files with 333 additions and 4 deletions

View File

@ -0,0 +1,49 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 660 200" role="img" aria-labelledby="pce-title pce-desc" font-family="ui-monospace, SFMono-Regular, Menlo, Consolas, monospace">
<title id="pce-title">Empty-payload control packets</title>
<desc id="pce-desc">Five packet types share an identical four-byte structure with no payload: NoMessage (0x00), ClientRequestNewSession (0x01), ClientSessionTerminated (0x05), ControllerSessionTerminated (0x06), and ControllerCannotStartNewSession (0x07). The wire packet is exactly four bytes total.</desc>
<style>
.label { font-size: 12px; font-weight: 700; fill: currentColor; }
.meta { font-size: 10px; opacity: 0.75; font-style: italic; fill: currentColor; }
.byte { font-size: 11px; font-weight: 600; fill: currentColor; text-anchor: middle; }
.cell { stroke: currentColor; stroke-width: 1; fill: currentColor; fill-opacity: 0.06; }
.cell-type { stroke: currentColor; stroke-width: 1; fill: currentColor; fill-opacity: 0.13; }
.cell-empty { stroke: currentColor; stroke-width: 1; fill: none; stroke-dasharray: 4 3; stroke-opacity: 0.5; }
.group-label { font-size: 10px; font-weight: 700; fill: currentColor; opacity: 0.55; letter-spacing: 0.06em; text-anchor: middle; }
</style>
<text x="20" y="28" class="label">Empty-payload control packets</text>
<text x="20" y="46" class="meta">4 bytes total on the wire — header only, no payload.</text>
<!-- Header row -->
<g transform="translate(20, 70)">
<!-- seq u16 BE: two cells -->
<rect x="0" y="0" width="60" height="48" class="cell"/>
<rect x="60" y="0" width="60" height="48" class="cell"/>
<!-- type -->
<rect x="120" y="0" width="80" height="48" class="cell-type"/>
<!-- reserved -->
<rect x="200" y="0" width="60" height="48" class="cell"/>
<!-- payload -->
<rect x="260" y="0" width="380" height="48" class="cell-empty"/>
<text x="30" y="20" class="byte">seq_hi</text>
<text x="30" y="36" class="byte">u8</text>
<text x="90" y="20" class="byte">seq_lo</text>
<text x="90" y="36" class="byte">u8</text>
<text x="160" y="20" class="byte">type</text>
<text x="160" y="36" class="byte">u8</text>
<text x="230" y="20" class="byte">reserved</text>
<text x="230" y="36" class="byte">0x00</text>
<text x="450" y="30" class="byte" font-style="italic" opacity="0.6">(no payload)</text>
</g>
<!-- Group label brackets -->
<text x="90" y="142" class="group-label">SEQ NUMBER · u16 BE</text>
<text x="160" y="142" class="group-label">PACKET TYPE</text>
<!-- Type byte values -->
<text x="20" y="170" class="meta" font-style="normal" font-weight="700">type =</text>
<text x="70" y="170" class="meta">0x00 NoMessage · 0x01 ClientRequestNewSession · 0x05 ClientSessionTerminated</text>
<text x="70" y="186" class="meta">0x06 ControllerSessionTerminated · 0x07 ControllerCannotStartNewSession</text>
</svg>

After

Width:  |  Height:  |  Size: 2.9 KiB

View File

@ -0,0 +1,66 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 660 200" role="img" aria-labelledby="pcans-title pcans-desc" font-family="ui-monospace, SFMono-Regular, Menlo, Consolas, monospace">
<title id="pcans-title">ControllerAckNewSession (type 0x02)</title>
<desc id="pcans-desc">Reply from the controller to a new session request. Carries a 7-byte payload: a hard-coded protocol version (00 01) followed by a 5-byte random SessionID nonce. The SessionID is what gets XOR-mixed with the ControllerKey to derive the AES session key.</desc>
<style>
.label { font-size: 12px; font-weight: 700; fill: currentColor; }
.meta { font-size: 10px; opacity: 0.75; font-style: italic; fill: currentColor; }
.byte { font-size: 11px; font-weight: 600; fill: currentColor; text-anchor: middle; }
.byte-acc { font-size: 11px; font-weight: 700; fill: var(--sl-color-accent, #d97706); text-anchor: middle; }
.cell { stroke: currentColor; stroke-width: 1; fill: currentColor; fill-opacity: 0.06; }
.cell-type { stroke: currentColor; stroke-width: 1; fill: currentColor; fill-opacity: 0.13; }
.cell-acc { stroke: var(--sl-color-accent, #d97706); stroke-width: 1; fill: var(--sl-color-accent, #d97706); fill-opacity: 0.18; }
.group-label { font-size: 10px; font-weight: 700; fill: currentColor; opacity: 0.55; letter-spacing: 0.06em; text-anchor: middle; }
.group-label-acc { font-size: 10px; font-weight: 700; fill: var(--sl-color-accent, #d97706); opacity: 0.95; letter-spacing: 0.06em; text-anchor: middle; }
</style>
<text x="20" y="28" class="label">ControllerAckNewSession (type 0x02)</text>
<text x="20" y="46" class="meta">11 bytes total. Plaintext on the wire.</text>
<g transform="translate(20, 70)">
<!-- header -->
<rect x="0" y="0" width="40" height="48" class="cell"/>
<rect x="40" y="0" width="40" height="48" class="cell"/>
<rect x="80" y="0" width="40" height="48" class="cell-type"/>
<rect x="120" y="0" width="40" height="48" class="cell"/>
<text x="20" y="20" class="byte">seq_hi</text>
<text x="20" y="36" class="byte">u8</text>
<text x="60" y="20" class="byte">seq_lo</text>
<text x="60" y="36" class="byte">u8</text>
<text x="100" y="20" class="byte">type</text>
<text x="100" y="36" class="byte">0x02</text>
<text x="140" y="20" class="byte">resv</text>
<text x="140" y="36" class="byte">0x00</text>
<!-- protocol version 2 bytes -->
<rect x="170" y="0" width="40" height="48" class="cell"/>
<rect x="210" y="0" width="40" height="48" class="cell"/>
<text x="190" y="20" class="byte">proto_hi</text>
<text x="190" y="36" class="byte">0x00</text>
<text x="230" y="20" class="byte">proto_lo</text>
<text x="230" y="36" class="byte">0x01</text>
<!-- session id 5 bytes -->
<rect x="260" y="0" width="40" height="48" class="cell-acc"/>
<rect x="300" y="0" width="40" height="48" class="cell-acc"/>
<rect x="340" y="0" width="40" height="48" class="cell-acc"/>
<rect x="380" y="0" width="40" height="48" class="cell-acc"/>
<rect x="420" y="0" width="40" height="48" class="cell-acc"/>
<text x="280" y="20" class="byte-acc">sid 0</text>
<text x="280" y="36" class="byte-acc">u8</text>
<text x="320" y="20" class="byte-acc">sid 1</text>
<text x="320" y="36" class="byte-acc">u8</text>
<text x="360" y="20" class="byte-acc">sid 2</text>
<text x="360" y="36" class="byte-acc">u8</text>
<text x="400" y="20" class="byte-acc">sid 3</text>
<text x="400" y="36" class="byte-acc">u8</text>
<text x="440" y="20" class="byte-acc">sid 4</text>
<text x="440" y="36" class="byte-acc">u8</text>
</g>
<text x="80" y="142" class="group-label">HEADER (4 bytes)</text>
<text x="210" y="142" class="group-label">PROTO VERSION</text>
<text x="360" y="142" class="group-label-acc">SESSION ID (5-byte nonce) — feeds session key XOR mix</text>
<text x="20" y="180" class="meta">PC Access hard-rejects any proto version other than 00 01. The SessionID is freshly random per session.</text>
</svg>

After

Width:  |  Height:  |  Size: 4.0 KiB

View File

@ -0,0 +1,77 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 660 280" role="img" aria-labelledby="polm-title polm-desc" font-family="ui-monospace, SFMono-Regular, Menlo, Consolas, monospace">
<title id="polm-title">OmniLinkMessage and OmniLink2Message — encrypted vs unencrypted variants</title>
<desc id="polm-desc">Four packet types share this layout. v1 encrypted (0x10), v1 plaintext (0x11), v2 encrypted (0x20), and v2 plaintext (0x21). The payload is one or more 16-byte AES blocks for encrypted variants, or the inner Message bytes laid out directly for unencrypted variants. The inner Message starts with a start byte (0x41 for v1, 0x21 for v2), a length, the opcode, data, and a two-byte CRC.</desc>
<style>
.label { font-size: 12px; font-weight: 700; fill: currentColor; }
.meta { font-size: 10px; opacity: 0.75; font-style: italic; fill: currentColor; }
.byte { font-size: 11px; font-weight: 600; fill: currentColor; text-anchor: middle; }
.byte-acc { font-size: 11px; font-weight: 700; fill: var(--sl-color-accent, #d97706); text-anchor: middle; }
.cell { stroke: currentColor; stroke-width: 1; fill: currentColor; fill-opacity: 0.06; }
.cell-type { stroke: currentColor; stroke-width: 1; fill: currentColor; fill-opacity: 0.13; }
.cell-enc { stroke: var(--sl-color-accent, #d97706); stroke-width: 1.2; fill: var(--sl-color-accent, #d97706); fill-opacity: 0.15; }
.cell-msg { stroke: currentColor; stroke-width: 1; fill: currentColor; fill-opacity: 0.04; }
.group-label { font-size: 10px; font-weight: 700; fill: currentColor; opacity: 0.55; letter-spacing: 0.06em; text-anchor: middle; }
.group-label-acc { font-size: 10px; font-weight: 700; fill: var(--sl-color-accent, #d97706); opacity: 0.95; letter-spacing: 0.06em; text-anchor: middle; }
.brace { stroke: currentColor; stroke-width: 1.2; fill: none; opacity: 0.45; }
</style>
<text x="20" y="28" class="label">OmniLinkMessage / OmniLink2Message — four variants</text>
<text x="20" y="46" class="meta">v1 = 0x10 encrypted · 0x11 plaintext · v2 = 0x20 encrypted · 0x21 plaintext · same shape, different framing.</text>
<!-- Encrypted variant -->
<text x="20" y="78" class="meta" font-weight="700" font-style="normal">encrypted variants (0x10 / 0x20) — payload is N × 16 bytes of AES ciphertext:</text>
<g transform="translate(20, 90)">
<rect x="0" y="0" width="40" height="44" class="cell"/>
<rect x="40" y="0" width="40" height="44" class="cell"/>
<rect x="80" y="0" width="40" height="44" class="cell-type"/>
<rect x="120" y="0" width="40" height="44" class="cell"/>
<text x="20" y="20" class="byte">seq_hi</text>
<text x="60" y="20" class="byte">seq_lo</text>
<text x="100" y="20" class="byte">type</text>
<text x="100" y="36" class="byte">10 / 20</text>
<text x="140" y="20" class="byte">resv</text>
<rect x="170" y="0" width="120" height="44" class="cell-enc"/>
<rect x="290" y="0" width="120" height="44" class="cell-enc"/>
<rect x="410" y="0" width="120" height="44" class="cell-enc"/>
<rect x="530" y="0" width="110" height="44" class="cell-enc"/>
<text x="230" y="20" class="byte-acc">block 1</text>
<text x="230" y="36" class="byte-acc">16 B</text>
<text x="350" y="20" class="byte-acc">block 2</text>
<text x="350" y="36" class="byte-acc">16 B</text>
<text x="470" y="20" class="byte-acc">block 3</text>
<text x="470" y="36" class="byte-acc"></text>
<text x="585" y="20" class="byte-acc">block N</text>
<text x="585" y="36" class="byte-acc">16 B</text>
</g>
<text x="80" y="155" class="group-label">HEADER (4 B)</text>
<text x="405" y="155" class="group-label-acc">CIPHERTEXT (per-block whitened)</text>
<!-- Brace down to inner message -->
<path d="M 175 162 Q 175 180 195 188 L 460 188 Q 480 188 480 200" class="brace"/>
<text x="480" y="210" text-anchor="middle" class="meta">decrypts + unwhitens to →</text>
<!-- Inner message -->
<text x="20" y="232" class="meta" font-weight="700" font-style="normal">inner Message (after AES + unwhiten, or directly on the wire for 0x11 / 0x21):</text>
<g transform="translate(20, 244)">
<rect x="0" y="0" width="60" height="32" class="cell-msg"/>
<rect x="60" y="0" width="60" height="32" class="cell-msg"/>
<rect x="120" y="0" width="60" height="32" class="cell-msg"/>
<rect x="180" y="0" width="320" height="32" class="cell-msg"/>
<rect x="500" y="0" width="100" height="32" class="cell-msg"/>
<text x="30" y="14" class="byte">start</text>
<text x="30" y="26" class="byte">41/21</text>
<text x="90" y="14" class="byte">length</text>
<text x="90" y="26" class="byte">u8</text>
<text x="150" y="14" class="byte">opcode</text>
<text x="150" y="26" class="byte">u8</text>
<text x="340" y="14" class="byte">data</text>
<text x="340" y="26" class="byte">N bytes</text>
<text x="550" y="14" class="byte">CRC u16</text>
<text x="550" y="26" class="byte">LE · MODBUS</text>
</g>
</svg>

After

Width:  |  Height:  |  Size: 4.9 KiB

View File

@ -0,0 +1,81 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 660 240" role="img" aria-labelledby="pss-title pss-desc" font-family="ui-monospace, SFMono-Regular, Menlo, Consolas, monospace">
<title id="pss-title">ClientRequestSecureSession (0x03) and ControllerAckSecureSession (0x04)</title>
<desc id="pss-desc">Both secure-session packets carry the same payload shape: the 5-byte SessionID echoed back, zero-padded to a 16-byte AES block, encrypted with the freshly-derived session key, and per-block whitened. 20 bytes total on the wire.</desc>
<style>
.label { font-size: 12px; font-weight: 700; fill: currentColor; }
.meta { font-size: 10px; opacity: 0.75; font-style: italic; fill: currentColor; }
.byte { font-size: 11px; font-weight: 600; fill: currentColor; text-anchor: middle; }
.byte-acc { font-size: 11px; font-weight: 700; fill: var(--sl-color-accent, #d97706); text-anchor: middle; }
.cell { stroke: currentColor; stroke-width: 1; fill: currentColor; fill-opacity: 0.06; }
.cell-type { stroke: currentColor; stroke-width: 1; fill: currentColor; fill-opacity: 0.13; }
.cell-enc { stroke: var(--sl-color-accent, #d97706); stroke-width: 1.2; fill: var(--sl-color-accent, #d97706); fill-opacity: 0.15; }
.cell-pad { stroke: var(--sl-color-accent, #d97706); stroke-width: 1; fill: var(--sl-color-accent, #d97706); fill-opacity: 0.06; stroke-dasharray: 3 2; }
.group-label { font-size: 10px; font-weight: 700; fill: currentColor; opacity: 0.55; letter-spacing: 0.06em; text-anchor: middle; }
.group-label-acc { font-size: 10px; font-weight: 700; fill: var(--sl-color-accent, #d97706); opacity: 0.95; letter-spacing: 0.06em; text-anchor: middle; }
.arrow { stroke: currentColor; stroke-width: 1.4; fill: none; opacity: 0.45; }
</style>
<text x="20" y="28" class="label">ClientRequestSecureSession (0x03) · ControllerAckSecureSession (0x04)</text>
<text x="20" y="46" class="meta">Same shape both directions. 20 bytes total. Encrypted payload = SessionID echo, zero-padded, AES-encrypted with per-block whitening.</text>
<!-- Plaintext form -->
<text x="20" y="76" class="meta" font-weight="700" font-style="normal">plaintext (before AES + whitening):</text>
<g transform="translate(20, 86)">
<rect x="0" y="0" width="40" height="40" class="cell-enc"/>
<rect x="40" y="0" width="40" height="40" class="cell-enc"/>
<rect x="80" y="0" width="40" height="40" class="cell-enc"/>
<rect x="120" y="0" width="40" height="40" class="cell-enc"/>
<rect x="160" y="0" width="40" height="40" class="cell-enc"/>
<text x="20" y="18" class="byte-acc">sid 0</text>
<text x="60" y="18" class="byte-acc">sid 1</text>
<text x="100" y="18" class="byte-acc">sid 2</text>
<text x="140" y="18" class="byte-acc">sid 3</text>
<text x="180" y="18" class="byte-acc">sid 4</text>
<text x="20" y="32" class="byte-acc">u8</text>
<text x="60" y="32" class="byte-acc">u8</text>
<text x="100" y="32" class="byte-acc">u8</text>
<text x="140" y="32" class="byte-acc">u8</text>
<text x="180" y="32" class="byte-acc">u8</text>
<!-- 11 zero-pad cells -->
<rect x="200" y="0" width="40" height="40" class="cell-pad"/>
<rect x="240" y="0" width="40" height="40" class="cell-pad"/>
<rect x="280" y="0" width="40" height="40" class="cell-pad"/>
<rect x="320" y="0" width="40" height="40" class="cell-pad"/>
<rect x="360" y="0" width="40" height="40" class="cell-pad"/>
<rect x="400" y="0" width="40" height="40" class="cell-pad"/>
<rect x="440" y="0" width="40" height="40" class="cell-pad"/>
<rect x="480" y="0" width="40" height="40" class="cell-pad"/>
<rect x="520" y="0" width="40" height="40" class="cell-pad"/>
<rect x="560" y="0" width="40" height="40" class="cell-pad"/>
<rect x="600" y="0" width="40" height="40" class="cell-pad"/>
<text x="420" y="20" class="byte-acc" font-style="italic" opacity="0.7">…zero pad to 16 bytes…</text>
</g>
<!-- Brackets -->
<text x="100" y="146" class="group-label-acc">SESSION ID ECHO</text>
<text x="420" y="146" class="group-label">ZERO PADDING</text>
<!-- Wire form -->
<text x="20" y="178" class="meta" font-weight="700" font-style="normal">on the wire (after AES + whitening):</text>
<g transform="translate(20, 188)">
<!-- header -->
<rect x="0" y="0" width="40" height="40" class="cell"/>
<rect x="40" y="0" width="40" height="40" class="cell"/>
<rect x="80" y="0" width="40" height="40" class="cell-type"/>
<rect x="120" y="0" width="40" height="40" class="cell"/>
<text x="20" y="18" class="byte">seq_hi</text>
<text x="60" y="18" class="byte">seq_lo</text>
<text x="100" y="18" class="byte">type</text>
<text x="100" y="32" class="byte">03/04</text>
<text x="140" y="18" class="byte">resv</text>
<text x="140" y="32" class="byte">0x00</text>
<!-- ciphertext -->
<rect x="170" y="0" width="470" height="40" class="cell-enc"/>
<text x="405" y="20" class="byte-acc">16-byte AES-128 ciphertext (one block)</text>
</g>
</svg>

After

Width:  |  Height:  |  Size: 5.0 KiB

View File

@ -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 HandshakeSequence from '../../../assets/diagrams/handshake-sequence.svg?raw';
import PacketStructure from '../../../assets/diagrams/packet-structure.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';
<div style="margin: 1.5rem 0;" set:html={HandshakeSequence} /> <div style="margin: 1.5rem 0;" set:html={HandshakeSequence} />
@ -81,14 +85,23 @@ UDP / 1808 TCP).
All offsets are **into the packet payload**, i.e., after the 4-byte outer All offsets are **into the packet payload**, i.e., after the 4-byte outer
header (`[seq_hi][seq_lo][type][reserved=0]`). header (`[seq_hi][seq_lo][type][reserved=0]`).
### `ClientRequestNewSession` (type 0x01) ### `NoMessage` (type 0x00) and `ClientRequestNewSession` (type 0x01)
<div style="margin: 1rem 0;" set:html={PacketEmpty} />
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 | | 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) ### `ControllerAckNewSession` (type 0x02)
<div style="margin: 1rem 0;" set:html={PacketAckNew} />
Payload size **7 bytes** (TCP reader hardcodes `tcpReadBytes(array, 7)` on this Payload size **7 bytes** (TCP reader hardcodes `tcpReadBytes(array, 7)` on this
type, line 1714). type, line 1714).
@ -100,7 +113,19 @@ type, line 1714).
Total wire packet: 4-byte header + 7-byte payload = 11 bytes. Total wire packet: 4-byte header + 7-byte payload = 11 bytes.
### `ClientRequestSecureSession` (type 0x03) ### `ClientRequestSecureSession` (type 0x03) and `ControllerAckSecureSession` (type 0x04)
<div style="margin: 1rem 0;" set:html={PacketSecureSession} />
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 Payload **before encryption** is 5 bytes; **on the wire** it is 16 bytes (one
AES block). 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 controller can only decrypt this if it computed the same key from its own
`ControllerKey` and the SessionID it generated. `ControllerKey` and the SessionID it generated.
### `ControllerAckSecureSession` (type 0x04) #### `ControllerAckSecureSession` (type 0x04)
Payload size **16 bytes** on the wire (TCP reader hardcodes Payload size **16 bytes** on the wire (TCP reader hardcodes
`tcpReadBytes(array, 16)`, line 1722-1729). `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 from accepting a wrong-key ack is the controller pre-validating step 3 and
sending `ControllerSessionTerminated` instead. 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)
<div style="margin: 1rem 0;" set:html={PacketOmniLink} />
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* ### v2 `Login` (inner opcode 42) — *defined but unused on TCP*
If the protocol ever calls for it, the layout is (`clsOL2MsgLogin.cs`): If the protocol ever calls for it, the layout is (`clsOL2MsgLogin.cs`):