@rhizomatics/signalk-einklabel-plugin 1.2.3 → 1.3.0-beta10

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 (36) hide show
  1. package/CHANGELOG.md +28 -0
  2. package/README.md +97 -9
  3. package/dist/cli/index.d.ts +4 -1
  4. package/dist/cli/index.js +30 -6
  5. package/dist/config.d.ts +53 -14
  6. package/dist/config.js +172 -41
  7. package/dist/devices/bleBackend.d.ts +49 -0
  8. package/dist/devices/bleBackend.js +268 -0
  9. package/dist/devices/bleDiscovery.d.ts +20 -2
  10. package/dist/devices/bleDiscovery.js +101 -3
  11. package/dist/devices/discoveryCoordinator.d.ts +25 -6
  12. package/dist/devices/discoveryCoordinator.js +148 -9
  13. package/dist/devices/gattConnection.d.ts +9 -0
  14. package/dist/devices/gattConnection.js +2 -0
  15. package/dist/devices/gicisky/compression.d.ts +22 -9
  16. package/dist/devices/gicisky/compression.js +128 -13
  17. package/dist/devices/gicisky/encode.d.ts +1 -1
  18. package/dist/devices/gicisky/encode.js +2 -2
  19. package/dist/devices/gicisky/index.d.ts +7 -2
  20. package/dist/devices/gicisky/index.js +118 -96
  21. package/dist/devices/gicisky/layout.d.ts +13 -1
  22. package/dist/devices/gicisky/layout.js +21 -0
  23. package/dist/devices/types.d.ts +48 -6
  24. package/dist/devices/types.js +2 -0
  25. package/dist/devices/zhsunyco/compression.d.ts +1 -0
  26. package/dist/devices/zhsunyco/compression.js +30 -0
  27. package/dist/devices/zhsunyco/index.d.ts +8 -2
  28. package/dist/devices/zhsunyco/index.js +92 -82
  29. package/dist/devices/zhsunyco/protocol.d.ts +2 -0
  30. package/dist/devices/zhsunyco/protocol.js +2 -0
  31. package/dist/plugin.js +38 -11
  32. package/dist/render/mirror.d.ts +11 -0
  33. package/dist/render/mirror.js +24 -0
  34. package/dist/repaintScheduler.js +29 -12
  35. package/docs/assets/images/mini_tidal_clock.png +0 -0
  36. package/package.json +3 -3
package/CHANGELOG.md CHANGED
@@ -1,3 +1,31 @@
1
+ # 1.3.0
2
+
3
+ ## BLE Manager
4
+
5
+ First implementation of using new SignalK BLE Manager rather than directly using the `bluez` services.
6
+
7
+ - Off by default until longer term stability demonstrated.
8
+ - It may be less reliable for some devices that can be reached with direct bluez access - try it and fall back to the direct mode if so.
9
+ - BLE Manager use reduces interference between plugins competing for same BLE devices
10
+ - Recommend using SignalK at least release v2.33.0.
11
+
12
+ ## Compression
13
+
14
+ Images are now compressed before sending - no change to what's displayed, but send time is massively reduced for labels with lots of empty space, so less battery is used and sends are less likely to fail.
15
+
16
+ - On by default for Zhsunyco labels and Gicisky 7.5"/10.2" labels; can be turned off per label, or with `--no-compress` on the CLI
17
+ - Experimental opt-in _Wire format_ `chunked` sends a Gicisky 4.2" BWR compressed too (`--compression-format chunked` on the CLI)
18
+
19
+ ## Advanced Options
20
+
21
+ Connect timeout and retries can now be defined per label.
22
+
23
+ Some labels may need images flipped, so a per-label _Mirror_ option can flip the image horizontally, vertically, or both (rotate 180°, for a label mounted upside down). Also available as `--mirror` on the CLI `paint` and `render` commands.
24
+
25
+ Different 'chunking' options to help with new label models.
26
+
27
+ All the advanced options now grouped together to make the config clearer. Note that if you roll back to a previous version you may have to re-enter these values (they are automatically moved to the new section when upgrading).
28
+
1
29
  # 1.2.3
2
30
 
3
31
  - Improved example tide template for 2.9" Gicisky, and added blank and error templates
package/README.md CHANGED
@@ -24,22 +24,31 @@ Being battery operated, they can be stuck on anywhere without wiring - the only
24
24
 
25
25
  Unlike some eInk projects, this plugin doesn't require any physical modification to the labels, or loading any new firmware. It can send an image to a supported shelf label fresh out of the box.
26
26
 
27
- Most of requirements below are to make 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 and this plugin will support that when it comes.
27
+ Most of requirements below are to make 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.
28
28
 
