ember-zli
Version:
Interact with EmberZNet-based adapters using zigbee-herdsman 'ember' driver
170 lines (169 loc) • 8.71 kB
JavaScript
import EventEmitter from "node:events";
import { EzspBuffalo } from "zigbee-herdsman/dist/adapter/ember/ezsp/buffalo.js";
import { logger } from "../index.js";
import { CPC_DEFAULT_COMMAND_TIMEOUT, CPC_FLAG_UNNUMBERED_POLL_FINAL, CPC_HDLC_ADDRESS_POS, CPC_HDLC_CONTROL_FRAME_TYPE_SHIFT, CPC_HDLC_CONTROL_POS, CPC_HDLC_CONTROL_UNNUMBERED_TYPE_SHIFT, CPC_HDLC_FCS_SIZE, CPC_HDLC_FLAG_POS, CPC_HDLC_FLAG_VAL, CPC_HDLC_FRAME_TYPE_UNNUMBERED, CPC_HDLC_HCS_POS, CPC_HDLC_HEADER_RAW_SIZE, CPC_HDLC_HEADER_SIZE, CPC_HDLC_LENGTH_POS, CPC_PAYLOAD_LENGTH_MAX, CPC_PROPERTY_ID_BOOTLOADER_REBOOT_MODE, CPC_PROPERTY_ID_SECONDARY_CPC_VERSION, CPC_SERVICE_ENDPOINT_ID_SYSTEM, CPC_SYSTEM_COMMAND_HEADER_SIZE, CPC_SYSTEM_REBOOT_MODE_BOOTLOADER, } from "./consts.js";
import { CpcSystemCommandId, CpcSystemStatus } from "./enums.js";
import { Transport, TransportEvent } from "./transport.js";
import { computeCRC16 } from "./utils.js";
const NS = { namespace: "cpc" };
export var CpcEvent;
(function (CpcEvent) {
CpcEvent["FAILED"] = "failed";
})(CpcEvent || (CpcEvent = {}));
export class Cpc extends EventEmitter {
transport;
buffalo;
sequence;
waiter;
constructor(portConf) {
super();
this.sequence = 0;
this.waiter = undefined;
this.transport = new Transport(portConf);
this.buffalo = new EzspBuffalo(Buffer.alloc(CPC_PAYLOAD_LENGTH_MAX), 0);
this.transport.on(TransportEvent.FAILED, this.onTransportFailed.bind(this));
this.transport.on(TransportEvent.DATA, this.onTransportData.bind(this));
}
async cpcGetVersion() {
this.buffalo.setPosition(0);
this.buffalo.writeUInt32(CPC_PROPERTY_ID_SECONDARY_CPC_VERSION);
// req: 14 00 0a00 c4 55d3 02 01 0400 03000000 baaa
// rsp: 14 00 1600 c4 57e5 06 01 1000 03000000 04000000 05000000 00000000 6d3c
const result = await this.sendSystemUFrame(CpcSystemCommandId.PROP_VALUE_GET);
if (!result) {
throw new Error("Invalid result from PROP_VALUE_GET(SECONDARY_CPC_VERSION) response");
}
// const propertyId = result.payload.readUInt32LE(0)
const major = result.payload.readUInt32LE(4);
const minor = result.payload.readUInt32LE(8);
const patch = result.payload.readUInt32LE(12);
return `${major}.${minor}.${patch}`;
}
async cpcLaunchStandaloneBootloader() {
this.buffalo.setPosition(0);
this.buffalo.writeUInt32(CPC_PROPERTY_ID_BOOTLOADER_REBOOT_MODE);
this.buffalo.writeUInt32(CPC_SYSTEM_REBOOT_MODE_BOOTLOADER);
// req: 14 00 0e00 c4 950f 02 01 0800 02020000 01000000 190d
// rsp: 14 00 0e00 c4 950f 06 01 0800 02020000 01000000 cd00
const result = await this.sendSystemUFrame(CpcSystemCommandId.PROP_VALUE_SET);
if (!result) {
throw new Error("Invalid result from PROP_VALUE_SET(BOOTLOADER_REBOOT_MODE) response.");
}
const status = result.payload[0];
// as of 4.5.0, this is actually returning UNIMPLEMENTED
if (status !== CpcSystemStatus.OK && status !== CpcSystemStatus.UNIMPLEMENTED) {
return status;
}
this.buffalo.setPosition(0);
// don't want to parse anything coming in after RESET is sent
this.transport.removeAllListeners(TransportEvent.DATA);
// req: 14 00 0300 90 b557 06 c660
await this.sendSystemUFrame(CpcSystemCommandId.RESET, true);
await new Promise((resolve) => {
setTimeout(resolve, 500);
});
return CpcSystemStatus.OK;
}
receiveSystemUFrame(data) {
if (data.length < CPC_HDLC_HEADER_RAW_SIZE) {
throw new Error(`Received invalid System UFrame length=${data.length} [${data.toString("hex")}].`);
}
const flag = data.readUInt8(CPC_HDLC_FLAG_POS);
if (flag !== CPC_HDLC_FLAG_VAL) {
throw new Error(`Received invalid System UFrame flag=${CPC_HDLC_FLAG_VAL}.`);
}
// const address = data.readUInt8(CPC_HDLC_ADDRESS_POS)
const frameLength = data.readUInt16LE(CPC_HDLC_LENGTH_POS);
const expectedFrameLength = data.length - CPC_HDLC_HEADER_RAW_SIZE;
if (expectedFrameLength !== frameLength) {
throw new Error(`Received invalid System UFrame length=${data.length} expected=${expectedFrameLength}.`);
}
const control = data.readUInt8(CPC_HDLC_CONTROL_POS);
const frameType = control >> CPC_HDLC_CONTROL_FRAME_TYPE_SHIFT;
if (frameType !== CPC_HDLC_FRAME_TYPE_UNNUMBERED) {
throw new Error(`Unsupported frame type ${frameType}.`);
}
// const unnumberedType = (control >> CPC_HDLC_CONTROL_UNNUMBERED_TYPE_SHIFT) & CPC_HDLC_CONTROL_UNNUMBERED_TYPE_MASK
const headerChecksum = data.readUInt16LE(CPC_HDLC_HEADER_SIZE);
const expectedHeaderChecksum = computeCRC16(data.subarray(0, CPC_HDLC_HEADER_SIZE)).readUInt16BE();
if (headerChecksum !== expectedHeaderChecksum) {
throw new Error(`Received invalid System UFrame headerChecksum=${headerChecksum} expected=${expectedHeaderChecksum}.`);
}
let i = CPC_HDLC_HEADER_RAW_SIZE;
const commandId = data.readUInt8(i++);
const seq = data.readUInt8(i++);
const length = data.readUInt8(i);
i += 2;
const payload = data.subarray(i, -CPC_HDLC_FCS_SIZE);
const frameChecksum = data.readUInt16LE(i + payload.length);
const expectedFrameChecksum = computeCRC16(data.subarray(CPC_HDLC_HEADER_RAW_SIZE, -CPC_HDLC_FCS_SIZE)).readUInt16BE();
if (frameChecksum !== expectedFrameChecksum) {
throw new Error(`Received invalid System UFrame frameChecksum=${frameChecksum} expected=${expectedFrameChecksum}.`);
}
const command = { commandId, seq, length, payload };
logger.debug(`Received System UFrame: ${JSON.stringify(command)}.`);
this.resolveSequence(command);
}
async sendSystemUFrame(commandId, noResponse = false) {
const payload = this.buffalo.getWritten();
this.sequence = (this.sequence + 1) & 0xff;
const header = Buffer.alloc(CPC_HDLC_HEADER_SIZE);
header.writeUInt8(CPC_HDLC_FLAG_VAL, CPC_HDLC_FLAG_POS);
header.writeUInt8(CPC_SERVICE_ENDPOINT_ID_SYSTEM, CPC_HDLC_ADDRESS_POS);
header.writeUInt16LE(CPC_SYSTEM_COMMAND_HEADER_SIZE + payload.length + CPC_HDLC_FCS_SIZE, CPC_HDLC_LENGTH_POS);
header.writeUInt8((CPC_HDLC_FRAME_TYPE_UNNUMBERED << CPC_HDLC_CONTROL_FRAME_TYPE_SHIFT) |
(CPC_FLAG_UNNUMBERED_POLL_FINAL << CPC_HDLC_CONTROL_UNNUMBERED_TYPE_SHIFT), CPC_HDLC_CONTROL_POS);
const buffer = Buffer.alloc(CPC_HDLC_HEADER_RAW_SIZE + CPC_SYSTEM_COMMAND_HEADER_SIZE + payload.length + CPC_HDLC_FCS_SIZE);
buffer.set(header, 0);
const headerChecksum = computeCRC16(header).readUInt16BE();
buffer.writeUInt16LE(headerChecksum, CPC_HDLC_HCS_POS);
let i = CPC_HDLC_HEADER_RAW_SIZE;
buffer.writeUInt8(commandId, i++);
buffer.writeUInt8(this.sequence, i++);
buffer.writeUInt16LE(payload.length, i);
i += 2;
buffer.set(payload, i);
i += payload.length;
const frameChecksum = computeCRC16(buffer.subarray(CPC_HDLC_HEADER_RAW_SIZE, i)).readUInt16BE();
buffer.writeUInt16LE(frameChecksum, i);
this.transport.write(buffer);
if (noResponse) {
return undefined;
}
return await this.waitForSequence(this.sequence, CPC_DEFAULT_COMMAND_TIMEOUT);
}
async start() {
return await this.transport.initPort();
}
async stop() {
await this.transport.close(false);
}
async onTransportData(received) {
logger.debug(`Received transport data: ${received.toString("hex")}.`, NS);
this.receiveSystemUFrame(received);
}
onTransportFailed() {
this.emit(CpcEvent.FAILED);
}
resolveSequence(command) {
if (this.waiter?.sequence === command.seq) {
clearTimeout(this.waiter.timeout);
this.waiter.resolve(command);
this.waiter = undefined;
}
}
waitForSequence(sequence, timeout = CPC_DEFAULT_COMMAND_TIMEOUT) {
return new Promise((resolve) => {
this.waiter = {
resolve,
sequence,
timeout: setTimeout(() => {
const msg = `Timed out waiting for sequence(${sequence}) after ${timeout}ms.`;
this.waiter = undefined;
logger.error(msg, NS);
this.emit(CpcEvent.FAILED);
}, timeout),
};
});
}
}