Lessons learned from debugging the client against real Lawo hardware.
[BOF=0xFE] [SLOT] [MSG_TYPE] [CMD] [VERSION] [FLAGS?] [DTD?] [APP_COUNT?] [PAYLOAD] [CRC_LO] [CRC_HI] [EOF=0xFF]
-
Message Type: Always
0x0Efor both data and keep-alive (NOT0x00) -
Escaping: Escape bytes
>= 0xF8by XOR with0x20and prefix with0xFDif byte >= 0xF8 { output.push(0xFD); output.push(byte ^ 0x20); }
-
CRC-16-CCITT:
- Use reflected polynomial table
- Calculate on escaped data (unescape while computing)
- Final CRC is inverted (
!crc) - CRC bytes themselves must be escaped if
>= 0xF8
-
Keep-alive frame:
FE 00 0E 01 01 [CRC] FF
60 xx -- GlowRoot (APPLICATION 0, constructed)
6b xx -- RootElementCollection (APPLICATION 11)
[elements]
Node contents must be wrapped in a SET (0x31), and strings use UTF8String (0x0C):
a1 xx -- context[1] (contents)
31 xx -- SET wrapper (REQUIRED!)
a0 xx -- context[0] (identifier)
0c xx -- UTF8String (NOT raw bytes!)
[string bytes]
Wrong (causes connection close):
a1 09 80 07 "facades" -- context[1] with primitive string
Correct:
a1 0d 31 0b a0 09 0c 07 "facades" -- SET > context[0] > UTF8String
Must be wrapped in context[0] (NumberedTreeNode):
a0 xx -- context[0] NumberedTreeNode wrapper (REQUIRED!)
6a xx -- QualifiedNode (APPLICATION 10)
a0 xx -- context[0] path
0d xx -- RELATIVE-OID
[path bytes]
a1 xx -- context[1] contents (with SET + UTF8String)
a2 xx -- context[2] children (commands)
- Tag:
0x0D(Universal Primitive 13) - Each component encoded with 7-bit chunks, MSB set for continuation
// Path [5, 1, 1] encodes as: 0d 03 05 01 01
// Path [5] encodes as: 0d 01 05Commands are wrapped in APPLICATION tags inside context[2]:
a2 xx -- context[2] children
64 xx -- ElementCollection (APPLICATION 4)
a0 xx -- context[0] NumberedTreeNode
62 xx -- Command (APPLICATION 2)
a0 xx -- context[0] command number
02 01 20 -- INTEGER 32 (GetDirectory)
Command numbers:
32(0x20) = GetDirectory33(0x21) = Subscribe34(0x22) = Unsubscribe35(0x23) = Invoke
a2 xx -- context[2] children
64 xx -- ElementCollection (APPLICATION 4)
a0 xx -- context[0]
76 xx -- Invocation (APPLICATION 22)
a0 xx -- context[0] invocation ID
02 xx [id]
a1 xx -- context[1] arguments (optional)
- Decode S101 with correct CRC verification
- Handle NumberedTreeNode wrapper (context[0])
- Parse QualifiedNode paths as RELATIVE-OID
- Handle commands inside context[2] > ElementCollection
- Wrap all elements in NumberedTreeNode (context[0])
- Use SET wrapper for node/parameter contents
- Use UTF8String for all string fields
- Encode paths as RELATIVE-OID
- Calculate CRC correctly (reflected, inverted)
When responding to GetDirectory, send children with full contents:
60 xx -- Root
6b xx -- RootElementCollection
a0 xx -- NumberedTreeNode
6a xx -- QualifiedNode
a0 xx -- path
a1 xx -- contents (SET wrapped)
a2 xx -- children
64 xx -- ElementCollection
[child nodes with their contents]
Compare frames with emberplus-connection (Node.js):
const { EmberClient } = require('emberplus-connection');
// Capture frames to compare byte-by-byte