0.2.14 refactor to using on/off with epoll, retiring pigipo. Readme now has some explanation on getting hardware prepared

master
David Kebler 2019-03-14 10:43:09 -07:00
parent 2516bea92a
commit e22c21823c
7 changed files with 141 additions and 119 deletions

1
.gitignore vendored
View File

@ -1,3 +1,4 @@
/node_modules/ /node_modules/
/coverage/ /coverage/
*.lock *.lock
archive/

View File

@ -2,3 +2,5 @@ tests/
test/ test/
*.test.js *.test.js
testing/ testing/
examples/
archive/

View File

@ -1,6 +1,6 @@
import Interrupt from '../src/interrupt' import Interrupt from '../src/interrupt'
// const delay = time => new Promise(res=>setTimeout(()=>res(),time)) const delay = time => new Promise(res=>setTimeout(()=>res(),time))
let interrupt = new Interrupt(24,{id:'test-interrupt', wait:0, hook:true, useRootNS:true}) let interrupt = new Interrupt(24,{id:'test-interrupt', wait:0, hook:true, useRootNS:true})
@ -18,8 +18,8 @@ let interrupt = new Interrupt(24,{id:'test-interrupt', wait:0, hook:true, useRo
return packet return packet
} }
interrupt.fire() interrupt.fire()
// await delay(3000) await delay(3000)
// process.kill(process.pid, 'SIGTERM') process.kill(process.pid, 'SIGTERM')
})().catch(err => { })().catch(err => {
console.error('FATAL: UNABLE TO START SYSTEM!\n',err) console.error('FATAL: UNABLE TO START SYSTEM!\n',err)

View File

@ -1,16 +1,14 @@
{ {
"name": "@uci/interrupt", "name": "@uci/interrupt",
"main": "src", "main": "src",
"version": "0.2.13", "version": "0.2.14",
"description": "a class for adding interrupt processesing for gpio pins on Raspberry Pi and Similar SBCs", "description": "a class for adding interrupt processesing for gpio pins on Raspberry Pi and Similar SBCs",
"scripts": { "scripts": {
"single": "sudo node -r esm examples/single", "single": "node -r esm examples/single",
"singlem": "MOCK=true DEBUG=true node -r esm examples/single",
"client": "node -r esm examples/client", "client": "node -r esm examples/client",
"singlelog": "UCI_LOG=true sudo node -r esm examples/single | pino-colada", "singlelog": "UCI_ENV=dev node -r esm examples/single",
"mmulti": "MOCK=true node --require esm examples/multi",
"multi": "sudo node --require esm examples/multi", "multi": "sudo node --require esm examples/multi",
"multilog": "UCI_LOG=true sudo node --require esm examples/multi | pino-colada" "multilog": "UCI_ENV=dev node --require esm examples/multi"
}, },
"author": "David Kebler", "author": "David Kebler",
"license": "MIT", "license": "MIT",
@ -29,23 +27,18 @@
"url": "https://github.com/uCOMmandIt/uci-interrrupt/issues" "url": "https://github.com/uCOMmandIt/uci-interrrupt/issues"
}, },
"homepage": "https://github.com/uCOMmandIt/uci-interrrupt#readme", "homepage": "https://github.com/uCOMmandIt/uci-interrrupt#readme",
"optionalDependencies": {
"pigpio": "^0.x"
},
"dependencies": { "dependencies": {
"@uci/base": "^0.1.16",
"@uci/i2c-device": "^0.1.12",
"@uci-utils/logger": "0.0.13", "@uci-utils/logger": "0.0.13",
"lodash.debounce": "^4.0.8" "@uci/base": "^0.1.16",
"death": "^1.1.0",
"onoff": "^4.1.0"
}, },
"devDependencies": { "devDependencies": {
"chai": "^4.1.2", "chai": "^4.1.2",
"chai-as-promised": "^7.1.1",
"codecov": "^3.0.0", "codecov": "^3.0.0",
"esm": "^3.0.58", "esm": "^3.0.58",
"istanbul": "^0.4.5", "istanbul": "^0.4.5",
"mocha": "^5.0.1", "mocha": "^5.0.1",
"nodemon": "^1.14.3", "nodemon": "^1.14.3"
"pigpio-mock": "uCOMmandIt/pigpio-mock#master"
} }
} }

