amr-ros-k4/docs/architecture.md

228 lines
12 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

# 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. BlocklyROS2 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'`
---