import Base from '@uci/base' import DeadJim from 'death' import { Gpio } from 'onoff' import logger from '@uci-utils/logger' let log = logger({package:'@uci/interrupt', file:'/src/interrupt.js'}) // a pin makes a socket (server/listner) for each pin to which a consumer can be connected // if opts .port/.path/.topic/.wport are base number/name to which pin number is added/appended (default being 9000,9100,'interrupt') // for specific port number pass itrt.port,itrn.path,itrm.topic,itrw.port which override it if present // conPacket is for connecting consumers. This will send this conPacket command on connect, which may needed to initilize something on related hardware class Interrupt extends Base { 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.resetEnabled = opts.reset|| opts. resetEnabled this.resetInterval = opts.resetInterval * 1000 // sets an interval timeout to check on status and send/emit reset command 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 // this is only used to monitor the status of the interrupt this.pull = opts.pull || 'down' this.__ready = this.edge === 'both' ? true : false // true is interrupt is ready 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.commands = { fire:this.fire.bind(this), status:this.status.bind(this), reset: this.reset.bind(this) } this.addNamespace('commands', 's') // give access to these commands above if a socket/server is created } // end constructor 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 }) // if (this.resetEnabled) log.debug({msg:'initial connect interrupt reset packet sent', ready:await this.reset(), method:'init', line:53}) // if (this.resetInterval && this.resetEnabled) setInterval(this.reset.bind(this),this.resetInterval) DeadJim( (signal,err) => { log.warn({signal:signal, method:'init', line:56, error:err, msg:'Interrupt process was killed, remove watchers, unexport'}) 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)) log.info({msg:'new interrupt pin created and watching', method:'init', line: 62, pin_num:this.pin_num, state:await this.status(), ready:this.__ready, edge:this.edge,debounce:this.wait}) } // 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 packet.cmd = 'reply' return packet } // returns true if pin is ready and waiting to trigger interrupt async status(packet) { let status = await this.pin.read() if (this.edge !=='both') this.__ready = this.pull==='down' ? !status : !!status // ready is always true for 'both' if (packet) { packet.pin = this.pin_num packet.state = status if (this.edge !=='both') packet.ready = this.__ready packet.cmd = 'reply' return packet } return status } async reset(packet) { let res = {} if (this.edge !=='both' && this.resetEnabled) { if (!this.__ready) { let reset = Object.assign({},this.packet) reset.cmd = this.resetCmd this.emit(this.resetCmd,reset) // emit locally await this.send(reset) await this.status() log.error({msg: `interrupt was forced reset. ready now? ${this.__ready}`}) res = {cmd:'reply', msg:`attempted interrupt reset ${this.__ready? 'succeeded' : 'failed'}`, reset:true, ready:this.__ready} } else res = {cmd:'reply', reset:false, ready:true, msg:'interrupt was ready, no action taken'} } else res = {cmd:'reply', reset:false, ready:true, msg:'reset NA or disabled'} if (packet) return Object.assign(packet,res) return this.__ready } // 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 packet.count = this.count packet.timeStamp = Date.now() packet.dateTime = new Date().toString() if (this._hookFunc) packet = await this._hookFunc.call(this,packet) log.debug({packet: packet, msg:'interrupt tripped, emit/send packet to all connected/listening'}) this.emit('interrupt',packet) // emit locally this.send(packet) // will send packet via client to any connected socket } // 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 export default 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) // }) }