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,282 @@
# Doxyfile 1.9.1
# Configuration file for Doxygen documentation generation
#---------------------------------------------------------------------------
# Project related configuration options
#---------------------------------------------------------------------------
DOXYFILE_ENCODING = UTF-8
PROJECT_NAME = "Locomotive Test Bench"
PROJECT_NUMBER = 1.0
PROJECT_BRIEF = "ESP32-based test bench for DC and DCC model locomotives"
PROJECT_LOGO =
OUTPUT_DIRECTORY = ./doc
CREATE_SUBDIRS = NO
ALLOW_UNICODE_NAMES = NO
OUTPUT_LANGUAGE = English
BRIEF_MEMBER_DESC = YES
REPEAT_BRIEF = YES
ABBREVIATE_BRIEF =
ALWAYS_DETAILED_SEC = NO
INLINE_INHERITED_MEMB = NO
FULL_PATH_NAMES = YES
STRIP_FROM_PATH =
STRIP_FROM_INC_PATH =
SHORT_NAMES = NO
JAVADOC_AUTOBRIEF = YES
JAVADOC_BANNER = NO
QT_AUTOBRIEF = NO
MULTILINE_CPP_IS_BRIEF = NO
INHERIT_DOCS = YES
SEPARATE_MEMBER_PAGES = NO
TAB_SIZE = 4
OPTIMIZE_OUTPUT_FOR_C = NO
OPTIMIZE_OUTPUT_JAVA = NO
OPTIMIZE_FOR_FORTRAN = NO
OPTIMIZE_OUTPUT_VHDL = NO
OPTIMIZE_OUTPUT_SLICE = NO
MARKDOWN_SUPPORT = YES
TOC_INCLUDE_HEADINGS = 5
AUTOLINK_SUPPORT = YES
BUILTIN_STL_SUPPORT = YES
CPP_CLI_SUPPORT = NO
DISTRIBUTE_GROUP_DOC = NO
GROUP_NESTED_COMPOUNDS = NO
SUBGROUPING = YES
INLINE_GROUPED_CLASSES = NO
INLINE_SIMPLE_STRUCTS = NO
TYPEDEF_HIDES_STRUCT = NO
#---------------------------------------------------------------------------
# Build related configuration options
#---------------------------------------------------------------------------
EXTRACT_ALL = YES
EXTRACT_PRIVATE = YES
EXTRACT_PRIV_VIRTUAL = NO
EXTRACT_PACKAGE = NO
EXTRACT_STATIC = YES
EXTRACT_LOCAL_CLASSES = YES
EXTRACT_LOCAL_METHODS = NO
EXTRACT_ANON_NSPACES = NO
HIDE_UNDOC_MEMBERS = NO
HIDE_UNDOC_CLASSES = NO
HIDE_FRIEND_COMPOUNDS = NO
HIDE_IN_BODY_DOCS = NO
INTERNAL_DOCS = NO
CASE_SENSE_NAMES = YES
HIDE_SCOPE_NAMES = NO
HIDE_COMPOUND_REFERENCE= NO
SHOW_INCLUDE_FILES = YES
SHOW_GROUPED_MEMB_INC = NO
FORCE_LOCAL_INCLUDES = NO
INLINE_INFO = YES
SORT_MEMBER_DOCS = YES
SORT_BRIEF_DOCS = NO
SORT_MEMBERS_CTORS_1ST = NO
SORT_GROUP_NAMES = NO
SORT_BY_SCOPE_NAME = NO
STRICT_PROTO_MATCHING = NO
GENERATE_TODOLIST = YES
GENERATE_TESTLIST = YES
GENERATE_BUGLIST = YES
GENERATE_DEPRECATEDLIST= YES
MAX_INITIALIZER_LINES = 30
SHOW_USED_FILES = YES
SHOW_FILES = YES
SHOW_NAMESPACES = YES
#---------------------------------------------------------------------------
# Configuration options related to warning and progress messages
#---------------------------------------------------------------------------
QUIET = NO
WARNINGS = YES
WARN_IF_UNDOCUMENTED = YES
WARN_IF_DOC_ERROR = YES
WARN_NO_PARAMDOC = YES
WARN_AS_ERROR = NO
WARN_FORMAT = "$file:$line: $text"
#---------------------------------------------------------------------------
# Configuration options related to the input files
#---------------------------------------------------------------------------
INPUT = ./src \
./include \
./README.md
INPUT_ENCODING = UTF-8
FILE_PATTERNS = *.cpp \
*.h \
*.md
RECURSIVE = YES
EXCLUDE =
EXCLUDE_SYMLINKS = NO
EXCLUDE_PATTERNS = */build/* \
*/.pio/* \
*/data/*
EXCLUDE_SYMBOLS =
EXAMPLE_PATH =
EXAMPLE_PATTERNS = *
EXAMPLE_RECURSIVE = NO
IMAGE_PATH =
INPUT_FILTER =
FILTER_PATTERNS =
FILTER_SOURCE_FILES = NO
FILTER_SOURCE_PATTERNS =
#---------------------------------------------------------------------------
# Configuration options related to source browsing
#---------------------------------------------------------------------------
SOURCE_BROWSER = YES
INLINE_SOURCES = NO
STRIP_CODE_COMMENTS = YES
REFERENCED_BY_RELATION = YES
REFERENCES_RELATION = YES
REFERENCES_LINK_SOURCE = YES
SOURCE_TOOLTIPS = YES
USE_HTAGS = NO
VERBATIM_HEADERS = YES
#---------------------------------------------------------------------------
# Configuration options related to the alphabetical class index
#---------------------------------------------------------------------------
ALPHABETICAL_INDEX = YES
COLS_IN_ALPHA_INDEX = 5
#---------------------------------------------------------------------------
# Configuration options related to the HTML output
#---------------------------------------------------------------------------
GENERATE_HTML = YES
HTML_OUTPUT = html
HTML_FILE_EXTENSION = .html
HTML_HEADER =
HTML_FOOTER =
HTML_STYLESHEET =
HTML_EXTRA_STYLESHEET =
HTML_EXTRA_FILES =
HTML_COLORSTYLE_HUE = 220
HTML_COLORSTYLE_SAT = 100
HTML_COLORSTYLE_GAMMA = 80
HTML_TIMESTAMP = YES
HTML_DYNAMIC_MENUS = YES
HTML_DYNAMIC_SECTIONS = NO
HTML_INDEX_NUM_ENTRIES = 100
GENERATE_DOCSET = NO
GENERATE_HTMLHELP = NO
GENERATE_QHP = NO
GENERATE_ECLIPSEHELP = NO
DISABLE_INDEX = NO
GENERATE_TREEVIEW = YES
ENUM_VALUES_PER_LINE = 4
TREEVIEW_WIDTH = 250
EXT_LINKS_IN_WINDOW = NO
HTML_FORMULA_FORMAT = png
FORMULA_FONTSIZE = 10
FORMULA_TRANSPARENT = YES
FORMULA_MACROFILE =
USE_MATHJAX = NO
SEARCHENGINE = YES
SERVER_BASED_SEARCH = NO
#---------------------------------------------------------------------------
# Configuration options related to the LaTeX output
#---------------------------------------------------------------------------
GENERATE_LATEX = NO
#---------------------------------------------------------------------------
# Configuration options related to the RTF output
#---------------------------------------------------------------------------
GENERATE_RTF = NO
#---------------------------------------------------------------------------
# Configuration options related to the man page output
#---------------------------------------------------------------------------
GENERATE_MAN = NO
#---------------------------------------------------------------------------
# Configuration options related to the XML output
#---------------------------------------------------------------------------
GENERATE_XML = NO
#---------------------------------------------------------------------------
# Configuration options related to the DOCBOOK output
#---------------------------------------------------------------------------
GENERATE_DOCBOOK = NO
#---------------------------------------------------------------------------
# Configuration options for the AutoGen Definitions output
#---------------------------------------------------------------------------
GENERATE_AUTOGEN_DEF = NO
#---------------------------------------------------------------------------
# Configuration options related to the Perl module output
#---------------------------------------------------------------------------
GENERATE_PERLMOD = NO
#---------------------------------------------------------------------------
# Configuration options related to the preprocessor
#---------------------------------------------------------------------------
ENABLE_PREPROCESSING = YES
MACRO_EXPANSION = YES
EXPAND_ONLY_PREDEF = NO
SEARCH_INCLUDES = YES
INCLUDE_PATH = ./include
INCLUDE_FILE_PATTERNS =
PREDEFINED = ARDUINO \
ESP32
EXPAND_AS_DEFINED =
SKIP_FUNCTION_MACROS = YES
#---------------------------------------------------------------------------
# Configuration options related to external references
#---------------------------------------------------------------------------
TAGFILES =
GENERATE_TAGFILE =
ALLEXTERNALS = NO
EXTERNAL_GROUPS = YES
EXTERNAL_PAGES = YES
#---------------------------------------------------------------------------
# Configuration options related to the dot tool
#---------------------------------------------------------------------------
CLASS_DIAGRAMS = YES
DIA_PATH =
HIDE_UNDOC_RELATIONS = YES
HAVE_DOT = NO
DOT_NUM_THREADS = 0
DOT_FONTNAME = Helvetica
DOT_FONTSIZE = 10
CLASS_GRAPH = YES
COLLABORATION_GRAPH = YES
GROUP_GRAPHS = YES
UML_LOOK = NO
UML_LIMIT_NUM_FIELDS = 10
TEMPLATE_RELATIONS = NO
INCLUDE_GRAPH = YES
INCLUDED_BY_GRAPH = YES
CALL_GRAPH = NO
CALLER_GRAPH = NO
GRAPHICAL_HIERARCHY = YES
DIRECTORY_GRAPH = YES
DOT_IMAGE_FORMAT = png
INTERACTIVE_SVG = NO
DOT_GRAPH_MAX_NODES = 50
MAX_DOT_GRAPH_DEPTH = 0
DOT_TRANSPARENT = NO
DOT_MULTI_TARGETS = NO
GENERATE_LEGEND = YES
DOT_CLEANUP = YES

View File

@@ -0,0 +1,48 @@
/*
* Pin Configuration Reference
*
* This file documents all pin assignments for quick reference.
* To change pins, edit the corresponding header files.
*/
// ===================================
// MOTOR CONTROLLER (LM18200)
// Defined in: include/MotorController.h
// ===================================
#define MOTOR_PWM_PIN 25 // PWM signal for speed control
#define MOTOR_DIR_PIN 26 // Direction: HIGH=forward, LOW=reverse
#define MOTOR_BRAKE_PIN 27 // Brake: LOW=brake, HIGH=release
// ===================================
// DCC GENERATOR
// Defined in: include/DCCGenerator.h
// ===================================
#define DCC_PIN_A 32 // DCC signal output A
#define DCC_PIN_B 33 // DCC signal output B (inverted)
// ===================================
// LED INDICATORS (WS2812)
// Defined in: include/LEDIndicator.h
// ===================================
#define LED_DATA_PIN 4 // WS2812 data line
#define NUM_LEDS 2 // LED 0: Power, LED 1: Mode
// ===================================
// AVAILABLE GPIO PINS (ESP32 D1 Mini)
// ===================================
// Used: 4, 25, 26, 27, 32, 33
// Available for expansion:
// - GPIO 5, 12, 13, 14, 15, 16, 17, 18, 19, 21, 22, 23
// - GPIO 34, 35, 36, 39 (Input only)
// Reserved for internal use:
// - GPIO 0, 2 (Boot/Flash)
// - GPIO 1, 3 (Serial TX/RX)
// ===================================
// NOTES
// ===================================
// - All control pins are outputs except where noted
// - Ensure adequate current capacity for motor loads
// - DCC outputs require proper signal conditioning
// - PWM frequency: 20kHz (defined in MotorController.cpp)
// - DCC timing follows NMRA standards

