diff --git a/README.md b/README.md
index eb1c2f9..a3d718b 100644
--- a/README.md
+++ b/README.md
@@ -18,8 +18,11 @@ other information that should be provided before the tool is installed.
- [SIL (Software-in-the-Loop) Host Build](#sil-software-in-the-loop-host-build)
- [Prerequisites](#prerequisites)
- [Compile and Run](#compile-and-run)
-- [Yocto Image Build (Raspberry Pi Target)](#yocto-image-build-raspberry-pi-target)
+- [minihildesk GUI Client](#minihildesk-gui-client)
- [Prerequisites](#prerequisites-1)
+ - [Compile and Run](#compile-and-run-1)
+- [Yocto Image Build (Raspberry Pi Target)](#yocto-image-build-raspberry-pi-target)
+ - [Prerequisites](#prerequisites-2)
- [Compile target image](#compile-target-image)
- [Flashing the Image to SD Card](#flashing-the-image-to-sd-card)
- [Option A: Using `bmaptool` (Recommended - Fast & Direct)](#option-a-using-bmaptool-recommended---fast--direct)
@@ -28,6 +31,10 @@ other information that should be provided before the tool is installed.
- [JSON-RPC 2.0 API Specification](#json-rpc-20-api-specification)
- [`set_relay`](#set_relay)
- [`get_relays`](#get_relays)
+ - [`start_timer`](#start_timer)
+ - [`start_pulse`](#start_pulse)
+ - [`start_blink`](#start_blink)
+ - [`get_relay_status`](#get_relay_status)
- [Docs](#docs)
- [Copyright and licence](#copyright-and-licence)
@@ -65,6 +72,35 @@ The daemon will boot in SIL mode and print:
---
+### minihildesk GUI Client
+
+**minihildesk** is a premium GTKmm-based C++ desktop GUI client designed to connect to the `minihild` server (either running on a Raspberry Pi target or locally in SIL mock mode).
+
+It features:
+- A connection bar supporting standard TCP connection, **SSL/TLS secure connection**, and **Mutual TLS (mTLS) client verification**.
+- A grid of 8 Relay Control Cards displaying status via glowing LED indicators and active green borders.
+- Multi-mode relay control per channel: **Toggle** (manual switch), **Timer** (seconds spin-input), **Pulse** (milliseconds spin-input up to 100,000 ms), and **Blink** (ON/OFF ms inputs and cycle count).
+- Automatic hardware safety auto-off logic: switching modes on an active channel immediately turns the relay OFF on the server before entering the new mode.
+- A monospace green log terminal at the bottom showcasing outgoing and incoming JSON-RPC traffic.
+
+#### Prerequisites
+Install the GTKmm-4.0 development headers, OpenSSL, and JSON library:
+```bash
+sudo apt-get install libgtkmm-4.0-dev libssl-dev nlohmann-json3-dev
+```
+
+#### Compile and Run
+```bash
+# Configure and compile using CMake
+cmake -B sw/minihildesk/build -S sw/minihildesk
+cmake --build sw/minihildesk/build
+
+# Start the desktop GUI app
+./sw/minihildesk/build/minihildesk
+```
+
+---
+
### Yocto Image Build (Raspberry Pi Target)
MiniHIL packages `minihild` into a custom Yocto Linux image (`minihil-image`) using `meta-raspberrypi` and Poky.
@@ -143,7 +179,7 @@ bzcat sw/build-minihil/tmp/deploy/images/raspberrypi3-64/minihil-image-raspberry
You can send JSON-RPC text frames (terminated by `\n`) to port `9000`.
#### `set_relay`
-Energize or de-energize a relay channel (1 to 8):
+Energize or de-energize a relay channel manually (1 to 8):
- **Request**:
```json
{"jsonrpc": "2.0", "method": "set_relay", "params": {"relay_id": 3, "state": true}, "id": 1}
@@ -154,7 +190,7 @@ Energize or de-energize a relay channel (1 to 8):
```
#### `get_relays`
-Query states of all 8 relay channels:
+Query immediate physical states of all 8 relay channels:
- **Request**:
```json
{"jsonrpc": "2.0", "method": "get_relays", "id": 2}
@@ -164,6 +200,50 @@ Query states of all 8 relay channels:
{"jsonrpc": "2.0", "result": {"1": false, "2": false, "3": true, "4": false, "5": false, "6": false, "7": false, "8": false}, "id": 2}
```
+#### `start_timer`
+Keep a relay active for a specific duration in seconds:
+- **Request**:
+ ```json
+ {"jsonrpc": "2.0", "method": "start_timer", "params": {"relay_id": 2, "seconds": 10}, "id": 3}
+ ```
+- **Response**:
+ ```json
+ {"jsonrpc": "2.0", "result": {"relay_id": 2, "seconds": 10, "success": true}, "id": 3}
+ ```
+
+#### `start_pulse`
+Generate a single momentary pulse in milliseconds (up to 100,000 ms):
+- **Request**:
+ ```json
+ {"jsonrpc": "2.0", "method": "start_pulse", "params": {"relay_id": 3, "duration_ms": 5000}, "id": 4}
+ ```
+- **Response**:
+ ```json
+ {"jsonrpc": "2.0", "result": {"relay_id": 3, "duration_ms": 5000, "success": true}, "id": 4}
+ ```
+
+#### `start_blink`
+Repeatedly cycle relay state ON and OFF:
+- **Request**:
+ ```json
+ {"jsonrpc": "2.0", "method": "start_blink", "params": {"relay_id": 4, "on_ms": 1000, "off_ms": 1000, "count": 5}, "id": 5}
+ ```
+- **Response**:
+ ```json
+ {"jsonrpc": "2.0", "result": {"relay_id": 4, "on_ms": 1000, "off_ms": 1000, "count": 5, "success": true}, "id": 5}
+ ```
+
+#### `get_relay_status`
+Query detailed diagnostic status and remaining time for a single channel:
+- **Request**:
+ ```json
+ {"jsonrpc": "2.0", "method": "get_relay_status", "params": {"relay_id": 2}, "id": 6}
+ ```
+- **Response**:
+ ```json
+ {"jsonrpc": "2.0", "result": {"relay_id": 2, "status": "Channel 2: ON (Timer, rem: 8s)"}, "id": 6}
+ ```
+
### Docs
[](https://minihil.readthedocs.io/projects/minihil/en/latest/?badge=latest)
@@ -182,4 +262,4 @@ Copyright (C) 2020 - 2026 by [electux.github.io/minihil](https://electux.github.
Lets help and support Raspberry PI && GNOME.
-
\ No newline at end of file
+
\ No newline at end of file
diff --git a/docs/build/doctrees/environment.pickle b/docs/build/doctrees/environment.pickle
index 60799f5..b712691 100644
Binary files a/docs/build/doctrees/environment.pickle and b/docs/build/doctrees/environment.pickle differ
diff --git a/docs/build/doctrees/index.doctree b/docs/build/doctrees/index.doctree
index 545e0a3..91d1c8f 100644
Binary files a/docs/build/doctrees/index.doctree and b/docs/build/doctrees/index.doctree differ
diff --git a/docs/build/html/_sources/index.rst.txt b/docs/build/html/_sources/index.rst.txt
index e434dc8..550f466 100644
--- a/docs/build/html/_sources/index.rst.txt
+++ b/docs/build/html/_sources/index.rst.txt
@@ -55,6 +55,38 @@ Compile and Run
The daemon will boot in SIL mode and print:
``[SilRelayController] Software-in-the-Loop simulation initialized.``
+minihildesk GUI Client
+----------------------
+
+**minihildesk** is a premium GTKmm-based C++ desktop GUI client designed to connect to the ``minihild`` server (either running on a Raspberry Pi target or locally in SIL mock mode).
+
+It features:
+
+* A connection bar supporting standard TCP connection, **SSL/TLS secure connection**, and **Mutual TLS (mTLS) client verification**.
+* A grid of 8 Relay Control Cards displaying status via glowing LED indicators and active green borders.
+* Multi-mode relay control per channel: **Toggle** (manual switch), **Timer** (seconds spin-input), **Pulse** (milliseconds spin-input up to 100,000 ms), and **Blink** (ON/OFF ms inputs and cycle count).
+* Automatic hardware safety auto-off logic: switching modes on an active channel immediately turns the relay OFF on the server before entering the new mode.
+* A monospace green log terminal at the bottom showcasing outgoing and incoming JSON-RPC traffic.
+
+Prerequisites
+~~~~~~~~~~~~~
+Install the GTKmm-4.0 development headers, OpenSSL, and JSON library:
+
+.. code-block:: bash
+
+ sudo apt-get install libgtkmm-4.0-dev libssl-dev nlohmann-json3-dev
+
+Compile and Run
+~~~~~~~~~~~~~~~
+.. code-block:: bash
+
+ # Configure and compile using CMake
+ cmake -B sw/minihildesk/build -S sw/minihildesk
+ cmake --build sw/minihildesk/build
+
+ # Start the desktop GUI app
+ ./sw/minihildesk/build/minihildesk
+
Yocto Image Build (Raspberry Pi Target)
---------------------------------------
@@ -113,6 +145,70 @@ Query states of all 8 relay channels:
{"jsonrpc": "2.0", "result": {"1": false, "2": false, "3": true, "4": false, "5": false, "6": false, "7": false, "8": false}, "id": 2}
+start_timer
+~~~~~~~~~~~
+Keep a relay active for a specific duration in seconds:
+
+* **Request**:
+
+ .. code-block:: json
+
+ {"jsonrpc": "2.0", "method": "start_timer", "params": {"relay_id": 2, "seconds": 10}, "id": 3}
+
+* **Response**:
+
+ .. code-block:: json
+
+ {"jsonrpc": "2.0", "result": {"relay_id": 2, "seconds": 10, "success": true}, "id": 3}
+
+start_pulse
+~~~~~~~~~~~
+Generate a single momentary pulse in milliseconds (up to 100,000 ms):
+
+* **Request**:
+
+ .. code-block:: json
+
+ {"jsonrpc": "2.0", "method": "start_pulse", "params": {"relay_id": 3, "duration_ms": 5000}, "id": 4}
+
+* **Response**:
+
+ .. code-block:: json
+
+ {"jsonrpc": "2.0", "result": {"relay_id": 3, "duration_ms": 5000, "success": true}, "id": 4}
+
+start_blink
+~~~~~~~~~~~
+Repeatedly cycle relay state ON and OFF:
+
+* **Request**:
+
+ .. code-block:: json
+
+ {"jsonrpc": "2.0", "method": "start_blink", "params": {"relay_id": 4, "on_ms": 1000, "off_ms": 1000, "count": 5}, "id": 5}
+
+* **Response**:
+
+ .. code-block:: json
+
+ {"jsonrpc": "2.0", "result": {"relay_id": 4, "on_ms": 1000, "off_ms": 1000, "count": 5, "success": true}, "id": 5}
+
+get_relay_status
+~~~~~~~~~~~~~~~~
+Query detailed diagnostic status and remaining time for a single channel:
+
+* **Request**:
+
+ .. code-block:: json
+
+ {"jsonrpc": "2.0", "method": "get_relay_status", "params": {"relay_id": 2}, "id": 6}
+
+* **Response**:
+
+ .. code-block:: json
+
+ {"jsonrpc": "2.0", "result": {"relay_id": 2, "status": "Channel 2: ON (Timer, rem: 8s)"}, "id": 6}
+
Copyright and licence
----------------------
@@ -127,7 +223,4 @@ Copyright and licence
Copyright (C) 2020 - 2026 by `electux.github.io/minihil `_
.. image:: https://raw.githubusercontent.com/electux/minihil/master/docs/foundations.png
- :alt: Raspberry Pi & GNOME Foundations
- :width: 681px
- :height: 100px
-
+ :alt: Foundations
diff --git a/docs/build/html/index.html b/docs/build/html/index.html
index 60d50a4..03dd946 100644
--- a/docs/build/html/index.html
+++ b/docs/build/html/index.html
@@ -69,11 +69,41 @@ Compile and Run
+
+minihildesk GUI Client
+minihildesk is a premium GTKmm-based C++ desktop GUI client designed to connect to the minihild server (either running on a Raspberry Pi target or locally in SIL mock mode).
+It features:
+
+A connection bar supporting standard TCP connection, SSL/TLS secure connection, and Mutual TLS (mTLS) client verification.
+A grid of 8 Relay Control Cards displaying status via glowing LED indicators and active green borders.
+Multi-mode relay control per channel: Toggle (manual switch), Timer (seconds spin-input), Pulse (milliseconds spin-input up to 100,000 ms), and Blink (ON/OFF ms inputs and cycle count).
+Automatic hardware safety auto-off logic: switching modes on an active channel immediately turns the relay OFF on the server before entering the new mode.
+A monospace green log terminal at the bottom showcasing outgoing and incoming JSON-RPC traffic.
+
+
+Prerequisites
+Install the GTKmm-4.0 development headers, OpenSSL, and JSON library:
+sudo apt-get install libgtkmm-4.0-dev libssl-dev nlohmann-json3-dev
+
+
+
+
+Compile and Run
+# Configure and compile using CMake
+cmake -B sw/minihildesk/build -S sw/minihildesk
+cmake --build sw/minihildesk/build
+
+# Start the desktop GUI app
+./sw/minihildesk/build/minihildesk
+
+
+
+
Yocto Image Build (Raspberry Pi Target)
MiniHIL packages minihild into a custom Yocto Linux image (minihil-image) using meta-raspberrypi and Poky.
-
-Prerequisites
+
+Prerequisites
Ensure your build host has all required packages for Yocto Scarthgap (refer to sw/rpi-base-platform/README.md for the list of packages).
@@ -123,13 +153,76 @@ get_relays
+
+start_pulse
+Generate a single momentary pulse in milliseconds (up to 100,000 ms):
+
+Request:
+{"jsonrpc": "2.0", "method": "start_pulse", "params": {"relay_id": 3, "duration_ms": 5000}, "id": 4}
+
+
+
+Response:
+{"jsonrpc": "2.0", "result": {"relay_id": 3, "duration_ms": 5000, "success": true}, "id": 4}
+
+
+
+
+
+
+start_blink
+Repeatedly cycle relay state ON and OFF:
+
+Request:
+{"jsonrpc": "2.0", "method": "start_blink", "params": {"relay_id": 4, "on_ms": 1000, "off_ms": 1000, "count": 5}, "id": 5}
+
+
+
+Response:
+{"jsonrpc": "2.0", "result": {"relay_id": 4, "on_ms": 1000, "off_ms": 1000, "count": 5, "success": true}, "id": 5}
+
+
+
+
+
+
+get_relay_status
+Query detailed diagnostic status and remaining time for a single channel:
+
+Request:
+{"jsonrpc": "2.0", "method": "get_relay_status", "params": {"relay_id": 2}, "id": 6}
+
+
+
+Response:
+{"jsonrpc": "2.0", "result": {"relay_id": 2, "status": "Channel 2: ON (Timer, rem: 8s)"}, "id": 6}
+
+
+
+
+
@@ -149,14 +242,23 @@
Compile and Run
-Yocto Image Build (Raspberry Pi Target)
+- minihildesk GUI Client
+
+- Yocto Image Build (Raspberry Pi Target)
- JSON-RPC 2.0 API Specification
- Copyright and licence
diff --git a/docs/build/html/searchindex.js b/docs/build/html/searchindex.js
index bb547d3..3e2c28b 100644
--- a/docs/build/html/searchindex.js
+++ b/docs/build/html/searchindex.js
@@ -1 +1 @@
-Search.setIndex({"docnames": ["index"], "filenames": ["index.rst"], "titles": ["minihil"], "terms": {"i": 0, "develop": 0, "devic": 0, "which": 0, "can": 0, "us": 0, "hwil": 0, "The": 0, "doc": 0, "introduc": 0, "tool": 0, "provid": 0, "instruct": 0, "how": 0, "instal": 0, "core": 0, "posix": 0, "c": 0, "daemon": 0, "server": 0, "It": 0, "listen": 0, "incom": 0, "connect": 0, "tcp": 0, "port": 0, "9000": 0, "pars": 0, "command": 0, "control": 0, "waveshar": 0, "relai": 0, "board": 0, "b": 0, "an": 0, "8": 0, "channel": 0, "via": 0, "standard": 0, "linux": 0, "charact": 0, "gpio": 0, "libgpiod": 0, "v2": 0, "design": 0, "decoupl": 0, "architectur": 0, "allow": 0, "easi": 0, "expans": 0, "e": 0, "g": 0, "ad": 0, "websocket": 0, "support": 0, "simul": 0, "so": 0, "you": 0, "test": 0, "its": 0, "directli": 0, "your": 0, "local": 0, "pc": 0, "without": 0, "hardwar": 0, "To": 0, "mock": 0, "mode": 0, "requir": 0, "header": 0, "librari": 0, "sudo": 0, "apt": 0, "get": 0, "nlohmann": 0, "json3": 0, "dev": 0, "configur": 0, "cmake": 0, "sw": 0, "": 0, "start": 0, "boot": 0, "print": 0, "silrelaycontrol": 0, "initi": 0, "packag": 0, "custom": 0, "meta": 0, "raspberrypi": 0, "poki": 0, "ensur": 0, "ha": 0, "all": 0, "scarthgap": 0, "refer": 0, "rpi": 0, "base": 0, "platform": 0, "readm": 0, "md": 0, "list": 0, "1": 0, "environ": 0, "sourc": 0, "set": 0, "up": 0, "config": 0, "layer": 0, "setup": 0, "env": 0, "sh": 0, "trigger": 0, "bitbak": 0, "thi": 0, "applic": 0, "bundl": 0, "systemd": 0, "servic": 0, "automat": 0, "output": 0, "flashabl": 0, "send": 0, "text": 0, "frame": 0, "termin": 0, "n": 0, "energ": 0, "de": 0, "request": 0, "jsonrpc": 0, "method": 0, "param": 0, "relay_id": 0, "3": 0, "state": 0, "true": 0, "id": 0, "respons": 0, "result": 0, "success": 0, "queri": 0, "fals": 0, "4": 0, "5": 0, "6": 0, "7": 0, "2020": 0, "2026": 0, "electux": 0, "github": 0, "io": 0}, "objects": {}, "objtypes": {}, "objnames": {}, "titleterms": {"minihil": 0, "about": 0, "minihild": 0, "sil": 0, "softwar": 0, "loop": 0, "host": 0, "build": 0, "prerequisit": 0, "compil": 0, "run": 0, "yocto": 0, "imag": 0, "raspberri": 0, "pi": 0, "target": 0, "json": 0, "rpc": 0, "2": 0, "0": 0, "api": 0, "specif": 0, "set_relai": 0, "get_relai": 0, "copyright": 0, "licenc": 0}, "envversion": {"sphinx.domains.c": 3, "sphinx.domains.changeset": 1, "sphinx.domains.citation": 1, "sphinx.domains.cpp": 9, "sphinx.domains.index": 1, "sphinx.domains.javascript": 3, "sphinx.domains.math": 2, "sphinx.domains.python": 4, "sphinx.domains.rst": 2, "sphinx.domains.std": 2, "sphinx": 60}, "alltitles": {"minihil": [[0, "minihil"]], "About minihild": [[0, "about-minihild"]], "SIL (Software-in-the-Loop) Host Build": [[0, "sil-software-in-the-loop-host-build"]], "Prerequisites": [[0, "prerequisites"], [0, "id1"]], "Compile and Run": [[0, "compile-and-run"]], "Yocto Image Build (Raspberry Pi Target)": [[0, "yocto-image-build-raspberry-pi-target"]], "Compile target image": [[0, "compile-target-image"]], "JSON-RPC 2.0 API Specification": [[0, "json-rpc-2-0-api-specification"]], "set_relay": [[0, "set-relay"]], "get_relays": [[0, "get-relays"]], "Copyright and licence": [[0, "copyright-and-licence"]]}, "indexentries": {}})
\ No newline at end of file
+Search.setIndex({"docnames": ["index"], "filenames": ["index.rst"], "titles": ["minihil"], "terms": {"i": 0, "develop": 0, "devic": 0, "which": 0, "can": 0, "us": 0, "hwil": 0, "The": 0, "doc": 0, "introduc": 0, "tool": 0, "provid": 0, "instruct": 0, "how": 0, "instal": 0, "core": 0, "posix": 0, "c": 0, "daemon": 0, "server": 0, "It": 0, "listen": 0, "incom": 0, "connect": 0, "tcp": 0, "port": 0, "9000": 0, "pars": 0, "command": 0, "control": 0, "waveshar": 0, "relai": 0, "board": 0, "b": 0, "an": 0, "8": 0, "channel": 0, "via": 0, "standard": 0, "linux": 0, "charact": 0, "gpio": 0, "libgpiod": 0, "v2": 0, "design": 0, "decoupl": 0, "architectur": 0, "allow": 0, "easi": 0, "expans": 0, "e": 0, "g": 0, "ad": 0, "websocket": 0, "support": 0, "simul": 0, "so": 0, "you": 0, "test": 0, "its": 0, "directli": 0, "your": 0, "local": 0, "pc": 0, "without": 0, "hardwar": 0, "To": 0, "mock": 0, "mode": 0, "requir": 0, "header": 0, "librari": 0, "sudo": 0, "apt": 0, "get": 0, "nlohmann": 0, "json3": 0, "dev": 0, "configur": 0, "cmake": 0, "sw": 0, "": 0, "start": 0, "boot": 0, "print": 0, "silrelaycontrol": 0, "initi": 0, "premium": 0, "gtkmm": 0, "base": 0, "desktop": 0, "either": 0, "featur": 0, "A": 0, "bar": 0, "ssl": 0, "tl": 0, "secur": 0, "mutual": 0, "mtl": 0, "verif": 0, "grid": 0, "card": 0, "displai": 0, "statu": 0, "glow": 0, "led": 0, "indic": 0, "activ": 0, "green": 0, "border": 0, "multi": 0, "per": 0, "toggl": 0, "manual": 0, "switch": 0, "timer": 0, "second": 0, "spin": 0, "input": 0, "puls": 0, "millisecond": 0, "up": 0, "100": 0, "000": 0, "m": 0, "blink": 0, "ON": 0, "off": 0, "cycl": 0, "count": 0, "automat": 0, "safeti": 0, "auto": 0, "logic": 0, "immedi": 0, "turn": 0, "befor": 0, "enter": 0, "new": 0, "monospac": 0, "log": 0, "termin": 0, "bottom": 0, "showcas": 0, "outgo": 0, "traffic": 0, "4": 0, "openssl": 0, "libgtkmm": 0, "libssl": 0, "app": 0, "packag": 0, "custom": 0, "meta": 0, "raspberrypi": 0, "poki": 0, "ensur": 0, "ha": 0, "all": 0, "scarthgap": 0, "refer": 0, "rpi": 0, "platform": 0, "readm": 0, "md": 0, "list": 0, "1": 0, "environ": 0, "sourc": 0, "set": 0, "config": 0, "layer": 0, "setup": 0, "env": 0, "sh": 0, "trigger": 0, "bitbak": 0, "thi": 0, "applic": 0, "bundl": 0, "systemd": 0, "servic": 0, "output": 0, "flashabl": 0, "send": 0, "text": 0, "frame": 0, "n": 0, "energ": 0, "de": 0, "request": 0, "jsonrpc": 0, "method": 0, "param": 0, "relay_id": 0, "3": 0, "state": 0, "true": 0, "id": 0, "respons": 0, "result": 0, "success": 0, "queri": 0, "fals": 0, "5": 0, "6": 0, "7": 0, "keep": 0, "durat": 0, "10": 0, "gener": 0, "singl": 0, "momentari": 0, "duration_m": 0, "5000": 0, "repeatedli": 0, "on_m": 0, "1000": 0, "off_m": 0, "detail": 0, "diagnost": 0, "remain": 0, "time": 0, "rem": 0, "2020": 0, "2026": 0, "electux": 0, "github": 0, "io": 0}, "objects": {}, "objtypes": {}, "objnames": {}, "titleterms": {"minihil": 0, "about": 0, "minihild": 0, "sil": 0, "softwar": 0, "loop": 0, "host": 0, "build": 0, "prerequisit": 0, "compil": 0, "run": 0, "minihildesk": 0, "gui": 0, "client": 0, "yocto": 0, "imag": 0, "raspberri": 0, "pi": 0, "target": 0, "json": 0, "rpc": 0, "2": 0, "0": 0, "api": 0, "specif": 0, "set_relai": 0, "get_relai": 0, "start_tim": 0, "start_puls": 0, "start_blink": 0, "get_relay_statu": 0, "copyright": 0, "licenc": 0}, "envversion": {"sphinx.domains.c": 3, "sphinx.domains.changeset": 1, "sphinx.domains.citation": 1, "sphinx.domains.cpp": 9, "sphinx.domains.index": 1, "sphinx.domains.javascript": 3, "sphinx.domains.math": 2, "sphinx.domains.python": 4, "sphinx.domains.rst": 2, "sphinx.domains.std": 2, "sphinx": 60}, "alltitles": {"minihil": [[0, "minihil"]], "About minihild": [[0, "about-minihild"]], "SIL (Software-in-the-Loop) Host Build": [[0, "sil-software-in-the-loop-host-build"]], "Prerequisites": [[0, "prerequisites"], [0, "id1"], [0, "id3"]], "Compile and Run": [[0, "compile-and-run"], [0, "id2"]], "minihildesk GUI Client": [[0, "minihildesk-gui-client"]], "Yocto Image Build (Raspberry Pi Target)": [[0, "yocto-image-build-raspberry-pi-target"]], "Compile target image": [[0, "compile-target-image"]], "JSON-RPC 2.0 API Specification": [[0, "json-rpc-2-0-api-specification"]], "set_relay": [[0, "set-relay"]], "get_relays": [[0, "get-relays"]], "start_timer": [[0, "start-timer"]], "start_pulse": [[0, "start-pulse"]], "start_blink": [[0, "start-blink"]], "get_relay_status": [[0, "get-relay-status"]], "Copyright and licence": [[0, "copyright-and-licence"]]}, "indexentries": {}})
\ No newline at end of file
diff --git a/docs/foundations.png b/docs/foundations.png
index 1c30bac..84fba3f 100644
Binary files a/docs/foundations.png and b/docs/foundations.png differ
diff --git a/docs/source/index.rst b/docs/source/index.rst
index e434dc8..550f466 100755
--- a/docs/source/index.rst
+++ b/docs/source/index.rst
@@ -55,6 +55,38 @@ Compile and Run
The daemon will boot in SIL mode and print:
``[SilRelayController] Software-in-the-Loop simulation initialized.``
+minihildesk GUI Client
+----------------------
+
+**minihildesk** is a premium GTKmm-based C++ desktop GUI client designed to connect to the ``minihild`` server (either running on a Raspberry Pi target or locally in SIL mock mode).
+
+It features:
+
+* A connection bar supporting standard TCP connection, **SSL/TLS secure connection**, and **Mutual TLS (mTLS) client verification**.
+* A grid of 8 Relay Control Cards displaying status via glowing LED indicators and active green borders.
+* Multi-mode relay control per channel: **Toggle** (manual switch), **Timer** (seconds spin-input), **Pulse** (milliseconds spin-input up to 100,000 ms), and **Blink** (ON/OFF ms inputs and cycle count).
+* Automatic hardware safety auto-off logic: switching modes on an active channel immediately turns the relay OFF on the server before entering the new mode.
+* A monospace green log terminal at the bottom showcasing outgoing and incoming JSON-RPC traffic.
+
+Prerequisites
+~~~~~~~~~~~~~
+Install the GTKmm-4.0 development headers, OpenSSL, and JSON library:
+
+.. code-block:: bash
+
+ sudo apt-get install libgtkmm-4.0-dev libssl-dev nlohmann-json3-dev
+
+Compile and Run
+~~~~~~~~~~~~~~~
+.. code-block:: bash
+
+ # Configure and compile using CMake
+ cmake -B sw/minihildesk/build -S sw/minihildesk
+ cmake --build sw/minihildesk/build
+
+ # Start the desktop GUI app
+ ./sw/minihildesk/build/minihildesk
+
Yocto Image Build (Raspberry Pi Target)
---------------------------------------
@@ -113,6 +145,70 @@ Query states of all 8 relay channels:
{"jsonrpc": "2.0", "result": {"1": false, "2": false, "3": true, "4": false, "5": false, "6": false, "7": false, "8": false}, "id": 2}
+start_timer
+~~~~~~~~~~~
+Keep a relay active for a specific duration in seconds:
+
+* **Request**:
+
+ .. code-block:: json
+
+ {"jsonrpc": "2.0", "method": "start_timer", "params": {"relay_id": 2, "seconds": 10}, "id": 3}
+
+* **Response**:
+
+ .. code-block:: json
+
+ {"jsonrpc": "2.0", "result": {"relay_id": 2, "seconds": 10, "success": true}, "id": 3}
+
+start_pulse
+~~~~~~~~~~~
+Generate a single momentary pulse in milliseconds (up to 100,000 ms):
+
+* **Request**:
+
+ .. code-block:: json
+
+ {"jsonrpc": "2.0", "method": "start_pulse", "params": {"relay_id": 3, "duration_ms": 5000}, "id": 4}
+
+* **Response**:
+
+ .. code-block:: json
+
+ {"jsonrpc": "2.0", "result": {"relay_id": 3, "duration_ms": 5000, "success": true}, "id": 4}
+
+start_blink
+~~~~~~~~~~~
+Repeatedly cycle relay state ON and OFF:
+
+* **Request**:
+
+ .. code-block:: json
+
+ {"jsonrpc": "2.0", "method": "start_blink", "params": {"relay_id": 4, "on_ms": 1000, "off_ms": 1000, "count": 5}, "id": 5}
+
+* **Response**:
+
+ .. code-block:: json
+
+ {"jsonrpc": "2.0", "result": {"relay_id": 4, "on_ms": 1000, "off_ms": 1000, "count": 5, "success": true}, "id": 5}
+
+get_relay_status
+~~~~~~~~~~~~~~~~
+Query detailed diagnostic status and remaining time for a single channel:
+
+* **Request**:
+
+ .. code-block:: json
+
+ {"jsonrpc": "2.0", "method": "get_relay_status", "params": {"relay_id": 2}, "id": 6}
+
+* **Response**:
+
+ .. code-block:: json
+
+ {"jsonrpc": "2.0", "result": {"relay_id": 2, "status": "Channel 2: ON (Timer, rem: 8s)"}, "id": 6}
+
Copyright and licence
----------------------
@@ -127,7 +223,4 @@ Copyright and licence
Copyright (C) 2020 - 2026 by `electux.github.io/minihil `_
.. image:: https://raw.githubusercontent.com/electux/minihil/master/docs/foundations.png
- :alt: Raspberry Pi & GNOME Foundations
- :width: 681px
- :height: 100px
-
+ :alt: Foundations
diff --git a/sw/minihildesk/build/minihildesk b/sw/minihildesk/build/minihildesk
index 8c7dd2c..b846ecd 100755
Binary files a/sw/minihildesk/build/minihildesk and b/sw/minihildesk/build/minihildesk differ
diff --git a/sw/minihildesk/build/resources.cc b/sw/minihildesk/build/resources.cc
index e5a418b..cf11f1c 100644
--- a/sw/minihildesk/build/resources.cc
+++ b/sw/minihildesk/build/resources.cc
@@ -6,53 +6,165 @@
# define SECTION
#endif
-static const SECTION union { const guint8 data[781]; const double alignment; void * const ptr;} resources_resource_data = {
+static const SECTION union { const guint8 data[2573]; const double alignment; void * const ptr;} resources_resource_data = {
"\107\126\141\162\151\141\156\164\000\000\000\000\000\000\000\000"
"\030\000\000\000\254\000\000\000\000\000\000\050\005\000\000\000"
"\000\000\000\000\002\000\000\000\002\000\000\000\003\000\000\000"
"\003\000\000\000\324\265\002\000\377\377\377\377\254\000\000\000"
"\001\000\114\000\260\000\000\000\264\000\000\000\014\240\142\334"
"\004\000\000\000\264\000\000\000\011\000\166\000\300\000\000\000"
- "\346\002\000\000\144\252\051\316\003\000\000\000\346\002\000\000"
- "\010\000\114\000\360\002\000\000\364\002\000\000\173\242\170\174"
- "\000\000\000\000\364\002\000\000\003\000\114\000\370\002\000\000"
- "\374\002\000\000\104\246\353\017\002\000\000\000\374\002\000\000"
- "\014\000\114\000\010\003\000\000\014\003\000\000\057\000\000\000"
+ "\346\011\000\000\144\252\051\316\003\000\000\000\346\011\000\000"
+ "\010\000\114\000\360\011\000\000\364\011\000\000\173\242\170\174"
+ "\000\000\000\000\364\011\000\000\003\000\114\000\370\011\000\000"
+ "\374\011\000\000\104\246\353\017\002\000\000\000\374\011\000\000"
+ "\014\000\114\000\010\012\000\000\014\012\000\000\057\000\000\000"
"\003\000\000\000\163\164\171\154\145\056\143\163\163\000\000\000"
- "\026\002\000\000\000\000\000\000\164\145\170\164\166\151\145\167"
- "\040\164\145\170\164\040\173\012\040\040\040\040\142\141\143\153"
- "\147\162\157\165\156\144\055\143\157\154\157\162\072\040\043\061"
- "\062\061\062\061\064\073\012\040\040\040\040\143\157\154\157\162"
- "\072\040\043\060\060\146\146\066\066\073\012\040\040\040\040\146"
- "\157\156\164\055\146\141\155\151\154\171\072\040\155\157\156\157"
- "\163\160\141\143\145\073\012\040\040\040\040\146\157\156\164\055"
- "\163\151\172\145\072\040\061\063\160\170\073\012\175\012\012\056"
- "\162\145\154\141\171\055\143\141\162\144\040\173\012\040\040\040"
- "\040\142\157\162\144\145\162\072\040\061\160\170\040\163\157\154"
- "\151\144\040\043\144\060\144\060\144\065\073\012\040\040\040\040"
- "\142\157\162\144\145\162\055\162\141\144\151\165\163\072\040\070"
- "\160\170\073\012\040\040\040\040\142\141\143\153\147\162\157\165"
- "\156\144\055\143\157\154\157\162\072\040\043\146\067\146\067\146"
- "\071\073\012\175\012\012\056\162\145\154\141\171\055\143\141\162"
- "\144\040\154\141\142\145\154\040\173\012\040\040\040\040\143\157"
- "\154\157\162\072\040\043\061\062\061\062\061\064\073\012\175\012"
- "\012\056\162\145\154\141\171\055\143\141\162\144\072\144\151\163"
- "\141\142\154\145\144\040\154\141\142\145\154\040\173\012\040\040"
- "\040\040\143\157\154\157\162\072\040\043\067\146\070\143\070\144"
- "\073\012\175\012\012\056\154\145\144\055\151\156\144\151\143\141"
+ "\026\011\000\000\000\000\000\000\057\052\040\115\141\151\156\040"
+ "\167\151\156\144\157\167\040\141\156\144\040\147\145\156\145\162"
+ "\151\143\040\142\157\170\145\163\040\052\057\012\167\151\156\144"
+ "\157\167\040\173\012\040\040\040\040\142\141\143\153\147\162\157"
+ "\165\156\144\055\143\157\154\157\162\072\040\043\061\062\061\062"
+ "\061\064\073\012\040\040\040\040\143\157\154\157\162\072\040\043"
+ "\146\146\146\146\146\146\073\012\040\040\040\040\146\157\156\164"
+ "\055\146\141\155\151\154\171\072\040\042\125\142\165\156\164\165"
+ "\042\054\040\042\123\145\147\157\145\040\125\111\042\054\040\163"
+ "\141\156\163\055\163\145\162\151\146\073\012\175\012\012\142\157"
+ "\170\040\173\012\040\040\040\040\142\141\143\153\147\162\157\165"
+ "\156\144\055\143\157\154\157\162\072\040\043\061\062\061\062\061"
+ "\064\073\012\175\012\012\057\052\040\115\157\156\157\163\160\141"
+ "\143\145\040\164\145\162\155\151\156\141\154\040\154\157\147\163"
+ "\040\141\164\040\142\157\164\164\157\155\040\052\057\012\164\145"
+ "\170\164\166\151\145\167\054\040\164\145\170\164\166\151\145\167"
+ "\040\164\145\170\164\054\040\164\145\170\164\166\151\145\167\056"
+ "\166\151\145\167\054\040\164\145\170\164\166\151\145\167\056\166"
+ "\151\145\167\040\164\145\170\164\040\173\012\040\040\040\040\142"
+ "\141\143\153\147\162\157\165\156\144\055\143\157\154\157\162\072"
+ "\040\043\060\142\060\142\060\143\073\012\040\040\040\040\143\157"
+ "\154\157\162\072\040\043\060\060\146\146\066\066\073\012\040\040"
+ "\040\040\146\157\156\164\055\146\141\155\151\154\171\072\040\042"
+ "\104\145\152\141\126\165\040\123\141\156\163\040\115\157\156\157"
+ "\042\054\040\042\103\157\165\162\151\145\162\040\116\145\167\042"
+ "\054\040\155\157\156\157\163\160\141\143\145\073\012\040\040\040"
+ "\040\146\157\156\164\055\163\151\172\145\072\040\061\063\160\170"
+ "\073\012\040\040\040\040\142\157\162\144\145\162\055\164\157\160"
+ "\072\040\061\160\170\040\163\157\154\151\144\040\043\062\145\062"
+ "\145\063\070\073\012\175\012\012\057\052\040\110\145\141\144\145"
+ "\162\040\142\141\162\040\154\141\171\157\165\164\040\163\164\171"
+ "\154\151\156\147\040\052\057\012\056\143\157\156\156\145\143\164"
+ "\151\157\156\055\142\141\162\040\173\012\040\040\040\040\142\141"
+ "\143\153\147\162\157\165\156\144\055\143\157\154\157\162\072\040"
+ "\043\061\141\061\141\061\146\073\012\040\040\040\040\142\157\162"
+ "\144\145\162\055\142\157\164\164\157\155\072\040\061\160\170\040"
+ "\163\157\154\151\144\040\043\062\144\062\144\063\065\073\012\040"
+ "\040\040\040\160\141\144\144\151\156\147\072\040\061\062\160\170"
+ "\073\012\175\012\012\056\143\157\156\156\145\143\164\151\157\156"
+ "\055\142\141\162\040\154\141\142\145\154\040\173\012\040\040\040"
+ "\040\143\157\154\157\162\072\040\043\146\146\146\146\146\146\073"
+ "\012\040\040\040\040\146\157\156\164\055\167\145\151\147\150\164"
+ "\072\040\142\157\154\144\073\012\175\012\012\057\052\040\103\141"
+ "\162\144\163\040\050\122\145\154\141\171\040\127\151\144\147\145"
+ "\164\163\051\040\052\057\012\056\162\145\154\141\171\055\143\141"
+ "\162\144\040\173\012\040\040\040\040\142\157\162\144\145\162\072"
+ "\040\062\160\170\040\163\157\154\151\144\040\043\062\141\062\141"
+ "\063\062\073\012\040\040\040\040\142\157\162\144\145\162\055\162"
+ "\141\144\151\165\163\072\040\061\062\160\170\073\012\040\040\040"
+ "\040\142\141\143\153\147\162\157\165\156\144\055\143\157\154\157"
+ "\162\072\040\043\061\141\061\141\061\146\073\012\040\040\040\040"
+ "\164\162\141\156\163\151\164\151\157\156\072\040\141\154\154\040"
+ "\060\056\063\163\040\145\141\163\145\073\012\175\012\012\057\052"
+ "\040\107\154\157\167\151\156\147\040\141\143\164\151\166\145\040"
+ "\142\157\162\144\145\162\040\167\150\145\156\040\162\145\154\141"
+ "\171\040\151\163\040\117\116\040\052\057\012\056\162\145\154\141"
+ "\171\055\143\141\162\144\056\141\143\164\151\166\145\055\162\145"
+ "\154\141\171\040\173\012\040\040\040\040\142\157\162\144\145\162"
+ "\055\143\157\154\157\162\072\040\043\062\145\143\143\067\061\073"
+ "\012\175\012\012\056\162\145\154\141\171\055\143\141\162\144\040"
+ "\154\141\142\145\154\040\173\012\040\040\040\040\143\157\154\157"
+ "\162\072\040\043\146\146\146\146\146\146\073\012\040\040\040\040"
+ "\146\157\156\164\055\167\145\151\147\150\164\072\040\156\157\162"
+ "\155\141\154\073\012\175\012\012\057\052\040\123\165\142\055\154"
+ "\141\142\145\154\163\040\050\145\056\147\056\040\144\145\163\143"
+ "\162\151\160\164\151\157\156\163\054\040\151\156\160\165\164\040"
+ "\154\141\142\145\154\163\051\040\052\057\012\056\162\145\154\141"
+ "\171\055\143\141\162\144\040\154\141\142\145\154\056\144\145\163"
+ "\143\055\164\145\170\164\040\173\012\040\040\040\040\143\157\154"
+ "\157\162\072\040\043\070\143\070\143\071\145\073\012\175\012\012"
+ "\057\052\040\114\105\104\040\111\156\144\151\143\141\164\157\162"
+ "\163\040\052\057\012\056\154\145\144\055\151\156\144\151\143\141"
"\164\157\162\040\173\012\040\040\040\040\146\157\156\164\055\163"
- "\151\172\145\072\040\062\064\160\170\073\012\175\012\012\056\162"
+ "\151\172\145\072\040\062\060\160\170\073\012\175\012\012\056\162"
"\145\154\141\171\055\143\141\162\144\040\154\141\142\145\154\056"
"\154\145\144\055\157\156\040\173\012\040\040\040\040\143\157\154"
"\157\162\072\040\043\062\145\143\143\067\061\073\012\040\040\040"
"\040\164\145\170\164\055\163\150\141\144\157\167\072\040\060\040"
- "\060\040\061\060\160\170\040\043\062\145\143\143\067\061\054\040"
- "\060\040\060\040\062\060\160\170\040\043\062\145\143\143\067\061"
- "\073\012\175\012\012\056\162\145\154\141\171\055\143\141\162\144"
- "\040\154\141\142\145\154\056\154\145\144\055\157\146\146\040\173"
- "\012\040\040\040\040\143\157\154\157\162\072\040\043\142\144\143"
- "\063\143\067\073\012\040\040\040\040\164\145\170\164\055\163\150"
- "\141\144\157\167\072\040\156\157\156\145\073\012\175\012\000\000"
+ "\060\040\070\160\170\040\043\062\145\143\143\067\061\054\040\060"
+ "\040\060\040\061\066\160\170\040\043\062\145\143\143\067\061\073"
+ "\012\175\012\012\056\162\145\154\141\171\055\143\141\162\144\040"
+ "\154\141\142\145\154\056\154\145\144\055\157\146\146\040\173\012"
+ "\040\040\040\040\143\157\154\157\162\072\040\043\145\067\064\143"
+ "\063\143\073\012\040\040\040\040\164\145\170\164\055\163\150\141"
+ "\144\157\167\072\040\060\040\060\040\070\160\170\040\043\145\067"
+ "\064\143\063\143\073\012\175\012\012\057\052\040\107\124\113\040"
+ "\111\156\160\165\164\040\163\164\171\154\151\156\147\040\052\057"
+ "\012\145\156\164\162\171\054\040\163\160\151\156\142\165\164\164"
+ "\157\156\054\040\143\157\155\142\157\142\157\170\040\173\012\040"
+ "\040\040\040\142\141\143\153\147\162\157\165\156\144\055\143\157"
+ "\154\157\162\072\040\043\062\065\062\065\062\142\073\012\040\040"
+ "\040\040\142\157\162\144\145\162\072\040\061\160\170\040\163\157"
+ "\154\151\144\040\043\063\145\063\145\064\141\073\012\040\040\040"
+ "\040\142\157\162\144\145\162\055\162\141\144\151\165\163\072\040"
+ "\066\160\170\073\012\040\040\040\040\143\157\154\157\162\072\040"
+ "\043\146\146\146\146\146\146\073\012\040\040\040\040\160\141\144"
+ "\144\151\156\147\072\040\066\160\170\073\012\175\012\012\145\156"
+ "\164\162\171\072\146\157\143\165\163\054\040\163\160\151\156\142"
+ "\165\164\164\157\156\072\146\157\143\165\163\054\040\143\157\155"
+ "\142\157\142\157\170\072\146\157\143\165\163\040\173\012\040\040"
+ "\040\040\142\157\162\144\145\162\055\143\157\154\157\162\072\040"
+ "\043\063\064\071\070\144\142\073\012\175\012\012\057\052\040\102"
+ "\165\164\164\157\156\040\163\164\171\154\151\156\147\163\040\052"
+ "\057\012\142\165\164\164\157\156\040\173\012\040\040\040\040\142"
+ "\141\143\153\147\162\157\165\156\144\055\143\157\154\157\162\072"
+ "\040\043\063\064\064\071\065\145\073\012\040\040\040\040\142\157"
+ "\162\144\145\162\072\040\061\160\170\040\163\157\154\151\144\040"
+ "\043\062\143\063\145\065\060\073\012\040\040\040\040\142\157\162"
+ "\144\145\162\055\162\141\144\151\165\163\072\040\066\160\170\073"
+ "\012\040\040\040\040\143\157\154\157\162\072\040\043\146\146\146"
+ "\146\146\146\073\012\040\040\040\040\146\157\156\164\055\167\145"
+ "\151\147\150\164\072\040\142\157\154\144\073\012\040\040\040\040"
+ "\160\141\144\144\151\156\147\072\040\066\160\170\040\061\066\160"
+ "\170\073\012\175\012\012\142\165\164\164\157\156\072\150\157\166"
+ "\145\162\040\173\012\040\040\040\040\142\141\143\153\147\162\157"
+ "\165\156\144\055\143\157\154\157\162\072\040\043\064\061\065\142"
+ "\067\066\073\012\175\012\012\142\165\164\164\157\156\072\141\143"
+ "\164\151\166\145\040\173\012\040\040\040\040\142\141\143\153\147"
+ "\162\157\165\156\144\055\143\157\154\157\162\072\040\043\062\143"
+ "\063\145\065\060\073\012\175\012\012\057\052\040\107\162\145\145"
+ "\156\040\143\157\156\156\145\143\164\057\144\151\163\143\157\156"
+ "\156\145\143\164\040\142\165\164\164\157\156\040\052\057\012\056"
+ "\143\157\156\156\145\143\164\055\142\164\156\040\173\012\040\040"
+ "\040\040\142\141\143\153\147\162\157\165\156\144\055\143\157\154"
+ "\157\162\072\040\043\062\145\143\143\067\061\073\012\040\040\040"
+ "\040\142\157\162\144\145\162\055\143\157\154\157\162\072\040\043"
+ "\062\067\141\145\066\060\073\012\040\040\040\040\143\157\154\157"
+ "\162\072\040\043\146\146\146\146\146\146\073\012\175\012\012\056"
+ "\143\157\156\156\145\143\164\055\142\164\156\072\150\157\166\145"
+ "\162\040\173\012\040\040\040\040\142\141\143\153\147\162\157\165"
+ "\156\144\055\143\157\154\157\162\072\040\043\062\067\141\145\066"
+ "\060\073\012\175\012\012\056\143\157\156\156\145\143\164\055\142"
+ "\164\156\072\141\143\164\151\166\145\040\173\012\040\040\040\040"
+ "\142\141\143\153\147\162\157\165\156\144\055\143\157\154\157\162"
+ "\072\040\043\062\061\071\066\065\063\073\012\175\012\012\057\052"
+ "\040\123\167\151\164\143\150\040\103\165\163\164\157\155\040\154"
+ "\141\171\157\165\164\040\052\057\012\163\167\151\164\143\150\040"
+ "\173\012\040\040\040\040\142\141\143\153\147\162\157\165\156\144"
+ "\055\143\157\154\157\162\072\040\043\063\145\063\145\064\141\073"
+ "\012\040\040\040\040\142\157\162\144\145\162\072\040\061\160\170"
+ "\040\163\157\154\151\144\040\043\062\144\062\144\063\065\073\012"
+ "\040\040\040\040\142\157\162\144\145\162\055\162\141\144\151\165"
+ "\163\072\040\061\064\160\170\073\012\175\012\012\163\167\151\164"
+ "\143\150\072\143\150\145\143\153\145\144\040\173\012\040\040\040"
+ "\040\142\141\143\153\147\162\157\165\156\144\055\143\157\154\157"
+ "\162\072\040\043\062\145\143\143\067\061\073\012\175\012\000\000"
"\050\165\165\141\171\051\145\154\145\143\164\165\170\057\000\000"
"\004\000\000\000\151\157\057\000\002\000\000\000\155\151\156\151"
"\150\151\154\144\145\163\153\057\001\000\000\000" };
diff --git a/sw/minihildesk/resources/style.css b/sw/minihildesk/resources/style.css
index 255e990..0efbc2b 100644
--- a/sw/minihildesk/resources/style.css
+++ b/sw/minihildesk/resources/style.css
@@ -1,34 +1,126 @@
-textview text {
+/* Main window and generic boxes */
+window {
background-color: #121214;
+ color: #ffffff;
+ font-family: "Ubuntu", "Segoe UI", sans-serif;
+}
+
+box {
+ background-color: #121214;
+}
+
+/* Monospace terminal logs at bottom */
+textview, textview text, textview.view, textview.view text {
+ background-color: #0b0b0c;
color: #00ff66;
- font-family: monospace;
+ font-family: "DejaVu Sans Mono", "Courier New", monospace;
font-size: 13px;
+ border-top: 1px solid #2e2e38;
+}
+
+/* Header bar layout styling */
+.connection-bar {
+ background-color: #1a1a1f;
+ border-bottom: 1px solid #2d2d35;
+ padding: 12px;
+}
+
+.connection-bar label {
+ color: #ffffff;
+ font-weight: bold;
}
+/* Cards (Relay Widgets) */
.relay-card {
- border: 1px solid #d0d0d5;
- border-radius: 8px;
- background-color: #f7f7f9;
+ border: 2px solid #2a2a32;
+ border-radius: 12px;
+ background-color: #1a1a1f;
+ transition: all 0.3s ease;
+}
+
+/* Glowing active border when relay is ON */
+.relay-card.active-relay {
+ border-color: #2ecc71;
}
.relay-card label {
- color: #121214;
+ color: #ffffff;
+ font-weight: normal;
}
-.relay-card:disabled label {
- color: #7f8c8d;
+/* Sub-labels (e.g. descriptions, input labels) */
+.relay-card label.desc-text {
+ color: #8c8c9e;
}
+/* LED Indicators */
.led-indicator {
- font-size: 24px;
+ font-size: 20px;
}
.relay-card label.led-on {
color: #2ecc71;
- text-shadow: 0 0 10px #2ecc71, 0 0 20px #2ecc71;
+ text-shadow: 0 0 8px #2ecc71, 0 0 16px #2ecc71;
}
.relay-card label.led-off {
- color: #bdc3c7;
- text-shadow: none;
+ color: #e74c3c;
+ text-shadow: 0 0 8px #e74c3c;
+}
+
+/* GTK Input styling */
+entry, spinbutton, combobox {
+ background-color: #25252b;
+ border: 1px solid #3e3e4a;
+ border-radius: 6px;
+ color: #ffffff;
+ padding: 6px;
+}
+
+entry:focus, spinbutton:focus, combobox:focus {
+ border-color: #3498db;
+}
+
+/* Button stylings */
+button {
+ background-color: #34495e;
+ border: 1px solid #2c3e50;
+ border-radius: 6px;
+ color: #ffffff;
+ font-weight: bold;
+ padding: 6px 16px;
+}
+
+button:hover {
+ background-color: #415b76;
+}
+
+button:active {
+ background-color: #2c3e50;
+}
+
+/* Green connect/disconnect button */
+.connect-btn {
+ background-color: #2ecc71;
+ border-color: #27ae60;
+ color: #ffffff;
+}
+
+.connect-btn:hover {
+ background-color: #27ae60;
+}
+
+.connect-btn:active {
+ background-color: #219653;
+}
+
+/* Switch Custom layout */
+switch {
+ background-color: #3e3e4a;
+ border: 1px solid #2d2d35;
+ border-radius: 14px;
+}
+
+switch:checked {
+ background-color: #2ecc71;
}
diff --git a/sw/minihildesk/src/app_controller.cc b/sw/minihildesk/src/app_controller.cc
index b3228fe..1e54ed0 100644
--- a/sw/minihildesk/src/app_controller.cc
+++ b/sw/minihildesk/src/app_controller.cc
@@ -55,6 +55,15 @@ constexpr std::string_view cResultKey{"result"};
// RPC Methods
constexpr std::string_view cMethodSetRelay{"set_relay"};
constexpr std::string_view cMethodGetRelays{"get_relays"};
+constexpr std::string_view cMethodStartTimer{"start_timer"};
+constexpr std::string_view cMethodStartPulse{"start_pulse"};
+constexpr std::string_view cMethodStartBlink{"start_blink"};
+
+constexpr std::string_view cSecondsKey{"seconds"};
+constexpr std::string_view cDurationMsKey{"duration_ms"};
+constexpr std::string_view cOnMsKey{"on_ms"};
+constexpr std::string_view cOffMsKey{"off_ms"};
+constexpr std::string_view cCountKey{"count"};
// Prefixes
constexpr std::string_view cTxPrefix{"[TX] "};
@@ -149,6 +158,30 @@ void AppController::toggleRelay(int relayId, bool state) {
sendJsonRpc(cMethodSetRelay.data(), params);
}
+void AppController::startTimer(int relayId, uint32_t seconds) {
+ nlohmann::json params;
+ params[cRelayIdKey.data()] = relayId;
+ params[cSecondsKey.data()] = seconds;
+ sendJsonRpc(cMethodStartTimer.data(), params);
+}
+
+void AppController::startPulse(int relayId, uint32_t durationMs) {
+ nlohmann::json params;
+ params[cRelayIdKey.data()] = relayId;
+ params[cDurationMsKey.data()] = durationMs;
+ sendJsonRpc(cMethodStartPulse.data(), params);
+}
+
+void AppController::startBlink(int relayId, uint32_t onMs, uint32_t offMs,
+ uint32_t count) {
+ nlohmann::json params;
+ params[cRelayIdKey.data()] = relayId;
+ params[cOnMsKey.data()] = onMs;
+ params[cOffMsKey.data()] = offMs;
+ params[cCountKey.data()] = count;
+ sendJsonRpc(cMethodStartBlink.data(), params);
+}
+
void AppController::queryAllRelays() { sendJsonRpc(cMethodGetRelays.data()); }
void AppController::sendJsonRpc(const std::string &method,
diff --git a/sw/minihildesk/src/app_controller.h b/sw/minihildesk/src/app_controller.h
index 1478315..3e19f34 100644
--- a/sw/minihildesk/src/app_controller.h
+++ b/sw/minihildesk/src/app_controller.h
@@ -51,6 +51,10 @@ class AppController : public IAppController {
bool isConnected() const override;
void toggleRelay(int relayId, bool state) override;
+ void startTimer(int relayId, uint32_t seconds) override;
+ void startPulse(int relayId, uint32_t durationMs) override;
+ void startBlink(int relayId, uint32_t onMs, uint32_t offMs,
+ uint32_t count) override;
void queryAllRelays() override;
sigc::signal &signal_log() override {
diff --git a/sw/minihildesk/src/iapp_controller.h b/sw/minihildesk/src/iapp_controller.h
index 61edaa7..c4cc97f 100644
--- a/sw/minihildesk/src/iapp_controller.h
+++ b/sw/minihildesk/src/iapp_controller.h
@@ -45,6 +45,10 @@ class IAppController {
virtual bool isConnected() const = 0;
virtual void toggleRelay(int relayId, bool state) = 0;
+ virtual void startTimer(int relayId, uint32_t seconds) = 0;
+ virtual void startPulse(int relayId, uint32_t durationMs) = 0;
+ virtual void startBlink(int relayId, uint32_t onMs, uint32_t offMs,
+ uint32_t count) = 0;
virtual void queryAllRelays() = 0;
virtual sigc::signal &signal_log() = 0;
diff --git a/sw/minihildesk/src/view/home.cc b/sw/minihildesk/src/view/home.cc
index 4efd32b..66ba809 100644
--- a/sw/minihildesk/src/view/home.cc
+++ b/sw/minihildesk/src/view/home.cc
@@ -28,11 +28,12 @@
#include
#include
#include
+#include
namespace {
constexpr std::string_view cTitle{"minihildesk"};
-constexpr int cDefaultWidth{720};
-constexpr int cDefaultHeight{560};
+constexpr int cDefaultWidth{1080};
+constexpr int cDefaultHeight{820};
constexpr std::string_view cStyleResourcePath{
"/io/electux/minihildesk/style.css"};
@@ -83,7 +84,7 @@ AppHome::AppHome(IAppController &controller)
cssProvider->load_from_resource(cStyleResourcePath.data());
Gtk::StyleContext::add_provider_for_display(
Gdk::Display::get_default(), cssProvider,
- GTK_STYLE_PROVIDER_PRIORITY_APPLICATION);
+ GTK_STYLE_PROVIDER_PRIORITY_USER);
// Header Setup
m_ipEntry.set_text(m_controller.getConfig().getIp());
@@ -109,6 +110,9 @@ AppHome::AppHome(IAppController &controller)
});
m_headerBox.set_margin(cHeaderMargin);
+ m_headerBox.get_style_context()->add_class("connection-bar");
+ m_connectBtn.get_style_context()->add_class("connect-btn");
+
m_headerBox.append(m_ipLabel);
m_headerBox.append(m_ipEntry);
m_headerBox.append(m_portLabel);
@@ -127,6 +131,12 @@ AppHome::AppHome(IAppController &controller)
auto widget = std::make_unique(relayId);
widget->signal_toggled().connect(
sigc::mem_fun(*this, &AppHome::onRelayToggled));
+ widget->signal_timer_started().connect(
+ sigc::mem_fun(*this, &AppHome::onRelayTimerStarted));
+ widget->signal_pulse_triggered().connect(
+ sigc::mem_fun(*this, &AppHome::onRelayPulseTriggered));
+ widget->signal_blink_started().connect(
+ sigc::mem_fun(*this, &AppHome::onRelayBlinkStarted));
int row = i / cRelayCols;
int col = i % cRelayCols;
@@ -195,9 +205,26 @@ void AppHome::onConnectClicked() {
}
void AppHome::onRelayToggled(int relayId, bool state) {
+ std::cout << "[AppHome] onRelayToggled for relay " << relayId << ", state: " << state << std::endl;
m_controller.toggleRelay(relayId, state);
}
+void AppHome::onRelayTimerStarted(int relayId, uint32_t seconds) {
+ std::cout << "[AppHome] onRelayTimerStarted for relay " << relayId << ", seconds: " << seconds << std::endl;
+ m_controller.startTimer(relayId, seconds);
+}
+
+void AppHome::onRelayPulseTriggered(int relayId, uint32_t durationMs) {
+ std::cout << "[AppHome] onRelayPulseTriggered for relay " << relayId << ", durationMs: " << durationMs << std::endl;
+ m_controller.startPulse(relayId, durationMs);
+}
+
+void AppHome::onRelayBlinkStarted(int relayId, uint32_t onMs, uint32_t offMs,
+ uint32_t count) {
+ std::cout << "[AppHome] onRelayBlinkStarted for relay " << relayId << ", onMs: " << onMs << ", offMs: " << offMs << ", count: " << count << std::endl;
+ m_controller.startBlink(relayId, onMs, offMs, count);
+}
+
void AppHome::postLogMessage(const std::string &msg) {
std::lock_guard lock(m_logMutex);
diff --git a/sw/minihildesk/src/view/home.h b/sw/minihildesk/src/view/home.h
index 06b6b3b..d89e5ea 100644
--- a/sw/minihildesk/src/view/home.h
+++ b/sw/minihildesk/src/view/home.h
@@ -55,6 +55,10 @@ class AppHome : public Gtk::ApplicationWindow, public IHomeView {
private:
void onConnectClicked();
void onRelayToggled(int relayId, bool state);
+ void onRelayTimerStarted(int relayId, uint32_t seconds);
+ void onRelayPulseTriggered(int relayId, uint32_t durationMs);
+ void onRelayBlinkStarted(int relayId, uint32_t onMs, uint32_t offMs,
+ uint32_t count);
// Signal handlers from controller
void onRelayStateUpdated(int relayId, bool state);
diff --git a/sw/minihildesk/src/view/pages/mode_page_blink.cc b/sw/minihildesk/src/view/pages/mode_page_blink.cc
new file mode 100644
index 0000000..96c9dd0
--- /dev/null
+++ b/sw/minihildesk/src/view/pages/mode_page_blink.cc
@@ -0,0 +1,99 @@
+////////////////////////////////////////////////////////////////////////////////////////////////////////
+///
+/// mode_page_blink.cc
+/// Copyright (C) 2025 - 2026 Vladimir Roncevic
+///
+/// minihildesk is free software: you can redistribute it and/or modify it
+/// under the terms of the GNU General Public License as published by the
+/// Free Software Foundation, either version 3 of the License, or
+/// (at your option) any later version.
+///
+/// minihildesk is distributed in the hope that it will be useful, but
+/// WITHOUT ANY WARRANTY; without even the implied warranty of
+/// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
+/// See the GNU General Public License for more details.
+///
+/// You should have received a copy of the GNU General Public License along
+/// with this program. If not, see .
+///
+////////////////////////////////////////////////////////////////////////////////////////////////////////
+#include
+#include
+
+namespace {
+constexpr std::string_view cBlinkDesc{"Cycle relay state ON and OFF."};
+constexpr std::string_view cOnLabel{"ON (ms):"};
+constexpr std::string_view cOffLabel{"OFF (ms):"};
+constexpr std::string_view cCountLabel{"Count:"};
+constexpr std::string_view cBtnStart{"Start Blink"};
+constexpr int cBoxSpacing{6};
+constexpr int cGridSpacing{4};
+constexpr double cMinTime{10.0};
+constexpr double cMaxTime{100000.0};
+constexpr double cStepTime{50.0};
+constexpr double cPageTime{250.0};
+constexpr double cMinCount{0.0}; // 0 = infinite
+constexpr double cMaxCount{1000.0};
+constexpr double cStepCount{1.0};
+constexpr double cPageCount{10.0};
+} // namespace
+
+namespace minihildesk::View {
+
+BlinkModePage::BlinkModePage(int relayId)
+ : Gtk::Box(Gtk::Orientation::VERTICAL, cBoxSpacing), m_relayId(relayId) {
+
+ m_descLabel.set_text(cBlinkDesc.data());
+ m_descLabel.set_wrap(true);
+ m_descLabel.set_justify(Gtk::Justification::CENTER);
+ m_descLabel.set_vexpand(true);
+ m_descLabel.set_valign(Gtk::Align::END);
+ append(m_descLabel);
+
+ m_onLabel.set_text(cOnLabel.data());
+ m_onSpin.set_range(cMinTime, cMaxTime);
+ m_onSpin.set_increments(cStepTime, cPageTime);
+ m_onSpin.set_value(500.0);
+ m_onSpin.set_width_chars(6);
+
+ m_offLabel.set_text(cOffLabel.data());
+ m_offSpin.set_range(cMinTime, cMaxTime);
+ m_offSpin.set_increments(cStepTime, cPageTime);
+ m_offSpin.set_value(500.0);
+ m_offSpin.set_width_chars(6);
+
+ m_countLabel.set_text(cCountLabel.data());
+ m_countSpin.set_range(cMinCount, cMaxCount);
+ m_countSpin.set_increments(cStepCount, cPageCount);
+ m_countSpin.set_value(5.0); // default 5 blinks
+
+ m_grid.set_row_spacing(cGridSpacing);
+ m_grid.set_column_spacing(cGridSpacing);
+ m_grid.set_halign(Gtk::Align::CENTER);
+
+ m_grid.attach(m_onLabel, 0, 0, 1, 1);
+ m_grid.attach(m_onSpin, 1, 0, 1, 1);
+ m_grid.attach(m_offLabel, 0, 1, 1, 1);
+ m_grid.attach(m_offSpin, 1, 1, 1, 1);
+ m_grid.attach(m_countLabel, 0, 2, 1, 1);
+ m_grid.attach(m_countSpin, 1, 2, 1, 1);
+ append(m_grid);
+
+ m_startButton.set_label(cBtnStart.data());
+ m_startButton.set_halign(Gtk::Align::CENTER);
+ m_startButton.set_margin_bottom(10);
+ m_startButton.signal_clicked().connect(
+ sigc::mem_fun(*this, &BlinkModePage::onStartClicked));
+ append(m_startButton);
+}
+
+void BlinkModePage::onStartClicked() {
+ uint32_t onMs = static_cast(m_onSpin.get_value());
+ uint32_t offMs = static_cast(m_offSpin.get_value());
+ uint32_t count = static_cast(m_countSpin.get_value());
+ std::cout << "[BlinkModePage] Start Blink clicked, onMs: " << onMs
+ << ", offMs: " << offMs << ", count: " << count << std::endl;
+ m_signalStarted.emit(onMs, offMs, count);
+}
+
+} // namespace minihildesk::View
diff --git a/sw/minihildesk/src/view/pages/mode_page_blink.h b/sw/minihildesk/src/view/pages/mode_page_blink.h
new file mode 100644
index 0000000..78cf9ec
--- /dev/null
+++ b/sw/minihildesk/src/view/pages/mode_page_blink.h
@@ -0,0 +1,61 @@
+////////////////////////////////////////////////////////////////////////////////////////////////////////
+///
+/// mode_page_blink.h
+/// Copyright (C) 2025 - 2026 Vladimir Roncevic
+///
+/// minihildesk is free software: you can redistribute it and/or modify it
+/// under the terms of the GNU General Public License as published by the
+/// Free Software Foundation, either version 3 of the License, or
+/// (at your option) any later version.
+///
+/// minihildesk is distributed in the hope that it will be useful, but
+/// WITHOUT ANY WARRANTY; without even the implied warranty of
+/// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
+/// See the GNU General Public License for more details.
+///
+/// You should have received a copy of the GNU General Public License along
+/// with this program. If not, see .
+///
+////////////////////////////////////////////////////////////////////////////////////////////////////////
+#pragma once
+
+#include
+#include
+#include
+#include
+#include
+#include
+
+namespace minihildesk::View {
+
+class BlinkModePage : public Gtk::Box {
+public:
+ explicit BlinkModePage(int relayId);
+ ~BlinkModePage() override = default;
+
+ sigc::signal &signal_started() {
+ return m_signalStarted;
+ }
+
+private:
+ void onStartClicked();
+
+ int m_relayId;
+ Gtk::Label m_descLabel;
+ Gtk::Grid m_grid;
+
+ Gtk::Label m_onLabel;
+ Gtk::SpinButton m_onSpin;
+
+ Gtk::Label m_offLabel;
+ Gtk::SpinButton m_offSpin;
+
+ Gtk::Label m_countLabel;
+ Gtk::SpinButton m_countSpin;
+
+ Gtk::Button m_startButton;
+
+ sigc::signal m_signalStarted;
+};
+
+} // namespace minihildesk::View
diff --git a/sw/minihildesk/src/view/pages/mode_page_pulse.cc b/sw/minihildesk/src/view/pages/mode_page_pulse.cc
new file mode 100644
index 0000000..db24686
--- /dev/null
+++ b/sw/minihildesk/src/view/pages/mode_page_pulse.cc
@@ -0,0 +1,73 @@
+////////////////////////////////////////////////////////////////////////////////////////////////////////
+///
+/// mode_page_pulse.cc
+/// Copyright (C) 2025 - 2026 Vladimir Roncevic
+///
+/// minihildesk is free software: you can redistribute it and/or modify it
+/// under the terms of the GNU General Public License as published by the
+/// Free Software Foundation, either version 3 of the License, or
+/// (at your option) any later version.
+///
+/// minihildesk is distributed in the hope that it will be useful, but
+/// WITHOUT ANY WARRANTY; without even the implied warranty of
+/// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
+/// See the GNU General Public License for more details.
+///
+/// You should have received a copy of the GNU General Public License along
+/// with this program. If not, see .
+///
+////////////////////////////////////////////////////////////////////////////////////////////////////////
+#include
+#include
+
+namespace {
+constexpr std::string_view cPulseDesc{"Generate a momentary active pulse."};
+constexpr std::string_view cPulseLabel{"Width (ms):"};
+constexpr std::string_view cBtnTrigger{"Trigger"};
+constexpr int cBoxSpacing{8};
+constexpr int cInputSpacing{5};
+constexpr double cMinMs{10.0};
+constexpr double cPulseTimeMax{100000.0};
+constexpr double cStepMs{50.0};
+constexpr double cPageMs{250.0};
+} // namespace
+
+namespace minihildesk::View {
+
+PulseModePage::PulseModePage(int relayId)
+ : Gtk::Box(Gtk::Orientation::VERTICAL, cBoxSpacing), m_relayId(relayId),
+ m_inputBox(Gtk::Orientation::HORIZONTAL, cInputSpacing) {
+
+ m_descLabel.set_text(cPulseDesc.data());
+ m_descLabel.set_wrap(true);
+ m_descLabel.set_justify(Gtk::Justification::CENTER);
+ m_descLabel.set_vexpand(true);
+ m_descLabel.set_valign(Gtk::Align::END);
+ append(m_descLabel);
+
+ m_pulseLabel.set_text(cPulseLabel.data());
+ m_pulseSpin.set_range(cMinMs, cPulseTimeMax);
+ m_pulseSpin.set_increments(cStepMs, cPageMs);
+ m_pulseSpin.set_width_chars(6);
+ m_pulseSpin.set_value(500.0); // default 500 ms
+
+ m_inputBox.set_halign(Gtk::Align::CENTER);
+ m_inputBox.append(m_pulseLabel);
+ m_inputBox.append(m_pulseSpin);
+ append(m_inputBox);
+
+ m_triggerButton.set_label(cBtnTrigger.data());
+ m_triggerButton.set_halign(Gtk::Align::CENTER);
+ m_triggerButton.set_margin_bottom(10);
+ m_triggerButton.signal_clicked().connect(
+ sigc::mem_fun(*this, &PulseModePage::onTriggerClicked));
+ append(m_triggerButton);
+}
+
+void PulseModePage::onTriggerClicked() {
+ uint32_t ms = static_cast(m_pulseSpin.get_value());
+ std::cout << "[PulseModePage] Trigger clicked, ms: " << ms << std::endl;
+ m_signalTriggered.emit(ms);
+}
+
+} // namespace minihildesk::View
diff --git a/sw/minihildesk/src/view/pages/mode_page_pulse.h b/sw/minihildesk/src/view/pages/mode_page_pulse.h
new file mode 100644
index 0000000..bdd63e7
--- /dev/null
+++ b/sw/minihildesk/src/view/pages/mode_page_pulse.h
@@ -0,0 +1,50 @@
+////////////////////////////////////////////////////////////////////////////////////////////////////////
+///
+/// mode_page_pulse.h
+/// Copyright (C) 2025 - 2026 Vladimir Roncevic
+///
+/// minihildesk is free software: you can redistribute it and/or modify it
+/// under the terms of the GNU General Public License as published by the
+/// Free Software Foundation, either version 3 of the License, or
+/// (at your option) any later version.
+///
+/// minihildesk is distributed in the hope that it will be useful, but
+/// WITHOUT ANY WARRANTY; without even the implied warranty of
+/// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
+/// See the GNU General Public License for more details.
+///
+/// You should have received a copy of the GNU General Public License along
+/// with this program. If not, see .
+///
+////////////////////////////////////////////////////////////////////////////////////////////////////////
+#pragma once
+
+#include
+#include
+#include
+#include
+#include
+
+namespace minihildesk::View {
+
+class PulseModePage : public Gtk::Box {
+public:
+ explicit PulseModePage(int relayId);
+ ~PulseModePage() override = default;
+
+ sigc::signal &signal_triggered() { return m_signalTriggered; }
+
+private:
+ void onTriggerClicked();
+
+ int m_relayId;
+ Gtk::Label m_descLabel;
+ Gtk::Box m_inputBox;
+ Gtk::Label m_pulseLabel;
+ Gtk::SpinButton m_pulseSpin;
+ Gtk::Button m_triggerButton;
+
+ sigc::signal m_signalTriggered;
+};
+
+} // namespace minihildesk::View
diff --git a/sw/minihildesk/src/view/pages/mode_page_timer.cc b/sw/minihildesk/src/view/pages/mode_page_timer.cc
new file mode 100644
index 0000000..708ec06
--- /dev/null
+++ b/sw/minihildesk/src/view/pages/mode_page_timer.cc
@@ -0,0 +1,72 @@
+////////////////////////////////////////////////////////////////////////////////////////////////////////
+///
+/// mode_page_timer.cc
+/// Copyright (C) 2025 - 2026 Vladimir Roncevic
+///
+/// minihildesk is free software: you can redistribute it and/or modify it
+/// under the terms of the GNU General Public License as published by the
+/// Free Software Foundation, either version 3 of the License, or
+/// (at your option) any later version.
+///
+/// minihildesk is distributed in the hope that it will be useful, but
+/// WITHOUT ANY WARRANTY; without even the implied warranty of
+/// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
+/// See the GNU General Public License for more details.
+///
+/// You should have received a copy of the GNU General Public License along
+/// with this program. If not, see .
+///
+////////////////////////////////////////////////////////////////////////////////////////////////////////
+#include
+#include
+
+namespace {
+constexpr std::string_view cTimerDesc{"Keep relay active for a duration."};
+constexpr std::string_view cDurationLabel{"Duration (s):"};
+constexpr std::string_view cBtnStart{"Start"};
+constexpr int cBoxSpacing{8};
+constexpr int cInputSpacing{5};
+constexpr double cMinSeconds{1.0};
+constexpr double cMaxSeconds{3600.0};
+constexpr double cStepSeconds{1.0};
+constexpr double cPageSeconds{10.0};
+} // namespace
+
+namespace minihildesk::View {
+
+TimerModePage::TimerModePage(int relayId)
+ : Gtk::Box(Gtk::Orientation::VERTICAL, cBoxSpacing), m_relayId(relayId),
+ m_inputBox(Gtk::Orientation::HORIZONTAL, cInputSpacing) {
+
+ m_descLabel.set_text(cTimerDesc.data());
+ m_descLabel.set_wrap(true);
+ m_descLabel.set_justify(Gtk::Justification::CENTER);
+ m_descLabel.set_vexpand(true);
+ m_descLabel.set_valign(Gtk::Align::END);
+ append(m_descLabel);
+
+ m_durationLabel.set_text(cDurationLabel.data());
+ m_durationSpin.set_range(cMinSeconds, cMaxSeconds);
+ m_durationSpin.set_increments(cStepSeconds, cPageSeconds);
+ m_durationSpin.set_value(10.0); // default 10 seconds
+
+ m_inputBox.set_halign(Gtk::Align::CENTER);
+ m_inputBox.append(m_durationLabel);
+ m_inputBox.append(m_durationSpin);
+ append(m_inputBox);
+
+ m_startButton.set_label(cBtnStart.data());
+ m_startButton.set_halign(Gtk::Align::CENTER);
+ m_startButton.set_margin_bottom(10);
+ m_startButton.signal_clicked().connect(
+ sigc::mem_fun(*this, &TimerModePage::onStartClicked));
+ append(m_startButton);
+}
+
+void TimerModePage::onStartClicked() {
+ uint32_t seconds = static_cast(m_durationSpin.get_value());
+ std::cout << "[TimerModePage] Start clicked, seconds: " << seconds << std::endl;
+ m_signalStarted.emit(seconds);
+}
+
+} // namespace minihildesk::View
diff --git a/sw/minihildesk/src/view/pages/mode_page_timer.h b/sw/minihildesk/src/view/pages/mode_page_timer.h
new file mode 100644
index 0000000..e2a1e39
--- /dev/null
+++ b/sw/minihildesk/src/view/pages/mode_page_timer.h
@@ -0,0 +1,50 @@
+////////////////////////////////////////////////////////////////////////////////////////////////////////
+///
+/// mode_page_timer.h
+/// Copyright (C) 2025 - 2026 Vladimir Roncevic
+///
+/// minihildesk is free software: you can redistribute it and/or modify it
+/// under the terms of the GNU General Public License as published by the
+/// Free Software Foundation, either version 3 of the License, or
+/// (at your option) any later version.
+///
+/// minihildesk is distributed in the hope that it will be useful, but
+/// WITHOUT ANY WARRANTY; without even the implied warranty of
+/// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
+/// See the GNU General Public License for more details.
+///
+/// You should have received a copy of the GNU General Public License along
+/// with this program. If not, see .
+///
+////////////////////////////////////////////////////////////////////////////////////////////////////////
+#pragma once
+
+#include
+#include
+#include
+#include
+#include
+
+namespace minihildesk::View {
+
+class TimerModePage : public Gtk::Box {
+public:
+ explicit TimerModePage(int relayId);
+ ~TimerModePage() override = default;
+
+ sigc::signal &signal_started() { return m_signalStarted; }
+
+private:
+ void onStartClicked();
+
+ int m_relayId;
+ Gtk::Label m_descLabel;
+ Gtk::Box m_inputBox;
+ Gtk::Label m_durationLabel;
+ Gtk::SpinButton m_durationSpin;
+ Gtk::Button m_startButton;
+
+ sigc::signal m_signalStarted;
+};
+
+} // namespace minihildesk::View
diff --git a/sw/minihildesk/src/view/pages/mode_page_toggle.cc b/sw/minihildesk/src/view/pages/mode_page_toggle.cc
new file mode 100644
index 0000000..3d56884
--- /dev/null
+++ b/sw/minihildesk/src/view/pages/mode_page_toggle.cc
@@ -0,0 +1,71 @@
+////////////////////////////////////////////////////////////////////////////////////////////////////////
+///
+/// mode_page_toggle.cc
+/// Copyright (C) 2025 - 2026 Vladimir Roncevic
+///
+/// minihildesk is free software: you can redistribute it and/or modify it
+/// under the terms of the GNU General Public License as published by the
+/// Free Software Foundation, either version 3 of the License, or
+/// (at your option) any later version.
+///
+/// minihildesk is distributed in the hope that it will be useful, but
+/// WITHOUT ANY WARRANTY; without even the implied warranty of
+/// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
+/// See the GNU General Public License for more details.
+///
+/// You should have received a copy of the GNU General Public License along
+/// with this program. If not, see .
+///
+////////////////////////////////////////////////////////////////////////////////////////////////////////
+#include
+
+namespace {
+constexpr std::string_view cToggleDesc{"Manually control relay state."};
+constexpr int cBoxSpacing{8};
+constexpr int cSwitchMarginBottom{10};
+constexpr int cSwitchMarginTop{5};
+} // namespace
+
+namespace minihildesk::View {
+
+ToggleModePage::ToggleModePage(int relayId)
+ : Gtk::Box(Gtk::Orientation::VERTICAL, cBoxSpacing), m_relayId(relayId) {
+
+ m_descLabel.set_text(cToggleDesc.data());
+ m_descLabel.set_wrap(true);
+ m_descLabel.set_justify(Gtk::Justification::CENTER);
+ m_descLabel.set_vexpand(true);
+ m_descLabel.set_valign(Gtk::Align::END);
+ append(m_descLabel);
+
+ m_switch.set_halign(Gtk::Align::CENTER);
+ m_switch.set_valign(Gtk::Align::CENTER);
+ m_switch.set_margin_bottom(cSwitchMarginBottom);
+ m_switch.set_margin_top(cSwitchMarginTop);
+
+ m_switch.signal_state_set().connect(
+ sigc::mem_fun(*this, &ToggleModePage::onStateSet), false);
+
+ append(m_switch);
+}
+
+void ToggleModePage::updateState(bool active) {
+ m_updating = true;
+ m_switch.set_active(active);
+ m_switch.set_state(active);
+ m_updating = false;
+}
+
+bool ToggleModePage::getState() const {
+ return m_switch.get_active();
+}
+
+bool ToggleModePage::onStateSet(bool state) {
+ if (m_updating) {
+ return false;
+ }
+ m_signalToggled.emit(state);
+ return false;
+}
+
+} // namespace minihildesk::View
diff --git a/sw/minihildesk/src/view/pages/mode_page_toggle.h b/sw/minihildesk/src/view/pages/mode_page_toggle.h
new file mode 100644
index 0000000..2a37452
--- /dev/null
+++ b/sw/minihildesk/src/view/pages/mode_page_toggle.h
@@ -0,0 +1,50 @@
+////////////////////////////////////////////////////////////////////////////////////////////////////////
+///
+/// mode_page_toggle.h
+/// Copyright (C) 2025 - 2026 Vladimir Roncevic
+///
+/// minihildesk is free software: you can redistribute it and/or modify it
+/// under the terms of the GNU General Public License as published by the
+/// Free Software Foundation, either version 3 of the License, or
+/// (at your option) any later version.
+///
+/// minihildesk is distributed in the hope that it will be useful, but
+/// WITHOUT ANY WARRANTY; without even the implied warranty of
+/// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
+/// See the GNU General Public License for more details.
+///
+/// You should have received a copy of the GNU General Public License along
+/// with this program. If not, see .
+///
+////////////////////////////////////////////////////////////////////////////////////////////////////////
+#pragma once
+
+#include
+#include
+#include
+#include
+
+namespace minihildesk::View {
+
+class ToggleModePage : public Gtk::Box {
+public:
+ explicit ToggleModePage(int relayId);
+ ~ToggleModePage() override = default;
+
+ void updateState(bool active);
+ bool getState() const;
+
+ sigc::signal &signal_toggled() { return m_signalToggled; }
+
+private:
+ bool onStateSet(bool state);
+
+ int m_relayId;
+ Gtk::Label m_descLabel;
+ Gtk::Switch m_switch;
+ bool m_updating{false};
+
+ sigc::signal m_signalToggled;
+};
+
+} // namespace minihildesk::View
diff --git a/sw/minihildesk/src/view/relay_widget.cc b/sw/minihildesk/src/view/relay_widget.cc
index 481d9cf..369607f 100644
--- a/sw/minihildesk/src/view/relay_widget.cc
+++ b/sw/minihildesk/src/view/relay_widget.cc
@@ -20,6 +20,7 @@
#include
#include
#include
+#include
namespace {
constexpr std::string_view cRelayCardClass{"relay-card"};
@@ -31,8 +32,6 @@ constexpr std::string_view cLedOffClass{"led-off"};
constexpr std::string_view cLedOnClass{"led-on"};
constexpr int cWidgetMargin{5};
-constexpr int cSwitchMarginBottom{10};
-constexpr int cSwitchMarginTop{5};
constexpr int cBoxMargin{12};
constexpr int cBoxSpacing{8};
constexpr int cHeaderSpacing{5};
@@ -43,7 +42,11 @@ namespace minihildesk::View {
RelayWidget::RelayWidget(int relayId)
: m_relayId(relayId),
m_box(Gtk::Orientation::VERTICAL, cBoxSpacing),
- m_headerBox(Gtk::Orientation::HORIZONTAL, cHeaderSpacing) {
+ m_headerBox(Gtk::Orientation::HORIZONTAL, cHeaderSpacing),
+ m_pageToggle(relayId),
+ m_pageTimer(relayId),
+ m_pagePulse(relayId),
+ m_pageBlink(relayId) {
set_margin(cWidgetMargin);
// Set card styling class
@@ -62,17 +65,41 @@ RelayWidget::RelayWidget(int relayId)
m_headerBox.append(m_titleLabel);
m_headerBox.append(m_indicatorLabel);
- m_switch.set_halign(Gtk::Align::CENTER);
- m_switch.set_valign(Gtk::Align::CENTER);
- m_switch.set_margin_bottom(cSwitchMarginBottom);
- m_switch.set_margin_top(cSwitchMarginTop);
-
- // Block default toggling behavior to handle it asynchronously
- m_switch.signal_state_set().connect(
- sigc::mem_fun(*this, &RelayWidget::onStateSet), false);
+ // Populate mode selection dropdown
+ m_modeCombo.append("Toggle");
+ m_modeCombo.append("Timer");
+ m_modeCombo.append("Pulse");
+ m_modeCombo.append("Blink");
+ m_modeCombo.set_active(0);
+ m_modeCombo.signal_changed().connect(
+ sigc::mem_fun(*this, &RelayWidget::onModeChanged));
+
+ // Stack settings
+ m_stack.set_transition_type(Gtk::StackTransitionType::CROSSFADE);
+ m_stack.set_transition_duration(150);
+ m_stack.set_hhomogeneous(true);
+ m_stack.set_vhomogeneous(true);
+ m_stack.set_vexpand(true);
+
+ m_stack.add(m_pageToggle, "toggle");
+ m_stack.add(m_pageTimer, "timer");
+ m_stack.add(m_pagePulse, "pulse");
+ m_stack.add(m_pageBlink, "blink");
+ m_stack.set_visible_child("toggle");
+
+ // Connect child page signals
+ m_pageToggle.signal_toggled().connect(
+ sigc::mem_fun(*this, &RelayWidget::onPageToggled));
+ m_pageTimer.signal_started().connect(
+ sigc::mem_fun(*this, &RelayWidget::onPageTimerStarted));
+ m_pagePulse.signal_triggered().connect(
+ sigc::mem_fun(*this, &RelayWidget::onPagePulseTriggered));
+ m_pageBlink.signal_started().connect(
+ sigc::mem_fun(*this, &RelayWidget::onPageBlinkStarted));
m_box.append(m_headerBox);
- m_box.append(m_switch);
+ m_box.append(m_modeCombo);
+ m_box.append(m_stack);
m_box.set_margin(cBoxMargin);
set_child(m_box);
@@ -81,35 +108,55 @@ RelayWidget::RelayWidget(int relayId)
void RelayWidget::updateState(bool active) {
m_active = active;
- // Temporarily disable the signal handler logic to prevent feedback loops
- m_updating = true;
- m_switch.set_active(active);
- m_switch.set_state(active);
- m_updating = false;
-
if (active) {
m_indicatorLabel.get_style_context()->remove_class(cLedOffClass.data());
m_indicatorLabel.get_style_context()->add_class(cLedOnClass.data());
+ get_style_context()->add_class("active-relay");
} else {
m_indicatorLabel.get_style_context()->remove_class(cLedOnClass.data());
m_indicatorLabel.get_style_context()->add_class(cLedOffClass.data());
+ get_style_context()->remove_class("active-relay");
}
+
+ m_pageToggle.updateState(active);
}
bool RelayWidget::getState() const { return m_active; }
-bool RelayWidget::onStateSet(bool state) {
- if (m_updating) {
- // Let state propagate visually
- return false;
+void RelayWidget::onModeChanged() {
+ int activeIndex = m_modeCombo.get_active_row_number();
+ switch (activeIndex) {
+ case 0: m_stack.set_visible_child("toggle"); break;
+ case 1: m_stack.set_visible_child("timer"); break;
+ case 2: m_stack.set_visible_child("pulse"); break;
+ case 3: m_stack.set_visible_child("blink"); break;
+ default: m_stack.set_visible_child("toggle"); break;
}
- // Emit signal to parent, which will request the change via TCP
+
+ // Automatically turn off the relay when mode changes for safety
+ if (m_active) {
+ m_signalToggled.emit(m_relayId, false);
+ }
+}
+
+void RelayWidget::onPageToggled(bool state) {
+ std::cout << "[RelayWidget] onPageToggled for relay " << m_relayId << ", state: " << state << std::endl;
m_signalToggled.emit(m_relayId, state);
+}
+
+void RelayWidget::onPageTimerStarted(uint32_t seconds) {
+ std::cout << "[RelayWidget] onPageTimerStarted for relay " << m_relayId << ", seconds: " << seconds << std::endl;
+ m_signalTimerStarted.emit(m_relayId, seconds);
+}
+
+void RelayWidget::onPagePulseTriggered(uint32_t durationMs) {
+ std::cout << "[RelayWidget] onPagePulseTriggered for relay " << m_relayId << ", durationMs: " << durationMs << std::endl;
+ m_signalPulseTriggered.emit(m_relayId, durationMs);
+}
- // Return false to allow the switch to slide immediately for smooth UI
- // response. If the server fails to update, the next periodic status poll will
- // correct it.
- return false;
+void RelayWidget::onPageBlinkStarted(uint32_t onMs, uint32_t offMs, uint32_t count) {
+ std::cout << "[RelayWidget] onPageBlinkStarted for relay " << m_relayId << ", onMs: " << onMs << ", offMs: " << offMs << ", count: " << count << std::endl;
+ m_signalBlinkStarted.emit(m_relayId, onMs, offMs, count);
}
} // namespace minihildesk::View
diff --git a/sw/minihildesk/src/view/relay_widget.h b/sw/minihildesk/src/view/relay_widget.h
index 147998e..76bf09e 100644
--- a/sw/minihildesk/src/view/relay_widget.h
+++ b/sw/minihildesk/src/view/relay_widget.h
@@ -20,10 +20,15 @@
#pragma once
#include
+#include
#include
#include
-#include
+#include
#include
+#include
+#include
+#include
+#include
namespace minihildesk::View {
@@ -36,21 +41,45 @@ class RelayWidget : public Gtk::Frame {
bool getState() const;
sigc::signal &signal_toggled() { return m_signalToggled; }
+ sigc::signal &signal_timer_started() {
+ return m_signalTimerStarted;
+ }
+ sigc::signal &signal_pulse_triggered() {
+ return m_signalPulseTriggered;
+ }
+ sigc::signal &
+ signal_blink_started() {
+ return m_signalBlinkStarted;
+ }
private:
- bool onStateSet(bool state);
+ void onModeChanged();
+
+ void onPageToggled(bool state);
+ void onPageTimerStarted(uint32_t seconds);
+ void onPagePulseTriggered(uint32_t durationMs);
+ void onPageBlinkStarted(uint32_t onMs, uint32_t offMs, uint32_t count);
int m_relayId;
bool m_active{false};
- bool m_updating{false};
Gtk::Box m_box;
Gtk::Box m_headerBox;
Gtk::Label m_titleLabel;
Gtk::Label m_indicatorLabel;
- Gtk::Switch m_switch;
+
+ Gtk::ComboBoxText m_modeCombo;
+ Gtk::Stack m_stack;
+
+ ToggleModePage m_pageToggle;
+ TimerModePage m_pageTimer;
+ PulseModePage m_pagePulse;
+ BlinkModePage m_pageBlink;
sigc::signal m_signalToggled;
+ sigc::signal m_signalTimerStarted;
+ sigc::signal m_signalPulseTriggered;
+ sigc::signal m_signalBlinkStarted;
};
} // namespace minihildesk::View