Using SCXML with ECMAScript on Arduino ESP32 Nano

Using SCXML with ECMAScript on Arduino ESP32 Nano

This tutorial shows how to run SCXML state machines with full ECMAScript (JavaScript) support on the Arduino Nano ESP32 board using JerryScript.

Overview

The Arduino Nano ESP32 is a compact development board with:

  • ESP32-S3 processor (dual-core, 240 MHz)
  • 384 KB SRAM
  • 8 MB Flash (up to 16 MB)
  • Built-in RGB LED, USB-C, Wi-Fi, Bluetooth

This makes it ideal for running SCXML state machines with ECMAScript expressions.

What you'll learn:

  • Setting up JerryScript library for Arduino
  • Generating SCXML code with ECMAScript datamodel
  • Registering native C functions callable from JavaScript
  • Running a blinking LED state machine with JS expressions

Prerequisites:


Hardware Setup

Arduino Nano ESP32 Pinout

                    USB-C
                   ┌─────┐
              D13 ─┤     ├─ D12
              3V3 ─┤     ├─ D11
             AREF ─┤     ├─ D10
         A0 (D26) ─┤     ├─ D9
         A1 (D27) ─┤     ├─ D8
         A2 (D28) ─┤     ├─ D7
         A3 (D29) ─┤     ├─ D6
         A4 (D30) ─┤     ├─ D5  (BOOT button on some boards)
         A5 (D31) ─┤     ├─ D4
         A6 (D32) ─┤     ├─ D3
         A7 (D33) ─┤     ├─ D2
              5V  ─┤     ├─ GND
              RST ─┤     ├─ RST
              GND ─┤     ├─ D0 (RX)
              VIN ─┤     ├─ D1 (TX)
                   └─────┘

Built-in RGB LED: GPIO 48 (accent LED)
Boot Button: GPIO 0 (directly usable)

Wiring for This Tutorial

For the blinking light switch example:

Component Pin Notes
External LED+ D2 (GPIO 5) Through 220Ω resistor
External LED- GND
Push Button D5 (GPIO 0) Uses internal pull-up
Button GND GND

Or use built-in components:

  • Built-in RGB LED on GPIO 48
  • Boot button on GPIO 0

Step 1: Install Required Software

Arduino IDE Setup

  1. Install Arduino IDE 2.x

  2. Add ESP32 board support:

    • Go to File → Preferences
    • Add to "Additional Board Manager URLs":
      https://raw.githubusercontent.com/espressif/arduino-esp32/gh-pages/package_esp32_index.json
    • Go to Tools → Board → Board Manager
    • Search "esp32" and install "esp32 by Espressif Systems"
  3. Select board:

    • Tools → Board → esp32 → Arduino Nano ESP32

Install JerryScript Library

See Building JerryScript Library or:

  1. Download pre-built JerryScript-ESP32-v3.0.0.zip
  2. Sketch → Include Library → Add .ZIP Library...
  3. Select the ZIP file
  4. Restart Arduino IDE

Step 2: Create the SCXML State Machine

Create a file LightSwitchBlinkingESP32.scxml:

