UNPKG

node-red-node-arduino

Version:

A Node-RED Node, to talk to a chip as I/O extender, which is running Firmata firmware

813 lines (705 loc) 40.1 kB
/* ************************************************************ This is a "Firmata protocol" based communication between Node-RED and a connected board running firmata firmware. It is working with many boards, not just Arduino. https://github.com/node-red/node-red-nodes ************************************************************ !How is it loading / running: 1. First, the in/Out nodes will wait, until the "Main-board" = FirmataBoard is: - connecting - than getting the Version-info from board - after success -> those "child-Nodes" getting notified by this event: nodeIn.on(c_brdStateChanged, function() - the firmware-level board name is getting stored at: brdNode.FirmwareName See: Const BdStates ... and ... function startBoardLoopTimer() 1.2 If it fails, because of: - no board is set - no COM-port is set - could not connect to that COM-port. ... than it will: - write an error to the console-log - show error at NR UI sidebar. - Emit (send) a message (brotcast) to all [in+out] nodes to: show red dot under the node on UI 2. After that, state of In/Out nodes will change only, if: - node is Closing - Board is Closing or Disconnected - reset signal sent - new interval sent 3. TODO: periodically do a TEST to see, if the connection still lives? ! Warning: board.Ready gets true, once Firmata: version+capabilities finished reading. Firmata's: board.Pins[] array record structure: { supportedModes: [ 0, 1, 3, 11, 16, [length]: 5 ], mode: 1, value: 0, report: 1, analogChannel: 127 } settings: { reportVersionTimeout: 5000, samplingInterval: 19, >> changed to 250ms serialport: { baudRate: 57600, highWaterMark: 256, path: 'COM3' } } transport.settings: { autoOpen: true, endOnClose: false, baudRate: 57600, // TODO: test, if higher rate would also connect? path: 'COM3' }, History: (v1.1+) https://github.com/node-red/node-red-nodes/issues/920#issuecomment-2709252741 TODO: - unplug-replug should restart everything - add samples Todo: (later) - password Hash protection (periodic?) - flushDigitalPorts() = setting array of ports at once, not one-by-one - 1x queryPinState() vs. digitalRead() ... at startup? Need to investigate - digitalRead() adds a Listener - Firmware-Name: show board name on Node-RED panel, which was set at firmata firmware at startup: get-name function? - update Baudrate during run via: transport.update() ... see: new module name: red-firmata - put / get functions - type identify by number, not string "ANALOG" RED.events.on('runtime-state', handleRuntimeState) in the onpalleteadd function. After the first time it detects obj.state === 'start' Other ideas (later): - flushDigitalPorts() = setting array of ports at once, not one-by-one - queryPinState() vs. digitalRead() ... at startup? Need to investigate - Firmware-Name: show board name on Node-RED panel, which was set at firmata firmware at startup: void setup() { Firmata.setFirmwareNameAndVersion("my-unique-ArduinoFirmata-name-42", FIRMATA_PROTOCOL_MAJOR_VERSION, FIRMATA_FIRMWARE_MINOR_VERSION)`; - FindBy-Firmware-Name: use this name for auto-search this unique board at startup, no matter which serial-port is it plugged Memo for debugging: if VSC not allowing to run: > node-red (from JavaScript terminal), copy this first: Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope CurrentUser */ module.exports = function(RED) { "use strict"; const { SerialPort } = require('serialport'); // changed format SP -> to {SP} at v9 => v10. See: https://serialport.io/docs/guide-upgrade const firmataBoard = require('./lib/firmata'); let moreLogs = RED.settings.verbose || false; // enable this for "debugging" // "global" constants: (these are used at Emitters too) const c_fbr = " -- FirmataBoard:"; // this is used to print consol-log messages equally. const c_ANALOG = "ANALOG"; const c_INPUT = "INPUT" ; const c_PULLUP = "PULLUP"; // also input, with resistor activated const c_OUTPUT = "OUTPUT"; const c_PWM = "PWM" ; const c_SERVO = "SERVO" ; const c_SYSEX = "SYSEX" ; const c_RESET = "RESET" ; const c_INTER = "INTERVAL"; const c_STRING = "STRING"; // used in both in+out node const c_invalidPin = "Invalid pin: "; const c_brdStateChanged = "BrdStatCh"; // used by board-emitter + children's .on() Combined with BoardNode's const c_brdReady = "brdReady" ; // same as ^c_brdStateCh.. // state types of the Firmata Board: const BdStates = Object.freeze({ // (freeze = immutable record) permanentError: -9, portNotSet : -4, // no COM-port specified unknownError : -3, disconnected : -2, tryReconnect : -1, start : 0, connecting : 1, gettingVersion: 2, OK : 3 // Ready }); const c_ring = "ring"; const c_dot = "dot" ; const c_red = "red" ; const c_green = "green" ; const c_yellow = "yellow"; const c_blue = "blue" ; const c_grey = "grey" ; // global error handling for uncaught errors: process.on('uncaughtException', (err, origin) => { const e = "!!! Unhandled error in: [35-arduino.js] node-red module. >> " + err + " >> ORIGIN: [%o]"; try { this.error( e, origin ); } catch (rrr) { console.error( e, origin ); } }); // RED.events.on("deploy", function() { // sadly this is not getting triggered on DEPLOY button click :-( // console.log(c_fbr + "MAIN - deploy happened!"); // manual says: "A new flow has been deployed" https://nodered.org/docs/api/ui/events/ // }); function resetBoard(_brdNode){ if ((_brdNode == null) || (_brdNode.board == null)) return; // console.log('pins: %o', nodeIn.board.analogPins); if (moreLogs) console.info(c_fbr + " Board RESET. Port= " + _brdNode.port ); // =parent.device try { _brdNode.board.reset(); if (moreLogs) console.info(c_fbr + " .. Reset sent. Port= " + _brdNode.port ); } catch(err) { _brdNode.error(c_fbr + "sending: reset failed. Error: " + err); } }; // state types of Input/Output nodes: const ndStats = Object.freeze({ // (freeze = immutable record) noBoard : -3, // no firmata Board specified wrongPin : -2, // Error pinConflict : -1, // Error: multiple nodes are set with same pin start : 0, equalsBoard : 1, // the node will reflect it's parent-board's status. Like: "no COM port set" OK : 3 }); function updateNodeStatus(_n, new_stat) { if (_n.parentNode == null) new_stat = ndStats.noBoard; if (((_n.parentNode != null) && (_n.parentNode.b_stat !== BdStates.OK)) || (new_stat === ndStats.equalsBoard) ) // if the main board has some error, it has priority, Except: [noBoard, missingPin pinConflict] { _n.n_status = ndStats.equalsBoard; switch (_n.parentNode.b_stat) { case BdStates.permanentError: _n.status({fill:c_red , shape:c_ring, text:"Permanent Error: Board"}); break; // -9 case BdStates.portNotSet : _n.status({fill:c_red , shape:c_ring, text:"Error: Port not set"}); break; // -4 case BdStates.unknownError : _n.status({fill:c_red , shape:c_ring, text:"Unknown Error: Board"}); break; // -3 case BdStates.disconnected : _n.status({fill:c_red , shape:c_ring, text:"Disconnected"}); break; case BdStates.tryReconnect : _n.status({fill:c_red , shape:c_dot , text:"Reconnecting.."}); break; case BdStates.gettingVersion: _n.status({fill:c_blue , shape:c_dot , text:"Getting name+version"}); break; case BdStates.connecting : _n.status({fill:c_grey , shape:c_dot , text:"node-red:common.status.connecting"}); break; default : _n.status({}); break; } } else { if (_n.n_status === new_stat) return; _n.n_status = new_stat; switch (new_stat) { case ndStats.noBoard : _n.status({fill:c_red , shape:c_ring, text:"Error: No board"}); break; // -3 case ndStats.wrongPin : _n.status({fill:c_red , shape:c_ring, text:"Error: Pin not set"}); break; // -2 case ndStats.pinConflict : _n.status({fill:c_red , shape:c_ring, text:"Error: Pin conflict"}); break; // -1 case ndStats.start : _n.status({fill:c_grey , shape:c_ring, text:"...start"}); break; // 0 // case ndStats.equalsBoard : = 1 case ndStats.OK : _n.status({fill:c_green , shape:c_ring, text:"OK"}); break; // +3 default : _n.status({}); break; } } } function pinAlreadyUsed (_parentNode, _newNode) { // Check, if there is already a Node registered with same pin -> report pin-conflict if (_parentNode.myChirdren.length === 0) return false; if (([c_RESET, c_STRING, c_SYSEX, c_INTER]).includes(_newNode.pinType)) return false; let _pin = _newNode.pin; if (_newNode.pinType === c_ANALOG) { if (_newNode.pin >= _parentNode.board.analogPins.length) return false; // wrong pin number _pin = _parentNode.board.analogPins[_newNode.pin]; // get from the reference Like: [26,27,28,29] _pin=3 -> _pin=29 } for (let i = 0; i < _parentNode.myChirdren.length; i++) { const _ch = _parentNode.myChirdren[i]; if (_ch == null ) continue; if (_ch === _newNode) continue; // itself function reportErrorAndExit() { updateNodeStatus(_newNode, ndStats.pinConflict); _newNode.error("This pin number is already in use by this Node:" + _newNode.id); return true; } if ( _ch.pinType === c_ANALOG) { if (_pin === _parentNode.board.analogPins[_ch.pin]) return reportErrorAndExit(); // found similar! Exit } else { if (_pin === _ch.pin) return reportErrorAndExit(); // found similar! Exit } } return false; // not found any pin-conflict } function removeFromChildren(_ch) { if (_ch != null) { let _x =_ch.parentNode.myChirdren.indexOf(_ch); if (_x >= 0) _ch.parentNode.myChirdren.splice(_x, 1); }; }; /** * The Board Definition - this opens (and closes) the connection. "n" contains the setup. */ function ArduinoNode(_setup) { RED.nodes.createNode(this, _setup); let brdNode = this; brdNode.b_stat = BdStates.start; // the status of the board brdNode.name = _setup.name || ""; brdNode.samplingInt= _setup.samplingInt || 250; if ((_setup.log2consol || moreLogs) === true) moreLogs = true; brdNode.port = _setup.device || ""; // port path. Like: "COM3" or "/dev/serial/by-id/usb-Arduino_RaspberryPi_Pico_076461E62D414FE3-if00" // see also: "n.settings.serialport.path" . Sadly the original HTML is named it "device" :-( brdNode.FirmwareName = ""; brdNode.board = null; // the actual Firmata-Board brdNode.emitBaseStr = "" + brdNode.id + "-"; // for generating unique string to emit + subcribe to it. brdNode.myChirdren = []; brdNode.loop = null; // timer brdNode.loopWaitMs = 0; // How many millisecond to wait until re-trying. This will grow in time dynimically, up to: 10sec brdNode.closing = false; //TODO: used to distinguish between normal close (true) or unwanted disconnection (cable / wifi) const startOptions = { samplingInterval: brdNode.samplingInt, serialport: { baudRate: 57600, // TODO: test, if it would be possible to increase speed during run via fimata.Update() path : brdNode.port } }; function updateBrdState(_newState) { if (brdNode.b_stat === _newState) return; // nothing changed -> exit brdNode.b_stat = _newState; // Send event to all nodeIn + nodeOut to update visual state for (let x = 0; x < brdNode.myChirdren.length; x++) { const _ch = brdNode.myChirdren[x]; if (_ch != null) _ch.emit(c_brdStateChanged, true); } } function startBoardLoopTimer() { if (brdNode.loop != null) return; // timer already set! if (brdNode.loopWaitMs < 10000) brdNode.loopWaitMs += (brdNode.loopWaitMs < 1500) ? 100 : 1000; brdNode.loop = setTimeout(function() { update_reconnect_MyBoard(); }, brdNode.loopWaitMs); } function update_reconnect_MyBoard() { clearTimeout(brdNode.loop); brdNode.loop = null; if (brdNode.board == null) return; // ... for any case if (brdNode.board.transport == null) return; // ... for any case const _t = brdNode.board.transport; // shorter form // if everything is fine. Startup already happened once, (and no disconnection since than,) so no need to run more times. if ((brdNode.b_stat === BdStates.OK) && (brdNode.board != null) && (brdNode.board.isReady) && (_t.isOpen)) { if (moreLogs) console.info(c_fbr + " is already running fine. No need to start again. Port:" + brdNode.port ); // TODO: send a test: queryPinState(pin, callback) brdNode.b_stat = BdStates.OK; brdNode.loopWaitMs = 0; // reset return; } let _newState = BdStates.connecting; // first time start if (brdNode.b_stat < 0) _newState = BdStates.tryReconnect; else if (!_t.opening && _t.isOpen && !brdNode.board.isReady) _newState = BdStates.gettingVersion; // first time start updateBrdState( _newState ); if (!_t.opening && !_t.isOpen) // if not opening and not opened _t.open(); startBoardLoopTimer(); // increase time and check again } let startupBrd = function() { if ( brdNode.port === "") { // no port is set ! updateBrdState(BdStates.portNotSet); return; } if (brdNode.board === null) {// if board does not exists yet ... brdNode.b_stat = BdStates.connecting; // first time start if (moreLogs) brdNode.log(c_fbr + "creating new instance. Port:" + brdNode.port + " | Sampling-interval=" + brdNode.samplingInt); // creating new board, starting async promice to report state changes: brdNode.board = new firmataBoard(brdNode.port, startOptions, function(e) { // (port, options, callback) if (e == null) { update_reconnect_MyBoard(); return; }; const _s = e.toString(); if (moreLogs) brdNode.log(c_fbr + brdNode.name + " Port:" + brdNode.port + " state changed to:" + _s + " | opening:" + brdNode.board.transport.opening + " | is open:" + brdNode.board.transport.isOpen); // *** if some kind of error happened *** // if ( (e.name === "Error") || (_s.indexOf("cannot open") !== -1) || (_s.indexOf("Error") !== -1) ) { updateBrdState(BdStates.permanentError); brdNode.error(RED._("arduino.errors.portnotfound", {device:brdNode.port})); brdNode.loopWaitMs = 20000; startBoardLoopTimer(); return; }; update_reconnect_MyBoard(); // this will update state, restart check-timer if needed. //if (brdNode.board.versionReceived === true) updateBrdState(BdStates.OK) //else updateBrdState(BdStates.connecting); }); } brdNode.board.transport.open(); brdNode.on('destroy', function() { brdNode.closing = true; brdNode.myChirdren = []; if (brdNode.board == null) return; // exit brdNode.board.removeAllListeners('connect'); brdNode.board.removeAllListeners('ready'); brdNode.board.removeAllListeners('close'); brdNode.board.removeAllListeners('disconnect'); }); // Firmata-board emitters: brdNode.board.on('error', function(err) { updateBrdState(BdStates.unknownError); if (moreLogs) brdNode.error(c_fbr + ' Error: ' + JSON.stringify(err) ); //+ }); // "connect" is called, once serial communication is established. After that queryFirmware is called. See: "ready" brdNode.board.on('connect', function() { brdNode.closing = false; if (moreLogs) brdNode.log(c_fbr + "connecting to:" + brdNode.port ); if (brdNode.FirmwareName) // version already aquired once updateBrdState(BdStates.OK); else { if (brdNode.board.versionReceived) brdNode.warn("versionReceived but FirmwareName=[]"); updateBrdState(BdStates.gettingVersion); startBoardLoopTimer(); }; }); // "ready" event is called, after the Firmware name+version + capabilities got querried within 5000ms successfully brdNode.board.on('ready', function() { brdNode.closing = false; brdNode.FirmwareName = brdNode.board.firmware.name; updateBrdState(BdStates.OK); brdNode.log(RED._("arduino.status.connected",{device:brdNode.port}) + " Firmware name:[" + brdNode.FirmwareName + "] " + RED._("arduino.status.version",{version: brdNode.board.version.major +"."+ brdNode.board.version.minor})); // notifying all children for (let x = 0; x < brdNode.myChirdren.length; x++) { const _ch = brdNode.myChirdren[x]; if (_ch != null) _ch.emit(c_brdReady, true); } }); brdNode.board.on('close', function(removed, done) { // Firmata (transport) closing // todo : removed updateBrdState(BdStates.disconnected); resetBoard(brdNode); // this will try to send a "last minute" signal to the board to: reset. if ( ! brdNode.closing) brdNode.error(RED._("arduino.status.portclosed")); if (done !== undefined) done(); }); brdNode.board.on('disconnect', function() { updateBrdState(BdStates.disconnected); if (moreLogs) brdNode.log(c_fbr + "Disconnected. Port:" + brdNode.port + " Firmware Name: ["+ brdNode.FirmwareName +"]"); if ( ! brdNode.closing) startBoardLoopTimer(); // do not start, if proper closing is happening }); }; //debugger // START board initialization the first time startupBrd(); if (brdNode.loop == null) startBoardLoopTimer(); else if (moreLogs) brdNode.log(c_fbr + "already present. Loop start skipped. "); // brdNode.removeAllListeners('close'); brdNode.on('close', function(removed, done) { // the Node itself is getting destoyed memo: function(done) did not work, TypeError brdNode.closing = true; if (!removed) updateBrdState(BdStates.disconnected); // this will notify clients too clearTimeout(brdNode.loop); brdNode.loop = null; if ((brdNode.board == null) || (brdNode.board.transport == null) ) { if (done !== undefined) done(); return; } if (brdNode.board.transport.closing) { if (moreLogs) { brdNode.log(c_fbr + "Nothing to do, because this port is already closing: " + brdNode.port); } if (done !== undefined) done(); return;// EXIT }; if (moreLogs) { brdNode.log(c_fbr + "Trying to close port:" + brdNode.port); } if (brdNode.board.transport.isOpen) { //resetBoard(brdNode); try { brdNode.board.transport.close(function(err) { if (moreLogs) { brdNode.log(RED._("arduino.status.portclosed") + err?"Err: ":"" , err); } }); if (done !== undefined) done(); } catch(e) { if (moreLogs) { brdNode.error("Could not close port: " + brdNode.port + (e?"Err: ":"") , e); } } } else { if (done !== undefined) done(); return;} }); } // *** REGISTERING the (parent) board node *** // // ******************************************** // RED.nodes.registerType("arduino-board", ArduinoNode); /** * The Input (child) Node * * pinType: can be ANALOG, DIGITAL ... (Warning! in html it is called "state") */ function DuinoNodeIn(_setupIn) { RED.nodes.createNode(this, _setupIn); // n = setupNode let nodeIn = this; nodeIn.n_status = ndStats.start; nodeIn.pin = _setupIn.pin; nodeIn.pinType = _setupIn.state; // "state" is a wrong naming at the html definition. It is the current "Type" of the Pin. (Like: "ANALOG" or "PWM"...) nodeIn.parentNode = RED.nodes.getNode(_setupIn.arduino); // n.arduino = The ArduinoNode's ID. It is defined at `package.json`: "node-red": {... "nodes": { "arduino": "35-arduino.js" } let loopIn = null; // timer // The creation process if (typeof nodeIn.parentNode === "object") { nodeIn.frmBoard = nodeIn.parentNode.board; // a shorter reference the parent-node's Firmata-Board class if (nodeIn.frmBoard == null) { updateNodeStatus(nodeIn, ndStats.noBoard); return; // EXIT }; nodeIn.parentNode.myChirdren.push(nodeIn); // subscribe to main nodes array to recieve state changes // handle if parent Node's (= firmata-Board's) status is changed nodeIn.on(c_brdStateChanged, function() { updateNodeStatus(nodeIn, nodeIn.n_status); }); // *** first initialization *** // let startupIn = function() { if (moreLogs) console.info(c_fbr + "Node-In created." + (nodeIn.name ? " Name=["+ nodeIn.name +"]" : "") + " Pin=" + nodeIn.pin + " Type=" + nodeIn.pinType); if (loopIn !== null) {clearTimeout(loopIn); loopIn = null}; //nodeIn.frmBoard.setMaxListeners(0); Deleted 2025-03-17. DO NOT USE THIS ! See: https://stackoverflow.com/a/44143119 // nodeIn.frmBoard.setMaxListeners(11); // no need either nodeIn.oldval = ""; updateNodeStatus(nodeIn, ndStats.equalsBoard); let doit = function() { if (pinAlreadyUsed(nodeIn.parentNode, nodeIn) === true) return; // EXIT; // pin-conflict check if (moreLogs) console.info(c_fbr + "Node-In init started." + (nodeIn.name ? " Name=["+ nodeIn.name +"]" : "") + " Pin=" + nodeIn.pin + " Type=" + nodeIn.pinType); let goodPin = (nodeIn.pin != null) && !isNaN(nodeIn.pin) && (nodeIn.pin >=0) && (nodeIn.pin < nodeIn.frmBoard.pins.length); if (goodPin === true && nodeIn.pinType === c_ANALOG) { goodPin = ( nodeIn.pin in nodeIn.frmBoard.analogPins ); // found analogue pin if (!goodPin){ nodeIn.error( c_invalidPin + nodeIn.pin + (nodeIn.frmBoard.analogPins ? ". Only these analogue pin numbers are allowed: [0.." + (nodeIn.frmBoard.analogPins.length-1) + "]/n Reference GPIOs:" + nodeIn.frmBoard.analogPins : "NO analogue pins are allowed with this firmware / board!")); //console.log('pins: %o', nodeIn.board.analogPins); // TESTS //const jsonString = JSON.stringify(node.board.pins); //console.log( jsonString ); //node.error( jsonString ); // test } } if (goodPin === true) { try { if (nodeIn.pinType === c_ANALOG) { nodeIn.frmBoard.pinMode(nodeIn.pin, 0x02); } else if (nodeIn.pinType === c_INPUT ) { nodeIn.frmBoard.pinMode(nodeIn.pin, 0x00); } else if (nodeIn.pinType === c_PULLUP) { nodeIn.frmBoard.pinMode(nodeIn.pin, 0x0B); } updateNodeStatus(nodeIn, ndStats.OK); } catch (setPinError) { updateNodeStatus(nodeIn, ndStats.wrongPin); nodeIn.error(c_fbr + "Input Pin ["+ nodeIn.pin +"] setting error:" + setPinError); return; } // subscribing to pin-event listeners. These will call at firmata-io.js: board.addListener(`analog-read-${pin}`, callback); if (nodeIn.pinType === c_ANALOG) { nodeIn.frmBoard.analogRead(nodeIn.pin, function(v) { if (nodeIn.n_status !== ndStats.OK) updateNodeStatus(nodeIn, ndStats.OK); if (v !== nodeIn.oldval) { nodeIn.oldval = v; nodeIn.send({payload:v, topic:"A"+nodeIn.pin}); } }); } else if (nodeIn.pinType === c_INPUT) { nodeIn.frmBoard.digitalRead(nodeIn.pin, function(v) { if (nodeIn.n_status !== ndStats.OK) updateNodeStatus(nodeIn, ndStats.OK); if (v !== nodeIn.oldval) { nodeIn.oldval = v; nodeIn.send({payload:v, topic:nodeIn.pin}); } }); // nodeIn.frmBoard.queryPinState(nodeIn.pin, callback); // TODO ? } else if (nodeIn.pinType === c_PULLUP) { nodeIn.frmBoard.digitalRead(nodeIn.pin, function(v) { if (nodeIn.n_status !== ndStats.OK) updateNodeStatus(nodeIn, ndStats.OK); if (v !== nodeIn.oldval) { nodeIn.oldval = v; nodeIn.send({payload:v, topic:nodeIn.pin}); } }); // nodeIn.frmBoard.queryPinState(nodeIn.pin); // TODO ? } else if (nodeIn.pinType == c_STRING) { nodeIn.frmBoard.on('string', function(v) { if (nodeIn.n_status !== ndStats.OK) updateNodeStatus(nodeIn, ndStats.OK); // if (v !== nodeIn.oldval) { //OMG! deleted 2025-03-17 // nodeIn.oldval = v; nodeIn.send({payload:v, topic:"string"}); // } }); }; } else { updateNodeStatus(nodeIn, ndStats.wrongPin); nodeIn.error(c_fbr + " Input Node " + c_invalidPin + nodeIn.pin); } } // wait first, until Board's capabilities are reported. Configure child node after that. if (nodeIn.frmBoard.isReady) { doit(); } else { nodeIn.once(c_brdReady, function() { doit(); }); } /* if (loopIn === null) { if (moreLogs) console.info(c_fbr + "Timeout set for loop. Name="+ nodeIn.name + " Pin=" + nodeIn.pin + " State=" + nodeIn.pinType); loopIn = setTimeout(function() { if (nodeIn.running === false) { startupIn(); } }, 4500); loopIn.name = "startupIN-timeout"; } */ } startupIn(); } else { updateNodeStatus(nodeIn, ndStats.noBoard); nodeIn.warn(c_fbr + "In: " + RED._("arduino.errors.portnotconf")); } nodeIn.on('close', function(removed, done) { // if remove === true -> it means this Node is getting deleted clearTimeout(loopIn); if (removed) {removeFromChildren(nodeIn);} else {updateNodeStatus(nodeIn, ndStats.equalsBoard);} if (done !== undefined) done(); }); } RED.nodes.registerType("arduino in", DuinoNodeIn); /** * The Output (child) Node * * pinType: can be ANALOG, DIGITAL ... (Warning! in html "pinType" is called "state") */ function DuinoNodeOut(_setupOut) { RED.nodes.createNode(this, _setupOut); let nodeOut = this; nodeOut.n_status = ndStats.start; nodeOut.pin = _setupOut.pin; nodeOut.pinType = _setupOut.state; // "state" is a wrong naming at the html definition. It is the current type of the Pin. (Like: "ANALOG" or "PWM"...) nodeOut.parentNode = RED.nodes.getNode(_setupOut.arduino); // _setupOut.arduino = The board config. It is defined at `package.json`: "node-red": {... "nodes": { "arduino": "35-arduino.js" } let loopOut = null; // timer // create an event-listener outside the scope of normal inputs, for "override-reset" if (nodeOut.pinType === c_RESET) { nodeOut.on("input", function(msg, send, done) { if ( Boolean(msg.payload) === true) { try { resetBoard(nodeOut.parentNode); if (moreLogs) nodeOut.warn(c_fbr + "... sending reset from NR"); } catch (_error) { nodeOut.error(c_fbr + "Could not send RESET to the board.", _error); }; if (done !== undefined) done(); } }); } // Test to see, what's inside: //console.log("ou- _setupOut.arduino: %o", _setupOut.arduino); //console.log("/n ====================================================================================== /n", n.arduino); //console.log("ou- serverConfig: %o", nodeOut.boardConfig.board); //console.log("/n ====================================================================================== /n", n.arduino); //console.log("ou- type of serverConfig: " + typeof nodeOut.boardConfig); // The creation process if (typeof nodeOut.parentNode === "object") { nodeOut.frmBoard = nodeOut.parentNode.board; // a shorter reference directly to Firmata class if (nodeOut.frmBoard == null) { updateNodeStatus(nodeOut, ndStats.noBoard); return; // EXIT }; nodeOut.parentNode.myChirdren.push(nodeOut); // subscribe to main nodes array to recieve state changes // if parent Node's (= firmata-Board's) status is changed nodeOut.on(c_brdStateChanged, function() { updateNodeStatus(nodeOut, nodeOut.n_status); }); let startupOut = function() { if (moreLogs) console.info(c_fbr + "Node-Out created." + (nodeOut.name ? " Name=["+ nodeOut.name + "]" : "") + " Pin=" + nodeOut.pin + " Type=" + nodeOut.pinType); if (loopOut !== null) {clearTimeout(loopOut); loopOut = null}; updateNodeStatus(nodeOut, ndStats.equalsBoard); let doit = function() { if (pinAlreadyUsed(nodeOut.parentNode, nodeOut) === true) return; // EXIT; // pin-conflict check if (moreLogs) console.info(c_fbr + "Node-Out init started." + (nodeOut.name ? " Name=["+ nodeOut.name + "]" : "") + " Pin=" + nodeOut.pin + " Type=" + nodeOut.pinType); if ((nodeOut.pin != null) && !isNaN(nodeOut.pin) && nodeOut.pin >=0 && nodeOut.pin < nodeOut.frmBoard.pins.length) { if (nodeOut.pinType === c_OUTPUT) { nodeOut.frmBoard.pinMode(nodeOut.pin, 0x01); } if (nodeOut.pinType === c_PWM ) { nodeOut.frmBoard.pinMode(nodeOut.pin, 0x03); } if (nodeOut.pinType === c_SERVO ) { nodeOut.frmBoard.pinMode(nodeOut.pin, 0x04); } updateNodeStatus(nodeOut, ndStats.OK); nodeOut.on("input", function(msg, send, done) { if ((msg == null) || (msg.payload == null)) { // NULL input -> send warning & exit nodeOut.warn("msg.payload must not be null!"); if (done !== undefined) done(); return; } if (nodeOut.frmBoard.isReady) { if (msg.payload == "reset") { msg.payload = ""; updateNodeStatus(nodeOut, ndStats.equalsBoard); resetBoard(nodeOut.parentNode); } else if (nodeOut.pinType === c_OUTPUT) { const str = msg.payload.toString(); if ((msg.payload === true ) || (str === "1") || (str.toLowerCase() === "on")) { nodeOut.frmBoard.digitalWrite(nodeOut.pin, nodeOut.frmBoard.HIGH); } else if ((msg.payload === false) || (str === "0") || (str.toLowerCase() === "off")) { nodeOut.frmBoard.digitalWrite(nodeOut.pin, nodeOut.frmBoard.LOW); }; } else if (nodeOut.pinType === c_PWM) { msg.payload = parseInt((msg.payload * 1) + 0.5); // round to int if ((msg.payload >= 0) && (msg.payload <= 255)) { nodeOut.frmBoard.analogWrite(nodeOut.pin, msg.payload); } else nodeOut.warn("PWM value must be: 0..255"); } else if (nodeOut.pinType === c_SERVO) { msg.payload = parseInt((msg.payload * 1) + 0.5); if ((msg.payload >= 0) && (msg.payload <= 180)) { nodeOut.frmBoard.servoWrite(nodeOut.pin, msg.payload); } else nodeOut.warn("PWM value must be: 0..180"); } else if (nodeOut.pinType === c_SYSEX) { nodeOut.frmBoard.sysexCommand(msg.payload); } else if (nodeOut.pinType === c_STRING) { nodeOut.frmBoard.sendString(msg.payload.toString()); } else /* if (nodeOut.pinType === c_RESET) { if ( Boolean(msg.payload) === true) { updateNodeStatus(nodeOut, ndStats.equalsBoard); resetBoard(nodeOut.parentNode); } } else */ if (nodeOut.pinType === c_INTER) { const i = 0 + msg.payload; if (i < 10 || i > 65535) { nodeOut.warn("Invalid new interval input value (10-65535): ["+ msg.payload +"]"); return; }; nodeOut.samplingInterval = i; nodeOut.frmBoard.setSamplingInterval(i); nodeOut.status({fill:c_yellow, shape:c_ring, text:"Interval= " + i}); } } if (done !== undefined) done(); }); } else { updateNodeStatus(nodeOut, ndStats.wrongPin); nodeIn.error(c_fbr + " Output Node " + c_invalidPin + nodeOut.pin); } } if (nodeOut.frmBoard.isReady) { doit(); } else { nodeOut.once(c_brdReady, function() { doit(); }); } /* if (loopOut === null) { if (moreLogs) console.info(c_fbr + "Timeout set for loop. Name="+ nodeOut.name +" Pin=" + nodeOut.pin + " Type=" + nodeOut.type); loopOut = setTimeout(function() { if (nodeOut.running === false) { startupOut(); } }, 4500); loopOut.name = "startupOut-timeout"; } */ } startupOut(); } else { updateNodeStatus(nodeOut, ndStats.noBoard); nodeOut.warn(c_fbr + "Out: " + RED._("arduino.errors.portnotconf")); } nodeOut.on('close', function(removed, done) { // if remove === true -> it means this Node is getting deleted clearTimeout(loopOut); if (removed === true) {removeFromChildren(nodeOut);} else {updateNodeStatus(nodeOut, ndStats.equalsBoard);}; if (done !== undefined) done(); }); } RED.nodes.registerType("arduino out", DuinoNodeOut); /** * Listing serial ports (used at Board-configuration, by clicking the SEARCH button on the right side of port editbox) * */ RED.httpAdmin.get("/arduinoports", RED.auth.needsPermission("arduino.read"), function(req, res) { let _arr = ["-- no ports found --"]; async function listSerialPorts() { try { const ports = await SerialPort.list(); if ((ports != null) && (Array.isArray(ports)) && ports.length !== 0) _arr = ports.map(p => p.path); res.json(_arr); } catch (err) { this.log('Error listing ports: '+ err); _arr.push( err.toString ); res.json(_arr); } } listSerialPorts(); }); }