From 71bd508a0ceb72caf59e2f0eb921da8b34abf237 Mon Sep 17 00:00:00 2001 From: David Kebler Date: Thu, 8 Feb 2018 09:19:46 -0800 Subject: [PATCH] first commit of packet based mcp code. incoming packet interpreted and sent on to bus listens for response and processes and sends back to requestor Working pin.test example --- .eslintrc.js | 33 +++ .gitignore | 1 + examples/ipc-test.mjs | 28 +++ examples/mcp.mjs | 17 ++ package.json | 7 +- src/{ => cjs}/mcp23008-17.js | 0 src/{ => cjs}/port-pin.js | 0 src/mcp23008-17.mjs | 304 ++++++++++++++++++++++++ src/mcp230xx-packet.mjs | 58 +++++ src/pin.mjs | 56 +++++ test/{gpio.test.js => gpio.test.js.off} | 0 test/mcp.test.old.js.off | 80 +++++++ 12 files changed, 583 insertions(+), 1 deletion(-) create mode 100644 .eslintrc.js create mode 100644 examples/ipc-test.mjs create mode 100644 examples/mcp.mjs rename src/{ => cjs}/mcp23008-17.js (100%) rename src/{ => cjs}/port-pin.js (100%) create mode 100644 src/mcp23008-17.mjs create mode 100644 src/mcp230xx-packet.mjs create mode 100644 src/pin.mjs rename test/{gpio.test.js => gpio.test.js.off} (100%) create mode 100644 test/mcp.test.old.js.off 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 index e61051f..24c853d 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ /node_modules/ /coverage/ +/syncd/ diff --git a/examples/ipc-test.mjs b/examples/ipc-test.mjs new file mode 100644 index 0000000..d84ffa4 --- /dev/null +++ b/examples/ipc-test.mjs @@ -0,0 +1,28 @@ +/* +* i2c bus unix socket and client in one for demo +* +*/ + +import Base from '../../uci-base/src/base' + +const PATH = '/opt/sockets/mcp.sock' + +const delay = time => new Promise(res=>setTimeout(()=>res(),time)) +; +(async () => { + + let mcpclient = new Base({id:'mcpclient', sockets:'uc#c>n', uc:{path:PATH}}) + + await mcpclient.init() + console.log('=============sending============') + let packet = {cmd:'pin.test'} + console.dir(packet) + await mcpclient.send(packet) + + await delay(3000) + process.kill(process.pid, 'SIGTERM') + + +})().catch(err => { + console.error('FATAL: UNABLE TO START SYSTEM!\n',err) +}) diff --git a/examples/mcp.mjs b/examples/mcp.mjs new file mode 100644 index 0000000..df98a52 --- /dev/null +++ b/examples/mcp.mjs @@ -0,0 +1,17 @@ +/* +* i2c bus with both unix and tcp socket using defaults. For TCP that is host OS name and port 8080 +* +*/ +import MCP230XX from '../src/mcp230xx-packet' +// const PATH = '' + + ; +(async () => { + + let mcp_chip = new MCP230XX({id:'mcp23008-27', address:'0x27', bus:{host:'sbc'} }) + + await mcp_chip.init() + +})().catch(err => { + console.error('FATAL: UNABLE TO START SYSTEM!\n',err) +}) diff --git a/package.json b/package.json index cca0d65..02f7a7d 100644 --- a/package.json +++ b/package.json @@ -4,6 +4,8 @@ "version": "0.1.2", "description": "Classes and Helper Functions for using the MCP chip on I2C Bus", "scripts": { + "mcp": "SOCKETS_DIR=/opt/sockets node_modules/.bin/nodemon --require @std/esm examples/mcp", + "mcpl": "DEBUG=true SOCKETS_DIR=/opt/sockets node_modules/.bin/nodemon --require @std/esm examples/mcp", "test": "./node_modules/.bin/mocha --reporter list --timeout 30000", "testw": "./node_modules/.bin/mocha --reporter list -- watch --timeout 30000", "testd": "DEBUG='1:*' ./node_modules/.bin/mocha --reporter list --watch --timeout 30000", @@ -33,11 +35,14 @@ "@uci/utils": "^0.1.0", "aggregation": "^1.2.0" }, + "@std/esm": "cjs", "devDependencies": { + "@std/esm": "^0.20.0", "chai": "^3.5.0", "codecov": "^1.0.1", "debug": "^2.6.8", "istanbul": "^0.4.5", - "mocha": "^3.4.2" + "mocha": "^3.4.2", + "nodemon": "^1.14.12" } } diff --git a/src/mcp23008-17.js b/src/cjs/mcp23008-17.js similarity index 100% rename from src/mcp23008-17.js rename to src/cjs/mcp23008-17.js diff --git a/src/port-pin.js b/src/cjs/port-pin.js similarity index 100% rename from src/port-pin.js rename to src/cjs/port-pin.js diff --git a/src/mcp23008-17.mjs b/src/mcp23008-17.mjs new file mode 100644 index 0000000..a4db9fc --- /dev/null +++ b/src/mcp23008-17.mjs @@ -0,0 +1,304 @@ +'use strict' + +const Device = require('@uci/i2c').Device, + portpin = require('./port-pin'), // classes for MCP port and pins + EventEmitter = require('events'), + _ = require('@uci/utils'), + aggregate = require('aggregation/es6') + +const + Port = portpin.Port, + pinConfigs = portpin.configs, // for export so pinConfigs can be accessed at runtime + pause = _.pPause, + debug = _.debug('mcp:23008-17') + +class MCP23008 extends aggregate(Device, EventEmitter) { + constructor(busObj, i2cAddress, opts = {}) { + super(busObj, i2cAddress, opts) + // opts could include options passed on to ports and pin including custom pin config, pin ids...see gpio.js + this.chip_config = opts.chip_config // TODO allow opts.chip_config to be a byte instead of a string pointer + this.ports = {} + opts.portID = 'A' + opts.pids = opts.pids ? opts.pids : opts.pidsA + this.ports.A = new Port(opts) + this.ports.A.state = new _.Byte(opts.stateA) + this.ports.A.interrupt = opts.interruptA ? opts.interruptA : opts.interrupt + } // end constructor + + // helpers + pin(id) { return this.ports.A.pin(id) } // get a reference to a particular pin's object + + pid(address) { return this.ports.A.pid(address) } // return pin id for a given address on a port + + pid(num) { return this.ports.A.pins[num - 1].id } + + portByPin(id) { + if (this.ports.A.pin(id)) { return 'A' } + return false + } + + state(port = 'A') { + return this.ports[port].state.value + } + + // get a handle to the ports interrupt + inter(port = 'A') { + return this.ports[port].interrupt + } + + // Must call after instantiating. + + async init() { + debug.L1(`\n=======\nInitializing ${this.id}`) + await this.writeChipCfg(this.chip_config) // chip settings + await this.writePinsCfg() + // write out any initial state(s) + for (let port in this.ports) { + if (this.state(port)) { + debug.L1(`writing initial port state ${port} ${this.state(port)}`) + await this.writePort(this.state(port), 'force', port) + } + } + } + + // must call after init if using interrupts + async start() { + debug.L1(`starting ${ this.id }`) + for (let port in this.ports) { + // if there are interrupts being used then start them and listeners + if (this.inter(port)) { + this.startInterrupt(port) + } + } + } + + async startInterrupt(port) { + await this.interruptReset(port) + debug.L1(`starting interrupt on port ${ port }`) + await this.inter(port).start() + let chip = this // scope `this` for use in the listener below + // bind handler to the chip so handler can read/write to chip/bank when interrupt is emitted + debug.L3('handler', this.inter(port).handler) + let ihandler = this.inter(port).handler.bind(this) + // inside the listener `this` is the interrupt not the chip/bank + this.inter(port).on('fired', async function () { + debug.L1(`interrupt from ${this.pin_number}`) + let data = await ihandler(port) + debug.L2(`port interrupt event 'fired' data => ${data.bank} on port ${data.port} pin# ${data.pin} pid ${data.pid}`) + chip.emit('fired', data) // emit up the class chain + }) + + } + + async interruptReset(port = 'A') { + await this.read(portReg(0x08, port)) + await pause(100) // give enough time for mcp to reset its interupt + debug.L1(`interrupt reset on ${this.id} port ${port}`) + } + + async interruptPin(port, format) { + if ('AB'.indexOf(port) === -1) { + format = port ? port : null + port = 'A' + } + let result = await this.read(portReg(0x07, port)) + return format ? _.byteFormat(result, { in: 'DEC', out: format }) : result + + } + + async writeChipCfg(cfg = 'default') { + let setting = chip_config[cfg] + let byte = _.byteFormat(setting.val, { in: setting.fmt, out: 'DEC' }) + debug.L1(`writing mcp chip config ${setting.val}`) + await this.write(chip_config.cmd, byte) + } + + // pin configurations should already be set before calling + async writePinsCfg() { + debug.L1('writing mcp pins config') + for (let port in this.ports) { + for (let setting in registers.pin_config) { + let reg = registers.pin_config[setting] + // TODO 0x10 should be based on chip config + let byte = 0 + for (let pin of this.ports[port].pins) { + byte += pin.address * pin.cfg[setting] + debug.L3(`port: ${ port } pin: ${pin.id} setting: ${ setting } reg: ${ reg } byte: ${ byte }`) + } + await this.write(portReg(reg, port), byte) + } + } + debug.L1('done writing mcp pins config') + } // end writePinsCfg + + async readPort(port, opts = {}) { + if ('AB'.indexOf(port) === -1) { + opts = port ? port : {} + port = 'A' + } + let cmd = opts.cmd ? opts.cmd : 'gpio' + let result = await this.read(portReg(registers.pin_cmd[cmd], port)) + debug.L2('read prev ', this.ports[port].state.toFmtPrev('ARY')) + debug.L2('read result', _.byteFormat(result, { in: 'DEC', out: 'ARY' })) + debug.L2('read state ', this.ports[port].state.toFmt('ARY')) + // if port pins changed without a write then update state + if (_.bitChanges(_.byteFormat(result, { in: 'DEC', out: 'ARY' }), this.ports[port].state.toFmt('ARY'))) { + this.ports[port].state.value = result + } + return opts.format ? this.ports[port].state.toFmt(opts.format) : result + } + + async readPin(pin, port, opts = {}) { + if ('AB'.indexOf(port) === -1) { + opts = port ? port : {} + port = 'A' + } + await this.readPort(port, opts) + // TODO return just true or false not the decimal of pin - will need new version on npm + return this.ports[port].state.bwOp(Math.pow(2, pin - 1), 'check') ? true : false + } + + async writePort(byte, op, port = 'A') { + // byte MUST be a decimal + debug.L2(`port:${port}, op:${op}, current pins state\n ${this.ports[port].state.toFmt('PLC') }`) + debug.L2(`byte passed \n [${ _.byteFormat(byte, { in: 'DEC', out: 'PLC'})}]:${byte}`) + if (op !== 'force') { + byte = this.ports[port].state.bwOp(byte, op) + } + debug.L2(`byte to write\n [${ _.byteFormat(byte, { in: 'DEC', out: 'PLC' })}]:${byte}\n=======`) + await this.write(portReg(registers.pin_cmd.gpio, port), byte) + //update to the saved state + this.ports[port].state.value = byte + } + + async pinsOn(pins, port, format) { + if ('AB'.indexOf(port) === -1) { + format = port + port = 'A' + } + if (format) { pins = _.byteFormat(pins, { in: format, out: 'DEC' }) } + await this.writePort(pins, 'on', port) + } + + async allOn(port = 'A') { + let pins = 255 + await this.writePort(pins, 'force', port) + } + + async pinsOff(pins, port, format) { + if ('AB'.indexOf(port) === -1) { + format = port + port = 'A' + } + if (format) { pins = _.byteFormat(pins, { in: format, out: 'DEC' }) } + await this.writePort(pins, 'off', port) + } + + async allOff(port = 'A') { + let pins = 0 + await this.writePort(pins, 'force', port) + } + + async toggle(pins, port, format) { + if ('AB'.indexOf(port) === -1) { + format = port + port = 'A' + } + if (format) { pins = _.byteFormat(pins, { in: format, out: 'DEC' }) } + await this.writePort(pins, 'toggle', port) + } + + async allToggle(port = 'A') { + let pins = 255 + await this.writePort(pins, 'toggle', port) + } + + async force(pins, port, format) { + if ('AB'.indexOf(port) === -1) { + format = port + port = 'A' + } + if (format) { pins = _.byteFormat(pins, { in: format, out: 'DEC' }) } + await this.writePort(pins, 'force', port) + } + +} // end 23008 + +class MCP23017 extends MCP23008 { + constructor(busObj, i2cAddress, opts = {}) { + super(busObj, i2cAddress, opts) + // add a second port + opts.portID = 'B' + opts.pids = opts.pidsB + this.ports.B = new Port(opts) + this.ports.B.state = new _.Byte(opts.stateB) + this.ports.B.interrupt = opts.interruptB ? opts.interruptB : opts.interrupt + } + + pin(id, port) { + if (!port) { + return this.ports.A.pin(id) ? this.ports.A.pin(id) : this.ports.B.pin(id) + } + return this.ports[port].pin(id) + } + + pid(num, port) { + console.log('pin for pid',this.ports[port].pins[num - 1]) + return this.ports[port].pins[num - 1].id + } + +} // end MCP23017 Class + +// EXPORTS +module.exports = { + MCP23008, + MCP23017, + pinConfigs +} + +// Local Functions and Settings============================= + +function portReg(reg, port) { + // TODO check chip configuartion to know what to add for port B. CUrrenly assume IOCON=1 + // index 0,1 ==== 'A','B' + if ((port === 'B') || (port === 1)) { return reg += 0x10 } else { return reg } +} + +/* ww1.microchip.com/downloads/en/DeviceDoc/21952b.pdf + * or see MCP23017.pdf and MCP 23008.pdf in /docs + * see table 1.5 and details in sections 1.6.x following + * !!! for 23017 MUST initialize with bit 7 "BANK" of IOCON = 1 for the addresses below + * then for all registers add 16 (0x10) to each reg address to talk to PortB pins + * this will make reg addresses be equilvant for 23008 and PortA of 23017 + * reg addresses in the config objects are all in Hexidecminal + */ +// Chip Configuration to be used with Register See Page 18 of 23017 doc +let chip_config = { + // byte: ['NULL','INTPOL','ODR','HAEN','DISSLW','SEQOP','MIRROR','BANK'] // see page 18 of 23017 datasheet for 8 setting details + cmd: 0x0A, // IOCON.BANK=0 (msb) at powerup so need to use 0x0A, if set to 1 then use + default: { + val: '10100010', // Split Banks port A + 0x10 = Port B,(ignored by 23008), Sequential operation disabled, active high=pulldown + fmt: 'STR' + }, + oneint: { + val: '11100100', // same as default execpt int pins connected + fmt: 'STR' + } +} + +let registers = { + pin_config: { + dir: 0, + ivrt: 1, + pullup: 6, + intr: 2, + usedef: 4, + defval: 3, + }, + pin_cmd: { + intf: 7, // readonly + intcap: 8, // readonly + gpio: 9, // read/write + olat: 10 // read only + } +} diff --git a/src/mcp230xx-packet.mjs b/src/mcp230xx-packet.mjs new file mode 100644 index 0000000..ecc49df --- /dev/null +++ b/src/mcp230xx-packet.mjs @@ -0,0 +1,58 @@ +// import Base from '@uci/base' +import Base from '../../uci-base/src/base' +import pin from './pin' +import _ from '@uci/utils/src/byte' + +import logger from '../../uci-logger/src/logger' +let log = {} +const LOG_OPTS = { + repo:'uci-mcp', + npm:'@uci/mcp', + file:'src/mcp230xx-packet.mjs', + class:'MCP230XX', + id:this.id, + instance_created:new Date().getTime() +} + +export default class MCP230XX extends Base { + constructor(opts) { + log = logger.child(LOG_OPTS) + if (opts.bus) { + if (opts.bus.host) { + opts.bus.socket = 'bus#c>t' + opts.bus.port = opts.bus.port || 1776 + } + if (opts.bus.path) opts.bus.socket = 'bus#c>n' + } else { + opts.bus = { path : (process.env.SOCKETS_DIR || __dirname) + '/i2c-bus.sock' } + opts.bus.socket = 'bus#c>n' + } + opts.nmcp = opts.nmcp || {path: (process.env.SOCKETS_DIR || __dirname) + '/mcp.sock'} + opts.sockets = 'nmcp#s>n,tmcp#s>t,'+ opts.bus.socket + console.log(opts) + super(opts) + if (!opts.address) log.fatal({opts:opts},'no i2c bus address supplied' ) + this.address = opts.address + this.pin = pin + this.busSend = this.busSend.bind(this) + + } + + async init(){ + await super.init() + } + + async busSend(packet) { + return await this.socket.bus.send(packet)} + + reply (packet) { + console.log('in bus reply', packet) + // this.emit('bus', packet) + this.emit(packet.id, packet) + } + + // setBusListener(packet) { + // // this.on(packet.cmd+Math.random().toString().slice(1)) + // } + +} // end of MCP230XX Class diff --git a/src/pin.mjs b/src/pin.mjs new file mode 100644 index 0000000..f37758d --- /dev/null +++ b/src/pin.mjs @@ -0,0 +1,56 @@ +// All the pin functions for corresponding packet cmd:'pin' +// every function should return a packet ready to send to the i2c-bus socket +// export {pin} + +const pin = { + cfg: function(packet){ + return new Promise( async (resolve) => { + console.log(packet) + return resolve() + }) + }, + test: function(packet){ + return new Promise( async (resolve, reject) => { + setTimeout(() => {reject({error:'no response from bus in 2000ms'})},2000) + let id = Math.random().toString().slice(2) + let buspacket = {cmd:'scan', id:id} + console.log('packet for bus', buspacket) + this.busSend(buspacket) + this.on(buspacket.id,function(busreply){ + console.log('reply emitted',busreply) + this.removeAllListeners(busreply.id) + let res = { cmd:'reply', _req:packet, _reqBus:buspacket} + if (busreply.response.indexOf(parseInt(this.address,16))!=-1) res.ready = true + res.address = this.address + console.log(res) + resolve(res) + }) + }) + }, + status: function(packet){ + return new Promise( async (resolve) => { + console.log(packet) + return resolve() + }) + }, + + // // state is equivalent to read + // status: packet => { + // }, + // // threse three only for output pins + // state : { + // on: packet => { + // }, + // off: packet => { + // }, + // toggle: packet => { + // } + // }, + // // will create packet to determin pin caused interrupt, packet will come from interrupt module + // interrupt: { + // find: packet =>{}, + // report: packet=>{} // come here after determining which pin to report to requester + // } +} + +export default pin diff --git a/test/gpio.test.js b/test/gpio.test.js.off similarity index 100% rename from test/gpio.test.js rename to test/gpio.test.js.off diff --git a/test/mcp.test.old.js.off b/test/mcp.test.old.js.off new file mode 100644 index 0000000..81de301 --- /dev/null +++ b/test/mcp.test.old.js.off @@ -0,0 +1,80 @@ +'use strict' + +const expect = require('chai').expect, + _u = require('@uci/utils'), + MCP = require('../lib/mcp23008-17') + +const bus = {} + +const config_sets = { + output: { + dir: 0, // 0 output,1 input + ivrt: 0, + pullup: 0, + intr: 0, // if intr = 0 usedef,deval not used + usedef: 0, // if usedef = 0 defval not used + defval: 0 + }, + toggle_switch: { + dir: 1, // 0 output,1 input + ivrt: 1, // for reading let 1 be zero and vice versa + pullup: 1, + intr: 1, // if intr = 0 usedef,deval not used + usedef: 0, // if usedef = 0 defval not used + defval: 0 + }, + momentary_switch: { + dir: 1, // 0 output,1 input + ivrt: 1, // for reading let 1 be zero and vice versa + pullup: 1, + intr: 1, // if intr = 0 usedef,deval not used + usedef: 1, // if usedef = 0 defval not used + defval: 1 + } +} + +// console.log(bus1) +// bus1.scan((devs)=>{console.log('devices: ', devs)}) +//bus1.scan().then(results=>console.log(results)) + +describe('I2C Device Classes - ', function () { + + let mcp17 = new MCP.MCP23017(bus, 0x20, { + pin_cfg_default: 'momentary_switch', + name: 'switches 1-16' + }) + + describe('MCP23017 Class - ', function () { + + it('can set and get a single pin config on both ports', function () { + expect(mcp17.pin(1).cfg, 'pin configs getter failed').to.deep.equal(config_sets.momentary_switch) + expect(mcp17.pin(8).cfg.dir, 'pin config getter failed').to.equal(1) + mcp17.pin(8).cfg.dir = 0 + expect(mcp17.pin(8).cfg.dir, 'pin address setter failed').to.equal(0) + expect(mcp17.pin(8, 'B').cfg.dir, 'pin address getter port B failed').to.equal(1) + mcp17.pin(8, 'B').cfg.dir = 0 + expect(mcp17.pin(8, 'B').config.dir, 'pin address setter failed').to.equal(0) + }) + + }) + + let mcp8 = new MCP.MCP23008(bus, 0x21, { + pin_cfg_default: 'toggle_switch', + name: 'test 1-8' + }) + + describe('MCP23008 Class - ', function () { + + it('can set and get a single pin config', function () { + expect(mcp8.pin(1).cfg, 'pin configs getter failed').to.deep.equal(config_sets.toggle_switch) + expect(mcp8.pin(8).cfg.dir, 'pin address getter failed').to.equal(1) + expect(mcp8.pin(8).cfg.ivrt, 'pin address getter failed').to.equal(1) + expect(mcp8.pin(8).cfg.usedef, 'pin address getter failed').to.equal(0) + mcp8.pin(8).cfg.dir = 0 + expect(mcp8.pin(8).cfg.dir, 'pin address setter failed').to.equal(0) + expect(mcp8.pin(8).cfg.ivrt, 'pin address getter failed').to.equal(1) + }) + + }) + +})