Compare commits
53 Commits
f9d4a3b971
...
project-re
| Author | SHA1 | Date | |
|---|---|---|---|
| 6c1ff0c4df | |||
| 3f0d5a76c6 | |||
| 10a770de59 | |||
| 1b0519aadf | |||
| e429a0874d | |||
| 3a05c80b25 | |||
| 5208f1370d | |||
| a59e8518cc | |||
| 2a2890b675 | |||
| 38fd3a6aac | |||
| c3df6565b7 | |||
| 140d2baa24 | |||
| 711341f362 | |||
| a5da0a61dd | |||
| b54c73edb1 | |||
| 2418d4e218 | |||
| 2b4890f052 | |||
| 85d493f24a | |||
| f486d4c4ab | |||
| 6cfd4b8b4d | |||
| 0088030d66 | |||
| 4d828b41f1 | |||
| 95f435923f | |||
| 33f2a15cf3 | |||
| c4e87a3125 | |||
| 773027f6b0 | |||
| 461cce7a48 | |||
| 23b88ada83 | |||
| c2916662e2 | |||
| 24087f5622 | |||
| 95fd88e93e | |||
| 21797d8507 | |||
| 6dcb11ae0c | |||
| 38d6dbe95a | |||
| b100a8acf7 | |||
| 269e9e88a1 | |||
| 8cab3eecc1 | |||
| 6a9e4773ea | |||
| b836f9a2f4 | |||
| 032ddf2cc0 | |||
| 1067796df4 | |||
| 6f81e84541 | |||
| 0d3696bf93 | |||
| b005fd5c11 | |||
| 6c15b7021f | |||
| 842b204d36 | |||
| 5ce96a662d | |||
| fbeaa916b9 | |||
| 423e3947ab | |||
| 3fd816daca | |||
| 7597f5ddc0 | |||
| 5971fde145 | |||
| f008dfb6f0 |
5
.vscode/settings.json
vendored
Normal file
5
.vscode/settings.json
vendored
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
{
|
||||||
|
"files.associations": {
|
||||||
|
"fwu.h": "c"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,3 +1,7 @@
|
|||||||
|
<img src="./docs/img/logo.png" alt="Logo" width="100"/>
|
||||||
|
|
||||||
|
🇩🇪 Deutsch | [🇬🇧 English](README.md) | [🇫🇷 Français](README.fr.md) | [🇪🇸 Español](README.es.md)
|
||||||
|
|
||||||
# Modulares Bewässerungssystem
|
# Modulares Bewässerungssystem
|
||||||
|
|
||||||
Dieses Projekt realisiert ein smartes, modulares Bewässerungssystem, das über Home Assistant gesteuert wird.
|
Dieses Projekt realisiert ein smartes, modulares Bewässerungssystem, das über Home Assistant gesteuert wird.
|
||||||
@@ -9,6 +13,8 @@ Die detaillierte Dokumentation befindet sich im Verzeichnis [`docs/`](./docs/):
|
|||||||
* **[Konzept](./docs/concept.de.md)**: Beschreibt die Systemarchitektur, die verwendeten Komponenten und die grundlegenden Design-Entscheidungen.
|
* **[Konzept](./docs/concept.de.md)**: Beschreibt die Systemarchitektur, die verwendeten Komponenten und die grundlegenden Design-Entscheidungen.
|
||||||
* **[MODBUS Register](./docs/modbus-registers.de.md)**: Definiert die Register-Map für die Kommunikation mit den Slave-Nodes.
|
* **[MODBUS Register](./docs/modbus-registers.de.md)**: Definiert die Register-Map für die Kommunikation mit den Slave-Nodes.
|
||||||
* **[Projektplan](./docs/planning.de.md)**: Enthält den Entwicklungs- und Implementierungsplan.
|
* **[Projektplan](./docs/planning.de.md)**: Enthält den Entwicklungs- und Implementierungsplan.
|
||||||
|
* **[Firmware-Handbuch](./docs/firmware-manual.de.md)**: Beschreibt den Funktionsumfang und die Bedienung der Slave-Node-Firmware.
|
||||||
|
* **[Modbus Test-Tool](./software/tools/modbus_tool/README.de.md)**: Anleitung für das Python-basierte Kommandozeilen-Tool zum Testen der Slaves.
|
||||||
|
|
||||||
## Schnellstart
|
## Schnellstart
|
||||||
|
|
||||||
|
|||||||
20
README.es.md
Normal file
20
README.es.md
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
<img src="./docs/img/logo.png" alt="Logo" width="100"/>
|
||||||
|
|
||||||
|
[🇩🇪 Deutsch](README.de.md) | [🇬🇧 English](README.md) | [🇫🇷 Français](README.fr.md) | 🇪🇸 Español
|
||||||
|
|
||||||
|
# Sistema de riego modular
|
||||||
|
|
||||||
|
Este proyecto implementa un sistema de riego inteligente y modular controlado a través de Home Assistant.
|
||||||
|
|
||||||
|
## Documentación
|
||||||
|
|
||||||
|
La documentación detallada se puede encontrar en el directorio [`docs/`](./docs/):
|
||||||
|
|
||||||
|
* **[Concepto](./docs/concept.es.md)**: Describe la arquitectura del sistema, los componentes utilizados y las decisiones de diseño básicas.
|
||||||
|
* **[Registros MODBUS](./docs/modbus-registers.es.md)**: Define el mapa de registros para la comunicación con los nodos esclavos.
|
||||||
|
* **[Plan del proyecto](./docs/planning.es.md)**: Contiene el plan de desarrollo e implementación.
|
||||||
|
|
||||||
|
## Inicio rápido
|
||||||
|
|
||||||
|
* **Hardware**: Los archivos KiCad para el hardware se encuentran en el directorio [`hardware/`](./hardware/).
|
||||||
|
* **Software**: El firmware basado en Zephyr para los nodos esclavos se encuentra en el directorio [`software/`](./software/).
|
||||||
20
README.fr.md
Normal file
20
README.fr.md
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
<img src="./docs/img/logo.png" alt="Logo" width="100"/>
|
||||||
|
|
||||||
|
[🇩🇪 Deutsch](README.de.md) | [🇬🇧 English](README.md) | 🇫🇷 Français | [🇪🇸 Español](README.es.md)
|
||||||
|
|
||||||
|
# Système d'irrigation modulaire
|
||||||
|
|
||||||
|
Ce projet met en œuvre un système d'irrigation intelligent et modulaire contrôlé via Home Assistant.
|
||||||
|
|
||||||
|
## Documentation
|
||||||
|
|
||||||
|
La documentation détaillée se trouve dans le répertoire [`docs/`](./docs/) :
|
||||||
|
|
||||||
|
* **[Concept](./docs/concept.fr.md)** : Décrit l'architecture du système, les composants utilisés et les décisions de conception de base.
|
||||||
|
* **[Registres MODBUS](./docs/modbus-registers.fr.md)** : Définit la carte des registres pour la communication avec les nœuds esclaves.
|
||||||
|
* **[Plan du projet](./docs/planning.fr.md)** : Contient le plan de développement et de mise en œuvre.
|
||||||
|
|
||||||
|
## Démarrage rapide
|
||||||
|
|
||||||
|
* **Matériel** : Les fichiers KiCad pour le matériel se trouvent dans le répertoire [`hardware/`](./hardware/).
|
||||||
|
* **Logiciel** : Le firmware basé sur Zephyr pour les nœuds esclaves se trouve dans le répertoire [`software/`](./software/).
|
||||||
@@ -1,4 +1,6 @@
|
|||||||
[🇩🇪 Deutsch](README.de.md) | [🇬🇧 English](README.md)
|
<img src="./docs/img/logo.png" alt="Logo" width="100"/>
|
||||||
|
|
||||||
|
[🇩🇪 Deutsch](README.de.md) | 🇬🇧 English | [🇫🇷 Français](README.fr.md) | [🇪🇸 Español](README.es.md)
|
||||||
|
|
||||||
# Modular Irrigation System
|
# Modular Irrigation System
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
[🇩🇪 Deutsch](concept.de.md) | [🇬🇧 English](concept.en.md)
|
<img src="./img/logo.png" alt="Logo" width="100"/>
|
||||||
|
|
||||||
|
🇩🇪 Deutsch | [🇬🇧 English](concept.en.md) | [🇫🇷 Français](concept.fr.md) | [🇪🇸 Español](concept.es.md)
|
||||||
|
|
||||||
# Konzept: Modulares Bewässerungssystem
|
# Konzept: Modulares Bewässerungssystem
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
[🇩🇪 Deutsch](concept.de.md) | [🇬🇧 English](concept.en.md)
|
<img src="./img/logo.png" alt="Logo" width="100"/>
|
||||||
|
|
||||||
|
[🇩🇪 Deutsch](concept.de.md) | 🇬🇧 English | [🇫🇷 Français](concept.fr.md) | [🇪🇸 Español](concept.es.md)
|
||||||
|
|
||||||
# Concept: Modular Irrigation System
|
# Concept: Modular Irrigation System
|
||||||
|
|
||||||
|
|||||||
77
docs/concept.es.md
Normal file
77
docs/concept.es.md
Normal file
@@ -0,0 +1,77 @@
|
|||||||
|
<img src="./img/logo.png" alt="Logo" width="100"/>
|
||||||
|
|
||||||
|
[🇩🇪 Deutsch](concept.de.md) | [🇬🇧 English](concept.en.md) | [🇫🇷 Français](concept.fr.md) | 🇪🇸 Español
|
||||||
|
|
||||||
|
# Concepto: Sistema de riego modular
|
||||||
|
|
||||||
|
Este documento describe el concepto de un sistema de riego modular e inteligente, controlado de forma centralizada a través de Home Assistant.
|
||||||
|
|
||||||
|
## 1. Descripción general de la arquitectura
|
||||||
|
|
||||||
|
El sistema se divide en tres capas lógicas para garantizar una alta flexibilidad y mantenibilidad:
|
||||||
|
|
||||||
|
1. **Capa de control (Home Assistant):** Toda la lógica, las automatizaciones y la interfaz de usuario residen en Home Assistant. Este es el "cerebro" del sistema.
|
||||||
|
2. **Capa de puerta de enlace (ESP32):** Un puro traductor de protocolos que actúa como puente entre la red doméstica (WLAN/Thread) y el sistema de bus físico de la planta. No contiene lógica de control propia.
|
||||||
|
3. **Capa de actuadores/sensores (nodos esclavos):** Módulos robustos y especializados que se controlan a través de un bus y realizan las tareas reales (conmutación de válvulas, lectura de sensores).
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
## 2. Componentes del sistema
|
||||||
|
|
||||||
|
* **Depósito de agua:** Un contenedor IBC sirve como depósito de agua.
|
||||||
|
* **Suministro de agua:** Un "ladrón de lluvia" en el bajante dirige el agua de lluvia al depósito.
|
||||||
|
* **Bomba:** Una bomba con un tanque de expansión de presión integrado proporciona la presión de agua necesaria.
|
||||||
|
* **Actuadores:** Válvulas de bola motorizadas de 12V para controlar las salidas de agua.
|
||||||
|
* **Sensor de nivel (preciso):** Un `QDY30A` con salida de 4-20mA e interfaz RS485/MODBUS para la medición continua del nivel del agua.
|
||||||
|
* **Sensores de nivel (Min/Max):** Sensores capacitivos opcionales (`XKC-Y25-NPN` o similar) como protección redundante contra el funcionamiento en seco y el desbordamiento.
|
||||||
|
|
||||||
|
## 3. Puerta de enlace
|
||||||
|
|
||||||
|
La interfaz de comunicación central se implementa como una puerta de enlace "tonta".
|
||||||
|
|
||||||
|
* **Hardware:** Una placa basada en `ESP32C6`.
|
||||||
|
* **Función:** La puerta de enlace actúa como un **convertidor transparente de MODBUS TCP/IP a MODBUS RTU**. Recibe comandos de la red doméstica y los reenvía al bus RS485 y viceversa. No ejecuta su propia lógica de control.
|
||||||
|
* **Conexión a Home Assistant:** La conexión se realiza a través de la red doméstica, ya sea a través de WLAN o en el futuro posiblemente a través de Thread/Matter. En Home Assistant, la integración oficial de MODBUS se utiliza para direccionar directamente la puerta de enlace y los esclavos detrás de ella.
|
||||||
|
|
||||||
|
## 4. Nodos esclavos
|
||||||
|
|
||||||
|
Los nodos esclavos son las unidades de trabajo en el campo. Para mantener bajo el esfuerzo en la producción de series pequeñas (por ejemplo, en JLCPCB), se busca un diseño de placa universal para todos los tipos de esclavos.
|
||||||
|
|
||||||
|
* **Microcontrolador:** Un `STM32G431PB`. Aunque potente, ofrece todos los periféricos necesarios (múltiples UART, ADC, CAN) y permite un diseño de hardware y software uniforme.
|
||||||
|
* **Periféricos por nodo:**
|
||||||
|
* **Dos salidas de lado alto (+12V):** Realizadas a través de un `VND7050AJ`. Perfecto para controlar las válvulas de motor de 12V (`Abrir`/`Cerrar`). La línea `Sense` del controlador se lee a través de un convertidor AD para realizar una detección de posición final sin interruptores de límite físicos midiendo la corriente del motor (corriente del motor en reposo ≈ 0).
|
||||||
|
* **Dos salidas de lado bajo (0V):** Salidas conmutadas a través de MOSFET de canal N. Se pueden utilizar para controlar LED de 12V en botones o para conmutar el relé de estado sólido para la bomba.
|
||||||
|
* **Dos entradas digitales:** Entradas directas y protegidas en el controlador para conectar botones o los sensores NPN capacitivos.
|
||||||
|
|
||||||
|
## 5. Sistema de bus: MODBUS-RTU a través de RS485
|
||||||
|
|
||||||
|
MODBUS-RTU se utiliza de forma consistente como sistema de bus.
|
||||||
|
|
||||||
|
* **Justificación:** Esta elección es pragmática, ya que el sensor de nivel ya requiere MODBUS. Esto significa que solo se requiere un único protocolo simple y extendido para todo el sistema.
|
||||||
|
* **Capa física:** El cableado se realiza a través de RS485. Se utiliza un cable Ethernet Cat-7 disponible en el mercado con conectores RJ45:
|
||||||
|
* 1 par trenzado para las señales de bus `A` y `B`.
|
||||||
|
* 3 pares de cables en paralelo para `+12V` y `GND` para suministrar energía a los esclavos.
|
||||||
|
|
||||||
|
## 6. Software
|
||||||
|
|
||||||
|
* **Sistema operativo (esclavos):** `Zephyr OS`. Es un sistema operativo en tiempo real moderno y potente que permite una estructura de firmware limpia y mantenible.
|
||||||
|
* **Implementación de la lógica:** Toda la lógica de control (por ejemplo, "Si el nivel < 20% y el día de la semana = lunes, entonces enciende la válvula 3 durante 10 minutos") se asigna exclusivamente en **Home Assistant** a través de su motor de automatización.
|
||||||
|
|
||||||
|
### 6.1. Actualización del firmware de los esclavos (OTA)
|
||||||
|
|
||||||
|
El firmware de los esclavos se puede actualizar durante el funcionamiento a través del bus sin necesidad de acceso físico directo.
|
||||||
|
|
||||||
|
* **Concepto:** Se utiliza el cargador de arranque `MCUBoot`. Este está desacoplado del protocolo de comunicación.
|
||||||
|
* **Procedimiento:**
|
||||||
|
1. Un script en Home Assistant lee el nuevo archivo de firmware (`firmware.bin`).
|
||||||
|
2. El script divide el archivo en pequeños paquetes de datos y los envía uno tras otro al esclavo de destino mediante un comando MODBUS.
|
||||||
|
3. La aplicación en ejecución en el esclavo recibe estos paquetes y los escribe directamente en la memoria flash secundaria ("ranura de actualización").
|
||||||
|
4. Después de una transmisión exitosa, el esclavo se reinicia por comando.
|
||||||
|
5. `MCUBoot` comprueba la firma de la nueva imagen, la copia en la ranura principal y la inicia.
|
||||||
|
* **Seguridad:** El nuevo firmware debe marcarse a sí mismo como "funcional" después de iniciarse. Si no lo hace (por ejemplo, debido a un bloqueo), la versión de firmware anterior y estable es restaurada automáticamente por `MCUBoot` en el siguiente reinicio por el watchdog.
|
||||||
|
|
||||||
|
## 7. Conceptos de seguridad y robustez
|
||||||
|
|
||||||
|
* **Comportamiento a prueba de fallos:** Cada nodo esclavo implementa un watchdog. Si no se recibe ninguna consulta MODBUS válida de la puerta de enlace durante un período de tiempo definido (por ejemplo, 15 segundos), el esclavo pasa a un estado seguro: todas las válvulas se cierran y los relés (por ejemplo, para la bomba) se apagan.
|
||||||
|
* **Circuitos de protección eléctrica:** Todas las interfaces externas están protegidas. Las líneas de bus RS485 (`A`/`B`) están protegidas contra sobretensiones con diodos TVS en cada placa. Las entradas y salidas reciben protección ESD básica.
|
||||||
|
* **Fuente de alimentación:** La tensión del bus de 12V se regula en cada nodo esclavo con un convertidor reductor `TPS5430DDAR` eficiente a los 3.3V necesarios para el microcontrolador y los controladores del bus.
|
||||||
77
docs/concept.fr.md
Normal file
77
docs/concept.fr.md
Normal file
@@ -0,0 +1,77 @@
|
|||||||
|
<img src="./img/logo.png" alt="Logo" width="100"/>
|
||||||
|
|
||||||
|
[🇩🇪 Deutsch](concept.de.md) | [🇬🇧 English](concept.en.md) | 🇫🇷 Français | [🇪🇸 Español](concept.es.md)
|
||||||
|
|
||||||
|
# Concept : Système d'irrigation modulaire
|
||||||
|
|
||||||
|
Ce document décrit le concept d'un système d'irrigation modulaire et intelligent, contrôlé de manière centralisée via Home Assistant.
|
||||||
|
|
||||||
|
## 1. Aperçu de l'architecture
|
||||||
|
|
||||||
|
Le système est divisé en trois couches logiques pour garantir une flexibilité et une maintenabilité élevées :
|
||||||
|
|
||||||
|
1. **Couche de contrôle (Home Assistant) :** Toute la logique, les automatisations et l'interface utilisateur résident dans Home Assistant. C'est le "cerveau" du système.
|
||||||
|
2. **Couche de passerelle (ESP32) :** Un simple traducteur de protocole qui sert de pont entre le réseau domestique (WLAN/Thread) et le système de bus physique de l'installation. Il ne contient aucune logique de contrôle propre.
|
||||||
|
3. **Couche d'acteurs/capteurs (nœuds esclaves) :** Des modules robustes et spécialisés, commandés via un bus, qui exécutent les tâches réelles (commutation de vannes, lecture de capteurs).
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
## 2. Composants du système
|
||||||
|
|
||||||
|
* **Réservoir d'eau :** Un conteneur IBC sert de réservoir d'eau.
|
||||||
|
* **Alimentation en eau :** Un "récupérateur d'eau de pluie" sur le tuyau de descente dirige l'eau de pluie dans le réservoir.
|
||||||
|
* **Pompe :** Une pompe avec un vase d'expansion intégré assure la pression d'eau nécessaire.
|
||||||
|
* **Actionneurs :** Des vannes à boisseau sphérique motorisées de 12V pour contrôler les sorties d'eau.
|
||||||
|
* **Capteur de niveau (précis) :** Un `QDY30A` avec une sortie 4-20mA et une interface RS485/MODBUS pour la mesure continue du niveau d'eau.
|
||||||
|
* **Capteurs de niveau (Min/Max) :** Des capteurs capacitifs optionnels (`XKC-Y25-NPN` ou similaire) comme protection redondante contre le fonctionnement à sec et le débordement.
|
||||||
|
|
||||||
|
## 3. Passerelle
|
||||||
|
|
||||||
|
L'interface de communication centrale est réalisée sous la forme d'une passerelle "stupide".
|
||||||
|
|
||||||
|
* **Matériel :** Une carte basée sur `ESP32C6`.
|
||||||
|
* **Fonction :** La passerelle agit comme un **convertisseur transparent MODBUS TCP/IP vers MODBUS RTU**. Elle reçoit les commandes du réseau domestique et les transmet au bus RS485 et vice versa. Elle n'exécute aucune logique de contrôle propre.
|
||||||
|
* **Connexion à Home Assistant :** La connexion se fait via le réseau domestique, soit par WLAN, soit à l'avenir éventuellement par Thread/Matter. Dans Home Assistant, l'intégration MODBUS officielle est utilisée pour adresser directement la passerelle et les esclaves qui se trouvent derrière.
|
||||||
|
|
||||||
|
## 4. Nœuds esclaves
|
||||||
|
|
||||||
|
Les nœuds esclaves sont les unités de travail sur le terrain. Pour réduire les efforts lors de la production en petites séries (par ex. chez JLCPCB), un design de platine universel pour tous les types d'esclaves est visé.
|
||||||
|
|
||||||
|
* **Microcontrôleur :** Un `STM32G431PB`. Bien que puissant, il offre toutes les périphériques nécessaires (plusieurs UART, ADC, CAN) et permet une conception matérielle et logicielle uniforme.
|
||||||
|
* **Périphériques par nœud :**
|
||||||
|
* **Deux sorties High-Side (+12V) :** Réalisées via un `VND7050AJ`. Parfait pour commander les vannes motorisées 12V (`Ouvrir`/`Fermer`). La ligne `Sense` du pilote est lue via un convertisseur AN pour réaliser une détection de fin de course sans interrupteurs de fin de course physiques en mesurant le courant du moteur (courant du moteur à l'arrêt ≈ 0).
|
||||||
|
* **Deux sorties Low-Side (0V) :** Sorties commutées via des MOSFET à canal N. Utilisables pour commander des LED 12V dans des boutons-poussoirs ou pour commuter le relais à semi-conducteurs pour la pompe.
|
||||||
|
* **Deux entrées numériques :** Entrées directes et protégées sur le contrôleur pour connecter des boutons-poussoirs ou les capteurs NPN capacitifs.
|
||||||
|
|
||||||
|
## 5. Système de bus : MODBUS-RTU via RS485
|
||||||
|
|
||||||
|
Le système de bus repose systématiquement sur MODBUS-RTU.
|
||||||
|
|
||||||
|
* **Justification :** Ce choix est pragmatique, car le capteur de niveau requiert déjà MODBUS. Ainsi, un seul protocole simple et largement répandu est nécessaire pour l'ensemble du système.
|
||||||
|
* **Couche physique :** Le câblage est réalisé via RS485. Un câble Ethernet Cat-7 du commerce avec des connecteurs RJ45 est utilisé :
|
||||||
|
* 1 paire torsadée pour les signaux de bus `A` et `B`.
|
||||||
|
* 3 paires de fils en parallèle pour `+12V` et `GND` pour l'alimentation des esclaves.
|
||||||
|
|
||||||
|
## 6. Logiciel
|
||||||
|
|
||||||
|
* **Système d'exploitation (esclaves) :** `Zephyr OS`. C'est un système d'exploitation temps réel moderne et performant qui permet une structure de firmware propre et maintenable.
|
||||||
|
* **Implémentation de la logique :** Toute la logique de commande (par ex. "Si niveau < 20% et jour de la semaine = Lundi, alors activer la vanne 3 pendant 10 minutes") est exclusivement représentée dans **Home Assistant** via son moteur d'automatisation.
|
||||||
|
|
||||||
|
### 6.1. Mise à jour du firmware des esclaves (OTA)
|
||||||
|
|
||||||
|
Le firmware des esclaves peut être mis à jour en cours de fonctionnement via le bus, sans nécessiter d'accès physique direct.
|
||||||
|
|
||||||
|
* **Concept :** Le bootloader `MCUBoot` est utilisé. Celui-ci est découplé du protocole de communication.
|
||||||
|
* **Déroulement :**
|
||||||
|
1. Un script dans Home Assistant lit le nouveau fichier de firmware (`firmware.bin`).
|
||||||
|
2. Le script décompose le fichier en petits paquets de données et les envoie successivement à l'esclave cible par commande MODBUS.
|
||||||
|
3. L'application en cours d'exécution sur l'esclave reçoit ces paquets et les écrit directement dans la mémoire flash secondaire ("slot de mise à jour").
|
||||||
|
4. Après une transmission réussie, l'esclave est redémarré par commande.
|
||||||
|
5. `MCUBoot` vérifie la signature de la nouvelle image, la copie dans le slot primaire et la démarre.
|
||||||
|
* **Sécurité :** Le nouveau firmware doit se marquer comme "fonctionnel" après le démarrage. S'il ne le fait pas (par ex. à cause d'un crash), la version précédente et stable du firmware est automatiquement restaurée par `MCUBoot` au prochain redémarrage par le watchdog.
|
||||||
|
|
||||||
|
## 7. Concepts de sécurité et de robustesse
|
||||||
|
|
||||||
|
* **Comportement Fail-Safe :** Chaque nœud esclave implémente un watchdog. Si aucune requête MODBUS valide n'est reçue de la passerelle pendant une période définie (par ex. 15 secondes), le nœud esclave passe dans un état sûr : toutes les vannes sont fermées et les relais (par ex. pour la pompe) sont désactivés.
|
||||||
|
* **Circuits de protection électrique :** Toutes les interfaces externes sont protégées. Les lignes de bus RS485 (`A`/`B`) sont protégées contre les surtensions par des diodes TVS sur chaque platine. Les entrées et les sorties reçoivent une protection ESD de base.
|
||||||
|
* **Alimentation électrique :** La tension de bus de 12V est régulée sur chaque nœud esclave avec un convertisseur abaisseur `TPS5430DDAR` efficace pour fournir les 3.3V nécessaires au microcontrôleur et aux pilotes de bus.
|
||||||
@@ -1,78 +1,77 @@
|
|||||||
<svg xmlns="http://www.w3.org/2000/svg" width="203.36" height="143.44" version="1.1" viewBox="0 0 203.36 143.44">
|
<svg width="650" height="500" viewBox="0 0 650 500" xmlns="http://www.w3.org/2000/svg">
|
||||||
<g transform="translate(-2.88 -78.28)">
|
<defs>
|
||||||
<g stroke-width=".26458">
|
<style>
|
||||||
<g fill="#fff" stroke="#000">
|
.box {
|
||||||
<rect x="3.14" y="78.54" width="88.9" height="21.16" rx="2" ry="2"/>
|
fill: #f0f7ff;
|
||||||
<rect x="3.14" y="105.4" width="88.9" height="21.16" rx="2" ry="2"/>
|
stroke: #0d47a1;
|
||||||
<rect x="3.14" y="132.26" width="88.9" height="21.16" rx="2" ry="2"/>
|
stroke-width: 1.5;
|
||||||
</g>
|
rx: 8;
|
||||||
<text x="47.59" y="86.9" font-family="sans-serif" font-size="5.64" letter-spacing="0" stroke-width=".26458" text-align="center" word-spacing="0">
|
}
|
||||||
<tspan x="47.59" y="86.9" text-anchor="middle">Control Layer</tspan>
|
.label-main {
|
||||||
<tspan x="47.59" y="93.45" text-anchor="middle">(Home Assistant)</tspan>
|
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
|
||||||
</text>
|
font-size: 16px;
|
||||||
<text x="47.59" y="113.76" font-family="sans-serif" font-size="5.64" letter-spacing="0" stroke-width=".26458" text-align="center" word-spacing="0">
|
font-weight: 500;
|
||||||
<tspan x="47.59" y="113.76" text-anchor="middle">Gateway Layer</tspan>
|
text-anchor: middle;
|
||||||
<tspan x="47.59" y="120.31" text-anchor="middle">(ESP32)</tspan>
|
fill: #111;
|
||||||
</text>
|
}
|
||||||
<text x="47.59" y="140.62" font-family="sans-serif" font-size="5.64" letter-spacing="0" stroke-width=".26458" text-align="center" word-spacing="0">
|
.label-sub {
|
||||||
<tspan x="47.59" y="140.62" text-anchor="middle">Actor/Sensor Layer</tspan>
|
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
|
||||||
<tspan x="47.59" y="147.17" text-anchor="middle">(Slaves)</tspan>
|
font-size: 12px;
|
||||||
</text>
|
text-anchor: middle;
|
||||||
</g>
|
fill: #444;
|
||||||
<path d="m47.59 99.7v5.7m-2.82-2.88h5.64" fill="none" stroke="#000" stroke-linecap="round" stroke-width="1.0583"/>
|
}
|
||||||
<path d="m47.59 126.56v5.7m-2.82-2.88h5.64" fill="none" stroke="#000" stroke-linecap="round" stroke-width="1.0583"/>
|
.label-arrow {
|
||||||
<g stroke-width=".26458">
|
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
|
||||||
<g transform="translate(100.58 -2.64)">
|
font-size: 13px;
|
||||||
<g fill="#fff" stroke="#000">
|
text-anchor: middle;
|
||||||
<rect x="3.14" y="108.04" width="88.9" height="21.16" rx="2" ry="2"/>
|
fill: #333;
|
||||||
<rect x="3.14" y="134.9" width="88.9" height="21.16" rx="2" ry="2"/>
|
}
|
||||||
<rect x="3.14" y="161.76" width="88.9" height="21.16" rx="2" ry="2"/>
|
.arrow-line {
|
||||||
<rect x="3.14" y="188.62" width="88.9" height="21.16" rx="2" ry="2"/>
|
stroke: #333;
|
||||||
</g>
|
stroke-width: 2;
|
||||||
<text x="47.59" y="118.94" font-family="sans-serif" font-size="5.64" letter-spacing="0" stroke-width=".26458" text-align="center" word-spacing="0">
|
}
|
||||||
<tspan x="47.59" y="118.94" text-anchor="middle">Level Sensor</tspan>
|
.arrow-head {
|
||||||
<tspan x="47.59" y="125.49" font-size="4.23" text-anchor="middle">(MODBUS Slave)</tspan>
|
fill: #333;
|
||||||
</text>
|
}
|
||||||
<text x="47.59" y="145.8" font-family="sans-serif" font-size="5.64" letter-spacing="0" stroke-width=".26458" text-align="center" word-spacing="0">
|
.bus-line {
|
||||||
<tspan x="47.59" y="145.8" text-anchor="middle">Valve Control 1</tspan>
|
stroke: #212121;
|
||||||
<tspan x="47.59" y="152.35" font-size="4.23" text-anchor="middle">(MODBUS Slave)</tspan>
|
stroke-width: 4;
|
||||||
</text>
|
}
|
||||||
<text x="47.59" y="172.66" font-family="sans-serif" font-size="5.64" letter-spacing="0" stroke-width=".26458" text-align="center" word-spacing="0">
|
.bus-connector {
|
||||||
<tspan x="47.59" y="172.66" text-anchor="middle">Valve Control n</tspan>
|
stroke: #212121;
|
||||||
<tspan x="47.59" y="179.21" font-size="4.23" text-anchor="middle">(MODBUS Slave)</tspan>
|
stroke-width: 2;
|
||||||
</text>
|
}
|
||||||
<text x="47.59" y="199.52" font-family="sans-serif" font-size="5.64" letter-spacing="0" stroke-width=".26458" text-align="center" word-spacing="0">
|
</style>
|
||||||
<tspan x="47.59" y="199.52" text-anchor="middle">Pump</tspan>
|
<marker id="arrowhead" viewBox="0 0 10 10" refX="5" refY="5" markerWidth="6" markerHeight="6" orient="auto-start-reverse">
|
||||||
<tspan x="47.59" y="206.07" font-size="4.23" text-anchor="middle">(switched via Slave)</tspan>
|
<path d="M 0 0 L 10 5 L 0 10 z" fill="#333" />
|
||||||
</text>
|
</marker>
|
||||||
</g>
|
</defs>
|
||||||
<path d="m148.17 132.26v-18.52m-2.82 2.82 2.82-2.82 2.82 2.82" fill="none" stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width=".529"/>
|
|
||||||
<path d="m148.17 159.12v-18.52m-2.82 2.82 2.82-2.82 2.82 2.82" fill="none" stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width=".529"/>
|
<text x="325" y="30" class="label-main" font-size="20">System Architecture</text>
|
||||||
<path d="m148.17 185.98v-18.52m-2.82 2.82 2.82-2.82 2.82 2.82" fill="none" stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width=".529"/>
|
<rect x="225" y="60" width="200" height="70" class="box"/>
|
||||||
<path d="m148.17 212.84v-18.52m-2.82 2.82 2.82-2.82 2.82 2.82" fill="none" stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width=".529"/>
|
<text x="325" y="90" class="label-main">Home Assistant</text>
|
||||||
</g>
|
<text x="325" y="110" class="label-sub">(Logic & UI)</text>
|
||||||
<g fill="none" stroke="#000" stroke-width=".52917">
|
<line x1="325" y1="130" x2="325" y2="170" class="arrow-line" marker-end="url(#arrowhead)"/>
|
||||||
<path d="m92.04 89.12h10.58"/>
|
<text x="445" y="155" class="label-arrow">WLAN / Thread</text>
|
||||||
<path d="m92.04 116h10.58"/>
|
<text x="445" y="170" class="label-arrow" font-size="11">(MODBUS TCP/IP)</text>
|
||||||
</g>
|
<rect x="225" y="180" width="200" height="70" class="box"/>
|
||||||
<path d="m102.62 89.12v-5.29h45.55v26.45" fill="none" stroke="#000" stroke-width=".52917"/>
|
<text x="325" y="210" class="label-main">Gateway (ESP32C6)</text>
|
||||||
<path d="m102.62 116h45.55v-23.8h-45.55z" fill="#fff" stroke="#000" stroke-width=".26458"/>
|
<text x="325" y="230" class="label-sub">(Protocol Translator)</text>
|
||||||
<text x="125.4" y="100.3" font-family="sans-serif" font-size="4.23" letter-spacing="0" stroke-width=".26458" text-align="center" word-spacing="0">
|
<line x1="325" y1="250" x2="325" y2="300" class="bus-line"/>
|
||||||
<tspan x="125.4" y="100.3" text-anchor="middle">Home Network</tspan>
|
<line x1="50" y1="300" x2="600" y2="300" class="bus-line"/>
|
||||||
<tspan x="125.4" y="105.11" text-anchor="middle">(WLAN / Thread)</tspan>
|
<text x="500" y="285" class="label-arrow">RS485 Bus (MODBUS RTU)</text>
|
||||||
</text>
|
<line x1="125" y1="300" x2="125" y2="340" class="bus-connector"/>
|
||||||
<text x="125.4" y="114.09" font-family="sans-serif" font-size="4.23" letter-spacing="0" stroke-width=".26458" text-align="center" word-spacing="0">
|
<rect x="50" y="340" width="150" height="60" class="box"/>
|
||||||
<tspan x="125.4" y="114.09" text-anchor="middle">MODBUS TCP/IP</tspan>
|
<text x="125" y="365" class="label-main">Slave Node</text>
|
||||||
</text>
|
<text x="125" y="385" class="label-sub">(Valve, Button)</text>
|
||||||
<path d="m102.62 142.84h45.55v-23.8h-45.55z" fill="#fff" stroke="#000" stroke-width=".26458"/>
|
<line x1="325" y1="300" x2="325" y2="340" class="bus-connector"/>
|
||||||
<text x="125.4" y="127.15" font-family="sans-serif" font-size="4.23" letter-spacing="0" stroke-width=".26458" text-align="center" word-spacing="0">
|
<rect x="250" y="340" width="150" height="60" class="box"/>
|
||||||
<tspan x="125.4" y="127.15" text-anchor="middle">Gateway</tspan>
|
<text x="325" y="365" class="label-main">Slave Node</text>
|
||||||
<tspan x="125.4" y="131.96" text-anchor="middle">(Protocol Translator)</tspan>
|
<text x="325" y="385" class="label-sub">(Pump, Sensors)</text>
|
||||||
</text>
|
<line x1="525" y1="300" x2="525" y2="340" class="bus-connector"/>
|
||||||
<text x="125.4" y="140.94" font-family="sans-serif" font-size="4.23" letter-spacing="0" stroke-width=".26458" text-align="center" word-spacing="0">
|
<rect x="450" y="340" width="150" height="60" class="box"/>
|
||||||
<tspan x="125.4" y="140.94" text-anchor="middle">MODBUS RTU</tspan>
|
<text x="525" y="365" class="label-main">Level Sensor</text>
|
||||||
<tspan x="125.4" y="145.75" text-anchor="middle">(RS485 Bus)</tspan>
|
<text x="525" y="385" class="label-sub">(QDY30A)</text>
|
||||||
</text>
|
<text x="325" y="440" class="label-sub" font-size="20">...</text>
|
||||||
<path d="m92.04 142.84h10.58"/>
|
|
||||||
</g>
|
|
||||||
</svg>
|
</svg>
|
||||||
|
Before Width: | Height: | Size: 5.5 KiB After Width: | Height: | Size: 3.1 KiB |
77
docs/img/architecture.es.svg
Normal file
77
docs/img/architecture.es.svg
Normal file
@@ -0,0 +1,77 @@
|
|||||||
|
<svg width="650" height="500" viewBox="0 0 650 500" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<defs>
|
||||||
|
<style>
|
||||||
|
.box {
|
||||||
|
fill: #f0f7ff;
|
||||||
|
stroke: #0d47a1;
|
||||||
|
stroke-width: 1.5;
|
||||||
|
rx: 8;
|
||||||
|
}
|
||||||
|
.label-main {
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 500;
|
||||||
|
text-anchor: middle;
|
||||||
|
fill: #111;
|
||||||
|
}
|
||||||
|
.label-sub {
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
|
||||||
|
font-size: 12px;
|
||||||
|
text-anchor: middle;
|
||||||
|
fill: #444;
|
||||||
|
}
|
||||||
|
.label-arrow {
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
|
||||||
|
font-size: 13px;
|
||||||
|
text-anchor: middle;
|
||||||
|
fill: #333;
|
||||||
|
}
|
||||||
|
.arrow-line {
|
||||||
|
stroke: #333;
|
||||||
|
stroke-width: 2;
|
||||||
|
}
|
||||||
|
.arrow-head {
|
||||||
|
fill: #333;
|
||||||
|
}
|
||||||
|
.bus-line {
|
||||||
|
stroke: #212121;
|
||||||
|
stroke-width: 4;
|
||||||
|
}
|
||||||
|
.bus-connector {
|
||||||
|
stroke: #212121;
|
||||||
|
stroke-width: 2;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
<marker id="arrowhead" viewBox="0 0 10 10" refX="5" refY="5" markerWidth="6" markerHeight="6" orient="auto-start-reverse">
|
||||||
|
<path d="M 0 0 L 10 5 L 0 10 z" fill="#333" />
|
||||||
|
</marker>
|
||||||
|
</defs>
|
||||||
|
|
||||||
|
<text x="325" y="30" class="label-main" font-size="20">Arquitectura del sistema</text>
|
||||||
|
<rect x="225" y="60" width="200" height="70" class="box"/>
|
||||||
|
<text x="325" y="90" class="label-main">Home Assistant</text>
|
||||||
|
<text x="325" y="110" class="label-sub">(Lógica e Interfaz de Usuario)</text>
|
||||||
|
<line x1="325" y1="130" x2="325" y2="170" class="arrow-line" marker-end="url(#arrowhead)"/>
|
||||||
|
<text x="445" y="155" class="label-arrow">WLAN / Thread</text>
|
||||||
|
<text x="445" y="170" class="label-arrow" font-size="11">(MODBUS TCP/IP)</text>
|
||||||
|
<rect x="225" y="180" width="200" height="70" class="box"/>
|
||||||
|
<text x="325" y="210" class="label-main">Pasarela (ESP32C6)</text>
|
||||||
|
<text x="325" y="230" class="label-sub">(Traductor de protocolo)</text>
|
||||||
|
<line x1="325" y1="250" x2="325" y2="300" class="bus-line"/>
|
||||||
|
<line x1="50" y1="300" x2="600" y2="300" class="bus-line"/>
|
||||||
|
<text x="500" y="285" class="label-arrow">Bus RS485 (MODBUS RTU)</text>
|
||||||
|
<line x1="125" y1="300" x2="125" y2="340" class="bus-connector"/>
|
||||||
|
<rect x="50" y="340" width="150" height="60" class="box"/>
|
||||||
|
<text x="125" y="365" class="label-main">Nodo esclavo</text>
|
||||||
|
<text x="125" y="385" class="label-sub">(Válvula, Botón)</text>
|
||||||
|
<line x1="325" y1="300" x2="325" y2="340" class="bus-connector"/>
|
||||||
|
<rect x="250" y="340" width="150" height="60" class="box"/>
|
||||||
|
<text x="325" y="365" class="label-main">Nodo esclavo</text>
|
||||||
|
<text x="325" y="385" class="label-sub">(Bomba, Sensores)</text>
|
||||||
|
<line x1="525" y1="300" x2="525" y2="340" class="bus-connector"/>
|
||||||
|
<rect x="450" y="340" width="150" height="60" class="box"/>
|
||||||
|
<text x="525" y="365" class="label-main">Sensor de nivel</text>
|
||||||
|
<text x="525" y="385" class="label-sub">(QDY30A)</text>
|
||||||
|
<text x="325" y="440" class="label-sub" font-size="20">...</text>
|
||||||
|
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 3.1 KiB |
78
docs/img/architecture.fr.svg
Normal file
78
docs/img/architecture.fr.svg
Normal file
@@ -0,0 +1,78 @@
|
|||||||
|
<svg width="650" height="500" viewBox="0 0 650 500" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<defs>
|
||||||
|
<style>
|
||||||
|
.box {
|
||||||
|
fill: #f0f7ff;
|
||||||
|
stroke: #0d47a1;
|
||||||
|
stroke-width: 1.5;
|
||||||
|
rx: 8;
|
||||||
|
}
|
||||||
|
.label-main {
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 500;
|
||||||
|
text-anchor: middle;
|
||||||
|
fill: #111;
|
||||||
|
}
|
||||||
|
.label-sub {
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
|
||||||
|
font-size: 12px;
|
||||||
|
text-anchor: middle;
|
||||||
|
fill: #444;
|
||||||
|
}
|
||||||
|
.label-arrow {
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
|
||||||
|
font-size: 13px;
|
||||||
|
text-anchor: middle;
|
||||||
|
fill: #333;
|
||||||
|
}
|
||||||
|
.arrow-line {
|
||||||
|
stroke: #333;
|
||||||
|
stroke-width: 2;
|
||||||
|
}
|
||||||
|
.arrow-head {
|
||||||
|
fill: #333;
|
||||||
|
}
|
||||||
|
.bus-line {
|
||||||
|
stroke: #212121;
|
||||||
|
stroke-width: 4;
|
||||||
|
}
|
||||||
|
.bus-connector {
|
||||||
|
stroke: #212121;
|
||||||
|
stroke-width: 2;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
<marker id="arrowhead" viewBox="0 0 10 10" refX="5" refY="5" markerWidth="6" markerHeight="6" orient="auto-start-reverse">
|
||||||
|
<path d="M 0 0 L 10 5 L 0 10 z" fill="#333" />
|
||||||
|
</marker>
|
||||||
|
</defs>
|
||||||
|
|
||||||
|
<text x="325" y="30" class="label-main" font-size="20">Architecture du système</text>
|
||||||
|
<rect x="225" y="60" width="200" height="70" class="box"/>
|
||||||
|
<text x="325" y="90" class="label-main">Home Assistant</text>
|
||||||
|
<text x="325" y="110" class="label-sub">(Logique & UI)</text>
|
||||||
|
<line x1="325" y1="130" x2="325" y2="170" class="arrow-line" marker-end="url(#arrowhead)"/>
|
||||||
|
<text x="445" y="155" class="label-arrow">WLAN / Thread</text>
|
||||||
|
<text x="445" y="170" class="label-arrow" font-size="11">(MODBUS TCP/IP)</text>
|
||||||
|
<rect x="225" y="180" width="200" height="70" class="box"/>
|
||||||
|
<text x="325" y="210" class="label-main">Passerelle (ESP32C6)</text>
|
||||||
|
<text x="325" y="230" class="label-sub">(Traducteur de protocole)</text>
|
||||||
|
<line x1="325" y1="250" x2="325" y2="300" class="bus-line"/>
|
||||||
|
<line x1="50" y1="300" x2="600" y2="300" class="bus-line"/>
|
||||||
|
<text x="500" y="285" class="label-arrow">Bus RS485 (MODBUS RTU)</text>
|
||||||
|
<line x1="125" y1="300" x2="125" y2="340" class="bus-connector"/>
|
||||||
|
<rect x="50" y="340" width="150" height="60" class="box"/>
|
||||||
|
<text x="125" y="365" class="label-main">Nœud esclave</text>
|
||||||
|
<text x="125" y="385" class="label-sub">(Vanne, Bouton)</text>
|
||||||
|
<line x1="325" y1="300" x2="325" y2="340" class="bus-connector"/>
|
||||||
|
<rect x="250" y="340" width="150" height="60" class="box"/>
|
||||||
|
<text x="325" y="365" class="label-main">Nœud esclave</text>
|
||||||
|
<text x="325" y="385" class="label-sub">(Pompe, Capteurs)</text>
|
||||||
|
<line x1="525" y1="300" x2="525" y2="340" class="bus-connector"/>
|
||||||
|
<rect x="450" y="340" width="150" height="60" class="box"/>
|
||||||
|
<text x="525" y="365" class="label-main">Capteur de niveau</text>
|
||||||
|
<text x="525" y="385" class="label-sub">(QDY30A)</text>
|
||||||
|
<text x="325" y="440" class="label-sub" font-size="20">...</text>
|
||||||
|
|
||||||
|
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 3.1 KiB |
BIN
docs/img/logo.png
Normal file
BIN
docs/img/logo.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 272 KiB |
16
docs/img/logo.svg
Normal file
16
docs/img/logo.svg
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
<svg width="100" height="120" viewBox="0 0 100 120" xmlns="http://www.w3.org/2000/svg" aria-labelledby="logoTitle">
|
||||||
|
<title id="logoTitle">Logo: Wassertropfen mit integriertem Chip</title>
|
||||||
|
|
||||||
|
<path
|
||||||
|
d="M50 115 C 85 85, 95 65, 95 45 A 45 45 0 1 0 5 45 C 5 65, 15 85, 50 115 Z"
|
||||||
|
fill="#2563EB"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<g fill="#FFFFFF">
|
||||||
|
<rect x="25" y="35" width="50" height="8" rx="2"/>
|
||||||
|
<rect x="25" y="50" width="35" height="8" rx="2"/>
|
||||||
|
<rect x="70" y="50" width="5" height="8" rx="2"/>
|
||||||
|
<rect x="25" y="65" width="50" height="8" rx="2"/>
|
||||||
|
</g>
|
||||||
|
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 568 B |
@@ -1,4 +1,6 @@
|
|||||||
[🇩🇪 Deutsch](modbus-registers.de.md) | [🇬🇧 English](modbus-registers.en.md)
|
<img src="./img/logo.png" alt="Logo" width="100"/>
|
||||||
|
|
||||||
|
🇩🇪 Deutsch | [🇬🇧 English](modbus-registers.en.md) | [🇫🇷 Français](modbus-registers.fr.md) | [🇪🇸 Español](modbus-registers.es.md)
|
||||||
|
|
||||||
# MODBUS Register Map Definition v1.0
|
# MODBUS Register Map Definition v1.0
|
||||||
|
|
||||||
@@ -47,6 +49,7 @@ Alle Register sind in einer einzigen, durchgehenden Liste pro Register-Typ (`Inp
|
|||||||
| **0x0002** | `MAX_SCHLIESSZEIT_S` | Ventil | Sicherheits-Timeout in Sekunden für den Schliessen-Vorgang. |
|
| **0x0002** | `MAX_SCHLIESSZEIT_S` | Ventil | Sicherheits-Timeout in Sekunden für den Schliessen-Vorgang. |
|
||||||
| **0x0010** | `DIGITAL_AUSGAENGE_ZUSTAND` | Ausgänge | Bitmaske zum Lesen und Schreiben der Ausgänge. Bit 0: Ausgang 1, Bit 1: Ausgang 2. `1`=AN, `0`=AUS. |
|
| **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. |
|
||||||
| **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. |
|
||||||
| **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. |
|
||||||
| **0x0101** | `FWU_CHUNK_OFFSET_LOW` | Firmware-Update | Untere 16 Bit des 32-Bit-Offsets, an den der nächste Chunk geschrieben werden soll. |
|
| **0x0101** | `FWU_CHUNK_OFFSET_LOW` | Firmware-Update | Untere 16 Bit des 32-Bit-Offsets, an den der nächste Chunk geschrieben werden soll. |
|
||||||
| **0x0102** | `FWU_CHUNK_OFFSET_HIGH` | Firmware-Update | Obere 16 Bit des 32-Bit-Offsets. |
|
| **0x0102** | `FWU_CHUNK_OFFSET_HIGH` | Firmware-Update | Obere 16 Bit des 32-Bit-Offsets. |
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
[🇩🇪 Deutsch](modbus-registers.de.md) | [🇬🇧 English](modbus-registers.en.md)
|
<img src="./img/logo.png" alt="Logo" width="100"/>
|
||||||
|
|
||||||
|
[🇩🇪 Deutsch](modbus-registers.de.md) | 🇬🇧 English | [🇫🇷 Français](modbus-registers.fr.md) | [🇪🇸 Español](modbus-registers.es.md)
|
||||||
|
|
||||||
# MODBUS Register Map Definition v1.0
|
# MODBUS Register Map Definition v1.0
|
||||||
|
|
||||||
|
|||||||
87
docs/modbus-registers.es.md
Normal file
87
docs/modbus-registers.es.md
Normal file
@@ -0,0 +1,87 @@
|
|||||||
|
<img src="./img/logo.png" alt="Logo" width="100"/>
|
||||||
|
|
||||||
|
[🇩🇪 Deutsch](modbus-registers.de.md) | [🇬🇧 English](modbus-registers.en.md) | [🇫🇷 Français](modbus-registers.fr.md) | 🇪🇸 Español
|
||||||
|
|
||||||
|
# Definición del mapa de registros MODBUS v1.0
|
||||||
|
|
||||||
|
## 1. Introducción
|
||||||
|
|
||||||
|
Este documento define los registros MODBUS para los nodos esclavos universales del sistema de riego.
|
||||||
|
|
||||||
|
### 1.1. Filosofía de direccionamiento
|
||||||
|
|
||||||
|
Todos los registros se definen en una única lista continua por tipo de registro (`Input` o `Holding`). Una columna "Categoría" asigna lógicamente la función. Las direcciones se agrupan en bloques para dejar espacio para futuras ampliaciones y para aumentar la legibilidad.
|
||||||
|
|
||||||
|
* **`0x0000 - 0x000F`**: Control y estado de la válvula
|
||||||
|
* **`0x0010 - 0x001F`**: Salidas digitales (LEDs / relés)
|
||||||
|
* **`0x0020 - 0x002F`**: Entradas digitales (botones / sensores)
|
||||||
|
* **`0x00F0 - 0x00FF`**: Configuración y estado general del dispositivo
|
||||||
|
* **`0x0100 - 0x01FF`**: Mecanismo de actualización de firmware
|
||||||
|
|
||||||
|
### 1.2. Códigos de función utilizados
|
||||||
|
|
||||||
|
* **`0x03` (Read Holding Registers):** Para leer registros `4xxxx`.
|
||||||
|
* **`0x04` (Read Input Registers):** Para leer registros `3xxxx`.
|
||||||
|
* **`0x06` (Write Single Register):** Para escribir un único registro `4xxxx`.
|
||||||
|
* **`0x10` (Write Multiple Registers):** Para escribir varios registros `4xxxx` a la vez.
|
||||||
|
|
||||||
|
## 2. Registros de entrada (3xxxx, solo lectura)
|
||||||
|
|
||||||
|
| Dirección (hex) | Nombre | Categoría | Descripción |
|
||||||
|
| :--- | :--- | :--- | :--- |
|
||||||
|
| **0x0000** | `VALVE_STATE_MOVEMENT` | Válvula | Registro de estado combinado. **Byte alto**: Movimiento (`0`=Inactivo, `1`=Abriendo, `2`=Cerrando, `3`=Error). **Byte bajo**: Estado (`0`=Cerrado, `1`=Abierto). |
|
||||||
|
| **0x0001** | `MOTOR_CURRENT_MA` | Válvula | Corriente actual del motor en miliamperios (mA). |
|
||||||
|
| **0x0020** | `DIGITAL_INPUTS_STATE` | Entradas | Máscara de bits de las entradas digitales. Bit 0: Entrada 1, Bit 1: Entrada 2. `1`=Activo. |
|
||||||
|
| **0x0021** | `BUTTON_EVENTS` | Entradas | Banderas de eventos para botones (Borrar al leer). Bit 0: Botón 1 presionado. Bit 1: Botón 2 presionado. |
|
||||||
|
| **0x00F0** | `FIRMWARE_VERSION_MAJOR_MINOR` | Sistema | p. ej. `0x0102` para v1.2. |
|
||||||
|
| **0x00F1** | `FIRMWARE_VERSION_PATCH` | Sistema | p. ej. `3` para v1.2.3. |
|
||||||
|
| **0x00F2** | `DEVICE_STATUS` | Sistema | `0`=OK, `1`=Error general. |
|
||||||
|
| **0x00F3** | `UPTIME_SECONDS_LOW` | Sistema | 16 bits inferiores del tiempo de actividad en segundos. |
|
||||||
|
| **0x00F4** | `UPTIME_SECONDS_HIGH` | Sistema | 16 bits superiores del tiempo de actividad. |
|
||||||
|
| **0x0100** | `FWU_LAST_CHUNK_CRC` | Actualización FW | Contiene el CRC16 del último trozo de datos recibido en el búfer. |
|
||||||
|
|
||||||
|
## 3. Registros de retención (4xxxx, lectura/escritura)
|
||||||
|
|
||||||
|
| Dirección (hex) | Nombre | Categoría | Descripción |
|
||||||
|
| :--- | :--- | :--- | :--- |
|
||||||
|
| **0x0000** | `VALVE_COMMAND` | Válvula | `1`=Abrir, `2`=Cerrar, `0`=Detener movimiento. |
|
||||||
|
| **0x0001** | `MAX_OPENING_TIME_S` | Válvula | Tiempo de espera de seguridad en segundos para el proceso de apertura. |
|
||||||
|
| **0x0002** | `MAX_CLOSING_TIME_S` | Válvula | Tiempo de espera de seguridad en segundos para el proceso de cierre. |
|
||||||
|
| **0x0010** | `DIGITAL_OUTPUTS_STATE` | Salidas | Máscara de bits para leer y escribir las salidas. Bit 0: Salida 1, Bit 1: Salida 2. `1`=ON, `0`=OFF. |
|
||||||
|
| **0x00F0** | `WATCHDOG_TIMEOUT_S` | Sistema | Tiempo de espera del watchdog de seguridad en segundos. `0`=Desactivado. |
|
||||||
|
| **0x0100** | `FWU_COMMAND` | Actualización FW | `1`: **Verificar trozo**: El último trozo transmitido fue considerado válido por el cliente. El esclavo ahora debe escribirlo en la flash. `2`: **Finalizar actualización**: Todos los trozos han sido transmitidos. Finalizar la instalación y reiniciar. |
|
||||||
|
| **0x0101** | `FWU_CHUNK_OFFSET_LOW` | Actualización FW | 16 bits inferiores del desplazamiento de 32 bits en el que se escribirá el siguiente trozo. |
|
||||||
|
| **0x0102** | `FWU_CHUNK_OFFSET_HIGH` | Actualización FW | 16 bits superiores del desplazamiento de 32 bits. |
|
||||||
|
| **0x0103** | `FWU_CHUNK_SIZE` | Actualización FW | Tamaño del siguiente trozo en bytes (máx. 256). |
|
||||||
|
| **0x0180** | `FWU_DATA_BUFFER` | Actualización FW | **Dirección de inicio** de un búfer de 128x16 bits (256 bytes). Corresponde a los registros `40384` a `40511`. |
|
||||||
|
|
||||||
|
## 4. Proceso detallado de actualización de firmware
|
||||||
|
|
||||||
|
Este proceso no tiene estado y es robusto frente a errores de transmisión.
|
||||||
|
|
||||||
|
1. **Cliente:** Selecciona un trozo (máx. 256 bytes) del archivo de firmware y calcula su CRC16.
|
||||||
|
2. **Cliente:** Escribe el desplazamiento de destino (p. ej. `0`) en `FWU_CHUNK_OFFSET_...` y el tamaño en `FWU_CHUNK_SIZE`.
|
||||||
|
3. **Cliente:** Escribe los datos del trozo en el `FWU_DATA_BUFFER` (desde la dirección `0x0180`).
|
||||||
|
4. **Esclavo:** Recibe los datos, los coloca en el búfer de RAM y calcula el CRC. El resultado se proporciona en `FWU_LAST_CHUNK_CRC` (`30256`).
|
||||||
|
5. **Cliente:** Lee `FWU_LAST_CHUNK_CRC` y compara el valor con el CRC autocalculado.
|
||||||
|
* **Error:** Volver al paso 3 para enviar el mismo trozo de nuevo.
|
||||||
|
* **Éxito:** Continúa con el siguiente paso.
|
||||||
|
6. **Cliente:** Escribe el comando `1` ("Verificar trozo") en `FWU_COMMAND` (`40256`).
|
||||||
|
7. **Esclavo:** Recibe el comando, toma el trozo verificado del búfer de RAM y lo escribe en la ubicación correcta en la memoria flash.
|
||||||
|
8. **Cliente:** Continúa con el siguiente trozo (vuelta al paso 1 con nuevo desplazamiento y datos).
|
||||||
|
9. **Último trozo:** Después de que el último trozo ha sido transferido y escrito en la flash con el comando `1`, el cliente escribe el comando `2` ("Finalizar actualización") en `FWU_COMMAND`.
|
||||||
|
10. **Esclavo:** Realiza las comprobaciones finales y se reinicia para que MCUBoot pueda realizar la instalación.
|
||||||
|
|
||||||
|
## Apéndice: Registros del sensor de nivel QDY30A
|
||||||
|
|
||||||
|
Estos registros pertenecen al sensor de nivel externo y también se pueden direccionar en el bus. Según el fabricante, se trata de registros de retención (`4xxxx`) que se leen con el código de función `0x03`.
|
||||||
|
|
||||||
|
| Dirección (hex) | Nombre | L/E | Descripción |
|
||||||
|
| :--- | :--- | :-- | :--- |
|
||||||
|
| **0x0000** | `NODE_ADDRESS` | L/E | Dirección del dispositivo del sensor (1-255). |
|
||||||
|
| **0x0001** | `BAUDRATE` | L/E | `0`=1200, `1`=2400, `2`=4800, `3`=9600, `4`=19200, `5`=38400, `6`=57600, `7`=115200. |
|
||||||
|
| **0x0002** | `UNIT` | L/E | `0`=Ninguno, `1`=cm, `2`=mm, `3`=MPa, `4`=Pa, `5`=kPa. |
|
||||||
|
| **0x0003** | `DECIMAL_PLACES` | L/E | Número de decimales para el valor medido (0-3). |
|
||||||
|
| **0x0004** | `CURRENT_MEASUREMENT` | L | El valor medido escalado como un entero de 16 bits con signo. |
|
||||||
|
| **0x0005** | `MEASURING_RANGE_ZERO_POINT` | L/E | Valor bruto para el punto cero de la escala. |
|
||||||
|
| **0x0006** | `MEASURING_RANGE_END_POINT` | L/E | Valor bruto para el punto final de la escala. |
|
||||||
87
docs/modbus-registers.fr.md
Normal file
87
docs/modbus-registers.fr.md
Normal file
@@ -0,0 +1,87 @@
|
|||||||
|
<img src="./img/logo.png" alt="Logo" width="100"/>
|
||||||
|
|
||||||
|
[🇩🇪 Deutsch](modbus-registers.de.md) | [🇬🇧 English](modbus-registers.en.md) | 🇫🇷 Français | [🇪🇸 Español](modbus-registers.es.md)
|
||||||
|
|
||||||
|
# Définition de la carte des registres MODBUS v1.0
|
||||||
|
|
||||||
|
## 1. Introduction
|
||||||
|
|
||||||
|
Ce document définit les registres MODBUS pour les nœuds esclaves universels du système d'irrigation.
|
||||||
|
|
||||||
|
### 1.1. Philosophie d'adressage
|
||||||
|
|
||||||
|
Tous les registres sont définis dans une seule liste continue par type de registre (`Input` ou `Holding`). Une colonne "Catégorie" attribue logiquement la fonction. Les adresses sont regroupées en blocs pour laisser de la place à de futures extensions et pour améliorer la lisibilité.
|
||||||
|
|
||||||
|
* **`0x0000 - 0x000F`** : Commande et état de la vanne
|
||||||
|
* **`0x0010 - 0x001F`** : Sorties numériques (LEDs / relais)
|
||||||
|
* **`0x0020 - 0x002F`** : Entrées numériques (boutons / capteurs)
|
||||||
|
* **`0x00F0 - 0x00FF`** : Configuration et état général de l'appareil
|
||||||
|
* **`0x0100 - 0x01FF`** : Mécanisme de mise à jour du firmware
|
||||||
|
|
||||||
|
### 1.2. Codes de fonction utilisés
|
||||||
|
|
||||||
|
* **`0x03` (Read Holding Registers) :** Pour lire les registres `4xxxx`.
|
||||||
|
* **`0x04` (Read Input Registers) :** Pour lire les registres `3xxxx`.
|
||||||
|
* **`0x06` (Write Single Register) :** Pour écrire un seul registre `4xxxx`.
|
||||||
|
* **`0x10` (Write Multiple Registers) :** Pour écrire plusieurs registres `4xxxx` à la fois.
|
||||||
|
|
||||||
|
## 2. Registres d'entrée (3xxxx, Lecture seule)
|
||||||
|
|
||||||
|
| Adresse (hex) | Nom | Catégorie | Description |
|
||||||
|
| :--- | :--- | :--- | :--- |
|
||||||
|
| **0x0000** | `VALVE_STATE_MOVEMENT` | Vanne | Registre d'état combiné. **Octet haut** : Mouvement (`0`=Inactif, `1`=Ouverture, `2`=Fermeture, `3`=Erreur). **Octet bas** : État (`0`=Fermé, `1`=Ouvert). |
|
||||||
|
| **0x0001** | `MOTOR_CURRENT_MA` | Vanne | Courant moteur actuel en milliampères (mA). |
|
||||||
|
| **0x0020** | `DIGITAL_INPUTS_STATE` | Entrées | Masque de bits des entrées numériques. Bit 0 : Entrée 1, Bit 1 : Entrée 2. `1`=Actif. |
|
||||||
|
| **0x0021** | `BUTTON_EVENTS` | Entrées | Indicateurs d'événements pour les boutons (Effacement à la lecture). Bit 0 : Bouton 1 pressé. Bit 1 : Bouton 2 pressé. |
|
||||||
|
| **0x00F0** | `FIRMWARE_VERSION_MAJOR_MINOR` | Système | ex. `0x0102` pour v1.2. |
|
||||||
|
| **0x00F1** | `FIRMWARE_VERSION_PATCH` | Système | ex. `3` pour v1.2.3. |
|
||||||
|
| **0x00F2** | `DEVICE_STATUS` | Système | `0`=OK, `1`=Erreur générale. |
|
||||||
|
| **0x00F3** | `UPTIME_SECONDS_LOW` | Système | 16 bits inférieurs du temps de fonctionnement en secondes. |
|
||||||
|
| **0x00F4** | `UPTIME_SECONDS_HIGH` | Système | 16 bits supérieurs du temps de fonctionnement. |
|
||||||
|
| **0x0100** | `FWU_LAST_CHUNK_CRC` | Mise à jour FW | Contient le CRC16 du dernier bloc de données reçu dans le tampon. |
|
||||||
|
|
||||||
|
## 3. Registres de maintien (4xxxx, Lecture/Écriture)
|
||||||
|
|
||||||
|
| Adresse (hex) | Nom | Catégorie | Description |
|
||||||
|
| :--- | :--- | :--- | :--- |
|
||||||
|
| **0x0000** | `VALVE_COMMAND` | Vanne | `1`=Ouvrir, `2`=Fermer, `0`=Arrêter le mouvement. |
|
||||||
|
| **0x0001** | `MAX_OPENING_TIME_S` | Vanne | Temporisation de sécurité en secondes pour le processus d'ouverture. |
|
||||||
|
| **0x0002** | `MAX_CLOSING_TIME_S` | Vanne | Temporisation de sécurité en secondes pour le processus de fermeture. |
|
||||||
|
| **0x0010** | `DIGITAL_OUTPUTS_STATE` | Sorties | Masque de bits pour lire et écrire les sorties. Bit 0 : Sortie 1, Bit 1 : Sortie 2. `1`=ON, `0`=OFF. |
|
||||||
|
| **0x00F0** | `WATCHDOG_TIMEOUT_S` | Système | Temporisation du watchdog de sécurité en secondes. `0`=Désactivé. |
|
||||||
|
| **0x0100** | `FWU_COMMAND` | Mise à jour FW | `1` : **Vérifier le bloc** : Le dernier bloc transmis a été jugé valide par le client. L'esclave doit maintenant l'écrire en flash. `2` : **Finaliser la mise à jour** : Tous les blocs ont été transmis. Finaliser l'installation et redémarrer. |
|
||||||
|
| **0x0101** | `FWU_CHUNK_OFFSET_LOW` | Mise à jour FW | 16 bits inférieurs de l'offset 32 bits où le prochain bloc doit être écrit. |
|
||||||
|
| **0x0102** | `FWU_CHUNK_OFFSET_HIGH` | Mise à jour FW | 16 bits supérieurs de l'offset 32 bits. |
|
||||||
|
| **0x0103** | `FWU_CHUNK_SIZE` | Mise à jour FW | Taille du prochain bloc en octets (max. 256). |
|
||||||
|
| **0x0180** | `FWU_DATA_BUFFER` | Mise à jour FW | **Adresse de début** d'un tampon de 128x16 bits (256 octets). Correspond aux registres `40384` à `40511`. |
|
||||||
|
|
||||||
|
## 4. Processus détaillé de mise à jour du firmware
|
||||||
|
|
||||||
|
Ce processus est sans état et robuste aux erreurs de transmission.
|
||||||
|
|
||||||
|
1. **Client :** Sélectionne un bloc (max. 256 octets) dans le fichier de firmware et calcule son CRC16.
|
||||||
|
2. **Client :** Écrit l'offset cible (par ex. `0`) dans `FWU_CHUNK_OFFSET_...` et la taille dans `FWU_CHUNK_SIZE`.
|
||||||
|
3. **Client :** Écrit les données du bloc dans le `FWU_DATA_BUFFER` (à partir de l'adresse `0x0180`).
|
||||||
|
4. **Esclave :** Reçoit les données, les place dans le tampon RAM et calcule le CRC. Le résultat est fourni dans `FWU_LAST_CHUNK_CRC` (`30256`).
|
||||||
|
5. **Client :** Lit `FWU_LAST_CHUNK_CRC` et compare la valeur avec le CRC auto-calculé.
|
||||||
|
* **Erreur :** Retourner à l'étape 3 pour renvoyer le même bloc.
|
||||||
|
* **Succès :** Continue à l'étape suivante.
|
||||||
|
6. **Client :** Écrit la commande `1` ("Vérifier le bloc") dans `FWU_COMMAND` (`40256`).
|
||||||
|
7. **Esclave :** Reçoit la commande, prend le bloc vérifié du tampon RAM et l'écrit à l'emplacement correct dans la mémoire flash.
|
||||||
|
8. **Client :** Continue avec le bloc suivant (retour à l'étape 1 avec un nouvel offset et de nouvelles données).
|
||||||
|
9. **Dernier bloc :** Après que le dernier bloc a été transféré et écrit en flash avec la commande `1`, le client écrit la commande `2` ("Finaliser la mise à jour") dans `FWU_COMMAND`.
|
||||||
|
10. **Esclave :** Effectue les vérifications finales et redémarre pour que MCUBoot puisse effectuer l'installation.
|
||||||
|
|
||||||
|
## Annexe : Registres du capteur de niveau QDY30A
|
||||||
|
|
||||||
|
Ces registres appartiennent au capteur de niveau externe et peuvent également être adressés sur le bus. Selon le fabricant, il s'agit de registres de maintien (`4xxxx`) qui sont lus avec le code de fonction `0x03`.
|
||||||
|
|
||||||
|
| Adresse (hex) | Nom | L/E | Description |
|
||||||
|
| :--- | :--- | :-- | :--- |
|
||||||
|
| **0x0000** | `NODE_ADDRESS` | L/E | Adresse de l'appareil du capteur (1-255). |
|
||||||
|
| **0x0001** | `BAUDRATE` | L/E | `0`=1200, `1`=2400, `2`=4800, `3`=9600, `4`=19200, `5`=38400, `6`=57600, `7`=115200. |
|
||||||
|
| **0x0002** | `UNIT` | L/E | `0`=Aucun, `1`=cm, `2`=mm, `3`=MPa, `4`=Pa, `5`=kPa. |
|
||||||
|
| **0x0003** | `DECIMAL_PLACES` | L/E | Nombre de décimales pour la valeur mesurée (0-3). |
|
||||||
|
| **0x0004** | `CURRENT_MEASUREMENT` | L | La valeur mesurée mise à l'échelle sous forme d'entier signé de 16 bits. |
|
||||||
|
| **0x0005** | `MEASURING_RANGE_ZERO_POINT` | L/E | Valeur brute pour le point zéro de l'échelle. |
|
||||||
|
| **0x0006** | `MEASURING_RANGE_END_POINT` | L/E | Valeur brute pour le point final de l'échelle. |
|
||||||
@@ -1,4 +1,6 @@
|
|||||||
[🇩🇪 Deutsch](planning.de.md) | [🇬🇧 English](planning.en.md)
|
<img src="./img/logo.png" alt="Logo" width="100"/>
|
||||||
|
|
||||||
|
🇩🇪 Deutsch | [🇬🇧 English](planning.en.md) | [🇫🇷 Français](planning.fr.md) | [🇪🇸 Español](planning.es.md)
|
||||||
|
|
||||||
# Projektplan: Modulares Bewässerungssystem
|
# Projektplan: Modulares Bewässerungssystem
|
||||||
|
|
||||||
@@ -8,7 +10,7 @@
|
|||||||
| ✅ | 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. |
|
||||||
| ☐ | **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 | | 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 Basis-Firmware für Slave-Node erstellen | | Hardware-Abstraktion (GPIOs, ADC, UART für RS485) implementieren. |
|
||||||
| ☐ | 1.3 MODBUS-RTU Stack auf dem Slave implementieren | | Basierend auf der definierten Register-Map. Zuerst nur lesende Funktionen (Status, Version). |
|
| ☐ | 1.3 MODBUS-RTU Stack auf dem Slave implementieren | | Basierend auf der definierten Register-Map. Zuerst nur lesende Funktionen (Status, Version). |
|
||||||
| ☐ | 1.4 Kernlogik implementieren (z.B. Ventilsteuerung) | | Umsetzung der `VENTIL_ZUSTAND_BEWEGUNG` Logik, Strommessung für Endlagen etc. |
|
| ☐ | 1.4 Kernlogik implementieren (z.B. Ventilsteuerung) | | Umsetzung der `VENTIL_ZUSTAND_BEWEGUNG` Logik, Strommessung für Endlagen etc. |
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
[🇩🇪 Deutsch](planning.de.md) | [🇬🇧 English](planning.en.md)
|
<img src="./img/logo.png" alt="Logo" width="100"/>
|
||||||
|
|
||||||
|
[🇩🇪 Deutsch](planning.de.md) | 🇬🇧 English | [🇫🇷 Français](planning.fr.md) | [🇪🇸 Español](planning.es.md)
|
||||||
|
|
||||||
# Project Plan: Modular Irrigation System
|
# Project Plan: Modular Irrigation System
|
||||||
|
|
||||||
@@ -8,7 +10,7 @@
|
|||||||
| ✅ | Create and finalize concept | 2025-06-30 | Architecture, components and basic architecture are defined. |
|
| ✅ | Create and finalize concept | 2025-06-30 | Architecture, components and basic architecture are defined. |
|
||||||
| ✅ | Define MODBUS Register Map | 2025-06-30 | The "API" of the slaves is defined and forms the basis for software development. |
|
| ✅ | Define MODBUS Register Map | 2025-06-30 | The "API" of the slaves is defined and forms the basis for software development. |
|
||||||
| ☐ | **Phase 1: Slave Node Prototype (STM32 Eval-Board)** | | **Goal:** A single slave is brought to life on the eval board. |
|
| ☐ | **Phase 1: Slave Node Prototype (STM32 Eval-Board)** | | **Goal:** A single slave is brought to life on the eval board. |
|
||||||
| ☐ | 1.1 Set up development environment for STM32/Zephyr | | Install toolchain, VS Code, Zephyr-SDK, MCUBoot etc. and get a "Hello World" running. |
|
| ✅ | 1.1 Set up development environment for STM32/Zephyr | 2025-06-30 | Install toolchain, VS Code, Zephyr-SDK, MCUBoot etc. and get a "Hello World" running. |
|
||||||
| ☐ | 1.2 Create basic firmware for slave node | | Implement hardware abstraction (GPIOs, ADC, UART for RS485). |
|
| ☐ | 1.2 Create basic firmware for slave node | | Implement hardware abstraction (GPIOs, ADC, UART for RS485). |
|
||||||
| ☐ | 1.3 Implement MODBUS-RTU stack on the slave | | Based on the defined register map. Initially only read functions (status, version). |
|
| ☐ | 1.3 Implement MODBUS-RTU stack on the slave | | Based on the defined register map. Initially only read functions (status, version). |
|
||||||
| ☐ | 1.4 Implement core logic (e.g. valve control) | | Implementation of the `VALVE_STATE_MOVEMENT` logic, current measurement for end positions etc. |
|
| ☐ | 1.4 Implement core logic (e.g. valve control) | | Implementation of the `VALVE_STATE_MOVEMENT` logic, current measurement for end positions etc. |
|
||||||
|
|||||||
35
docs/planning.es.md
Normal file
35
docs/planning.es.md
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
<img src="./img/logo.png" alt="Logo" width="100"/>
|
||||||
|
|
||||||
|
[🇩🇪 Deutsch](planning.de.md) | [🇬🇧 English](planning.en.md) | [🇫🇷 Français](planning.fr.md) | 🇪🇸 Español
|
||||||
|
|
||||||
|
# Plan del proyecto: Sistema de riego modular
|
||||||
|
|
||||||
|
| Hecho | Tarea | Fecha | Observaciones |
|
||||||
|
| :---: | :--- | :--- | :--- |
|
||||||
|
| ✅ | **Fase 0: Planificación y definición** | | |
|
||||||
|
| ✅ | Crear y finalizar el concepto | 30.06.2025 | La arquitectura, los componentes y la arquitectura básica están definidos. |
|
||||||
|
| ✅ | Definir el mapa de registros MODBUS | 30.06.2025 | La "API" de los esclavos está definida y constituye la base para el desarrollo del software. |
|
||||||
|
| ☐ | **Fase 1: Prototipo de nodo esclavo (placa de evaluación STM32)** | | **Objetivo:** Un único esclavo cobra vida en la placa de evaluación. |
|
||||||
|
| ✅ | 1.1 Configurar el entorno de desarrollo para STM32/Zephyr | 30.06.2025 | Instalar la cadena de herramientas, VS Code, el SDK de Zephyr, MCUBoot, etc. y hacer que funcione un "Hola Mundo". |
|
||||||
|
| ☐ | 1.2 Crear el firmware básico para el nodo esclavo | | Implementar la abstracción de hardware (GPIO, ADC, UART para RS485). |
|
||||||
|
| ☐ | 1.3 Implementar la pila MODBUS-RTU en el esclavo | | Basado en el mapa de registros definido. Inicialmente solo funciones de lectura (estado, versión). |
|
||||||
|
| ☐ | 1.4 Implementar la lógica central (p. ej., control de válvulas) | | Implementación de la lógica `VALVE_STATE_MOVEMENT`, medición de corriente para posiciones finales, etc. |
|
||||||
|
| ☐ | **Fase 2: Verificación del firmware del esclavo** | | **Objetivo:** Demostrar que el esclavo se adhiere exactamente a la especificación MODBUS. |
|
||||||
|
| ☐ | 2.1 Probar el nodo esclavo con un PC a través de un adaptador USB-MODBUS | | **Hito crítico.** Leer y escribir los registros con herramientas como "QModMaster" o un script de Python. El firmware del esclavo se valida así independientemente de la puerta de enlace. |
|
||||||
|
| ☐ | 2.2 Probar el mecanismo de actualización de firmware | | Probar el proceso de actualización completo (fragmentación, comprobación de CRC) con un script desde el PC. El esclavo inicialmente solo escribe el firmware en un área de RAM no utilizada. |
|
||||||
|
| ☐ | **Fase 3: Diseño de hardware y construcción de prototipos** | | **Objetivo:** Pasar del desarrollo en la placa de evaluación a un PCB a medida. |
|
||||||
|
| ☐ | 3.1 Diseñar el esquema y el diseño del PCB para el nodo esclavo | | Basado en la experiencia con la placa de evaluación. |
|
||||||
|
| ☐ | 3.2 Pedir y ensamblar placas prototipo | | Por ejemplo, en JLCPCB. Soldar los componentes THT (conectores, etc.) uno mismo. |
|
||||||
|
| ☐ | 3.3 Puesta en marcha del hardware del primer prototipo | | Comprobar voltajes, cargar el firmware y repetir las pruebas de la fase 2 para validar el hardware. |
|
||||||
|
| ☐ | 3.4 Implementar la rutina de escritura en flash para la actualización del firmware | | Aplicar el proceso validado en la RAM en el paso 2.2 a la memoria flash real. |
|
||||||
|
| ☐ | **Fase 4: Desarrollo de la puerta de enlace (placa de evaluación ESP32)** | | **Objetivo:** Construir el puente desde el mundo RS485 a la red doméstica. |
|
||||||
|
| ☐ | 4.1 Crear el firmware de la puerta de enlace (ESPHome) | | Configurar una puerta de enlace simple de MODBUS TCP a RTU en la placa de evaluación ESP32C6. |
|
||||||
|
| ☐ | 4.2 Conectar y probar la puerta de enlace con el prototipo de nodo esclavo | | Probar la cadena: PC (como cliente MODBUS TCP) -> WLAN -> Puerta de enlace -> RS485 -> Esclavo. |
|
||||||
|
| ☐ | **Fase 5: Integración del sistema en Home Assistant** | | **Objetivo:** Hacer que el sistema sea "inteligente". |
|
||||||
|
| ☐ | 5.1 Configurar la integración de MODBUS en Home Assistant | | Crear los sensores y las entidades para el nodo esclavo en `configuration.yaml` o a través de la interfaz de usuario. |
|
||||||
|
| ☐ | 5.2 Crear paneles y automatizaciones en HA | | Visualización de los estados (válvula, bomba, etc.) y creación de la lógica de riego real. |
|
||||||
|
| ☐ | 5.3 Desarrollar un script de Python para la actualización de firmware en HA | | Implementación de la carga basada en fragmentos como un script que se puede llamar desde HA. |
|
||||||
|
| ☐ | **Fase 6: Montaje y puesta en marcha** | | **Objetivo:** Instalar el sistema terminado. |
|
||||||
|
| ☐ | 6.1 Construir y probar todos los nodos esclavos necesarios | | Probar cada esclavo individualmente con el PC a través de un adaptador USB y configurar la dirección MODBUS. |
|
||||||
|
| ☐ | 6.2 Instalación final y cableado del sistema | | Instalación de los componentes en el cobertizo, cableado del bus RS485. |
|
||||||
|
| ☐ | 6.3 Prueba y calibración general del sistema | | Calibrar el sensor de nivel, comprobar los tiempos de funcionamiento de las válvulas, probar el comportamiento a prueba de fallos. |
|
||||||
35
docs/planning.fr.md
Normal file
35
docs/planning.fr.md
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
<img src="./img/logo.png" alt="Logo" width="100"/>
|
||||||
|
|
||||||
|
[🇩🇪 Deutsch](planning.de.md) | [🇬🇧 English](planning.en.md) | 🇫🇷 Français | [🇪🇸 Español](planning.es.md)
|
||||||
|
|
||||||
|
# Plan de projet : Système d'irrigation modulaire
|
||||||
|
|
||||||
|
| Fait | Tâche | Date | Remarques |
|
||||||
|
| :---: | :--- | :--- | :--- |
|
||||||
|
| ✅ | **Phase 0 : Planification & Définition** | | |
|
||||||
|
| ✅ | Créer et finaliser le concept | 30.06.2025 | L'architecture, les composants et l'architecture de base sont définis. |
|
||||||
|
| ✅ | Définir la carte des registres MODBUS | 30.06.2025 | L'"API" des esclaves est définie et constitue la base du développement logiciel. |
|
||||||
|
| ☐ | **Phase 1 : Prototype de nœud esclave (carte d'évaluation STM32)** | | **Objectif :** Un seul esclave est mis en service sur la carte d'évaluation. |
|
||||||
|
| ✅ | 1.1 Mettre en place l'environnement de développement pour STM32/Zephyr | 30.06.2025 | Installer la chaîne d'outils, VS Code, le SDK Zephyr, MCUBoot, etc. et faire fonctionner un "Hello World". |
|
||||||
|
| ☐ | 1.2 Créer le firmware de base pour le nœud esclave | | Implémenter l'abstraction matérielle (GPIO, ADC, UART pour RS485). |
|
||||||
|
| ☐ | 1.3 Implémenter la pile MODBUS-RTU sur l'esclave | | Basé sur la carte des registres définie. D'abord uniquement les fonctions de lecture (état, version). |
|
||||||
|
| ☐ | 1.4 Implémenter la logique de base (par ex. commande de vanne) | | Implémentation de la logique `VALVE_STATE_MOVEMENT`, mesure du courant pour les positions finales, etc. |
|
||||||
|
| ☐ | **Phase 2 : Vérification du firmware de l'esclave** | | **Objectif :** Prouver que l'esclave respecte exactement la spécification MODBUS. |
|
||||||
|
| ☐ | 2.1 Tester le nœud esclave avec un PC via un adaptateur USB-MODBUS | | **Jalon critique.** Lire et écrire les registres avec des outils comme "QModMaster" ou un script Python. Le firmware de l'esclave est ainsi validé indépendamment de la passerelle. |
|
||||||
|
| ☐ | 2.2 Tester le mécanisme de mise à jour du firmware | | Tester le processus de mise à jour complet (fragmentation, vérification CRC) avec un script depuis le PC. L'esclave n'écrit d'abord le firmware que dans une zone RAM inutilisée. |
|
||||||
|
| ☐ | **Phase 3 : Conception matérielle et construction de prototypes** | | **Objectif :** Passer du développement sur la carte d'évaluation à un PCB sur mesure. |
|
||||||
|
| ☐ | 3.1 Concevoir le schéma et le layout du PCB pour le nœud esclave | | Basé sur l'expérience avec la carte d'évaluation. |
|
||||||
|
| ☐ | 3.2 Commander et assembler les cartes prototypes | | Par ex. chez JLCPCB. Souder soi-même les composants THT (connecteurs, etc.). |
|
||||||
|
| ☐ | 3.3 Mise en service matérielle du premier prototype | | Vérifier les tensions, charger le firmware et répéter les tests de la phase 2 pour valider le matériel. |
|
||||||
|
| ☐ | 3.4 Implémenter la routine d'écriture flash pour la mise à jour du firmware | | Appliquer le processus validé en RAM à l'étape 2.2 à la mémoire flash réelle. |
|
||||||
|
| ☐ | **Phase 4 : Développement de la passerelle (carte d'évaluation ESP32)** | | **Objectif :** Construire le pont entre le monde RS485 et le réseau domestique. |
|
||||||
|
| ☐ | 4.1 Créer le firmware de la passerelle (ESPHome) | | Mettre en place une simple passerelle MODBUS TCP vers RTU sur la carte d'évaluation ESP32C6. |
|
||||||
|
| ☐ | 4.2 Connecter et tester la passerelle avec le prototype de nœud esclave | | Tester la chaîne : PC (en tant que client MODBUS TCP) -> WLAN -> Passerelle -> RS485 -> Esclave. |
|
||||||
|
| ☐ | **Phase 5 : Intégration du système dans Home Assistant** | | **Objectif :** Rendre le système "intelligent". |
|
||||||
|
| ☐ | 5.1 Configurer l'intégration MODBUS dans Home Assistant | | Créer les capteurs et les entités pour le nœud esclave dans `configuration.yaml` ou via l'interface utilisateur. |
|
||||||
|
| ☐ | 5.2 Créer des tableaux de bord et des automatisations dans HA | | Visualisation des états (vanne, pompe, etc.) et création de la logique d'irrigation réelle. |
|
||||||
|
| ☐ | 5.3 Développer un script Python pour la mise à jour du firmware dans HA | | Implémentation du téléchargement basé sur des fragments sous forme de script pouvant être appelé depuis HA. |
|
||||||
|
| ☐ | **Phase 6 : Montage et mise en service** | | **Objectif :** Installer le système final. |
|
||||||
|
| ☐ | 6.1 Construire et tester tous les nœuds esclaves nécessaires | | Tester chaque esclave individuellement avec le PC via un adaptateur USB et configurer l'adresse MODBUS. |
|
||||||
|
| ☐ | 6.2 Installation finale et câblage du système | | Installation des composants dans l'abri, câblage du bus RS485. |
|
||||||
|
| ☐ | 6.3 Test et calibrage de l'ensemble du système | | Calibrer le capteur de niveau, vérifier les temps de fonctionnement des vannes, tester le comportement de sécurité. |
|
||||||
6
lib/fwu/CMakeLists.txt
Normal file
6
lib/fwu/CMakeLists.txt
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
cmake_minimum_required(VERSION 3.20)
|
||||||
|
|
||||||
|
project(fwu)
|
||||||
|
|
||||||
|
target_sources(fwu PRIVATE src/fwu.c)
|
||||||
|
target_include_directories(fwu PUBLIC include)
|
||||||
6
lib/modbus_server/CMakeLists.txt
Normal file
6
lib/modbus_server/CMakeLists.txt
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
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)
|
||||||
6
lib/valve/CMakeLists.txt
Normal file
6
lib/valve/CMakeLists.txt
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
cmake_minimum_required(VERSION 3.20)
|
||||||
|
|
||||||
|
project(valve)
|
||||||
|
|
||||||
|
target_sources(valve PRIVATE src/valve.c)
|
||||||
|
target_include_directories(valve PUBLIC include)
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
# SPDX-License-Identifier: Apache-2.0
|
|
||||||
|
|
||||||
cmake_minimum_required(VERSION 3.20.0)
|
|
||||||
|
|
||||||
# This line should ideally be after project() and find_package(Zephyr)
|
|
||||||
# target_include_directories(app PRIVATE ${ZEPHYR_BASE}/include/zephyr/drivers) # <-- WRONG POSITION
|
|
||||||
|
|
||||||
list(APPEND BOARD_ROOT ${CMAKE_CURRENT_SOURCE_DIR})
|
|
||||||
|
|
||||||
find_package(Zephyr REQUIRED HINTS $ENV{ZEPHYR_BASE})
|
|
||||||
project(valve_node)
|
|
||||||
|
|
||||||
target_include_directories(app PRIVATE ${ZEPHYR_BASE}/include/zephyr/drivers)
|
|
||||||
target_sources(app PRIVATE src/main2.c)
|
|
||||||
@@ -1,25 +0,0 @@
|
|||||||
# SPDX-License-Identifier: Apache-2.0
|
|
||||||
|
|
||||||
cmake_minimum_required(VERSION 3.20.0)
|
|
||||||
|
|
||||||
list(APPEND BOARD_ROOT ${CMAKE_CURRENT_SOURCE_DIR})
|
|
||||||
|
|
||||||
find_package(Zephyr REQUIRED HINTS $ENV{ZEPHYR_BASE})
|
|
||||||
project(valve_node)
|
|
||||||
|
|
||||||
target_sources(app PRIVATE src/main.c)
|
|
||||||
target_sources(app PRIVATE lib/canbus.c)
|
|
||||||
|
|
||||||
# source files for modbus waterlevel sensor
|
|
||||||
zephyr_library_sources_ifdef(CONFIG_HAS_MODBUS_WATERLEVEL_SENSOR
|
|
||||||
lib/waterlevel_sensor.c
|
|
||||||
)
|
|
||||||
|
|
||||||
#source files for valve
|
|
||||||
zephyr_library_sources_ifdef(CONFIG_HAS_VALVE
|
|
||||||
lib/valve.c
|
|
||||||
)
|
|
||||||
|
|
||||||
zephyr_include_directories(
|
|
||||||
lib
|
|
||||||
)
|
|
||||||
1
software/Kconfig
Normal file
1
software/Kconfig
Normal file
@@ -0,0 +1 @@
|
|||||||
|
rsource "lib/Kconfig"
|
||||||
9
software/apps/gateway/CMakeLists.txt
Normal file
9
software/apps/gateway/CMakeLists.txt
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
cmake_minimum_required(VERSION 3.20)
|
||||||
|
|
||||||
|
# Include the main 'software' directory as a module to find boards, libs, etc.
|
||||||
|
list(APPEND ZEPHYR_EXTRA_MODULES ${CMAKE_CURRENT_SOURCE_DIR}/../..)
|
||||||
|
|
||||||
|
find_package(Zephyr REQUIRED HINTS $ENV{ZEPHYR_BASE})
|
||||||
|
project(gateway)
|
||||||
|
|
||||||
|
target_sources(app PRIVATE src/main.c)
|
||||||
2
software/apps/gateway/prj.conf
Normal file
2
software/apps/gateway/prj.conf
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
# Gateway Configuration
|
||||||
|
CONFIG_NETWORKING=y
|
||||||
13
software/apps/gateway/src/main.c
Normal file
13
software/apps/gateway/src/main.c
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2025 Eduard Iten
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: Apache-2.0
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include <zephyr/kernel.h>
|
||||||
|
|
||||||
|
int main(void)
|
||||||
|
{
|
||||||
|
printk("Hello from Gateway!\n");
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
8
software/apps/slave_node/CMakeLists.txt
Normal file
8
software/apps/slave_node/CMakeLists.txt
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
cmake_minimum_required(VERSION 3.20)
|
||||||
|
|
||||||
|
find_package(Zephyr REQUIRED HINTS $ENV{ZEPHYR_BASE})
|
||||||
|
|
||||||
|
project(slave_node LANGUAGES C)
|
||||||
|
zephyr_include_directories(../../include)
|
||||||
|
add_subdirectory(../../lib lib)
|
||||||
|
target_sources(app PRIVATE src/main.c)
|
||||||
2
software/apps/slave_node/Kconfig
Normal file
2
software/apps/slave_node/Kconfig
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
rsource "../../lib/Kconfig"
|
||||||
|
source "Kconfig.zephyr"
|
||||||
7
software/apps/slave_node/boards/bluepill_f103rb.conf
Normal file
7
software/apps/slave_node/boards/bluepill_f103rb.conf
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
# Disable UART console
|
||||||
|
CONFIG_UART_CONSOLE=n
|
||||||
|
|
||||||
|
# Enable RTT console
|
||||||
|
CONFIG_RTT_CONSOLE=y
|
||||||
|
CONFIG_USE_SEGGER_RTT=y
|
||||||
|
CONFIG_SHELL_BACKEND_RTT=y
|
||||||
43
software/apps/slave_node/boards/bluepill_f103rb.overlay
Normal file
43
software/apps/slave_node/boards/bluepill_f103rb.overlay
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
/ {
|
||||||
|
chosen {
|
||||||
|
zephyr,console = &rtt;
|
||||||
|
zephyr,shell = &rtt;
|
||||||
|
zephyr,settings-partition = &storage_partition;
|
||||||
|
};
|
||||||
|
|
||||||
|
rtt: rtt {
|
||||||
|
compatible = "segger,rtt-uart";
|
||||||
|
#address-cells = <1>;
|
||||||
|
#size-cells = <0>;
|
||||||
|
label = "RTT";
|
||||||
|
status = "okay";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
&flash0 {
|
||||||
|
partitions {
|
||||||
|
compatible = "fixed-partitions";
|
||||||
|
#address-cells = <1>;
|
||||||
|
#size-cells = <1>;
|
||||||
|
|
||||||
|
/* Application partition starts at the beginning of flash */
|
||||||
|
slot0_partition: partition@0 {
|
||||||
|
label = "image-0";
|
||||||
|
reg = <0x00000000 DT_SIZE_K(120)>;
|
||||||
|
};
|
||||||
|
|
||||||
|
/* Use the last 8K for settings */
|
||||||
|
storage_partition: partition@1E000 {
|
||||||
|
label = "storage";
|
||||||
|
reg = <0x0001E000 DT_SIZE_K(8)>;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
&usart1 {
|
||||||
|
modbus0 {
|
||||||
|
compatible = "zephyr,modbus-serial";
|
||||||
|
status = "okay";
|
||||||
|
};
|
||||||
|
status = "okay";
|
||||||
|
};
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
&usart1 {
|
||||||
|
modbus0 {
|
||||||
|
compatible = "zephyr,modbus-serial";
|
||||||
|
status = "okay";
|
||||||
|
};
|
||||||
|
status = "okay";
|
||||||
|
pinctrl-0 = <&usart1_tx_pa9 &usart1_rx_pa10>;
|
||||||
|
pinctrl-names = "default";
|
||||||
|
};
|
||||||
10
software/apps/slave_node/cdc-acm.overlay
Normal file
10
software/apps/slave_node/cdc-acm.overlay
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
&zephyr_udc0 {
|
||||||
|
cdc_acm_uart0: cdc_acm_uart0 {
|
||||||
|
compatible = "zephyr,cdc-acm-uart";
|
||||||
|
|
||||||
|
modbus0 {
|
||||||
|
compatible = "zephyr,modbus-serial";
|
||||||
|
status = "okay";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
4
software/apps/slave_node/overlay-cdc-acm.conf
Normal file
4
software/apps/slave_node/overlay-cdc-acm.conf
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
CONFIG_USB_DEVICE_STACK=y
|
||||||
|
CONFIG_USB_DEVICE_PRODUCT="Modbus slave node"
|
||||||
|
CONFIG_UART_LINE_CTRL=y
|
||||||
|
CONFIG_USB_DEVICE_INITIALIZE_AT_BOOT=n
|
||||||
23
software/apps/slave_node/prj.conf
Normal file
23
software/apps/slave_node/prj.conf
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
# Enable Console and printk for logging
|
||||||
|
CONFIG_CONSOLE=y
|
||||||
|
CONFIG_LOG=y
|
||||||
|
|
||||||
|
# Enable Shell
|
||||||
|
CONFIG_SHELL=y
|
||||||
|
CONFIG_REBOOT=y
|
||||||
|
|
||||||
|
# Enable Settings Subsystem
|
||||||
|
CONFIG_SETTINGS=y
|
||||||
|
CONFIG_SETTINGS_NVS=y
|
||||||
|
CONFIG_NVS=y
|
||||||
|
CONFIG_FLASH=y
|
||||||
|
CONFIG_FLASH_MAP=y
|
||||||
|
CONFIG_FLASH_PAGE_LAYOUT=y
|
||||||
|
CONFIG_SETTINGS_LOG_LEVEL_DBG=y
|
||||||
|
|
||||||
|
# Config modbus
|
||||||
|
CONFIG_UART_INTERRUPT_DRIVEN=y
|
||||||
|
CONFIG_MODBUS=y
|
||||||
|
CONFIG_MODBUS_ROLE_SERVER=y
|
||||||
|
CONFIG_MODBUS_BUFFER_SIZE=256
|
||||||
|
|
||||||
28
software/apps/slave_node/src/main.c
Normal file
28
software/apps/slave_node/src/main.c
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
#include <zephyr/kernel.h>
|
||||||
|
#include <zephyr/settings/settings.h>
|
||||||
|
#include <zephyr/logging/log.h>
|
||||||
|
#include <lib/modbus_server.h>
|
||||||
|
#include <lib/valve.h>
|
||||||
|
#include <lib/fwu.h>
|
||||||
|
|
||||||
|
LOG_MODULE_REGISTER(main, LOG_LEVEL_INF);
|
||||||
|
|
||||||
|
int main(void)
|
||||||
|
{
|
||||||
|
LOG_INF("Starting Irrigation System Slave Node");
|
||||||
|
|
||||||
|
if (settings_subsys_init() || settings_load()) {
|
||||||
|
LOG_ERR("Settings initialization or loading failed");
|
||||||
|
}
|
||||||
|
|
||||||
|
valve_init();
|
||||||
|
fwu_init();
|
||||||
|
|
||||||
|
if (modbus_server_init()) {
|
||||||
|
LOG_ERR("Modbus RTU server initialization failed");
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
LOG_INF("Irrigation System Slave Node started successfully");
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
# Copyright (c) 2025 Eduard Iten
|
||||||
|
# SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
config BOARD_BLUEPILL_F103RB
|
||||||
|
select SOC_STM32F103XB
|
||||||
106
software/boards/iten/bluepill_f103rb/bluepill_f103rb.dts
Normal file
106
software/boards/iten/bluepill_f103rb/bluepill_f103rb.dts
Normal file
@@ -0,0 +1,106 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2025 Eduard Iten
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: Apache-2.0
|
||||||
|
*/
|
||||||
|
|
||||||
|
/dts-v1/;
|
||||||
|
#include <st/f1/stm32f1.dtsi>
|
||||||
|
#include <st/f1/stm32f103Xb.dtsi>
|
||||||
|
#include <st/f1/stm32f103r(8-b)tx-pinctrl.dtsi>
|
||||||
|
#include <zephyr/dt-bindings/input/input-event-codes.h>
|
||||||
|
|
||||||
|
/ {
|
||||||
|
model = "Blue-Pill STM32F103RB";
|
||||||
|
compatible = "iten,bluepill-f103rb";
|
||||||
|
|
||||||
|
chosen {
|
||||||
|
zephyr,console = &usart1;
|
||||||
|
zephyr,shell-uart = &usart1;
|
||||||
|
zephyr,sram = &sram0;
|
||||||
|
zephyr,flash = &flash0;
|
||||||
|
};
|
||||||
|
|
||||||
|
leds {
|
||||||
|
compatible = "gpio-leds";
|
||||||
|
led_status: led_status {
|
||||||
|
gpios = <&gpioc 13 GPIO_ACTIVE_LOW>;
|
||||||
|
label = "User LED";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
aliases {
|
||||||
|
led0 = &led_status;
|
||||||
|
watchdog0 = &iwdg;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
&clk_lsi {
|
||||||
|
status = "okay";
|
||||||
|
};
|
||||||
|
|
||||||
|
&clk_hse {
|
||||||
|
clock-frequency = <DT_FREQ_M(8)>;
|
||||||
|
status = "okay";
|
||||||
|
};
|
||||||
|
|
||||||
|
&pll {
|
||||||
|
mul = <9>;
|
||||||
|
clocks = <&clk_hse>;
|
||||||
|
status = "okay";
|
||||||
|
};
|
||||||
|
|
||||||
|
&rcc {
|
||||||
|
clocks = <&pll>;
|
||||||
|
clock-frequency = <DT_FREQ_M(72)>;
|
||||||
|
ahb-prescaler = <1>;
|
||||||
|
apb1-prescaler = <2>;
|
||||||
|
apb2-prescaler = <1>;
|
||||||
|
adc-prescaler = <6>;
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
&usart1 {
|
||||||
|
pinctrl-0 = <&usart1_tx_pa9 &usart1_rx_pa10>;
|
||||||
|
pinctrl-names = "default";
|
||||||
|
current-speed = <115200>;
|
||||||
|
status = "okay";
|
||||||
|
};
|
||||||
|
|
||||||
|
&iwdg {
|
||||||
|
status = "okay";
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
&clk_lsi {
|
||||||
|
status = "okay";
|
||||||
|
};
|
||||||
|
|
||||||
|
&clk_hse {
|
||||||
|
clock-frequency = <DT_FREQ_M(8)>;
|
||||||
|
status = "okay";
|
||||||
|
};
|
||||||
|
|
||||||
|
&pll {
|
||||||
|
mul = <9>;
|
||||||
|
clocks = <&clk_hse>;
|
||||||
|
status = "okay";
|
||||||
|
};
|
||||||
|
|
||||||
|
&rcc {
|
||||||
|
clocks = <&pll>;
|
||||||
|
clock-frequency = <DT_FREQ_M(72)>;
|
||||||
|
ahb-prescaler = <1>;
|
||||||
|
apb1-prescaler = <2>;
|
||||||
|
apb2-prescaler = <1>;
|
||||||
|
adc-prescaler = <6>;
|
||||||
|
};
|
||||||
|
|
||||||
|
&exti {
|
||||||
|
status = "okay";
|
||||||
|
};
|
||||||
|
|
||||||
|
&dma1 {
|
||||||
|
status = "okay";
|
||||||
|
};
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
# Copyright (c) 2025 Eduard Iten
|
||||||
|
# SPDX-License-Identifier: Apache-2.0
|
||||||
|
CONFIG_SERIAL=y
|
||||||
|
CONFIG_GPIO=y
|
||||||
@@ -1,11 +1,12 @@
|
|||||||
|
# Copyright (c) 2025 Eduard Iten
|
||||||
# SPDX-License-Identifier: Apache-2.0
|
# SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
# keep first
|
# keep first
|
||||||
board_runner_args(stm32cubeprogrammer "--port=swd" "--reset-mode=hw")
|
|
||||||
board_runner_args(jlink "--device=STM32F103RB" "--speed=4000")
|
board_runner_args(jlink "--device=STM32F103RB" "--speed=4000")
|
||||||
|
board_runner_args(stm32cubeprogrammer "--port=swd" "--reset-mode=hw")
|
||||||
|
|
||||||
# keep first
|
# keep first
|
||||||
|
include(${ZEPHYR_BASE}/boards/common/jlink.board.cmake)
|
||||||
include(${ZEPHYR_BASE}/boards/common/stm32cubeprogrammer.board.cmake)
|
include(${ZEPHYR_BASE}/boards/common/stm32cubeprogrammer.board.cmake)
|
||||||
include(${ZEPHYR_BASE}/boards/common/openocd-stm32.board.cmake)
|
include(${ZEPHYR_BASE}/boards/common/openocd-stm32.board.cmake)
|
||||||
include(${ZEPHYR_BASE}/boards/common/jlink.board.cmake)
|
|
||||||
include(${ZEPHYR_BASE}/boards/common/blackmagicprobe.board.cmake)
|
include(${ZEPHYR_BASE}/boards/common/blackmagicprobe.board.cmake)
|
||||||
8
software/boards/iten/bluepill_f103rb/board.yml
Normal file
8
software/boards/iten/bluepill_f103rb/board.yml
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
# Copyright (c) 2025 Eduard Iten
|
||||||
|
# SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
board:
|
||||||
|
name: bluepill_f103rb
|
||||||
|
vendor: iten
|
||||||
|
socs:
|
||||||
|
- name: stm32f103xb
|
||||||
@@ -1,58 +0,0 @@
|
|||||||
config BOARD_VALVE_NODE
|
|
||||||
select SOC_STM32F103XB
|
|
||||||
|
|
||||||
mainmenu "APP CAN Settings"
|
|
||||||
config LOOPBACK_MODE
|
|
||||||
bool "Loopback LOOPBACK_MODE"
|
|
||||||
default n
|
|
||||||
help
|
|
||||||
Set the can controller to loopback mode.
|
|
||||||
This allows testing without a second board.
|
|
||||||
|
|
||||||
mainmenu "APP Logging Settings"
|
|
||||||
config LOG_CAN_LEVEL
|
|
||||||
int "Log level for CAN"
|
|
||||||
default 3
|
|
||||||
range 0 7
|
|
||||||
help
|
|
||||||
Set the log level for CAN messages.
|
|
||||||
0 = None, 1 = Error, 2 = Warning, 3 = Info, 4 = Debug, 5 = Trace, 6 = Debug2, 7 = Debug3
|
|
||||||
|
|
||||||
config LOG_SETTINGS_LEVEL
|
|
||||||
int "Log level for settings"
|
|
||||||
default 3
|
|
||||||
range 0 7
|
|
||||||
help
|
|
||||||
Set the log level for CAN messages.
|
|
||||||
0 = None, 1 = Error, 2 = Warning, 3 = Info, 4 = Debug, 5 = Trace, 6 = Debug2, 7 = Debug3
|
|
||||||
|
|
||||||
config LOG_WATERLEVELSENSOR_LEVEL
|
|
||||||
int "Log level for waterlevel sensor"
|
|
||||||
default 3
|
|
||||||
range 0 7
|
|
||||||
help
|
|
||||||
Set the log level for CAN messages.
|
|
||||||
0 = None, 1 = Error, 2 = Warning, 3 = Info, 4 = Debug, 5 = Trace, 6 = Debug2, 7 = Debug3
|
|
||||||
|
|
||||||
config LOG_VALVE_LEVEL
|
|
||||||
int "Log level for valve control"
|
|
||||||
default 3
|
|
||||||
range 0 7
|
|
||||||
help
|
|
||||||
Set the log level for valve control messages.
|
|
||||||
0 = None, 1 = Error, 2 = Warning, 3 = Info, 4 = Debug, 5 = Trace, 6 = Debug2, 7 = Debug3
|
|
||||||
|
|
||||||
mainmenu "Irrigation controller node configuration"
|
|
||||||
config HAS_MODBUS_WATERLEVEL_SENSOR
|
|
||||||
bool "Has modbus water level sensor"
|
|
||||||
default n
|
|
||||||
help
|
|
||||||
Enable modbus water level sensor support.
|
|
||||||
This allows reading the water level from a modbus device.
|
|
||||||
|
|
||||||
config HAS_VALVE
|
|
||||||
bool "Has valve control"
|
|
||||||
default n
|
|
||||||
help
|
|
||||||
Enable valve control support.
|
|
||||||
This allows controlling valves via CAN messages.
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
board:
|
|
||||||
name: valve_node
|
|
||||||
full_name: Irrigation system CANbus valve node
|
|
||||||
socs:
|
|
||||||
- name: stm32f103xb
|
|
||||||
# revision:
|
|
||||||
# format: number
|
|
||||||
# default: "1"
|
|
||||||
# revisions:
|
|
||||||
# -name: "1"
|
|
||||||
@@ -1,206 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright (c) 2017 Linaro Limited
|
|
||||||
*
|
|
||||||
* SPDX-License-Identifier: Apache-2.0
|
|
||||||
*/
|
|
||||||
|
|
||||||
/dts-v1/;
|
|
||||||
#include <st/f1/stm32f1.dtsi>
|
|
||||||
#include <st/f1/stm32f103Xb.dtsi>
|
|
||||||
#include <st/f1/stm32f103r(8-b)tx-pinctrl.dtsi>
|
|
||||||
#include <zephyr/dt-bindings/input/input-event-codes.h>
|
|
||||||
|
|
||||||
/ {
|
|
||||||
model = "Iten engineering Valve Node";
|
|
||||||
compatible = "st,stm32f103rb";
|
|
||||||
|
|
||||||
can_loopback0: can_loopback0 {
|
|
||||||
status = "disabled";
|
|
||||||
compatible = "zephyr,can-loopback";
|
|
||||||
};
|
|
||||||
|
|
||||||
chosen {
|
|
||||||
zephyr,console = &usart1;
|
|
||||||
zephyr,shell-uart = &usart1;
|
|
||||||
zephyr,sram = &sram0;
|
|
||||||
zephyr,flash = &flash0;
|
|
||||||
zephyr,canbus = &can1;
|
|
||||||
};
|
|
||||||
|
|
||||||
leds: leds {
|
|
||||||
compatible = "gpio-leds";
|
|
||||||
|
|
||||||
green_led_2: led_2 {
|
|
||||||
gpios = <&gpiob 2 GPIO_ACTIVE_HIGH>;
|
|
||||||
label = "User LD2";
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
gpio_keys {
|
|
||||||
compatible = "gpio-keys";
|
|
||||||
|
|
||||||
user_button: button {
|
|
||||||
label = "User";
|
|
||||||
gpios = <&gpioc 13 GPIO_ACTIVE_LOW>;
|
|
||||||
zephyr,code = <INPUT_KEY_0>;
|
|
||||||
};
|
|
||||||
|
|
||||||
endstopopen: endstop_open {
|
|
||||||
gpios = <&gpiob 4 (GPIO_ACTIVE_LOW | GPIO_PULL_UP)>;
|
|
||||||
label = "Endstop Open";
|
|
||||||
};
|
|
||||||
|
|
||||||
endstopclose: endstop_closed {
|
|
||||||
gpios = <&gpiob 5 (GPIO_ACTIVE_LOW | GPIO_PULL_UP)>;
|
|
||||||
label = "Endstop Close";
|
|
||||||
};
|
|
||||||
|
|
||||||
statusopen: status_open {
|
|
||||||
gpios = <&gpiob 14 (GPIO_ACTIVE_LOW | GPIO_PULL_UP)>;
|
|
||||||
label = "Status Open";
|
|
||||||
};
|
|
||||||
|
|
||||||
statusclose: status_close {
|
|
||||||
gpios = <&gpioa 8 (GPIO_ACTIVE_LOW | GPIO_PULL_UP)>;
|
|
||||||
label = "Status Close";
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
aliases {
|
|
||||||
led0 = &green_led_2;
|
|
||||||
sw0 = &user_button;
|
|
||||||
watchdog0 = &iwdg;
|
|
||||||
die-temp0 = &die_temp;
|
|
||||||
adc-motor-current = &motor_current_channel;
|
|
||||||
adc-vref = &vref_channel;
|
|
||||||
};
|
|
||||||
|
|
||||||
};
|
|
||||||
|
|
||||||
&clk_lsi {
|
|
||||||
status = "okay";
|
|
||||||
};
|
|
||||||
|
|
||||||
&clk_hse {
|
|
||||||
clock-frequency = <DT_FREQ_M(8)>;
|
|
||||||
status = "okay";
|
|
||||||
};
|
|
||||||
|
|
||||||
&pll {
|
|
||||||
mul = <9>;
|
|
||||||
clocks = <&clk_hse>;
|
|
||||||
status = "okay";
|
|
||||||
};
|
|
||||||
|
|
||||||
&rcc {
|
|
||||||
clocks = <&pll>;
|
|
||||||
clock-frequency = <DT_FREQ_M(72)>;
|
|
||||||
ahb-prescaler = <1>;
|
|
||||||
apb1-prescaler = <2>;
|
|
||||||
apb2-prescaler = <1>;
|
|
||||||
adc-prescaler = <6>;
|
|
||||||
};
|
|
||||||
|
|
||||||
&usart1 {
|
|
||||||
pinctrl-0 = <&usart1_tx_pa9 &usart1_rx_pa10>;
|
|
||||||
pinctrl-names = "default";
|
|
||||||
status = "okay";
|
|
||||||
current-speed = <115200>;
|
|
||||||
};
|
|
||||||
|
|
||||||
&usart2 {
|
|
||||||
pinctrl-0 = <&usart2_tx_pa2 &usart2_rx_pa3>;
|
|
||||||
current-speed = <9600>;
|
|
||||||
pinctrl-names = "default";
|
|
||||||
status = "okay";
|
|
||||||
modbus0 {
|
|
||||||
compatible = "zephyr,modbus-serial";
|
|
||||||
status = "okay";
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
&usart3 {
|
|
||||||
pinctrl-0 = <&usart3_tx_pb10 &usart3_rx_pb11>;
|
|
||||||
current-speed = <115200>;
|
|
||||||
pinctrl-names = "default";
|
|
||||||
};
|
|
||||||
|
|
||||||
&i2c1 {
|
|
||||||
pinctrl-0 = <&i2c1_scl_remap1_pb8 &i2c1_sda_remap1_pb9>;
|
|
||||||
pinctrl-names = "default";
|
|
||||||
status = "okay";
|
|
||||||
clock-frequency = <I2C_BITRATE_FAST>;
|
|
||||||
};
|
|
||||||
|
|
||||||
&iwdg {
|
|
||||||
status = "okay";
|
|
||||||
};
|
|
||||||
|
|
||||||
&rtc {
|
|
||||||
clocks = <&rcc STM32_CLOCK_BUS_APB1 0x10000000>,
|
|
||||||
<&rcc STM32_SRC_LSI RTC_SEL(2)>;
|
|
||||||
status = "okay";
|
|
||||||
};
|
|
||||||
|
|
||||||
&adc1 {
|
|
||||||
pinctrl-0 = <&adc_pb1_pins>;
|
|
||||||
pinctrl-names = "default";
|
|
||||||
status = "okay";
|
|
||||||
|
|
||||||
#address-cells = <1>;
|
|
||||||
#size-cells = <0>;
|
|
||||||
|
|
||||||
motor_current_channel: channel@9 {
|
|
||||||
reg = <0x9>;
|
|
||||||
zephyr,gain = "ADC_GAIN_1";
|
|
||||||
zephyr,reference = "ADC_REF_VDD_1";
|
|
||||||
zephyr,acquisition-time = <49159>;
|
|
||||||
zephyr,resolution = <12>;
|
|
||||||
};
|
|
||||||
|
|
||||||
vref_channel: channel@11 { /* 17 dezimal = 11 hex */
|
|
||||||
reg = <0x11>;
|
|
||||||
zephyr,gain = "ADC_GAIN_1";
|
|
||||||
zephyr,reference = "ADC_REF_VDD_1";
|
|
||||||
zephyr,acquisition-time = <49159>;
|
|
||||||
zephyr,resolution = <12>;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
&die_temp {
|
|
||||||
status = "okay";
|
|
||||||
};
|
|
||||||
|
|
||||||
&dma1 {
|
|
||||||
status = "okay";
|
|
||||||
};
|
|
||||||
|
|
||||||
&flash0 {
|
|
||||||
partitions {
|
|
||||||
compatible = "fixed-partitions";
|
|
||||||
#address-cells = <1>;
|
|
||||||
#size-cells = <1>;
|
|
||||||
|
|
||||||
storage_partition: partition@1f800 {
|
|
||||||
label = "storage";
|
|
||||||
reg = <0x0001f800 DT_SIZE_K(2)>;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
&can1 {
|
|
||||||
pinctrl-0 = <&can_rx_pa11 &can_tx_pa12>;
|
|
||||||
pinctrl-names = "default";
|
|
||||||
status= "okay";
|
|
||||||
bitrate = <125000>;
|
|
||||||
};
|
|
||||||
|
|
||||||
&exti {
|
|
||||||
status = "okay";
|
|
||||||
};
|
|
||||||
|
|
||||||
&pinctrl {
|
|
||||||
adc_pb1_pins: adc_pb1_pins {
|
|
||||||
pinmux = <STM32F1_PINMUX('B', 1, ANALOG, NO_REMAP)>;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
@@ -1,31 +0,0 @@
|
|||||||
# SPDX-License-Identifier: Apache-2.0
|
|
||||||
|
|
||||||
# enable uart driver
|
|
||||||
CONFIG_SERIAL=y
|
|
||||||
# enable console
|
|
||||||
CONFIG_CONSOLE=y
|
|
||||||
|
|
||||||
# enable GPIO
|
|
||||||
CONFIG_GPIO=y
|
|
||||||
|
|
||||||
# modbus config
|
|
||||||
CONFIG_UART_INTERRUPT_DRIVEN=y
|
|
||||||
CONFIG_UART_LINE_CTRL=n
|
|
||||||
CONFIG_MODBUS=y
|
|
||||||
CONFIG_MODBUS_ROLE_CLIENT=y
|
|
||||||
|
|
||||||
# can config
|
|
||||||
CONFIG_CAN=y
|
|
||||||
CONFIG_CAN_INIT_PRIORITY=80
|
|
||||||
#CONFIG_CAN_MAX_FILTER=5
|
|
||||||
CONFIG_CAN_ACCEPT_RTR=y
|
|
||||||
|
|
||||||
# settings
|
|
||||||
CONFIG_FLASH=y
|
|
||||||
CONFIG_FLASH_MAP=y
|
|
||||||
CONFIG_SETTINGS=y
|
|
||||||
CONFIG_SETTINGS_RUNTIME=y
|
|
||||||
CONFIG_NVS=y
|
|
||||||
CONFIG_SETTINGS_NVS=y
|
|
||||||
CONFIG_HEAP_MEM_POOL_SIZE=256
|
|
||||||
CONFIG_MPU_ALLOW_FLASH_WRITE=y
|
|
||||||
10
software/include/lib/fwu.h
Normal file
10
software/include/lib/fwu.h
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
#ifndef FWU_H
|
||||||
|
#define FWU_H
|
||||||
|
|
||||||
|
#include <stdint.h>
|
||||||
|
|
||||||
|
void fwu_init(void);
|
||||||
|
void fwu_handler(uint16_t addr, uint16_t reg);
|
||||||
|
uint16_t fwu_get_last_chunk_crc(void);
|
||||||
|
|
||||||
|
#endif // FWU_H
|
||||||
52
software/include/lib/modbus_server.h
Normal file
52
software/include/lib/modbus_server.h
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
#ifndef MODBUS_SERVER_H
|
||||||
|
#define MODBUS_SERVER_H
|
||||||
|
|
||||||
|
#include <stdint.h>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Modbus Input Register Addresses.
|
||||||
|
*/
|
||||||
|
enum {
|
||||||
|
/* Valve Control & Status */
|
||||||
|
REG_INPUT_VALVE_STATE_MOVEMENT = 0x0000,
|
||||||
|
REG_INPUT_MOTOR_CURRENT_MA = 0x0001,
|
||||||
|
/* Digital Inputs */
|
||||||
|
REG_INPUT_DIGITAL_INPUTS_STATE = 0x0020,
|
||||||
|
REG_INPUT_BUTTON_EVENTS = 0x0021,
|
||||||
|
/* System Config & Status */
|
||||||
|
REG_INPUT_FIRMWARE_VERSION_MAJOR_MINOR = 0x00F0,
|
||||||
|
REG_INPUT_FIRMWARE_VERSION_PATCH = 0x00F1,
|
||||||
|
REG_INPUT_DEVICE_STATUS = 0x00F2,
|
||||||
|
REG_INPUT_UPTIME_SECONDS_LOW = 0x00F3,
|
||||||
|
REG_INPUT_UPTIME_SECONDS_HIGH = 0x00F4,
|
||||||
|
/* Firmware Update */
|
||||||
|
REG_INPUT_FWU_LAST_CHUNK_CRC = 0x0100,
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Modbus Holding Register Addresses.
|
||||||
|
*/
|
||||||
|
enum {
|
||||||
|
/* Valve Control */
|
||||||
|
REG_HOLDING_VALVE_COMMAND = 0x0000,
|
||||||
|
REG_HOLDING_MAX_OPENING_TIME_S = 0x0001,
|
||||||
|
REG_HOLDING_MAX_CLOSING_TIME_S = 0x0002,
|
||||||
|
/* Digital Outputs */
|
||||||
|
REG_HOLDING_DIGITAL_OUTPUTS_STATE = 0x0010,
|
||||||
|
/* System Config */
|
||||||
|
REG_HOLDING_WATCHDOG_TIMEOUT_S = 0x00F0,
|
||||||
|
REG_HOLDING_DEVICE_RESET = 0x00F1,
|
||||||
|
/* Firmware Update */
|
||||||
|
REG_HOLDING_FWU_COMMAND = 0x0100,
|
||||||
|
REG_HOLDING_FWU_CHUNK_OFFSET_LOW = 0x0101,
|
||||||
|
REG_HOLDING_FWU_CHUNK_OFFSET_HIGH = 0x0102,
|
||||||
|
REG_HOLDING_FWU_CHUNK_SIZE = 0x0103,
|
||||||
|
REG_HOLDING_FWU_DATA_BUFFER = 0x0180,
|
||||||
|
};
|
||||||
|
|
||||||
|
int modbus_server_init(void);
|
||||||
|
int modbus_reconfigure(uint32_t baudrate, uint8_t unit_id);
|
||||||
|
uint32_t modbus_get_baudrate(void);
|
||||||
|
uint8_t modbus_get_unit_id(void);
|
||||||
|
|
||||||
|
#endif // MODBUS_SERVER_H
|
||||||
23
software/include/lib/valve.h
Normal file
23
software/include/lib/valve.h
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
#ifndef VALVE_H
|
||||||
|
#define VALVE_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 };
|
||||||
|
|
||||||
|
void valve_init(void);
|
||||||
|
void valve_open(void);
|
||||||
|
void valve_close(void);
|
||||||
|
void valve_stop(void);
|
||||||
|
|
||||||
|
enum valve_state valve_get_state(void);
|
||||||
|
enum valve_movement valve_get_movement(void);
|
||||||
|
uint16_t valve_get_motor_current(void);
|
||||||
|
|
||||||
|
void valve_set_max_open_time(uint16_t seconds);
|
||||||
|
void valve_set_max_close_time(uint16_t seconds);
|
||||||
|
uint16_t valve_get_max_open_time(void);
|
||||||
|
uint16_t valve_get_max_close_time(void);
|
||||||
|
|
||||||
|
#endif // VALVE_H
|
||||||
5
software/lib/CMakeLists.txt
Normal file
5
software/lib/CMakeLists.txt
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
add_subdirectory_ifdef(CONFIG_LIB_FWU fwu)
|
||||||
|
add_subdirectory_ifdef(CONFIG_LIB_MODBUS_SERVER modbus_server)
|
||||||
|
add_subdirectory_ifdef(CONFIG_LIB_VALVE valve)
|
||||||
|
add_subdirectory_ifdef(CONFIG_SHELL_SYSTEM shell_system)
|
||||||
|
add_subdirectory_ifdef(CONFIG_SHELL_MODBUS shell_modbus)
|
||||||
8
software/lib/Kconfig
Normal file
8
software/lib/Kconfig
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
menu "Irrigation system software libraries"
|
||||||
|
|
||||||
|
rsource "fwu/Kconfig"
|
||||||
|
rsource "modbus_server/Kconfig"
|
||||||
|
rsource "valve/Kconfig"
|
||||||
|
rsource "shell_system/Kconfig"
|
||||||
|
rsource "shell_modbus/Kconfig"
|
||||||
|
endmenu
|
||||||
@@ -1,286 +0,0 @@
|
|||||||
#include "canbus.h"
|
|
||||||
#include <zephyr/logging/log.h>
|
|
||||||
#include <zephyr/kernel.h>
|
|
||||||
#include <zephyr/sys/byteorder.h>
|
|
||||||
#include <zephyr/drivers/can.h>
|
|
||||||
#include <stdlib.h>
|
|
||||||
|
|
||||||
#ifdef CONFIG_HAS_MODBUS_WATERLEVEL_SENSOR
|
|
||||||
#include "waterlevel_sensor.h"
|
|
||||||
#endif // CONFIG_HAS_MODBUS_WATERLEVEL_SENSOR
|
|
||||||
|
|
||||||
#ifdef CONFIG_HAS_VALVE
|
|
||||||
#include "valve.h"
|
|
||||||
#endif // CONFIG_HAS_VALVE
|
|
||||||
|
|
||||||
const struct device *const can_dev = DEVICE_DT_GET(DT_CHOSEN(zephyr_canbus));
|
|
||||||
|
|
||||||
LOG_MODULE_REGISTER(canbus, CONFIG_LOG_CAN_LEVEL);
|
|
||||||
|
|
||||||
K_MSGQ_DEFINE(canbus_msgq, sizeof(struct can_frame), CANBUS_RX_MSGQ_SIZE, 4);
|
|
||||||
K_THREAD_STACK_DEFINE(canbus_rx_stack, CANBUS_RX_THREAD_STACK_SIZE);
|
|
||||||
|
|
||||||
uint8_t node_id = 0; // Default node ID, can be set later
|
|
||||||
uint8_t can_msg_id = 0;
|
|
||||||
|
|
||||||
k_tid_t canbus_rx_thread_id;
|
|
||||||
struct k_thread canbus_rx_thread_data;
|
|
||||||
|
|
||||||
int canbus_rx_filter_id = -1;
|
|
||||||
|
|
||||||
void canbus_rx_callback(const struct device *dev, struct can_frame *frame, void *user_data)
|
|
||||||
{
|
|
||||||
int rc;
|
|
||||||
ARG_UNUSED(dev);
|
|
||||||
ARG_UNUSED(user_data);
|
|
||||||
|
|
||||||
if (frame->id >> 8 != node_id) // Check if the frame ID matches the node ID
|
|
||||||
{
|
|
||||||
LOG_DBG("Received CAN frame with ID %d, but it does not match node ID %d", frame->id >> 8, node_id);
|
|
||||||
return; // Ignore frames that do not match the node ID
|
|
||||||
}
|
|
||||||
|
|
||||||
// Process the received CAN frame
|
|
||||||
rc = k_msgq_put(&canbus_msgq, frame, K_NO_WAIT);
|
|
||||||
if (rc != 0)
|
|
||||||
{
|
|
||||||
LOG_ERR("Failed to put CAN frame into message queue: %d", rc);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void canbus_thread(void *arg1, void *arg2, void *arg3)
|
|
||||||
{
|
|
||||||
ARG_UNUSED(arg1);
|
|
||||||
ARG_UNUSED(arg2);
|
|
||||||
ARG_UNUSED(arg3);
|
|
||||||
|
|
||||||
LOG_INF("CAN bus thread started");
|
|
||||||
|
|
||||||
// Main loop for CAN bus operations
|
|
||||||
while (1)
|
|
||||||
{
|
|
||||||
int rc;
|
|
||||||
struct can_frame frame;
|
|
||||||
k_msgq_get(&canbus_msgq, &frame, K_FOREVER); // Wait for a message from the queue
|
|
||||||
LOG_DBG("Received CAN frame with ID: 0x%02x, DLC: %d, RTR: %d",
|
|
||||||
frame.id, frame.dlc, (uint8_t)(frame.flags & CAN_FRAME_RTR));
|
|
||||||
LOG_HEXDUMP_DBG(frame.data, frame.dlc, "CAN frame data");
|
|
||||||
uint8_t reg = frame.id & 0xFF; // Extract register from the frame ID
|
|
||||||
bool is_rtr = (frame.flags & CAN_FRAME_RTR) != 0; // Check if it's a remote transmission request
|
|
||||||
switch (reg)
|
|
||||||
{
|
|
||||||
#ifdef CONFIG_HAS_MODBUS_WATERLEVEL_SENSOR
|
|
||||||
case CANBUS_REG_WATERLEVEL_LEVEL:
|
|
||||||
case CANBUS_REG_WATERLEVEL_ZERO_POINT:
|
|
||||||
case CANBUS_REG_WATERLEVEL_MAX_POINT:
|
|
||||||
waterlevel_command_t command;
|
|
||||||
command.cmd = is_rtr ? WATERLEVEL_CMD_GET : WATERLEVEL_CMD_SET; // Determine command type based on RTR
|
|
||||||
command.reg = reg; // Set the register ID
|
|
||||||
int16_t value = 0; // Initialize value to 0
|
|
||||||
if (!is_rtr) // If it's not a remote request, extract the value from the frame data
|
|
||||||
{
|
|
||||||
if (frame.dlc < sizeof(command.data))
|
|
||||||
{ LOG_ERR("Received frame with insufficient data length: %d", frame.dlc);
|
|
||||||
continue; // Skip processing if data length is insufficient
|
|
||||||
}
|
|
||||||
value = sys_be16_to_cpu(*(uint16_t *)frame.data); // Convert data from big-endian to host byte order
|
|
||||||
command.data = value; // Set the data field
|
|
||||||
LOG_DBG("Setting command data to: %d", value);
|
|
||||||
}
|
|
||||||
extern struct k_msgq waterlevel_sensor_msgq; // Declare the water level sensor message queue
|
|
||||||
k_msgq_put(&waterlevel_sensor_msgq, &command, K_NO_WAIT);
|
|
||||||
break;
|
|
||||||
#endif // CONFIG_HAS_MODBUS_WATERLEVEL_SENSOR
|
|
||||||
#ifdef CONFIG_HAS_VALVE
|
|
||||||
case CANBUS_REG_VALVE_STATUS:
|
|
||||||
case CANBUS_REG_VALVE_OPERATION:
|
|
||||||
if (is_rtr)
|
|
||||||
{
|
|
||||||
LOG_DBG("Received remote request for valve status or operation");
|
|
||||||
if (reg == CANBUS_REG_VALVE_STATUS)
|
|
||||||
{
|
|
||||||
valve_send_status(); // Send the current valve status
|
|
||||||
}
|
|
||||||
else if (reg == CANBUS_REG_VALVE_OPERATION)
|
|
||||||
{
|
|
||||||
valve_send_operation(); // Send the current valve operation state
|
|
||||||
} else {
|
|
||||||
LOG_ERR("Unknown valve register: 0x%02x", reg);
|
|
||||||
continue; // Skip processing if the register is unknown
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
LOG_ERR("Received CAN frame with data for valve status or operation, but RTR is not set");
|
|
||||||
continue; // Skip processing if RTR is not set for valve status or operation
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case CANBUS_REG_VALVE_COMMAND:
|
|
||||||
{
|
|
||||||
if (is_rtr) {
|
|
||||||
LOG_ERR("Received remote request for valve command, but this is not supported");
|
|
||||||
continue; // Skip processing if RTR is set for valve command
|
|
||||||
} else {
|
|
||||||
// Extract the command from the frame data
|
|
||||||
if (frame.dlc < sizeof(uint8_t)) {
|
|
||||||
LOG_ERR("Received frame with insufficient data length for valve command: %d", frame.dlc);
|
|
||||||
continue; // Skip processing if data length is insufficient
|
|
||||||
}
|
|
||||||
uint8_t command = frame.data[0]; // Get the command from the first byte of data
|
|
||||||
LOG_DBG("Received valve command: 0x%02x", command);
|
|
||||||
rc = valve_cmd(command); // Send the command to the valve
|
|
||||||
if (rc < 0) {
|
|
||||||
LOG_ERR("Failed to send valve command: %d", rc);
|
|
||||||
continue; // Skip processing if sending the command failed
|
|
||||||
}
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
default:
|
|
||||||
LOG_DBG("Received CAN frame with unknown register: 0x%02x", reg);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
#endif // CONFIG_HAS_VALVE
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
int canbus_init(void)
|
|
||||||
{
|
|
||||||
int rc = 0;
|
|
||||||
|
|
||||||
if (!device_is_ready(can_dev))
|
|
||||||
{
|
|
||||||
LOG_ERR("CAN device %s is not ready", can_dev->name);
|
|
||||||
return -ENODEV;
|
|
||||||
}
|
|
||||||
|
|
||||||
#ifdef CONFIG_LOOPBACK_MODE
|
|
||||||
rc = can_set_mode(can_dev, CAN_MODE_LOOPBACK);
|
|
||||||
if (rc != 0)
|
|
||||||
{
|
|
||||||
LOG_ERR("Failed to set CAN loopback mode: %d", rc);
|
|
||||||
return rc;
|
|
||||||
}
|
|
||||||
#endif
|
|
||||||
|
|
||||||
rc = can_start(can_dev);
|
|
||||||
if (rc != 0)
|
|
||||||
{
|
|
||||||
printf("Error starting CAN controller [%d]", rc);
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
LOG_DBG("CAN device %s is ready", can_dev->name);
|
|
||||||
// Initialize the CAN RX thread
|
|
||||||
canbus_rx_thread_id = k_thread_create(&canbus_rx_thread_data, canbus_rx_stack,
|
|
||||||
K_THREAD_STACK_SIZEOF(canbus_rx_stack), canbus_thread,
|
|
||||||
NULL, NULL, NULL,
|
|
||||||
CANBUS_RX_THREAD_PRIORITY, 0, K_NO_WAIT);
|
|
||||||
k_thread_name_set(canbus_rx_thread_id, "canbus_rx");
|
|
||||||
|
|
||||||
const struct can_filter filter = {
|
|
||||||
.id = node_id << 8, // Standard ID with node ID in the first byte
|
|
||||||
.mask = 0x700, // Mask to match the first byte of the ID
|
|
||||||
.flags = 0, // No special flags
|
|
||||||
};
|
|
||||||
canbus_rx_filter_id = can_add_rx_filter(can_dev, canbus_rx_callback, NULL, &filter);
|
|
||||||
LOG_DBG("CAN RX filter added for node ID %d", canbus_rx_filter_id);
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
void canbus_tx8_callback(const struct device *dev, int error, void *user_data)
|
|
||||||
{
|
|
||||||
ARG_UNUSED(dev);
|
|
||||||
uint8_t frame_id = *(uint8_t *)user_data; // Retrieve the frame ID from user data
|
|
||||||
|
|
||||||
if (error != 0)
|
|
||||||
{
|
|
||||||
LOG_ERR("CAN transmission error. Error code: %d, Frame ID: %d", error, frame_id);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
LOG_DBG("CAN message with Frame ID %d sent successfully", frame_id);
|
|
||||||
}
|
|
||||||
free(user_data); // Free the allocated memory for frame ID
|
|
||||||
}
|
|
||||||
|
|
||||||
int canbus_send8(uint16_t reg, uint8_t value)
|
|
||||||
{
|
|
||||||
int rc = 0;
|
|
||||||
struct can_frame frame = {
|
|
||||||
.id = (node_id << 8) | reg, // Standard ID with node ID in the first byte
|
|
||||||
.dlc = sizeof(value), // Data Length Code (DLC)
|
|
||||||
};
|
|
||||||
frame.data[0] = value; // Set the value in the first byte of the data
|
|
||||||
uint8_t *frame_id = malloc(sizeof(uint8_t)); // Allocate memory for frame ID
|
|
||||||
LOG_DBG("Preparing to send 8-bit value 0x%02x to register 0x%02x on node %d", value, reg, node_id);
|
|
||||||
if (frame_id == NULL)
|
|
||||||
{
|
|
||||||
LOG_ERR("Failed to allocate memory for frame ID");
|
|
||||||
return -ENOMEM; // Not enough memory
|
|
||||||
}
|
|
||||||
*frame_id = can_msg_id++; // Increment message ID for uniqueness
|
|
||||||
LOG_DBG("Using frame ID: %d", *frame_id);
|
|
||||||
rc = can_send(can_dev, &frame, CANBUS_TX_TIMEOUT, canbus_tx8_callback, frame_id);
|
|
||||||
// Send the CAN frame with a timeout and callback
|
|
||||||
if (rc != 0)
|
|
||||||
{
|
|
||||||
LOG_ERR("Failed to queue CAN frame: %d", rc);
|
|
||||||
free(frame_id); // Free the allocated memory for frame ID
|
|
||||||
return rc;
|
|
||||||
}
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
void canbus_tx16_callback(const struct device *dev, int error, void *user_data)
|
|
||||||
{
|
|
||||||
ARG_UNUSED(dev);
|
|
||||||
uint8_t frame_id = *(uint8_t *)user_data; // Retrieve the frame ID from user data
|
|
||||||
|
|
||||||
if (error != 0)
|
|
||||||
{
|
|
||||||
LOG_ERR("CAN transmission error. Error code: %d, Frame ID: %d", error, frame_id);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
LOG_DBG("CAN message with Frame ID %d sent successfully", frame_id);
|
|
||||||
}
|
|
||||||
free(user_data); // Free the allocated memory for frame ID
|
|
||||||
}
|
|
||||||
|
|
||||||
int canbus_send16(uint16_t reg, uint16_t value)
|
|
||||||
{
|
|
||||||
int rc = 0;
|
|
||||||
union data_type
|
|
||||||
{
|
|
||||||
int16_t value;
|
|
||||||
uint8_t bytes[2];
|
|
||||||
} data;
|
|
||||||
data.value = sys_cpu_to_be16(value); // Convert value to big-endian format
|
|
||||||
|
|
||||||
struct can_frame frame = {
|
|
||||||
.id = (node_id << 8) | reg, // Standard ID with node ID in the first byte
|
|
||||||
.dlc = sizeof(data), // Data Length Code (DLC)
|
|
||||||
};
|
|
||||||
memcpy(frame.data, data.bytes, sizeof(data)); // Copy data into the frame
|
|
||||||
uint8_t *frame_id = malloc(sizeof(uint8_t)); // Allocate memory for frame ID
|
|
||||||
LOG_DBG("Preparing to send 16-bit value 0x%04x to register 0x%02x on node %d", value, reg, node_id);
|
|
||||||
if (frame_id == NULL)
|
|
||||||
{
|
|
||||||
LOG_ERR("Failed to allocate memory for frame ID");
|
|
||||||
return -ENOMEM; // Not enough memory
|
|
||||||
}
|
|
||||||
*frame_id = can_msg_id++; // Increment message ID for uniqueness
|
|
||||||
LOG_DBG("Using frame ID: %d", *frame_id);
|
|
||||||
rc = can_send(can_dev, &frame, CANBUS_TX_TIMEOUT, canbus_tx16_callback, frame_id);
|
|
||||||
// Send the CAN frame with a timeout and callback
|
|
||||||
if (rc != 0)
|
|
||||||
{
|
|
||||||
LOG_ERR("Failed to queue CAN frame: %d", rc);
|
|
||||||
free(frame_id); // Free the allocated memory for frame ID
|
|
||||||
return rc;
|
|
||||||
}
|
|
||||||
|
|
||||||
LOG_DBG("Queued 16-bit value 0x%04x to register 0x%02x on node %d, frame ID: %d",
|
|
||||||
value, reg, node_id, *frame_id);
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
#ifndef __CANBUS_H__
|
|
||||||
#define __CANBUS_H__
|
|
||||||
|
|
||||||
#include <stdint.h>
|
|
||||||
#include "canbus_registers.h"
|
|
||||||
|
|
||||||
#define CANBUS_RX_THREAD_STACK_SIZE (512) // Stack size for the CAN RX thread
|
|
||||||
#define CANBUS_RX_THREAD_PRIORITY (5) // Priority for the CAN RX thread
|
|
||||||
#define CANBUS_RX_MSGQ_SIZE (5) // Size of the message queue for CAN RX thread
|
|
||||||
#define CANBUS_TX_TIMEOUT K_MSEC(100) // Timeout for sending CAN messages in milliseconds
|
|
||||||
|
|
||||||
int canbus_init(void);
|
|
||||||
int canbus_send8(uint16_t reg, uint8_t value);
|
|
||||||
int canbus_send16(uint16_t reg, uint16_t value);
|
|
||||||
|
|
||||||
#endif // __CANBUS_H__
|
|
||||||
@@ -1,42 +0,0 @@
|
|||||||
#ifndef __CANBUS_REGISTERS_H__
|
|
||||||
#define __CANBUS_REGISTERS_H__
|
|
||||||
|
|
||||||
enum canbus_registers {
|
|
||||||
CANBUS_REG_REBOOT = 0x00,
|
|
||||||
CANBUS_REG_STATE = 0x01,
|
|
||||||
CANBUS_REG_ERROR = 0x02,
|
|
||||||
|
|
||||||
CANBUS_REG_VALVE_STATUS = 0x10,
|
|
||||||
CANBUS_REG_VALVE_OPERATION = 0x11,
|
|
||||||
CANBUS_REG_VALVE_COMMAND = 0x12,
|
|
||||||
|
|
||||||
CANBUS_REG_WATERLEVEL_STATE = 0x20,
|
|
||||||
CANBUS_REG_WATERLEVEL_LEVEL = 0x21,
|
|
||||||
CANBUS_REG_WATERLEVEL_ZERO_POINT = 0x22,
|
|
||||||
CANBUS_REG_WATERLEVEL_MAX_POINT = 0x23,
|
|
||||||
};
|
|
||||||
|
|
||||||
enum valve_status {
|
|
||||||
VALVE_STATE_CLOSED = 0x00,
|
|
||||||
VALVE_STATE_OPEN = 0x01,
|
|
||||||
VALVE_STATE_ERROR = 0x02,
|
|
||||||
VALVE_STATE_UNKNOWN = 0x03,
|
|
||||||
};
|
|
||||||
|
|
||||||
enum valve_operation_state {
|
|
||||||
VALVE_OPERATION_IDLE = 0x00,
|
|
||||||
VALVE_OPERATION_OPENING = 0x01,
|
|
||||||
VALVE_OPERATION_CLOSING = 0x02,
|
|
||||||
};
|
|
||||||
|
|
||||||
enum valve_command {
|
|
||||||
VALVE_COMMAND_STOP = 0x00,
|
|
||||||
VALVE_COMMAND_OPEN = 0x01,
|
|
||||||
VALVE_COMMAND_CLOSE = 0x02,
|
|
||||||
};
|
|
||||||
|
|
||||||
enum waterlevel_state {
|
|
||||||
WATERLEVEL_STATE_OK = 0x00,
|
|
||||||
WATERLEVEL_STATE_MODBUS_ERROR = 0x02,
|
|
||||||
};
|
|
||||||
#endif // __CANBUS_REGISTERS_H__
|
|
||||||
1
software/lib/fwu/CMakeLists.txt
Normal file
1
software/lib/fwu/CMakeLists.txt
Normal file
@@ -0,0 +1 @@
|
|||||||
|
zephyr_library_sources(fwu.c)
|
||||||
5
software/lib/fwu/Kconfig
Normal file
5
software/lib/fwu/Kconfig
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
config LIB_FWU
|
||||||
|
bool "Enable Firmware Update Library"
|
||||||
|
default y
|
||||||
|
help
|
||||||
|
Enable the Firmware Update Library.
|
||||||
45
software/lib/fwu/fwu.c
Normal file
45
software/lib/fwu/fwu.c
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
#include <zephyr/kernel.h>
|
||||||
|
#include <zephyr/sys/crc.h>
|
||||||
|
#include <zephyr/sys/byteorder.h>
|
||||||
|
#include <zephyr/logging/log.h>
|
||||||
|
#include <lib/fwu.h>
|
||||||
|
|
||||||
|
LOG_MODULE_REGISTER(fwu, LOG_LEVEL_INF);
|
||||||
|
|
||||||
|
#define FWU_BUFFER_SIZE 256
|
||||||
|
static uint8_t fwu_buffer[FWU_BUFFER_SIZE];
|
||||||
|
static uint32_t fwu_chunk_offset = 0;
|
||||||
|
static uint16_t fwu_chunk_size = 0;
|
||||||
|
static uint16_t fwu_last_chunk_crc = 0;
|
||||||
|
|
||||||
|
void fwu_init(void) {}
|
||||||
|
|
||||||
|
void fwu_handler(uint16_t addr, uint16_t reg)
|
||||||
|
{
|
||||||
|
// This is a simplified handler. In a real scenario, you would have a proper mapping
|
||||||
|
// between register addresses and actions.
|
||||||
|
if (addr == 0x0100) { // FWU_COMMAND
|
||||||
|
if (reg == 1) { LOG_INF("FWU: Chunk at offset %u (size %u) verified.", fwu_chunk_offset, fwu_chunk_size); }
|
||||||
|
else if (reg == 2) { LOG_INF("FWU: Finalize command received. Rebooting (simulated)."); }
|
||||||
|
} else if (addr == 0x0101) { // FWU_CHUNK_OFFSET_LOW
|
||||||
|
fwu_chunk_offset = (fwu_chunk_offset & 0xFFFF0000) | reg;
|
||||||
|
} else if (addr == 0x0102) { // FWU_CHUNK_OFFSET_HIGH
|
||||||
|
fwu_chunk_offset = (fwu_chunk_offset & 0x0000FFFF) | ((uint32_t)reg << 16);
|
||||||
|
} else if (addr == 0x0103) { // FWU_CHUNK_SIZE
|
||||||
|
fwu_chunk_size = (reg > FWU_BUFFER_SIZE) ? FWU_BUFFER_SIZE : reg;
|
||||||
|
} else if (addr >= 0x0180 && addr < (0x0180 + (FWU_BUFFER_SIZE / 2))) {
|
||||||
|
uint16_t index = (addr - 0x0180) * 2;
|
||||||
|
if (index < sizeof(fwu_buffer)) {
|
||||||
|
sys_put_be16(reg, &fwu_buffer[index]);
|
||||||
|
if (index + 2 >= fwu_chunk_size) {
|
||||||
|
fwu_last_chunk_crc = crc16_ccitt(0xffff, fwu_buffer, fwu_chunk_size);
|
||||||
|
LOG_INF("FWU: Chunk received, CRC is 0x%04X", fwu_last_chunk_crc);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
uint16_t fwu_get_last_chunk_crc(void)
|
||||||
|
{
|
||||||
|
return fwu_last_chunk_crc;
|
||||||
|
}
|
||||||
1
software/lib/modbus_server/CMakeLists.txt
Normal file
1
software/lib/modbus_server/CMakeLists.txt
Normal file
@@ -0,0 +1 @@
|
|||||||
|
zephyr_library_sources(modbus_server.c)
|
||||||
5
software/lib/modbus_server/Kconfig
Normal file
5
software/lib/modbus_server/Kconfig
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
config LIB_MODBUS_SERVER
|
||||||
|
bool "Enable Modbus Server Library"
|
||||||
|
default y
|
||||||
|
help
|
||||||
|
Enable the Modbus Server Library.
|
||||||
202
software/lib/modbus_server/modbus_server.c
Normal file
202
software/lib/modbus_server/modbus_server.c
Normal file
@@ -0,0 +1,202 @@
|
|||||||
|
#include <zephyr/kernel.h>
|
||||||
|
#include <zephyr/drivers/uart.h>
|
||||||
|
#include <zephyr/device.h>
|
||||||
|
#include <zephyr/modbus/modbus.h>
|
||||||
|
#include <zephyr/logging/log.h>
|
||||||
|
#include <zephyr/settings/settings.h>
|
||||||
|
#include <zephyr/sys/reboot.h>
|
||||||
|
#include <lib/modbus_server.h>
|
||||||
|
#include <lib/valve.h>
|
||||||
|
#include <lib/fwu.h>
|
||||||
|
|
||||||
|
#include <zephyr/usb/usb_device.h>
|
||||||
|
|
||||||
|
LOG_MODULE_REGISTER(modbus_server, LOG_LEVEL_INF);
|
||||||
|
|
||||||
|
static int modbus_iface;
|
||||||
|
static struct modbus_iface_param server_param = {
|
||||||
|
.mode = MODBUS_MODE_RTU,
|
||||||
|
.server = {.user_cb = NULL, .unit_id = 1},
|
||||||
|
.serial = {.baud = 19200, .parity = UART_CFG_PARITY_NONE},
|
||||||
|
};
|
||||||
|
|
||||||
|
static uint16_t watchdog_timeout_s = 0;
|
||||||
|
static struct k_timer watchdog_timer;
|
||||||
|
|
||||||
|
static void watchdog_timer_handler(struct k_timer *timer_id)
|
||||||
|
{
|
||||||
|
LOG_WRN("Modbus watchdog expired! Closing valve as a fail-safe.");
|
||||||
|
valve_close();
|
||||||
|
}
|
||||||
|
|
||||||
|
static inline void reset_watchdog(void)
|
||||||
|
{
|
||||||
|
if (watchdog_timeout_s > 0)
|
||||||
|
{
|
||||||
|
k_timer_start(&watchdog_timer, K_SECONDS(watchdog_timeout_s), K_NO_WAIT);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static int holding_reg_rd(uint16_t addr, uint16_t *reg)
|
||||||
|
{
|
||||||
|
reset_watchdog();
|
||||||
|
switch (addr)
|
||||||
|
{
|
||||||
|
case REG_HOLDING_MAX_OPENING_TIME_S:
|
||||||
|
*reg = valve_get_max_open_time();
|
||||||
|
break;
|
||||||
|
case REG_HOLDING_MAX_CLOSING_TIME_S:
|
||||||
|
*reg = valve_get_max_close_time();
|
||||||
|
break;
|
||||||
|
case REG_HOLDING_WATCHDOG_TIMEOUT_S:
|
||||||
|
*reg = watchdog_timeout_s;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
*reg = 0;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int holding_reg_wr(uint16_t addr, uint16_t reg)
|
||||||
|
{
|
||||||
|
reset_watchdog();
|
||||||
|
switch (addr)
|
||||||
|
{
|
||||||
|
case REG_HOLDING_VALVE_COMMAND:
|
||||||
|
if (reg == 1)
|
||||||
|
{
|
||||||
|
valve_open();
|
||||||
|
}
|
||||||
|
else if (reg == 2)
|
||||||
|
{
|
||||||
|
valve_close();
|
||||||
|
}
|
||||||
|
else if (reg == 0)
|
||||||
|
{
|
||||||
|
valve_stop();
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case REG_HOLDING_MAX_OPENING_TIME_S:
|
||||||
|
valve_set_max_open_time(reg);
|
||||||
|
break;
|
||||||
|
case REG_HOLDING_MAX_CLOSING_TIME_S:
|
||||||
|
valve_set_max_close_time(reg);
|
||||||
|
break;
|
||||||
|
case REG_HOLDING_WATCHDOG_TIMEOUT_S:
|
||||||
|
watchdog_timeout_s = reg;
|
||||||
|
if (watchdog_timeout_s > 0)
|
||||||
|
{
|
||||||
|
LOG_INF("Watchdog enabled with %u s timeout.", watchdog_timeout_s);
|
||||||
|
reset_watchdog();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
LOG_INF("Watchdog disabled.");
|
||||||
|
k_timer_stop(&watchdog_timer);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case REG_HOLDING_DEVICE_RESET:
|
||||||
|
if (reg == 1)
|
||||||
|
{
|
||||||
|
LOG_WRN("Modbus reset command received. Rebooting...");
|
||||||
|
sys_reboot(SYS_REBOOT_WARM);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
fwu_handler(addr, reg);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int input_reg_rd(uint16_t addr, uint16_t *reg)
|
||||||
|
{
|
||||||
|
reset_watchdog();
|
||||||
|
uint32_t uptime_s = k_uptime_get_32() / 1000;
|
||||||
|
switch (addr)
|
||||||
|
{
|
||||||
|
case REG_INPUT_VALVE_STATE_MOVEMENT:
|
||||||
|
*reg = (valve_get_movement() << 8) | (valve_get_state() & 0xFF);
|
||||||
|
break;
|
||||||
|
case REG_INPUT_MOTOR_CURRENT_MA:
|
||||||
|
*reg = valve_get_motor_current();
|
||||||
|
break;
|
||||||
|
case REG_INPUT_UPTIME_SECONDS_LOW:
|
||||||
|
*reg = (uint16_t)(uptime_s & 0xFFFF);
|
||||||
|
break;
|
||||||
|
case REG_INPUT_UPTIME_SECONDS_HIGH:
|
||||||
|
*reg = (uint16_t)(uptime_s >> 16);
|
||||||
|
break;
|
||||||
|
case REG_INPUT_FWU_LAST_CHUNK_CRC:
|
||||||
|
*reg = fwu_get_last_chunk_crc();
|
||||||
|
break;
|
||||||
|
case REG_INPUT_FIRMWARE_VERSION_MAJOR_MINOR:
|
||||||
|
*reg = (0 << 8) | 0;
|
||||||
|
break;
|
||||||
|
case REG_INPUT_FIRMWARE_VERSION_PATCH:
|
||||||
|
*reg = 2;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
*reg = 0;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
static struct modbus_user_callbacks mbs_cbs = {
|
||||||
|
.holding_reg_rd = holding_reg_rd,
|
||||||
|
.holding_reg_wr = holding_reg_wr,
|
||||||
|
.input_reg_rd = input_reg_rd,
|
||||||
|
};
|
||||||
|
|
||||||
|
#define MODBUS_NODE DT_COMPAT_GET_ANY_STATUS_OKAY(zephyr_modbus_serial)
|
||||||
|
|
||||||
|
int modbus_server_init(void)
|
||||||
|
{
|
||||||
|
k_timer_init(&watchdog_timer, watchdog_timer_handler, NULL);
|
||||||
|
const char iface_name[] = {DEVICE_DT_NAME(MODBUS_NODE)};
|
||||||
|
#if DT_NODE_HAS_COMPAT(DT_PARENT(MODBUS_NODE), zephyr_cdc_acm_uart)
|
||||||
|
const struct device *const dev = DEVICE_DT_GET(DT_PARENT(MODBUS_NODE));
|
||||||
|
uint32_t dtr = 0;
|
||||||
|
|
||||||
|
if (!device_is_ready(dev) || usb_enable(NULL))
|
||||||
|
{
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
while (!dtr)
|
||||||
|
{
|
||||||
|
uart_line_ctrl_get(dev, UART_LINE_CTRL_DTR, &dtr);
|
||||||
|
k_sleep(K_MSEC(100));
|
||||||
|
}
|
||||||
|
|
||||||
|
LOG_INF("Client connected to server on %s", dev->name);
|
||||||
|
#endif
|
||||||
|
modbus_iface = modbus_iface_get_by_name(iface_name);
|
||||||
|
if (modbus_iface < 0)
|
||||||
|
{
|
||||||
|
return modbus_iface;
|
||||||
|
}
|
||||||
|
server_param.server.user_cb = &mbs_cbs;
|
||||||
|
return modbus_init_server(modbus_iface, server_param);
|
||||||
|
}
|
||||||
|
|
||||||
|
int modbus_reconfigure(uint32_t baudrate, uint8_t unit_id)
|
||||||
|
{
|
||||||
|
server_param.serial.baud = baudrate;
|
||||||
|
server_param.server.unit_id = unit_id;
|
||||||
|
|
||||||
|
int ret = modbus_init_server(modbus_iface, server_param);
|
||||||
|
|
||||||
|
if (ret == 0)
|
||||||
|
{
|
||||||
|
settings_save_one("modbus/baudrate", &baudrate, sizeof(baudrate));
|
||||||
|
settings_save_one("modbus/unit_id", &unit_id, sizeof(unit_id));
|
||||||
|
}
|
||||||
|
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint32_t modbus_get_baudrate(void) { return server_param.serial.baud; }
|
||||||
|
uint8_t modbus_get_unit_id(void) { return server_param.server.unit_id; }
|
||||||
1
software/lib/shell_modbus/CMakeLists.txt
Normal file
1
software/lib/shell_modbus/CMakeLists.txt
Normal file
@@ -0,0 +1 @@
|
|||||||
|
zephyr_library_sources(shell_modbus.c)
|
||||||
5
software/lib/shell_modbus/Kconfig
Normal file
5
software/lib/shell_modbus/Kconfig
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
config SHELL_MODBUS
|
||||||
|
bool "Enable Shell Modbus"
|
||||||
|
default y
|
||||||
|
help
|
||||||
|
Enable the modnbus shell commands.
|
||||||
119
software/lib/shell_modbus/shell_modbus.c
Normal file
119
software/lib/shell_modbus/shell_modbus.c
Normal file
@@ -0,0 +1,119 @@
|
|||||||
|
#include <zephyr/shell/shell.h>
|
||||||
|
#include <stdlib.h>
|
||||||
|
#include <lib/modbus_server.h>
|
||||||
|
#include <lib/valve.h>
|
||||||
|
|
||||||
|
static int cmd_modbus_set_baud(const struct shell *sh, size_t argc, char **argv)
|
||||||
|
{
|
||||||
|
if (argc != 2) {
|
||||||
|
shell_error(sh, "Usage: set_baud <baudrate>");
|
||||||
|
return -EINVAL;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint32_t new_baud = (uint32_t)strtoul(argv[1], NULL, 10);
|
||||||
|
const uint32_t valid_baud_rates[] = {1200, 2400, 4800, 9600, 19200, 38400, 57600, 115200};
|
||||||
|
bool is_valid = false;
|
||||||
|
|
||||||
|
for (int i = 0; i < ARRAY_SIZE(valid_baud_rates); i++) {
|
||||||
|
if (new_baud == valid_baud_rates[i]) {
|
||||||
|
is_valid = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!is_valid) {
|
||||||
|
char error_msg[128];
|
||||||
|
int offset = snprintf(error_msg, sizeof(error_msg), "Invalid baudrate. Valid rates are: ");
|
||||||
|
for (int i = 0; i < ARRAY_SIZE(valid_baud_rates); i++) {
|
||||||
|
offset += snprintf(error_msg + offset, sizeof(error_msg) - offset, "%u ", valid_baud_rates[i]);
|
||||||
|
}
|
||||||
|
shell_error(sh, "%s", error_msg);
|
||||||
|
return -EINVAL;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (modbus_reconfigure(new_baud, modbus_get_unit_id()) != 0) {
|
||||||
|
shell_error(sh, "Failed to apply new baudrate");
|
||||||
|
} else {
|
||||||
|
shell_print(sh, "Modbus baudrate set to: %u (and saved)", new_baud);
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int cmd_modbus_set_id(const struct shell *sh, size_t argc, char **argv)
|
||||||
|
{
|
||||||
|
if (argc != 2) {
|
||||||
|
shell_error(sh, "Usage: set_id <slave_id>");
|
||||||
|
return -EINVAL;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint32_t new_id_u32 = (uint32_t)strtoul(argv[1], NULL, 10);
|
||||||
|
if (new_id_u32 == 0 || new_id_u32 > 247) {
|
||||||
|
shell_error(sh, "Invalid slave ID: %s. Must be between 1 and 247.", argv[1]);
|
||||||
|
return -EINVAL;
|
||||||
|
}
|
||||||
|
uint8_t new_id = (uint8_t)new_id_u32;
|
||||||
|
|
||||||
|
if (modbus_reconfigure(modbus_get_baudrate(), new_id) != 0) {
|
||||||
|
shell_error(sh, "Failed to apply new slave ID");
|
||||||
|
} else {
|
||||||
|
shell_print(sh, "Modbus slave ID set to: %u (and saved)", new_id);
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int cmd_valve_set_open_time(const struct shell *sh, size_t argc, char **argv)
|
||||||
|
{
|
||||||
|
if (argc != 2) {
|
||||||
|
shell_error(sh, "Usage: set_open_time <seconds>");
|
||||||
|
return -EINVAL;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint16_t seconds = (uint16_t)strtoul(argv[1], NULL, 10);
|
||||||
|
valve_set_max_open_time(seconds);
|
||||||
|
shell_print(sh, "Max opening time set to: %u seconds (and saved)", seconds);
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int cmd_valve_set_close_time(const struct shell *sh, size_t argc, char **argv)
|
||||||
|
{
|
||||||
|
if (argc != 2) {
|
||||||
|
shell_error(sh, "Usage: set_close_time <seconds>");
|
||||||
|
return -EINVAL;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint16_t seconds = (uint16_t)strtoul(argv[1], NULL, 10);
|
||||||
|
valve_set_max_close_time(seconds);
|
||||||
|
shell_print(sh, "Max closing time set to: %u seconds (and saved)", seconds);
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int cmd_config_show(const struct shell *sh, size_t argc, char **argv)
|
||||||
|
{
|
||||||
|
shell_print(sh, "Current Modbus Configuration:");
|
||||||
|
shell_print(sh, " Baudrate: %u", modbus_get_baudrate());
|
||||||
|
shell_print(sh, " Slave ID: %u", modbus_get_unit_id());
|
||||||
|
shell_print(sh, "Current Valve Configuration:");
|
||||||
|
shell_print(sh, " Max Opening Time: %u s", valve_get_max_open_time());
|
||||||
|
shell_print(sh, " Max Closing Time: %u s", valve_get_max_close_time());
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
SHELL_STATIC_SUBCMD_SET_CREATE(sub_modbus_cmds,
|
||||||
|
SHELL_CMD(set_baud, NULL, "Set Modbus baudrate", cmd_modbus_set_baud),
|
||||||
|
SHELL_CMD(set_id, NULL, "Set Modbus slave ID", cmd_modbus_set_id),
|
||||||
|
SHELL_SUBCMD_SET_END
|
||||||
|
);
|
||||||
|
|
||||||
|
SHELL_STATIC_SUBCMD_SET_CREATE(sub_valve_cmds,
|
||||||
|
SHELL_CMD(set_open_time, NULL, "Set max valve opening time", cmd_valve_set_open_time),
|
||||||
|
SHELL_CMD(set_close_time, NULL, "Set max valve closing time", cmd_valve_set_close_time),
|
||||||
|
SHELL_SUBCMD_SET_END
|
||||||
|
);
|
||||||
|
|
||||||
|
SHELL_CMD_REGISTER(modbus, &sub_modbus_cmds, "Modbus configuration", NULL);
|
||||||
|
SHELL_CMD_REGISTER(valve, &sub_valve_cmds, "Valve configuration", NULL);
|
||||||
|
SHELL_CMD_REGISTER(show_config, NULL, "Show all configurations", cmd_config_show);
|
||||||
1
software/lib/shell_system/CMakeLists.txt
Normal file
1
software/lib/shell_system/CMakeLists.txt
Normal file
@@ -0,0 +1 @@
|
|||||||
|
zephyr_library_sources(shell_system.c)
|
||||||
5
software/lib/shell_system/Kconfig
Normal file
5
software/lib/shell_system/Kconfig
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
config SHELL_SYSTEM
|
||||||
|
bool "Enable Shell System"
|
||||||
|
default y
|
||||||
|
help
|
||||||
|
Enable the system commands.
|
||||||
12
software/lib/shell_system/shell_system.c
Normal file
12
software/lib/shell_system/shell_system.c
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
#include <zephyr/shell/shell.h>
|
||||||
|
#include <zephyr/sys/reboot.h>
|
||||||
|
|
||||||
|
static int cmd_reset(const struct shell *sh, size_t argc, char **argv)
|
||||||
|
{
|
||||||
|
shell_print(sh, "Rebooting system...");
|
||||||
|
k_sleep(K_MSEC(100)); // Allow the shell to print the message
|
||||||
|
sys_reboot(SYS_REBOOT_WARM);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
SHELL_CMD_REGISTER(reset, NULL, "Reboot the system", cmd_reset);
|
||||||
@@ -1,163 +0,0 @@
|
|||||||
#include <zephyr/kernel.h>
|
|
||||||
#include <zephyr/logging/log.h>
|
|
||||||
|
|
||||||
#include "canbus.h"
|
|
||||||
#include "canbus_registers.h"
|
|
||||||
#include "valve.h"
|
|
||||||
|
|
||||||
LOG_MODULE_REGISTER(valve, CONFIG_LOG_VALVE_LEVEL);
|
|
||||||
|
|
||||||
K_THREAD_STACK_DEFINE(valve_thread_stack, VALVE_THREAD_STACK_SIZE);
|
|
||||||
K_MSGQ_DEFINE(valve_msgq, sizeof(int), VALVE_MSGQ_SIZE, 4);
|
|
||||||
|
|
||||||
k_tid_t valve_thread_id;
|
|
||||||
struct k_thread valve_thread_data;
|
|
||||||
|
|
||||||
valve_status_t valve_status_data = {
|
|
||||||
.valve_state = VALVE_STATE_UNKNOWN,
|
|
||||||
.valve_operation = VALVE_OPERATION_IDLE,
|
|
||||||
};
|
|
||||||
|
|
||||||
int valve_start_thread(void)
|
|
||||||
{
|
|
||||||
int rc;
|
|
||||||
|
|
||||||
// Initialize the valve
|
|
||||||
rc = valve_init();
|
|
||||||
if (rc < 0)
|
|
||||||
{
|
|
||||||
LOG_ERR("Failed to initialize valve: %d", rc);
|
|
||||||
return rc;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create the valve thread
|
|
||||||
valve_thread_id = k_thread_create(&valve_thread_data, valve_thread_stack,
|
|
||||||
K_THREAD_STACK_SIZEOF(valve_thread_stack),
|
|
||||||
(k_thread_entry_t)valve_cmd, NULL, NULL, NULL,
|
|
||||||
VALVE_THREAD_PRIORITY, 0, K_NO_WAIT);
|
|
||||||
k_thread_name_set(valve_thread_id, "valve");
|
|
||||||
|
|
||||||
LOG_INF("Valve thread started successfully");
|
|
||||||
while (1)
|
|
||||||
{
|
|
||||||
// Wait for commands from the message queue
|
|
||||||
int cmd;
|
|
||||||
rc = k_msgq_get(&valve_msgq, &cmd, VALVE_STATE_INTERVAL);
|
|
||||||
if (rc == 0)
|
|
||||||
{
|
|
||||||
// Process the command
|
|
||||||
rc = valve_cmd(cmd);
|
|
||||||
if (rc < 0)
|
|
||||||
{
|
|
||||||
LOG_ERR("Failed to process valve command: %d", rc);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
valve_send_status(); // Send current valve status periodically
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
int valve_init(void)
|
|
||||||
{
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
int valve_cmd(int cmd)
|
|
||||||
{
|
|
||||||
switch (cmd)
|
|
||||||
{
|
|
||||||
case VALVE_COMMAND_OPEN:
|
|
||||||
if (valve_status_data.valve_state != VALVE_STATE_OPEN)
|
|
||||||
{
|
|
||||||
valve_status_data.valve_state = VALVE_STATE_OPEN;
|
|
||||||
valve_status_data.valve_operation = VALVE_OPERATION_OPENING;
|
|
||||||
valve_send_status(); // Send updated status before opening
|
|
||||||
valve_send_operation(); // Send updated operation state before opening
|
|
||||||
k_sleep(VALVE_OPENING_TIME); // Simulate opening time
|
|
||||||
valve_status_data.valve_operation = VALVE_OPERATION_IDLE; // Set operation to idle after opening
|
|
||||||
valve_send_status(); // Send updated status after opening
|
|
||||||
valve_send_operation(); // Send updated operation state after opening
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case VALVE_COMMAND_CLOSE:
|
|
||||||
if (valve_status_data.valve_state != VALVE_STATE_CLOSED)
|
|
||||||
{
|
|
||||||
valve_status_data.valve_operation = VALVE_OPERATION_CLOSING;
|
|
||||||
valve_send_operation(); // Send updated operation state before closing
|
|
||||||
k_sleep(VALVE_CLOSING_TIME); // Simulate closing time
|
|
||||||
valve_status_data.valve_state = VALVE_STATE_CLOSED; // Set valve state to closed after closing
|
|
||||||
valve_status_data.valve_operation = VALVE_OPERATION_IDLE; // Set operation to idle after closing
|
|
||||||
valve_send_status(); // Send updated status after closing
|
|
||||||
valve_send_operation(); // Send updated operation state after closing
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case VALVE_COMMAND_STOP:
|
|
||||||
valve_status_data.valve_operation = VALVE_OPERATION_IDLE;
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
LOG_ERR("Unknown valve command: %d", cmd);
|
|
||||||
return -EINVAL; // Invalid command
|
|
||||||
}
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
int valve_send_status(void)
|
|
||||||
{
|
|
||||||
int rc = canbus_send8(CANBUS_REG_VALVE_STATUS, valve_status_data.valve_state);
|
|
||||||
if (rc != 0)
|
|
||||||
{
|
|
||||||
LOG_ERR("Failed to send valve status: %d", rc);
|
|
||||||
return rc;
|
|
||||||
}
|
|
||||||
char *state_str;
|
|
||||||
switch (valve_status_data.valve_state)
|
|
||||||
{
|
|
||||||
case VALVE_STATE_CLOSED:
|
|
||||||
state_str = "CLOSED";
|
|
||||||
break;
|
|
||||||
case VALVE_STATE_OPEN:
|
|
||||||
state_str = "OPEN";
|
|
||||||
break;
|
|
||||||
case VALVE_STATE_ERROR:
|
|
||||||
state_str = "ERROR";
|
|
||||||
break;
|
|
||||||
case VALVE_STATE_UNKNOWN:
|
|
||||||
state_str = "UNKNOWN";
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
state_str = "INVALID";
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
LOG_INF("Valve status sent: %s", state_str);
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
int valve_send_operation(void)
|
|
||||||
{
|
|
||||||
int rc = canbus_send8(CANBUS_REG_VALVE_OPERATION, valve_status_data.valve_operation);
|
|
||||||
if (rc != 0)
|
|
||||||
{
|
|
||||||
LOG_ERR("Failed to send valve operation: %d", rc);
|
|
||||||
return rc;
|
|
||||||
}
|
|
||||||
char *operation_str;
|
|
||||||
switch (valve_status_data.valve_operation)
|
|
||||||
{
|
|
||||||
case VALVE_OPERATION_IDLE:
|
|
||||||
operation_str = "IDLE";
|
|
||||||
break;
|
|
||||||
case VALVE_OPERATION_OPENING:
|
|
||||||
operation_str = "OPENING";
|
|
||||||
break;
|
|
||||||
case VALVE_OPERATION_CLOSING:
|
|
||||||
operation_str = "CLOSING";
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
operation_str = "UNKNOWN";
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
LOG_INF("Valve operation sent: %s", operation_str);
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
@@ -1,27 +0,0 @@
|
|||||||
#ifndef __VALVE_H__
|
|
||||||
#define __VALVE_H__
|
|
||||||
|
|
||||||
#define VALVE_OPENING_TIME K_MSEC(4500) // Time to open the valve
|
|
||||||
#define VALVE_CLOSING_TIME K_MSEC(4500) // Time to close the valve
|
|
||||||
#define VALVE_MAX_OPENING_TIME K_MSEC(5000) // Maximum time to open the valve
|
|
||||||
#define VALVE_MAX_CLOSING_TIME K_MSEC(5000) // Maximum time to close the valve
|
|
||||||
#define VALVE_STATE_INTERVAL K_SECONDS(5 * 60) // Interval to check the valve state
|
|
||||||
#define VALVE_THREAD_STACK_SIZE (512) // Stack size for the valve thread
|
|
||||||
#define VALVE_THREAD_PRIORITY (2) // Priority for the valve thread
|
|
||||||
#define VALVE_MSGQ_SIZE (5) // Size of the message queue for valve operations
|
|
||||||
|
|
||||||
#include <stdint.h>
|
|
||||||
#include "canbus_registers.h"
|
|
||||||
|
|
||||||
typedef struct {
|
|
||||||
uint8_t valve_state;
|
|
||||||
uint8_t valve_operation;
|
|
||||||
} valve_status_t;
|
|
||||||
|
|
||||||
int valve_init(void);
|
|
||||||
int valve_cmd(int cmd);
|
|
||||||
int valve_send_status(void);
|
|
||||||
int valve_send_operation(void);
|
|
||||||
|
|
||||||
|
|
||||||
#endif // __VALVE_H__
|
|
||||||
1
software/lib/valve/CMakeLists.txt
Normal file
1
software/lib/valve/CMakeLists.txt
Normal file
@@ -0,0 +1 @@
|
|||||||
|
zephyr_library_sources(valve.c)
|
||||||
5
software/lib/valve/Kconfig
Normal file
5
software/lib/valve/Kconfig
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
config LIB_VALVE
|
||||||
|
bool "Enable Valve Library"
|
||||||
|
default y
|
||||||
|
help
|
||||||
|
Enable the Valve Library.
|
||||||
62
software/lib/valve/valve.c
Normal file
62
software/lib/valve/valve.c
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
#include <zephyr/kernel.h>
|
||||||
|
#include <zephyr/settings/settings.h>
|
||||||
|
#include <zephyr/logging/log.h>
|
||||||
|
#include <lib/valve.h>
|
||||||
|
|
||||||
|
LOG_MODULE_REGISTER(valve, LOG_LEVEL_INF);
|
||||||
|
|
||||||
|
static enum valve_state current_state = VALVE_STATE_CLOSED;
|
||||||
|
static enum valve_movement current_movement = VALVE_MOVEMENT_IDLE;
|
||||||
|
static uint16_t max_opening_time_s = 60;
|
||||||
|
static uint16_t max_closing_time_s = 60;
|
||||||
|
static struct k_work_delayable valve_work;
|
||||||
|
|
||||||
|
static void valve_work_handler(struct k_work *work)
|
||||||
|
{
|
||||||
|
if (current_movement == VALVE_MOVEMENT_OPENING) {
|
||||||
|
LOG_INF("Virtual valve finished opening");
|
||||||
|
} else if (current_movement == VALVE_MOVEMENT_CLOSING) {
|
||||||
|
current_state = VALVE_STATE_CLOSED;
|
||||||
|
LOG_INF("Virtual valve finished closing");
|
||||||
|
}
|
||||||
|
current_movement = VALVE_MOVEMENT_IDLE;
|
||||||
|
}
|
||||||
|
|
||||||
|
void valve_init(void)
|
||||||
|
{
|
||||||
|
k_work_init_delayable(&valve_work, valve_work_handler);
|
||||||
|
settings_load_one("valve/max_open_time", &max_opening_time_s, sizeof(max_opening_time_s));
|
||||||
|
settings_load_one("valve/max_close_time", &max_closing_time_s, sizeof(max_closing_time_s));
|
||||||
|
}
|
||||||
|
|
||||||
|
void valve_open(void)
|
||||||
|
{
|
||||||
|
if (current_state == VALVE_STATE_CLOSED) {
|
||||||
|
current_state = VALVE_STATE_OPEN;
|
||||||
|
current_movement = VALVE_MOVEMENT_OPENING;
|
||||||
|
k_work_schedule(&valve_work, K_SECONDS(max_opening_time_s));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void valve_close(void)
|
||||||
|
{
|
||||||
|
if (current_state == VALVE_STATE_OPEN) {
|
||||||
|
current_movement = VALVE_MOVEMENT_CLOSING;
|
||||||
|
k_work_schedule(&valve_work, K_SECONDS(max_closing_time_s));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void valve_stop(void)
|
||||||
|
{
|
||||||
|
k_work_cancel_delayable(&valve_work);
|
||||||
|
current_movement = VALVE_MOVEMENT_IDLE;
|
||||||
|
}
|
||||||
|
|
||||||
|
enum valve_state valve_get_state(void) { return current_state; }
|
||||||
|
enum valve_movement valve_get_movement(void) { return current_movement; }
|
||||||
|
uint16_t valve_get_motor_current(void) { return (current_movement != VALVE_MOVEMENT_IDLE) ? 150 : 10; }
|
||||||
|
|
||||||
|
void valve_set_max_open_time(uint16_t seconds) { max_opening_time_s = seconds; settings_save_one("valve/max_open_time", &max_opening_time_s, sizeof(max_opening_time_s)); }
|
||||||
|
void valve_set_max_close_time(uint16_t seconds) { max_closing_time_s = seconds; settings_save_one("valve/max_close_time", &max_closing_time_s, sizeof(max_closing_time_s)); }
|
||||||
|
uint16_t valve_get_max_open_time(void) { return max_opening_time_s; }
|
||||||
|
uint16_t valve_get_max_close_time(void) { return max_closing_time_s; }
|
||||||
@@ -1,383 +0,0 @@
|
|||||||
#include "waterlevel_sensor.h"
|
|
||||||
#include <zephyr/kernel.h>
|
|
||||||
#include <zephyr/logging/log.h>
|
|
||||||
#include <zephyr/modbus/modbus.h>
|
|
||||||
#include <stdlib.h>
|
|
||||||
#include "canbus.h"
|
|
||||||
|
|
||||||
LOG_MODULE_REGISTER(wls, CONFIG_LOG_WATERLEVELSENSOR_LEVEL);
|
|
||||||
|
|
||||||
#define MODBUS_NODE DT_COMPAT_GET_ANY_STATUS_OKAY(zephyr_modbus_serial)
|
|
||||||
|
|
||||||
struct k_thread waterlevel_sensor_thread_data;
|
|
||||||
K_THREAD_STACK_DEFINE(waterlevel_sensor_stack, WATERLEVEL_SENSOR_STACK_SIZE);
|
|
||||||
K_MSGQ_DEFINE(waterlevel_sensor_msgq, sizeof(waterlevel_command_t), WATERLEVEL_MESSAGE_QUEUE_SIZE, 4);
|
|
||||||
|
|
||||||
static int modbus_client_iface;
|
|
||||||
|
|
||||||
volatile static struct
|
|
||||||
{
|
|
||||||
int level; // Water level value
|
|
||||||
int zeropoint; // Minimum value
|
|
||||||
int maxpoint; // Maximum value
|
|
||||||
int factor; // Factor for unit conversion
|
|
||||||
bool factor_set; // Flag to indicate if factor is set
|
|
||||||
} waterlevel_measurement = {
|
|
||||||
.factor_set = false,
|
|
||||||
};
|
|
||||||
|
|
||||||
struct modbus_iface_param client_param = {
|
|
||||||
.mode = MODBUS_MODE_RTU,
|
|
||||||
.rx_timeout = 50000, // Timeout for receiving data in milliseconds
|
|
||||||
.serial = {
|
|
||||||
.baud = 9600,
|
|
||||||
.parity = UART_CFG_PARITY_NONE,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
static int waterlevel_modbus_init() {
|
|
||||||
const char iface_name[] = {DEVICE_DT_NAME(MODBUS_NODE)};
|
|
||||||
modbus_client_iface = modbus_iface_get_by_name(iface_name);
|
|
||||||
if (modbus_client_iface < 0)
|
|
||||||
{
|
|
||||||
LOG_ERR("Failed to get Modbus interface by name: %s", iface_name);
|
|
||||||
return modbus_client_iface;
|
|
||||||
}
|
|
||||||
LOG_DBG("Initializing modbus client interface: %s", iface_name);
|
|
||||||
return modbus_init_client(modbus_client_iface, client_param);
|
|
||||||
}
|
|
||||||
|
|
||||||
static int waterlevel_modbus_read(void) {
|
|
||||||
int rc;
|
|
||||||
union
|
|
||||||
{
|
|
||||||
struct
|
|
||||||
{
|
|
||||||
int16_t unit; // Unit of measurement (e.g., cm, mm)
|
|
||||||
int16_t decimals; // Number of decimal places for the measurement
|
|
||||||
int16_t level; // Water level value
|
|
||||||
int16_t zeropoint; // Zero point for the measurement
|
|
||||||
int16_t maxpoint; // Maximum point for the measurement
|
|
||||||
};
|
|
||||||
int16_t data[5]; // Data array for holding registers
|
|
||||||
} waterlevel_modbus_data;
|
|
||||||
rc = modbus_read_holding_regs(modbus_client_iface, WATERLEVEL_SENSOR_MODBUS_NODE_ID, 0x0002, waterlevel_modbus_data.data, sizeof(waterlevel_modbus_data.data) / sizeof(waterlevel_modbus_data.data[0]));
|
|
||||||
if (rc < 0)
|
|
||||||
{
|
|
||||||
LOG_ERR("Failed to read holding registers, node <%d>, returncode: %d", WATERLEVEL_SENSOR_MODBUS_NODE_ID, rc);
|
|
||||||
return rc;
|
|
||||||
}
|
|
||||||
|
|
||||||
LOG_DBG("Got values. Unit: %d, Decimals: %d, Level: %d, Zero Point: %d, Max Point: %d",
|
|
||||||
waterlevel_modbus_data.unit,
|
|
||||||
waterlevel_modbus_data.decimals,
|
|
||||||
waterlevel_modbus_data.level,
|
|
||||||
waterlevel_modbus_data.zeropoint,
|
|
||||||
waterlevel_modbus_data.maxpoint);
|
|
||||||
|
|
||||||
LOG_HEXDUMP_DBG(waterlevel_modbus_data.data, sizeof(waterlevel_modbus_data.data), "Waterlevel Sensor Holding Registers Data");
|
|
||||||
|
|
||||||
switch (waterlevel_modbus_data.unit)
|
|
||||||
{
|
|
||||||
case 1: // cm
|
|
||||||
waterlevel_measurement.factor = 10;
|
|
||||||
break;
|
|
||||||
case 2: // mm
|
|
||||||
waterlevel_measurement.factor = 1;
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
LOG_ERR("Unknown unit: %d", waterlevel_modbus_data.unit);
|
|
||||||
waterlevel_measurement.factor_set = false;
|
|
||||||
return -EINVAL;
|
|
||||||
}
|
|
||||||
switch (waterlevel_modbus_data.decimals)
|
|
||||||
{
|
|
||||||
case 0: // no decimals
|
|
||||||
waterlevel_measurement.factor /= 1;
|
|
||||||
break;
|
|
||||||
case 1: // one decimal
|
|
||||||
waterlevel_measurement.factor /= 10;
|
|
||||||
break;
|
|
||||||
case 2: // two decimals
|
|
||||||
waterlevel_measurement.factor /= 100;
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
LOG_ERR("Unknown decimals: %d", waterlevel_modbus_data.decimals);
|
|
||||||
waterlevel_measurement.factor_set = false;
|
|
||||||
return -EINVAL;
|
|
||||||
}
|
|
||||||
waterlevel_measurement.factor_set = true;
|
|
||||||
waterlevel_measurement.level = waterlevel_modbus_data.level * waterlevel_measurement.factor;
|
|
||||||
waterlevel_measurement.zeropoint = waterlevel_modbus_data.zeropoint * waterlevel_measurement.factor;
|
|
||||||
waterlevel_measurement.maxpoint = waterlevel_modbus_data.maxpoint * waterlevel_measurement.factor;
|
|
||||||
|
|
||||||
LOG_DBG("Water level: %dmm, zero point: %dmm, maximum point: %dmm",
|
|
||||||
waterlevel_measurement.level,
|
|
||||||
waterlevel_measurement.zeropoint,
|
|
||||||
waterlevel_measurement.maxpoint);
|
|
||||||
LOG_HEXDUMP_DBG(waterlevel_modbus_data.data, sizeof(waterlevel_modbus_data.data), "Waterlevel Sensor Holding Registers Data");
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
static int waterlevel_send_level(void) {
|
|
||||||
if (!waterlevel_measurement.factor_set) {
|
|
||||||
LOG_ERR("Factor not set, cannot send water level");
|
|
||||||
return -EINVAL;
|
|
||||||
}
|
|
||||||
|
|
||||||
LOG_INF("Sending water level: %dmm", waterlevel_measurement.level);
|
|
||||||
canbus_send16(CANBUS_REG_WATERLEVEL_LEVEL, waterlevel_measurement.level);
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
static int waterlevel_send_zero_point(void) {
|
|
||||||
if (!waterlevel_measurement.factor_set) {
|
|
||||||
LOG_ERR("Factor not set, cannot send zero point");
|
|
||||||
return -EINVAL;
|
|
||||||
}
|
|
||||||
|
|
||||||
LOG_INF("Sending water zero point: %dmm", waterlevel_measurement.zeropoint);
|
|
||||||
canbus_send16(CANBUS_REG_WATERLEVEL_ZERO_POINT, waterlevel_measurement.zeropoint);
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
static int waterlevel_send_max_point(void) {
|
|
||||||
if (!waterlevel_measurement.factor_set) {
|
|
||||||
LOG_ERR("Factor not set, cannot send maximum point");
|
|
||||||
return -EINVAL;
|
|
||||||
}
|
|
||||||
|
|
||||||
LOG_INF("Sending water maximum point: %dmm", waterlevel_measurement.maxpoint);
|
|
||||||
canbus_send16(CANBUS_REG_WATERLEVEL_MAX_POINT, waterlevel_measurement.maxpoint);
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
static int waterlevel_set_zero_point(int zeropoint) {
|
|
||||||
if (!waterlevel_measurement.factor_set) {
|
|
||||||
LOG_ERR("Factor not set, cannot set zero point");
|
|
||||||
return -EINVAL;
|
|
||||||
}
|
|
||||||
|
|
||||||
int16_t zeropoint_modbus = zeropoint / waterlevel_measurement.factor;
|
|
||||||
int rc = modbus_write_holding_regs(modbus_client_iface, WATERLEVEL_SENSOR_MODBUS_NODE_ID, 0x0005, &zeropoint_modbus, 1);
|
|
||||||
if (rc < 0) {
|
|
||||||
LOG_ERR("Failed to write zero point: %d", rc);
|
|
||||||
return rc;
|
|
||||||
}
|
|
||||||
|
|
||||||
waterlevel_measurement.zeropoint = zeropoint; // Update the local measurement structure
|
|
||||||
LOG_INF("Zero point set to: %dmm", waterlevel_measurement.zeropoint);
|
|
||||||
rc = waterlevel_send_zero_point();
|
|
||||||
if (rc < 0) {
|
|
||||||
LOG_ERR("Failed to send zero point: %d", rc);
|
|
||||||
return rc;
|
|
||||||
}
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
static int waterlevel_set_max_point(int maxpoint) {
|
|
||||||
if (!waterlevel_measurement.factor_set) {
|
|
||||||
LOG_ERR("Factor not set, cannot set maximum point");
|
|
||||||
return -EINVAL;
|
|
||||||
}
|
|
||||||
|
|
||||||
int16_t maxpoint_modbus = maxpoint / waterlevel_measurement.factor;
|
|
||||||
int rc = modbus_write_holding_regs(modbus_client_iface, WATERLEVEL_SENSOR_MODBUS_NODE_ID, 0x0006, &maxpoint_modbus, 1);
|
|
||||||
if (rc < 0) {
|
|
||||||
LOG_ERR("Failed to write maximum point: %d", rc);
|
|
||||||
return rc;
|
|
||||||
}
|
|
||||||
|
|
||||||
waterlevel_measurement.maxpoint = maxpoint; // Update the local measurement structure
|
|
||||||
LOG_INF("Maximum point set to: %dmm", waterlevel_measurement.maxpoint);
|
|
||||||
rc = waterlevel_send_max_point();
|
|
||||||
if (rc < 0) {
|
|
||||||
LOG_ERR("Failed to send maximum point: %d", rc);
|
|
||||||
return rc;
|
|
||||||
}
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
void waterlevel_sensor_thread(void *arg1, void *arg2, void *arg3)
|
|
||||||
{
|
|
||||||
ARG_UNUSED(arg1);
|
|
||||||
ARG_UNUSED(arg2);
|
|
||||||
ARG_UNUSED(arg3);
|
|
||||||
|
|
||||||
// Initialize the Modbus client
|
|
||||||
int rc = waterlevel_modbus_init();
|
|
||||||
if (rc < 0)
|
|
||||||
{
|
|
||||||
LOG_ERR("Failed to initialize Modbus client: %d", rc);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
rc = waterlevel_modbus_read();
|
|
||||||
if (rc < 0) {
|
|
||||||
LOG_ERR("Failed to read initial water level: %d", rc);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
waterlevel_send_level();
|
|
||||||
waterlevel_send_zero_point();
|
|
||||||
waterlevel_send_max_point();
|
|
||||||
|
|
||||||
// Initialize the last transmission time and level
|
|
||||||
// Use k_uptime_get_32() to get the current uptime in milliseconds
|
|
||||||
// and store the initial water level measurement.
|
|
||||||
// This will be used to determine when to send updates.
|
|
||||||
|
|
||||||
uint32_t last_transmission_time_ms = k_uptime_get_32();
|
|
||||||
int32_t last_transmission_level = waterlevel_measurement.level;
|
|
||||||
|
|
||||||
while (1)
|
|
||||||
{
|
|
||||||
uint32_t current_time_ms = k_uptime_get_32();
|
|
||||||
uint32_t delta_time = current_time_ms-last_transmission_time_ms;
|
|
||||||
waterlevel_command_t command;
|
|
||||||
|
|
||||||
rc = waterlevel_modbus_read();
|
|
||||||
if (rc < 0)
|
|
||||||
{
|
|
||||||
LOG_ERR("Failed to read water level: %d", rc);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (delta_time >= WATERLEVEL_SENSOR_MAX_UPDATE_INTERVAL_MS ||
|
|
||||||
abs(waterlevel_measurement.level - last_transmission_level) >= WATERLEVEL_SENSOR_MIN_DELTA) {
|
|
||||||
rc = waterlevel_send_level();
|
|
||||||
if (rc < 0) {
|
|
||||||
LOG_ERR("Failed to send water level: %d", rc);
|
|
||||||
} else {
|
|
||||||
last_transmission_time_ms = current_time_ms;
|
|
||||||
last_transmission_level = waterlevel_measurement.level;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
while (k_msgq_get(&waterlevel_sensor_msgq, &command, K_NO_WAIT) == 0)
|
|
||||||
{
|
|
||||||
switch(command.cmd)
|
|
||||||
{
|
|
||||||
case WATERLEVEL_CMD_SET:
|
|
||||||
switch (command.reg)
|
|
||||||
{
|
|
||||||
case CANBUS_REG_WATERLEVEL_ZERO_POINT: // Set zero point
|
|
||||||
rc = waterlevel_set_zero_point(command.data);
|
|
||||||
if (rc < 0) {
|
|
||||||
LOG_ERR("Failed to set zero point: %d", rc);
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case CANBUS_REG_WATERLEVEL_MAX_POINT: // Set maximum point
|
|
||||||
rc = waterlevel_set_max_point(command.data);
|
|
||||||
if (rc < 0) {
|
|
||||||
LOG_ERR("Failed to set maximum point: %d", rc);
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
LOG_ERR("Unknown register for SET command: 0x%02X", command.reg);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case WATERLEVEL_CMD_GET:
|
|
||||||
switch (command.reg)
|
|
||||||
{
|
|
||||||
case CANBUS_REG_WATERLEVEL_LEVEL: // Get water level
|
|
||||||
waterlevel_send_level();
|
|
||||||
break;
|
|
||||||
case CANBUS_REG_WATERLEVEL_ZERO_POINT: // Get zero point
|
|
||||||
waterlevel_send_zero_point();
|
|
||||||
break;
|
|
||||||
case CANBUS_REG_WATERLEVEL_MAX_POINT: // Get maximum point
|
|
||||||
waterlevel_send_max_point();
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
LOG_ERR("Unknown register for GET command: 0x%02X", command.reg);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
LOG_ERR("Unknown command type: %d", command.cmd);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
int waterlevel_sensor_start_thread(void)
|
|
||||||
{
|
|
||||||
k_tid_t waterlevel_sensor_thread_id;
|
|
||||||
|
|
||||||
// Start the thread
|
|
||||||
waterlevel_sensor_thread_id = k_thread_create(&waterlevel_sensor_thread_data, waterlevel_sensor_stack,
|
|
||||||
K_THREAD_STACK_SIZEOF(waterlevel_sensor_stack), waterlevel_sensor_thread,
|
|
||||||
NULL, NULL, NULL,
|
|
||||||
WATERLEVEL_SENSOR_THREAD_PRIORITY, 0, K_NO_WAIT);
|
|
||||||
|
|
||||||
if (waterlevel_sensor_thread_id == NULL)
|
|
||||||
{
|
|
||||||
LOG_ERR("Failed to create water level sensor thread");
|
|
||||||
return -ENOMEM;
|
|
||||||
}
|
|
||||||
k_thread_name_set(waterlevel_sensor_thread_id, "waterlevel_sensor");
|
|
||||||
|
|
||||||
LOG_INF("Water level sensor thread started successfully");
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
#ifdef CONFIG_SHELL
|
|
||||||
#include <zephyr/shell/shell.h>
|
|
||||||
|
|
||||||
void waterlevel_set_zero_point_shell(const struct shell *shell, size_t argc, char **argv) {
|
|
||||||
if (argc != 2) {
|
|
||||||
shell_error(shell, "Usage: waterlevel_sensor set_zero_point <zeropoint>");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
int zeropoint = atoi(argv[1]);
|
|
||||||
int rc = waterlevel_set_zero_point(zeropoint);
|
|
||||||
if (rc < 0) {
|
|
||||||
shell_error(shell, "Failed to set zero point: %d", rc);
|
|
||||||
} else {
|
|
||||||
shell_print(shell, "Zero point set to: %dmm", zeropoint);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void waterlevel_set_max_point_shell(const struct shell *shell, size_t argc, char **argv) {
|
|
||||||
if (argc != 2) {
|
|
||||||
shell_error(shell, "Usage: waterlevel_sensor set_max_point <maxpoint>");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
int maxpoint = atoi(argv[1]);
|
|
||||||
int rc = waterlevel_set_max_point(maxpoint);
|
|
||||||
if (rc < 0) {
|
|
||||||
shell_error(shell, "Failed to set maximum point: %d", rc);
|
|
||||||
} else {
|
|
||||||
shell_print(shell, "Maximum point set to: %dmm", maxpoint);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void waterlevel_sensor_print_shell(const struct shell *shell, size_t argc, char **argv) {
|
|
||||||
ARG_UNUSED(argc);
|
|
||||||
ARG_UNUSED(argv);
|
|
||||||
|
|
||||||
waterlevel_modbus_read();
|
|
||||||
if (!waterlevel_measurement.factor_set) {
|
|
||||||
shell_error(shell, "Factor not set, cannot print water level");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
shell_print(shell, "Current water level: %4dmm", waterlevel_measurement.level);
|
|
||||||
shell_print(shell, "Zero point: %4dmm", waterlevel_measurement.zeropoint);
|
|
||||||
shell_print(shell, "Maximum point: %4dmm", waterlevel_measurement.maxpoint);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Define the shell commands for the water level sensor
|
|
||||||
SHELL_STATIC_SUBCMD_SET_CREATE(
|
|
||||||
waterlevel_sensor_cmds,
|
|
||||||
SHELL_CMD(print, NULL, "Print the current water level, zero point, and maximum point", waterlevel_sensor_print_shell),
|
|
||||||
SHELL_CMD(setzero, NULL, "Set the zero point for the water level sensor", waterlevel_set_zero_point_shell),
|
|
||||||
SHELL_CMD(setmax, NULL, "Set the maximum point for the water level sensor", waterlevel_set_max_point_shell),
|
|
||||||
SHELL_SUBCMD_SET_END);
|
|
||||||
|
|
||||||
SHELL_CMD_REGISTER(wls, &waterlevel_sensor_cmds, "Water level sensor commands", NULL);
|
|
||||||
#endif // CONFIG_SHELL
|
|
||||||
@@ -1,27 +0,0 @@
|
|||||||
#ifndef __WATERLEVEL_SENSOR_H__
|
|
||||||
#define __WATERLEVEL_SENSOR_H__
|
|
||||||
|
|
||||||
#define WATERLEVEL_SENSOR_STACK_SIZE (512)
|
|
||||||
#define WATERLEVEL_SENSOR_THREAD_PRIORITY (2)
|
|
||||||
#define WATERLEVEL_MESSAGE_QUEUE_SIZE (5) // Size of the message queue for water level sensor thread
|
|
||||||
#define WATERLEVEL_SENSOR_READ_INTERVAL_MS (5000) // Interval for reading the water level sensor in milliseconds
|
|
||||||
#define WATERLEVEL_SENSOR_MIN_DELTA (2) // Minimum change in water level to trigger an update
|
|
||||||
#define WATERLEVEL_SENSOR_MAX_UPDATE_INTERVAL_MS (600000) // Maximum interval for updating the water level in milliseconds
|
|
||||||
|
|
||||||
#define WATERLEVEL_SENSOR_MODBUS_NODE_ID (0x01) // Modbus node ID for the water level sensor
|
|
||||||
#define WATERLEVEL_SENSOR_MODBUS_BAUD_RATE (9600) // Baud rate for Modbus communication
|
|
||||||
|
|
||||||
#include <inttypes.h>
|
|
||||||
|
|
||||||
int waterlevel_sensor_start_thread(void);
|
|
||||||
|
|
||||||
typedef struct {
|
|
||||||
uint8_t reg;
|
|
||||||
enum {
|
|
||||||
WATERLEVEL_CMD_SET,
|
|
||||||
WATERLEVEL_CMD_GET,
|
|
||||||
} cmd;
|
|
||||||
int16_t data; // Data to be set
|
|
||||||
} waterlevel_command_t;
|
|
||||||
|
|
||||||
#endif // __WATERLEVEL_SENSOR_H__
|
|
||||||
@@ -1,20 +0,0 @@
|
|||||||
CONFIG_HAS_MODBUS_WATERLEVEL_SENSOR=y
|
|
||||||
CONFIG_HAS_VALVE=y
|
|
||||||
|
|
||||||
CONFIG_LOG=y
|
|
||||||
CONFIG_LOG_DEFAULT_LEVEL=3
|
|
||||||
# CONFIG_LOG_CAN_LEVEL=4
|
|
||||||
# CONFIG_LOG_WATERLEVELSENSOR_LEVEL=4
|
|
||||||
# CONFIG_LOG_VALVE_LEVEL=4
|
|
||||||
CONFIG_CBPRINTF_FP_SUPPORT=y
|
|
||||||
CONFIG_UART_CONSOLE=y # Console on USART1
|
|
||||||
|
|
||||||
# CAN loopback mode for testing
|
|
||||||
#CONFIG_LOOPBACK_MODE=y
|
|
||||||
CONFIG_SHELL=y
|
|
||||||
CONFIG_CAN_SHELL=y
|
|
||||||
CONFIG_GPIO_SHELL=y
|
|
||||||
CONFIG_REBOOT=y
|
|
||||||
|
|
||||||
CONFIG_ADC=y
|
|
||||||
CONFIG_ADC_STM32=y
|
|
||||||
@@ -1,53 +0,0 @@
|
|||||||
#include <zephyr/logging/log.h>
|
|
||||||
#include <zephyr/kernel.h>
|
|
||||||
#include <zephyr/sys/byteorder.h>
|
|
||||||
#include <zephyr/shell/shell.h>
|
|
||||||
#include "canbus.h"
|
|
||||||
#include "canbus_registers.h"
|
|
||||||
|
|
||||||
#ifdef CONFIG_HAS_MODBUS_WATERLEVEL_SENSOR
|
|
||||||
// Include the water level sensor header file when the feature is enabled
|
|
||||||
#include "waterlevel_sensor.h"
|
|
||||||
#endif // CONFIG_HAS_MODBUS_WATERLEVEL_SENSOR
|
|
||||||
|
|
||||||
#ifdef CONFIG_HAS_VALVE
|
|
||||||
#include "valve.h"
|
|
||||||
#endif // CONFIG_HAS_VALVE
|
|
||||||
|
|
||||||
LOG_MODULE_REGISTER(main, CONFIG_LOG_DEFAULT_LEVEL);
|
|
||||||
|
|
||||||
int main(void)
|
|
||||||
{
|
|
||||||
LOG_INF("Starting main application...");
|
|
||||||
canbus_init();
|
|
||||||
k_sleep(K_MSEC(3000)); // Allow some time for CAN initialization
|
|
||||||
#ifdef CONFIG_HAS_MODBUS_WATERLEVEL_SENSOR
|
|
||||||
int rc = waterlevel_sensor_start_thread();
|
|
||||||
if (rc < 0)
|
|
||||||
{
|
|
||||||
LOG_ERR("Failed to start water level sensor thread: %d", rc);
|
|
||||||
return rc;
|
|
||||||
}
|
|
||||||
#endif // CONFIG_HAS_MODBUS_WATERLEVEL_SENSOR
|
|
||||||
|
|
||||||
valve_cmd(VALVE_COMMAND_CLOSE); // Ensure the valve is closed at startup
|
|
||||||
|
|
||||||
LOG_INF("Main application started successfully.");
|
|
||||||
return 0; // Return 0 on success
|
|
||||||
}
|
|
||||||
|
|
||||||
#ifdef CONFIG_SHELL
|
|
||||||
#include <zephyr/shell/shell.h>
|
|
||||||
#include <zephyr/sys/reboot.h>
|
|
||||||
|
|
||||||
static int reboot_shell_cmd(const struct shell *shell, size_t argc, char **argv)
|
|
||||||
{
|
|
||||||
ARG_UNUSED(argc);
|
|
||||||
ARG_UNUSED(argv);
|
|
||||||
shell_print(shell, "Rebooting the node in 1 second...");
|
|
||||||
k_sleep(K_SECONDS(1));
|
|
||||||
sys_reboot(SYS_REBOOT_COLD);
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
SHELL_CMD_REGISTER(reboot, NULL, "Reboot the node", reboot_shell_cmd);
|
|
||||||
#endif // CONFIG_SHELL
|
|
||||||
@@ -1,86 +0,0 @@
|
|||||||
#include <zephyr/kernel.h>
|
|
||||||
#include <zephyr/drivers/adc.h>
|
|
||||||
#include <zephyr/logging/log.h>
|
|
||||||
#include <zephyr/device.h>
|
|
||||||
#include <zephyr/devicetree.h>
|
|
||||||
#include <stdint.h>
|
|
||||||
|
|
||||||
LOG_MODULE_REGISTER(main2, LOG_LEVEL_DBG);
|
|
||||||
|
|
||||||
#define MOTOR_ADC_NODE DT_ALIAS(adc_motor_current)
|
|
||||||
#define VREF_ADC_NODE DT_ALIAS(adc_vref)
|
|
||||||
|
|
||||||
static const struct device * const adc_dev = DEVICE_DT_GET(DT_PARENT(MOTOR_ADC_NODE));
|
|
||||||
static const uint8_t motor_channel_id = DT_REG_ADDR(MOTOR_ADC_NODE);
|
|
||||||
static const uint8_t vref_channel_id = DT_REG_ADDR(VREF_ADC_NODE);
|
|
||||||
|
|
||||||
int main(void)
|
|
||||||
{
|
|
||||||
int err;
|
|
||||||
int16_t adc_raw_value;
|
|
||||||
|
|
||||||
LOG_INF("Starting ADC test with direct register setup...");
|
|
||||||
|
|
||||||
if (!device_is_ready(adc_dev)) {
|
|
||||||
LOG_ERR("ADC device is not ready");
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
LOG_INF("Manually setting up ADC registers...");
|
|
||||||
|
|
||||||
uint32_t adc_base = DT_REG_ADDR(DT_NODELABEL(adc1));
|
|
||||||
|
|
||||||
volatile uint32_t *ADC_CR2 = (uint32_t *)(adc_base + 0x08);
|
|
||||||
volatile uint32_t *ADC_SMPR1 = (uint32_t *)(adc_base + 0x0C);
|
|
||||||
volatile uint32_t *ADC_SMPR2 = (uint32_t *)(adc_base + 0x10);
|
|
||||||
|
|
||||||
// Schritt 1: Internen VREFINT-Kanal einschalten
|
|
||||||
const uint32_t ADC_CR2_TSVREFE_BIT = 23;
|
|
||||||
*ADC_CR2 |= (1 << ADC_CR2_TSVREFE_BIT);
|
|
||||||
LOG_INF("VREFINT channel enabled via CR2 register.");
|
|
||||||
|
|
||||||
// Schritt 2: Lange Abtastzeiten für Stabilität setzen
|
|
||||||
*ADC_SMPR2 |= (0b111 << (3 * 9));
|
|
||||||
*ADC_SMPR1 |= (0b111 << (3 * (17 - 10)));
|
|
||||||
LOG_INF("Acquisition times set directly in SMPR registers.");
|
|
||||||
|
|
||||||
|
|
||||||
k_busy_wait(10);
|
|
||||||
|
|
||||||
|
|
||||||
while (1) {
|
|
||||||
int32_t motor_raw = 0;
|
|
||||||
int32_t vref_raw = 0;
|
|
||||||
|
|
||||||
struct adc_sequence sequence = {
|
|
||||||
.buffer = &adc_raw_value,
|
|
||||||
.buffer_size = sizeof(adc_raw_value),
|
|
||||||
.resolution = 12,
|
|
||||||
};
|
|
||||||
|
|
||||||
// Motor-Kanal lesen
|
|
||||||
sequence.channels = BIT(motor_channel_id);
|
|
||||||
if (adc_read(adc_dev, &sequence) == 0) {
|
|
||||||
motor_raw = adc_raw_value;
|
|
||||||
}
|
|
||||||
|
|
||||||
// VREF-Kanal lesen
|
|
||||||
sequence.channels = BIT(vref_channel_id);
|
|
||||||
if (adc_read(adc_dev, &sequence) == 0) {
|
|
||||||
vref_raw = adc_raw_value;
|
|
||||||
}
|
|
||||||
|
|
||||||
// VDD-Berechnung mit dem generischen, aber für Sie gut funktionierenden 1200mV-Wert
|
|
||||||
int32_t vdd_mv = (vref_raw > 0) ? (1200 * 4095 / vref_raw) : 0;
|
|
||||||
int32_t motor_mv = 0;
|
|
||||||
|
|
||||||
if (motor_raw > 0 && vdd_mv > 0) {
|
|
||||||
motor_mv = motor_raw;
|
|
||||||
err = adc_raw_to_millivolts(vdd_mv, ADC_GAIN_1, 12, &motor_mv);
|
|
||||||
}
|
|
||||||
|
|
||||||
LOG_INF("Motor Raw: %4d, Motor mV: %4d | VDD: %4d mV", motor_raw, motor_mv, vdd_mv);
|
|
||||||
k_sleep(K_MSEC(2000));
|
|
||||||
}
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
103
software/tools/modbus_tool/README.de.md
Normal file
103
software/tools/modbus_tool/README.de.md
Normal file
@@ -0,0 +1,103 @@
|
|||||||
|
<img src="../../../docs/img/logo.png" alt="Logo" width="100"/>
|
||||||
|
|
||||||
|
# Modbus Tool für Bewässerungssystem-Knoten
|
||||||
|
|
||||||
|
Dieses Python-Skript bietet eine interaktive Kommandozeilen-Benutzeroberfläche (TUI) zur Steuerung und Überwachung eines Ventil-Knotens des Bewässerungssystems über Modbus RTU.
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
- **Interaktive Benutzeroberfläche:** Eine benutzerfreundliche, auf `curses` basierende Oberfläche, die eine einfache Bedienung ermöglicht.
|
||||||
|
- **Live-Statusanzeige:** Zeigt tabellarisch und in Echtzeit alle wichtigen Register des Slaves an:
|
||||||
|
- Ventilstatus (Zustand, Bewegung, Motorstrom)
|
||||||
|
- Zustand der digitalen Ein- und Ausgänge
|
||||||
|
- "Clear-on-Read" Taster-Events
|
||||||
|
- Systemkonfiguration (Öffnungs-/Schließzeiten, Watchdog-Timeout)
|
||||||
|
- Gerätestatus (Firmware-Version, Uptime)
|
||||||
|
- **Volle Kontrolle:** Ermöglicht das Senden von Befehlen zum Öffnen, Schließen und Stoppen des Ventils sowie zum Umschalten der digitalen Ausgänge.
|
||||||
|
- **Konfiguration zur Laufzeit:** Die maximalen Öffnungs-/Schließzeiten und der Watchdog-Timeout können direkt in der Oberfläche geändert werden.
|
||||||
|
- **Simulierter Firmware-Upload:** Implementiert den vollständigen, in der Dokumentation beschriebenen Firmware-Update-Prozess. Das Tool sendet eine `firmware.bin`-Datei in Chunks an den Slave und folgt dem CRC-Verifizierungs-Protokoll.
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
### Voraussetzungen
|
||||||
|
|
||||||
|
- Python 3.x
|
||||||
|
- `pip` (Python Paket-Installer)
|
||||||
|
|
||||||
|
### Installation der Abhängigkeiten
|
||||||
|
|
||||||
|
Die benötigten Python-Pakete sind in der Datei `requirements.txt` aufgeführt. Sie können auf zwei Arten installiert werden: global oder in einer virtuellen Umgebung (empfohlen).
|
||||||
|
|
||||||
|
#### Option 1: Installation mit virtueller Umgebung (empfohlen)
|
||||||
|
|
||||||
|
Eine virtuelle Umgebung isoliert die Projekt-Abhängigkeiten von Ihrem globalen Python-System, was Konflikte vermeidet.
|
||||||
|
|
||||||
|
1. **Virtuelle Umgebung erstellen:**
|
||||||
|
Führen Sie im Verzeichnis `software/tools/modbus_tool` den folgenden Befehl aus, um eine Umgebung im Ordner `.venv` zu erstellen:
|
||||||
|
```bash
|
||||||
|
python3 -m venv .venv
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Umgebung aktivieren:**
|
||||||
|
- **Linux / macOS:**
|
||||||
|
```bash
|
||||||
|
source .venv/bin/activate
|
||||||
|
```
|
||||||
|
Ihre Shell-Anzeige sollte sich ändern und `(.venv)` am Anfang zeigen.
|
||||||
|
- **Windows (cmd.exe):**
|
||||||
|
```bash
|
||||||
|
.venv\Scripts\activate.bat
|
||||||
|
```
|
||||||
|
- **Windows (PowerShell):**
|
||||||
|
```powershell
|
||||||
|
.venv\Scripts\Activate.ps1
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **Abhängigkeiten installieren:**
|
||||||
|
Wenn die Umgebung aktiv ist, installieren Sie die Pakete:
|
||||||
|
```bash
|
||||||
|
pip install -r requirements.txt
|
||||||
|
```
|
||||||
|
|
||||||
|
4. **Umgebung deaktivieren:**
|
||||||
|
Wenn Sie fertig sind, können Sie die Umgebung mit folgendem Befehl wieder verlassen:
|
||||||
|
```bash
|
||||||
|
deactivate
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Option 2: Globale Installation (nicht empfohlen)
|
||||||
|
|
||||||
|
Wenn Sie keine virtuelle Umgebung verwenden möchten, können Sie die Pakete direkt in Ihrem globalen Python-System installieren.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pip install -r requirements.txt
|
||||||
|
```
|
||||||
|
|
||||||
|
## Verwendung
|
||||||
|
|
||||||
|
Stellen Sie sicher, dass das Skript ausführbar ist:
|
||||||
|
```bash
|
||||||
|
chmod +x modbus_tool.py
|
||||||
|
```
|
||||||
|
|
||||||
|
Starten Sie das Tool, indem Sie den seriellen Port als Argument übergeben:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./modbus_tool.py /dev/ttyACM0
|
||||||
|
```
|
||||||
|
Ersetzen Sie `/dev/ttyACM0` durch den korrekten Port Ihres Geräts.
|
||||||
|
|
||||||
|
### Kommandozeilen-Argumente
|
||||||
|
|
||||||
|
- `port`: (Erforderlich) Der serielle Port (z.B. `/dev/ttyACM0` oder `COM3`).
|
||||||
|
- `--baud`: Die Baudrate (Standard: `19200`).
|
||||||
|
- `--slave-id`: Die Modbus Slave ID des Geräts (Standard: `1`).
|
||||||
|
- `--interval`: Das Abfrageintervall für den Status in Sekunden (Standard: `1.0`).
|
||||||
|
|
||||||
|
### Bedienung der Oberfläche
|
||||||
|
|
||||||
|
- **Navigation:** Verwenden Sie die **Pfeiltasten (↑/↓)**, um zwischen den Menüpunkten zu navigieren.
|
||||||
|
- **Auswählen:** Drücken Sie **Enter**, um den ausgewählten Befehl auszuführen.
|
||||||
|
- **Werte eingeben:** Bei Aktionen wie "Set Watchdog" werden Sie zur Eingabe eines Wertes aufgefordert. Geben Sie den Wert ein und bestätigen Sie mit **Enter**.
|
||||||
|
- **Firmware Update:** Diese Funktion startet den Upload der Datei `firmware.bin` aus dem aktuellen Verzeichnis. Während des Updates wird eine Fortschrittsanzeige dargestellt.
|
||||||
|
- **Beenden:** Wählen Sie den Menüpunkt **"Exit"** und drücken Sie **Enter**.
|
||||||
BIN
software/tools/modbus_tool/firmware.bin
Normal file
BIN
software/tools/modbus_tool/firmware.bin
Normal file
Binary file not shown.
330
software/tools/modbus_tool/modbus_tool.py
Executable file
330
software/tools/modbus_tool/modbus_tool.py
Executable file
@@ -0,0 +1,330 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
import argparse
|
||||||
|
import threading
|
||||||
|
import time
|
||||||
|
import sys
|
||||||
|
import curses
|
||||||
|
import os
|
||||||
|
from pymodbus.client import ModbusSerialClient
|
||||||
|
from pymodbus.exceptions import ModbusException
|
||||||
|
|
||||||
|
# --- Register Definitions ---
|
||||||
|
# (omitted for brevity, no changes here)
|
||||||
|
REG_INPUT_VALVE_STATE_MOVEMENT = 0x0000
|
||||||
|
REG_INPUT_MOTOR_CURRENT_MA = 0x0001
|
||||||
|
REG_INPUT_DIGITAL_INPUTS_STATE = 0x0020
|
||||||
|
REG_INPUT_BUTTON_EVENTS = 0x0021
|
||||||
|
REG_INPUT_FIRMWARE_VERSION_MAJOR_MINOR = 0x00F0
|
||||||
|
REG_INPUT_FIRMWARE_VERSION_PATCH = 0x00F1
|
||||||
|
REG_INPUT_DEVICE_STATUS = 0x00F2
|
||||||
|
REG_INPUT_UPTIME_SECONDS_LOW = 0x00F3
|
||||||
|
REG_INPUT_UPTIME_SECONDS_HIGH = 0x00F4
|
||||||
|
REG_INPUT_FWU_LAST_CHUNK_CRC = 0x0100
|
||||||
|
REG_HOLDING_VALVE_COMMAND = 0x0000
|
||||||
|
REG_HOLDING_MAX_OPENING_TIME_S = 0x0001
|
||||||
|
REG_HOLDING_MAX_CLOSING_TIME_S = 0x0002
|
||||||
|
REG_HOLDING_DIGITAL_OUTPUTS_STATE = 0x0010
|
||||||
|
REG_HOLDING_WATCHDOG_TIMEOUT_S = 0x00F0
|
||||||
|
REG_HOLDING_DEVICE_RESET = 0x00F1
|
||||||
|
REG_HOLDING_FWU_COMMAND = 0x0100
|
||||||
|
REG_HOLDING_FWU_CHUNK_OFFSET_LOW = 0x0101
|
||||||
|
REG_HOLDING_FWU_CHUNK_OFFSET_HIGH = 0x0102
|
||||||
|
REG_HOLDING_FWU_CHUNK_SIZE = 0x0103
|
||||||
|
REG_HOLDING_FWU_DATA_BUFFER = 0x0180
|
||||||
|
|
||||||
|
|
||||||
|
# --- Global State ---
|
||||||
|
stop_event = threading.Event()
|
||||||
|
client = None
|
||||||
|
status_data = {}
|
||||||
|
status_lock = threading.Lock()
|
||||||
|
update_status = {"running": False, "message": "", "progress": 0.0}
|
||||||
|
update_lock = threading.Lock()
|
||||||
|
|
||||||
|
def format_uptime(seconds):
|
||||||
|
if not isinstance(seconds, (int, float)) or seconds < 0: return "N/A"
|
||||||
|
if seconds == 0: return "0s"
|
||||||
|
days, rem = divmod(seconds, 86400); hours, rem = divmod(rem, 3600); minutes, secs = divmod(rem, 60)
|
||||||
|
parts = []
|
||||||
|
if days > 0: parts.append(f"{int(days)}d")
|
||||||
|
if hours > 0: parts.append(f"{int(hours)}h")
|
||||||
|
if minutes > 0: parts.append(f"{int(minutes)}m")
|
||||||
|
if secs > 0 or not parts: parts.append(f"{int(secs)}s")
|
||||||
|
return " ".join(parts)
|
||||||
|
|
||||||
|
def poll_status(slave_id, interval):
|
||||||
|
global status_data
|
||||||
|
reconnect_attempts = 0
|
||||||
|
max_reconnect_attempts = 5
|
||||||
|
reconnect_delay = 1 # seconds
|
||||||
|
|
||||||
|
while not stop_event.is_set():
|
||||||
|
if update_status["running"]:
|
||||||
|
time.sleep(interval)
|
||||||
|
continue
|
||||||
|
|
||||||
|
new_data = {}
|
||||||
|
try:
|
||||||
|
if not client.is_socket_open():
|
||||||
|
reconnect_attempts += 1
|
||||||
|
if reconnect_attempts >= max_reconnect_attempts:
|
||||||
|
new_data["error"] = f"Failed to reconnect after {max_reconnect_attempts} attempts. Exiting."
|
||||||
|
stop_event.set()
|
||||||
|
break
|
||||||
|
|
||||||
|
# Attempt to connect
|
||||||
|
if client.connect():
|
||||||
|
reconnect_attempts = 0
|
||||||
|
new_data["error"] = None # Clear error on successful reconnect
|
||||||
|
else:
|
||||||
|
new_data["error"] = f"Connection lost. Attempting to reconnect ({reconnect_attempts}/{max_reconnect_attempts})..."
|
||||||
|
time.sleep(reconnect_delay)
|
||||||
|
continue
|
||||||
|
|
||||||
|
# If connected, try to read data
|
||||||
|
ir_valve = client.read_input_registers(REG_INPUT_VALVE_STATE_MOVEMENT, count=2, slave=slave_id)
|
||||||
|
ir_dig = client.read_input_registers(REG_INPUT_DIGITAL_INPUTS_STATE, count=2, slave=slave_id)
|
||||||
|
ir_sys = client.read_input_registers(REG_INPUT_FIRMWARE_VERSION_MAJOR_MINOR, count=5, slave=slave_id)
|
||||||
|
hr_valve = client.read_holding_registers(REG_HOLDING_MAX_OPENING_TIME_S, count=2, slave=slave_id)
|
||||||
|
hr_dig = client.read_holding_registers(REG_HOLDING_DIGITAL_OUTPUTS_STATE, count=1, slave=slave_id)
|
||||||
|
hr_sys = client.read_holding_registers(REG_HOLDING_WATCHDOG_TIMEOUT_S, count=1, slave=slave_id)
|
||||||
|
|
||||||
|
for res in [ir_valve, ir_dig, ir_sys, hr_valve, hr_dig, hr_sys]:
|
||||||
|
if res.isError():
|
||||||
|
raise ModbusException(str(res))
|
||||||
|
|
||||||
|
valve_state_raw = ir_valve.registers[0]
|
||||||
|
movement_map = {0: "Idle", 1: "Opening", 2: "Closing", 3: "Error"}
|
||||||
|
state_map = {0: "Closed", 1: "Open"}
|
||||||
|
new_data["movement"] = movement_map.get(valve_state_raw >> 8, 'Unknown')
|
||||||
|
new_data["state"] = state_map.get(valve_state_raw & 0xFF, 'Unknown')
|
||||||
|
new_data["motor_current"] = f"{ir_valve.registers[1]} mA"
|
||||||
|
new_data["open_time"] = f"{hr_valve.registers[0]}s"
|
||||||
|
new_data["close_time"] = f"{hr_valve.registers[1]}s"
|
||||||
|
new_data["digital_inputs"] = f"0x{ir_dig.registers[0]:04X}"
|
||||||
|
new_data["button_events"] = f"0x{ir_dig.registers[1]:04X}"
|
||||||
|
new_data["digital_outputs"] = f"0x{hr_dig.registers[0]:04X}"
|
||||||
|
|
||||||
|
fw_major = ir_sys.registers[0] >> 8
|
||||||
|
fw_minor = ir_sys.registers[0] & 0xFF
|
||||||
|
fw_patch = ir_sys.registers[1]
|
||||||
|
uptime_seconds = (ir_sys.registers[4] << 16) | ir_sys.registers[3]
|
||||||
|
new_data["firmware"] = f"v{fw_major}.{fw_minor}.{fw_patch}"
|
||||||
|
new_data["device_status"] = "OK" if ir_sys.registers[2] == 0 else "ERROR"
|
||||||
|
new_data["uptime"] = format_uptime(uptime_seconds)
|
||||||
|
new_data["watchdog"] = f"{hr_sys.registers[0]}s"
|
||||||
|
new_data["error"] = None # Clear any previous error on successful read
|
||||||
|
reconnect_attempts = 0 # Reset attempts on successful communication
|
||||||
|
except Exception as e:
|
||||||
|
new_data["error"] = f"Communication Error: {e}. Closing connection."
|
||||||
|
client.close() # Close connection to force reconnect attempt in next loop
|
||||||
|
finally:
|
||||||
|
with status_lock:
|
||||||
|
status_data = new_data
|
||||||
|
time.sleep(interval)
|
||||||
|
|
||||||
|
def firmware_update_thread(slave_id, filepath):
|
||||||
|
global update_status
|
||||||
|
with update_lock:
|
||||||
|
update_status = {"running": True, "message": "Starting update...", "progress": 0.0}
|
||||||
|
try:
|
||||||
|
with open(filepath, 'rb') as f: firmware = f.read()
|
||||||
|
file_size = len(firmware)
|
||||||
|
chunk_size = 248
|
||||||
|
offset = 0
|
||||||
|
while offset < file_size:
|
||||||
|
chunk = firmware[offset:offset + chunk_size]
|
||||||
|
with update_lock:
|
||||||
|
update_status["message"] = f"Sending chunk {offset//chunk_size + 1}/{(file_size + chunk_size - 1)//chunk_size}..."
|
||||||
|
update_status["progress"] = offset / file_size
|
||||||
|
client.write_register(REG_HOLDING_FWU_CHUNK_OFFSET_LOW, offset & 0xFFFF, slave=slave_id)
|
||||||
|
client.write_register(REG_HOLDING_FWU_CHUNK_OFFSET_HIGH, (offset >> 16) & 0xFFFF, slave=slave_id)
|
||||||
|
client.write_register(REG_HOLDING_FWU_CHUNK_SIZE, len(chunk), slave=slave_id)
|
||||||
|
padded_chunk = chunk + (b'\x00' if len(chunk) % 2 != 0 else b'')
|
||||||
|
registers = [int.from_bytes(padded_chunk[i:i+2], 'big') for i in range(0, len(padded_chunk), 2)]
|
||||||
|
burst_size_regs = 16
|
||||||
|
for i in range(0, len(registers), burst_size_regs):
|
||||||
|
reg_burst = registers[i:i + burst_size_regs]
|
||||||
|
start_addr = REG_HOLDING_FWU_DATA_BUFFER + i
|
||||||
|
client.write_registers(start_addr, reg_burst, slave=slave_id)
|
||||||
|
time.sleep(0.02)
|
||||||
|
time.sleep(0.1)
|
||||||
|
client.read_input_registers(REG_INPUT_FWU_LAST_CHUNK_CRC, count=1, slave=slave_id)
|
||||||
|
client.write_register(REG_HOLDING_FWU_COMMAND, 1, slave=slave_id)
|
||||||
|
offset += len(chunk)
|
||||||
|
with update_lock:
|
||||||
|
update_status["progress"] = 1.0
|
||||||
|
update_status["message"] = "Finalizing update..."
|
||||||
|
client.write_register(REG_HOLDING_FWU_COMMAND, 2, slave=slave_id)
|
||||||
|
time.sleep(1)
|
||||||
|
with update_lock: update_status["message"] = "Update complete! Slave is rebooting."
|
||||||
|
time.sleep(2)
|
||||||
|
except Exception as e:
|
||||||
|
with update_lock: update_status["message"] = f"Error: {e}"
|
||||||
|
time.sleep(3)
|
||||||
|
finally:
|
||||||
|
with update_lock: update_status["running"] = False
|
||||||
|
|
||||||
|
def draw_button(stdscr, y, x, text, selected=False):
|
||||||
|
"""Draws a button, handling selection highlight."""
|
||||||
|
color = curses.color_pair(2) if selected else curses.color_pair(1)
|
||||||
|
button_width = len(text) + 2
|
||||||
|
stdscr.addstr(y, x, " " * button_width, color)
|
||||||
|
stdscr.addstr(y, x + 1, text, color)
|
||||||
|
|
||||||
|
def file_browser(stdscr):
|
||||||
|
"""A simple curses file browser."""
|
||||||
|
curses.curs_set(1)
|
||||||
|
path = os.getcwd()
|
||||||
|
selected_index = 0
|
||||||
|
|
||||||
|
while True:
|
||||||
|
stdscr.clear()
|
||||||
|
h, w = stdscr.getmaxyx()
|
||||||
|
stdscr.addstr(0, 0, f"Select Firmware File: {path}".ljust(w-1), curses.color_pair(2))
|
||||||
|
|
||||||
|
try:
|
||||||
|
items = sorted(os.listdir(path))
|
||||||
|
except OSError as e:
|
||||||
|
items = [f".. (Error: {e})"]
|
||||||
|
|
||||||
|
items.insert(0, "..")
|
||||||
|
|
||||||
|
for i, item_name in enumerate(items):
|
||||||
|
if i >= h - 2: break
|
||||||
|
display_name = item_name
|
||||||
|
if os.path.isdir(os.path.join(path, item_name)):
|
||||||
|
display_name += "/"
|
||||||
|
|
||||||
|
if i == selected_index:
|
||||||
|
stdscr.addstr(i + 1, 0, display_name, curses.color_pair(2))
|
||||||
|
else:
|
||||||
|
stdscr.addstr(i + 1, 0, display_name)
|
||||||
|
|
||||||
|
key = stdscr.getch()
|
||||||
|
|
||||||
|
if key == curses.KEY_UP:
|
||||||
|
selected_index = max(0, selected_index - 1)
|
||||||
|
elif key == curses.KEY_DOWN:
|
||||||
|
selected_index = min(len(items) - 1, selected_index + 1)
|
||||||
|
elif key == curses.KEY_ENTER or key in [10, 13]:
|
||||||
|
selected_item_path = os.path.join(path, items[selected_index])
|
||||||
|
if os.path.isdir(selected_item_path):
|
||||||
|
path = os.path.abspath(selected_item_path)
|
||||||
|
selected_index = 0
|
||||||
|
else:
|
||||||
|
return selected_item_path
|
||||||
|
elif key == 27: # ESC key
|
||||||
|
return None
|
||||||
|
|
||||||
|
def main_menu(stdscr, slave_id):
|
||||||
|
global status_data, update_status
|
||||||
|
curses.curs_set(0); stdscr.nodelay(1); stdscr.timeout(100)
|
||||||
|
curses.start_color(); curses.init_pair(1, curses.COLOR_WHITE, curses.COLOR_BLUE); curses.init_pair(2, curses.COLOR_BLUE, curses.COLOR_WHITE); curses.init_pair(3, curses.COLOR_RED, curses.COLOR_BLUE)
|
||||||
|
stdscr.bkgd(' ', curses.color_pair(1))
|
||||||
|
|
||||||
|
menu = ["Open Valve", "Close Valve", "Stop Valve", "Toggle Output 1", "Toggle Output 2", "Set Watchdog", "Reset Node", "Firmware Update", "Exit"]
|
||||||
|
current_row_idx = 0
|
||||||
|
message, message_time = "", 0
|
||||||
|
input_mode, input_prompt, input_str, input_target_reg = False, "", "", 0
|
||||||
|
|
||||||
|
while not stop_event.is_set():
|
||||||
|
h, w = stdscr.getmaxyx()
|
||||||
|
key = stdscr.getch()
|
||||||
|
|
||||||
|
with update_lock: is_updating = update_status["running"]
|
||||||
|
|
||||||
|
if is_updating:
|
||||||
|
pass
|
||||||
|
elif input_mode:
|
||||||
|
if key in [10, 13]:
|
||||||
|
try:
|
||||||
|
value = int(input_str)
|
||||||
|
client.write_register(input_target_reg, value, slave=slave_id)
|
||||||
|
message = f"-> Set register 0x{input_target_reg:04X} to {value}"
|
||||||
|
except Exception as e: message = f"-> Error: {e}"
|
||||||
|
message_time, input_mode, input_str = time.time(), False, ""
|
||||||
|
elif key == curses.KEY_BACKSPACE or key == 127: input_str = input_str[:-1]
|
||||||
|
elif key != -1 and chr(key).isprintable(): input_str += chr(key)
|
||||||
|
else:
|
||||||
|
if key == curses.KEY_UP: current_row_idx = (current_row_idx - 1) % len(menu)
|
||||||
|
elif key == curses.KEY_DOWN: current_row_idx = (current_row_idx + 1) % len(menu)
|
||||||
|
elif key == curses.KEY_ENTER or key in [10, 13]:
|
||||||
|
selected_option = menu[current_row_idx]
|
||||||
|
message_time = time.time()
|
||||||
|
if selected_option == "Exit": stop_event.set(); continue
|
||||||
|
elif selected_option == "Open Valve": client.write_register(REG_HOLDING_VALVE_COMMAND, 1, slave=slave_id); message = "-> Sent OPEN command"
|
||||||
|
elif selected_option == "Close Valve": client.write_register(REG_HOLDING_VALVE_COMMAND, 2, slave=slave_id); message = "-> Sent CLOSE command"
|
||||||
|
elif selected_option == "Stop Valve": client.write_register(REG_HOLDING_VALVE_COMMAND, 0, slave=slave_id); message = "-> Sent STOP command"
|
||||||
|
elif "Toggle Output" in selected_option:
|
||||||
|
bit = 0 if "1" in selected_option else 1
|
||||||
|
try:
|
||||||
|
current_val = client.read_holding_registers(REG_HOLDING_DIGITAL_OUTPUTS_STATE, count=1, slave=slave_id).registers[0]
|
||||||
|
client.write_register(REG_HOLDING_DIGITAL_OUTPUTS_STATE, current_val ^ (1 << bit), slave=slave_id)
|
||||||
|
message = f"-> Toggled Output {bit+1}"
|
||||||
|
except Exception as e: message = f"-> Error: {e}"
|
||||||
|
elif selected_option == "Set Watchdog":
|
||||||
|
input_mode, input_prompt, input_target_reg = True, "Enter Watchdog Timeout (s): ", REG_HOLDING_WATCHDOG_TIMEOUT_S
|
||||||
|
elif selected_option == "Reset Node":
|
||||||
|
try:
|
||||||
|
client.write_register(REG_HOLDING_DEVICE_RESET, 1, slave=slave_id)
|
||||||
|
message = "-> Sent RESET command. Node should reboot."
|
||||||
|
except Exception as e:
|
||||||
|
message = f"-> Error sending reset: {e}"
|
||||||
|
elif selected_option == "Firmware Update":
|
||||||
|
filepath = file_browser(stdscr)
|
||||||
|
if filepath:
|
||||||
|
threading.Thread(target=firmware_update_thread, args=(slave_id, filepath), daemon=True).start()
|
||||||
|
else:
|
||||||
|
message = "-> Firmware update cancelled."
|
||||||
|
|
||||||
|
stdscr.clear()
|
||||||
|
if is_updating:
|
||||||
|
with update_lock: prog, msg = update_status["progress"], update_status["message"]
|
||||||
|
stdscr.addstr(h // 2 - 1, w // 2 - 25, "FIRMWARE UPDATE IN PROGRESS", curses.A_BOLD | curses.color_pair(2))
|
||||||
|
stdscr.addstr(h // 2, w // 2 - 25, f"[{'#' * int(prog * 50):<50}] {prog:.0%}")
|
||||||
|
stdscr.addstr(h // 2 + 1, w // 2 - 25, msg.ljust(50))
|
||||||
|
else:
|
||||||
|
with status_lock: current_data = status_data.copy()
|
||||||
|
bold, normal = curses.color_pair(1) | curses.A_BOLD, curses.color_pair(1)
|
||||||
|
if current_data.get("error"): stdscr.addstr(0, 0, current_data["error"], curses.color_pair(3) | curses.A_BOLD)
|
||||||
|
else:
|
||||||
|
col1, col2, col3, col4 = 2, 30, 58, 88
|
||||||
|
stdscr.addstr(1, col1, "State:", bold); stdscr.addstr(1, col1 + 18, str(current_data.get('state', 'N/A')), normal)
|
||||||
|
stdscr.addstr(2, col1, "Movement:", bold); stdscr.addstr(2, col1 + 18, str(current_data.get('movement', 'N/A')), normal)
|
||||||
|
stdscr.addstr(3, col1, "Motor Current:", bold); stdscr.addstr(3, col1 + 18, str(current_data.get('motor_current', 'N/A')), normal)
|
||||||
|
stdscr.addstr(1, col2, "Digital Inputs:", bold); stdscr.addstr(1, col2 + 18, str(current_data.get('digital_inputs', 'N/A')), normal)
|
||||||
|
stdscr.addstr(2, col2, "Digital Outputs:", bold); stdscr.addstr(2, col2 + 18, str(current_data.get('digital_outputs', 'N/A')), normal)
|
||||||
|
stdscr.addstr(3, col2, "Button Events:", bold); stdscr.addstr(3, col2 + 18, str(current_data.get('button_events', 'N/A')), normal)
|
||||||
|
stdscr.addstr(1, col3, "Max Open Time:", bold); stdscr.addstr(1, col3 + 16, str(current_data.get('open_time', 'N/A')), normal)
|
||||||
|
stdscr.addstr(2, col3, "Max Close Time:", bold); stdscr.addstr(2, col3 + 16, str(current_data.get('close_time', 'N/A')), normal)
|
||||||
|
stdscr.addstr(3, col3, "Watchdog:", bold); stdscr.addstr(3, col3 + 16, str(current_data.get('watchdog', 'N/A')), normal)
|
||||||
|
stdscr.addstr(1, col4, "Firmware:", bold); stdscr.addstr(1, col4 + 14, str(current_data.get('firmware', 'N/A')), normal)
|
||||||
|
stdscr.addstr(2, col4, "Uptime:", bold); stdscr.addstr(2, col4 + 14, str(current_data.get('uptime', 'N/A')), normal)
|
||||||
|
stdscr.addstr(3, col4, "Dev. Status:", bold); stdscr.addstr(3, col4 + 14, str(current_data.get('device_status', 'N/A')), normal)
|
||||||
|
stdscr.addstr(5, 0, "─" * (w - 1), normal)
|
||||||
|
for idx, row in enumerate(menu):
|
||||||
|
draw_button(stdscr, h // 2 - len(menu) + (idx * 2), w // 2 - len(row) // 2, row, idx == current_row_idx)
|
||||||
|
if time.time() - message_time < 2.0: stdscr.addstr(h - 2, 0, message.ljust(w - 1), curses.color_pair(1) | curses.A_BOLD)
|
||||||
|
if input_mode:
|
||||||
|
curses.curs_set(1); stdscr.addstr(h - 2, 0, (input_prompt + input_str).ljust(w-1), curses.color_pair(2)); stdscr.move(h - 2, len(input_prompt) + len(input_str))
|
||||||
|
else: curses.curs_set(0)
|
||||||
|
stdscr.refresh()
|
||||||
|
|
||||||
|
def main():
|
||||||
|
global client
|
||||||
|
parser = argparse.ArgumentParser(description="Modbus tool for irrigation system nodes.")
|
||||||
|
parser.add_argument("port", help="Serial port"); parser.add_argument("--baud", type=int, default=19200); parser.add_argument("--slave-id", type=int, default=1); parser.add_argument("--interval", type=float, default=1.0)
|
||||||
|
args = parser.parse_args()
|
||||||
|
client = ModbusSerialClient(port=args.port, baudrate=args.baud, stopbits=1, bytesize=8, parity="N", timeout=1)
|
||||||
|
if not client.connect(): print(f"Error: Failed to connect to serial port {args.port}"); sys.exit(1)
|
||||||
|
print("Successfully connected. Starting UI..."); time.sleep(0.5)
|
||||||
|
threading.Thread(target=poll_status, args=(args.slave_id, args.interval), daemon=True).start()
|
||||||
|
try: curses.wrapper(main_menu, args.slave_id)
|
||||||
|
finally:
|
||||||
|
stop_event.set()
|
||||||
|
print("\nExiting...")
|
||||||
|
if client.is_socket_open(): client.close()
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
2
software/tools/modbus_tool/requirements.txt
Normal file
2
software/tools/modbus_tool/requirements.txt
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
pymodbus>=3.6.0
|
||||||
|
pyserial>=3.5
|
||||||
@@ -1,132 +0,0 @@
|
|||||||
{
|
|
||||||
"folders": [
|
|
||||||
{
|
|
||||||
"path": "."
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"settings": {
|
|
||||||
// Hush CMake
|
|
||||||
"cmake.configureOnOpen": false,
|
|
||||||
|
|
||||||
// IntelliSense
|
|
||||||
"C_Cpp.default.compilerPath": "${userHome}/zephyr-sdk-0.17.1/arm-zephyr-eabi/bin/arm-zephyr-eabi-gcc",
|
|
||||||
"C_Cpp.default.compileCommands": "${workspaceFolder}/build/compile_commands.json",
|
|
||||||
|
|
||||||
// File Associations
|
|
||||||
"files.associations": {
|
|
||||||
"waterlevel_sensor.h": "c",
|
|
||||||
"shell.h": "c",
|
|
||||||
"can.h": "c"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"tasks": {
|
|
||||||
"version": "2.0.0",
|
|
||||||
"tasks": [
|
|
||||||
{
|
|
||||||
"label": "West Build",
|
|
||||||
"type": "shell",
|
|
||||||
"group": {
|
|
||||||
"kind": "build",
|
|
||||||
"isDefault": true
|
|
||||||
},
|
|
||||||
"command": "${userHome}/zephyrproject/.venv/bin/west",
|
|
||||||
"args": [
|
|
||||||
"build",
|
|
||||||
"-p",
|
|
||||||
"auto",
|
|
||||||
"-b",
|
|
||||||
"valve_node"
|
|
||||||
],
|
|
||||||
"problemMatcher": [
|
|
||||||
"$gcc"
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"label": "West Configurable Build",
|
|
||||||
"type": "shell",
|
|
||||||
"group": {
|
|
||||||
"kind": "build",
|
|
||||||
},
|
|
||||||
"command": "${userHome}/zephyrproject/.venv/bin/west",
|
|
||||||
"args": [
|
|
||||||
"build",
|
|
||||||
"-p",
|
|
||||||
"${input:pristine}",
|
|
||||||
"-b",
|
|
||||||
"${input:board}"
|
|
||||||
],
|
|
||||||
"problemMatcher": [
|
|
||||||
"$gcc"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"label": "West Flash",
|
|
||||||
"type": "shell",
|
|
||||||
"command": "${userHome}/zephyrproject/.venv/bin/west",
|
|
||||||
"args": [
|
|
||||||
"flash"
|
|
||||||
],
|
|
||||||
"problemMatcher": [
|
|
||||||
"$gcc"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"inputs": [
|
|
||||||
{
|
|
||||||
"id": "board",
|
|
||||||
"type": "promptString",
|
|
||||||
"default": "vave_node",
|
|
||||||
"description": "See https://docs.zephyrproject.org/latest/boards/index.html"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "pristine",
|
|
||||||
"type": "pickString",
|
|
||||||
"description": "Choose when to run a pristine build",
|
|
||||||
"default": "auto",
|
|
||||||
"options": [
|
|
||||||
"auto",
|
|
||||||
"always",
|
|
||||||
"never"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"launch": {
|
|
||||||
"version": "0.2.0",
|
|
||||||
"configurations": [
|
|
||||||
{
|
|
||||||
"name": "Launch",
|
|
||||||
"device": "STM32F103RB",
|
|
||||||
"cwd": "${workspaceFolder}",
|
|
||||||
"executable": "build/zephyr/zephyr.elf",
|
|
||||||
"request": "launch",
|
|
||||||
"type": "cortex-debug",
|
|
||||||
//"runToEntryPoint": "main",
|
|
||||||
"servertype": "jlink",
|
|
||||||
"gdbPath": "${userHome}/zephyr-sdk-0.17.1/arm-zephyr-eabi/bin/arm-zephyr-eabi-gdb",
|
|
||||||
"preLaunchTask": "West Build"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "Attach",
|
|
||||||
"device": "STM32F103RB",
|
|
||||||
"cwd": "${workspaceFolder}",
|
|
||||||
"executable": "build/zephyr/zephyr.elf",
|
|
||||||
"request": "attach",
|
|
||||||
"type": "cortex-debug",
|
|
||||||
//"runToEntryPoint": "main",
|
|
||||||
"servertype": "jlink",
|
|
||||||
"gdbPath": "${userHome}/zephyr-sdk-0.17.1/arm-zephyr-eabi/bin/arm-zephyr-eabi-gdb"
|
|
||||||
},
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"extensions": {
|
|
||||||
"recommendations": [
|
|
||||||
"ms-vscode.cpptools-extension-pack",
|
|
||||||
"ms-python.python",
|
|
||||||
"ms-vscode.vscode-embedded-tools",
|
|
||||||
"ms-vscode.vscode-serial-monitor",
|
|
||||||
"marus25.cortex-debug",
|
|
||||||
"donjayamanne.python-environment-manager"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user