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
JavaScript
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);
};