import Base from '@uci/base' import EventEmitter from 'events' import DeadJim from 'death' import { Gpio } from 'onoff' import logger from '@uci-utils/logger' let log = logger({package:'@uci/interrupt', file:'/src/interrupt.js'}) function ExtendsInterrupt (pin,opts= {}) { if (!new.target) { throw new Error('Uncaught TypeError: Class constructor Interrupt cannot be invoked without \'new\'') } if (isNaN(pin)) { throw new Error('supplied pin argument is not a number') } const SuperClass = opts.multiple ? EventEmitter : Base // this always emits local events but will also send if any consumers are created/online class Interrupt extends SuperClass { constructor(pin, opts = {}) { pin = Number(pin) // make sure pin is a number! super(opts) this.id = opts.name || (opts.id || 'interrupt') + ':' + pin log.debug({ pins: pin, opts: opts, method:'constructor', line:16, msg:'created interrupt with these opts'}) this.pin_num = pin this.resetCmd = opts.resetCmd || 'interrupt.reset' this.resetInterval = opts.resetInterval this.mock = opts.mock || process.env.MOCK this.wait = opts.wait || 0 // debounce is off by default // https://github.com/fivdi/onoff#gpiogpio-direction--edge--options this.edge = opts.edge || 'rising' // falling,both,none=no interrupt // pull down/up (down is default) can't be set here // it is done by in DTOs or in RPI in config.txt or via an external pullup/down resistor // this setting is only needed to monitor the state of the ready interrupt and should match what is set in hardware this.pull = opts.pull || 'down' this.pin = {} this.count = 0 this.packet = opts.packet || {} this.packet.id = this.id this.packet.pin = this.pin_num this.packet.cmd = this.packet.cmd || opts.cmd || opts.interruptCmd || 'interrupt' this.packet.count = this.count this.multiple = opts.multiple // multiple means that this is one of multiple interrupt pins thus it only emits, no direct socket communication if (opts.multiple) { this.send = ()=>{} // if not using uci-base then ignore the send and push methods this.push = ()=>{} } else { this.amendConsumerCommands({reset:this.reset.bind(this)}) this.amendSocketCommands({ reset:this.reset.bind(this), status:this.status.bind(this), fire:this.fire.bind(this), intervalReset:this.intervalReset.bind(this) }) } } // end construtor async init() { this.count = 0 // TODO devel mock versions for testing on other than sbc with gpios this.pin = new Gpio(this.pin_num, 'in', this.edge, { debounceTimeout:this.wait }) DeadJim( (signal,err) => { log.warn({signal:signal, method:'init', line:56, error:err, msg:'Interrupt process was killed, remove watchers, unexport'}) clearInterval(this._intervalReset) this.pin.unwatchAll() this.pin.unexport() // kill the kernel entry }) this.pin.watch( function (err,value) { log.debug('interrupt tripped, value:', value, 'error:', err) this.count +=1 this._interruptProcess(value,err) }.bind(this)) await this.intervalReset(this.resetInterval) const state = await this.status() this.emit('log',{level:'interrupt', msg:`new interrupt pin ${this.pin_num} created and watching`, state:state, edge:this.edge,debounce:this.wait}) if (super.init) super.init() // only call if superclass has init (i.e. uci base) } // end init // manual firing for testing async fire(packet={}) { log.debug({method:'fire', line:82, msg:`mock manually firing interrupt for pin ${this.pin_num}`}) await this._interruptProcess(1) packet.status = 'fired' packet.pin = this.pin_num return packet } // returns true if pin is ready and waiting to trigger interrupt async status(packet) { let state = await this.pin.read() let ready = this.edge==='both' ? true : this.pull==='down' ? !state : !!state // ready is always true for 'both' this.emit('ready',ready) if (packet) { packet.pin = this.pin_num packet.state = state if (this.edge !=='both') packet.ready = ready return packet } return ready } async intervalReset(packet) { let interval = typeof packet === 'number'? packet : (packet || {}).interval if (!interval || interval<=0) { clearInterval(this._intervalReset) this._intervalReset = null } else { this._intervalReset = setInterval(this.reset.bind(this), interval*1000) } return this._intervalReset ? true : false } // remote reset async reset(packet={}) { let res = {} this.emit('log',{level:'info', msg:`interrupt reset request for pin ${this.pin_num}`}) if (this.edge ==='both') res = {level:'info', reset:false, ready:true, msg:'interrupt triggered on rising and falling, no reset action needed'} else { if(!await this.status()) { delete packet._header this.emit('reset',packet) // emit locally, then instance can listen and take custome action (e.g. send reset to mcp) packet.cmd = this.resetCmd let state = await this.status() res = {level:state?'debug':'error', msg:`attempted interrupt reset of pin ${this.pin_num} ${state ? 'succeeded' : 'failed'}`, reset:true, ready:state} } else res = {level:'info', reset:false, ready:true, msg:`pin ${this.pin_num} interrupt was ready, no action taken`} } this.emit('log',res) return res } // creates an interrupt event packet and emits/sends it // use hook to do more processing async _interruptProcess(value,err) { let packet = Object.assign({},this.packet) packet.id = this.id packet.pin = this.pin_num packet.error = err packet.state = value // state of gpio pin packet.count = this.count packet.timeStamp = Date.now() packet.dateTime = new Date().toString() if (this._hookFunc) packet = await this._hookFunc.call(this,packet) this.emit('log',{level:'interrupt', packet: packet, msg:`interrupt tripped for pin ${this.pin_num}`}) this.emit('interrupt',packet) // emit locally this.push(packet) // no need to await reply, push to any connected consumer by default } // replace default processor function arguments are value of pin and any error registerProcessor(func) { this._interruptProcess = func } // hook into default interrupt processor for custom processing including packet modification registerHook(func) { if (func) this._hookFunc = func else this._hookFunc=defaultHook } } // end Class return new Interrupt(pin,opts) } export default ExtendsInterrupt export { ExtendsInterrupt as Interrupt } //default hook async function defaultHook(packet) { // return a promise or use await if anything async happens in hook // new Promise((resolve) => { console.log('==========example hook fucntion =============') console.log(`pin ${packet.pin} on sbc gpio bus has thrown an interrupt`) console.log('can change anything in the packet in this hook') console.log('to replace this use .registerHook(function)') console.log('============================') return packet // resolve(packet) // }) }