diff --git a/.github/workflows/restat_base_c_checker.yml b/.github/workflows/restat_base_c_checker.yml
new file mode 100644
index 0000000..4571b0c
--- /dev/null
+++ b/.github/workflows/restat_base_c_checker.yml
@@ -0,0 +1,33 @@
+name: restat_base C checker
+on:
+ push:
+ branches: [ master ]
+ paths:
+ - 'sw/restat_base/src/**/*.c'
+ - 'sw/restat_base/src/**/*.h'
+ pull_request:
+ branches: [ master ]
+ paths:
+ - 'sw/restat_base/src/**/*.c'
+ - 'sw/restat_base/src/**/*.h'
+jobs:
+ build:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v2
+ - name: Check length of line in modules
+ id: long_line_checker
+ run: |
+ echo "Checking the length of lines in modules"
+ modules_ok=0
+ modules=($(find sw/restat_base/src/ -type f -name '*.c' -exec echo '{}' \;))
+ for mod in "${modules[@]}"; do line_length=$(wc -L < "${mod}"); echo "${mod} : $line_length"; [[ $line_length -gt 300 ]] && modules_ok=1; done
+ [[ $modules_ok -eq 0 ]] && echo "All modules are good" || exit 1
+ - name: Check number of lines in modules
+ id: num_line_checker
+ run: |
+ echo "Checking the number of lines in modules"
+ modules_ok=0
+ modules=($(find sw/restat_base/src/ -type f -name '*.c' -exec echo '{}' \;))
+ for mod in "${modules[@]}"; do line_numbers=$(wc -l < "${mod}"); echo "${mod} : $line_numbers"; [[ $line_numbers -gt 300 ]] && modules_ok=1; done
+ [[ $modules_ok -eq 0 ]] && echo "All modules are good" && exit 0 || exit 1
diff --git a/.github/workflows/restat_desktop_cc_checker.yml b/.github/workflows/restat_desktop_cc_checker.yml
index fc4e053..1f52807 100755
--- a/.github/workflows/restat_desktop_cc_checker.yml
+++ b/.github/workflows/restat_desktop_cc_checker.yml
@@ -1,13 +1,15 @@
-name: restat-desktop CC checker
+name: restatdesk CC checker
on:
push:
branches: [ master ]
paths:
- - 'sw/restat-desktop/**'
+ - 'sw/restatdesk/**/*.cc'
+ - 'sw/restatdesk/**/*.h'
pull_request:
branches: [ master ]
paths:
- - 'sw/restat-desktop/**'
+ - 'sw/restatdesk/**/*.cc'
+ - 'sw/restatdesk/**/*.h'
jobs:
build:
runs-on: ubuntu-latest
@@ -16,19 +18,16 @@ jobs:
- name: Check length of line in modules
id: long_line_checker
run: |
- echo Checking length of line in modules
+ echo "Checking the length of lines in modules"
modules_ok=0
- modules=($(find sw/ -type f -name '*.cc' -exec echo '{}' \;))
- for mod in "${modules[@]}"; do line_length=$(wc -L < "${mod}"); [[ $line_length -gt 80 ]] && modules_ok=1; done
- [[ $modules_ok -eq 0 ]] && echo ::set-output name=status::success || echo ::set-output name=status::failure
+ modules=($(find sw/restatdesk/ -type f -name '*.cc' -exec echo '{}' \;))
+ for mod in "${modules[@]}"; do line_length=$(wc -L < "${mod}"); echo "${mod} : $line_length"; [[ $line_length -gt 500 ]] && modules_ok=1; done
+ [[ $modules_ok -eq 0 ]] && echo "All modules are good" || exit 1
- name: Check number of lines in modules
id: num_line_checker
run: |
- echo Checking number of lines in modules
+ echo "Checking the number of lines in modules"
modules_ok=0
- modules=($(find sw/ -type f -name '*.cc' -exec echo '{}' \;))
- for mod in "${modules[@]}"; do line_numbers=$(wc -l < "${mod}"); [[ $line_numbers -gt 300 ]] && modules_ok=1; done
- [[ $modules_ok -eq 0 ]] && echo ::set-output name=status::success || echo ::set-output name=status::failure
- - name: Check on failures
- if: steps.long_line_checker.outputs.status == 'failure' || steps.num_line_checker.outputs.status == 'failure'
- run: exit 1
+ modules=($(find sw/restatdesk/ -type f -name '*.cc' -exec echo '{}' \;))
+ for mod in "${modules[@]}"; do line_numbers=$(wc -l < "${mod}"); echo "${mod} : $line_numbers"; [[ $line_numbers -gt 500 ]] && modules_ok=1; done
+ [[ $modules_ok -eq 0 ]] && echo "All modules are good" && exit 0 || exit 1
diff --git a/.github/workflows/restat_desktop_cc_checker_build.yml b/.github/workflows/restat_desktop_cc_checker_build.yml
index 6829cb9..43b369e 100644
--- a/.github/workflows/restat_desktop_cc_checker_build.yml
+++ b/.github/workflows/restat_desktop_cc_checker_build.yml
@@ -1,31 +1,38 @@
-name: restat-desktop C++ CI
+name: restatdesk build checker
on:
push:
branches: [ master ]
paths:
- - 'sw/restat-desktop/**'
+ - 'sw/restatdesk/**/*.cc'
+ - 'sw/restatdesk/**/*.h'
pull_request:
branches: [ master ]
paths:
- - 'sw/restat-desktop/**'
+ - 'sw/restatdesk/**/*.cc'
+ - 'sw/restatdesk/**/*.h'
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- - name: install GTK--
+ - name: install gtkmm
id: install_gtkmm
run: |
sudo apt-get update
- sudo apt-get install jq
- sudo apt-get install libgtkmm-3.0-dev
+ sudo apt-get upgrade -y
+ sudo apt-get install -y pkg-config
+ sudo apt-get install -y libserial-dev
+ sudo apt-get install -y libgtkmm-3.0-dev
+ sudo apt-get install -y libgtk-4-dev
+ sudo apt-get install -y software-properties-common
+ sudo apt-get install -y libgtkmm-4.0-dev
- name: make all
id: gnu_make_all
run: |
- cd sw/restat-desktop/build
+ cd sw/restatdesk/build
make all
- name: make clean
id: gnu_make_clean
run: |
- cd sw/restat-desktop/build
+ cd sw/restatdesk/build
make clean
diff --git a/.github/workflows/restat_device_c_checker.yml b/.github/workflows/restat_device_c_checker.yml
deleted file mode 100755
index e7776ab..0000000
--- a/.github/workflows/restat_device_c_checker.yml
+++ /dev/null
@@ -1,34 +0,0 @@
-name: restat-device C checker
-on:
- push:
- branches: [ master ]
- paths:
- - 'sw/restat-device0/**'
- pull_request:
- branches: [ master ]
- paths:
- - 'sw/restat-device1/**'
-jobs:
- build:
- runs-on: ubuntu-latest
- steps:
- - uses: actions/checkout@v2
- - name: Check length of line in modules
- id: long_line_checker
- run: |
- echo Checking length of line in modules
- modules_ok=0
- modules=($(find sw/ -type f -name '*.c' -exec echo '{}' \;))
- for mod in "${modules[@]}"; do line_length=$(wc -L < "${mod}"); [[ $line_length -gt 80 ]] && modules_ok=1; done
- [[ $modules_ok -eq 0 ]] && echo ::set-output name=status::success || echo ::set-output name=status::failure
- - name: Check number of lines in modules
- id: num_line_checker
- run: |
- echo Checking number of lines in modules
- modules_ok=0
- modules=($(find sw/ -type f -name '*.c' -exec echo '{}' \;))
- for mod in "${modules[@]}"; do line_numbers=$(wc -l < "${mod}"); [[ $line_numbers -gt 300 ]] && modules_ok=1; done
- [[ $modules_ok -eq 0 ]] && echo ::set-output name=status::success || echo ::set-output name=status::failure
- - name: Check on failures
- if: steps.long_line_checker.outputs.status == 'failure' || steps.num_line_checker.outputs.status == 'failure'
- run: exit 1
diff --git a/.github/workflows/restat_toc.yaml b/.github/workflows/restat_toc.yaml
deleted file mode 100755
index ab779a6..0000000
--- a/.github/workflows/restat_toc.yaml
+++ /dev/null
@@ -1,11 +0,0 @@
-name: restat toc
-on:
- push:
- paths:
- - '**.md'
-jobs:
- generateTOC:
- name: TOC Generator
- runs-on: ubuntu-latest
- steps:
- - uses: technote-space/toc-generator@v2
diff --git a/.github/workflows/restat_toc.yml b/.github/workflows/restat_toc.yml
new file mode 100644
index 0000000..c09313e
--- /dev/null
+++ b/.github/workflows/restat_toc.yml
@@ -0,0 +1,25 @@
+name: restat toc
+
+on:
+ push:
+ branches: [ master ]
+ paths:
+ - '**.md'
+ pull_request:
+ branches: [ master ]
+ paths:
+ - '**.md'
+
+permissions:
+ contents: write
+
+jobs:
+ generateTOC:
+ name: restat toc
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v4
+ - name: Generate TOC
+ uses: technote-space/toc-generator@v4
+ with:
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
diff --git a/.gitignore b/.gitignore
index e0c44ec..8d9adab 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,3 +1,87 @@
+# ==========================================
+# 1. Global & Common Ignores
+# ==========================================
+
+# OS Metadata
+.DS_Store
+Thumbs.db
+*~
+*.swp
+*.swo
+
+# Compiler & Build Artifacts (Generic)
+*.o
+*.obj
+*.a
+*.lib
+*.so
+*.dylib
+*.dll
+*.d
+*.slo
+*.lo
+*.gch
+*.pch
+*.mod
+*.smod
+*.lai
+*.la
+*.exe
+*.out
+*.app
+*.lss
+core
+
+# Development Caches & Local Configurations
+.cache/
+.clangd
+.idea/
+.vs/
+*.pfx
+
+# Python generic ignores
__pycache__/
*.py[cod]
-*.o
\ No newline at end of file
+*.pyi
+.venv/
+venv/
+ENV/
+env/
+
+# CMake generic ignores
+CMakeCache.txt
+CMakeFiles/
+CMakeScripts/
+Testing/
+Makefile
+cmake_install.cmake
+install_manifest.txt
+compile_commands.json
+CTestTestfile.cmake
+_deps/
+CPackConfig.cmake
+CPackSourceConfig.cmake
+CTestConfig.cmake
+
+
+# ==========================================
+# 2. Sub-project Ignores (Software - sw/)
+# ==========================================
+
+### sw/restat_base
+sw/restat_base/build/
+
+### sw/restatdesk
+sw/restatdesk/build/
+sw/restatdesk/resources.cc
+
+
+# ==========================================
+# 3. Sub-project Ignores (Hardware - hw/)
+# ==========================================
+
+# KiCad / CAD backups and local caches
+hw/**/*.kicad_pcb-bak
+hw/**/*.sch-bak
+hw/**/fp-info-cache
+hw/**/*-backups/
\ No newline at end of file
diff --git a/README.md b/README.md
index 2a79b16..315d652 100644
--- a/README.md
+++ b/README.md
@@ -10,51 +10,156 @@ other information that should be provided before the tool is installed.
[](https://github.com/electux/restat/issues)
[](https://github.com/electux/restat/graphs/contributors)
+ [](https://github.com/electux/restat/actions/workflows/restat_base_c_checker.yml)
+ [](https://github.com/electux/restat/actions/workflows/restat_desktop_cc_checker.yml)
+ [](https://github.com/electux/restat/actions/workflows/restat_desktop_cc_checker_build.yml)
+ [](https://github.com/electux/restat/actions/workflows/restat_toc.yml)
+
**Table of Contents**
+- [System Components](#system-components)
+ - [1. restat Firmware (`restat_base`)](#1-restat-firmware-restat_base)
+ - [2. restatdesk GUI Application (`restatdesk`)](#2-restatdesk-gui-application-restatdesk)
+- [Command Interface (restat Protocol)](#command-interface-restat-protocol)
- [Installation](#installation)
+ - [1. Firmware (`restat_base`)](#1-firmware-restat_base)
+ - [2. Desktop Application (`restatdesk`)](#2-desktop-application-restatdesk)
- [Usage](#usage)
+ - [1. GUI Panel](#1-gui-panel)
+ - [2. Test Commands](#2-test-commands)
- [Dependencies](#dependencies)
-- [Project structure](#project-structure)
+ - [Firmware (`restat_base`)](#firmware-restat_base)
+ - [Desktop GUI (`restatdesk`)](#desktop-gui-restatdesk)
+- [Project Structure](#project-structure)
- [Docs](#docs)
- [Copyright and licence](#copyright-and-licence)
+### System Components
+
+**restat** consists of two primary components:
+
+#### 1. restat Firmware (`restat_base`)
+A low-level C firmware designed for Pico RP2040 microcontrollers to actuate, time, and persist relay outputs.
+* **Role**: Actuates 8 mechanical relay channels using high-precision timers and persists configurations directly to onboard Flash memory.
+* **Features**:
+ * Multi-interface Command Dispatcher (Serial, TCP/IP, BLE).
+ * Variable Execution Plan Parser: Processes complex execution strings concurrently to set states and schedules.
+ * Flash-Based Plan Persistence: Automatically stores execution plans at a `1.5 MB` flash offset (`0x180000`) and restores configurations upon reboot.
+ * Precise Timing Engine: Millisecond-level accuracy for Timed modes.
+ * Safety Watchdog & Sound Indicators: Recovery loops coupled with active buzzer signaling.
+
+#### 2. restatdesk GUI Application (`restatdesk`)
+A responsive, cross-platform C++ application built on **gtkmm 4** and **GTK 4**.
+* **Role**: Configures, controls, and monitors the relay station in real-time.
+* **Features**:
+ * Real-Time LED Indicators: High-visibility green status lights showing live channel states.
+ * Configurable Relay Modes:
+ * **Toggle Mode**: Activates or deactivates relays on click.
+ * **Timer Mode**: Executes timed sequences with flexible starting states (Active = ON on Start, Deactive = OFF on Start).
+ * Dynamic Plan Builder: Serializes only active relay states into a single, variable-length plan command to protect relay lifespans.
+ * Sleek Green-on-Black Dark Mode: Custom CSS styles compiled directly into the binary as GResource packages.
+ * Multithreaded System Logs: Visualized console output and persistent file logging.
+
+---
+
+### Command Interface (restat Protocol)
+
+All commands sent to the `restat_base` firmware must be wrapped in start/end markers: `
-
-
-
+
diff --git a/docs/GNOME.png b/docs/GNOME.png
deleted file mode 100644
index dad3734..0000000
Binary files a/docs/GNOME.png and /dev/null differ
diff --git a/docs/RPi.png b/docs/RPi.png
deleted file mode 100644
index 7975ced..0000000
Binary files a/docs/RPi.png and /dev/null differ
diff --git a/docs/build/doctrees/environment.pickle b/docs/build/doctrees/environment.pickle
index 2dacc20..0aea87e 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 193b1eb..6e4d688 100644
Binary files a/docs/build/doctrees/index.doctree and b/docs/build/doctrees/index.doctree differ
diff --git a/docs/build/html/.buildinfo b/docs/build/html/.buildinfo
index a4d5510..8bc6723 100644
--- a/docs/build/html/.buildinfo
+++ b/docs/build/html/.buildinfo
@@ -1,4 +1,4 @@
# Sphinx build info version 1
# This file hashes the configuration used when building these files. When it is not found, a full rebuild will be done.
-config: 2e449e5e4407c6ca24699f725982b8ce
+config: 574c3deb57811c1dd9a2ed1359a26029
tags: 645f666f9bcd5a90fca523b33c5a78b7
diff --git a/docs/build/html/_sources/index.rst.txt b/docs/build/html/_sources/index.rst.txt
index 23329a7..220f5c3 100644
--- a/docs/build/html/_sources/index.rst.txt
+++ b/docs/build/html/_sources/index.rst.txt
@@ -12,7 +12,7 @@ other information that should be provided before the tool is installed.
self
-|GitHub issues| |Documentation Status| |GitHub contributors|
+|GitHub issues| |Documentation Status| |GitHub contributors| |restat_base C checker| |restatdesk CC checker| |restatdesk build checker| |restat toc|
.. |GitHub issues| image:: https://img.shields.io/github/issues/electux/restat.svg
:target: https://github.com/electux/restat/issues
@@ -23,33 +23,168 @@ other information that should be provided before the tool is installed.
.. |Documentation Status| image:: https://readthedocs.org/projects/restat/badge/?version=latest
:target: https://restat.readthedocs.io/projects/restat/en/latest/?badge=latest
+.. |restat_base C checker| image:: https://github.com/electux/restat/actions/workflows/restat_base_c_checker.yml/badge.svg
+ :target: https://github.com/electux/restat/actions/workflows/restat_base_c_checker.yml
+
+.. |restatdesk CC checker| image:: https://github.com/electux/restat/actions/workflows/restat_desktop_cc_checker.yml/badge.svg
+ :target: https://github.com/electux/restat/actions/workflows/restat_desktop_cc_checker.yml
+
+.. |restatdesk build checker| image:: https://github.com/electux/restat/actions/workflows/restat_desktop_cc_checker_build.yml/badge.svg
+ :target: https://github.com/electux/restat/actions/workflows/restat_desktop_cc_checker_build.yml
+
+.. |restat toc| image:: https://github.com/electux/restat/actions/workflows/restat_toc.yml/badge.svg
+ :target: https://github.com/electux/restat/actions/workflows/restat_toc.yml
+
+System Components
+-----------------
+
+**restat** consists of two primary components:
+
+1. restat Firmware (``restat_base``)
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+A low-level C firmware designed for Pico RP2040 microcontrollers to actuate, time, and persist relay outputs.
+
+* **Role**: Actuates 8 mechanical relay channels using high-precision timers and persists configurations directly to onboard Flash memory.
+* **Features**:
+ * Multi-interface Command Dispatcher (Serial, TCP/IP, BLE).
+ * Variable Execution Plan Parser: Processes complex execution strings concurrently to set states and schedules.
+ * Flash-Based Plan Persistence: Automatically stores execution plans at a ``1.5 MB`` flash offset (``0x180000``) and restores configurations upon reboot.
+ * Precise Timing Engine: Millisecond-level accuracy for Timed modes.
+ * Safety Watchdog & Sound Indicators: Recovery loops coupled with active buzzer signaling.
+
+2. restatdesk GUI Application (``restatdesk``)
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+A responsive, cross-platform C++ application built on **gtkmm 4** and **GTK 4**.
+
+* **Role**: Configures, controls, and monitors the relay station in real-time.
+* **Features**:
+
+ * Real-Time LED Indicators: High-visibility green status lights showing live channel states.
+ * Configurable Relay Modes:
+
+ * **Toggle Mode**: Activates or deactivates relays on click.
+ * **Timer Mode**: Executes timed sequences with flexible starting states (Active = ON on Start, Deactive = OFF on Start).
+
+ * Dynamic Plan Builder: Serializes only active relay states into a single, variable-length plan command to protect relay lifespans.
+ * Sleek Green-on-Black Dark Mode: Custom CSS styles compiled directly into the binary as GResource packages.
+ * Multithreaded System Logs: Visualized console output and persistent file logging.
+
+
+Command Interface (restat Protocol)
+-----------------------------------
+
+All commands sent to the ``restat_base`` firmware must be wrapped in start/end markers: ``restat¶
other information that should be provided before the tool is installed.
restat consists of two primary components:
+restat_base)¶A low-level C firmware designed for Pico RP2040 microcontrollers to actuate, time, and persist relay outputs.
+Role: Actuates 8 mechanical relay channels using high-precision timers and persists configurations directly to onboard Flash memory.
Features:
+* Multi-interface Command Dispatcher (Serial, TCP/IP, BLE).
+* Variable Execution Plan Parser: Processes complex execution strings concurrently to set states and schedules.
+* Flash-Based Plan Persistence: Automatically stores execution plans at a 1.5 MB flash offset (0x180000) and restores configurations upon reboot.
+* Precise Timing Engine: Millisecond-level accuracy for Timed modes.
+* Safety Watchdog & Sound Indicators: Recovery loops coupled with active buzzer signaling.
restatdesk)¶A responsive, cross-platform C++ application built on gtkmm 4 and GTK 4.
+Role: Configures, controls, and monitors the relay station in real-time.
Features:
+Real-Time LED Indicators: High-visibility green status lights showing live channel states.
Configurable Relay Modes:
+Toggle Mode: Activates or deactivates relays on click.
Timer Mode: Executes timed sequences with flexible starting states (Active = ON on Start, Deactive = OFF on Start).
Dynamic Plan Builder: Serializes only active relay states into a single, variable-length plan command to protect relay lifespans.
Sleek Green-on-Black Dark Mode: Custom CSS styles compiled directly into the binary as GResource packages.
Multithreaded System Logs: Visualized console output and persistent file logging.
All commands sent to the restat_base firmware must be wrapped in start/end markers: <rs#TARGET#ACTION#PARAMS#end>.
Command |
+Action |
+Description |
+
|---|---|---|
|
+Identify |
+Returns board identification (e.g. |
+
|
+Get Version |
+Returns firmware version (e.g. |
+
|
+Channel ON |
+Sets channel |
+
|
+Channel OFF |
+Sets channel |
+
|
+Timed Channel |
+Starts timer on channel |
+
|
+All ON |
+Turns all channels ON concurrently |
+
|
+All OFF |
+Turns all channels OFF concurrently |
+
|
+Binary Mask |
+Sets all 8 channels to binary state |
+
|
+Execute Plan |
+Sends a variable-length configuration plan containing relay states and saves |
+
|
+Channel Status |
+Returns the status string of channel |
+
|
+All Status |
+Returns status dump for all channels |
+
|
+Reset |
+Performs soft system reset using watchdog |
+
Navigate to release page download and extract release archive.
-To install restat type the following
-in progress
+
+1. Firmware (restat_base)¶
+Setup the Raspberry Pi Pico SDK on your host system, then build:
+cd sw/restat_base
+mkdir -p build && cd build
+cmake ..
+make -j$(nproc)
+
+
+Boot the board in BOOTSEL mode and copy the compiled restat_base.uf2 to the Pico mass storage volume.
+
+
+2. Desktop Application (restatdesk)¶
+Ensure all C++23 compiler tools and GTK 4 libraries are installed, then run:
+cd sw/restatdesk/build
+make all
+
+
+The executable restatdesk will be ready inside the build/ directory.
+
+
+
+Usage¶
+
+1. GUI Panel¶
+Launch the desktop client:
+./sw/restatdesk/build/restatdesk
+Configure the connection (Serial, TCP/IP, or BLE) in the Settings window, toggle active relay options in the UI, and click Execute Plan in the main window or under the Command menu to transmit changes.
+
+
+2. Test Commands¶
+Connect directly to the station via terminal console or socket connection (e.g. minicom or nc) and send raw protocol frames:
+# Set channel 1 to ON
+<rs#ch#1#on#end>
+
+
+
Dependencies¶
-
-- restat requires next modules and libraries
in progress
-
-
+
+Firmware (restat_base)¶
+
+Raspberry Pi Pico SDK (v1.5.0+)
+GCC ARM Embedded Toolchain (arm-none-eabi-gcc)
+CMake & GNU Make
+
+
+
+Desktop GUI (restatdesk)¶
+
+C++23 compatible compiler (GCC 13+)
+gtkmm-4.0 & libgtk-4-dev
+libserial-dev
+pkg-config
+GNU Make
+
+
-Project structure¶
-restat is based on POP.
-in progress
-
-
+Project Structure¶
+The codebase layout is structured as follows:
+
+sw/restat_base/ - Core Pico firmware:
+* src/command/ - Dispatcher and frame parser.
+* src/device/ - Watchdog, buzzer, relay, and flash persistence drivers.
+sw/restat_base_tests/ - C++ GoogleTest suite for host-based firmware logic validation.
+sw/restatdesk/ - GTK 4 client:
+* com/ - Serial, TCP, and BLE client layers.
+* config/ - File configuration load/store manager.
+* model/ - Relay delegates, channel states, and business logic.
+* view/ - GUI window tabs, widgets, and GResource styled assets.
+sw/restatdesk_tests/ - C++ GoogleTest suite for desktop client controllers and helpers.
+docs/ - Documentation source files.
+
Copyright and licence¶
Copyright (C) 2020 - 2024 by electux.github.io/restat
+Lets help and support Raspberry PI && GNOME.
+
+
@@ -81,9 +242,28 @@ Copyright and licenceTable of Contents
- restat
-- Installation
-- Dependencies
-- Project structure
+- System Components
+
+- Command Interface (restat Protocol)
+- Installation
+
+- Usage
+
+- Dependencies
+
+- Project Structure
- Copyright and licence
diff --git a/docs/build/html/searchindex.js b/docs/build/html/searchindex.js
index 8af1f4d..3e0d4b9 100644
--- a/docs/build/html/searchindex.js
+++ b/docs/build/html/searchindex.js
@@ -1 +1 @@
-Search.setIndex({"docnames": ["index"], "filenames": ["index.rst"], "titles": ["restat"], "terms": {"i": 0, "control": 0, "relai": 0, "interfac": 0, "station": 0, "automat": 0, "The": 0, "readm": 0, "us": 0, "introduc": 0, "tool": 0, "provid": 0, "instruct": 0, "how": 0, "ani": 0, "machin": 0, "mai": 0, "have": 0, "other": 0, "inform": 0, "should": 0, "befor": 0, "navig": 0, "releas": 0, "page": 0, "download": 0, "extract": 0, "archiv": 0, "To": 0, "type": 0, "follow": 0, "progress": 0, "requir": 0, "next": 0, "modul": 0, "librari": 0, "base": 0, "pop": 0, "c": 0, "2020": 0, "2024": 0, "electux": 0, "github": 0, "io": 0}, "objects": {}, "objtypes": {}, "objnames": {}, "titleterms": {"restat": 0, "instal": 0, "depend": 0, "project": 0, "structur": 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.ext.viewcode": 1, "sphinx": 60}, "alltitles": {"restat": [[0, "restat"]], "Installation": [[0, "installation"]], "Dependencies": [[0, "dependencies"]], "Project structure": [[0, "project-structure"]], "Copyright and licence": [[0, "copyright-and-licence"]]}, "indexentries": {}})
\ No newline at end of file
+Search.setIndex({"docnames": ["index"], "filenames": ["index.rst"], "titles": ["restat"], "terms": {"i": 0, "control": 0, "relai": 0, "station": 0, "automat": 0, "The": 0, "readm": 0, "us": 0, "introduc": 0, "tool": 0, "provid": 0, "instruct": 0, "how": 0, "ani": 0, "machin": 0, "mai": 0, "have": 0, "other": 0, "inform": 0, "should": 0, "befor": 0, "consist": 0, "two": 0, "primari": 0, "A": 0, "low": 0, "level": 0, "c": 0, "design": 0, "pico": 0, "rp2040": 0, "microcontrol": 0, "actuat": 0, "time": 0, "persist": 0, "output": 0, "role": 0, "8": 0, "mechan": 0, "channel": 0, "high": 0, "precis": 0, "timer": 0, "configur": 0, "directli": 0, "onboard": 0, "flash": 0, "memori": 0, "featur": 0, "multi": 0, "dispatch": 0, "serial": 0, "tcp": 0, "ip": 0, "ble": 0, "variabl": 0, "execut": 0, "plan": 0, "parser": 0, "process": 0, "complex": 0, "string": 0, "concurr": 0, "set": 0, "state": 0, "schedul": 0, "base": 0, "store": 0, "5": 0, "mb": 0, "offset": 0, "0x180000": 0, "restor": 0, "upon": 0, "reboot": 0, "engin": 0, "millisecond": 0, "accuraci": 0, "mode": 0, "safeti": 0, "watchdog": 0, "sound": 0, "indic": 0, "recoveri": 0, "loop": 0, "coupl": 0, "activ": 0, "buzzer": 0, "signal": 0, "respons": 0, "cross": 0, "platform": 0, "built": 0, "gtkmm": 0, "4": 0, "gtk": 0, "monitor": 0, "real": 0, "led": 0, "visibl": 0, "green": 0, "statu": 0, "light": 0, "show": 0, "live": 0, "toggl": 0, "deactiv": 0, "click": 0, "sequenc": 0, "flexibl": 0, "start": 0, "ON": 0, "off": 0, "dynam": 0, "builder": 0, "onli": 0, "singl": 0, "length": 0, "protect": 0, "lifespan": 0, "sleek": 0, "black": 0, "dark": 0, "custom": 0, "css": 0, "style": 0, "compil": 0, "binari": 0, "gresourc": 0, "packag": 0, "multithread": 0, "log": 0, "visual": 0, "consol": 0, "file": 0, "all": 0, "sent": 0, "must": 0, "wrap": 0, "end": 0, "marker": 0, "r": 0, "target": 0, "action": 0, "param": 0, "descript": 0, "sy": 0, "id": 0, "identifi": 0, "return": 0, "board": 0, "identif": 0, "e": 0, "g": 0, "333": 0, "2023": 0, "0": 0, "version": 0, "get": 0, "v1": 0, "ch": 0, "x": 0, "tmr": 0, "m": 0, "turn": 0, "mask": 0, "10101010": 0, "plan_str": 0, "send": 0, "contain": 0, "save": 0, "stat": 0, "dump": 0, "reset": 0, "perform": 0, "soft": 0, "setup": 0, "raspberri": 0, "pi": 0, "sdk": 0, "your": 0, "host": 0, "build": 0, "cd": 0, "sw": 0, "mkdir": 0, "p": 0, "cmake": 0, "make": 0, "j": 0, "nproc": 0, "boot": 0, "bootsel": 0, "copi": 0, "uf2": 0, "mass": 0, "storag": 0, "volum": 0, "ensur": 0, "23": 0, "librari": 0, "ar": 0, "run": 0, "readi": 0, "insid": 0, "directori": 0, "launch": 0, "client": 0, "connect": 0, "window": 0, "option": 0, "ui": 0, "main": 0, "under": 0, "menu": 0, "transmit": 0, "chang": 0, "via": 0, "termin": 0, "socket": 0, "minicom": 0, "nc": 0, "raw": 0, "frame": 0, "gcc": 0, "arm": 0, "embed": 0, "toolchain": 0, "none": 0, "eabi": 0, "gnu": 0, "compat": 0, "13": 0, "libgtk": 0, "dev": 0, "libseri": 0, "pkg": 0, "config": 0, "codebas": 0, "layout": 0, "follow": 0, "core": 0, "src": 0, "devic": 0, "driver": 0, "restat_base_test": 0, "googletest": 0, "suit": 0, "logic": 0, "valid": 0, "com": 0, "layer": 0, "load": 0, "manag": 0, "model": 0, "deleg": 0, "busi": 0, "view": 0, "tab": 0, "widget": 0, "asset": 0, "restatdesk_test": 0, "helper": 0, "doc": 0, "document": 0, "sourc": 0, "2020": 0, "2024": 0, "electux": 0, "github": 0, "io": 0, "let": 0, "help": 0, "support": 0, "gnome": 0}, "objects": {}, "objtypes": {}, "objnames": {}, "titleterms": {"restat": 0, "system": 0, "compon": 0, "1": 0, "firmwar": 0, "restat_bas": 0, "2": 0, "restatdesk": 0, "gui": 0, "applic": 0, "command": 0, "interfac": 0, "protocol": 0, "instal": 0, "desktop": 0, "usag": 0, "panel": 0, "test": 0, "depend": 0, "project": 0, "structur": 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.ext.viewcode": 1, "sphinx": 60}, "alltitles": {"restat": [[0, "restat"]], "System Components": [[0, "system-components"]], "1. restat Firmware (restat_base)": [[0, "restat-firmware-restat-base"]], "2. restatdesk GUI Application (restatdesk)": [[0, "restatdesk-gui-application-restatdesk"]], "Command Interface (restat Protocol)": [[0, "command-interface-restat-protocol"]], "Installation": [[0, "installation"]], "1. Firmware (restat_base)": [[0, "firmware-restat-base"]], "2. Desktop Application (restatdesk)": [[0, "desktop-application-restatdesk"]], "Usage": [[0, "usage"]], "1. GUI Panel": [[0, "gui-panel"]], "2. Test Commands": [[0, "test-commands"]], "Dependencies": [[0, "dependencies"]], "Firmware (restat_base)": [[0, "id1"]], "Desktop GUI (restatdesk)": [[0, "desktop-gui-restatdesk"]], "Project Structure": [[0, "project-structure"]], "Copyright and licence": [[0, "copyright-and-licence"]]}, "indexentries": {}})
\ No newline at end of file
diff --git a/docs/foundations.png b/docs/foundations.png
new file mode 100644
index 0000000..40cb7e6
Binary files /dev/null and b/docs/foundations.png differ
diff --git a/docs/source/conf.py b/docs/source/conf.py
index 81747f1..fb0e043 100755
--- a/docs/source/conf.py
+++ b/docs/source/conf.py
@@ -21,11 +21,10 @@
import sys
from os.path import abspath
-from typing import Any, List, Dict, Tuple
__author__ = 'Vladimir Roncevic'
__copyright__ = '(C) 2024, https://vroncevic.github.io/restat'
-__credits__: List[str] = ['Vladimir Roncevic', 'Python Software Foundation']
+__credits__: list[str] = ['Vladimir Roncevic', 'Python Software Foundation']
__license__ = 'https://github.com/vroncevic/restat/blob/dev/LICENSE'
__version__ = '1.0.0'
__maintainer__ = 'Vladimir Roncevic'
@@ -39,30 +38,30 @@
author: str = 'Vladimir Roncevic '
version: str = '1.0.0'
release: str = 'https://github.com/vroncevic/restat/releases'
-extensions: List[str] = ['sphinx.ext.autodoc', 'sphinx.ext.viewcode', ]
-templates_path: List[str] = ['_templates']
+extensions: list[str] = ['sphinx.ext.autodoc', 'sphinx.ext.viewcode', ]
+templates_path: list[str] = ['_templates']
source_suffix: str = '.rst'
master_doc: str = 'index'
language: str = 'en'
-html_static_path: List[str] = ['_static']
-exclude_patterns: List[str] = []
+html_static_path: list[str] = ['_static']
+exclude_patterns: list[str] = []
pygments_style: str = 'sphinx'
html_theme: str = 'classic'
-html_static_path: List[str] = ['_static']
+html_static_path: list[str] = ['_static']
htmlhelp_basename: str = 'restatdoc'
-latex_elements: Dict[Any, Any] = {}
-latex_documents: List[Tuple[Any, ...]] = [(
+latex_elements: dict[object, object] = {}
+latex_documents: list[tuple[object, ...]] = [(
master_doc, 'restat.tex', 'restat Documentation',
'Vladimir Roncevic \\textless{}elektron.ronca@gmail.com\\textgreater{}',
'manual'
)]
-man_pages: List[Tuple[Any, ...]] = [(
+man_pages: list[tuple[object, ...]] = [(
master_doc, 'restat', 'restat Documentation', [author], 1
)]
-texinfo_documents: List[Tuple[Any, ...]] = [(
+texinfo_documents: list[tuple[object, ...]] = [(
master_doc, 'restat', 'restat Documentation',
author, 'restat', 'One line description of project.',
'Miscellaneous'
)]
epub_title: str = project
-epub_exclude_files: List[str] = ['search.html']
+epub_exclude_files: list[str] = ['search.html']
diff --git a/docs/source/index.rst b/docs/source/index.rst
index 23329a7..220f5c3 100755
--- a/docs/source/index.rst
+++ b/docs/source/index.rst
@@ -12,7 +12,7 @@ other information that should be provided before the tool is installed.
self
-|GitHub issues| |Documentation Status| |GitHub contributors|
+|GitHub issues| |Documentation Status| |GitHub contributors| |restat_base C checker| |restatdesk CC checker| |restatdesk build checker| |restat toc|
.. |GitHub issues| image:: https://img.shields.io/github/issues/electux/restat.svg
:target: https://github.com/electux/restat/issues
@@ -23,33 +23,168 @@ other information that should be provided before the tool is installed.
.. |Documentation Status| image:: https://readthedocs.org/projects/restat/badge/?version=latest
:target: https://restat.readthedocs.io/projects/restat/en/latest/?badge=latest
+.. |restat_base C checker| image:: https://github.com/electux/restat/actions/workflows/restat_base_c_checker.yml/badge.svg
+ :target: https://github.com/electux/restat/actions/workflows/restat_base_c_checker.yml
+
+.. |restatdesk CC checker| image:: https://github.com/electux/restat/actions/workflows/restat_desktop_cc_checker.yml/badge.svg
+ :target: https://github.com/electux/restat/actions/workflows/restat_desktop_cc_checker.yml
+
+.. |restatdesk build checker| image:: https://github.com/electux/restat/actions/workflows/restat_desktop_cc_checker_build.yml/badge.svg
+ :target: https://github.com/electux/restat/actions/workflows/restat_desktop_cc_checker_build.yml
+
+.. |restat toc| image:: https://github.com/electux/restat/actions/workflows/restat_toc.yml/badge.svg
+ :target: https://github.com/electux/restat/actions/workflows/restat_toc.yml
+
+System Components
+-----------------
+
+**restat** consists of two primary components:
+
+1. restat Firmware (``restat_base``)
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+A low-level C firmware designed for Pico RP2040 microcontrollers to actuate, time, and persist relay outputs.
+
+* **Role**: Actuates 8 mechanical relay channels using high-precision timers and persists configurations directly to onboard Flash memory.
+* **Features**:
+ * Multi-interface Command Dispatcher (Serial, TCP/IP, BLE).
+ * Variable Execution Plan Parser: Processes complex execution strings concurrently to set states and schedules.
+ * Flash-Based Plan Persistence: Automatically stores execution plans at a ``1.5 MB`` flash offset (``0x180000``) and restores configurations upon reboot.
+ * Precise Timing Engine: Millisecond-level accuracy for Timed modes.
+ * Safety Watchdog & Sound Indicators: Recovery loops coupled with active buzzer signaling.
+
+2. restatdesk GUI Application (``restatdesk``)
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+A responsive, cross-platform C++ application built on **gtkmm 4** and **GTK 4**.
+
+* **Role**: Configures, controls, and monitors the relay station in real-time.
+* **Features**:
+
+ * Real-Time LED Indicators: High-visibility green status lights showing live channel states.
+ * Configurable Relay Modes:
+
+ * **Toggle Mode**: Activates or deactivates relays on click.
+ * **Timer Mode**: Executes timed sequences with flexible starting states (Active = ON on Start, Deactive = OFF on Start).
+
+ * Dynamic Plan Builder: Serializes only active relay states into a single, variable-length plan command to protect relay lifespans.
+ * Sleek Green-on-Black Dark Mode: Custom CSS styles compiled directly into the binary as GResource packages.
+ * Multithreaded System Logs: Visualized console output and persistent file logging.
+
+
+Command Interface (restat Protocol)
+-----------------------------------
+
+All commands sent to the ``restat_base`` firmware must be wrapped in start/end markers: ````.
+
++-------------------------------------+------------------+-----------------------------------------------------------------------------------+
+| Command | Action | Description |
++=====================================+==================+===================================================================================+
+| ```` | Identify | Returns board identification (e.g. ````) |
++-------------------------------------+------------------+-----------------------------------------------------------------------------------+
+| ```` | Get Version | Returns firmware version (e.g. ````) |
++-------------------------------------+------------------+-----------------------------------------------------------------------------------+
+| ```` | Channel ON | Sets channel ``X`` (1-8) to ON (active) state |
++-------------------------------------+------------------+-----------------------------------------------------------------------------------+
+| ```` | Channel OFF | Sets channel ``X`` (1-8) to OFF (deactive) state |
++-------------------------------------+------------------+-----------------------------------------------------------------------------------+
+| ```` | Timed Channel | Starts timer on channel ``X`` for ``MS`` milliseconds starting ON |
++-------------------------------------+------------------+-----------------------------------------------------------------------------------+
+| ```` | All ON | Turns all channels ON concurrently |
++-------------------------------------+------------------+-----------------------------------------------------------------------------------+
+| ```` | All OFF | Turns all channels OFF concurrently |
++-------------------------------------+------------------+-----------------------------------------------------------------------------------+
+| ```` | Binary Mask | Sets all 8 channels to binary state ``MASK`` (e.g. ``10101010``) |
++-------------------------------------+------------------+-----------------------------------------------------------------------------------+
+| ```` | Execute Plan | Sends a variable-length configuration plan containing relay states and saves |
++-------------------------------------+------------------+-----------------------------------------------------------------------------------+
+| ```` | Channel Status | Returns the status string of channel ``X`` |
++-------------------------------------+------------------+-----------------------------------------------------------------------------------+
+| ```` | All Status | Returns status dump for all channels |
++-------------------------------------+------------------+-----------------------------------------------------------------------------------+
+| ```` | Reset | Performs soft system reset using watchdog |
++-------------------------------------+------------------+-----------------------------------------------------------------------------------+
+
Installation
--------------
+------------
+
+1. Firmware (``restat_base``)
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+Setup the Raspberry Pi Pico SDK on your host system, then build:
+
+.. code-block:: bash
-Navigate to release `page`_ download and extract release archive.
+ cd sw/restat_base
+ mkdir -p build && cd build
+ cmake ..
+ make -j$(nproc)
-.. _page: https://github.com/electux/restat/releases
+Boot the board in BOOTSEL mode and copy the compiled ``restat_base.uf2`` to the Pico mass storage volume.
-To install **restat** type the following
+2. Desktop Application (``restatdesk``)
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+Ensure all C++23 compiler tools and GTK 4 libraries are installed, then run:
.. code-block:: bash
- in progress
+ cd sw/restatdesk/build
+ make all
-Dependencies
--------------
+The executable ``restatdesk`` will be ready inside the ``build/`` directory.
+
+Usage
+-----
+
+1. GUI Panel
+~~~~~~~~~~~~
+Launch the desktop client:
-**restat** requires next modules and libraries
- in progress
+.. code-block:: bash
+
+ ./sw/restatdesk/build/restatdesk
-Project structure
-------------------
+Configure the connection (Serial, TCP/IP, or BLE) in the Settings window, toggle active relay options in the UI, and click **Execute Plan** in the main window or under the ``Command`` menu to transmit changes.
-**restat** is based on POP.
+2. Test Commands
+~~~~~~~~~~~~~~~~
+Connect directly to the station via terminal console or socket connection (e.g. ``minicom`` or ``nc``) and send raw protocol frames:
.. code-block:: bash
- in progress
+ # Set channel 1 to ON
+
+
+Dependencies
+------------
+
+Firmware (``restat_base``)
+~~~~~~~~~~~~~~~~~~~~~~~~~~
+* Raspberry Pi Pico SDK (v1.5.0+)
+* GCC ARM Embedded Toolchain (``arm-none-eabi-gcc``)
+* CMake & GNU Make
+
+Desktop GUI (``restatdesk``)
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+* C++23 compatible compiler (GCC 13+)
+* ``gtkmm-4.0`` & ``libgtk-4-dev``
+* ``libserial-dev``
+* ``pkg-config``
+* GNU Make
+
+Project Structure
+-----------------
+
+The codebase layout is structured as follows:
+
+* `sw/restat_base/ `_ - Core Pico firmware:
+ * ``src/command/`` - Dispatcher and frame parser.
+ * ``src/device/`` - Watchdog, buzzer, relay, and flash persistence drivers.
+* `sw/restat_base_tests/ `_ - C++ GoogleTest suite for host-based firmware logic validation.
+* `sw/restatdesk/ `_ - GTK 4 client:
+ * ``com/`` - Serial, TCP, and BLE client layers.
+ * ``config/`` - File configuration load/store manager.
+ * ``model/`` - Relay delegates, channel states, and business logic.
+ * ``view/`` - GUI window tabs, widgets, and GResource styled assets.
+* `sw/restatdesk_tests/ `_ - C++ GoogleTest suite for desktop client controllers and helpers.
+* `docs/ `_ - Documentation source files.
Copyright and licence
----------------------
@@ -64,3 +199,11 @@ Copyright and licence
Copyright (C) 2020 - 2024 by `electux.github.io/restat `_
+Lets help and support Raspberry PI && GNOME.
+
+.. image:: https://raw.githubusercontent.com/electux/restat/master/docs/foundations.png
+ :alt: Foundations
+ :width: 192px
+ :height: 109px
+
+
diff --git a/restat.code-workspace b/restat.code-workspace
index 876a149..2b13ffd 100644
--- a/restat.code-workspace
+++ b/restat.code-workspace
@@ -4,5 +4,16 @@
"path": "."
}
],
- "settings": {}
+ "settings": {
+ "files.exclude": {
+ "**/*.o": true,
+ "**/*.lss": true,
+ "**/core": true,
+ "**/resources.cc": true,
+ "**/build/": true,
+ "**/.built": true,
+ "**/.depend": true,
+ "**/Make.dep": true
+ }
+ }
}
\ No newline at end of file
diff --git a/sw/.editorconfig b/sw/.editorconfig
index ffea4cf..72716a4 100644
--- a/sw/.editorconfig
+++ b/sw/.editorconfig
@@ -1,3 +1,3 @@
-[**.{c,h,cfg}]
+[**.*]
indent_style = tab
tab_width = 4
diff --git a/sw/restat-desktop/build/Makefile b/sw/restat-desktop/build/Makefile
deleted file mode 100644
index 493adf7..0000000
--- a/sw/restat-desktop/build/Makefile
+++ /dev/null
@@ -1,28 +0,0 @@
-#
-# @brief restat-desktop
-# @version 1.0.0
-# @date Mon 13 Dec 2021 11:45:04 PM CET
-# @company None, free software to use 2021
-# @author Vladimir Roncevic
-#
-
-CC=g++
-RM=rm
-
-include ccflags.mk
-include ldflags.mk
-include sources.mk
-include objects.mk
-
-EXECUTABLE=restat-desktop
-
-all: $(SOURCES) $(EXECUTABLE)
-
-$(EXECUTABLE): $(OBJECTS)
- $(CC) $(LDFLAGS) $(OBJECTS) -o $@ `pkg-config gtkmm-3.0 --libs`
-
-.cc.o:
- $(CC) $(CFLAGS) $< -o $@ `pkg-config gtkmm-3.0 --cflags`
-
-clean:
- $(RM) -rf ../*.o $(EXECUTABLE)
diff --git a/sw/restat-desktop/build/ccflags.mk b/sw/restat-desktop/build/ccflags.mk
deleted file mode 100644
index ce0af30..0000000
--- a/sw/restat-desktop/build/ccflags.mk
+++ /dev/null
@@ -1,9 +0,0 @@
-#
-# @brief restat-desktop
-# @version 1.0.0
-# @date Mon 13 Dec 2021 11:45:04 PM CET
-# @company None, free software to use 2021
-# @author Vladimir Roncevic
-#
-
-CFLAGS=-c -Wall
diff --git a/sw/restat-desktop/build/ldflags.mk b/sw/restat-desktop/build/ldflags.mk
deleted file mode 100644
index 3dd0f64..0000000
--- a/sw/restat-desktop/build/ldflags.mk
+++ /dev/null
@@ -1,9 +0,0 @@
-#
-# @brief restat-desktop
-# @version 1.0.0
-# @date Mon 13 Dec 2021 11:45:04 PM CET
-# @company None, free software to use 2021
-# @author Vladimir Roncevic
-#
-
-LDFLAGS=
diff --git a/sw/restat-desktop/build/objects.mk b/sw/restat-desktop/build/objects.mk
deleted file mode 100644
index 851491b..0000000
--- a/sw/restat-desktop/build/objects.mk
+++ /dev/null
@@ -1,9 +0,0 @@
-#
-# @brief restat-desktop
-# @version 1.0.0
-# @date Mon 13 Dec 2021 11:45:04 PM CET
-# @company None, free software to use 2021
-# @author Vladimir Roncevic
-#
-
-OBJECTS=$(SOURCES:.cc=.o)
diff --git a/sw/restat-desktop/build/sources.mk b/sw/restat-desktop/build/sources.mk
deleted file mode 100644
index b0908c6..0000000
--- a/sw/restat-desktop/build/sources.mk
+++ /dev/null
@@ -1,9 +0,0 @@
-#
-# @brief restat-desktop
-# @version 1.0.0
-# @date Mon 13 Dec 2021 11:45:04 PM CET
-# @company None, free software to use 2021
-# @author Vladimir Roncevic
-#
-
-SOURCES=../restat_desktop.cc ../main.cc
diff --git a/sw/restat-desktop/main.cc b/sw/restat-desktop/main.cc
deleted file mode 100644
index ffcd131..0000000
--- a/sw/restat-desktop/main.cc
+++ /dev/null
@@ -1,27 +0,0 @@
-/* -*- Mode: C; indent-tabs-mode: t; c-basic-offset: 4; tab-width: 4 -*- */
-/*
- * main.cc
- * Copyright (C) 2021 Vladimir Roncevic
- *
- * restat 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.
- *
- * restat 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 "restat_desktop.h"
-#include
-
-int main(int argc, char *argv[])
-{
- auto app = Gtk::Application::create(argc, argv, "org.gtkmm.restat");
- Restat window_instance;
- return app->run(window_instance);
-}
diff --git a/sw/restat-desktop/restat_desktop.cc b/sw/restat-desktop/restat_desktop.cc
deleted file mode 100644
index c54cf28..0000000
--- a/sw/restat-desktop/restat_desktop.cc
+++ /dev/null
@@ -1,32 +0,0 @@
-/* -*- Mode: C; indent-tabs-mode: t; c-basic-offset: 4; tab-width: 4 -*- */
-/*
- * restat_desktop.cc
- * Copyright (C) 2021 Vladimir Roncevic
- *
- * restat 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.
- *
- * restat 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 "restat_desktop.h"
-
-Restat::Restat()
-{
- this->set_title(RESTAT_TITLE);
- this->set_border_width(RESTAT_BORDER_WIDTH);
- this->add(this->main_grid);
-}
-
-Restat::~Restat()
-{
- std::cout << "Clean up code/data" << std::endl;
-}
diff --git a/sw/restat-desktop/restat_desktop.h b/sw/restat-desktop/restat_desktop.h
deleted file mode 100644
index 5387525..0000000
--- a/sw/restat-desktop/restat_desktop.h
+++ /dev/null
@@ -1,40 +0,0 @@
-/* -*- Mode: C; indent-tabs-mode: t; c-basic-offset: 4; tab-width: 4 -*- */
-/*
- * restat_desktop.h
- * Copyright (C) 2021 Vladimir Roncevic
- *
- * restat 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.
- *
- * restat 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 .
- */
-
-#ifndef RESTAT_DESKTOP_H
-#define RESTAT_DESKTOP_H
-
-#include
-#include
-
-#define RESTAT_TITLE "restat"
-#define RESTAT_VERSION "1.0.0"
-#define RESTAT_BORDER_WIDTH 10
-
-class Restat : public Gtk::Window
-{
-public:
- Restat();
- ~Restat();
-
-protected:
- Gtk::Grid main_grid;
-};
-
-#endif
diff --git a/sw/restat-device0/CMakeLists.txt b/sw/restat-device0/CMakeLists.txt
deleted file mode 100644
index 8a6008e..0000000
--- a/sw/restat-device0/CMakeLists.txt
+++ /dev/null
@@ -1,27 +0,0 @@
-#
-# @brief restat main entry point for build process
-# @version v1.0.3
-# @date 2021-12-14
-# @author Vladimir Roncevic
-#
-
-# Setup CMake version and RPI Pico SDK import
-cmake_minimum_required(VERSION 3.12)
-include(pico_sdk_import.cmake)
-
-# Setup project info
-project(restat-device0 C CXX ASM)
-set(CMAKE_PROJECT_VERSION 1.0.0)
-set(CMAKE_C_STANDARD 11)
-set(CMAKE_CXX_STANDARD 17)
-set(PRO_PATH ${PROJECT_SOURCE_DIR})
-
-# Initialize the SDK
-pico_sdk_init()
-
-# Setup project url
-include(pro_auto_set_url.cmake)
-
-# Add source directory
-add_subdirectory(src)
-
diff --git a/sw/restat-device0/src/CMakeLists.txt b/sw/restat-device0/src/CMakeLists.txt
deleted file mode 100644
index f2a066c..0000000
--- a/sw/restat-device0/src/CMakeLists.txt
+++ /dev/null
@@ -1,19 +0,0 @@
-#
-# @brief restat setup output files
-# @version v1.0.3
-# @date 2021-12-14
-# @author Vladimir Roncevic
-#
-
-# Create executable
-add_executable("${CMAKE_PROJECT_NAME}" main.c)
-
-# Pull in our pico_stdlib which pulls in commonly used features
-target_link_libraries("${CMAKE_PROJECT_NAME}" pico_stdlib)
-
-# Create extra output files map/bin/hex
-pico_add_extra_outputs("${CMAKE_PROJECT_NAME}")
-
-# Add url via pico_set_program_url
-pro_auto_set_url("${CMAKE_PROJECT_NAME}")
-
diff --git a/sw/restat-device0/src/main.c b/sw/restat-device0/src/main.c
deleted file mode 100644
index ecf1c0d..0000000
--- a/sw/restat-device0/src/main.c
+++ /dev/null
@@ -1,37 +0,0 @@
-/* -*- Mode: C; indent-tabs-mode: t; c-basic-offset: 4; tab-width: 4 -*- */
-/*
- * main.c
- * Copyright (C) 2021 Vladimir Roncevic
- *
- * restat 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.
- *
- * restat 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_name. If not, see .
- */
-
-#include "pico/stdlib.h"
-
-int main() {
-#ifndef PICO_DEFAULT_LED_PIN
-#warning blink example requires a board with a regular LED
-#else
- const uint LED_PIN = PICO_DEFAULT_LED_PIN;
- gpio_init(LED_PIN);
- gpio_set_dir(LED_PIN, GPIO_OUT);
- while (true) {
- gpio_put(LED_PIN, 1);
- sleep_ms(250);
- gpio_put(LED_PIN, 0);
- sleep_ms(250);
- }
-#endif
-}
-
diff --git a/sw/restat-device1/CMakeLists.txt b/sw/restat-device1/CMakeLists.txt
deleted file mode 100644
index 0338f96..0000000
--- a/sw/restat-device1/CMakeLists.txt
+++ /dev/null
@@ -1,27 +0,0 @@
-#
-# @brief restat main entry point for build process
-# @version v1.0.3
-# @date 2021-12-14
-# @author Vladimir Roncevic
-#
-
-# Setup CMake version and RPI Pico SDK import
-cmake_minimum_required(VERSION 3.12)
-include(pico_sdk_import.cmake)
-
-# Setup project info
-project(restat-device1 C CXX ASM)
-set(CMAKE_PROJECT_VERSION 1.0.0)
-set(CMAKE_C_STANDARD 11)
-set(CMAKE_CXX_STANDARD 17)
-set(PRO_PATH ${PROJECT_SOURCE_DIR})
-
-# Initialize the SDK
-pico_sdk_init()
-
-# Setup project url
-include(pro_auto_set_url.cmake)
-
-# Add source directory
-add_subdirectory(src)
-
diff --git a/sw/restat-device1/pico_sdk_import.cmake b/sw/restat-device1/pico_sdk_import.cmake
deleted file mode 100644
index d36d2a9..0000000
--- a/sw/restat-device1/pico_sdk_import.cmake
+++ /dev/null
@@ -1,97 +0,0 @@
-#
-# @brief restat RPI Pico SDK import
-# @version v1.0.3
-# @date 2021-12-14
-# @author Vladimir Roncevic
-#
-
-# Checking SDK path
-if (DEFINED ENV{PICO_SDK_PATH} AND (NOT PICO_SDK_PATH))
- set(PICO_SDK_PATH $ENV{PICO_SDK_PATH})
- message("Using PICO_SDK_PATH from environment ('${PICO_SDK_PATH}')")
-endif ()
-
-# Checking SDK path fetched from GIT
-if (DEFINED ENV{PICO_SDK_FETCH_FROM_GIT} AND (NOT PICO_SDK_FETCH_FROM_GIT))
- set(PICO_SDK_FETCH_FROM_GIT $ENV{PICO_SDK_FETCH_FROM_GIT})
- message(
- "Using PICO_SDK_FETCH_FROM_GIT"
- "from environment ('${PICO_SDK_FETCH_FROM_GIT}')"
- )
-endif ()
-
-if (DEFINED ENV{PICO_SDK_FETCH_FROM_GIT_PATH} AND (NOT PICO_SDK_FETCH_FROM_GIT_PATH))
- set(PICO_SDK_FETCH_FROM_GIT_PATH $ENV{PICO_SDK_FETCH_FROM_GIT_PATH})
- message(
- "Using PICO_SDK_FETCH_FROM_GIT_PATH"
- "from environment ('${PICO_SDK_FETCH_FROM_GIT_PATH}')"
- )
-endif ()
-
-# Setup RPI PICO SDK path
-set(
- PICO_SDK_PATH "${PICO_SDK_PATH}" CACHE PATH "Path to the RPI Pi Pico SDK"
-)
-set(
- PICO_SDK_FETCH_FROM_GIT "${PICO_SDK_FETCH_FROM_GIT}"
- CACHE BOOL
- "Set to ON to fetch copy of SDK from git if not otherwise locatable"
-)
-set(
- PICO_SDK_FETCH_FROM_GIT_PATH "${PICO_SDK_FETCH_FROM_GIT_PATH}"
- CACHE FILEPATH "location to download SDK"
-)
-
-# Fetch RPI PI SDK from GitHub
-if (NOT PICO_SDK_PATH)
- if (PICO_SDK_FETCH_FROM_GIT)
- include(FetchContent)
- set(FETCHCONTENT_BASE_DIR_SAVE ${FETCHCONTENT_BASE_DIR})
- if (PICO_SDK_FETCH_FROM_GIT_PATH)
- get_filename_component(
- FETCHCONTENT_BASE_DIR "${PICO_SDK_FETCH_FROM_GIT_PATH}"
- REALPATH BASE_DIR "${CMAKE_SOURCE_DIR}"
- )
- endif ()
- FetchContent_Declare(
- pico_sdk
- GIT_REPOSITORY https://github.com/RPIpi/pico-sdk
- GIT_TAG master
- )
- if (NOT pico_sdk)
- message("Downloading RPI Pi Pico SDK")
- FetchContent_Populate(pico_sdk)
- set(PICO_SDK_PATH ${pico_sdk_SOURCE_DIR})
- endif ()
- set(FETCHCONTENT_BASE_DIR ${FETCHCONTENT_BASE_DIR_SAVE})
- else ()
- message(
- FATAL_ERROR
- "SDK location was not specified."
- "Please set PICO_SDK_PATH or set PICO_SDK_FETCH_FROM_GIT."
- )
- endif ()
-endif ()
-
-get_filename_component(
- PICO_SDK_PATH "${PICO_SDK_PATH}" REALPATH BASE_DIR "${CMAKE_BINARY_DIR}"
-)
-if (NOT EXISTS ${PICO_SDK_PATH})
- message(FATAL_ERROR "Directory '${PICO_SDK_PATH}' not found")
-endif ()
-
-set(PICO_SDK_INIT_CMAKE_FILE ${PICO_SDK_PATH}/pico_sdk_init.cmake)
-
-# Check RPI PI SDK init configuration
-if (NOT EXISTS ${PICO_SDK_INIT_CMAKE_FILE})
- message(
- FATAL_ERROR
- "Dir '${PICO_SDK_PATH}' does not appear to contain RPI Pi Pico SDK"
- )
-endif ()
-
-set(
- PICO_SDK_PATH ${PICO_SDK_PATH} CACHE PATH "Path to RPI Pi Pico SDK" FORCE
-)
-
-include(${PICO_SDK_INIT_CMAKE_FILE})
diff --git a/sw/restat-device1/pro_auto_set_url.cmake b/sw/restat-device1/pro_auto_set_url.cmake
deleted file mode 100644
index fa80ae1..0000000
--- a/sw/restat-device1/pro_auto_set_url.cmake
+++ /dev/null
@@ -1,15 +0,0 @@
-#
-# @brief restat setup project url
-# @version v1.0.3
-# @date 2021-12-14
-# @author Vladimir Roncevic
-#
-
-# Macro for setup project url
-macro(pro_auto_set_url TARGET)
- file(
- RELATIVE_PATH URL_REL_PATH "${PRO_PATH}" "${CMAKE_CURRENT_LIST_DIR}"
- )
- pico_set_program_url(${TARGET} "${URL_REL_PATH}")
-endmacro()
-
diff --git a/sw/restat-device1/src/CMakeLists.txt b/sw/restat-device1/src/CMakeLists.txt
deleted file mode 100644
index f2a066c..0000000
--- a/sw/restat-device1/src/CMakeLists.txt
+++ /dev/null
@@ -1,19 +0,0 @@
-#
-# @brief restat setup output files
-# @version v1.0.3
-# @date 2021-12-14
-# @author Vladimir Roncevic
-#
-
-# Create executable
-add_executable("${CMAKE_PROJECT_NAME}" main.c)
-
-# Pull in our pico_stdlib which pulls in commonly used features
-target_link_libraries("${CMAKE_PROJECT_NAME}" pico_stdlib)
-
-# Create extra output files map/bin/hex
-pico_add_extra_outputs("${CMAKE_PROJECT_NAME}")
-
-# Add url via pico_set_program_url
-pro_auto_set_url("${CMAKE_PROJECT_NAME}")
-
diff --git a/sw/restat-device1/src/main.c b/sw/restat-device1/src/main.c
deleted file mode 100644
index ecf1c0d..0000000
--- a/sw/restat-device1/src/main.c
+++ /dev/null
@@ -1,37 +0,0 @@
-/* -*- Mode: C; indent-tabs-mode: t; c-basic-offset: 4; tab-width: 4 -*- */
-/*
- * main.c
- * Copyright (C) 2021 Vladimir Roncevic
- *
- * restat 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.
- *
- * restat 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_name. If not, see .
- */
-
-#include "pico/stdlib.h"
-
-int main() {
-#ifndef PICO_DEFAULT_LED_PIN
-#warning blink example requires a board with a regular LED
-#else
- const uint LED_PIN = PICO_DEFAULT_LED_PIN;
- gpio_init(LED_PIN);
- gpio_set_dir(LED_PIN, GPIO_OUT);
- while (true) {
- gpio_put(LED_PIN, 1);
- sleep_ms(250);
- gpio_put(LED_PIN, 0);
- sleep_ms(250);
- }
-#endif
-}
-
diff --git a/sw/restat_base/.clang-format b/sw/restat_base/.clang-format
new file mode 100644
index 0000000..e89be99
--- /dev/null
+++ b/sw/restat_base/.clang-format
@@ -0,0 +1,20 @@
+BasedOnStyle: LLVM
+IndentWidth: 2
+TabWidth: 2
+UseTab: Never
+ColumnLimit: 80
+
+# Align function call arguments and parameters to block indent (closing parenthesis on a new line)
+AlignAfterOpenBracket: BlockIndent
+
+# Keep C++11 braced lists formatted nicely
+Cpp11BracedListStyle: true
+
+# Brace wrapping settings (Attach style, open braces on the same line)
+BreakBeforeBraces: Attach
+
+# Other styling preferences
+AllowShortBlocksOnASingleLine: Never
+AllowShortFunctionsOnASingleLine: None
+AllowShortIfStatementsOnASingleLine: Never
+AllowShortLoopsOnASingleLine: false
diff --git a/sw/restat-desktop/.editorconfig b/sw/restat_base/.editorconfig
similarity index 100%
rename from sw/restat-desktop/.editorconfig
rename to sw/restat_base/.editorconfig
diff --git a/sw/restat_base/.vscode/c_cpp_properties.json b/sw/restat_base/.vscode/c_cpp_properties.json
new file mode 100644
index 0000000..bb410fd
--- /dev/null
+++ b/sw/restat_base/.vscode/c_cpp_properties.json
@@ -0,0 +1,17 @@
+{
+ "configurations": [
+ {
+ "name": "restat-base",
+ "includePath": [
+ "${workspaceFolder}/**"
+ ],
+ "defines": [],
+ "compilerPath": "/usr/bin/arm-none-eabi-gcc",
+ "intelliSenseMode": "linux-gcc-arm",
+ "cStandard": "c11",
+ "cppStandard": "c++17",
+ "compileCommands": "${workspaceFolder}/compile_commands.json"
+ }
+ ],
+ "version": 4
+}
diff --git a/sw/restat_base/.vscode/settings.json b/sw/restat_base/.vscode/settings.json
new file mode 100644
index 0000000..0c04c53
--- /dev/null
+++ b/sw/restat_base/.vscode/settings.json
@@ -0,0 +1,3 @@
+{
+ "cmake.configureOnOpen": false
+}
diff --git a/sw/restat_base/.vscode/tasks.json b/sw/restat_base/.vscode/tasks.json
new file mode 100644
index 0000000..339e2f8
--- /dev/null
+++ b/sw/restat_base/.vscode/tasks.json
@@ -0,0 +1,26 @@
+{
+ "version": "2.0.0",
+ "tasks": [
+ {
+ "label": "restat-base build",
+ "type": "shell",
+ "command": "",
+ "args": [
+ "make",
+ "--directory=${workspaceFolder}/build"
+ ],
+ "problemMatcher": []
+ },
+ {
+ "label": "restat-base clean",
+ "type": "shell",
+ "command": "",
+ "args": [
+ "make",
+ "--directory=${workspaceFolder}/build",
+ "clean"
+ ],
+ "problemMatcher": []
+ }
+ ]
+}
diff --git a/sw/restat_base/CMakeLists.txt b/sw/restat_base/CMakeLists.txt
new file mode 100644
index 0000000..865b59c
--- /dev/null
+++ b/sw/restat_base/CMakeLists.txt
@@ -0,0 +1,33 @@
+#
+# @brief restat-base main entry point for build process
+# @version v1.0.3
+# @date 2021-12-15
+# @author Vladimir Roncevic
+#
+
+# Sets CMake version
+cmake_minimum_required(VERSION 3.12)
+
+# Pico SDK import setup
+include(pico_sdk_import.cmake)
+
+# Sets project info
+project(restat-base C CXX ASM)
+set(CMAKE_EXPORT_COMPILE_COMMANDS ON)
+set(CMAKE_PROJECT_VERSION 1.0.0)
+set(CMAKE_C_STANDARD 11)
+set(CMAKE_CXX_STANDARD 17)
+set(PRO_PATH ${PROJECT_SOURCE_DIR})
+set(PICO_BOARD "pico")
+
+# Initializes the SDK
+pico_sdk_init()
+
+# Adds linker flags
+add_link_options(-Wl,-gc-sections,--print-memory-usage)
+
+# Sets project url
+include(pro_auto_set_url.cmake)
+
+# Adds source directory
+add_subdirectory(src)
diff --git a/sw/restat-device0/pico_sdk_import.cmake b/sw/restat_base/pico_sdk_import.cmake
similarity index 96%
rename from sw/restat-device0/pico_sdk_import.cmake
rename to sw/restat_base/pico_sdk_import.cmake
index d36d2a9..5acd121 100644
--- a/sw/restat-device0/pico_sdk_import.cmake
+++ b/sw/restat_base/pico_sdk_import.cmake
@@ -1,10 +1,12 @@
#
-# @brief restat RPI Pico SDK import
+# @brief restat-base RPI Pico SDK import
# @version v1.0.3
-# @date 2021-12-14
+# @date 2021-12-15
# @author Vladimir Roncevic
#
+set(PICO_SDK_PATH "/opt/pico/pico-sdk")
+
# Checking SDK path
if (DEFINED ENV{PICO_SDK_PATH} AND (NOT PICO_SDK_PATH))
set(PICO_SDK_PATH $ENV{PICO_SDK_PATH})
diff --git a/sw/restat-device0/pro_auto_set_url.cmake b/sw/restat_base/pro_auto_set_url.cmake
similarity index 82%
rename from sw/restat-device0/pro_auto_set_url.cmake
rename to sw/restat_base/pro_auto_set_url.cmake
index fa80ae1..c7f5c96 100644
--- a/sw/restat-device0/pro_auto_set_url.cmake
+++ b/sw/restat_base/pro_auto_set_url.cmake
@@ -1,7 +1,7 @@
#
-# @brief restat setup project url
+# @brief restat-base setup project url
# @version v1.0.3
-# @date 2021-12-14
+# @date 2021-12-15
# @author Vladimir Roncevic
#
@@ -12,4 +12,3 @@ macro(pro_auto_set_url TARGET)
)
pico_set_program_url(${TARGET} "${URL_REL_PATH}")
endmacro()
-
diff --git a/sw/restat_base/restat-base.code-workspace b/sw/restat_base/restat-base.code-workspace
new file mode 100644
index 0000000..e80851b
--- /dev/null
+++ b/sw/restat_base/restat-base.code-workspace
@@ -0,0 +1,19 @@
+{
+ "folders": [
+ {
+ "path": "."
+ }
+ ],
+ "settings": {
+ "clangd.arguments": [
+ "--query-driver=/usr/bin/arm-none-eabi-*"
+ ],
+ "files.exclude": {
+ "**/*.o": true,
+ "**/.built": true,
+ "**/.depend": true,
+ "**/Make.dep": true,
+ "**/build/": true
+ }
+ }
+}
diff --git a/sw/restat_base/src/CMakeLists.txt b/sw/restat_base/src/CMakeLists.txt
new file mode 100644
index 0000000..8ac6de8
--- /dev/null
+++ b/sw/restat_base/src/CMakeLists.txt
@@ -0,0 +1,43 @@
+#
+# @brief restat-base setup output files
+# @version v1.0.3
+# @date 2021-12-15
+# @author Vladimir Roncevic
+#
+
+# Prepares PIO target for WS2812 (headers and implementation)
+pico_simple_hardware_target(pio)
+
+# Enable verbose logging globally
+add_compile_definitions(VERBOSE)
+
+# Adds sub-structure includes
+include_directories(.)
+include_directories(io)
+include_directories(ws2812)
+include_directories(device)
+include_directories(command)
+
+# Adds sub-structure sources
+add_subdirectory(io)
+add_subdirectory(ws2812)
+add_subdirectory(device)
+add_subdirectory(command)
+
+# Sets executable
+add_executable("${CMAKE_PROJECT_NAME}" main.c)
+
+# Enables USB output
+pico_enable_stdio_usb("${CMAKE_PROJECT_NAME}" 1)
+
+# Disables UART output
+pico_enable_stdio_uart("${CMAKE_PROJECT_NAME}" 0)
+
+# Links all libraries
+target_link_libraries("${CMAKE_PROJECT_NAME}" command device io ws2812)
+
+# Creates extra output files map/bin/hex
+pico_add_extra_outputs("${CMAKE_PROJECT_NAME}")
+
+# Adds URL via pico_set_program_url
+pro_auto_set_url("${CMAKE_PROJECT_NAME}")
diff --git a/sw/restat_base/src/command/CMakeLists.txt b/sw/restat_base/src/command/CMakeLists.txt
new file mode 100644
index 0000000..00311aa
--- /dev/null
+++ b/sw/restat_base/src/command/CMakeLists.txt
@@ -0,0 +1,9 @@
+#
+# @brief command library setup
+# @version v1.0.0
+# @date 2026-08-21
+#
+
+add_library(command parser.c command_tbl.c)
+target_include_directories(command PUBLIC .)
+target_link_libraries(command device pico_stdlib)
diff --git a/sw/restat_base/src/command/command_tbl.c b/sw/restat_base/src/command/command_tbl.c
new file mode 100644
index 0000000..2f84f15
--- /dev/null
+++ b/sw/restat_base/src/command/command_tbl.c
@@ -0,0 +1,289 @@
+/* -*- Mode: C; indent-tabs-mode: t; c-basic-offset: 4; tab-width: 4 -*- */
+/*
+ * command_tbl.c
+ * Copyright (C) 2025 Vladimir Roncevic
+ *
+ * restat-base 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.
+ *
+ * restat-base 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_name. If not, see .
+ */
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+static const char *const RESTAT_VERSION = "restat v1.0.0";
+static const char *const RESTAT_BOARD_ID = "rs:333:2023:0";
+
+static const uint8_t STATUS_LED_DEFAULT_VAL = 255;
+
+static const char CMD_CHANNEL_MIN_CHAR = '1';
+static const char CMD_CHANNEL_MAX_CHAR = '8';
+static const size_t CMD_CHANNEL_CHAR_INDEX = 6;
+
+static const size_t CMD_TIMER_ARGS_OFFSET = 12;
+static const size_t CMD_PULSE_ARGS_OFFSET = 14;
+static const size_t CMD_BLINK_ARGS_OFFSET = 14;
+static const size_t CMD_MASK_ARGS_OFFSET = 12;
+
+static const size_t CMD_MASK_SUFFIX_OFFSET = 20;
+
+static const size_t CMD_PREFIX_CH_LEN = 6;
+static const char *const CMD_PREFIX_CH = "rs#ch#";
+
+static const size_t CMD_TMR_LEN = 5;
+static const char *const CMD_TMR = "#tmr#";
+
+static const size_t CMD_PULSE_LEN = 7;
+static const char *const CMD_PULSE = "#pulse#";
+
+static const size_t CMD_BLINK_LEN = 7;
+static const char *const CMD_BLINK = "#blink#";
+
+static const char *const CMD_STAT_SUFFIX = "#stat#end";
+
+static const size_t CMD_PREFIX_MASK_LEN = 12;
+static const char *const CMD_PREFIX_MASK = "rs#all#mask#";
+
+static const char *const CMD_SUFFIX_END = "#end";
+
+typedef void (*cmd_handler_t)(const char *cmd_str);
+
+typedef struct {
+ const char *pattern;
+ cmd_handler_t handler;
+} command_entry_t;
+
+static void cmd_handle_board_id(const char *cmd_str) {
+ (void)cmd_str;
+ printf("", RESTAT_BOARD_ID);
+}
+
+static void cmd_handle_version(const char *cmd_str) {
+ (void)cmd_str;
+ printf("", RESTAT_VERSION);
+}
+
+static void cmd_handle_all_channels(const char *cmd_str) {
+ bool on = (strstr(cmd_str, "#on#") != NULL);
+
+ relay_set_all(on);
+ if (on) {
+ status_led_write(
+ STATUS_LED_DEFAULT_VAL, STATUS_LED_DEFAULT_VAL, STATUS_LED_DEFAULT_VAL
+ );
+ } else {
+ status_led_write(0, 0, 0);
+ }
+ printf("", on ? "on" : "off");
+}
+
+static void cmd_handle_channel(const char *cmd_str) {
+ char ch_char = cmd_str[CMD_CHANNEL_CHAR_INDEX];
+ uint32_t channel = ch_char - CMD_CHANNEL_MIN_CHAR;
+ bool on = (strstr(cmd_str, "#on#") != NULL);
+
+ if (channel < RELAY_NUM_CHANNELS) {
+ relay_set(channel, on);
+ printf("", ch_char, on ? "on" : "off");
+ }
+}
+
+static void cmd_handle_timer(const char *cmd_str) {
+ char ch_char = cmd_str[CMD_CHANNEL_CHAR_INDEX];
+ uint32_t channel = ch_char - CMD_CHANNEL_MIN_CHAR;
+ uint32_t milliseconds = 0;
+
+ if (sscanf(cmd_str + CMD_TIMER_ARGS_OFFSET, "%u#end", &milliseconds) == 1) {
+ if (channel < RELAY_NUM_CHANNELS) {
+ relay_start_timer(channel, milliseconds, true);
+ printf(
+ "", ch_char, milliseconds
+ );
+ }
+ }
+}
+
+static void cmd_handle_plan(char *cmd_str) {
+ // Persist the original plan string first
+ persist_save_plan(cmd_str);
+
+ // Parse command: "rs#all#plan#ch#1#on#ch#3#tmron#500#end"
+ char *token = strtok(cmd_str, "#"); // "rs"
+ if (!token) return;
+ token = strtok(NULL, "#"); // "all"
+ if (!token) return;
+ token = strtok(NULL, "#"); // "plan"
+ if (!token) return;
+
+ while (1) {
+ token = strtok(NULL, "#"); // "ch" or "end"
+ if (!token || strcmp(token, "end") == 0) {
+ break;
+ }
+
+ if (strcmp(token, "ch") == 0) {
+ token = strtok(NULL, "#"); // index
+ if (!token) break;
+ uint32_t channel = (uint32_t)(atoi(token) - 1);
+
+ token = strtok(NULL, "#"); // mode: "on", "off", "tmron", "tmroff"
+ if (!token) break;
+
+ if (strcmp(token, "off") == 0) {
+ relay_set(channel, false);
+ } else if (strcmp(token, "on") == 0) {
+ relay_set(channel, true);
+ } else if (strcmp(token, "tmron") == 0) {
+ token = strtok(NULL, "#");
+ if (token) {
+ uint32_t duration_ms = (uint32_t)atoi(token);
+ relay_start_timer(channel, duration_ms, true);
+ }
+ } else if (strcmp(token, "tmroff") == 0) {
+ token = strtok(NULL, "#");
+ if (token) {
+ uint32_t duration_ms = (uint32_t)atoi(token);
+ relay_start_timer(channel, duration_ms, false);
+ }
+ }
+ }
+ }
+
+ printf("");
+}
+
+static void cmd_handle_mask(const char *cmd_str) {
+ for (uint32_t i = 0; i < RELAY_NUM_CHANNELS; i++) {
+ char bit = cmd_str[CMD_MASK_ARGS_OFFSET + i];
+ bool on = (bit == '1');
+ relay_set(i, on);
+ }
+ printf(
+ "", cmd_str + CMD_MASK_ARGS_OFFSET
+ );
+}
+
+static void cmd_handle_reset(const char *cmd_str) {
+ (void)cmd_str;
+ printf("");
+ fflush(stdout);
+ buzzer_beep_stop();
+ watchdog_reboot(0, 0, 0);
+}
+
+static void cmd_handle_channel_status(const char *cmd_str) {
+ char ch_char = cmd_str[CMD_CHANNEL_CHAR_INDEX];
+ uint32_t channel = ch_char - CMD_CHANNEL_MIN_CHAR;
+ if (channel < RELAY_NUM_CHANNELS) {
+ char status_buf[64];
+ relay_get_status(channel, status_buf, sizeof(status_buf));
+ printf("", status_buf);
+ }
+}
+
+static void cmd_handle_all_status(const char *cmd_str) {
+ (void)cmd_str;
+ char all_status_buf[256] = "channels: ";
+ uint32_t offset = 10;
+ for (uint32_t i = 0; i < RELAY_NUM_CHANNELS; i++) {
+ char status_buf[48];
+ relay_get_status(i, status_buf, sizeof(status_buf));
+ const char *state_str = strstr(status_buf, "ON") ? "ON" : "OFF";
+ int written = snprintf(
+ all_status_buf + offset, sizeof(all_status_buf) - offset, "%u:%s ",
+ i + 1, state_str
+ );
+ if (written > 0) {
+ offset += (uint32_t)written;
+ }
+ }
+ printf("", all_status_buf);
+}
+
+static const command_entry_t command_tbl[] = {
+ {"rs#ch#1#on#end", cmd_handle_channel},
+ {"rs#ch#1#off#end", cmd_handle_channel},
+ {"rs#ch#2#on#end", cmd_handle_channel},
+ {"rs#ch#2#off#end", cmd_handle_channel},
+ {"rs#ch#3#on#end", cmd_handle_channel},
+ {"rs#ch#3#off#end", cmd_handle_channel},
+ {"rs#ch#4#on#end", cmd_handle_channel},
+ {"rs#ch#4#off#end", cmd_handle_channel},
+ {"rs#ch#5#on#end", cmd_handle_channel},
+ {"rs#ch#5#off#end", cmd_handle_channel},
+ {"rs#ch#6#on#end", cmd_handle_channel},
+ {"rs#ch#6#off#end", cmd_handle_channel},
+ {"rs#ch#7#on#end", cmd_handle_channel},
+ {"rs#ch#7#off#end", cmd_handle_channel},
+ {"rs#ch#8#on#end", cmd_handle_channel},
+ {"rs#ch#8#off#end", cmd_handle_channel},
+ {"rs#all#on#end", cmd_handle_all_channels},
+ {"rs#all#off#end", cmd_handle_all_channels},
+ {"rs#all#stat#end", cmd_handle_all_status},
+ {"rs#sys#reset#end", cmd_handle_reset},
+ {"rs#sys#id#end", cmd_handle_board_id},
+ {"rs#sys#version#end", cmd_handle_version},
+};
+
+static const size_t command_tbl_size =
+ sizeof(command_tbl) / sizeof(command_tbl[0]);
+
+void command_dispatch(const char *cmd_str) {
+ // Check for plan commands: "rs#all#plan#..."
+ if (strncmp(cmd_str, "rs#all#plan#", 12) == 0) {
+ char local_buf[256];
+ strncpy(local_buf, cmd_str, sizeof(local_buf) - 1);
+ local_buf[sizeof(local_buf) - 1] = '\0';
+ cmd_handle_plan(local_buf);
+ return;
+ }
+
+ // Check for timer commands: "rs#ch#X#tmr#Y#end"
+ if (strncmp(cmd_str, CMD_PREFIX_CH, CMD_PREFIX_CH_LEN) == 0 &&
+ cmd_str[CMD_CHANNEL_CHAR_INDEX] >= CMD_CHANNEL_MIN_CHAR &&
+ cmd_str[CMD_CHANNEL_CHAR_INDEX] <= CMD_CHANNEL_MAX_CHAR &&
+ strncmp(cmd_str + CMD_CHANNEL_CHAR_INDEX + 1, CMD_TMR, CMD_TMR_LEN) ==
+ 0) {
+ cmd_handle_timer(cmd_str);
+ return;
+ }
+
+ // Check for channel status commands: "rs#ch#X#stat#end"
+ if (strncmp(cmd_str, CMD_PREFIX_CH, CMD_PREFIX_CH_LEN) == 0 &&
+ cmd_str[CMD_CHANNEL_CHAR_INDEX] >= CMD_CHANNEL_MIN_CHAR &&
+ cmd_str[CMD_CHANNEL_CHAR_INDEX] <= CMD_CHANNEL_MAX_CHAR &&
+ strcmp(cmd_str + CMD_CHANNEL_CHAR_INDEX + 1, CMD_STAT_SUFFIX) == 0) {
+ cmd_handle_channel_status(cmd_str);
+ return;
+ }
+
+ // Check for mask commands: "rs#all#mask#10101010#end"
+ if (strncmp(cmd_str, CMD_PREFIX_MASK, CMD_PREFIX_MASK_LEN) == 0 &&
+ strcmp(cmd_str + CMD_MASK_SUFFIX_OFFSET, CMD_SUFFIX_END) == 0) {
+ cmd_handle_mask(cmd_str);
+ return;
+ }
+
+ for (size_t i = 0; i < command_tbl_size; i++) {
+ if (strcmp(cmd_str, command_tbl[i].pattern) == 0) {
+ command_tbl[i].handler(cmd_str);
+ return;
+ }
+ }
+}
diff --git a/sw/restat_base/src/command/dispatcher.h b/sw/restat_base/src/command/dispatcher.h
new file mode 100644
index 0000000..dca9c1f
--- /dev/null
+++ b/sw/restat_base/src/command/dispatcher.h
@@ -0,0 +1,35 @@
+/* -*- Mode: C; indent-tabs-mode: t; c-basic-offset: 4; tab-width: 4 -*- */
+/*
+ * dispatcher.h
+ * Copyright (C) 2025 Vladimir Roncevic
+ *
+ * restat-base 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.
+ *
+ * restat-base 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_name. If not, see .
+ */
+#pragma once
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+////////////////////////////////////////////////////////////////////////////
+/// @brief Dispatches a parsed command string to its registered handler
+///
+/// Matches the input string against the command registration table.
+///
+/// @param cmd_str [in] The null-terminated command string to route
+void command_dispatch(const char *cmd_str);
+
+#ifdef __cplusplus
+}
+#endif
diff --git a/sw/restat_base/src/command/parser.c b/sw/restat_base/src/command/parser.c
new file mode 100644
index 0000000..289e328
--- /dev/null
+++ b/sw/restat_base/src/command/parser.c
@@ -0,0 +1,80 @@
+/* -*- Mode: C; indent-tabs-mode: t; c-basic-offset: 4; tab-width: 4 -*- */
+/*
+ * parser.c
+ * Copyright (C) 2025 Vladimir Roncevic
+ *
+ * restat-base 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.
+ *
+ * restat-base 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_name. If not, see .
+ */
+#include
+#include
+
+typedef enum { PARSER_STATE_IDLE, PARSER_STATE_RECEIVING } parser_state_t;
+
+static parser_state_t parser_state = PARSER_STATE_IDLE;
+static uint32_t parser_idx = 0;
+static const uint32_t PARSER_TIMEOUT_US = 0;
+
+////////////////////////////////////////////////////////////////////////////
+/// @brief Processes a single character inside the parsing state machine
+///
+/// @param c [in] Character to process
+/// @param buf [out] Destination buffer to store parsed command
+/// @param max_len [in] Maximum capacity of the destination buffer
+/// @return True if a complete command frame was successfully parsed, else false
+static bool parser_process_char(char c, char *buf, uint32_t max_len) {
+ if (c == '\r' || c == '\n') {
+ return false;
+ }
+
+ if (c == '<') {
+ parser_state = PARSER_STATE_RECEIVING;
+ parser_idx = 0;
+ return false;
+ }
+
+ if (parser_state == PARSER_STATE_RECEIVING) {
+ if (c == '>') {
+ buf[parser_idx] = '\0';
+ parser_state = PARSER_STATE_IDLE;
+ parser_idx = 0;
+ return true;
+ }
+
+ if (parser_idx < (max_len - 1)) {
+ buf[parser_idx++] = c;
+ } else {
+ parser_state = PARSER_STATE_IDLE;
+ parser_idx = 0;
+ }
+ }
+
+ return false;
+}
+
+////////////////////////////////////////////////////////////////////////////
+/// @brief Reads and parses command requests from the input stream
+///
+/// @param buf [out] Destination buffer to store parsed command
+/// @param max_len [in] Maximum capacity of the destination buffer
+/// @return True if a command was parsed, else false
+bool parser_get_command(char *buf, uint32_t max_len) {
+ int16_t rc = getchar_timeout_us(PARSER_TIMEOUT_US);
+ while (rc != PICO_ERROR_TIMEOUT) {
+ if (parser_process_char((char)rc, buf, max_len)) {
+ return true;
+ }
+ rc = getchar_timeout_us(PARSER_TIMEOUT_US);
+ }
+ return false;
+}
diff --git a/sw/restat_base/src/command/parser.h b/sw/restat_base/src/command/parser.h
new file mode 100644
index 0000000..2ff9f86
--- /dev/null
+++ b/sw/restat_base/src/command/parser.h
@@ -0,0 +1,41 @@
+/* -*- Mode: C; indent-tabs-mode: t; c-basic-offset: 4; tab-width: 4 -*- */
+/*
+ * parser.h
+ * Copyright (C) 2025 Vladimir Roncevic
+ *
+ * restat-base 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.
+ *
+ * restat-base 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_name. If not, see .
+ */
+#pragma once
+
+#include
+#include
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+////////////////////////////////////////////////////////////////////////////
+/// @brief Fetches and parses a command frame from the USB CDC interface
+///
+/// Accumulates incoming characters. Only returns true and populates `buf`
+/// when a complete frame enclosed in '<' and '>' markers is received.
+///
+/// @param buf [out] Destination buffer to store parsed command
+/// @param max_len [in] Maximum capacity of the destination buffer
+/// @return True if a complete command frame was successfully parsed, else false
+bool parser_get_command(char *buf, uint32_t max_len);
+
+#ifdef __cplusplus
+}
+#endif
diff --git a/sw/restat_base/src/device/CMakeLists.txt b/sw/restat_base/src/device/CMakeLists.txt
new file mode 100644
index 0000000..764702b
--- /dev/null
+++ b/sw/restat_base/src/device/CMakeLists.txt
@@ -0,0 +1,9 @@
+#
+# @brief device library setup
+# @version v1.0.0
+# @date 2026-08-21
+#
+
+add_library(device relay.c buzzer.c status_led.c persist.c)
+target_include_directories(device PUBLIC .)
+target_link_libraries(device io ws2812 pico_stdlib hardware_pio hardware_flash)
diff --git a/sw/restat_base/src/device/buzzer.c b/sw/restat_base/src/device/buzzer.c
new file mode 100644
index 0000000..1d22818
--- /dev/null
+++ b/sw/restat_base/src/device/buzzer.c
@@ -0,0 +1,73 @@
+/* -*- Mode: C; indent-tabs-mode: t; c-basic-offset: 4; tab-width: 4 -*- */
+/*
+ * buzzer.c
+ * Copyright (C) 2025 Vladimir Roncevic
+ *
+ * restat-base 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.
+ *
+ * restat-base 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_name. If not, see .
+ */
+#include
+#include
+#include
+
+static const uint32_t BUZZER_PIN = 6;
+static const uint32_t BUZZER_PWM_CHAN = 0; // PWM_CHAN_A
+
+static const uint16_t BUZZER_BEEP_START_DUTY = 80;
+static const uint32_t BUZZER_BEEP_START_MS = 100;
+
+static const uint16_t BUZZER_BEEP_CHANGED_DUTY = 80;
+static const uint32_t BUZZER_BEEP_CHANGED_MS = 50;
+
+static const uint16_t BUZZER_BEEP_STOP_DUTY = 100;
+static const uint32_t BUZZER_BEEP_STOP_MS = 300;
+
+static const uint64_t US_TO_MS = 1000;
+
+bool buzzer_init(void) {
+ io_pwm_init(BUZZER_PIN, BUZZER_PWM_CHAN);
+ io_pwm_write(BUZZER_PIN, BUZZER_PWM_CHAN, 0);
+ return true;
+}
+
+void buzzer_write(uint16_t value) {
+ io_pwm_write(BUZZER_PIN, BUZZER_PWM_CHAN, value);
+}
+
+void buzzer_beep_start(void) {
+ buzzer_write(BUZZER_BEEP_START_DUTY);
+ sleep_ms(BUZZER_BEEP_START_MS);
+ buzzer_write(0);
+}
+
+static uint64_t buzzer_off_time = 0;
+
+void buzzer_beep_changed(void) {
+ buzzer_write(BUZZER_BEEP_CHANGED_DUTY);
+ buzzer_off_time = time_us_64() / US_TO_MS + BUZZER_BEEP_CHANGED_MS;
+}
+
+void buzzer_tick(void) {
+ if (buzzer_off_time > 0) {
+ if ((time_us_64() / US_TO_MS) >= buzzer_off_time) {
+ buzzer_write(0);
+ buzzer_off_time = 0;
+ }
+ }
+}
+
+void buzzer_beep_stop(void) {
+ buzzer_write(BUZZER_BEEP_STOP_DUTY);
+ sleep_ms(BUZZER_BEEP_STOP_MS);
+ buzzer_write(0);
+}
diff --git a/sw/restat_base/src/device/buzzer.h b/sw/restat_base/src/device/buzzer.h
new file mode 100644
index 0000000..aa29fac
--- /dev/null
+++ b/sw/restat_base/src/device/buzzer.h
@@ -0,0 +1,58 @@
+/* -*- Mode: C; indent-tabs-mode: t; c-basic-offset: 4; tab-width: 4 -*- */
+/*
+ * buzzer.h
+ * Copyright (C) 2025 Vladimir Roncevic
+ *
+ * restat-base 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.
+ *
+ * restat-base 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_name. If not, see .
+ */
+#pragma once
+
+#include
+#include
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+////////////////////////////////////////////////////////////////////////////
+/// @brief Initializes PWM for the buzzer
+///
+/// @return True if initialization succeeded, false otherwise
+bool buzzer_init(void);
+
+////////////////////////////////////////////////////////////////////////////
+/// @brief Writes a duty cycle level to the buzzer PWM channel
+///
+/// @param value [in] PWM level value (0 to 100)
+void buzzer_write(uint16_t value);
+
+////////////////////////////////////////////////////////////////////////////
+/// @brief Triggers a short startup notification beep
+void buzzer_beep_start(void);
+
+////////////////////////////////////////////////////////////////////////////
+/// @brief Triggers a short non-blocking notification beep for state changes
+void buzzer_beep_changed(void);
+
+////////////////////////////////////////////////////////////////////////////
+/// @brief Polling tick function to turn off buzzer non-blockingly
+void buzzer_tick(void);
+
+////////////////////////////////////////////////////////////////////////////
+/// @brief Triggers a longer shutdown/reset beep (blocking, pre-reset)
+void buzzer_beep_stop(void);
+
+#ifdef __cplusplus
+}
+#endif
diff --git a/sw/restat_base/src/device/persist.c b/sw/restat_base/src/device/persist.c
new file mode 100644
index 0000000..8392060
--- /dev/null
+++ b/sw/restat_base/src/device/persist.c
@@ -0,0 +1,58 @@
+/* -*- Mode: C; indent-tabs-mode: t; c-basic-offset: 4; tab-width: 4 -*- */
+/*
+ * persist.c
+ * Copyright (C) 2026 Vladimir Roncevic
+ *
+ * restat-base 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.
+ *
+ * restat-base 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
+#include
+#include
+
+#define FLASH_TARGET_OFFSET (1536 * 1024) // 1.5 MB offset
+
+void persist_save_plan(const char *plan_str) {
+ // Prepare a page buffer (256 bytes) to write
+ uint8_t write_buf[FLASH_PAGE_SIZE];
+ memset(write_buf, 0, sizeof(write_buf));
+
+ // Copy plan_str to buffer
+ strncpy((char *)write_buf, plan_str, sizeof(write_buf) - 1);
+
+ // Disable interrupts before erasing and writing
+ uint32_t ints = save_and_disable_interrupts();
+
+ // Erase one sector (4096 bytes) at the target offset
+ flash_range_erase(FLASH_TARGET_OFFSET, FLASH_SECTOR_SIZE);
+
+ // Write one page (256 bytes)
+ flash_range_program(FLASH_TARGET_OFFSET, write_buf, FLASH_PAGE_SIZE);
+
+ // Restore interrupts
+ restore_interrupts(ints);
+}
+
+bool persist_load_plan(char *buf, size_t max_len) {
+ // Flash is mapped to memory starting at XIP_BASE (0x10000000)
+ const uint8_t *flash_target_contents = (const uint8_t *)(XIP_BASE + FLASH_TARGET_OFFSET);
+
+ // Check if it starts with a valid pattern
+ if (strncmp((const char *)flash_target_contents, "rs#all#plan#", 12) == 0) {
+ strncpy(buf, (const char *)flash_target_contents, max_len - 1);
+ buf[max_len - 1] = '\0';
+ return true;
+ }
+ return false;
+}
diff --git a/sw/restat_base/src/device/persist.h b/sw/restat_base/src/device/persist.h
new file mode 100644
index 0000000..6d86ae9
--- /dev/null
+++ b/sw/restat_base/src/device/persist.h
@@ -0,0 +1,31 @@
+/* -*- Mode: C; indent-tabs-mode: t; c-basic-offset: 4; tab-width: 4 -*- */
+/*
+ * persist.h
+ * Copyright (C) 2026 Vladimir Roncevic
+ *
+ * restat-base 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.
+ *
+ * restat-base 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
+
+#define PERSIST_MAX_LEN 256
+
+// Save a plan string to flash
+void persist_save_plan(const char *plan_str);
+
+// Load the plan string from flash. Returns true if valid plan was found.
+bool persist_load_plan(char *buf, size_t max_len);
diff --git a/sw/restat_base/src/device/relay.c b/sw/restat_base/src/device/relay.c
new file mode 100644
index 0000000..01eb8b5
--- /dev/null
+++ b/sw/restat_base/src/device/relay.c
@@ -0,0 +1,137 @@
+/* -*- Mode: C; indent-tabs-mode: t; c-basic-offset: 4; tab-width: 4 -*- */
+/*
+ * relay.c
+ * Copyright (C) 2025 Vladimir Roncevic
+ *
+ * restat-base 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.
+ *
+ * restat-base 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_name. If not, see .
+ */
+#include
+#include
+#include
+#include
+#include
+
+static const uint32_t relay_pins[RELAY_NUM_CHANNELS] = {
+ 21, 20, 19, 18, 17, 16, 15, 14,
+};
+
+static const uint64_t US_TO_MS = 1000;
+static const uint64_t MS_TO_SEC = 1000;
+
+typedef struct {
+ relay_mode_t mode;
+ bool active;
+ uint64_t start_time;
+ uint64_t duration_ms;
+ bool start_on;
+} relay_state_internal_t;
+
+static relay_state_internal_t relay_states[RELAY_NUM_CHANNELS];
+static bool current_phys_state[RELAY_NUM_CHANNELS] = {false};
+
+bool relay_init(void) {
+ for (int i = 0; i < RELAY_NUM_CHANNELS; i++) {
+ io_gpio_init(relay_pins[i], true);
+ io_gpio_write(relay_pins[i], true);
+ current_phys_state[i] = false;
+
+ relay_states[i].mode = RELAY_MODE_TOGGLE;
+ relay_states[i].active = false;
+ relay_states[i].start_time = 0;
+ relay_states[i].duration_ms = 0;
+ relay_states[i].start_on = false;
+ }
+ return true;
+}
+
+static void relay_set_phys(uint32_t channel, bool state) {
+ if (channel < RELAY_NUM_CHANNELS) {
+ if (current_phys_state[channel] != state) {
+ current_phys_state[channel] = state;
+ io_gpio_write(relay_pins[channel], !state);
+ buzzer_beep_changed();
+ }
+ }
+}
+
+void relay_set(uint32_t channel, bool state) {
+ if (channel < RELAY_NUM_CHANNELS) {
+ relay_states[channel].active = false;
+ relay_states[channel].mode = RELAY_MODE_TOGGLE;
+ relay_set_phys(channel, state);
+ }
+}
+
+void relay_set_all(bool state) {
+ for (int i = 0; i < RELAY_NUM_CHANNELS; i++) {
+ relay_set(i, state);
+ }
+}
+
+void relay_start_timer(uint32_t channel, uint32_t duration_ms, bool start_on) {
+ if (channel < RELAY_NUM_CHANNELS) {
+ relay_states[channel].mode = RELAY_MODE_TIMER;
+ relay_states[channel].active = true;
+ relay_states[channel].start_time = time_us_64() / US_TO_MS;
+ relay_states[channel].duration_ms = (uint64_t)duration_ms;
+ relay_states[channel].start_on = start_on;
+ relay_set_phys(channel, start_on);
+ }
+}
+
+void relay_tick(void) {
+ uint64_t now_ms = time_us_64() / US_TO_MS;
+ for (uint32_t i = 0; i < RELAY_NUM_CHANNELS; i++) {
+ if (!relay_states[i].active) {
+ continue;
+ }
+
+ if (relay_states[i].mode == RELAY_MODE_TIMER) {
+ if (now_ms - relay_states[i].start_time >= relay_states[i].duration_ms) {
+ bool next_state = !relay_states[i].start_on;
+ relay_set_phys(i, next_state);
+ relay_states[i].active = false;
+ relay_states[i].mode = RELAY_MODE_TOGGLE;
+ printf("", i + 1, next_state ? "on" : "off");
+ fflush(stdout);
+ }
+ }
+ }
+}
+
+void relay_get_status(uint32_t channel, char *buf, uint32_t max_len) {
+ if (channel >= RELAY_NUM_CHANNELS) {
+ snprintf(buf, max_len, "invalid channel");
+ return;
+ }
+
+ const char *phys_state = current_phys_state[channel] ? "ON" : "OFF";
+ if (!relay_states[channel].active) {
+ snprintf(buf, max_len, "Channel %u: %s (Toggle)", channel + 1, phys_state);
+ } else {
+ switch (relay_states[channel].mode) {
+ case RELAY_MODE_TIMER: {
+ uint64_t elapsed = time_us_64() / US_TO_MS - relay_states[channel].start_time;
+ uint64_t remaining = (relay_states[channel].duration_ms > elapsed) ?
+ (relay_states[channel].duration_ms - elapsed) : 0;
+ snprintf(buf, max_len, "Channel %u: %s (Timer, rem: %llums)",
+ channel + 1, phys_state, remaining);
+ break;
+ }
+ default:
+ snprintf(buf, max_len, "Channel %u: %s", channel + 1, phys_state);
+ break;
+ }
+ }
+}
diff --git a/sw/restat_base/src/device/relay.h b/sw/restat_base/src/device/relay.h
new file mode 100644
index 0000000..af166f8
--- /dev/null
+++ b/sw/restat_base/src/device/relay.h
@@ -0,0 +1,72 @@
+/* -*- Mode: C; indent-tabs-mode: t; c-basic-offset: 4; tab-width: 4 -*- */
+/*
+ * relay.h
+ * Copyright (C) 2025 Vladimir Roncevic
+ *
+ * restat-base 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.
+ *
+ * restat-base 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_name. If not, see .
+ */
+#pragma once
+
+#include
+#include
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+////////////////////////////////////////////////////////////////////////////
+/// @brief Total number of relay channels
+enum { RELAY_NUM_CHANNELS = 8 };
+
+////////////////////////////////////////////////////////////////////////////
+/// @brief Relay modes
+typedef enum {
+ RELAY_MODE_TOGGLE,
+ RELAY_MODE_TIMER
+} relay_mode_t;
+
+////////////////////////////////////////////////////////////////////////////
+/// @brief Initializes all relay GPIO channels
+///
+/// @return True if initialization succeeded, false otherwise
+bool relay_init(void);
+
+////////////////////////////////////////////////////////////////////////////
+/// @brief Sets the state of a specific relay channel
+///
+/// @param channel [in] Relay channel index (0 to RELAY_NUM_CHANNELS - 1)
+/// @param state [in] True to turn ON, false to turn OFF
+void relay_set(uint32_t channel, bool state);
+
+////////////////////////////////////////////////////////////////////////////
+/// @brief Sets the state of all relay channels simultaneously
+///
+/// @param state [in] True to turn all ON, false to turn all OFF
+void relay_set_all(bool state);
+
+////////////////////////////////////////////////////////////////////////////
+/// @brief Starts a timer mode on a relay channel
+void relay_start_timer(uint32_t channel, uint32_t duration_ms, bool start_on);
+
+////////////////////////////////////////////////////////////////////////////
+/// @brief Non-blocking tick routine for relay timings
+void relay_tick(void);
+
+////////////////////////////////////////////////////////////////////////////
+/// @brief Returns a status string for the given relay channel
+void relay_get_status(uint32_t channel, char *buf, uint32_t max_len);
+
+#ifdef __cplusplus
+}
+#endif
diff --git a/sw/restat_base/src/device/status_led.c b/sw/restat_base/src/device/status_led.c
new file mode 100644
index 0000000..3ccb62e
--- /dev/null
+++ b/sw/restat_base/src/device/status_led.c
@@ -0,0 +1,49 @@
+/* -*- Mode: C; indent-tabs-mode: t; c-basic-offset: 4; tab-width: 4 -*- */
+/*
+ * status_led.c
+ * Copyright (C) 2025 Vladimir Roncevic
+ *
+ * restat-base 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.
+ *
+ * restat-base 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_name. If not, see .
+ */
+#include
+#include
+#include
+
+static const uint32_t STATUS_LED_PIN = 13;
+static const uint32_t STATUS_LED_PIO_SM = 0;
+static const uint32_t STATUS_LED_FREQ = 800000;
+static const uint32_t PIXEL_RGB_SHIFT_R = 8;
+static const uint32_t PIXEL_RGB_SHIFT_G = 16;
+static const uint32_t PIO_SM_PUT_SHIFT = 8;
+
+bool status_led_init(void) {
+ ws2812_init init = {
+ .pio = pio0,
+ .sm = STATUS_LED_PIO_SM,
+ .offset = pio_add_program(pio0, &ws2812_program),
+ .pin = STATUS_LED_PIN,
+ .freq = STATUS_LED_FREQ,
+ .rgbw = false};
+
+ ws2812_program_init(&init);
+ status_led_write(0, 0, 0);
+ return true;
+}
+
+void status_led_write(uint8_t red, uint8_t green, uint8_t blue) {
+ uint pixel_rgb = ((uint)(red) << PIXEL_RGB_SHIFT_R) |
+ ((uint)(green) << PIXEL_RGB_SHIFT_G) |
+ ((uint)(blue));
+ pio_sm_put_blocking(pio0, STATUS_LED_PIO_SM, pixel_rgb << PIO_SM_PUT_SHIFT);
+}
diff --git a/sw/restat_base/src/device/status_led.h b/sw/restat_base/src/device/status_led.h
new file mode 100644
index 0000000..ad4e8b9
--- /dev/null
+++ b/sw/restat_base/src/device/status_led.h
@@ -0,0 +1,44 @@
+/* -*- Mode: C; indent-tabs-mode: t; c-basic-offset: 4; tab-width: 4 -*- */
+/*
+ * status_led.h
+ * Copyright (C) 2025 Vladimir Roncevic
+ *
+ * restat-base 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.
+ *
+ * restat-base 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_name. If not, see .
+ */
+#pragma once
+
+#include
+#include
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+////////////////////////////////////////////////////////////////////////////
+/// @brief Initializes PIO state machine for the WS2812 status LED
+///
+/// @return True if initialization succeeded, false otherwise
+bool status_led_init(void);
+
+////////////////////////////////////////////////////////////////////////////
+/// @brief Writes red, green, and blue values to the WS2812 status LED
+///
+/// @param red [in] Red component intensity (0 to 255)
+/// @param green [in] Green component intensity (0 to 255)
+/// @param blue [in] Blue component intensity (0 to 255)
+void status_led_write(uint8_t red, uint8_t green, uint8_t blue);
+
+#ifdef __cplusplus
+}
+#endif
diff --git a/sw/restat_base/src/io/CMakeLists.txt b/sw/restat_base/src/io/CMakeLists.txt
new file mode 100644
index 0000000..001df28
--- /dev/null
+++ b/sw/restat_base/src/io/CMakeLists.txt
@@ -0,0 +1,9 @@
+#
+# @brief io library setup
+# @version v1.0.0
+# @date 2026-08-21
+#
+
+add_library(io io_gpio.c io_pwm.c)
+target_include_directories(io PUBLIC .)
+target_link_libraries(io pico_stdlib hardware_gpio hardware_pwm)
diff --git a/sw/restat_base/src/io/io_gpio.c b/sw/restat_base/src/io/io_gpio.c
new file mode 100644
index 0000000..c18a9cf
--- /dev/null
+++ b/sw/restat_base/src/io/io_gpio.c
@@ -0,0 +1,38 @@
+/* -*- Mode: C; indent-tabs-mode: t; c-basic-offset: 4; tab-width: 4 -*- */
+/*
+ * io_gpio.c
+ * Copyright (C) 2025 Vladimir Roncevic
+ *
+ * restat-base 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.
+ *
+ * restat-base 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_name. If not, see .
+ */
+#include
+#include
+#include
+
+void io_gpio_init(uint32_t pin, bool is_output) {
+ gpio_init(pin);
+ if (is_output) {
+ gpio_set_dir(pin, GPIO_OUT);
+ } else {
+ gpio_set_dir(pin, GPIO_IN);
+ }
+}
+
+void io_gpio_write(uint32_t pin, bool value) {
+ gpio_put(pin, value);
+}
+
+bool io_gpio_read(uint32_t pin) {
+ return gpio_get(pin);
+}
diff --git a/sw/restat_base/src/io/io_gpio.h b/sw/restat_base/src/io/io_gpio.h
new file mode 100644
index 0000000..dbba959
--- /dev/null
+++ b/sw/restat_base/src/io/io_gpio.h
@@ -0,0 +1,51 @@
+/* -*- Mode: C; indent-tabs-mode: t; c-basic-offset: 4; tab-width: 4 -*- */
+/*
+ * io_gpio.h
+ * Copyright (C) 2025 Vladimir Roncevic
+ *
+ * restat-base 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.
+ *
+ * restat-base 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_name. If not, see .
+ */
+#pragma once
+
+#include
+#include
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+////////////////////////////////////////////////////////////////////////////
+/// @brief Initializes a GPIO pin as input or output
+///
+/// @param pin [in] GPIO pin number to initialize
+/// @param is_output [in] True to configure as output, false for input
+void io_gpio_init(uint32_t pin, bool is_output);
+
+////////////////////////////////////////////////////////////////////////////
+/// @brief Drives a GPIO pin to a high or low level
+///
+/// @param pin [in] GPIO pin number to write
+/// @param value [in] True to set high, false to set low
+void io_gpio_write(uint32_t pin, bool value);
+
+////////////////////////////////////////////////////////////////////////////
+/// @brief Reads the current state of a GPIO pin
+///
+/// @param pin [in] GPIO pin number to read
+/// @return True if the pin is high, false if low
+bool io_gpio_read(uint32_t pin);
+
+#ifdef __cplusplus
+}
+#endif
diff --git a/sw/restat_base/src/io/io_pwm.c b/sw/restat_base/src/io/io_pwm.c
new file mode 100644
index 0000000..fb09433
--- /dev/null
+++ b/sw/restat_base/src/io/io_pwm.c
@@ -0,0 +1,42 @@
+/* -*- Mode: C; indent-tabs-mode: t; c-basic-offset: 4; tab-width: 4 -*- */
+/*
+ * io_pwm.c
+ * Copyright (C) 2025 Vladimir Roncevic
+ *
+ * restat-base 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.
+ *
+ * restat-base 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_name. If not, see .
+ */
+#include
+#include
+#include
+
+static const uint32_t PWM_WRAP_VAL = 500;
+static const uint32_t PWM_INITIAL_LEVEL = 1;
+static const float PWM_CLKDIV = 50.0f;
+static const uint16_t PWM_MAX_DUTY_LEVEL = 100;
+
+void io_pwm_init(uint32_t pin, uint32_t channel) {
+ gpio_set_function(pin, GPIO_FUNC_PWM);
+ uint32_t slice_num = pwm_gpio_to_slice_num(pin);
+ pwm_set_wrap(slice_num, PWM_WRAP_VAL);
+ pwm_set_chan_level(slice_num, channel, PWM_INITIAL_LEVEL);
+ pwm_set_clkdiv(slice_num, PWM_CLKDIV);
+ pwm_set_enabled(slice_num, true);
+}
+
+void io_pwm_write(uint32_t pin, uint32_t channel, uint16_t duty_level) {
+ if (duty_level <= PWM_MAX_DUTY_LEVEL) {
+ uint32_t slice_num = pwm_gpio_to_slice_num(pin);
+ pwm_set_chan_level(slice_num, channel, duty_level);
+ }
+}
diff --git a/sw/restat_base/src/io/io_pwm.h b/sw/restat_base/src/io/io_pwm.h
new file mode 100644
index 0000000..e3ce71b
--- /dev/null
+++ b/sw/restat_base/src/io/io_pwm.h
@@ -0,0 +1,44 @@
+/* -*- Mode: C; indent-tabs-mode: t; c-basic-offset: 4; tab-width: 4 -*- */
+/*
+ * io_pwm.h
+ * Copyright (C) 2025 Vladimir Roncevic
+ *
+ * restat-base 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.
+ *
+ * restat-base 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_name. If not, see .
+ */
+#pragma once
+
+#include
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+////////////////////////////////////////////////////////////////////////////
+/// @brief Initializes PWM on a specific pin and PWM channel
+///
+/// @param pin [in] GPIO pin to initialize for PWM
+/// @param channel [in] PWM channel to use
+void io_pwm_init(uint32_t pin, uint32_t channel);
+
+////////////////////////////////////////////////////////////////////////////
+/// @brief Writes PWM duty cycle to a specific pin and PWM channel
+///
+/// @param pin [in] GPIO pin with PWM enabled
+/// @param channel [in] PWM channel to write
+/// @param duty_percent [in] Duty cycle as percentage (0-100)
+void io_pwm_write(uint32_t pin, uint32_t channel, uint16_t duty_percent);
+
+#ifdef __cplusplus
+}
+#endif
diff --git a/sw/restat_base/src/main.c b/sw/restat_base/src/main.c
new file mode 100644
index 0000000..5781441
--- /dev/null
+++ b/sw/restat_base/src/main.c
@@ -0,0 +1,103 @@
+/* -*- Mode: C; indent-tabs-mode: t; c-basic-offset: 4; tab-width: 4 -*- */
+/*
+ * main.c
+ * Copyright (C) 2025 Vladimir Roncevic
+ *
+ * restat-base 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.
+ *
+ * restat-base 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_name. If not, see .
+ */
+
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+enum { RESTAT_REQ_LEN = 256 };
+static const uint32_t USB_CONN_CHECK_MS = 100;
+
+////////////////////////////////////////////////////////////////////////////
+/// @brief Performs unified initialization of all hardware devices
+/// @return true for success else false
+static bool device_init(void) {
+ if (!stdio_init_all()) {
+ return false;
+ }
+
+ if (!relay_init()) {
+ return false;
+ }
+
+ if (!status_led_init()) {
+ return false;
+ }
+
+ if (!buzzer_init()) {
+ return false;
+ }
+
+ buzzer_beep_start();
+
+ return true;
+}
+
+////////////////////////////////////////////////////////////////////////////
+/// @brief Main entry point for restat-base
+/// @return 0 for success exit else 1 for failed exit
+int main() {
+ ////////////////////////////////////////////////////////////////////////////
+ /// Performs device initialization
+ if (!device_init()) {
+ return 1;
+ }
+
+ ////////////////////////////////////////////////////////////////////////////
+ /// Waits for USB connection
+ while (!tud_cdc_connected()) {
+ sleep_ms(USB_CONN_CHECK_MS);
+ }
+
+ // Load and execute persisted plan if any
+ char persisted_plan[PERSIST_MAX_LEN] = {0};
+ if (persist_load_plan(persisted_plan, sizeof(persisted_plan))) {
+ printf("\n", persisted_plan);
+ fflush(stdout);
+ command_dispatch(persisted_plan);
+ }
+
+ ////////////////////////////////////////////////////////////////////////////
+ /// Command buffer for received request
+ char request[RESTAT_REQ_LEN] = {0};
+
+ while (true) {
+ ////////////////////////////////////////////////////////////////////////////
+ /// Run non-blocking timings
+ relay_tick();
+ buzzer_tick();
+
+ ////////////////////////////////////////////////////////////////////////////
+ /// Fetch and process channel command request
+ if (parser_get_command(request, RESTAT_REQ_LEN)) {
+ command_dispatch(request);
+ memset(request, 0, RESTAT_REQ_LEN * sizeof(char));
+ }
+ }
+
+ return 0;
+}
diff --git a/sw/restat_base/src/ws2812/CMakeLists.txt b/sw/restat_base/src/ws2812/CMakeLists.txt
new file mode 100644
index 0000000..b9a61b8
--- /dev/null
+++ b/sw/restat_base/src/ws2812/CMakeLists.txt
@@ -0,0 +1,10 @@
+#
+# @brief restat-base ws2812 pio
+# @version v1.0.3
+# @date 2023-09-05
+# @author Vladimir Roncevic
+#
+
+# Adds WS2812 target library as interface (header-only)
+add_library(ws2812 INTERFACE)
+target_include_directories(ws2812 INTERFACE .)
diff --git a/sw/restat_base/src/ws2812/ws2812.pio.h b/sw/restat_base/src/ws2812/ws2812.pio.h
new file mode 100644
index 0000000..547afe3
--- /dev/null
+++ b/sw/restat_base/src/ws2812/ws2812.pio.h
@@ -0,0 +1,188 @@
+/* -*- Mode: C; indent-tabs-mode: t; c-basic-offset: 4; tab-width: 4 -*- */
+/*
+ * ws2812.pio.h
+ * Copyright (C) 2025 Vladimir Roncevic
+ *
+ * restat-base 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.
+ *
+ * restat-base 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_name. If not, see .
+ */
+#pragma once
+
+#if !PICO_NO_HARDWARE
+#include "hardware/clocks.h"
+#include "hardware/pio.h"
+
+#endif
+
+////////////////////////////////////////////////////////////////////////////
+/// @brief PIO program instructions for WS2812
+static const uint16_t ws2812_program_instructions[] = {
+ // .wrap_target
+ 0x6221, // 0: out x, 1 side 0 [2]
+ 0x1123, // 1: jmp !x, 3 side 1 [1]
+ 0x1400, // 2: jmp 0 side 1 [4]
+ 0xa442, // 3: nop side 0 [4]
+ // .wrap
+};
+
+#if !PICO_NO_HARDWARE
+
+////////////////////////////////////////////////////////////////////////////
+/// @brief PIO program instruction structure for WS2812
+static const struct pio_program ws2812_program = {
+ .instructions = ws2812_program_instructions,
+ .length = 4,
+ .origin = -1,
+};
+
+////////////////////////////////////////////////////////////////////////////
+/// @brief Prepares default configuration
+/// @param offset represents programm address
+/// @return PIO configuration structure for WS2812
+static inline pio_sm_config ws2812_program_get_default_config(uint offset) {
+ const uint wrap_target_val = 0;
+ const uint wrap_val = 3;
+
+ pio_sm_config config = pio_get_default_sm_config();
+ uint wrap_target = offset + wrap_target_val;
+ uint wrap = offset + wrap_val;
+
+ sm_config_set_wrap(&config, wrap_target, wrap);
+ sm_config_set_sideset(&config, 1, false, false);
+
+ return config;
+}
+
+////////////////////////////////////////////////////////////////////////////
+/// @brief WS2812 init structure
+typedef struct _ws2812_init {
+ PIO pio;
+ uint sm;
+ uint offset;
+ uint pin;
+ float freq;
+ bool rgbw;
+} ws2812_init;
+
+////////////////////////////////////////////////////////////////////////////
+/// @brief Performs WS2812 program initialization
+/// @param init represents pointer to WS2812 init structure
+static inline void ws2812_program_init(ws2812_init *init) {
+ const int t1 = 2;
+ const int t2 = 5;
+ const int t3 = 3;
+
+ pio_gpio_init(init->pio, init->pin);
+ pio_sm_set_consecutive_pindirs(init->pio, init->sm, init->pin, 1, true);
+
+ pio_sm_config config = ws2812_program_get_default_config(init->offset);
+
+ sm_config_set_sideset_pins(&config, init->pin);
+ sm_config_set_out_shift(&config, false, true, init->rgbw ? 32 : 24);
+ sm_config_set_fifo_join(&config, PIO_FIFO_JOIN_TX);
+
+ int cycles_per_bit = (t1 + t2 + t3);
+ float div = clock_get_hz(clk_sys) / (init->freq * cycles_per_bit);
+
+ sm_config_set_clkdiv(&config, div);
+ pio_sm_init(init->pio, init->sm, init->offset, &config);
+ pio_sm_set_enabled(init->pio, init->sm, true);
+}
+
+#endif
+
+////////////////////////////////////////////////////////////////////////////
+/// @brief PIO program instructions for WS2812 (parallel)
+static const uint16_t ws2812_parallel_program_instructions[] = {
+ // .wrap_target
+ 0x6020, // 0: out x, 32
+ 0xa10b, // 1: mov pins, !null [1]
+ 0xa401, // 2: mov pins, x [4]
+ 0xa103, // 3: mov pins, null [1]
+ // .wrap
+};
+
+#if !PICO_NO_HARDWARE
+
+////////////////////////////////////////////////////////////////////////////
+/// @brief PIO program instruction structure for WS2812 (parallel)
+static const struct pio_program ws2812_parallel_program = {
+ .instructions = ws2812_parallel_program_instructions,
+ .length = 4,
+ .origin = -1,
+};
+
+////////////////////////////////////////////////////////////////////////////
+/// @brief Prepares default configuration (parallel)
+/// @param offset represents program address
+/// @return PIO configuration structure for WS2812
+static inline pio_sm_config
+ws2812_parallel_program_get_default_config(uint offset) {
+ const uint wrap_target_val = 0;
+ const uint wrap_val = 3;
+
+ pio_sm_config config = pio_get_default_sm_config();
+ uint wrap_target = offset + wrap_target_val;
+ uint wrap = offset + wrap_val;
+ sm_config_set_wrap(&config, wrap_target, wrap);
+
+ return config;
+}
+
+////////////////////////////////////////////////////////////////////////////
+/// @brief WS2812 parallel init structure
+typedef struct _ws2812_parallel_init {
+ PIO pio;
+ uint sm;
+ uint offset;
+ uint pin_base;
+ uint pin_count;
+ float freq;
+} ws2812_parallel_init;
+
+////////////////////////////////////////////////////////////////////////////
+/// @brief Performs WS2812 parallel program initialization
+/// @param init represents pointer to WS2812 parallel init structure
+static inline void ws2812_parallel_program_init(ws2812_parallel_init *init) {
+ const int t1 = 2;
+ const int t2 = 5;
+ const int t3 = 3;
+
+ uint max_pin_count = init->pin_base + init->pin_count;
+
+ for (uint i = init->pin_base; i < max_pin_count; i++) {
+ pio_gpio_init(init->pio, i);
+ }
+
+ pio_sm_set_consecutive_pindirs(
+ init->pio, init->sm, init->pin_base, init->pin_count, true
+ );
+
+ pio_sm_config config =
+ ws2812_parallel_program_get_default_config(init->offset);
+
+ sm_config_set_out_shift(&config, true, true, 32);
+ sm_config_set_out_pins(&config, init->pin_base, init->pin_count);
+ sm_config_set_out_pins(&config, init->pin_base, init->pin_count);
+ sm_config_set_fifo_join(&config, PIO_FIFO_JOIN_TX);
+
+ int cycles_per_bit = (t1 + t2 + t3);
+
+ float div = clock_get_hz(clk_sys) / (init->freq * cycles_per_bit);
+
+ sm_config_set_clkdiv(&config, div);
+ pio_sm_init(init->pio, init->sm, init->offset, &config);
+ pio_sm_set_enabled(init->pio, init->sm, true);
+}
+
+#endif
diff --git a/sw/restat_base_tests/CMakeLists.txt b/sw/restat_base_tests/CMakeLists.txt
new file mode 100644
index 0000000..706f3d4
--- /dev/null
+++ b/sw/restat_base_tests/CMakeLists.txt
@@ -0,0 +1,49 @@
+cmake_minimum_required(VERSION 3.12)
+project(restat_base_tests C CXX)
+
+set(CMAKE_EXPORT_COMPILE_COMMANDS ON)
+enable_testing()
+
+# 1. Include Mocks first so that standard Pico and device includes are intercepted
+include_directories(
+ ${CMAKE_CURRENT_SOURCE_DIR}
+ ${CMAKE_CURRENT_SOURCE_DIR}/mocks
+ ${CMAKE_CURRENT_SOURCE_DIR}/../restat_base/src
+ ${CMAKE_CURRENT_SOURCE_DIR}/../restat_base/src/command
+)
+
+# 2. Add Googletest subdirectory from restatdesk_tests (offline/local reuse)
+add_subdirectory(
+ ${CMAKE_CURRENT_SOURCE_DIR}/../restatdesk_tests/googletest
+ ${CMAKE_CURRENT_BINARY_DIR}/googletest_build
+)
+
+# Include Googletest headers
+include_directories(
+ ${CMAKE_CURRENT_SOURCE_DIR}/../restatdesk_tests/googletest/googletest/include
+ ${CMAKE_CURRENT_SOURCE_DIR}/../restatdesk_tests/googletest/googlemock/include
+)
+
+# 3. Create test executable compiling base C modules with host mock structures
+add_executable(
+ restat_base_tests
+ main.cc
+ mocks/mocks.cc
+ ../restat_base/src/command/parser.c
+ ../restat_base/src/command/command_tbl.c
+ tests/test_parser.cc
+ tests/test_command.cc
+)
+
+# 4. Link GoogleTest libraries
+target_link_libraries(
+ restat_base_tests
+ PUBLIC
+ gtest
+ gtest_main
+ gmock
+ gmock_main
+)
+
+include(GoogleTest)
+gtest_discover_tests(restat_base_tests)
diff --git a/sw/restat_base_tests/build/googletest_build/googletest/generated/GTestConfig.cmake b/sw/restat_base_tests/build/googletest_build/googletest/generated/GTestConfig.cmake
new file mode 100644
index 0000000..9ab9a5e
--- /dev/null
+++ b/sw/restat_base_tests/build/googletest_build/googletest/generated/GTestConfig.cmake
@@ -0,0 +1,37 @@
+
+####### Expanded from @PACKAGE_INIT@ by configure_package_config_file() #######
+####### Any changes to this file will be overwritten by the next CMake run ####
+####### The input file was Config.cmake.in ########
+
+get_filename_component(PACKAGE_PREFIX_DIR "${CMAKE_CURRENT_LIST_DIR}/../../../" ABSOLUTE)
+
+macro(set_and_check _var _file)
+ set(${_var} "${_file}")
+ if(NOT EXISTS "${_file}")
+ message(FATAL_ERROR "File or directory ${_file} referenced by variable ${_var} does not exist !")
+ endif()
+endmacro()
+
+macro(check_required_components _NAME)
+ foreach(comp ${${_NAME}_FIND_COMPONENTS})
+ if(NOT ${_NAME}_${comp}_FOUND)
+ if(${_NAME}_FIND_REQUIRED_${comp})
+ set(${_NAME}_FOUND FALSE)
+ endif()
+ endif()
+ endforeach()
+endmacro()
+
+####################################################################################
+include(CMakeFindDependencyMacro)
+if (ON)
+ set(THREADS_PREFER_PTHREAD_FLAG )
+ find_dependency(Threads)
+endif()
+if (OFF)
+ find_dependency(absl)
+ find_dependency(re2)
+endif()
+
+include("${CMAKE_CURRENT_LIST_DIR}/GTestTargets.cmake")
+check_required_components("")
diff --git a/sw/restat_base_tests/build/googletest_build/googletest/generated/GTestConfigVersion.cmake b/sw/restat_base_tests/build/googletest_build/googletest/generated/GTestConfigVersion.cmake
new file mode 100644
index 0000000..a290d14
--- /dev/null
+++ b/sw/restat_base_tests/build/googletest_build/googletest/generated/GTestConfigVersion.cmake
@@ -0,0 +1,43 @@
+# This is a basic version file for the Config-mode of find_package().
+# It is used by write_basic_package_version_file() as input file for configure_file()
+# to create a version-file which can be installed along a config.cmake file.
+#
+# The created file sets PACKAGE_VERSION_EXACT if the current version string and
+# the requested version string are exactly the same and it sets
+# PACKAGE_VERSION_COMPATIBLE if the current version is >= requested version.
+# The variable CVF_VERSION must be set before calling configure_file().
+
+set(PACKAGE_VERSION "1.17.0")
+
+if (PACKAGE_FIND_VERSION_RANGE)
+ # Package version must be in the requested version range
+ if ((PACKAGE_FIND_VERSION_RANGE_MIN STREQUAL "INCLUDE" AND PACKAGE_VERSION VERSION_LESS PACKAGE_FIND_VERSION_MIN)
+ OR ((PACKAGE_FIND_VERSION_RANGE_MAX STREQUAL "INCLUDE" AND PACKAGE_VERSION VERSION_GREATER PACKAGE_FIND_VERSION_MAX)
+ OR (PACKAGE_FIND_VERSION_RANGE_MAX STREQUAL "EXCLUDE" AND PACKAGE_VERSION VERSION_GREATER_EQUAL PACKAGE_FIND_VERSION_MAX)))
+ set(PACKAGE_VERSION_COMPATIBLE FALSE)
+ else()
+ set(PACKAGE_VERSION_COMPATIBLE TRUE)
+ endif()
+else()
+ if(PACKAGE_VERSION VERSION_LESS PACKAGE_FIND_VERSION)
+ set(PACKAGE_VERSION_COMPATIBLE FALSE)
+ else()
+ set(PACKAGE_VERSION_COMPATIBLE TRUE)
+ if(PACKAGE_FIND_VERSION STREQUAL PACKAGE_VERSION)
+ set(PACKAGE_VERSION_EXACT TRUE)
+ endif()
+ endif()
+endif()
+
+
+# if the installed or the using project don't have CMAKE_SIZEOF_VOID_P set, ignore it:
+if("${CMAKE_SIZEOF_VOID_P}" STREQUAL "" OR "8" STREQUAL "")
+ return()
+endif()
+
+# check that the installed version has the same 32/64bit-ness as the one which is currently searching:
+if(NOT CMAKE_SIZEOF_VOID_P STREQUAL "8")
+ math(EXPR installedBits "8 * 8")
+ set(PACKAGE_VERSION "${PACKAGE_VERSION} (${installedBits}bit)")
+ set(PACKAGE_VERSION_UNSUITABLE TRUE)
+endif()
diff --git a/sw/restat_base_tests/build/googletest_build/googletest/generated/gmock.pc b/sw/restat_base_tests/build/googletest_build/googletest/generated/gmock.pc
new file mode 100644
index 0000000..e152eba
--- /dev/null
+++ b/sw/restat_base_tests/build/googletest_build/googletest/generated/gmock.pc
@@ -0,0 +1,10 @@
+libdir=/usr/local/lib
+includedir=/usr/local/include
+
+Name: gmock
+Description: GoogleMock (without main() function)
+Version: 1.17.0
+URL: https://github.com/google/googletest
+Requires: gtest = 1.17.0
+Libs: -L${libdir} -lgmock
+Cflags: -I${includedir} -DGTEST_HAS_PTHREAD=1
diff --git a/sw/restat_base_tests/build/googletest_build/googletest/generated/gmock_main.pc b/sw/restat_base_tests/build/googletest_build/googletest/generated/gmock_main.pc
new file mode 100644
index 0000000..df9620a
--- /dev/null
+++ b/sw/restat_base_tests/build/googletest_build/googletest/generated/gmock_main.pc
@@ -0,0 +1,10 @@
+libdir=/usr/local/lib
+includedir=/usr/local/include
+
+Name: gmock_main
+Description: GoogleMock (with main() function)
+Version: 1.17.0
+URL: https://github.com/google/googletest
+Requires: gmock = 1.17.0
+Libs: -L${libdir} -lgmock_main
+Cflags: -I${includedir} -DGTEST_HAS_PTHREAD=1
diff --git a/sw/restat_base_tests/build/googletest_build/googletest/generated/gtest.pc b/sw/restat_base_tests/build/googletest_build/googletest/generated/gtest.pc
new file mode 100644
index 0000000..aeb8b52
--- /dev/null
+++ b/sw/restat_base_tests/build/googletest_build/googletest/generated/gtest.pc
@@ -0,0 +1,9 @@
+libdir=/usr/local/lib
+includedir=/usr/local/include
+
+Name: gtest
+Description: GoogleTest (without main() function)
+Version: 1.17.0
+URL: https://github.com/google/googletest
+Libs: -L${libdir} -lgtest
+Cflags: -I${includedir} -DGTEST_HAS_PTHREAD=1
diff --git a/sw/restat_base_tests/build/googletest_build/googletest/generated/gtest_main.pc b/sw/restat_base_tests/build/googletest_build/googletest/generated/gtest_main.pc
new file mode 100644
index 0000000..d103700
--- /dev/null
+++ b/sw/restat_base_tests/build/googletest_build/googletest/generated/gtest_main.pc
@@ -0,0 +1,10 @@
+libdir=/usr/local/lib
+includedir=/usr/local/include
+
+Name: gtest_main
+Description: GoogleTest (with main() function)
+Version: 1.17.0
+URL: https://github.com/google/googletest
+Requires: gtest = 1.17.0
+Libs: -L${libdir} -lgtest_main
+Cflags: -I${includedir} -DGTEST_HAS_PTHREAD=1
diff --git a/sw/restat_base_tests/build/restat_base_tests b/sw/restat_base_tests/build/restat_base_tests
new file mode 100755
index 0000000..71925f6
Binary files /dev/null and b/sw/restat_base_tests/build/restat_base_tests differ
diff --git a/sw/restat_base_tests/build/restat_base_tests[1]_include.cmake b/sw/restat_base_tests/build/restat_base_tests[1]_include.cmake
new file mode 100644
index 0000000..2836a0e
--- /dev/null
+++ b/sw/restat_base_tests/build/restat_base_tests[1]_include.cmake
@@ -0,0 +1,5 @@
+if(EXISTS "/data/dev/raspberry/restat/github/restat/sw/restat_base_tests/build/restat_base_tests[1]_tests.cmake")
+ include("/data/dev/raspberry/restat/github/restat/sw/restat_base_tests/build/restat_base_tests[1]_tests.cmake")
+else()
+ add_test(restat_base_tests_NOT_BUILT restat_base_tests_NOT_BUILT)
+endif()
diff --git a/sw/restat_base_tests/build/restat_base_tests[1]_tests.cmake b/sw/restat_base_tests/build/restat_base_tests[1]_tests.cmake
new file mode 100644
index 0000000..45b18c5
--- /dev/null
+++ b/sw/restat_base_tests/build/restat_base_tests[1]_tests.cmake
@@ -0,0 +1,27 @@
+add_test([=[ParserTest.WellFormedCommand]=] /data/dev/raspberry/restat/github/restat/sw/restat_base_tests/build/restat_base_tests [==[--gtest_filter=ParserTest.WellFormedCommand]==] --gtest_also_run_disabled_tests)
+set_tests_properties([=[ParserTest.WellFormedCommand]=] PROPERTIES WORKING_DIRECTORY /data/dev/raspberry/restat/github/restat/sw/restat_base_tests/build SKIP_REGULAR_EXPRESSION [==[\[ SKIPPED \]]==])
+add_test([=[ParserTest.IncompleteCommand]=] /data/dev/raspberry/restat/github/restat/sw/restat_base_tests/build/restat_base_tests [==[--gtest_filter=ParserTest.IncompleteCommand]==] --gtest_also_run_disabled_tests)
+set_tests_properties([=[ParserTest.IncompleteCommand]=] PROPERTIES WORKING_DIRECTORY /data/dev/raspberry/restat/github/restat/sw/restat_base_tests/build SKIP_REGULAR_EXPRESSION [==[\[ SKIPPED \]]==])
+add_test([=[ParserTest.IgnoresWhitespace]=] /data/dev/raspberry/restat/github/restat/sw/restat_base_tests/build/restat_base_tests [==[--gtest_filter=ParserTest.IgnoresWhitespace]==] --gtest_also_run_disabled_tests)
+set_tests_properties([=[ParserTest.IgnoresWhitespace]=] PROPERTIES WORKING_DIRECTORY /data/dev/raspberry/restat/github/restat/sw/restat_base_tests/build SKIP_REGULAR_EXPRESSION [==[\[ SKIPPED \]]==])
+add_test([=[ParserTest.BufferOverflowHandling]=] /data/dev/raspberry/restat/github/restat/sw/restat_base_tests/build/restat_base_tests [==[--gtest_filter=ParserTest.BufferOverflowHandling]==] --gtest_also_run_disabled_tests)
+set_tests_properties([=[ParserTest.BufferOverflowHandling]=] PROPERTIES WORKING_DIRECTORY /data/dev/raspberry/restat/github/restat/sw/restat_base_tests/build SKIP_REGULAR_EXPRESSION [==[\[ SKIPPED \]]==])
+add_test([=[CommandTest.BoardIdAndVersion]=] /data/dev/raspberry/restat/github/restat/sw/restat_base_tests/build/restat_base_tests [==[--gtest_filter=CommandTest.BoardIdAndVersion]==] --gtest_also_run_disabled_tests)
+set_tests_properties([=[CommandTest.BoardIdAndVersion]=] PROPERTIES WORKING_DIRECTORY /data/dev/raspberry/restat/github/restat/sw/restat_base_tests/build SKIP_REGULAR_EXPRESSION [==[\[ SKIPPED \]]==])
+add_test([=[CommandTest.ChannelControl]=] /data/dev/raspberry/restat/github/restat/sw/restat_base_tests/build/restat_base_tests [==[--gtest_filter=CommandTest.ChannelControl]==] --gtest_also_run_disabled_tests)
+set_tests_properties([=[CommandTest.ChannelControl]=] PROPERTIES WORKING_DIRECTORY /data/dev/raspberry/restat/github/restat/sw/restat_base_tests/build SKIP_REGULAR_EXPRESSION [==[\[ SKIPPED \]]==])
+add_test([=[CommandTest.AllChannelsControl]=] /data/dev/raspberry/restat/github/restat/sw/restat_base_tests/build/restat_base_tests [==[--gtest_filter=CommandTest.AllChannelsControl]==] --gtest_also_run_disabled_tests)
+set_tests_properties([=[CommandTest.AllChannelsControl]=] PROPERTIES WORKING_DIRECTORY /data/dev/raspberry/restat/github/restat/sw/restat_base_tests/build SKIP_REGULAR_EXPRESSION [==[\[ SKIPPED \]]==])
+add_test([=[CommandTest.TimerCommand]=] /data/dev/raspberry/restat/github/restat/sw/restat_base_tests/build/restat_base_tests [==[--gtest_filter=CommandTest.TimerCommand]==] --gtest_also_run_disabled_tests)
+set_tests_properties([=[CommandTest.TimerCommand]=] PROPERTIES WORKING_DIRECTORY /data/dev/raspberry/restat/github/restat/sw/restat_base_tests/build SKIP_REGULAR_EXPRESSION [==[\[ SKIPPED \]]==])
+add_test([=[CommandTest.MaskCommand]=] /data/dev/raspberry/restat/github/restat/sw/restat_base_tests/build/restat_base_tests [==[--gtest_filter=CommandTest.MaskCommand]==] --gtest_also_run_disabled_tests)
+set_tests_properties([=[CommandTest.MaskCommand]=] PROPERTIES WORKING_DIRECTORY /data/dev/raspberry/restat/github/restat/sw/restat_base_tests/build SKIP_REGULAR_EXPRESSION [==[\[ SKIPPED \]]==])
+add_test([=[CommandTest.PlanCommand]=] /data/dev/raspberry/restat/github/restat/sw/restat_base_tests/build/restat_base_tests [==[--gtest_filter=CommandTest.PlanCommand]==] --gtest_also_run_disabled_tests)
+set_tests_properties([=[CommandTest.PlanCommand]=] PROPERTIES WORKING_DIRECTORY /data/dev/raspberry/restat/github/restat/sw/restat_base_tests/build SKIP_REGULAR_EXPRESSION [==[\[ SKIPPED \]]==])
+add_test([=[CommandTest.ChannelStatus]=] /data/dev/raspberry/restat/github/restat/sw/restat_base_tests/build/restat_base_tests [==[--gtest_filter=CommandTest.ChannelStatus]==] --gtest_also_run_disabled_tests)
+set_tests_properties([=[CommandTest.ChannelStatus]=] PROPERTIES WORKING_DIRECTORY /data/dev/raspberry/restat/github/restat/sw/restat_base_tests/build SKIP_REGULAR_EXPRESSION [==[\[ SKIPPED \]]==])
+add_test([=[CommandTest.AllStatus]=] /data/dev/raspberry/restat/github/restat/sw/restat_base_tests/build/restat_base_tests [==[--gtest_filter=CommandTest.AllStatus]==] --gtest_also_run_disabled_tests)
+set_tests_properties([=[CommandTest.AllStatus]=] PROPERTIES WORKING_DIRECTORY /data/dev/raspberry/restat/github/restat/sw/restat_base_tests/build SKIP_REGULAR_EXPRESSION [==[\[ SKIPPED \]]==])
+add_test([=[CommandTest.SystemReset]=] /data/dev/raspberry/restat/github/restat/sw/restat_base_tests/build/restat_base_tests [==[--gtest_filter=CommandTest.SystemReset]==] --gtest_also_run_disabled_tests)
+set_tests_properties([=[CommandTest.SystemReset]=] PROPERTIES WORKING_DIRECTORY /data/dev/raspberry/restat/github/restat/sw/restat_base_tests/build SKIP_REGULAR_EXPRESSION [==[\[ SKIPPED \]]==])
+set( restat_base_tests_TESTS ParserTest.WellFormedCommand ParserTest.IncompleteCommand ParserTest.IgnoresWhitespace ParserTest.BufferOverflowHandling CommandTest.BoardIdAndVersion CommandTest.ChannelControl CommandTest.AllChannelsControl CommandTest.TimerCommand CommandTest.MaskCommand CommandTest.PlanCommand CommandTest.ChannelStatus CommandTest.AllStatus CommandTest.SystemReset)
diff --git a/sw/restat_base_tests/main.cc b/sw/restat_base_tests/main.cc
new file mode 100644
index 0000000..dc42b1a
--- /dev/null
+++ b/sw/restat_base_tests/main.cc
@@ -0,0 +1,6 @@
+#include "gtest/gtest.h"
+
+int main(int argc, char **argv) {
+ ::testing::InitGoogleTest(&argc, argv);
+ return RUN_ALL_TESTS();
+}
diff --git a/sw/restat_base_tests/mocks/device/buzzer.h b/sw/restat_base_tests/mocks/device/buzzer.h
new file mode 100644
index 0000000..60b6c63
--- /dev/null
+++ b/sw/restat_base_tests/mocks/device/buzzer.h
@@ -0,0 +1,11 @@
+#pragma once
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+void buzzer_beep_stop(void);
+
+#ifdef __cplusplus
+}
+#endif
diff --git a/sw/restat_base_tests/mocks/device/persist.h b/sw/restat_base_tests/mocks/device/persist.h
new file mode 100644
index 0000000..7cbc455
--- /dev/null
+++ b/sw/restat_base_tests/mocks/device/persist.h
@@ -0,0 +1,11 @@
+#pragma once
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+void persist_save_plan(const char *plan);
+
+#ifdef __cplusplus
+}
+#endif
diff --git a/sw/restat_base_tests/mocks/device/relay.h b/sw/restat_base_tests/mocks/device/relay.h
new file mode 100644
index 0000000..e435619
--- /dev/null
+++ b/sw/restat_base_tests/mocks/device/relay.h
@@ -0,0 +1,26 @@
+#pragma once
+
+#include
+#include
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+enum { RELAY_NUM_CHANNELS = 8 };
+
+typedef enum {
+ RELAY_MODE_TOGGLE,
+ RELAY_MODE_TIMER
+} relay_mode_t;
+
+bool relay_init(void);
+void relay_set(uint32_t channel, bool state);
+void relay_set_all(bool state);
+void relay_start_timer(uint32_t channel, uint32_t milliseconds, bool start_on);
+void relay_tick(void);
+void relay_get_status(uint32_t channel, char *buf, uint32_t max_len);
+
+#ifdef __cplusplus
+}
+#endif
diff --git a/sw/restat_base_tests/mocks/device/status_led.h b/sw/restat_base_tests/mocks/device/status_led.h
new file mode 100644
index 0000000..6e1bc9d
--- /dev/null
+++ b/sw/restat_base_tests/mocks/device/status_led.h
@@ -0,0 +1,13 @@
+#pragma once
+
+#include
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+void status_led_write(uint8_t r, uint8_t g, uint8_t b);
+
+#ifdef __cplusplus
+}
+#endif
diff --git a/sw/restat_base_tests/mocks/hardware/watchdog.h b/sw/restat_base_tests/mocks/hardware/watchdog.h
new file mode 100644
index 0000000..b042e03
--- /dev/null
+++ b/sw/restat_base_tests/mocks/hardware/watchdog.h
@@ -0,0 +1,13 @@
+#pragma once
+
+#include
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+void watchdog_reboot(uint32_t pc, uint32_t sp, uint32_t delay_ms);
+
+#ifdef __cplusplus
+}
+#endif
diff --git a/sw/restat_base_tests/mocks/mocks.cc b/sw/restat_base_tests/mocks/mocks.cc
new file mode 100644
index 0000000..90175ca
--- /dev/null
+++ b/sw/restat_base_tests/mocks/mocks.cc
@@ -0,0 +1,121 @@
+#include "mocks.h"
+#include "pico/stdlib.h"
+#include "hardware/watchdog.h"
+#include "device/relay.h"
+#include "device/buzzer.h"
+#include "device/status_led.h"
+#include "device/persist.h"
+#include
+#include
+
+std::queue mock_input_queue;
+std::vector relay_calls;
+static std::string channel_statuses[RELAY_NUM_CHANNELS];
+
+bool buzzer_beep_stop_called = false;
+uint8_t mock_led_r = 0;
+uint8_t mock_led_g = 0;
+uint8_t mock_led_b = 0;
+bool watchdog_reboot_called = false;
+std::string last_saved_plan;
+
+void mock_push_input(const std::string& str) {
+ for (char c : str) {
+ mock_input_queue.push(c);
+ }
+}
+
+void mock_clear_input() {
+ std::queue empty;
+ std::swap(mock_input_queue, empty);
+}
+
+void mock_clear_relay_calls() {
+ relay_calls.clear();
+}
+
+void mock_set_channel_status(uint32_t channel, const std::string& status) {
+ if (channel < RELAY_NUM_CHANNELS) {
+ channel_statuses[channel] = status;
+ }
+}
+
+void mock_clear_system_states() {
+ buzzer_beep_stop_called = false;
+ mock_led_r = 0;
+ mock_led_g = 0;
+ mock_led_b = 0;
+ watchdog_reboot_called = false;
+ last_saved_plan.clear();
+ for (uint32_t i = 0; i < RELAY_NUM_CHANNELS; i++) {
+ channel_statuses[i].clear();
+ }
+}
+
+extern "C" {
+
+int16_t getchar_timeout_us(uint32_t timeout_us) {
+ (void)timeout_us;
+ if (mock_input_queue.empty()) {
+ return PICO_ERROR_TIMEOUT;
+ }
+ char c = mock_input_queue.front();
+ mock_input_queue.pop();
+ return (int16_t)(uint8_t)c;
+}
+
+void watchdog_reboot(uint32_t pc, uint32_t sp, uint32_t delay_ms) {
+ (void)pc;
+ (void)sp;
+ (void)delay_ms;
+ watchdog_reboot_called = true;
+}
+
+bool relay_init(void) {
+ return true;
+}
+
+void relay_set(uint32_t channel, bool state) {
+ RelayMockCall call = {RelayMockCall::SET, channel, state, 0, false};
+ relay_calls.push_back(call);
+}
+
+void relay_set_all(bool state) {
+ RelayMockCall call = {RelayMockCall::SET_ALL, 0, state, 0, false};
+ relay_calls.push_back(call);
+}
+
+void relay_start_timer(uint32_t channel, uint32_t milliseconds, bool start_on) {
+ RelayMockCall call = {RelayMockCall::TIMER, channel, false, milliseconds, start_on};
+ relay_calls.push_back(call);
+}
+
+void relay_tick(void) {
+ // No-op
+}
+
+void relay_get_status(uint32_t channel, char *buf, uint32_t max_len) {
+ if (channel < RELAY_NUM_CHANNELS) {
+ if (!channel_statuses[channel].empty()) {
+ snprintf(buf, max_len, "%s", channel_statuses[channel].c_str());
+ } else {
+ snprintf(buf, max_len, "channel %u: OFF", channel + 1);
+ }
+ }
+}
+
+void buzzer_beep_stop(void) {
+ buzzer_beep_stop_called = true;
+}
+
+void status_led_write(uint8_t r, uint8_t g, uint8_t b) {
+ mock_led_r = r;
+ mock_led_g = g;
+ mock_led_b = b;
+}
+
+void persist_save_plan(const char *plan) {
+ last_saved_plan = plan;
+}
+
+}
diff --git a/sw/restat_base_tests/mocks/mocks.h b/sw/restat_base_tests/mocks/mocks.h
new file mode 100644
index 0000000..02486ef
--- /dev/null
+++ b/sw/restat_base_tests/mocks/mocks.h
@@ -0,0 +1,37 @@
+#pragma once
+
+#include
+#include
+#include
+#include
+#include
+
+// Mock queue for getchar_timeout_us
+extern std::queue mock_input_queue;
+void mock_push_input(const std::string& str);
+void mock_clear_input();
+
+// Mock call registry for relay
+struct RelayMockCall {
+ enum CallType {
+ SET, SET_ALL, TIMER
+ } type;
+ uint32_t channel;
+ bool state;
+ uint32_t timer_ms;
+ bool timer_start_on;
+};
+
+extern std::vector relay_calls;
+void mock_clear_relay_calls();
+void mock_set_channel_status(uint32_t channel, const std::string& status);
+
+// Mock states for buzzer, status_led, watchdog, persist
+extern bool buzzer_beep_stop_called;
+extern uint8_t mock_led_r;
+extern uint8_t mock_led_g;
+extern uint8_t mock_led_b;
+extern bool watchdog_reboot_called;
+extern std::string last_saved_plan;
+
+void mock_clear_system_states();
diff --git a/sw/restat_base_tests/mocks/pico/stdlib.h b/sw/restat_base_tests/mocks/pico/stdlib.h
new file mode 100644
index 0000000..9aa68d1
--- /dev/null
+++ b/sw/restat_base_tests/mocks/pico/stdlib.h
@@ -0,0 +1,16 @@
+#pragma once
+
+#include
+#include
+
+#define PICO_ERROR_TIMEOUT (-1)
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+int16_t getchar_timeout_us(uint32_t timeout_us);
+
+#ifdef __cplusplus
+}
+#endif
diff --git a/sw/restat_base_tests/restat_base_tests.code-workspace b/sw/restat_base_tests/restat_base_tests.code-workspace
new file mode 100644
index 0000000..5709732
--- /dev/null
+++ b/sw/restat_base_tests/restat_base_tests.code-workspace
@@ -0,0 +1,8 @@
+{
+ "folders": [
+ {
+ "path": "."
+ }
+ ],
+ "settings": {}
+}
diff --git a/sw/restat_base_tests/tests/test_command.cc b/sw/restat_base_tests/tests/test_command.cc
new file mode 100644
index 0000000..e4fd03b
--- /dev/null
+++ b/sw/restat_base_tests/tests/test_command.cc
@@ -0,0 +1,178 @@
+#include "gtest/gtest.h"
+#include "mocks/mocks.h"
+#include "command/dispatcher.h"
+#include
+#include
+#include
+
+class StdoutCapture {
+public:
+ StdoutCapture() {
+ fflush(stdout);
+ old_stdout = dup(fileno(stdout));
+ if (pipe(pipe_fds) == 0) {
+ dup2(pipe_fds[1], fileno(stdout));
+ close(pipe_fds[1]);
+ }
+ }
+ std::string GetCapture() {
+ fflush(stdout);
+ dup2(old_stdout, fileno(stdout));
+ close(old_stdout);
+
+ std::string out;
+ char buf[1024];
+ ssize_t bytes_read;
+ int flags = fcntl(pipe_fds[0], F_GETFL, 0);
+ fcntl(pipe_fds[0], F_SETFL, flags | O_NONBLOCK);
+ while ((bytes_read = read(pipe_fds[0], buf, sizeof(buf) - 1)) > 0) {
+ buf[bytes_read] = '\0';
+ out += buf;
+ }
+ close(pipe_fds[0]);
+ return out;
+ }
+private:
+ int old_stdout;
+ int pipe_fds[2];
+};
+
+class CommandTest : public ::testing::Test {
+protected:
+ void SetUp() override {
+ mock_clear_relay_calls();
+ mock_clear_system_states();
+ }
+};
+
+TEST_F(CommandTest, BoardIdAndVersion) {
+ {
+ StdoutCapture capture;
+ command_dispatch("rs#sys#id#end");
+ std::string output = capture.GetCapture();
+ EXPECT_EQ(output, "");
+ }
+ {
+ StdoutCapture capture;
+ command_dispatch("rs#sys#version#end");
+ std::string output = capture.GetCapture();
+ EXPECT_EQ(output, "");
+ }
+}
+
+TEST_F(CommandTest, ChannelControl) {
+ StdoutCapture capture;
+ command_dispatch("rs#ch#1#on#end");
+ std::string output = capture.GetCapture();
+ EXPECT_EQ(output, "");
+ ASSERT_EQ(relay_calls.size(), 1);
+ EXPECT_EQ(relay_calls[0].type, RelayMockCall::SET);
+ EXPECT_EQ(relay_calls[0].channel, 0);
+ EXPECT_TRUE(relay_calls[0].state);
+
+ mock_clear_relay_calls();
+ {
+ StdoutCapture capture2;
+ command_dispatch("rs#ch#8#off#end");
+ std::string output2 = capture2.GetCapture();
+ EXPECT_EQ(output2, "");
+ }
+ ASSERT_EQ(relay_calls.size(), 1);
+ EXPECT_EQ(relay_calls[0].type, RelayMockCall::SET);
+ EXPECT_EQ(relay_calls[0].channel, 7);
+ EXPECT_FALSE(relay_calls[0].state);
+}
+
+TEST_F(CommandTest, AllChannelsControl) {
+ {
+ StdoutCapture capture;
+ command_dispatch("rs#all#on#end");
+ std::string output = capture.GetCapture();
+ EXPECT_EQ(output, "");
+ ASSERT_EQ(relay_calls.size(), 1);
+ EXPECT_EQ(relay_calls[0].type, RelayMockCall::SET_ALL);
+ EXPECT_TRUE(relay_calls[0].state);
+ EXPECT_EQ(mock_led_r, 255);
+ EXPECT_EQ(mock_led_g, 255);
+ EXPECT_EQ(mock_led_b, 255);
+ }
+ {
+ mock_clear_relay_calls();
+ StdoutCapture capture;
+ command_dispatch("rs#all#off#end");
+ std::string output = capture.GetCapture();
+ EXPECT_EQ(output, "");
+ ASSERT_EQ(relay_calls.size(), 1);
+ EXPECT_EQ(relay_calls[0].type, RelayMockCall::SET_ALL);
+ EXPECT_FALSE(relay_calls[0].state);
+ EXPECT_EQ(mock_led_r, 0);
+ EXPECT_EQ(mock_led_g, 0);
+ EXPECT_EQ(mock_led_b, 0);
+ }
+}
+
+TEST_F(CommandTest, TimerCommand) {
+ StdoutCapture capture;
+ command_dispatch("rs#ch#3#tmr#15000#end");
+ std::string output = capture.GetCapture();
+ EXPECT_EQ(output, "");
+ ASSERT_EQ(relay_calls.size(), 1);
+ EXPECT_EQ(relay_calls[0].type, RelayMockCall::TIMER);
+ EXPECT_EQ(relay_calls[0].channel, 2);
+ EXPECT_EQ(relay_calls[0].timer_ms, 15000);
+ EXPECT_TRUE(relay_calls[0].timer_start_on);
+}
+
+TEST_F(CommandTest, MaskCommand) {
+ StdoutCapture capture;
+ command_dispatch("rs#all#mask#10101010#end");
+ std::string output = capture.GetCapture();
+ EXPECT_EQ(output, "");
+ ASSERT_EQ(relay_calls.size(), 8);
+ for (uint32_t i = 0; i < 8; i++) {
+ EXPECT_EQ(relay_calls[i].type, RelayMockCall::SET);
+ EXPECT_EQ(relay_calls[i].channel, i);
+ EXPECT_EQ(relay_calls[i].state, (i % 2 == 0));
+ }
+}
+
+TEST_F(CommandTest, PlanCommand) {
+ StdoutCapture capture;
+ command_dispatch("rs#all#plan#ch#1#on#ch#2#tmroff#15000#end");
+ std::string output = capture.GetCapture();
+ EXPECT_EQ(output, "");
+ EXPECT_EQ(last_saved_plan, "rs#all#plan#ch#1#on#ch#2#tmroff#15000#end");
+ ASSERT_EQ(relay_calls.size(), 2);
+ EXPECT_EQ(relay_calls[0].type, RelayMockCall::SET);
+ EXPECT_EQ(relay_calls[0].channel, 0);
+ EXPECT_TRUE(relay_calls[0].state);
+
+ EXPECT_EQ(relay_calls[1].type, RelayMockCall::TIMER);
+ EXPECT_EQ(relay_calls[1].channel, 1);
+ EXPECT_EQ(relay_calls[1].timer_ms, 15000);
+ EXPECT_FALSE(relay_calls[1].timer_start_on);
+}
+
+TEST_F(CommandTest, ChannelStatus) {
+ mock_set_channel_status(3, "channel 4: ON (timer)");
+ StdoutCapture capture;
+ command_dispatch("rs#ch#4#stat#end");
+ std::string output = capture.GetCapture();
+ EXPECT_EQ(output, "");
+}
+
+TEST_F(CommandTest, AllStatus) {
+ StdoutCapture capture;
+ command_dispatch("rs#all#stat#end");
+ std::string output = capture.GetCapture();
+ EXPECT_EQ(output, "");
+}
+
+TEST_F(CommandTest, SystemReset) {
+ StdoutCapture capture;
+ command_dispatch("rs#sys#reset#end");
+ std::string output = capture.GetCapture();
+ EXPECT_EQ(output, "");
+ EXPECT_TRUE(buzzer_beep_stop_called);
+ EXPECT_TRUE(watchdog_reboot_called);
+}
diff --git a/sw/restat_base_tests/tests/test_parser.cc b/sw/restat_base_tests/tests/test_parser.cc
new file mode 100644
index 0000000..078dc9e
--- /dev/null
+++ b/sw/restat_base_tests/tests/test_parser.cc
@@ -0,0 +1,40 @@
+#include "gtest/gtest.h"
+#include "mocks/mocks.h"
+#include "command/parser.h"
+
+class ParserTest : public ::testing::Test {
+protected:
+ void SetUp() override {
+ mock_clear_input();
+ }
+};
+
+TEST_F(ParserTest, WellFormedCommand) {
+ mock_push_input("");
+ char buf[64];
+ bool success = parser_get_command(buf, sizeof(buf));
+ EXPECT_TRUE(success);
+ EXPECT_STREQ(buf, "rs#ch#1#on#end");
+}
+
+TEST_F(ParserTest, IncompleteCommand) {
+ mock_push_input("\n");
+ char buf[64];
+ bool success = parser_get_command(buf, sizeof(buf));
+ EXPECT_TRUE(success);
+ EXPECT_STREQ(buf, "rs#ch#2#off#end");
+}
+
+TEST_F(ParserTest, BufferOverflowHandling) {
+ mock_push_input("<12345678901234567890>");
+ char buf[10];
+ bool success = parser_get_command(buf, sizeof(buf));
+ EXPECT_FALSE(success);
+}
diff --git a/sw/restatdesk/.clang-format b/sw/restatdesk/.clang-format
new file mode 100644
index 0000000..8c75b63
--- /dev/null
+++ b/sw/restatdesk/.clang-format
@@ -0,0 +1,6 @@
+BasedOnStyle: LLVM
+IndentWidth: 4
+TabWidth: 4
+UseTab: Never
+NamespaceIndentation: All
+AlignAfterOpenBracket: BlockIndent
diff --git a/sw/restat-desktop/build/.editorconfig b/sw/restatdesk/.editorconfig
similarity index 100%
rename from sw/restat-desktop/build/.editorconfig
rename to sw/restatdesk/.editorconfig
diff --git a/sw/restatdesk/.vscode/.editorconfig b/sw/restatdesk/.vscode/.editorconfig
new file mode 100644
index 0000000..b92ec54
--- /dev/null
+++ b/sw/restatdesk/.vscode/.editorconfig
@@ -0,0 +1,3 @@
+[**.*]
+indent_style = tab
+tab_width = 4
\ No newline at end of file
diff --git a/sw/restatdesk/.vscode/launch.json b/sw/restatdesk/.vscode/launch.json
new file mode 100644
index 0000000..19e21dc
--- /dev/null
+++ b/sw/restatdesk/.vscode/launch.json
@@ -0,0 +1,30 @@
+{
+ "version": "0.2.0",
+ "configurations": [
+ {
+ "name": "restatdesk debug",
+ "type": "cppdbg",
+ "request": "launch",
+ "program": "${workspaceFolder}/build/restatdesk",
+ "args": [],
+ "stopAtEntry": false,
+ "cwd": "${fileDirname}",
+ "environment": [],
+ "externalConsole": false,
+ "MIMode": "gdb",
+ "preLaunchTask": "restatdesk build",
+ "setupCommands": [
+ {
+ "description": "Enable pretty-printing for gdb",
+ "text": "-enable-pretty-printing",
+ "ignoreFailures": true
+ },
+ {
+ "description": "Set Disassembly Flavor to Intel",
+ "text": "-gdb-set disassembly-flavor intel",
+ "ignoreFailures": true
+ }
+ ]
+ }
+ ]
+}
\ No newline at end of file
diff --git a/sw/restatdesk/.vscode/settings.json b/sw/restatdesk/.vscode/settings.json
new file mode 100644
index 0000000..bd659fc
--- /dev/null
+++ b/sw/restatdesk/.vscode/settings.json
@@ -0,0 +1,40 @@
+{
+ "files.associations": {
+ "*.h": "cpp"
+ },
+ "clangd.fallbackFlags": [
+ "-std=c++23",
+ "-I${workspaceFolder}",
+ "-I/usr/include/gtkmm-4.0",
+ "-I/usr/lib/x86_64-linux-gnu/gtkmm-4.0/include",
+ "-I/usr/include/giomm-2.68",
+ "-I/usr/lib/x86_64-linux-gnu/giomm-2.68/include",
+ "-I/usr/include/glib-2.0",
+ "-I/usr/lib/x86_64-linux-gnu/glib-2.0/include",
+ "-I/usr/include/libmount",
+ "-I/usr/include/blkid",
+ "-I/usr/include/glibmm-2.68",
+ "-I/usr/lib/x86_64-linux-gnu/glibmm-2.68/include",
+ "-I/usr/include/sigc++-3.0",
+ "-I/usr/lib/x86_64-linux-gnu/sigc++-3.0/include",
+ "-I/usr/include/gtk-4.0",
+ "-I/usr/include/pango-1.0",
+ "-I/usr/include/harfbuzz",
+ "-I/usr/include/freetype2",
+ "-I/usr/include/libpng16",
+ "-I/usr/include/fribidi",
+ "-I/usr/include/cairo",
+ "-I/usr/include/pixman-1",
+ "-I/usr/include/gdk-pixbuf-2.0",
+ "-I/usr/include/x86_64-linux-gnu",
+ "-I/usr/include/webp",
+ "-I/usr/include/graphene-1.0",
+ "-I/usr/lib/x86_64-linux-gnu/graphene-1.0/include",
+ "-I/usr/include/cairomm-1.16",
+ "-I/usr/lib/x86_64-linux-gnu/cairomm-1.16/include",
+ "-I/usr/include/pangomm-2.48",
+ "-I/usr/lib/x86_64-linux-gnu/pangomm-2.48/include",
+ "-I/usr/include/gtk-4.0/unix-print",
+ "-pthread"
+ ]
+}
\ No newline at end of file
diff --git a/sw/restatdesk/.vscode/tasks.json b/sw/restatdesk/.vscode/tasks.json
new file mode 100644
index 0000000..b619073
--- /dev/null
+++ b/sw/restatdesk/.vscode/tasks.json
@@ -0,0 +1,26 @@
+{
+ "version": "2.0.0",
+ "tasks": [
+ {
+ "label": "restatdesk build",
+ "type": "shell",
+ "command": "",
+ "args": [
+ "make",
+ "--directory=${workspaceFolder}/build"
+ ],
+ "problemMatcher": []
+ },
+ {
+ "label": "restatdesk clean",
+ "type": "shell",
+ "command": "",
+ "args": [
+ "make",
+ "--directory=${workspaceFolder}/build",
+ "clean"
+ ],
+ "problemMatcher": []
+ }
+ ]
+}
diff --git a/sw/restatdesk/app_controller.cc b/sw/restatdesk/app_controller.cc
new file mode 100644
index 0000000..6a4f410
--- /dev/null
+++ b/sw/restatdesk/app_controller.cc
@@ -0,0 +1,389 @@
+////////////////////////////////////////////////////////////////////////////////////////////////////////
+///
+/// app_controller.cc
+/// Copyright (C) 2025 - 2026 Vladimir Roncevic
+///
+/// restatdesk 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.
+///
+/// restatdesk 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
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+using namespace Electux::App;
+
+namespace {
+ constexpr std::chrono::milliseconds cThreadSleepDuration{100};
+} // namespace
+
+AppController::AppController(
+ std::unique_ptr configManager,
+ std::unique_ptr comChannel,
+ std::unique_ptr comConfigurator,
+ std::unique_ptr logger,
+ std::unique_ptr commandFormatter,
+ std::unique_ptr responseProcessor
+)
+ : m_configManager(std::move(configManager)),
+ m_comChannel(std::move(comChannel)),
+ m_comConfigurator(std::move(comConfigurator)),
+ m_logger(std::move(logger)),
+ m_commandFormatter(std::move(commandFormatter)),
+ m_responseProcessor(std::move(responseProcessor)) {}
+
+AppController::~AppController() {
+ m_stopThread = true;
+ if (m_readThread.joinable()) {
+ m_readThread.join();
+ }
+ if (m_comChannel) {
+ m_comChannel->close();
+ }
+}
+
+void AppController::startup() {
+ m_configManager->init();
+ configureLogger();
+ configureComChannel();
+
+ m_stopThread = false;
+ m_readThread = std::thread(&AppController::readLoop, this);
+
+ m_logger->log("Application started.", Logger::LogLevel::Info);
+
+ if (m_commandFormatter && m_comChannel && m_comChannel->isOpen()) {
+ std::string cmdBoardId = m_commandFormatter->getCommandBoardId();
+
+ if (!cmdBoardId.empty()) {
+ std::vector cmdBytes(cmdBoardId.begin(), cmdBoardId.end());
+ m_comChannel->write(cmdBytes);
+ }
+
+ std::string cmdVersion = m_commandFormatter->getCommandVersion();
+
+ if (!cmdVersion.empty()) {
+ std::vector cmdBytes(cmdVersion.begin(), cmdVersion.end());
+ m_comChannel->write(cmdBytes);
+ }
+
+ std::string cmdAllStat =
+ m_commandFormatter->getCommandStatusAllChannels();
+
+ if (!cmdAllStat.empty()) {
+ std::vector cmdBytes(cmdAllStat.begin(), cmdAllStat.end());
+ m_comChannel->write(cmdBytes);
+ }
+ }
+}
+
+void AppController::turnOnAllChannels() {
+ if (m_commandFormatter && m_comChannel && m_comChannel->isOpen()) {
+ std::string cmd = m_commandFormatter->getCommandOnAllChannels();
+ if (!cmd.empty()) {
+ std::vector cmdBytes(cmd.begin(), cmd.end());
+ m_comChannel->write(cmdBytes);
+ }
+ }
+}
+
+void AppController::turnOffAllChannels() {
+ if (m_commandFormatter && m_comChannel && m_comChannel->isOpen()) {
+ std::string cmd = m_commandFormatter->getCommandOffAllChannels();
+ if (!cmd.empty()) {
+ std::vector cmdBytes(cmd.begin(), cmd.end());
+ m_comChannel->write(cmdBytes);
+ }
+ }
+}
+
+void AppController::requestAllChannelsStatus() {
+ if (m_commandFormatter && m_comChannel && m_comChannel->isOpen()) {
+ std::string cmd = m_commandFormatter->getCommandStatusAllChannels();
+ if (!cmd.empty()) {
+ std::vector cmdBytes(cmd.begin(), cmd.end());
+ m_comChannel->write(cmdBytes);
+ }
+ }
+}
+
+void AppController::resetSystem() {
+ if (m_commandFormatter && m_comChannel && m_comChannel->isOpen()) {
+ std::string cmd = m_commandFormatter->getCommandReset();
+ if (!cmd.empty()) {
+ std::vector cmdBytes(cmd.begin(), cmd.end());
+ m_comChannel->write(cmdBytes);
+ std::this_thread::sleep_for(std::chrono::milliseconds(100));
+ m_comChannel->close();
+ }
+ }
+}
+
+void AppController::requestBoardId() {
+ if (m_commandFormatter && m_comChannel && m_comChannel->isOpen()) {
+ std::string cmd = m_commandFormatter->getCommandBoardId();
+ if (!cmd.empty()) {
+ std::vector cmdBytes(cmd.begin(), cmd.end());
+ m_comChannel->write(cmdBytes);
+ }
+ }
+}
+
+void AppController::requestVersion() {
+ if (m_commandFormatter && m_comChannel && m_comChannel->isOpen()) {
+ std::string cmd = m_commandFormatter->getCommandVersion();
+ if (!cmd.empty()) {
+ std::vector cmdBytes(cmd.begin(), cmd.end());
+ m_comChannel->write(cmdBytes);
+ }
+ }
+}
+
+void AppController::shutdown() {
+ m_stopThread = true;
+
+ if (m_readThread.joinable()) {
+ m_readThread.join();
+ }
+
+ if (m_comChannel) {
+ m_comChannel->close();
+ }
+
+ if (m_logger) {
+ m_logger->log("Application shutting down.", Logger::LogLevel::Info);
+ m_logger->close();
+ }
+
+ m_configManager->store(true);
+}
+
+void AppController::configureLogger() {
+ if (!m_logger) {
+ return;
+ }
+
+ m_logger->close();
+
+ auto &config = getModel();
+ auto pathKey = config.toString(Model::ModelLogKey::FilePath);
+ auto levelKey = config.toString(Model::ModelLogKey::LogLevel);
+
+ m_logger->setOutputFile(config.getEntity(pathKey));
+
+ uint32_t levelIdx =
+ static_cast(std::stoul(config.getEntity(levelKey)));
+ m_logger->setLevel(static_cast(levelIdx));
+
+ m_logger->open();
+}
+
+void AppController::configureComChannel() {
+ if (!m_comChannel || !m_comConfigurator) {
+ return;
+ }
+
+ m_comChannel->close();
+ m_comConfigurator->configure(getModel(), m_comChannel.get());
+}
+
+const Model::IModel &AppController::getModel() const {
+ return m_configManager->getConfig();
+}
+
+void AppController::onSetupChanged(const Model::SettingsSetup &setup) {
+ const auto &oldConfig = m_configManager->getConfig();
+ auto &newConfig = *setup.m_config;
+
+ bool logChanged = hasLoggerConfigChanged(oldConfig, newConfig);
+ bool comChanged = hasSerialConfigChanged(oldConfig, newConfig) ||
+ hasGeneralConfigChanged(oldConfig, newConfig) ||
+ hasBleConfigChanged(oldConfig, newConfig);
+
+ m_configManager->setConfig(newConfig);
+ m_configManager->store();
+
+ if (logChanged) {
+ configureLogger();
+ }
+
+ if (comChanged) {
+ configureComChannel();
+ }
+
+ getModel().emit_changed();
+}
+
+void AppController::executePlan() {
+ if (m_commandFormatter && m_comChannel && m_comChannel->isOpen()) {
+ std::vector states;
+ for (size_t i = 0; i < Model::Channel::cNumOfChannels; ++i) {
+ states.push_back(getModel().getChannelState(i));
+ }
+ std::string cmd = m_commandFormatter->getCommandPlan(states);
+ if (!cmd.empty()) {
+ m_logger->log("Executing variable plan on channel: " + cmd, Logger::LogLevel::Info);
+ std::vector cmdBytes(cmd.begin(), cmd.end());
+ m_comChannel->write(cmdBytes);
+ m_signalDataReceived.emit(cmd);
+ }
+ }
+}
+
+
+
+bool AppController::hasSerialConfigChanged(
+ const Model::IModel &oldConfig, const Model::IModel &newConfig
+) {
+ for (int k = static_cast(Model::ModelSerialKey::Device);
+ k <= static_cast(Model::ModelSerialKey::Flow); ++k) {
+ auto key = oldConfig.toString(static_cast(k));
+
+ if (oldConfig.getEntity(key) != newConfig.getEntity(key)) {
+ return true;
+ }
+ }
+ return false;
+}
+
+bool AppController::hasLoggerConfigChanged(
+ const Model::IModel &oldConfig, const Model::IModel &newConfig
+) {
+ for (int k = static_cast(Model::ModelLogKey::FilePath);
+ k <= static_cast(Model::ModelLogKey::LogLevel); ++k) {
+ auto key = oldConfig.toString(static_cast(k));
+
+ if (oldConfig.getEntity(key) != newConfig.getEntity(key)) {
+ return true;
+ }
+ }
+ return false;
+}
+
+bool AppController::hasGeneralConfigChanged(
+ const Model::IModel &oldConfig, const Model::IModel &newConfig
+) {
+ for (int k = static_cast(Model::ModelGeneralKey::ComType);
+ k <= static_cast(Model::ModelGeneralKey::TcpPort); ++k) {
+ auto key = oldConfig.toString(static_cast(k));
+
+ if (oldConfig.getEntity(key) != newConfig.getEntity(key)) {
+ return true;
+ }
+ }
+ return false;
+}
+
+bool AppController::hasBleConfigChanged(
+ const Model::IModel &oldConfig, const Model::IModel &newConfig
+) {
+ for (int k = static_cast(Model::ModelBleKey::Address);
+ k <= static_cast(Model::ModelBleKey::TxUuid); ++k) {
+ auto key = oldConfig.toString(static_cast(k));
+
+ if (oldConfig.getEntity(key) != newConfig.getEntity(key)) {
+ return true;
+ }
+ }
+ return false;
+}
+
+sigc::signal AppController::signal_data_received() {
+ return m_signalDataReceived;
+}
+
+void AppController::readLoop() {
+ while (!m_stopThread) {
+ try {
+ if (m_comChannel && m_comChannel->isOpen()) {
+ std::vector buffer;
+ m_comChannel->read(buffer, 1);
+
+ if (!m_stopThread && !m_comChannel->isOpen()) {
+ std::string errorMsg = "\n[Error] Connection lost.";
+ auto *switchable =
+ dynamic_cast(m_comChannel.get());
+ if (switchable) {
+ if (switchable->getActiveCom() ==
+ switchable->getSerialCom()) {
+ errorMsg =
+ "\n[Error] Serial port is no longer available "
+ "(I/O error). Device disconnected.";
+ } else if (
+ switchable->getActiveCom() ==
+ switchable->getTcpCom()
+ ) {
+ errorMsg =
+ "\n[Error] TCP connection lost (I/O error).";
+ } else if (
+ switchable->getActiveCom() ==
+ switchable->getBleCom()
+ ) {
+ errorMsg =
+ "\n[Error] BLE connection lost (I/O error).";
+ }
+ }
+ m_signalDataReceived.emit(errorMsg);
+ if (m_logger) {
+ m_logger->log(
+ "Communication channel lost (I/O error).",
+ Logger::LogLevel::Error
+ );
+ }
+ } else if (!buffer.empty()) {
+ std::string dataStr(buffer.begin(), buffer.end());
+
+ if (m_responseProcessor) {
+ auto payloads = m_responseProcessor->process(dataStr);
+
+ for (const auto &payload : payloads) {
+ m_signalDataReceived.emit(payload);
+ }
+
+ } else {
+ m_signalDataReceived.emit(dataStr);
+ }
+ }
+
+ } else {
+ static auto lastReconnectTry = std::chrono::steady_clock::now();
+ auto now = std::chrono::steady_clock::now();
+ if (now - lastReconnectTry >= std::chrono::seconds(2)) {
+ lastReconnectTry = now;
+ if (m_logger) {
+ m_logger->log(
+ "Communication channel closed. Attempting to "
+ "reconnect...",
+ Logger::LogLevel::Warning
+ );
+ }
+ configureComChannel();
+ }
+ std::this_thread::sleep_for(cThreadSleepDuration);
+ }
+ } catch (...) {
+ std::this_thread::sleep_for(cThreadSleepDuration);
+ }
+ }
+}
diff --git a/sw/restatdesk/app_controller.h b/sw/restatdesk/app_controller.h
new file mode 100644
index 0000000..3b685f5
--- /dev/null
+++ b/sw/restatdesk/app_controller.h
@@ -0,0 +1,110 @@
+////////////////////////////////////////////////////////////////////////////////////////////////////////
+///
+/// app_controller.h
+/// Copyright (C) 2025 - 2026 Vladimir Roncevic
+///
+/// restatdesk 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.
+///
+/// restatdesk 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
+#include
+
+namespace Electux::App::Com {
+ class ICom;
+ class IComConfigurator;
+} // namespace Electux::App::Com
+namespace Electux::App::Logger {
+ class ILog;
+}
+namespace Electux::App::Command {
+ class ICommandFormatter;
+ class IResponseProcessor;
+}
+
+namespace Electux::App {
+ ////////////////////////////////////////////////////////////////////////////////////////////////////
+ /// @class AppController
+ /// @brief Coordination controller managing config loading, storing, and
+ /// model change propagation.
+ ////////////////////////////////////////////////////////////////////////////////////////////////////
+ class AppController : public IAppController {
+ public:
+ AppController(
+ std::unique_ptr configManager,
+ std::unique_ptr comChannel,
+ std::unique_ptr comConfigurator,
+ std::unique_ptr logger,
+ std::unique_ptr commandFormatter,
+ std::unique_ptr responseProcessor
+ );
+ virtual ~AppController() override;
+
+ AppController(const AppController &) = delete;
+ AppController &operator=(const AppController &) = delete;
+
+ void startup() override;
+ void shutdown() override;
+
+ const Model::IModel &getModel() const override;
+
+ void onSetupChanged(const Model::SettingsSetup &setup) override;
+
+ void executePlan() override;
+
+ sigc::signal signal_data_received() override;
+
+ void turnOnAllChannels() override;
+ void turnOffAllChannels() override;
+ void requestAllChannelsStatus() override;
+ void resetSystem() override;
+ void requestBoardId() override;
+ void requestVersion() override;
+
+ private:
+ void configureLogger();
+ void configureComChannel();
+ bool hasSerialConfigChanged(
+ const Model::IModel &oldConfig, const Model::IModel &newConfig
+ );
+ bool hasGeneralConfigChanged(
+ const Model::IModel &oldConfig, const Model::IModel &newConfig
+ );
+ bool hasBleConfigChanged(
+ const Model::IModel &oldConfig, const Model::IModel &newConfig
+ );
+ bool hasLoggerConfigChanged(
+ const Model::IModel &oldConfig, const Model::IModel &newConfig
+ );
+
+ void readLoop();
+
+ std::unique_ptr m_configManager;
+ std::unique_ptr m_comChannel;
+ std::unique_ptr m_comConfigurator;
+ std::unique_ptr m_logger;
+ std::unique_ptr m_commandFormatter;
+ std::unique_ptr m_responseProcessor;
+
+ std::thread m_readThread;
+ std::atomic m_stopThread{false};
+ sigc::signal m_signalDataReceived;
+ };
+} // namespace Electux::App
diff --git a/sw/restatdesk/app_controller_factory.cc b/sw/restatdesk/app_controller_factory.cc
new file mode 100644
index 0000000..7f87174
--- /dev/null
+++ b/sw/restatdesk/app_controller_factory.cc
@@ -0,0 +1,42 @@
+////////////////////////////////////////////////////////////////////////////////////////////////////////
+///
+/// app_controller_factory.cc
+/// Copyright (C) 2025 - 2026 Vladimir Roncevic
+///
+/// restatdesk 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.
+///
+/// restatdesk 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 Electux::App {
+ std::unique_ptr createAppController(
+ std::unique_ptr configManager,
+ std::unique_ptr comChannel,
+ std::unique_ptr comConfigurator,
+ std::unique_ptr logger,
+ std::unique_ptr