diff --git a/README.md b/README.md index 388ffe4..362d393 100644 --- a/README.md +++ b/README.md @@ -1,22 +1,180 @@ # MQTT-DCC ESP32 PlatformIO Project -This project runs on an ESP32 (PlatformIO) and: -- Connects to WiFi and an MQTT broker -- Listens on the `dcc/command` topic for JSON accessory commands -- Generates a simplified DCC accessory packet and simulates sending it via a GPIO pin -- Publishes status/acknowledgements to `dcc/status` +Ce projet cible un ESP32 sous PlatformIO et permet de : +- se connecter à un réseau Wi-Fi +- se connecter à un broker MQTT +- recevoir des commandes d'accessoires DCC au format JSON sur le topic `dcc/command` +- générer un paquet DCC et le transmettre sur une alimenation à base de LM18200 via sortie GPIO +- publier des retours d'état sur le topic `dcc/status` -Setup: -1. Edit `include/config.h` and set `WIFI_SSID`, `WIFI_PASSWORD`, `MQTT_SERVER`, and other values. -2. Build and upload with PlatformIO. +## Fonctionnalités principales -Command format (JSON published to `dcc/command`): +- Configuration Wi-Fi via un portail web AP si la connexion Wi-Fi échoue +- Stockage des identifiants Wi-Fi dans la mémoire non volatile (NVS) +- Stockage des identifiants MQTT et nom d'hôte dans la mémoire non volatile (NVS) +- Séparation du code Wi-Fi et MQTT dans des modules dédiés +- Génération de paquets d'accessoire DCC + +## Configuration + +1. Ouvrir `include/config.h`. +2. Vérifier ou modifier les paramètres MQTT : + - `MQTT_SERVER` + - `MQTT_PORT` + - `MQTT_TOPIC_CMD` + - `MQTT_TOPIC_STATUS` + - `MQTT_TOPIC_RESPONSE` +3. Si nécessaire, ajuster les pins DCC : + - `DCC_DIR_PIN` + - `DCC_PWM_PIN` +4. Le SSID et le mot de passe Wi-Fi sont stockés dans la mémoire non volatile et sont configurables via le portail AP. + +> Ce projet utilise désormais la librairie `locoduino/DCCpp` pour générer un vrai signal DCC NMRA compatible avec un driver LM18200. +> `DCC_DIR_PIN` est utilisé pour le signal DCC et `DCC_PWM_PIN` pour l'activation de l'interface. + +- Au démarrage, l'ESP tente de se connecter au Wi-Fi avec les identifiants enregistrés. +- Si l'accès Wi-Fi n'est pas disponible, il démarre un point d'accès nommé `DCC_Config`. Et permet via un smartphone de définir les SSID / Key à l'aide d'un portail captif +- Ouvrir le navigateur à l'adresse IP affichée dans la console série pour configurer le SSID et le mot de passe. +- Après enregistrement, l'ESP redémarre et tente de se reconnecter au Wi-Fi. + +## Format de commande MQTT + +Publier un message JSON sur le topic `dcc/command` : + +```json { - "id": "optional-correlation-id", + "id": "1234", "address": 123, "state": true } +``` -Notes: -- The DCC packet generator here is a simplified representation. For NMRA-compliant output you will need a timing-accurate bitstream and the proper packet framing. -- Want me to add NMRA timing/pulse generation, OTA, or an example MQTT client for testing? Ask and I'll add it. +- `id` : identifiant éventuel pour corrélation +- `address` : adresse de l'accessoire +- `state` : `true` pour activer / `false` pour désactiver + +## Exemple Node-RED + +Dans Node-RED, utilisez un noeud `inject` puis un noeud `mqtt out` : + +1. Configurez un noeud `mqtt broker` avec l'adresse de votre broker MQTT. +2. Ajoutez un noeud `inject` et définissez la charge utile (`payload`) avec le JSON ci-dessus. +3. Mettez le `topic` du noeud `mqtt out` à `dcc/command`. +4. Connectez le noeud `inject` au noeud `mqtt out`. + +Exemple de flux simple : + +```json +[{"id":"inject_dcc","type":"inject","z":"flow","name":"Commande DCC","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"dcc/command","payload":"{\"id\":\"1234\",\"address\":123,\"state\":true}","payloadType":"json","wires":[["mqtt_out"]]},{"id":"mqtt_out","type":"mqtt out","z":"flow","name":"Envoi MQTT","topic":"dcc/command","qos":"0","retain":"false","broker":"mqtt_broker"}] +``` + +Cela permet d’intégrer facilement le projet dans un système global basé sur Node-RED. + +## Déploiement + +Compiler et téléverser avec PlatformIO : + +```bash +pio run -e esp32dev --target upload +``` + +## Structure du projet + +- `src/main.cpp` : programme principal +- `src/WifiManager.cpp` : gestion du Wi-Fi et du portail de configuration +- `src/MqttManager.cpp` : gestion de la connexion MQTT +- `lib/Dcc/` : génération de paquets DCC +- `include/config.h` : paramètres de configuration + +## Améliorations possibles + +- ajout d'un vrai générateur NMRA DCC avec horloge de sortie +- interface web de configuration plus complète +- gestion OTA +- publication d'un message de statut plus riche + +# Annexe - Le DCC + +Le pilotage des accessoires en DCC classique (NMRA) et via l'écosystème DCC-EX repose sur le même principe fondamental : l'envoi de paquets de données numériques sur les rails (ou un bus dédié) à destination de décodeurs d'accessoires (aiguillages, signaux, relais). +Cependant, la manière de spécifier l'Adresse et la Valeur (Commande/État) diffère légèrement selon qu'il s'agit du protocole bas niveau sur les rails ou des commandes textuelles envoyées à une centrale DCC-EX. + +## Le Pilotage DCC Classique + +Dans le standard NMRA (S-9.2.1), les accessoires sont pilotés via des paquets d'accessoires de base (Basic Accessory Decoder Packet). + +### L'Adresse + +Contrairement aux locomotives qui ont une adresse simple (1 à 10239), l'adressage des accessoires en DCC est historiquement découpé : + +- **L'Adresse de Décodeur** (Decoder Address) : Un décodeur physique d'accessoires (ex: une carte à 4 sorties) possède une adresse principale (1 à 511). +- **La Sortie / Pair** (Sub-address / Output) : Chaque décodeur gère généralement 4 sorties (paires d'adresses). +- **L'Adresse Unitaire** (Linear / Output Address) : Pour simplifier, les centrales et logiciels traduisent cela en une adresse linéaire continue (1, 2, 3, 4, 5, 6...). + +On a : +``` +Adresse Linéaire = ((AdresseDécodeur - 1) * 4) + Numéro de Sortie +``` +### La Valeur (Position et État) + +Un paquet d'accessoire DCC ne transmet pas une vitesse (0-128) comme une locomotive, mais un bit de direction et un bit d'activation : +- Direction / Position (1 bit) : + - 1 = Dévie / Closed / Red / Active (ex: Aiguillage en déviation) + - 0 = Droit / Thrown / Green / Inactive (ex: Aiguillage en voie directe) +- Power / Enable (1 bit) : + - 1 = Bobine sous tension (Commande envoyée) + - 0 = Bobine coupée (Relâchement de l'impulsion pour éviter de brûler le moteur solénoïde) + +## Le Pilotage avec DCC-EX + +DCC-EX est un système open-source pour Arduino/ESP32. Il simplifie la complexité des octets NMRA en proposant une API texte très simple via port série, Wi-Fi ou Bluetooth. + +Dans DCC-EX, les accessoires sont pilotés selon deux approches : **Directe** (numéro de pin ou paquet DCC) ou **Virtuelle** (Turnouts/Routes). + +### Méthode 1 : Commande Directe d'Aiguillage / Accessoire DCC + +Pour envoyer une commande DCC d'accessoire directement sur les rails à une adresse linéaire + +``` +a +``` + +ou plus communément pour un aiguillage (Turnout) : + +``` + > +``` + +Paramètres : + + - **ADRESSE** : L'adresse linéaire de l'accessoire (ex: 1 à 2048). + - **VALEUR / ÉTAT** : + - 1 : Thrown (Dévié / Actif) + - 0 : Closed (Droit / Inactif) + - C : Toggle / Inverser la position actuelle. + +Exemple : +- -> Met l'aiguillage DCC N°12 en position Déviée (Thrown). +- -> Met l'aiguillage DCC N°12 en position Directe (Closed). + +### Méthode 2 : Définition des "Turnouts" (Accessoires déclarés dans DCC-EX) + +DCC-EX permet d'associer des adresses virtuels ou des broches physiques (Servomoteurs, Relais sur Arduino) à des objets "Turnout". + +Dans le fichier de configuration myAutomations.cpp ou config.h, vous définissez vos accessoires : + +```cpp +// Aiguillage sur décodeur DCC à l'adresse 5 +Turnout::createDCC(5, "Aiguillage Gare"); + +// Aiguillage piloté directement par un Servomoteur connecté sur la Pin 9 de l'Arduino +Turnout::createSERVO(6, 9, 100, 400, 1, "Aiguillage Servo"); +``` + +Une fois définis, ils se pilotent via le protocole API texte : + +``` + > +``` + +- Exemple : (Mettre le Turnout 5 en Thrown). +- Exemple : (Mettre le Turnout 5 en Closed). \ No newline at end of file diff --git a/include/config.h b/include/config.h index c80ebfe..36c9d06 100644 --- a/include/config.h +++ b/include/config.h @@ -2,14 +2,15 @@ #define CONFIG_H // MQTT broker settings (leave MQTT server values as needed) -#define MQTT_SERVER "192.168.1.10" +#define MQTT_SERVER "192.168.1.53" #define MQTT_PORT 1883 #define MQTT_TOPIC_CMD "dcc/command" #define MQTT_TOPIC_STATUS "dcc/status" #define MQTT_TOPIC_RESPONSE "dcc/response" -// Pin used for simulated DCC output (adjust as needed) -#define DCC_OUTPUT_PIN 27 +// Pin used for DCC output (LM18200 IC or similar) +#define DCC_DIR_PIN 27 +#define DCC_PWM_PIN 26 // Access point SSID shown when no WiFi credentials are configured #define DEFAULT_AP_SSID "DCC_Config" diff --git a/lib/Dcc/Dcc.cpp b/lib/Dcc/Dcc.cpp index 44972ff..620ce15 100644 --- a/lib/Dcc/Dcc.cpp +++ b/lib/Dcc/Dcc.cpp @@ -1,26 +1,20 @@ #include "Dcc.h" #include -std::vector Dcc::generateAccessoryPacket(uint16_t addr, bool activate) { - // This produces a simple, application-level packet representation. - // For NMRA-compliant bitstreams you will need a timing-accurate generator. - std::vector p; - p.push_back((uint8_t)(addr >> 8)); - p.push_back((uint8_t)(addr & 0xFF)); - p.push_back(activate ? 1 : 0); - return p; +void Dcc::begin(int dirPin, int pwmPin) { + DCCpp::begin(); + DCCpp::beginMain(UNDEFINED_PIN, dirPin, pwmPin, UNDEFINED_PIN); + DCCpp::powerOn(true, false); } -void Dcc::sendPacket(const std::vector &packet, int pin) { - // Simple simulation: print packet and toggle pin briefly for each byte. - Serial.print("DCC packet: "); - for (size_t i = 0; i < packet.size(); ++i) { - Serial.print(packet[i], HEX); - Serial.print(' '); - digitalWrite(pin, HIGH); - delay(2); - digitalWrite(pin, LOW); - delay(2); +void Dcc::sendAccessory(uint16_t linearAddress, bool activate) { + if (linearAddress == 0) { + Serial.println("Adresse DCC invalide"); + return; } - Serial.println(); + + int boardAddress = ((linearAddress - 1) / 4) + 1; + int subAddress = (linearAddress - 1) % 4; + byte activateByte = activate ? 1 : 0; + DCCpp::setAccessory(boardAddress, subAddress, activateByte); } diff --git a/lib/Dcc/Dcc.h b/lib/Dcc/Dcc.h index c546d42..fb0f6d7 100644 --- a/lib/Dcc/Dcc.h +++ b/lib/Dcc/Dcc.h @@ -1,14 +1,14 @@ #pragma once #include #include +#include + +#ifndef UNDEFINED_PIN +#define UNDEFINED_PIN 255 +#endif class Dcc { public: - // Generate a simplified DCC accessory packet (bytes). - // addr: accessory address (application-dependent) - // activate: true = ON, false = OFF - static std::vector generateAccessoryPacket(uint16_t addr, bool activate); - - // Send packet via a GPIO pin (simulation/pulse). Implementation may be hardware-specific. - static void sendPacket(const std::vector &packet, int pin); + static void begin(int dirPin, int pwmPin); + static void sendAccessory(uint16_t linearAddress, bool activate); }; diff --git a/platformio.ini b/platformio.ini index a9f1574..d1dd93a 100644 --- a/platformio.ini +++ b/platformio.ini @@ -6,6 +6,7 @@ monitor_speed = 115200 lib_deps = PubSubClient ArduinoJson + locoduino/DCCpp build_flags = -DCORE_DEBUG_LEVEL=0 diff --git a/src/WifiManager.cpp b/src/WifiManager.cpp index b172176..8bed15f 100644 --- a/src/WifiManager.cpp +++ b/src/WifiManager.cpp @@ -62,6 +62,8 @@ void WifiManager::handleSave() { void WifiManager::begin() { loadCredentials(); + WiFi.mode(WIFI_STA); + if (ssid.length() > 0) { Serial.print("Connecting to WiFi"); WiFi.begin(ssid.c_str(), pass.c_str()); @@ -78,6 +80,7 @@ void WifiManager::begin() { Serial.println(WiFi.localIP()); apMode = false; } else { + WiFi.disconnect(true); startAP(); } } diff --git a/src/main.cpp b/src/main.cpp index 9819524..70ed0b8 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -25,8 +25,7 @@ void onMqttMessage(char* topic, uint8_t* payload, unsigned int length) { bool state = doc["state"] | false; String id = doc["id"] | ""; - auto pkt = Dcc::generateAccessoryPacket(addr, state); - Dcc::sendPacket(pkt, DCC_OUTPUT_PIN); + Dcc::sendAccessory(addr, state); // send ACK StaticJsonDocument<128> ack; @@ -48,8 +47,12 @@ void onMqttMessage(char* topic, uint8_t* payload, unsigned int length) { void setup() { Serial.begin(115200); - pinMode(DCC_OUTPUT_PIN, OUTPUT); - digitalWrite(DCC_OUTPUT_PIN, LOW); + pinMode(DCC_DIR_PIN, OUTPUT); + pinMode(DCC_PWM_PIN, OUTPUT); + digitalWrite(DCC_DIR_PIN, LOW); + digitalWrite(DCC_PWM_PIN, LOW); + + Dcc::begin(DCC_DIR_PIN, DCC_PWM_PIN); wifiManager.begin(); @@ -60,6 +63,7 @@ void setup() { void loop() { wifiManager.loop(); mqttManager.loop(); + DCCpp::loop(); if (wifiManager.isConnected() && mqttManager.connected() && !mqttSubscribed) { mqttManager.subscribe(MQTT_TOPIC_CMD);