0.3.1 mcp230xxi rework

added default consumer socket to interrupt process, named pipe by default
 added commands.interrupt to consumer namespace so pushed packets from interrupt process will be handled.
interrupt process could still 'send' the packet if that is prefered but command would be prefaced with 'interrupt.'
Fixed the interrupt:reset ready observer to it will get the interrupt ready state correctly.
ready state after reset is sent in outgoing packet so action can be taken if something goes wrong.
creates an iterrupt name for observers based on passed opts.interrupt.name or 'interrupt' by default.
Improved switches example that uses options read from yaml file.  Desgined to work with multi intererupt example and i2cbus example.
master
David Kebler 2020-03-24 14:50:50 -07:00
parent 890c130f5f
commit 5c57e614e9
3 changed files with 172 additions and 80 deletions

View File

@ -1,64 +1,133 @@
/* import { readFile as read } from 'fs-read-data'
* import onDeath from 'ondeath'
*
*/
import { MCP230XXi } from '../src' import { MCP230XXi } from '../src'
const HOST = process.env.BUS_HOST let options = {}
const PATH = process.env.BUS_PATH
const PORT = process.env.BUS_PORT || 1776
const ADDRESS = process.env.DEVICE_ADDR || 0x26
const CHIP17 = !!process.env.CHIP17 || true
const LISTEN_PORT = process.env.PORT || 9001
let switches = new MCP230XXi([9,10],{id:'switches', chip17:CHIP17, path:PATH, host:HOST, port:PORT, address:ADDRESS, iport:LISTEN_PORT}) ;
// switches.registerSocket('interrupt','c','t',{host:'switchesd.local', port:1777})
;
(async () => { (async () => {
if (process.env.VERBOSE==='true') { options = await read('./examples/switches.yaml')
switches.on('log', async log => { options.id = options.id || options.name || 'switches'
if (log.level!=='trace') {
console.log( // console.log('----------------switches start options--------\n',options,'\n---------------------------------')
'LOG:',log.level,':', // console.log('----------------switches interrupt options--------\n',options.interrupt,'\n---------------------------------')
log.msg,
'socket:',log.socketName, let switches = new MCP230XXi(options)
// TODO make this part of UCI logger
if ((process.env.UCI_ENV || '').includes('dev')) {
switches.on('log',ev => {
switch (ev.level) {
// case 'warning':
// case 'error':
// case 'testing':
// case 'ready':
// case 'state':
case 'fatal':
// case 'mcp':
// case 'info':
// case 'debug':
// case 'interrupt':
console.log(ev.level.toUpperCase(),'\n',ev)
break
}
})
process.once('SIGUSR2', async () => {
console.log('shutting down nodemon')
await shutdown.call(switches)
process.kill(process.pid, 'SIGUSR2')
})
} // dev
else {
onDeath( async () => {
console.log('\nswitches plugin device, \nHe\'s dead Jim')
await shutdown.call(switches)
console.log('shutdown done')
})
}
// uncomment to see connection events
// switches.on('connection:socket', async ev => {
// console.log('connection event: outbound > ', ev.state,'to socket',ev.socketName)// if (ev.state ==='connected') switches.duplex = true
// })
// switches.on('connection:consumer', async ev => {
// console.log('connection event: inbound >', ev.state,'from consumer',ev.name, ev.data)// if (ev.state ==='connected') switches.duplex = true
// })
if (options.interrupt) {
console.log('interrupt processor registered')
switches.registerInterruptProcessor(iprocessor) // register interrupt processor from below
}
switches.amendConsumerCommands(
{
reply: async function (packet) { // silence end point send command to bus
if (!packet.error) {
// console.log('reply processor', packet)
// if (packet._header.path.includes('i2c-bus')) {
// if (packet.args.cmd ===24 || packet.args.cmd===8) console.log('=== interrupt reset at i2c bus===')
// } else console.log('reply back from master socket', packet)
} else console.log('processing error at socket', packet)
}
}
) )
if (log.packet) console.dir(log.packet)
await switches.init()
console.log('after init',switches.ready.observers,switches.ready.state)
switches.ready.all.subscribe(
function (ready) {
if (!ready) {
console.log(options.name, 'YIKES! some observer is still not reporting ready')
let failed = this.ready.state.filter(obs=> obs[1]===false).map(obs=>[obs[0],this.ready.getObserverDetails(obs)])
console.log('those that have failed\n',failed)
// notifiy here
} else {
console.log(options.name, ': This switches Hardware is...ONLINE!!!!')
// console.log('state\n,',this.ready.state)
// here is where to continue with startup /circuits reset
} }
}) }.bind(switches))
switches.on('connection:socket', async ev => { // setInterval(function () {
console.log('connection event: outbound > ', ev.state,'to socket',ev.socketName)// if (ev.state ==='connected') switches.duplex = true // let failed = this.ready.state.filter(obs=> obs[1]===false).map(obs=>[obs[0],this.ready.getObserverDetails(obs)])
}) // console.log('those that have failed\n',failed)
// }.bind(switches),5000)
switches.on('connection:consumer', async ev => { // pins are outputs by default or all interrupt pulldown if interrupt(s) being used.
// console.log(ev) // set customized pin configurations here or later via master controller and database
console.log('connection event: inbound >', ev.state,'from consumer',ev.name)// if (ev.state ==='connected') switches.duplex = true
})
}
console.log('ready observers for switches', switches.ready.observerNames)
switches.ready.addObserverDetails('bus',{msg:'this is some details related to bus observer'})
switches.ready.subscribe(x=>{
console.log('=====================switches ready================?',x ? 'YES':'NO')
console.log('what has failed: ',switches.ready.failure,' details:', switches.ready.details.get(switches.ready.failure)||'none')
})
// this.ready.subscribe('interrupt:connected',(res)=>console.log('interrupt connected............',res))
// this.ready.subscribe('mcp',(res)=>console.log('mcp............',res))
// this.ready.subscribe('interrupt:reset',(res)=>console.log('interrupt reset............',res))
let res = await switches.socketsInit()
if (res.errors) console.log(res)
})().catch(err => { })().catch(err => {
console.error('FATAL: UNABLE TO START SYSTEM!\n',err) console.log('FATAL: UNABLE TO START switches - KILLING FOR OS RESTART !\n',err, options)
process.exitCode = 1
process.kill(process.pid, 'SIGINT')
}) })
function iprocessor (packet) {
packet.hardware = options.id // id needs to be the _id of the switches device in the database
packet.cmd = 'switches.triggered'
console.log('interrupt processor, could send on to another controller process')
console.dir(packet)
}
async function shutdown (){
let names = Object.keys(this.getSocket())
console.log(names)
for (let name of names) {
console.log(name)
console.log(await this.removeSocket(name))
console.log('after remove', name)
}
this.removeAllListeners()
}

