Initialisation depot

This commit is contained in:
Serge NOEL
2026-02-10 12:12:11 +01:00
commit c3176e8d79
818 changed files with 52573 additions and 0 deletions

View File

@@ -0,0 +1,139 @@
/**
* @file AccessoryOutputs.cpp
* @brief Accessory Output Controller Implementation
*/
#include "AccessoryOutputs.h"
AccessoryOutputs::AccessoryOutputs()
: currentSpeed(0), lastBlinkUpdate(0), blinkState(false) {
pins[0] = 0;
pins[1] = 0;
modes[0] = ACC_OFF;
modes[1] = ACC_OFF;
pwmValues[0] = 0;
pwmValues[1] = 0;
mappedFunctions[0] = 255;
mappedFunctions[1] = 255;
memset(functionStates, 0, sizeof(functionStates));
}
bool AccessoryOutputs::begin(uint8_t output1Pin, uint8_t output2Pin) {
pins[0] = output1Pin;
pins[1] = output2Pin;
// Configure pins
pinMode(pins[0], OUTPUT);
pinMode(pins[1], OUTPUT);
// Setup PWM channels
ledcSetup(pwmChannels[0], pwmFrequency, pwmResolution);
ledcSetup(pwmChannels[1], pwmFrequency, pwmResolution);
ledcAttachPin(pins[0], pwmChannels[0]);
ledcAttachPin(pins[1], pwmChannels[1]);
// Initialize outputs to off
ledcWrite(pwmChannels[0], 0);
ledcWrite(pwmChannels[1], 0);
return true;
}
void AccessoryOutputs::setMode(uint8_t outputNum, AccessoryMode mode) {
if (outputNum >= 1 && outputNum <= 2) {
modes[outputNum - 1] = mode;
}
}
void AccessoryOutputs::setPWM(uint8_t outputNum, uint8_t dutyCycle) {
if (outputNum >= 1 && outputNum <= 2) {
pwmValues[outputNum - 1] = dutyCycle;
}
}
void AccessoryOutputs::mapFunction(uint8_t outputNum, uint8_t functionNum) {
if (outputNum >= 1 && outputNum <= 2 && functionNum <= 28) {
mappedFunctions[outputNum - 1] = functionNum;
}
}
void AccessoryOutputs::setFunctionState(uint8_t functionNum, bool state) {
if (functionNum <= 28) {
functionStates[functionNum] = state;
}
}
void AccessoryOutputs::setSpeed(uint8_t speed) {
currentSpeed = speed;
}
void AccessoryOutputs::update() {
unsigned long currentTime = millis();
// Update blink state (1 Hz)
if (currentTime - lastBlinkUpdate >= 500) {
lastBlinkUpdate = currentTime;
blinkState = !blinkState;
}
// Update each output
updateOutput(0);
updateOutput(1);
}
void AccessoryOutputs::updateOutput(uint8_t outputNum) {
if (outputNum >= 2) return;
bool shouldBeOn = false;
uint8_t pwmValue = 255;
switch (modes[outputNum]) {
case ACC_OFF:
shouldBeOn = false;
break;
case ACC_ON:
shouldBeOn = true;
break;
case ACC_FUNCTION:
if (mappedFunctions[outputNum] != 255) {
shouldBeOn = functionStates[mappedFunctions[outputNum]];
}
break;
case ACC_PWM:
shouldBeOn = true;
pwmValue = pwmValues[outputNum];
break;
case ACC_BLINK:
shouldBeOn = blinkState;
break;
case ACC_SPEED_DEPENDENT:
if (currentSpeed >= 2) {
shouldBeOn = true;
// Map speed (2-127) to PWM (0-255)
pwmValue = map(currentSpeed, 2, 127, 0, 255);
} else {
shouldBeOn = false;
}
break;
}
// Apply output
if (shouldBeOn) {
ledcWrite(pwmChannels[outputNum], pwmValue);
} else {
ledcWrite(pwmChannels[outputNum], 0);
}
}
void AccessoryOutputs::setOutput(uint8_t outputNum, bool state) {
if (outputNum >= 1 && outputNum <= 2) {
uint8_t idx = outputNum - 1;
ledcWrite(pwmChannels[idx], state ? 255 : 0);
}
}

View File