xml
<?xml version="1.0" encoding="UTF-8"?>
<scxml xmlns="http://www.w3.org/2005/07/scxml"
       xmlns:gen="http://vscxml.com/generator"
       version="1.0"
       datamodel="ecmascript"
       initial="init"
       name="LightSwitchBlinkingESP32">

  <!-- Generator configuration for Arduino -->
  <gen:generator lang="c">
    <gen:includes><![CDATA[
/* Arduino/ESP32 configuration */
#define SCXML_INTERNAL_QUEUE_SIZE 4
#define SCXML_SENDID_SIZE 16
#define SCXML_MAX_ACTIVE_TIMERS 4
#define SCXML_TIMER_SENDID_SIZE 16
#define SCXML_MAX_INVOKES 0
#define SCXML_INVOKEID_SIZE 1
#define SCXML_LOG_ENABLED 1

#ifdef ARDUINO
#include <Arduino.h>
#endif
]]></gen:includes>
  </gen:generator>

  <!-- Data variables (JavaScript) -->
  <datamodel>
    <data id="led_state" expr="0"/>
    <data id="blink_count" expr="0"/>
    <data id="max_blinks" expr="60"/>
  </datamodel>

  <!-- Initial state -->
  <state id="init">
    <onentry>
      <script>setLED(0);</script>
      <log label="INIT" expr="'System initialized'"/>
    </onentry>
    <transition target="off"/>
  </state>

  <!-- LED Off state -->
  <state id="off">
    <onentry>
      <log label="STATE" expr="'OFF'"/>
      <script>
        led_state = 0;
        setLED(0);
      </script>
    </onentry>
    <transition event="button" target="blinking">
      <script>blink_count = 0;</script>
    </transition>
  </state>

  <!-- Blinking state with nested states -->
  <state id="blinking" initial="blink_on">
    <onentry>
      <log label="STATE" expr="'BLINKING (press again for permanent ON)'"/>
      <!-- Auto-timeout after 12 seconds -->
      <send id="auto_off" event="timeout" delay="12s"/>
    </onentry>
    <onexit>
      <cancel sendid="auto_off"/>
    </onexit>

    <!-- Transition to permanent on -->
    <transition event="button" target="permanent_on"/>

    <!-- Timeout returns to off -->
    <transition event="timeout" target="off">
      <log label="INFO" expr="'Auto-off after timeout'"/>
    </transition>

    <!-- Blink ON sub-state -->
    <state id="blink_on">
      <onentry>
        <script>
          led_state = 1;
          setLED(1);
          blink_count = blink_count + 1;
        </script>
        <send id="blink_off_timer" event="blink" delay="200ms"/>
      </onentry>
      <onexit>
        <cancel sendid="blink_off_timer"/>
      </onexit>
      <transition event="blink" target="blink_off"/>
      <!-- Safety: stop blinking after max_blinks -->
      <transition cond="blink_count >= max_blinks" target="permanent_on"/>
    </state>

    <!-- Blink OFF sub-state -->
    <state id="blink_off">
      <onentry>
        <script>
          led_state = 0;
          setLED(0);
        </script>
        <send id="blink_on_timer" event="blink" delay="200ms"/>
      </onentry>
      <onexit>
        <cancel sendid="blink_on_timer"/>
      </onexit>
      <transition event="blink" target="blink_on"/>
    </state>
  </state>

  <!-- Permanent ON state -->
  <state id="permanent_on">
    <onentry>
      <log label="STATE" expr="'PERMANENT ON'"/>
      <script>
        led_state = 1;
        setLED(1);
      </script>
    </onentry>
    <transition event="button" target="off"/>
  </state>

</scxml>

Step 3: Generate C Code

For Arduino projects, use --code-only to generate just the state machine files. For ECMAScript support, also use --amalgamate to get the single-header runtime:

bash
# Generate state machine + amalgamated runtime (for Arduino/embedded)
scxml-gen LightSwitchBlinkingESP32.scxml \
    --target c \
    --code-only \
    --amalgamate \
    -o LightSwitchBlinkingESP32.c

This generates:

  • LightSwitchBlinkingESP32.c - State machine implementation
  • LightSwitchBlinkingESP32.h - Header with types and API
  • scxml_runtime.h - Amalgamated SCXML runtime (single-header)
note

Without --code-only, the generator creates a complete CMake project. For Arduino, you only need the state machine files plus the amalgamated runtime.


Step 4: Create Arduino Project

Project Structure

LightSwitchBlinkingESP32/
├── LightSwitchBlinkingESP32.ino    # Main sketch
├── config.h                         # Pin configuration
├── LightSwitchBlinkingESP32.c      # Generated
├── LightSwitchBlinkingESP32.h      # Generated
└── LightSwitchBlinkingESP32.cpp.h  # Generated (amalgamated)

config.h

cpp
#ifndef CONFIG_H
#define CONFIG_H

// =============================================================================
// Hardware Configuration for Arduino Nano ESP32
// =============================================================================

// LED Configuration
// Option 1: External LED on GPIO 5 (D2)
#define LED_PIN 5

// Option 2: Built-in RGB LED (uncomment to use)
// #define LED_PIN 48
// #define USE_RGB_LED

// Button Configuration
// Using the BOOT button (directly accessible on GPIO 0)
#define BUTTON_PIN 0

// Timing
#define DEBOUNCE_MS 50

#endif // CONFIG_H

LightSwitchBlinkingESP32.ino

cpp
/**
 * SCXML Light Switch with ECMAScript - Arduino Nano ESP32
 *
 * Demonstrates running an SCXML state machine with JavaScript expressions
 * using JerryScript on ESP32.
 *
 * Behavior:
 *   - Press button: Start blinking (200ms on/off cycle)
 *   - Press again while blinking: Switch to permanent ON
 *   - Press again: Turn OFF
 *   - Auto-off: Returns to OFF after 12 seconds of blinking
 *
 * The LED control is done via JavaScript: setLED(0) or setLED(1)
 */

// =============================================================================
// Build Configuration (MUST be before includes)
// =============================================================================

