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:
- Arduino IDE 2.x with ESP32 board support
- JerryScript Arduino library (see Building JerryScript Library)
- VSCXML-Generator-CLI
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
Install Arduino IDE 2.x
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"
Select board:
- Tools → Board → esp32 → Arduino Nano ESP32
Install JerryScript Library
See Building JerryScript Library or:
- Download pre-built
JerryScript-ESP32-v3.0.0.zip - Sketch → Include Library → Add .ZIP Library...
- Select the ZIP file
- Restart Arduino IDE
Step 2: Create the SCXML State Machine
Create a file LightSwitchBlinkingESP32.scxml:
<?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:
# 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 implementationLightSwitchBlinkingESP32.h- Header with types and APIscxml_runtime.h- Amalgamated SCXML runtime (single-header)
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
#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
/**
* 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.hscxml_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:
// 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
Connect your Arduino Nano ESP32 via USB-C
In Arduino IDE:
- Tools → Board → Arduino Nano ESP32
- Tools → Port → (select your board's port)
Click Upload
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
SCXML
<script>blocks contain JavaScript code:xml<script>setLED(1);</script>JerryScript evaluates the JavaScript at runtime
Native functions like
setLEDare registered in C and called from JS:cppscxml_ecma_register_native(ctx, "setLED", native_setLED, NULL);The native function receives the argument as a string:
cppvoid 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:
<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:
<script>
var data = JSON.stringify({pin: 5, value: 1});
setGPIO(data);
</script>
#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:
// In C: Set a JS variable
scxml_ecma_assign(ctx, "sensor_value", "42");
<!-- 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:
- Library is installed correctly
#include <jerryscript.h>compiles without errors- Try the basic JerryScript example first
"Failed to initialize JerryScript"
Heap allocation failed. Check:
- Free heap with
ESP.getFreeHeap() - Reduce
JERRY_GLOBAL_HEAP_SIZEif needed - Ensure no other large allocations before
scxml_ecma_init()
State machine doesn't respond to button
- Check button wiring and pin configuration
- Verify Serial output shows "Button pressed!"
- Check that events match:
LIGHTSWITCHBLINKINGESP32_EVT_BUTTON
LED doesn't change
- Verify
native_setLEDis being called (check Serial output) - Check LED pin configuration in
config.h - For RGB LED, ensure
USE_RGB_LEDis defined
"undefined reference to scxml_ecma_*"
The ECMAScript datamodel files aren't being compiled. Ensure:
datamodel_ecmascript.cis in the sketch folderSCXML_USE_JERRYSCRIPTis defined before includes
Next Steps
- Add more native functions for sensors, displays, etc.
- Use
<send>withtarget="#_internal"for inter-state communication - Implement
<invoke>for spawning child state machines - Add Wi-Fi connectivity and remote event triggering