@rhizomatics/signalk-einklabel-plugin 0.6.3 → 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.
Files changed (56) hide show
  1. package/CHANGELOG.md +23 -0
  2. package/README.md +37 -8
  3. package/dist/cli/index.js +130 -96
  4. package/dist/cli/liveContext.d.ts +2 -2
  5. package/dist/cli/liveContext.js +15 -15
  6. package/dist/cli/log.d.ts +1 -1
  7. package/dist/cli/log.js +4 -4
  8. package/dist/config.d.ts +12 -3
  9. package/dist/config.js +71 -49
  10. package/dist/devices/bleDiscovery.d.ts +1 -1
  11. package/dist/devices/bleDiscovery.js +9 -3
  12. package/dist/devices/registry.d.ts +1 -1
  13. package/dist/devices/types.d.ts +4 -4
  14. package/dist/devices/zhsunyco/encode.d.ts +2 -2
  15. package/dist/devices/zhsunyco/index.d.ts +3 -3
  16. package/dist/devices/zhsunyco/index.js +23 -11
  17. package/dist/devices/zhsunyco/metadata.d.ts +1 -1
  18. package/dist/devices/zhsunyco/metadata.js +12 -12
  19. package/dist/devices/zhsunyco/protocol.js +10 -10
  20. package/dist/httpJson.js +3 -3
  21. package/dist/index.d.ts +3 -3
  22. package/dist/pathMeta.d.ts +1 -1
  23. package/dist/pathMeta.js +1 -1
  24. package/dist/plugin.d.ts +1 -1
  25. package/dist/plugin.js +16 -13
  26. package/dist/pluginVersion.d.ts +9 -0
  27. package/dist/pluginVersion.js +12 -2
  28. package/dist/render/assets.d.ts +14 -6
  29. package/dist/render/assets.js +65 -10
  30. package/dist/render/binding.d.ts +9 -2
  31. package/dist/render/binding.js +23 -23
  32. package/dist/render/fonts.js +6 -6
  33. package/dist/render/formatters.d.ts +1 -1
  34. package/dist/render/formatters.js +25 -22
  35. package/dist/render/png.d.ts +1 -1
  36. package/dist/render/svgRenderer.d.ts +9 -8
  37. package/dist/render/svgRenderer.js +33 -27
  38. package/dist/render/types.d.ts +1 -1
  39. package/dist/repaintScheduler.d.ts +2 -2
  40. package/dist/repaintScheduler.js +26 -22
  41. package/dist/resolveApiUrl.js +3 -3
  42. package/dist/unitCategories.d.ts +1 -1
  43. package/dist/unitCategories.js +3 -3
  44. package/docs/assets/screenshots/plugin_config.png +0 -0
  45. package/package.json +42 -39
  46. package/templates/assets/lunar_phases/README.md +18 -0
  47. package/templates/tide.svg +11 -11
  48. /package/docs/assets/{inkscape_desc.png → screenshots/inkscape_desc.png} +0 -0
  49. /package/{resources/svg → templates/assets}/lunar_phases/first_quarter.svg +0 -0
  50. /package/{resources/svg → templates/assets}/lunar_phases/full_moon.svg +0 -0
  51. /package/{resources/svg → templates/assets}/lunar_phases/new_moon.svg +0 -0
  52. /package/{resources/svg → templates/assets}/lunar_phases/third_quarter.svg +0 -0
  53. /package/{resources/svg → templates/assets}/lunar_phases/waning_crescent.svg +0 -0
  54. /package/{resources/svg → templates/assets}/lunar_phases/waning_gibbous.svg +0 -0
  55. /package/{resources/svg → templates/assets}/lunar_phases/waxing_crescent.svg +0 -0
  56. /package/{resources/svg → templates/assets}/lunar_phases/waxing_gibbous.svg +0 -0
