Initialisation depot

This commit is contained in:
2025-11-30 09:58:00 +01:00
commit 56d8cd96c8
28 changed files with 3154 additions and 0 deletions

95
src/Config.cpp Normal file
View File

@@ -0,0 +1,95 @@
/**
* @file Config.cpp
* @brief Implementation of configuration management
*/
#include "Config.h"
/**
* @brief Constructor - sets default configuration values
*
* Initializes all settings to safe defaults:
* - WiFi: AP mode with default SSID "LocoTestBench"
* - System: DC analog mode, address 3, stopped
*/
Config::Config() {
// Default values
wifi.ssid = "";
wifi.password = "";
wifi.isAPMode = true;
wifi.apSSID = "LocoTestBench";
wifi.apPassword = "12345678";
system.isDCCMode = false;
system.dccAddress = 3;
system.speed = 0;
system.direction = 1;
system.dccFunctions = 0;
}
/**
* @brief Initialize configuration system
*
* Opens NVS namespace and loads saved configuration.
* If no saved config exists, defaults are used.
*/
void Config::begin() {
preferences.begin("loco-config", false);
load();
}
/**
* @brief Save all configuration to persistent storage
*
* Writes WiFi and system settings to NVS flash memory.
* Settings persist across power cycles and reboots.
*/
void Config::save() {
// WiFi settings
preferences.putString("wifi_ssid", wifi.ssid);
preferences.putString("wifi_pass", wifi.password);
}
/**
* @brief Load configuration from persistent storage
*
* Reads all settings from NVS. If a setting doesn't exist,
* the current (default) value is retained.
*/
void Config::load() {
// WiFi settingstring("ap_ssid", wifi.apSSID);
preferences.putString("ap_pass", wifi.apPassword);
// System settings
preferences.putBool("is_dcc", system.isDCCMode);
preferences.putUShort("dcc_addr", system.dccAddress);
preferences.putUChar("speed", system.speed);
preferences.putUChar("direction", system.direction);
preferences.putUInt("dcc_func", system.dccFunctions);
}
/**
* @brief Reset all settings to factory defaults
*
* Clears NVS storage and reinitializes with default values.
* @warning All saved configuration will be permanently lost!
*/
void Config::reset() {
preferences.clear();
Config(); // Reset to defaults
save();
wifi.ssid = preferences.getString("wifi_ssid", "");
wifi.password = preferences.getString("wifi_pass", "");
wifi.isAPMode = preferences.getBool("wifi_ap", true);
wifi.apSSID = preferences.getString("ap_ssid", "LocoTestBench");
wifi.apPassword = preferences.getString("ap_pass", "12345678");
// System settings
system.isDCCMode = preferences.getBool("is_dcc", false);
system.dccAddress = preferences.getUShort("dcc_addr", 3);
system.speed = preferences.getUChar("speed", 0);
system.direction = preferences.getUChar("direction", 1);
system.dccFunctions = preferences.getUInt("dcc_func", 0);
}

199
src/DCCGenerator.cpp Normal file
View File

