Skip to content

Commit 8937096

Browse files
authored
Merge pull request #36 from freshworkstudio/v2
v2.0.0: TypeScript rewrite with full v1 compatibility
2 parents 0844102 + 42a61d4 commit 8937096

54 files changed

Lines changed: 6603 additions & 1742 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/workflows/ci.yml

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
name: CI
2+
3+
on:
4+
push:
5+
branches: [master]
6+
pull_request:
7+
8+
jobs:
9+
test:
10+
runs-on: ubuntu-latest
11+
strategy:
12+
matrix:
13+
node-version: [18, 20, 22]
14+
steps:
15+
- uses: actions/checkout@v4
16+
with:
17+
persist-credentials: false
18+
- uses: actions/setup-node@v4
19+
with:
20+
node-version: ${{ matrix.node-version }}
21+
cache: npm
22+
- run: npm ci
23+
- run: npm run typecheck
24+
- run: npm run build
25+
- run: npm test
26+
27+
# ESLint 10 requires Node >= 20.19, above the package's runtime floor (18.17),
28+
# so linting runs once on a modern Node instead of inside the matrix.
29+
lint:
30+
runs-on: ubuntu-latest
31+
steps:
32+
- uses: actions/checkout@v4
33+
with:
34+
persist-credentials: false
35+
- uses: actions/setup-node@v4
36+
with:
37+
node-version: 22
38+
cache: npm
39+
- run: npm ci
40+
- run: npm run lint

.github/workflows/release.yml

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
name: Release
2+
3+
on:
4+
release:
5+
types: [published]
6+
7+
jobs:
8+
publish:
9+
runs-on: ubuntu-latest
10+
permissions:
11+
contents: read
12+
id-token: write
13+
steps:
14+
- uses: actions/checkout@v4
15+
with:
16+
persist-credentials: false
17+
- uses: actions/setup-node@v4
18+
with:
19+
node-version: 22
20+
cache: npm
21+
registry-url: https://registry.npmjs.org
22+
- run: npm ci
23+
- run: npm run typecheck
24+
- run: npm run lint
25+
- run: npm run build
26+
- run: npm test
27+
- run: npm publish --provenance --access public
28+
env:
29+
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}

.gitignore

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,4 @@
1-
npm-debug.log
2-
node_modules
1+
node_modules
2+
dist
3+
coverage
4+
*.log

.jscsrc

Lines changed: 0 additions & 5 deletions
This file was deleted.

