Refactor UI components and enhance functionality

- Updated the code panel to use a textarea for code output instead of a preformatted text element, allowing for better user interaction.
- Modified CSS styles for the code output area to improve layout and usability.
- Introduced new HMI widgets including buttons, sliders, and switches with appropriate styles and configurations.
- Updated workspace JSON to reflect changes in block types and IDs, ensuring compatibility with new UI elements.
- Created a new workspace for button interactions, integrating button presses with slider and switch states.
master
a2nr 2026-03-18 22:20:44 +07:00
parent 086e5dce0c
commit 3039b1d109
16 changed files with 1527 additions and 367 deletions

View File

@ -68,4 +68,36 @@ See [`_native_save_dialog()`](src/blockly_app/blockly_app/app.py:29) in `app.py`
**Impact:** This is **informational only**. pywebview tries GTK first, falls back to Qt (which is installed via `pyqtwebengine`). The application works correctly with the Qt backend.
### Blockly workspace tidak ikut resize saat panel di-drag
**Symptom:** Drag vertical/horizontal divider, Blockly canvas tidak resize — tetap ukuran lama.
**Cause:** `resizable-panels.js` hanya panggil `Blockly.svgResize()` tanpa update dimensi `#blockly-div` yang `position: absolute`.
**Solution:** Update `#blockly-div` width/height dari `#blockly-area` offsetWidth/Height sebelum `svgResize`. File: `core/resizable-panels.js` (mousemove handler).
### App freeze saat kedua program pakai `while(true)`
**Symptom:** App tidak responsif (freeze) saat `main_program` dan `main_hmi_program` keduanya punya `while(true)` loop.
**Cause:** User menulis `while(true)` di `main_hmi_program`. Auto-wrapper menambah outer while-loop, tapi inner `while(true)` dengan HMI calls (synchronous) tidak pernah yield ke event loop.
**Solution:** HMI shadowed `highlightBlock` diubah dari sync no-op ke `async function` dengan `stopRequested` check. Setiap `await highlightBlock()` di generated code yield ke event loop + bisa di-stop. File: `core/debug-engine.js``_runConcurrent()` dan `_runDebugConcurrent()`.
### Variabel tidak ter-share antara main dan HMI program
**Symptom:** Variable yang di-set di `main_program` tidak terlihat di `main_hmi_program` (tetap default value).
**Cause:** `definitions` (berisi `var led;`) di-eval di dua IIFE terpisah → dua scope terpisah.
**Solution:** Gabung kedua program dalam SATU eval — outer IIFE berisi `definitions`, dua inner IIFE (main + HMI) close over shared scope. File: `core/debug-engine.js``_runConcurrent()` dan `_runDebugConcurrent()`.
### Delete HMI block tidak menghapus preview widget; undo muncul blank widget
**Symptom:** Hapus HMI block → widget masih ada di HMI panel. Undo → widget muncul tapi kosong.
**Cause:** `_blockToWidget` Map di `hmi-preview.js` kehilangan sinkronisasi saat undo/redo. Blockly events pada undo tidak selalu re-add mapping.
**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`.
---

275
readme.md
View File

@ -2,8 +2,8 @@
> **Project**: Blockly ROS2 Robot Controller (Kiwi Wheel AMR)
> **ROS2 Distro**: Jazzy
> **Last Updated**: 2026-03-16
> **Current Focus**: Task #5 — Unified Odometry Interface
> **Last Updated**: 2026-03-18
> **Current Focus**: Task #6 — HMI Interactive Widgets
Dokumentasi lengkap dapat dilihat di [DOCUMENTATION.md](DOCUMENTATION.md).
@ -27,8 +27,14 @@ Berikut ini adalah template untuk pembuatan task :
## <nomor task> <judul task> : <state: [ ] >
jelaskan permasalah di bab ini
### Bug 1 [ ] : Keterangan bug untuk rekap permasalahan
**Symtomp** : jelaskan masalahnya!
**Root Couse** : masalah ini dikarenakan apa?
**Fix** : bagaimana cara fix nya?
### Definition Of Done
jelaskan apa yang dimaksut untuk menyelesaikan task
[ ] DoD 1
[ ] DoD 2
[ ] Bug 1 ...
```
@ -36,7 +42,6 @@ jelaskan apa yang dimaksut untuk menyelesaikan task
# Potential Enhancements
this list is short by priority
- **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
@ -309,146 +314,164 @@ Fields: `X (cm)`, `Y (cm)`, `Heading (rad)`, `Vel X (cm/s)`, `Vel Y (cm/s)`, `An
- [ ] End-to-end: Blockly → executor (real) → cache `odometry_encoder/odom` → return JSON
- [x] Integration test `test_block_odometry.py` passes di dummy mode
## 6 Enhancement: Blockly UI — HMI Panel, Print Console & Concurrent Execution : [x]
## 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".
LabVIEW punya 2 view: **Front Panel** (HMI — controls & indicators) dan **Block Diagram** (visual programming). Di project ini, Blockly sudah jadi "Block Diagram". Enhancement ini menambahkan **"Front Panel"** — HMI panel dengan grid layout, resizable panels, design-time widget preview, concurrent HMI loop, plus **print console** dan **string blocks** untuk debugging.
**Key Architecture Decisions**:
- Print block dan HMI blocks **tidak membutuhkan ROS2 action** — murni client-side JavaScript. Zero latency, no network round-trip.
- **Concurrent execution**: `main_program` + `main_hmi_program` berjalan bersamaan via `Promise.all()`. HMI loop auto-wrapped dalam `while` loop ~20Hz.
- **Design-time preview**: Widget muncul di HMI panel saat block diletakkan (workspace change listener), bukan hanya saat runtime.
- **Grid layout**: gridstack.js untuk drag-reposition dan drag-resize widget. Layout disimpan bersama workspace.
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
#### Phase 1: Print Block & Text Category
#### A. Block Definitions (6 file baru)
Semua di `src/blockly_app/blockly_app/ui/blockly/blocks/`:
**A. Print Statement Block** — `print.js`
- Category: `Program`, statement block: `Print [value input]`
- Generator: `consoleLog(String(VALUE), 'print');` — no `executeAction`, purely client-side
- Works with any expression via `String(expr)` pattern
| 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. Text Category** — built-in Blockly blocks: `text`, `text_join`, `text_length`
#### 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)
#### Phase 2: HMI Panel Infrastructure
#### 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
**A. Resizable Split View** — Blockly on left, HMI panel on right
- Drag dividers (`resizable-panels.js`): vertical (Blockly↔HMI) dan horizontal (workspace↔console)
- Clamps: HMI 200px50% viewport; console 80px40% viewport
- `Blockly.svgResize(workspace)` dipanggil setiap move
#### 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)
**B. HMI Manager** — `hmi-manager.js`
- Global `HMI` object: `setLED()`, `setNumber()`, `setText()`, `setGauge()`, `clearAll()`
- Two modes: `design` (grid unlocked, preview values) / `runtime` (grid locked, live values)
- gridstack.js integration: `init()`, `addWidget()`, `removeWidget()`, `getLayout()`, `loadLayout()`
- Pure DOM API rendering (no innerHTML) — XSS safe
### Bug 1 [x] : UI Freeze pada `while(true)` Loop di Main Program
**C. Design-time Preview** — `hmi-preview.js`
- Workspace change listener (`BLOCK_CREATE`, `BLOCK_DELETE`, `BLOCK_CHANGE`)
- Widgets appear/disappear as blocks are placed/deleted
- `_hmiPreviewScan` for workspace-io.js to call after import
**Symptom**: Ketika Main Program menggunakan `while(true)` untuk polling `HMI.getButton()`, seluruh UI freeze — HMI panel tidak update, tombol Stop tidak responsif.
#### Phase 3: HMI Blocks
**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.
| Block | Type | Category | Fungsi |
|-------|------|----------|--------|
| `hmiSetLed` | Statement | HMI | Set LED indicator on/off with color |
| `hmiSetNumber` | Statement | HMI | Display numeric value with unit label |
| `hmiSetText` | Statement | HMI | Display text string |
| `hmiSetGauge` | Statement | HMI | Display gauge bar with min/max range |
Semua HMI blocks: purely client-side (call `HMI.*()` functions), Field `NAME` untuk widget identifier, Value input untuk dynamic value.
#### Phase 4: Concurrent Execution — `main_hmi_program`
**A. `mainHmiProgram.js`** — Hat block, category "Program", color `#00BCD4`
- Generator does NOT emit `highlightBlock()` — HMI runs full speed
- Enforced: max 1 `main_hmi_program` per workspace (same as `main_program`)
**B. `generateCode(ws)`** returns `{ definitions, mainCode, hmiCode }` when HMI block present
**C. `debug-engine.js`** — `runProgram()` / `runDebug()` dispatch to single or concurrent mode:
- `_runSingle()` / `_runDebugSingle()` — original behavior (no HMI block)
- `_runConcurrent()` / `_runDebugConcurrent()``Promise.all()` with HMI while-loop
- HMI eval scope shadows `highlightBlock` to no-op (full speed)
- Main program drives completion → `stopRequested` signals HMI loop exit
- Debug mode: only main program has stepping/breakpoints; HMI uninterrupted
**D. Save/Load** — `workspace-io.js` saves `{ workspace, hmiLayout }`, backward-compatible
**Use Case Example** — Concurrent odometry monitoring:
```
main_program: main_hmi_program:
forever: HMI Set Number "X" = getVal [X] from [odom]
set [odom] to getOdometry [Encoder] HMI Set Number "Y" = getVal [Y] from [odom]
digital_out(17, true) HMI Set Gauge "Heading" = getVal [θ] from [odom]
delay(1000) HMI Set LED "Running" = true, color: green
digital_out(17, false)
while(true) → await highlightBlock() → microtask yield → while(true) → ...
↑ macrotask queue STARVED
(click events, setTimeout, paint — tidak pernah jalan)
```
### Files Changed
**Fix**: Tambahkan periodic yield ke macrotask queue (~60Hz) di `highlightBlock` override. Setiap 16ms, `setTimeout(r, 0)` memaksa browser memproses macrotask sebelum resume.
**New files (11)**:
1. `blocks/print.js` — Print block
2. `blocks/mainHmiProgram.js` — HMI program hat block
3. `blocks/hmiSetLed.js` — LED indicator block
4. `blocks/hmiSetNumber.js` — Numeric display block
5. `blocks/hmiSetText.js` — Text display block
6. `blocks/hmiSetGauge.js` — Gauge bar block
7. `core/hmi-manager.js` — HMI state manager + gridstack integration
8. `core/hmi-preview.js` — Design-time widget preview
9. `core/resizable-panels.js` — Drag-to-resize panels
10. `vendor/gridstack-all.js` — gridstack.js vendor file
11. `vendor/gridstack.min.css` — gridstack CSS
```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); });
}
};
```
**Modified files (7)**:
1. `index.html` — Layout restructure, drag dividers, gridstack CSS/JS, dark theme overrides
2. `blocks/manifest.js` — Add all new block files
3. `core/registry.js` — Add Text built-in category
4. `core/async-procedures.js` — Return structured `{ definitions, mainCode, hmiCode }`
5. `core/debug-engine.js` — Concurrent run/debug, HMI.setMode() lifecycle
6. `core/ui-tabs.js` — Display structured code in Code tab
7. `workspace-init.js` — HMI.init(), initHMIPreview(), initResizablePanels(), enforce single main_hmi_program
Fix diterapkan di:
- `_runConcurrent()` — Run mode concurrent (Main + HMI)
- `_runSingle()` — Run mode single (Main saja, sebelumnya tidak punya override sama sekali)
**No Python changes** — semua murni client-side JavaScript. No build step needed.
### 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 `<button>` baru.
Event `click` membutuhkan `mousedown` + `mouseup` pada **elemen DOM yang sama**. Karena DOM button diganti setiap ~50ms, jika user menekan button (mousedown) lalu re-render terjadi sebelum mouseup, elemen yang menerima mousedown sudah tidak ada — `click` event tidak pernah fire, `widget._pressed` tetap `false`.
```
t=0ms : User mousedown pada Button-A (DOM element)
t=16ms : requestAnimationFrame → _render() → Button-A DIHANCURKAN → Button-B DIBUAT
t=100ms : User mouseup → tapi Button-A sudah tidak ada!
→ click event TIDAK FIRE
→ widget._pressed tetap false
t=500ms : getButton('Btn1') → return false ← bug
```
**Fix**: Ganti event `click``pointerdown` di `_renderButton()` (`hmi-manager.js`). `pointerdown` fire **langsung saat ditekan** tanpa menunggu mouseup, sehingga state tersimpan sebelum re-render menghancurkan DOM.
```javascript
// SEBELUM (click — butuh mousedown+mouseup pada elemen yang sama):
btn.addEventListener('click', function () {
widget._pressed = true;
});
// SESUDAH (pointerdown — fire langsung saat ditekan):
btn.addEventListener('pointerdown', function () {
widget._pressed = true;
});
```
### Bug 3 [x] : `setSwitch()` menimpa state user toggle setiap 50ms
**Symptom**: Ketika switch state ditampung ke variabel (`led = getSwitch()`) lalu digunakan di `setSwitch('Switch1', Boolean(led))` dalam HMI loop, switch tidak bisa di-toggle — selalu stuck di `false`.
**Root Cause**: HMI loop berjalan ~20Hz (50ms), Main Program polling setiap 500ms. Ketika user toggle switch → `widget.state = true`, dalam 50ms HMI loop memanggil `setSwitch('Switch1', Boolean(led))` dimana `led` masih bernilai lama (`false`) — **menimpa toggle user kembali ke `false`** sebelum Main Program sempat membacanya.
```
t=0ms : led = undefined → HMI: setSwitch('Switch1', false) → widget.state = false
t=50ms : User klik switch → widget.state = true ✓
t=55ms : HMI loop: setSwitch('Switch1', Boolean(led)) → led masih false!
→ widget.state = false ← USER TOGGLE DITIMPA!
t=500ms : Main: led = getSwitch('Switch1') → false (sudah ditimpa)
```
Masalah yang sama dengan Slider yang sudah di-solve menggunakan `_userValue` / `_userInteracting` tracking.
**Fix**: Tambahkan `_userState` tracking — memisahkan state dari user interaction vs programmatic `setSwitch()`. Mengikuti pola yang sama dengan Slider (`_userValue`).
Perubahan di `hmi-manager.js`:
1. `_renderSwitch` — render menggunakan `_userState` jika ada; toggle menyimpan ke `_userState` (bukan `state`); ganti `click``pointerdown` (konsisten dengan fix button Bug 2)
2. `setSwitch` — hanya re-render jika user belum interaksi (`_userState === undefined`)
3. `getSwitch` — return `_userState` jika ada, fallback ke `state`
4. `_resetToPreview``delete widget._userState` saat kembali ke design mode
### Bug 4 [x] : `setSlider()` menimpa posisi drag user setelah release
**Symptom**: Slider stuck di angka 0 — tidak bisa di-drag. HMI loop memanggil `HMI.setSlider('Slider1', Number(slide), 0, 100)` dimana `slide` awalnya 0, dan setiap kali user drag lalu release, slider kembali ke 0.
**Root Cause**: `_userInteracting` flag hanya `true` **selama drag aktif** (mousedown→mouseup). Begitu user release slider, `_userInteracting = false`, dan dalam 50ms HMI loop memanggil `setSlider()` yang menimpa `_userValue` kembali ke nilai `slide` lama (0) — sebelum Main Program sempat membaca via `getSlider()`.
```
t=0ms : User drag slider ke 50 → _userInteracting=true, _userValue=50
t=100ms : User release → _userInteracting=false
t=105ms : HMI loop: setSlider('Slider1', Number(slide), 0, 100)
→ _userInteracting is false → _userValue = 0 ← DITIMPA!
t=500ms : Main: slide = getSlider('Slider1') → 0 (sudah ditimpa)
```
Masalah identik dengan Switch (Bug 3) — `_userInteracting` hanya protect selama interaksi aktif, bukan setelahnya.
**Fix**: Tambahkan `_userHasInteracted` flag yang **persist** setelah release. Sekali user pernah drag slider, `setSlider()` tidak akan overwrite `_userValue` lagi.
Perubahan di `hmi-manager.js`:
1. `_renderSlider` — set `_userHasInteracted = true` pada `input` event (saat user drag)
2. `setSlider` — skip `_userValue` overwrite dan `_scheduleRender` jika `_userHasInteracted` true
3. `_resetToPreview` — reset `_userHasInteracted = false` saat kembali ke design mode
### Definition Of Done
- [x] Print block generate `consoleLog(String(...), 'print')`, output tampil di console
- [x] Text category: `text`, `text_join`, `text_length` blocks di toolbox
- [x] Resizable panels: drag dividers between Blockly↔HMI and workspace↔console
- [x] HMI panel dengan gridstack grid layout (drag-reposition, drag-resize widgets)
- [x] Design-time preview: widgets appear when blocks placed, disappear when deleted
- [x] HMI LED/Number/Text/Gauge blocks create/update widgets via `HMI.set*()`
- [x] HMI widgets persist across loop iterations (update in place)
- [x] `main_hmi_program` block runs concurrently with `main_program` via Promise.all()
- [x] HMI loop auto-wrapped at ~20Hz, runs at full speed (no highlightBlock)
- [x] Debug mode: main program has stepping/breakpoints, HMI runs uninterrupted
- [x] Code tab displays both main and HMI code sections
- [x] Save/load preserves workspace + HMI grid layout (backward-compatible)
- [x] All blocks work in debug mode (highlightBlock + step through)
- [x] 6 block file dibuat (SET + GET untuk button, slider, switch)
- [x] `hmi-manager.js` — render, setter, getter, layout serialization
- [x] `hmi-preview.js` — design-time preview untuk 3 SET block
- [x] `manifest.js` — 6 entry baru terdaftar
- [x] CSS untuk button, slider, switch di `index.html`
- [x] Bug 1
- [x] Bug 2
- [x] Bug 3
- [x] Bug 4
- [x] Manual test: SET block → preview widget muncul di design mode
- [x] Manual test: Run program → button clickable, slider draggable, switch toggleable
- [x] Manual test: GET block membaca state interaksi user dengan benar
- [x] Manual test: `while(true)` loop di Main Program tidak freeze UI
- [x] Manual test: Save/load workspace — widget positions preserved
### Known Bugs (ditemukan saat testing manual, fix sudah ditulis tapi belum diverifikasi)
**Bug 1 — Blockly workspace tidak ikut resize saat panel di-drag**
- **Root cause**: `resizable-panels.js` hanya panggil `Blockly.svgResize()` tanpa update dimensi `#blockly-div` yang `position: absolute`
- **Fix**: Update `#blockly-div` width/height dari `#blockly-area` offsetWidth/Height sebelum svgResize
- **File**: `core/resizable-panels.js` (mousemove handler)
- **Verifikasi**: Drag vertical divider → Blockly canvas resize smoothly. Drag horizontal divider → sama
**Bug 2 — App freeze saat kedua program pakai `while(true)`**
- **Root cause**: User menulis `while(true)` di `main_hmi_program`. Auto-wrapper menambah outer while-loop, tapi inner `while(true)` dengan HMI calls (synchronous) tidak pernah yield ke event loop
- **Fix**: HMI shadowed `highlightBlock` diubah dari sync no-op ke `async function` dengan `stopRequested` check. Setiap `await highlightBlock()` di generated code yield ke event loop + bisa di-stop. Juga override `window.highlightBlock` di non-debug `_runConcurrent` untuk main program
- **File**: `core/debug-engine.js``_runConcurrent()` dan `_runDebugConcurrent()`
- **Verifikasi**: `while(true)` di kedua program → app responsif. Klik Stop → keduanya berhenti
**Bug 3 — Variabel tidak ter-share antara main dan HMI program**
- **Root cause**: `definitions` (berisi `var led;`) di-eval di dua IIFE terpisah → dua scope terpisah. Main set `led = 1`, HMI baca `led` dari scope sendiri (tetap 0)
- **Fix**: Gabung kedua program dalam SATU eval — outer IIFE berisi `definitions`, dua inner IIFE (main + HMI) close over shared scope. Variabel yang diubah di main langsung terlihat di HMI via closure
- **File**: `core/debug-engine.js``_runConcurrent()` dan `_runDebugConcurrent()`
- **Verifikasi**: Set `led = 1` di main, baca `led` di HMI → LED update. Test dengan delay loop toggle variable
**Bug 4 — Delete HMI block tidak menghapus preview widget; undo muncul blank widget**
- **Root cause**: `_blockToWidget` Map di `hmi-preview.js` kehilangan sinkronisasi saat undo/redo. Blockly events pada undo tidak selalu re-add mapping, sehingga widget jadi orphan
- **Fix**: 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`
- **Verifikasi**: Place HMI block → widget muncul. Delete → widget hilang. Undo → widget kembali. Redo → widget hilang lagi

View File

@ -0,0 +1,34 @@
/**
* Block: hmiGetButton Read button press state (latch-until-read).
*
* Type: Value block (Boolean output, client-side)
* Category: HMI
*
* Returns true once per press, then auto-resets to false.
* Use inside the HMI program loop to detect button clicks.
*/
BlockRegistry.register({
name: 'hmiGetButton',
category: 'HMI',
categoryColor: '#00BCD4',
color: '#00BCD4',
tooltip: 'Returns true once when button is pressed (latch-until-read)',
definition: {
init: function () {
this.appendDummyInput()
.appendField('HMI Button pressed?')
.appendField(new Blockly.FieldTextInput('Btn1'), 'NAME');
this.setOutput(true, 'Boolean');
this.setColour('#00BCD4');
this.setTooltip('Returns true once when button is pressed (latch-until-read)');
},
},
generator: function (block) {
var name = block.getFieldValue('NAME');
var code = "HMI.getButton('" + name + "')";
return [code, Blockly.JavaScript.ORDER_FUNCTION_CALL];
},
});

View File

@ -0,0 +1,33 @@
/**
* Block: hmiGetSlider Read current slider value.
*
* Type: Value block (Number output, client-side)
* Category: HMI
*
* Returns the current slider position (set by user drag or setSlider).
*/
BlockRegistry.register({
name: 'hmiGetSlider',
category: 'HMI',
categoryColor: '#00BCD4',
color: '#00BCD4',
tooltip: 'Read the current slider value',
definition: {
init: function () {
this.appendDummyInput()
.appendField('HMI Slider value')
.appendField(new Blockly.FieldTextInput('Slider1'), 'NAME');
this.setOutput(true, 'Number');
this.setColour('#00BCD4');
this.setTooltip('Read the current slider value');
},
},
generator: function (block) {
var name = block.getFieldValue('NAME');
var code = "HMI.getSlider('" + name + "')";
return [code, Blockly.JavaScript.ORDER_FUNCTION_CALL];
},
});

View File

@ -0,0 +1,33 @@
/**
* Block: hmiGetSwitch Read current switch on/off state.
*
* Type: Value block (Boolean output, client-side)
* Category: HMI
*
* Returns true if switch is ON, false if OFF.
*/
BlockRegistry.register({
name: 'hmiGetSwitch',
category: 'HMI',
categoryColor: '#00BCD4',
color: '#00BCD4',
tooltip: 'Read the current switch state (on/off)',
definition: {
init: function () {
this.appendDummyInput()
.appendField('HMI Switch state')
.appendField(new Blockly.FieldTextInput('Switch1'), 'NAME');
this.setOutput(true, 'Boolean');
this.setColour('#00BCD4');
this.setTooltip('Read the current switch state (on/off)');
},
},
generator: function (block) {
var name = block.getFieldValue('NAME');
var code = "HMI.getSwitch('" + name + "')";
return [code, Blockly.JavaScript.ORDER_FUNCTION_CALL];
},
});

View File

@ -0,0 +1,49 @@
/**
* Block: hmiSetButton Create a clickable button on the HMI panel.
*
* Type: Statement block (client-side, no ROS2 action)
* Category: HMI
*
* Creates or updates a button widget in the HMI panel.
* Use hmiGetButton to read press state (latch-until-read).
*/
BlockRegistry.register({
name: 'hmiSetButton',
category: 'HMI',
categoryColor: '#00BCD4',
color: '#00BCD4',
tooltip: 'Create a clickable button on the HMI panel',
definition: {
init: function () {
this.appendDummyInput()
.appendField('HMI Button')
.appendField(new Blockly.FieldTextInput('Btn1'), 'NAME')
.appendField('label')
.appendField(new Blockly.FieldTextInput('Press'), 'LABEL')
.appendField('color')
.appendField(new Blockly.FieldDropdown([
['blue', '#2196f3'],
['green', '#4caf50'],
['red', '#f44336'],
['orange', '#ff9800'],
['gray', '#607d8b'],
]), 'COLOR');
this.setPreviousStatement(true, null);
this.setNextStatement(true, null);
this.setColour('#00BCD4');
this.setTooltip('Create a clickable button on the HMI panel');
},
},
generator: function (block) {
var name = block.getFieldValue('NAME');
var label = block.getFieldValue('LABEL');
var color = block.getFieldValue('COLOR');
return (
"await highlightBlock('" + block.id + "');\n" +
"HMI.setButton('" + name + "', '" + label + "', '" + color + "');\n"
);
},
});

View File

@ -0,0 +1,49 @@
/**
* Block: hmiSetSlider Create a draggable slider on the HMI panel.
*
* Type: Statement block (client-side, no ROS2 action)
* Category: HMI
*
* Creates or updates a slider widget with min/max range.
* Use hmiGetSlider to read the current user-dragged value.
*/
BlockRegistry.register({
name: 'hmiSetSlider',
category: 'HMI',
categoryColor: '#00BCD4',
color: '#00BCD4',
tooltip: 'Create a draggable slider on the HMI panel',
definition: {
init: function () {
this.appendValueInput('VALUE')
.appendField('HMI Slider')
.appendField(new Blockly.FieldTextInput('Slider1'), 'NAME')
.setCheck('Number')
.appendField('=');
this.appendDummyInput()
.appendField('min')
.appendField(new Blockly.FieldNumber(0), 'MIN')
.appendField('max')
.appendField(new Blockly.FieldNumber(100), 'MAX');
this.setPreviousStatement(true, null);
this.setNextStatement(true, null);
this.setColour('#00BCD4');
this.setTooltip('Create a draggable slider on the HMI panel');
},
},
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.setSlider('" + name + "', Number(" + value + "), " + min + ", " + max + ");\n"
);
},
});

View File

@ -0,0 +1,42 @@
/**
* Block: hmiSetSwitch Create a toggle switch on the HMI panel.
*
* Type: Statement block (client-side, no ROS2 action)
* Category: HMI
*
* Creates or updates a toggle switch widget.
* Use hmiGetSwitch to read the current on/off state.
*/
BlockRegistry.register({
name: 'hmiSetSwitch',
category: 'HMI',
categoryColor: '#00BCD4',
color: '#00BCD4',
tooltip: 'Create a toggle switch on the HMI panel',
definition: {
init: function () {
this.appendValueInput('STATE')
.appendField('HMI Switch')
.appendField(new Blockly.FieldTextInput('Switch1'), 'NAME')
.setCheck('Boolean')
.appendField('state:');
this.setPreviousStatement(true, null);
this.setNextStatement(true, null);
this.setColour('#00BCD4');
this.setTooltip('Create a toggle switch on the HMI panel');
},
},
generator: function (block) {
var name = block.getFieldValue('NAME');
var state = Blockly.JavaScript.valueToCode(
block, 'STATE', Blockly.JavaScript.ORDER_ATOMIC
) || 'false';
return (
"await highlightBlock('" + block.id + "');\n" +
"HMI.setSwitch('" + name + "', Boolean(" + state + "));\n"
);
},
});

View File

@ -22,4 +22,10 @@ const BLOCK_FILES = [
'hmiSetNumber.js',
'hmiSetText.js',
'hmiSetGauge.js',
'hmiSetButton.js',
'hmiSetSlider.js',
'hmiSetSwitch.js',
'hmiGetButton.js',
'hmiGetSlider.js',
'hmiGetSwitch.js',
];

View File

@ -86,6 +86,20 @@ async function _runSingle(code) {
debugState.stopRequested = false;
updateButtonStates();
// Override highlightBlock — yields to macrotask queue periodically (~60Hz)
// so while(true) loops don't starve the event loop (Stop button, UI paint).
var originalHighlight = window.highlightBlock;
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) {
_lastYield = now;
await new Promise(function (r) { setTimeout(r, 0); });
}
};
HMI.setMode('runtime');
consoleLog('=== Program started ===', 'info');
@ -102,6 +116,7 @@ async function _runSingle(code) {
consoleLog('Error: ' + e.message, 'error');
}
} finally {
window.highlightBlock = originalHighlight;
debugState.isRunning = false;
debugState.isPaused = false;
workspace.highlightBlock(null);
@ -129,11 +144,21 @@ async function _runConcurrent(codeResult) {
debugState.stopRequested = false;
updateButtonStates();
// Override highlightBlock for main program — visual feedback + stop check
// Override highlightBlock for main program — visual feedback + stop check.
// Periodically yields to the macrotask queue (~60Hz) so the browser can
// process click events, paint, and let the HMI loop run via setTimeout.
// Without this, while(true) loops with only sync operations would starve
// the event loop (await on microtask never yields to macrotask queue).
var originalHighlight = window.highlightBlock;
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) {
_lastYield = now;
await new Promise(function (r) { setTimeout(r, 0); });
}
};
HMI.setMode('runtime');

View File

@ -25,6 +25,9 @@ const HMI = (() => {
number: [2, 1],
text: [3, 1],
gauge: [3, 2],
button: [2, 1],
slider: [3, 1],
switch: [2, 1],
};
// Throttle rendering
@ -197,6 +200,18 @@ const HMI = (() => {
var mid = ((widget.min || 0) + (widget.max || 100)) / 2;
widget.value = mid;
break;
case 'button':
widget._pressed = false;
break;
case 'slider':
widget._userValue = ((widget.min || 0) + (widget.max || 100)) / 2;
widget._userInteracting = false;
widget._userHasInteracted = false;
break;
case 'switch':
widget.state = false;
delete widget._userState;
break;
}
_scheduleRender(name);
}
@ -222,6 +237,8 @@ const HMI = (() => {
if (widget.type === 'led') config.color = widget.color;
if (widget.type === 'number') config.unit = widget.unit;
if (widget.type === 'gauge') { config.min = widget.min; config.max = widget.max; }
if (widget.type === 'button') { config.label = widget.label; config.color = widget.color; }
if (widget.type === 'slider') { config.min = widget.min; config.max = widget.max; }
items.push({
name: name,
@ -268,9 +285,15 @@ const HMI = (() => {
var widget = _widgets.get(name);
if (!widget) return;
// Skip render if user is actively dragging slider
if (widget.type === 'slider' && widget._userInteracting) return;
var el = _getWidgetEl(name);
if (!el) return;
// Store name for event handlers that need to trigger re-render
widget._renderName = name;
// Clear previous content
el.textContent = '';
@ -297,6 +320,15 @@ const HMI = (() => {
case 'gauge':
_renderGauge(body, widget);
break;
case 'button':
_renderButton(body, widget);
break;
case 'slider':
_renderSlider(body, widget);
break;
case 'switch':
_renderSwitch(body, widget);
break;
default:
body.textContent = 'Unknown widget type';
}
@ -370,6 +402,87 @@ const HMI = (() => {
body.appendChild(labels);
}
function _renderButton(body, widget) {
var btn = document.createElement('button');
btn.className = 'hmi-button';
btn.textContent = widget.label || 'Press';
btn.style.backgroundColor = widget.color || '#2196f3';
if (_mode === 'runtime') {
btn.addEventListener('pointerdown', function () {
widget._pressed = true;
});
} else {
btn.disabled = true;
}
body.appendChild(btn);
}
function _renderSlider(body, widget) {
var min = widget.min || 0;
var max = widget.max || 100;
var val = widget._userValue !== undefined ? widget._userValue : (widget.value || 0);
val = Math.max(min, Math.min(max, Number(val)));
var row = document.createElement('div');
row.style.display = 'flex';
row.style.alignItems = 'center';
var slider = document.createElement('input');
slider.type = 'range';
slider.className = 'hmi-slider';
slider.min = min;
slider.max = max;
slider.step = (max - min) <= 1 ? '0.01' : '1';
slider.value = val;
var valLabel = document.createElement('span');
valLabel.className = 'hmi-slider-value';
valLabel.textContent = String(val);
if (_mode === 'runtime') {
slider.addEventListener('input', function () {
widget._userValue = Number(slider.value);
widget._userHasInteracted = true;
valLabel.textContent = String(slider.value);
});
slider.addEventListener('mousedown', function () { widget._userInteracting = true; });
slider.addEventListener('touchstart', function () { widget._userInteracting = true; });
slider.addEventListener('mouseup', function () { widget._userInteracting = false; });
slider.addEventListener('touchend', function () { widget._userInteracting = false; });
} else {
slider.disabled = true;
}
row.appendChild(slider);
row.appendChild(valLabel);
body.appendChild(row);
}
function _renderSwitch(body, widget) {
var current = widget._userState !== undefined ? widget._userState : widget.state;
var track = document.createElement('div');
track.className = 'hmi-switch-track' + (current ? ' on' : '');
var thumb = document.createElement('div');
thumb.className = 'hmi-switch-thumb';
track.appendChild(thumb);
var label = document.createElement('span');
label.className = 'hmi-switch-label';
label.textContent = current ? 'ON' : 'OFF';
if (_mode === 'runtime') {
track.addEventListener('pointerdown', function () {
var cur = widget._userState !== undefined ? widget._userState : widget.state;
widget._userState = !cur;
_scheduleRender(widget._renderName);
});
}
body.appendChild(track);
body.appendChild(label);
}
// ─── Public API (called from generated Blockly code) ──────────────────
function setLED(name, state, color) {
@ -416,6 +529,70 @@ const HMI = (() => {
}
}
// ─── Interactive widget setters ──────────────────────────────────────
function setButton(name, label, color) {
var existing = _widgets.get(name);
if (existing) {
existing.label = label || existing.label || 'Press';
existing.color = color || existing.color || '#2196f3';
_scheduleRender(name);
} else {
addWidget(name, 'button', { label: label || 'Press', color: color || '#2196f3', _pressed: false });
}
}
function setSlider(name, value, min, max) {
var existing = _widgets.get(name);
if (existing) {
existing.min = min;
existing.max = max;
// Only update display value if user has never interacted
if (!existing._userInteracting && !existing._userHasInteracted) {
existing._userValue = value;
_scheduleRender(name);
}
existing.value = value;
} else {
addWidget(name, 'slider', { value: value, min: min, max: max, _userValue: value, _userInteracting: false, _userHasInteracted: false });
}
}
function setSwitch(name, state) {
var existing = _widgets.get(name);
if (existing) {
existing.state = Boolean(state);
// Only re-render if user hasn't interacted yet
if (existing._userState === undefined) {
_scheduleRender(name);
}
} else {
addWidget(name, 'switch', { state: Boolean(state) });
}
}
// ─── Interactive widget getters ──────────────────────────────────────
function getButton(name) {
var widget = _widgets.get(name);
if (!widget) return false;
var pressed = Boolean(widget._pressed);
widget._pressed = false; // latch-until-read
return pressed;
}
function getSlider(name) {
var widget = _widgets.get(name);
if (!widget) return 0;
return Number(widget._userValue !== undefined ? widget._userValue : widget.value);
}
function getSwitch(name) {
var widget = _widgets.get(name);
if (!widget) return false;
return Boolean(widget._userState !== undefined ? widget._userState : widget.state);
}
/**
* Remove all widgets and reset grid.
*/
@ -430,6 +607,8 @@ const HMI = (() => {
return {
init, addWidget, removeWidget, updateWidget,
setMode, getMode, getLayout, loadLayout,
setLED, setNumber, setText, setGauge, clearAll,
setLED, setNumber, setText, setGauge,
setButton, getButton, setSlider, getSlider, setSwitch, getSwitch,
clearAll,
};
})();

View File

@ -19,14 +19,45 @@ function initHMIPreview(ws) {
hmiSetNumber: 'number',
hmiSetText: 'text',
hmiSetGauge: 'gauge',
hmiSetButton: 'button',
hmiSetSlider: 'slider',
hmiSetSwitch: 'switch',
};
// Name prefixes for auto-increment (extracted from default field values)
var _namePrefixPattern = /^(.+?)(\d+)$/;
/**
* Generate a unique widget name by auto-incrementing the numeric suffix.
* e.g., if 'LED1' is taken, returns 'LED2', then 'LED3', etc.
*/
function _uniqueName(baseName) {
var taken = false;
_blockToWidget.forEach(function (n) { if (n === baseName) taken = true; });
if (!taken) return baseName;
var match = _namePrefixPattern.exec(baseName);
var prefix = match ? match[1] : baseName;
var counter = match ? Number(match[2]) : 1;
while (true) {
counter++;
var candidate = prefix + counter;
var used = false;
_blockToWidget.forEach(function (n) { if (n === candidate) used = true; });
if (!used) return candidate;
}
}
// Default preview configs per widget type
var _previewDefaults = {
led: { state: false, color: '#4caf50' },
number: { value: '---', unit: '' },
text: { text: '' },
gauge: { value: 50, min: 0, max: 100 },
button: { label: 'Press', color: '#2196f3', _pressed: false },
slider: { value: 50, min: 0, max: 100, _userValue: 50, _userInteracting: false },
switch: { state: false },
};
/**
@ -48,6 +79,16 @@ function initHMIPreview(ws) {
config.value = (config.min + config.max) / 2;
} else if (type === 'text') {
config.text = name || '';
} else if (type === 'button') {
config.label = block.getFieldValue('LABEL') || 'Press';
config.color = block.getFieldValue('COLOR') || '#2196f3';
} else if (type === 'slider') {
config.min = Number(block.getFieldValue('MIN')) || 0;
config.max = Number(block.getFieldValue('MAX')) || 100;
config.value = (config.min + config.max) / 2;
config._userValue = config.value;
} else if (type === 'switch') {
config.state = false;
}
return { name: name, config: config };
@ -89,6 +130,13 @@ function initHMIPreview(ws) {
var info = _readBlockConfig(block);
var name = info.name || block.type + '_' + block.id.substring(0, 4);
// Auto-increment name if already taken by another block
var uniqueName = _uniqueName(name);
if (uniqueName !== name) {
block.setFieldValue(uniqueName, 'NAME');
name = uniqueName;
}
_blockToWidget.set(block.id, name);
HMI.addWidget(name, type, info.config, block.id);
}
@ -99,10 +147,17 @@ function initHMIPreview(ws) {
var ids = event.ids || [event.blockId];
for (var i = 0; i < ids.length; i++) {
var name = _blockToWidget.get(ids[i]);
var deletedId = ids[i];
var name = _blockToWidget.get(deletedId);
if (name) {
_blockToWidget.delete(deletedId);
// Only remove widget if no other block still uses this name
var stillUsed = false;
_blockToWidget.forEach(function (n) { if (n === name) stillUsed = true; });
if (!stillUsed) {
HMI.removeWidget(name);
_blockToWidget.delete(ids[i]);
}
}
}
}

View File

@ -59,5 +59,5 @@ function refreshCodePanel() {
display = typeof codeResult === 'string' ? codeResult : '';
}
document.getElementById('code-output').textContent = display || '// (no blocks)';
document.getElementById('code-output').value = display || '// (no blocks)';
}

View File

@ -138,17 +138,24 @@
#code-panel {
flex: 1;
background: #1a1a1a;
overflow: auto;
padding: 12px;
overflow: hidden;
}
#code-output {
width: 100%;
height: 100%;
margin: 0;
padding: 12px;
border: none;
outline: none;
resize: none;
background: #1a1a1a;
font-family: 'Consolas', 'Courier New', monospace;
font-size: 13px;
color: #d4d4d4;
white-space: pre-wrap;
word-break: break-word;
white-space: pre;
overflow: auto;
box-sizing: border-box;
}
#btn-export {
@ -339,6 +346,64 @@
margin-top: 2px;
}
/* Button widget */
.hmi-button {
padding: 6px 16px;
border: 1px solid #555;
border-radius: 4px;
color: #fff;
font-size: 13px;
font-weight: 600;
cursor: pointer;
user-select: none;
transition: filter 0.1s;
}
.hmi-button:active { filter: brightness(0.7); }
.hmi-button:hover { filter: brightness(0.9); }
/* Slider widget */
.hmi-slider {
width: 100%;
accent-color: #4caf50;
cursor: pointer;
}
.hmi-slider-value {
font-family: 'Consolas', 'Courier New', monospace;
font-size: 14px;
font-weight: 600;
margin-left: 8px;
}
/* Switch widget */
.hmi-switch-track {
width: 40px;
height: 22px;
border-radius: 11px;
background: #555;
position: relative;
cursor: pointer;
display: inline-block;
transition: background 0.2s;
vertical-align: middle;
}
.hmi-switch-track.on { background: #4caf50; }
.hmi-switch-thumb {
width: 18px;
height: 18px;
border-radius: 50%;
background: #fff;
position: absolute;
top: 2px;
left: 2px;
transition: left 0.2s;
}
.hmi-switch-track.on .hmi-switch-thumb { left: 20px; }
.hmi-switch-label {
font-size: 13px;
margin-left: 8px;
vertical-align: middle;
}
/* Design-time preview indicator */
.hmi-widget-preview .hmi-widget-body { opacity: 0.5; }
.hmi-widget-preview .hmi-widget-label::after {
@ -376,7 +441,7 @@
<div id="blockly-div"></div>
</div>
<div id="code-panel" style="display:none">
<pre id="code-output"></pre>
<textarea id="code-output" readonly></textarea>
</div>
</div>
<div id="resize-v" class="resize-divider-v"></div>

View File

@ -10,6 +10,53 @@
"y": -30,
"inputs": {
"BODY": {
"block": {
"type": "controls_whileUntil",
"id": "$uyZgjkMX%3!oZkaV{:v",
"fields": {
"MODE": "WHILE"
},
"inputs": {
"BOOL": {
"block": {
"type": "logic_compare",
"id": ".qR|JeS~%Aq7^G/.FC5$",
"fields": {
"OP": "NEQ"
},
"inputs": {
"A": {
"block": {
"type": "hmiGetButton",
"id": "$IXubt[ys3S3jc`.w5F0",
"fields": {
"NAME": "Btn1"
}
}
},
"B": {
"block": {
"type": "logic_boolean",
"id": ")Uq|v]]Tho:rGgn1ik{H",
"fields": {
"BOOL": "TRUE"
}
}
}
}
}
},
"DO": {
"block": {
"type": "delay",
"id": "CY3QgWQ(jalnV(6VA:.w",
"fields": {
"DURATION_MS": 500
}
}
}
},
"next": {
"block": {
"type": "controls_whileUntil",
"id": "qWwrS*25QaeI4eIwq+~%",
@ -168,16 +215,16 @@
"id": "7v:u)fVh$GAwIhdwtH-i",
"fields": {
"VAR": {
"id": "C_:{ED@bJimgLzEmC6(`"
"id": ",S57`x^[^{+#%(k%Q5~)"
}
},
"inputs": {
"VALUE": {
"block": {
"type": "math_number",
"id": "x.-GBy*1d`n}m-;O;Nre",
"type": "hmiGetSwitch",
"id": ";OlVmrI`tP2Wn98MhQ(6",
"fields": {
"NUM": 1
"NAME": "Switch1"
}
}
}
@ -258,10 +305,10 @@
"inputs": {
"VALUE": {
"block": {
"type": "math_number",
"id": "[C@fwlekugl(`pi1b;1(",
"type": "hmiGetSlider",
"id": "3;jHO[lTaxLBbEe*$~?N",
"fields": {
"NUM": 100
"NAME": "Slider1"
}
}
}
@ -376,11 +423,13 @@
}
}
}
}
}
},
{
"type": "procedures_defreturn",
"id": "4W(2:w1NGV^I;j6@^_I|",
"x": 330,
"x": 530,
"y": -30,
"extraState": {
"params": [
@ -451,11 +500,40 @@
"y": 130,
"inputs": {
"BODY": {
"block": {
"type": "hmiSetButton",
"id": "0mslI4E`fhAji;c29:)%",
"fields": {
"NAME": "Btn1",
"LABEL": "Press",
"COLOR": "#2196f3"
},
"next": {
"block": {
"type": "hmiSetSwitch",
"id": "Z9(pm)t5OJE/wOSg+Xm/",
"fields": {
"NAME": "Switch1"
},
"inputs": {
"STATE": {
"block": {
"type": "variables_get",
"id": "1K:Kr/,*|b`yzsoI%M,n",
"fields": {
"VAR": {
"id": ",S57`x^[^{+#%(k%Q5~)"
}
}
}
}
},
"next": {
"block": {
"type": "hmiSetLed",
"id": "]1*kO+b!c-UMEf9QD)Tj",
"fields": {
"NAME": "LED1",
"NAME": "pinInLed",
"COLOR": "#4caf50"
},
"inputs": {
@ -491,6 +569,49 @@
}
}
},
"next": {
"block": {
"type": "hmiSetLed",
"id": "7cx}vtbz1RDUUmaiM7EW",
"fields": {
"NAME": "SWITCH",
"COLOR": "#2196f3"
},
"inputs": {
"STATE": {
"block": {
"type": "variables_get",
"id": "[rrmqtM21gFF)[f_~Nly",
"fields": {
"VAR": {
"id": ",S57`x^[^{+#%(k%Q5~)"
}
}
}
}
},
"next": {
"block": {
"type": "hmiSetSlider",
"id": "fT?r]`hGbs8uNq.x6Q~f",
"fields": {
"NAME": "Slider1",
"MIN": 0,
"MAX": 100
},
"inputs": {
"VALUE": {
"block": {
"type": "variables_get",
"id": "lPoTKTh-11[Vvl*Pr.xB",
"fields": {
"VAR": {
"id": "[g,f6Mp!O$eZPCFs0U[H"
}
}
}
}
},
"next": {
"block": {
"type": "hmiSetGauge",
@ -564,6 +685,14 @@
}
}
}
}
}
}
}
}
}
}
}
]
},
"variables": [
@ -590,14 +719,39 @@
{
"name": "text",
"id": "xjE5n-FetBK*,)qj?pyn"
},
{
"name": "switchLed",
"id": ",S57`x^[^{+#%(k%Q5~)"
}
]
},
"hmiLayout": [
{
"name": "LED1",
"name": "Btn1",
"type": "button",
"x": 0,
"y": 6,
"w": 2,
"h": 1,
"config": {
"label": "Press",
"color": "#2196f3"
}
},
{
"name": "Switch1",
"type": "switch",
"x": 2,
"y": 6,
"w": 2,
"h": 1,
"config": {}
},
{
"name": "pinInLed",
"type": "led",
"x": 1,
"x": 0,
"y": 1,
"w": 2,
"h": 1,
@ -605,12 +759,35 @@
"color": "#4caf50"
}
},
{
"name": "SWITCH",
"type": "led",
"x": 2,
"y": 1,
"w": 2,
"h": 1,
"config": {
"color": "#2196f3"
}
},
{
"name": "Slider1",
"type": "slider",
"x": 0,
"y": 2,
"w": 6,
"h": 1,
"config": {
"min": 0,
"max": 100
}
},
{
"name": "Gauge1",
"type": "gauge",
"x": 1,
"y": 2,
"w": 2,
"x": 0,
"y": 3,
"w": 6,
"h": 2,
"config": {
"min": 0,
@ -629,7 +806,7 @@
{
"name": "Value1",
"type": "number",
"x": 3,
"x": 4,
"y": 1,
"w": 2,
"h": 1,

358
workspace_button.json Normal file
View File

@ -0,0 +1,358 @@
{
"workspace": {
"blocks": {
"languageVersion": 0,
"blocks": [
{
"type": "main_program",
"id": "FwZNMjJgJ)7HoDA$5w.;",
"x": 10,
"y": 250,
"inputs": {
"BODY": {
"block": {
"type": "variables_set",
"id": "jq.DqU:AzS]6s/=;P5n)",
"fields": {
"VAR": {
"id": "Egl#Lg:b:#oT8py7HNji"
}
},
"inputs": {
"VALUE": {
"block": {
"type": "math_number",
"id": "%M3jtK*#Dl$Jk,IMAHYw",
"fields": {
"NUM": 0
}
}
}
},
"next": {
"block": {
"type": "print",
"id": "[wR+UihNLips(NA%h9`_",
"inputs": {
"TEXT": {
"block": {
"type": "text",
"id": "aqU+];u/*%~8.k8bdz4V",
"fields": {
"TEXT": "press buttonn to start"
}
}
}
},
"next": {
"block": {
"type": "controls_whileUntil",
"id": "//-l#r#1N5TDLuN+#zx1",
"fields": {
"MODE": "WHILE"
},
"inputs": {
"BOOL": {
"block": {
"type": "logic_compare",
"id": "5Dy3Mnq[_?Fmme`41wi{",
"fields": {
"OP": "NEQ"
},
"inputs": {
"A": {
"block": {
"type": "hmiGetButton",
"id": "b|}/Rl9Y}1N.PpvdAwa1",
"fields": {
"NAME": "Btn1"
}
}
},
"B": {
"block": {
"type": "logic_boolean",
"id": "FRS+]Kg{aBCBK.+WNc|@",
"fields": {
"BOOL": "TRUE"
}
}
}
}
}
},
"DO": {
"block": {
"type": "delay",
"id": "zI{i7r}Tw4ywwzOf*YSn",
"fields": {
"DURATION_MS": 500
}
}
}
},
"next": {
"block": {
"type": "controls_whileUntil",
"id": "4*{fL8MMBf/C:G0q}M6[",
"fields": {
"MODE": "WHILE"
},
"inputs": {
"BOOL": {
"block": {
"type": "logic_boolean",
"id": "Uf(_X29G;xDO4Isn`XPq",
"fields": {
"BOOL": "TRUE"
}
}
},
"DO": {
"block": {
"type": "variables_set",
"id": "5.J3m#77vjYS3JBRJ{@3",
"fields": {
"VAR": {
"id": "Egl#Lg:b:#oT8py7HNji"
}
},
"inputs": {
"VALUE": {
"block": {
"type": "hmiGetSlider",
"id": "Kh)UlG`=^iajkCHO+cb9",
"fields": {
"NAME": "Slider1"
}
}
}
},
"next": {
"block": {
"type": "variables_set",
"id": ".,zM~oP,:yQiM{qn0KRG",
"fields": {
"VAR": {
"id": "bPB1tcV1*$)p4V+TC:iO"
}
},
"inputs": {
"VALUE": {
"block": {
"type": "hmiGetSwitch",
"id": "=a=-/mx/QldA)%JCB=C=",
"fields": {
"NAME": "Switch1"
}
}
}
},
"next": {
"block": {
"type": "delay",
"id": "yP/~i8^p/`Ez.![mkDa@",
"fields": {
"DURATION_MS": 500
}
}
}
}
}
}
}
}
}
}
}
}
}
}
}
}
}
},
{
"type": "main_hmi_program",
"id": "aQ23Jgk4vutyFR7Uf@F9",
"x": 650,
"y": 190,
"inputs": {
"BODY": {
"block": {
"type": "hmiSetButton",
"id": "`M)|pBM2F6eliBk[dl2N",
"fields": {
"NAME": "Btn1",
"LABEL": "Press",
"COLOR": "#2196f3"
},
"next": {
"block": {
"type": "hmiSetLed",
"id": "DMUx+:TEc%RvY6|sScm,",
"fields": {
"NAME": "LED1",
"COLOR": "#4caf50"
},
"inputs": {
"STATE": {
"block": {
"type": "variables_get",
"id": "aGXray@ADZEW0!1mQ$8k",
"fields": {
"VAR": {
"id": "bPB1tcV1*$)p4V+TC:iO"
}
}
}
}
},
"next": {
"block": {
"type": "hmiSetSwitch",
"id": "~Aaps.COC1?b%FuVndIr",
"fields": {
"NAME": "Switch1"
},
"inputs": {
"STATE": {
"block": {
"type": "variables_get",
"id": "y1--K#B+PYZWO}[}rh?Z",
"fields": {
"VAR": {
"id": "bPB1tcV1*$)p4V+TC:iO"
}
}
}
}
},
"next": {
"block": {
"type": "hmiSetSlider",
"id": "(5vus?:c@2QsU=MsdGGy",
"fields": {
"NAME": "Slider1",
"MIN": 0,
"MAX": 100
},
"inputs": {
"VALUE": {
"block": {
"type": "variables_get",
"id": "P$)Ot7t0iM5M(Z#iu98]",
"fields": {
"VAR": {
"id": "Egl#Lg:b:#oT8py7HNji"
}
}
}
}
},
"next": {
"block": {
"type": "hmiSetGauge",
"id": "+1},^Hibu{ls;[e1?yvQ",
"fields": {
"NAME": "Gauge1",
"MIN": 0,
"MAX": 100
},
"inputs": {
"VALUE": {
"block": {
"type": "variables_get",
"id": "?[`BA?SK]j;z](3EU2AQ",
"fields": {
"VAR": {
"id": "Egl#Lg:b:#oT8py7HNji"
}
}
}
}
}
}
}
}
}
}
}
}
}
}
}
}
}
]
},
"variables": [
{
"name": "led",
"id": "bPB1tcV1*$)p4V+TC:iO"
},
{
"name": "slide",
"id": "Egl#Lg:b:#oT8py7HNji"
}
]
},
"hmiLayout": [
{
"name": "Btn1",
"type": "button",
"x": 2,
"y": 0,
"w": 2,
"h": 1,
"config": {
"label": "Press",
"color": "#2196f3"
}
},
{
"name": "LED1",
"type": "led",
"x": 0,
"y": 0,
"w": 2,
"h": 1,
"config": {
"color": "#4caf50"
}
},
{
"name": "Switch1",
"type": "switch",
"x": 4,
"y": 0,
"w": 2,
"h": 1,
"config": {}
},
{
"name": "Slider1",
"type": "slider",
"x": 0,
"y": 1,
"w": 3,
"h": 1,
"config": {
"min": 0,
"max": 100
}
},
{
"name": "Gauge1",
"type": "gauge",
"x": 3,
"y": 1,
"w": 3,
"h": 2,
"config": {
"min": 0,
"max": 100
}
}
]
}