@@ -0,0 +1,199 @@
/**
* @file DCCGenerator.cpp
* @brief Implementation of DCC signal generation
*/
#include "DCCGenerator.h"
/**
* @brief Constructor - initialize with safe defaults
*/
DCCGenerator::DCCGenerator() :
enabled(false),
currentAddress(3),
currentSpeed(0),
currentDirection(1),
functionStates(0),
lastPacketTime(0) {
}
void DCCGenerator::begin() {
pinMode(DCC_PIN_A, OUTPUT);
pinMode(DCC_PIN_B, OUTPUT);
digitalWrite(DCC_PIN_A, LOW);
digitalWrite(DCC_PIN_B, LOW);
Serial.println("DCC Generator initialized");
Serial.printf("DCC Pin A: %d, DCC Pin B: %d\n", DCC_PIN_A, DCC_PIN_B);
}
void DCCGenerator::enable() {
enabled = true;
Serial.println("DCC mode enabled");
}
void DCCGenerator::disable() {
enabled = false;
digitalWrite(DCC_PIN_A, LOW);
digitalWrite(DCC_PIN_B, LOW);
Serial.println("DCC mode disabled");
}
void DCCGenerator::setLocoSpeed(uint16_t address, uint8_t speed, uint8_t direction) {
currentAddress = address;
currentSpeed = speed;
currentDirection = direction;
Serial.printf("DCC: Addr=%d, Speed=%d, Dir=%s\n",
address, speed, direction ? "FWD" : "REV");
}
void DCCGenerator::setFunction(uint16_t address, uint8_t function, bool state) {
currentAddress = address;
if (function <= 28) {
if (state) {
functionStates |= (1UL << function);
} else {
functionStates &= ~(1UL << function);
}
Serial.printf("DCC: Function F%d = %s\n", function, state ? "ON" : "OFF");
}
}
void DCCGenerator::update() {
if (!enabled) return;
unsigned long now = millis();
if (now - lastPacketTime >= PACKET_INTERVAL) {
lastPacketTime = now;
sendSpeedPacket();
// Periodically send function packets
static uint8_t packetCount = 0;
packetCount++;
if (packetCount % 3 == 0) {
sendFunctionPacket(1); // F0-F4
}
}
}
void DCCGenerator::sendBit(bool value) {
int duration = value ? DCC_ONE_BIT_PULSE_DURATION : DCC_ZERO_BIT_PULSE_DURATION;
// First half-cycle
digitalWrite(DCC_PIN_A, HIGH);
digitalWrite(DCC_PIN_B, LOW);
delayMicroseconds(duration);
// Second half-cycle
digitalWrite(DCC_PIN_A, LOW);
digitalWrite(DCC_PIN_B, HIGH);
delayMicroseconds(duration);
}
void DCCGenerator::sendPreamble() {
for (int i = 0; i < 14; i++) {
sendBit(1); // Send '1' bits
}
}
void DCCGenerator::sendByte(uint8_t data) {
for (int i = 7; i >= 0; i--) {
sendBit((data >> i) & 0x01);
}
}
void DCCGenerator::sendPacket(uint8_t* data, uint8_t length) {
sendPreamble();
// Packet start bit
sendBit(0);
// Send data bytes with separator bits
for (uint8_t i = 0; i < length; i++) {
sendByte(data[i]);
if (i < length - 1) {
sendBit(0); // Data byte separator
}
}
// Packet end bit
sendBit(1);
}
uint8_t DCCGenerator::calculateChecksum(uint8_t* data, uint8_t length) {
uint8_t checksum = 0;
for (uint8_t i = 0; i < length; i++) {
checksum ^= data[i];
}
return checksum;
}
void DCCGenerator::sendSpeedPacket() {
uint8_t packet[4];
uint8_t packetLength = 0;
// Address byte (short address: 1-127)
if (currentAddress <= 127) {
packet[packetLength++] = currentAddress & 0x7F;
} else {
// Long address (128-10239)
packet[packetLength++] = 0xC0 | ((currentAddress >> 8) & 0x3F);
packet[packetLength++] = currentAddress & 0xFF;
}
// Speed and direction instruction (128-step mode)
// Instruction: 0b00111111
uint8_t speedByte = 0b00111111; // 128-step speed control
// Convert speed (0-100) to DCC speed (0-126)
uint8_t dccSpeed = map(currentSpeed, 0, 100, 0, 126);
// Encode direction and speed
if (dccSpeed == 0) {
speedByte = 0b00111111; // Stop
} else {
// Bit 7: direction (1=forward, 0=reverse)
// Bits 0-6: speed (1-126, with 0 and 1 both meaning stop)
speedByte = 0b00111111;
speedByte |= (currentDirection ? 0x80 : 0x00);
speedByte = (speedByte & 0x80) | (dccSpeed & 0x7F);
}
packet[packetLength++] = speedByte;
// Error detection byte
packet[packetLength++] = calculateChecksum(packet, packetLength);
sendPacket(packet, packetLength);
}
void DCCGenerator::sendFunctionPacket(uint8_t group) {
uint8_t packet[4];
uint8_t packetLength = 0;
// Address byte
if (currentAddress <= 127) {
packet[packetLength++] = currentAddress & 0x7F;
} else {
packet[packetLength++] = 0xC0 | ((currentAddress >> 8) & 0x3F);
packet[packetLength++] = currentAddress & 0xFF;
}
// Function group 1 (F0-F4)
if (group == 1) {
uint8_t functionByte = 0b10000000; // Function group 1
functionByte |= ((functionStates & 0x01) ? 0x10 : 0x00); // F0
functionByte |= ((functionStates & 0x02) ? 0x01 : 0x00); // F1
functionByte |= ((functionStates & 0x04) ? 0x02 : 0x00); // F2
functionByte |= ((functionStates & 0x08) ? 0x04 : 0x00); // F3
functionByte |= ((functionStates & 0x10) ? 0x08 : 0x00); // F4
packet[packetLength++] = functionByte;
}
// Error detection byte
packet[packetLength++] = calculateChecksum(packet, packetLength);
sendPacket(packet, packetLength);
}

