@rhizomatics/signalk-einklabel-plugin 0.9.0-beta1 → 0.9.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,14 +1,20 @@
1
1
  # 0.9.0
2
2
 
3
+ ## Tide Clock Example
3
4
  - Rename `third_quarter` to `last_quarter` for moon phase icons in `templates/.assets/lunar_phases` to match Derived Data plugin
4
5
  - `tide.svg` renamed to `tides\416x240-BWRY.svg`. Original maintained but labelled as deprecated
5
- - Added a simpler 250x128 version of tide clock for 2.13" labels
6
+ - Added a simpler *250x128* version of tide clock for 2.13" labels
7
+ ## Fixes
6
8
  - Fix configuration JSON holding older versions of itself as subentries
7
- - Repaint state used to track which labels to repaint separately tracks the template and data changes
9
+ and data changes
8
10
  - Fix most cases of scanned devices not appearing in device dropdown choice
11
+ ## Improvements
12
+ - Repaint state used to track which labels to repaint separately tracks the template
9
13
  - `nearestColour` algorithm in Zhsunyco driver supports ESLs that only have BWR or BW
10
- - New 'ALL' as device option, and by default disable initial scan, to optimize support for single devices
11
- - Scan and explicit device selection only required then for boats with multiple devices
14
+ ## Device Selection
15
+ - New 'ALL' as device option, and by default disable initial scan, to optimize support for single devices, so can configure and go without waiting for scan
16
+ - Scan and explicit device selection only required for boats with multiple ESL devices
17
+ ## Templating
12
18
  - Now support a directory of templates, where each is named like `416-240-BWRY.svg` to support same functions on different devices.
13
19
  - Picks the template within the directory that most closely matches the tide clock height/width/colour-set, if not matched then height/width, and then best h/w ratio for nearest width
14
20
  - `template/assets` is now `template/.assets`
package/README.md CHANGED
@@ -72,12 +72,17 @@ npm install @rhizomatics/signalk-einklabel-plugin
72
72
 
73
73
  ![Tide Clock](docs/assets/screenshots/example_tidal_clock.png)
74
74
 
