Initialisation depot
This commit is contained in:
92
ESP32/DCC-Bench/src/Config.cpp
Normal file
92
ESP32/DCC-Bench/src/Config.cpp
Normal 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();
|
||||
}
|
||||
455
ESP32/DCC-Bench/src/DCCGenerator.cpp
Normal file
455
ESP32/DCC-Bench/src/DCCGenerator.cpp
Normal 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;
|
||||
}
|
||||
115
ESP32/DCC-Bench/src/LEDIndicator.cpp
Normal file
115
ESP32/DCC-Bench/src/LEDIndicator.cpp
Normal 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)");
|
||||
}
|
||||
68
ESP32/DCC-Bench/src/MotorController.cpp
Normal file
68
ESP32/DCC-Bench/src/MotorController.cpp
Normal 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
|
||||
}
|
||||
28
ESP32/DCC-Bench/src/RelayController.cpp
Normal file
28
ESP32/DCC-Bench/src/RelayController.cpp
Normal 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");
|
||||
}
|
||||
943
ESP32/DCC-Bench/src/TouchscreenUI.cpp
Normal file
943
ESP32/DCC-Bench/src/TouchscreenUI.cpp
Normal 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");
|
||||
}
|
||||
109
ESP32/DCC-Bench/src/main.cpp
Normal file
109
ESP32/DCC-Bench/src/main.cpp
Normal 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);
|
||||
}
|
||||
Reference in New Issue
Block a user