114
src/LEDIndicator.cpp Normal file
View File

@@ -0,0 +1,114 @@
/**
* @file LEDIndicator.cpp
* @brief Implementation of LED status indicators
*/
#include "LEDIndicator.h"
/**
* @brief Constructor - initialize with default state
*/
LEDIndicator::LEDIndicator() :
powerOn(false),
dccMode(false),
brightness(128),
lastUpdate(0),
pulsePhase(0) {
}
void LEDIndicator::begin() {
FastLED.addLeds<WS2812, LED_DATA_PIN, GRB>(leds, NUM_LEDS);
FastLED.setBrightness(brightness);
// Initialize both LEDs to off
leds[LED_POWER] = COLOR_OFF;
leds[LED_MODE] = COLOR_OFF;
FastLED.show();
Serial.println("LED Indicator initialized");
Serial.printf("LED Data Pin: %d, Num LEDs: %d\n", LED_DATA_PIN, NUM_LEDS);
}
void LEDIndicator::update() {
unsigned long now = millis();
// Update power LED
if (powerOn) {
leds[LED_POWER] = COLOR_POWER_ON;
} else {
leds[LED_POWER] = COLOR_POWER_OFF;
}
// Update mode LED with subtle pulsing effect
if (now - lastUpdate > 20) {
lastUpdate = now;
pulsePhase++;
// Create gentle pulse effect
uint8_t pulseBrightness = 128 + (sin8(pulsePhase * 2) / 4);
CRGB baseColor = dccMode ? COLOR_DCC : COLOR_ANALOG;
leds[LED_MODE] = baseColor;
leds[LED_MODE].fadeToBlackBy(255 - pulseBrightness);
}
FastLED.show();
}
void LEDIndicator::setPowerOn(bool on) {
if (powerOn != on) {
powerOn = on;
if (on) {
powerOnSequence();
}
}
}
void LEDIndicator::setMode(bool isDCC) {
if (dccMode != isDCC) {
dccMode = isDCC;
modeChangeEffect();
}
}
void LEDIndicator::setBrightness(uint8_t newBrightness) {
brightness = newBrightness;
FastLED.setBrightness(brightness);
}
void LEDIndicator::powerOnSequence() {
// Quick flash sequence on power on
for (int i = 0; i < 3; i++) {
leds[LED_POWER] = COLOR_POWER_ON;
FastLED.show();
delay(100);
leds[LED_POWER] = COLOR_OFF;
FastLED.show();
delay(100);
}
leds[LED_POWER] = COLOR_POWER_ON;
FastLED.show();
Serial.println("LED: Power ON sequence");
}
void LEDIndicator::modeChangeEffect() {
// Smooth transition effect when changing modes
CRGB targetColor = dccMode ? COLOR_DCC : COLOR_ANALOG;
// Fade out
for (int i = 255; i >= 0; i -= 15) {
leds[LED_MODE].fadeToBlackBy(15);
FastLED.show();
delay(10);
}
// Fade in new color
for (int i = 0; i <= 255; i += 15) {
leds[LED_MODE] = targetColor;
leds[LED_MODE].fadeToBlackBy(255 - i);
FastLED.show();
delay(10);
}
Serial.printf("LED: Mode changed to %s\n", dccMode ? "DCC (Blue)" : "Analog (Yellow)");
}

68
src/MotorController.cpp Normal file
View File

