ROADMAP plus granular task files per phase. Phase 1 (12 tasks + 1.13 device authority) covers Codec 8/8E/16 telemetry ingestion; Phase 2 (6 tasks) covers Codec 12/14 outbound commands; Phase 3 enumerates deferred items.
6.0 KiB
Task 2.5 — Codec 12 encoder + handler
Phase: 2 — Outbound commands
Status: ⬜ Not started
Depends on: 2.3, 2.4
Wiki refs: docs/wiki/sources/teltonika-data-sending-protocols.md § Codec 12, docs/wiki/concepts/phase-2-commands.md
Goal
Encode Codec 12 (0x0C) command frames for outbound delivery; parse Codec 12 response frames coming back from devices.
Deliverables
src/adapters/teltonika/codec/command/codec12.ts:encodeCodec12Command(payload: string): Bufferproduces the on-the-wire byte sequence.parseCodec12Response(buf: Buffer): { kind: 'ack' | 'unexpected'; text: string }parses an inbound response frame.- A
codec12CommandHandler: CodecDataHandlerthat the inbound framing layer (task 1.4) registers for codec ID0x0C. This handler does not producePositionrecords; it routes the response payload to the per-socket write queue'snotifyResponse.
- Test file
test/codec12.test.tswith at least:- The two canonical doc examples (
getinforequest + response,getiorequest + response). - One synthetic command with non-ASCII bytes in the payload to verify HEX encoding.
- The two canonical doc examples (
Specification
Frame structure (server → device)
[Preamble 4B = 0x00000000]
[DataSize 4B BE] ← from CodecID through CmdQty2 inclusive
[CodecID 1B = 0x0C]
[CmdQty1 1B = 0x01]
[Type 1B = 0x05] ← 0x05 = command from server
[CmdSize 4B BE] ← length of command payload bytes
[Command X B] ← ASCII command, encoded as raw bytes (NOT hex-encoded)
[CmdQty2 1B = 0x01]
[CRC 4B BE] ← CRC-16/IBM, lower 2 bytes; computed over CodecID..CmdQty2
Encoder pseudocode:
export function encodeCodec12Command(payload: string): Buffer {
const cmd = Buffer.from(payload, 'ascii');
const cmdSize = cmd.length;
const dataSize = 1 + 1 + 1 + 4 + cmdSize + 1; // CodecID + CmdQty1 + Type + CmdSize + Command + CmdQty2
const out = Buffer.alloc(4 + 4 + dataSize + 4); // Preamble + DataSize + body + CRC
let off = 0;
out.writeUInt32BE(0, off); off += 4;
out.writeUInt32BE(dataSize, off); off += 4;
out.writeUInt8(0x0C, off); off += 1;
out.writeUInt8(0x01, off); off += 1;
out.writeUInt8(0x05, off); off += 1;
out.writeUInt32BE(cmdSize, off); off += 4;
cmd.copy(out, off); off += cmdSize;
out.writeUInt8(0x01, off); off += 1;
const body = out.subarray(8, 8 + dataSize); // CodecID through CmdQty2
const crc = crc16Ibm(body);
out.writeUInt32BE(crc, off);
return out;
}
Verify against the canonical doc's getinfo example: input getinfo → output 000000000000000F0C010500000007676574696E666F0100004312.
Response structure (device → server)
Identical frame shape, but Type = 0x06:
[Preamble 4B][DataSize 4B][CodecID 0x0C][RspQty1 1B][Type 0x06][RspSize 4B][Response X B][RspQty2 1B][CRC 4B]
The response field is ASCII text, e.g. INI:2019/7/22 7:22 RTC:....
Parser:
export function parseCodec12Response(body: Buffer): { kind: 'ack'; text: string } | { kind: 'unexpected'; reason: string } {
// body is post-framing-layer: starts at CodecID
const codecId = body[0];
if (codecId !== 0x0C) return { kind: 'unexpected', reason: `wrong codec ${codecId.toString(16)}` };
const rspQty1 = body[1];
const type = body[2];
if (type !== 0x06) return { kind: 'unexpected', reason: `expected response type 0x06, got 0x${type.toString(16)}` };
const rspSize = body.readUInt32BE(3);
const text = body.subarray(7, 7 + rspSize).toString('ascii');
// body[7 + rspSize] is RspQty2; CRC was already validated upstream.
return { kind: 'ack', text };
}
Routing inbound responses to the right command
The inbound framing layer (task 1.4) sees a frame with codec 0x0C and dispatches to codec12CommandHandler. That handler retrieves the session's SocketWriteQueue (from the session context) and calls queue.notifyResponse(rawBody). The write queue's awaitResponse promise resolves with the body; the command consumer (task 2.4) then calls parseCodec12Response to extract the text.
This is the seam where Phase 2 plugs into Phase 1's framing layer. Phase 1 already supports it because:
- The codec dispatch is a registry — Phase 2 just registers a new handler.
- Phase 1's handler interface returns
{ recordCount: number }for ACK count. For Codec 12, the device does not expect a record-count ACK — responses are inherently their own ACK. The handler returns{ recordCount: 0 }and the framing layer's ACK send path skips the write whenrecordCountis 0. Update task 1.4 to honor this if not already.
Open question: is
recordCount: 0the right signal to skip ACK? Or should the handler interface return{ ack: Buffer | null }instead? The latter is cleaner. Recommendation: add an explicitackreturn slot toCodecDataHandlerin this task and update the data codec handlers to return{ ack: makeRecordCountAck(n) }. Phase 2's command handlers return{ ack: null }.
Acceptance criteria
encodeCodec12Command('getinfo')produces the canonical doc bytes exactly (compare hex strings).parseCodec12Responsecorrectly decodes the doc'sgetinforesponse into theINI:2019/7/22...ASCII string.- An end-to-end test: simulate a device that responds to a Codec 12 command, verify the round-trip command_id → encoded frame → device response → parsed text → published to
commands:responses. - CRC of every encoded frame validates against
crc16Ibm. - An incoming Codec 12 frame with
Type != 0x06is logged at warn (unexpected protocol direction) and not surfaced to the command consumer.
Risks / open questions
- The interface change (returning
{ ack }instead of{ recordCount }) is a Phase 1 retrofit. Cost: minor — three Phase 1 codec handlers update their return shape. Benefit: cleaner Phase 2 plug-in. - The
getinfocanonical CRC in the doc is0x00004312. Verify the encoder matches before declaring done.
Done
(Fill in once complete.)