@wms-js/lib 0.1.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.
Files changed (78) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +16 -0
  3. package/dist/command/ack-match.d.ts +5 -0
  4. package/dist/command/ack-match.js +9 -0
  5. package/dist/command/ack-match.js.map +1 -0
  6. package/dist/command/session.d.ts +23 -0
  7. package/dist/command/session.js +98 -0
  8. package/dist/command/session.js.map +1 -0
  9. package/dist/command/types.d.ts +14 -0
  10. package/dist/command/types.js +2 -0
  11. package/dist/command/types.js.map +1 -0
  12. package/dist/commands/name.d.ts +25 -0
  13. package/dist/commands/name.js +209 -0
  14. package/dist/commands/name.js.map +1 -0
  15. package/dist/controller.d.ts +26 -0
  16. package/dist/controller.js +108 -0
  17. package/dist/controller.js.map +1 -0
  18. package/dist/frame/errors.d.ts +4 -0
  19. package/dist/frame/errors.js +9 -0
  20. package/dist/frame/errors.js.map +1 -0
  21. package/dist/frame/parser.d.ts +13 -0
  22. package/dist/frame/parser.js +69 -0
  23. package/dist/frame/parser.js.map +1 -0
  24. package/dist/frame/serializer.d.ts +1 -0
  25. package/dist/frame/serializer.js +5 -0
  26. package/dist/frame/serializer.js.map +1 -0
  27. package/dist/index.d.ts +33 -0
  28. package/dist/index.js +22 -0
  29. package/dist/index.js.map +1 -0
  30. package/dist/logging/logger.d.ts +15 -0
  31. package/dist/logging/logger.js +60 -0
  32. package/dist/logging/logger.js.map +1 -0
  33. package/dist/network/events.d.ts +6 -0
  34. package/dist/network/events.js +24 -0
  35. package/dist/network/events.js.map +1 -0
  36. package/dist/network/manager.d.ts +36 -0
  37. package/dist/network/manager.js +281 -0
  38. package/dist/network/manager.js.map +1 -0
  39. package/dist/network/types.d.ts +35 -0
  40. package/dist/network/types.js +2 -0
  41. package/dist/network/types.js.map +1 -0
  42. package/dist/parsers/device-scan-response.d.ts +10 -0
  43. package/dist/parsers/device-scan-response.js +24 -0
  44. package/dist/parsers/device-scan-response.js.map +1 -0
  45. package/dist/parsers/device-scan.d.ts +6 -0
  46. package/dist/parsers/device-scan.js +14 -0
  47. package/dist/parsers/device-scan.js.map +1 -0
  48. package/dist/parsers/device-status.d.ts +14 -0
  49. package/dist/parsers/device-status.js +23 -0
  50. package/dist/parsers/device-status.js.map +1 -0
  51. package/dist/parsers/move-response.d.ts +8 -0
  52. package/dist/parsers/move-response.js +16 -0
  53. package/dist/parsers/move-response.js.map +1 -0
  54. package/dist/parsers/network-join.d.ts +8 -0
  55. package/dist/parsers/network-join.js +29 -0
  56. package/dist/parsers/network-join.js.map +1 -0
  57. package/dist/parsers/network-params.d.ts +7 -0
  58. package/dist/parsers/network-params.js +19 -0
  59. package/dist/parsers/network-params.js.map +1 -0
  60. package/dist/parsers/wave-request.d.ts +5 -0
  61. package/dist/parsers/wave-request.js +13 -0
  62. package/dist/parsers/wave-request.js.map +1 -0
  63. package/dist/parsers/wave-response.d.ts +6 -0
  64. package/dist/parsers/wave-response.js +14 -0
  65. package/dist/parsers/wave-response.js.map +1 -0
  66. package/dist/parsers/weather-station.d.ts +9 -0
  67. package/dist/parsers/weather-station.js +30 -0
  68. package/dist/parsers/weather-station.js.map +1 -0
  69. package/dist/routing/broadcast-router.d.ts +9 -0
  70. package/dist/routing/broadcast-router.js +40 -0
  71. package/dist/routing/broadcast-router.js.map +1 -0
  72. package/dist/serial/driver.d.ts +8 -0
  73. package/dist/serial/driver.js +2 -0
  74. package/dist/serial/driver.js.map +1 -0
  75. package/dist/testing/mock-serial.d.ts +22 -0
  76. package/dist/testing/mock-serial.js +62 -0
  77. package/dist/testing/mock-serial.js.map +1 -0
  78. package/package.json +29 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Giuseppe Iannello
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,16 @@
1
+ # @wms-js/lib
2
+
3
+ Core library for the WMS radio protocol used by Warema motorised blinds and awnings.
4
+
5
+ Provides frame parsing, serial abstractions, broadcast routing, and a `NetworkManager`
6
+ for device discovery, status monitoring, and position control.
7
+
8
+ ## Install
9
+
10
+ ```sh
11
+ npm install @wms-js/lib
12
+ ```
13
+
14
+ ## License
15
+
16
+ MIT
@@ -0,0 +1,5 @@
1
+ import { type AckMatcher } from "./types.js";
2
+ export declare const ackMatch: {
3
+ exact(type: string): AckMatcher;
4
+ prefix(prefix: string): AckMatcher;
5
+ };
@@ -0,0 +1,9 @@
1
+ export const ackMatch = {
2
+ exact(type) {
3
+ return (frame) => (frame === type ? "" : null);
4
+ },
5
+ prefix(prefix) {
6
+ return (frame) => (frame.startsWith(prefix) ? frame.slice(prefix.length) : null);
7
+ },
8
+ };
9
+ //# sourceMappingURL=ack-match.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ack-match.js","sourceRoot":"","sources":["../../src/command/ack-match.ts"],"names":[],"mappings":"AAEA,MAAM,CAAC,MAAM,QAAQ,GAAG;IACtB,KAAK,CAAC,IAAY;QAChB,OAAO,CAAC,KAAa,EAAE,EAAE,CAAC,CAAC,KAAK,KAAK,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,CAAA;IACxD,CAAC;IAED,MAAM,CAAC,MAAc;QACnB,OAAO,CAAC,KAAa,EAAE,EAAE,CAAC,CAAC,KAAK,CAAC,UAAU,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,KAAK,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAA;IAC1F,CAAC;CACF,CAAA"}
@@ -0,0 +1,23 @@
1
+ import { type SendResult, type SessionOptions } from "./types.js";
2
+ export declare class CommandSession {
3
+ readonly command: string;
4
+ readonly ack: Promise<SendResult>;
5
+ readonly promise: Promise<unknown>;
6
+ /** @internal called by RadioController when session completes */
7
+ _onDone: (() => void) | null;
8
+ private resolveAck;
9
+ private readonly responseHandlers;
10
+ private responseWindow;
11
+ private responseWindowTimer;
12
+ private cancelled;
13
+ private resolveDone;
14
+ private readonly options;
15
+ constructor(command: string, options: SessionOptions);
16
+ /** @internal called by RadioController when session becomes active */
17
+ _startAckTimer(): void;
18
+ onResponse(handler: (frame: string) => void): () => void;
19
+ feedFrame(frame: string): boolean;
20
+ cancel(): void;
21
+ private closeResponseWindow;
22
+ private finish;
23
+ }
@@ -0,0 +1,98 @@
1
+ export class CommandSession {
2
+ command;
3
+ ack;
4
+ promise;
5
+ /** @internal called by RadioController when session completes */
6
+ _onDone = null;
7
+ resolveAck = null;
8
+ responseHandlers = new Set();
9
+ responseWindow = false;
10
+ responseWindowTimer = null;
11
+ cancelled = false;
12
+ resolveDone = null;
13
+ options;
14
+ constructor(command, options) {
15
+ this.command = command;
16
+ this.options = {
17
+ ackTimeoutMs: 100,
18
+ responseWindowMs: 500,
19
+ ...options,
20
+ };
21
+ this.ack = new Promise((resolve) => {
22
+ this.resolveAck = resolve;
23
+ });
24
+ this.promise = new Promise((resolve) => {
25
+ this.resolveDone = resolve;
26
+ });
27
+ this.promise.then(() => this._onDone?.());
28
+ }
29
+ /** @internal called by RadioController when session becomes active */
30
+ _startAckTimer() {
31
+ setTimeout(() => {
32
+ if (this.cancelled || this.responseWindow)
33
+ return;
34
+ this.resolveAck?.({ kind: "timeout" });
35
+ this.cancel();
36
+ }, this.options.ackTimeoutMs);
37
+ }
38
+ onResponse(handler) {
39
+ this.responseHandlers.add(handler);
40
+ return () => {
41
+ this.responseHandlers.delete(handler);
42
+ };
43
+ }
44
+ feedFrame(frame) {
45
+ if (this.cancelled)
46
+ return false;
47
+ if (frame === "f") {
48
+ this.resolveAck?.({ kind: "fail" });
49
+ this.cancel();
50
+ return true;
51
+ }
52
+ if (!this.responseWindow) {
53
+ const stripped = this.options.ackMatcher(frame);
54
+ if (stripped !== null) {
55
+ if (this.options.responseWindowMs !== undefined && this.options.responseWindowMs > 0) {
56
+ this.responseWindow = true;
57
+ this.resolveAck?.({ kind: "ack", frame: stripped });
58
+ this.responseWindowTimer = setTimeout(() => {
59
+ this.closeResponseWindow();
60
+ }, this.options.responseWindowMs);
61
+ }
62
+ else {
63
+ this.resolveAck?.({ kind: "ack", frame: stripped });
64
+ this.cancel();
65
+ }
66
+ return true;
67
+ }
68
+ return false;
69
+ }
70
+ for (const handler of this.responseHandlers) {
71
+ handler(frame);
72
+ }
73
+ return true;
74
+ }
75
+ cancel() {
76
+ if (this.cancelled)
77
+ return;
78
+ this.cancelled = true;
79
+ this.responseHandlers.clear();
80
+ if (this.responseWindowTimer !== null) {
81
+ clearTimeout(this.responseWindowTimer);
82
+ this.responseWindowTimer = null;
83
+ }
84
+ if (!this.responseWindow) {
85
+ this.resolveAck?.({ kind: "timeout" });
86
+ }
87
+ this.finish();
88
+ }
89
+ closeResponseWindow() {
90
+ this.responseWindow = false;
91
+ this.finish();
92
+ }
93
+ finish() {
94
+ this.resolveDone?.();
95
+ this.resolveDone = null;
96
+ }
97
+ }
98
+ //# sourceMappingURL=session.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"session.js","sourceRoot":"","sources":["../../src/command/session.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,14 @@
1
+ export type AckMatcher = (frame: string) => string | null;
2
+ export interface SessionOptions {
3
+ ackMatcher: AckMatcher;
4
+ ackTimeoutMs?: number;
5
+ responseWindowMs?: number;
6
+ }
7
+ export type SendResult = {
8
+ kind: "ack";
9
+ frame: string;
10
+ } | {
11
+ kind: "fail";
12
+ } | {
13
+ kind: "timeout";
14
+ };
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["../../src/command/types.ts"],"names":[],"mappings":""}
@@ -0,0 +1,25 @@
1
+ import { RadioController } from "../controller.js";
2
+ import type { DeviceScanResponse } from "../parsers/device-scan-response.js";
3
+ import type { DeviceStatus } from "../parsers/device-status.js";
4
+ export interface NetworkParams {
5
+ receiveBroadcasts: boolean;
6
+ channel: number;
7
+ panId: string;
8
+ }
9
+ export declare class Commands {
10
+ private radio;
11
+ constructor(radio: RadioController);
12
+ getName(): Promise<string>;
13
+ getVersion(): Promise<string>;
14
+ setNetworkParameters(params: NetworkParams): Promise<void>;
15
+ setEncryptionKey(key: string): Promise<void>;
16
+ scanNetwork(panId: string, timeoutMs?: number): Promise<DeviceScanResponse[]>;
17
+ getDeviceStatus(serialNumber: string, timeoutMs?: number): Promise<DeviceStatus>;
18
+ waveDevice(serialNumber: string, timeoutMs?: number): Promise<{
19
+ serialNumber: string;
20
+ code?: string;
21
+ }>;
22
+ stopDevice(serialNumber: string, timeoutMs?: number): Promise<void>;
23
+ moveToPosition(serialNumber: string, position: number, inclination?: number, timeoutMs?: number): Promise<void>;
24
+ private sendMoveCommand;
25
+ }
@@ -0,0 +1,209 @@
1
+ import { ackMatch } from "../command/ack-match.js";
2
+ import { deviceScanResponseMatcher } from "../parsers/device-scan-response.js";
3
+ import { deviceStatusMatcher } from "../parsers/device-status.js";
4
+ import { waveResponseMatcher } from "../parsers/wave-response.js";
5
+ import { waveRequestMatcher } from "../parsers/wave-request.js";
6
+ export class Commands {
7
+ radio;
8
+ constructor(radio) {
9
+ this.radio = radio;
10
+ }
11
+ async getName() {
12
+ const session = this.radio.send("G", {
13
+ ackMatcher: ackMatch.prefix("g"),
14
+ responseWindowMs: 0,
15
+ });
16
+ const ack = await session.ack;
17
+ if (ack.kind === "timeout") {
18
+ throw new Error("getName: ack timeout");
19
+ }
20
+ if (ack.kind === "fail") {
21
+ throw new Error("getName: command rejected");
22
+ }
23
+ return ack.frame;
24
+ }
25
+ async getVersion() {
26
+ const session = this.radio.send("V", {
27
+ ackMatcher: ackMatch.prefix("v"),
28
+ responseWindowMs: 0,
29
+ });
30
+ const ack = await session.ack;
31
+ if (ack.kind === "timeout") {
32
+ throw new Error("getVersion: ack timeout");
33
+ }
34
+ if (ack.kind === "fail") {
35
+ throw new Error("getVersion: command rejected");
36
+ }
37
+ return ack.frame.trim();
38
+ }
39
+ async setNetworkParameters(params) {
40
+ if (params.channel < 11 || params.channel > 26) {
41
+ throw new Error("setNetworkParameters: invalid channel");
42
+ }
43
+ if (!/^[0-9A-Fa-f]{4}$/.test(params.panId)) {
44
+ throw new Error("setNetworkParameters: invalid PAN ID");
45
+ }
46
+ const modeChar = params.receiveBroadcasts ? "%" : "#";
47
+ const panId = params.panId.toUpperCase();
48
+ const frame = `M ${modeChar} ${params.channel} ${panId}`;
49
+ const session = this.radio.send(frame, {
50
+ ackMatcher: ackMatch.exact("a"),
51
+ responseWindowMs: 0,
52
+ });
53
+ const ack = await session.ack;
54
+ if (ack.kind === "fail") {
55
+ throw new Error("setNetworkParameters: command rejected");
56
+ }
57
+ if (ack.kind === "timeout") {
58
+ throw new Error("setNetworkParameters: ack timeout");
59
+ }
60
+ }
61
+ async setEncryptionKey(key) {
62
+ if (!/^[0-9A-Fa-f]{32}$/.test(key)) {
63
+ throw new Error("setEncryptionKey: invalid key");
64
+ }
65
+ const frame = `K 401 ${key.toUpperCase()}`;
66
+ const session = this.radio.send(frame, {
67
+ ackMatcher: ackMatch.exact("a"),
68
+ responseWindowMs: 0,
69
+ });
70
+ const ack = await session.ack;
71
+ if (ack.kind === "fail") {
72
+ throw new Error("setEncryptionKey: command rejected");
73
+ }
74
+ if (ack.kind === "timeout") {
75
+ throw new Error("setEncryptionKey: ack timeout");
76
+ }
77
+ }
78
+ // NOTE: responseWindowMs consumes ALL serial frames during the scan window,
79
+ // suppressing broadcast handlers (weather station, pairing, etc.). This is
80
+ // acceptable because scanning is infrequent and short-lived (~3s).
81
+ async scanNetwork(panId, timeoutMs = 3000) {
82
+ if (!/^[0-9A-Fa-f]{4}$/.test(panId)) {
83
+ throw new Error("scanNetwork: invalid PAN ID");
84
+ }
85
+ const seen = new Map();
86
+ const frame = `R04FFFFFF7020${panId.toUpperCase()}02`;
87
+ const session = this.radio.send(frame, {
88
+ ackMatcher: ackMatch.exact("a"),
89
+ responseWindowMs: timeoutMs,
90
+ });
91
+ session.onResponse((content) => {
92
+ const parsed = deviceScanResponseMatcher(content);
93
+ if (parsed && !seen.has(parsed.serialNumber)) {
94
+ seen.set(parsed.serialNumber, parsed);
95
+ }
96
+ });
97
+ const ack = await session.ack;
98
+ if (ack.kind === "fail") {
99
+ throw new Error("scanNetwork: command rejected");
100
+ }
101
+ if (ack.kind === "timeout") {
102
+ throw new Error("scanNetwork: ack timeout");
103
+ }
104
+ await session.promise;
105
+ return [...seen.values()];
106
+ }
107
+ async getDeviceStatus(serialNumber, timeoutMs = 2000) {
108
+ if (!/^[0-9A-Fa-f]{6}$/.test(serialNumber)) {
109
+ throw new Error("getDeviceStatus: invalid serial number");
110
+ }
111
+ const frame = `R06${serialNumber.toUpperCase()}801001000005`;
112
+ const session = this.radio.send(frame, {
113
+ ackMatcher: ackMatch.exact("a"),
114
+ responseWindowMs: timeoutMs,
115
+ });
116
+ let result = null;
117
+ session.onResponse((content) => {
118
+ const parsed = deviceStatusMatcher(content);
119
+ if (parsed && !result) {
120
+ result = parsed;
121
+ session.cancel();
122
+ }
123
+ });
124
+ const ack = await session.ack;
125
+ if (ack.kind === "fail") {
126
+ throw new Error("getDeviceStatus: command rejected");
127
+ }
128
+ if (ack.kind === "timeout") {
129
+ throw new Error("getDeviceStatus: ack timeout");
130
+ }
131
+ await session.promise;
132
+ if (!result) {
133
+ throw new Error("getDeviceStatus: no response from device");
134
+ }
135
+ return result;
136
+ }
137
+ async waveDevice(serialNumber, timeoutMs = 2000) {
138
+ if (!/^[0-9A-Fa-f]{6}$/.test(serialNumber)) {
139
+ throw new Error("waveDevice: invalid serial number");
140
+ }
141
+ const serial = serialNumber.toUpperCase();
142
+ const frame = `R06${serial}7050`;
143
+ const session = this.radio.send(frame, {
144
+ ackMatcher: ackMatch.exact("a"),
145
+ responseWindowMs: timeoutMs,
146
+ });
147
+ let result = null;
148
+ session.onResponse((content) => {
149
+ if (result)
150
+ return;
151
+ const wr = waveResponseMatcher(content);
152
+ if (wr && wr.serialNumber === serial) {
153
+ result = { serialNumber: wr.serialNumber, code: wr.code };
154
+ session.cancel();
155
+ return;
156
+ }
157
+ const wr2 = waveRequestMatcher(content);
158
+ if (wr2 && wr2.serialNumber === serial) {
159
+ result = { serialNumber: wr2.serialNumber };
160
+ session.cancel();
161
+ }
162
+ });
163
+ const ack = await session.ack;
164
+ if (ack.kind === "fail") {
165
+ throw new Error("waveDevice: command rejected");
166
+ }
167
+ if (ack.kind === "timeout") {
168
+ throw new Error("waveDevice: ack timeout");
169
+ }
170
+ await session.promise;
171
+ if (!result) {
172
+ throw new Error("waveDevice: no response from device");
173
+ }
174
+ return result;
175
+ }
176
+ async stopDevice(serialNumber, timeoutMs = 2000) {
177
+ if (!/^[0-9A-Fa-f]{6}$/.test(serialNumber)) {
178
+ throw new Error("stopDevice: invalid serial number");
179
+ }
180
+ const cmd = `R06${serialNumber.toUpperCase()}707001`;
181
+ return this.sendMoveCommand(cmd, timeoutMs);
182
+ }
183
+ async moveToPosition(serialNumber, position, inclination = 0, timeoutMs = 2000) {
184
+ if (!/^[0-9A-Fa-f]{6}$/.test(serialNumber)) {
185
+ throw new Error("moveToPosition: invalid serial number");
186
+ }
187
+ if (position < 0 || position > 100) {
188
+ throw new Error("moveToPosition: position must be 0-100");
189
+ }
190
+ const pp = Math.round(position * 2).toString(16).toUpperCase().padStart(2, "0");
191
+ const ww = Math.round(inclination + 127).toString(16).toUpperCase().padStart(2, "0");
192
+ const cmd = `R06${serialNumber.toUpperCase()}707003${pp}${ww}0000`;
193
+ return this.sendMoveCommand(cmd, timeoutMs);
194
+ }
195
+ async sendMoveCommand(rawCommand, timeoutMs) {
196
+ const session = this.radio.send(rawCommand, {
197
+ ackMatcher: ackMatch.exact("a"),
198
+ responseWindowMs: 0,
199
+ });
200
+ const ack = await session.ack;
201
+ if (ack.kind === "fail") {
202
+ throw new Error(`${rawCommand.startsWith("R06") && rawCommand.includes("707001") ? "stopDevice" : "moveToPosition"}: command rejected`);
203
+ }
204
+ if (ack.kind === "timeout") {
205
+ throw new Error(`${rawCommand.startsWith("R06") && rawCommand.includes("707001") ? "stopDevice" : "moveToPosition"}: ack timeout`);
206
+ }
207
+ }
208
+ }
209
+ //# sourceMappingURL=name.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"name.js","sourceRoot":"","sources":["../../src/commands/name.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,26 @@
1
+ import { CommandSession } from "./command/session.js";
2
+ import { type SessionOptions } from "./command/types.js";
3
+ import { type SerialDriver } from "./serial/driver.js";
4
+ export declare class RadioController {
5
+ private driver;
6
+ private parser;
7
+ private queue;
8
+ private activeOp;
9
+ private broadcastHandlers;
10
+ private errorHandlers;
11
+ private unsubs;
12
+ private _isOpen;
13
+ constructor(driver: SerialDriver);
14
+ get isOpen(): boolean;
15
+ open(path: string): Promise<void>;
16
+ close(): Promise<void>;
17
+ send(command: string, options: SessionOptions): CommandSession;
18
+ onBroadcast(handler: (frame: string) => void): () => void;
19
+ onError(handler: (error: Error) => void): () => void;
20
+ private processQueue;
21
+ private onSerialData;
22
+ private routeFrame;
23
+ private cancelActiveOp;
24
+ private emitError;
25
+ private onSerialClose;
26
+ }
@@ -0,0 +1,108 @@
1
+ import { FrameParser } from "./frame/parser.js";
2
+ import { serializeFrame } from "./frame/serializer.js";
3
+ import { CommandSession } from "./command/session.js";
4
+ import { debug } from "./logging/logger.js";
5
+ export class RadioController {
6
+ driver;
7
+ parser = new FrameParser();
8
+ queue = [];
9
+ activeOp = null;
10
+ broadcastHandlers = new Set();
11
+ errorHandlers = new Set();
12
+ unsubs = [];
13
+ _isOpen = false;
14
+ constructor(driver) {
15
+ this.driver = driver;
16
+ this.parser.onMalformedFrame((error) => {
17
+ this.emitError(error);
18
+ });
19
+ }
20
+ get isOpen() {
21
+ return this._isOpen;
22
+ }
23
+ async open(path) {
24
+ await this.driver.open(path);
25
+ this._isOpen = true;
26
+ this.unsubs.push(this.driver.onData((data) => this.onSerialData(data)));
27
+ this.unsubs.push(this.driver.onError((error) => this.emitError(error)));
28
+ this.unsubs.push(this.driver.onClose(() => this.onSerialClose()));
29
+ }
30
+ async close() {
31
+ this.cancelActiveOp();
32
+ for (const op of this.queue) {
33
+ op.cancel();
34
+ }
35
+ this.queue = [];
36
+ for (const unsub of this.unsubs) {
37
+ unsub();
38
+ }
39
+ this.unsubs = [];
40
+ this._isOpen = false;
41
+ await this.driver.close();
42
+ }
43
+ send(command, options) {
44
+ const session = new CommandSession(command, options);
45
+ session._onDone = () => {
46
+ if (this.activeOp === session) {
47
+ this.activeOp = null;
48
+ this.processQueue();
49
+ }
50
+ };
51
+ this.queue.push(session);
52
+ this.processQueue();
53
+ return session;
54
+ }
55
+ onBroadcast(handler) {
56
+ this.broadcastHandlers.add(handler);
57
+ return () => { this.broadcastHandlers.delete(handler); };
58
+ }
59
+ onError(handler) {
60
+ this.errorHandlers.add(handler);
61
+ return () => { this.errorHandlers.delete(handler); };
62
+ }
63
+ processQueue() {
64
+ if (this.activeOp !== null || this.queue.length === 0)
65
+ return;
66
+ const op = this.queue.shift();
67
+ this.activeOp = op;
68
+ op._startAckTimer();
69
+ const raw = serializeFrame(op.command);
70
+ debug(">>", op.command);
71
+ this.driver.write(raw);
72
+ }
73
+ onSerialData(data) {
74
+ const frames = this.parser.feed(data);
75
+ for (const frame of frames) {
76
+ this.routeFrame(frame);
77
+ }
78
+ }
79
+ routeFrame(frame) {
80
+ if (this.activeOp !== null) {
81
+ const consumed = this.activeOp.feedFrame(frame);
82
+ if (consumed) {
83
+ debug("<<", `${frame} (session: ${this.activeOp.command})`);
84
+ return;
85
+ }
86
+ }
87
+ debug("<<", `${frame} (broadcast)`);
88
+ for (const handler of this.broadcastHandlers) {
89
+ handler(frame);
90
+ }
91
+ }
92
+ cancelActiveOp() {
93
+ if (this.activeOp !== null) {
94
+ this.activeOp.cancel();
95
+ this.activeOp = null;
96
+ }
97
+ }
98
+ emitError(error) {
99
+ for (const handler of this.errorHandlers) {
100
+ handler(error);
101
+ }
102
+ }
103
+ onSerialClose() {
104
+ this.cancelActiveOp();
105
+ this._isOpen = false;
106
+ }
107
+ }
108
+ //# sourceMappingURL=controller.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"controller.js","sourceRoot":"","sources":["../src/controller.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,4 @@
1
+ export declare class MalformedFrameError extends Error {
2
+ readonly partial: string;
3
+ constructor(partial: string);
4
+ }
@@ -0,0 +1,9 @@
1
+ export class MalformedFrameError extends Error {
2
+ partial;
3
+ constructor(partial) {
4
+ super(`Malformed frame: no closing brace within timeout`);
5
+ this.name = "MalformedFrameError";
6
+ this.partial = partial;
7
+ }
8
+ }
9
+ //# sourceMappingURL=errors.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"errors.js","sourceRoot":"","sources":["../../src/frame/errors.ts"],"names":[],"mappings":"AAAA,MAAM,OAAO,mBAAoB,SAAQ,KAAK;IACnC,OAAO,CAAQ;IAExB,YAAY,OAAe;QACzB,KAAK,CAAC,kDAAkD,CAAC,CAAA;QACzD,IAAI,CAAC,IAAI,GAAG,qBAAqB,CAAA;QACjC,IAAI,CAAC,OAAO,GAAG,OAAO,CAAA;IACxB,CAAC;CACF"}
@@ -0,0 +1,13 @@
1
+ import { MalformedFrameError } from "./errors.js";
2
+ export declare class FrameParser {
3
+ private buffer;
4
+ private malformedHandler;
5
+ private malformedTimer;
6
+ private readonly partialTimeoutMs;
7
+ constructor(partialTimeoutMs?: number);
8
+ onMalformedFrame(handler: (error: MalformedFrameError) => void): () => void;
9
+ feed(data: Uint8Array): string[];
10
+ reset(): void;
11
+ private startMalformedTimer;
12
+ private clearMalformedTimer;
13
+ }