# 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): Buffer` produces the on-the-wire byte sequence. - `parseCodec12Response(buf: Buffer): { kind: 'ack' | 'unexpected'; text: string }` parses an inbound response frame. - A `codec12CommandHandler: CodecDataHandler` that the **inbound** framing layer (task 1.4) registers for codec ID `0x0C`. This handler does not produce `Position` records; it routes the response payload to the per-socket write queue's `notifyResponse`. - Test file `test/codec12.test.ts` with at least: - The two canonical doc examples (`getinfo` request + response, `getio` request + response). - One synthetic command with non-ASCII bytes in the payload to verify HEX encoding. ## 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: ```ts 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: ```ts 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: 1. The codec dispatch is a registry — Phase 2 just registers a new handler. 2. 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 when `recordCount` is 0. **Update task 1.4 to honor this** if not already. > **Open question:** is `recordCount: 0` the right signal to skip ACK? Or should the handler interface return `{ ack: Buffer | null }` instead? The latter is cleaner. **Recommendation:** add an explicit `ack` return slot to `CodecDataHandler` in 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). - [ ] `parseCodec12Response` correctly decodes the doc's `getinfo` response into the `INI: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 != 0x06` is 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 `getinfo` canonical CRC in the doc is `0x00004312`. Verify the encoder matches before declaring done. ## Done (Fill in once complete.)