@rhizomatics/signalk-einklabel-plugin 0.6.3 → 0.6.4

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 CHANGED
@@ -1,3 +1,8 @@
1
+ # 0.6.4
2
+
3
+ - Log improvements for device paint requests
4
+ - CLI scan now shows `hwid`, and adds reason for failing to retrieve `battery` level
5
+
1
6
  # 0.6.3
2
7
 
3
8
  - Improve logging if an image cannot be found in an `assets` directory to match a SignalK path value
package/README.md CHANGED
@@ -7,7 +7,7 @@
7
7
  [![code style: prettier](https://img.shields.io/badge/code_style-prettier-ff69b4.svg)](https://github.com/prettier/prettier)
8
8
  [![License](https://img.shields.io/badge/License-Apache_2.0-blue.svg)](https://github.com/rhizomatics/signalk-einklabel-plugin/blob/main/LICENSE)
9
9
 
10
- ** Fully working but limited vendor/product support **
10
+ ** Fully working but limited vendor/product support and requires Linux for device access **
11
11
 
12
12
  A SignalK plugin to display data from SignalK paths, APIs and plugins on Electronic Shelf Labels (ESL) over a Bluetooth Low Energy (BLE) connection using simple SVG templates.
13
13
 
@@ -25,7 +25,9 @@ Unlike some eInk projects, this plugin doesn't require any physical modification
25
25
 
26
26
  Most of these requirements are about making SignalK work with Bluetooth Low Energy, which is good thing to have anyway, since vendors like Victron, Switchbot, Ruuvi and others have BLE enabled hardware that's useful to have on a boat. [Direct BLE support](https://github.com/SignalK/signalk-server/issues/2411) in SignalK is being planned in 2026.
27
27
 
28
- 1. A SignalK server, preferably running Linux (MacOS does weird things with bluetooth)
28
+ 1. A SignalK server, **running Linux**
29
+ - MacOS and Windows aren't supported by the [BLE interface layer](https://www.npmjs.com/package/@naugehyde/node-ble), however can be used for template development and
30
+ debugging (everything except `scan` and `paint`)
29
31
  2. A Bluetooth adapter, that can handle BLE (Bluetooth Low Energy), which is Bluetooth v4.0 or higher
30
32
 
31
33
  - Bluetooth adapters for Linux can be tricky, TP-Link UB400 and Asus USB-BT500 are two well-known and available ones
@@ -73,6 +75,24 @@ The tide clock needs the [signalk-tides](https://github.com/openwatersio/signalk
73
75
  To show the lunar phase, the `environment.moon.phaseName` path is required, which can
74
76
  be easily achieved by installing and configuring the `derived-data` plugin.
75
77
 
78
+ ## Scanning for Devices
79
+
80
+ Since these are ultra-low power devices, they don't respond instantly to either identify themselves or accept a new image. By default, both scanning and painting have time-outs to wait for a response, which can be altered in the plugin configuration or CLI argument.
81
+
82
+ One other quirk is that some devices respond with a different name at different times, for example the genric `WOESL` sometimes and model specific `WL17500C74` other times. However, the MAC address, e.g. `66:66:17:50:0D:2B` is constant, and this is what's tracked by the plugin.
83
+
84
+ The plugin will optionally re-scan whenever it starts up, although this isn't essential once a label has been configured.
85
+
86
+ ### Scans from CLI
87
+
88
+ The command line tools, run from inside the `.signalk` directory, can be used to help troubleshoot
89
+
90
+ - Scan for longer, in this example 90 seconds
91
+ - ```npx esl-cli scan -d 90```
92
+ - Scan for all BLE devices, whatever they are
93
+ - ```npx esl-cli scan -a```
94
+
95
+
76
96
  ## Templating
77
97
 
78
98
  Templates are simply SVG files, to which expressions can be added to use SignalK data, with options to make it easier to read, like rounding or simplifying dates and times. The template can have sample data in the placeholder, so is easy to layout and visualize.
package/dist/cli/index.js CHANGED
@@ -114,7 +114,7 @@ program
114
114
  .option('-a, --all-devices', "list every nearby BLE device, not just ones a registered driver recognised - unmatched devices show address/name/mfr/rssi only, since there's no driver to do a vendor-specific read like battery")
115
115
  .action(async (opts) => {
116
116
  const durationMs = Number(opts.duration) * 1000;
117
- const header = ['vendor', 'address', 'name', 'pid', 'label', 'mfr', 'battery', 'rssi'];
117
+ const header = ['vendor', 'address', 'name', 'pid', 'hwid', 'label', 'mfr', 'battery', 'rssi'];
118
118
  const rows = [];
119
119
  let matchedCount = 0;
120
120
  const drivers = (0, registry_1.allDrivers)();
@@ -129,7 +129,7 @@ program
129
129
  .getRSSI()
130
130
  .then((value) => (value === undefined ? undefined : Number(value)))
131
131
  .catch(() => undefined);
132
- rows.push(['(unmatched)', address, name ?? '', '', '', mfr, '', String(rssi ?? '')]);
132
+ rows.push(['(unmatched)', address, name ?? '', '', '', '', mfr, '', String(rssi ?? '')]);
133
133
  }
134
134
  return;
135
135
  }
@@ -137,9 +137,10 @@ program
137
137
  const found = await driver.identifyDevice(device, address, name, manufacturerId, manufacturerData);
138
138
  (0, log_1.logDebug)(`${driver.vendor}: identified ${found.name ?? found.address}`);
139
139
  const pid = found.pid !== undefined ? `0x${found.pid.toString(16).padStart(4, '0')}` : '';
140
+ const hwid = found.hwVersion ? `0x${found.hwVersion}` : '';
140
141
  const label = found.metadata?.label ?? '';
141
142
  const battery = found.batteryMv !== undefined ? `${found.batteryMv}mV` : '';
142
- rows.push([driver.vendor, found.address, found.name ?? '', pid, label, mfr, battery, String(found.rssi ?? '')]);
143
+ rows.push([driver.vendor, found.address, found.name ?? '', pid, hwid, label, mfr, battery, String(found.rssi ?? '')]);
143
144
  });
144
145
  });
145
146
  if (matchedCount === 0) {
@@ -192,7 +193,7 @@ program
192
193
  const connectTimeoutMs = Number(opts.connectTimeout) * 1000;
193
194
  await (0, bleDiscovery_1.withRetries)(Number(opts.retries), async (attempt) => {
194
195
  if (attempt > 1) {
195
- (0, log_1.logDebug)(`paint attempt ${attempt}/${opts.retries}`);
196
+ (0, log_1.logDebug)(`Attempting paint ${attempt}/${opts.retries}`);
196
197
  }
197
198
  await driver.paint(bitmap, { address: opts.address, aesKey: opts.aesKey, modelOverride, connectTimeoutMs });
198
199
  });
@@ -31,7 +31,7 @@ class ZhsunycoDriver {
31
31
  }
32
32
  async identifyDevice(device, address, name, manufacturerId, manufacturerData) {
33
33
  const advertisedInfo = manufacturerData ? (0, protocol_1.decodeAdvertisedInfo)(manufacturerData) : undefined;
34
- const { info, batteryMv } = await readDeviceDetails(device, advertisedInfo);
34
+ const { info, batteryMv } = await readDeviceDetails(device, address, advertisedInfo);
35
35
  return {
36
36
  address,
37
37
  name,
@@ -113,7 +113,7 @@ exports.ZhsunycoDriver = ZhsunycoDriver;
113
113
  * config characteristic `paint()` reads, just to identify the device rather than to size a
114
114
  * render.
115
115
  */
116
- async function readDeviceDetails(device, advertisedInfo) {
116
+ async function readDeviceDetails(device, address, advertisedInfo) {
117
117
  const fallback = { info: advertisedInfo, batteryMv: undefined };
118
118
  const read = async () => {
119
119
  try {
@@ -134,7 +134,12 @@ async function readDeviceDetails(device, advertisedInfo) {
134
134
  await device.disconnect();
135
135
  }
136
136
  }
137
- catch {
137
+ catch (err) {
138
+ // Swallowed rather than thrown - a device that refuses this connect (e.g. busy elsewhere,
139
+ // out of range) should still show up in the scan with whatever the advertisement itself
140
+ // carried, just without battery/PID-by-read. Logged so a blank battery column has a reason
141
+ // instead of looking like the read was simply never attempted.
142
+ console.error(`zhsunyco [${address}]: battery/config read failed: ${err.message}`);
138
143
  return fallback;
139
144
  }
140
145
  };
@@ -142,5 +147,11 @@ async function readDeviceDetails(device, advertisedInfo) {
142
147
  // `getPrimaryService`/`readValue`) has no timeout of its own either - race the whole read so
143
148
  // one unresponsive device can't stall the rest of the scan (see `plugin.ts`'s `scanInProgress`,
144
149
  // 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)]);
150
+ return Promise.race([
151
+ read(),
152
+ (0, bleDiscovery_1.sleep)(SCAN_CONNECT_TIMEOUT_MS * 2).then(() => {
153
+ console.error(`zhsunyco [${address}]: battery/config read timed out after ${SCAN_CONNECT_TIMEOUT_MS * 2}ms`);
154
+ return fallback;
155
+ }),
156
+ ]);
146
157
  }
@@ -149,18 +149,21 @@ async function considerRepaint(app, config, device, state, getApiUrl) {
149
149
  const renderer = new svgRenderer_1.SvgRenderer();
150
150
  const bitmap = await renderer.render(templatePath, renderContext, metadata.width, metadata.height - metadata.voffset);
151
151
  const connectTimeoutMs = config.paintConnectTimeoutSeconds * 1000;
152
+ let paintDurationMs = 0;
152
153
  await (0, bleDiscovery_1.withRetries)(config.paintRetries, async (attempt) => {
153
154
  if (attempt > 1) {
154
- app.debug(`"${device.friendlyName}": paint attempt ${attempt}/${config.paintRetries}`);
155
+ app.debug(`"${device.friendlyName}": attempting paint ${attempt}/${config.paintRetries}`);
155
156
  }
157
+ const startedAt = Date.now();
156
158
  await driver.paint(bitmap, { address: model.address, aesKey: device.aesKey, connectTimeoutMs });
159
+ paintDurationMs = Date.now() - startedAt;
157
160
  });
158
161
  state[device.friendlyName] = { hash };
159
162
  saveState(app, state);
160
163
  if (device.forceRepaint) {
161
164
  clearForceRepaint(app, device.friendlyName);
162
165
  }
163
- app.debug(`"${device.friendlyName}": repainted`);
166
+ app.debug(`"${device.friendlyName}": repainted (paint took ${paintDurationMs}ms)`);
164
167
  }
165
168
  function startRepaintScheduler(app, config) {
166
169
  const state = loadState(app);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rhizomatics/signalk-einklabel-plugin",
3
- "version": "0.6.3",
3
+ "version": "0.6.4",
4
4
  "description": "Display SignalK data on eInk Electronic Shelf Labels",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",