signalk-siparu 0.2.9 → 0.2.11
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/NOTICE +42 -0
- package/README.md +4 -3
- package/package.json +3 -2
- 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 +102 -10
- package/plugin/dist/query.d.ts +3 -1
- package/plugin/dist/query.js +43 -48
- package/plugin/dist/rollup.d.ts +3 -1
- package/plugin/dist/rollup.js +21 -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/public/assets/Logbook-DpHBKbVS.js +1 -0
- package/public/assets/Map-D84VPs-d.js +111 -0
- package/public/assets/Remote-B66DSZVi.js +1 -0
- package/public/assets/SecurityHelp-D_IuBUYm.js +1 -0
- package/public/assets/Voyage-DBV-C78R.js +1 -0
- package/public/assets/export-BS6h3d6E.js +9 -0
- package/public/assets/index-BwOZQYHE.js +20 -0
- package/public/assets/index-QctSvxcL.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/NOTICE
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
Siparu
|
|
2
|
+
Copyright (c) 2026 Siparu
|
|
3
|
+
|
|
4
|
+
This product is licensed under the Apache License, Version 2.0 (see LICENSE).
|
|
5
|
+
|
|
6
|
+
It includes third-party components under their own licenses:
|
|
7
|
+
|
|
8
|
+
-------------------------------------------------------------------------------
|
|
9
|
+
vector-seamarks - https://github.com/josxha/vector-seamarks
|
|
10
|
+
Copyright (c) 2024, Joscha Eckert (josxha)
|
|
11
|
+
BSD 2-Clause License
|
|
12
|
+
|
|
13
|
+
Redistribution and use in source and binary forms, with or without
|
|
14
|
+
modification, are permitted provided that the following conditions are met:
|
|
15
|
+
|
|
16
|
+
1. Redistributions of source code must retain the above copyright notice,
|
|
17
|
+
this list of conditions and the following disclaimer.
|
|
18
|
+
2. Redistributions in binary form must reproduce the above copyright notice,
|
|
19
|
+
this list of conditions and the following disclaimer in the documentation
|
|
20
|
+
and/or other materials provided with the distribution.
|
|
21
|
+
|
|
22
|
+
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
|
23
|
+
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
|
24
|
+
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
|
25
|
+
ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
|
|
26
|
+
LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
|
27
|
+
CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
|
28
|
+
SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
|
29
|
+
INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
|
30
|
+
CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
|
31
|
+
ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
|
32
|
+
POSSIBILITY OF SUCH DAMAGE.
|
|
33
|
+
|
|
34
|
+
Adapted style layers in webapp/src/map/seamarks-layers.json; seamark sprite
|
|
35
|
+
sheets are served from the tile host.
|
|
36
|
+
|
|
37
|
+
-------------------------------------------------------------------------------
|
|
38
|
+
Map data and assets served from the tile host (not bundled in this package):
|
|
39
|
+
|
|
40
|
+
OpenStreetMap contributors - ODbL 1.0 - https://www.openstreetmap.org/copyright
|
|
41
|
+
Protomaps basemaps - BSD-3-Clause - https://github.com/protomaps/basemaps
|
|
42
|
+
Noto Sans glyphs - SIL OFL 1.1 - https://github.com/protomaps/basemaps-assets
|
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.11",
|
|
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",
|
|
@@ -9,7 +9,8 @@
|
|
|
9
9
|
"main": "plugin/dist/index.js",
|
|
10
10
|
"files": [
|
|
11
11
|
"plugin/dist",
|
|
12
|
-
"public"
|
|
12
|
+
"public",
|
|
13
|
+
"NOTICE"
|
|
13
14
|
],
|
|
14
15
|
"signalk-plugin-enabled-by-default": true,
|
|
15
16
|
"signalk": {
|
|
@@ -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, {
|