Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

PASE Commissioner #982

Merged
merged 17 commits into from
Jul 1, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,12 @@ The main work (all changes without a GitHub username in brackets in the below li
- matter.js API:
- Feature: Adds default implementations for i18n clusters including Localization, Time Format Localization and Unit Localization.
- Feature: Adds interactionBegin and interactionEnd events for ClusterBehaviors to demarcate online interactions that mutate state.
- matter.js Legacy API:
- matter.js Controller API:
- Breaking: commissionNode() in CommissioningController now returns the Node-ID and not the PairedNode instance.
- Feature: (Experimental!) Adds PaseCommissioner to allow to execute the initial (PASE based) commissioning process separately from the operational completion of the commissioning process, also allowed to be BLE only.
- Feature: Allows to complete the commissioning process for a node where this process was started by a PASE commissioner
- Feature: Allows to commission a node without directly connecting to it
- matter.js Legacy API:
- Deprecation: We've deprecated the hand-generated device type definitions used by the pre-0.8.0 API in DeviceTypes.ts. These device type definitions remain at Matter 1.1.
- Removal: We removed old Scenes cluster implementation which was never fully implemented or used by any Matter controller
- matter.js-react-native:
Expand Down
12 changes: 10 additions & 2 deletions packages/matter-node-shell.js/src/shell/cmd_commission.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

import { NodeCommissioningOptions } from "@project-chip/matter.js";
import { BasicInformationCluster, DescriptorCluster, GeneralCommissioning } from "@project-chip/matter.js/cluster";
import { MatterError } from "@project-chip/matter.js/common";
import { NodeId } from "@project-chip/matter.js/datatype";
import { Logger } from "@project-chip/matter.js/log";
import { ManualPairingCodeCodec, QrCode } from "@project-chip/matter.js/schema";
Expand Down Expand Up @@ -122,9 +123,16 @@ export default function commands(theNode: MatterNode) {
};
}

const node = await theNode.commissioningController.commissionNode(options);
const commissionedNodeId =
await theNode.commissioningController.commissionNode(options);

console.log("Commissioned Node:", node.nodeId);
console.log("Commissioned Node:", commissionedNodeId);

const node = theNode.commissioningController.getConnectedNode(commissionedNodeId);
if (node === undefined) {
// Should not happen
throw new MatterError("Node not found after commissioning.");
}

