UNPKG

@mcampa/pca9685

Version:

PCA9685 I2C 16-channel PWM/servo driver module

452 lines (372 loc) 16.5 kB
/* * src/pca9685.ts * https://github.com/101100/pca9685 * * Library for PCA9685 I2C 16-channel PWM/servo driver. * * Copyright (c) 2015 Jason Heard * Licensed under the MIT license. */ import * as debugFactory from "debug"; import { I2CBus } from "i2c-bus"; import { Observable } from "rxjs/Observable"; import { Subject } from "rxjs/Subject"; import { Subscriber } from "rxjs/Subscriber"; import "rxjs/add/operator/concatMap"; const constants = { modeRegister1: 0x00, // MODE1 modeRegister1Default: 0x01, sleepBit: 0x10, restartBit: 0x80, modeRegister2: 0x01, // MODE2 modeRegister2Default: 0x04, channel0OnStepLowByte: 0x06, // LED0_ON_L channel0OnStepHighByte: 0x07, // LED0_ON_H channel0OffStepLowByte: 0x08, // LED0_OFF_L channel0OffStepHighByte: 0x09, // LED0_OFF_H registersPerChannel: 4, allChannelsOnStepLowByte: 0xFA, // ALL_LED_ON_L allChannelsOnStepHighByte: 0xFB, // ALL_LED_ON_H allChannelsOffStepLowByte: 0xFC, // ALL_LED_OFF_L allChannelsOffStepHighByte: 0xFD, // ALL_LED_OFF_H channelFullOnOrOff: 0x10, // must be sent to the off step high byte preScale: 0xFE, // PRE_SCALE stepsPerCycle: 4096, defaultAddress: 0x40, defaultFrequency: 50, baseClockHertz: 25000000 }; export interface Pca9685Options { /** An open I2CBus object to be used to communicate with the PCA9685 driver. */ i2c: I2CBus; /** * The I2C address of the PCA9685 driver. * * If not specified, the default address of 0x40 will be used. * * @default 0x40 */ address?: number; /** If truthy, will configure debugging messages to be printed to the console. */ debug?: boolean; /** * The frequency that should be used for the PCA9685 driver. * * If not specified, the default frequency of 50 Hz will be used. * * @default 50 */ frequency?: number; } interface I2cPacketGroup { /** The packets to send. */ packets: { /** The command code. */ command: number; /** The data byte to write. */ byte: number; }[]; /** A callback to call after the packets have been sent or an error occurs. */ callback: (error?: any) => any; } function defaultCallback(err: any): void { if (err) { console.log("Error writing to PCA8685 via I2C", err); } } export class Pca9685Driver { /** * Constructs a new PCA9685 driver. * * @param options * Configuration options for the driver. * @param callback * Callback called once the driver has been initialized. */ constructor(options: Pca9685Options, callback: (error: any) => any) { if (typeof options !== "object") { throw new Error("options must be an object."); } if (!options.i2c) { throw new Error("options.i2c must be specified."); } if (typeof callback !== "function") { throw new Error("callback must be a function."); } if (options.debug) { debugFactory.enable("pca9685"); } this.i2c = options.i2c; this.address = options.address || constants.defaultAddress; this.commandSubject = new Subject<I2cPacketGroup>(); this.debug = debugFactory("pca9685"); this.frequency = options.frequency || constants.defaultFrequency; const cycleLengthMicroSeconds = 1000000 / this.frequency; this.stepLengthMicroSeconds = cycleLengthMicroSeconds / constants.stepsPerCycle; this.send = this.send.bind(this); const sendOnePacket = (command: number, byte: number, sendCallback: (error: any) => any) => { this.i2c.writeByte(this.address, command, byte, sendCallback); }; // create a stream that will send each packet group in sequence using the async writeByte command this.commandSubject .concatMap(group => { return new Observable<void>((subscriber: Subscriber<void>) => { let nextPacket = 0; function sendNextPacket(err?: any): void { if (err) { // notify the callback of the error callback(err); // complete the stream so that the next I2C packet group can be sent subscriber.complete(); } else if (nextPacket < group.packets.length) { const thisPacket = nextPacket; nextPacket += 1; sendOnePacket(group.packets[thisPacket].command, group.packets[thisPacket].byte, sendNextPacket); } else { // notify the callback with a success (no error parameter) group.callback(); // complete the stream so that the next I2C packet group can be sent subscriber.complete(); } } sendNextPacket(); }); }) .subscribe(); this.debug("Reseting PCA9685"); // queue initialization packets this.send([ { command: constants.modeRegister1, byte: constants.modeRegister1Default }, { command: constants.modeRegister2, byte: constants.modeRegister2Default } ], sendError => { if (sendError) { callback(sendError); } else { this.allChannelsOff(offError => { if (offError) { callback(offError); } else { this.setFrequency(this.frequency, callback); } }); } }); } /** * Clean up the PCA9685 driver by turning off all channels and preventing future commands. */ dispose(): void { this.allChannelsOff(); this.commandSubject.complete(); this.commandSubject.unsubscribe(); } /** * Sets the on and off steps for the given channel. * * @param channel * Output channel to configure. * @param onStep * The step number when the channel should turn on. * @param offStep * The step number when the channel should turn off. * @param callback * Optional callback called once the on and off steps has been set for the given channel. */ setPulseRange(channel: number, onStep: number, offStep: number, callback?: (error: any) => any): void { if (typeof channel !== "number" || channel < 0 || channel > 15) { throw new Error("channel must be in the range 0 to 15."); } if (typeof onStep !== "number" || onStep < 0 || onStep > 0xFFF) { throw new Error("onStep must be in the range 0 to 4095 (0xFFF)."); } if (typeof offStep !== "number" || offStep < 0 || offStep > 0xFFF) { throw new Error("offStep must be in the range 0 to 4095 (0xFFF)."); } this.debug("Setting PWM channel, channel: %d, onStep: %d, offStep: %d", channel, onStep, offStep); this.send([ { command: constants.channel0OnStepLowByte + constants.registersPerChannel * channel, byte: onStep & 0xFF }, { command: constants.channel0OnStepHighByte + constants.registersPerChannel * channel, byte: (onStep >> 8) & 0x0F }, { command: constants.channel0OffStepLowByte + constants.registersPerChannel * channel, byte: offStep & 0xFF }, { command: constants.channel0OffStepHighByte + constants.registersPerChannel * channel, byte: (offStep >> 8) & 0x0F } ], callback || defaultCallback); } /** * Sets the pulse length for the given channel. * * @param channel * Output channel to configure. * @param pulseLengthMicroSeconds * The length of the pulse for the given channel in microseconds. * @param onStep * Optional The step number when the channel should turn on (defaults * to 0). * @param callback * Optional callback called once the pulse length has been set for the given channel. */ setPulseLength(channel: number, pulseLengthMicroSeconds: number, onStep: number = 0, callback?: (error: any) => any): void { if (typeof channel !== "number" || channel < 0 || channel > 15) { throw new Error("Channel must be in the range 0 to 15."); } if (typeof pulseLengthMicroSeconds !== "number") { throw new Error("pulseLengthMicroSeconds must be a number."); } if (onStep && (typeof onStep !== "number" || onStep < 0 || onStep > 0xFFF)) { throw new Error("onStep must be in the range 0 to 4095 (0xFFF)."); } this.debug("Setting PWM channel, channel: %d, pulseLength: %d, onStep: %d", channel, pulseLengthMicroSeconds, onStep); if (pulseLengthMicroSeconds <= 0.0) { this.channelOff(channel, callback); return; } const pulseLengthInSteps = Math.round(pulseLengthMicroSeconds / this.stepLengthMicroSeconds) - 1; if (pulseLengthInSteps > 0xFFF) { this.channelOn(channel, callback); return; } const offStep = (onStep + pulseLengthInSteps) % constants.stepsPerCycle; this.setPulseRange(channel, onStep, offStep, callback); } /** * Sets the duty cycle for the given channel. * * @param channel * Output channel to configure. * @param dutyCycleDecimalPercentage * The duty cycle for the given channel as a decimal percentage. * @param onStep * Optional The step number when the channel should turn on (defaults * to 0). * @param callback * Optional callback called once the duty cycle has been set for the given channel. */ setDutyCycle(channel: number, dutyCycleDecimalPercentage: number, onStep: number = 0, callback?: (error: any) => any): void { if (typeof channel !== "number" || channel < 0 || channel > 15) { throw new Error("Channel must be in the range 0 to 15."); } if (typeof dutyCycleDecimalPercentage !== "number") { throw new Error("dutyCycleDecimalPercentage must be a number."); } if (onStep && (typeof onStep !== "number" || onStep < 0 || onStep > 0xFFF)) { throw new Error("onStep must be in the range 0 to 4095 (0xFFF)."); } this.debug("Setting PWM channel, channel: %d, dutyCycle: %d, onStep: %d", channel, dutyCycleDecimalPercentage, onStep); if (dutyCycleDecimalPercentage <= 0.0) { this.channelOff(channel, callback); return; } else if (dutyCycleDecimalPercentage >= 1.0) { this.channelOn(channel, callback); return; } const offStep = (onStep + Math.round(dutyCycleDecimalPercentage * constants.stepsPerCycle) - 1) % constants.stepsPerCycle; this.setPulseRange(channel, onStep, offStep, callback); } /** * Turns all channels off. * * @param callback * Optional callback called once all of the channels have been turned off. */ allChannelsOff(callback?: (error: any) => any): void { this.debug("Turning off all channels"); // Setting the high byte of the all channel off step to 0x10 will turn // off all channels. this.send([ { command: constants.allChannelsOffStepHighByte, byte: constants.channelFullOnOrOff } ], callback || defaultCallback); } /** * Turns off the given channel. * * @param channel * Output channel to turn off. * @param callback * Optional callback called once the channel has been turned off. */ channelOff(channel: number, callback?: (error: any) => any): void { if (typeof channel !== "number" || channel < 0 || channel > 15) { throw new Error("Channel must be in the range 0 to 15."); } this.debug("Turning off channel: %d", channel); // Setting the high byte of the off step to 0x10 will turn off the channel. this.send([ { command: constants.channel0OffStepHighByte + constants.registersPerChannel * channel, byte: constants.channelFullOnOrOff } ], callback || defaultCallback); } /** * Turns on the given channel. * * @param channel * Output channel to turn on. * @param callback * Optional callback called once the channel has been turned on. */ channelOn(channel: number, callback?: (error: any) => any): void { if (typeof channel !== "number" || channel < 0 || channel > 15) { throw new Error("Channel must be in the range 0 to 15."); } this.debug("Turning on channel: %d", channel); // Setting the high byte of the on step to 0x10 will turn on the channel // as long as the high byte of the off step does not have the bit 0x10 set. this.send([ { command: constants.channel0OnStepHighByte + constants.registersPerChannel * channel, byte: constants.channelFullOnOrOff }, { command: constants.channel0OffStepHighByte + constants.registersPerChannel * channel, byte: 0 } ], callback || defaultCallback); } /** * Queue the given I2C packets to be sent to the PCA9685 over the I2C bus. * * @param packets * The I2C packets to send. * @param callback * Callback called once the packets have been sent or an error occurs. */ private send(packets: { command: number, byte: number}[], callback: (error: any) => any): void { this.commandSubject.next({ packets, callback }); } /** * Set the internal frequency of the PCA9685 to the given value. * * @param frequency * The new frequency value. * @param callback * Callback called once the frequency has been sent or an error occurs. */ private setFrequency(frequency: number, callback: (error: any) => void): void { // 25MHz base clock, 12 bit (4096 steps per cycle) let prescale = Math.round(constants.baseClockHertz / (constants.stepsPerCycle * frequency)) - 1; this.debug("Setting PWM frequency to %d Hz", frequency); this.debug("Pre-scale value: %d", prescale); this.i2c.readByte(this.address, constants.modeRegister1, Pca9685Driver.createSetFrequencyStep2(this.send, this.debug, prescale, callback)); } private static createSetFrequencyStep2(sendFunc: (packets: { command: number, byte: number}[], callback: (error: any) => any) => void, debug: debugFactory.IDebugger, prescale: number, callback: (error?: any) => void): (err: any, byte: number) => void { callback = typeof callback === "function" ? callback : () => { return; }; return function setFrequencyStep2(err: any, byte: number): void { if (err) { debug("Error reading mode (to set frequency)", err); callback(err); } const oldmode = byte; const newmode = (oldmode & ~constants.restartBit) | constants.sleepBit; debug("Setting prescale to: %d", prescale); sendFunc([ { command: constants.modeRegister1, byte: newmode }, { command: constants.preScale, byte: Math.floor(prescale) }, { command: constants.modeRegister1, byte: oldmode } ], sendError => { if (sendError) { callback(sendError); } else { // documentation says that 500 microseconds are required // before restart is sent, so a timeout of 10 milliseconds // should be plenty setTimeout(() => { debug("Restarting controller"); sendFunc([ { command: constants.modeRegister1, byte: oldmode | constants.restartBit } ], callback); }, 10); } }); }; } private address: number; private commandSubject: Subject<I2cPacketGroup>; private debug: debugFactory.IDebugger; private frequency: number; private i2c: I2CBus; private stepLengthMicroSeconds: number; }