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 [![Documentation Status](https://readthedocs.org/projects/minihil/badge/?version=latest)](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. -Raspberry Pi & GNOME Foundations \ No newline at end of file +Foundations \ 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[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:

+
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_timer

+

Keep a relay active for a specific duration in seconds:

+
    +
  • Request:

    +
    {"jsonrpc": "2.0", "method": "start_timer", "params": {"relay_id": 2, "seconds": 10}, "id": 3}
    +
    +
    +
  • +
  • Response:

    +
    {"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:

    +
    {"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}
    +
    +
    +
  • +
+
+ +
+

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 @@

Table of Contents

  • 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