Desktop Tutorial: State Machine with ECMAScript Datamodel

Desktop Tutorial: State Machine with ECMAScript Datamodel

This tutorial shows how to build the same light switch state machine for Linux and Windows using:

  • ECMAScript datamodel - JavaScript expressions evaluated at runtime
  • CMake - Cross-platform build system
  • C++ project - Modern C++ wrapper around generated C code
  • JerryScript - Lightweight JavaScript engine for embedded/IoT

Arduino vs Desktop: Key Differences

Aspect Arduino (Bare Metal) Desktop (Linux/Windows)
Datamodel native-java (compiled to C) ecmascript (runtime evaluation)
Timers Polling in main loop Background threads
Expressions Transpiled to C code Evaluated by JerryScript
Memory ~2KB RAM ~512KB heap for JerryScript
Build Arduino IDE CMake

Why ECMAScript on Desktop?

  1. Runtime flexibility - Change expressions without recompiling
  2. Rich syntax - Full JavaScript for complex logic
  3. Dynamic typing - No need to declare types
  4. Debugging - Expressions visible in logs

The State Machine

Same design as Arduino - intuitive unidirectional flow:

off ──button──► timed_on ──button──► permanent_on ──button──► off
                    │
                    └──timeout (10s)──► off

Part 1: The SCXML Definition

xml
<?xml version="1.0" encoding="UTF-8"?>
<scxml xmlns="http://www.w3.org/2005/07/scxml"
       version="1.0"
       datamodel="ecmascript"
       initial="off"
       name="LightSwitch">

    <datamodel>
        <!-- JavaScript variables - stored in JerryScript engine -->
        <data id="led_on" expr="false"/>
        <data id="press_count" expr="0"/>
        <data id="timeout_seconds" expr="10"/>
    </datamodel>

    <!-- OFF: LED is off -->
    <state id="off">
        <onentry>
            <assign location="led_on" expr="false"/>
            <log label="STATE" expr="'OFF (press count: ' + press_count + ')'"/>
        </onentry>

        <transition event="button" target="timed_on">
            <!-- JavaScript expression - increment counter -->
            <assign location="press_count" expr="press_count + 1"/>
        </transition>
    </state>

    <!-- TIMED_ON: LED on with auto-off timer -->
    <state id="timed_on">
        <onentry>
            <assign location="led_on" expr="true"/>
            <log label="STATE" expr="'TIMED_ON (' + timeout_seconds + 's auto-off)'"/>
            <!-- Dynamic delay using JavaScript expression -->
            <send id="auto_off" event="timeout" delayexpr="timeout_seconds + 's'"/>
        </onentry>

        <onexit>
            <cancel sendid="auto_off"/>
        </onexit>

        <transition event="button" target="permanent_on">
            <assign location="press_count" expr="press_count + 1"/>
        </transition>

        <transition event="timeout" target="off"/>
    </state>

    <!-- PERMANENT_ON: LED on permanently -->
    <state id="permanent_on">
        <onentry>
            <assign location="led_on" expr="true"/>
            <log label="STATE" expr="'PERMANENT_ON'"/>
        </onentry>

        <transition event="button" target="off">
            <assign location="press_count" expr="press_count + 1"/>
        </transition>
    </state>

</scxml>

ECMAScript Features Demonstrated

1. JavaScript Variables

xml
<data id="press_count" expr="0"/>

Variables are stored in JerryScript's heap, not C structs.

2. String Concatenation

xml
<log label="STATE" expr="'OFF (press count: ' + press_count + ')'"/>

Full JavaScript string operations available.

3. Dynamic Delay Expression

xml
<send id="auto_off" event="timeout" delayexpr="timeout_seconds + 's'"/>

The delayexpr attribute evaluates a JavaScript expression at runtime.

4. Runtime Expression Evaluation

All expr and cond attributes are evaluated by JerryScript, not compiled to C.


Part 2: Generate C Code

The generator creates a complete, ready-to-build project by default:

bash
scxml-gen light_switch.scxml --target c -o LightSwitch.c

Generated files:

  • LightSwitch.h - Header with API
  • LightSwitch.c - Implementation
  • scxml_runtime.h - Single-header runtime (amalgamated)
  • CMakeLists.txt - Build configuration
  • main.c - Sample application

