-
Notifications
You must be signed in to change notification settings - Fork 163
v2.0.0: TypeScript rewrite with full v1 compatibility #36
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
2 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,40 @@ | ||
| name: CI | ||
|
|
||
| on: | ||
| push: | ||
| branches: [master] | ||
| pull_request: | ||
|
|
||
| jobs: | ||
| test: | ||
| runs-on: ubuntu-latest | ||
| strategy: | ||
| matrix: | ||
| node-version: [18, 20, 22] | ||
| steps: | ||
| - uses: actions/checkout@v4 | ||
| with: | ||
| persist-credentials: false | ||
| - uses: actions/setup-node@v4 | ||
| with: | ||
| node-version: ${{ matrix.node-version }} | ||
| cache: npm | ||
| - run: npm ci | ||
| - run: npm run typecheck | ||
| - run: npm run build | ||
| - run: npm test | ||
|
|
||
| # ESLint 10 requires Node >= 20.19, above the package's runtime floor (18.17), | ||
| # so linting runs once on a modern Node instead of inside the matrix. | ||
| lint: | ||
| runs-on: ubuntu-latest | ||
| steps: | ||
| - uses: actions/checkout@v4 | ||
| with: | ||
| persist-credentials: false | ||
| - uses: actions/setup-node@v4 | ||
| with: | ||
| node-version: 22 | ||
| cache: npm | ||
| - run: npm ci | ||
| - run: npm run lint | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,29 @@ | ||
| name: Release | ||
|
|
||
| on: | ||
| release: | ||
| types: [published] | ||
|
|
||
| jobs: | ||
| publish: | ||
| runs-on: ubuntu-latest | ||
| permissions: | ||
| contents: read | ||
| id-token: write | ||
| steps: | ||
| - uses: actions/checkout@v4 | ||
|
coderabbitai[bot] marked this conversation as resolved.
|
||
| with: | ||
| persist-credentials: false | ||
| - uses: actions/setup-node@v4 | ||
| with: | ||
| node-version: 22 | ||
| cache: npm | ||
| registry-url: https://registry.npmjs.org | ||
| - run: npm ci | ||
| - run: npm run typecheck | ||
| - run: npm run lint | ||
| - run: npm run build | ||
| - run: npm test | ||
| - run: npm publish --provenance --access public | ||
| env: | ||
| NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,2 +1,4 @@ | ||
| npm-debug.log | ||
| node_modules | ||
| node_modules | ||
| dist | ||
| coverage | ||
| *.log |
This file was deleted.
Oops, something went wrong.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,103 @@ | ||
| # Migrating from v1 to v2 | ||
|
|
||
| **TL;DR: you probably don't need to change anything.** v2 ships a full | ||
| compatibility layer: the v1 API (`gps.server(...)`, snake_case events, | ||
| `login_authorized(true)`, custom v1 adapters) keeps working unchanged. | ||
| The only hard requirement is **Node.js >= 18.17**. | ||
|
|
||
| The v1 API is deprecated and will be removed in v3, so new code should use | ||
| the v2 API. Using any legacy entry point prints one `DeprecationWarning` | ||
| per process (silence it with `node --no-deprecation`). | ||
|
|
||
| ## What you get by migrating | ||
|
|
||
| - TypeScript types for everything (events included). | ||
| - Proper TCP framing: positions no longer get lost/corrupted when packets | ||
| arrive fragmented or coalesced (v1 assumed 1 socket event = 1 packet). | ||
| - No more crashes on `ECONNRESET` (v1 issue #12). | ||
| - Idle connections are cleaned up automatically (v1 issue #31). | ||
| - Zero runtime dependencies (v1 needed `crc`, `crc16-ccitt-node`, | ||
| `node.extend` and an undeclared `crc-itu`). | ||
| - New protocols: SinoTrack ST-901 / H02 (`adapters.ST901`) and Concox | ||
| GK309 (`adapters.GK309`), plus correct CRC handling for GT06. | ||
|
|
||
| ## Side-by-side | ||
|
|
||
| v1: | ||
|
|
||
| ```js | ||
| var gps = require('gps-tracking'); | ||
|
|
||
| var server = gps.server({ port: 8090, device_adapter: 'TK103', debug: true }, function (device, connection) { | ||
| device.on('login_request', function (device_id, msg_parts) { | ||
| this.login_authorized(true); | ||
| }); | ||
| device.on('ping', function (data) { | ||
| console.log(data.latitude, data.longitude); | ||
| }); | ||
| device.on('alarm', function (alarm_code, alarm_data, msg_data) { | ||
| console.log(alarm_code); | ||
| }); | ||
| }); | ||
| ``` | ||
|
|
||
| v2: | ||
|
|
||
| ```ts | ||
| import { createServer, adapters } from 'gps-tracking'; | ||
|
|
||
| const server = createServer({ port: 8090, adapter: adapters.TK103, logger: console }); | ||
|
|
||
| server.on('connection', (device) => { | ||
| device.on('loginRequest', () => device.acceptLogin()); | ||
| device.on('ping', (position) => console.log(position.latitude, position.longitude)); | ||
| device.on('alarm', (alarm) => console.log(alarm.code, alarm.message)); | ||
| }); | ||
|
|
||
| await server.listen(); | ||
| ``` | ||
|
|
||
| ## API mapping | ||
|
|
||
| | v1 (deprecated, still works) | v2 | | ||
| | ------------------------------------------ | ------------------------------------------------------ | | ||
| | `gps.server(opts, callback)` | `createServer(opts)` + `server.on('connection', ...)` | | ||
| | `device_adapter: 'TK103'` (string) | `adapter: adapters.TK103` (class) | | ||
| | `device_adapter: require('./my-adapter')` | subclass of `BaseAdapter` (see README) | | ||
| | `debug: true` | `logger: console` | | ||
| | auto-listen on creation | `await server.listen()` (explicit) | | ||
| | `device.on('login_request', cb)` | `device.on('loginRequest', cb)` | | ||
| | `device.login_authorized(true / false)` | `device.acceptLogin()` / `device.rejectLogin()` | | ||
| | `device.on('ping', (data) => ...)` | same event, typed `GpsPosition` payload | | ||
| | `device.on('alarm', (code, data, parts))` | `device.on('alarm', (alarm, packet))` | | ||
| | `device.getUID()` / `device.setUID()` | `device.id` (read-only) | | ||
| | `device.getName()` / `device.setName()` | `device.name` | | ||
| | `device.loged` | `device.isAuthenticated` | | ||
| | `device.set_refresh_time(i, d)` | `device.setRefreshInterval(i, d)` | | ||
| | `server.find_device(id)` | `server.getDevice(id)` | | ||
| | `server.send_to(id, msg)` | `server.sendTo(id, msg)` | | ||
| | `server.setDebug(v)` / `getDebug()` | `logger` option | | ||
| | `connection` callback argument | `device.socket` | | ||
|
|
||
| ## Behaviour changes to be aware of | ||
|
|
||
| Even with the compat layer, a few things behave differently (for the better): | ||
|
|
||
| - **GT06/GT02A device ids** no longer include the leading `0` of the | ||
| 16-digit BCD field: you now get the plain 15-digit IMEI | ||
| (`0123456789012345` → `123456789012345`). Update stored ids if needed. | ||
| - **TK510 dates** were read as `YYMMDD` in v1; the payload is RMC-style | ||
| `DDMMYY` and v2 parses it accordingly. TK510 speeds are now converted | ||
| from knots to km/h. | ||
| - **Position timestamps** are parsed as UTC `Date`s (v1 used the server's | ||
| local timezone). | ||
| - **Corrupt frames** no longer throw strings that could crash the process: | ||
| they emit a `parseError` event (or are discarded) and the connection | ||
| keeps working. | ||
| - **GT06 acks** now echo the device's real serial number and use the | ||
| correct CRC-16/X-25, so command responses work on real hardware | ||
| (v1 sent a hardcoded ack that only matched serial 1). | ||
| - **Custom v1 adapters** run through a passthrough framer that preserves | ||
| the v1 "1 socket event = 1 packet" behaviour, so they work exactly as | ||
| before — but they don't benefit from the TCP fragmentation fix. Port | ||
| them to `BaseAdapter` to get proper framing. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,185 @@ | ||
| # GPS Tracking Server for Node.js | ||
|
|
||
| [](https://github.com/freshworkstudio/gps-tracking-nodejs/actions/workflows/ci.yml) | ||
| [](https://www.npmjs.com/package/gps-tracking) | ||
| [](LICENSE) | ||
|
|
||
| Create TCP listeners for GPS tracking devices in a few lines. Written in | ||
| TypeScript, zero runtime dependencies, dual ESM/CJS, Node.js >= 18.17. | ||
|
|
||
| ```ts | ||
| import { createServer, adapters } from 'gps-tracking'; | ||
|
|
||
| const server = createServer({ port: 8090, adapter: adapters.TK103 }); | ||
|
|
||
| server.on('connection', (device) => { | ||
| device.on('loginRequest', () => device.acceptLogin()); | ||
|
|
||
| device.on('ping', (position) => { | ||
| console.log(`${device.id}: ${position.latitude}, ${position.longitude} @ ${position.speed} km/h`); | ||
| }); | ||
|
|
||
| device.on('alarm', (alarm) => { | ||
| console.log(`ALARM from ${device.id}: ${alarm.code} — ${alarm.message}`); | ||
| }); | ||
| }); | ||
|
|
||
| await server.listen(); | ||
| ``` | ||
|
|
||
| > **Coming from v1?** Your code still works unchanged — the whole v1 API | ||
| > (`gps.server(...)`, snake_case events) ships as a deprecated compatibility | ||
| > layer. See [MIGRATION.md](MIGRATION.md). | ||
|
|
||
| ## Installation | ||
|
|
||
| ```bash | ||
| npm install gps-tracking | ||
| ``` | ||
|
|
||
| ## Supported devices | ||
|
|
||
| | Adapter | Protocol | Devices | | ||
| | ------------------ | -------- | --------------------------------------------------- | | ||
| | `adapters.TK103` | GPS103 | TK103 and clones | | ||
| | `adapters.GT06` | GT06 | Concox GT06, GT06N and clones | | ||
| | `adapters.GK309` | GT06 | Concox GK309, GK301, GK306 | | ||
| | `adapters.ST901` | H02 | SinoTrack ST-901 and other H02 devices (`adapters.H02` is an alias) | | ||
| | `adapters.GT02A` | GT02 | GT02A | | ||
| | `adapters.TK510` | TK510 | TK510 | | ||
|
|
||
| Each server instance listens for **one** protocol. To support several device | ||
| models at once, run one server per protocol on different ports. | ||
|
|
||
| ## How it works | ||
|
|
||
| 1. `createServer({ port, adapter })` starts a TCP server for one protocol. | ||
| 2. Every connection becomes a `Device` that emits typed events. | ||
| 3. The adapter handles framing (TCP fragmentation included), parsing and | ||
| protocol acks (login, heartbeats, alarms) automatically. | ||
|
|
||
| ### Server options | ||
|
|
||
| ```ts | ||
| createServer({ | ||
| adapter: adapters.GT06, // required: adapter class | ||
| port: 8090, // default 8090 (0 = random free port) | ||
| host: '0.0.0.0', // optional bind address | ||
| connectionTimeoutMs: 120_000, // destroy idle connections (0 disables) | ||
| maxFrameLength: 4096, // discard corrupt/oversized frames | ||
| logger: console, // any {debug,info,warn,error}; default: silent | ||
| }); | ||
| ``` | ||
|
|
||
| ### Server API | ||
|
|
||
| ```ts | ||
| const address = await server.listen(); // AddressInfo (address.port) | ||
| server.getDevice('865205035331981'); // Device | undefined | ||
| server.devices; // ReadonlyMap<string, Device> | ||
| server.sendTo('865205035331981', cmd); // boolean | ||
| await server.close(); | ||
| ``` | ||
|
|
||
| Events: `listening`, `connection`, `disconnect`, `error`, `close`. | ||
|
|
||
| ### Device API | ||
|
|
||
| ```ts | ||
| device.id; // IMEI / protocol id (undefined until identified) | ||
| device.isAuthenticated; // true after acceptLogin (or first packet for H02) | ||
| device.acceptLogin(); // accept a pending loginRequest | ||
| device.rejectLogin({ disconnect: true }); | ||
| device.send(data); // raw Buffer | string to the device | ||
| device.setRefreshInterval(30, 3600); // when the protocol supports it | ||
| device.disconnect(); | ||
| device.socket; // the underlying net.Socket | ||
| ``` | ||
|
|
||
| Events: `loginRequest`, `login`, `identified`, `ping`, `alarm`, `packet`, | ||
| `parseError`, `error`, `timeout`, `disconnect`. | ||
|
|
||
| `ping` delivers a typed `GpsPosition`: | ||
|
|
||
| ```ts | ||
| interface GpsPosition { | ||
| latitude: number; | ||
| longitude: number; | ||
| time: Date; // UTC | ||
| valid?: boolean; // GPS fix validity | ||
| speed?: number; // km/h | ||
| orientation?: number; // degrees, north = 0 | ||
| mileage?: number; | ||
| satellites?: number; | ||
| extra?: Record<string, unknown>; // protocol-specific (LBS, ignition, ...) | ||
| } | ||
| ``` | ||
|
|
||
| ## Writing a custom adapter | ||
|
|
||
| Extend `BaseAdapter`: choose a framer (how packets are delimited on the TCP | ||
| stream) and parse each frame into a `ParsedPacket`. | ||
|
|
||
| ```ts | ||
| import { BaseAdapter, DelimiterFramer, PacketParseError } from 'gps-tracking'; | ||
| import type { Framer, FramerOptions, ParsedPacket } from 'gps-tracking'; | ||
|
|
||
| export class MyAdapter extends BaseAdapter { | ||
| static override readonly protocol = 'MYPROTO'; | ||
| static override readonly modelName = 'MY-DEVICE'; | ||
|
|
||
| createFramer(options: FramerOptions): Framer { | ||
| // Also available: LengthPrefixedFramer for binary marker+length protocols. | ||
| return new DelimiterFramer({ start: 0x24, end: 0x0a, ...options }); // $ ... \n | ||
| } | ||
|
|
||
| parsePacket(frame: Buffer): ParsedPacket { | ||
| const [id, cmd, payload] = frame.toString().slice(1, -1).split(','); | ||
| if (!id || !cmd) throw new PacketParseError('malformed frame'); | ||
|
|
||
| if (cmd === 'LOGIN') return { cmd, raw: frame, action: 'loginRequest', deviceId: id }; | ||
| if (cmd === 'POS') { | ||
| return { | ||
| cmd, raw: frame, deviceId: id, action: 'ping', | ||
| position: { latitude: 1, longitude: 2, time: new Date() /* parse payload */ }, | ||
| }; | ||
| } | ||
| return { cmd, raw: frame, deviceId: id, action: 'other' }; | ||
| } | ||
|
|
||
| authorize(): void { | ||
| this.device.send('$OK\n'); // login ack | ||
| } | ||
| } | ||
| ``` | ||
|
|
||
| Use it with `createServer({ adapter: MyAdapter, ... })`. Protocols without a | ||
| login handshake (like H02) can set `static override readonly requiresLogin = false`; | ||
| devices then authenticate automatically with their first valid packet. | ||
|
|
||
| Optional hooks: `requestLogin`, `ackPing`, `ackAlarm`, `ackHeartbeat`, | ||
| `handleCommand`, `setRefreshInterval`. One adapter instance is created per | ||
| connection, so instance fields are safe for per-connection state. | ||
|
|
||
| ## GPS emulator | ||
|
|
||
| To test without hardware, check the companion emulator: | ||
| [freshworkstudio/gps-tracking-emulator](https://github.com/freshworkstudio/gps-tracking-emulator) | ||
|
|
||
| ## Contributing | ||
|
|
||
| ```bash | ||
| npm install | ||
| npm test # vitest (unit + TCP integration) | ||
| npm run typecheck | ||
| npm run lint | ||
| npm run build # tsup → dist (ESM + CJS + types) | ||
| ``` | ||
|
|
||
| Protocol fixtures in `test/` come from official protocol documents | ||
| (Concox GK309 V1.8, HuaSunTeK H02 V1.0.5) and real captured packets — please | ||
| include fixtures with new adapters. | ||
|
|
||
| ## License | ||
|
|
||
| [MIT](LICENSE) |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.