zigbee-herdsman
Version:
An open source ZigBee gateway solution with node.js.
357 lines • 15.7 kB
JavaScript
"use strict";
/* v8 ignore start */
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
var ownKeys = function(o) {
ownKeys = Object.getOwnPropertyNames || function (o) {
var ar = [];
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
return ar;
};
return ownKeys(o);
};
return function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
__setModuleDefault(result, mod);
return result;
};
})();
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.ZBOSSDriver = void 0;
const node_assert_1 = __importDefault(require("node:assert"));
const node_events_1 = __importDefault(require("node:events"));
const es6_1 = __importDefault(require("fast-deep-equal/es6"));
const utils_1 = require("../../utils");
const logger_1 = require("../../utils/logger");
const Zdo = __importStar(require("../../zspec/zdo"));
const commands_1 = require("./commands");
const enums_1 = require("./enums");
const frame_1 = require("./frame");
const uart_1 = require("./uart");
const NS = "zh:zboss:driv";
const MAX_INIT_ATTEMPTS = 5;
class ZBOSSDriver extends node_events_1.default {
port;
waitress;
queue;
tsn = 1; // command sequence
nwkOpt;
netInfo; // expected valid upon startup of driver
constructor(options, nwkOpt) {
super();
this.nwkOpt = nwkOpt;
this.queue = new utils_1.Queue();
this.waitress = new utils_1.Waitress(this.waitressValidator, this.waitressTimeoutFormatter);
this.port = new uart_1.ZBOSSUart(options);
this.port.on("frame", this.onFrame.bind(this));
}
async connect() {
logger_1.logger.info("Driver connecting", NS);
let status = false;
for (let i = 0; i < MAX_INIT_ATTEMPTS; i++) {
status = await this.port.resetNcp();
// fail early if we couldn't even get the port set up
if (!status) {
return status;
}
status = await this.port.start();
if (status) {
logger_1.logger.info("Driver connected", NS);
return status;
}
}
return status;
}
async reset(options = enums_1.ResetOptions.NoOptions) {
logger_1.logger.info("Driver reset", NS);
this.port.inReset = true;
await this.execCommand(enums_1.CommandId.NCP_RESET, { options }, 10000);
}
async startup(transmitPower) {
logger_1.logger.info("Driver startup", NS);
let result = "resumed";
if (await this.needsToBeInitialised(this.nwkOpt)) {
// need to check the backup
// const restore = await this.needsToBeRestore(this.nwkOpt);
const restore = false;
if (this.netInfo.joined) {
logger_1.logger.info("Leaving current network and forming new network", NS);
await this.reset(enums_1.ResetOptions.FactoryReset);
}
if (restore) {
// // restore
// logger.info('Restore network from backup', NS);
// await this.formNetwork(true);
// result = 'restored';
}
else {
// reset
logger_1.logger.info("Form network", NS);
await this.formNetwork(); // false
result = "reset";
}
}
else {
await this.execCommand(enums_1.CommandId.NWK_START_WITHOUT_FORMATION, {});
}
await this.execCommand(enums_1.CommandId.SET_TC_POLICY, { type: enums_1.PolicyType.LINK_KEY_REQUIRED, value: 0 });
await this.execCommand(enums_1.CommandId.SET_TC_POLICY, { type: enums_1.PolicyType.IC_REQUIRED, value: 0 });
await this.execCommand(enums_1.CommandId.SET_TC_POLICY, { type: enums_1.PolicyType.TC_REJOIN_ENABLED, value: 1 });
await this.execCommand(enums_1.CommandId.SET_TC_POLICY, { type: enums_1.PolicyType.IGNORE_TC_REJOIN, value: 0 });
await this.execCommand(enums_1.CommandId.SET_TC_POLICY, { type: enums_1.PolicyType.APS_INSECURE_JOIN, value: 0 });
await this.execCommand(enums_1.CommandId.SET_TC_POLICY, { type: enums_1.PolicyType.DISABLE_NWK_MGMT_CHANNEL_UPDATE, value: 0 });
await this.addEndpoint(1, 260, 0xbeef, [0x0000, 0x0003, 0x0006, 0x000a, 0x0019, 0x001a, 0x0300], [
0x0000, 0x0003, 0x0004, 0x0005, 0x0006, 0x0008, 0x0020, 0x0300, 0x0400, 0x0402, 0x0405, 0x0406, 0x0500, 0x0b01, 0x0b03, 0x0b04,
0x0702, 0x1000, 0xfc01, 0xfc02,
]);
await this.addEndpoint(242, 0xa1e0, 0x61, [], [0x0021]);
await this.execCommand(enums_1.CommandId.SET_RX_ON_WHEN_IDLE, { rxOn: 1 });
//await this.execCommand(CommandId.SET_ED_TIMEOUT, {timeout: 8});
//await this.execCommand(CommandId.SET_MAX_CHILDREN, {children: 100});
if (transmitPower != null) {
await this.execCommand(enums_1.CommandId.SET_TX_POWER, { txPower: transmitPower });
}
return result;
}
async needsToBeInitialised(options) {
let valid = true;
this.netInfo = await this.getNetworkInfo();
logger_1.logger.debug(() => `Current network parameters: ${JSON.stringify(this.netInfo)}`, NS);
if (this.netInfo) {
valid = valid && this.netInfo.nodeType === enums_1.DeviceType.COORDINATOR;
valid = valid && options.panID === this.netInfo.network.panID;
valid = valid && options.channelList.includes(this.netInfo.network.channel);
valid = valid && (0, es6_1.default)(Buffer.from(options.extendedPanID || []), Buffer.from(this.netInfo.network.extendedPanID));
}
else {
valid = false;
}
return !valid;
}
async getNetworkInfo() {
let result = await this.execCommand(enums_1.CommandId.GET_JOINED, {});
const joined = result.payload.joined === 1;
if (!joined) {
logger_1.logger.debug("Network not formed", NS);
}
result = await this.execCommand(enums_1.CommandId.GET_ZIGBEE_ROLE, {});
const nodeType = result.payload.role;
result = await this.execCommand(enums_1.CommandId.GET_LOCAL_IEEE_ADDR, { mac: 0 });
const ieeeAddr = result.payload.ieee;
result = await this.execCommand(enums_1.CommandId.GET_EXTENDED_PAN_ID, {});
// TODO: bug in extendedPanID - got reversed value
const extendedPanID = result.payload.extendedPanID.reverse();
result = await this.execCommand(enums_1.CommandId.GET_PAN_ID, {});
const panID = result.payload.panID;
result = await this.execCommand(enums_1.CommandId.GET_ZIGBEE_CHANNEL, {});
const channel = result.payload.channel;
return {
joined,
nodeType,
ieeeAddr,
network: {
panID,
extendedPanID,
channel,
},
};
}
async addEndpoint(endpoint, profileId, deviceId, inputClusters, outputClusters) {
const res = await this.execCommand(enums_1.CommandId.AF_SET_SIMPLE_DESC, {
endpoint: endpoint,
profileID: profileId,
deviceID: deviceId,
version: 0,
inputClusterCount: inputClusters.length,
outputClusterCount: outputClusters.length,
inputClusters: inputClusters,
outputClusters: outputClusters,
});
logger_1.logger.debug(() => `Adding endpoint: ${JSON.stringify(res)}`, NS);
}
getChannelMask(channels) {
return channels.reduce((mask, channel) => mask | (1 << channel), 0);
}
async formNetwork() {
const channelMask = this.getChannelMask(this.nwkOpt.channelList);
await this.execCommand(enums_1.CommandId.SET_ZIGBEE_ROLE, { role: enums_1.DeviceType.COORDINATOR });
await this.execCommand(enums_1.CommandId.SET_ZIGBEE_CHANNEL_MASK, { page: 0, mask: channelMask });
await this.execCommand(enums_1.CommandId.SET_PAN_ID, { panID: this.nwkOpt.panID });
// await this.execCommand(CommandId.SET_EXTENDED_PAN_ID, {extendedPanID: this.nwkOpt.extendedPanID});
await this.execCommand(enums_1.CommandId.SET_NWK_KEY, { nwkKey: this.nwkOpt.networkKey, index: 0 });
const res = await this.execCommand(enums_1.CommandId.NWK_FORMATION, {
len: 1,
channels: [{ page: 0, mask: channelMask }],
duration: 0x05,
distribFlag: 0x00,
distribNwk: 0x0000,
extendedPanID: this.nwkOpt.extendedPanID,
}, 20000);
logger_1.logger.debug(() => `Forming network: ${JSON.stringify(res)}`, NS);
}
async stop() {
await this.port.stop();
logger_1.logger.info("Driver stopped", NS);
}
onFrame(frame) {
logger_1.logger.debug(() => `<== Frame: ${JSON.stringify(frame)}`, NS);
const handled = this.waitress.resolve(frame);
if (!handled) {
this.emit("frame", frame);
}
}
isInitialized() {
return this.port.portOpen && !this.port.inReset;
}
async execCommand(commandId, params = {}, timeout = 10000) {
logger_1.logger.debug(() => `==> ${enums_1.CommandId[commandId]}(${commandId}): ${JSON.stringify(params)}`, NS);
if (!this.port.portOpen) {
throw new Error("Connection not initialized");
}
return await this.queue.execute(async () => {
const frame = (0, frame_1.makeFrame)(frame_1.FrameType.REQUEST, commandId, params);
frame.tsn = this.tsn;
const waiter = this.waitFor(commandId, commandId === enums_1.CommandId.NCP_RESET ? undefined : this.tsn, timeout);
this.tsn = (this.tsn + 1) & 255;
try {
logger_1.logger.debug(() => `==> FRAME: ${JSON.stringify(frame)}`, NS);
await this.port.sendFrame(frame);
const response = await waiter.start().promise;
if (response?.payload?.status !== enums_1.StatusCodeGeneric.OK) {
throw new Error(`Error on command ${enums_1.CommandId[commandId]}(${commandId}): ${JSON.stringify(response)}`);
}
return response;
}
catch (error) {
this.waitress.remove(waiter.ID);
logger_1.logger.error(`==> Error: ${error}`, NS);
throw new Error(`Failure send ${commandId}:${JSON.stringify(frame)}`);
}
});
}
waitFor(commandId, tsn, timeout = 10000) {
return this.waitress.waitFor({ commandId, tsn }, timeout);
}
waitressTimeoutFormatter(matcher, timeout) {
return `${JSON.stringify(matcher)} after ${timeout}ms`;
}
waitressValidator(payload, matcher) {
return (matcher.tsn === undefined || matcher.tsn === payload.tsn) && matcher.commandId === payload.commandId;
}
async request(ieee, profileID, clusterID, dstEp, srcEp, data) {
const payload = {
paramLength: 21,
dataLength: data.length,
addr: ieee,
profileID: profileID,
clusterID: clusterID,
dstEndpoint: dstEp,
srcEndpoint: srcEp,
radius: 3,
dstAddrMode: 3, // ADDRESS MODE ieee
txOptions: 2, // ROUTE DISCOVERY
useAlias: 0,
aliasAddr: 0,
aliasSequence: 0,
data: data,
};
return await this.execCommand(enums_1.CommandId.APSDE_DATA_REQ, payload);
}
async brequest(addr, profileID, clusterID, dstEp, srcEp, data) {
const payload = {
paramLength: 21,
dataLength: data.length,
addr: `0x${addr.toString(16).padStart(16, "0")}`,
profileID: profileID,
clusterID: clusterID,
dstEndpoint: dstEp,
srcEndpoint: srcEp,
radius: 3,
dstAddrMode: 2, // ADDRESS MODE broadcast
txOptions: 2, // ROUTE DISCOVERY
useAlias: 0,
aliasAddr: 0,
aliasSequence: 0,
data: data,
};
return await this.execCommand(enums_1.CommandId.APSDE_DATA_REQ, payload);
}
async grequest(group, profileID, clusterID, srcEp, data) {
const payload = {
paramLength: 20,
dataLength: data.length,
addr: `0x${group.toString(16).padStart(16, "0")}`,
profileID: profileID,
clusterID: clusterID,
srcEndpoint: srcEp,
radius: 3,
dstAddrMode: 1, // ADDRESS MODE group
txOptions: 2, // ROUTE DISCOVERY
useAlias: 0,
aliasAddr: 0,
aliasSequence: 0,
data: data,
};
return await this.execCommand(enums_1.CommandId.APSDE_DATA_REQ, payload);
}
async requestZdo(clusterId, payload, disableResponse) {
if (!this.port.portOpen) {
throw new Error("Connection not initialized");
}
const commandId = commands_1.ZDO_REQ_CLUSTER_ID_TO_ZBOSS_COMMAND_ID[clusterId];
(0, node_assert_1.default)(commandId !== undefined, `ZDO cluster ID '${clusterId}' not supported.`);
const cmdLog = `${Zdo.ClusterId[clusterId]}(cmd: ${commandId})`;
logger_1.logger.debug(() => `===> ZDO ${cmdLog}: ${payload.toString("hex")}`, NS);
return await this.queue.execute(async () => {
const buf = Buffer.alloc(5 + payload.length);
buf.writeInt8(0, 0);
buf.writeInt8(frame_1.FrameType.REQUEST, 1);
buf.writeUInt16LE(commandId, 2);
buf.writeUInt8(this.tsn, 4);
buf.set(payload, 5);
let waiter;
if (!disableResponse) {
waiter = this.waitFor(commandId, this.tsn, 10000);
}
this.tsn = (this.tsn + 1) & 255;
try {
await this.port.sendBuffer(buf);
if (waiter) {
return await waiter.start().promise;
}
}
catch (error) {
if (waiter) {
this.waitress.remove(waiter.ID);
}
logger_1.logger.debug(`=x=> Failed to send ${cmdLog}: ${error.stack}`, NS);
throw new Error(`Failed to send ${cmdLog}.`);
}
});
}
async ieeeByNwk(nwk) {
return (await this.execCommand(enums_1.CommandId.NWK_GET_IEEE_BY_SHORT, { nwk: nwk })).payload.ieee;
}
}
exports.ZBOSSDriver = ZBOSSDriver;
//# sourceMappingURL=driver.js.map