import { Injectable, Logger, OnApplicationBootstrap, OnModuleDestroy } from '@nestjs/common'; import * as net from 'net'; import { GpsTrackingService } from '../gps-tracking.service'; import { buildAck, GT06_PROTOCOL, parseStream } from './gt06.codec'; interface Session { buffer: Buffer; imei: string | null; } const MAX_BUFFER = 64 * 1024; /** * Raw TCP listener for GT06 GPS trackers. Trackers open a socket, send a login * (IMEI), then stream location/heartbeat/alarm packets; we decode, persist via * {@link GpsTrackingService}, and ACK login/heartbeat/alarm so the device keeps * the connection alive. Disabled when GT06_TCP_PORT=0. */ @Injectable() export class Gt06Server implements OnApplicationBootstrap, OnModuleDestroy { private readonly logger = new Logger(Gt06Server.name); private server?: net.Server; private readonly sessions = new Map(); constructor(private readonly gps: GpsTrackingService) {} onApplicationBootstrap(): void { const port = Number(process.env.GT06_TCP_PORT ?? 5023); if (!port) { this.logger.log('GT06 TCP listener disabled (GT06_TCP_PORT=0)'); return; } const host = process.env.GT06_TCP_HOST ?? '0.0.0.0'; this.server = net.createServer((socket) => this.onConnection(socket)); this.server.on('error', (err) => this.logger.error(`GT06 server error: ${String(err)}`)); this.server.listen(port, host, () => this.logger.log(`GT06 GPS tracker listener on ${host}:${port}`)); } onModuleDestroy(): void { for (const socket of this.sessions.keys()) socket.destroy(); this.sessions.clear(); this.server?.close(); } private onConnection(socket: net.Socket): void { this.sessions.set(socket, { buffer: Buffer.alloc(0), imei: null }); socket.on('data', (chunk) => void this.onData(socket, chunk)); socket.on('error', () => this.sessions.delete(socket)); socket.on('close', () => this.sessions.delete(socket)); } private async onData(socket: net.Socket, chunk: Buffer): Promise { const session = this.sessions.get(socket); if (!session) return; session.buffer = Buffer.concat([session.buffer, chunk]); if (session.buffer.length > MAX_BUFFER) session.buffer = Buffer.alloc(0); // drop garbage const { packets, rest } = parseStream(session.buffer); session.buffer = rest; for (const pkt of packets) { try { await this.handle(socket, session, pkt); } catch (err) { this.logger.error(`Failed to handle GT06 packet (${pkt.type}): ${String(err)}`); } } } private async handle( socket: net.Socket, session: Session, pkt: ReturnType['packets'][number], ): Promise { switch (pkt.type) { case 'login': session.imei = pkt.imei; await this.gps.handleLogin(pkt.imei); socket.write(buildAck(GT06_PROTOCOL.LOGIN, pkt.serial)); break; case 'heartbeat': if (session.imei) await this.gps.handleHeartbeat(session.imei, pkt.status); socket.write(buildAck(GT06_PROTOCOL.HEARTBEAT, pkt.serial)); break; case 'location': if (session.imei) await this.gps.handleFix(session.imei, pkt.gps); break; case 'alarm': if (session.imei) await this.gps.handleFix(session.imei, pkt.gps, pkt.status.alarm, pkt.status); socket.write(buildAck(GT06_PROTOCOL.ALARM, pkt.serial)); break; default: break; } } }