For existing projects, use --code-only to generate just the state machine:

bash
scxml-gen light_switch.scxml --target c --code-only -o src/LightSwitch.c

Generated Code Differences (ECMAScript vs Native)

ECMAScript version uses runtime evaluation:

c
static void LightSwitch_enter_S_OFF(LightSwitch* sm) {
    SCXML_BIT_SET(LIGHTSWITCH_S_OFF, sm->configuration);

    // Variable assignment via JerryScript
    sm->datamodel_ops->assign(sm->datamodel_ctx, "led_on", "false");

    // Log with expression evaluated at runtime
    char _log_buf[256];
    sm->datamodel_ops->eval_string(sm->datamodel_ctx,
        "'OFF (press count: ' + press_count + ')'",
        _log_buf, sizeof(_log_buf));
    SCXML_LOG("STATE", _log_buf);
}

Compare to the Arduino version (using datamodel="native-java"):

c
// Native datamodel: expressions become direct C code (no runtime evaluation)
sm->data.led_on = false;
SCXML_LOG("STATE", "OFF");

Note on naming: The datamodel is called native-java because it uses Java-like expression syntax in the SCXML file. But when using --target c, those expressions are compiled to C code, not Java. There is no runtime interpreter.


Part 3: Project Structure

examples/desktop/
├── light_switch.scxml      # State machine definition
├── CMakeLists.txt          # Build configuration
├── main.cpp                # C++ application
└── generated/
    ├── LightSwitch.h       # Generated header
    └── LightSwitch.c       # Generated implementation

Part 4: CMakeLists.txt

cmake
cmake_minimum_required(VERSION 3.16)
project(light_switch_demo VERSION 1.0 LANGUAGES C CXX)

set(CMAKE_C_STANDARD 11)
set(CMAKE_CXX_STANDARD 17)

# Fetch JerryScript v3.0.0
include(FetchContent)
FetchContent_Declare(jerryscript
    GIT_REPOSITORY https://github.com/jerryscript-project/jerryscript.git
    GIT_TAG v3.0.0
    GIT_SHALLOW TRUE
)

# Configure JerryScript (512KB heap is sufficient for a single state machine)
set(JERRY_GLOBAL_HEAP_SIZE "(512)" CACHE STRING "" FORCE)
FetchContent_MakeAvailable(jerryscript)

# SCXML Runtime with ECMAScript support
set(SCXML_ROOT "${CMAKE_CURRENT_SOURCE_DIR}/../../scxml-transpiled-c")

add_library(scxml_runtime STATIC
    ${SCXML_ROOT}/src/datamodel_ecmascript.c
    ${SCXML_ROOT}/src/datamodel_null.c
    ${SCXML_ROOT}/src/scxml_trace.c
    ${SCXML_ROOT}/src/scxml_xml_dom.c
)

target_include_directories(scxml_runtime PUBLIC ${SCXML_ROOT}/include)
target_link_libraries(scxml_runtime PUBLIC jerry-core jerry-port)
target_compile_definitions(scxml_runtime PUBLIC SCXML_USE_JERRYSCRIPT=1)

# Platform threading
if(NOT WIN32)
    find_package(Threads REQUIRED)
    target_link_libraries(scxml_runtime PUBLIC Threads::Threads)
endif()

# Generated state machine
add_library(light_switch_sm STATIC generated/LightSwitch.c)
target_include_directories(light_switch_sm PUBLIC generated)
target_link_libraries(light_switch_sm PUBLIC scxml_runtime)

# Main application
add_executable(light_switch main.cpp)
target_link_libraries(light_switch PRIVATE light_switch_sm)

Part 5: C++ Application (main.cpp)

cpp
#include <iostream>
#include <string>
#include <thread>
#include <atomic>
#include <chrono>

extern "C" {
#include "scxml/scxml_transpiled.h"
#include "scxml/scxml_datamodel.h"
#include "LightSwitch.h"
}

std::atomic<bool> running{true};
LightSwitch sm;

