14 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
65 changed files with 2215 additions and 42 deletions

64
.gitignore vendored
View File

@@ -1 +1,65 @@
**/build **/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

@@ -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) cmake_minimum_required(VERSION 3.20.5)
# Include the main 'software' directory as a module to find boards, libs, etc.
list(APPEND ZEPHYR_EXTRA_MODULES ${CMAKE_CURRENT_SOURCE_DIR}/../..)
find_package(Zephyr REQUIRED HINTS $ENV{ZEPHYR_BASE}) find_package(Zephyr REQUIRED HINTS $ENV{ZEPHYR_BASE})
project(gateway)
project(gateway)
target_sources(app PRIVATE src/main.c) 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 @@
/* #include <zephyr/fs/fs.h>
* Copyright (c) 2025 Eduard Iten #include <zephyr/fs/littlefs.h>
*
* SPDX-License-Identifier: Apache-2.0
*/
#include <zephyr/kernel.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) 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; 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}) find_package(Zephyr REQUIRED HINTS $ENV{ZEPHYR_BASE})
project(slave_node LANGUAGES C) project(slave_node LANGUAGES C)
zephyr_include_directories(../../include) zephyr_include_directories(../../include)
add_subdirectory(../../lib lib) add_subdirectory(../../lib lib)
target_sources(app PRIVATE src/main.c) 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,12 +7,12 @@
compatible = "st,vnd7050aj"; compatible = "st,vnd7050aj";
status = "okay"; status = "okay";
input0-gpios = <&gpiob 7 GPIO_ACTIVE_HIGH>; input0-gpios = <&gpiob 3 GPIO_ACTIVE_HIGH>;
input1-gpios = <&gpiob 9 GPIO_ACTIVE_HIGH>; input1-gpios = <&gpiob 4 GPIO_ACTIVE_HIGH>;
select0-gpios = <&gpiob 5 GPIO_ACTIVE_HIGH>; select0-gpios = <&gpiob 7 GPIO_ACTIVE_HIGH>;
select1-gpios = <&gpiob 6 GPIO_ACTIVE_HIGH>; select1-gpios = <&gpiob 9 GPIO_ACTIVE_HIGH>;
sense-enable-gpios = <&gpiob 4 GPIO_ACTIVE_HIGH>; sense-enable-gpios = <&gpiob 6 GPIO_ACTIVE_HIGH>;
fault-reset-gpios = <&gpiob 3 GPIO_ACTIVE_LOW>; fault-reset-gpios = <&gpiob 5 GPIO_ACTIVE_LOW>;
io-channels = <&adc1 1>; io-channels = <&adc1 1>;
r-sense-ohms = <1500>; r-sense-ohms = <1500>;
k-vcc = <4000>; 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,7 +22,7 @@ CONFIG_SETTINGS_LOG_LEVEL_DBG=y
CONFIG_UART_INTERRUPT_DRIVEN=y CONFIG_UART_INTERRUPT_DRIVEN=y
CONFIG_MODBUS=y CONFIG_MODBUS=y
CONFIG_MODBUS_ROLE_SERVER=y CONFIG_MODBUS_ROLE_SERVER=y
CONFIG_MODBUS_BUFFER_SIZE=256 CONFIG_MODBUS_LOG_LEVEL_DBG=y
# Enable VND7050AJ # Enable VND7050AJ
CONFIG_VND7050AJ=y CONFIG_VND7050AJ=y

View File

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

View File

@@ -191,4 +191,24 @@ uint16_t valve_get_obstacle_threshold_open(void);
*/ */
uint16_t valve_get_obstacle_threshold_close(void); 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 #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_SYSTEM shell_system)
add_subdirectory_ifdef(CONFIG_SHELL_MODBUS shell_modbus) add_subdirectory_ifdef(CONFIG_SHELL_MODBUS shell_modbus)
add_subdirectory_ifdef(CONFIG_SHELL_VALVE shell_valve) 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_system/Kconfig"
rsource "shell_modbus/Kconfig" rsource "shell_modbus/Kconfig"
rsource "shell_valve/Kconfig" rsource "shell_valve/Kconfig"
rsource "vnd7050aj/Kconfig"
endmenu endmenu

View File

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

View File

@@ -8,7 +8,6 @@
*/ */
#include <zephyr/device.h> #include <zephyr/device.h>
#include <zephyr/drivers/misc/vnd7050aj/vnd7050aj.h>
#include <zephyr/drivers/uart.h> #include <zephyr/drivers/uart.h>
#include <zephyr/kernel.h> #include <zephyr/kernel.h>
#include <zephyr/logging/log.h> #include <zephyr/logging/log.h>
@@ -20,6 +19,7 @@
#include <lib/fwu.h> #include <lib/fwu.h>
#include <lib/modbus_server.h> #include <lib/modbus_server.h>
#include <lib/valve.h> #include <lib/valve.h>
#include <lib/vnd7050aj.h>
LOG_MODULE_REGISTER(modbus_server, LOG_LEVEL_INF); LOG_MODULE_REGISTER(modbus_server, LOG_LEVEL_INF);

