mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-09-02 15:03:39 +00:00
208 lines
6.8 KiB
TypeScript
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]),
|
|
]);
|
|
}
|