// LED visualization
void print_led_state() {
    std::cout << "\n+-------------------------------------+\n";

    if (LightSwitch_is_in(&sm, LIGHTSWITCH_S_OFF)) {
        std::cout << "|           LED: [ OFF ]              |\n";
        std::cout << "|  State: OFF                         |\n";
    } else if (LightSwitch_is_in(&sm, LIGHTSWITCH_S_TIMED_ON)) {
        std::cout << "|           LED: [*ON*]               |\n";
        std::cout << "|  State: TIMED_ON (auto-off 10s)     |\n";
    } else if (LightSwitch_is_in(&sm, LIGHTSWITCH_S_PERMANENT_ON)) {
        std::cout << "|           LED: [*ON*]               |\n";
        std::cout << "|  State: PERMANENT_ON                |\n";
    }

    std::cout << "|  [Enter] = button   [q] = quit      |\n";
    std::cout << "+-------------------------------------+\n> ";
}

// Background thread processes timer events
void event_processor_thread() {
    int last_state = -1;
    while (running) {
        // Process delayed events (timer callbacks queue events here)
        LightSwitch_process_delayed(&sm);

        // Check for state change
        int current = LightSwitch_is_in(&sm, LIGHTSWITCH_S_OFF) ? 0 :
                      LightSwitch_is_in(&sm, LIGHTSWITCH_S_TIMED_ON) ? 1 : 2;
        if (current != last_state) {
            last_state = current;
            print_led_state();
        }

        std::this_thread::sleep_for(std::chrono::milliseconds(50));
    }
}

int main() {
    std::cout << "Light Switch Demo (ECMAScript Datamodel)\n\n";

    // Initialize state machine (creates JerryScript context)
    LightSwitch_init(&sm);
    LightSwitch_start(&sm);

    print_led_state();

    // Start background event processor
    std::thread processor(event_processor_thread);

    // Input loop
    std::string input;
    while (running) {
        std::getline(std::cin, input);
        if (input == "q") { running = false; break; }

        std::cout << ">> Button pressed!\n";
        LightSwitch_send(&sm, LIGHTSWITCH_EVT_BUTTON, nullptr);
    }

    processor.join();
    LightSwitch_destroy(&sm);
    return 0;
}

Part 6: How Desktop Timers Work

Unlike Arduino's polling approach, desktop platforms use background threads:

┌─────────────────────────────────────────────────────────────────┐
│                     Main Thread                                 │
├─────────────────────────────────────────────────────────────────┤
│ 1. LightSwitch_send(BUTTON)                                     │
│ 2. State machine enters timed_on                                │
│ 3. <send delay="10s"> creates timer                             │
│    └──► Spawns background thread                                │
│ 4. Main thread continues (non-blocking)                         │
└─────────────────────────────────────────────────────────────────┘

┌─────────────────────────────────────────────────────────────────┐
│                   Timer Thread (Background)                     │
├─────────────────────────────────────────────────────────────────┤
│ 1. Sleep for 10 seconds                                         │
│ 2. Wake up, call LightSwitch_send(TIMEOUT)                      │
│ 3. Event queued to state machine                                │
│ 4. Thread exits                                                 │
└─────────────────────────────────────────────────────────────────┘

┌─────────────────────────────────────────────────────────────────┐
│                  Event Processor Thread                         │
├─────────────────────────────────────────────────────────────────┤
│ while (running) {                                               │
│     LightSwitch_process_delayed(&sm);  // Handle queued events  │
│     sleep(50ms);                                                │
│ }                                                               │
└─────────────────────────────────────────────────────────────────┘

No Polling Required!

On desktop, you don't need:

cpp
// NOT needed on desktop:
scxml_bare_metal_tick(elapsed);
scxml_timers_process();

The platform headers (platform_posix.h / platform_windows.h) spawn threads for timers automatically.

You only need:

cpp
LightSwitch_process_delayed(&sm);  // Process queued events

Part 7: Build and Run

Linux

bash
# Generate C code
scxml-gen light_switch.scxml --target c -o generated/LightSwitch.c

# Build
cmake -B build
cmake --build build

# Run
./build/light_switch

Windows (Visual Studio)

powershell
# Generate C code
scxml-gen light_switch.scxml --target c -o generated/LightSwitch.c

