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,92 @@
/**
* @file Config.cpp
* @brief Implementation of configuration management
*
* @author Locomotive Test Bench Project
* @date 2025
*/
#include "Config.h"
/**
* @brief Constructor - sets default configuration values
*
* Initializes all settings to safe defaults:
* - System: DC analog mode, 2-rail, power off, address 3, stopped
*/
Config::Config() {
// Default system values
system.isDCCMode = false;
system.is3Rail = false;
system.powerOn = 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 system settings to NVS flash memory.
* Settings persist across power cycles and reboots.
*/
void Config::save() {
// System settings
preferences.putBool("is_dcc", system.isDCCMode);
preferences.putBool("is_3rail", system.is3Rail);
preferences.putBool("power_on", system.powerOn);
preferences.putUShort("dcc_addr", system.dccAddress);
preferences.putUChar("speed", system.speed);
preferences.putUChar("direction", system.direction);
preferences.putUInt("dcc_func", system.dccFunctions);
}
/**
* @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() {
// System settings
system.isDCCMode = preferences.getBool("is_dcc", false);
system.is3Rail = preferences.getBool("is_3rail", false);
system.powerOn = preferences.getBool("power_on", 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);
}
/**
* @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();
// Reset to defaults
system.isDCCMode = false;
system.is3Rail = false;
system.powerOn = false;
system.dccAddress = 3;
system.speed = 0;
system.direction = 1;
system.dccFunctions = 0;
save();
}

View File

@@ -0,0 +1,455 @@
/**
* @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);
// Calibrate ACS712 current sensor zero point
calibrateCurrentSensor();
}
void DCCGenerator::calibrateCurrentSensor() {
#define CURRENT_SENSE_PIN 35
Serial.println("Calibrating ACS712 current sensor...");
Serial.println("Ensure no locomotive is on track and power is OFF");
delay(500); // Give time for user to see message
float sum = 0;
const int samples = 100;
for (int i = 0; i < samples; i++) {
int adc = analogRead(CURRENT_SENSE_PIN);
float voltage = (adc / 4095.0) * 3.3;
sum += voltage;
delay(10);
}
float zeroVoltage = sum / samples;
Serial.printf("ACS712 Zero Point: %.3fV (expected ~2.5V)\n", zeroVoltage);
if (abs(zeroVoltage - 2.5) > 0.3) {
Serial.println("WARNING: Zero voltage significantly different from 2.5V");
Serial.println("Check ACS712 wiring and 5V power supply");
} else {
Serial.println("ACS712 calibration OK");
}
}
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);
}
// ========================================
// Programming Track Methods
// ========================================
bool DCCGenerator::factoryReset() {
Serial.println("DCC Programming: Factory Reset (CV8 = 8)");
// Factory reset is CV8 = 8
bool success = writeCV(8, 8);
if (success) {
Serial.println("Factory reset successful");
} else {
Serial.println("Factory reset failed - no ACK");
}
return success;
}
bool DCCGenerator::setDecoderAddress(uint16_t address) {
Serial.printf("DCC Programming: Set Address = %d\n", address);
bool success = false;
if (address >= 1 && address <= 127) {
// Short address - write to CV1
success = writeCV(1, address);
if (success) {
// Also set CV29 bit 5 = 0 for short address mode
uint8_t cv29;
if (readCV(29, &cv29)) {
cv29 &= ~0x20; // Clear bit 5
writeCV(29, cv29);
}
Serial.printf("Short address %d set successfully\n", address);
}
} else if (address >= 128 && address <= 10239) {
// Long address - write to CV17 and CV18
uint8_t cv17 = 0xC0 | ((address >> 8) & 0x3F);
uint8_t cv18 = address & 0xFF;
bool cv17ok = writeCV(17, cv17);
bool cv18ok = writeCV(18, cv18);
if (cv17ok && cv18ok) {
// Set CV29 bit 5 = 1 for long address mode
uint8_t cv29;
if (readCV(29, &cv29)) {
cv29 |= 0x20; // Set bit 5
writeCV(29, cv29);
}
Serial.printf("Long address %d set successfully (CV17=%d, CV18=%d)\n",
address, cv17, cv18);
success = true;
}
} else {
Serial.println("Invalid address (must be 1-10239)");
return false;
}
if (!success) {
Serial.println("Set address failed - no ACK");
}
return success;
}
bool DCCGenerator::readCV(uint16_t cv, uint8_t* value) {
if (cv < 1 || cv > 1024) {
Serial.println("Invalid CV number (must be 1-1024)");
return false;
}
Serial.printf("DCC Programming: Read CV%d\n", cv);
// Use bit-wise verify method (more reliable than direct read)
uint8_t result = 0;
for (int bit = 0; bit < 8; bit++) {
// Test if bit is set
uint8_t packet[4];
uint8_t packetLength = 0;
// Service mode instruction: Verify Bit
packet[packetLength++] = 0x78 | ((cv >> 8) & 0x03); // 0111 10aa
packet[packetLength++] = cv & 0xFF;
packet[packetLength++] = 0xE8 | bit; // 111K 1BBB (K=1 for verify, BBB=bit position)
packet[packetLength++] = calculateChecksum(packet, packetLength);
// Send packet and check for ACK
sendServiceModePacket(packet, packetLength);
if (waitForAck()) {
result |= (1 << bit); // Bit is 1
}
delay(20); // Wait between bit verifications
}
*value = result;
Serial.printf("CV%d = %d (0x%02X)\n", cv, result, result);
return true; // Bit-wise verify always returns a value
}
bool DCCGenerator::writeCV(uint16_t cv, uint8_t value) {
if (cv < 1 || cv > 1024) {
Serial.println("Invalid CV number (must be 1-1024)");
return false;
}
Serial.printf("DCC Programming: Write CV%d = %d (0x%02X)\n", cv, value, value);
// Service mode instruction: Verify Byte (write with verification)
uint8_t packet[4];
uint8_t packetLength = 0;
packet[packetLength++] = 0x7C | ((cv >> 8) & 0x03); // 0111 11aa
packet[packetLength++] = cv & 0xFF;
packet[packetLength++] = value;
packet[packetLength++] = calculateChecksum(packet, packetLength);
// Send write packet multiple times for reliability
for (int i = 0; i < 3; i++) {
sendServiceModePacket(packet, packetLength);
delay(30);
}
// Verify the write
bool success = verifyByte(cv, value);
if (success) {
Serial.printf("CV%d write verified\n", cv);
} else {
Serial.printf("CV%d write failed - no ACK on verify\n", cv);
}
return success;
}
// ========================================
// Programming Track Helper Methods
// ========================================
void DCCGenerator::sendServiceModePacket(uint8_t* data, uint8_t length) {
// Service mode packets use longer preamble (20+ bits)
for (int i = 0; i < 22; i++) {
sendBit(1);
}
// Packet start bit
sendBit(0);
// Send data bytes
for (uint8_t i = 0; i < length; i++) {
sendByte(data[i]);
if (i < length - 1) {
sendBit(0); // Inter-byte bit
}
}
// Packet end bit
sendBit(1);
// Recovery time
delayMicroseconds(200);
}
bool DCCGenerator::verifyByte(uint16_t cv, uint8_t value) {
uint8_t packet[4];
uint8_t packetLength = 0;
// Service mode: Verify Byte
packet[packetLength++] = 0x74 | ((cv >> 8) & 0x03); // 0111 01aa
packet[packetLength++] = cv & 0xFF;
packet[packetLength++] = value;
packet[packetLength++] = calculateChecksum(packet, packetLength);
sendServiceModePacket(packet, packetLength);
return waitForAck();
}
bool DCCGenerator::waitForAck() {
// ACK detection using ACS712 current sensor
// Decoder draws 60mA+ pulse for 6ms to acknowledge
#define CURRENT_SENSE_PIN 35
#define ACS712_ZERO_VOLTAGE 2.5 // 2.5V at 0A (Vcc/2) - calibrate if needed
#define ACS712_SENSITIVITY 0.185 // 185 mV/A for ACS712-05A model
#define ACK_CURRENT_THRESHOLD 0.055 // 55mA threshold (slightly below 60mA for margin)
unsigned long startTime = millis();
int sampleCount = 0;
float maxCurrent = 0;
// Wait up to 20ms for ACK pulse
while (millis() - startTime < 20) {
int adcValue = analogRead(CURRENT_SENSE_PIN);
float voltage = (adcValue / 4095.0) * 3.3; // Convert ADC to voltage
float current = abs((voltage - ACS712_ZERO_VOLTAGE) / ACS712_SENSITIVITY);
if (current > maxCurrent) {
maxCurrent = current;
}
// If current spike detected (60mA+)
if (current > ACK_CURRENT_THRESHOLD) {
Serial.printf("ACK detected! Current: %.1fmA (ADC: %d, Voltage: %.3fV)\n",
current * 1000, adcValue, voltage);
return true;
}
sampleCount++;
delayMicroseconds(100); // Sample every 100μs
}
Serial.printf("No ACK detected (max current: %.1fmA, samples: %d)\n",
maxCurrent * 1000, sampleCount);
return false;
}

View File

@@ -0,0 +1,115 @@
/**
* @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)
// {}
LEDIndicator::LEDIndicator(){}
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)");
}

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
}

View File

@@ -0,0 +1,28 @@
/**
* @file RelayController.cpp
* @brief Implementation of relay controller for track configuration switching
*
* @author Locomotive Test Bench Project
* @date 2025
*/
#include "RelayController.h"
RelayController::RelayController() : is3Rail(false) {
}
void RelayController::begin() {
pinMode(RELAY_PIN, OUTPUT);
digitalWrite(RELAY_PIN, LOW); // Start in 2-rail mode
is3Rail = false;
Serial.println("Relay Controller initialized - 2-rail mode");
}
void RelayController::setRailMode(bool mode3Rail) {
is3Rail = mode3Rail;
digitalWrite(RELAY_PIN, is3Rail ? HIGH : LOW);
Serial.print("Rail mode changed to: ");
Serial.println(is3Rail ? "3-rail" : "2-rail");
}

View File

@@ -0,0 +1,943 @@
/**
* @file TouchscreenUI.cpp
* @brief Implementation of touchscreen user interface
*
* @author Locomotive Test Bench Project
* @date 2025
*/
#include "TouchscreenUI.h"
TouchscreenUI::TouchscreenUI(Config* cfg, MotorController* motor, DCCGenerator* dcc, RelayController* relay)
: touch(TOUCH_CS), config(cfg), motorController(motor), dccGenerator(dcc), relayController(relay) {
powerOn = false;
lastSpeed = 0;
lastDirection = 0;
lastIsDCC = true;
lastIs3Rail = false;
lastDccFunctions = 0;
sliderPressed = false;
programmingMode = false;
cvNumber = 1;
cvValue = 0;
newAddress = 3;
keypadMode = 0; // Start with address entry
}
void TouchscreenUI::begin() {
// Initialize TFT display
tft.init();
tft.setRotation(1); // Landscape orientation (320x240)
tft.fillScreen(COLOR_BG);
// Initialize touch
touch.begin();
touch.setRotation(1);
// Setup UI element positions
// Power button (top-left)
btnPower.x = 10;
btnPower.y = 10;
btnPower.w = 70;
btnPower.h = 50;
btnPower.label = "POWER";
btnPower.visible = true;
// Mode button (DCC/Analog)
btnMode.x = 90;
btnMode.y = 10;
btnMode.w = 70;
btnMode.h = 50;
btnMode.label = "MODE";
btnMode.visible = true;
// Rails button (2/3 rails)
btnRails.x = 170;
btnRails.y = 10;
btnRails.w = 70;
btnRails.h = 50;
btnRails.label = "RAILS";
btnRails.visible = true;
// Direction button
btnDirection.x = 250;
btnDirection.y = 10;
btnDirection.w = 60;
btnDirection.h = 50;
btnDirection.label = "DIR";
btnDirection.visible = true;
// DCC function buttons (F0-F12) - 13 buttons in compact grid
// Layout: 2 rows of function buttons below main controls
int btnW = 38;
int btnH = 28;
int startX = 10;
int startY = 68;
int spacing = 2;
for (int i = 0; i < NUM_FUNCTIONS; i++) {
int col = i % 8; // 8 buttons per row
int row = i / 8;
btnFunctions[i].x = startX + col * (btnW + spacing);
btnFunctions[i].y = startY + row * (btnH + spacing);
btnFunctions[i].w = btnW;
btnFunctions[i].h = btnH;
btnFunctions[i].label = "F" + String(i);
btnFunctions[i].visible = config->system.isDCCMode; // Only visible in DCC mode
}
// DCC Address button (only in DCC mode)
btnDccAddress.x = 10;
btnDccAddress.y = 68 + 2 * (btnH + spacing);
btnDccAddress.w = 80;
btnDccAddress.h = 28;
btnDccAddress.label = "ADDR";
btnDccAddress.visible = config->system.isDCCMode;
// Programming button (only in DCC mode)
btnProgramming.x = 100;
btnProgramming.y = 68 + 2 * (btnH + spacing);
btnProgramming.w = 80;
btnProgramming.h = 28;
btnProgramming.label = "PROG";
btnProgramming.visible = config->system.isDCCMode;
// Speed slider (horizontal, bottom half)
sliderX = 20;
sliderY = 120;
sliderW = 280;
sliderH = 40;
sliderKnobX = sliderX;
// Draw initial UI
drawUI();
Serial.println("Touchscreen UI initialized");
}
void TouchscreenUI::update() {
// Check for touch events
if (touch.touched()) {
TS_Point p = touch.getPoint();
// Map touch coordinates to screen coordinates
int16_t x = mapTouch(p.x, TS_MIN_X, TS_MAX_X, 0, 320);
int16_t y = mapTouch(p.y, TS_MIN_Y, TS_MAX_Y, 0, 240);
// Bounds checking
x = constrain(x, 0, 319);
y = constrain(y, 0, 239);
handleTouch(x, y);
// Debounce
delay(100);
}
// Update UI if state changed
if (lastSpeed != config->system.speed ||
lastDirection != config->system.direction ||
lastIsDCC != config->system.isDCCMode ||
lastIs3Rail != config->system.is3Rail ||
(config->system.isDCCMode && lastDccFunctions != config->system.dccFunctions)) {
// If mode changed, redraw everything
if (lastIsDCC != config->system.isDCCMode) {
redraw();
} else {
drawStatusBar();
if (config->system.isDCCMode && lastDccFunctions != config->system.dccFunctions) {
drawDccFunctions();
}
}
lastSpeed = config->system.speed;
lastDirection = config->system.direction;
lastIsDCC = config->system.isDCCMode;
lastIs3Rail = config->system.is3Rail;
lastDccFunctions = config->system.dccFunctions;
}
}
void TouchscreenUI::redraw() {
tft.fillScreen(COLOR_BG);
drawUI();
}
void TouchscreenUI::drawUI() {
if (programmingMode) {
drawProgrammingScreen();
return;
}
drawPowerButton();
drawModeButton();
drawRailsButton();
drawDirectionButton();
if (config->system.isDCCMode) {
drawDccFunctions();
drawDccAddressButton();
}
drawSpeedSlider();
drawStatusBar();
}
void TouchscreenUI::drawPowerButton() {
uint16_t color = powerOn ? COLOR_POWER_ON : COLOR_POWER_OFF;
tft.fillRoundRect(btnPower.x, btnPower.y, btnPower.w, btnPower.h, 5, color);
tft.drawRoundRect(btnPower.x, btnPower.y, btnPower.w, btnPower.h, 5, COLOR_TEXT);
tft.setTextColor(COLOR_TEXT);
tft.setTextDatum(MC_DATUM);
tft.drawString(powerOn ? "ON" : "OFF", btnPower.x + btnPower.w/2, btnPower.y + btnPower.h/2, 2);
}
void TouchscreenUI::drawModeButton() {
uint16_t color = config->system.isDCCMode ? COLOR_DCC : COLOR_ANALOG;
tft.fillRoundRect(btnMode.x, btnMode.y, btnMode.w, btnMode.h, 5, color);
tft.drawRoundRect(btnMode.x, btnMode.y, btnMode.w, btnMode.h, 5, COLOR_TEXT);
tft.setTextColor(COLOR_BG);
tft.setTextDatum(MC_DATUM);
tft.drawString(config->system.isDCCMode ? "DCC" : "DC", btnMode.x + btnMode.w/2, btnMode.y + btnMode.h/2, 2);
}
void TouchscreenUI::drawRailsButton() {
uint16_t color = config->system.is3Rail ? COLOR_SLIDER_ACTIVE : COLOR_BUTTON;
tft.fillRoundRect(btnRails.x, btnRails.y, btnRails.w, btnRails.h, 5, color);
tft.drawRoundRect(btnRails.x, btnRails.y, btnRails.w, btnRails.h, 5, COLOR_TEXT);
tft.setTextColor(COLOR_TEXT);
tft.setTextDatum(MC_DATUM);
tft.drawString(config->system.is3Rail ? "3-Rail" : "2-Rail", btnRails.x + btnRails.w/2, btnRails.y + btnRails.h/2, 2);
}
void TouchscreenUI::drawDirectionButton() {
tft.fillRoundRect(btnDirection.x, btnDirection.y, btnDirection.w, btnDirection.h, 5, COLOR_BUTTON);
tft.drawRoundRect(btnDirection.x, btnDirection.y, btnDirection.w, btnDirection.h, 5, COLOR_TEXT);
tft.setTextColor(COLOR_TEXT);
tft.setTextDatum(MC_DATUM);
tft.drawString(config->system.direction ? "FWD" : "REV", btnDirection.x + btnDirection.w/2, btnDirection.y + btnDirection.h/2, 2);
}
void TouchscreenUI::drawSpeedSlider() {
// Draw slider track
tft.fillRoundRect(sliderX, sliderY, sliderW, sliderH, 5, COLOR_SLIDER);
// Calculate knob position based on speed
sliderKnobX = sliderX + (config->system.speed * (sliderW - 20)) / 100;
// Draw speed text above slider
tft.setTextColor(COLOR_TEXT);
tft.setTextDatum(MC_DATUM);
tft.fillRect(sliderX, sliderY - 30, sliderW, 25, COLOR_BG);
String speedText = "Speed: " + String(config->system.speed) + "%";
tft.drawString(speedText, sliderX + sliderW/2, sliderY - 15, 4);
// Draw active portion of slider
if (config->system.speed > 0) {
tft.fillRoundRect(sliderX, sliderY, sliderKnobX - sliderX + 10, sliderH, 5, COLOR_SLIDER_ACTIVE);
}
// Draw knob
tft.fillCircle(sliderKnobX + 10, sliderY + sliderH/2, 15, COLOR_TEXT);
}
void TouchscreenUI::drawStatusBar() {
// Status bar at bottom
int y = 200;
tft.fillRect(0, y, 320, 40, COLOR_PANEL);
tft.setTextColor(COLOR_TEXT);
tft.setTextDatum(TL_DATUM);
String status = "PWR:" + String(powerOn ? "ON" : "OFF");
status += " | Mode:" + String(config->system.isDCCMode ? "DCC" : "DC");
status += " | " + String(config->system.is3Rail ? "3-Rail" : "2-Rail");
if (config->system.isDCCMode && powerOn) {
status += " | Addr:" + String(config->system.dccAddress);
}
tft.drawString(status, 5, y + 5, 2);
tft.drawString("Speed:" + String(config->system.speed) + "% " + String(config->system.direction ? "FWD" : "REV"),
5, y + 20, 2);
}
void TouchscreenUI::handleTouch(int16_t x, int16_t y) {
// If in programming mode, handle differently
if (programmingMode) {
// Check back button
if (x >= btnProgBack.x && x <= btnProgBack.x + btnProgBack.w &&
y >= btnProgBack.y && y <= btnProgBack.y + btnProgBack.h) {
exitProgrammingMode();
return;
}
// Check factory reset button
if (x >= btnFactoryReset.x && x <= btnFactoryReset.x + btnFactoryReset.w &&
y >= btnFactoryReset.y && y <= btnFactoryReset.y + btnFactoryReset.h) {
performFactoryReset();
return;
}
// Check set address button
if (x >= btnSetAddress.x && x <= btnSetAddress.x + btnSetAddress.w &&
y >= btnSetAddress.y && y <= btnSetAddress.y + btnSetAddress.h) {
performSetAddress();
return;
}
// Check read CV button
if (x >= btnReadCV.x && x <= btnReadCV.x + btnReadCV.w &&
y >= btnReadCV.y && y <= btnReadCV.y + btnReadCV.h) {
performReadCV();
return;
}
// Check write CV button
if (x >= btnWriteCV.x && x <= btnWriteCV.x + btnWriteCV.w &&
y >= btnWriteCV.y && y <= btnWriteCV.y + btnWriteCV.h) {
performWriteCV();
return;
}
// Check numeric keypad
for (int i = 0; i < NUM_KEYPAD_BUTTONS; i++) {
if (x >= btnKeypad[i].x && x <= btnKeypad[i].x + btnKeypad[i].w &&
y >= btnKeypad[i].y && y <= btnKeypad[i].y + btnKeypad[i].h) {
handleKeypadPress(i);
return;
}
}
return;
}
// Normal mode touch handling
// Check power button
if (x >= btnPower.x && x <= btnPower.x + btnPower.w &&
y >= btnPower.y && y <= btnPower.y + btnPower.h) {
updatePowerState(!powerOn);
return;
}
// Check mode button
if (x >= btnMode.x && x <= btnMode.x + btnMode.w &&
y >= btnMode.y && y <= btnMode.y + btnMode.h) {
updateMode(!config->system.isDCCMode);
return;
}
// Check rails button
if (x >= btnRails.x && x <= btnRails.x + btnRails.w &&
y >= btnRails.y && y <= btnRails.y + btnRails.h) {
updateRailMode(!config->system.is3Rail);
return;
}
// Check direction button
if (x >= btnDirection.x && x <= btnDirection.x + btnDirection.w &&
y >= btnDirection.y && y <= btnDirection.y + btnDirection.h) {
updateDirection();
return;
}
// Check DCC function buttons (only in DCC mode)
if (config->system.isDCCMode) {
for (int i = 0; i < NUM_FUNCTIONS; i++) {
if (x >= btnFunctions[i].x && x <= btnFunctions[i].x + btnFunctions[i].w &&
y >= btnFunctions[i].y && y <= btnFunctions[i].y + btnFunctions[i].h) {
toggleDccFunction(i);
return;
}
}
// Check DCC address button (placeholder for future address entry)
if (x >= btnDccAddress.x && x <= btnDccAddress.x + btnDccAddress.w &&
y >= btnDccAddress.y && y <= btnDccAddress.y + btnDccAddress.h) {
// Future: Show numeric keypad for address entry
Serial.println("DCC Address button pressed - feature coming soon");
return;
}
// Check programming button
if (x >= btnProgramming.x && x <= btnProgramming.x + btnProgramming.w &&
y >= btnProgramming.y && y <= btnProgramming.y + btnProgramming.h) {
enterProgrammingMode();
return;
}
}
// Check slider
if (x >= sliderX && x <= sliderX + sliderW &&
y >= sliderY - 10 && y <= sliderY + sliderH + 10) {
// Calculate speed from touch position
int newSpeed = ((x - sliderX) * 100) / sliderW;
newSpeed = constrain(newSpeed, 0, 100);
updateSpeed(newSpeed);
return;
}
}
void TouchscreenUI::updatePowerState(bool state) {
powerOn = state;
if (!powerOn) {
// Turn everything off
config->system.speed = 0;
motorController->stop();
dccGenerator->disable();
} else {
// Power on - restore based on mode
if (config->system.isDCCMode) {
dccGenerator->enable();
dccGenerator->setLocoSpeed(config->system.dccAddress, config->system.speed, config->system.direction);
} else {
motorController->setSpeed(config->system.speed, config->system.direction);
}
}
config->save();
drawPowerButton();
drawSpeedSlider();
drawStatusBar();
Serial.print("Power: ");
Serial.println(powerOn ? "ON" : "OFF");
}
void TouchscreenUI::updateMode(bool isDCC) {
// Always power off when changing modes
powerOn = false;
config->system.speed = 0;
config->system.isDCCMode = isDCC;
// Stop both controllers
motorController->stop();
dccGenerator->disable();
config->save();
drawPowerButton();
drawModeButton();
drawSpeedSlider();
drawStatusBar();
Serial.print("Mode changed to: ");
Serial.println(isDCC ? "DCC" : "DC Analog");
Serial.println("Power automatically turned OFF");
}
void TouchscreenUI::updateRailMode(bool is3Rail) {
config->system.is3Rail = is3Rail;
relayController->setRailMode(is3Rail);
config->save();
drawRailsButton();
drawStatusBar();
}
void TouchscreenUI::updateDirection() {
config->system.direction = !config->system.direction;
if (powerOn) {
if (config->system.isDCCMode) {
dccGenerator->setLocoSpeed(config->system.dccAddress, config->system.speed, config->system.direction);
} else {
motorController->setSpeed(config->system.speed, config->system.direction);
}
}
config->save();
drawDirectionButton();
drawStatusBar();
Serial.print("Direction: ");
Serial.println(config->system.direction ? "Forward" : "Reverse");
}
void TouchscreenUI::updateSpeed(uint8_t newSpeed) {
config->system.speed = newSpeed;
if (powerOn) {
if (config->system.isDCCMode) {
dccGenerator->setLocoSpeed(config->system.dccAddress, config->system.speed, config->system.direction);
} else {
motorController->setSpeed(config->system.speed, config->system.direction);
}
}
config->save();
drawSpeedSlider();
drawStatusBar();
}
int16_t TouchscreenUI::mapTouch(int16_t value, int16_t inMin, int16_t inMax, int16_t outMin, int16_t outMax) {
return (value - inMin) * (outMax - outMin) / (inMax - inMin) + outMin;
}
void TouchscreenUI::drawDccFunctions() {
// Only draw if in DCC mode
if (!config->system.isDCCMode) {
// Clear the function button area
tft.fillRect(0, 68, 320, 60, COLOR_BG);
return;
}
// Draw all function buttons
for (int i = 0; i < NUM_FUNCTIONS; i++) {
bool isActive = (config->system.dccFunctions >> i) & 0x01;
uint16_t color = isActive ? COLOR_FUNCTION_ON : COLOR_FUNCTION_OFF;
tft.fillRoundRect(btnFunctions[i].x, btnFunctions[i].y,
btnFunctions[i].w, btnFunctions[i].h, 3, color);
tft.drawRoundRect(btnFunctions[i].x, btnFunctions[i].y,
btnFunctions[i].w, btnFunctions[i].h, 3, COLOR_TEXT);
tft.setTextColor(COLOR_TEXT);
tft.setTextDatum(MC_DATUM);
tft.drawString(btnFunctions[i].label,
btnFunctions[i].x + btnFunctions[i].w/2,
btnFunctions[i].y + btnFunctions[i].h/2, 1);
}
}
void TouchscreenUI::drawDccAddressButton() {
if (!config->system.isDCCMode) {
return;
}
tft.fillRoundRect(btnDccAddress.x, btnDccAddress.y,
btnDccAddress.w, btnDccAddress.h, 3, COLOR_BUTTON);
tft.drawRoundRect(btnDccAddress.x, btnDccAddress.y,
btnDccAddress.w, btnDccAddress.h, 3, COLOR_TEXT);
tft.setTextColor(COLOR_TEXT);
tft.setTextDatum(MC_DATUM);
String addrText = "A:" + String(config->system.dccAddress);
tft.drawString(addrText,
btnDccAddress.x + btnDccAddress.w/2,
btnDccAddress.y + btnDccAddress.h/2, 2);
// Draw programming button
tft.fillRoundRect(btnProgramming.x, btnProgramming.y,
btnProgramming.w, btnProgramming.h, 3, COLOR_DCC);
tft.drawRoundRect(btnProgramming.x, btnProgramming.y,
btnProgramming.w, btnProgramming.h, 3, COLOR_TEXT);
tft.setTextColor(COLOR_BG);
tft.drawString("PROG",
btnProgramming.x + btnProgramming.w/2,
btnProgramming.y + btnProgramming.h/2, 2);
}
void TouchscreenUI::toggleDccFunction(uint8_t function) {
if (!config->system.isDCCMode || function >= NUM_FUNCTIONS) {
return;
}
// Toggle the function bit
config->system.dccFunctions ^= (1 << function);
// Send to DCC generator if power is on
if (powerOn) {
bool state = (config->system.dccFunctions >> function) & 0x01;
dccGenerator->setFunction(config->system.dccAddress, function, state);
}
// Save configuration
config->save();
// Redraw function buttons
drawDccFunctions();
Serial.print("DCC Function F");
Serial.print(function);
Serial.print(": ");
Serial.println((config->system.dccFunctions >> function) & 0x01 ? "ON" : "OFF");
}
void TouchscreenUI::enterProgrammingMode() {
programmingMode = true;
cvNumber = 1;
cvValue = 0;
newAddress = config->system.dccAddress;
keypadMode = 0; // Start with address entry
tft.fillScreen(COLOR_BG);
drawProgrammingScreen();
Serial.println("Entered DCC Programming Mode");
}
void TouchscreenUI::exitProgrammingMode() {
programmingMode = false;
tft.fillScreen(COLOR_BG);
drawUI();
Serial.println("Exited DCC Programming Mode");
}
void TouchscreenUI::drawProgrammingScreen() {
tft.fillScreen(COLOR_BG);
// Title
tft.setTextColor(COLOR_DCC);
tft.setTextDatum(TC_DATUM);
tft.drawString("DCC PROGRAMMING", 160, 5, 4);
// Back button
btnProgBack.x = 5;
btnProgBack.y = 5;
btnProgBack.w = 60;
btnProgBack.h = 30;
tft.fillRoundRect(btnProgBack.x, btnProgBack.y, btnProgBack.w, btnProgBack.h, 5, COLOR_POWER_OFF);
tft.drawRoundRect(btnProgBack.x, btnProgBack.y, btnProgBack.w, btnProgBack.h, 5, COLOR_TEXT);
tft.setTextColor(COLOR_TEXT);
tft.setTextDatum(MC_DATUM);
tft.drawString("BACK", btnProgBack.x + btnProgBack.w/2, btnProgBack.y + btnProgBack.h/2, 2);
// Factory Reset button
btnFactoryReset.x = 10;
btnFactoryReset.y = 45;
btnFactoryReset.w = 140;
btnFactoryReset.h = 35;
tft.fillRoundRect(btnFactoryReset.x, btnFactoryReset.y, btnFactoryReset.w, btnFactoryReset.h, 5, COLOR_POWER_OFF);
tft.drawRoundRect(btnFactoryReset.x, btnFactoryReset.y, btnFactoryReset.w, btnFactoryReset.h, 5, COLOR_TEXT);
tft.setTextColor(COLOR_TEXT);
tft.drawString("FACTORY RESET", btnFactoryReset.x + btnFactoryReset.w/2, btnFactoryReset.y + btnFactoryReset.h/2, 2);
// Set Address section
btnSetAddress.x = 170;
btnSetAddress.y = 45;
btnSetAddress.w = 140;
btnSetAddress.h = 35;
tft.fillRoundRect(btnSetAddress.x, btnSetAddress.y, btnSetAddress.w, btnSetAddress.h, 5, COLOR_POWER_ON);
tft.drawRoundRect(btnSetAddress.x, btnSetAddress.y, btnSetAddress.w, btnSetAddress.h, 5, COLOR_TEXT);
tft.setTextColor(COLOR_TEXT);
tft.drawString("SET ADDRESS", btnSetAddress.x + btnSetAddress.w/2, btnSetAddress.y + btnSetAddress.h/2, 2);
// Address display with selection indicator
tft.setTextColor(COLOR_TEXT);
tft.setTextDatum(TL_DATUM);
tft.drawString("New Addr:", 175, 85, 2);
// Highlight selected field
if (keypadMode == 0) {
tft.fillRoundRect(245, 83, 60, 22, 3, COLOR_FUNCTION_ON);
}
tft.setTextColor(keypadMode == 0 ? COLOR_BG : COLOR_FUNCTION_ON);
tft.setTextDatum(TR_DATUM);
tft.drawString(String(newAddress), 300, 85, 4);
// CV Programming section
tft.setTextColor(COLOR_TEXT);
tft.setTextDatum(TL_DATUM);
tft.drawString("CV#:", 10, 110, 2);
if (keypadMode == 1) {
tft.fillRoundRect(50, 108, 80, 22, 3, COLOR_DCC);
}
tft.setTextColor(keypadMode == 1 ? COLOR_BG : COLOR_DCC);
tft.setTextDatum(TR_DATUM);
tft.drawString(String(cvNumber), 125, 110, 4);
tft.setTextColor(COLOR_TEXT);
tft.setTextDatum(TL_DATUM);
tft.drawString("Val:", 140, 110, 2);
if (keypadMode == 2) {
tft.fillRoundRect(180, 108, 60, 22, 3, COLOR_DCC);
}
tft.setTextColor(keypadMode == 2 ? COLOR_BG : COLOR_DCC);
tft.setTextDatum(TR_DATUM);
tft.drawString(String(cvValue), 235, 110, 4);
// Mode selector hint
tft.setTextColor(COLOR_BUTTON);
tft.setTextDatum(TL_DATUM);
String modeText = "Editing: ";
if (keypadMode == 0) modeText += "ADDRESS";
else if (keypadMode == 1) modeText += "CV NUMBER";
else modeText += "CV VALUE";
tft.drawString(modeText, 245, 110, 1);
// Read/Write CV buttons
btnReadCV.x = 10;
btnReadCV.y = 140;
btnReadCV.w = 145;
btnReadCV.h = 30;
tft.fillRoundRect(btnReadCV.x, btnReadCV.y, btnReadCV.w, btnReadCV.h, 5, COLOR_ANALOG);
tft.drawRoundRect(btnReadCV.x, btnReadCV.y, btnReadCV.w, btnReadCV.h, 5, COLOR_TEXT);
tft.setTextColor(COLOR_BG);
tft.setTextDatum(MC_DATUM);
tft.drawString("READ CV", btnReadCV.x + btnReadCV.w/2, btnReadCV.y + btnReadCV.h/2, 2);
btnWriteCV.x = 165;
btnWriteCV.y = 140;
btnWriteCV.w = 145;
btnWriteCV.h = 30;
tft.fillRoundRect(btnWriteCV.x, btnWriteCV.y, btnWriteCV.w, btnWriteCV.h, 5, COLOR_FUNCTION_ON);
tft.drawRoundRect(btnWriteCV.x, btnWriteCV.y, btnWriteCV.w, btnWriteCV.h, 5, COLOR_TEXT);
tft.setTextColor(COLOR_BG);
tft.drawString("WRITE CV", btnWriteCV.x + btnWriteCV.w/2, btnWriteCV.y + btnWriteCV.h/2, 2);
// Draw numeric keypad
drawNumericKeypad();
// Status area
drawProgrammingStatus();
}
void TouchscreenUI::drawNumericKeypad() {
// Numeric keypad layout: 3x4 grid (1-9, 0, backspace, enter)
int btnW = 60;
int btnH = 30;
int startX = 50;
int startY = 175;
int spacing = 5;
const char* labels[] = {"1", "2", "3", "4", "5", "6", "7", "8", "9", "<", "0", "OK"};
for (int i = 0; i < NUM_KEYPAD_BUTTONS; i++) {
int col = i % 3;
int row = i / 3;
btnKeypad[i].x = startX + col * (btnW + spacing);
btnKeypad[i].y = startY + row * (btnH + spacing);
btnKeypad[i].w = btnW;
btnKeypad[i].h = btnH;
btnKeypad[i].label = labels[i];
uint16_t color = COLOR_BUTTON;
if (i == 9) color = COLOR_POWER_OFF; // Backspace in red
if (i == 11) color = COLOR_POWER_ON; // OK in green
tft.fillRoundRect(btnKeypad[i].x, btnKeypad[i].y, btnKeypad[i].w, btnKeypad[i].h, 3, color);
tft.drawRoundRect(btnKeypad[i].x, btnKeypad[i].y, btnKeypad[i].w, btnKeypad[i].h, 3, COLOR_TEXT);
tft.setTextColor(COLOR_TEXT);
tft.setTextDatum(MC_DATUM);
tft.drawString(btnKeypad[i].label, btnKeypad[i].x + btnKeypad[i].w/2, btnKeypad[i].y + btnKeypad[i].h/2, 2);
}
}
void TouchscreenUI::drawProgrammingStatus() {
// Status message area at bottom
tft.fillRect(0, 215, 320, 25, COLOR_PANEL);
tft.setTextColor(COLOR_TEXT);
tft.setTextDatum(TC_DATUM);
tft.drawString("Programming Track Mode - Loco on Prog Track", 160, 220, 1);
}
void TouchscreenUI::handleKeypadPress(uint8_t key) {
uint16_t* currentValue;
uint16_t maxValue;
// Select which value we're editing
if (keypadMode == 0) {
currentValue = &newAddress;
maxValue = 10239;
} else if (keypadMode == 1) {
currentValue = &cvNumber;
maxValue = 1024;
} else {
currentValue = (uint16_t*)&cvValue; // Cast for consistency
maxValue = 255;
}
if (key < 9) {
// Number keys 1-9
*currentValue = (*currentValue) * 10 + (key + 1);
if (*currentValue > maxValue) *currentValue = key + 1; // Reset if too large
} else if (key == 9) {
// Backspace
*currentValue = (*currentValue) / 10;
if (keypadMode == 0 && *currentValue == 0) *currentValue = 1; // Address min is 1
if (keypadMode == 1 && *currentValue == 0) *currentValue = 1; // CV min is 1
} else if (key == 10) {
// 0 key
*currentValue = (*currentValue) * 10;
if (*currentValue > maxValue) *currentValue = 0;
} else if (key == 11) {
// OK - move to next field
keypadMode = (keypadMode + 1) % 3;
Serial.print("Switched to mode: ");
if (keypadMode == 0) Serial.println("ADDRESS");
else if (keypadMode == 1) Serial.println("CV NUMBER");
else Serial.println("CV VALUE");
}
// Constrain to valid range
if (keypadMode == 2) {
cvValue = constrain(*currentValue, 0, 255);
}
// Redraw the screen to update values
drawProgrammingScreen();
}
void TouchscreenUI::performFactoryReset() {
Serial.println("FACTORY RESET - Sending CV8 = 8");
// Update status
tft.fillRect(0, 215, 320, 25, COLOR_POWER_OFF);
tft.setTextColor(COLOR_TEXT);
tft.setTextDatum(TC_DATUM);
tft.drawString("Sending Factory Reset... CV8 = 8", 160, 220, 1);
// Call DCCGenerator factory reset
bool success = dccGen->factoryReset();
// Update status based on result
delay(500);
tft.fillRect(0, 215, 320, 25, success ? COLOR_FUNCTION_ON : COLOR_POWER_OFF);
tft.setTextColor(COLOR_BG);
tft.setTextDatum(TC_DATUM);
if (success) {
tft.drawString("Factory Reset Complete!", 160, 220, 1);
} else {
tft.drawString("Factory Reset Failed - No ACK", 160, 220, 1);
}
delay(2000);
drawProgrammingStatus();
Serial.println("Factory reset command sent");
}
void TouchscreenUI::performSetAddress() {
if (newAddress < 1 || newAddress > 10239) {
Serial.println("Invalid address range");
tft.fillRect(0, 215, 320, 25, COLOR_POWER_OFF);
tft.setTextColor(COLOR_TEXT);
tft.setTextDatum(TC_DATUM);
tft.drawString("ERROR: Address must be 1-10239", 160, 220, 1);
delay(2000);
drawProgrammingStatus();
return;
}
Serial.print("Setting DCC Address to: ");
Serial.println(newAddress);
// Update status
tft.fillRect(0, 215, 320, 25, COLOR_FUNCTION_ON);
tft.setTextColor(COLOR_BG);
tft.setTextDatum(TC_DATUM);
tft.drawString("Programming Address " + String(newAddress) + "...", 160, 220, 1);
// Call DCCGenerator to set address
bool success = dccGen->setDecoderAddress(newAddress);
// Update status based on result
delay(500);
tft.fillRect(0, 215, 320, 25, success ? COLOR_FUNCTION_ON : COLOR_POWER_OFF);
tft.setTextColor(COLOR_BG);
tft.setTextDatum(TC_DATUM);
if (success) {
tft.drawString("Address " + String(newAddress) + " Set!", 160, 220, 1);
// Update config with new address
config->system.dccAddress = newAddress;
config->save();
} else {
tft.drawString("Address Programming Failed - No ACK", 160, 220, 1);
}
delay(2000);
drawProgrammingStatus();
Serial.println("Address programming complete");
}
void TouchscreenUI::performReadCV() {
if (cvNumber < 1 || cvNumber > 1024) {
Serial.println("Invalid CV number");
tft.fillRect(0, 215, 320, 25, COLOR_POWER_OFF);
tft.setTextColor(COLOR_TEXT);
tft.setTextDatum(TC_DATUM);
tft.drawString("ERROR: CV must be 1-1024", 160, 220, 1);
delay(2000);
drawProgrammingStatus();
return;
}
Serial.print("Reading CV");
Serial.println(cvNumber);
// Update status
tft.fillRect(0, 215, 320, 25, COLOR_ANALOG);
tft.setTextColor(COLOR_BG);
tft.setTextDatum(TC_DATUM);
tft.drawString("Reading CV" + String(cvNumber) + "...", 160, 220, 1);
// Call DCCGenerator to read CV
uint8_t readValue = 0;
bool success = dccGen->readCV(cvNumber, &readValue);
if (success) {
cvValue = readValue;
Serial.print("CV");
Serial.print(cvNumber);
Serial.print(" = ");
Serial.println(cvValue);
// Update status
delay(500);
tft.fillRect(0, 215, 320, 25, COLOR_FUNCTION_ON);
tft.setTextColor(COLOR_BG);
tft.setTextDatum(TC_DATUM);
tft.drawString("CV" + String(cvNumber) + " = " + String(cvValue), 160, 220, 1);
delay(1500);
} else {
tft.fillRect(0, 215, 320, 25, COLOR_POWER_OFF);
tft.setTextColor(COLOR_TEXT);
tft.setTextDatum(TC_DATUM);
tft.drawString("Read Failed - No Response", 160, 220, 1);
delay(1500);
}
drawProgrammingScreen();
}
void TouchscreenUI::performWriteCV() {
if (cvNumber < 1 || cvNumber > 1024) {
Serial.println("Invalid CV number");
tft.fillRect(0, 215, 320, 25, COLOR_POWER_OFF);
tft.setTextColor(COLOR_TEXT);
tft.setTextDatum(TC_DATUM);
tft.drawString("ERROR: CV must be 1-1024", 160, 220, 1);
delay(2000);
drawProgrammingStatus();
return;
}
Serial.print("Writing CV");
Serial.print(cvNumber);
Serial.print(" = ");
Serial.println(cvValue);
// Update status
tft.fillRect(0, 215, 320, 25, COLOR_FUNCTION_ON);
tft.setTextColor(COLOR_BG);
tft.setTextDatum(TC_DATUM);
tft.drawString("Writing CV" + String(cvNumber) + " = " + String(cvValue) + "...", 160, 220, 1);
// Call DCCGenerator to write CV
bool success = dccGen->writeCV(cvNumber, cvValue);
// Update status based on result
delay(500);
tft.fillRect(0, 215, 320, 25, success ? COLOR_FUNCTION_ON : COLOR_POWER_OFF);
tft.setTextColor(COLOR_BG);
tft.setTextDatum(TC_DATUM);
if (success) {
tft.drawString("CV" + String(cvNumber) + " = " + String(cvValue) + " Verified!", 160, 220, 1);
} else {
tft.drawString("Write Failed - No ACK", 160, 220, 1);
}
delay(1500);
drawProgrammingStatus();
Serial.println("CV write complete");
}

View File

@@ -0,0 +1,109 @@
/**
* @file main.cpp
* @brief Main application entry point for Locomotive Test Bench
*
* Orchestrates all system components:
* - Configuration management
* - Touchscreen UI
* - Motor control (DC analog)
* - DCC signal generation
* - Relay control for 2-rail/3-rail switching
*
* @author Locomotive Test Bench Project
* @date 2025
* @version 2.0
*/
#include <Arduino.h>
#include "Config.h"
#include "MotorController.h"
#include "DCCGenerator.h"
#include "RelayController.h"
#include "TouchscreenUI.h"
// Global objects
Config config;
MotorController motorController;
DCCGenerator dccGenerator;
RelayController relayController;
TouchscreenUI touchUI(&config, &motorController, &dccGenerator, &relayController);
/**
* @brief Setup function - runs once at startup
*
* Initializes all hardware and software components in correct order:
* 1. Serial communication
* 2. Configuration system
* 3. Relay controller
* 4. Motor controller
* 5. DCC generator
* 6. Touchscreen UI
*/
void setup() {
// Initialize serial communication
Serial.begin(115200);
delay(1000);
Serial.println("\n\n=================================");
Serial.println(" Locomotive Test Bench v2.0");
Serial.println(" ESP32-2432S028R Edition");
Serial.println("=================================\n");
// Load configuration
config.begin();
Serial.println("Configuration loaded");
// Initialize relay controller
relayController.begin();
relayController.setRailMode(config.system.is3Rail);
// Initialize motor controller
motorController.begin();
// Initialize DCC generator
dccGenerator.begin();
// Initialize touchscreen UI
touchUI.begin();
// Set initial mode (but power is off by default)
if (config.system.isDCCMode && config.system.powerOn) {
dccGenerator.enable();
dccGenerator.setLocoSpeed(
config.system.dccAddress,
config.system.speed,
config.system.direction
);
} else if (!config.system.isDCCMode && config.system.powerOn) {
motorController.setSpeed(
config.system.speed,
config.system.direction
);
}
Serial.println("\n=================================");
Serial.println("Setup complete!");
Serial.println("=================================");
Serial.print("Mode: ");
Serial.println(config.system.isDCCMode ? "DCC" : "DC Analog");
Serial.print("Rail Mode: ");
Serial.println(config.system.is3Rail ? "3-Rail" : "2-Rail");
Serial.print("Power: ");
Serial.println(config.system.powerOn ? "ON" : "OFF");
Serial.println("=================================\n");
}
void loop() {
// Update touchscreen UI (handles all user interactions)
touchUI.update();
// Update DCC signal generation (if enabled)
if (config.system.isDCCMode && touchUI.isPowerOn()) {
dccGenerator.update();
} else if (!config.system.isDCCMode && touchUI.isPowerOn()) {
motorController.update();
}
// Small delay to prevent watchdog issues
delay(1);
}