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

ZDO spec: improve build/read logic and typing. #1186

Merged
merged 1 commit into from
Sep 14, 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
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