signalk-masterbus 0.1.0

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 (45) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +128 -0
  3. package/bin/darwin-arm64/masterbus-signalk +0 -0
  4. package/bin/darwin-x64/masterbus-signalk +0 -0
  5. package/bin/linux-arm/masterbus-signalk +0 -0
  6. package/bin/linux-arm64/masterbus-signalk +0 -0
  7. package/bin/linux-x64/masterbus-signalk +0 -0
  8. package/bin/win32-x64/masterbus-signalk.exe +0 -0
  9. package/package.json +82 -0
  10. package/plugin/client.d.ts +39 -0
  11. package/plugin/client.d.ts.map +1 -0
  12. package/plugin/client.js +93 -0
  13. package/plugin/client.js.map +1 -0
  14. package/plugin/config.d.ts +146 -0
  15. package/plugin/config.d.ts.map +1 -0
  16. package/plugin/config.js +143 -0
  17. package/plugin/config.js.map +1 -0
  18. package/plugin/daemon.d.ts +63 -0
  19. package/plugin/daemon.d.ts.map +1 -0
  20. package/plugin/daemon.js +223 -0
  21. package/plugin/daemon.js.map +1 -0
  22. package/plugin/index.d.ts +16 -0
  23. package/plugin/index.d.ts.map +1 -0
  24. package/plugin/index.js +199 -0
  25. package/plugin/index.js.map +1 -0
  26. package/plugin/put-bridge.d.ts +47 -0
  27. package/plugin/put-bridge.d.ts.map +1 -0
  28. package/plugin/put-bridge.js +116 -0
  29. package/plugin/put-bridge.js.map +1 -0
  30. package/plugin/router.d.ts +20 -0
  31. package/plugin/router.d.ts.map +1 -0
  32. package/plugin/router.js +38 -0
  33. package/plugin/router.js.map +1 -0
  34. package/plugin/stream-forwarder.d.ts +54 -0
  35. package/plugin/stream-forwarder.d.ts.map +1 -0
  36. package/plugin/stream-forwarder.js +139 -0
  37. package/plugin/stream-forwarder.js.map +1 -0
  38. package/plugin/types.d.ts +136 -0
  39. package/plugin/types.d.ts.map +1 -0
  40. package/plugin/types.js +19 -0
  41. package/plugin/types.js.map +1 -0
  42. package/public/assets/icon.svg +6 -0
  43. package/public/assets/index-D5ZZgo9o.js +49 -0
  44. package/public/assets/index-rkv4IC1b.css +1 -0
  45. package/public/index.html +14 -0