MIGRATION.md

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
# Migrating from v1 to v2
2+
3+
**TL;DR: you probably don't need to change anything.** v2 ships a full
4+
compatibility layer: the v1 API (`gps.server(...)`, snake_case events,
5+
`login_authorized(true)`, custom v1 adapters) keeps working unchanged.
6+
The only hard requirement is **Node.js >= 18.17**.
7+
8+
The v1 API is deprecated and will be removed in v3, so new code should use
9+
the v2 API. Using any legacy entry point prints one `DeprecationWarning`
10+
per process (silence it with `node --no-deprecation`).
11+
12+
## What you get by migrating
13+
14+
- TypeScript types for everything (events included).
15+
- Proper TCP framing: positions no longer get lost/corrupted when packets
16+
arrive fragmented or coalesced (v1 assumed 1 socket event = 1 packet).
17+
- No more crashes on `ECONNRESET` (v1 issue #12).
18+
- Idle connections are cleaned up automatically (v1 issue #31).
19+
- Zero runtime dependencies (v1 needed `crc`, `crc16-ccitt-node`,
20+
`node.extend` and an undeclared `crc-itu`).
21+
- New protocols: SinoTrack ST-901 / H02 (`adapters.ST901`) and Concox
22+
GK309 (`adapters.GK309`), plus correct CRC handling for GT06.
23+
24+
## Side-by-side
25+
26+
v1:
27+
28+
```js
29+
var gps = require('gps-tracking');
30+
31+
var server = gps.server({ port: 8090, device_adapter: 'TK103', debug: true }, function (device, connection) {
32+
device.on('login_request', function (device_id, msg_parts) {
33+
this.login_authorized(true);
34+
});
35+
device.on('ping', function (data) {
36+
console.log(data.latitude, data.longitude);
37+
});
38+
device.on('alarm', function (alarm_code, alarm_data, msg_data) {
39+
console.log(alarm_code);
40+
});
41+
});
42+
```
43+
44+
v2:
45+
46+
```ts
47+
import { createServer, adapters } from 'gps-tracking';
48+
49+
const server = createServer({ port: 8090, adapter: adapters.TK103, logger: console });
50+
51+
server.on('connection', (device) => {
52+
device.on('loginRequest', () => device.acceptLogin());
53+
device.on('ping', (position) => console.log(position.latitude, position.longitude));
54+
device.on('alarm', (alarm) => console.log(alarm.code, alarm.message));
55+
});
56+
57+
await server.listen();
58+
```
59+
60+
## API mapping
61+
62+
| v1 (deprecated, still works) | v2 |
63+
| ------------------------------------------ | ------------------------------------------------------ |
64+
| `gps.server(opts, callback)` | `createServer(opts)` + `server.on('connection', ...)` |
65+
| `device_adapter: 'TK103'` (string) | `adapter: adapters.TK103` (class) |
66+
| `device_adapter: require('./my-adapter')` | subclass of `BaseAdapter` (see README) |
67+
| `debug: true` | `logger: console` |
68+
| auto-listen on creation | `await server.listen()` (explicit) |
69+
| `device.on('login_request', cb)` | `device.on('loginRequest', cb)` |
70+
| `device.login_authorized(true / false)` | `device.acceptLogin()` / `device.rejectLogin()` |
71+
| `device.on('ping', (data) => ...)` | same event, typed `GpsPosition` payload |
72+
| `device.on('alarm', (code, data, parts))` | `device.on('alarm', (alarm, packet))` |
73+
| `device.getUID()` / `device.setUID()` | `device.id` (read-only) |
74+
| `device.getName()` / `device.setName()` | `device.name` |
75+
| `device.loged` | `device.isAuthenticated` |
76+
| `device.set_refresh_time(i, d)` | `device.setRefreshInterval(i, d)` |
77+
| `server.find_device(id)` | `server.getDevice(id)` |
78+
| `server.send_to(id, msg)` | `server.sendTo(id, msg)` |
79+
| `server.setDebug(v)` / `getDebug()` | `logger` option |
80+
| `connection` callback argument | `device.socket` |
81+
82+
## Behaviour changes to be aware of
83+
84+
Even with the compat layer, a few things behave differently (for the better):
85+
86+
- **GT06/GT02A device ids** no longer include the leading `0` of the
87+
16-digit BCD field: you now get the plain 15-digit IMEI
88+
(`0123456789012345``123456789012345`). Update stored ids if needed.
89+
- **TK510 dates** were read as `YYMMDD` in v1; the payload is RMC-style
90+
`DDMMYY` and v2 parses it accordingly. TK510 speeds are now converted
91+
from knots to km/h.
92+
- **Position timestamps** are parsed as UTC `Date`s (v1 used the server's
93+
local timezone).
94+
- **Corrupt frames** no longer throw strings that could crash the process:
95+
they emit a `parseError` event (or are discarded) and the connection
96+
keeps working.
97+
- **GT06 acks** now echo the device's real serial number and use the
98+
correct CRC-16/X-25, so command responses work on real hardware
99+
(v1 sent a hardcoded ack that only matched serial 1).
100+
- **Custom v1 adapters** run through a passthrough framer that preserves
101+
the v1 "1 socket event = 1 packet" behaviour, so they work exactly as
102+
before — but they don't benefit from the TCP fragmentation fix. Port
103+
them to `BaseAdapter` to get proper framing.

README.md

Lines changed: 185 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,185 @@
1+
# GPS Tracking Server for Node.js
2+
3+
[![CI](https://github.com/freshworkstudio/gps-tracking-nodejs/actions/workflows/ci.yml/badge.svg)](https://github.com/freshworkstudio/gps-tracking-nodejs/actions/workflows/ci.yml)
4+
[![npm](https://img.shields.io/npm/v/gps-tracking.svg)](https://www.npmjs.com/package/gps-tracking)
5+
[![License](https://img.shields.io/badge/license-MIT-brightgreen.svg)](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

Comments
 (0)