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.
- package/LICENSE +201 -0
- package/README.md +128 -0
- package/bin/darwin-arm64/masterbus-signalk +0 -0
- package/bin/darwin-x64/masterbus-signalk +0 -0
- package/bin/linux-arm/masterbus-signalk +0 -0
- package/bin/linux-arm64/masterbus-signalk +0 -0
- package/bin/linux-x64/masterbus-signalk +0 -0
- package/bin/win32-x64/masterbus-signalk.exe +0 -0
- package/package.json +82 -0
- package/plugin/client.d.ts +39 -0
- package/plugin/client.d.ts.map +1 -0
- package/plugin/client.js +93 -0
- package/plugin/client.js.map +1 -0
- package/plugin/config.d.ts +146 -0
- package/plugin/config.d.ts.map +1 -0
- package/plugin/config.js +143 -0
- package/plugin/config.js.map +1 -0
- package/plugin/daemon.d.ts +63 -0
- package/plugin/daemon.d.ts.map +1 -0
- package/plugin/daemon.js +223 -0
- package/plugin/daemon.js.map +1 -0
- package/plugin/index.d.ts +16 -0
- package/plugin/index.d.ts.map +1 -0
- package/plugin/index.js +199 -0
- package/plugin/index.js.map +1 -0
- package/plugin/put-bridge.d.ts +47 -0
- package/plugin/put-bridge.d.ts.map +1 -0
- package/plugin/put-bridge.js +116 -0
- package/plugin/put-bridge.js.map +1 -0
- package/plugin/router.d.ts +20 -0
- package/plugin/router.d.ts.map +1 -0
- package/plugin/router.js +38 -0
- package/plugin/router.js.map +1 -0
- package/plugin/stream-forwarder.d.ts +54 -0
- package/plugin/stream-forwarder.d.ts.map +1 -0
- package/plugin/stream-forwarder.js +139 -0
- package/plugin/stream-forwarder.js.map +1 -0
- package/plugin/types.d.ts +136 -0
- package/plugin/types.d.ts.map +1 -0
- package/plugin/types.js +19 -0
- package/plugin/types.js.map +1 -0
- package/public/assets/icon.svg +6 -0
- package/public/assets/index-D5ZZgo9o.js +49 -0
- package/public/assets/index-rkv4IC1b.css +1 -0
- package/public/index.html +14 -0
package/plugin/index.js
ADDED
|
@@ -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"}
|
package/plugin/router.js
ADDED
|
@@ -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
|