CtrlK
BlogDocsLog inGet started
Tessl Logo

tessl/npm-arduino-firmata

Node.js implementation of the Arduino Firmata protocol for controlling Arduino boards from JavaScript applications

Overview
Eval results
Files

events.mddocs/

Event System

Real-time event monitoring for pin state changes, board connection status, and custom SysEx messages using EventEmitter2 for comprehensive Arduino communication.

Capabilities

Connection Events

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)

Connect Event

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);
});

Board Ready Event

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
});

Board Version Event

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');
  }
});

Digital I/O Events

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
}

Digital Change Monitoring

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
  }
});

Multi-pin Digital Monitoring

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);
  }
});

Analog I/O Events

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
}

Analog Change Monitoring

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`);
  }
});

Sensor Data Logging

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

SysEx Events

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
}

SysEx Message Handling

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);
  }
});

Event-Driven Application Example

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

Event Listener Management

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);
});

Event System Architecture

EventEmitter2 Integration

Arduino Firmata extends EventEmitter2, providing enhanced event capabilities:

  • Wildcard events: Listen to multiple event types with patterns
  • Namespaced events: Organize events with dot notation
  • Event priority: Control event handler execution order
  • Maximum listeners: Configure listener limits per event

Event Loop Considerations

Arduino events are processed in the Node.js event loop:

  • Events are fired asynchronously when data arrives from Arduino
  • Event handlers should be non-blocking for optimal performance
  • Heavy processing should be delegated to worker threads or queued

Performance Guidelines

  1. Keep handlers fast: Avoid blocking operations in event handlers
  2. Debounce noisy signals: Use timers for switch debouncing
  3. Batch operations: Group related I/O operations together
  4. Remove unused listeners: Clean up event handlers when not needed
  5. Monitor memory: Check for event listener memory leaks in long-running apps
// 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

docs

analog-io.md

connection.md

digital-io.md

events.md

index.md

servo.md

sysex.md

tile.json