@@ -0,0 +1,68 @@
/**
* @file MotorController.cpp
* @brief Implementation of DC motor control
*/
#include "MotorController.h"
/**
* @brief Constructor - initialize with safe defaults
*/
MotorController::MotorController() : currentSpeed(0), currentDirection(1) {
}
void MotorController::begin() {
// Configure pins
pinMode(MOTOR_DIR_PIN, OUTPUT);
pinMode(MOTOR_BRAKE_PIN, OUTPUT);
// Setup PWM
ledcSetup(PWM_CHANNEL, PWM_FREQUENCY, PWM_RESOLUTION);
ledcAttachPin(MOTOR_PWM_PIN, PWM_CHANNEL);
// Initialize to safe state
digitalWrite(MOTOR_BRAKE_PIN, HIGH); // Release brake (active low)
digitalWrite(MOTOR_DIR_PIN, HIGH); // Forward direction
ledcWrite(PWM_CHANNEL, 0); // Zero speed
Serial.println("Motor Controller initialized");
Serial.printf("PWM Pin: %d, DIR Pin: %d, BRAKE Pin: %d\n",
MOTOR_PWM_PIN, MOTOR_DIR_PIN, MOTOR_BRAKE_PIN);
}
void MotorController::setSpeed(uint8_t speed, uint8_t direction) {
currentSpeed = speed;
currentDirection = direction;
// Release brake
digitalWrite(MOTOR_BRAKE_PIN, HIGH);
// Set direction
digitalWrite(MOTOR_DIR_PIN, direction ? HIGH : LOW);
// Set PWM duty cycle
// Speed is 0-100, convert to 0-255
uint16_t pwmValue = map(speed, 0, 100, 0, 255);
ledcWrite(PWM_CHANNEL, pwmValue);
Serial.printf("Motor: Speed=%d%%, Direction=%s, PWM=%d\n",
speed, direction ? "FWD" : "REV", pwmValue);
}
void MotorController::stop() {
currentSpeed = 0;
ledcWrite(PWM_CHANNEL, 0);
digitalWrite(MOTOR_BRAKE_PIN, HIGH); // Release brake
Serial.println("Motor stopped");
}
void MotorController::brake() {
ledcWrite(PWM_CHANNEL, 0);
digitalWrite(MOTOR_BRAKE_PIN, LOW); // Activate brake (active low)
currentSpeed = 0;
Serial.println("Motor brake activated");
}
void MotorController::update() {
// Placeholder for future safety checks or smooth acceleration
}

158
src/WebServer.cpp Normal file
View File

