Decode a captured Omni-Link packet
You’ve captured raw bytes between a client and an Omni controller and you want to know what they say. The library’s primitives let you decode them in a few lines.
What you have
Section titled “What you have”A hex dump from tcpdump -X, Wireshark, or similar — something like:
0000: 00 01 02 00 ....Or a longer one with the encrypted payload:
0000: 00 03 20 00 c4 8a 9f 0e d2 7e b3 51 88 a4 1c 6f0010: 44 21 9b f8 00 a3 5d 2cStep 1 — decode the outer packet
Section titled “Step 1 — decode the outer packet”from omni_pca.packet import Packetfrom omni_pca.opcodes import PacketType
raw = bytes.fromhex("00 03 20 00 c4 8a 9f 0e d2 7e b3 51 88 a4 1c 6f 44 21 9b f8 00 a3 5d 2c")pkt = Packet.decode(raw)print(f"seq={pkt.seq} type={PacketType(pkt.type).name} reserved={pkt.reserved}")print(f"payload: {len(pkt.data)} bytes ({pkt.data.hex(' ')})")Output:
seq=3 type=OmniLink2Message reserved=0payload: 20 bytes (c4 8a 9f 0e d2 7e b3 51 88 a4 1c 6f 44 21 9b f8 00 a3 5d 2c)The 4-byte header is plaintext: [seq_hi seq_lo type reserved]. For
OmniLink2Message (type 0x20) and OmniLinkMessage (0x10), the
payload is AES-encrypted and zero-padded to a 16-byte boundary; you
need the session key and the sequence number to decrypt it.
Step 2 — decrypt the payload (if you have the key)
Section titled “Step 2 — decrypt the payload (if you have the key)”from omni_pca.crypto import decrypt_message_payload
# Session key = derive_session_key(controller_key, session_id) — see belowSESSION_KEY = bytes.fromhex("6ba7b4e9b4656de3cd7edd4c650cdb0c")
plaintext = decrypt_message_payload( ciphertext=pkt.data, seq=pkt.seq, session_key=SESSION_KEY,)print(f"plaintext: {plaintext.hex(' ')}")If the key is right and the per-block whitening
unwound correctly, you’ll see a valid Omni v2 message starting with
0x21. If it comes back looking like noise, you have the wrong key,
the wrong sequence number, or you’re missing the whitening step.
Step 3 — decode the inner message
Section titled “Step 3 — decode the inner message”from omni_pca.message import Messagefrom omni_pca.opcodes import OmniLink2MessageType
msg = Message.decode(plaintext)opcode = OmniLink2MessageType(msg.opcode)print(f"opcode={opcode.name} length={msg.length} crc_valid={msg.crc_is_valid}")print(f"data: {msg.data.hex(' ')}")This validates the CRC-16/MODBUS, parses the opcode byte, and gives
you the raw payload. From there, dispatch on opcode to one of the
typed parsers in omni_pca.models:
from omni_pca.models import SystemInformation
if opcode == OmniLink2MessageType.SystemInformation: info = SystemInformation.parse(msg.data[1:]) # strip the opcode byte print(info)Step 4 — unencrypted handshake packets
Section titled “Step 4 — unencrypted handshake packets”The ControllerAckNewSession (type 0x02) and ClientRequestNewSession
(type 0x01) packets are not AES-encrypted — they’re plaintext. The
ack carries the 5-byte SessionID directly:
ack_raw = bytes.fromhex("00 01 02 00 00 01 a3 b2 c1 d4 e5")pkt = Packet.decode(ack_raw)# pkt.data layout (clsOmniLinkConnection.cs:1416):# bytes 0..1 = protocol version (0x00 0x01)# bytes 2..6 = SessionIDproto_version = (pkt.data[0] << 8) | pkt.data[1]session_id = pkt.data[2:7]print(f"proto v{proto_version}, session_id={session_id.hex()}")With the SessionID + your ControllerKey you can derive the session AES key:
from omni_pca.crypto import derive_session_key
CONTROLLER_KEY = bytes.fromhex("6ba7b4e9b4656de3cd7edd4c650cdb09")session_key = derive_session_key(CONTROLLER_KEY, session_id)Now you can decrypt every subsequent packet using the steps above.
Capturing packets in the first place
Section titled “Capturing packets in the first place”# Capture all traffic to/from the panel for 5 minutes:sudo tcpdump -i any -w omni.pcap -s 0 'host 192.168.1.9 and port 4369' &sleep 300; sudo killall tcpdump
# Then in Wireshark / tshark:tshark -r omni.pcap -T fields -e tcp.payload | headOr for a quick interactive look:
sudo tcpdump -i any -X 'host 192.168.1.9 and port 4369'The -X shows hex + ASCII inline.
Where this is most useful
Section titled “Where this is most useful”- Confirming the per-block whitening quirk is applied correctly when porting the protocol to another language.
- Diagnosing why a third-party Omni client (jomnilinkII / pyomnilink / homebrew implementation) is dropping the secure session — usually one of the two quirks isn’t implemented.
- Capturing real-world unsolicited push events to feed into the mock panel as fixtures.