// Enable ECMAScript datamodel with JerryScript
#define SCXML_USE_JERRYSCRIPT 1

// Use bare-metal platform (no OS)
#define SCXML_PLATFORM_BARE_METAL 1

// Enable logging to Serial
#define SCXML_LOG_ENABLED 1

// Custom log macro for Arduino Serial output
#define SCXML_LOG(label, msg, ...) do { \
    Serial.print(F("[")); \
    Serial.print(F(label)); \
    Serial.print(F("] ")); \
    Serial.println(F(msg)); \
} while(0)

// =============================================================================
// Includes
// =============================================================================

#include <Arduino.h>
#include "config.h"

// JerryScript headers (from library)
extern "C" {
#include <jerryscript.h>
}

// SCXML ECMAScript datamodel
#include "scxml/datamodel_ecmascript.h"

// Generated state machine (amalgamated header)
#include "LightSwitchBlinkingESP32.cpp.h"

// =============================================================================
// Global Variables
// =============================================================================

// State machine instance
LightSwitchBlinkingESP32 sm;

// Timing for main loop
uint32_t last_tick_time = 0;
uint32_t last_button_time = 0;
bool last_button_state = HIGH;

// =============================================================================
// Native Functions (called from JavaScript)
// =============================================================================

/**
 * setLED(value) - Control the LED from JavaScript
 *
 * Called from SCXML: <script>setLED(1);</script>
 *
 * @param arg String argument from JS (we parse it as integer)
 * @param user_data User data (unused)
 */
extern "C" void native_setLED(const char* arg, void* user_data) {
    (void)user_data;

    int value = 0;
    if (arg != NULL) {
        value = atoi(arg);
    }

    #ifdef USE_RGB_LED
    // For built-in RGB LED (active low on some boards)
    neopixelWrite(LED_PIN, value ? 255 : 0, value ? 255 : 0, value ? 255 : 0);
    #else
    // For standard LED
    digitalWrite(LED_PIN, value ? HIGH : LOW);
    #endif

    // Debug output
    Serial.print(F("[LED] "));
    Serial.println(value ? F("ON") : F("OFF"));
}

/**
 * logMessage(msg) - Log a message from JavaScript
 *
 * Called from SCXML: <script>logMessage('Hello!');</script>
 */
extern "C" void native_logMessage(const char* arg, void* user_data) {
    (void)user_data;
    Serial.print(F("[JS] "));
    Serial.println(arg ? arg : "(null)");
}

// =============================================================================
// Setup
// =============================================================================

void setup() {
    // Initialize Serial
    Serial.begin(115200);
    while (!Serial && millis() < 3000) {
        delay(10);  // Wait up to 3s for Serial
    }

    Serial.println();
    Serial.println(F("========================================"));
    Serial.println(F("  SCXML ECMAScript Demo - Nano ESP32"));
    Serial.println(F("========================================"));

    // Initialize hardware
    Serial.println(F("[INIT] Configuring GPIO..."));

    #ifdef USE_RGB_LED
    // RGB LED doesn't need pinMode
    neopixelWrite(LED_PIN, 0, 0, 0);  // Off
    #else
    pinMode(LED_PIN, OUTPUT);
    digitalWrite(LED_PIN, LOW);
    #endif

    pinMode(BUTTON_PIN, INPUT_PULLUP);

    // Initialize JerryScript engine
    Serial.println(F("[INIT] Starting JerryScript engine..."));
    if (!scxml_ecma_init()) {
        Serial.println(F("[ERROR] Failed to initialize JerryScript!"));
        while (1) delay(1000);
    }

    Serial.print(F("[INIT] JerryScript version: "));
    Serial.println(scxml_ecma_version());

    // Initialize state machine
    Serial.println(F("[INIT] Initializing state machine..."));
    LightSwitchBlinkingESP32_init(&sm);

    // Register native functions BEFORE starting the state machine
    // These become global JavaScript functions callable from SCXML expressions
    Serial.println(F("[INIT] Registering native functions..."));

    ScxmlEcmaContext* ecma_ctx = (ScxmlEcmaContext*)sm.datamodel_ctx;

    if (!scxml_ecma_register_native(ecma_ctx, "setLED", native_setLED, NULL)) {
        Serial.println(F("[ERROR] Failed to register setLED!"));
    }

    if (!scxml_ecma_register_native(ecma_ctx, "logMessage", native_logMessage, NULL)) {
        Serial.println(F("[ERROR] Failed to register logMessage!"));
    }

    // Start the state machine
    Serial.println(F("[INIT] Starting state machine..."));
    LightSwitchBlinkingESP32_start(&sm);

    // Ready
    last_tick_time = millis();

    Serial.println();
    Serial.println(F("========================================"));
    Serial.println(F("  Ready! Press button to toggle LED"));
    Serial.println(F("========================================"));
    Serial.println();

    // Print memory info
    Serial.print(F("[INFO] Free heap: "));
    Serial.print(ESP.getFreeHeap());
    Serial.println(F(" bytes"));
}

