From d76b897eb2586268ae119e697c533cfa0bc85ba8 Mon Sep 17 00:00:00 2001 From: Eduard Iten Date: Thu, 17 Jul 2025 15:18:22 +0200 Subject: [PATCH] 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 --- .gitmodules | 3 + .vscode/tasks.json | 14 + software/apps/bl_test/sysbuild/flash.dtsi | 0 software/apps/gateway/prj.conf | 43 ++- software/apps/gateway/src/main.c | 125 +++++++ .../apps/gateway/sysbuild/gateway.overlay | 4 - software/apps/slave_node/CMakeLists.txt | 2 + .../apps/slave_node/boards/native_sim.overlay | 47 +++ .../slave_node/dts/bindings/st,vnd7050aj.yaml | 88 +++++ software/apps/slave_node/prj.conf | 2 +- software/apps/slave_node/src/main.c | 8 +- software/apps/slave_node/sysbuild.conf | 5 + software/empty.bin | 0 software/include/lib/hw_shared.h | 0 software/include/lib/vnd7050aj.h | 74 ++++ software/lib/CMakeLists.txt | 3 +- software/lib/Kconfig | 1 + software/lib/modbus_server/modbus_server.c | 2 +- software/lib/shell_modbus/shell_modbus.c | 1 + software/lib/valve/valve.c | 2 +- software/lib/vnd7050aj/CMakeLists.txt | 1 + software/lib/vnd7050aj/Kconfig | 33 ++ software/lib/vnd7050aj/vnd7050aj.c | 333 ++++++++++++++++++ software/pristine.sh | 1 + software/pristine.sysbuild.sh | 3 + software/settings.bin | Bin 0 -> 43 bytes .../communication_debug.log | 0 .../modbus_valve_simulator.py | 216 ++++++++++++ .../modbus_valve_simulator/requirements.txt | 2 + .../setup_virtual_comports.sh | 58 +++ .../simulator_debug.log | 0 .../simulator_output.log | 0 32 files changed, 1048 insertions(+), 23 deletions(-) create mode 100644 .gitmodules create mode 100644 .vscode/tasks.json create mode 100644 software/apps/bl_test/sysbuild/flash.dtsi create mode 100644 software/apps/slave_node/boards/native_sim.overlay create mode 100644 software/apps/slave_node/dts/bindings/st,vnd7050aj.yaml create mode 100644 software/apps/slave_node/sysbuild.conf create mode 100644 software/empty.bin create mode 100644 software/include/lib/hw_shared.h create mode 100644 software/include/lib/vnd7050aj.h create mode 100644 software/lib/vnd7050aj/CMakeLists.txt create mode 100644 software/lib/vnd7050aj/Kconfig create mode 100644 software/lib/vnd7050aj/vnd7050aj.c create mode 100644 software/pristine.sh create mode 100644 software/pristine.sysbuild.sh create mode 100644 software/settings.bin create mode 100644 software/tools/modbus_valve_simulator/communication_debug.log create mode 100644 software/tools/modbus_valve_simulator/modbus_valve_simulator.py create mode 100644 software/tools/modbus_valve_simulator/requirements.txt create mode 100755 software/tools/modbus_valve_simulator/setup_virtual_comports.sh create mode 100644 software/tools/modbus_valve_simulator/simulator_debug.log create mode 100644 software/tools/modbus_valve_simulator/simulator_output.log diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..c285803 --- /dev/null +++ b/.gitmodules @@ -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 diff --git a/.vscode/tasks.json b/.vscode/tasks.json new file mode 100644 index 0000000..800a574 --- /dev/null +++ b/.vscode/tasks.json @@ -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" + ] + } + ] +} \ No newline at end of file diff --git a/software/apps/bl_test/sysbuild/flash.dtsi b/software/apps/bl_test/sysbuild/flash.dtsi new file mode 100644 index 0000000..e69de29 diff --git a/software/apps/gateway/prj.conf b/software/apps/gateway/prj.conf index ebebb31..d26247c 100644 --- a/software/apps/gateway/prj.conf +++ b/software/apps/gateway/prj.conf @@ -1,28 +1,47 @@ -# Enable logging +# ------------------- +# Logging and Console +# ------------------- CONFIG_LOG=y CONFIG_UART_CONSOLE=y -# Enable shell +# ------------- +# Zephyr Shell +# ------------- CONFIG_SHELL=y CONFIG_KERNEL_SHELL=y CONFIG_REBOOT=y -# Enable MCUMGR +# ------------------- +# MCUmgr OS Management +# ------------------- CONFIG_MCUMGR=y - -# Enable MCUMGR OS management group only CONFIG_MCUMGR_GRP_OS=y - -# Configure MCUMGR transport to UART 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 - -CONFIG_MCUMGR_GRP_IMG=y -CONFIG_IMG_MANAGER=y -CONFIG_MCUBOOT_IMG_MANAGER=y -CONFIG_STREAM_FLASH=y \ No newline at end of file diff --git a/software/apps/gateway/src/main.c b/software/apps/gateway/src/main.c index 875d168..b27bee9 100644 --- a/software/apps/gateway/src/main.c +++ b/software/apps/gateway/src/main.c @@ -1,11 +1,136 @@ +#include +#include #include #include +#include +#include #include +#include 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 "); + 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 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; } \ No newline at end of file diff --git a/software/apps/gateway/sysbuild/gateway.overlay b/software/apps/gateway/sysbuild/gateway.overlay index 60bc405..a79806e 100644 --- a/software/apps/gateway/sysbuild/gateway.overlay +++ b/software/apps/gateway/sysbuild/gateway.overlay @@ -6,7 +6,3 @@ zephyr,code-partition = &slot0_partition; }; }; - -&usb_serial { - status = "okay"; -}; \ No newline at end of file diff --git a/software/apps/slave_node/CMakeLists.txt b/software/apps/slave_node/CMakeLists.txt index 8771a0f..34ec969 100644 --- a/software/apps/slave_node/CMakeLists.txt +++ b/software/apps/slave_node/CMakeLists.txt @@ -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) diff --git a/software/apps/slave_node/boards/native_sim.overlay b/software/apps/slave_node/boards/native_sim.overlay new file mode 100644 index 0000000..6e3de37 --- /dev/null +++ b/software/apps/slave_node/boards/native_sim.overlay @@ -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 = ; + zephyr,resolution = <12>; + }; +}; diff --git a/software/apps/slave_node/dts/bindings/st,vnd7050aj.yaml b/software/apps/slave_node/dts/bindings/st,vnd7050aj.yaml new file mode 100644 index 0000000..a356d9f --- /dev/null +++ b/software/apps/slave_node/dts/bindings/st,vnd7050aj.yaml @@ -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. + diff --git a/software/apps/slave_node/prj.conf b/software/apps/slave_node/prj.conf index 20ab1e4..0e6a2e0 100644 --- a/software/apps/slave_node/prj.conf +++ b/software/apps/slave_node/prj.conf @@ -22,7 +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 diff --git a/software/apps/slave_node/src/main.c b/software/apps/slave_node/src/main.c index fdeb2ee..0ba8b6f 100644 --- a/software/apps/slave_node/src/main.c +++ b/software/apps/slave_node/src/main.c @@ -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"); diff --git a/software/apps/slave_node/sysbuild.conf b/software/apps/slave_node/sysbuild.conf new file mode 100644 index 0000000..e9174e8 --- /dev/null +++ b/software/apps/slave_node/sysbuild.conf @@ -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 \ No newline at end of file diff --git a/software/empty.bin b/software/empty.bin new file mode 100644 index 0000000..e69de29 diff --git a/software/include/lib/hw_shared.h b/software/include/lib/hw_shared.h new file mode 100644 index 0000000..e69de29 diff --git a/software/include/lib/vnd7050aj.h b/software/include/lib/vnd7050aj.h new file mode 100644 index 0000000..f253d93 --- /dev/null +++ b/software/include/lib/vnd7050aj.h @@ -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 +#include + +#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_ */ diff --git a/software/lib/CMakeLists.txt b/software/lib/CMakeLists.txt index 1750124..9292528 100644 --- a/software/lib/CMakeLists.txt +++ b/software/lib/CMakeLists.txt @@ -3,4 +3,5 @@ add_subdirectory_ifdef(CONFIG_LIB_MODBUS_SERVER modbus_server) 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) \ No newline at end of file +add_subdirectory_ifdef(CONFIG_SHELL_VALVE shell_valve) +add_subdirectory_ifdef(CONFIG_VND7050AJ vnd7050aj) \ No newline at end of file diff --git a/software/lib/Kconfig b/software/lib/Kconfig index 71ce8c8..6a7b715 100644 --- a/software/lib/Kconfig +++ b/software/lib/Kconfig @@ -6,4 +6,5 @@ rsource "valve/Kconfig" rsource "shell_system/Kconfig" rsource "shell_modbus/Kconfig" rsource "shell_valve/Kconfig" +rsource "vnd7050aj/Kconfig" endmenu \ No newline at end of file diff --git a/software/lib/modbus_server/modbus_server.c b/software/lib/modbus_server/modbus_server.c index b1489dd..541d954 100644 --- a/software/lib/modbus_server/modbus_server.c +++ b/software/lib/modbus_server/modbus_server.c @@ -8,7 +8,6 @@ */ #include -#include #include #include #include @@ -20,6 +19,7 @@ #include #include #include +#include LOG_MODULE_REGISTER(modbus_server, LOG_LEVEL_INF); diff --git a/software/lib/shell_modbus/shell_modbus.c b/software/lib/shell_modbus/shell_modbus.c index 751f7a0..17ccbcc 100644 --- a/software/lib/shell_modbus/shell_modbus.c +++ b/software/lib/shell_modbus/shell_modbus.c @@ -10,6 +10,7 @@ #include #include +#include #include /** diff --git a/software/lib/valve/valve.c b/software/lib/valve/valve.c index f6f3e5a..9a57c51 100644 --- a/software/lib/valve/valve.c +++ b/software/lib/valve/valve.c @@ -9,11 +9,11 @@ #include #include -#include #include #include #include #include +#include #define VND_NODE DT_ALIAS(vnd7050aj) #if !DT_NODE_HAS_STATUS(VND_NODE, okay) diff --git a/software/lib/vnd7050aj/CMakeLists.txt b/software/lib/vnd7050aj/CMakeLists.txt new file mode 100644 index 0000000..8781540 --- /dev/null +++ b/software/lib/vnd7050aj/CMakeLists.txt @@ -0,0 +1 @@ +zephyr_library_sources(vnd7050aj.c) \ No newline at end of file diff --git a/software/lib/vnd7050aj/Kconfig b/software/lib/vnd7050aj/Kconfig new file mode 100644 index 0000000..50739de --- /dev/null +++ b/software/lib/vnd7050aj/Kconfig @@ -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 diff --git a/software/lib/vnd7050aj/vnd7050aj.c b/software/lib/vnd7050aj/vnd7050aj.c new file mode 100644 index 0000000..164db59 --- /dev/null +++ b/software/lib/vnd7050aj/vnd7050aj.c @@ -0,0 +1,333 @@ +/* + * Copyright (c) 2025, Eduard Iten + * SPDX-License-Identifier: Apache-2.0 + */ + +#define DT_DRV_COMPAT st_vnd7050aj + +#include +#include +#include +#include +#include +#include + +#include + +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; +} diff --git a/software/pristine.sh b/software/pristine.sh new file mode 100644 index 0000000..b1d3437 --- /dev/null +++ b/software/pristine.sh @@ -0,0 +1 @@ +source /home/edi/zephyrproject/.venv/bin/activate && source /home/edi/zephyrproject/zephyr/zephyr-env.sh && rm -r build ;west build -p always -b esp32c6_devkitc/esp32c6/hpcore apps/gateway -D CMAKE_OBJCOPY=/home/edi/zephyr-sdk-0.17.1/riscv64-zephyr-elf/bin/riscv64-zephyr-elf-objcopy; diff --git a/software/pristine.sysbuild.sh b/software/pristine.sysbuild.sh new file mode 100644 index 0000000..a356770 --- /dev/null +++ b/software/pristine.sysbuild.sh @@ -0,0 +1,3 @@ +source /home/edi/zephyrproject/.venv/bin/activate && \ + source /home/edi/zephyrproject/zephyr/zephyr-env.sh && \ + rm -r build ;west build --sysbuild -p always -b esp32c6_devkitc/esp32c6/hpcore apps/gateway -D CMAKE_OBJCOPY=/home/edi/zephyr-sdk-0.17.1/riscv64-zephyr-elf/bin/riscv64-zephyr-elf-objcopy; diff --git a/software/settings.bin b/software/settings.bin new file mode 100644 index 0000000000000000000000000000000000000000..adc819c34598668cce05436304bf54a35f9de6fe GIT binary patch literal 43 wcmdO7$gR{bPAw_P%uClVOUx-vwe?9YQ7={~Ni8l>D9OkyR!{= 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 \ No newline at end of file diff --git a/software/tools/modbus_valve_simulator/requirements.txt b/software/tools/modbus_valve_simulator/requirements.txt new file mode 100644 index 0000000..28880a4 --- /dev/null +++ b/software/tools/modbus_valve_simulator/requirements.txt @@ -0,0 +1,2 @@ +pymodbus +pyserial diff --git a/software/tools/modbus_valve_simulator/setup_virtual_comports.sh b/software/tools/modbus_valve_simulator/setup_virtual_comports.sh new file mode 100755 index 0000000..658538e --- /dev/null +++ b/software/tools/modbus_valve_simulator/setup_virtual_comports.sh @@ -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=: 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" diff --git a/software/tools/modbus_valve_simulator/simulator_debug.log b/software/tools/modbus_valve_simulator/simulator_debug.log new file mode 100644 index 0000000..e69de29 diff --git a/software/tools/modbus_valve_simulator/simulator_output.log b/software/tools/modbus_valve_simulator/simulator_output.log new file mode 100644 index 0000000..e69de29