c8a5f4cd68
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.
118 lines
6.0 KiB
Markdown
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.)
|