'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 } }