Skip to content

Commit

Permalink
Merge pull request #25 from mfucci/refactor-device
Browse files Browse the repository at this point in the history
Partial implementation of attribute subscription
  • Loading branch information
mfucci authored Sep 27, 2022
2 parents 7faa215 + 02af7b2 commit 9e52d18
Show file tree
Hide file tree
Showing 21 changed files with 434 additions and 206 deletions.
8 changes: 4 additions & 4 deletions src/Main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,12 +65,12 @@ class Main {
))
.addProtocolHandler(Protocol.INTERACTION_MODEL, new InteractionProtocol(new Device([
new Endpoint(0x00, DEVICE.ROOT, [
new BasicCluster({ vendorName, vendorId, productName, productId }),
new GeneralCommissioningCluster(),
new OperationalCredentialsCluster({devicePrivateKey: DevicePrivateKey, deviceCertificate: DeviceCertificate, deviceIntermediateCertificate: ProductIntermediateCertificate, certificateDeclaration: CertificateDeclaration}),
BasicCluster.Builder({ vendorName, vendorId, productName, productId }),
GeneralCommissioningCluster.Builder(),
OperationalCredentialsCluster.Builder({devicePrivateKey: DevicePrivateKey, deviceCertificate: DeviceCertificate, deviceIntermediateCertificate: ProductIntermediateCertificate, certificateDeclaration: CertificateDeclaration}),
]),
new Endpoint(0x01, DEVICE.ON_OFF_LIGHT, [
new OnOffCluster(commandExecutor("on"), commandExecutor("off")),
OnOffCluster.Builder(commandExecutor("on"), commandExecutor("off")),
]),
])))
.start()
Expand Down
9 changes: 7 additions & 2 deletions src/interaction/InteractionMessages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@
import { TlvType } from "../codec/TlvCodec";
import { AnyT, ArrayT, BooleanT, Field, ObjectT, OptionalField, UnsignedIntT, UnsignedLongT } from "../codec/TlvObjectCodec";

export const StatusReport = ObjectT({
status: OptionalField(0, UnsignedIntT),
});

