@matter/protocol 0.17.2-alpha.0-20260609-97109a2d3 → 0.17.2
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/cjs/ble/BleConsts.d.ts +3 -12
- package/dist/cjs/ble/BleConsts.d.ts.map +1 -1
- package/dist/cjs/ble/BleConsts.js.map +1 -1
- package/dist/cjs/ble/BtpSessionHandler.d.ts +4 -18
- package/dist/cjs/ble/BtpSessionHandler.d.ts.map +1 -1
- package/dist/cjs/ble/BtpSessionHandler.js +60 -57
- package/dist/cjs/ble/BtpSessionHandler.js.map +1 -1
- package/dist/cjs/codec/BtpCodec.d.ts.map +1 -1
- package/dist/cjs/codec/BtpCodec.js +1 -5
- package/dist/cjs/codec/BtpCodec.js.map +1 -1
- package/dist/cjs/peer/PeerCommunicationError.d.ts +11 -0
- package/dist/cjs/peer/PeerCommunicationError.d.ts.map +1 -1
- package/dist/cjs/peer/PeerCommunicationError.js +3 -0
- package/dist/cjs/peer/PeerCommunicationError.js.map +1 -1
- package/dist/cjs/protocol/MessageExchange.d.ts.map +1 -1
- package/dist/cjs/protocol/MessageExchange.js +1 -1
- package/dist/cjs/protocol/MessageExchange.js.map +1 -1
- package/dist/esm/ble/BleConsts.d.ts +3 -12
- package/dist/esm/ble/BleConsts.d.ts.map +1 -1
- package/dist/esm/ble/BleConsts.js.map +1 -1
- package/dist/esm/ble/BtpSessionHandler.d.ts +4 -18
- package/dist/esm/ble/BtpSessionHandler.d.ts.map +1 -1
- package/dist/esm/ble/BtpSessionHandler.js +60 -57
- package/dist/esm/ble/BtpSessionHandler.js.map +1 -1
- package/dist/esm/codec/BtpCodec.d.ts.map +1 -1
- package/dist/esm/codec/BtpCodec.js +1 -5
- package/dist/esm/codec/BtpCodec.js.map +1 -1
- package/dist/esm/peer/PeerCommunicationError.d.ts +11 -0
- package/dist/esm/peer/PeerCommunicationError.d.ts.map +1 -1
- package/dist/esm/peer/PeerCommunicationError.js +3 -0
- package/dist/esm/peer/PeerCommunicationError.js.map +1 -1
- package/dist/esm/protocol/MessageExchange.d.ts.map +1 -1
- package/dist/esm/protocol/MessageExchange.js +6 -2
- package/dist/esm/protocol/MessageExchange.js.map +1 -1
- package/package.json +5 -5
- package/src/ble/BleConsts.ts +3 -12
- package/src/ble/BtpSessionHandler.ts +85 -69
- package/src/codec/BtpCodec.ts +1 -2
- package/src/peer/PeerCommunicationError.ts +11 -0
- package/src/protocol/MessageExchange.ts +6 -2
|
@@ -112,6 +112,9 @@ export class BtpSessionHandler {
|
|
|
112
112
|
|
|
113
113
|
await writeBleCallback(handshakeResponse);
|
|
114
114
|
|
|
115
|
+
// Start awaiting the ack for the just-sent handshake response only now that it is on the wire.
|
|
116
|
+
btpSession.ackReceiveTimer.start();
|
|
117
|
+
|
|
115
118
|
return btpSession;
|
|
116
119
|
}
|
|
117
120
|
|
|
@@ -163,10 +166,9 @@ export class BtpSessionHandler {
|
|
|
163
166
|
throw new BtpProtocolError(`Unsupported BTP version ${btpVersion}`);
|
|
164
167
|
}
|
|
165
168
|
if (role === "peripheral") {
|
|
166
|
-
//
|
|
167
|
-
// nothing to acknowledge until the first fragment arrives.
|
|
169
|
+
// The handshake request is unsequenced, so nothing is pending to acknowledge yet.
|
|
168
170
|
this.prevAckedSequenceNumber = this.prevIncomingSequenceNumber;
|
|
169
|
-
|
|
171
|
+
// ackReceiveTimer is started once the handshake response has actually been written (see factory).
|
|
170
172
|
} else {
|
|
171
173
|
this.sendAckTimer.start();
|
|
172
174
|
this.prevIncomingSequenceNumber = 0;
|
|
@@ -304,11 +306,9 @@ export class BtpSessionHandler {
|
|
|
304
306
|
await this.handleMatterMessagePayload(payloadToProcess);
|
|
305
307
|
}
|
|
306
308
|
|
|
307
|
-
// A received ack may have reopened the remote window
|
|
308
|
-
// previously held back; the ack pending for this packet piggybacks on them.
|
|
309
|
+
// A received ack may have reopened the remote window; flush held-back fragments (they piggyback the ack).
|
|
309
310
|
await this.processSendQueue();
|
|
310
|
-
|
|
311
|
-
// Otherwise, if nothing is queued to piggyback on and our receive window has run low, ack proactively.
|
|
311
|
+
// If nothing was queued to piggyback on, ack proactively when our receive window is low.
|
|
312
312
|
await this.sendImmediateAckIfWindowLow();
|
|
313
313
|
} catch (error) {
|
|
314
314
|
logger.warn(`Error while handling incoming BTP data:`, error);
|
|
@@ -396,10 +396,11 @@ export class BtpSessionHandler {
|
|
|
396
396
|
|
|
397
397
|
const segmentPayload = currentProcessedMessage.readByteArray(this.fragmentSize - btpHeaderLength);
|
|
398
398
|
|
|
399
|
+
const ackNumberToSend = hasAckNumber ? this.prevIncomingSequenceNumber : undefined;
|
|
399
400
|
const btpPacket = {
|
|
400
401
|
header: packetHeader,
|
|
401
402
|
payload: {
|
|
402
|
-
ackNumber:
|
|
403
|
+
ackNumber: ackNumberToSend,
|
|
403
404
|
sequenceNumber: this.getNextSequenceNumber(),
|
|
404
405
|
messageLength: packetHeader.isBeginningSegment ? remainingMessageLength : undefined, // remainingMessageLength if the fill length on beginning packet
|
|
405
406
|
segmentPayload,
|
|
@@ -410,22 +411,28 @@ export class BtpSessionHandler {
|
|
|
410
411
|
const packet = BtpCodec.encodeBtpPacket(btpPacket);
|
|
411
412
|
logger.debug(`Sending BTP packet raw: ${Bytes.toHex(packet)}`);
|
|
412
413
|
|
|
414
|
+
// Commit the ack before the await so a concurrent send can't observe the stale value and ack the same
|
|
415
|
+
// sequence twice (spec-compliant peers reject a duplicate ack).
|
|
416
|
+
const previousAckedSequenceNumber = this.prevAckedSequenceNumber;
|
|
417
|
+
if (ackNumberToSend !== undefined) {
|
|
418
|
+
this.prevAckedSequenceNumber = ackNumberToSend;
|
|
419
|
+
}
|
|
420
|
+
|
|
413
421
|
try {
|
|
414
422
|
await this.writeBleCallback(packet);
|
|
415
423
|
} catch (error) {
|
|
416
|
-
//
|
|
417
|
-
//
|
|
418
|
-
|
|
419
|
-
BleDisconnectedError.accept(error);
|
|
420
|
-
logger.debug("BTP packet send failed because BLE is disconnected", Diagnostic.errorMessage(error));
|
|
424
|
+
// Roll back before re-raising so every error type (not just an absorbed disconnect) leaves clean
|
|
425
|
+
// state; the queue is cleared to drop partially-consumed DataReaders.
|
|
426
|
+
this.prevAckedSequenceNumber = previousAckedSequenceNumber;
|
|
421
427
|
this.queuedOutgoingMatterMessages.length = 0;
|
|
422
428
|
this.sendInProgress = false;
|
|
429
|
+
BleDisconnectedError.accept(error);
|
|
430
|
+
logger.debug(
|
|
431
|
+
`BTP packet (seq ${btpPacket.payload.sequenceNumber}) send failed because BLE is disconnected`,
|
|
432
|
+
Diagnostic.errorMessage(error),
|
|
433
|
+
);
|
|
423
434
|
return;
|
|
424
435
|
}
|
|
425
|
-
// Update ACK bookkeeping only after the packet was sent successfully
|
|
426
|
-
if (hasAckNumber) {
|
|
427
|
-
this.prevAckedSequenceNumber = this.prevIncomingSequenceNumber;
|
|
428
|
-
}
|
|
429
436
|
|
|
430
437
|
if (!this.ackReceiveTimer.isRunning) {
|
|
431
438
|
this.ackReceiveTimer.start(); // starts the timer
|
|
@@ -444,6 +451,11 @@ export class BtpSessionHandler {
|
|
|
444
451
|
}
|
|
445
452
|
}
|
|
446
453
|
this.sendInProgress = false;
|
|
454
|
+
|
|
455
|
+
// A pending ack that could not be piggybacked (e.g. an incoming packet arrived mid-send) would otherwise
|
|
456
|
+
// be stranded until the send-ack timer, since the timer is one-shot and may have already fired while a
|
|
457
|
+
// send held the lock; flush it now that the send completed.
|
|
458
|
+
await this.sendStandaloneAck();
|
|
447
459
|
}
|
|
448
460
|
|
|
449
461
|
/**
|
|
@@ -475,11 +487,7 @@ export class BtpSessionHandler {
|
|
|
475
487
|
await this.sendStandaloneAck();
|
|
476
488
|
}
|
|
477
489
|
|
|
478
|
-
/**
|
|
479
|
-
* §4.19.4.8: send a pending acknowledgement immediately, ahead of the send-ack timer, once our receive window
|
|
480
|
-
* has shrunk to the immediate-ack threshold. Skipped when an outgoing message is queued, since that fragment
|
|
481
|
-
* will piggyback the acknowledgement.
|
|
482
|
-
*/
|
|
490
|
+
/** §4.19.4.8: ack proactively once our receive window runs low; skipped if queued data will piggyback it. */
|
|
483
491
|
private async sendImmediateAckIfWindowLow() {
|
|
484
492
|
if (this.queuedOutgoingMatterMessages.length > 0 || this.sendInProgress) return;
|
|
485
493
|
if (this.localWindowFreeSlots() > MatterBle.BTP_IMMEDIATE_ACK_WINDOW_THRESHOLD) return;
|
|
@@ -488,37 +496,59 @@ export class BtpSessionHandler {
|
|
|
488
496
|
|
|
489
497
|
/** Send a stand-alone acknowledgement packet if one is pending. */
|
|
490
498
|
private async sendStandaloneAck() {
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
isEndingSegment: false,
|
|
501
|
-
},
|
|
502
|
-
payload: {
|
|
503
|
-
ackNumber: this.prevIncomingSequenceNumber,
|
|
504
|
-
sequenceNumber: this.getNextSequenceNumber(),
|
|
505
|
-
},
|
|
506
|
-
};
|
|
507
|
-
const packet = BtpCodec.encodeBtpPacket(btpPacket);
|
|
499
|
+
// Mutually exclusive with processSendQueue (both directions): if a send is in progress it will piggyback
|
|
500
|
+
// the pending ack, and two concurrent writes could otherwise reach the wire out of order.
|
|
501
|
+
if (this.sendInProgress) return;
|
|
502
|
+
const ackNumberToSend = this.prevIncomingSequenceNumber;
|
|
503
|
+
if (ackNumberToSend === this.prevAckedSequenceNumber) return;
|
|
504
|
+
// §4.19.4.7: an ack still consumes a remote-window slot, so never send into a full window.
|
|
505
|
+
if (!this.canSend(true)) return;
|
|
506
|
+
|
|
507
|
+
this.sendInProgress = true;
|
|
508
508
|
try {
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
509
|
+
logger.debug(`Sending BTP ACK for sequence number ${ackNumberToSend}`);
|
|
510
|
+
const btpPacket = {
|
|
511
|
+
header: {
|
|
512
|
+
isHandshakeRequest: false,
|
|
513
|
+
hasManagementOpcode: false,
|
|
514
|
+
hasAckNumber: true,
|
|
515
|
+
isBeginningSegment: false,
|
|
516
|
+
isContinuingSegment: false,
|
|
517
|
+
isEndingSegment: false,
|
|
518
|
+
},
|
|
519
|
+
payload: {
|
|
520
|
+
ackNumber: ackNumberToSend,
|
|
521
|
+
sequenceNumber: this.getNextSequenceNumber(),
|
|
522
|
+
},
|
|
523
|
+
};
|
|
524
|
+
const packet = BtpCodec.encodeBtpPacket(btpPacket);
|
|
525
|
+
|
|
526
|
+
// Commit the ack before the await so an interleaved send can't re-ack the same sequence
|
|
527
|
+
// (spec-compliant peers reject a duplicate ack).
|
|
528
|
+
const previousAckedSequenceNumber = this.prevAckedSequenceNumber;
|
|
529
|
+
this.prevAckedSequenceNumber = ackNumberToSend;
|
|
530
|
+
try {
|
|
531
|
+
await this.writeBleCallback(packet);
|
|
532
|
+
} catch (error) {
|
|
533
|
+
// Roll back before re-raising so a failed write never leaves the ack marked as sent.
|
|
534
|
+
this.prevAckedSequenceNumber = previousAckedSequenceNumber;
|
|
535
|
+
BleDisconnectedError.accept(error);
|
|
536
|
+
logger.debug(
|
|
537
|
+
`BTP ACK (seq ${btpPacket.payload.sequenceNumber}, ack ${ackNumberToSend}) send failed because BLE is disconnected`,
|
|
538
|
+
Diagnostic.errorMessage(error),
|
|
539
|
+
);
|
|
540
|
+
return;
|
|
541
|
+
}
|
|
542
|
+
this.sendAckTimer.stop();
|
|
543
|
+
if (!this.ackReceiveTimer.isRunning) {
|
|
544
|
+
this.ackReceiveTimer.start(); // starts the timer
|
|
545
|
+
}
|
|
546
|
+
} finally {
|
|
547
|
+
this.sendInProgress = false;
|
|
521
548
|
}
|
|
549
|
+
|
|
550
|
+
// Flush any Matter message that was queued while the ack held the send lock.
|
|
551
|
+
await this.processSendQueue();
|
|
522
552
|
}
|
|
523
553
|
|
|
524
554
|
/**
|
|
@@ -543,38 +573,24 @@ export class BtpSessionHandler {
|
|
|
543
573
|
return this.sequenceNumber;
|
|
544
574
|
}
|
|
545
575
|
|
|
546
|
-
/**
|
|
547
|
-
* Wrap-safe forward distance between two sequence numbers (modulo 256). The -1 "nothing yet" sentinels map
|
|
548
|
-
* to 255, so callers must keep those sentinels distinct from a real sequence number 255 (true today because
|
|
549
|
-
* sequence numbers start at 0 and a session never has 256 outstanding packets).
|
|
550
|
-
*/
|
|
576
|
+
/** Wrap-safe forward distance between two sequence numbers (modulo 256); the -1 sentinels read as 255. */
|
|
551
577
|
private modularDistance(from: number, to: number): number {
|
|
552
578
|
return (((to - from) % 256) + 256) % 256;
|
|
553
579
|
}
|
|
554
580
|
|
|
555
|
-
/**
|
|
556
|
-
* Free slots remaining in the remote peer's receive window: the negotiated window size minus the number of
|
|
557
|
-
* sent-but-unacknowledged packets. Gates our sending (§4.19.4.7). Mirrors CHIP's mRemoteReceiveWindowSize.
|
|
558
|
-
*/
|
|
581
|
+
/** Free slots in the remote peer's receive window; gates our sending (§4.19.4.7). */
|
|
559
582
|
private remoteWindowFreeSlots(): number {
|
|
560
583
|
return this.clientWindowSize - this.modularDistance(this.prevIncomingAckNumber, this.sequenceNumber);
|
|
561
584
|
}
|
|
562
585
|
|
|
563
|
-
/**
|
|
564
|
-
* Free slots remaining in our own receive window: the negotiated window size minus the number of
|
|
565
|
-
* received-but-unacknowledged packets. Drives proactive acknowledgement (§4.19.4.8). Mirrors CHIP's
|
|
566
|
-
* mLocalReceiveWindowSize.
|
|
567
|
-
*/
|
|
586
|
+
/** Free slots in our own receive window; drives proactive acknowledgement (§4.19.4.8). */
|
|
568
587
|
private localWindowFreeSlots(): number {
|
|
569
588
|
return (
|
|
570
589
|
this.clientWindowSize - this.modularDistance(this.prevAckedSequenceNumber, this.prevIncomingSequenceNumber)
|
|
571
590
|
);
|
|
572
591
|
}
|
|
573
592
|
|
|
574
|
-
/**
|
|
575
|
-
* Whether a packet may be sent now without violating the remote receive window rules. A packet carrying an
|
|
576
|
-
* acknowledgement may use the last open slot; a data-only packet must leave it free (§4.19.4.7).
|
|
577
|
-
*/
|
|
593
|
+
/** §4.19.4.7: a data-only packet must leave the last window slot free; an ack-bearing packet may use it. */
|
|
578
594
|
private canSend(carriesAck: boolean): boolean {
|
|
579
595
|
const free = this.remoteWindowFreeSlots();
|
|
580
596
|
if (free <= 0) return false;
|
package/src/codec/BtpCodec.ts
CHANGED
|
@@ -266,9 +266,8 @@ export class BtpCodec {
|
|
|
266
266
|
throw new BtpProtocolError("Please use the specific methods to encode a Handshake packet");
|
|
267
267
|
}
|
|
268
268
|
|
|
269
|
+
// The handshake and management-opcode bits are rejected above, so only the segment/ack bits remain.
|
|
269
270
|
const header =
|
|
270
|
-
// (isHandshakeRequest ? BtpHeaderBits.HandshakeBit : 0) | ... but always false here
|
|
271
|
-
// (hasManagementOpcode ? BtpHeaderBits.ManagementMsg : 0) | ... but alw<ys false here
|
|
272
271
|
(hasAckNumber ? BtpHeaderBits.AckMsg : 0) |
|
|
273
272
|
(isEndingSegment ? BtpHeaderBits.EndSegment : 0) |
|
|
274
273
|
(isContinuingSegment ? BtpHeaderBits.ContinuingSegment : 0) |
|
|
@@ -45,6 +45,17 @@ export class PeerUnresponsiveError extends TransientPeerCommunicationError {
|
|
|
45
45
|
}
|
|
46
46
|
}
|
|
47
47
|
|
|
48
|
+
/**
|
|
49
|
+
* Thrown when an expected message did not arrive on an active exchange before the timeout.
|
|
50
|
+
*
|
|
51
|
+
* Subtype of {@link PeerUnresponsiveError}, distinguished from the bare error which is raised when our own outbound
|
|
52
|
+
* message was never acknowledged (the peer may never have received it). This subtype means the exchange was live but
|
|
53
|
+
* the awaited message — a command response or a subscription/server report — never came. It does not by itself imply
|
|
54
|
+
* our request was delivered; a caller that reads strictly after a successful (acked) send may additionally infer
|
|
55
|
+
* delivery from that ordering.
|
|
56
|
+
*/
|
|
57
|
+
export class PeerMessageMissingError extends PeerUnresponsiveError {}
|
|
58
|
+
|
|
48
59
|
/**
|
|
49
60
|
* Thrown when a session is closed due to peer shutdown.
|
|
50
61
|
*/
|
|
@@ -7,7 +7,11 @@
|
|
|
7
7
|
import { Message, PacketHeader, SessionType } from "#codec/MessageCodec.js";
|
|
8
8
|
import { Mark } from "#common/Mark.js";
|
|
9
9
|
import { NetworkProfile } from "#peer/NetworkProfile.js";
|
|
10
|
-
import {
|
|
10
|
+
import {
|
|
11
|
+
PeerMessageMissingError,
|
|
12
|
+
PeerUnresponsiveError,
|
|
13
|
+
TransientPeerCommunicationError,
|
|
14
|
+
} from "#peer/PeerCommunicationError.js";
|
|
11
15
|
import { GroupSession } from "#session/GroupSession.js";
|
|
12
16
|
import type { NodeSession } from "#session/NodeSession.js";
|
|
13
17
|
import { Session } from "#session/Session.js";
|
|
@@ -696,7 +700,7 @@ export class MessageExchange {
|
|
|
696
700
|
abort: options?.abort,
|
|
697
701
|
|
|
698
702
|
timeoutHandler: () => {
|
|
699
|
-
throw new
|
|
703
|
+
throw new PeerMessageMissingError(timeout!);
|
|
700
704
|
},
|
|
701
705
|
});
|
|
702
706
|
|