34 Commits

Author SHA1 Message Date
fc0add8583 feat: Adjust valve obstacle detection thresholds
Reduced the current thresholds for obstacle detection during valve opening and closing from 500mA to 200mA. This makes the obstacle detection more sensitive.

refactor: Simplify valve_work_handler logic

Refactored the  function to directly call  when an obstacle is detected or the valve reaches its end position. This removes redundant code and improves the clarity of the control flow.
Signed-off-by: Eduard Iten <eduard@iten.pro>
2025-07-11 08:57:02 +02:00
66cdc3ae27 fix: Force UI redraw on successful Modbus reconnection
Implemented a mechanism to force a full UI redraw in the Modbus tool upon successful reconnection to the serial port. The  function now sets a  flag in the shared status data, which is then detected by the  function. Upon detection,  clears the screen and removes the flag, ensuring that any stale error messages are cleared and the UI is fully refreshed.
Signed-off-by: Eduard Iten <eduard@iten.pro>
2025-07-11 08:53:16 +02:00
32bb77926f fix: Reduce flickering in Modbus tool UI over SSH
Replaced  with  and  with  in the  and  functions of . This change optimizes screen updates in the Curses-based UI, which should significantly reduce flickering when running the tool over SSH connections.
Signed-off-by: Eduard Iten <eduard@iten.pro>
2025-07-11 08:14:43 +02:00
4df0181d7f feat: Remove 'Toggle Output' options from Modbus tool
Removed the 'Toggle Output 1' and 'Toggle Output 2' menu options from the Modbus tool's main menu. This simplifies the user interface by removing functionality that is not directly related to the core valve control.
Signed-off-by: Eduard Iten <eduard@iten.pro>
2025-07-11 08:12:50 +02:00
d6fb501594 docs: Add Doxygen comment for movement_timeout_handler
feat: Configure valve logging via Kconfig

This commit adds a Kconfig option  to control the log level of the valve library.
- : Added the new Kconfig option.
- : Updated  to use  and adjusted log levels for debug messages.
- : Enabled debug logging for the valve library by setting .

refactor: Adjust k-vcc calibration value for VND7050AJ

Updated the  calibration value in  from 4139 to 3816 for the VND7050AJ driver.
Signed-off-by: Eduard Iten <eduard@iten.pro>
2025-07-11 08:07:41 +02:00
Your Name
76d0d0647c feat: Implement obstacle detection for valve movement
Implement obstacle detection for valve movement that stops the motor if the current exceeds a predefined threshold during opening or closing.

- :
    - Added new defines  and  with a default value of 500 mA.
- :
    - Modified  function to compare the measured current with the new obstacle thresholds.
    - If the threshold is exceeded, the valve movement is stopped and the status is set to .

Signed-off-by: Your Name <your.email@example.com>
2025-07-11 01:21:41 +02:00
3de42a46c2 refactor: Entferne ungenutzte Funktion
Die Funktion  wurde aus  und  entfernt, da sie im Code nicht mehr verwendet wird. Die Stromwerte werden stattdessen über  und  abgerufen.

Diese Änderung entfernt ungenutzten Code und verbessert die Code-Sauberkeit.
2025-07-11 01:09:46 +02:00
ddaaa8988d feat: Modbus-Register für Endstromschwellenwerte korrigiert
Behebt ein Problem, bei dem das Python-Tool 0 mA für die Endstromschwellenwerte anzeigte.

Die Zephyr-Anwendung definierte zuvor nur ein einzelnes Modbus-Register für den Endstromschwellenwert, während das Python-Tool separate Register für das Öffnen und Schließen erwartete.

Änderungen:
- :
    -  wurde in  umbenannt.
    -  wurde als neues Register hinzugefügt.
- :
    - Implementierung der Lese- und Schreib-Callbacks für  und  unter Verwendung der entsprechenden -Bibliotheksfunktionen.

Diese Änderungen stellen sicher, dass die Zephyr-Anwendung die Endstromschwellenwerte korrekt über Modbus bereitstellt und das Python-Tool diese Werte nun richtig lesen und schreiben kann.
2025-07-11 01:01:45 +02:00
b937c52bcc Revert "feat(valve): Implement obstacle detection with configurable thresholds"
This reverts commit 3c2235733b.
2025-07-11 00:35:19 +02:00
3c2235733b feat(valve): Implement obstacle detection with configurable thresholds
Introduces separate configurable current thresholds for obstacle detection
during valve opening and closing movements.

- Added  state to .
- Added  and
   to .
- Modified  to implement obstacle detection in ,
  setting  on high current, and to load/save
  these new thresholds via settings.
- Added new setter/getter functions for obstacle thresholds to  and .
- Updated  with new shell commands (, )
  and updated  to display these settings.
- Updated  to document the new registers and error states.
- Updated  to include new register definitions, menu options,
  and display of obstacle current thresholds.
2025-07-11 00:27:31 +02:00
a3e8d5c168 refactor(shell): Improve shell command naming and output formatting
- Renamed shell commands in  and  to be shorter
  and remove underscores (e.g.,  to ).
- Consolidated get functions into a single show command for both valve
  and Modbus settings (e.g., , ).
- Adjusted output formatting for show commands to be right-aligned and
  remove horizontal lines for better readability.
- Fixed missing getter function implementations in  and their
  declarations in .
- Ensured  is correctly selected in
  to make valve shell commands available.
2025-07-11 00:16:43 +02:00
5fd904de9e fix(valve): Start movement timer only if timeout is greater than 0
Ensures that the k_timer for valve movement timeouts is only started if
the configured max_opening_time_s or max_closing_time_s is greater than 0.
This prevents unnecessary timer activations when timeouts are disabled or zero.
2025-07-10 23:45:21 +02:00
92bb171e85 feat(valve): Make end-position current thresholds configurable
Introduces separate Modbus holding registers for configurable end-position
current thresholds for both opening and closing valve movements.

- Added REG_HOLDING_VALVE_END_CURRENT_THRESHOLD_OPEN_MA and
  REG_HOLDING_VALVE_END_CURRENT_THRESHOLD_CLOSE_MA to modbus_server.h.
- Modified valve.c to use these new thresholds and save/load them via settings.
- Added new setter functions to valve.h.
- Created new shell_valve library with commands to set/get these thresholds.
- Updated modbus_tool.py to include new menu options for setting thresholds.
- Updated docs/modbus-registers.de.md to document the new registers.