29
- 1. A SignalK server, **running Linux**
29
+ This plugin can reach BLE hardware two ways - pick whichever fits your setup:
30
30
 
31
- - MacOS and Windows aren't supported by the [BLE interface layer](https://www.npmjs.com/package/@naugehyde/node-ble), however they can be used for template development and
31
+ - **Direct BlueZ access** (default) - the plugin talks to BlueZ over D-Bus itself. Requirements 1-3 below apply.
32
+ - **SignalK BLE Manager API** (opt-in) - SignalK server >= 2.32.0 ships a [BLE Provider/Consumer API](https://github.com/SignalK/signalk-server/issues/2411) (admin UI: "BLE Manager") that arbitrates adapter access across every BLE-consuming plugin instead of each one grabbing `hci0` for itself, and can source BLE over a remote gateway instead of local hardware at all. Enable the "Use the SignalK BLE Manager API" setting in this plugin's config once it's available (it only appears once the running server has it) - requirements 1-3 below then become the SignalK server's problem, under its own Bluetooth admin settings, not this plugin's.
33
+
34
+ 1. A SignalK server, **running Linux** (direct BlueZ mode only - BLE Manager mode with a remote gateway provider has no such requirement)
35
+
36
+ - MacOS and Windows aren't supported by the [BLE interface layer](https://www.npmjs.com/package/@naugehyde/node-ble) in direct BlueZ mode, however they can be used for template development and
32
37
  debugging (everything except `scan` and `paint`)
33
38
 
34
- 2. A Bluetooth adapter, that can handle BLE (Bluetooth Low Energy).
39
+ 2. A Bluetooth adapter, that can handle BLE (Bluetooth Low Energy) - direct BlueZ mode only; in BLE Manager mode this is whatever the server's own Bluetooth settings provide.
35
40
 
36
- - Bluetooth adapters for Linux can be tricky, TP-Link UB400 and Asus USB-BT500 are two well-known and available ones
41
+ - Bluetooth adapters for Linux can be tricky
42
+ - TP-Link UB400 and Asus USB-BT500 are two well-known and available ones, though the ASUS USB-BT500 one can have problems with some Pi type boards
43
+ - CSR4.0 dongles (CSR8510 chip) have had kernel support for years, and there are well known work arounds for some of them, including in the Linux kernel since v5.17
37
44
  - Some Raspberry Pi models come with suitable Bluetooth built in
45
+ - See advice at [Recommended Bluetooth Adapters for Linux](https://github.com/morrownr/USB-WiFi/blob/main/home/Recommended_Bluetooth_Adapters_for_Linux.md)
46
+ - Bluetooth adapters typically prefer being in USB2.0 ports rather than USB3.0 ports, since often the USB3.0 implementation leaks radio energy on the same 2.4Ghz spectrum as Bluetooth. If no USB2.0 port available, try a shielded USB2.0 extension lead to distance the dongle from the port. Some dongle manufacturees seems to do a better job at shielding for this than others.
38
47
 
39
48
  > - Don't worry about the very latest Bluetooth versions, 4.0 is minimum for BLE, 5.0 is nice
40
49
  > - Home Assistant is massively more popular than SignalK, and often also run on Raspberry Pi and similar, so good source of advice
41
50
 
42
- 3. `bluez` package installed in Linux
51
+ 3. `bluez` package installed in Linux - direct BlueZ mode only
43
52
 
44
53
  - No need to do this if you have a Raspberry Pi with recent Raspian version, since bluez comes built in.
45
54
  - If you're not running a Raspberry Pi, then ensure that the `dbus` package is installed
@@ -82,6 +91,10 @@ Use the standard configuration option in the SignalK menu for the plugin.
82
91
 
83
92
  Template available as 416x240-BWRY for 3.7" ESLs and a simpler template, sized 250x128, also BWRY, for the cheapest 2.13" labels.
84
93
 
94
+ This mini tide clock is a 2.9" Gicisky device, less than £10 inc delivery in summer 2026.
95
+
96
+ ![2.9" Tide Clock](docs/assets/images/mini_tidal_clock.png)
97
+
85
98
  #### Pre-requisites
86
99
 
87
100
  A _tides_ provider plugin for the Resources API installed and enabled, currently one of:
@@ -121,10 +134,16 @@ Enable the plugin, and use the large **+** sign to add a label, which opens up t
121
134
  - If it's a SignalK path, enter it next, for example `environment.tide.state`
122
135
  - If it's time based, enter how many hours between repaints, for example 00:00/08:00/16:00 for an 8h schedule, and if you want a specific number of minutes after the hour.
123
136
 
124
- There are also two more advanced options, which can usually be ignored.
137
+ - _If the render doesn't match the panel size_ - see [Reframing](#reframing)
125
138
 
126
- - _BLE AES key_ - Only needed if the default key doesn't work and you have a better alternative, otherwise ignore
139
+ The rest are grouped under _Advanced settings_, and can usually be ignored.
140
+
141
+ - _Compress upload_, _Mirror_ and _Wire format_ - see [Other Image Options](#other-image-options)
127
142
  - _Force Repaint_ - Next time the label is due to be painted, update even if the data or template hasn't changed (this flag will automatically be cleared after this.)
143
+ - _BLE AES key_ - Only needed if the default key doesn't work and you have a better alternative, otherwise ignore
144
+ - _Paint connect timeout_ and _Paint retries_ for this device - leave blank to use the plugin-wide settings, or set them for a label that's slower to respond or further away than the others
145
+
146
+ Configs saved by earlier versions are moved into this layout automatically when the plugin starts.
128
147
 
129
148
  When the plugin starts, it will automatically re-paint the label if it's new, or the last timed slot was missed and the data has changed.
130
149
 
@@ -165,6 +184,14 @@ Templates are simply SVG files, to which expressions can be added to use SignalK
165
184
 
166
185
  There's some wiggle room with the `reframe` options to use a template that's a bit too small, or too large, for the label, although best results come from a template that's precisely matching the pixel height and width of the label. Next best is template that has the same aspect ratio, so it can be cleanly scaled. `crop` is the 2nd least worst, though if its only a handful of pixels its often not worth worring about a separate template and `crop` is just fine. `scale` is likely to look worst, since it will force an image in regardless of aspect ratio.
167
186
 
187
+ ### Other Image Options
188
+
189
+ Each label has a few more settings for how the image is sent, under _Advanced settings_. The CLI `paint` command has matching options (see [Command Line Interface](#command-line-interface)), so you can try them on a label before changing the plugin config.
190
+
191
+ - _Compress upload_ - on by default. Sends much less data over Bluetooth, so painting is quicker, uses less of the label's battery and is less likely to time out. Works for Zhsunyco labels and Gicisky 7.5"/10.2" labels, and is ignored for others. Turn it off if a label stops updating.
192
+ - _Wire format (Gicisky, experimental)_ - `auto` by default. `chunked` sends a Gicisky 4.2" BWR label compressed, the same way as the 7.5"/10.2". The vendor's own app has been seen doing this, but it hasn't been tested on current firmware. Set it back to `auto` if the label stops updating.
193
+ - _Mirror_ - `none` by default. `horizontal` or `vertical` fixes a label model whose image comes out mirrored. `both` rotates the image 180°, for a label that has to be mounted upside down.
194
+
168
195
  ### Template Families (multiple panel sizes/colours)
169
196
 
170
197
  A "Template" selection can either be one specific `.svg` file, or a _directory_ holding several versions of the same template for different panel sizes/colour-sets, e.g. `templates/tides/416x240-BWRY.svg` and `templates/tides/250x128-BWRY.svg` both implement the tide clock, just at different sizes.
@@ -296,6 +323,12 @@ Left unset, both `render` and `paint` default `-w/--width`/`--height` to the tem
296
323
 
297
324
  The main SignalK plugin offers the same choice per device (defaulting to `crop` there too) in each device's own config - "If the render doesn't match the panel size".
298
325
 
326
+ `paint` has matching options for the other per-label image settings too (see [Other Image Options](#other-image-options)):
327
+
328
+ - `--mirror <mode>` - `none` (default), `horizontal`, `vertical`, or `both` (rotate 180°). `render` also takes `--mirror`, to preview the flip as a PNG without a label
329
+ - `--no-compress` - send the image uncompressed, to rule compression out if a label won't update
330
+ - `--compression-format <format>` - `auto` (default) or `chunked`, to try the experimental compressed format on a Gicisky 4.2" BWR
331
+
299
332
  `esl-cli` can also be extended with new subcommands by a `-r/--require`'d package - see [Extending](#extending) below - which is how [`@rhizomatics/signalk-einklabel-genai-plugin`](#genai-rendering) adds its own `prompt`/`generate` commands for testing prompts without a device.
300
333
 
301
334
  ( 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` )
@@ -364,12 +397,24 @@ The label address previously discovered via `esl-cli scan`
364
397
  npx esl-cli paint -t templates/tides/250x128-BWRY.svg -a FF:FF:92:84:53:93
365
398
  ```
366
399
 
367
- If the label turns out to be a different size than the template (e.g. it's a 250x128 template on a 416x240 panel), that's normally rejected as a mismatch - add `--reframe` to fit it instead:
400
+ If the label turns out to be a different size than the template (e.g. it's a 250x128 template on a 416x240 panel), it's cropped to fit by default - add `--reframe scale` to stretch it instead:
368
401
 
369
402
  ```bash
370
403
  npx esl-cli paint -t templates/tides/250x128-BWRY.svg -a FF:FF:92:84:53:93 --reframe scale
371
404
  ```
372
405
 
406
+ If a label doesn't update, try sending it uncompressed to see whether compression is the cause:
407
+
408
+ ```bash
409
+ npx esl-cli paint -t templates/tides/250x128-BWRY.svg -a FF:FF:92:84:53:93 --no-compress
410
+ ```
411
+
412
+ If the image comes out mirrored, try each `--mirror` mode until it looks right, then set the same _Mirror_ option in the label's config:
413
+
414
+ ```bash
415
+ npx esl-cli paint -t templates/tides/250x128-BWRY.svg -a FF:FF:92:84:53:93 --mirror horizontal
416
+ ```
417
+
373
418
  #### Test Template Without Updating Label
374
419
 
375
420
  This will work even if you don't have a label, or even bluetooth. (The `-u` can be left out if your SignalK server running locally on default ports).
@@ -461,6 +506,8 @@ That's the bundled fallback warning, not necessarily an error in this plugin - i
461
506
 
462
507
  ### SignalK starts before the Bluetooth daemon — does the plugin need `bluetoothd` running at boot?
463
508
 
509
+ This and the next entry are about direct BlueZ mode only - if the "Use the SignalK BLE Manager API" setting is enabled, adapter/dongle lifecycle is the SignalK server's problem to manage once, for every BLE-consuming plugin, not this plugin's.
510
+
464
511
  The plugin retries BLE adapter initialisation with backoff (starting at 2s, capping at 30s) if `bluetoothd`/D-Bus isn't up yet when the plugin starts, so a slow-starting Bluetooth stack on boot will no longer strand it — it keeps retrying until the adapter appears rather than failing once and giving up. You'll see `BLE adapter not ready … — retrying in Ns …` in the SignalK logs in the meantime.
465
512
 
466
513
  That said, it's cleaner to fix the boot ordering at the systemd level so the plugin finds the adapter ready on its first attempt. If SignalK runs as a systemd service (`systemctl status signalk`) and its unit file has no `[Unit]` section (check with `systemctl cat signalk`), add one:
@@ -486,8 +533,44 @@ sudo systemctl restart signalk
486
533
 
487
534
  This tells systemd to start `bluetoothd` first and wait for it before starting SignalK, rather than relying on both racing to start in parallel at boot.
488
535
 
536
+ ### Bluetooth Dongle with "No gpio to reset"
537
+
538
+ Example log:
539
+
540
+ ```
541
+ Bluetooth: hci0: No gpio to reset Realtek device, ignoring
542
+ Bluetooth: hci0: Unable to disable scanning: -110
543
+ Bluetooth: hci0: command 0x2042 tx timeout
544
+ Bluetooth: hci0: Opcode 0x2042 failed: -110
545
+ ```
546
+
547
+ This happens when USB autosuspend cycles the dongle in and out of low-power suspend while idle. When bluetoothd sends an HCI command while the device is suspended or mid-resume, it never gets answered — adapters like the popular ASUS USB-500 lack a GPIO to allow reset and its stuck, and spams logs.
548
+
549
+ In these examples the dongle is for vendor `0b05` and product `190e`, adapt for your own devices, use `lsusb` to find out, and if there's no `lsusb` command, install the `usbutils` package.
550
+
551
+ #### Example udev rule fix
552
+
553
+ Following file created at `/etc/udev/rules.d/99-bt500-no-autosuspend.rules`
554
+
555
+ ```
556
+ # Disable USB autosuspend for the ASUS USB-BT500 (RTL8761BU, 0b05:190e).
557
+ ACTION=="add", SUBSYSTEM=="usb", ATTR{idVendor}=="0b05", ATTR{idProduct}=="190e", TEST=="power/control", ATTR{power/control}="on"
558
+ ```
559
+
560
+ If `tlp` running to minimize power, it may have its own rules trying to suspend the Bluetooth dongle.
561
+
562
+ #### Example tlp fix
563
+
564
+ Following file created at /etc/tlp.d/99-bt500-no-autosuspend.conf
565
+
566
+ ```
567
+ USB_DENYLIST="0b05:190e"
568
+ ```
569
+
489
570
  ## Other ESL and General eInk Resources
490
571
 
572
+ ### Components
573
+
491
574
  - [Open ePaper Link](https://openepaperlink.de) - Alternative open source firmware to flash onto eInk shelf labels, with Home Assistant integration.
492
575
  - [zhsunyco-esl](https://github.com/roxburghm/zhsunyco-esl) - Python interface
493
576
  - [WoLink](https://github.com/NickWaterton/Wolink) - Python interface and protocol analysis
@@ -496,9 +579,14 @@ This tells systemd to start `bluetoothd` first and wait for it before starting S
496
579
  - [esp32-esl-system](https://github.com/giobauermeister/esp32-esl-system) - Docker and ESP32 based system for updating ESLs.
497
580
  - [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.
498
581
  - [ha-panda](https://github.com/moryoav/ha-panda) - Home Assistant integration for Panda ESLs ( a similar vendor to Zhsunyco).
582
+
583
+ ### Notes and Experiences
584
+
585
+ - [Cabalist Gicisky Image Notes](https://github.com/Cabalist/gicisky_image_notes)
499
586
  - [Dmitry.gr](https://dmitry.gr/?r=05.Projects&proj=29.%20eInk%20Price%20Tags) - Personal site of an ESL hacker
500
587
  - [Aaron Christobel](https://www.youtube.com/@atc1441) - YouTube channel of an ESL hacker.
501
588
  - [rbaron.net](https://rbaron.net/blog/2022/07/29/Daisy-chaining-multiple-electronic-shelf-labels) - Blog of an early ESL hacker.
589
+ ### Retail
502
590
  - [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.
503
591
  - [WaveShare](https://www.waveshare.com/product/displays/e-paper.htm) - Wide range of eInk displays for hardware projects, not limited to ESLs.
504
592
 
@@ -1,13 +1,16 @@
1
1
  #!/usr/bin/env node
2
2
  import { Command } from "commander";
3
- import { Colour } from "../devices/types";
3
+ import { Colour, CompressionFormat } from "../devices/types";
4
4
  import { ReframeMode } from "../render/reframe";
5
+ import { MirrorMode } from "../render/mirror";
5
6
  import { Binding } from "../render/binding";
6
7
  import { TemplateContext } from "../render/types";
7
8
  /** Tried in order when -u/--url is omitted (and -e/--example-data isn't given) - first one that answers wins. */
8
9
  export declare const DEFAULT_SIGNALK_URLS: string[];
9
10
  export declare function parseColours(code: string): Colour[];
10
11
  export declare function parseReframeMode(value: string): ReframeMode;
12
+ export declare function parseMirrorMode(value: string): MirrorMode;
13
+ export declare function parseCompressionFormat(value: string): CompressionFormat;
11
14
  /** Shared by every command that takes -u/--url and -e/--example-data - -e wins when both are present; when neither is given, probes DEFAULT_SIGNALK_URLS for a default. */
12
15
  export declare function assembleContext(opts: {
13
16
  url?: string;
package/dist/cli/index.js CHANGED
@@ -4,6 +4,8 @@ Object.defineProperty(exports, "__esModule", { value: true });
4
4
  exports.program = exports.DEFAULT_SIGNALK_URLS = void 0;
5
5
  exports.parseColours = parseColours;
6
6
  exports.parseReframeMode = parseReframeMode;
7
+ exports.parseMirrorMode = parseMirrorMode;
8
+ exports.parseCompressionFormat = parseCompressionFormat;
7
9
  exports.assembleContext = assembleContext;
8
10
  const promises_1 = require("fs/promises");
9
11
  const path_1 = require("path");
@@ -13,6 +15,8 @@ const registry_1 = require("../devices/registry");
13
15
  const zhsunyco_1 = require("../devices/zhsunyco");
14
16
  const gicisky_1 = require("../devices/gicisky");
15
17
  const bleDiscovery_1 = require("../devices/bleDiscovery");
18
+ const types_1 = require("../devices/types");
19
+ const mirror_1 = require("../render/mirror");
16
20
  const svgRenderer_1 = require("../render/svgRenderer");
17
21
  const png_1 = require("../render/png");
18
22
  const binding_1 = require("../render/binding");
@@ -45,6 +49,18 @@ function parseReframeMode(value) {
45
49
  }
46
50
  return value;
47
51
  }
52
+ function parseMirrorMode(value) {
53
+ if (!mirror_1.MIRROR_MODES.includes(value)) {
54
+ throw new Error(`unknown --mirror value "${value}" - expected one of ${mirror_1.MIRROR_MODES.join(", ")}`);
55
+ }
56
+ return value;
57
+ }
58
+ function parseCompressionFormat(value) {
59
+ if (!types_1.COMPRESSION_FORMATS.includes(value)) {
60
+ throw new Error(`unknown --compression-format value "${value}" - expected one of ${types_1.COMPRESSION_FORMATS.join(", ")}`);
61
+ }
62
+ return value;
63
+ }
48
64
  /** Probes DEFAULT_SIGNALK_URLS in order and returns the first that answers a plain GET - used when -u/--url is omitted. */
49
65
  async function resolveDefaultUrl() {
50
66
  for (const candidate of exports.DEFAULT_SIGNALK_URLS) {
@@ -179,18 +195,18 @@ exports.program
179
195
  await (0, bleDiscovery_1.forEachAdvertisedDevice)(adapter, async ({ device, address, name, manufacturerId, manufacturerData }) => {
180
196
  const driver = drivers.find((candidate) => candidate.matchesAdvertisement(name, manufacturerId));
181
197
  const mfr = manufacturerId !== undefined ? `0x${manufacturerId.toString(16).padStart(4, "0")}` : "";
198
+ const rssi = await device
199
+ .getRSSI()
200
+ .then((value) => (value === undefined ? undefined : Number(value)))
201
+ .catch(() => undefined);
182
202
  if (!driver) {
183
203
  if (opts.allDevices) {
184
- const rssi = await device
185
- .getRSSI()
186
- .then((value) => (value === undefined ? undefined : Number(value)))
187
- .catch(() => undefined);
188
204
  rows.push(["(unmatched)", address, name ?? "", "", "", "", mfr, "", String(rssi ?? "")]);
189
205
  }
190
206
  return;
191
207
  }
192
208
  matchedCount++;
193
- const found = await driver.identifyDevice(device, address, name, manufacturerId, manufacturerData);
209
+ const found = await driver.identifyDevice({ address, name, manufacturerId, manufacturerData, rssi }, () => (0, bleDiscovery_1.connectWithTimeout)(device, VENDOR_IDENTIFY_TIMEOUT_MS).then(() => (0, bleDiscovery_1.openNodeBleGattConnection)(device)));
194
210
  (0, log_1.logDebug)(`${driver.vendor}: identified ${found.name ?? found.address}`);
195
211
  const pid = found.pid !== undefined ? `0x${found.pid.toString(16).padStart(4, "0")}` : "";
196
212
  const hwid = found.hwVersion ? `0x${found.hwVersion}` : "";
@@ -226,6 +242,9 @@ exports.program
226
242
  .option("--voffset <px>", "vertical pixel offset of the panel - overrides the looked-up model for unsupported hardware (requires --colours)", "0")
227
243
  .option("--colours <code>", "device colour palette for unsupported hardware: BW, BWR, or BWRY - overrides the looked-up model (uses --width/--height/--voffset)")
228
244
  .option("--reframe <mode>", "how to fit the rendered image onto the device's actual panel size when it doesn't match: crop (default - place at top-left, truncating or leaving the rest blank), scale (stretch the template to the panel), fixed (reject the mismatch instead)", "crop")
245
+ .option("--mirror <mode>", "flip the image before sending: none (default), horizontal, vertical, or both (rotate 180°)", "none")
246
+ .option("--no-compress", 'send the image uncompressed (zhsunyco; gicisky 7.5"/10.2" or --compression-format chunked - compression is on by default)')
247
+ .option("--compression-format <format>", 'gicisky wire format: auto (default - the model\'s usual format) or chunked (experimental - send a 4.2" BWR compressed like the 7.5"/10.2")', "auto")
229
248
  .option("--connect-timeout <seconds>", "BLE connect timeout before giving up on an attempt", "30")
230
249
  .option("--retries <n>", "number of paint attempts (including the first) before giving up", "3")
231
250
  .action(async (opts) => {
@@ -269,6 +288,9 @@ exports.program
269
288
  modelOverride,
270
289
  connectTimeoutMs,
271
290
  reframe: parseReframeMode(opts.reframe),
291
+ mirror: parseMirrorMode(opts.mirror),
292
+ compress: opts.compress,
293
+ compressionFormat: parseCompressionFormat(opts.compressionFormat),
272
294
  });
273
295
  });
274
296
  console.log(`painted ${opts.address} (${bitmap.width}x${bitmap.height}) ${opts.colours}`);
@@ -287,7 +309,9 @@ exports.program
287
309
  .option("-f, --font <path>", "override a bundled font with this file (repeatable) - defaults to the bundled monospace/sans-serif/serif trio",
288
310
  // See the -r/--require option above for why `| undefined` is needed here despite commander's typings.
289
311
  (value, previous = []) => [...previous, value])
312
+ .option("--mirror <mode>", "flip the PNG the same way paint --mirror would: none (default), horizontal, vertical, or both", "none")
290
313
  .action(async (opts) => {
314
+ const mirror = parseMirrorMode(opts.mirror);
291
315
  const svgSource = await (0, promises_1.readFile)(opts.template, "utf-8");
292
316
  const declared = (0, binding_1.readTemplateDimensions)(svgSource);
293
317
  const width = resolveDimension(opts.width, [declared.width], DEFAULT_RENDER_WIDTH);
@@ -295,7 +319,7 @@ exports.program
295
319
  const bindings = (0, binding_1.findBindings)(svgSource);
296
320
  const context = await assembleContext(opts, bindings);
297
321
  const renderer = opts.font ? new svgRenderer_1.SvgRenderer(opts.font) : new svgRenderer_1.SvgRenderer();
298
- const bitmap = await renderer.render(opts.template, context, width, height, (0, path_1.dirname)(opts.template), config_1.BUNDLED_TEMPLATES_DIR);
322
+ const bitmap = (0, mirror_1.mirrorBitmap)(await renderer.render(opts.template, context, width, height, (0, path_1.dirname)(opts.template), config_1.BUNDLED_TEMPLATES_DIR), mirror);
299
323
  await (0, promises_1.writeFile)(opts.output, (0, png_1.bitmapToPng)(bitmap));
300
324
  console.log(`wrote ${opts.output} (${bitmap.width}x${bitmap.height})`);
301
325
  });
package/dist/config.d.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  import { ServerAPI } from "@signalk/server-api";
2
- import { Colour, DiscoveredDevice } from "./devices/types";
2
+ import { Colour, CompressionFormat, DiscoveredDevice } from "./devices/types";
3
3
  import { ReframeMode } from "./render/reframe";
4
+ import { MirrorMode } from "./render/mirror";
4
5
  /**
5
6
  * Special `device` value meaning "every currently-known discovered device" instead of one specific
6
7
  * BLE address - lets a single `DeviceConfig` entry (one template, one trigger) broadcast to every
@@ -26,8 +27,6 @@ export interface DeviceConfig {
26
27
  * `./render/templateProviders.ts`) tailoring content to where the label actually sits.
27
28
  */
28
29
  description?: string;
29
- /** Per-device override; if omitted, the vendor driver may fall back to a stock/manufacturer-default key. */
30
- aesKey?: string;
31
30
  /**
32
31
  * Either one specific `.svg` file, or the name of a *template-family* directory holding several
33
32
  * same-purpose templates for different panel sizes/colour-sets, named `<width>x<height>-<colours>.svg`
@@ -48,8 +47,6 @@ export interface DeviceConfig {
48
47
  intervalHours?: number;
49
48
  /** ...at this minute past the hour. */
50
49
  intervalMinute?: number;
51
- /** One-shot override to repaint even if the data is unchanged; cleared automatically once that repaint completes. */
52
- forceRepaint?: boolean;
53
50
  /**
54
51
  * How to fit the rendered image onto the device's actual panel size when it doesn't match (see
55
52
  * `ReframeMode`) - e.g. a template family with no variant sized for this particular label. Left
@@ -58,7 +55,34 @@ export interface DeviceConfig {
58
55
  * showing *something*, even off-size, beats a repaint that just fails outright.
59
56
  */
60
57
  reframe?: ReframeMode;
58
+ /** Settings most labels never need, grouped so the admin UI shows them in their own "Advanced settings" box. */
59
+ advanced?: AdvancedDeviceSettings;
61
60
  }
61
+ export interface AdvancedDeviceSettings {
62
+ /** Compress the upload (zhsunyco, and gicisky's chunked 7.5"/10.2" panels - ignored otherwise). Unset means on; turn off if a device fails to show compressed images. */
63
+ compress?: boolean;
64
+ /** Flip the image before sending - for a panel whose layout is mirrored, or one mounted upside down (`"both"`). Unset means `"none"`. */
65
+ mirror?: MirrorMode;
66
+ /**
67
+ * Experimental opt-in wire format (gicisky only) - `"chunked"` sends a 4.2" BWR (or another plain
68
+ * two-plane panel) QuickLZ-compressed like the 7.5"/10.2". Unset means `"auto"`, the model's own
69
+ * format. See `CompressionFormat`.
70
+ */
71
+ compressionFormat?: CompressionFormat;
72
+ /** One-shot override to repaint even if the data is unchanged; cleared automatically once that repaint completes. */
73
+ forceRepaint?: boolean;
74
+ /** Per-device override; if omitted, the vendor driver may fall back to a stock/manufacturer-default key. */
75
+ aesKey?: string;
76
+ /** Per-device override of `PluginConfig.paintConnectTimeoutSeconds` - unset uses the plugin-wide value. */
77
+ paintConnectTimeoutSeconds?: number;
78
+ /** Per-device override of `PluginConfig.paintRetries` - unset uses the plugin-wide value. */
79
+ paintRetries?: number;
80
+ }
81
+ /** Applies `migrateDeviceConfig` to every device entry - see `healStoredConfig` for persisting the result. */
82
+ export declare function migrateConfig<T extends Partial<PluginConfig>>(config: T): {
83
+ config: T;
84
+ migrated: boolean;
85
+ };
62
86
  export interface PluginConfig {
63
87
  /**
64
88
  * Directory the plugin scans for template files, instead of an upload UI - follows
@@ -71,10 +95,20 @@ export interface PluginConfig {
71
95
  * signalk-bluetti-plugin does. Off by default - a `device: ALL_DEVICES` entry scans on demand the
72
96
  * first time it has nothing discovered yet (see `resolveTargets` in `repaintScheduler.ts`), and an
73
97
  * explicit device selection only ever needed this to populate the dropdown once at initial setup.
98
+ * Also unnecessary with `useBleApi` on, which discovers continuously in the background instead (see
99
+ * `startBleApiDiscoveryListener` in `discoveryCoordinator.ts`) - kept meaningful mainly for direct
100
+ * BlueZ access, which has no continuous-scan equivalent to piggyback on.
74
101
  */
75
102
  scanOnStart: boolean;
76
103
  /** How long the startup scan runs, in seconds. */
77
104
  scanDurationSeconds: number;
105
+ /**
106
+ * Route BLE access through the SignalK server's BLE Manager API (`app.bleApi`, server >= 2.32.0)
107
+ * instead of connecting to BlueZ directly, so this plugin shares the adapter with other BLE plugins
108
+ * instead of contending for it - see `bleBackend.ts`. Off by default, and only ever offered in the
109
+ * config schema when the running server actually has `app.bleApi` (see `configSchema` below).
110
+ */
111
+ useBleApi: boolean;
78
112
  /** How long to wait for a device to accept a BLE connection before giving up on a repaint attempt, in seconds. */
79
113
  paintConnectTimeoutSeconds: number;
80
114
  /** How many times to attempt a repaint (including the first try) before giving up and reporting failure. */
@@ -130,16 +164,21 @@ export declare const RENDER_FALLBACK_TEMPLATE_NAME = ".error";
130
164
  export declare function defaultConfig(): PluginConfig;
131
165
  export declare function readCurrentConfig(app: ServerAPI): Partial<PluginConfig>;
132
166
  /**
133
- * Actively rewrites the on-disk file once it's nested (see `readCurrentConfig`'s doc comment) -
134
- * `readCurrentConfig` alone only self-heals in memory for callers that go through it, but the admin
135
- * UI's own config-editing form round-trips whatever raw JSON it was handed verbatim, including a
136
- * stray nested `configuration` key it never touches (no schema field maps to it) - so left alone,
137
- * every future save from the UI keeps re-persisting that dead weight forever (see
138
- * `support/signalk-einklabel-plugin.json`). Called once at plugin start, which - unlike
139
- * `clearForceRepaint` - isn't gated on any device having `forceRepaint` set, so a nested file gets
140
- * flattened even if nothing ever triggers that path.
167
+ * Actively rewrites the on-disk file when it's in an outdated shape - `readCurrentConfig` alone only
168
+ * fixes things up in memory for callers that go through it, but the admin UI's config form
169
+ * round-trips whatever raw JSON it was handed verbatim. Two shapes are healed:
170
+ *
171
+ * - A nested file (see `readCurrentConfig`'s doc comment): left alone, the UI keeps re-persisting a
172
+ * stray nested `configuration` key it never touches (no schema field maps to it) forever (see
173
+ * `support/signalk-einklabel-plugin.json`).
174
+ * - Advanced device settings saved before they were grouped under `advanced` (see
175
+ * `migrateDeviceConfig`): left alone, the UI would show that group empty, even though the plugin
176
+ * itself still honours the old values.
177
+ *
178
+ * Called once at plugin start, which - unlike `clearForceRepaint` - isn't gated on any device having
179
+ * `forceRepaint` set, so an outdated file gets fixed even if nothing ever triggers that path.
141
180
  */
142
- export declare function healNestedConfig(app: ServerAPI): void;
181
+ export declare function healStoredConfig(app: ServerAPI): void;
143
182
  /** See `resolveDir` - `templatesDir`'s own resolution. */
144
183
  export declare function resolveTemplatesDir(templatesDir: string | undefined): string;
145
184
  export declare function parseDevice(device: string): {