@@ -0,0 +1,116 @@
/**
* @file CVManager.cpp
* @brief Configuration Variable Manager Implementation
*/
#include "CVManager.h"
CVManager::CVManager() {}
bool CVManager::begin() {
if (!preferences.begin("dcc-decoder", false)) {
return false;
}
// Check if this is first boot
if (preferences.getUChar("initialized", 0) == 0) {
setDefaultCVs();
preferences.putUChar("initialized", 1);
}
return true;
}
uint8_t CVManager::readCV(uint16_t cvNumber, uint8_t defaultValue) {
if (cvNumber < 1 || cvNumber > MAX_CV_NUMBER) {
return defaultValue;
}
return preferences.getUChar(getCVKey(cvNumber).c_str(), defaultValue);
}
bool CVManager::writeCV(uint16_t cvNumber, uint8_t value) {
if (cvNumber < 1 || cvNumber > MAX_CV_NUMBER) {
return false;
}
return preferences.putUChar(getCVKey(cvNumber).c_str(), value);
}
void CVManager::resetToDefaults() {
preferences.clear();
setDefaultCVs();
preferences.putUChar("initialized", 1);
}
uint16_t CVManager::getLocoAddress() {
// Check CV29 bit 5 to determine address mode
uint8_t cv29 = readCV(CV_CONFIG_DATA_1, 0x06);
if (cv29 & 0x20) {
// Long address mode
uint8_t highByte = readCV(CV_EXTENDED_ADDRESS_HIGH, 0xC0);
uint8_t lowByte = readCV(CV_EXTENDED_ADDRESS_LOW, 0x03);
return ((highByte & 0x3F) << 8) | lowByte;
} else {
// Short address mode
return readCV(CV_PRIMARY_ADDRESS, 3);
}
}
void CVManager::setLocoAddress(uint16_t address) {
if (address >= 1 && address <= 127) {
// Short address
writeCV(CV_PRIMARY_ADDRESS, address);
// Clear long address bit in CV29
uint8_t cv29 = readCV(CV_CONFIG_DATA_1, 0x06);
cv29 &= ~0x20;
writeCV(CV_CONFIG_DATA_1, cv29);
} else if (address >= 128 && address <= 10239) {
// Long address
uint8_t highByte = 0xC0 | ((address >> 8) & 0x3F);
uint8_t lowByte = address & 0xFF;
writeCV(CV_EXTENDED_ADDRESS_HIGH, highByte);
writeCV(CV_EXTENDED_ADDRESS_LOW, lowByte);
// Set long address bit in CV29
uint8_t cv29 = readCV(CV_CONFIG_DATA_1, 0x06);
cv29 |= 0x20;
writeCV(CV_CONFIG_DATA_1, cv29);
}
}
bool CVManager::isLongAddress() {
uint8_t cv29 = readCV(CV_CONFIG_DATA_1, 0x06);
return (cv29 & 0x20) != 0;
}
void CVManager::setDefaultCVs() {
// Standard CVs
writeCV(CV_PRIMARY_ADDRESS, 3); // Default address 3
writeCV(CV_VSTART, 1); // Start voltage
writeCV(CV_ACCEL_RATE, 10); // Acceleration rate
writeCV(CV_DECEL_RATE, 10); // Deceleration rate
writeCV(CV_VHIGH, 255); // Max voltage
writeCV(CV_VMID, 128); // Mid voltage
writeCV(CV_VERSION_ID, 1); // Version 1
writeCV(CV_MANUFACTURER_ID, 13); // DIY decoder
writeCV(CV_TOTAL_PWM_PERIOD, 20); // 20ms PWM period
writeCV(CV_CONFIG_DATA_1, 0x06); // 128 speed steps, short address
// Custom CVs
writeCV(CV_MOTOR_KP, 50); // PID Kp = 5.0
writeCV(CV_MOTOR_KI, 5); // PID Ki = 0.5
writeCV(CV_MOTOR_KD, 10); // PID Kd = 1.0
writeCV(CV_RAILCOM_ENABLE, 1); // RailCom enabled
writeCV(CV_LOAD_COMP_ENABLE, 1); // Load compensation enabled
writeCV(CV_LED_BRIGHTNESS, 128); // 50% brightness
writeCV(CV_ACCESSORY_1_MODE, ACC_FUNCTION); // Function controlled
writeCV(CV_ACCESSORY_2_MODE, ACC_FUNCTION); // Function controlled
}
String CVManager::getCVKey(uint16_t cvNumber) {
return "cv" + String(cvNumber);
}

View File