View File

@@ -0,0 +1,438 @@
# 🚂 Locomotive Test Bench
A comprehensive testing platform for model/scale locomotives using ESP32 (D1 Mini ESP32) and LM18200 H-Bridge motor driver. This system supports both **DC Analog** and **DCC Digital** control modes with a responsive web interface.
## Features
### Control Modes
- **DC Analog Mode**: Traditional PWM-based speed control with bidirectional operation
- **DCC Digital Mode**: Full DCC protocol support for digital model locomotives
- 128-step speed control
- Function control (F0-F12)
- Short and long address support (1-10239)
### WiFi Capabilities
- **Access Point Mode**: Create a standalone WiFi network
- **Client Mode**: Connect to existing WiFi networks
- Automatic reconnection in client mode
- Runtime WiFi configuration via web interface
### Web Interface
- Responsive Bootstrap-based design
- Real-time status monitoring
- Speed control with visual slider
- Direction control (forward/reverse)
- Emergency stop button
- DCC address configuration
- Function button controls (F0-F12) for DCC mode
- WiFi settings management
- Mobile-friendly design
## Hardware Requirements
### Components
- **ESP32 D1 Mini** (or compatible ESP32 board)
- **LM18200 H-Bridge Motor Driver**
- **2x WS2812 RGB LEDs** (for status indication)
- **Power Supply**: Suitable for your locomotive scale (typically 12-18V)
- Model locomotive (DC or DCC compatible)
### Pin Connections
#### LM18200 Motor Driver (DC Analog Mode)
| LM18200 Pin | ESP32 Pin | Description |
|-------------|-----------|-------------|
| PWM | GPIO 25 | PWM speed control |
| DIR | GPIO 26 | Direction control |
| BRAKE | GPIO 27 | Brake control (active low) |
| OUT1 | - | Motor terminal 1 |
| OUT2 | - | Motor terminal 2 |
| Vcc | 5V | Logic power |
| GND | GND | Ground |
#### DCC Signal Output
| Signal | ESP32 Pin | Description |
|--------|-----------|-------------|
| DCC A | GPIO 32 | DCC Signal A |
| DCC B | GPIO 33 | DCC Signal B (inverted) |
#### Status LEDs (WS2812)
| LED | ESP32 Pin | Function | Colors |
|-----|-----------|----------|---------|
| Data | GPIO 4 | LED strip data | - |
| LED 0 | - | Power status | Green=ON, Red=OFF |
| LED 1 | - | Mode indicator | Blue=DCC, Yellow=Analog |
**Note**: DCC signals require appropriate signal conditioning and booster circuitry for track connection.
### Wiring Diagram Notes
1. Connect LM18200 motor outputs to track or locomotive
2. Ensure proper power supply voltage for your scale
3. DCC mode requires additional booster circuit (not included in basic schematic)
4. Use appropriate heat sinking for LM18200
## Software Setup
### Prerequisites
- [Visual Studio Code](https://code.visualstudio.com/)
- [PlatformIO IDE Extension](https://platformio.org/install/ide?install=vscode)
### Installation Steps
1. **Clone or download this project**
```bash
cd /your/projects/folder
git clone <repository-url>
cd LocomotiveTestBench
```
2. **Open in VS Code**
- Open VS Code
- File → Open Folder → Select `LocomotiveTestBench` folder
3. **Download Bootstrap files for offline use**
```bash
cd data
chmod +x download_bootstrap.sh
./download_bootstrap.sh
```
Or download manually:
- [Bootstrap CSS](https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css) → `data/css/bootstrap.min.css`
- [Bootstrap JS](https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js) → `data/js/bootstrap.bundle.min.js`
4. **Upload Filesystem (LittleFS)**
- Click PlatformIO icon in sidebar
- Under PROJECT TASKS → Upload Filesystem Image
- Wait for upload to complete
5. **Build the project**
- Select "Build" under PROJECT TASKS
6. **Upload to ESP32**
- Connect ESP32 via USB
- Click "Upload" under PROJECT TASKS
- Wait for upload to complete
7. **Monitor Serial Output** (optional)
- Click "Monitor" to see debug output
- Default baud rate: 115200
## Configuration
### First Time Setup
1. **Power on the ESP32**
- Default mode: Access Point
- SSID: `LocoTestBench`
- Password: `12345678`
2. **Connect to WiFi**
- Use phone/computer to connect to `LocoTestBench` network
- Default IP: `192.168.4.1`
3. **Access Web Interface**
- Open browser: `http://192.168.4.1`
- You should see the Locomotive Test Bench interface
### WiFi Configuration
#### Access Point Mode (Default)
- Creates standalone network
- Default SSID: `LocoTestBench`
- Default Password: `12345678`
- IP Address: `192.168.4.1`
#### Client Mode
1. Open web interface
2. Expand "WiFi Configuration"
3. Select "Client (Connect to Network)"
4. Enter your network SSID and password
5. Click "Save & Restart"
6. Device will restart and connect to your network
7. Check serial monitor for assigned IP address
## Usage Guide
### DC Analog Mode
1. **Select Mode**
- Click "DC Analog" button in Control Mode section
2. **Set Speed**
- Use slider to adjust speed (0-100%)
- Speed shown in large display
3. **Change Direction**
- Click "🔄 Reverse" button to toggle direction
- Arrow indicator shows current direction (→ forward, ← reverse)
4. **Emergency Stop**
- Click "⏹ STOP" button to immediately stop locomotive
### DCC Digital Mode
1. **Select Mode**
- Click "DCC Digital" button in Control Mode section
- DCC sections will appear
2. **Set Locomotive Address**
- Enter DCC address (1-10239)
- Click "Set" button
- Address is saved to memory
3. **Control Speed**
- Use slider to adjust speed (0-100%)
- Direction control works same as analog mode
4. **DCC Functions**
- Function buttons (F0-F12) appear in DCC mode
- Click button to toggle function ON/OFF
- Active functions shown in darker color
## Pin Customization
To change pin assignments, edit these files:
### Motor Controller Pins
Edit `include/MotorController.h`:
```cpp
#define MOTOR_PWM_PIN 25 // Change as needed
#define MOTOR_DIR_PIN 26 // Change as needed
#define MOTOR_BRAKE_PIN 27 // Change as needed
```
### DCC Output Pins
Edit `include/DCCGenerator.h`:
```cpp
#define DCC_PIN_A 32 // Change as needed
#define DCC_PIN_B 33 // Change as needed
```
### LED Indicator Pins
Edit `include/LEDIndicator.h`:
```cpp
#define LED_DATA_PIN 4 // WS2812 data pin
#define NUM_LEDS 2 // Number of LEDs
```
## API Documentation
This project includes comprehensive API documentation using Doxygen.
### Generate Documentation
```bash
# Install Doxygen (if not already installed)
# Ubuntu/Debian: sudo apt-get install doxygen graphviz
# macOS: brew install doxygen graphviz
# Generate documentation
./generate_docs.sh
# View documentation
xdg-open doc/html/index.html # Linux
open doc/html/index.html # macOS
```
The documentation includes:
- Detailed class descriptions
- Function/method documentation
- Parameter and return value descriptions
- Code examples and usage notes
- Cross-referenced source code
See `doc/README.md` for more information.
## Project Structure
```
LocomotiveTestBench/
├── platformio.ini # PlatformIO configuration
├── Doxyfile # Doxygen configuration for API docs
├── generate_docs.sh # Script to generate documentation
├── README.md # This file
├── doc/ # Generated API documentation
│ └── html/ # HTML documentation (generated)
├── data/ # Filesystem (uploaded to LittleFS)
│ ├── index.html # Main web interface
│ ├── css/
│ │ ├── bootstrap.min.css # Bootstrap CSS (local)
│ │ └── style.css # Custom styles
│ └── js/
│ ├── bootstrap.bundle.min.js # Bootstrap JS (local)
│ └── app.js # Application JavaScript
├── include/ # Header files
│ ├── Config.h # Configuration management
│ ├── WiFiManager.h # WiFi connectivity
│ ├── MotorController.h # DC motor control
│ ├── DCCGenerator.h # DCC signal generation
│ ├── LEDIndicator.h # WS2812 LED status indicators
│ └── WebServer.h # Web server & API
└── src/ # Source files
├── main.cpp # Main application
├── Config.cpp # Configuration implementation
├── WiFiManager.cpp # WiFi implementation
├── MotorController.cpp # Motor control implementation
├── DCCGenerator.cpp # DCC implementation
├── LEDIndicator.cpp # LED indicator implementation
└── WebServer.cpp # Web server implementation
```
## API Reference
### REST API Endpoints
#### GET /api/status
Returns current system status
```json
{
"mode": "dcc",
"speed": 50,
"direction": 1,
"dccAddress": 3,
"ip": "192.168.4.1",
"wifiMode": "ap"
}
```
#### POST /api/mode
Set control mode
```json
{"mode": "dcc"} // or "analog"
```
#### POST /api/speed
Set speed and direction
```json
{"speed": 75, "direction": 1}
```
#### POST /api/dcc/address
Set DCC locomotive address
```json
{"address": 1234}
```
#### POST /api/dcc/function
Control DCC function
```json
{"function": 0, "state": true}
```
#### POST /api/wifi
Configure WiFi (triggers restart)
```json
{
"isAPMode": false,
"ssid": "YourNetwork",
"password": "YourPassword"
}
```
## Troubleshooting
### Cannot Connect to WiFi AP
- Verify ESP32 has power
- Check default SSID: `LocoTestBench`
- Default password: `12345678`
- Try restarting ESP32
### Web Interface Not Loading
- Verify correct IP address (check serial monitor)
- Try `http://192.168.4.1` in AP mode
- Check if LittleFS mounted successfully (serial output)
- Ensure filesystem was uploaded (Upload Filesystem Image)
- Clear browser cache and reload
- Try different browser
### Bootstrap/CSS Not Loading
- Verify Bootstrap files are downloaded to `data/css/` and `data/js/`
- Re-run `data/download_bootstrap.sh` script
- Upload filesystem image again
- Check browser console for 404 errors
### Motor Not Running (DC Mode)
- Check LM18200 connections
- Verify power supply is connected
- Check pin definitions match your wiring
- Use serial monitor to verify commands are received
### DCC Not Working
- Verify DCC pins are correctly connected
- DCC requires proper signal conditioning/booster
- Check locomotive is DCC-compatible
- Verify correct address is programmed in locomotive
### Upload Failed
- Check USB cable connection
- Try different USB port
- Press BOOT button on ESP32 during upload
- Check correct board selected in platformio.ini
## Technical Details
### DCC Protocol Implementation
- NMRA DCC Standard compliant
- 128-step speed control
- Function groups support (F0-F12 currently implemented)
- Configurable preamble (14 bits)
- Error detection with XOR checksum
### PWM Specifications (DC Mode)
- Frequency: 20 kHz
- Resolution: 8-bit (0-255)
- Duty cycle: 0-100% (mapped from speed)
### WiFi Specifications
- AP Mode: 802.11 b/g/n
- Client Mode: Auto-reconnect enabled
- Default reconnect interval: 30 seconds
## Safety Notes
⚠️ **Important Safety Information**
- Always disconnect power before wiring changes
- Use appropriate fuses for your scale
- Never exceed voltage ratings of your locomotives
- LM18200 requires adequate heat sinking
- Test with low voltage before full power
- Emergency stop should be easily accessible
## Future Enhancements
Potential features for future versions:
- [ ] PWM frequency adjustment
- [ ] Current monitoring and overload protection
- [ ] Multiple locomotive support
- [ ] Consist/multi-unit control
- [ ] Extended DCC functions (F13-F28)
- [ ] MQTT integration
- [ ] Locomotive profile storage
- [ ] Mobile app
## License
This project is provided as-is for educational and hobbyist purposes.
## Credits
- ESP32 Arduino Core
- ESPAsyncWebServer library
- Bootstrap CSS framework
- ArduinoJson library
## Support
For issues, questions, or contributions:
- Check serial monitor output for debugging
- Verify hardware connections
- Review pin configurations
- Test with known-good locomotive
---
**Version**: 1.0
**Last Updated**: November 2025
**Compatible Boards**: ESP32 D1 Mini, ESP32 DevKit, other ESP32 variants
**Framework**: Arduino for ESP32

View File

@@ -0,0 +1,107 @@
# Wiring Diagram
## LM18200 H-Bridge Connection
```
ESP32 D1 Mini LM18200 Track/Motor
GPIO 25 (PWM) ──────────────► PWM
GPIO 26 (DIR) ──────────────► DIR
GPIO 27 (BRAKE)──────────────► BRAKE
5V ──────────────► Vcc
GND ──────────────► GND
VS ◄───────── 12-18V Power Supply (+)
GND ◄───────── Power Supply GND
OUT1 ──────────► Track Rail 1
OUT2 ──────────► Track Rail 2
```
## DCC Signal Output (Optional Booster Required)
```
ESP32 D1 Mini DCC Booster Track
GPIO 32 (DCC_A) ────────────► Signal A
GPIO 33 (DCC_B) ────────────► Signal B
Power In ◄──── 12-18V Supply
Track A ──────────► Rail 1
Track B ──────────► Rail 2
```
## WS2812 LED Indicators
```
ESP32 D1 Mini WS2812 LEDs
GPIO 4 (LED_DATA) ──────────► DIN
5V ──────────► VCC
GND ──────────► GND
LED 0: Power Status
- Green: Power ON
- Red: Power OFF
LED 1: Mode Indicator
- Blue (pulsing): DCC mode
- Yellow (pulsing): Analog mode
```
## Complete System Diagram
```
┌─────────────────┐
│ Power Supply │
│ 12-18V DC │
└────────┬─────────┘
┌────────┴─────────┐
│ │
┌───────▼────────┐ ┌──────▼──────┐
│ LM18200 │ │ 5V Regulator│
│ H-Bridge │ │ (if needed) │
└───────┬────────┘ └──────┬───────┘
│ │
│ ┌────────▼────────┐
│ │ ESP32 D1 Mini │
│ │ │
│ │ GPIO 25 → PWM │───┐
│ │ GPIO 26 → DIR │───┤
│ │ GPIO 27 → BRAKE│───┤
│ │ │ │
│ │ GPIO 32 → DCC A│ │
│ │ GPIO 33 → DCC B│ │
│ │ │ │
│ │ GPIO 4 → LEDS │───┼──► WS2812 LEDs
│ │ │ │ (Power & Mode)
│ │ WiFi (Built-in)│ │
│ └─────────────────┘ │
│ │
└───────────────────────────────┘
┌───────▼────────┐
│ Track/Rails │
│ │
│ ┌──────────┐ │
│ │Locomotive│ │
│ └──────────┘ │
└────────────────┘
```
## Pin Summary Table
| Function | ESP32 Pin | Device Pin | Notes |
|----------|-----------|------------|-------|
| Motor PWM | GPIO 25 | LM18200 PWM | 20kHz PWM signal |
| Motor Direction | GPIO 26 | LM18200 DIR | High=Forward, Low=Reverse |
| Motor Brake | GPIO 27 | LM18200 BRAKE | Active LOW |
| DCC Signal A | GPIO 32 | DCC Booster A | Requires booster circuit |
| DCC Signal B | GPIO 33 | DCC Booster B | Inverted signal |
| LED Data | GPIO 4 | WS2812 DIN | 2 LEDs for status |
| LED Power | 5V | WS2812 VCC | LED strip power |
| Power (5V) | 5V | LM18200 Vcc | Logic power |
| Ground | GND | LM18200/LEDs GND | Common ground |

View File

@@ -0,0 +1,110 @@
# Web Interface Files Setup
This directory contains the web interface files that will be uploaded to the ESP32's LittleFS filesystem.
## Structure
```
data/
├── index.html # Main HTML page
├── css/
│ ├── bootstrap.min.css # Bootstrap CSS (local copy)
│ └── style.css # Custom styles
└── js/
├── bootstrap.bundle.min.js # Bootstrap JS (local copy)
└── app.js # Application JavaScript
```
## How to Upload Files to ESP32
### Method 1: Using PlatformIO (Recommended)
1. **Install LittleFS Uploader**:
- In VS Code, open PlatformIO Home
- Go to Platforms → Espressif 32
- Make sure it's installed/updated
2. **Download Bootstrap Files** (if not already present):
Download these files and place them in the appropriate directories:
- **Bootstrap CSS** (v5.3.0):
- URL: https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css
- Save to: `data/css/bootstrap.min.css`
- **Bootstrap JS** (v5.3.0):
- URL: https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js
- Save to: `data/js/bootstrap.bundle.min.js`
3. **Upload Filesystem**:
- In VS Code, click PlatformIO icon
- Under PROJECT TASKS → env:wemos_d1_mini32
- Click "Upload Filesystem Image"
- Wait for upload to complete
### Method 2: Manual Download and Upload
If you need to download Bootstrap files manually:
```bash
# Navigate to data directories
cd data/css
wget https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css
cd ../js
wget https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js
```
Or use curl:
```bash
cd data/css
curl -o bootstrap.min.css https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css
cd ../js
curl -o bootstrap.bundle.min.js https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js
```
### Verifying Upload
After uploading, you can verify the files are present by:
1. Opening Serial Monitor (115200 baud)
2. Look for "LittleFS mounted successfully" message
3. Access the web interface and check browser console for any 404 errors
## File Sizes (Approximate)
- `bootstrap.min.css`: ~190 KB
- `bootstrap.bundle.min.js`: ~220 KB
- `style.css`: ~1 KB
- `app.js`: ~4 KB
- `index.html`: ~4 KB
**Total**: ~420 KB (ESP32 has 1.5MB+ available for LittleFS)
## Benefits of LittleFS Approach
**Better Organization**: Separate HTML, CSS, and JS files
**Offline Operation**: Works without internet connection
**Easier Maintenance**: Edit files without recompiling firmware
**Faster Updates**: Only upload filesystem when web files change
**Better Performance**: No need to parse embedded strings
**Standard Development**: Use familiar web development workflow
## Troubleshooting
### LittleFS Mount Failed
- Ensure filesystem is properly formatted
- Try uploading filesystem image again
- Check serial output for detailed error messages
### 404 Errors on Static Files
- Verify files are in correct directories
- Check file names match exactly (case-sensitive)
- Re-upload filesystem image
### Bootstrap Not Loading
- Download Bootstrap files to `data/css/` and `data/js/`
- Upload filesystem image
- Clear browser cache and reload

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,44 @@
body {
padding: 20px;
background-color: #f5f5f5;
}
.container {
max-width: 800px;
background: white;
padding: 30px;
border-radius: 10px;
box-shadow: 0 0 20px rgba(0,0,0,0.1);
}
.status-indicator {
width: 20px;
height: 20px;
border-radius: 50%;
display: inline-block;
margin-right: 10px;
}
.status-connected {
background-color: #28a745;
}
.status-disconnected {
background-color: #dc3545;
}
.speed-value {
font-size: 2em;
font-weight: bold;
text-align: center;
margin: 20px 0;
}
.function-btn {
margin: 5px;
}
.direction-indicator {
font-size: 1.2em;
margin-left: 10px;
}

View File

@@ -0,0 +1,27 @@
#!/bin/bash
# Script to download Bootstrap files for offline use
echo "Downloading Bootstrap 5.3.0 files..."
# Create directories if they don't exist
mkdir -p css
mkdir -p js
# Download Bootstrap CSS
echo "Downloading Bootstrap CSS..."
curl -L -o css/bootstrap.min.css https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css
# Download Bootstrap JS Bundle (includes Popper)
echo "Downloading Bootstrap JS Bundle..."
curl -L -o js/bootstrap.bundle.min.js https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js
echo ""
echo "Download complete!"
echo ""
echo "Files downloaded:"
echo " - css/bootstrap.min.css ($(du -h css/bootstrap.min.css | cut -f1))"
echo " - js/bootstrap.bundle.min.js ($(du -h js/bootstrap.bundle.min.js | cut -f1))"
echo ""
echo "Now you can upload the filesystem to your ESP32:"
echo " 1. In VS Code, open PlatformIO"
echo " 2. Click 'Upload Filesystem Image' under PROJECT TASKS"

View File

@@ -0,0 +1,124 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Locomotive Test Bench</title>
<link href="/css/bootstrap.min.css" rel="stylesheet">
<link href="/css/style.css" rel="stylesheet">
</head>
<body>
<div class="container">
<h1 class="text-center mb-4">🚂 Locomotive Test Bench</h1>
<!-- Status Section -->
<div class="card mb-3">
<div class="card-body">
<h5 class="card-title">Status</h5>
<p><span class="status-indicator" id="statusIndicator"></span>
<span id="statusText">Connecting...</span></p>
<p class="mb-0"><small>IP: <span id="ipAddress">-</span></small></p>
</div>
</div>
<!-- Control Mode Section -->
<div class="card mb-3">
<div class="card-body">
<h5 class="card-title">Control Mode</h5>
<div class="btn-group w-100" role="group">
<input type="radio" class="btn-check" name="mode" id="modeAnalog" value="analog" autocomplete="off">
<label class="btn btn-outline-primary" for="modeAnalog">DC Analog</label>
<input type="radio" class="btn-check" name="mode" id="modeDCC" value="dcc" autocomplete="off">
<label class="btn btn-outline-primary" for="modeDCC">DCC Digital</label>
</div>
</div>
</div>
<!-- DCC Address Section -->
<div class="card mb-3" id="dccSection" style="display: none;">
<div class="card-body">
<h5 class="card-title">DCC Configuration</h5>
<div class="row">
<div class="col-md-8">
<label for="dccAddress" class="form-label">Locomotive Address</label>
<input type="number" class="form-control" id="dccAddress" min="1" max="10239" value="3">
</div>
<div class="col-md-4">
<label class="form-label">&nbsp;</label>
<button class="btn btn-primary w-100" onclick="setDCCAddress()">Set</button>
</div>
</div>
</div>
</div>
<!-- Speed Control Section -->
<div class="card mb-3">
<div class="card-body">
<h5 class="card-title">Speed Control
<span class="direction-indicator" id="directionIndicator"></span>
</h5>
<div class="speed-value" id="speedValue">0%</div>
<input type="range" class="form-range" id="speedSlider" min="0" max="100" value="0">
<div class="d-flex justify-content-between mt-3">
<button class="btn btn-danger" onclick="emergencyStop()">⏹ STOP</button>
<button class="btn btn-secondary" onclick="reverseDirection()">🔄 Reverse</button>
</div>
</div>
</div>
<!-- DCC Functions Section -->
<div class="card mb-3" id="functionsSection" style="display: none;">
<div class="card-body">
<h5 class="card-title">DCC Functions</h5>
<div id="functionButtons" class="d-flex flex-wrap">
<!-- Function buttons will be generated dynamically -->
</div>
</div>
</div>
<!-- WiFi Configuration Section -->
<div class="card">
<div class="card-body">
<h5 class="card-title">WiFi Configuration</h5>
<button class="btn btn-info w-100 mb-3" type="button" data-bs-toggle="collapse" data-bs-target="#wifiConfig">
Show WiFi Settings
</button>
<div class="collapse" id="wifiConfig">
<div class="mb-3">
<label class="form-label">WiFi Mode</label>
<select class="form-select" id="wifiMode">
<option value="ap">Access Point</option>
<option value="client">Client (Connect to Network)</option>
</select>
</div>
<div id="apSettings">
<div class="mb-3">
<label for="apSSID" class="form-label">AP SSID</label>
<input type="text" class="form-control" id="apSSID" value="LocoTestBench">
</div>
<div class="mb-3">
<label for="apPassword" class="form-label">AP Password</label>
<input type="password" class="form-control" id="apPassword" value="12345678">
</div>
</div>
<div id="clientSettings" style="display: none;">
<div class="mb-3">
<label for="wifiSSID" class="form-label">Network SSID</label>
<input type="text" class="form-control" id="wifiSSID">
</div>
<div class="mb-3">
<label for="wifiPassword" class="form-label">Network Password</label>
<input type="password" class="form-control" id="wifiPassword">
</div>
</div>
<button class="btn btn-warning w-100" onclick="saveWiFiSettings()">Save & Restart</button>
</div>
</div>
</div>
</div>
<script src="/js/bootstrap.bundle.min.js"></script>
<script src="/js/app.js"></script>
</body>
</html>

View File

@@ -0,0 +1,182 @@
let currentMode = 'analog';
let currentDirection = 1;
let dccFunctions = 0;
// Initialize
document.addEventListener('DOMContentLoaded', function() {
loadStatus();
setupEventListeners();
generateFunctionButtons();
setInterval(loadStatus, 2000);
});
function setupEventListeners() {
document.getElementById('speedSlider').addEventListener('input', function(e) {
updateSpeed(e.target.value);
});
document.querySelectorAll('input[name="mode"]').forEach(radio => {
radio.addEventListener('change', function(e) {
setMode(e.target.value);
});
});
document.getElementById('wifiMode').addEventListener('change', function(e) {
toggleWiFiSettings(e.target.value);
});
}
function generateFunctionButtons() {
const container = document.getElementById('functionButtons');
for (let i = 0; i <= 12; i++) {
const btn = document.createElement('button');
btn.className = 'btn btn-outline-secondary function-btn';
btn.id = 'f' + i;
btn.textContent = 'F' + i;
btn.onclick = () => toggleFunction(i);
container.appendChild(btn);
}
}
async function loadStatus() {
try {
const response = await fetch('/api/status');
const data = await response.json();
document.getElementById('statusIndicator').className = 'status-indicator status-connected';
document.getElementById('statusText').textContent = 'Connected';
document.getElementById('ipAddress').textContent = data.ip || '-';
currentMode = data.mode;
currentDirection = data.direction;
document.getElementById(data.mode === 'dcc' ? 'modeDCC' : 'modeAnalog').checked = true;
document.getElementById('speedSlider').value = data.speed;
document.getElementById('speedValue').textContent = data.speed + '%';
document.getElementById('dccAddress').value = data.dccAddress;
updateUIForMode(data.mode);
updateDirectionIndicator();
} catch (error) {
document.getElementById('statusIndicator').className = 'status-indicator status-disconnected';
document.getElementById('statusText').textContent = 'Disconnected';
}
}
function updateUIForMode(mode) {
currentMode = mode;
document.getElementById('dccSection').style.display = mode === 'dcc' ? 'block' : 'none';
document.getElementById('functionsSection').style.display = mode === 'dcc' ? 'block' : 'none';
}
async function setMode(mode) {
try {
await fetch('/api/mode', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({mode: mode})
});
updateUIForMode(mode);
} catch (error) {
console.error('Error setting mode:', error);
}
}
async function updateSpeed(speed) {
document.getElementById('speedValue').textContent = speed + '%';
try {
await fetch('/api/speed', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({
speed: parseInt(speed),
direction: currentDirection
})
});
} catch (error) {
console.error('Error setting speed:', error);
}
}
async function emergencyStop() {
document.getElementById('speedSlider').value = 0;
updateSpeed(0);
}
async function reverseDirection() {
currentDirection = currentDirection === 1 ? 0 : 1;
updateDirectionIndicator();
const speed = document.getElementById('speedSlider').value;
updateSpeed(speed);
}
function updateDirectionIndicator() {
document.getElementById('directionIndicator').textContent = currentDirection === 1 ? '→' : '←';
}
async function setDCCAddress() {
const address = document.getElementById('dccAddress').value;
try {
await fetch('/api/dcc/address', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({address: parseInt(address)})
});
} catch (error) {
console.error('Error setting DCC address:', error);
}
}
async function toggleFunction(fn) {
const btn = document.getElementById('f' + fn);
const isActive = btn.classList.contains('btn-secondary');
if (isActive) {
btn.classList.remove('btn-secondary');
btn.classList.add('btn-outline-secondary');
} else {
btn.classList.remove('btn-outline-secondary');
btn.classList.add('btn-secondary');
}
try {
await fetch('/api/dcc/function', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({
function: fn,
state: !isActive
})
});
} catch (error) {
console.error('Error setting function:', error);
}
}
function toggleWiFiSettings(mode) {
document.getElementById('apSettings').style.display = mode === 'ap' ? 'block' : 'none';
document.getElementById('clientSettings').style.display = mode === 'client' ? 'block' : 'none';
}
async function saveWiFiSettings() {
const mode = document.getElementById('wifiMode').value;
const config = {
isAPMode: mode === 'ap',
apSSID: document.getElementById('apSSID').value,
apPassword: document.getElementById('apPassword').value,
ssid: document.getElementById('wifiSSID').value,
password: document.getElementById('wifiPassword').value
};
if (confirm('Save WiFi settings and restart?')) {
try {
await fetch('/api/wifi', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify(config)
});
alert('Settings saved. Device will restart...');
} catch (error) {
console.error('Error saving WiFi settings:', error);
}
}
}

