zigbee-herdsman
Version:
An open source ZigBee gateway solution with node.js.
279 lines • 12.9 kB
JavaScript
"use strict";
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 (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
__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 });
const assert_1 = __importDefault(require("assert"));
const events_1 = __importDefault(require("events"));
const net_1 = __importDefault(require("net"));
const utils_1 = require("../../../utils");
const logger_1 = require("../../../utils/logger");
const zdo_1 = require("../../../zspec/zdo");
const serialPort_1 = require("../../serialPort");
const socketPortUtils_1 = __importDefault(require("../../socketPortUtils"));
const Constants = __importStar(require("../constants"));
const unpi_1 = require("../unpi");
const constants_1 = require("../unpi/constants");
const definition_1 = __importDefault(require("./definition"));
const utils_2 = require("./utils");
const zpiObject_1 = __importDefault(require("./zpiObject"));
const { COMMON: { ZnpCommandStatus }, Utils: { statusDescription }, } = Constants;
const timeouts = {
SREQ: 6000,
reset: 30000,
default: 10000,
};
const NS = 'zh:zstack:znp';
class Znp extends events_1.default.EventEmitter {
path;
baudRate;
rtscts;
serialPort;
socketPort;
unpiWriter;
unpiParser;
initialized;
queue;
waitress;
constructor(path, baudRate, rtscts) {
super();
this.path = path;
this.baudRate = typeof baudRate === 'number' ? baudRate : 115200;
this.rtscts = typeof rtscts === 'boolean' ? rtscts : false;
this.initialized = false;
this.queue = new utils_1.Queue();
this.waitress = new utils_1.Waitress(this.waitressValidator, this.waitressTimeoutFormatter);
this.unpiWriter = new unpi_1.Writer();
this.unpiParser = new unpi_1.Parser();
}
onUnpiParsed(frame) {
try {
const object = zpiObject_1.default.fromUnpiFrame(frame);
logger_1.logger.debug(() => `<-- ${object}`, NS);
this.waitress.resolve(object);
this.emit('received', object);
}
catch (error) {
logger_1.logger.error(`Error while parsing to ZpiObject '${error}'`, NS);
}
}
isInitialized() {
return this.initialized;
}
onPortError(error) {
logger_1.logger.error(`Port error: ${error}`, NS);
}
onPortClose() {
logger_1.logger.info('Port closed', NS);
this.initialized = false;
this.emit('close');
}
async open() {
return socketPortUtils_1.default.isTcpPath(this.path) ? await this.openSocketPort() : await this.openSerialPort();
}
async openSerialPort() {
const options = { path: this.path, baudRate: this.baudRate, rtscts: this.rtscts, autoOpen: false };
logger_1.logger.info(`Opening SerialPort with ${JSON.stringify(options)}`, NS);
this.serialPort = new serialPort_1.SerialPort(options);
this.unpiWriter.pipe(this.serialPort);
this.serialPort.pipe(this.unpiParser);
this.unpiParser.on('parsed', this.onUnpiParsed.bind(this));
try {
await this.serialPort.asyncOpen();
logger_1.logger.info('Serialport opened', NS);
this.serialPort.once('close', this.onPortClose.bind(this));
this.serialPort.once('error', this.onPortError.bind(this));
this.initialized = true;
await this.skipBootloader();
}
catch (error) {
this.initialized = false;
if (this.serialPort.isOpen) {
this.serialPort.close();
}
throw error;
}
}
async openSocketPort() {
const info = socketPortUtils_1.default.parseTcpPath(this.path);
logger_1.logger.info(`Opening TCP socket with ${info.host}:${info.port}`, NS);
this.socketPort = new net_1.default.Socket();
this.socketPort.setNoDelay(true);
this.socketPort.setKeepAlive(true, 15000);
this.unpiWriter.pipe(this.socketPort);
this.socketPort.pipe(this.unpiParser);
this.unpiParser.on('parsed', this.onUnpiParsed.bind(this));
return await new Promise((resolve, reject) => {
this.socketPort.on('connect', function () {
logger_1.logger.info('Socket connected', NS);
});
// eslint-disable-next-line @typescript-eslint/no-this-alias
const self = this;
this.socketPort.on('ready', async function () {
logger_1.logger.info('Socket ready', NS);
await self.skipBootloader();
self.initialized = true;
resolve();
});
this.socketPort.once('close', this.onPortClose.bind(this));
this.socketPort.on('error', function () {
logger_1.logger.info('Socket error', NS);
reject(new Error(`Error while opening socket`));
self.initialized = false;
});
this.socketPort.connect(info.port, info.host);
});
}
async skipBootloader() {
try {
await this.request(constants_1.Subsystem.SYS, 'ping', { capabilities: 1 }, undefined, 250);
}
catch {
// Skip bootloader on CC2530/CC2531
// Send magic byte: https://github.com/Koenkk/zigbee2mqtt/issues/1343 to bootloader
// and give ZNP 1 second to start.
try {
logger_1.logger.info('Writing CC2530/CC2531 skip bootloader payload', NS);
this.unpiWriter.writeBuffer(Buffer.from([0xef]));
await (0, utils_1.Wait)(1000);
await this.request(constants_1.Subsystem.SYS, 'ping', { capabilities: 1 }, undefined, 250);
}
catch {
// Skip bootloader on some CC2652 devices (e.g. zzh-p)
logger_1.logger.info('Skip bootloader for CC2652/CC1352', NS);
if (this.serialPort) {
await this.serialPort.asyncSet({ dtr: false, rts: false });
await (0, utils_1.Wait)(150);
await this.serialPort.asyncSet({ dtr: false, rts: true });
await (0, utils_1.Wait)(150);
await this.serialPort.asyncSet({ dtr: false, rts: false });
await (0, utils_1.Wait)(150);
}
}
}
}
async close() {
logger_1.logger.info('closing', NS);
this.queue.clear();
if (this.initialized) {
this.initialized = false;
if (this.serialPort) {
try {
await this.serialPort.asyncFlushAndClose();
}
catch (error) {
this.emit('close');
throw error;
}
}
else {
this.socketPort.destroy();
}
}
this.emit('close');
}
async requestWithReply(subsystem, command, payload, waiterID, timeout, expectedStatuses = [ZnpCommandStatus.SUCCESS]) {
const reply = await this.request(subsystem, command, payload, waiterID, timeout, expectedStatuses);
if (reply === undefined) {
throw new Error(`Command ${command} has no reply`);
}
return reply;
}
request(subsystem, command, payload, waiterID, timeout, expectedStatuses = [ZnpCommandStatus.SUCCESS]) {
if (!this.initialized) {
throw new Error('Cannot request when znp has not been initialized yet');
}
const object = zpiObject_1.default.createRequest(subsystem, command, payload);
return this.queue.execute(async () => {
logger_1.logger.debug(() => `--> ${object}`, NS);
if (object.type === constants_1.Type.SREQ) {
const t = object.command.name === 'bdbStartCommissioning' || object.command.name === 'startupFromApp' ? 40000 : timeouts.SREQ;
const waiter = this.waitress.waitFor({ type: constants_1.Type.SRSP, subsystem: object.subsystem, command: object.command.name }, timeout || t);
this.unpiWriter.writeFrame(object.unpiFrame);
const result = await waiter.start().promise;
if (result?.payload.status !== undefined && !expectedStatuses.includes(result.payload.status)) {
if (typeof waiterID === 'number') {
this.waitress.remove(waiterID);
}
throw new Error(`--> '${object}' failed with status '${statusDescription(result.payload.status)}' (expected '${expectedStatuses.map(statusDescription)}')`);
}
else {
return result;
}
}
else if (object.type === constants_1.Type.AREQ && object.isResetCommand()) {
const waiter = this.waitress.waitFor({ type: constants_1.Type.AREQ, subsystem: constants_1.Subsystem.SYS, command: 'resetInd' }, timeout || timeouts.reset);
this.queue.clear();
this.unpiWriter.writeFrame(object.unpiFrame);
return await waiter.start().promise;
}
else {
/* istanbul ignore else */
if (object.type === constants_1.Type.AREQ) {
this.unpiWriter.writeFrame(object.unpiFrame);
}
else {
throw new Error(`Unknown type '${object.type}'`);
}
}
});
}
requestZdo(clusterId, payload, waiterID) {
return this.queue.execute(async () => {
const cmd = definition_1.default[constants_1.Subsystem.ZDO].find((c) => (0, utils_2.isMtCmdSreqZdo)(c) && c.zdoClusterId === clusterId);
(0, assert_1.default)(cmd, `Command for ZDO cluster ID '${clusterId}' not supported.`);
const unpiFrame = new unpi_1.Frame(constants_1.Type.SREQ, constants_1.Subsystem.ZDO, cmd.ID, payload);
const waiter = this.waitress.waitFor({ type: constants_1.Type.SRSP, subsystem: constants_1.Subsystem.ZDO, command: cmd.name }, timeouts.SREQ);
this.unpiWriter.writeFrame(unpiFrame);
const result = await waiter.start().promise;
if (result?.payload.status !== undefined && result.payload.status !== ZnpCommandStatus.SUCCESS) {
if (waiterID !== undefined) {
this.waitress.remove(waiterID);
}
throw new Error(`--> 'SREQ: ZDO - ${zdo_1.ClusterId[clusterId]} - ${payload.toString('hex')}' failed with status '${statusDescription(result.payload.status)}'`);
}
});
}
waitressTimeoutFormatter(matcher, timeout) {
return `${constants_1.Type[matcher.type]} - ${constants_1.Subsystem[matcher.subsystem]} - ${matcher.command} after ${timeout}ms`;
}
waitFor(type, subsystem, command, target, transid, state, timeout = timeouts.default) {
return this.waitress.waitFor({ type, subsystem, command, target, transid, state }, timeout);
}
waitressValidator(zpiObject, matcher) {
return (matcher.type === zpiObject.type &&
matcher.subsystem == zpiObject.subsystem &&
matcher.command === zpiObject.command.name &&
(matcher.target === undefined ||
(typeof matcher.target === 'number'
? matcher.target === zpiObject.payload.srcaddr
: matcher.target === zpiObject.payload.zdo?.[1]?.eui64)) &&
(matcher.transid === undefined || matcher.transid === zpiObject.payload.transid) &&
(matcher.state === undefined || matcher.state === zpiObject.payload.state));
}
}
exports.default = Znp;
//# sourceMappingURL=znp.js.map