uci-interrupt/src/interrupt.js

192 lines
7.8 KiB
JavaScript

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 = {}) {
if (typeof pin !=='number') pin = parseInt(pin) // make sure pin is a number!
// opts.conPacket = (opts.resetCmd && !opts.conPacket) ? { cmd: opts.resetCmd, pin: pin } : opts.conPacket // will use either option
// log.debug({conPacket: opts.conPacket, msg:'connection packet for consumers'})
// if (opts.path || opts.itrn) {
// opts.itrn = opts.itrn || {}
// if (opts.path && typeof opts.path !=='boolean') opts.path = opts.path + ':' + pin
// if (typeof opts.path ==='boolean') opts.path = ''
// opts.itrn.path = opts.itrn.path || opts.path || 'interrupt:' + pin
// opts.itrn.conPacket = opts.conPacket
// opts.sockets = (opts.sockets ? opts.sockets + ',' : '') + 'itrn#s>n'
// }
// if (opts.topic || opts.itrm) {
// opts.itrm = opts.itrm || {}
// opts.itrm.topics = opts.itrm.topic || opts.topic +'/'+ pin || 'interrupt/' + pin
// opts.sockets = (opts.sockets ? opts.sockets + ',' : '') + 'itrm#s>m'
// }
//
// if (opts.itrw || opts.wport || opts.wport===0 ) {
// opts.itrw = opts.itrw || {}
// if (opts.wport) opts.wport = opts.wport + +pin
// opts.itrw.port = opts.itrw.port || opts.wport || 9100 + +pin
// opts.sockets = (opts.sockets ? opts.sockets + ',' : '') + 'itrw#s>w'
// }
// // default is a tcp socket server at 9000+pin
// // if (opts.itrt || opts.port || opts.port===0 || !opts.sockets ) {
// if (opts.itrt || opts.port || opts.port===0 ) {
// opts.itrt = opts.itrt || {}
// if (opts.port) opts.port = opts.port + +pin
// opts.itrt.port = opts.itrt.port || opts.port || 9000 + +pin
// opts.itrt.conPacket = opts.conPacket
// opts.sockets = (opts.sockets ? opts.sockets + ',' : '') + 'itrt#s>t'
// }
super(opts)
this.id = (opts.id || 'interrupt') + ':' + pin
log.info({ pins: pin, opts: opts }, 'created interrupt with these opts')
this.pin_num = pin
this.resetCmd = opts.resetCmd || 'interrupt.reset'
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 = false // true is interrupt is ready
this.pin = {}
this.hook = opts.hook
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 = 0
this._hookFunc = defaultHook
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() {
await super.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 })
console.log('initial connect reset sent',await this.reset())
DeadJim( (signal,err) => {
log.warn({signal:signal, error:err, msg:'Interrupt was killed'})
this.pin.unwatchAll()
this.pin.unexport() // kill the kernel entry
})
log.debug({msg:'new interrupt pin created and watching', num:this.pin_num, status:await this.status() ? 'ready' : 'not ready', pin:this.pin, edge:this.edge,debounce:this.wait})
log.debug('setting reconnect listener')
this.socket.mcp.on('reconnected', () => console.log('test listen reconnected'))
this.consumersListen('reconnected', () => {
log.debug('reconnected to a socket send a reset request')
this.reset()
})
this.socket.mcp.emit('reconnected')
if (this.resetInterval) setInterval(this.commands.reset,this.resetInterval)
this.pin.watch( function (err,value) {
log.debug('sbc interrupt tripped, value:', value, 'error:', err)
this.count +=1
this._interruptProcess(value,err)
}.bind(this))
} // end init
// manual firing for testing
async fire(packet={}) {
log.info({msg:`mock manually firing interrupt for pin ${this.pin_num}`})
await this._interruptProcess(1)
packet.status = 'fired'
packet.ipin = 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()
this.ready = this.pull==='down' ? !status : !!status
packet.pin = this.pin_num
packet.cmd = 'reply'
packet.ready = this.ready
return packet
}
async reset(packet={}) {
if (!(await this.status()).ready) {
packet.cmd = this.resetCmd
packet.pin =this.pin_num
this.emit(this.resetCmd) // emit locally
await this.send(packet)
await this.push(packet)
log.error({msg: `interrupt was forced reset. ready now? ${(await this.status()).ready}`})
return {cmd:'reply', reset:true, ready: (await this.status()).ready}
}
return {cmd:'reply', reset:false, ready:true}
}
// use hook to do more processing
async _interruptProcess(value,err) {
let packet = Object.assign({},this.packet)
packet.error = err
packet.state = value
packet.count = this.count
packet.timeStamp = Date.now()
packet.dateTime = new Date().toString()
if (this.hook) packet = await this._hookFunc.call(this,packet)
log.debug({packet: packet, msg:'interrupt tripped, emit/send/push packet to all connected/listening'})
this.emit('interrupt',packet) // emit locally
this.send(packet) // will send a packet via client to any socket
this.push(packet) // will push on any socket
}
registerHook(func) {
this._hookFunc = func
}
} // 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('==========default hook =============')
console.log(`pin ${packet.pin} on sbc gpio bus has thrown an interrupt`)
console.log(`emitting/sending/pushing to all connected socket client with cmd:${packet.cmd}`)
console.dir(packet)
console.log('replace by a new function with .registerHook(function) to overwrite this')
console.log('Must be async/promise returning if anything async happens in your hook')
console.log('This hook allows you to modify/add to the packet being pushed to connected clients')
console.log('the function will be bound to the instance for complete access')
console.log('if you pass a hash for .hook you can use it here as this.hook')
console.log('the hook options contains', this.hook)
console.log('by default the instance id will be attached to the packet before this')
return packet
// resolve(packet)
// })
}