Skip to content

Commit

Permalink
Merge branch 'main' into re-enable-switchtests
Browse files Browse the repository at this point in the history
  • Loading branch information
Apollon77 authored Dec 27, 2024
2 parents 5c38db4 + 9e7d818 commit 528cf87
Show file tree
Hide file tree
Showing 22 changed files with 521 additions and 123 deletions.
7 changes: 6 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ The main work (all changes without a GitHub username in brackets in the below li

- @matter/node
- Enhancement: Matter protocol initialization now runs independently of and after behavior initialization, giving behaviors more flexibility in participating in protocol setup
- Enhancement: Each new PASE session now automatically arms the failsafe timer for 60s as required by specs
- Fix: Fixes withBehaviors() method on endpoints

- @matter/nodejs-ble
Expand All @@ -30,7 +31,11 @@ The main work (all changes without a GitHub username in brackets in the below li
- FIx: Restores the possibility to cancel a (continuous) discovery for commissionable devices

- @project-chip/matter.js
- Enhancement: Improves Reconnection Handling for devices that use Persisted Subscriptions
- Feature: (Breaking) Added Fabric Label for Controller as required property to initialize the Controller
including setting the Fabric Label when commissioning and validating and updating the Fabric Label on
connection
- Feature: Allows to update the Fabric Label during controller runtime using `updateFabricLabel()` on CommissioningController
- Enhancement: Improves Reconnection Handling for devices that use persisted subscriptions
- Enhancement: Use data type definitions from Model for Controller Device type definitions

## 0.11.9 (2024-12-11)
Expand Down
2 changes: 1 addition & 1 deletion chip-testing/src/NamedPipeCommandHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ export class NamedPipeCommandHandler extends CommandPipe {
this.#namedPipeSocket = new Socket({ fd: this.#namedPipe.fd });
console.log(`Named pipe opened: ${this.filename}`);

this.#namedPipeSocket.on("data", this.onData.bind(this));
this.#namedPipeSocket.on("data", this.onData);

this.#namedPipeSocket.on("error", err => {
console.log("Named pipe error:", err);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,10 @@ class ControllerNode {
const controllerStorage = storageManager.createContext("Controller");
const ip = controllerStorage.has("ip") ? controllerStorage.get<string>("ip") : getParameter("ip");
const port = controllerStorage.has("port") ? controllerStorage.get<number>("port") : getIntParameter("port");
const adminFabricLabel = controllerStorage.has("fabricLabel")
? controllerStorage.get<string>("fabricLabel")
: "matter.js Controller";
controllerStorage.set("fabricLabel", adminFabricLabel);

const pairingCode = getParameter("pairingcode");
let longDiscriminator, setupPin, shortDiscriminator;
Expand Down Expand Up @@ -181,6 +185,7 @@ class ControllerNode {
const matterServer = new MatterServer(storageManager);
const commissioningController = new CommissioningController({
autoConnect: false,
adminFabricLabel,
});
await matterServer.addCommissioningController(commissioningController);

Expand Down
5 changes: 5 additions & 0 deletions packages/examples/src/controller/ControllerNode.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,10 @@ class ControllerNode {
? await controllerStorage.get<string>("uniqueid")
: (environment.vars.string("uniqueid") ?? Time.nowMs().toString());
await controllerStorage.set("uniqueid", uniqueId);
const adminFabricLabel = (await controllerStorage.has("fabriclabel"))
? await controllerStorage.get<string>("fabriclabel")
: (environment.vars.string("fabriclabel") ?? "matter.js Controller");
await controllerStorage.set("fabriclabel", adminFabricLabel);

const pairingCode = environment.vars.string("pairingcode");
let longDiscriminator, setupPin, shortDiscriminator;
Expand Down Expand Up @@ -136,6 +140,7 @@ class ControllerNode {
id: uniqueId,
},
autoConnect: false,
adminFabricLabel,
});

/**
Expand Down
1 change: 1 addition & 0 deletions packages/examples/src/controller/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ npm run matter-controller -- --pairingcode=12345678901
This will commission a MatterServer device (for debugging/capability showing purpose only for now).

The following parameters are available and used to initially commission a device (they can be omitted after this):
* --fabriclabel: the fabric label to use for commissioning (default: "matter.js Controller")
* If the IP and Port of the device is known (should be only the case in testing cases) you can use the following parameters:
* --ip: the IP address of the device to commission (can be used but discovery via pairingcode or discriminator or also just pin (passode) is most likely better)
* --port the port of the device to commission (default: 5540)
Expand Down
89 changes: 87 additions & 2 deletions packages/matter.js/src/CommissioningController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
* SPDX-License-Identifier: Apache-2.0
*/

import { OperationalCredentials } from "#clusters";
import {
Environment,
ImplementationError,
Expand All @@ -15,6 +16,7 @@ import {
StorageContext,
SyncStorage,
UdpInterface,
UnexpectedDataError,
} from "#general";
import { LegacyControllerStore } from "#LegacyControllerStore.js";
import { ControllerStore } from "#node";
Expand Down Expand Up @@ -42,7 +44,7 @@ import {
TypeFromPartialBitSchema,
VendorId,
} from "#types";
import { CommissioningControllerNodeOptions, PairedNode } from "./device/PairedNode.js";
import { CommissioningControllerNodeOptions, NodeStates, PairedNode } from "./device/PairedNode.js";
import { MatterController } from "./MatterController.js";
import { MatterNode } from "./MatterNode.js";

Expand Down Expand Up @@ -108,6 +110,13 @@ export type CommissioningControllerOptions = CommissioningControllerNodeOptions
*/
readonly caseAuthenticatedTags?: CaseAuthenticatedTag[];

/**
* The Fabric Label to set for the commissioned devices. The #label is used for users to identify the admin.
* The maximum length are 32 characters!
* The value will automatically be checked on connection to a node and updated if necessary.
*/
readonly adminFabricLabel: string;

/**
* When used with the new API Environment set the environment here and the CommissioningServer will self-register
* on the environment when you call start().
Expand Down Expand Up @@ -137,6 +146,7 @@ export class CommissioningController extends MatterNode {

private controllerInstance?: MatterController;
private initializedNodes = new Map<NodeId, PairedNode>();
private nodeUpdateLabelHandlers = new Map<NodeId, (nodeState: NodeStates) => Promise<void>>();
private sessionDisconnectedHandler = new Map<NodeId, () => Promise<void>>();

/**
Expand Down Expand Up @@ -188,7 +198,8 @@ export class CommissioningController extends MatterNode {
if (this.controllerInstance !== undefined) {
return this.controllerInstance;
}
const { localPort, adminFabricId, adminVendorId, adminFabricIndex, caseAuthenticatedTags } = this.options;
const { localPort, adminFabricId, adminVendorId, adminFabricIndex, caseAuthenticatedTags, adminFabricLabel } =
this.options;

if (environment === undefined && storage === undefined) {
throw new ImplementationError("Storage not initialized correctly.");
Expand Down Expand Up @@ -222,6 +233,7 @@ export class CommissioningController extends MatterNode {
adminFabricId,
adminFabricIndex,
caseAuthenticatedTags,
adminFabricLabel,
});
if (this.mdnsBroadcaster) {
controller.addBroadcaster(this.mdnsBroadcaster.createInstanceBroadcaster(port));
Expand Down Expand Up @@ -563,6 +575,79 @@ export class CommissioningController extends MatterNode {
getActiveSessionInformation() {
return this.controllerInstance?.getActiveSessionInformation() ?? [];
}

async validateAndUpdateFabricLabel(nodeId: NodeId) {
const controller = this.assertControllerIsStarted();
const node = this.initializedNodes.get(nodeId);
if (node === undefined) {
throw new ImplementationError(`Node ${nodeId} is not connected!`);
}
const operationalCredentialsCluster = node.getRootClusterClient(OperationalCredentials.Cluster);
if (operationalCredentialsCluster === undefined) {
throw new UnexpectedDataError(`Node ${nodeId}: Operational Credentials Cluster not available!`);
}
const fabrics = await operationalCredentialsCluster.getFabricsAttribute(false, true);
if (fabrics.length !== 1) {
logger.info(`Invalid fabrics returned from node ${nodeId}.`, fabrics);
return;
}
const label = controller.fabricConfig.label;
const fabric = fabrics[0];
if (fabric.label !== label) {
logger.info(
`Node ${nodeId}: Fabric label "${fabric.label}" does not match requested admin fabric Label "${label}". Updating...`,
);
await operationalCredentialsCluster.updateFabricLabel({
label,
fabricIndex: fabric.fabricIndex,
});
}
}

async updateFabricLabel(label: string) {
const controller = this.assertControllerIsStarted();
if (controller.fabricConfig.label === label) {
return;
}
await controller.updateFabricLabel(label);

for (const node of this.initializedNodes.values()) {
if (node.isConnected) {
// When Node is connected, update the fabric label on the node directly
try {
await this.validateAndUpdateFabricLabel(node.nodeId);
return;
} catch (error) {
logger.warn(`Error updating fabric label on node ${node.nodeId}:`, error);
}
}
if (!node.remoteInitializationDone) {
// Node not online and was also not yet initialized, means update happens
// automatically when node connects
logger.info(`Node ${node.nodeId} is offline. Fabric label will be updated on next connection.`);
return;
}
logger.info(
`Node ${node.nodeId} is reconnecting. Delaying fabric label update to when node is back online.`,
);

// If no update handler is registered, register one
// TODO: Convert this next to a task system for node tasks and also better handle error cases
if (!this.nodeUpdateLabelHandlers.has(node.nodeId)) {
const updateOnReconnect = (nodeState: NodeStates) => {
if (nodeState === NodeStates.Connected) {
this.validateAndUpdateFabricLabel(node.nodeId)
.catch(error => logger.warn(`Error updating fabric label on node ${node.nodeId}:`, error))
.finally(() => {
node.events.stateChanged.off(updateOnReconnect);
this.nodeUpdateLabelHandlers.delete(node.nodeId);
});
}
};
node.events.stateChanged.on(updateOnReconnect);
}
}
}
}

export async function configureNetwork(options: {
Expand Down
44 changes: 39 additions & 5 deletions packages/matter.js/src/MatterController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,7 @@ export class MatterController {
adminFabricId?: FabricId;
adminFabricIndex?: FabricIndex;
caseAuthenticatedTags?: CaseAuthenticatedTag[];
adminFabricLabel: string;
}): Promise<MatterController> {
const {
controllerStore,
Expand All @@ -112,6 +113,7 @@ export class MatterController {
adminFabricId = FabricId(DEFAULT_FABRIC_ID),
adminFabricIndex = FabricIndex(DEFAULT_FABRIC_INDEX),
caseAuthenticatedTags,
adminFabricLabel,
} = options;

const ca = await CertificateAuthority.create(controllerStore.caStorage);
Expand All @@ -129,6 +131,7 @@ export class MatterController {
netInterfaces,
certificateManager: ca,
fabric,
adminFabricLabel,
sessionClosedCallback,
});
} else {
Expand All @@ -148,7 +151,8 @@ export class MatterController {
.setRootCert(ca.rootCert)
.setRootNodeId(rootNodeId)
.setIdentityProtectionKey(ipkValue)
.setRootVendorId(adminVendorId ?? DEFAULT_ADMIN_VENDOR_ID);
.setRootVendorId(adminVendorId ?? DEFAULT_ADMIN_VENDOR_ID)
.setLabel(adminFabricLabel);
fabricBuilder.setOperationalCert(
ca.generateNoc(fabricBuilder.publicKey, adminFabricId, rootNodeId, caseAuthenticatedTags),
);
Expand All @@ -160,6 +164,7 @@ export class MatterController {
netInterfaces,
certificateManager: ca,
fabric,
adminFabricLabel,
sessionClosedCallback,
});
}
Expand All @@ -172,9 +177,17 @@ export class MatterController {
fabricConfig: Fabric.Config;
scanners: ScannerSet;
netInterfaces: NetInterfaceSet;
adminFabricLabel: string;
sessionClosedCallback?: (peerNodeId: NodeId) => void;
}): Promise<MatterController> {
const { certificateAuthorityConfig, fabricConfig, scanners, netInterfaces, sessionClosedCallback } = options;
const {
certificateAuthorityConfig,
fabricConfig,
adminFabricLabel,
scanners,
netInterfaces,
sessionClosedCallback,
} = options;

// Verify an appropriate network interface is available
if (!netInterfaces.hasInterfaceFor(ChannelType.BLE)) {
Expand All @@ -200,6 +213,7 @@ export class MatterController {
netInterfaces,
certificateManager,
fabric,
adminFabricLabel,
sessionClosedCallback,
});
await controller.construction;
Expand Down Expand Up @@ -232,9 +246,18 @@ export class MatterController {
netInterfaces: NetInterfaceSet;
certificateManager: CertificateAuthority;
fabric: Fabric;
adminFabricLabel: string;
sessionClosedCallback?: (peerNodeId: NodeId) => void;
}) {
const { controllerStore, scanners, netInterfaces, certificateManager, fabric, sessionClosedCallback } = options;
const {
controllerStore,
scanners,
netInterfaces,
certificateManager,
fabric,
sessionClosedCallback,
adminFabricLabel,
} = options;
this.#store = controllerStore;
this.scanners = scanners;
this.netInterfaces = netInterfaces;
Expand All @@ -244,6 +267,10 @@ export class MatterController {

const fabricManager = new FabricManager();
fabricManager.addFabric(fabric);
// Overwrite the persist callback and store fabric when needed
fabric.persistCallback = async () => {
await this.#store.fabricStorage.set("fabric", this.fabric.config);
};

this.sessionManager = new SessionManager({
fabrics: fabricManager,
Expand Down Expand Up @@ -293,6 +320,9 @@ export class MatterController {
this.#construction = Construction(this, async () => {
await this.peers.construction.ready;
await this.sessionManager.construction.ready;
if (this.fabric.label !== adminFabricLabel) {
await fabric.setLabel(adminFabricLabel);
}
});
}

Expand Down Expand Up @@ -371,7 +401,7 @@ export class MatterController {

const address = await this.commissioner.commissionWithDiscovery(commissioningOptions);

await this.#store.fabricStorage.set("fabric", this.fabric.config);
await this.fabric.persist();

return address.nodeId;
}
Expand Down Expand Up @@ -411,7 +441,7 @@ export class MatterController {
await this.peers.delete(this.fabric.addressOf(peerNodeId));
throw new CommissioningError(`Commission error on commissioningComplete: ${errorCode}, ${debugText}`);
}
await this.#store.fabricStorage.set("fabric", this.fabric.config);
await this.fabric.persist();
}

isCommissioned() {
Expand Down Expand Up @@ -536,6 +566,10 @@ export class MatterController {
await peer.dataStore.construction;
return peer.dataStore.retrieveAttributes(endpointId, clusterId);
}

async updateFabricLabel(label: string) {
await this.fabric.setLabel(label);
}
}

class CommissionedNodeStore extends PeerAddressStore {
Expand Down
1 change: 1 addition & 0 deletions packages/matter.js/src/PaseCommissioner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,7 @@ export class PaseCommissioner {
fabricConfig: fabricConfig,
scanners,
netInterfaces,
adminFabricLabel: this.options.fabricConfig.label,
});
}

Expand Down
7 changes: 7 additions & 0 deletions packages/matter.js/src/device/PairedNode.ts
Original file line number Diff line number Diff line change
Expand Up @@ -629,6 +629,13 @@ export class PairedNode {
const allClusterAttributes = await this.readAllAttributes();
await this.#initializeEndpointStructure(allClusterAttributes, anyInitializationDone);
}
if (!this.#remoteInitializationDone) {
try {
await this.commissioningController.validateAndUpdateFabricLabel(this.nodeId);
} catch (error) {
logger.info(`Node ${this.nodeId}: Error updating fabric label`, error);
}
}
this.#reconnectErrorCount = 0;
this.#setConnectionState(NodeStates.Connected);
await this.events.initializedFromRemote.emit(this.#nodeDetails.toStorageData());
Expand Down
Loading

0 comments on commit 528cf87

Please sign in to comment.