const AttributePathT = ObjectT({
endpointId: OptionalField(2, UnsignedIntT),
clusterId: OptionalField(3, UnsignedIntT),
Expand All @@ -19,7 +23,8 @@ export const ReadRequestT = ObjectT({
interactionModelRevision: Field(0xFF, UnsignedIntT),
});

export const ReadResponseT = ObjectT({
export const DataReportT = ObjectT({
subscriptionId: OptionalField(0, UnsignedIntT),
values: Field(1, ArrayT(ObjectT({
value: Field(1, ObjectT({
version: Field(0, UnsignedIntT),
Expand Down Expand Up @@ -64,7 +69,7 @@ export const SubscribeRequestT = ObjectT({

export const SubscribeResponseT = ObjectT({
subscriptionId: Field(0, UnsignedIntT),
minIntervalFloorSeconds: Field(1, UnsignedIntT),
minIntervalFloorSeconds: OptionalField(1, UnsignedIntT),
maxIntervalCeilingSeconds: Field(2, UnsignedIntT),
});

Expand Down
71 changes: 47 additions & 24 deletions src/interaction/InteractionMessenger.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,13 @@

import { JsType, TlvObjectCodec } from "../codec/TlvObjectCodec";
import { MessageExchange } from "../server/MessageExchange";
import { InvokeRequestT, InvokeResponseT, ReadRequestT, ReadResponseT, SubscribeRequestT, SubscribeResponseT } from "./InteractionMessages";
import { StatusResponseT } from "./cluster/OperationalCredentialsMessages";
import { InvokeRequestT, InvokeResponseT, ReadRequestT, DataReportT, SubscribeRequestT, SubscribeResponseT } from "./InteractionMessages";

export const enum Status {
Success = 0x00,
Failure = 0x01,
}

export const enum MessageType {
StatusResponse = 0x01,
Expand All @@ -22,7 +28,7 @@ export const enum MessageType {
}

export type ReadRequest = JsType<typeof ReadRequestT>;
export type ReadResponse = JsType<typeof ReadResponseT>;
export type DataReport = JsType<typeof DataReportT>;
export type SubscribeRequest = JsType<typeof SubscribeRequestT>;
export type SubscribeResponse = JsType<typeof SubscribeResponseT>;
export type InvokeRequest = JsType<typeof InvokeRequestT>;
Expand All @@ -32,31 +38,48 @@ export class InteractionMessenger {

constructor(
private readonly exchange: MessageExchange,
private readonly handleReadRequest: (request: ReadRequest) => ReadResponse,
private readonly handleSubscribeRequest: (request: SubscribeRequest) => SubscribeResponse,
private readonly handleInvokeRequest: (request: InvokeRequest) => Promise<InvokeResponse>,
) {}

async handleRequest() {
async handleRequest(
handleReadRequest: (request: ReadRequest) => DataReport,
handleSubscribeRequest: (request: SubscribeRequest) => SubscribeResponse | undefined,
handleInvokeRequest: (request: InvokeRequest) => Promise<InvokeResponse>,
) {
const message = await this.exchange.nextMessage();
switch (message.payloadHeader.messageType) {
case MessageType.ReadRequest:
const readRequest = TlvObjectCodec.decode(message.payload, ReadRequestT);
const readResponse = this.handleReadRequest(readRequest);
this.exchange.send(MessageType.ReportData, TlvObjectCodec.encode(readResponse, ReadResponseT));
break;
case MessageType.SubscribeRequest:
const subscribeRequest = TlvObjectCodec.decode(message.payload, SubscribeRequestT);
const subscribeResponse = this.handleSubscribeRequest(subscribeRequest);
this.exchange.send(MessageType.SubscribeResponse, TlvObjectCodec.encode(subscribeResponse, SubscribeResponseT));
break;
case MessageType.InvokeCommandRequest:
const invokeRequest = TlvObjectCodec.decode(message.payload, InvokeRequestT);
const invokeResponse = await this.handleInvokeRequest(invokeRequest);
this.exchange.send(MessageType.InvokeCommandResponse, TlvObjectCodec.encode(invokeResponse, InvokeResponseT));
break;
default:
throw new Error(`Unsupported message type ${message.payloadHeader.messageType}`);
try {
switch (message.payloadHeader.messageType) {
case MessageType.ReadRequest:
const readRequest = TlvObjectCodec.decode(message.payload, ReadRequestT);
this.sendDataReport(handleReadRequest(readRequest));
break;
case MessageType.SubscribeRequest:
const subscribeRequest = TlvObjectCodec.decode(message.payload, SubscribeRequestT);
const subscribeResponse = handleSubscribeRequest(subscribeRequest);
if (subscribeRequest === undefined) {
this.sendStatus(Status.Success);
} else {
this.exchange.send(MessageType.SubscribeResponse, TlvObjectCodec.encode(subscribeResponse, SubscribeResponseT));
}
break;
case MessageType.InvokeCommandRequest:
const invokeRequest = TlvObjectCodec.decode(message.payload, InvokeRequestT);
const invokeResponse = await handleInvokeRequest(invokeRequest);
this.exchange.send(MessageType.InvokeCommandResponse, TlvObjectCodec.encode(invokeResponse, InvokeResponseT));
break;
default:
throw new Error(`Unsupported message type ${message.payloadHeader.messageType}`);
}
} catch (error) {
console.error(error);
this.sendStatus(Status.Failure);
}
}

sendDataReport(dataReport: DataReport) {
this.exchange.send(MessageType.ReportData, TlvObjectCodec.encode(dataReport, DataReportT));
}

private sendStatus(status: Status) {
this.exchange.send(MessageType.StatusResponse, TlvObjectCodec.encode({status}, StatusResponseT));
}
}
76 changes: 61 additions & 15 deletions src/interaction/InteractionProtocol.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,45 +5,58 @@
*/

import { Device } from "./model/Device";
import { ProtocolHandler } from "../server/MatterServer";
import { ExchangeSocket, MatterServer, Protocol, ProtocolHandler } from "../server/MatterServer";
import { MessageExchange } from "../server/MessageExchange";
import { InteractionMessenger, InvokeRequest, InvokeResponse, ReadRequest, ReadResponse, SubscribeRequest, SubscribeResponse } from "./InteractionMessenger";
import { InteractionMessenger, InvokeRequest, InvokeResponse, ReadRequest, DataReport, SubscribeRequest, SubscribeResponse } from "./InteractionMessenger";
import { SecureSession } from "../session/SecureSession";
import { Attribute, Report } from "./model/Attribute";
import { Session } from "../session/Session";

export class InteractionProtocol implements ProtocolHandler {
constructor(
private readonly device: Device,
) {}

async onNewExchange(exchange: MessageExchange) {
const messenger = new InteractionMessenger(
exchange,
await new InteractionMessenger(exchange).handleRequest(
readRequest => this.handleReadRequest(exchange, readRequest),
subscribeRequest => this.handleSubscribeRequest(exchange, subscribeRequest),
invokeRequest => this.handleInvokeRequest(exchange, invokeRequest),
);
await messenger.handleRequest();
}

handleReadRequest(exchange: MessageExchange, {attributes}: ReadRequest): ReadResponse {
console.log(`Received read request from ${exchange.channel.getName()}: ${attributes.map(({endpointId = "*", clusterId = "*", attributeId = "*"}) => `${endpointId}/${clusterId}/${attributeId}`).join(", ")}`);
handleReadRequest(exchange: MessageExchange, {attributes: attributePaths}: ReadRequest): DataReport {
console.log(`Received read request from ${exchange.channel.getName()}: ${attributePaths.map(({endpointId = "*", clusterId = "*", attributeId = "*"}) => `${endpointId}/${clusterId}/${attributeId}`).join(", ")}`);

return {
isFabricFiltered: true,
interactionModelRevision: 1,
values: attributes.flatMap(path => this.device.getAttributeValues(path)).map(value => ({value})),
values: attributePaths.flatMap(path => this.device.getAttributes(path)).map(attribute => ({ value: attribute.getValue() })),
};
}

handleSubscribeRequest(exchange: MessageExchange, { minIntervalFloorSeconds, maxIntervalCeilingSeconds }: SubscribeRequest): SubscribeResponse {
handleSubscribeRequest(exchange: MessageExchange, { minIntervalFloorSeconds, maxIntervalCeilingSeconds, attributeRequests, keepSubscriptions }: SubscribeRequest): SubscribeResponse | undefined {
console.log(`Received subscribe request from ${exchange.channel.getName()}`);

// TODO: implement this
if (!exchange.session.isSecure()) throw new Error("Subscriptions are only implemented on secure sessions");

return {
subscriptionId: 0,
minIntervalFloorSeconds,
maxIntervalCeilingSeconds,
};
const session = exchange.session as SecureSession;

if (!keepSubscriptions) {
session.clearSubscriptions();
}

if (attributeRequests !== undefined) {
const attributes = attributeRequests.flatMap(path => this.device.getAttributes(path));

if (attributeRequests.length === 0) throw new Error("Invalid subscription request");

return {
subscriptionId: session.addSubscription(SubscriptionHandler.Builder(session, exchange.channel.channel, session.getServer(), attributes)),
minIntervalFloorSeconds,
maxIntervalCeilingSeconds,
};
}
}

async handleInvokeRequest(exchange: MessageExchange, {invokes}: InvokeRequest): Promise<InvokeResponse> {
Expand All @@ -63,3 +76,36 @@ export class InteractionProtocol implements ProtocolHandler {
};
}
}

export class SubscriptionHandler {

static Builder = (session: Session, channel: ExchangeSocket<Buffer>, server: MatterServer, attributes: Attribute<any>[]) => (subscriptionId: number) => new SubscriptionHandler(subscriptionId, session, channel, server, attributes);

constructor(
readonly subscriptionId: number,
private readonly session: Session,
private readonly channel: ExchangeSocket<Buffer>,
private readonly server: MatterServer,
private readonly attributes: Attribute<any>[],
) {
// TODO: implement minIntervalFloorSeconds and maxIntervalCeilingSeconds

attributes.forEach(attribute => attribute.addSubscription(this));
}

sendReport(report: Report) {
// TODO: this should be sent to the last discovered address of this node instead of the one used to request the subscription

const exchange = this.server.initiateExchange(this.session, this.channel, Protocol.INTERACTION_MODEL);
new InteractionMessenger(exchange).sendDataReport({
subscriptionId: this.subscriptionId,
isFabricFiltered: true,
interactionModelRevision: 1,
values: [{ value: report }],
});
}

cancel() {
this.attributes.forEach(attribute => attribute.removeSubscription(this.subscriptionId));
}
}
18 changes: 9 additions & 9 deletions src/interaction/cluster/BasicCluster.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@
*/

import { StringT, UnsignedIntT } from "../../codec/TlvObjectCodec";
import { Attribute } from "../model/Attribute";
import { Cluster } from "../model/Cluster";

interface BasicClusterConf {
Expand All @@ -16,17 +15,18 @@ interface BasicClusterConf {
}

export class BasicCluster extends Cluster {
constructor({ vendorName, vendorId, productName, productId }: BasicClusterConf) {
static Builder = (conf: BasicClusterConf) => (endpointId: number) => new BasicCluster(endpointId, conf);

constructor(endpointId: number, { vendorName, vendorId, productName, productId }: BasicClusterConf) {
super(
endpointId,
0x28,
"Basic",
[],
[
new Attribute(1, "VendorName", StringT, vendorName),
new Attribute(2, "VendorID", UnsignedIntT, vendorId),
new Attribute(3, "ProductName", StringT, productName),
new Attribute(4, "ProductID", UnsignedIntT, productId),
],
);

this.addAttribute(1, "VendorName", StringT, vendorName);
this.addAttribute(2, "VendorID", UnsignedIntT, vendorId);
this.addAttribute(3, "ProductName", StringT, productName);
this.addAttribute(4, "ProductID", UnsignedIntT, productId);
}
}
34 changes: 18 additions & 16 deletions src/interaction/cluster/DescriptorCluster.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,30 +5,32 @@
*/

import { ArrayT, Field, ObjectT, UnsignedIntT } from "../../codec/TlvObjectCodec";
import { Attribute } from "../model/Attribute";
import { Cluster } from "../model/Cluster";
import { Endpoint } from "../model/Endpoint";

const CLUSTER_ID = 0x1d;

export class DescriptorCluster extends Cluster {
constructor(endpoint: Endpoint, allEndpoints: Endpoint[]) {
static Builder = (allEndpoints: Endpoint[]) => (endpointId: number) => new DescriptorCluster(endpointId, allEndpoints);

constructor(endpointId: number, allEndpoints: Endpoint[]) {
super(
CLUSTER_ID,
endpointId,
0x1d,
"Descriptor",
[],
[
new Attribute(0, "DeviceList", ArrayT(ObjectT({
type: Field(0, UnsignedIntT),
revision: Field(1, UnsignedIntT),
})), [{
type: endpoint.device.code,
revision: 1,
}]),
new Attribute(1, "ServerList", ArrayT(UnsignedIntT), [CLUSTER_ID, ...endpoint.getClusterIds()]),
new Attribute(3, "ClientList", ArrayT(UnsignedIntT), []),
new Attribute(4, "PartsList", ArrayT(UnsignedIntT), endpoint.id === 0 ? allEndpoints.map(endpoint => endpoint.id).filter(endpointId => endpointId !== 0) : []),
],
);
const endpoint = allEndpoints.find(endpoint => endpoint.id === endpointId);
if (endpoint === undefined) throw new Error(`Endpoint with id ${endpointId} doesn't exist`);

this.addAttribute(0, "DeviceList", ArrayT(ObjectT({
type: Field(0, UnsignedIntT),
revision: Field(1, UnsignedIntT),
})), [{
type: endpoint.device.code,
revision: 1,
}]);
this.addAttribute(1, "ServerList", ArrayT(UnsignedIntT), [CLUSTER_ID, ...endpoint.getClusterIds()]);
this.addAttribute(3, "ClientList", ArrayT(UnsignedIntT), []);
this.addAttribute(4, "PartsList", ArrayT(UnsignedIntT), endpointId === 0 ? allEndpoints.map(endpoint => endpoint.id).filter(endpointId => endpointId !== 0) : []);
}
}
31 changes: 16 additions & 15 deletions src/interaction/cluster/GeneralCommissioningCluster.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,8 @@
*/

import { Cluster } from "../model/Cluster";
import { Attribute } from "../model/Attribute";
import { Field, JsType, ObjectT, StringT, UnsignedIntT } from "../../codec/TlvObjectCodec";
import { Command, NoArgumentsT } from "../model/Command";
import { NoArgumentsT } from "../model/Command";

const enum RegulatoryLocationType {
Indoor = 0,
Expand Down Expand Up @@ -50,25 +49,27 @@ type SuccessFailureReponse = JsType<typeof SuccessFailureReponseT>;
const SuccessResponse = {errorCode: CommissioningError.Ok, debugText: ""};

export class GeneralCommissioningCluster extends Cluster {
private readonly attributes = {
breadcrumb: new Attribute(0, "Breadcrumb", UnsignedIntT, 0),
comminssioningInfo: new Attribute(1, "BasicCommissioningInfo", BasicCommissioningInfoT, {failSafeExpiryLengthSeconds: 60 /* 1mn */}),
regulatoryConfig: new Attribute(2, "RegulatoryConfig", UnsignedIntT, RegulatoryLocationType.Indoor),
locationCapability: new Attribute(3, "LocationCapability", UnsignedIntT, RegulatoryLocationType.IndoorOutdoor),
}
static Builder = () => (endpointId: number) => new GeneralCommissioningCluster(endpointId);

private readonly attributes;

constructor() {
constructor(endpointId: number) {
super(
endpointId,
0x30,
"General Commissioning",
[
new Command(0, 1, "ArmFailSafe", ArmFailSafeRequestT, SuccessFailureReponseT, request => this.handleArmFailSafeRequest(request)),
new Command(2, 3, "SetRegulatoryConfig", SetRegulatoryConfigRequestT, SuccessFailureReponseT, request => this.setRegulatoryConfig(request)),
new Command(4, 5, "CommissioningComplete", NoArgumentsT, SuccessFailureReponseT, () => this.handleCommissioningComplete()),
],
);

this.addAttributes(Object.values(this.attributes));
this.addCommand(0, 1, "ArmFailSafe", ArmFailSafeRequestT, SuccessFailureReponseT, request => this.handleArmFailSafeRequest(request));
this.addCommand(2, 3, "SetRegulatoryConfig", SetRegulatoryConfigRequestT, SuccessFailureReponseT, request => this.setRegulatoryConfig(request));
this.addCommand(4, 5, "CommissioningComplete", NoArgumentsT, SuccessFailureReponseT, () => this.handleCommissioningComplete());

this.attributes = {
breadcrumb: this.addAttribute(0, "Breadcrumb", UnsignedIntT, 0),
comminssioningInfo: this.addAttribute(1, "BasicCommissioningInfo", BasicCommissioningInfoT, {failSafeExpiryLengthSeconds: 60 /* 1mn */}),
regulatoryConfig: this.addAttribute(2, "RegulatoryConfig", UnsignedIntT, RegulatoryLocationType.Indoor),
locationCapability: this.addAttribute(3, "LocationCapability", UnsignedIntT, RegulatoryLocationType.IndoorOutdoor),
};
}

private handleArmFailSafeRequest({breadcrumb}: ArmFailSafeRequest): SuccessFailureReponse {
Expand Down
Loading

0 comments on commit 9e52d18

Please sign in to comment.