From 6f454c97074aee4575c4706929138631179ac1e4 Mon Sep 17 00:00:00 2001 From: a2nr Date: Wed, 18 Mar 2026 22:27:56 +0700 Subject: [PATCH] feat: add HMI control widgets (Button, Slider, Switch) with user interaction tracking; update documentation and block definitions --- docs/architecture.md | 16 +++- docs/troubleshooting.md | 32 ++++++++ readme.md | 167 +++----------------------------------- src/blockly_app/BLOCKS.md | 42 +++++++++- src/blockly_app/README.md | 12 +++ 5 files changed, 109 insertions(+), 160 deletions(-) diff --git a/docs/architecture.md b/docs/architecture.md index f781b4e..015afc3 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -120,7 +120,7 @@ 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. +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 **indicators** (satu arah: code → display) dan **controls** (dua arah: user input ↔ code) yang dikontrol programmatically dari generated code. ``` ┌────────────────────────────────────────────────────────────────────────┐ @@ -154,7 +154,7 @@ Terinspirasi oleh LabVIEW yang memiliki **Front Panel** (controls & indicators) | 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 Manager | `core/hmi-manager.js` | Global `HMI` object — indicators: `setLED()`, `setNumber()`, `setText()`, `setGauge()`; controls: `setButton()`/`getButton()`, `setSlider()`/`getSlider()`, `setSwitch()`/`getSwitch()`; lifecycle: `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()`. | @@ -162,7 +162,7 @@ Terinspirasi oleh LabVIEW yang memiliki **Front Panel** (controls & indicators) - **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 types — Indicators** (satu arah: code → display): | Widget | JS API | Fungsi | |--------|--------|--------| @@ -171,6 +171,16 @@ Terinspirasi oleh LabVIEW yang memiliki **Front Panel** (controls & indicators) | Text | `HMI.setText(name, text)` | Text string display | | Gauge | `HMI.setGauge(name, value, min, max)` | Horizontal bar gauge with range | +**Widget types — Controls** (dua arah: user input ↔ code): + +| Widget | SET API | GET API | Fungsi | +|--------|---------|---------|--------| +| Button | `HMI.setButton(name, label, color)` | `HMI.getButton(name)` → Boolean | Latch-until-read: return `true` sekali per klik, auto-reset ke `false` | +| Slider | `HMI.setSlider(name, value, min, max)` | `HMI.getSlider(name)` → Number | Drag range input. `_userValue` tracking mencegah `setSlider()` menimpa posisi user | +| Switch | `HMI.setSwitch(name, state)` | `HMI.getSwitch(name)` → Boolean | Toggle ON/OFF. `_userState` tracking mencegah `setSwitch()` menimpa toggle user | + +Control widgets menggunakan **user interaction tracking** — state dari user (klik/drag/toggle) disimpan terpisah dari programmatic `set*()` call, sehingga HMI loop yang memanggil `set*()` setiap ~50ms tidak menimpa input user. Design-time preview auto-increment widget names saat block duplikat di-place. + ### 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()`. diff --git a/docs/troubleshooting.md b/docs/troubleshooting.md index d8fd761..ee85d78 100644 --- a/docs/troubleshooting.md +++ b/docs/troubleshooting.md @@ -100,4 +100,36 @@ See [`_native_save_dialog()`](src/blockly_app/blockly_app/app.py:29) in `app.py` **Solution:** Tambah `_reconcile()` dengan 100ms debounce setelah setiap workspace event. Fungsi ini compare HMI blocks di workspace vs `_blockToWidget` map, hapus widget orphan, tambah widget yang belum ter-track. File: `core/hmi-preview.js`. +### UI freeze pada `while(true)` loop di Main Program (polling HMI controls) + +**Symptom:** Main Program dengan `while(true)` untuk polling `HMI.getButton()` → seluruh UI freeze, HMI panel tidak update, tombol Stop tidak responsif. + +**Cause:** `highlightBlock` override hanya yield ke microtask queue. Microtask tidak memberi giliran ke macrotask queue (click events, setTimeout, requestAnimationFrame). + +**Solution:** Tambahkan periodic yield ke macrotask queue (~60Hz) di `highlightBlock` override. Setiap 16ms, `setTimeout(r, 0)` memaksa browser memproses macrotask. Diterapkan di `_runConcurrent()` dan `_runSingle()`. File: `core/debug-engine.js`. + +### HMI Button `getButton()` selalu return `false` meskipun sudah diklik + +**Symptom:** `while(getButton()!= true) {delay(500);}` tidak pernah keluar dari loop. + +**Cause:** HMI loop (~20Hz) memanggil `setButton()` → `_scheduleRender()` → `_render()` menghancurkan DOM button lama dan membuat baru setiap ~50ms. Event `click` butuh mousedown+mouseup pada elemen DOM yang sama — karena DOM diganti mid-click, `click` event tidak pernah fire. + +**Solution:** Ganti `click` → `pointerdown` di `_renderButton()`. `pointerdown` fire langsung saat ditekan tanpa menunggu mouseup. File: `core/hmi-manager.js`. + +### HMI Switch/Slider tidak bisa di-toggle/drag — selalu reset ke initial value + +**Symptom:** `setSwitch('Switch1', Boolean(led))` dalam HMI loop menimpa user toggle setiap ~50ms. Slider juga reset ke 0 setelah user release. + +**Cause:** HMI loop berjalan 10x lebih cepat dari Main Program. `setSwitch()` langsung overwrite `widget.state`, dan `setSlider()` overwrite `_userValue` segera setelah `_userInteracting = false` (mouseup). + +**Solution:** User interaction tracking — state user disimpan terpisah dari programmatic state. Switch: `_userState` field, tidak ditimpa oleh `setSwitch()`. Slider: `_userHasInteracted` flag persist setelah release, mencegah `setSlider()` overwrite `_userValue`. File: `core/hmi-manager.js`. + +### Dua HMI block dengan nama sama hanya membuat satu widget + +**Symptom:** Drag dua block `HMI LED` dari toolbox → hanya satu widget muncul di panel (keduanya default ke `LED1`). + +**Cause:** `_widgets` Map menggunakan nama sebagai key. Block kedua dengan nama sama hanya update widget yang ada, bukan membuat baru. + +**Solution:** Auto-increment nama di `_handleCreate()` — jika `LED1` sudah dipakai, block baru otomatis menjadi `LED2`. Block field di-update via `setFieldValue()`. Juga fix `_handleDelete()` untuk tidak menghapus widget jika block lain masih menggunakan nama yang sama. File: `core/hmi-preview.js`. + --- diff --git a/readme.md b/readme.md index 8c6f2e2..81ae86a 100644 --- a/readme.md +++ b/readme.md @@ -43,7 +43,7 @@ jelaskan permasalah di bab ini # Potential Enhancements this list is short by priority - **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 +- **Launch files**: ROS2 launch files to start all nodes with one command includ node in raspberry pi. composite blockly dan executor yang memiliki composit 2 jenis yaitu menggunakan executor dummy dan executor-hw. - **Simulation**: Integrate with Gazebo/Isaac Sim for testing Kiwi Wheel kinematics before deploying to hardware - **Block categories**: Future blocks grouped into Robot, Sensors, Navigation categories @@ -315,163 +315,18 @@ Fields: `X (cm)`, `Y (cm)`, `Heading (rad)`, `Vel X (cm/s)`, `Vel Y (cm/s)`, `An - [x] Integration test `test_block_odometry.py` passes di dummy mode ## 6 Enhancement: HMI Interactive Widgets — Button, Slider, Switch : [x] -HMI panel sebelumnya hanya memiliki widget **indicator** (satu arah: code → display): LED, Number, Text, Gauge. Enhancement ini menambahkan widget **control** (dua arah: user input ↔ code): Button, Slider, dan Switch — mengikuti konsep LabVIEW "Controls vs Indicators". +Menambahkan widget **control** (dua arah: user input ↔ code): Button, Slider, dan Switch — mengikuti konsep LabVIEW "Controls vs Indicators". Setiap control memiliki SET block (statement) dan GET block (value). Semua client-side JS, tidak ada perubahan Python/ROS2. -Setiap widget control memiliki **SET block** (statement, buat/konfigurasi widget) dan **GET block** (value, baca state interaksi user). Semua client-side JS, tidak ada perubahan Python handler atau ROS2. - -### Implementasi - -#### A. Block Definitions (6 file baru) -Semua di `src/blockly_app/blockly_app/ui/blockly/blocks/`: - -| File | Tipe Block | Deskripsi | Output | -|------|-----------|-----------|--------| -| `hmiSetButton.js` | Statement | `HMI Button [name] label [text] color [dropdown]` | — | -| `hmiSetSlider.js` | Statement | `HMI Slider [name] = [value] min [n] max [n]` | — | -| `hmiSetSwitch.js` | Statement | `HMI Switch [name] state: [boolean]` | — | -| `hmiGetButton.js` | Value | `HMI Button pressed? [name]` | Boolean | -| `hmiGetSlider.js` | Value | `HMI Slider value [name]` | Number | -| `hmiGetSwitch.js` | Value | `HMI Switch state [name]` | Boolean | - -#### B. HMI Manager (`hmi-manager.js`) -- 3 render function: `_renderButton`, `_renderSlider`, `_renderSwitch` -- 3 setter: `setButton(name, label, color)`, `setSlider(name, value, min, max)`, `setSwitch(name, state)` -- 3 getter: `getButton(name)`, `getSlider(name)`, `getSwitch(name)` -- Interaktivitas hanya aktif di runtime mode — design mode non-interactive (preview) - -#### C. Getter Behavior -- **Button — latch-until-read**: `getButton()` return `true` sekali per klik, lalu auto-reset ke `false`. Mencegah satu klik terbaca berkali-kali di HMI loop 20Hz -- **Slider — `_userValue` tracking**: Memisahkan nilai user-drag dari programmatic `setSlider()`. Mencegah `setSlider()` di loop menimpa posisi drag user -- **Switch — toggle**: `getSwitch()` return boolean state saat ini. User klik untuk toggle ON/OFF - -#### D. File yang Dimodifikasi -- `manifest.js` — 6 entry baru -- `hmi-manager.js` — render, setter, getter, serialization, default sizes -- `hmi-preview.js` — design-time preview untuk 3 SET block -- `index.html` — CSS untuk button, slider (range input), switch (toggle track + thumb) - -### Bug 1 [x] : UI Freeze pada `while(true)` Loop di Main Program - -**Symptom**: Ketika Main Program menggunakan `while(true)` untuk polling `HMI.getButton()`, seluruh UI freeze — HMI panel tidak update, tombol Stop tidak responsif. - -**Root Cause**: JavaScript single-threaded. `highlightBlock` override di `_runConcurrent` (debug-engine.js) hanya yield ke **microtask queue** (`await` pada fungsi sinkron → resolved promise). Microtask tidak pernah memberi giliran ke **macrotask queue** dimana click events, `requestAnimationFrame`, dan HMI loop `setTimeout(r, 50)` berada. - -``` -while(true) → await highlightBlock() → microtask yield → while(true) → ... - ↑ macrotask queue STARVED - (click events, setTimeout, paint — tidak pernah jalan) -``` - -**Fix**: Tambahkan periodic yield ke macrotask queue (~60Hz) di `highlightBlock` override. Setiap 16ms, `setTimeout(r, 0)` memaksa browser memproses macrotask sebelum resume. - -```javascript -var _lastYield = Date.now(); -window.highlightBlock = async function (blockId) { - if (debugState.stopRequested) throw new Error('STOP_EXECUTION'); - originalHighlight(blockId); - var now = Date.now(); - if (now - _lastYield >= 16) { // 16ms = ~60fps - _lastYield = now; - await new Promise(function (r) { setTimeout(r, 0); }); - } -}; -``` - -Fix diterapkan di: -- `_runConcurrent()` — Run mode concurrent (Main + HMI) -- `_runSingle()` — Run mode single (Main saja, sebelumnya tidak punya override sama sekali) - -### Bug 2 [x]: Button tidak cukup cepat untuk menangkap logika dari UI. - -**Symptom**: Ketika membuat program `while(getButton()!= true) {delay(500);}` logika button dari HMI tidak tercatat — `getButton()` selalu return `false` meskipun button sudah diklik. - -**Root Cause**: HMI loop (~20Hz) memanggil `setButton('Btn1', 'Press', '#2196f3')` setiap ~50ms. Setiap panggilan memicu `_scheduleRender` → `requestAnimationFrame` → `_render()` yang **menghancurkan DOM button lama** (`el.textContent = ''`) dan membuat elemen `