# Build
cmake -B build -G "Visual Studio 17 2022"
cmake --build build --config Release

# Run
.\build\Release\light_switch.exe

Windows (MinGW)

bash
cmake -B build -G "MinGW Makefiles"
cmake --build build
./build/light_switch.exe

Part 8: User Interaction

Unlike Arduino with a physical button, the desktop demo uses keyboard input:

Key Action
Enter Send button event (simulates button press)
q + Enter Quit the program

The input loop is simple:

cpp
std::string input;
while (running) {
    std::getline(std::cin, input);
    if (input == "q") break;

    // Any input (including just Enter) = button press
    LightSwitch_send(&sm, LIGHTSWITCH_EVT_BUTTON, nullptr);
}

Part 9: Expected Output

Light Switch Demo (ECMAScript Datamodel)

+-------------------------------------+
|           LED: [ OFF ]              |
|  State: OFF                         |
|  [Enter] = button   [q] = quit      |
+-------------------------------------+
>
>> Button pressed!

+-------------------------------------+
|           LED: [*ON*]               |
|  State: TIMED_ON (auto-off 10s)     |
|  [Enter] = button   [q] = quit      |
+-------------------------------------+
>

Wait 10 seconds without pressing:

+-------------------------------------+
|           LED: [ OFF ]              |
|  State: OFF                         |
|  [Enter] = button   [q] = quit      |
+-------------------------------------+
>

Part 10: How Events Are Raised

Events can come from three sources:

1. External Code (C++ Application)

cpp
// Send event by ID (fastest)
LightSwitch_send(&sm, LIGHTSWITCH_EVT_BUTTON, nullptr);

// Send event by name (for dynamic events)
LightSwitch_send_name(&sm, "button", nullptr);

2. SCXML <raise> - Internal Events

xml
<onentry>
    <raise event="internal_check"/>
</onentry>

Internal events are processed immediately in the current macrostep (before external events).

3. SCXML <send> - External Events

xml
<!-- Immediate external event -->
<send event="done"/>

<!-- Delayed event (creates timer) -->
<send event="timeout" delay="10s"/>

<!-- Dynamic delay -->
<send event="timeout" delayexpr="timeout_seconds + 's'"/>

External events are queued and processed in the next macrostep.

Event Processing Order (W3C Compliant)

1. Process all internal events (from <raise>)
2. Check eventless transitions
3. Process ONE external event
4. Repeat

Part 11: ECMAScript vs Native - When to Use Which

Use ECMAScript When Use Native When
Desktop/server applications Embedded/microcontrollers
Complex business logic Minimal footprint needed
Runtime configuration needed Maximum performance
Rapid prototyping No dynamic memory available
Full JavaScript syntax needed Compile-time optimization

Memory comparison:

  • ECMAScript: ~512KB heap for JerryScript (configurable)
  • Native: ~500 bytes RAM total

Part 12: Accessing Variables from C++

ECMAScript variables live in JerryScript. To access them from C++:

cpp
// Read a variable
char buffer[64];
sm.datamodel_ops->eval_string(sm.datamodel_ctx, "press_count", buffer, sizeof(buffer));
int count = std::stoi(buffer);

// Modify a variable
sm.datamodel_ops->assign(sm.datamodel_ctx, "timeout_seconds", "30");

For native datamodel (Arduino), variables are direct C struct fields:

cpp
int count = sm.data.press_count;  // Direct access
sm.data.timeout_seconds = 30;     // Direct modification

Key Takeaways

  1. datamodel="ecmascript" - Expressions evaluated by JerryScript at runtime

  2. Background threads for timers - No polling needed on desktop

  3. CMake + FetchContent - Automatic JerryScript download and build

  4. process_delayed() only - No tick/timers_process on desktop

  5. Same SCXML, different datamodel - Switch between native and ECMAScript by changing one attribute


Source Files


Next Steps

  • Add complex expressions: Use JavaScript objects, arrays, string methods
  • Runtime reconfiguration: Modify timeout_seconds via UI
  • Multiple machines: Run several state machines with shared JerryScript context
  • Invoke child machines: Use <invoke> for hierarchical state machines