@@ -0,0 +1,158 @@
/**
* @file WebServer.cpp
* @brief Implementation of web server and REST API
*/
#include "WebServer.h"
#include <LittleFS.h>
/**
* @brief Constructor
*/
WebServerManager::WebServerManager(Config* cfg, MotorController* motor, DCCGenerator* dcc, LEDIndicator* led)
: config(cfg), motorController(motor), dccGenerator(dcc), ledIndicator(led), server(80) {
}
void WebServerManager::begin() {
// Initialize LittleFS
if (!LittleFS.begin(true)) {
Serial.println("LittleFS Mount Failed");
return;
}
Serial.println("LittleFS mounted successfully");
setupRoutes();
server.begin();
Serial.println("Web server started on port 80");
}
void WebServerManager::setupRoutes() {
// Serve main page
server.on("/", HTTP_GET, [this](AsyncWebServerRequest *request) {
request->send(LittleFS, "/index.html", "text/html");
});
// Serve static files (CSS, JS, Bootstrap)
server.serveStatic("/css/", LittleFS, "/css/");
server.serveStatic("/js/", LittleFS, "/js/");
// API endpoints
server.on("/api/status", HTTP_GET, [this](AsyncWebServerRequest *request) {
handleGetStatus(request);
});
server.on("/api/mode", HTTP_POST, [this](AsyncWebServerRequest *request) {
handleSetMode(request);
}, NULL, [this](AsyncWebServerRequest *request, uint8_t *data, size_t len, size_t index, size_t total) {
// Body handler
DynamicJsonDocument doc(256);
deserializeJson(doc, (const char*)data);
String mode = doc["mode"].as<String>();
config->system.isDCCMode = (mode == "dcc");
if (config->system.isDCCMode) {
motorController->stop();
dccGenerator->enable();
ledIndicator->setMode(true);
} else {
dccGenerator->disable();
ledIndicator->setMode(false);
}
config->save();
request->send(200, "application/json", "{\"status\":\"ok\"}");
});
server.on("/api/speed", HTTP_POST, [this](AsyncWebServerRequest *request) {
// Will be handled by body handler
}, NULL, [this](AsyncWebServerRequest *request, uint8_t *data, size_t len, size_t index, size_t total) {
DynamicJsonDocument doc(256);
deserializeJson(doc, (const char*)data);
uint8_t speed = doc["speed"];
uint8_t direction = doc["direction"];
config->system.speed = speed;
config->system.direction = direction;
if (config->system.isDCCMode) {
dccGenerator->setLocoSpeed(config->system.dccAddress, speed, direction);
} else {
motorController->setSpeed(speed, direction);
}
request->send(200, "application/json", "{\"status\":\"ok\"}");
});
server.on("/api/dcc/address", HTTP_POST, [this](AsyncWebServerRequest *request) {
// Will be handled by body handler
}, NULL, [this](AsyncWebServerRequest *request, uint8_t *data, size_t len, size_t index, size_t total) {
DynamicJsonDocument doc(256);
deserializeJson(doc, (const char*)data);
config->system.dccAddress = doc["address"];
config->save();
request->send(200, "application/json", "{\"status\":\"ok\"}");
});
server.on("/api/dcc/function", HTTP_POST, [this](AsyncWebServerRequest *request) {
// Will be handled by body handler
}, NULL, [this](AsyncWebServerRequest *request, uint8_t *data, size_t len, size_t index, size_t total) {
DynamicJsonDocument doc(256);
deserializeJson(doc, (const char*)data);
uint8_t function = doc["function"];
bool state = doc["state"];
dccGenerator->setFunction(config->system.dccAddress, function, state);
request->send(200, "application/json", "{\"status\":\"ok\"}");
});
server.on("/api/wifi", HTTP_POST, [this](AsyncWebServerRequest *request) {
// Will be handled by body handler
}, NULL, [this](AsyncWebServerRequest *request, uint8_t *data, size_t len, size_t index, size_t total) {
DynamicJsonDocument doc(512);
deserializeJson(doc, (const char*)data);
config->wifi.isAPMode = doc["isAPMode"];
config->wifi.apSSID = doc["apSSID"].as<String>();
config->wifi.apPassword = doc["apPassword"].as<String>();
config->wifi.ssid = doc["ssid"].as<String>();
config->wifi.password = doc["password"].as<String>();
config->save();
request->send(200, "application/json", "{\"status\":\"ok\"}");
delay(1000);
ESP.restart();
});
}
void WebServerManager::handleGetStatus(AsyncWebServerRequest *request) {
String json = getStatusJSON();
request->send(200, "application/json", json);
}
String WebServerManager::getStatusJSON() {
DynamicJsonDocument doc(512);
doc["mode"] = config->system.isDCCMode ? "dcc" : "analog";
doc["speed"] = config->system.speed;
doc["direction"] = config->system.direction;
doc["dccAddress"] = config->system.dccAddress;
// doc["ip"] = config->wifi.isAPMode ? WiFi.softAPIP().toString() : WiFi.localIP().toString();
doc["ip"] = "TODO";
doc["wifiMode"] = config->wifi.isAPMode ? "ap" : "client";
String output;
serializeJson(doc, output);
return output;
}
void WebServerManager::update() {
// AsyncWebServer handles requests asynchronously
}

103
src/WiFiManager.cpp Normal file
View File

@@ -0,0 +1,103 @@
/**
* @file WiFiManager.cpp
* @brief Implementation of WiFi management
*/
#include "WiFiManager.h"
/**
* @brief Constructor
*/
WiFiManager::WiFiManager(Config* cfg) : config(cfg), lastReconnectAttempt(0) {
}
void WiFiManager::begin() {
WiFi.mode(WIFI_MODE_NULL);
delay(100);
if (config->wifi.isAPMode) {
setupAccessPoint();
} else {
connectToWiFi();
}
}
void WiFiManager::setupAccessPoint() {
Serial.println("Setting up Access Point...");
WiFi.mode(WIFI_AP);
bool success = WiFi.softAP(
config->wifi.apSSID.c_str(),
config->wifi.apPassword.c_str()
);
if (success) {
IPAddress IP = WiFi.softAPIP();
Serial.print("AP IP address: ");
Serial.println(IP);
Serial.print("AP SSID: ");
Serial.println(config->wifi.apSSID);
} else {
Serial.println("Failed to create Access Point!");
}
}
void WiFiManager::connectToWiFi() {
if (config->wifi.ssid.length() == 0) {
Serial.println("No WiFi credentials configured. Starting AP mode.");
config->wifi.isAPMode = true;
setupAccessPoint();
return;
}
Serial.println("Connecting to WiFi...");
Serial.print("SSID: ");
Serial.println(config->wifi.ssid);
WiFi.mode(WIFI_STA);
WiFi.begin(config->wifi.ssid.c_str(), config->wifi.password.c_str());
int attempts = 0;
while (WiFi.status() != WL_CONNECTED && attempts < 20) {
delay(500);
Serial.print(".");
attempts++;
}
if (WiFi.status() == WL_CONNECTED) {
Serial.println("\nWiFi connected!");
Serial.print("IP address: ");
Serial.println(WiFi.localIP());
} else {
Serial.println("\nFailed to connect to WiFi. Starting AP mode.");
config->wifi.isAPMode = true;
setupAccessPoint();
}
}
bool WiFiManager::isConnected() {
if (config->wifi.isAPMode) {
return WiFi.softAPgetStationNum() > 0 || true; // AP is always "connected"
}
return WiFi.status() == WL_CONNECTED;
}
String WiFiManager::getIPAddress() {
if (config->wifi.isAPMode) {
return WiFi.softAPIP().toString();
}
return WiFi.localIP().toString();
}
void WiFiManager::update() {
// Auto-reconnect if in STA mode and disconnected
if (!config->wifi.isAPMode && WiFi.status() != WL_CONNECTED) {
unsigned long now = millis();
if (now - lastReconnectAttempt > RECONNECT_INTERVAL) {
lastReconnectAttempt = now;
Serial.println("Attempting to reconnect to WiFi...");
WiFi.disconnect();
WiFi.begin(config->wifi.ssid.c_str(), config->wifi.password.c_str());
}
}
}

