diff --git a/.gitignore b/.gitignore index faad3eb..d749731 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ /node_modules/ /coverage/ *.lock +archive/ diff --git a/.npmignore b/.npmignore index f16fc41..acd6b52 100644 --- a/.npmignore +++ b/.npmignore @@ -2,3 +2,5 @@ tests/ test/ *.test.js testing/ +examples/ +archive/ diff --git a/examples/single.js b/examples/single.js index 64bb17b..3d531af 100644 --- a/examples/single.js +++ b/examples/single.js @@ -1,6 +1,6 @@ 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}) @@ -18,8 +18,8 @@ let interrupt = new Interrupt(24,{id:'test-interrupt', wait:0, hook:true, useRo return packet } interrupt.fire() - // await delay(3000) - // process.kill(process.pid, 'SIGTERM') + await delay(3000) + process.kill(process.pid, 'SIGTERM') })().catch(err => { console.error('FATAL: UNABLE TO START SYSTEM!\n',err) diff --git a/package.json b/package.json index 7ee62fc..4af2f11 100644 --- a/package.json +++ b/package.json @@ -1,16 +1,14 @@ { "name": "@uci/interrupt", "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", "scripts": { - "single": "sudo node -r esm examples/single", - "singlem": "MOCK=true DEBUG=true node -r esm examples/single", + "single": "node -r esm examples/single", "client": "node -r esm examples/client", - "singlelog": "UCI_LOG=true sudo node -r esm examples/single | pino-colada", - "mmulti": "MOCK=true node --require esm examples/multi", + "singlelog": "UCI_ENV=dev node -r esm examples/single", "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", "license": "MIT", @@ -29,23 +27,18 @@ "url": "https://github.com/uCOMmandIt/uci-interrrupt/issues" }, "homepage": "https://github.com/uCOMmandIt/uci-interrrupt#readme", - "optionalDependencies": { - "pigpio": "^0.x" - }, "dependencies": { - "@uci/base": "^0.1.16", - "@uci/i2c-device": "^0.1.12", "@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": { "chai": "^4.1.2", - "chai-as-promised": "^7.1.1", "codecov": "^3.0.0", "esm": "^3.0.58", "istanbul": "^0.4.5", "mocha": "^5.0.1", - "nodemon": "^1.14.3", - "pigpio-mock": "uCOMmandIt/pigpio-mock#master" + "nodemon": "^1.14.3" } } diff --git a/readme.md b/readme.md index 312d00b..1eb6e31 100644 --- a/readme.md +++ b/readme.md @@ -1,4 +1,4 @@ -# uCOMmandIt Interrupt Package +# uCOMmandIt Interrupt Package for SBC GPio Pins [![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) [![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 -assuming they would be pigio C, compatiable. You must have pigpio C library installed first. +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. -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 */ + }; + }; + }; +}; +``` diff --git a/src/interrupt.js b/src/interrupt.js index 83ae75f..f0b09ab 100644 --- a/src/interrupt.js +++ b/src/interrupt.js @@ -1,17 +1,18 @@ -let bus = {} -import debounce from 'lodash.debounce' import Base from '@uci/base' +import DeadJim from 'death' +import { Gpio } from 'onoff' 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 +// 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) ? opts.conPacket : { cmd: opts.resetCmd, pin: pin } // 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') - // 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 + 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 @@ -42,85 +43,63 @@ class Interrupt extends Base { } super(opts) this.id = (opts.id || 'interrupt') + ':' + pin - log = logger({ name: 'interrupt', id: this.id }) 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.wait = opts.wait || 0 // debounce is off by default - this.dbopts = { - maxWait: opts.maxwait || 500, - leading: opts.leading || true, - trailing: opts.trailing || false - } - this.edge = opts.edge - this.pull = opts.pull - this.pin = {} //set at init + // https://github.com/fivdi/onoff#gpiogpio-direction--edge--options + this.edge = opts.edge || 'rising' // falling,both,none=no interrupt + // this.pull = opts.pull || 'low' // 'high' + this.pin = {} this.hook = opts.hook 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.count = 0 + this._hookFunc = defaultHook + this.s = { fire:this.fire.bind(this)} // make fire available via consumer packet send } // end constructor async init() { await super.init() - // for cntrl-c exit of interrupt - // create the pigio pin_num - // TODO check for rpi and pigpio, if not available use onoff - // wrap all needed commands so can call module for pigpio or onoff - if (process.env.UCI_MOCK==='true') bus = await import('pigpio-mock') - 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 + + DeadJim( (signal,err) => { + log.warn({signal:signal, error:err, msg:'Interrupt was killed'}) + this.pin.unwatchAll() + this.pin.unexport() // kill the kernel entry + }) - process.on('SIGINT', () => { - 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.pin = new Gpio(this.pin_num, 'in', this.edge, { debounceTimeout:this.wait }) - 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 // manual firing for testing - async fire() { + async fire(packet) { console.log('manually firing interrupt for pin', this.pin_num) - this.pin.emit('interrupt', 1) - return Promise.resolve({ status: 'fired' }) - } - - exit() { - bus.terminate() - return Promise.reject('keyboard termination...terminating interrupts') + await this._interruptProcess(this.packet,null,'manual') + packet.status = 'fired' + return packet } // 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.time = new Date().getTime() - packet.id = this.id if (this.hook) packet = await this._hookFunc.call(this,packet) - log.info({ packet: packet }, 'packet pushing to all clients') - this.push(packet) + log.debug({ pin:this.pin, packet: packet }, 'packet pushing to all clients') + await this.push(packet) } @@ -140,10 +119,10 @@ async function defaultHook(packet) { console.log('==========default hook =============') 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.dir('packet) + 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 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('if you pass a hash for .hook you can use it here as this.hook') console.log('the hook options contains', this.hook) diff --git a/src/interrupts.js b/src/interrupts.js index 9f36910..7ca636c 100644 --- a/src/interrupts.js +++ b/src/interrupts.js @@ -8,6 +8,7 @@ class Interrupts { this.id = this.id || 'interrupts' this.pins = pins this.interrupt = {} + this.s = { fire:this.fire.bind(this)} // make fire available via consumer packet send log = logger({ name: 'interrupts', id: this.id }) let pinopts = {} pins.forEach(pin => { @@ -19,8 +20,7 @@ class Interrupts { if (typeof pin !=='number') pin = parseInt(pin) pinopts[pin] = Object.assign({}, opts, pinopts[pin]) pinopts[pin].id = (opts.id || 'interrupt') + ':' + pin - pinopts[pin].conPacket = { cmd: opts.resetCmd, pin: pin } - log.info({ opts: pinopts[pin] }, `pin options for pin ${pin}`) + log.debug({ opts: pinopts[pin] }, `pin options for pin ${pin}`) this.interrupt[pin] = new Interrupt(pin, pinopts[pin]) }) } @@ -34,25 +34,20 @@ class Interrupts { } // manual firing for testing - fire(pin) { - if (pin) { - this.interrupt[pin].pin.emit('interrupt', 1) - console.log('manually firing interrupt for pin', pin) - } else { - console.log('manually firing interrupt for pins', this.pins) - - this.pins.forEach(async pin => { - this.interrupt[pin].pin.emit('interrupt', 1) - }) - } + async fire(packet) { + if (packet.pin) return await this.interrupt[packet.pin].fire(packet) + for ( let pin of packet.pins) { + packet[pin] = await this.interrupt[pin].fire(packet) + } return packet } - setHook(func) { + + registerHook(func) { this.pins.forEach(async pin => { - this.interrupt[pin].hook = func + this.interrupt[pin].registerHook(func) }) } - // new test + // used for testing push(packet) { this.pins.forEach(async pin => { console.log('=======all push============', pin, packet) @@ -62,19 +57,3 @@ class Interrupts { } // end Class 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 -}