View File

@ -1,17 +1,19 @@
{ {
"name": "@uci/mcp", "name": "@uci/mcp",
"main": "src", "main": "src",
"version": "0.3.0", "version": "0.3.1",
"description": "Classes and Helper Functions for using the MCP chip on I2C Bus", "description": "Classes and Helper Functions for using the MCP chip on I2C Bus",
"scripts": { "scripts": {
"outputs": "./node_modules/.bin/nodemon -r esm --preserve-symlinks examples/outputs", "outputs": "./node_modules/.bin/nodemon -r esm --preserve-symlinks examples/outputs",
"switches": "./node_modules/.bin/nodemon -r esm --preserve-symlinks examples/switches", "switches": "node -r esm -preserve-symlinks examples/switches",
"switches:dev": "UCI_ENV=dev ESM_DISABLE_CACHE=true ./node_modules/.bin/nodemon -r esm --preserve-symlinks examples/switches",
"swrl": "UCI_ENV=dev node -r esm examples/mcp-switch-relay", "swrl": "UCI_ENV=dev node -r esm examples/mcp-switch-relay",
"swr": "node -r esm examples/mcp-switch-relay", "swr": "node -r esm examples/mcp-switch-relay",
"test": "./node_modules/.bin/mocha --reporter list --timeout 30000", "test": "./node_modules/.bin/mocha --reporter list --timeout 30000",
"testw": "./node_modules/.bin/mocha --reporter list -- watch --timeout 30000", "testw": "./node_modules/.bin/mocha --reporter list -- watch --timeout 30000",
"sample": "node demo/sample.js", "sample": "node demo/sample.js",
"inter": "sudo node -r esm examples/interrupt" "inter": "sudo node -r esm examples/interrupt",
"yalcu": "./node_modules/.bin/nodemon --watch /home/sysadmin/.yalc/**/*.js --exec /opt/node-global-apps/bin/yalc update"
}, },
"author": "David Kebler", "author": "David Kebler",
"license": "MIT", "license": "MIT",
@ -31,12 +33,14 @@
"dependencies": { "dependencies": {
"@uci-utils/byte": "^0.2.3", "@uci-utils/byte": "^0.2.3",
"@uci-utils/logger": "0.0.16", "@uci-utils/logger": "0.0.16",
"@uci/i2c-device": "^0.3.0" "@uci/i2c-device": "^0.3.1"
}, },
"devDependencies": { "devDependencies": {
"chai": "^4.2.0", "chai": "^4.2.0",
"esm": "^3.2.25", "esm": "^3.2.25",
"mocha": "^7.1.0", "fs-read-data": "^1.0.4",
"nodemon": "^2.0.2" "mocha": "^7.1.1",
"nodemon": "^2.0.2",
"ondeath": "^1.0.0"
} }
} }

