Compare commits
13 Commits
6cb17be451
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 0c4a728c17 | |||
| d92a1d9533 | |||
| 9325fa20c8 | |||
| 08c47f00f8 | |||
| 1cba00df8c | |||
| 35bd208cc0 | |||
| 48cfcd5d4c | |||
| d76b897eb2 | |||
| 0713f8255e | |||
| fc089e5a33 | |||
| ef966cb078 | |||
| 6f304efb57 | |||
| e1ae96506d |
64
.gitignore
vendored
64
.gitignore
vendored
@@ -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
3
.gitmodules
vendored
Normal 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
14
.vscode/tasks.json
vendored
Normal 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"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
0
software/apps/bl_test/sysbuild/flash.dtsi
Normal file
0
software/apps/bl_test/sysbuild/flash.dtsi
Normal file
10
software/apps/can_node/CMakeLists.txt
Normal file
10
software/apps/can_node/CMakeLists.txt
Normal 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)
|
||||||
2
software/apps/can_node/Kconfig
Normal file
2
software/apps/can_node/Kconfig
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
rsource "../../lib/Kconfig"
|
||||||
|
source "Kconfig.zephyr"
|
||||||
5
software/apps/can_node/VERSION
Normal file
5
software/apps/can_node/VERSION
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
VERSION_MAJOR = 0
|
||||||
|
VERSION_MINOR = 0
|
||||||
|
PATCHLEVEL = 1
|
||||||
|
VERSION_TWEAK = 1
|
||||||
|
EXTRAVERSION = devel
|
||||||
7
software/apps/can_node/boards/bluepill_f103rb.conf
Normal file
7
software/apps/can_node/boards/bluepill_f103rb.conf
Normal 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
|
||||||
43
software/apps/can_node/boards/bluepill_f103rb.overlay
Normal file
43
software/apps/can_node/boards/bluepill_f103rb.overlay
Normal 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";
|
||||||
|
};
|
||||||
47
software/apps/can_node/boards/native_sim.overlay
Normal file
47
software/apps/can_node/boards/native_sim.overlay
Normal 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>;
|
||||||
|
};
|
||||||
|
};
|
||||||
48
software/apps/can_node/boards/weact_stm32g431_core.overlay
Normal file
48
software/apps/can_node/boards/weact_stm32g431_core.overlay
Normal 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";
|
||||||
|
};
|
||||||
16
software/apps/can_node/cdc-acm.overlay
Normal file
16
software/apps/can_node/cdc-acm.overlay
Normal 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;
|
||||||
|
};
|
||||||
88
software/apps/can_node/dts/bindings/st,vnd7050aj.yaml
Normal file
88
software/apps/can_node/dts/bindings/st,vnd7050aj.yaml
Normal 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.
|
||||||
|
|
||||||
4
software/apps/can_node/overlay-cdc-acm.conf
Normal file
4
software/apps/can_node/overlay-cdc-acm.conf
Normal 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
|
||||||
31
software/apps/can_node/prj.conf
Normal file
31
software/apps/can_node/prj.conf
Normal 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
|
||||||
40
software/apps/can_node/src/main.c
Normal file
40
software/apps/can_node/src/main.c
Normal 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;
|
||||||
|
}
|
||||||
5
software/apps/can_node/sysbuild.conf
Normal file
5
software/apps/can_node/sysbuild.conf
Normal 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
|
||||||
@@ -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)
|
||||||
|
|||||||
44
software/apps/gateway/README.md
Normal file
44
software/apps/gateway/README.md
Normal 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.
|
||||||
5
software/apps/gateway/VERSION
Normal file
5
software/apps/gateway/VERSION
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
VERSION_MAJOR = 0
|
||||||
|
VERSION_MINOR = 0
|
||||||
|
PATCHLEVEL = 1
|
||||||
|
VERSION_TWEAK = 0
|
||||||
|
EXTRAVERSION = devel
|
||||||
16
software/apps/gateway/boards/common_4MB.dtsi
Normal file
16
software/apps/gateway/boards/common_4MB.dtsi
Normal 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";
|
||||||
|
};
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
#include "common_4MB.dtsi"
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
2
software/apps/gateway/sysbuild.conf
Normal file
2
software/apps/gateway/sysbuild.conf
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
SB_CONFIG_BOOTLOADER_MCUBOOT=y
|
||||||
|
SB_CONFIG_MCUBOOT_MODE_SINGLE_APP=y
|
||||||
8
software/apps/gateway/sysbuild/gateway.overlay
Normal file
8
software/apps/gateway/sysbuild/gateway.overlay
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
#include "../boards/common_4MB.dtsi"
|
||||||
|
|
||||||
|
/* Application Configuration - Firmware goes to slot0_partition (0x20000) */
|
||||||
|
/ {
|
||||||
|
chosen {
|
||||||
|
zephyr,code-partition = &slot0_partition;
|
||||||
|
};
|
||||||
|
};
|
||||||
3
software/apps/gateway/sysbuild/mcuboot.conf
Normal file
3
software/apps/gateway/sysbuild/mcuboot.conf
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
CONFIG_LOG=y
|
||||||
|
CONFIG_MCUBOOT_LOG_LEVEL_INF=y
|
||||||
|
CONFIG_UART_CONSOLE=n
|
||||||
12
software/apps/gateway/sysbuild/mcuboot.overlay
Normal file
12
software/apps/gateway/sysbuild/mcuboot.overlay
Normal 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;
|
||||||
|
};
|
||||||
|
|
||||||
|
};
|
||||||
1
software/apps/mqtt_gateway
Submodule
1
software/apps/mqtt_gateway
Submodule
Submodule software/apps/mqtt_gateway added at 6e669cfc4e
@@ -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)
|
||||||
|
|||||||
47
software/apps/slave_node/boards/native_sim.overlay
Normal file
47
software/apps/slave_node/boards/native_sim.overlay
Normal 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>;
|
||||||
|
};
|
||||||
|
};
|
||||||
@@ -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>;
|
||||||
|
|||||||
88
software/apps/slave_node/dts/bindings/st,vnd7050aj.yaml
Normal file
88
software/apps/slave_node/dts/bindings/st,vnd7050aj.yaml
Normal 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.
|
||||||
|
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -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");
|
||||||
|
|||||||
5
software/apps/slave_node/sysbuild.conf
Normal file
5
software/apps/slave_node/sysbuild.conf
Normal 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
5
software/esphome/.gitignore
vendored
Normal 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
106
software/esphome/can.yaml
Normal 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]
|
||||||
55
software/esphome/create_secrets.py
Executable file
55
software/esphome/create_secrets.py
Executable 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}")
|
||||||
166
software/esphome/irrigation_system.yaml
Normal file
166
software/esphome/irrigation_system.yaml
Normal 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);
|
||||||
2
software/esphome/requirements.txt
Normal file
2
software/esphome/requirements.txt
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
ruamel.yaml
|
||||||
|
esphome
|
||||||
4
software/esphome/secrets.yaml.example
Normal file
4
software/esphome/secrets.yaml.example
Normal 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'
|
||||||
41
software/include/lib/can_ids.h
Normal file
41
software/include/lib/can_ids.h
Normal 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
|
||||||
0
software/include/lib/hw_shared.h
Normal file
0
software/include/lib/hw_shared.h
Normal 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
|
||||||
74
software/include/lib/vnd7050aj.h
Normal file
74
software/include/lib/vnd7050aj.h
Normal 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_ */
|
||||||
@@ -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)
|
||||||
@@ -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
|
||||||
@@ -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.
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|
||||||
|
|||||||
@@ -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, ¤t_ma);
|
vnd7050aj_read_load_current(vnd7050aj_dev, VALVE_CHANNEL_OPEN, ¤t_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, ¤t_ma);
|
vnd7050aj_read_load_current(vnd7050aj_dev, VALVE_CHANNEL_CLOSE, ¤t_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);
|
||||||
|
}
|
||||||
|
|||||||
1
software/lib/vnd7050aj/CMakeLists.txt
Normal file
1
software/lib/vnd7050aj/CMakeLists.txt
Normal file
@@ -0,0 +1 @@
|
|||||||
|
zephyr_library_sources(vnd7050aj.c)
|
||||||
33
software/lib/vnd7050aj/Kconfig
Normal file
33
software/lib/vnd7050aj/Kconfig
Normal 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
|
||||||
333
software/lib/vnd7050aj/vnd7050aj.c
Normal file
333
software/lib/vnd7050aj/vnd7050aj.c
Normal 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;
|
||||||
|
}
|
||||||
216
software/tools/modbus_valve_simulator/modbus_valve_simulator.py
Normal file
216
software/tools/modbus_valve_simulator/modbus_valve_simulator.py
Normal 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
|
||||||
2
software/tools/modbus_valve_simulator/requirements.txt
Normal file
2
software/tools/modbus_valve_simulator/requirements.txt
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
pymodbus
|
||||||
|
pyserial
|
||||||
58
software/tools/modbus_valve_simulator/setup_virtual_comports.sh
Executable file
58
software/tools/modbus_valve_simulator/setup_virtual_comports.sh
Executable 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"
|
||||||
133
software/tools/rtu_gateway/rtu_gateway.py
Normal file → Executable file
133
software/tools/rtu_gateway/rtu_gateway.py
Normal file → Executable file
@@ -4,63 +4,78 @@ import serial
|
|||||||
import serial_asyncio
|
import serial_asyncio
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
|
||||||
# Globale Variable für den seriellen Writer, um Zugriff im TCP-Handler zu ermöglichen
|
# Globale Variablen für den seriellen Reader und Writer
|
||||||
|
serial_reader = None
|
||||||
serial_writer = None
|
serial_writer = None
|
||||||
|
|
||||||
def log_message(message: str):
|
def log_message(message: str):
|
||||||
"""Gibt eine formatierte Log-Nachricht mit Zeitstempel aus."""
|
"""Gibt eine formatierte Log-Nachricht mit Zeitstempel aus."""
|
||||||
print(f"[{datetime.now().strftime('%Y-%m-%d %H:%M:%S')}] {message}")
|
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):
|
async def handle_tcp_client(reader: asyncio.StreamReader, writer: asyncio.StreamWriter, verbose: bool):
|
||||||
"""Bearbeitet eine einzelne TCP-Client-Verbindung."""
|
"""Bearbeitet eine einzelne TCP-Client-Verbindung."""
|
||||||
global serial_writer
|
global serial_reader, serial_writer
|
||||||
peername = writer.get_extra_info('peername')
|
peername = writer.get_extra_info('peername')
|
||||||
log_message(f"✅ Client verbunden: {peername}")
|
log_message(f"✅ Client verbunden: {peername}")
|
||||||
|
|
||||||
if not serial_writer:
|
if not serial_writer or not serial_reader:
|
||||||
log_message("❌ Fehler: Serielle Verbindung ist nicht aktiv. Client wird getrennt.")
|
log_message("❌ Fehler: Serielle Verbindung ist nicht aktiv. Client wird getrennt.")
|
||||||
writer.close()
|
writer.close(); await writer.wait_closed()
|
||||||
await writer.wait_closed()
|
|
||||||
return
|
return
|
||||||
|
|
||||||
try:
|
try:
|
||||||
while True:
|
while True:
|
||||||
# Daten vom TCP-Client (Home Assistant) lesen
|
|
||||||
tcp_request = await reader.read(256)
|
tcp_request = await reader.read(256)
|
||||||
if not tcp_request:
|
if not tcp_request: break
|
||||||
# Verbindung vom Client geschlossen
|
|
||||||
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
|
||||||
|
|
||||||
# Anfrage an das serielle Gerät weiterleiten
|
|
||||||
serial_writer.write(tcp_request)
|
serial_writer.write(tcp_request)
|
||||||
await serial_writer.drain()
|
await serial_writer.drain()
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# Auf Antwort vom seriellen Gerät warten (mit Timeout)
|
serial_response = await asyncio.wait_for(serial_reader.read(256), timeout=2.0)
|
||||||
serial_response = await asyncio.wait_for(serial_writer.protocol.transport.serial.read_async(256), timeout=2.0)
|
if verbose: log_verbose_response(serial_response)
|
||||||
|
|
||||||
# Antwort an den TCP-Client senden
|
|
||||||
writer.write(serial_response)
|
writer.write(serial_response)
|
||||||
await writer.drain()
|
await writer.drain()
|
||||||
|
|
||||||
if verbose:
|
|
||||||
# Zerlegen der Modbus-Antwort für das Logging
|
|
||||||
dev_id = tcp_request[0]
|
|
||||||
func_code = tcp_request[1]
|
|
||||||
# Einfache Register-Extraktion (variiert je nach Funktion)
|
|
||||||
# Dies ist eine Annahme für gängige Lesefunktionen
|
|
||||||
if func_code in [3, 4]:
|
|
||||||
reg_addr = int.from_bytes(tcp_request[2:4], 'big')
|
|
||||||
data_hex = serial_response[3:-2].hex() # Daten ohne ID, Func, Count und CRC
|
|
||||||
log_message(f"VERBOSE: id {dev_id:03d}, reg 0x{reg_addr:04x}, data 0x{data_hex}")
|
|
||||||
else:
|
|
||||||
log_message(f"VERBOSE: id {dev_id:03d}, data {serial_response.hex()}")
|
|
||||||
|
|
||||||
except asyncio.TimeoutError:
|
except asyncio.TimeoutError:
|
||||||
if verbose:
|
log_message("VERBOSE: <-- Timeout from DEV")
|
||||||
reg_addr = int.from_bytes(tcp_request[2:4], 'big')
|
|
||||||
log_message(f"VERBOSE: reg 0x{reg_addr:04x} <timeout>")
|
|
||||||
# Kein Timeout an den Client senden, das Protokoll selbst behandelt dies
|
|
||||||
|
|
||||||
except asyncio.CancelledError:
|
except asyncio.CancelledError:
|
||||||
log_message("TCP-Handler wurde abgebrochen.")
|
log_message("TCP-Handler wurde abgebrochen.")
|
||||||
@@ -68,36 +83,34 @@ async def handle_tcp_client(reader: asyncio.StreamReader, writer: asyncio.Stream
|
|||||||
log_message(f"TCP-Verbindungsfehler: {e}")
|
log_message(f"TCP-Verbindungsfehler: {e}")
|
||||||
finally:
|
finally:
|
||||||
log_message(f"🔌 Client getrennt: {peername}")
|
log_message(f"🔌 Client getrennt: {peername}")
|
||||||
writer.close()
|
writer.close(); await writer.wait_closed()
|
||||||
await writer.wait_closed()
|
|
||||||
|
|
||||||
|
|
||||||
async def serial_reconnector(comport, baudrate):
|
async def serial_reconnector(comport, baudrate):
|
||||||
"""Versucht, die serielle Verbindung wiederherzustellen."""
|
"""Versucht, die serielle Verbindung wiederherzustellen."""
|
||||||
global serial_writer
|
global serial_reader, serial_writer
|
||||||
for attempt in range(1, 6):
|
for attempt in range(1, 6):
|
||||||
log_message(f"🚨 Serielle Verbindung verloren! Versuch {attempt}/5 in 5 Sekunden...")
|
log_message(f"🚨 Serielle Verbindung verloren! Versuch {attempt}/5 in 5 Sekunden...")
|
||||||
await asyncio.sleep(5)
|
await asyncio.sleep(5)
|
||||||
try:
|
try:
|
||||||
# Erneuter Verbindungsversuch
|
reader_obj, writer_obj = await serial_asyncio.open_serial_connection(
|
||||||
_, writer = await serial_asyncio.open_serial_connection(
|
|
||||||
url=comport, baudrate=baudrate, rtscts=False, dsrdtr=False
|
url=comport, baudrate=baudrate, rtscts=False, dsrdtr=False
|
||||||
)
|
)
|
||||||
serial_writer = writer # Globale Variable aktualisieren
|
serial_reader, serial_writer = reader_obj, writer_obj
|
||||||
log_message(f"✅ Serielle Verbindung zu {comport} wiederhergestellt.")
|
log_message(f"✅ Serielle Verbindung zu {comport} wiederhergestellt.")
|
||||||
return writer # Erfolgreich, gib den neuen Writer zurück
|
return True
|
||||||
except (serial.SerialException, FileNotFoundError) as e:
|
except (serial.SerialException, FileNotFoundError) as e:
|
||||||
log_message(f"❌ Wiederverbindung fehlgeschlagen: {e}")
|
log_message(f"❌ Wiederverbindung fehlgeschlagen: {e}")
|
||||||
|
|
||||||
log_message("💥 Konnte serielle Verbindung nach 5 Versuchen nicht wiederherstellen. Programm wird beendet.")
|
log_message("💥 Konnte serielle Verbindung nach 5 Versuchen nicht wiederherstellen. Programm wird beendet.")
|
||||||
# Sauberes Beenden des gesamten Programms
|
try:
|
||||||
loop = asyncio.get_running_loop()
|
asyncio.get_running_loop().stop()
|
||||||
loop.stop()
|
except RuntimeError: pass
|
||||||
return None
|
return False
|
||||||
|
|
||||||
async def main(args):
|
async def main(args):
|
||||||
"""Hauptfunktion zum Starten des Servers und der seriellen Verbindung."""
|
"""Hauptfunktion zum Starten des Servers und der seriellen Verbindung."""
|
||||||
global serial_writer
|
global serial_reader, serial_writer
|
||||||
log_message("--- Modbus RTU zu TCP Gateway ---")
|
log_message("--- Modbus RTU zu TCP Gateway ---")
|
||||||
log_message(f"Serieller Port: {args.comport}")
|
log_message(f"Serieller Port: {args.comport}")
|
||||||
log_message(f"Baudrate: {args.baudrate}")
|
log_message(f"Baudrate: {args.baudrate}")
|
||||||
@@ -105,35 +118,33 @@ async def main(args):
|
|||||||
log_message(f"Verbose Modus: {'Aktiv' if args.verbose else 'Inaktiv'}")
|
log_message(f"Verbose Modus: {'Aktiv' if args.verbose else 'Inaktiv'}")
|
||||||
log_message("---------------------------------")
|
log_message("---------------------------------")
|
||||||
|
|
||||||
# Initiale serielle Verbindung herstellen
|
|
||||||
try:
|
try:
|
||||||
_, serial_writer = await serial_asyncio.open_serial_connection(
|
serial_reader, serial_writer = await serial_asyncio.open_serial_connection(
|
||||||
url=args.comport, baudrate=args.baudrate, rtscts=False, dsrdtr=False
|
url=args.comport, baudrate=args.baudrate, rtscts=False, dsrdtr=False
|
||||||
)
|
)
|
||||||
log_message(f"✅ Serielle Verbindung zu {args.comport} erfolgreich hergestellt.")
|
log_message(f"✅ Serielle Verbindung zu {args.comport} erfolgreich hergestellt.")
|
||||||
except (serial.SerialException, FileNotFoundError) as e:
|
except (serial.SerialException, FileNotFoundError) as e:
|
||||||
log_message(f"❌ Kritischer Fehler bei der initialen Verbindung: {e}")
|
log_message(f"❌ Kritischer Fehler bei der initialen Verbindung: {e}")
|
||||||
serial_writer = await serial_reconnector(args.comport, args.baudrate)
|
if not await serial_reconnector(args.comport, args.baudrate): return
|
||||||
if not serial_writer:
|
|
||||||
return # Beenden, wenn die Wiederverbindung fehlschlägt
|
|
||||||
|
|
||||||
# TCP-Server starten
|
|
||||||
server_handler = lambda r, w: handle_tcp_client(r, w, args.verbose)
|
|
||||||
server = await asyncio.start_server(server_handler, '0.0.0.0', args.tcpport)
|
|
||||||
|
|
||||||
|
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()
|
addr = server.sockets[0].getsockname()
|
||||||
log_message(f"👂 Server lauscht auf {addr}")
|
log_message(f"👂 Server lauscht auf {addr}")
|
||||||
|
|
||||||
async with server:
|
async with server:
|
||||||
# Überwache die serielle Verbindung im Hintergrund
|
|
||||||
while True:
|
while True:
|
||||||
if not serial_writer or not serial_writer.protocol.transport.is_open():
|
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...")
|
log_message("Serielle Verbindung unterbrochen. Starte Wiederverbindungs-Logik...")
|
||||||
serial_writer = await serial_reconnector(args.comport, args.baudrate)
|
if not await serial_reconnector(args.comport, args.baudrate):
|
||||||
if not serial_writer:
|
server.close(); break
|
||||||
server.close() # Server stoppen, wenn seriell nicht mehr geht
|
await asyncio.sleep(2)
|
||||||
break # Schleife und Programm beenden
|
|
||||||
await asyncio.sleep(1) # Kurze Pause, um die CPU zu schonen
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
parser = argparse.ArgumentParser(description="Modbus RTU zu TCP Gateway für Home Assistant.")
|
parser = argparse.ArgumentParser(description="Modbus RTU zu TCP Gateway für Home Assistant.")
|
||||||
@@ -147,6 +158,6 @@ if __name__ == "__main__":
|
|||||||
try:
|
try:
|
||||||
asyncio.run(main(args))
|
asyncio.run(main(args))
|
||||||
except KeyboardInterrupt:
|
except KeyboardInterrupt:
|
||||||
log_message("👋 Programm wird durch Benutzer beendet.")
|
log_message("\n👋 Programm wird durch Benutzer beendet.")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
log_message(f"💥 Unerwarteter Fehler im Hauptprogramm: {e}")
|
log_message(f"💥 Unerwarteter Fehler im Hauptprogramm: {e}")
|
||||||
|
|||||||
Reference in New Issue
Block a user