UNPKG

raspi-pcf8574

Version:

Control each pin of a PCF8574/PCF8574A I2C port expander IC.

365 lines (307 loc) 13 kB
/* * Node.js PCF8574/PCF8574A * * Copyright (c) 2017 Peter Müller <peter@crycode.de> (https://crycode.de) * * Node.js module for controlling each pin of a PCF8574/PCF8574A I2C port expander IC. */ import {EventEmitter} from 'events'; import * as Promise from 'bluebird'; import {I2cBus} from 'i2c-bus'; import {Gpio} from 'onoff'; /** * Class for handling a PCF8574/PCF8574A IC. */ export class PCF8574 extends EventEmitter { /** Constant for undefined pin direction (unused pin). */ public static readonly DIR_UNDEF = -1; /** Constant for input pin direction. */ public static readonly DIR_IN = 1; /** Constant for output pin direction. */ public static readonly DIR_OUT = 0; /** Object containing all GPIOs used by any PCF8574 instance. */ private static _allInstancesUsedGpios = {}; /** The instance of the i2c-bus, which is used for the I2C communication. */ private _i2cBus:I2cBus; /** The address of the PCF8574/PCF8574A IC. */ private _address:number; /** Direction of each pin. By default all pin directions are undefined. */ private _directions:Array<number> = [ PCF8574.DIR_UNDEF, PCF8574.DIR_UNDEF, PCF8574.DIR_UNDEF, PCF8574.DIR_UNDEF, PCF8574.DIR_UNDEF, PCF8574.DIR_UNDEF, PCF8574.DIR_UNDEF, PCF8574.DIR_UNDEF ]; /** Bitmask for all input pins. Used to set all input pins to high on the PCF8574/PCF8574A IC. */ private _inputPinBitmask:number = 0; /** Bitmask for inverted pins. */ private _inverted:number; /** Bitmask representing the current state of the pins. */ private _currentState:number; /** Flag if we are currently polling changes from the PCF8574/PCF8574A IC. */ private _currentlyPolling:boolean = false; /** Instance of the used GPIO to detect interrupts, or null if no interrupt is used. */ private _gpio:Gpio = null; /** * Constructor for a new PCF8574/PCF8574A instance. * If you use this IC with one or more input pins, you have to call ... * a) enableInterrupt(gpioPin) to detect interrupts from the IC using a GPIO pin, or * b) doPoll() frequently enough to detect input changes with manually polling. * @param {I2cBus} i2cBus Instance of an opened i2c-bus. * @param {number} address The address of the PCF8574/PCF8574A IC. * @param {boolean|number} initialState The initial state of the pins of this IC. You can set a bitmask to define each pin seprately, or use true/false for all pins at once. */ constructor(i2cBus:I2cBus, address:number, initialState:boolean|number){ super(); // bind the _handleInterrupt method strictly to this instance this._handleInterrupt = this._handleInterrupt.bind(this); this._i2cBus = i2cBus; if(address < 0 || address > 255){ throw new Error('Address out of range'); } this._address = address; // nothing inverted by default this._inverted = 0; if(initialState === true){ initialState = 255; }else if(initialState === false){ initialState = 0; }else if(typeof(initialState) !== 'number' || initialState < 0 || initialState > 255){ throw new Error('InitalState bitmask out of range'); } // save the inital state as current sate and write it to the IC this._currentState = initialState; this._i2cBus.sendByteSync(this._address, this._currentState); } /** * Enable the interrupt detection on the specified GPIO pin. * You can use one GPIO pin for multiple instances of the PCF8574 class. * @param {number} gpioPin BCM number of the pin, which will be used for the interrupts from the PCF8574/8574A IC. */ public enableInterrupt(gpioPin:number):void{ if(PCF8574._allInstancesUsedGpios[gpioPin] != null){ // use already initalized GPIO this._gpio = PCF8574._allInstancesUsedGpios[gpioPin]; this._gpio['pcf8574UseCount']++; }else{ // init the GPIO as input with falling edge, // because the PCF8574/PCF8574A will lower the interrupt line on changes this._gpio = new Gpio(gpioPin, 'in', 'falling'); this._gpio['pcf8574UseCount'] = 1; } this._gpio.watch(this._handleInterrupt); } /** * Internal function to handle a GPIO interrupt. */ private _handleInterrupt():void{ // poll the current state and ignore any rejected promise this._poll().catch(()=>{ }); } /** * Disable the interrupt detection. * This will unexport the interrupt GPIO, if it is not used by an other instance of this class. */ public disableInterrupt():void{ // release the used GPIO if(this._gpio !== null){ // remove the interrupt handling this._gpio.unwatch(this._handleInterrupt); // decrease the use count of the GPIO and unexport it if not used anymore this._gpio['pcf8574UseCount']--; if(this._gpio['pcf8574UseCount'] === 0){ this._gpio.unexport(); } this._gpio = null; } } /** * Helper function to set/clear one bit in a bitmask. * @param {number} current The current bitmask. * @param {PCF8574.PinNumber} pin The bit-number in the bitmask. * @param {boolean} value The new value for the bit. (true=set, false=clear) * @return {number} The new (modified) bitmask. */ private _setStatePin(current:number, pin:PCF8574.PinNumber, value:boolean):number{ if(value){ // set the bit return current | 1 << pin; }else{ // clear the bit return current & ~(1 << pin); } } /** * Write the current stateto the IC. * @param {number} newState (optional) The new state which will be set. If omitted the current state will be used. * @return {Promise} Promise which gets resolved when the state is written to the IC, or rejected in case of an error. */ private _setNewState(newState?:number):Promise<{}>{ return new Promise((resolve:()=>void, reject:(err:Error)=>void)=>{ if(typeof(newState) === 'number'){ this._currentState = newState; } // repect inverted with bitmask using XOR let newIcState = this._currentState ^ this._inverted; // set all input pins to high newIcState = newIcState | this._inputPinBitmask; this._i2cBus.sendByte(this._address, newIcState, (err:Error)=>{ if(err){ reject(err); }else{ resolve(); } }); }); } /** * Manually poll changed inputs from the PCF8574/PCF8574A IC. * If a change on an input is detected, an "input" Event will be emitted with a data object containing the "pin" and the new "value". * This have to be called frequently enough if you don't use a GPIO for interrupt detection. * If you poll again before the last poll was completed, the promise will be rejected with an error. * @return {Promise} */ public doPoll():Promise<{}>{ return this._poll(); } /** * Internal function to poll the changes from the PCF8574/PCF8574A IC. * If a change on an input is detected, an "input" Event will be emitted with a data object containing the "pin" and the new "value". * This is called if an interrupt occured, or if doPoll() is called manually. * Additionally this is called if a new input is defined to read the current state of this pin. * @param {PCF8574.PinNumber} noEmit (optional) Pin number of a pin which should not trigger an event. (used for getting the current state while defining a pin as input) * @return {Promise} */ private _poll(noEmit?:PCF8574.PinNumber):Promise<{}>{ if(this._currentlyPolling){ return Promise.reject('An other poll is in progress'); } this._currentlyPolling = true; return new Promise((resolve:()=>void, reject:(err:Error)=>void)=>{ // read from the IC this._i2cBus.receiveByte(this._address,(err:Error, readState:number)=>{ this._currentlyPolling = false; if(err){ reject(err); return; } // repect inverted with bitmask using XOR readState = readState ^ this._inverted; // check each input for changes for(let pin = 0; pin < 8; pin++){ if(this._directions[pin] !== PCF8574.DIR_IN){ continue; // isn't an input pin } if((this._currentState>>pin) % 2 !== (readState>>pin) % 2){ // pin changed let value:boolean = ((readState>>pin) % 2 !== 0); this._currentState = this._setStatePin(this._currentState, <PCF8574.PinNumber>pin, value); if(noEmit !== pin){ this.emit('input', <PCF8574.InputData>{pin: pin, value: value}); } } } resolve(); }); }); } /** * Define a pin as an output. * This marks the pin to be used as an output pin. * @param {PCF8574.PinNumber} pin The pin number. (0 to 7) * @param {boolean} inverted true if this pin should be handled inverted (true=low, false=high) * @param {boolean} initialValue (optional) The initial value of this pin, which will be set immediatly. * @return {Promise} */ public outputPin(pin:PCF8574.PinNumber, inverted:boolean, initialValue?:boolean):Promise<{}>{ if(pin < 0 || pin > 7){ return Promise.reject(new Error('Pin out of range')); } this._inverted = this._setStatePin(this._inverted, pin, inverted); this._inputPinBitmask = this._setStatePin(this._inputPinBitmask, pin, false); this._directions[pin] = PCF8574.DIR_OUT; // set the initial value only if it is defined, otherwise keep the last value (probably from the initial state) if(typeof(initialValue) === 'undefined'){ return Promise.resolve(null); }else{ return this._setPinInternal(pin, initialValue); } } /** * Define a pin as an input. * This marks the pin for input processing and activates the high level on this pin. * @param {PCF8574.PinNumber} pin The pin number. (0 to 7) * @param {boolean} inverted true if this pin should be handled inverted (high=false, low=true) * @return {Promise} */ public inputPin(pin:PCF8574.PinNumber, inverted:boolean):Promise<{}>{ if(pin < 0 || pin > 7){ return Promise.reject(new Error('Pin out of range')); } this._inverted = this._setStatePin(this._inverted, pin, inverted); this._inputPinBitmask = this._setStatePin(this._inputPinBitmask, pin, true); this._directions[pin] = PCF8574.DIR_IN; // call _setNewState() to activate the high level on the input pin ... return this._setNewState() // ... and then poll all current inputs with noEmit on this pin to suspress the event .then(()=>{ return this._poll(pin); }); } /** * Set the value of an output pin. * If no value is given, the pin will be toggled. * @param {PCF8574.PinNumber} pin The pin number. (0 to 7) * @param {boolean} value The new value for this pin. * @return {Promise} */ public setPin(pin:PCF8574.PinNumber, value?:boolean):Promise<{}>{ if(pin < 0 || pin > 7){ return Promise.reject(new Error('Pin out of range')); } if(this._directions[pin] !== PCF8574.DIR_OUT){ return Promise.reject(new Error('Pin is not defined as output')); } if(typeof(value) == 'undefined'){ // set value dependend on current state to toggle value = !((this._currentState>>pin) % 2 !== 0) } return this._setPinInternal(pin, value); } /** * Internal function to set the state of a pin, regardless its direction. * @param {PCF8574.PinNumber} pin The pin number. (0 to 7) * @param {boolean} value The new value. * @return {Promise} */ private _setPinInternal(pin:PCF8574.PinNumber, value:boolean):Promise<{}>{ let newState:number = this._setStatePin(this._currentState, pin, value); return this._setNewState(newState); } /** * Set the given value to all output pins. * @param {boolean} value The new value for all output pins. * @return {Promise} */ private setAllPins(value:boolean):Promise<{}>{ let newState:number = this._currentState; for(let pin = 0; pin < 8; pin++){ if(this._directions[pin] !== PCF8574.DIR_OUT){ continue; // isn't an output pin } newState = this._setStatePin(newState, <PCF8574.PinNumber>pin, value); } return this._setNewState(newState); } /** * Returns the current value of a pin. * This returns the last saved value, not the value currently returned by the PCF8574/PCF9574A IC. * To get the current value call doPoll() first, if you're not using interrupts. * @param {PCF8574.PinNumber} pin The pin number. (0 to 7) * @return {boolean} The current value. */ public getPinValue(pin:PCF8574.PinNumber):boolean{ if(pin < 0 || pin > 7){ return false; } return ((this._currentState>>pin) % 2 !== 0) } }