@rhizomatics/signalk-einklabel-plugin 0.6.4 → 0.7.1
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 +22 -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 +16 -38
- 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/render/unwrapSignalkTree.d.ts +15 -0
- package/dist/render/unwrapSignalkTree.js +28 -0
- package/dist/repaintScheduler.d.ts +2 -2
- package/dist/repaintScheduler.js +37 -27
- 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/liveContext.js
CHANGED
|
@@ -8,38 +8,16 @@ exports.assembleExampleContext = assembleExampleContext;
|
|
|
8
8
|
const promises_1 = require("fs/promises");
|
|
9
9
|
const path_1 = __importDefault(require("path"));
|
|
10
10
|
const formatters_1 = require("../render/formatters");
|
|
11
|
+
const unwrapSignalkTree_1 = require("../render/unwrapSignalkTree");
|
|
11
12
|
const httpJson_1 = require("../httpJson");
|
|
12
13
|
const unitCategories_1 = require("../unitCategories");
|
|
13
14
|
const pathMeta_1 = require("../pathMeta");
|
|
14
15
|
const pluginVersion_1 = require("../pluginVersion");
|
|
15
16
|
const log_1 = require("./log");
|
|
16
|
-
const RESOURCES_API_PATH =
|
|
17
|
+
const RESOURCES_API_PATH = "/signalk/v2/api/resources";
|
|
17
18
|
/** `context=self` -> `vessels/self`, `context=vessels.urn:mrn:imo:mmsi:1` -> `vessels/urn:mrn:imo:mmsi:1` - matches the REST path for that context's full-data-model subtree. */
|
|
18
19
|
function contextPath(context) {
|
|
19
|
-
return context ===
|
|
20
|
-
}
|
|
21
|
-
/**
|
|
22
|
-
* `GET .../vessels/self` (or any other subtree) returns SignalK's full delta-tree shape, where every
|
|
23
|
-
* leaf is wrapped as `{ value, $source, timestamp, ... }` rather than the bare value - unlike
|
|
24
|
-
* `app.getSelfPath`/`getPath` in the live plugin (repaintScheduler.ts), which already return bare
|
|
25
|
-
* values. Recursively unwraps every such leaf so `path=` bindings resolve the same way over HTTP as
|
|
26
|
-
* they do live. Only applied to `signalk` fetches - `resources` responses have no such wrapper.
|
|
27
|
-
*
|
|
28
|
-
* Keys off `value` alone, not also requiring `timestamp`/`$source` - not every server includes both on
|
|
29
|
-
* every leaf, and a `value` key is otherwise meaningless on a SignalK data-model node (it isn't a
|
|
30
|
-
* regular vessel property name), so there's no real risk of unwrapping something that isn't this
|
|
31
|
-
* wrapper.
|
|
32
|
-
*/
|
|
33
|
-
function unwrapSignalkTree(node) {
|
|
34
|
-
if (node === null || typeof node !== 'object')
|
|
35
|
-
return node;
|
|
36
|
-
if (Array.isArray(node))
|
|
37
|
-
return node.map(unwrapSignalkTree);
|
|
38
|
-
const obj = node;
|
|
39
|
-
if ('value' in obj) {
|
|
40
|
-
return obj.value;
|
|
41
|
-
}
|
|
42
|
-
return Object.fromEntries(Object.entries(obj).map(([key, value]) => [key, unwrapSignalkTree(value)]));
|
|
20
|
+
return context === "self" ? "vessels/self" : context.replace(/\./g, "/");
|
|
43
21
|
}
|
|
44
22
|
/**
|
|
45
23
|
* CLI counterpart to `assembleRawContext` + `considerRepaint`'s `meta` injection in
|
|
@@ -52,11 +30,11 @@ function unwrapSignalkTree(node) {
|
|
|
52
30
|
*/
|
|
53
31
|
async function assembleLiveContext(signalkUrl, bindings) {
|
|
54
32
|
const signalk = {};
|
|
55
|
-
const contexts = new Set(bindings.filter((binding) => binding.source ===
|
|
33
|
+
const contexts = new Set(bindings.filter((binding) => binding.source === "signalk").map((binding) => binding.context));
|
|
56
34
|
for (const context of contexts) {
|
|
57
35
|
const url = `${signalkUrl}/signalk/v1/api/${contextPath(context)}`;
|
|
58
36
|
(0, log_1.logDebug)(`GET ${url}`);
|
|
59
|
-
signalk[context] = unwrapSignalkTree(await (0, httpJson_1.fetchJson)(url));
|
|
37
|
+
signalk[context] = (0, unwrapSignalkTree_1.unwrapSignalkTree)(await (0, httpJson_1.fetchJson)(url));
|
|
60
38
|
}
|
|
61
39
|
const pathMeta = {};
|
|
62
40
|
for (const context of contexts) {
|
|
@@ -69,7 +47,7 @@ async function assembleLiveContext(signalkUrl, bindings) {
|
|
|
69
47
|
}
|
|
70
48
|
}
|
|
71
49
|
const resources = {};
|
|
72
|
-
const resourceNames = new Set(bindings.filter((binding) => binding.source ===
|
|
50
|
+
const resourceNames = new Set(bindings.filter((binding) => binding.source === "resources").map((binding) => binding.resource));
|
|
73
51
|
for (const name of resourceNames) {
|
|
74
52
|
const url = `${signalkUrl}${RESOURCES_API_PATH}/${name}`;
|
|
75
53
|
(0, log_1.logDebug)(`GET ${url}`);
|
|
@@ -89,7 +67,7 @@ async function assembleLiveContext(signalkUrl, bindings) {
|
|
|
89
67
|
async function readJsonFile(filePath) {
|
|
90
68
|
let raw;
|
|
91
69
|
try {
|
|
92
|
-
raw = await (0, promises_1.readFile)(filePath,
|
|
70
|
+
raw = await (0, promises_1.readFile)(filePath, "utf-8");
|
|
93
71
|
}
|
|
94
72
|
catch (err) {
|
|
95
73
|
throw new Error(`could not read example data file ${filePath} - ${err.message}`);
|
|
@@ -116,34 +94,34 @@ async function readJsonFile(filePath) {
|
|
|
116
94
|
*/
|
|
117
95
|
async function assembleExampleContext(examplesDir, bindings) {
|
|
118
96
|
const signalk = {};
|
|
119
|
-
const contexts = new Set(bindings.filter((binding) => binding.source ===
|
|
97
|
+
const contexts = new Set(bindings.filter((binding) => binding.source === "signalk").map((binding) => binding.context));
|
|
120
98
|
if (contexts.size > 0) {
|
|
121
|
-
const vesselsPath = path_1.default.join(examplesDir,
|
|
99
|
+
const vesselsPath = path_1.default.join(examplesDir, "vessels.json");
|
|
122
100
|
(0, log_1.logDebug)(`reading ${vesselsPath}`);
|
|
123
101
|
const vessels = await readJsonFile(vesselsPath);
|
|
124
102
|
for (const context of contexts) {
|
|
125
|
-
const segments = contextPath(context).split(
|
|
103
|
+
const segments = contextPath(context).split("/");
|
|
126
104
|
let node = vessels;
|
|
127
105
|
for (const segment of segments) {
|
|
128
|
-
node = node === null || typeof node !==
|
|
106
|
+
node = node === null || typeof node !== "object" ? undefined : node[segment];
|
|
129
107
|
}
|
|
130
108
|
if (node === undefined) {
|
|
131
|
-
throw new Error(`example data file ${vesselsPath} has no "${segments.join(
|
|
109
|
+
throw new Error(`example data file ${vesselsPath} has no "${segments.join(".")}" subtree for context "${context}"`);
|
|
132
110
|
}
|
|
133
|
-
signalk[context] = unwrapSignalkTree(node);
|
|
111
|
+
signalk[context] = (0, unwrapSignalkTree_1.unwrapSignalkTree)(node);
|
|
134
112
|
}
|
|
135
113
|
}
|
|
136
114
|
const resources = {};
|
|
137
|
-
const resourceNames = new Set(bindings.filter((binding) => binding.source ===
|
|
115
|
+
const resourceNames = new Set(bindings.filter((binding) => binding.source === "resources").map((binding) => binding.resource));
|
|
138
116
|
for (const name of resourceNames) {
|
|
139
|
-
const resourcePath = path_1.default.join(examplesDir,
|
|
117
|
+
const resourcePath = path_1.default.join(examplesDir, "resources", `${name}.json`);
|
|
140
118
|
(0, log_1.logDebug)(`reading ${resourcePath}`);
|
|
141
119
|
resources[name] = await readJsonFile(resourcePath);
|
|
142
120
|
}
|
|
143
121
|
const categoryNames = new Set(bindings.filter((binding) => binding.category).map((binding) => binding.category));
|
|
144
122
|
let categories = {};
|
|
145
123
|
if (categoryNames.size > 0) {
|
|
146
|
-
const categoriesPath = path_1.default.join(examplesDir,
|
|
124
|
+
const categoriesPath = path_1.default.join(examplesDir, "categories.json");
|
|
147
125
|
(0, log_1.logDebug)(`reading ${categoriesPath}`);
|
|
148
126
|
categories = (await readJsonFile(categoriesPath));
|
|
149
127
|
for (const category of categoryNames) {
|
package/dist/cli/log.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export type LogLevel =
|
|
1
|
+
export type LogLevel = "info" | "debug";
|
|
2
2
|
/** Set once from the global `--log-level` option (see index.ts's `preAction` hook) - applies to every command. */
|
|
3
3
|
export declare function setLogLevel(level: string): void;
|
|
4
4
|
/** Internal tracing (e.g. which URL is being fetched) - silent unless --log-level debug, so default output stays uncluttered. */
|
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
|
]);
|