zigbee-herdsman
Version:
An open source ZigBee gateway solution with node.js.
558 lines (494 loc) • 20.3 kB
text/typescript
/* v8 ignore start */
import {EventEmitter} from "node:events";
import {logger} from "../../../utils/logger";
import * as Zdo from "../../../zspec/zdo";
import PARAM, {
type Command,
type DataStateResponse,
type GpDataInd,
type ParamChannel,
type ParamChannelMask,
type ParamExtPanId,
type ParamMac,
type ParamNwkAddr,
type ParamPanId,
type ParamPermitJoin,
type ReceivedDataResponse,
type Request,
} from "./constants";
import {apsBusyQueue, busyQueue, enableRTS, enableRtsTimeout} from "./driver";
const NS = "zh:deconz:frameparser";
const MIN_BUFFER_SIZE = 3;
const littleEndian = true;
const lastReceivedGpInd = {srcId: 0, commandId: 0, frameCounter: 0};
export const frameParserEvents = new EventEmitter();
function parseReadParameterResponse(view: DataView): Command | null {
const parameterId = view.getUint8(7);
switch (parameterId) {
case PARAM.PARAM.Network.MAC: {
const mac: ParamMac = view.getBigUint64(8, littleEndian).toString(16);
let result = mac;
while (result.length < 16) {
result = `0${result}`;
}
result = `0x${result}`;
logger.debug(`MAC: ${result}`, NS);
return result;
}
case PARAM.PARAM.Network.PAN_ID: {
const panId: ParamPanId = view.getUint16(8, littleEndian);
logger.debug(`PANID: ${panId.toString(16)}`, NS);
return panId;
}
case PARAM.PARAM.Network.NWK_ADDRESS: {
const nwkAddr: ParamNwkAddr = view.getUint16(8, littleEndian);
logger.debug(`NWKADDR: ${nwkAddr.toString(16)}`, NS);
return nwkAddr;
}
case PARAM.PARAM.Network.EXT_PAN_ID: {
const extPanId: ParamExtPanId = view.getBigUint64(8, littleEndian).toString(16);
let res = extPanId;
while (res.length < 16) {
res = `0${res}`;
}
res = `0x${res}`;
logger.debug(`EXT_PANID: ${res}`, NS);
return res;
}
case PARAM.PARAM.Network.APS_EXT_PAN_ID: {
const apsExtPanId: ParamExtPanId = view.getBigUint64(8, littleEndian).toString(16);
let resAEPID = apsExtPanId;
while (resAEPID.length < 16) {
resAEPID = `0${resAEPID}`;
}
resAEPID = `0x${resAEPID}`;
logger.debug(`APS_EXT_PANID: ${resAEPID}`, NS);
return resAEPID;
}
case PARAM.PARAM.Network.NETWORK_KEY: {
const networkKey1 = view.getBigUint64(9).toString(16);
let res1 = networkKey1;
while (res1.length < 16) {
res1 = `0${res1}`;
}
const networkKey2 = view.getBigUint64(17).toString(16);
let res2 = networkKey2;
while (res2.length < 16) {
res2 = `0${res2}`;
}
logger.debug("NETWORK_KEY: hidden", NS);
return `0x${res1}${res2}`;
}
case PARAM.PARAM.Network.CHANNEL: {
const channel: ParamChannel = view.getUint8(8);
logger.debug(`CHANNEL: ${channel}`, NS);
return channel;
}
case PARAM.PARAM.Network.CHANNEL_MASK: {
const chMask: ParamChannelMask = view.getUint32(8, littleEndian);
logger.debug(`CHANNELMASK: ${chMask.toString(16)}`, NS);
return chMask;
}
case PARAM.PARAM.Network.PERMIT_JOIN: {
const permitJoin: ParamPermitJoin = view.getUint8(8);
logger.debug(`PERMIT_JOIN: ${permitJoin}`, NS);
return permitJoin;
}
case PARAM.PARAM.Network.WATCHDOG_TTL: {
const ttl: ParamPermitJoin = view.getUint32(8);
logger.debug(`WATCHDOG_TTL: ${ttl}`, NS);
return ttl;
}
default:
//throw new Error(`unknown parameter id ${parameterId}`);
logger.debug(`unknown parameter id ${parameterId}`, NS);
return null;
}
}
function parseReadFirmwareResponse(view: DataView): number[] {
const fw = [view.getUint8(5), view.getUint8(6), view.getUint8(7), view.getUint8(8)];
logger.debug(`read firmware version response - version: ${fw}`, NS);
return fw;
}
function parseDeviceStateResponse(view: DataView): number {
const flag = view.getUint8(5);
logger.debug(`device state: ${flag.toString(2)}`, NS);
frameParserEvents.emit("receivedDataNotification", flag);
return flag;
}
function parseChangeNetworkStateResponse(view: DataView): number {
const status = view.getUint8(2);
const state = view.getUint8(5);
logger.debug(`change network state - status: ${status} new state: ${state}`, NS);
return state;
}
function parseQuerySendDataStateResponse(view: DataView): DataStateResponse | null {
try {
const commandId = view.getUint8(0);
const seqNr = view.getUint8(1);
const status = view.getUint8(2);
if (status !== 0) {
if (status !== 5) {
logger.debug(`DATA_CONFIRM RESPONSE - seqNr.: ${seqNr} status: ${status}`, NS);
}
return null;
}
const frameLength = 7;
const payloadLength = view.getUint16(5, littleEndian);
const deviceState = view.getUint8(7);
const requestId = view.getUint8(8);
const destAddrMode = view.getUint8(9);
let destAddr64: string | undefined;
let destAddr16: number | undefined;
let destEndpoint: number | undefined;
let destAddr = "";
if (destAddrMode === PARAM.PARAM.addressMode.IEEE_ADDR) {
let res = view.getBigUint64(10, littleEndian).toString(16);
while (res.length < 16) {
res = `0${res}`;
}
destAddr64 = res;
// const buf2 = view.buffer.slice(18, view.buffer.byteLength);
destAddr = destAddr64;
} else {
destAddr16 = view.getUint16(10, littleEndian);
// const buf2 = view.buffer.slice(12, view.buffer.byteLength);
destAddr = destAddr16.toString(16);
}
if (destAddrMode === PARAM.PARAM.addressMode.NWK_ADDR || destAddrMode === PARAM.PARAM.addressMode.IEEE_ADDR) {
destEndpoint = view.getUint8(view.byteLength - 7);
}
const srcEndpoint = view.getUint8(view.byteLength - 6);
const confirmStatus = view.getInt8(view.byteLength - 5);
let newStatus = deviceState.toString(2);
for (let l = 0; l <= 8 - newStatus.length; l++) {
newStatus = `0${newStatus}`;
}
// resolve send data request promise
const i = apsBusyQueue.findIndex((r: Request) => r.request && r.request.requestId === requestId);
if (i < 0) {
return null;
}
clearTimeout(enableRtsTimeout);
enableRTS(); // enable ReadyToSend because confirm received
const req: Request = apsBusyQueue[i];
// TODO timeout (at driver.ts)
if (confirmStatus !== 0) {
// reject if status is not SUCCESS
//logger.debug("REJECT APS_REQUEST - request id: " + requestId + " confirm status: " + confirmStatus, NS);
req.reject(new Error(`confirmStatus=${confirmStatus}`));
} else {
//logger.debug("RESOLVE APS_REQUEST - request id: " + requestId + " confirm status: " + confirmStatus, NS);
req.resolve(confirmStatus);
}
//remove from busyqueue
apsBusyQueue.splice(i, 1);
logger.debug(`DATA_CONFIRM RESPONSE - destAddr: 0x${destAddr} request id: ${requestId} confirm status: ${confirmStatus}`, NS);
frameParserEvents.emit("receivedDataNotification", deviceState);
return {
commandId,
seqNr,
status,
frameLength,
payloadLength,
deviceState,
requestId,
destAddrMode,
destAddr16,
destAddr64,
destEndpoint,
srcEndpoint,
confirmStatus,
};
} catch (error) {
logger.debug(`DATA_CONFIRM RESPONSE - ${error}`, NS);
return null;
}
}
function parseReadReceivedDataResponse(view: DataView): ReceivedDataResponse | null {
// min 28 bytelength
try {
let buf2: ArrayBuffer;
let buf3: ArrayBuffer;
const commandId = view.getUint8(0);
const seqNr = view.getUint8(1);
const status = view.getUint8(2);
if (status !== 0) {
if (status !== 5) {
logger.debug(`DATA_INDICATION RESPONSE - seqNr.: ${seqNr} status: ${status}`, NS);
}
return null;
}
const frameLength = view.getUint16(3, littleEndian);
const payloadLength = view.getUint16(5, littleEndian);
const deviceState = view.getUint8(7);
const destAddrMode = view.getUint8(8);
let destAddr64: string | undefined;
let destAddr16: number | undefined;
let destAddr = "";
if (destAddrMode === PARAM.PARAM.addressMode.IEEE_ADDR) {
let res = view.getBigUint64(9, littleEndian).toString(16);
while (res.length < 16) {
res = `0${res}`;
}
destAddr64 = res;
buf2 = view.buffer.slice(17, view.buffer.byteLength);
destAddr = destAddr64;
} else {
destAddr16 = view.getUint16(9, littleEndian);
buf2 = view.buffer.slice(11, view.buffer.byteLength);
destAddr = destAddr16.toString(16);
}
view = new DataView(buf2);
const destEndpoint = view.getUint8(0);
const srcAddrMode = view.getUint8(1);
let srcAddr64: string | undefined;
let srcAddr16: number | undefined;
let srcAddr = "";
if (srcAddrMode === PARAM.PARAM.addressMode.NWK_ADDR || srcAddrMode === 0x04) {
srcAddr16 = view.getUint16(2, littleEndian);
buf3 = view.buffer.slice(4, view.buffer.byteLength);
srcAddr = srcAddr16.toString(16);
}
if (srcAddrMode === PARAM.PARAM.addressMode.IEEE_ADDR || srcAddrMode === 0x04) {
let res = view.getBigUint64(2, littleEndian).toString(16);
while (res.length < 16) {
res = `0${res}`;
}
srcAddr64 = res;
buf3 = view.buffer.slice(10, view.buffer.byteLength);
srcAddr = srcAddr64;
}
// biome-ignore lint/style/noNonNullAssertion: ignored using `--suppress`
view = new DataView(buf3!); // XXX: not validated?
const srcEndpoint = view.getUint8(0);
const profileId = view.getUint16(1, littleEndian);
const clusterId = view.getUint16(3, littleEndian);
const asduLength = view.getUint16(5, littleEndian);
const asduPayload = Buffer.from(view.buffer.slice(7, asduLength + 7));
const lqi = view.getUint8(view.byteLength - 8);
const rssi = view.getInt8(view.byteLength - 3);
let newStatus = deviceState.toString(2);
for (let l = 0; l <= 8 - newStatus.length; l++) {
newStatus = `0${newStatus}`;
}
logger.debug(
`DATA_INDICATION RESPONSE - seqNr. ${seqNr} srcAddr: 0x${srcAddr} destAddr: 0x${destAddr} profile id: 0x${profileId.toString(16)} cluster id: 0x${clusterId.toString(16)} lqi: ${lqi}`,
NS,
);
logger.debug(`response payload: ${asduPayload.toString("hex")}`, NS);
frameParserEvents.emit("receivedDataNotification", deviceState);
const response: ReceivedDataResponse = {
commandId,
seqNr,
status,
frameLength,
payloadLength,
deviceState,
destAddrMode,
destAddr16,
destAddr64,
destEndpoint,
srcAddrMode,
srcAddr16,
srcAddr64,
srcEndpoint,
profileId,
clusterId,
asduLength,
asduPayload,
lqi,
rssi,
zdo: profileId === Zdo.ZDO_PROFILE_ID ? Zdo.Buffalo.readResponse(true, clusterId, asduPayload) : undefined,
};
frameParserEvents.emit("receivedDataPayload", response);
return response;
} catch (error) {
logger.debug(`DATA_INDICATION RESPONSE - ${error}`, NS);
return null;
}
}
function parseEnqueueSendDataResponse(view: DataView): number | null {
try {
const status = view.getUint8(2);
const requestId = view.getUint8(8);
const deviceState = view.getUint8(7);
logger.debug(`DATA_REQUEST RESPONSE - request id: ${requestId} status: ${status}`, NS);
frameParserEvents.emit("receivedDataNotification", deviceState);
return deviceState;
} catch (error) {
logger.debug(`parseEnqueueSendDataResponse - ${error}`, NS);
return null;
}
}
function parseWriteParameterResponse(view: DataView): number | null {
try {
const parameterId = view.getUint8(7);
logger.debug(`write parameter response - parameter id: ${parameterId} - status: ${view.getUint8(2)}`, NS);
return parameterId;
} catch (error) {
logger.debug(`parseWriteParameterResponse - ${error}`, NS);
return null;
}
}
function parseReceivedDataNotification(view: DataView): number | null {
try {
const deviceState = view.getUint8(5);
logger.debug(`DEVICE_STATE changed: ${deviceState.toString(2)}`, NS);
frameParserEvents.emit("receivedDataNotification", deviceState);
return deviceState;
} catch (error) {
logger.debug(`parseReceivedDataNotification - ${error}`, NS);
return null;
}
}
function parseGreenPowerDataIndication(view: DataView): GpDataInd | null {
try {
let id: number;
let rspId: number;
let options: number;
let srcId: number;
let frameCounter: number;
let commandId: number;
let commandFrameSize: number;
let commandFrame: Buffer;
const seqNr = view.getUint8(1);
if (view.byteLength < 30) {
logger.debug("GP data notification", NS);
id = 0x00; // 0 = notification, 4 = commissioning
rspId = 0x01; // 1 = pairing, 2 = commissioning
options = 0;
view.getUint16(7, littleEndian); // frame ctrl field(7) ext.fcf(8)
srcId = view.getUint32(9, littleEndian);
frameCounter = view.getUint32(13, littleEndian);
commandId = view.getUint8(17);
commandFrameSize = view.byteLength - 18 - 6; // cut 18 from begin and 4 (sec mic) and 2 from end (cfc)
commandFrame = Buffer.from(view.buffer.slice(18, commandFrameSize + 18));
} else {
logger.debug("GP commissioning notification", NS);
id = 0x04; // 0 = notification, 4 = commissioning
rspId = 0x01; // 1 = pairing, 2 = commissioning
options = view.getUint16(14, littleEndian); // opt(14) ext.opt(15)
srcId = view.getUint32(8, littleEndian);
frameCounter = view.getUint32(36, littleEndian);
commandId = view.getUint8(12);
commandFrameSize = view.byteLength - 13 - 2; // cut 13 from begin and 2 from end (cfc)
commandFrame = Buffer.from(view.buffer.slice(13, commandFrameSize + 13));
}
const ind: GpDataInd = {
rspId,
seqNr,
id,
options,
srcId,
frameCounter,
commandId,
commandFrameSize,
commandFrame,
};
if (!(lastReceivedGpInd.srcId === srcId && lastReceivedGpInd.commandId === commandId && lastReceivedGpInd.frameCounter === frameCounter)) {
lastReceivedGpInd.srcId = srcId;
lastReceivedGpInd.commandId = commandId;
lastReceivedGpInd.frameCounter = frameCounter;
//logger.debug(`GP_DATA_INDICATION - src id: ${srcId} cmd id: ${commandId} frameCounter: ${frameCounter}`, NS);
logger.debug(
`GP_DATA_INDICATION - src id: 0x${srcId.toString(16)} cmd id: 0x${commandId.toString(16)} frameCounter: 0x${frameCounter.toString(16)}`,
NS,
);
frameParserEvents.emit("receivedGreenPowerIndication", ind);
}
return ind;
} catch (error) {
logger.debug(`GREEN_POWER INDICATION - ${error}`, NS);
return null;
}
}
function parseMacPollCommand(_view: DataView): number {
//logger.debug("Received command MAC_POLL", NS);
return 28;
}
function parseBeaconRequest(_view: DataView): number {
logger.debug("Received Beacon Request", NS);
return 31;
}
function parseUnknownCommand(view: DataView): number {
const id = view.getUint8(0);
logger.debug(`received unknown command - id ${id}`, NS);
return id;
}
function getParserForCommandId(id: number): (view: DataView) => Command | object | number | null {
switch (id) {
case PARAM.PARAM.FrameType.ReadParameter:
return parseReadParameterResponse;
case PARAM.PARAM.FrameType.WriteParameter:
return parseWriteParameterResponse;
case PARAM.PARAM.FrameType.ReadFirmwareVersion:
return parseReadFirmwareResponse;
case PARAM.PARAM.FrameType.ReadDeviceState:
return parseDeviceStateResponse;
case PARAM.PARAM.APS.DATA_INDICATION:
return parseReadReceivedDataResponse;
case PARAM.PARAM.APS.DATA_REQUEST:
return parseEnqueueSendDataResponse;
case PARAM.PARAM.APS.DATA_CONFIRM:
return parseQuerySendDataStateResponse;
case PARAM.PARAM.FrameType.DeviceStateChanged:
return parseReceivedDataNotification;
case PARAM.PARAM.NetworkState.CHANGE_NETWORK_STATE:
return parseChangeNetworkStateResponse;
case PARAM.PARAM.FrameType.GreenPowerDataInd:
return parseGreenPowerDataIndication;
case 28:
return parseMacPollCommand;
case 31:
return parseBeaconRequest;
default:
return parseUnknownCommand;
//throw new Error(`unknown command id ${id}`);
}
}
function processFrame(frame: Uint8Array): void {
const [seqNumber, status, command, commandId] = parseFrame(frame);
//logger.debug(`process frame with seq: ${seqNumber} status: ${status}`, NS);
let queue = busyQueue;
if (commandId === PARAM.PARAM.APS.DATA_INDICATION || commandId === PARAM.PARAM.APS.DATA_REQUEST || commandId === PARAM.PARAM.APS.DATA_CONFIRM) {
queue = apsBusyQueue;
}
const i = queue.findIndex((r: Request) => r.seqNumber === seqNumber);
if (i < 0) {
return;
}
const req: Request = queue[i];
if (commandId === PARAM.PARAM.APS.DATA_REQUEST) {
// if confirm is true resolve request only when data confirm arrives
// TODO only return if a confirm was requested. if no confirm needed: go ahead
//if (req.confirm === true) {
return;
//}
}
//remove from busyqueue
queue.splice(i, 1);
if (status !== 0) {
// reject if status is not SUCCESS
//logger.debug("REJECT REQUEST", NS);
req.reject(new Error(`status=${status}`));
} else {
//logger.debug("RESOLVE REQUEST", NS);
req.resolve(command);
}
}
function parseFrame(frame: Uint8Array): [number | null, number | null, ReturnType<ReturnType<typeof getParserForCommandId>>, number | null] {
if (frame.length < MIN_BUFFER_SIZE) {
logger.debug("received frame size to small - discard frame", NS);
return [null, null, null, null];
}
const view = new DataView(frame.buffer);
const commandId = view.getUint8(0);
const seqNumber = view.getUint8(1);
const status = view.getUint8(2);
//const frameLength = view.getUint16(3, littleEndian);
//const payloadLength = view.getUint16(5, littleEndian);
const parser = getParserForCommandId(commandId);
return [seqNumber, status, parser(view), commandId];
}
export default processFrame;