File diff suppressed because one or more lines are too long

9
LocomotiveTestBench/doc/.gitignore vendored Normal file
View File

@@ -0,0 +1,9 @@
# Ignore generated documentation
html/
latex/
man/
rtf/
xml/
# Keep this README
!README.md

View File

@@ -0,0 +1,143 @@
# API Documentation
This directory contains the auto-generated API documentation for the Locomotive Test Bench project.
## Generating Documentation
### Prerequisites
Install Doxygen (and optionally Graphviz for diagrams):
**Ubuntu/Debian:**
```bash
sudo apt-get install doxygen graphviz
```
**macOS:**
```bash
brew install doxygen graphviz
```
**Fedora/RHEL:**
```bash
sudo dnf install doxygen graphviz
```
**Windows:**
Download from [doxygen.nl](https://www.doxygen.nl/download.html)
### Generate Documentation
Run the generation script from the project root:
```bash
./generate_docs.sh
```
Or manually:
```bash
doxygen Doxyfile
```
### View Documentation
Open the generated HTML documentation:
```bash
# Linux
xdg-open doc/html/index.html
# macOS
open doc/html/index.html
# Windows
start doc/html/index.html
```
Or navigate to: `doc/html/index.html` in your browser.
## Documentation Structure
The generated documentation includes:
### Main Pages
- **Main Page**: Project overview and introduction
- **Classes**: All class definitions with member details
- **Files**: Source and header file listings
- **Namespaces**: Code organization structure
### For Each Class
- **Detailed Description**: Purpose and functionality
- **Member Functions**: All public/private methods
- **Member Variables**: All data members
- **Constructor/Destructor**: Object lifecycle
- **Usage Examples**: Where available
### Key Classes Documented
1. **Config** - Configuration management and persistent storage
2. **WiFiManager** - WiFi connectivity (AP and Client modes)
3. **MotorController** - DC motor control via LM18200
4. **DCCGenerator** - DCC protocol signal generation
5. **LEDIndicator** - WS2812 LED status indicators
6. **WebServerManager** - Web interface and REST API
## Customizing Documentation
Edit `Doxyfile` in the project root to customize:
- `PROJECT_NAME` - Project title
- `PROJECT_NUMBER` - Version number
- `PROJECT_BRIEF` - Short description
- `OUTPUT_DIRECTORY` - Where to generate docs
- `EXTRACT_PRIVATE` - Include private members
- `GENERATE_LATEX` - Generate PDF documentation
- `HAVE_DOT` - Enable class diagrams (requires Graphviz)
## Documentation Format
The code uses **Doxygen-style comments**:
```cpp
/**
* @brief Short description
*
* Detailed description can span
* multiple lines.
*
* @param paramName Description of parameter
* @return Description of return value
* @note Additional notes
* @warning Important warnings
*/
void exampleFunction(int paramName);
```
## Updating Documentation
When you modify code:
1. Update Doxygen comments in source files
2. Run `./generate_docs.sh` to regenerate
3. Review changes in browser
4. Commit updated source files (not generated HTML)
## CI/CD Integration
To auto-generate docs in CI/CD:
```yaml
# Example GitHub Actions
- name: Generate Documentation
run: |
sudo apt-get install doxygen
./generate_docs.sh
```
## Notes
- The `doc/` directory is typically added to `.gitignore`
- Only source comments are version controlled
- Documentation is regenerated as needed
- HTML output is ~2-5 MB depending on project size

View File

@@ -0,0 +1,37 @@
#!/bin/bash
# Script to generate API documentation using Doxygen
echo "Generating API documentation with Doxygen..."
echo ""
# Check if Doxygen is installed
if ! command -v doxygen &> /dev/null
then
echo "ERROR: Doxygen is not installed!"
echo ""
echo "To install Doxygen:"
echo " Ubuntu/Debian: sudo apt-get install doxygen graphviz"
echo " macOS: brew install doxygen graphviz"
echo " Fedora: sudo dnf install doxygen graphviz"
echo ""
exit 1
fi
# Run Doxygen
doxygen Doxyfile
if [ $? -eq 0 ]; then
echo ""
echo "Documentation generated successfully!"
echo ""
echo "Output location: ./doc/html/index.html"
echo ""
echo "To view the documentation:"
echo " Open in browser: file://$(pwd)/doc/html/index.html"
echo " Or run: xdg-open doc/html/index.html"
echo ""
else
echo ""
echo "ERROR: Documentation generation failed!"
exit 1
fi

View File

@@ -0,0 +1,102 @@
/**
* @file Config.h
* @brief Configuration management for the Locomotive Test Bench
*
* This module handles persistent storage of WiFi and system settings
* using ESP32's Preferences library (NVS - Non-Volatile Storage).
*
* @author Locomotive Test Bench Project
* @date 2025
*/
#ifndef CONFIG_H
#define CONFIG_H
#include <Arduino.h>
#include <Preferences.h>
/**
* @struct WiFiConfig
* @brief WiFi configuration parameters
*
* Stores both Access Point and Client mode settings.
*/
struct WiFiConfig {
String ssid; ///< WiFi network SSID (Client mode)
String password; ///< WiFi network password (Client mode)
bool isAPMode; ///< True = AP mode, False = Client mode
String apSSID; ///< Access Point SSID
String apPassword; ///< Access Point password (min 8 characters)
};
/**
* @struct SystemConfig
* @brief System operation configuration
*
* Stores current control mode and locomotive parameters.
*/
struct SystemConfig {
bool isDCCMode; ///< True = DCC digital, False = DC analog
uint16_t dccAddress; ///< DCC locomotive address (1-10239)
uint8_t speed; ///< Speed setting (0-100%)
uint8_t direction; ///< Direction: 0 = reverse, 1 = forward
uint32_t dccFunctions; ///< Bit field for DCC functions F0-F28
};
/**
* @class Config
* @brief Configuration manager with persistent storage
*
* Manages all configuration parameters and provides persistent
* storage using ESP32's NVS (Non-Volatile Storage) via Preferences.
*
* @note All settings are automatically saved to flash memory
* and persist across reboots.
*/
class Config {
public:
/**
* @brief Constructor - initializes with default values
*/
Config();
/**
* @brief Initialize preferences and load saved settings
*
* Must be called during setup() before using configuration.
* Loads previously saved settings from NVS.
*/
void begin();
/**
* @brief Save current configuration to NVS
*
* Writes all WiFi and system settings to persistent storage.
* Should be called after any configuration changes.
*/
void save();
/**
* @brief Load configuration from NVS
*
* Reads previously saved settings. Called automatically
* by begin(), but can be called manually to reload.
*/
void load();
/**
* @brief Reset all settings to defaults
*
* Clears all stored preferences and resets to factory defaults.
* Use with caution - all saved settings will be lost.
*/
void reset();
WiFiConfig wifi; ///< WiFi configuration settings
SystemConfig system; ///< System operation settings
private:
Preferences preferences; ///< ESP32 NVS preferences object
};
#endif

View File

@@ -0,0 +1,158 @@
/**
* @file DCCGenerator.h
* @brief NMRA DCC (Digital Command Control) signal generator
*
* Generates DCC protocol signals for controlling digital model locomotives.
* Implements NMRA DCC standard with support for:
* - Short addresses (1-127) and long addresses (128-10239)
* - 128-step speed control
* - Function control (F0-F12 implemented, expandable to F28)
*
* @note Requires external DCC booster circuit for track output
* @author Locomotive Test Bench Project
* @date 2025
*/
#ifndef DCC_GENERATOR_H
#define DCC_GENERATOR_H
#include <Arduino.h>
// Pin definitions for DCC output
#define DCC_PIN_A 32 ///< DCC Signal A output pin
#define DCC_PIN_B 33 ///< DCC Signal B output pin (inverted)
// DCC timing constants (microseconds) - NMRA standard
#define DCC_ONE_BIT_TOTAL_DURATION_MAX 64 ///< Max duration for '1' bit
#define DCC_ONE_BIT_TOTAL_DURATION_MIN 55 ///< Min duration for '1' bit
#define DCC_ZERO_BIT_TOTAL_DURATION_MAX 10000 ///< Max duration for '0' bit
#define DCC_ZERO_BIT_TOTAL_DURATION_MIN 95 ///< Min duration for '0' bit
#define DCC_ONE_BIT_PULSE_DURATION 58 ///< Half-cycle for '1' bit (58μs)
#define DCC_ZERO_BIT_PULSE_DURATION 100 ///< Half-cycle for '0' bit (100μs)
/**
* @class DCCGenerator
* @brief DCC protocol signal generator
*
* Generates NMRA-compliant DCC signals for digital locomotive control.
* Supports variable speed, direction, and function commands.
*
* @warning Output signals are low-power logic level.
* Requires external booster circuit for track connection.
*/
class DCCGenerator {
public:
/**
* @brief Constructor
*/
DCCGenerator();
/**
* @brief Initialize DCC generator hardware
*
* Configures output pins to idle state.
*/
void begin();
/**
* @brief Enable DCC signal generation
*
* Starts sending DCC packets to the track.
*/
void enable();
/**
* @brief Disable DCC signal generation
*
* Stops DCC output and sets pins to safe state.
*/
void disable();
/**
* @brief Set locomotive speed and direction
* @param address DCC address (1-10239)
* @param speed Speed value (0-100%)
* @param direction Direction: 0 = reverse, 1 = forward
*/
void setLocoSpeed(uint16_t address, uint8_t speed, uint8_t direction);
/**
* @brief Control DCC function
* @param address DCC address (1-10239)
* @param function Function number (0-28)
* @param state true = ON, false = OFF
*/
void setFunction(uint16_t address, uint8_t function, bool state);
/**
* @brief Update DCC signal generation
*
* Must be called regularly from main loop to send DCC packets.
* Sends speed and function packets at appropriate intervals.
*/
void update();
/**
* @brief Check if DCC is enabled
* @return true if DCC mode is active
*/
bool isEnabled() { return enabled; }
private:
bool enabled; ///< DCC generator enabled flag
uint16_t currentAddress; ///< Current locomotive address
uint8_t currentSpeed; ///< Current speed setting
uint8_t currentDirection; ///< Current direction (0=rev, 1=fwd)
uint32_t functionStates; ///< Function states bit field
unsigned long lastPacketTime; ///< Timestamp of last packet sent
static const unsigned long PACKET_INTERVAL = 30; ///< Packet interval (ms)
// DCC packet construction and transmission
/**
* @brief Send a complete DCC packet
* @param data Byte array containing packet data
* @param length Number of bytes in packet
*/
void sendPacket(uint8_t* data, uint8_t length);
/**
* @brief Send a single DCC bit
* @param value true = '1' bit, false = '0' bit
*/
void sendBit(bool value);
/**
* @brief Send DCC preamble (14 '1' bits)
*/
void sendPreamble();
/**
* @brief Send a single byte
* @param data Byte to send
*/
void sendByte(uint8_t data);
/**
* @brief Send speed command packet
*/
void sendSpeedPacket();
/**
* @brief Send function group packet
* @param group Function group number
*/
void sendFunctionPacket(uint8_t group);
/**
* @brief Calculate XOR checksum
* @param data Data bytes
* @param length Number of bytes
* @return XOR checksum byte
*/
uint8_t calculateChecksum(uint8_t* data, uint8_t length);
};
#endif

View File

@@ -0,0 +1,105 @@
/**
* @file LEDIndicator.h
* @brief WS2812 RGB LED status indicators
*
* Provides visual feedback using two WS2812 LEDs:
* - LED 0: Power status (Green = ON, Red = OFF)
* - LED 1: Mode indicator (Blue = DCC, Yellow = Analog)
*
* @author Locomotive Test Bench Project
* @date 2025
*/
#ifndef LED_INDICATOR_H
#define LED_INDICATOR_H
#include <Arduino.h>
#include <FastLED.h>
// Pin definition for WS2812 LEDs
#define LED_DATA_PIN 4 ///< Data pin for WS2812 strip
#define NUM_LEDS 2 ///< Number of LEDs (Power + Mode)
// LED indices
#define LED_POWER 0 ///< Power status indicator
#define LED_MODE 1 ///< Mode indicator (DCC/Analog)
/**
* @class LEDIndicator
* @brief Manages WS2812 RGB LED status displays
*
* Controls two LEDs for system status indication:
* - Power LED: Shows system power state with boot animation
* - Mode LED: Shows control mode with pulsing effect
*/
class LEDIndicator {
public:
/**
* @brief Constructor
*/
LEDIndicator();
/**
* @brief Initialize LED hardware
*
* Configures FastLED library and sets LEDs to off state.
*/
void begin();
/**
* @brief Update LED display
*
* Must be called regularly from main loop to update
* pulsing effects and animations.
*/
void update();
/**
* @brief Set power status
* @param on true = power on (green), false = off (red)
*/
void setPowerOn(bool on);
/**
* @brief Set operating mode
* @param isDCC true = DCC mode (blue), false = Analog (yellow)
*/
void setMode(bool isDCC);
/**
* @brief Set LED brightness
* @param brightness Brightness level (0-255)
*/
void setBrightness(uint8_t brightness);
/**
* @brief Play power-on animation sequence
*
* Shows 3-flash boot sequence on power LED.
*/
void powerOnSequence();
/**
* @brief Play mode change animation
*
* Smooth fade transition when switching modes.
*/
void modeChangeEffect();
private:
CRGB leds[NUM_LEDS]; ///< LED array
bool powerOn; ///< Power status flag
bool dccMode; ///< Mode flag (DCC/Analog)
uint8_t brightness; ///< Current brightness level
unsigned long lastUpdate; ///< Last update timestamp
uint8_t pulsePhase; ///< Pulse animation phase
// LED color definitions
static const CRGB COLOR_POWER_ON = CRGB::Green; ///< Power ON color
static const CRGB COLOR_POWER_OFF = CRGB::Red; ///< Power OFF color
static const CRGB COLOR_DCC = CRGB::Blue; ///< DCC mode color
static const CRGB COLOR_ANALOG = CRGB::Yellow; ///< Analog mode color
static const CRGB COLOR_OFF = CRGB::Black; ///< LED off state
};
#endif

View File

@@ -0,0 +1,101 @@
/**
* @file MotorController.h
* @brief DC motor control using LM18200 H-Bridge driver
*
* Provides bidirectional PWM motor control with brake functionality.
* Suitable for DC analog model locomotive control.
*
* @author Locomotive Test Bench Project
* @date 2025
*/
#ifndef MOTOR_CONTROLLER_H
#define MOTOR_CONTROLLER_H
#include <Arduino.h>
// Pin definitions for LM18200
// These can be adjusted based on your D1 Mini ESP32 wiring
#define MOTOR_PWM_PIN 25 ///< PWM signal output pin
#define MOTOR_DIR_PIN 26 ///< Direction control pin
#define MOTOR_BRAKE_PIN 27 ///< Brake control pin (active low)
/**
* @class MotorController
* @brief Controls DC motor via LM18200 H-Bridge
*
* Features:
* - Variable speed control (0-100%)
* - Bidirectional operation (forward/reverse)
* - Electronic braking
* - 20kHz PWM frequency for silent operation
* - 8-bit resolution (256 speed steps)
*/
class MotorController {
public:
/**
* @brief Constructor
*/
MotorController();
/**
* @brief Initialize motor controller hardware
*
* Configures GPIO pins and PWM channels.
* Sets motor to safe stopped state.
*/
void begin();
/**
* @brief Set motor speed and direction
* @param speed Speed value (0-100%)
* @param direction Direction: 0 = reverse, 1 = forward
*/
void setSpeed(uint8_t speed, uint8_t direction);
/**
* @brief Stop motor (coast to stop)
*
* Sets speed to zero and releases brake.
* Motor will coast to a stop.
*/
void stop();
/**
* @brief Apply electronic brake
*
* Activates LM18200 brake function for quick stop.
* More aggressive than stop().
*/
void brake();
/**
* @brief Update motor controller state
*
* Called from main loop for safety checks.
* Currently placeholder for future features.
*/
void update();
/**
* @brief Get current speed setting
* @return Speed (0-100%)
*/
uint8_t getCurrentSpeed() { return currentSpeed; }
/**
* @brief Get current direction
* @return Direction: 0 = reverse, 1 = forward
*/
uint8_t getCurrentDirection() { return currentDirection; }
private:
uint8_t currentSpeed; ///< Current speed setting (0-100)
uint8_t currentDirection; ///< Current direction (0=rev, 1=fwd)
static const int PWM_CHANNEL = 0; ///< ESP32 PWM channel
static const int PWM_FREQUENCY = 20000; ///< PWM frequency in Hz
static const int PWM_RESOLUTION = 8; ///< PWM resolution in bits
};
#endif

View File

@@ -0,0 +1,140 @@
/**
* @file WebServer.h
* @brief Web server and REST API for remote control
*
* Provides web-based control interface with:
* - Responsive Bootstrap-based UI
* - RESTful API for control and configuration
* - LittleFS-based file serving
* - Real-time status updates
*
* @author Locomotive Test Bench Project
* @date 2025
*/
#ifndef WEB_SERVER_H
#define WEB_SERVER_H
#include <Arduino.h>
#include <ESPAsyncWebServer.h>
#include <AsyncTCP.h>
#include <ArduinoJson.h>
#include "Config.h"
#include "MotorController.h"
#include "DCCGenerator.h"
#include "LEDIndicator.h"
/**
* @class WebServerManager
* @brief Manages web server and API endpoints
*
* Serves web interface from LittleFS and provides REST API
* for controlling the locomotive test bench remotely.
*
* API Endpoints:
* - GET /api/status - Get current system status
* - POST /api/mode - Set control mode (analog/dcc)
* - POST /api/speed - Set speed and direction
* - POST /api/dcc/address - Set DCC address
* - POST /api/dcc/function - Control DCC functions
* - POST /api/wifi - Configure WiFi settings
*/
class WebServerManager {
public:
/**
* @brief Constructor
* @param cfg Pointer to Config object
* @param motor Pointer to MotorController
* @param dcc Pointer to DCCGenerator
* @param led Pointer to LEDIndicator
*/
WebServerManager(Config* cfg, MotorController* motor, DCCGenerator* dcc, LEDIndicator* led);
/**
* @brief Initialize web server
*
* Mounts LittleFS, sets up routes, and starts AsyncWebServer.
*/
void begin();
/**
* @brief Update web server (currently unused)
*
* AsyncWebServer handles requests asynchronously.
*/
void update();
private:
Config* config; ///< Configuration manager
MotorController* motorController; ///< Motor controller instance
DCCGenerator* dccGenerator; ///< DCC generator instance
LEDIndicator* ledIndicator; ///< LED indicator instance
AsyncWebServer server; ///< Async web server (port 80)
/**
* @brief Set up all HTTP routes and handlers
*/
void setupRoutes();
/**
* @brief Handle root page request
* @param request HTTP request object
*/
void handleRoot(AsyncWebServerRequest *request);
/**
* @brief Handle status request
* @param request HTTP request object
*/
void handleGetStatus(AsyncWebServerRequest *request);
/**
* @brief Handle mode change request
* @param request HTTP request object
*/
void handleSetMode(AsyncWebServerRequest *request);
/**
* @brief Handle speed setting request
* @param request HTTP request object
*/
void handleSetSpeed(AsyncWebServerRequest *request);
/**
* @brief Handle DCC function request
* @param request HTTP request object
*/
void handleSetFunction(AsyncWebServerRequest *request);
/**
* @brief Handle config retrieval request
* @param request HTTP request object
*/
void handleGetConfig(AsyncWebServerRequest *request);
/**
* @brief Handle WiFi configuration request
* @param request HTTP request object
*/
void handleSetWiFi(AsyncWebServerRequest *request);
/**
* @brief Handle restart request
* @param request HTTP request object
*/
void handleRestart(AsyncWebServerRequest *request);
/**
* @brief Get system status as JSON
* @return JSON string with status information
*/
String getStatusJSON();
/**
* @brief Get configuration as JSON
* @return JSON string with configuration
*/
String getConfigJSON();
};
#endif

View File

@@ -0,0 +1,87 @@
/**
* @file WiFiManager.h
* @brief WiFi connection management for AP and Client modes
*
* Handles WiFi connectivity in both Access Point and Client modes,
* with automatic reconnection support.
*
* @author Locomotive Test Bench Project
* @date 2025
*/
#ifndef WIFI_MANAGER_H
#define WIFI_MANAGER_H
#include <Arduino.h>
#include <WiFi.h>
#include "Config.h"
/**
* @class WiFiManager
* @brief Manages WiFi connectivity and modes
*
* Provides WiFi functionality in two modes:
* - Access Point (AP): Creates standalone network
* - Client (STA): Connects to existing WiFi network
*
* Features automatic reconnection in client mode.
*/
class WiFiManager {
public:
/**
* @brief Constructor
* @param cfg Pointer to Config object for WiFi settings
*/
WiFiManager(Config* cfg);
/**
* @brief Initialize WiFi based on configuration
*
* Sets up either AP or Client mode based on config settings.
* Called during system startup.
*/
void begin();
/**
* @brief Set up Access Point mode
*
* Creates a standalone WiFi network using configured
* SSID and password. Default IP: 192.168.4.1
*/
void setupAccessPoint();
/**
* @brief Connect to existing WiFi network
*
* Attempts to connect as client to configured network.
* Falls back to AP mode if connection fails after 10 seconds.
*/
void connectToWiFi();
/**
* @brief Check if WiFi is connected
* @return true if connected (or AP mode active), false otherwise
*/
bool isConnected();
/**
* @brief Get current IP address
* @return IP address as string (AP IP or STA IP)
*/
String getIPAddress();
/**
* @brief Update WiFi status and handle reconnection
*
* Should be called regularly from main loop.
* Handles automatic reconnection in client mode.
*/
void update();
private:
Config* config; ///< Pointer to configuration object
unsigned long lastReconnectAttempt; ///< Timestamp of last reconnect attempt
static const unsigned long RECONNECT_INTERVAL = 30000; ///< Reconnect interval (30 seconds)
};
#endif

View File

@@ -0,0 +1,33 @@
; PlatformIO Project Configuration File
;
; Build options: build flags, source filter
; Upload options: custom upload port, speed and extra flags
; Library options: dependencies, extra library storages
; Advanced options: extra scripting
;
; Please visit documentation for the other options and examples
; https://docs.platformio.org/page/projectconf.html
[env:wemos_d1_mini32]
platform = espressif32
board = wemos_d1_mini32
framework = arduino
monitor_speed = 115200
upload_speed = 921600
; Build options
build_flags =
-D ARDUINO_ARCH_ESP32
-D CONFIG_ASYNC_TCP_RUNNING_CORE=1
-D CONFIG_ASYNC_TCP_USE_WDT=1
; Library dependencies
lib_deps =
bblanchon/ArduinoJson@^6.21.3
me-no-dev/ESP Async WebServer@^1.2.3
me-no-dev/AsyncTCP@^1.1.1
https://github.com/tuxiaoya/DCCpp.git
fastled/FastLED@^3.6.0
; Filesystem for web interface
board_build.filesystem = littlefs

View File

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

View File

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

View File

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

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

View File

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

View File

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