amr-ros-k4/src/blockly_app/README.md

186 lines
12 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# blockly_app — File Reference
### 6.1 Application Layer — `blockly_app`
#### [`blockly_app/app.py`](blockly_app/app.py) — Application Entry Point
**Purpose:** Combines pywebview (desktop UI) with a ROS2 Action Client. This is the bridge between the JavaScript Blockly runtime and the ROS2 ecosystem.
**Key components:**
| Component | Description |
|---|---|
| [`_native_save_dialog()` / `_native_open_dialog()`](blockly_app/app.py:29) | Opens native OS file dialogs via `tkinter.filedialog`. Uses Tcl/Tk interpreter separate from Qt — no thread conflict. Available in pixi environment without extra dependencies. |
| [`_wait_for_future(future, timeout_sec)`](blockly_app/app.py:63) | Polls `future.done()` without calling `rclpy.spin()`. Used because the node is already being spun by a background thread. |
| [`BlocklyAPI`](blockly_app/app.py:80) | Python class exposed to JavaScript via pywebview. Its methods are callable as `window.pywebview.api.<method>()`. |
| [`BlocklyAPI.execute_action()`](blockly_app/app.py:95) | Sends a ROS2 Action Goal and blocks until the result arrives. Returns `{success: bool, message: str}` to JavaScript. |
| [`BlocklyAPI.save_workspace(json_string)`](blockly_app/app.py:145) | Opens native "Save As" dialog via tkinter, writes workspace JSON to chosen file. Returns `{success, path}` directly to JS. |
| [`BlocklyAPI.load_workspace()`](blockly_app/app.py:168) | Opens native "Open" dialog via tkinter, reads and validates JSON, returns `{success, data, path}` to JS. |
| [`main()`](blockly_app/app.py:190) | Initializes rclpy, creates the Action Client, starts the background spin thread with `MultiThreadedExecutor`, creates the pywebview window, and handles cleanup on exit. |
**Threading design:** The `main()` function starts `MultiThreadedExecutor.spin()` in a daemon thread. When JavaScript calls `execute_action()`, the method uses `_wait_for_future()` to poll for completion — it never calls `rclpy.spin_until_future_complete()`, which would conflict with the background spin.
#### [`blockly_app/ui/index.html`](blockly_app/ui/index.html) — Main UI
**Purpose:** The single HTML page that hosts the Blockly workspace, toolbar buttons, and console panel.
**Structure:**
- **Toolbar**: Run, Step Over, Step Into, Stop buttons, and Debug Mode toggle
- **Blockly Workspace**: The drag-and-drop canvas
- **Console Panel**: Scrollable log output showing execution progress
- **Script loading**: Loads Blockly vendor files, then core infrastructure, then blocks via auto-loader
**Script loading order** (fixed — never needs changing when adding blocks):
1. `vendor/blockly.min.js` + other Blockly libs
2. `blockly/core/registry.js` → `breakpoints.js` → `bridge.js` → `debug-engine.js` → `ui-controls.js` → `ui-tabs.js` → `workspace-io.js`
3. `blockly/blocks/manifest.js` + `blockly/loader.js`
4. `blockly/workspace-init.js`
5. Inline script: `loadAllBlocks().then(() => initWorkspace())`
#### [`blockly_app/ui/blockly/core/registry.js`](blockly_app/ui/blockly/core/registry.js) — Block Registry
**Purpose:** Central registration system for custom blocks. Each block file calls `BlockRegistry.register()` to self-register its visual definition, code generator, and toolbox metadata.
| Method | Description |
|---|---|
| `BlockRegistry.register(config)` | Register a block with name, category, color, definition, and generator |
| `BlockRegistry.getBlocks()` | Get all registered blocks |
| `BlockRegistry.getToolboxJSON()` | Build Blockly toolbox JSON from registered blocks + built-in categories |
#### [`blockly_app/ui/blockly/core/ui-tabs.js`](blockly_app/ui/blockly/core/ui-tabs.js) — Tab Switching
**Purpose:** Manages switching between the **Blocks** editor tab and the **Code** preview tab.
| Function | Description |
|---|---|
| `switchTab(tab)` | Shows/hides `#blockly-area` and `#code-panel`. Calls `Blockly.svgResize()` when returning to Blocks tab (canvas dimensions are zeroed while hidden). |
| `refreshCodePanel()` | Regenerates JS code from workspace via `javascript.javascriptGenerator.workspaceToCode()` and displays in `#code-output`. |
The Code tab updates automatically on every workspace change (via a change listener in `workspace-init.js`).
#### [`blockly_app/ui/blockly/core/workspace-io.js`](blockly_app/ui/blockly/core/workspace-io.js) — Workspace Export/Import
**Purpose:** Exports workspace to JSON (Save As dialog) and imports from JSON (Open dialog) via the Python bridge.
| Function | Description |
|---|---|
| `exportWorkspace()` | Serializes workspace with `Blockly.serialization.workspaces.save()`, calls `window.pywebview.api.save_workspace(json)`, logs result path. |
| `importWorkspace()` | Calls `window.pywebview.api.load_workspace()`, clears workspace, loads returned JSON with `Blockly.serialization.workspaces.load()`. |
Both functions use `async/await` — they return after the file dialog closes and the file has been read/written.
#### [`blockly_app/ui/blockly/core/bridge.js`](blockly_app/ui/blockly/core/bridge.js) — pywebview Bridge
**Purpose:** Provides `executeAction(command, params)` which calls Python via the pywebview JS-to-Python bridge. Falls back to a mock when running outside pywebview (browser dev).
#### [`blockly_app/ui/blockly/core/async-procedures.js`](blockly_app/ui/blockly/core/async-procedures.js) — Async Code Generation
**Purpose:** Overrides Blockly's built-in procedure generators for async/await support, overrides `variables_set` for debug highlight, and provides `generateCode(ws)` which replaces `workspaceToCode()`.
| Component | Description |
|---|---|
| `procedures_defreturn` override | Generates `async function` instead of `function` |
| `procedures_callreturn` override | Wraps in async IIFE with `enterFunction()` / `exitFunction()` for call depth tracking |
| `procedures_callnoreturn` override | Adds `await highlightBlock()` + call depth tracking |
| `variables_set` override | Adds `await highlightBlock()` before variable assignment for debug support |
| `generateCode(ws)` | Main-block-aware code generation. Returns plain string or `{ definitions, mainCode, hmiCode }` for concurrent execution. |
#### [`blockly_app/ui/blockly/core/debug-engine.js`](blockly_app/ui/blockly/core/debug-engine.js) — Debug Engine
**Purpose:** Implements Run, Debug, Step Over, Step Into, and Stop functionality. Supports single-program and concurrent (main+HMI) execution modes.
| Function | Description |
|---|---|
| `runProgram()` | Non-debug execution: detects single vs concurrent mode, dispatches accordingly |
| `_runSingle(code)` | Single program execution (no HMI block present) |
| `_runConcurrent(codeResult)` | Concurrent execution: main + HMI via `Promise.all()` with shared variable scope |
| `runDebug()` | Debug execution: detects single vs concurrent mode |
| `_runDebugSingle(code)` | Single program debug with breakpoints and stepping |
| `_runDebugConcurrent(codeResult)` | Concurrent debug: main has stepping, HMI runs uninterrupted |
| `stepOver()` | Resumes from pause, executes current block, pauses at next block at same call depth |
| `stepInto()` | Resumes from pause, pauses at very next highlightBlock call |
| `continueExecution()` | Resumes and runs until next breakpoint |
| `stopExecution()` | Sets stopRequested flag, cancels in-flight actions, resolves pending pause Promise |
| `highlightBlock(blockId)` | Highlights the currently executing block in the workspace |
#### [`blockly_app/ui/blockly/blocks/manifest.js`](blockly_app/ui/blockly/blocks/manifest.js) — Block Manifest
**Purpose:** Lists all block files to auto-load. This is the **only file you edit** when adding a new block (besides creating the block file itself).
```js
const BLOCK_FILES = ['digitalOut.js', 'digitalIn.js', 'delay.js'];
```
#### [`blockly_app/ui/blockly/core/hmi-manager.js`](blockly_app/ui/blockly/core/hmi-manager.js) — HMI State Manager
**Purpose:** Global `HMI` object providing LabVIEW-style Front Panel with gridstack.js grid layout. Manages widget lifecycle, mode switching (design/runtime), and layout serialization.
| Method | Description |
|---|---|
| `HMI.init()` | Initialize GridStack instance. Called once after DOM is ready. |
| `HMI.addWidget(name, type, config, blockId)` | Add widget to grid. Updates if exists. |
| `HMI.removeWidget(name)` | Remove widget from grid and DOM. |
| `HMI.setMode(mode)` | Switch between `'design'` (grid unlocked) and `'runtime'` (grid locked). |
| `HMI.setLED(name, state, color)` | Set LED on/off with color. Called from generated code. |
| `HMI.setNumber(name, value, unit)` | Display numeric value with unit label. |
| `HMI.setText(name, text)` | Display text string. |
| `HMI.setGauge(name, value, min, max)` | Display gauge bar with range. |
| `HMI.setButton(name, label, color)` | Create/configure button widget. |
| `HMI.getButton(name)` → Boolean | Latch-until-read: return `true` once per click, then auto-reset. |
| `HMI.setSlider(name, value, min, max)` | Create/configure slider widget. `_userHasInteracted` protects user drag. |
| `HMI.getSlider(name)` → Number | Return user-dragged value (or programmatic value if no interaction). |
| `HMI.setSwitch(name, state)` | Create/configure toggle switch widget. `_userState` protects user toggle. |
| `HMI.getSwitch(name)` → Boolean | Return user-toggled state (or programmatic state if no interaction). |
| `HMI.getLayout()` / `HMI.loadLayout(items)` | Serialize/restore grid positions for save/load. |
| `HMI.clearAll()` | Remove all widgets and reset grid. |
#### [`blockly_app/ui/blockly/core/hmi-preview.js`](blockly_app/ui/blockly/core/hmi-preview.js) — Design-time Widget Preview
**Purpose:** Workspace change listener that creates/removes HMI widgets as blocks are placed/deleted. Widgets appear in the HMI panel at design time, not just at runtime.
| Function | Description |
|---|---|
| `initHMIPreview(ws)` | Sets up workspace change listener. Tracks block-to-widget mapping. |
| `_handleCreate(event)` | Creates widget when HMI block is placed. |
| `_handleDelete(event)` | Removes widget when HMI block is deleted. |
| `_handleChange(event)` | Updates widget when block field changes (NAME, COLOR, UNIT, MIN, MAX). |
| `_reconcile()` | Debounced (100ms) full sync — catches undo/redo edge cases. |
| `window._hmiPreviewScan()` | Scans existing blocks after workspace import to recreate previews. |
#### [`blockly_app/ui/blockly/core/resizable-panels.js`](blockly_app/ui/blockly/core/resizable-panels.js) — Resizable Split Panels
**Purpose:** Drag-to-resize dividers between Blockly workspace, HMI panel, and console. Updates `#blockly-div` dimensions and calls `Blockly.svgResize()` on each move.
| Divider | Direction | Range |
|---------|-----------|-------|
| Vertical (Blockly↔HMI) | Horizontal drag | HMI 200px–50% viewport |
| Horizontal (workspace↔console) | Vertical drag | Console 80px–40% viewport |
#### [`blockly_app/ui/blockly/blocks/`](blockly_app/ui/blockly/blocks/) — Block Definitions
All block files registered in [`manifest.js`](blockly_app/ui/blockly/blocks/manifest.js):
| File | Category | Type | Execution Model |
|------|----------|------|-----------------|
| `mainProgram.js` | Program | Hat block | N/A (entry point) |
| `mainHmiProgram.js` | Program | Hat block | N/A (HMI entry point) |
| `print.js` | Program | Statement | Client-side (`consoleLog()`) |
| `digitalOut.js` | Robot | Statement | ROS2 action (`digital_out`) |
| `digitalIn.js` | Robot | Output | ROS2 action (`digital_in`) |
| `delay.js` | Robot | Statement | ROS2 action (`delay`) |
| `pwmWrite.js` | Robot | Statement | ROS2 action (`pwm_write`) |
| `odometryRead.js` | Robot | Output | ROS2 action (`odometry_read`) |
| `odometryGet.js` | Robot | Output | Client-side (JSON field extract) |
| `hmiSetLed.js` | HMI | Statement | Client-side (`HMI.setLED()`) |
| `hmiSetNumber.js` | HMI | Statement | Client-side (`HMI.setNumber()`) |
| `hmiSetText.js` | HMI | Statement | Client-side (`HMI.setText()`) |
| `hmiSetGauge.js` | HMI | Statement | Client-side (`HMI.setGauge()`) |
| `hmiSetButton.js` | HMI | Statement | Client-side (`HMI.setButton()`) |
| `hmiGetButton.js` | HMI | Output | Client-side (`HMI.getButton()`) |
| `hmiSetSlider.js` | HMI | Statement | Client-side (`HMI.setSlider()`) |
| `hmiGetSlider.js` | HMI | Output | Client-side (`HMI.getSlider()`) |
| `hmiSetSwitch.js` | HMI | Statement | Client-side (`HMI.setSwitch()`) |
| `hmiGetSwitch.js` | HMI | Output | Client-side (`HMI.getSwitch()`) |
Each block file calls `BlockRegistry.register()` with all metadata, so the toolbox is automatically generated. See [BLOCKS.md](BLOCKS.md) for the full guide to creating blocks.