@rhizomatics/signalk-einklabel-plugin 0.8.1 → 0.9.0-beta1
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 +19 -0
- package/README.md +29 -5
- package/dist/config.d.ts +52 -8
- package/dist/config.js +144 -15
- package/dist/devices/discoveredDevicesStore.d.ts +32 -0
- package/dist/devices/discoveredDevicesStore.js +67 -0
- package/dist/devices/discoveryCoordinator.d.ts +19 -0
- package/dist/devices/discoveryCoordinator.js +59 -0
- package/dist/devices/zhsunyco/encode.js +33 -11
- package/dist/devices/zhsunyco/index.js +1 -1
- package/dist/plugin.js +23 -62
- package/dist/render/assets.d.ts +7 -1
- package/dist/render/assets.js +28 -12
- package/dist/render/svgRenderer.js +1 -1
- package/dist/repaintScheduler.js +132 -40
- package/package.json +1 -1
- package/templates/{assets → .assets}/lunar_phases/README.md +6 -2
- package/templates/.assets/lunar_phases/third_quarter.svg +52 -0
- package/templates/tide.svg +18 -10
- package/templates/tides/250x128-BWRY.svg +160 -0
- package/templates/tides/416x240-BWRY.svg +288 -0
- /package/templates/{assets → .assets}/lunar_phases/first_quarter.svg +0 -0
- /package/templates/{assets → .assets}/lunar_phases/full_moon.svg +0 -0
- /package/templates/{assets/lunar_phases/third_quarter.svg → .assets/lunar_phases/last_quarter.svg} +0 -0
- /package/templates/{assets → .assets}/lunar_phases/new_moon.svg +0 -0
- /package/templates/{assets → .assets}/lunar_phases/waning_crescent.svg +0 -0
- /package/templates/{assets → .assets}/lunar_phases/waning_gibbous.svg +0 -0
- /package/templates/{assets → .assets}/lunar_phases/waxing_crescent.svg +0 -0
- /package/templates/{assets → .assets}/lunar_phases/waxing_gibbous.svg +0 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,22 @@
|
|
|
1
|
+
# 0.9.0
|
|
2
|
+
|
|
3
|
+
- Rename `third_quarter` to `last_quarter` for moon phase icons in `templates/.assets/lunar_phases` to match Derived Data plugin
|
|
4
|
+
- `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
|
+
- 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
|
|
8
|
+
- Fix most cases of scanned devices not appearing in device dropdown choice
|
|
9
|
+
- `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
|
|
12
|
+
- Now support a directory of templates, where each is named like `416-240-BWRY.svg` to support same functions on different devices.
|
|
13
|
+
- 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
|
+
- `template/assets` is now `template/.assets`
|
|
15
|
+
|
|
16
|
+
# 0.8.2
|
|
17
|
+
|
|
18
|
+
- Work around a SignalK bug in Server API where `ResourcesApi.listResources()` merges provider values in undetermined order and ignores specified provider.
|
|
19
|
+
|
|
1
20
|
# 0.8.1
|
|
2
21
|
|
|
3
22
|
- A preferred `provider` can now be set for `resources`.
|
package/README.md
CHANGED
|
@@ -89,7 +89,11 @@ Since these are ultra-low power devices, they don't respond instantly to either
|
|
|
89
89
|
|
|
90
90
|
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.
|
|
91
91
|
|
|
92
|
-
The plugin
|
|
92
|
+
The plugin can optionally re-scan whenever it starts up (off by default), although this isn't essential once a label has been configured. Devices found by any scan are remembered across restarts - see the FAQ below.
|
|
93
|
+
|
|
94
|
+
#### Selecting a Device
|
|
95
|
+
|
|
96
|
+
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.
|
|
93
97
|
|
|
94
98
|
### Scheduling
|
|
95
99
|
|
|
@@ -107,6 +111,16 @@ The devices will be painted when the plugin starts, and then every time the sele
|
|
|
107
111
|
|
|
108
112
|
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.
|
|
109
113
|
|
|
114
|
+
### Template Families (multiple panel sizes/colours)
|
|
115
|
+
|
|
116
|
+
A "Template" selection can either be one specific `.svg` file, or a _directory_ holding several same-purpose templates 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. Each file is named `<width>x<height>-<colours>.svg`, where `<colours>` is one letter per supported colour: `B`(lack)/`W`(hite)/`R`(ed)/`Y`(ellow) - e.g. `BWRY` for a 4-colour panel, `BWR` for a 3-colour one.
|
|
117
|
+
|
|
118
|
+
Selecting the directory (e.g. `tides`) instead of one file lets one `DeviceConfig` entry - especially a `device: "All discovered devices"` entry covering several different physical panels - automatically pick the best-fitting file for each device's actual size/colours, trying in order:
|
|
119
|
+
|
|
120
|
+
1. An exact width/height/colour-set match.
|
|
121
|
+
2. Failing that, width/height alone (any colour-set).
|
|
122
|
+
3. Failing that too, the nearest width, tie-broken by whichever file's own height/width ratio is closest to the device's.
|
|
123
|
+
|
|
110
124
|
### Template Source Specification
|
|
111
125
|
|
|
112
126
|
In the `description` of the SVG text box, use a comma separated set of key value pairs to define the data source and formatting.
|
|
@@ -156,17 +170,17 @@ These can all be combined as in `source=resources,resource=tides,provider=tides,
|
|
|
156
170
|
|
|
157
171
|
### Non-Textual Fields (Images)
|
|
158
172
|
|
|
159
|
-
The same `<desc>` mechanism works on an `<image>` element instead of a `<text>` element, for a value that's better shown as a picture than as text - a moon phase icon, a wind direction arrow, a weather condition glyph, and so on. Rather than substituting text, the resolved value picks one of a directory of `.svg` files to embed, by an extra required `assets=` key naming that directory - an
|
|
173
|
+
The same `<desc>` mechanism works on an `<image>` element instead of a `<text>` element, for a value that's better shown as a picture than as text - a moon phase icon, a wind direction arrow, a weather condition glyph, and so on. Rather than substituting text, the resolved value picks one of a directory of `.svg` files to embed, by an extra required `assets=` key naming that directory - an `.assets/<name>` sub-directory looked up in your configured `templates` directory first, and the bundled `templates` directory otherwise. For example, the tide clock's moon phase icon uses:
|
|
160
174
|
|
|
161
175
|
```
|
|
162
176
|
path=environment.moon.phaseName,assets=lunar_phases
|
|
163
177
|
```
|
|
164
178
|
|
|
165
|
-
which resolves against `templates
|
|
179
|
+
which resolves against `templates/.assets/lunar_phases/` (bundled, or your own configured `templates` directory's `.assets/lunar_phases/` if you have one). The resolved value (e.g. `"Waning Gibbous"`, as published by the [derived-data](https://www.npmjs.com/package/signalk-derived-data) plugin) is normalized to match a filename - lower-cased, punctuation and spaces collapsed to underscores - so `"Waning Gibbous"` picks `waning_gibbous.svg` out of that directory. If the underlying path has no value at all (e.g. the `derived-data` plugin isn't installed), or the value doesn't normalize to any file in the directory, the `<image>` element is omitted from that render - no broken image, no placeholder, nothing shown - and a line is logged to the console so a missing/unmatched value isn't silently invisible.
|
|
166
180
|
|
|
167
|
-
If you don't like the bundled moon phase icons, save your own `<value>.svg` files in the
|
|
181
|
+
If you don't like the bundled moon phase icons, save your own `<value>.svg` files in the `.assets/lunar_phases` sub-directory of your configured `templates` directory - the whole directory is used in place of the bundled one, so add all 8 phases you want to keep, not just the ones you're changing.
|
|
168
182
|
|
|
169
|
-
This is a general mechanism, not specific to moon phases - any `source`/`context`/`path`/`format` combination valid for a `<text>` binding works here too (a `source=resources` value, an explicit `category=`, etc.), the only difference is the required `assets=` directory and the "no match -> no image" behaviour instead of substituted text. To add your own, put a directory of `<value>.svg` files under an
|
|
183
|
+
This is a general mechanism, not specific to moon phases - any `source`/`context`/`path`/`format` combination valid for a `<text>` binding works here too (a `source=resources` value, an explicit `category=`, etc.), the only difference is the required `assets=` directory and the "no match -> no image" behaviour instead of substituted text. To add your own, put a directory of `<value>.svg` files under an `.assets/<name>` sub-directory of your `templates` directory, add an `<image>` element in your SVG editor at the size/position you want, and give it a `<desc>` the same way you would a text field - overriding just the template, just its assets, or both together, all work independently.
|
|
170
184
|
|
|
171
185
|
### Fonts
|
|
172
186
|
|
|
@@ -305,6 +319,10 @@ For example, `npx esl-cli fields -t templates/tide.svg -e examples` will show al
|
|
|
305
319
|
|
|
306
320
|
## Frequently Asked Questions
|
|
307
321
|
|
|
322
|
+
### I can't see my device as a choice on the drop-down list after scan
|
|
323
|
+
|
|
324
|
+
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
|
+
|
|
308
326
|
### Sometimes values are missing on the display
|
|
309
327
|
|
|
310
328
|
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.
|
|
@@ -322,9 +340,15 @@ If you're a global cruiser, then use something like [signalk-set-gps-timezone](h
|
|
|
322
340
|
|
|
323
341
|
Try a BLE proxy device, ESP32 is popular for this.
|
|
324
342
|
|
|
343
|
+
### Can't edit the text contents of SVG template in VSCode
|
|
344
|
+
|
|
345
|
+
If you have an SVG viewer extension, this wll show the image rather than allowing editing of text. To solve, right click on the file in VSCode _Explorer_ view and choose to edit with _Text Editor_.
|
|
346
|
+
|
|
325
347
|
## Other ESL and General eInk Resources
|
|
326
348
|
|
|
327
349
|
- [Open ePaper Link](https://openepaperlink.de) - Alternative open source firmware to flash onto eInk shelf labels, with Home Assistant integration.
|
|
350
|
+
- [zhsynyco-esl](https://github.com/roxburghm/zhsunyco-esl) - Python interface
|
|
351
|
+
- [WoLink](https://github.com/NickWaterton/Wolink) - Python interface and protocol analysis
|
|
328
352
|
- [e-ink dashboard for Signal K](https://github.com/meri-imperiumi/dashboard) - Waveshare display based multi instrument display.
|
|
329
353
|
- [eInk Dashboard Modern SK](https://github.com/VladimirKalachikhin/e-inkDashboardModernSK) - SignalK dashboard for non-ESL eInk display.
|
|
330
354
|
- [esp32-esl-system](https://github.com/giobauermeister/esp32-esl-system) - Docker and ESP32 based system for updating ESLs.
|
package/dist/config.d.ts
CHANGED
|
@@ -1,15 +1,31 @@
|
|
|
1
1
|
import { ServerAPI } from "@signalk/server-api";
|
|
2
|
-
import { DiscoveredDevice } from "./devices/types";
|
|
2
|
+
import { Colour, DiscoveredDevice } from "./devices/types";
|
|
3
|
+
/**
|
|
4
|
+
* Special `device` value meaning "every currently-known discovered device" instead of one specific
|
|
5
|
+
* BLE address - lets a single `DeviceConfig` entry (one template, one trigger) broadcast to every
|
|
6
|
+
* physical label without picking each one out of the scan dropdown individually, e.g. several
|
|
7
|
+
* identical labels on a boat, or just to skip the scan-then-select step for the common single-label
|
|
8
|
+
* case. Resolved lazily at repaint time (see `resolveTargets` in `repaintScheduler.ts`), triggering
|
|
9
|
+
* an on-demand scan itself if nothing's been discovered yet.
|
|
10
|
+
*/
|
|
11
|
+
export declare const ALL_DEVICES = "ALL";
|
|
3
12
|
export interface DeviceConfig {
|
|
4
13
|
friendlyName: string;
|
|
5
14
|
/**
|
|
6
|
-
* `"<vendor>:<pid>[:<hwVersion>]@<address>"`, picked from a combined enum of recently
|
|
7
|
-
* scanned devices so one selection sets both the model (width/height/colours, known
|
|
8
|
-
*
|
|
15
|
+
* Either `"<vendor>:<pid>[:<hwVersion>]@<address>"`, picked from a combined enum of recently
|
|
16
|
+
* scanned devices so one selection sets both the model (width/height/colours, known without a
|
|
17
|
+
* live BLE read) and the BLE address, or the special value `ALL_DEVICES` (see above).
|
|
9
18
|
*/
|
|
10
19
|
device: string;
|
|
11
20
|
/** Per-device override; if omitted, the vendor driver may fall back to a stock/manufacturer-default key. */
|
|
12
21
|
aesKey?: string;
|
|
22
|
+
/**
|
|
23
|
+
* Either one specific `.svg` file, or the name of a *template-family* directory holding several
|
|
24
|
+
* same-purpose templates for different panel sizes/colour-sets, named `<width>x<height>-<colours>.svg`
|
|
25
|
+
* (e.g. `templates/tides/416x240-BWRY.svg`) - `pickBestVariant` then picks whichever file inside it
|
|
26
|
+
* best matches this entry's actual device(s), so one `DeviceConfig` (and one `ALL_DEVICES` entry in
|
|
27
|
+
* particular) works across different panel sizes without the user picking a file for each.
|
|
28
|
+
*/
|
|
13
29
|
templateName: string;
|
|
14
30
|
repaintTrigger: "subscription" | "interval";
|
|
15
31
|
/** SignalK path to subscribe to when `repaintTrigger` is `subscription` - a repaint is considered on every delta. */
|
|
@@ -28,7 +44,12 @@ export interface PluginConfig {
|
|
|
28
44
|
* `~/.signalk`, or an absolute path. Use `resolveTemplatesDir` to turn this into an actual path.
|
|
29
45
|
*/
|
|
30
46
|
templatesDir: string;
|
|
31
|
-
/**
|
|
47
|
+
/**
|
|
48
|
+
* Run a short BLE scan on plugin start and report discoveries via plugin status, like
|
|
49
|
+
* signalk-bluetti-plugin does. Off by default - a `device: ALL_DEVICES` entry scans on demand the
|
|
50
|
+
* first time it has nothing discovered yet (see `resolveTargets` in `repaintScheduler.ts`), and an
|
|
51
|
+
* explicit device selection only ever needed this to populate the dropdown once at initial setup.
|
|
52
|
+
*/
|
|
32
53
|
scanOnStart: boolean;
|
|
33
54
|
/** How long the startup scan runs, in seconds. */
|
|
34
55
|
scanDurationSeconds: number;
|
|
@@ -69,10 +90,23 @@ export interface PluginConfig {
|
|
|
69
90
|
* user's `templatesDir` takes priority. Exported so `SvgRenderer` can fall back to it when resolving
|
|
70
91
|
* an `assets=` binding's directory (see `resolveAssetPath` in `./render/assets.ts`) - overriding a
|
|
71
92
|
* bundled template shouldn't also require duplicating its bundled asset sets (e.g.
|
|
72
|
-
* `templates
|
|
93
|
+
* `templates/.assets/lunar_phases`) just to keep a binding the override never touched working.
|
|
73
94
|
*/
|
|
74
95
|
export declare const BUNDLED_TEMPLATES_DIR: string;
|
|
75
96
|
export declare function defaultConfig(): PluginConfig;
|
|
97
|
+
/**
|
|
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.
|
|
108
|
+
*/
|
|
109
|
+
export declare function readCurrentConfig(app: ServerAPI): Partial<PluginConfig>;
|
|
76
110
|
/**
|
|
77
111
|
* Resolves the user-facing `templatesDir` setting to an actual directory, mirroring
|
|
78
112
|
* signalk-parquet's `outputDirectory` convention: empty means the default location, a relative
|
|
@@ -86,7 +120,17 @@ export declare function parseDevice(device: string): {
|
|
|
86
120
|
hwVersion?: string;
|
|
87
121
|
address: string;
|
|
88
122
|
} | undefined;
|
|
89
|
-
/**
|
|
90
|
-
|
|
123
|
+
/**
|
|
124
|
+
* Resolves a template name to an actual file path - a local template overrides the bundled one of the
|
|
125
|
+
* same name. `templateName` can also name a template-family *directory* (see `pickBestVariant`), in
|
|
126
|
+
* which case `target` (the device's actual width/height/colours) picks the best file inside it. A
|
|
127
|
+
* `target`-less call, or one where `templateName` just isn't a directory, falls through to the plain
|
|
128
|
+
* flat-file behaviour - the only kind the CLI (which always names an exact file) ever uses.
|
|
129
|
+
*/
|
|
130
|
+
export declare function resolveTemplatePath(templatesDir: string, templateName: string, target?: {
|
|
131
|
+
width: number;
|
|
132
|
+
height: number;
|
|
133
|
+
colours: Colour[];
|
|
134
|
+
}): string;
|
|
91
135
|
export declare function configSchema(app: ServerAPI, discovered?: DiscoveredDevice[]): object;
|
|
92
136
|
export declare function configUiSchema(): object;
|
package/dist/config.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.BUNDLED_TEMPLATES_DIR = void 0;
|
|
3
|
+
exports.BUNDLED_TEMPLATES_DIR = exports.ALL_DEVICES = void 0;
|
|
4
4
|
exports.defaultConfig = defaultConfig;
|
|
5
|
+
exports.readCurrentConfig = readCurrentConfig;
|
|
5
6
|
exports.resolveTemplatesDir = resolveTemplatesDir;
|
|
6
7
|
exports.parseDevice = parseDevice;
|
|
7
8
|
exports.resolveTemplatePath = resolveTemplatePath;
|
|
@@ -11,13 +12,22 @@ const fs_1 = require("fs");
|
|
|
11
12
|
const os_1 = require("os");
|
|
12
13
|
const path_1 = require("path");
|
|
13
14
|
const resolveApiUrl_1 = require("./resolveApiUrl");
|
|
15
|
+
/**
|
|
16
|
+
* Special `device` value meaning "every currently-known discovered device" instead of one specific
|
|
17
|
+
* BLE address - lets a single `DeviceConfig` entry (one template, one trigger) broadcast to every
|
|
18
|
+
* physical label without picking each one out of the scan dropdown individually, e.g. several
|
|
19
|
+
* identical labels on a boat, or just to skip the scan-then-select step for the common single-label
|
|
20
|
+
* case. Resolved lazily at repaint time (see `resolveTargets` in `repaintScheduler.ts`), triggering
|
|
21
|
+
* an on-demand scan itself if nothing's been discovered yet.
|
|
22
|
+
*/
|
|
23
|
+
exports.ALL_DEVICES = "ALL";
|
|
14
24
|
/**
|
|
15
25
|
* The package's own bundled `templates/` directory (ships alongside `dist/`, see
|
|
16
26
|
* package.json's `files`) - templates here are always available, but a same-named template in the
|
|
17
27
|
* user's `templatesDir` takes priority. Exported so `SvgRenderer` can fall back to it when resolving
|
|
18
28
|
* an `assets=` binding's directory (see `resolveAssetPath` in `./render/assets.ts`) - overriding a
|
|
19
29
|
* bundled template shouldn't also require duplicating its bundled asset sets (e.g.
|
|
20
|
-
* `templates
|
|
30
|
+
* `templates/.assets/lunar_phases`) just to keep a binding the override never touched working.
|
|
21
31
|
*/
|
|
22
32
|
exports.BUNDLED_TEMPLATES_DIR = (0, path_1.join)(__dirname, "..", "templates");
|
|
23
33
|
const SIGNALK_HOME_DIR = (0, path_1.join)((0, os_1.homedir)(), ".signalk");
|
|
@@ -25,7 +35,7 @@ const DEFAULT_TEMPLATES_DIR = (0, path_1.join)(SIGNALK_HOME_DIR, "einklabel", "t
|
|
|
25
35
|
function defaultConfig() {
|
|
26
36
|
return {
|
|
27
37
|
templatesDir: "",
|
|
28
|
-
scanOnStart:
|
|
38
|
+
scanOnStart: false,
|
|
29
39
|
scanDurationSeconds: 20,
|
|
30
40
|
paintConnectTimeoutSeconds: 30,
|
|
31
41
|
paintRetries: 3,
|
|
@@ -33,6 +43,46 @@ function defaultConfig() {
|
|
|
33
43
|
devices: [],
|
|
34
44
|
};
|
|
35
45
|
}
|
|
46
|
+
const PLUGIN_CONFIG_KEYS = [
|
|
47
|
+
"templatesDir",
|
|
48
|
+
"scanOnStart",
|
|
49
|
+
"scanDurationSeconds",
|
|
50
|
+
"paintConnectTimeoutSeconds",
|
|
51
|
+
"paintRetries",
|
|
52
|
+
"settleSeconds",
|
|
53
|
+
"signalkApiUrl",
|
|
54
|
+
"devices",
|
|
55
|
+
];
|
|
56
|
+
/**
|
|
57
|
+
* `app.readPluginOptions()`/`savePluginOptions()` aren't actually symmetric despite what their doc
|
|
58
|
+
* comments imply: signalk-server's `readPluginOptions()` returns the *whole* on-disk file
|
|
59
|
+
* (`{ configuration, enabled, enableDebug, enableLogging }`), not just the `configuration` object
|
|
60
|
+
* `savePluginOptions()` writes into - see signalk-server's `interfaces/plugins.ts`
|
|
61
|
+
* (`appCopy.readPluginOptions = () => getPluginOptions(plugin.id)` vs.
|
|
62
|
+
* `appCopy.savePluginOptions = (configuration, cb) => savePluginOptions(id, { ...getPluginOptions(id), configuration }, cb)`).
|
|
63
|
+
* Spreading that return value straight back into a save call therefore nests the whole file one level
|
|
64
|
+
* deeper inside its own `configuration` key every time (see `support/signalk-einklabel-plugin.json` and
|
|
65
|
+
* `clearForceRepaint` in `./repaintScheduler.ts`). Unwrapping any such nesting and keeping only recognised
|
|
66
|
+
* fields here means every save collapses back down instead of growing, self-healing an already-corrupted file.
|
|
67
|
+
*/
|
|
68
|
+
function readCurrentConfig(app) {
|
|
69
|
+
let raw = app.readPluginOptions();
|
|
70
|
+
while (raw &&
|
|
71
|
+
typeof raw === "object" &&
|
|
72
|
+
"configuration" in raw &&
|
|
73
|
+
typeof raw.configuration === "object") {
|
|
74
|
+
raw = raw.configuration;
|
|
75
|
+
}
|
|
76
|
+
const result = {};
|
|
77
|
+
if (raw && typeof raw === "object") {
|
|
78
|
+
for (const key of PLUGIN_CONFIG_KEYS) {
|
|
79
|
+
if (key in raw) {
|
|
80
|
+
result[key] = raw[key];
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
return result;
|
|
85
|
+
}
|
|
36
86
|
/**
|
|
37
87
|
* Resolves the user-facing `templatesDir` setting to an actual directory, mirroring
|
|
38
88
|
* signalk-parquet's `outputDirectory` convention: empty means the default location, a relative
|
|
@@ -52,12 +102,13 @@ function resolveTemplatesDir(templatesDir) {
|
|
|
52
102
|
* so the user can at least see what was found and report the PID; repainting such a device still
|
|
53
103
|
* does nothing without a model override, since there's no width/height to render with) - plus, as a
|
|
54
104
|
* fallback, whatever's already saved so an existing selection doesn't vanish from the dropdown just
|
|
55
|
-
* because this particular run hasn't re-scanned it yet.
|
|
105
|
+
* because this particular run hasn't re-scanned it yet. `ALL_DEVICES` is always offered first,
|
|
106
|
+
* regardless of what's been scanned - see its own doc comment.
|
|
56
107
|
*/
|
|
57
108
|
function deviceOptions(discovered, current) {
|
|
58
|
-
const values = [];
|
|
59
|
-
const labels = [];
|
|
60
|
-
const seen = new Set();
|
|
109
|
+
const values = [exports.ALL_DEVICES];
|
|
110
|
+
const labels = ["All discovered devices"];
|
|
111
|
+
const seen = new Set([exports.ALL_DEVICES]);
|
|
61
112
|
for (const found of discovered) {
|
|
62
113
|
if (found.pid === undefined) {
|
|
63
114
|
continue;
|
|
@@ -97,14 +148,89 @@ function listSvgFiles(dir) {
|
|
|
97
148
|
return [];
|
|
98
149
|
}
|
|
99
150
|
}
|
|
100
|
-
|
|
151
|
+
const VARIANT_COLOUR_LETTERS = { B: "black", W: "white", R: "red", Y: "yellow" };
|
|
152
|
+
const VARIANT_FILENAME = /^(\d+)x(\d+)-([BWRY]+)\.svg$/;
|
|
153
|
+
/**
|
|
154
|
+
* Parses a template-family file name, e.g. `416x240-BWRY.svg` -> width 416, height 240, colours
|
|
155
|
+
* black/white/red/yellow (one letter per supported colour: B/W/R/Y). `undefined` for anything that
|
|
156
|
+
* doesn't match this convention, e.g. a plain, non-family template file.
|
|
157
|
+
*/
|
|
158
|
+
function parseTemplateVariant(fileName) {
|
|
159
|
+
const match = VARIANT_FILENAME.exec(fileName);
|
|
160
|
+
if (!match)
|
|
161
|
+
return undefined;
|
|
162
|
+
const colours = match[3].split("").map((letter) => VARIANT_COLOUR_LETTERS[letter]);
|
|
163
|
+
return { fileName, width: Number(match[1]), height: Number(match[2]), colours };
|
|
164
|
+
}
|
|
165
|
+
function listTemplateVariants(dir) {
|
|
166
|
+
return listSvgFiles(dir)
|
|
167
|
+
.map(parseTemplateVariant)
|
|
168
|
+
.filter((variant) => variant !== undefined);
|
|
169
|
+
}
|
|
170
|
+
/** A directory only counts as a template-family option if it actually has at least one parseable variant file in it - otherwise it's something else entirely, e.g. `.assets`. */
|
|
171
|
+
function listTemplateFamilies(dir) {
|
|
172
|
+
let entries;
|
|
173
|
+
try {
|
|
174
|
+
entries = (0, fs_1.readdirSync)(dir, { withFileTypes: true });
|
|
175
|
+
}
|
|
176
|
+
catch {
|
|
177
|
+
return [];
|
|
178
|
+
}
|
|
179
|
+
return entries
|
|
180
|
+
.filter((entry) => entry.isDirectory() && listTemplateVariants((0, path_1.join)(dir, entry.name)).length > 0)
|
|
181
|
+
.map((entry) => entry.name);
|
|
182
|
+
}
|
|
183
|
+
/** Local templates (files or template-family directories) take priority over a same-named bundled one; both show up as options. */
|
|
101
184
|
function templateNameOptions(templatesDir) {
|
|
102
|
-
const local = listSvgFiles(templatesDir);
|
|
103
|
-
const bundled = listSvgFiles(exports.BUNDLED_TEMPLATES_DIR).filter((name) => !local.includes(name));
|
|
185
|
+
const local = [...listSvgFiles(templatesDir), ...listTemplateFamilies(templatesDir)];
|
|
186
|
+
const bundled = [...listSvgFiles(exports.BUNDLED_TEMPLATES_DIR), ...listTemplateFamilies(exports.BUNDLED_TEMPLATES_DIR)].filter((name) => !local.includes(name));
|
|
104
187
|
return [...local, ...bundled];
|
|
105
188
|
}
|
|
106
|
-
|
|
107
|
-
|
|
189
|
+
function sameColours(a, b) {
|
|
190
|
+
if (a.length !== b.length)
|
|
191
|
+
return false;
|
|
192
|
+
const setA = new Set(a);
|
|
193
|
+
return b.every((colour) => setA.has(colour));
|
|
194
|
+
}
|
|
195
|
+
/**
|
|
196
|
+
* Picks the best-fitting file within a template-family directory (see `parseTemplateVariant`) for a
|
|
197
|
+
* device's actual panel, trying each of these in turn: (1) an exact width/height/colour-set match,
|
|
198
|
+
* (2) width/height alone (ignoring colour-set) next, and (3) failing either, the nearest width -
|
|
199
|
+
* tie-broken by whichever candidate's own height/width ratio is closest to the target's, so a
|
|
200
|
+
* differently-proportioned panel doesn't just get an arbitrarily stretched/cropped near-width match.
|
|
201
|
+
*/
|
|
202
|
+
function pickBestVariant(variants, target) {
|
|
203
|
+
if (variants.length === 0)
|
|
204
|
+
return undefined;
|
|
205
|
+
const exact = variants.find((v) => v.width === target.width && v.height === target.height && sameColours(v.colours, target.colours));
|
|
206
|
+
if (exact)
|
|
207
|
+
return exact;
|
|
208
|
+
const sizeMatch = variants.find((v) => v.width === target.width && v.height === target.height);
|
|
209
|
+
if (sizeMatch)
|
|
210
|
+
return sizeMatch;
|
|
211
|
+
const targetRatio = target.height / target.width;
|
|
212
|
+
const nearestWidth = Math.min(...variants.map((v) => Math.abs(v.width - target.width)));
|
|
213
|
+
const nearestWidthCandidates = variants.filter((v) => Math.abs(v.width - target.width) === nearestWidth);
|
|
214
|
+
return nearestWidthCandidates.reduce((best, candidate) => Math.abs(candidate.height / candidate.width - targetRatio) < Math.abs(best.height / best.width - targetRatio) ? candidate : best);
|
|
215
|
+
}
|
|
216
|
+
/**
|
|
217
|
+
* Resolves a template name to an actual file path - a local template overrides the bundled one of the
|
|
218
|
+
* same name. `templateName` can also name a template-family *directory* (see `pickBestVariant`), in
|
|
219
|
+
* which case `target` (the device's actual width/height/colours) picks the best file inside it. A
|
|
220
|
+
* `target`-less call, or one where `templateName` just isn't a directory, falls through to the plain
|
|
221
|
+
* flat-file behaviour - the only kind the CLI (which always names an exact file) ever uses.
|
|
222
|
+
*/
|
|
223
|
+
function resolveTemplatePath(templatesDir, templateName, target) {
|
|
224
|
+
if (target) {
|
|
225
|
+
const dir = [(0, path_1.join)(templatesDir, templateName), (0, path_1.join)(exports.BUNDLED_TEMPLATES_DIR, templateName)].find((candidate) => (0, fs_1.existsSync)(candidate) && (0, fs_1.statSync)(candidate).isDirectory());
|
|
226
|
+
if (dir) {
|
|
227
|
+
const best = pickBestVariant(listTemplateVariants(dir), target);
|
|
228
|
+
if (!best) {
|
|
229
|
+
throw new Error(`template directory "${templateName}" has no valid "<width>x<height>-<colours>.svg" files`);
|
|
230
|
+
}
|
|
231
|
+
return (0, path_1.join)(dir, best.fileName);
|
|
232
|
+
}
|
|
233
|
+
}
|
|
108
234
|
const localPath = (0, path_1.join)(templatesDir, templateName);
|
|
109
235
|
return (0, fs_1.existsSync)(localPath) ? localPath : (0, path_1.join)(exports.BUNDLED_TEMPLATES_DIR, templateName);
|
|
110
236
|
}
|
|
@@ -114,7 +240,7 @@ function withEnum(schema, values, names) {
|
|
|
114
240
|
}
|
|
115
241
|
function configSchema(app, discovered = []) {
|
|
116
242
|
const defaults = defaultConfig();
|
|
117
|
-
const current = { ...defaults, ...app
|
|
243
|
+
const current = { ...defaults, ...readCurrentConfig(app) };
|
|
118
244
|
const { values: deviceValues, labels: deviceLabels } = deviceOptions(discovered, current);
|
|
119
245
|
return {
|
|
120
246
|
type: "object",
|
|
@@ -130,7 +256,8 @@ function configSchema(app, discovered = []) {
|
|
|
130
256
|
scanOnStart: {
|
|
131
257
|
type: "boolean",
|
|
132
258
|
title: "Scan for devices on plugin start",
|
|
133
|
-
description: 'Runs a short BLE scan so discovered devices show up in a device\'s "Device" picker below.'
|
|
259
|
+
description: 'Runs a short BLE scan so discovered devices show up in a device\'s "Device" picker below. ' +
|
|
260
|
+
'Not needed if every device uses "All discovered devices" - that scans on demand instead.',
|
|
134
261
|
default: defaults.scanOnStart,
|
|
135
262
|
},
|
|
136
263
|
scanDurationSeconds: {
|
|
@@ -180,7 +307,9 @@ function configSchema(app, discovered = []) {
|
|
|
180
307
|
device: withEnum({
|
|
181
308
|
type: "string",
|
|
182
309
|
title: "Device",
|
|
183
|
-
description: "
|
|
310
|
+
description: "Either a specific device found by a scan (plugin start, or `esl-cli scan`), setting both the model and BLE " +
|
|
311
|
+
'address, or "All discovered devices" to paint this same template/trigger to every device the plugin currently ' +
|
|
312
|
+
"knows about - simplest for a single label, and also covers several identical labels without listing each one.",
|
|
184
313
|
}, deviceValues, deviceLabels),
|
|
185
314
|
templateName: withEnum({ type: "string", title: "Template" }, templateNameOptions(resolveTemplatesDir(current.templatesDir))),
|
|
186
315
|
repaintTrigger: {
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { ServerAPI } from "@signalk/server-api";
|
|
2
|
+
import { DiscoveredDevice } from "./types";
|
|
3
|
+
/**
|
|
4
|
+
* How long a previously-scanned device that isn't found again in a later scan is still kept as a
|
|
5
|
+
* dropdown option, before being dropped - covers a device that's simply asleep/out of BLE range for
|
|
6
|
+
* one scan, without keeping stale entries around forever. Not yet user-configurable.
|
|
7
|
+
*/
|
|
8
|
+
export declare const DISCOVERED_DEVICE_TTL_MS: number;
|
|
9
|
+
export type DiscoveredDeviceRecord = DiscoveredDevice & {
|
|
10
|
+
lastSeenAt: number;
|
|
11
|
+
};
|
|
12
|
+
export type DiscoveredDevicesState = Record<string, DiscoveredDeviceRecord>;
|
|
13
|
+
export declare function loadDiscoveredDevices(app: ServerAPI): DiscoveredDevicesState;
|
|
14
|
+
/**
|
|
15
|
+
* Merges one scan's finds into the previously persisted set, keyed by BLE address (the one stable
|
|
16
|
+
* identifier across scans - see README's note on devices advertising under different names at
|
|
17
|
+
* different times). A device found again gets `lastSeenAt` bumped to `now`; one not found this time
|
|
18
|
+
* is kept as-is as long as it's within `DISCOVERED_DEVICE_TTL_MS` of its own `lastSeenAt`, and dropped
|
|
19
|
+
* once older than that - so the dropdown survives a plugin restart and the occasional missed scan,
|
|
20
|
+
* without accumulating devices that are gone for good.
|
|
21
|
+
*/
|
|
22
|
+
export declare function mergeDiscoveredDevices(previous: DiscoveredDevicesState, foundThisScan: DiscoveredDevice[], now: number): DiscoveredDevicesState;
|
|
23
|
+
/** Loads the persisted set, merges in this scan's finds, saves the result, and returns it. */
|
|
24
|
+
export declare function recordScanResults(app: ServerAPI, foundThisScan: DiscoveredDevice[], now?: number): DiscoveredDevicesState;
|
|
25
|
+
/**
|
|
26
|
+
* Bumps a single device's `lastSeenAt` outside of a scan - called after a successful paint, since
|
|
27
|
+
* connecting to paint it is just as much positive proof it's still there as a discovery scan hit.
|
|
28
|
+
* Without this, a configured device that's actively being repainted (typically with `scanOnStart`
|
|
29
|
+
* off once set up, per the README) would otherwise silently age out of the persisted set after
|
|
30
|
+
* `DISCOVERED_DEVICE_TTL_MS` even though it's demonstrably still in range and working.
|
|
31
|
+
*/
|
|
32
|
+
export declare function touchDiscoveredDevice(app: ServerAPI, device: DiscoveredDevice, now?: number): void;
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.DISCOVERED_DEVICE_TTL_MS = void 0;
|
|
4
|
+
exports.loadDiscoveredDevices = loadDiscoveredDevices;
|
|
5
|
+
exports.mergeDiscoveredDevices = mergeDiscoveredDevices;
|
|
6
|
+
exports.recordScanResults = recordScanResults;
|
|
7
|
+
exports.touchDiscoveredDevice = touchDiscoveredDevice;
|
|
8
|
+
const fs_1 = require("fs");
|
|
9
|
+
const path_1 = require("path");
|
|
10
|
+
/**
|
|
11
|
+
* How long a previously-scanned device that isn't found again in a later scan is still kept as a
|
|
12
|
+
* dropdown option, before being dropped - covers a device that's simply asleep/out of BLE range for
|
|
13
|
+
* one scan, without keeping stale entries around forever. Not yet user-configurable.
|
|
14
|
+
*/
|
|
15
|
+
exports.DISCOVERED_DEVICE_TTL_MS = 24 * 60 * 60 * 1000;
|
|
16
|
+
function storePath(app) {
|
|
17
|
+
return (0, path_1.join)(app.getDataDirPath(), "discovered-devices.json");
|
|
18
|
+
}
|
|
19
|
+
function loadDiscoveredDevices(app) {
|
|
20
|
+
try {
|
|
21
|
+
return JSON.parse((0, fs_1.readFileSync)(storePath(app), "utf-8"));
|
|
22
|
+
}
|
|
23
|
+
catch {
|
|
24
|
+
return {};
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
function saveDiscoveredDevices(app, state) {
|
|
28
|
+
(0, fs_1.writeFileSync)(storePath(app), JSON.stringify(state));
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Merges one scan's finds into the previously persisted set, keyed by BLE address (the one stable
|
|
32
|
+
* identifier across scans - see README's note on devices advertising under different names at
|
|
33
|
+
* different times). A device found again gets `lastSeenAt` bumped to `now`; one not found this time
|
|
34
|
+
* is kept as-is as long as it's within `DISCOVERED_DEVICE_TTL_MS` of its own `lastSeenAt`, and dropped
|
|
35
|
+
* once older than that - so the dropdown survives a plugin restart and the occasional missed scan,
|
|
36
|
+
* without accumulating devices that are gone for good.
|
|
37
|
+
*/
|
|
38
|
+
function mergeDiscoveredDevices(previous, foundThisScan, now) {
|
|
39
|
+
const next = {};
|
|
40
|
+
for (const found of foundThisScan) {
|
|
41
|
+
next[found.address] = { ...found, lastSeenAt: now };
|
|
42
|
+
}
|
|
43
|
+
for (const [address, record] of Object.entries(previous)) {
|
|
44
|
+
if (!(address in next) && now - record.lastSeenAt < exports.DISCOVERED_DEVICE_TTL_MS) {
|
|
45
|
+
next[address] = record;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
return next;
|
|
49
|
+
}
|
|
50
|
+
/** Loads the persisted set, merges in this scan's finds, saves the result, and returns it. */
|
|
51
|
+
function recordScanResults(app, foundThisScan, now = Date.now()) {
|
|
52
|
+
const merged = mergeDiscoveredDevices(loadDiscoveredDevices(app), foundThisScan, now);
|
|
53
|
+
saveDiscoveredDevices(app, merged);
|
|
54
|
+
return merged;
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Bumps a single device's `lastSeenAt` outside of a scan - called after a successful paint, since
|
|
58
|
+
* connecting to paint it is just as much positive proof it's still there as a discovery scan hit.
|
|
59
|
+
* Without this, a configured device that's actively being repainted (typically with `scanOnStart`
|
|
60
|
+
* off once set up, per the README) would otherwise silently age out of the persisted set after
|
|
61
|
+
* `DISCOVERED_DEVICE_TTL_MS` even though it's demonstrably still in range and working.
|
|
62
|
+
*/
|
|
63
|
+
function touchDiscoveredDevice(app, device, now = Date.now()) {
|
|
64
|
+
const state = loadDiscoveredDevices(app);
|
|
65
|
+
state[device.address] = { ...device, lastSeenAt: now };
|
|
66
|
+
saveDiscoveredDevices(app, state);
|
|
67
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { ServerAPI } from "@signalk/server-api";
|
|
2
|
+
import { DiscoveredDevicesState } from "./discoveredDevicesStore";
|
|
3
|
+
import { DiscoveredDevice } from "./types";
|
|
4
|
+
export interface ScanResult {
|
|
5
|
+
/** Devices this particular scan actually found - distinct from `merged`, which also carries over anything still-fresh from before. */
|
|
6
|
+
foundThisScan: DiscoveredDevice[];
|
|
7
|
+
/** The full persisted set after merging `foundThisScan` in - see `discoveredDevicesStore.ts`. */
|
|
8
|
+
merged: DiscoveredDevicesState;
|
|
9
|
+
}
|
|
10
|
+
/** epoch ms a scan was started at, if one is currently running - `undefined` otherwise. */
|
|
11
|
+
export declare function scanInProgressSince(): number | undefined;
|
|
12
|
+
/**
|
|
13
|
+
* Runs one BLE discovery scan and persists what it finds. node-ble/BlueZ has no scan-cancellation
|
|
14
|
+
* API and can't run two discovery sessions at once, so a caller that arrives while a scan is
|
|
15
|
+
* already running (e.g. the startup scan and an on-demand scan for a `device: "ALL"` repaint both
|
|
16
|
+
* wanting to scan at the same moment) is hooked into that *same* in-flight scan's eventual result
|
|
17
|
+
* instead of starting a second BlueZ session, which would make both fail.
|
|
18
|
+
*/
|
|
19
|
+
export declare function ensureScan(app: ServerAPI, durationSeconds: number): Promise<ScanResult>;
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.scanInProgressSince = scanInProgressSince;
|
|
4
|
+
exports.ensureScan = ensureScan;
|
|
5
|
+
const bleDiscovery_1 = require("./bleDiscovery");
|
|
6
|
+
const discoveredDevicesStore_1 = require("./discoveredDevicesStore");
|
|
7
|
+
const registry_1 = require("./registry");
|
|
8
|
+
let inFlight;
|
|
9
|
+
/** epoch ms a scan was started at, if one is currently running - `undefined` otherwise. */
|
|
10
|
+
function scanInProgressSince() {
|
|
11
|
+
return inFlight?.startedAt;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Runs one BLE discovery scan and persists what it finds. node-ble/BlueZ has no scan-cancellation
|
|
15
|
+
* API and can't run two discovery sessions at once, so a caller that arrives while a scan is
|
|
16
|
+
* already running (e.g. the startup scan and an on-demand scan for a `device: "ALL"` repaint both
|
|
17
|
+
* wanting to scan at the same moment) is hooked into that *same* in-flight scan's eventual result
|
|
18
|
+
* instead of starting a second BlueZ session, which would make both fail.
|
|
19
|
+
*/
|
|
20
|
+
function ensureScan(app, durationSeconds) {
|
|
21
|
+
if (inFlight) {
|
|
22
|
+
return inFlight.promise;
|
|
23
|
+
}
|
|
24
|
+
const promise = runScan(app, durationSeconds).finally(() => {
|
|
25
|
+
inFlight = undefined;
|
|
26
|
+
});
|
|
27
|
+
inFlight = { startedAt: Date.now(), promise };
|
|
28
|
+
return promise;
|
|
29
|
+
}
|
|
30
|
+
async function runScan(app, durationSeconds) {
|
|
31
|
+
const foundThisScan = [];
|
|
32
|
+
const drivers = (0, registry_1.allDrivers)();
|
|
33
|
+
try {
|
|
34
|
+
await (0, bleDiscovery_1.withDiscovery)(durationSeconds * 1000, async (adapter) => {
|
|
35
|
+
await (0, bleDiscovery_1.forEachAdvertisedDevice)(adapter, async ({ device, address, name, manufacturerId, manufacturerData }) => {
|
|
36
|
+
const driver = drivers.find((candidate) => candidate.matchesAdvertisement(name, manufacturerId));
|
|
37
|
+
if (!driver) {
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
const found = await driver.identifyDevice(device, address, name, manufacturerId, manufacturerData).catch((err) => {
|
|
41
|
+
app.debug(`${driver.vendor} scan failed: ${err.message}\n${err.stack ?? ""}`);
|
|
42
|
+
return undefined;
|
|
43
|
+
});
|
|
44
|
+
if (!found) {
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
foundThisScan.push(found);
|
|
48
|
+
const pid = found.pid !== undefined ? `0x${found.pid.toString(16).padStart(4, "0")}` : "unknown";
|
|
49
|
+
const hwid = found.hwVersion ?? "unknown";
|
|
50
|
+
app.debug(`discovered ${driver.vendor} device "${found.name ?? ""}" [${found.address}] pid=${pid} hwid=${hwid}`);
|
|
51
|
+
});
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
catch (err) {
|
|
55
|
+
app.debug(`scan failed: ${err.message}\n${err.stack ?? ""}`);
|
|
56
|
+
}
|
|
57
|
+
const merged = (0, discoveredDevicesStore_1.recordScanResults)(app, foundThisScan);
|
|
58
|
+
return { foundThisScan, merged };
|
|
59
|
+
}
|