import SerialPort from 'serialport'; import Frame from './frame'; import {EventEmitter} from 'events'; import {Debug} from '../debug'; import SerialPortUtils from "../../serialPortUtils"; import SocketPortUtils from "../../socketPortUtils"; import net from "net"; import {Queue} from "../../../utils"; import {SerialPortOptions} from "../../tstype"; import {ZiGateCommandCode, ZiGateMessageCode, ZiGateObjectPayload} from "./constants"; import ZiGateObject from "./ziGateObject"; import {ZclFrame} from "../../../zcl"; import Waitress from "../../../utils/waitress"; import {ZiGateResponseMatcher, ZiGateResponseMatcherRule} from "./commandType"; const debug = Debug('driver'); const autoDetectDefinitions = [ {manufacturer: 'zz', vendorId: 'id', productId: '00'}, ]; const timeouts = { reset: 30000, default: 3000, }; type WaitressMatcher = { ziGateObject: ZiGateObject, rules: ZiGateResponseMatcher }; function zeroPad(number: number, size?: number): string { return (number).toString(16).padStart(size || 4, '0'); } function resolve(path: string | [], obj: { [k: string]: any }, separator = '.'): any { const properties = Array.isArray(path) ? path : path.split(separator); return properties.reduce((prev, curr) => prev && prev[curr], obj); } export default class ZiGate extends EventEmitter { private path: string; private baudRate: number; private rtscts: boolean; private initialized: boolean; // private timeoutResetTimeout: any; // private apsRequestFreeSlots: number; private parser: EventEmitter; private serialPort: SerialPort; private seqNumber: number; private portType: 'serial' | 'socket'; private socketPort: net.Socket; private queue: Queue; // private waitress: Waitress; public portWrite: any; private waitress: Waitress; public constructor(path: string, serialPortOptions: SerialPortOptions) { super(); // eslint-disable-next-line prefer-rest-params debug.log('construct', arguments); this.path = path; this.baudRate = typeof serialPortOptions.baudRate === 'number' ? serialPortOptions.baudRate : 115200; this.rtscts = typeof serialPortOptions.rtscts === 'boolean' ? serialPortOptions.rtscts : false; this.portType = SocketPortUtils.isTcpPath(path) ? 'socket' : 'serial'; this.initialized = false; this.queue = new Queue(1); this.waitress = new Waitress( this.waitressValidator, this.waitressTimeoutFormatter); } public static async isValidPath(path: string): Promise { return SerialPortUtils.is(path, autoDetectDefinitions); } public static async autoDetectPath(): Promise { const paths = await SerialPortUtils.find(autoDetectDefinitions); return paths.length > 0 ? paths[0] : null; } public open(): Promise { return this.portType === 'serial' ? this.openSerialPort() : this.openSocketPort(); } public close(): Promise { debug.error('close'); return new Promise((resolve, reject) => { if (this.initialized) { this.initialized = false; this.portWrite = null; if (this.portType === 'serial') { this.serialPort.flush((): void => { this.serialPort.close((error): void => { this.serialPort = null; error == null ? resolve() : reject(new Error(`Error while closing serialPort '${error}'`)); this.emit('close'); }); }); } else { // @ts-ignore this.socketPort.destroy((error?: Error): void => { this.socketPort = null; error == null ? resolve() : reject(new Error(`Error while closing serialPort '${error}'`)); this.emit('close'); }); } } else { resolve(); this.emit('close'); } }); } public async sendCommand(code: ZiGateCommandCode, payload?: ZiGateObjectPayload): Promise { // @TODO ? // const argument = arguments; return this.queue.execute(async () => { try { debug.log( 'Send command \x1b[42m>>>> ' + ZiGateCommandCode[code] + ' 0x' + zeroPad(code) + ' <<<<\x1b[0m ', ); debug.log('payload: ', payload); const ziGateObject = ZiGateObject.createRequest(code, payload); const frame = ziGateObject.toZiGateFrame(); const sendBuffer = frame.toBuffer(); debug.log('Send command buff: ', sendBuffer); const waiters: Promise[] = []; ziGateObject.command.response.forEach((rules) => { waiters.push(this.waitress.waitFor({ziGateObject, rules}, timeouts.default).start().promise); }); // if (ziGateObject.command.response) { // waiter = this.waitress.waitFor({ // responseType: ziGateObject.command.wait_response, // commandCode: ziGateObject.code // }, timeouts.default); // } else if (ziGateObject.command.wait_status) { // waiter = this.waitress.waitFor( // {responseType: 0x8000, commandCode: ziGateObject.code}, // timeouts.default // ); // } this.portWrite.write(sendBuffer); return Promise.race(waiters); // return new Promise(async (resolve, reject) => { // const firstResult = await Promise.race(waiters); // // if (firstResult.code === 0x8000 && firstResult.payload.status !== 0) { // reject(); // } // if (firstResult.code === 0x8000 // && firstResult.payload.status === 0 // && // ) { // && count promiss >0 // const dataResult = await Promise.race(waiters); // } //status // }); } catch (e) { debug.error(e); return new Promise((resolve, reject) => { reject(); }); } }); } public waitFor(matcher: WaitressMatcher, timeout: number = timeouts.default): { start: () => { promise: Promise; ID: number }; ID: number } { return this.waitress.waitFor(matcher, timeout); } private async openSerialPort(): Promise { this.serialPort = new SerialPort(this.path, { baudRate: this.baudRate, dataBits: 8, parity: 'none', /* one of ['none', 'even', 'mark', 'odd', 'space'] */ stopBits: 1, /* one of [1,2] */ lock: false, autoOpen: false }); this.parser = this.serialPort.pipe( new SerialPort.parsers.Delimiter( {delimiter: [Frame.STOP_BYTE], includeDelimiter: true} ), ); this.parser.on('data', this.onSerialData.bind(this)); this.portWrite = this.serialPort; return new Promise((resolve, reject): void => { this.serialPort.open(async (err: unknown): Promise => { if (err) { this.serialPort = null; this.parser = null; this.path = null; this.initialized = false; const error = `Error while opening serialPort '${err}'`; debug.error(error); reject(new Error(error)); } else { debug.log('Successfully connected ZiGate port \'' + this.path + '\''); this.serialPort.on('error', (error) => { debug.error(`serialPort error: ${error}`); }); this.serialPort.on('close', this.onPortClose.bind(this)); this.initialized = true; resolve(); } }); }); } private async openSocketPort(): Promise { const info = SocketPortUtils.parseTcpPath(this.path); debug.log(`Opening TCP socket with ${info.host}:${info.port}`); this.socketPort = new net.Socket(); this.socketPort.setNoDelay(true); this.socketPort.setKeepAlive(true, 15000); this.parser = this.socketPort.pipe( new SerialPort.parsers.Delimiter({delimiter: [Frame.STOP_BYTE], includeDelimiter: true}), ); this.parser.on('data', this.onSerialData.bind(this)); this.portWrite = this.socketPort; return new Promise((resolve, reject): void => { this.socketPort.on('connect', function () { debug.log('Socket connected'); }); // eslint-disable-next-line const self = this; this.socketPort.on('ready', async function () { debug.log('Socket ready'); self.initialized = true; resolve(); }); this.socketPort.once('close', this.onPortClose); this.socketPort.on('error', (error) => { debug.log('Socket error', error); // reject(new Error(`Error while opening socket`)); reject(); self.initialized = false; }); this.socketPort.connect(info.port, info.host); }); } private onSerialError(err: string): void { debug.error('serial error: ', err); } private onPortClose(): void { debug.log('serial closed'); this.initialized = false; this.emit('close'); } private onSerialData(buffer: Buffer): void { try { debug.log(`--- parseNext `, buffer); const frame = new Frame(buffer); const code = frame.readMsgCode(); let msgName; try { msgName = ZiGateMessageCode[code] ? ZiGateMessageCode[code] : ''; msgName += ' 0x' + zeroPad(code); } catch (e) { } debug.log(`--> parsed frame \x1b[1;34m>>>> ${msgName} <<<<`); // debug.log(frame); try { const ziGateObject = ZiGateObject.fromZiGateFrame(frame); if (ziGateObject === undefined) return; // debug.log(ziGateObject.payload, frame.readRSSI()); this.waitress.resolve(ziGateObject); if (code !== ZiGateMessageCode.Status || ziGateObject.payload.status !== 0) { debug.info(`--> frame to object `, ziGateObject.payload); } if (code === ZiGateMessageCode.DataIndication) { debug.info('raw'); let zclFrame = null; switch (ziGateObject.payload.clusterID) { case 0x8005: // @ts-ignore zclFrame = ZiGateObject.fromBufer(0x8005, ziGateObject.payload.payload); break; default: try { // @ts-ignore zclFrame = ZclFrame.fromBuffer(ziGateObject.payload.clusterID, ziGateObject.payload.payload); } catch (e) { zclFrame = null; } } this.emit('received', {ziGateObject, zclFrame}); } else if (code === ZiGateMessageCode.LeaveIndication) { debug.log('raw leave'); this.emit('LeaveIndication', {ziGateObject}); } else if (code === ZiGateMessageCode.DeviceAnnounce) { debug.log('raw announce'); this.emit('DeviceAnnounce', {ziGateObject}); } } catch (error) { this.emit('receivedRaw', {error, frame}); // debug.error(`'${error.stack}'`); } } catch (error) { debug.error(`Error while parsing to ZiGateObject '${error.stack}'`); } } private waitressTimeoutFormatter(matcher: WaitressMatcher, timeout: number): string { return `${matcher} after ${timeout}ms`; } private waitressValidator(ziGateObject: ZiGateObject, matcher: WaitressMatcher): boolean { const validator = (rule: ZiGateResponseMatcherRule): boolean => { try { let expectedValue: string | number; if (typeof rule.value === "undefined" && typeof rule.expectedProperty !== "undefined") { expectedValue = resolve(rule.expectedProperty, matcher.ziGateObject); } else { expectedValue = rule.value; } const receivedValue = resolve(rule.receivedProperty, ziGateObject); // debug.info(expectedValue, receivedValue, rule); return rule.matcher(expectedValue, receivedValue); } catch (e) { return false; } }; return matcher.rules.every(validator); } }