Skip to content

Commit

Permalink
feat: Ember: Support for EmberZNet v8.0.0 (#1094)
Browse files Browse the repository at this point in the history
* Support EZSP version switching.

* Add/update commands. Add v13/14 status matching. Cleanup.

* Remove EmberStatus enum.

* Fix regression previous commit.

* Update ezsp comments. Fixes/cleanup.

* Cleanup.

* Set new protocol version.

* Remove `messageContents` from `ezspMessageSentHandler`

* Fix comment.

* Lower log level for failed config.

* Remove deprecated `ezspSetSourceRoute`

* Move to Zdo spec for response parsing.

* Move to Zdo spec for request building.

* Cleanup startup sequence. Remove configs better left to firmware defaults.

* Separate buffers for responses and callbacks. Improve queue behavior.

* Cleanup tokens manager.

* Fix missing zdo event. Add typing for `EventEmitter`s. Cleanup.
  • Loading branch information
Nerivec authored Jul 1, 2024
1 parent 9b6d78f commit c87ccd4
Show file tree
Hide file tree
Showing 19 changed files with 5,175 additions and 6,104 deletions.
1,948 changes: 571 additions & 1,377 deletions src/adapter/ember/adapter/emberAdapter.ts

Large diffs are not rendered by default.

24 changes: 8 additions & 16 deletions src/adapter/ember/adapter/oneWaitress.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,10 @@
/* istanbul ignore file */
import equals from 'fast-deep-equal/es6';
import {ZclPayload} from '../../events';
import {NodeId} from '../../../zspec/tstypes';
import {TOUCHLINK_PROFILE_ID} from '../../../zspec/consts';
import {EmberApsFrame, EmberNodeId} from '../types';
import {EmberZdoStatus} from '../zdo';
import {logger} from '../../../utils/logger';

const NS = 'zh:ember:waitress';
import {EmberApsFrame} from '../types';
import * as Zdo from '../../../zspec/zdo/';

/** Events specific to OneWaitress usage. */
export enum OneWaitressEvents {
Expand All @@ -22,7 +20,7 @@ type OneWaitressMatcher = {
* Matches `indexOrDestination` in `ezspMessageSentHandler` or `sender` in `ezspIncomingMessageHandler`
* Except for InterPAN touchlink, it should always be present.
*/
target?: EmberNodeId,
target?: NodeId,
apsFrame: EmberApsFrame,
/** Cluster ID for when the response doesn't match the request. Takes priority over apsFrame.clusterId. Should be mostly for ZDO requests. */
responseClusterId?: number,
Expand Down Expand Up @@ -110,7 +108,7 @@ export class EmberOneWaitress {
* @param payload
* @returns
*/
public resolveZDO(status: EmberZdoStatus, sender: EmberNodeId, apsFrame: EmberApsFrame, payload: unknown): boolean {
public resolveZDO(sender: NodeId, apsFrame: EmberApsFrame, payload: unknown | Zdo.StatusError): boolean {
for (const [index, waiter] of this.waiters.entries()) {
if (waiter.timedout) {
this.waiters.delete(index);
Expand All @@ -127,16 +125,10 @@ export class EmberOneWaitress {

this.waiters.delete(index);

if (status === EmberZdoStatus.ZDP_SUCCESS) {
waiter.resolve(payload);
} else if (status === EmberZdoStatus.ZDP_NO_ENTRY) {
// XXX: bypassing fail here since Z2M seems to trigger ZDO remove-type commands without checking current state
// Z2M also fails with ZCL payload NOT_FOUND though. This should be removed once upstream fixes that.
logger.info(`[ZDO] Received status ZDP_NO_ENTRY for "${sender}" cluster "${apsFrame.clusterId}". Ignoring.`, NS);
waiter.resolve(payload);
if (payload instanceof Zdo.StatusError || payload instanceof Error) {
waiter.reject(new Error(`[ZDO] Failed response for '${sender}' cluster '${apsFrame.clusterId}' ${payload.message}.`));
} else {
waiter.reject(new Error(`[ZDO] Failed response by NCP for "${sender}" cluster "${apsFrame.clusterId}" `
+ `with status=${EmberZdoStatus[status]}.`));
waiter.resolve(payload);
}

return true;
Expand Down
101 changes: 61 additions & 40 deletions src/adapter/ember/adapter/requestQueue.ts
Original file line number Diff line number Diff line change
@@ -1,22 +1,21 @@
/* istanbul ignore file */
import {logger} from "../../../utils/logger";
import {EmberStatus, EzspStatus} from "../enums";
import {EzspStatus, SLStatus} from "../enums";
import {EzspError} from "../ezspError";

const NS = 'zh:ember:queue';

interface EmberRequestQueueEntry {
/**
* Times tried to successfully send the call.
* This has no maximum, but since it is only for temporary issues, it will either succeed after a couple of tries, or hard fail.
*/
/** Times tried to successfully send the call. */
sendAttempts: number;
/** The function the entry is supposed to execute. */
func: () => Promise<EmberStatus>;
func: () => Promise<SLStatus>;
/** The wrapping promise's reject to reject if necessary. */
reject: (reason: Error) => void;
};

export const NETWORK_BUSY_DEFER_MSEC = 500;
export const MAX_SEND_ATTEMPTS = 3;
export const HIGH_COUNT = 4;
export const BUSY_DEFER_MSEC = 500;
export const NETWORK_DOWN_DEFER_MSEC = 1500;

export class EmberRequestQueue {
Expand All @@ -35,12 +34,28 @@ export class EmberRequestQueue {
this.priorityQueue = [];
}

/**
* Number of requests in both regular and priority queues.
*/
get totalQueued(): number {
return this.queue.length + this.priorityQueue.length;
}

/**
* If true, total queued requests count is considered high.
*/
get isHigh(): boolean {
return this.totalQueued > HIGH_COUNT;
}

/**
* Empty each queue.
*/
public clear(): void {
this.queue = [];
this.priorityQueue = [];

logger.info(`Request queues cleared.`, NS);
}

/**
Expand All @@ -59,7 +74,7 @@ export class EmberRequestQueue {
public startDispatching(): void {
this.dispatching = true;

setTimeout(this.dispatch.bind(this), 0);
setTimeout(this.dispatch.bind(this), 1);

logger.info(`Request dispatching started.`, NS);
}
Expand All @@ -76,8 +91,9 @@ export class EmberRequestQueue {
* @param prioritize If true, function will be enqueued in the priority queue. Defaults to false.
* @returns new length of the queue.
*/
public enqueue(func: () => Promise<EmberStatus>, reject: (reason: Error) => void, prioritize: boolean = false): number {
public enqueue(func: () => Promise<SLStatus>, reject: (reason: Error) => void, prioritize: boolean = false): number {
logger.debug(`Status queue=${this.queue.length} priorityQueue=${this.priorityQueue.length}.`, NS);

return (prioritize ? this.priorityQueue : this.queue).push({
sendAttempts: 0,
func,
Expand All @@ -88,7 +104,7 @@ export class EmberRequestQueue {
/**
* Dispatch the head of the queue.
*
* If request `func` throws, catch error and reject the request. `ezsp${x}` functions throw `EzspStatus` as error.
* If request `func` throws, catch error and reject the request. `ezsp${x}` functions throw `EzspError`.
*
* If request `func` resolves but has an error, look at what error, and determine if should retry or remove the request from queue.
*
Expand All @@ -110,36 +126,41 @@ export class EmberRequestQueue {
}

if (entry) {
entry.sendAttempts++;

// NOTE: refer to `enqueue()` comment to keep logic in sync with expectations, adjust comment on change.
try {
const status: EmberStatus = (await entry.func());

if ((status === EmberStatus.MAX_MESSAGE_LIMIT_REACHED) || (status === EmberStatus.NETWORK_BUSY)) {
logger.debug(`Dispatching deferred: NCP busy.`, NS);
this.defer(NETWORK_BUSY_DEFER_MSEC);
} else if (status === EmberStatus.NETWORK_DOWN) {
logger.debug(`Dispatching deferred: Network not ready`, NS);
this.defer(NETWORK_DOWN_DEFER_MSEC);
} else {
// success
(fromPriorityQueue ? this.priorityQueue : this.queue).shift();

if (status !== EmberStatus.SUCCESS) {
entry.reject(new Error(EmberStatus[status]));
entry.sendAttempts++;// enqueued at zero

if (entry.sendAttempts > MAX_SEND_ATTEMPTS) {
entry.reject(new Error(`Failed ${MAX_SEND_ATTEMPTS} attempts to send`));
} else {
// NOTE: refer to `enqueue()` comment to keep logic in sync with expectations, adjust comment on change.
try {
const status: SLStatus = (await entry.func());

// XXX: add NOT_READY?
if ((status === SLStatus.ZIGBEE_MAX_MESSAGE_LIMIT_REACHED) || (status === SLStatus.BUSY)) {
logger.debug(`Dispatching deferred: Adapter busy.`, NS);
this.defer(BUSY_DEFER_MSEC);
} else if (status === SLStatus.NETWORK_DOWN) {
logger.debug(`Dispatching deferred: Network not ready`, NS);
this.defer(NETWORK_DOWN_DEFER_MSEC);
} else {
// success
(fromPriorityQueue ? this.priorityQueue : this.queue).shift();

if (status !== SLStatus.OK) {
entry.reject(new Error(SLStatus[status]));
}
}
} catch (err) {// EzspStatusError from ezsp${x} commands, except for stuff rejected by OneWaitress, but that's never "retry"
if ((err as EzspError).code === EzspStatus.NO_TX_SPACE) {
logger.debug(`Dispatching deferred: Host busy.`, NS);
this.defer(BUSY_DEFER_MSEC);
} else if ((err as EzspError).code === EzspStatus.NOT_CONNECTED) {
logger.debug(`Dispatching deferred: Network not ready`, NS);
this.defer(NETWORK_DOWN_DEFER_MSEC);
} else {
(fromPriorityQueue ? this.priorityQueue : this.queue).shift();
entry.reject(err);
}
}
} catch (err) {// message is EzspStatus string from ezsp${x} commands, except for stuff rejected by OneWaitress, but that's never "retry"
if (err.message === EzspStatus[EzspStatus.NO_TX_SPACE]) {
logger.debug(`Dispatching deferred: Host busy.`, NS);
this.defer(NETWORK_BUSY_DEFER_MSEC);
} else if (err.message === EzspStatus[EzspStatus.NOT_CONNECTED]) {
logger.debug(`Dispatching deferred: Network not ready`, NS);
this.defer(NETWORK_DOWN_DEFER_MSEC);
} else {
(fromPriorityQueue ? this.priorityQueue : this.queue).shift();
entry.reject(err);
}
}
}
Expand Down
Loading

0 comments on commit c87ccd4

Please sign in to comment.