diff --git a/DOCUMENTATION.md b/DOCUMENTATION.md index 2b86fb9..c76f58c 100644 --- a/DOCUMENTATION.md +++ b/DOCUMENTATION.md @@ -9,12 +9,12 @@ See [readme.md](readme.md) for project overview and status. | Topic | File | |---|---| -| System architecture & Blockly–ROS2 integration flow | [docs/architecture.md](docs/architecture.md) | +| System architecture, HMI Panel & concurrent execution | [docs/architecture.md](docs/architecture.md) | | Installation, directory structure & running | [docs/installation.md](docs/installation.md) | | Troubleshooting & known issues | [docs/troubleshooting.md](docs/troubleshooting.md) | | Guide: adding a new ROS2 package | [docs/ros2-package-guide.md](docs/ros2-package-guide.md) | -| `blockly_app` — file reference | [src/blockly_app/README.md](src/blockly_app/README.md) | -| `blockly_app` — creating custom blocks (full guide + reference) | [src/blockly_app/BLOCKS.md](src/blockly_app/BLOCKS.md) | +| `blockly_app` — file reference (incl. HMI core modules) | [src/blockly_app/README.md](src/blockly_app/README.md) | +| `blockly_app` — creating blocks (ROS2 action + client-side + HMI) | [src/blockly_app/BLOCKS.md](src/blockly_app/BLOCKS.md) | | `blockly_executor` — file reference, handlers & testing guide | [src/blockly_executor/README.md](src/blockly_executor/README.md) | | `blockly_interfaces` — ROS2 action & message interfaces | [src/blockly_interfaces/README.md](src/blockly_interfaces/README.md) | | `gpio_node` — Raspberry Pi GPIO node (C++, libgpiod) | [src/gpio_node/README.md](src/gpio_node/README.md) | diff --git a/docs/architecture.md b/docs/architecture.md index 762468d..f781b4e 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -18,7 +18,10 @@ │ │ variables (native)│ When encountering a robot │ │ │ • Block highlighting│ action block, Blockly calls │ │ │ during execution │ Python and WAITS for result. │ -│ │ │ │ +│ │ • HMI Panel (right) │ │ +│ │ LED/Number/Text/ │ Client-side blocks (print, HMI) │ +│ │ Gauge widgets │ call JS functions directly — no │ +│ │ │ ROS2 round-trip. │ │ │ [Run] [Stop] │ │ │ └──────────┬───────────┘ │ │ │ JS ↔ Python bridge (pywebview API) │ @@ -115,9 +118,125 @@ This interface is **generic by design** — adding new commands never requires m --- +### 2.4 HMI Panel — LabVIEW-style Front Panel + +Terinspirasi oleh LabVIEW yang memiliki **Front Panel** (controls & indicators) dan **Block Diagram** (visual programming). Blockly sudah menjadi "Block Diagram". HMI Panel adalah "Front Panel" — menampilkan widget indikator (LED, Number, Text, Gauge) yang dikontrol programmatically dari generated code. + +``` +┌────────────────────────────────────────────────────────────────────────┐ +│ Toolbar: [Run] [Debug] [Step] [Stop] [Blocks|Code] [Save] [Open]│ +├──────────────────────────┬──────────┬──────────────────────────────────┤ +│ │ drag ↔ │ │ +│ Blockly Workspace │ divider │ HMI Panel (gridstack.js) │ +│ │ │ │ +│ ┌─────────────────────┐ │ │ ┌──────┐ ┌──────────────────┐ │ +│ │ Main Program │ │ │ │ LED1 │ │ Heading [rad] │ │ +│ │ set [odom] to ... │ │ │ │ * ON │ │ 1.57 │ │ +│ │ digital_out(17,1) │ │ │ └──────┘ └──────────────────┘ │ +│ │ delay(1000) │ │ │ ┌──────────────────────────┐ │ +│ └─────────────────────┘ │ │ │ Speed [cm/s] │ │ +│ ┌─────────────────────┐ │ │ │ 42.5 │ │ +│ │ HMI Program │ │ │ └──────────────────────────┘ │ +│ │ HMI LED "LED1" ... │ │ │ ┌──────────────────────────┐ │ +│ │ HMI Number "Hd".. │ │ │ │ Battery ### 72% │ │ +│ └─────────────────────┘ │ │ └──────────────────────────┘ │ +│ │ │ │ +├──────────────────────────┴──────────┴──────────────────────────────────┤ +│ drag ↕ divider │ +├────────────────────────────────────────────────────────────────────────┤ +│ Console: === Program started === │ +│ USER LOG >>> Hello World │ +│ === Program completed === │ +└────────────────────────────────────────────────────────────────────────┘ +``` + +**Key components:** + +| Module | File | Fungsi | +|--------|------|--------| +| HMI Manager | `core/hmi-manager.js` | Global `HMI` object — `setLED()`, `setNumber()`, `setText()`, `setGauge()`, `clearAll()`. GridStack integration, layout serialization, mode management (design/runtime). | +| HMI Preview | `core/hmi-preview.js` | Workspace change listener — widgets appear/disappear saat block di-place/delete (design-time preview). Reconcile function handles undo/redo edge cases. | +| Resizable Panels | `core/resizable-panels.js` | Drag-to-resize dividers: vertical (Blockly↔HMI) dan horizontal (workspace↔console). Auto-resize Blockly canvas via `Blockly.svgResize()`. | + +**Two modes:** +- **Design mode**: Grid unlocked (drag/resize widgets), preview values, dimmed appearance. Active saat tidak ada program berjalan. +- **Runtime mode**: Grid locked, live values dari running code, bright appearance. Active saat program berjalan. + +**Widget types:** + +| Widget | JS API | Fungsi | +|--------|--------|--------| +| LED | `HMI.setLED(name, state, color)` | On/off indicator with configurable color | +| Number | `HMI.setNumber(name, value, unit)` | Numeric display with unit label | +| Text | `HMI.setText(name, text)` | Text string display | +| Gauge | `HMI.setGauge(name, value, min, max)` | Horizontal bar gauge with range | + +### 2.5 Concurrent Execution — Main Program + HMI Program + +Saat workspace memiliki **dua program block** (`main_program` + `main_hmi_program`), keduanya berjalan bersamaan via `Promise.all()`. + +``` +generateCode(workspace) + | + v +Returns { definitions, mainCode, hmiCode } + | + v +debug-engine.js detects structured result -> _runConcurrent() + | + v +Single execution with shared scope: ++----------------------------------------------------------+ +| (async function() { | +| // -- Shared definitions (variables, functions) ----- | +| var myVar; | +| async function myFunc() { ... } | +| | +| var _main = (async function() { | +| // Main program -- has highlightBlock (visual+stop) | +| await highlightBlock('...'); | +| await executeAction('digital_out', {...}); | +| })(); | +| | +| var _hmi = (async function() { | +| // HMI program -- highlightBlock shadowed to no-op | +| var highlightBlock = async function() { | +| if (debugState.stopRequested) throw ...; | +| }; | +| while (!debugState.stopRequested) { | +| HMI.setLED('LED1', true, '#4caf50'); | +| HMI.setNumber('X', myVar); | +| await new Promise(r => setTimeout(r, 50)); //20Hz | +| } | +| })(); | +| | +| await _main; // main drives completion| +| debugState.stopRequested = true; // signal HMI to stop| +| await _hmi; // wait for HMI cleanup | +| })() | ++----------------------------------------------------------+ +``` + +**Design decisions:** +- **Shared scope**: Definitions (variables, functions) ada di outer IIFE — kedua program share via closure. Variable yang diubah di main langsung terlihat di HMI. +- **Main drives completion**: Saat main selesai, `stopRequested = true` -> HMI loop keluar. +- **HMI full speed**: `highlightBlock` di-shadow ke async no-op (hanya check stop). Tidak ada visual delay. +- **Debug mode**: Hanya main program yang memiliki stepping/breakpoints. HMI berjalan tanpa interupsi. + +### 2.6 Two Block Execution Models + +Blocks terbagi dua berdasarkan cara eksekusi: + +| Model | Blocks | Mechanism | Latency | +|-------|--------|-----------|---------| +| **ROS2 action** | `digitalOut`, `digitalIn`, `delay`, `pwmWrite`, `odometryRead` | `await executeAction()` -> Python -> ROS2 -> handler -> result | Network round-trip (~10-100ms) | +| **Client-side** | `print`, `hmiSetLed`, `hmiSetNumber`, `hmiSetText`, `hmiSetGauge` | Direct JS function call (`consoleLog()`, `HMI.set*()`) | Instant (~0ms) | + +Client-side blocks **tidak membutuhkan Python handler** — generator langsung memanggil fungsi JavaScript global. Ini membuat HMI responsif karena tidak ada overhead jaringan. + --- -## 8. Blockly–ROS2 Integration Flow +## 8. Blockly-ROS2 Integration Flow ### 8.1 End-to-End Execution Flow @@ -183,11 +302,23 @@ User presses [Run] 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: ```javascript -highlightBlock('block_abc123'); +await highlightBlock('block_abc123'); await executeAction('digital_out', { gpio: '17', state: 'true' }); ``` -Native Blockly blocks (loops, conditionals, variables) use Blockly's built-in JavaScript generators. +Client-side blocks (print, HMI) generate direct JS function calls instead of `executeAction`: + +```javascript +await highlightBlock('block_def456'); +consoleLog(String(' USER LOG >>> ' + myVar), 'print'); + +await highlightBlock('block_ghi789'); +HMI.setLED('LED1', Boolean(true), '#4caf50'); +``` + +`generateCode(ws)` (in [`async-procedures.js`](../src/blockly_app/blockly_app/ui/blockly/core/async-procedures.js)) replaces direct `workspaceToCode()`. When a `main_program` block exists, only function definitions + main body are generated. See §8.6 for full details. + +Native Blockly blocks (loops, conditionals) use Blockly's built-in generators. `variables_set` is overridden to add `highlightBlock()` for debug support. ### 8.3 pywebview Bridge Mechanism @@ -226,10 +357,37 @@ def _wait_for_future(future, timeout_sec=30.0): 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'` +1. [`runDebug()`](../src/blockly_app/blockly_app/ui/blockly/core/debug-engine.js:200) wraps `executeAction` with breakpoint checking +2. `highlightBlock()` is overridden to an async version that can pause execution +3. All pause logic lives in the async `highlightBlock()` override — this is the single pause point +4. Pause conditions: first block, breakpoints, step-over/step-into boundaries +5. A 300ms delay is added between blocks for visual feedback (auto-run mode) +6. Stop sets `stopRequested = true` and resolves any pending pause Promise, causing the next `highlightBlock` call to throw `'STOP_EXECUTION'` + +**Call depth tracking** (for Step Over): +- `enterFunction()` / `exitFunction()` injected around procedure calls by `async-procedures.js` +- Step Over skips blocks deeper than `stepStartDepth` (function bodies) +- Step Into pauses at every `highlightBlock()` regardless of depth + +**Concurrent debug mode** (main + HMI programs): +- Main program: full debug stepping (breakpoints, step over/into, visual highlighting) +- HMI program: runs uninterrupted at full speed — `highlightBlock` shadowed to async stop check +- Both share variable scope via closure in single outer IIFE + +### 8.6 Code Generation Pipeline — `generateCode(ws)` + +[`async-procedures.js`](../src/blockly_app/blockly_app/ui/blockly/core/async-procedures.js) provides `generateCode(ws)` which replaces `workspaceToCode()`: + +| Workspace state | Return value | Execution path | +|----------------|-------------|----------------| +| No `main_program` block | Plain string (backward compatible) | `_runSingle()` | +| `main_program` only | Plain string (definitions + main body) | `_runSingle()` | +| `main_program` + `main_hmi_program` | `{ definitions, mainCode, hmiCode }` | `_runConcurrent()` | + +**Built-in generator overrides** in `async-procedures.js`: +- `procedures_defreturn` / `procedures_defnoreturn` → generates `async function` instead of `function` +- `procedures_callreturn` → wraps in async IIFE with `enterFunction()` / `exitFunction()` +- `procedures_callnoreturn` → adds `await highlightBlock()` + call depth tracking +- `variables_set` → adds `await highlightBlock()` before variable assignment --- diff --git a/readme.md b/readme.md index db70232..f25ff04 100644 --- a/readme.md +++ b/readme.md @@ -36,7 +36,7 @@ jelaskan apa yang dimaksut untuk menyelesaikan task # Potential Enhancements this list is short by priority -- **Blockly UI Enhancement**: +- **add more block in HMI**: i like to add block button, slider, and switch - **Feasibility Study to implement Controller**: mobile robot need controller to move flawlesly. - **Launch files**: ROS2 launch files to start all nodes with one command includ node in raspberry pi - **Simulation**: Integrate with Gazebo/Isaac Sim for testing Kiwi Wheel kinematics before deploying to hardware diff --git a/src/blockly_app/BLOCKS.md b/src/blockly_app/BLOCKS.md index 1962ce5..7463f84 100644 --- a/src/blockly_app/BLOCKS.md +++ b/src/blockly_app/BLOCKS.md @@ -899,7 +899,171 @@ BlockRegistry.register({ --- -### 7.15 `executeAction()` and `highlightBlock()` Reference +### 7.15 Template E — Client-side Statement Block (no Python handler) + +Client-side blocks call JavaScript functions directly — no `executeAction()`, no Python handler, no ROS2 round-trip. Used for **print** (console output) and **HMI widgets** (LED, Number, Text, Gauge). + +**Print block** — calls `consoleLog()` directly: + +```js +// src/blockly_app/blockly_app/ui/blockly/blocks/print.js + +BlockRegistry.register({ + name: 'print', + category: 'Program', + categoryColor: '#FF9800', + color: '#FFCA28', + tooltip: 'Print a value to the console for debugging', + + definition: { + init: function () { + this.appendValueInput('TEXT') + .appendField('Print'); + this.setPreviousStatement(true, null); + this.setNextStatement(true, null); + this.setColour('#FFCA28'); + this.setTooltip('Print a value to the console for debugging'); + }, + }, + + generator: function (block) { + var value = Blockly.JavaScript.valueToCode( + block, 'TEXT', Blockly.JavaScript.ORDER_ATOMIC + ) || "''"; + return ( + "await highlightBlock('" + block.id + "');\n" + + "consoleLog(String( ' USER LOG >>> ' + " + value + "), 'print');\n" + // ^^^^^^^^^^^^^^^^ direct JS function call — no executeAction! + ); + }, +}); +``` + +**No Python handler needed.** The block works entirely in the browser. Add the JS filename to `manifest.js` and it's done. + +--- + +### 7.16 Template F — HMI Widget Block (client-side, design-time preview) + +HMI blocks create/update widgets in the HMI Panel. They call `HMI.set*()` functions directly (client-side) and support design-time preview — widgets appear when blocks are placed, not just at runtime. + +**HMI LED block:** + +```js +// src/blockly_app/blockly_app/ui/blockly/blocks/hmiSetLed.js + +BlockRegistry.register({ + name: 'hmiSetLed', + category: 'HMI', + categoryColor: '#00BCD4', + color: '#00BCD4', + tooltip: 'Set an LED indicator on/off in the HMI panel', + + definition: { + init: function () { + this.appendValueInput('STATE') + .appendField('HMI LED') + .appendField(new Blockly.FieldTextInput('LED1'), 'NAME') + .appendField('color') + .appendField(new Blockly.FieldDropdown([ + ['green', '#4caf50'], + ['red', '#f44336'], + ['yellow', '#ffeb3b'], + ['blue', '#2196f3'], + ['orange', '#ff9800'], + ['white', '#ffffff'], + ]), 'COLOR') + .setCheck('Boolean') + .appendField('state:'); + this.setPreviousStatement(true, null); + this.setNextStatement(true, null); + this.setColour('#00BCD4'); + this.setTooltip('Set an LED indicator on/off in the HMI panel'); + }, + }, + + generator: function (block) { + var name = block.getFieldValue('NAME'); + var color = block.getFieldValue('COLOR'); + var state = Blockly.JavaScript.valueToCode( + block, 'STATE', Blockly.JavaScript.ORDER_ATOMIC + ) || 'false'; + return ( + "await highlightBlock('" + block.id + "');\n" + + "HMI.setLED('" + name + "', Boolean(" + state + "), '" + color + "');\n" + // ^^^^^^^^^^ direct call to HMI manager — no executeAction! + ); + }, +}); +``` + +**HMI Number block** — numeric display with unit: + +```js +generator: function (block) { + var name = block.getFieldValue('NAME'); + var unit = block.getFieldValue('UNIT'); + var value = Blockly.JavaScript.valueToCode( + block, 'VALUE', Blockly.JavaScript.ORDER_ATOMIC + ) || '0'; + return ( + "await highlightBlock('" + block.id + "');\n" + + "HMI.setNumber('" + name + "', Number(" + value + "), '" + unit + "');\n" + ); + }, +``` + +**HMI Gauge block** — bar gauge with min/max: + +```js +generator: function (block) { + var name = block.getFieldValue('NAME'); + var min = block.getFieldValue('MIN'); + var max = block.getFieldValue('MAX'); + var value = Blockly.JavaScript.valueToCode( + block, 'VALUE', Blockly.JavaScript.ORDER_ATOMIC + ) || '0'; + return ( + "await highlightBlock('" + block.id + "');\n" + + "HMI.setGauge('" + name + "', Number(" + value + "), " + min + ", " + max + ");\n" + ); + }, +``` + +**Key differences from ROS2 action blocks:** +- Generator calls `HMI.set*()` instead of `executeAction()` +- No Python handler file needed +- Widgets get design-time preview via `hmi-preview.js` (auto-detected by block type) +- All HMI blocks use `FieldTextInput('Name')` for widget identifier (`NAME` field) +- Category: `HMI`, color: `#00BCD4` + +**Adding a new HMI widget type:** +1. Add render function `_renderFoo(body, widget)` in `core/hmi-manager.js` +2. Add public API method `setFoo(name, ...)` that stores state and calls `_scheduleRender(name)` +3. Add `case 'foo':` to the `_render()` switch +4. Create block file `blocks/hmiFoo.js` — generator calls `HMI.setFoo(...)` +5. Add to `manifest.js` +6. Add entry to `HMI_BLOCK_TYPES` in `core/hmi-preview.js` for design-time preview + +--- + +### 7.17 Block Type Overview — All Categories + +| Category | Blocks | Execution Model | +|----------|--------|-----------------| +| **Program** | `main_program`, `main_hmi_program`, `print` | Entry points / client-side | +| **Robot** | `digitalOut`, `digitalIn`, `delay`, `pwmWrite`, `odometryRead`, `odometryGet` | ROS2 action (except `odometryGet`) | +| **HMI** | `hmiSetLed`, `hmiSetNumber`, `hmiSetText`, `hmiSetGauge` | Client-side (`HMI.set*()`) | +| **Logic** | `controls_if`, `logic_compare`, `logic_operation`, `logic_boolean` | Built-in Blockly | +| **Loops** | `controls_repeat_ext`, `controls_whileUntil` | Built-in Blockly | +| **Math** | `math_number`, `math_arithmetic` | Built-in Blockly | +| **Text** | `text`, `text_join`, `text_length` | Built-in Blockly | +| **Variables** | `variables_set`, `variables_get` | Built-in Blockly (with highlight override) | +| **Functions** | `procedures_defnoreturn`, `procedures_callnoreturn`, etc. | Built-in Blockly (with async override) | + +--- + +### 7.18 `executeAction()` and `highlightBlock()` Reference Both functions are provided by [bridge.js](blockly_app/ui/blockly/core/bridge.js) and injected into the eval context by [debug-engine.js](blockly_app/ui/blockly/core/debug-engine.js). @@ -932,7 +1096,7 @@ Visually highlights the currently executing block in the workspace. Call it **fi --- -### 7.16 Quick Reference: Blockly Field Types +### 7.19 Quick Reference: Blockly Field Types | Field | Constructor | Returns | Use Case | |-------|-------------|---------|----------| @@ -948,7 +1112,7 @@ All field values retrieved via `block.getFieldValue('FIELD_NAME')` are **strings --- -### 7.17 Quick Reference: Input and Connection Types +### 7.20 Quick Reference: Input and Connection Types | Method | What it creates | When to use | |--------|----------------|-------------| @@ -963,7 +1127,7 @@ All field values retrieved via `block.getFieldValue('FIELD_NAME')` are **strings --- -### 7.18 Naming Conventions +### 7.21 Naming Conventions | Item | Convention | Example | |------|-----------|---------| @@ -976,11 +1140,14 @@ All field values retrieved via `block.getFieldValue('FIELD_NAME')` are **strings --- -### 7.19 Step-by-Step Checklist — Adding a New Block +### 7.22 Step-by-Step Checklist — Adding a New Block + +**Path A — ROS2 Action Block** (robot commands, sensor reads): ``` 1. Create src/blockly_app/…/ui/blockly/blocks/.js └─ Call BlockRegistry.register({ name, category, color, definition, generator }) + └─ generator calls await executeAction('name', { key: 'value' }) └─ name must exactly match the Python @handler string 2. Edit src/blockly_app/…/ui/blockly/blocks/manifest.js @@ -998,6 +1165,20 @@ All field values retrieved via `block.getFieldValue('FIELD_NAME')` are **strings pixi run test -- src/blockly_executor/test/test_block_.py -v ``` +**Path B — Client-side Block** (print, HMI, pure JS): + +``` +1. Create src/blockly_app/…/ui/blockly/blocks/.js + └─ Call BlockRegistry.register({ name, category, color, definition, generator }) + └─ generator calls JS function directly (consoleLog(), HMI.set*(), etc.) + └─ NO executeAction(), NO Python handler needed + +2. Edit src/blockly_app/…/ui/blockly/blocks/manifest.js + └─ Add '.js' to the BLOCK_FILES array + +3. Test pixi run app — drag block, click Run (no executor needed for client-side blocks) +``` + No changes needed to `index.html`, `executor_node.py`, `handlers/__init__.py`, or `BlocklyAction.action`. diff --git a/src/blockly_app/README.md b/src/blockly_app/README.md index 17847cd..c93ecd2 100644 --- a/src/blockly_app/README.md +++ b/src/blockly_app/README.md @@ -73,17 +73,34 @@ Both functions use `async/await` — they return after the file dialog closes an **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. +**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: wraps generated code in async function and eval()s it | -| `runDebug()` | Debug execution: wraps executeAction to check breakpoints and add delays | -| `stepOver()` | Resumes from pause, executes current block, pauses at next block | +| `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 | -| `stopExecution()` | Sets stopRequested flag, resolves any pending pause Promise | +| `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 @@ -94,8 +111,63 @@ Both functions use `async/await` — they return after the file dialog closes an const BLOCK_FILES = ['digitalOut.js', 'digitalIn.js', 'delay.js']; ``` -#### [`blockly_app/ui/blockly/blocks/digitalOut.js`](blockly_app/ui/blockly/blocks/digitalOut.js) — Example Block +#### [`blockly_app/ui/blockly/core/hmi-manager.js`](blockly_app/ui/blockly/core/hmi-manager.js) — HMI State Manager -**Purpose:** Self-contained block definition. Contains both the visual appearance AND the code generator. +**Purpose:** Global `HMI` object providing LabVIEW-style Front Panel with gridstack.js grid layout. Manages widget lifecycle, mode switching (design/runtime), and layout serialization. -Each block file calls `BlockRegistry.register()` with all metadata, so the toolbox is automatically generated. +| 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.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()`) | + +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. diff --git a/workspace.json b/workspace.json index d70a96c..124bf63 100644 --- a/workspace.json +++ b/workspace.json @@ -48,156 +48,172 @@ }, "next": { "block": { - "type": "digitalOut", - "id": "Cz1MB5`}~cPRiZhh$/P9", + "type": "variables_set", + "id": "qzDOA-}@MU,]iqj`er}D", "fields": { - "GPIO": 17 + "VAR": { + "id": "[g,f6Mp!O$eZPCFs0U[H" + } }, "inputs": { - "digitalOut": { + "VALUE": { "block": { - "type": "procedures_callreturn", - "id": "U|7!ynVp5Z.nwD_`4=Z,", - "extraState": { - "name": "foo", - "params": [ - "logic" - ] - }, - "inputs": { - "ARG0": { - "block": { - "type": "logic_boolean", - "id": "HXRaHRaPE)[G3WGmOi-T", - "fields": { - "BOOL": "TRUE" - } - } - } + "type": "math_number", + "id": "39KE0jn61fTR1Qp4)d57", + "fields": { + "NUM": 500 } } } }, "next": { "block": { - "type": "print", - "id": "#iq/$E*oB0V`%5O}O+2!", + "type": "digitalOut", + "id": "Cz1MB5`}~cPRiZhh$/P9", + "fields": { + "GPIO": 17 + }, "inputs": { - "TEXT": { + "digitalOut": { "block": { - "type": "text", - "id": "^ZnZat6;hW,I`H7H15^^", - "fields": { - "TEXT": "cek" + "type": "procedures_callreturn", + "id": "U|7!ynVp5Z.nwD_`4=Z,", + "extraState": { + "name": "foo", + "params": [ + "logic" + ] + }, + "inputs": { + "ARG0": { + "block": { + "type": "logic_boolean", + "id": "HXRaHRaPE)[G3WGmOi-T", + "fields": { + "BOOL": "TRUE" + } + } + } } } } }, "next": { "block": { - "type": "delay", - "id": "IXp?_lac7+V*GG!lW{]0", + "type": "variables_set", + "id": "5WG|J40M0!L^mM5x_2@N", "fields": { - "DURATION_MS": 1000 + "VAR": { + "id": "xjE5n-FetBK*,)qj?pyn" + } + }, + "inputs": { + "VALUE": { + "block": { + "type": "text", + "id": "^ZnZat6;hW,I`H7H15^^", + "fields": { + "TEXT": "cek" + } + } + } }, "next": { "block": { - "type": "variables_set", - "id": "1@OTYSIBwU0xvg(2?-y;", - "fields": { - "VAR": { - "id": "C_:{ED@bJimgLzEmC6(`" - } - }, + "type": "print", + "id": "#iq/$E*oB0V`%5O}O+2!", "inputs": { - "VALUE": { + "TEXT": { "block": { - "type": "digitalIn", - "id": "DJpFh.6H~L9fX2V4SDJd", + "type": "variables_get", + "id": "4[hYi)09%}I%Pj4}fhdb", "fields": { - "GPIO": 16 + "VAR": { + "id": "xjE5n-FetBK*,)qj?pyn" + } } } } }, "next": { "block": { - "type": "variables_set", - "id": "7v:u)fVh$GAwIhdwtH-i", + "type": "delay", + "id": "IXp?_lac7+V*GG!lW{]0", "fields": { - "VAR": { - "id": "C_:{ED@bJimgLzEmC6(`" - } - }, - "inputs": { - "VALUE": { - "block": { - "type": "math_number", - "id": "x.-GBy*1d`n}m-;O;Nre", - "fields": { - "NUM": 1 - } - } - } + "DURATION_MS": 1000 }, "next": { "block": { - "type": "print", - "id": "iB`R-:UTF#c3EXAU.Qh-", + "type": "variables_set", + "id": "1@OTYSIBwU0xvg(2?-y;", + "fields": { + "VAR": { + "id": "C_:{ED@bJimgLzEmC6(`" + } + }, "inputs": { - "TEXT": { + "VALUE": { "block": { - "type": "text", - "id": "oA52{ydq^ZsJF:O@NU,d", + "type": "digitalIn", + "id": "DJpFh.6H~L9fX2V4SDJd", "fields": { - "TEXT": "cok" + "GPIO": 16 } } } }, "next": { "block": { - "type": "delay", - "id": "VQy`Sl3]ey49sP%+N6$R", + "type": "variables_set", + "id": "7v:u)fVh$GAwIhdwtH-i", "fields": { - "DURATION_MS": 1000 + "VAR": { + "id": "C_:{ED@bJimgLzEmC6(`" + } + }, + "inputs": { + "VALUE": { + "block": { + "type": "math_number", + "id": "x.-GBy*1d`n}m-;O;Nre", + "fields": { + "NUM": 1 + } + } + } }, "next": { "block": { "type": "variables_set", - "id": "i|LkDgVjImZd2}owndlz", + "id": "toVbALp.{C5C+{9GMK%e", "fields": { "VAR": { - "id": "[g,f6Mp!O$eZPCFs0U[H" + "id": "xjE5n-FetBK*,)qj?pyn" } }, "inputs": { "VALUE": { "block": { - "type": "math_number", - "id": "[C@fwlekugl(`pi1b;1(", + "type": "text", + "id": "oA52{ydq^ZsJF:O@NU,d", "fields": { - "NUM": 100 + "TEXT": "cok" } } } }, "next": { "block": { - "type": "pwmWrite", - "id": "Ezn#r.|lvDj5{Q1-C:E$", - "fields": { - "ADDRESS": "64", - "CHANNEL": 0 - }, + "type": "print", + "id": "iB`R-:UTF#c3EXAU.Qh-", "inputs": { - "PWM_VALUE": { + "TEXT": { "block": { "type": "variables_get", - "id": "OkA-}PRPzgi;[I)@vcG$", + "id": "+wF~Y65u+aNA.ZX3!^Ra", "fields": { "VAR": { - "id": "[g,f6Mp!O$eZPCFs0U[H" + "id": "xjE5n-FetBK*,)qj?pyn" } } } @@ -206,49 +222,125 @@ "next": { "block": { "type": "variables_set", - "id": "m{+MhlXz-1tpl`mPPBh5", + "id": "E8`?q[Z3*IpN3,52Y=jA", "fields": { "VAR": { - "id": "ju{xs[rjZumqS87$0nhu" + "id": "[g,f6Mp!O$eZPCFs0U[H" } }, "inputs": { "VALUE": { "block": { - "type": "odometryRead", - "id": "v=Js89HC8D0UUA.-pN[q", + "type": "math_number", + "id": "iOJP=T-{O5h_^;{Yz?%(", "fields": { - "SOURCE": "encoder" + "NUM": 3000 } } } }, "next": { "block": { - "type": "variables_set", - "id": "]LcUOwlc-y=`.e?EVgTa", + "type": "delay", + "id": "VQy`Sl3]ey49sP%+N6$R", "fields": { - "VAR": { - "id": "Ug!mIa*[PnsL?H#9Ar*G" - } + "DURATION_MS": 1000 }, - "inputs": { - "VALUE": { - "block": { - "type": "odometryGet", - "id": "VG2Q/8?zcyU}s4!W;V/M", - "fields": { - "FIELD": "x" - }, - "inputs": { - "VAR": { + "next": { + "block": { + "type": "variables_set", + "id": "i|LkDgVjImZd2}owndlz", + "fields": { + "VAR": { + "id": "[g,f6Mp!O$eZPCFs0U[H" + } + }, + "inputs": { + "VALUE": { + "block": { + "type": "math_number", + "id": "[C@fwlekugl(`pi1b;1(", + "fields": { + "NUM": 100 + } + } + } + }, + "next": { + "block": { + "type": "pwmWrite", + "id": "Ezn#r.|lvDj5{Q1-C:E$", + "fields": { + "ADDRESS": "64", + "CHANNEL": 0 + }, + "inputs": { + "PWM_VALUE": { + "block": { + "type": "variables_get", + "id": "OkA-}PRPzgi;[I)@vcG$", + "fields": { + "VAR": { + "id": "[g,f6Mp!O$eZPCFs0U[H" + } + } + } + } + }, + "next": { "block": { - "type": "variables_get", - "id": "^AW6|z21?ycRyzJ2y5u9", + "type": "variables_set", + "id": "m{+MhlXz-1tpl`mPPBh5", "fields": { "VAR": { "id": "ju{xs[rjZumqS87$0nhu" } + }, + "inputs": { + "VALUE": { + "block": { + "type": "odometryRead", + "id": "v=Js89HC8D0UUA.-pN[q", + "fields": { + "SOURCE": "encoder" + } + } + } + }, + "next": { + "block": { + "type": "variables_set", + "id": "]LcUOwlc-y=`.e?EVgTa", + "fields": { + "VAR": { + "id": "Ug!mIa*[PnsL?H#9Ar*G" + } + }, + "inputs": { + "VALUE": { + "block": { + "type": "odometryGet", + "id": "VG2Q/8?zcyU}s4!W;V/M", + "fields": { + "FIELD": "x" + }, + "inputs": { + "VAR": { + "block": { + "type": "variables_get", + "id": "^AW6|z21?ycRyzJ2y5u9", + "fields": { + "VAR": { + "id": "ju{xs[rjZumqS87$0nhu" + } + } + } + } + } + } + } + } + } } } } @@ -355,8 +447,8 @@ { "type": "main_hmi_program", "id": "l!A!vr#-Z*nShL9rf[fa", - "x": 690, - "y": 30, + "x": 370, + "y": 130, "inputs": { "BODY": { "block": { @@ -398,6 +490,75 @@ } } } + }, + "next": { + "block": { + "type": "hmiSetGauge", + "id": "FeWSZv_C@ci}5(#:EV.A", + "fields": { + "NAME": "Gauge1", + "MIN": 0, + "MAX": 4069 + }, + "inputs": { + "VALUE": { + "block": { + "type": "variables_get", + "id": "LIIgG+UMkthq!S.K}dX8", + "fields": { + "VAR": { + "id": "[g,f6Mp!O$eZPCFs0U[H" + } + } + } + } + }, + "next": { + "block": { + "type": "hmiSetText", + "id": "1d7kB$!lQpY@szAnVaLx", + "fields": { + "NAME": "Status" + }, + "inputs": { + "TEXT": { + "block": { + "type": "variables_get", + "id": "`usj,%r!VHQt}Ucr]Bxv", + "fields": { + "VAR": { + "id": "xjE5n-FetBK*,)qj?pyn" + } + } + } + } + }, + "next": { + "block": { + "type": "hmiSetNumber", + "id": "-^@C9g@boq7MU?u2E}f9", + "fields": { + "NAME": "Value1", + "UNIT": "pin" + }, + "inputs": { + "VALUE": { + "block": { + "type": "variables_get", + "id": "Cpu4;(,q`6WS@(f@L,|6", + "fields": { + "VAR": { + "id": "[g,f6Mp!O$eZPCFs0U[H" + } + } + } + } + } + } + } + } + } + } } } } @@ -425,6 +586,10 @@ { "name": "valX", "id": "Ug!mIa*[PnsL?H#9Ar*G" + }, + { + "name": "text", + "id": "xjE5n-FetBK*,)qj?pyn" } ] }, @@ -433,12 +598,44 @@ "name": "LED1", "type": "led", "x": 1, - "y": 0, + "y": 1, "w": 2, "h": 1, "config": { "color": "#4caf50" } + }, + { + "name": "Gauge1", + "type": "gauge", + "x": 1, + "y": 2, + "w": 2, + "h": 2, + "config": { + "min": 0, + "max": 4069 + } + }, + { + "name": "Status", + "type": "text", + "x": 0, + "y": 0, + "w": 6, + "h": 1, + "config": {} + }, + { + "name": "Value1", + "type": "number", + "x": 3, + "y": 1, + "w": 2, + "h": 1, + "config": { + "unit": "pin" + } } ] } \ No newline at end of file