"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; } if (invertPins.includes(enablePin)) { pinsToInvert |= 0x10; } } data.push(pinsToInvert, END_SYSEX); writeToTransport(this, data); } /** * Asks the arduino to set the stepper position to 0 * Note: This is not a move. We are setting the current position equal to zero * @param {number} deviceNum Device number for the stepper (range 0-9) */ accelStepperZero(deviceNum) { writeToTransport(this, [ START_SYSEX, ACCELSTEPPER, 0x01, // STEPPER_ZERO from firmware deviceNum, END_SYSEX, ]); } /** * Asks the arduino to move a stepper a number of steps * (and optionally with and acceleration and deceleration) * speed is in units of steps/sec * @param {number} deviceNum Device number for the stepper (range 0-5) * @param {number} steps Number of steps to make */ accelStepperStep(deviceNum, steps, callback) { writeToTransport(this, [ START_SYSEX, ACCELSTEPPER, 0x02, // STEPPER_STEP from firmware deviceNum, ...encode32BitSignedInteger(steps), END_SYSEX, ]); if (callback) { this.once(`stepper-done-${deviceNum}`, callback); } } /** * Asks the arduino to move a stepper to a specific location * @param {number} deviceNum Device number for the stepper (range 0-5) * @param {number} position Desired position */ accelStepperTo(deviceNum, position, callback) { writeToTransport(this, [ START_SYSEX, ACCELSTEPPER, 0x03, // STEPPER_TO from firmware deviceNum, ...encode32BitSignedInteger(position), END_SYSEX, ]); if (callback) { this.once(`stepper-done-${deviceNum}`, callback); } } /** * Asks the arduino to enable/disable a stepper * @param {number} deviceNum Device number for the stepper (range 0-9) * @param {boolean} [enabled] */ accelStepperEnable(deviceNum, enabled = true) { writeToTransport(this, [ START_SYSEX, ACCELSTEPPER, 0x04, // ENABLE from firmware deviceNum, enabled & 0x01, END_SYSEX, ]); } /** * Asks the arduino to stop a stepper * @param {number} deviceNum Device number for the stepper (range 0-9) */ accelStepperStop(deviceNum) { writeToTransport(this, [ START_SYSEX, ACCELSTEPPER, 0x05, // STEPPER_STOP from firmware deviceNum, END_SYSEX, ]); } /** * Asks the arduino to report the position of a stepper * @param {number} deviceNum Device number for the stepper (range 0-9) */ accelStepperReportPosition(deviceNum, callback) { writeToTransport(this, [ START_SYSEX, ACCELSTEPPER, 0x06, // STEPPER_REPORT_POSITION from firmware deviceNum, END_SYSEX, ]); /* istanbul ignore else */ if (callback) { this.once(`stepper-position-${deviceNum}`, callback); } } /** * Asks the arduino to set the acceleration for a stepper * @param {number} deviceNum Device number for the stepper (range 0-9) * @param {number} acceleration Desired acceleration in steps per sec^2 */ accelStepperAcceleration(deviceNum, acceleration) { writeToTransport(this, [ START_SYSEX, ACCELSTEPPER, 0x08, // STEPPER_SET_ACCELERATION from firmware deviceNum, ...encodeCustomFloat(acceleration), END_SYSEX, ]); } /** * Asks the arduino to set the max speed for a stepper * @param {number} deviceNum Device number for the stepper (range 0-9) * @param {number} speed Desired speed or maxSpeed in steps per second * @param {function} [callback] */ accelStepperSpeed(deviceNum, speed) { writeToTransport(this, [ START_SYSEX, ACCELSTEPPER, 0x09, // STEPPER_SET_SPEED from firmware deviceNum, ...encodeCustomFloat(speed), END_SYSEX, ]); } /** * Asks the arduino to configure a multiStepper group * @param {object} options Options: * {number} groupNum: Group number for the multiSteppers (range 0-5) * {number} devices: array of accelStepper device numbers in group **/ multiStepperConfig(options) { writeToTransport(this, [ START_SYSEX, ACCELSTEPPER, 0x20, // MULTISTEPPER_CONFIG from firmware options.groupNum, ...options.devices, END_SYSEX, ]); } /** * Asks the arduino to move a multiStepper group * @param {number} groupNum Group number for the multiSteppers (range 0-5) * @param {number} positions array of absolute stepper positions **/ multiStepperTo(groupNum, positions, callback) { if (groupNum < 0 || groupNum > 5) { throw new RangeError( `Invalid "groupNum": ${groupNum}. Expected "groupNum" between 0-5` ); } writeToTransport(this, [ START_SYSEX, ACCELSTEPPER, 0x21, // MULTISTEPPER_TO from firmware groupNum, ...positions.reduce( (a, b) => a.concat(...encode32BitSignedInteger(b)), [] ), END_SYSEX, ]); /* istanbul ignore else */ if (callback) { this.once(`multi-stepper-done-${groupNum}`, callback); } } /** * Asks the arduino to stop a multiStepper group * @param {number} groupNum: Group number for the multiSteppers (range 0-5) **/ multiStepperStop(groupNum) { /* istanbul ignore else */ if (groupNum < 0 || groupNum > 5) { throw new RangeError( `Invalid "groupNum": ${groupNum}. Expected "groupNum" between 0-5` ); } writeToTransport(this, [ START_SYSEX, ACCELSTEPPER, 0x23, // MULTISTEPPER_STOP from firmware groupNum, END_SYSEX, ]); } /** * Stepper functions to support AdvancedFirmata's asynchronous control of stepper motors * https://github.com/soundanalogous/AdvancedFirmata */ /** * Asks the arduino to configure a stepper motor with the given config to allow asynchronous control of the stepper * @param {number} deviceNum Device number for the stepper (range 0-5, expects steppers to be setup in order from 0 to 5) * @param {number} type One of this.STEPPER.TYPE.* * @param {number} stepsPerRev Number of steps motor takes to make one revolution * @param {number} stepOrMotor1Pin If using EasyDriver type stepper driver, this is direction pin, otherwise it is motor 1 pin * @param {number} dirOrMotor2Pin If using EasyDriver type stepper driver, this is step pin, otherwise it is motor 2 pin * @param {number} [motorPin3] Only required if type == this.STEPPER.TYPE.FOUR_WIRE * @param {number} [motorPin4] Only required if type == this.STEPPER.TYPE.FOUR_WIRE */ stepperConfig( deviceNum, type, stepsPerRev, dirOrMotor1Pin, dirOrMotor2Pin, motorPin3, motorPin4 ) { writeToTransport(this, [ START_SYSEX, STEPPER, 0x00, // STEPPER_CONFIG from firmware deviceNum, type, stepsPerRev & 0x7f, (stepsPerRev >> 7) & 0x7f, dirOrMotor1Pin, dirOrMotor2Pin, ...(type === this.STEPPER.TYPE.FOUR_WIRE ? [motorPin3, motorPin4] : []), END_SYSEX, ]); } /** * Asks the arduino to move a stepper a number of steps at a specific speed * (and optionally with and acceleration and deceleration) * speed is in units of .01 rad/sec * accel and decel are in units of .01 rad/sec^2 * TODO: verify the units of speed, accel, and decel * @param {number} deviceNum Device number for the stepper (range 0-5) * @param {number} direction One of this.STEPPER.DIRECTION.* * @param {number} steps Number of steps to make * @param {number} speed * @param {number|function} accel Acceleration or if accel and decel are not used, then it can be the callback * @param {number} [decel] * @param {function} [callback] */ stepperStep(deviceNum, direction, steps, speed, accel, decel, callback) { if (typeof accel === "function") { callback = accel; accel = 0; decel = 0; } writeToTransport(this, [ START_SYSEX, STEPPER, 0x01, // STEPPER_STEP from firmware deviceNum, direction, // one of this.STEPPER.DIRECTION.* steps & 0x7f, (steps >> 7) & 0x7f, (steps >> 14) & 0x7f, speed & 0x7f, (speed >> 7) & 0x7f, ...(accel > 0 || decel > 0 ? [accel & 0x7f, (accel >> 7) & 0x7f, decel & 0x7f, (decel >> 7) & 0x7f] : []), END_SYSEX, ]); /* istanbul ignore else */ if (callback) { this.once(`stepper-done-${deviceNum}`, callback); } } /** * Asks the Arduino to configure a hardware or serial port. * @param {object} options Options: * portId {number} The serial port to use (HW_SERIAL1, HW_SERIAL2, HW_SERIAL3, SW_SERIAL0, * SW_SERIAL1, SW_SERIAL2, SW_SERIAL3) * baud {number} The baud rate of the serial port * rxPin {number} [SW Serial only] The RX pin of the SoftwareSerial instance * txPin {number} [SW Serial only] The TX pin of the SoftwareSerial instance */ serialConfig(options) { let portId; let baud; let rxPin; let txPin; /* istanbul ignore else */ if (typeof options === "object" && options !== null) { portId = options.portId; baud = options.baud; rxPin = options.rxPin; txPin = options.txPin; } /* istanbul ignore else */ if (typeof portId === "undefined") { throw new Error( "portId must be specified, see SERIAL_PORT_IDs for options." ); } baud = baud || 57600; const data = [ START_SYSEX, SERIAL_MESSAGE, SERIAL_CONFIG | portId, baud & 0x7f, (baud >> 7) & 0x7f, (baud >> 14) & 0x7f, ]; if ( portId > 7 && typeof rxPin !== "undefined" && typeof txPin !== "undefined" ) { data.push(rxPin, txPin); } else if (portId > 7) { throw new Error( "Both RX and TX pins must be defined when using Software Serial." ); } data.push(END_SYSEX); writeToTransport(this, data); } /** * Write an array of bytes to the specified serial port. * @param {number} portId The serial port to write to. * @param {Array} inBytes An array of bytes to write to the serial port. */ serialWrite(portId, bytes) { const data = [START_SYSEX, SERIAL_MESSAGE, SERIAL_WRITE | portId]; for (let i = 0, len = bytes.length; i < len; i++) { data.push(bytes[i] & 0x7f, (bytes[i] >> 7) & 0x7f); } data.push(END_SYSEX); /* istanbul ignore else */ if (bytes.length > 0) { writeToTransport(this, data); } } /** * Start continuous reading of the specified serial port. The port is checked for data each * iteration of the main Arduino loop. * @param {number} portId The serial port to start reading continuously. * @param {number} maxBytesToRead [Optional] The maximum number of bytes to read per iteration. * If there are less bytes in the buffer, the lesser number of bytes will be returned. A value of 0 * indicates that all available bytes in the buffer should be read. * @param {function} callback A function to call when we have received the bytes. */ serialRead(portId, maxBytesToRead, callback) { const data = [ START_SYSEX, SERIAL_MESSAGE, SERIAL_READ | portId, this.SERIAL_MODES.CONTINUOUS_READ, ]; if (arguments.length === 2 && typeof maxBytesToRead === "function") { callback = maxBytesToRead; } else { data.push(maxBytesToRead & 0x7f, (maxBytesToRead >> 7) & 0x7f); } data.push(END_SYSEX); writeToTransport(this, data); this.on(`serial-data-${portId}`, callback); } /** * Stop continuous reading of the specified serial port. This does not close the port, it stops * reading it but keeps the port open. * @param {number} portId The serial port to stop reading. */ serialStop(portId) { writeToTransport(this, [ START_SYSEX, SERIAL_MESSAGE, SERIAL_READ | portId, this.SERIAL_MODES.STOP_READING, END_SYSEX, ]); this.removeAllListeners(`serial-data-${portId}`); } /** * Close the specified serial port. * @param {number} portId The serial port to close. */ serialClose(portId) { writeToTransport(this, [ START_SYSEX, SERIAL_MESSAGE, SERIAL_CLOSE | portId, END_SYSEX, ]); } /** * Flush the specified serial port. For hardware serial, this waits for the transmission of * outgoing serial data to complete. For software serial, this removed any buffered incoming serial * data. * @param {number} portId The serial port to flush. */ serialFlush(portId) { writeToTransport(this, [ START_SYSEX, SERIAL_MESSAGE, SERIAL_FLUSH | portId, END_SYSEX, ]); } /** * For SoftwareSerial only. Only a single SoftwareSerial instance can read data at a time. * Call this method to set this port to be the reading port in the case there are multiple * SoftwareSerial instances. * @param {number} portId The serial port to listen on. */ serialListen(portId) { // listen only applies to software serial ports if (portId < 8) { return; } writeToTransport(this, [ START_SYSEX, SERIAL_MESSAGE, SERIAL_LISTEN | portId, END_SYSEX, ]); } /** * Allow user code to handle arbitrary sysex responses * * @param {number} commandByte The commandByte must be associated with some message * that's expected from the slave device. The handler is * called with an array of _raw_ data from the slave. Data * decoding must be done within the handler itself. * * Use Firmata.decode(data) to extract useful values from * the incoming response data. * * @param {function} handler Function which handles receipt of responses matching * commandByte. */ sysexResponse(commandByte, handler) { if (Firmata.SYSEX_RESPONSE[commandByte]) { throw new Error(`${commandByte} is not an available SYSEX_RESPONSE byte`); } Firmata.SYSEX_RESPONSE[commandByte] = (board) => handler.call(board, board.buffer.slice(2, -1)); return this; } /* * Allow user to remove sysex response handlers. * * @param {number} commandByte The commandByte to disassociate with a handler * previously set via `sysexResponse( commandByte, handler)`. */ clearSysexResponse(commandByte) { /* istanbul ignore else */ if (Firmata.SYSEX_RESPONSE[commandByte]) { delete Firmata.SYSEX_RESPONSE[commandByte]; } } /** * Allow user code to send arbitrary sysex messages * * @param {Array} message The message array is expected to be all necessary bytes * between START_SYSEX and END_SYSEX (non-inclusive). It will * be assumed that the data in the message array is * already encoded as 2 7-bit bytes LSB first. * * */ sysexCommand(message) { if (!message || !message.length) { throw new Error("Sysex Command cannot be empty"); } writeToTransport(this, [START_SYSEX, ...message.slice(), END_SYSEX]); return this; } /** * Send SYSTEM_RESET to arduino */ reset() { writeToTransport(this, [SYSTEM_RESET]); } /** * Firmata.isAcceptablePort Determines if a `port` object (from SerialPort.list()) * is a valid Arduino (or similar) device. * @return {Boolean} true if port can be connected to by Firmata */ static isAcceptablePort(port) { let rport = /usb|acm|^com/i; if (rport.test(port.path)) { return true; } return false; } /** * Firmata.requestPort(callback) Request an acceptable port to connect to. * callback(error, port) */ static requestPort(callback) { if (!Transport || (Transport && typeof Transport.list !== "function")) { process.nextTick(() => { callback(new Error("No Transport provided"), null); }); return; } Transport.list() .then((ports) => { const port = ports.find( (port) => Firmata.isAcceptablePort(port) && port ); if (port) { callback(null, port); } else { callback(new Error("No Acceptable Port Found"), null); } }) .catch((error) => { callback(error, null); }); } // Expose encode/decode for custom sysex messages static encode(data) { const encoded = []; const length = data.length; for (let i = 0; i < length; i++) { encoded.push(data[i] & 0x7f, (data[i] >> 7) & 0x7f); } return encoded; } static decode(data) { const decoded = []; if (data.length % 2 !== 0) { throw new Error( "Firmata.decode(data) called with odd number of data bytes" ); } while (data.length) { const lsb = data.shift(); const msb = data.shift(); decoded.push(lsb | (msb << 7)); } return decoded; } } // Prototype Compatibility Aliases Firmata.prototype.analogWrite = Firmata.prototype.pwmWrite; // Static Compatibility Aliases Firmata.Board = Firmata; Firmata.SYSEX_RESPONSE = SYSEX_RESPONSE; Firmata.MIDI_RESPONSE = MIDI_RESPONSE; // The following are used internally. /** * writeToTransport Due to the non-blocking behaviour of transport write * operations, dependent programs need a way to know * when all writes are complete. Every write increments * a `pending` value, when the write operation has * completed, the `pending` value is decremented. * * @param {Board} board An active Board instance * @param {Array} data An array of 8 and 7 bit values that will be * wrapped in a Buffer and written to the transport. */ function writeToTransport(board, data) { board.pending++; board.transport.write(Buffer.from(data), () => board.pending--); } function i2cRequest(board, bytes) { const active = i2cActive.get(board); if (!active) { throw new Error( "I2C is not enabled for this board. To enable, call the i2cConfig() method." ); } // Do not tamper with I2C_CONFIG messages if (bytes[1] === I2C_REQUEST) { const address = bytes[2]; // If no peripheral settings exist, make them. if (!active[address]) { active[address] = { stopTX: true, }; } // READ (8) or CONTINUOUS_READ (16) // value & 0b00011000 if (bytes[3] & I2C_READ_MASK) { // Invert logic to accomodate default = true, // which is actually stopTX = 0 bytes[3] |= Number(!active[address].stopTX) << 6; } } writeToTransport(board, bytes); } function encode32BitSignedInteger(data) { const negative = data < 0; data = Math.abs(data); const encoded = [ data & 0x7f, (data >> 7) & 0x7f, (data >> 14) & 0x7f, (data >> 21) & 0x7f, (data >> 28) & 0x07, ]; if (negative) { encoded[encoded.length - 1] |= 0x08; } return encoded; } function decode32BitSignedInteger(bytes) { let result = (bytes[0] & 0x7f) | ((bytes[1] & 0x7f) << 7) | ((bytes[2] & 0x7f) << 14) | ((bytes[3] & 0x7f) << 21) | ((bytes[4] & 0x07) << 28); if (bytes[4] >> 3) { result *= -1; } return result; } const MAX_SIGNIFICAND = Math.pow(2, 23); function encodeCustomFloat(input) { const sign = input < 0 ? 1 : 0; input = Math.abs(input); const base10 = Math.floor(Math.log10(input)); // Shift decimal to start of significand let exponent = 0 + base10; input /= Math.pow(10, base10); // Shift decimal to the right as far as we can while (!Number.isInteger(input) && input < MAX_SIGNIFICAND) { exponent -= 1; input *= 10; } // Reduce precision if necessary while (input > MAX_SIGNIFICAND) { exponent += 1; input /= 10; } input = Math.trunc(input); exponent += 11; const encoded = [ input & 0x7f, (input >> 7) & 0x7f, (input >> 14) & 0x7f, ((input >> 21) & 0x03) | ((exponent & 0x0f) << 2) | ((sign & 0x01) << 6), ]; return encoded; } function decodeCustomFloat(input) { const exponent = ((input[3] >> 2) & 0x0f) - 11; const sign = (input[3] >> 6) & 0x01; let result = input[0] | (input[1] << 7) | (input[2] << 14) | ((input[3] & 0x03) << 21); if (sign) { result *= -1; } return result * Math.pow(10, exponent); } /* istanbul ignore else */ if (process.env.IS_TEST_MODE) { let transport = null; Firmata.test = { i2cPeripheralSettings(board) { return i2cActive.get(board); }, get i2cActive() { return i2cActive; }, set transport(value) { transport = Transport; Transport = value; }, restoreTransport() { Transport = transport; }, encode32BitSignedInteger, decode32BitSignedInteger, encodeCustomFloat, decodeCustomFloat, writeToTransport, symbols: { SYM_sendOneWireRequest, SYM_sendOneWireSearch, }, }; } const bindTransport = function (transport) { Transport = transport; return Firmata; }; bindTransport.Firmata = Firmata; module.exports = bindTransport;