package/dist/cli/log.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- export type LogLevel = 'info' | 'debug';
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 = ['info', 'debug'];
6
- let currentLevel = 'info';
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 === 'debug')
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 '@signalk/server-api';
2
- import { DiscoveredDevice } from './devices/types';
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: 'subscription' | 'interval';
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
- /** The package's own bundled `templates/` directory (ships alongside `dist/`, see package.json's `files`) - templates here are always available, but a same-named template in the user's `templatesDir` takes priority. */
14
- const BUNDLED_TEMPLATES_DIR = (0, path_1.join)(__dirname, '..', 'templates');
15
- const SIGNALK_HOME_DIR = (0, path_1.join)((0, os_1.homedir)(), '.signalk');
16
- const DEFAULT_TEMPLATES_DIR = (0, path_1.join)(SIGNALK_HOME_DIR, 'einklabel', 'templates');
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, '0')} (${found.address})`;
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 ?? '').split(':');
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('.svg'));
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: 'object',
119
+ type: "object",
112
120
  properties: {
113
121
  templatesDir: {
114
- type: 'string',
115
- title: 'Templates directory',
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
- 'with the same name as a bundled one takes priority.',
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: 'boolean',
123
- title: 'Scan for devices on plugin start',
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: 'number',
129
- title: 'Scan duration (seconds)',
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: 'number',
136
- title: 'Paint connect timeout (seconds)',
137
- description: 'How long to wait for a device to accept a BLE connection before giving up on a repaint attempt.',
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: 'number',
143
- title: 'Paint retries',
144
- description: 'How many times to attempt a repaint (including the first try) before giving up and reporting failure.',
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: 'string',
150
- title: 'SignalK API base URL (leave blank to auto-detect)',
151
- 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.',
152
- enum: ['', ...resolveApiUrl_1.SIGNALK_API_URL_OPTIONS],
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: 'array',
156
- title: 'Devices',
163
+ type: "array",
164
+ title: "Devices",
157
165
  items: {
158
- type: 'object',
159
- required: ['friendlyName', 'device', 'templateName', 'repaintTrigger'],
166
+ type: "object",
167
+ required: ["friendlyName", "device", "templateName", "repaintTrigger"],
160
168
  properties: {
161
- friendlyName: { type: 'string', title: 'Friendly name' },
169
+ friendlyName: { type: "string", title: "Friendly name" },
162
170
  device: withEnum({
163
- type: 'string',
164
- title: 'Device',
165
- description: 'Picked from devices found by a scan (plugin start, or `esl-cli scan`) - sets both the model and BLE address.',
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: 'string', title: 'Template' }, templateNameOptions(resolveTemplatesDir(current.templatesDir))),
168
- repaintTrigger: { type: 'string', title: 'Repaint trigger', enum: ['subscription', 'interval'] },
169
- triggerPath: { type: 'string', title: 'Trigger SignalK path (if repaint trigger is subscription)' },
170
- intervalHours: { type: 'number', title: 'Repaint every N hours (if repaint trigger is interval)', minimum: 1 },
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: 'number',
173
- title: 'Minutes past the hour (if repaint trigger is interval)',
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: { type: 'string', title: 'BLE AES key (vendor-specific; leave blank to use a default key)' },
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: 'boolean',
181
- title: 'Force repaint',
182
- description: 'Repaint even if the data is unchanged - clears itself automatically once that repaint completes',
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: { 'ui:widget': 'radio' },
217
+ repaintTrigger: { "ui:widget": "radio" },
196
218
  },
197
219
  },
198
220
  };
@@ -1,4 +1,4 @@
1
- import { Adapter, Bluetooth, Device } from '@naugehyde/node-ble';
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 !== 'linux') {
26
+ if (process.platform !== "linux") {
27
27
  throw new Error(`BLE support requires Linux (BlueZ over D-Bus); "${process.platform}" is not supported - ` +
28
- 'run this on the target Linux host (e.g. the NanoPi), not on macOS/Windows.');
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({ address, device, name, manufacturerId, manufacturerData: key === undefined ? undefined : manufacturerData[key] });
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 './types';
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[];
@@ -1,6 +1,6 @@
1
- import { Device } from '@naugehyde/node-ble';
2
- import { Bitmap } from '../render/types';
3
- export type Colour = 'black' | 'white' | 'red' | 'yellow';
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, 'pid'>;
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 '../../render/types';
2
- import { DeviceMetadata } from '../types';
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 '@naugehyde/node-ble';
2
- import { Bitmap } from '../../render/types';
3
- import { DeviceMetadata, DiscoveredDevice, VendorDeviceConfig, VendorDriver } from '../types';
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 = 'zhsunyco';
21
+ this.vendor = "zhsunyco";
21
22
  }
22
23
  matchesAdvertisement(name, manufacturerId) {
23
- return manufacturerId === protocol_1.ZHSUNYCO_MANUFACTURER_ID || (name ?? '').startsWith('WL') || (name ?? '').startsWith('WOESL');
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);
@@ -31,7 +32,7 @@ class ZhsunycoDriver {
31
32
  }
32
33
  async identifyDevice(device, address, name, manufacturerId, manufacturerData) {
33
34
  const advertisedInfo = manufacturerData ? (0, protocol_1.decodeAdvertisedInfo)(manufacturerData) : undefined;
34
- const { info, batteryMv } = await readDeviceDetails(device, advertisedInfo);
35
+ const { info, batteryMv } = await readDeviceDetails(device, address, advertisedInfo);
35
36
  return {
36
37
  address,
37
38
  name,
@@ -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('zhsunyco device did not return valid config data');
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, '0')} - ` +
71
- 'pass --width/--height/--voffset/--colours to describe it manually');
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('valuechanged', (data) => {
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, '0')} after refresh`));
81
+ reject(new Error(`zhsunyco device reported error 0x${errorCode.toString(16).padStart(2, "0")} after refresh`));
81
82
  }
82
83
  });
83
84
  });
@@ -113,7 +114,7 @@ exports.ZhsunycoDriver = ZhsunycoDriver;
113
114
  * config characteristic `paint()` reads, just to identify the device rather than to size a
114
115
  * render.
115
116
  */
116
- async function readDeviceDetails(device, advertisedInfo) {
117
+ async function readDeviceDetails(device, address, advertisedInfo) {
117
118
  const fallback = { info: advertisedInfo, batteryMv: undefined };
118
119
  const read = async () => {
119
120
  try {
@@ -134,7 +135,12 @@ async function readDeviceDetails(device, advertisedInfo) {
134
135
  await device.disconnect();
135
136
  }
136
137
  }
137
- catch {
138
+ catch (err) {
139
+ // Swallowed rather than thrown - a device that refuses this connect (e.g. busy elsewhere,
140
+ // out of range) should still show up in the scan with whatever the advertisement itself
141
+ // carried, just without battery/PID-by-read. Logged so a blank battery column has a reason
142
+ // instead of looking like the read was simply never attempted.
143
+ console.error(`${pluginVersion_1.PLUGIN_NAME}: zhsunyco [${address}]: battery/config read failed: ${err.message}`);
138
144
  return fallback;
139
145
  }
140
146
  };
@@ -142,5 +148,11 @@ async function readDeviceDetails(device, advertisedInfo) {
142
148
  // `getPrimaryService`/`readValue`) has no timeout of its own either - race the whole read so
143
149
  // one unresponsive device can't stall the rest of the scan (see `plugin.ts`'s `scanInProgress`,
144
150
  // which otherwise stays set forever and silently skips every later scan).
145
- return Promise.race([read(), (0, bleDiscovery_1.sleep)(SCAN_CONNECT_TIMEOUT_MS * 2).then(() => fallback)]);
151
+ return Promise.race([
152
+ read(),
153
+ (0, bleDiscovery_1.sleep)(SCAN_CONNECT_TIMEOUT_MS * 2).then(() => {
154
+ console.error(`${pluginVersion_1.PLUGIN_NAME}: zhsunyco [${address}]: battery/config read timed out after ${SCAN_CONNECT_TIMEOUT_MS * 2}ms`);
155
+ return fallback;
156
+ }),
157
+ ]);
146
158
  }
@@ -1,4 +1,4 @@
1
- import { DeviceMetadata } from '../types';
1
+ import { DeviceMetadata } from "../types";
2
2
  /**
3
3
  * Keyed by PID within the Zhsunyco/Wolink namespace only — this table is not shared
4
4
  * with other vendors' PID spaces.
@@ -26,7 +26,7 @@ exports.ZHSUNYCO_PID_METADATA = [
26
26
  width: 200,
27
27
  height: 200,
28
28
  voffset: 0,
29
- colours: ['black', 'white', 'red', 'yellow'],
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: ['black', 'white', 'red', 'yellow'],
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: ['black', 'white', 'red', 'yellow'],
45
+ colours: ["black", "white", "red", "yellow"],
46
46
  },
47
47
  {
48
48
  pid: 0x000e,
49
- hwVersion: '0103',
49
+ hwVersion: "0103",
50
50
  label: '2.13"',
51
51
  width: 250,
52
52
  height: 128,
53
53
  voffset: 0,
54
- colours: ['black', 'white', 'red', 'yellow'],
54
+ colours: ["black", "white", "red", "yellow"],
55
55
  },
56
56
  {
57
57
  pid: 0x000e,
58
- hwVersion: '0201',
58
+ hwVersion: "0201",
59
59
  label: '3.5"',
60
60
  width: 384,
61
61
  height: 184,
62
62
  voffset: 0,
63
- colours: ['black', 'white', 'red', 'yellow'],
63
+ colours: ["black", "white", "red", "yellow"],
64
64
  },
65
65
  {
66
66
  pid: 0x000e,
67
- hwVersion: '0203',
67
+ hwVersion: "0203",
68
68
  label: '7.5"',
69
69
  width: 800,
70
70
  height: 480,
71
71
  voffset: 0,
72
- colours: ['black', 'white', 'red', 'yellow'],
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: ['black', 'white', 'red', 'yellow'],
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: ['black', 'white', 'red', 'yellow'],
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: ['black', 'white', 'red', 'yellow'],
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 = '30323032-4c53-4545-4c42-4b4e494c4f57';
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: '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',
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, '0'),
37
- hwVersion: data.readUInt16BE(6).toString(16).padStart(4, '0'),
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, 'hex') : Buffer.from(exports.DEFAULT_BLE_AUTH);
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)('aes-128-cbc', key, Buffer.alloc(16));
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
  }