# System Architecture & Integration Flow ## 2. System Architecture ### 2.1 High-Level Architecture Diagram ``` ┌────────────────────────────────────────────────────────────────┐ │ Desktop Application │ │ (pywebview) │ │ │ │ ┌──────────────────────┐ │ │ │ Blockly UI │ HTML/JS │ │ │ │ │ │ │ • User assembles │ Blockly is the EXECUTOR — │ │ │ blocks visually │ not just an editor. │ │ │ • if/else, loops, │ │ │ │ variables (native)│ When encountering a robot │ │ │ • Block highlighting│ action block, Blockly calls │ │ │ during execution │ Python and WAITS for result. │ │ │ │ │ │ │ [Run] [Stop] │ │ │ └──────────┬───────────┘ │ │ │ JS ↔ Python bridge (pywebview API) │ │ │ • execute_action(command, keys, values) │ │ │ ← return: {success, message} │ │ ┌──────────▼───────────┐ │ │ │ BlocklyAPI │ Python / rclpy │ │ │ (blockly_app/app.py)│ │ │ │ │ Runs in pywebview thread; │ │ │ • Receives command │ polls futures resolved by │ │ │ from Blockly │ background spin thread. │ │ │ • Sends Action Goal │ │ │ │ • Waits for Result │ │ │ │ • Returns to JS │ │ │ └──────────┬───────────┘ │ └─────────────┼──────────────────────────────────────────────────┘ │ ROS2 Action — BlocklyAction.action │ (one action per call) ┌─────────────▼──────────────────────────────────────────────────┐ │ Executor Node │ │ (Action Server) │ │ │ │ Handles ONE action at a time. Has no concept of │ │ "program" — sequencing is controlled entirely by Blockly. │ │ │ │ ┌─────────────────────┐ │ │ │ HandlerRegistry │ Extensible command map │ │ │ • led_on │ │ │ │ • led_off │ │ │ │ • delay │ │ │ └──────────┬──────────┘ │ │ ┌──────────▼──────────┐ │ │ │ Hardware Interface │ Abstraction layer │ │ │ • DummyHardware │ for dev & test │ │ │ • GpioHardware │ for Raspberry Pi │ │ └─────────────────────┘ │ └────────────────────────────────────────────────────────────────┘ ``` ### 2.2 Threading Model The application uses a carefully designed threading model to avoid rclpy's "Executor is already spinning" error: ``` ┌─────────────────────────────────────────────────────┐ │ Process: app.py │ │ │ │ Main Thread (pywebview) │ │ ├── webview.start() ← blocks here │ │ ├── BlocklyAPI.execute_action() called from JS │ │ │ └── _wait_for_future() ← polls future.done() │ │ │ (does NOT call spin) │ │ │ │ │ Background Thread (daemon) │ │ └── MultiThreadedExecutor.spin() │ │ └── processes action client callbacks │ │ (goal response, result, feedback) │ └─────────────────────────────────────────────────────┘ ``` **Why `MultiThreadedExecutor` in the app but NOT in the executor node:** - **App (client side):** Uses [`MultiThreadedExecutor`](../src/blockly_app/blockly_app/app.py:162) because the background spin thread must process action client callbacks while the main thread polls `future.done()`. A single-threaded executor would work too, but `MultiThreadedExecutor` ensures callbacks are processed promptly. - **Executor Node (server side):** Uses simple [`rclpy.spin(node)`](../src/blockly_executor/blockly_executor/executor_node.py:123) with the default single-threaded executor. Using `MultiThreadedExecutor` with `ReentrantCallbackGroup` on the server side causes action result delivery failures with `rmw_fastrtps_cpp` — the client receives default-constructed results (`success=False, message=''`) instead of the actual values. ### 2.3 ROS2 Interface Contract Defined in [`BlocklyAction.action`](../src/blockly_interfaces/action/BlocklyAction.action): ``` # GOAL — one instruction to execute string command # e.g. "led_on", "delay", "move_forward" string[] param_keys # e.g. ["pin"] string[] param_values # e.g. ["3"] --- # RESULT — sent after action completes or fails bool success string message # success message or informative error description --- # FEEDBACK — sent during execution string status # "executing" | "done" | "error" ``` This interface is **generic by design** — adding new commands never requires modifying the `.action` file. The `command` + `param_keys`/`param_values` pattern supports any instruction with any parameters. --- --- ## 8. Blockly–ROS2 Integration Flow ### 8.1 End-to-End Execution Flow When the user presses **Run**, the following sequence occurs: ``` User presses [Run] │ ▼ ① Blockly generates JavaScript code from workspace blocks javascript.javascriptGenerator.workspaceToCode(workspace) │ ▼ ② Generated code is wrapped in async function and eval()'d (async function() { highlightBlock('block_abc123'); await executeAction('led_on', { pin: '1' }); highlightBlock('block_def456'); await executeAction('delay', { duration_ms: '500' }); })() │ ▼ ③ executeAction() calls Python via pywebview bridge window.pywebview.api.execute_action("led_on", ["pin"], ["1"]) │ ▼ ④ BlocklyAPI.execute_action() builds ROS2 Action Goal goal.command = "led_on" goal.param_keys = ["pin"] goal.param_values = ["1"] │ ▼ ⑤ Action Client sends goal asynchronously send_future = client.send_goal_async(goal) │ ▼ ⑥ _wait_for_future() polls until goal is accepted (background spin thread processes the callback) │ ▼ ⑦ Executor Node receives goal, publishes "executing" feedback │ ▼ ⑧ HandlerRegistry.execute("led_on", {"pin": "1"}) → hardware.set_led(1, True) → returns (True, "LED on pin 1 turned ON") │ ▼ ⑨ Executor Node calls goal_handle.succeed(), returns Result │ ▼ ⑩ _wait_for_future() receives result, returns to BlocklyAPI │ ▼ ⑪ BlocklyAPI returns {success: true, message: "..."} to JavaScript │ ▼ ⑫ Blockly continues to next block (await resolves) ``` ### 8.2 Code Generation Pipeline Each custom block has a **code generator** defined in its block file (e.g., [`blocks/led_on.js`](../src/blockly_app/blockly_app/ui/blockly/blocks/led_on.js)) that produces JavaScript code. For example, the `led_on` block with pin=3 generates: ```javascript highlightBlock('block_abc123'); await executeAction('led_on', { pin: '3' }); ``` Native Blockly blocks (loops, conditionals, variables) use Blockly's built-in JavaScript generators. ### 8.3 pywebview Bridge Mechanism pywebview exposes Python objects to JavaScript through `window.pywebview.api`. In [`app.py`](../src/blockly_app/blockly_app/app.py:181): ```python window = webview.create_window(..., js_api=api) ``` This makes all public methods of `BlocklyAPI` callable from JavaScript: ```javascript const result = await window.pywebview.api.execute_action("led_on", ["pin"], ["3"]); // result = { success: true, message: "LED on pin 3 turned ON" } ``` The call is **synchronous from JavaScript's perspective** — the `await` pauses Blockly's execution until Python returns. ### 8.4 Future Waiting Without Blocking The [`_wait_for_future()`](../src/blockly_app/blockly_app/app.py:26) function is the key to avoiding the "Executor is already spinning" error: ```python def _wait_for_future(future, timeout_sec=30.0): deadline = time.monotonic() + timeout_sec while not future.done(): if time.monotonic() > deadline: raise TimeoutError(...) time.sleep(0.01) # 10ms polling return future.result() ``` **Why this works:** The background thread running `MultiThreadedExecutor.spin()` processes all ROS2 callbacks, including action client responses. When a response arrives, the executor's spin loop invokes the callback which marks the future as done. The `_wait_for_future()` function simply waits for this to happen. ### 8.5 Debug Mode Flow When Debug Mode is enabled: 1. [`runDebug()`](../src/blockly_app/blockly_app/ui/blockly/core/debug-engine.js:87) wraps `executeAction` with breakpoint checking 2. Before each action, it checks if `debugState.currentBlockId` is in `activeBreakpoints` 3. If a breakpoint is hit, execution pauses via a `Promise` that only resolves when the user clicks Step Over/Step Into 4. A 300ms delay is added between blocks for visual feedback 5. Stop sets `stopRequested = true` and resolves any pending pause Promise, causing the next `executeAction` call to throw `'STOP_EXECUTION'` ---