**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):
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
Each custom block has a **code generator** defined in its block file (e.g., [`blocks/digitalOut.js`](../src/blockly_app/blockly_app/ui/blockly/blocks/digitalOut.js)) that produces JavaScript code. For example, the `digitalOut` block with gpio=17 and state=true generates:
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'`