UNPKG

node-red-contrib-minelert-universal-io

Version:

Node-RED nodes for Universal IO Gateway and I/O Modules

141 lines (116 loc) 3.98 kB
const { SerialPort } = require('serialport'); const { ReadlineParser } = require('@serialport/parser-readline'); const mqtt = require('mqtt'); const { EventBus, logInfo } = require('@minelert-smartlink/common'); // Shared port cache to reuse open ports across nodes const portCache = {}; function getSharedSerialPort(path, baudRate) { if (!portCache[path]) { const port = new SerialPort({ path, baudRate, autoOpen: false, }); const parser = port.pipe(new ReadlineParser({ delimiter: '\n' })); portCache[path] = { port, parser }; } return portCache[path]; } module.exports = function (RED) { function GatewayNode(config) { RED.nodes.createNode(this, config); const node = this; // Load config const portPath = config.serialPort || '/dev/ttyAMA1'; const baudRate = parseInt(config.baudRate) || 115200; const mqttHost = config.mqttHost || 'mqtt://localhost'; const mqttTopic = config.mqttTopic || 'application/+/device/+/event/up'; if (!portPath) { node.error("Serial port path is missing from config."); return; } node.log(`Opening serial port: ${portPath} @ ${baudRate}bps`); // Setup serial port and parser let parser; try { const shared = getSharedSerialPort(portPath, baudRate); node.port = shared.port; parser = shared.parser; } catch (err) { node.error(`Failed to setup serial port: ${err.message}`); return; } // Open port if not already open if (!node.port.isOpen && !node.port.opening) { node.port.open((err) => { if (err) { node.error(`Serial port open error: ${err.message}`); } else { node.log(`Serial port ${portPath} opened`); } }); } else { node.log(`Serial port ${portPath} already open`); } // Handle incoming serial data parser.on('data', (data) => { node.log(`Received serial: ${data}`); try { const message = JSON.parse(data); if (message.action === 'IO_STATE') { EventBus.emit('io-state-change', message); } } catch (err) { node.warn(`Failed to parse serial JSON: ${err.message}`); } }); // MQTT setup const client = mqtt.connect(mqttHost); // this.listeners = []; // this.registerListener = (cb) => this.listeners.push(cb); client.on('connect', () => { node.log('MQTT connected'); client.subscribe(mqttTopic, (err) => { if (err) node.error(`MQTT subscribe error: ${err.message}`); else node.log(`Subscribed to MQTT topic: ${mqttTopic}`); }); }); client.on('message', (topic, message) => { const payloadStr = message.toString(); try { const payload = JSON.parse(payloadStr); // node.log(JSON.stringify(payload)) EventBus.emit('chirpstack-event', { topic, payload }); // node.listeners.forEach(cb => cb({ topic, payload })); } catch (err) { node.error(`Invalid MQTT JSON: ${err.message}`); } }); client.on('error', (err) => { node.error(`MQTT error: ${err.message}`); }); // Send command over serial (from within Node-RED or EventBus) function sendCommand(command) { const frame = JSON.stringify(command) + '\n'; node.log(`Sending: ${frame}`); node.port.write(frame, (err) => { if (err) node.error(`Serial write error: ${err.message}`); }); } node.sendCommand = sendCommand; EventBus.on('send-command', sendCommand); // Cleanup on node shutdown node.on('close', (removed, done) => { node.log('Closing Gateway node...'); if (node.port && node.port.isOpen) { node.port.close(() => { delete portCache[portPath]; node.log(`Closed serial port: ${portPath}`); }); } if (client.connected) client.end(); done(); }); } RED.nodes.registerType('gateway', GatewayNode); };