@@ -0,0 +1,309 @@
/**
* @file ConfigServer.cpp
* @brief WiFi/Bluetooth Configuration Server Implementation
*/
#include "ConfigServer.h"
ConfigServer::ConfigServer(CVManager& cvManager)
: cvMgr(cvManager), server(nullptr), ws(nullptr),
active(false), statusCallback(nullptr), lastStatusUpdate(0) {}
bool ConfigServer::begin(const char* ssid, const char* password, bool useAP) {
// Initialize WiFi
if (useAP) {
// Access Point mode
String apName = ssid ? String(ssid) : getDefaultAPName();
String apPass = password ? String(password) : "dcc12345";
WiFi.softAP(apName.c_str(), apPass.c_str());
Serial.println("WiFi AP started");
Serial.print("AP Name: ");
Serial.println(apName);
Serial.print("IP Address: ");
Serial.println(WiFi.softAPIP());
} else {
// Station mode
if (!ssid) return false;
WiFi.begin(ssid, password);
int attempts = 0;
while (WiFi.status() != WL_CONNECTED && attempts < 20) {
delay(500);
Serial.print(".");
attempts++;
}
if (WiFi.status() != WL_CONNECTED) {
Serial.println("\nWiFi connection failed");
return false;
}
Serial.println("\nWiFi connected");
Serial.print("IP Address: ");
Serial.println(WiFi.localIP());
}
// Create web server
server = new AsyncWebServer(80);
ws = new AsyncWebSocket("/ws");
setupWebSocket();
setupHTTPRoutes();
server->begin();
active = true;
return true;
}
void ConfigServer::stop() {
if (server) {
server->end();
delete server;
server = nullptr;
}
if (ws) {
delete ws;
ws = nullptr;
}
WiFi.disconnect();
active = false;
}
void ConfigServer::update() {
if (!active) return;
// Send periodic status updates
unsigned long currentTime = millis();
if (currentTime - lastStatusUpdate >= 1000) { // Every second
lastStatusUpdate = currentTime;
broadcastStatus();
}
}
void ConfigServer::setupWebSocket() {
ws->onEvent([this](AsyncWebSocket* server, AsyncWebSocketClient* client,
AwsEventType type, void* arg, uint8_t* data, size_t len) {
handleWebSocketEvent(server, client, type, arg, data, len);
});
server->addHandler(ws);
}
void ConfigServer::setupHTTPRoutes() {
// Serve basic HTML page
server->on("/", HTTP_GET, [](AsyncWebServerRequest* request) {
String html = R"html(
<!DOCTYPE html>
<html>
<head>
<title>DCC Loco Decoder Config</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<style>
body { font-family: Arial; margin: 20px; }
.container { max-width: 600px; margin: 0 auto; }
button { padding: 10px 20px; margin: 5px; }
input { padding: 5px; margin: 5px; }
.status { background: #f0f0f0; padding: 10px; margin: 10px 0; }
</style>
</head>
<body>
<div class="container">
<h1>DCC Locomotive Decoder</h1>
<div class="status" id="status">Connecting...</div>
<h2>Configuration Variables</h2>
<div>
<label>CV Number: <input type="number" id="cvNum" min="1" max="1024" value="1"></label>
<button onclick="readCV()">Read CV</button>
</div>
<div>
<label>CV Value: <input type="number" id="cvVal" min="0" max="255" value="0"></label>
<button onclick="writeCV()">Write CV</button>
</div>
<div id="cvResult"></div>
<h2>Actions</h2>
<button onclick="resetDecoder()">Reset to Defaults</button>
</div>
<script>
const ws = new WebSocket('ws://' + location.host + '/ws');
ws.onmessage = (event) => {
const msg = JSON.parse(event.data);
console.log('Received:', msg);
if (msg.type === 'status') {
document.getElementById('status').innerHTML =
'Address: ' + msg.address + '<br>' +
'Speed: ' + msg.speed + '<br>' +
'Direction: ' + (msg.direction ? 'Forward' : 'Reverse');
} else if (msg.type === 'cv_read') {
document.getElementById('cvResult').innerHTML =
'CV' + msg.cv + ' = ' + msg.value;
} else if (msg.type === 'cv_write') {
document.getElementById('cvResult').innerHTML =
msg.success ? 'CV written successfully' : 'Write failed';
}
};
function readCV() {
const cv = parseInt(document.getElementById('cvNum').value);
ws.send(JSON.stringify({command: 'read_cv', cv: cv}));
}
function writeCV() {
const cv = parseInt(document.getElementById('cvNum').value);
const value = parseInt(document.getElementById('cvVal').value);
ws.send(JSON.stringify({command: 'write_cv', cv: cv, value: value}));
}
function resetDecoder() {
if (confirm('Reset all CVs to defaults?')) {
ws.send(JSON.stringify({command: 'reset'}));
}
}
</script>
</body>
</html>
)html";
request->send(200, "text/html", html);
});
}
void ConfigServer::handleWebSocketEvent(AsyncWebSocket* server, AsyncWebSocketClient* client,
AwsEventType type, void* arg, uint8_t* data, size_t len) {
if (type == WS_EVT_CONNECT) {
Serial.printf("WebSocket client #%u connected\n", client->id());
handleGetStatus(client);
} else if (type == WS_EVT_DISCONNECT) {
Serial.printf("WebSocket client #%u disconnected\n", client->id());
} else if (type == WS_EVT_DATA) {
AwsFrameInfo* info = (AwsFrameInfo*)arg;
if (info->final && info->index == 0 && info->len == len && info->opcode == WS_TEXT) {
data[len] = 0;
handleWebSocketMessage(client, data, len);
}
}
}
void ConfigServer::handleWebSocketMessage(void* clientPtr, uint8_t* data, size_t len) {
AsyncWebSocketClient* client = (AsyncWebSocketClient*)clientPtr;
StaticJsonDocument<256> doc;
DeserializationError error = deserializeJson(doc, data, len);
if (error) {
Serial.println("JSON parse error");
return;
}
const char* command = doc["command"];
if (strcmp(command, "read_cv") == 0) {
handleReadCV(client, doc.as<JsonObject>());
} else if (strcmp(command, "write_cv") == 0) {
handleWriteCV(client, doc.as<JsonObject>());
} else if (strcmp(command, "get_status") == 0) {
handleGetStatus(client);
} else if (strcmp(command, "reset") == 0) {
handleReset(client);
}
}
void ConfigServer::handleReadCV(AsyncWebSocketClient* client, JsonObject& json) {
uint16_t cvNum = json["cv"];
uint8_t value = cvMgr.readCV(cvNum, 0);
StaticJsonDocument<128> response;
response["type"] = "cv_read";
response["cv"] = cvNum;
response["value"] = value;
String output;
serializeJson(response, output);
client->text(output);
}
void ConfigServer::handleWriteCV(AsyncWebSocketClient* client, JsonObject& json) {
uint16_t cvNum = json["cv"];
uint8_t value = json["value"];
bool success = cvMgr.writeCV(cvNum, value);
StaticJsonDocument<128> response;
response["type"] = "cv_write";
response["success"] = success;
String output;
serializeJson(response, output);
client->text(output);
}
void ConfigServer::handleGetStatus(AsyncWebSocketClient* client) {
StaticJsonDocument<256> response;
JsonObject status = response.to<JsonObject>();
status["type"] = "status";
if (statusCallback) {
statusCallback(status);
} else {
status["address"] = cvMgr.getLocoAddress();
status["speed"] = 0;
status["direction"] = true;
}
String output;
serializeJson(response, output);
client->text(output);
}
void ConfigServer::handleReset(AsyncWebSocketClient* client) {
cvMgr.resetToDefaults();
sendResponse(client, "reset", true, "Decoder reset to defaults");
}
void ConfigServer::sendResponse(AsyncWebSocketClient* client, const char* type,
bool success, const char* message) {
StaticJsonDocument<128> response;
response["type"] = type;
response["success"] = success;
if (message) {
response["message"] = message;
}
String output;
serializeJson(response, output);
client->text(output);
}
void ConfigServer::broadcastStatus() {
if (!ws || ws->count() == 0) return;
StaticJsonDocument<256> response;
JsonObject status = response.to<JsonObject>();
status["type"] = "status";
if (statusCallback) {
statusCallback(status);
} else {
status["address"] = cvMgr.getLocoAddress();
}
String output;
serializeJson(response, output);
ws->textAll(output);
}
void ConfigServer::setStatusCallback(StatusCallback callback) {
statusCallback = callback;
}
String ConfigServer::getDefaultAPName() {
uint64_t chipid = ESP.getEfuseMac();
return "DCC-Loco-" + String((uint32_t)(chipid >> 32), HEX);
}

View File

