@rhizomatics/signalk-einklabel-plugin 0.7.1 → 0.8.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 CHANGED
@@ -1,3 +1,12 @@
1
+ # 0.8.0
2
+
3
+ - New `settle` time, configurable and defaulting to 120 seconds, to wait after plugin startup before attempting to access paths
4
+ - First few minutes of a SignalK server startup can be a mess of plugins missing their dependencies and logging errors.
5
+ - Schedule correctly picks up on previous schedule at startup, only repainting if the last scheduled time was overdue
6
+ - Path based subscriptions ignore temporarily missing paths, so displays don't waste their tiny batteries on flapping values
7
+ - Documentation now has a FAQ plus lots of other ESL/eInk hacking links.
8
+
9
+
1
10
  # 0.7.1
2
11
 
3
12
  - Fix path value retrieved for Image Fields when running in live plugin
package/README.md CHANGED
@@ -7,7 +7,7 @@
7
7
  [![code style: oxfmt](https://img.shields.io/badge/code_style-oxfmt-blue.svg)](https://github.com)
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 and requires Linux for device access **
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
 
@@ -48,7 +48,7 @@ Most of these requirements are about making SignalK work with Bluetooth Low Ener
48
48
 
49
49
  5. Correct time zone set on server if local time is to be shown on display
50
50
 
51
- - Use `raspi-config` on a Raspberry Pi, or `timedatectl` on a Linux server
51
+ - See [FAQ](#faq-timezone)
52
52
  - If not set, everything will work, but you may see the wrong zone or not have daylight savings applied
53
53
 
54
54
  Once you have all of that, it may be worth also installing [signalk-victron-ble](https://github.com/stefanor/signalk-victron-ble) or [bt-sensors-plugin](https://github.com/naugehyde/bt-sensors-plugin-sk) to pull in data from other sensors and equipment.
@@ -83,7 +83,7 @@ Use the standard configuration option in the SignalK menu for the plugin.
83
83
 
84
84
  ![Plugin Configuration](docs/assets/screenshots/plugin_config.png)
85
85
 
86
- ## Scanning for Devices
86
+ ### Scanning for Devices
87
87
 
88
88
  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.
89
89
 
@@ -91,14 +91,17 @@ One other quirk is that some devices respond with a different name at different
91
91
 
92
92
  The plugin will optionally re-scan whenever it starts up, although this isn't essential once a label has been configured.
93
93
 
94
- ### Scans from CLI
94
+ ### Scheduling
95
95
 
96
- The command line tools, run from inside the `.signalk` directory, can be used to help troubleshoot
96
+ There are two ways of scheduling scans:
97
97
 
98
- - Scan for longer, in this example 90 seconds
99
- - `npx esl-cli scan -d 90`
100
- - Scan for all BLE devices, whatever they are
101
- - `npx esl-cli scan -a`
98
+ #### Time Based
99
+
100
+ This will schedule at fixed hours of the day, for example 00:00/08:00/16:00 for an 8h schedule, and at a selected minutes past the hour. At plugin startup/restart, the devices will be repainted if they missed their last slot.
101
+
102
+ #### Path Subscription
103
+
104
+ The devices will be painted when the plugin starts, and then every time the selected SignalK path changes.
102
105
 
103
106
  ## Templating
104
107
 
@@ -171,21 +174,6 @@ Three font types are loaded by default, use the generic font family, or exact fo
171
174
  - `sans-serif` - `Roboto`
172
175
  - `monospace` - `Roboto Mono`
173
176
 
174
- ## Command Line Interface
175
-
176
- To get fast feedback on templates and shelf devices without updating and configuring SignalK, a CLI call `esl-cli` is provided when the module is manually installed that has these commands. Use `--help` to get all the options.
177
-
178
- - `vendors` - list supported vendors
179
- - `scan` - report supported devices found from a BLE scan
180
-
181
- See also the commands useful for debugging under [Developing Templates]
182
-
183
- - `render` - transform an SVG template and data into a PNG
184
- - `paint` - render an SVG template and data to a selected ESL
185
-
186
- The width, height, vertical offset and colour palette for the device is taken from the internal register of devices, however can be overridden on the command line. This could be used to help you choose what size of label to buy, or to get an unsupported label working.
187
-
188
- ( The CLI can also be run from a checked out module, or by opening a terminal shell at `~/.signalk/node_modules/@rhizomatics/signalk-einklabel-plugin`, as `npx esl-cli command --args` )
189
177
 
190
178
  ## Vendors
191
179
 
@@ -214,6 +202,31 @@ The primary things managed and provided by the plugin are:
214
202
  - Optional: left blank, the plugin probes the probable values in likelihood order at startup - `http://localhost:3000`, `http://localhost`, `https://localhost`. Set it explicitly to skip probing
215
203
  - Either way, errors clearly if nothing responds (wrong port) or the probe is rejected (anonymous read access not enabled) - these endpoints must allow anonymous read access, since the plugin has no login flow
216
204
 
205
+ ## Command Line Interface
206
+
207
+ To get fast feedback on templates and shelf devices without updating and configuring SignalK, a CLI call `esl-cli` is provided when the module is manually installed that has these commands. Use `--help` to get all the options.
208
+
209
+ - `vendors` - list supported vendors
210
+ - `scan` - report supported devices found from a BLE scan
211
+
212
+ See also the commands useful for debugging under [Developing Templates]
213
+
214
+ - `render` - transform an SVG template and data into a PNG
215
+ - `paint` - render an SVG template and data to a selected ESL
216
+
217
+ The width, height, vertical offset and colour palette for the device is taken from the internal register of devices, however can be overridden on the command line. This could be used to help you choose what size of label to buy, or to get an unsupported label working.
218
+
219
+ ( The CLI can also be run from a checked out module, or by opening a terminal shell at `~/.signalk/node_modules/@rhizomatics/signalk-einklabel-plugin`, as `npx esl-cli command --args` )
220
+
221
+ ### Scans from CLI
222
+
223
+ The command line tools, run from inside the `.signalk` directory, can be used to help troubleshoot
224
+
225
+ - Scan for longer, in this example 90 seconds
226
+ - `npx esl-cli scan -d 90`
227
+ - Scan for all BLE devices, whatever they are
228
+ - `npx esl-cli scan -a`
229
+
217
230
  ## Extending
218
231
 
219
232
  ### Hardware
@@ -226,6 +239,8 @@ Additional vendors and devices can be added by a separate npm package that imple
226
239
 
227
240
  ### Developing Templates
228
241
 
242
+ See the [Templating] section for more details.
243
+
229
244
  Templates can be added to the configurable directory. [Inkscape](https://inkscape.org) free, open source, and recommended for editing templates, or your own favourite editor, or by hand in a text editor for hard core (or just tidying up the template side).
230
245
 
231
246
  ![Example Field Definition](docs/assets/screenshots/inkscape_desc.png)
@@ -238,6 +253,7 @@ Inkscape adds its own metadata to images, which can be stripped off by exporting
238
253
 
239
254
  Due to a limitation in the `resvg-wasm` library used to turn SVGs into images, the `font-family` is limited to `serif`,`sans-serif`,`monospace` or the exact name of one of the installed fonts - `Roboto` (sans serif), `Roboto Serif` or `Roboto Mono`. Inkscape has its own fonts, which won't match what's available in the SignalK plugin, so for more precise design, install [Roboto from Google](https://fonts.google.com/specimen/Roboto) via the web page, `brew` on MacOS or similar.
240
255
 
256
+
241
257
  ### Debugging Templates
242
258
 
243
259
  The `esl-cli` can be used to debug and validate templates quickly:
@@ -286,3 +302,38 @@ extremes.0.level source=resources,resource=tides,path=extremes[0].level,
286
302
  - `categories.json` - SignalK unit categories needed for `category=depth` type formatting
287
303
 
288
304
  For example, `npx esl-cli fields -t templates/tide.svg -e examples` will show all the field data that will be populated from the example API, vessel and category data in the `examples` local directory.
305
+
306
+ ## Frequently Asked Questions
307
+
308
+ ### Sometimes values are missing on the display
309
+
310
+ If the plugin repaints a display at server startup, then the plugin that provides the data may not have started ( or in the case of `derived-data` the plugin that the plugin depends on! ) and unlike Home Assistant, there's no good way of sequencing the start of plugins.
311
+
312
+ Use the *settle* time, to impose a minimum wait between the eInk Label plugin being initialized, and it attempting to paint any displays, and increase this value if its still missing data.
313
+
314
+ ### Times are showing incorrectly {#faq-timezone}
315
+
316
+ If times are in the wrong timezone, or don't have daylight savings applied correctly,
317
+ then check that the server itself (at the Linux level, not SignalK, which doesn't know)is configured for your timezone, assuming of course that you're a coastal sailor. Use `raspi-config` on a Raspberry Pi, or `timedatectl` on a Linux server.
318
+
319
+ If you're a global cruiser, then use something like [signalk-set-gps-timezone](https://github.com/hoeken/signalk-set-gps-timezone) to set the value in the operating system.
320
+
321
+ ### The ESL signal is too weak from my SignalK server
322
+
323
+ Try a BLE proxy device, ESP32 is popular for this.
324
+
325
+ ## Other ESL and General eInk Resources
326
+
327
+ - [Open ePaper Link](https://openepaperlink.de) - Alternative open source firmware to flash onto eInk shelf labels, with Home Assistant integration.
328
+ - [e-ink dashboard for Signal K](https://github.com/meri-imperiumi/dashboard) - Waveshare display based multi instrument display.
329
+ - [eInk Dashboard Modern SK](https://github.com/VladimirKalachikhin/e-inkDashboardModernSK) - SignalK dashboard for non-ESL eInk display.
330
+ - [esp32-esl-system](https://github.com/giobauermeister/esp32-esl-system) - Docker and ESP32 based system for updating ESLs.
331
+ - [hass-gicisky](https://github.com/eigger/hass-gicisky) - Home Assistant integration for Gicisky ESLs ( a similar vendor to Zhsunyco). Uses [imagespec](https://github.com/eigger/imagespec) for templating.
332
+ - [ha-panda](https://github.com/moryoav/ha-panda) - Home Assistant integration for Panda ESLs ( a similar vendor to Zhsunyco).
333
+ - [Dmitry.gr](https://dmitry.gr/?r=05.Projects&proj=29.%20eInk%20Price%20Tags) - Personal site of an ESL hacker
334
+ - [Aaron Christobel](https://www.youtube.com/@atc1441) - YouTube channel of an ESL hacker.
335
+ - [rbaron.net](https://rbaron.net/blog/2022/07/29/Daisy-chaining-multiple-electronic-shelf-labels) - Blog of an early ESL hacker.
336
+ - [Pimoroni](https://shop.pimoroni.com/collections/displays?tags=e-ink%20Displays) - All shapes and sizes of eInk displays, aimed at hackers, and with an [inky](https://github.com/pimoroni/inky) GitHub project to support them.
337
+ - [WaveShare](https://www.waveshare.com/product/displays/e-paper.htm) - Wide range of eInk displays for hardware projects, not limited to ESLs.
338
+
339
+ See also the [Boat Tech Directory](https://boat-tech-directory.rhizomatics.org.uk).
package/dist/config.d.ts CHANGED
@@ -36,6 +36,15 @@ export interface PluginConfig {
36
36
  paintConnectTimeoutSeconds: number;
37
37
  /** How many times to attempt a repaint (including the first try) before giving up and reporting failure. */
38
38
  paintRetries: number;
39
+ /**
40
+ * How long after plugin start to hold off on every repaint trigger (startup check, interval, and
41
+ * subscription alike) - the first minute or two of a SignalK server's life is a chaos of plugin
42
+ * dependency sequencing (e.g. `derived-data` may not have published `environment.moon.phaseName`
43
+ * yet), so a repaint attempted immediately at startup can render with missing/wrong data, and hash
44
+ * dedup (see `considerRepaint`) then means it silently stays that way until the underlying data
45
+ * happens to change again. Defaults to 120s - see `startRepaintScheduler`.
46
+ */
47
+ settleSeconds: number;
39
48
  /**
40
49
  * Base URL of this SignalK server, used for: (1) a `signalk`-sourced numeric value's automatic unit
41
50
  * conversion (`GET .../vessels/<context>/meta`, see `../pathMeta.ts`) unless `format=raw`, and (2) an
package/dist/config.js CHANGED
@@ -29,6 +29,7 @@ function defaultConfig() {
29
29
  scanDurationSeconds: 20,
30
30
  paintConnectTimeoutSeconds: 30,
31
31
  paintRetries: 3,
32
+ settleSeconds: 120,
32
33
  devices: [],
33
34
  };
34
35
  }
@@ -153,6 +154,15 @@ function configSchema(app, discovered = []) {
153
154
  minimum: 1,
154
155
  default: defaults.paintRetries,
155
156
  },
157
+ settleSeconds: {
158
+ type: "number",
159
+ title: "Settle time after plugin start (seconds)",
160
+ description: "Holds off every repaint (startup check, interval, and subscription alike) until this long after the plugin starts - " +
161
+ "the first minute or two of SignalK startup is a chaos of plugin dependency sequencing, so data a template needs " +
162
+ "(e.g. from the derived-data plugin) may not be published yet.",
163
+ minimum: 0,
164
+ default: defaults.settleSeconds,
165
+ },
156
166
  signalkApiUrl: {
157
167
  type: "string",
158
168
  title: "SignalK API base URL (leave blank to auto-detect)",
@@ -135,8 +135,8 @@ class SvgRenderer {
135
135
  const dirProblem = (0, assets_1.describeAssetsDirProblem)(templatesDir, bundledTemplatesDir, binding.assets);
136
136
  console.error(key
137
137
  ? `${pluginVersion_1.PLUGIN_NAME}: image "${descElement.textContent}" has no asset file for value "${key}" in "${binding.assets}"`
138
- : `${pluginVersion_1.PLUGIN_NAME}: image "${descElement.textContent}" resolved to no usable value to pick an asset file with`);
139
- console.error(`${pluginVersion_1.PLUGIN_NAME}: ${dirProblem ?? `assets directory for "${binding.assets}" checked out fine - the miss is just this value's`}`);
138
+ : `${pluginVersion_1.PLUGIN_NAME}: image "${descElement.textContent}" resolved to empty value to select asset`);
139
+ console.error(`${pluginVersion_1.PLUGIN_NAME}: ${dirProblem ?? `assets directory for "${binding.assets}" is present`}`);
140
140
  element.parentNode?.removeChild(element);
141
141
  continue;
142
142
  }
@@ -3,4 +3,14 @@ import { PluginConfig } from "./config";
3
3
  export interface RepaintScheduler {
4
4
  stop(): void;
5
5
  }
6
+ /**
7
+ * The most recent wall-clock instant at or before `now` that an `interval`-triggered device's own
8
+ * periodic check (below, in `startRepaintScheduler`) would fire at - every `intervalHours` hours,
9
+ * at `intervalMinute` minutes past the hour (e.g. hours=8,minute=0 -> 00:00, 08:00, 16:00 each day).
10
+ * Used only by the deferred startup catch-up check to tell an interval device that's already been
11
+ * painted for its current slot (skip - the regular periodic check will catch the *next* slot in due
12
+ * course) from one that's overdue (e.g. the plugin was down across a scheduled slot - paint now
13
+ * rather than waiting up to `intervalHours` for the next one).
14
+ */
15
+ export declare function mostRecentScheduledSlot(now: Date, intervalHours: number, intervalMinute: number): Date;
6
16
  export declare function startRepaintScheduler(app: ServerAPI, config: PluginConfig): RepaintScheduler;
@@ -1,5 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.mostRecentScheduledSlot = mostRecentScheduledSlot;
3
4
  exports.startRepaintScheduler = startRepaintScheduler;
4
5
  const crypto_1 = require("crypto");
5
6
  const path_1 = require("path");
@@ -17,6 +18,23 @@ const resolveApiUrl_1 = require("./resolveApiUrl");
17
18
  const pluginVersion_1 = require("./pluginVersion");
18
19
  const INTERVAL_POLL_MS = 60000;
19
20
  const SUBSCRIPTION_DEBOUNCE_MS = 2000;
21
+ /**
22
+ * The most recent wall-clock instant at or before `now` that an `interval`-triggered device's own
23
+ * periodic check (below, in `startRepaintScheduler`) would fire at - every `intervalHours` hours,
24
+ * at `intervalMinute` minutes past the hour (e.g. hours=8,minute=0 -> 00:00, 08:00, 16:00 each day).
25
+ * Used only by the deferred startup catch-up check to tell an interval device that's already been
26
+ * painted for its current slot (skip - the regular periodic check will catch the *next* slot in due
27
+ * course) from one that's overdue (e.g. the plugin was down across a scheduled slot - paint now
28
+ * rather than waiting up to `intervalHours` for the next one).
29
+ */
30
+ function mostRecentScheduledSlot(now, intervalHours, intervalMinute) {
31
+ const slot = new Date(now);
32
+ slot.setHours(Math.floor(now.getHours() / intervalHours) * intervalHours, intervalMinute, 0, 0);
33
+ if (slot.getTime() > now.getTime()) {
34
+ slot.setHours(slot.getHours() - intervalHours);
35
+ }
36
+ return slot;
37
+ }
20
38
  function statePath(app) {
21
39
  return (0, path_1.join)(app.getDataDirPath(), "repaint-state.json");
22
40
  }
@@ -168,7 +186,7 @@ async function considerRepaint(app, config, device, state, getApiUrl) {
168
186
  await driver.paint(bitmap, { address: model.address, aesKey: device.aesKey, connectTimeoutMs });
169
187
  paintDurationMs = Date.now() - startedAt;
170
188
  });
171
- state[device.friendlyName] = { hash };
189
+ state[device.friendlyName] = { hash, repaintedAt: Date.now() };
172
190
  saveState(app, state);
173
191
  if (device.forceRepaint) {
174
192
  clearForceRepaint(app, device.friendlyName);
@@ -179,7 +197,21 @@ function startRepaintScheduler(app, config) {
179
197
  const state = loadState(app);
180
198
  const unsubscribes = [];
181
199
  const getApiUrl = (0, resolveApiUrl_1.createApiUrlResolver)(config.signalkApiUrl);
182
- const repaint = (device) => considerRepaint(app, config, device, state, getApiUrl).catch((err) => app.debug(`"${device.friendlyName}": repaint failed: ${err.message}`));
200
+ // The first minute or two of a SignalK server's life is a chaos of plugin dependency sequencing -
201
+ // a repaint attempted before other plugins (e.g. derived-data) have published the data a template
202
+ // needs renders with missing/wrong values, and hash dedup (see `considerRepaint`) then means it
203
+ // silently stays that way until the underlying data happens to change again. So every trigger
204
+ // (startup check, interval, subscription alike) funnels through this one gate.
205
+ const startedAt = Date.now();
206
+ const settleMs = (config.settleSeconds ?? 120) * 1000;
207
+ const repaint = (device) => {
208
+ const elapsedMs = Date.now() - startedAt;
209
+ if (elapsedMs < settleMs) {
210
+ app.debug(`"${device.friendlyName}": still settling (${Math.round(elapsedMs / 1000)}s/${Math.round(settleMs / 1000)}s) - skipping repaint`);
211
+ return Promise.resolve();
212
+ }
213
+ return considerRepaint(app, config, device, state, getApiUrl).catch((err) => app.debug(`"${device.friendlyName}": repaint failed: ${err.message}`));
214
+ };
183
215
  const intervalDevices = config.devices.filter((device) => device.repaintTrigger === "interval");
184
216
  if (intervalDevices.length > 0) {
185
217
  const timer = setInterval(() => {
@@ -197,15 +229,43 @@ function startRepaintScheduler(app, config) {
197
229
  for (const device of config.devices) {
198
230
  if (device.repaintTrigger === "subscription" && device.triggerPath) {
199
231
  const stream = app.streambundle.getSelfStream(device.triggerPath).debounce(SUBSCRIPTION_DEBOUNCE_MS);
200
- const unsub = stream.onValue(() => repaint(device));
232
+ // A path can report a transient `null`/missing value (e.g. a sensor briefly drops out) then go
233
+ // right back to what it was before - e.g. 1111, then missing, then 1111 again. Ignoring the
234
+ // missing emission entirely (rather than repainting for it) means it's never actually painted,
235
+ // so when the value comes back the same as before, `considerRepaint`'s hash dedup sees nothing
236
+ // changed since the last real paint and skips too - net effect: no repaint at all for this
237
+ // blip, instead of one flickering the display blank/wrong and a second restoring it.
238
+ const unsub = stream.onValue((value) => {
239
+ if (value !== null && value !== undefined)
240
+ void repaint(device);
241
+ });
201
242
  unsubscribes.push(unsub);
202
243
  }
203
244
  }
204
- // Check every device once at startup - harmless given hash dedup, and covers newly-added
205
- // devices or a forceRepaint left set from before a restart.
206
- for (const device of config.devices) {
207
- void repaint(device);
208
- }
245
+ // Check every device once, after the settle window. Deferred (rather than run immediately) so a
246
+ // subscription-triggered device whose path is slow-changing (e.g. tide/moon data that might not
247
+ // change again for hours) still gets its first real paint promptly once settled, instead of
248
+ // waiting on that path's next unrelated update - it's then left alone until the path changes
249
+ // again, same as always. An interval-triggered device, though, only gets this catch-up paint if
250
+ // it's actually overdue - already painted for the current scheduled slot (e.g. plugin restarted
251
+ // mid-way through its interval) just waits for the regular periodic check above to hit the *next*
252
+ // slot, rather than jumping the gun with an extra unscheduled paint every time the plugin restarts.
253
+ const startupCheckTimer = setTimeout(() => {
254
+ for (const device of config.devices) {
255
+ if (device.repaintTrigger === "interval" && !device.forceRepaint) {
256
+ const hours = device.intervalHours ?? 1;
257
+ const minute = device.intervalMinute ?? 0;
258
+ const dueSlot = mostRecentScheduledSlot(new Date(), hours, minute);
259
+ const repaintedAt = state[device.friendlyName]?.repaintedAt;
260
+ if (repaintedAt !== undefined && repaintedAt >= dueSlot.getTime()) {
261
+ app.debug(`"${device.friendlyName}": already painted for the current ${hours}h schedule slot - skipping startup catch-up`);
262
+ continue;
263
+ }
264
+ }
265
+ void repaint(device);
266
+ }
267
+ }, settleMs);
268
+ unsubscribes.push(() => clearTimeout(startupCheckTimer));
209
269
  return {
210
270
  stop() {
211
271
  for (const unsubscribe of unsubscribes)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rhizomatics/signalk-einklabel-plugin",
3
- "version": "0.7.1",
3
+ "version": "0.8.0",
4
4
  "description": "Display SignalK data on eInk Electronic Shelf Labels",
5
5
  "keywords": [
6
6
  "ble",