@@ -0,0 +1,199 @@
1
+ import path from 'node:path';
2
+ import { DaemonClient } from './client.js';
3
+ import { SCHEMA, UI_SCHEMA, withDefaults } from './config.js';
4
+ import { DaemonManager, errMsg, resolveBinary } from './daemon.js';
5
+ import { PutBridge } from './put-bridge.js';
6
+ import { registerRoutes } from './router.js';
7
+ import { StreamForwarder } from './stream-forwarder.js';
8
+ export const PLUGIN_ID = 'signalk-masterbus';
9
+ /** How often the daemon is asked for its status and the mapping re-synced. */
10
+ const POLL_MS = 10_000;
11
+ /**
12
+ * Mastervolt MasterBus for Signal K.
13
+ *
14
+ * The plugin is a thin layer over `masterbus-signalk`, the daemon from the
15
+ * masterbus project. In *bundled* mode the plugin starts the daemon on this
16
+ * machine and keeps its config, mapping and schema cache under the plugin's
17
+ * data directory; in *external* mode it connects to one running elsewhere.
18
+ * Either way it speaks the same two things to it: the delta stream, which is
19
+ * republished through `handleMessage`, and the control API, which the
20
+ * mapping editor (the webapp under `public/`) reaches through this plugin's
21
+ * router and which turns Signal K PUTs into field writes.
22
+ */
23
+ export default function plugin(app) {
24
+ let config = null;
25
+ let daemon = null;
26
+ let client = null;
27
+ let forwarder = null;
28
+ let bridge = null;
29
+ let poller = null;
30
+ let lastStatus = null;
31
+ let lastError = null;
32
+ let mappingSeen = '';
33
+ // Bumped on every start/stop so a start still in flight when stop() runs
34
+ // notices and does not resurrect anything.
35
+ let generation = 0;
36
+ const summary = () => ({
37
+ mode: config?.mode ?? null,
38
+ connected: client !== null && lastStatus !== null,
39
+ daemon: daemon ? { running: daemon.running, recentLog: daemon.recentLog } : null,
40
+ status: lastStatus,
41
+ error: lastError,
42
+ stream: forwarder ? { connected: forwarder.connected, ...forwarder.counts } : null,
43
+ putPaths: bridge?.paths ?? []
44
+ });
45
+ const statusLine = (s) => {
46
+ const where = config?.mode === 'bundled' ? 'Bundled daemon' : `Daemon at ${client?.endpoint.host}`;
47
+ const problems = s.diagnostics.errors > 0
48
+ ? `; ${s.diagnostics.errors} mapping entr${s.diagnostics.errors === 1 ? 'y' : 'ies'} skipped, see the editor`
49
+ : '';
50
+ return (`${where}: streaming ${s.streaming} field${s.streaming === 1 ? '' : 's'} from ` +
51
+ `${s.mapped.devices} of ${s.devices} device${s.devices === 1 ? '' : 's'}` +
52
+ (s.devices === 0 ? ' (discovering)' : '') +
53
+ problems);
54
+ };
55
+ const setError = (msg) => {
56
+ lastError = msg;
57
+ app.setPluginError(msg);
58
+ };
59
+ /** Ask the daemon how it is doing and keep the PUT handlers in step. */
60
+ const poll = async () => {
61
+ if (!client || !bridge)
62
+ return;
63
+ try {
64
+ const status = await client.status();
65
+ lastStatus = status;
66
+ lastError = null;
67
+ const mapping = await client.mapping();
68
+ const putKey = JSON.stringify(Object.entries(mapping.devices).map(([s, d]) => [
69
+ s,
70
+ Object.entries(d.fields)
71
+ .filter(([, f]) => f.put)
72
+ .map(([k, f]) => [k, f.path])
73
+ ]));
74
+ if (putKey !== mappingSeen) {
75
+ mappingSeen = putKey;
76
+ const { added, removed } = bridge.sync(mapping);
77
+ if (added.length + removed.length > 0) {
78
+ app.debug(`PUT targets: +${added.length} -${removed.length}: ${bridge.paths.join(' ')}`);
79
+ }
80
+ }
81
+ app.setPluginStatus(statusLine(status));
82
+ }
83
+ catch (err) {
84
+ lastStatus = null;
85
+ setError(`Daemon: ${errMsg(err)}`);
86
+ }
87
+ };
88
+ const asyncStart = async (cfg, gen) => {
89
+ let endpoint;
90
+ if (cfg.mode === 'bundled') {
91
+ const binary = resolveBinary(cfg.bundled.binaryPath);
92
+ const dataDir = path.join(app.getDataDirPath(), 'masterbus');
93
+ const d = new DaemonManager({
94
+ binary,
95
+ dataDir,
96
+ config: cfg.bundled,
97
+ events: {
98
+ log: (line) => {
99
+ app.debug(`daemon: ${line}`);
100
+ },
101
+ exited: (code, signal, restartIn) => {
102
+ const why = `the daemon exited (${code ?? signal ?? '?'})`;
103
+ if (restartIn === null) {
104
+ app.debug(why);
105
+ }
106
+ else {
107
+ setError(`${why}; restarting in ${restartIn / 1000} s. ${d.recentLog.slice(-3).join(' | ')}`);
108
+ }
109
+ },
110
+ ready: () => {
111
+ app.setPluginStatus('Daemon restarted; reconnecting');
112
+ void poll();
113
+ }
114
+ }
115
+ });
116
+ daemon = d;
117
+ app.setPluginStatus(`Starting ${path.basename(binary)}…`);
118
+ await d.start();
119
+ if (gen !== generation) {
120
+ await d.stop();
121
+ return;
122
+ }
123
+ endpoint = {
124
+ host: '127.0.0.1',
125
+ apiPort: cfg.bundled.apiPort,
126
+ streamPort: cfg.bundled.streamPort,
127
+ token: d.token
128
+ };
129
+ }
130
+ else {
131
+ endpoint = { ...cfg.external };
132
+ app.setPluginStatus(`Connecting to ${endpoint.host}:${endpoint.apiPort}…`);
133
+ }
134
+ const c = new DaemonClient(endpoint);
135
+ // A version check before anything else; a wrong daemon is refused loudly.
136
+ lastStatus = await c.status();
137
+ if (gen !== generation)
138
+ return;
139
+ client = c;
140
+ const b = new PutBridge(app, c, PLUGIN_ID);
141
+ b.setInstallerCode(cfg.installerCode);
142
+ bridge = b;
143
+ const f = new StreamForwarder(app, {
144
+ pluginId: PLUGIN_ID,
145
+ host: endpoint.host,
146
+ port: endpoint.streamPort,
147
+ debug: (m) => {
148
+ app.debug(m);
149
+ }
150
+ });
151
+ forwarder = f;
152
+ f.start();
153
+ await poll();
154
+ poller = setInterval(() => {
155
+ void poll();
156
+ }, POLL_MS);
157
+ };
158
+ return {
159
+ id: PLUGIN_ID,
160
+ name: 'MasterBus',
161
+ description: 'Mastervolt MasterBus: publish battery, charger and inverter values, edit the mapping ' +
162
+ 'in the browser, and switch devices from Signal K.',
163
+ schema: SCHEMA,
164
+ uiSchema: UI_SCHEMA,
165
+ start(options) {
166
+ const gen = ++generation;
167
+ config = withDefaults(options);
168
+ lastError = null;
169
+ void asyncStart(config, gen).catch((err) => {
170
+ if (gen !== generation)
171
+ return;
172
+ setError(errMsg(err));
173
+ });
174
+ },
175
+ async stop() {
176
+ generation++;
177
+ if (poller) {
178
+ clearInterval(poller);
179
+ poller = null;
180
+ }
181
+ forwarder?.stop();
182
+ forwarder = null;
183
+ bridge?.clear();
184
+ bridge = null;
185
+ client = null;
186
+ lastStatus = null;
187
+ if (daemon) {
188
+ const d = daemon;
189
+ daemon = null;
190
+ await d.stop();
191
+ }
192
+ app.setPluginStatus('Stopped');
193
+ },
194
+ registerWithRouter(router) {
195
+ registerRoutes(router, { client: () => client, summary });
196
+ }
197
+ };
198
+ }
199
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,47 @@
1
+ import type { ActionResult } from '@signalk/server-api';
2
+ import { type Login, type Mapping } from './types.js';
3
+ /** The slice of the server the bridge needs. */
4
+ export interface PutBridgeApp {
5
+ registerPutHandler(context: string, path: string, callback: (context: string, path: string, value: unknown, cb: (result: ActionResult) => void) => ActionResult, source?: string): void;
6
+ debug?(msg: string): void;
7
+ }
8
+ /** The slice of the client the bridge needs. */
9
+ export interface PutBridgeClient {
10
+ writeValue(serial: string, field: string, value: unknown, login?: Login): Promise<{
11
+ applied: unknown;
12
+ published: unknown;
13
+ }>;
14
+ }
15
+ /**
16
+ * Turns the mapping's `"put": true` entries into Signal K PUT handlers.
17
+ *
18
+ * The server has no way to unregister a handler, so one is registered per
19
+ * path the first time it is seen and stays; what it does is looked up in a
20
+ * live table on every call. A path that is no longer a put target answers
21
+ * with a 404-style failure rather than writing to a stale field.
22
+ *
23
+ * A write refused for lack of access level is retried once with the
24
+ * installer code when one is configured.
25
+ */
26
+ export declare class PutBridge {
27
+ private readonly app;
28
+ private readonly client;
29
+ private readonly pluginId;
30
+ private readonly targets;
31
+ private readonly registered;
32
+ private installerCode;
33
+ constructor(app: PutBridgeApp, client: PutBridgeClient, pluginId: string);
34
+ setInstallerCode(code: number | null): void;
35
+ /** The paths currently accepting PUTs. */
36
+ get paths(): string[];
37
+ /** Bring the live table in line with a mapping; returns what changed. */
38
+ sync(mapping: Mapping): {
39
+ added: string[];
40
+ removed: string[];
41
+ };
42
+ /** Drop every target (the plugin is stopping); registrations remain. */
43
+ clear(): void;
44
+ private readonly handler;
45
+ private write;
46
+ }
47
+ //# sourceMappingURL=put-bridge.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"put-bridge.d.ts","sourceRoot":"","sources":["../src/put-bridge.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,qBAAqB,CAAA;AAEvD,OAAO,EAAe,KAAK,KAAK,EAAE,KAAK,OAAO,EAAE,MAAM,YAAY,CAAA;AAElE,gDAAgD;AAChD,MAAM,WAAW,YAAY;IAC3B,kBAAkB,CAChB,OAAO,EAAE,MAAM,EACf,IAAI,EAAE,MAAM,EACZ,QAAQ,EAAE,CACR,OAAO,EAAE,MAAM,EACf,IAAI,EAAE,MAAM,EACZ,KAAK,EAAE,OAAO,EACd,EAAE,EAAE,CAAC,MAAM,EAAE,YAAY,KAAK,IAAI,KAC/B,YAAY,EACjB,MAAM,CAAC,EAAE,MAAM,GACd,IAAI,CAAA;IACP,KAAK,CAAC,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,CAAA;CAC1B;AAED,gDAAgD;AAChD,MAAM,WAAW,eAAe;IAC9B,UAAU,CACR,MAAM,EAAE,MAAM,EACd,KAAK,EAAE,MAAM,EACb,KAAK,EAAE,OAAO,EACd,KAAK,CAAC,EAAE,KAAK,GACZ,OAAO,CAAC;QAAE,OAAO,EAAE,OAAO,CAAC;QAAC,SAAS,EAAE,OAAO,CAAA;KAAE,CAAC,CAAA;CACrD;AAOD;;;;;;;;;;GAUG;AACH,qBAAa,SAAS;IACpB,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAc;IAClC,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAiB;IACxC,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAQ;IACjC,OAAO,CAAC,QAAQ,CAAC,OAAO,CAA4B;IACpD,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAoB;IAC/C,OAAO,CAAC,aAAa,CAAsB;gBAE/B,GAAG,EAAE,YAAY,EAAE,MAAM,EAAE,eAAe,EAAE,QAAQ,EAAE,MAAM;IAMxE,gBAAgB,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,GAAG,IAAI;IAI3C,0CAA0C;IAC1C,IAAI,KAAK,IAAI,MAAM,EAAE,CAEpB;IAED,yEAAyE;IACzE,IAAI,CAAC,OAAO,EAAE,OAAO,GAAG;QAAE,KAAK,EAAE,MAAM,EAAE,CAAC;QAAC,OAAO,EAAE,MAAM,EAAE,CAAA;KAAE;IAuB9D,wEAAwE;IACxE,KAAK,IAAI,IAAI;IAIb,OAAO,CAAC,QAAQ,CAAC,OAAO,CAsBvB;YAEa,KAAK;CAsBpB"}
@@ -0,0 +1,116 @@
1
+ import { DaemonError } from './types.js';
2
+ /**
3
+ * Turns the mapping's `"put": true` entries into Signal K PUT handlers.
4
+ *
5
+ * The server has no way to unregister a handler, so one is registered per
6
+ * path the first time it is seen and stays; what it does is looked up in a
7
+ * live table on every call. A path that is no longer a put target answers
8
+ * with a 404-style failure rather than writing to a stale field.
9
+ *
10
+ * A write refused for lack of access level is retried once with the
11
+ * installer code when one is configured.
12
+ */
13
+ export class PutBridge {
14
+ app;
15
+ client;
16
+ pluginId;
17
+ targets = new Map();
18
+ registered = new Set();
19
+ installerCode = null;
20
+ constructor(app, client, pluginId) {
21
+ this.app = app;
22
+ this.client = client;
23
+ this.pluginId = pluginId;
24
+ }
25
+ setInstallerCode(code) {
26
+ this.installerCode = code;
27
+ }
28
+ /** The paths currently accepting PUTs. */
29
+ get paths() {
30
+ return [...this.targets.keys()].sort();
31
+ }
32
+ /** Bring the live table in line with a mapping; returns what changed. */
33
+ sync(mapping) {
34
+ const next = new Map();
35
+ for (const [serial, dm] of Object.entries(mapping.devices)) {
36
+ for (const [field, fm] of Object.entries(dm.fields)) {
37
+ if (fm.put && fm.path)
38
+ next.set(fm.path, { serial, field });
39
+ }
40
+ }
41
+ const added = [];
42
+ const removed = [];
43
+ for (const path of this.targets.keys())
44
+ if (!next.has(path))
45
+ removed.push(path);
46
+ for (const [path, target] of next) {
47
+ if (!this.targets.has(path))
48
+ added.push(path);
49
+ this.targets.set(path, target);
50
+ }
51
+ for (const path of removed)
52
+ this.targets.delete(path);
53
+ for (const path of added) {
54
+ if (this.registered.has(path))
55
+ continue;
56
+ this.registered.add(path);
57
+ this.app.registerPutHandler('vessels.self', path, this.handler, this.pluginId);
58
+ }
59
+ return { added: added.sort(), removed: removed.sort() };
60
+ }
61
+ /** Drop every target (the plugin is stopping); registrations remain. */
62
+ clear() {
63
+ this.targets.clear();
64
+ }
65
+ handler = (_context, path, value, cb) => {
66
+ const target = this.targets.get(path);
67
+ if (!target) {
68
+ return {
69
+ state: 'COMPLETED',
70
+ statusCode: 404,
71
+ message: `${path} is not a MasterBus PUT target`
72
+ };
73
+ }
74
+ void this.write(target, value)
75
+ .then((r) => {
76
+ cb(r);
77
+ })
78
+ .catch((err) => {
79
+ cb({ state: 'COMPLETED', statusCode: 500, message: errMsg(err) });
80
+ });
81
+ return { state: 'PENDING' };
82
+ };
83
+ async write(target, value) {
84
+ try {
85
+ await this.client.writeValue(target.serial, target.field, value);
86
+ return { state: 'COMPLETED', statusCode: 200 };
87
+ }
88
+ catch (err) {
89
+ if (err instanceof DaemonError && err.needs === 'login' && this.installerCode !== null) {
90
+ this.app.debug?.(`${target.serial} ${target.field}: read-only; retrying with installer login`);
91
+ try {
92
+ await this.client.writeValue(target.serial, target.field, value, {
93
+ level: 'installer',
94
+ code: this.installerCode
95
+ });
96
+ return { state: 'COMPLETED', statusCode: 200 };
97
+ }
98
+ catch (again) {
99
+ return failure(again);
100
+ }
101
+ }
102
+ return failure(err);
103
+ }
104
+ }
105
+ }
106
+ function failure(err) {
107
+ if (err instanceof DaemonError) {
108
+ const statusCode = err.status >= 400 && err.status < 600 ? err.status : 502;
109
+ return { state: 'COMPLETED', statusCode, message: err.message };
110
+ }
111
+ return { state: 'COMPLETED', statusCode: 502, message: errMsg(err) };
112
+ }
113
+ function errMsg(err) {
114
+ return err instanceof Error ? err.message : String(err);
115
+ }
116
+ //# sourceMappingURL=put-bridge.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"put-bridge.js","sourceRoot":"","sources":["../src/put-bridge.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,20 @@
1
+ import type { PluginRouter } from '@signalk/server-api';
2
+ import type { DaemonClient } from './client.js';
3
+ /** What the router needs from the plugin: the client of the moment. */
4
+ export interface RouterState {
5
+ client: () => DaemonClient | null;
6
+ /** A one-line summary for `/status`. */
7
+ summary: () => Record<string, unknown>;
8
+ }
9
+ /**
10
+ * Mounts the editor's routes under `/plugins/signalk-masterbus/`:
11
+ *
12
+ * - `/status` — what the plugin knows, for the editor's header.
13
+ * - `/api/*` — proxied to the daemon's control API with the token added, so
14
+ * the browser never needs to reach the daemon (which in external mode may
15
+ * be on another host) and Signal K's own login covers it. Reads are open
16
+ * to read-only users; anything that changes the mapping or writes to the
17
+ * bus is admin-only, which is the router's default.
18
+ */
19
+ export declare function registerRoutes(router: PluginRouter, state: RouterState): void;
20
+ //# sourceMappingURL=router.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"router.d.ts","sourceRoot":"","sources":["../src/router.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,qBAAqB,CAAA;AAEvD,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,aAAa,CAAA;AAE/C,uEAAuE;AACvE,MAAM,WAAW,WAAW;IAC1B,MAAM,EAAE,MAAM,YAAY,GAAG,IAAI,CAAA;IACjC,wCAAwC;IACxC,OAAO,EAAE,MAAM,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;CACvC;AAED;;;;;;;;;GASG;AACH,wBAAgB,cAAc,CAAC,MAAM,EAAE,YAAY,EAAE,KAAK,EAAE,WAAW,GAAG,IAAI,CA0B7E"}
@@ -0,0 +1,38 @@
1
+ /**
2
+ * Mounts the editor's routes under `/plugins/signalk-masterbus/`:
3
+ *
4
+ * - `/status` — what the plugin knows, for the editor's header.
5
+ * - `/api/*` — proxied to the daemon's control API with the token added, so
6
+ * the browser never needs to reach the daemon (which in external mode may
7
+ * be on another host) and Signal K's own login covers it. Reads are open
8
+ * to read-only users; anything that changes the mapping or writes to the
9
+ * bus is admin-only, which is the router's default.
10
+ */
11
+ export function registerRoutes(router, state) {
12
+ const proxy = (req, res) => {
13
+ const client = state.client();
14
+ if (!client) {
15
+ res.status(503).json({ error: 'the MasterBus daemon is not connected' });
16
+ return;
17
+ }
18
+ // Everything after `/api` on this router, query string included.
19
+ const tail = req.originalUrl.replace(/^.*?\/api(?=\/|\?|$)/, '');
20
+ const body = req.method === 'GET' ? undefined : req.body;
21
+ client
22
+ .raw(req.method, `/api${tail}`, body)
23
+ .then(({ status, body }) => {
24
+ res.status(status).json(body);
25
+ })
26
+ .catch((err) => {
27
+ res.status(502).json({ error: err instanceof Error ? err.message : String(err) });
28
+ });
29
+ };
30
+ router.access('readonly').get('/status', (_req, res) => {
31
+ res.json(state.summary());
32
+ });
33
+ // Express 4 wildcards (the server's Express): `*` rather than `{*path}`.
34
+ router.access('readonly').get('/api/*', proxy);
35
+ router.put('/api/*', proxy);
36
+ router.post('/api/*', proxy);
37
+ }
38
+ //# sourceMappingURL=router.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"router.js","sourceRoot":"","sources":["../src/router.ts"],"names":[],"mappings":"AAYA;;;;;;;;;GASG;AACH,MAAM,UAAU,cAAc,CAAC,MAAoB,EAAE,KAAkB;IACrE,MAAM,KAAK,GAAG,CAAC,GAAY,EAAE,GAAa,EAAQ,EAAE;QAClD,MAAM,MAAM,GAAG,KAAK,CAAC,MAAM,EAAE,CAAA;QAC7B,IAAI,CAAC,MAAM,EAAE,CAAC;YACZ,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,uCAAuC,EAAE,CAAC,CAAA;YACxE,OAAM;QACR,CAAC;QACD,iEAAiE;QACjE,MAAM,IAAI,GAAG,GAAG,CAAC,WAAW,CAAC,OAAO,CAAC,sBAAsB,EAAE,EAAE,CAAC,CAAA;QAChE,MAAM,IAAI,GAAG,GAAG,CAAC,MAAM,KAAK,KAAK,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAE,GAAG,CAAC,IAAgB,CAAA;QACrE,MAAM;aACH,GAAG,CAAC,GAAG,CAAC,MAAM,EAAE,OAAO,IAAI,EAAE,EAAE,IAAI,CAAC;aACpC,IAAI,CAAC,CAAC,EAAE,MAAM,EAAE,IAAI,EAAE,EAAE,EAAE;YACzB,GAAG,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAA;QAC/B,CAAC,CAAC;aACD,KAAK,CAAC,CAAC,GAAY,EAAE,EAAE;YACtB,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,EAAE,CAAC,CAAA;QACnF,CAAC,CAAC,CAAA;IACN,CAAC,CAAA;IACD,MAAM,CAAC,MAAM,CAAC,UAAU,CAAC,CAAC,GAAG,CAAC,SAAS,EAAE,CAAC,IAAI,EAAE,GAAG,EAAE,EAAE;QACrD,GAAG,CAAC,IAAI,CAAC,KAAK,CAAC,OAAO,EAAE,CAAC,CAAA;IAC3B,CAAC,CAAC,CAAA;IACF,yEAAyE;IACzE,MAAM,CAAC,MAAM,CAAC,UAAU,CAAC,CAAC,GAAG,CAAC,QAAQ,EAAE,KAAK,CAAC,CAAA;IAC9C,MAAM,CAAC,GAAG,CAAC,QAAQ,EAAE,KAAK,CAAC,CAAA;IAC3B,MAAM,CAAC,IAAI,CAAC,QAAQ,EAAE,KAAK,CAAC,CAAA;AAC9B,CAAC"}
@@ -0,0 +1,54 @@
1
+ import type { Delta } from '@signalk/server-api';
2
+ /** The slice of the server the forwarder needs, so tests can pass a stub. */
3
+ export interface ForwarderApp {
4
+ handleMessage(id: string, msg: Partial<Delta>): void;
5
+ }
6
+ export interface ForwarderOptions {
7
+ pluginId: string;
8
+ host: string;
9
+ port: number;
10
+ debug?: (msg: string) => void;
11
+ reconnectMs?: number;
12
+ }
13
+ /**
14
+ * Reads the daemon's delta stream (newline-delimited Signal K deltas over
15
+ * TCP) and republishes every update through `app.handleMessage`, so the
16
+ * values, unit `meta` and notifications land on the server's own model,
17
+ * attributed to this plugin.
18
+ *
19
+ * `$source` and `timestamp` are dropped from each update: the server stamps
20
+ * both, and keeping the daemon's `$source` would make the data look like it
21
+ * came from a provider the admin UI cannot find.
22
+ *
23
+ * Reconnects with a fixed interval; the daemon replays the static metadata
24
+ * and notification states on every new connection, so nothing is lost.
25
+ */
26
+ export declare class StreamForwarder {
27
+ private readonly app;
28
+ private readonly opts;
29
+ private socket;
30
+ private timer;
31
+ private closed;
32
+ private _connected;
33
+ private _deltas;
34
+ private _values;
35
+ constructor(app: ForwarderApp, opts: ForwarderOptions);
36
+ get connected(): boolean;
37
+ /** Deltas and value entries forwarded so far. */
38
+ get counts(): {
39
+ deltas: number;
40
+ values: number;
41
+ };
42
+ start(): void;
43
+ stop(): void;
44
+ private connect;
45
+ private schedule;
46
+ /** One line of the stream. Exposed for tests. */
47
+ forward(line: string): void;
48
+ }
49
+ /**
50
+ * The delta with `$source` and `timestamp` removed from every update, or
51
+ * null when it is not a delta with updates.
52
+ */
53
+ export declare function strip(parsed: unknown): Partial<Delta> | null;
54
+ //# sourceMappingURL=stream-forwarder.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"stream-forwarder.d.ts","sourceRoot":"","sources":["../src/stream-forwarder.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,qBAAqB,CAAA;AAEhD,6EAA6E;AAC7E,MAAM,WAAW,YAAY;IAC3B,aAAa,CAAC,EAAE,EAAE,MAAM,EAAE,GAAG,EAAE,OAAO,CAAC,KAAK,CAAC,GAAG,IAAI,CAAA;CACrD;AAED,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,EAAE,MAAM,CAAA;IAChB,IAAI,EAAE,MAAM,CAAA;IACZ,IAAI,EAAE,MAAM,CAAA;IACZ,KAAK,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,IAAI,CAAA;IAC7B,WAAW,CAAC,EAAE,MAAM,CAAA;CACrB;AAED;;;;;;;;;;;;GAYG;AACH,qBAAa,eAAe;IAC1B,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAc;IAClC,OAAO,CAAC,QAAQ,CAAC,IAAI,CAA4B;IACjD,OAAO,CAAC,MAAM,CAA0B;IACxC,OAAO,CAAC,KAAK,CAA6C;IAC1D,OAAO,CAAC,MAAM,CAAQ;IACtB,OAAO,CAAC,UAAU,CAAQ;IAC1B,OAAO,CAAC,OAAO,CAAI;IACnB,OAAO,CAAC,OAAO,CAAI;gBAEP,GAAG,EAAE,YAAY,EAAE,IAAI,EAAE,gBAAgB;IASrD,IAAI,SAAS,IAAI,OAAO,CAEvB;IAED,iDAAiD;IACjD,IAAI,MAAM,IAAI;QAAE,MAAM,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE,CAE/C;IAED,KAAK,IAAI,IAAI;IAKb,IAAI,IAAI,IAAI;IAWZ,OAAO,CAAC,OAAO;IAwBf,OAAO,CAAC,QAAQ;IAQhB,iDAAiD;IACjD,OAAO,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI;CAsB5B;AAED;;;GAGG;AACH,wBAAgB,KAAK,CAAC,MAAM,EAAE,OAAO,GAAG,OAAO,CAAC,KAAK,CAAC,GAAG,IAAI,CAa5D"}
@@ -0,0 +1,139 @@
1
+ import net from 'node:net';
2
+ import readline from 'node:readline';
3
+ /**
4
+ * Reads the daemon's delta stream (newline-delimited Signal K deltas over
5
+ * TCP) and republishes every update through `app.handleMessage`, so the
6
+ * values, unit `meta` and notifications land on the server's own model,
7
+ * attributed to this plugin.
8
+ *
9
+ * `$source` and `timestamp` are dropped from each update: the server stamps
10
+ * both, and keeping the daemon's `$source` would make the data look like it
11
+ * came from a provider the admin UI cannot find.
12
+ *
13
+ * Reconnects with a fixed interval; the daemon replays the static metadata
14
+ * and notification states on every new connection, so nothing is lost.
15
+ */
16
+ export class StreamForwarder {
17
+ app;
18
+ opts;
19
+ socket = null;
20
+ timer = null;
21
+ closed = false;
22
+ _connected = false;
23
+ _deltas = 0;
24
+ _values = 0;
25
+ constructor(app, opts) {
26
+ this.app = app;
27
+ this.opts = {
28
+ debug: () => { },
29
+ reconnectMs: 3000,
30
+ ...opts
31
+ };
32
+ }
33
+ get connected() {
34
+ return this._connected;
35
+ }
36
+ /** Deltas and value entries forwarded so far. */
37
+ get counts() {
38
+ return { deltas: this._deltas, values: this._values };
39
+ }
40
+ start() {
41
+ this.closed = false;
42
+ this.connect();
43
+ }
44
+ stop() {
45
+ this.closed = true;
46
+ if (this.timer) {
47
+ clearTimeout(this.timer);
48
+ this.timer = null;
49
+ }
50
+ this.socket?.destroy();
51
+ this.socket = null;
52
+ this._connected = false;
53
+ }
54
+ connect() {
55
+ if (this.closed)
56
+ return;
57
+ const { host, port } = this.opts;
58
+ this.opts.debug(`connecting to the stream at ${host}:${port}`);
59
+ const socket = net.connect({ host, port });
60
+ this.socket = socket;
61
+ socket.setKeepAlive(true, 10_000);
62
+ socket.on('connect', () => {
63
+ this._connected = true;
64
+ this.opts.debug('stream connected');
65
+ });
66
+ readline.createInterface({ input: socket }).on('line', (line) => {
67
+ this.forward(line);
68
+ });
69
+ socket.on('error', (err) => {
70
+ this.opts.debug(`stream error: ${err.message}`);
71
+ });
72
+ socket.on('close', () => {
73
+ this._connected = false;
74
+ if (this.socket === socket)
75
+ this.socket = null;
76
+ if (!this.closed)
77
+ this.schedule();
78
+ });
79
+ }
80
+ schedule() {
81
+ if (this.timer || this.closed)
82
+ return;
83
+ this.timer = setTimeout(() => {
84
+ this.timer = null;
85
+ this.connect();
86
+ }, this.opts.reconnectMs);
87
+ }
88
+ /** One line of the stream. Exposed for tests. */
89
+ forward(line) {
90
+ const text = line.trim();
91
+ if (!text)
92
+ return;
93
+ let parsed;
94
+ try {
95
+ parsed = JSON.parse(text);
96
+ }
97
+ catch {
98
+ this.opts.debug(`stream: not JSON: ${text.slice(0, 80)}`);
99
+ return;
100
+ }
101
+ const delta = strip(parsed);
102
+ if (!delta)
103
+ return;
104
+ try {
105
+ this.app.handleMessage(this.opts.pluginId, delta);
106
+ this._deltas++;
107
+ for (const u of delta.updates ?? []) {
108
+ this._values += ('values' in u ? u.values.length : 0) + ('meta' in u ? u.meta.length : 0);
109
+ }
110
+ }
111
+ catch (err) {
112
+ this.opts.debug(`handleMessage failed: ${err instanceof Error ? err.message : String(err)}`);
113
+ }
114
+ }
115
+ }
116
+ /**
117
+ * The delta with `$source` and `timestamp` removed from every update, or
118
+ * null when it is not a delta with updates.
119
+ */
120
+ export function strip(parsed) {
121
+ if (!parsed || typeof parsed !== 'object')
122
+ return null;
123
+ const root = parsed;
124
+ if (!Array.isArray(root.updates))
125
+ return null;
126
+ const updates = [];
127
+ for (const u of root.updates) {
128
+ if (!u || typeof u !== 'object')
129
+ continue;
130
+ const { $source: _s, timestamp: _t, ...rest } = u;
131
+ if (!Array.isArray(rest.values) && !Array.isArray(rest.meta))
132
+ continue;
133
+ updates.push(rest);
134
+ }
135
+ if (updates.length === 0)
136
+ return null;
137
+ return { updates };
138
+ }
139
+ //# sourceMappingURL=stream-forwarder.js.map