@matter-server/ble-proxy 0.7.2-alpha.0-20260601-e778038 → 0.8.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/dist/esm/BleProxyConnection.d.ts +34 -0
- package/dist/esm/BleProxyConnection.d.ts.map +1 -0
- package/dist/esm/BleProxyConnection.js +210 -0
- package/dist/esm/BleProxyConnection.js.map +6 -0
- package/dist/esm/BleProxyHandler.d.ts +15 -20
- package/dist/esm/BleProxyHandler.d.ts.map +1 -1
- package/dist/esm/BleProxyHandler.js +114 -183
- package/dist/esm/BleProxyHandler.js.map +1 -1
- package/dist/esm/ProxyBleChannel.d.ts.map +1 -1
- package/dist/esm/ProxyBleChannel.js +70 -40
- package/dist/esm/ProxyBleChannel.js.map +1 -1
- package/dist/esm/ProxyBleClient.d.ts.map +1 -1
- package/dist/esm/ProxyBleClient.js +16 -24
- package/dist/esm/ProxyBleClient.js.map +1 -1
- package/dist/esm/index.d.ts +1 -0
- package/dist/esm/index.d.ts.map +1 -1
- package/dist/esm/index.js +1 -0
- package/dist/esm/index.js.map +1 -1
- package/package.json +21 -20
- package/src/BleProxyConnection.ts +282 -0
- package/src/BleProxyHandler.ts +121 -246
- package/src/ProxyBleChannel.ts +92 -56
- package/src/ProxyBleClient.ts +22 -26
- package/src/index.ts +1 -0
package/src/ProxyBleChannel.ts
CHANGED
|
@@ -78,16 +78,20 @@ export class ProxyBleCentralInterface implements Transport {
|
|
|
78
78
|
}
|
|
79
79
|
|
|
80
80
|
const { peripheralAddress } = address;
|
|
81
|
-
const { hasAdditionalAdvertisementData } = this.#bleScanner.getDiscoveredDevice(peripheralAddress);
|
|
82
81
|
|
|
83
|
-
|
|
84
|
-
|
|
82
|
+
const connection = this.#handler.getOwner(peripheralAddress);
|
|
83
|
+
if (!connection) {
|
|
84
|
+
throw new BleError(`No connected BLE proxy client owns peripheral ${peripheralAddress}`);
|
|
85
85
|
}
|
|
86
86
|
|
|
87
|
-
|
|
87
|
+
const discovered = this.#bleScanner.getDiscoveredDevice(peripheralAddress);
|
|
88
|
+
const { hasAdditionalAdvertisementData } = discovered;
|
|
89
|
+
const rssi = discovered.peripheral.rssi;
|
|
90
|
+
|
|
91
|
+
logger.debug(`Connecting to peripheral ${peripheralAddress} (rssi=${rssi ?? "n/a"}) via proxy`);
|
|
88
92
|
|
|
89
93
|
// 1. Connect
|
|
90
|
-
const { connection_handle, mtu: peripheralMtu } = await
|
|
94
|
+
const { connection_handle, mtu: peripheralMtu } = await connection.sendCommand(BleProxyCommand.Connect, {
|
|
91
95
|
address: peripheralAddress,
|
|
92
96
|
});
|
|
93
97
|
|
|
@@ -95,11 +99,17 @@ export class ProxyBleCentralInterface implements Transport {
|
|
|
95
99
|
if (mtu > MatterBle.MAXIMUM_BTP_MTU) {
|
|
96
100
|
mtu = MatterBle.MAXIMUM_BTP_MTU;
|
|
97
101
|
}
|
|
98
|
-
logger.
|
|
102
|
+
logger.info(
|
|
103
|
+
`Connected to ${peripheralAddress}, handle=${connection_handle}, mtu=${mtu}, rssi=${rssi ?? "n/a"}`,
|
|
104
|
+
);
|
|
105
|
+
|
|
106
|
+
// The owner connection's observables outlive a failed open; a leaked observer would corrupt
|
|
107
|
+
// the next channel on the same connection. Detach on every exit path; assigned once registered.
|
|
108
|
+
let detachObservers: (() => void) | undefined;
|
|
99
109
|
|
|
100
110
|
try {
|
|
101
111
|
// 2. Discover services
|
|
102
|
-
const { services } = await
|
|
112
|
+
const { services } = await connection.sendCommand(BleProxyCommand.DiscoverServices, {
|
|
103
113
|
connection_handle,
|
|
104
114
|
});
|
|
105
115
|
|
|
@@ -109,7 +119,7 @@ export class ProxyBleCentralInterface implements Transport {
|
|
|
109
119
|
}
|
|
110
120
|
|
|
111
121
|
// 3. Discover characteristics
|
|
112
|
-
const { characteristics } = await
|
|
122
|
+
const { characteristics } = await connection.sendCommand(BleProxyCommand.DiscoverCharacteristics, {
|
|
113
123
|
connection_handle,
|
|
114
124
|
service_uuid: matterService.uuid,
|
|
115
125
|
});
|
|
@@ -136,34 +146,15 @@ export class ProxyBleCentralInterface implements Transport {
|
|
|
136
146
|
// 4. Read C3 if present and has additional data
|
|
137
147
|
if (c3Uuid && hasAdditionalAdvertisementData) {
|
|
138
148
|
logger.debug(`Reading additional commissioning data from C3`);
|
|
139
|
-
await
|
|
149
|
+
await connection.sendCommand(BleProxyCommand.ReadCharacteristic, {
|
|
140
150
|
connection_handle,
|
|
141
151
|
characteristic_uuid: c3Uuid,
|
|
142
152
|
});
|
|
143
|
-
// Additional commissioning data read but not used directly by the proxy -
|
|
144
|
-
// it's handled by matter.js commissioning flow internally
|
|
145
153
|
}
|
|
146
154
|
|
|
147
|
-
// 5.
|
|
148
|
-
//
|
|
149
|
-
//
|
|
150
|
-
// handshake indication immediately after Write Response can fire before the
|
|
151
|
-
// proxy client has enabled notifications, and the indication is lost.
|
|
152
|
-
const btpHandshakeRequest = BtpCodec.encodeBtpHandshakeRequest({
|
|
153
|
-
versions: MatterBle.BTP_SUPPORTED_VERSIONS,
|
|
154
|
-
attMtu: mtu,
|
|
155
|
-
clientWindowSize: MatterBle.BTP_MAXIMUM_WINDOW_SIZE,
|
|
156
|
-
});
|
|
157
|
-
logger.debug(`Sending BTP handshake request on C1 and subscribing C2 atomically`);
|
|
158
|
-
await this.#handler.sendCommand(BleProxyCommand.WriteAndSubscribe, {
|
|
159
|
-
connection_handle,
|
|
160
|
-
write_uuid: c1Uuid,
|
|
161
|
-
write_value: Buffer.from(btpHandshakeRequest as ArrayBuffer).toString("base64"),
|
|
162
|
-
write_response: true,
|
|
163
|
-
subscribe_uuid: c2Uuid,
|
|
164
|
-
});
|
|
165
|
-
|
|
166
|
-
// 6. Wait for BTP handshake response via binary frame
|
|
155
|
+
// 5. Register the handshake observer BEFORE sending: the C2 indication is a separate
|
|
156
|
+
// binary frame that can beat the WriteAndSubscribe response, and binaryFrameReceived
|
|
157
|
+
// drops frames emitted with no listener attached.
|
|
167
158
|
const {
|
|
168
159
|
promise: handshakePromise,
|
|
169
160
|
resolver: handshakeResolver,
|
|
@@ -191,34 +182,74 @@ export class ProxyBleCentralInterface implements Transport {
|
|
|
191
182
|
}
|
|
192
183
|
}
|
|
193
184
|
};
|
|
194
|
-
|
|
185
|
+
connection.binaryFrameReceived.on(handshakeObserver);
|
|
186
|
+
|
|
187
|
+
// 6. Write C1 and subscribe C2 atomically so the peripheral can't fire its indication
|
|
188
|
+
// before notifications are enabled (no round-trip between Write Response and CCCD enable).
|
|
189
|
+
const btpHandshakeRequest = BtpCodec.encodeBtpHandshakeRequest({
|
|
190
|
+
versions: MatterBle.BTP_SUPPORTED_VERSIONS,
|
|
191
|
+
attMtu: mtu,
|
|
192
|
+
clientWindowSize: MatterBle.BTP_MAXIMUM_WINDOW_SIZE,
|
|
193
|
+
});
|
|
194
|
+
logger.debug(`Sending BTP handshake request on C1 and subscribing C2 atomically`);
|
|
195
195
|
|
|
196
196
|
let handshakeResponse: Uint8Array;
|
|
197
197
|
try {
|
|
198
|
+
await connection.sendCommand(BleProxyCommand.WriteAndSubscribe, {
|
|
199
|
+
connection_handle,
|
|
200
|
+
write_uuid: c1Uuid,
|
|
201
|
+
write_value: Buffer.from(btpHandshakeRequest as ArrayBuffer).toString("base64"),
|
|
202
|
+
write_response: true,
|
|
203
|
+
subscribe_uuid: c2Uuid,
|
|
204
|
+
});
|
|
198
205
|
handshakeResponse = await handshakePromise;
|
|
199
206
|
} finally {
|
|
200
|
-
|
|
207
|
+
connection.binaryFrameReceived.off(handshakeObserver);
|
|
201
208
|
btpHandshakeTimeout.stop();
|
|
202
209
|
}
|
|
203
210
|
|
|
204
|
-
// 7.
|
|
205
|
-
//
|
|
211
|
+
// 7. Register the live observer BEFORE creating the session (same non-buffering frame
|
|
212
|
+
// race as step 5); frames seen before the session exists are buffered, then flushed below.
|
|
206
213
|
const onMatterMessageListener = this.#onMatterMessageListener;
|
|
207
214
|
const channelRef: { channel?: ProxyBleChannel } = {};
|
|
215
|
+
const sessionRef: { session?: BtpSessionHandler } = {};
|
|
216
|
+
const earlyFrames = new Array<Uint8Array>();
|
|
217
|
+
|
|
218
|
+
const forwardToBtp = (payload: Uint8Array) => {
|
|
219
|
+
sessionRef.session
|
|
220
|
+
?.handleIncomingBleData(payload)
|
|
221
|
+
.catch(error =>
|
|
222
|
+
logger.warn(`Peripheral ${peripheralAddress}: Error handling incoming BLE data`, error),
|
|
223
|
+
);
|
|
224
|
+
};
|
|
208
225
|
|
|
226
|
+
const binaryObserver = (frame: { connectionHandle: number; opcode: number; payload: Uint8Array }) => {
|
|
227
|
+
if (frame.connectionHandle === connection_handle && frame.opcode === BinaryFrameOpcode.Notification) {
|
|
228
|
+
const payload = new Uint8Array(frame.payload);
|
|
229
|
+
if (sessionRef.session) {
|
|
230
|
+
forwardToBtp(payload);
|
|
231
|
+
} else {
|
|
232
|
+
earlyFrames.push(payload);
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
};
|
|
236
|
+
connection.binaryFrameReceived.on(binaryObserver);
|
|
237
|
+
detachObservers = () => connection.binaryFrameReceived.off(binaryObserver);
|
|
238
|
+
|
|
239
|
+
// 8. Create the BTP session.
|
|
209
240
|
const btpSession = await BtpSessionHandler.createAsCentral(
|
|
210
241
|
handshakeResponse,
|
|
211
242
|
// Write callback: send binary frame to proxy client
|
|
212
243
|
async (data: Bytes) => {
|
|
213
244
|
const bytes = data instanceof Uint8Array ? data : new Uint8Array(data as ArrayBuffer);
|
|
214
|
-
|
|
245
|
+
connection.sendBinaryFrame(BinaryFrameOpcode.WriteData, connection_handle, bytes);
|
|
215
246
|
},
|
|
216
247
|
// Disconnect callback
|
|
217
248
|
async () => {
|
|
218
249
|
if (!channelRef.channel?.connected) return;
|
|
219
250
|
logger.debug(`Disconnecting from ${peripheralAddress} via proxy`);
|
|
220
251
|
try {
|
|
221
|
-
await
|
|
252
|
+
await connection.sendCommand(BleProxyCommand.Disconnect, { connection_handle });
|
|
222
253
|
} catch (error) {
|
|
223
254
|
logger.debug(
|
|
224
255
|
`Peripheral ${peripheralAddress}: Error sending Disconnect to proxy client`,
|
|
@@ -234,18 +265,12 @@ export class ProxyBleCentralInterface implements Transport {
|
|
|
234
265
|
}
|
|
235
266
|
},
|
|
236
267
|
);
|
|
268
|
+
sessionRef.session = btpSession;
|
|
237
269
|
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
.handleIncomingBleData(new Uint8Array(frame.payload))
|
|
243
|
-
.catch(error =>
|
|
244
|
-
logger.warn(`Peripheral ${peripheralAddress}: Error handling incoming BLE data`, error),
|
|
245
|
-
);
|
|
246
|
-
}
|
|
247
|
-
};
|
|
248
|
-
this.#handler.binaryFrameReceived.on(binaryObserver);
|
|
270
|
+
for (const payload of earlyFrames) {
|
|
271
|
+
forwardToBtp(payload);
|
|
272
|
+
}
|
|
273
|
+
earlyFrames.length = 0;
|
|
249
274
|
|
|
250
275
|
// 9. Handle unexpected disconnects from proxy client
|
|
251
276
|
const eventObserver = (event: string, data: Record<string, unknown>) => {
|
|
@@ -261,21 +286,32 @@ export class ProxyBleCentralInterface implements Transport {
|
|
|
261
286
|
.catch(error => logger.debug(`Peripheral ${peripheralAddress}: Error closing channel`, error));
|
|
262
287
|
}
|
|
263
288
|
};
|
|
264
|
-
|
|
289
|
+
connection.eventReceived.on(eventObserver);
|
|
290
|
+
|
|
291
|
+
// The Disconnected event covers one peripheral; this covers the whole owning client vanishing.
|
|
292
|
+
const ownerClosedObserver = () => {
|
|
293
|
+
logger.info(`Owning proxy client for ${peripheralAddress} disconnected`);
|
|
294
|
+
channelRef.channel?.markDisconnected();
|
|
295
|
+
channelRef.channel
|
|
296
|
+
?.close()
|
|
297
|
+
.catch(error => logger.debug(`Peripheral ${peripheralAddress}: Error closing channel`, error));
|
|
298
|
+
};
|
|
299
|
+
connection.closed.on(ownerClosedObserver);
|
|
265
300
|
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
301
|
+
detachObservers = () => {
|
|
302
|
+
connection.binaryFrameReceived.off(binaryObserver);
|
|
303
|
+
connection.eventReceived.off(eventObserver);
|
|
304
|
+
connection.closed.off(ownerClosedObserver);
|
|
270
305
|
};
|
|
271
306
|
|
|
272
|
-
const proxyChannel = new ProxyBleChannel(peripheralAddress, btpSession,
|
|
307
|
+
const proxyChannel = new ProxyBleChannel(peripheralAddress, btpSession, detachObservers);
|
|
273
308
|
channelRef.channel = proxyChannel;
|
|
274
309
|
return proxyChannel;
|
|
275
310
|
} catch (error) {
|
|
276
|
-
// Clean up on failure — best-effort tear-down
|
|
311
|
+
// Clean up on failure — detach observers and best-effort tear-down on the proxy side
|
|
312
|
+
detachObservers?.();
|
|
277
313
|
try {
|
|
278
|
-
await
|
|
314
|
+
await connection.sendCommand(BleProxyCommand.Disconnect, { connection_handle });
|
|
279
315
|
} catch (cleanupError) {
|
|
280
316
|
logger.debug(`Peripheral ${peripheralAddress}: Error during connect-failure cleanup`, cleanupError);
|
|
281
317
|
}
|
package/src/ProxyBleClient.ts
CHANGED
|
@@ -6,8 +6,9 @@
|
|
|
6
6
|
|
|
7
7
|
import { createPromise, Logger, Seconds, withTimeout } from "@matter/main";
|
|
8
8
|
import { BleError, MatterBle } from "@matter/main/protocol";
|
|
9
|
+
import type { BleProxyConnection } from "./BleProxyConnection.js";
|
|
9
10
|
import type { BleProxyHandler } from "./BleProxyHandler.js";
|
|
10
|
-
import {
|
|
11
|
+
import type { DeviceDiscoveredData } from "./BleProxyProtocol.js";
|
|
11
12
|
|
|
12
13
|
const logger = Logger.get("ProxyBleClient");
|
|
13
14
|
|
|
@@ -43,18 +44,19 @@ export class ProxyBleClient {
|
|
|
43
44
|
#deviceDiscoveredCallback: ((peripheral: ProxyPeripheral, matterServiceData: Uint8Array) => void) | undefined;
|
|
44
45
|
#isScanning = false;
|
|
45
46
|
|
|
46
|
-
readonly #
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
47
|
+
readonly #deviceDiscoveredObserver = (data: DeviceDiscoveredData, _connection: BleProxyConnection) => {
|
|
48
|
+
this.#handleDeviceDiscovered(data);
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
readonly #scanStoppedObserver = (reason: string) => {
|
|
52
|
+
logger.info(`Scan stopped by proxy client: ${reason}`);
|
|
53
|
+
this.#isScanning = false;
|
|
53
54
|
};
|
|
54
55
|
|
|
55
56
|
constructor(handler: BleProxyHandler) {
|
|
56
57
|
this.#handler = handler;
|
|
57
|
-
this.#handler.
|
|
58
|
+
this.#handler.deviceDiscovered.on(this.#deviceDiscoveredObserver);
|
|
59
|
+
this.#handler.scanStopped.on(this.#scanStoppedObserver);
|
|
58
60
|
}
|
|
59
61
|
|
|
60
62
|
setDiscoveryCallback(callback: (peripheral: ProxyPeripheral, matterServiceData: Uint8Array) => void): void {
|
|
@@ -88,31 +90,23 @@ export class ProxyBleClient {
|
|
|
88
90
|
logger.debug("Start BLE scanning via proxy ...");
|
|
89
91
|
// Matter discovery only needs one event per state change; opt out of the spec's
|
|
90
92
|
// default true so a 10 Hz peripheral advertise doesn't flood the WebSocket.
|
|
91
|
-
await this.#handler.
|
|
92
|
-
service_uuids: ["fff6"],
|
|
93
|
-
allow_duplicates: false,
|
|
94
|
-
});
|
|
93
|
+
await this.#handler.startScan({ service_uuids: ["fff6"], allow_duplicates: false });
|
|
95
94
|
this.#isScanning = true;
|
|
96
95
|
}
|
|
97
96
|
|
|
98
97
|
async stopScanning(): Promise<void> {
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
if (!this.#handler.connected) {
|
|
104
|
-
this.#isScanning = false;
|
|
105
|
-
return;
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
logger.debug("Stop BLE scanning via proxy ...");
|
|
109
|
-
await this.#handler.sendCommand(BleProxyCommand.StopScan);
|
|
98
|
+
// Clear hub scan intent unconditionally: a transient all-clients-disconnect can reset
|
|
99
|
+
// #isScanning while the hub still intends to scan, and skipping stopScan here would let
|
|
100
|
+
// a reconnecting client auto-resume a scan the caller already ended.
|
|
110
101
|
this.#isScanning = false;
|
|
102
|
+
logger.debug("Stop BLE scanning via proxy ...");
|
|
103
|
+
await this.#handler.stopScan();
|
|
111
104
|
}
|
|
112
105
|
|
|
113
106
|
close(): void {
|
|
114
107
|
this.#isScanning = false;
|
|
115
|
-
this.#handler.
|
|
108
|
+
this.#handler.deviceDiscovered.off(this.#deviceDiscoveredObserver);
|
|
109
|
+
this.#handler.scanStopped.off(this.#scanStoppedObserver);
|
|
116
110
|
}
|
|
117
111
|
|
|
118
112
|
#handleDeviceDiscovered(data: DeviceDiscoveredData): void {
|
|
@@ -150,7 +144,9 @@ export class ProxyBleClient {
|
|
|
150
144
|
return;
|
|
151
145
|
}
|
|
152
146
|
|
|
153
|
-
logger.
|
|
147
|
+
logger.info(
|
|
148
|
+
`Discovered commissionable device ${address} (${name ?? "unnamed"}) rssi=${rssi ?? "n/a"} via proxy`,
|
|
149
|
+
);
|
|
154
150
|
this.#discoveredPeripherals.set(address, { peripheral, matterServiceData });
|
|
155
151
|
this.#deviceDiscoveredCallback?.(peripheral, matterServiceData);
|
|
156
152
|
}
|
package/src/index.ts
CHANGED