@aurora-blue/signalk-connector 0.2.0-dev.22 → 0.2.0-dev.26
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/README.md +13 -1
- package/VERSION +1 -1
- package/dist/src/config.d.ts +79 -0
- package/dist/src/config.js +92 -0
- package/dist/src/connector.d.ts +10 -0
- package/dist/src/connector.js +75 -12
- package/dist/src/delta.js +70 -2
- package/dist/src/index.js +2 -1
- package/dist/src/noforeignland.d.ts +57 -0
- package/dist/src/noforeignland.js +425 -0
- package/dist/src/telemetry-catalog.d.ts +3 -1
- package/dist/src/telemetry-catalog.js +32 -3
- package/dist/src/telemetry-reducer.js +2 -1
- package/dist/src/types.d.ts +2 -1
- package/dist/src/version.d.ts +1 -1
- package/dist/src/version.js +1 -1
- package/package.json +4 -2
- package/public/ci-tokens.css +6 -0
- package/public/favicon.ico +0 -0
- package/public/favicon.svg +13 -2
- package/public/index.html +4 -3
- package/public/locales/de.json +2 -2
- package/public/locales/en.json +2 -2
- package/public/locales/es.json +2 -2
- package/public/locales/fr.json +2 -2
package/README.md
CHANGED
|
@@ -51,7 +51,9 @@ Select Aurora Blue ABOARD Connector Status under Signal K Webapps for the cross-
|
|
|
51
51
|
|
|
52
52
|
The connector exchanges the pairing code once and clears it from the Signal K plugin configuration after successful pairing. The returned device token is stored only in the protected connector state file and is never written to logs. The pairing is bound to the configured target URL. Changing that URL requires a new pairing code and pauses collection and transfer until the replacement pairing succeeds. The configuration and status page warn that a successful replacement permanently discards telemetry buffered for the previous server. A failed pairing attempt retains the existing pairing and buffer.
|
|
53
53
|
|
|
54
|
-
The default configuration sends up to 250 events per batch, waits up to five seconds to combine normal telemetry, samples
|
|
54
|
+
The default configuration sends up to 250 events per batch, waits up to five seconds to combine normal telemetry, samples own-vessel Signal K paths every second, samples selected AIS paths every five seconds, and enables the preliminary underway change-or-heartbeat profile. Navigation, performance, steering observations, wind, depth, current, tide, outside weather, water and heave are captured independently of displayed fields. Additional scoped family paths preserve bounded structured source values, with a two-second minimum change interval and 60-second heartbeat under normal storage pressure. Ground- and water-referenced true wind remain separate. Deploy compatible Server acceptance before this Connector expansion; older Servers reject unsupported paths. See the [canonical acquisition decision](../aurora-blue-documentation/docs/decisions/ADR-0153-display-independent-navigation-capture.md). AIS can be disabled independently. The `auto` storage profile selects a constrained, standard, or extended byte budget from filesystem capacity while preserving a free-space reserve. Its effective event limit is derived from measured journal bytes per event instead of a fixed 10,000-event default. Protocol 1.1 permits no more than 500 events per batch, and the server continues to accept exact protocol 1.0 backlog batches during upgrades.
|
|
55
|
+
|
|
56
|
+
The connector also subscribes to scalar values below `electrical.switches.*`, including separate channels on multi-output devices. Switch paths use the instant subscription policy so a periodic retransmission of a cached Signal K value is not mistaken for fresh device evidence. The server may use selected `.state` leaves for conservative equipment-runtime estimates. A silent or unpowered device stops contributing runtime after a short bounded interval; accurate long-running counters require a qualified device-originated liveness source.
|
|
55
57
|
|
|
56
58
|
The optional anchor alarm is an experimental development scaffold. When enabled, position is requested every 250 milliseconds and every received position reaches the safety engine before telemetry reduction. The alarm publishes a local Signal K notification and detects stale position input without requiring the Aurora Blue target. It must not yet be used as the sole safety device.
|
|
57
59
|
|
|
@@ -117,6 +119,16 @@ The public package name is `@aurora-blue/signalk-connector`. Once its first rele
|
|
|
117
119
|
|
|
118
120
|
When migrating from the unscoped development package `aurora-blue-signalk-connector`, back up Signal K configuration and plugin data first. Stop Signal K and replace only the installed package; preserve the existing plugin configuration and data directory. Do not install both package names together: they deliberately share one runtime plugin ID. Restart and verify pairing, buffered records and status before resuming normal use.
|
|
119
121
|
|
|
122
|
+
The Signal K Webapps tile uses `signalk.appIcon` pointing to the bundled canonical `public/favicon.svg`: the white ABOARD sailing mark on the approved variant B gradient. It loads locally without an external image service.
|
|
123
|
+
|
|
120
124
|
Release maintainers use the protected GitLab pipeline described in the central Connector publication document. Package preparation does not itself publish a release or guarantee App Store indexing.
|
|
121
125
|
|
|
122
126
|
The authoritative architecture and protocol documentation is maintained in the separate `aurora-blue-documentation` repository.
|
|
127
|
+
|
|
128
|
+
## Optional NoForeignLand publisher (development)
|
|
129
|
+
|
|
130
|
+
The connector can publish observed own-vessel tracks through an independent, bounded offline queue using its existing Signal K observations. Enable the optional NoForeignLand settings only after obtaining dedicated ABOARD application credentials and a boat key, disabling the original plugin and completing queue handover. Existing official-plugin configuration blocks startup to prevent duplicate publication. The publisher is disabled by default.
|
|
131
|
+
|
|
132
|
+
This development integration is not yet qualified for live replacement. Provider approval, actual API delivery, settings UI verification, backlog handover and onboard resource measurements remain pending. Arbitrary cron expressions, automatic import of the original queue and unlimited sent-track archives are not implemented. Ambiguous acknowledgements can cause a replay; exactly-once delivery is not claimed.
|
|
133
|
+
|
|
134
|
+
See the canonical [integration and handover documentation](../aurora-blue-documentation/docs/NOFOREIGNLAND_CONNECTOR.md) and ADR-0151. Never reuse the official plugin's embedded application key or include credentials in diagnostic output.
|
package/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
0.2.0-dev.
|
|
1
|
+
0.2.0-dev.26
|
package/dist/src/config.d.ts
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import type { AnchorAlarmConfig } from "./safety/anchor-alarm";
|
|
2
2
|
import type { StorageProfile } from "./storage-budget";
|
|
3
3
|
import type { OperatingProfile } from "./telemetry-reducer";
|
|
4
|
+
import type { NoForeignLandConfig } from "./noforeignland";
|
|
4
5
|
export interface ConnectorConfig {
|
|
6
|
+
noforeignland?: NoForeignLandConfig;
|
|
5
7
|
targetUrl: string;
|
|
6
8
|
deviceName: string;
|
|
7
9
|
pairingCode?: string;
|
|
@@ -32,6 +34,75 @@ export declare const pluginSchema: {
|
|
|
32
34
|
readonly type: "object";
|
|
33
35
|
readonly required: readonly ["targetUrl"];
|
|
34
36
|
readonly properties: {
|
|
37
|
+
readonly noforeignland: {
|
|
38
|
+
readonly type: "object";
|
|
39
|
+
readonly title: "NoForeignLand publishing";
|
|
40
|
+
readonly description: "Optional direct track publication. Requires dedicated ABOARD application credentials. Disable the separate NoForeignLand plugin before enabling this publisher.";
|
|
41
|
+
readonly properties: {
|
|
42
|
+
readonly enabled: {
|
|
43
|
+
readonly type: "boolean";
|
|
44
|
+
readonly title: "Publish to NoForeignLand";
|
|
45
|
+
readonly default: false;
|
|
46
|
+
};
|
|
47
|
+
readonly applicationKey: {
|
|
48
|
+
readonly type: "string";
|
|
49
|
+
readonly title: "ABOARD application key";
|
|
50
|
+
};
|
|
51
|
+
readonly boatKey: {
|
|
52
|
+
readonly type: "string";
|
|
53
|
+
readonly title: "Boat API key";
|
|
54
|
+
};
|
|
55
|
+
readonly source: {
|
|
56
|
+
readonly type: "string";
|
|
57
|
+
readonly title: "Position source (empty selects automatically)";
|
|
58
|
+
};
|
|
59
|
+
readonly sampleSeconds: {
|
|
60
|
+
readonly type: "integer";
|
|
61
|
+
readonly title: "Minimum sample interval (seconds)";
|
|
62
|
+
readonly minimum: 10;
|
|
63
|
+
readonly maximum: 3600;
|
|
64
|
+
readonly default: 60;
|
|
65
|
+
};
|
|
66
|
+
readonly minimumMoveMeters: {
|
|
67
|
+
readonly type: "integer";
|
|
68
|
+
readonly title: "Minimum movement (meters)";
|
|
69
|
+
readonly minimum: 0;
|
|
70
|
+
readonly maximum: 10000;
|
|
71
|
+
readonly default: 80;
|
|
72
|
+
};
|
|
73
|
+
readonly minimumSpeedKnots: {
|
|
74
|
+
readonly type: "number";
|
|
75
|
+
readonly title: "Minimum speed (knots)";
|
|
76
|
+
readonly minimum: 0;
|
|
77
|
+
readonly maximum: 100;
|
|
78
|
+
readonly default: 1.5;
|
|
79
|
+
};
|
|
80
|
+
readonly maximumVelocity: {
|
|
81
|
+
readonly type: "integer";
|
|
82
|
+
readonly title: "Maximum plausible velocity (meters per second)";
|
|
83
|
+
readonly minimum: 1;
|
|
84
|
+
readonly maximum: 500;
|
|
85
|
+
readonly default: 50;
|
|
86
|
+
};
|
|
87
|
+
readonly sendWhileMoving: {
|
|
88
|
+
readonly type: "boolean";
|
|
89
|
+
readonly title: "Send while moving";
|
|
90
|
+
readonly default: true;
|
|
91
|
+
};
|
|
92
|
+
readonly keepalive: {
|
|
93
|
+
readonly type: "boolean";
|
|
94
|
+
readonly title: "Publish a fresh position every 24 hours";
|
|
95
|
+
readonly default: true;
|
|
96
|
+
};
|
|
97
|
+
readonly sendIntervalSeconds: {
|
|
98
|
+
readonly type: "integer";
|
|
99
|
+
readonly title: "Send interval (seconds)";
|
|
100
|
+
readonly minimum: 60;
|
|
101
|
+
readonly maximum: 86400;
|
|
102
|
+
readonly default: 600;
|
|
103
|
+
};
|
|
104
|
+
};
|
|
105
|
+
};
|
|
35
106
|
readonly targetUrl: {
|
|
36
107
|
readonly type: "string";
|
|
37
108
|
readonly title: "Aurora Blue target URL";
|
|
@@ -181,6 +252,14 @@ export declare const pluginSchema: {
|
|
|
181
252
|
};
|
|
182
253
|
};
|
|
183
254
|
export declare const pluginUiSchema: {
|
|
255
|
+
readonly noforeignland: {
|
|
256
|
+
readonly applicationKey: {
|
|
257
|
+
readonly "ui:widget": "password";
|
|
258
|
+
};
|
|
259
|
+
readonly boatKey: {
|
|
260
|
+
readonly "ui:widget": "password";
|
|
261
|
+
};
|
|
262
|
+
};
|
|
184
263
|
readonly targetUrl: {
|
|
185
264
|
readonly "ui:options": {
|
|
186
265
|
readonly enableMarkdownInDescription: true;
|
package/dist/src/config.js
CHANGED
|
@@ -104,7 +104,32 @@ const resolveConfig = (input) => {
|
|
|
104
104
|
if (anchorHysteresisMeters >= anchorRadiusMeters) {
|
|
105
105
|
throw new Error("Anchor hysteresis must be smaller than the anchor radius.");
|
|
106
106
|
}
|
|
107
|
+
if (raw.noforeignland !== undefined &&
|
|
108
|
+
(raw.noforeignland === null ||
|
|
109
|
+
typeof raw.noforeignland !== "object" ||
|
|
110
|
+
Array.isArray(raw.noforeignland))) {
|
|
111
|
+
throw new Error("NoForeignLand settings must be an object.");
|
|
112
|
+
}
|
|
113
|
+
const nfl = (raw.noforeignland ?? {});
|
|
114
|
+
const nflEnabled = nfl.enabled === true;
|
|
115
|
+
const secret = (value) => typeof value === "string" ? value.trim() : "";
|
|
116
|
+
if (nflEnabled && (!secret(nfl.applicationKey) || !secret(nfl.boatKey))) {
|
|
117
|
+
throw new Error("NoForeignLand requires an ABOARD application key and a boat key.");
|
|
118
|
+
}
|
|
107
119
|
return {
|
|
120
|
+
noforeignland: {
|
|
121
|
+
enabled: nflEnabled,
|
|
122
|
+
applicationKey: secret(nfl.applicationKey),
|
|
123
|
+
boatKey: secret(nfl.boatKey),
|
|
124
|
+
source: secret(nfl.source),
|
|
125
|
+
sampleSeconds: integer(nfl.sampleSeconds, 60, 10, 3600, "NoForeignLand sample interval"),
|
|
126
|
+
minimumMoveMeters: integer(nfl.minimumMoveMeters, 80, 0, 10000, "NoForeignLand minimum movement"),
|
|
127
|
+
minimumSpeedKnots: finiteNumber(nfl.minimumSpeedKnots, 0, 100, "NoForeignLand minimum speed") ?? 1.5,
|
|
128
|
+
maximumVelocity: integer(nfl.maximumVelocity, 50, 1, 500, "NoForeignLand maximum velocity"),
|
|
129
|
+
sendWhileMoving: nfl.sendWhileMoving !== false,
|
|
130
|
+
keepalive: nfl.keepalive !== false,
|
|
131
|
+
sendIntervalSeconds: integer(nfl.sendIntervalSeconds, 600, 60, 86400, "NoForeignLand send interval"),
|
|
132
|
+
},
|
|
108
133
|
targetUrl: parsedUrl.toString().replace(/\/$/, ""),
|
|
109
134
|
deviceName: deviceName || "Aurora Blue Signal K",
|
|
110
135
|
pairingCode: typeof raw.pairingCode === "string" && raw.pairingCode.trim() !== ""
|
|
@@ -151,6 +176,69 @@ exports.pluginSchema = {
|
|
|
151
176
|
type: "object",
|
|
152
177
|
required: ["targetUrl"],
|
|
153
178
|
properties: {
|
|
179
|
+
noforeignland: {
|
|
180
|
+
type: "object",
|
|
181
|
+
title: "NoForeignLand publishing",
|
|
182
|
+
description: "Optional direct track publication. Requires dedicated ABOARD application credentials. Disable the separate NoForeignLand plugin before enabling this publisher.",
|
|
183
|
+
properties: {
|
|
184
|
+
enabled: {
|
|
185
|
+
type: "boolean",
|
|
186
|
+
title: "Publish to NoForeignLand",
|
|
187
|
+
default: false,
|
|
188
|
+
},
|
|
189
|
+
applicationKey: { type: "string", title: "ABOARD application key" },
|
|
190
|
+
boatKey: { type: "string", title: "Boat API key" },
|
|
191
|
+
source: {
|
|
192
|
+
type: "string",
|
|
193
|
+
title: "Position source (empty selects automatically)",
|
|
194
|
+
},
|
|
195
|
+
sampleSeconds: {
|
|
196
|
+
type: "integer",
|
|
197
|
+
title: "Minimum sample interval (seconds)",
|
|
198
|
+
minimum: 10,
|
|
199
|
+
maximum: 3600,
|
|
200
|
+
default: 60,
|
|
201
|
+
},
|
|
202
|
+
minimumMoveMeters: {
|
|
203
|
+
type: "integer",
|
|
204
|
+
title: "Minimum movement (meters)",
|
|
205
|
+
minimum: 0,
|
|
206
|
+
maximum: 10000,
|
|
207
|
+
default: 80,
|
|
208
|
+
},
|
|
209
|
+
minimumSpeedKnots: {
|
|
210
|
+
type: "number",
|
|
211
|
+
title: "Minimum speed (knots)",
|
|
212
|
+
minimum: 0,
|
|
213
|
+
maximum: 100,
|
|
214
|
+
default: 1.5,
|
|
215
|
+
},
|
|
216
|
+
maximumVelocity: {
|
|
217
|
+
type: "integer",
|
|
218
|
+
title: "Maximum plausible velocity (meters per second)",
|
|
219
|
+
minimum: 1,
|
|
220
|
+
maximum: 500,
|
|
221
|
+
default: 50,
|
|
222
|
+
},
|
|
223
|
+
sendWhileMoving: {
|
|
224
|
+
type: "boolean",
|
|
225
|
+
title: "Send while moving",
|
|
226
|
+
default: true,
|
|
227
|
+
},
|
|
228
|
+
keepalive: {
|
|
229
|
+
type: "boolean",
|
|
230
|
+
title: "Publish a fresh position every 24 hours",
|
|
231
|
+
default: true,
|
|
232
|
+
},
|
|
233
|
+
sendIntervalSeconds: {
|
|
234
|
+
type: "integer",
|
|
235
|
+
title: "Send interval (seconds)",
|
|
236
|
+
minimum: 60,
|
|
237
|
+
maximum: 86400,
|
|
238
|
+
default: 600,
|
|
239
|
+
},
|
|
240
|
+
},
|
|
241
|
+
},
|
|
154
242
|
targetUrl: {
|
|
155
243
|
type: "string",
|
|
156
244
|
title: "Aurora Blue target URL",
|
|
@@ -306,6 +394,10 @@ exports.pluginSchema = {
|
|
|
306
394
|
},
|
|
307
395
|
};
|
|
308
396
|
exports.pluginUiSchema = {
|
|
397
|
+
noforeignland: {
|
|
398
|
+
applicationKey: { "ui:widget": "password" },
|
|
399
|
+
boatKey: { "ui:widget": "password" },
|
|
400
|
+
},
|
|
309
401
|
targetUrl: {
|
|
310
402
|
"ui:options": {
|
|
311
403
|
enableMarkdownInDescription: true,
|
package/dist/src/connector.d.ts
CHANGED
|
@@ -43,11 +43,21 @@ export declare class ConnectorRuntime {
|
|
|
43
43
|
private nextCapacityRefreshAt;
|
|
44
44
|
private storageBudget;
|
|
45
45
|
private readonly emissionTimes;
|
|
46
|
+
private publisher;
|
|
46
47
|
private captureTail;
|
|
47
48
|
private pendingCaptureEvents;
|
|
48
49
|
private nextCaptureStatusAt;
|
|
49
50
|
constructor(app: ServerAPI, store: StateStore, config: ConnectorConfig, options?: ConnectorRuntimeOptions);
|
|
50
51
|
start(): Promise<void>;
|
|
52
|
+
publisherStatus(): {
|
|
53
|
+
state: import("./noforeignland").PublisherState;
|
|
54
|
+
message: string;
|
|
55
|
+
pending: number;
|
|
56
|
+
dropped: number;
|
|
57
|
+
savedAt: number | null;
|
|
58
|
+
sentAt: number | null;
|
|
59
|
+
source: string | null;
|
|
60
|
+
} | null;
|
|
51
61
|
stop(): Promise<void>;
|
|
52
62
|
capture(delta: Delta, subscriptionScope?: SubscriptionScope): Promise<void>;
|
|
53
63
|
private captureEvents;
|
package/dist/src/connector.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.ConnectorRuntime = exports.CONNECTOR_VERSION = void 0;
|
|
4
|
+
const node_path_1 = require("node:path");
|
|
5
|
+
const noforeignland_1 = require("./noforeignland");
|
|
4
6
|
const node_crypto_1 = require("node:crypto");
|
|
5
7
|
const backoff_1 = require("./backoff");
|
|
6
8
|
const delta_1 = require("./delta");
|
|
@@ -53,6 +55,7 @@ class ConnectorRuntime {
|
|
|
53
55
|
nextCapacityRefreshAt = 0;
|
|
54
56
|
storageBudget;
|
|
55
57
|
emissionTimes = [];
|
|
58
|
+
publisher = null;
|
|
56
59
|
captureTail = Promise.resolve();
|
|
57
60
|
pendingCaptureEvents = 0;
|
|
58
61
|
nextCaptureStatusAt = 0;
|
|
@@ -73,6 +76,47 @@ class ConnectorRuntime {
|
|
|
73
76
|
}
|
|
74
77
|
async start() {
|
|
75
78
|
await this.store.load();
|
|
79
|
+
if (this.config.noforeignland?.enabled) {
|
|
80
|
+
this.publisher = new noforeignland_1.NoForeignLandPublisher((0, node_path_1.join)(this.app.getDataDirPath(), "noforeignland"), this.config.noforeignland, (status) => {
|
|
81
|
+
this.app.handleMessage("aurora-blue-signalk-connector", {
|
|
82
|
+
context: "vessels.self",
|
|
83
|
+
updates: [
|
|
84
|
+
{
|
|
85
|
+
timestamp: this.now(),
|
|
86
|
+
values: [
|
|
87
|
+
{
|
|
88
|
+
path: "noforeignland.status",
|
|
89
|
+
value: status.message,
|
|
90
|
+
},
|
|
91
|
+
{
|
|
92
|
+
path: "noforeignland.status_boolean",
|
|
93
|
+
value: status.state === "error" || status.state === "waiting"
|
|
94
|
+
? 1
|
|
95
|
+
: 0,
|
|
96
|
+
},
|
|
97
|
+
...(status.savedAt === null
|
|
98
|
+
? []
|
|
99
|
+
: [
|
|
100
|
+
{
|
|
101
|
+
path: "noforeignland.savepoint",
|
|
102
|
+
value: new Date(status.savedAt).toISOString(),
|
|
103
|
+
},
|
|
104
|
+
]),
|
|
105
|
+
...(status.sentAt === null
|
|
106
|
+
? []
|
|
107
|
+
: [
|
|
108
|
+
{
|
|
109
|
+
path: "noforeignland.sent_to_api",
|
|
110
|
+
value: new Date(status.sentAt).toISOString(),
|
|
111
|
+
},
|
|
112
|
+
]),
|
|
113
|
+
],
|
|
114
|
+
},
|
|
115
|
+
],
|
|
116
|
+
});
|
|
117
|
+
});
|
|
118
|
+
await this.publisher.start(this.autoSchedule);
|
|
119
|
+
}
|
|
76
120
|
await this.refreshStorageBudget(true);
|
|
77
121
|
this.client ??= new http_client_1.AuroraBlueHttpClient(this.config.targetUrl, this.config.requestTimeoutMs, fetch, this.config.compressionEnabled
|
|
78
122
|
? this.config.compressionThresholdBytes
|
|
@@ -90,6 +134,7 @@ class ConnectorRuntime {
|
|
|
90
134
|
}
|
|
91
135
|
if (state.device_id === null) {
|
|
92
136
|
this.pairingState = "unpaired";
|
|
137
|
+
this.configureSubscriptions();
|
|
93
138
|
this.app.setPluginStatus("Unpaired - enter an Aurora Blue pairing code in plugin settings.");
|
|
94
139
|
return;
|
|
95
140
|
}
|
|
@@ -135,6 +180,9 @@ class ConnectorRuntime {
|
|
|
135
180
|
this.schedule(0);
|
|
136
181
|
}
|
|
137
182
|
}
|
|
183
|
+
publisherStatus() {
|
|
184
|
+
return this.publisher?.status() ?? null;
|
|
185
|
+
}
|
|
138
186
|
async stop() {
|
|
139
187
|
this.stopped = true;
|
|
140
188
|
if (this.timer !== null) {
|
|
@@ -152,6 +200,7 @@ class ConnectorRuntime {
|
|
|
152
200
|
for (const unsubscribe of this.unsubscribes.splice(0)) {
|
|
153
201
|
unsubscribe();
|
|
154
202
|
}
|
|
203
|
+
await this.publisher?.stop();
|
|
155
204
|
await this.captureTail;
|
|
156
205
|
await this.store.summary();
|
|
157
206
|
}
|
|
@@ -161,6 +210,8 @@ class ConnectorRuntime {
|
|
|
161
210
|
}
|
|
162
211
|
const receivedAt = this.now();
|
|
163
212
|
const events = (0, delta_1.eventsFromDelta)(delta, this.bootId, receivedAt, subscriptionScope);
|
|
213
|
+
for (const event of events)
|
|
214
|
+
this.publisher?.observe(event);
|
|
164
215
|
// Safety observes current positions immediately, even when historical
|
|
165
216
|
// telemetry admission is saturated by slow storage.
|
|
166
217
|
for (const event of events) {
|
|
@@ -318,14 +369,24 @@ class ConnectorRuntime {
|
|
|
318
369
|
return;
|
|
319
370
|
}
|
|
320
371
|
if (this.pairingState !== "paired") {
|
|
321
|
-
if (this.config.anchorAlarm.enabled) {
|
|
322
|
-
this.subscribeToContext("vessels.self", [
|
|
372
|
+
if (this.config.anchorAlarm.enabled || this.publisher) {
|
|
373
|
+
this.subscribeToContext("vessels.self", [
|
|
374
|
+
"navigation.position",
|
|
375
|
+
...(this.publisher ? ["navigation.speedOverGround"] : []),
|
|
376
|
+
], this.config.anchorAlarm.enabled
|
|
377
|
+
? this.config.safetySamplePeriodMs
|
|
378
|
+
: this.config.samplePeriodMs);
|
|
323
379
|
}
|
|
324
380
|
return;
|
|
325
381
|
}
|
|
326
382
|
if (this.operatingMode === "standby") {
|
|
327
|
-
if (this.config.anchorAlarm.enabled) {
|
|
328
|
-
this.subscribeToContext("vessels.self", [
|
|
383
|
+
if (this.config.anchorAlarm.enabled || this.publisher) {
|
|
384
|
+
this.subscribeToContext("vessels.self", [
|
|
385
|
+
"navigation.position",
|
|
386
|
+
...(this.publisher ? ["navigation.speedOverGround"] : []),
|
|
387
|
+
], this.config.anchorAlarm.enabled
|
|
388
|
+
? this.config.safetySamplePeriodMs
|
|
389
|
+
: this.config.samplePeriodMs);
|
|
329
390
|
}
|
|
330
391
|
return;
|
|
331
392
|
}
|
|
@@ -337,14 +398,16 @@ class ConnectorRuntime {
|
|
|
337
398
|
subscribeToContext(context, paths, period) {
|
|
338
399
|
this.app.subscriptionmanager.subscribe({
|
|
339
400
|
context,
|
|
340
|
-
subscribe: paths.map((path) =>
|
|
341
|
-
path: path,
|
|
342
|
-
|
|
343
|
-
path
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
401
|
+
subscribe: paths.map((path) => context === "vessels.self" && path === "electrical.switches.*"
|
|
402
|
+
? { path: path, policy: "instant", minPeriod: 0 }
|
|
403
|
+
: {
|
|
404
|
+
path: path,
|
|
405
|
+
period: context === "vessels.self" &&
|
|
406
|
+
(path === "navigation.position" || path === "navigation.*") &&
|
|
407
|
+
this.config.anchorAlarm.enabled
|
|
408
|
+
? Math.min(this.config.samplePeriodMs, this.config.safetySamplePeriodMs)
|
|
409
|
+
: period,
|
|
410
|
+
}),
|
|
348
411
|
}, this.unsubscribes, (error) => {
|
|
349
412
|
this.app.setPluginError("Signal K subscription failed.");
|
|
350
413
|
this.app.error(error instanceof Error ? error.message : String(error));
|
package/dist/src/delta.js
CHANGED
|
@@ -26,6 +26,28 @@ const sourceReference = (update) => {
|
|
|
26
26
|
}
|
|
27
27
|
return null;
|
|
28
28
|
};
|
|
29
|
+
// Bound generic navigation payloads without changing their source shape.
|
|
30
|
+
const validNavigationValue = (value) => {
|
|
31
|
+
let nodes = 0;
|
|
32
|
+
const visit = (item, depth) => {
|
|
33
|
+
if (++nodes > 1024 || depth > 8)
|
|
34
|
+
return false;
|
|
35
|
+
if (item === null || typeof item === "boolean")
|
|
36
|
+
return true;
|
|
37
|
+
if (typeof item === "number")
|
|
38
|
+
return Number.isFinite(item);
|
|
39
|
+
if (typeof item === "string")
|
|
40
|
+
return !item.includes("\u0000");
|
|
41
|
+
if (Array.isArray(item))
|
|
42
|
+
return item.every((child) => visit(child, depth + 1));
|
|
43
|
+
if (typeof item !== "object")
|
|
44
|
+
return false;
|
|
45
|
+
return Object.entries(item).every(([key, child]) => !key.includes("\u0000") && visit(child, depth + 1));
|
|
46
|
+
};
|
|
47
|
+
return (value !== null &&
|
|
48
|
+
visit(value, 0) &&
|
|
49
|
+
Buffer.byteLength(JSON.stringify(value), "utf8") <= 16_384);
|
|
50
|
+
};
|
|
29
51
|
const validValue = (context, path, value) => {
|
|
30
52
|
const definition = (0, telemetry_catalog_1.telemetryDefinition)(context, path);
|
|
31
53
|
if (definition === null) {
|
|
@@ -48,11 +70,27 @@ const validValue = (context, path, value) => {
|
|
|
48
70
|
longitude > 180) {
|
|
49
71
|
return null;
|
|
50
72
|
}
|
|
51
|
-
return
|
|
73
|
+
return validNavigationValue(value)
|
|
74
|
+
? { ...position, latitude, longitude }
|
|
75
|
+
: null;
|
|
76
|
+
}
|
|
77
|
+
if (definition.valueKind === "json") {
|
|
78
|
+
return validNavigationValue(value)
|
|
79
|
+
? structuredClone(value)
|
|
80
|
+
: null;
|
|
52
81
|
}
|
|
53
82
|
if (definition.valueKind === "number") {
|
|
54
83
|
return typeof value === "number" && Number.isFinite(value) ? value : null;
|
|
55
84
|
}
|
|
85
|
+
if (definition.valueKind === "switch") {
|
|
86
|
+
if (typeof value === "boolean")
|
|
87
|
+
return value;
|
|
88
|
+
if (typeof value === "number" && Number.isFinite(value))
|
|
89
|
+
return value;
|
|
90
|
+
return typeof value === "string" && value.length > 0
|
|
91
|
+
? value.slice(0, 255)
|
|
92
|
+
: null;
|
|
93
|
+
}
|
|
56
94
|
if (definition.valueKind === "string") {
|
|
57
95
|
if (path === "mmsi" && !/^[0-9]{9}$/.test(String(value))) {
|
|
58
96
|
return null;
|
|
@@ -101,6 +139,32 @@ const flattenedStaticValues = (context, path, value) => {
|
|
|
101
139
|
{ path: "navigation.state", value: navigation?.state },
|
|
102
140
|
];
|
|
103
141
|
};
|
|
142
|
+
const flattenedSwitchValues = (path, value) => {
|
|
143
|
+
if (path !== "electrical.switches" &&
|
|
144
|
+
!path.startsWith("electrical.switches.")) {
|
|
145
|
+
return [{ path, value }];
|
|
146
|
+
}
|
|
147
|
+
const leaves = [];
|
|
148
|
+
const visit = (currentPath, currentValue, depth) => {
|
|
149
|
+
if (leaves.length >= 100 || depth > 8)
|
|
150
|
+
return;
|
|
151
|
+
if (currentValue === null || Array.isArray(currentValue))
|
|
152
|
+
return;
|
|
153
|
+
if (typeof currentValue !== "object") {
|
|
154
|
+
leaves.push({ path: currentPath, value: currentValue });
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
for (const [name, child] of Object.entries(currentValue)) {
|
|
158
|
+
if (!/^[A-Za-z0-9_:-]+$/.test(name))
|
|
159
|
+
continue;
|
|
160
|
+
visit(`${currentPath}.${name}`, child, depth + 1);
|
|
161
|
+
if (leaves.length >= 100)
|
|
162
|
+
break;
|
|
163
|
+
}
|
|
164
|
+
};
|
|
165
|
+
visit(path, value, 0);
|
|
166
|
+
return leaves;
|
|
167
|
+
};
|
|
104
168
|
const eventsFromDelta = (delta, bootId, receivedAt, subscriptionScope = "self") => {
|
|
105
169
|
const events = [];
|
|
106
170
|
const context = eventContext(delta, subscriptionScope);
|
|
@@ -115,7 +179,11 @@ const eventsFromDelta = (delta, bootId, receivedAt, subscriptionScope = "self")
|
|
|
115
179
|
if (pathValue.state?.timedOut === true) {
|
|
116
180
|
continue;
|
|
117
181
|
}
|
|
118
|
-
|
|
182
|
+
const path = String(pathValue.path);
|
|
183
|
+
const candidates = context === "vessels.self"
|
|
184
|
+
? flattenedSwitchValues(path, pathValue.value)
|
|
185
|
+
: flattenedStaticValues(context, path, pathValue.value);
|
|
186
|
+
for (const candidate of candidates) {
|
|
119
187
|
const value = validValue(context, candidate.path, candidate.value);
|
|
120
188
|
if (value === null) {
|
|
121
189
|
continue;
|
package/dist/src/index.js
CHANGED
|
@@ -27,11 +27,12 @@ const createPlugin = (app) => {
|
|
|
27
27
|
generated_at: new Date().toISOString(),
|
|
28
28
|
last_error: lastError,
|
|
29
29
|
status,
|
|
30
|
+
noforeignland: runtime?.publisherStatus() ?? null,
|
|
30
31
|
};
|
|
31
32
|
};
|
|
32
33
|
return {
|
|
33
34
|
id: "aurora-blue-signalk-connector",
|
|
34
|
-
name: "Aurora Blue ABOARD
|
|
35
|
+
name: "Aurora Blue ABOARD SK",
|
|
35
36
|
description: "Buffers selected Signal K data and transfers it to Aurora Blue ABOARD.",
|
|
36
37
|
schema: config_1.pluginSchema,
|
|
37
38
|
uiSchema: config_1.pluginUiSchema,
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import type { EventInput } from "./types";
|
|
2
|
+
export interface NoForeignLandConfig {
|
|
3
|
+
enabled: boolean;
|
|
4
|
+
applicationKey: string;
|
|
5
|
+
boatKey: string;
|
|
6
|
+
source: string;
|
|
7
|
+
sampleSeconds: number;
|
|
8
|
+
minimumMoveMeters: number;
|
|
9
|
+
minimumSpeedKnots: number;
|
|
10
|
+
maximumVelocity: number;
|
|
11
|
+
sendWhileMoving: boolean;
|
|
12
|
+
keepalive: boolean;
|
|
13
|
+
sendIntervalSeconds: number;
|
|
14
|
+
}
|
|
15
|
+
export type PublisherState = "disabled" | "ready" | "waiting" | "sending" | "error";
|
|
16
|
+
/** Independent, bounded publisher consuming the connector's existing observations. */
|
|
17
|
+
export declare class NoForeignLandPublisher {
|
|
18
|
+
private readonly directory;
|
|
19
|
+
private readonly config;
|
|
20
|
+
private readonly emit;
|
|
21
|
+
private readonly request;
|
|
22
|
+
private readonly now;
|
|
23
|
+
private journal;
|
|
24
|
+
private state;
|
|
25
|
+
private message;
|
|
26
|
+
private selectedSource;
|
|
27
|
+
private sourceSeenAt;
|
|
28
|
+
private speed;
|
|
29
|
+
private tail;
|
|
30
|
+
private outstanding;
|
|
31
|
+
private flight;
|
|
32
|
+
private controller;
|
|
33
|
+
private timer;
|
|
34
|
+
private stopped;
|
|
35
|
+
private dropped;
|
|
36
|
+
private nextSendAt;
|
|
37
|
+
private readonly file;
|
|
38
|
+
private journalBytes;
|
|
39
|
+
constructor(directory: string, config: NoForeignLandConfig, emit: (status: ReturnType<NoForeignLandPublisher["status"]>) => void, request?: typeof fetch, now?: () => number);
|
|
40
|
+
status(): {
|
|
41
|
+
state: PublisherState;
|
|
42
|
+
message: string;
|
|
43
|
+
pending: number;
|
|
44
|
+
dropped: number;
|
|
45
|
+
savedAt: number | null;
|
|
46
|
+
sentAt: number | null;
|
|
47
|
+
source: string | null;
|
|
48
|
+
};
|
|
49
|
+
start(schedule?: boolean): Promise<void>;
|
|
50
|
+
observe(event: EventInput): void;
|
|
51
|
+
idle(): Promise<void>;
|
|
52
|
+
flush(): Promise<void>;
|
|
53
|
+
private send;
|
|
54
|
+
private save;
|
|
55
|
+
private schedule;
|
|
56
|
+
stop(): Promise<void>;
|
|
57
|
+
}
|
|
@@ -0,0 +1,425 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.NoForeignLandPublisher = void 0;
|
|
4
|
+
const node_crypto_1 = require("node:crypto");
|
|
5
|
+
const promises_1 = require("node:fs/promises");
|
|
6
|
+
const node_path_1 = require("node:path");
|
|
7
|
+
const geometry_1 = require("./geometry");
|
|
8
|
+
const version_1 = require("./version");
|
|
9
|
+
const MAX_POINTS = 10_000;
|
|
10
|
+
const MAX_FILE_BYTES = 4_000_000;
|
|
11
|
+
const ENDPOINT = "https://www.noforeignland.com/home/api/v1/boat/tracking/track";
|
|
12
|
+
/** Independent, bounded publisher consuming the connector's existing observations. */
|
|
13
|
+
class NoForeignLandPublisher {
|
|
14
|
+
directory;
|
|
15
|
+
config;
|
|
16
|
+
emit;
|
|
17
|
+
request;
|
|
18
|
+
now;
|
|
19
|
+
journal;
|
|
20
|
+
state = "disabled";
|
|
21
|
+
message = "Disabled";
|
|
22
|
+
selectedSource = null;
|
|
23
|
+
sourceSeenAt = 0;
|
|
24
|
+
speed = null;
|
|
25
|
+
tail = Promise.resolve();
|
|
26
|
+
outstanding = 0;
|
|
27
|
+
flight = null;
|
|
28
|
+
controller = null;
|
|
29
|
+
timer = null;
|
|
30
|
+
stopped = false;
|
|
31
|
+
dropped = 0;
|
|
32
|
+
nextSendAt = 0;
|
|
33
|
+
file;
|
|
34
|
+
journalBytes = 0;
|
|
35
|
+
constructor(directory, config, emit, request = fetch, now = Date.now) {
|
|
36
|
+
this.directory = directory;
|
|
37
|
+
this.config = config;
|
|
38
|
+
this.emit = emit;
|
|
39
|
+
this.request = request;
|
|
40
|
+
this.now = now;
|
|
41
|
+
this.file = (0, node_path_1.join)(directory, "publisher.jsonl");
|
|
42
|
+
this.journal = {
|
|
43
|
+
version: 1,
|
|
44
|
+
account: (0, node_crypto_1.createHash)("sha256").update(config.boatKey).digest("hex"),
|
|
45
|
+
pending: [],
|
|
46
|
+
last: null,
|
|
47
|
+
sentAt: null,
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
status() {
|
|
51
|
+
return {
|
|
52
|
+
state: this.state,
|
|
53
|
+
message: this.message,
|
|
54
|
+
pending: this.journal.pending.length,
|
|
55
|
+
dropped: this.dropped,
|
|
56
|
+
savedAt: this.journal.last?.timestamp ?? null,
|
|
57
|
+
sentAt: this.journal.sentAt,
|
|
58
|
+
source: this.selectedSource,
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
async start(schedule = true) {
|
|
62
|
+
if (!this.config.enabled)
|
|
63
|
+
return;
|
|
64
|
+
try {
|
|
65
|
+
// Signal K stores the two historical plugin IDs alongside this plugin.
|
|
66
|
+
for (const name of [
|
|
67
|
+
"signalk-to-noforeignland.json",
|
|
68
|
+
"@noforeignland-signalk-to-noforeignland.json",
|
|
69
|
+
]) {
|
|
70
|
+
try {
|
|
71
|
+
const other = JSON.parse(await (0, promises_1.readFile)((0, node_path_1.join)((0, node_path_1.dirname)((0, node_path_1.dirname)(this.directory)), name), "utf8"));
|
|
72
|
+
if (other.enabled === true) {
|
|
73
|
+
this.state = "error";
|
|
74
|
+
this.message =
|
|
75
|
+
"Disable the separate NoForeignLand plugin before enabling publication";
|
|
76
|
+
this.emit(this.status());
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
catch (error) {
|
|
81
|
+
if (error.code !== "ENOENT")
|
|
82
|
+
throw error;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
await (0, promises_1.mkdir)(this.directory, { recursive: true, mode: 0o700 });
|
|
86
|
+
try {
|
|
87
|
+
if ((await (0, promises_1.stat)(this.file)).size > MAX_FILE_BYTES)
|
|
88
|
+
throw new Error("oversized");
|
|
89
|
+
const contents = await (0, promises_1.readFile)(this.file, "utf8");
|
|
90
|
+
const lines = contents.trimEnd().split("\n");
|
|
91
|
+
const saved = JSON.parse(lines.shift() ?? "");
|
|
92
|
+
if (saved.version !== 1 ||
|
|
93
|
+
saved.account !== this.journal.account ||
|
|
94
|
+
!Array.isArray(saved.pending))
|
|
95
|
+
throw new Error("invalid journal or changed boat");
|
|
96
|
+
for (const line of lines) {
|
|
97
|
+
const record = JSON.parse(line);
|
|
98
|
+
if (record.point && validPoint(record.point)) {
|
|
99
|
+
if (saved.last && record.point.timestamp <= saved.last.timestamp)
|
|
100
|
+
throw new Error("invalid order");
|
|
101
|
+
saved.pending.push(record.point);
|
|
102
|
+
saved.last = record.point;
|
|
103
|
+
}
|
|
104
|
+
else if (Number.isFinite(record.ack) &&
|
|
105
|
+
Number.isFinite(record.sentAt)) {
|
|
106
|
+
saved.pending = saved.pending.filter((point) => point.timestamp > record.ack);
|
|
107
|
+
saved.sentAt = record.sentAt;
|
|
108
|
+
}
|
|
109
|
+
else
|
|
110
|
+
throw new Error("invalid record");
|
|
111
|
+
if (saved.pending.length > MAX_POINTS)
|
|
112
|
+
throw new Error("queue limit");
|
|
113
|
+
}
|
|
114
|
+
if (saved.pending.length > MAX_POINTS ||
|
|
115
|
+
!saved.pending.every(validPoint) ||
|
|
116
|
+
(saved.last !== null && !validPoint(saved.last)) ||
|
|
117
|
+
(saved.sentAt !== null && !Number.isFinite(saved.sentAt)))
|
|
118
|
+
throw new Error("invalid journal");
|
|
119
|
+
this.journalBytes = Buffer.byteLength(contents);
|
|
120
|
+
this.journal = saved;
|
|
121
|
+
}
|
|
122
|
+
catch (error) {
|
|
123
|
+
if (error.code !== "ENOENT")
|
|
124
|
+
throw error;
|
|
125
|
+
}
|
|
126
|
+
this.state = "ready";
|
|
127
|
+
this.message = "Waiting for current position";
|
|
128
|
+
this.nextSendAt = this.now() + this.config.sendIntervalSeconds * 1000;
|
|
129
|
+
if (schedule)
|
|
130
|
+
this.schedule();
|
|
131
|
+
}
|
|
132
|
+
catch {
|
|
133
|
+
this.state = "error";
|
|
134
|
+
this.message = "Publisher storage unavailable or belongs to another boat";
|
|
135
|
+
}
|
|
136
|
+
this.emit(this.status());
|
|
137
|
+
}
|
|
138
|
+
observe(event) {
|
|
139
|
+
if (!this.config.enabled ||
|
|
140
|
+
this.stopped ||
|
|
141
|
+
this.state === "disabled" ||
|
|
142
|
+
this.state === "error" ||
|
|
143
|
+
event.context !== "vessels.self")
|
|
144
|
+
return;
|
|
145
|
+
if (event.path !== "navigation.position" &&
|
|
146
|
+
event.path !== "navigation.speedOverGround")
|
|
147
|
+
return;
|
|
148
|
+
if (this.outstanding >= 32) {
|
|
149
|
+
this.dropped++;
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
this.outstanding++;
|
|
153
|
+
this.tail = this.tail
|
|
154
|
+
.then(async () => {
|
|
155
|
+
if (this.stopped || this.state === "error")
|
|
156
|
+
return;
|
|
157
|
+
const time = event.source_timestamp === null
|
|
158
|
+
? NaN
|
|
159
|
+
: Date.parse(event.source_timestamp);
|
|
160
|
+
if (!Number.isFinite(time) ||
|
|
161
|
+
time > this.now() + 5000 ||
|
|
162
|
+
time < this.now() - 120_000)
|
|
163
|
+
return;
|
|
164
|
+
if (this.config.source && event.source !== this.config.source)
|
|
165
|
+
return;
|
|
166
|
+
if (event.path === "navigation.speedOverGround") {
|
|
167
|
+
if (typeof event.value === "number" && event.value >= 0)
|
|
168
|
+
this.speed = { value: event.value, at: time, source: event.source };
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
const position = event.value;
|
|
172
|
+
const point = {
|
|
173
|
+
...position,
|
|
174
|
+
timestamp: time,
|
|
175
|
+
source: event.source,
|
|
176
|
+
};
|
|
177
|
+
if (!validPoint(point))
|
|
178
|
+
return;
|
|
179
|
+
if (this.selectedSource !== null &&
|
|
180
|
+
event.source !== this.selectedSource &&
|
|
181
|
+
this.now() - this.sourceSeenAt < 300_000)
|
|
182
|
+
return;
|
|
183
|
+
this.selectedSource = event.source;
|
|
184
|
+
this.sourceSeenAt = this.now();
|
|
185
|
+
const last = this.journal.last;
|
|
186
|
+
if (last && time <= last.timestamp)
|
|
187
|
+
return;
|
|
188
|
+
const distance = last ? (0, geometry_1.distanceMeters)(last, point) : 0;
|
|
189
|
+
if (last &&
|
|
190
|
+
distance / ((time - last.timestamp) / 1000) >
|
|
191
|
+
this.config.maximumVelocity)
|
|
192
|
+
return;
|
|
193
|
+
if (last && time - last.timestamp < this.config.sampleSeconds * 1000)
|
|
194
|
+
return;
|
|
195
|
+
const heartbeat = this.config.keepalive &&
|
|
196
|
+
last !== null &&
|
|
197
|
+
time - last.timestamp >= 86_400_000;
|
|
198
|
+
if (!heartbeat) {
|
|
199
|
+
if (last && distance < this.config.minimumMoveMeters)
|
|
200
|
+
return;
|
|
201
|
+
if (this.config.minimumSpeedKnots > 0 &&
|
|
202
|
+
(!this.speed ||
|
|
203
|
+
this.speed.source !== event.source ||
|
|
204
|
+
Math.abs(time - this.speed.at) > 120_000 ||
|
|
205
|
+
this.speed.value * 1.943844 < this.config.minimumSpeedKnots))
|
|
206
|
+
return;
|
|
207
|
+
}
|
|
208
|
+
if (this.journal.pending.length >= MAX_POINTS) {
|
|
209
|
+
this.dropped++;
|
|
210
|
+
this.message = "Offline queue full - new points omitted";
|
|
211
|
+
this.emit(this.status());
|
|
212
|
+
return;
|
|
213
|
+
}
|
|
214
|
+
const updated = {
|
|
215
|
+
...this.journal,
|
|
216
|
+
pending: [...this.journal.pending, point],
|
|
217
|
+
last: point,
|
|
218
|
+
};
|
|
219
|
+
await this.save(updated);
|
|
220
|
+
this.journal = updated;
|
|
221
|
+
this.emit(this.status());
|
|
222
|
+
})
|
|
223
|
+
.catch(() => {
|
|
224
|
+
this.state = "error";
|
|
225
|
+
this.message = "Publisher storage failed; pending track retained";
|
|
226
|
+
this.emit(this.status());
|
|
227
|
+
})
|
|
228
|
+
.finally(() => {
|
|
229
|
+
this.outstanding--;
|
|
230
|
+
});
|
|
231
|
+
}
|
|
232
|
+
async idle() {
|
|
233
|
+
await this.tail;
|
|
234
|
+
}
|
|
235
|
+
async flush() {
|
|
236
|
+
if (this.flight)
|
|
237
|
+
return this.flight;
|
|
238
|
+
this.flight = this.send().finally(() => {
|
|
239
|
+
this.flight = null;
|
|
240
|
+
});
|
|
241
|
+
return this.flight;
|
|
242
|
+
}
|
|
243
|
+
async send() {
|
|
244
|
+
await this.tail;
|
|
245
|
+
if (this.stopped ||
|
|
246
|
+
!this.config.enabled ||
|
|
247
|
+
this.state === "error" ||
|
|
248
|
+
this.state === "disabled" ||
|
|
249
|
+
!this.journal.pending.length ||
|
|
250
|
+
this.now() < this.nextSendAt)
|
|
251
|
+
return;
|
|
252
|
+
if (!this.config.sendWhileMoving &&
|
|
253
|
+
this.journal.last &&
|
|
254
|
+
this.now() - this.journal.last.timestamp <
|
|
255
|
+
this.config.sampleSeconds * 2000)
|
|
256
|
+
return;
|
|
257
|
+
const batch = this.journal.pending.slice(0, 500);
|
|
258
|
+
this.state = "sending";
|
|
259
|
+
this.message = "Sending track";
|
|
260
|
+
this.emit(this.status());
|
|
261
|
+
this.controller = new AbortController();
|
|
262
|
+
const timeout = setTimeout(() => this.controller?.abort(), 30_000);
|
|
263
|
+
try {
|
|
264
|
+
const response = await this.request(ENDPOINT, {
|
|
265
|
+
method: "POST",
|
|
266
|
+
redirect: "error",
|
|
267
|
+
signal: this.controller.signal,
|
|
268
|
+
headers: {
|
|
269
|
+
"X-NFL-API-Key": this.config.applicationKey,
|
|
270
|
+
"Accept-Encoding": "identity",
|
|
271
|
+
},
|
|
272
|
+
body: new URLSearchParams({
|
|
273
|
+
timestamp: String(batch[batch.length - 1].timestamp),
|
|
274
|
+
track: JSON.stringify(batch.map((p) => [p.timestamp, p.latitude, p.longitude])),
|
|
275
|
+
boatApiKey: this.config.boatKey,
|
|
276
|
+
pluginVersion: `aurora-blue/${version_1.CONNECTOR_VERSION}`,
|
|
277
|
+
nodeVersion: process.version,
|
|
278
|
+
skVersion: "unknown",
|
|
279
|
+
}),
|
|
280
|
+
});
|
|
281
|
+
if (!response.ok) {
|
|
282
|
+
await response.body?.cancel();
|
|
283
|
+
if (response.status >= 400 &&
|
|
284
|
+
response.status < 500 &&
|
|
285
|
+
response.status !== 429) {
|
|
286
|
+
this.state = "error";
|
|
287
|
+
this.message = `Publication rejected (HTTP ${response.status}); check configuration`;
|
|
288
|
+
return;
|
|
289
|
+
}
|
|
290
|
+
throw new Error("retry");
|
|
291
|
+
}
|
|
292
|
+
// Require an explicit acknowledgement. An ambiguous response retains data;
|
|
293
|
+
// exactly-once delivery requires a provider-supported idempotency contract.
|
|
294
|
+
const reader = response.body?.getReader();
|
|
295
|
+
if (!reader)
|
|
296
|
+
throw new Error("missing response");
|
|
297
|
+
let body = "";
|
|
298
|
+
for (;;) {
|
|
299
|
+
const chunk = await reader.read();
|
|
300
|
+
if (chunk.done)
|
|
301
|
+
break;
|
|
302
|
+
if (body.length + chunk.value.length > 4096) {
|
|
303
|
+
await reader.cancel();
|
|
304
|
+
throw new Error("large response");
|
|
305
|
+
}
|
|
306
|
+
body += new TextDecoder().decode(chunk.value);
|
|
307
|
+
}
|
|
308
|
+
if (JSON.parse(body).status !== "ok") {
|
|
309
|
+
this.state = "error";
|
|
310
|
+
this.message = "Publication rejected by provider; check configuration";
|
|
311
|
+
return;
|
|
312
|
+
}
|
|
313
|
+
const cutoff = batch[batch.length - 1].timestamp;
|
|
314
|
+
// Serialize acknowledgement with writes that arrived during the request.
|
|
315
|
+
this.tail = this.tail.then(async () => {
|
|
316
|
+
const updated = {
|
|
317
|
+
...this.journal,
|
|
318
|
+
pending: this.journal.pending.filter((p) => p.timestamp > cutoff),
|
|
319
|
+
sentAt: this.now(),
|
|
320
|
+
};
|
|
321
|
+
await this.save(updated);
|
|
322
|
+
this.journal = updated;
|
|
323
|
+
});
|
|
324
|
+
try {
|
|
325
|
+
await this.tail;
|
|
326
|
+
}
|
|
327
|
+
catch {
|
|
328
|
+
this.tail = Promise.resolve();
|
|
329
|
+
this.state = "error";
|
|
330
|
+
this.message =
|
|
331
|
+
"Publication acknowledged but checkpoint failed; inspect before retry";
|
|
332
|
+
return;
|
|
333
|
+
}
|
|
334
|
+
this.state = "ready";
|
|
335
|
+
this.message = "Track published";
|
|
336
|
+
}
|
|
337
|
+
catch {
|
|
338
|
+
if (this.state !== "error") {
|
|
339
|
+
this.state = "waiting";
|
|
340
|
+
this.message = "Transfer delayed; pending track retained";
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
finally {
|
|
344
|
+
clearTimeout(timeout);
|
|
345
|
+
this.controller = null;
|
|
346
|
+
this.nextSendAt = this.now() + this.config.sendIntervalSeconds * 1000;
|
|
347
|
+
this.emit(this.status());
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
async save(updated) {
|
|
351
|
+
const capacity = await (0, promises_1.statfs)(this.directory);
|
|
352
|
+
if (capacity.bavail * capacity.bsize < 32 * 1024 * 1024)
|
|
353
|
+
throw new Error("reserve");
|
|
354
|
+
if (this.journalBytes === 0 || this.journalBytes > 2_000_000) {
|
|
355
|
+
const payload = JSON.stringify(updated) + "\n";
|
|
356
|
+
if (Buffer.byteLength(payload) > MAX_FILE_BYTES)
|
|
357
|
+
throw new Error("limit");
|
|
358
|
+
const temporary = `${this.file}.tmp`;
|
|
359
|
+
const handle = await (0, promises_1.open)(temporary, "w", 0o600);
|
|
360
|
+
try {
|
|
361
|
+
await handle.writeFile(payload);
|
|
362
|
+
await handle.sync();
|
|
363
|
+
}
|
|
364
|
+
finally {
|
|
365
|
+
await handle.close();
|
|
366
|
+
}
|
|
367
|
+
await (0, promises_1.rename)(temporary, this.file);
|
|
368
|
+
const directory = await (0, promises_1.open)(this.directory, "r");
|
|
369
|
+
try {
|
|
370
|
+
await directory.sync();
|
|
371
|
+
}
|
|
372
|
+
finally {
|
|
373
|
+
await directory.close();
|
|
374
|
+
}
|
|
375
|
+
this.journalBytes = Buffer.byteLength(payload);
|
|
376
|
+
return;
|
|
377
|
+
}
|
|
378
|
+
const record = updated.last !== this.journal.last
|
|
379
|
+
? { point: updated.last }
|
|
380
|
+
: {
|
|
381
|
+
ack: this.journal.pending[this.journal.pending.length - updated.pending.length - 1]?.timestamp,
|
|
382
|
+
sentAt: updated.sentAt,
|
|
383
|
+
};
|
|
384
|
+
const payload = JSON.stringify(record) + "\n";
|
|
385
|
+
const handle = await (0, promises_1.open)(this.file, "a", 0o600);
|
|
386
|
+
try {
|
|
387
|
+
await handle.writeFile(payload);
|
|
388
|
+
await handle.sync();
|
|
389
|
+
}
|
|
390
|
+
finally {
|
|
391
|
+
await handle.close();
|
|
392
|
+
}
|
|
393
|
+
this.journalBytes += Buffer.byteLength(payload);
|
|
394
|
+
}
|
|
395
|
+
schedule() {
|
|
396
|
+
this.timer = setTimeout(() => {
|
|
397
|
+
void this.flush().finally(() => {
|
|
398
|
+
if (!this.stopped)
|
|
399
|
+
this.schedule();
|
|
400
|
+
});
|
|
401
|
+
}, this.config.sendIntervalSeconds * 1000);
|
|
402
|
+
this.timer.unref();
|
|
403
|
+
}
|
|
404
|
+
async stop() {
|
|
405
|
+
this.stopped = true;
|
|
406
|
+
if (this.timer)
|
|
407
|
+
clearTimeout(this.timer);
|
|
408
|
+
this.controller?.abort();
|
|
409
|
+
await this.flight;
|
|
410
|
+
await this.tail;
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
exports.NoForeignLandPublisher = NoForeignLandPublisher;
|
|
414
|
+
function validPoint(value) {
|
|
415
|
+
if (!value || typeof value !== "object")
|
|
416
|
+
return false;
|
|
417
|
+
const p = value;
|
|
418
|
+
return (Number.isFinite(p.timestamp) &&
|
|
419
|
+
Number.isFinite(p.latitude) &&
|
|
420
|
+
Math.abs(p.latitude) <= 90 &&
|
|
421
|
+
Number.isFinite(p.longitude) &&
|
|
422
|
+
Math.abs(p.longitude) <= 180 &&
|
|
423
|
+
(p.source === null ||
|
|
424
|
+
(typeof p.source === "string" && p.source.length <= 255)));
|
|
425
|
+
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { EventContext } from "./types";
|
|
2
2
|
export type TelemetryScope = "self" | "ais";
|
|
3
|
-
export type ValueKind = "number" | "position" | "string" | "aisShipType";
|
|
3
|
+
export type ValueKind = "number" | "position" | "string" | "boolean" | "switch" | "aisShipType" | "json";
|
|
4
4
|
export type ChangeKind = "scalar" | "angle" | "position" | "identity";
|
|
5
5
|
export interface TelemetryDefinition {
|
|
6
6
|
scope: TelemetryScope;
|
|
@@ -13,6 +13,8 @@ export interface TelemetryDefinition {
|
|
|
13
13
|
}
|
|
14
14
|
export declare const SELF_TELEMETRY: readonly TelemetryDefinition[];
|
|
15
15
|
export declare const AIS_TELEMETRY: readonly TelemetryDefinition[];
|
|
16
|
+
export declare const NAVIGATION_SUBSCRIPTION_PATHS: readonly ["navigation.*", "environment.wind.*", "environment.depth.*", "environment.current", "environment.current.*", "environment.tide.*", "environment.outside.*", "environment.water.*", "environment.heave", "performance.*", "steering.*"];
|
|
17
|
+
export declare const isNavigationPath: (path: string) => boolean;
|
|
16
18
|
export declare const SELF_SUBSCRIPTION_PATHS: string[];
|
|
17
19
|
export declare const AIS_SUBSCRIPTION_PATHS: string[];
|
|
18
20
|
export declare const isAisContext: (context: string) => context is EventContext;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.telemetryDefinition = exports.isAisContext = exports.AIS_SUBSCRIPTION_PATHS = exports.SELF_SUBSCRIPTION_PATHS = exports.AIS_TELEMETRY = exports.SELF_TELEMETRY = void 0;
|
|
3
|
+
exports.telemetryDefinition = exports.isAisContext = exports.AIS_SUBSCRIPTION_PATHS = exports.SELF_SUBSCRIPTION_PATHS = exports.isNavigationPath = exports.NAVIGATION_SUBSCRIPTION_PATHS = exports.AIS_TELEMETRY = exports.SELF_TELEMETRY = void 0;
|
|
4
4
|
const degrees = (value) => (value * Math.PI) / 180;
|
|
5
5
|
const self = (path, valueKind, changeKind, minimumIntervalMs, maximumHeartbeatMs, changeThreshold) => ({
|
|
6
6
|
scope: "self",
|
|
@@ -72,7 +72,27 @@ exports.AIS_TELEMETRY = [
|
|
|
72
72
|
ais("communication.callsignVhf", "string", "identity", 60_000, 21_600_000, 0),
|
|
73
73
|
ais("design.aisShipType", "aisShipType", "identity", 60_000, 21_600_000, 0),
|
|
74
74
|
];
|
|
75
|
-
exports.
|
|
75
|
+
exports.NAVIGATION_SUBSCRIPTION_PATHS = [
|
|
76
|
+
"navigation.*",
|
|
77
|
+
"environment.wind.*",
|
|
78
|
+
"environment.depth.*",
|
|
79
|
+
"environment.current",
|
|
80
|
+
"environment.current.*",
|
|
81
|
+
"environment.tide.*",
|
|
82
|
+
"environment.outside.*",
|
|
83
|
+
"environment.water.*",
|
|
84
|
+
"environment.heave",
|
|
85
|
+
"performance.*",
|
|
86
|
+
"steering.*",
|
|
87
|
+
];
|
|
88
|
+
const isNavigationPath = (path) => path.length <= 255 &&
|
|
89
|
+
(/^(?:navigation|performance|steering|environment\.(?:wind|depth|current|tide|outside|water))(?:\.[A-Za-z0-9_:-]+)+$/.test(path) ||
|
|
90
|
+
path === "environment.current" ||
|
|
91
|
+
path === "environment.heave");
|
|
92
|
+
exports.isNavigationPath = isNavigationPath;
|
|
93
|
+
exports.SELF_SUBSCRIPTION_PATHS = exports.SELF_TELEMETRY.filter((definition) => !(0, exports.isNavigationPath)(definition.path))
|
|
94
|
+
.map((definition) => definition.path)
|
|
95
|
+
.concat(...exports.NAVIGATION_SUBSCRIPTION_PATHS, "electrical.switches.*");
|
|
76
96
|
exports.AIS_SUBSCRIPTION_PATHS = exports.AIS_TELEMETRY.map((definition) => definition.path);
|
|
77
97
|
const matchesPath = (pattern, path) => {
|
|
78
98
|
const patternParts = pattern.split(".");
|
|
@@ -83,11 +103,20 @@ const matchesPath = (pattern, path) => {
|
|
|
83
103
|
const isAisContext = (context) => /^vessels\.(?:urn:mrn:imo:mmsi:)?[0-9]{9}$/.test(context);
|
|
84
104
|
exports.isAisContext = isAisContext;
|
|
85
105
|
const telemetryDefinition = (context, path) => {
|
|
106
|
+
if (context === "vessels.self" &&
|
|
107
|
+
path.startsWith("electrical.switches.") &&
|
|
108
|
+
path.length <= 255 &&
|
|
109
|
+
/^electrical\.switches\.[A-Za-z0-9_:-]+(?:\.[A-Za-z0-9_:-]+)+$/.test(path)) {
|
|
110
|
+
return self(path, "switch", "identity", 0, 60_000, 0);
|
|
111
|
+
}
|
|
86
112
|
const definitions = context === "vessels.self"
|
|
87
113
|
? exports.SELF_TELEMETRY
|
|
88
114
|
: (0, exports.isAisContext)(context)
|
|
89
115
|
? exports.AIS_TELEMETRY
|
|
90
116
|
: [];
|
|
91
|
-
return (definitions.find((definition) => matchesPath(definition.path, path)) ??
|
|
117
|
+
return (definitions.find((definition) => matchesPath(definition.path, path)) ??
|
|
118
|
+
(context === "vessels.self" && (0, exports.isNavigationPath)(path)
|
|
119
|
+
? self(path, "json", "identity", 2_000, 60_000, 0)
|
|
120
|
+
: null));
|
|
92
121
|
};
|
|
93
122
|
exports.telemetryDefinition = telemetryDefinition;
|
|
@@ -51,7 +51,8 @@ const changedEnough = (kind, previous, current, threshold) => {
|
|
|
51
51
|
!("latitude" in current)) {
|
|
52
52
|
return true;
|
|
53
53
|
}
|
|
54
|
-
return (0, geometry_1.distanceMeters)(previous, current) >=
|
|
54
|
+
return ((0, geometry_1.distanceMeters)(previous, current) >=
|
|
55
|
+
threshold);
|
|
55
56
|
}
|
|
56
57
|
if (kind === "identity") {
|
|
57
58
|
return JSON.stringify(previous) !== JSON.stringify(current);
|
package/dist/src/types.d.ts
CHANGED
|
@@ -12,6 +12,7 @@ export interface RuntimeConfiguration {
|
|
|
12
12
|
export type EventContext = "vessels.self" | `vessels.${number}` | `vessels.urn:mrn:imo:mmsi:${number}`;
|
|
13
13
|
export type AllowedPath = string;
|
|
14
14
|
export interface PositionValue {
|
|
15
|
+
altitude?: number;
|
|
15
16
|
latitude: number;
|
|
16
17
|
longitude: number;
|
|
17
18
|
}
|
|
@@ -19,7 +20,7 @@ export interface AisShipTypeValue {
|
|
|
19
20
|
id: number;
|
|
20
21
|
name: string;
|
|
21
22
|
}
|
|
22
|
-
export type EventValue = number | string | PositionValue | AisShipTypeValue;
|
|
23
|
+
export type EventValue = number | string | boolean | PositionValue | AisShipTypeValue | Record<string, unknown> | unknown[];
|
|
23
24
|
export interface EventInput {
|
|
24
25
|
boot_id: string;
|
|
25
26
|
context: EventContext;
|
package/dist/src/version.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export declare const CONNECTOR_VERSION = "0.2.0-dev.
|
|
1
|
+
export declare const CONNECTOR_VERSION = "0.2.0-dev.26";
|
package/dist/src/version.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@aurora-blue/signalk-connector",
|
|
3
|
-
"version": "0.2.0-dev.
|
|
3
|
+
"version": "0.2.0-dev.26",
|
|
4
4
|
"description": "Signal K edge connector for the Aurora Blue ABOARD ingest protocol",
|
|
5
5
|
"private": false,
|
|
6
6
|
"main": "dist/src/index.js",
|
|
@@ -18,7 +18,8 @@
|
|
|
18
18
|
"signalk-category-cloud"
|
|
19
19
|
],
|
|
20
20
|
"signalk": {
|
|
21
|
-
"
|
|
21
|
+
"appIcon": "./favicon.svg",
|
|
22
|
+
"displayName": "Aurora Blue ABOARD SK"
|
|
22
23
|
},
|
|
23
24
|
"signalk-plugin-enabled-by-default": false,
|
|
24
25
|
"engines": {
|
|
@@ -43,6 +44,7 @@
|
|
|
43
44
|
"verify:platforms": "sh tools/verify-platforms.sh",
|
|
44
45
|
"release:build": "node tools/release.mjs build",
|
|
45
46
|
"release:stage": "node tools/release.mjs stage",
|
|
47
|
+
"release:publish": "node tools/release.mjs publish",
|
|
46
48
|
"release:verify": "node tools/release.mjs verify"
|
|
47
49
|
},
|
|
48
50
|
"devDependencies": {
|
package/public/ci-tokens.css
CHANGED
|
@@ -46,6 +46,9 @@
|
|
|
46
46
|
--aboard-danger: #b83245;
|
|
47
47
|
--aboard-scrim-rgb: 15 34 39;
|
|
48
48
|
--aboard-shadow-rgb: 25 51 57;
|
|
49
|
+
--aboard-map-departure: #f0c76e;
|
|
50
|
+
--aboard-map-arrival: #65e0c0;
|
|
51
|
+
--aboard-map-endpoint-ink: #12313c;
|
|
49
52
|
--aboard-map-water: #cfe5ec;
|
|
50
53
|
--aboard-map-land: #e6ebe1;
|
|
51
54
|
--aboard-map-coast: #789197;
|
|
@@ -134,6 +137,9 @@
|
|
|
134
137
|
:root { --aboard-font-title: 1.75rem; }
|
|
135
138
|
}
|
|
136
139
|
|
|
140
|
+
/* Machine-readable codes retain a white quiet zone in every theme. */
|
|
141
|
+
:root { --aboard-qr-background: #fff; --aboard-qr-foreground: #000; }
|
|
142
|
+
|
|
137
143
|
:root { --aboard-chart: url("aurora-blue-archipelago-light.svg"); }
|
|
138
144
|
@media (prefers-color-scheme: dark) { :root:not([data-theme]) { --aboard-chart: url("aurora-blue-archipelago-dark.svg"); } }
|
|
139
145
|
:root[data-theme="dark"] { --aboard-chart: url("aurora-blue-archipelago-dark.svg"); }
|
|
Binary file
|
package/public/favicon.svg
CHANGED
|
@@ -1,7 +1,16 @@
|
|
|
1
1
|
<?xml version="1.0" encoding="UTF-8"?>
|
|
2
|
-
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0
|
|
2
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1024 1024" role="img" aria-labelledby="title">
|
|
3
3
|
<title id="title">Aurora Blue ABOARD</title>
|
|
4
|
-
<
|
|
4
|
+
<defs>
|
|
5
|
+
<linearGradient id="aboard-icon-gradient" x1="100%" y1="0%" x2="0%" y2="100%">
|
|
6
|
+
<stop offset="0%" stop-color="#279fa8"/>
|
|
7
|
+
<stop offset="52%" stop-color="#167f88"/>
|
|
8
|
+
<stop offset="100%" stop-color="#123e56"/>
|
|
9
|
+
</linearGradient>
|
|
10
|
+
</defs>
|
|
11
|
+
<rect width="1024" height="1024" fill="url(#aboard-icon-gradient)"/>
|
|
12
|
+
<g transform="translate(13.319897172236494 13.319897172236494) scale(1.687580720229318)">
|
|
13
|
+
<g fill="#ffffff" transform="matrix(1,0,0,1,-889,-2810)">
|
|
5
14
|
<g transform="matrix(1,0,0,1,-637,622)">
|
|
6
15
|
<g transform="matrix(2.572721,0,0,2.572721,1472.604933,2173.221564)">
|
|
7
16
|
<g transform="matrix(0.720795,0,0,0.720795,199.370511,207.270387)">
|
|
@@ -16,4 +25,6 @@
|
|
|
16
25
|
</g>
|
|
17
26
|
</g>
|
|
18
27
|
</g>
|
|
28
|
+
|
|
29
|
+
</g>
|
|
19
30
|
</svg>
|
package/public/index.html
CHANGED
|
@@ -4,8 +4,9 @@
|
|
|
4
4
|
<meta charset="utf-8" />
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
6
6
|
<meta name="color-scheme" content="dark light" />
|
|
7
|
-
<title data-i18n="page.title">Aurora Blue ABOARD
|
|
8
|
-
<link rel="icon" href="favicon.
|
|
7
|
+
<title data-i18n="page.title">Aurora Blue ABOARD SK</title>
|
|
8
|
+
<link rel="icon" href="favicon.ico?v=6" type="image/x-icon" />
|
|
9
|
+
<link rel="icon" href="favicon.svg?v=6" type="image/svg+xml" />
|
|
9
10
|
<link rel="stylesheet" href="ci-fonts.css" />
|
|
10
11
|
<link rel="stylesheet" href="ci-tokens.css" />
|
|
11
12
|
<link rel="stylesheet" href="ci-brand.css?v=5" />
|
|
@@ -16,7 +17,7 @@
|
|
|
16
17
|
<header class="brand-header">
|
|
17
18
|
<div
|
|
18
19
|
class="aurora-blue-brand"
|
|
19
|
-
aria-label="Aurora Blue ABOARD
|
|
20
|
+
aria-label="Aurora Blue ABOARD SK"
|
|
20
21
|
data-i18n-aria-label="brand.label"
|
|
21
22
|
style="
|
|
22
23
|
--aurora-blue-mark-url: url("aurora-blue-aboard-mark.svg");
|
package/public/locales/de.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
|
-
"page.title": "Aurora Blue ABOARD
|
|
2
|
+
"page.title": "Aurora Blue ABOARD SK",
|
|
3
3
|
"brand.context": "Signal K Connector",
|
|
4
|
-
"brand.label": "Aurora Blue ABOARD
|
|
4
|
+
"brand.label": "Aurora Blue ABOARD SK",
|
|
5
5
|
"appearance.system": "Systemdarstellung: {appearance}",
|
|
6
6
|
"appearance.dark": "dunkel",
|
|
7
7
|
"appearance.light": "hell",
|
package/public/locales/en.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
|
-
"page.title": "Aurora Blue ABOARD
|
|
2
|
+
"page.title": "Aurora Blue ABOARD SK",
|
|
3
3
|
"brand.context": "Signal K Connector",
|
|
4
|
-
"brand.label": "Aurora Blue ABOARD
|
|
4
|
+
"brand.label": "Aurora Blue ABOARD SK",
|
|
5
5
|
"appearance.system": "System appearance: {appearance}",
|
|
6
6
|
"appearance.dark": "dark",
|
|
7
7
|
"appearance.light": "light",
|
package/public/locales/es.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
|
-
"page.title": "
|
|
2
|
+
"page.title": "Aurora Blue ABOARD SK",
|
|
3
3
|
"brand.context": "Conector Signal K",
|
|
4
|
-
"brand.label": "Aurora Blue ABOARD
|
|
4
|
+
"brand.label": "Aurora Blue ABOARD SK",
|
|
5
5
|
"appearance.system": "Apariencia del sistema: {appearance}",
|
|
6
6
|
"appearance.dark": "oscura",
|
|
7
7
|
"appearance.light": "clara",
|
package/public/locales/fr.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
|
-
"page.title": "
|
|
2
|
+
"page.title": "Aurora Blue ABOARD SK",
|
|
3
3
|
"brand.context": "Connecteur Signal K",
|
|
4
|
-
"brand.label": "Aurora Blue ABOARD
|
|
4
|
+
"brand.label": "Aurora Blue ABOARD SK",
|
|
5
5
|
"appearance.system": "Apparence du système : {appearance}",
|
|
6
6
|
"appearance.dark": "sombre",
|
|
7
7
|
"appearance.light": "claire",
|