This enhances the flexibility and calibration of the valve control system.
2025-07-10 23:42:41 +02:00
bd8a7a766c style: Apply clang-format to C/C++ source files
Applied consistent code formatting using clang-format to all C/C++ source
and header files in the 'software/' directory.
2025-07-10 23:33:50 +02:00
8f89713866 feat(modbus_tool): Add set functions for max open/close times
Adds new menu options to the Modbus tool to allow setting the maximum
opening and closing times for the valve via Modbus registers.
2025-07-10 23:31:32 +02:00
bf29061db6 feat(slave): Implement VND7050AJ and core valve functionality; docs: Update German documentation and project plan
- Updated Doxygen comments in header files (valve.h, fwu.h, modbus_server.h) to be consistent and in English.
- Translated German register names in docs/modbus-registers.de.md to English.
- Updated docs/concept.de.md to reflect new details on current measurement and sensors.
- Updated docs/planning.de.md to reflect completed tasks in Phase 1.
- Implemented VND7050AJ and core functionality including current and voltage measurement and end-position detection.
2025-07-10 21:13:17 +02:00
c1622bb01c Just saving 2025-07-10 17:23:22 +02:00
222ffea568 Refactor VND7050AJ sensor configuration to eliminate redundancy
- Create centralized sensor multiplexer node (vnd7050aj_mux) with shared configuration
- Consolidate ADC channel, GPIO pins, and reference voltage in single location
- Update sensor bindings to reference centralized mux via sensor-mux property
- Add channel-based sensor selection using mux-channel property (0-3)
- Refactor ADC sensor library to use centralized GPIO and channel control
- Update valve library to use new vnd7050aj_mux node reference
- Eliminate duplicate ADC/GPIO definitions between voltage and current sensors
- Ensure configuration consistency and prevent mismatched settings

Benefits:
- Single source of truth for VND7050AJ hardware configuration
- Impossible to have inconsistent GPIO/ADC settings between sensors
- Simplified maintenance and scalability for additional sensors
- Clean channel-based multiplexer selection interface
2025-07-08 17:05:34 +02:00
a9a0626913 Implement real ADC readings with VND7050AJ sensor multiplexing
- Switch from simulated to real ADC readings in adc_sensor library
- Add GPIO control for VND7050AJ sensor selection (sen, s0, s1 pins)
- Implement proper ADC device and channel setup for voltage/current measurements
- Enable ADC driver in prj.conf (CONFIG_ADC=y)
- Disable simulation mode (CONFIG_ADC_SENSOR_SIMULATED=n)
- Add devicetree bindings for custom supply voltage and motor current sensors
- Update overlay with adc_sensors nodes using PB4, PB5, PB6 pins
- Integrate real ADC readings into Modbus server registers
- Support HSE/HSI clock source toggling in overlay configuration
2025-07-08 16:50:27 +02:00
b11f844415 feat: Add ADC sensor device tree bindings and configuration
Introduces device tree bindings for custom ADC voltage and current sensors,
allowing for flexible configuration of sensor inputs and associated GPIOs.
This enables proper hardware abstraction for ADC measurements.

The example overlay file
has been removed as its content is now integrated or superseded by the new
binding definitions.
2025-07-08 16:43:27 +02:00
2e8a86bc54 Added return code when modubs server init fails 2025-07-08 16:08:49 +02:00
224adccf6b testing precommit hook 2025-07-08 16:06:37 +02:00
9b7159d5a4 added formatting 2025-07-08 16:06:11 +02:00
bc327acc41 docs: Add Doxygen comments to library files
Added Doxygen-style comments to all C source and header files in the
 and  directories. This improves
code documentation and enables VSCode tooltip help.

Additionally, short inline comments were added to all global variables
for better clarity.
2025-07-08 15:48:13 +02:00
c9b0f38576 feat(lib): Introduce adc_sensor library
Adds a new `adc_sensor` library to abstract reading analog values from ADC channels. The output of this library is currently simulated.

This library is now used by the `modbus_server` to read the motor current and the main supply voltage, replacing the previous implementation. This change improves modularity by centralizing ADC-related code into a dedicated module.

The build system has been updated to include the new library.
2025-07-08 15:19:44 +02:00
edf0fb2563 feat(slave_node): Add HSI clock configuration and cleanup
Adds a commented-out clock configuration to the  file. This allows switching the clock source from the external high-speed oscillator (HSE) to the internal high-speed oscillator (HSI), which can be useful if an external crystal is not present.

Also, removes the debug log level for the settings subsystem from the project configuration.
2025-07-08 15:06:31 +02:00
537d76ef5d feat(app): Integrate application versioning
This commit introduces application versioning, exposing version information through the Modbus server and logging it at startup.

- Add  to provide version information
- Update  to log the application version at startup
- Update  to expose firmware version via Modbus
- Add file association for  in
2025-07-08 14:41:01 +02:00
45d011952f fix(valve): Correct VND7050AJ initialization and pin configuration
- Initialize RST pin as active to keep VND7050AJ out of reset state
- Clarify S0/S1 pins as output select pins with descriptive comments
- Add initialization logging to show configured max open/close times
- Ensure proper valve controller startup sequence
2025-07-03 19:04:20 +02:00
bb25134b6c 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.
2025-07-03 18:59:01 +02:00
9f96384aa5 fix(cdc-acm): Correct CDC ACM overlay configuration
This commit fixes an issue in the `cdc-acm.overlay` file.
2025-07-03 18:57:06 +02:00
b543579393 feat(modbus): Add supply voltage register and display in tool
This commit introduces a new Modbus input register for the system's supply voltage.

- The `modbus-registers.de.md` documentation is updated to include the `SUPPLY_VOLTAGE_MV` register at address `0x00F5` within the system block.
- The `modbus_server.h` header defines the new register.
- The `modbus_server.c` implementation provides a fixed value (12300 mV) for this register.
- The `modbus_tool.py` script is updated to read and display this new supply voltage value in the UI.

This lays the groundwork for integrating actual voltage measurements in the future.
2025-07-03 18:47:48 +02:00
69cf7e9511 feat(valve): Implement GPIO control for VND7050AJ
This commit implements the real valve control using the GPIOs connected to the VND7050AJ driver.

- The `weact_stm32g431_core.overlay` is updated with a specific compatible string and a device tree label for the valve controller.
- `valve.h` is extended to include GPIO device specifications.
- `valve.c` now initializes and controls the GPIOs for opening and closing the valve, including the reset logic. The IN0 and IN1 pins are interlocked to prevent simultaneous activation. The RST pin is activated before each movement and deactivated afterward.

This replaces the previous virtual/simulated valve logic with actual hardware control.
2025-07-03 18:17:31 +02:00
8df7aef51b Removed unused lib dir 2025-07-03 17:47:48 +02:00
f6ee0a5122 feat(weact_stm32g431_core): Configure VND7050AJ driver pins in overlay
Updated the weact_stm32g431_core.overlay to define the GPIO and ADC
pin assignments for the VND7050AJ driver. This includes:
- Digital I/O pins (IN0, IN1, RST, S0, S1, SEN) configured as GPIOs.
- Analog input pin (MULTISENSE/PA0) configured for ADC1.
2025-07-03 17:39:04 +02:00
50 changed files with 1328 additions and 310 deletions

View 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.

View File

@@ -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. * **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:** * **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 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. * **Zwei digitale Eingänge:** Direkte, geschützte Eingänge am Controller zum Anschluss von Tastern oder den kapazitiven NPN-Sensoren.

