Skip to content

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.

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 6f
0010: 44 21 9b f8 00 a3 5d 2c
from omni_pca.packet import Packet
from 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=0
payload: 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 below
SESSION_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.

from omni_pca.message import Message
from 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)

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 = SessionID
proto_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.

Terminal window
# 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 | head

Or for a quick interactive look:

Terminal window
sudo tcpdump -i any -X 'host 192.168.1.9 and port 4369'

The -X shows hex + ASCII inline.

  • 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.