Skip to content

Commit

Permalink
PASE Commissioner (#982)
Browse files Browse the repository at this point in the history
* getter change and code simplify

* Enhance Commissioner

Enhance the "doOperationalDeviceConnection" callback with the option to notify the commissioner that the completion of the commissioning process was done via other ways (e.g. from a server differently from the commissioner instance).

* Enhance/Streamline CommissioningController

The commisioning options got an optional callback to allow custom commissioning completion ways.
Additionally streamline the interface (breaking) to allow just to commission to a device without a connection afterwards.

* Enhance RootCertificateManager init

And allow the class to be initialized with data and without storage too.

* Split out Env options from Controller to own type

* Move NatterController to options constructor

* Adjust Controller to generally work also without IP network

... but the standard "create" is still requiring it, so external interface stays unchanged

* No custom commissioning-complete for standard controller

... because makes no sense for now.

* Introduces StubCommissioner

This class is a minimalistic commissioning controller that allows to just execute the initial commissioning process without completion. For this it gets initialized with the Root certificate data and the fabric data from a Controller fabric that already exists. A callback is called when operational  discovery and completing the commissioning is needed.

* Allows custom commissioning completion

* Better name PASECommissioner

* Changelog

* Changelog

* linter fix

* exposes data for initialization of PaseCommissioner
  • Loading branch information
Apollon77 authored Jul 1, 2024
1 parent df4ae8f commit 367a66c
Show file tree
Hide file tree
Showing 9 changed files with 553 additions and 124 deletions.
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

0 comments on commit 367a66c

Please sign in to comment.