// Important: This is a temporary API to proof the methods working and this will change soon and is NOT stable!
// It is provided to proof the concept
Expand Down
20 changes: 12 additions & 8 deletions packages/matter-node.js/test/IntegrationTest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ import {
NodeId,
VendorId,
} from "@project-chip/matter.js/datatype";
import { NodeStateInformation, OnOffLightDevice } from "@project-chip/matter.js/device";
import { NodeStateInformation, OnOffLightDevice, PairedNode } from "@project-chip/matter.js/device";
import { FabricBuilder, FabricJsonObject } from "@project-chip/matter.js/fabric";
import {
DecodedEventData,
Expand Down Expand Up @@ -291,7 +291,7 @@ describe("Integration Test", () => {
});

await commissioningController.start();
const node = await commissioningController.commissionNode({
const nodeId = await commissioningController.commissionNode({
discovery: {
knownAddress: { ip: SERVER_IPv6, port: matterPort, type: "udp" },
identifierData: { longDiscriminator },
Expand All @@ -304,14 +304,16 @@ describe("Integration Test", () => {
stateInformationCallback: (nodeId: NodeId, nodeState: NodeStateInformation) =>
nodeStateChangesController1Node1.push({ nodeId, nodeState, time: MockTime.nowMs() }),
});
const node = commissioningController.getConnectedNode(nodeId);
expect(node).to.be.an.instanceOf(PairedNode);

Time.get = () => mockTimeInstance;

Network.get = () => {
throw new Error("Network should not be requested post starting");
};

assert.deepEqual(commissioningController.getCommissionedNodes(), [node.nodeId]);
assert.deepEqual(commissioningController.getCommissionedNodes(), [nodeId]);
assert.equal(commissioningChangedCallsServer.length, 1);
assert.equal(commissioningChangedCallsServer[0].fabricIndex, FabricIndex(1));
assert.equal(sessionChangedCallsServer.length, 1);
Expand All @@ -320,10 +322,10 @@ describe("Integration Test", () => {
assert.equal(sessionInfo.length, 1);
assert.ok(sessionInfo[0].fabric);
assert.equal(sessionInfo[0].fabric.fabricIndex, FabricIndex(1));
assert.equal(sessionInfo[0].nodeId, node.nodeId);
assert.equal(sessionInfo[0].nodeId, nodeId);

assert.equal(nodeStateChangesController1Node1.length, 1);
assert.equal(nodeStateChangesController1Node1[0].nodeId, node.nodeId);
assert.equal(nodeStateChangesController1Node1[0].nodeId, nodeId);
assert.equal(nodeStateChangesController1Node1[0].nodeState, NodeStateInformation.Connected);
}).timeout(10000);

Expand Down Expand Up @@ -1332,7 +1334,7 @@ describe("Integration Test", () => {

const existingNodes = commissioningController.getCommissionedNodes();

const node = await commissioningController.commissionNode({
const nodeId = await commissioningController.commissionNode({
discovery: {
knownAddress: { ip: SERVER_IPv6, port: matterPort2, type: "udp" },
identifierData: { longDiscriminator },
Expand All @@ -1345,10 +1347,12 @@ describe("Integration Test", () => {
stateInformationCallback: (nodeId: NodeId, nodeState: NodeStateInformation) =>
nodeStateChangesController1Node2.push({ nodeId, nodeState, time: MockTime.nowMs() }),
});
const node = commissioningController.getConnectedNode(nodeId);
expect(node).to.be.an.instanceOf(PairedNode);

Time.get = () => mockTimeInstance;

assert.deepEqual(commissioningController.getCommissionedNodes(), [...existingNodes, node.nodeId]);
assert.deepEqual(commissioningController.getCommissionedNodes(), [...existingNodes, nodeId]);

assert.equal(commissioningServer2CertificateProviderCalled, true);
assert.equal(commissioningChangedCallsServer2.length, 1);
Expand All @@ -1360,7 +1364,7 @@ describe("Integration Test", () => {
assert.equal(sessionInfo[0].numberOfActiveSubscriptions, 0);

assert.equal(nodeStateChangesController1Node2.length, 1);
assert.equal(nodeStateChangesController1Node2[0].nodeId, node.nodeId);
assert.equal(nodeStateChangesController1Node2[0].nodeId, nodeId);
assert.equal(nodeStateChangesController1Node2[0].nodeState, NodeStateInformation.Connected);
});

Expand Down
83 changes: 54 additions & 29 deletions packages/matter.js/src/CommissioningController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import { GlobalAttributes } from "./cluster/Cluster.js";
import { SupportedAttributeClient } from "./cluster/client/AttributeClient.js";
import { BasicInformation } from "./cluster/definitions/BasicInformationCluster.js";
import { ImplementationError, InternalError } from "./common/MatterError.js";
import { CommissionableDevice, CommissionableDeviceIdentifiers } from "./common/Scanner.js";
import { CommissionableDevice, CommissionableDeviceIdentifiers, DiscoveryData } from "./common/Scanner.js";
import { ServerAddress } from "./common/ServerAddress.js";
import { CaseAuthenticatedTag } from "./datatype/CaseAuthenticatedTag.js";
import { EndpointNumber } from "./datatype/EndpointNumber.js";
Expand Down Expand Up @@ -41,6 +41,18 @@ const logger = new Logger("CommissioningController");
// TODO decline using setRoot*Cluster
// TODO Decline cluster access after announced/paired

export type ControllerEnvironmentOptions = {
/**
* Environment to register the node with on start()
*/
readonly environment: Environment;

/**
* Unique id to register to node.
*/
readonly id: string;
};

/**
* Constructor options for the CommissioningController class
*/
Expand Down Expand Up @@ -89,17 +101,7 @@ export type CommissioningControllerOptions = CommissioningControllerNodeOptions
* When used with the new API Environment set the environment here and the CommissioningServer will self-register
* on the environment when you call start().
*/
readonly environment?: {
/**
* Environment to register the node with on start()
*/
readonly environment: Environment;

/**
* Unique id to register to node.
*/
readonly id: string;
};
readonly environment?: ControllerEnvironmentOptions;
};

/** Options needed to commission a new node */
Expand Down Expand Up @@ -174,6 +176,16 @@ export class CommissioningController extends MatterNode {
return this.controllerInstance?.nodeId;
}

get paseCommissionerData() {
const controller = this.assertControllerIsStarted(
"The CommissioningController needs to be started to get the PASE commissioner data.",
);
return {
rootCertificateData: controller.rootCertificateData,
fabricData: controller.fabricData,
};
}

assertIsAddedToMatterServer() {
if (this.mdnsScanner === undefined || (this.storage === undefined && this.environment === undefined)) {
throw new ImplementationError("Add the node to the Matter instance before.");
Expand Down Expand Up @@ -218,17 +230,17 @@ export class CommissioningController extends MatterNode {
throw new InternalError("Storage not initialized correctly."); // Should not happen
}

return await MatterController.create(
return await MatterController.create({
sessionStorage,
rootCertificateStorage,
fabricStorage,
nodesStorage,
mdnsScanner,
this.ipv4Disabled
netInterfaceIpv4: this.ipv4Disabled
? undefined
: await UdpInterface.create(Network.get(), "udp4", localPort, this.listeningAddressIpv4),
await UdpInterface.create(Network.get(), "udp6", localPort, this.listeningAddressIpv6),
peerNodeId => {
netInterfaceIpv6: await UdpInterface.create(Network.get(), "udp6", localPort, this.listeningAddressIpv6),
sessionClosedCallback: peerNodeId => {
logger.info(`Session for peer node ${peerNodeId} disconnected ...`);
const handler = this.sessionDisconnectedHandler.get(peerNodeId);
if (handler !== undefined) {
Expand All @@ -239,27 +251,41 @@ export class CommissioningController extends MatterNode {
adminFabricId,
adminFabricIndex,
caseAuthenticatedTags,
);
});
}

/**
* Commissions/Pairs a new device into the controller fabric. The method returns a PairedNode instance of the
* paired node on success.
* Commissions/Pairs a new device into the controller fabric. The method returns the NodeId of the commissioned node.
*/
async commissionNode(nodeOptions: NodeCommissioningOptions) {
async commissionNode(nodeOptions: NodeCommissioningOptions, connectNodeAfterCommissioning = true) {
this.assertIsAddedToMatterServer();
const controller = this.assertControllerIsStarted();

const nodeId = await controller.commission(nodeOptions);

return this.connectNode(nodeId, {
...nodeOptions,
autoSubscribe: nodeOptions.autoSubscribe ?? this.options.autoSubscribe,
subscribeMinIntervalFloorSeconds:
nodeOptions.subscribeMinIntervalFloorSeconds ?? this.options.subscribeMinIntervalFloorSeconds,
subscribeMaxIntervalCeilingSeconds:
nodeOptions.subscribeMaxIntervalCeilingSeconds ?? this.options.subscribeMaxIntervalCeilingSeconds,
});
if (connectNodeAfterCommissioning) {
await this.connectNode(nodeId, {
...nodeOptions,
autoSubscribe: nodeOptions.autoSubscribe ?? this.options.autoSubscribe,
subscribeMinIntervalFloorSeconds:
nodeOptions.subscribeMinIntervalFloorSeconds ?? this.options.subscribeMinIntervalFloorSeconds,
subscribeMaxIntervalCeilingSeconds:
nodeOptions.subscribeMaxIntervalCeilingSeconds ?? this.options.subscribeMaxIntervalCeilingSeconds,
});
}

return nodeId;
}

/**
* Completes the commissioning process for a node when the initial commissioning process was done by a PASE
* commissioner. This method should be called to discover the device operational and complete the commissioning
* process.
*/
completeCommissioningForNode(peerNodeId: NodeId, discoveryData?: DiscoveryData) {
this.assertIsAddedToMatterServer();
const controller = this.assertControllerIsStarted();
return controller.completeCommissioning(peerNodeId, discoveryData);
}

/** Check if a given node id is commissioned on this controller. */
Expand Down Expand Up @@ -512,7 +538,6 @@ export class CommissioningController extends MatterNode {

const mdnsService = await environment.load(MdnsService);
this.ipv4Disabled = !mdnsService.enableIpv4;
console.log("Init ipv4: ", this.ipv4Disabled);
this.setMdnsBroadcaster(mdnsService.broadcaster);
this.setMdnsScanner(mdnsService.scanner);

Expand Down
Loading