Node.js implementation of the Arduino Firmata protocol for controlling Arduino boards from JavaScript applications
Real-time event monitoring for pin state changes, board connection status, and custom SysEx messages using EventEmitter2 for comprehensive Arduino communication.
Events fired during Arduino connection establishment and management.
// Connection event types (no parameters)
// arduino.on('connect', callback)
// arduino.on('boardReady', callback)
// arduino.on('boardVersion', callback)Fired when Arduino connection is fully established and ready for I/O operations.
arduino.on('connect', function() {
console.log("Connected to: " + arduino.serialport_name);
console.log("Board version: " + arduino.boardVersion);
// Arduino is now ready for I/O operations
arduino.digitalWrite(13, true);
});Fired when Arduino board is detected and initialization has begun (before full connection).
arduino.on('boardReady', function() {
console.log('Arduino board detected and initializing...');
// Connection will be complete when 'connect' event fires
});Fired when Arduino board firmware version information is received.
// arduino.on('boardVersion', function(version) { ... })
// Parameters:
// version (string) - Board firmware version (e.g., "2.3")arduino.on('boardVersion', function(version) {
console.log('Arduino firmware version:', version);
// Check for minimum required version
const [major, minor] = version.split('.').map(Number);
if (major < 2 || (major === 2 && minor < 2)) {
console.warn('Warning: Firmata 2.2+ recommended for full functionality');
}
});Real-time monitoring of digital pin state changes.
// Digital change event
// arduino.on('digitalChange', function(event) { ... })
// Event object properties:
interface DigitalChangeEvent {
pin: number; // Pin number that changed (0-13+)
value: boolean; // New pin state (true = HIGH, false = LOW)
old_value: boolean; // Previous pin state
}Monitor digital pin state changes for button presses, switch states, and sensor triggers.
// Set up digital input monitoring
arduino.pinMode(7, ArduinoFirmata.INPUT);
arduino.pinMode(8, ArduinoFirmata.INPUT);
arduino.on('digitalChange', function(event) {
console.log(`Pin ${event.pin}: ${event.old_value ? 'HIGH' : 'LOW'} → ${event.value ? 'HIGH' : 'LOW'}`);
// Button press detection (LOW to HIGH transition)
if (event.pin === 7 && event.value === true && event.old_value === false) {
console.log('Button on pin 7 pressed!');
arduino.digitalWrite(13, true); // Turn on LED
}
// Button release detection (HIGH to LOW transition)
if (event.pin === 7 && event.value === false && event.old_value === true) {
console.log('Button on pin 7 released!');
arduino.digitalWrite(13, false); // Turn off LED
}
});const inputPins = [2, 3, 4, 5, 6, 7, 8];
// Configure all pins as inputs
inputPins.forEach(pin => {
arduino.pinMode(pin, ArduinoFirmata.INPUT);
});
// Monitor all configured pins
arduino.on('digitalChange', function(event) {
if (inputPins.includes(event.pin)) {
console.log(`Input pin ${event.pin} changed to ${event.value ? 'HIGH' : 'LOW'}`);
// Update status LED based on any active input
const anyHigh = inputPins.some(pin => arduino.digitalRead(pin));
arduino.digitalWrite(13, anyHigh);
}
});Real-time monitoring of analog pin value changes for sensor data and continuous monitoring.
// Analog change event
// arduino.on('analogChange', function(event) { ... })
// Event object properties:
interface AnalogChangeEvent {
pin: number; // Analog pin number that changed (0-5)
value: number; // New analog reading (0-1023)
old_value: number; // Previous analog reading
}Monitor analog sensors for continuous data collection and threshold-based triggers.
arduino.on('analogChange', function(event) {
const voltage = (event.value / 1023) * 5.0;
console.log(`Analog pin A${event.pin}: ${event.value} (${voltage.toFixed(2)}V)`);
// Temperature sensor monitoring (pin A0)
if (event.pin === 0) {
const temperature = voltage * 100; // LM35 temperature sensor
console.log(`Temperature: ${temperature.toFixed(1)}°C`);
// Temperature threshold alarm
if (temperature > 30 && event.old_value / 1023 * 5 * 100 <= 30) {
console.log('High temperature alert!');
arduino.digitalWrite(13, true);
}
}
// Light sensor controlling LED brightness (pin A1)
if (event.pin === 1) {
const brightness = Math.round((event.value / 1023) * 255);
arduino.analogWrite(9, brightness);
console.log(`Auto-brightness: ${brightness}/255`);
}
});const sensorData = {
temperature: [],
light: [],
potentiometer: []
};
arduino.on('analogChange', function(event) {
const timestamp = new Date().toISOString();
const value = event.value;
switch(event.pin) {
case 0: // Temperature sensor
const tempC = (value / 1023 * 5) * 100;
sensorData.temperature.push({ timestamp, value: tempC });
break;
case 1: // Light sensor
const lightLevel = Math.round((value / 1023) * 100);
sensorData.light.push({ timestamp, value: lightLevel });
break;
case 2: // Potentiometer
const potValue = Math.round((value / 1023) * 100);
sensorData.potentiometer.push({ timestamp, value: potValue });
break;
}
// Keep only last 100 readings per sensor
Object.keys(sensorData).forEach(sensor => {
if (sensorData[sensor].length > 100) {
sensorData[sensor].shift();
}
});
});
// Function to get recent sensor averages
function getSensorAverage(sensor, samples = 10) {
const data = sensorData[sensor];
if (data.length === 0) return null;
const recent = data.slice(-samples);
const sum = recent.reduce((total, reading) => total + reading.value, 0);
return sum / recent.length;
}Handle custom SysEx messages for extended Arduino functionality and bidirectional communication.
// SysEx message event
// arduino.on('sysex', function(event) { ... })
// Event object properties:
interface SysexEvent {
command: number; // SysEx command byte (0-127)
data: number[]; // Array of data bytes received
}Process custom SysEx messages from Arduino firmware.
arduino.on('sysex', function(event) {
console.log(`SysEx command: 0x${event.command.toString(16)}`);
console.log('Data bytes:', event.data);
switch(event.command) {
case 0x01: // LED blink completion
console.log('LED blink sequence completed');
break;
case 0x10: // Custom sensor data
if (event.data.length >= 2) {
const sensorId = event.data[0];
const rawValue = event.data[1];
console.log(`Custom sensor ${sensorId}: ${rawValue}`);
}
break;
case 0x20: // Multi-byte data example
if (event.data.length >= 3) {
const id = event.data[0];
const highByte = event.data[1];
const lowByte = event.data[2];
const value = (highByte << 7) | lowByte; // Reconstruct 14-bit value
console.log(`14-bit value for ID ${id}: ${value}`);
}
break;
default:
console.log('Unknown SysEx command:', event.command);
}
});Complete example showing event-driven Arduino control application.
const ArduinoFirmata = require('arduino-firmata');
const arduino = new ArduinoFirmata();
// Application state
const appState = {
connected: false,
buttons: {},
sensors: {},
ledState: false
};
arduino.connect();
// Connection management
arduino.on('connect', function() {
console.log('Arduino application connected');
appState.connected = true;
// Setup pins
arduino.pinMode(7, ArduinoFirmata.INPUT); // Button
arduino.pinMode(13, ArduinoFirmata.OUTPUT); // Status LED
// Initial status blink
blinkStatusLED(3);
});
arduino.on('boardVersion', function(version) {
console.log(`Running on Arduino firmware ${version}`);
});
// Digital input handling
arduino.on('digitalChange', function(event) {
appState.buttons[event.pin] = {
state: event.value,
changed: Date.now()
};
// Button press actions
if (event.pin === 7 && event.value === true) {
console.log('Button pressed - toggling LED');
appState.ledState = !appState.ledState;
arduino.digitalWrite(13, appState.ledState);
}
});
// Analog sensor monitoring
arduino.on('analogChange', function(event) {
appState.sensors[event.pin] = {
value: event.value,
voltage: (event.value / 1023) * 5,
updated: Date.now()
};
// Auto-brightness based on light sensor
if (event.pin === 1) {
const brightness = Math.round((event.value / 1023) * 255);
arduino.analogWrite(9, brightness);
}
});
// Custom protocol handling
arduino.on('sysex', function(event) {
if (event.command === 0x50) { // Status request
// Send status response
const statusData = [
appState.ledState ? 1 : 0,
Object.keys(appState.buttons).length,
Object.keys(appState.sensors).length
];
arduino.sysex(0x51, statusData);
}
});
// Utility functions
function blinkStatusLED(times, interval = 200) {
let count = 0;
const blinkInterval = setInterval(function() {
arduino.digitalWrite(13, count % 2 === 0);
count++;
if (count >= times * 2) {
clearInterval(blinkInterval);
arduino.digitalWrite(13, appState.ledState);
}
}, interval);
}
// Periodic status reporting
setInterval(function() {
if (appState.connected) {
console.log('Application Status:');
console.log(' LED State:', appState.ledState);
console.log(' Active Buttons:', Object.keys(appState.buttons).length);
console.log(' Active Sensors:', Object.keys(appState.sensors).length);
}
}, 10000);Best practices for managing event listeners in long-running applications.
class ArduinoController {
constructor() {
this.arduino = new ArduinoFirmata();
this.eventHandlers = {};
this.setupEventHandlers();
}
setupEventHandlers() {
// Store handlers for later cleanup
this.eventHandlers.connect = () => {
console.log('Arduino connected');
this.onConnect();
};
this.eventHandlers.digitalChange = (event) => {
this.handleDigitalChange(event);
};
this.eventHandlers.analogChange = (event) => {
this.handleAnalogChange(event);
};
this.eventHandlers.sysex = (event) => {
this.handleSysexMessage(event);
};
// Register handlers
this.arduino.on('connect', this.eventHandlers.connect);
this.arduino.on('digitalChange', this.eventHandlers.digitalChange);
this.arduino.on('analogChange', this.eventHandlers.analogChange);
this.arduino.on('sysex', this.eventHandlers.sysex);
}
connect() {
this.arduino.connect();
}
disconnect() {
// Remove event listeners before disconnecting
Object.keys(this.eventHandlers).forEach(event => {
this.arduino.removeListener(event, this.eventHandlers[event]);
});
this.arduino.close();
}
onConnect() {
// Override in subclass
}
handleDigitalChange(event) {
// Override in subclass
}
handleAnalogChange(event) {
// Override in subclass
}
handleSysexMessage(event) {
// Override in subclass
}
}
// Usage
const controller = new ArduinoController();
controller.connect();
// Clean shutdown
process.on('SIGINT', () => {
console.log('Shutting down...');
controller.disconnect();
process.exit(0);
});Arduino Firmata extends EventEmitter2, providing enhanced event capabilities:
Arduino events are processed in the Node.js event loop:
// Example: Debounced button handling
let buttonTimeout = null;
arduino.on('digitalChange', function(event) {
if (event.pin === 7) { // Button pin
// Clear existing timeout
if (buttonTimeout) {
clearTimeout(buttonTimeout);
}
// Set new timeout for debouncing
buttonTimeout = setTimeout(() => {
if (arduino.digitalRead(7) === true) {
console.log('Debounced button press confirmed');
// Handle button press
}
buttonTimeout = null;
}, 50); // 50ms debounce
}
});Install with Tessl CLI
npx tessl i tessl/npm-arduino-firmata