2651 lines
70 KiB
JavaScript

"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;