@@ -0,0 +1,232 @@
/**
* @file DCCDecoder.cpp
* @brief DCC Signal Decoder Implementation
*/
#include "DCCDecoder.h"
DCCDecoder* DCCDecoder::instance = nullptr;
DCCDecoder::DCCDecoder()
: dccInputPin(0), locoAddress(3), currentSpeed(0), direction(true),
functions(0), packetIndex(0), bitCount(0), assemblingPacket(false),
lastBitTime(0), lastValidPacket(0) {
instance = this;
}
bool DCCDecoder::begin(uint8_t dccPin) {
dccInputPin = dccPin;
pinMode(dccInputPin, INPUT);
// Attach interrupt for DCC signal
attachInterrupt(digitalPinToInterrupt(dccInputPin), dccISR, CHANGE);
return true;
}
void IRAM_ATTR DCCDecoder::dccISR() {
if (instance) {
instance->process();
}
}
void DCCDecoder::process() {
unsigned long currentTime = micros();
unsigned long pulseDuration = currentTime - lastBitTime;
lastBitTime = currentTime;
// Check for DCC ONE bit (52-64 µs)
if (pulseDuration >= DCC_ONE_BIT_MIN && pulseDuration <= DCC_ONE_BIT_MAX) {
if (assemblingPacket) {
// Shift in a '1' bit
packetBuffer[packetIndex] = (packetBuffer[packetIndex] << 1) | 1;
bitCount++;
if (bitCount >= 8) {
packetIndex++;
bitCount = 0;
if (packetIndex >= MAX_DCC_PACKET_SIZE) {
assemblingPacket = false;
}
}
} else {
// Preamble bit
if (pulseDuration >= DCC_ONE_BIT_MIN) {
// Start of new packet after preamble
packetIndex = 0;
bitCount = 0;
assemblingPacket = true;
memset(packetBuffer, 0, sizeof(packetBuffer));
}
}
}
// Check for DCC ZERO bit (95-9900 µs)
else if (pulseDuration >= DCC_ZERO_BIT_MIN && pulseDuration <= DCC_ZERO_BIT_MAX) {
if (assemblingPacket) {
// Shift in a '0' bit
packetBuffer[packetIndex] = (packetBuffer[packetIndex] << 1);
bitCount++;
if (bitCount >= 8) {
packetIndex++;
bitCount = 0;
// Check for end of packet (more than 3 bytes minimum)
if (packetIndex >= 3) {
decodeDCCPacket();
assemblingPacket = false;
}
if (packetIndex >= MAX_DCC_PACKET_SIZE) {
assemblingPacket = false;
}
}
}
}
}
void DCCDecoder::decodeDCCPacket() {
// Validate checksum
uint8_t checksum = 0;
for (uint8_t i = 0; i < packetIndex - 1; i++) {
checksum ^= packetBuffer[i];
}
if (checksum != packetBuffer[packetIndex - 1]) {
return; // Invalid packet
}
lastValidPacket = millis();
// Check address
uint16_t packetAddress;
uint8_t dataStart;
if (packetBuffer[0] == 0xFF) {
// Idle packet
return;
} else if (packetBuffer[0] >= 0xC0) {
// Long address (14-bit)
packetAddress = ((packetBuffer[0] & 0x3F) << 8) | packetBuffer[1];
dataStart = 2;
} else if (packetBuffer[0] >= 1 && packetBuffer[0] <= 127) {
// Short address (7-bit)
packetAddress = packetBuffer[0];
dataStart = 1;
} else {
return; // Invalid address
}
// Check if packet is for this decoder
if (packetAddress != locoAddress && packetAddress != 0) {
return; // Not for us (0 = broadcast)
}
// Process instruction byte
uint8_t instruction = packetBuffer[dataStart];
if ((instruction & 0xC0) == 0x40) {
// Speed and direction (01DCSSSS or 001DSSSS for 14/28 step)
processSpeedPacket(&packetBuffer[dataStart], packetIndex - dataStart - 1);
} else if ((instruction & 0xE0) == 0x80) {
// Function group (100XXXXX)
processFunctionPacket(&packetBuffer[dataStart], packetIndex - dataStart - 1);
} else if ((instruction & 0xF0) == 0xA0) {
// Function group (1011XXXX)
processFunctionPacket(&packetBuffer[dataStart], packetIndex - dataStart - 1);
} else if ((instruction & 0xE0) == 0xC0) {
// Feature expansion
processFunctionPacket(&packetBuffer[dataStart], packetIndex - dataStart - 1);
}
}
void DCCDecoder::processSpeedPacket(uint8_t* data, uint8_t len) {
if (len < 1) return;
uint8_t instruction = data[0];
// Check for 128-step speed (0x3F = 00111111)
if ((instruction & 0xC0) == 0x40) {
// 01DCSSSS (14/28 step mode)
direction = (instruction & 0x20) ? true : false;
uint8_t speedBits = instruction & 0x0F;
if (len >= 2) {
// 128 step mode: second byte contains speed
currentSpeed = data[1] & 0x7F;
} else {
// 14/28 step mode
currentSpeed = speedBits * 9; // Approximate scaling
}
} else if ((instruction & 0xE0) == 0x20) {
// 001DSSSS (reverse operation control)
direction = (instruction & 0x10) ? true : false;
}
}
void DCCDecoder::processFunctionPacket(uint8_t* data, uint8_t len) {
if (len < 1) return;
uint8_t instruction = data[0];
if ((instruction & 0xF0) == 0x80) {
// 100DDDDD - Function group 1 (F0-F4)
if (instruction & 0x10) functions |= (1 << 0); else functions &= ~(1 << 0); // F0
if (instruction & 0x01) functions |= (1 << 1); else functions &= ~(1 << 1); // F1
if (instruction & 0x02) functions |= (1 << 2); else functions &= ~(1 << 2); // F2
if (instruction & 0x04) functions |= (1 << 3); else functions &= ~(1 << 3); // F3
if (instruction & 0x08) functions |= (1 << 4); else functions &= ~(1 << 4); // F4
} else if ((instruction & 0xF0) == 0xB0) {
// 1011DDDD - Function group 2 (F5-F8)
if (instruction & 0x01) functions |= (1 << 5); else functions &= ~(1 << 5); // F5
if (instruction & 0x02) functions |= (1 << 6); else functions &= ~(1 << 6); // F6
if (instruction & 0x04) functions |= (1 << 7); else functions &= ~(1 << 7); // F7
if (instruction & 0x08) functions |= (1 << 8); else functions &= ~(1 << 8); // F8
} else if ((instruction & 0xF0) == 0xA0) {
// 1010DDDD - Function group 3 (F9-F12)
if (instruction & 0x01) functions |= (1 << 9); else functions &= ~(1 << 9); // F9
if (instruction & 0x02) functions |= (1 << 10); else functions &= ~(1 << 10); // F10
if (instruction & 0x04) functions |= (1 << 11); else functions &= ~(1 << 11); // F11
if (instruction & 0x08) functions |= (1 << 12); else functions &= ~(1 << 12); // F12
} else if (instruction == 0xDE) {
// F13-F20
if (len >= 2) {
uint8_t funcByte = data[1];
for (uint8_t i = 0; i < 8; i++) {
if (funcByte & (1 << i)) {
functions |= (1 << (13 + i));
} else {
functions &= ~(1 << (13 + i));
}
}
}
} else if (instruction == 0xDF) {
// F21-F28
if (len >= 2) {
uint8_t funcByte = data[1];
for (uint8_t i = 0; i < 8; i++) {
if (funcByte & (1 << i)) {
functions |= (1 << (21 + i));
} else {
functions &= ~(1 << (21 + i));
}
}
}
}
}
bool DCCDecoder::getFunction(uint8_t functionNum) const {
if (functionNum > 28) return false;
return (functions & (1 << functionNum)) != 0;
}
bool DCCDecoder::hasValidSignal() const {
return (millis() - lastValidPacket) < 1000; // Valid if packet within last second
}
void DCCDecoder::setAddress(uint16_t address) {
if (address >= 1 && address <= 10239) {
locoAddress = address;
}
}

