signalk-siparu 0.1.21 → 0.1.23
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/package.json +1 -1
- package/plugin/dist/contract.d.ts +66 -0
- package/plugin/dist/index.js +10 -0
- package/plugin/dist/live.d.ts +33 -4
- package/plugin/dist/live.js +90 -6
- package/plugin/dist/track.d.ts +16 -0
- package/plugin/dist/track.js +31 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "signalk-siparu",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.23",
|
|
4
4
|
"description": "Kept aboard, proven ashore. An impartial, timestamped record of every voyage: position, wind, depth and logbook, written on the boat and readable from anywhere. A read-only Signal K plugin with a built-in dashboard.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"signalk-node-server-plugin",
|
|
@@ -189,6 +189,59 @@ export type SnapshotsResponse = {
|
|
|
189
189
|
message: string;
|
|
190
190
|
};
|
|
191
191
|
};
|
|
192
|
+
/**
|
|
193
|
+
* The voyages RPC, a third sibling on the same live socket. Where history asks for one gauge's
|
|
194
|
+
* series and snapshots for whole rows over a window, this asks for the boat's recent voyages -
|
|
195
|
+
* the list the local /voyages REST serves. It carries no query, only how many of the newest to
|
|
196
|
+
* return; like its siblings it is a read of the boat's own store and reaches nothing near
|
|
197
|
+
* Signal K. The boat clamps the count, so a request cannot ask for more than she will give.
|
|
198
|
+
*/
|
|
199
|
+
export interface VoyagesRequest {
|
|
200
|
+
type: 'voyages';
|
|
201
|
+
id: string;
|
|
202
|
+
/** How many of the newest voyages to return. Clamped boat-side to the REST bounds. */
|
|
203
|
+
limit: number;
|
|
204
|
+
}
|
|
205
|
+
/** The boat's answer to one VoyagesRequest. The voyages or a reason, never both. */
|
|
206
|
+
export type VoyagesResponse = {
|
|
207
|
+
type: 'voyages';
|
|
208
|
+
id: string;
|
|
209
|
+
result: VoyageListResult;
|
|
210
|
+
} | {
|
|
211
|
+
type: 'voyages';
|
|
212
|
+
id: string;
|
|
213
|
+
error: {
|
|
214
|
+
code: string;
|
|
215
|
+
message: string;
|
|
216
|
+
};
|
|
217
|
+
};
|
|
218
|
+
/**
|
|
219
|
+
* The track RPC, a fourth sibling on the same live socket. Where voyages asks for the list, this
|
|
220
|
+
* asks for one voyage's recorded path - the fixes the local /voyages/:id/track REST serves,
|
|
221
|
+
* drawn as a line on a chart ashore. It carries the voyage's id and nothing else; like its
|
|
222
|
+
* siblings it is a read of the boat's own store and reaches nothing near Signal K. The boat
|
|
223
|
+
* decimates a long track before she sends it, so a request cannot pull an unbounded stream over
|
|
224
|
+
* the wire (a day under way at 1 Hz is tens of thousands of fixes).
|
|
225
|
+
*/
|
|
226
|
+
export interface TrackRequest {
|
|
227
|
+
type: 'track';
|
|
228
|
+
id: string;
|
|
229
|
+
/** Which voyage to draw, by its Voyage.id. */
|
|
230
|
+
voyageId: number;
|
|
231
|
+
}
|
|
232
|
+
/** The boat's answer to one TrackRequest. The path or a reason, never both. */
|
|
233
|
+
export type TrackResponse = {
|
|
234
|
+
type: 'track';
|
|
235
|
+
id: string;
|
|
236
|
+
result: TrackResult;
|
|
237
|
+
} | {
|
|
238
|
+
type: 'track';
|
|
239
|
+
id: string;
|
|
240
|
+
error: {
|
|
241
|
+
code: string;
|
|
242
|
+
message: string;
|
|
243
|
+
};
|
|
244
|
+
};
|
|
192
245
|
export interface LiveResult extends Snapshot {
|
|
193
246
|
/** Seconds since the newest delta touched any subscribed path; null before first delta. */
|
|
194
247
|
data_age_s: number | null;
|
|
@@ -295,6 +348,10 @@ export interface Voyage {
|
|
|
295
348
|
end_port: string | null;
|
|
296
349
|
status: 'open' | 'closed';
|
|
297
350
|
}
|
|
351
|
+
/** The boat's answer to one VoyagesRequest: her recent voyages, newest first. */
|
|
352
|
+
export interface VoyageListResult {
|
|
353
|
+
voyages: Voyage[];
|
|
354
|
+
}
|
|
298
355
|
/** One aggregation window on the voyage stats card. */
|
|
299
356
|
export interface VoyageWindowStats {
|
|
300
357
|
distance_nm: number;
|
|
@@ -315,6 +372,15 @@ export interface TrackPoint {
|
|
|
315
372
|
/** Knots, rounded; null when unavailable. */
|
|
316
373
|
sog: number | null;
|
|
317
374
|
}
|
|
375
|
+
/**
|
|
376
|
+
* The boat's answer to one TrackRequest: one voyage's path, oldest fix first.
|
|
377
|
+
* `decimated` is true when the boat thinned a long track before sending it, so a reader can say
|
|
378
|
+
* the line is a faithful shape but not every recorded fix.
|
|
379
|
+
*/
|
|
380
|
+
export interface TrackResult {
|
|
381
|
+
track: TrackPoint[];
|
|
382
|
+
decimated: boolean;
|
|
383
|
+
}
|
|
318
384
|
/** One nearby AIS vessel, sanitized and distance-filtered server-side. */
|
|
319
385
|
export interface AisTarget {
|
|
320
386
|
mmsi: string;
|
package/plugin/dist/index.js
CHANGED
|
@@ -62,6 +62,7 @@ const rollup_1 = require("./rollup");
|
|
|
62
62
|
const store_1 = require("./store");
|
|
63
63
|
const time_1 = require("./time");
|
|
64
64
|
const live_1 = require("./live");
|
|
65
|
+
const track_1 = require("./track");
|
|
65
66
|
const uplink_1 = require("./uplink");
|
|
66
67
|
const voyagelog_1 = require("./voyagelog");
|
|
67
68
|
const PLUGIN_ID = 'siparu';
|
|
@@ -349,6 +350,15 @@ module.exports = (app) => {
|
|
|
349
350
|
// Her whole recorded rows over a window, the logbook read - the same store the local
|
|
350
351
|
// /snapshots serves, reached here and nowhere near Signal K.
|
|
351
352
|
onSnapshotsQuery: (q) => qs.snapshots(q, Date.now()),
|
|
353
|
+
// Her recent voyages, the list the local /voyages serves. The count is clamped to the
|
|
354
|
+
// same 1..500 bounds the REST route enforces, since vl.list does not clamp its own.
|
|
355
|
+
onVoyagesQuery: async (limit) => ({
|
|
356
|
+
voyages: vl.list(Math.min(Math.max(1, limit || 50), 500))
|
|
357
|
+
}),
|
|
358
|
+
// One voyage's path, the line the local /voyages/:id/track serves. Decimated before it
|
|
359
|
+
// crosses the wire: vl.track returns every recorded fix (a long voyage is tens of
|
|
360
|
+
// thousands), which the local REST may serve but a single timed socket reply may not.
|
|
361
|
+
onTrackQuery: (voyageId) => vl.track(voyageId, Date.now()).then(track_1.decimateTrack),
|
|
352
362
|
debug: (msg) => app.debug(msg)
|
|
353
363
|
});
|
|
354
364
|
liveUplink = ws;
|
package/plugin/dist/live.d.ts
CHANGED
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
* fleet, and every owner would have to walk down to their boat to fix a bug that was ours.
|
|
17
17
|
*/
|
|
18
18
|
import type { RemoteLink } from './remotelink';
|
|
19
|
-
import type { PathSeriesResult, SnapshotsQuery, SnapshotsResult } from './contract';
|
|
19
|
+
import type { PathSeriesResult, SnapshotsQuery, SnapshotsResult, TrackResult, VoyageListResult } from './contract';
|
|
20
20
|
/**
|
|
21
21
|
* How often a frame goes up while the socket is open, when she is under way.
|
|
22
22
|
*
|
|
@@ -99,6 +99,20 @@ export interface LiveDeps {
|
|
|
99
99
|
* so an old relay or a boat wired without it simply never grows the ear.
|
|
100
100
|
*/
|
|
101
101
|
onSnapshotsQuery?: (query: SnapshotsQuery) => Promise<SnapshotsResult>;
|
|
102
|
+
/**
|
|
103
|
+
* Answers a shore voyages request - her recent voyages, the list the local REST /voyages
|
|
104
|
+
* serves - from the same store. A third sibling of onHistoryQuery: a read, never a command,
|
|
105
|
+
* and it never reaches Signal K. Absent leaves the socket deaf to voyages requests, so an old
|
|
106
|
+
* relay or a boat wired without it simply never grows the ear.
|
|
107
|
+
*/
|
|
108
|
+
onVoyagesQuery?: (limit: number) => Promise<VoyageListResult>;
|
|
109
|
+
/**
|
|
110
|
+
* Answers a shore track request - one voyage's recorded path, the line the local REST
|
|
111
|
+
* /voyages/:id/track serves - from the same store. A fourth sibling of onHistoryQuery: a read,
|
|
112
|
+
* never a command, and it never reaches Signal K. Absent leaves the socket deaf to track
|
|
113
|
+
* requests, so an old relay or a boat wired without it simply never grows the ear.
|
|
114
|
+
*/
|
|
115
|
+
onTrackQuery?: (voyageId: number) => Promise<TrackResult>;
|
|
102
116
|
debug: (msg: string) => void;
|
|
103
117
|
/** Injected in tests. In production this is the `ws` adapter at the bottom of the file. */
|
|
104
118
|
connect?: (url: string, token: string) => LiveSocket;
|
|
@@ -186,9 +200,24 @@ export declare class LiveUplink {
|
|
|
186
200
|
*/
|
|
187
201
|
private handleSnapshots;
|
|
188
202
|
/**
|
|
189
|
-
*
|
|
190
|
-
*
|
|
191
|
-
*
|
|
203
|
+
* A voyages request from the shore, answered from the boat's own store - a third sibling of
|
|
204
|
+
* handleHistory, and just as narrow. Parse, act only if it is a voyages request, and read the
|
|
205
|
+
* store, never Signal K. The boat clamps the count before it reads, so a request cannot ask
|
|
206
|
+
* her for more than she will give.
|
|
207
|
+
*/
|
|
208
|
+
private handleVoyages;
|
|
209
|
+
/**
|
|
210
|
+
* A track request from the shore, answered from the boat's own store - a fourth sibling of
|
|
211
|
+
* handleHistory, and just as narrow. Parse, act only if it is a track request, and read the
|
|
212
|
+
* store, never Signal K. The boat decimates a long path before it answers, so a request cannot
|
|
213
|
+
* pull an unbounded stream over the wire.
|
|
214
|
+
*/
|
|
215
|
+
private handleTrack;
|
|
216
|
+
/**
|
|
217
|
+
* Send a history, snapshots, voyages or track answer, but only if it still belongs to the
|
|
218
|
+
* socket that asked. A query reads the disk while the line may drop and redial underneath it;
|
|
219
|
+
* the generation guard is what keeps a slow answer from landing on a fresh connection that
|
|
220
|
+
* never asked.
|
|
192
221
|
*/
|
|
193
222
|
private reply;
|
|
194
223
|
private keepalive;
|
package/plugin/dist/live.js
CHANGED
|
@@ -202,11 +202,14 @@ class LiveUplink {
|
|
|
202
202
|
return;
|
|
203
203
|
}
|
|
204
204
|
// Beyond a pong, the shore may ask the boat to send back her own recorded history: one
|
|
205
|
-
// gauge's series (handleHistory)
|
|
206
|
-
//
|
|
207
|
-
//
|
|
205
|
+
// gauge's series (handleHistory), whole snapshot rows (handleSnapshots), her recent voyages
|
|
206
|
+
// (handleVoyages) or one voyage's path (handleTrack). None is a command; each drops in
|
|
207
|
+
// silence anything that is not its own request, and anything that is none is not acted on
|
|
208
|
+
// at all, because the shore may not steer a boat.
|
|
208
209
|
this.handleHistory(gen, data);
|
|
209
210
|
this.handleSnapshots(gen, data);
|
|
211
|
+
this.handleVoyages(gen, data);
|
|
212
|
+
this.handleTrack(gen, data);
|
|
210
213
|
});
|
|
211
214
|
sock.onClose((code) => {
|
|
212
215
|
if (gen !== this.gen)
|
|
@@ -353,9 +356,68 @@ class LiveUplink {
|
|
|
353
356
|
});
|
|
354
357
|
}
|
|
355
358
|
/**
|
|
356
|
-
*
|
|
357
|
-
*
|
|
358
|
-
*
|
|
359
|
+
* A voyages request from the shore, answered from the boat's own store - a third sibling of
|
|
360
|
+
* handleHistory, and just as narrow. Parse, act only if it is a voyages request, and read the
|
|
361
|
+
* store, never Signal K. The boat clamps the count before it reads, so a request cannot ask
|
|
362
|
+
* her for more than she will give.
|
|
363
|
+
*/
|
|
364
|
+
handleVoyages(gen, data) {
|
|
365
|
+
const handler = this.deps.onVoyagesQuery;
|
|
366
|
+
if (!handler)
|
|
367
|
+
return;
|
|
368
|
+
let msg;
|
|
369
|
+
try {
|
|
370
|
+
msg = JSON.parse(data);
|
|
371
|
+
}
|
|
372
|
+
catch {
|
|
373
|
+
return;
|
|
374
|
+
}
|
|
375
|
+
if (!isVoyagesRequest(msg))
|
|
376
|
+
return;
|
|
377
|
+
const { id, limit } = msg;
|
|
378
|
+
handler(limit).then((result) => this.reply(gen, { type: 'voyages', id, result }), (err) => {
|
|
379
|
+
this.deps.debug(`voyages query failed: ${String(err)}`);
|
|
380
|
+
this.reply(gen, {
|
|
381
|
+
type: 'voyages',
|
|
382
|
+
id,
|
|
383
|
+
error: { code: 'VOYAGES_FAILED', message: 'voyages query failed' }
|
|
384
|
+
});
|
|
385
|
+
});
|
|
386
|
+
}
|
|
387
|
+
/**
|
|
388
|
+
* A track request from the shore, answered from the boat's own store - a fourth sibling of
|
|
389
|
+
* handleHistory, and just as narrow. Parse, act only if it is a track request, and read the
|
|
390
|
+
* store, never Signal K. The boat decimates a long path before it answers, so a request cannot
|
|
391
|
+
* pull an unbounded stream over the wire.
|
|
392
|
+
*/
|
|
393
|
+
handleTrack(gen, data) {
|
|
394
|
+
const handler = this.deps.onTrackQuery;
|
|
395
|
+
if (!handler)
|
|
396
|
+
return;
|
|
397
|
+
let msg;
|
|
398
|
+
try {
|
|
399
|
+
msg = JSON.parse(data);
|
|
400
|
+
}
|
|
401
|
+
catch {
|
|
402
|
+
return;
|
|
403
|
+
}
|
|
404
|
+
if (!isTrackRequest(msg))
|
|
405
|
+
return;
|
|
406
|
+
const { id, voyageId } = msg;
|
|
407
|
+
handler(voyageId).then((result) => this.reply(gen, { type: 'track', id, result }), (err) => {
|
|
408
|
+
this.deps.debug(`track query failed: ${String(err)}`);
|
|
409
|
+
this.reply(gen, {
|
|
410
|
+
type: 'track',
|
|
411
|
+
id,
|
|
412
|
+
error: { code: 'TRACK_FAILED', message: 'track query failed' }
|
|
413
|
+
});
|
|
414
|
+
});
|
|
415
|
+
}
|
|
416
|
+
/**
|
|
417
|
+
* Send a history, snapshots, voyages or track answer, but only if it still belongs to the
|
|
418
|
+
* socket that asked. A query reads the disk while the line may drop and redial underneath it;
|
|
419
|
+
* the generation guard is what keeps a slow answer from landing on a fresh connection that
|
|
420
|
+
* never asked.
|
|
359
421
|
*/
|
|
360
422
|
reply(gen, msg) {
|
|
361
423
|
if (gen !== this.gen || !this.sock)
|
|
@@ -493,6 +555,28 @@ function isSnapshotsRequest(m) {
|
|
|
493
555
|
typeof o.query === 'object' &&
|
|
494
556
|
o.query !== null);
|
|
495
557
|
}
|
|
558
|
+
/**
|
|
559
|
+
* A voyages request, told apart the same way: the type tag is the gate. It carries no query,
|
|
560
|
+
* only a count - so the tag, the id and a numeric limit are checked. The limit's bounds are the
|
|
561
|
+
* boat's to enforce (she clamps it before reading), so they are not re-checked here.
|
|
562
|
+
*/
|
|
563
|
+
function isVoyagesRequest(m) {
|
|
564
|
+
if (typeof m !== 'object' || m === null)
|
|
565
|
+
return false;
|
|
566
|
+
const o = m;
|
|
567
|
+
return o.type === 'voyages' && typeof o.id === 'string' && typeof o.limit === 'number';
|
|
568
|
+
}
|
|
569
|
+
/**
|
|
570
|
+
* A track request, told apart the same way: the type tag is the gate. It carries a voyage id, so
|
|
571
|
+
* the tag, the id and a numeric voyageId are checked. Whether that voyage exists is the store's
|
|
572
|
+
* to answer (an unknown id reads back an empty path), so it is not re-checked here.
|
|
573
|
+
*/
|
|
574
|
+
function isTrackRequest(m) {
|
|
575
|
+
if (typeof m !== 'object' || m === null)
|
|
576
|
+
return false;
|
|
577
|
+
const o = m;
|
|
578
|
+
return o.type === 'track' && typeof o.id === 'string' && typeof o.voyageId === 'number';
|
|
579
|
+
}
|
|
496
580
|
/**
|
|
497
581
|
* The real socket.
|
|
498
582
|
*
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { TrackPoint, TrackResult } from './contract';
|
|
2
|
+
/**
|
|
3
|
+
* The most fixes a track carries over the wire.
|
|
4
|
+
*
|
|
5
|
+
* A day under way at roughly 1 Hz is tens of thousands of fixes; a line on a chart needs a small
|
|
6
|
+
* fraction of that, and the shore's request waits on a single reply with a fixed timeout. The
|
|
7
|
+
* local REST is left untouched - a browser aboard has no such limit and can ask for every fix -
|
|
8
|
+
* so this caps only what crosses the wire.
|
|
9
|
+
*/
|
|
10
|
+
export declare const MAX_TRACK_POINTS = 2000;
|
|
11
|
+
/**
|
|
12
|
+
* Thin a track to at most `max` fixes at an even stride, always keeping the first and last so the
|
|
13
|
+
* line still starts and ends where the voyage did. Returns the track untouched when it already
|
|
14
|
+
* fits, with a flag saying whether anything was dropped - a faithful shape, not every fix.
|
|
15
|
+
*/
|
|
16
|
+
export declare function decimateTrack(points: TrackPoint[], max?: number): TrackResult;
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.MAX_TRACK_POINTS = void 0;
|
|
4
|
+
exports.decimateTrack = decimateTrack;
|
|
5
|
+
/**
|
|
6
|
+
* The most fixes a track carries over the wire.
|
|
7
|
+
*
|
|
8
|
+
* A day under way at roughly 1 Hz is tens of thousands of fixes; a line on a chart needs a small
|
|
9
|
+
* fraction of that, and the shore's request waits on a single reply with a fixed timeout. The
|
|
10
|
+
* local REST is left untouched - a browser aboard has no such limit and can ask for every fix -
|
|
11
|
+
* so this caps only what crosses the wire.
|
|
12
|
+
*/
|
|
13
|
+
exports.MAX_TRACK_POINTS = 2000;
|
|
14
|
+
/**
|
|
15
|
+
* Thin a track to at most `max` fixes at an even stride, always keeping the first and last so the
|
|
16
|
+
* line still starts and ends where the voyage did. Returns the track untouched when it already
|
|
17
|
+
* fits, with a flag saying whether anything was dropped - a faithful shape, not every fix.
|
|
18
|
+
*/
|
|
19
|
+
function decimateTrack(points, max = exports.MAX_TRACK_POINTS) {
|
|
20
|
+
if (points.length <= max || max < 2)
|
|
21
|
+
return { track: points, decimated: false };
|
|
22
|
+
const stride = Math.ceil(points.length / max);
|
|
23
|
+
const out = [];
|
|
24
|
+
// length > max >= 2 here, so the first and last are always present.
|
|
25
|
+
for (let i = 0; i < points.length; i += stride)
|
|
26
|
+
out.push(points[i]);
|
|
27
|
+
const last = points[points.length - 1];
|
|
28
|
+
if (out[out.length - 1] !== last)
|
|
29
|
+
out.push(last);
|
|
30
|
+
return { track: out, decimated: true };
|
|
31
|
+
}
|