View File

@ -1,4 +1,4 @@
# uCOMmandIt Interrupt Package # uCOMmandIt Interrupt Package for SBC GPio Pins
<!-- find and replace the package name to match --> <!-- find and replace the package name to match -->
[![Build Status](https://img.shields.io/travis/uCOMmandIt/uci-interrupt.svg?branch=master)](https://travis-ci.org/uCOMmandIt/uci-interrupt) [![Build Status](https://img.shields.io/travis/uCOMmandIt/uci-interrupt.svg?branch=master)](https://travis-ci.org/uCOMmandIt/uci-interrupt)
@ -6,11 +6,79 @@
[![devDependencies](https://img.shields.io/david/dev/uCOMmandIt/uci-interrupt.svg)](https://david-dm.org/uCOMmandIt/uci-interrupt?type=dev) [![devDependencies](https://img.shields.io/david/dev/uCOMmandIt/uci-interrupt.svg)](https://david-dm.org/uCOMmandIt/uci-interrupt?type=dev)
[![codecov](https://img.shields.io/codecov/c/github/uCOMmandIt/uci-interrupt/master.svg)](https://codecov.io/gh/uCOMmandIt/uci-interrupt) [![codecov](https://img.shields.io/codecov/c/github/uCOMmandIt/uci-interrupt/master.svg)](https://codecov.io/gh/uCOMmandIt/uci-interrupt)
This module creates an ES6 Interrupt class by extending the event emitter class for use . It calls uses the pigio C library via pigpio javascript bindings package in order to set up one or more pins on an SBC as interrupts that are then listened for. This module creates an instance of UCI Packect Interrupt class for a SBC gpio pin supporting interrupt via epoll (kernel gpio/export) by extending the UCI base class.
The pigipio C library is specifically coded for the Raspberry Pi but might be adapatable to any single board computer (sbc) with Gpios Each pin will create it's own socket (based on options pased) By default it will create a tcp socket at 9000+pin number. I can create a socket for all transports.
assuming they would be pigio C, compatiable. You must have pigpio C library installed first.
When a gpio pin so set up is tripped this class emits a "fired" message. If you pass a handler function it will be called when the "fired" message is emitted or you can set up your own "fired" listener to do as you please. When a gpio pin so set up is tripped this class pushes a UCI packet to all connected consumers.
You'll need to refer to https://github.com/fivdi/pigpio to make sure you have the pigpio C library set up. You can pass a custom packet to push via the options and/or ammend the basic packet via a hook you provide.
By default the packet will send and packet.cmd='interrupt' but you can customize that via either passing .cmd in the .packet option or passing .pushCmd
UCI tcp and pipe transports sockets support an initial connection packet sent to connecting consumers by by passing .conPacket= { } or .resetCmd= to instance.
This allows one to take some initial action related to interrupt (e.g. an mcp chip can reset it's interrupt connect to a sbc gpio pin)
## Set up hardware GPio bus pins as interrupts for use with UCI Interrupt
### Enable access to GPios
make sure your user is in the `gpio` group
Give `gpio` group permission to reading/writing from/to pins. On raspbien installs this should already work for pi user. For other distros and other hardware the following rule put in a file in `/etc/udev/rules.d/` should work.
```
SUBSYSTEM=="gpio*", PROGRAM="/bin/sh -c 'find -L /sys/class/gpio/ -maxdepth 2 -exec chown root:gpio {} \; -exec chmod 770 {} \; || true'"
```
if you get permission errors then likely this rule is not effective. Take a look at /sys/class/gpio folder and see if gpio group has been added appropriately (e.g. to /export and /unexport).
### Set hardware pull
You must tell the gpio bus hardware about which pins you want to use as interrupts and what pull state you want. This only needs to be done once and is somewhat specific to the sbc you are using.
#### Raspberry Pi
For raspberry pi with recent distro/kernel (e.g. Ubuntu 18.04,mainline 4.15) you can use add a line to config.txt in /boot or subdir therein. Add a line like this
`gpio=9,10,24=pd`
The pin defaults can be seen in Table 6-31 on pages 102 and 103 of the [BCM2835 ARM Peripherals](http://www.farnell.com/datasheets/1521578.pdf) documentation.
Other possible [settings](https://www.raspberrypi.org/forums/viewtopic.php?f=117&t=208748) are
```
ip - Input
op - Output
a0-a5 - Alt0-Alt5
dh - Driving high (for outputs)
dl - Driving low (for outputs)
pu - Pull up
pd - Pull down
pn/np - No pull
```
#### Other SBC GPio buses
For other SBCs with a gpio bus you'll need to consult the manufactuer docs/forum but you should be able to create and add device tree blobs to set the gpio pins. It may involve enabling device tree overlays and installing the dto package, etc. Here is a [How to for the Raspberry Pi](https://github.com/fivdi/onoff/wiki/Enabling-Pullup-and-Pulldown-Resistors-on-The-Raspberry-Pi) which of course is not necessary (see above) but will give you a start on other hardware
Example dts file for raspberry pi
```
/dts-v1/;
/plugin/;
/ {
compatible = "brcm,bcm2708";
fragment@0 {
target = <&gpio>;
__overlay__ {
pinctrl-names = "default";
pinctrl-0 = <&my_pins>;
my_pins: my_pins {
brcm,pins = <7 8 9>; /* gpio no. */
brcm,function = <0 0 0>; /* 0:in, 1:out */
brcm,pull = <1 1 2>; /* 2:up 1:down 0:none */
};
};
};
};
```

View File

@ -1,17 +1,18 @@
let bus = {}
import debounce from 'lodash.debounce'
import Base from '@uci/base' import Base from '@uci/base'
import DeadJim from 'death'
import { Gpio } from 'onoff'
import logger from '@uci-utils/logger' import logger from '@uci-utils/logger'
let log = {} 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 // 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 { class Interrupt extends Base {
constructor(pin, opts = {}) { constructor(pin, opts = {}) {
if (typeof pin !=='number') pin = parseInt(pin) // make sure pin is a number! if (typeof pin !=='number') pin = parseInt(pin) // make sure pin is a number!
opts.conPacket = (opts.resetCmd && !opts.conPacket) ? opts.conPacket : { cmd: opts.resetCmd, pin: pin } // will use either option opts.conPacket = (opts.resetCmd && !opts.conPacket) ? { cmd: opts.resetCmd, pin: pin } : opts.conPacket // will use either option
// if opts .port/.path/.topic/.wport are base number/name to which pin number is added/appended (default being 9000,9100,'interrupt') log.debug({conPacket: opts.conPacket, msg:'connection packet for consumers'})
// for specific port number pass itrt.port,itrn.path,itrm.topic,itrw.port which override it if present
// conPacket is for uci socket. This will send this command on connect, needed to initially reset the interrupt
if (opts.path || opts.itrn) { if (opts.path || opts.itrn) {
opts.itrn = opts.itrn || {} opts.itrn = opts.itrn || {}
if (opts.path && typeof opts.path !=='boolean') opts.path = opts.path + ':' + pin if (opts.path && typeof opts.path !=='boolean') opts.path = opts.path + ':' + pin
@ -42,85 +43,63 @@ class Interrupt extends Base {
} }
super(opts) super(opts)
this.id = (opts.id || 'interrupt') + ':' + pin this.id = (opts.id || 'interrupt') + ':' + pin
log = logger({ name: 'interrupt', id: this.id })
log.info({ pins: pin, opts: opts }, 'created interrupt with these opts') log.info({ pins: pin, opts: opts }, 'created interrupt with these opts')
this.pin_num = +pin this.pin_num = pin
this.mock = opts.mock || process.env.MOCK this.mock = opts.mock || process.env.MOCK
this.wait = opts.wait || 0 // debounce is off by default this.wait = opts.wait || 0 // debounce is off by default
this.dbopts = { // https://github.com/fivdi/onoff#gpiogpio-direction--edge--options
maxWait: opts.maxwait || 500, this.edge = opts.edge || 'rising' // falling,both,none=no interrupt
leading: opts.leading || true, // this.pull = opts.pull || 'low' // 'high'
trailing: opts.trailing || false this.pin = {}
}
this.edge = opts.edge
this.pull = opts.pull
this.pin = {} //set at init
this.hook = opts.hook this.hook = opts.hook
this.packet = opts.packet || {} this.packet = opts.packet || {}
this.packet.pin = pin this.packet.id = this.id
this.packet.pin = this.pin_num
this.packet.cmd = this.packet.cmd || opts.pushCmd || 'interrupt' this.packet.cmd = this.packet.cmd || opts.pushCmd || 'interrupt'
this.packet.count = 0 this.packet.count = 0
this._hookFunc = defaultHook
this.s = { fire:this.fire.bind(this)} // make fire available via consumer packet send
} // end constructor } // end constructor
async init() { async init() {
await super.init() await super.init()
// for cntrl-c exit of interrupt
// create the pigio pin_num DeadJim( (signal,err) => {
// TODO check for rpi and pigpio, if not available use onoff log.warn({signal:signal, error:err, msg:'Interrupt was killed'})
// wrap all needed commands so can call module for pigpio or onoff this.pin.unwatchAll()
if (process.env.UCI_MOCK==='true') bus = await import('pigpio-mock') this.pin.unexport() // kill the kernel entry
else bus = await import('pigpio')
this.pin = new bus.Gpio(this.pin_num, {
mode: bus.Gpio.INPUT,
pullUpDown: this.pull || bus.Gpio.PUD_DOWN
// do not! set edge here as it will start the emitter -- see pigio js
}) })
process.on('SIGINT', () => { this.pin = new Gpio(this.pin_num, 'in', this.edge, { debounceTimeout:this.wait })
this.exit()
.then(resp => console.log('\n', resp)) // unexport on cntrl-c
.catch(err => console.log('error:', err))
})
let cb = () => {}
if (this.wait === 0) {
cb = this._interruptProcess.bind(this, this.packet)
log.info({ packet: this.packet },`starting interrupt on pin ${this.pin_num} without debounce`)
} else {
cb = debounce(
this._interruptProcess.bind(this, this.packet),
this.wait,
this.dbopts
)
log.info({ packet: this.packet, wait: this.wait, options: this.dbopts },`starting interrupt on pin ${this.pin_num} with debounce wait:${this.wait}`)
}
this.pin.on('interrupt', cb)
// rock n roll!!, start the pigpio interrupt
if (!this.mock) this.pin.enableInterrupt(this.edge || bus.Gpio.RISING_EDGE)
this.registerHook(defaultHook) log.debug({msg:'new interrupt pin created and watching', pin:this.pin, edge:this.edge,debounce:this.wait})
this.pin.watch( function (err,value) {
log.debug('sbc interrupt tripped', err, value)
this._interruptProcess(this.packet,err,value)
}.bind(this))
} // end init } // end init
// manual firing for testing // manual firing for testing
async fire() { async fire(packet) {
console.log('manually firing interrupt for pin', this.pin_num) console.log('manually firing interrupt for pin', this.pin_num)
this.pin.emit('interrupt', 1) await this._interruptProcess(this.packet,null,'manual')
return Promise.resolve({ status: 'fired' }) packet.status = 'fired'
} return packet
exit() {
bus.terminate()
return Promise.reject('keyboard termination...terminating interrupts')
} }
// use hook to do more processing // use hook to do more processing
async _interruptProcess(packet) { async _interruptProcess(packet,err,value) {
console.log('from watch listener', packet.pin, err, value)
packet.error = err
packet.state = value
packet.count += 1 packet.count += 1
packet.time = new Date().getTime() packet.time = new Date().getTime()
packet.id = this.id
if (this.hook) packet = await this._hookFunc.call(this,packet) if (this.hook) packet = await this._hookFunc.call(this,packet)
log.info({ packet: packet }, 'packet pushing to all clients') log.debug({ pin:this.pin, packet: packet }, 'packet pushing to all clients')
this.push(packet) await this.push(packet)
} }
@ -140,10 +119,10 @@ async function defaultHook(packet) {
console.log('==========default hook =============') console.log('==========default hook =============')
console.log(`pin ${packet.pin} on sbc gpio bus has thrown an interrupt`) console.log(`pin ${packet.pin} on sbc gpio bus has thrown an interrupt`)
console.log(`pushing to all connected socket client with cmd:${packet.cmd}`) console.log(`pushing to all connected socket client with cmd:${packet.cmd}`)
console.dir('packet) console.dir(packet)
console.log('replace by a new function with .registerHook(function) to overwrite this') 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('Must be async/promise returning if anything async happens in your hook')
console.log('This hook allow you to modify/add to the packet being pushed to connected clients') 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('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('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('the hook options contains', this.hook)

View File

@ -8,6 +8,7 @@ class Interrupts {
this.id = this.id || 'interrupts' this.id = this.id || 'interrupts'
this.pins = pins this.pins = pins
this.interrupt = {} this.interrupt = {}
this.s = { fire:this.fire.bind(this)} // make fire available via consumer packet send
log = logger({ name: 'interrupts', id: this.id }) log = logger({ name: 'interrupts', id: this.id })
let pinopts = {} let pinopts = {}
pins.forEach(pin => { pins.forEach(pin => {
@ -19,8 +20,7 @@ class Interrupts {
if (typeof pin !=='number') pin = parseInt(pin) if (typeof pin !=='number') pin = parseInt(pin)
pinopts[pin] = Object.assign({}, opts, pinopts[pin]) pinopts[pin] = Object.assign({}, opts, pinopts[pin])
pinopts[pin].id = (opts.id || 'interrupt') + ':' + pin pinopts[pin].id = (opts.id || 'interrupt') + ':' + pin
pinopts[pin].conPacket = { cmd: opts.resetCmd, pin: pin } log.debug({ opts: pinopts[pin] }, `pin options for pin ${pin}`)
log.info({ opts: pinopts[pin] }, `pin options for pin ${pin}`)
this.interrupt[pin] = new Interrupt(pin, pinopts[pin]) this.interrupt[pin] = new Interrupt(pin, pinopts[pin])
}) })
} }
@ -34,25 +34,20 @@ class Interrupts {
} }
// manual firing for testing // manual firing for testing
fire(pin) { async fire(packet) {
if (pin) { if (packet.pin) return await this.interrupt[packet.pin].fire(packet)
this.interrupt[pin].pin.emit('interrupt', 1) for ( let pin of packet.pins) {
console.log('manually firing interrupt for pin', pin) packet[pin] = await this.interrupt[pin].fire(packet)
} else { } return packet
console.log('manually firing interrupt for pins', this.pins)
this.pins.forEach(async pin => {
this.interrupt[pin].pin.emit('interrupt', 1)
})
}
} }
setHook(func) {
registerHook(func) {
this.pins.forEach(async pin => { this.pins.forEach(async pin => {
this.interrupt[pin].hook = func this.interrupt[pin].registerHook(func)
}) })
} }
// new test // used for testing
push(packet) { push(packet) {
this.pins.forEach(async pin => { this.pins.forEach(async pin => {
console.log('=======all push============', pin, packet) console.log('=======all push============', pin, packet)
@ -62,19 +57,3 @@ class Interrupts {
} // end Class } // end Class
export default Interrupts export default Interrupts
// default hook
const hook = packet => {
console.log('======Common for all Pins Default Hook=================')
console.log(
`pin ${packet.pin} on sbc gpio bus has thrown ${packet.count}th interrupt`
)
console.log(
'sending to all connected consumers/clients with default cmd:"interrupt"'
)
console.dir(packet)
console.log('this is the default beforeHook')
console.log('add .hook for your instance or extended class')
console.log('=======================')
return packet
}