signalk-siparu 0.2.10 → 0.2.12
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/LICENSE +1 -1
- package/README.md +4 -3
- package/package.json +1 -1
- package/plugin/dist/aggregate.d.ts +27 -0
- package/plugin/dist/aggregate.js +94 -0
- package/plugin/dist/ais.d.ts +1 -0
- package/plugin/dist/ais.js +7 -0
- package/plugin/dist/aisreceiver.d.ts +17 -0
- package/plugin/dist/aisreceiver.js +95 -0
- package/plugin/dist/config.js +2 -1
- package/plugin/dist/contract.d.ts +126 -0
- package/plugin/dist/contract.js +29 -0
- package/plugin/dist/index.js +31 -6
- package/plugin/dist/live.d.ts +11 -2
- package/plugin/dist/live.js +104 -11
- package/plugin/dist/query.d.ts +3 -1
- package/plugin/dist/query.js +43 -48
- package/plugin/dist/rollup.d.ts +2 -1
- package/plugin/dist/rollup.js +8 -130
- package/plugin/dist/time.d.ts +2 -0
- package/plugin/dist/time.js +8 -0
- package/plugin/dist/units.d.ts +1 -0
- package/plugin/dist/units.js +26 -1
- package/plugin/dist/uplink.js +2 -1
- package/public/assets/Logbook-B8_oMwuZ.js +1 -0
- package/public/assets/Map-CEwUZlRh.js +111 -0
- package/public/assets/Remote--W7QE6vQ.js +1 -0
- package/public/assets/SecurityHelp-CmkDtKXa.js +1 -0
- package/public/assets/Voyage-B5ibpNOg.js +1 -0
- package/public/assets/export-CQYXr7nu.js +9 -0
- package/public/assets/index-D8cInHV9.js +20 -0
- package/public/assets/index-DnKxzCIS.css +1 -0
- package/public/assets/jsx-runtime-CaR_m4Xc.js +1 -0
- package/public/assets/style-D1uNa2dX.js +762 -0
- package/public/index.html +3 -2
- package/public/assets/Logbook-VNvBbXql.js +0 -1
- package/public/assets/Map-Bdfx5YSk.js +0 -105
- package/public/assets/Voyage-CrdZ-Mdn.js +0 -5
- package/public/assets/index-Bws3NPSA.css +0 -1
- package/public/assets/index-C_sXr_1m.js +0 -21
- package/public/assets/style-B2k4QVOw.css +0 -1
- package/public/assets/style-DroXqNI8.js +0 -762
package/LICENSE
CHANGED
|
@@ -187,7 +187,7 @@
|
|
|
187
187
|
same "printed page" as the copyright notice for easier
|
|
188
188
|
identification within third-party archives.
|
|
189
189
|
|
|
190
|
-
Copyright
|
|
190
|
+
Copyright 2026 Siparu Ltd
|
|
191
191
|
|
|
192
192
|
Licensed under the Apache License, Version 2.0 (the "License");
|
|
193
193
|
you may not use this file except in compliance with the License.
|
package/README.md
CHANGED
|
@@ -138,9 +138,10 @@ code off the helm. A caller with no browser headers, curl or a script on her own
|
|
|
138
138
|
is not affected. Neither is the reading surface above.
|
|
139
139
|
|
|
140
140
|
The two voyage edits are reachable from the boat's own network only. What the
|
|
141
|
-
shore can ask her is a closed set of
|
|
142
|
-
(`history`, `snapshots`, `voyages`, `track`, `phases`
|
|
143
|
-
|
|
141
|
+
shore can ask her is a closed set of ten questions, all of them reads
|
|
142
|
+
(`history`, `snapshots`, `voyages`, `track`, `phases`, `rollups`, `stats`,
|
|
143
|
+
`fuel`, `ais`, `health`); nothing on that socket changes anything aboard, and
|
|
144
|
+
the REST surface above is not proxied to it.
|
|
144
145
|
|
|
145
146
|
### The uplink, when she is paired
|
|
146
147
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "signalk-siparu",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.12",
|
|
4
4
|
"description": "Kept aboard, proven ashore. Position, wind, depth and voyage history, recorded on the boat and readable from anywhere: sealed to the devices you name, signed on the way out. A read-only Signal K plugin with a built-in dashboard.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"signalk-node-server-plugin",
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { MetricAgg, MetricField, RollupHour } from './contract';
|
|
2
|
+
export declare const SPEED_GUARD_KN = 80;
|
|
3
|
+
export interface NumAgg extends MetricAgg {
|
|
4
|
+
min: number;
|
|
5
|
+
max: number;
|
|
6
|
+
avg: number;
|
|
7
|
+
n: number;
|
|
8
|
+
}
|
|
9
|
+
export declare function haversineNm(lat1: number, lon1: number, lat2: number, lon2: number): number;
|
|
10
|
+
export declare function mergeAggs(aggs: readonly MetricAgg[]): NumAgg | null;
|
|
11
|
+
export interface WindowAgg {
|
|
12
|
+
count: number;
|
|
13
|
+
first_ts: number;
|
|
14
|
+
last_ts: number;
|
|
15
|
+
distance_nm: number;
|
|
16
|
+
pos_first: {
|
|
17
|
+
lat: number;
|
|
18
|
+
lon: number;
|
|
19
|
+
} | null;
|
|
20
|
+
pos_last: {
|
|
21
|
+
lat: number;
|
|
22
|
+
lon: number;
|
|
23
|
+
} | null;
|
|
24
|
+
metrics: Partial<Record<MetricField, MetricAgg>>;
|
|
25
|
+
path_metrics?: Record<string, MetricAgg>;
|
|
26
|
+
}
|
|
27
|
+
export declare function mergeHours(hours: readonly RollupHour[], guardKn?: number): WindowAgg;
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.SPEED_GUARD_KN = void 0;
|
|
4
|
+
exports.haversineNm = haversineNm;
|
|
5
|
+
exports.mergeAggs = mergeAggs;
|
|
6
|
+
exports.mergeHours = mergeHours;
|
|
7
|
+
const contract_1 = require("./contract");
|
|
8
|
+
const EARTH_RADIUS_NM = 3440.065;
|
|
9
|
+
exports.SPEED_GUARD_KN = 80;
|
|
10
|
+
const LINEAR_FIELDS = (0, contract_1.metricFields)('linear');
|
|
11
|
+
const LAST_ONLY_FIELDS = (0, contract_1.metricFields)('angular', 'text');
|
|
12
|
+
function haversineNm(lat1, lon1, lat2, lon2) {
|
|
13
|
+
const toRad = (d) => (d * Math.PI) / 180;
|
|
14
|
+
const dLat = toRad(lat2 - lat1);
|
|
15
|
+
const dLon = toRad(lon2 - lon1);
|
|
16
|
+
const a = Math.sin(dLat / 2) ** 2 + Math.cos(toRad(lat1)) * Math.cos(toRad(lat2)) * Math.sin(dLon / 2) ** 2;
|
|
17
|
+
return 2 * EARTH_RADIUS_NM * Math.asin(Math.min(1, Math.sqrt(a)));
|
|
18
|
+
}
|
|
19
|
+
function mergeAggs(aggs) {
|
|
20
|
+
let min = Infinity;
|
|
21
|
+
let max = -Infinity;
|
|
22
|
+
let sum = 0;
|
|
23
|
+
let n = 0;
|
|
24
|
+
let last = null;
|
|
25
|
+
for (const m of aggs) {
|
|
26
|
+
if (typeof m.min !== 'number' || typeof m.max !== 'number' || typeof m.avg !== 'number')
|
|
27
|
+
continue;
|
|
28
|
+
const mn = m.n ?? 1;
|
|
29
|
+
if (m.min < min)
|
|
30
|
+
min = m.min;
|
|
31
|
+
if (m.max > max)
|
|
32
|
+
max = m.max;
|
|
33
|
+
sum += m.avg * mn;
|
|
34
|
+
n += mn;
|
|
35
|
+
if (m.last !== null && m.last !== undefined)
|
|
36
|
+
last = m.last;
|
|
37
|
+
}
|
|
38
|
+
return n > 0 ? { min, max, avg: sum / n, n, last } : null;
|
|
39
|
+
}
|
|
40
|
+
function mergeHours(hours, guardKn = exports.SPEED_GUARD_KN) {
|
|
41
|
+
const sorted = [...hours].sort((a, b) => a.hour.localeCompare(b.hour));
|
|
42
|
+
const metrics = {};
|
|
43
|
+
for (const field of LINEAR_FIELDS) {
|
|
44
|
+
const agg = mergeAggs(sorted.map((h) => h.metrics[field]).filter((m) => m !== undefined));
|
|
45
|
+
if (agg)
|
|
46
|
+
metrics[field] = agg;
|
|
47
|
+
}
|
|
48
|
+
for (const field of LAST_ONLY_FIELDS) {
|
|
49
|
+
for (let i = sorted.length - 1; i >= 0; i--) {
|
|
50
|
+
const m = sorted[i]?.metrics[field];
|
|
51
|
+
if (m && m.last !== null && m.last !== undefined) {
|
|
52
|
+
metrics[field] = { last: m.last };
|
|
53
|
+
break;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
const path_metrics = {};
|
|
58
|
+
const keys = new Set();
|
|
59
|
+
for (const h of sorted)
|
|
60
|
+
if (h.path_metrics)
|
|
61
|
+
for (const k of Object.keys(h.path_metrics))
|
|
62
|
+
keys.add(k);
|
|
63
|
+
for (const key of keys) {
|
|
64
|
+
const agg = mergeAggs(sorted.map((h) => h.path_metrics?.[key]).filter((m) => m !== undefined));
|
|
65
|
+
if (agg)
|
|
66
|
+
path_metrics[key] = agg;
|
|
67
|
+
}
|
|
68
|
+
let distance = sorted.reduce((acc, h) => acc + h.distance_nm, 0);
|
|
69
|
+
for (let i = 0; i < sorted.length - 1; i++) {
|
|
70
|
+
const a = sorted[i];
|
|
71
|
+
const b = sorted[i + 1];
|
|
72
|
+
if (!a?.pos_last || !b?.pos_first)
|
|
73
|
+
continue;
|
|
74
|
+
const dtH = (b.first_ts - a.last_ts) / 3_600_000;
|
|
75
|
+
if (dtH <= 0)
|
|
76
|
+
continue;
|
|
77
|
+
const segNm = haversineNm(a.pos_last.lat, a.pos_last.lon, b.pos_first.lat, b.pos_first.lon);
|
|
78
|
+
if (segNm / dtH <= guardKn)
|
|
79
|
+
distance += segNm;
|
|
80
|
+
}
|
|
81
|
+
return {
|
|
82
|
+
count: sorted.reduce((acc, h) => acc + h.count, 0),
|
|
83
|
+
first_ts: sorted[0]?.first_ts ?? 0,
|
|
84
|
+
last_ts: sorted[sorted.length - 1]?.last_ts ?? 0,
|
|
85
|
+
distance_nm: distance,
|
|
86
|
+
pos_first: sorted.map((h) => h.pos_first).find((p) => p !== null) ?? null,
|
|
87
|
+
pos_last: [...sorted]
|
|
88
|
+
.reverse()
|
|
89
|
+
.map((h) => h.pos_last)
|
|
90
|
+
.find((p) => p !== null) ?? null,
|
|
91
|
+
metrics,
|
|
92
|
+
...(Object.keys(path_metrics).length > 0 ? { path_metrics } : {})
|
|
93
|
+
};
|
|
94
|
+
}
|
package/plugin/dist/ais.d.ts
CHANGED
|
@@ -6,3 +6,4 @@ export interface AisQuery {
|
|
|
6
6
|
}
|
|
7
7
|
export declare function buildAisFeed(vessels: unknown, selfContext: string, now: number, q: AisQuery): AisFeed;
|
|
8
8
|
export declare function clampAisQuery(maxNm?: number, maxAgeMin?: number, limit?: number): AisQuery;
|
|
9
|
+
export declare function seesOtherVessels(vessels: unknown, selfContext: string): boolean;
|
package/plugin/dist/ais.js
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.buildAisFeed = buildAisFeed;
|
|
4
4
|
exports.clampAisQuery = clampAisQuery;
|
|
5
|
+
exports.seesOtherVessels = seesOtherVessels;
|
|
5
6
|
const rollup_1 = require("./rollup");
|
|
6
7
|
const MS_TO_KN = 1.9438;
|
|
7
8
|
function nodeValue(node) {
|
|
@@ -111,3 +112,9 @@ function clampAisQuery(maxNm, maxAgeMin, limit) {
|
|
|
111
112
|
limit: clamp(limit, 30, 1, 400)
|
|
112
113
|
};
|
|
113
114
|
}
|
|
115
|
+
function seesOtherVessels(vessels, selfContext) {
|
|
116
|
+
if (!vessels || typeof vessels !== 'object')
|
|
117
|
+
return false;
|
|
118
|
+
const selfKey = selfContext.replace(/^vessels\./, '').trim();
|
|
119
|
+
return Object.keys(vessels).some((k) => k !== selfKey);
|
|
120
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export interface AisReceiverStatus {
|
|
2
|
+
receiver_seen: boolean;
|
|
3
|
+
first_seen_ts: number | null;
|
|
4
|
+
}
|
|
5
|
+
export declare class AisReceiverMemory {
|
|
6
|
+
private readonly file;
|
|
7
|
+
private seen;
|
|
8
|
+
private written;
|
|
9
|
+
private firstSeenTs;
|
|
10
|
+
private writeChain;
|
|
11
|
+
constructor(dataDir: string);
|
|
12
|
+
load(): void;
|
|
13
|
+
note(now: number): void;
|
|
14
|
+
status(): AisReceiverStatus;
|
|
15
|
+
flush(): Promise<void>;
|
|
16
|
+
private write;
|
|
17
|
+
}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.AisReceiverMemory = void 0;
|
|
37
|
+
const fs = __importStar(require("fs"));
|
|
38
|
+
const path = __importStar(require("path"));
|
|
39
|
+
const FILE_VERSION = 1;
|
|
40
|
+
class AisReceiverMemory {
|
|
41
|
+
file;
|
|
42
|
+
seen = false;
|
|
43
|
+
written = false;
|
|
44
|
+
firstSeenTs = null;
|
|
45
|
+
writeChain = Promise.resolve();
|
|
46
|
+
constructor(dataDir) {
|
|
47
|
+
this.file = path.join(dataDir, 'ais.json');
|
|
48
|
+
}
|
|
49
|
+
load() {
|
|
50
|
+
let text;
|
|
51
|
+
try {
|
|
52
|
+
text = fs.readFileSync(this.file, 'utf8');
|
|
53
|
+
}
|
|
54
|
+
catch {
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
this.seen = true;
|
|
58
|
+
this.written = true;
|
|
59
|
+
try {
|
|
60
|
+
const raw = JSON.parse(text);
|
|
61
|
+
if (raw.v === FILE_VERSION && typeof raw.first_seen_ts === 'number') {
|
|
62
|
+
this.firstSeenTs = raw.first_seen_ts;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
catch {
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
note(now) {
|
|
69
|
+
if (this.written)
|
|
70
|
+
return;
|
|
71
|
+
this.written = true;
|
|
72
|
+
if (!this.seen) {
|
|
73
|
+
this.seen = true;
|
|
74
|
+
this.firstSeenTs = now;
|
|
75
|
+
}
|
|
76
|
+
const shape = { v: FILE_VERSION, first_seen_ts: this.firstSeenTs ?? now };
|
|
77
|
+
this.write(async () => {
|
|
78
|
+
const tmp = `${this.file}.tmp`;
|
|
79
|
+
await fs.promises.writeFile(tmp, JSON.stringify(shape, null, 2), 'utf8');
|
|
80
|
+
await fs.promises.rename(tmp, this.file);
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
status() {
|
|
84
|
+
return { receiver_seen: this.seen, first_seen_ts: this.firstSeenTs };
|
|
85
|
+
}
|
|
86
|
+
flush() {
|
|
87
|
+
return this.writeChain.catch(() => undefined);
|
|
88
|
+
}
|
|
89
|
+
write(run) {
|
|
90
|
+
this.writeChain = this.writeChain.then(run, run).catch(() => {
|
|
91
|
+
this.written = false;
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
exports.AisReceiverMemory = AisReceiverMemory;
|
package/plugin/dist/config.js
CHANGED
|
@@ -4,6 +4,7 @@ exports.CONFIG_SCHEMA = exports.RELAY_URL = exports.KN_TO_MS = exports.INTERNAL
|
|
|
4
4
|
exports.isCalendarMonthDay = isCalendarMonthDay;
|
|
5
5
|
exports.safeRelayUrl = safeRelayUrl;
|
|
6
6
|
exports.resolveOptions = resolveOptions;
|
|
7
|
+
const aggregate_1 = require("./aggregate");
|
|
7
8
|
exports.DEFAULTS = {
|
|
8
9
|
boatName: '',
|
|
9
10
|
snapshotSeconds: 60,
|
|
@@ -29,7 +30,7 @@ exports.INTERNAL = {
|
|
|
29
30
|
samplePeriodMs: 2000,
|
|
30
31
|
staleMs: 30_000,
|
|
31
32
|
fabricationHorizonMs: 120_000,
|
|
32
|
-
rollupSpeedGuardKn:
|
|
33
|
+
rollupSpeedGuardKn: aggregate_1.SPEED_GUARD_KN,
|
|
33
34
|
degradedAfterMs: 5 * 60_000
|
|
34
35
|
};
|
|
35
36
|
exports.KN_TO_MS = 0.514444;
|
|
@@ -26,10 +26,37 @@ export interface Snapshot {
|
|
|
26
26
|
path_values?: Record<string, number>;
|
|
27
27
|
}
|
|
28
28
|
export type MetricField = Exclude<keyof Snapshot, 'ts' | 'path_values'>;
|
|
29
|
+
export type MetricKind = 'position' | 'linear' | 'angular' | 'text';
|
|
30
|
+
export declare const METRIC_KIND: {
|
|
31
|
+
readonly lat: "position";
|
|
32
|
+
readonly lon: "position";
|
|
33
|
+
readonly sog: "linear";
|
|
34
|
+
readonly rate_of_turn: "linear";
|
|
35
|
+
readonly magnetic_variation: "linear";
|
|
36
|
+
readonly magnetic_deviation: "linear";
|
|
37
|
+
readonly wind_speed_apparent: "linear";
|
|
38
|
+
readonly wind_speed_true: "linear";
|
|
39
|
+
readonly wind_gust: "linear";
|
|
40
|
+
readonly air_temp_k: "linear";
|
|
41
|
+
readonly air_pressure_pa: "linear";
|
|
42
|
+
readonly depth: "linear";
|
|
43
|
+
readonly water_temp_k: "linear";
|
|
44
|
+
readonly gps_satellites: "linear";
|
|
45
|
+
readonly cog: "angular";
|
|
46
|
+
readonly heading_mag: "angular";
|
|
47
|
+
readonly heading_true: "angular";
|
|
48
|
+
readonly wind_angle_apparent: "angular";
|
|
49
|
+
readonly wind_direction_true: "angular";
|
|
50
|
+
readonly nav_state: "text";
|
|
51
|
+
readonly ais_class: "text";
|
|
52
|
+
readonly depth_datum: "text";
|
|
53
|
+
};
|
|
54
|
+
export declare function metricFields(...kinds: MetricKind[]): MetricField[];
|
|
29
55
|
export interface MetricAgg {
|
|
30
56
|
min?: number;
|
|
31
57
|
max?: number;
|
|
32
58
|
avg?: number;
|
|
59
|
+
n?: number;
|
|
33
60
|
last: number | string | null;
|
|
34
61
|
}
|
|
35
62
|
export interface RollupHour {
|
|
@@ -77,6 +104,7 @@ export interface SnapshotsQuery {
|
|
|
77
104
|
export interface SnapshotsResult {
|
|
78
105
|
rows: Snapshot[];
|
|
79
106
|
clamped: boolean;
|
|
107
|
+
minutesFrom?: number;
|
|
80
108
|
}
|
|
81
109
|
export interface PathSeriesPoint {
|
|
82
110
|
ts: number;
|
|
@@ -89,6 +117,7 @@ export interface PathSeriesResult {
|
|
|
89
117
|
path: string;
|
|
90
118
|
points: PathSeriesPoint[];
|
|
91
119
|
clamped: boolean;
|
|
120
|
+
minutesFrom?: number;
|
|
92
121
|
}
|
|
93
122
|
export interface HistoryRequest {
|
|
94
123
|
type: 'history';
|
|
@@ -191,6 +220,8 @@ export interface InventoryResult {
|
|
|
191
220
|
}
|
|
192
221
|
export interface HealthResult {
|
|
193
222
|
status: 'ok' | 'degraded';
|
|
223
|
+
security_off?: boolean;
|
|
224
|
+
pairing_locked?: boolean;
|
|
194
225
|
now: number;
|
|
195
226
|
started_at: number;
|
|
196
227
|
version: string;
|
|
@@ -218,6 +249,10 @@ export interface HealthResult {
|
|
|
218
249
|
last_hour: string | null;
|
|
219
250
|
hours_pending: number;
|
|
220
251
|
};
|
|
252
|
+
ais: {
|
|
253
|
+
receiver_seen: boolean;
|
|
254
|
+
first_seen_ts: number | null;
|
|
255
|
+
};
|
|
221
256
|
sealing: {
|
|
222
257
|
devices: number;
|
|
223
258
|
mode: 'sealed' | 'blocked' | 'none';
|
|
@@ -347,3 +382,94 @@ export interface DevicePublicKey {
|
|
|
347
382
|
pub: string;
|
|
348
383
|
approvals?: DeviceApproval[];
|
|
349
384
|
}
|
|
385
|
+
export interface RollupsRequest {
|
|
386
|
+
type: 'rollups';
|
|
387
|
+
id: string;
|
|
388
|
+
from: number;
|
|
389
|
+
to: number;
|
|
390
|
+
}
|
|
391
|
+
export interface RollupsResult {
|
|
392
|
+
rows: RollupHour[];
|
|
393
|
+
}
|
|
394
|
+
export type RollupsResponse = {
|
|
395
|
+
type: 'rollups';
|
|
396
|
+
id: string;
|
|
397
|
+
result: RollupsResult;
|
|
398
|
+
} | {
|
|
399
|
+
type: 'rollups';
|
|
400
|
+
id: string;
|
|
401
|
+
error: {
|
|
402
|
+
code: string;
|
|
403
|
+
message: string;
|
|
404
|
+
};
|
|
405
|
+
};
|
|
406
|
+
export interface StatsRequest {
|
|
407
|
+
type: 'stats';
|
|
408
|
+
id: string;
|
|
409
|
+
}
|
|
410
|
+
export type StatsResponse = {
|
|
411
|
+
type: 'stats';
|
|
412
|
+
id: string;
|
|
413
|
+
result: VoyageStatsResult;
|
|
414
|
+
} | {
|
|
415
|
+
type: 'stats';
|
|
416
|
+
id: string;
|
|
417
|
+
error: {
|
|
418
|
+
code: string;
|
|
419
|
+
message: string;
|
|
420
|
+
};
|
|
421
|
+
};
|
|
422
|
+
export interface FuelPathsView {
|
|
423
|
+
available: string[];
|
|
424
|
+
selected: string[];
|
|
425
|
+
}
|
|
426
|
+
export interface FuelRequest {
|
|
427
|
+
type: 'fuel';
|
|
428
|
+
id: string;
|
|
429
|
+
}
|
|
430
|
+
export type FuelResponse = {
|
|
431
|
+
type: 'fuel';
|
|
432
|
+
id: string;
|
|
433
|
+
result: FuelPathsView;
|
|
434
|
+
} | {
|
|
435
|
+
type: 'fuel';
|
|
436
|
+
id: string;
|
|
437
|
+
error: {
|
|
438
|
+
code: string;
|
|
439
|
+
message: string;
|
|
440
|
+
};
|
|
441
|
+
};
|
|
442
|
+
export interface AisRequest {
|
|
443
|
+
type: 'ais';
|
|
444
|
+
id: string;
|
|
445
|
+
maxNm?: number;
|
|
446
|
+
limit?: number;
|
|
447
|
+
}
|
|
448
|
+
export type AisResponse = {
|
|
449
|
+
type: 'ais';
|
|
450
|
+
id: string;
|
|
451
|
+
result: AisFeed;
|
|
452
|
+
} | {
|
|
453
|
+
type: 'ais';
|
|
454
|
+
id: string;
|
|
455
|
+
error: {
|
|
456
|
+
code: string;
|
|
457
|
+
message: string;
|
|
458
|
+
};
|
|
459
|
+
};
|
|
460
|
+
export interface HealthRequest {
|
|
461
|
+
type: 'health';
|
|
462
|
+
id: string;
|
|
463
|
+
}
|
|
464
|
+
export type HealthResponse = {
|
|
465
|
+
type: 'health';
|
|
466
|
+
id: string;
|
|
467
|
+
result: HealthResult;
|
|
468
|
+
} | {
|
|
469
|
+
type: 'health';
|
|
470
|
+
id: string;
|
|
471
|
+
error: {
|
|
472
|
+
code: string;
|
|
473
|
+
message: string;
|
|
474
|
+
};
|
|
475
|
+
};
|
package/plugin/dist/contract.js
CHANGED
|
@@ -1,2 +1,31 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.METRIC_KIND = void 0;
|
|
4
|
+
exports.metricFields = metricFields;
|
|
5
|
+
exports.METRIC_KIND = {
|
|
6
|
+
lat: 'position',
|
|
7
|
+
lon: 'position',
|
|
8
|
+
sog: 'linear',
|
|
9
|
+
rate_of_turn: 'linear',
|
|
10
|
+
magnetic_variation: 'linear',
|
|
11
|
+
magnetic_deviation: 'linear',
|
|
12
|
+
wind_speed_apparent: 'linear',
|
|
13
|
+
wind_speed_true: 'linear',
|
|
14
|
+
wind_gust: 'linear',
|
|
15
|
+
air_temp_k: 'linear',
|
|
16
|
+
air_pressure_pa: 'linear',
|
|
17
|
+
depth: 'linear',
|
|
18
|
+
water_temp_k: 'linear',
|
|
19
|
+
gps_satellites: 'linear',
|
|
20
|
+
cog: 'angular',
|
|
21
|
+
heading_mag: 'angular',
|
|
22
|
+
heading_true: 'angular',
|
|
23
|
+
wind_angle_apparent: 'angular',
|
|
24
|
+
wind_direction_true: 'angular',
|
|
25
|
+
nav_state: 'text',
|
|
26
|
+
ais_class: 'text',
|
|
27
|
+
depth_datum: 'text'
|
|
28
|
+
};
|
|
29
|
+
function metricFields(...kinds) {
|
|
30
|
+
return Object.keys(exports.METRIC_KIND).filter((f) => kinds.includes(exports.METRIC_KIND[f]));
|
|
31
|
+
}
|
package/plugin/dist/index.js
CHANGED
|
@@ -35,6 +35,7 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
35
35
|
const fs = __importStar(require("node:fs"));
|
|
36
36
|
const path = __importStar(require("node:path"));
|
|
37
37
|
const ais_1 = require("./ais");
|
|
38
|
+
const aisreceiver_1 = require("./aisreceiver");
|
|
38
39
|
const charts_1 = require("./charts");
|
|
39
40
|
const config_1 = require("./config");
|
|
40
41
|
const fingerprint_1 = require("./fingerprint");
|
|
@@ -84,6 +85,7 @@ module.exports = (app) => {
|
|
|
84
85
|
let boatKeyStore = null;
|
|
85
86
|
let sealingLatch = null;
|
|
86
87
|
let anchorStore = null;
|
|
88
|
+
let aisMemory = null;
|
|
87
89
|
let screensNow = null;
|
|
88
90
|
let reloadAnchor = null;
|
|
89
91
|
let timer = null;
|
|
@@ -183,6 +185,14 @@ module.exports = (app) => {
|
|
|
183
185
|
}
|
|
184
186
|
return s;
|
|
185
187
|
}
|
|
188
|
+
function fuelPathsView() {
|
|
189
|
+
return {
|
|
190
|
+
available: state
|
|
191
|
+
? Object.keys(state.dynamicPaths(Date.now())).filter((p) => p.startsWith('propulsion.') && p.endsWith('.fuel.rate'))
|
|
192
|
+
: [],
|
|
193
|
+
selected: opts?.fuelRatePaths ?? []
|
|
194
|
+
};
|
|
195
|
+
}
|
|
186
196
|
async function health() {
|
|
187
197
|
if (!state || !store || !rollups || !query)
|
|
188
198
|
throw new Error('not started');
|
|
@@ -195,6 +205,8 @@ module.exports = (app) => {
|
|
|
195
205
|
started_at: startedAt,
|
|
196
206
|
version: packageVersion(),
|
|
197
207
|
boat_name: opts.boatName || app.getSelfPath('name') || null,
|
|
208
|
+
security_off: (0, pairing_1.securityOff)(app, {}),
|
|
209
|
+
pairing_locked: (0, pairing_1.securityOff)(app, {}) && !(opts?.acceptOpenNetwork ?? false),
|
|
198
210
|
last_delta_ts: state.lastDeltaTs,
|
|
199
211
|
last_snapshot_ts: lastSnapshotTs,
|
|
200
212
|
snapshots_today: snapshotsToday,
|
|
@@ -207,6 +219,7 @@ module.exports = (app) => {
|
|
|
207
219
|
oldest_raw: usage.oldest
|
|
208
220
|
},
|
|
209
221
|
rollup: await rollups.status(now),
|
|
222
|
+
ais: aisMemory?.status() ?? { receiver_seen: false, first_seen_ts: null },
|
|
210
223
|
sealing: (() => {
|
|
211
224
|
const view = screensNow?.();
|
|
212
225
|
const held = sealingState();
|
|
@@ -320,8 +333,17 @@ module.exports = (app) => {
|
|
|
320
333
|
}
|
|
321
334
|
}
|
|
322
335
|
});
|
|
336
|
+
const ais = new aisreceiver_1.AisReceiverMemory(app.getDataDirPath());
|
|
337
|
+
aisMemory = ais;
|
|
338
|
+
ais.load();
|
|
339
|
+
const noteAisTraffic = () => {
|
|
340
|
+
if ((0, ais_1.seesOtherVessels)(app.getPath('vessels'), app.selfContext))
|
|
341
|
+
ais.note(Date.now());
|
|
342
|
+
};
|
|
343
|
+
noteAisTraffic();
|
|
323
344
|
timer = setInterval(() => {
|
|
324
345
|
void writeSnapshot();
|
|
346
|
+
noteAisTraffic();
|
|
325
347
|
}, opts.snapshotSeconds * 1000);
|
|
326
348
|
(0, rest_1.setRestDeps)({
|
|
327
349
|
live,
|
|
@@ -391,6 +413,11 @@ module.exports = (app) => {
|
|
|
391
413
|
onPhasesQuery: async (limit) => ({
|
|
392
414
|
phases: pl.list(Math.min(Math.max(1, limit || 50), 500))
|
|
393
415
|
}),
|
|
416
|
+
onRollupsQuery: async (from, to) => ({ rows: await ru.readHourly(from, to) }),
|
|
417
|
+
onStatsQuery: () => vl.stats(ru, Date.now()),
|
|
418
|
+
onFuelQuery: async () => fuelPathsView(),
|
|
419
|
+
onAisQuery: async (maxNm, limit) => (0, ais_1.buildAisFeed)(app.getPath('vessels'), app.selfContext, Date.now(), (0, ais_1.clampAisQuery)(maxNm, undefined, limit)),
|
|
420
|
+
onHealthQuery: () => health(),
|
|
394
421
|
seal: (frame) => seal.seal(frame),
|
|
395
422
|
onWatching: () => ks.prompt(),
|
|
396
423
|
openAsk: (envelope) => seal.open(envelope),
|
|
@@ -453,6 +480,8 @@ module.exports = (app) => {
|
|
|
453
480
|
await sealingLatch.flush();
|
|
454
481
|
if (anchorStore)
|
|
455
482
|
await anchorStore.flush();
|
|
483
|
+
if (aisMemory)
|
|
484
|
+
await aisMemory.flush();
|
|
456
485
|
state = null;
|
|
457
486
|
store = null;
|
|
458
487
|
rollups = null;
|
|
@@ -466,6 +495,7 @@ module.exports = (app) => {
|
|
|
466
495
|
boatKeyStore = null;
|
|
467
496
|
sealingLatch = null;
|
|
468
497
|
anchorStore = null;
|
|
498
|
+
aisMemory = null;
|
|
469
499
|
screensNow = null;
|
|
470
500
|
reloadAnchor = null;
|
|
471
501
|
app.setPluginStatus('Stopped');
|
|
@@ -498,12 +528,7 @@ module.exports = (app) => {
|
|
|
498
528
|
app,
|
|
499
529
|
acceptOpenNetwork: () => opts?.acceptOpenNetwork ?? false,
|
|
500
530
|
getConfig: () => currentConfig,
|
|
501
|
-
fuelPathsView
|
|
502
|
-
available: state
|
|
503
|
-
? Object.keys(state.dynamicPaths(Date.now())).filter((p) => p.startsWith('propulsion.') && p.endsWith('.fuel.rate'))
|
|
504
|
-
: [],
|
|
505
|
-
selected: opts?.fuelRatePaths ?? []
|
|
506
|
-
}),
|
|
531
|
+
fuelPathsView,
|
|
507
532
|
restart: (configuration) => pluginRestart?.(configuration)
|
|
508
533
|
});
|
|
509
534
|
(0, voyage_routes_1.registerVoyageEditRoutes)(router, {
|
package/plugin/dist/live.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { RemoteLink } from './remotelink';
|
|
2
|
-
import type { PathSeriesResult, SnapshotsQuery, SnapshotsResult, TrackResult, VoyageListResult, PhaseListResult } from './contract';
|
|
2
|
+
import type { PathSeriesResult, SnapshotsQuery, SnapshotsResult, TrackResult, VoyageListResult, PhaseListResult, RollupsResult, VoyageStatsResult, FuelPathsView, AisFeed, HealthResult } from './contract';
|
|
3
3
|
export declare const FRAME_EVERY_MS = 2000;
|
|
4
4
|
export declare const STILL_FRAME_EVERY_MS = 60000;
|
|
5
5
|
export declare const REPORTING_STALE_MS: number;
|
|
@@ -47,6 +47,11 @@ export interface LiveDeps {
|
|
|
47
47
|
onVoyagesQuery?: (limit: number) => Promise<VoyageListResult>;
|
|
48
48
|
onTrackQuery?: (voyageId: number) => Promise<TrackResult>;
|
|
49
49
|
onPhasesQuery?: (limit: number) => Promise<PhaseListResult>;
|
|
50
|
+
onRollupsQuery?: (from: number, to: number) => Promise<RollupsResult>;
|
|
51
|
+
onStatsQuery?: () => Promise<VoyageStatsResult>;
|
|
52
|
+
onFuelQuery?: () => Promise<FuelPathsView>;
|
|
53
|
+
onAisQuery?: (maxNm?: number, limit?: number) => Promise<AisFeed>;
|
|
54
|
+
onHealthQuery?: () => Promise<HealthResult>;
|
|
50
55
|
onWatching?: () => void;
|
|
51
56
|
debug: (msg: string) => void;
|
|
52
57
|
connect?: (url: string, token: string) => LiveSocket;
|
|
@@ -90,6 +95,8 @@ export declare class LiveUplink {
|
|
|
90
95
|
private handleVoyages;
|
|
91
96
|
private handleTrack;
|
|
92
97
|
private handlePhases;
|
|
98
|
+
private handleMore;
|
|
99
|
+
private failedRead;
|
|
93
100
|
private refuse;
|
|
94
101
|
private reply;
|
|
95
102
|
private keepalive;
|
|
@@ -99,8 +106,10 @@ export declare class LiveUplink {
|
|
|
99
106
|
private redial;
|
|
100
107
|
private clearTimers;
|
|
101
108
|
}
|
|
109
|
+
type MoreType = 'rollups' | 'stats' | 'fuel' | 'ais' | 'health';
|
|
102
110
|
export declare function rpcEnvelope(data: string): {
|
|
103
|
-
type: 'history' | 'snapshots' | 'voyages' | 'track' | 'phases';
|
|
111
|
+
type: 'history' | 'snapshots' | 'voyages' | 'track' | 'phases' | MoreType;
|
|
104
112
|
id: string;
|
|
105
113
|
} | null;
|
|
106
114
|
export declare function isWatchingNote(data: string): boolean;
|
|
115
|
+
export {};
|