View File

@@ -29,25 +29,29 @@ Alle Register sind in einer einzigen, durchgehenden Liste pro Register-Typ (`Inp
| Adresse (hex) | Name | Zugehörigkeit | Beschreibung | | 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). | | **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_MA` | Ventil | Aktueller Motorstrom in Milliampere (mA). | | **0x0001** | `MOTORSTROM_OPEN_MA` | Ventil | Motorstrom beim Öffnen in Milliampere (mA). |
| **0x0020** | `DIGITAL_EINGAENGE_ZUSTAND` | Eingänge | Bitmaske der digitalen Eingänge. Bit 0: Eingang 1, Bit 1: Eingang 2. `1`=Aktiv. | | **0x0002** | `MOTORSTROM_CLOSE_MA` | Ventil | Motorstrom beim Schließen in Milliampere (mA). |
| **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. | | **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. | | **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. | | **0x00F1** | `FIRMWARE_VERSION_PATCH` | System | z.B. `3` für v1.2.3. |
| **0x00F2** | `DEVICE_STATUS` | System | `0`=OK, `1`=Allgemeiner Fehler. | | **0x00F2** | `DEVICE_STATUS` | System | `0`=OK, `1`=Allgemeiner Fehler. |
| **0x00F3** | `UPTIME_SECONDS_LOW` | System | Untere 16 Bit der Uptime in Sekunden. | | **0x00F3** | `UPTIME_SECONDS_LOW` | System | Untere 16 Bit der Uptime in Sekunden. |
| **0x00F4** | `UPTIME_SECONDS_HIGH` | System | Obere 16 Bit der Uptime. | | **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. | | **0x0100** | `FWU_LAST_CHUNK_CRC` | Firmware-Update | Enthält den CRC16 des zuletzt im Puffer empfangenen Daten-Chunks. |
## 3. Holding Registers (4xxxx, Read/Write) ## 3. Holding Registers (4xxxx, Read/Write)
| Adresse (hex) | Name | Zugehörigkeit | Beschreibung | | Adresse (hex) | Name | Zugehörigkeit | Beschreibung |
| :------------ | :---------------------------- | :---------------- | :---------------------------------------------------------------------------------------------------------------------------------------- | | :------------ | :---------------------------- | :---------------- | :---------------------------------------------------------------------------------------------------------------------------------------- |
| **0x0000** | `VENTIL_BEFEHL` | Ventil | `1`=Öffnen, `2`=Schliessen, `0`=Bewegung stoppen. | | **0x0000** | `VALVE_COMMAND` | Ventil | `1`=Öffnen, `2`=Schliessen, `0`=Bewegung stoppen. |
| **0x0001** | `MAX_OEFFNUNGSZEIT_S` | Ventil | Sicherheits-Timeout in Sekunden für den Öffnen-Vorgang. | | **0x0001** | `MAX_OPENING_TIME_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. | | **0x0002** | `MAX_CLOSING_TIME_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. | | **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. | | **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. | | **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. | | **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 | | 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. | | **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. | | **0x0002** | `UNIT` | 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). | | **0x0003** | `DECIMAL_PLACES` | R/W | Anzahl der Dezimalstellen für den Messwert (0-3). |
| **0x0004** | `MESSWERT_AKTUELL` | R | Der skalierte Messwert als vorzeichenbehafteter 16-Bit-Integer. | | **0x0004** | `CURRENT_MEASUREMENT` | R | Der skalierte Messwert als vorzeichenbehafteter 16-Bit-Integer. |
| **0x0005** | `MESSBEREICH_NULLPUNKT` | R/W | Rohwert für den Nullpunkt der Skala. | | **0x0005** | `MEASUREMENT_RANGE_ZERO_POINT` | R/W | Rohwert für den Nullpunkt der Skala. |
| **0x0006** | `MESSBEREICH_ENDPUNKT` | R/W | Rohwert für den Endpunkt der Skala. | | **0x0006** | `MEASUREMENT_RANGE_END_POINT` | R/W | Rohwert für den Endpunkt der Skala. |

View File

@@ -9,11 +9,13 @@
| ✅ | **Phase 0: Planung & Definition** | | | | ✅ | **Phase 0: Planung & Definition** | | |
| ✅ | Konzept erstellen und finalisieren | 30.06.2025 | Architektur, Komponenten und grundlegende Architektur sind festgelegt. | | ✅ | 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. | | ✅ | 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. | | ☐ | **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.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.2 Hardware-Abstraktion (VND7050AJ, RS485) | 10.07.2025 | Implementierung der Treiber für den VND7050AJ und die RS485-Kommunikation. |
| | 1.3 MODBUS-RTU Stack auf dem Slave implementieren | | Basierend auf der definierten Register-Map. Zuerst nur lesende Funktionen (Status, Version). | | | 1.3 Basis-Firmware für Slave-Node erstellen | 10.07.2025 | Hardware-Abstraktion (GPIOs) implementiert. |
| | 1.4 Kernlogik implementieren (z.B. Ventilsteuerung) | | Umsetzung der `VENTIL_ZUSTAND_BEWEGUNG` Logik, Strommessung für Endlagen etc. | | | 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. | | ☐ | **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.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. | | ☐ | 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. |

View File

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

View File

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

View File

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

View File

@@ -1,12 +1,15 @@
{ {
// Hush CMake // Hush CMake
"cmake.configureOnOpen": false, "cmake.configureOnOpen": false,
// IntelliSense
// IntelliSense "C_Cpp.default.compilerPath": "${userHome}/zephyr-sdk-0.17.1/arm-zephyr-eabi/bin/arm-zephyr-eabi-gcc.exe",
"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",
"C_Cpp.default.compileCommands": "${workspaceFolder}/build/compile_commands.json", // File Associations
"files.associations": {
// File Associations "app_version.h": "c"
"files.associations": { },
} "C_Cpp.clang_format_style": "file",
"nrf-connect.applications": [
"${workspaceFolder}/apps/slave_node"
],
} }

View File

