@signalk/tracks-plugin 2.0.2 → 3.0.0-beta.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +181 -14
- package/dist/bootstrap.test.d.ts +1 -0
- package/dist/e2e.test-utils.d.ts +49 -0
- package/dist/glitchFilter.d.ts +51 -0
- package/dist/glitchFilter.test.d.ts +1 -0
- package/dist/harness.test-utils.d.ts +59 -0
- package/dist/historyReconcile.test.d.ts +1 -0
- package/dist/index.d.ts +135 -0
- package/dist/index.js +12898 -0
- package/dist/index.js.map +1 -0
- package/dist/plugin.e2e.test.d.ts +1 -0
- package/dist/provider.e2e.test.d.ts +1 -0
- package/dist/readme.test.d.ts +1 -0
- package/dist/reconcile.d.ts +16 -0
- package/dist/reconcile.test.d.ts +1 -0
- package/dist/routes.test.d.ts +1 -0
- package/dist/selfTrack.test.d.ts +1 -0
- package/dist/source.test.d.ts +1 -0
- package/dist/sourceWatch.d.ts +44 -0
- package/dist/sourceWatch.test.d.ts +1 -0
- package/dist/sqlitePlugin.test.d.ts +1 -0
- package/dist/sqliteStore.d.ts +95 -0
- package/dist/sqliteStore.test.d.ts +1 -0
- package/dist/stateGate.d.ts +40 -0
- package/dist/stateGate.test.d.ts +1 -0
- package/dist/store.d.ts +63 -0
- package/dist/store.test.d.ts +1 -0
- package/dist/timeWindow.d.ts +50 -0
- package/dist/timeWindow.test.d.ts +1 -0
- package/dist/trackApi.d.ts +77 -0
- package/dist/trackApi.e2e.test.d.ts +1 -0
- package/dist/trackProvider.d.ts +21 -0
- package/dist/trackProvider.test.d.ts +1 -0
- package/dist/tracks.d.ts +67 -0
- package/dist/tracks.test.d.ts +1 -0
- package/dist/types.d.ts +55 -0
- package/dist/utils.d.ts +31 -0
- package/dist/utils.test.d.ts +1 -0
- package/package.json +59 -8
- package/index.js +0 -1
- package/signalk-tracks-plugin-1.0.0.tgz +0 -0
package/README.md
CHANGED
|
@@ -1,30 +1,168 @@
|
|
|
1
|
-
# @signalk/tracks
|
|
2
|
-
This repo contains two npm modules:
|
|
3
|
-
- `@signalk/tracks` - RxJs based module that accumulates positions into a track, using a configured time resolution and retains a sliding window of points. Contains the code for both a Signal K server plugin that implements the track api and client side "TrackAccumulator" class that manages the track for a single vessel. TrackAccumulator can optionally bootstrap the track data from the server. The result is available as `Observable<LatLng[]>`.
|
|
4
|
-
- `@signalk/tracks-plugin` Convenience module that exposes the plugin part of `@signalk/tracks`.
|
|
1
|
+
# @signalk/tracks-plugin
|
|
5
2
|
|
|
3
|
+
Signal K server plugin that accumulates vessel positions into tracks and implements the track API.
|
|
6
4
|
|
|
7
|
-
|
|
8
|
-
|
|
5
|
+
Positions are accumulated into a per-vessel sliding window using a configured time resolution. The
|
|
6
|
+
**Where tracks come from after a restart** setting chooses what happens to them:
|
|
9
7
|
|
|
8
|
+
| Setting | Behaviour |
|
|
9
|
+
| --------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
10
|
+
| `history` | _(default)_ Held in memory, and the own vessel's track is refilled on startup from a history provider such as `signalk-to-influxdb2` or `signalk-questdb`. |
|
|
11
|
+
| `sqlite` | Recorded to a database file in the plugin's data directory, so every vessel's track survives a restart with no other plugin required. |
|
|
12
|
+
| `memory` | Held in memory only; tracks start empty after a restart. |
|
|
13
|
+
|
|
14
|
+
All three work with no other plugin installed — with `history` and no provider present, tracks simply
|
|
15
|
+
build up from live data. `sqlite` also takes a retention setting, in days, for how long to keep
|
|
16
|
+
positions.
|
|
17
|
+
|
|
18
|
+
This replaces the earlier `bootstrapFromHistory` checkbox. Existing configurations keep working:
|
|
19
|
+
the setting is still read, with `false` mapping to `memory` and anything else to `history`.
|
|
20
|
+
|
|
21
|
+
The package also exports a client side `TrackAccumulator` class that manages the track for a single
|
|
22
|
+
vessel, exposing the result as `Observable<LatLngTuple[]>`.
|
|
23
|
+
|
|
24
|
+
## Where a track comes from
|
|
25
|
+
|
|
26
|
+
With a history provider installed — [signalk-questdb](https://www.npmjs.com/package/signalk-questdb),
|
|
27
|
+
[signalk-to-influxdb2](https://www.npmjs.com/package/signalk-to-influxdb2) — a track is answered from
|
|
28
|
+
both it and the plugin's own store: the provider is the finer record for as long as its retention
|
|
29
|
+
reaches, and the store is what remains of everything older or of any period the provider missed.
|
|
30
|
+
|
|
31
|
+
Nothing needs configuring for this, and the plugin works exactly as before with no provider
|
|
32
|
+
installed. See [docs/history-and-storage.md](docs/history-and-storage.md) for what that means in
|
|
33
|
+
practice.
|
|
34
|
+
|
|
35
|
+
## Glitch filtering
|
|
36
|
+
|
|
37
|
+
A receiver occasionally reports a position far from the vessel — a bad almanac, a multipath
|
|
38
|
+
reflection, a unit resetting. On a live map it flickers past. In a stored track it is permanent:
|
|
39
|
+
one spike stretches the bounding box across an ocean and draws a line over the chart every time
|
|
40
|
+
the track is rendered.
|
|
41
|
+
|
|
42
|
+
Positions implying a speed above **Discard positions implying a speed above this (knots)** since
|
|
43
|
+
the previous accepted fix are discarded, defaulting to 100 knots. That is well above any real
|
|
44
|
+
vessel, and glitches usually miss it by orders of magnitude rather than by a little. The test is
|
|
45
|
+
speed rather than distance, so a track resuming after a long gap — a passage with the plugin
|
|
46
|
+
stopped, or an AIS target reappearing — is not filtered. Set it to 0 to record everything.
|
|
47
|
+
|
|
48
|
+
The same check runs over positions loaded from a history provider at startup, since those carry
|
|
49
|
+
the same glitches.
|
|
50
|
+
|
|
51
|
+
## Pausing while not under way
|
|
52
|
+
|
|
53
|
+
A boat on a mooring for the winter emits a position every second and travels nowhere. Setting
|
|
54
|
+
**Pause recording while navigation.state is one of** stops those months costing any rows.
|
|
55
|
+
|
|
56
|
+
Off by default, and it needs `navigation.state` to be set — by
|
|
57
|
+
[signalk-autostate](https://github.com/meri-imperiumi/signalk-autostate) or by hand. A vessel
|
|
58
|
+
that reports no state is always recorded, so nothing changes for an install that has not opted in.
|
|
59
|
+
|
|
60
|
+
Two limits are deliberate. **`anchored` is offered but rarely wanted**: an anchor alarm watches
|
|
61
|
+
exactly the track a vessel makes while swinging on its rode, so pausing there would break it.
|
|
62
|
+
And **AIS targets are never paused** — their navigational status comes from the transponder and
|
|
63
|
+
is often stale, so a vessel under way still reporting `moored` would otherwise vanish from the
|
|
64
|
+
track.
|
|
65
|
+
|
|
66
|
+
While paused, the plugin says so in its status on the server dashboard.
|
|
67
|
+
|
|
68
|
+
## Position sources
|
|
69
|
+
|
|
70
|
+
`navigation.position` often arrives from several sources at once — an internal GPS, an AIS
|
|
71
|
+
transponder, a plotter echoing its own fix. Signal K decides which one wins through **source
|
|
72
|
+
priority**, and the stream this plugin records from is already filtered by it, so normally only
|
|
73
|
+
the winning source is stored.
|
|
74
|
+
|
|
75
|
+
When no priority rule matches the path, though, every source comes through. Their fixes are
|
|
76
|
+
metres apart, so the track zigzags between receivers instead of following the boat. If that is
|
|
77
|
+
happening, the plugin says so in its status on the server dashboard, naming the sources it has
|
|
78
|
+
seen. The fix is to set a source priority for `navigation.position` in the server settings.
|
|
10
79
|
|
|
11
80
|
# Usage:
|
|
12
81
|
|
|
13
|
-
|
|
82
|
+
**Retrieve track for an individual vessel:**
|
|
83
|
+
|
|
84
|
+
`/signalk/v1/api/vessels/<vesselId>/track`
|
|
85
|
+
|
|
86
|
+
_`<vesselId>` may be `self` or a fully qualified context such as `urn:mrn:imo:mmsi:123456789`._
|
|
14
87
|
|
|
15
|
-
|
|
88
|
+
**Retrieve the own vessel's track:**
|
|
89
|
+
|
|
90
|
+
`/signalk/v1/api/self/track`
|
|
91
|
+
|
|
92
|
+
---
|
|
93
|
+
|
|
94
|
+
**Narrow a track to a time window:**
|
|
95
|
+
|
|
96
|
+
`/signalk/v1/api/self/track?from=2026-08-09T06:00:00Z&to=2026-08-09T12:00:00Z`
|
|
97
|
+
|
|
98
|
+
`/signalk/v1/api/self/track?duration=6h`
|
|
99
|
+
|
|
100
|
+
_`from` and `to` are ISO-8601 timestamps; `to` defaults to now. `duration` is a window
|
|
101
|
+
ending now. A window ending at now includes its most recent point; one ending earlier is
|
|
102
|
+
half-open, so consecutive windows tile without returning the shared point twice._
|
|
103
|
+
|
|
104
|
+
_`timespan` and `timespanOffset` are also accepted for Freeboard-SK compatibility, where
|
|
105
|
+
`timespan=23h×panOffset=1` means "23 hours ending an hour ago". They are not part of
|
|
106
|
+
the proposed track API and are expected to be superseded by `from`/`to`._
|
|
16
107
|
|
|
17
108
|
---
|
|
18
109
|
|
|
19
|
-
|
|
110
|
+
**Reduce the number of points returned:**
|
|
111
|
+
|
|
112
|
+
`/signalk/v1/api/self/track?duration=24h&resolution=5m`
|
|
113
|
+
|
|
114
|
+
_`resolution` is the minimum spacing between returned points. Durations accept a bare
|
|
115
|
+
number of seconds or an `s`/`m`/`h`/`d` suffix. The first and last points are always kept,
|
|
116
|
+
so thinning never shortens the track._
|
|
117
|
+
|
|
118
|
+
---
|
|
119
|
+
|
|
120
|
+
**Retrieve the time each position was recorded:**
|
|
121
|
+
|
|
122
|
+
`/signalk/v1/api/self/track?duration=6h×`
|
|
123
|
+
|
|
124
|
+
```json
|
|
125
|
+
{
|
|
126
|
+
"type": "MultiLineString",
|
|
127
|
+
"coordinates": [
|
|
128
|
+
[
|
|
129
|
+
[24.9, 60.1],
|
|
130
|
+
[25.0, 60.2]
|
|
131
|
+
]
|
|
132
|
+
],
|
|
133
|
+
"times": [["2026-08-14T09:00:00.000Z", "2026-08-14T09:01:00.000Z"]],
|
|
134
|
+
"context": "vessels.urn:mrn:imo:mmsi:123456789",
|
|
135
|
+
"isSelf": true
|
|
136
|
+
}
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
_`times` adds a `times` array positionally aligned with `coordinates`: `times[i][j]` is when
|
|
140
|
+
`coordinates[i][j]` was recorded, as ISO-8601 UTC. It is opt-in because the response grows by
|
|
141
|
+
roughly a third and clients that only draw the geometry have no use for it. Accepts
|
|
142
|
+
`true`/`1`/`yes` and `false`/`0`/`no`; a valueless `?times` reads as true._
|
|
143
|
+
|
|
144
|
+
_`context` is the fully qualified context the track belongs to, and `isSelf` says whether it is
|
|
145
|
+
the own vessel. Asking for `self` resolves the alias, so the response tells you which vessel
|
|
146
|
+
`self` actually is._
|
|
147
|
+
|
|
148
|
+
---
|
|
149
|
+
|
|
150
|
+
**Retrieve tracks for all vessels:**
|
|
20
151
|
|
|
21
152
|
`/signalk/v1/api/tracks`
|
|
22
153
|
|
|
23
154
|
_If `maxRadius` is specified only vessels with last track position within this distance are returned._
|
|
24
155
|
|
|
156
|
+
_Each entry carries `isSelf`, so the own vessel can be told from an AIS target without
|
|
157
|
+
string-matching the context against the server's self identity._
|
|
158
|
+
|
|
159
|
+
_`?times` works here too, adding a `times` array to every vessel's entry. Note that asking for
|
|
160
|
+
times also segments each track on the gap threshold, so `coordinates` and `times` line up;
|
|
161
|
+
without `times` each vessel keeps its single unsegmented line._
|
|
162
|
+
|
|
25
163
|
---
|
|
26
164
|
|
|
27
|
-
|
|
165
|
+
**Retrieve tracks for all vessels within a given radius (in meters) from your vessel position:**
|
|
28
166
|
|
|
29
167
|
`/signalk/v1/api/tracks?radius=50000`
|
|
30
168
|
|
|
@@ -32,14 +170,43 @@ _Note: This value overrides the `maxRadius` value specified in plugin configurat
|
|
|
32
170
|
|
|
33
171
|
---
|
|
34
172
|
|
|
35
|
-
|
|
173
|
+
**Retrieve tracks for all vessels within a bounded area:**
|
|
36
174
|
|
|
37
175
|
`/signalk/v1/api/tracks?bbox=130,-35,139,-33`
|
|
38
176
|
|
|
39
|
-
_Bounded area is defined as `
|
|
177
|
+
_Bounded area is defined as `west, south, east, north` — GeoJSON coordinate order, the
|
|
178
|
+
same as the coordinates this endpoint returns, the Resources API, and the v2 Track API._
|
|
40
179
|
|
|
41
|
-
|
|
180
|
+
_A box crossing the antimeridian is expressed with `west` greater than `east`, for
|
|
181
|
+
example `bbox=175,-10,-175,10`._
|
|
42
182
|
|
|
43
|
-
|
|
183
|
+
> **Changed:** this parameter was `lat1, lon1, lat2, lon2` — latitude first — up to and
|
|
184
|
+
> including 2.0.2. A box in the old order is still four valid numbers, so it will not be
|
|
185
|
+
> rejected; it will simply describe a different area. Freeboard-SK is unaffected, as it
|
|
186
|
+
> filters by `radius` rather than `bbox`.
|
|
44
187
|
|
|
45
188
|
---
|
|
189
|
+
|
|
190
|
+
# Development
|
|
191
|
+
|
|
192
|
+
```bash
|
|
193
|
+
npm ci
|
|
194
|
+
npm run build # vite library build -> dist/
|
|
195
|
+
npm test # vitest
|
|
196
|
+
npm run test:e2e # against a real signalk-server; see below
|
|
197
|
+
npm run typecheck # tsc --noEmit
|
|
198
|
+
npm run lint # eslint
|
|
199
|
+
npm run format # prettier --write
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
`npm run test:e2e` packs the plugin, installs it into a throwaway config directory, boots a real
|
|
203
|
+
Signal K server against it and feeds positions as deltas — so it covers plugin loading, route
|
|
204
|
+
mounting and the delta path, none of which the unit suite can. It needs a built server checkout;
|
|
205
|
+
set `SIGNALK_SERVER_DIR` if yours is not at `~/dev/xxx_signalk-server`. A second tier installs a
|
|
206
|
+
real history provider (signalk-questdb) into that server and exercises the startup bootstrap
|
|
207
|
+
through it; it skips itself if no QuestDB is reachable at `QUESTDB_URL`. Neither tier runs in CI.
|
|
208
|
+
|
|
209
|
+
The package is ESM only and targets Node >= 22.5.0, the release that added `node:sqlite`. ESM alone
|
|
210
|
+
would only need 20.19, the first release in which the Signal K server's `require()`-based plugin
|
|
211
|
+
loader can load an ES module, but the sqlite track source raises the floor. The server itself
|
|
212
|
+
requires Node >= 22, so this rules out nothing that could have run the plugin anyway.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/** QuestDB endpoint used by the history-provider tier. */
|
|
2
|
+
export declare const QUESTDB_URL: string;
|
|
3
|
+
export interface E2EServer {
|
|
4
|
+
url: string;
|
|
5
|
+
configDir: string;
|
|
6
|
+
/** Send a position delta as a provider would, over the WebSocket API. */
|
|
7
|
+
feed: (context: string, position: [number, number], timestamp?: number, source?: string) => Promise<void>;
|
|
8
|
+
/** GET a path under /signalk/v1/api and parse the JSON. */
|
|
9
|
+
api: (path: string) => Promise<unknown>;
|
|
10
|
+
/**
|
|
11
|
+
* GET a path under /signalk/v2/api, keeping the status.
|
|
12
|
+
*
|
|
13
|
+
* The status is what distinguishes "no provider registered" (501) from an
|
|
14
|
+
* answered query, which is the thing a provider registration test is about.
|
|
15
|
+
*/
|
|
16
|
+
apiV2: (path: string) => Promise<{
|
|
17
|
+
status: number;
|
|
18
|
+
body: unknown;
|
|
19
|
+
}>;
|
|
20
|
+
/** The server's own vessel context, as it resolved it. */
|
|
21
|
+
selfContext: string;
|
|
22
|
+
stop: () => void;
|
|
23
|
+
}
|
|
24
|
+
export interface E2EOptions {
|
|
25
|
+
/** Plugin configuration written to plugin-config-data/tracks.json. */
|
|
26
|
+
config?: Record<string, unknown>;
|
|
27
|
+
/** Seconds to wait for the server to answer before giving up. */
|
|
28
|
+
timeoutSeconds?: number;
|
|
29
|
+
/**
|
|
30
|
+
* Other plugins to install and enable alongside this one, as
|
|
31
|
+
* `{ 'npm-package-name': pluginConfiguration }`.
|
|
32
|
+
*
|
|
33
|
+
* Used to stand up a real history provider so the bootstrap can be exercised
|
|
34
|
+
* through `getHistoryApi()` — the interface this plugin actually depends on —
|
|
35
|
+
* rather than against a provider's private storage.
|
|
36
|
+
*/
|
|
37
|
+
plugins?: Record<string, Record<string, unknown>>;
|
|
38
|
+
/** Port to boot on, when a test needs its own server. */
|
|
39
|
+
port?: number;
|
|
40
|
+
}
|
|
41
|
+
export declare function startServer(options?: E2EOptions): Promise<E2EServer>;
|
|
42
|
+
/**
|
|
43
|
+
* Whether a QuestDB is listening, so the provider tier can skip cleanly.
|
|
44
|
+
*
|
|
45
|
+
* A liveness probe only. What the provider stores, and how, is its own
|
|
46
|
+
* business: this plugin reaches it through `getHistoryApi()` and must keep
|
|
47
|
+
* working across any change to that.
|
|
48
|
+
*/
|
|
49
|
+
export declare function questdbAvailable(): Promise<boolean>;
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { Context, LatLngTuple } from './types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Drops positions that imply an impossible speed since the last accepted one.
|
|
4
|
+
*
|
|
5
|
+
* A GPS occasionally emits a fix hundreds of miles away — a bad almanac, a
|
|
6
|
+
* multipath reflection, a receiver resetting. On a live map it flickers past.
|
|
7
|
+
* In a stored track it is permanent: one spike stretches the bounding box
|
|
8
|
+
* across an ocean, inflates the distance travelled, and drags the line across
|
|
9
|
+
* the chart every time the track is drawn.
|
|
10
|
+
*
|
|
11
|
+
* Speed, not distance, is the test. A vessel can legitimately jump a long way
|
|
12
|
+
* between two fixes if the gap between them is long enough — after a passage
|
|
13
|
+
* with the plugin stopped, or when a slow-reporting AIS target reappears. What
|
|
14
|
+
* cannot happen is covering that distance faster than a vessel moves.
|
|
15
|
+
*
|
|
16
|
+
* Requested in https://github.com/SignalK/tracks/issues/48
|
|
17
|
+
*/
|
|
18
|
+
/**
|
|
19
|
+
* Default ceiling in knots.
|
|
20
|
+
*
|
|
21
|
+
* Above any displacement vessel and most planing craft, and well above the
|
|
22
|
+
* ~50kn an AIS Class A target reports at the top of its scale, so a real fix is
|
|
23
|
+
* never dropped. Glitches are typically wrong by whole degrees of latitude, so
|
|
24
|
+
* they miss this by orders of magnitude rather than by a little.
|
|
25
|
+
*/
|
|
26
|
+
export declare const DEFAULT_MAX_SPEED_KNOTS = 100;
|
|
27
|
+
export interface GlitchFilterConfig {
|
|
28
|
+
/** Ceiling in knots. 0 or below disables the filter. */
|
|
29
|
+
maxSpeedKnots: number;
|
|
30
|
+
}
|
|
31
|
+
export declare class GlitchFilter {
|
|
32
|
+
private readonly last;
|
|
33
|
+
private readonly maxSpeedKnots;
|
|
34
|
+
/** Rejections per context, for the plugin status line. */
|
|
35
|
+
private readonly rejected;
|
|
36
|
+
constructor(config: GlitchFilterConfig);
|
|
37
|
+
/**
|
|
38
|
+
* Whether to keep this position.
|
|
39
|
+
*
|
|
40
|
+
* Accepting updates the reference point, so a sustained move — a genuinely
|
|
41
|
+
* fast vessel, or a track resuming after a gap — is not fought after the
|
|
42
|
+
* first fix.
|
|
43
|
+
*/
|
|
44
|
+
accept(context: Context, position: LatLngTuple, timestamp: number): boolean;
|
|
45
|
+
/** How many positions have been rejected for a context. */
|
|
46
|
+
rejectedCount(context: Context): number;
|
|
47
|
+
/** Total rejections across every context. */
|
|
48
|
+
totalRejected(): number;
|
|
49
|
+
/** Forget every reference point; used when the plugin restarts. */
|
|
50
|
+
clear(): void;
|
|
51
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { Express } from 'express';
|
|
2
|
+
import { TrackApi } from './trackApi.js';
|
|
3
|
+
import { LatLngTuple } from './types.js';
|
|
4
|
+
/**
|
|
5
|
+
* Test harness that stands the plugin up behind a real express app, mounted the
|
|
6
|
+
* way the Signal K server mounts it (`app.use('/signalk/v1/api', …)`), so route
|
|
7
|
+
* patterns and parameter parsing are exercised rather than mocked.
|
|
8
|
+
*
|
|
9
|
+
* Note the server runs express 4; `express` is pinned to ^4 in devDependencies
|
|
10
|
+
* so the wildcard route syntax under test matches production.
|
|
11
|
+
*/
|
|
12
|
+
export declare const SELF_CONTEXT = "vessels.urn:mrn:imo:mmsi:123456789";
|
|
13
|
+
export declare const OTHER_CONTEXT = "vessels.urn:mrn:imo:mmsi:987654321";
|
|
14
|
+
export interface TestHarness {
|
|
15
|
+
app: Express;
|
|
16
|
+
/**
|
|
17
|
+
* Feed a position delta as the streambundle would. `timestamp` dates the
|
|
18
|
+
* point, so time-window queries can be tested without faking the clock.
|
|
19
|
+
*
|
|
20
|
+
* Note `throttleTime` thins on the leading edge against the wall clock, so a
|
|
21
|
+
* synchronous burst yields a single point regardless of the timestamps given.
|
|
22
|
+
* Use `seedTrack` to install a back-dated track instead.
|
|
23
|
+
*/
|
|
24
|
+
emit: (context: string, position: LatLngTuple, timestamp?: number, source?: string) => void;
|
|
25
|
+
/** Status messages the plugin has pushed to the server dashboard. */
|
|
26
|
+
statuses: string[];
|
|
27
|
+
/**
|
|
28
|
+
* Install a track with explicit timestamps, bypassing the throttled bus. This
|
|
29
|
+
* is the entry point the History API bootstrap uses.
|
|
30
|
+
*/
|
|
31
|
+
seedTrack: (context: string, positions: LatLngTuple[], timestamps: number[]) => void;
|
|
32
|
+
/** Self position as reported by `getSelfPath`, used for radius filtering. */
|
|
33
|
+
setSelfPosition: (position: LatLngTuple | undefined) => void;
|
|
34
|
+
/** The own vessel's navigation.state, as reported by `getSelfPath`. */
|
|
35
|
+
setSelfState: (state: string | undefined) => void;
|
|
36
|
+
stop: () => void;
|
|
37
|
+
errors: unknown[][];
|
|
38
|
+
/**
|
|
39
|
+
* The v2 Track API provider the plugin registered, or undefined when the
|
|
40
|
+
* server offered no `registerTrackApiProvider`.
|
|
41
|
+
*/
|
|
42
|
+
trackProvider: () => TrackApi | undefined;
|
|
43
|
+
/** How many times the plugin registered a provider. */
|
|
44
|
+
registrations: () => number;
|
|
45
|
+
}
|
|
46
|
+
export interface HarnessOptions {
|
|
47
|
+
/** Plugin config overrides. Bootstrap is disabled by default so tests stay hermetic. */
|
|
48
|
+
config?: Record<string, unknown>;
|
|
49
|
+
selfContext?: string;
|
|
50
|
+
selfPosition?: LatLngTuple;
|
|
51
|
+
/** Initial navigation.state for the own vessel. */
|
|
52
|
+
selfState?: string;
|
|
53
|
+
/**
|
|
54
|
+
* Stand in for a server without the v2 Track API, to check the plugin still
|
|
55
|
+
* starts when `registerTrackApiProvider` is absent.
|
|
56
|
+
*/
|
|
57
|
+
withoutTrackApi?: boolean;
|
|
58
|
+
}
|
|
59
|
+
export declare function createHarness(options?: HarnessOptions): TestHarness;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import { Temporal } from '@js-temporal/polyfill';
|
|
2
|
+
import { Router } from 'express';
|
|
3
|
+
import { TrackApi } from './trackApi.js';
|
|
4
|
+
import { TrackStore } from './store.js';
|
|
5
|
+
import { Context, Debug, Position } from './types.js';
|
|
6
|
+
export interface ContextPosition {
|
|
7
|
+
context: Context;
|
|
8
|
+
value: Position;
|
|
9
|
+
/** ISO-8601 time the value was recorded, as carried by the Signal K delta. */
|
|
10
|
+
timestamp?: string;
|
|
11
|
+
/**
|
|
12
|
+
* Which source produced the value.
|
|
13
|
+
*
|
|
14
|
+
* The bus is priority-filtered, so normally this is the one source the
|
|
15
|
+
* server selected. Seeing several for one context means no priority rule is
|
|
16
|
+
* matching the path — which is what `SourceWatch` reports on.
|
|
17
|
+
*/
|
|
18
|
+
$source?: string;
|
|
19
|
+
}
|
|
20
|
+
interface HistoryValuesQuery {
|
|
21
|
+
context: string;
|
|
22
|
+
from: Temporal.Instant;
|
|
23
|
+
to: Temporal.Instant;
|
|
24
|
+
pathSpecs: {
|
|
25
|
+
path: string;
|
|
26
|
+
aggregate: string;
|
|
27
|
+
}[];
|
|
28
|
+
resolution: number;
|
|
29
|
+
}
|
|
30
|
+
interface HistoryApi {
|
|
31
|
+
getValues(query: HistoryValuesQuery): Promise<HistoryValuesResponse>;
|
|
32
|
+
}
|
|
33
|
+
interface HistoryValuesResponse {
|
|
34
|
+
context: string;
|
|
35
|
+
range: {
|
|
36
|
+
from: string;
|
|
37
|
+
to: string;
|
|
38
|
+
};
|
|
39
|
+
values: unknown[];
|
|
40
|
+
/** Each element: [timestamp_string, [lon, lat]] */
|
|
41
|
+
data: unknown[];
|
|
42
|
+
}
|
|
43
|
+
interface App {
|
|
44
|
+
debug: Debug;
|
|
45
|
+
error: (...args: unknown[]) => void;
|
|
46
|
+
streambundle: {
|
|
47
|
+
getBus: (path: string) => {
|
|
48
|
+
onValue: (cb: (x: ContextPosition) => void) => () => void;
|
|
49
|
+
};
|
|
50
|
+
};
|
|
51
|
+
getSelfPath: (path: string) => unknown;
|
|
52
|
+
selfContext: string;
|
|
53
|
+
/** Shown against the plugin in the server's dashboard. Absent on older servers. */
|
|
54
|
+
setPluginStatus?: (msg: string) => void;
|
|
55
|
+
/** Plugin-private directory for persistent data; absent on older servers. */
|
|
56
|
+
getDataDirPath?: () => string;
|
|
57
|
+
/** Resolves the named provider, or the configured default when omitted. */
|
|
58
|
+
getHistoryApi?: (providerId?: string) => Promise<HistoryApi>;
|
|
59
|
+
/**
|
|
60
|
+
* Offer this plugin's tracks to the v2 Track API. Absent on servers older
|
|
61
|
+
* than SignalK/signalk-server#2995.
|
|
62
|
+
*
|
|
63
|
+
* The server unregisters the provider itself when the plugin stops, so
|
|
64
|
+
* `stop()` here must not do it a second time.
|
|
65
|
+
*/
|
|
66
|
+
registerTrackApiProvider?: (provider: TrackApi) => void;
|
|
67
|
+
config?: {
|
|
68
|
+
settings?: {
|
|
69
|
+
historyApi?: {
|
|
70
|
+
defaultProvider?: string;
|
|
71
|
+
};
|
|
72
|
+
};
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
interface Plugin {
|
|
76
|
+
start: (c: TracksPluginConfig) => void;
|
|
77
|
+
stop: () => void;
|
|
78
|
+
signalKApiRoutes: (r: Router) => Router;
|
|
79
|
+
id: string;
|
|
80
|
+
name: string;
|
|
81
|
+
description: string;
|
|
82
|
+
schema: Record<string, unknown>;
|
|
83
|
+
/**
|
|
84
|
+
* The live accumulator, or undefined before `start()`.
|
|
85
|
+
*
|
|
86
|
+
* Exposed so a track can be installed or inspected without going through the
|
|
87
|
+
* position bus, which throttles against the wall clock. The Signal K server
|
|
88
|
+
* does not use this.
|
|
89
|
+
*/
|
|
90
|
+
getTracks: () => TrackStore | undefined;
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* Where tracks come from when the server starts.
|
|
94
|
+
*
|
|
95
|
+
* - `memory`: start empty, accumulate from the position bus only.
|
|
96
|
+
* - `history`: refill from a History provider at startup, then accumulate.
|
|
97
|
+
* - `sqlite`: persist to disk, so a restart resumes from what was recorded.
|
|
98
|
+
*
|
|
99
|
+
* One setting rather than a boolean per source: they are alternatives, and as
|
|
100
|
+
* independent booleans a user could enable two and have the same positions
|
|
101
|
+
* loaded twice.
|
|
102
|
+
*/
|
|
103
|
+
export type TrackSource = 'memory' | 'history' | 'sqlite';
|
|
104
|
+
interface TracksPluginConfig {
|
|
105
|
+
resolution?: number;
|
|
106
|
+
pointsToKeep?: number;
|
|
107
|
+
maxAge?: number;
|
|
108
|
+
maxRadius?: number;
|
|
109
|
+
source?: TrackSource;
|
|
110
|
+
/** @deprecated superseded by `source`; still honoured for existing configs. */
|
|
111
|
+
bootstrapFromHistory?: boolean;
|
|
112
|
+
/** Days of history to keep when `source` is `sqlite`. 0 keeps everything. */
|
|
113
|
+
retentionDays?: number;
|
|
114
|
+
/** Minutes without a fix that start a new track segment. 0 disables. */
|
|
115
|
+
segmentGapMinutes?: number;
|
|
116
|
+
/** Speed above which a position is treated as a glitch. 0 disables. */
|
|
117
|
+
maxSpeedKnots?: number;
|
|
118
|
+
/** navigation.state values that pause recording of the own vessel. */
|
|
119
|
+
pauseWhenState?: string[];
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* Resolve the effective source, honouring the setting this replaced.
|
|
123
|
+
*
|
|
124
|
+
* `bootstrapFromHistory` shipped as a boolean defaulting to true, so an
|
|
125
|
+
* installed plugin has it saved in its config either way. Reading `source`
|
|
126
|
+
* first and only then falling back keeps those installs working: an explicit
|
|
127
|
+
* `false` still means "do not touch the History API", and everything else
|
|
128
|
+
* lands on the old default of `history`.
|
|
129
|
+
*/
|
|
130
|
+
export declare const resolveSource: (config: TracksPluginConfig) => TrackSource;
|
|
131
|
+
export default function ThePlugin(app: App): Plugin;
|
|
132
|
+
export { Tracks, TrackAccumulator } from './tracks.js';
|
|
133
|
+
export type { TracksConfig } from './tracks.js';
|
|
134
|
+
export type { TrackStore } from './store.js';
|
|
135
|
+
export type * from './types.js';
|