Files
edr-platform/apps/edr-freight-api/src/modules/gps-tracking/gt06/gt06.codec.ts
2026-07-07 06:39:02 +00:00

208 lines
6.8 KiB
TypeScript

/**
* GT06 GPS-tracker protocol codec.
*
* Frame: 0x78 0x78 | len(1) | protocol(1) | content(N) | serial(2) | crc(2) | 0x0D 0x0A
* `len` counts protocol..crc (= 5 + N). CRC-ITU (CRC-16/X.25) is computed over
* len..serial (inclusive) and equals the 2 crc bytes.
*/
const START = 0x7878;
const STOP = 0x0d0a;
export const GT06_PROTOCOL = {
LOGIN: 0x01,
LOCATION: 0x12,
HEARTBEAT: 0x13,
STRING: 0x15,
ALARM: 0x16,
ADDRESS_BY_PHONE: 0x1a,
SERVER_COMMAND: 0x80,
} as const;
/** CRC-16/X.25 (a.k.a. CRC-ITU) used by GT06 — reflected, poly 0x8408, init/xorout 0xFFFF. */
export function crcItu(bytes: Buffer): number {
let fcs = 0xffff;
for (const b of bytes) {
fcs ^= b;
for (let i = 0; i < 8; i++) {
fcs = fcs & 1 ? (fcs >> 1) ^ 0x8408 : fcs >> 1;
}
}
return (~fcs) & 0xffff;
}
export interface Gt06Gps {
time: string; // ISO (UTC)
satellites: number;
latitude: number;
longitude: number;
speed: number; // km/h
course: number; // 0-360
positioned: boolean;
}
export interface Gt06Lbs {
mcc: number;
mnc: number;
lac: number;
cellId: number;
}
export interface Gt06Status {
terminalInfo: number;
voltageLevel: number;
gsmLevel: number;
alarm: number; // former byte of alarm/language
charging: boolean;
accOn: boolean;
gpsTracking: boolean;
oilCut: boolean;
}
export type Gt06Packet =
| { type: 'login'; protocol: number; serial: number; imei: string }
| { type: 'location'; protocol: number; serial: number; gps: Gt06Gps; lbs: Gt06Lbs }
| { type: 'heartbeat'; protocol: number; serial: number; status: Gt06Status }
| { type: 'alarm'; protocol: number; serial: number; gps: Gt06Gps; lbs: Gt06Lbs; status: Gt06Status }
| { type: 'unknown'; protocol: number; serial: number };
/** Terminal ID (8 BCD bytes) → 15-digit IMEI (drops the leading pad nibble). */
function decodeImei(buf: Buffer): string {
return buf.toString('hex').replace(/^0/, '');
}
function decodeDateTime(buf: Buffer, off: number): string {
const year = 2000 + buf[off];
const month = buf[off + 1];
const day = buf[off + 2];
const hour = buf[off + 3];
const min = buf[off + 4];
const sec = buf[off + 5];
return new Date(Date.UTC(year, month - 1, day, hour, min, sec)).toISOString();
}
/** Convert a GT06 lat/long raw uint32 to decimal degrees (magnitude only). */
function rawToDegrees(raw: number): number {
return raw / 30000 / 60;
}
function decodeGps(buf: Buffer, off: number): Gt06Gps {
const time = decodeDateTime(buf, off);
const lenSat = buf[off + 6];
const satellites = lenSat & 0x0f;
const latRaw = buf.readUInt32BE(off + 7);
const lonRaw = buf.readUInt32BE(off + 11);
const speed = buf[off + 15];
const cs = buf.readUInt16BE(off + 16);
const hi = (cs >> 8) & 0xff;
const positioned = Boolean(hi & 0x10); // BYTE_1 Bit4
const isWest = Boolean(hi & 0x08); // BYTE_1 Bit3 (1 = West)
const isNorth = Boolean(hi & 0x04); // BYTE_1 Bit2 (1 = North)
const course = cs & 0x03ff; // BYTE_1 Bit1-0 + BYTE_2
let latitude = rawToDegrees(latRaw);
let longitude = rawToDegrees(lonRaw);
if (!isNorth) latitude = -latitude;
if (isWest) longitude = -longitude;
return { time, satellites, latitude, longitude, speed, course, positioned };
}
function decodeStatus(buf: Buffer, off: number): Gt06Status {
const terminalInfo = buf[off];
const voltageLevel = buf[off + 1];
const gsmLevel = buf[off + 2];
const alarm = buf[off + 3]; // alarm/language former byte
return {
terminalInfo,
voltageLevel,
gsmLevel,
alarm,
oilCut: Boolean(terminalInfo & 0x80),
gpsTracking: Boolean(terminalInfo & 0x40),
charging: Boolean(terminalInfo & 0x04),
accOn: Boolean(terminalInfo & 0x02),
};
}
function decodeLbs(buf: Buffer, off: number): Gt06Lbs {
return {
mcc: buf.readUInt16BE(off),
mnc: buf[off + 2],
lac: buf.readUInt16BE(off + 3),
cellId: buf.readUIntBE(off + 5, 3),
};
}
function decodeFrame(frame: Buffer): Gt06Packet | null {
// frame = 78 78 len ...content... serial(2) crc(2) 0D 0A
const len = frame[2];
const protocol = frame[3];
const serialOff = 3 + (len - 4); // after protocol + content, before serial(2)+crc(2)
const serial = frame.readUInt16BE(serialOff);
const contentOff = 4; // start of content (after protocol)
switch (protocol) {
case GT06_PROTOCOL.LOGIN:
return { type: 'login', protocol, serial, imei: decodeImei(frame.subarray(contentOff, contentOff + 8)) };
case GT06_PROTOCOL.LOCATION:
return { type: 'location', protocol, serial, gps: decodeGps(frame, contentOff), lbs: decodeLbs(frame, contentOff + 18) };
case GT06_PROTOCOL.HEARTBEAT:
return { type: 'heartbeat', protocol, serial, status: decodeStatus(frame, contentOff) };
case GT06_PROTOCOL.ALARM: {
const gps = decodeGps(frame, contentOff);
// content: date(6)+lenSat(1)+lat(4)+lng(4)+speed(1)+course(2)=18, lbsLen(1), lbs(8), status(1+1+1+2)
const lbs = decodeLbs(frame, contentOff + 18 + 1);
const status = decodeStatus(frame, contentOff + 18 + 1 + 8);
return { type: 'alarm', protocol, serial, gps, lbs, status };
}
default:
return { type: 'unknown', protocol, serial };
}
}
/**
* Pull all complete frames out of a stream buffer. Returns the decoded packets
* (skipping CRC-failed ones) and the trailing bytes that form a partial frame.
*/
export function parseStream(buffer: Buffer): { packets: Gt06Packet[]; rest: Buffer } {
const packets: Gt06Packet[] = [];
let i = 0;
while (i + 5 <= buffer.length) {
if (buffer.readUInt16BE(i) !== START) {
i += 1; // resync
continue;
}
const len = buffer[i + 2];
const frameLen = 2 + 1 + len + 2; // start + lenByte + (protocol..crc) + stop
if (i + frameLen > buffer.length) break; // incomplete
const frame = buffer.subarray(i, i + frameLen);
if (frame.readUInt16BE(frameLen - 2) === STOP) {
// CRC over len..serial (frame[2 .. frameLen-4]); crc bytes are frameLen-4..frameLen-3.
const crcCalc = crcItu(frame.subarray(2, frameLen - 4));
const crcRecv = frame.readUInt16BE(frameLen - 4);
if (crcCalc === crcRecv) {
const pkt = decodeFrame(frame);
if (pkt) packets.push(pkt);
}
i += frameLen;
} else {
i += 1; // bad frame, resync
}
}
return { packets, rest: buffer.subarray(i) };
}
/** Build a server → terminal ACK (login/heartbeat/alarm) echoing the serial. */
export function buildAck(protocol: number, serial: number): Buffer {
const body = Buffer.alloc(3); // protocol + serial(2)
body[0] = protocol;
body.writeUInt16BE(serial, 1);
const len = body.length + 2; // + crc(2)
const forCrc = Buffer.concat([Buffer.from([len]), body]);
const crc = crcItu(forCrc);
return Buffer.concat([
Buffer.from([0x78, 0x78, len]),
body,
Buffer.from([(crc >> 8) & 0xff, crc & 0xff, 0x0d, 0x0a]),
]);
}