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

fix!: More Green Power fixes #21

Merged
merged 5 commits into from
Mar 21, 2025
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
12 changes: 6 additions & 6 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "zigbee-on-host",
"version": "0.1.4",
"version": "0.1.5",
"description": "ZigBee stack designed to run on a host and communicate with a radio co-processor (RCP)",
"engines": {
"node": ">=20.17.0"
Expand Down Expand Up @@ -35,7 +35,7 @@
"homepage": "https://github.com/Nerivec/zigbee-on-host#readme",
"devDependencies": {
"@biomejs/biome": "1.9.4",
"@types/node": "^22.13.10",
"@types/node": "^22.13.11",
"@vitest/coverage-v8": "^3.0.9",
"serialport": "^13.0.0",
"typescript": "^5.8.2",
Expand Down
1 change: 1 addition & 0 deletions src/dev/minimal-adapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -222,6 +222,7 @@ export class MinimalAdapter {
await this.driver.formNetwork();
// allow joins on start for 254 seconds
this.driver.allowJoins(0xfe, true);
this.driver.gpEnterCommissioningMode(0xfe);

this.driver.on("frame", this.onFrame.bind(this));
this.driver.on("deviceJoined", this.onDeviceJoined.bind(this));
Expand Down
131 changes: 107 additions & 24 deletions src/drivers/ot-rcp-driver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@ import {
encodeZigbeeNWKFrame,
} from "../zigbee/zigbee-nwk.js";
import {
ZigbeeNWKGPCommandId,
ZigbeeNWKGPFrameType,
type ZigbeeNWKGPHeader,
decodeZigbeeNWKGPFrameControl,
Expand Down Expand Up @@ -723,7 +724,14 @@ export class OTRCPDriver extends EventEmitter<AdapterDriverEventMap> {

private readonly trustCenterPolicies: TrustCenterPolicies;
private macAssociationPermit: boolean;
private permitJoinTimeout: NodeJS.Timeout | undefined;
private allowJoinTimeout: NodeJS.Timeout | undefined;

//----- Green Power (see 14-0563-18)

private gpCommissioningMode: boolean;
private gpCommissioningWindowTimeout: NodeJS.Timeout | undefined;
private gpLastMACSequenceNumber: number;
private gpLastSecurityFrameCounter: number;

//---- NWK

Expand Down Expand Up @@ -794,6 +802,11 @@ export class OTRCPDriver extends EventEmitter<AdapterDriverEventMap> {
};
this.macAssociationPermit = false;

//---- Green Power
this.gpCommissioningMode = false;
this.gpLastMACSequenceNumber = -1;
this.gpLastSecurityFrameCounter = -1;

//---- NWK
this.netParams = netParams;
this.tcVerifyKeyHash = Buffer.alloc(0); // set by `loadState`
Expand Down Expand Up @@ -934,6 +947,7 @@ export class OTRCPDriver extends EventEmitter<AdapterDriverEventMap> {
logger.info("======== Driver stopping ========", NS);

this.disallowJoins();
this.gpExitCommissioningMode();

// pre-emptive
this.networkUp = false;
Expand Down Expand Up @@ -1101,26 +1115,33 @@ export class OTRCPDriver extends EventEmitter<AdapterDriverEventMap> {
const [nwkGPFCF, nwkGPFCFOutOffset] = decodeZigbeeNWKGPFrameControl(macPayload, 0);
const [nwkGPHeader, nwkGPHOutOffset] = decodeZigbeeNWKGPHeader(macPayload, nwkGPFCFOutOffset, nwkGPFCF);

if (nwkGPHeader.sourceId === undefined) {
logger.debug(() => "<-~- NWKGP Ignoring frame without srcId", NS);
if (
nwkGPHeader.frameControl.frameType !== ZigbeeNWKGPFrameType.DATA &&
nwkGPHeader.frameControl.frameType !== ZigbeeNWKGPFrameType.MAINTENANCE
) {
logger.debug(() => `<-~- NWKGP Ignoring frame with type ${nwkGPHeader.frameControl.frameType}`, NS);
return;
}

if (nwkGPHeader.frameControl.frameType === ZigbeeNWKGPFrameType.DATA) {
const nwkGPPayload = decodeZigbeeNWKGPPayload(
macPayload,
nwkGPHOutOffset,
this.netParams.networkKey,
macHeader.source64,
nwkGPFCF,
nwkGPHeader,
if (this.checkZigbeeNWKGPDuplicate(macHeader, nwkGPHeader)) {
logger.debug(
() =>
`<-~- NWKGP Ignoring duplicate frame macSeqNum=${macHeader.sequenceNumber} nwkGPFC=${nwkGPHeader.securityFrameCounter}`,
NS,
);

this.processZigbeeNWKGPDataFrame(nwkGPPayload, macHeader, nwkGPHeader, metadata?.rssi ?? 0);
} else {
logger.debug(() => `<-~- NWKGP Ignoring frame with type ${nwkGPHeader.frameControl.frameType}`, NS);
return;
}

const nwkGPPayload = decodeZigbeeNWKGPPayload(
macPayload,
nwkGPHOutOffset,
this.netParams.networkKey,
macHeader.source64,
nwkGPFCF,
nwkGPHeader,
);

this.processZigbeeNWKGPFrame(nwkGPPayload, macHeader, nwkGPHeader, metadata?.rssi ?? 0);
} else {
logger.debug(() => `<-x- NWKGP Invalid frame addressing ${macFCF.destAddrMode} (${macHeader.destination16})`, NS);
return;
Expand Down Expand Up @@ -2538,16 +2559,50 @@ export class OTRCPDriver extends EventEmitter<AdapterDriverEventMap> {

// #region Zigbee NWK GP layer

public processZigbeeNWKGPDataFrame(data: Buffer, macHeader: MACHeader, nwkHeader: ZigbeeNWKGPHeader, rssi: number): void {
public checkZigbeeNWKGPDuplicate(macHeader: MACHeader, nwkHeader: ZigbeeNWKGPHeader): boolean {
let duplicate = false;

if (nwkHeader.securityFrameCounter !== undefined) {
if (nwkHeader.securityFrameCounter === this.gpLastSecurityFrameCounter) {
duplicate = true;
}

this.gpLastSecurityFrameCounter = nwkHeader.securityFrameCounter;
} else if (macHeader.sequenceNumber !== undefined) {
if (macHeader.sequenceNumber === this.gpLastMACSequenceNumber) {
duplicate = true;
}

this.gpLastMACSequenceNumber = macHeader.sequenceNumber;
}

return duplicate;
}

/**
* See 14-0563-19 #A.3.8.2
* @param data
* @param macHeader
* @param nwkHeader
* @param rssi
* @returns
*/
public processZigbeeNWKGPFrame(data: Buffer, macHeader: MACHeader, nwkHeader: ZigbeeNWKGPHeader, rssi: number): void {
let offset = 0;
const cmdId = data.readUInt8(offset);
offset += 1;
const framePayload = data.subarray(offset);

logger.debug(
() => `<=== NWKGP[cmdId=${cmdId} dstPANId=${macHeader.destinationPANId} dst64=${macHeader.destination64} srcId=${nwkHeader.sourceId}]`,
NS,
);
if (
!this.gpCommissioningMode &&
(cmdId === ZigbeeNWKGPCommandId.COMMISSIONING || cmdId === ZigbeeNWKGPCommandId.SUCCESS || cmdId === ZigbeeNWKGPCommandId.CHANNEL_REQUEST)
) {
logger.debug(() => `<=~= NWKGP[cmdId=${cmdId} src=${nwkHeader.sourceId}:${macHeader.source64}] Not in commissioning mode`, NS);

return;
}

logger.debug(() => `<=== NWKGP[cmdId=${cmdId} src=${nwkHeader.sourceId}:${macHeader.source64}]`, NS);

setImmediate(() => {
this.emit("gpFrame", cmdId, framePayload, macHeader, nwkHeader, rssi);
Expand Down Expand Up @@ -4020,16 +4075,18 @@ export class OTRCPDriver extends EventEmitter<AdapterDriverEventMap> {
* @param duration The length of time in seconds during which the trust center will allow joins.
* The value 0x00 and 0xff indicate that permission is disabled or enabled, respectively, without a specified time limit.
* 0xff is clamped to 0xfe for security reasons
* @param macAssociationPermit If true, also allow association on coordinator itself. If false, only change TC policies
* @param macAssociationPermit If true, also allow association on coordinator itself.
*/
public allowJoins(duration: number, macAssociationPermit: boolean): void {
if (duration > 0) {
clearTimeout(this.permitJoinTimeout);
clearTimeout(this.allowJoinTimeout);
this.trustCenterPolicies.allowJoins = true;
this.trustCenterPolicies.allowRejoinsWithWellKnownKey = true;
this.macAssociationPermit = macAssociationPermit;

this.permitJoinTimeout = setTimeout(this.disallowJoins.bind(this), Math.min(duration, 0xfe) * 1000);
this.allowJoinTimeout = setTimeout(this.disallowJoins.bind(this), Math.min(duration, 0xfe) * 1000);

logger.info(`Allowed joins for ${duration} seconds (self=${macAssociationPermit})`, NS);
} else {
this.disallowJoins();
}
Expand All @@ -4039,11 +4096,37 @@ export class OTRCPDriver extends EventEmitter<AdapterDriverEventMap> {
* Revert allowing joins (keeps `allowRejoinsWithWellKnownKey=true`).
*/
public disallowJoins(): void {
clearTimeout(this.permitJoinTimeout);
clearTimeout(this.allowJoinTimeout);

this.trustCenterPolicies.allowJoins = false;
this.trustCenterPolicies.allowRejoinsWithWellKnownKey = true;
this.macAssociationPermit = false;

logger.info("Disallowed joins", NS);
}

/**
* Put the coordinator in Green Power commissioning mode.
* @param commissioningWindow Defaults to 180 if unspecified. Max 254
*/
public gpEnterCommissioningMode(commissioningWindow = 180): void {
if (commissioningWindow > 0) {
this.gpCommissioningMode = true;

this.gpCommissioningWindowTimeout = setTimeout(this.gpExitCommissioningMode.bind(this), Math.min(commissioningWindow, 0xfe) * 1000);

logger.info(`Entered Green Power commissioning mode for ${commissioningWindow} seconds`, NS);
} else {
this.gpExitCommissioningMode();
}
}

public gpExitCommissioningMode(): void {
clearTimeout(this.gpCommissioningWindowTimeout);

this.gpCommissioningMode = false;

logger.info("Exited Green Power commissioning mode", NS);
}

/**
Expand Down
45 changes: 45 additions & 0 deletions test/data.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2040,3 +2040,48 @@ export const NET4_ROUTE_RECORD_FROM_4B8E_RELAY_CB47 = Buffer.from([
]);

// #endregion

// #region

// GP-stuff from `ember` Zigbee2MQTT 2.1.3-dev (commit #ef5b3de5)

// export const NET5_TC_KEY = Buffer.from([0x5a, 0x69, 0x67, 0x42, 0x65, 0x65, 0x41, 0x6c, 0x6c, 0x69, 0x61, 0x6e, 0x63, 0x65, 0x30, 0x39]);
export const NET5_PAN_ID = 0xe6b4;
export const NET5_EXTENDED_PAN_ID = Buffer.from([0xdd, 0xdd, 0xdd, 0xdd, 0xdd, 0xdd, 0xdd, 0xdd]);
export const NET5_NETWORK_KEY = Buffer.from([0x43, 0xa3, 0x0b, 0xe5, 0x3f, 0xee, 0xd5, 0x21, 0x04, 0xfd, 0x82, 0xd6, 0x57, 0xa3, 0xcb, 0x4a]);
export const NET5_COORD_EUI64 = Buffer.from([0x6c, 0x5c, 0xb1, 0xff, 0xfe, 0xfc, 0x78, 0x0a]);

/**
* IEEE 802.15.4 Data, Dst: Broadcast
* Frame Control Field: 0x0801, Frame Type: Data, Destination Addressing Mode: Short/16-bit, Frame Version: IEEE Std 802.15.4-2003, Source Addressing Mode: None
* .... .... .... .001 = Frame Type: Data (0x1)
* .... .... .... 0... = Security Enabled: False
* .... .... ...0 .... = Frame Pending: False
* .... .... ..0. .... = Acknowledge Request: False
* .... .... .0.. .... = PAN ID Compression: False
* .... .... 0... .... = Reserved: False
* .... ...0 .... .... = Sequence Number Suppression: False
* .... ..0. .... .... = Information Elements Present: False
* .... 10.. .... .... = Destination Addressing Mode: Short/16-bit (0x2)
* ..00 .... .... .... = Frame Version: IEEE Std 802.15.4-2003 (0)
* 00.. .... .... .... = Source Addressing Mode: None (0x0)
* Sequence Number: 1
* Destination PAN: 0xffff
* Destination: 0xffff
* FCS: 0x7808 (Correct)
*
* ZGP stub NWK header Maintenance
* Frame Control Field: 0x4d, Frame Type: Maintenance, Auto Commissioning Maintenance
* .... ..01 = Frame Type: Maintenance (0x1)
* ..00 11.. = Protocol Version: 3
* .1.. .... = Auto Commissioning: True
* 0... .... = NWK Frame Extension: False
* Command Frame: Channel Request
* ZGPD Command ID: Channel Request (0xe3)
* Channel Toggling Behaviour: 0x85
* .... 0101 = Rx channel in the next attempt: 0x5
* 1000 .... = Rx channel in the second next attempt: 0x8
*/
export const NET5_GP_CHANNEL_REQUEST_BCAST = Buffer.from([0x1, 0x8, 0x1, 0xff, 0xff, 0xff, 0xff, 0x4d, 0xe3, 0x85, 0x8, 0x78]);

// #endregion
Loading