View File

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

View File

@@ -111,26 +111,75 @@ static int cmd_valve_show(const struct shell *sh, size_t argc, char **argv)
return 0; 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_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(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(close_t, NULL, "Set max close time (seconds)", cmd_valve_set_close_t),
SHELL_CMD(set_end_curr_open, SHELL_CMD(end_curr_open,
NULL, NULL,
"Set end current threshold for opening (mA)", "Set end current threshold for opening (mA)",
cmd_valve_set_end_curr_open), cmd_valve_set_end_curr_open),
SHELL_CMD(set_end_curr_close, SHELL_CMD(end_curr_close,
NULL, NULL,
"Set end current threshold for closing (mA)", "Set end current threshold for closing (mA)",
cmd_valve_set_end_curr_close), cmd_valve_set_end_curr_close),
SHELL_CMD(set_obstacle_open, SHELL_CMD(obstacle_curr_open,
NULL, NULL,
"Set obstacle threshold for opening (mA)", "Set obstacle threshold for opening (mA)",
cmd_valve_set_obstacle_open), cmd_valve_set_obstacle_open),
SHELL_CMD(set_obstacle_close, SHELL_CMD(obstacle_curr_close,
NULL, NULL,
"Set obstacle threshold for closing (mA)", "Set obstacle threshold for closing (mA)",
cmd_valve_set_obstacle_close), cmd_valve_set_obstacle_close),
SHELL_CMD(show, NULL, "Show valve configuration", cmd_valve_show),
SHELL_SUBCMD_SET_END); 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 config LIB_VALVE
bool "Enable Valve Library" bool "Enable Valve Library"
default y default n
help help
Enable the Valve Library. Enable the Valve Library.

View File

@@ -9,11 +9,11 @@
#include <zephyr/device.h> #include <zephyr/device.h>
#include <zephyr/drivers/gpio.h> #include <zephyr/drivers/gpio.h>
#include <zephyr/drivers/misc/vnd7050aj/vnd7050aj.h>
#include <zephyr/kernel.h> #include <zephyr/kernel.h>
#include <zephyr/logging/log.h> #include <zephyr/logging/log.h>
#include <zephyr/settings/settings.h> #include <zephyr/settings/settings.h>
#include <lib/valve.h> #include <lib/valve.h>
#include <lib/vnd7050aj.h>
#define VND_NODE DT_ALIAS(vnd7050aj) #define VND_NODE DT_ALIAS(vnd7050aj)
#if !DT_NODE_HAS_STATUS(VND_NODE, okay) #if !DT_NODE_HAS_STATUS(VND_NODE, okay)
@@ -49,7 +49,7 @@ static void valve_work_handler(struct k_work *work)
if (current_movement == VALVE_MOVEMENT_OPENING) { if (current_movement == VALVE_MOVEMENT_OPENING) {
vnd7050aj_read_load_current(vnd7050aj_dev, VALVE_CHANNEL_OPEN, &current_ma); vnd7050aj_read_load_current(vnd7050aj_dev, VALVE_CHANNEL_OPEN, &current_ma);
LOG_DBG("Current load during opening: %d mA", current_ma); valve_current_open_callback(current_ma);
if (current_ma > obstacle_threshold_open_ma) { if (current_ma > obstacle_threshold_open_ma) {
LOG_ERR( LOG_ERR(
"Obstacle detected during opening (current: %d mA), stopping motor.", "Obstacle detected during opening (current: %d mA), stopping motor.",
@@ -64,7 +64,7 @@ static void valve_work_handler(struct k_work *work)
LOG_DBG("Valve finished opening"); LOG_DBG("Valve finished opening");
} else if (current_movement == VALVE_MOVEMENT_CLOSING) { } else if (current_movement == VALVE_MOVEMENT_CLOSING) {
vnd7050aj_read_load_current(vnd7050aj_dev, VALVE_CHANNEL_CLOSE, &current_ma); vnd7050aj_read_load_current(vnd7050aj_dev, VALVE_CHANNEL_CLOSE, &current_ma);
LOG_DBG("Current load during closing: %d mA", current_ma); valve_current_close_callback(current_ma);
if (current_ma > obstacle_threshold_close_ma) { if (current_ma > obstacle_threshold_close_ma) {
LOG_ERR( LOG_ERR(
"Obstacle detected during closing (current: %d mA), stopping motor.", "Obstacle detected during closing (current: %d mA), stopping motor.",
@@ -106,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_OPEN, false);
vnd7050aj_set_output_state(vnd7050aj_dev, VALVE_CHANNEL_CLOSE, 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) int valve_init(void)
@@ -291,3 +292,13 @@ uint16_t valve_get_obstacle_threshold_close(void)
{ {
return obstacle_threshold_close_ma; 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

@@ -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}")