Compare commits

..

4 Commits

Author SHA1 Message Date
Eduard Iten 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
Eduard Iten 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
Eduard Iten 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
Eduard Iten ef966cb078 Played around with the irrigaton system yaml
Signed-off-by: Eduard Iten <eduard@iten.pro>
2025-07-14 14:12:48 +02:00
41 changed files with 1284 additions and 86 deletions

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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_MODBUS=y
CONFIG_MODBUS_ROLE_SERVER=y
CONFIG_MODBUS_BUFFER_SIZE=256
CONFIG_MODBUS_LOG_LEVEL_DBG=y
# Enable VND7050AJ
CONFIG_VND7050AJ=y

View File

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

View File

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

0
software/empty.bin Normal file
View File

View File

@ -1,3 +1,11 @@
# ===================================================================
# 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
@ -5,7 +13,7 @@ esphome:
esp32:
board: esp32-c6-devkitm-1
framework:
type: esp-idf
type: esp-idf # Set to esp-idf as required by the ESP32-C6 board
wifi:
ssid: !secret wifi_ssid
@ -24,88 +32,135 @@ logger:
web_server:
# UART-Bus für Modbus
# ===================================================================
# 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
# Modbus-Komponente (der Hub)
# --- Base Modbus component for the bus ---
modbus:
- id: modbus1
- id: modbus_hub
uart_id: uart_bus
# --- Modbus Controller for the specific valve device ---
modbus_controller:
- id: valve_device
address: 0x01
modbus_id: modbus1
number:
- platform: modbus_controller
modbus_controller_id: valve_device
id: valve_controller_command
name: "Valve Control"
address: 0x01
value_type: U_WORD
# min_value: 0
# max_value: 2
# step: 1
globals:
- id: my_valve_is_open
type: bool
restore_value: false
initial_value: 'true'
valve:
- platform: template
name: "Modbus Ventil"
id: my_modbus_valve
# Lambda, um den aktuellen Zustand zu bestimmen
# Liest den Zustand aus der globalen Variable
lambda: |-
return id(my_valve_is_open);
# Aktion beim Drücken auf "Öffnen"
open_action:
- number.set:
id: valve_controller_command
value: 1
- globals.set:
id: my_valve_is_open
value: 'true'
# Aktion beim Drücken auf "Schliessen"
close_action:
- number.set:
id: valve_controller_command
value: 2
- globals.set:
id: my_valve_is_open
value: 'false'
# (Optional) Aktion beim Drücken auf "Stopp"
stop_action:
- number.set:
id: valve_controller_command
value: 0
- 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:
- platform: modbus_controller
modbus_controller_id: valve_device
name: "Supply Voltage"
register_type: read
device_class: voltage
entity_category: diagnostic
accuracy_decimals: 2
# 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: |-
return x / 1000.0;
address: 0x00F5
unit_of_measurement: "V"
value_type: U_WORD
// 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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -9,11 +9,11 @@
#include <zephyr/device.h>
#include <zephyr/drivers/gpio.h>
#include <zephyr/drivers/misc/vnd7050aj/vnd7050aj.h>
#include <zephyr/kernel.h>
#include <zephyr/logging/log.h>
#include <zephyr/settings/settings.h>
#include <lib/valve.h>
#include <lib/vnd7050aj.h>
#define VND_NODE DT_ALIAS(vnd7050aj)
#if !DT_NODE_HAS_STATUS(VND_NODE, okay)

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;
}

1
software/pristine.sh Normal file
View File

@ -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;

View File

@ -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;

BIN
software/settings.bin Normal file

Binary file not shown.

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"