Skip to content

Commit

Permalink
feat: Base support for channel switching without re-pairing, support …
Browse files Browse the repository at this point in the history
…it for Ember adapter (#977)

* Base support for channel switching without re-pairing

* Add Controller switchChannel test.

* [deconz] Fix network config "changed" scope variables updating

* Lower time to switch to 60sec post-start

* Add `supportsSwitchChannel` function + tests

* More tests (and suppress annoying ASH logging during tests).

* Rework to run immediately post adapter start. Rename to changeChannel.

* Update controller.ts

---------

Co-authored-by: Koen Kanters <koenkanters94@gmail.com>
  • Loading branch information
Nerivec and Koenkk committed Mar 19, 2024
1 parent 778cbad commit 5693789
Show file tree
Hide file tree
Showing 13 changed files with 265 additions and 27 deletions.
4 changes: 4 additions & 0 deletions src/adapter/adapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -171,6 +171,10 @@ abstract class Adapter extends events.EventEmitter {

public abstract getNetworkParameters(): Promise<TsType.NetworkParameters>;

public abstract supportsChangeChannel(): Promise<boolean>;

public abstract changeChannel(newChannel: number): Promise<void>;

public abstract setTransmitPower(value: number): Promise<void>;

public abstract addInstallCode(ieeeAddress: string, key: Buffer): Promise<void>;
Expand Down
16 changes: 12 additions & 4 deletions src/adapter/deconz/adapter/deconzAdapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1000,10 +1000,10 @@ class DeconzAdapter extends Adapter {
await this.driver.changeNetworkStateRequest(PARAM.PARAM.Network.NET_CONNECTED);
await this.sleep(2000);

let panid: any = await this.driver.readParameterRequest(PARAM.PARAM.Network.PAN_ID);
let expanid: any = await this.driver.readParameterRequest(PARAM.PARAM.Network.APS_EXT_PAN_ID);
let channel: any = await this.driver.readParameterRequest(PARAM.PARAM.Network.CHANNEL);
let networkKey: any = await this.driver.readParameterRequest(PARAM.PARAM.Network.NETWORK_KEY);
panid = await this.driver.readParameterRequest(PARAM.PARAM.Network.PAN_ID);
expanid = await this.driver.readParameterRequest(PARAM.PARAM.Network.APS_EXT_PAN_ID);
channel = await this.driver.readParameterRequest(PARAM.PARAM.Network.CHANNEL);
networkKey = await this.driver.readParameterRequest(PARAM.PARAM.Network.NETWORK_KEY);
}

return {
Expand Down Expand Up @@ -1042,6 +1042,14 @@ class DeconzAdapter extends Adapter {
throw new Error("not supported");
}

public async supportsChangeChannel(): Promise<boolean> {
return false;
}

public async changeChannel(newChannel: number): Promise<void> {
throw new Error("not supported");
}

public async setTransmitPower(value: number): Promise<void> {
throw new Error("not supported");
}
Expand Down
113 changes: 96 additions & 17 deletions src/adapter/ember/adapter/emberAdapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,8 @@ import {
SIMPLE_DESCRIPTOR_RESPONSE,
BIND_RESPONSE,
UNBIND_RESPONSE,
LEAVE_RESPONSE
LEAVE_RESPONSE,
NWK_UPDATE_REQUEST
} from "../zdo";
import {
EMBER_BROADCAST_ADDRESS,
Expand Down Expand Up @@ -152,7 +153,7 @@ import {EmberRequestQueue} from "./requestQueue";
import {FIXED_ENDPOINTS} from "./endpoints";
import {aesMmoHashInit, initNetworkCache, initSecurityManagerContext} from "../utils/initters";
import {randomBytes} from "crypto";
import {EmberOneWaitress} from "./oneWaitress";
import {EmberOneWaitress, OneWaitressEvents} from "./oneWaitress";
// import {EmberTokensManager} from "./tokensManager";

const debug = Debug('zigbee-herdsman:adapter:ember:adapter');
Expand Down Expand Up @@ -227,14 +228,6 @@ enum RoutingTableStatus {
RESERVED3 = 0x7,
};

/** Events specific to OneWaitress usage. */
enum OneWaitressEvents {
STACK_STATUS_NETWORK_UP = 'STACK_STATUS_NETWORK_UP',
STACK_STATUS_NETWORK_DOWN = 'STACK_STATUS_NETWORK_DOWN',
STACK_STATUS_NETWORK_OPENED = 'STACK_STATUS_NETWORK_OPENED',
STACK_STATUS_NETWORK_CLOSED = 'STACK_STATUS_NETWORK_CLOSED',
};

enum NetworkInitAction {
/** Ain't that nice! */
DONE,
Expand Down Expand Up @@ -522,6 +515,13 @@ export class EmberAdapter extends Adapter {
console.log(`[STACK STATUS] Network closed.`);
break;
}
case EmberStatus.CHANNEL_CHANGED: {
this.oneWaitress.resolveEvent(OneWaitressEvents.STACK_STATUS_CHANNEL_CHANGED);
// invalidate cache
this.networkCache.parameters.radioChannel = INVALID_RADIO_CHANNEL;
console.log(`[STACK STATUS] Channel changed.`);
break;
}
default: {
debug(`[STACK STATUS] ${EmberStatus[status]}.`);
break;
Expand Down Expand Up @@ -1093,8 +1093,7 @@ export class EmberAdapter extends Adapter {

// XXX: should not force a form when it's only a channel change, just change the channel, wait a sec, then continue the logic
if ((npStatus === EmberStatus.SUCCESS) && (nodeType === EmberNodeType.COORDINATOR) && (this.networkOptions.panID === netParams.panId)
&& (equals(this.networkOptions.extendedPanID, netParams.extendedPanId))
&& (this.networkOptions.channelList.includes(netParams.radioChannel))) {
&& (equals(this.networkOptions.extendedPanID, netParams.extendedPanId))) {
// config matches adapter so far, no error, we can check the network key
const context = initSecurityManagerContext();
context.coreKeyType = SecManKeyType.NETWORK;
Expand Down Expand Up @@ -1157,8 +1156,6 @@ export class EmberAdapter extends Adapter {
console.log(`[INIT TC] No valid backup found.`);
action = NetworkInitAction.FORM_CONFIG;
}
} else {
action = NetworkInitAction.DONE;// just to be clear
}

//---- from here on, we assume everything is in place for whatever decision was taken above
Expand Down Expand Up @@ -2591,6 +2588,51 @@ export class EmberAdapter extends Adapter {
return this.sendZDORequestBuffer(target, PERMIT_JOINING_REQUEST, options);
}

/**
* ZDO
*
* @see NWK_UPDATE_REQUEST
*
* @param target
* @param scanChannels uint8_t[]
* @param duration uint8_t
* @param count uint8_t
* @param manager
*/
private async emberNetworkUpdateRequest(target: EmberNodeId, scanChannels: number[], duration: number, count: number | null,
manager: EmberNodeId | null, options: EmberApsOption): Promise<[EmberStatus, apsFrame: EmberApsFrame, messageTag: number]> {
this.zdoRequestBuffalo.setPosition(ZDO_MESSAGE_OVERHEAD);

this.zdoRequestBuffalo.writeUInt32(scanChannels.reduce((a, c) => a + (1 << c), 0));// to uint32_t
this.zdoRequestBuffalo.writeUInt8(duration);

if (count != null) {
this.zdoRequestBuffalo.writeUInt8(count);
}

if (manager != null) {
this.zdoRequestBuffalo.writeUInt16(manager);
}

debug(`~~~> [ZDO NWK_UPDATE_REQUEST target=${target} scanChannels=${scanChannels} duration=${duration} count=${count} manager=${manager}]`);
return this.sendZDORequestBuffer(target, NWK_UPDATE_REQUEST, options);
}

private async emberScanChannelsRequest(target: EmberNodeId, scanChannels: number[], duration: number, count: number, options: EmberApsOption):
Promise<[EmberStatus, apsFrame: EmberApsFrame, messageTag: number]> {
return this.emberNetworkUpdateRequest(target, scanChannels, duration, count, null, options);
}

private async emberChannelChangeRequest(target: EmberNodeId, channel: number, options: EmberApsOption):
Promise<[EmberStatus, apsFrame: EmberApsFrame, messageTag: number]> {
return this.emberNetworkUpdateRequest(target, [channel], 0xFE, null, null, options);
}

private async emberSetActiveChannelsAndNwkManagerIdRequest(target: EmberNodeId, scanChannels: number[], manager: EmberNodeId,
options: EmberApsOption): Promise<[EmberStatus, apsFrame: EmberApsFrame, messageTag: number]> {
return this.emberNetworkUpdateRequest(target, scanChannels, 0xFF, null, manager, options);
}

//---- END Ember ZDO

//-- START Adapter implementation
Expand Down Expand Up @@ -2795,14 +2837,14 @@ export class EmberAdapter extends Adapter {

// first call will cache for the others, but in all likelihood, it will all be from freshly cached after init
// since Controller caches this also.
const channel = (await this.emberGetRadioChannel());
const panID = (await this.emberGetPanId());
const extendedPanID = (await this.emberGetExtendedPanId());
const channel = (await this.emberGetRadioChannel());

resolve({
panID: panID,
panID,
extendedPanID: parseInt(Buffer.from(extendedPanID).toString('hex'), 16),
channel: channel,
channel,
});

return EmberStatus.SUCCESS;
Expand All @@ -2812,6 +2854,43 @@ export class EmberAdapter extends Adapter {
});
}

public async supportsChangeChannel(): Promise<boolean> {
return true;
}

// queued
public async changeChannel(newChannel: number): Promise<void> {
return new Promise<void>((resolve, reject): void => {
this.requestQueue.enqueue(
async (): Promise<EmberStatus> => {
this.checkInterpanLock();

// eslint-disable-next-line @typescript-eslint/no-unused-vars
const [status, apsFrame, messageTag] = (await this.emberChannelChangeRequest(
EMBER_SLEEPY_BROADCAST_ADDRESS,
newChannel,
DEFAULT_APS_OPTIONS,
));

if (status !== EmberStatus.SUCCESS) {
console.error(`[ZDO] Failed broadcast channel change to "${newChannel}" with status=${EmberStatus[status]}.`);
return status;
}

await this.oneWaitress.startWaitingForEvent(
{eventName: OneWaitressEvents.STACK_STATUS_CHANNEL_CHANGED},
DEFAULT_NETWORK_REQUEST_TIMEOUT * 2,// observed to ~9sec
'[ZDO] Change Channel',
);

resolve();
return EmberStatus.SUCCESS;
},
reject,
);
});
}

// queued
public async setTransmitPower(value: number): Promise<void> {
if (typeof value !== 'number') {
Expand Down
9 changes: 9 additions & 0 deletions src/adapter/ember/adapter/oneWaitress.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,15 @@ import {EmberApsFrame, EmberNodeId} from "../types";
import {EmberZdoStatus} from "../zdo";


/** Events specific to OneWaitress usage. */
export enum OneWaitressEvents {
STACK_STATUS_NETWORK_UP = 'STACK_STATUS_NETWORK_UP',
STACK_STATUS_NETWORK_DOWN = 'STACK_STATUS_NETWORK_DOWN',
STACK_STATUS_NETWORK_OPENED = 'STACK_STATUS_NETWORK_OPENED',
STACK_STATUS_NETWORK_CLOSED = 'STACK_STATUS_NETWORK_CLOSED',
STACK_STATUS_CHANNEL_CHANGED = 'STACK_STATUS_CHANNEL_CHANGED',
};

type OneWaitressMatcher = {
/**
* Matches `indexOrDestination` in `ezspMessageSentHandler` or `sender` in `ezspIncomingMessageHandler`
Expand Down
9 changes: 9 additions & 0 deletions src/adapter/ezsp/adapter/ezspAdapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -711,6 +711,15 @@ class EZSPAdapter extends Adapter {
});
}

public async supportsChangeChannel(): Promise<boolean> {
return false;
}

// eslint-disable-next-line @typescript-eslint/no-unused-vars
public async changeChannel(newChannel: number): Promise<void> {
return Promise.reject(new Error("Not supported"));
}

public async setTransmitPower(value: number): Promise<void> {
debug(`setTransmitPower to ${value}`);
return this.queue.execute<void>(async () => {
Expand Down
2 changes: 1 addition & 1 deletion src/adapter/z-stack/adapter/manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -139,7 +139,7 @@ export class ZnpAdapterManager {
/* istanbul ignore next */
const configMatchesAdapter = (
nib &&
Utils.compareChannelLists(this.nwkOptions.channelList, nib.channelList) &&
Utils.compareChannelLists(this.nwkOptions.channelList, nib.channelList) &&// TODO: remove?
this.nwkOptions.panId === nib.nwkPanId &&
(
this.nwkOptions.extendedPanId.equals(nib.extendedPANID) ||
Expand Down
23 changes: 23 additions & 0 deletions src/adapter/z-stack/adapter/zStackAdapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -918,6 +918,29 @@ class ZStackAdapter extends Adapter {
});
}

public async supportsChangeChannel(): Promise<boolean> {
return false;
}

public async changeChannel(newChannel: number): Promise<void> {
return this.queue.execute<void>(async () => {
this.checkInterpanLock();

const payload = {
dstaddr: 0xFFFF,// broadcast with sleepy
dstaddrmode: AddressMode.ADDR_BROADCAST,
channelmask: [newChannel].reduce((a, c) => a + (1 << c), 0),
scanduration: 0xFE,// change channel
// scancount: null,// TODO: what's "not present" here?
// nwkmanageraddr: null,// TODO: what's "not present" here?
};

await this.znp.request(Subsystem.ZDO, 'mgmtNwkUpdateReq', payload);
// wait for the broadcast to propagate and the adapter to actually change
await Wait(10000);
});
}

public async setTransmitPower(value: number): Promise<void> {
return this.queue.execute<void>(async () => {
await this.znp.request(Subsystem.SYS, 'stackTune', {operation: 0, value});
Expand Down
3 changes: 2 additions & 1 deletion src/adapter/z-stack/znp/definition.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1548,13 +1548,14 @@ const Definition: {
},
{
name: 'mgmtNwkUpdateReq',
ID: 55,
ID: 55,// TODO: 0x0038 => 56??
type: CommandType.SREQ,
request: [
{name: 'dstaddr', parameterType: ParameterType.UINT16},
{name: 'dstaddrmode', parameterType: ParameterType.UINT8},
{name: 'channelmask', parameterType: ParameterType.UINT32},
{name: 'scanduration', parameterType: ParameterType.UINT8},
// TODO: below two have various combinations of present/not present depending on scanduration
{name: 'scancount', parameterType: ParameterType.UINT8},
{name: 'nwkmanageraddr', parameterType: ParameterType.UINT16},
],
Expand Down
9 changes: 9 additions & 0 deletions src/adapter/zigate/adapter/zigateAdapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -206,6 +206,15 @@ class ZiGateAdapter extends Adapter {
throw new Error("This adapter does not support backup");
};

public async supportsChangeChannel(): Promise<boolean> {
return false;
}

// eslint-disable-next-line @typescript-eslint/no-unused-vars
public async changeChannel(newChannel: number): Promise<void> {
throw new Error("not supported");
};

public async setTransmitPower(value: number): Promise<void> {
debug.log('setTransmitPower, %o', arguments);
return this.driver.sendCommand(ZiGateCommandCode.SetTXpower, {value: value})
Expand Down
22 changes: 22 additions & 0 deletions src/controller/controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,18 @@ class Controller extends events.EventEmitter {
debug.log(`Starting with options '${JSON.stringify(this.options)}'`);
const startResult = await this.adapter.start();
debug.log(`Started with result '${startResult}'`);

// Check if we have to change the channel, only do this when adapter `resumed` because:
// - `getNetworkParameters` might be return wrong info because it needs to propogate after backup restore
// - If result is not `resumed` (`reset` or `restored`), the adapter should comission with the channel from `this.options.network`
if ((startResult === 'resumed') && (await this.adapter.supportsChangeChannel())) {
const netParams = (await this.getNetworkParameters());

if (this.options.network.channelList[0] !== netParams.channel) {
await this.changeChannel();
}
}

Entity.injectAdapter(this.adapter);

// log injection
Expand Down Expand Up @@ -414,6 +426,16 @@ class Controller extends events.EventEmitter {
return Group.create(groupID);
}

/**
* Broadcast a network-wide channel change.
*/
private async changeChannel(): Promise<void> {
debug.log(`Broadcasting change channel to '${this.options.network.channelList[0]}'.`);
await this.adapter.changeChannel(this.options.network.channelList[0]);

this.networkParametersCached = null;// invalidate cache
}

/**
* Set transmit power of the adapter
*/
Expand Down
4 changes: 4 additions & 0 deletions test/adapter/ember/ash.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ import {lowByte} from '../../../src/adapter/ember/utils/math';
import {EzspFrameID} from '../../../src/adapter/ember/ezsp/enums.ts';
import {Wait} from '../../../src/utils/';

const consoleLogNative = console.log;

// XXX: Below are copies from uart>ash.ts, should be kept in sync (avoids export)
/** max frames sent without being ACKed (1-7) */
const CONFIG_TX_K = 3;
Expand Down Expand Up @@ -72,9 +74,11 @@ describe('Ember UART ASH Protocol', () => {

beforeAll(async () => {
jest.useRealTimers();// messes with serialport promise handling otherwise?
console.log = jest.fn();
});
afterAll(async () => {
jest.useRealTimers();
console.log = consoleLogNative;
});
beforeEach(() => {
for (const mock of mocks) {
Expand Down

0 comments on commit 5693789

Please sign in to comment.