133
src/main.cpp Normal file
View File

@@ -0,0 +1,133 @@
/**
* @file main.cpp
* @brief Main application entry point for Locomotive Test Bench
*
* Orchestrates all system components:
* - Configuration management
* - WiFi connectivity
* - Motor control (DC analog)
* - DCC signal generation
* - LED status indicators
* - Web server interface
*
* @author Locomotive Test Bench Project
* @date 2025
* @version 1.0
*/
#include <Arduino.h>
#include "Config.h"
#include "WiFiManager.h"
#include "MotorController.h"
#include "DCCGenerator.h"
#include "LEDIndicator.h"
#include "WebServer.h"
// Global objects
Config config;
WiFiManager wifiManager(&config);
MotorController motorController;
DCCGenerator dccGenerator;
LEDIndicator ledIndicator;
WebServerManager webServer(&config, &motorController, &dccGenerator, &ledIndicator);
/**
* @brief Setup function - runs once at startup
*
* Initializes all hardware and software components in correct order:
* 1. Serial communication
* 2. Configuration system
* 3. WiFi connectivity
* 4. LED indicators
* 5. Motor controller
* 6. DCC generator
* 7. Web server
*/
void setup() {
// Initialize serial communication
Serial.begin(115200);
delay(1000);
Serial.println("\n\n=================================");
Serial.println(" Locomotive Test Bench v1.0");
Serial.println("=================================\n");
// Load configuration
config.begin();
Serial.println("Configuration loaded");
// Initialize WiFi
wifiManager.begin();
// Initialize LED indicator
ledIndicator.begin();
ledIndicator.setPowerOn(true);
// Initialize motor controller
motorController.begin();
// Initialize DCC generator
dccGenerator.begin();
// Set initial mode and LED
if (config.system.isDCCMode) {
dccGenerator.enable();
ledIndicator.setMode(true);
dccGenerator.setLocoSpeed(
config.system.dccAddress,
config.system.speed,
config.system.direction
);
} else {
ledIndicator.setMode(false);
motorController.setSpeed(
config.system.speed,
config.system.direction
);
Serial.println("=================================\\n");
}
// }
/**
* @brief Main loop - runs continuously
*
* Updates all system components:
* - WiFi connection monitoring
* - LED status display
* - DCC signal generation (if enabled)
* - Motor control updates (if in analog mode)
*
* @note Small delay prevents watchdog timer issues
*/
// void loop() {
// Update WiFi connection status
Serial.println("\n=================================");
Serial.println("Setup complete!");
Serial.println("=================================");
Serial.print("Mode: ");
Serial.println(config.system.isDCCMode ? "DCC" : "DC Analog");
Serial.print("Web interface: http://");
Serial.println(wifiManager.getIPAddress());
Serial.println("=================================\n");
}
void loop() {
// Update WiFi connection status
wifiManager.update();
// Update LED indicators
ledIndicator.update();
// Update DCC signal generation (if enabled)
if (config.system.isDCCMode) {
dccGenerator.update();
} else {
motorController.update();
}
// Web server updates (handled by AsyncWebServer)
webServer.update();
// Small delay to prevent watchdog issues
delay(1);
}