commit 54d021fbbda938eccc4716cf89ab08f2ae5065bc Author: David Kebler Date: Mon Jun 17 12:14:33 2019 -0700 Initial commit of on/off gpio initially forked from uci-interrupt diff --git a/.eslintrc.js b/.eslintrc.js new file mode 100644 index 0000000..2bed546 --- /dev/null +++ b/.eslintrc.js @@ -0,0 +1,33 @@ +module.exports = { + "ecmaFeatures": { + "modules": true, + "spread" : true, + "restParams" : true + }, + "env": { + "es6": true, + "node": true, + "mocha": true + }, + "parserOptions": { + "ecmaVersion": 2017, + "sourceType": "module" + }, + "extends": "eslint:recommended", + "rules": { + "indent": [ + "error", + 2 + ], + "no-console": 0, + "semi": ["error", "never"], + "linebreak-style": [ + "error", + "unix" + ], + "quotes": [ + "error", + "single" + ] + } +} diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..d749731 --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +/node_modules/ +/coverage/ +*.lock +archive/ diff --git a/.npmignore b/.npmignore new file mode 100644 index 0000000..acd6b52 --- /dev/null +++ b/.npmignore @@ -0,0 +1,6 @@ +tests/ +test/ +*.test.js +testing/ +examples/ +archive/ diff --git a/examples/client.js b/examples/client.js new file mode 100644 index 0000000..b9ba7d1 --- /dev/null +++ b/examples/client.js @@ -0,0 +1,27 @@ +import Base from '@uci/base' + +// const HOST = 'localhost' +const HOST = 'pine64.kebler.net' +const PORT = 9075 +let client = new Base({sockets:'client#c>t', client:{host:HOST, port:PORT}, id:'gpios-client'}) + + +// client.c.reply = async function (packet) { +// return new Promise((resolve) => { +// console.log('reply from gpio with packet') +// console.dir(packet) +// resolve() +// }) +// } + +; +(async () => { + + await client.init() + console.log('RETURN', await client.send({cmd: 'on', pins:'all'})) + // console.log('RETURN', await client.send({cmd: 'off', pins:'all'})) + process.exit() + +})().catch(err => { + console.error('FATAL: UNABLE TO START SYSTEM!\n',err) +}) diff --git a/examples/homeassistant.js b/examples/homeassistant.js new file mode 100644 index 0000000..dd49634 --- /dev/null +++ b/examples/homeassistant.js @@ -0,0 +1,54 @@ + +// TODO make this an HA plugin to support HA convention --- + +///***************** HOME ASSISTANT ************************ +// uses main broker socket but formats incoming and outgoing packets for HA convention +// TODO Make sure subscribed topics includes 'lighting/set/#' + +const TOPIC = 'relays' + +function register(name) { + + this.beforeProcessHook(async (packet) => { + console.log('incoming mqtt packet to modify') + console.dir(packet) + let cmd = packet.cmd.split('/') + packet.cmd = `${packet.data.toLowerCase()}` + packet.pins = cmd[2] + delete packet.data + console.log('translated to uci packet') + console.dir(packet) + return packet + }, + {name:name}) + + this.afterProcessHook(async (packet) => { + console.log('processed packet available to modify again', packet) + if (packet.error) { + let npacket = {} + npacket.cmd = 'error' + npacket.payload = JSON.stringify(packet) + // npacket.payload = packet.error + return npacket + } + return packet + }, + {name:name}) + + this.beforeSendHook(async (packet) => { + console.log('beforeSendHook', packet) + if (packet.cmd === 'status') { + let num = Object.keys(packet.pins)[0] + packet.cmd = `${TOPIC}/status/${num}` + packet.payload = packet.pins[num] ? 'on' : 'off' + console.log('=============modified packet sent to broker================') + console.dir(packet) + console.log('================') + } + return packet + }, + {name:name}) + +} + +export default register diff --git a/examples/multi.js b/examples/multi.js new file mode 100644 index 0000000..5c1e162 --- /dev/null +++ b/examples/multi.js @@ -0,0 +1,24 @@ +import Gpios from '../src/gpios' +import hahooks from './homeassistant.js' + +const PINS = [80,73,69,230,229,361,75,74,70] + +let relays = new Gpios(PINS, {pinOpts:{activeLow:true}}) + +const BROKER = 'nas.kebler.net' +const TCP_PORT = 9075 + +; +(async () => { + await relays.addSocket('mqs','s','m', {host:BROKER, topics:['relays/set/+']}) + hahooks.call(relays,'mqs') + await relays.addSocket('tcp','s','t', {port:TCP_PORT}) + await relays.addSocket('web','s','w') + await relays.init() + + // await relays.getSocket('mqs').subscribe(['relays/#']) + +})().catch(err => { + console.error('FATAL: UNABLE TO START SYSTEM!\n',err) + // process.kill(process.pid, 'SIGTERM') +}) diff --git a/package.json b/package.json new file mode 100644 index 0000000..577b744 --- /dev/null +++ b/package.json @@ -0,0 +1,41 @@ +{ + "name": "@uci/gpio", + "main": "src", + "version": "0.1.10", + "description": "a class for adding simple on/off read of gpio pins on Raspberry Pi and Similar SBCs", + "scripts": { + "client": "node -r esm examples/client", + "multi": "node --require esm examples/multi", + "multilog": "UCI_ENV=dev node --require esm examples/multi" + }, + "author": "David Kebler", + "license": "MIT", + "repository": { + "type": "git", + "url": "git+https://github.com/uCOMmandIt/uci-interrrupt.git" + }, + "keywords": [ + "node.js", + "communication", + "serial", + "utilities", + "helpers" + ], + "bugs": { + "url": "https://github.com/uCOMmandIt/uci-interrrupt/issues" + }, + "homepage": "https://github.com/uCOMmandIt/uci-interrrupt#readme", + "dependencies": { + "@uci-utils/logger": "0.0.14", + "@uci/base": "^0.1.23", + "death": "^1.1.0", + "onoff": "^4.1.1", + "p-settle": "^3.1.0" + }, + "devDependencies": { + "chai": "^4.1.2", + "esm": "^3.2.25", + "mocha": "^5.0.1", + "nodemon": "^1.14.3" + } +} diff --git a/readme.md b/readme.md new file mode 100644 index 0000000..d4773f2 --- /dev/null +++ b/readme.md @@ -0,0 +1,25 @@ +# uCOMmandIt GPIO 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) +[![Inline docs](http://inch-ci.org/github/uCOMmandIt/uci-interrupt.svg?branch=master)](http://inch-ci.org/github/uCOMmandIt/uci-interrupt) +[![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 instance of UCI Packet GPIO for a SBC gpio pin. This class does NOT include support for interrupt based input. See UCI-Interrupt package for that. + +## Set up hardware GPio bus pins as interrupts for use with UCI Interrupt + +### Enable access to GPios +create (if need be) a gpio group and make sure your user is in the `gpio` group + + +change group to gpio for /sys/class/gpio/export and unexport and mode to 770. + +make a new udev rules file. + +``` +SUBSYSTEM=="gpio*", PROGRAM="/bin/sh -c 'find -L /sys/class/gpio/ -maxdepth 2 -exec chown root:gpio {} \; -exec chmod 770 {} \; || true'" +``` + +That should allow access via the on/off package which is a dependency. diff --git a/src/commands.js b/src/commands.js new file mode 100644 index 0000000..b88ee75 --- /dev/null +++ b/src/commands.js @@ -0,0 +1,54 @@ +const pSettle = require('p-settle') +import logger from '@uci-utils/logger' +let log = logger({package:'@uci/gpio', file:'/src/commands.js'}) + +export default { + on: async function(packet) { + packet.pinCmd = 'on' + this.commands.set(packet) + return packet + }, + off: async function(packet) { + packet.pinCmd = 'off' + await this.commands.set(packet) + return packet + }, + toggle: async function(packet) { + packet.pinCmd = 'toggle' + await this.commands.set(packet) + return packet + }, + set: async function(packet) { + packet.pinCmd = packet.pinCmd || 'set' + let pins = parsePins.call(this, packet.pins || packet.pin) + packet.res = await pSettle( + pins.map(pin => { + return this.pins[pin][packet.pinCmd](packet.value) + }) + ) + packet = await this.commands.get(packet) + this.emit('gpios',packet) // emit locally + this.push(packet) // will push on any socket + return packet + }, + + get: async function(packet) { + let pins = parsePins.call(this,packet.pins || packet.pin) + packet.pins = {} + pins.forEach(pin => packet.pins[pin] = this.pins[pin].value) + packet.cmd = 'status' + return packet + } +} + +const parsePins = function(pins) { + if (typeof pins==='number') pins = [pins] + if (typeof pins==='string') { + if (pins==='all') pins = Object.keys(this.pins) + else { + pins = pins.split(/[,:\s]+/).map(Number).filter( (x) => !Number.isNaN(x)) + } + } + // TODO plain object is pin and value pairs + return pins +} diff --git a/src/gpio.js b/src/gpio.js new file mode 100644 index 0000000..cdbfdb6 --- /dev/null +++ b/src/gpio.js @@ -0,0 +1,73 @@ +import DeadJim from 'death' +import { Gpio } from 'onoff' +import to from 'await-to-js' + + +import logger from '@uci-utils/logger' + +let log = logger({package:'@uci/gpio', file:'/src/gpio.js'}) + +class Pin extends Gpio { + constructor(pin, opts = {}) { + if (typeof pin !=='number') pin = parseInt(pin) // make sure pin is a number! + // TODO check against a set of known acceptable pin numbers based on SBC + log.debug({pin:pin, msg:'construtor - creating gpio pin'}) + if (isNaN(pin)) throw new Error('not a valid gpio pin number') + super(pin,opts.direction || 'out',opts) + this.id = (opts.id || 'gpio') + ':' + pin + log.debug({ pin: pin, opts: opts, method:'constructor', line:15, msg:'created gpio with these opts'}) + this.number = pin + this.direction = opts.direction || 'out' + this.value = opts.init || 0 + } // end constructor + + async init() { + this.set(this.value) + DeadJim((signal,err) => { + log.warn({signal:signal, method:'init', line:56, error:err, msg:'gpio process was killed, unexporting pin'}) + this.unexport() // kill the kernel entry + }) + } + + async on() { return await this.set(1) } + async off() { return await this.set(0) } + async toggle() { return await this.set(!this.value) } + + async set(value) { + let [err] = await to(this.write(parseInt(value) || 0)) + if (err) { + log.warn({error:err, pin:this.number, msg:'error reading gpio pin'}) + throw err + } + let curValue = await this.read() + if (value !== curValue) throw new Error(`pin ${this.number} was not set correctly: ${value}:${curValue}`) + return curValue + } + + async read() { + let [err, res] = await to(super.read()) + if (err) { + log.warn({error:err, pin:this.pin, msg:'error reading gpio pin'}) + return err + } + this.value=res + this.state = res ? 'on' : 'off' + return res + } + + async get() { return this.value } + +} // end Class + +export default Pin + + +//example hook +// async function defaultHook(packet) { +// return a promise or use await if anything async happens in hook +// new Promise((resolve) => { +// console.log('==========default hook =============') +// return packet +// resolve(packet) +// }) +// } diff --git a/src/gpios.js b/src/gpios.js new file mode 100644 index 0000000..3cb9cb0 --- /dev/null +++ b/src/gpios.js @@ -0,0 +1,49 @@ + +import Base from '@uci/base' +import logger from '@uci-utils/logger' + +import Gpio from './gpio' +import commands from './commands' + +let log = {} + +// pins is array of gpio pin numbers, options is object where pin number is a key for that pin's options (if needed) +// by default a pin is a simple output +// for single pin just use the Gpio Class itself + +class Gpios extends Base { + constructor(pins, opts={}) { + super(opts) + this.id = this.id || 'gpios' + this.pins = {} + log = logger({ name: 'gpios', id: this.id, package:'@uci/gpios', file:'src/gpios.js'}) + pins.forEach(pin => { + if (typeof pin !=='number') pin = parseInt(pin) + opts[pin] = opts[pin] ? opts[pin] : opts.pinOpts + log.debug({ opts: opts[pin], line:27, msg:`pin options for pin ${pin}`}) + this.pins[pin] = new Gpio(pin, opts[pin]) + }) + this.commands = this.bindFuncs(commands) + this.addNamespace('commands', 's') // give access to these commands above if a socket/server is created + } + + async init() { + await super.init() + return Promise.all( + Object.keys(this.pins).map(pin => { + return this.pins[pin].init() + }) + ) + } + + + // register same hook to all pins + registerHook(func) { + this.pins.forEach(async pin => { + this.pins[pin].registerHook(func) + }) + } + +} // end Class + +export default Gpios diff --git a/src/index.js b/src/index.js new file mode 100644 index 0000000..cb68a3b --- /dev/null +++ b/src/index.js @@ -0,0 +1,5 @@ +import Gpio from './gpio' +import Gpios from './gpios' + +export { Gpio, Gpios } +export default Gpio diff --git a/test/gpio.test.js b/test/gpio.test.js new file mode 100644 index 0000000..a8ed8c7 --- /dev/null +++ b/test/gpio.test.js @@ -0,0 +1,46 @@ +'use strict' + +const + Gpio = require('../src/interrupt'), + expect = require('chai').expect + +// TODO finish simple test of a pin as interrupt + +describe( + 'Testing ', + function () { + hooks() + interrupt() + someothertests() + }) + +//****************** TESTS ********************** +function interrupt() { + it('==> tests an interrupt', async function () { + + expect(result, 'test failed').to.equal('expectedresult') + + }) +} + +function someothertests() { + it('==> test something', async function () { + + let result = await someasyncfunction() + expect(result, 'test failed').to.equal('expectedresult') + await pause(1000) + + }) +} + +function hooks() { + + before(async() => { + + }) + + beforeEach(async() => {}) + + after(async() => {}) + +}