Files
tcp-ingestion/.planning/phase-2-commands/05-codec-12.md
T
julian c8a5f4cd68 Add Phase 1 and Phase 2 planning documents
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.
2026-04-30 15:50:49 +02:00

118 lines
6.0 KiB
Markdown

# 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.)