Skip to content

Commit

Permalink
fix: ZDO spec: improve build/read logic and typing (#1186)
Browse files Browse the repository at this point in the history
  • Loading branch information
Nerivec authored Sep 14, 2024
1 parent 4fa371d commit c8cb557
Show file tree
Hide file tree
Showing 23 changed files with 1,694 additions and 1,254 deletions.
3 changes: 2 additions & 1 deletion src/adapter/adapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ interface AdapterEventMap {
}

abstract class Adapter extends events.EventEmitter<AdapterEventMap> {
public readonly greenPowerGroup = 0x0b84;
public hasZdoMessageOverhead: boolean;
protected networkOptions: TsType.NetworkOptions;
protected adapterOptions: TsType.AdapterOptions;
protected serialPortOptions: TsType.SerialPortOptions;
Expand All @@ -34,6 +34,7 @@ abstract class Adapter extends events.EventEmitter<AdapterEventMap> {
adapterOptions: TsType.AdapterOptions,
) {
super();
this.hasZdoMessageOverhead = true;
this.networkOptions = networkOptions;
this.adapterOptions = adapterOptions;
this.serialPortOptions = serialPortOptions;
Expand Down
1 change: 1 addition & 0 deletions src/adapter/deconz/adapter/deconzAdapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ class DeconzAdapter extends Adapter {

public constructor(networkOptions: NetworkOptions, serialPortOptions: SerialPortOptions, backupPath: string, adapterOptions: AdapterOptions) {
super(networkOptions, serialPortOptions, backupPath, adapterOptions);
this.hasZdoMessageOverhead = true;

const concurrent = this.adapterOptions && this.adapterOptions.concurrent ? this.adapterOptions.concurrent : 2;

Expand Down
137 changes: 61 additions & 76 deletions src/adapter/ember/adapter/emberAdapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@ import * as ZSpec from '../../../zspec';
import {EUI64, ExtendedPanId, NodeId, PanId} from '../../../zspec/tstypes';
import * as Zcl from '../../../zspec/zcl';
import * as Zdo from '../../../zspec/zdo';
import {BuffaloZdo} from '../../../zspec/zdo/buffaloZdo';
import * as ZdoTypes from '../../../zspec/zdo/definition/tstypes';
import {DeviceAnnouncePayload, DeviceJoinedPayload, DeviceLeavePayload, NetworkAddressPayload, ZclPayload} from '../../events';
import SerialPortUtils from '../../serialPortUtils';
Expand Down Expand Up @@ -523,9 +522,9 @@ export class EmberAdapter extends Adapter {
* @param messageContents The content of the response.
*/
private async onZDOResponse(apsFrame: EmberApsFrame, sender: NodeId, messageContents: Buffer): Promise<void> {
try {
const payload = BuffaloZdo.readResponse(apsFrame.clusterId, messageContents, true);
const [status, payload] = Zdo.Buffalo.readResponse(this.hasZdoMessageOverhead, apsFrame.clusterId, messageContents);

if (status === Zdo.Status.SUCCESS) {
logger.debug(() => `<~~~ [ZDO ${Zdo.ClusterId[apsFrame.clusterId]} from=${sender} ${payload ? JSON.stringify(payload) : 'OK'}]`, NS);
this.oneWaitress.resolveZDO(sender, apsFrame, payload);

Expand All @@ -540,8 +539,8 @@ export class EmberAdapter extends Adapter {
ieeeAddr: (payload as ZdoTypes.EndDeviceAnnounce).eui64,
} as DeviceAnnouncePayload);
}
} catch (error) {
this.oneWaitress.resolveZDO(sender, apsFrame, error);
} else {
this.oneWaitress.resolveZDO(sender, apsFrame, new Zdo.StatusError(status));
}
}

Expand Down Expand Up @@ -1513,43 +1512,6 @@ export class EmberAdapter extends Adapter {
return [status, reContext?.result];
}

/**
* Enable local permit join and optionally broadcast the ZDO Mgmt_Permit_Join_req message.
* This API can be called from any device type and still return EMBER_SUCCESS.
* If the API is called from an end device, the permit association bit will just be left off.
*
* @param duration uint8_t The duration that the permit join bit will remain on
* and other devices will be able to join the current network.
* @param broadcastMgmtPermitJoin whether or not to broadcast the ZDO Mgmt_Permit_Join_req message.
*
* @returns status of whether or not permit join was enabled.
* @returns apsFrame Will be null if not broadcasting.
* @returns messageTag The tag passed to ezspSend${x} function.
*/
private async emberPermitJoining(
duration: number,
broadcastMgmtPermitJoin: boolean,
): Promise<[SLStatus, apsFrame: EmberApsFrame | undefined, messageTag: number | undefined]> {
let status = await this.ezsp.ezspPermitJoining(duration);
let apsFrame: EmberApsFrame | undefined;
let messageTag: number | undefined;

logger.debug(`Permit joining for ${duration} sec. status=${[status]}`, NS);

if (broadcastMgmtPermitJoin) {
// `authentication`: TC significance always 1 (zb specs)
const zdoPayload = BuffaloZdo.buildPermitJoining(duration, 1, []);
[status, apsFrame, messageTag] = await this.sendZDORequest(
ZSpec.BroadcastAddress.DEFAULT,
Zdo.ClusterId.PERMIT_JOINING_REQUEST,
zdoPayload,
DEFAULT_APS_OPTIONS,
);
}

return [status, apsFrame, messageTag];
}

/**
* Set the trust center policy bitmask using decision.
* @param decision
Expand Down Expand Up @@ -1854,7 +1816,15 @@ export class EmberAdapter extends Adapter {
return await this.queue.execute<void>(async () => {
this.checkInterpanLock();

const zdoPayload = BuffaloZdo.buildChannelChangeRequest(newChannel, null);
const zdoPayload = Zdo.Buffalo.buildRequest(
this.hasZdoMessageOverhead,
Zdo.ClusterId.NWK_UPDATE_REQUEST,
[newChannel],
0xfe,
undefined,
undefined,
undefined,
);
const [status] = await this.sendZDORequest(
ZSpec.BroadcastAddress.SLEEPY,
Zdo.ClusterId.NWK_UPDATE_REQUEST,
Expand Down Expand Up @@ -2012,7 +1982,7 @@ export class EmberAdapter extends Adapter {
await preJoining();

// `authentication`: TC significance always 1 (zb specs)
const zdoPayload = BuffaloZdo.buildPermitJoining(seconds, 1, []);
const zdoPayload = Zdo.Buffalo.buildRequest(this.hasZdoMessageOverhead, Zdo.ClusterId.PERMIT_JOINING_REQUEST, seconds, 1, []);
const [status, apsFrame] = await this.sendZDORequest(
networkAddress,
Zdo.ClusterId.PERMIT_JOINING_REQUEST,
Expand All @@ -2034,35 +2004,36 @@ export class EmberAdapter extends Adapter {
);
});
} else {
// coordinator-only, or all
// coordinator-only (0), or all
return await this.queue.execute<void>(async () => {
this.checkInterpanLock();
await preJoining();

// local permit join if `Coordinator`-only requested, else local + broadcast
const [status] = await this.emberPermitJoining(seconds, networkAddress === ZSpec.COORDINATOR_ADDRESS ? false : true);
const status = await this.ezsp.ezspPermitJoining(seconds);

if (status !== SLStatus.OK) {
throw new Error(`[ZDO] Failed permit joining request with status=${SLStatus[status]}.`);
throw new Error(`[ZDO] Failed coordinator permit joining request with status=${SLStatus[status]}.`);
}

// NOTE: because Z2M is refreshing the permit join duration early to prevent it from closing
// (every 200sec, even if only opened for 254sec), we can't wait for the stack opened status,
// as it won't trigger again if already opened... so instead we assume it worked
// NOTE2: with EZSP, 255=forever, and 254=max, but since upstream logic uses fixed 254 with interval refresh,
// we can't simply bypass upstream calls if called for "forever" to prevent useless NCP calls (3-4 each time),
// until called with 0 (disable), since we don't know if it was requested for forever or not...
// TLDR: upstream logic change required to allow this
// if (seconds) {
// await this.oneWaitress.startWaitingForEvent(
// {eventName: OneWaitressEvents.STACK_STATUS_NETWORK_OPENED},
// DEFAULT_ZCL_REQUEST_TIMEOUT,
// '[ZDO] Permit Joining',
// );
// } else {
// // NOTE: CLOSED stack status is not triggered if the network was not OPENED in the first place, so don't wait for it
// // same kind of problem as described above (upstream always tries to close after start, but EZSP already is)
// }
logger.debug(`Permit joining on coordinator for ${seconds} sec.`, NS);

// broadcast permit joining ZDO
if (networkAddress === undefined) {
// `authentication`: TC significance always 1 (zb specs)
const zdoPayload = Zdo.Buffalo.buildRequest(this.hasZdoMessageOverhead, Zdo.ClusterId.PERMIT_JOINING_REQUEST, seconds, 1, []);

const [bcStatus] = await this.sendZDORequest(
ZSpec.BroadcastAddress.DEFAULT,
Zdo.ClusterId.PERMIT_JOINING_REQUEST,
zdoPayload,
DEFAULT_APS_OPTIONS,
);

if (bcStatus !== SLStatus.OK) {
// don't throw, coordinator succeeded at least
logger.error(`[ZDO] Failed broadcast permit joining request with status=${SLStatus[bcStatus]}.`, NS);
}
}
});
}
}
Expand All @@ -2074,7 +2045,7 @@ export class EmberAdapter extends Adapter {

const neighbors: TsType.LQINeighbor[] = [];
const request = async (startIndex: number): Promise<[tableEntries: number, entryCount: number]> => {
const zdoPayload = BuffaloZdo.buildLqiTableRequest(startIndex);
const zdoPayload = Zdo.Buffalo.buildRequest(this.hasZdoMessageOverhead, Zdo.ClusterId.LQI_TABLE_REQUEST, startIndex);
const [status, apsFrame] = await this.sendZDORequest(
networkAddress,
Zdo.ClusterId.LQI_TABLE_REQUEST,
Expand Down Expand Up @@ -2130,7 +2101,7 @@ export class EmberAdapter extends Adapter {

const table: TsType.RoutingTableEntry[] = [];
const request = async (startIndex: number): Promise<[tableEntries: number, entryCount: number]> => {
const zdoPayload = BuffaloZdo.buildRoutingTableRequest(startIndex);
const zdoPayload = Zdo.Buffalo.buildRequest(this.hasZdoMessageOverhead, Zdo.ClusterId.ROUTING_TABLE_REQUEST, startIndex);
const [status, apsFrame] = await this.sendZDORequest(
networkAddress,
Zdo.ClusterId.ROUTING_TABLE_REQUEST,
Expand All @@ -2156,7 +2127,7 @@ export class EmberAdapter extends Adapter {
for (const entry of result.entryList) {
table.push({
destinationAddress: entry.destinationAddress,
status: TsType.RoutingTableStatus[entry.status], // get str value from enum to satisfy upstream's needs
status: entry.status,
nextHop: entry.nextHopAddress,
});
}
Expand Down Expand Up @@ -2184,7 +2155,7 @@ export class EmberAdapter extends Adapter {
return await this.queue.execute<TsType.NodeDescriptor>(async () => {
this.checkInterpanLock();

const zdoPayload = BuffaloZdo.buildNodeDescriptorRequest(networkAddress);
const zdoPayload = Zdo.Buffalo.buildRequest(this.hasZdoMessageOverhead, Zdo.ClusterId.NODE_DESCRIPTOR_REQUEST, networkAddress);
const [status, apsFrame] = await this.sendZDORequest(
networkAddress,
Zdo.ClusterId.NODE_DESCRIPTOR_REQUEST,
Expand Down Expand Up @@ -2220,9 +2191,9 @@ export class EmberAdapter extends Adapter {
}

/* istanbul ignore else */
if (result.serverMask.stackComplianceResivion < CURRENT_ZIGBEE_SPEC_REVISION) {
if (result.serverMask.stackComplianceRevision < CURRENT_ZIGBEE_SPEC_REVISION) {
// always 0 before rev. 21 where field was added
const rev = result.serverMask.stackComplianceResivion < 21 ? 'pre-21' : result.serverMask.stackComplianceResivion;
const rev = result.serverMask.stackComplianceRevision < 21 ? 'pre-21' : result.serverMask.stackComplianceRevision;

logger.warning(
`[ZDO] Device '${networkAddress}' is only compliant to revision '${rev}' of the ZigBee specification (current revision: ${CURRENT_ZIGBEE_SPEC_REVISION}).`,
Expand All @@ -2239,7 +2210,7 @@ export class EmberAdapter extends Adapter {
return await this.queue.execute<TsType.ActiveEndpoints>(async () => {
this.checkInterpanLock();

const zdoPayload = BuffaloZdo.buildActiveEndpointsRequest(networkAddress);
const zdoPayload = Zdo.Buffalo.buildRequest(this.hasZdoMessageOverhead, Zdo.ClusterId.ACTIVE_ENDPOINTS_REQUEST, networkAddress);
const [status, apsFrame] = await this.sendZDORequest(
networkAddress,
Zdo.ClusterId.ACTIVE_ENDPOINTS_REQUEST,
Expand Down Expand Up @@ -2269,7 +2240,12 @@ export class EmberAdapter extends Adapter {
return await this.queue.execute<TsType.SimpleDescriptor>(async () => {
this.checkInterpanLock();

const zdoPayload = BuffaloZdo.buildSimpleDescriptorRequest(networkAddress, endpointID);
const zdoPayload = Zdo.Buffalo.buildRequest(
this.hasZdoMessageOverhead,
Zdo.ClusterId.SIMPLE_DESCRIPTOR_REQUEST,
networkAddress,
endpointID,
);
const [status, apsFrame] = await this.sendZDORequest(
networkAddress,
Zdo.ClusterId.SIMPLE_DESCRIPTOR_REQUEST,
Expand Down Expand Up @@ -2315,7 +2291,9 @@ export class EmberAdapter extends Adapter {
return await this.queue.execute<void>(async () => {
this.checkInterpanLock();

const zdoPayload = BuffaloZdo.buildBindRequest(
const zdoPayload = Zdo.Buffalo.buildRequest(
this.hasZdoMessageOverhead,
Zdo.ClusterId.BIND_REQUEST,
sourceIeeeAddress as EUI64,
sourceEndpoint,
clusterID,
Expand Down Expand Up @@ -2361,7 +2339,9 @@ export class EmberAdapter extends Adapter {
return await this.queue.execute<void>(async () => {
this.checkInterpanLock();

const zdoPayload = BuffaloZdo.buildUnbindRequest(
const zdoPayload = Zdo.Buffalo.buildRequest(
this.hasZdoMessageOverhead,
Zdo.ClusterId.UNBIND_REQUEST,
sourceIeeeAddress as EUI64,
sourceEndpoint,
clusterID,
Expand Down Expand Up @@ -2399,7 +2379,12 @@ export class EmberAdapter extends Adapter {
return await this.queue.execute<void>(async () => {
this.checkInterpanLock();

const zdoPayload = BuffaloZdo.buildLeaveRequest(ieeeAddr as EUI64, Zdo.LeaveRequestFlags.WITHOUT_REJOIN);
const zdoPayload = Zdo.Buffalo.buildRequest(
this.hasZdoMessageOverhead,
Zdo.ClusterId.LEAVE_REQUEST,
ieeeAddr as EUI64,
Zdo.LeaveRequestFlags.WITHOUT_REJOIN,
);
const [status, apsFrame] = await this.sendZDORequest(networkAddress, Zdo.ClusterId.LEAVE_REQUEST, zdoPayload, DEFAULT_APS_OPTIONS);

if (status !== SLStatus.OK) {
Expand Down
3 changes: 1 addition & 2 deletions src/adapter/ember/adapter/oneWaitress.ts
Original file line number Diff line number Diff line change
Expand Up @@ -126,8 +126,7 @@ export class EmberOneWaitress {
if (
sender === waiter.matcher.target &&
apsFrame.profileId === waiter.matcher.apsFrame.profileId &&
apsFrame.clusterId ===
(waiter.matcher.responseClusterId != null ? waiter.matcher.responseClusterId : waiter.matcher.apsFrame.clusterId)
apsFrame.clusterId === (waiter.matcher.responseClusterId ?? waiter.matcher.apsFrame.clusterId)
) {
clearTimeout(waiter.timer);

Expand Down
3 changes: 2 additions & 1 deletion src/adapter/ezsp/adapter/ezspAdapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ class EZSPAdapter extends Adapter {

public constructor(networkOptions: NetworkOptions, serialPortOptions: SerialPortOptions, backupPath: string, adapterOptions: AdapterOptions) {
super(networkOptions, serialPortOptions, backupPath, adapterOptions);
this.hasZdoMessageOverhead = true;

this.waitress = new Waitress<Events.ZclPayload, WaitressMatcher>(this.waitressValidator, this.waitressTimeoutFormatter);
this.interpanLock = false;
Expand All @@ -65,7 +66,7 @@ class EZSPAdapter extends Adapter {
logger.debug(`Adapter concurrent: ${concurrent}`, NS);
this.queue = new Queue(concurrent);

this.driver = new Driver(this.serialPortOptions, this.networkOptions, this.greenPowerGroup, backupPath);
this.driver = new Driver(this.serialPortOptions, this.networkOptions, backupPath);
this.driver.on('close', this.onDriverClose.bind(this));
this.driver.on('deviceJoined', this.handleDeviceJoin.bind(this));
this.driver.on('deviceLeft', this.handleDeviceLeft.bind(this));
Expand Down
7 changes: 3 additions & 4 deletions src/adapter/ezsp/driver/driver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import equals from 'fast-deep-equal/es6';

import {Wait, Waitress} from '../../../utils';
import {logger} from '../../../utils/logger';
import * as ZSpec from '../../../zspec';
import {Clusters} from '../../../zspec/zcl/definition/cluster';
import {EZSPAdapterBackup} from '../adapter/backup';
import * as TsType from './../../tstype';
Expand Down Expand Up @@ -92,7 +93,6 @@ export class Driver extends EventEmitter {
// @ts-expect-error XXX: init in startup
public ezsp: Ezsp;
private nwkOpt: TsType.NetworkOptions;
private greenPowerGroup: number;
// @ts-expect-error XXX: init in startup
public networkParams: EmberNetworkParameters;
// @ts-expect-error XXX: init in startup
Expand All @@ -114,12 +114,11 @@ export class Driver extends EventEmitter {
private serialOpt: TsType.SerialPortOptions;
public backupMan: EZSPAdapterBackup;

constructor(serialOpt: TsType.SerialPortOptions, nwkOpt: TsType.NetworkOptions, greenPowerGroup: number, backupPath: string) {
constructor(serialOpt: TsType.SerialPortOptions, nwkOpt: TsType.NetworkOptions, backupPath: string) {
super();

this.nwkOpt = nwkOpt;
this.serialOpt = serialOpt;
this.greenPowerGroup = greenPowerGroup;
this.waitress = new Waitress<EmberFrame, EmberWaitressMatcher>(this.waitressValidator, this.waitressTimeoutFormatter);
this.backupMan = new EZSPAdapterBackup(this, backupPath);
}
Expand Down Expand Up @@ -296,7 +295,7 @@ export class Driver extends EventEmitter {

this.multicast = new Multicast(this);
await this.multicast.startup([]);
await this.multicast.subscribe(this.greenPowerGroup, 242);
await this.multicast.subscribe(ZSpec.GP_GROUP_ID, ZSpec.GP_ENDPOINT);
// await this.multicast.subscribe(1, 901);

return result;
Expand Down
12 changes: 0 additions & 12 deletions src/adapter/tstype.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,17 +51,6 @@ interface LQI {
neighbors: LQINeighbor[];
}

enum RoutingTableStatus {
ACTIVE = 0x0,
DISCOVERY_UNDERWAY = 0x1,
DISCOVERY_FAILED = 0x2,
INACTIVE = 0x3,
VALIDATION_UNDERWAY = 0x4,
RESERVED1 = 0x5,
RESERVED2 = 0x6,
RESERVED3 = 0x7,
}

interface RoutingTableEntry {
destinationAddress: number;
status: string;
Expand Down Expand Up @@ -124,5 +113,4 @@ export {
StartResult,
RoutingTableEntry,
AdapterOptions,
RoutingTableStatus,
};
Loading

0 comments on commit c8cb557

Please sign in to comment.