@aurora-blue/signalk-connector 0.2.0-dev.20

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.
Files changed (52) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +122 -0
  3. package/THIRD_PARTY_NOTICES.md +11 -0
  4. package/VERSION +1 -0
  5. package/dist/src/backoff.d.ts +1 -0
  6. package/dist/src/backoff.js +9 -0
  7. package/dist/src/config.d.ts +192 -0
  8. package/dist/src/config.js +317 -0
  9. package/dist/src/connector.d.ts +69 -0
  10. package/dist/src/connector.js +487 -0
  11. package/dist/src/delta.d.ts +4 -0
  12. package/dist/src/delta.js +137 -0
  13. package/dist/src/geometry.d.ts +3 -0
  14. package/dist/src/geometry.js +25 -0
  15. package/dist/src/http-client.d.ts +22 -0
  16. package/dist/src/http-client.js +158 -0
  17. package/dist/src/index.d.ts +3 -0
  18. package/dist/src/index.js +93 -0
  19. package/dist/src/safety/anchor-alarm.d.ts +33 -0
  20. package/dist/src/safety/anchor-alarm.js +79 -0
  21. package/dist/src/state-store.d.ts +45 -0
  22. package/dist/src/state-store.js +491 -0
  23. package/dist/src/storage-budget.d.ts +35 -0
  24. package/dist/src/storage-budget.js +88 -0
  25. package/dist/src/telemetry-catalog.d.ts +19 -0
  26. package/dist/src/telemetry-catalog.js +93 -0
  27. package/dist/src/telemetry-reducer.d.ts +22 -0
  28. package/dist/src/telemetry-reducer.js +150 -0
  29. package/dist/src/types.d.ts +115 -0
  30. package/dist/src/types.js +5 -0
  31. package/dist/src/version.d.ts +1 -0
  32. package/dist/src/version.js +4 -0
  33. package/package.json +60 -0
  34. package/public/aurora-blue-aboard-mark.svg +19 -0
  35. package/public/aurora-blue-archipelago-dark.svg +32 -0
  36. package/public/aurora-blue-archipelago-light.svg +81 -0
  37. package/public/ci-brand.css +77 -0
  38. package/public/ci-fonts.css +28 -0
  39. package/public/ci-tokens.css +139 -0
  40. package/public/favicon.svg +19 -0
  41. package/public/fonts/LICENSE +96 -0
  42. package/public/fonts/ubuntu-latin-300-normal.woff2 +0 -0
  43. package/public/fonts/ubuntu-latin-400-normal.woff2 +0 -0
  44. package/public/fonts/ubuntu-latin-500-normal.woff2 +0 -0
  45. package/public/fonts/ubuntu-latin-700-normal.woff2 +0 -0
  46. package/public/index.html +177 -0
  47. package/public/locales/de.json +82 -0
  48. package/public/locales/en.json +82 -0
  49. package/public/locales/es.json +82 -0
  50. package/public/locales/fr.json +82 -0
  51. package/public/status.css +262 -0
  52. package/public/status.js +237 -0
