From fd3fc380d89a164cca7aa35c9eb841c83adf9c1a Mon Sep 17 00:00:00 2001 From: Serge NOEL Date: Wed, 5 Aug 2026 09:14:31 +0200 Subject: [PATCH] Initialisation depot --- README.md | 22 +++++++++++ include/MqttManager.h | 24 ++++++++++++ include/WifiManager.h | 24 ++++++++++++ include/config.h | 17 ++++++++ lib/Dcc/Dcc.cpp | 26 +++++++++++++ lib/Dcc/Dcc.h | 14 +++++++ platformio.ini | 11 ++++++ src/MqttManager.cpp | 52 +++++++++++++++++++++++++ src/WifiManager.cpp | 90 +++++++++++++++++++++++++++++++++++++++++++ src/main.cpp | 70 +++++++++++++++++++++++++++++++++ 10 files changed, 350 insertions(+) create mode 100644 README.md create mode 100644 include/MqttManager.h create mode 100644 include/WifiManager.h create mode 100644 include/config.h create mode 100644 lib/Dcc/Dcc.cpp create mode 100644 lib/Dcc/Dcc.h create mode 100644 platformio.ini create mode 100644 src/MqttManager.cpp create mode 100644 src/WifiManager.cpp create mode 100644 src/main.cpp diff --git a/README.md b/README.md new file mode 100644 index 0000000..388ffe4 --- /dev/null +++ b/README.md @@ -0,0 +1,22 @@ +# 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` + +Setup: +1. Edit `include/config.h` and set `WIFI_SSID`, `WIFI_PASSWORD`, `MQTT_SERVER`, and other values. +2. Build and upload with PlatformIO. + +Command format (JSON published to `dcc/command`): +{ + "id": "optional-correlation-id", + "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. diff --git a/include/MqttManager.h b/include/MqttManager.h new file mode 100644 index 0000000..c9407e1 --- /dev/null +++ b/include/MqttManager.h @@ -0,0 +1,24 @@ +#pragma once +#include +#include +#include + +class MqttManager { +public: + using MessageCallback = std::function; + MqttManager(); + void begin(const char* server, uint16_t port, MessageCallback cb); + void loop(); + bool publish(const char* topic, const char* payload); + bool subscribe(const char* topic); + bool connected(); +private: + WiFiClient espClient; + PubSubClient client{espClient}; + MessageCallback messageCb; + const char* serverAddr = nullptr; + uint16_t serverPort = 0; + static MqttManager* instance; + static void staticCallback(char* topic, byte* payload, unsigned int length); + void connect(); +}; diff --git a/include/WifiManager.h b/include/WifiManager.h new file mode 100644 index 0000000..dae16d9 --- /dev/null +++ b/include/WifiManager.h @@ -0,0 +1,24 @@ +#pragma once +#include +#include +#include + +class WifiManager { +public: + WifiManager(); + void begin(); + bool isConnected(); + void loop(); + String getSSID(); + String getPassword(); +private: + WebServer server{80}; + String ssid; + String pass; + bool apMode; + void startAP(); + void handleRoot(); + void handleSave(); + void loadCredentials(); + void saveCredentials(const String &s, const String &p); +}; diff --git a/include/config.h b/include/config.h new file mode 100644 index 0000000..c80ebfe --- /dev/null +++ b/include/config.h @@ -0,0 +1,17 @@ +#ifndef CONFIG_H +#define CONFIG_H + +// MQTT broker settings (leave MQTT server values as needed) +#define MQTT_SERVER "192.168.1.10" +#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 + +// Access point SSID shown when no WiFi credentials are configured +#define DEFAULT_AP_SSID "DCC_Config" + +#endif // CONFIG_H diff --git a/lib/Dcc/Dcc.cpp b/lib/Dcc/Dcc.cpp new file mode 100644 index 0000000..44972ff --- /dev/null +++ b/lib/Dcc/Dcc.cpp @@ -0,0 +1,26 @@ +#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::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); + } + Serial.println(); +} diff --git a/lib/Dcc/Dcc.h b/lib/Dcc/Dcc.h new file mode 100644 index 0000000..c546d42 --- /dev/null +++ b/lib/Dcc/Dcc.h @@ -0,0 +1,14 @@ +#pragma once +#include +#include + +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); +}; diff --git a/platformio.ini b/platformio.ini new file mode 100644 index 0000000..a9f1574 --- /dev/null +++ b/platformio.ini @@ -0,0 +1,11 @@ +[env:esp32dev] +platform = espressif32 +board = esp32dev +framework = arduino +monitor_speed = 115200 +lib_deps = + PubSubClient + ArduinoJson + +build_flags = + -DCORE_DEBUG_LEVEL=0 diff --git a/src/MqttManager.cpp b/src/MqttManager.cpp new file mode 100644 index 0000000..bafb571 --- /dev/null +++ b/src/MqttManager.cpp @@ -0,0 +1,52 @@ +#include "MqttManager.h" +#include + +MqttManager* MqttManager::instance = nullptr; + +MqttManager::MqttManager() {} + +void MqttManager::begin(const char* server, uint16_t port, MessageCallback cb) { + serverAddr = server; + serverPort = port; + messageCb = cb; + client.setServer(serverAddr, serverPort); + instance = this; + client.setCallback(MqttManager::staticCallback); +} + +void MqttManager::staticCallback(char* topic, byte* payload, unsigned int length) { + if (instance && instance->messageCb) instance->messageCb(topic, payload, length); +} + +void MqttManager::connect() { + while (!client.connected()) { + Serial.print("Connecting to MQTT..."); + if (client.connect("esp32-dcc")) { + Serial.println("connected"); + } else { + Serial.print("failed rc="); + Serial.print(client.state()); + Serial.println("; retrying in 2s"); + delay(2000); + } + } +} + +void MqttManager::loop() { + if (WiFi.status() == WL_CONNECTED) { + if (!client.connected()) connect(); + client.loop(); + } +} + +bool MqttManager::publish(const char* topic, const char* payload) { + if (!client.connected()) return false; + return client.publish(topic, payload); +} + +bool MqttManager::subscribe(const char* topic) { + if (!client.connected()) return false; + return client.subscribe(topic); +} + +bool MqttManager::connected() { return client.connected(); } diff --git a/src/WifiManager.cpp b/src/WifiManager.cpp new file mode 100644 index 0000000..b172176 --- /dev/null +++ b/src/WifiManager.cpp @@ -0,0 +1,90 @@ +#include "WifiManager.h" +#include "config.h" +#include +#include + +static const char* AP_SSID = DEFAULT_AP_SSID; + +WifiManager::WifiManager(): ssid(""), pass(""), apMode(false) {} + +void WifiManager::loadCredentials() { + Preferences prefs; + prefs.begin("dcc", true); + ssid = prefs.getString("ssid", ""); + pass = prefs.getString("pass", ""); + prefs.end(); +} + +void WifiManager::saveCredentials(const String &s, const String &p) { + Preferences prefs; + prefs.begin("dcc", false); + prefs.putString("ssid", s); + prefs.putString("pass", p); + prefs.end(); +} + +void WifiManager::startAP() { + apMode = true; + WiFi.mode(WIFI_AP); + WiFi.softAP(AP_SSID); + IPAddress ip = WiFi.softAPIP(); + Serial.print("AP started: "); + Serial.println(AP_SSID); + Serial.print("Configure at http://"); + Serial.println(ip); + server.on("/", std::bind(&WifiManager::handleRoot, this)); + server.on("/save", std::bind(&WifiManager::handleSave, this)); + server.begin(); +} + +void WifiManager::handleRoot() { + String page = "

