Implement Phase 1 tasks 1.5-1.7 + 1.9 (Codec 8/8E/16 parsers + fixture suite)
- Codec 8 parser (1-byte IO IDs, no NX/Generation Type) - Codec 8 Extended parser (2-byte IO IDs + variable-length NX section) - Codec 16 parser (mixed widths + Generation Type, supports IO IDs > 255) - Shared GPS element / timestamp helpers in gps-element.ts - Fixture loader with bigint/Buffer sentinel encoding and auto-discovery - 12 fixture pairs across codec8/8E/16 (canonical doc + synthetic edge cases) - Cross-checked Codec 8 against Traccar's TeltonikaProtocolDecoder (no discrepancies) 26 new tests. Total 62 passing across 10 test files. typecheck/lint/test/build all clean.
This commit is contained in:
@@ -0,0 +1,165 @@
|
||||
import type { Position } from '../../../../core/types.js';
|
||||
import type { CodecDataHandler, CodecHandlerContext } from '../registry.js';
|
||||
import { parseTimestamp, parseGpsElement } from './gps-element.js';
|
||||
|
||||
/**
|
||||
* Codec 16 (`0x10`) AVL data parser.
|
||||
*
|
||||
* Mixed-width layout — the trap:
|
||||
* - Counts (N total, N1, N2, N4, N8): 1 byte ← same as Codec 8
|
||||
* - IO IDs: 2 bytes ← same as Codec 8E
|
||||
* - Event IO ID: 2 bytes ← same as Codec 8E
|
||||
* - Generation Type: 1 byte ← unique to Codec 16
|
||||
*
|
||||
* Full IO element layout:
|
||||
* [Event IO ID 2B]
|
||||
* [Generation Type 1B]
|
||||
* [N total 1B]
|
||||
* [N1 1B] then N1 × ([IO ID 2B][Value 1B])
|
||||
* [N2 1B] then N2 × ([IO ID 2B][Value 2B])
|
||||
* [N4 1B] then N4 × ([IO ID 2B][Value 4B])
|
||||
* [N8 1B] then N8 × ([IO ID 2B][Value 8B — bigint])
|
||||
*
|
||||
* No NX section (unlike Codec 8E).
|
||||
*
|
||||
* Generation Type (0–7) is stored as attributes['__generation_type'].
|
||||
* Values 3 (Reserved) are accepted without rejection — logged at debug level only.
|
||||
*
|
||||
* The body received here is the full frame payload:
|
||||
* [CodecID 1B][N1 1B][AVL records...][N2 1B]
|
||||
*/
|
||||
|
||||
function parsePriority(raw: number, ctx: CodecHandlerContext): 0 | 1 | 2 {
|
||||
if (raw === 0 || raw === 1 || raw === 2) {
|
||||
return raw;
|
||||
}
|
||||
// TODO(1.10): emit metric teltonika_priority_out_of_range_total
|
||||
ctx.logger.debug({ raw_priority: raw }, 'priority value out of range [0,2]; clamping to 2 (Panic)');
|
||||
return 2;
|
||||
}
|
||||
|
||||
function parseIoElements16(
|
||||
body: Buffer,
|
||||
offset: number,
|
||||
ctx: CodecHandlerContext,
|
||||
): {
|
||||
attributes: Record<string, number | bigint | Buffer>;
|
||||
nextOffset: number;
|
||||
} {
|
||||
const attributes: Record<string, number | bigint | Buffer> = {};
|
||||
|
||||
// Event IO ID — 2 bytes (same as 8E)
|
||||
const eventIoId = body.readUInt16BE(offset);
|
||||
offset += 2;
|
||||
attributes['__event'] = eventIoId;
|
||||
|
||||
// Generation Type — 1 byte (unique to Codec 16)
|
||||
const generationType = body.readUInt8(offset);
|
||||
offset += 1;
|
||||
if (generationType === 3) {
|
||||
ctx.logger.debug({ generation_type: generationType }, 'reserved Generation Type value 3 observed');
|
||||
}
|
||||
attributes['__generation_type'] = generationType;
|
||||
|
||||
// N total — 1 byte (like Codec 8, unlike Codec 8E)
|
||||
offset += 1; // read but skip — Nk sections carry individual counts
|
||||
|
||||
// N1 — 1B count, 2B IO ID, 1B value
|
||||
const n1 = body.readUInt8(offset);
|
||||
offset += 1;
|
||||
for (let i = 0; i < n1; i++) {
|
||||
const ioId = body.readUInt16BE(offset);
|
||||
offset += 2;
|
||||
attributes[String(ioId)] = body.readUInt8(offset);
|
||||
offset += 1;
|
||||
}
|
||||
|
||||
// N2 — 1B count, 2B IO ID, 2B value
|
||||
const n2 = body.readUInt8(offset);
|
||||
offset += 1;
|
||||
for (let i = 0; i < n2; i++) {
|
||||
const ioId = body.readUInt16BE(offset);
|
||||
offset += 2;
|
||||
attributes[String(ioId)] = body.readUInt16BE(offset);
|
||||
offset += 2;
|
||||
}
|
||||
|
||||
// N4 — 1B count, 2B IO ID, 4B value
|
||||
const n4 = body.readUInt8(offset);
|
||||
offset += 1;
|
||||
for (let i = 0; i < n4; i++) {
|
||||
const ioId = body.readUInt16BE(offset);
|
||||
offset += 2;
|
||||
attributes[String(ioId)] = body.readUInt32BE(offset);
|
||||
offset += 4;
|
||||
}
|
||||
|
||||
// N8 — 1B count, 2B IO ID, 8B value (bigint)
|
||||
const n8 = body.readUInt8(offset);
|
||||
offset += 1;
|
||||
for (let i = 0; i < n8; i++) {
|
||||
const ioId = body.readUInt16BE(offset);
|
||||
offset += 2;
|
||||
attributes[String(ioId)] = body.readBigUInt64BE(offset);
|
||||
offset += 8;
|
||||
}
|
||||
|
||||
return { attributes, nextOffset: offset };
|
||||
}
|
||||
|
||||
function parseOneRecord(
|
||||
body: Buffer,
|
||||
offset: number,
|
||||
imei: string,
|
||||
ctx: CodecHandlerContext,
|
||||
): { position: Position; nextOffset: number } {
|
||||
const { value: timestamp, nextOffset: off1 } = parseTimestamp(body, offset);
|
||||
const rawPriority = body.readUInt8(off1);
|
||||
const priority = parsePriority(rawPriority, ctx);
|
||||
const { value: gps, nextOffset: off2 } = parseGpsElement(body, off1 + 1);
|
||||
|
||||
const { attributes, nextOffset: off3 } = parseIoElements16(body, off2, ctx);
|
||||
|
||||
const position: Position = {
|
||||
device_id: imei,
|
||||
timestamp,
|
||||
latitude: gps.latitude,
|
||||
longitude: gps.longitude,
|
||||
altitude: gps.altitude,
|
||||
angle: gps.angle,
|
||||
speed: gps.speed,
|
||||
satellites: gps.satellites,
|
||||
priority,
|
||||
attributes,
|
||||
};
|
||||
|
||||
return { position, nextOffset: off3 };
|
||||
}
|
||||
|
||||
export const codec16Handler: CodecDataHandler = {
|
||||
codec_id: 0x10,
|
||||
|
||||
async handle(body: Buffer, ctx: CodecHandlerContext): Promise<{ recordCount: number }> {
|
||||
// Body layout: [CodecID 1B][N1 1B][records...][N2 1B]
|
||||
const recordCount = body.readUInt8(1);
|
||||
const n2Offset = body.length - 1;
|
||||
|
||||
let offset = 2; // start of first record
|
||||
|
||||
for (let i = 0; i < recordCount; i++) {
|
||||
const { position, nextOffset } = parseOneRecord(body, offset, ctx.imei, ctx);
|
||||
offset = nextOffset;
|
||||
await ctx.publish(position);
|
||||
}
|
||||
|
||||
// Cursor invariant: after all records, cursor must be exactly at N2
|
||||
if (offset !== n2Offset) {
|
||||
throw new Error(
|
||||
`Codec 16 cursor invariant violated: expected offset ${n2Offset} after ${recordCount} records, got ${offset}. ` +
|
||||
`Body length: ${body.length}`,
|
||||
);
|
||||
}
|
||||
|
||||
return { recordCount };
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,159 @@
|
||||
import type { Position } from '../../../../core/types.js';
|
||||
import type { CodecDataHandler, CodecHandlerContext } from '../registry.js';
|
||||
import { parseTimestamp, parseGpsElement } from './gps-element.js';
|
||||
|
||||
/**
|
||||
* Codec 8 (`0x08`) AVL data parser.
|
||||
*
|
||||
* IO element layout — 1-byte everything:
|
||||
* [Event IO ID 1B]
|
||||
* [N total 1B]
|
||||
* [N1 1B] then N1 × ([IO ID 1B][Value 1B])
|
||||
* [N2 1B] then N2 × ([IO ID 1B][Value 2B])
|
||||
* [N4 1B] then N4 × ([IO ID 1B][Value 4B])
|
||||
* [N8 1B] then N8 × ([IO ID 1B][Value 8B — bigint])
|
||||
*
|
||||
* The body received here is the full frame payload:
|
||||
* [CodecID 1B][N1 1B][AVL records...][N2 1B]
|
||||
*
|
||||
* After parsing all records the cursor must sit exactly at the N2 byte
|
||||
* (body.length - 1). Any drift means a parser bug — we throw with offset
|
||||
* details rather than silently emit corrupt positions.
|
||||
*/
|
||||
|
||||
function parsePriority(raw: number, ctx: CodecHandlerContext): 0 | 1 | 2 {
|
||||
if (raw === 0 || raw === 1 || raw === 2) {
|
||||
return raw;
|
||||
}
|
||||
// TODO(1.10): emit metric teltonika_priority_out_of_range_total
|
||||
ctx.logger.debug({ raw_priority: raw }, 'priority value out of range [0,2]; clamping to 2 (Panic)');
|
||||
return 2;
|
||||
}
|
||||
|
||||
function parseIoElements(
|
||||
body: Buffer,
|
||||
offset: number,
|
||||
ioIdWidth: 1 | 2,
|
||||
countWidth: 1 | 2,
|
||||
): {
|
||||
attributes: Record<string, number | bigint | Buffer>;
|
||||
eventIoId: number;
|
||||
nextOffset: number;
|
||||
} {
|
||||
const readId = ioIdWidth === 1
|
||||
? (off: number) => ({ id: body.readUInt8(off), next: off + 1 })
|
||||
: (off: number) => ({ id: body.readUInt16BE(off), next: off + 2 });
|
||||
|
||||
const readCount = countWidth === 1
|
||||
? (off: number) => ({ count: body.readUInt8(off), next: off + 1 })
|
||||
: (off: number) => ({ count: body.readUInt16BE(off), next: off + 2 });
|
||||
|
||||
// Event IO ID
|
||||
const { id: eventIoId, next: off1 } = readId(offset);
|
||||
// N total (read but not validated — individual Nk sections carry the real counts)
|
||||
const { next: off2 } = readCount(off1);
|
||||
|
||||
const attributes: Record<string, number | bigint | Buffer> = {};
|
||||
attributes['__event'] = eventIoId;
|
||||
|
||||
let off = off2;
|
||||
|
||||
// N1 — 1-byte values
|
||||
const { count: n1, next: afterN1 } = readCount(off);
|
||||
off = afterN1;
|
||||
for (let i = 0; i < n1; i++) {
|
||||
const { id, next: afterId } = readId(off);
|
||||
off = afterId;
|
||||
attributes[String(id)] = body.readUInt8(off);
|
||||
off += 1;
|
||||
}
|
||||
|
||||
// N2 — 2-byte values
|
||||
const { count: n2, next: afterN2 } = readCount(off);
|
||||
off = afterN2;
|
||||
for (let i = 0; i < n2; i++) {
|
||||
const { id, next: afterId } = readId(off);
|
||||
off = afterId;
|
||||
attributes[String(id)] = body.readUInt16BE(off);
|
||||
off += 2;
|
||||
}
|
||||
|
||||
// N4 — 4-byte values
|
||||
const { count: n4, next: afterN4 } = readCount(off);
|
||||
off = afterN4;
|
||||
for (let i = 0; i < n4; i++) {
|
||||
const { id, next: afterId } = readId(off);
|
||||
off = afterId;
|
||||
attributes[String(id)] = body.readUInt32BE(off);
|
||||
off += 4;
|
||||
}
|
||||
|
||||
// N8 — 8-byte values (bigint)
|
||||
const { count: n8, next: afterN8 } = readCount(off);
|
||||
off = afterN8;
|
||||
for (let i = 0; i < n8; i++) {
|
||||
const { id, next: afterId } = readId(off);
|
||||
off = afterId;
|
||||
attributes[String(id)] = body.readBigUInt64BE(off);
|
||||
off += 8;
|
||||
}
|
||||
|
||||
return { attributes, eventIoId, nextOffset: off };
|
||||
}
|
||||
|
||||
function parseOneRecord(
|
||||
body: Buffer,
|
||||
offset: number,
|
||||
imei: string,
|
||||
ctx: CodecHandlerContext,
|
||||
): { position: Position; nextOffset: number } {
|
||||
const { value: timestamp, nextOffset: off1 } = parseTimestamp(body, offset);
|
||||
const rawPriority = body.readUInt8(off1);
|
||||
const priority = parsePriority(rawPriority, ctx);
|
||||
const { value: gps, nextOffset: off2 } = parseGpsElement(body, off1 + 1);
|
||||
|
||||
const { attributes, nextOffset: off3 } = parseIoElements(body, off2, 1, 1);
|
||||
|
||||
const position: Position = {
|
||||
device_id: imei,
|
||||
timestamp,
|
||||
latitude: gps.latitude,
|
||||
longitude: gps.longitude,
|
||||
altitude: gps.altitude,
|
||||
angle: gps.angle,
|
||||
speed: gps.speed,
|
||||
satellites: gps.satellites,
|
||||
priority,
|
||||
attributes,
|
||||
};
|
||||
|
||||
return { position, nextOffset: off3 };
|
||||
}
|
||||
|
||||
export const codec8Handler: CodecDataHandler = {
|
||||
codec_id: 0x08,
|
||||
|
||||
async handle(body: Buffer, ctx: CodecHandlerContext): Promise<{ recordCount: number }> {
|
||||
// Body layout: [CodecID 1B][N1 1B][records...][N2 1B]
|
||||
const recordCount = body.readUInt8(1);
|
||||
const n2Offset = body.length - 1;
|
||||
|
||||
let offset = 2; // start of first record
|
||||
|
||||
for (let i = 0; i < recordCount; i++) {
|
||||
const { position, nextOffset } = parseOneRecord(body, offset, ctx.imei, ctx);
|
||||
offset = nextOffset;
|
||||
await ctx.publish(position);
|
||||
}
|
||||
|
||||
// Cursor invariant: after all records, cursor must be exactly at N2
|
||||
if (offset !== n2Offset) {
|
||||
throw new Error(
|
||||
`Codec 8 cursor invariant violated: expected offset ${n2Offset} after ${recordCount} records, got ${offset}. ` +
|
||||
`Body length: ${body.length}`,
|
||||
);
|
||||
}
|
||||
|
||||
return { recordCount };
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,162 @@
|
||||
import type { Position } from '../../../../core/types.js';
|
||||
import type { CodecDataHandler, CodecHandlerContext } from '../registry.js';
|
||||
import { parseTimestamp, parseGpsElement } from './gps-element.js';
|
||||
|
||||
/**
|
||||
* Codec 8 Extended (`0x8E`) AVL data parser.
|
||||
*
|
||||
* IO element layout — 2-byte fields + variable-length NX section:
|
||||
* [Event IO ID 2B]
|
||||
* [N total 2B]
|
||||
* [N1 2B] then N1 × ([IO ID 2B][Value 1B])
|
||||
* [N2 2B] then N2 × ([IO ID 2B][Value 2B])
|
||||
* [N4 2B] then N4 × ([IO ID 2B][Value 4B])
|
||||
* [N8 2B] then N8 × ([IO ID 2B][Value 8B — bigint])
|
||||
* [NX 2B] then NX × ([IO ID 2B][Length 2B][Value <Length> bytes — Buffer])
|
||||
*
|
||||
* The body received here is the full frame payload:
|
||||
* [CodecID 1B][N1 1B][AVL records...][N2 1B]
|
||||
*
|
||||
* NX values are stored as Buffer using buf.subarray() — zero-copy views into
|
||||
* the underlying frame buffer. This is safe because the frame buffer outlives
|
||||
* the publish call.
|
||||
*/
|
||||
|
||||
function parsePriority(raw: number, ctx: CodecHandlerContext): 0 | 1 | 2 {
|
||||
if (raw === 0 || raw === 1 || raw === 2) {
|
||||
return raw;
|
||||
}
|
||||
// TODO(1.10): emit metric teltonika_priority_out_of_range_total
|
||||
ctx.logger.debug({ raw_priority: raw }, 'priority value out of range [0,2]; clamping to 2 (Panic)');
|
||||
return 2;
|
||||
}
|
||||
|
||||
function parseIoElements8E(
|
||||
body: Buffer,
|
||||
offset: number,
|
||||
): {
|
||||
attributes: Record<string, number | bigint | Buffer>;
|
||||
nextOffset: number;
|
||||
} {
|
||||
const attributes: Record<string, number | bigint | Buffer> = {};
|
||||
|
||||
// Event IO ID — 2 bytes
|
||||
const eventIoId = body.readUInt16BE(offset);
|
||||
offset += 2;
|
||||
attributes['__event'] = eventIoId;
|
||||
|
||||
// N total — 2 bytes (read and skip; individual Nk sections carry counts)
|
||||
offset += 2;
|
||||
|
||||
// N1 — 2B count, 2B IO ID, 1B value
|
||||
const n1 = body.readUInt16BE(offset);
|
||||
offset += 2;
|
||||
for (let i = 0; i < n1; i++) {
|
||||
const ioId = body.readUInt16BE(offset);
|
||||
offset += 2;
|
||||
attributes[String(ioId)] = body.readUInt8(offset);
|
||||
offset += 1;
|
||||
}
|
||||
|
||||
// N2 — 2B count, 2B IO ID, 2B value
|
||||
const n2 = body.readUInt16BE(offset);
|
||||
offset += 2;
|
||||
for (let i = 0; i < n2; i++) {
|
||||
const ioId = body.readUInt16BE(offset);
|
||||
offset += 2;
|
||||
attributes[String(ioId)] = body.readUInt16BE(offset);
|
||||
offset += 2;
|
||||
}
|
||||
|
||||
// N4 — 2B count, 2B IO ID, 4B value
|
||||
const n4 = body.readUInt16BE(offset);
|
||||
offset += 2;
|
||||
for (let i = 0; i < n4; i++) {
|
||||
const ioId = body.readUInt16BE(offset);
|
||||
offset += 2;
|
||||
attributes[String(ioId)] = body.readUInt32BE(offset);
|
||||
offset += 4;
|
||||
}
|
||||
|
||||
// N8 — 2B count, 2B IO ID, 8B value (bigint)
|
||||
const n8 = body.readUInt16BE(offset);
|
||||
offset += 2;
|
||||
for (let i = 0; i < n8; i++) {
|
||||
const ioId = body.readUInt16BE(offset);
|
||||
offset += 2;
|
||||
attributes[String(ioId)] = body.readBigUInt64BE(offset);
|
||||
offset += 8;
|
||||
}
|
||||
|
||||
// NX — 2B count, 2B IO ID, 2B length, <length> bytes value (Buffer, zero-copy)
|
||||
const nx = body.readUInt16BE(offset);
|
||||
offset += 2;
|
||||
for (let i = 0; i < nx; i++) {
|
||||
const ioId = body.readUInt16BE(offset);
|
||||
offset += 2;
|
||||
const valueLength = body.readUInt16BE(offset);
|
||||
offset += 2;
|
||||
// Zero-copy view; safe because the frame buffer outlives publish
|
||||
attributes[String(ioId)] = body.subarray(offset, offset + valueLength);
|
||||
offset += valueLength;
|
||||
}
|
||||
|
||||
return { attributes, nextOffset: offset };
|
||||
}
|
||||
|
||||
function parseOneRecord(
|
||||
body: Buffer,
|
||||
offset: number,
|
||||
imei: string,
|
||||
ctx: CodecHandlerContext,
|
||||
): { position: Position; nextOffset: number } {
|
||||
const { value: timestamp, nextOffset: off1 } = parseTimestamp(body, offset);
|
||||
const rawPriority = body.readUInt8(off1);
|
||||
const priority = parsePriority(rawPriority, ctx);
|
||||
const { value: gps, nextOffset: off2 } = parseGpsElement(body, off1 + 1);
|
||||
|
||||
const { attributes, nextOffset: off3 } = parseIoElements8E(body, off2);
|
||||
|
||||
const position: Position = {
|
||||
device_id: imei,
|
||||
timestamp,
|
||||
latitude: gps.latitude,
|
||||
longitude: gps.longitude,
|
||||
altitude: gps.altitude,
|
||||
angle: gps.angle,
|
||||
speed: gps.speed,
|
||||
satellites: gps.satellites,
|
||||
priority,
|
||||
attributes,
|
||||
};
|
||||
|
||||
return { position, nextOffset: off3 };
|
||||
}
|
||||
|
||||
export const codec8eHandler: CodecDataHandler = {
|
||||
codec_id: 0x8e,
|
||||
|
||||
async handle(body: Buffer, ctx: CodecHandlerContext): Promise<{ recordCount: number }> {
|
||||
// Body layout: [CodecID 1B][N1 1B][records...][N2 1B]
|
||||
const recordCount = body.readUInt8(1);
|
||||
const n2Offset = body.length - 1;
|
||||
|
||||
let offset = 2; // start of first record
|
||||
|
||||
for (let i = 0; i < recordCount; i++) {
|
||||
const { position, nextOffset } = parseOneRecord(body, offset, ctx.imei, ctx);
|
||||
offset = nextOffset;
|
||||
await ctx.publish(position);
|
||||
}
|
||||
|
||||
// Cursor invariant: after all records, cursor must be exactly at N2
|
||||
if (offset !== n2Offset) {
|
||||
throw new Error(
|
||||
`Codec 8E cursor invariant violated: expected offset ${n2Offset} after ${recordCount} records, got ${offset}. ` +
|
||||
`Body length: ${body.length}`,
|
||||
);
|
||||
}
|
||||
|
||||
return { recordCount };
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,53 @@
|
||||
/**
|
||||
* Shared GPS element and timestamp parsers used by all three AVL codecs
|
||||
* (Codec 8, 8E, 16). Extracted here to keep each codec file focused on its
|
||||
* IO element layout differences.
|
||||
*/
|
||||
|
||||
/**
|
||||
* The 15-byte GPS element parsed out of every AVL record.
|
||||
*/
|
||||
export type GpsElement = {
|
||||
readonly longitude: number; // decimal degrees, signed
|
||||
readonly latitude: number; // decimal degrees, signed
|
||||
readonly altitude: number; // meters above sea level, signed
|
||||
readonly angle: number; // heading 0–360°, unsigned
|
||||
readonly satellites: number; // satellites in use
|
||||
readonly speed: number; // km/h; 0x0000 may mean "GPS invalid" — preserved verbatim
|
||||
};
|
||||
|
||||
/**
|
||||
* Parses an 8-byte big-endian unsigned timestamp (milliseconds since UNIX epoch)
|
||||
* starting at `offset`. Uses BigInt read to avoid any precision loss, then
|
||||
* converts to Number for Date construction — values are well within safe range
|
||||
* until ~year 285,000.
|
||||
*/
|
||||
export function parseTimestamp(buf: Buffer, offset: number): { value: Date; nextOffset: number } {
|
||||
const ms = buf.readBigUInt64BE(offset);
|
||||
return {
|
||||
value: new Date(Number(ms)),
|
||||
nextOffset: offset + 8,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses the 15-byte GPS element block:
|
||||
* [Lon 4B][Lat 4B][Alt 2B][Angle 2B][Sats 1B][Speed 2B]
|
||||
*
|
||||
* Longitude and latitude are two's-complement signed 32-bit integers.
|
||||
* Node's readInt32BE interprets the sign bit correctly; dividing by 1e7
|
||||
* converts the fixed-point integer to decimal degrees.
|
||||
*/
|
||||
export function parseGpsElement(buf: Buffer, offset: number): { value: GpsElement; nextOffset: number } {
|
||||
const longitude = buf.readInt32BE(offset) / 1e7;
|
||||
const latitude = buf.readInt32BE(offset + 4) / 1e7;
|
||||
const altitude = buf.readInt16BE(offset + 8);
|
||||
const angle = buf.readUInt16BE(offset + 10);
|
||||
const satellites = buf.readUInt8(offset + 12);
|
||||
const speed = buf.readUInt16BE(offset + 13);
|
||||
|
||||
return {
|
||||
value: { longitude, latitude, altitude, angle, satellites, speed },
|
||||
nextOffset: offset + 15,
|
||||
};
|
||||
}
|
||||
@@ -5,6 +5,9 @@ import { AllowAllAuthority } from './device-authority.js';
|
||||
import { readImeiHandshake, HandshakeError } from './handshake.js';
|
||||
import { BufferedReader, readNextFrame, FrameDropError } from './frame.js';
|
||||
import { CodecRegistry } from './codec/registry.js';
|
||||
import { codec8Handler } from './codec/data/codec8.js';
|
||||
import { codec8eHandler } from './codec/data/codec8e.js';
|
||||
import { codec16Handler } from './codec/data/codec16.js';
|
||||
|
||||
export type TeltonikaAdapterOptions = {
|
||||
readonly port: number;
|
||||
@@ -26,7 +29,14 @@ export type TeltonikaAdapterOptions = {
|
||||
export function createTeltonikaAdapter(options: TeltonikaAdapterOptions): Adapter {
|
||||
const authority: DeviceAuthority = options.deviceAuthority ?? new AllowAllAuthority();
|
||||
const strictDeviceAuth = options.strictDeviceAuth ?? false;
|
||||
const codecRegistry = options.codecRegistry ?? new CodecRegistry();
|
||||
|
||||
// Build default registry with all three Phase 1 data codecs registered.
|
||||
// Callers can pass their own registry (e.g. in tests) to override.
|
||||
const defaultRegistry = new CodecRegistry();
|
||||
defaultRegistry.register(codec8Handler);
|
||||
defaultRegistry.register(codec8eHandler);
|
||||
defaultRegistry.register(codec16Handler);
|
||||
const codecRegistry = options.codecRegistry ?? defaultRegistry;
|
||||
|
||||
return {
|
||||
name: 'teltonika',
|
||||
|
||||
Reference in New Issue
Block a user