Compare commits
34 Commits
project-re
...
fc0add8583
| Author | SHA1 | Date | |
|---|---|---|---|
| fc0add8583 | |||
| 66cdc3ae27 | |||
| 32bb77926f | |||
| 4df0181d7f | |||
| d6fb501594 | |||
|
|
76d0d0647c | ||
| 3de42a46c2 | |||
| ddaaa8988d | |||
| b937c52bcc | |||
| 3c2235733b | |||
| a3e8d5c168 | |||
| 5fd904de9e | |||
| 92bb171e85 | |||
| bd8a7a766c | |||
| 8f89713866 | |||
| bf29061db6 | |||
| c1622bb01c | |||
| 222ffea568 | |||
| a9a0626913 | |||
| b11f844415 | |||
| 2e8a86bc54 | |||
| 224adccf6b | |||
| 9b7159d5a4 | |||
| bc327acc41 | |||
| c9b0f38576 | |||
| edf0fb2563 | |||
| 537d76ef5d | |||
| 45d011952f | |||
| bb25134b6c | |||
| 9f96384aa5 | |||
| b543579393 | |||
| 69cf7e9511 | |||
| 8df7aef51b | |||
| f6ee0a5122 |
7
.gemini_commit_message.txt
Normal file
7
.gemini_commit_message.txt
Normal file
@@ -0,0 +1,7 @@
|
||||
feat(modbus): Implement persistent and improved reconfiguration for Modbus server
|
||||
|
||||
This commit enhances the Modbus server's configuration handling by:
|
||||
|
||||
- Loading saved baudrate and unit ID settings during initialization, ensuring persistence across reboots.
|
||||
- Providing improved feedback during `modbus_reconfigure`, including logging for successful changes and informing the user when a device restart is required for changes to take effect.
|
||||
- Saving new configuration settings even if immediate reinitialization fails, allowing them to be applied on the next boot.
|
||||
@@ -39,7 +39,7 @@ Die Slave-Nodes sind die Arbeitseinheiten im Feld. Um bei der Fertigung kleiner
|
||||
|
||||
* **Mikrocontroller:** Ein `STM32G431PB`. Dieser ist zwar leistungsstark, bietet aber alle nötigen Peripherien (mehrere UARTs, ADCs, CAN) und ermöglicht ein einheitliches Hardware- und Software-Design.
|
||||
* **Peripherie pro Node:**
|
||||
* **Zwei High-Side Ausgänge (+12V):** Realisiert über einen `VND7050AJ`. Perfekt zur Ansteuerung der 12V-Motorventile (`Öffnen`/`Schliessen`). Die `Sense`-Leitung des Treibers wird über einen AD-Wandler ausgelesen, um durch Messung des Motorstroms eine Endlagen-Erkennung ohne physische Endschalter zu realisieren (Motorstrom im Stillstand ≈ 0).
|
||||
* **Zwei High-Side Ausgänge (+12V):** Realisiert über einen `VND7050AJ`. Perfekt zur Ansteuerung der 12V-Motorventile (`Öffnen`/`Schliessen`). Die `Sense`-Leitung des Treibers wird über einen AD-Wandler ausgelesen, um durch Messung des Motorstroms eine Endlagen-Erkennung ohne physische Endschalter zu realisieren (Motorstrom im Stillstand ≈ 0). Zusätzlich können die Temperatur und die Versorgungsspannung des Treibers ausgelesen werden.
|
||||
* **Zwei Low-Side Ausgänge (0V):** Über N-Kanal-MOSFETs geschaltete Ausgänge. Nutzbar zur Ansteuerung von 12V-LEDs in Tastern oder zum Schalten des Halbleiter-Relais für die Pumpe.
|
||||
* **Zwei digitale Eingänge:** Direkte, geschützte Eingänge am Controller zum Anschluss von Tastern oder den kapazitiven NPN-Sensoren.
|
||||
|
||||
|
||||
@@ -29,25 +29,29 @@ Alle Register sind in einer einzigen, durchgehenden Liste pro Register-Typ (`Inp
|
||||
|
||||
| Adresse (hex) | Name | Zugehörigkeit | Beschreibung |
|
||||
| :------------ | :----------------------------- | :---------------- | :---------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| **0x0000** | `VENTIL_ZUSTAND_BEWEGUNG` | Ventil | Kombiniertes Status-Register. **High-Byte**: Bewegung (`0`=Idle, `1`=Öffnet, `2`=Schliesst, `3`=Fehler). **Low-Byte**: Zustand (`0`=Geschlossen, `1`=Geöffnet). |
|
||||
| **0x0001** | `MOTORSTROM_MA` | Ventil | Aktueller Motorstrom in Milliampere (mA). |
|
||||
| **0x0020** | `DIGITAL_EINGAENGE_ZUSTAND` | Eingänge | Bitmaske der digitalen Eingänge. Bit 0: Eingang 1, Bit 1: Eingang 2. `1`=Aktiv. |
|
||||
| **0x0021** | `TASTER_EVENTS` | Eingänge | Event-Flags für Taster (Clear-on-Read). Bit 0: Taster 1 gedrückt. Bit 1: Taster 2 gedrückt. |
|
||||
| **0x0000** | `VALVE_STATE_MOVEMENT` | Ventil | Kombiniertes Status-Register. **High-Byte**: Bewegung (`0`=Idle, `1`=Öffnet, `2`=Schliesst, `3`=Fehler). **Low-Byte**: Zustand (`0`=Geschlossen, `1`=Geöffnet). |
|
||||
| **0x0001** | `MOTORSTROM_OPEN_MA` | Ventil | Motorstrom beim Öffnen in Milliampere (mA). |
|
||||
| **0x0002** | `MOTORSTROM_CLOSE_MA` | Ventil | Motorstrom beim Schließen in Milliampere (mA). |
|
||||
| **0x0020** | `DIGITAL_INPUTS_STATE` | Eingänge | Bitmaske der digitalen Eingänge. Bit 0: Eingang 1, Bit 1: Eingang 2. `1`=Aktiv. |
|
||||
| **0x0021** | `BUTTON_EVENTS` | Eingänge | Event-Flags für Taster (Clear-on-Read). Bit 0: Taster 1 gedrückt. Bit 1: Taster 2 gedrückt. |
|
||||
| **0x00F0** | `FIRMWARE_VERSION_MAJOR_MINOR` | System | z.B. `0x0102` für v1.2. |
|
||||
| **0x00F1** | `FIRMWARE_VERSION_PATCH` | System | z.B. `3` für v1.2.3. |
|
||||
| **0x00F2** | `DEVICE_STATUS` | System | `0`=OK, `1`=Allgemeiner Fehler. |
|
||||
| **0x00F3** | `UPTIME_SECONDS_LOW` | System | Untere 16 Bit der Uptime in Sekunden. |
|
||||
| **0x00F4** | `UPTIME_SECONDS_HIGH` | System | Obere 16 Bit der Uptime. |
|
||||
| **0x00F5** | `SUPPLY_VOLTAGE_MV` | System | Aktuelle Versorgungsspannung in Millivolt (mV). |
|
||||
| **0x0100** | `FWU_LAST_CHUNK_CRC` | Firmware-Update | Enthält den CRC16 des zuletzt im Puffer empfangenen Daten-Chunks. |
|
||||
|
||||
## 3. Holding Registers (4xxxx, Read/Write)
|
||||
|
||||
| Adresse (hex) | Name | Zugehörigkeit | Beschreibung |
|
||||
| :------------ | :---------------------------- | :---------------- | :---------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| **0x0000** | `VENTIL_BEFEHL` | Ventil | `1`=Öffnen, `2`=Schliessen, `0`=Bewegung stoppen. |
|
||||
| **0x0001** | `MAX_OEFFNUNGSZEIT_S` | Ventil | Sicherheits-Timeout in Sekunden für den Öffnen-Vorgang. |
|
||||
| **0x0002** | `MAX_SCHLIESSZEIT_S` | Ventil | Sicherheits-Timeout in Sekunden für den Schliessen-Vorgang. |
|
||||
| **0x0010** | `DIGITAL_AUSGAENGE_ZUSTAND` | Ausgänge | Bitmaske zum Lesen und Schreiben der Ausgänge. Bit 0: Ausgang 1, Bit 1: Ausgang 2. `1`=AN, `0`=AUS. |
|
||||
| **0x0000** | `VALVE_COMMAND` | Ventil | `1`=Öffnen, `2`=Schliessen, `0`=Bewegung stoppen. |
|
||||
| **0x0001** | `MAX_OPENING_TIME_S` | Ventil | Sicherheits-Timeout in Sekunden für den Öffnen-Vorgang. |
|
||||
| **0x0002** | `MAX_CLOSING_TIME_S` | Ventil | Sicherheits-Timeout in Sekunden für den Schliessen-Vorgang. |
|
||||
| **0x0003** | `END_CURRENT_THRESHOLD_OPEN_MA` | Ventil | Minimaler Stromschwellenwert in mA zur Endlagenerkennung beim Öffnen. |
|
||||
| **0x0004** | `END_CURRENT_THRESHOLD_CLOSE_MA` | Ventil | Minimaler Stromschwellenwert in mA zur Endlagenerkennung beim Schliessen. |
|
||||
| **0x0010** | `DIGITAL_OUTPUTS_STATE` | Ausgänge | Bitmaske zum Lesen und Schreiben der Ausgänge. Bit 0: Ausgang 1, Bit 1: Ausgang 2. `1`=AN, `0`=AUS. |
|
||||
| **0x00F0** | `WATCHDOG_TIMEOUT_S` | System | Timeout des Fail-Safe-Watchdogs in Sekunden. `0`=Deaktiviert. |
|
||||
| **0x00F1** | `DEVICE_RESET` | System | Schreibt `1` um das Gerät neu zu starten. |
|
||||
| **0x0100** | `FWU_COMMAND` | Firmware-Update | `1`: **Verify Chunk**: Der zuletzt übertragene Chunk wurde vom Client als gültig befunden. Der Slave soll ihn nun ins Flash schreiben. `2`: **Finalize Update**: Alle Chunks sind übertragen. Installation abschliessen und neu starten. |
|
||||
@@ -79,10 +83,10 @@ Diese Register gehören zum externen Füllstandsensor und können auf dem Bus eb
|
||||
|
||||
| Adresse (hex) | Name | R/W | Beschreibung |
|
||||
| :------------ | :------------------------- | :-- | :---------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| **0x0000** | `NODE_ADRESSE` | R/W | Geräteadresse des Sensors (1-255). |
|
||||
| **0x0000** | `NODE_ADDRESS` | R/W | Geräteadresse des Sensors (1-255). |
|
||||
| **0x0001** | `BAUDRATE` | R/W | `0`=1200, `1`=2400, `2`=4800, `3`=9600, `4`=19200, `5`=38400, `6`=57600, `7`=115200. |
|
||||
| **0x0002** | `EINHEIT` | R/W | `0`=Keine, `1`=cm, `2`=mm, `3`=MPa, `4`=Pa, `5`=kPa. |
|
||||
| **0x0003** | `NACHKOMMASTELLEN` | R/W | Anzahl der Dezimalstellen für den Messwert (0-3). |
|
||||
| **0x0004** | `MESSWERT_AKTUELL` | R | Der skalierte Messwert als vorzeichenbehafteter 16-Bit-Integer. |
|
||||
| **0x0005** | `MESSBEREICH_NULLPUNKT` | R/W | Rohwert für den Nullpunkt der Skala. |
|
||||
| **0x0006** | `MESSBEREICH_ENDPUNKT` | R/W | Rohwert für den Endpunkt der Skala. |
|
||||
| **0x0002** | `UNIT` | R/W | `0`=Keine, `1`=cm, `2`=mm, `3`=MPa, `4`=Pa, `5`=kPa. |
|
||||
| **0x0003** | `DECIMAL_PLACES` | R/W | Anzahl der Dezimalstellen für den Messwert (0-3). |
|
||||
| **0x0004** | `CURRENT_MEASUREMENT` | R | Der skalierte Messwert als vorzeichenbehafteter 16-Bit-Integer. |
|
||||
| **0x0005** | `MEASUREMENT_RANGE_ZERO_POINT` | R/W | Rohwert für den Nullpunkt der Skala. |
|
||||
| **0x0006** | `MEASUREMENT_RANGE_END_POINT` | R/W | Rohwert für den Endpunkt der Skala. |
|
||||
|
||||
@@ -9,11 +9,13 @@
|
||||
| ✅ | **Phase 0: Planung & Definition** | | |
|
||||
| ✅ | Konzept erstellen und finalisieren | 30.06.2025 | Architektur, Komponenten und grundlegende Architektur sind festgelegt. |
|
||||
| ✅ | MODBUS Register Map definieren | 30.06.2025 | Die "API" der Slaves ist definiert und bildet die Grundlage für die Software-Entwicklung. |
|
||||
| ✅ | Header- und deutsche Dokumentation aktualisiert | 10.07.2025 | Doxygen-Kommentare in Headern und deutsche .md-Dateien auf den neuesten Stand gebracht und übersetzt. |
|
||||
| ☐ | **Phase 1: Slave-Node Prototyp (STM32 Eval-Board)** | | **Ziel:** Ein einzelner Slave wird auf dem Eval-Board zum Leben erweckt. |
|
||||
| ✅ | 1.1 Entwicklungsumgebung für STM32/Zephyr einrichten | 30.06.2025 | Toolchain, VS Code, Zephyr-SDK, MCUBoot etc. installieren und ein "Hello World" zum Laufen bringen. |
|
||||
| ☐ | 1.2 Basis-Firmware für Slave-Node erstellen | | Hardware-Abstraktion (GPIOs, ADC, UART für RS485) implementieren. |
|
||||
| ☐ | 1.3 MODBUS-RTU Stack auf dem Slave implementieren | | Basierend auf der definierten Register-Map. Zuerst nur lesende Funktionen (Status, Version). |
|
||||
| ☐ | 1.4 Kernlogik implementieren (z.B. Ventilsteuerung) | | Umsetzung der `VENTIL_ZUSTAND_BEWEGUNG` Logik, Strommessung für Endlagen etc. |
|
||||
| ✅ | 1.2 Hardware-Abstraktion (VND7050AJ, RS485) | 10.07.2025 | Implementierung der Treiber für den VND7050AJ und die RS485-Kommunikation. |
|
||||
| ✅ | 1.3 Basis-Firmware für Slave-Node erstellen | 10.07.2025 | Hardware-Abstraktion (GPIOs) implementiert. |
|
||||
| ✅ | 1.3 MODBUS-RTU Stack auf dem Slave implementieren | 10.07.2025 | Basierend auf der definierten Register-Map. Zuerst nur lesende Funktionen (Status, Version). |
|
||||
| ✅ | 1.4 Kernlogik implementieren (z.B. Ventilsteuerung) | 10.07.2025 | Umsetzung der `VALVE_STATE_MOVEMENT` Logik, Strommessung für Endlagen etc. |
|
||||
| ☐ | **Phase 2: Verifikation der Slave-Firmware** | | **Ziel:** Nachweisen, dass der Slave sich exakt an die MODBUS-Spezifikation hält. |
|
||||
| ☐ | 2.1 Slave-Node mit PC via USB-MODBUS-Adapter testen | | **Kritischer Meilenstein.** Mit Tools wie "QModMaster" oder einem Python-Skript die Register lesen & schreiben. Die Slave-Firmware wird so unabhängig vom Gateway validiert. |
|
||||
| ☐ | 2.2 Firmware-Update Mechanismus testen | | Den kompletten Update-Prozess (Chunking, CRC-Check) mit einem Skript vom PC aus testen. Der Slave schreibt die Firmware dabei vorerst nur in einen ungenutzten RAM-Bereich. |
|
||||
|
||||
@@ -1,6 +0,0 @@
|
||||
cmake_minimum_required(VERSION 3.20)
|
||||
|
||||
project(fwu)
|
||||
|
||||
target_sources(fwu PRIVATE src/fwu.c)
|
||||
target_include_directories(fwu PUBLIC include)
|
||||
@@ -1,6 +0,0 @@
|
||||
cmake_minimum_required(VERSION 3.20)
|
||||
|
||||
project(modbus_server)
|
||||
|
||||
target_sources(modbus_server PRIVATE src/modbus_server.c)
|
||||
target_include_directories(modbus_server PUBLIC include)
|
||||
@@ -1,6 +0,0 @@
|
||||
cmake_minimum_required(VERSION 3.20)
|
||||
|
||||
project(valve)
|
||||
|
||||
target_sources(valve PRIVATE src/valve.c)
|
||||
target_include_directories(valve PUBLIC include)
|
||||
56
setup-format-hook.sh
Executable file
56
setup-format-hook.sh
Executable file
@@ -0,0 +1,56 @@
|
||||
#!/bin/sh
|
||||
|
||||
# This script sets up a Git pre-commit hook to automatically format C/C++ files
|
||||
# in the 'software/' subdirectory using clang-format.
|
||||
|
||||
# Define the path for the pre-commit hook
|
||||
HOOK_DIR=".git/hooks"
|
||||
HOOK_FILE="$HOOK_DIR/pre-commit"
|
||||
|
||||
# Create the hooks directory if it doesn't exist
|
||||
mkdir -p "$HOOK_DIR"
|
||||
|
||||
# Create the pre-commit hook script using a 'here document'
|
||||
cat > "$HOOK_FILE" << 'EOF'
|
||||
#!/bin/sh
|
||||
|
||||
# --- Pre-commit hook for clang-format ---
|
||||
#
|
||||
# This hook formats staged C, C++, and Objective-C files in the 'software/'
|
||||
# subdirectory before a commit is made.
|
||||
# It automatically finds the .clang-format file in the software/ directory.
|
||||
#
|
||||
|
||||
# Directory to be formatted
|
||||
TARGET_DIR="software/"
|
||||
|
||||
# Use git diff to find staged files that are Added (A), Copied (C), or Modified (M).
|
||||
# We filter for files only within the TARGET_DIR.
|
||||
# The grep regex matches common C/C++ and Objective-C file extensions.
|
||||
FILES_TO_FORMAT=$(git diff --cached --name-only --diff-filter=ACM "$TARGET_DIR" | grep -E '\.(c|h|cpp|hpp|cxx|hxx|cc|hh|m|mm)$')
|
||||
|
||||
if [ -z "$FILES_TO_FORMAT" ]; then
|
||||
# No relevant files to format, exit successfully.
|
||||
exit 0
|
||||
fi
|
||||
|
||||
echo "› Running clang-format on staged files in '$TARGET_DIR'..."
|
||||
|
||||
# Run clang-format in-place on the identified files.
|
||||
# clang-format will automatically find the .clang-format file in the software/ directory
|
||||
# or any of its parent directories.
|
||||
echo "$FILES_TO_FORMAT" | xargs clang-format -i
|
||||
|
||||
# Since clang-format may have changed the files, we need to re-stage them.
|
||||
echo "$FILES_TO_FORMAT" | xargs git add
|
||||
|
||||
echo "› Formatting complete."
|
||||
|
||||
exit 0
|
||||
EOF
|
||||
|
||||
# Make the hook executable
|
||||
chmod +x "$HOOK_FILE"
|
||||
|
||||
echo "✅ Git pre-commit hook has been set up successfully."
|
||||
echo " It will now automatically format files in the '$PWD/software' directory before each commit."
|
||||
142
software/.clang-format
Normal file
142
software/.clang-format
Normal file
@@ -0,0 +1,142 @@
|
||||
# Zephyr Project .clang-format configuration
|
||||
# Based on Linux kernel style with Zephyr-specific adaptations
|
||||
|
||||
# Use LLVM as the base style and customize from there
|
||||
BasedOnStyle: LLVM
|
||||
|
||||
# Language settings
|
||||
Language: Cpp
|
||||
|
||||
# Indentation settings
|
||||
IndentWidth: 8
|
||||
TabWidth: 8
|
||||
UseTab: ForIndentation
|
||||
|
||||
# Line length
|
||||
ColumnLimit: 100
|
||||
|
||||
# Brace settings
|
||||
BreakBeforeBraces: Linux
|
||||
BraceWrapping:
|
||||
AfterClass: true
|
||||
AfterControlStatement: false
|
||||
AfterEnum: true
|
||||
AfterFunction: true
|
||||
AfterNamespace: true
|
||||
AfterStruct: true
|
||||
AfterUnion: true
|
||||
BeforeCatch: true
|
||||
BeforeElse: false
|
||||
IndentBraces: false
|
||||
SplitEmptyFunction: true
|
||||
SplitEmptyRecord: true
|
||||
SplitEmptyNamespace: true
|
||||
|
||||
# Always add braces for control statements (Zephyr requirement)
|
||||
RemoveBracesLLVM: false
|
||||
|
||||
# Control statement settings
|
||||
SpaceBeforeParens: ControlStatements
|
||||
SpacesInParentheses: false
|
||||
|
||||
# Function settings
|
||||
AllowShortFunctionsOnASingleLine: None
|
||||
AllowShortBlocksOnASingleLine: Empty
|
||||
AllowShortIfStatementsOnASingleLine: Never
|
||||
AllowShortLoopsOnASingleLine: false
|
||||
AllowShortCaseLabelsOnASingleLine: false
|
||||
|
||||
# Pointer and reference alignment
|
||||
PointerAlignment: Right
|
||||
ReferenceAlignment: Right
|
||||
|
||||
# Spacing settings
|
||||
SpaceAfterCStyleCast: false
|
||||
SpaceAfterLogicalNot: false
|
||||
SpaceBeforeAssignmentOperators: true
|
||||
SpaceBeforeCpp11BracedList: false
|
||||
SpaceBeforeCtorInitializerColon: true
|
||||
SpaceBeforeInheritanceColon: true
|
||||
SpaceBeforeRangeBasedForLoopColon: true
|
||||
SpaceInEmptyParentheses: false
|
||||
SpacesBeforeTrailingComments: 1
|
||||
SpacesInAngles: false
|
||||
SpacesInCStyleCastParentheses: false
|
||||
SpacesInContainerLiterals: false
|
||||
SpacesInSquareBrackets: false
|
||||
|
||||
# Alignment settings
|
||||
AlignAfterOpenBracket: DontAlign
|
||||
AlignConsecutiveAssignments: false
|
||||
AlignConsecutiveDeclarations: false
|
||||
AlignEscapedNewlines: Right
|
||||
AlignOperands: false
|
||||
AlignTrailingComments: false
|
||||
|
||||
# Breaking settings
|
||||
AlwaysBreakAfterDefinitionReturnType: None
|
||||
AlwaysBreakAfterReturnType: None
|
||||
AlwaysBreakBeforeMultilineStrings: false
|
||||
AlwaysBreakTemplateDeclarations: false
|
||||
BinPackArguments: false
|
||||
BinPackParameters: false
|
||||
BreakBeforeBinaryOperators: None
|
||||
BreakBeforeTernaryOperators: true
|
||||
BreakConstructorInitializersBeforeComma: false
|
||||
BreakAfterJavaFieldAnnotations: false
|
||||
BreakStringLiterals: true
|
||||
|
||||
# Penalties (used for line breaking decisions)
|
||||
PenaltyBreakAssignment: 2
|
||||
PenaltyBreakBeforeFirstCallParameter: 19
|
||||
PenaltyBreakComment: 300
|
||||
PenaltyBreakFirstLessLess: 120
|
||||
PenaltyBreakString: 1000
|
||||
PenaltyExcessCharacter: 1000000
|
||||
PenaltyReturnTypeOnItsOwnLine: 60
|
||||
|
||||
# Comment settings
|
||||
ReflowComments: true
|
||||
CommentPragmas: '^ IWYU pragma:'
|
||||
|
||||
# Sorting settings
|
||||
SortIncludes: true
|
||||
SortUsingDeclarations: true
|
||||
|
||||
# Preprocessor settings
|
||||
IndentPPDirectives: None
|
||||
MacroBlockBegin: ''
|
||||
MacroBlockEnd: ''
|
||||
|
||||
# Misc settings
|
||||
CompactNamespaces: false
|
||||
ConstructorInitializerAllOnOneLineOrOnePerLine: false
|
||||
ConstructorInitializerIndentWidth: 4
|
||||
ContinuationIndentWidth: 4
|
||||
Cpp11BracedListStyle: true
|
||||
DerivePointerAlignment: false
|
||||
DisableFormat: false
|
||||
ExperimentalAutoDetectBinPacking: false
|
||||
FixNamespaceComments: true
|
||||
ForEachMacros: ['LISTIFY', 'FOR_EACH', 'FOR_EACH_FIXED_ARG', 'FOR_EACH_IDX', 'FOR_EACH_IDX_FIXED_ARG', 'FOR_EACH_NONEMPTY_TERM', 'Z_FOR_EACH', 'Z_FOR_EACH_FIXED_ARG', 'Z_FOR_EACH_IDX', 'Z_FOR_EACH_IDX_FIXED_ARG']
|
||||
IncludeBlocks: Preserve
|
||||
IncludeCategories:
|
||||
- Regex: '^<zephyr/.*\.h>'
|
||||
Priority: 1
|
||||
- Regex: '^<.*\.h>'
|
||||
Priority: 2
|
||||
- Regex: '^<.*'
|
||||
Priority: 3
|
||||
- Regex: '.*'
|
||||
Priority: 4
|
||||
IndentCaseLabels: false
|
||||
IndentWrappedFunctionNames: false
|
||||
JavaScriptQuotes: Leave
|
||||
JavaScriptWrapImports: true
|
||||
KeepEmptyLinesAtTheStartOfBlocks: false
|
||||
MaxEmptyLinesToKeep: 1
|
||||
NamespaceIndentation: None
|
||||
ObjCBinPackProtocolList: Auto
|
||||
ObjCBlockIndentWidth: 2
|
||||
ObjCSpaceAfterProperty: false
|
||||
ObjCSpaceBeforeProtocolList: true
|
||||
23
software/.vscode/settings.json
vendored
23
software/.vscode/settings.json
vendored
@@ -1,12 +1,15 @@
|
||||
{
|
||||
// Hush CMake
|
||||
"cmake.configureOnOpen": false,
|
||||
|
||||
// IntelliSense
|
||||
"C_Cpp.default.compilerPath": "${userHome}/zephyr-sdk-0.17.1/arm-zephyr-eabi/bin/arm-zephyr-eabi-gcc.exe",
|
||||
"C_Cpp.default.compileCommands": "${workspaceFolder}/build/compile_commands.json",
|
||||
|
||||
// File Associations
|
||||
"files.associations": {
|
||||
}
|
||||
// Hush CMake
|
||||
"cmake.configureOnOpen": false,
|
||||
// IntelliSense
|
||||
"C_Cpp.default.compilerPath": "${userHome}/zephyr-sdk-0.17.1/arm-zephyr-eabi/bin/arm-zephyr-eabi-gcc.exe",
|
||||
"C_Cpp.default.compileCommands": "${workspaceFolder}/build/compile_commands.json",
|
||||
// File Associations
|
||||
"files.associations": {
|
||||
"app_version.h": "c"
|
||||
},
|
||||
"C_Cpp.clang_format_style": "file",
|
||||
"nrf-connect.applications": [
|
||||
"${workspaceFolder}/apps/slave_node"
|
||||
],
|
||||
}
|
||||
40
software/.vscode/tasks.json
vendored
40
software/.vscode/tasks.json
vendored
@@ -2,32 +2,20 @@
|
||||
"version": "2.0.0",
|
||||
"tasks": [
|
||||
{
|
||||
"label": "West Build",
|
||||
"type": "shell",
|
||||
"group": {
|
||||
"kind": "build",
|
||||
"isDefault": true
|
||||
},
|
||||
"linux": {
|
||||
"command": "${userHome}/zephyrproject/.venv/bin/west"
|
||||
},
|
||||
"windows": {
|
||||
"command": "${userHome}/zephyrproject/.venv/Scripts/west.exe"
|
||||
},
|
||||
"osx": {
|
||||
"command": "${userHome}/zephyrproject/.venv/bin/west"
|
||||
},
|
||||
"args": [
|
||||
"build",
|
||||
"-p",
|
||||
"auto",
|
||||
"-b",
|
||||
"valve_node"
|
||||
],
|
||||
"problemMatcher": [
|
||||
"$gcc"
|
||||
]
|
||||
},
|
||||
"label": "Format All C/C++ Files",
|
||||
"type": "shell",
|
||||
"command": "find . -name \"*.c\" -o -name \"*.h\" | xargs clang-format -i",
|
||||
"problemMatcher": [],
|
||||
"group": {
|
||||
"kind": "build",
|
||||
"isDefault": true
|
||||
},
|
||||
"presentation": {
|
||||
"reveal": "silent",
|
||||
"clear": true,
|
||||
"panel": "shared"
|
||||
}
|
||||
},
|
||||
{
|
||||
"label": "West Configurable Build",
|
||||
"type": "shell",
|
||||
|
||||
@@ -1 +1,7 @@
|
||||
rsource "lib/Kconfig"
|
||||
rsource "lib/shell_valve/Kconfig"
|
||||
|
||||
config SLAVE_NODE_APP
|
||||
bool "Slave Node Application"
|
||||
default y
|
||||
select SHELL_VALVE
|
||||
|
||||
0
software/apps/firmware_node/CMakeLists.txt
Normal file
0
software/apps/firmware_node/CMakeLists.txt
Normal file
0
software/apps/firmware_node/README.md
Normal file
0
software/apps/firmware_node/README.md
Normal file
0
software/apps/firmware_node/pm.yml
Normal file
0
software/apps/firmware_node/pm.yml
Normal file
0
software/apps/firmware_node/prj.conf
Normal file
0
software/apps/firmware_node/prj.conf
Normal file
0
software/apps/firmware_node/src/main.c
Normal file
0
software/apps/firmware_node/src/main.c
Normal file
0
software/apps/firmware_node/sysbuild.cmake
Normal file
0
software/apps/firmware_node/sysbuild.cmake
Normal file
0
software/apps/firmware_node/sysbuild.conf
Normal file
0
software/apps/firmware_node/sysbuild.conf
Normal file
0
software/apps/firmware_node/sysbuild/mcuboot.conf
Normal file
0
software/apps/firmware_node/sysbuild/mcuboot.conf
Normal file
12
software/apps/slave_node/.vscode/c_cpp_properties.json
vendored
Normal file
12
software/apps/slave_node/.vscode/c_cpp_properties.json
vendored
Normal file
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"configurations": [
|
||||
{
|
||||
"name": "Linux",
|
||||
"compileCommands": "${workspaceFolder}/build/compile_commands.json",
|
||||
"cStandard": "c99",
|
||||
"cppStandard": "gnu++17",
|
||||
"intelliSenseMode": "linux-gcc-arm"
|
||||
}
|
||||
],
|
||||
"version": 4
|
||||
}
|
||||
5
software/apps/slave_node/VERSION
Normal file
5
software/apps/slave_node/VERSION
Normal file
@@ -0,0 +1,5 @@
|
||||
VERSION_MAJOR = 0
|
||||
VERSION_MINOR = 0
|
||||
PATCHLEVEL = 1
|
||||
VERSION_TWEAK = 1
|
||||
EXTRAVERSION = devel
|
||||
@@ -1,9 +1,48 @@
|
||||
/ {
|
||||
aliases {
|
||||
vnd7050aj = &vnd7050aj;
|
||||
};
|
||||
|
||||
vnd7050aj: vnd7050aj {
|
||||
compatible = "st,vnd7050aj";
|
||||
status = "okay";
|
||||
|
||||
input0-gpios = <&gpiob 7 GPIO_ACTIVE_HIGH>;
|
||||
input1-gpios = <&gpiob 9 GPIO_ACTIVE_HIGH>;
|
||||
select0-gpios = <&gpiob 5 GPIO_ACTIVE_HIGH>;
|
||||
select1-gpios = <&gpiob 6 GPIO_ACTIVE_HIGH>;
|
||||
sense-enable-gpios = <&gpiob 4 GPIO_ACTIVE_HIGH>;
|
||||
fault-reset-gpios = <&gpiob 3 GPIO_ACTIVE_LOW>;
|
||||
io-channels = <&adc1 1>;
|
||||
r-sense-ohms = <1500>;
|
||||
k-vcc = <3816>;
|
||||
};
|
||||
};
|
||||
|
||||
&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>;
|
||||
pinctrl-0 = <&usart1_tx_pa9 &usart1_rx_pa10>; // PA9=TX, PA10=RX for Modbus communication
|
||||
pinctrl-names = "default";
|
||||
};
|
||||
@@ -1,3 +1,5 @@
|
||||
#include <zephyr/dt-bindings/gpio/gpio.h>
|
||||
|
||||
&zephyr_udc0 {
|
||||
cdc_acm_uart0: cdc_acm_uart0 {
|
||||
compatible = "zephyr,cdc-acm-uart";
|
||||
@@ -8,3 +10,7 @@
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
&usart1 {
|
||||
/delete-node/ modbus0;
|
||||
};
|
||||
@@ -5,6 +5,9 @@ CONFIG_LOG=y
|
||||
# Enable Shell
|
||||
CONFIG_SHELL=y
|
||||
CONFIG_REBOOT=y
|
||||
CONFIG_SHELL_MODBUS=y
|
||||
CONFIG_SHELL_VALVE=y
|
||||
CONFIG_SHELL_SYSTEM=y
|
||||
|
||||
# Enable Settings Subsystem
|
||||
CONFIG_SETTINGS=y
|
||||
@@ -21,3 +24,6 @@ CONFIG_MODBUS=y
|
||||
CONFIG_MODBUS_ROLE_SERVER=y
|
||||
CONFIG_MODBUS_BUFFER_SIZE=256
|
||||
|
||||
# Enable VND7050AJ
|
||||
CONFIG_VND7050AJ=y
|
||||
CONFIG_LOG_VALVE_LEVEL=4
|
||||
@@ -1,9 +1,9 @@
|
||||
#include <zephyr/kernel.h>
|
||||
#include <zephyr/settings/settings.h>
|
||||
#include <zephyr/logging/log.h>
|
||||
#include <zephyr/settings/settings.h>
|
||||
#include <lib/fwu.h>
|
||||
#include <lib/modbus_server.h>
|
||||
#include <lib/valve.h>
|
||||
#include <lib/fwu.h>
|
||||
|
||||
LOG_MODULE_REGISTER(main, LOG_LEVEL_INF);
|
||||
|
||||
|
||||
@@ -3,8 +3,45 @@
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
/**
|
||||
* @file fwu.h
|
||||
* @brief API for the Firmware Update (FWU) library.
|
||||
*
|
||||
* This library provides the core logic for handling the over-the-air firmware
|
||||
* update process via Modbus. It manages the data buffer, processes commands,
|
||||
* and calculates CRC checksums for data verification.
|
||||
*/
|
||||
|
||||
/**
|
||||
* @brief Initializes the firmware update module.
|
||||
*
|
||||
* This function currently does nothing but is a placeholder for future
|
||||
* initialization logic.
|
||||
*/
|
||||
void fwu_init(void);
|
||||
|
||||
/**
|
||||
* @brief Handles incoming Modbus register writes related to firmware updates.
|
||||
*
|
||||
* This function is the main entry point for the FWU process. It parses the
|
||||
* address and value from a Modbus write operation and takes appropriate action,
|
||||
* such as storing metadata (offset, size) or data chunks, and processing
|
||||
* commands (verify, finalize).
|
||||
*
|
||||
* @param addr The Modbus register address being written to.
|
||||
* @param reg The 16-bit value being written to the register.
|
||||
*/
|
||||
void fwu_handler(uint16_t addr, uint16_t reg);
|
||||
|
||||
/**
|
||||
* @brief Gets the CRC16-CCITT of the last received firmware chunk.
|
||||
*
|
||||
* After a data chunk is fully received into the buffer, this function can be
|
||||
* called to retrieve the calculated CRC checksum. The master can then compare
|
||||
* this with its own calculated CRC to verify data integrity.
|
||||
*
|
||||
* @return The 16-bit CRC of the last chunk.
|
||||
*/
|
||||
uint16_t fwu_get_last_chunk_crc(void);
|
||||
|
||||
#endif // FWU_H
|
||||
|
||||
@@ -4,49 +4,174 @@
|
||||
#include <stdint.h>
|
||||
|
||||
/**
|
||||
* @brief Modbus Input Register Addresses.
|
||||
* @file modbus_server.h
|
||||
* @brief API for the Modbus server implementation.
|
||||
*
|
||||
* This file defines the Modbus register map and provides functions to
|
||||
* initialize and manage the Modbus server.
|
||||
*/
|
||||
|
||||
/**
|
||||
* @brief Modbus Input Register Addresses (Read-Only).
|
||||
* @see docs/modbus-registers.de.md
|
||||
*/
|
||||
enum {
|
||||
/* Valve Control & Status */
|
||||
REG_INPUT_VALVE_STATE_MOVEMENT = 0x0000,
|
||||
REG_INPUT_MOTOR_CURRENT_MA = 0x0001,
|
||||
/* Digital Inputs */
|
||||
REG_INPUT_DIGITAL_INPUTS_STATE = 0x0020,
|
||||
REG_INPUT_BUTTON_EVENTS = 0x0021,
|
||||
/* System Config & Status */
|
||||
/**
|
||||
* @brief Combined status register for the valve.
|
||||
* High-Byte: Movement (0=Idle, 1=Opening, 2=Closing, 3=Error).
|
||||
* Low-Byte: State (0=Closed, 1=Open).
|
||||
*/
|
||||
REG_INPUT_VALVE_STATE_MOVEMENT = 0x0000,
|
||||
/**
|
||||
* @brief Motor current during opening in milliamperes (mA).
|
||||
*/
|
||||
REG_INPUT_MOTOR_OPEN_CURRENT_MA = 0x0001,
|
||||
/**
|
||||
* @brief Motor current during closing in milliamperes (mA).
|
||||
*/
|
||||
REG_INPUT_MOTOR_CLOSE_CURRENT_MA = 0x0002,
|
||||
/**
|
||||
* @brief Bitmask of digital inputs. Bit 0: Input 1, Bit 1: Input 2.
|
||||
* 1=Active.
|
||||
*/
|
||||
REG_INPUT_DIGITAL_INPUTS_STATE = 0x0020,
|
||||
/**
|
||||
* @brief Event flags for buttons (Clear-on-Read). Bit 0: Button 1 pressed.
|
||||
* Bit 1: Button 2 pressed.
|
||||
*/
|
||||
REG_INPUT_BUTTON_EVENTS = 0x0021,
|
||||
/**
|
||||
* @brief Firmware version, e.g., 0x0102 for v1.2.
|
||||
*/
|
||||
REG_INPUT_FIRMWARE_VERSION_MAJOR_MINOR = 0x00F0,
|
||||
REG_INPUT_FIRMWARE_VERSION_PATCH = 0x00F1,
|
||||
REG_INPUT_DEVICE_STATUS = 0x00F2,
|
||||
REG_INPUT_UPTIME_SECONDS_LOW = 0x00F3,
|
||||
REG_INPUT_UPTIME_SECONDS_HIGH = 0x00F4,
|
||||
/* Firmware Update */
|
||||
REG_INPUT_FWU_LAST_CHUNK_CRC = 0x0100,
|
||||
/**
|
||||
* @brief Firmware version patch level, e.g., 3 for v1.2.3.
|
||||
*/
|
||||
REG_INPUT_FIRMWARE_VERSION_PATCH = 0x00F1,
|
||||
/**
|
||||
* @brief Device status (0=OK, 1=General Error).
|
||||
*/
|
||||
REG_INPUT_DEVICE_STATUS = 0x00F2,
|
||||
/**
|
||||
* @brief Lower 16 bits of uptime in seconds.
|
||||
*/
|
||||
REG_INPUT_UPTIME_SECONDS_LOW = 0x00F3,
|
||||
/**
|
||||
* @brief Upper 16 bits of uptime in seconds.
|
||||
*/
|
||||
REG_INPUT_UPTIME_SECONDS_HIGH = 0x00F4,
|
||||
/**
|
||||
* @brief Current supply voltage in millivolts (mV).
|
||||
*/
|
||||
REG_INPUT_SUPPLY_VOLTAGE_MV = 0x00F5,
|
||||
/**
|
||||
* @brief CRC16 of the last received data chunk in the buffer for firmware
|
||||
* update.
|
||||
*/
|
||||
REG_INPUT_FWU_LAST_CHUNK_CRC = 0x0100
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief Modbus Holding Register Addresses.
|
||||
* @brief Modbus Holding Register Addresses (Read/Write).
|
||||
* @see docs/modbus-registers.de.md
|
||||
*/
|
||||
enum {
|
||||
/* Valve Control */
|
||||
REG_HOLDING_VALVE_COMMAND = 0x0000,
|
||||
REG_HOLDING_MAX_OPENING_TIME_S = 0x0001,
|
||||
REG_HOLDING_MAX_CLOSING_TIME_S = 0x0002,
|
||||
/* Digital Outputs */
|
||||
REG_HOLDING_DIGITAL_OUTPUTS_STATE = 0x0010,
|
||||
/* System Config */
|
||||
REG_HOLDING_WATCHDOG_TIMEOUT_S = 0x00F0,
|
||||
REG_HOLDING_DEVICE_RESET = 0x00F1,
|
||||
/* Firmware Update */
|
||||
REG_HOLDING_FWU_COMMAND = 0x0100,
|
||||
REG_HOLDING_FWU_CHUNK_OFFSET_LOW = 0x0101,
|
||||
REG_HOLDING_FWU_CHUNK_OFFSET_HIGH = 0x0102,
|
||||
REG_HOLDING_FWU_CHUNK_SIZE = 0x0103,
|
||||
REG_HOLDING_FWU_DATA_BUFFER = 0x0180,
|
||||
/**
|
||||
* @brief Valve control command (1=Open, 2=Close, 0=Stop movement).
|
||||
*/
|
||||
REG_HOLDING_VALVE_COMMAND = 0x0000,
|
||||
/**
|
||||
* @brief Safety timeout in seconds for the opening process.
|
||||
*/
|
||||
REG_HOLDING_MAX_OPENING_TIME_S = 0x0001,
|
||||
/**
|
||||
* @brief Safety timeout in seconds for the closing process.
|
||||
*/
|
||||
REG_HOLDING_MAX_CLOSING_TIME_S = 0x0002,
|
||||
/**
|
||||
* @brief Minimum current threshold in mA for end-position detection.
|
||||
*/
|
||||
REG_HOLDING_END_CURRENT_THRESHOLD_OPEN_MA = 0x0003,
|
||||
/**
|
||||
* @brief Minimum current threshold in mA for end-position detection during
|
||||
* closing.
|
||||
*/
|
||||
REG_HOLDING_END_CURRENT_THRESHOLD_CLOSE_MA = 0x0004,
|
||||
/**
|
||||
* @brief Bitmask for reading and writing digital outputs. Bit 0: Output 1,
|
||||
* Bit 1: Output 2. 1=ON, 0=OFF.
|
||||
*/
|
||||
REG_HOLDING_DIGITAL_OUTPUTS_STATE = 0x0010,
|
||||
/**
|
||||
* @brief Fail-safe watchdog timeout in seconds. 0=Disabled.
|
||||
*/
|
||||
REG_HOLDING_WATCHDOG_TIMEOUT_S = 0x00F0,
|
||||
/**
|
||||
* @brief Writing 1 restarts the device.
|
||||
*/
|
||||
REG_HOLDING_DEVICE_RESET = 0x00F1,
|
||||
/**
|
||||
* @brief Command for firmware update.
|
||||
* 1: Verify Chunk - Slave writes the last chunk to flash.
|
||||
* 2: Finalize Update - Complete installation and restart.
|
||||
*/
|
||||
REG_HOLDING_FWU_COMMAND = 0x0100,
|
||||
/**
|
||||
* @brief Lower 16 bits of the 32-bit offset for the next firmware update
|
||||
* chunk.
|
||||
*/
|
||||
REG_HOLDING_FWU_CHUNK_OFFSET_LOW = 0x0101,
|
||||
/**
|
||||
* @brief Upper 16 bits of the 32-bit offset for the next firmware update
|
||||
* chunk.
|
||||
*/
|
||||
REG_HOLDING_FWU_CHUNK_OFFSET_HIGH = 0x0102,
|
||||
/**
|
||||
* @brief Size of the next firmware update chunk in bytes (max. 256).
|
||||
*/
|
||||
REG_HOLDING_FWU_CHUNK_SIZE = 0x0103,
|
||||
/**
|
||||
* @brief Start address of the 256-byte buffer for firmware update data.
|
||||
*/
|
||||
REG_HOLDING_FWU_DATA_BUFFER = 0x0180,
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief Initializes the Modbus server.
|
||||
*
|
||||
* This function sets up the Modbus RTU server interface, loads saved settings
|
||||
* (baudrate, unit ID), and starts listening for requests.
|
||||
*
|
||||
* @return 0 on success, or a negative error code on failure.
|
||||
*/
|
||||
int modbus_server_init(void);
|
||||
|
||||
/**
|
||||
* @brief Reconfigures the Modbus server at runtime.
|
||||
*
|
||||
* Updates the baudrate and unit ID of the server. If the reconfiguration
|
||||
* fails, the settings are saved and will be applied after a device reset.
|
||||
*
|
||||
* @param baudrate The new baudrate to set.
|
||||
* @param unit_id The new Modbus unit ID (slave address).
|
||||
* @return 0 on success, or a negative error code if immediate reconfiguration
|
||||
* fails. Returns 0 even on failure if settings could be saved for the next
|
||||
* boot.
|
||||
*/
|
||||
int modbus_reconfigure(uint32_t baudrate, uint8_t unit_id);
|
||||
|
||||
/**
|
||||
* @brief Gets the current baudrate of the Modbus server.
|
||||
*
|
||||
* @return The current baudrate.
|
||||
*/
|
||||
uint32_t modbus_get_baudrate(void);
|
||||
|
||||
/**
|
||||
* @brief Gets the current unit ID of the Modbus server.
|
||||
*
|
||||
* @return The current unit ID.
|
||||
*/
|
||||
uint8_t modbus_get_unit_id(void);
|
||||
|
||||
#endif // MODBUS_SERVER_H
|
||||
|
||||
@@ -1,23 +1,167 @@
|
||||
#ifndef VALVE_H
|
||||
#define VALVE_H
|
||||
|
||||
#include <zephyr/drivers/gpio.h>
|
||||
#include <stdint.h>
|
||||
|
||||
enum valve_state { VALVE_STATE_CLOSED, VALVE_STATE_OPEN };
|
||||
enum valve_movement { VALVE_MOVEMENT_IDLE, VALVE_MOVEMENT_OPENING, VALVE_MOVEMENT_CLOSING, VALVE_MOVEMENT_ERROR };
|
||||
/**
|
||||
* @file valve.h
|
||||
* @brief API for controlling the motorized valve.
|
||||
*
|
||||
* This library provides functions to initialize, open, close, and stop the
|
||||
* valve. It also allows getting the valve's state and movement status, and
|
||||
* configuring the maximum opening and closing times.
|
||||
*/
|
||||
|
||||
void valve_init(void);
|
||||
#define VALVE_CHANNEL_OPEN 0
|
||||
#define VALVE_CHANNEL_CLOSE 1
|
||||
#define VALVE_ENDPOSITION_CHECK_INTERVAL K_MSEC(100)
|
||||
|
||||
#define VALVE_OBSTACLE_THRESHOLD_OPEN_MA 200
|
||||
#define VALVE_OBSTACLE_THRESHOLD_CLOSE_MA 200
|
||||
|
||||
/**
|
||||
* @brief Represents the static state of the valve (open or closed).
|
||||
*/
|
||||
enum valve_state {
|
||||
VALVE_STATE_CLOSED, /**< The valve is fully closed. */
|
||||
VALVE_STATE_OPEN, /**< The valve is fully open. */
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief Represents the dynamic movement status of the valve.
|
||||
*/
|
||||
enum valve_movement {
|
||||
VALVE_MOVEMENT_IDLE, /**< The valve is not moving. */
|
||||
VALVE_MOVEMENT_OPENING, /**< The valve is currently opening. */
|
||||
VALVE_MOVEMENT_CLOSING, /**< The valve is currently closing. */
|
||||
VALVE_MOVEMENT_ERROR /**< An error occurred during movement. */
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief Initializes the valve control system.
|
||||
*
|
||||
* Configures the GPIOs and loads saved settings for timeouts.
|
||||
* This function must be called before any other valve functions.
|
||||
*
|
||||
* @return 0 on success, or a negative error code on failure.
|
||||
*/
|
||||
int valve_init(void);
|
||||
|
||||
/**
|
||||
* @brief Starts opening the valve.
|
||||
*
|
||||
* The valve will open for the configured maximum opening time.
|
||||
*/
|
||||
void valve_open(void);
|
||||
|
||||
/**
|
||||
* @brief Starts closing the valve.
|
||||
*
|
||||
* The valve will close for the configured maximum closing time.
|
||||
*/
|
||||
void valve_close(void);
|
||||
|
||||
/**
|
||||
* @brief Stops any ongoing valve movement immediately.
|
||||
*/
|
||||
void valve_stop(void);
|
||||
|
||||
/**
|
||||
* @brief Gets the current static state of the valve.
|
||||
*
|
||||
* @return The current valve state (VALVE_STATE_CLOSED or VALVE_STATE_OPEN).
|
||||
*/
|
||||
enum valve_state valve_get_state(void);
|
||||
enum valve_movement valve_get_movement(void);
|
||||
uint16_t valve_get_motor_current(void);
|
||||
|
||||
/**
|
||||
* @brief Gets the current movement status of the valve.
|
||||
*
|
||||
* @return The current movement status.
|
||||
*/
|
||||
enum valve_movement valve_get_movement(void);
|
||||
|
||||
/**
|
||||
* @brief Sets the maximum time for the valve to open.
|
||||
*
|
||||
* @param seconds The timeout in seconds.
|
||||
*/
|
||||
void valve_set_max_open_time(uint16_t seconds);
|
||||
|
||||
/**
|
||||
* @brief Sets the maximum time for the valve to close.
|
||||
*
|
||||
* @param seconds The timeout in seconds.
|
||||
*/
|
||||
void valve_set_max_close_time(uint16_t seconds);
|
||||
|
||||
/**
|
||||
* @brief Sets the current threshold for end-position detection during opening.
|
||||
*
|
||||
* @param current_ma The current threshold in milliamps.
|
||||
*/
|
||||
void valve_set_end_current_threshold_open(uint16_t current_ma);
|
||||
|
||||
/**
|
||||
* @brief Sets the current threshold for end-position detection during closing.
|
||||
*
|
||||
* @param current_ma The current threshold in milliamps.
|
||||
*/
|
||||
void valve_set_end_current_threshold_close(uint16_t current_ma);
|
||||
|
||||
/**
|
||||
* @brief Gets the current threshold for end-position detection during opening.
|
||||
*
|
||||
* @return The current threshold in milliamps.
|
||||
*/
|
||||
uint16_t valve_get_end_current_threshold_open(void);
|
||||
|
||||
/**
|
||||
* @brief Gets the current threshold for end-position detection during closing.
|
||||
*
|
||||
* @return The current threshold in milliamps.
|
||||
*/
|
||||
uint16_t valve_get_end_current_threshold_close(void);
|
||||
|
||||
/**
|
||||
* @brief Gets the configured maximum opening time.
|
||||
*
|
||||
* @return The timeout in seconds.
|
||||
*/
|
||||
uint16_t valve_get_max_open_time(void);
|
||||
|
||||
/**
|
||||
* @brief Gets the configured maximum closing time.
|
||||
*
|
||||
* @return The timeout in seconds.
|
||||
*/
|
||||
uint16_t valve_get_max_close_time(void);
|
||||
|
||||
/**
|
||||
* @brief Gets the current drawn by the valve motor during opening.
|
||||
*
|
||||
* @return The motor current in milliamps.
|
||||
*/
|
||||
int32_t valve_get_opening_current(void);
|
||||
|
||||
/**
|
||||
* @brief Gets the current drawn by the valve motor during closing.
|
||||
*
|
||||
* @return The motor current in milliamps.
|
||||
*/
|
||||
int32_t valve_get_closing_current(void);
|
||||
|
||||
/**
|
||||
* @brief Gets the temperature of the valve motor driver.
|
||||
*
|
||||
* @return The temperature in degrees Celsius.
|
||||
*/
|
||||
int32_t valve_get_vnd_temp(void);
|
||||
|
||||
/**
|
||||
* @brief Gets the voltage supplied to the valve motor driver.
|
||||
*
|
||||
* @return The voltage in millivolts.
|
||||
*/
|
||||
int32_t valve_get_vnd_voltage(void);
|
||||
#endif // VALVE_H
|
||||
|
||||
@@ -3,3 +3,4 @@ add_subdirectory_ifdef(CONFIG_LIB_MODBUS_SERVER modbus_server)
|
||||
add_subdirectory_ifdef(CONFIG_LIB_VALVE valve)
|
||||
add_subdirectory_ifdef(CONFIG_SHELL_SYSTEM shell_system)
|
||||
add_subdirectory_ifdef(CONFIG_SHELL_MODBUS shell_modbus)
|
||||
add_subdirectory_ifdef(CONFIG_SHELL_VALVE shell_valve)
|
||||
@@ -5,4 +5,5 @@ rsource "modbus_server/Kconfig"
|
||||
rsource "valve/Kconfig"
|
||||
rsource "shell_system/Kconfig"
|
||||
rsource "shell_modbus/Kconfig"
|
||||
rsource "shell_valve/Kconfig"
|
||||
endmenu
|
||||
@@ -1,45 +1,64 @@
|
||||
/**
|
||||
* @file fwu.c
|
||||
* @brief Implementation of the Firmware Update (FWU) library.
|
||||
*
|
||||
* This file implements the logic for receiving a new firmware image in chunks
|
||||
* over Modbus. It maintains a buffer for the incoming data, calculates the CRC
|
||||
* of the received chunk, and handles commands to verify the chunk and finalize
|
||||
* the update process. The actual writing to flash is simulated.
|
||||
*/
|
||||
|
||||
#include <zephyr/kernel.h>
|
||||
#include <zephyr/sys/crc.h>
|
||||
#include <zephyr/sys/byteorder.h>
|
||||
#include <zephyr/logging/log.h>
|
||||
#include <zephyr/sys/byteorder.h>
|
||||
#include <zephyr/sys/crc.h>
|
||||
#include <lib/fwu.h>
|
||||
|
||||
LOG_MODULE_REGISTER(fwu, LOG_LEVEL_INF);
|
||||
|
||||
#define FWU_BUFFER_SIZE 256
|
||||
static uint8_t fwu_buffer[FWU_BUFFER_SIZE];
|
||||
static uint32_t fwu_chunk_offset = 0;
|
||||
static uint16_t fwu_chunk_size = 0;
|
||||
static uint16_t fwu_last_chunk_crc = 0;
|
||||
static uint8_t fwu_buffer[FWU_BUFFER_SIZE]; // Buffer to store incoming
|
||||
// firmware data chunks
|
||||
static uint32_t fwu_chunk_offset = 0; // Offset for the current firmware chunk in the overall image
|
||||
static uint16_t fwu_chunk_size = 0; // Size of the current firmware chunk
|
||||
static uint16_t fwu_last_chunk_crc = 0; // CRC16 of the last received firmware chunk
|
||||
|
||||
void fwu_init(void) {}
|
||||
void fwu_init(void)
|
||||
{
|
||||
}
|
||||
|
||||
void fwu_handler(uint16_t addr, uint16_t reg)
|
||||
{
|
||||
// This is a simplified handler. In a real scenario, you would have a proper mapping
|
||||
// between register addresses and actions.
|
||||
if (addr == 0x0100) { // FWU_COMMAND
|
||||
if (reg == 1) { LOG_INF("FWU: Chunk at offset %u (size %u) verified.", fwu_chunk_offset, fwu_chunk_size); }
|
||||
else if (reg == 2) { LOG_INF("FWU: Finalize command received. Rebooting (simulated)."); }
|
||||
} else if (addr == 0x0101) { // FWU_CHUNK_OFFSET_LOW
|
||||
fwu_chunk_offset = (fwu_chunk_offset & 0xFFFF0000) | reg;
|
||||
} else if (addr == 0x0102) { // FWU_CHUNK_OFFSET_HIGH
|
||||
fwu_chunk_offset = (fwu_chunk_offset & 0x0000FFFF) | ((uint32_t)reg << 16);
|
||||
} else if (addr == 0x0103) { // FWU_CHUNK_SIZE
|
||||
fwu_chunk_size = (reg > FWU_BUFFER_SIZE) ? FWU_BUFFER_SIZE : reg;
|
||||
} else if (addr >= 0x0180 && addr < (0x0180 + (FWU_BUFFER_SIZE / 2))) {
|
||||
uint16_t index = (addr - 0x0180) * 2;
|
||||
if (index < sizeof(fwu_buffer)) {
|
||||
sys_put_be16(reg, &fwu_buffer[index]);
|
||||
if (index + 2 >= fwu_chunk_size) {
|
||||
fwu_last_chunk_crc = crc16_ccitt(0xffff, fwu_buffer, fwu_chunk_size);
|
||||
LOG_INF("FWU: Chunk received, CRC is 0x%04X", fwu_last_chunk_crc);
|
||||
}
|
||||
}
|
||||
}
|
||||
// This is a simplified handler. In a real scenario, you would have a proper
|
||||
// mapping between register addresses and actions.
|
||||
if (addr == 0x0100) { // FWU_COMMAND
|
||||
if (reg == 1) {
|
||||
LOG_INF("FWU: Chunk at offset %u (size %u) verified.",
|
||||
fwu_chunk_offset,
|
||||
fwu_chunk_size);
|
||||
} else if (reg == 2) {
|
||||
LOG_INF("FWU: Finalize command received. Rebooting (simulated).");
|
||||
}
|
||||
} else if (addr == 0x0101) { // FWU_CHUNK_OFFSET_LOW
|
||||
fwu_chunk_offset = (fwu_chunk_offset & 0xFFFF0000) | reg;
|
||||
} else if (addr == 0x0102) { // FWU_CHUNK_OFFSET_HIGH
|
||||
fwu_chunk_offset = (fwu_chunk_offset & 0x0000FFFF) | ((uint32_t)reg << 16);
|
||||
} else if (addr == 0x0103) { // FWU_CHUNK_SIZE
|
||||
fwu_chunk_size = (reg > FWU_BUFFER_SIZE) ? FWU_BUFFER_SIZE : reg;
|
||||
} else if (addr >= 0x0180 && addr < (0x0180 + (FWU_BUFFER_SIZE / 2))) {
|
||||
uint16_t index = (addr - 0x0180) * 2;
|
||||
if (index < sizeof(fwu_buffer)) {
|
||||
sys_put_be16(reg, &fwu_buffer[index]);
|
||||
if (index + 2 >= fwu_chunk_size) {
|
||||
fwu_last_chunk_crc =
|
||||
crc16_ccitt(0xffff, fwu_buffer, fwu_chunk_size);
|
||||
LOG_INF("FWU: Chunk received, CRC is 0x%04X", fwu_last_chunk_crc);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
uint16_t fwu_get_last_chunk_crc(void)
|
||||
{
|
||||
return fwu_last_chunk_crc;
|
||||
return fwu_last_chunk_crc;
|
||||
}
|
||||
|
||||
@@ -1,47 +1,77 @@
|
||||
#include <zephyr/kernel.h>
|
||||
#include <zephyr/drivers/uart.h>
|
||||
/**
|
||||
* @file modbus_server.c
|
||||
* @brief Modbus RTU server implementation for the irrigation system slave node.
|
||||
*
|
||||
* This file implements the Modbus server logic, including register callbacks,
|
||||
* watchdog handling, and dynamic reconfiguration. It interfaces with other
|
||||
* libraries like valve control, ADC sensors, and firmware updates.
|
||||
*/
|
||||
|
||||
#include <zephyr/device.h>
|
||||
#include <zephyr/modbus/modbus.h>
|
||||
#include <zephyr/drivers/misc/vnd7050aj/vnd7050aj.h>
|
||||
#include <zephyr/drivers/uart.h>
|
||||
#include <zephyr/kernel.h>
|
||||
#include <zephyr/logging/log.h>
|
||||
#include <zephyr/modbus/modbus.h>
|
||||
#include <zephyr/settings/settings.h>
|
||||
#include <zephyr/sys/reboot.h>
|
||||
#include <zephyr/usb/usb_device.h>
|
||||
#include <app_version.h>
|
||||
#include <lib/fwu.h>
|
||||
#include <lib/modbus_server.h>
|
||||
#include <lib/valve.h>
|
||||
#include <lib/fwu.h>
|
||||
|
||||
#include <zephyr/usb/usb_device.h>
|
||||
|
||||
LOG_MODULE_REGISTER(modbus_server, LOG_LEVEL_INF);
|
||||
|
||||
static int modbus_iface;
|
||||
static struct modbus_iface_param server_param = {
|
||||
.mode = MODBUS_MODE_RTU,
|
||||
.server = {.user_cb = NULL, .unit_id = 1},
|
||||
.serial = {.baud = 19200, .parity = UART_CFG_PARITY_NONE},
|
||||
.mode = MODBUS_MODE_RTU,
|
||||
.server = {.user_cb = NULL, .unit_id = 1},
|
||||
.serial = {.baud = 19200, .parity = UART_CFG_PARITY_NONE},
|
||||
};
|
||||
|
||||
static uint16_t watchdog_timeout_s = 0;
|
||||
static struct k_timer watchdog_timer;
|
||||
|
||||
/**
|
||||
* @brief Timer handler for the Modbus watchdog.
|
||||
*
|
||||
* This function is called when the watchdog timer expires, indicating a loss
|
||||
* of communication with the Modbus master. It triggers a fail-safe action,
|
||||
* which is to close the valve.
|
||||
*
|
||||
* @param timer_id Pointer to the timer instance.
|
||||
*/
|
||||
static void watchdog_timer_handler(struct k_timer *timer_id)
|
||||
{
|
||||
LOG_WRN("Modbus watchdog expired! Closing valve as a fail-safe.");
|
||||
valve_close();
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Resets the Modbus watchdog timer.
|
||||
*
|
||||
* This function should be called upon receiving any valid Modbus request
|
||||
* to prevent the watchdog from expiring.
|
||||
*/
|
||||
static inline void reset_watchdog(void)
|
||||
{
|
||||
if (watchdog_timeout_s > 0)
|
||||
{
|
||||
if (watchdog_timeout_s > 0) {
|
||||
k_timer_start(&watchdog_timer, K_SECONDS(watchdog_timeout_s), K_NO_WAIT);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Callback for reading Modbus holding registers.
|
||||
*
|
||||
* @param addr Register address.
|
||||
* @param reg Pointer to store the read value.
|
||||
* @return 0 on success.
|
||||
*/
|
||||
static int holding_reg_rd(uint16_t addr, uint16_t *reg)
|
||||
{
|
||||
reset_watchdog();
|
||||
switch (addr)
|
||||
{
|
||||
switch (addr) {
|
||||
case REG_HOLDING_MAX_OPENING_TIME_S:
|
||||
*reg = valve_get_max_open_time();
|
||||
break;
|
||||
@@ -51,6 +81,12 @@ static int holding_reg_rd(uint16_t addr, uint16_t *reg)
|
||||
case REG_HOLDING_WATCHDOG_TIMEOUT_S:
|
||||
*reg = watchdog_timeout_s;
|
||||
break;
|
||||
case REG_HOLDING_END_CURRENT_THRESHOLD_OPEN_MA:
|
||||
*reg = valve_get_end_current_threshold_open();
|
||||
break;
|
||||
case REG_HOLDING_END_CURRENT_THRESHOLD_CLOSE_MA:
|
||||
*reg = valve_get_end_current_threshold_close();
|
||||
break;
|
||||
default:
|
||||
*reg = 0;
|
||||
break;
|
||||
@@ -58,22 +94,23 @@ static int holding_reg_rd(uint16_t addr, uint16_t *reg)
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Callback for writing Modbus holding registers.
|
||||
*
|
||||
* @param addr Register address.
|
||||
* @param reg Value to write.
|
||||
* @return 0 on success.
|
||||
*/
|
||||
static int holding_reg_wr(uint16_t addr, uint16_t reg)
|
||||
{
|
||||
reset_watchdog();
|
||||
switch (addr)
|
||||
{
|
||||
switch (addr) {
|
||||
case REG_HOLDING_VALVE_COMMAND:
|
||||
if (reg == 1)
|
||||
{
|
||||
if (reg == 1) {
|
||||
valve_open();
|
||||
}
|
||||
else if (reg == 2)
|
||||
{
|
||||
} else if (reg == 2) {
|
||||
valve_close();
|
||||
}
|
||||
else if (reg == 0)
|
||||
{
|
||||
} else if (reg == 0) {
|
||||
valve_stop();
|
||||
}
|
||||
break;
|
||||
@@ -83,22 +120,24 @@ static int holding_reg_wr(uint16_t addr, uint16_t reg)
|
||||
case REG_HOLDING_MAX_CLOSING_TIME_S:
|
||||
valve_set_max_close_time(reg);
|
||||
break;
|
||||
case REG_HOLDING_END_CURRENT_THRESHOLD_OPEN_MA:
|
||||
valve_set_end_current_threshold_open(reg);
|
||||
break;
|
||||
case REG_HOLDING_END_CURRENT_THRESHOLD_CLOSE_MA:
|
||||
valve_set_end_current_threshold_close(reg);
|
||||
break;
|
||||
case REG_HOLDING_WATCHDOG_TIMEOUT_S:
|
||||
watchdog_timeout_s = reg;
|
||||
if (watchdog_timeout_s > 0)
|
||||
{
|
||||
if (watchdog_timeout_s > 0) {
|
||||
LOG_INF("Watchdog enabled with %u s timeout.", watchdog_timeout_s);
|
||||
reset_watchdog();
|
||||
}
|
||||
else
|
||||
{
|
||||
} else {
|
||||
LOG_INF("Watchdog disabled.");
|
||||
k_timer_stop(&watchdog_timer);
|
||||
}
|
||||
break;
|
||||
case REG_HOLDING_DEVICE_RESET:
|
||||
if (reg == 1)
|
||||
{
|
||||
if (reg == 1) {
|
||||
LOG_WRN("Modbus reset command received. Rebooting...");
|
||||
sys_reboot(SYS_REBOOT_WARM);
|
||||
}
|
||||
@@ -110,17 +149,26 @@ static int holding_reg_wr(uint16_t addr, uint16_t reg)
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Callback for reading Modbus input registers.
|
||||
*
|
||||
* @param addr Register address.
|
||||
* @param reg Pointer to store the read value.
|
||||
* @return 0 on success.
|
||||
*/
|
||||
static int input_reg_rd(uint16_t addr, uint16_t *reg)
|
||||
{
|
||||
reset_watchdog();
|
||||
uint32_t uptime_s = k_uptime_get_32() / 1000;
|
||||
switch (addr)
|
||||
{
|
||||
switch (addr) {
|
||||
case REG_INPUT_VALVE_STATE_MOVEMENT:
|
||||
*reg = (valve_get_movement() << 8) | (valve_get_state() & 0xFF);
|
||||
break;
|
||||
case REG_INPUT_MOTOR_CURRENT_MA:
|
||||
*reg = valve_get_motor_current();
|
||||
case REG_INPUT_MOTOR_OPEN_CURRENT_MA:
|
||||
*reg = (uint16_t)valve_get_opening_current();
|
||||
break;
|
||||
case REG_INPUT_MOTOR_CLOSE_CURRENT_MA:
|
||||
*reg = (uint16_t)valve_get_closing_current();
|
||||
break;
|
||||
case REG_INPUT_UPTIME_SECONDS_LOW:
|
||||
*reg = (uint16_t)(uptime_s & 0xFFFF);
|
||||
@@ -128,14 +176,23 @@ static int input_reg_rd(uint16_t addr, uint16_t *reg)
|
||||
case REG_INPUT_UPTIME_SECONDS_HIGH:
|
||||
*reg = (uint16_t)(uptime_s >> 16);
|
||||
break;
|
||||
case REG_INPUT_SUPPLY_VOLTAGE_MV:
|
||||
*reg = (uint16_t)valve_get_vnd_voltage();
|
||||
break;
|
||||
case REG_INPUT_FWU_LAST_CHUNK_CRC:
|
||||
*reg = fwu_get_last_chunk_crc();
|
||||
break;
|
||||
case REG_INPUT_FIRMWARE_VERSION_MAJOR_MINOR:
|
||||
*reg = (0 << 8) | 0;
|
||||
*reg = (APP_VERSION_MAJOR << 8) | APP_VERSION_MINOR;
|
||||
break;
|
||||
case REG_INPUT_FIRMWARE_VERSION_PATCH:
|
||||
*reg = 2;
|
||||
*reg = APP_PATCHLEVEL;
|
||||
break;
|
||||
case REG_HOLDING_END_CURRENT_THRESHOLD_OPEN_MA:
|
||||
*reg = valve_get_end_current_threshold_open();
|
||||
break;
|
||||
case REG_HOLDING_END_CURRENT_THRESHOLD_CLOSE_MA:
|
||||
*reg = valve_get_end_current_threshold_close();
|
||||
break;
|
||||
default:
|
||||
*reg = 0;
|
||||
@@ -145,9 +202,10 @@ static int input_reg_rd(uint16_t addr, uint16_t *reg)
|
||||
}
|
||||
|
||||
static struct modbus_user_callbacks mbs_cbs = {
|
||||
.holding_reg_rd = holding_reg_rd,
|
||||
.holding_reg_wr = holding_reg_wr,
|
||||
.input_reg_rd = input_reg_rd,
|
||||
// Modbus server callback functions
|
||||
.holding_reg_rd = holding_reg_rd,
|
||||
.holding_reg_wr = holding_reg_wr,
|
||||
.input_reg_rd = input_reg_rd,
|
||||
};
|
||||
|
||||
#define MODBUS_NODE DT_COMPAT_GET_ANY_STATUS_OKAY(zephyr_modbus_serial)
|
||||
@@ -155,18 +213,27 @@ static struct modbus_user_callbacks mbs_cbs = {
|
||||
int modbus_server_init(void)
|
||||
{
|
||||
k_timer_init(&watchdog_timer, watchdog_timer_handler, NULL);
|
||||
|
||||
// Load saved settings
|
||||
uint32_t saved_baudrate = 19200;
|
||||
uint8_t saved_unit_id = 1;
|
||||
settings_load_one("modbus/baudrate", &saved_baudrate, sizeof(saved_baudrate));
|
||||
settings_load_one("modbus/unit_id", &saved_unit_id, sizeof(saved_unit_id));
|
||||
|
||||
// Apply loaded settings
|
||||
server_param.serial.baud = saved_baudrate;
|
||||
server_param.server.unit_id = saved_unit_id;
|
||||
|
||||
const char iface_name[] = {DEVICE_DT_NAME(MODBUS_NODE)};
|
||||
#if DT_NODE_HAS_COMPAT(DT_PARENT(MODBUS_NODE), zephyr_cdc_acm_uart)
|
||||
const struct device *const dev = DEVICE_DT_GET(DT_PARENT(MODBUS_NODE));
|
||||
uint32_t dtr = 0;
|
||||
|
||||
if (!device_is_ready(dev) || usb_enable(NULL))
|
||||
{
|
||||
if (!device_is_ready(dev) || usb_enable(NULL)) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
while (!dtr)
|
||||
{
|
||||
while (!dtr) {
|
||||
uart_line_ctrl_get(dev, UART_LINE_CTRL_DTR, &dtr);
|
||||
k_sleep(K_MSEC(100));
|
||||
}
|
||||
@@ -174,29 +241,49 @@ int modbus_server_init(void)
|
||||
LOG_INF("Client connected to server on %s", dev->name);
|
||||
#endif
|
||||
modbus_iface = modbus_iface_get_by_name(iface_name);
|
||||
if (modbus_iface < 0)
|
||||
{
|
||||
if (modbus_iface < 0) {
|
||||
return modbus_iface;
|
||||
}
|
||||
server_param.server.user_cb = &mbs_cbs;
|
||||
|
||||
LOG_INF("Starting Modbus server: baudrate=%u, unit_id=%u", saved_baudrate, saved_unit_id);
|
||||
return modbus_init_server(modbus_iface, server_param);
|
||||
}
|
||||
|
||||
int modbus_reconfigure(uint32_t baudrate, uint8_t unit_id)
|
||||
{
|
||||
// Update parameters
|
||||
server_param.serial.baud = baudrate;
|
||||
server_param.server.unit_id = unit_id;
|
||||
|
||||
// Try to reinitialize - this should work for most cases
|
||||
int ret = modbus_init_server(modbus_iface, server_param);
|
||||
|
||||
if (ret == 0)
|
||||
{
|
||||
if (ret == 0) {
|
||||
settings_save_one("modbus/baudrate", &baudrate, sizeof(baudrate));
|
||||
settings_save_one("modbus/unit_id", &unit_id, sizeof(unit_id));
|
||||
LOG_INF("Modbus reconfigured: baudrate=%u, unit_id=%u", baudrate, unit_id);
|
||||
} else {
|
||||
LOG_ERR("Failed to reconfigure Modbus: %d", ret);
|
||||
LOG_INF("Modbus reconfiguration requires restart to take effect");
|
||||
|
||||
// Save settings for next boot
|
||||
settings_save_one("modbus/baudrate", &baudrate, sizeof(baudrate));
|
||||
settings_save_one("modbus/unit_id", &unit_id, sizeof(unit_id));
|
||||
|
||||
LOG_INF("Settings saved. Type 'reset' to restart the device and apply the "
|
||||
"change.");
|
||||
return 0; // Return success since settings are saved
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
uint32_t modbus_get_baudrate(void) { return server_param.serial.baud; }
|
||||
uint8_t modbus_get_unit_id(void) { return server_param.server.unit_id; }
|
||||
uint32_t modbus_get_baudrate(void)
|
||||
{
|
||||
return server_param.serial.baud;
|
||||
}
|
||||
uint8_t modbus_get_unit_id(void)
|
||||
{
|
||||
return server_param.server.unit_id;
|
||||
}
|
||||
@@ -1,5 +1,7 @@
|
||||
config SHELL_MODBUS
|
||||
bool "Enable Shell Modbus"
|
||||
default y
|
||||
default n
|
||||
depends on SHELL
|
||||
depends on LIB_MODBUS_SERVER
|
||||
help
|
||||
Enable the modnbus shell commands.
|
||||
Enable the modbus shell commands.
|
||||
@@ -1,12 +1,29 @@
|
||||
#include <zephyr/shell/shell.h>
|
||||
#include <stdlib.h>
|
||||
#include <lib/modbus_server.h>
|
||||
#include <lib/valve.h>
|
||||
/**
|
||||
* @file shell_modbus.c
|
||||
* @brief Provides shell commands for Modbus and valve configuration.
|
||||
*
|
||||
* This file implements a set of commands for the Zephyr shell to allow
|
||||
* runtime configuration of the Modbus server (baudrate, slave ID) and the
|
||||
* valve (max opening/closing times). The settings are persisted to non-volatile
|
||||
* storage.
|
||||
*/
|
||||
|
||||
static int cmd_modbus_set_baud(const struct shell *sh, size_t argc, char **argv)
|
||||
#include <zephyr/shell/shell.h>
|
||||
#include <lib/modbus_server.h>
|
||||
#include <stdlib.h>
|
||||
|
||||
/**
|
||||
* @brief Shell command to set the Modbus baudrate.
|
||||
*
|
||||
* @param sh The shell instance.
|
||||
* @param argc Argument count.
|
||||
* @param argv Argument values.
|
||||
* @return 0 on success, -EINVAL on error.
|
||||
*/
|
||||
static int cmd_modbus_setb(const struct shell *sh, size_t argc, char **argv)
|
||||
{
|
||||
if (argc != 2) {
|
||||
shell_error(sh, "Usage: set_baud <baudrate>");
|
||||
shell_error(sh, "Usage: setb <baudrate>");
|
||||
return -EINVAL;
|
||||
}
|
||||
|
||||
@@ -23,9 +40,13 @@ static int cmd_modbus_set_baud(const struct shell *sh, size_t argc, char **argv)
|
||||
|
||||
if (!is_valid) {
|
||||
char error_msg[128];
|
||||
int offset = snprintf(error_msg, sizeof(error_msg), "Invalid baudrate. Valid rates are: ");
|
||||
int offset =
|
||||
snprintf(error_msg, sizeof(error_msg), "Invalid baudrate. Valid rates are: ");
|
||||
for (int i = 0; i < ARRAY_SIZE(valid_baud_rates); i++) {
|
||||
offset += snprintf(error_msg + offset, sizeof(error_msg) - offset, "%u ", valid_baud_rates[i]);
|
||||
offset += snprintf(error_msg + offset,
|
||||
sizeof(error_msg) - offset,
|
||||
"%u ",
|
||||
valid_baud_rates[i]);
|
||||
}
|
||||
shell_error(sh, "%s", error_msg);
|
||||
return -EINVAL;
|
||||
@@ -40,10 +61,18 @@ static int cmd_modbus_set_baud(const struct shell *sh, size_t argc, char **argv)
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int cmd_modbus_set_id(const struct shell *sh, size_t argc, char **argv)
|
||||
/**
|
||||
* @brief Shell command to set the Modbus slave ID.
|
||||
*
|
||||
* @param sh The shell instance.
|
||||
* @param argc Argument count.
|
||||
* @param argv Argument values.
|
||||
* @return 0 on success, -EINVAL on error.
|
||||
*/
|
||||
static int cmd_modbus_setid(const struct shell *sh, size_t argc, char **argv)
|
||||
{
|
||||
if (argc != 2) {
|
||||
shell_error(sh, "Usage: set_id <slave_id>");
|
||||
shell_error(sh, "Usage: setid <slave_id>");
|
||||
return -EINVAL;
|
||||
}
|
||||
|
||||
@@ -63,57 +92,28 @@ static int cmd_modbus_set_id(const struct shell *sh, size_t argc, char **argv)
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int cmd_valve_set_open_time(const struct shell *sh, size_t argc, char **argv)
|
||||
/**
|
||||
* @brief Shell command to show the current Modbus configuration.
|
||||
*
|
||||
* @param sh The shell instance.
|
||||
* @param argc Argument count.
|
||||
* @param argv Argument values.
|
||||
* @return 0 on success.
|
||||
*/
|
||||
static int cmd_modbus_show(const struct shell *sh, size_t argc, char **argv)
|
||||
{
|
||||
if (argc != 2) {
|
||||
shell_error(sh, "Usage: set_open_time <seconds>");
|
||||
return -EINVAL;
|
||||
}
|
||||
const int label_width = 15;
|
||||
|
||||
uint16_t seconds = (uint16_t)strtoul(argv[1], NULL, 10);
|
||||
valve_set_max_open_time(seconds);
|
||||
shell_print(sh, "Max opening time set to: %u seconds (and saved)", seconds);
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int cmd_valve_set_close_time(const struct shell *sh, size_t argc, char **argv)
|
||||
{
|
||||
if (argc != 2) {
|
||||
shell_error(sh, "Usage: set_close_time <seconds>");
|
||||
return -EINVAL;
|
||||
}
|
||||
|
||||
uint16_t seconds = (uint16_t)strtoul(argv[1], NULL, 10);
|
||||
valve_set_max_close_time(seconds);
|
||||
shell_print(sh, "Max closing time set to: %u seconds (and saved)", seconds);
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int cmd_config_show(const struct shell *sh, size_t argc, char **argv)
|
||||
{
|
||||
shell_print(sh, "Current Modbus Configuration:");
|
||||
shell_print(sh, " Baudrate: %u", modbus_get_baudrate());
|
||||
shell_print(sh, " Slave ID: %u", modbus_get_unit_id());
|
||||
shell_print(sh, "Current Valve Configuration:");
|
||||
shell_print(sh, " Max Opening Time: %u s", valve_get_max_open_time());
|
||||
shell_print(sh, " Max Closing Time: %u s", valve_get_max_close_time());
|
||||
shell_print(sh, "Modbus Settings:");
|
||||
shell_print(sh, "%*s %u", label_width, "Baudrate:", modbus_get_baudrate());
|
||||
shell_print(sh, "%*s %u", label_width, "Slave ID:", modbus_get_unit_id());
|
||||
return 0;
|
||||
}
|
||||
|
||||
SHELL_STATIC_SUBCMD_SET_CREATE(sub_modbus_cmds,
|
||||
SHELL_CMD(set_baud, NULL, "Set Modbus baudrate", cmd_modbus_set_baud),
|
||||
SHELL_CMD(set_id, NULL, "Set Modbus slave ID", cmd_modbus_set_id),
|
||||
SHELL_SUBCMD_SET_END
|
||||
);
|
||||
SHELL_CMD(setb, NULL, "Set Modbus baudrate", cmd_modbus_setb),
|
||||
SHELL_CMD(setid, NULL, "Set Modbus slave ID", cmd_modbus_setid),
|
||||
SHELL_CMD(show, NULL, "Show Modbus configuration", cmd_modbus_show),
|
||||
SHELL_SUBCMD_SET_END);
|
||||
|
||||
SHELL_STATIC_SUBCMD_SET_CREATE(sub_valve_cmds,
|
||||
SHELL_CMD(set_open_time, NULL, "Set max valve opening time", cmd_valve_set_open_time),
|
||||
SHELL_CMD(set_close_time, NULL, "Set max valve closing time", cmd_valve_set_close_time),
|
||||
SHELL_SUBCMD_SET_END
|
||||
);
|
||||
|
||||
SHELL_CMD_REGISTER(modbus, &sub_modbus_cmds, "Modbus configuration", NULL);
|
||||
SHELL_CMD_REGISTER(valve, &sub_valve_cmds, "Valve configuration", NULL);
|
||||
SHELL_CMD_REGISTER(show_config, NULL, "Show all configurations", cmd_config_show);
|
||||
SHELL_CMD_REGISTER(modbus, &sub_modbus_cmds, "Modbus commands", NULL);
|
||||
@@ -1,5 +1,5 @@
|
||||
config SHELL_SYSTEM
|
||||
bool "Enable Shell System"
|
||||
default y
|
||||
default n
|
||||
help
|
||||
Enable the system commands.
|
||||
@@ -1,6 +1,25 @@
|
||||
/**
|
||||
* @file shell_system.c
|
||||
* @brief Provides basic system-level shell commands.
|
||||
*
|
||||
* This file implements essential system commands for the Zephyr shell,
|
||||
* such as rebooting the device.
|
||||
*/
|
||||
|
||||
#include <zephyr/shell/shell.h>
|
||||
#include <zephyr/sys/reboot.h>
|
||||
|
||||
/**
|
||||
* @brief Shell command to reset the system.
|
||||
*
|
||||
* This command performs a warm reboot of the device after a short delay
|
||||
* to ensure the shell message is printed.
|
||||
*
|
||||
* @param sh The shell instance.
|
||||
* @param argc Argument count.
|
||||
* @param argv Argument values.
|
||||
* @return 0 on success.
|
||||
*/
|
||||
static int cmd_reset(const struct shell *sh, size_t argc, char **argv)
|
||||
{
|
||||
shell_print(sh, "Rebooting system...");
|
||||
|
||||
1
software/lib/shell_valve/CMakeLists.txt
Normal file
1
software/lib/shell_valve/CMakeLists.txt
Normal file
@@ -0,0 +1 @@
|
||||
zephyr_library_sources(shell_valve.c)
|
||||
7
software/lib/shell_valve/Kconfig
Normal file
7
software/lib/shell_valve/Kconfig
Normal file
@@ -0,0 +1,7 @@
|
||||
config SHELL_VALVE
|
||||
bool "Shell Valve commands"
|
||||
default n
|
||||
depends on SHELL
|
||||
depends on LIB_VALVE
|
||||
help
|
||||
Enable the valve shell commands.
|
||||
92
software/lib/shell_valve/shell_valve.c
Normal file
92
software/lib/shell_valve/shell_valve.c
Normal file
@@ -0,0 +1,92 @@
|
||||
#include <zephyr/kernel.h>
|
||||
#include <zephyr/shell/shell.h>
|
||||
#include <lib/valve.h>
|
||||
#include <stdlib.h>
|
||||
|
||||
static int cmd_valve_set_open_t(const struct shell *sh, size_t argc, char **argv)
|
||||
{
|
||||
if (argc != 2) {
|
||||
shell_print(sh, "Usage: valve set_open_t <seconds>");
|
||||
return -EINVAL;
|
||||
}
|
||||
|
||||
uint16_t seconds = (uint16_t)atoi(argv[1]);
|
||||
valve_set_max_open_time(seconds);
|
||||
shell_print(sh, "Max open time set to %u seconds.", seconds);
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int cmd_valve_set_close_t(const struct shell *sh, size_t argc, char **argv)
|
||||
{
|
||||
if (argc != 2) {
|
||||
shell_print(sh, "Usage: valve set_close_t <seconds>");
|
||||
return -EINVAL;
|
||||
}
|
||||
|
||||
uint16_t seconds = (uint16_t)atoi(argv[1]);
|
||||
valve_set_max_close_time(seconds);
|
||||
shell_print(sh, "Max close time set to %u seconds.", seconds);
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int cmd_valve_set_end_curr_open(const struct shell *sh, size_t argc, char **argv)
|
||||
{
|
||||
if (argc != 2) {
|
||||
shell_print(sh, "Usage: valve set_end_curr_open <milliamps>");
|
||||
return -EINVAL;
|
||||
}
|
||||
|
||||
uint16_t current_ma = (uint16_t)atoi(argv[1]);
|
||||
valve_set_end_current_threshold_open(current_ma);
|
||||
shell_print(sh, "End current threshold (open) set to %u mA.", current_ma);
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int cmd_valve_set_end_curr_close(const struct shell *sh, size_t argc, char **argv)
|
||||
{
|
||||
if (argc != 2) {
|
||||
shell_print(sh, "Usage: valve set_end_curr_close <milliamps>");
|
||||
return -EINVAL;
|
||||
}
|
||||
|
||||
uint16_t current_ma = (uint16_t)atoi(argv[1]);
|
||||
valve_set_end_current_threshold_close(current_ma);
|
||||
shell_print(sh, "End current threshold (close) set to %u mA.", current_ma);
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int cmd_valve_show(const struct shell *sh, size_t argc, char **argv)
|
||||
{
|
||||
const int label_width = 30;
|
||||
|
||||
shell_print(sh, "Valve Settings:");
|
||||
shell_print(sh, "%*s %u s", label_width, "Max Open Time:", valve_get_max_open_time());
|
||||
shell_print(sh, "%*s %u s", label_width, "Max Close Time:", valve_get_max_close_time());
|
||||
shell_print(sh,
|
||||
"%*s %u mA",
|
||||
label_width,
|
||||
"End Current Threshold (Open):",
|
||||
valve_get_end_current_threshold_open());
|
||||
shell_print(sh,
|
||||
"%*s %u mA",
|
||||
label_width,
|
||||
"End Current Threshold (Close):",
|
||||
valve_get_end_current_threshold_close());
|
||||
return 0;
|
||||
}
|
||||
|
||||
SHELL_STATIC_SUBCMD_SET_CREATE(sub_valve_settings,
|
||||
SHELL_CMD(set_open_t, NULL, "Set max open time (seconds)", cmd_valve_set_open_t),
|
||||
SHELL_CMD(set_close_t, NULL, "Set max close time (seconds)", cmd_valve_set_close_t),
|
||||
SHELL_CMD(set_end_curr_open,
|
||||
NULL,
|
||||
"Set end current threshold for opening (mA)",
|
||||
cmd_valve_set_end_curr_open),
|
||||
SHELL_CMD(set_end_curr_close,
|
||||
NULL,
|
||||
"Set end current threshold for closing (mA)",
|
||||
cmd_valve_set_end_curr_close),
|
||||
SHELL_CMD(show, NULL, "Show valve configuration", cmd_valve_show),
|
||||
SHELL_SUBCMD_SET_END);
|
||||
|
||||
SHELL_CMD_REGISTER(valve, &sub_valve_settings, "Valve commands", NULL);
|
||||
@@ -3,3 +3,12 @@ config LIB_VALVE
|
||||
default y
|
||||
help
|
||||
Enable the Valve Library.
|
||||
|
||||
if LIB_VALVE
|
||||
config LOG_VALVE_LEVEL
|
||||
int "Valve Log Level"
|
||||
default 3
|
||||
help
|
||||
Set the log level for the Valve Library.
|
||||
0 = None, 1 = Error, 2 = Warning, 3 = Info, 4 = Debug
|
||||
endif # LIB_VALVE
|
||||
|
||||
@@ -1,62 +1,257 @@
|
||||
/**
|
||||
* @file valve.c
|
||||
* @brief Implementation of the motorized valve control library.
|
||||
*
|
||||
* This file contains the logic for controlling a motorized valve using a
|
||||
* VND7050AJ high-side driver. It uses a delayed work item to handle the
|
||||
* safety timeouts for opening and closing operations.
|
||||
*/
|
||||
|
||||
#include <zephyr/device.h>
|
||||
#include <zephyr/drivers/gpio.h>
|
||||
#include <zephyr/drivers/misc/vnd7050aj/vnd7050aj.h>
|
||||
#include <zephyr/kernel.h>
|
||||
#include <zephyr/settings/settings.h>
|
||||
#include <zephyr/logging/log.h>
|
||||
#include <zephyr/settings/settings.h>
|
||||
#include <lib/valve.h>
|
||||
|
||||
LOG_MODULE_REGISTER(valve, LOG_LEVEL_INF);
|
||||
#define VND_NODE DT_ALIAS(vnd7050aj)
|
||||
#if !DT_NODE_HAS_STATUS(VND_NODE, okay)
|
||||
#error VND7050AJ node is not defined or enabled
|
||||
#endif
|
||||
const struct device *vnd7050aj_dev = DEVICE_DT_GET(VND_NODE);
|
||||
|
||||
static enum valve_state current_state = VALVE_STATE_CLOSED;
|
||||
LOG_MODULE_REGISTER(valve, CONFIG_LOG_VALVE_LEVEL);
|
||||
|
||||
static enum valve_state current_state = VALVE_STATE_OPEN;
|
||||
static enum valve_movement current_movement = VALVE_MOVEMENT_IDLE;
|
||||
static uint16_t max_opening_time_s = 60;
|
||||
static uint16_t max_closing_time_s = 60;
|
||||
static uint16_t max_opening_time_s = 10;
|
||||
static uint16_t max_closing_time_s = 10;
|
||||
static uint16_t end_current_threshold_open_ma = 10;
|
||||
static uint16_t end_current_threshold_close_ma = 10;
|
||||
static struct k_work_delayable valve_work;
|
||||
static struct k_timer movement_timer;
|
||||
|
||||
/**
|
||||
* @brief Work handler for end position checks of the valve.
|
||||
*
|
||||
* This function is called periodically to check if the valve has reached its
|
||||
* end position. It reads the current load on the motor and determines if the
|
||||
* valve has reached its target position.
|
||||
*
|
||||
* @param work Pointer to the k_work item.
|
||||
*/
|
||||
static void valve_work_handler(struct k_work *work)
|
||||
{
|
||||
if (current_movement == VALVE_MOVEMENT_OPENING) {
|
||||
LOG_INF("Virtual valve finished opening");
|
||||
} else if (current_movement == VALVE_MOVEMENT_CLOSING) {
|
||||
current_state = VALVE_STATE_CLOSED;
|
||||
LOG_INF("Virtual valve finished closing");
|
||||
}
|
||||
current_movement = VALVE_MOVEMENT_IDLE;
|
||||
int current_ma = 0;
|
||||
|
||||
if (current_movement == VALVE_MOVEMENT_OPENING) {
|
||||
vnd7050aj_read_load_current(vnd7050aj_dev, VALVE_CHANNEL_OPEN, ¤t_ma);
|
||||
LOG_DBG("Current load during opening: %d mA", current_ma);
|
||||
if (current_ma > VALVE_OBSTACLE_THRESHOLD_OPEN_MA) {
|
||||
LOG_ERR(
|
||||
"Obstacle detected during opening (current: %d mA), stopping motor.",
|
||||
current_ma);
|
||||
current_movement = VALVE_MOVEMENT_ERROR;
|
||||
valve_stop();
|
||||
return;
|
||||
} else if (current_ma > end_current_threshold_open_ma) {
|
||||
k_work_schedule(&valve_work, VALVE_ENDPOSITION_CHECK_INTERVAL);
|
||||
return;
|
||||
}
|
||||
LOG_DBG("Valve finished opening");
|
||||
} else if (current_movement == VALVE_MOVEMENT_CLOSING) {
|
||||
vnd7050aj_read_load_current(vnd7050aj_dev, VALVE_CHANNEL_CLOSE, ¤t_ma);
|
||||
LOG_DBG("Current load during closing: %d mA", current_ma);
|
||||
if (current_ma > VALVE_OBSTACLE_THRESHOLD_CLOSE_MA) {
|
||||
LOG_ERR(
|
||||
"Obstacle detected during closing (current: %d mA), stopping motor.",
|
||||
current_ma);
|
||||
current_movement = VALVE_MOVEMENT_ERROR;
|
||||
valve_stop();
|
||||
return;
|
||||
} else if (current_ma > end_current_threshold_close_ma) {
|
||||
k_work_schedule(&valve_work, VALVE_ENDPOSITION_CHECK_INTERVAL);
|
||||
return;
|
||||
}
|
||||
current_state = VALVE_STATE_CLOSED;
|
||||
LOG_DBG("Valve finished closing");
|
||||
}
|
||||
current_movement = VALVE_MOVEMENT_IDLE;
|
||||
|
||||
valve_stop();
|
||||
}
|
||||
|
||||
void valve_init(void)
|
||||
/**
|
||||
* @brief Timer handler for valve movement timeouts.
|
||||
*
|
||||
* This function is called when the maximum allowed time for valve movement
|
||||
* (opening or closing) has been reached. It stops the valve motor, cancels
|
||||
* any pending end-position checks, and sets the movement status to error.
|
||||
*
|
||||
* @param timer Pointer to the k_timer instance that expired.
|
||||
*/
|
||||
void movement_timeout_handler(struct k_timer *timer)
|
||||
{
|
||||
k_work_init_delayable(&valve_work, valve_work_handler);
|
||||
settings_load_one("valve/max_open_time", &max_opening_time_s, sizeof(max_opening_time_s));
|
||||
settings_load_one("valve/max_close_time", &max_closing_time_s, sizeof(max_closing_time_s));
|
||||
// Stop the end position check if the timer expires
|
||||
k_work_cancel_delayable(&valve_work);
|
||||
if (current_movement == VALVE_MOVEMENT_OPENING) {
|
||||
LOG_WRN("Valve opening timeout reached, stopping motor.");
|
||||
current_movement = VALVE_MOVEMENT_ERROR;
|
||||
} else if (current_movement == VALVE_MOVEMENT_CLOSING) {
|
||||
LOG_WRN("Valve closing timeout reached, stopping motor.");
|
||||
current_movement = VALVE_MOVEMENT_ERROR;
|
||||
}
|
||||
vnd7050aj_set_output_state(vnd7050aj_dev, VALVE_CHANNEL_OPEN, false);
|
||||
vnd7050aj_set_output_state(vnd7050aj_dev, VALVE_CHANNEL_CLOSE, false);
|
||||
current_state = VALVE_STATE_CLOSED;
|
||||
}
|
||||
|
||||
int valve_init(void)
|
||||
{
|
||||
if (!device_is_ready(vnd7050aj_dev)) {
|
||||
LOG_ERR("VND7050AJ device is not ready");
|
||||
return -ENODEV;
|
||||
}
|
||||
|
||||
k_work_init_delayable(&valve_work, valve_work_handler);
|
||||
k_timer_init(&movement_timer, movement_timeout_handler, NULL);
|
||||
|
||||
settings_load_one("valve/max_open_time", &max_opening_time_s, sizeof(max_opening_time_s));
|
||||
settings_load_one("valve/max_close_time", &max_closing_time_s, sizeof(max_closing_time_s));
|
||||
settings_load_one("valve/end_current_threshold_open",
|
||||
&end_current_threshold_open_ma,
|
||||
sizeof(end_current_threshold_open_ma));
|
||||
settings_load_one("valve/end_current_threshold_close",
|
||||
&end_current_threshold_close_ma,
|
||||
sizeof(end_current_threshold_close_ma));
|
||||
|
||||
LOG_INF("Valve initialized: max_open=%us, max_close=%us, end_curr_open=%umA, "
|
||||
"end_curr_close=%umA",
|
||||
max_opening_time_s,
|
||||
max_closing_time_s,
|
||||
end_current_threshold_open_ma,
|
||||
end_current_threshold_close_ma);
|
||||
valve_close();
|
||||
return 0;
|
||||
}
|
||||
|
||||
void valve_open(void)
|
||||
{
|
||||
if (current_state == VALVE_STATE_CLOSED) {
|
||||
current_state = VALVE_STATE_OPEN;
|
||||
current_movement = VALVE_MOVEMENT_OPENING;
|
||||
k_work_schedule(&valve_work, K_SECONDS(max_opening_time_s));
|
||||
}
|
||||
LOG_DBG("Opening valve");
|
||||
vnd7050aj_reset_fault(vnd7050aj_dev);
|
||||
vnd7050aj_set_output_state(vnd7050aj_dev, VALVE_CHANNEL_CLOSE, false);
|
||||
vnd7050aj_set_output_state(vnd7050aj_dev, VALVE_CHANNEL_OPEN, true);
|
||||
current_state =
|
||||
VALVE_STATE_OPEN; /* Security: assume valve open as soon as it starts opening */
|
||||
current_movement = VALVE_MOVEMENT_OPENING;
|
||||
if (max_opening_time_s > 0) {
|
||||
k_timer_start(&movement_timer, K_SECONDS(max_opening_time_s), K_NO_WAIT);
|
||||
}
|
||||
k_work_schedule(&valve_work, K_MSEC(100));
|
||||
}
|
||||
|
||||
void valve_close(void)
|
||||
{
|
||||
if (current_state == VALVE_STATE_OPEN) {
|
||||
current_movement = VALVE_MOVEMENT_CLOSING;
|
||||
k_work_schedule(&valve_work, K_SECONDS(max_closing_time_s));
|
||||
}
|
||||
LOG_DBG("Closing valve");
|
||||
vnd7050aj_reset_fault(vnd7050aj_dev);
|
||||
vnd7050aj_set_output_state(vnd7050aj_dev, VALVE_CHANNEL_OPEN, false);
|
||||
vnd7050aj_set_output_state(vnd7050aj_dev, VALVE_CHANNEL_CLOSE, true);
|
||||
if (max_closing_time_s > 0) {
|
||||
k_timer_start(&movement_timer, K_SECONDS(max_closing_time_s), K_NO_WAIT);
|
||||
}
|
||||
current_movement = VALVE_MOVEMENT_CLOSING;
|
||||
k_work_schedule(&valve_work, VALVE_ENDPOSITION_CHECK_INTERVAL);
|
||||
}
|
||||
|
||||
void valve_stop(void)
|
||||
{
|
||||
k_work_cancel_delayable(&valve_work);
|
||||
current_movement = VALVE_MOVEMENT_IDLE;
|
||||
k_work_cancel_delayable(&valve_work);
|
||||
k_timer_stop(&movement_timer);
|
||||
vnd7050aj_set_output_state(vnd7050aj_dev, VALVE_CHANNEL_OPEN, false);
|
||||
vnd7050aj_set_output_state(vnd7050aj_dev, VALVE_CHANNEL_CLOSE, false);
|
||||
current_movement = VALVE_MOVEMENT_IDLE;
|
||||
}
|
||||
|
||||
enum valve_state valve_get_state(void) { return current_state; }
|
||||
enum valve_movement valve_get_movement(void) { return current_movement; }
|
||||
uint16_t valve_get_motor_current(void) { return (current_movement != VALVE_MOVEMENT_IDLE) ? 150 : 10; }
|
||||
enum valve_state valve_get_state(void)
|
||||
{
|
||||
return current_state;
|
||||
}
|
||||
enum valve_movement valve_get_movement(void)
|
||||
{
|
||||
return current_movement;
|
||||
}
|
||||
|
||||
void valve_set_max_open_time(uint16_t seconds) { max_opening_time_s = seconds; settings_save_one("valve/max_open_time", &max_opening_time_s, sizeof(max_opening_time_s)); }
|
||||
void valve_set_max_close_time(uint16_t seconds) { max_closing_time_s = seconds; settings_save_one("valve/max_close_time", &max_closing_time_s, sizeof(max_closing_time_s)); }
|
||||
uint16_t valve_get_max_open_time(void) { return max_opening_time_s; }
|
||||
uint16_t valve_get_max_close_time(void) { return max_closing_time_s; }
|
||||
void valve_set_max_open_time(uint16_t seconds)
|
||||
{
|
||||
max_opening_time_s = seconds;
|
||||
settings_save_one("valve/max_open_time", &max_opening_time_s, sizeof(max_opening_time_s));
|
||||
}
|
||||
void valve_set_max_close_time(uint16_t seconds)
|
||||
{
|
||||
max_closing_time_s = seconds;
|
||||
settings_save_one("valve/max_close_time", &max_closing_time_s, sizeof(max_closing_time_s));
|
||||
}
|
||||
|
||||
void valve_set_end_current_threshold_open(uint16_t current_ma)
|
||||
{
|
||||
end_current_threshold_open_ma = current_ma;
|
||||
settings_save_one("valve/end_current_threshold_open",
|
||||
&end_current_threshold_open_ma,
|
||||
sizeof(end_current_threshold_open_ma));
|
||||
}
|
||||
|
||||
void valve_set_end_current_threshold_close(uint16_t current_ma)
|
||||
{
|
||||
end_current_threshold_close_ma = current_ma;
|
||||
settings_save_one("valve/end_current_threshold_close",
|
||||
&end_current_threshold_close_ma,
|
||||
sizeof(end_current_threshold_close_ma));
|
||||
}
|
||||
|
||||
uint16_t valve_get_max_open_time(void)
|
||||
{
|
||||
return max_opening_time_s;
|
||||
}
|
||||
uint16_t valve_get_max_close_time(void)
|
||||
{
|
||||
return max_closing_time_s;
|
||||
}
|
||||
|
||||
uint16_t valve_get_end_current_threshold_open(void)
|
||||
{
|
||||
return end_current_threshold_open_ma;
|
||||
}
|
||||
|
||||
uint16_t valve_get_end_current_threshold_close(void)
|
||||
{
|
||||
return end_current_threshold_close_ma;
|
||||
}
|
||||
|
||||
int32_t valve_get_opening_current(void)
|
||||
{
|
||||
int32_t current;
|
||||
vnd7050aj_read_load_current(vnd7050aj_dev, VALVE_CHANNEL_OPEN, ¤t);
|
||||
return current;
|
||||
}
|
||||
|
||||
int32_t valve_get_closing_current(void)
|
||||
{
|
||||
int32_t current;
|
||||
vnd7050aj_read_load_current(vnd7050aj_dev, VALVE_CHANNEL_CLOSE, ¤t);
|
||||
return current;
|
||||
}
|
||||
|
||||
int32_t valve_get_vnd_temp(void)
|
||||
{
|
||||
int32_t temp_c;
|
||||
vnd7050aj_read_chip_temp(vnd7050aj_dev, &temp_c);
|
||||
return temp_c;
|
||||
}
|
||||
|
||||
int32_t valve_get_vnd_voltage(void)
|
||||
{
|
||||
int32_t voltage_mv;
|
||||
vnd7050aj_read_supply_voltage(vnd7050aj_dev, &voltage_mv);
|
||||
return voltage_mv;
|
||||
}
|
||||
|
||||
@@ -11,7 +11,8 @@ from pymodbus.exceptions import ModbusException
|
||||
# --- Register Definitions ---
|
||||
# (omitted for brevity, no changes here)
|
||||
REG_INPUT_VALVE_STATE_MOVEMENT = 0x0000
|
||||
REG_INPUT_MOTOR_CURRENT_MA = 0x0001
|
||||
REG_INPUT_MOTOR_OPEN_CURRENT_MA = 0x0001
|
||||
REG_INPUT_MOTOR_CLOSE_CURRENT_MA = 0x0002
|
||||
REG_INPUT_DIGITAL_INPUTS_STATE = 0x0020
|
||||
REG_INPUT_BUTTON_EVENTS = 0x0021
|
||||
REG_INPUT_FIRMWARE_VERSION_MAJOR_MINOR = 0x00F0
|
||||
@@ -19,10 +20,13 @@ REG_INPUT_FIRMWARE_VERSION_PATCH = 0x00F1
|
||||
REG_INPUT_DEVICE_STATUS = 0x00F2
|
||||
REG_INPUT_UPTIME_SECONDS_LOW = 0x00F3
|
||||
REG_INPUT_UPTIME_SECONDS_HIGH = 0x00F4
|
||||
REG_INPUT_SUPPLY_VOLTAGE_MV = 0x00F5
|
||||
REG_INPUT_FWU_LAST_CHUNK_CRC = 0x0100
|
||||
REG_HOLDING_VALVE_COMMAND = 0x0000
|
||||
REG_HOLDING_MAX_OPENING_TIME_S = 0x0001
|
||||
REG_HOLDING_MAX_CLOSING_TIME_S = 0x0002
|
||||
REG_HOLDING_END_CURRENT_THRESHOLD_OPEN_MA = 0x0003
|
||||
REG_HOLDING_END_CURRENT_THRESHOLD_CLOSE_MA = 0x0004
|
||||
REG_HOLDING_DIGITAL_OUTPUTS_STATE = 0x0010
|
||||
REG_HOLDING_WATCHDOG_TIMEOUT_S = 0x00F0
|
||||
REG_HOLDING_DEVICE_RESET = 0x00F1
|
||||
@@ -75,21 +79,24 @@ def poll_status(slave_id, interval):
|
||||
# Attempt to connect
|
||||
if client.connect():
|
||||
reconnect_attempts = 0
|
||||
new_data["error"] = None # Clear error on successful reconnect
|
||||
with status_lock:
|
||||
status_data["error"] = None # Clear error in status_data immediately
|
||||
time.sleep(0.1) # Allow UI to refresh with cleared error
|
||||
else:
|
||||
new_data["error"] = f"Connection lost. Attempting to reconnect ({reconnect_attempts}/{max_reconnect_attempts})..."
|
||||
time.sleep(reconnect_delay)
|
||||
continue
|
||||
|
||||
# If connected, try to read data
|
||||
ir_valve = client.read_input_registers(REG_INPUT_VALVE_STATE_MOVEMENT, count=2, slave=slave_id)
|
||||
ir_valve = client.read_input_registers(REG_INPUT_VALVE_STATE_MOVEMENT, count=1, slave=slave_id)
|
||||
ir_current = client.read_input_registers(REG_INPUT_MOTOR_OPEN_CURRENT_MA, count=2, slave=slave_id)
|
||||
ir_dig = client.read_input_registers(REG_INPUT_DIGITAL_INPUTS_STATE, count=2, slave=slave_id)
|
||||
ir_sys = client.read_input_registers(REG_INPUT_FIRMWARE_VERSION_MAJOR_MINOR, count=5, slave=slave_id)
|
||||
hr_valve = client.read_holding_registers(REG_HOLDING_MAX_OPENING_TIME_S, count=2, slave=slave_id)
|
||||
ir_sys = client.read_input_registers(REG_INPUT_FIRMWARE_VERSION_MAJOR_MINOR, count=6, slave=slave_id)
|
||||
hr_valve = client.read_holding_registers(REG_HOLDING_MAX_OPENING_TIME_S, count=4, slave=slave_id)
|
||||
hr_dig = client.read_holding_registers(REG_HOLDING_DIGITAL_OUTPUTS_STATE, count=1, slave=slave_id)
|
||||
hr_sys = client.read_holding_registers(REG_HOLDING_WATCHDOG_TIMEOUT_S, count=1, slave=slave_id)
|
||||
|
||||
for res in [ir_valve, ir_dig, ir_sys, hr_valve, hr_dig, hr_sys]:
|
||||
for res in [ir_valve, ir_current, ir_dig, ir_sys, hr_valve, hr_dig, hr_sys]:
|
||||
if res.isError():
|
||||
raise ModbusException(str(res))
|
||||
|
||||
@@ -98,9 +105,12 @@ def poll_status(slave_id, interval):
|
||||
state_map = {0: "Closed", 1: "Open"}
|
||||
new_data["movement"] = movement_map.get(valve_state_raw >> 8, 'Unknown')
|
||||
new_data["state"] = state_map.get(valve_state_raw & 0xFF, 'Unknown')
|
||||
new_data["motor_current"] = f"{ir_valve.registers[1]} mA"
|
||||
new_data["motor_current_open"] = f"{ir_current.registers[0]} mA"
|
||||
new_data["motor_current_close"] = f"{ir_current.registers[1]} mA"
|
||||
new_data["open_time"] = f"{hr_valve.registers[0]}s"
|
||||
new_data["close_time"] = f"{hr_valve.registers[1]}s"
|
||||
new_data["end_curr_open"] = f"{hr_valve.registers[2]}mA"
|
||||
new_data["end_curr_close"] = f"{hr_valve.registers[3]}mA"
|
||||
new_data["digital_inputs"] = f"0x{ir_dig.registers[0]:04X}"
|
||||
new_data["button_events"] = f"0x{ir_dig.registers[1]:04X}"
|
||||
new_data["digital_outputs"] = f"0x{hr_dig.registers[0]:04X}"
|
||||
@@ -109,9 +119,11 @@ def poll_status(slave_id, interval):
|
||||
fw_minor = ir_sys.registers[0] & 0xFF
|
||||
fw_patch = ir_sys.registers[1]
|
||||
uptime_seconds = (ir_sys.registers[4] << 16) | ir_sys.registers[3]
|
||||
supply_voltage_mv = ir_sys.registers[5]
|
||||
new_data["firmware"] = f"v{fw_major}.{fw_minor}.{fw_patch}"
|
||||
new_data["device_status"] = "OK" if ir_sys.registers[2] == 0 else "ERROR"
|
||||
new_data["uptime"] = format_uptime(uptime_seconds)
|
||||
new_data["supply_voltage"] = f"{supply_voltage_mv / 1000.0:.2f} V"
|
||||
new_data["watchdog"] = f"{hr_sys.registers[0]}s"
|
||||
new_data["error"] = None # Clear any previous error on successful read
|
||||
reconnect_attempts = 0 # Reset attempts on successful communication
|
||||
@@ -179,7 +191,7 @@ def file_browser(stdscr):
|
||||
selected_index = 0
|
||||
|
||||
while True:
|
||||
stdscr.clear()
|
||||
stdscr.erase()
|
||||
h, w = stdscr.getmaxyx()
|
||||
stdscr.addstr(0, 0, f"Select Firmware File: {path}".ljust(w-1), curses.color_pair(2))
|
||||
|
||||
@@ -223,7 +235,9 @@ def main_menu(stdscr, slave_id):
|
||||
curses.start_color(); curses.init_pair(1, curses.COLOR_WHITE, curses.COLOR_BLUE); curses.init_pair(2, curses.COLOR_BLUE, curses.COLOR_WHITE); curses.init_pair(3, curses.COLOR_RED, curses.COLOR_BLUE)
|
||||
stdscr.bkgd(' ', curses.color_pair(1))
|
||||
|
||||
menu = ["Open Valve", "Close Valve", "Stop Valve", "Toggle Output 1", "Toggle Output 2", "Set Watchdog", "Reset Node", "Firmware Update", "Exit"]
|
||||
menu = ["Open Valve", "Close Valve", "Stop Valve", "Settings", "Reset Node", "Firmware Update", "Exit"]
|
||||
settings_menu = ["Set Max Open Time", "Set Max Close Time", "Set End Current Open", "Set End Current Close", "Set Watchdog", "Back"]
|
||||
current_menu = menu
|
||||
current_row_idx = 0
|
||||
message, message_time = "", 0
|
||||
input_mode, input_prompt, input_str, input_target_reg = False, "", "", 0
|
||||
@@ -247,22 +261,25 @@ def main_menu(stdscr, slave_id):
|
||||
elif key == curses.KEY_BACKSPACE or key == 127: input_str = input_str[:-1]
|
||||
elif key != -1 and chr(key).isprintable(): input_str += chr(key)
|
||||
else:
|
||||
if key == curses.KEY_UP: current_row_idx = (current_row_idx - 1) % len(menu)
|
||||
elif key == curses.KEY_DOWN: current_row_idx = (current_row_idx + 1) % len(menu)
|
||||
if key == curses.KEY_UP: current_row_idx = (current_row_idx - 1) % len(current_menu)
|
||||
elif key == curses.KEY_DOWN: current_row_idx = (current_row_idx + 1) % len(current_menu)
|
||||
elif key == curses.KEY_ENTER or key in [10, 13]:
|
||||
selected_option = menu[current_row_idx]
|
||||
selected_option = current_menu[current_row_idx]
|
||||
message_time = time.time()
|
||||
if selected_option == "Exit": stop_event.set(); continue
|
||||
elif selected_option == "Back": current_menu = menu; current_row_idx = 0; continue
|
||||
elif selected_option == "Settings": current_menu = settings_menu; current_row_idx = 0; continue
|
||||
elif selected_option == "Open Valve": client.write_register(REG_HOLDING_VALVE_COMMAND, 1, slave=slave_id); message = "-> Sent OPEN command"
|
||||
elif selected_option == "Close Valve": client.write_register(REG_HOLDING_VALVE_COMMAND, 2, slave=slave_id); message = "-> Sent CLOSE command"
|
||||
elif selected_option == "Stop Valve": client.write_register(REG_HOLDING_VALVE_COMMAND, 0, slave=slave_id); message = "-> Sent STOP command"
|
||||
elif "Toggle Output" in selected_option:
|
||||
bit = 0 if "1" in selected_option else 1
|
||||
try:
|
||||
current_val = client.read_holding_registers(REG_HOLDING_DIGITAL_OUTPUTS_STATE, count=1, slave=slave_id).registers[0]
|
||||
client.write_register(REG_HOLDING_DIGITAL_OUTPUTS_STATE, current_val ^ (1 << bit), slave=slave_id)
|
||||
message = f"-> Toggled Output {bit+1}"
|
||||
except Exception as e: message = f"-> Error: {e}"
|
||||
elif selected_option == "Set Max Open Time":
|
||||
input_mode, input_prompt, input_target_reg = True, "Enter Max Open Time (s): ", REG_HOLDING_MAX_OPENING_TIME_S
|
||||
elif selected_option == "Set Max Close Time":
|
||||
input_mode, input_prompt, input_target_reg = True, "Enter Max Close Time (s): ", REG_HOLDING_MAX_CLOSING_TIME_S
|
||||
elif selected_option == "Set End Current Open":
|
||||
input_mode, input_prompt, input_target_reg = True, "Enter End Current Threshold Open (mA): ", REG_HOLDING_END_CURRENT_THRESHOLD_OPEN_MA
|
||||
elif selected_option == "Set End Current Close":
|
||||
input_mode, input_prompt, input_target_reg = True, "Enter End Current Threshold Close (mA): ", REG_HOLDING_END_CURRENT_THRESHOLD_CLOSE_MA
|
||||
elif selected_option == "Set Watchdog":
|
||||
input_mode, input_prompt, input_target_reg = True, "Enter Watchdog Timeout (s): ", REG_HOLDING_WATCHDOG_TIMEOUT_S
|
||||
elif selected_option == "Reset Node":
|
||||
@@ -278,7 +295,7 @@ def main_menu(stdscr, slave_id):
|
||||
else:
|
||||
message = "-> Firmware update cancelled."
|
||||
|
||||
stdscr.clear()
|
||||
stdscr.erase()
|
||||
if is_updating:
|
||||
with update_lock: prog, msg = update_status["progress"], update_status["message"]
|
||||
stdscr.addstr(h // 2 - 1, w // 2 - 25, "FIRMWARE UPDATE IN PROGRESS", curses.A_BOLD | curses.color_pair(2))
|
||||
@@ -292,24 +309,28 @@ def main_menu(stdscr, slave_id):
|
||||
col1, col2, col3, col4 = 2, 30, 58, 88
|
||||
stdscr.addstr(1, col1, "State:", bold); stdscr.addstr(1, col1 + 18, str(current_data.get('state', 'N/A')), normal)
|
||||
stdscr.addstr(2, col1, "Movement:", bold); stdscr.addstr(2, col1 + 18, str(current_data.get('movement', 'N/A')), normal)
|
||||
stdscr.addstr(3, col1, "Motor Current:", bold); stdscr.addstr(3, col1 + 18, str(current_data.get('motor_current', 'N/A')), normal)
|
||||
stdscr.addstr(3, col1, "Open Current:", bold); stdscr.addstr(3, col1 + 18, str(current_data.get('motor_current_open', 'N/A')), normal)
|
||||
stdscr.addstr(4, col1, "Close Current:", bold); stdscr.addstr(4, col1 + 18, str(current_data.get('motor_current_close', 'N/A')), normal)
|
||||
stdscr.addstr(1, col2, "Digital Inputs:", bold); stdscr.addstr(1, col2 + 18, str(current_data.get('digital_inputs', 'N/A')), normal)
|
||||
stdscr.addstr(2, col2, "Digital Outputs:", bold); stdscr.addstr(2, col2 + 18, str(current_data.get('digital_outputs', 'N/A')), normal)
|
||||
stdscr.addstr(3, col2, "Button Events:", bold); stdscr.addstr(3, col2 + 18, str(current_data.get('button_events', 'N/A')), normal)
|
||||
stdscr.addstr(1, col3, "Max Open Time:", bold); stdscr.addstr(1, col3 + 16, str(current_data.get('open_time', 'N/A')), normal)
|
||||
stdscr.addstr(2, col3, "Max Close Time:", bold); stdscr.addstr(2, col3 + 16, str(current_data.get('close_time', 'N/A')), normal)
|
||||
stdscr.addstr(3, col3, "Watchdog:", bold); stdscr.addstr(3, col3 + 16, str(current_data.get('watchdog', 'N/A')), normal)
|
||||
stdscr.addstr(4, col3, "End Curr Open:", bold); stdscr.addstr(4, col3 + 16, str(current_data.get('end_curr_open', 'N/A')), normal)
|
||||
stdscr.addstr(5, col3, "End Curr Close:", bold); stdscr.addstr(5, col3 + 16, str(current_data.get('end_curr_close', 'N/A')), normal)
|
||||
stdscr.addstr(1, col4, "Firmware:", bold); stdscr.addstr(1, col4 + 14, str(current_data.get('firmware', 'N/A')), normal)
|
||||
stdscr.addstr(2, col4, "Uptime:", bold); stdscr.addstr(2, col4 + 14, str(current_data.get('uptime', 'N/A')), normal)
|
||||
stdscr.addstr(3, col4, "Dev. Status:", bold); stdscr.addstr(3, col4 + 14, str(current_data.get('device_status', 'N/A')), normal)
|
||||
stdscr.addstr(5, 0, "─" * (w - 1), normal)
|
||||
for idx, row in enumerate(menu):
|
||||
draw_button(stdscr, h // 2 - len(menu) + (idx * 2), w // 2 - len(row) // 2, row, idx == current_row_idx)
|
||||
stdscr.addstr(4, col4, "Supply V:", bold); stdscr.addstr(4, col4 + 14, str(current_data.get('supply_voltage', 'N/A')), normal)
|
||||
stdscr.addstr(6, 0, "─" * (w - 1), normal)
|
||||
for idx, row in enumerate(current_menu):
|
||||
draw_button(stdscr, 7 + (idx * 2), w // 2 - len(row) // 2, row, idx == current_row_idx)
|
||||
if time.time() - message_time < 2.0: stdscr.addstr(h - 2, 0, message.ljust(w - 1), curses.color_pair(1) | curses.A_BOLD)
|
||||
if input_mode:
|
||||
curses.curs_set(1); stdscr.addstr(h - 2, 0, (input_prompt + input_str).ljust(w-1), curses.color_pair(2)); stdscr.move(h - 2, len(input_prompt) + len(input_str))
|
||||
else: curses.curs_set(0)
|
||||
stdscr.refresh()
|
||||
curses.doupdate()
|
||||
|
||||
def main():
|
||||
global client
|
||||
|
||||
Reference in New Issue
Block a user