UNPKG

node-red-node-arduino

Version:

A Node-RED Node, to talk to a chip as I/O extender, which is running Firmata firmware

1,841 lines (1,582 loc) 71.4 kB
"use strict"; // Built-in Dependencies const Emitter = require("events"); // Internal Dependencies const Encoder7Bit = require("./encoder7bit"); const OneWire = require("./onewireutils"); // Program specifics const i2cActive = new Map(); /** * constants */ const ANALOG_MAPPING_QUERY = 0x69; const ANALOG_MAPPING_RESPONSE = 0x6a; const ANALOG_MESSAGE = 0xe0; const CAPABILITY_QUERY = 0x6b; const CAPABILITY_RESPONSE = 0x6c; const DIGITAL_MESSAGE = 0x90; const END_SYSEX = 0xf7; const EXTENDED_ANALOG = 0x6f; const I2C_CONFIG = 0x78; const I2C_REPLY = 0x77; const I2C_REQUEST = 0x76; const I2C_READ_MASK = 0x18; // 0b00011000 // const I2C_END_TX_MASK = 0x40; // 0b01000000 const ONEWIRE_CONFIG_REQUEST = 0x41; const ONEWIRE_DATA = 0x73; const ONEWIRE_DELAY_REQUEST_BIT = 0x10; const ONEWIRE_READ_REPLY = 0x43; const ONEWIRE_READ_REQUEST_BIT = 0x08; const ONEWIRE_RESET_REQUEST_BIT = 0x01; const ONEWIRE_SEARCH_ALARMS_REPLY = 0x45; const ONEWIRE_SEARCH_ALARMS_REQUEST = 0x44; const ONEWIRE_SEARCH_REPLY = 0x42; const ONEWIRE_SEARCH_REQUEST = 0x40; const ONEWIRE_WITHDATA_REQUEST_BITS = 0x3c; const ONEWIRE_WRITE_REQUEST_BIT = 0x20; const PIN_MODE = 0xf4; const PIN_STATE_QUERY = 0x6d; const PIN_STATE_RESPONSE = 0x6e; const PING_READ = 0x75; // const PULSE_IN = 0x74; // const PULSE_OUT = 0x73; const QUERY_FIRMWARE = 0x79; const REPORT_ANALOG = 0xc0; const REPORT_DIGITAL = 0xd0; const REPORT_VERSION = 0xf9; const SAMPLING_INTERVAL = 0x7a; const SERVO_CONFIG = 0x70; const SERIAL_MESSAGE = 0x60; const SERIAL_CONFIG = 0x10; const SERIAL_WRITE = 0x20; const SERIAL_READ = 0x30; const SERIAL_REPLY = 0x40; const SERIAL_CLOSE = 0x50; const SERIAL_FLUSH = 0x60; const SERIAL_LISTEN = 0x70; const START_SYSEX = 0xf0; const STEPPER = 0x72; const ACCELSTEPPER = 0x62; const STRING_DATA = 0x71; const SYSTEM_RESET = 0xff; const MAX_PIN_COUNT = 128; const SYM_sendOneWireSearch = Symbol("sendOneWireSearch"); const SYM_sendOneWireRequest = Symbol("sendOneWireRequest"); /** * MIDI_RESPONSE contains functions to be called when we receive a MIDI message from the arduino. * used as a switch object as seen here http://james.padolsey.com/javascript/how-to-avoid-switch-case-syndrome/ * @private */ const MIDI_RESPONSE = { /** * Handles a REPORT_VERSION response and emits the reportversion event. * @private * @param {Board} board the current arduino board we are working with. */ [REPORT_VERSION](board) { board.version.major = board.buffer[1]; board.version.minor = board.buffer[2]; board.emit("reportversion"); }, /** * Handles a ANALOG_MESSAGE response and emits "analog-read" and "analog-read-"+n events where n is the pin number. * @private * @param {Board} board the current arduino board we are working with. */ [ANALOG_MESSAGE](board) { const pin = board.buffer[0] & 0x0f; const value = board.buffer[1] | (board.buffer[2] << 7); /* istanbul ignore else */ if (board.pins[board.analogPins[pin]]) { board.pins[board.analogPins[pin]].value = value; } board.emit(`analog-read-${pin}`, value); board.emit("analog-read", { pin, value, }); }, /** * Handles a DIGITAL_MESSAGE response and emits: * "digital-read" * "digital-read-"+n * * Where n is the pin number. * * @private * @param {Board} board the current arduino board we are working with. */ [DIGITAL_MESSAGE](board) { const port = board.buffer[0] & 0x0f; const portValue = board.buffer[1] | (board.buffer[2] << 7); for (let i = 0; i < 8; i++) { const pin = 8 * port + i; const pinRec = board.pins[pin]; const bit = 1 << i; if ( pinRec && (pinRec.mode === board.MODES.INPUT || pinRec.mode === board.MODES.PULLUP) ) { pinRec.value = (portValue >> (i & 0x07)) & 0x01; if (pinRec.value) { board.ports[port] |= bit; } else { board.ports[port] &= ~bit; } let { value } = pinRec; board.emit(`digital-read-${pin}`, value); board.emit("digital-read", { pin, value, }); } } }, }; /** * SYSEX_RESPONSE contains functions to be called when we receive a SYSEX message from the arduino. * used as a switch object as seen here http://james.padolsey.com/javascript/how-to-avoid-switch-case-syndrome/ * @private */ const SYSEX_RESPONSE = { /** * Handles a QUERY_FIRMWARE response and emits the "queryfirmware" event * @private * @param {Board} board the current arduino board we are working with. */ [QUERY_FIRMWARE](board) { const length = board.buffer.length - 2; const buffer = Buffer.alloc(Math.round((length - 4) / 2)); let byte = 0; let offset = 0; for (let i = 4; i < length; i += 2) { byte = ((board.buffer[i] & 0x7f) | ((board.buffer[i + 1] & 0x7f) << 7)) & 0xff; buffer.writeUInt8(byte, offset++); } (board.firmware = { name: buffer.toString(), version: { major: board.buffer[2], minor: board.buffer[3], }, }), board.emit("queryfirmware"); }, /** * Handles a CAPABILITY_RESPONSE response and emits the "capability-query" event * @private * @param {Board} board the current arduino board we are working with. */ [CAPABILITY_RESPONSE](board) { const modes = Object.keys(board.MODES).map((key) => board.MODES[key]); let mode, resolution; let capability = 0; function supportedModes(capability) { return modes.reduce((accum, mode) => { if (capability & (1 << mode)) { accum.push(mode); } return accum; }, []); } // Only create pins if none have been previously created on the instance. if (!board.pins.length) { for (let i = 2, n = 0; i < board.buffer.length - 1; i++) { if (board.buffer[i] === 0x7f) { board.pins.push({ supportedModes: supportedModes(capability), mode: undefined, value: 0, report: 1, }); capability = 0; n = 0; continue; } if (n === 0) { mode = board.buffer[i]; resolution = (1 << board.buffer[i + 1]) - 1; capability |= 1 << mode; // ADC Resolution of Analog Inputs if (mode === board.MODES.ANALOG && board.RESOLUTION.ADC === null) { board.RESOLUTION.ADC = resolution; } // PWM Resolution of PWM Outputs if (mode === board.MODES.PWM && board.RESOLUTION.PWM === null) { board.RESOLUTION.PWM = resolution; } // DAC Resolution of DAC Outputs // if (mode === board.MODES.DAC && board.RESOLUTION.DAC === null) { // board.RESOLUTION.DAC = resolution; // } } n ^= 1; } } board.emit("capability-query"); }, /** * Handles a PIN_STATE response and emits the 'pin-state-'+n event where n is the pin number. * * Note about pin state: For output modes, the state is any value that has been * previously written to the pin. For input modes, the state is the status of * the pullup resistor. * @private * @param {Board} board the current arduino board we are working with. */ [PIN_STATE_RESPONSE](board) { let pin = board.buffer[2]; board.pins[pin].mode = board.buffer[3]; board.pins[pin].state = board.buffer[4]; if (board.buffer.length > 6) { board.pins[pin].state |= board.buffer[5] << 7; } if (board.buffer.length > 7) { board.pins[pin].state |= board.buffer[6] << 14; } board.emit(`pin-state-${pin}`); }, /** * Handles a ANALOG_MAPPING_RESPONSE response and emits the "analog-mapping-query" event. * @private * @param {Board} board the current arduino board we are working with. */ [ANALOG_MAPPING_RESPONSE](board) { let pin = 0; let currentValue; for (let i = 2; i < board.buffer.length - 1; i++) { currentValue = board.buffer[i]; board.pins[pin].analogChannel = currentValue; if (currentValue !== 127) { board.analogPins.push(pin); } pin++; } board.emit("analog-mapping-query"); }, /** * Handles a I2C_REPLY response and emits the "I2C-reply-"+n event where n is the slave address of the I2C device. * The event is passed the buffer of data sent from the I2C Device * @private * @param {Board} board the current arduino board we are working with. */ [I2C_REPLY](board) { const reply = []; const address = (board.buffer[2] & 0x7f) | ((board.buffer[3] & 0x7f) << 7); const register = (board.buffer[4] & 0x7f) | ((board.buffer[5] & 0x7f) << 7); for (let i = 6, length = board.buffer.length - 1; i < length; i += 2) { reply.push(board.buffer[i] | (board.buffer[i + 1] << 7)); } board.emit(`I2C-reply-${address}-${register}`, reply); }, [ONEWIRE_DATA](board) { const subCommand = board.buffer[2]; if (!SYSEX_RESPONSE[subCommand]) { return; } SYSEX_RESPONSE[subCommand](board); }, [ONEWIRE_SEARCH_REPLY](board) { const pin = board.buffer[3]; const buffer = board.buffer.slice(4, board.buffer.length - 1); board.emit(`1-wire-search-reply-${pin}`, OneWire.readDevices(buffer)); }, [ONEWIRE_SEARCH_ALARMS_REPLY](board) { const pin = board.buffer[3]; const buffer = board.buffer.slice(4, board.buffer.length - 1); board.emit( `1-wire-search-alarms-reply-${pin}`, OneWire.readDevices(buffer) ); }, [ONEWIRE_READ_REPLY](board) { const encoded = board.buffer.slice(4, board.buffer.length - 1); const decoded = Encoder7Bit.from7BitArray(encoded); const correlationId = (decoded[1] << 8) | decoded[0]; board.emit(`1-wire-read-reply-${correlationId}`, decoded.slice(2)); }, /** * Handles a STRING_DATA response and logs the string to the console. * @private * @param {Board} board the current arduino board we are working with. */ [STRING_DATA](board) { board.emit( "string", Buffer.from(board.buffer.slice(2, -1)).toString().replace(/\0/g, "") ); }, /** * Response from pingRead */ [PING_READ](board) { const pin = (board.buffer[2] & 0x7f) | ((board.buffer[3] & 0x7f) << 7); const durationBuffer = [ (board.buffer[4] & 0x7f) | ((board.buffer[5] & 0x7f) << 7), (board.buffer[6] & 0x7f) | ((board.buffer[7] & 0x7f) << 7), (board.buffer[8] & 0x7f) | ((board.buffer[9] & 0x7f) << 7), (board.buffer[10] & 0x7f) | ((board.buffer[11] & 0x7f) << 7), ]; const duration = (durationBuffer[0] << 24) + (durationBuffer[1] << 16) + (durationBuffer[2] << 8) + durationBuffer[3]; board.emit(`ping-read-${pin}`, duration); }, /** * Handles the message from a stepper completing move * @param {Board} board */ [STEPPER](board) { const deviceNum = board.buffer[2]; board.emit(`stepper-done-${deviceNum}`, true); }, /** * Handles the message from a stepper or group of steppers completing move * @param {Board} board */ [ACCELSTEPPER](board) { const command = board.buffer[2]; const deviceNum = board.buffer[3]; const value = command === 0x06 || command === 0x0a ? decode32BitSignedInteger(board.buffer.slice(4, 9)) : null; if (command === 0x06) { board.emit(`stepper-position-${deviceNum}`, value); } if (command === 0x0a) { board.emit(`stepper-done-${deviceNum}`, value); } if (command === 0x24) { board.emit(`multi-stepper-done-${deviceNum}`); } }, /** * Handles a SERIAL_REPLY response and emits the "serial-data-"+n event where n is the id of the * serial port. * The event is passed the buffer of data sent from the serial device * @private * @param {Board} board the current arduino board we are working with. */ [SERIAL_MESSAGE](board) { const command = board.buffer[2] & START_SYSEX; const portId = board.buffer[2] & 0x0f; const reply = []; /* istanbul ignore else */ if (command === SERIAL_REPLY) { for (let i = 3, len = board.buffer.length; i < len - 1; i += 2) { reply.push((board.buffer[i + 1] << 7) | board.buffer[i]); } board.emit(`serial-data-${portId}`, reply); } }, }; /** * The default transport class */ let Transport = null; /** * @class The Board object represents an arduino board. * @augments EventEmitter * @param {String} port This is the serial port the arduino is connected to. * @param {function} function A function to be called when the arduino is ready to communicate. * @property MODES All the modes available for pins on this arduino board. * @property I2C_MODES All the I2C modes available. * @property SERIAL_MODES All the Serial modes available. * @property SERIAL_PORT_ID ID values to pass as the portId parameter when calling serialConfig. * @property HIGH A constant to set a pins value to HIGH when the pin is set to an output. * @property LOW A constant to set a pins value to LOW when the pin is set to an output. * @property pins An array of pin object literals. * @property analogPins An array of analog pins and their corresponding indexes in the pins array. * @property version An object indicating the major and minor version of the firmware currently running. * @property firmware An object indicating the name, major and minor version of the firmware currently running. * @property buffer An array holding the current bytes received from the arduino. * @property {SerialPort} sp The serial port object used to communicate with the arduino. */ class Firmata extends Emitter { constructor(port, options, callback) { super(); if (typeof options === "function" || typeof options === "undefined") { callback = options; options = {}; } const board = this; const defaults = { reportVersionTimeout: 5000, samplingInterval: 19, serialport: { baudRate: 57600, // https://github.com/node-serialport/node-serialport/blob/5.0.0/UPGRADE_GUIDE.md#open-options highWaterMark: 256, path: port, }, }; const settings = Object.assign({}, defaults, options); this.isReady = false; this.MODES = { INPUT: 0x00, OUTPUT: 0x01, ANALOG: 0x02, PWM: 0x03, SERVO: 0x04, SHIFT: 0x05, I2C: 0x06, ONEWIRE: 0x07, STEPPER: 0x08, SERIAL: 0x0a, PULLUP: 0x0b, IGNORE: 0x7f, PING_READ: 0x75, UNKOWN: 0x10, }; this.I2C_MODES = { WRITE: 0, READ: 1, CONTINUOUS_READ: 2, STOP_READING: 3, }; this.STEPPER = { TYPE: { DRIVER: 1, TWO_WIRE: 2, THREE_WIRE: 3, FOUR_WIRE: 4, }, STEP_SIZE: { WHOLE: 0, HALF: 1, }, RUN_STATE: { STOP: 0, ACCEL: 1, DECEL: 2, RUN: 3, }, DIRECTION: { CCW: 0, CW: 1, }, }; this.SERIAL_MODES = { CONTINUOUS_READ: 0x00, STOP_READING: 0x01, }; // ids for hardware and software serial ports on the board this.SERIAL_PORT_IDs = { HW_SERIAL0: 0x00, HW_SERIAL1: 0x01, HW_SERIAL2: 0x02, HW_SERIAL3: 0x03, SW_SERIAL0: 0x08, SW_SERIAL1: 0x09, SW_SERIAL2: 0x10, SW_SERIAL3: 0x11, // Default can be used by dependant libraries to key on a // single property name when negotiating ports. // // Firmata elects SW_SERIAL0: 0x08 as its DEFAULT DEFAULT: 0x08, }; // map to the pin resolution value in the capability query response this.SERIAL_PIN_TYPES = { RES_RX0: 0x00, RES_TX0: 0x01, RES_RX1: 0x02, RES_TX1: 0x03, RES_RX2: 0x04, RES_TX2: 0x05, RES_RX3: 0x06, RES_TX3: 0x07, }; this.RESOLUTION = { ADC: null, DAC: null, PWM: null, }; this.HIGH = 1; this.LOW = 0; this.pins = []; this.ports = Array(16).fill(0); this.analogPins = []; this.version = {}; this.firmware = {}; this.buffer = []; this.versionReceived = false; this.name = "Firmata"; this.settings = settings; this.pending = 0; this.digitalPortQueue = 0x0000; if (typeof port === "object") { this.transport = port; } else { if (!Transport) { throw new Error("Missing Default Transport"); } this.transport = new Transport(settings.serialport); } this.transport.on("close", (event) => { // https://github.com/node-serialport/node-serialport/blob/5.0.0/UPGRADE_GUIDE.md#opening-and-closing if (event && event.disconnected) { this.emit("disconnect"); return; } this.emit("close"); }); this.transport.on("open", (event) => { this.emit("open", event); // Legacy this.emit("connect", event); }); this.transport.on("error", (error) => { if (!this.isReady && typeof callback === "function") { callback(error); } else { this.emit("error", error); } }); this.transport.on("data", (data) => { for (let i = 0; i < data.length; i++) { let byte = data[i]; // we dont want to push 0 as the first byte on our buffer if (this.buffer.length === 0 && byte === 0) { continue; } else { this.buffer.push(byte); let first = this.buffer[0]; let last = this.buffer[this.buffer.length - 1]; // [START_SYSEX, ... END_SYSEX] if (first === START_SYSEX && last === END_SYSEX) { let handler = SYSEX_RESPONSE[this.buffer[1]]; // Ensure a valid SYSEX_RESPONSE handler exists // Only process these AFTER the REPORT_VERSION // message has been received and processed. if (handler && this.versionReceived) { handler(this); } // It is possible for the board to have // existing activity from a previous run // that will leave any of the following // active: // // - ANALOG_MESSAGE // - SERIAL_READ // - I2C_REQUEST, CONTINUOUS_READ // // This means that we will receive these // messages on transport "open", before any // handshake can occur. We MUST assert // that we will only process this buffer // AFTER the REPORT_VERSION message has // been received. Not doing so will result // in the appearance of the program "hanging". // // Since we cannot do anything with this data // until _after_ REPORT_VERSION, discard it. // this.buffer.length = 0; } else if (first === START_SYSEX && this.buffer.length > 0) { // we have a new command after an incomplete sysex command let currByte = data[i]; if (currByte > 0x7f) { this.buffer.length = 0; this.buffer.push(currByte); } } else { /* istanbul ignore else */ if (first !== START_SYSEX) { // Check if data gets out of sync: first byte in buffer // must be a valid response if not START_SYSEX // Identify response on first byte let response = first < START_SYSEX ? first & START_SYSEX : first; // Check if the first byte is possibly // a valid MIDI_RESPONSE (handler) /* istanbul ignore else */ if ( response !== REPORT_VERSION && response !== ANALOG_MESSAGE && response !== DIGITAL_MESSAGE ) { // If not valid, then we received garbage and can discard // whatever bytes have been been queued. this.buffer.length = 0; } } } // There are 3 bytes in the buffer and the first is not START_SYSEX: // Might have a MIDI Command if (this.buffer.length === 3 && first !== START_SYSEX) { // response bytes under 0xF0 we have a multi byte operation let response = first < START_SYSEX ? first & START_SYSEX : first; /* istanbul ignore else */ if (MIDI_RESPONSE[response]) { // It's ok that this.versionReceived will be set to // true every time a valid MIDI_RESPONSE is received. // This condition is necessary to ensure that REPORT_VERSION // is called first. if (this.versionReceived || first === REPORT_VERSION) { this.versionReceived = true; MIDI_RESPONSE[response](this); } this.buffer.length = 0; } else { // A bad serial read must have happened. // Reseting the buffer will allow recovery. this.buffer.length = 0; } } } } }); // if we have not received the version within the allotted // time specified by the reportVersionTimeout (user or default), // then send an explicit request for it. this.reportVersionTimeoutId = setTimeout(() => { /* istanbul ignore else */ if (this.versionReceived === false) { this.reportVersion(function () {}); this.queryFirmware(function () {}); } }, settings.reportVersionTimeout); function ready() { board.isReady = true; board.emit("ready"); /* istanbul ignore else */ if (typeof callback === "function") { callback(); } } // Await the reported version. this.once("reportversion", () => { clearTimeout(this.reportVersionTimeoutId); this.versionReceived = true; this.once("queryfirmware", () => { // Only preemptively set the sampling interval if `samplingInterval` // property was _explicitly_ set as a constructor option. if (options.samplingInterval !== undefined) { this.setSamplingInterval(options.samplingInterval); } if (settings.skipCapabilities) { this.analogPins = settings.analogPins || this.analogPins; this.pins = settings.pins || this.pins; /* istanbul ignore else */ if (!this.pins.length) { for (var i = 0; i < (settings.pinCount || MAX_PIN_COUNT); i++) { var supportedModes = []; var analogChannel = this.analogPins.indexOf(i); if (analogChannel < 0) { analogChannel = 127; } this.pins.push({ supportedModes, analogChannel }); } } // If the capabilities query is skipped, // default resolution values will be used. // // Based on ATmega328/P // this.RESOLUTION.ADC = 0x3ff; this.RESOLUTION.PWM = 0x0ff; ready(); } else { this.queryCapabilities(() => { this.queryAnalogMapping(ready); }); } }); }); } /** * Asks the arduino to tell us its version. * @param {function} callback A function to be called when the arduino has reported its version. */ reportVersion(callback) { this.once("reportversion", callback); writeToTransport(this, [REPORT_VERSION]); } /** * Asks the arduino to tell us its firmware version. * @param {function} callback A function to be called when the arduino has reported its firmware version. */ queryFirmware(callback) { this.once("queryfirmware", callback); writeToTransport(this, [START_SYSEX, QUERY_FIRMWARE, END_SYSEX]); } /** * Asks the arduino to read analog data. Turn on reporting for this pin. * @param {number} pin The pin to read analog data * @param {function} callback A function to call when we have the analag data. */ analogRead(pin, callback) { this.reportAnalogPin(pin, 1); this.addListener(`analog-read-${pin}`, callback); } /** * Write a PWM value Asks the arduino to write an analog message. * @param {number} pin The pin to write analog data to. * @param {number} value The data to write to the pin between 0 and this.RESOLUTION.PWM. */ pwmWrite(pin, value) { let data; this.pins[pin].value = value; if (pin > 15) { data = [ START_SYSEX, EXTENDED_ANALOG, pin, value & 0x7f, (value >> 7) & 0x7f, ]; if (value > 0x00004000) { data[data.length] = (value >> 14) & 0x7f; } if (value > 0x00200000) { data[data.length] = (value >> 21) & 0x7f; } if (value > 0x10000000) { data[data.length] = (value >> 28) & 0x7f; } data[data.length] = END_SYSEX; } else { data = [ANALOG_MESSAGE | pin, value & 0x7f, (value >> 7) & 0x7f]; } writeToTransport(this, data); } /** * Set a pin to SERVO mode with an explicit PWM range. * * @param {number} pin The pin the servo is connected to * @param {number} min A 14-bit signed int. * @param {number} max A 14-bit signed int. */ servoConfig(pin, min, max) { if (typeof pin === "object" && pin !== null) { let temp = pin; pin = temp.pin; min = temp.min; max = temp.max; } if (typeof pin === "undefined") { throw new Error("servoConfig: pin must be specified"); } if (typeof min === "undefined") { throw new Error("servoConfig: min must be specified"); } if (typeof max === "undefined") { throw new Error("servoConfig: max must be specified"); } // [0] START_SYSEX (0xF0) // [1] SERVO_CONFIG (0x70) // [2] pin number (0-127) // [3] minPulse LSB (0-6) // [4] minPulse MSB (7-13) // [5] maxPulse LSB (0-6) // [6] maxPulse MSB (7-13) // [7] END_SYSEX (0xF7) this.pins[pin].mode = this.MODES.SERVO; writeToTransport(this, [ START_SYSEX, SERVO_CONFIG, pin, min & 0x7f, (min >> 7) & 0x7f, max & 0x7f, (max >> 7) & 0x7f, END_SYSEX, ]); } /** * Asks the arduino to move a servo * @param {number} pin The pin the servo is connected to * @param {number} value The degrees to move the servo to. */ servoWrite(...args) { // Values less than 544 will be treated as angles in degrees // (valid values in microseconds are handled as microseconds) this.analogWrite(...args); } /** * Asks the arduino to set the pin to a certain mode. * @param {number} pin The pin you want to change the mode of. * @param {number} mode The mode you want to set. Must be one of board.MODES */ pinMode(pin, mode) { if (mode === this.MODES.ANALOG) { // Because pinMode may be called before analogRead(pin, () => {}), but isn't // necessary to initiate an analog read on an analog pin, we'll assign the // mode here, but do nothing further. In analogRead(), the call to // reportAnalogPin(pin, 1) is all that's needed to turn on analog input // reading. // // reportAnalogPin(...) will reconcile the pin number as well, the // same operation we use here to assign a "mode": this.pins[this.analogPins[pin]].mode = mode; } else { this.pins[pin].mode = mode; writeToTransport(this, [PIN_MODE, pin, mode]); } } /** * Asks the arduino to write a value to a digital pin * @param {number} pin The pin you want to write a value to. * @param {number} value The value you want to write. Must be board.HIGH or board.LOW * @param {boolean} enqueue When true, the local state is updated but the command is not sent to the Arduino */ digitalWrite(pin, value, enqueue) { let port = this.updateDigitalPort(pin, value); if (enqueue) { this.digitalPortQueue |= 1 << port; } else { this.writeDigitalPort(port); } } /** * Update local store of digital port state * @param {number} pin The pin you want to write a value to. * @param {number} value The value you want to write. Must be board.HIGH or board.LOW */ updateDigitalPort(pin, value) { const port = pin >> 3; const bit = 1 << (pin & 0x07); this.pins[pin].value = value; if (value) { this.ports[port] |= bit; } else { this.ports[port] &= ~bit; } return port; } /** * Write queued digital ports */ flushDigitalPorts() { for (let i = 0; i < this.ports.length; i++) { if (this.digitalPortQueue >> i) { this.writeDigitalPort(i); } } this.digitalPortQueue = 0x0000; } /** * Update a digital port (group of 8 digital pins) on the Arduino * @param {number} port The port you want to update. */ writeDigitalPort(port) { writeToTransport(this, [ DIGITAL_MESSAGE | port, this.ports[port] & 0x7f, (this.ports[port] >> 7) & 0x7f, ]); } /** * Asks the arduino to read digital data. Turn on reporting for this pin's port. * * @param {number} pin The pin to read data from * @param {function} callback The function to call when data has been received */ digitalRead(pin, callback) { this.reportDigitalPin(pin, 1); this.addListener(`digital-read-${pin}`, callback); } /** * Asks the arduino to tell us its capabilities * @param {function} callback A function to call when we receive the capabilities */ queryCapabilities(callback) { this.once("capability-query", callback); writeToTransport(this, [START_SYSEX, CAPABILITY_QUERY, END_SYSEX]); } /** * Asks the arduino to tell us its analog pin mapping * @param {function} callback A function to call when we receive the pin mappings. */ queryAnalogMapping(callback) { this.once("analog-mapping-query", callback); writeToTransport(this, [START_SYSEX, ANALOG_MAPPING_QUERY, END_SYSEX]); } /** * Asks the arduino to tell us the current state of a pin * @param {number} pin The pin we want to the know the state of * @param {function} callback A function to call when we receive the pin state. */ queryPinState(pin, callback) { this.once(`pin-state-${pin}`, callback); writeToTransport(this, [START_SYSEX, PIN_STATE_QUERY, pin, END_SYSEX]); } /** * Sends a string to the arduino * @param {String} string to send to the device */ sendString(string) { const bytes = Buffer.from(`${string}\0`, "utf8"); const data = []; data.push(START_SYSEX, STRING_DATA); for (let i = 0, length = bytes.length; i < length; i++) { data.push(bytes[i] & 0x7f, (bytes[i] >> 7) & 0x7f); } data.push(END_SYSEX); writeToTransport(this, data); } /** * Sends a I2C config request to the arduino board with an optional * value in microseconds to delay an I2C Read. Must be called before * an I2C Read or Write * @param {number} delay in microseconds to set for I2C Read */ sendI2CConfig(delay) { return this.i2cConfig(delay); } /** * Enable I2C with an optional read delay. Must be called before * an I2C Read or Write * * Supersedes sendI2CConfig * * @param {number} delay in microseconds to set for I2C Read * * or * * @param {object} with a single property `delay` */ i2cConfig(options) { let settings = i2cActive.get(this); let delay; if (!settings) { settings = { /* Keys will be I2C peripheral addresses */ }; i2cActive.set(this, settings); } if (typeof options === "number") { delay = options; } else { if (typeof options === "object" && options !== null) { delay = Number(options.delay); // When an address was explicitly specified, there may also be // peripheral specific instructions in the config. if (typeof options.address !== "undefined") { if (!settings[options.address]) { settings[options.address] = { stopTX: true, }; } } // When settings have been explicitly provided, just bulk assign // them to the existing settings, even if that's empty. This // allows for reconfiguration as needed. if (typeof options.settings !== "undefined") { Object.assign(settings[options.address], options.settings); /* - stopTX: true | false Set `stopTX` to `false` if this peripheral expects Wire to keep the transmission connection alive between setting a register and requesting bytes. Defaults to `true`. */ } } } settings.delay = delay = delay || 0; i2cRequest(this, [ START_SYSEX, I2C_CONFIG, delay & 0xff, (delay >> 8) & 0xff, END_SYSEX, ]); return this; } /** * Asks the arduino to send an I2C request to a device * @param {number} slaveAddress The address of the I2C device * @param {Array} bytes The bytes to send to the device */ sendI2CWriteRequest(slaveAddress, bytes) { const data = []; /* istanbul ignore next */ bytes = bytes || []; data.push( START_SYSEX, I2C_REQUEST, slaveAddress, this.I2C_MODES.WRITE << 3 ); for (let i = 0, length = bytes.length; i < length; i++) { data.push(bytes[i] & 0x7f, (bytes[i] >> 7) & 0x7f); } data.push(END_SYSEX); i2cRequest(this, data); } /** * Write data to a register * * @param {number} address The address of the I2C device. * @param {Array} cmdRegOrData An array of bytes * * Write a command to a register * * @param {number} address The address of the I2C device. * @param {number} cmdRegOrData The register * @param {Array} inBytes An array of bytes * */ i2cWrite(address, registerOrData, inBytes) { /** * registerOrData: * [... arbitrary bytes] * * or * * registerOrData, inBytes: * command [, ...] * */ const data = [START_SYSEX, I2C_REQUEST, address, this.I2C_MODES.WRITE << 3]; // If i2cWrite was used for an i2cWriteReg call... if ( arguments.length === 3 && !Array.isArray(registerOrData) && !Array.isArray(inBytes) ) { return this.i2cWriteReg(address, registerOrData, inBytes); } // Fix arguments if called with Firmata.js API if (arguments.length === 2) { if (Array.isArray(registerOrData)) { inBytes = registerOrData.slice(); registerOrData = inBytes.shift(); } else { inBytes = []; } } const bytes = Buffer.from([registerOrData].concat(inBytes)); for (var i = 0, length = bytes.length; i < length; i++) { data.push(bytes[i] & 0x7f, (bytes[i] >> 7) & 0x7f); } data.push(END_SYSEX); i2cRequest(this, data); return this; } /** * Write data to a register * * @param {number} address The address of the I2C device. * @param {number} register The register. * @param {number} byte The byte value to write. * */ i2cWriteReg(address, register, byte) { i2cRequest(this, [ START_SYSEX, I2C_REQUEST, address, this.I2C_MODES.WRITE << 3, // register register & 0x7f, (register >> 7) & 0x7f, // byte byte & 0x7f, (byte >> 7) & 0x7f, END_SYSEX, ]); return this; } /** * Asks the arduino to request bytes from an I2C device * @param {number} slaveAddress The address of the I2C device * @param {number} numBytes The number of bytes to receive. * @param {function} callback A function to call when we have received the bytes. */ sendI2CReadRequest(address, numBytes, callback) { i2cRequest(this, [ START_SYSEX, I2C_REQUEST, address, this.I2C_MODES.READ << 3, numBytes & 0x7f, (numBytes >> 7) & 0x7f, END_SYSEX, ]); this.once(`I2C-reply-${address}-0`, callback); } // TODO: Refactor i2cRead and i2cReadOnce // to share most operations. /** * Initialize a continuous I2C read. * * @param {number} address The address of the I2C device * @param {number} register Optionally set the register to read from. * @param {number} numBytes The number of bytes to receive. * @param {function} callback A function to call when we have received the bytes. */ i2cRead(address, register, bytesToRead, callback) { if ( arguments.length === 3 && typeof register === "number" && typeof bytesToRead === "function" ) { callback = bytesToRead; bytesToRead = register; register = null; } const data = [ START_SYSEX, I2C_REQUEST, address, this.I2C_MODES.CONTINUOUS_READ << 3, ]; let event = `I2C-reply-${address}-`; if (register !== null) { data.push(register & 0x7f, (register >> 7) & 0x7f); } else { register = 0; } event += register; data.push(bytesToRead & 0x7f, (bytesToRead >> 7) & 0x7f, END_SYSEX); this.on(event, callback); i2cRequest(this, data); return this; } /** * Stop continuous reading of the specified I2C address or register. * * @param {object} options Options: * bus {number} The I2C bus (on supported platforms) * address {number} The I2C peripheral address to stop reading. * * @param {number} address The I2C peripheral address to stop reading. */ i2cStop(options) { // There may be more values in the future // var options = {}; // null or undefined? Do nothing. if (options == null) { return; } if (typeof options === "number") { options = { address: options, }; } writeToTransport(this, [ START_SYSEX, I2C_REQUEST, options.address, this.I2C_MODES.STOP_READING << 3, END_SYSEX, ]); Object.keys(this._events).forEach((event) => { if (event.startsWith(`I2C-reply-${options.address}`)) { this.removeAllListeners(event); } }); } /** * Perform a single I2C read * * Supersedes sendI2CReadRequest * * Read bytes from address * * @param {number} address The address of the I2C device * @param {number} register Optionally set the register to read from. * @param {number} numBytes The number of bytes to receive. * @param {function} callback A function to call when we have received the bytes. * */ i2cReadOnce(address, register, bytesToRead, callback) { if ( arguments.length === 3 && typeof register === "number" && typeof bytesToRead === "function" ) { callback = bytesToRead; bytesToRead = register; register = null; } const data = [START_SYSEX, I2C_REQUEST, address, this.I2C_MODES.READ << 3]; let event = `I2C-reply-${address}-`; if (register !== null) { data.push(register & 0x7f, (register >> 7) & 0x7f); } else { register = 0; } event += register; data.push(bytesToRead & 0x7f, (bytesToRead >> 7) & 0x7f, END_SYSEX); this.once(event, callback); i2cRequest(this, data); return this; } /** * Configure the passed pin as the controller in a 1-wire bus. * Pass as enableParasiticPower true if you want the data pin to power the bus. * @param pin * @param enableParasiticPower */ sendOneWireConfig(pin, enableParasiticPower) { writeToTransport(this, [ START_SYSEX, ONEWIRE_DATA, ONEWIRE_CONFIG_REQUEST, pin, enableParasiticPower ? 0x01 : 0x00, END_SYSEX, ]); } /** * Searches for 1-wire devices on the bus. The passed callback should accept * and error argument and an array of device identifiers. * @param pin * @param callback */ sendOneWireSearch(pin, callback) { this[SYM_sendOneWireSearch]( ONEWIRE_SEARCH_REQUEST, `1-wire-search-reply-${pin}`, pin, callback ); } /** * Searches for 1-wire devices on the bus in an alarmed state. The passed callback * should accept and error argument and an array of device identifiers. * @param pin * @param callback */ sendOneWireAlarmsSearch(pin, callback) { this[SYM_sendOneWireSearch]( ONEWIRE_SEARCH_ALARMS_REQUEST, `1-wire-search-alarms-reply-${pin}`, pin, callback ); } [SYM_sendOneWireSearch](type, event, pin, callback) { writeToTransport(this, [START_SYSEX, ONEWIRE_DATA, type, pin, END_SYSEX]); const timeout = setTimeout(() => { /* istanbul ignore next */ callback( new Error( "1-Wire device search timeout - are you running ConfigurableFirmata?" ) ); }, 5000); this.once(event, (devices) => { clearTimeout(timeout); callback(null, devices); }); } /** * Reads data from a device on the bus and invokes the passed callback. * * N.b. ConfigurableFirmata will issue the 1-wire select command internally. * @param pin * @param device * @param numBytesToRead * @param callback */ sendOneWireRead(pin, device, numBytesToRead, callback) { const correlationId = Math.floor(Math.random() * 255); /* istanbul ignore next */ const timeout = setTimeout(() => { /* istanbul ignore next */ callback( new Error( "1-Wire device read timeout - are you running ConfigurableFirmata?" ) ); }, 5000); this[SYM_sendOneWireRequest]( pin, ONEWIRE_READ_REQUEST_BIT, device, numBytesToRead, correlationId, null, null, `1-wire-read-reply-${correlationId}`, (data) => { clearTimeout(timeout); callback(null, data); } ); } /** * Resets all devices on the bus. * @param pin */ sendOneWireReset(pin) { this[SYM_sendOneWireRequest](pin, ONEWIRE_RESET_REQUEST_BIT); } /** * Writes data to the bus to be received by the passed device. The device * should be obtained from a previous call to sendOneWireSearch. * * N.b. ConfigurableFirmata will issue the 1-wire select command internally. * @param pin * @param device * @param data */ sendOneWireWrite(pin, device, data) { this[SYM_sendOneWireRequest]( pin, ONEWIRE_WRITE_REQUEST_BIT, device, null, null, null, Array.isArray(data) ? data : [data] ); } /** * Tells firmata to not do anything for the passed amount of ms. For when you * need to give a device attached to the bus time to do a calculation. * @param pin */ sendOneWireDelay(pin, delay) { this[SYM_sendOneWireRequest]( pin, ONEWIRE_DELAY_REQUEST_BIT, null, null, null, delay ); } /** * Sends the passed data to the passed device on the bus, reads the specified * number of bytes and invokes the passed callback. * * N.b. ConfigurableFirmata will issue the 1-wire select command internally. * @param pin * @param device * @param data * @param numBytesToRead * @param callback */ sendOneWireWriteAndRead(pin, device, data, numBytesToRead, callback) { const correlationId = Math.floor(Math.random() * 255); /* istanbul ignore next */ const timeout = setTimeout(() => { /* istanbul ignore next */ callback( new Error( "1-Wire device read timeout - are you running ConfigurableFirmata?" ) ); }, 5000); this[SYM_sendOneWireRequest]( pin, ONEWIRE_WRITE_REQUEST_BIT | ONEWIRE_READ_REQUEST_BIT, device, numBytesToRead, correlationId, null, Array.isArray(data) ? data : [data], `1-wire-read-reply-${correlationId}`, (data) => { clearTimeout(timeout); callback(null, data); } ); } // see http://firmata.org/wiki/Proposals#OneWire_Proposal [SYM_sendOneWireRequest]( pin, subcommand, device, numBytesToRead, correlationId, delay, dataToWrite, event, callback ) { const bytes = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]; if (device || numBytesToRead || correlationId || delay || dataToWrite) { subcommand = subcommand | ONEWIRE_WITHDATA_REQUEST_BITS; } if (device) { bytes.splice(...[0, 8].concat(device)); } if (numBytesToRead) { bytes[8] = numBytesToRead & 0xff; bytes[9] = (numBytesToRead >> 8) & 0xff; } if (correlationId) { bytes[10] = correlationId & 0xff; bytes[11] = (correlationId >> 8) & 0xff; } if (delay) { bytes[12] = delay & 0xff; bytes[13] = (delay >> 8) & 0xff; bytes[14] = (delay >> 16) & 0xff; bytes[15] = (delay >> 24) & 0xff; } if (dataToWrite) { bytes.push(...dataToWrite); } const output = [ START_SYSEX, ONEWIRE_DATA, subcommand, pin, ...Encoder7Bit.to7BitArray(bytes), END_SYSEX, ]; writeToTransport(this, output); if (event && callback) { this.once(event, callback); } } /** * Set sampling interval in millis. Default is 19 ms * @param {number} interval The sampling interval in ms > 10 */ setSamplingInterval(interval) { const safeint = interval < 10 ? 10 : interval > 65535 ? 65535 : interval; this.settings.samplingInterval = safeint; writeToTransport(this, [ START_SYSEX, SAMPLING_INTERVAL, safeint & 0x7f, (safeint >> 7) & 0x7f, END_SYSEX, ]); } /** * Get sampling interval in millis. Default is 19 ms * * @return {number} samplingInterval */ getSamplingInterval() { return this.settings.samplingInterval; } /** * Set reporting on pin * @param {number} pin The pin to turn on/off reporting * @param {number} value Binary value to turn reporting on/off */ reportAnalogPin(pin, value) { /* istanbul ignore else */ if (value === 0 || value === 1) { this.pins[this.analogPins[pin]].report = value; writeToTransport(this, [REPORT_ANALOG | pin, value]); } } /** * Set reporting on pin * @param {number} pin The pin to turn on/off reporting * @param {number} value Binary value to turn reporting on/off */ reportDigitalPin(pin, value) { const port = pin >> 3; /* istanbul ignore else */ if (value === 0 || value === 1) { this.pins[pin].report = value; writeToTransport(this, [REPORT_DIGITAL | port, value]); } } /** * * */ pingRead(options, callback) { if (!this.pins[options.pin].supportedModes.includes(PING_READ)) { throw new Error("Please upload PingFirmata to the board"); } const { pin, value, pulseOut = 0, timeout = 1000000 } = options; writeToTransport(this, [ START_SYSEX, PING_READ, pin, value, ...Firmata.encode([ (pulseOut >> 24) & 0xff, (pulseOut >> 16) & 0xff, (pulseOut >> 8) & 0xff, pulseOut & 0xff, ]), ...Firmata.encode([ (timeout >> 24) & 0xff, (timeout >> 16) & 0xff, (timeout >> 8) & 0xff, timeout & 0xff, ]), END_SYSEX, ]); this.once(`ping-read-${pin}`, callback); } /** * Stepper functions to support version 2 of ConfigurableFirmata's asynchronous control of stepper motors * https://github.com/soundanalogous/ConfigurableFirmata */ /** * Asks the arduino to configure a stepper motor with the given config to allow asynchronous control of the stepper * @param {object} opts Options: * {number} deviceNum: Device number for the stepper (range 0-9) * {number} type: One of this.STEPPER.TYPE.* * {number} stepSize: One of this.STEPPER.STEP_SIZE.* * {number} stepPin: Only used if STEPPER.TYPE.DRIVER * {number} directionPin: Only used if STEPPER.TYPE.DRIVER * {number} motorPin1: motor pin 1 * {number} motorPin2: motor pin 2 * {number} [motorPin3]: Only required if type == this.STEPPER.TYPE.THREE_WIRE || this.STEPPER.TYPE.FOUR_WIRE * {number} [motorPin4]: Only required if type == this.STEPPER.TYPE.FOUR_WIRE * {number} [enablePin]: Enable pin * {array} [invertPins]: Array of pins to invert */ accelStepperConfig(options) { let { deviceNum, invertPins, motorPin1, motorPin2, motorPin3, motorPin4, enablePin, stepSize = this.STEPPER.STEP_SIZE.WHOLE, type = this.STEPPER.TYPE.FOUR_WIRE, } = options; const data = [ START_SYSEX, ACCELSTEPPER, 0x00, // STEPPER_CONFIG from firmware deviceNum, ]; let iface = ((type & 0x07) << 4) | ((stepSize & 0x07) << 1); let pinsToInvert = 0x00; if (typeof enablePin !== "undefined") { iface = iface | 0x01; } data.push(iface); [ "stepPin", "motorPin1", "directionPin", "motorPin2", "motorPin3", "motorPin4", "enablePin", ].forEach((pin) => { if (typeof options[pin] !== "undefined") { data.push(options[pin]); } }); if (Array.isArray(invertPins)) { if (invertPins.includes(motorPin1)) { pinsToInvert |= 0x01; } if (invertPins.includes(motorPin2)) { pinsToInvert |= 0x02; } if (invertPins.includes(motorPin3)) { pinsToInvert |= 0x04; } if (invertPins.includes(motorPin4)) { pinsToInvert |= 0x08;