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
parent
086e5dce0c
commit
3039b1d109
|
|
@ -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
275
readme.md
|
|
@ -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 200px–50% viewport; console 80px–40% 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
|
||||
|
|
|
|||
|
|
@ -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];
|
||||
},
|
||||
});
|
||||
|
|
@ -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];
|
||||
},
|
||||
});
|
||||
|
|
@ -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];
|
||||
},
|
||||
});
|
||||
|
|
@ -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"
|
||||
);
|
||||
},
|
||||
});
|
||||
|
|
@ -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"
|
||||
);
|
||||
},
|
||||
});
|
||||
|
|
@ -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"
|
||||
);
|
||||
},
|
||||
});
|
||||
|
|
@ -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',
|
||||
];
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
};
|
||||
})();
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
HMI.removeWidget(name);
|
||||
_blockToWidget.delete(ids[i]);
|
||||
_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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)';
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
637
workspace.json
637
workspace.json
|
|
@ -12,160 +12,187 @@
|
|||
"BODY": {
|
||||
"block": {
|
||||
"type": "controls_whileUntil",
|
||||
"id": "qWwrS*25QaeI4eIwq+~%",
|
||||
"id": "$uyZgjkMX%3!oZkaV{:v",
|
||||
"fields": {
|
||||
"MODE": "WHILE"
|
||||
},
|
||||
"inputs": {
|
||||
"BOOL": {
|
||||
"block": {
|
||||
"type": "logic_boolean",
|
||||
"id": "iL:?Vnm{E8Pa6(SX-nEl",
|
||||
"type": "logic_compare",
|
||||
"id": ".qR|JeS~%Aq7^G/.FC5$",
|
||||
"fields": {
|
||||
"BOOL": "TRUE"
|
||||
"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": "variables_set",
|
||||
"id": "vCG[_})zpGPcSQBid!Yj",
|
||||
"type": "delay",
|
||||
"id": "CY3QgWQ(jalnV(6VA:.w",
|
||||
"fields": {
|
||||
"VAR": {
|
||||
"id": "C_:{ED@bJimgLzEmC6(`"
|
||||
}
|
||||
},
|
||||
"inputs": {
|
||||
"VALUE": {
|
||||
"block": {
|
||||
"type": "math_number",
|
||||
"id": "!uHp`,,`q_M2%HMnG0@[",
|
||||
"fields": {
|
||||
"NUM": 0
|
||||
}
|
||||
"DURATION_MS": 500
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"next": {
|
||||
"block": {
|
||||
"type": "controls_whileUntil",
|
||||
"id": "qWwrS*25QaeI4eIwq+~%",
|
||||
"fields": {
|
||||
"MODE": "WHILE"
|
||||
},
|
||||
"inputs": {
|
||||
"BOOL": {
|
||||
"block": {
|
||||
"type": "logic_boolean",
|
||||
"id": "iL:?Vnm{E8Pa6(SX-nEl",
|
||||
"fields": {
|
||||
"BOOL": "TRUE"
|
||||
}
|
||||
}
|
||||
},
|
||||
"next": {
|
||||
"DO": {
|
||||
"block": {
|
||||
"type": "variables_set",
|
||||
"id": "qzDOA-}@MU,]iqj`er}D",
|
||||
"id": "vCG[_})zpGPcSQBid!Yj",
|
||||
"fields": {
|
||||
"VAR": {
|
||||
"id": "[g,f6Mp!O$eZPCFs0U[H"
|
||||
"id": "C_:{ED@bJimgLzEmC6(`"
|
||||
}
|
||||
},
|
||||
"inputs": {
|
||||
"VALUE": {
|
||||
"block": {
|
||||
"type": "math_number",
|
||||
"id": "39KE0jn61fTR1Qp4)d57",
|
||||
"id": "!uHp`,,`q_M2%HMnG0@[",
|
||||
"fields": {
|
||||
"NUM": 500
|
||||
"NUM": 0
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"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": "variables_set",
|
||||
"id": "5WG|J40M0!L^mM5x_2@N",
|
||||
"type": "digitalOut",
|
||||
"id": "Cz1MB5`}~cPRiZhh$/P9",
|
||||
"fields": {
|
||||
"VAR": {
|
||||
"id": "xjE5n-FetBK*,)qj?pyn"
|
||||
}
|
||||
"GPIO": 17
|
||||
},
|
||||
"inputs": {
|
||||
"VALUE": {
|
||||
"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": "print",
|
||||
"id": "#iq/$E*oB0V`%5O}O+2!",
|
||||
"type": "variables_set",
|
||||
"id": "5WG|J40M0!L^mM5x_2@N",
|
||||
"fields": {
|
||||
"VAR": {
|
||||
"id": "xjE5n-FetBK*,)qj?pyn"
|
||||
}
|
||||
},
|
||||
"inputs": {
|
||||
"TEXT": {
|
||||
"VALUE": {
|
||||
"block": {
|
||||
"type": "variables_get",
|
||||
"id": "4[hYi)09%}I%Pj4}fhdb",
|
||||
"type": "text",
|
||||
"id": "^ZnZat6;hW,I`H7H15^^",
|
||||
"fields": {
|
||||
"VAR": {
|
||||
"id": "xjE5n-FetBK*,)qj?pyn"
|
||||
}
|
||||
"TEXT": "cek"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"next": {
|
||||
"block": {
|
||||
"type": "delay",
|
||||
"id": "IXp?_lac7+V*GG!lW{]0",
|
||||
"fields": {
|
||||
"DURATION_MS": 1000
|
||||
"type": "print",
|
||||
"id": "#iq/$E*oB0V`%5O}O+2!",
|
||||
"inputs": {
|
||||
"TEXT": {
|
||||
"block": {
|
||||
"type": "variables_get",
|
||||
"id": "4[hYi)09%}I%Pj4}fhdb",
|
||||
"fields": {
|
||||
"VAR": {
|
||||
"id": "xjE5n-FetBK*,)qj?pyn"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"next": {
|
||||
"block": {
|
||||
"type": "variables_set",
|
||||
"id": "1@OTYSIBwU0xvg(2?-y;",
|
||||
"type": "delay",
|
||||
"id": "IXp?_lac7+V*GG!lW{]0",
|
||||
"fields": {
|
||||
"VAR": {
|
||||
"id": "C_:{ED@bJimgLzEmC6(`"
|
||||
}
|
||||
},
|
||||
"inputs": {
|
||||
"VALUE": {
|
||||
"block": {
|
||||
"type": "digitalIn",
|
||||
"id": "DJpFh.6H~L9fX2V4SDJd",
|
||||
"fields": {
|
||||
"GPIO": 16
|
||||
}
|
||||
}
|
||||
}
|
||||
"DURATION_MS": 1000
|
||||
},
|
||||
"next": {
|
||||
"block": {
|
||||
"type": "variables_set",
|
||||
"id": "7v:u)fVh$GAwIhdwtH-i",
|
||||
"id": "1@OTYSIBwU0xvg(2?-y;",
|
||||
"fields": {
|
||||
"VAR": {
|
||||
"id": "C_:{ED@bJimgLzEmC6(`"
|
||||
|
|
@ -174,10 +201,10 @@
|
|||
"inputs": {
|
||||
"VALUE": {
|
||||
"block": {
|
||||
"type": "math_number",
|
||||
"id": "x.-GBy*1d`n}m-;O;Nre",
|
||||
"type": "digitalIn",
|
||||
"id": "DJpFh.6H~L9fX2V4SDJd",
|
||||
"fields": {
|
||||
"NUM": 1
|
||||
"GPIO": 16
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -185,124 +212,124 @@
|
|||
"next": {
|
||||
"block": {
|
||||
"type": "variables_set",
|
||||
"id": "toVbALp.{C5C+{9GMK%e",
|
||||
"id": "7v:u)fVh$GAwIhdwtH-i",
|
||||
"fields": {
|
||||
"VAR": {
|
||||
"id": "xjE5n-FetBK*,)qj?pyn"
|
||||
"id": ",S57`x^[^{+#%(k%Q5~)"
|
||||
}
|
||||
},
|
||||
"inputs": {
|
||||
"VALUE": {
|
||||
"block": {
|
||||
"type": "text",
|
||||
"id": "oA52{ydq^ZsJF:O@NU,d",
|
||||
"type": "hmiGetSwitch",
|
||||
"id": ";OlVmrI`tP2Wn98MhQ(6",
|
||||
"fields": {
|
||||
"TEXT": "cok"
|
||||
"NAME": "Switch1"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"next": {
|
||||
"block": {
|
||||
"type": "print",
|
||||
"id": "iB`R-:UTF#c3EXAU.Qh-",
|
||||
"type": "variables_set",
|
||||
"id": "toVbALp.{C5C+{9GMK%e",
|
||||
"fields": {
|
||||
"VAR": {
|
||||
"id": "xjE5n-FetBK*,)qj?pyn"
|
||||
}
|
||||
},
|
||||
"inputs": {
|
||||
"TEXT": {
|
||||
"VALUE": {
|
||||
"block": {
|
||||
"type": "variables_get",
|
||||
"id": "+wF~Y65u+aNA.ZX3!^Ra",
|
||||
"type": "text",
|
||||
"id": "oA52{ydq^ZsJF:O@NU,d",
|
||||
"fields": {
|
||||
"VAR": {
|
||||
"id": "xjE5n-FetBK*,)qj?pyn"
|
||||
}
|
||||
"TEXT": "cok"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"next": {
|
||||
"block": {
|
||||
"type": "variables_set",
|
||||
"id": "E8`?q[Z3*IpN3,52Y=jA",
|
||||
"fields": {
|
||||
"VAR": {
|
||||
"id": "[g,f6Mp!O$eZPCFs0U[H"
|
||||
}
|
||||
},
|
||||
"type": "print",
|
||||
"id": "iB`R-:UTF#c3EXAU.Qh-",
|
||||
"inputs": {
|
||||
"VALUE": {
|
||||
"TEXT": {
|
||||
"block": {
|
||||
"type": "math_number",
|
||||
"id": "iOJP=T-{O5h_^;{Yz?%(",
|
||||
"type": "variables_get",
|
||||
"id": "+wF~Y65u+aNA.ZX3!^Ra",
|
||||
"fields": {
|
||||
"NUM": 3000
|
||||
"VAR": {
|
||||
"id": "xjE5n-FetBK*,)qj?pyn"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"next": {
|
||||
"block": {
|
||||
"type": "delay",
|
||||
"id": "VQy`Sl3]ey49sP%+N6$R",
|
||||
"type": "variables_set",
|
||||
"id": "E8`?q[Z3*IpN3,52Y=jA",
|
||||
"fields": {
|
||||
"DURATION_MS": 1000
|
||||
"VAR": {
|
||||
"id": "[g,f6Mp!O$eZPCFs0U[H"
|
||||
}
|
||||
},
|
||||
"inputs": {
|
||||
"VALUE": {
|
||||
"block": {
|
||||
"type": "math_number",
|
||||
"id": "iOJP=T-{O5h_^;{Yz?%(",
|
||||
"fields": {
|
||||
"NUM": 3000
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"next": {
|
||||
"block": {
|
||||
"type": "variables_set",
|
||||
"id": "i|LkDgVjImZd2}owndlz",
|
||||
"type": "delay",
|
||||
"id": "VQy`Sl3]ey49sP%+N6$R",
|
||||
"fields": {
|
||||
"VAR": {
|
||||
"id": "[g,f6Mp!O$eZPCFs0U[H"
|
||||
}
|
||||
},
|
||||
"inputs": {
|
||||
"VALUE": {
|
||||
"block": {
|
||||
"type": "math_number",
|
||||
"id": "[C@fwlekugl(`pi1b;1(",
|
||||
"fields": {
|
||||
"NUM": 100
|
||||
}
|
||||
}
|
||||
}
|
||||
"DURATION_MS": 1000
|
||||
},
|
||||
"next": {
|
||||
"block": {
|
||||
"type": "pwmWrite",
|
||||
"id": "Ezn#r.|lvDj5{Q1-C:E$",
|
||||
"type": "variables_set",
|
||||
"id": "i|LkDgVjImZd2}owndlz",
|
||||
"fields": {
|
||||
"ADDRESS": "64",
|
||||
"CHANNEL": 0
|
||||
"VAR": {
|
||||
"id": "[g,f6Mp!O$eZPCFs0U[H"
|
||||
}
|
||||
},
|
||||
"inputs": {
|
||||
"PWM_VALUE": {
|
||||
"VALUE": {
|
||||
"block": {
|
||||
"type": "variables_get",
|
||||
"id": "OkA-}PRPzgi;[I)@vcG$",
|
||||
"type": "hmiGetSlider",
|
||||
"id": "3;jHO[lTaxLBbEe*$~?N",
|
||||
"fields": {
|
||||
"VAR": {
|
||||
"id": "[g,f6Mp!O$eZPCFs0U[H"
|
||||
}
|
||||
"NAME": "Slider1"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"next": {
|
||||
"block": {
|
||||
"type": "variables_set",
|
||||
"id": "m{+MhlXz-1tpl`mPPBh5",
|
||||
"type": "pwmWrite",
|
||||
"id": "Ezn#r.|lvDj5{Q1-C:E$",
|
||||
"fields": {
|
||||
"VAR": {
|
||||
"id": "ju{xs[rjZumqS87$0nhu"
|
||||
}
|
||||
"ADDRESS": "64",
|
||||
"CHANNEL": 0
|
||||
},
|
||||
"inputs": {
|
||||
"VALUE": {
|
||||
"PWM_VALUE": {
|
||||
"block": {
|
||||
"type": "odometryRead",
|
||||
"id": "v=Js89HC8D0UUA.-pN[q",
|
||||
"type": "variables_get",
|
||||
"id": "OkA-}PRPzgi;[I)@vcG$",
|
||||
"fields": {
|
||||
"SOURCE": "encoder"
|
||||
"VAR": {
|
||||
"id": "[g,f6Mp!O$eZPCFs0U[H"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -310,28 +337,50 @@
|
|||
"next": {
|
||||
"block": {
|
||||
"type": "variables_set",
|
||||
"id": "]LcUOwlc-y=`.e?EVgTa",
|
||||
"id": "m{+MhlXz-1tpl`mPPBh5",
|
||||
"fields": {
|
||||
"VAR": {
|
||||
"id": "Ug!mIa*[PnsL?H#9Ar*G"
|
||||
"id": "ju{xs[rjZumqS87$0nhu"
|
||||
}
|
||||
},
|
||||
"inputs": {
|
||||
"VALUE": {
|
||||
"block": {
|
||||
"type": "odometryGet",
|
||||
"id": "VG2Q/8?zcyU}s4!W;V/M",
|
||||
"type": "odometryRead",
|
||||
"id": "v=Js89HC8D0UUA.-pN[q",
|
||||
"fields": {
|
||||
"FIELD": "x"
|
||||
},
|
||||
"inputs": {
|
||||
"VAR": {
|
||||
"block": {
|
||||
"type": "variables_get",
|
||||
"id": "^AW6|z21?ycRyzJ2y5u9",
|
||||
"fields": {
|
||||
"VAR": {
|
||||
"id": "ju{xs[rjZumqS87$0nhu"
|
||||
"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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -380,7 +429,7 @@
|
|||
{
|
||||
"type": "procedures_defreturn",
|
||||
"id": "4W(2:w1NGV^I;j6@^_I|",
|
||||
"x": 330,
|
||||
"x": 530,
|
||||
"y": -30,
|
||||
"extraState": {
|
||||
"params": [
|
||||
|
|
@ -452,62 +501,28 @@
|
|||
"inputs": {
|
||||
"BODY": {
|
||||
"block": {
|
||||
"type": "hmiSetLed",
|
||||
"id": "]1*kO+b!c-UMEf9QD)Tj",
|
||||
"type": "hmiSetButton",
|
||||
"id": "0mslI4E`fhAji;c29:)%",
|
||||
"fields": {
|
||||
"NAME": "LED1",
|
||||
"COLOR": "#4caf50"
|
||||
},
|
||||
"inputs": {
|
||||
"STATE": {
|
||||
"block": {
|
||||
"type": "logic_compare",
|
||||
"id": "}?u/kp[mk2!n~i8gaCPu",
|
||||
"fields": {
|
||||
"OP": "NEQ"
|
||||
},
|
||||
"inputs": {
|
||||
"A": {
|
||||
"block": {
|
||||
"type": "variables_get",
|
||||
"id": "c;5GoC0CMnq-ck;%rMDE",
|
||||
"fields": {
|
||||
"VAR": {
|
||||
"id": "C_:{ED@bJimgLzEmC6(`"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"B": {
|
||||
"block": {
|
||||
"type": "math_number",
|
||||
"id": "#x9E1:31XXQM9stsU!TZ",
|
||||
"fields": {
|
||||
"NUM": 0
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
"NAME": "Btn1",
|
||||
"LABEL": "Press",
|
||||
"COLOR": "#2196f3"
|
||||
},
|
||||
"next": {
|
||||
"block": {
|
||||
"type": "hmiSetGauge",
|
||||
"id": "FeWSZv_C@ci}5(#:EV.A",
|
||||
"type": "hmiSetSwitch",
|
||||
"id": "Z9(pm)t5OJE/wOSg+Xm/",
|
||||
"fields": {
|
||||
"NAME": "Gauge1",
|
||||
"MIN": 0,
|
||||
"MAX": 4069
|
||||
"NAME": "Switch1"
|
||||
},
|
||||
"inputs": {
|
||||
"VALUE": {
|
||||
"STATE": {
|
||||
"block": {
|
||||
"type": "variables_get",
|
||||
"id": "LIIgG+UMkthq!S.K}dX8",
|
||||
"id": "1K:Kr/,*|b`yzsoI%M,n",
|
||||
"fields": {
|
||||
"VAR": {
|
||||
"id": "[g,f6Mp!O$eZPCFs0U[H"
|
||||
"id": ",S57`x^[^{+#%(k%Q5~)"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -515,19 +530,40 @@
|
|||
},
|
||||
"next": {
|
||||
"block": {
|
||||
"type": "hmiSetText",
|
||||
"id": "1d7kB$!lQpY@szAnVaLx",
|
||||
"type": "hmiSetLed",
|
||||
"id": "]1*kO+b!c-UMEf9QD)Tj",
|
||||
"fields": {
|
||||
"NAME": "Status"
|
||||
"NAME": "pinInLed",
|
||||
"COLOR": "#4caf50"
|
||||
},
|
||||
"inputs": {
|
||||
"TEXT": {
|
||||
"STATE": {
|
||||
"block": {
|
||||
"type": "variables_get",
|
||||
"id": "`usj,%r!VHQt}Ucr]Bxv",
|
||||
"type": "logic_compare",
|
||||
"id": "}?u/kp[mk2!n~i8gaCPu",
|
||||
"fields": {
|
||||
"VAR": {
|
||||
"id": "xjE5n-FetBK*,)qj?pyn"
|
||||
"OP": "NEQ"
|
||||
},
|
||||
"inputs": {
|
||||
"A": {
|
||||
"block": {
|
||||
"type": "variables_get",
|
||||
"id": "c;5GoC0CMnq-ck;%rMDE",
|
||||
"fields": {
|
||||
"VAR": {
|
||||
"id": "C_:{ED@bJimgLzEmC6(`"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"B": {
|
||||
"block": {
|
||||
"type": "math_number",
|
||||
"id": "#x9E1:31XXQM9stsU!TZ",
|
||||
"fields": {
|
||||
"NUM": 0
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -535,20 +571,113 @@
|
|||
},
|
||||
"next": {
|
||||
"block": {
|
||||
"type": "hmiSetNumber",
|
||||
"id": "-^@C9g@boq7MU?u2E}f9",
|
||||
"type": "hmiSetLed",
|
||||
"id": "7cx}vtbz1RDUUmaiM7EW",
|
||||
"fields": {
|
||||
"NAME": "Value1",
|
||||
"UNIT": "pin"
|
||||
"NAME": "SWITCH",
|
||||
"COLOR": "#2196f3"
|
||||
},
|
||||
"inputs": {
|
||||
"VALUE": {
|
||||
"STATE": {
|
||||
"block": {
|
||||
"type": "variables_get",
|
||||
"id": "Cpu4;(,q`6WS@(f@L,|6",
|
||||
"id": "[rrmqtM21gFF)[f_~Nly",
|
||||
"fields": {
|
||||
"VAR": {
|
||||
"id": "[g,f6Mp!O$eZPCFs0U[H"
|
||||
"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",
|
||||
"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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
Loading…
Reference in New Issue