@matter/protocol 0.12.0-alpha.0-20250108-7ae2a767d → 0.12.0-alpha.0-20250110-6349da2f0
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/interaction/ServerSubscription.d.ts.map +1 -1
- package/dist/cjs/interaction/ServerSubscription.js +7 -3
- package/dist/cjs/interaction/ServerSubscription.js.map +1 -1
- package/dist/cjs/mdns/MdnsBroadcaster.d.ts +3 -3
- package/dist/cjs/mdns/MdnsBroadcaster.d.ts.map +1 -1
- package/dist/cjs/mdns/MdnsBroadcaster.js +55 -31
- package/dist/cjs/mdns/MdnsBroadcaster.js.map +1 -1
- package/dist/cjs/mdns/MdnsScanner.d.ts.map +1 -1
- package/dist/cjs/mdns/MdnsScanner.js +226 -28
- package/dist/cjs/mdns/MdnsScanner.js.map +1 -1
- package/dist/cjs/mdns/MdnsServer.d.ts +5 -1
- package/dist/cjs/mdns/MdnsServer.d.ts.map +1 -1
- package/dist/cjs/mdns/MdnsServer.js +12 -7
- package/dist/cjs/mdns/MdnsServer.js.map +1 -1
- package/dist/cjs/peer/ControllerCommissioningFlow.d.ts.map +1 -1
- package/dist/cjs/peer/ControllerCommissioningFlow.js +35 -2
- package/dist/cjs/peer/ControllerCommissioningFlow.js.map +1 -1
- package/dist/cjs/protocol/DeviceAdvertiser.d.ts.map +1 -1
- package/dist/cjs/protocol/DeviceAdvertiser.js +6 -1
- package/dist/cjs/protocol/DeviceAdvertiser.js.map +1 -1
- package/dist/cjs/protocol/DeviceCommissioner.d.ts.map +1 -1
- package/dist/cjs/protocol/DeviceCommissioner.js +5 -1
- package/dist/cjs/protocol/DeviceCommissioner.js.map +1 -1
- package/dist/esm/interaction/ServerSubscription.d.ts.map +1 -1
- package/dist/esm/interaction/ServerSubscription.js +7 -3
- package/dist/esm/interaction/ServerSubscription.js.map +1 -1
- package/dist/esm/mdns/MdnsBroadcaster.d.ts +3 -3
- package/dist/esm/mdns/MdnsBroadcaster.d.ts.map +1 -1
- package/dist/esm/mdns/MdnsBroadcaster.js +56 -31
- package/dist/esm/mdns/MdnsBroadcaster.js.map +1 -1
- package/dist/esm/mdns/MdnsScanner.d.ts.map +1 -1
- package/dist/esm/mdns/MdnsScanner.js +226 -28
- package/dist/esm/mdns/MdnsScanner.js.map +1 -1
- package/dist/esm/mdns/MdnsServer.d.ts +5 -1
- package/dist/esm/mdns/MdnsServer.d.ts.map +1 -1
- package/dist/esm/mdns/MdnsServer.js +12 -7
- package/dist/esm/mdns/MdnsServer.js.map +1 -1
- package/dist/esm/peer/ControllerCommissioningFlow.d.ts.map +1 -1
- package/dist/esm/peer/ControllerCommissioningFlow.js +36 -3
- package/dist/esm/peer/ControllerCommissioningFlow.js.map +1 -1
- package/dist/esm/protocol/DeviceAdvertiser.d.ts.map +1 -1
- package/dist/esm/protocol/DeviceAdvertiser.js +6 -1
- package/dist/esm/protocol/DeviceAdvertiser.js.map +1 -1
- package/dist/esm/protocol/DeviceCommissioner.d.ts.map +1 -1
- package/dist/esm/protocol/DeviceCommissioner.js +5 -1
- package/dist/esm/protocol/DeviceCommissioner.js.map +1 -1
- package/package.json +6 -6
- package/src/interaction/ServerSubscription.ts +7 -3
- package/src/mdns/MdnsBroadcaster.ts +63 -35
- package/src/mdns/MdnsScanner.ts +264 -43
- package/src/mdns/MdnsServer.ts +20 -7
- package/src/peer/ControllerCommissioningFlow.ts +48 -3
- package/src/protocol/DeviceAdvertiser.ts +7 -1
- package/src/protocol/DeviceCommissioner.ts +11 -2
package/src/mdns/MdnsScanner.ts
CHANGED
|
@@ -56,6 +56,7 @@ const MDNS_EXPIRY_GRACE_PERIOD_FACTOR = 1.05;
|
|
|
56
56
|
|
|
57
57
|
type MatterServerRecordWithExpire = ServerAddressIp & Lifespan;
|
|
58
58
|
|
|
59
|
+
/** Type for commissionable Device records including Lifespan details. */
|
|
59
60
|
type CommissionableDeviceRecordWithExpire = Omit<CommissionableDevice, "addresses"> &
|
|
60
61
|
Lifespan & {
|
|
61
62
|
addresses: Map<string, MatterServerRecordWithExpire>; // Override addresses type to include expiration
|
|
@@ -65,17 +66,27 @@ type CommissionableDeviceRecordWithExpire = Omit<CommissionableDevice, "addresse
|
|
|
65
66
|
P?: number; // Additional Field for Product ID
|
|
66
67
|
};
|
|
67
68
|
|
|
69
|
+
/** Type for operational Device records including Lifespan details. */
|
|
68
70
|
type OperationalDeviceRecordWithExpire = Omit<OperationalDevice, "addresses"> &
|
|
69
71
|
Lifespan & {
|
|
70
72
|
addresses: Map<string, MatterServerRecordWithExpire>; // Override addresses type to include expiration
|
|
71
73
|
};
|
|
72
74
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
75
|
+
/** Type for any DNS record with Lifespan (discoveredAt and ttl) details. */
|
|
76
|
+
type AnyDnsRecordWithExpiry = DnsRecord<any> & Lifespan;
|
|
77
|
+
|
|
78
|
+
/** Type for DNS answers with Address details structured for better direct access performance. */
|
|
79
|
+
type StructuredDnsAddressAnswers = {
|
|
80
|
+
addressesV4?: Record<string, Map<string, AnyDnsRecordWithExpiry>>; // IPv4 Address record by name and value (IP)
|
|
81
|
+
addressesV6?: Record<string, Map<string, AnyDnsRecordWithExpiry>>; // IPv6 Address record by name and value (IP)
|
|
77
82
|
};
|
|
78
83
|
|
|
84
|
+
/** Type for DNS answers with Lifespan details structured for better direct access performance. */
|
|
85
|
+
type StructuredDnsAnswers = {
|
|
86
|
+
operational?: Record<number, AnyDnsRecordWithExpiry[]>; // Operational Matter device records by recordType
|
|
87
|
+
commissionable?: Record<number, AnyDnsRecordWithExpiry[]>; // Commissionable Matter device records by recordType
|
|
88
|
+
} & StructuredDnsAddressAnswers;
|
|
89
|
+
|
|
79
90
|
/** The initial number of seconds between two announcements. MDNS specs require 1-2 seconds, so lets use the middle. */
|
|
80
91
|
const START_ANNOUNCE_INTERVAL_SECONDS = 1.5;
|
|
81
92
|
|
|
@@ -102,12 +113,19 @@ export class MdnsScanner implements Scanner {
|
|
|
102
113
|
);
|
|
103
114
|
}
|
|
104
115
|
|
|
116
|
+
/** Active announces by queryId with queries and known answers */
|
|
105
117
|
readonly #activeAnnounceQueries = new Map<string, { queries: DnsQuery[]; answers: StructuredDnsAnswers }>();
|
|
106
|
-
#queryTimer?: Timer;
|
|
107
|
-
#nextAnnounceIntervalSeconds = START_ANNOUNCE_INTERVAL_SECONDS;
|
|
108
118
|
|
|
119
|
+
/** Known IP addresses by network interface */
|
|
120
|
+
readonly #discoveredIpRecords = new Map<string, StructuredDnsAddressAnswers>();
|
|
121
|
+
|
|
122
|
+
/** Known operational device records by Matter Qname */
|
|
109
123
|
readonly #operationalDeviceRecords = new Map<string, OperationalDeviceRecordWithExpire>();
|
|
124
|
+
|
|
125
|
+
/** Known commissionable device records by queryId */
|
|
110
126
|
readonly #commissionableDeviceRecords = new Map<string, CommissionableDeviceRecordWithExpire>();
|
|
127
|
+
|
|
128
|
+
/** Waiters for specific queryIds to resolve a promise when a record is discovered */
|
|
111
129
|
readonly #recordWaiters = new Map<
|
|
112
130
|
string,
|
|
113
131
|
{
|
|
@@ -117,9 +135,11 @@ export class MdnsScanner implements Scanner {
|
|
|
117
135
|
cancelResolver?: (value: void) => void;
|
|
118
136
|
}
|
|
119
137
|
>();
|
|
138
|
+
|
|
139
|
+
#queryTimer?: Timer;
|
|
140
|
+
#nextAnnounceIntervalSeconds = START_ANNOUNCE_INTERVAL_SECONDS;
|
|
120
141
|
readonly #periodicTimer: Timer;
|
|
121
142
|
#closing = false;
|
|
122
|
-
|
|
123
143
|
readonly #multicastServer: UdpMulticastServer;
|
|
124
144
|
readonly #enableIpv4?: boolean;
|
|
125
145
|
|
|
@@ -149,7 +169,9 @@ export class MdnsScanner implements Scanner {
|
|
|
149
169
|
const allQueries = Array.from(this.#activeAnnounceQueries.values());
|
|
150
170
|
const queries = allQueries.flatMap(({ queries }) => queries);
|
|
151
171
|
const answers = allQueries.flatMap(({ answers }) =>
|
|
152
|
-
Object.values(answers).flatMap(answer =>
|
|
172
|
+
Object.values(answers).flatMap(answer =>
|
|
173
|
+
Object.values(answer).flatMap(records => (Array.isArray(records) ? records : records.values())),
|
|
174
|
+
),
|
|
153
175
|
);
|
|
154
176
|
|
|
155
177
|
this.#queryTimer = Time.getTimer("MDNS discovery", this.#nextAnnounceIntervalSeconds * 1000, () =>
|
|
@@ -249,9 +271,14 @@ export class MdnsScanner implements Scanner {
|
|
|
249
271
|
this.#queryTimer = Time.getTimer("MDNS discovery", 0, () => this.#sendQueries()).start();
|
|
250
272
|
}
|
|
251
273
|
|
|
252
|
-
|
|
274
|
+
/**
|
|
275
|
+
* Combines the known answers from all active queries and the known IP addresses from the network
|
|
276
|
+
* interface into one data package
|
|
277
|
+
*/
|
|
278
|
+
#getActiveQueryEarlierAnswers(netInterface: string) {
|
|
253
279
|
return this.#combineStructuredAnswers(
|
|
254
280
|
...[...this.#activeAnnounceQueries.values()].map(({ answers }) => answers),
|
|
281
|
+
this.#discoveredIpRecords.get(netInterface) ?? {},
|
|
255
282
|
);
|
|
256
283
|
}
|
|
257
284
|
|
|
@@ -714,21 +741,38 @@ export class MdnsScanner implements Scanner {
|
|
|
714
741
|
#structureAnswers(...answersList: DnsRecord<any>[][]): StructuredDnsAnswers {
|
|
715
742
|
const structuredAnswers: StructuredDnsAnswers = {};
|
|
716
743
|
|
|
744
|
+
const discoveredAt = Time.nowMs();
|
|
717
745
|
answersList.forEach(answers =>
|
|
718
746
|
answers.forEach(answer => {
|
|
719
747
|
const { name, recordType } = answer;
|
|
720
748
|
if (name.endsWith(MATTER_SERVICE_QNAME)) {
|
|
721
749
|
structuredAnswers.operational = structuredAnswers.operational ?? {};
|
|
722
750
|
structuredAnswers.operational[recordType] = structuredAnswers.operational[recordType] ?? [];
|
|
723
|
-
structuredAnswers.operational[recordType].push(
|
|
751
|
+
structuredAnswers.operational[recordType].push({
|
|
752
|
+
discoveredAt,
|
|
753
|
+
...answer,
|
|
754
|
+
});
|
|
724
755
|
} else if (name.endsWith(MATTER_COMMISSION_SERVICE_QNAME)) {
|
|
725
756
|
structuredAnswers.commissionable = structuredAnswers.commissionable ?? {};
|
|
726
757
|
structuredAnswers.commissionable[recordType] = structuredAnswers.commissionable[recordType] ?? [];
|
|
727
|
-
structuredAnswers.commissionable[recordType].push(
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
758
|
+
structuredAnswers.commissionable[recordType].push({
|
|
759
|
+
discoveredAt,
|
|
760
|
+
...answer,
|
|
761
|
+
});
|
|
762
|
+
} else if (recordType === DnsRecordType.AAAA) {
|
|
763
|
+
structuredAnswers.addressesV6 = structuredAnswers.addressesV6 ?? {};
|
|
764
|
+
structuredAnswers.addressesV6[name] = structuredAnswers.addressesV6[name] ?? new Map();
|
|
765
|
+
structuredAnswers.addressesV6[name].set(answer.value, {
|
|
766
|
+
discoveredAt,
|
|
767
|
+
...answer,
|
|
768
|
+
});
|
|
769
|
+
} else if (this.#enableIpv4 && recordType === DnsRecordType.A) {
|
|
770
|
+
structuredAnswers.addressesV4 = structuredAnswers.addressesV4 ?? {};
|
|
771
|
+
structuredAnswers.addressesV4[name] = structuredAnswers.addressesV4[name] ?? new Map();
|
|
772
|
+
structuredAnswers.addressesV4[name].set(answer.value, {
|
|
773
|
+
discoveredAt,
|
|
774
|
+
...answer,
|
|
775
|
+
});
|
|
732
776
|
}
|
|
733
777
|
}),
|
|
734
778
|
);
|
|
@@ -737,10 +781,12 @@ export class MdnsScanner implements Scanner {
|
|
|
737
781
|
}
|
|
738
782
|
|
|
739
783
|
#combineStructuredAnswers(...answersList: StructuredDnsAnswers[]): StructuredDnsAnswers {
|
|
784
|
+
// Special type for easier combination of answers
|
|
740
785
|
const combinedAnswers: {
|
|
741
|
-
operational?: Record<number, Map<string,
|
|
742
|
-
commissionable?: Record<number, Map<string,
|
|
743
|
-
|
|
786
|
+
operational?: Record<number, Map<string, AnyDnsRecordWithExpiry>>;
|
|
787
|
+
commissionable?: Record<number, Map<string, AnyDnsRecordWithExpiry>>;
|
|
788
|
+
addressesV4?: Record<string, Map<string, AnyDnsRecordWithExpiry>>;
|
|
789
|
+
addressesV6?: Record<string, Map<string, AnyDnsRecordWithExpiry>>;
|
|
744
790
|
} = {};
|
|
745
791
|
|
|
746
792
|
for (const answers of answersList) {
|
|
@@ -748,11 +794,18 @@ export class MdnsScanner implements Scanner {
|
|
|
748
794
|
combinedAnswers.operational = combinedAnswers.operational ?? {};
|
|
749
795
|
for (const [recordType, records] of Object.entries(answers.operational) as unknown as [
|
|
750
796
|
number,
|
|
751
|
-
|
|
797
|
+
AnyDnsRecordWithExpiry[],
|
|
752
798
|
][]) {
|
|
753
799
|
combinedAnswers.operational[recordType] = combinedAnswers.operational[recordType] ?? new Map();
|
|
754
800
|
records.forEach(record => {
|
|
755
|
-
combinedAnswers.operational![recordType].
|
|
801
|
+
const existingRecord = combinedAnswers.operational![recordType].get(record.name);
|
|
802
|
+
if (existingRecord && existingRecord.discoveredAt < record.discoveredAt) {
|
|
803
|
+
if (record.ttl === 0) {
|
|
804
|
+
combinedAnswers.operational![recordType].delete(record.name);
|
|
805
|
+
} else {
|
|
806
|
+
combinedAnswers.operational![recordType].set(record.name, record);
|
|
807
|
+
}
|
|
808
|
+
}
|
|
756
809
|
});
|
|
757
810
|
}
|
|
758
811
|
}
|
|
@@ -760,24 +813,58 @@ export class MdnsScanner implements Scanner {
|
|
|
760
813
|
combinedAnswers.commissionable = combinedAnswers.commissionable ?? {};
|
|
761
814
|
for (const [recordType, records] of Object.entries(answers.commissionable) as unknown as [
|
|
762
815
|
number,
|
|
763
|
-
|
|
816
|
+
AnyDnsRecordWithExpiry[],
|
|
764
817
|
][]) {
|
|
765
818
|
combinedAnswers.commissionable[recordType] =
|
|
766
819
|
combinedAnswers.commissionable[recordType] ?? new Map();
|
|
767
820
|
records.forEach(record => {
|
|
768
|
-
combinedAnswers.commissionable![recordType].
|
|
821
|
+
const existingRecord = combinedAnswers.commissionable![recordType].get(record.name);
|
|
822
|
+
if (existingRecord && existingRecord.discoveredAt < record.discoveredAt) {
|
|
823
|
+
if (record.ttl === 0) {
|
|
824
|
+
combinedAnswers.commissionable![recordType].delete(record.name);
|
|
825
|
+
} else {
|
|
826
|
+
combinedAnswers.commissionable![recordType].set(record.name, record);
|
|
827
|
+
}
|
|
828
|
+
}
|
|
769
829
|
});
|
|
770
830
|
}
|
|
771
831
|
}
|
|
772
|
-
if (answers.
|
|
773
|
-
combinedAnswers.
|
|
774
|
-
for (const [name, records] of Object.entries(answers.
|
|
832
|
+
if (answers.addressesV6) {
|
|
833
|
+
combinedAnswers.addressesV6 = combinedAnswers.addressesV6 ?? {};
|
|
834
|
+
for (const [name, records] of Object.entries(answers.addressesV6) as unknown as [
|
|
775
835
|
string,
|
|
776
|
-
|
|
836
|
+
Map<string, AnyDnsRecordWithExpiry>,
|
|
777
837
|
][]) {
|
|
778
|
-
combinedAnswers.
|
|
779
|
-
|
|
780
|
-
|
|
838
|
+
combinedAnswers.addressesV6[name] = combinedAnswers.addressesV6[name] ?? new Map();
|
|
839
|
+
Object.values(records).forEach(record => {
|
|
840
|
+
const existingRecord = combinedAnswers.addressesV6![name].get(record.value);
|
|
841
|
+
if (existingRecord && existingRecord.discoveredAt < record.discoveredAt) {
|
|
842
|
+
if (record.ttl === 0) {
|
|
843
|
+
combinedAnswers.addressesV6![name].delete(name);
|
|
844
|
+
} else {
|
|
845
|
+
combinedAnswers.addressesV6![name].set(name, record);
|
|
846
|
+
}
|
|
847
|
+
}
|
|
848
|
+
});
|
|
849
|
+
}
|
|
850
|
+
}
|
|
851
|
+
if (this.#enableIpv4 && answers.addressesV4) {
|
|
852
|
+
combinedAnswers.addressesV4 = combinedAnswers.addressesV4 ?? {};
|
|
853
|
+
for (const [name, records] of Object.entries(answers.addressesV4) as unknown as [
|
|
854
|
+
string,
|
|
855
|
+
Map<string, AnyDnsRecordWithExpiry>,
|
|
856
|
+
][]) {
|
|
857
|
+
combinedAnswers.addressesV4[name] = combinedAnswers.addressesV4[name] ?? new Map();
|
|
858
|
+
Object.values(records).forEach(record => {
|
|
859
|
+
const existingRecord = combinedAnswers.addressesV4![name].get(record.value);
|
|
860
|
+
if (existingRecord && existingRecord.discoveredAt < record.discoveredAt) {
|
|
861
|
+
if (record.ttl === 0) {
|
|
862
|
+
combinedAnswers.addressesV4![name].delete(name);
|
|
863
|
+
} else {
|
|
864
|
+
combinedAnswers.addressesV4![name].set(name, record);
|
|
865
|
+
}
|
|
866
|
+
}
|
|
867
|
+
});
|
|
781
868
|
}
|
|
782
869
|
}
|
|
783
870
|
}
|
|
@@ -799,8 +886,11 @@ export class MdnsScanner implements Scanner {
|
|
|
799
886
|
]),
|
|
800
887
|
);
|
|
801
888
|
}
|
|
802
|
-
if (combinedAnswers.
|
|
803
|
-
result.
|
|
889
|
+
if (combinedAnswers.addressesV6) {
|
|
890
|
+
result.addressesV6 = combinedAnswers.addressesV6;
|
|
891
|
+
}
|
|
892
|
+
if (this.#enableIpv4 && combinedAnswers.addressesV4) {
|
|
893
|
+
result.addressesV4 = combinedAnswers.addressesV4;
|
|
804
894
|
}
|
|
805
895
|
|
|
806
896
|
return result;
|
|
@@ -819,12 +909,77 @@ export class MdnsScanner implements Scanner {
|
|
|
819
909
|
|
|
820
910
|
const answers = this.#structureAnswers([...message.answers, ...message.additionalRecords]);
|
|
821
911
|
|
|
822
|
-
const formerAnswers = this.#getActiveQueryEarlierAnswers();
|
|
912
|
+
const formerAnswers = this.#getActiveQueryEarlierAnswers(netInterface);
|
|
823
913
|
// Check if we got operational discovery records and handle them
|
|
824
914
|
this.#handleOperationalRecords(answers, formerAnswers, netInterface);
|
|
825
915
|
|
|
826
916
|
// Else check if we got commissionable discovery records and handle them
|
|
827
917
|
this.#handleCommissionableRecords(answers, formerAnswers, netInterface);
|
|
918
|
+
|
|
919
|
+
this.#updateIpRecords(answers, netInterface);
|
|
920
|
+
}
|
|
921
|
+
|
|
922
|
+
/**
|
|
923
|
+
* Update the discovered matter relevant IP records with the new data from the DNS message.
|
|
924
|
+
*/
|
|
925
|
+
#updateIpRecords(answers: StructuredDnsAnswers, netInterface: string) {
|
|
926
|
+
const interfaceRecords = this.#discoveredIpRecords.get(netInterface);
|
|
927
|
+
if (interfaceRecords === undefined) {
|
|
928
|
+
return;
|
|
929
|
+
}
|
|
930
|
+
let updated = false;
|
|
931
|
+
if (answers.addressesV6) {
|
|
932
|
+
for (const [target, ipAddresses] of Object.entries(answers.addressesV6)) {
|
|
933
|
+
if (interfaceRecords.addressesV6?.[target] !== undefined) {
|
|
934
|
+
for (const [ip, record] of Object.entries(ipAddresses)) {
|
|
935
|
+
if (record.ttl === 0) {
|
|
936
|
+
interfaceRecords.addressesV6[target].delete(ip);
|
|
937
|
+
} else {
|
|
938
|
+
interfaceRecords.addressesV6[target].set(ip, record);
|
|
939
|
+
}
|
|
940
|
+
updated = true;
|
|
941
|
+
}
|
|
942
|
+
}
|
|
943
|
+
}
|
|
944
|
+
}
|
|
945
|
+
if (this.#enableIpv4 && answers.addressesV4) {
|
|
946
|
+
for (const [target, ipAddresses] of Object.entries(answers.addressesV4)) {
|
|
947
|
+
if (interfaceRecords.addressesV4?.[target] !== undefined) {
|
|
948
|
+
for (const [ip, record] of Object.entries(ipAddresses)) {
|
|
949
|
+
if (record.ttl === 0) {
|
|
950
|
+
interfaceRecords.addressesV4[target].delete(ip);
|
|
951
|
+
} else {
|
|
952
|
+
interfaceRecords.addressesV4[target].set(ip, record);
|
|
953
|
+
}
|
|
954
|
+
updated = true;
|
|
955
|
+
}
|
|
956
|
+
}
|
|
957
|
+
}
|
|
958
|
+
}
|
|
959
|
+
if (updated) {
|
|
960
|
+
this.#discoveredIpRecords.set(netInterface, interfaceRecords);
|
|
961
|
+
}
|
|
962
|
+
}
|
|
963
|
+
|
|
964
|
+
/**
|
|
965
|
+
* Register Matter relevant IP records for later usage.
|
|
966
|
+
*/
|
|
967
|
+
#registerIpRecords(ipAddresses: AnyDnsRecordWithExpiry[], netInterface: string) {
|
|
968
|
+
const interfaceRecords = this.#discoveredIpRecords.get(netInterface) ?? {};
|
|
969
|
+
for (const record of ipAddresses) {
|
|
970
|
+
const { recordType, name, value: ip, ttl } = record as DnsRecord<string>;
|
|
971
|
+
if (ttl === 0) continue; // Skip records with ttl=0
|
|
972
|
+
if (recordType === DnsRecordType.AAAA) {
|
|
973
|
+
interfaceRecords.addressesV6 = interfaceRecords.addressesV6 ?? {};
|
|
974
|
+
interfaceRecords.addressesV6[name] = interfaceRecords.addressesV6[name] ?? new Map();
|
|
975
|
+
interfaceRecords.addressesV6[name].set(ip, record);
|
|
976
|
+
} else if (this.#enableIpv4 && recordType === DnsRecordType.A) {
|
|
977
|
+
interfaceRecords.addressesV4 = interfaceRecords.addressesV4 ?? {};
|
|
978
|
+
interfaceRecords.addressesV4[name] = interfaceRecords.addressesV4[name] ?? new Map();
|
|
979
|
+
interfaceRecords.addressesV4[name].set(ip, record);
|
|
980
|
+
}
|
|
981
|
+
}
|
|
982
|
+
this.#discoveredIpRecords.set(netInterface, interfaceRecords);
|
|
828
983
|
}
|
|
829
984
|
|
|
830
985
|
#handleIpRecords(
|
|
@@ -832,12 +987,21 @@ export class MdnsScanner implements Scanner {
|
|
|
832
987
|
target: string,
|
|
833
988
|
netInterface: string,
|
|
834
989
|
): { value: string; ttl: number }[] {
|
|
835
|
-
const ipRecords =
|
|
836
|
-
|
|
837
|
-
.
|
|
838
|
-
(
|
|
839
|
-
|
|
840
|
-
)
|
|
990
|
+
const ipRecords = new Array<AnyDnsRecordWithExpiry>();
|
|
991
|
+
answers.forEach(answer => {
|
|
992
|
+
if (answer.addressesV6?.[target]) {
|
|
993
|
+
ipRecords.push(...answer.addressesV6[target].values());
|
|
994
|
+
}
|
|
995
|
+
if (this.#enableIpv4 && answer.addressesV4?.[target]) {
|
|
996
|
+
ipRecords.push(...answer.addressesV4[target].values());
|
|
997
|
+
}
|
|
998
|
+
});
|
|
999
|
+
if (ipRecords.length === 0) {
|
|
1000
|
+
return [];
|
|
1001
|
+
}
|
|
1002
|
+
|
|
1003
|
+
// Remember the IP records for later Matter usage
|
|
1004
|
+
this.#registerIpRecords(ipRecords, netInterface); // Register for potential later usage
|
|
841
1005
|
|
|
842
1006
|
// If an IP is included multiple times we only keep the latest record
|
|
843
1007
|
const collectedIps = new Map<string, { value: string; ttl: number }>();
|
|
@@ -950,6 +1114,7 @@ export class MdnsScanner implements Scanner {
|
|
|
950
1114
|
discoveredAt,
|
|
951
1115
|
ttl: ttl * 1000,
|
|
952
1116
|
};
|
|
1117
|
+
const ipsInitiallyEmpty = device.addresses.size === 0;
|
|
953
1118
|
const { addresses } = device;
|
|
954
1119
|
if (ips.length > 0) {
|
|
955
1120
|
for (const { value: ip, ttl } of ips) {
|
|
@@ -967,9 +1132,9 @@ export class MdnsScanner implements Scanner {
|
|
|
967
1132
|
addresses.set(address.ip, address);
|
|
968
1133
|
}
|
|
969
1134
|
device.addresses = addresses;
|
|
970
|
-
if (
|
|
1135
|
+
if (ipsInitiallyEmpty) {
|
|
971
1136
|
logger.debug(
|
|
972
|
-
`Added IPs for operational device ${matterName} to cache (interface ${netInterface}):`,
|
|
1137
|
+
`Added ${addresses.size} IPs for operational device ${matterName} to cache (interface ${netInterface}):`,
|
|
973
1138
|
...MdnsScanner.deviceAddressDiagnostics(addresses),
|
|
974
1139
|
);
|
|
975
1140
|
}
|
|
@@ -1066,7 +1231,7 @@ export class MdnsScanner implements Scanner {
|
|
|
1066
1231
|
continue;
|
|
1067
1232
|
}
|
|
1068
1233
|
|
|
1069
|
-
const
|
|
1234
|
+
const recordAddressesKnown = storedRecord.addresses.size > 0;
|
|
1070
1235
|
|
|
1071
1236
|
const ips = this.#handleIpRecords([formerAnswers, answers], target, netInterface);
|
|
1072
1237
|
if (ips.length > 0) {
|
|
@@ -1099,6 +1264,11 @@ export class MdnsScanner implements Scanner {
|
|
|
1099
1264
|
`Requesting IP addresses for commissionable device ${record.name} (interface ${netInterface}).`,
|
|
1100
1265
|
);
|
|
1101
1266
|
this.#setQueryRecords(queryId, queries, answers);
|
|
1267
|
+
} else if (!recordAddressesKnown) {
|
|
1268
|
+
logger.debug(
|
|
1269
|
+
`Added ${storedRecord.addresses.size} IPs for commissionable device ${record.name} to cache (interface ${netInterface}):`,
|
|
1270
|
+
...MdnsScanner.deviceAddressDiagnostics(storedRecord.addresses),
|
|
1271
|
+
);
|
|
1102
1272
|
}
|
|
1103
1273
|
if (storedRecord.addresses.size === 0) continue;
|
|
1104
1274
|
|
|
@@ -1106,7 +1276,7 @@ export class MdnsScanner implements Scanner {
|
|
|
1106
1276
|
if (queryId === undefined) continue;
|
|
1107
1277
|
|
|
1108
1278
|
queryMissingDataForInstances.delete(record.name); // No need to query anymore, we have anything we need
|
|
1109
|
-
this.#finishWaiter(queryId, true,
|
|
1279
|
+
this.#finishWaiter(queryId, true, recordAddressesKnown);
|
|
1110
1280
|
}
|
|
1111
1281
|
|
|
1112
1282
|
// We have to query for the SRV records for the missing commissionable devices where we only had TXT records
|
|
@@ -1199,10 +1369,61 @@ export class MdnsScanner implements Scanner {
|
|
|
1199
1369
|
});
|
|
1200
1370
|
}
|
|
1201
1371
|
if (now > expires && !addresses.size) {
|
|
1202
|
-
// device expired and also has no
|
|
1372
|
+
// device expired and also has no addresses anymore
|
|
1203
1373
|
this.#commissionableDeviceRecords.delete(recordKey);
|
|
1204
1374
|
}
|
|
1205
1375
|
});
|
|
1376
|
+
|
|
1377
|
+
[...this.#activeAnnounceQueries.values()].forEach(({ answers }) => this.#expireStructuredAnswers(answers, now));
|
|
1378
|
+
|
|
1379
|
+
this.#discoveredIpRecords.forEach(answers => this.#expireStructuredAnswers(answers, now));
|
|
1380
|
+
}
|
|
1381
|
+
|
|
1382
|
+
#expireStructuredAnswers(data: StructuredDnsAnswers, now: number) {
|
|
1383
|
+
if (data.operational) {
|
|
1384
|
+
Object.keys(data.operational).forEach(recordType => {
|
|
1385
|
+
const type = parseInt(recordType);
|
|
1386
|
+
data.operational![type] = data.operational![type].filter(
|
|
1387
|
+
({ discoveredAt, ttl }) => now < discoveredAt + this.#effectiveTTL(ttl * 1000),
|
|
1388
|
+
);
|
|
1389
|
+
if (data.operational![type].length === 0) {
|
|
1390
|
+
delete data.operational![type];
|
|
1391
|
+
}
|
|
1392
|
+
});
|
|
1393
|
+
}
|
|
1394
|
+
if (data.commissionable) {
|
|
1395
|
+
Object.keys(data.commissionable).forEach(recordType => {
|
|
1396
|
+
const type = parseInt(recordType);
|
|
1397
|
+
data.commissionable![type] = data.commissionable![type].filter(
|
|
1398
|
+
({ discoveredAt, ttl }) => now < discoveredAt + this.#effectiveTTL(ttl * 1000),
|
|
1399
|
+
);
|
|
1400
|
+
if (data.commissionable![type].length === 0) {
|
|
1401
|
+
delete data.commissionable![type];
|
|
1402
|
+
}
|
|
1403
|
+
});
|
|
1404
|
+
}
|
|
1405
|
+
if (data.addressesV6) {
|
|
1406
|
+
Object.keys(data.addressesV6).forEach(name => {
|
|
1407
|
+
for (const [ip, { discoveredAt, ttl }] of data.addressesV6![name].entries()) {
|
|
1408
|
+
if (now < discoveredAt + this.#effectiveTTL(ttl * 1000)) continue; // not expired yet
|
|
1409
|
+
data.addressesV6![name].delete(ip);
|
|
1410
|
+
}
|
|
1411
|
+
if (data.addressesV6![name].size === 0) {
|
|
1412
|
+
delete data.addressesV6![name];
|
|
1413
|
+
}
|
|
1414
|
+
});
|
|
1415
|
+
}
|
|
1416
|
+
if (data.addressesV4) {
|
|
1417
|
+
Object.keys(data.addressesV4).forEach(name => {
|
|
1418
|
+
for (const [ip, { discoveredAt, ttl }] of data.addressesV4![name].entries()) {
|
|
1419
|
+
if (now < discoveredAt + this.#effectiveTTL(ttl * 1000)) continue; // not expired yet
|
|
1420
|
+
data.addressesV4![name].delete(ip);
|
|
1421
|
+
}
|
|
1422
|
+
if (data.addressesV4![name].size === 0) {
|
|
1423
|
+
delete data.addressesV4![name];
|
|
1424
|
+
}
|
|
1425
|
+
});
|
|
1426
|
+
}
|
|
1206
1427
|
}
|
|
1207
1428
|
|
|
1208
1429
|
static discoveryDataDiagnostics(data: DiscoveryData) {
|
package/src/mdns/MdnsServer.ts
CHANGED
|
@@ -115,14 +115,14 @@ export class MdnsServer {
|
|
|
115
115
|
: [];
|
|
116
116
|
if (knownAnswers.length > 0) {
|
|
117
117
|
for (const knownAnswersRecord of knownAnswers) {
|
|
118
|
-
answers = answers.filter(record => !isDeepEqual(record, knownAnswersRecord));
|
|
118
|
+
answers = answers.filter(record => !isDeepEqual(record, knownAnswersRecord, true));
|
|
119
119
|
if (answers.length === 0) break; // Nothing to send
|
|
120
120
|
}
|
|
121
121
|
if (answers.length === 0) continue; // Nothing to send
|
|
122
122
|
if (additionalRecords.length > 0) {
|
|
123
123
|
for (const knownAnswersRecord of knownAnswers) {
|
|
124
124
|
additionalRecords = additionalRecords.filter(
|
|
125
|
-
record => !isDeepEqual(record, knownAnswersRecord),
|
|
125
|
+
record => !isDeepEqual(record, knownAnswersRecord, true),
|
|
126
126
|
);
|
|
127
127
|
}
|
|
128
128
|
}
|
|
@@ -268,7 +268,8 @@ export class MdnsServer {
|
|
|
268
268
|
).catch(error => logger.error(error));
|
|
269
269
|
}
|
|
270
270
|
|
|
271
|
-
async expireAnnouncements(announcedNetPort?: number
|
|
271
|
+
async expireAnnouncements(options?: { announcedNetPort?: number; type?: AnnouncementType; forInstance?: string }) {
|
|
272
|
+
const { announcedNetPort, type, forInstance: instanceToExpire } = options ?? {};
|
|
272
273
|
await MatterAggregateError.allSettled(
|
|
273
274
|
this.#records.keys().map(async netInterface => {
|
|
274
275
|
const records = await this.#records.get(netInterface);
|
|
@@ -280,13 +281,25 @@ export class MdnsServer {
|
|
|
280
281
|
portType !== this.buildTypePortKey(type, announcedNetPort)
|
|
281
282
|
)
|
|
282
283
|
continue;
|
|
283
|
-
|
|
284
|
-
|
|
284
|
+
const recordsToProcess =
|
|
285
|
+
instanceToExpire !== undefined
|
|
286
|
+
? portTypeRecords.filter(
|
|
287
|
+
({ forInstance }) => forInstance !== undefined && instanceToExpire === forInstance,
|
|
288
|
+
)
|
|
289
|
+
: portTypeRecords;
|
|
290
|
+
const instanceSet = new Set<string>();
|
|
291
|
+
recordsToProcess.forEach(record => {
|
|
285
292
|
record.ttl = 0;
|
|
286
|
-
if (
|
|
287
|
-
|
|
293
|
+
if (record.recordType === DnsRecordType.TXT) {
|
|
294
|
+
instanceSet.add(record.name);
|
|
288
295
|
}
|
|
289
296
|
});
|
|
297
|
+
const instanceName =
|
|
298
|
+
instanceSet.size > 1
|
|
299
|
+
? "multiple"
|
|
300
|
+
: instanceSet.size === 1
|
|
301
|
+
? Array.from(instanceSet.values())[0]
|
|
302
|
+
: "";
|
|
290
303
|
logger.debug(
|
|
291
304
|
`Expiring records`,
|
|
292
305
|
Diagnostic.dict({
|
|
@@ -10,7 +10,7 @@ import { GeneralCommissioning } from "#clusters/general-commissioning";
|
|
|
10
10
|
import { NetworkCommissioning } from "#clusters/network-commissioning";
|
|
11
11
|
import { OperationalCredentials } from "#clusters/operational-credentials";
|
|
12
12
|
import { TimeSynchronizationCluster } from "#clusters/time-synchronization";
|
|
13
|
-
import { Bytes, ChannelType, Crypto, Logger, MatterError, Time, UnexpectedDataError
|
|
13
|
+
import { Bytes, ChannelType, Crypto, Logger, MatterError, repackErrorAs, Time, UnexpectedDataError } from "#general";
|
|
14
14
|
import {
|
|
15
15
|
ClusterId,
|
|
16
16
|
ClusterType,
|
|
@@ -107,6 +107,9 @@ type CommissioningStep = {
|
|
|
107
107
|
|
|
108
108
|
/** Logic function to execute */
|
|
109
109
|
stepLogic: () => Promise<CommissioningStepResult>;
|
|
110
|
+
|
|
111
|
+
/** Optional flag to indicate that the failsafe timer should be rearmed in any case before this step. */
|
|
112
|
+
reArmFailsafe?: boolean;
|
|
110
113
|
};
|
|
111
114
|
|
|
112
115
|
/** Data that are collected initially or through the commissioning process and can be used also by other steps. */
|
|
@@ -194,9 +197,15 @@ export class ControllerCommissioningFlow {
|
|
|
194
197
|
async executeCommissioning() {
|
|
195
198
|
this.#sortSteps();
|
|
196
199
|
|
|
200
|
+
let failSafeTimerReArmedAfterPreviousStep = false;
|
|
197
201
|
for (const step of this.#commissioningSteps) {
|
|
198
202
|
logger.info(`Executing commissioning step ${step.stepNumber}.${step.subStepNumber}: ${step.name}`);
|
|
199
203
|
try {
|
|
204
|
+
if (step.reArmFailsafe && !failSafeTimerReArmedAfterPreviousStep) {
|
|
205
|
+
logger.debug(`Re-Arming failsafe timer before executing step`);
|
|
206
|
+
await this.#armFailsafe();
|
|
207
|
+
}
|
|
208
|
+
failSafeTimerReArmedAfterPreviousStep = false;
|
|
200
209
|
const result = await step.stepLogic();
|
|
201
210
|
this.#setCommissioningStepResult(step, result);
|
|
202
211
|
|
|
@@ -222,6 +231,7 @@ export class ControllerCommissioningFlow {
|
|
|
222
231
|
)}s elapsed since last arm failsafe, re-arming failsafe`,
|
|
223
232
|
);
|
|
224
233
|
await this.#armFailsafe();
|
|
234
|
+
failSafeTimerReArmedAfterPreviousStep = true;
|
|
225
235
|
}
|
|
226
236
|
}
|
|
227
237
|
|
|
@@ -369,6 +379,7 @@ export class ControllerCommissioningFlow {
|
|
|
369
379
|
stepNumber: 12, // includes step 13
|
|
370
380
|
subStepNumber: 2,
|
|
371
381
|
name: "NetworkCommissioning.Wifi",
|
|
382
|
+
reArmFailsafe: true,
|
|
372
383
|
stepLogic: () => this.#configureNetworkWifi(),
|
|
373
384
|
});
|
|
374
385
|
}
|
|
@@ -377,6 +388,7 @@ export class ControllerCommissioningFlow {
|
|
|
377
388
|
stepNumber: 12, // includes step 13
|
|
378
389
|
subStepNumber: 3,
|
|
379
390
|
name: "NetworkCommissioning.Thread",
|
|
391
|
+
reArmFailsafe: true,
|
|
380
392
|
stepLogic: () => this.#configureNetworkThread(),
|
|
381
393
|
});
|
|
382
394
|
}
|
|
@@ -390,6 +402,7 @@ export class ControllerCommissioningFlow {
|
|
|
390
402
|
stepNumber: 14, // includes step 15 (CASE connection)
|
|
391
403
|
subStepNumber: 1,
|
|
392
404
|
name: "Reconnect",
|
|
405
|
+
reArmFailsafe: true,
|
|
393
406
|
stepLogic: () => this.#reconnectWithDevice(),
|
|
394
407
|
});
|
|
395
408
|
|
|
@@ -1149,13 +1162,45 @@ export class ControllerCommissioningFlow {
|
|
|
1149
1162
|
*
|
|
1150
1163
|
*/
|
|
1151
1164
|
async #reconnectWithDevice() {
|
|
1152
|
-
|
|
1165
|
+
const isConcurrentFlow = this.#collectedCommissioningData.supportsConcurrentConnection !== false;
|
|
1166
|
+
|
|
1167
|
+
logger.debug(`Reconnecting with device with ${isConcurrentFlow ? "concurrent" : "non-concurrent"} flow ...`);
|
|
1168
|
+
|
|
1169
|
+
// Reconnection with discovery could take longer then the default failsafe time, so we need to
|
|
1170
|
+
// re-arm the failsafe when we are in a concurrent commissioning flow also in parallel to
|
|
1171
|
+
// the operative reconnection
|
|
1172
|
+
// TODO: Check whats needed for non-concurrent commissioning flows (maybe arm initially longer?)
|
|
1173
|
+
const reArmFailsafeInterval = Time.getPeriodicTimer(
|
|
1174
|
+
"Re-Arm Failsafe during reconnect",
|
|
1175
|
+
this.#failSafeTimeMs / 2,
|
|
1176
|
+
() => {
|
|
1177
|
+
const now = Time.nowMs();
|
|
1178
|
+
if (this.#commissioningExpiryTime !== undefined && now < this.#commissioningExpiryTime) {
|
|
1179
|
+
logger.error(
|
|
1180
|
+
`Re-Arm Failsafe Timer during reconnect with device. Time left: ${Math.round((this.#commissioningExpiryTime - now) / 1000)}s`,
|
|
1181
|
+
);
|
|
1182
|
+
this.#armFailsafe().catch(error => {
|
|
1183
|
+
logger.error("Error while re-arming failsafe during reconnect", error);
|
|
1184
|
+
reArmFailsafeInterval.stop();
|
|
1185
|
+
});
|
|
1186
|
+
} else {
|
|
1187
|
+
// Stop as soon as we are over the maximum commissioning time
|
|
1188
|
+
reArmFailsafeInterval.stop();
|
|
1189
|
+
}
|
|
1190
|
+
},
|
|
1191
|
+
);
|
|
1192
|
+
if (isConcurrentFlow) {
|
|
1193
|
+
reArmFailsafeInterval.start();
|
|
1194
|
+
}
|
|
1195
|
+
|
|
1153
1196
|
const transitionResult = await this.#transitionToCase(
|
|
1154
1197
|
this.#interactionClient.address,
|
|
1155
1198
|
// Assume concurrent connections are supported if not know (which should not be the case when we came here)
|
|
1156
|
-
|
|
1199
|
+
isConcurrentFlow,
|
|
1157
1200
|
);
|
|
1158
1201
|
|
|
1202
|
+
reArmFailsafeInterval.stop();
|
|
1203
|
+
|
|
1159
1204
|
if (transitionResult === undefined) {
|
|
1160
1205
|
logger.debug("CASE commissioning handled externally, terminating commissioning flow");
|
|
1161
1206
|
return {
|