Files
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

5.3 KiB

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.

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

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:

  • ackstatus = 'responded', response = text.
  • nackstatus = 'failed', failure_reason = 'imei_mismatch'. The command was delivered but rejected — important nuance for operator dashboards.
  • unexpectedstatus = '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.)