UNPKG

@pizzaprogram/mcp-pcf-aio

Version:

Node-Red node for multiple MCP23017 chips (each 16 input or output) + PCF8574(A) (8 I/O). The I2C Bus number can be specified too.

872 lines (732 loc) 46.6 kB
'Strict mode' // const { truncate } = require("fs"); const { clearInterval } = require("timers"); debugger // const { normalize } = require("path"); // const { abort } = require("process"); // const { isNumberObject } = require("util/types"); // Read more: // CHIP SECTION - For inputs this reads the chip and fires the input module // https://ww1.microchip.com/downloads/en/devicedoc/20001952c.pdf // NodeRED forum about enhancing this component: // https://discourse.nodered.org/t/node-red-contrib-mcp23017chip/37999 // For lisencing >> see Lisence file in the upper directory. // * 1 = Pin is configured as an input. // * 0 = Pin is configured as an output. // * See "3.5.1 I/O Direction register". // *** chip initialisation (IOCON values) *** // // ... enables "byte mode" (IOCON.BANK = 0 and IOCON.SEQOP = 0). //bit7 BANK = 0 : sequential register addresses (See PDF: Table 3-3) //bit6 MIRROR = 0 : use configure Interrupt //bit5 SEQOP = 0 : sequential operation disabled, address pointer does not increment //bit4 DISSLW = 0 : The "Slew Rate" bit controls the slew rate function on the SDA pin. If enabled, the SDA slew rate will be controlled when driving from a high to low. //bit3 HAEN = 0 : hardware address pin is always enabled on 23017 //bit2 ODR = 0 : open drain output. Enables/disables the INT pin for open-drain configuration. Setting this bit overrides the INTPOL bit. //bit1 INTPOL = 0 : interrupt active low (Sets the polarity of the INT pin. This bit is functional only when the ODR bit is cleared, configuring the INT pin as active push-pull) //bit0 xxx unused // for example SEQOP = 1: write ( addr, IOCON, 0b00100000 ); = 32 dec = 0x20 module.exports = function(RED) { const busStateTexts = [ "Opening i2c Bus", // 0 ... actually this is done only virtually, the opening happens only at first read/write "Reading current state", // 1 "Writing byte", // 2 "Closing i2c bus"]; // 3 const log2consol = true; // enabling it shows detailed logs in: node-red-log const timerLog = false; // !! WARNING !! << if true, it will fill up the log with ALL read events (up to 50x3 msg. pro sec !! if read interval is 20ms) // IOCON.BANK = 0 mode << !!! Using this is NOT recommended, because it causing "sequential read" of A/B bank, not a fixed side const BNK0_IODIR_A = 0x00; //< Controls the direction of the data Input/Output for port A. const BNK0_IODIR_B = 0x01; //< Controls the direction of the data Input/Output for port B. const BNK0_IPOL_A = 0x02; //< Configures the polarity on the corresponding GPIO_ port bits for input port A. const BNK0_IPOL_B = 0x03; //< Configures the polarity on the corresponding GPIO_ port bits for input port B. const BNK0_GPINTEN_A = 0x04; //< Controls the input interrupt-on-change for each pin of port A. const BNK0_GPINTEN_B = 0x05; //< Controls the input interrupt-on-change for each pin of port B. const BNK0_DEFVAL_A = 0x06; //< Controls the default comparison value for interrupt-on-change for port A. const BNK0_DEFVAL_B = 0x07; //< Controls the default comparison value for interrupt-on-change for port B. const BNK0_INTCON_A = 0x08; //< Controls how the associated pin value is compared for the interrupt-on-change for port A. const BNK0_INTCON_B = 0x09; //< Controls how the associated pin value is compared for the interrupt-on-change for port B. const BNK0_IOCON_A = 0x0A; //< Controls the device. (0, INTPOL, ODR, HAEN, DISSLW, SEQOP, MIRROR, BANK) = 0x0B too const BNK0_IOCON_B = 0x0B; //< Controls the device. (0, INTPOL, ODR, HAEN, DISSLW, SEQOP, MIRROR, BANK) = 0x0B too const BNK0_GPPU_A = 0x0C; //< Controls the input pull-up resistors for the port A pins. const BNK0_GPPU_B = 0x0D; //< Controls the input pull-up resistors for the port B pins. const BNK0_INTF_A = 0x0E; //< Reflects the input interrupt condition on the port A pins. const BNK0_INTF_B = 0x0F; //< Reflects the input interrupt condition on the port B pins. const BNK0_INTCAP_A = 0x10; //< Captures the port A value at the time the interrupt occurred. const BNK0_INTCAP_B = 0x11; //< Captures the port B value at the time the interrupt occurred. const BNK0_GPIO_A = 0x12; //< Reflects the value on the port A. const BNK0_GPIO_B = 0x13; //< Reflects the value on the port B. const BNK0_OLAT_A = 0x14; //< Provides access to the port A output latches. const BNK0_OLAT_B = 0x15; //< Provides access to the port B output latches. // The following register addresses assume IOCON.BANK = 1 << THIS IS what this program is using const BNK1_IODIR_A = 0x00; const BNK1_IPOL_A = 0x01; const BNK1_GPINTEN_A = 0x02; const BNK1_DEFVAL_A = 0x03; const BNK1_INTCON_A = 0x04; const BNK1_IOCON_A = 0x05; const BNK1_GPPU_A = 0x06; const BNK1_INTF_A = 0x07; const BNK1_INTCAP_A = 0x08; const BNK1_GPIO_A = 0x09; const BNK1_OLAT_A = 0x0A; const BNK1_IODIR_B = 0x10; const BNK1_IPOL_B = 0x11; const BNK1_GPINTEN_B = 0x12; const BNK1_DEFVAL_B = 0x13; const BNK1_INTCON_B = 0x14; const BNK1_IOCON_B = 0x15; const BNK1_GPPU_B = 0x16; const BNK1_INTF_B = 0x17; const BNK1_INTCAP_B = 0x18; const BNK1_GPIO_B = 0x19; const BNK1_OLAT_B = 0x1A; var i2cModule = require("i2c-bus"); // https://github.com/fivdi/i2c-bus var process = require('process'); const performance = require('perf_hooks').performance; process.on('uncaughtException', (err, origin) => { console.error( "!!! Unhandled error in MPC23017/PCF8574 node-red module. >> " + err + " >> ORIGIN: " + origin ); }); // *** Bit manipulation functions: //Get bit function getBit(number, bitPosition) { return (number & (1 << bitPosition)) === 0 ? 0 : 1; } //Set Bit function setBit(number, bitPosition) { return number | (1 << bitPosition); } //Clear Bit function clearBit(number, bitPosition) { const mask = ~(1 << bitPosition); return number & mask; } //Update Bit function updateBit(number, bitPosition, bitValue) { const bitValueNormalized = bitValue ? 1 : 0; const clearMask = ~(1 << bitPosition); return (number & clearMask) | (bitValueNormalized << bitPosition); } function showState (_obj, _onOffState, _errorState) { // _errorState: if address is taken or a global error occurred while trying read/write to the chip if (log2consol) console.log(" ...state update >> _onOffState: " + _onOffState +" globalState= "+ _errorState + " Node_id=" + _obj.id); if ((_errorState >= 2) || (_onOffState == -2)) { // ERROR ! // it is impossible to determine if a bus is existing and working, so the whole chip will be set into "error state" _obj.status({fill:"red" ,shape:"dot" ,text:"! Error." + _errorState >= 2 ? " Re-test:" + _errorState + "sec" : ""}); } else { if (_errorState == 0) { // Uninitialized _obj.status({fill:"yellow",shape:"ring",text:"unknown yet"}); } else { if (_errorState == 1) { // Working :-) const _onOff = _obj.invert ? !_onOffState : _onOffState; if (_onOff == true) { _obj.status({fill:"green" ,shape:"dot" ,text:"On"}); }else if (_onOff == false){ _obj.status({fill:"grey" ,shape:"ring",text:"Off"}); } } } } } function mcp_pcf_chipNode(n) { RED.nodes.createNode(this, n); var mainChipNode = this; this.busNum = parseInt(n.busNum, 10); // converts string to decimal (10) this.addr = parseInt(n.addr , 16); // converts from HEXA (16) to decimal (10) if (isNaN(this.addr)) {this.addr = 0x20; } this.MaxBits = (this.addr >= 0x40 ? 8 : 16); this.isInputs = 0x0000; // which ports are input ports (saved in binary form) this.pullUps = 0x0000; // this.inverts = 0x0000; this.startAllHIGH = n.startAllHIGH; // Some relay boards are negated. (HIGH = OFF) this.ids = [null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null]; //Array(16) this.globalState = 0; // 0=uninitialized 1=working: on/off=see:ids 2=error this.errorCount = 0; this.allStates = -1; // 0x0000; this.lastTimeRed = 0; // when was the last time a successfull a full 16bit READ operation happened (inputs) this.readLength = 0; // how long did it take the last read sequence (inputs)(ms) // Timer related variables: this.interval = 0 + n.interval; // if ((this.interval < 20) && (this.interval != 0)) this.interval = 20; // one readout takes at least 8 ms x2 banks this.origInterv = this.interval; this.chipTimer = null; this.timerIsRunning = false; this.rwIsHappening = false; // global atomic bool (TODO for later) // *** GLOBAL RW context *** // // >> to prevent Read-Write operations happening at the same time on the same i2c Bus. let global_i2c_bus_RW_ctx = this.context().global; const _i2c_ctx_name = "i2c"+ this.busNum + "RW"; if (global_i2c_bus_RW_ctx.get(_i2c_ctx_name) == null) { if (log2consol) console.log(" MCP/FCP context does not exists yet. Creating now: " + _i2c_ctx_name); global_i2c_bus_RW_ctx.set(_i2c_ctx_name, false); } else if (log2consol) console.log(" MCP/FCP context does exists already: " + _i2c_ctx_name ); // TODO: block RW operations happening at the same time... this.RW_check = function() { if ( mainChipNode.rwIsHappening ) return false; if (global_i2c_bus_RW_ctx.get(_i2c_ctx_name) == null) return false; mainChipNode.rwIsHappening = true; global_i2c_bus_RW_ctx.set(_i2c_ctx_name, true); return true; } this.RW_finish = function() { mainChipNode.rwIsHappening = false; global_i2c_bus_RW_ctx.set(_i2c_ctx_name, false); } if (log2consol) console.log(" MCP/PCF chip initialization OK. BusNumber=" + this.busNum + " Address=" + this.addr + " id:" + this.id); /* // Sleep 4ms to wait for rwIsHappening gets false again const sleep = (delay) => new Promise((resolve) => setTimeout(resolve, delay)); const waitForPrevOperationToFinish = async () => { let maxTimeout = 0; while ((maxTimeout < 40) && this.rwIsHappening) { await sleep(4); maxTimeout += 4; } return; } */ /* ### INITIALIZATION of the Chip ### */ /* ################################## */ this.initializeBit = function(_bitNum, _isInput, _pullUp, _callerNode){ const _parCh = _callerNode.parentChip; if (log2consol) console.log(" MCP/PCF init-Bit started... Addr=" + _parCh.addr + " bitNum=" + _bitNum + " isInput=" + _isInput + " pullUp=" + _pullUp + " startAllHigh=" + _parCh.startAllHIGH + " LastState="+ _callerNode.lastState); if (_parCh.ids[_bitNum] != null ) { if (log2consol) console.log("!!MCP chip-node-ids[_bitNum] != null ALREADY exists a node for this Bit set:" + _parCh.ids[_bitNum]); if (_parCh.ids[_bitNum] != _callerNode.id ) { if (log2consol) console.log("!!MCP chip-node-ids[_bitNum] != _callerNode.id =" + _callerNode.id ); _callerNode.lastState = -2; // error state showState(_callerNode, -2, 2); // show red error status at the corner of the Node _callerNode.error("!!MCP/PCF pin is already used by an other node: Bit=" + _bitNum + " Bus=" + _parCh.busNum + " Addr=" + _parCh.addr + " ID=" + _parCh.ids[_bitNum]); return false; } // } for (var i=0; i < _parCh.MaxBits; i++){ //NEED TO REMOVE ANY OTHER REFERENCES TO THIS ID (MaxBits= 8 or 16) if (_parCh.ids[i] == _callerNode.id) { _parCh.ids[i] = null; } } _parCh.ids[_bitNum] = _callerNode.id; // remember, which pin (bitNum) is this Node assigned to. if ( ! _parCh.RW_check() ) return false; let _processState = 0; try { let aBus = i2cModule.openSync(_parCh.busNum); // *** PCF8574 or PCF8574A chip *** // if (_parCh.addr >= 0x40) { if ((_parCh.startAllHIGH == true) && (_callerNode.lastState = -2)){ if (log2consol) console.log(" PCF Now Setting ALL pins to HIGH. A+B = 1111111111111111 Addr=" + Math.ceil( _parCh.addr / 2 )); aBus.sendByteSync( Math.ceil( _parCh.addr / 2 ), 0xFF); _parCh.lastTimeRed = performance.now(); _parCh.allStates = 0xFF; _parCh.startAllHIGH = false; // 1x running is enough. Turn it off now. } else { _parCh.allStates = aBus.receiveByteSync( Math.ceil( _parCh.addr / 2 ) ); } } else { // *** MCP23017 Chip *** // function bbb () { let bank0 = -1; let bank1 = -1; bank0 = aBus.readByteSync(_parCh.addr, BNK0_IOCON_B); bank1 = aBus.readByteSync(_parCh.addr, BNK1_IOCON_A); console.log("************** Bank IOCON_B_BNK0=" + bank0.toString(2) + " ********* Bank IOCON_A_BNK1=" + bank1.toString(2)); console.log("**** A0=" + aBus.readByteSync(_parCh.addr, BNK0_OLAT_A).toString(2) + " B0=" + aBus.readByteSync(_parCh.addr, BNK0_OLAT_B).toString(2)); console.log("**** A1=" + aBus.readByteSync(_parCh.addr, BNK1_OLAT_A).toString(2) + " B1=" + aBus.readByteSync(_parCh.addr, BNK1_OLAT_B).toString(2)); } // First forcing chip's IOCON.BANK=1 mode twice, so it WILL be =1 . //bbb(); aBus.writeByteSync(_parCh.addr, BNK0_IOCON_B , 0xA0); //set mode IOCON bank to 8bit mode See:Page-17 TABLE 3-4 at chip PDF //bbb(); // setting IOCON to interrupt 6.bit=MIRROR:1 + 2.bit=ODR:1 (Open-Drain output of interrupt) // 11100100 = 0xE4 aBus.writeByteSync(_parCh.addr, BNK1_IOCON_A , 0xE4); //set mode IOCON bank to 8bit mode See:Page-17 TABLE 3-4 at chip PDF //bbb(); _processState = 2; // Turn On ALL pins at start if ((_parCh.startAllHIGH == true) && (_callerNode.lastState = -2)){ if (log2consol) console.log(" MCP Now Setting ALL pins to HIGH. A+B = 1111111111111111"); aBus.writeByteSync(_parCh.addr, BNK1_OLAT_A, 0xFF); //Set output A to 11111111 aBus.writeByteSync(_parCh.addr, BNK1_OLAT_B, 0xFF); //Set output B to 11111111 _parCh.allStates = 0xFFFF; _parCh.startAllHIGH = false; // 1x running is enough. Turn it off now. } if (_parCh.allStates = -1) { let ip1 = aBus.readByteSync(_parCh.addr, BNK1_GPIO_A); let ip2 = aBus.readByteSync(_parCh.addr, BNK1_GPIO_B); _parCh.lastTimeRed = performance.now(); ip2 = (ip2 << 8); _parCh.allStates = ip1 + ip2; // adding together with "or" = ip1 | ip2; if (log2consol) console.log(" MCP First READ OK. A="+ip1.toString(2)+" B="+ip2.toString(2)+" allStates=" + _parCh.allStates); } if (_isInput) {_parCh.isInputs = _parCh.isInputs | (1 << _bitNum) } else {_parCh.isInputs = _parCh.isInputs & ~(1 << _bitNum) }; if (_bitNum < 8) {aBus.writeByteSync(_parCh.addr, BNK1_IODIR_A, _parCh.isInputs & 0xFF);} //update in out mode A else {aBus.writeByteSync(_parCh.addr, BNK1_IODIR_B, (_parCh.isInputs >> 8) & 0xFF);} //update in out mode B if (_isInput) { if (_pullUp) { _parCh.pullUps = _parCh.pullUps | (1 << _bitNum) } else { _parCh.pullUps = _parCh.pullUps & ~(1 << _bitNum) }; // if (_invert) { _parCh.inverts = _parCh.inverts | (1 << _bitNum) } else { _parCh.inverts = _parCh.inverts & ~(1 << _bitNum) }; if (log2consol) console.log(" MCP Input pullups=" + _parCh.pullUps); if (_bitNum < 8) {aBus.writeByteSync(_parCh.addr, BNK1_GPPU_A , _parCh.pullUps & 0xFF);} //update input pull-up 100kQ resistor A else {aBus.writeByteSync(_parCh.addr, BNK1_GPPU_B , (_parCh.pullUps >> 8) & 0xFF);} //update input pull-up 100kQ resistor B aBus.writeByteSync(_parCh.addr, BNK1_IPOL_A, 0x00); //turn OFF input invert A aBus.writeByteSync(_parCh.addr, BNK1_IPOL_B, 0x00); //turn OFF input invert B if (_bitNum < 8) {aBus.writeByteSync(_parCh.addr, BNK1_IPOL_A, 0x00);} //disable input invert A else {aBus.writeByteSync(_parCh.addr, BNK1_IPOL_B, 0x00);} //disable input invert B // if (_bitNum < 8) {aBus.writeByteSync(_parCh.addr, IPOL_A, _parCh.inverts & 0xFF);} //update input invert A // else {aBus.writeByteSync(_parCh.addr, IPOL_B, (_parCh.inverts >> 8) & 0xFF);} //update input invert B if (_bitNum < 8) {aBus.writeByteSync(_parCh.addr, BNK1_GPINTEN_A, _parCh.isInputs & 0xFF);} //update interrupts A else {aBus.writeByteSync(_parCh.addr, BNK1_GPINTEN_B, (_parCh.isInputs >> 8) & 0xFF);} //update interrupts B } } // MCP chip _processState = 3; aBus.closeSync(); aBus = null; if (log2consol) console.log("OK. MCP/PCF Bit-initialization finished. Bus closed."); _parCh.globalState = 1; // means: Working :) _callerNode.lastState = getBit( _parCh.allStates, _bitNum ); // SET LAST STATE _parCh.RW_finish(); // release Read-Write blocking return true; } catch (err) { _parCh.RW_finish(); if (_parCh.globalState < 60) _parCh.globalState += 2; // The whole chip in error mode, because the Bus could not be opened _callerNode.lastState = -2; _callerNode.error( busStateTexts[_processState] + " failed. Bus=" + _parCh.busNum + " Pin=" + _bitNum + "\n Error:" + err); showState( _callerNode, false, _parCh.globalState ); aBus = null; return false; }; } // ********** TIMER ********** // ... for input // *************************** // this.startChipTimer = function(_newInterval) { if (log2consol) console.log(" MCP/PCF startChipTimer = " + _newInterval +" ms"); if ((_newInterval == undefined) || (_newInterval == 0)) { console.log(" MCP/PCF Timer interval is UNDEFINED or 0 ! Timer will not be started, old may be cleared. Exiting."); if (mainChipNode.chipTimer) clearInterval(mainChipNode.chipTimer); return null; } if (mainChipNode.chipTimer != null) { // timer is already running if (log2consol) console.log(" MCP/PCF Timer is already running"); if (mainChipNode.interval == _newInterval) { if (log2consol) console.log(" MCP/PCF This timer interval is already set. There is nothing to do."); return null; } // nothing to do clearInterval(mainChipNode.chipTimer); // clear old, so a new can started mainChipNode.interval = _newInterval; mainChipNode.chipTimer = null; if (log2consol) console.log(" MCP/PCF Old timer destroyed."); } // STARTING a Timer in repeat mode if (log2consol) console.log(" MCP/PCF Starting Timer now..."); mainChipNode.chipTimer = setInterval(mainChipNode.myTimer, mainChipNode.interval ); } // START the timer now ... >> moved this part to InputNode-creation //this.startChipTimer( this.interval ); // START, if any input nodes are available this.myTimer = function(read1x) { let _processState = 0; const _addr = mainChipNode.addr; if (isNaN(mainChipNode.busNum)) { console.error(" MCP/PCF chip myTimer busNum is undefined. Exiting."); mainChipNode.globalState += 2; return false; } if ( ! mainChipNode.RW_check() ) { console.error(" MCP/PCF myTimer is already running. Preventing to overlap > Exiting."); return false; } // prevent overlapping const _readTime = performance.now(); // millisec. To change the Timer value, if a too short period is set. try { if (timerLog && log2consol) console.log(" MCP/PCF myTimer: opening bus... Time: " + new Date( new Date().getTime() ).toISOString().slice(11, -1) ); // this.on('error', function( i2cErr ) { timerErrorHandler(i2cErr) } ); // start async error handling let _aBus = i2cModule.openSync(mainChipNode.busNum); _processState = 1; let ipAll = 0; if (_addr >= 0x40) { // it is a PCF8574 chip if (timerLog && log2consol) console.log(" PCF8574 >> Now reading 8bit. Addr=" + _addr/2); ipAll = _aBus.receiveByteSync( Math.ceil( _addr / 2 ) ); if (timerLog && log2consol) console.log(" PCF8574 Read success ipAll00=" + ipAll.toString(2)); } else { if (timerLog && log2consol) console.log(" MCP23017 >> Now reading A+B banks... Typeof _aBUS:" + typeof(_aBus)); let ipA = _aBus.readByteSync(_addr, BNK1_GPIO_A); let ipB = _aBus.readByteSync(_addr, BNK1_GPIO_B); ipAll = ipA + (ipB << 8); if (timerLog && log2consol) console.log(" MCP23017 Read success ipA00=" + ipA.toString(2) + " ipB00=" + ipB.toString(2) + " ipALL (dec)=" + ipAll); } // mainChipNode.lastTimeRed = performance.now(); _processState = 3; _aBus.closeSync(); if (mainChipNode.globalState != 1) { mainChipNode.globalState = 1; // successful read occured. No more "error state" or "uninitialised" if (mainChipNode.interval != mainChipNode.origInterv) { if (timerLog && log2consol) console.log(" MCP/PCF Starting ChipTimer. Interval=" + mainChipNode.origInterv); mainChipNode.startChipTimer( mainChipNode.origInterv ); // this will delete the old timer and start normally again } } // ********* Now checking ALL the possible nodes, if any of these needs to be updated if (ipAll != mainChipNode.allStates){ let diffWord = ipAll ^ mainChipNode.allStates; if (log2consol) console.log("!Change! of an i2c MCP/PCF input. IP MASK0000=" + ipAll.toString(2) + " Diff Mask0000=" + diffWord.toString(2)); for (let i=0; i < mainChipNode.MaxBits; i++){ // (MaxBits= 8 or 16) if (diffWord & (1 << i)){ const newState = (((ipAll & (1 << i)) == 0) ? false : true); if ( mainChipNode.ids[i] != null) { const n = RED.nodes.getNode(mainChipNode.ids[i]); if (log2consol) console.log(" MCP/PCF > ..searching for node i=" + i + " id=" + mainChipNode.ids[i] + " found node:" + n + " glob_inp=" + mainChipNode.isInputs); if (n != null) { //&& (mainChipNode.isInputs & (1 << i)) > 0){ // check bit is used and is an input if (log2consol) console.log(" MCP/PCF > Found a Node needs to be updated. Bit=" + i + " newState=" + newState + " LastState=" + n.lastState); n.changed(newState, read1x); } } } } mainChipNode.allStates = ipAll; } } catch (err) { if (mainChipNode.globalState < 63) mainChipNode.globalState += 2; // The whole chip in error mode, because the Bus could not be opened. Increasing next time-read to 2-4-6-..-60 sec. err.discription = busStateTexts[_processState] + " failed."; err.busNumber = mainChipNode.busNum; err.address = _addr; err.allStates = mainChipNode.allStates; console.error(err.discription + " [Bus="+ mainChipNode.busNum +"] [Addr=" + _addr + "] [mainChipNode.allStates=" + mainChipNode.allStates + "]"); mainChipNode.error(err); mainChipNode.RW_finish(); try { // update ALL nodes, so they show "error" for (var i=0; i < mainChipNode.MaxBits; i++){ //(MaxBits= 8 or 16) if ( mainChipNode.ids[i] != null) { const n = RED.nodes.getNode(mainChipNode.ids[i]); if (n != null) { showState(n, -2, mainChipNode.globalState); //if (n.type == "MCP PCF In") n.changed(-2, true); } } } if ((_processState < 3) && !read1x) { // if !read1x = called from debounce ... it should not restart mainChipNode.startChipTimer( mainChipNode.globalState * 1000 ); // re-try every 2-4-6-...60 sec. } } catch (err){ console.error(err); }; return false; }; mainChipNode.RW_finish(); mainChipNode.lastTimeRed = performance.now(); //new Date().getTime(); mainChipNode.readLength = mainChipNode.lastTimeRed - _readTime; if (! read1x) { // if "continuous read" is happening now if (mainChipNode.interval < mainChipNode.readLength) { // the time the reading took was too long. Increased the interval to double of that (ms). mainChipNode.warn(" MCP/PCF Interval (" + mainChipNode.interval + "ms) is too short for input. Setting new time = " + (mainChipNode.readLength * 2).toString()); mainChipNode.startChipTimer( Math.trunc(mainChipNode.readLength * 2)); // double the waiting period } else if ((mainChipNode.origInterv != mainChipNode.interval) && (mainChipNode.readLength < mainChipNode.origInterv)) { mainChipNode.startChipTimer( mainChipNode.origInterv ); // set back original interval } } return true; } this.on('close', function() { // stopping or deleting the Main-Chip-config try { if (mainChipNode.chipTimer) { if (log2consol) console.log(" MCP/PCF Closing ... Clearing myTimer."); clearInterval(mainChipNode.chipTimer); mainChipNode.chipTimer = null; } } catch (err) { console.error( " MCP/PCF Error while closing timer: " + err ); }; try { global_i2c_bus_RW_ctx.set(_i2c_ctx_name, undefined); // clearing global context } catch {} }); } // REGISTERING the main chip : RED.nodes.registerType("mcp_pcf_chip", mcp_pcf_chipNode); //INPUT SECTION function mcp_pcf_inNode(_inputConfig) { RED.nodes.createNode(this, _inputConfig); var node = this; this.bitNum = _inputConfig.bitNum; this.pullUp = _inputConfig.pullUp; this.invert = _inputConfig.invert; this.debounce = _inputConfig.debounce; this.deB_timer = null; this.onMsg = _inputConfig.onMsg; this.offMsg = _inputConfig.offMsg; this.lastState = -2; this.initOK = false; // check Master-Chip setup let _parentChipNode = RED.nodes.getNode(_inputConfig.chip); this.parentChip = _parentChipNode; if (!_parentChipNode) { node.error("[MCP23017 + PCF8574] Master-global-Chip not found! Skipping in-node creation."); showState(node, true, 0); return null; } if (this.debounce > (_parentChipNode.interval - 20)) this.debounce = _parentChipNode.interval - 20; // 1 read needs 20ms >> so debounce should be that much shorter if (this.debounce < 0) this.debounce = 0; if (log2consol) console.log("---"); if (log2consol) console.log(">>> Initializing PCF8574 or MPC23017 Input node >> bitNum=" + this.bitNum + " pullUp=" + this.pullUp + " invert=" + this.invert + " id=" + this.id ); this.initOK = _parentChipNode.initializeBit (this.bitNum, true, this.pullUp, node); showState(node, this.lastState, _parentChipNode.globalState); // shows uninit (yellow) or error (red) this.deB_timer = null; //deBounce this.on('close', function() { if (node.deB_timer != null){ if (log2consol) console.log(" MCP/PCF > clearing old Debounce Input timer... [Bit=" + node.bitNum + "]"); clearTimeout(node.deB_timer); node.deB_timer = null; } }); this.updateState = function(_state, _msg) { if (node.lastState != _state) { if (log2consol) console.log(">> MCP/PCF Input State Changed=" + _state + " [Bit=" + node.bitNum + "] id=" + node.id); showState(node, _state, _parentChipNode.globalState); // will show inverted, if needed node.lastState = _state; } if (_parentChipNode.globalState == 1){ const nullmsg = (_msg == null); if (nullmsg) _msg = {}; const _stateINV = node.invert ? !_state : _state; if ( _stateINV && node.onMsg ) _msg.payload = true; if (! _stateINV && node.offMsg) _msg.payload = false; if (nullmsg && (_msg.payload != null)) {node.send( _msg )} else return _msg; // if called from "read_1x" input >> do not send yet } } this.changed = function( _state, _read1x ) { if (node.deB_timer != null){ if (log2consol) console.log(" MCP/PCF > clearing old Debounce Input timer... [Bit=" + node.bitNum + "]"); clearTimeout(node.deB_timer); node.deB_timer = null; } if ( !_read1x && (node.debounce != 0) && (_parentChipNode.globalState == 1) && ( (_state == true) || (_state == false) ) ) { // Start debounce re-checks the last state node.deB_state = _state; node.deB_timer = setTimeout(node.deBounceEnd, node.debounce, _state); if (log2consol) console.log(" MCP/PCF > New input Debounce timer set. TimeEnd=" + node.debounce + " State=" + _state); } else { node.updateState(_state, null); node.deB_state = _state; } } this.deBounceEnd = function(_state){ node.deB_timer = null; if (_parentChipNode.globalState > 3) { if (log2consol) console.log(" MCP/PCF > Input timer Bounce CANCELED because chip is in Global-Error-State" ); return false; } let _now = performance.now(); if ((_now - node.lastTimeRed) < node.debounce * 1.2 ) { _state = getBit( _parentChipNode.allStates, node.bitNum ); node.updateState(_state, null); } else { let _read_success = _parentChipNode.myTimer(true); // forcing to re-read the current state from chip if ( _read_success ){ _state = getBit( _parentChipNode.allStates, node.bitNum ); if (log2consol) console.log(" MCP/PCF > Input timer Bounce Ended. [NewState=" + _state + "] [Last State=" + node.lastState + "] [Deb.state=" + node.deB_state +"] [Bit=" + node.bitNum + "] Ellapsed=" + (_now - node.lastTimeRed) + "ms" ); if (_state == node.deB_state) node.updateState(_state, null); } else { node.deB_timer = setTimeout(node.deBounceEnd, node.debounce, _state); } } } this.on('input', function(msg, send, done) { // triggering an Immediate read if payload is True or 1 >> to support Interrupts if (msg.payload) { let _parCh = node.parentChip; if (log2consol) console.log(" MCP/PCF > Input Force-read triggered! msg.payload=" + msg.payload ); msg.immediateReadSuccess = _parCh.myTimer(true); // result is True, if succesfully red, false if any error occured during read msg.readingTimeLengthMs = _parCh.readLength; msg.allStatesRaw = _parCh.allStates; msg.allBinary = msg.immediateReadSuccess ? _parCh.allStates.toString(2) : ""; msg = node.updateState( node.lastState, msg ); // << this will add (inverted) .payload if (log2consol) console.log(" MCP/PCF > Now sending msg. msg.payload=" + msg.payload ); node.send(msg); if (done) done(); } }); this.on('close', function() { // stopping or deleting this node let _parCh = node.parentChip; try { for (let i=0; i < _parCh.MaxBits; i++){ //(MaxBits= 8 or 16) if ( _parCh.ids[i] == node.id) { _parCh.ids[i] = null; break; } } } catch (err) { console.error( " MCP/PCF Error while closing an In-Node: " + err ); }; }); if (this.initOK) { _parentChipNode.startChipTimer( _parentChipNode.interval ); }// START continuous read, if any input nodes are available else { } } RED.nodes.registerType("MCP PCF In", mcp_pcf_inNode); //OUTPUT SECTION function mcp_pcf_outNode(_OutputConfig) { RED.nodes.createNode(this, _OutputConfig); var node = this; node.bitNum = _OutputConfig.bitNum; node.invert = _OutputConfig.invert; node.lastState = -2; node.initOK = false; // check Master-Chip setup let _parentChipNode = RED.nodes.getNode(_OutputConfig.chip); // this is not a visible node, but a hidden Chip-configuration one node.parentChip = _parentChipNode; if (!_parentChipNode) { node.error("Master MCP23017 or PCF8574 Chip not set! Skipping node creation. Node.id=" + node.id); showState(node, -2, 2); return null; } node.startAllHIGH = _parentChipNode.startAllHIGH; if (log2consol) console.log("---"); console.log(">>> Initializing PCF8574 or MPC23017 Output node >> invert=" + node.invert + " bitNum=" + node.bitNum + " ID=" + node.id); this.initOK = _parentChipNode.initializeBit(node.bitNum, false, false, node); showState(node, this.lastState, _parentChipNode.globalState); // shows uninit (yellow) or error (red) this.changed = function( _state, _read1x ) { showState(node, _state, _parentChipNode.globalState); // will show inverted, if needed node.lastState = _state; } this.setOutput = function(_bitNum, _newState, _callerNode){ let _processState = 0; if ( ! _callerNode) { consol.error(" MCP PCF setOutpup >> _callerNode=null !"); return false; } let _parCh = _callerNode.parentChip; if ( ! _parCh) { _callerNode.error(" MCP PCF setOutpup >> _callerNode.parentChip=null !"); return false; } const _addr = _parCh.addr; if ( ! _addr) { _callerNode.error(" MCP PCF setOutpup >> parentChip.addr=null !"); return false; } if ( ! _parCh.RW_check() ) return false; // checks, if there is any try { let ip8 = -1; let ip16 = -1; if (log2consol) console.log(" MCP/PCF setOutput "+ _callerNode.id +" > Addr=" + _addr + " BitNum=" + _bitNum + " _newState:" + _newState +" > opening bus..."); let _aBus = i2cModule.openSync(_parCh.busNum); _processState = 1; // Set ALL pins to 0000000000000000 or 1111111111111111 if (_bitNum == -1) { let on_off = _newState ? 0xFF : 0x00; if (_addr >= 0x40) { // This is a PCF 8bit chip _aBus.sendByteSync (Math.ceil( _addr / 2 ), on_off & 0xFF); _parCh.lastTimeRed = performance.now(); if (log2consol) console.log(" PCF 8bit chip ALL set to=" + on_off); _parCh.allStates = on_off; } else { // MCP chip _aBus.writeByteSync(_addr, BNK1_OLAT_A, on_off & 0xFF); //Set output A _aBus.writeByteSync(_addr, BNK1_OLAT_B, on_off & 0xFF); //Set output B _parCh.lastTimeRed = performance.now(); if (log2consol) console.log(" MPC 16bit chip ALL set to=" + on_off); _parCh.allStates = _newState ? 0xFFFF : 0x00; } for (let i=0; i < _parCh.MaxBits; i++){ //(MaxBits= 8 or 16) if ( _parCh.ids[i] != null) { const n = RED.nodes.getNode(_parCh.ids[i]); if (n != null) { showState( n , _newState, _parCh.globalState); n.lastState = _newState; } } } } // Set one pin only to: 0 or 1 else { // first it reads the current state of pins of 1 bank (A or B) (takes 4ms) if (_bitNum < 8) { if (_addr >= 0x40) ip8 = _aBus.receiveByteSync(Math.ceil( _addr / 2 )); // PCF chip else ip8 = _aBus.readByteSync(_addr, BNK1_GPIO_A); ip16 = ip8; } else { ip8 = _aBus.readByteSync(_addr, BNK1_GPIO_B); ip16 = (ip8 << 8); // make it 16 bit } _parCh.lastTimeRed = performance.now(); if (log2consol) console.log(" Read before write success ip8="+ ip8 + " ip16="+ ip16 ); ip16 = updateBit(ip16, _bitNum, _newState); //16 bit if (log2consol) console.log(" Updated bit ip16="+ ip16 ); //if (_newState) {ip1 = ip1 | 1 << _bitNum ;} //else {ip1 = ip1 & ~(1 << _bitNum);} _processState = 2; if (_addr >= 0x40) _aBus.sendByteSync(Math.ceil( _addr / 2 ), ip16 & 0xFF); // PCF chip else { if (_bitNum < 8) {_aBus.writeByteSync(_addr, BNK1_OLAT_A, ip16 & 0xFF);} //Set output A else {_aBus.writeByteSync(_addr, BNK1_OLAT_B, (ip16 >> 8) & 0xFF);} //Set output B } } _processState = 3; _aBus.closeSync(); _aBus = null; _parCh.globalState = 1; // working let n = _callerNode; if ( n.bitNum != _bitNum ) n = _parCh.ids[_bitNum]; // if the function was called with a "direct-msg-control-method" change the right node if exists if (n != null) showState(n, _newState, 1); if (log2consol) console.log(":-) MCP/PCF setOutput finished. Closing bus. ip1="+ ip16 ); _parCh.RW_finish(); return true; } catch (err) { if (_parCh.globalState < 60) _parCh.globalState += 2; // The whole chip in error mode, because the Bus could not be opened _callerNode.lastState = -2; showState(_callerNode, -2, 2); let _ee = busStateTexts[_processState] + " failed. Bus="+ _parCh.busNum +" Addr=" + _addr + " Pin="+_bitNum + " NewState=" + _newState; console.error(_ee + " " + err); _callerNode.error([_ee, err]); _aBus = null; _parCh.RW_finish(); return false; }; } this.on('input', function(msg) { let _parCh = node.parentChip; if (!node.initOK) { if (log2consol) console.log(" MCP/PCF Out > New msg recieved, but Node not initialized yet. ID=" + node.id + " bitNum=" + node.bitNum); node.initOK = _parCh.initializeBit(node.bitNum, false, false, this.id); if (!node.initOK) {return null;} } if (! _parCh) { consol.error(" MCP PCF Out >> input msg recieved, but ParentChip of Node is null!"); return null } // *** SET only 1 pin via msg JSON *** // if ( msg.payload == -1 ) { if (log2consol) console.log(" MCP/PCF > Set direct pin Chip Addr=" + _parCh.addr); const _Pin1 = parseInt( msg.pin ); if ((_Pin1 == NaN) || (_Pin1 < 0) || (_Pin1 > 15) ) { node.error("msg.pin not properly set for direct control of a MCP or PCF chip. It must be a number between 0-7 or 8-15"); return false; } if (msg.state == null) { node.error("msg.state not set for direct control of a MCP or PCF chip."); return false; } const _OnOff1 = (msg.state == true) || (msg.state == 1); //safe boolean conversion node.setOutput(_Pin1, _OnOff1, node); } else if ( msg.payload == "all0" ) { if (log2consol) console.log(" MCP/PCF > Set ALL pins to 0000... Chip Addr=" + _parCh.addr); node.setOutput(-1, false, node); } else if ( msg.payload == "all1" ) { if (log2consol) console.log(" MCP/PCF > Set ALL pins to 1111... Chip Addr=" + _parCh.addr); node.setOutput(-1, true, node); } else { //console.log("OUT: NEW input... payl=" + msg.payload); let _pinOn = (msg.payload == true) || (msg.payload == 1); //safe boolean conversion let _invPinOn = node.invert ? !_pinOn : _pinOn; if (node.setOutput(node.bitNum, _invPinOn, node)) { showState(node, _invPinOn, _parCh.globalState); node.lastState = _invPinOn; } } }) ; this.on('close', function() { // stopping or deleting this node let _parCh = node.parentChip; try { for (let i=0; i < _parCh.MaxBits; i++){ //(MaxBits= 8 or 16) if ( _parCh.ids[i] == node.id) { _parCh.ids[i] = null; break; } } } catch (err) { console.error( " MCP/PCF Error while closing an In-Node: " + err ); }; }); } RED.nodes.registerType("MCP PCF Out", mcp_pcf_outNode); }