UNPKG

knxultimate

Version:

KNX IP protocol implementation for Node. This is the ENGINE of Node-Red KNX-Ultimate node.

494 lines 16.3 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); const serialport_1 = require("serialport"); const TypedEmitter_1 = require("../TypedEmitter"); const KnxLog_1 = require("../KnxLog"); const DEFAULT_PATH = '/dev/ttyAMA0'; const DEFAULT_TIMEOUT_MS = 1200; const RESET_TIMEOUT_MS = 3000; const RESET_RETRIES = 3; const CLOSE_GRACE_MS = 2000; const ACK_BYTE = 0xe5; const COMM_MODE_FRAME = Buffer.from([ 0xf6, 0x00, 0x08, 0x01, 0x34, 0x10, 0x01, 0x00, ]); class SerialFT12 extends TypedEmitter_1.TypedEventEmitter { constructor(options) { super(); this.options = options; this.rxBuffer = Buffer.alloc(0); this.pendingAck = 0; this.sendToggle = false; this.isClosing = false; this.logger = (0, KnxLog_1.module)('FT12'); } static async listPorts() { const list = await serialport_1.SerialPort.list(); return list.map((item) => ({ path: item.path, manufacturer: item.manufacturer || undefined, serialNumber: item.serialNumber || undefined, vendorId: item.vendorId || undefined, productId: item.productId || undefined, type: item.pnpId || undefined, locationId: item.locationId || undefined, })); } async open() { if (this.port) return; await this.waitAfterCloseIfNeeded(); const handler = await this.createSerialPort(); this.port = handler; this.attachPort(handler); await this.initialize(); } async close() { this.isClosing = true; const port = this.port; if (!port) { this.isClosing = false; return; } try { await this.sendReset(); await new Promise((resolve) => setTimeout(resolve, 50)); } catch (err) { try { this.logger.warn(`FT1.2 close: reset before close failed: ${err.message}`); } catch { } } await new Promise((resolve) => { let settled = false; let timeout; const done = () => { if (settled) return; settled = true; if (timeout) clearTimeout(timeout); port.off('close', done); resolve(); }; timeout = setTimeout(() => { try { this.logger.warn('FT1.2 close timed out; forcing cleanup'); } catch { } done(); }, this.options.timeoutMs ?? DEFAULT_TIMEOUT_MS); timeout.unref?.(); port.once('close', done); try { port.close((err) => { if (err) { this.logger.error(`FT1.2 close error: ${err.message}`); } done(); }); } catch (error) { this.logger.error(`FT1.2 close exception: ${error.message}`); done(); } }); try { if (port.isOpen) { try { this.logger.warn('FT1.2 close: port still open after close request, retrying'); } catch { } try { port.close((err) => { if (err) { try { this.logger.error(`FT1.2 force close error: ${err.message}`); } catch { } } }); } catch (err) { try { this.logger.error(`FT1.2 force close exception: ${err.message}`); } catch { } } } } catch { } this.detachPortListeners(port); const pendingAck = this.awaitingAck; if (pendingAck) { clearTimeout(pendingAck.timer); pendingAck.reject(new Error('Serial FT1.2 port closed')); this.awaitingAck = undefined; } this.port = undefined; this.rxBuffer = Buffer.alloc(0); this.pendingAck = 0; this.lastCloseAt = Date.now(); this.isClosing = false; } async sendCemiPayload(payload) { if (!this.port) throw new Error('Serial FT1.2 port is not open'); const frame = this.buildLongFrame(payload); await this.writeFrameWithAck(frame); } async initialize() { const isKBERRY = this.options.isKBERRY ?? true; await this.sendReset(); if (isKBERRY) { try { this.logger.debug('FT1.2 → SetServerItem indication sending=on'); } catch { } await this.setIndicationSending(true); try { this.logger.debug('FT1.2 → M_PropWrite COMM_MODE LinkLayer (f6 00 08 01 34 10 01 00)'); } catch { } await this.sendCommMode(); try { this.logger.debug('FT1.2 → M_PropWrite AddressTable length=0 (f6 04 00 01 02 10 01 00 00)'); } catch { } try { await this.disableGroupFilter(); } catch (err) { this.logger.warn(`Unable to disable group-address filter: ${err.message}`); } } else { await this.sendCommMode(); } this.emit('ready'); } async sendReset() { let attempt = 0; let lastErr; while (attempt < RESET_RETRIES) { attempt += 1; try { this.logger.debug(`FT1.2 → RESET_REQ 10 40 40 16 (attempt ${attempt}/${RESET_RETRIES})`); } catch { } const ackPromise = this.waitForAck('reset', this.options.timeoutMs ?? RESET_TIMEOUT_MS); await this.writeRaw(Buffer.from([0x10, 0x40, 0x40, 0x16])); try { await ackPromise; return; } catch (err) { lastErr = err; try { this.logger.warn(`FT1.2 RESET ack timeout (attempt ${attempt}/${RESET_RETRIES}): ${lastErr.message}`); } catch { } if (attempt < RESET_RETRIES) { await new Promise((resolve) => setTimeout(resolve, 200)); } } } try { this.logger.warn(`FT1.2 RESET ack missing after ${RESET_RETRIES} attempts, continuing initialisation`); } catch { } } async sendCommMode() { await this.sendCemiPayload(COMM_MODE_FRAME); } async setIndicationSending(enable) { const itemId = 0x0010; const value = Buffer.from([enable ? 0x01 : 0x00]); await this.sendBaosServerItem(itemId, value); } async disableGroupFilter() { const cemi = Buffer.from([ 0xf6, 0x04, 0x00, 0x01, 0x02, 0x10, 0x01, 0x00, 0x00, ]); await this.sendCemiPayload(cemi); } async sendBaosServerItem(itemId, data) { const payload = Buffer.alloc(9 + data.length); let offset = 0; payload[offset++] = 0xf0; payload[offset++] = 0x02; payload.writeUInt16BE(itemId, offset); offset += 2; payload.writeUInt16BE(1, offset); offset += 2; payload.writeUInt16BE(itemId, offset); offset += 2; payload[offset++] = data.length; data.copy(payload, offset); await this.sendBaosPayload(payload); } async sendBaosPayload(payload) { if (!this.port) throw new Error('Serial FT1.2 port is not open'); try { this.logger.debug(`FT1.2 TX BAOS ${payload.toString('hex')}`); } catch { } const frame = this.buildLongFrame(payload); await this.writeFrameWithAck(frame); } async createSerialPort() { const serialOptions = { path: this.options.path || DEFAULT_PATH, baudRate: this.options.baudRate ?? 19200, dataBits: (this.options.dataBits ?? 8), stopBits: (this.options.stopBits ?? 1), parity: this.options.parity ?? 'even', rtscts: this.options.rtscts ?? false, lock: this.options.lock ?? true, autoOpen: false, }; return new Promise((resolve, reject) => { const port = new serialport_1.SerialPort(serialOptions); port.open((err) => { if (err) { reject(err); return; } const desiredDtr = this.options.dtr ?? true; port.set({ dtr: desiredDtr }, (setErr) => { if (setErr) { this.logger.warn(`Unable to set DTR: ${setErr.message}`); } resolve(port); }); }); }); } attachPort(port) { const onData = (chunk) => this.handleChunk(chunk); const onError = (error) => this.emit('error', error); const onClose = () => { this.port = undefined; setTimeout(() => { this.emit('close'); }, 2000).unref?.(); }; this.portListeners = { data: onData, error: onError, close: onClose, }; port.on('data', onData); port.on('error', onError); port.on('close', onClose); } detachPortListeners(port) { if (!this.portListeners) return; const { data, error, close } = this.portListeners; port.off('data', data); port.off('error', error); port.off('close', close); this.portListeners = undefined; } handleChunk(chunk) { try { this.logger.debug(`FT1.2 RX chunk ${chunk.toString('hex')}`); } catch { } this.rxBuffer = Buffer.concat([this.rxBuffer, chunk]); while (this.rxBuffer.length > 0) { const byte = this.rxBuffer[0]; if (byte === ACK_BYTE) { this.consumeBytes(1); this.resolveAck(); continue; } if (byte === 0x10) { if (this.rxBuffer.length < 4) break; const frame = this.rxBuffer.subarray(0, 4); this.consumeBytes(4); this.handleShortFrame(frame); continue; } if (byte === 0x68) { if (this.rxBuffer.length < 6) break; const len = this.rxBuffer[1]; const total = len + 6; if (this.rxBuffer.length < total) break; const frame = this.rxBuffer.subarray(0, total); this.consumeBytes(total); this.handleLongFrame(frame); continue; } if (byte === 0xa0) { if (this.rxBuffer.length < 2) break; const len = this.rxBuffer[1]; const total = len + 2; if (this.rxBuffer.length < total) break; this.consumeBytes(total); continue; } this.consumeBytes(1); } } consumeBytes(count) { this.rxBuffer = this.rxBuffer.subarray(count); } handleShortFrame(frame) { if (frame.length !== 4) return; this.sendAck(); } handleLongFrame(frame) { if (frame.length < 8) return; if (frame[0] !== 0x68 || frame[3] !== 0x68) return; const len = frame[1]; if (frame[2] !== len) return; if (frame[frame.length - 1] !== 0x16) return; const checksum = frame[frame.length - 2]; let calc = frame[4]; const payloadLen = len - 1; const payload = frame.subarray(5, 5 + payloadLen); for (const byte of payload) { calc = (calc + byte) & 0xff; } if (calc !== checksum) { this.logger.warn('Invalid FT1.2 checksum, dropping frame'); return; } this.sendAck(); if (payload.length === 0) return; if (payload[0] === 0xf0) { try { this.logger.debug(`FT1.2 RX BAOS ${payload.toString('hex')}`); } catch { } return; } try { this.logger.debug(`FT1.2 RX cEMI ${payload.toString('hex')}`); } catch { } this.emit('cemi', payload); } sendAck() { this.writeRaw(Buffer.from([ACK_BYTE])).catch(() => { }); } buildLongFrame(payload) { const len = payload.length + 1; const frame = Buffer.alloc(payload.length + 7); frame[0] = 0x68; frame[1] = len; frame[2] = len; frame[3] = 0x68; frame[4] = this.sendToggle ? 0x73 : 0x53; this.sendToggle = !this.sendToggle; payload.copy(frame, 5); let checksum = frame[4]; for (let i = 0; i < payload.length; i += 1) { checksum = (checksum + payload[i]) & 0xff; } frame[frame.length - 2] = checksum; frame[frame.length - 1] = 0x16; return frame; } async writeFrameWithAck(frame) { const ackPromise = this.waitForAck('frame'); try { this.logger.debug(`FT1.2 TX frame ${frame.toString('hex')}`); } catch { } await this.writeRaw(frame); await ackPromise; } writeRaw(buffer) { if (!this.port) { return Promise.reject(new Error('Serial port is closed')); } return new Promise((resolve, reject) => { this.port.write(buffer, (err) => { if (err) { reject(err); return; } this.port.drain((drainErr) => { if (drainErr) { reject(drainErr); return; } resolve(); }); }); }); } waitForAck(label, timeoutMs = this.options.timeoutMs ?? DEFAULT_TIMEOUT_MS) { if (this.awaitingAck) { try { this.logger.warn(`Previous FT1.2 ACK promise was still pending (${label}), dropping it`); } catch { } clearTimeout(this.awaitingAck.timer); this.awaitingAck = undefined; } if (this.pendingAck > 0) { this.pendingAck -= 1; return Promise.resolve(); } return new Promise((resolve, reject) => { const timer = setTimeout(() => { this.awaitingAck = undefined; reject(new Error(`Timeout waiting for FT1.2 ACK (${label})`)); }, timeoutMs); this.awaitingAck = { resolve: () => { clearTimeout(timer); this.awaitingAck = undefined; resolve(); }, reject: (err) => { clearTimeout(timer); this.awaitingAck = undefined; reject(err); }, timer, }; }); } resolveAck() { if (!this.awaitingAck) return; this.awaitingAck.resolve(); } async waitAfterCloseIfNeeded() { if (!this.lastCloseAt) return; const elapsed = Date.now() - this.lastCloseAt; const delay = CLOSE_GRACE_MS - elapsed; if (delay > 0) { await new Promise((resolve) => setTimeout(resolve, delay)); } } } exports.default = SerialFT12; //# sourceMappingURL=SerialFT12.js.map