14 Commits

Author SHA1 Message Date
2e8a86bc54 Added return code when modubs server init fails 2025-07-08 16:08:49 +02:00
224adccf6b testing precommit hook 2025-07-08 16:06:37 +02:00
9b7159d5a4 added formatting 2025-07-08 16:06:11 +02:00
bc327acc41 docs: Add Doxygen comments to library files
Added Doxygen-style comments to all C source and header files in the
 and  directories. This improves
code documentation and enables VSCode tooltip help.

Additionally, short inline comments were added to all global variables
for better clarity.
2025-07-08 15:48:13 +02:00
c9b0f38576 feat(lib): Introduce adc_sensor library
Adds a new `adc_sensor` library to abstract reading analog values from ADC channels. The output of this library is currently simulated.

This library is now used by the `modbus_server` to read the motor current and the main supply voltage, replacing the previous implementation. This change improves modularity by centralizing ADC-related code into a dedicated module.

The build system has been updated to include the new library.
2025-07-08 15:19:44 +02:00
edf0fb2563 feat(slave_node): Add HSI clock configuration and cleanup
Adds a commented-out clock configuration to the  file. This allows switching the clock source from the external high-speed oscillator (HSE) to the internal high-speed oscillator (HSI), which can be useful if an external crystal is not present.

Also, removes the debug log level for the settings subsystem from the project configuration.
2025-07-08 15:06:31 +02:00
537d76ef5d feat(app): Integrate application versioning
This commit introduces application versioning, exposing version information through the Modbus server and logging it at startup.

- Add  to provide version information
- Update  to log the application version at startup
- Update  to expose firmware version via Modbus
- Add file association for  in
2025-07-08 14:41:01 +02:00
45d011952f fix(valve): Correct VND7050AJ initialization and pin configuration
- Initialize RST pin as active to keep VND7050AJ out of reset state
- Clarify S0/S1 pins as output select pins with descriptive comments
- Add initialization logging to show configured max open/close times
- Ensure proper valve controller startup sequence
2025-07-03 19:04:20 +02:00
bb25134b6c feat(modbus): Implement persistent and improved reconfiguration for Modbus server
This commit enhances the Modbus server's configuration handling by:

- Loading saved baudrate and unit ID settings during initialization, ensuring persistence across reboots.
- Providing improved feedback during `modbus_reconfigure`, including logging for successful changes and informing the user when a device restart is required for changes to take effect.
- Saving new configuration settings even if immediate reinitialization fails, allowing them to be applied on the next boot.
2025-07-03 18:59:01 +02:00
9f96384aa5 fix(cdc-acm): Correct CDC ACM overlay configuration
This commit fixes an issue in the `cdc-acm.overlay` file.
2025-07-03 18:57:06 +02:00
b543579393 feat(modbus): Add supply voltage register and display in tool
This commit introduces a new Modbus input register for the system's supply voltage.

- The `modbus-registers.de.md` documentation is updated to include the `SUPPLY_VOLTAGE_MV` register at address `0x00F5` within the system block.
- The `modbus_server.h` header defines the new register.
- The `modbus_server.c` implementation provides a fixed value (12300 mV) for this register.
- The `modbus_tool.py` script is updated to read and display this new supply voltage value in the UI.

This lays the groundwork for integrating actual voltage measurements in the future.
2025-07-03 18:47:48 +02:00
69cf7e9511 feat(valve): Implement GPIO control for VND7050AJ
This commit implements the real valve control using the GPIOs connected to the VND7050AJ driver.

- The `weact_stm32g431_core.overlay` is updated with a specific compatible string and a device tree label for the valve controller.
- `valve.h` is extended to include GPIO device specifications.
- `valve.c` now initializes and controls the GPIOs for opening and closing the valve, including the reset logic. The IN0 and IN1 pins are interlocked to prevent simultaneous activation. The RST pin is activated before each movement and deactivated afterward.

This replaces the previous virtual/simulated valve logic with actual hardware control.
2025-07-03 18:17:31 +02:00
8df7aef51b Removed unused lib dir 2025-07-03 17:47:48 +02:00
f6ee0a5122 feat(weact_stm32g431_core): Configure VND7050AJ driver pins in overlay
Updated the weact_stm32g431_core.overlay to define the GPIO and ADC
pin assignments for the VND7050AJ driver. This includes:
- Digital I/O pins (IN0, IN1, RST, S0, S1, SEN) configured as GPIOs.
- Analog input pin (MULTISENSE/PA0) configured for ADC1.
2025-07-03 17:39:04 +02:00
47 changed files with 1207 additions and 453 deletions

View File

@@ -0,0 +1,7 @@
feat(modbus): Implement persistent and improved reconfiguration for Modbus server
This commit enhances the Modbus server's configuration handling by:
- Loading saved baudrate and unit ID settings during initialization, ensuring persistence across reboots.
- Providing improved feedback during `modbus_reconfigure`, including logging for successful changes and informing the user when a device restart is required for changes to take effect.
- Saving new configuration settings even if immediate reinitialization fails, allowing them to be applied on the next boot.

View File

