24 Commits

Author SHA1 Message Date
0c4a728c17 feat(can): Add CAN ID definitions
This commit introduces the CAN ID definitions for the irrigation system.
It defines the structure of the CAN IDs and macros for priority and function.
Signed-off-by: Eduard Iten <eduard@iten.pro>
2025-07-22 11:02:27 +02:00
d92a1d9533 feat(app): Add CAN node application
This commit introduces a new CAN node application for the irrigation system.

The application initializes the settings subsystem and the valve system. It is intended to be used on a CAN bus to control a valve.
Signed-off-by: Eduard Iten <eduard@iten.pro>
2025-07-22 10:54:21 +02:00
9325fa20c8 feat(valve): Add current measurement callbacks and shell commands
This commit introduces several enhancements to the valve library.

- New weak callback functions `valve_current_open_callback` and `valve_current_close_callback` are added to allow the application to monitor the current during valve opening and closing operations.
- The `movement_timeout_handler` now correctly sets the valve state to `VALVE_STATE_OPEN` and movement to `VALVE_MOVEMENT_IDLE` upon timeout.
- New shell commands `valve open`, `valve close`, and `valve stop` are added for direct control of the valve.
- The existing setting commands are reorganized under a `valve set` subcommand, and their names are shortened (e.g., `set_open_t` to `open_t`).
- The default configuration for `LIB_MODBUS_SERVER` and `LIB_VALVE` is changed to `n`.
Signed-off-by: Eduard Iten <eduard@iten.pro>
2025-07-22 10:53:24 +02:00
08c47f00f8 feat(esphome): Add CAN configuration
Signed-off-by: Eduard Iten <eduard@iten.pro>
2025-07-22 08:55:30 +02:00
1cba00df8c feat: Add mqtt_gateway application and .gitignore
Signed-off-by: Eduard Iten <eduard@iten.pro>
2025-07-22 08:53:23 +02:00
35bd208cc0 refactor: Remove unused files
Signed-off-by: Eduard Iten <eduard@iten.pro>
2025-07-22 08:49:03 +02:00
48cfcd5d4c Fixed board definition
Signed-off-by: Eduard Iten <eduard@iten.pro>
2025-07-21 17:30:14 +02:00
d76b897eb2 feat: Integrate VND7050AJ driver and enhance gateway settings
This commit introduces the VND7050AJ driver as a new submodule and integrates it into the project.

Key changes include:
- Added  as a git submodule.
- Enhanced the gateway application () with LittleFS and the settings subsystem.
  - Implemented new shell commands (, , ) for managing custom settings.
  - Added functionality to compact the settings file.
- Updated  to include new library dependencies and log  return code.
- Adjusted include paths for  in relevant files.
Signed-off-by: Eduard Iten <eduard@iten.pro>
2025-07-17 15:18:22 +02:00
0713f8255e gateway: working shell on uart0 and mcumgr on usb_serial, minimal OS mgmt config\n
Signed-off-by: Eduard Iten <eduard@iten.pro>
2025-07-15 11:21:19 +02:00
fc089e5a33 gateway: working shell on uart0 and mcumgr on usb_serial, minimal OS mgmt config\n
Signed-off-by: Eduard Iten <eduard@iten.pro>
2025-07-15 11:20:18 +02:00
ef966cb078 Played around with the irrigaton system yaml
Signed-off-by: Eduard Iten <eduard@iten.pro>
2025-07-14 14:12:48 +02:00
6f304efb57 feat(esphome): Add initial irrigation system configuration
This commit introduces the initial ESPHome configuration for the irrigation system.

- `irrigation_system.yaml`: ESPHome configuration with Modbus valve control.

- `create_secrets.py`: Script to generate `secrets.yaml`.

- `secrets.yaml.example`: Example secrets file.

- `requirements.txt`: Python dependencies.

- `.gitignore`: Standard ESPHome gitignore file.
Signed-off-by: Eduard Iten <eduard@iten.pro>
2025-07-14 11:32:25 +02:00
e1ae96506d (fix) RTU gateway
fixed rtu gateway
Signed-off-by: Eduard Iten <eduard@iten.pro>
2025-07-12 16:08:49 +02:00
6cb17be451 (feat) added RTU gateway
Signed-off-by: Eduard Iten <eduard@iten.pro>
2025-07-12 14:13:09 +02:00
54e991294b feat(docs): align modbus register documentation with implementation
The modbus register documentation in `docs/modbus-registers.de.md` has been updated to be consistent with the implementation in `software/include/lib/modbus_server.h`.

- Register names in the documentation now match the programmatic names in the header file.
- Missing registers `REG_HOLDING_OBSTACLE_THRESHOLD_OPEN_MA` and `REG_HOLDING_OBSTACLE_THRESHOLD_CLOSE_MA` have been added to the documentation.
Signed-off-by: Eduard Iten <eduard@iten.pro>
2025-07-12 09:31:50 +02:00
0227e54198 bootloader somehow working
Signed-off-by: Eduard Iten <eduard@iten.pro>
2025-07-11 15:10:10 +02:00
c3c23efc95 Cleaning up
Signed-off-by: Eduard Iten <eduard@iten.pro>
2025-07-11 11:43:47 +02:00
4466b677a6 refactor: Integrate Modbus register defines into enums in modbus_server.h
Moved Modbus register definitions from  into enums within . This centralizes register definitions, improves type safety, and enhances code readability.

- : Added  and  to the  of holding registers.
- : Removed the  directive for .
- : Deleted this file as its contents are now integrated into .
Signed-off-by: Eduard Iten <eduard@iten.pro>
2025-07-11 09:58:51 +02:00
dcbd02ad7a refactor: Rename obstacle current settings in Modbus tool UI
Renamed "Set Obstacle Open" and "Set Obstacle Close" menu options to "Set Obstacle Current Open" and "Set Obstacle Current Close" respectively in . This provides more precise terminology for the obstacle detection current thresholds.
Signed-off-by: Eduard Iten <eduard@iten.pro>
2025-07-11 09:25:19 +02:00
8467b3e347 feat: Make valve obstacle detection parameters configurable via settings and shell
This commit introduces configurable obstacle detection thresholds for the valve, allowing them to be set and persisted via the Zephyr settings subsystem and controlled through the shell and Modbus tool.

- `software/lib/valve/Kconfig`: Added new Kconfig options `VALVE_OBSTACLE_THRESHOLD_OPEN_MA` and `VALVE_OBSTACLE_THRESHOLD_CLOSE_MA` for compile-time configuration and default values.
- `software/include/lib/valve.h`: Removed hardcoded defines and added API functions for setting and getting obstacle thresholds.
- `software/lib/valve/valve.c`:
    - Updated `valve_work_handler` to use the new configurable obstacle thresholds.
    - Integrated loading and saving of obstacle thresholds via the settings subsystem in `valve_init`.
    - Implemented the new setter and getter functions for obstacle thresholds.
    - Updated the `LOG_INF` message in `valve_init` to display the new obstacle threshold values.
- `software/apps/slave_node/prj.conf`: Added default values for the new Kconfig options.
- `software/lib/shell_valve/shell_valve.c`: Added new shell commands `valve set_obstacle_open` and `valve set_obstacle_close` to modify the obstacle thresholds, and updated `valve show` to display them.
- `software/tools/modbus_tool/modbus_tool.py`:
    - Defined new Modbus holding registers (`REG_HOLDING_OBSTACLE_THRESHOLD_OPEN_MA`, `REG_HOLDING_OBSTACLE_THRESHOLD_CLOSE_MA`).
    - Updated `poll_status` to read these new registers.
    - Modified the `main_menu` to include "Set Obstacle Open" and "Set Obstacle Close" options in the settings menu, allowing users to view and modify these parameters.
- `software/lib/modbus_server/modbus_server.c`:
    - Updated `holding_reg_rd` to read the new obstacle threshold registers.
    - Updated `holding_reg_wr` to write to the new obstacle threshold registers.
    - Removed incorrect `REG_HOLDING_END_CURRENT_THRESHOLD_OPEN_MA` and `REG_HOLDING_END_CURRENT_THRESHOLD_CLOSE_MA` cases from `input_reg_rd`.
- `software/include/lib/modbus_registers.h`: Created a new header file to centralize Modbus register definitions, which were previously hardcoded in `modbus_tool.py`.
Signed-off-by: Eduard Iten <eduard@iten.pro>
2025-07-11 09:15:19 +02:00
fc0add8583 feat: Adjust valve obstacle detection thresholds
Reduced the current thresholds for obstacle detection during valve opening and closing from 500mA to 200mA. This makes the obstacle detection more sensitive.

refactor: Simplify valve_work_handler logic

Refactored the  function to directly call  when an obstacle is detected or the valve reaches its end position. This removes redundant code and improves the clarity of the control flow.
Signed-off-by: Eduard Iten <eduard@iten.pro>
2025-07-11 08:57:02 +02:00
66cdc3ae27 fix: Force UI redraw on successful Modbus reconnection
Implemented a mechanism to force a full UI redraw in the Modbus tool upon successful reconnection to the serial port. The  function now sets a  flag in the shared status data, which is then detected by the  function. Upon detection,  clears the screen and removes the flag, ensuring that any stale error messages are cleared and the UI is fully refreshed.
Signed-off-by: Eduard Iten <eduard@iten.pro>
2025-07-11 08:53:16 +02:00
32bb77926f fix: Reduce flickering in Modbus tool UI over SSH
Replaced  with  and  with  in the  and  functions of . This change optimizes screen updates in the Curses-based UI, which should significantly reduce flickering when running the tool over SSH connections.
Signed-off-by: Eduard Iten <eduard@iten.pro>
2025-07-11 08:14:43 +02:00
4df0181d7f feat: Remove 'Toggle Output' options from Modbus tool
Removed the 'Toggle Output 1' and 'Toggle Output 2' menu options from the Modbus tool's main menu. This simplifies the user interface by removing functionality that is not directly related to the core valve control.
Signed-off-by: Eduard Iten <eduard@iten.pro>
2025-07-11 08:12:50 +02:00
88 changed files with 2643 additions and 120 deletions

64
.gitignore vendored
View File

@@ -1 +1,65 @@
**/build
# Zephyr build directories
build/
build-*/
*/build/
**/build/
# Zephyr out-of-tree build directories
out-of-tree-build/
# Files generated by the build system
zephyr.elf
zephyr.bin
zephyr.hex
zephyr.map
zephyr.strip
zephyr.lst
zephyr.asm
zephyr.stat
zephyr.a
zephyr.o
*.o
*.a
*.so
*.so.*
*.dll
*.exe
# Cmake
CMakeCache.txt
CMakeFiles/
cmake_install.cmake
CTestTestfile.cmake
compile_commands.json
# Kconfig generated files
.config
.config.old
autoconf.h
# Doxygen
doxygen/
# west
.west/
west.yml.bak
# Editor-specific files
.vscode/
.idea/
*.swp
*~
*.bak
*.orig
# Python
__pycache__/
*.pyc
# Mac OS X
.DS_Store
# Windows
Thumbs.db

3
.gitmodules vendored Normal file
View File

@@ -0,0 +1,3 @@
[submodule "software/modules/zephyr_vnd7050aj_driver"]
path = software/modules/zephyr_vnd7050aj_driver
url = https://gitea.iten.pro/edi/zephyr_vnd7050aj_driver.git

14
.vscode/tasks.json vendored Normal file
View File

@@ -0,0 +1,14 @@
{
"version": "2.0.0",
"tasks": [
{
"type": "shell",
"label": "Build Zephyr app",
"command": "west build -b weact_stm32g431_core .",
"group": "build",
"problemMatcher": [
"$gcc"
]
}
]
}

View File