View File

@@ -0,0 +1,132 @@
/**
* @file LEDController.cpp
* @brief WS2812 LED Controller Implementation
*/
#include "LEDController.h"
LEDController::LEDController()
: numLEDs(0), dataPin(0), direction(true), lastUpdate(0), effectCounter(0) {
memset(functionStates, 0, sizeof(functionStates));
for (uint8_t i = 0; i < MAX_LEDS; i++) {
ledConfig[i].mode = LIGHT_OFF;
ledConfig[i].color = CRGB::White;
ledConfig[i].mappedFunction = 255;
}
}
bool LEDController::begin(uint8_t ledPin, uint8_t numLeds) {
if (numLeds > MAX_LEDS) {
numLeds = MAX_LEDS;
}
dataPin = ledPin;
numLEDs = numLeds;
// Initialize FastLED
FastLED.addLeds<WS2812, dataPin, GRB>(leds, numLEDs);
FastLED.setBrightness(DEFAULT_BRIGHTNESS);
FastLED.clear();
FastLED.show();
return true;
}
void LEDController::update() {
unsigned long currentTime = millis();
if (currentTime - lastUpdate >= 20) { // Update at ~50 Hz
lastUpdate = currentTime;
effectCounter++;
for (uint8_t i = 0; i < numLEDs; i++) {
updateLED(i);
}
FastLED.show();
}
}
void LEDController::updateLED(uint8_t ledIndex) {
if (ledIndex >= numLEDs) return;
LEDConfig& config = ledConfig[ledIndex];
bool shouldBeOn = false;
// Determine if LED should be on based on mode
switch (config.mode) {
case LIGHT_OFF:
shouldBeOn = false;
break;
case LIGHT_ON:
shouldBeOn = true;
break;
case LIGHT_BLINK:
shouldBeOn = (effectCounter % 50) < 25; // ~1 Hz blink
break;
case LIGHT_PULSE:
{
uint8_t brightness = (sin8(effectCounter * 5) >> 1) + 128;
leds[ledIndex] = config.color;
leds[ledIndex].fadeToBlackBy(255 - brightness);
return;
}
case LIGHT_DIRECTION_FRONT:
shouldBeOn = direction;
break;
case LIGHT_DIRECTION_REAR:
shouldBeOn = !direction;
break;
}
// Check function mapping
if (config.mappedFunction != 255) {
shouldBeOn = shouldBeOn && functionStates[config.mappedFunction];
}
// Set LED color
if (shouldBeOn) {
leds[ledIndex] = config.color;
} else {
leds[ledIndex] = CRGB::Black;
}
}
void LEDController::setLEDMode(uint8_t ledIndex, LightMode mode) {
if (ledIndex < MAX_LEDS) {
ledConfig[ledIndex].mode = mode;
}
}
void LEDController::setLEDColor(uint8_t ledIndex, uint8_t r, uint8_t g, uint8_t b) {
if (ledIndex < MAX_LEDS) {
ledConfig[ledIndex].color = CRGB(r, g, b);
}
}
void LEDController::setBrightness(uint8_t brightness) {
FastLED.setBrightness(brightness);
}
void LEDController::setDirection(bool forward) {
direction = forward;
}
void LEDController::mapFunctionToLED(uint8_t functionNum, uint8_t ledIndex, LightMode mode) {
if (ledIndex < MAX_LEDS && functionNum <= 28) {
ledConfig[ledIndex].mappedFunction = functionNum;
ledConfig[ledIndex].mode = mode;
}
}
void LEDController::setFunctionState(uint8_t functionNum, bool state) {
if (functionNum <= 28) {
functionStates[functionNum] = state;
}
}

View File