@@ -38,6 +38,7 @@ Alle Register sind in einer einzigen, durchgehenden Liste pro Register-Typ (`Inp
| **0x00F2** | `DEVICE_STATUS` | System | `0`=OK, `1`=Allgemeiner Fehler. |
| **0x00F3** | `UPTIME_SECONDS_LOW` | System | Untere 16 Bit der Uptime in Sekunden. |
| **0x00F4** | `UPTIME_SECONDS_HIGH` | System | Obere 16 Bit der Uptime. |
| **0x00F5** | `SUPPLY_VOLTAGE_MV` | System | Aktuelle Versorgungsspannung in Millivolt (mV). |
| **0x0100** | `FWU_LAST_CHUNK_CRC` | Firmware-Update | Enthält den CRC16 des zuletzt im Puffer empfangenen Daten-Chunks. |
## 3. Holding Registers (4xxxx, Read/Write)

View File

@@ -1,6 +0,0 @@
cmake_minimum_required(VERSION 3.20)
project(fwu)
target_sources(fwu PRIVATE src/fwu.c)
target_include_directories(fwu PUBLIC include)

View File

@@ -1,6 +0,0 @@
cmake_minimum_required(VERSION 3.20)
project(modbus_server)
target_sources(modbus_server PRIVATE src/modbus_server.c)
target_include_directories(modbus_server PUBLIC include)

View File

@@ -1,6 +0,0 @@
cmake_minimum_required(VERSION 3.20)
project(valve)
target_sources(valve PRIVATE src/valve.c)
target_include_directories(valve PUBLIC include)

56
setup-format-hook.sh Executable file
View File

@@ -0,0 +1,56 @@
#!/bin/sh
# This script sets up a Git pre-commit hook to automatically format C/C++ files
# in the 'software/' subdirectory using clang-format.
# Define the path for the pre-commit hook
HOOK_DIR=".git/hooks"
HOOK_FILE="$HOOK_DIR/pre-commit"
# Create the hooks directory if it doesn't exist
mkdir -p "$HOOK_DIR"
# Create the pre-commit hook script using a 'here document'
cat > "$HOOK_FILE" << 'EOF'
#!/bin/sh
# --- Pre-commit hook for clang-format ---
#
# This hook formats staged C, C++, and Objective-C files in the 'software/'
# subdirectory before a commit is made.
# It automatically finds the .clang-format file in the software/ directory.
#
# Directory to be formatted
TARGET_DIR="software/"
# Use git diff to find staged files that are Added (A), Copied (C), or Modified (M).
# We filter for files only within the TARGET_DIR.
# The grep regex matches common C/C++ and Objective-C file extensions.
FILES_TO_FORMAT=$(git diff --cached --name-only --diff-filter=ACM "$TARGET_DIR" | grep -E '\.(c|h|cpp|hpp|cxx|hxx|cc|hh|m|mm)$')
if [ -z "$FILES_TO_FORMAT" ]; then
# No relevant files to format, exit successfully.
exit 0
fi
echo " Running clang-format on staged files in '$TARGET_DIR'..."
# Run clang-format in-place on the identified files.
# clang-format will automatically find the .clang-format file in the software/ directory
# or any of its parent directories.
echo "$FILES_TO_FORMAT" | xargs clang-format -i
# Since clang-format may have changed the files, we need to re-stage them.
echo "$FILES_TO_FORMAT" | xargs git add
echo " Formatting complete."
exit 0
EOF
# Make the hook executable
chmod +x "$HOOK_FILE"
echo "✅ Git pre-commit hook has been set up successfully."
echo " It will now automatically format files in the '$PWD/software' directory before each commit."

5
software/.clang-format Normal file
View File

@@ -0,0 +1,5 @@
# .clang-format
BasedOnStyle: Google
#IndentWidth: 4
#ColumnLimit: 100
#AllowShortFunctionsOnASingleLine: None

View File

@@ -1,12 +1,12 @@
{
// Hush CMake
"cmake.configureOnOpen": false,
// IntelliSense
"C_Cpp.default.compilerPath": "${userHome}/zephyr-sdk-0.17.1/arm-zephyr-eabi/bin/arm-zephyr-eabi-gcc.exe",
"C_Cpp.default.compileCommands": "${workspaceFolder}/build/compile_commands.json",
// File Associations
"files.associations": {
}
// Hush CMake
"cmake.configureOnOpen": false,
// IntelliSense
"C_Cpp.default.compilerPath": "${userHome}/zephyr-sdk-0.17.1/arm-zephyr-eabi/bin/arm-zephyr-eabi-gcc.exe",
"C_Cpp.default.compileCommands": "${workspaceFolder}/build/compile_commands.json",
// File Associations
"files.associations": {
"app_version.h": "c"
},
"C_Cpp.clang_format_style": "file",
}

View File

@@ -2,32 +2,20 @@
"version": "2.0.0",
"tasks": [
{
"label": "West Build",
"type": "shell",
"group": {
"kind": "build",
"isDefault": true
},
"linux": {
"command": "${userHome}/zephyrproject/.venv/bin/west"
},
"windows": {
"command": "${userHome}/zephyrproject/.venv/Scripts/west.exe"
},
"osx": {
"command": "${userHome}/zephyrproject/.venv/bin/west"
},
"args": [
"build",
"-p",
"auto",
"-b",
"valve_node"
],
"problemMatcher": [
"$gcc"
]
},
"label": "Format All C/C++ Files",
"type": "shell",
"command": "find . -name \"*.c\" -o -name \"*.h\" | xargs clang-format -i",
"problemMatcher": [],
"group": {
"kind": "build",
"isDefault": true
},
"presentation": {
"reveal": "silent",
"clear": true,
"panel": "shared"
}
},
{
"label": "West Configurable Build",
"type": "shell",

View File

View File

View File

View File

View File

@@ -6,8 +6,7 @@
#include <zephyr/kernel.h>
int main(void)
{
printk("Hello from Gateway!\n");
return 0;
int main(void) {
printk("Hello from Gateway!\n");
return 0;
}

View File

@@ -0,0 +1,12 @@
{
"configurations": [
{
"name": "Linux",
"compileCommands": "${workspaceFolder}/build/compile_commands.json",
"cStandard": "c99",
"cppStandard": "gnu++17",
"intelliSenseMode": "linux-gcc-arm"
}
],
"version": 4
}

View File

@@ -0,0 +1,5 @@
VERSION_MAJOR = 0
VERSION_MINOR = 0
PATCHLEVEL = 1
VERSION_TWEAK = 1
EXTRAVERSION = devel

View File

@@ -1,9 +1,71 @@
/ {
vnd7050aj: vnd7050aj {
compatible = "vnd7050aj-valve-controller";
status = "okay";
// VND7050AJ GPIO pin definitions
in0-gpios = <&gpiob 7 GPIO_ACTIVE_HIGH>; // IN0 (PB7) - Input 0 control signal
in1-gpios = <&gpiob 9 GPIO_ACTIVE_HIGH>; // IN1 (PB9) - Input 1 control signal
rst-gpios = <&gpiob 3 GPIO_ACTIVE_HIGH>; // RST (PB3) - Reset pin for VND7050AJ
sen-gpios = <&gpiob 4 GPIO_ACTIVE_HIGH>; // SEN (PB4) - Sense Enable for current monitoring
s0-gpios = <&gpiob 6 GPIO_ACTIVE_HIGH>; // S0 (PB6) - Status/Select 0 output from VND7050AJ
s1-gpios = <&gpiob 5 GPIO_ACTIVE_HIGH>; // S1 (PB5) - Status/Select 1 output from VND7050AJ
};
};
// Clock configuration: Uncomment the following section to use calibrated HSI instead of HSE
//&clk_hse {
// status = "disabled"; // Disable external crystal oscillator
//};
//
//&clk_hsi {
// status = "okay"; // Enable internal high-speed oscillator (16 MHz, calibrated)
//};
//
//&pll {
// // Change PLL source from HSE to HSI
// clocks = <&clk_hsi>;
// // Adjust multipliers to maintain 144 MHz system clock with 16 MHz HSI input
// // HSI = 16 MHz, div-m = 4, mul-n = 72, div-r = 2
// // PLL_VCO = (16 MHz / 4) * 72 = 288 MHz
// // SYSCLK = 288 MHz / 2 = 144 MHz
// div-m = <4>; // Divide HSI by 4 (16 MHz / 4 = 4 MHz)
// mul-n = <72>; // Multiply by 72 (4 MHz * 72 = 288 MHz)
// div-r = <2>; // Divide by 2 for system clock (288 MHz / 2 = 144 MHz)
//};
&usart1 {
modbus0 {
compatible = "zephyr,modbus-serial";
status = "okay";
};
status = "okay";
pinctrl-0 = <&usart1_tx_pa9 &usart1_rx_pa10>;
pinctrl-0 = <&usart1_tx_pa9 &usart1_rx_pa10>; // PA9=TX, PA10=RX for Modbus communication
pinctrl-names = "default";
};
&adc1 { // ADC1 wird für PA0 verwendet
status = "okay"; // ADC1 aktivieren
pinctrl-0 = <&adc1_in1_pa0>; // Pinmux für PA0 als ADC1_IN1
pinctrl-names = "default";
st,adc-clock-source = "SYNC";
st,adc-prescaler = <4>;
#address-cells = <1>;
#size-cells = <0>;
// Definition des ADC-Kanals für MULTISENSE (PA0)
channel@1 { // ADC1_IN1 ist Kanal 1
reg = <1>; // Kanalnummer
zephyr,gain = "ADC_GAIN_1";
zephyr,reference = "ADC_REF_INTERNAL";
zephyr,acquisition-time = <ADC_ACQ_TIME_DEFAULT>;
zephyr,resolution = <12>;
};
};
&pinctrl {
// Pinmux für PA0 als ADC1_IN1 (Analogmodus)
adc1_in1_pa0: adc1_in1_pa0 {
pinmux = <STM32_PINMUX('A', 0, ANALOG)>; // PA0 in den Analogmodus setzen
};
};

View File

@@ -8,3 +8,7 @@
};
};
};
&usart1 {
/delete-node/ modbus0;
};

View File

@@ -0,0 +1,35 @@
# VND7050AJ Valve Controller binding
description: VND7050AJ valve controller GPIO configuration
compatible: "vnd7050aj-valve-controller"
properties:
in0-gpios:
type: phandle-array
description: GPIO for IN0 control signal
required: true
in1-gpios:
type: phandle-array
description: GPIO for IN1 control signal
required: true
rst-gpios:
type: phandle-array
description: GPIO for reset pin
required: true
sen-gpios:
type: phandle-array
description: GPIO for sense enable pin
required: true
s0-gpios:
type: phandle-array
description: GPIO for status/select 0 pin
required: true
s1-gpios:
type: phandle-array
description: GPIO for status/select 1 pin
required: true

View File

@@ -13,7 +13,6 @@ CONFIG_NVS=y
CONFIG_FLASH=y
CONFIG_FLASH_MAP=y
CONFIG_FLASH_PAGE_LAYOUT=y
CONFIG_SETTINGS_LOG_LEVEL_DBG=y
# Config modbus
CONFIG_UART_INTERRUPT_DRIVEN=y

View File

@@ -1,28 +1,31 @@
#include <zephyr/kernel.h>
#include <zephyr/settings/settings.h>
#include <zephyr/logging/log.h>
#include <app_version.h>
#include <lib/fwu.h>
#include <lib/modbus_server.h>
#include <lib/valve.h>
#include <lib/fwu.h>
#include <zephyr/kernel.h>
#include <zephyr/logging/log.h>
#include <zephyr/settings/settings.h>
LOG_MODULE_REGISTER(main, LOG_LEVEL_INF);
int main(void)
{
LOG_INF("Starting Irrigation System Slave Node");
int main(void) {
int rc;
LOG_INF("Starting Irrigation System Slave Node version %s (Build version %s)",
APP_VERSION_STRING, STRINGIFY(APP_BUILD_VERSION));
if (settings_subsys_init() || settings_load()) {
LOG_ERR("Settings initialization or loading failed");
}
if (settings_subsys_init() || settings_load()) {
LOG_ERR("Settings initialization or loading failed");
}
valve_init();
fwu_init();
valve_init();
fwu_init();
if (modbus_server_init()) {
LOG_ERR("Modbus RTU server initialization failed");
return 0;
}
rc = modbus_server_init();
if (rc < 0) {
LOG_ERR("Modbus RTU server initialization failed: %d", rc);
return 0;
}
LOG_INF("Irrigation System Slave Node started successfully");
return 0;
LOG_INF("Irrigation System Slave Node started successfully");
return 0;
}

View File

@@ -0,0 +1,46 @@
#ifndef ADC_SENSOR_H
#define ADC_SENSOR_H
#include <stdint.h>
/**
* @file adc_sensor.h
* @brief API for the ADC sensor library.
*
* This library provides functions to initialize and read from the ADC sensors,
* specifically for measuring supply voltage and motor current.
* It can operate in a real or simulated mode.
*/
/**
* @brief Initializes the ADC sensor system.
*
* This function sets up the necessary ADC channels and configurations.
* It should be called once before any other function in this library.
* In simulated mode, it logs the simulated values.
*
* @return 0 on success, or a negative error code on failure.
*/
int adc_sensor_init(void);
/**
* @brief Gets the current supply voltage reading.
*
* This function reads the value from the corresponding ADC channel and converts
* it to millivolts.
*
* @return The supply voltage in millivolts (mV).
*/
uint16_t adc_sensor_get_voltage_mv(void);
/**
* @brief Gets the current motor current reading.
*
* This function reads the value from the motor driver's sense pin via ADC
* and converts it to milliamps. This is used for end-stop detection.
*
* @return The motor current in milliamps (mA).
*/
uint16_t adc_sensor_get_current_ma(void);
#endif /* ADC_SENSOR_H */

View File

@@ -3,8 +3,45 @@
#include <stdint.h>
/**
* @file fwu.h
* @brief API for the Firmware Update (FWU) library.
*
* This library provides the core logic for handling the over-the-air firmware
* update process via Modbus. It manages the data buffer, processes commands,
* and calculates CRC checksums for data verification.
*/
/**
* @brief Initializes the firmware update module.
*
* This function currently does nothing but is a placeholder for future
* initialization logic.
*/
void fwu_init(void);
/**
* @brief Handles incoming Modbus register writes related to firmware updates.
*
* This function is the main entry point for the FWU process. It parses the
* address and value from a Modbus write operation and takes appropriate action,
* such as storing metadata (offset, size) or data chunks, and processing
* commands (verify, finalize).
*
* @param addr The Modbus register address being written to.
* @param reg The 16-bit value being written to the register.
*/
void fwu_handler(uint16_t addr, uint16_t reg);
/**
* @brief Gets the CRC16-CCITT of the last received firmware chunk.
*
* After a data chunk is fully received into the buffer, this function can be
* called to retrieve the calculated CRC checksum. The master can then compare
* this with its own calculated CRC to verify data integrity.
*
* @return The 16-bit CRC of the last chunk.
*/
uint16_t fwu_get_last_chunk_crc(void);
#endif // FWU_H
#endif // FWU_H

View File

@@ -4,49 +4,161 @@
#include <stdint.h>
/**
* @brief Modbus Input Register Addresses.
* @file modbus_server.h
* @brief API for the Modbus server implementation.
*
* This file defines the Modbus register map and provides functions to
* initialize and manage the Modbus server.
*/
/**
* @brief Modbus Input Register Addresses (Read-Only).
* @see docs/modbus-registers.de.md
*/
enum {
/* Valve Control & Status */
REG_INPUT_VALVE_STATE_MOVEMENT = 0x0000,
REG_INPUT_MOTOR_CURRENT_MA = 0x0001,
/* Digital Inputs */
REG_INPUT_DIGITAL_INPUTS_STATE = 0x0020,
REG_INPUT_BUTTON_EVENTS = 0x0021,
/* System Config & Status */
REG_INPUT_FIRMWARE_VERSION_MAJOR_MINOR = 0x00F0,
REG_INPUT_FIRMWARE_VERSION_PATCH = 0x00F1,
REG_INPUT_DEVICE_STATUS = 0x00F2,
REG_INPUT_UPTIME_SECONDS_LOW = 0x00F3,
REG_INPUT_UPTIME_SECONDS_HIGH = 0x00F4,
/* Firmware Update */
REG_INPUT_FWU_LAST_CHUNK_CRC = 0x0100,
/**
* @brief Kombiniertes Status-Register für das Ventil.
* High-Byte: Bewegung (0=Idle, 1=Öffnet, 2=Schliesst, 3=Fehler).
* Low-Byte: Zustand (0=Geschlossen, 1=Geöffnet).
*/
REG_INPUT_VALVE_STATE_MOVEMENT = 0x0000,
/**
* @brief Aktueller Motorstrom in Milliampere (mA).
*/
REG_INPUT_MOTOR_CURRENT_MA = 0x0001,
/**
* @brief Bitmaske der digitalen Eingänge. Bit 0: Eingang 1, Bit 1: Eingang 2.
* 1=Aktiv.
*/
REG_INPUT_DIGITAL_INPUTS_STATE = 0x0020,
/**
* @brief Event-Flags für Taster (Clear-on-Read). Bit 0: Taster 1 gedrückt.
* Bit 1: Taster 2 gedrückt.
*/
REG_INPUT_BUTTON_EVENTS = 0x0021,
/**
* @brief Firmware-Version, z.B. 0x0102 für v1.2.
*/
REG_INPUT_FIRMWARE_VERSION_MAJOR_MINOR = 0x00F0,
/**
* @brief Firmware-Version Patch-Level, z.B. 3 für v1.2.3.
*/
REG_INPUT_FIRMWARE_VERSION_PATCH = 0x00F1,
/**
* @brief Gerätestatus (0=OK, 1=Allgemeiner Fehler).
*/
REG_INPUT_DEVICE_STATUS = 0x00F2,
/**
* @brief Untere 16 Bit der Uptime in Sekunden.
*/
REG_INPUT_UPTIME_SECONDS_LOW = 0x00F3,
/**
* @brief Obere 16 Bit der Uptime in Sekunden.
*/
REG_INPUT_UPTIME_SECONDS_HIGH = 0x00F4,
/**
* @brief Aktuelle Versorgungsspannung in Millivolt (mV).
*/
REG_INPUT_SUPPLY_VOLTAGE_MV = 0x00F5,
/**
* @brief CRC16 des zuletzt im Puffer empfangenen Daten-Chunks für das
* Firmware-Update.
*/
REG_INPUT_FWU_LAST_CHUNK_CRC = 0x0100
};
/**
* @brief Modbus Holding Register Addresses.
* @brief Modbus Holding Register Addresses (Read/Write).
* @see docs/modbus-registers.de.md
*/
enum {
/* Valve Control */
REG_HOLDING_VALVE_COMMAND = 0x0000,
REG_HOLDING_MAX_OPENING_TIME_S = 0x0001,
REG_HOLDING_MAX_CLOSING_TIME_S = 0x0002,
/* Digital Outputs */
REG_HOLDING_DIGITAL_OUTPUTS_STATE = 0x0010,
/* System Config */
REG_HOLDING_WATCHDOG_TIMEOUT_S = 0x00F0,
REG_HOLDING_DEVICE_RESET = 0x00F1,
/* Firmware Update */
REG_HOLDING_FWU_COMMAND = 0x0100,
REG_HOLDING_FWU_CHUNK_OFFSET_LOW = 0x0101,
REG_HOLDING_FWU_CHUNK_OFFSET_HIGH = 0x0102,
REG_HOLDING_FWU_CHUNK_SIZE = 0x0103,
REG_HOLDING_FWU_DATA_BUFFER = 0x0180,
/**
* @brief Ventilsteuerungsbefehl (1=Öffnen, 2=Schliessen, 0=Bewegung stoppen).
*/
REG_HOLDING_VALVE_COMMAND = 0x0000,
/**
* @brief Sicherheits-Timeout in Sekunden für den Öffnen-Vorgang.
*/
REG_HOLDING_MAX_OPENING_TIME_S = 0x0001,
/**
* @brief Sicherheits-Timeout in Sekunden für den Schliessen-Vorgang.
*/
REG_HOLDING_MAX_CLOSING_TIME_S = 0x0002,
/**
* @brief Bitmaske zum Lesen und Schreiben der digitalen Ausgänge. Bit 0:
* Ausgang 1, Bit 1: Ausgang 2. 1=AN, 0=AUS.
*/
REG_HOLDING_DIGITAL_OUTPUTS_STATE = 0x0010,
/**
* @brief Timeout des Fail-Safe-Watchdogs in Sekunden. 0=Deaktiviert.
*/
REG_HOLDING_WATCHDOG_TIMEOUT_S = 0x00F0,
/**
* @brief Schreiben von 1 startet das Gerät neu.
*/
REG_HOLDING_DEVICE_RESET = 0x00F1,
/**
* @brief Befehl für das Firmware-Update.
* 1: Verify Chunk - Slave schreibt den letzten Chunk ins Flash.
* 2: Finalize Update - Installation abschliessen und neu starten.
*/
REG_HOLDING_FWU_COMMAND = 0x0100,
/**
* @brief Untere 16 Bit des 32-Bit-Offsets für den nächsten
* Firmware-Update-Chunk.
*/
REG_HOLDING_FWU_CHUNK_OFFSET_LOW = 0x0101,
/**
* @brief Obere 16 Bit des 32-Bit-Offsets für den nächsten
* Firmware-Update-Chunk.
*/
REG_HOLDING_FWU_CHUNK_OFFSET_HIGH = 0x0102,
/**
* @brief Grösse des nächsten Firmware-Update-Chunks in Bytes (max. 256).
*/
REG_HOLDING_FWU_CHUNK_SIZE = 0x0103,
/**
* @brief Startadresse des 256-Byte-Puffers für Firmware-Update-Daten.
*/
REG_HOLDING_FWU_DATA_BUFFER = 0x0180,
};
/**
* @brief Initializes the Modbus server.
*
* This function sets up the Modbus RTU server interface, loads saved settings
* (baudrate, unit ID), and starts listening for requests.
*
* @return 0 on success, or a negative error code on failure.
*/
int modbus_server_init(void);
/**
* @brief Reconfigures the Modbus server at runtime.
*
* Updates the baudrate and unit ID of the server. If the reconfiguration
* fails, the settings are saved and will be applied after a device reset.
*
* @param baudrate The new baudrate to set.
* @param unit_id The new Modbus unit ID (slave address).
* @return 0 on success, or a negative error code if immediate reconfiguration
* fails. Returns 0 even on failure if settings could be saved for the next
* boot.
*/
int modbus_reconfigure(uint32_t baudrate, uint8_t unit_id);
/**
* @brief Gets the current baudrate of the Modbus server.
*
* @return The current baudrate.
*/
uint32_t modbus_get_baudrate(void);
/**
* @brief Gets the current unit ID of the Modbus server.
*
* @return The current unit ID.
*/
uint8_t modbus_get_unit_id(void);
#endif // MODBUS_SERVER_H
#endif // MODBUS_SERVER_H

View File

@@ -2,22 +2,122 @@
#define VALVE_H
#include <stdint.h>
#include <zephyr/drivers/gpio.h>
enum valve_state { VALVE_STATE_CLOSED, VALVE_STATE_OPEN };
enum valve_movement { VALVE_MOVEMENT_IDLE, VALVE_MOVEMENT_OPENING, VALVE_MOVEMENT_CLOSING, VALVE_MOVEMENT_ERROR };
/**
* @file valve.h
* @brief API for controlling the motorized valve.
*
* This library provides functions to initialize, open, close, and stop the
* valve. It also allows getting the valve's state and movement status, and
* configuring the maximum opening and closing times.
*/
/**
* @brief Defines the GPIO pins used for the valve controller.
*/
struct valve_gpios {
const struct gpio_dt_spec
in0; /**< Control input 0 for the VND7050AJ driver. */
const struct gpio_dt_spec
in1; /**< Control input 1 for the VND7050AJ driver. */
const struct gpio_dt_spec rst; /**< Reset pin for the VND7050AJ driver. */
const struct gpio_dt_spec sen; /**< Sense (current measurement) pin. */
const struct gpio_dt_spec s0; /**< S0 select pin. */
const struct gpio_dt_spec s1; /**< S1 select pin. */
};
/**
* @brief Represents the static state of the valve (open or closed).
*/
enum valve_state {
VALVE_STATE_CLOSED, /**< The valve is fully closed. */
VALVE_STATE_OPEN, /**< The valve is fully open. */
};
/**
* @brief Represents the dynamic movement status of the valve.
*/
enum valve_movement {
VALVE_MOVEMENT_IDLE, /**< The valve is not moving. */
VALVE_MOVEMENT_OPENING, /**< The valve is currently opening. */
VALVE_MOVEMENT_CLOSING, /**< The valve is currently closing. */
VALVE_MOVEMENT_ERROR /**< An error occurred during movement. */
};
/**
* @brief Initializes the valve control system.
*
* Configures the GPIOs and loads saved settings for timeouts.
*/
void valve_init(void);
/**
* @brief Starts opening the valve.
*
* The valve will open for the configured maximum opening time.
*/
void valve_open(void);
/**
* @brief Starts closing the valve.
*
* The valve will close for the configured maximum closing time.
*/
void valve_close(void);
/**
* @brief Stops any ongoing valve movement immediately.
*/
void valve_stop(void);
/**
* @brief Gets the current static state of the valve.
*
* @return The current valve state (VALVE_STATE_CLOSED or VALVE_STATE_OPEN).
*/
enum valve_state valve_get_state(void);
/**
* @brief Gets the current movement status of the valve.
*
* @return The current movement status.
*/
enum valve_movement valve_get_movement(void);
/**
* @brief Gets the motor current.
*
* @return The motor current in milliamps (currently simulated).
*/
uint16_t valve_get_motor_current(void);
/**
* @brief Sets the maximum time for the valve to open.
*
* @param seconds The timeout in seconds.
*/
void valve_set_max_open_time(uint16_t seconds);
/**
* @brief Sets the maximum time for the valve to close.
*
* @param seconds The timeout in seconds.
*/
void valve_set_max_close_time(uint16_t seconds);
/**
* @brief Gets the configured maximum opening time.
*
* @return The timeout in seconds.
*/
uint16_t valve_get_max_open_time(void);
/**
* @brief Gets the configured maximum closing time.
*
* @return The timeout in seconds.
*/
uint16_t valve_get_max_close_time(void);
#endif // VALVE_H
#endif // VALVE_H

View File

@@ -1,3 +1,4 @@
add_subdirectory_ifdef(CONFIG_ADC_SENSOR adc_sensor)
add_subdirectory_ifdef(CONFIG_LIB_FWU fwu)
add_subdirectory_ifdef(CONFIG_LIB_MODBUS_SERVER modbus_server)
add_subdirectory_ifdef(CONFIG_LIB_VALVE valve)

View File

@@ -1,5 +1,6 @@
menu "Irrigation system software libraries"
rsource "adc_sensor/Kconfig"
rsource "fwu/Kconfig"
rsource "modbus_server/Kconfig"
rsource "valve/Kconfig"

View File

@@ -0,0 +1 @@
zephyr_library_sources(adc_sensor.c)

View File

@@ -0,0 +1,16 @@
config ADC_SENSOR
bool "ADC sensor library"
default y
help
Enable ADC sensor library for voltage and current measurements.
if ADC_SENSOR
config ADC_SENSOR_SIMULATED
bool "Use simulated ADC readings"
default y
help
Use simulated values instead of real ADC readings.
Voltage: 12000mV, Current: 45mA
endif # ADC_SENSOR

View File

@@ -0,0 +1,69 @@
/**
* @file adc_sensor.c
* @brief Implementation of the ADC sensor library.
*
* This file contains the implementation for initializing and reading from ADC
* sensors. It currently provides simulated values for voltage and current, with
* placeholders for real hardware ADC implementation.
*/
#include <lib/adc_sensor.h>
#include <zephyr/kernel.h>
#include <zephyr/logging/log.h>
LOG_MODULE_REGISTER(adc_sensor, LOG_LEVEL_INF);
// Simulated values
#define SIMULATED_VOLTAGE_MV 12000
#define SIMULATED_CURRENT_MA 45
static bool initialized =
false; // Flag to indicate if the ADC sensor is initialized
int adc_sensor_init(void) {
if (initialized) {
return 0;
}
#ifdef CONFIG_ADC_SENSOR_SIMULATED
LOG_INF("ADC sensor initialized (simulated mode)");
LOG_INF("Simulated values: %dmV, %dmA", SIMULATED_VOLTAGE_MV,
SIMULATED_CURRENT_MA);
#else
// TODO: Initialize real ADC hardware
LOG_INF("ADC sensor initialized (real ADC mode - not yet implemented)");
#endif
initialized = true;
return 0;
}
uint16_t adc_sensor_get_voltage_mv(void) {
if (!initialized) {
LOG_WRN("ADC sensor not initialized, calling adc_sensor_init()");
adc_sensor_init();
}
#ifdef CONFIG_ADC_SENSOR_SIMULATED
return SIMULATED_VOLTAGE_MV;
#else
// TODO: Read real ADC value for voltage
// For now return simulated value
return SIMULATED_VOLTAGE_MV;
#endif
}
uint16_t adc_sensor_get_current_ma(void) {
if (!initialized) {
LOG_WRN("ADC sensor not initialized, calling adc_sensor_init()");
adc_sensor_init();
}
#ifdef CONFIG_ADC_SENSOR_SIMULATED
return SIMULATED_CURRENT_MA;
#else
// TODO: Read real ADC value for current
// For now return simulated value
return SIMULATED_CURRENT_MA;
#endif
}

View File

@@ -1,45 +1,58 @@
#include <zephyr/kernel.h>
#include <zephyr/sys/crc.h>
#include <zephyr/sys/byteorder.h>
#include <zephyr/logging/log.h>
/**
* @file fwu.c
* @brief Implementation of the Firmware Update (FWU) library.
*
* This file implements the logic for receiving a new firmware image in chunks
* over Modbus. It maintains a buffer for the incoming data, calculates the CRC
* of the received chunk, and handles commands to verify the chunk and finalize
* the update process. The actual writing to flash is simulated.
*/
#include <lib/fwu.h>
#include <zephyr/kernel.h>
#include <zephyr/logging/log.h>
#include <zephyr/sys/byteorder.h>
#include <zephyr/sys/crc.h>
LOG_MODULE_REGISTER(fwu, LOG_LEVEL_INF);
#define FWU_BUFFER_SIZE 256
static uint8_t fwu_buffer[FWU_BUFFER_SIZE];
static uint32_t fwu_chunk_offset = 0;
static uint16_t fwu_chunk_size = 0;
static uint16_t fwu_last_chunk_crc = 0;
static uint8_t fwu_buffer[FWU_BUFFER_SIZE]; // Buffer to store incoming
// firmware data chunks
static uint32_t fwu_chunk_offset =
0; // Offset for the current firmware chunk in the overall image
static uint16_t fwu_chunk_size = 0; // Size of the current firmware chunk
static uint16_t fwu_last_chunk_crc =
0; // CRC16 of the last received firmware chunk
void fwu_init(void) {}
void fwu_handler(uint16_t addr, uint16_t reg)
{
// This is a simplified handler. In a real scenario, you would have a proper mapping
// between register addresses and actions.
if (addr == 0x0100) { // FWU_COMMAND
if (reg == 1) { LOG_INF("FWU: Chunk at offset %u (size %u) verified.", fwu_chunk_offset, fwu_chunk_size); }
else if (reg == 2) { LOG_INF("FWU: Finalize command received. Rebooting (simulated)."); }
} else if (addr == 0x0101) { // FWU_CHUNK_OFFSET_LOW
fwu_chunk_offset = (fwu_chunk_offset & 0xFFFF0000) | reg;
} else if (addr == 0x0102) { // FWU_CHUNK_OFFSET_HIGH
fwu_chunk_offset = (fwu_chunk_offset & 0x0000FFFF) | ((uint32_t)reg << 16);
} else if (addr == 0x0103) { // FWU_CHUNK_SIZE
fwu_chunk_size = (reg > FWU_BUFFER_SIZE) ? FWU_BUFFER_SIZE : reg;
} else if (addr >= 0x0180 && addr < (0x0180 + (FWU_BUFFER_SIZE / 2))) {
uint16_t index = (addr - 0x0180) * 2;
if (index < sizeof(fwu_buffer)) {
sys_put_be16(reg, &fwu_buffer[index]);
if (index + 2 >= fwu_chunk_size) {
fwu_last_chunk_crc = crc16_ccitt(0xffff, fwu_buffer, fwu_chunk_size);
LOG_INF("FWU: Chunk received, CRC is 0x%04X", fwu_last_chunk_crc);
}
}
void fwu_handler(uint16_t addr, uint16_t reg) {
// This is a simplified handler. In a real scenario, you would have a proper
// mapping between register addresses and actions.
if (addr == 0x0100) { // FWU_COMMAND
if (reg == 1) {
LOG_INF("FWU: Chunk at offset %u (size %u) verified.", fwu_chunk_offset,
fwu_chunk_size);
} else if (reg == 2) {
LOG_INF("FWU: Finalize command received. Rebooting (simulated).");
}
} else if (addr == 0x0101) { // FWU_CHUNK_OFFSET_LOW
fwu_chunk_offset = (fwu_chunk_offset & 0xFFFF0000) | reg;
} else if (addr == 0x0102) { // FWU_CHUNK_OFFSET_HIGH
fwu_chunk_offset = (fwu_chunk_offset & 0x0000FFFF) | ((uint32_t)reg << 16);
} else if (addr == 0x0103) { // FWU_CHUNK_SIZE
fwu_chunk_size = (reg > FWU_BUFFER_SIZE) ? FWU_BUFFER_SIZE : reg;
} else if (addr >= 0x0180 && addr < (0x0180 + (FWU_BUFFER_SIZE / 2))) {
uint16_t index = (addr - 0x0180) * 2;
if (index < sizeof(fwu_buffer)) {
sys_put_be16(reg, &fwu_buffer[index]);
if (index + 2 >= fwu_chunk_size) {
fwu_last_chunk_crc = crc16_ccitt(0xffff, fwu_buffer, fwu_chunk_size);
LOG_INF("FWU: Chunk received, CRC is 0x%04X", fwu_last_chunk_crc);
}
}
}
}
uint16_t fwu_get_last_chunk_crc(void)
{
return fwu_last_chunk_crc;
}
uint16_t fwu_get_last_chunk_crc(void) { return fwu_last_chunk_crc; }

View File

@@ -1,201 +1,263 @@
#include <zephyr/kernel.h>
#include <zephyr/drivers/uart.h>
#include <zephyr/device.h>
#include <zephyr/modbus/modbus.h>
#include <zephyr/logging/log.h>
#include <zephyr/settings/settings.h>
#include <zephyr/sys/reboot.h>
/**
* @file modbus_server.c
* @brief Modbus RTU server implementation for the irrigation system slave node.
*
* This file implements the Modbus server logic, including register callbacks,
* watchdog handling, and dynamic reconfiguration. It interfaces with other
* libraries like valve control, ADC sensors, and firmware updates.
*/
#include <app_version.h>
#include <lib/adc_sensor.h>
#include <lib/fwu.h>
#include <lib/modbus_server.h>
#include <lib/valve.h>
#include <lib/fwu.h>
#include <zephyr/device.h>
#include <zephyr/drivers/uart.h>
#include <zephyr/kernel.h>
#include <zephyr/logging/log.h>
#include <zephyr/modbus/modbus.h>
#include <zephyr/settings/settings.h>
#include <zephyr/sys/reboot.h>
#include <zephyr/usb/usb_device.h>
LOG_MODULE_REGISTER(modbus_server, LOG_LEVEL_INF);
static int modbus_iface;
static struct modbus_iface_param server_param = {
.mode = MODBUS_MODE_RTU,
.server = {.user_cb = NULL, .unit_id = 1},
.serial = {.baud = 19200, .parity = UART_CFG_PARITY_NONE},
.mode = MODBUS_MODE_RTU,
.server = {.user_cb = NULL, .unit_id = 1},
.serial = {.baud = 19200, .parity = UART_CFG_PARITY_NONE},
};
static uint16_t watchdog_timeout_s = 0;
static struct k_timer watchdog_timer;
static void watchdog_timer_handler(struct k_timer *timer_id)
{
LOG_WRN("Modbus watchdog expired! Closing valve as a fail-safe.");
valve_close();
/**
* @brief Timer handler for the Modbus watchdog.
*
* This function is called when the watchdog timer expires, indicating a loss
* of communication with the Modbus master. It triggers a fail-safe action,
* which is to close the valve.
*
* @param timer_id Pointer to the timer instance.
*/
static void watchdog_timer_handler(struct k_timer *timer_id) {
LOG_WRN("Modbus watchdog expired! Closing valve as a fail-safe.");
valve_close();
}
static inline void reset_watchdog(void)
{
if (watchdog_timeout_s > 0)
{
k_timer_start(&watchdog_timer, K_SECONDS(watchdog_timeout_s), K_NO_WAIT);
}
/**
* @brief Resets the Modbus watchdog timer.
*
* This function should be called upon receiving any valid Modbus request
* to prevent the watchdog from expiring.
*/
static inline void reset_watchdog(void) {
if (watchdog_timeout_s > 0) {
k_timer_start(&watchdog_timer, K_SECONDS(watchdog_timeout_s), K_NO_WAIT);
}
}
static int holding_reg_rd(uint16_t addr, uint16_t *reg)
{
reset_watchdog();
switch (addr)
{
case REG_HOLDING_MAX_OPENING_TIME_S:
*reg = valve_get_max_open_time();
break;
case REG_HOLDING_MAX_CLOSING_TIME_S:
*reg = valve_get_max_close_time();
break;
case REG_HOLDING_WATCHDOG_TIMEOUT_S:
*reg = watchdog_timeout_s;
break;
default:
*reg = 0;
break;
}
return 0;
/**
* @brief Callback for reading Modbus holding registers.
*
* @param addr Register address.
* @param reg Pointer to store the read value.
* @return 0 on success.
*/
static int holding_reg_rd(uint16_t addr, uint16_t *reg) {
reset_watchdog();
switch (addr) {
case REG_HOLDING_MAX_OPENING_TIME_S:
*reg = valve_get_max_open_time();
break;
case REG_HOLDING_MAX_CLOSING_TIME_S:
*reg = valve_get_max_close_time();
break;
case REG_HOLDING_WATCHDOG_TIMEOUT_S:
*reg = watchdog_timeout_s;
break;
default:
*reg = 0;
break;
}
return 0;
}
static int holding_reg_wr(uint16_t addr, uint16_t reg)
{
reset_watchdog();
switch (addr)
{
case REG_HOLDING_VALVE_COMMAND:
if (reg == 1)
{
valve_open();
}
else if (reg == 2)
{
valve_close();
}
else if (reg == 0)
{
valve_stop();
}
break;
case REG_HOLDING_MAX_OPENING_TIME_S:
valve_set_max_open_time(reg);
break;
case REG_HOLDING_MAX_CLOSING_TIME_S:
valve_set_max_close_time(reg);
break;
case REG_HOLDING_WATCHDOG_TIMEOUT_S:
watchdog_timeout_s = reg;
if (watchdog_timeout_s > 0)
{
LOG_INF("Watchdog enabled with %u s timeout.", watchdog_timeout_s);
reset_watchdog();
}
else
{
LOG_INF("Watchdog disabled.");
k_timer_stop(&watchdog_timer);
}
break;
case REG_HOLDING_DEVICE_RESET:
if (reg == 1)
{
LOG_WRN("Modbus reset command received. Rebooting...");
sys_reboot(SYS_REBOOT_WARM);
}
break;
default:
fwu_handler(addr, reg);
break;
}
return 0;
/**
* @brief Callback for writing Modbus holding registers.
*
* @param addr Register address.
* @param reg Value to write.
* @return 0 on success.
*/
static int holding_reg_wr(uint16_t addr, uint16_t reg) {
reset_watchdog();
switch (addr) {
case REG_HOLDING_VALVE_COMMAND:
if (reg == 1) {
valve_open();
} else if (reg == 2) {
valve_close();
} else if (reg == 0) {
valve_stop();
}
break;
case REG_HOLDING_MAX_OPENING_TIME_S:
valve_set_max_open_time(reg);
break;
case REG_HOLDING_MAX_CLOSING_TIME_S:
valve_set_max_close_time(reg);
break;
case REG_HOLDING_WATCHDOG_TIMEOUT_S:
watchdog_timeout_s = reg;
if (watchdog_timeout_s > 0) {
LOG_INF("Watchdog enabled with %u s timeout.", watchdog_timeout_s);
reset_watchdog();
} else {
LOG_INF("Watchdog disabled.");
k_timer_stop(&watchdog_timer);
}
break;
case REG_HOLDING_DEVICE_RESET:
if (reg == 1) {
LOG_WRN("Modbus reset command received. Rebooting...");
sys_reboot(SYS_REBOOT_WARM);
}
break;
default:
fwu_handler(addr, reg);
break;
}
return 0;
}
static int input_reg_rd(uint16_t addr, uint16_t *reg)
{
reset_watchdog();
uint32_t uptime_s = k_uptime_get_32() / 1000;
switch (addr)
{
case REG_INPUT_VALVE_STATE_MOVEMENT:
*reg = (valve_get_movement() << 8) | (valve_get_state() & 0xFF);
break;
case REG_INPUT_MOTOR_CURRENT_MA:
*reg = valve_get_motor_current();
break;
case REG_INPUT_UPTIME_SECONDS_LOW:
*reg = (uint16_t)(uptime_s & 0xFFFF);
break;
case REG_INPUT_UPTIME_SECONDS_HIGH:
*reg = (uint16_t)(uptime_s >> 16);
break;
case REG_INPUT_FWU_LAST_CHUNK_CRC:
*reg = fwu_get_last_chunk_crc();
break;
case REG_INPUT_FIRMWARE_VERSION_MAJOR_MINOR:
*reg = (0 << 8) | 0;
break;
case REG_INPUT_FIRMWARE_VERSION_PATCH:
*reg = 2;
break;
default:
*reg = 0;
break;
}
return 0;
/**
* @brief Callback for reading Modbus input registers.
*
* @param addr Register address.
* @param reg Pointer to store the read value.
* @return 0 on success.
*/
static int input_reg_rd(uint16_t addr, uint16_t *reg) {
reset_watchdog();
uint32_t uptime_s = k_uptime_get_32() / 1000;
switch (addr) {
case REG_INPUT_VALVE_STATE_MOVEMENT:
*reg = (valve_get_movement() << 8) | (valve_get_state() & 0xFF);
break;
case REG_INPUT_MOTOR_CURRENT_MA:
*reg = adc_sensor_get_current_ma();
break;
case REG_INPUT_UPTIME_SECONDS_LOW:
*reg = (uint16_t)(uptime_s & 0xFFFF);
break;
case REG_INPUT_UPTIME_SECONDS_HIGH:
*reg = (uint16_t)(uptime_s >> 16);
break;
case REG_INPUT_SUPPLY_VOLTAGE_MV:
*reg = adc_sensor_get_voltage_mv();
break;
case REG_INPUT_FWU_LAST_CHUNK_CRC:
*reg = fwu_get_last_chunk_crc();
break;
case REG_INPUT_FIRMWARE_VERSION_MAJOR_MINOR:
*reg = (APP_VERSION_MAJOR << 8) | APP_VERSION_MINOR;
break;
case REG_INPUT_FIRMWARE_VERSION_PATCH:
*reg = APP_PATCHLEVEL;
break;
default:
*reg = 0;
break;
}
return 0;
}
static struct modbus_user_callbacks mbs_cbs = {
.holding_reg_rd = holding_reg_rd,
.holding_reg_wr = holding_reg_wr,
.input_reg_rd = input_reg_rd,
// Modbus server callback functions
.holding_reg_rd = holding_reg_rd,
.holding_reg_wr = holding_reg_wr,
.input_reg_rd = input_reg_rd,
};
#define MODBUS_NODE DT_COMPAT_GET_ANY_STATUS_OKAY(zephyr_modbus_serial)
int modbus_server_init(void)
{
k_timer_init(&watchdog_timer, watchdog_timer_handler, NULL);
const char iface_name[] = {DEVICE_DT_NAME(MODBUS_NODE)};
int modbus_server_init(void) {
k_timer_init(&watchdog_timer, watchdog_timer_handler, NULL);
// Initialize ADC sensor
int ret = adc_sensor_init();
if (ret < 0) {
LOG_ERR("Failed to initialize ADC sensor: %d", ret);
return ret;
}
// Load saved settings
uint32_t saved_baudrate = 19200;
uint8_t saved_unit_id = 1;
settings_load_one("modbus/baudrate", &saved_baudrate, sizeof(saved_baudrate));
settings_load_one("modbus/unit_id", &saved_unit_id, sizeof(saved_unit_id));
// Apply loaded settings
server_param.serial.baud = saved_baudrate;
server_param.server.unit_id = saved_unit_id;
const char iface_name[] = {DEVICE_DT_NAME(MODBUS_NODE)};
#if DT_NODE_HAS_COMPAT(DT_PARENT(MODBUS_NODE), zephyr_cdc_acm_uart)
const struct device *const dev = DEVICE_DT_GET(DT_PARENT(MODBUS_NODE));
uint32_t dtr = 0;
const struct device *const dev = DEVICE_DT_GET(DT_PARENT(MODBUS_NODE));
uint32_t dtr = 0;
if (!device_is_ready(dev) || usb_enable(NULL))
{
return 0;
}
if (!device_is_ready(dev) || usb_enable(NULL)) {
return 0;
}
while (!dtr)
{
uart_line_ctrl_get(dev, UART_LINE_CTRL_DTR, &dtr);
k_sleep(K_MSEC(100));
}
while (!dtr) {
uart_line_ctrl_get(dev, UART_LINE_CTRL_DTR, &dtr);
k_sleep(K_MSEC(100));
}
LOG_INF("Client connected to server on %s", dev->name);
LOG_INF("Client connected to server on %s", dev->name);
#endif
modbus_iface = modbus_iface_get_by_name(iface_name);
if (modbus_iface < 0)
{
return modbus_iface;
}
server_param.server.user_cb = &mbs_cbs;
return modbus_init_server(modbus_iface, server_param);
modbus_iface = modbus_iface_get_by_name(iface_name);
if (modbus_iface < 0) {
return modbus_iface;
}
server_param.server.user_cb = &mbs_cbs;
LOG_INF("Starting Modbus server: baudrate=%u, unit_id=%u", saved_baudrate,
saved_unit_id);
return modbus_init_server(modbus_iface, server_param);
}
int modbus_reconfigure(uint32_t baudrate, uint8_t unit_id)
{
server_param.serial.baud = baudrate;
server_param.server.unit_id = unit_id;
int modbus_reconfigure(uint32_t baudrate, uint8_t unit_id) {
// Update parameters
server_param.serial.baud = baudrate;
server_param.server.unit_id = unit_id;
int ret = modbus_init_server(modbus_iface, server_param);
// Try to reinitialize - this should work for most cases
int ret = modbus_init_server(modbus_iface, server_param);
if (ret == 0)
{
settings_save_one("modbus/baudrate", &baudrate, sizeof(baudrate));
settings_save_one("modbus/unit_id", &unit_id, sizeof(unit_id));
}
if (ret == 0) {
settings_save_one("modbus/baudrate", &baudrate, sizeof(baudrate));
settings_save_one("modbus/unit_id", &unit_id, sizeof(unit_id));
LOG_INF("Modbus reconfigured: baudrate=%u, unit_id=%u", baudrate, unit_id);
} else {
LOG_ERR("Failed to reconfigure Modbus: %d", ret);
LOG_INF("Modbus reconfiguration requires restart to take effect");
return ret;
// Save settings for next boot
settings_save_one("modbus/baudrate", &baudrate, sizeof(baudrate));
settings_save_one("modbus/unit_id", &unit_id, sizeof(unit_id));
LOG_INF(
"Settings saved. Type 'reset' to restart the device and apply the "
"change.");
return 0; // Return success since settings are saved
}
return ret;
}
uint32_t modbus_get_baudrate(void) { return server_param.serial.baud; }

View File

@@ -1,119 +1,176 @@
#include <zephyr/shell/shell.h>
#include <stdlib.h>
/**
* @file shell_modbus.c
* @brief Provides shell commands for Modbus and valve configuration.
*
* This file implements a set of commands for the Zephyr shell to allow
* runtime configuration of the Modbus server (baudrate, slave ID) and the
* valve (max opening/closing times). The settings are persisted to non-volatile
* storage.
*/
#include <lib/modbus_server.h>
#include <lib/valve.h>
#include <stdlib.h>
#include <zephyr/shell/shell.h>
static int cmd_modbus_set_baud(const struct shell *sh, size_t argc, char **argv)
{
if (argc != 2) {
shell_error(sh, "Usage: set_baud <baudrate>");
return -EINVAL;
}
/**
* @brief Shell command to set the Modbus baudrate.
*
* @param sh The shell instance.
* @param argc Argument count.
* @param argv Argument values.
* @return 0 on success, -EINVAL on error.
*/
static int cmd_modbus_set_baud(const struct shell *sh, size_t argc,
char **argv) {
if (argc != 2) {
shell_error(sh, "Usage: set_baud <baudrate>");
return -EINVAL;
}
uint32_t new_baud = (uint32_t)strtoul(argv[1], NULL, 10);
const uint32_t valid_baud_rates[] = {1200, 2400, 4800, 9600, 19200, 38400, 57600, 115200};
bool is_valid = false;
uint32_t new_baud = (uint32_t)strtoul(argv[1], NULL, 10);
const uint32_t valid_baud_rates[] = {1200, 2400, 4800, 9600,
19200, 38400, 57600, 115200};
bool is_valid = false;
for (int i = 0; i < ARRAY_SIZE(valid_baud_rates); i++) {
if (new_baud == valid_baud_rates[i]) {
is_valid = true;
break;
}
}
if (!is_valid) {
char error_msg[128];
int offset = snprintf(error_msg, sizeof(error_msg), "Invalid baudrate. Valid rates are: ");
for (int i = 0; i < ARRAY_SIZE(valid_baud_rates); i++) {
offset += snprintf(error_msg + offset, sizeof(error_msg) - offset, "%u ", valid_baud_rates[i]);
}
shell_error(sh, "%s", error_msg);
return -EINVAL;
}
if (modbus_reconfigure(new_baud, modbus_get_unit_id()) != 0) {
shell_error(sh, "Failed to apply new baudrate");
} else {
shell_print(sh, "Modbus baudrate set to: %u (and saved)", new_baud);
}
return 0;
}
static int cmd_modbus_set_id(const struct shell *sh, size_t argc, char **argv)
{
if (argc != 2) {
shell_error(sh, "Usage: set_id <slave_id>");
return -EINVAL;
}
uint32_t new_id_u32 = (uint32_t)strtoul(argv[1], NULL, 10);
if (new_id_u32 == 0 || new_id_u32 > 247) {
shell_error(sh, "Invalid slave ID: %s. Must be between 1 and 247.", argv[1]);
return -EINVAL;
}
uint8_t new_id = (uint8_t)new_id_u32;
if (modbus_reconfigure(modbus_get_baudrate(), new_id) != 0) {
shell_error(sh, "Failed to apply new slave ID");
} else {
shell_print(sh, "Modbus slave ID set to: %u (and saved)", new_id);
}
return 0;
}
static int cmd_valve_set_open_time(const struct shell *sh, size_t argc, char **argv)
{
if (argc != 2) {
shell_error(sh, "Usage: set_open_time <seconds>");
return -EINVAL;
for (int i = 0; i < ARRAY_SIZE(valid_baud_rates); i++) {
if (new_baud == valid_baud_rates[i]) {
is_valid = true;
break;
}
}
uint16_t seconds = (uint16_t)strtoul(argv[1], NULL, 10);
valve_set_max_open_time(seconds);
shell_print(sh, "Max opening time set to: %u seconds (and saved)", seconds);
return 0;
}
static int cmd_valve_set_close_time(const struct shell *sh, size_t argc, char **argv)
{
if (argc != 2) {
shell_error(sh, "Usage: set_close_time <seconds>");
return -EINVAL;
if (!is_valid) {
char error_msg[128];
int offset = snprintf(error_msg, sizeof(error_msg),
"Invalid baudrate. Valid rates are: ");
for (int i = 0; i < ARRAY_SIZE(valid_baud_rates); i++) {
offset += snprintf(error_msg + offset, sizeof(error_msg) - offset, "%u ",
valid_baud_rates[i]);
}
shell_error(sh, "%s", error_msg);
return -EINVAL;
}
uint16_t seconds = (uint16_t)strtoul(argv[1], NULL, 10);
valve_set_max_close_time(seconds);
shell_print(sh, "Max closing time set to: %u seconds (and saved)", seconds);
if (modbus_reconfigure(new_baud, modbus_get_unit_id()) != 0) {
shell_error(sh, "Failed to apply new baudrate");
} else {
shell_print(sh, "Modbus baudrate set to: %u (and saved)", new_baud);
}
return 0;
return 0;
}
static int cmd_config_show(const struct shell *sh, size_t argc, char **argv)
{
shell_print(sh, "Current Modbus Configuration:");
shell_print(sh, " Baudrate: %u", modbus_get_baudrate());
shell_print(sh, " Slave ID: %u", modbus_get_unit_id());
shell_print(sh, "Current Valve Configuration:");
shell_print(sh, " Max Opening Time: %u s", valve_get_max_open_time());
shell_print(sh, " Max Closing Time: %u s", valve_get_max_close_time());
return 0;
/**
* @brief Shell command to set the Modbus slave ID.
*
* @param sh The shell instance.
* @param argc Argument count.
* @param argv Argument values.
* @return 0 on success, -EINVAL on error.
*/
static int cmd_modbus_set_id(const struct shell *sh, size_t argc, char **argv) {
if (argc != 2) {
shell_error(sh, "Usage: set_id <slave_id>");
return -EINVAL;
}
uint32_t new_id_u32 = (uint32_t)strtoul(argv[1], NULL, 10);
if (new_id_u32 == 0 || new_id_u32 > 247) {
shell_error(sh, "Invalid slave ID: %s. Must be between 1 and 247.",
argv[1]);
return -EINVAL;
}
uint8_t new_id = (uint8_t)new_id_u32;
if (modbus_reconfigure(modbus_get_baudrate(), new_id) != 0) {
shell_error(sh, "Failed to apply new slave ID");
} else {
shell_print(sh, "Modbus slave ID set to: %u (and saved)", new_id);
}
return 0;
}
/**
* @brief Shell command to set the valve's maximum opening time.
*
* @param sh The shell instance.
* @param argc Argument count.
* @param argv Argument values.
* @return 0 on success, -EINVAL on error.
*/
static int cmd_valve_set_open_time(const struct shell *sh, size_t argc,
char **argv) {
if (argc != 2) {
shell_error(sh, "Usage: set_open_time <seconds>");
return -EINVAL;
}
uint16_t seconds = (uint16_t)strtoul(argv[1], NULL, 10);
valve_set_max_open_time(seconds);
shell_print(sh, "Max opening time set to: %u seconds (and saved)", seconds);
return 0;
}
/**
* @brief Shell command to set the valve's maximum closing time.
*
* @param sh The shell instance.
* @param argc Argument count.
* @param argv Argument values.
* @return 0 on success, -EINVAL on error.
*/
static int cmd_valve_set_close_time(const struct shell *sh, size_t argc,
char **argv) {
if (argc != 2) {
shell_error(sh, "Usage: set_close_time <seconds>");
return -EINVAL;
}
uint16_t seconds = (uint16_t)strtoul(argv[1], NULL, 10);
valve_set_max_close_time(seconds);
shell_print(sh, "Max closing time set to: %u seconds (and saved)", seconds);
return 0;
}
/**
* @brief Shell command to show the current configuration.
*
* @param sh The shell instance.
* @param argc Argument count.
* @param argv Argument values.
* @return 0 on success.
*/
static int cmd_config_show(const struct shell *sh, size_t argc, char **argv) {
shell_print(sh, "Current Modbus Configuration:");
shell_print(sh, " Baudrate: %u", modbus_get_baudrate());
shell_print(sh, " Slave ID: %u", modbus_get_unit_id());
shell_print(sh, "Current Valve Configuration:");
shell_print(sh, " Max Opening Time: %u s", valve_get_max_open_time());
shell_print(sh, " Max Closing Time: %u s", valve_get_max_close_time());
return 0;
}
SHELL_STATIC_SUBCMD_SET_CREATE(sub_modbus_cmds,
SHELL_CMD(set_baud, NULL, "Set Modbus baudrate", cmd_modbus_set_baud),
SHELL_CMD(set_id, NULL, "Set Modbus slave ID", cmd_modbus_set_id),
SHELL_SUBCMD_SET_END
);
SHELL_CMD(set_baud, NULL, "Set Modbus baudrate",
cmd_modbus_set_baud),
SHELL_CMD(set_id, NULL, "Set Modbus slave ID",
cmd_modbus_set_id),
SHELL_SUBCMD_SET_END);
SHELL_STATIC_SUBCMD_SET_CREATE(sub_valve_cmds,
SHELL_CMD(set_open_time, NULL, "Set max valve opening time", cmd_valve_set_open_time),
SHELL_CMD(set_close_time, NULL, "Set max valve closing time", cmd_valve_set_close_time),
SHELL_SUBCMD_SET_END
);
SHELL_CMD(set_open_time, NULL,
"Set max valve opening time",
cmd_valve_set_open_time),
SHELL_CMD(set_close_time, NULL,
"Set max valve closing time",
cmd_valve_set_close_time),
SHELL_SUBCMD_SET_END);
SHELL_CMD_REGISTER(modbus, &sub_modbus_cmds, "Modbus configuration", NULL);
SHELL_CMD_REGISTER(valve, &sub_valve_cmds, "Valve configuration", NULL);
SHELL_CMD_REGISTER(show_config, NULL, "Show all configurations", cmd_config_show);
SHELL_CMD_REGISTER(show_config, NULL, "Show all configurations",
cmd_config_show);

View File

@@ -1,12 +1,30 @@
/**
* @file shell_system.c
* @brief Provides basic system-level shell commands.
*
* This file implements essential system commands for the Zephyr shell,
* such as rebooting the device.
*/
#include <zephyr/shell/shell.h>
#include <zephyr/sys/reboot.h>
static int cmd_reset(const struct shell *sh, size_t argc, char **argv)
{
shell_print(sh, "Rebooting system...");
k_sleep(K_MSEC(100)); // Allow the shell to print the message
sys_reboot(SYS_REBOOT_WARM);
return 0;
/**
* @brief Shell command to reset the system.
*
* This command performs a warm reboot of the device after a short delay
* to ensure the shell message is printed.
*
* @param sh The shell instance.
* @param argc Argument count.
* @param argv Argument values.
* @return 0 on success.
*/
static int cmd_reset(const struct shell *sh, size_t argc, char **argv) {
shell_print(sh, "Rebooting system...");
k_sleep(K_MSEC(100)); // Allow the shell to print the message
sys_reboot(SYS_REBOOT_WARM);
return 0;
}
SHELL_CMD_REGISTER(reset, NULL, "Reboot the system", cmd_reset);

View File

@@ -1,62 +1,121 @@
#include <zephyr/kernel.h>
#include <zephyr/settings/settings.h>
#include <zephyr/logging/log.h>
/**
* @file valve.c
* @brief Implementation of the motorized valve control library.
*
* This file contains the logic for controlling a motorized valve using a
* VND7050AJ high-side driver. It uses a delayed work item to handle the
* safety timeouts for opening and closing operations.
*/
#include <lib/valve.h>
#include <zephyr/device.h>
#include <zephyr/drivers/gpio.h>
#include <zephyr/kernel.h>
#include <zephyr/logging/log.h>
#include <zephyr/settings/settings.h>
LOG_MODULE_REGISTER(valve, LOG_LEVEL_INF);
static const struct valve_gpios valve_gpios = {
.in0 = GPIO_DT_SPEC_GET(DT_NODELABEL(vnd7050aj), in0_gpios),
.in1 = GPIO_DT_SPEC_GET(DT_NODELABEL(vnd7050aj), in1_gpios),
.rst = GPIO_DT_SPEC_GET(DT_NODELABEL(vnd7050aj), rst_gpios),
.sen = GPIO_DT_SPEC_GET(DT_NODELABEL(vnd7050aj), sen_gpios),
.s0 = GPIO_DT_SPEC_GET(DT_NODELABEL(vnd7050aj), s0_gpios),
.s1 = GPIO_DT_SPEC_GET(DT_NODELABEL(vnd7050aj), s1_gpios),
};
static enum valve_state current_state = VALVE_STATE_CLOSED;
static enum valve_movement current_movement = VALVE_MOVEMENT_IDLE;
static uint16_t max_opening_time_s = 60;
static uint16_t max_closing_time_s = 60;
static struct k_work_delayable valve_work;
static struct k_work_delayable
valve_work; // Work item for scheduling valve movement timeouts
static void valve_work_handler(struct k_work *work)
{
if (current_movement == VALVE_MOVEMENT_OPENING) {
LOG_INF("Virtual valve finished opening");
} else if (current_movement == VALVE_MOVEMENT_CLOSING) {
current_state = VALVE_STATE_CLOSED;
LOG_INF("Virtual valve finished closing");
}
current_movement = VALVE_MOVEMENT_IDLE;
/**
* @brief Work handler for valve movement timeouts.
*
* This function is executed when the valve's movement timer expires.
* It stops the motor to prevent damage and updates the valve's state.
*
* @param work Pointer to the k_work item.
*/
static void valve_work_handler(struct k_work *work) {
gpio_pin_set_dt(&valve_gpios.in0, 0);
gpio_pin_set_dt(&valve_gpios.in1, 0);
gpio_pin_set_dt(&valve_gpios.rst, 0);
if (current_movement == VALVE_MOVEMENT_OPENING) {
LOG_INF("Valve finished opening");
} else if (current_movement == VALVE_MOVEMENT_CLOSING) {
current_state = VALVE_STATE_CLOSED;
LOG_INF("Valve finished closing");
}
current_movement = VALVE_MOVEMENT_IDLE;
}
void valve_init(void)
{
k_work_init_delayable(&valve_work, valve_work_handler);
settings_load_one("valve/max_open_time", &max_opening_time_s, sizeof(max_opening_time_s));
settings_load_one("valve/max_close_time", &max_closing_time_s, sizeof(max_closing_time_s));
void valve_init(void) {
k_work_init_delayable(&valve_work, valve_work_handler);
settings_load_one("valve/max_open_time", &max_opening_time_s,
sizeof(max_opening_time_s));
settings_load_one("valve/max_close_time", &max_closing_time_s,
sizeof(max_closing_time_s));
gpio_pin_configure_dt(&valve_gpios.in0, GPIO_OUTPUT_INACTIVE);
gpio_pin_configure_dt(&valve_gpios.in1, GPIO_OUTPUT_INACTIVE);
gpio_pin_configure_dt(&valve_gpios.rst,
GPIO_OUTPUT_ACTIVE); // Keep VND7050AJ out of reset
gpio_pin_configure_dt(&valve_gpios.sen, GPIO_OUTPUT_INACTIVE);
gpio_pin_configure_dt(&valve_gpios.s0,
GPIO_OUTPUT_INACTIVE); // S0 select pin - output
gpio_pin_configure_dt(&valve_gpios.s1,
GPIO_OUTPUT_INACTIVE); // S1 select pin - output
LOG_INF("Valve initialized: max_open=%us, max_close=%us", max_opening_time_s,
max_closing_time_s);
}
void valve_open(void)
{
if (current_state == VALVE_STATE_CLOSED) {
current_state = VALVE_STATE_OPEN;
current_movement = VALVE_MOVEMENT_OPENING;
k_work_schedule(&valve_work, K_SECONDS(max_opening_time_s));
}
void valve_open(void) {
if (current_state == VALVE_STATE_CLOSED) {
gpio_pin_set_dt(&valve_gpios.rst, 1);
gpio_pin_set_dt(&valve_gpios.in1, 0);
gpio_pin_set_dt(&valve_gpios.in0, 1);
current_state = VALVE_STATE_OPEN;
current_movement = VALVE_MOVEMENT_OPENING;
k_work_schedule(&valve_work, K_MSEC(max_opening_time_s * 1000 * 0.9));
}
}
void valve_close(void)
{
if (current_state == VALVE_STATE_OPEN) {
current_movement = VALVE_MOVEMENT_CLOSING;
k_work_schedule(&valve_work, K_SECONDS(max_closing_time_s));
}
void valve_close(void) {
if (current_state == VALVE_STATE_OPEN) {
gpio_pin_set_dt(&valve_gpios.rst, 1);
gpio_pin_set_dt(&valve_gpios.in0, 0);
gpio_pin_set_dt(&valve_gpios.in1, 1);
current_movement = VALVE_MOVEMENT_CLOSING;
k_work_schedule(&valve_work, K_MSEC(max_closing_time_s * 1000 * 0.9));
}
}
void valve_stop(void)
{
k_work_cancel_delayable(&valve_work);
current_movement = VALVE_MOVEMENT_IDLE;
void valve_stop(void) {
k_work_cancel_delayable(&valve_work);
current_movement = VALVE_MOVEMENT_IDLE;
}
enum valve_state valve_get_state(void) { return current_state; }
enum valve_movement valve_get_movement(void) { return current_movement; }
uint16_t valve_get_motor_current(void) { return (current_movement != VALVE_MOVEMENT_IDLE) ? 150 : 10; }
uint16_t valve_get_motor_current(void) {
return (current_movement != VALVE_MOVEMENT_IDLE) ? 150 : 10;
}
void valve_set_max_open_time(uint16_t seconds) { max_opening_time_s = seconds; settings_save_one("valve/max_open_time", &max_opening_time_s, sizeof(max_opening_time_s)); }
void valve_set_max_close_time(uint16_t seconds) { max_closing_time_s = seconds; settings_save_one("valve/max_close_time", &max_closing_time_s, sizeof(max_closing_time_s)); }
void valve_set_max_open_time(uint16_t seconds) {
max_opening_time_s = seconds;
settings_save_one("valve/max_open_time", &max_opening_time_s,
sizeof(max_opening_time_s));
}
void valve_set_max_close_time(uint16_t seconds) {
max_closing_time_s = seconds;
settings_save_one("valve/max_close_time", &max_closing_time_s,
sizeof(max_closing_time_s));
}
uint16_t valve_get_max_open_time(void) { return max_opening_time_s; }
uint16_t valve_get_max_close_time(void) { return max_closing_time_s; }

View File

@@ -19,6 +19,7 @@ REG_INPUT_FIRMWARE_VERSION_PATCH = 0x00F1
REG_INPUT_DEVICE_STATUS = 0x00F2
REG_INPUT_UPTIME_SECONDS_LOW = 0x00F3
REG_INPUT_UPTIME_SECONDS_HIGH = 0x00F4
REG_INPUT_SUPPLY_VOLTAGE_MV = 0x00F5
REG_INPUT_FWU_LAST_CHUNK_CRC = 0x0100
REG_HOLDING_VALVE_COMMAND = 0x0000
REG_HOLDING_MAX_OPENING_TIME_S = 0x0001
@@ -84,7 +85,7 @@ def poll_status(slave_id, interval):
# If connected, try to read data
ir_valve = client.read_input_registers(REG_INPUT_VALVE_STATE_MOVEMENT, count=2, slave=slave_id)
ir_dig = client.read_input_registers(REG_INPUT_DIGITAL_INPUTS_STATE, count=2, slave=slave_id)
ir_sys = client.read_input_registers(REG_INPUT_FIRMWARE_VERSION_MAJOR_MINOR, count=5, slave=slave_id)
ir_sys = client.read_input_registers(REG_INPUT_FIRMWARE_VERSION_MAJOR_MINOR, count=6, slave=slave_id)
hr_valve = client.read_holding_registers(REG_HOLDING_MAX_OPENING_TIME_S, count=2, slave=slave_id)
hr_dig = client.read_holding_registers(REG_HOLDING_DIGITAL_OUTPUTS_STATE, count=1, slave=slave_id)
hr_sys = client.read_holding_registers(REG_HOLDING_WATCHDOG_TIMEOUT_S, count=1, slave=slave_id)
@@ -109,9 +110,11 @@ def poll_status(slave_id, interval):
fw_minor = ir_sys.registers[0] & 0xFF
fw_patch = ir_sys.registers[1]
uptime_seconds = (ir_sys.registers[4] << 16) | ir_sys.registers[3]
supply_voltage_mv = ir_sys.registers[5]
new_data["firmware"] = f"v{fw_major}.{fw_minor}.{fw_patch}"
new_data["device_status"] = "OK" if ir_sys.registers[2] == 0 else "ERROR"
new_data["uptime"] = format_uptime(uptime_seconds)
new_data["supply_voltage"] = f"{supply_voltage_mv / 1000.0:.2f} V"
new_data["watchdog"] = f"{hr_sys.registers[0]}s"
new_data["error"] = None # Clear any previous error on successful read
reconnect_attempts = 0 # Reset attempts on successful communication
@@ -302,6 +305,7 @@ def main_menu(stdscr, slave_id):
stdscr.addstr(1, col4, "Firmware:", bold); stdscr.addstr(1, col4 + 14, str(current_data.get('firmware', 'N/A')), normal)
stdscr.addstr(2, col4, "Uptime:", bold); stdscr.addstr(2, col4 + 14, str(current_data.get('uptime', 'N/A')), normal)
stdscr.addstr(3, col4, "Dev. Status:", bold); stdscr.addstr(3, col4 + 14, str(current_data.get('device_status', 'N/A')), normal)
stdscr.addstr(4, col4, "Supply V:", bold); stdscr.addstr(4, col4 + 14, str(current_data.get('supply_voltage', 'N/A')), normal)
stdscr.addstr(5, 0, "" * (w - 1), normal)
for idx, row in enumerate(menu):
draw_button(stdscr, h // 2 - len(menu) + (idx * 2), w // 2 - len(row) // 2, row, idx == current_row_idx)