@@ -2,32 +2,20 @@
"version": "2.0.0", "version": "2.0.0",
"tasks": [ "tasks": [
{ {
"label": "West Build", "label": "Format All C/C++ Files",
"type": "shell", "type": "shell",
"group": { "command": "find . -name \"*.c\" -o -name \"*.h\" | xargs clang-format -i",
"kind": "build", "problemMatcher": [],
"isDefault": true "group": {
}, "kind": "build",
"linux": { "isDefault": true
"command": "${userHome}/zephyrproject/.venv/bin/west" },
}, "presentation": {
"windows": { "reveal": "silent",
"command": "${userHome}/zephyrproject/.venv/Scripts/west.exe" "clear": true,
}, "panel": "shared"
"osx": { }
"command": "${userHome}/zephyrproject/.venv/bin/west" },
},
"args": [
"build",
"-p",
"auto",
"-b",
"valve_node"
],
"problemMatcher": [
"$gcc"
]
},
{ {
"label": "West Configurable Build", "label": "West Configurable Build",
"type": "shell", "type": "shell",

View File

@@ -1 +1,7 @@
rsource "lib/Kconfig" rsource "lib/Kconfig"
rsource "lib/shell_valve/Kconfig"
config SLAVE_NODE_APP
bool "Slave Node Application"
default y
select SHELL_VALVE

View File

View File

View File

View File

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

View File

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

View File

@@ -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 { &usart1 {
modbus0 { modbus0 {
compatible = "zephyr,modbus-serial"; compatible = "zephyr,modbus-serial";
status = "okay"; status = "okay";
}; };
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"; pinctrl-names = "default";
}; };

View File

@@ -1,3 +1,5 @@
#include <zephyr/dt-bindings/gpio/gpio.h>
&zephyr_udc0 { &zephyr_udc0 {
cdc_acm_uart0: cdc_acm_uart0 { cdc_acm_uart0: cdc_acm_uart0 {
compatible = "zephyr,cdc-acm-uart"; compatible = "zephyr,cdc-acm-uart";
@@ -8,3 +10,7 @@
}; };
}; };
}; };
&usart1 {
/delete-node/ modbus0;
};

View File

@@ -5,6 +5,9 @@ CONFIG_LOG=y
# Enable Shell # Enable Shell
CONFIG_SHELL=y CONFIG_SHELL=y
CONFIG_REBOOT=y CONFIG_REBOOT=y
CONFIG_SHELL_MODBUS=y
CONFIG_SHELL_VALVE=y
CONFIG_SHELL_SYSTEM=y
# Enable Settings Subsystem # Enable Settings Subsystem
CONFIG_SETTINGS=y CONFIG_SETTINGS=y
@@ -21,3 +24,6 @@ CONFIG_MODBUS=y
CONFIG_MODBUS_ROLE_SERVER=y CONFIG_MODBUS_ROLE_SERVER=y
CONFIG_MODBUS_BUFFER_SIZE=256 CONFIG_MODBUS_BUFFER_SIZE=256
# Enable VND7050AJ
CONFIG_VND7050AJ=y
CONFIG_LOG_VALVE_LEVEL=4

View File

@@ -1,9 +1,9 @@
#include <zephyr/kernel.h> #include <zephyr/kernel.h>
#include <zephyr/settings/settings.h>
#include <zephyr/logging/log.h> #include <zephyr/logging/log.h>
#include <zephyr/settings/settings.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/fwu.h>
LOG_MODULE_REGISTER(main, LOG_LEVEL_INF); LOG_MODULE_REGISTER(main, LOG_LEVEL_INF);

View File

@@ -3,8 +3,45 @@
#include <stdint.h> #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); 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); 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); uint16_t fwu_get_last_chunk_crc(void);
#endif // FWU_H #endif // FWU_H

View File

@@ -4,49 +4,174 @@
#include <stdint.h> #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 { enum {
/* Valve Control & Status */ /**
REG_INPUT_VALVE_STATE_MOVEMENT = 0x0000, * @brief Combined status register for the valve.
REG_INPUT_MOTOR_CURRENT_MA = 0x0001, * High-Byte: Movement (0=Idle, 1=Opening, 2=Closing, 3=Error).
/* Digital Inputs */ * Low-Byte: State (0=Closed, 1=Open).
REG_INPUT_DIGITAL_INPUTS_STATE = 0x0020, */
REG_INPUT_BUTTON_EVENTS = 0x0021, REG_INPUT_VALVE_STATE_MOVEMENT = 0x0000,
/* System Config & Status */ /**
* @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_MAJOR_MINOR = 0x00F0,
REG_INPUT_FIRMWARE_VERSION_PATCH = 0x00F1, /**
REG_INPUT_DEVICE_STATUS = 0x00F2, * @brief Firmware version patch level, e.g., 3 for v1.2.3.
REG_INPUT_UPTIME_SECONDS_LOW = 0x00F3, */
REG_INPUT_UPTIME_SECONDS_HIGH = 0x00F4, REG_INPUT_FIRMWARE_VERSION_PATCH = 0x00F1,
/* Firmware Update */ /**
REG_INPUT_FWU_LAST_CHUNK_CRC = 0x0100, * @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 { enum {
/* Valve Control */ /**
REG_HOLDING_VALVE_COMMAND = 0x0000, * @brief Valve control command (1=Open, 2=Close, 0=Stop movement).
REG_HOLDING_MAX_OPENING_TIME_S = 0x0001, */
REG_HOLDING_MAX_CLOSING_TIME_S = 0x0002, REG_HOLDING_VALVE_COMMAND = 0x0000,
/* Digital Outputs */ /**
REG_HOLDING_DIGITAL_OUTPUTS_STATE = 0x0010, * @brief Safety timeout in seconds for the opening process.
/* System Config */ */
REG_HOLDING_WATCHDOG_TIMEOUT_S = 0x00F0, REG_HOLDING_MAX_OPENING_TIME_S = 0x0001,
REG_HOLDING_DEVICE_RESET = 0x00F1, /**
/* Firmware Update */ * @brief Safety timeout in seconds for the closing process.
REG_HOLDING_FWU_COMMAND = 0x0100, */
REG_HOLDING_FWU_CHUNK_OFFSET_LOW = 0x0101, REG_HOLDING_MAX_CLOSING_TIME_S = 0x0002,
REG_HOLDING_FWU_CHUNK_OFFSET_HIGH = 0x0102, /**
REG_HOLDING_FWU_CHUNK_SIZE = 0x0103, * @brief Minimum current threshold in mA for end-position detection.
REG_HOLDING_FWU_DATA_BUFFER = 0x0180, */
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); 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); 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); 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); uint8_t modbus_get_unit_id(void);
#endif // MODBUS_SERVER_H #endif // MODBUS_SERVER_H

View File

@@ -1,23 +1,167 @@
#ifndef VALVE_H #ifndef VALVE_H
#define VALVE_H #define VALVE_H
#include <zephyr/drivers/gpio.h>
#include <stdint.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); void valve_open(void);
/**
* @brief Starts closing the valve.
*
* The valve will close for the configured maximum closing time.
*/
void valve_close(void); void valve_close(void);
/**
* @brief Stops any ongoing valve movement immediately.
*/
void valve_stop(void); 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_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); 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); 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); 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); 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 #endif // VALVE_H

View File

@@ -3,3 +3,4 @@ add_subdirectory_ifdef(CONFIG_LIB_MODBUS_SERVER modbus_server)
add_subdirectory_ifdef(CONFIG_LIB_VALVE valve) 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)

View File

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

View File