Configure WiFi

" + "
" + "SSID:
" + "Password:
" + "
"; + server.send(200, "text/html", page); +} + +void WifiManager::handleSave() { + if (server.hasArg("ssid")) { + String s = server.arg("ssid"); + String p = server.arg("pass"); + saveCredentials(s, p); + String resp = "Saved. Rebooting..."; + server.send(200, "text/html", resp); + delay(1000); + ESP.restart(); + } else { + server.send(400, "text/plain", "Missing ssid"); + } +} + +void WifiManager::begin() { + loadCredentials(); + if (ssid.length() > 0) { + Serial.print("Connecting to WiFi"); + WiFi.begin(ssid.c_str(), pass.c_str()); + unsigned long start = millis(); + while (WiFi.status() != WL_CONNECTED && millis() - start < 10000) { + delay(250); + Serial.print('.'); + } + Serial.println(); + } + + if (WiFi.status() == WL_CONNECTED) { + Serial.print("WiFi connected, IP: "); + Serial.println(WiFi.localIP()); + apMode = false; + } else { + startAP(); + } +} + +bool WifiManager::isConnected() { return WiFi.status() == WL_CONNECTED; } + +void WifiManager::loop() { if (apMode) server.handleClient(); } + +String WifiManager::getSSID() { return ssid; } +String WifiManager::getPassword() { return pass; } diff --git a/src/main.cpp b/src/main.cpp new file mode 100644 index 0000000..9819524 --- /dev/null +++ b/src/main.cpp @@ -0,0 +1,70 @@ +#include +#include +#include +#include +#include "config.h" +#include "Dcc.h" +#include "WifiManager.h" +#include "MqttManager.h" + +WifiManager wifiManager; +MqttManager mqttManager; + +static bool mqttSubscribed = false; + +void onMqttMessage(char* topic, uint8_t* payload, unsigned int length) { + StaticJsonDocument<256> doc; + DeserializationError err = deserializeJson(doc, payload, length); + if (err) { + Serial.println("Invalid JSON in MQTT message"); + return; + } + + if (strcmp(topic, MQTT_TOPIC_CMD) == 0) { + uint16_t addr = doc["address"] | 0; + bool state = doc["state"] | false; + String id = doc["id"] | ""; + + auto pkt = Dcc::generateAccessoryPacket(addr, state); + Dcc::sendPacket(pkt, DCC_OUTPUT_PIN); + + // send ACK + StaticJsonDocument<128> ack; + ack["id"] = id; + ack["status"] = "sent"; + char buf[128]; + size_t n = serializeJson(ack, buf); + mqttManager.publish(MQTT_TOPIC_STATUS, buf); + } + + if (strcmp(topic, MQTT_TOPIC_RESPONSE) == 0) { + // forward any response to status topic + // ensure payload is null-terminated + String resp; + for (unsigned int i = 0; i < length; ++i) resp += (char)payload[i]; + mqttManager.publish(MQTT_TOPIC_STATUS, resp.c_str()); + } +} + +void setup() { + Serial.begin(115200); + pinMode(DCC_OUTPUT_PIN, OUTPUT); + digitalWrite(DCC_OUTPUT_PIN, LOW); + + wifiManager.begin(); + + // MQTT manager can be started even if WiFi is not connected yet; it will connect when possible + mqttManager.begin(MQTT_SERVER, MQTT_PORT, onMqttMessage); +} + +void loop() { + wifiManager.loop(); + mqttManager.loop(); + + if (wifiManager.isConnected() && mqttManager.connected() && !mqttSubscribed) { + mqttManager.subscribe(MQTT_TOPIC_CMD); + mqttManager.subscribe(MQTT_TOPIC_RESPONSE); + mqttManager.publish(MQTT_TOPIC_STATUS, "ready"); + mqttSubscribed = true; + } +}