signalk-wakelogger 0.2.0-beta.5 → 0.2.0-beta.6
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 +17 -0
- package/README.md +2 -0
- package/dist/config/defaults.d.ts +1 -0
- package/dist/config/defaults.js +2 -1
- 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/protocol.d.ts +1 -0
- package/dist/courses/protocol.js +5 -0
- package/dist/courses/protocol.js.map +1 -1
- package/dist/index.js +269 -7
- package/dist/index.js.map +1 -1
- package/dist/race/averages.d.ts +12 -0
- package/dist/race/averages.js +44 -0
- package/dist/race/averages.js.map +1 -0
- package/dist/race/detection.d.ts +65 -0
- package/dist/race/detection.js +150 -0
- package/dist/race/detection.js.map +1 -0
- package/dist/race/dynamic.d.ts +114 -0
- package/dist/race/dynamic.js +265 -0
- package/dist/race/dynamic.js.map +1 -0
- package/dist/race/observation-window.d.ts +81 -0
- package/dist/race/observation-window.js +198 -0
- package/dist/race/observation-window.js.map +1 -0
- package/dist/race/observations.d.ts +54 -0
- package/dist/race/observations.js +212 -0
- package/dist/race/observations.js.map +1 -0
- package/dist/race/onboard-service.d.ts +58 -0
- package/dist/race/onboard-service.js +229 -0
- package/dist/race/onboard-service.js.map +1 -0
- package/dist/race/onboard-store.d.ts +66 -0
- package/dist/race/onboard-store.js +141 -0
- package/dist/race/onboard-store.js.map +1 -0
- package/dist/race/pack.d.ts +126 -0
- package/dist/race/pack.js +317 -0
- package/dist/race/pack.js.map +1 -0
- package/dist/race/plan.d.ts +21 -0
- package/dist/race/plan.js +45 -0
- package/dist/race/plan.js.map +1 -0
- package/dist/race/progression-service.d.ts +33 -0
- package/dist/race/progression-service.js +84 -0
- package/dist/race/progression-service.js.map +1 -0
- package/dist/race/progression-store.d.ts +27 -0
- package/dist/race/progression-store.js +90 -0
- package/dist/race/progression-store.js.map +1 -0
- package/dist/race/race-pack-protocol.d.ts +114 -0
- package/dist/race/race-pack-protocol.js +463 -0
- package/dist/race/race-pack-protocol.js.map +1 -0
- package/dist/race/race-pack-store.d.ts +34 -0
- package/dist/race/race-pack-store.js +181 -0
- package/dist/race/race-pack-store.js.map +1 -0
- package/dist/race/sailing/physics.d.ts +86 -0
- package/dist/race/sailing/physics.js +254 -0
- package/dist/race/sailing/physics.js.map +1 -0
- package/dist/race/sailing/selection.d.ts +109 -0
- package/dist/race/sailing/selection.js +452 -0
- package/dist/race/sailing/selection.js.map +1 -0
- package/dist/signalk/normaliser.d.ts +6 -0
- package/dist/signalk/normaliser.js +11 -0
- package/dist/signalk/normaliser.js.map +1 -1
- package/dist/signalk/subscriber.js +3 -1
- package/dist/signalk/subscriber.js.map +1 -1
- package/dist/telemetry/types.d.ts +4 -0
- package/dist/transport/mqtt-client.d.ts +16 -2
- package/dist/transport/mqtt-client.js +80 -6
- package/dist/transport/mqtt-client.js.map +1 -1
- package/dist/transport/topics.d.ts +3 -0
- package/dist/transport/topics.js +3 -1
- package/dist/transport/topics.js.map +1 -1
- package/docs/onboard-race-guidance.md +547 -0
- package/docs/race-pack-schema.md +315 -0
- package/package.json +7 -5
- package/public/api-client.mjs +7 -0
- package/public/app.mjs +121 -2
- package/public/index.html +1 -1
- package/public/race-plan.mjs +89 -0
- package/public/style.css +1 -0
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,23 @@
|
|
|
2
2
|
|
|
3
3
|
Updates for boat owners and crew: what's new, what's improved, and anything you need to do.
|
|
4
4
|
|
|
5
|
+
## [0.2.0-beta.6] - 2026-09-26
|
|
6
|
+
|
|
7
|
+
### Onboard race planning matches Wake Logger exactly
|
|
8
|
+
|
|
9
|
+
- The onboard planner now starts the current leg from your boat's actual position, uses the real current in its speed and arrival estimates, and no longer assumes a fixed six-knot midpoint speed. Remaining distance and arrival times reflect where you actually are.
|
|
10
|
+
- Onboard wind is only used for the current leg once a genuine five-minute observation window is ready (30 good samples across at least four minutes, newest within 30 seconds). Until then the downloaded forecast is used, with a clear note.
|
|
11
|
+
- A downloaded course is only used when its course definition matches the Race Pack, so an edited course can't be planned against an old pack. Reversed courses are not planned rather than being calculated the wrong way round.
|
|
12
|
+
- Wake Logger cloud now calculates the remaining race with exactly the same rules as the onboard planner, so the two agree. Raw instrument samples are aggregated with the same method on both sides, and the Live tracking ON and OFF views show the same plan and sail recommendation.
|
|
13
|
+
|
|
14
|
+
### Reliable Race Pack and race-plan hand-over
|
|
15
|
+
|
|
16
|
+
- A removed or archived course now clears the onboard Race Pack durably, so a stale copy can't reappear after a restart or reconnect.
|
|
17
|
+
- Onboard race-plan updates are only removed once Wake Logger confirms it stored them; if the confirmation is missed they are sent again after reconnecting or restarting.
|
|
18
|
+
- Onboard race-plan updates now carry the recording session they came from, so Wake Logger links them to the right outing even when they arrive late.
|
|
19
|
+
- Switching Live tracking off sends one final onboard status and then stops transmitting immediately. The final onboard status is kept by the server after the plugin disconnects, and an unexpected connection loss still reports the vessel offline.
|
|
20
|
+
- Added a production-like broker test (`npm run test:broker:dynsec`) that runs against a real Mosquitto dynamic-security broker and the Wake Logger cloud provisioning/framing functions.
|
|
21
|
+
|
|
5
22
|
## [0.2.0-beta.5] - 2026-09-16
|
|
6
23
|
|
|
7
24
|
### Live tracking when you want it
|
package/README.md
CHANGED
|
@@ -55,6 +55,8 @@ Open the **Wake Logger webapp** from Signal K's Web Apps list, using a phone, ta
|
|
|
55
55
|
|
|
56
56
|
The onboard map uses the same instrument styling as Wake Logger's live view, adapts to phone and tablet screens, and includes a full-screen view. It shows the complete course, your boat, the next mark and current leg. Distance, bearing, cross-track error, VMG and arrival estimates appear when Signal K can calculate them; unavailable readings are left clearly marked.
|
|
57
57
|
|
|
58
|
+
Alongside the map, the webapp shows an **onboard race plan**. While **Live tracking** is on, Wake Logger cloud calculates the remaining race: it starts the current leg from your boat's actual position, samples the downloaded forecast at each leg's estimated time, and accounts for current, so the suggested heading, sail plan and arrival estimate keep up with how the race is actually going. When **Live tracking** is off, the vessel performs the same calculation on board from the Race Pack synchronised before departure, so the plan is still available with no internet access. If no Race Pack has synchronised yet, the map and course still work and the plan is clearly marked as unavailable.
|
|
59
|
+
|
|
58
60
|
Use **Advance point** to move to the next course point, or select a point and choose **Set point** to correct your progress. Progression is manual in this release: it does not automatically judge start-line crossings, mark roundings, gates or finishes.
|
|
59
61
|
|
|
60
62
|
Your course is also available as a standard Signal K route for other onboard applications. If another app selects a different active route, Wake Logger shows that state. Choose **Activate Wake Logger course** when you want to return to it.
|
package/dist/config/defaults.js
CHANGED
|
@@ -22,7 +22,8 @@ function parseConfig(value) {
|
|
|
22
22
|
samplePeriodMs: boundedNumber(input.samplePeriodMs, exports.DEFAULTS.samplePeriodMs, 250, 60_000),
|
|
23
23
|
maxOutboxMb: boundedNumber(input.maxOutboxMb, 250, 10, 4096),
|
|
24
24
|
maxOutboxDays: boundedNumber(input.maxOutboxDays, 7, 1, 30),
|
|
25
|
-
debugTelemetry: input.debugTelemetry === true
|
|
25
|
+
debugTelemetry: input.debugTelemetry === true,
|
|
26
|
+
raceProgressionMode: input.raceProgressionMode === 'suggest' || input.raceProgressionMode === 'off' ? input.raceProgressionMode : 'auto'
|
|
26
27
|
};
|
|
27
28
|
}
|
|
28
29
|
function boundedNumber(value, fallback, min, max) {
|
|
@@ -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":";;;AAqBA,kCAeC;AApCY,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;AAaV,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;QAC7C,mBAAmB,EAAE,KAAK,CAAC,mBAAmB,KAAK,SAAS,IAAI,KAAK,CAAC,mBAAmB,KAAK,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,mBAAmB,CAAC,CAAC,CAAC,MAAM;KACzI,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
|
@@ -47,5 +47,13 @@ export declare const configSchema: {
|
|
|
47
47
|
readonly title: "Enable telemetry debug logging";
|
|
48
48
|
readonly default: false;
|
|
49
49
|
};
|
|
50
|
+
readonly raceProgressionMode: {
|
|
51
|
+
readonly type: "string";
|
|
52
|
+
readonly title: "Mark detection mode";
|
|
53
|
+
readonly enum: readonly ["auto", "suggest", "off"];
|
|
54
|
+
readonly enumNames: readonly ["Automatic", "Suggest", "Off"];
|
|
55
|
+
readonly default: "auto";
|
|
56
|
+
readonly description: "Automatic advances the active course point when the onboard app detects a rounding, start or finish; it also records evidence for Wake Logger. Suggest asks the crew first. Off records evidence only.";
|
|
57
|
+
};
|
|
50
58
|
};
|
|
51
59
|
};
|
package/dist/config/schema.js
CHANGED
|
@@ -49,6 +49,14 @@ exports.configSchema = {
|
|
|
49
49
|
type: 'boolean',
|
|
50
50
|
title: 'Enable telemetry debug logging',
|
|
51
51
|
default: false
|
|
52
|
+
},
|
|
53
|
+
raceProgressionMode: {
|
|
54
|
+
type: 'string',
|
|
55
|
+
title: 'Mark detection mode',
|
|
56
|
+
enum: ['auto', 'suggest', 'off'],
|
|
57
|
+
enumNames: ['Automatic', 'Suggest', 'Off'],
|
|
58
|
+
default: 'auto',
|
|
59
|
+
description: 'Automatic advances the active course point when the onboard app detects a rounding, start or finish; it also records evidence for Wake Logger. Suggest asks the crew first. Off records evidence only.'
|
|
52
60
|
}
|
|
53
61
|
}
|
|
54
62
|
};
|
|
@@ -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,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"}
|
|
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;QACD,mBAAmB,EAAE;YACnB,IAAI,EAAE,QAAQ;YACd,KAAK,EAAE,qBAAqB;YAC5B,IAAI,EAAE,CAAC,MAAM,EAAE,SAAS,EAAE,KAAK,CAAC;YAChC,SAAS,EAAE,CAAC,WAAW,EAAE,SAAS,EAAE,KAAK,CAAC;YAC1C,OAAO,EAAE,MAAM;YACf,WAAW,EAAE,wMAAwM;SACtN;KACF;CACO,CAAA"}
|
package/dist/courses/protocol.js
CHANGED
|
@@ -41,13 +41,18 @@ function parseCourse(payload) {
|
|
|
41
41
|
if (doc.activeWaypointIndex !== undefined && (!Number.isSafeInteger(doc.activeWaypointIndex)
|
|
42
42
|
|| doc.activeWaypointIndex < 0 || doc.activeWaypointIndex >= points.length))
|
|
43
43
|
throw new CourseError('course_invalid_point_index');
|
|
44
|
+
if (doc.courseDefinitionDigest !== undefined && !isDigest(doc.courseDefinitionDigest))
|
|
45
|
+
throw new CourseError('course_invalid_digest');
|
|
44
46
|
return {
|
|
45
47
|
v: 1, action: 'activate', revision: doc.revision, courseId: doc.courseId, name: doc.name, updatedAt: doc.updatedAt,
|
|
46
48
|
start: point(doc.start), marks: doc.marks.map(point), finish: point(doc.finish),
|
|
47
49
|
...(Number.isSafeInteger(doc.racePlanId) ? { racePlanId: doc.racePlanId } : {}),
|
|
50
|
+
...(isDigest(doc.courseDefinitionDigest) ? { courseDefinitionDigest: doc.courseDefinitionDigest.toLowerCase() } : {}),
|
|
48
51
|
...(doc.activeWaypointIndex !== undefined ? { activeWaypointIndex: doc.activeWaypointIndex } : {})
|
|
49
52
|
};
|
|
50
53
|
}
|
|
54
|
+
const COURSE_DIGEST = /^[0-9a-fA-F]{64}$/;
|
|
55
|
+
function isDigest(value) { return typeof value === 'string' && COURSE_DIGEST.test(value); }
|
|
51
56
|
function point(value) { return { id: value.id, name: value.name, latitude: value.latitude, longitude: value.longitude, ...(value.notes !== undefined ? { notes: value.notes } : {}) }; }
|
|
52
57
|
function validText(value, maximum) { return typeof value === 'string' && value.trim().length > 0 && value.length <= maximum; }
|
|
53
58
|
function validPoint(value) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"protocol.js","sourceRoot":"","sources":["../../src/courses/protocol.ts"],"names":[],"mappings":";;;
|
|
1
|
+
{"version":3,"file":"protocol.js","sourceRoot":"","sources":["../../src/courses/protocol.ts"],"names":[],"mappings":";;;AA6BA,oCAAmI;AACnI,kCA0BC;AA9BD,MAAa,WAAY,SAAQ,KAAK;IACf;IAArB,YAAqB,IAAY;QAAI,KAAK,CAAC,IAAI,CAAC,CAAA;QAA3B,SAAI,GAAJ,IAAI,CAAQ;IAAgB,CAAC;CACnD;AAFD,kCAEC;AACD,SAAgB,YAAY,CAAC,MAA4B,IAAmB,OAAO,CAAC,MAAM,CAAC,KAAK,EAAE,GAAG,MAAM,CAAC,KAAK,EAAE,MAAM,CAAC,MAAM,CAAC,CAAA,CAAC,CAAC;AACnI,SAAgB,WAAW,CAAC,OAAe;IACzC,IAAI,OAAO,CAAC,MAAM,GAAG,EAAE,GAAG,IAAI;QAAE,MAAM,IAAI,WAAW,CAAC,kBAAkB,CAAC,CAAA;IACzE,IAAI,KAAc,CAAA;IAClB,IAAI,CAAC;QAAC,KAAK,GAAG,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC,CAAA;IAAC,CAAC;IAAC,MAAM,CAAC;QAAC,MAAM,IAAI,WAAW,CAAC,qBAAqB,CAAC,CAAA;IAAC,CAAC;IAC3G,MAAM,GAAG,GAAG,KAAkD,CAAA;IAC9D,IAAI,CAAC,GAAG,IAAI,GAAG,CAAC,CAAC,KAAK,CAAC,IAAI,CAAC,MAAM,CAAC,aAAa,CAAC,GAAG,CAAC,QAAQ,CAAC,IAAI,CAAC,GAAG,CAAC,QAAQ,IAAI,CAAC,CAAC,GAAG,CAAC;WACpF,OAAO,GAAG,CAAC,SAAS,KAAK,QAAQ,IAAI,CAAC,0EAA0E,CAAC,IAAI,CAAC,GAAG,CAAC,SAAS,CAAC,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,SAAS,CAAC,CAAC;QAAE,MAAM,IAAI,WAAW,CAAC,gBAAgB,CAAC,CAAA;IACjO,IAAK,GAA2B,CAAC,MAAM,KAAK,OAAO,EAAE,CAAC;QACpD,IAAI,GAAG,CAAC,QAAQ,KAAK,IAAI;YAAE,MAAM,IAAI,WAAW,CAAC,gBAAgB,CAAC,CAAA;QAClE,OAAO,EAAE,CAAC,EAAE,CAAC,EAAE,MAAM,EAAE,OAAO,EAAE,QAAQ,EAAE,GAAG,CAAC,QAAS,EAAE,QAAQ,EAAE,IAAI,EAAE,SAAS,EAAE,GAAG,CAAC,SAAS,EAAE,CAAA;IACrG,CAAC;IACD,IAAI,GAAG,CAAC,MAAM,KAAK,UAAU,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,QAAQ,EAAE,GAAG,CAAC,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,IAAI,EAAE,GAAG,CAAC;WACtF,CAAC,KAAK,CAAC,OAAO,CAAC,GAAG,CAAC,KAAK,CAAC,IAAI,GAAG,CAAC,KAAK,CAAC,MAAM,GAAG,GAAG,IAAI,CAAC,UAAU,CAAC,GAAG,CAAC,KAAK,CAAC,IAAI,CAAC,UAAU,CAAC,GAAG,CAAC,MAAM,CAAC;WACxG,CAAC,GAAG,CAAC,KAAK,CAAC,KAAK,CAAC,UAAU,CAAC;QAAE,MAAM,IAAI,WAAW,CAAC,gBAAgB,CAAC,CAAA;IAC1E,MAAM,MAAM,GAAG,CAAC,GAAG,CAAC,KAAK,EAAE,GAAG,GAAG,CAAC,KAAK,EAAE,GAAG,CAAC,MAAM,CAAC,CAAA;IACpD,IAAI,IAAI,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,CAAC,EAAE,CAAC,CAAC,CAAC,IAAI,KAAK,MAAM,CAAC,MAAM;QAAE,MAAM,IAAI,WAAW,CAAC,2BAA2B,CAAC,CAAA;IACvH,IAAI,GAAG,CAAC,mBAAmB,KAAK,SAAS,IAAI,CAAC,CAAC,MAAM,CAAC,aAAa,CAAC,GAAG,CAAC,mBAAmB,CAAC;WACvF,GAAG,CAAC,mBAAmB,GAAG,CAAC,IAAI,GAAG,CAAC,mBAAmB,IAAI,MAAM,CAAC,MAAM,CAAC;QAAE,MAAM,IAAI,WAAW,CAAC,4BAA4B,CAAC,CAAA;IAClI,IAAI,GAAG,CAAC,sBAAsB,KAAK,SAAS,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,sBAAsB,CAAC;QAAE,MAAM,IAAI,WAAW,CAAC,uBAAuB,CAAC,CAAA;IACrI,OAAO;QACL,CAAC,EAAE,CAAC,EAAE,MAAM,EAAE,UAAU,EAAE,QAAQ,EAAE,GAAG,CAAC,QAAS,EAAE,QAAQ,EAAE,GAAG,CAAC,QAAS,EAAE,IAAI,EAAE,GAAG,CAAC,IAAK,EAAE,SAAS,EAAE,GAAG,CAAC,SAAS;QACrH,KAAK,EAAE,KAAK,CAAC,GAAG,CAAC,KAAM,CAAC,EAAE,KAAK,EAAE,GAAG,CAAC,KAAK,CAAC,GAAG,CAAC,KAAK,CAAC,EAAE,MAAM,EAAE,KAAK,CAAC,GAAG,CAAC,MAAO,CAAC;QACjF,GAAG,CAAC,MAAM,CAAC,aAAa,CAAC,GAAG,CAAC,UAAU,CAAC,CAAC,CAAC,CAAC,EAAE,UAAU,EAAE,GAAG,CAAC,UAAU,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;QAC/E,GAAG,CAAC,QAAQ,CAAC,GAAG,CAAC,sBAAsB,CAAC,CAAC,CAAC,CAAC,EAAE,sBAAsB,EAAE,GAAG,CAAC,sBAAsB,CAAC,WAAW,EAAE,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;QACrH,GAAG,CAAC,GAAG,CAAC,mBAAmB,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,mBAAmB,EAAE,GAAG,CAAC,mBAAmB,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;KACnG,CAAA;AACH,CAAC;AACD,MAAM,aAAa,GAAG,mBAAmB,CAAA;AACzC,SAAS,QAAQ,CAAC,KAAc,IAAqB,OAAO,OAAO,KAAK,KAAK,QAAQ,IAAI,aAAa,CAAC,IAAI,CAAC,KAAK,CAAC,CAAA,CAAC,CAAC;AACpH,SAAS,KAAK,CAAC,KAAkB,IAAiB,OAAO,EAAE,EAAE,EAAE,KAAK,CAAC,EAAE,EAAE,IAAI,EAAE,KAAK,CAAC,IAAI,EAAE,QAAQ,EAAE,KAAK,CAAC,QAAQ,EAAE,SAAS,EAAE,KAAK,CAAC,SAAS,EAAE,GAAG,CAAC,KAAK,CAAC,KAAK,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,KAAK,EAAE,KAAK,CAAC,KAAK,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,CAAA,CAAC,CAAC;AACjN,SAAS,SAAS,CAAC,KAAc,EAAE,OAAe,IAAqB,OAAO,OAAO,KAAK,KAAK,QAAQ,IAAI,KAAK,CAAC,IAAI,EAAE,CAAC,MAAM,GAAG,CAAC,IAAI,KAAK,CAAC,MAAM,IAAI,OAAO,CAAA,CAAC,CAAC;AAC/J,SAAS,UAAU,CAAC,KAAc;IAChC,MAAM,SAAS,GAAG,KAAgC,CAAA;IAClD,OAAO,CAAC,CAAC,SAAS,IAAI,SAAS,CAAC,SAAS,CAAC,EAAE,EAAE,GAAG,CAAC,IAAI,SAAS,CAAC,SAAS,CAAC,IAAI,EAAE,GAAG,CAAC;WAC/E,CAAC,SAAS,CAAC,KAAK,KAAK,SAAS,IAAI,CAAC,OAAO,SAAS,CAAC,KAAK,KAAK,QAAQ,IAAI,SAAS,CAAC,KAAK,CAAC,MAAM,IAAI,IAAI,CAAC,CAAC;WAC1G,MAAM,CAAC,QAAQ,CAAC,SAAS,CAAC,QAAQ,CAAC,IAAI,IAAI,CAAC,GAAG,CAAC,SAAS,CAAC,QAAQ,CAAC,IAAI,EAAE;WACzE,MAAM,CAAC,QAAQ,CAAC,SAAS,CAAC,SAAS,CAAC,IAAI,IAAI,CAAC,GAAG,CAAC,SAAS,CAAC,SAAS,CAAC,IAAI,GAAG,CAAA;AACnF,CAAC"}
|
package/dist/index.js
CHANGED
|
@@ -7,6 +7,13 @@ const node_path_1 = __importDefault(require("node:path"));
|
|
|
7
7
|
const course_store_1 = require("./courses/course-store");
|
|
8
8
|
const protocol_1 = require("./courses/protocol");
|
|
9
9
|
const native_course_1 = require("./courses/native-course");
|
|
10
|
+
const progression_service_1 = require("./race/progression-service");
|
|
11
|
+
const progression_store_1 = require("./race/progression-store");
|
|
12
|
+
const observations_1 = require("./race/observations");
|
|
13
|
+
const race_pack_store_1 = require("./race/race-pack-store");
|
|
14
|
+
const race_pack_protocol_1 = require("./race/race-pack-protocol");
|
|
15
|
+
const onboard_store_1 = require("./race/onboard-store");
|
|
16
|
+
const onboard_service_1 = require("./race/onboard-service");
|
|
10
17
|
const history_1 = require("./tracking/history");
|
|
11
18
|
const schema_1 = require("./config/schema");
|
|
12
19
|
const defaults_1 = require("./config/defaults");
|
|
@@ -32,6 +39,13 @@ const constructor = (app) => {
|
|
|
32
39
|
let queueStatsAt;
|
|
33
40
|
let transport;
|
|
34
41
|
let courses;
|
|
42
|
+
let progression;
|
|
43
|
+
let racePacks;
|
|
44
|
+
let racePackReceiver;
|
|
45
|
+
let observations;
|
|
46
|
+
let onboardSnapshots;
|
|
47
|
+
let onboard;
|
|
48
|
+
let onboardCourse = null;
|
|
35
49
|
let courseInitializationError;
|
|
36
50
|
let courseFailureAcknowledgement;
|
|
37
51
|
let tripState;
|
|
@@ -65,7 +79,7 @@ const constructor = (app) => {
|
|
|
65
79
|
pairingAbortController?.abort();
|
|
66
80
|
associationAbortController?.abort();
|
|
67
81
|
// Stop all transmission immediately when a saved configuration restarts us.
|
|
68
|
-
const stopping = transport?.stop(false);
|
|
82
|
+
const stopping = transport?.stop({ publishOffline: false, force: true });
|
|
69
83
|
const previous = initialization;
|
|
70
84
|
const thisGeneration = ++generation;
|
|
71
85
|
initialization = (async () => {
|
|
@@ -142,10 +156,11 @@ const constructor = (app) => {
|
|
|
142
156
|
await persistTrackingMode(mode, revision, thisGeneration);
|
|
143
157
|
if (revision === trackingRevision && generation === thisGeneration) {
|
|
144
158
|
activeUploadMode = mode;
|
|
159
|
+
await onboard?.setAuthority(mode);
|
|
145
160
|
if (mode === 'automatic' && !wasAutomatic) {
|
|
146
161
|
if (outbox)
|
|
147
162
|
await updateHistory(await outbox.stats(), true);
|
|
148
|
-
await transport?.stop(false);
|
|
163
|
+
await transport?.stop({ publishOffline: false, force: true });
|
|
149
164
|
connectTransport?.();
|
|
150
165
|
}
|
|
151
166
|
await updateStatus();
|
|
@@ -181,6 +196,7 @@ const constructor = (app) => {
|
|
|
181
196
|
if (!courses)
|
|
182
197
|
throw new protocol_1.CourseError('no_selected_course');
|
|
183
198
|
await courses.activate();
|
|
199
|
+
await syncProgressionCourse();
|
|
184
200
|
void transport?.publishCourseAcknowledgement().catch((error) => app.error(`Course acknowledgement deferred: ${safeError(error)}`));
|
|
185
201
|
response.status(200).json({ ...await courses.status(), uploadMode: activeUploadMode, connectionState });
|
|
186
202
|
}
|
|
@@ -207,6 +223,87 @@ const constructor = (app) => {
|
|
|
207
223
|
next(error);
|
|
208
224
|
}
|
|
209
225
|
});
|
|
226
|
+
readRouter.get?.('/progression', async (_request, response, next) => {
|
|
227
|
+
try {
|
|
228
|
+
response.status(200).json(progression?.status() ?? { mode: null, revision: 0, activeIndex: 0, pending: null, lastDetection: null });
|
|
229
|
+
}
|
|
230
|
+
catch (error) {
|
|
231
|
+
next(error);
|
|
232
|
+
}
|
|
233
|
+
});
|
|
234
|
+
writeRouter.post('/progression/mode', async (request, response, next) => {
|
|
235
|
+
const mode = request.body?.mode;
|
|
236
|
+
if (mode !== 'auto' && mode !== 'suggest' && mode !== 'off') {
|
|
237
|
+
response.status(400).json({ error: 'invalid_progression_mode' });
|
|
238
|
+
return;
|
|
239
|
+
}
|
|
240
|
+
if (!progression) {
|
|
241
|
+
response.status(409).json({ error: 'progression_unavailable' });
|
|
242
|
+
return;
|
|
243
|
+
}
|
|
244
|
+
try {
|
|
245
|
+
await progression.setMode(mode);
|
|
246
|
+
response.status(200).json(progression.status());
|
|
247
|
+
}
|
|
248
|
+
catch (error) {
|
|
249
|
+
next(error);
|
|
250
|
+
}
|
|
251
|
+
});
|
|
252
|
+
writeRouter.post('/progression/resolve', async (request, response, next) => {
|
|
253
|
+
const body = request.body;
|
|
254
|
+
if ((body?.resolution !== 'accepted' && body?.resolution !== 'dismissed') || !Number.isSafeInteger(body?.pointIndex)) {
|
|
255
|
+
response.status(400).json({ error: 'invalid_resolution' });
|
|
256
|
+
return;
|
|
257
|
+
}
|
|
258
|
+
if (!progression) {
|
|
259
|
+
response.status(409).json({ error: 'progression_unavailable' });
|
|
260
|
+
return;
|
|
261
|
+
}
|
|
262
|
+
try {
|
|
263
|
+
const resolved = await progression.resolve(body.resolution, body.pointIndex);
|
|
264
|
+
if (resolved)
|
|
265
|
+
response.status(200).json(progression.status());
|
|
266
|
+
else
|
|
267
|
+
response.status(409).json({ error: 'no_pending_detection', ...progression.status() });
|
|
268
|
+
}
|
|
269
|
+
catch (error) {
|
|
270
|
+
next(error);
|
|
271
|
+
}
|
|
272
|
+
});
|
|
273
|
+
readRouter.get?.('/race-plan', async (_request, response, next) => {
|
|
274
|
+
try {
|
|
275
|
+
const status = onboard?.status();
|
|
276
|
+
response.status(200).json({
|
|
277
|
+
...(status ?? { calculationAuthority: activeUploadMode === 'local_only' ? 'onboard' : 'cloud', lastLocalCalculationAt: null, reason: 'onboard_unavailable' }),
|
|
278
|
+
uploadMode: activeUploadMode,
|
|
279
|
+
connectionState,
|
|
280
|
+
latestSnapshot: onboardSnapshots?.latest() ?? null
|
|
281
|
+
});
|
|
282
|
+
}
|
|
283
|
+
catch (error) {
|
|
284
|
+
next(error);
|
|
285
|
+
}
|
|
286
|
+
});
|
|
287
|
+
writeRouter.post('/race-plan/recalculate', async (_request, response, next) => {
|
|
288
|
+
try {
|
|
289
|
+
if (!onboard) {
|
|
290
|
+
response.status(409).json({ error: 'onboard_unavailable' });
|
|
291
|
+
return;
|
|
292
|
+
}
|
|
293
|
+
// Explicit recalculation respects the product authority rule: while
|
|
294
|
+
// Live tracking is automatic the cloud is authoritative and an
|
|
295
|
+
// onboard recalculation must not run, even if MQTT is offline.
|
|
296
|
+
if (activeUploadMode !== 'local_only') {
|
|
297
|
+
response.status(409).json({ error: 'cloud_authority', uploadMode: activeUploadMode, ...onboard.status() });
|
|
298
|
+
return;
|
|
299
|
+
}
|
|
300
|
+
const snapshot = await onboard.refresh(true);
|
|
301
|
+
response.status(200).json({ calculated: !!snapshot, snapshot, status: onboard.status() });
|
|
302
|
+
}
|
|
303
|
+
catch (error) {
|
|
304
|
+
next(error);
|
|
305
|
+
}
|
|
306
|
+
});
|
|
210
307
|
// Registered directly: unpairing keeps Signal K's admin-only default.
|
|
211
308
|
pluginRouter.post('/forget-credentials', async (_request, response, next) => {
|
|
212
309
|
try {
|
|
@@ -242,6 +339,11 @@ const constructor = (app) => {
|
|
|
242
339
|
'/course': { get: { summary: 'Read cached and native course state', responses: { '200': { description: 'Course state' } } } },
|
|
243
340
|
'/course/map-readiness': { post: { summary: 'Report locally verified chart readiness for a course revision', responses: { '200': { description: 'Readiness saved' }, '409': { description: 'Invalid or stale revision' } } } },
|
|
244
341
|
'/course/activate': { post: { summary: 'Explicitly activate the selected cached course', responses: { '200': { description: 'Course active' }, '409': { description: 'No selected course or native course unavailable' } } } },
|
|
342
|
+
'/progression': { get: { summary: 'Read mark detection mode, active point and any pending detection', responses: { '200': { description: 'Progression status' } } } },
|
|
343
|
+
'/progression/mode': { post: { summary: 'Change mark detection between automatic, suggest and off', responses: { '200': { description: 'Mode saved' }, '400': { description: 'Invalid mode' }, '409': { description: 'Progression unavailable' } } } },
|
|
344
|
+
'/progression/resolve': { post: { summary: 'Resolve a pending detection as accepted or dismissed for its point index', responses: { '200': { description: 'Resolution recorded' }, '400': { description: 'Invalid resolution' }, '409': { description: 'No matching pending detection' } } } },
|
|
345
|
+
'/race-plan': { get: { summary: 'Read onboard Race Plan calculation authority, Race Pack state and the latest onboard snapshot', responses: { '200': { description: 'Onboard race plan state' } } } },
|
|
346
|
+
'/race-plan/recalculate': { post: { summary: 'Request an explicit onboard Race Plan recalculation (local-only authority only)', responses: { '200': { description: 'Recalculation attempted' }, '409': { description: 'Onboard planner unavailable or cloud is authoritative' } } } },
|
|
245
347
|
'/forget-credentials': {
|
|
246
348
|
post: {
|
|
247
349
|
summary: 'Forget Wake Logger credentials while preserving all device outboxes',
|
|
@@ -251,6 +353,90 @@ const constructor = (app) => {
|
|
|
251
353
|
}
|
|
252
354
|
})
|
|
253
355
|
};
|
|
356
|
+
async function syncProgressionCourse() {
|
|
357
|
+
if (!courses) {
|
|
358
|
+
progression?.updateCourse(null);
|
|
359
|
+
onboardCourse = null;
|
|
360
|
+
return;
|
|
361
|
+
}
|
|
362
|
+
try {
|
|
363
|
+
const status = await courses.status();
|
|
364
|
+
progression?.updateCourse({
|
|
365
|
+
revision: status.cachedCourse?.revision ?? 0,
|
|
366
|
+
points: status.routePoints ?? [],
|
|
367
|
+
reverse: status.native?.course?.activeRoute?.reverse === true,
|
|
368
|
+
activeIndex: status.native?.course?.activeRoute?.pointIndex ?? 0,
|
|
369
|
+
matches: status.native?.activeMatchesDesired === true
|
|
370
|
+
});
|
|
371
|
+
const points = status.routePoints ?? [];
|
|
372
|
+
onboardCourse = status.native?.activeMatchesDesired === true && status.cachedCourse?.courseId && points.length >= 2
|
|
373
|
+
? {
|
|
374
|
+
courseId: status.cachedCourse.courseId,
|
|
375
|
+
racePlanId: status.cachedCourse.racePlanId ?? null,
|
|
376
|
+
courseDefinitionDigest: status.cachedCourse.courseDefinitionDigest ?? null,
|
|
377
|
+
activeIndex: status.native?.course?.activeRoute?.pointIndex ?? 0,
|
|
378
|
+
totalPoints: points.length,
|
|
379
|
+
reverse: status.native?.course?.activeRoute?.reverse === true
|
|
380
|
+
}
|
|
381
|
+
: null;
|
|
382
|
+
}
|
|
383
|
+
catch {
|
|
384
|
+
progression?.updateCourse(null);
|
|
385
|
+
onboardCourse = null;
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
function racingUnderway() {
|
|
389
|
+
if (onboardCourse && onboardCourse.activeIndex > 0 && onboardCourse.activeIndex < onboardCourse.totalPoints)
|
|
390
|
+
return true;
|
|
391
|
+
return tripState?.currentState().state === 'MOVING';
|
|
392
|
+
}
|
|
393
|
+
async function flushProgressionEvidence() {
|
|
394
|
+
if (!progression || !transport)
|
|
395
|
+
return;
|
|
396
|
+
const pending = progression.pendingEvents();
|
|
397
|
+
if (!pending.length)
|
|
398
|
+
return;
|
|
399
|
+
const published = [];
|
|
400
|
+
for (const event of pending) {
|
|
401
|
+
try {
|
|
402
|
+
if (!await transport.publishEvidence(event))
|
|
403
|
+
break;
|
|
404
|
+
}
|
|
405
|
+
catch {
|
|
406
|
+
break;
|
|
407
|
+
}
|
|
408
|
+
published.push(event.sequence);
|
|
409
|
+
}
|
|
410
|
+
if (published.length)
|
|
411
|
+
await progression.markPublished(published);
|
|
412
|
+
}
|
|
413
|
+
// Onboard snapshots are historical evidence: they only leave the vessel once
|
|
414
|
+
// Live tracking is restored. A snapshot stays durable until Wake Logger
|
|
415
|
+
// application-ACKs it; published-but-unacknowledged snapshots are retried.
|
|
416
|
+
async function flushOnboardSnapshots() {
|
|
417
|
+
if (!onboardSnapshots || !transport || activeUploadMode !== 'automatic')
|
|
418
|
+
return;
|
|
419
|
+
const queue = [...onboardSnapshots.pending(), ...onboardSnapshots.unacknowledged()].slice(0, 5);
|
|
420
|
+
if (!queue.length)
|
|
421
|
+
return;
|
|
422
|
+
const published = [];
|
|
423
|
+
for (const event of queue) {
|
|
424
|
+
try {
|
|
425
|
+
if (!await transport.publishRacePlanSnapshot(event.snapshot))
|
|
426
|
+
break;
|
|
427
|
+
}
|
|
428
|
+
catch {
|
|
429
|
+
break;
|
|
430
|
+
}
|
|
431
|
+
published.push(event.sequence);
|
|
432
|
+
}
|
|
433
|
+
if (published.length)
|
|
434
|
+
await onboardSnapshots.markPublished(published);
|
|
435
|
+
}
|
|
436
|
+
function publishOnboardSnapshot(_snapshot) {
|
|
437
|
+
if (activeUploadMode === 'automatic')
|
|
438
|
+
void flushOnboardSnapshots();
|
|
439
|
+
}
|
|
254
440
|
async function cleanupResources() {
|
|
255
441
|
stopSubscription?.();
|
|
256
442
|
stopSubscription = undefined;
|
|
@@ -267,6 +453,17 @@ const constructor = (app) => {
|
|
|
267
453
|
currentSampler = undefined;
|
|
268
454
|
connectTransport = undefined;
|
|
269
455
|
await sampleOperation;
|
|
456
|
+
await progression?.close();
|
|
457
|
+
progression = undefined;
|
|
458
|
+
onboard?.close();
|
|
459
|
+
onboard = undefined;
|
|
460
|
+
await onboardSnapshots?.close();
|
|
461
|
+
onboardSnapshots = undefined;
|
|
462
|
+
await racePacks?.close();
|
|
463
|
+
racePacks = undefined;
|
|
464
|
+
racePackReceiver = undefined;
|
|
465
|
+
observations = undefined;
|
|
466
|
+
onboardCourse = null;
|
|
270
467
|
await courses?.close();
|
|
271
468
|
courses = undefined;
|
|
272
469
|
courseInitializationError = undefined;
|
|
@@ -362,6 +559,43 @@ const constructor = (app) => {
|
|
|
362
559
|
courseInitializationError = safeError(error);
|
|
363
560
|
app.error(`Wake Logger course cache unavailable; telemetry recording continues: ${courseInitializationError}`);
|
|
364
561
|
}
|
|
562
|
+
const progressionStore = new progression_store_1.RaceProgressionStore(node_path_1.default.join(dataDirectory, 'race-progression', credentials.deviceId, 'state.json'));
|
|
563
|
+
try {
|
|
564
|
+
await progressionStore.open();
|
|
565
|
+
progression = new progression_service_1.RaceProgressionService({ store: progressionStore, defaultMode: config.raceProgressionMode });
|
|
566
|
+
}
|
|
567
|
+
catch (error) {
|
|
568
|
+
progression = undefined;
|
|
569
|
+
app.error(`Wake Logger mark detection unavailable: ${safeError(error)}`);
|
|
570
|
+
}
|
|
571
|
+
await syncProgressionCourse();
|
|
572
|
+
observations = new observations_1.ObservationCollector();
|
|
573
|
+
const packs = new race_pack_store_1.RacePackStore(node_path_1.default.join(dataDirectory, 'race-packs', credentials.deviceId));
|
|
574
|
+
try {
|
|
575
|
+
await packs.open();
|
|
576
|
+
}
|
|
577
|
+
catch (error) {
|
|
578
|
+
app.error(`Wake Logger race pack store unavailable: ${safeError(error)}`);
|
|
579
|
+
}
|
|
580
|
+
racePacks = packs;
|
|
581
|
+
racePackReceiver = new race_pack_protocol_1.RacePackReceiver({ store: packs });
|
|
582
|
+
const snapshotStore = new onboard_store_1.OnboardSnapshotStore(node_path_1.default.join(dataDirectory, 'onboard-plans', credentials.deviceId, 'state.json'));
|
|
583
|
+
try {
|
|
584
|
+
await snapshotStore.open();
|
|
585
|
+
}
|
|
586
|
+
catch (error) {
|
|
587
|
+
app.error(`Wake Logger onboard plan history unavailable: ${safeError(error)}`);
|
|
588
|
+
}
|
|
589
|
+
onboardSnapshots = snapshotStore;
|
|
590
|
+
onboard = new onboard_service_1.OnboardRaceService({
|
|
591
|
+
packs,
|
|
592
|
+
snapshots: snapshotStore,
|
|
593
|
+
observations,
|
|
594
|
+
course: () => onboardCourse,
|
|
595
|
+
racing: racingUnderway,
|
|
596
|
+
trackingSessionId: () => tripState?.currentState().trackingSessionId ?? null,
|
|
597
|
+
onCalculated: publishOnboardSnapshot
|
|
598
|
+
});
|
|
365
599
|
const normaliser = new normaliser_1.TelemetryNormaliser();
|
|
366
600
|
const tripFile = node_path_1.default.join(dataDirectory, 'trip-state.json');
|
|
367
601
|
const trip = new recording_store_1.RecordingStore(node_path_1.default.join(dataDirectory, 'recordings', credentials.deviceId, 'state.json'));
|
|
@@ -412,8 +646,11 @@ const constructor = (app) => {
|
|
|
412
646
|
void confirmAssociation(credentials, thisGeneration);
|
|
413
647
|
},
|
|
414
648
|
onCourse: async (payload) => {
|
|
415
|
-
if (courses)
|
|
416
|
-
|
|
649
|
+
if (courses) {
|
|
650
|
+
const acknowledgement = await courses.receive(payload);
|
|
651
|
+
await syncProgressionCourse();
|
|
652
|
+
return acknowledgement;
|
|
653
|
+
}
|
|
417
654
|
let revision = 0;
|
|
418
655
|
try {
|
|
419
656
|
revision = (0, protocol_1.parseCourse)(payload).revision;
|
|
@@ -423,10 +660,16 @@ const constructor = (app) => {
|
|
|
423
660
|
return courseFailureAcknowledgement;
|
|
424
661
|
},
|
|
425
662
|
getCourseAcknowledgement: () => courses?.acknowledgement() ?? courseFailureAcknowledgement,
|
|
663
|
+
onRacePackManifest: async (payload) => racePackReceiver?.acceptManifest(payload) ?? null,
|
|
664
|
+
onRacePackChunk: async (payload, topicIndex) => racePackReceiver?.acceptChunk(payload, topicIndex) ?? null,
|
|
665
|
+
getRacePackAck: () => racePackReceiver?.currentAck() ?? null,
|
|
426
666
|
onRecordingAcks: async (acks) => {
|
|
427
667
|
sampleOperation = sampleOperation.then(() => trip.acknowledge(acks));
|
|
428
668
|
await sampleOperation;
|
|
429
669
|
},
|
|
670
|
+
onRacePlanSnapshotAcks: async (ids) => {
|
|
671
|
+
await onboardSnapshots?.acknowledge(ids);
|
|
672
|
+
},
|
|
430
673
|
onMode: (mode) => { if (transport === instance)
|
|
431
674
|
sampler.updateMode(activeUploadMode === 'local_only' ? 'NORMAL' : mode); },
|
|
432
675
|
onProfile: async (replacement) => {
|
|
@@ -443,7 +686,15 @@ const constructor = (app) => {
|
|
|
443
686
|
if (activeUploadMode === 'local_only') {
|
|
444
687
|
transport = new mqtt_client_1.WakeLoggerTransport(credentials, selected.store, { profile, onState: () => undefined });
|
|
445
688
|
}
|
|
446
|
-
stopSubscription = (0, subscriber_1.subscribeToTelemetry)(app, (delta) =>
|
|
689
|
+
stopSubscription = (0, subscriber_1.subscribeToTelemetry)(app, (delta) => {
|
|
690
|
+
normaliser.ingest(delta);
|
|
691
|
+
observations?.ingest(delta);
|
|
692
|
+
if (!progression)
|
|
693
|
+
return;
|
|
694
|
+
const fix = normaliser.latestFix();
|
|
695
|
+
if (fix)
|
|
696
|
+
progression.fix({ at: Date.now(), ...fix });
|
|
697
|
+
});
|
|
447
698
|
const sample = async () => {
|
|
448
699
|
if (generation !== thisGeneration)
|
|
449
700
|
return;
|
|
@@ -475,11 +726,12 @@ const constructor = (app) => {
|
|
|
475
726
|
}, sampler.samplePeriodMs());
|
|
476
727
|
};
|
|
477
728
|
scheduleSample();
|
|
478
|
-
statusTimer = setInterval(() => void updateStatus(), 10_000);
|
|
729
|
+
statusTimer = setInterval(() => { void syncProgressionCourse(); void onboard?.refresh(false); void updateStatus(); }, 10_000);
|
|
479
730
|
if (activeUploadMode === 'automatic')
|
|
480
731
|
connectTransport();
|
|
481
732
|
else
|
|
482
733
|
connectionState = 'recording_locally';
|
|
734
|
+
await onboard?.setAuthority(activeUploadMode);
|
|
483
735
|
await updateStatus();
|
|
484
736
|
}
|
|
485
737
|
async function updateStatus(detail, beginHistory = false) {
|
|
@@ -514,6 +766,8 @@ const constructor = (app) => {
|
|
|
514
766
|
profileRevision: activeProfile?.revision,
|
|
515
767
|
...transport?.transportMetrics()
|
|
516
768
|
});
|
|
769
|
+
void flushProgressionEvidence();
|
|
770
|
+
void flushOnboardSnapshots();
|
|
517
771
|
if (connectionState === 'device_revoked') {
|
|
518
772
|
app.setPluginStatus(`Wake Logger: Device revoked — enter a new pairing code; ${queue}${dropped}${sequence}`);
|
|
519
773
|
return;
|
|
@@ -550,9 +804,17 @@ const constructor = (app) => {
|
|
|
550
804
|
activeUploadMode = 'local_only';
|
|
551
805
|
pairingAbortController?.abort();
|
|
552
806
|
associationAbortController?.abort();
|
|
553
|
-
|
|
807
|
+
// Send one bounded retained final status before shutting transmission down.
|
|
808
|
+
// Its failure cannot prevent the fail-closed local-only transition.
|
|
809
|
+
const stopping = (async () => {
|
|
810
|
+
await transport?.publishFinalLocalOnlyStatus().catch(() => undefined);
|
|
811
|
+
// Graceful end: a DISCONNECT suppresses the retained Last Will so it cannot
|
|
812
|
+
// overwrite the retained local_only status after the plugin is gone.
|
|
813
|
+
await transport?.stop({ publishOffline: false, force: false });
|
|
814
|
+
})();
|
|
554
815
|
currentSampler?.updateMode('NORMAL');
|
|
555
816
|
connectionState = 'recording_locally';
|
|
817
|
+
void onboard?.setAuthority('local_only');
|
|
556
818
|
return stopping;
|
|
557
819
|
}
|
|
558
820
|
async function persistTrackingMode(mode, revision, thisGeneration) {
|