@@ -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/kernel.h>
#include <zephyr/sys/crc.h>
#include <zephyr/sys/byteorder.h>
#include <zephyr/logging/log.h> #include <zephyr/logging/log.h>
#include <zephyr/sys/byteorder.h>
#include <zephyr/sys/crc.h>
#include <lib/fwu.h> #include <lib/fwu.h>
LOG_MODULE_REGISTER(fwu, LOG_LEVEL_INF); LOG_MODULE_REGISTER(fwu, LOG_LEVEL_INF);
#define FWU_BUFFER_SIZE 256 #define FWU_BUFFER_SIZE 256
static uint8_t fwu_buffer[FWU_BUFFER_SIZE]; static uint8_t fwu_buffer[FWU_BUFFER_SIZE]; // Buffer to store incoming
static uint32_t fwu_chunk_offset = 0; // firmware data chunks
static uint16_t fwu_chunk_size = 0; static uint32_t fwu_chunk_offset = 0; // Offset for the current firmware chunk in the overall image
static uint16_t fwu_last_chunk_crc = 0; 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) void fwu_handler(uint16_t addr, uint16_t reg)
{ {
// This is a simplified handler. In a real scenario, you would have a proper mapping // This is a simplified handler. In a real scenario, you would have a proper
// between register addresses and actions. // mapping between register addresses and actions.
if (addr == 0x0100) { // FWU_COMMAND if (addr == 0x0100) { // FWU_COMMAND
if (reg == 1) { LOG_INF("FWU: Chunk at offset %u (size %u) verified.", fwu_chunk_offset, fwu_chunk_size); } if (reg == 1) {
else if (reg == 2) { LOG_INF("FWU: Finalize command received. Rebooting (simulated)."); } LOG_INF("FWU: Chunk at offset %u (size %u) verified.",
} else if (addr == 0x0101) { // FWU_CHUNK_OFFSET_LOW fwu_chunk_offset,
fwu_chunk_offset = (fwu_chunk_offset & 0xFFFF0000) | reg; fwu_chunk_size);
} else if (addr == 0x0102) { // FWU_CHUNK_OFFSET_HIGH } else if (reg == 2) {
fwu_chunk_offset = (fwu_chunk_offset & 0x0000FFFF) | ((uint32_t)reg << 16); LOG_INF("FWU: Finalize command received. Rebooting (simulated).");
} else if (addr == 0x0103) { // FWU_CHUNK_SIZE }
fwu_chunk_size = (reg > FWU_BUFFER_SIZE) ? FWU_BUFFER_SIZE : reg; } else if (addr == 0x0101) { // FWU_CHUNK_OFFSET_LOW
} else if (addr >= 0x0180 && addr < (0x0180 + (FWU_BUFFER_SIZE / 2))) { fwu_chunk_offset = (fwu_chunk_offset & 0xFFFF0000) | reg;
uint16_t index = (addr - 0x0180) * 2; } else if (addr == 0x0102) { // FWU_CHUNK_OFFSET_HIGH
if (index < sizeof(fwu_buffer)) { fwu_chunk_offset = (fwu_chunk_offset & 0x0000FFFF) | ((uint32_t)reg << 16);
sys_put_be16(reg, &fwu_buffer[index]); } else if (addr == 0x0103) { // FWU_CHUNK_SIZE
if (index + 2 >= fwu_chunk_size) { fwu_chunk_size = (reg > FWU_BUFFER_SIZE) ? FWU_BUFFER_SIZE : reg;
fwu_last_chunk_crc = crc16_ccitt(0xffff, fwu_buffer, fwu_chunk_size); } else if (addr >= 0x0180 && addr < (0x0180 + (FWU_BUFFER_SIZE / 2))) {
LOG_INF("FWU: Chunk received, CRC is 0x%04X", fwu_last_chunk_crc); 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) uint16_t fwu_get_last_chunk_crc(void)
{ {
return fwu_last_chunk_crc; return fwu_last_chunk_crc;
} }

View File

@@ -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/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/logging/log.h>
#include <zephyr/modbus/modbus.h>
#include <zephyr/settings/settings.h> #include <zephyr/settings/settings.h>
#include <zephyr/sys/reboot.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/modbus_server.h>
#include <lib/valve.h> #include <lib/valve.h>
#include <lib/fwu.h>
#include <zephyr/usb/usb_device.h>
LOG_MODULE_REGISTER(modbus_server, LOG_LEVEL_INF); LOG_MODULE_REGISTER(modbus_server, LOG_LEVEL_INF);
static int modbus_iface; static int modbus_iface;
static struct modbus_iface_param server_param = { static struct modbus_iface_param server_param = {
.mode = MODBUS_MODE_RTU, .mode = MODBUS_MODE_RTU,
.server = {.user_cb = NULL, .unit_id = 1}, .server = {.user_cb = NULL, .unit_id = 1},
.serial = {.baud = 19200, .parity = UART_CFG_PARITY_NONE}, .serial = {.baud = 19200, .parity = UART_CFG_PARITY_NONE},
}; };
static uint16_t watchdog_timeout_s = 0; static uint16_t watchdog_timeout_s = 0;
static struct k_timer watchdog_timer; 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) static void watchdog_timer_handler(struct k_timer *timer_id)
{ {
LOG_WRN("Modbus watchdog expired! Closing valve as a fail-safe."); LOG_WRN("Modbus watchdog expired! Closing valve as a fail-safe.");
valve_close(); 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) 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); 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) static int holding_reg_rd(uint16_t addr, uint16_t *reg)
{ {
reset_watchdog(); reset_watchdog();
switch (addr) switch (addr) {
{
case REG_HOLDING_MAX_OPENING_TIME_S: case REG_HOLDING_MAX_OPENING_TIME_S:
*reg = valve_get_max_open_time(); *reg = valve_get_max_open_time();
break; break;
@@ -51,6 +81,12 @@ static int holding_reg_rd(uint16_t addr, uint16_t *reg)
case REG_HOLDING_WATCHDOG_TIMEOUT_S: case REG_HOLDING_WATCHDOG_TIMEOUT_S:
*reg = watchdog_timeout_s; *reg = watchdog_timeout_s;
break; 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: default:
*reg = 0; *reg = 0;
break; break;
@@ -58,22 +94,23 @@ static int holding_reg_rd(uint16_t addr, uint16_t *reg)
return 0; 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) static int holding_reg_wr(uint16_t addr, uint16_t reg)
{ {
reset_watchdog(); reset_watchdog();
switch (addr) switch (addr) {
{
case REG_HOLDING_VALVE_COMMAND: case REG_HOLDING_VALVE_COMMAND:
if (reg == 1) if (reg == 1) {
{
valve_open(); valve_open();
} } else if (reg == 2) {
else if (reg == 2)
{
valve_close(); valve_close();
} } else if (reg == 0) {
else if (reg == 0)
{
valve_stop(); valve_stop();
} }
break; break;
@@ -83,22 +120,24 @@ static int holding_reg_wr(uint16_t addr, uint16_t reg)
case REG_HOLDING_MAX_CLOSING_TIME_S: case REG_HOLDING_MAX_CLOSING_TIME_S:
valve_set_max_close_time(reg); valve_set_max_close_time(reg);
break; 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: case REG_HOLDING_WATCHDOG_TIMEOUT_S:
watchdog_timeout_s = reg; 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); LOG_INF("Watchdog enabled with %u s timeout.", watchdog_timeout_s);
reset_watchdog(); reset_watchdog();
} } else {
else
{
LOG_INF("Watchdog disabled."); LOG_INF("Watchdog disabled.");
k_timer_stop(&watchdog_timer); k_timer_stop(&watchdog_timer);
} }
break; break;
case REG_HOLDING_DEVICE_RESET: case REG_HOLDING_DEVICE_RESET:
if (reg == 1) if (reg == 1) {
{
LOG_WRN("Modbus reset command received. Rebooting..."); LOG_WRN("Modbus reset command received. Rebooting...");
sys_reboot(SYS_REBOOT_WARM); sys_reboot(SYS_REBOOT_WARM);
} }
@@ -110,17 +149,26 @@ static int holding_reg_wr(uint16_t addr, uint16_t reg)
return 0; 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) static int input_reg_rd(uint16_t addr, uint16_t *reg)
{ {
reset_watchdog(); reset_watchdog();
uint32_t uptime_s = k_uptime_get_32() / 1000; uint32_t uptime_s = k_uptime_get_32() / 1000;
switch (addr) switch (addr) {
{
case REG_INPUT_VALVE_STATE_MOVEMENT: case REG_INPUT_VALVE_STATE_MOVEMENT:
*reg = (valve_get_movement() << 8) | (valve_get_state() & 0xFF); *reg = (valve_get_movement() << 8) | (valve_get_state() & 0xFF);
break; break;
case REG_INPUT_MOTOR_CURRENT_MA: case REG_INPUT_MOTOR_OPEN_CURRENT_MA:
*reg = valve_get_motor_current(); *reg = (uint16_t)valve_get_opening_current();
break;
case REG_INPUT_MOTOR_CLOSE_CURRENT_MA:
*reg = (uint16_t)valve_get_closing_current();
break; break;
case REG_INPUT_UPTIME_SECONDS_LOW: case REG_INPUT_UPTIME_SECONDS_LOW:
*reg = (uint16_t)(uptime_s & 0xFFFF); *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: case REG_INPUT_UPTIME_SECONDS_HIGH:
*reg = (uint16_t)(uptime_s >> 16); *reg = (uint16_t)(uptime_s >> 16);
break; break;
case REG_INPUT_SUPPLY_VOLTAGE_MV:
*reg = (uint16_t)valve_get_vnd_voltage();
break;
case REG_INPUT_FWU_LAST_CHUNK_CRC: case REG_INPUT_FWU_LAST_CHUNK_CRC:
*reg = fwu_get_last_chunk_crc(); *reg = fwu_get_last_chunk_crc();
break; break;
case REG_INPUT_FIRMWARE_VERSION_MAJOR_MINOR: case REG_INPUT_FIRMWARE_VERSION_MAJOR_MINOR:
*reg = (0 << 8) | 0; *reg = (APP_VERSION_MAJOR << 8) | APP_VERSION_MINOR;
break; break;
case REG_INPUT_FIRMWARE_VERSION_PATCH: 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; break;
default: default:
*reg = 0; *reg = 0;
@@ -145,9 +202,10 @@ static int input_reg_rd(uint16_t addr, uint16_t *reg)
} }
static struct modbus_user_callbacks mbs_cbs = { static struct modbus_user_callbacks mbs_cbs = {
.holding_reg_rd = holding_reg_rd, // Modbus server callback functions
.holding_reg_wr = holding_reg_wr, .holding_reg_rd = holding_reg_rd,
.input_reg_rd = input_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) #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) int modbus_server_init(void)
{ {
k_timer_init(&watchdog_timer, watchdog_timer_handler, NULL); 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)}; const char iface_name[] = {DEVICE_DT_NAME(MODBUS_NODE)};
#if DT_NODE_HAS_COMPAT(DT_PARENT(MODBUS_NODE), zephyr_cdc_acm_uart) #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)); const struct device *const dev = DEVICE_DT_GET(DT_PARENT(MODBUS_NODE));
uint32_t dtr = 0; uint32_t dtr = 0;
if (!device_is_ready(dev) || usb_enable(NULL)) if (!device_is_ready(dev) || usb_enable(NULL)) {
{
return 0; return 0;
} }
while (!dtr) while (!dtr) {
{
uart_line_ctrl_get(dev, UART_LINE_CTRL_DTR, &dtr); uart_line_ctrl_get(dev, UART_LINE_CTRL_DTR, &dtr);
k_sleep(K_MSEC(100)); k_sleep(K_MSEC(100));
} }
@@ -174,29 +241,49 @@ int modbus_server_init(void)
LOG_INF("Client connected to server on %s", dev->name); LOG_INF("Client connected to server on %s", dev->name);
#endif #endif
modbus_iface = modbus_iface_get_by_name(iface_name); modbus_iface = modbus_iface_get_by_name(iface_name);
if (modbus_iface < 0) if (modbus_iface < 0) {
{
return modbus_iface; return modbus_iface;
} }
server_param.server.user_cb = &mbs_cbs; 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); return modbus_init_server(modbus_iface, server_param);
} }
int modbus_reconfigure(uint32_t baudrate, uint8_t unit_id) int modbus_reconfigure(uint32_t baudrate, uint8_t unit_id)
{ {
// Update parameters
server_param.serial.baud = baudrate; server_param.serial.baud = baudrate;
server_param.server.unit_id = unit_id; 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); 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/baudrate", &baudrate, sizeof(baudrate));
settings_save_one("modbus/unit_id", &unit_id, sizeof(unit_id)); 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; return ret;
} }
uint32_t modbus_get_baudrate(void) { return server_param.serial.baud; } uint32_t modbus_get_baudrate(void)
uint8_t modbus_get_unit_id(void) { return server_param.server.unit_id; } {
return server_param.serial.baud;
}
uint8_t modbus_get_unit_id(void)
{
return server_param.server.unit_id;
}

View File

@@ -1,5 +1,7 @@
config SHELL_MODBUS config SHELL_MODBUS
bool "Enable Shell Modbus" bool "Enable Shell Modbus"
default y default n
depends on SHELL
depends on LIB_MODBUS_SERVER
help help
Enable the modnbus shell commands. Enable the modbus shell commands.

View File

@@ -1,12 +1,29 @@
#include <zephyr/shell/shell.h> /**
#include <stdlib.h> * @file shell_modbus.c
#include <lib/modbus_server.h> * @brief Provides shell commands for Modbus and valve configuration.
#include <lib/valve.h> *
* 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) { if (argc != 2) {
shell_error(sh, "Usage: set_baud <baudrate>"); shell_error(sh, "Usage: setb <baudrate>");
return -EINVAL; return -EINVAL;
} }
@@ -23,9 +40,13 @@ static int cmd_modbus_set_baud(const struct shell *sh, size_t argc, char **argv)
if (!is_valid) { if (!is_valid) {
char error_msg[128]; 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++) { 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); shell_error(sh, "%s", error_msg);
return -EINVAL; return -EINVAL;
@@ -40,10 +61,18 @@ static int cmd_modbus_set_baud(const struct shell *sh, size_t argc, char **argv)
return 0; 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) { if (argc != 2) {
shell_error(sh, "Usage: set_id <slave_id>"); shell_error(sh, "Usage: setid <slave_id>");
return -EINVAL; return -EINVAL;
} }
@@ -63,57 +92,28 @@ static int cmd_modbus_set_id(const struct shell *sh, size_t argc, char **argv)
return 0; 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) { const int label_width = 15;
shell_error(sh, "Usage: set_open_time <seconds>");
return -EINVAL;
}
uint16_t seconds = (uint16_t)strtoul(argv[1], NULL, 10); shell_print(sh, "Modbus Settings:");
valve_set_max_open_time(seconds); shell_print(sh, "%*s %u", label_width, "Baudrate:", modbus_get_baudrate());
shell_print(sh, "Max opening time set to: %u seconds (and saved)", seconds); shell_print(sh, "%*s %u", label_width, "Slave ID:", modbus_get_unit_id());
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());
return 0; return 0;
} }
SHELL_STATIC_SUBCMD_SET_CREATE(sub_modbus_cmds, SHELL_STATIC_SUBCMD_SET_CREATE(sub_modbus_cmds,
SHELL_CMD(set_baud, NULL, "Set Modbus baudrate", cmd_modbus_set_baud), SHELL_CMD(setb, NULL, "Set Modbus baudrate", cmd_modbus_setb),
SHELL_CMD(set_id, NULL, "Set Modbus slave ID", cmd_modbus_set_id), SHELL_CMD(setid, NULL, "Set Modbus slave ID", cmd_modbus_setid),
SHELL_SUBCMD_SET_END SHELL_CMD(show, NULL, "Show Modbus configuration", cmd_modbus_show),
); SHELL_SUBCMD_SET_END);
SHELL_STATIC_SUBCMD_SET_CREATE(sub_valve_cmds, SHELL_CMD_REGISTER(modbus, &sub_modbus_cmds, "Modbus commands", NULL);
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);