@@ -0,0 +1,213 @@
/**
* @file MotorDriver.cpp
* @brief TB67H450FNG Motor Driver Implementation
*/
#include "MotorDriver.h"
MotorDriver::MotorDriver()
: pinIN1(0), pinIN2(0), pinPWM(0), pinCurrentSense(255),
targetSpeed(0), currentSpeed(0), targetDirection(true),
loadCompensationEnabled(false), accelRate(10), decelRate(10),
lastSpeedUpdate(0), Kp(1.0), Ki(0.1), Kd(0.5),
integral(0), lastError(0), targetCurrent(0) {}
bool MotorDriver::begin(uint8_t in1Pin, uint8_t in2Pin, uint8_t pwmPin, uint8_t currentSensePin) {
pinIN1 = in1Pin;
pinIN2 = in2Pin;
pinPWM = pwmPin;
pinCurrentSense = currentSensePin;
// Configure pins
pinMode(pinIN1, OUTPUT);
pinMode(pinIN2, OUTPUT);
pinMode(pinPWM, OUTPUT);
if (pinCurrentSense != 255) {
pinMode(pinCurrentSense, INPUT);
}
// Setup PWM
ledcSetup(pwmChannel, pwmFrequency, pwmResolution);
ledcAttachPin(pinPWM, pwmChannel);
ledcWrite(pwmChannel, 0);
// Set initial state (brake)
digitalWrite(pinIN1, LOW);
digitalWrite(pinIN2, LOW);
return true;
}
void MotorDriver::setSpeed(uint8_t speed, bool forward) {
targetSpeed = speed;
targetDirection = forward;
}
void MotorDriver::emergencyStop() {
targetSpeed = 0;
currentSpeed = 0;
// Apply brake
digitalWrite(pinIN1, HIGH);
digitalWrite(pinIN2, HIGH);
ledcWrite(pwmChannel, 0);
}
void MotorDriver::update() {
updateAcceleration();
if (loadCompensationEnabled && pinCurrentSense != 255) {
updateLoadCompensation();
} else {
applyMotorControl();
}
}
void MotorDriver::updateAcceleration() {
unsigned long currentTime = millis();
if (currentTime - lastSpeedUpdate < 50) {
return; // Update every 50ms
}
lastSpeedUpdate = currentTime;
if (currentSpeed < targetSpeed) {
// Accelerate
uint8_t delta = (accelRate > 0) ? (255 / accelRate) : 1;
if (currentSpeed + delta >= targetSpeed) {
currentSpeed = targetSpeed;
} else {
currentSpeed += delta;
}
} else if (currentSpeed > targetSpeed) {
// Decelerate
uint8_t delta = (decelRate > 0) ? (255 / decelRate) : 1;
if (currentSpeed <= delta || currentSpeed - delta <= targetSpeed) {
currentSpeed = targetSpeed;
} else {
currentSpeed -= delta;
}
}
}
void MotorDriver::applyMotorControl() {
if (currentSpeed == 0) {
// Stop/brake
digitalWrite(pinIN1, LOW);
digitalWrite(pinIN2, LOW);
ledcWrite(pwmChannel, 0);
return;
}
if (currentSpeed == 1) {
// Emergency stop (brake)
digitalWrite(pinIN1, HIGH);
digitalWrite(pinIN2, HIGH);
ledcWrite(pwmChannel, 0);
return;
}
// Map DCC speed (2-127) to PWM (0-255)
uint16_t pwmValue = map(currentSpeed, 2, 127, 0, 255);
// Set direction
if (targetDirection) {
// Forward
digitalWrite(pinIN1, HIGH);
digitalWrite(pinIN2, LOW);
} else {
// Reverse
digitalWrite(pinIN1, LOW);
digitalWrite(pinIN2, HIGH);
}
// Set PWM
ledcWrite(pwmChannel, pwmValue);
}
void MotorDriver::updateLoadCompensation() {
// Read current
uint16_t currentMa = readCurrent();
// PID control
float error = targetCurrent - currentMa;
integral += error;
// Anti-windup
if (integral > 1000) integral = 1000;
if (integral < -1000) integral = -1000;
float derivative = error - lastError;
lastError = error;
float correction = (Kp * error) + (Ki * integral) + (Kd * derivative);
// Apply correction to PWM
if (currentSpeed == 0 || currentSpeed == 1) {
applyMotorControl();
return;
}
uint16_t basePwm = map(currentSpeed, 2, 127, 0, 255);
int16_t adjustedPwm = basePwm + (int16_t)correction;
// Clamp PWM
if (adjustedPwm < 0) adjustedPwm = 0;
if (adjustedPwm > 255) adjustedPwm = 255;
// Set direction
if (targetDirection) {
digitalWrite(pinIN1, HIGH);
digitalWrite(pinIN2, LOW);
} else {
digitalWrite(pinIN1, LOW);
digitalWrite(pinIN2, HIGH);
}
ledcWrite(pwmChannel, adjustedPwm);
}
uint16_t MotorDriver::readCurrent() {
if (pinCurrentSense == 255) {
return 0;
}
// Read ADC value
uint16_t adcValue = analogRead(pinCurrentSense);
// Convert to milliamps (this is hardware dependent)
// Assuming 3.3V reference, 12-bit ADC, and current sense amplifier
// Adjust this based on your actual hardware
uint16_t currentMa = (adcValue * 3300) / 4096; // Simplified conversion
return currentMa;
}
uint16_t MotorDriver::getMotorCurrent() {
return readCurrent();
}
void MotorDriver::setLoadCompensation(bool enable) {
loadCompensationEnabled = enable;
if (enable) {
integral = 0;
lastError = 0;
}
}
void MotorDriver::setPIDParameters(float kp, float ki, float kd) {
Kp = kp;
Ki = ki;
Kd = kd;
}
void MotorDriver::setAccelRate(uint8_t rate) {
accelRate = rate;
}
void MotorDriver::setDecelRate(uint8_t rate) {
decelRate = rate;
}

View File

@@ -0,0 +1,127 @@
/**
* @file RailCom.cpp
* @brief RailCom Feedback Implementation
*/
#include "RailCom.h"
// RailCom 4-to-8 bit encoding table
const uint8_t RailCom::railcom4to8[16] = {
0xAC, 0xE5, 0xD3, 0xF0,
0x99, 0xCC, 0xB4, 0x78,
0xA3, 0xE1, 0xD5, 0xF2,
0x9A, 0xCA, 0xB8, 0x71
};
RailCom::RailCom()
: txPin(0), cutoutPin(255), enabled(false), locoAddress(3),
currentSpeed(0), currentDirection(true), railcomSerial(nullptr),
lastCutoutTime(0), inCutout(false) {}
bool RailCom::begin(uint8_t txPinNum, uint8_t cutoutDetectPin) {
txPin = txPinNum;
cutoutPin = cutoutDetectPin;
// Initialize UART for RailCom
// RailCom uses 250kbaud, 8N1
railcomSerial = &Serial1;
railcomSerial->begin(250000, SERIAL_8N1, -1, txPin); // RX not used
if (cutoutPin != 255) {
pinMode(cutoutPin, INPUT);
}
enabled = true;
return true;
}
void RailCom::setEnabled(bool enable) {
enabled = enable;
}
void RailCom::setAddress(uint16_t address) {
locoAddress = address;
}
void RailCom::setDecoderState(uint8_t speed, bool direction) {
currentSpeed = speed;
currentDirection = direction;
}
void RailCom::update() {
if (!enabled) return;
// Check for cutout signal if pin is configured
if (cutoutPin != 255) {
bool cutoutDetected = digitalRead(cutoutPin) == LOW;
if (cutoutDetected && !inCutout) {
// Rising edge of cutout
inCutout = true;
lastCutoutTime = micros();
sendRailComData();
} else if (!cutoutDetected && inCutout) {
// Falling edge of cutout
inCutout = false;
}
}
}
void RailCom::sendRailComData() {
if (!enabled || !railcomSerial) return;
// Wait for Channel 1 window (26-177 µs)
delayMicroseconds(RAILCOM_CHANNEL1_START);
sendChannel1();
// Wait for Channel 2 window (193-454 µs)
unsigned long elapsed = micros() - lastCutoutTime;
if (elapsed < RAILCOM_CHANNEL2_START) {
delayMicroseconds(RAILCOM_CHANNEL2_START - elapsed);
}
sendChannel2();
}
void RailCom::sendChannel1() {
// Channel 1: Send locomotive address (ID)
// Format: 2 bytes encoded with 4-to-8 encoding
uint8_t addrLow = locoAddress & 0x0F;
uint8_t addrHigh = (locoAddress >> 4) & 0x0F;
uint8_t byte1 = encode4to8(addrHigh);
uint8_t byte2 = encode4to8(addrLow);
railcomSerial->write(byte1);
railcomSerial->write(byte2);
railcomSerial->flush();
}
void RailCom::sendChannel2() {
// Channel 2: Send status/data
// We can send speed, function states, etc.
// For simplicity, send basic status
// Bit 0-6: Speed (0-127)
// Bit 7: Direction
uint8_t statusByte = currentSpeed & 0x7F;
if (currentDirection) {
statusByte |= 0x80;
}
uint8_t dataLow = statusByte & 0x0F;
uint8_t dataHigh = (statusByte >> 4) & 0x0F;
uint8_t byte1 = encode4to8(dataHigh);
uint8_t byte2 = encode4to8(dataLow);
railcomSerial->write(byte1);
railcomSerial->write(byte2);
railcomSerial->flush();
}
uint8_t RailCom::encode4to8(uint8_t data) {
if (data >= 16) return 0xAC; // Invalid, return first code
return railcom4to8[data];
}

