signalk-wakelogger 0.2.0-beta.2 → 0.2.0-beta.3
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/CHANGELOG.md +19 -0
- package/README.md +7 -1
- package/dist/config/defaults.d.ts +1 -0
- package/dist/config/defaults.js +1 -0
- package/dist/config/defaults.js.map +1 -1
- package/dist/config/schema.d.ts +8 -0
- package/dist/config/schema.js +8 -0
- package/dist/config/schema.js.map +1 -1
- package/dist/courses/course-store.d.ts +19 -0
- package/dist/courses/course-store.js +182 -0
- package/dist/courses/course-store.js.map +1 -0
- package/dist/courses/native-course.d.ts +41 -0
- package/dist/courses/native-course.js +101 -0
- package/dist/courses/native-course.js.map +1 -0
- package/dist/courses/protocol.d.ts +43 -0
- package/dist/courses/protocol.js +60 -0
- package/dist/courses/protocol.js.map +1 -0
- package/dist/index.js +122 -29
- package/dist/index.js.map +1 -1
- package/dist/telemetry/types.d.ts +13 -0
- package/dist/transport/mqtt-client.d.ts +7 -2
- package/dist/transport/mqtt-client.js +26 -7
- package/dist/transport/mqtt-client.js.map +1 -1
- package/dist/transport/topics.d.ts +2 -0
- package/dist/transport/topics.js +2 -1
- package/dist/transport/topics.js.map +1 -1
- package/dist/trips/recording-store.d.ts +16 -0
- package/dist/trips/recording-store.js +127 -0
- package/dist/trips/recording-store.js.map +1 -0
- package/dist/trips/state-machine.js +3 -3
- package/dist/trips/state-machine.js.map +1 -1
- package/docs/course-sync.md +63 -0
- package/docs/onboard-mapping.md +125 -0
- package/docs/trip-detection.md +7 -1
- package/package.json +15 -9
- package/public/api-client.mjs +44 -0
- package/public/app.mjs +219 -0
- package/public/chart-sources.mjs +57 -0
- package/public/course-progress.mjs +49 -0
- package/public/index.html +16 -0
- package/public/map-preparation.mjs +60 -0
- package/public/style.css +1 -0
- package/public/vendor/LEAFLET-LICENSE +26 -0
- package/public/vendor/images/layers-2x.png +0 -0
- package/public/vendor/images/layers.png +0 -0
- package/public/vendor/images/marker-icon-2x.png +0 -0
- package/public/vendor/images/marker-icon.png +0 -0
- package/public/vendor/images/marker-shadow.png +0 -0
- package/public/vendor/leaflet.css +661 -0
- package/public/vendor/leaflet.js +6 -0
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,25 @@ All notable changes follow [Keep a Changelog](https://keepachangelog.com/) and s
|
|
|
4
4
|
|
|
5
5
|
## [Unreleased]
|
|
6
6
|
|
|
7
|
+
## [0.2.0-beta.3] - 2026-09-14
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
|
|
11
|
+
- Local-only recording mode, durable recording manifests and deferred historical uploads with committed receipt acknowledgements.
|
|
12
|
+
- Validated, retained cloud course synchronization with revision acknowledgements and restart-safe local persistence.
|
|
13
|
+
- Deterministic native Signal K routes, active-course ownership protection and manual waypoint progression.
|
|
14
|
+
- Packaged onboard interactive map using native navigation calculations and discovered Signal K chart resources, including bounded local chart coverage verification.
|
|
15
|
+
|
|
16
|
+
### Fixed
|
|
17
|
+
|
|
18
|
+
- Preserve offline samples and trip boundaries through outages and abrupt restarts while prioritizing current position updates.
|
|
19
|
+
|
|
20
|
+
### Compatibility
|
|
21
|
+
|
|
22
|
+
- Per-recording upload progress and course synchronization require the companion WakeLogger backend update. Deploy that backend before enabling these features.
|
|
23
|
+
- Offline basemaps require separately provisioned licensed Signal K charts. Automatic online tile prefetch remains a documented design.
|
|
24
|
+
|
|
25
|
+
|
|
7
26
|
## [0.2.0-beta.2] - 2026-09-02
|
|
8
27
|
|
|
9
28
|
### Fixed
|
package/README.md
CHANGED
|
@@ -50,14 +50,20 @@ Signal K supplies the onboard observations; this plugin delivers them reliably;
|
|
|
50
50
|
|
|
51
51
|
## Offline operation
|
|
52
52
|
|
|
53
|
+
To defer uploads deliberately, pair the plugin first, then set **Upload mode** to **local_only** and save in Signal K Plugin Config. The setting survives restarts. Recording continues at the selected profile’s normal quality while the plugin makes no Wake Logger pairing or MQTT connection. When ready, set **automatic** and save; live tracking resumes and stored history uploads in the background. The local queue count shows how much remains. This setting controls this plugin’s traffic, not other Signal K plugins or boat internet services.
|
|
54
|
+
|
|
53
55
|
The default queue retains up to seven days or 250 MB. Records have persistent device-scoped sequence numbers and checksums. MQTT broker acknowledgement is not enough to remove a record: Wake Logger must acknowledge that the database commit completed. If a configured limit is reached, the oldest backlog is discarded, the newest data is preserved, and the dropped count is shown in plugin status.
|
|
54
56
|
|
|
55
|
-
Useful status examples include `Not paired`, `connecting`, `online`, `offline`, `degraded`, queue count/size, and dropped-record count. Enable telemetry debug logging only while troubleshooting; it does not log payloads or secrets.
|
|
57
|
+
Useful status examples include `recording_locally`, `Not paired`, `connecting`, `online`, `offline`, `degraded`, queue count/size, and dropped-record count. Enable telemetry debug logging only while troubleshooting; it does not log payloads or secrets.
|
|
56
58
|
|
|
57
59
|
An authenticated Signal K administrator can call `POST /plugins/signalk-wakelogger/forget-credentials` to forget the local cloud credential before entering a fresh pairing code. This intentionally preserves the installation identity, consumed-code fingerprint, retired device outboxes, sequence records and diagnostics; it does not revoke the cloud association, so revoke it in Wake Logger as well when retiring a device. A forgotten pairing code must be replaced with a new one.
|
|
58
60
|
|
|
59
61
|
Wake Logger administrators maintain approved telemetry profiles. Vessel Owners and First Mates can select one; other crew can see the selection but cannot change it. Navigation stays at its configured priority while depth and apparent wind can slow under constrained or offline conditions. Profile revisions and application results are visible to Wake Logger operations.
|
|
60
62
|
|
|
63
|
+
## Course navigation
|
|
64
|
+
|
|
65
|
+
Select a race course in Wake Logger to deliver it to the boat. The plugin caches the course and creates a named native Signal K route, which remains available offline. If another app controls the active route, Wake Logger reports the mismatch and waits for explicit local activation. Course progression and navigation values come from Signal K; map readiness is tracked separately from course receipt. See [course synchronization](docs/course-sync.md) for the protocol, local controls and chart-readiness states.
|
|
66
|
+
|
|
61
67
|
## Development
|
|
62
68
|
|
|
63
69
|
```sh
|
package/dist/config/defaults.js
CHANGED
|
@@ -14,6 +14,7 @@ exports.DEFAULTS = {
|
|
|
14
14
|
function parseConfig(value) {
|
|
15
15
|
const input = value;
|
|
16
16
|
return {
|
|
17
|
+
uploadMode: input.uploadMode === 'local_only' ? 'local_only' : 'automatic',
|
|
17
18
|
pairingCode: typeof input.pairingCode === 'string' && input.pairingCode.trim() ? input.pairingCode.trim() : undefined,
|
|
18
19
|
pairingApiUrl: typeof input.pairingApiUrl === 'string' && /^https:\/\//.test(input.pairingApiUrl)
|
|
19
20
|
? input.pairingApiUrl
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"defaults.js","sourceRoot":"","sources":["../../src/config/defaults.ts"],"names":[],"mappings":";;;
|
|
1
|
+
{"version":3,"file":"defaults.js","sourceRoot":"","sources":["../../src/config/defaults.ts"],"names":[],"mappings":";;;AAoBA,kCAcC;AAlCY,QAAA,QAAQ,GAAG;IACtB,aAAa,EAAE,gDAAgD;IAC/D,cAAc,EAAE,IAAI;IACpB,SAAS,EAAE,EAAE;IACb,eAAe,EAAE,EAAE,GAAG,IAAI;IAC1B,cAAc,EAAE,GAAG,GAAG,IAAI,GAAG,IAAI;IACjC,cAAc,EAAE,CAAC,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,IAAI;IACvC,YAAY,EAAE,CAAC,GAAG,IAAI,GAAG,IAAI;CACrB,CAAA;AAYV,SAAgB,WAAW,CAAC,KAAa;IACvC,MAAM,KAAK,GAAG,KAAgC,CAAA;IAC9C,OAAO;QACL,UAAU,EAAE,KAAK,CAAC,UAAU,KAAK,YAAY,CAAC,CAAC,CAAC,YAAY,CAAC,CAAC,CAAC,WAAW;QAC1E,WAAW,EAAE,OAAO,KAAK,CAAC,WAAW,KAAK,QAAQ,IAAI,KAAK,CAAC,WAAW,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC,KAAK,CAAC,WAAW,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC,SAAS;QACrH,aAAa,EACX,OAAO,KAAK,CAAC,aAAa,KAAK,QAAQ,IAAI,aAAa,CAAC,IAAI,CAAC,KAAK,CAAC,aAAa,CAAC;YAChF,CAAC,CAAC,KAAK,CAAC,aAAa;YACrB,CAAC,CAAC,gBAAQ,CAAC,aAAa;QAC5B,cAAc,EAAE,aAAa,CAAC,KAAK,CAAC,cAAc,EAAE,gBAAQ,CAAC,cAAc,EAAE,GAAG,EAAE,MAAM,CAAC;QACzF,WAAW,EAAE,aAAa,CAAC,KAAK,CAAC,WAAW,EAAE,GAAG,EAAE,EAAE,EAAE,IAAI,CAAC;QAC5D,aAAa,EAAE,aAAa,CAAC,KAAK,CAAC,aAAa,EAAE,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC;QAC3D,cAAc,EAAE,KAAK,CAAC,cAAc,KAAK,IAAI;KAC9C,CAAA;AACH,CAAC;AAED,SAAS,aAAa,CAAC,KAAc,EAAE,QAAgB,EAAE,GAAW,EAAE,GAAW;IAC/E,OAAO,OAAO,KAAK,KAAK,QAAQ,IAAI,MAAM,CAAC,QAAQ,CAAC,KAAK,CAAC,IAAI,KAAK,IAAI,GAAG,IAAI,KAAK,IAAI,GAAG,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,QAAQ,CAAA;AAC/G,CAAC"}
|
package/dist/config/schema.d.ts
CHANGED
|
@@ -2,6 +2,14 @@ export declare const configSchema: {
|
|
|
2
2
|
readonly type: "object";
|
|
3
3
|
readonly additionalProperties: false;
|
|
4
4
|
readonly properties: {
|
|
5
|
+
readonly uploadMode: {
|
|
6
|
+
readonly type: "string";
|
|
7
|
+
readonly title: "Upload mode";
|
|
8
|
+
readonly enum: readonly ["automatic", "local_only"];
|
|
9
|
+
readonly enumNames: readonly ["Automatic upload", "Record locally"];
|
|
10
|
+
readonly default: "automatic";
|
|
11
|
+
readonly description: "local_only records on this boat without any Wake Logger network traffic. Pair first. Save automatic when ready to upload the queue.";
|
|
12
|
+
};
|
|
5
13
|
readonly pairingCode: {
|
|
6
14
|
readonly type: "string";
|
|
7
15
|
readonly title: "Wake Logger pairing code";
|
package/dist/config/schema.js
CHANGED
|
@@ -5,6 +5,14 @@ exports.configSchema = {
|
|
|
5
5
|
type: 'object',
|
|
6
6
|
additionalProperties: false,
|
|
7
7
|
properties: {
|
|
8
|
+
uploadMode: {
|
|
9
|
+
type: 'string',
|
|
10
|
+
title: 'Upload mode',
|
|
11
|
+
enum: ['automatic', 'local_only'],
|
|
12
|
+
enumNames: ['Automatic upload', 'Record locally'],
|
|
13
|
+
default: 'automatic',
|
|
14
|
+
description: 'local_only records on this boat without any Wake Logger network traffic. Pair first. Save automatic when ready to upload the queue.'
|
|
15
|
+
},
|
|
8
16
|
pairingCode: {
|
|
9
17
|
type: 'string',
|
|
10
18
|
title: 'Wake Logger pairing code',
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"schema.js","sourceRoot":"","sources":["../../src/config/schema.ts"],"names":[],"mappings":";;;AAAa,QAAA,YAAY,GAAG;IAC1B,IAAI,EAAE,QAAQ;IACd,oBAAoB,EAAE,KAAK;IAC3B,UAAU,EAAE;QACV,WAAW,EAAE;YACX,IAAI,EAAE,QAAQ;YACd,KAAK,EAAE,0BAA0B;YACjC,WAAW,EAAE,qFAAqF;SACnG;QACD,aAAa,EAAE;YACb,IAAI,EAAE,QAAQ;YACd,KAAK,EAAE,iBAAiB;YACxB,OAAO,EAAE,gDAAgD;YACzD,OAAO,EAAE,WAAW;SACrB;QACD,cAAc,EAAE;YACd,IAAI,EAAE,SAAS;YACf,KAAK,EAAE,gCAAgC;YACvC,OAAO,EAAE,GAAG;YACZ,OAAO,EAAE,KAAK;YACd,OAAO,EAAE,IAAI;SACd;QACD,WAAW,EAAE;YACX,IAAI,EAAE,SAAS;YACf,KAAK,EAAE,iCAAiC;YACxC,OAAO,EAAE,EAAE;YACX,OAAO,EAAE,IAAI;YACb,OAAO,EAAE,GAAG;SACb;QACD,aAAa,EAAE;YACb,IAAI,EAAE,SAAS;YACf,KAAK,EAAE,kCAAkC;YACzC,OAAO,EAAE,CAAC;YACV,OAAO,EAAE,EAAE;YACX,OAAO,EAAE,CAAC;SACX;QACD,cAAc,EAAE;YACd,IAAI,EAAE,SAAS;YACf,KAAK,EAAE,gCAAgC;YACvC,OAAO,EAAE,KAAK;SACf;KACF;CACO,CAAA"}
|
|
1
|
+
{"version":3,"file":"schema.js","sourceRoot":"","sources":["../../src/config/schema.ts"],"names":[],"mappings":";;;AAAa,QAAA,YAAY,GAAG;IAC1B,IAAI,EAAE,QAAQ;IACd,oBAAoB,EAAE,KAAK;IAC3B,UAAU,EAAE;QACV,UAAU,EAAE;YACV,IAAI,EAAE,QAAQ;YACd,KAAK,EAAE,aAAa;YACpB,IAAI,EAAE,CAAC,WAAW,EAAE,YAAY,CAAC;YACjC,SAAS,EAAE,CAAC,kBAAkB,EAAE,gBAAgB,CAAC;YACjD,OAAO,EAAE,WAAW;YACpB,WAAW,EAAE,qIAAqI;SACnJ;QACD,WAAW,EAAE;YACX,IAAI,EAAE,QAAQ;YACd,KAAK,EAAE,0BAA0B;YACjC,WAAW,EAAE,qFAAqF;SACnG;QACD,aAAa,EAAE;YACb,IAAI,EAAE,QAAQ;YACd,KAAK,EAAE,iBAAiB;YACxB,OAAO,EAAE,gDAAgD;YACzD,OAAO,EAAE,WAAW;SACrB;QACD,cAAc,EAAE;YACd,IAAI,EAAE,SAAS;YACf,KAAK,EAAE,gCAAgC;YACvC,OAAO,EAAE,GAAG;YACZ,OAAO,EAAE,KAAK;YACd,OAAO,EAAE,IAAI;SACd;QACD,WAAW,EAAE;YACX,IAAI,EAAE,SAAS;YACf,KAAK,EAAE,iCAAiC;YACxC,OAAO,EAAE,EAAE;YACX,OAAO,EAAE,IAAI;YACb,OAAO,EAAE,GAAG;SACb;QACD,aAAa,EAAE;YACb,IAAI,EAAE,SAAS;YACf,KAAK,EAAE,kCAAkC;YACzC,OAAO,EAAE,CAAC;YACV,OAAO,EAAE,EAAE;YACX,OAAO,EAAE,CAAC;SACX;QACD,cAAc,EAAE;YACd,IAAI,EAAE,SAAS;YACf,KAAK,EAAE,gCAAgC;YACvC,OAAO,EAAE,KAAK;SACf;KACF;CACO,CAAA"}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { NativeCourseService } from './native-course';
|
|
2
|
+
import { type CourseAcknowledgement } from './protocol';
|
|
3
|
+
export declare class CourseStore {
|
|
4
|
+
private readonly target;
|
|
5
|
+
private readonly native;
|
|
6
|
+
private snapshot;
|
|
7
|
+
private operation;
|
|
8
|
+
constructor(target: string, native: NativeCourseService);
|
|
9
|
+
open(): Promise<void>;
|
|
10
|
+
acknowledgement(): Promise<CourseAcknowledgement | undefined>;
|
|
11
|
+
status(): Promise<object>;
|
|
12
|
+
receive(payload: Buffer): Promise<CourseAcknowledgement>;
|
|
13
|
+
activate(): Promise<void>;
|
|
14
|
+
setMapReadiness(revision: unknown, status: unknown): Promise<void>;
|
|
15
|
+
close(): Promise<void>;
|
|
16
|
+
private activation;
|
|
17
|
+
private persist;
|
|
18
|
+
private exclusive;
|
|
19
|
+
}
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.CourseStore = void 0;
|
|
7
|
+
const node_fs_1 = require("node:fs");
|
|
8
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
9
|
+
const native_course_1 = require("./native-course");
|
|
10
|
+
const protocol_1 = require("./protocol");
|
|
11
|
+
class CourseStore {
|
|
12
|
+
target;
|
|
13
|
+
native;
|
|
14
|
+
snapshot = { version: 1 };
|
|
15
|
+
operation = Promise.resolve();
|
|
16
|
+
constructor(target, native) {
|
|
17
|
+
this.target = target;
|
|
18
|
+
this.native = native;
|
|
19
|
+
}
|
|
20
|
+
async open() {
|
|
21
|
+
try {
|
|
22
|
+
const stored = JSON.parse(await node_fs_1.promises.readFile(this.target, 'utf8'));
|
|
23
|
+
if (stored.version !== 1)
|
|
24
|
+
throw new protocol_1.CourseError('course_checkpoint_invalid');
|
|
25
|
+
if (stored.desired)
|
|
26
|
+
stored.desired = (0, protocol_1.parseCourse)(Buffer.from(JSON.stringify(stored.desired)));
|
|
27
|
+
if (stored.cachedCourse) {
|
|
28
|
+
const course = (0, protocol_1.parseCourse)(Buffer.from(JSON.stringify(stored.cachedCourse)));
|
|
29
|
+
if (course.action !== 'activate')
|
|
30
|
+
throw new protocol_1.CourseError('course_checkpoint_invalid');
|
|
31
|
+
stored.cachedCourse = course;
|
|
32
|
+
}
|
|
33
|
+
if (stored.desired?.action === 'activate' && JSON.stringify(stored.desired) !== JSON.stringify(stored.cachedCourse))
|
|
34
|
+
throw new protocol_1.CourseError('course_checkpoint_invalid');
|
|
35
|
+
if (stored.acknowledgement && (!['applied', 'rejected'].includes(stored.acknowledgement.status) || !Number.isSafeInteger(stored.acknowledgement.revision) || stored.acknowledgement.revision < 0))
|
|
36
|
+
throw new protocol_1.CourseError('course_checkpoint_invalid');
|
|
37
|
+
this.snapshot = stored;
|
|
38
|
+
if (stored.cachedCourse && this.native.available()) {
|
|
39
|
+
try {
|
|
40
|
+
await this.native.ensureResource(stored.cachedCourse);
|
|
41
|
+
// The installed native clearDestination wrapper does not persist its
|
|
42
|
+
// clear. Reconcile desired clear even offline after server restart.
|
|
43
|
+
if (stored.desired?.action === 'clear')
|
|
44
|
+
await this.native.clearOwned(stored.cachedCourse);
|
|
45
|
+
}
|
|
46
|
+
catch {
|
|
47
|
+
this.snapshot.acknowledgement = { v: 1, revision: stored.desired?.revision ?? stored.cachedCourse.revision, status: 'rejected', errorCode: 'native_route_restore_failed' };
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
catch (error) {
|
|
52
|
+
if (error.code !== 'ENOENT')
|
|
53
|
+
throw error;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
async acknowledgement() {
|
|
57
|
+
const acknowledgement = this.snapshot.acknowledgement;
|
|
58
|
+
if (!acknowledgement)
|
|
59
|
+
return undefined;
|
|
60
|
+
return { ...acknowledgement, activation: await this.activation(), mapReadiness: this.snapshot.mapReadiness?.revision === this.snapshot.desired?.revision ? this.snapshot.mapReadiness?.status ?? 'unknown' : 'unknown' };
|
|
61
|
+
}
|
|
62
|
+
async status() {
|
|
63
|
+
const course = this.snapshot.cachedCourse;
|
|
64
|
+
const native = await this.native.current();
|
|
65
|
+
return {
|
|
66
|
+
desired: this.snapshot.desired ?? null, cachedCourse: course ?? null,
|
|
67
|
+
acknowledgement: await this.acknowledgement() ?? null,
|
|
68
|
+
routePoints: course ? (0, protocol_1.coursePoints)(course) : [],
|
|
69
|
+
native: {
|
|
70
|
+
available: this.native.available(), course: native,
|
|
71
|
+
ownedRouteId: course ? (0, native_course_1.nativeRouteId)(course.courseId) : null,
|
|
72
|
+
activeMatchesDesired: !!course && this.snapshot.desired?.action === 'activate' && await this.native.matches(course), conflict: await this.activation() === 'conflict'
|
|
73
|
+
}
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
receive(payload) {
|
|
77
|
+
return this.exclusive(async () => {
|
|
78
|
+
let document;
|
|
79
|
+
try {
|
|
80
|
+
document = (0, protocol_1.parseCourse)(payload);
|
|
81
|
+
const previous = this.snapshot.desired;
|
|
82
|
+
if (previous && document.revision < previous.revision)
|
|
83
|
+
throw new protocol_1.CourseError('course_revision_rollback');
|
|
84
|
+
if (previous && document.revision === previous.revision && JSON.stringify(previous) !== JSON.stringify(document))
|
|
85
|
+
throw new protocol_1.CourseError('course_revision_conflict');
|
|
86
|
+
const duplicate = previous?.revision === document.revision && this.snapshot.acknowledgement?.status === 'applied';
|
|
87
|
+
const previousCourse = this.snapshot.cachedCourse;
|
|
88
|
+
// Persist the accepted desired document before any navigation action.
|
|
89
|
+
const next = { ...this.snapshot, desired: document, cachedCourse: document.action === 'activate' ? document : previousCourse, acknowledgement: undefined };
|
|
90
|
+
await this.persist(next);
|
|
91
|
+
this.snapshot = next;
|
|
92
|
+
if (document.action === 'clear')
|
|
93
|
+
await this.native.clearOwned(previousCourse);
|
|
94
|
+
else {
|
|
95
|
+
await this.native.ensureResource(document);
|
|
96
|
+
if (!duplicate)
|
|
97
|
+
await this.native.activate(document, false, previousCourse);
|
|
98
|
+
}
|
|
99
|
+
const acknowledgement = { v: 1, revision: document.revision, status: 'applied', activation: await this.activation() };
|
|
100
|
+
await this.persist({ ...this.snapshot, acknowledgement });
|
|
101
|
+
this.snapshot.acknowledgement = acknowledgement;
|
|
102
|
+
return acknowledgement;
|
|
103
|
+
}
|
|
104
|
+
catch (error) {
|
|
105
|
+
const acknowledgement = { v: 1, revision: extractRevision(payload), status: 'rejected', errorCode: error instanceof protocol_1.CourseError ? error.code : 'native_course_failed', activation: await this.activation() };
|
|
106
|
+
await this.persist({ ...this.snapshot, acknowledgement });
|
|
107
|
+
this.snapshot.acknowledgement = acknowledgement;
|
|
108
|
+
return acknowledgement;
|
|
109
|
+
}
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
activate() {
|
|
113
|
+
return this.exclusive(async () => {
|
|
114
|
+
const course = this.snapshot.cachedCourse;
|
|
115
|
+
if (!course || this.snapshot.desired?.action !== 'activate')
|
|
116
|
+
throw new protocol_1.CourseError('no_selected_course');
|
|
117
|
+
await this.native.ensureResource(course);
|
|
118
|
+
await this.native.activate(course, true);
|
|
119
|
+
const acknowledgement = { v: 1, revision: course.revision, status: 'applied', activation: 'active' };
|
|
120
|
+
await this.persist({ ...this.snapshot, acknowledgement });
|
|
121
|
+
this.snapshot.acknowledgement = acknowledgement;
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
setMapReadiness(revision, status) {
|
|
125
|
+
return this.exclusive(async () => {
|
|
126
|
+
if (revision !== this.snapshot.desired?.revision || !Number.isSafeInteger(revision))
|
|
127
|
+
throw new protocol_1.CourseError('course_revision_conflict');
|
|
128
|
+
if (typeof status !== 'string' || !['unknown', 'unavailable', 'online_only', 'preparing', 'offline_ready'].includes(status))
|
|
129
|
+
throw new protocol_1.CourseError('map_readiness_invalid');
|
|
130
|
+
const next = { ...this.snapshot, mapReadiness: { revision: revision, status: status } };
|
|
131
|
+
await this.persist(next);
|
|
132
|
+
this.snapshot = next;
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
async close() { await this.operation; }
|
|
136
|
+
async activation() {
|
|
137
|
+
const course = this.snapshot.cachedCourse;
|
|
138
|
+
if (course && this.snapshot.desired?.action === 'activate' && await this.native.matches(course))
|
|
139
|
+
return 'active';
|
|
140
|
+
return (await this.native.current())?.activeRoute?.href ? 'conflict' : 'inactive';
|
|
141
|
+
}
|
|
142
|
+
async persist(value) {
|
|
143
|
+
await node_fs_1.promises.mkdir(node_path_1.default.dirname(this.target), { recursive: true, mode: 0o700 });
|
|
144
|
+
const temporary = `${this.target}.tmp`;
|
|
145
|
+
const handle = await node_fs_1.promises.open(temporary, 'w', 0o600);
|
|
146
|
+
try {
|
|
147
|
+
await handle.writeFile(`${JSON.stringify(value)}\n`);
|
|
148
|
+
await handle.sync();
|
|
149
|
+
}
|
|
150
|
+
finally {
|
|
151
|
+
await handle.close();
|
|
152
|
+
}
|
|
153
|
+
await node_fs_1.promises.rename(temporary, this.target);
|
|
154
|
+
if (process.platform !== 'win32') {
|
|
155
|
+
const directory = await node_fs_1.promises.open(node_path_1.default.dirname(this.target), 'r');
|
|
156
|
+
try {
|
|
157
|
+
await directory.sync();
|
|
158
|
+
}
|
|
159
|
+
finally {
|
|
160
|
+
await directory.close();
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
exclusive(action) {
|
|
165
|
+
const result = this.operation.then(action);
|
|
166
|
+
this.operation = result.then(() => undefined, () => undefined);
|
|
167
|
+
return result;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
exports.CourseStore = CourseStore;
|
|
171
|
+
function extractRevision(payload) {
|
|
172
|
+
if (payload.length > 64 * 1024)
|
|
173
|
+
return 0;
|
|
174
|
+
try {
|
|
175
|
+
const value = JSON.parse(payload.toString('utf8'));
|
|
176
|
+
return Number.isSafeInteger(value.revision) && (value.revision ?? 0) > 0 ? value.revision : 0;
|
|
177
|
+
}
|
|
178
|
+
catch {
|
|
179
|
+
return 0;
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
//# sourceMappingURL=course-store.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"course-store.js","sourceRoot":"","sources":["../../src/courses/course-store.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { type ActiveCourseDocument } from './protocol';
|
|
2
|
+
export interface NativeCourse {
|
|
3
|
+
activeRoute?: {
|
|
4
|
+
href?: string;
|
|
5
|
+
pointIndex?: number;
|
|
6
|
+
pointTotal?: number;
|
|
7
|
+
reverse?: boolean;
|
|
8
|
+
name?: string;
|
|
9
|
+
} | null;
|
|
10
|
+
nextPoint?: unknown;
|
|
11
|
+
previousPoint?: unknown;
|
|
12
|
+
startTime?: string;
|
|
13
|
+
[key: string]: unknown;
|
|
14
|
+
}
|
|
15
|
+
export interface NativeCourseApp {
|
|
16
|
+
resourcesApi?: {
|
|
17
|
+
getResource: (type: string, id: string) => Promise<unknown>;
|
|
18
|
+
setResource: (type: string, id: string, value: unknown) => Promise<unknown> | void;
|
|
19
|
+
};
|
|
20
|
+
getCourse?: () => Promise<NativeCourse> | NativeCourse;
|
|
21
|
+
activateRoute?: (options: {
|
|
22
|
+
href: string;
|
|
23
|
+
pointIndex: number;
|
|
24
|
+
reverse: boolean;
|
|
25
|
+
}) => Promise<unknown> | void;
|
|
26
|
+
clearDestination?: () => Promise<unknown> | void;
|
|
27
|
+
}
|
|
28
|
+
export declare function nativeRouteId(courseId: string): string;
|
|
29
|
+
export declare function nativeRouteHref(courseId: string): string;
|
|
30
|
+
export declare class NativeCourseService {
|
|
31
|
+
private readonly app;
|
|
32
|
+
constructor(app: NativeCourseApp);
|
|
33
|
+
available(): boolean;
|
|
34
|
+
current(): Promise<NativeCourse | null>;
|
|
35
|
+
matches(course: ActiveCourseDocument): Promise<boolean>;
|
|
36
|
+
ensureResource(course: ActiveCourseDocument): Promise<void>;
|
|
37
|
+
activate(course: ActiveCourseDocument, explicit: boolean, previousCourse?: ActiveCourseDocument): Promise<'active' | 'conflict'>;
|
|
38
|
+
clearOwned(course?: ActiveCourseDocument): Promise<void>;
|
|
39
|
+
private readRoute;
|
|
40
|
+
private assertAvailable;
|
|
41
|
+
}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.NativeCourseService = void 0;
|
|
4
|
+
exports.nativeRouteId = nativeRouteId;
|
|
5
|
+
exports.nativeRouteHref = nativeRouteHref;
|
|
6
|
+
const node_crypto_1 = require("node:crypto");
|
|
7
|
+
const protocol_1 = require("./protocol");
|
|
8
|
+
function nativeRouteId(courseId) {
|
|
9
|
+
const hash = (0, node_crypto_1.createHash)('sha256').update(`signalk-wakelogger:course:${courseId}`).digest('hex');
|
|
10
|
+
return `${hash.slice(0, 8)}-${hash.slice(8, 12)}-4${hash.slice(13, 16)}-a${hash.slice(17, 20)}-${hash.slice(20, 32)}`;
|
|
11
|
+
}
|
|
12
|
+
function nativeRouteHref(courseId) { return `/resources/routes/${nativeRouteId(courseId)}`; }
|
|
13
|
+
class NativeCourseService {
|
|
14
|
+
app;
|
|
15
|
+
constructor(app) {
|
|
16
|
+
this.app = app;
|
|
17
|
+
}
|
|
18
|
+
available() { return [this.app.resourcesApi?.getResource, this.app.resourcesApi?.setResource, this.app.getCourse, this.app.activateRoute, this.app.clearDestination].every((method) => typeof method === 'function'); }
|
|
19
|
+
async current() { return this.app.getCourse ? structuredClone(await this.app.getCourse() ?? null) : null; }
|
|
20
|
+
async matches(course) { return (await this.current())?.activeRoute?.href === nativeRouteHref(course.courseId); }
|
|
21
|
+
async ensureResource(course) {
|
|
22
|
+
this.assertAvailable();
|
|
23
|
+
const id = nativeRouteId(course.courseId);
|
|
24
|
+
const resource = {
|
|
25
|
+
name: course.name, description: `Wake Logger course ${course.courseId}`,
|
|
26
|
+
feature: {
|
|
27
|
+
type: 'Feature', geometry: { type: 'LineString', coordinates: (0, protocol_1.coursePoints)(course).map((point) => [point.longitude, point.latitude]) },
|
|
28
|
+
properties: { wakelogger: { courseId: course.courseId, revision: course.revision }, coordinatesMeta: (0, protocol_1.coursePoints)(course).map((point) => ({ name: point.name, ...(point.notes !== undefined ? { notes: point.notes } : {}) })) }
|
|
29
|
+
}
|
|
30
|
+
};
|
|
31
|
+
const existing = await this.readRoute(id);
|
|
32
|
+
if (existing && !ownedBy(existing, course.courseId))
|
|
33
|
+
throw new protocol_1.CourseError('native_route_conflict');
|
|
34
|
+
if (JSON.stringify(existing) === JSON.stringify(resource))
|
|
35
|
+
return;
|
|
36
|
+
await this.app.resourcesApi.setResource('routes', id, resource);
|
|
37
|
+
// Signal K 2.31's wrapper does not return its provider write promise. Read
|
|
38
|
+
// back the resource before activating, and fail visibly if it never commits.
|
|
39
|
+
for (let attempt = 0; attempt < 20; attempt += 1) {
|
|
40
|
+
const stored = await this.readRoute(id);
|
|
41
|
+
if (stored && ownedBy(stored, course.courseId)
|
|
42
|
+
&& JSON.stringify(stored.feature.geometry) === JSON.stringify(resource.feature.geometry)
|
|
43
|
+
&& stored.feature.properties.wakelogger.revision === course.revision)
|
|
44
|
+
return;
|
|
45
|
+
await new Promise((resolve) => setTimeout(resolve, 50));
|
|
46
|
+
}
|
|
47
|
+
throw new protocol_1.CourseError('native_route_write_unconfirmed');
|
|
48
|
+
}
|
|
49
|
+
async activate(course, explicit, previousCourse) {
|
|
50
|
+
this.assertAvailable();
|
|
51
|
+
const current = (await this.current())?.activeRoute?.href;
|
|
52
|
+
const target = nativeRouteHref(course.courseId);
|
|
53
|
+
if (!explicit && current && current !== target) {
|
|
54
|
+
if (!previousCourse || current !== nativeRouteHref(previousCourse.courseId))
|
|
55
|
+
return 'conflict';
|
|
56
|
+
const previousResource = await this.readRoute(nativeRouteId(previousCourse.courseId));
|
|
57
|
+
if (!previousResource || !ownedBy(previousResource, previousCourse.courseId))
|
|
58
|
+
return 'conflict';
|
|
59
|
+
}
|
|
60
|
+
// Replayed desired state must not reset an ongoing native course's progress.
|
|
61
|
+
if (current === target && !explicit && previousCourse?.revision === course.revision)
|
|
62
|
+
return 'active';
|
|
63
|
+
const existingIndex = (await this.current())?.activeRoute?.pointIndex;
|
|
64
|
+
const pointIndex = current === target && !explicit && Number.isInteger(existingIndex)
|
|
65
|
+
? Math.min(existingIndex, (0, protocol_1.coursePoints)(course).length - 1)
|
|
66
|
+
: course.activeWaypointIndex ?? Math.min(1, (0, protocol_1.coursePoints)(course).length - 1);
|
|
67
|
+
await this.app.activateRoute({ href: target, pointIndex, reverse: false });
|
|
68
|
+
if (!await this.matches(course))
|
|
69
|
+
throw new protocol_1.CourseError('native_activation_unconfirmed');
|
|
70
|
+
return 'active';
|
|
71
|
+
}
|
|
72
|
+
async clearOwned(course) {
|
|
73
|
+
this.assertAvailable();
|
|
74
|
+
if (course && await this.matches(course)) {
|
|
75
|
+
const resource = await this.readRoute(nativeRouteId(course.courseId));
|
|
76
|
+
if (resource && ownedBy(resource, course.courseId))
|
|
77
|
+
await this.app.clearDestination();
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
async readRoute(id) {
|
|
81
|
+
try {
|
|
82
|
+
return await this.app.resourcesApi.getResource('routes', id);
|
|
83
|
+
}
|
|
84
|
+
catch (error) {
|
|
85
|
+
const failure = error;
|
|
86
|
+
if (/no provider for routes/i.test(failure.message ?? ''))
|
|
87
|
+
throw new protocol_1.CourseError('native_routes_unavailable');
|
|
88
|
+
if (failure.status === 404 || failure.statusCode === 404 || failure.code === 'ENOENT' || /not found/i.test(failure.message ?? ''))
|
|
89
|
+
return undefined;
|
|
90
|
+
throw error;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
assertAvailable() { if (!this.available())
|
|
94
|
+
throw new protocol_1.CourseError('native_course_unavailable'); }
|
|
95
|
+
}
|
|
96
|
+
exports.NativeCourseService = NativeCourseService;
|
|
97
|
+
function ownedBy(value, courseId) {
|
|
98
|
+
const candidate = value;
|
|
99
|
+
return candidate?.feature?.properties?.wakelogger?.courseId === courseId;
|
|
100
|
+
}
|
|
101
|
+
//# sourceMappingURL=native-course.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"native-course.js","sourceRoot":"","sources":["../../src/courses/native-course.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
export interface CoursePoint {
|
|
2
|
+
id: string;
|
|
3
|
+
name: string;
|
|
4
|
+
latitude: number;
|
|
5
|
+
longitude: number;
|
|
6
|
+
notes?: string;
|
|
7
|
+
}
|
|
8
|
+
export interface ActiveCourseDocument {
|
|
9
|
+
v: 1;
|
|
10
|
+
action: 'activate';
|
|
11
|
+
revision: number;
|
|
12
|
+
courseId: string;
|
|
13
|
+
racePlanId?: number;
|
|
14
|
+
name: string;
|
|
15
|
+
updatedAt: string;
|
|
16
|
+
start: CoursePoint;
|
|
17
|
+
marks: CoursePoint[];
|
|
18
|
+
finish: CoursePoint;
|
|
19
|
+
activeWaypointIndex?: number;
|
|
20
|
+
}
|
|
21
|
+
export interface ClearCourseDocument {
|
|
22
|
+
v: 1;
|
|
23
|
+
action: 'clear';
|
|
24
|
+
revision: number;
|
|
25
|
+
courseId: null;
|
|
26
|
+
updatedAt: string;
|
|
27
|
+
}
|
|
28
|
+
export type CourseDocument = ActiveCourseDocument | ClearCourseDocument;
|
|
29
|
+
export type MapReadiness = 'unknown' | 'unavailable' | 'online_only' | 'preparing' | 'offline_ready';
|
|
30
|
+
export interface CourseAcknowledgement {
|
|
31
|
+
mapReadiness?: MapReadiness;
|
|
32
|
+
v: 1;
|
|
33
|
+
revision: number;
|
|
34
|
+
status: 'applied' | 'rejected';
|
|
35
|
+
errorCode?: string;
|
|
36
|
+
activation?: 'active' | 'inactive' | 'conflict';
|
|
37
|
+
}
|
|
38
|
+
export declare class CourseError extends Error {
|
|
39
|
+
readonly code: string;
|
|
40
|
+
constructor(code: string);
|
|
41
|
+
}
|
|
42
|
+
export declare function coursePoints(course: ActiveCourseDocument): CoursePoint[];
|
|
43
|
+
export declare function parseCourse(payload: Buffer): CourseDocument;
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.CourseError = void 0;
|
|
4
|
+
exports.coursePoints = coursePoints;
|
|
5
|
+
exports.parseCourse = parseCourse;
|
|
6
|
+
class CourseError extends Error {
|
|
7
|
+
code;
|
|
8
|
+
constructor(code) {
|
|
9
|
+
super(code);
|
|
10
|
+
this.code = code;
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
exports.CourseError = CourseError;
|
|
14
|
+
function coursePoints(course) { return [course.start, ...course.marks, course.finish]; }
|
|
15
|
+
function parseCourse(payload) {
|
|
16
|
+
if (payload.length > 64 * 1024)
|
|
17
|
+
throw new CourseError('course_too_large');
|
|
18
|
+
let value;
|
|
19
|
+
try {
|
|
20
|
+
value = JSON.parse(payload.toString('utf8'));
|
|
21
|
+
}
|
|
22
|
+
catch {
|
|
23
|
+
throw new CourseError('course_invalid_json');
|
|
24
|
+
}
|
|
25
|
+
const doc = value;
|
|
26
|
+
if (!doc || doc.v !== 1 || !Number.isSafeInteger(doc.revision) || (doc.revision ?? 0) < 1
|
|
27
|
+
|| typeof doc.updatedAt !== 'string' || !/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d{1,6})?(?:Z|[+-]\d{2}:\d{2})$/.test(doc.updatedAt) || !Number.isFinite(Date.parse(doc.updatedAt)))
|
|
28
|
+
throw new CourseError('course_invalid');
|
|
29
|
+
if (doc.action === 'clear') {
|
|
30
|
+
if (doc.courseId !== null)
|
|
31
|
+
throw new CourseError('course_invalid');
|
|
32
|
+
return { v: 1, action: 'clear', revision: doc.revision, courseId: null, updatedAt: doc.updatedAt };
|
|
33
|
+
}
|
|
34
|
+
if (doc.action !== 'activate' || !validText(doc.courseId, 120) || !validText(doc.name, 255)
|
|
35
|
+
|| !Array.isArray(doc.marks) || doc.marks.length > 198 || !validPoint(doc.start) || !validPoint(doc.finish)
|
|
36
|
+
|| !doc.marks.every(validPoint))
|
|
37
|
+
throw new CourseError('course_invalid');
|
|
38
|
+
const points = [doc.start, ...doc.marks, doc.finish];
|
|
39
|
+
if (new Set(points.map((point) => point.id)).size !== points.length)
|
|
40
|
+
throw new CourseError('course_duplicate_point_id');
|
|
41
|
+
if (doc.activeWaypointIndex !== undefined && (!Number.isSafeInteger(doc.activeWaypointIndex)
|
|
42
|
+
|| doc.activeWaypointIndex < 0 || doc.activeWaypointIndex >= points.length))
|
|
43
|
+
throw new CourseError('course_invalid_point_index');
|
|
44
|
+
return {
|
|
45
|
+
v: 1, action: 'activate', revision: doc.revision, courseId: doc.courseId, name: doc.name, updatedAt: doc.updatedAt,
|
|
46
|
+
start: point(doc.start), marks: doc.marks.map(point), finish: point(doc.finish),
|
|
47
|
+
...(Number.isSafeInteger(doc.racePlanId) ? { racePlanId: doc.racePlanId } : {}),
|
|
48
|
+
...(doc.activeWaypointIndex !== undefined ? { activeWaypointIndex: doc.activeWaypointIndex } : {})
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
function point(value) { return { id: value.id, name: value.name, latitude: value.latitude, longitude: value.longitude, ...(value.notes !== undefined ? { notes: value.notes } : {}) }; }
|
|
52
|
+
function validText(value, maximum) { return typeof value === 'string' && value.trim().length > 0 && value.length <= maximum; }
|
|
53
|
+
function validPoint(value) {
|
|
54
|
+
const candidate = value;
|
|
55
|
+
return !!candidate && validText(candidate.id, 120) && validText(candidate.name, 255)
|
|
56
|
+
&& (candidate.notes === undefined || (typeof candidate.notes === 'string' && candidate.notes.length <= 1000))
|
|
57
|
+
&& Number.isFinite(candidate.latitude) && Math.abs(candidate.latitude) <= 90
|
|
58
|
+
&& Number.isFinite(candidate.longitude) && Math.abs(candidate.longitude) <= 180;
|
|
59
|
+
}
|
|
60
|
+
//# sourceMappingURL=protocol.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"protocol.js","sourceRoot":"","sources":["../../src/courses/protocol.ts"],"names":[],"mappings":"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"}
|