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.
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: CodecDataHandlerregistered for codec ID0x0E.
- Test file
test/codec14.test.tscovering: doc canonical example (getverround 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:
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_totalto 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.parseCodec14Responsecorrectly decodes the doc's ACK response (IMEI + version string).parseCodec14Responsecorrectly 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:responsescorrectly. - IMEI HEX encoding round-trips through
imeiToHexand the response parser.
Risks / open questions
- nACK with
RspSizenot equal to 8 is malformed but we should fail safe (treat asunexpected) 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.)