View File

@ -1,9 +1,6 @@
import {MCP230XX, map, changed, isPlainObject, to, merge} from './mcp230xx' import {MCP230XX, map, changed, isPlainObject, to, merge} from './mcp230xx'
import { byteFormat } from '@uci-utils/byte' import { byteFormat } from '@uci-utils/byte'
// import Ready from '@uci-utils/ready'
import logger from '@uci-utils/logger'
let log = {}
class MCP230XXi extends MCP230XX { class MCP230XXi extends MCP230XX {
constructor(pins, options={}) { constructor(pins, options={}) {
@ -12,14 +9,27 @@ class MCP230XXi extends MCP230XX {
if (opts.interrupt) delete opts.interrupt.pins // if .interrupt was passed then .pins must be removed if (opts.interrupt) delete opts.interrupt.pins // if .interrupt was passed then .pins must be removed
opts.lport = opts.lport || (opts.interrupt || {}).port || opts.iport opts.lport = opts.lport || (opts.interrupt || {}).port || opts.iport
if (!opts.lport) opts.lpath = opts.lpath || (opts.interrupt || {}).ipath || opts.ipath || 'interrupt' // must has a socket litener for interrupt process if (!opts.lport) opts.lpath = opts.lpath || (opts.interrupt || {}).ipath || opts.ipath || 'interrupt' // must has a socket litener for interrupt process
// a socket option of name `interrupt` MUST be supplied or the default below will be used which assume only a named pipe of `interrupt`
const interrupt = {
name: 'interrupt',
type: 'c',
transport: 'n',
options:
{ name: 'mcpi',
data: { name: 'mcpi', dep: 'interrupt' },
initTimeout: 0,
retryWait: 5,
path: 'interrupt'
}
}
// set the required interrupt consumer to the default options if none supplied
if (!opts.sockets) opts.sockets = [ interrupt ]
else if (!opts.sockets.find(socket=>socket.name==='interrupt')) {
opts.sockets.push(interrupt)
}
super(opts) super(opts)
this.pinsCfg = opts.pinsCfg || this.chip17 ? [{port:'A', pins:'all', cfg:'input_interrupt'},{port:'B', pins:'all', cfg:'input_interrupt'}] : [{pins:'all', cfg:'input_interrupt'}] this.pinsCfg = opts.pinsCfg || this.chip17 ? [{port:'A', pins:'all', cfg:'input_interrupt'},{port:'B', pins:'all', cfg:'input_interrupt'}] : [{pins:'all', cfg:'input_interrupt'}]
log = logger({
file: 'src/mcp230xxi.js',
class: 'MCP230XXi',
name: 'mcp-interrupt',
id: this.id
})
pins.forEach((pin,index) => { pins.forEach((pin,index) => {
this[pin] = opts['i' + pin] || {} this[pin] = opts['i' + pin] || {}
@ -27,28 +37,33 @@ class MCP230XXi extends MCP230XX {
}) })
this.pins = pins this.pins = pins
this.commands.interrupt = this.bindFuncs(icommands) // add interrupt pin commands to base set in "command.js" this.commands.interrupt = this.bindFuncs(icommands) // add interrupt pin commands to base set in "command.js"
this.addNamespace('commands.interrupt', 'c') // by default allow access to interrupt commands via pushed packet (gpio interrupt)
// they will be available via send as well to socket as interrupt.
this._interruptProcess = process.bind(this) // default processor this._interruptProcess = process.bind(this) // default processor
this.iname = (opts.interrupt || {}).name || 'interrupt' // name of interrupt consumer
this.ready.addObserver(`${opts.interrupt.name}:process`) // will emit on received interrupt process ready packet
let interruptobs = this.ready.combineObservers(`${opts.interrupt.name}`,['mcp:configure',this.getSocket(opts.interrupt.name).obsName,`${opts.interrupt.name}:process`]) this.ready.addObserver(`${this.iname}:process`) // will emit on received interrupt process ready packet
// ready to reset interrupt when mcp is ready, the interrupt process is connected and it up
let interruptobs = this.ready.combineObservers(this.iname,['mcp:configure',this.getSocket(this.iname).obsName,`${this.iname}:process`])
this.ready.addObserver('interrupt:reset',interruptobs this.ready.addObserver('interrupt:reset',interruptobs
.pipe( .pipe(
map(async ready => { map(async ready => {
if (ready) { if (ready) {
let imcpready = await this.resetInterrupt() await this.resetInterrupt()
if (imcpready){ const istatus = await this.send(this.iname,{cmd:'status'}) // get the status from the gpio interrupt
let res = await this.send(opts.interrupt.name,{cmd:'status'}) if (istatus.pins) ready = istatus.pins.reduce((acc,pin)=>acc&&(pin.ready!=null?pin.ready :false),true)
ready = res.ready || res.pins.reduce((acc,pin)=>acc&&pin.ready,true) else ready = istatus.ready
} if (!ready) this.emit('log',{level:'fatal', msg:'gpio interrrupts were not initially reset',status:istatus})
} }
return ready return ready
}) //map }) //map
)) ))
let socket = this.getSocket(`${options.name}:${options.interrupt.remote ? 't':'n'}`) // mcp t or n socket was created // get listening socket and watch for incoming connection from interrupt process
this.consumerConnected(socket,{add:true,consumer:options.interrupt.name}) let socket = this.getSocket(`${options.name}:${options.interrupt.remote ? 't':'n'}`) || this.getSocket(options.name)// mcp t or n socket was created
this.consumerConnected(socket,{add:true,consumer:this.iname})
} // end constructor } // end constructor
@ -61,12 +76,10 @@ class MCP230XXi extends MCP230XX {
async resetInterrupt(pins) { async resetInterrupt(pins) {
if (!Array.isArray(pins)) pins = pins ? [pins] : this.pins if (!Array.isArray(pins)) pins = pins ? [pins] : this.pins
this._ireset = (await Promise.all(pins.map(async pin => { return (await Promise.all(pins.map(async pin => {
await this.bus.read(this.getPort(pin) !== 'B' ? 0x08 : 0x18) // 0x08 is intcap interrupt capture register await this.bus.read(this.getPort(pin) !== 'B' ? 0x08 : 0x18) // 0x08 is intcap interrupt capture register
return(await this.interruptState(pin)) return(await this.interruptState(pin))
}) }))).reduce((res,val) => res && val,true)
)).reduce((res,val) => res && val)
return this._ireset
} }
getPort(pin) { getPort(pin) {
@ -112,28 +125,34 @@ const icommands = {
}, },
// finds which mcp pin caused the interrupt // finds which mcp pin caused the interrupt
find: async function(inter) { find: async function(inter) {
let reply = {}
// this.emit('log',{level:'debug', msg:'incoming interrupt', packet:inter})
let ipin = inter.ipin || inter.pin let ipin = inter.ipin || inter.pin
if (!await this.interruptState(ipin)) { // if it's already reset then this is false trip if (!await this.interruptState(ipin)) { // if it's already reset then this is false trip
let res = await this.commands.pin.status({ pins: 'all', reg: 'intf', port:this.getPort(ipin) }) // read port interrupt status const res = await this.commands.pin.status({ pins: 'all', reg: 'intf', port:this.getPort(ipin) }) // read port interrupt status
let status = await this.resetInterrupt(ipin) // now reset const status = await this.resetInterrupt(ipin) // now reset
let istatus = await this.send(this.iname,{cmd:'status'}) // get the status from the gpio interrupt
if (istatus.ready==null && istatus.pins) istatus.ready = !!istatus.pins.find(el => el.pin === ipin).ready
let pin = byteFormat(res.port||[], { in: 'ARY', out: 'PLC' })[0] let pin = byteFormat(res.port||[], { in: 'ARY', out: 'PLC' })[0]
if (!pin) { if (!pin) {
log.warn({cmd:'interrupt.find', line:132, inter:inter, msg:'no pin associated with interrupt'}) this.emit('log',{level:'warn', cmd:inter.cmd, inter:inter, msg:'no pin associated with interrupt'})
return { error: 'no pin associated with interrupt' } reply = { cmd:'error', error: 'no pin associated with interrupt' }
} }
else { // avoid bad interrupt (i.e. no pin caused interrupt) else { // avoida bad interrupt (i.e. no pin caused interrupt)
delete inter.cmd; delete inter._header delete inter.cmd; delete inter._header
inter.ipin = ipin inter.ipin = ipin
inter.pin = pin inter.pin = pin
inter.port = this[ipin].mport inter.port = this[ipin].mport
inter.interrupt_ready = status inter.mpc_interrupt_reset = status
inter.reset = istatus.ready
inter.state = (await this.commands.pin.status({pin:pin, port:this.getPort(ipin)})).state inter.state = (await this.commands.pin.status({pin:pin, port:this.getPort(ipin)})).state
inter.closed = inter.state === 'on' ? true : false inter.closed = inter.state === 'on' ? true : false
this._interruptProcess(inter) this._interruptProcess(inter)
// replying with reset command which is also a check reply = {cmd:'reply', msg:'mcp interrupt pin was found', inter:inter}
return {cmd:'reset', pin:ipin}
}
}
} }
if (!istatus.ready) this.emit('log',{level:'fatal', msg:'gpio interrrupt was not reset after firing',interrupt:inter})
} else reply = { cmd:'reply', msg:'interrupt was already reset nothing to find/do' }
return reply
} // end find
} // end commands } // end commands