@rhizomatics/signalk-einklabel-plugin 0.6.4 → 0.7.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/CHANGELOG.md +18 -0
- package/README.md +19 -10
- package/dist/cli/index.js +129 -96
- package/dist/cli/liveContext.d.ts +2 -2
- package/dist/cli/liveContext.js +15 -15
- package/dist/cli/log.d.ts +1 -1
- package/dist/cli/log.js +4 -4
- package/dist/config.d.ts +12 -3
- package/dist/config.js +71 -49
- package/dist/devices/bleDiscovery.d.ts +1 -1
- package/dist/devices/bleDiscovery.js +9 -3
- package/dist/devices/registry.d.ts +1 -1
- package/dist/devices/types.d.ts +4 -4
- package/dist/devices/zhsunyco/encode.d.ts +2 -2
- package/dist/devices/zhsunyco/index.d.ts +3 -3
- package/dist/devices/zhsunyco/index.js +10 -9
- package/dist/devices/zhsunyco/metadata.d.ts +1 -1
- package/dist/devices/zhsunyco/metadata.js +12 -12
- package/dist/devices/zhsunyco/protocol.js +10 -10
- package/dist/httpJson.js +3 -3
- package/dist/index.d.ts +3 -3
- package/dist/pathMeta.d.ts +1 -1
- package/dist/pathMeta.js +1 -1
- package/dist/plugin.d.ts +1 -1
- package/dist/plugin.js +16 -13
- package/dist/pluginVersion.d.ts +9 -0
- package/dist/pluginVersion.js +12 -2
- package/dist/render/assets.d.ts +14 -6
- package/dist/render/assets.js +65 -10
- package/dist/render/binding.d.ts +9 -2
- package/dist/render/binding.js +23 -23
- package/dist/render/fonts.js +6 -6
- package/dist/render/formatters.d.ts +1 -1
- package/dist/render/formatters.js +25 -22
- package/dist/render/png.d.ts +1 -1
- package/dist/render/svgRenderer.d.ts +9 -8
- package/dist/render/svgRenderer.js +33 -27
- package/dist/render/types.d.ts +1 -1
- package/dist/repaintScheduler.d.ts +2 -2
- package/dist/repaintScheduler.js +21 -20
- package/dist/resolveApiUrl.js +3 -3
- package/dist/unitCategories.d.ts +1 -1
- package/dist/unitCategories.js +3 -3
- package/docs/assets/screenshots/plugin_config.png +0 -0
- package/package.json +42 -39
- package/templates/assets/lunar_phases/README.md +18 -0
- package/templates/tide.svg +11 -11
- /package/docs/assets/{inkscape_desc.png → screenshots/inkscape_desc.png} +0 -0
- /package/{resources/svg → templates/assets}/lunar_phases/first_quarter.svg +0 -0
- /package/{resources/svg → templates/assets}/lunar_phases/full_moon.svg +0 -0
- /package/{resources/svg → templates/assets}/lunar_phases/new_moon.svg +0 -0
- /package/{resources/svg → templates/assets}/lunar_phases/third_quarter.svg +0 -0
- /package/{resources/svg → templates/assets}/lunar_phases/waning_crescent.svg +0 -0
- /package/{resources/svg → templates/assets}/lunar_phases/waning_gibbous.svg +0 -0
- /package/{resources/svg → templates/assets}/lunar_phases/waxing_crescent.svg +0 -0
- /package/{resources/svg → templates/assets}/lunar_phases/waxing_gibbous.svg +0 -0
package/dist/cli/log.js
CHANGED
|
@@ -2,17 +2,17 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.setLogLevel = setLogLevel;
|
|
4
4
|
exports.logDebug = logDebug;
|
|
5
|
-
const LEVELS = [
|
|
6
|
-
let currentLevel =
|
|
5
|
+
const LEVELS = ["info", "debug"];
|
|
6
|
+
let currentLevel = "info";
|
|
7
7
|
/** Set once from the global `--log-level` option (see index.ts's `preAction` hook) - applies to every command. */
|
|
8
8
|
function setLogLevel(level) {
|
|
9
9
|
if (!LEVELS.includes(level)) {
|
|
10
|
-
throw new Error(`unknown --log-level "${level}" - expected one of ${LEVELS.join(
|
|
10
|
+
throw new Error(`unknown --log-level "${level}" - expected one of ${LEVELS.join(", ")}`);
|
|
11
11
|
}
|
|
12
12
|
currentLevel = level;
|
|
13
13
|
}
|
|
14
14
|
/** Internal tracing (e.g. which URL is being fetched) - silent unless --log-level debug, so default output stays uncluttered. */
|
|
15
15
|
function logDebug(message) {
|
|
16
|
-
if (currentLevel ===
|
|
16
|
+
if (currentLevel === "debug")
|
|
17
17
|
console.error(`[debug] ${message}`);
|
|
18
18
|
}
|
package/dist/config.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { ServerAPI } from
|
|
2
|
-
import { DiscoveredDevice } from
|
|
1
|
+
import { ServerAPI } from "@signalk/server-api";
|
|
2
|
+
import { DiscoveredDevice } from "./devices/types";
|
|
3
3
|
export interface DeviceConfig {
|
|
4
4
|
friendlyName: string;
|
|
5
5
|
/**
|
|
@@ -11,7 +11,7 @@ export interface DeviceConfig {
|
|
|
11
11
|
/** Per-device override; if omitted, the vendor driver may fall back to a stock/manufacturer-default key. */
|
|
12
12
|
aesKey?: string;
|
|
13
13
|
templateName: string;
|
|
14
|
-
repaintTrigger:
|
|
14
|
+
repaintTrigger: "subscription" | "interval";
|
|
15
15
|
/** SignalK path to subscribe to when `repaintTrigger` is `subscription` - a repaint is considered on every delta. */
|
|
16
16
|
triggerPath?: string;
|
|
17
17
|
/** When `repaintTrigger` is `interval`: repaint every N hours... */
|
|
@@ -54,6 +54,15 @@ export interface PluginConfig {
|
|
|
54
54
|
signalkApiUrl?: string;
|
|
55
55
|
devices: DeviceConfig[];
|
|
56
56
|
}
|
|
57
|
+
/**
|
|
58
|
+
* The package's own bundled `templates/` directory (ships alongside `dist/`, see
|
|
59
|
+
* package.json's `files`) - templates here are always available, but a same-named template in the
|
|
60
|
+
* user's `templatesDir` takes priority. Exported so `SvgRenderer` can fall back to it when resolving
|
|
61
|
+
* an `assets=` binding's directory (see `resolveAssetPath` in `./render/assets.ts`) - overriding a
|
|
62
|
+
* bundled template shouldn't also require duplicating its bundled asset sets (e.g.
|
|
63
|
+
* `templates/assets/lunar_phases`) just to keep a binding the override never touched working.
|
|
64
|
+
*/
|
|
65
|
+
export declare const BUNDLED_TEMPLATES_DIR: string;
|
|
57
66
|
export declare function defaultConfig(): PluginConfig;
|
|
58
67
|
/**
|
|
59
68
|
* Resolves the user-facing `templatesDir` setting to an actual directory, mirroring
|
package/dist/config.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.BUNDLED_TEMPLATES_DIR = void 0;
|
|
3
4
|
exports.defaultConfig = defaultConfig;
|
|
4
5
|
exports.resolveTemplatesDir = resolveTemplatesDir;
|
|
5
6
|
exports.parseDevice = parseDevice;
|
|
@@ -10,13 +11,20 @@ const fs_1 = require("fs");
|
|
|
10
11
|
const os_1 = require("os");
|
|
11
12
|
const path_1 = require("path");
|
|
12
13
|
const resolveApiUrl_1 = require("./resolveApiUrl");
|
|
13
|
-
/**
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
14
|
+
/**
|
|
15
|
+
* The package's own bundled `templates/` directory (ships alongside `dist/`, see
|
|
16
|
+
* package.json's `files`) - templates here are always available, but a same-named template in the
|
|
17
|
+
* user's `templatesDir` takes priority. Exported so `SvgRenderer` can fall back to it when resolving
|
|
18
|
+
* an `assets=` binding's directory (see `resolveAssetPath` in `./render/assets.ts`) - overriding a
|
|
19
|
+
* bundled template shouldn't also require duplicating its bundled asset sets (e.g.
|
|
20
|
+
* `templates/assets/lunar_phases`) just to keep a binding the override never touched working.
|
|
21
|
+
*/
|
|
22
|
+
exports.BUNDLED_TEMPLATES_DIR = (0, path_1.join)(__dirname, "..", "templates");
|
|
23
|
+
const SIGNALK_HOME_DIR = (0, path_1.join)((0, os_1.homedir)(), ".signalk");
|
|
24
|
+
const DEFAULT_TEMPLATES_DIR = (0, path_1.join)(SIGNALK_HOME_DIR, "einklabel", "templates");
|
|
17
25
|
function defaultConfig() {
|
|
18
26
|
return {
|
|
19
|
-
templatesDir:
|
|
27
|
+
templatesDir: "",
|
|
20
28
|
scanOnStart: true,
|
|
21
29
|
scanDurationSeconds: 20,
|
|
22
30
|
paintConnectTimeoutSeconds: 30,
|
|
@@ -53,7 +61,7 @@ function deviceOptions(discovered, current) {
|
|
|
53
61
|
if (found.pid === undefined) {
|
|
54
62
|
continue;
|
|
55
63
|
}
|
|
56
|
-
const modelToken = [found.vendor, found.pid, found.hwVersion].filter((part) => part !== undefined).join(
|
|
64
|
+
const modelToken = [found.vendor, found.pid, found.hwVersion].filter((part) => part !== undefined).join(":");
|
|
57
65
|
const value = `${modelToken}@${found.address}`;
|
|
58
66
|
if (seen.has(value)) {
|
|
59
67
|
continue;
|
|
@@ -62,7 +70,7 @@ function deviceOptions(discovered, current) {
|
|
|
62
70
|
values.push(value);
|
|
63
71
|
const label = found.metadata
|
|
64
72
|
? `${found.vendor} ${found.metadata.label} (${found.address})`
|
|
65
|
-
: `${found.vendor} unrecognised PID 0x${found.pid.toString(16).padStart(4,
|
|
73
|
+
: `${found.vendor} unrecognised PID 0x${found.pid.toString(16).padStart(4, "0")} (${found.address})`;
|
|
66
74
|
labels.push(label);
|
|
67
75
|
}
|
|
68
76
|
for (const device of current.devices) {
|
|
@@ -75,14 +83,14 @@ function deviceOptions(discovered, current) {
|
|
|
75
83
|
return { values, labels };
|
|
76
84
|
}
|
|
77
85
|
function parseDevice(device) {
|
|
78
|
-
const [modelToken, address] = device.split(
|
|
79
|
-
const [vendor, pidStr, hwVersion] = (modelToken ??
|
|
86
|
+
const [modelToken, address] = device.split("@");
|
|
87
|
+
const [vendor, pidStr, hwVersion] = (modelToken ?? "").split(":");
|
|
80
88
|
const pid = Number(pidStr);
|
|
81
89
|
return vendor && address && Number.isInteger(pid) ? { vendor, pid, hwVersion, address } : undefined;
|
|
82
90
|
}
|
|
83
91
|
function listSvgFiles(dir) {
|
|
84
92
|
try {
|
|
85
|
-
return (0, fs_1.readdirSync)(dir).filter((name) => name.endsWith(
|
|
93
|
+
return (0, fs_1.readdirSync)(dir).filter((name) => name.endsWith(".svg"));
|
|
86
94
|
}
|
|
87
95
|
catch {
|
|
88
96
|
return [];
|
|
@@ -91,13 +99,13 @@ function listSvgFiles(dir) {
|
|
|
91
99
|
/** Local templates take priority over a same-named bundled one; both show up as options. */
|
|
92
100
|
function templateNameOptions(templatesDir) {
|
|
93
101
|
const local = listSvgFiles(templatesDir);
|
|
94
|
-
const bundled = listSvgFiles(BUNDLED_TEMPLATES_DIR).filter((name) => !local.includes(name));
|
|
102
|
+
const bundled = listSvgFiles(exports.BUNDLED_TEMPLATES_DIR).filter((name) => !local.includes(name));
|
|
95
103
|
return [...local, ...bundled];
|
|
96
104
|
}
|
|
97
105
|
/** Resolves a template name to an actual file path - a local template overrides the bundled one of the same name. */
|
|
98
106
|
function resolveTemplatePath(templatesDir, templateName) {
|
|
99
107
|
const localPath = (0, path_1.join)(templatesDir, templateName);
|
|
100
|
-
return (0, fs_1.existsSync)(localPath) ? localPath : (0, path_1.join)(BUNDLED_TEMPLATES_DIR, templateName);
|
|
108
|
+
return (0, fs_1.existsSync)(localPath) ? localPath : (0, path_1.join)(exports.BUNDLED_TEMPLATES_DIR, templateName);
|
|
101
109
|
}
|
|
102
110
|
/** JSON Schema forbids an empty `enum` array, so only attach one when there's at least one option - otherwise the whole config schema fails validation. */
|
|
103
111
|
function withEnum(schema, values, names) {
|
|
@@ -108,78 +116,92 @@ function configSchema(app, discovered = []) {
|
|
|
108
116
|
const current = { ...defaults, ...app.readPluginOptions() };
|
|
109
117
|
const { values: deviceValues, labels: deviceLabels } = deviceOptions(discovered, current);
|
|
110
118
|
return {
|
|
111
|
-
type:
|
|
119
|
+
type: "object",
|
|
112
120
|
properties: {
|
|
113
121
|
templatesDir: {
|
|
114
|
-
type:
|
|
115
|
-
title:
|
|
122
|
+
type: "string",
|
|
123
|
+
title: "Templates directory",
|
|
116
124
|
description: `Relative path from ~/.signalk (e.g., "esl/templates" becomes ~/.signalk/esl/templates). ` +
|
|
117
125
|
`Leave empty for default (${DEFAULT_TEMPLATES_DIR}). Absolute paths also supported. A template here ` +
|
|
118
|
-
|
|
126
|
+
"with the same name as a bundled one takes priority. Also used to override SVG assets, like the bundled lunar_phases.",
|
|
119
127
|
default: defaults.templatesDir,
|
|
120
128
|
},
|
|
121
129
|
scanOnStart: {
|
|
122
|
-
type:
|
|
123
|
-
title:
|
|
130
|
+
type: "boolean",
|
|
131
|
+
title: "Scan for devices on plugin start",
|
|
124
132
|
description: 'Runs a short BLE scan so discovered devices show up in a device\'s "Device" picker below.',
|
|
125
133
|
default: defaults.scanOnStart,
|
|
126
134
|
},
|
|
127
135
|
scanDurationSeconds: {
|
|
128
|
-
type:
|
|
129
|
-
title:
|
|
136
|
+
type: "number",
|
|
137
|
+
title: "Scan duration (seconds)",
|
|
130
138
|
description: 'How long the startup scan runs - increase if devices are missing from the "Device" picker below.',
|
|
131
139
|
minimum: 1,
|
|
132
140
|
default: defaults.scanDurationSeconds,
|
|
133
141
|
},
|
|
134
142
|
paintConnectTimeoutSeconds: {
|
|
135
|
-
type:
|
|
136
|
-
title:
|
|
137
|
-
description:
|
|
143
|
+
type: "number",
|
|
144
|
+
title: "Paint connect timeout (seconds)",
|
|
145
|
+
description: "How long to wait for a device to accept a BLE connection before giving up on a repaint attempt.",
|
|
138
146
|
minimum: 1,
|
|
139
147
|
default: defaults.paintConnectTimeoutSeconds,
|
|
140
148
|
},
|
|
141
149
|
paintRetries: {
|
|
142
|
-
type:
|
|
143
|
-
title:
|
|
144
|
-
description:
|
|
150
|
+
type: "number",
|
|
151
|
+
title: "Paint retries",
|
|
152
|
+
description: "How many times to attempt a repaint (including the first try) before giving up and reporting failure.",
|
|
145
153
|
minimum: 1,
|
|
146
154
|
default: defaults.paintRetries,
|
|
147
155
|
},
|
|
148
156
|
signalkApiUrl: {
|
|
149
|
-
type:
|
|
150
|
-
title:
|
|
151
|
-
description:
|
|
152
|
-
enum: [
|
|
157
|
+
type: "string",
|
|
158
|
+
title: "SignalK API base URL (leave blank to auto-detect)",
|
|
159
|
+
description: "Used for plugin access to SignalK REST APIs not yet integrated for direct plugin access. Left blank, the plugin probes the likely options at startup (3000, 80, 443 ) - only set this manually to skip probing. Anonymous read access is required.",
|
|
160
|
+
enum: ["", ...resolveApiUrl_1.SIGNALK_API_URL_OPTIONS],
|
|
153
161
|
},
|
|
154
162
|
devices: {
|
|
155
|
-
type:
|
|
156
|
-
title:
|
|
163
|
+
type: "array",
|
|
164
|
+
title: "Devices",
|
|
157
165
|
items: {
|
|
158
|
-
type:
|
|
159
|
-
required: [
|
|
166
|
+
type: "object",
|
|
167
|
+
required: ["friendlyName", "device", "templateName", "repaintTrigger"],
|
|
160
168
|
properties: {
|
|
161
|
-
friendlyName: { type:
|
|
169
|
+
friendlyName: { type: "string", title: "Friendly name" },
|
|
162
170
|
device: withEnum({
|
|
163
|
-
type:
|
|
164
|
-
title:
|
|
165
|
-
description:
|
|
171
|
+
type: "string",
|
|
172
|
+
title: "Device",
|
|
173
|
+
description: "Picked from devices found by a scan (plugin start, or `esl-cli scan`) - sets both the model and BLE address.",
|
|
166
174
|
}, deviceValues, deviceLabels),
|
|
167
|
-
templateName: withEnum({ type:
|
|
168
|
-
repaintTrigger: {
|
|
169
|
-
|
|
170
|
-
|
|
175
|
+
templateName: withEnum({ type: "string", title: "Template" }, templateNameOptions(resolveTemplatesDir(current.templatesDir))),
|
|
176
|
+
repaintTrigger: {
|
|
177
|
+
type: "string",
|
|
178
|
+
title: "Repaint trigger",
|
|
179
|
+
enum: ["subscription", "interval"],
|
|
180
|
+
},
|
|
181
|
+
triggerPath: {
|
|
182
|
+
type: "string",
|
|
183
|
+
title: "Trigger SignalK path (if repaint trigger is subscription)",
|
|
184
|
+
},
|
|
185
|
+
intervalHours: {
|
|
186
|
+
type: "number",
|
|
187
|
+
title: "Repaint every N hours (if repaint trigger is interval)",
|
|
188
|
+
minimum: 1,
|
|
189
|
+
},
|
|
171
190
|
intervalMinute: {
|
|
172
|
-
type:
|
|
173
|
-
title:
|
|
191
|
+
type: "number",
|
|
192
|
+
title: "Minutes past the hour (if repaint trigger is interval)",
|
|
174
193
|
minimum: 0,
|
|
175
194
|
maximum: 59,
|
|
176
195
|
default: 0,
|
|
177
196
|
},
|
|
178
|
-
aesKey: {
|
|
197
|
+
aesKey: {
|
|
198
|
+
type: "string",
|
|
199
|
+
title: "BLE AES key (vendor-specific; leave blank to use a default key)",
|
|
200
|
+
},
|
|
179
201
|
forceRepaint: {
|
|
180
|
-
type:
|
|
181
|
-
title:
|
|
182
|
-
description:
|
|
202
|
+
type: "boolean",
|
|
203
|
+
title: "Force repaint",
|
|
204
|
+
description: "Repaint even if the data is unchanged - clears itself automatically once that repaint completes",
|
|
183
205
|
default: false,
|
|
184
206
|
},
|
|
185
207
|
},
|
|
@@ -192,7 +214,7 @@ function configUiSchema() {
|
|
|
192
214
|
return {
|
|
193
215
|
devices: {
|
|
194
216
|
items: {
|
|
195
|
-
repaintTrigger: {
|
|
217
|
+
repaintTrigger: { "ui:widget": "radio" },
|
|
196
218
|
},
|
|
197
219
|
},
|
|
198
220
|
};
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Adapter, Bluetooth, Device } from
|
|
1
|
+
import { Adapter, Bluetooth, Device } from "@naugehyde/node-ble";
|
|
2
2
|
export declare function sleep(ms: number): Promise<void>;
|
|
3
3
|
/**
|
|
4
4
|
* Drop-in replacement for `node-ble`'s `createBluetooth` that fails fast and clearly when
|
|
@@ -23,9 +23,9 @@ function sleep(ms) {
|
|
|
23
23
|
* `createBluetooth()` or its callers can see it; it takes the whole process down.
|
|
24
24
|
*/
|
|
25
25
|
function createBluetooth() {
|
|
26
|
-
if (process.platform !==
|
|
26
|
+
if (process.platform !== "linux") {
|
|
27
27
|
throw new Error(`BLE support requires Linux (BlueZ over D-Bus); "${process.platform}" is not supported - ` +
|
|
28
|
-
|
|
28
|
+
"run this on the target Linux host (e.g. the NanoPi), not on macOS/Windows.");
|
|
29
29
|
}
|
|
30
30
|
return (0, node_ble_1.createBluetooth)();
|
|
31
31
|
}
|
|
@@ -53,7 +53,13 @@ async function forEachAdvertisedDevice(adapter, fn) {
|
|
|
53
53
|
const manufacturerData = await device.getManufacturerData().catch(() => undefined);
|
|
54
54
|
const [key] = Object.keys(manufacturerData ?? {});
|
|
55
55
|
const manufacturerId = key === undefined ? undefined : Number(key);
|
|
56
|
-
await fn({
|
|
56
|
+
await fn({
|
|
57
|
+
address,
|
|
58
|
+
device,
|
|
59
|
+
name,
|
|
60
|
+
manufacturerId,
|
|
61
|
+
manufacturerData: key === undefined ? undefined : manufacturerData[key],
|
|
62
|
+
});
|
|
57
63
|
}
|
|
58
64
|
}
|
|
59
65
|
/**
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { VendorDriver } from
|
|
1
|
+
import { VendorDriver } from "./types";
|
|
2
2
|
export declare function registerDriver(driver: VendorDriver): void;
|
|
3
3
|
export declare function getDriver(vendor: string): VendorDriver | undefined;
|
|
4
4
|
export declare function allDrivers(): VendorDriver[];
|
package/dist/devices/types.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { Device } from
|
|
2
|
-
import { Bitmap } from
|
|
3
|
-
export type Colour =
|
|
1
|
+
import { Device } from "@naugehyde/node-ble";
|
|
2
|
+
import { Bitmap } from "../render/types";
|
|
3
|
+
export type Colour = "black" | "white" | "red" | "yellow";
|
|
4
4
|
/**
|
|
5
5
|
* Static facts about one device model, keyed by (vendor, pid) by the registry —
|
|
6
6
|
* PID alone is not assumed unique across vendors.
|
|
@@ -39,7 +39,7 @@ export interface DiscoveredDevice {
|
|
|
39
39
|
batteryMv?: number;
|
|
40
40
|
}
|
|
41
41
|
/** Manual model facts a user can supply for hardware that isn't yet in a driver's PID table. */
|
|
42
|
-
export type DeviceModelOverride = Omit<DeviceMetadata,
|
|
42
|
+
export type DeviceModelOverride = Omit<DeviceMetadata, "pid">;
|
|
43
43
|
/** Per-device settings the user supplies when registering a device, beyond what's in DeviceMetadata. */
|
|
44
44
|
export interface VendorDeviceConfig {
|
|
45
45
|
address: string;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { Bitmap } from
|
|
2
|
-
import { DeviceMetadata } from
|
|
1
|
+
import { Bitmap } from "../../render/types";
|
|
2
|
+
import { DeviceMetadata } from "../types";
|
|
3
3
|
/**
|
|
4
4
|
* Quantises a common RGBA bitmap and packs it into the Wolink wire format: 2 bits per
|
|
5
5
|
* pixel, 4 pixels per byte, column-major, with both axes flipped (RAM is x/y-flipped
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { Device } from
|
|
2
|
-
import { Bitmap } from
|
|
3
|
-
import { DeviceMetadata, DiscoveredDevice, VendorDeviceConfig, VendorDriver } from
|
|
1
|
+
import { Device } from "@naugehyde/node-ble";
|
|
2
|
+
import { Bitmap } from "../../render/types";
|
|
3
|
+
import { DeviceMetadata, DiscoveredDevice, VendorDeviceConfig, VendorDriver } from "../types";
|
|
4
4
|
export declare class ZhsunycoDriver implements VendorDriver {
|
|
5
5
|
readonly vendor = "zhsunyco";
|
|
6
6
|
matchesAdvertisement(name: string | undefined, manufacturerId: number | undefined): boolean;
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.ZhsunycoDriver = void 0;
|
|
4
4
|
const bleDiscovery_1 = require("../bleDiscovery");
|
|
5
|
+
const pluginVersion_1 = require("../../pluginVersion");
|
|
5
6
|
const metadata_1 = require("./metadata");
|
|
6
7
|
const encode_1 = require("./encode");
|
|
7
8
|
const protocol_1 = require("./protocol");
|
|
@@ -17,10 +18,10 @@ const SCAN_CONNECT_TIMEOUT_MS = 10000;
|
|
|
17
18
|
const DEFAULT_PAINT_CONNECT_TIMEOUT_MS = 60000;
|
|
18
19
|
class ZhsunycoDriver {
|
|
19
20
|
constructor() {
|
|
20
|
-
this.vendor =
|
|
21
|
+
this.vendor = "zhsunyco";
|
|
21
22
|
}
|
|
22
23
|
matchesAdvertisement(name, manufacturerId) {
|
|
23
|
-
return manufacturerId === protocol_1.ZHSUNYCO_MANUFACTURER_ID || (name ??
|
|
24
|
+
return manufacturerId === protocol_1.ZHSUNYCO_MANUFACTURER_ID || (name ?? "").startsWith("WL") || (name ?? "").startsWith("WOESL");
|
|
24
25
|
}
|
|
25
26
|
metadataForPid(pid, hwVersion) {
|
|
26
27
|
const candidates = metadata_1.ZHSUNYCO_PID_METADATA.filter((model) => model.pid === pid);
|
|
@@ -63,21 +64,21 @@ class ZhsunycoDriver {
|
|
|
63
64
|
const statusChar = await service.getCharacteristic(protocol_1.WOLINK_CHARACTERISTIC_UUIDS.status);
|
|
64
65
|
const info = (0, protocol_1.decodeAdvertisedInfo)(await configChar.readValue());
|
|
65
66
|
if (!info) {
|
|
66
|
-
throw new Error(
|
|
67
|
+
throw new Error("zhsunyco device did not return valid config data");
|
|
67
68
|
}
|
|
68
69
|
const metadata = config.modelOverride ? { pid: info.pid, ...config.modelOverride } : this.metadataForPid(info.pid, info.hwVersion);
|
|
69
70
|
if (!metadata) {
|
|
70
|
-
throw new Error(`zhsunyco device reports unrecognised PID 0x${info.pid.toString(16).padStart(4,
|
|
71
|
-
|
|
71
|
+
throw new Error(`zhsunyco device reports unrecognised PID 0x${info.pid.toString(16).padStart(4, "0")} - ` +
|
|
72
|
+
"pass --width/--height/--voffset/--colours to describe it manually");
|
|
72
73
|
}
|
|
73
74
|
const statusReceived = new Promise((resolve, reject) => {
|
|
74
|
-
statusChar.once(
|
|
75
|
+
statusChar.once("valuechanged", (data) => {
|
|
75
76
|
const { errorCode } = (0, protocol_1.decodeStatus)(data);
|
|
76
77
|
if (errorCode === 0) {
|
|
77
78
|
resolve();
|
|
78
79
|
}
|
|
79
80
|
else {
|
|
80
|
-
reject(new Error(`zhsunyco device reported error 0x${errorCode.toString(16).padStart(2,
|
|
81
|
+
reject(new Error(`zhsunyco device reported error 0x${errorCode.toString(16).padStart(2, "0")} after refresh`));
|
|
81
82
|
}
|
|
82
83
|
});
|
|
83
84
|
});
|
|
@@ -139,7 +140,7 @@ async function readDeviceDetails(device, address, advertisedInfo) {
|
|
|
139
140
|
// out of range) should still show up in the scan with whatever the advertisement itself
|
|
140
141
|
// carried, just without battery/PID-by-read. Logged so a blank battery column has a reason
|
|
141
142
|
// instead of looking like the read was simply never attempted.
|
|
142
|
-
console.error(
|
|
143
|
+
console.error(`${pluginVersion_1.PLUGIN_NAME}: zhsunyco [${address}]: battery/config read failed: ${err.message}`);
|
|
143
144
|
return fallback;
|
|
144
145
|
}
|
|
145
146
|
};
|
|
@@ -150,7 +151,7 @@ async function readDeviceDetails(device, address, advertisedInfo) {
|
|
|
150
151
|
return Promise.race([
|
|
151
152
|
read(),
|
|
152
153
|
(0, bleDiscovery_1.sleep)(SCAN_CONNECT_TIMEOUT_MS * 2).then(() => {
|
|
153
|
-
console.error(
|
|
154
|
+
console.error(`${pluginVersion_1.PLUGIN_NAME}: zhsunyco [${address}]: battery/config read timed out after ${SCAN_CONNECT_TIMEOUT_MS * 2}ms`);
|
|
154
155
|
return fallback;
|
|
155
156
|
}),
|
|
156
157
|
]);
|
|
@@ -26,7 +26,7 @@ exports.ZHSUNYCO_PID_METADATA = [
|
|
|
26
26
|
width: 200,
|
|
27
27
|
height: 200,
|
|
28
28
|
voffset: 0,
|
|
29
|
-
colours: [
|
|
29
|
+
colours: ["black", "white", "red", "yellow"],
|
|
30
30
|
},
|
|
31
31
|
{
|
|
32
32
|
pid: 0x000a,
|
|
@@ -34,7 +34,7 @@ exports.ZHSUNYCO_PID_METADATA = [
|
|
|
34
34
|
width: 250,
|
|
35
35
|
height: 128,
|
|
36
36
|
voffset: 0,
|
|
37
|
-
colours: [
|
|
37
|
+
colours: ["black", "white", "red", "yellow"],
|
|
38
38
|
},
|
|
39
39
|
{
|
|
40
40
|
pid: 0x000e,
|
|
@@ -42,34 +42,34 @@ exports.ZHSUNYCO_PID_METADATA = [
|
|
|
42
42
|
width: 416,
|
|
43
43
|
height: 240,
|
|
44
44
|
voffset: 0,
|
|
45
|
-
colours: [
|
|
45
|
+
colours: ["black", "white", "red", "yellow"],
|
|
46
46
|
},
|
|
47
47
|
{
|
|
48
48
|
pid: 0x000e,
|
|
49
|
-
hwVersion:
|
|
49
|
+
hwVersion: "0103",
|
|
50
50
|
label: '2.13"',
|
|
51
51
|
width: 250,
|
|
52
52
|
height: 128,
|
|
53
53
|
voffset: 0,
|
|
54
|
-
colours: [
|
|
54
|
+
colours: ["black", "white", "red", "yellow"],
|
|
55
55
|
},
|
|
56
56
|
{
|
|
57
57
|
pid: 0x000e,
|
|
58
|
-
hwVersion:
|
|
58
|
+
hwVersion: "0201",
|
|
59
59
|
label: '3.5"',
|
|
60
60
|
width: 384,
|
|
61
61
|
height: 184,
|
|
62
62
|
voffset: 0,
|
|
63
|
-
colours: [
|
|
63
|
+
colours: ["black", "white", "red", "yellow"],
|
|
64
64
|
},
|
|
65
65
|
{
|
|
66
66
|
pid: 0x000e,
|
|
67
|
-
hwVersion:
|
|
67
|
+
hwVersion: "0203",
|
|
68
68
|
label: '7.5"',
|
|
69
69
|
width: 800,
|
|
70
70
|
height: 480,
|
|
71
71
|
voffset: 0,
|
|
72
|
-
colours: [
|
|
72
|
+
colours: ["black", "white", "red", "yellow"],
|
|
73
73
|
},
|
|
74
74
|
{
|
|
75
75
|
pid: 0x0012,
|
|
@@ -77,7 +77,7 @@ exports.ZHSUNYCO_PID_METADATA = [
|
|
|
77
77
|
width: 296,
|
|
78
78
|
height: 128,
|
|
79
79
|
voffset: 0,
|
|
80
|
-
colours: [
|
|
80
|
+
colours: ["black", "white", "red", "yellow"],
|
|
81
81
|
},
|
|
82
82
|
{
|
|
83
83
|
pid: 0x0016,
|
|
@@ -85,7 +85,7 @@ exports.ZHSUNYCO_PID_METADATA = [
|
|
|
85
85
|
width: 400,
|
|
86
86
|
height: 300,
|
|
87
87
|
voffset: 0,
|
|
88
|
-
colours: [
|
|
88
|
+
colours: ["black", "white", "red", "yellow"],
|
|
89
89
|
},
|
|
90
90
|
{
|
|
91
91
|
pid: 0x001a,
|
|
@@ -93,6 +93,6 @@ exports.ZHSUNYCO_PID_METADATA = [
|
|
|
93
93
|
width: 648,
|
|
94
94
|
height: 480,
|
|
95
95
|
voffset: 0,
|
|
96
|
-
colours: [
|
|
96
|
+
colours: ["black", "white", "red", "yellow"],
|
|
97
97
|
},
|
|
98
98
|
];
|
|
@@ -12,15 +12,15 @@ const crypto_1 = require("crypto");
|
|
|
12
12
|
* Wolink ESL GATT service ("WOLINKBLEESL2020") and characteristic UUIDs, transcribed
|
|
13
13
|
* from the reference driver (examples/device_driver/zhunyco/wolink_ble.py).
|
|
14
14
|
*/
|
|
15
|
-
exports.WOLINK_SERVICE_UUID =
|
|
15
|
+
exports.WOLINK_SERVICE_UUID = "30323032-4c53-4545-4c42-4b4e494c4f57";
|
|
16
16
|
/** BLE manufacturer ID Zhsunyco/Wolink devices advertise under. */
|
|
17
17
|
exports.ZHSUNYCO_MANUFACTURER_ID = 0xbbaa;
|
|
18
18
|
exports.WOLINK_CHARACTERISTIC_UUIDS = {
|
|
19
|
-
data:
|
|
20
|
-
config:
|
|
21
|
-
authenticate:
|
|
22
|
-
status:
|
|
23
|
-
battery:
|
|
19
|
+
data: "31323032-4c53-4545-4c42-4b4e494c4f57",
|
|
20
|
+
config: "32323032-4c53-4545-4c42-4b4e494c4f57",
|
|
21
|
+
authenticate: "33323032-4c53-4545-4c42-4b4e494c4f57",
|
|
22
|
+
status: "34323032-4c53-4545-4c42-4b4e494c4f57",
|
|
23
|
+
battery: "35323032-4c53-4545-4c42-4b4e494c4f57",
|
|
24
24
|
};
|
|
25
25
|
exports.COMMAND = {
|
|
26
26
|
uploadBlock: 0xa500,
|
|
@@ -33,8 +33,8 @@ function decodeAdvertisedInfo(data) {
|
|
|
33
33
|
}
|
|
34
34
|
return {
|
|
35
35
|
pid: data.readUInt16BE(2),
|
|
36
|
-
appVersion: data.readUInt16BE(4).toString(16).padStart(4,
|
|
37
|
-
hwVersion: data.readUInt16BE(6).toString(16).padStart(4,
|
|
36
|
+
appVersion: data.readUInt16BE(4).toString(16).padStart(4, "0"),
|
|
37
|
+
hwVersion: data.readUInt16BE(6).toString(16).padStart(4, "0"),
|
|
38
38
|
};
|
|
39
39
|
}
|
|
40
40
|
function decodeBatteryMv(data) {
|
|
@@ -60,7 +60,7 @@ const AES_CHALLENGE_LENGTH = 16;
|
|
|
60
60
|
exports.DEFAULT_BLE_AUTH = [155, 96, 159, 40, 188, 73, 226, 87, 41, 189, 123, 141, 242, 43, 68, 32];
|
|
61
61
|
/** Resolves the configured per-device hex key, falling back to `DEFAULT_BLE_AUTH`. */
|
|
62
62
|
function resolveAesKey(aesKeyHex) {
|
|
63
|
-
return aesKeyHex ? Buffer.from(aesKeyHex,
|
|
63
|
+
return aesKeyHex ? Buffer.from(aesKeyHex, "hex") : Buffer.from(exports.DEFAULT_BLE_AUTH);
|
|
64
64
|
}
|
|
65
65
|
/**
|
|
66
66
|
* Encrypts the device's auth challenge with the AES-128 key (CBC, zero IV).
|
|
@@ -77,7 +77,7 @@ function authResponse(challenge, key) {
|
|
|
77
77
|
if (challenge.length !== AES_CHALLENGE_LENGTH) {
|
|
78
78
|
throw new Error(`zhsunyco auth challenge expected ${AES_CHALLENGE_LENGTH} bytes, got ${challenge.length}`);
|
|
79
79
|
}
|
|
80
|
-
const cipher = (0, crypto_1.createCipheriv)(
|
|
80
|
+
const cipher = (0, crypto_1.createCipheriv)("aes-128-cbc", key, Buffer.alloc(16));
|
|
81
81
|
cipher.setAutoPadding(false);
|
|
82
82
|
return Buffer.concat([cipher.update(challenge), cipher.final()]);
|
|
83
83
|
}
|
package/dist/httpJson.js
CHANGED
|
@@ -10,7 +10,7 @@ exports.fetchJson = fetchJson;
|
|
|
10
10
|
function describeCause(cause) {
|
|
11
11
|
const errors = cause.errors;
|
|
12
12
|
if (Array.isArray(errors)) {
|
|
13
|
-
return errors.map(describeCause).join(
|
|
13
|
+
return errors.map(describeCause).join("; ");
|
|
14
14
|
}
|
|
15
15
|
if (cause instanceof Error) {
|
|
16
16
|
return cause.message || cause.code || cause.toString();
|
|
@@ -33,8 +33,8 @@ async function fetchJson(url) {
|
|
|
33
33
|
throw new Error(`fetch failed: ${url} - ${detail}`);
|
|
34
34
|
}
|
|
35
35
|
if (!response.ok) {
|
|
36
|
-
const body = await response.text().catch(() =>
|
|
37
|
-
throw new Error(`fetch failed: ${url} (${response.status} ${response.statusText})${body ? ` - ${body}` :
|
|
36
|
+
const body = await response.text().catch(() => "");
|
|
37
|
+
throw new Error(`fetch failed: ${url} (${response.status} ${response.statusText})${body ? ` - ${body}` : ""}`);
|
|
38
38
|
}
|
|
39
39
|
return response.json();
|
|
40
40
|
}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { ServerAPI, Plugin } from
|
|
2
|
-
import { registerDriver, getDriver, allDrivers } from
|
|
3
|
-
import type { VendorDriver as VendorDriverType, DeviceMetadata as DeviceMetadataType, DiscoveredDevice as DiscoveredDeviceType, VendorDeviceConfig as VendorDeviceConfigType, Colour as ColourType } from
|
|
1
|
+
import { ServerAPI, Plugin } from "@signalk/server-api";
|
|
2
|
+
import { registerDriver, getDriver, allDrivers } from "./devices/registry";
|
|
3
|
+
import type { VendorDriver as VendorDriverType, DeviceMetadata as DeviceMetadataType, DiscoveredDevice as DiscoveredDeviceType, VendorDeviceConfig as VendorDeviceConfigType, Colour as ColourType } from "./devices/types";
|
|
4
4
|
/**
|
|
5
5
|
* Public extension point for vendor packages. A package that adds support for a new
|
|
6
6
|
* ESL vendor (e.g. `signalk-esl-shoplabelcorp-plugin`) imports this module and calls
|
package/dist/pathMeta.d.ts
CHANGED
package/dist/pathMeta.js
CHANGED
|
@@ -4,7 +4,7 @@ exports.fetchPathMeta = fetchPathMeta;
|
|
|
4
4
|
const httpJson_1 = require("./httpJson");
|
|
5
5
|
/** `self` -> `vessels/self`, `vessels.urn:mrn:imo:mmsi:1` -> `vessels/urn:mrn:imo:mmsi:1` - matches the REST path for that context's whole-vessel metadata. */
|
|
6
6
|
function metaContextPath(context) {
|
|
7
|
-
return context ===
|
|
7
|
+
return context === "self" ? "vessels/self" : context.replace(/\./g, "/");
|
|
8
8
|
}
|
|
9
9
|
/**
|
|
10
10
|
* Fetches all of a vessel's per-path metadata in one request - `GET .../vessels/<context>/meta` returns
|
package/dist/plugin.d.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { Plugin, ServerAPI } from
|
|
1
|
+
import { Plugin, ServerAPI } from "@signalk/server-api";
|
|
2
2
|
export declare function createPlugin(app: ServerAPI): Plugin;
|