193 lines
7.3 KiB
JavaScript
193 lines
7.3 KiB
JavaScript
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)
|
|
// })
|
|
}
|