|
| 1 | +# GPS Tracking Server for Node.js |
| 2 | + |
| 3 | +[](https://github.com/freshworkstudio/gps-tracking-nodejs/actions/workflows/ci.yml) |
| 4 | +[](https://www.npmjs.com/package/gps-tracking) |
| 5 | +[](LICENSE) |
| 6 | + |
| 7 | +Create TCP listeners for GPS tracking devices in a few lines. Written in |
| 8 | +TypeScript, zero runtime dependencies, dual ESM/CJS, Node.js >= 18.17. |
| 9 | + |
| 10 | +```ts |
| 11 | +import { createServer, adapters } from 'gps-tracking'; |
| 12 | + |
| 13 | +const server = createServer({ port: 8090, adapter: adapters.TK103 }); |
| 14 | + |
| 15 | +server.on('connection', (device) => { |
| 16 | + device.on('loginRequest', () => device.acceptLogin()); |
| 17 | + |
| 18 | + device.on('ping', (position) => { |
| 19 | + console.log(`${device.id}: ${position.latitude}, ${position.longitude} @ ${position.speed} km/h`); |
| 20 | + }); |
| 21 | + |
| 22 | + device.on('alarm', (alarm) => { |
| 23 | + console.log(`ALARM from ${device.id}: ${alarm.code} — ${alarm.message}`); |
| 24 | + }); |
| 25 | +}); |
| 26 | + |
| 27 | +await server.listen(); |
| 28 | +``` |
| 29 | + |
| 30 | +> **Coming from v1?** Your code still works unchanged — the whole v1 API |
| 31 | +> (`gps.server(...)`, snake_case events) ships as a deprecated compatibility |
| 32 | +> layer. See [MIGRATION.md](MIGRATION.md). |
| 33 | +
|
| 34 | +## Installation |
| 35 | + |
| 36 | +```bash |
| 37 | +npm install gps-tracking |
| 38 | +``` |
| 39 | + |
| 40 | +## Supported devices |
| 41 | + |
| 42 | +| Adapter | Protocol | Devices | |
| 43 | +| ------------------ | -------- | --------------------------------------------------- | |
| 44 | +| `adapters.TK103` | GPS103 | TK103 and clones | |
| 45 | +| `adapters.GT06` | GT06 | Concox GT06, GT06N and clones | |
| 46 | +| `adapters.GK309` | GT06 | Concox GK309, GK301, GK306 | |
| 47 | +| `adapters.ST901` | H02 | SinoTrack ST-901 and other H02 devices (`adapters.H02` is an alias) | |
| 48 | +| `adapters.GT02A` | GT02 | GT02A | |
| 49 | +| `adapters.TK510` | TK510 | TK510 | |
| 50 | + |
| 51 | +Each server instance listens for **one** protocol. To support several device |
| 52 | +models at once, run one server per protocol on different ports. |
| 53 | + |
| 54 | +## How it works |
| 55 | + |
| 56 | +1. `createServer({ port, adapter })` starts a TCP server for one protocol. |
| 57 | +2. Every connection becomes a `Device` that emits typed events. |
| 58 | +3. The adapter handles framing (TCP fragmentation included), parsing and |
| 59 | + protocol acks (login, heartbeats, alarms) automatically. |
| 60 | + |
| 61 | +### Server options |
| 62 | + |
| 63 | +```ts |
| 64 | +createServer({ |
| 65 | + adapter: adapters.GT06, // required: adapter class |
| 66 | + port: 8090, // default 8090 (0 = random free port) |
| 67 | + host: '0.0.0.0', // optional bind address |
| 68 | + connectionTimeoutMs: 120_000, // destroy idle connections (0 disables) |
| 69 | + maxFrameLength: 4096, // discard corrupt/oversized frames |
| 70 | + logger: console, // any {debug,info,warn,error}; default: silent |
| 71 | +}); |
| 72 | +``` |
| 73 | + |
| 74 | +### Server API |
| 75 | + |
| 76 | +```ts |
| 77 | +const address = await server.listen(); // AddressInfo (address.port) |
| 78 | +server.getDevice('865205035331981'); // Device | undefined |
| 79 | +server.devices; // ReadonlyMap<string, Device> |
| 80 | +server.sendTo('865205035331981', cmd); // boolean |
| 81 | +await server.close(); |
| 82 | +``` |
| 83 | + |
| 84 | +Events: `listening`, `connection`, `disconnect`, `error`, `close`. |
| 85 | + |
| 86 | +### Device API |
| 87 | + |
| 88 | +```ts |
| 89 | +device.id; // IMEI / protocol id (undefined until identified) |
| 90 | +device.isAuthenticated; // true after acceptLogin (or first packet for H02) |
| 91 | +device.acceptLogin(); // accept a pending loginRequest |
| 92 | +device.rejectLogin({ disconnect: true }); |
| 93 | +device.send(data); // raw Buffer | string to the device |
| 94 | +device.setRefreshInterval(30, 3600); // when the protocol supports it |
| 95 | +device.disconnect(); |
| 96 | +device.socket; // the underlying net.Socket |
| 97 | +``` |
| 98 | + |
| 99 | +Events: `loginRequest`, `login`, `identified`, `ping`, `alarm`, `packet`, |
| 100 | +`parseError`, `error`, `timeout`, `disconnect`. |
| 101 | + |
| 102 | +`ping` delivers a typed `GpsPosition`: |
| 103 | + |
| 104 | +```ts |
| 105 | +interface GpsPosition { |
| 106 | + latitude: number; |
| 107 | + longitude: number; |
| 108 | + time: Date; // UTC |
| 109 | + valid?: boolean; // GPS fix validity |
| 110 | + speed?: number; // km/h |
| 111 | + orientation?: number; // degrees, north = 0 |
| 112 | + mileage?: number; |
| 113 | + satellites?: number; |
| 114 | + extra?: Record<string, unknown>; // protocol-specific (LBS, ignition, ...) |
| 115 | +} |
| 116 | +``` |
| 117 | + |
| 118 | +## Writing a custom adapter |
| 119 | + |
| 120 | +Extend `BaseAdapter`: choose a framer (how packets are delimited on the TCP |
| 121 | +stream) and parse each frame into a `ParsedPacket`. |
| 122 | + |
| 123 | +```ts |
| 124 | +import { BaseAdapter, DelimiterFramer, PacketParseError } from 'gps-tracking'; |
| 125 | +import type { Framer, FramerOptions, ParsedPacket } from 'gps-tracking'; |
| 126 | + |
| 127 | +export class MyAdapter extends BaseAdapter { |
| 128 | + static override readonly protocol = 'MYPROTO'; |
| 129 | + static override readonly modelName = 'MY-DEVICE'; |
| 130 | + |
| 131 | + createFramer(options: FramerOptions): Framer { |
| 132 | + // Also available: LengthPrefixedFramer for binary marker+length protocols. |
| 133 | + return new DelimiterFramer({ start: 0x24, end: 0x0a, ...options }); // $ ... \n |
| 134 | + } |
| 135 | + |
| 136 | + parsePacket(frame: Buffer): ParsedPacket { |
| 137 | + const [id, cmd, payload] = frame.toString().slice(1, -1).split(','); |
| 138 | + if (!id || !cmd) throw new PacketParseError('malformed frame'); |
| 139 | + |
| 140 | + if (cmd === 'LOGIN') return { cmd, raw: frame, action: 'loginRequest', deviceId: id }; |
| 141 | + if (cmd === 'POS') { |
| 142 | + return { |
| 143 | + cmd, raw: frame, deviceId: id, action: 'ping', |
| 144 | + position: { latitude: 1, longitude: 2, time: new Date() /* parse payload */ }, |
| 145 | + }; |
| 146 | + } |
| 147 | + return { cmd, raw: frame, deviceId: id, action: 'other' }; |
| 148 | + } |
| 149 | + |
| 150 | + authorize(): void { |
| 151 | + this.device.send('$OK\n'); // login ack |
| 152 | + } |
| 153 | +} |
| 154 | +``` |
| 155 | + |
| 156 | +Use it with `createServer({ adapter: MyAdapter, ... })`. Protocols without a |
| 157 | +login handshake (like H02) can set `static override readonly requiresLogin = false`; |
| 158 | +devices then authenticate automatically with their first valid packet. |
| 159 | + |
| 160 | +Optional hooks: `requestLogin`, `ackPing`, `ackAlarm`, `ackHeartbeat`, |
| 161 | +`handleCommand`, `setRefreshInterval`. One adapter instance is created per |
| 162 | +connection, so instance fields are safe for per-connection state. |
| 163 | + |
| 164 | +## GPS emulator |
| 165 | + |
| 166 | +To test without hardware, check the companion emulator: |
| 167 | +[freshworkstudio/gps-tracking-emulator](https://github.com/freshworkstudio/gps-tracking-emulator) |
| 168 | + |
| 169 | +## Contributing |
| 170 | + |
| 171 | +```bash |
| 172 | +npm install |
| 173 | +npm test # vitest (unit + TCP integration) |
| 174 | +npm run typecheck |
| 175 | +npm run lint |
| 176 | +npm run build # tsup → dist (ESM + CJS + types) |
| 177 | +``` |
| 178 | + |
| 179 | +Protocol fixtures in `test/` come from official protocol documents |
| 180 | +(Concox GK309 V1.8, HuaSunTeK H02 V1.0.5) and real captured packets — please |
| 181 | +include fixtures with new adapters. |
| 182 | + |
| 183 | +## License |
| 184 | + |
| 185 | +[MIT](LICENSE) |
0 commit comments