View File

@@ -1,5 +1,5 @@
config SHELL_SYSTEM config SHELL_SYSTEM
bool "Enable Shell System" bool "Enable Shell System"
default y default n
help help
Enable the system commands. Enable the system commands.

View File

@@ -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/shell/shell.h>
#include <zephyr/sys/reboot.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) static int cmd_reset(const struct shell *sh, size_t argc, char **argv)
{ {
shell_print(sh, "Rebooting system..."); shell_print(sh, "Rebooting system...");

View File

@@ -0,0 +1 @@
zephyr_library_sources(shell_valve.c)

View 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.

View 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);

View File

@@ -3,3 +3,12 @@ config LIB_VALVE
default y default y
help help
Enable the Valve Library. 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

View File

@@ -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/kernel.h>
#include <zephyr/settings/settings.h>
#include <zephyr/logging/log.h> #include <zephyr/logging/log.h>
#include <zephyr/settings/settings.h>
#include <lib/valve.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 enum valve_movement current_movement = VALVE_MOVEMENT_IDLE;
static uint16_t max_opening_time_s = 60; static uint16_t max_opening_time_s = 10;
static uint16_t max_closing_time_s = 60; 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_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) static void valve_work_handler(struct k_work *work)
{ {
if (current_movement == VALVE_MOVEMENT_OPENING) { int current_ma = 0;
LOG_INF("Virtual valve finished opening");
} else if (current_movement == VALVE_MOVEMENT_CLOSING) { if (current_movement == VALVE_MOVEMENT_OPENING) {
current_state = VALVE_STATE_CLOSED; vnd7050aj_read_load_current(vnd7050aj_dev, VALVE_CHANNEL_OPEN, &current_ma);
LOG_INF("Virtual valve finished closing"); LOG_DBG("Current load during opening: %d mA", current_ma);
} if (current_ma > VALVE_OBSTACLE_THRESHOLD_OPEN_MA) {
current_movement = VALVE_MOVEMENT_IDLE; 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, &current_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); // Stop the end position check if the timer expires
settings_load_one("valve/max_open_time", &max_opening_time_s, sizeof(max_opening_time_s)); k_work_cancel_delayable(&valve_work);
settings_load_one("valve/max_close_time", &max_closing_time_s, sizeof(max_closing_time_s)); 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) void valve_open(void)
{ {
if (current_state == VALVE_STATE_CLOSED) { LOG_DBG("Opening valve");
current_state = VALVE_STATE_OPEN; vnd7050aj_reset_fault(vnd7050aj_dev);
current_movement = VALVE_MOVEMENT_OPENING; vnd7050aj_set_output_state(vnd7050aj_dev, VALVE_CHANNEL_CLOSE, false);
k_work_schedule(&valve_work, K_SECONDS(max_opening_time_s)); 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) void valve_close(void)
{ {
if (current_state == VALVE_STATE_OPEN) { LOG_DBG("Closing valve");
current_movement = VALVE_MOVEMENT_CLOSING; vnd7050aj_reset_fault(vnd7050aj_dev);
k_work_schedule(&valve_work, K_SECONDS(max_closing_time_s)); 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) void valve_stop(void)
{ {
k_work_cancel_delayable(&valve_work); k_work_cancel_delayable(&valve_work);
current_movement = VALVE_MOVEMENT_IDLE; 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_state valve_get_state(void)
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; } 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_open_time(uint16_t seconds)
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; } max_opening_time_s = seconds;
uint16_t valve_get_max_close_time(void) { return max_closing_time_s; } 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, &current);
return current;
}
int32_t valve_get_closing_current(void)
{
int32_t current;
vnd7050aj_read_load_current(vnd7050aj_dev, VALVE_CHANNEL_CLOSE, &current);
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;
}

View File

@@ -11,7 +11,8 @@ from pymodbus.exceptions import ModbusException
# --- Register Definitions --- # --- Register Definitions ---
# (omitted for brevity, no changes here) # (omitted for brevity, no changes here)
REG_INPUT_VALVE_STATE_MOVEMENT = 0x0000 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_DIGITAL_INPUTS_STATE = 0x0020
REG_INPUT_BUTTON_EVENTS = 0x0021 REG_INPUT_BUTTON_EVENTS = 0x0021
REG_INPUT_FIRMWARE_VERSION_MAJOR_MINOR = 0x00F0 REG_INPUT_FIRMWARE_VERSION_MAJOR_MINOR = 0x00F0
@@ -19,10 +20,13 @@ REG_INPUT_FIRMWARE_VERSION_PATCH = 0x00F1
REG_INPUT_DEVICE_STATUS = 0x00F2 REG_INPUT_DEVICE_STATUS = 0x00F2
REG_INPUT_UPTIME_SECONDS_LOW = 0x00F3 REG_INPUT_UPTIME_SECONDS_LOW = 0x00F3
REG_INPUT_UPTIME_SECONDS_HIGH = 0x00F4 REG_INPUT_UPTIME_SECONDS_HIGH = 0x00F4
REG_INPUT_SUPPLY_VOLTAGE_MV = 0x00F5
REG_INPUT_FWU_LAST_CHUNK_CRC = 0x0100 REG_INPUT_FWU_LAST_CHUNK_CRC = 0x0100
REG_HOLDING_VALVE_COMMAND = 0x0000 REG_HOLDING_VALVE_COMMAND = 0x0000
REG_HOLDING_MAX_OPENING_TIME_S = 0x0001 REG_HOLDING_MAX_OPENING_TIME_S = 0x0001
REG_HOLDING_MAX_CLOSING_TIME_S = 0x0002 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_DIGITAL_OUTPUTS_STATE = 0x0010
REG_HOLDING_WATCHDOG_TIMEOUT_S = 0x00F0 REG_HOLDING_WATCHDOG_TIMEOUT_S = 0x00F0
REG_HOLDING_DEVICE_RESET = 0x00F1 REG_HOLDING_DEVICE_RESET = 0x00F1
@@ -75,21 +79,24 @@ def poll_status(slave_id, interval):
# Attempt to connect # Attempt to connect
if client.connect(): if client.connect():
reconnect_attempts = 0 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: else:
new_data["error"] = f"Connection lost. Attempting to reconnect ({reconnect_attempts}/{max_reconnect_attempts})..." new_data["error"] = f"Connection lost. Attempting to reconnect ({reconnect_attempts}/{max_reconnect_attempts})..."
time.sleep(reconnect_delay) time.sleep(reconnect_delay)
continue continue
# If connected, try to read data # 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_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) 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=2, 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_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) 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(): if res.isError():
raise ModbusException(str(res)) raise ModbusException(str(res))
@@ -98,9 +105,12 @@ def poll_status(slave_id, interval):
state_map = {0: "Closed", 1: "Open"} state_map = {0: "Closed", 1: "Open"}
new_data["movement"] = movement_map.get(valve_state_raw >> 8, 'Unknown') new_data["movement"] = movement_map.get(valve_state_raw >> 8, 'Unknown')
new_data["state"] = state_map.get(valve_state_raw & 0xFF, '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["open_time"] = f"{hr_valve.registers[0]}s"
new_data["close_time"] = f"{hr_valve.registers[1]}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["digital_inputs"] = f"0x{ir_dig.registers[0]:04X}"
new_data["button_events"] = f"0x{ir_dig.registers[1]:04X}" new_data["button_events"] = f"0x{ir_dig.registers[1]:04X}"
new_data["digital_outputs"] = f"0x{hr_dig.registers[0]: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_minor = ir_sys.registers[0] & 0xFF
fw_patch = ir_sys.registers[1] fw_patch = ir_sys.registers[1]
uptime_seconds = (ir_sys.registers[4] << 16) | ir_sys.registers[3] 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["firmware"] = f"v{fw_major}.{fw_minor}.{fw_patch}"
new_data["device_status"] = "OK" if ir_sys.registers[2] == 0 else "ERROR" new_data["device_status"] = "OK" if ir_sys.registers[2] == 0 else "ERROR"
new_data["uptime"] = format_uptime(uptime_seconds) 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["watchdog"] = f"{hr_sys.registers[0]}s"
new_data["error"] = None # Clear any previous error on successful read new_data["error"] = None # Clear any previous error on successful read
reconnect_attempts = 0 # Reset attempts on successful communication reconnect_attempts = 0 # Reset attempts on successful communication
@@ -179,7 +191,7 @@ def file_browser(stdscr):
selected_index = 0 selected_index = 0
while True: while True:
stdscr.clear() stdscr.erase()
h, w = stdscr.getmaxyx() h, w = stdscr.getmaxyx()
stdscr.addstr(0, 0, f"Select Firmware File: {path}".ljust(w-1), curses.color_pair(2)) 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) 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)) 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 current_row_idx = 0
message, message_time = "", 0 message, message_time = "", 0
input_mode, input_prompt, input_str, input_target_reg = False, "", "", 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 == curses.KEY_BACKSPACE or key == 127: input_str = input_str[:-1]
elif key != -1 and chr(key).isprintable(): input_str += chr(key) elif key != -1 and chr(key).isprintable(): input_str += chr(key)
else: else:
if key == curses.KEY_UP: 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(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]: 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() message_time = time.time()
if selected_option == "Exit": stop_event.set(); continue 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 == "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 == "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 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: elif selected_option == "Set Max Open Time":
bit = 0 if "1" in selected_option else 1 input_mode, input_prompt, input_target_reg = True, "Enter Max Open Time (s): ", REG_HOLDING_MAX_OPENING_TIME_S
try: elif selected_option == "Set Max Close Time":
current_val = client.read_holding_registers(REG_HOLDING_DIGITAL_OUTPUTS_STATE, count=1, slave=slave_id).registers[0] input_mode, input_prompt, input_target_reg = True, "Enter Max Close Time (s): ", REG_HOLDING_MAX_CLOSING_TIME_S
client.write_register(REG_HOLDING_DIGITAL_OUTPUTS_STATE, current_val ^ (1 << bit), slave=slave_id) elif selected_option == "Set End Current Open":
message = f"-> Toggled Output {bit+1}" input_mode, input_prompt, input_target_reg = True, "Enter End Current Threshold Open (mA): ", REG_HOLDING_END_CURRENT_THRESHOLD_OPEN_MA
except Exception as e: message = f"-> Error: {e}" 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": elif selected_option == "Set Watchdog":
input_mode, input_prompt, input_target_reg = True, "Enter Watchdog Timeout (s): ", REG_HOLDING_WATCHDOG_TIMEOUT_S input_mode, input_prompt, input_target_reg = True, "Enter Watchdog Timeout (s): ", REG_HOLDING_WATCHDOG_TIMEOUT_S
elif selected_option == "Reset Node": elif selected_option == "Reset Node":
@@ -278,7 +295,7 @@ def main_menu(stdscr, slave_id):
else: else:
message = "-> Firmware update cancelled." message = "-> Firmware update cancelled."
stdscr.clear() stdscr.erase()
if is_updating: if is_updating:
with update_lock: prog, msg = update_status["progress"], update_status["message"] 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)) 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 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(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(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(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(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(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(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(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(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(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(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(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) stdscr.addstr(4, col4, "Supply V:", bold); stdscr.addstr(4, col4 + 14, str(current_data.get('supply_voltage', 'N/A')), normal)
for idx, row in enumerate(menu): stdscr.addstr(6, 0, "" * (w - 1), normal)
draw_button(stdscr, h // 2 - len(menu) + (idx * 2), w // 2 - len(row) // 2, row, idx == current_row_idx) 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 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: 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)) 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) else: curses.curs_set(0)
stdscr.refresh() curses.doupdate()
def main(): def main():
global client global client