344
ESP32/DCC-Loco/src/main.cpp Normal file
View File

@@ -0,0 +1,344 @@
/**
* @file main.cpp
* @brief DCC Locomotive Decoder - Main Entry Point
*
* ESP32-H2 based DCC decoder with:
* - DCC signal decoding
* - TB67H450FNG motor control with load compensation
* - WS2812 LED control
* - RailCom feedback
* - 2x N-FET accessory outputs
* - WiFi/Bluetooth configuration via WebSocket
*/
#include <Arduino.h>
#include "DCCDecoder.h"
#include "CVManager.h"
#include "LEDController.h"
#include "MotorDriver.h"
#include "RailCom.h"
#include "AccessoryOutputs.h"
#include "ConfigServer.h"
// ==================== PIN DEFINITIONS ====================
// Adjust these based on your hardware wiring
// DCC Input
#define PIN_DCC_INPUT 4 // DCC signal input (with optocoupler)
// Motor Driver (TB67H450FNG)
#define PIN_MOTOR_IN1 5 // Motor phase A
#define PIN_MOTOR_IN2 6 // Motor phase B
#define PIN_MOTOR_PWM 7 // PWM speed control
#define PIN_MOTOR_CURRENT 8 // Current sense (ADC)
// WS2812 LEDs
#define PIN_LED_DATA 9 // WS2812 data line
#define NUM_LEDS 4 // Number of LEDs in strip
// RailCom
#define PIN_RAILCOM_TX 10 // RailCom transmit (UART1 TX)
#define PIN_RAILCOM_CUTOUT 11 // DCC cutout detection (optional)
// Accessory Outputs (N-FETs)
#define PIN_ACCESSORY_1 12 // Accessory output 1
#define PIN_ACCESSORY_2 13 // Accessory output 2
// Configuration Button
#define PIN_CONFIG_BUTTON 14 // Hold to enter config mode
// ==================== GLOBAL OBJECTS ====================
DCCDecoder dccDecoder;
CVManager cvManager;
LEDController ledController;
MotorDriver motorDriver;
RailCom railCom;
AccessoryOutputs accessories;
ConfigServer* configServer = nullptr;
// ==================== STATE VARIABLES ====================
bool configMode = false;
unsigned long configButtonPressTime = 0;
const unsigned long CONFIG_HOLD_TIME = 3000; // 3 seconds to enter config mode
// ==================== FUNCTION DECLARATIONS ====================
void checkConfigButton();
void enterConfigMode();
void exitConfigMode();
void updateDecoderStatus(JsonObject& status);
void syncStatesFromDecoder();
// ==================== SETUP ====================
void setup() {
Serial.begin(115200);
delay(1000);
Serial.println("\n\n======================================");
Serial.println("DCC Locomotive Decoder");
Serial.println("ESP32-H2 Version 1.0");
Serial.println("======================================\n");
// Initialize configuration button
pinMode(PIN_CONFIG_BUTTON, INPUT_PULLUP);
// Initialize CV Manager
Serial.print("Initializing CV Manager... ");
if (cvManager.begin()) {
Serial.println("OK");
} else {
Serial.println("FAILED");
}
// Load configuration from CVs
uint16_t locoAddress = cvManager.getLocoAddress();
uint8_t accelRate = cvManager.readCV(CV_ACCEL_RATE, 10);
uint8_t decelRate = cvManager.readCV(CV_DECEL_RATE, 10);
uint8_t ledBrightness = cvManager.readCV(CV_LED_BRIGHTNESS, 128);
bool railComEnabled = cvManager.readCV(CV_RAILCOM_ENABLE, 1) != 0;
bool loadCompEnabled = cvManager.readCV(CV_LOAD_COMP_ENABLE, 1) != 0;
Serial.println("\nConfiguration:");
Serial.printf(" Locomotive Address: %d %s\n", locoAddress,
cvManager.isLongAddress() ? "(Long)" : "(Short)");
Serial.printf(" Accel Rate: %d\n", accelRate);
Serial.printf(" Decel Rate: %d\n", decelRate);
Serial.printf(" RailCom: %s\n", railComEnabled ? "Enabled" : "Disabled");
Serial.printf(" Load Compensation: %s\n", loadCompEnabled ? "Enabled" : "Disabled");
// Initialize DCC Decoder
Serial.print("\nInitializing DCC Decoder... ");
if (dccDecoder.begin(PIN_DCC_INPUT)) {
dccDecoder.setAddress(locoAddress);
Serial.println("OK");
} else {
Serial.println("FAILED");
}
// Initialize Motor Driver
Serial.print("Initializing Motor Driver... ");
if (motorDriver.begin(PIN_MOTOR_IN1, PIN_MOTOR_IN2, PIN_MOTOR_PWM, PIN_MOTOR_CURRENT)) {
motorDriver.setAccelRate(accelRate);
motorDriver.setDecelRate(decelRate);
motorDriver.setLoadCompensation(loadCompEnabled);
// Load PID parameters from CVs
float kp = cvManager.readCV(CV_MOTOR_KP, 50) / 10.0;
float ki = cvManager.readCV(CV_MOTOR_KI, 5) / 10.0;
float kd = cvManager.readCV(CV_MOTOR_KD, 10) / 10.0;
motorDriver.setPIDParameters(kp, ki, kd);
Serial.println("OK");
} else {
Serial.println("FAILED");
}
// Initialize LED Controller
Serial.print("Initializing LED Controller... ");
if (ledController.begin(PIN_LED_DATA, NUM_LEDS)) {
ledController.setBrightness(ledBrightness);
// Configure default LED mappings
ledController.setLEDMode(0, LIGHT_DIRECTION_FRONT); // Front headlight
ledController.setLEDColor(0, 255, 255, 200); // Warm white
ledController.setLEDMode(1, LIGHT_DIRECTION_REAR); // Rear headlight
ledController.setLEDColor(1, 255, 0, 0); // Red
ledController.mapFunctionToLED(1, 2, LIGHT_ON); // F1 -> LED 2
ledController.mapFunctionToLED(2, 3, LIGHT_BLINK); // F2 -> LED 3 (blink)
Serial.println("OK");
} else {
Serial.println("FAILED");
}
// Initialize RailCom
if (railComEnabled) {
Serial.print("Initializing RailCom... ");
if (railCom.begin(PIN_RAILCOM_TX, PIN_RAILCOM_CUTOUT)) {
railCom.setAddress(locoAddress);
Serial.println("OK");
} else {
Serial.println("FAILED");
}
} else {
Serial.println("RailCom disabled");
}
// Initialize Accessory Outputs
Serial.print("Initializing Accessory Outputs... ");
if (accessories.begin(PIN_ACCESSORY_1, PIN_ACCESSORY_2)) {
// Load modes from CVs
AccessoryMode mode1 = (AccessoryMode)cvManager.readCV(CV_ACCESSORY_1_MODE, ACC_FUNCTION);
AccessoryMode mode2 = (AccessoryMode)cvManager.readCV(CV_ACCESSORY_2_MODE, ACC_FUNCTION);
accessories.setMode(1, mode1);
accessories.setMode(2, mode2);
// Map to functions F3 and F4 by default
accessories.mapFunction(1, 3); // F3 -> Output 1
accessories.mapFunction(2, 4); // F4 -> Output 2
Serial.println("OK");
} else {
Serial.println("FAILED");
}
Serial.println("\n======================================");
Serial.println("Decoder Ready!");
Serial.println("Waiting for DCC signal...");
Serial.println("Hold CONFIG button for 3s to enter");
Serial.println("configuration mode.");
Serial.println("======================================\n");
}
// ==================== MAIN LOOP ====================
void loop() {
// Check for configuration mode entry
checkConfigButton();
if (configMode) {
// Configuration mode - just update the config server
if (configServer) {
configServer->update();
}
delay(10);
return;
}
// ==================== NORMAL OPERATION MODE ====================
// Sync decoder states
syncStatesFromDecoder();
// Update all modules
motorDriver.update();
ledController.update();
railCom.update();
accessories.update();
// Periodic status output (every 5 seconds)
static unsigned long lastStatusPrint = 0;
if (millis() - lastStatusPrint >= 5000) {
lastStatusPrint = millis();
if (dccDecoder.hasValidSignal()) {
Serial.printf("DCC OK | Addr:%d | Speed:%d | Dir:%s | Current:%dmA\n",
dccDecoder.getAddress(),
dccDecoder.getSpeed(),
dccDecoder.getDirection() ? "FWD" : "REV",
motorDriver.getMotorCurrent());
} else {
Serial.println("No DCC signal detected");
}
}
delay(1); // Small delay to prevent watchdog issues
}
// ==================== HELPER FUNCTIONS ====================
void checkConfigButton() {
bool buttonPressed = (digitalRead(PIN_CONFIG_BUTTON) == LOW);
if (buttonPressed) {
if (configButtonPressTime == 0) {
configButtonPressTime = millis();
} else if (!configMode && (millis() - configButtonPressTime >= CONFIG_HOLD_TIME)) {
enterConfigMode();
}
} else {
// Button released
if (configMode && configButtonPressTime > 0) {
// Short press in config mode = exit
exitConfigMode();
}
configButtonPressTime = 0;
}
}
void enterConfigMode() {
configMode = true;
Serial.println("\n======================================");
Serial.println("ENTERING CONFIGURATION MODE");
Serial.println("======================================");
// Stop motor
motorDriver.emergencyStop();
// Create and start configuration server
configServer = new ConfigServer(cvManager);
configServer->setStatusCallback(updateDecoderStatus);
// Start in AP mode with default name
if (configServer->begin(nullptr, nullptr, true)) {
Serial.println("Configuration server started");
Serial.println("Connect to WiFi AP to configure");
Serial.println("Press CONFIG button again to exit");
} else {
Serial.println("Failed to start configuration server");
delete configServer;
configServer = nullptr;
configMode = false;
}
}
void exitConfigMode() {
Serial.println("\n======================================");
Serial.println("EXITING CONFIGURATION MODE");
Serial.println("======================================\n");
if (configServer) {
configServer->stop();
delete configServer;
configServer = nullptr;
}
// Reload configuration
uint16_t newAddress = cvManager.getLocoAddress();
dccDecoder.setAddress(newAddress);
railCom.setAddress(newAddress);
uint8_t newBrightness = cvManager.readCV(CV_LED_BRIGHTNESS, 128);
ledController.setBrightness(newBrightness);
configMode = false;
Serial.println("Decoder ready - waiting for DCC signal");
}
void updateDecoderStatus(JsonObject& status) {
status["address"] = dccDecoder.getAddress();
status["speed"] = dccDecoder.getSpeed();
status["direction"] = dccDecoder.getDirection();
status["signal"] = dccDecoder.hasValidSignal();
status["current"] = motorDriver.getMotorCurrent();
// Add function states
JsonArray functions = status.createNestedArray("functions");
for (uint8_t i = 0; i <= 12; i++) {
functions.add(dccDecoder.getFunction(i));
}
}
void syncStatesFromDecoder() {
// Get current state from DCC decoder
uint8_t speed = dccDecoder.getSpeed();
bool direction = dccDecoder.getDirection();
// Update motor
motorDriver.setSpeed(speed, direction);
// Update LEDs
ledController.setDirection(direction);
for (uint8_t i = 0; i <= 28; i++) {
bool funcState = dccDecoder.getFunction(i);
ledController.setFunctionState(i, funcState);
accessories.setFunctionState(i, funcState);
}
// Update accessories
accessories.setSpeed(speed);
// Update RailCom
railCom.setDecoderState(speed, direction);
}