// =============================================================================
// Main Loop
// =============================================================================

void loop() {
    uint32_t now = millis();

    // -------------------------------------------------------------------------
    // 1. Process timers (REQUIRED for delayed events like <send delay="200ms">)
    // -------------------------------------------------------------------------
    if (sm.executor != NULL) {
        if (scxml_executor_requires_tick(sm.executor)) {
            uint32_t elapsed = now - last_tick_time;
            if (elapsed > 0) {
                scxml_executor_tick(sm.executor, elapsed);
                last_tick_time = now;
            }
        }
    }

    // -------------------------------------------------------------------------
    // 2. Read button with debouncing
    // -------------------------------------------------------------------------
    bool button_state = digitalRead(BUTTON_PIN);

    // Detect falling edge (button press) with debounce
    if (button_state == LOW && last_button_state == HIGH) {
        if (now - last_button_time > DEBOUNCE_MS) {
            Serial.println(F("\n>>> Button pressed!"));

            // Send button event to state machine
            LightSwitchBlinkingESP32_send(
                &sm,
                LIGHTSWITCHBLINKINGESP32_EVT_BUTTON,
                NULL
            );

            last_button_time = now;
        }
    }
    last_button_state = button_state;

    // -------------------------------------------------------------------------
    // 3. Process any pending delayed events
    // -------------------------------------------------------------------------
    LightSwitchBlinkingESP32_process_delayed(&sm);

    // -------------------------------------------------------------------------
    // 4. Small delay to prevent tight loop
    // -------------------------------------------------------------------------
    delay(1);
}

// =============================================================================
// Optional: Error Handler Override
// =============================================================================

/**
 * Custom JerryScript fatal error handler (optional override)
 */
extern "C" void jerry_port_fatal(jerry_fatal_code_t code) {
    Serial.print(F("\n[JERRY FATAL ERROR] Code: "));
    Serial.println((int)code);
    Serial.println(F("System halted. Press RESET to restart."));
    Serial.flush();

    // Blink LED rapidly to indicate error
    #ifndef USE_RGB_LED
    pinMode(LED_PIN, OUTPUT);
    #endif

    while (1) {
        #ifdef USE_RGB_LED
        neopixelWrite(LED_PIN, 255, 0, 0);  // Red
        delay(100);
        neopixelWrite(LED_PIN, 0, 0, 0);    // Off
        delay(100);
        #else
        digitalWrite(LED_PIN, HIGH);
        delay(100);
        digitalWrite(LED_PIN, LOW);
        delay(100);
        #endif
    }
}

Step 5: Add SCXML ECMAScript Support Files

Copy these files from your scxml-gen project to your Arduino sketch folder:

Required Files

From scxml-transpiled-c/include/scxml/:

  • datamodel_ecmascript.h
  • scxml_datamodel.h

From scxml-transpiled-c/src/:

  • datamodel_ecmascript.c

Create scxml_xml_dom.h Stub

Since we don't need XML DOM support for basic usage, create a stub:

cpp
// scxml/scxml_xml_dom.h - Stub for Arduino (no XML DOM)
#ifndef SCXML_XML_DOM_H
#define SCXML_XML_DOM_H

#ifdef __cplusplus
extern "C" {
#endif

#include <jerryscript.h>
#include <stdbool.h>

static inline jerry_value_t scxml_xml_parse_to_js(const char* xml) {
    (void)xml;
    return jerry_undefined();
}

static inline bool scxml_xml_js_try_serialize(jerry_value_t value,
                                               char* buffer,
                                               size_t buffer_size) {
    (void)value;
    (void)buffer;
    (void)buffer_size;
    return false;
}

static inline void scxml_xml_cleanup_all(void) {}

#ifdef __cplusplus
}
#endif

#endif // SCXML_XML_DOM_H

Final Project Structure

LightSwitchBlinkingESP32/
├── LightSwitchBlinkingESP32.ino
├── config.h
├── LightSwitchBlinkingESP32.c
├── LightSwitchBlinkingESP32.h
├── LightSwitchBlinkingESP32.cpp.h
└── scxml/
    ├── datamodel_ecmascript.h
    ├── datamodel_ecmascript.c
    ├── scxml_datamodel.h
    └── scxml_xml_dom.h