75
- The tide clock needs the [signalk-tides](https://github.com/openwatersio/signalk-tides) plugin to be installed and publishing tides to the Resources API. The [tide.svg](https://github.com/rhizomatics/signalk-einklabel-plugin/blob/main/templates/tide.svg) can be customized to run with other APIs or take data only from SignalK data paths. For example, `source=resources,resource=tides,provider=tides,path=extremes[0].time,format=local_time` gets the first tide time, ensures its the preferred `signalk-tides` provider and makes it a simple local time rather than a UTC date time.
75
+ Template available as 416x240-BWRY for 3.7" ESLs and a simpler template, sized 250x128, also BWRY, for the cheapest 2.13" labels.
76
76
 
77
- To show the lunar phase, the `environment.moon.phaseName` path is required, which can
77
+ #### Pre-requisites
78
+
79
+ * [signalk-tides](https://github.com/openwatersio/signalk-tides) plugin to be installed and publishing tides to the Resources API.
80
+ - The [tides](https://github.com/rhizomatics/signalk-einklabel-plugin/blob/main/templates/tides/) templates can be customized to run with other APIs or take data only from SignalK data paths.
81
+ - In the template it uses paths like `source=resources,resource=tides,provider=tides,path=extremes[0].time,format=local_time` to get the first tide time, ensures its the preferred `signalk-tides` provider and makes it a simple local time rather than a UTC date time.
82
+ * To show the lunar phase, the `environment.moon.phaseName` path is required, which can
78
83
  be easily achieved by installing and configuring the `derived-data` plugin.
79
84
 
80
- ## Configuration
85
+ ## Configuration
81
86
 
82
87
  Use the standard configuration option in the SignalK menu for the plugin.
83
88
 
@@ -95,7 +100,7 @@ The plugin can optionally re-scan whenever it starts up (off by default), althou
95
100
 
96
101
  A device's "Device" field can either be a specific device picked from the scan dropdown, or **"All discovered devices"**, which paints that same template/trigger to every device the plugin currently knows about. This is the simplest option for a boat with just one label - there's no need to scan first and pick it out, and if nothing's been discovered yet, selecting it triggers a scan itself the first time it's needed. It also covers several identical labels with one config entry, without listing each one out.
97
102
 
98
- ### Scheduling
103
+ ### Scheduling
99
104
 
100
105
  There are two ways of scheduling scans:
101
106
 
@@ -323,6 +328,8 @@ For example, `npx esl-cli fields -t templates/tide.svg -e examples` will show al
323
328
 
324
329
  SignalK plugins lack ability to self-update after something like a scan, so first time round you may have to close the config and reload it to see this. Subsequently the plugin will remember all scanned devices, and only drop previously seen ones if it goes 24 hours without a positive scan or with failed paint attempts.
325
330
 
331
+ Easiest way to solve this is to choose 'All Discovered Devices' in the device configuration, and it will paint any compatible devices it finds on future scans.
332
+
326
333
  ### Sometimes values are missing on the display
327
334
 
328
335
  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.
package/dist/config.d.ts CHANGED
@@ -94,19 +94,18 @@ export interface PluginConfig {
94
94
  */
95
95
  export declare const BUNDLED_TEMPLATES_DIR: string;
96
96
  export declare function defaultConfig(): PluginConfig;
97
+ export declare function readCurrentConfig(app: ServerAPI): Partial<PluginConfig>;
97
98
  /**
98
- * `app.readPluginOptions()`/`savePluginOptions()` aren't actually symmetric despite what their doc
99
- * comments imply: signalk-server's `readPluginOptions()` returns the *whole* on-disk file
100
- * (`{ configuration, enabled, enableDebug, enableLogging }`), not just the `configuration` object
101
- * `savePluginOptions()` writes into - see signalk-server's `interfaces/plugins.ts`
102
- * (`appCopy.readPluginOptions = () => getPluginOptions(plugin.id)` vs.
103
- * `appCopy.savePluginOptions = (configuration, cb) => savePluginOptions(id, { ...getPluginOptions(id), configuration }, cb)`).
104
- * Spreading that return value straight back into a save call therefore nests the whole file one level
105
- * deeper inside its own `configuration` key every time (see `support/signalk-einklabel-plugin.json` and
106
- * `clearForceRepaint` in `./repaintScheduler.ts`). Unwrapping any such nesting and keeping only recognised
107
- * fields here means every save collapses back down instead of growing, self-healing an already-corrupted file.
99
+ * Actively rewrites the on-disk file once it's nested (see `readCurrentConfig`'s doc comment) -
100
+ * `readCurrentConfig` alone only self-heals in memory for callers that go through it, but the admin
101
+ * UI's own config-editing form round-trips whatever raw JSON it was handed verbatim, including a
102
+ * stray nested `configuration` key it never touches (no schema field maps to it) - so left alone,
103
+ * every future save from the UI keeps re-persisting that dead weight forever (see
104
+ * `support/signalk-einklabel-plugin.json`). Called once at plugin start, which - unlike
105
+ * `clearForceRepaint` - isn't gated on any device having `forceRepaint` set, so a nested file gets
106
+ * flattened even if nothing ever triggers that path.
108
107
  */
109
- export declare function readCurrentConfig(app: ServerAPI): Partial<PluginConfig>;
108
+ export declare function healNestedConfig(app: ServerAPI): void;
110
109
  /**
111
110
  * Resolves the user-facing `templatesDir` setting to an actual directory, mirroring
112
111
  * signalk-parquet's `outputDirectory` convention: empty means the default location, a relative
package/dist/config.js CHANGED
@@ -3,6 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.BUNDLED_TEMPLATES_DIR = exports.ALL_DEVICES = void 0;
4
4
  exports.defaultConfig = defaultConfig;
5
5
  exports.readCurrentConfig = readCurrentConfig;
6
+ exports.healNestedConfig = healNestedConfig;
6
7
  exports.resolveTemplatesDir = resolveTemplatesDir;
7
8
  exports.parseDevice = parseDevice;
8
9
  exports.resolveTemplatePath = resolveTemplatePath;
@@ -65,14 +66,21 @@ const PLUGIN_CONFIG_KEYS = [
65
66
  * `clearForceRepaint` in `./repaintScheduler.ts`). Unwrapping any such nesting and keeping only recognised
66
67
  * fields here means every save collapses back down instead of growing, self-healing an already-corrupted file.
67
68
  */
68
- function readCurrentConfig(app) {
69
- let raw = app.readPluginOptions();
69
+ function unwrapNestedConfiguration(raw) {
70
+ let levels = 0;
70
71
  while (raw &&
71
72
  typeof raw === "object" &&
72
73
  "configuration" in raw &&
73
74
  typeof raw.configuration === "object") {
74
75
  raw = raw.configuration;
76
+ levels++;
75
77
  }
78
+ // One level is the normal, expected shape signalk-server itself stores on disk (see the doc
79
+ // comment above) - only a *second* nested `configuration` key onwards is the corruption this
80
+ // guards against.
81
+ return { unwrapped: raw, wasNested: levels > 1 };
82
+ }
83
+ function pickKnownKeys(raw) {
76
84
  const result = {};
77
85
  if (raw && typeof raw === "object") {
78
86
  for (const key of PLUGIN_CONFIG_KEYS) {
@@ -83,6 +91,31 @@ function readCurrentConfig(app) {
83
91
  }
84
92
  return result;
85
93
  }
94
+ function readCurrentConfig(app) {
95
+ const { unwrapped } = unwrapNestedConfiguration(app.readPluginOptions());
96
+ return pickKnownKeys(unwrapped);
97
+ }
98
+ /**
99
+ * Actively rewrites the on-disk file once it's nested (see `readCurrentConfig`'s doc comment) -
100
+ * `readCurrentConfig` alone only self-heals in memory for callers that go through it, but the admin
101
+ * UI's own config-editing form round-trips whatever raw JSON it was handed verbatim, including a
102
+ * stray nested `configuration` key it never touches (no schema field maps to it) - so left alone,
103
+ * every future save from the UI keeps re-persisting that dead weight forever (see
104
+ * `support/signalk-einklabel-plugin.json`). Called once at plugin start, which - unlike
105
+ * `clearForceRepaint` - isn't gated on any device having `forceRepaint` set, so a nested file gets
106
+ * flattened even if nothing ever triggers that path.
107
+ */
108
+ function healNestedConfig(app) {
109
+ const { unwrapped, wasNested } = unwrapNestedConfiguration(app.readPluginOptions());
110
+ if (!wasNested)
111
+ return;
112
+ app.savePluginOptions(pickKnownKeys(unwrapped), (err) => {
113
+ if (err)
114
+ app.debug(`failed to clean up legacy nested plugin config: ${err.message}`);
115
+ else
116
+ app.debug("cleaned up a legacy nested plugin config file on disk");
117
+ });
118
+ }
86
119
  /**
87
120
  * Resolves the user-facing `templatesDir` setting to an actual directory, mirroring
88
121
  * signalk-parquet's `outputDirectory` convention: empty means the default location, a relative
package/dist/plugin.js CHANGED
@@ -49,6 +49,7 @@ function createPlugin(app) {
49
49
  ...config,
50
50
  };
51
51
  app.debug(`starting with ${pluginConfig.devices.length} configured device(s)`);
52
+ (0, config_1.healNestedConfig)(app);
52
53
  if (pluginConfig.scanOnStart) {
53
54
  void runStartupScan(app, pluginConfig.scanDurationSeconds);
54
55
  }
@@ -234,11 +234,22 @@ async function considerRepaint(app, config, device, target, state, getApiUrl) {
234
234
  });
235
235
  const rawContext = await assembleRawContext(app, apiUrl, bindings);
236
236
  const templateHash = hashTemplate(templateMtimeMs);
237
+ // Hashed from `rawContext`, not `renderContext` below - `meta.repainted` (and the rest of `meta`)
238
+ // is only added after this point, deliberately, so a template merely *displaying* the repaint
239
+ // timestamp doesn't perpetually invalidate its own dedup and force a repaint every check. A full
240
+ // paint flashes the whole panel several times, and there's no confirmed partial-refresh path on
241
+ // these devices - so a bound value simply being unchanged is worth trusting over any staleness in
242
+ // a displayed clock.
237
243
  const dataHash = hashData(rawContext);
238
244
  const stateKey = stateKeyFor(device.friendlyName, address);
239
245
  const previous = state[stateKey];
240
246
  const templateChanged = previous?.templateHash !== templateHash;
241
247
  const dataChanged = previous?.dataHash !== dataHash;
248
+ // The devices are battery-constrained, so a repaint is skipped whenever neither the template nor
249
+ // the bound data has changed since the last successful paint - regardless of what triggered this
250
+ // check (interval or subscription) or whether it's a regular scheduled tick vs. the deferred
251
+ // startup catch-up (see `startupCheckTimer`). `forceRepaint` is the explicit, one-shot override
252
+ // for "repaint anyway".
242
253
  if (!templateChanged && !dataChanged && !device.forceRepaint) {
243
254
  app.debug(`${label}: data unchanged, skipping repaint`);
244
255
  return;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rhizomatics/signalk-einklabel-plugin",
3
- "version": "0.9.0-beta1",
3
+ "version": "0.9.0",
4
4
  "description": "Display SignalK data on eInk Electronic Shelf Labels",
5
5
  "keywords": [
6
6
  "ble",