From 81501dfbdf3890fe835bfe7f9cc7f6aaea25e001 Mon Sep 17 00:00:00 2001 From: Dave Conway-Jones Date: Fri, 17 Nov 2023 15:07:38 +0000 Subject: [PATCH] Move Firmata into Arduino node so we don't rely on their version of serialport --- hardware/Arduino/35-arduino.js | 4 +- hardware/Arduino/LICENSE | 32 +- hardware/Arduino/lib/com.js | 67 + hardware/Arduino/lib/encoder7bit.js | 49 + hardware/Arduino/lib/firmata-io.js | 2650 ++++++++++++++++++++++++++ hardware/Arduino/lib/firmata.js | 3 + hardware/Arduino/lib/onewireutils.js | 47 + hardware/Arduino/package.json | 9 +- 8 files changed, 2854 insertions(+), 7 deletions(-) create mode 100644 hardware/Arduino/lib/com.js create mode 100644 hardware/Arduino/lib/encoder7bit.js create mode 100644 hardware/Arduino/lib/firmata-io.js create mode 100644 hardware/Arduino/lib/firmata.js create mode 100644 hardware/Arduino/lib/onewireutils.js diff --git a/hardware/Arduino/35-arduino.js b/hardware/Arduino/35-arduino.js index 8464312e..7d62b8b1 100644 --- a/hardware/Arduino/35-arduino.js +++ b/hardware/Arduino/35-arduino.js @@ -2,7 +2,7 @@ module.exports = function(RED) { "use strict"; - var Board = require('firmata'); + var Board = require('./lib/firmata'); var SP = require('serialport'); // The Board Definition - this opens (and closes) the connection @@ -165,7 +165,7 @@ module.exports = function(RED) { node.board.digitalWrite(node.pin, node.board.LOW); } } - if (node.state === "PWM") { + if (node.state === "PWM") { msg.payload = parseInt((msg.payload * 1) + 0.5); if ((msg.payload >= 0) && (msg.payload <= 255)) { node.board.analogWrite(node.pin, msg.payload); diff --git a/hardware/Arduino/LICENSE b/hardware/Arduino/LICENSE index f5b60114..72d6a54a 100644 --- a/hardware/Arduino/LICENSE +++ b/hardware/Arduino/LICENSE @@ -1,4 +1,4 @@ -Copyright 2016 JS Foundation and other contributors, https://js.foundation/ +Copyright 2016-2023 JS Foundation and other contributors, https://js.foundation/ Copyright 2013-2016 IBM Corp. Licensed under the Apache License, Version 2.0 (the "License"); @@ -12,3 +12,33 @@ distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. + + +Firmata lib License - The MIT License + +Copyright (c) 2011-2015 Julian Gautier julian.gautier@alumni.neumont.edu +Copyright (c) 2015-2020 The Firmata.js Authors (see AUTHORS.md) +Authors +Rick Waldron Julian Gautier Glen Arrowsmith Jeff Hoefs Olivier Louvignes +Francis Gulotta Luis Montes Lars Toft Jacobsen Érico Jean-Philippe Côté +Ernesto Laval Dustan Kasten Lars Gregori Andrey Bezugliy Donovan Buck +Pravdomil jywarren Andrew Stewart Richard Rodd Dwayn Matthies Sarah GP +achingbrain David Resseguie Nick Stewart the1337guy + +Permission is hereby granted, free of charge, to any person obtaining a +copy of this software and associated documentation files (the 'Software'), +to deal in the Software without restriction, including without limitation +the rights to use, copy, modify, merge, publish, distribute, sublicense, +and/or sell copies of the Software, and to permit persons to whom the Software +is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS +OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL +THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. \ No newline at end of file diff --git a/hardware/Arduino/lib/com.js b/hardware/Arduino/lib/com.js new file mode 100644 index 00000000..6acb42d0 --- /dev/null +++ b/hardware/Arduino/lib/com.js @@ -0,0 +1,67 @@ +"use strict"; + +const Emitter = require("events"); + +class TransportStub extends Emitter { + constructor(path /*, options, openCallback*/) { + super(); + this.isOpen = true; + this.baudRate = 0; + this.path = path; + } + + write(buffer) { + // Tests are written to work with arrays not buffers + // this shouldn't impact the data, just the container + // This also should be changed in future test rewrites + /* istanbul ignore else */ + if (Buffer.isBuffer(buffer)) { + buffer = Array.from(buffer); + } + + this.lastWrite = buffer; + this.emit("write", buffer); + } + + static list() { + /* istanbul ignore next */ + return Promise.resolve([]); + } +} + +// This trash is necessary for stubbing with sinon. +TransportStub.SerialPort = TransportStub; + +let com; +let error; +let SerialPort; + +try { + /* istanbul ignore else */ + if (process.env.IS_TEST_MODE) { + com = TransportStub; + } else { + SerialPort = require("serialport").SerialPort; + com = SerialPort; + } +} catch (err) { + /* istanbul ignore next */ + error = err; +} + +/* istanbul ignore if */ +if (com == null) { + if (process.env.IS_TEST_MODE) { + com = TransportStub; + } else { + console.log("It looks like serialport didn't install properly."); + console.log( + "More information can be found here https://serialport.io/docs/guide-installation" + ); + console.log(`The result of requiring the package is: ${SerialPort}`); + console.log(error); + throw "Missing serialport dependency"; + } +} + +module.exports = com; diff --git a/hardware/Arduino/lib/encoder7bit.js b/hardware/Arduino/lib/encoder7bit.js new file mode 100644 index 00000000..0274bfc3 --- /dev/null +++ b/hardware/Arduino/lib/encoder7bit.js @@ -0,0 +1,49 @@ +"use strict"; +/** + * "Inspired" by Encoder7Bit.h/Encoder7Bit.cpp in the + * Firmata source code. + */ +module.exports = { + to7BitArray(data) { + let shift = 0; + let previous = 0; + const output = []; + + for (let byte of data) { + if (shift === 0) { + output.push(byte & 0x7f); + shift++; + previous = byte >> 7; + } else { + output.push(((byte << shift) & 0x7f) | previous); + if (shift === 6) { + output.push(byte >> 1); + shift = 0; + } else { + shift++; + previous = byte >> (8 - shift); + } + } + } + + /* istanbul ignore else */ + if (shift > 0) { + output.push(previous); + } + + return output; + }, + from7BitArray(encoded) { + const expectedBytes = encoded.length * 7 >> 3; + const decoded = []; + + for (let i = 0; i < expectedBytes; i++) { + const j = i << 3; + const pos = (j / 7) >>> 0; + const shift = j % 7; + decoded[i] = (encoded[pos] >> shift) | ((encoded[pos + 1] << (7 - shift)) & 0xFF); + } + + return decoded; + } +}; diff --git a/hardware/Arduino/lib/firmata-io.js b/hardware/Arduino/lib/firmata-io.js new file mode 100644 index 00000000..1fa15a16 --- /dev/null +++ b/hardware/Arduino/lib/firmata-io.js @@ -0,0 +1,2650 @@ +"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; diff --git a/hardware/Arduino/lib/firmata.js b/hardware/Arduino/lib/firmata.js new file mode 100644 index 00000000..34f46604 --- /dev/null +++ b/hardware/Arduino/lib/firmata.js @@ -0,0 +1,3 @@ +"use strict"; + +module.exports = require("./firmata-io.js")(require("./com")); diff --git a/hardware/Arduino/lib/onewireutils.js b/hardware/Arduino/lib/onewireutils.js new file mode 100644 index 00000000..29ea60e1 --- /dev/null +++ b/hardware/Arduino/lib/onewireutils.js @@ -0,0 +1,47 @@ +"use strict"; +const Encoder7Bit = require("./encoder7bit"); +const OneWireUtils = { + crc8(data) { + let crc = 0; + + for (let inbyte of data) { + for (let n = 8; n; n--) { + const mix = (crc ^ inbyte) & 0x01; + crc >>= 1; + + if (mix) { + crc ^= 0x8C; + } + + inbyte >>= 1; + } + } + + return crc; + }, + + readDevices(data) { + const deviceBytes = Encoder7Bit.from7BitArray(data); + const devices = []; + + for (let i = 0; i < deviceBytes.length; i += 8) { + const device = deviceBytes.slice(i, i + 8); + + if (device.length !== 8) { + continue; + } + + const check = OneWireUtils.crc8(device.slice(0, 7)); + + if (check !== device[7]) { + console.error("ROM invalid!"); + } + + devices.push(device); + } + + return devices; + } +}; + +module.exports = OneWireUtils; diff --git a/hardware/Arduino/package.json b/hardware/Arduino/package.json index ae4eb679..34e055ee 100644 --- a/hardware/Arduino/package.json +++ b/hardware/Arduino/package.json @@ -1,9 +1,9 @@ { "name": "node-red-node-arduino", - "version": "0.3.1", + "version": "1.0.0", "description": "A Node-RED node to talk to an Arduino running firmata", "dependencies": { - "firmata": "^2.3.0" + "serialport": "^12.0.0" }, "repository": { "type": "git", @@ -17,16 +17,17 @@ "firmata" ], "node-red": { + "version": ">=3.0.0", "nodes": { "arduino": "35-arduino.js" } }, "engines": { - "node": ">=8" + "node": ">=16.0.0" }, "author": { "name": "Dave Conway-Jones", - "email": "ceejay@vnet.ibm.com", + "email": "dceejay@gmail.com", "url": "http://nodered.org" } }