Step 6: Upload and Test

  1. Connect your Arduino Nano ESP32 via USB-C

  2. In Arduino IDE:

    • Tools → Board → Arduino Nano ESP32
    • Tools → Port → (select your board's port)
  3. Click Upload

  4. Open Serial Monitor (115200 baud)

Expected Output

========================================
  SCXML ECMAScript Demo - Nano ESP32
========================================
[INIT] Configuring GPIO...
[INIT] Starting JerryScript engine...
[INIT] JerryScript version: 3.0.0
[INIT] Initializing state machine...
[INIT] Registering native functions...
[INIT] Starting state machine...
[INIT] System initialized
[STATE] OFF
[LED] OFF

========================================
  Ready! Press button to toggle LED
========================================

[INFO] Free heap: 245632 bytes

>>> Button pressed!
[STATE] BLINKING (press again for permanent ON)
[LED] ON
[LED] OFF
[LED] ON
[LED] OFF
...

>>> Button pressed!
[STATE] PERMANENT ON
[LED] ON

>>> Button pressed!
[STATE] OFF
[LED] OFF

Understanding the Code

How JavaScript Execution Works

  1. SCXML <script> blocks contain JavaScript code:

    xml
    <script>setLED(1);</script>
  2. JerryScript evaluates the JavaScript at runtime

  3. Native functions like setLED are registered in C and called from JS:

    cpp
    scxml_ecma_register_native(ctx, "setLED", native_setLED, NULL);
  4. The native function receives the argument as a string:

    cpp
    void native_setLED(const char* arg, void* user_data) {
        int value = atoi(arg);  // "1" -> 1
        digitalWrite(LED_PIN, value ? HIGH : LOW);
    }

Data Variables

JavaScript variables declared in <datamodel> are accessible in all expressions:

xml
<datamodel>
    <data id="blink_count" expr="0"/>
</datamodel>

<!-- Later in the state machine -->
<script>blink_count = blink_count + 1;</script>
<transition cond="blink_count >= max_blinks" target="done"/>

Memory Usage

Component Flash RAM
Arduino ESP32 core ~250 KB ~30 KB
JerryScript engine ~150 KB ~64 KB (heap)
SCXML runtime ~20 KB ~2 KB
Your state machine ~5 KB ~500 B
Available ~3.5 MB ~280 KB

Advanced: Passing Complex Data

Passing Objects to Native Functions

For more complex data, use JSON:

xml
<script>
    var data = JSON.stringify({pin: 5, value: 1});
    setGPIO(data);
</script>
cpp
#include <ArduinoJson.h>

void native_setGPIO(const char* arg, void* user_data) {
    StaticJsonDocument<64> doc;
    deserializeJson(doc, arg);

    int pin = doc["pin"];
    int value = doc["value"];

    digitalWrite(pin, value);
}

Returning Values to JavaScript

Currently, native functions return undefined. For bidirectional communication, use global variables:

cpp
// In C: Set a JS variable
scxml_ecma_assign(ctx, "sensor_value", "42");
xml
<!-- In SCXML: Read the variable -->
<transition cond="sensor_value > 40" target="alert"/>

Troubleshooting

"JerryScript version: 0.0.0"

The JerryScript library isn't properly linked. Check:

  1. Library is installed correctly
  2. #include <jerryscript.h> compiles without errors
  3. Try the basic JerryScript example first

"Failed to initialize JerryScript"

Heap allocation failed. Check:

  1. Free heap with ESP.getFreeHeap()
  2. Reduce JERRY_GLOBAL_HEAP_SIZE if needed
  3. Ensure no other large allocations before scxml_ecma_init()

State machine doesn't respond to button

  1. Check button wiring and pin configuration
  2. Verify Serial output shows "Button pressed!"
  3. Check that events match: LIGHTSWITCHBLINKINGESP32_EVT_BUTTON

LED doesn't change

  1. Verify native_setLED is being called (check Serial output)
  2. Check LED pin configuration in config.h
  3. For RGB LED, ensure USE_RGB_LED is defined

"undefined reference to scxml_ecma_*"

The ECMAScript datamodel files aren't being compiled. Ensure:

  1. datamodel_ecmascript.c is in the sketch folder
  2. SCXML_USE_JERRYSCRIPT is defined before includes

Next Steps

  • Add more native functions for sensors, displays, etc.
  • Use <send> with target="#_internal" for inter-state communication
  • Implement <invoke> for spawning child state machines
  • Add Wi-Fi connectivity and remote event triggering

References