@@ -30,35 +30,37 @@ Alle Register sind in einer einzigen, durchgehenden Liste pro Register-Typ (`Inp
| Adresse (hex) | Name | Zugehörigkeit | Beschreibung |
| :------------ | :----------------------------- | :---------------- | :---------------------------------------------------------------------------------------------------------------------------------------- |
| **0x0000** | `VALVE_STATE_MOVEMENT` | Ventil | Kombiniertes Status-Register. **High-Byte**: Bewegung (`0`=Idle, `1`=Öffnet, `2`=Schliesst, `3`=Fehler). **Low-Byte**: Zustand (`0`=Geschlossen, `1`=Geöffnet). |
| **0x0001** | `MOTORSTROM_OPEN_MA` | Ventil | Motorstrom beim Öffnen in Milliampere (mA). |
| **0x0002** | `MOTORSTROM_CLOSE_MA` | Ventil | Motorstrom beim Schließen in Milliampere (mA). |
| **0x0020** | `DIGITAL_INPUTS_STATE` | Eingänge | Bitmaske der digitalen Eingänge. Bit 0: Eingang 1, Bit 1: Eingang 2. `1`=Aktiv. |
| **0x0021** | `BUTTON_EVENTS` | Eingänge | Event-Flags für Taster (Clear-on-Read). Bit 0: Taster 1 gedrückt. Bit 1: Taster 2 gedrückt. |
| **0x00F0** | `FIRMWARE_VERSION_MAJOR_MINOR` | System | z.B. `0x0102` für v1.2. |
| **0x00F1** | `FIRMWARE_VERSION_PATCH` | System | z.B. `3` für v1.2.3. |
| **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. |
| **0x0001** | `REG_INPUT_MOTOR_OPEN_CURRENT_MA` | Ventil | Motorstrom beim Öffnen in Milliampere (mA). |
| **0x0002** | `REG_INPUT_MOTOR_CLOSE_CURRENT_MA` | Ventil | Motorstrom beim Schließen in Milliampere (mA). |
| **0x0020** | `REG_INPUT_DIGITAL_INPUTS_STATE` | Eingänge | Bitmaske der digitalen Eingänge. Bit 0: Eingang 1, Bit 1: Eingang 2. `1`=Aktiv. |
| **0x0021** | `REG_INPUT_BUTTON_EVENTS` | Eingänge | Event-Flags für Taster (Clear-on-Read). Bit 0: Taster 1 gedrückt. Bit 1: Taster 2 gedrückt. |
| **0x00F0** | `REG_INPUT_FIRMWARE_VERSION_MAJOR_MINOR` | System | z.B. `0x0102` für v1.2. |
| **0x00F1** | `REG_INPUT_FIRMWARE_VERSION_PATCH` | System | z.B. `3` für v1.2.3. |
| **0x00F2** | `REG_INPUT_DEVICE_STATUS` | System | `0`=OK, `1`=Allgemeiner Fehler. |
| **0x00F3** | `REG_INPUT_UPTIME_SECONDS_LOW` | System | Untere 16 Bit der Uptime in Sekunden. |
| **0x00F4** | `REG_INPUT_UPTIME_SECONDS_HIGH` | System | Obere 16 Bit der Uptime. |
| **0x00F5** | `REG_INPUT_SUPPLY_VOLTAGE_MV` | System | Aktuelle Versorgungsspannung in Millivolt (mV). |
| **0x0100** | `REG_INPUT_FWU_LAST_CHUNK_CRC` | Firmware-Update | Enthält den CRC16 des zuletzt im Puffer empfangenen Daten-Chunks. |
## 3. Holding Registers (4xxxx, Read/Write)
| Adresse (hex) | Name | Zugehörigkeit | Beschreibung |
| :------------ | :---------------------------- | :---------------- | :---------------------------------------------------------------------------------------------------------------------------------------- |
| **0x0000** | `VALVE_COMMAND` | Ventil | `1`=Öffnen, `2`=Schliessen, `0`=Bewegung stoppen. |
| **0x0001** | `MAX_OPENING_TIME_S` | Ventil | Sicherheits-Timeout in Sekunden für den Öffnen-Vorgang. |
| **0x0002** | `MAX_CLOSING_TIME_S` | Ventil | Sicherheits-Timeout in Sekunden für den Schliessen-Vorgang. |
| **0x0003** | `END_CURRENT_THRESHOLD_OPEN_MA` | Ventil | Minimaler Stromschwellenwert in mA zur Endlagenerkennung beim Öffnen. |
| **0x0004** | `END_CURRENT_THRESHOLD_CLOSE_MA` | Ventil | Minimaler Stromschwellenwert in mA zur Endlagenerkennung beim Schliessen. |
| **0x0010** | `DIGITAL_OUTPUTS_STATE` | Ausgänge | Bitmaske zum Lesen und Schreiben der Ausgänge. Bit 0: Ausgang 1, Bit 1: Ausgang 2. `1`=AN, `0`=AUS. |
| **0x00F0** | `WATCHDOG_TIMEOUT_S` | System | Timeout des Fail-Safe-Watchdogs in Sekunden. `0`=Deaktiviert. |
| **0x00F1** | `DEVICE_RESET` | System | Schreibt `1` um das Gerät neu zu starten. |
| **0x0100** | `FWU_COMMAND` | Firmware-Update | `1`: **Verify Chunk**: Der zuletzt übertragene Chunk wurde vom Client als gültig befunden. Der Slave soll ihn nun ins Flash schreiben. `2`: **Finalize Update**: Alle Chunks sind übertragen. Installation abschliessen und neu starten. |
| **0x0101** | `FWU_CHUNK_OFFSET_LOW` | Firmware-Update | Untere 16 Bit des 32-Bit-Offsets, an den der nächste Chunk geschrieben werden soll. |
| **0x0102** | `FWU_CHUNK_OFFSET_HIGH` | Firmware-Update | Obere 16 Bit des 32-Bit-Offsets. |
| **0x0103** | `FWU_CHUNK_SIZE` | Firmware-Update | Grösse des nächsten Chunks in Bytes (max. 256). |
| **0x0180** | `FWU_DATA_BUFFER` | Firmware-Update | **Startadresse** eines 128x16-bit Puffers (256 Bytes). Entspricht den Registern `40384` bis `40511`. |
| **0x0000** | `REG_HOLDING_VALVE_COMMAND` | Ventil | `1`=Öffnen, `2`=Schliessen, `0`=Bewegung stoppen. |
| **0x0001** | `REG_HOLDING_MAX_OPENING_TIME_S` | Ventil | Sicherheits-Timeout in Sekunden für den Öffnen-Vorgang. |
| **0x0002** | `REG_HOLDING_MAX_CLOSING_TIME_S` | Ventil | Sicherheits-Timeout in Sekunden für den Schliessen-Vorgang. |
| **0x0003** | `REG_HOLDING_END_CURRENT_THRESHOLD_OPEN_MA` | Ventil | Minimaler Stromschwellenwert in mA zur Endlagenerkennung beim Öffnen. |
| **0x0004** | `REG_HOLDING_END_CURRENT_THRESHOLD_CLOSE_MA` | Ventil | Minimaler Stromschwellenwert in mA zur Endlagenerkennung beim Schliessen. |
| **0x0005** | `REG_HOLDING_OBSTACLE_THRESHOLD_OPEN_MA` | Ventil | Stromschwellenwert in mA für die Hinderniserkennung beim Öffnen. |
| **0x0006** | `REG_HOLDING_OBSTACLE_THRESHOLD_CLOSE_MA` | Ventil | Stromschwellenwert in mA für die Hinderniserkennung beim Schließen. |
| **0x0010** | `REG_HOLDING_DIGITAL_OUTPUTS_STATE` | Ausgänge | Bitmaske zum Lesen und Schreiben der Ausgänge. Bit 0: Ausgang 1, Bit 1: Ausgang 2. `1`=AN, `0`=AUS. |
| **0x00F0** | `REG_HOLDING_WATCHDOG_TIMEOUT_S` | System | Timeout des Fail-Safe-Watchdogs in Sekunden. `0`=Deaktiviert. |
| **0x00F1** | `REG_HOLDING_DEVICE_RESET` | System | Schreibt `1` um das Gerät neu zu starten. |
| **0x0100** | `REG_HOLDING_FWU_COMMAND` | Firmware-Update | `1`: **Verify Chunk**: Der zuletzt übertragene Chunk wurde vom Client als gültig befunden. Der Slave soll ihn nun ins Flash schreiben. `2`: **Finalize Update**: Alle Chunks sind übertragen. Installation abschliessen und neu starten. |
| **0x0101** | `REG_HOLDING_FWU_CHUNK_OFFSET_LOW` | Firmware-Update | Untere 16 Bit des 32-Bit-Offsets, an den der nächste Chunk geschrieben werden soll. |
| **0x0102** | `REG_HOLDING_FWU_CHUNK_OFFSET_HIGH` | Firmware-Update | Obere 16 Bit des 32-Bit-Offsets. |
| **0x0103** | `REG_HOLDING_FWU_CHUNK_SIZE` | Firmware-Update | Grösse des nächsten Chunks in Bytes (max. 256). |
| **0x0180** | `REG_HOLDING_FWU_DATA_BUFFER` | Firmware-Update | **Startadresse** eines 128x16-bit Puffers (256 Bytes). Entspricht den Registern `40384` bis `40511`. |
## 4. Detaillierter Firmware-Update-Prozess

View File

@@ -1,7 +1 @@
rsource "lib/Kconfig"
rsource "lib/shell_valve/Kconfig"
config SLAVE_NODE_APP
bool "Slave Node Application"
default y
select SHELL_VALVE

View File

@@ -0,0 +1,7 @@
cmake_minimum_required(VERSION 3.20.0)
find_package(Zephyr REQUIRED HINTS $ENV{ZEPHYR_BASE})
project(bl_test)
# Add application source files
target_sources(app PRIVATE src/main.c)

View File

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

View File

@@ -0,0 +1,46 @@
# Enable Console and printk for logging via UART
CONFIG_CONSOLE=y
CONFIG_LOG=y
CONFIG_UART_CONSOLE=y
# Enable more detailed MCUMGR logging
CONFIG_MCUMGR_LOG_LEVEL_DBG=y
CONFIG_IMG_MANAGER_LOG_LEVEL_DBG=y
CONFIG_STREAM_FLASH_LOG_LEVEL_DBG=y
# Enable USB for MCUMGR only
CONFIG_USB_DEVICE_STACK=y
CONFIG_USB_CDC_ACM=y
CONFIG_USB_DEVICE_INITIALIZE_AT_BOOT=y
# USB CDC ACM buffer configuration for better MCUMGR performance
CONFIG_USB_CDC_ACM_RINGBUF_SIZE=1024
# Set log level to info for reasonable size
CONFIG_LOG_DEFAULT_LEVEL=3
# Enable MCUMGR info logging (not debug to save space)
CONFIG_MCUMGR_LOG_LEVEL_INF=y
# Enable USB CDC info logging
CONFIG_USB_CDC_ACM_LOG_LEVEL_INF=y
# STEP 5.2 - Enable mcumgr DFU in application
# Enable MCUMGR
CONFIG_MCUMGR=y # Enable MCUMGR management for both OS and Images
CONFIG_MCUMGR_GRP_OS=y
CONFIG_MCUMGR_GRP_IMG=y
# Configure MCUMGR transport to UART (will use USB-CDC via chosen device)
CONFIG_MCUMGR_TRANSPORT_UART=y
# Dependencies
# Configure dependencies for CONFIG_MCUMGR
CONFIG_NET_BUF=y
CONFIG_ZCBOR=y
CONFIG_CRC=y # Configure dependencies for CONFIG_MCUMGR_GRP_IMG
CONFIG_FLASH=y
CONFIG_IMG_MANAGER=y # Configure dependencies for CONFIG_IMG_MANAGER
CONFIG_STREAM_FLASH=y
CONFIG_FLASH_MAP=y # Configure dependencies for CONFIG_MCUMGR_TRANSPORT_USB_CDC
CONFIG_BASE64=y

View File

@@ -0,0 +1,11 @@
#include <zephyr/kernel.h>
#include <zephyr/logging/log.h>
#include <app_version.h>
LOG_MODULE_REGISTER(bl_test_app, LOG_LEVEL_INF);
int main(void)
{
LOG_INF("Hello World from bl_test! This is version %s", APP_VERSION_EXTENDED_STRING);
return 0;
}

View File

@@ -0,0 +1 @@
SB_CONFIG_BOOTLOADER_MCUBOOT=y

View File

@@ -0,0 +1,9 @@
#include "common.dtsi"
/* Application Configuration - Firmware wird in slot0_partition geschrieben */
/ {
chosen {
zephyr,code-partition = &slot0_partition;
zephyr,uart-mcumgr = &cdc_acm_uart0;
};
};

View File

@@ -0,0 +1,94 @@
/*
* Common Devicetree Configuration für weact_stm32g431_core
* - Konfiguriert einen W25Q128 Flash-Speicher auf SPI2
* - Konfiguriert USB-CDC für MCUMGR
* - Setzt den Chip Select (CS) Pin auf PA5
* - Weist das Label "flash1" zu
*/
/* Partitions für internes Flash (STM32G431) */
&flash0 {
/delete-node/ partitions; /* Entferne die Standard-Partitionen */
partitions {
compatible = "fixed-partitions";
#address-cells = <1>;
#size-cells = <1>;
/* MCUboot bootloader - 48 KB */
boot_partition: partition@0 {
label = "mcuboot";
reg = <0x00000000 0x0000C000>;
};
/* Slot0 partition für primäres Application Image - 80 KB (20 sectors @ 4KB) */
slot0_partition: partition@C000 {
label = "image-0";
reg = <0x0000C000 0x00014000>;
};
};
};
/* USB-CDC Konfiguration für MCUMGR */
&usb {
status = "okay";
cdc_acm_uart0: cdc_acm_uart0 {
compatible = "zephyr,cdc-acm-uart";
};
};
/ {
chosen {
zephyr,uart-mcumgr = &cdc_acm_uart0;
};
};
&spi2 {
/* Definiere die Pins für SCK, MISO, MOSI auf Port B */
pinctrl-0 = <&spi2_sck_pb13 &spi2_miso_pb14 &spi2_mosi_pb15>;
pinctrl-names = "default";
status = "okay";
/* === Chip Select (CS) auf PA5 gesetzt === */
cs-gpios = <&gpioa 5 (GPIO_ACTIVE_LOW | GPIO_PULL_UP)>;
/* Definiere den Flash-Chip als SPI NOR Gerät */
flash1: flash@0 {
compatible = "jedec,spi-nor";
reg = <0>;
label = "flash1";
/* JEDEC ID für einen Winbond W25Q128 (16 MBytes) */
jedec-id = [ef 40 18];
/* Speichergröße in Bytes (16 MBytes) */
size = <DT_SIZE_M(16)>;
/* Maximale Taktfrequenz - angepasst an STM32G431 Limits */
spi-max-frequency = <1000000>;
/* Partitions für externes Flash */
partitions {
compatible = "fixed-partitions";
#address-cells = <1>;
#size-cells = <1>;
/* Slot1 partition für MCUboot (sekundäres Image) - 80 KB (20 sectors @ 4KB) */
slot1_partition: partition@0 {
label = "image-1";
reg = <0x00000000 0x00014000>;
};
/* Scratch partition für MCUboot - 80 KB (20 sectors @ 4KB) */
scratch_partition: partition@14000 {
label = "scratch";
reg = <0x00014000 0x00014000>;
};
/* Speicher partition für LittleFS - ~15.83 MB */
storage_partition: partition@28000 {
label = "storage";
reg = <0x00028000 0x00FD8000>;
};
};
};
};

View File

@@ -0,0 +1,23 @@
CONFIG_LOG=y
CONFIG_MCUBOOT_LOG_LEVEL_INF=y
# Enable UART console for MCUboot debug output
CONFIG_UART_CONSOLE=y
CONFIG_CONSOLE=y
CONFIG_MCUBOOT_INDICATION_LED=y
# Enable external SPI flash support
CONFIG_SPI=y
CONFIG_SPI_NOR=y
CONFIG_SPI_NOR_SFDP_DEVICETREE=n
CONFIG_FLASH=y
CONFIG_FLASH_MAP=y
CONFIG_GPIO=y
# Add SPI NOR specific configurations - use 4KB page size (required by driver)
CONFIG_SPI_NOR_FLASH_LAYOUT_PAGE_SIZE=4096
CONFIG_SPI_NOR_INIT_PRIORITY=80
# Set maximum image sectors manually since auto doesn't work with external flash
CONFIG_BOOT_MAX_IMG_SECTORS_AUTO=n
CONFIG_BOOT_MAX_IMG_SECTORS=80

View File

@@ -0,0 +1,8 @@
#include "common.dtsi"
/* MCUboot Configuration - Bootloader wird in boot_partition geschrieben */
/ {
chosen {
zephyr,code-partition = &boot_partition;
};
};

View File

@@ -0,0 +1,10 @@
cmake_minimum_required(VERSION 3.20)
find_package(Zephyr REQUIRED HINTS $ENV{ZEPHYR_BASE})
project(can_node LANGUAGES C)
zephyr_include_directories(../../include)
add_subdirectory(../../lib lib)
target_sources(app PRIVATE src/main.c)

View File

@@ -0,0 +1,2 @@
rsource "../../lib/Kconfig"
source "Kconfig.zephyr"

View File

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

View File

@@ -0,0 +1,7 @@
# Disable UART console
CONFIG_UART_CONSOLE=n
# Enable RTT console
CONFIG_RTT_CONSOLE=y
CONFIG_USE_SEGGER_RTT=y
CONFIG_SHELL_BACKEND_RTT=y

View File

@@ -0,0 +1,43 @@
/ {
chosen {
zephyr,console = &rtt;
zephyr,shell = &rtt;
zephyr,settings-partition = &storage_partition;
};
rtt: rtt {
compatible = "segger,rtt-uart";
#address-cells = <1>;
#size-cells = <0>;
label = "RTT";
status = "okay";
};
};
&flash0 {
partitions {
compatible = "fixed-partitions";
#address-cells = <1>;
#size-cells = <1>;
/* Application partition starts at the beginning of flash */
slot0_partition: partition@0 {
label = "image-0";
reg = <0x00000000 DT_SIZE_K(120)>;
};
/* Use the last 8K for settings */
storage_partition: partition@1E000 {
label = "storage";
reg = <0x0001E000 DT_SIZE_K(8)>;
};
};
};
&usart1 {
modbus0 {
compatible = "zephyr,modbus-serial";
status = "okay";
};
status = "okay";
};

View File

@@ -0,0 +1,47 @@
/ {
aliases {
vnd7050aj = &vnd7050aj;
};
vnd7050aj: vnd7050aj {
compatible = "st,vnd7050aj";
status = "okay";
input0-gpios = <&gpio0 1 GPIO_ACTIVE_HIGH>;
input1-gpios = <&gpio0 2 GPIO_ACTIVE_HIGH>;
select0-gpios = <&gpio0 3 GPIO_ACTIVE_HIGH>;
select1-gpios = <&gpio0 4 GPIO_ACTIVE_HIGH>;
sense-enable-gpios = <&gpio0 5 GPIO_ACTIVE_HIGH>;
fault-reset-gpios = <&gpio0 6 GPIO_ACTIVE_LOW>;
io-channels = <&adc0 0>;
r-sense-ohms = <1500>;
k-vcc = <4000>;
};
modbus_uart: uart_2 {
compatible = "zephyr,native-pty-uart";
status = "okay";
current-speed = <19200>;
modbus0: modbus0 {
compatible = "zephyr,modbus-serial";
status = "okay";
};
};
};
&adc0 {
#address-cells = <1>;
#size-cells = <0>;
ref-internal-mv = <3300>;
ref-external1-mv = <5000>;
channel@0 {
reg = <0>;
zephyr,gain = "ADC_GAIN_1";
zephyr,reference = "ADC_REF_INTERNAL";
zephyr,acquisition-time = <ADC_ACQ_TIME_DEFAULT>;
zephyr,resolution = <12>;
};
};

View File

@@ -0,0 +1,48 @@
/ {
aliases {
vnd7050aj = &vnd7050aj;
};
vnd7050aj: vnd7050aj {
compatible = "st,vnd7050aj";
status = "okay";
input0-gpios = <&gpiob 3 GPIO_ACTIVE_HIGH>;
input1-gpios = <&gpiob 4 GPIO_ACTIVE_HIGH>;
select0-gpios = <&gpiob 0 GPIO_ACTIVE_HIGH>;
select1-gpios = <&gpiob 1 GPIO_ACTIVE_HIGH>;
sense-enable-gpios = <&gpiob 6 GPIO_ACTIVE_HIGH>;
fault-reset-gpios = <&gpiob 5 GPIO_ACTIVE_LOW>;
io-channels = <&adc1 1>;
r-sense-ohms = <1500>;
k-vcc = <4000>;
};
};
&adc1 {
status = "okay";
pinctrl-0 = <&adc1_in1_pa0>;
pinctrl-names = "default";
st,adc-clock-source = "SYNC";
st,adc-prescaler = <4>;
#address-cells = <1>;
#size-cells = <0>;
channel@1 {
reg = <1>;
zephyr,gain = "ADC_GAIN_1";
zephyr,reference = "ADC_REF_INTERNAL";
zephyr,acquisition-time = <ADC_ACQ_TIME_DEFAULT>;
zephyr,resolution = <12>;
};
};
&usart1 {
modbus0 {
compatible = "zephyr,modbus-serial";
status = "okay";
};
status = "okay";
pinctrl-0 = <&usart1_tx_pa9 &usart1_rx_pa10>; // PA9=TX, PA10=RX for Modbus communication
pinctrl-names = "default";
};

View File

@@ -0,0 +1,16 @@
#include <zephyr/dt-bindings/gpio/gpio.h>
&zephyr_udc0 {
cdc_acm_uart0: cdc_acm_uart0 {
compatible = "zephyr,cdc-acm-uart";
modbus0 {
compatible = "zephyr,modbus-serial";
status = "okay";
};
};
};
&usart1 {
/delete-node/ modbus0;
};

View File

@@ -0,0 +1,88 @@
# Copyright (c) 2024, Eduard Iten
# SPDX-License-Identifier: Apache-2.0
description: |
STMicroelectronics VND7050AJ dual-channel high-side driver.
This is a GPIO and ADC controlled device.
compatible: "st,vnd7050aj"
include: base.yaml
properties:
input0-gpios:
type: phandle-array
required: true
description: GPIO to control output channel 0.
input1-gpios:
type: phandle-array
required: true
description: GPIO to control output channel 1.
select0-gpios:
type: phandle-array
required: true
description: GPIO for MultiSense selection bit 0.
select1-gpios:
type: phandle-array
required: true
description: GPIO for MultiSense selection bit 1.
sense-enable-gpios:
type: phandle-array
required: true
description: GPIO to enable the MultiSense output.
fault-reset-gpios:
type: phandle-array
required: true
description: GPIO to reset a latched fault (active-low).
io-channels:
type: phandle-array
required: true
description: |
ADC channel connected to the MultiSense pin. This should be an
io-channels property pointing to the ADC controller and channel number.
r-sense-ohms:
type: int
required: true
description: |
Value of the external sense resistor connected from the MultiSense
pin to GND, specified in Ohms. This is critical for correct
conversion of the analog readings.
k-factor:
type: int
default: 1500
description: |
Factor between PowerMOS and SenseMOS.
k-vcc:
type: int
default: 8000
description: |
VCC sense ratio multiplied by 1000. Used for supply voltage calculation.
t-sense-0:
type: int
default: 25
description: |
Temperature sense reference temperature in degrees Celsius.
v-sense-0:
type: int
default: 2070
description: |
Temperature sense reference voltage in millivolts.
k-tchip:
type: int
default: -5500
description: |
Temperature sense gain coefficient multiplied by 1000.
Used for chip temperature calculation.

View File

@@ -0,0 +1,4 @@
CONFIG_USB_DEVICE_STACK=y
CONFIG_USB_DEVICE_PRODUCT="Modbus slave node"
CONFIG_UART_LINE_CTRL=y
CONFIG_USB_DEVICE_INITIALIZE_AT_BOOT=n

View File

@@ -0,0 +1,31 @@
# Enable Console and printk for logging
CONFIG_CONSOLE=y
CONFIG_LOG=y
# Enable Shell
CONFIG_SHELL=y
CONFIG_REBOOT=y
CONFIG_SHELL_MODBUS=n
CONFIG_SHELL_VALVE=y
CONFIG_SHELL_SYSTEM=y
# Enable Settings Subsystem
CONFIG_SETTINGS=y
CONFIG_SETTINGS_NVS=y
CONFIG_NVS=y
CONFIG_FLASH=y
CONFIG_FLASH_MAP=y
CONFIG_FLASH_PAGE_LAYOUT=y
# Config modbus
CONFIG_UART_INTERRUPT_DRIVEN=y
CONFIG_MODBUS=y
CONFIG_MODBUS_ROLE_SERVER=y
CONFIG_MODBUS_LOG_LEVEL_DBG=y
# enable Valve Driver
CONFIG_LIB_VALVE=y
CONFIG_LOG_VALVE_LEVEL=4
# Enable VND7050AJ
CONFIG_VND7050AJ=y

View File

@@ -0,0 +1,40 @@
#include <zephyr/kernel.h>
#include <zephyr/logging/log.h>
#include <zephyr/settings/settings.h>
#include <app_version.h>
#include <lib/valve.h>
#include <lib/vnd7050aj.h>
LOG_MODULE_REGISTER(main, LOG_LEVEL_INF);
int main(void)
{
int rc;
LOG_INF("Starting Irrigation System CAN Node, Version %s", APP_VERSION_EXTENDED_STRING);
/* Initialize settings subsystem */
rc = settings_subsys_init();
if (rc != 0) {
LOG_ERR("Failed to initialize settings subsystem (%d)", rc);
return rc;
}
LOG_INF("Settings subsystem initialized");
/* Load settings from storage */
rc = settings_load();
if (rc == 0) {
LOG_INF("Settings loaded successfully");
} else {
LOG_WRN("Failed to load settings (%d)", rc);
}
/* Initialize valve system */
rc = valve_init();
if (rc != 0) {
LOG_ERR("Failed to initialize valve system (%d)", rc);
return rc;
}
LOG_INF("Valve system initialized");
return 0;
}

View File

@@ -0,0 +1,5 @@
SB_CONFIG_BOOTLOADER_MCUBOOT=y
SB_CONFIG_MCUBOOT_MODE_SINGLE_APP=y
CONFIG_LOG=y
CONFIG_MCUBOOT_LOG_LEVEL_INF=y

View File

@@ -1,9 +1,8 @@
cmake_minimum_required(VERSION 3.20)
# Include the main 'software' directory as a module to find boards, libs, etc.
list(APPEND ZEPHYR_EXTRA_MODULES ${CMAKE_CURRENT_SOURCE_DIR}/../..)
cmake_minimum_required(VERSION 3.20.5)
find_package(Zephyr REQUIRED HINTS $ENV{ZEPHYR_BASE})
project(gateway)
project(gateway)
target_sources(app PRIVATE src/main.c)
target_include_directories(app PRIVATE include)

View File

@@ -0,0 +1,44 @@
# README for the Hello World Zephyr Application
## Overview
This is a minimal Hello World application built using the Zephyr RTOS. The application demonstrates basic logging functionality by printing a message every 5 seconds, including the version number of the application.
## Project Structure
The project consists of the following files:
- `src/main.c`: The entry point of the application that initializes logging and sets up a timer.
- `include/app_version.h`: Header file that defines the application version.
- `VERSION`: A text file containing the version number of the application.
- `prj.conf`: Configuration file for the Zephyr project, specifying necessary options.
- `CMakeLists.txt`: Build configuration file for CMake.
- `README.md`: Documentation for the project.
## Building the Application
To build the application, follow these steps:
1. Ensure you have the Zephyr development environment set up.
2. Navigate to the `apps/gateway` directory.
3. Run the following command to build the application:
```
west build -b <your_board> .
```
Replace `<your_board>` with the appropriate board name.
## Running the Application
After building the application, you can flash it to your board using:
```
west flash
```
Once the application is running, you will see log messages printed every 5 seconds, including the version number.
## Version
The version of this application can be found in the `VERSION` file and is also included in the log messages.

View File

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

View File

@@ -0,0 +1,16 @@
&flash0 {
reg = <0x0 0x400000>; /* 4MB flash */
};
#include "espressif/partitions_0x0_default_4M.dtsi"
/ {
chosen {
zephyr,shell-uart = &uart0;
zephyr,uart-mcumgr = &usb_serial;
};
};
&usb_serial {
status = "okay";
};

View File

@@ -0,0 +1 @@
#include "common_4MB.dtsi"

View File

@@ -1,2 +1,47 @@
# Gateway Configuration
CONFIG_NETWORKING=y
# -------------------
# Logging and Console
# -------------------
CONFIG_LOG=y
CONFIG_UART_CONSOLE=y
# -------------
# Zephyr Shell
# -------------
CONFIG_SHELL=y
CONFIG_KERNEL_SHELL=y
CONFIG_REBOOT=y
# -------------------
# MCUmgr OS Management
# -------------------
CONFIG_MCUMGR=y
CONFIG_MCUMGR_GRP_OS=y
CONFIG_MCUMGR_TRANSPORT_UART=y
# -------------------
# MCUmgr Filesystem Group
# -------------------
CONFIG_MCUMGR_GRP_FS=y
# -------------------
# LittleFS and Flash
# -------------------
CONFIG_FILE_SYSTEM=y
CONFIG_FILE_SYSTEM_LITTLEFS=y
CONFIG_FLASH=y
CONFIG_FLASH_MAP=y
# -------------------
# Settings Subsystem
# -------------------
CONFIG_SETTINGS=y
CONFIG_SETTINGS_FILE=y
CONFIG_SETTINGS_FILE_PATH="/lfs/settings.bin"
# -------------------
# Dependencies
# -------------------
CONFIG_NET_BUF=y
CONFIG_ZCBOR=y
CONFIG_CRC=y
CONFIG_BASE64=y

View File

@@ -1,13 +1,136 @@
/*
* Copyright (c) 2025 Eduard Iten
*
* SPDX-License-Identifier: Apache-2.0
*/
#include <zephyr/fs/fs.h>
#include <zephyr/fs/littlefs.h>
#include <zephyr/kernel.h>
#include <zephyr/logging/log.h>
#include <zephyr/settings/settings.h>
#include <zephyr/shell/shell.h>
#include <app_version.h>
#include <string.h>
LOG_MODULE_REGISTER(hello_world);
/* LittleFS mount configuration for 'storage_partition' partition */
FS_LITTLEFS_DECLARE_DEFAULT_CONFIG(storage_partition);
static struct fs_mount_t littlefs_mnt = {
.type = FS_LITTLEFS,
.mnt_point = "/lfs",
.fs_data = &storage_partition, // default config macro
.storage_dev = (void *)FIXED_PARTITION_ID(storage_partition),
};
static char my_setting[32] = "default";
static int my_settings_set(const char *name, size_t len, settings_read_cb read_cb, void *cb_arg)
{
if (strcmp(name, "value") == 0) {
if (len > sizeof(my_setting) - 1) {
len = sizeof(my_setting) - 1;
}
if (read_cb(cb_arg, my_setting, len) == len) {
my_setting[len] = '\0';
return 0;
}
return -EINVAL;
}
return -ENOENT;
}
static int my_settings_export(int (*export_func)(const char *, const void *, size_t))
{
return export_func("my/setting/value", my_setting, strlen(my_setting));
}
SETTINGS_STATIC_HANDLER_DEFINE(my, "my/setting", NULL, my_settings_set, NULL, my_settings_export);
static int cmd_my_get(const struct shell *shell, size_t argc, char **argv)
{
shell_print(shell, "my_setting = '%s'", my_setting);
return 0;
}
static int cmd_my_reset(const struct shell *shell, size_t argc, char **argv)
{
strcpy(my_setting, "default");
settings_save();
shell_print(shell, "my_setting reset to default");
return 0;
}
// Improved set command: join all arguments for whitespace support
static int cmd_my_set(const struct shell *shell, size_t argc, char **argv)
{
if (argc < 2) {
shell_error(shell, "Usage: my set <value>");
return -EINVAL;
}
// Join all argv[1..] with spaces
size_t i, pos = 0;
my_setting[0] = '\0';
for (i = 1; i < argc; ++i) {
size_t left = sizeof(my_setting) - 1 - pos;
if (left == 0)
break;
strncat(my_setting, argv[i], left);
pos = strlen(my_setting);
if (i < argc - 1 && left > 1) {
strncat(my_setting, " ", left - 1);
pos = strlen(my_setting);
}
}
my_setting[sizeof(my_setting) - 1] = '\0';
settings_save();
shell_print(shell, "my_setting set to '%s'", my_setting);
return 0;
}
SHELL_STATIC_SUBCMD_SET_CREATE(my_subcmds,
SHELL_CMD(get, NULL, "Get my_setting", cmd_my_get),
SHELL_CMD(set, NULL, "Set my_setting (supports spaces)", cmd_my_set),
SHELL_CMD(reset, NULL, "Reset my_setting to default and compact settings file", cmd_my_reset),
SHELL_SUBCMD_SET_END);
SHELL_CMD_REGISTER(my, &my_subcmds, "My settings commands", NULL);
static void compact_settings_file(void)
{
struct fs_file_t file;
fs_file_t_init(&file);
int rc = fs_open(&file, "/lfs/settings.bin", FS_O_WRITE | FS_O_CREATE | FS_O_TRUNC);
if (rc == 0) {
fs_close(&file);
LOG_INF("Settings file compacted (truncated and recreated)");
} else if (rc == -ENOENT) {
LOG_INF("Settings file did not exist, created new");
} else {
LOG_ERR("Failed to compact settings file (%d)", rc);
}
settings_save();
}
int main(void)
{
printk("Hello from Gateway!\n");
int rc = fs_mount(&littlefs_mnt);
if (rc < 0) {
LOG_ERR("Error mounting LittleFS [%d]", rc);
} else {
LOG_INF("LittleFS mounted at /lfs");
}
/* Initialize settings subsystem */
settings_subsys_init();
LOG_INF("Settings subsystem initialized");
/* Load settings from storage */
rc = settings_load();
if (rc == 0) {
LOG_INF("Settings loaded: my_setting='%s'", my_setting);
} else {
LOG_ERR("Failed to load settings (%d)", rc);
}
/* Compact settings file on each start */
compact_settings_file();
LOG_INF("Hello World! Version: %s", APP_VERSION_EXTENDED_STRING);
return 0;
}

View File

@@ -0,0 +1,2 @@
SB_CONFIG_BOOTLOADER_MCUBOOT=y
SB_CONFIG_MCUBOOT_MODE_SINGLE_APP=y

View File

@@ -0,0 +1,8 @@
#include "../boards/common_4MB.dtsi"
/* Application Configuration - Firmware goes to slot0_partition (0x20000) */
/ {
chosen {
zephyr,code-partition = &slot0_partition;
};
};

View File

@@ -0,0 +1,3 @@
CONFIG_LOG=y
CONFIG_MCUBOOT_LOG_LEVEL_INF=y
CONFIG_UART_CONSOLE=n

View File

@@ -0,0 +1,12 @@
#include "../boards/common_4MB.dtsi"
/* MCUboot Configuration - Bootloader goes to boot_partition (0x0) */
/ {
chosen {
zephyr,code-partition = &boot_partition;
};
aliases {
mcuboot-button0 = &user_button1;
};
};

Submodule software/apps/mqtt_gateway added at 6e669cfc4e

View File

@@ -3,6 +3,8 @@ cmake_minimum_required(VERSION 3.20)
find_package(Zephyr REQUIRED HINTS $ENV{ZEPHYR_BASE})
project(slave_node LANGUAGES C)
zephyr_include_directories(../../include)
add_subdirectory(../../lib lib)
target_sources(app PRIVATE src/main.c)

View File

@@ -0,0 +1,47 @@
/ {
aliases {
vnd7050aj = &vnd7050aj;
};
vnd7050aj: vnd7050aj {
compatible = "st,vnd7050aj";
status = "okay";
input0-gpios = <&gpio0 1 GPIO_ACTIVE_HIGH>;
input1-gpios = <&gpio0 2 GPIO_ACTIVE_HIGH>;
select0-gpios = <&gpio0 3 GPIO_ACTIVE_HIGH>;
select1-gpios = <&gpio0 4 GPIO_ACTIVE_HIGH>;
sense-enable-gpios = <&gpio0 5 GPIO_ACTIVE_HIGH>;
fault-reset-gpios = <&gpio0 6 GPIO_ACTIVE_LOW>;
io-channels = <&adc0 0>;
r-sense-ohms = <1500>;
k-vcc = <4000>;
};
modbus_uart: uart_2 {
compatible = "zephyr,native-pty-uart";
status = "okay";
current-speed = <19200>;
modbus0: modbus0 {
compatible = "zephyr,modbus-serial";
status = "okay";
};
};
};
&adc0 {
#address-cells = <1>;
#size-cells = <0>;
ref-internal-mv = <3300>;
ref-external1-mv = <5000>;
channel@0 {
reg = <0>;
zephyr,gain = "ADC_GAIN_1";
zephyr,reference = "ADC_REF_INTERNAL";
zephyr,acquisition-time = <ADC_ACQ_TIME_DEFAULT>;
zephyr,resolution = <12>;
};
};

View File

@@ -7,15 +7,15 @@
compatible = "st,vnd7050aj";
status = "okay";
input0-gpios = <&gpiob 7 GPIO_ACTIVE_HIGH>;
input1-gpios = <&gpiob 9 GPIO_ACTIVE_HIGH>;
select0-gpios = <&gpiob 5 GPIO_ACTIVE_HIGH>;
select1-gpios = <&gpiob 6 GPIO_ACTIVE_HIGH>;
sense-enable-gpios = <&gpiob 4 GPIO_ACTIVE_HIGH>;
fault-reset-gpios = <&gpiob 3 GPIO_ACTIVE_LOW>;
input0-gpios = <&gpiob 3 GPIO_ACTIVE_HIGH>;
input1-gpios = <&gpiob 4 GPIO_ACTIVE_HIGH>;
select0-gpios = <&gpiob 7 GPIO_ACTIVE_HIGH>;
select1-gpios = <&gpiob 9 GPIO_ACTIVE_HIGH>;
sense-enable-gpios = <&gpiob 6 GPIO_ACTIVE_HIGH>;
fault-reset-gpios = <&gpiob 5 GPIO_ACTIVE_LOW>;
io-channels = <&adc1 1>;
r-sense-ohms = <1500>;
k-vcc = <3816>;
k-vcc = <4000>;
};
};

View File

@@ -0,0 +1,88 @@
# Copyright (c) 2024, Eduard Iten
# SPDX-License-Identifier: Apache-2.0
description: |
STMicroelectronics VND7050AJ dual-channel high-side driver.
This is a GPIO and ADC controlled device.
compatible: "st,vnd7050aj"
include: base.yaml
properties:
input0-gpios:
type: phandle-array
required: true
description: GPIO to control output channel 0.
input1-gpios:
type: phandle-array
required: true
description: GPIO to control output channel 1.
select0-gpios:
type: phandle-array
required: true
description: GPIO for MultiSense selection bit 0.
select1-gpios:
type: phandle-array
required: true
description: GPIO for MultiSense selection bit 1.
sense-enable-gpios:
type: phandle-array
required: true
description: GPIO to enable the MultiSense output.
fault-reset-gpios:
type: phandle-array
required: true
description: GPIO to reset a latched fault (active-low).
io-channels:
type: phandle-array
required: true
description: |
ADC channel connected to the MultiSense pin. This should be an
io-channels property pointing to the ADC controller and channel number.
r-sense-ohms:
type: int
required: true
description: |
Value of the external sense resistor connected from the MultiSense
pin to GND, specified in Ohms. This is critical for correct
conversion of the analog readings.
k-factor:
type: int
default: 1500
description: |
Factor between PowerMOS and SenseMOS.
k-vcc:
type: int
default: 8000
description: |
VCC sense ratio multiplied by 1000. Used for supply voltage calculation.
t-sense-0:
type: int
default: 25
description: |
Temperature sense reference temperature in degrees Celsius.
v-sense-0:
type: int
default: 2070
description: |
Temperature sense reference voltage in millivolts.
k-tchip:
type: int
default: -5500
description: |
Temperature sense gain coefficient multiplied by 1000.
Used for chip temperature calculation.

View File

@@ -22,8 +22,7 @@ CONFIG_SETTINGS_LOG_LEVEL_DBG=y
CONFIG_UART_INTERRUPT_DRIVEN=y
CONFIG_MODBUS=y
CONFIG_MODBUS_ROLE_SERVER=y
CONFIG_MODBUS_BUFFER_SIZE=256
CONFIG_MODBUS_LOG_LEVEL_DBG=y
# Enable VND7050AJ
CONFIG_VND7050AJ=y
CONFIG_LOG_VALVE_LEVEL=4

View File

@@ -9,6 +9,7 @@ LOG_MODULE_REGISTER(main, LOG_LEVEL_INF);
int main(void)
{
int rc;
LOG_INF("Starting Irrigation System Slave Node");
if (settings_subsys_init() || settings_load()) {
@@ -18,9 +19,10 @@ int main(void)
valve_init();
fwu_init();
if (modbus_server_init()) {
LOG_ERR("Modbus RTU server initialization failed");
return 0;
rc = modbus_server_init();
if (rc) {
LOG_ERR("Modbus server initialization failed: %d", rc);
return rc;
}
LOG_INF("Irrigation System Slave Node started successfully");

View File

@@ -0,0 +1,5 @@
SB_CONFIG_BOOTLOADER_MCUBOOT=y
SB_CONFIG_MCUBOOT_MODE_SINGLE_APP=y
CONFIG_LOG=y
CONFIG_MCUBOOT_LOG_LEVEL_INF=y

5
software/esphome/.gitignore vendored Normal file
View File

@@ -0,0 +1,5 @@
# Gitignore settings for ESPHome
# This is an example and may include too much for your use-case.
# You can modify this file to suit your needs.
/.esphome/
/secrets.yaml

106
software/esphome/can.yaml Normal file
View File

@@ -0,0 +1,106 @@
# ===================================================================
# ESPHome Configuration
# CAN-Bus Master für ein Bewässerungssystem auf Basis des ESP32-C6
#
# Version 10: Finale Korrektur der Lambda-Signatur gemäß Dokumentation
# ===================================================================
esphome:
name: can-bridge
friendly_name: Irrigation can bridge
esp32:
board: esp32-c6-devkitm-1
framework:
type: esp-idf # Erforderlich für den ESP32-C6
# --- Netzwerk & Sicherheit ---
wifi:
ssid: !secret wifi_ssid
password: !secret wifi_password
fast_connect: true
api:
encryption:
key: !secret api_key
ota:
platform: esphome
password: !secret ota_password
logger:
web_server:
# --- Globale Variablen ---
globals:
- id: ventil_2_can_state
type: int
initial_value: '0' # Startet als "geschlossen"
# --- CAN-Bus Konfiguration ---
canbus:
- platform: esp32_can
id: my_can_bus
tx_pin: GPIO5
rx_pin: GPIO4
bit_rate: 125kbps
can_id: 0x000 # Erforderlich, um Parser-Fehler zu beheben.
on_frame:
# Horcht nur auf die Statusmeldung von Knoten 2 (ID 0x422)
- can_id: 0x422
then:
- lambda: |-
if (x.size() < 1) {
ESP_LOGW("on_can_frame", "Received empty Frame for ID 0x422");
return;
}
int received_state = x[0];
id(ventil_2_can_state) = received_state;
ESP_LOGD("on_can_frame", "Received state from Valve 2: %i", received_state);
- valve.template.publish:
id: ventil_2
current_operation: !lambda |-
int state = id(ventil_2_can_state);
if (state == 2) {
return VALVE_OPERATION_OPENING;
} else if (state == 3) {
return VALVE_OPERATION_CLOSING;
} else {
return VALVE_OPERATION_IDLE;
}
# --- Home Assistant Entitäten ---
valve:
- platform: template
name: "Ventil 2"
id: ventil_2
# Diese Lambda meldet nur den binären End-Zustand (offen/geschlossen)
lambda: |-
if (id(ventil_2_can_state) == 0) {
return VALVE_CLOSED;
} else if (id(ventil_2_can_state) == 1) {
return VALVE_OPEN;
} else {
return NAN;
}
# Aktionen zum Steuern des Ventils
open_action:
- canbus.send:
canbus_id: my_can_bus
can_id: 0x210
data: [0x02, 0x01]
close_action:
- canbus.send:
canbus_id: my_can_bus
can_id: 0x210
data: [0x02, 0x00]
stop_action:
- canbus.send:
canbus_id: my_can_bus
can_id: 0x210
data: [0x02, 0x03]

View File

@@ -0,0 +1,55 @@
#!/usr/bin/env python3
import secrets
import string
import os
import base64
from ruamel.yaml import YAML
def generate_password(length=32):
"""Generate a random password."""
alphabet = string.ascii_letters + string.digits
return ''.join(secrets.choice(alphabet) for i in range(length))
def generate_api_key():
"""Generate a random 32-byte key and base64 encode it."""
return base64.b64encode(secrets.token_bytes(32)).decode('utf-8')
SECRETS_FILE = 'secrets.yaml'
# In a real ESPHome project, secrets are often included from a central location
# but for this script, we'll assume it's in the current directory.
# You might need to adjust this path.
secrets_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), SECRETS_FILE)
yaml = YAML()
yaml.preserve_quotes = True
# To prevent line wrapping
yaml.width = 4096
try:
with open(secrets_path, 'r') as f:
secrets_data = yaml.load(f)
if secrets_data is None:
secrets_data = {}
except FileNotFoundError:
print(f"Info: '{SECRETS_FILE}' not found. A new file will be created.")
secrets_data = {}
# Generate new random passwords
new_api_key = generate_api_key()
new_ota_password = generate_password()
# Update the dictionary with the new passwords
if 'api_password' in secrets_data:
del secrets_data['api_password']
secrets_data['api_key'] = new_api_key
secrets_data['ota_password'] = new_ota_password
# Write the updated dictionary back to the YAML file
with open(secrets_path, 'w') as f:
yaml.dump(secrets_data, f)
print(f"Successfully updated '{SECRETS_FILE}'.")
print("New values:")
print(f" api_key: {new_api_key}")
print(f" ota_password: {new_ota_password}")

View File

@@ -0,0 +1,166 @@
# ===================================================================
# ESPHome Configuration - Final Version
#
# This version corrects the C++ function call inside the valve actions
# to use the correct `send` method from the ModbusDevice base class,
# which is compatible with the esp-idf framework.
# ===================================================================
esphome:
name: irrigation-system
friendly_name: Bewässerung
esp32:
board: esp32-c6-devkitm-1
framework:
type: esp-idf # Set to esp-idf as required by the ESP32-C6 board
wifi:
ssid: !secret wifi_ssid
password: !secret wifi_password
fast_connect: true
api:
encryption:
key: !secret api_key
ota:
platform: esphome
password: !secret ota_password
logger:
web_server:
# ===================================================================
# HARDWARE SETUP - COMPLETE
# ===================================================================
# --- UART for RS485 Communication ---
uart:
id: uart_bus
tx_pin: GPIO1
rx_pin: GPIO2
baud_rate: 9600
data_bits: 8
stop_bits: 1
parity: NONE
# --- Base Modbus component for the bus ---
modbus:
- id: modbus_hub
uart_id: uart_bus
# --- Modbus Controller for the specific valve device ---
modbus_controller:
- id: valve_controller
modbus_id: modbus_hub
address: 0 # The Modbus address of your valve. Change if not 0.
# update_interval: 1s
# ===================================================================
# SENSORS - COMPLETE
# ===================================================================
sensor:
# This sensor reads the raw 16-bit value from the valve's input register.
- platform: modbus_controller
modbus_controller_id: valve_controller
name: "Valve Raw Status"
id: valve_raw_status
internal: true # Hide from Home Assistant UI
register_type: read # 'read' is the valid type for input registers
address: 0x0000 # The address of the register to read
value_type: U_WORD # Read the full 16-bit unsigned word
- platform: modbus_controller
modbus_controller_id: valve_controller
name: "VDD"
id: valve_vdd
register_type: read # 'read' is the valid type for input registers
address: 0x00FC # The address of the register to read
value_type: U_WORD # Read the full 16-bit unsigned word
entity_category: diagnostic # Mark as diagnostic
unit_of_measurement: "V"
accuracy_decimals: 2 # Show two decimal places
# Apply filters to convert the raw value to volts and update periodically
filters:
- lambda: |-
// Convert the raw VDD value to volts
return x / 1000.0; // Assuming the value is in millivolts
- heartbeat: 60s # Update every 60 seconds
- delta: 200 # Only update if the value changes by more than 200 mV
# ===================================================================
# TEXT SENSORS FOR HUMAN-READABLE STATUS
# ===================================================================
text_sensor:
# 1. This text sensor extracts the HIGH BYTE for the operation status.
- platform: template
name: "Valve Operation"
id: valve_operation_status
icon: "mdi:state-machine"
lambda: |-
// Extract the high byte from the raw status sensor
// using a bitwise right shift.
int operation_code = (int)id(valve_raw_status).state >> 8;
switch (operation_code) {
case 0: return {"Idle"};
case 1: return {"Opening"};
case 2: return {"Closing"};
case 3: return {"Obstacle Detected"};
case 4: return {"End Position Not Reached"};
default: return {"Unknown Operation"};
}
# 2. This text sensor extracts the LOW BYTE for the current valve state.
- platform: template
name: "Valve Position"
id: valve_position_status
icon: "mdi:valve"
lambda: |-
// Extract the low byte from the raw status sensor
// using a bitwise AND mask.
int state_code = (int)id(valve_raw_status).state & 0xFF;
switch (state_code) {
case 0: return {"Closed"};
case 1: return {"Open"};
default: return {"Unknown"};
}
# ===================================================================
# THE MAIN VALVE COMPONENT
# ===================================================================
valve:
- platform: template
name: "Modbus Controlled Valve"
id: modbus_valve
optimistic: false
# The lambda determines the current state (open or closed) of the valve.
lambda: |-
int state_code = (int)id(valve_raw_status).state & 0xFF;
if (state_code == 1) {
return true; // Open
} else if (state_code == 0) {
return false; // Closed
} else {
return {}; // Unknown
}
# Action to execute when the "OPEN" button is pressed.
open_action:
- lambda: |-
// Use the send() command inherited from ModbusDevice
// Function 0x06: Write Single Register
// Payload for value 1 is {0x00, 0x01}
const uint8_t data[] = {0x00, 0x01};
id(valve_controller).send(0x06, 0x0000, 1, 2, data);
# Action to execute when the "CLOSE" button is pressed.
close_action:
- lambda: |-
// Payload for value 2 is {0x00, 0x02}
const uint8_t data[] = {0x00, 0x02};
id(valve_controller).send(0x06, 0x0000, 1, 2, data);
# Action to execute when the "STOP" button is pressed.
stop_action:
- lambda: |-
// Payload for value 3 is {0x00, 0x03}
const uint8_t data[] = {0x00, 0x03};
id(valve_controller).send(0x06, 0x0000, 1, 2, data);

View File

@@ -0,0 +1,2 @@
ruamel.yaml
esphome

View File

@@ -0,0 +1,4 @@
wifi_ssid: 'PUT YOUR WIFI SSID HERE'
wifi_password: 'PUT YOUR WIFI PASSWORD HERE'
api_key: 'PUT YOUR KEY HERE OR USE create_secrets.py'
ota_password: 'PUT YOUR KEY HERE OR USE create_secrets.py'

View File

@@ -0,0 +1,41 @@
#ifndef CAN_IDS_H
#define CAN_IDS_H
/*
CAN ID structure for the irrigation system.
PPP FFFF NNNN
PPP: Priority
000: Network segment
001: Critical error
010: Commands
100: Status messages
110: measurements
111: Info messages
FFFF: Function
0001: Valve Commands
0010: Valve States
0011: IO Commands
0100: IO States
0101: Measurements
0111: Sysem Functions (e.g. reset, firmware update)
NNNN: Node ID
*/
#define CAN_ID_PRIORITY_NETWORK 0x000
#define CAN_ID_PRIORITY_CRITICAL_ERROR 0x100
#define CAN_ID_PRIORITY_COMMANDS 0x200
#define CAN_ID_PRIORITY_STATUS 0x400
#define CAN_ID_PRIORITY_MEASUREMENTS 0x600
#define CAN_ID_PRIORITY_INFO 0x700
#define CAN_ID_FUNCTION_VALVE_COMMANDS 0x010
#define CAN_ID_FUNCTION_VALVE_STATES 0x020
#define CAN_ID_FUNCTION_IO_COMMANDS 0x030
#define CAN_ID_FUNCTION_IO_STATES 0x040
#define CAN_ID_FUNCTION_MEASUREMENTS 0x050
#define CAN_ID_FUNCTION_SYSTEM_FUNCTIONS 0x070
#endif // CAN_IDS_H

View File

@@ -97,6 +97,14 @@ enum {
* closing.
*/
REG_HOLDING_END_CURRENT_THRESHOLD_CLOSE_MA = 0x0004,
/**
* @brief Current threshold in mA for obstacle detection during opening.
*/
REG_HOLDING_OBSTACLE_THRESHOLD_OPEN_MA = 0x0005,
/**
* @brief Current threshold in mA for obstacle detection during closing.
*/
REG_HOLDING_OBSTACLE_THRESHOLD_CLOSE_MA = 0x0006,
/**
* @brief Bitmask for reading and writing digital outputs. Bit 0: Output 1,
* Bit 1: Output 2. 1=ON, 0=OFF.

View File

@@ -15,10 +15,8 @@
#define VALVE_CHANNEL_OPEN 0
#define VALVE_CHANNEL_CLOSE 1
#define VALVE_ENDPOSITION_CHECK_INTERVAL K_MSEC(100)
#define VALVE_OBSTACLE_THRESHOLD_OPEN_MA 500
#define VALVE_OBSTACLE_THRESHOLD_CLOSE_MA 500
#define VALVE_CURRENT_CHECK_INTERVAL K_MSEC(CONFIG_VALVE_INTERVALL_CURRENT_CHECK_MS)
#define VALVE_INITIAL_CURRENT_CHECK_INTERVAL K_MSEC(CONFIG_VALVE_INITIAL_INTERVALL_CURRENT_CHECK_MS)
/**
* @brief Represents the static state of the valve (open or closed).
@@ -164,4 +162,53 @@ int32_t valve_get_vnd_temp(void);
* @return The voltage in millivolts.
*/
int32_t valve_get_vnd_voltage(void);
/**
* @brief Sets the current threshold for obstacle detection during opening.
*
* @param current_ma The current threshold in milliamps.
*/
void valve_set_obstacle_threshold_open(uint16_t current_ma);
/**
* @brief Sets the current threshold for obstacle detection during closing.
*
* @param current_ma The current threshold in milliamps.
*/
void valve_set_obstacle_threshold_close(uint16_t current_ma);
/**
* @brief Gets the current threshold for obstacle detection during opening.
*
* @return The current threshold in milliamps.
*/
uint16_t valve_get_obstacle_threshold_open(void);
/**
* @brief Gets the current threshold for obstacle detection during closing.
*
* @return The current threshold in milliamps.
*/
uint16_t valve_get_obstacle_threshold_close(void);
/**
* @brief Callback function called during valve opening with current readings.
*
* This is a weak function that can be overridden to provide custom handling
* of current readings during valve opening operations.
*
* @param current_ma The current reading in milliamps.
*/
void valve_current_open_callback(int current_ma);
/**
* @brief Callback function called during valve closing with current readings.
*
* This is a weak function that can be overridden to provide custom handling
* of current readings during valve closing operations.
*
* @param current_ma The current reading in milliamps.
*/
void valve_current_close_callback(int current_ma);
#endif // VALVE_H

View File

@@ -0,0 +1,74 @@
/*
* Copyright (c) 2025, Eduard Iten
* SPDX-License-Identifier: Apache-2.0
*/
#ifndef ZEPHYR_INCLUDE_DRIVERS_MISC_VND7050AJ_H_
#define ZEPHYR_INCLUDE_DRIVERS_MISC_VND7050AJ_H_
#include <zephyr/device.h>
#include <zephyr/kernel.h>
#ifdef __cplusplus
extern "C" {
#endif
/**
* @brief Channel identifiers for the VND7050AJ.
*/
#define VND7050AJ_CHANNEL_0 0
#define VND7050AJ_CHANNEL_1 1
/**
* @brief Sets the state of a specific output channel.
*
* @param dev Pointer to the device structure for the driver instance.
* @param channel The channel to control (VND7050AJ_CHANNEL_0 or VND7050AJ_CHANNEL_1).
* @param state The desired state (true for ON, false for OFF).
* @return 0 on success, negative error code on failure.
*/
int vnd7050aj_set_output_state(const struct device *dev, uint8_t channel, bool state);
/**
* @brief Reads the load current for a specific channel.
*
* @param dev Pointer to the device structure for the driver instance.
* @param channel The channel to measure (VND7050AJ_CHANNEL_0 or VND7050AJ_CHANNEL_1).
* @param[out] current_ma Pointer to store the measured current in milliamperes (mA).
* @return 0 on success, negative error code on failure.
*/
int vnd7050aj_read_load_current(const struct device *dev, uint8_t channel, int32_t *current_ma);
/**
* @brief Reads the VCC supply voltage.
*
* @param dev Pointer to the device structure for the driver instance.
* @param[out] voltage_mv Pointer to store the measured voltage in millivolts (mV).
* @return 0 on success, negative error code on failure.
*/
int vnd7050aj_read_supply_voltage(const struct device *dev, int32_t *voltage_mv);
/**
* @brief Reads the internal chip temperature.
*
* @param dev Pointer to the device structure for the driver instance.
* @param[out] temp_c Pointer to store the measured temperature in degrees Celsius (°C).
* @return 0 on success, negative error code on failure.
*/
int vnd7050aj_read_chip_temp(const struct device *dev, int32_t *temp_c);
/**
* @brief Resets a latched fault condition.
*
* This function sends a low pulse to the FaultRST pin.
*
* @param dev Pointer to the device structure for the driver instance.
* @return 0 on success, negative error code on failure.
*/
int vnd7050aj_reset_fault(const struct device *dev);
#ifdef __cplusplus
}
#endif
#endif /* ZEPHYR_INCLUDE_DRIVERS_MISC_VND7050AJ_H_ */

View File

@@ -4,3 +4,4 @@ add_subdirectory_ifdef(CONFIG_LIB_VALVE valve)
add_subdirectory_ifdef(CONFIG_SHELL_SYSTEM shell_system)
add_subdirectory_ifdef(CONFIG_SHELL_MODBUS shell_modbus)
add_subdirectory_ifdef(CONFIG_SHELL_VALVE shell_valve)
add_subdirectory_ifdef(CONFIG_VND7050AJ vnd7050aj)

View File

@@ -6,4 +6,5 @@ rsource "valve/Kconfig"
rsource "shell_system/Kconfig"
rsource "shell_modbus/Kconfig"
rsource "shell_valve/Kconfig"
rsource "vnd7050aj/Kconfig"
endmenu

View File

@@ -1,5 +1,5 @@
config LIB_MODBUS_SERVER
bool "Enable Modbus Server Library"
default y
default n
help
Enable the Modbus Server Library.

View File

@@ -8,7 +8,6 @@
*/
#include <zephyr/device.h>
#include <zephyr/drivers/misc/vnd7050aj/vnd7050aj.h>
#include <zephyr/drivers/uart.h>
#include <zephyr/kernel.h>
#include <zephyr/logging/log.h>
@@ -20,6 +19,7 @@
#include <lib/fwu.h>
#include <lib/modbus_server.h>
#include <lib/valve.h>
#include <lib/vnd7050aj.h>
LOG_MODULE_REGISTER(modbus_server, LOG_LEVEL_INF);
@@ -87,6 +87,12 @@ static int holding_reg_rd(uint16_t addr, uint16_t *reg)
case REG_HOLDING_END_CURRENT_THRESHOLD_CLOSE_MA:
*reg = valve_get_end_current_threshold_close();
break;
case REG_HOLDING_OBSTACLE_THRESHOLD_OPEN_MA:
*reg = valve_get_obstacle_threshold_open();
break;
case REG_HOLDING_OBSTACLE_THRESHOLD_CLOSE_MA:
*reg = valve_get_obstacle_threshold_close();
break;
default:
*reg = 0;
break;
@@ -126,6 +132,12 @@ static int holding_reg_wr(uint16_t addr, uint16_t reg)
case REG_HOLDING_END_CURRENT_THRESHOLD_CLOSE_MA:
valve_set_end_current_threshold_close(reg);
break;
case REG_HOLDING_OBSTACLE_THRESHOLD_OPEN_MA:
valve_set_obstacle_threshold_open(reg);
break;
case REG_HOLDING_OBSTACLE_THRESHOLD_CLOSE_MA:
valve_set_obstacle_threshold_close(reg);
break;
case REG_HOLDING_WATCHDOG_TIMEOUT_S:
watchdog_timeout_s = reg;
if (watchdog_timeout_s > 0) {
@@ -188,12 +200,6 @@ static int input_reg_rd(uint16_t addr, uint16_t *reg)
case REG_INPUT_FIRMWARE_VERSION_PATCH:
*reg = APP_PATCHLEVEL;
break;
case REG_HOLDING_END_CURRENT_THRESHOLD_OPEN_MA:
*reg = valve_get_end_current_threshold_open();
break;
case REG_HOLDING_END_CURRENT_THRESHOLD_CLOSE_MA:
*reg = valve_get_end_current_threshold_close();
break;
default:
*reg = 0;
break;

View File

@@ -10,6 +10,7 @@
#include <zephyr/shell/shell.h>
#include <lib/modbus_server.h>
#include <stdio.h>
#include <stdlib.h>
/**

View File

@@ -55,6 +55,32 @@ static int cmd_valve_set_end_curr_close(const struct shell *sh, size_t argc, cha
return 0;
}
static int cmd_valve_set_obstacle_open(const struct shell *sh, size_t argc, char **argv)
{
if (argc != 2) {
shell_print(sh, "Usage: valve set_obstacle_open <milliamps>");
return -EINVAL;
}
uint16_t current_ma = (uint16_t)atoi(argv[1]);
valve_set_obstacle_threshold_open(current_ma);
shell_print(sh, "Obstacle threshold (open) set to %u mA.", current_ma);
return 0;
}
static int cmd_valve_set_obstacle_close(const struct shell *sh, size_t argc, char **argv)
{
if (argc != 2) {
shell_print(sh, "Usage: valve set_obstacle_close <milliamps>");
return -EINVAL;
}
uint16_t current_ma = (uint16_t)atoi(argv[1]);
valve_set_obstacle_threshold_close(current_ma);
shell_print(sh, "Obstacle threshold (close) set to %u mA.", current_ma);
return 0;
}
static int cmd_valve_show(const struct shell *sh, size_t argc, char **argv)
{
const int label_width = 30;
@@ -72,21 +98,88 @@ static int cmd_valve_show(const struct shell *sh, size_t argc, char **argv)
label_width,
"End Current Threshold (Close):",
valve_get_end_current_threshold_close());
shell_print(sh,
"%*s %u mA",
label_width,
"Obstacle Threshold (Open):",
valve_get_obstacle_threshold_open());
shell_print(sh,
"%*s %u mA",
label_width,
"Obstacle Threshold (Close):",
valve_get_obstacle_threshold_close());
return 0;
}
static int cmd_valve_open(const struct shell *sh, size_t argc, char **argv)
{
ARG_UNUSED(argc);
ARG_UNUSED(argv);
if (valve_get_movement() != VALVE_MOVEMENT_IDLE) {
shell_print(sh, "Valve is already moving.");
return -EBUSY;
}
valve_open();
shell_print(sh, "Valve is opening.");
return 0;
}
static int cmd_valve_close(const struct shell *sh, size_t argc, char **argv)
{
ARG_UNUSED(argc);
ARG_UNUSED(argv);
if (valve_get_movement() != VALVE_MOVEMENT_IDLE) {
shell_print(sh, "Valve is already moving.");
return -EBUSY;
}
valve_close();
shell_print(sh, "Valve is closing.");
return 0;
}
static int cmd_valve_stop(const struct shell *sh, size_t argc, char **argv)
{
ARG_UNUSED(argc);
ARG_UNUSED(argv);
if (valve_get_movement() == VALVE_MOVEMENT_IDLE) {
shell_print(sh, "Valve is already stopped.");
return -EINVAL;
}
valve_stop();
shell_print(sh, "Valve movement stopped.");
return 0;
}
SHELL_STATIC_SUBCMD_SET_CREATE(sub_valve_settings,
SHELL_CMD(set_open_t, NULL, "Set max open time (seconds)", cmd_valve_set_open_t),
SHELL_CMD(set_close_t, NULL, "Set max close time (seconds)", cmd_valve_set_close_t),
SHELL_CMD(set_end_curr_open,
SHELL_CMD(open_t, NULL, "Set max open time (seconds)", cmd_valve_set_open_t),
SHELL_CMD(close_t, NULL, "Set max close time (seconds)", cmd_valve_set_close_t),
SHELL_CMD(end_curr_open,
NULL,
"Set end current threshold for opening (mA)",
cmd_valve_set_end_curr_open),
SHELL_CMD(set_end_curr_close,
SHELL_CMD(end_curr_close,
NULL,
"Set end current threshold for closing (mA)",
cmd_valve_set_end_curr_close),
SHELL_CMD(show, NULL, "Show valve configuration", cmd_valve_show),
SHELL_CMD(obstacle_curr_open,
NULL,
"Set obstacle threshold for opening (mA)",
cmd_valve_set_obstacle_open),
SHELL_CMD(obstacle_curr_close,
NULL,
"Set obstacle threshold for closing (mA)",
cmd_valve_set_obstacle_close),
SHELL_SUBCMD_SET_END);
SHELL_CMD_REGISTER(valve, &sub_valve_settings, "Valve commands", NULL);
SHELL_STATIC_SUBCMD_SET_CREATE(valve_cmds,
SHELL_CMD(show, NULL, "Show valve configuration", cmd_valve_show),
SHELL_CMD(set, &sub_valve_settings, "Valve settings commands", NULL),
SHELL_CMD(open, NULL, "Open the valve", cmd_valve_open),
SHELL_CMD(close, NULL, "Close the valve", cmd_valve_close),
SHELL_CMD(stop, NULL, "Stop the valve movement", cmd_valve_stop),
SHELL_SUBCMD_SET_END);
SHELL_CMD_REGISTER(valve, &valve_cmds, "Valve commands", NULL);

View File

@@ -1,6 +1,6 @@
config LIB_VALVE
bool "Enable Valve Library"
default y
default n
help
Enable the Valve Library.
@@ -11,4 +11,35 @@ config LOG_VALVE_LEVEL
help
Set the log level for the Valve Library.
0 = None, 1 = Error, 2 = Warning, 3 = Info, 4 = Debug
config VALVE_INTERVALL_CURRENT_CHECK_MS
int "Interval Current Check (ms)"
default 100
help
Set the interval in milliseconds for checking the motor current
during valve operation. This is used to detect obstacles.
config VALVE_INITIAL_INTERVALL_CURRENT_CHECK_MS
int "Initial Current Check (ms)"
default 200
help
Set the initial delay in milliseconds before the first current check
after starting the valve operation. This allows the motor to stabilize.
config VALVE_OBSTACLE_THRESHOLD_OPEN_MA
int "Obstacle Threshold Open (mA)"
default 200
help
Set the current threshold in milliamps for obstacle detection
during valve opening. If the motor current exceeds this value,
an obstacle is detected and the valve stops.
config VALVE_OBSTACLE_THRESHOLD_CLOSE_MA
int "Obstacle Threshold Close (mA)"
default 200
help
Set the current threshold in milliamps for obstacle detection
during vaslve closing. If the motor current exceeds this value,
an obstacle is detected and the valve stops.
endif # LIB_VALVE

View File

@@ -9,11 +9,11 @@
#include <zephyr/device.h>
#include <zephyr/drivers/gpio.h>
#include <zephyr/drivers/misc/vnd7050aj/vnd7050aj.h>
#include <zephyr/kernel.h>
#include <zephyr/logging/log.h>
#include <zephyr/settings/settings.h>
#include <lib/valve.h>
#include <lib/vnd7050aj.h>
#define VND_NODE DT_ALIAS(vnd7050aj)
#if !DT_NODE_HAS_STATUS(VND_NODE, okay)
@@ -29,6 +29,8 @@ static uint16_t max_opening_time_s = 10;
static uint16_t max_closing_time_s = 10;
static uint16_t end_current_threshold_open_ma = 10;
static uint16_t end_current_threshold_close_ma = 10;
static uint16_t obstacle_threshold_open_ma = CONFIG_VALVE_OBSTACLE_THRESHOLD_OPEN_MA;
static uint16_t obstacle_threshold_close_ma = CONFIG_VALVE_OBSTACLE_THRESHOLD_CLOSE_MA;
static struct k_work_delayable valve_work;
static struct k_timer movement_timer;
@@ -47,31 +49,31 @@ static void valve_work_handler(struct k_work *work)
if (current_movement == VALVE_MOVEMENT_OPENING) {
vnd7050aj_read_load_current(vnd7050aj_dev, VALVE_CHANNEL_OPEN, &current_ma);
LOG_DBG("Current load during opening: %d mA", current_ma);
if (current_ma > VALVE_OBSTACLE_THRESHOLD_OPEN_MA) {
valve_current_open_callback(current_ma);
if (current_ma > obstacle_threshold_open_ma) {
LOG_ERR(
"Obstacle detected during opening (current: %d mA), stopping motor.",
current_ma);
current_movement = VALVE_MOVEMENT_ERROR;
valve_stop();
goto work_handler_cleanup;
return;
} else if (current_ma > end_current_threshold_open_ma) {
k_work_schedule(&valve_work, VALVE_ENDPOSITION_CHECK_INTERVAL);
k_work_schedule(&valve_work, VALVE_CURRENT_CHECK_INTERVAL);
return;
}
LOG_DBG("Valve finished opening");
} else if (current_movement == VALVE_MOVEMENT_CLOSING) {
vnd7050aj_read_load_current(vnd7050aj_dev, VALVE_CHANNEL_CLOSE, &current_ma);
LOG_DBG("Current load during closing: %d mA", current_ma);
if (current_ma > VALVE_OBSTACLE_THRESHOLD_CLOSE_MA) {
valve_current_close_callback(current_ma);
if (current_ma > obstacle_threshold_close_ma) {
LOG_ERR(
"Obstacle detected during closing (current: %d mA), stopping motor.",
current_ma);
current_movement = VALVE_MOVEMENT_ERROR;
valve_stop();
goto work_handler_cleanup;
return;
} else if (current_ma > end_current_threshold_close_ma) {
k_work_schedule(&valve_work, VALVE_ENDPOSITION_CHECK_INTERVAL);
k_work_schedule(&valve_work, VALVE_CURRENT_CHECK_INTERVAL);
return;
}
current_state = VALVE_STATE_CLOSED;
@@ -79,12 +81,7 @@ static void valve_work_handler(struct k_work *work)
}
current_movement = VALVE_MOVEMENT_IDLE;
work_handler_cleanup:
// Reset the movement timer
k_timer_stop(&movement_timer);
vnd7050aj_set_output_state(vnd7050aj_dev, VALVE_CHANNEL_OPEN, false);
vnd7050aj_set_output_state(vnd7050aj_dev, VALVE_CHANNEL_CLOSE, false);
valve_stop();
}
/**
@@ -109,7 +106,8 @@ void movement_timeout_handler(struct k_timer *timer)
}
vnd7050aj_set_output_state(vnd7050aj_dev, VALVE_CHANNEL_OPEN, false);
vnd7050aj_set_output_state(vnd7050aj_dev, VALVE_CHANNEL_CLOSE, false);
current_state = VALVE_STATE_CLOSED;
current_state = VALVE_STATE_OPEN;
current_movement = VALVE_MOVEMENT_IDLE;
}
int valve_init(void)
@@ -130,13 +128,21 @@ int valve_init(void)
settings_load_one("valve/end_current_threshold_close",
&end_current_threshold_close_ma,
sizeof(end_current_threshold_close_ma));
settings_load_one("valve/obstacle_threshold_open",
&obstacle_threshold_open_ma,
sizeof(obstacle_threshold_open_ma));
settings_load_one("valve/obstacle_threshold_close",
&obstacle_threshold_close_ma,
sizeof(obstacle_threshold_close_ma));
LOG_INF("Valve initialized: max_open=%us, max_close=%us, end_curr_open=%umA, "
"end_curr_close=%umA",
"end_curr_close=%umA, obs_open=%umA, obs_close=%umA",
max_opening_time_s,
max_closing_time_s,
end_current_threshold_open_ma,
end_current_threshold_close_ma);
end_current_threshold_close_ma,
obstacle_threshold_open_ma,
obstacle_threshold_close_ma);
valve_close();
return 0;
}
@@ -147,13 +153,13 @@ void valve_open(void)
vnd7050aj_reset_fault(vnd7050aj_dev);
vnd7050aj_set_output_state(vnd7050aj_dev, VALVE_CHANNEL_CLOSE, false);
vnd7050aj_set_output_state(vnd7050aj_dev, VALVE_CHANNEL_OPEN, true);
current_state = VALVE_STATE_OPEN;
current_movement = VALVE_MOVEMENT_OPENING; /* Security: assume valve open as
soons as it starts opening */
current_state =
VALVE_STATE_OPEN; /* Security: assume valve open as soon as it starts opening */
current_movement = VALVE_MOVEMENT_OPENING;
if (max_opening_time_s > 0) {
k_timer_start(&movement_timer, K_SECONDS(max_opening_time_s), K_NO_WAIT);
}
k_work_schedule(&valve_work, K_MSEC(100));
k_work_schedule(&valve_work, VALVE_INITIAL_CURRENT_CHECK_INTERVAL);
}
void valve_close(void)
@@ -166,7 +172,7 @@ void valve_close(void)
k_timer_start(&movement_timer, K_SECONDS(max_closing_time_s), K_NO_WAIT);
}
current_movement = VALVE_MOVEMENT_CLOSING;
k_work_schedule(&valve_work, VALVE_ENDPOSITION_CHECK_INTERVAL);
k_work_schedule(&valve_work, VALVE_INITIAL_CURRENT_CHECK_INTERVAL);
}
void valve_stop(void)
@@ -260,3 +266,39 @@ int32_t valve_get_vnd_voltage(void)
vnd7050aj_read_supply_voltage(vnd7050aj_dev, &voltage_mv);
return voltage_mv;
}
void valve_set_obstacle_threshold_open(uint16_t current_ma)
{
obstacle_threshold_open_ma = current_ma;
settings_save_one("valve/obstacle_threshold_open",
&obstacle_threshold_open_ma,
sizeof(obstacle_threshold_open_ma));
}
void valve_set_obstacle_threshold_close(uint16_t current_ma)
{
obstacle_threshold_close_ma = current_ma;
settings_save_one("valve/obstacle_threshold_close",
&obstacle_threshold_close_ma,
sizeof(obstacle_threshold_close_ma));
}
uint16_t valve_get_obstacle_threshold_open(void)
{
return obstacle_threshold_open_ma;
}
uint16_t valve_get_obstacle_threshold_close(void)
{
return obstacle_threshold_close_ma;
}
__weak void valve_current_open_callback(int current_ma)
{
LOG_DBG("Open current callback: %d mA", current_ma);
}
__weak void valve_current_close_callback(int current_ma)
{
LOG_DBG("Close current callback: %d mA", current_ma);
}

View File

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

View File

@@ -0,0 +1,33 @@
# SPDX-License-Identifier: Apache-2.0
config VND7050AJ
bool "VND7050AJ driver"
default n
select ADC
select GPIO
help
Enable support for the VND7050AJ high-side driver.
if VND7050AJ
config VND7050AJ_INIT_PRIORITY
int "VND7050AJ initialization priority"
default 80
help
VND7050AJ driver initialization priority. This should be set to a value
that ensures the driver is initialized after the required subsystems
(like GPIO, ADC) but before application code runs.
config VND7050AJ_LOG_LEVEL
int "VND7050AJ Log level"
depends on LOG
default 3
range 0 4
help
Sets log level for VND7050AJ driver.
Levels are:
0 OFF, do not write
1 ERROR, only write LOG_ERR
2 WARNING, write LOG_WRN in addition to previous level
3 INFO, write LOG_INF in addition to previous levels
4 DEBUG, write LOG_DBG in addition to previous levels
endif # VND7050AJ

View File

@@ -0,0 +1,333 @@
/*
* Copyright (c) 2025, Eduard Iten
* SPDX-License-Identifier: Apache-2.0
*/
#define DT_DRV_COMPAT st_vnd7050aj
#include <zephyr/device.h>
#include <zephyr/drivers/adc.h>
#include <zephyr/drivers/gpio.h>
#include <zephyr/kernel.h>
#include <zephyr/logging/log.h>
#include <zephyr/sys/util.h>
#include <lib/vnd7050aj.h>
LOG_MODULE_REGISTER(VND7050AJ, CONFIG_VND7050AJ_LOG_LEVEL);
/* Diagnostic selection modes */
enum vnd7050aj_diag_mode {
DIAG_CURRENT_CH0,
DIAG_CURRENT_CH1,
DIAG_VCC,
DIAG_TEMP,
};
struct vnd7050aj_config {
struct gpio_dt_spec input0_gpio;
struct gpio_dt_spec input1_gpio;
struct gpio_dt_spec sel0_gpio;
struct gpio_dt_spec sel1_gpio;
struct gpio_dt_spec sen_gpio;
struct gpio_dt_spec fault_reset_gpio;
struct adc_dt_spec io_channels;
uint32_t r_sense_ohms;
uint32_t k_factor; /* Current sense ratio */
uint32_t k_vcc; /* VCC sense ratio * 1000 */
int32_t t_sense_0; /* Temp sense reference temperature in °C */
uint32_t v_sense_0; /* Temp sense reference voltage in mV */
uint32_t k_tchip; /* Temp sense gain in °C/mV * 1000 */
};
struct vnd7050aj_data {
struct k_mutex lock;
};
static int vnd7050aj_init(const struct device *dev)
{
const struct vnd7050aj_config *config = dev->config;
struct vnd7050aj_data *data = dev->data;
int err;
k_mutex_init(&data->lock);
LOG_DBG("Initializing VND7050AJ device %s", dev->name);
/* --- Check if all required devices are ready --- */
if (!gpio_is_ready_dt(&config->input0_gpio)) {
LOG_ERR("Input0 GPIO port is not ready");
return -ENODEV;
}
if (!gpio_is_ready_dt(&config->input1_gpio)) {
LOG_ERR("Input1 GPIO port is not ready");
return -ENODEV;
}
if (!gpio_is_ready_dt(&config->sel0_gpio)) {
LOG_ERR("Select0 GPIO port is not ready");
return -ENODEV;
}
if (!gpio_is_ready_dt(&config->sel1_gpio)) {
LOG_ERR("Select1 GPIO port is not ready");
return -ENODEV;
}
if (!gpio_is_ready_dt(&config->sen_gpio)) {
LOG_ERR("Sense GPIO port is not ready");
return -ENODEV;
}
if (!gpio_is_ready_dt(&config->fault_reset_gpio)) {
LOG_ERR("Fault Reset GPIO port is not ready");
return -ENODEV;
}
if (!adc_is_ready_dt(&config->io_channels)) {
LOG_ERR("ADC controller not ready");
return -ENODEV;
}
/* --- Configure GPIOs to their initial states --- */
err = gpio_pin_configure_dt(&config->input0_gpio, GPIO_OUTPUT_INACTIVE);
if (err) {
LOG_ERR("Failed to configure input0 GPIO: %d", err);
return err;
}
err = gpio_pin_configure_dt(&config->input1_gpio, GPIO_OUTPUT_INACTIVE);
if (err) {
LOG_ERR("Failed to configure input1 GPIO: %d", err);
return err;
}
err = gpio_pin_configure_dt(&config->sel0_gpio, GPIO_OUTPUT_INACTIVE);
if (err) {
LOG_ERR("Failed to configure select0 GPIO: %d", err);
return err;
}
err = gpio_pin_configure_dt(&config->sel1_gpio, GPIO_OUTPUT_INACTIVE);
if (err) {
LOG_ERR("Failed to configure select1 GPIO: %d", err);
return err;
}
err = gpio_pin_configure_dt(&config->sen_gpio, GPIO_OUTPUT_INACTIVE);
if (err) {
LOG_ERR("Failed to configure sense GPIO: %d", err);
return err;
}
err = gpio_pin_configure_dt(
&config->fault_reset_gpio, GPIO_OUTPUT_ACTIVE); /* Active-low, so init high */
if (err) {
LOG_ERR("Failed to configure fault reset GPIO: %d", err);
return err;
}
/* --- Configure the ADC channel --- */
err = adc_channel_setup_dt(&config->io_channels);
if (err) {
LOG_ERR("Failed to setup ADC channel: %d", err);
return err;
}
LOG_DBG("Device %s initialized", dev->name);
return 0;
}
#define VND7050AJ_DEFINE(inst) \
static struct vnd7050aj_data vnd7050aj_data_##inst; \
\
static const struct vnd7050aj_config vnd7050aj_config_##inst = { \
.input0_gpio = GPIO_DT_SPEC_INST_GET(inst, input0_gpios), \
.input1_gpio = GPIO_DT_SPEC_INST_GET(inst, input1_gpios), \
.sel0_gpio = GPIO_DT_SPEC_INST_GET(inst, select0_gpios), \
.sel1_gpio = GPIO_DT_SPEC_INST_GET(inst, select1_gpios), \
.sen_gpio = GPIO_DT_SPEC_INST_GET(inst, sense_enable_gpios), \
.fault_reset_gpio = GPIO_DT_SPEC_INST_GET(inst, fault_reset_gpios), \
.io_channels = ADC_DT_SPEC_INST_GET(inst), \
.r_sense_ohms = DT_INST_PROP(inst, r_sense_ohms), \
.k_factor = DT_INST_PROP(inst, k_factor), \
.k_vcc = DT_INST_PROP(inst, k_vcc), \
.t_sense_0 = DT_INST_PROP(inst, t_sense_0), \
.v_sense_0 = DT_INST_PROP(inst, v_sense_0), \
.k_tchip = DT_INST_PROP(inst, k_tchip), \
}; \
\
DEVICE_DT_INST_DEFINE(inst, \
vnd7050aj_init, \
NULL, /* No PM support yet */ \
&vnd7050aj_data_##inst, \
&vnd7050aj_config_##inst, \
POST_KERNEL, \
CONFIG_VND7050AJ_INIT_PRIORITY, \
NULL); /* No API struct needed for custom API */
DT_INST_FOREACH_STATUS_OKAY(VND7050AJ_DEFINE)
int vnd7050aj_set_output_state(const struct device *dev, uint8_t channel, bool state)
{
const struct vnd7050aj_config *config = dev->config;
if (channel != VND7050AJ_CHANNEL_0 && channel != VND7050AJ_CHANNEL_1) {
return -EINVAL;
}
const struct gpio_dt_spec *gpio =
(channel == VND7050AJ_CHANNEL_0) ? &config->input0_gpio : &config->input1_gpio;
return gpio_pin_set_dt(gpio, (int)state);
}
static int vnd7050aj_read_sense_voltage(
const struct device *dev, enum vnd7050aj_diag_mode mode, int32_t *voltage_mv)
{
const struct vnd7050aj_config *config = dev->config;
struct vnd7050aj_data *data = dev->data;
int err = 0;
/* Initialize the buffer to zero */
*voltage_mv = 0;
struct adc_sequence sequence = {
.buffer = voltage_mv,
.buffer_size = sizeof(*voltage_mv),
#ifdef CONFIG_ADC_CALIBRATION
.calibrate = true,
#endif
};
adc_sequence_init_dt(&config->io_channels, &sequence);
k_mutex_lock(&data->lock, K_FOREVER);
/* Step 1: Select diagnostic mode */
switch (mode) {
case DIAG_CURRENT_CH0:
gpio_pin_set_dt(&config->sel0_gpio, 0);
gpio_pin_set_dt(&config->sel1_gpio, 0);
gpio_pin_set_dt(&config->sen_gpio, 1);
break;
case DIAG_CURRENT_CH1:
gpio_pin_set_dt(&config->sel0_gpio, 0);
gpio_pin_set_dt(&config->sel1_gpio, 1);
gpio_pin_set_dt(&config->sen_gpio, 1);
break;
case DIAG_TEMP:
gpio_pin_set_dt(&config->sel0_gpio, 1);
gpio_pin_set_dt(&config->sel1_gpio, 0);
gpio_pin_set_dt(&config->sen_gpio, 1);
break;
case DIAG_VCC:
gpio_pin_set_dt(&config->sel1_gpio, 1);
gpio_pin_set_dt(&config->sel0_gpio, 1);
gpio_pin_set_dt(&config->sen_gpio, 1);
break;
default:
err = -ENOTSUP;
goto cleanup;
}
/* Allow time for GPIO changes to settle and ADC input to stabilize */
k_msleep(1);
/* Initialize buffer before read */
*voltage_mv = 0;
err = adc_read_dt(&config->io_channels, &sequence);
if (err) {
LOG_ERR("ADC read failed: %d", err);
goto cleanup;
}
LOG_DBG("ADC read completed, raw value: %d", *voltage_mv);
err = adc_raw_to_millivolts_dt(&config->io_channels, voltage_mv);
if (err) {
LOG_ERR("ADC raw to millivolts conversion failed: %d", err);
goto cleanup;
}
LOG_DBG("ADC Reading (without processing) %dmV", *voltage_mv);
cleanup:
/* Deactivate sense enable to save power */
gpio_pin_set_dt(&config->sen_gpio, 0);
k_mutex_unlock(&data->lock);
return err;
}
int vnd7050aj_read_load_current(const struct device *dev, uint8_t channel, int32_t *current_ma)
{
const struct vnd7050aj_config *config = dev->config;
int32_t sense_mv;
int err;
if (channel != VND7050AJ_CHANNEL_0 && channel != VND7050AJ_CHANNEL_1) {
return -EINVAL;
}
enum vnd7050aj_diag_mode mode =
(channel == VND7050AJ_CHANNEL_0) ? DIAG_CURRENT_CH0 : DIAG_CURRENT_CH1;
err = vnd7050aj_read_sense_voltage(dev, mode, &sense_mv);
if (err) {
return err;
}
/* Formula according to datasheet: I_OUT = (V_SENSE / R_SENSE) * K_IL */
/* To avoid floating point, we calculate in microamps and then convert to milliamps */
int64_t current_ua = ((int64_t)sense_mv * 1000 * config->k_factor) / config->r_sense_ohms;
*current_ma = (int32_t)(current_ua / 1000);
return 0;
}
int vnd7050aj_read_chip_temp(const struct device *dev, int32_t *temp_c)
{
const struct vnd7050aj_config *config = dev->config;
int32_t sense_mv;
int err;
err = vnd7050aj_read_sense_voltage(dev, DIAG_TEMP, &sense_mv);
if (err) {
return err;
}
/* Calculate temperature difference in kelvin first to avoid overflow */
int32_t voltage_diff = sense_mv - (int32_t)config->v_sense_0;
int32_t temp_diff_kelvin = (voltage_diff * 1000) / (int32_t)config->k_tchip;
*temp_c = config->t_sense_0 + temp_diff_kelvin;
LOG_DBG("Voltage diff: %d mV, Temp diff: %d milli°C, Final temp: %d °C",
voltage_diff,
temp_diff_kelvin,
*temp_c);
return 0;
}
int vnd7050aj_read_supply_voltage(const struct device *dev, int32_t *voltage_mv)
{
const struct vnd7050aj_config *config = dev->config;
int32_t sense_mv;
int err;
err = vnd7050aj_read_sense_voltage(dev, DIAG_VCC, &sense_mv);
if (err) {
return err;
}
/* Formula from datasheet: VCC = V_SENSE * K_VCC */
*voltage_mv = (sense_mv * config->k_vcc) / 1000;
return 0;
}
int vnd7050aj_reset_fault(const struct device *dev)
{
const struct vnd7050aj_config *config = dev->config;
int err;
/* Pulse the active-low fault reset pin */
err = gpio_pin_set_dt(&config->fault_reset_gpio, 0);
if (err) {
return err;
}
k_msleep(1); /* Short pulse */
err = gpio_pin_set_dt(&config->fault_reset_gpio, 1);
return err;
}

View File

@@ -9,7 +9,6 @@ from pymodbus.client import ModbusSerialClient
from pymodbus.exceptions import ModbusException
# --- Register Definitions ---
# (omitted for brevity, no changes here)
REG_INPUT_VALVE_STATE_MOVEMENT = 0x0000
REG_INPUT_MOTOR_OPEN_CURRENT_MA = 0x0001
REG_INPUT_MOTOR_CLOSE_CURRENT_MA = 0x0002
@@ -27,6 +26,8 @@ REG_HOLDING_MAX_OPENING_TIME_S = 0x0001
REG_HOLDING_MAX_CLOSING_TIME_S = 0x0002
REG_HOLDING_END_CURRENT_THRESHOLD_OPEN_MA = 0x0003
REG_HOLDING_END_CURRENT_THRESHOLD_CLOSE_MA = 0x0004
REG_HOLDING_OBSTACLE_THRESHOLD_OPEN_MA = 0x0005
REG_HOLDING_OBSTACLE_THRESHOLD_CLOSE_MA = 0x0006
REG_HOLDING_DIGITAL_OUTPUTS_STATE = 0x0010
REG_HOLDING_WATCHDOG_TIMEOUT_S = 0x00F0
REG_HOLDING_DEVICE_RESET = 0x00F1
@@ -79,7 +80,9 @@ def poll_status(slave_id, interval):
# Attempt to connect
if client.connect():
reconnect_attempts = 0
new_data["error"] = None # Clear error on successful reconnect
with status_lock:
status_data["error"] = None # Clear error in status_data immediately
time.sleep(0.1) # Allow UI to refresh with cleared error
else:
new_data["error"] = f"Connection lost. Attempting to reconnect ({reconnect_attempts}/{max_reconnect_attempts})..."
time.sleep(reconnect_delay)
@@ -90,7 +93,7 @@ def poll_status(slave_id, interval):
ir_current = client.read_input_registers(REG_INPUT_MOTOR_OPEN_CURRENT_MA, 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=6, slave=slave_id)
hr_valve = client.read_holding_registers(REG_HOLDING_MAX_OPENING_TIME_S, count=4, slave=slave_id)
hr_valve = client.read_holding_registers(REG_HOLDING_MAX_OPENING_TIME_S, count=6, 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,6 +112,8 @@ def poll_status(slave_id, interval):
new_data["close_time"] = f"{hr_valve.registers[1]}s"
new_data["end_curr_open"] = f"{hr_valve.registers[2]}mA"
new_data["end_curr_close"] = f"{hr_valve.registers[3]}mA"
new_data["obstacle_open"] = f"{hr_valve.registers[4]}mA"
new_data["obstacle_close"] = f"{hr_valve.registers[5]}mA"
new_data["digital_inputs"] = f"0x{ir_dig.registers[0]:04X}"
new_data["button_events"] = f"0x{ir_dig.registers[1]:04X}"
new_data["digital_outputs"] = f"0x{hr_dig.registers[0]:04X}"
@@ -189,7 +194,7 @@ def file_browser(stdscr):
selected_index = 0
while True:
stdscr.clear()
stdscr.erase()
h, w = stdscr.getmaxyx()
stdscr.addstr(0, 0, f"Select Firmware File: {path}".ljust(w-1), curses.color_pair(2))
@@ -233,8 +238,8 @@ def main_menu(stdscr, slave_id):
curses.start_color(); curses.init_pair(1, curses.COLOR_WHITE, curses.COLOR_BLUE); curses.init_pair(2, curses.COLOR_BLUE, curses.COLOR_WHITE); curses.init_pair(3, curses.COLOR_RED, curses.COLOR_BLUE)
stdscr.bkgd(' ', curses.color_pair(1))
menu = ["Open Valve", "Close Valve", "Stop Valve", "Toggle Output 1", "Toggle Output 2", "Settings", "Reset Node", "Firmware Update", "Exit"]
settings_menu = ["Set Max Open Time", "Set Max Close Time", "Set End Current Open", "Set End Current Close", "Set Watchdog", "Back"]
menu = ["Open Valve", "Close Valve", "Stop Valve", "Settings", "Reset Node", "Firmware Update", "Exit"]
settings_menu = ["Set Max Open Time", "Set Max Close Time", "Set End Current Open", "Set End Current Close", "Set Obstacle Current Open", "Set Obstacle Current Close", "Set Watchdog", "Back"]
current_menu = menu
current_row_idx = 0
message, message_time = "", 0
@@ -270,13 +275,6 @@ def main_menu(stdscr, slave_id):
elif selected_option == "Open Valve": client.write_register(REG_HOLDING_VALVE_COMMAND, 1, slave=slave_id); message = "-> Sent OPEN command"
elif selected_option == "Close Valve": client.write_register(REG_HOLDING_VALVE_COMMAND, 2, slave=slave_id); message = "-> Sent CLOSE command"
elif selected_option == "Stop Valve": client.write_register(REG_HOLDING_VALVE_COMMAND, 0, slave=slave_id); message = "-> Sent STOP command"
elif "Toggle Output" in selected_option:
bit = 0 if "1" in selected_option else 1
try:
current_val = client.read_holding_registers(REG_HOLDING_DIGITAL_OUTPUTS_STATE, count=1, slave=slave_id).registers[0]
client.write_register(REG_HOLDING_DIGITAL_OUTPUTS_STATE, current_val ^ (1 << bit), slave=slave_id)
message = f"-> Toggled Output {bit+1}"
except Exception as e: message = f"-> Error: {e}"
elif selected_option == "Set Max Open Time":
input_mode, input_prompt, input_target_reg = True, "Enter Max Open Time (s): ", REG_HOLDING_MAX_OPENING_TIME_S
elif selected_option == "Set Max Close Time":
@@ -287,6 +285,10 @@ def main_menu(stdscr, slave_id):
input_mode, input_prompt, input_target_reg = True, "Enter End Current Threshold Close (mA): ", REG_HOLDING_END_CURRENT_THRESHOLD_CLOSE_MA
elif selected_option == "Set Watchdog":
input_mode, input_prompt, input_target_reg = True, "Enter Watchdog Timeout (s): ", REG_HOLDING_WATCHDOG_TIMEOUT_S
elif selected_option == "Set Obstacle Current Open":
input_mode, input_prompt, input_target_reg = True, "Enter Obstacle Threshold Open (mA): ", REG_HOLDING_OBSTACLE_THRESHOLD_OPEN_MA
elif selected_option == "Set Obstacle Current Close":
input_mode, input_prompt, input_target_reg = True, "Enter Obstacle Threshold Close (mA): ", REG_HOLDING_OBSTACLE_THRESHOLD_CLOSE_MA
elif selected_option == "Reset Node":
try:
client.write_register(REG_HOLDING_DEVICE_RESET, 1, slave=slave_id)
@@ -300,7 +302,7 @@ def main_menu(stdscr, slave_id):
else:
message = "-> Firmware update cancelled."
stdscr.clear()
stdscr.erase()
if is_updating:
with update_lock: prog, msg = update_status["progress"], update_status["message"]
stdscr.addstr(h // 2 - 1, w // 2 - 25, "FIRMWARE UPDATE IN PROGRESS", curses.A_BOLD | curses.color_pair(2))
@@ -324,18 +326,20 @@ def main_menu(stdscr, slave_id):
stdscr.addstr(3, col3, "Watchdog:", bold); stdscr.addstr(3, col3 + 16, str(current_data.get('watchdog', 'N/A')), normal)
stdscr.addstr(4, col3, "End Curr Open:", bold); stdscr.addstr(4, col3 + 16, str(current_data.get('end_curr_open', 'N/A')), normal)
stdscr.addstr(5, col3, "End Curr Close:", bold); stdscr.addstr(5, col3 + 16, str(current_data.get('end_curr_close', 'N/A')), normal)
stdscr.addstr(6, col3, "Obstacle Open:", bold); stdscr.addstr(6, col3 + 16, str(current_data.get('obstacle_open', 'N/A')), normal)
stdscr.addstr(7, col3, "Obstacle Close:", bold); stdscr.addstr(7, col3 + 16, str(current_data.get('obstacle_close', 'N/A')), normal)
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(6, 0, "" * (w - 1), normal)
stdscr.addstr(8, 0, "" * (w - 1), normal)
for idx, row in enumerate(current_menu):
draw_button(stdscr, h // 2 - len(current_menu) + (idx * 2), w // 2 - len(row) // 2, row, idx == current_row_idx)
draw_button(stdscr, 9 + (idx * 2), w // 2 - len(row) // 2, row, idx == current_row_idx)
if time.time() - message_time < 2.0: stdscr.addstr(h - 2, 0, message.ljust(w - 1), curses.color_pair(1) | curses.A_BOLD)
if input_mode:
curses.curs_set(1); stdscr.addstr(h - 2, 0, (input_prompt + input_str).ljust(w-1), curses.color_pair(2)); stdscr.move(h - 2, len(input_prompt) + len(input_str))
else: curses.curs_set(0)
stdscr.refresh()
curses.doupdate()
def main():
global client

View File

@@ -0,0 +1,216 @@
import argparse
import time
import logging
import threading
import serial
import asyncio
from pymodbus.server import StartSerialServer
from pymodbus.datastore import ModbusSequentialDataBlock, ModbusSlaveContext, ModbusServerContext
from pymodbus.framer.rtu import FramerRTU
# --- Configuration ---
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s', handlers=[logging.FileHandler('communication_debug.log', 'w')])
log = logging.getLogger()
logging.getLogger("pymodbus").setLevel(logging.DEBUG)
# --- Constants from Documentation ---
# Input Registers (3xxxx)
REG_INPUT_VALVE_STATE_MOVEMENT = 0x0000
REG_INPUT_MOTOR_OPEN_CURRENT_MA = 0x0001
REG_INPUT_MOTOR_CLOSE_CURRENT_MA = 0x0002
REG_INPUT_SUPPLY_VOLTAGE_MV = 0x00F5
# Holding Registers (4xxxx)
REG_HOLDING_VALVE_COMMAND = 0x0000
# --- Simulation Parameters ---
SUPPLY_VOLTAGE = 12345 # mV
MOTOR_CURRENT_IDLE = 0 # mA
MOTOR_CURRENT_MOVING = 80 # mA
VALVE_TRAVEL_TIME = 4.5 # seconds
# --- Valve Logic ---
class ValveController:
"""Holds the state and logic for the simulated valve based on documentation."""
def __init__(self, node_id):
self.node_id = node_id
self.lock = threading.Lock()
# Internal State
self.movement = 0 # 0=Idle, 1=Öffnet, 2=Schliesst
self.state = 0 # 0=Geschlossen, 1=Geöffnet
self.is_moving = False
self.movement_start_time = 0
self.target_state = 0 # 0 for close, 1 for open
def start_movement(self, command):
"""Initiates a valve movement based on a holding register command."""
with self.lock:
if self.is_moving:
log.warning(f"[Node {self.node_id}] Valve is already moving. Ignoring command.")
return
# Command 1: Open
if command == 1 and self.state == 0:
log.info(f"[Node {self.node_id}] Received command to OPEN valve.")
self.movement = 1 # Öffnet
self.target_state = 1 # Geöffnet
# Command 2: Close
elif command == 2 and self.state == 1:
log.info(f"[Node {self.node_id}] Received command to CLOSE valve.")
self.movement = 2 # Schliesst
self.target_state = 0 # Geschlossen
# Command 0: Stop
elif command == 0:
if self.is_moving:
log.info(f"[Node {self.node_id}] Received command to STOP valve.")
self.is_moving = False
self.movement = 0 # Idle
return
else:
log.info(f"[Node {self.node_id}] Valve is already in the requested state or command is invalid.")
return
self.is_moving = True
self.movement_start_time = time.time()
def update_state(self):
"""Updates the valve's position and state if it's moving."""
with self.lock:
if not self.is_moving:
return
elapsed_time = time.time() - self.movement_start_time
if elapsed_time >= VALVE_TRAVEL_TIME:
# Movement is complete
self.is_moving = False
self.state = self.target_state
self.movement = 0 # Idle
log.info(f"[Node {self.node_id}] Valve movement finished. State: {'Open' if self.state == 1 else 'Closed'}")
# --- Modbus Datastore Blocks ---
class CustomDataBlock(ModbusSequentialDataBlock):
def __init__(self, controller):
self.controller = controller
# Initialize registers to a safe default size, they will be dynamically updated.
super().__init__(0, [0] * 256)
def setValues(self, address, values):
# Handle writes to the VALVE_COMMAND register
if address == REG_HOLDING_VALVE_COMMAND:
if values:
self.controller.start_movement(values[0])
else:
log.info(f"[Node {self.controller.node_id}] Write to unhandled holding register 0x{address:04X} with value(s): {values}")
super().setValues(address, values)
def getValues(self, address, count=1):
self.controller.update_state() # Update valve state before returning values
log.debug(f"getValues: requested address={address}")
# Handle specific input registers
if (address - 1) == REG_INPUT_VALVE_STATE_MOVEMENT:
valve_state_movement = (self.controller.movement << 8) | self.controller.state
return [valve_state_movement]
elif (address - 1) == REG_INPUT_MOTOR_OPEN_CURRENT_MA:
motor_current = MOTOR_CURRENT_MOVING if self.controller.movement == 1 else MOTOR_CURRENT_IDLE
return [motor_current]
elif (address - 1) == REG_INPUT_MOTOR_CLOSE_CURRENT_MA:
motor_current = MOTOR_CURRENT_MOVING if self.controller.movement == 2 else MOTOR_CURRENT_IDLE
return [motor_current]
elif (address - 1) == REG_INPUT_SUPPLY_VOLTAGE_MV:
return [SUPPLY_VOLTAGE]
else:
# For any other register, return 0xbeaf
return [0xbeaf] * count
# --- Main Server ---
async def run_server(port, node_id, baudrate):
"""Sets up and runs the Modbus RTU server."""
controller = ValveController(node_id)
datablock = CustomDataBlock(controller)
store = ModbusSlaveContext(
di=datablock, # Input Registers
co=None, # Coils (not used)
hr=datablock, # Holding Registers
ir=datablock, # Re-using for simplicity, maps to the same logic
)
context = ModbusServerContext(slaves={node_id: store}, single=False)
log.info(f"Starting Modbus RTU Valve Simulator on {port}")
log.info(f"Node ID: {node_id}, Baudrate: {baudrate}")
log.info("--- Register Map ---")
log.info("Input Registers (Read-Only):")
log.info(f" 0x{REG_INPUT_VALVE_STATE_MOVEMENT:04X}: VALVE_STATE_MOVEMENT")
log.info(f" 0x{REG_INPUT_MOTOR_OPEN_CURRENT_MA:04X}: MOTOR_OPEN_CURRENT_MA")
log.info(f" 0x{REG_INPUT_MOTOR_CLOSE_CURRENT_MA:04X}: MOTOR_CLOSE_CURRENT_MA")
log.info(f" 0x{REG_INPUT_SUPPLY_VOLTAGE_MV:04X}: SUPPLY_VOLTAGE_MV")
log.info("Holding Registers (Read/Write):")
log.info(f" 0x{REG_HOLDING_VALVE_COMMAND:04X}: VALVE_COMMAND (1=Open, 2=Close, 0=Stop)")
log.info("Server listening.")
while True:
try:
data = ser.read(ser.in_waiting or 1) # Read available data or wait for 1 byte
if data:
# Process the request
request = framer.processIncomingPacket(data)
if request:
log_line = "<-- Reg: "
reg_addr_hex = f"0x{request.address:04X}" if hasattr(request, 'address') else "N/A"
rw_indicator = ""
reg_type_indicator = ""
data_info = ""
if request.function_code in [0x01, 0x02, 0x03, 0x04]: # Read operations
rw_indicator = "r"
if request.function_code == 0x03: reg_type_indicator = "Hold."
elif request.function_code == 0x04: reg_type_indicator = "Input"
log_line += f"{reg_addr_hex}{rw_indicator}"
elif request.function_code in [0x05, 0x06, 0x0F, 0x10]: # Write operations
rw_indicator = "w"
if request.function_code == 0x06 or request.function_code == 0x10: reg_type_indicator = "Hold."
elif request.function_code == 0x05 or request.function_code == 0x0F: reg_type_indicator = "Coil"
if hasattr(request, 'value') and request.value is not None: # For single write (0x05, 0x06)
data_info = f" Data: 0x{request.value:04X}"
elif hasattr(request, 'values') and request.values is not None: # For multiple write (0x0F, 0x10)
data_info = " Data: 0x" + "".join([f"{val:04X}" for val in request.values])
elif hasattr(request, 'bits') and request.bits is not None: # For multiple coil write (0x0F)
data_info = " Data: 0x" + "".join([f"{int(bit):X}" for bit in request.bits])
log_line += f"{reg_addr_hex}{rw_indicator} Type: {reg_type_indicator}{data_info}"
else:
log_line = f"<-- Func: 0x{request.function_code:02X} Raw: {data.hex()}"
print(log_line)
sys.stdout.flush()
response = request.execute(context)
if response:
pdu = framer.buildPacket(response)
ser.write(pdu)
response_reg_addr = f"0x{request.address:04X}" if hasattr(request, 'address') else "N/A"
response_data_hex = ""
response_data_dec = ""
if hasattr(response, 'registers') and response.registers is not None:
response_data_hex = "".join([f"{val:04X}" for val in response.registers])
response_data_dec = ", ".join([str(val) for val in response.registers])
elif hasattr(response, 'bits') and response.bits is not None:
response_data_hex = "".join([f"{int(bit):X}" for bit in response.bits])
response_data_dec = ", ".join([str(int(bit)) for bit in response.bits])
elif hasattr(response, 'value') and response.value is not None: # For single write response
response_data_hex = f"{response.value:04X}"
response_data_dec = str(response.value)
print(f"--> Reg: {response_reg_addr} Data: 0x{response_data_hex} (Dec: {response_data_dec})")
sys.stdout.flush()
except Exception as e:
print(f"Error during serial communication: {e}")
sys.stderr.flush()
await asyncio.sleep(0.001) # Small delay to prevent busy-waiting

View File

@@ -0,0 +1,2 @@
pymodbus
pyserial

View File

@@ -0,0 +1,58 @@
#!/bin/bash
#
# This script creates a pair of virtual serial ports (pseudo-terminals)
# that are linked together, allowing two applications to communicate
# as if they were connected by a physical null-modem cable.
#
# It uses `socat`, a powerful command-line utility for data transfer.
# --- Check for socat ---
if ! command -v socat &> /dev/null
then
echo "Error: 'socat' is not installed. It is required to create virtual serial ports."
echo "Please install it using your package manager."
echo "For Debian/Ubuntu: sudo apt-get update && sudo apt-get install socat"
echo "For Fedora: sudo dnf install socat"
exit 1
fi
# --- Configuration ---
# The script will create symlinks to the virtual ports in the current directory
# for easy access.
PORT1="./vcom_a"
PORT2="./vcom_b"
# --- Cleanup function ---
# This function will be called when the script exits to remove the symlinks.
trap 'cleanup' EXIT
cleanup() {
echo -e "\nCleaning up..."
rm -f "$PORT1" "$PORT2"
echo "Removed symlinks '$PORT1' and '$PORT2'."
}
# --- Main Execution ---
echo "=================================================="
echo " Virtual Serial Port Pair Setup"
echo "=================================================="
echo
echo "Creating a linked pair of virtual serial ports."
echo " - Port A will be available at: $PORT1"
echo " - Port B will be available at: $PORT2"
echo
echo "You can now connect the simulator to one port and your client script to the other."
echo "Example:"
echo " Terminal 1: python modbus_valve_simulator.py $PORT1"
echo " Terminal 2: python your_client_script.py $PORT2"
echo
echo "Press [Ctrl+C] to shut down the virtual ports and exit."
echo "--------------------------------------------------"
# The core command.
# -d -d: Increases verbosity to show data transfer.
# pty: Creates a pseudo-terminal (virtual port).
# raw,echo=0: Puts the terminal in raw mode, suitable for serial data.
# link=<path>: Creates a symbolic link to the PTY device for a stable name.
socat -d -d pty,raw,echo=0,link="$PORT1" pty,raw,echo=0,link="$PORT2"

View File

@@ -0,0 +1 @@
pyserial-asyncio>=0.6

View File

@@ -0,0 +1,163 @@
import asyncio
import argparse
import serial
import serial_asyncio
from datetime import datetime
# Globale Variablen für den seriellen Reader und Writer
serial_reader = None
serial_writer = None
def log_message(message: str):
"""Gibt eine formatierte Log-Nachricht mit Zeitstempel aus."""
print(f"[{datetime.now().strftime('%Y-%m-%d %H:%M:%S')}] {message}")
def log_verbose_request(req: bytes):
"""Gibt eine detaillierte, formatierte Log-Nachricht für eine Anfrage aus."""
if len(req) < 6: return
log_message(f"VERBOSE: --> REQ from HA: {req.hex(' ')}")
slave_id = req[0]
func_code = req[1]
addr = int.from_bytes(req[2:4], 'big')
# Für Lese-/Schreibbefehle
if func_code in [1, 2, 3, 4, 5, 6, 15, 16] and len(req) >= 6:
count_or_data = int.from_bytes(req[4:6], 'big')
log_message(f"VERBOSE: Parsed: slave={slave_id}, func={func_code}, addr={addr}, count/val={count_or_data}")
else:
log_message(f"VERBOSE: Parsed: slave={slave_id}, func={func_code}")
def log_verbose_response(res: bytes):
"""Gibt eine detaillierte, formatierte Log-Nachricht für eine Antwort aus."""
if len(res) < 5: return
log_message(f"VERBOSE: <-- RES from DEV: {res.hex(' ')}")
slave_id = res[0]
func_code = res[1]
if func_code < 0x80: # Keine Fehler-Antwort
byte_count = res[2]
data = res[3:-2].hex(' ')
log_message(f"VERBOSE: Parsed: slave={slave_id}, func={func_code}, bytes={byte_count}, data=[{data}]")
else: # Fehler-Antwort
error_code = res[2]
log_message(f"VERBOSE: ERROR: slave={slave_id}, func={func_code}, err_code={error_code}")
async def handle_tcp_client(reader: asyncio.StreamReader, writer: asyncio.StreamWriter, verbose: bool):
"""Bearbeitet eine einzelne TCP-Client-Verbindung."""
global serial_reader, serial_writer
peername = writer.get_extra_info('peername')
log_message(f"✅ Client verbunden: {peername}")
if not serial_writer or not serial_reader:
log_message("❌ Fehler: Serielle Verbindung ist nicht aktiv. Client wird getrennt.")
writer.close(); await writer.wait_closed()
return
try:
while True:
tcp_request = await reader.read(256)
if not tcp_request: break
if verbose: log_verbose_request(tcp_request)
# *** HINZUGEFÜGTE ÄNDERUNG: Eine kleine Pause vor dem Senden ***
# Dies gibt empfindlichen Geräten oder langsamen Bussen Zeit.
await asyncio.sleep(0.05) # 50ms Verzögerung
serial_writer.write(tcp_request)
await serial_writer.drain()
try:
serial_response = await asyncio.wait_for(serial_reader.read(256), timeout=2.0)
if verbose: log_verbose_response(serial_response)
writer.write(serial_response)
await writer.drain()
except asyncio.TimeoutError:
log_message("VERBOSE: <-- Timeout from DEV")
except asyncio.CancelledError:
log_message("TCP-Handler wurde abgebrochen.")
except Exception as e:
log_message(f"TCP-Verbindungsfehler: {e}")
finally:
log_message(f"🔌 Client getrennt: {peername}")
writer.close(); await writer.wait_closed()
async def serial_reconnector(comport, baudrate):
"""Versucht, die serielle Verbindung wiederherzustellen."""
global serial_reader, serial_writer
for attempt in range(1, 6):
log_message(f"🚨 Serielle Verbindung verloren! Versuch {attempt}/5 in 5 Sekunden...")
await asyncio.sleep(5)
try:
reader_obj, writer_obj = await serial_asyncio.open_serial_connection(
url=comport, baudrate=baudrate, rtscts=False, dsrdtr=False
)
serial_reader, serial_writer = reader_obj, writer_obj
log_message(f"✅ Serielle Verbindung zu {comport} wiederhergestellt.")
return True
except (serial.SerialException, FileNotFoundError) as e:
log_message(f"❌ Wiederverbindung fehlgeschlagen: {e}")
log_message("💥 Konnte serielle Verbindung nach 5 Versuchen nicht wiederherstellen. Programm wird beendet.")
try:
asyncio.get_running_loop().stop()
except RuntimeError: pass
return False
async def main(args):
"""Hauptfunktion zum Starten des Servers und der seriellen Verbindung."""
global serial_reader, serial_writer
log_message("--- Modbus RTU zu TCP Gateway ---")
log_message(f"Serieller Port: {args.comport}")
log_message(f"Baudrate: {args.baudrate}")
log_message(f"TCP Port: {args.tcpport}")
log_message(f"Verbose Modus: {'Aktiv' if args.verbose else 'Inaktiv'}")
log_message("---------------------------------")
try:
serial_reader, serial_writer = await serial_asyncio.open_serial_connection(
url=args.comport, baudrate=args.baudrate, rtscts=False, dsrdtr=False
)
log_message(f"✅ Serielle Verbindung zu {args.comport} erfolgreich hergestellt.")
except (serial.SerialException, FileNotFoundError) as e:
log_message(f"❌ Kritischer Fehler bei der initialen Verbindung: {e}")
if not await serial_reconnector(args.comport, args.baudrate): return
server = await asyncio.start_server(
lambda r, w: handle_tcp_client(r, w, args.verbose), '0.0.0.0', args.tcpport
)
addr = server.sockets[0].getsockname()
log_message(f"👂 Server lauscht auf {addr}")
async with server:
while True:
try:
if serial_reader and hasattr(serial_reader, '_transport') and serial_reader._transport:
_ = serial_reader._transport.serial.cts
else:
raise serial.SerialException("Transport nicht verfügbar.")
except (serial.SerialException, AttributeError, BrokenPipeError, TypeError):
log_message("Serielle Verbindung unterbrochen. Starte Wiederverbindungs-Logik...")
if not await serial_reconnector(args.comport, args.baudrate):
server.close(); break
await asyncio.sleep(2)
if __name__ == "__main__":
parser = argparse.ArgumentParser(description="Modbus RTU zu TCP Gateway für Home Assistant.")
parser.add_argument("comport", help="Der serielle Port (z.B. /dev/ttyUSB0 oder COM3)")
parser.add_argument("-b", "--baudrate", type=int, default=19200, help="Baudrate der seriellen Verbindung (Standard: 19200)")
parser.add_argument("-p", "--tcpport", type=int, default=502, help="TCP-Port, auf dem der Server lauscht (Standard: 502)")
parser.add_argument("-v", "--verbose", action="store_true", help="Verbose-Modus aktivieren für detaillierte Logs")
args = parser.parse_args()
try:
asyncio.run(main(args))
except KeyboardInterrupt:
log_message("\n👋 Programm wird durch Benutzer beendet.")
except Exception as e:
log_message(f"💥 Unerwarteter Fehler im Hauptprogramm: {e}")