@rhizomatics/signalk-einklabel-plugin 1.0.0 → 1.2.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 +11 -0
- package/README.md +55 -5
- package/dist/cli/index.d.ts +2 -0
- package/dist/cli/index.js +46 -11
- package/dist/devices/gicisky/compression.d.ts +16 -0
- package/dist/devices/gicisky/compression.js +34 -0
- package/dist/devices/gicisky/encode.d.ts +10 -0
- package/dist/devices/gicisky/encode.js +130 -0
- package/dist/devices/gicisky/index.d.ts +11 -0
- package/dist/devices/gicisky/index.js +164 -0
- package/dist/devices/gicisky/layout.d.ts +35 -0
- package/dist/devices/gicisky/layout.js +35 -0
- package/dist/devices/gicisky/metadata.d.ts +16 -0
- package/dist/devices/gicisky/metadata.js +127 -0
- package/dist/devices/gicisky/protocol.d.ts +53 -0
- package/dist/devices/gicisky/protocol.js +88 -0
- package/dist/devices/types.d.ts +3 -0
- package/dist/devices/zhsunyco/index.js +3 -1
- package/dist/plugin.js +2 -0
- package/dist/render/binding.d.ts +9 -0
- package/dist/render/binding.js +21 -0
- package/dist/render/reframe.d.ts +12 -0
- package/dist/render/reframe.js +36 -0
- package/package.json +12 -10
- package/dist/render/llmPrompt.d.ts +0 -86
- package/dist/render/llmPrompt.js +0 -199
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.GiciskyDriver = void 0;
|
|
4
|
+
const bleDiscovery_1 = require("../bleDiscovery");
|
|
5
|
+
const metadata_1 = require("./metadata");
|
|
6
|
+
const layout_1 = require("./layout");
|
|
7
|
+
const encode_1 = require("./encode");
|
|
8
|
+
const reframe_1 = require("../../render/reframe");
|
|
9
|
+
const protocol_1 = require("./protocol");
|
|
10
|
+
const DEVICE_DISCOVERY_TIMEOUT_MS = 30000;
|
|
11
|
+
const DEFAULT_PAINT_CONNECT_TIMEOUT_MS = 60000;
|
|
12
|
+
const ACK_TIMEOUT_MS = 15000;
|
|
13
|
+
/** Fallback if a device's `requestBlockSize` ack doesn't decode - matches the block size both reference drivers assume. */
|
|
14
|
+
const DEFAULT_BLOCK_SIZE = 244;
|
|
15
|
+
/** Bytes of image data per chunk: block size minus the 4-byte part-index header each chunk is prefixed with. */
|
|
16
|
+
const PART_INDEX_HEADER_LENGTH = 4;
|
|
17
|
+
const MAX_STALLED_REPEATS = 3;
|
|
18
|
+
class GiciskyDriver {
|
|
19
|
+
constructor() {
|
|
20
|
+
this.vendor = "gicisky";
|
|
21
|
+
}
|
|
22
|
+
matchesAdvertisement(_name, manufacturerId) {
|
|
23
|
+
return manufacturerId === protocol_1.GICISKY_MANUFACTURER_ID;
|
|
24
|
+
}
|
|
25
|
+
metadataForPid(pid) {
|
|
26
|
+
return metadata_1.GICISKY_PID_METADATA.find((model) => model.pid === pid);
|
|
27
|
+
}
|
|
28
|
+
supportedDevices() {
|
|
29
|
+
return metadata_1.GICISKY_PID_METADATA;
|
|
30
|
+
}
|
|
31
|
+
async identifyDevice(device, address, name, manufacturerId, manufacturerData) {
|
|
32
|
+
const info = manufacturerData ? (0, protocol_1.decodeAdvertisedInfo)(manufacturerData) : undefined;
|
|
33
|
+
return {
|
|
34
|
+
address,
|
|
35
|
+
name,
|
|
36
|
+
vendor: this.vendor,
|
|
37
|
+
pid: info?.deviceId,
|
|
38
|
+
metadata: info ? this.metadataForPid(info.deviceId) : undefined,
|
|
39
|
+
manufacturerId,
|
|
40
|
+
batteryMv: info?.batteryMv,
|
|
41
|
+
rssi: await device
|
|
42
|
+
.getRSSI()
|
|
43
|
+
.then((value) => (value === undefined ? undefined : Number(value)))
|
|
44
|
+
.catch(() => undefined),
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
async paint(bitmap, config) {
|
|
48
|
+
const { bluetooth, destroy } = (0, bleDiscovery_1.createBluetooth)();
|
|
49
|
+
try {
|
|
50
|
+
const adapter = await bluetooth.defaultAdapter();
|
|
51
|
+
const device = await (0, bleDiscovery_1.getOrDiscoverDevice)(adapter, config.address, DEVICE_DISCOVERY_TIMEOUT_MS);
|
|
52
|
+
/**
|
|
53
|
+
* Unlike zhsunyco, there's no GATT characteristic that reports the device's PID on demand -
|
|
54
|
+
* the only source for it is the advertisement, cached on the `Device` object by BlueZ from
|
|
55
|
+
* the last time it was seen (the same cache `identifyDevice`/a scan reads, just without
|
|
56
|
+
* connecting first - see `forEachAdvertisedDevice` in `bleDiscovery.ts`).
|
|
57
|
+
*/
|
|
58
|
+
const manufacturerData = await device
|
|
59
|
+
.getManufacturerData()
|
|
60
|
+
.then((data) => data[protocol_1.GICISKY_MANUFACTURER_ID.toString()])
|
|
61
|
+
.catch(() => undefined);
|
|
62
|
+
const info = manufacturerData ? (0, protocol_1.decodeAdvertisedInfo)(manufacturerData) : undefined;
|
|
63
|
+
const metadata = config.modelOverride
|
|
64
|
+
? { pid: info?.deviceId ?? 0, ...config.modelOverride }
|
|
65
|
+
: info
|
|
66
|
+
? this.metadataForPid(info.deviceId)
|
|
67
|
+
: undefined;
|
|
68
|
+
if (!metadata) {
|
|
69
|
+
throw new Error(info === undefined
|
|
70
|
+
? "gicisky device has no cached advertisement to identify it from - scan for it first, or pass --width/--height/--voffset/--colours to describe it manually"
|
|
71
|
+
: `gicisky device reports unrecognised deviceId 0x${info.deviceId.toString(16).padStart(4, "0")} - ` +
|
|
72
|
+
"pass --width/--height/--voffset/--colours to describe it manually");
|
|
73
|
+
}
|
|
74
|
+
const layout = (info && layout_1.GICISKY_PID_LAYOUT[info.deviceId]) || (0, layout_1.defaultLayoutFor)(metadata.colours);
|
|
75
|
+
const framed = (0, reframe_1.reframeBitmap)(bitmap, metadata.width, metadata.height, config.reframe ?? "fixed");
|
|
76
|
+
const payload = (0, encode_1.encodeBitmap)(framed, metadata, layout);
|
|
77
|
+
await (0, bleDiscovery_1.connectWithTimeout)(device, config.connectTimeoutMs ?? DEFAULT_PAINT_CONNECT_TIMEOUT_MS);
|
|
78
|
+
try {
|
|
79
|
+
const gatt = await device.gatt();
|
|
80
|
+
const { cmd, img } = await findCommandAndImageCharacteristics(gatt);
|
|
81
|
+
await cmd.startNotifications();
|
|
82
|
+
try {
|
|
83
|
+
const startAck = await writeAndAwaitAck(cmd, cmd, Buffer.from([0x01]));
|
|
84
|
+
const chunkSize = ((0, protocol_1.decodeBlockSize)(startAck) ?? DEFAULT_BLOCK_SIZE) - PART_INDEX_HEADER_LENGTH;
|
|
85
|
+
await writeAndAwaitAck(cmd, cmd, (0, protocol_1.writeScreenCommand)(payload.length, layout.packing === "chunked"));
|
|
86
|
+
const startImageAck = await writeAndAwaitAck(cmd, cmd, Buffer.from([0x03]));
|
|
87
|
+
const started = (0, protocol_1.decodeTransferAck)(startImageAck);
|
|
88
|
+
if (!started?.ok) {
|
|
89
|
+
throw new Error(`gicisky device rejected start-image-transfer request: ${startImageAck.toString("hex")}`);
|
|
90
|
+
}
|
|
91
|
+
let part = started.nextPart;
|
|
92
|
+
let lastPart = -1;
|
|
93
|
+
let repeats = 0;
|
|
94
|
+
while (part * chunkSize < payload.length) {
|
|
95
|
+
const chunk = payload.subarray(part * chunkSize, Math.min(part * chunkSize + chunkSize, payload.length));
|
|
96
|
+
const ack = await writeAndAwaitAck(img, cmd, (0, protocol_1.imageChunkPacket)(part, chunk));
|
|
97
|
+
const decoded = (0, protocol_1.decodeTransferAck)(ack);
|
|
98
|
+
if (!decoded?.ok) {
|
|
99
|
+
throw new Error(`gicisky device reported an error transferring image part ${part}: ${ack.toString("hex")}`);
|
|
100
|
+
}
|
|
101
|
+
if (decoded.nextPart === lastPart) {
|
|
102
|
+
repeats++;
|
|
103
|
+
if (repeats >= MAX_STALLED_REPEATS) {
|
|
104
|
+
throw new Error(`gicisky image transfer stalled - device kept re-requesting part ${decoded.nextPart}`);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
else {
|
|
108
|
+
repeats = 0;
|
|
109
|
+
lastPart = decoded.nextPart;
|
|
110
|
+
}
|
|
111
|
+
part = decoded.nextPart;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
finally {
|
|
115
|
+
await cmd.stopNotifications().catch(() => { });
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
finally {
|
|
119
|
+
await device.disconnect();
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
finally {
|
|
123
|
+
destroy();
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
exports.GiciskyDriver = GiciskyDriver;
|
|
128
|
+
/**
|
|
129
|
+
* Walks every service whose UUID starts `0000f`, collecting their characteristics and sorting by
|
|
130
|
+
* 16-bit UUID value - mirrors both reference drivers, which locate their command/image
|
|
131
|
+
* characteristics this way rather than by a hardcoded service UUID (see `protocol.ts`).
|
|
132
|
+
*/
|
|
133
|
+
async function findCommandAndImageCharacteristics(gatt) {
|
|
134
|
+
const candidates = [];
|
|
135
|
+
for (const serviceUuid of await gatt.services()) {
|
|
136
|
+
if (!serviceUuid.toLowerCase().startsWith(protocol_1.CANDIDATE_SERVICE_UUID_PREFIX)) {
|
|
137
|
+
continue;
|
|
138
|
+
}
|
|
139
|
+
const service = await gatt.getPrimaryService(serviceUuid);
|
|
140
|
+
for (const charUuid of await service.characteristics()) {
|
|
141
|
+
candidates.push({ uuid: charUuid, char: await service.getCharacteristic(charUuid) });
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
candidates.sort((a, b) => parseInt(a.uuid.slice(4, 8), 16) - parseInt(b.uuid.slice(4, 8), 16));
|
|
145
|
+
if (candidates.length < 2) {
|
|
146
|
+
throw new Error(`gicisky device exposes ${candidates.length} candidate characteristic(s) under a "0000f..." service, expected at least 2`);
|
|
147
|
+
}
|
|
148
|
+
return { cmd: candidates[0].char, img: candidates[1].char };
|
|
149
|
+
}
|
|
150
|
+
/** Writes `data` to `writeChar`, then awaits the next notification on `notifyChar` (which may be the same characteristic). */
|
|
151
|
+
function writeAndAwaitAck(writeChar, notifyChar, data) {
|
|
152
|
+
const ack = new Promise((resolve, reject) => {
|
|
153
|
+
const timer = setTimeout(() => {
|
|
154
|
+
notifyChar.removeListener("valuechanged", onValue);
|
|
155
|
+
reject(new Error("gicisky device did not acknowledge in time"));
|
|
156
|
+
}, ACK_TIMEOUT_MS);
|
|
157
|
+
function onValue(value) {
|
|
158
|
+
clearTimeout(timer);
|
|
159
|
+
resolve(value);
|
|
160
|
+
}
|
|
161
|
+
notifyChar.once("valuechanged", onValue);
|
|
162
|
+
});
|
|
163
|
+
return writeChar.writeValueWithoutResponse(data).then(() => ack);
|
|
164
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Per-model wire-layout quirks, keyed the same way as `GICISKY_PID_METADATA` (by `deviceId`).
|
|
3
|
+
* These aren't part of the shared `DeviceMetadata` shape since no other vendor needs them -
|
|
4
|
+
* `DeviceMetadata.width`/`.height` is the pre-rotation "visual" canvas (what a template is
|
|
5
|
+
* designed at); this table says how `encode.ts` turns that into the physical byte layout each
|
|
6
|
+
* model actually expects. Transcribed from `DeviceEntry`/`GiciskyClient._make_image_packet` in
|
|
7
|
+
* hass-gicisky's `gicisky_ble/devices.py` and `gicisky_ble/writer.py`.
|
|
8
|
+
*/
|
|
9
|
+
export type GiciskyPacking =
|
|
10
|
+
/** Plain concatenated bit-planes, no framing - covers most panels. */
|
|
11
|
+
"plain"
|
|
12
|
+
/** Two bit-planes each split into raw 64-byte chunks framed per `compression.ts`'s `compress()` - the 7.5"/10.2" panels. */
|
|
13
|
+
| "chunked"
|
|
14
|
+
/**
|
|
15
|
+
* A panel this driver can identify (for `scan`/discovery) but can't paint correctly yet - either
|
|
16
|
+
* its vendor-firmware "compressed" line framing doesn't reconcile with the row-major bit-packing
|
|
17
|
+
* `_make_image_packet` otherwise produces (the 3.7" panel, and the 7.5" panel on pre-`0x8101`
|
|
18
|
+
* firmware, both of which use this legacy `compression=True` mode rather than `compression2`),
|
|
19
|
+
* or it needs a resize/resample step this driver doesn't implement (the TFT panel).
|
|
20
|
+
*/
|
|
21
|
+
| "unsupported";
|
|
22
|
+
export interface GiciskyLayout {
|
|
23
|
+
/** Degrees the composed image is rotated (always a multiple of 90) before bit-packing. */
|
|
24
|
+
rotation: 0 | 90 | 180 | 270;
|
|
25
|
+
mirrorX: boolean;
|
|
26
|
+
mirrorY: boolean;
|
|
27
|
+
/** Swaps which bit value means "white" in the BW plane - some panels' controllers wire the panel's polarity the other way round. */
|
|
28
|
+
invertLuminance: boolean;
|
|
29
|
+
/** 2 bits/pixel across the device's full 4-colour palette, rather than separate 1-bit BW/red planes. */
|
|
30
|
+
fourColour: boolean;
|
|
31
|
+
packing: GiciskyPacking;
|
|
32
|
+
}
|
|
33
|
+
export declare const GICISKY_PID_LAYOUT: Record<number, GiciskyLayout>;
|
|
34
|
+
/** Best-effort layout for a `modelOverride`d PID this table has no entry for - assumes the common case. */
|
|
35
|
+
export declare function defaultLayoutFor(colours: string[]): GiciskyLayout;
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.GICISKY_PID_LAYOUT = void 0;
|
|
4
|
+
exports.defaultLayoutFor = defaultLayoutFor;
|
|
5
|
+
const DEFAULT_LAYOUT = {
|
|
6
|
+
rotation: 0,
|
|
7
|
+
mirrorX: false,
|
|
8
|
+
mirrorY: false,
|
|
9
|
+
invertLuminance: false,
|
|
10
|
+
fourColour: false,
|
|
11
|
+
packing: "plain",
|
|
12
|
+
};
|
|
13
|
+
exports.GICISKY_PID_LAYOUT = {
|
|
14
|
+
0x00a0: { ...DEFAULT_LAYOUT, packing: "unsupported" },
|
|
15
|
+
0x000b: { ...DEFAULT_LAYOUT, rotation: 270, mirrorX: true },
|
|
16
|
+
0x010b: { ...DEFAULT_LAYOUT, rotation: 270, mirrorX: true },
|
|
17
|
+
0x0028: { ...DEFAULT_LAYOUT, rotation: 90 },
|
|
18
|
+
0x0033: { ...DEFAULT_LAYOUT, rotation: 90 },
|
|
19
|
+
0x002e: { ...DEFAULT_LAYOUT, rotation: 90, fourColour: true },
|
|
20
|
+
0x022b: { ...DEFAULT_LAYOUT, rotation: 180, mirrorX: true, packing: "unsupported" },
|
|
21
|
+
0x004b: { ...DEFAULT_LAYOUT },
|
|
22
|
+
0x004e: { ...DEFAULT_LAYOUT, fourColour: true },
|
|
23
|
+
/**
|
|
24
|
+
* Only the newer (post-`0x8101`) firmware's `compression2` framing is implemented (see
|
|
25
|
+
* `packing: "chunked"` above) - a 7.5" panel still on `0x8101` needs the legacy `compression`
|
|
26
|
+
* framing this driver doesn't support yet, and `paint()` has no way to tell the two apart
|
|
27
|
+
* without re-reading the advertised firmware version at paint time.
|
|
28
|
+
*/
|
|
29
|
+
0x012b: { ...DEFAULT_LAYOUT, mirrorY: true, invertLuminance: true, packing: "chunked" },
|
|
30
|
+
0x008b: { ...DEFAULT_LAYOUT, packing: "chunked" },
|
|
31
|
+
};
|
|
32
|
+
/** Best-effort layout for a `modelOverride`d PID this table has no entry for - assumes the common case. */
|
|
33
|
+
function defaultLayoutFor(colours) {
|
|
34
|
+
return { ...DEFAULT_LAYOUT, fourColour: colours.includes("yellow") };
|
|
35
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { DeviceMetadata } from "../types";
|
|
2
|
+
/**
|
|
3
|
+
* Keyed by the 14-bit `deviceId` decoded from the advertisement (see `decodeAdvertisedInfo` in
|
|
4
|
+
* `protocol.ts`) - Gicisky's own namespace, not shared with other vendors' PID spaces.
|
|
5
|
+
*
|
|
6
|
+
* Sourced from `DEVICE_TYPES` in hass-gicisky's `gicisky_ble/devices.py`, the more actively
|
|
7
|
+
* maintained of the two reference implementations in the sibling `lab` checkouts. That file also
|
|
8
|
+
* carries a much longer comment table of `deviceId`s seen in the wild that aren't yet mapped to
|
|
9
|
+
* physical panel facts (`width`/`height`/colours) - those aren't reproduced here since there'd be
|
|
10
|
+
* nothing to paint correctly for them yet.
|
|
11
|
+
*
|
|
12
|
+
* `width`/`height` are the pre-rotation "visual" canvas size - what a template should be designed
|
|
13
|
+
* at - matching `DeviceEntry.width`/`.height` there; `paintLayout` (`layout.ts`) then rotates/mirrors
|
|
14
|
+
* during encoding to produce the physical wire layout.
|
|
15
|
+
*/
|
|
16
|
+
export declare const GICISKY_PID_METADATA: DeviceMetadata[];
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.GICISKY_PID_METADATA = void 0;
|
|
4
|
+
/**
|
|
5
|
+
* Keyed by the 14-bit `deviceId` decoded from the advertisement (see `decodeAdvertisedInfo` in
|
|
6
|
+
* `protocol.ts`) - Gicisky's own namespace, not shared with other vendors' PID spaces.
|
|
7
|
+
*
|
|
8
|
+
* Sourced from `DEVICE_TYPES` in hass-gicisky's `gicisky_ble/devices.py`, the more actively
|
|
9
|
+
* maintained of the two reference implementations in the sibling `lab` checkouts. That file also
|
|
10
|
+
* carries a much longer comment table of `deviceId`s seen in the wild that aren't yet mapped to
|
|
11
|
+
* physical panel facts (`width`/`height`/colours) - those aren't reproduced here since there'd be
|
|
12
|
+
* nothing to paint correctly for them yet.
|
|
13
|
+
*
|
|
14
|
+
* `width`/`height` are the pre-rotation "visual" canvas size - what a template should be designed
|
|
15
|
+
* at - matching `DeviceEntry.width`/`.height` there; `paintLayout` (`layout.ts`) then rotates/mirrors
|
|
16
|
+
* during encoding to produce the physical wire layout.
|
|
17
|
+
*/
|
|
18
|
+
exports.GICISKY_PID_METADATA = [
|
|
19
|
+
{
|
|
20
|
+
pid: 0x00a0,
|
|
21
|
+
manufacturer: "Gicisky",
|
|
22
|
+
label: '2.1" TFT BW',
|
|
23
|
+
width: 250,
|
|
24
|
+
height: 132,
|
|
25
|
+
voffset: 0,
|
|
26
|
+
colours: bw(),
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
pid: 0x000b,
|
|
30
|
+
manufacturer: "Gicisky",
|
|
31
|
+
label: '2.1" BWR',
|
|
32
|
+
width: 212,
|
|
33
|
+
height: 104,
|
|
34
|
+
voffset: 0,
|
|
35
|
+
colours: bwr(),
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
pid: 0x010b,
|
|
39
|
+
manufacturer: "Gicisky",
|
|
40
|
+
label: '2.1" BWR',
|
|
41
|
+
width: 250,
|
|
42
|
+
height: 128,
|
|
43
|
+
voffset: 0,
|
|
44
|
+
colours: bwr(),
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
pid: 0x0028,
|
|
48
|
+
manufacturer: "Gicisky",
|
|
49
|
+
label: '2.9" BW',
|
|
50
|
+
width: 296,
|
|
51
|
+
height: 128,
|
|
52
|
+
voffset: 0,
|
|
53
|
+
colours: bw(),
|
|
54
|
+
},
|
|
55
|
+
{
|
|
56
|
+
pid: 0x0033,
|
|
57
|
+
manufacturer: "Gicisky",
|
|
58
|
+
label: '2.9" BWR',
|
|
59
|
+
width: 296,
|
|
60
|
+
height: 128,
|
|
61
|
+
voffset: 0,
|
|
62
|
+
colours: bwr(),
|
|
63
|
+
},
|
|
64
|
+
{
|
|
65
|
+
pid: 0x002e,
|
|
66
|
+
manufacturer: "Gicisky",
|
|
67
|
+
label: '2.9" BWRY',
|
|
68
|
+
width: 296,
|
|
69
|
+
height: 128,
|
|
70
|
+
voffset: 0,
|
|
71
|
+
colours: bwry(),
|
|
72
|
+
},
|
|
73
|
+
{
|
|
74
|
+
pid: 0x022b,
|
|
75
|
+
manufacturer: "Gicisky",
|
|
76
|
+
label: '3.7" BWR',
|
|
77
|
+
width: 240,
|
|
78
|
+
height: 416,
|
|
79
|
+
voffset: 0,
|
|
80
|
+
colours: bwr(),
|
|
81
|
+
},
|
|
82
|
+
{
|
|
83
|
+
pid: 0x004b,
|
|
84
|
+
manufacturer: "Gicisky",
|
|
85
|
+
label: '4.2" BWR',
|
|
86
|
+
width: 400,
|
|
87
|
+
height: 300,
|
|
88
|
+
voffset: 0,
|
|
89
|
+
colours: bwr(),
|
|
90
|
+
},
|
|
91
|
+
{
|
|
92
|
+
pid: 0x004e,
|
|
93
|
+
manufacturer: "Gicisky",
|
|
94
|
+
label: '4.2" BWRY',
|
|
95
|
+
width: 400,
|
|
96
|
+
height: 300,
|
|
97
|
+
voffset: 0,
|
|
98
|
+
colours: bwry(),
|
|
99
|
+
},
|
|
100
|
+
{
|
|
101
|
+
pid: 0x012b,
|
|
102
|
+
manufacturer: "Gicisky",
|
|
103
|
+
label: '7.5" BWR',
|
|
104
|
+
width: 800,
|
|
105
|
+
height: 480,
|
|
106
|
+
voffset: 0,
|
|
107
|
+
colours: bwr(),
|
|
108
|
+
},
|
|
109
|
+
{
|
|
110
|
+
pid: 0x008b,
|
|
111
|
+
manufacturer: "Gicisky",
|
|
112
|
+
label: '10.2" BWR',
|
|
113
|
+
width: 960,
|
|
114
|
+
height: 640,
|
|
115
|
+
voffset: 0,
|
|
116
|
+
colours: bwr(),
|
|
117
|
+
},
|
|
118
|
+
];
|
|
119
|
+
function bw() {
|
|
120
|
+
return ["black", "white"];
|
|
121
|
+
}
|
|
122
|
+
function bwr() {
|
|
123
|
+
return ["black", "white", "red"];
|
|
124
|
+
}
|
|
125
|
+
function bwry() {
|
|
126
|
+
return ["black", "white", "red", "yellow"];
|
|
127
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Gicisky ESL BLE protocol, transcribed from two reference implementations in the sibling
|
|
3
|
+
* `lab` checkouts:
|
|
4
|
+
* - gicisky-tag (github.com/xxxxx/gicisky-tag) - a minimal CLI writer/scanner.
|
|
5
|
+
* - hass-gicisky (github.com/eigger/hass-gicisky) - the more actively maintained Home
|
|
6
|
+
* Assistant integration, whose `gicisky_ble` package this mostly follows.
|
|
7
|
+
*/
|
|
8
|
+
/** BLE manufacturer ID Gicisky devices advertise under (20563 decimal). */
|
|
9
|
+
export declare const GICISKY_MANUFACTURER_ID = 20563;
|
|
10
|
+
/**
|
|
11
|
+
* The two GATT characteristics used for painting, found dynamically rather than hardcoded:
|
|
12
|
+
* both reference drivers walk every service whose UUID starts `0000f...`, collect their
|
|
13
|
+
* characteristics, and sort by the 16-bit UUID value - the lowest (observed as `fef1`) is the
|
|
14
|
+
* command/status characteristic, the next (observed as `fef2`) carries image data. Doing the
|
|
15
|
+
* same walk here (see `findCommandAndImageCharacteristics` in `index.ts`) survives a firmware
|
|
16
|
+
* that puts these under a different parent service UUID than expected.
|
|
17
|
+
*/
|
|
18
|
+
export declare const CANDIDATE_SERVICE_UUID_PREFIX = "0000f";
|
|
19
|
+
export interface AdvertisedInfo {
|
|
20
|
+
/** `((data[4] << 8) | data[0]) & 0x3FFF` - matches this driver's `GICISKY_PID_METADATA` keys. */
|
|
21
|
+
deviceId: number;
|
|
22
|
+
/** Un-masked `(data[4] << 8) | data[0])`, kept only for logging - not currently used to disambiguate models. */
|
|
23
|
+
hardware: number;
|
|
24
|
+
firmware: number;
|
|
25
|
+
batteryMv: number;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Decodes the 5-byte manufacturer-data payload Gicisky devices advertise. Mirrors
|
|
29
|
+
* `GiciskyBluetoothDeviceData._parse_gicisky` (hass-gicisky's `parser.py`).
|
|
30
|
+
*/
|
|
31
|
+
export declare function decodeAdvertisedInfo(data: Buffer): AdvertisedInfo | undefined;
|
|
32
|
+
export declare const COMMAND: {
|
|
33
|
+
readonly requestBlockSize: 1;
|
|
34
|
+
readonly writeScreen: 2;
|
|
35
|
+
readonly startImageTransfer: 3;
|
|
36
|
+
};
|
|
37
|
+
/** `write_start_with_response`'s ack: `[0x01, <blockSize LE16>]` - observed as 244 on real hardware. */
|
|
38
|
+
export declare function decodeBlockSize(ack: Buffer): number | undefined;
|
|
39
|
+
/**
|
|
40
|
+
* Builds the `writeScreen` command announcing the total encoded payload size.
|
|
41
|
+
*
|
|
42
|
+
* `compression2` devices (7.5"/10.2") expect a 6-byte packet with a trailing `0x01` flag byte;
|
|
43
|
+
* every other device expects an 8-byte packet with 3 trailing zero bytes. Mirrors
|
|
44
|
+
* `GiciskyClient._make_cmd_packet` (hass-gicisky's `writer.py`).
|
|
45
|
+
*/
|
|
46
|
+
export declare function writeScreenCommand(payloadLength: number, compression2: boolean): Buffer;
|
|
47
|
+
/** Builds one image-data packet: a 4-byte LE part index followed by that part's chunk. */
|
|
48
|
+
export declare function imageChunkPacket(part: number, chunk: Buffer): Buffer;
|
|
49
|
+
/** Ack shared by `startImageTransfer` and every image-data chunk: `[0x05, 0x00, <nextPart LE32>]`. */
|
|
50
|
+
export declare function decodeTransferAck(ack: Buffer): {
|
|
51
|
+
ok: boolean;
|
|
52
|
+
nextPart: number;
|
|
53
|
+
} | undefined;
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Gicisky ESL BLE protocol, transcribed from two reference implementations in the sibling
|
|
4
|
+
* `lab` checkouts:
|
|
5
|
+
* - gicisky-tag (github.com/xxxxx/gicisky-tag) - a minimal CLI writer/scanner.
|
|
6
|
+
* - hass-gicisky (github.com/eigger/hass-gicisky) - the more actively maintained Home
|
|
7
|
+
* Assistant integration, whose `gicisky_ble` package this mostly follows.
|
|
8
|
+
*/
|
|
9
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
10
|
+
exports.COMMAND = exports.CANDIDATE_SERVICE_UUID_PREFIX = exports.GICISKY_MANUFACTURER_ID = void 0;
|
|
11
|
+
exports.decodeAdvertisedInfo = decodeAdvertisedInfo;
|
|
12
|
+
exports.decodeBlockSize = decodeBlockSize;
|
|
13
|
+
exports.writeScreenCommand = writeScreenCommand;
|
|
14
|
+
exports.imageChunkPacket = imageChunkPacket;
|
|
15
|
+
exports.decodeTransferAck = decodeTransferAck;
|
|
16
|
+
/** BLE manufacturer ID Gicisky devices advertise under (20563 decimal). */
|
|
17
|
+
exports.GICISKY_MANUFACTURER_ID = 0x5053;
|
|
18
|
+
/**
|
|
19
|
+
* The two GATT characteristics used for painting, found dynamically rather than hardcoded:
|
|
20
|
+
* both reference drivers walk every service whose UUID starts `0000f...`, collect their
|
|
21
|
+
* characteristics, and sort by the 16-bit UUID value - the lowest (observed as `fef1`) is the
|
|
22
|
+
* command/status characteristic, the next (observed as `fef2`) carries image data. Doing the
|
|
23
|
+
* same walk here (see `findCommandAndImageCharacteristics` in `index.ts`) survives a firmware
|
|
24
|
+
* that puts these under a different parent service UUID than expected.
|
|
25
|
+
*/
|
|
26
|
+
exports.CANDIDATE_SERVICE_UUID_PREFIX = "0000f";
|
|
27
|
+
/**
|
|
28
|
+
* Decodes the 5-byte manufacturer-data payload Gicisky devices advertise. Mirrors
|
|
29
|
+
* `GiciskyBluetoothDeviceData._parse_gicisky` (hass-gicisky's `parser.py`).
|
|
30
|
+
*/
|
|
31
|
+
function decodeAdvertisedInfo(data) {
|
|
32
|
+
if (data.length !== 5) {
|
|
33
|
+
return undefined;
|
|
34
|
+
}
|
|
35
|
+
const hardware = (data[4] << 8) | data[0];
|
|
36
|
+
return {
|
|
37
|
+
deviceId: hardware & 0x3fff,
|
|
38
|
+
hardware,
|
|
39
|
+
firmware: (data[2] << 8) | data[3],
|
|
40
|
+
batteryMv: data[1] * 100,
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
exports.COMMAND = {
|
|
44
|
+
requestBlockSize: 0x01,
|
|
45
|
+
writeScreen: 0x02,
|
|
46
|
+
startImageTransfer: 0x03,
|
|
47
|
+
};
|
|
48
|
+
/** `write_start_with_response`'s ack: `[0x01, <blockSize LE16>]` - observed as 244 on real hardware. */
|
|
49
|
+
function decodeBlockSize(ack) {
|
|
50
|
+
if (ack.length < 3 || ack[0] !== exports.COMMAND.requestBlockSize) {
|
|
51
|
+
return undefined;
|
|
52
|
+
}
|
|
53
|
+
return ack.readUInt16LE(1);
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Builds the `writeScreen` command announcing the total encoded payload size.
|
|
57
|
+
*
|
|
58
|
+
* `compression2` devices (7.5"/10.2") expect a 6-byte packet with a trailing `0x01` flag byte;
|
|
59
|
+
* every other device expects an 8-byte packet with 3 trailing zero bytes. Mirrors
|
|
60
|
+
* `GiciskyClient._make_cmd_packet` (hass-gicisky's `writer.py`).
|
|
61
|
+
*/
|
|
62
|
+
function writeScreenCommand(payloadLength, compression2) {
|
|
63
|
+
if (compression2) {
|
|
64
|
+
const packet = Buffer.alloc(6);
|
|
65
|
+
packet[0] = exports.COMMAND.writeScreen;
|
|
66
|
+
packet.writeUInt32LE(payloadLength, 1);
|
|
67
|
+
packet[5] = 0x01;
|
|
68
|
+
return packet;
|
|
69
|
+
}
|
|
70
|
+
const packet = Buffer.alloc(8);
|
|
71
|
+
packet[0] = exports.COMMAND.writeScreen;
|
|
72
|
+
packet.writeUInt32LE(payloadLength, 1);
|
|
73
|
+
return packet;
|
|
74
|
+
}
|
|
75
|
+
/** Builds one image-data packet: a 4-byte LE part index followed by that part's chunk. */
|
|
76
|
+
function imageChunkPacket(part, chunk) {
|
|
77
|
+
const packet = Buffer.alloc(4 + chunk.length);
|
|
78
|
+
packet.writeUInt32LE(part, 0);
|
|
79
|
+
chunk.copy(packet, 4);
|
|
80
|
+
return packet;
|
|
81
|
+
}
|
|
82
|
+
/** Ack shared by `startImageTransfer` and every image-data chunk: `[0x05, 0x00, <nextPart LE32>]`. */
|
|
83
|
+
function decodeTransferAck(ack) {
|
|
84
|
+
if (ack.length < 6 || ack[0] !== 0x05) {
|
|
85
|
+
return undefined;
|
|
86
|
+
}
|
|
87
|
+
return { ok: ack[1] === 0x00, nextPart: ack.readUInt32LE(2) };
|
|
88
|
+
}
|
package/dist/devices/types.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { Device } from "@naugehyde/node-ble";
|
|
2
2
|
import { Bitmap } from "../render/types";
|
|
3
|
+
import { ReframeMode } from "../render/reframe";
|
|
3
4
|
export type Colour = "black" | "white" | "red" | "yellow";
|
|
4
5
|
/**
|
|
5
6
|
* Static facts about one device model, keyed by (vendor, pid) by the registry —
|
|
@@ -57,6 +58,8 @@ export interface VendorDeviceConfig {
|
|
|
57
58
|
modelOverride?: DeviceModelOverride;
|
|
58
59
|
/** How long to wait for the BLE connect step before giving up - if omitted, the driver picks its own default. */
|
|
59
60
|
connectTimeoutMs?: number;
|
|
61
|
+
/** How to fit the bitmap onto the panel when its size doesn't already match - see `ReframeMode`. Defaults to `"fixed"`, i.e. unchanged (encoding then rejects the mismatch, as it always has). */
|
|
62
|
+
reframe?: ReframeMode;
|
|
60
63
|
}
|
|
61
64
|
export interface VendorDriver {
|
|
62
65
|
vendor: string;
|
|
@@ -5,6 +5,7 @@ const bleDiscovery_1 = require("../bleDiscovery");
|
|
|
5
5
|
const pluginVersion_1 = require("../../pluginVersion");
|
|
6
6
|
const metadata_1 = require("./metadata");
|
|
7
7
|
const encode_1 = require("./encode");
|
|
8
|
+
const reframe_1 = require("../../render/reframe");
|
|
8
9
|
const protocol_1 = require("./protocol");
|
|
9
10
|
/** node-ble has no MTU API; this matches the reference driver's mtu(247)-9 default. */
|
|
10
11
|
const UPLOAD_CHUNK_SIZE = 238;
|
|
@@ -86,7 +87,8 @@ class ZhsunycoDriver {
|
|
|
86
87
|
const challenge = await authChar.readValue();
|
|
87
88
|
await authChar.writeValueWithoutResponse((0, protocol_1.authResponse)(challenge, aesKey));
|
|
88
89
|
await (0, bleDiscovery_1.sleep)(AUTH_SETTLE_DELAY_MS);
|
|
89
|
-
const
|
|
90
|
+
const framed = (0, reframe_1.reframeBitmap)(bitmap, metadata.width, metadata.height - metadata.voffset, config.reframe ?? "fixed");
|
|
91
|
+
const pixelData = (0, encode_1.encodeBitmap)(framed, metadata);
|
|
90
92
|
for (let offset = 0; offset < pixelData.length; offset += UPLOAD_CHUNK_SIZE) {
|
|
91
93
|
const chunk = pixelData.subarray(offset, offset + UPLOAD_CHUNK_SIZE);
|
|
92
94
|
await dataChar.writeValueWithResponse(Buffer.concat([(0, protocol_1.commandHeader)(protocol_1.COMMAND.uploadBlock, offset), chunk]));
|
package/dist/plugin.js
CHANGED
|
@@ -4,6 +4,7 @@ exports.createPlugin = createPlugin;
|
|
|
4
4
|
const config_1 = require("./config");
|
|
5
5
|
const registry_1 = require("./devices/registry");
|
|
6
6
|
const zhsunyco_1 = require("./devices/zhsunyco");
|
|
7
|
+
const gicisky_1 = require("./devices/gicisky");
|
|
7
8
|
const discoveryCoordinator_1 = require("./devices/discoveryCoordinator");
|
|
8
9
|
const discoveredDevicesStore_1 = require("./devices/discoveredDevicesStore");
|
|
9
10
|
const repaintScheduler_1 = require("./repaintScheduler");
|
|
@@ -33,6 +34,7 @@ async function runStartupScan(app, durationSeconds) {
|
|
|
33
34
|
}
|
|
34
35
|
function createPlugin(app) {
|
|
35
36
|
(0, registry_1.registerDriver)(new zhsunyco_1.ZhsunycoDriver());
|
|
37
|
+
(0, registry_1.registerDriver)(new gicisky_1.GiciskyDriver());
|
|
36
38
|
let scheduler;
|
|
37
39
|
const plugin = {
|
|
38
40
|
id: "signalk-einklabel-plugin",
|
package/dist/render/binding.d.ts
CHANGED
|
@@ -76,6 +76,15 @@ export declare function resourceContextKey(binding: Pick<Binding, "resource" | "
|
|
|
76
76
|
* way as a `<text>` binding.
|
|
77
77
|
*/
|
|
78
78
|
export declare function findBindings(svgSource: string): Binding[];
|
|
79
|
+
/**
|
|
80
|
+
* Reads the root `<svg>` element's declared `width`/`height` (falling back to the 3rd/4th `viewBox`
|
|
81
|
+
* numbers when either attribute is missing) - lets a caller default a render size to what the template
|
|
82
|
+
* itself was authored for, e.g. the CLI's -w/--width and --height options.
|
|
83
|
+
*/
|
|
84
|
+
export declare function readTemplateDimensions(svgSource: string): {
|
|
85
|
+
width?: number;
|
|
86
|
+
height?: number;
|
|
87
|
+
};
|
|
79
88
|
/** Resolves a parsed `Binding` against the render context assembled by `assembleRawContext`. */
|
|
80
89
|
export declare function resolveBinding(binding: Binding, context: TemplateContext): unknown;
|
|
81
90
|
/**
|
package/dist/render/binding.js
CHANGED
|
@@ -3,6 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.parseBinding = parseBinding;
|
|
4
4
|
exports.resourceContextKey = resourceContextKey;
|
|
5
5
|
exports.findBindings = findBindings;
|
|
6
|
+
exports.readTemplateDimensions = readTemplateDimensions;
|
|
6
7
|
exports.resolveBinding = resolveBinding;
|
|
7
8
|
exports.renderBinding = renderBinding;
|
|
8
9
|
exports.buildLabelContext = buildLabelContext;
|
|
@@ -99,6 +100,26 @@ function findBindings(svgSource) {
|
|
|
99
100
|
}
|
|
100
101
|
return bindings;
|
|
101
102
|
}
|
|
103
|
+
/**
|
|
104
|
+
* Reads the root `<svg>` element's declared `width`/`height` (falling back to the 3rd/4th `viewBox`
|
|
105
|
+
* numbers when either attribute is missing) - lets a caller default a render size to what the template
|
|
106
|
+
* itself was authored for, e.g. the CLI's -w/--width and --height options.
|
|
107
|
+
*/
|
|
108
|
+
function readTemplateDimensions(svgSource) {
|
|
109
|
+
const doc = new xmldom_1.DOMParser().parseFromString(svgSource, "image/svg+xml");
|
|
110
|
+
const svg = doc.getElementsByTagName("svg").item(0);
|
|
111
|
+
if (!svg)
|
|
112
|
+
return {};
|
|
113
|
+
const viewBox = svg.getAttribute("viewBox")?.trim().split(/\s+/).map(Number);
|
|
114
|
+
const viewBoxWidth = viewBox?.length === 4 && Number.isFinite(viewBox[2]) ? viewBox[2] : undefined;
|
|
115
|
+
const viewBoxHeight = viewBox?.length === 4 && Number.isFinite(viewBox[3]) ? viewBox[3] : undefined;
|
|
116
|
+
const width = parseFloat(svg.getAttribute("width") ?? "");
|
|
117
|
+
const height = parseFloat(svg.getAttribute("height") ?? "");
|
|
118
|
+
return {
|
|
119
|
+
width: Number.isFinite(width) ? width : viewBoxWidth,
|
|
120
|
+
height: Number.isFinite(height) ? height : viewBoxHeight,
|
|
121
|
+
};
|
|
122
|
+
}
|
|
102
123
|
/** Supports both `a.[0].b` and `a[0].b` array index notation, matching `setAtPath` in repaintScheduler.ts. */
|
|
103
124
|
function getAtPath(obj, path) {
|
|
104
125
|
const segments = path
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { Bitmap } from "./types";
|
|
2
|
+
/**
|
|
3
|
+
* How a rendered bitmap is fitted onto a device's actual panel size when the two don't match -
|
|
4
|
+
* `"fixed"` (the default, and the only behaviour before this existed) makes no change, leaving each
|
|
5
|
+
* vendor driver's own exact-size check in `encodeBitmap` to reject the mismatch; `"scale"` scales
|
|
6
|
+
* the source onto the target dimensions independently per axis (not preserving aspect ratio);
|
|
7
|
+
* `"crop"` keeps source pixels 1:1, placing them from the top-left and either truncating whatever
|
|
8
|
+
* doesn't fit (source bigger than target) or leaving the extra target space blank (source smaller).
|
|
9
|
+
*/
|
|
10
|
+
export type ReframeMode = "fixed" | "scale" | "crop";
|
|
11
|
+
/** Applies `mode` to fit `bitmap` onto a `targetWidth`x`targetHeight` panel - a no-op whenever the size already matches, regardless of mode. */
|
|
12
|
+
export declare function reframeBitmap(bitmap: Bitmap, targetWidth: number, targetHeight: number, mode: ReframeMode): Bitmap;
|