Skip to content

Commit 47be0e9

Browse files
authored
fix: ot-ti has no support for PHY_CCA_THRESHOLD (#26)
1 parent cab31b2 commit 47be0e9

File tree

6 files changed

+179
-57
lines changed

6 files changed

+179
-57
lines changed

.github/workflows/ci.yaml

+1-3
Original file line numberDiff line numberDiff line change
@@ -40,9 +40,7 @@ jobs:
4040

4141
- run: npm run build:prod
4242

43-
# coverage disabled in early stages
44-
# - run: npm run test:cov
45-
- run: npm run test
43+
- run: npm run test:cov
4644

4745
# get some "in-workflow" reference numbers for future comparison
4846
# TODO: send results to PR as needed

package-lock.json

+2-2
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "zigbee-on-host",
3-
"version": "0.1.8",
3+
"version": "0.1.9",
44
"description": "ZigBee stack designed to run on a host and communicate with a radio co-processor (RCP)",
55
"engines": {
66
"node": ">=20.17.0"

src/drivers/ot-rcp-driver.ts

+16-2
Original file line numberDiff line numberDiff line change
@@ -881,6 +881,10 @@ export class OTRCPDriver extends EventEmitter<AdapterDriverEventMap> {
881881
return this.#rcpMinHostAPIVersion;
882882
}
883883

884+
get currentSpinelTID(): number {
885+
return this.#spinelTID + 1;
886+
}
887+
884888
// #endregion
885889

886890
// #region TIDs/counters
@@ -1058,10 +1062,12 @@ export class OTRCPDriver extends EventEmitter<AdapterDriverEventMap> {
10581062
// logger.debug(() => `<--- HDLC[length=${hdlcFrame.length}]`, NS);
10591063
const spinelFrame = decodeSpinelFrame(hdlcFrame);
10601064

1065+
/* v8 ignore start */
10611066
if (spinelFrame.header.flg !== SPINEL_HEADER_FLG_SPINEL) {
10621067
// non-Spinel frame (likely BLE HCI)
10631068
return;
10641069
}
1070+
/* v8 ignore stop */
10651071

10661072
logger.debug(() => `<--- SPINEL[tid=${spinelFrame.header.tid} cmdId=${spinelFrame.commandId} len=${spinelFrame.payload.byteLength}]`, NS);
10671073

@@ -4222,7 +4228,7 @@ export class OTRCPDriver extends EventEmitter<AdapterDriverEventMap> {
42224228
await this.setProperty(writePropertyC(SpinelPropertyId.PHY_CHAN, this.netParams.channel));
42234229

42244230
// TODO: ?
4225-
// await this.setPHYCCAThreshold(10);
4231+
// try { await this.setPHYCCAThreshold(10); } catch (error) {}
42264232
await this.setPHYTXPower(this.netParams.txPower);
42274233

42284234
await this.setProperty(writePropertyE(SpinelPropertyId.MAC_15_4_LADDR, this.netParams.eui64));
@@ -4235,7 +4241,13 @@ export class OTRCPDriver extends EventEmitter<AdapterDriverEventMap> {
42354241
const txPower = await this.getPHYTXPower();
42364242
const radioRSSI = await this.getPHYRSSI();
42374243
this.rssiMin = await this.getPHYRXSensitivity();
4238-
const ccaThreshold = await this.getPHYCCAThreshold();
4244+
let ccaThreshold: number | undefined;
4245+
4246+
try {
4247+
ccaThreshold = await this.getPHYCCAThreshold();
4248+
} catch (error) {
4249+
logger.debug(() => `PHY_CCA_THRESHOLD: ${error}`, NS);
4250+
}
42394251

42404252
logger.info(
42414253
`======== Network started (PHY: txPower=${txPower}dBm rssi=${radioRSSI}dBm rxSensitivity=${this.rssiMin}dBm ccaThreshold=${ccaThreshold}dBm) ========`,
@@ -5354,6 +5366,7 @@ export class OTRCPDriver extends EventEmitter<AdapterDriverEventMap> {
53545366

53555367
/**
53565368
* The CCA (clear-channel assessment) threshold.
5369+
* NOTE: Currently not implemented in: ot-ti
53575370
* @returns dBm (int8)
53585371
*/
53595372
public async getPHYCCAThreshold(): Promise<number> {
@@ -5366,6 +5379,7 @@ export class OTRCPDriver extends EventEmitter<AdapterDriverEventMap> {
53665379
* The CCA (clear-channel assessment) threshold.
53675380
* Set to -128 to disable.
53685381
* The value will be rounded down to a value that is supported by the underlying radio hardware.
5382+
* NOTE: Currently not implemented in: ot-ti
53695383
* @param ccaThreshold dBm (>= -128 and <= 127)
53705384
*/
53715385
public async setPHYCCAThreshold(ccaThreshold: number): Promise<void> {

test/ot-rcp-driver.test.ts

+154-48
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,8 @@ import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, it, vi }
77
import { DEFAULT_WIRESHARK_IP, DEFAULT_ZEP_UDP_PORT, createWiresharkZEPFrame } from "../src/dev/wireshark";
88
import { OTRCPDriver, type SourceRouteTableEntry } from "../src/drivers/ot-rcp-driver";
99
import { SpinelCommandId } from "../src/spinel/commands";
10-
import { decodeHdlcFrame } from "../src/spinel/hdlc";
1110
import { SpinelPropertyId } from "../src/spinel/properties";
12-
import { SPINEL_HEADER_FLG_SPINEL, type SpinelFrame, decodeSpinelFrame, encodeSpinelFrame } from "../src/spinel/spinel";
11+
import { SPINEL_HEADER_FLG_SPINEL, encodeSpinelFrame } from "../src/spinel/spinel";
1312
import { SpinelStatus } from "../src/spinel/statuses";
1413
import { MACAssociationStatus, type MACCapabilities, type MACHeader, decodeMACFrameControl, decodeMACHeader } from "../src/zigbee/mac";
1514
import { ZigbeeConsts } from "../src/zigbee/zigbee";
@@ -62,7 +61,6 @@ import {
6261

6362
const randomBigInt = (): bigint => BigInt(`0x${randomBytes(8).toString("hex")}`);
6463

65-
const RESET_POWER_ON_FRAME_HEX = "7e80060070ee747e";
6664
const COMMON_FFD_MAC_CAP: MACCapabilities = {
6765
alternatePANCoordinator: false,
6866
deviceType: 1,
@@ -80,6 +78,61 @@ const COMMON_RFD_MAC_CAP: MACCapabilities = {
8078
allocateAddress: true,
8179
};
8280

81+
const START_FRAMES_SILABS = {
82+
protocolVersion: "7e8106010403db0a7e",
83+
ncpVersion:
84+
"7e820602534c2d4f50454e5448524541442f322e352e322e305f4769744875622d3166636562323235623b2045465233323b204d617220313920323032352031333a34353a343400b5dc7e",
85+
interfaceType: "7e83060303573a7e",
86+
rcpAPIVersion: "7e8406b0010a681f7e",
87+
rcpMinHostAPIVersion: "7e8506b101048ea77e",
88+
resetPowerOn: "7e80060070ee747e",
89+
};
90+
const START_FRAMES_TI = {
91+
protocolVersion: "7e8106010403db0a7e",
92+
ncpVersion:
93+
"7e8206024f50454e5448524541442f312e342e302d4b6f656e6b6b2d323032352e322e313b204343313358585f4343323658583b2046656220203320323032352032313a30303a303200ef147e",
94+
interfaceType: "7e83060303573a7e",
95+
rcpAPIVersion: "7e8406b0010be10e7e",
96+
rcpMinHostAPIVersion: "7e8506b101048ea77e",
97+
resetPowerOn: "7e80060070ee747e",
98+
};
99+
const FORM_FRAMES_SILABS = {
100+
phyEnabled: "7e87062001f2627e",
101+
phyChan: "7e88062114ff8e7e",
102+
phyTxPowerSet: "7e8906257d339b817e",
103+
mac154LAddr: "7e8a06344d325a6e6f486f5a8f327e",
104+
mac154SAddr: "7e8b0635000047f67e",
105+
mac154PANId: "7e8c0636d98579727e",
106+
macRxOnWhenIdleMode: "7e8d060000e68c7e",
107+
macRawStreamEnabled: "7e8e06370108437e",
108+
phyTxPowerGet: "7e8106257d3343647e",
109+
phyRSSIGet: "7e820626983d517e",
110+
phyRXSensitivityGet: "7e8306279c7a127e",
111+
phyCCAThresholdGet: "7e840624b5f0d37e",
112+
};
113+
const FORM_FRAMES_TI = {
114+
phyEnabled: "7e87062001f2627e",
115+
phyChan: "7e88062114ff8e7e",
116+
phyTxPowerSet: "7e890625052cf47e",
117+
mac154LAddr: "7e8a06344d325a6e6f486f5a8f327e",
118+
mac154SAddr: "7e8b0635000047f67e",
119+
mac154PANId: "7e8c0636d9c57d5d307e",
120+
macRxOnWhenIdleMode: "7e8d060000e68c7e",
121+
macRawStreamEnabled: "7e8e06370108437e",
122+
phyTxPowerGet: "7e81062505f47d317e",
123+
phyRSSIGet: "7e820626ef05567e",
124+
phyRXSensitivityGet: "7e830627a6a38c7e",
125+
phyCCAThresholdGet: "7e8406000297567e",
126+
};
127+
// const STOP_FRAMES_SILABS = {
128+
// macRawStreamEnabled: "7e8b063700d63c7e",
129+
// phyEnabled: "7e8c0620006eb37e",
130+
// }
131+
// const STOP_FRAMES_TI = {
132+
// macRawStreamEnabled: "7e8c063700f76b7e",
133+
// phyEnabled: "7e8d062000d5af7e",
134+
// }
135+
83136
describe("OT RCP Driver", () => {
84137
let wiresharkSeqNum: number;
85138
let wiresharkSocket: Socket | undefined;
@@ -146,50 +199,50 @@ describe("OT RCP Driver", () => {
146199
return Buffer.from(encHdlcFrame.data.subarray(0, encHdlcFrame.length));
147200
};
148201

149-
const mockGetPropertyPayload = (hex: string): SpinelFrame => decodeSpinelFrame(decodeHdlcFrame(Buffer.from(hex, "hex")));
150-
151-
const mockStart = async (driver: OTRCPDriver, loadState = true, timeoutReset = false) => {
202+
const mockStart = async (driver: OTRCPDriver, loadState = true, timeoutReset = false, frames = START_FRAMES_SILABS) => {
152203
if (driver) {
153204
let loadStateSpy: ReturnType<typeof vi.spyOn> | undefined;
154205

155206
if (!loadState) {
156207
loadStateSpy = vi.spyOn(driver, "loadState").mockResolvedValue(undefined);
157208
}
158209

159-
const getPropertySpy = vi
160-
.spyOn(driver, "getProperty")
161-
.mockResolvedValueOnce(mockGetPropertyPayload("7e8106010403db0a7e")) // PROTOCOL_VERSION
162-
.mockResolvedValueOnce(
163-
mockGetPropertyPayload(
164-
"7e820602534c2d4f50454e5448524541442f322e352e322e305f4769744875622d3166636562323235623b2045465233323b204d617220313920323032352031333a34353a343400b5dc7e",
165-
),
166-
) // NCP_VERSION
167-
.mockResolvedValueOnce(mockGetPropertyPayload("7e83060303573a7e")) // INTERFACE_TYPE
168-
.mockResolvedValueOnce(mockGetPropertyPayload("7e8406b0010a681f7e")) // RCP_API_VERSION
169-
.mockResolvedValueOnce(mockGetPropertyPayload("7e8506b101048ea77e")); // RCP_MIN_HOST_API_VERSION
170-
171-
const waitForResetSpy = vi.spyOn(driver, "waitForReset").mockImplementationOnce(async () => {
172-
const p = driver.waitForReset();
173-
174-
if (timeoutReset) {
175-
await vi.advanceTimersByTimeAsync(5500);
176-
} else {
177-
driver.parser._transform(Buffer.from(RESET_POWER_ON_FRAME_HEX, "hex"), "utf8", () => {});
178-
await vi.advanceTimersByTimeAsync(10);
210+
let i = -1;
211+
const orderedFrames = [
212+
frames.protocolVersion,
213+
frames.ncpVersion,
214+
frames.interfaceType,
215+
frames.rcpAPIVersion,
216+
frames.rcpMinHostAPIVersion,
217+
frames.resetPowerOn,
218+
];
219+
220+
const reply = async () => {
221+
await vi.advanceTimersByTimeAsync(5);
222+
223+
// skip cancel byte
224+
if (i >= 0) {
225+
if (i === 5 && timeoutReset) {
226+
await vi.advanceTimersByTimeAsync(5500);
227+
}
228+
229+
driver.parser._transform(Buffer.from(orderedFrames[i], "hex"), "utf8", () => {});
230+
await vi.advanceTimersByTimeAsync(5);
179231
}
180232

181-
await p;
182-
});
233+
i++;
183234

184-
await driver.start();
185-
186-
nextTidFromStartup += 1; // sendCommand RESET
235+
if (i === orderedFrames.length) {
236+
driver.writer.removeListener("data", reply);
237+
}
238+
};
187239

240+
driver.writer.on("data", reply);
241+
await driver.start();
188242
loadStateSpy?.mockRestore();
189-
getPropertySpy.mockRestore();
190-
waitForResetSpy.mockRestore();
191-
192243
await vi.advanceTimersByTimeAsync(100); // flush
244+
245+
nextTidFromStartup = driver.currentSpinelTID + 1;
193246
}
194247
};
195248

@@ -207,17 +260,41 @@ describe("OT RCP Driver", () => {
207260

208261
await vi.advanceTimersByTimeAsync(100); // flush
209262
}
263+
264+
nextTidFromStartup = 1;
210265
};
211266

212-
const mockFormNetwork = async (driver: OTRCPDriver, registerTimers = false) => {
267+
const mockFormNetwork = async (driver: OTRCPDriver, registerTimers = false, frames = FORM_FRAMES_SILABS) => {
213268
if (driver) {
214-
const setPropertySpy = vi.spyOn(driver, "setProperty").mockResolvedValue();
215-
const getPropertySpy = vi
216-
.spyOn(driver, "getProperty")
217-
.mockResolvedValueOnce(mockGetPropertyPayload("7e8106257d3343647e")) // PHY_TX_POWER
218-
.mockResolvedValueOnce(mockGetPropertyPayload("7e82062695d88a7e")) // PHY_RSSI
219-
.mockResolvedValueOnce(mockGetPropertyPayload("7e8306279c7a127e")) // PHY_RX_SENSITIVITY
220-
.mockResolvedValueOnce(mockGetPropertyPayload("7e840624b5f0d37e")); // PHY_CCA_THRESHOLD
269+
let i = 0;
270+
const orderedFrames = [
271+
frames.phyEnabled,
272+
frames.phyChan,
273+
frames.phyTxPowerSet,
274+
frames.mac154LAddr,
275+
frames.mac154SAddr,
276+
frames.mac154PANId,
277+
frames.macRxOnWhenIdleMode,
278+
frames.macRawStreamEnabled,
279+
frames.phyTxPowerGet,
280+
frames.phyRSSIGet,
281+
frames.phyRXSensitivityGet,
282+
frames.phyCCAThresholdGet,
283+
];
284+
285+
const reply = async () => {
286+
await vi.advanceTimersByTimeAsync(5);
287+
driver.parser._transform(Buffer.from(orderedFrames[i], "hex"), "utf8", () => {});
288+
await vi.advanceTimersByTimeAsync(5);
289+
290+
i++;
291+
292+
if (i === orderedFrames.length) {
293+
driver.writer.removeListener("data", reply);
294+
}
295+
};
296+
297+
driver.writer.on("data", reply);
221298

222299
let registerTimersSpy: ReturnType<typeof vi.spyOn> | undefined;
223300

@@ -229,11 +306,11 @@ describe("OT RCP Driver", () => {
229306

230307
await driver.formNetwork();
231308

232-
setPropertySpy.mockRestore();
233-
getPropertySpy.mockRestore();
234309
registerTimersSpy?.mockRestore();
235310

236311
await vi.advanceTimersByTimeAsync(100); // flush
312+
313+
nextTidFromStartup = driver.currentSpinelTID + 1;
237314
}
238315
};
239316

@@ -269,7 +346,7 @@ describe("OT RCP Driver", () => {
269346
expect(sendZigbeeNWKLinkStatusSpy).toHaveBeenCalledTimes(1 + 1); // *2 by spy mock
270347
expect(sendZigbeeNWKRouteReqSpy).toHaveBeenCalledTimes(1 + 1); // *2 by spy mock
271348

272-
nextTidFromStartup += 2;
349+
nextTidFromStartup = driver.currentSpinelTID + 1;
273350

274351
return [linksSpy, manyToOneSpy, destination16Spy];
275352
}
@@ -601,9 +678,11 @@ describe("OT RCP Driver", () => {
601678
await expect(driver.resetNetwork()).rejects.toThrow("Cannot reset network after state already loaded");
602679
});
603680

604-
it("forms network", async () => {
605-
await mockStart(driver);
606-
await mockFormNetwork(driver);
681+
it("starts & forms network - Silabs", async () => {
682+
const consoleInfoSpy = vi.spyOn(console, "info");
683+
684+
await mockStart(driver, true, false, START_FRAMES_SILABS);
685+
await mockFormNetwork(driver, false, FORM_FRAMES_SILABS);
607686

608687
expect(driver.isNetworkUp()).toStrictEqual(true);
609688
expect(driver.protocolVersionMajor).toStrictEqual(4);
@@ -612,6 +691,33 @@ describe("OT RCP Driver", () => {
612691
expect(driver.interfaceType).toStrictEqual(3);
613692
expect(driver.rcpAPIVersion).toStrictEqual(10);
614693
expect(driver.rcpMinHostAPIVersion).toStrictEqual(4);
694+
695+
expect(consoleInfoSpy).toHaveBeenCalledWith(
696+
expect.stringContaining(
697+
"ot-rcp-driver: ======== Network started (PHY: txPower=19dBm rssi=-104dBm rxSensitivity=-100dBm ccaThreshold=-75dBm) ========",
698+
),
699+
);
700+
});
701+
702+
it("starts & forms network - TI", async () => {
703+
const consoleInfoSpy = vi.spyOn(console, "info");
704+
705+
await mockStart(driver, true, false, START_FRAMES_TI);
706+
await mockFormNetwork(driver, false, FORM_FRAMES_TI);
707+
708+
expect(driver.isNetworkUp()).toStrictEqual(true);
709+
expect(driver.protocolVersionMajor).toStrictEqual(4);
710+
expect(driver.protocolVersionMinor).toStrictEqual(3);
711+
expect(driver.ncpVersion).toStrictEqual("OPENTHREAD/1.4.0-Koenkk-2025.2.1; CC13XX_CC26XX; Feb 3 2025 21:00:02");
712+
expect(driver.interfaceType).toStrictEqual(3);
713+
expect(driver.rcpAPIVersion).toStrictEqual(11);
714+
expect(driver.rcpMinHostAPIVersion).toStrictEqual(4);
715+
716+
expect(consoleInfoSpy).toHaveBeenCalledWith(
717+
expect.stringContaining(
718+
"ot-rcp-driver: ======== Network started (PHY: txPower=5dBm rssi=-17dBm rxSensitivity=-90dBm ccaThreshold=undefineddBm) ========",
719+
),
720+
);
615721
});
616722

617723
it("throws when trying to form network before state is loaded", async () => {

test/vitest.config.mts

+5-1
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,11 @@ export default defineConfig({
1818
reporter: ["text", "html"],
1919
reportOnFailure: false,
2020
thresholds: {
21-
100: true,
21+
/** current dev status, should maintain above this */
22+
statements: 70,
23+
functions: 75,
24+
branches: 75,
25+
lines: 70,
2226
},
2327
},
2428
},

0 commit comments

Comments
 (0)