UNPKG

@joe-ab1do/mcp-pcf-aio

Version:

Node-Red nodes for i2c port expanders MCP23008, MCP23017, PCF8574(A) & PCF8575

759 lines (688 loc) 62 kB
'Strict mode' const spawnSync = require('node:child_process').spawnSync; //used to get list of i2c buses const i2cModule = require('i2c-bus'); // https://github.com/fivdi/i2c-bus const process = require('process'); //used in line 8 const performance = require('perf_hooks').performance; //only performance.now() is used process.on('uncaughtException', (err, origin) => { console.error( "!!! Unhandled error in MCP230XX/PCF857X node-red module. >> " + err + " >> ORIGIN: " + origin ); }); debugger module.exports = function(RED) { //****** SETUP SECTION ******/ const busStateTexts = [ //used in catch(err) "Opening i2c Bus", // processState 0 ... actually this is done only virtually, the opening happens only at first read/write "Reading current state", // processState 1 "Writing byte", // processState 2 "Closing i2c bus"]; // processState 3 var log2console = false; // enabled on configuration node to show detailed logsfor that node in: node-red-log const timerLog = false; // !! WARNING !! << if true, will fill up the log with ALL read events (up to 50x3 msg. per sec !! if read interval is 20ms) // *** THESE REGISTER ADDRESSES ARE ONLY RELEVANT FOR MCP230xx CHIPS; PCF857x(A) CHIPS DO NOT HAVE REGISTERS TO SET ***/ // IOCON.BANK = 0 << !!! Non-Bank mode: Using this is NOT USED HERE.. const BNK0_IODIR_A = 0x00; const BNK0_IODIR_B = 0x01; const BNK0_IPOL_A = 0x02; const BNK0_IPOL_B = 0x03; const BNK0_GPINTEN_A = 0x04; const BNK0_GPINTEN_B = 0x05; const BNK0_DEFVAL_A = 0x06; const BNK0_DEFVAL_B = 0x07; const BNK0_INTCON_A = 0x08; const BNK0_INTCON_B = 0x09; const BNK0_IOCON_A = 0x0A; const BNK0_IOCON_B = 0x0B; const BNK0_GPPU_A = 0x0C; const BNK0_GPPU_B = 0x0D; const BNK0_INTF_A = 0x0E; const BNK0_INTF_B = 0x0F; const BNK0_INTCAP_A = 0x10; const BNK0_INTCAP_B = 0x11; const BNK0_GPIO_A = 0x12; const BNK0_GPIO_B = 0x13; const BNK0_OLAT_A = 0x14; const BNK0_OLAT_B = 0x15; // IOCON.BANK = 1 << BANK MODE: THIS IS WHAT THIS PROGRAM USES // MCP23008 only uses Bank A (register addresses 0X00 - 0X0A); MCP23017 uses both Banks A and B //Bank A: const BNK1_IODIR_A = 0x00; //< Defines the direction of a port: Input/Output. const BNK1_IPOL_A = 0x01; //< Sets the polarity inversion of input ports: GPIO register bit equals inverted input or not. const BNK1_GPINTEN_A = 0x02; //< Controls the interrupt-on-change for each input. const BNK1_DEFVAL_A = 0x03; //< Sets the input comparison value: interrupt will only occur if input differs from corresponding DEFVAL bit const BNK1_INTCON_A = 0x04; //< Controls whether input is compared to previous value or to DEFVAL register value before firing an interrupt. const BNK1_IOCON_A = 0x05; //< Controls the device. (0, INTPOL, ODR, HAEN, DISSLW, SEQOP, MIRROR, BANK) const BNK1_GPPU_A = 0x06; //< Controls engagement of the input pull-up resistors for the input pins. const BNK1_INTF_A = 0x07; //< Identifies the input that led to the interrupt condition. const BNK1_INTCAP_A = 0x08; //< Captures the input value at the time the interrupt occurred. const BNK1_GPIO_A = 0x09; //< Reflects the value on all pins: outputs and (inverted) inputs. const BNK1_OLAT_A = 0x0A; //< Provides access to the output latches. //Bank B const BNK1_IODIR_B = 0x10; //< Defines the direction of a port: Input/Output. const BNK1_IPOL_B = 0x11; //< Sets the polarity inversion of input ports: GPIO register bit equals inverted input or not. const BNK1_GPINTEN_B = 0x12; //< Controls the interrupt-on-change for each input. const BNK1_DEFVAL_B = 0x13; //< Sets the input comparison value: interrupt will only occur if input differs from corresponding DEFVAL bit const BNK1_INTCON_B = 0x14; //< Controls whether input is compared to previous value or to DEFVAL register value before firing an interrupt. const BNK1_IOCON_B = 0x15; //< Controls engagement of the input pull-up resistors for the input pins. const BNK1_GPPU_B = 0x16; //< Controls engagement of the input pull-up resistors for the input pins. const BNK1_INTF_B = 0x17; //< Identifies the input that led to the interrupt condition. const BNK1_INTCAP_B = 0x18; //< Captures the input value at the time the interrupt occurred. const BNK1_GPIO_B = 0x19; //< Reflects the value on all pins: outputs and (inverted) inputs. const BNK1_OLAT_B = 0x1A; //< Provides access to the output latches. // *** Bit manipulation helper 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); } //****** END SETUP SECTION ******/ //****** NODE STATUS SECTION ******/ function showStatus (_obj, _onOffStatus, _errorStatus) { // _errorStatus: if address is already in use or a global error occurred while trying read/write to the chip if (log2console) console.log(" ...status update >> _onOffStatus: " + _onOffStatus +" globalState= "+ _errorStatus + " Node_id=" + _obj.id); if ((_errorStatus >= 2) || (_onOffStatus == -2)) { // ERROR ! // it is impossible to determine if an i2c bus exists and works, so the whole chip is set to "error state" _obj.status({fill:"red" ,shape:"dot" ,text:"! Error." + _errorStatus >= 2 ? " Re-test:" + _errorStatus + "sec" : ""}); } else { if (_errorStatus == 0) { // Uninitialized _obj.status({fill:"yellow",shape:"ring",text:"unknown yet"}); } else { if (_errorStatus == 1) { // Working :-) const _onOff = _onOffStatus; //const _onOff = _obj.invert ? !_onOffStatus : _onOffStatus; if (_onOff == true) { _obj.status({fill:"green" ,shape:"dot" ,text:"On"}); } else if (_onOff == false){ _obj.status({fill:"grey" ,shape:"ring",text:"Off"}); } } } } } //****** END NODE STATUS SECTION ******/ //****** MAIN CHIP CREATION ******/ function mcp_pcf_chipNode(n) { RED.nodes.createNode(this, n); var mainChipNode = this; this.chipType = n.chipType; this.busNum = parseInt(n.busNum, 10); // converts string to decimal (10) this.addr = parseInt(n.addr , 16); // converts from HEXA (16) string to decimal this.maxBits =(this.chipType==="MCP23017" || this.chipType==="PCF8575") ? 16 : 8; this.isInputs = 0x0000; // keeps track of input ports (saved in hexadecimal form) this.pullUps = 0x0000; // keeps track of pullUps (not relevant for PCF chips) this.startAllHIGH = n.startAllHIGH; // Some relay boards use negative logic (HIGH = OFF) << ab1do: only relevant for MCP chips: PCF chips default to HIGH this.logging = n.logging; //false by default; if true log is written to node-red-log this.ids = new Array(this.maxBits).fill(null); //depending on chiptype, 8 or 16 element null array this.globalState = 0; // 0=uninitialized 1=working: on/off=see:ids 2=error this.errorCount = 0; this.allStates = -1; // 0x1111; this.lastTimeRead = 0; // when was the last time a successfull full 8/16bit READ operation happened (inputs only) this.readLength = 0; // how long did the last read sequence take (inputs)(ms) // Timer related variables: this.interval = 0 + n.interval; this.origInterv = this.interval; this.chipTimer = null; this.timerIsRunning = false; log2console = true; if (log2console) console.log(" "+this.chipType+" chip initialization OK. BusNumber=" + this.busNum + " Address=0X" + this.addr.toString(16) + " id:" + this.id+" startAllHigh = "+this.startAllHIGH); log2console = mainChipNode.logging; /* ### INITIALIZATION of the Chip ### */ /* ################################## */ this.initializeBit = function(_bitNum, _isInput, _pullUp, _callerNode){ //< Only _bitNum and _callerNode relevant for PCF chips const _parCh = _callerNode.parentChip; var ip1, ip2; if (log2console) console.log(" "+_parCh.chipType+" init-Pin started... i2cAddr = 0X" + _parCh.addr.toString(16) + " pinNum=" + _bitNum + " isInput=" + _isInput + " pullUp=" + _pullUp + " startAllHigh=" + _parCh.startAllHIGH + " LastState="+ _callerNode.lastState); if (_parCh.ids[_bitNum] != null ) {//<<< ab1do: May not need anymore, now that client disallows creation of more than 1 node using same pin on a chip if (log2console) console.log("!!MCP chip-node-ids[_bitNum] != null: a node is ALREADY connected to this pin:" + _parCh.ids[_bitNum]); if (_parCh.ids[_bitNum] != _callerNode.id) { if (log2console) console.log("!!MCP chip-node-ids[_bitNum] != _callerNode.id =" + _callerNode.id); _callerNode.lastState = -2; // error state showStatus(_callerNode, -2, 2); // show red error status at the corner of the Node _callerNode.error("!!MCP/PCF pin is already used by another node: Bit=" + _bitNum + " Bus=" + _parCh.busNum + " Addr = 0X" + _parCh.addr.toString(16) + " 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) this Node is assigned to. let _processState = 0; try { let aBus = i2cModule.openSync(_parCh.busNum);//<< ab1do: .openSync by default does not allow access to i2cbus if bus is already in use. switch(_parCh.chipType) { case ("PCF8574"): case("PCF8574A"): // Check if need to Turn On ALL pins at start if ((_parCh.startAllHIGH == true) && (_callerNode.lastState = -2)){ // unnecessary? According to datasheet, PCF857X by default start all high. if (log2console) console.log(" "+_parCh.chipType+" Now Setting ALL output pins to HIGH; Addr = 0X" + _parCh.addr.toString(16)); _parCh.allStates = 0xFF; } else { _parCh.allStates = aBus.receiveByteSync(_parCh.addr); } if (_isInput) {_parCh.isInputs = _parCh.isInputs | (1 << _bitNum)} //identify input nodes else {_parCh.isInputs = _parCh.isInputs & ~(1 << _bitNum)} //...or outputs _parCh.allStates =_parCh.allStates | _parCh.isInputs; // bitwise OR allStates of chip with isInputs to change only output nodes aBus.sendByteSync(_parCh.addr, _parCh.allStates); //update chip with modified allStates: changes only output pins if(this.interval == undefined || this.interval == 0 ) {this.reviewStates(true,false)} //if not polling, read once to set input according input value if (log2console) console.log(" "+_parCh.chipType+" First READ OK; allStates=" + _parCh.allStates.toString(2).padStart(8,"0")); break; case("PCF8575"): // Check if need to Turn On ALL pins at start if ((_parCh.startAllHIGH == true) && (_callerNode.lastState = -2)){ if (log2console) console.log(" PCF8575 Now Setting ALL output pins to HIGH; Addr = 0X" + _parCh.addr.toString(16)); _parCh.allStates = 0xFFFF; } else { _parCh.allStates = aBus.readWordSync(_parCh.addr,_parCh.addr);//<<< RECEIVES 2 BYTES: LSB followed by MSB } if (_isInput) {_parCh.isInputs = _parCh.isInputs | (1 << _bitNum)} else {_parCh.isInputs = _parCh.isInputs & ~(1 << _bitNum)} _parCh.allStates =_parCh.allStates | _parCh.isInputs; aBus.writeWordSync(_parCh.addr, _parCh.allStates & 0xFF,(_parCh.allStates>>8) & 0xFF);//update chip with modified allStates (LSB then MSB) if(this.interval == undefined || this.interval == 0 ) {this.reviewStates(true,false)} //if not polling, read once to set input nodes according their input values if (log2console) console.log(" PCF8575 First READ OK; allStates=" + _parCh.allStates.toString(2).padStart(16,"0")); break; // End PCF chip case("MCP23017"): function bbb () { // this proc. is only for testing 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)); } //bbb(); aBus.writeByteSync(_parCh.addr, BNK0_IOCON_B, 0xA0); //Only has effect if MCP23017 is not in Bank mode (BNK0_IOCON_B address does not // exist if MCP23017 already in Bank mode) See:Page-17 TABLE 3-4 of MCP23017 datasheet PDF //bbb(); // IOCON: keep chip in Bank mode: bit7 BANK ->1 // functionally OR BankA/BankB interrupts: bit6 MIRROR ->1 // disable address pointer auto-increment: bit5 SEQOP ->1 // enable SDA slew rate control: bit4 DISSLEW->0 // HAEN only relevant for SPI version of chip: bit3 HAEN ->x // interrupt output as open drain: bit2 ODR ->1 (if instead active interrupt ->0) // if ODR, then INTPOL not relevant: bit1 INTPOL ->x (otherwise active high->1, active low->0) // bit 0 ignored: bit0 ->x // 11100100 = 0xE4 aBus.writeByteSync(_parCh.addr, BNK1_IOCON_A, 0xE4); // See:Page-17 TABLE 3-4 of MCP23017 datasheet PDF //bbb(); _processState = 2; // Check if need to Turn On ALL pins at start if ((_parCh.startAllHIGH == true) && (_callerNode.lastState = -2)) { if (log2console) console.log("MCP23017 Now Setting ALL pins to HIGH. A+B = 1111111111111111 Addr = 0X" + _parCh.addr.toString(16)); aBus.writeByteSync(_parCh.addr, BNK1_OLAT_A, 0xFF); //Set output A to 11111111 (LSB) aBus.writeByteSync(_parCh.addr, BNK1_OLAT_B, 0xFF); //Set output B to 11111111 (MSB) _parCh.allStates = 0xFFFF; // 16 bit: Bank-A GPIO 0-7 + Bank-B GPIO 0-7 shifted up (LSB+MSB) } if (_parCh.allStates = -1) { ip1 = aBus.readByteSync(_parCh.addr, BNK1_GPIO_A); //read PortA GPIO pins (LSB) ip2 = aBus.readByteSync(_parCh.addr, BNK1_GPIO_B); //read PortB GPIO pins (MSB) _parCh.lastTimeRead = performance.now(); ip2 = (ip2 << 8); //shift ip2 up to MSB of 16 bit combined PortA and PortB values _parCh.allStates = ip1 + ip2; // combine ip1 and up-shifted ip2 to form 16 bit word; } if (log2console) console.log(" MCP23017 First READ OK; A="+ip1.toString(2).padStart(16,"0")+" B="+ip2.toString(2).padStart(16,"0")+" allStates=" + _parCh.allStates.toString(2).padStart(16,"0")); // Set Registers // _parCh.isInputs determines which pins are inputs (IODIR = 1) or outputs (IODIR = 0) if (_isInput) {_parCh.isInputs = _parCh.isInputs | (1 << _bitNum);}// input: e.g. if _bitNum = 5, shift 1 left 5, then bitwise or with whatever _parCh.isInputs is makes pin 5 an input else {_parCh.isInputs = _parCh.isInputs & ~(1 << _bitNum);}//output: e.g. if _bitNum = 5, shift 1 left 5, then bitwise and (not bit5) with whatever _parCh.isInputs is makes pin 5 an output // set both banks aBus.writeByteSync(_parCh.addr, BNK1_IODIR_A, _parCh.isInputs & 0xFF); //update in/out mode A 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 (log2console) console.log(" MCP23017 Input pullups=" + _parCh.pullUps); if (_bitNum < 8) {aBus.writeByteSync(_parCh.addr, BNK1_GPPU_A , _parCh.pullUps & 0xFF);} //set internal pull-up 100K resistor A else {aBus.writeByteSync(_parCh.addr, BNK1_GPPU_B , (_parCh.pullUps >> 8) & 0xFF);} //set internal pull-up 100K resistor B if (_bitNum < 8) {aBus.writeByteSync(_parCh.addr, BNK1_IPOL_A, 0x00);} //disable Input invert(=POLarity) A else {aBus.writeByteSync(_parCh.addr, BNK1_IPOL_B, 0x00);} //disable Input invert(=POLarity) B if (_bitNum < 8) {aBus.writeByteSync(_parCh.addr, BNK1_GPINTEN_A, _parCh.isInputs & 0xFF);} //set INTerrupts ENable A else {aBus.writeByteSync(_parCh.addr, BNK1_GPINTEN_B, (_parCh.isInputs >> 8) & 0xFF);} //set INTerrupts ENable B if (_bitNum < 8) {aBus.writeByteSync(_parCh.addr, BNK1_INTCON_A, 0x00);} //set INTerrupts CONtrol A to compare Input A to previous value else {aBus.writeByteSync(_parCh.addr, BNK1_INTCON_B, 0x00);} //set INTerrupts CONtrol B to compare Input B to previous value } break; case("MCP23008"): // IOCON: bit 7 ignored: bit7 ->x // bit 6 ignored: bit6 ->x // disable address pointer auto-increment: bit5 SEQOP ->1 // enable SDA slew rate control: bit4 DISSLEW ->0 // HAEN only relevant for SPI version of chip: bit3 HAEN ->x // interrupt output as open drain: bit2 ODR ->1 // if ODR, then INTPOL not relevant: bit1 INTPOL ->x (if ODR=0, then active high->1, active low->0) // bit 0 ignored: bit0 ->x // (11)100100 = 0xE4 (two MSBs ignored by MCP23008) aBus.writeByteSync(_parCh.addr, BNK1_IOCON_A, 0xE4);// See:Page-8 TABLE 1-3 MCP23008 datasheet PDF _processState = 2; // Check if need to Turn On ALL pins at start if ((_parCh.startAllHIGH == true) && (_callerNode.lastState = -2)){ if (log2console) console.log(" MCP23008 Now Setting ALL pins to HIGH. A = 1111111 Addr = 0X" + _parCh.addr.toString(16)); aBus.writeByteSync(_parCh.addr, BNK1_OLAT_A, 0xFF); //Set output (A) to 11111111 _parCh.allStates = 0xFF; // 8 bit: (PortA) GPIO 0-7 } if (_parCh.allStates = -1) { ip1 = aBus.readByteSync(_parCh.addr, BNK1_GPIO_A); //read (Bank-A) GPIO pins _parCh.lastTimeRead = performance.now(); _parCh.allStates = ip1; } if (log2console) console.log(" MCP23008 First READ OK; A="+ip1.toString(2).padStart(8,"0")+" allStates=" + _parCh.allStates.toString(2).padStart(8,"0")); // Set Registers // _parCh.isInputs determines which pins are inputs (IODIR = 1) or outputs (IODIR = 0) if (_isInput) {_parCh.isInputs = _parCh.isInputs | (1 << _bitNum); }// input: e.g. if _bitNum = 5, shift 1 left 5, then bitwise or with whatever _parCh.isInputs is makes pin 5 an input else {_parCh.isInputs = _parCh.isInputs & ~(1 << _bitNum); }//output: e.g. if _bitNum = 5, shift 1 left 5, then bitwise and (not bit5) with whatever _parCh.isInputs is makes pin 5 an output aBus.writeByteSync(_parCh.addr, BNK1_IODIR_A, _parCh.isInputs & 0xFF); //update in/out ports(A) if (_isInput) { if (_pullUp) {_parCh.pullUps = _parCh.pullUps | (1 << _bitNum);} else {_parCh.pullUps = _parCh.pullUps & ~(1 << _bitNum);} if (log2console) console.log(" MCP23008 Input pullups=" + _parCh.pullUps); aBus.writeByteSync(_parCh.addr, BNK1_GPPU_A , _parCh.pullUps & 0xFF); //set internal pull-up 100K resistor(A) aBus.writeByteSync(_parCh.addr, BNK1_IPOL_A, 0x00); //disable Input invert(=POLarity) A aBus.writeByteSync(_parCh.addr, BNK1_GPINTEN_A, _parCh.isInputs & 0xFF); //set INTerrupts ENable A aBus.writeByteSync(_parCh.addr, BNK1_INTCON_A, _parCh.isInputs & 0xFF); //set INTerrupts CONtrol } break; // END MCP chip } _processState = 3; aBus.closeSync(); aBus = null; if (log2console) console.log(" "+_parCh.chipType+" Bit-initialization finished. Bus closed."); _parCh.globalState = 1; // means: Working :) _callerNode.lastState = getBit( _parCh.allStates, _bitNum ); // SET LAST STATE 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; _callerNode.error( busStateTexts[_processState] + " failed. Bus=" + _parCh.busNum + " Pin=" + _bitNum + "\n Error:" + err); showStatus( _callerNode, false, _parCh.globalState ); aBus = null; return false; } } /* ### END INITIALIZATION of the Chip ### */ // ********** TIMER ********** // ... for input polling // *************************** // this.startChipTimer = function(_newInterval) { if (log2console) console.log(" "+this.chipType+" startChipTimer = " + _newInterval +" ms"); if ((_newInterval == undefined) || (_newInterval == 0)) { if (log2console) console.log(" "+this.chipType+" Timer interval is UNDEFINED or 0 ! Timer will not be started, old may be cleared. Exiting Timer."); if (mainChipNode.chipTimer) clearInterval(mainChipNode.chipTimer); return null; } if (mainChipNode.chipTimer != null) { // timer is already running if (log2console) console.log(" MCP/PCF Timer is already running"); if (mainChipNode.interval == _newInterval) { if (log2console) 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 be started mainChipNode.interval = _newInterval; mainChipNode.chipTimer = null; if (log2console) console.log(" MCP/PCF Old timer destroyed."); } // STARTING a Timer in repeat mode if (log2console) console.log(" MCP/PCF Starting Timer now..."); mainChipNode.chipTimer = setInterval(mainChipNode.reviewStates, mainChipNode.interval); } this.reviewStates = function(read1x,interrupt) { //used to be called myTimer let _processState = 0; const _chipType = mainChipNode.chipType; const _addr = mainChipNode.addr; if (isNaN(mainChipNode.busNum)) { //<<< ab1do: may not need anymore: NaN is detected at design stage console.error(" MCP/PCF chip reviewStates busNum is undefined. Exiting."); mainChipNode.globalState += 2; return false; } const _readTime = performance.now(); // millisec. To change the Timer value, if too short a period is set. try { if (timerLog && log2console) console.log(" MCP/PCF reviewStates: opening bus... Time: " + new Date( new Date().getTime() ).toISOString().slice(11, -1) ); let _aBus = i2cModule.openSync(mainChipNode.busNum); _processState = 1; let ipAll = -1; let ipA=-1, ipB=-1; switch(_chipType) { case ("PCF8574"): case("PCF8574A"): if (timerLog && log2console) console.log(" PCF8574(A) >> Now reading 8bits. Addr = 0X" + _addr.toString(16)); ipAll = _aBus.receiveByteSync(_addr); if (timerLog && log2console) console.log(" PCF8574(A) Read success ipAll00=" + ipAll.toString(2).padStart(8,"0")); break; /**** Using readWordSync: needs chip address and cmd. Because PCF8575 has no registers to read from, cmd = chip address.*/ case ("PCF8575"): if (timerLog && log2console) console.log(" PCF8575 >> Now reading 16bits. Addr = 0X" + _addr.toString(16)); ipAll = _aBus.readWordSync(_addr,_addr);// LSB followed by MSB if (timerLog && log2console) console.log(" PCF8575 Read success ipAll00=" + ipAll.toString(2).padStart(16,"0")); break; case ("MCP23017"): if (timerLog && log2console) console.log("MCP23017 >> Now reading A+B banks... Typeof _aBUS:" + typeof(_aBus)); ipA = _aBus.readByteSync(_addr, BNK1_GPIO_A); ipB = _aBus.readByteSync(_addr, BNK1_GPIO_B); ipAll = ipA + (ipB << 8); //if (log2console) console.log("MCP23017 Read success ipA00=" + ipA.toString(2).padStart(8,"0") + " ipB00=" + ipB.toString(2).padStart(8,"0") + " ipALL =" + ipAll.toString(2).padStart(16,"0")); if (timerLog && log2console) console.log("MCP23017 Read success ipA00=" + ipA.toString(2).padStart(8,"0") + " ipB00=" + ipB.toString(2).padStart(8,"0") + " ipALL =" + ipAll.toString(2).padStart(16,"0")); break; case ("MCP23008"): if (timerLog && log2console) console.log("MCP23008 >> Now reading 8Bits... Typeof _aBUS:" + typeof(_aBus)); ipAll = _aBus.readByteSync(_addr, BNK1_GPIO_A); if (timerLog && log2console) console.log("MCP23008 Read success ipA00=" + ipA.toString(2).padStart(8,"0") + " ipALL = " + ipAll.toString(2).padStart(8,"0")); break; } _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 && log2console) 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, to see if any of these needs to be updated if (ipAll != mainChipNode.allStates){ let diffWord = ipAll ^ mainChipNode.allStates; // bitwise XOR operator if (log2console) console.log(_chipType + " > Existing States: "+ipAll.toString(2).padStart( mainChipNode.maxBits,"0")); if (log2console) console.log(_chipType + " > New States: "+mainChipNode.allStates.toString(2).padStart(mainChipNode.maxBits,"0")); if (log2console) console.log(_chipType + " >!Change! of an input: Diff Mask= "+diffWord.toString(2).padStart( mainChipNode.maxBits,"0")); 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 (n != null) {// && (mainChipNode.isInputs & (1 << i) == 0)) { // check bit is used and is not an input n.changed(newState, read1x,interrupt); } } } } 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 = 0X" + _addr.toString(16) + "] [mainChipNode.allStates=" + mainChipNode.allStates + "]"); mainChipNode.error(err); 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) { showStatus(n, -2, mainChipNode.globalState); } } } 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.lastTimeRead = performance.now(); //new Date().getTime(); mainChipNode.readLength = mainChipNode.lastTimeRead - _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 (log2console) console.log(" MCP/PCF Closing ... Clearing chipTimer."); 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.diagnostics= _inputConfig.diagnostics this.lastState = -2; this.initOK = false; // check Master-Chip setup let _parentChipNode = RED.nodes.getNode(_inputConfig.chip); this.parentChip = _parentChipNode; let _parCh = node.parentChip; if (!_parentChipNode) { node.error("[MCP230XX + PCF857X] Master-global-Chip not found! Skipping input-node creation."); showStatus(node, true, 0); return null; } log2console = _parCh.logging; if(log2console) console.log(">>>> DEBOUNCE = "+this.debounce+" ms and _parentChipNode.interval = "+_parentChipNode.interval+" ms"); if (log2console) console.log("---"); if (log2console) console.log(">>> Initializing "+_parCh.chipType+" Input node >> bitNum=" + this.bitNum + " pullUp=" + this.pullUp + " invert=" + this.invert + " id=" + this.id ); this.initOK = _parentChipNode.initializeBit (this.bitNum, true, this.pullUp, node);// this.pullUp ignored for PCF chips showStatus(node, this.lastState, _parentChipNode.globalState); // shows uninit (yellow) or error (red) this.on('close', function() { if (node.deB_timer != null){ if (log2console) console.log(" MCP/PCF > clearing old Debounce Input timer... [Pin=" + node.bitNum + "]"); clearTimeout(node.deB_timer); node.deB_timer = null; } }); this.updateState = function(_state, _msg, _interrupt) { if (node.lastState != _state) { if (log2console) console.log(_parCh.chipType + " > Pin " + node.bitNum + " changed from "+node.lastState+" to " + _state + "; id=" + node.id); showStatus(node, _state, _parentChipNode.globalState); // will show inverted status, 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; _msg.interrupt = _interrupt; 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, _interrupt ) { if (node.deB_timer != null){ if (log2console) console.log(" MCP/PCF > clearing old Debounce Input timer... [Pin=" + 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 (log2console) console.log(" MCP/PCF > New input Debounce timer set. TimeEnd=" + node.debounce + " State=" + _state); } else { node.updateState(_state, null, _interrupt); node.deB_state = _state; } } this.deBounceEnd = function(_state){ node.deB_timer = null; if (_parentChipNode.globalState > 3) { if (log2console) console.log(" MCP/PCF > Input timer deBounce CANCELED because chip is in Global-Error-State" ); return false; } let _now = performance.now(); if ((_now - node.lastTimeRead) < node.debounce) { //changed node.debounce*1.2 to node.debounce _state = getBit( _parentChipNode.allStates, node.bitNum ); node.updateState(_state, null, false); if(log2console) console.log("updateState called from this.deBounceEnd"); } else { let _read_success = _parentChipNode.reviewStates(true,false); // forcing to re-read the current state from chip if ( _read_success ) { _state = getBit( _parentChipNode.allStates, node.bitNum ); if (log2console) 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.lastTimeRead) + "ms" ); if (_state == node.deB_state) node.updateState(_state, null, false); } else {node.deB_timer = setTimeout(node.deBounceEnd, node.debounce, _state);} } } this.on('input', function(msg, send, done) { // triggers an immediate read if payload is False or 0 >> to support Interrupts // Changed to active low to comply with PCF active low as well as MCP ODR active low if (!msg.payload) {// interrupt activated when 0 or false let _parCh = node.parentChip; send = node.diagnostics;// true if checked, false if not checked; if (msg.payload===0) {if (log2console) console.log(_parCh.chipType+" > Interrupt detected! Reading Input");} if (msg.payload===false) {if (log2console) console.log(_parCh.chipType+" > Read Now detected: Reading Input");} let readSuccess = _parCh.reviewStates(true,true); // result is True, if succesfully read, false if any error occured during read; sends msg on input change if(send) { //Send a msg with diagnostic data msg.readSuccess = readSuccess; // result is True, if succesfully read, false if any error occured during read msg.readTime = _parCh.readLength; msg.allStates = msg.readSuccess ? _parCh.allStates.toString(2).padStart(_parCh.maxBits,"0") : ""; msg = node.updateState( node.lastState, msg, true ); // << this will add (inverted) .payload node.send(msg); // this msg includes msg.topic = gpio/#, where # is the GPIO port number that interrupt is connected to } 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( " "+_parCh.chipType+" Error while closing an Input-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.legacy = _OutputConfig.legacy; node.lastState = -2; node.initOK = false; // check Master-Chip setup let _parentChipNode = RED.nodes.getNode(_OutputConfig.chip); // hidden Chip-configuration node node.parentChip = _parentChipNode; if (!_parentChipNode) { node.error("Master MCP230XX or PCF857X Chip not set! Skipping node creation. Node.id=" + node.id); showStatus(node, -2, 2); return null; } node.startAllHIGH = _parentChipNode.startAllHIGH; log2console = _parentChipNode.logging; if (log2console) { console.log("---"); console.log(">>> Initializing "+_parentChipNode.chipType+" Output node >> invert=" + node.invert + " pinNum=" + node.bitNum + " ID=" + node.id); } this.initOK = _parentChipNode.initializeBit(node.bitNum, false, false, node); showStatus(node, this.lastState, _parentChipNode.globalState); // shows uninitialized (yellow) or error (red) this.changed = function( _state, _read1x ) { showStatus(node, _state, _parentChipNode.globalState); // will show inverted, if needed node.lastState = _state; } this.setOutput = function(_bitNum, _newState, _callerNode){ let _processState = 0; if ( ! _callerNode) { console.error( _chipType + "setOutput >> _callerNode=null !"); return false; } let _parCh = _callerNode.parentChip; if ( ! _parCh) { _callerNode.error( _chipType + "setOutput >> _callerNode.parentChip=null !"); return false; } const _addr = _parCh.addr; if ( ! _addr) { _callerNode.error( _chipType + "setOutput >> parentChip.addr=null !"); return false; } const _chipType = _parCh.chipType; log2console = _parCh.logging; try { let ip8 = -1; let ip16 = -1; if (log2console) console.log(_chipType +" setOutput "+ _callerNode.id +" > Addr = 0X" + _addr.toString(16) + " PinNum=" + _bitNum + " _newState:" + _newState +" > opening bus..."); let _aBus = i2cModule.openSync(_parCh.busNum); _processState = 1; // Set ALL output pins to 0 or 1-> msg.topic = all, msg.payload = true/false if (_bitNum == -1) { let on_off = _newState? 0xFFFF : 0x0000; switch(_chipType) { case("PCF8574"): case("PCF8574A"): on_off = on_off | _parCh.isInputs; // bitwise OR allStates of chip with isInputs to change only output nodes _aBus.sendByteSync (_addr, on_off & 0xFF); _parCh.reviewStates(true,false); _parCh.lastTimeRead = performance.now(); break; case("PCF8575"): on_off = on_off | _parCh.isInputs; // change only output nodes _aBus.writeWordSync(_addr,on_off & 0xFF,(on_off>>8) & 0xFF);//set pins 0-7 and 8-15; _parCh.reviewStates(true,false); _parCh.lastTimeRead = performance.now(); break; case("MCP23017"): _aBus.writeByteSync(_addr, BNK1_OLAT_A, on_off & 0xFF); //Set output A, has no effect on Pins configured as inputs _aBus.writeByteSync(_addr, BNK1_OLAT_B, on_off & 0xFF); //Set output B, has no effect on Pins configured as inputs _parCh.lastTimeRead = performance.now(); _parCh.allStates = on_off; break; case("MCP23008"): _aBus.writeByteSync(_addr, BNK1_OLAT_A, on_off & 0xFF); //Set output, has no effect on Pins configured as inputs _parCh.lastTimeRead = performance.now(); _parCh.allStates = on_off & 0xFF; break; } 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 (log2console) {console.log(_chipType+" isInputs = "+_parCh.isInputs.toString(2).padStart(16,"0"))} if (n != null && getBit(_parCh.isInputs,i)==0) { //ONLY UPDATE STATES of OUTPUT NODES showStatus( n, _newState, _parCh.globalState); n.lastState = _newState; } } } } // Set only one pin to: 0 or 1 -> (msg.topic = any AND msg.pin = pin#) OR (msg.topic != all AND != any); msg.payload = true/false else { // first read the current state of LSB or Bank A (takes 4ms) if (_bitNum < 8) { switch(_chipType){ case("PCF8574"): case("PCF8574A"): ip8 = _aBus.receiveByteSync(_addr); break; case("MCP23008"): case("MCP23017"): ip8 = _aBus.readByteSync(_addr, BNK1_GPIO_A);//MCP23017: LSB break; case("PCF8575"): ip16 =_aBus.readWordSync(_addr,_addr); break; //PCF8575 always reads 2 bytes (=1 word) so only ip16 is relevant } if (_chipType != "PCF8575") {ip16 = ip8;}//PCF8575 ip16 already completely known; MCP23017 ip8 is LSB of ip16