mcp23017-promise
Version:
MCP23017 library with promises.
396 lines (361 loc) • 12.7 kB
text/typescript
import { BusInterface, Device, DeviceInterface } from "async-i2c-bus";
export const A0 = 1 << 0;
export const A1 = 1 << 1;
export const A2 = 1 << 2;
export const A3 = 1 << 3;
export const A4 = 1 << 4;
export const A5 = 1 << 5;
export const A6 = 1 << 6;
export const A7 = 1 << 7;
export const B0 = 1 << 8;
export const B1 = 1 << 9;
export const B2 = 1 << 10;
export const B3 = 1 << 11;
export const B4 = 1 << 12;
export const B5 = 1 << 13;
export const B6 = 1 << 14;
export const B7 = 1 << 15;
const PINS_A = 0xFF;
const PINS_B = 0xFF00;
/** Direction of the I/O pins */
export enum Direction {
/** Output pin */
OUT,
/** Input pin without pull-up */
IN,
/** Input pin with pull-up */
IN_UP,
}
/** Interupt mode */
export enum InteruptMode {
/** No interupt (used to reset) */
INT_None,
/** Interupt when not one */
INT_One,
/** Interupt when not zero */
INT_Zero,
/** Interupt on change */
INT_Change,
}
/** An interupt definition */
export interface InteruptDefinition {
/** Pin number */
pin: number;
/** Interupt mode of the pin */
mode: InteruptMode;
/** Callback if any */
callback?: (val: number) => void;
}
/** Interupt handler to use as an abstract type */
export interface InteruptHandler {
/** Map from pin number to callback */
[pin: number]: (val: number) => void;
}
/** Pin value */
export enum Level {
/** Low or 0 */
LOW = 0,
/** High or 1 */
HIGH,
}
/** Options for the MCP23017 object */
export interface IMCP23017Options {
/** Address of the chip on the I2C bus */
address?: number;
/** Use of the interleaved (default) or separate bank mode */
separate?: boolean;
}
interface IRegisterPair {
A: number;
B: number;
}
interface IRegisterConfiguration {
IODIR: IRegisterPair;
IPOL: IRegisterPair;
GPINTEN: IRegisterPair;
DEFVAL: IRegisterPair;
INTCON: IRegisterPair;
GPPU: IRegisterPair;
INTF: IRegisterPair;
INTCAP: IRegisterPair;
GPIO: IRegisterPair;
OLAT: IRegisterPair;
IOCON: number;
}
export const IOCON_INTPOL = 1 << 1;
export const IOCON_ODR = 1 << 2;
export const IOCON_HAEN = 1 << 3;
export const IOCON_DISSLW = 1 << 4;
export const IOCON_SEQOP = 1 << 5;
export const IOCON_MIRROR = 1 << 6;
export const IOCON_BANK = 1 << 7;
const bankSeparate: IRegisterConfiguration = {
DEFVAL: { A: 0x03, B: 0x13 },
GPINTEN: { A: 0x02, B: 0x12 },
GPIO: { A: 0x09, B: 0x19 },
GPPU: { A: 0x06, B: 0x16 },
INTCAP: { A: 0x08, B: 0x18 },
INTCON: { A: 0x04, B: 0x14 },
INTF: { A: 0x07, B: 0x17 },
IOCON: 0x05,
IODIR: { A: 0x00, B: 0x10 },
IPOL: { A: 0x01, B: 0x11 },
OLAT: { A: 0x0A, B: 0x1A },
};
const bankInterleaved: IRegisterConfiguration = {
DEFVAL: { A: 0x06, B: 0x07 },
GPINTEN: { A: 0x04, B: 0x05 },
GPIO: { A: 0x12, B: 0x13 },
GPPU: { A: 0x0C, B: 0x0D },
INTCAP: { A: 0x10, B: 0x11 },
INTCON: { A: 0x08, B: 0x09 },
INTF: { A: 0x0E, B: 0x0F },
IOCON: 0x0A,
IODIR: { A: 0x00, B: 0x01 },
IPOL: { A: 0x02, B: 0x03 },
OLAT: { A: 0x14, B: 0x15 },
};
interface IntState {
inten: number;
intcon: number;
defval: number;
handler: InteruptHandler;
}
/** Options for the driver */
export interface Options {
/** Address of the MCP (0x20 if not provided) */
address?: number;
/** Should the two banks be seaparate or interleaved (default false) */
separate?: boolean;
}
/** This class drives a single MCP23017 GPIO extender */
export class MCP23017 {
private device: DeviceInterface;
private separate: boolean;
private bank: IRegisterConfiguration;
private cache: Buffer;
/** Constructor.
*
* @param bus an instance of i2c bus. May be closed at this point
* @param options options for the driver.
*/
constructor(bus: BusInterface, options: Options = {}) {
this.device = Device({bus, address: options.address || 0x20 });
this.separate = options.separate || false;
this.bank = this.separate ? bankSeparate : bankInterleaved;
this.cache = Buffer.allocUnsafe(0);
}
/** Initialize the class
*
* Must be called with an open bus
*/
public async init(): Promise<void> {
this.cache = await this.initCache();
}
/** Reads the configuration byte of the chip
* @return the value of the configuration as a byte in a promise.
*/
public readConfigByte(): Promise<number> {
return this.device.readByte(this.bank.IOCON);
}
/** Change the way register banks are organize.
* @param separate set to true for registers organized as two
* independent banks and false for interleaved (default)
* @return a promisse fullfilled when the operation is performed.
*/
public async setSeparate(separate: boolean): Promise<void> {
const iocon = this.bank.IOCON;
if (this.separate !== separate) {
this.separate = separate;
this.bank = this.separate ? bankSeparate : bankInterleaved;
const ioconVal = await this.device.readByte(iocon);
await this.writeByte(iocon, ioconVal ^ IOCON_BANK);
this.cache = await this.initCache();
}
}
/** Sets the direction of a set of GPI/O pins and eventually pull-up status.
* @param pins: the set of pins (number used as a bit set)
* @param direction: the direction of the pin (can be IN OUT or IN_UP)
* @return a promisse fullfilled when the operation is performed.
*/
public async setDirection(
pins: number, direction: Direction,
): Promise<void> {
const iodir = this.readCache(this.bank.IODIR);
const newIodir =
direction === Direction.OUT ? iodir & ~pins : iodir | pins;
const changesIodir = newIodir ^ iodir;
await this.writeBanks(newIodir, changesIodir, this.bank.IODIR);
if (direction !== Direction.OUT) {
const pullup = this.readCache(this.bank.GPPU);
const newPullup = (
direction === Direction.IN_UP ? pullup
| pins : pullup & ~pins);
const changesPullup = pullup ^ newPullup;
await this.writeBanks(newPullup, changesPullup, this.bank.GPPU);
}
}
/** Set the polarity of the output (ie XOR with it)
* @param polarity of each pin as a bit array
*/
public async setPolarity(pins: number): Promise<void> {
const ipol = this.readCache(this.bank.IPOL);
const changes = ipol ^ pins;
await this.writeBanks(pins, changes, this.bank.IPOL);
}
/** Reads the GPIO
* @return the value of the GPIO Pin in a promise.
*/
public async read(): Promise<number> {
return await this.readBanks(true, true, this.bank.GPIO);
}
/** Modify the GPIO
* @param set: bits to set as a bit field
* @param unset: bits to set as a bit field
*/
public async write(set: number, unset: number): Promise<void> {
const gpio = this.readCache(this.bank.OLAT);
const newGpio = (gpio | set) & ~unset;
const changesGpio = gpio ^ newGpio;
await this.writeBanks(newGpio, changesGpio, this.bank.OLAT);
}
/** Register changes to the interupt handling
* @param intDefs: a list of interupt handling description that
* will be (dis)activated
* @param handler: an optional interupt handler that was in place
* @return the new interupt handler.
*/
public async register(
intDefs: InteruptDefinition[],
handler: InteruptHandler = {}): Promise<InteruptHandler> {
function addDef(
intState: IntState,
intDef: InteruptDefinition,
): IntState {
switch (intDef.mode) {
case InteruptMode.INT_None:
intState.inten &= ~intDef.pin;
delete intState.handler[intDef.pin];
break;
case InteruptMode.INT_Change:
intState.inten |= intDef.pin;
intState.intcon &= ~intDef.pin;
if (intDef.callback) {
intState.handler[intDef.pin] = intDef.callback;
}
break;
case InteruptMode.INT_Zero:
intState.inten |= intDef.pin;
intState.intcon |= intDef.pin;
intState.defval &= ~intDef.pin;
if (intDef.callback) {
intState.handler[intDef.pin] = intDef.callback;
}
break;
case InteruptMode.INT_One:
intState.inten |= intDef.pin;
intState.intcon |= intDef.pin;
intState.defval |= intDef.pin;
if (intDef.callback) {
intState.handler[intDef.pin] = intDef.callback;
}
break;
}
return intState;
}
const inten = this.readCache(this.bank.GPINTEN);
const intcon = this.readCache(this.bank.INTCON);
const defval = this.readCache(this.bank.DEFVAL);
const state = intDefs.reduce(addDef, {inten, intcon, defval, handler});
await this.writeBanks(
state.defval, defval ^ state.defval, this.bank.DEFVAL);
await this.writeBanks(
state.defval, intcon ^ state.intcon, this.bank.INTCON);
await this.writeBanks(
state.defval, inten ^ state.inten, this.bank.GPINTEN);
return state.handler;
}
/** Handle interupts received.
* @param handler: an interupt handler built during the registration.
*/
public async handle(handler: InteruptHandler): Promise<void> {
const intf = await this.readBanks(true, true, this.bank.INTF);
const intcap = await this.readBanks(true, true, this.bank.INTCAP);
let mask = 1;
for (let pin = 0; pin < 16; pin++) {
if (mask & intf) {
const callback = handler[pin];
if (callback) {
callback(mask & intcap ? Level.HIGH : Level.LOW);
}
}
mask <<= 1;
}
}
private readCache(regs: IRegisterPair): number {
return this.cache[regs.A] | (this.cache[regs.B] << 8);
}
private async initCache(): Promise<Buffer> {
const buffer = Buffer.alloc(0x1B);
if (this.separate) {
const bufA = await this.readBuffer(0, 0xA);
const bufB = await this.readBuffer(0x10, 0xA);
bufA.copy(buffer, 0); bufB.copy(buffer, 0x10);
} else {
const buf = await this.readBuffer(0, 0x16);
buf.copy(buffer, 0);
}
return buffer;
}
private async readBuffer(register: number, len: number): Promise<Buffer> {
const buffer = Buffer.alloc(len);
const lenRes = await this.device.readI2cBlock(register, len, buffer);
if (lenRes !== len) {
throw new Error("Bad length");
}
return buffer;
}
private async writeByte(register: number, byte: number): Promise<void> {
await this.device.writeByte(register, byte);
this.cache[register] = byte;
}
private async writeWord(register: number, word: number): Promise<void> {
await this.device.writeWord(register, word);
this.cache[register] = word & 0xff;
this.cache[register + 1] = (word >> 8) & 0xff;
}
private async writeBanks(
pins: number, changes: number,
regs: IRegisterPair): Promise<void> {
const needsA = (changes & PINS_A) !== 0;
const needsB = (changes & PINS_B) !== 0;
if (this.separate) {
if (needsA) {
await this.writeByte(regs.A, pins & PINS_A);
}
if (needsB) {
await this.writeByte(regs.B, (pins & PINS_B) >> 8);
}
} else {
await this.writeWord(regs.A, pins & 0xFFFF);
}
}
private async readBanks(
needsA: boolean,
needsB: boolean,
regs: IRegisterPair): Promise<number> {
if (this.separate) {
let valA = 0;
if (needsA) {
valA = await this.device.readByte(regs.A);
}
let valB = 0;
if (needsB) {
valB = await this.device.readByte(regs.B);
}
return valA | (valB << 8);
} else {
return await this.device.readWord(regs.A);
}
}
}