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.
This commit is contained in:
@@ -0,0 +1,118 @@
|
||||
# Task 2.6 — Codec 14 encoder + ACK/nACK handler
|
||||
|
||||
**Phase:** 2 — Outbound commands
|
||||
**Status:** ⬜ Not started
|
||||
**Depends on:** 2.5 (shares utility code), 2.3, 2.4
|
||||
**Wiki refs:** `docs/wiki/sources/teltonika-data-sending-protocols.md` § Codec 14, `docs/wiki/concepts/phase-2-commands.md`
|
||||
|
||||
## Goal
|
||||
|
||||
Encode Codec 14 (`0x0E`) command frames with embedded IMEI; parse responses with both ACK (`0x06`) and **nACK (`0x11`)** types.
|
||||
|
||||
## Deliverables
|
||||
|
||||
- `src/adapters/teltonika/codec/command/codec14.ts`:
|
||||
- `encodeCodec14Command(imei: string, payload: string): Buffer`.
|
||||
- `parseCodec14Response(buf: Buffer): { kind: 'ack'; imei: string; text: string } | { kind: 'nack'; imei: string } | { kind: 'unexpected'; reason: string }`.
|
||||
- `codec14CommandHandler: CodecDataHandler` registered for codec ID `0x0E`.
|
||||
- Test file `test/codec14.test.ts` covering: doc canonical example (`getver` round trip with both ACK and nACK responses).
|
||||
|
||||
## Specification
|
||||
|
||||
### Frame structure (server → device)
|
||||
|
||||
```
|
||||
[Preamble 4B]
|
||||
[DataSize 4B] ← from CodecID through CmdQty2
|
||||
[CodecID 1B = 0x0E]
|
||||
[CmdQty1 1B = 0x01]
|
||||
[Type 1B = 0x05]
|
||||
[CmdSize 4B] ← command bytes + 8 (IMEI size)
|
||||
[IMEI 8B HEX] ← e.g. IMEI 123456789123456 → 0x0123456789123456
|
||||
[Command X B] ← ASCII command bytes
|
||||
[CmdQty2 1B = 0x01]
|
||||
[CRC 4B]
|
||||
```
|
||||
|
||||
**IMEI encoding rule:** the device IMEI is encoded as 8 bytes in HEX. For a 15-digit IMEI like `352093081452251`, prepend a leading zero (`0352093081452251`) and parse as a 16-hex-char value → 8 bytes: `0x03 52 09 30 81 45 22 51`. **Not** ASCII like the handshake.
|
||||
|
||||
```ts
|
||||
function imeiToHex(imei: string): Buffer {
|
||||
// 15 digits → prepend "0" → 16 hex chars → 8 bytes
|
||||
const padded = imei.padStart(16, '0');
|
||||
if (!/^\d{16}$/.test(padded)) throw new Error(`bad IMEI: ${imei}`);
|
||||
return Buffer.from(padded, 'hex');
|
||||
}
|
||||
```
|
||||
|
||||
### Response structure (device → server)
|
||||
|
||||
Two cases:
|
||||
|
||||
**ACK** (`Type = 0x06`): IMEI matched; command executed.
|
||||
```
|
||||
[Preamble][DataSize][CodecID 0x0E][RspQty1][Type 0x06][RspSize][IMEI 8B][Response X B][RspQty2][CRC]
|
||||
```
|
||||
|
||||
**nACK** (`Type = 0x11`): IMEI did not match; command not executed.
|
||||
```
|
||||
[Preamble][DataSize][CodecID 0x0E][RspQty1][Type 0x11][RspSize=0x08][IMEI 8B][RspQty2][CRC]
|
||||
```
|
||||
|
||||
Note: nACK has `RspSize = 8` (the IMEI itself counts), no Response bytes.
|
||||
|
||||
### Parser
|
||||
|
||||
```ts
|
||||
export function parseCodec14Response(body: Buffer):
|
||||
| { kind: 'ack'; imei: string; text: string }
|
||||
| { kind: 'nack'; imei: string }
|
||||
| { kind: 'unexpected'; reason: string }
|
||||
{
|
||||
const codecId = body[0];
|
||||
if (codecId !== 0x0E) return { kind: 'unexpected', reason: `wrong codec 0x${codecId.toString(16)}` };
|
||||
const type = body[2];
|
||||
const rspSize = body.readUInt32BE(3);
|
||||
const imeiHex = body.subarray(7, 15).toString('hex');
|
||||
const imei = imeiHex.replace(/^0+/, ''); // strip leading zero used for padding
|
||||
if (type === 0x06) {
|
||||
const text = body.subarray(15, 15 + rspSize - 8).toString('ascii');
|
||||
return { kind: 'ack', imei, text };
|
||||
}
|
||||
if (type === 0x11) {
|
||||
return { kind: 'nack', imei };
|
||||
}
|
||||
return { kind: 'unexpected', reason: `unknown response type 0x${type.toString(16)}` };
|
||||
}
|
||||
```
|
||||
|
||||
### Mapping to `commands:responses`
|
||||
|
||||
The command consumer (task 2.4) handles all three outcomes:
|
||||
|
||||
- `ack` → `status = 'responded'`, `response = text`.
|
||||
- `nack` → `status = 'failed'`, `failure_reason = 'imei_mismatch'`. The command was *delivered* but rejected — important nuance for operator dashboards.
|
||||
- `unexpected` → `status = 'failed'`, `failure_reason = 'protocol_error'`.
|
||||
|
||||
### Firmware version requirement
|
||||
|
||||
Codec 14 requires FMB.Ver.03.25.04.Rev.00 or newer. Older firmware will not understand the codec ID and may close the connection. The Phase 2 design relies on Directus knowing which devices support which codecs (potentially a `firmware_version` column on a `devices` collection). The Ingestion service does not enforce this; it just sends what it's told.
|
||||
|
||||
> **Open question:** should we expose a metric `teltonika_codec14_unexpected_total` to detect cases where Codec 14 was sent but the device closed the connection (suggesting outdated firmware)? Probably yes; add to task 2.6 deliverables and the metrics inventory.
|
||||
|
||||
## Acceptance criteria
|
||||
|
||||
- [ ] `encodeCodec14Command('352093081452251', 'getver')` produces the canonical doc bytes exactly: `00000000000000160E01050000000E0352093081452251676574766572010000D2C1`.
|
||||
- [ ] `parseCodec14Response` correctly decodes the doc's ACK response (IMEI + version string).
|
||||
- [ ] `parseCodec14Response` correctly decodes the doc's nACK response (IMEI mismatch case).
|
||||
- [ ] An end-to-end test simulates a device that ACKs Codec 14 and a device that nACKs; verify both terminal statuses land in `commands:responses` correctly.
|
||||
- [ ] IMEI HEX encoding round-trips through `imeiToHex` and the response parser.
|
||||
|
||||
## Risks / open questions
|
||||
|
||||
- nACK with `RspSize` not equal to 8 is malformed but we should fail safe (treat as `unexpected`) rather than read past buffer bounds.
|
||||
- Should the Ingestion service also log the IMEI from the nACK response (which is the *server's claim*) and compare to the *actual* IMEI of the connection (from handshake)? If they differ, something seriously wrong is happening. **Yes — log at error if they differ.** Add to acceptance criteria.
|
||||
|
||||
## Done
|
||||
|
||||
(Fill in once complete.)
|
||||
Reference in New Issue
Block a user