@@ -0,0 +1,158 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.AuroraBlueHttpClient = exports.AuroraBlueHttpError = void 0;
4
+ const node_zlib_1 = require("node:zlib");
5
+ const version_1 = require("./version");
6
+ const types_1 = require("./types");
7
+ const defaultRuntimeConfiguration = () => ({
8
+ version: 0,
9
+ operating_mode: "active",
10
+ poll_after_seconds: 60,
11
+ });
12
+ const runtimeConfigurationFrom = (value) => {
13
+ if (value === undefined) {
14
+ return defaultRuntimeConfiguration();
15
+ }
16
+ const configuration = value;
17
+ if (typeof configuration.version !== "number" ||
18
+ !Number.isInteger(configuration.version) ||
19
+ configuration.version < 1 ||
20
+ !["active", "standby", "disabled"].includes(String(configuration.operating_mode)) ||
21
+ typeof configuration.poll_after_seconds !== "number" ||
22
+ !Number.isInteger(configuration.poll_after_seconds) ||
23
+ configuration.poll_after_seconds < 10 ||
24
+ configuration.poll_after_seconds > 3600) {
25
+ throw new Error("Aurora Blue runtime configuration is invalid.");
26
+ }
27
+ return configuration;
28
+ };
29
+ class AuroraBlueHttpError extends Error {
30
+ status;
31
+ constructor(status, message) {
32
+ super(message);
33
+ this.status = status;
34
+ this.name = "AuroraBlueHttpError";
35
+ }
36
+ }
37
+ exports.AuroraBlueHttpError = AuroraBlueHttpError;
38
+ class AuroraBlueHttpClient {
39
+ baseUrl;
40
+ timeoutMs;
41
+ fetchImplementation;
42
+ compressionThresholdBytes;
43
+ constructor(baseUrl, timeoutMs, fetchImplementation = fetch, compressionThresholdBytes = 1_024) {
44
+ this.baseUrl = baseUrl;
45
+ this.timeoutMs = timeoutMs;
46
+ this.fetchImplementation = fetchImplementation;
47
+ this.compressionThresholdBytes = compressionThresholdBytes;
48
+ }
49
+ async pair(pairingCode, deviceName, connectorVersion) {
50
+ const response = await this.request("/api/v1/pairings/exchange", {
51
+ method: "POST",
52
+ body: JSON.stringify({
53
+ pairing_code: pairingCode,
54
+ device_name: deviceName,
55
+ connector_version: connectorVersion,
56
+ }),
57
+ });
58
+ if (response.protocol_version !== types_1.PROTOCOL_VERSION ||
59
+ typeof response.device_id !== "string" ||
60
+ response.token_type !== "Bearer" ||
61
+ typeof response.access_token !== "string" ||
62
+ response.access_token === "") {
63
+ throw new Error("Aurora Blue pairing response is invalid.");
64
+ }
65
+ return {
66
+ ...response,
67
+ runtime_configuration: runtimeConfigurationFrom(response.runtime_configuration),
68
+ };
69
+ }
70
+ async sendBatch(batch, accessToken) {
71
+ const json = JSON.stringify(batch);
72
+ const compress = this.compressionThresholdBytes !== null &&
73
+ Buffer.byteLength(json) >= this.compressionThresholdBytes;
74
+ const response = await this.request("/api/v1/ingest/batches", {
75
+ method: "POST",
76
+ headers: {
77
+ Authorization: `Bearer ${accessToken}`,
78
+ ...(compress ? { "Content-Encoding": "gzip" } : {}),
79
+ },
80
+ body: compress ? new Uint8Array((0, node_zlib_1.gzipSync)(json)) : json,
81
+ });
82
+ if (typeof response.accepted_through !== "number" ||
83
+ !Number.isInteger(response.accepted_through) ||
84
+ typeof response.server_time !== "string" ||
85
+ Number.isNaN(Date.parse(response.server_time))) {
86
+ throw new Error("Aurora Blue ingest acknowledgement is invalid.");
87
+ }
88
+ return {
89
+ ...response,
90
+ runtime_configuration: runtimeConfigurationFrom(response.runtime_configuration),
91
+ };
92
+ }
93
+ async fetchRuntimeConfiguration(accessToken) {
94
+ const response = await this.request("/api/v1/devices/self/runtime-configuration", {
95
+ method: "GET",
96
+ headers: { Authorization: `Bearer ${accessToken}` },
97
+ });
98
+ if (typeof response.server_time !== "string" ||
99
+ Number.isNaN(Date.parse(response.server_time))) {
100
+ throw new Error("Aurora Blue runtime configuration response is invalid.");
101
+ }
102
+ return {
103
+ runtime_configuration: runtimeConfigurationFrom(response.runtime_configuration),
104
+ server_time: response.server_time,
105
+ };
106
+ }
107
+ async request(path, init) {
108
+ const abort = new AbortController();
109
+ const timeout = setTimeout(() => abort.abort(), this.timeoutMs);
110
+ try {
111
+ const response = await this.fetchImplementation(new URL(path, `${this.baseUrl}/`), {
112
+ ...init,
113
+ signal: abort.signal,
114
+ headers: {
115
+ Accept: "application/json",
116
+ "Content-Type": "application/json",
117
+ "X-Aurora-Blue-Connector-Version": version_1.CONNECTOR_VERSION,
118
+ ...init.headers,
119
+ },
120
+ });
121
+ const body = await this.readJson(response);
122
+ if (!response.ok) {
123
+ throw new AuroraBlueHttpError(response.status, `Aurora Blue request failed with HTTP ${response.status}.`);
124
+ }
125
+ return body;
126
+ }
127
+ catch (error) {
128
+ if (error instanceof AuroraBlueHttpError) {
129
+ throw error;
130
+ }
131
+ if (error instanceof Error && error.name === "AbortError") {
132
+ throw new Error("Aurora Blue request timed out.");
133
+ }
134
+ throw new Error("Aurora Blue target is unavailable.", { cause: error });
135
+ }
136
+ finally {
137
+ clearTimeout(timeout);
138
+ }
139
+ }
140
+ async readJson(response) {
141
+ const text = await response.text();
142
+ if (text === "") {
143
+ return {};
144
+ }
145
+ try {
146
+ const parsed = JSON.parse(text);
147
+ return parsed !== null &&
148
+ typeof parsed === "object" &&
149
+ !Array.isArray(parsed)
150
+ ? parsed
151
+ : {};
152
+ }
153
+ catch {
154
+ return {};
155
+ }
156
+ }
157
+ }
158
+ exports.AuroraBlueHttpClient = AuroraBlueHttpClient;
@@ -0,0 +1,3 @@
1
+ import type { Plugin, ServerAPI } from "@signalk/server-api";
2
+ declare const createPlugin: (app: ServerAPI) => Plugin;
3
+ export = createPlugin;
@@ -0,0 +1,93 @@
1
+ "use strict";
2
+ const config_1 = require("./config");
3
+ const connector_1 = require("./connector");
4
+ const state_store_1 = require("./state-store");
5
+ const createPlugin = (app) => {
6
+ let runtime = null;
7
+ let startup = null;
8
+ let lifecycle = "stopped";
9
+ let lastError = null;
10
+ const recordError = (message) => {
11
+ lifecycle = "error";
12
+ lastError = {
13
+ message,
14
+ occurred_at: new Date().toISOString(),
15
+ };
16
+ };
17
+ const statusResponse = async () => {
18
+ let status = null;
19
+ if (runtime !== null) {
20
+ const { deviceId, ...publicStatus } = await runtime.status();
21
+ void deviceId;
22
+ status = publicStatus;
23
+ }
24
+ return {
25
+ connector_version: connector_1.CONNECTOR_VERSION,
26
+ lifecycle,
27
+ generated_at: new Date().toISOString(),
28
+ last_error: lastError,
29
+ status,
30
+ };
31
+ };
32
+ return {
33
+ id: "aurora-blue-signalk-connector",
34
+ name: "Aurora Blue ABOARD Connector",
35
+ description: "Buffers selected Signal K data and transfers it to Aurora Blue ABOARD.",
36
+ schema: config_1.pluginSchema,
37
+ uiSchema: config_1.pluginUiSchema,
38
+ registerWithRouter: (router) => {
39
+ const compatibleRouter = router;
40
+ compatibleRouter.get("/status", (_request, response) => {
41
+ void statusResponse()
42
+ .then((status) => response.json(status))
43
+ .catch(() => response.status(500).json({
44
+ message: "Aurora Blue ABOARD connector status is unavailable.",
45
+ }));
46
+ });
47
+ },
48
+ start: (input) => {
49
+ lifecycle = "starting";
50
+ lastError = null;
51
+ try {
52
+ const config = (0, config_1.resolveConfig)(input);
53
+ const store = new state_store_1.StateStore(app.getDataDirPath(), {
54
+ segmentMaximumBytes: config.bufferSegmentBytes,
55
+ });
56
+ runtime = new connector_1.ConnectorRuntime(app, store, config);
57
+ startup = runtime.start().then(() => {
58
+ lifecycle = "running";
59
+ if (config.pairingCode === undefined) {
60
+ return;
61
+ }
62
+ const sanitized = { ...input };
63
+ delete sanitized.pairingCode;
64
+ app.savePluginOptions(sanitized, (error) => {
65
+ if (error !== null) {
66
+ app.error("Paired successfully, but the short-lived pairing code could not be cleared.");
67
+ }
68
+ });
69
+ });
70
+ void startup.catch((error) => {
71
+ recordError("Connector startup failed.");
72
+ app.setPluginError(error instanceof Error
73
+ ? error.message
74
+ : "Connector startup failed.");
75
+ });
76
+ }
77
+ catch (error) {
78
+ recordError("Connector configuration is invalid.");
79
+ app.setPluginError(error instanceof Error
80
+ ? error.message
81
+ : "Connector configuration is invalid.");
82
+ }
83
+ },
84
+ stop: async () => {
85
+ await startup?.catch(() => undefined);
86
+ await runtime?.stop();
87
+ startup = null;
88
+ runtime = null;
89
+ lifecycle = "stopped";
90
+ },
91
+ };
92
+ };
93
+ module.exports = createPlugin;
@@ -0,0 +1,33 @@
1
+ import type { PositionValue } from "../types";
2
+ export type AnchorAlarmState = "inactive" | "armed" | "warning" | "alarm" | "position-lost";
3
+ export interface AnchorAlarmConfig {
4
+ enabled: boolean;
5
+ anchor: PositionValue | null;
6
+ radiusMeters: number;
7
+ hysteresisMeters: number;
8
+ confirmationMs: number;
9
+ stalePositionMs: number;
10
+ }
11
+ export interface AnchorAlarmSnapshot {
12
+ state: AnchorAlarmState;
13
+ changedAt: string;
14
+ distanceMeters: number | null;
15
+ lastPositionAt: string | null;
16
+ }
17
+ export interface AnchorAlarmTransition extends AnchorAlarmSnapshot {
18
+ previousState: AnchorAlarmState;
19
+ }
20
+ export declare class AnchorAlarmEngine {
21
+ private readonly config;
22
+ private state;
23
+ private stateChangedAt;
24
+ private outsideSince;
25
+ private lastPositionAt;
26
+ private lastDistanceMeters;
27
+ constructor(config: AnchorAlarmConfig, startedAt: number);
28
+ observe(position: PositionValue, observedAt: number): AnchorAlarmTransition | null;
29
+ checkStale(now: number): AnchorAlarmTransition | null;
30
+ snapshot(): AnchorAlarmSnapshot;
31
+ private isConfigured;
32
+ private transition;
33
+ }
@@ -0,0 +1,79 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.AnchorAlarmEngine = void 0;
4
+ const geometry_1 = require("../geometry");
5
+ class AnchorAlarmEngine {
6
+ config;
7
+ state;
8
+ stateChangedAt;
9
+ outsideSince = null;
10
+ lastPositionAt = null;
11
+ lastDistanceMeters = null;
12
+ constructor(config, startedAt) {
13
+ this.config = config;
14
+ this.state = this.isConfigured() ? "armed" : "inactive";
15
+ this.stateChangedAt = startedAt;
16
+ }
17
+ observe(position, observedAt) {
18
+ if (!this.isConfigured()) {
19
+ return null;
20
+ }
21
+ this.lastPositionAt = observedAt;
22
+ const anchor = this.config.anchor;
23
+ const distance = (0, geometry_1.distanceMeters)(anchor, position);
24
+ this.lastDistanceMeters = distance;
25
+ if (this.state === "alarm") {
26
+ const clearThreshold = Math.max(0, this.config.radiusMeters - this.config.hysteresisMeters);
27
+ if (distance > clearThreshold) {
28
+ return null;
29
+ }
30
+ this.outsideSince = null;
31
+ return this.transition("armed", observedAt);
32
+ }
33
+ if (distance <= this.config.radiusMeters) {
34
+ this.outsideSince = null;
35
+ return this.transition("armed", observedAt);
36
+ }
37
+ if (this.outsideSince === null) {
38
+ this.outsideSince = observedAt;
39
+ }
40
+ if (observedAt - this.outsideSince >= this.config.confirmationMs) {
41
+ return this.transition("alarm", observedAt);
42
+ }
43
+ return this.transition("warning", observedAt);
44
+ }
45
+ checkStale(now) {
46
+ if (!this.isConfigured()) {
47
+ return null;
48
+ }
49
+ const reference = this.lastPositionAt ?? this.stateChangedAt;
50
+ if (now - reference < this.config.stalePositionMs) {
51
+ return null;
52
+ }
53
+ this.outsideSince = null;
54
+ return this.transition("position-lost", now);
55
+ }
56
+ snapshot() {
57
+ return {
58
+ state: this.state,
59
+ changedAt: new Date(this.stateChangedAt).toISOString(),
60
+ distanceMeters: this.lastDistanceMeters,
61
+ lastPositionAt: this.lastPositionAt === null
62
+ ? null
63
+ : new Date(this.lastPositionAt).toISOString(),
64
+ };
65
+ }
66
+ isConfigured() {
67
+ return this.config.enabled && this.config.anchor !== null;
68
+ }
69
+ transition(next, changedAt) {
70
+ if (next === this.state) {
71
+ return null;
72
+ }
73
+ const previousState = this.state;
74
+ this.state = next;
75
+ this.stateChangedAt = changedAt;
76
+ return { previousState, ...this.snapshot() };
77
+ }
78
+ }
79
+ exports.AnchorAlarmEngine = AnchorAlarmEngine;
@@ -0,0 +1,45 @@
1
+ import { type BufferedEvent, type EventInput, type IngestBatch, type PairingResponse, type OperatingMode, type PersistedMetadata, type PersistedState } from "./types";
2
+ import type { FilesystemCapacity, JournalMetrics } from "./storage-budget";
3
+ export interface StateStoreOptions {
4
+ now?: () => string;
5
+ uuid?: () => string;
6
+ segmentMaximumBytes?: number;
7
+ filesystemCapacity?: () => Promise<FilesystemCapacity | null>;
8
+ }
9
+ export declare class StateStore {
10
+ private readonly directory;
11
+ private readonly metadataPath;
12
+ private readonly now;
13
+ private readonly uuid;
14
+ private readonly segmentMaximumBytes;
15
+ private readonly capacityProvider;
16
+ private state;
17
+ private segments;
18
+ private journalBytes;
19
+ private tail;
20
+ constructor(directory: string, options?: StateStoreOptions);
21
+ load(): Promise<void>;
22
+ snapshot(): Promise<PersistedState>;
23
+ summary(): Promise<Omit<PersistedMetadata, "pending_batch"> & {
24
+ bufferedEvents: number;
25
+ }>;
26
+ journalMetrics(): Promise<JournalMetrics>;
27
+ filesystemCapacity(): Promise<FilesystemCapacity | null>;
28
+ setPairing(pairing: PairingResponse): Promise<void>;
29
+ setOperatingMode(operatingMode: OperatingMode): Promise<void>;
30
+ append(event: EventInput, eventLimit: number, byteLimit?: number): Promise<BufferedEvent | null>;
31
+ pendingBatch(maximumEvents: number): Promise<IngestBatch | null>;
32
+ acknowledge(acceptedThrough: number): Promise<void>;
33
+ private serialized;
34
+ private appendRecord;
35
+ private readMetadata;
36
+ private readJournal;
37
+ private readSegment;
38
+ private removeAcknowledgedSegments;
39
+ private migrateLegacy;
40
+ private preserveLegacyBackup;
41
+ private removeStaleTemporaryFiles;
42
+ private persistMetadata;
43
+ private exists;
44
+ private readFilesystemCapacity;
45
+ }