From 81d828b9f778afd4e577519fb7f0035268dcaa40 Mon Sep 17 00:00:00 2001 From: Nerivec <62446222+Nerivec@users.noreply.github.com> Date: Sat, 17 Aug 2024 22:51:50 +0200 Subject: [PATCH] fix: Ember: fix CCA issues in busy environments (broadcast errors) (#1153) --- src/adapter/ember/adapter/emberAdapter.ts | 18 ++++++++++ src/adapter/ember/enums.ts | 23 +++++++++++++ src/adapter/ember/ezsp/ezsp.ts | 3 +- test/adapter/ember/emberAdapter.test.ts | 42 ++++++++++++++++++++++- 4 files changed, 84 insertions(+), 2 deletions(-) diff --git a/src/adapter/ember/adapter/emberAdapter.ts b/src/adapter/ember/adapter/emberAdapter.ts index ef29f2c8c6..ceb890e1c2 100644 --- a/src/adapter/ember/adapter/emberAdapter.ts +++ b/src/adapter/ember/adapter/emberAdapter.ts @@ -52,6 +52,7 @@ import { EmberDeviceUpdate, EmberIncomingMessageType, EmberTransmitPriority, + IEEE802154CcaMode, } from '../enums'; import {EzspBuffalo} from '../ezsp/buffalo'; import {EMBER_ENCRYPTION_KEY_SIZE, EZSP_MIN_PROTOCOL_VERSION, EZSP_PROTOCOL_VERSION, EZSP_STACK_TYPE_MESH} from '../ezsp/consts'; @@ -194,6 +195,8 @@ type StackConfig = { END_DEVICE_POLL_TIMEOUT: number; /** <0-65535> (Default: 300) @see EzspConfigId.TRANSIENT_KEY_TIMEOUT_S */ TRANSIENT_KEY_TIMEOUT_S: number; + /**@see Ezsp.ezspSetRadioIeee802154CcaMode */ + CCA_MODE?: keyof typeof IEEE802154CcaMode; }; /** @@ -215,6 +218,7 @@ export const DEFAULT_STACK_CONFIG: Readonly = { TRANSIENT_DEVICE_TIMEOUT: 10000, END_DEVICE_POLL_TIMEOUT: 8, // zigpc: 8 TRANSIENT_KEY_TIMEOUT_S: 300, // zigpc: 65535 + CCA_MODE: undefined, // not set by default }; /** Default behavior is to disable app key requests */ const ALLOW_APP_KEY_REQUESTS = false; @@ -378,6 +382,13 @@ export class EmberAdapter extends Adapter { logger.error(`[STACK CONFIG] Invalid TRANSIENT_KEY_TIMEOUT_S, using default.`, NS); } + config.CCA_MODE = config.CCA_MODE ?? undefined; // always default to undefined + + if (config.CCA_MODE && IEEE802154CcaMode[config.CCA_MODE] === undefined) { + config.CCA_MODE = undefined; + logger.error(`[STACK CONFIG] Invalid CCA_MODE, ignoring.`, NS); + } + logger.info(`Using stack config ${JSON.stringify(config)}.`, NS); return config; } catch {} @@ -713,6 +724,13 @@ export class EmberAdapter extends Adapter { await this.emberSetEzspConfigValue(EzspConfigId.MAX_END_DEVICE_CHILDREN, this.stackConfig.MAX_END_DEVICE_CHILDREN); await this.emberSetEzspConfigValue(EzspConfigId.END_DEVICE_POLL_TIMEOUT, this.stackConfig.END_DEVICE_POLL_TIMEOUT); await this.emberSetEzspConfigValue(EzspConfigId.TRANSIENT_KEY_TIMEOUT_S, this.stackConfig.TRANSIENT_KEY_TIMEOUT_S); + // XXX: temp-fix: forces a side-effect in the firmware that prevents broadcast issues in environments with unusual interferences + await this.emberSetEzspValue(EzspValueId.CCA_THRESHOLD, 1, [0]); + + if (this.stackConfig.CCA_MODE) { + // validated in `loadStackConfig` + await this.ezsp.ezspSetRadioIeee802154CcaMode(IEEE802154CcaMode[this.stackConfig.CCA_MODE]); + } // WARNING: From here on EZSP commands that affect memory allocation on the NCP should no longer be called (like resizing tables) diff --git a/src/adapter/ember/enums.ts b/src/adapter/ember/enums.ts index c3b49bde64..7aa497e0d6 100644 --- a/src/adapter/ember/enums.ts +++ b/src/adapter/ember/enums.ts @@ -2098,3 +2098,26 @@ export enum EmberTransmitPriority { NORMAL = 1, SCAN_OKAY = 2, } + +export enum IEEE802154CcaMode { + /** RSSI-based CCA. CCA reports a busy medium upon detecting any energy above -75 (default RAIL_CsmaConfig_t.ccaThreshold). */ + RSSI = 0, + /** + * Signal Identifier-based CCA. CCA reports a busy medium only upon the detection of a signal compliant with this standard + * with the same modulation and spreading characteristics of the PHY that is currently in use. + */ + SIGNAL = 1, + /** + * RSSI or signal identifier-based CCA. CCA reports a busy medium on either detecting any energy above + * -75 (default RAIL_CsmaConfig_t.ccaThreshold) or detection of a signal compliant with this standard with the same modulation + * and spreading characteristics of the PHY that is currently in use. + */ + SIGNAL_OR_RSSI = 2, + /** + * RSSI and signal identifier-based CCA. CCA reports a busy medium only on detecting any energy above -75 (default RAIL_CsmaConfig_t.ccaThreshold) + * of a signal compliant with this standard with the same modulation and spreading characteristics of the PHY that is currently in use. + */ + SIGNAL_AND_RSSI = 3, + /** ALOHA. Always transmit CCA=1. CCA always reports an idle medium. */ + ALWAYS_TRANSMIT = 4, +} diff --git a/src/adapter/ember/ezsp/ezsp.ts b/src/adapter/ember/ezsp/ezsp.ts index 4f7c992136..ec7d8da584 100644 --- a/src/adapter/ember/ezsp/ezsp.ts +++ b/src/adapter/ember/ezsp/ezsp.ts @@ -57,6 +57,7 @@ import { EmberGPStatus, EmberApsOption, EmberTransmitPriority, + IEEE802154CcaMode, } from '../enums'; import {EzspError} from '../ezspError'; import { @@ -3808,7 +3809,7 @@ export class Ezsp extends EventEmitter { * @param ccaMode uint8_t A RAIL_IEEE802154_CcaMode_t value. * @returns An SLStatus value indicating the success or failure of the command. */ - async ezspSetRadioIeee802154CcaMode(ccaMode: number): Promise { + async ezspSetRadioIeee802154CcaMode(ccaMode: IEEE802154CcaMode): Promise { const sendBuffalo = this.startCommand(EzspFrameID.SET_RADIO_IEEE802154_CCA_MODE); sendBuffalo.writeUInt8(ccaMode); diff --git a/test/adapter/ember/emberAdapter.test.ts b/test/adapter/ember/emberAdapter.test.ts index 0196fece23..1db2e2c614 100644 --- a/test/adapter/ember/emberAdapter.test.ts +++ b/test/adapter/ember/emberAdapter.test.ts @@ -23,6 +23,7 @@ import { EmberOutgoingMessageType, EmberVersionType, EzspStatus, + IEEE802154CcaMode, SecManDerivedKeyType, SecManFlag, SecManKeyType, @@ -159,6 +160,7 @@ const mockEzspNetworkState = jest.fn().mockResolvedValue(EmberNetworkStatus.JOIN const mockEzspGetEui64 = jest.fn().mockResolvedValue(DEFAULT_COORDINATOR_IEEE); const mockEzspSetConcentrator = jest.fn().mockResolvedValue(SLStatus.OK); const mockEzspSetSourceRouteDiscoveryMode = jest.fn().mockResolvedValue(1240 /* ms */); +const mockEzspSetRadioIeee802154CcaMode = jest.fn().mockResolvedValue(SLStatus.OK); // not OK by default since used to detected unreged EP const mockEzspGetEndpointFlags = jest.fn().mockResolvedValue([SLStatus.NOT_FOUND, EzspEndpointFlag.DISABLED]); const mockEzspAddEndpoint = jest.fn().mockResolvedValue(SLStatus.OK); @@ -282,6 +284,7 @@ jest.mock('../../../src/adapter/ember/ezsp/ezsp', () => ({ ezspGetEui64: mockEzspGetEui64, ezspSetConcentrator: mockEzspSetConcentrator, ezspSetSourceRouteDiscoveryMode: mockEzspSetSourceRouteDiscoveryMode, + ezspSetRadioIeee802154CcaMode: mockEzspSetRadioIeee802154CcaMode, ezspGetEndpointFlags: mockEzspGetEndpointFlags, ezspAddEndpoint: mockEzspAddEndpoint, ezspNetworkInit: mockEzspNetworkInit, @@ -333,6 +336,7 @@ const ezspMocks = [ mockEzspGetEui64, mockEzspSetConcentrator, mockEzspSetSourceRouteDiscoveryMode, + mockEzspSetRadioIeee802154CcaMode, mockEzspGetEndpointFlags, mockEzspAddEndpoint, mockEzspNetworkInit, @@ -473,6 +477,7 @@ describe('Ember Adapter Layer', () => { TRANSIENT_DEVICE_TIMEOUT: 1000, END_DEVICE_POLL_TIMEOUT: 12, TRANSIENT_KEY_TIMEOUT_S: 500, + CCA_MODE: 'SIGNAL_AND_RSSI', }; writeFileSync(STACK_CONFIG_PATH, JSON.stringify(config, undefined, 2)); @@ -497,6 +502,22 @@ describe('Ember Adapter Layer', () => { TRANSIENT_DEVICE_TIMEOUT: 65536, END_DEVICE_POLL_TIMEOUT: 15, TRANSIENT_KEY_TIMEOUT_S: 65536, + CCA_MODE: 'abcd', + }; + + writeFileSync(STACK_CONFIG_PATH, JSON.stringify(config, undefined, 2)); + + adapter = new EmberAdapter(DEFAULT_NETWORK_OPTIONS, DEFAULT_SERIAL_PORT_OPTIONS, backupPath, DEFAULT_ADAPTER_OPTIONS); + + expect(adapter.stackConfig).toStrictEqual(DEFAULT_STACK_CONFIG); + + // cleanup + unlinkSync(STACK_CONFIG_PATH); + }); + + it('Loads only valid custom stack config - null CCA_MODE', () => { + const config = { + CCA_MODE: null, }; writeFileSync(STACK_CONFIG_PATH, JSON.stringify(config, undefined, 2)); @@ -546,7 +567,6 @@ describe('Ember Adapter Layer', () => { }); it('Starts with custom stack config', async () => { - adapter = new EmberAdapter(DEFAULT_NETWORK_OPTIONS, DEFAULT_SERIAL_PORT_OPTIONS, backupPath, DEFAULT_ADAPTER_OPTIONS); const config = { CONCENTRATOR_RAM_TYPE: 'low', CONCENTRATOR_MIN_TIME: 1, @@ -558,6 +578,7 @@ describe('Ember Adapter Layer', () => { TRANSIENT_DEVICE_TIMEOUT: 1000, END_DEVICE_POLL_TIMEOUT: 12, TRANSIENT_KEY_TIMEOUT_S: 500, + CCA_MODE: 'SIGNAL_AND_RSSI', }; writeFileSync(STACK_CONFIG_PATH, JSON.stringify(config, undefined, 2)); @@ -580,6 +601,25 @@ describe('Ember Adapter Layer', () => { config.CONCENTRATOR_DELIVERY_FAILURE_THRESHOLD, config.CONCENTRATOR_MAX_HOPS, ); + expect(mockEzspSetRadioIeee802154CcaMode).toHaveBeenCalledWith(IEEE802154CcaMode.SIGNAL_AND_RSSI); + + // cleanup + unlinkSync(STACK_CONFIG_PATH); + }); + + it('Starts with custom stack config invalid CCA_MODE', async () => { + const config = { + CCA_MODE: 'abcd', + }; + + writeFileSync(STACK_CONFIG_PATH, JSON.stringify(config, undefined, 2)); + + adapter = new EmberAdapter(DEFAULT_NETWORK_OPTIONS, DEFAULT_SERIAL_PORT_OPTIONS, backupPath, DEFAULT_ADAPTER_OPTIONS); + const result = adapter.start(); + + await jest.advanceTimersByTimeAsync(5000); + await expect(result).resolves.toStrictEqual('resumed'); + expect(mockEzspSetRadioIeee802154CcaMode).toHaveBeenCalledTimes(0); // cleanup unlinkSync(STACK_CONFIG_PATH);