Initialisation depot
This commit is contained in:
314
ESP32/DCC-Loco/Hardware/README.md
Normal file
314
ESP32/DCC-Loco/Hardware/README.md
Normal file
@@ -0,0 +1,314 @@
|
||||
# Hardware Design Files
|
||||
|
||||
This folder is reserved for KiCad hardware design files for the DCC Locomotive Decoder.
|
||||
|
||||
## Planned Contents
|
||||
|
||||
- **Schematic**: Complete circuit schematic (`.kicad_sch`)
|
||||
- **PCB Layout**: Printed circuit board design (`.kicad_pcb`)
|
||||
- **Bill of Materials**: Component list (BOM.csv)
|
||||
- **Gerber Files**: Manufacturing files
|
||||
- **3D Models**: Component models
|
||||
- **Assembly Drawings**: Assembly instructions
|
||||
|
||||
## Current Status
|
||||
|
||||
🚧 **Under Development**
|
||||
|
||||
Hardware design files will be added in future releases.
|
||||
|
||||
## Design Goals
|
||||
|
||||
- Compact form factor suitable for HO/N scale locomotives
|
||||
- Single or dual-sided PCB (TBD)
|
||||
- Through-hole or SMD components (TBD)
|
||||
- Easy assembly and testing
|
||||
- Robust protection circuits
|
||||
- Proper EMI/EMC considerations
|
||||
|
||||
## Sections
|
||||
|
||||
The PCB will include the following sections:
|
||||
|
||||
1. **Power Supply**
|
||||
- Track power input with TVS protection
|
||||
- Schottky diode bridge rectifier (4x SS54: 5A, 40V)
|
||||
- Bulk filtering capacitors (470µF-1000µF electrolytic)
|
||||
- 3.3V LDO regulator for ESP32-H2 logic
|
||||
- Separate motor power feed to TB67H450FNG VM pin
|
||||
- Ceramic bypass capacitors (0.1µF near ICs)
|
||||
|
||||
2. **DCC Input Stage**
|
||||
- Optocoupler isolation
|
||||
- Signal conditioning
|
||||
- Protection diodes
|
||||
|
||||
3. **Motor Driver**
|
||||
- TB67H450FNG H-bridge
|
||||
- Current sense circuit (0.1Ω shunt resistor)
|
||||
- Bootstrap capacitors (if needed for gate drive)
|
||||
- Flyback diodes (usually internal to TB67H450FNG)
|
||||
- Bulk motor power capacitor (100µF near VM pin)
|
||||
|
||||
4. **Microcontroller**
|
||||
- ESP32-H2 module or bare chip
|
||||
- Programming header
|
||||
- Reset and boot buttons
|
||||
|
||||
5. **LED Output**
|
||||
- WS2812 connector
|
||||
- Level shifter (if needed)
|
||||
- Power filtering
|
||||
|
||||
6. **RailCom**
|
||||
- RailCom transmitter circuit
|
||||
- Cutout detection
|
||||
- Track coupling circuit
|
||||
|
||||
7. **Accessory Outputs**
|
||||
- 2x N-FET drivers
|
||||
- Screw terminals or connectors
|
||||
- Protection circuits
|
||||
|
||||
8. **Configuration**
|
||||
- Configuration button
|
||||
- Status LED
|
||||
- Optional programming port
|
||||
|
||||
## Component Selection
|
||||
|
||||
### Key Components
|
||||
|
||||
- **Bridge Rectifier**: 4x SS54 Schottky diodes (5A, 40V, SMA/DO-214AC) or SS56 (5A, 60V)
|
||||
- Lower forward drop (~0.5V per diode, 1V total vs 2V for standard bridge)
|
||||
- Better efficiency = less heat
|
||||
- Fast switching for DCC frequency
|
||||
- Arrange in standard bridge configuration
|
||||
- **Microcontroller**: ESP32-H2 (RISC-V, Zigbee/Thread)
|
||||
- **Motor Driver**: Toshiba TB67H450FNG (dual H-bridge, 3.5A)
|
||||
- **Optocoupler**: 6N137 (fast) or PC817 (general purpose)
|
||||
- **N-FETs**: AO3400A (SOT-23, 4A, 44mΩ RDS(on) @ 2.5V)
|
||||
- For accessory outputs (max 350mA each)
|
||||
- Logic-level compatible with 3.3V GPIO
|
||||
- Low cost (~$0.05-0.10)
|
||||
- **Voltage Regulator**: AMS1117-3.3 (800mA) or HT7333 (LDO, low dropout)
|
||||
- **Current Sense Resistor**: 0.1Ω, 1W metal film or wire-wound
|
||||
- **LEDs**: WS2812B or compatible addressable RGB LEDs
|
||||
|
||||
### Connectors
|
||||
|
||||
- **Motor**: 2-pin screw terminal or JST-XH
|
||||
- **Track Input**: 2-pin screw terminal
|
||||
- **LED Strip**: 3-pin JST connector
|
||||
- **Accessories**: 2x 2-pin screw terminals
|
||||
- **Programming**: 6-pin header (GND, 3V3, TX, RX, IO0, EN)
|
||||
|
||||
## Design Considerations
|
||||
|
||||
### Power Supply Schematic
|
||||
|
||||

|
||||
|
||||
```ditaa {cmd=true args=["-E"]}
|
||||
DCC Track Input (12-18V AC/DC)
|
||||
|
|
||||
v
|
||||
+-----+-----+
|
||||
| TVS | P6KE24A bidirectional
|
||||
| Diode |
|
||||
+-----+-----+
|
||||
|
|
||||
+-------------+-------------+
|
||||
| |
|
||||
Track+ Track-
|
||||
| |
|
||||
+-------+-------+ +-------+-------+
|
||||
| D1 | | D3 |
|
||||
| SS54 5A | | SS54 5A |
|
||||
+-------+-------+ +-------+-------+
|
||||
| |
|
||||
+--------->DC+<-------------+
|
||||
|
|
||||
| +----------------------------+
|
||||
+--| 1000uF/25V Electrolytic |
|
||||
| +----------------------------+
|
||||
|
|
||||
+---> TB67H450FNG VM (Motor Power)
|
||||
|
|
||||
| +----------------------------+
|
||||
+--| AMS1117-3.3 or HT7333 LDO |
|
||||
| +----------------------------+
|
||||
| |
|
||||
| +--| 100uF |---> 3.3V Logic
|
||||
| |
|
||||
+-------+-------+ | | +-------+-------+
|
||||
| D2 | | | | D4 |
|
||||
| SS54 5A | | | | SS54 5A |
|
||||
+-------+-------+ | | +-------+-------+
|
||||
| | | |
|
||||
+--------->GND<--------+----------+
|
||||
|
|
||||
Common Ground
|
||||
|
||||
```
|
||||
|
||||
**Bridge Configuration:**
|
||||
- Track inputs: Connect to DCC rails (polarity-independent)
|
||||
- DC+ rail: 10-16V after rectification
|
||||
- Forward drop: ~1V total (0.5V per diode pair)
|
||||
- SS54 Schottky: 5A continuous, 40V rating
|
||||
- Handles motor (1-3A) + logic (~200mA) simultaneously
|
||||
|
||||
**Component Values:**
|
||||
- Bridge: 4x SS54 (SMA package)
|
||||
- Bulk cap: 1000µF/25V electrolytic
|
||||
- LDO input cap: 10µF ceramic
|
||||
- LDO output cap: 100µF electrolytic + 0.1µF ceramic
|
||||
- TVS: P6KE24A or 1.5KE24CA
|
||||
|
||||
### RailCom Transmitter Schematic
|
||||
|
||||

|
||||
|
||||
```ditaa {cmd=true args=["-E"]}
|
||||
ESP32-H2 GPIO10 (UART1 TX)
|
||||
|
|
||||
v
|
||||
+-----+-----+
|
||||
| 1kΩ | Pull-up
|
||||
+-----+-----+
|
||||
|
|
||||
+--------+
|
||||
| |
|
||||
+-----+-----+ |
|
||||
| NPN BJT | | BC817 or 2N3904
|
||||
| Q1 |<-+
|
||||
+-----+-----+
|
||||
|C
|
||||
|
|
||||
+-----------+
|
||||
| |
|
||||
+----+----+ +---+---+
|
||||
| 10Ω | | 100pF | Snubber
|
||||
+----+----+ +---+---+
|
||||
| |
|
||||
+-----------+--------> To Track (via DCC cutout)
|
||||
|
|
||||
|
|
||||
+-----+-----+
|
||||
| 10kΩ | Pull-down
|
||||
+-----+-----+
|
||||
|
|
||||
v
|
||||
GND
|
||||
|
||||
RailCom Cutout Detection (Optional GPIO11)
|
||||
|
||||
Track Signal ---+
|
||||
|
|
||||
+---+---+
|
||||
|Voltage| Resistor divider
|
||||
|Divider| 22kΩ / 10kΩ
|
||||
+---+---+
|
||||
|
|
||||
+---> GPIO11 (Cutout Detect)
|
||||
|
|
||||
+---+---+
|
||||
| 0.1µF | Filter capacitor
|
||||
+---+---+
|
||||
|
|
||||
GND
|
||||
```
|
||||
|
||||
**RailCom Operation:**
|
||||
1. **Cutout Detection**: DCC command station creates ~450µs cutout window
|
||||
2. **Channel Timing**:
|
||||
- Channel 1: 26-177µs (address broadcast)
|
||||
- Channel 2: 193-454µs (status data)
|
||||
3. **Transmit**: UART TX at 250kbaud during cutout
|
||||
4. **Encoding**: 4-to-8 bit encoding per RailCom spec
|
||||
|
||||
**Components:**
|
||||
- Q1: BC817 NPN (SOT-23) or 2N3904
|
||||
- R1: 1kΩ base resistor
|
||||
- R2: 10Ω series resistor (current limit)
|
||||
- R3: 10kΩ pull-down
|
||||
- C1: 100pF snubber capacitor
|
||||
- Cutout divider: 22kΩ + 10kΩ (scales track voltage to 3.3V)
|
||||
|
||||
**Important Notes:**
|
||||
- RailCom transmits ONLY during DCC cutout window
|
||||
- Requires command station with RailCom support
|
||||
- Cutout detection is optional (can use timing from last DCC packet)
|
||||
- Q1 must switch fast enough for 250kbaud (BC817: fT=100MHz)
|
||||
|
||||
### Thermal Management
|
||||
|
||||
- Adequate copper pour for motor driver heat dissipation
|
||||
- Thermal vias under motor driver IC
|
||||
- Consider adding heatsink mounting holes
|
||||
- Keep power traces wide (minimum 2mm for motor power)
|
||||
- Bridge diodes: Place on copper pour for heat spreading
|
||||
|
||||
### Layout Guidelines
|
||||
|
||||
- Keep DCC input traces short and isolated
|
||||
- Star ground topology for power
|
||||
- Separate analog and digital grounds near ADC
|
||||
- Shield sensitive signals (DCC input, current sense)
|
||||
- Keep high-speed traces short (WS2812 data <10cm)
|
||||
- Proper decoupling capacitors near ICs (0.1µF within 5mm)
|
||||
- Wide traces for rectifier output (2-3mm minimum)
|
||||
|
||||
### Protection
|
||||
|
||||
- TVS diode on track input (P6KE24A bidirectional)
|
||||
- Schottky diodes provide inherent fast response
|
||||
|
||||
- Reverse polarity protection on track input
|
||||
- TVS diodes on all external connections
|
||||
- Overcurrent protection on motor output
|
||||
- ESD protection on user-accessible pins
|
||||
|
||||
### Testing
|
||||
|
||||
- Test points for key signals (DC+, 3.3V, DCC signal, motor outputs)
|
||||
- LED indicators for power (3.3V rail), DCC signal presence, status
|
||||
- Easy access to programming pins
|
||||
- Measure bridge rectifier forward drop (should be ~1V under load)
|
||||
- Current sense test point for motor current monitoring
|
||||
|
||||
## Future Enhancements
|
||||
|
||||
- Dual motor driver option
|
||||
- Sound module integration (I2S DAC)
|
||||
- Additional function outputs
|
||||
- Servo outputs (2-4 channels)
|
||||
- SUSI interface
|
||||
- Optional Bluetooth antenna
|
||||
|
||||
## Contributing
|
||||
|
||||
If you'd like to contribute to the hardware design:
|
||||
|
||||
1. Use KiCad 7.0 or newer
|
||||
2. Follow IPC design standards
|
||||
3. Include clear documentation
|
||||
4. Provide design rationale for key decisions
|
||||
5. Test thoroughly before sharing
|
||||
|
||||
## References
|
||||
|
||||
- [NMRA DCC Standards](https://www.nmra.org/dcc-standards)
|
||||
- [TB67H450FNG Datasheet](https://toshiba.semicon-storage.com/ap-en/semiconductor/product/motor-driver-ics/brushed-dc-motor-driver-ics/detail.TB67H450FNG.html)
|
||||
- [ESP32-H2 Datasheet](https://www.espressif.com/sites/default/files/documentation/esp32-h2_datasheet_en.pdf)
|
||||
- [RailCom Specification](https://www.lenz-elektronik.de/railcom/)
|
||||
|
||||
## License
|
||||
|
||||
Hardware designs will be released under CERN Open Hardware License v2 - Permissive (CERN-OHL-P).
|
||||
|
||||
---
|
||||
|
||||
**Status**: Planned for future release
|
||||
**Last Updated**: 2026-01-15
|
||||
39
ESP32/DCC-Loco/Hardware/power-supply.ditaa
Normal file
39
ESP32/DCC-Loco/Hardware/power-supply.ditaa
Normal file
@@ -0,0 +1,39 @@
|
||||
DCC Track Input (12-18V AC/DC)
|
||||
|
|
||||
v
|
||||
+-----+-----+
|
||||
| TVS | P6KE24A bidirectional
|
||||
| Diode |
|
||||
+-----+-----+
|
||||
|
|
||||
+-------------+-------------+
|
||||
| |
|
||||
Track+ Track-
|
||||
| |
|
||||
+-------+-------+ +-------+-------+
|
||||
| D1 | | D3 |
|
||||
| SS54 5A | | SS54 5A |
|
||||
+-------+-------+ +-------+-------+
|
||||
| |
|
||||
+--------->DC+<-------------+
|
||||
|
|
||||
| +----------------------------+
|
||||
+--| 1000uF/25V Electrolytic |
|
||||
| +----------------------------+
|
||||
|
|
||||
+---> TB67H450FNG VM (Motor Power)
|
||||
|
|
||||
| +----------------------------+
|
||||
+--| AMS1117-3.3 or HT7333 LDO |
|
||||
| +----------------------------+
|
||||
| |
|
||||
| +--| 100uF |---> 3.3V Logic
|
||||
| |
|
||||
+-------+-------+ | | +-------+-------+
|
||||
| D2 | | | | D4 |
|
||||
| SS54 5A | | | | SS54 5A |
|
||||
+-------+-------+ | | +-------+-------+
|
||||
| | | |
|
||||
+--------->GND<--------+----------+
|
||||
|
|
||||
Common Ground
|
||||
BIN
ESP32/DCC-Loco/Hardware/power-supply.png
Normal file
BIN
ESP32/DCC-Loco/Hardware/power-supply.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 26 KiB |
47
ESP32/DCC-Loco/Hardware/railcom.ditaa
Normal file
47
ESP32/DCC-Loco/Hardware/railcom.ditaa
Normal file
@@ -0,0 +1,47 @@
|
||||
ESP32-H2 GPIO10 (UART1 TX)
|
||||
|
|
||||
v
|
||||
+-----+-----+
|
||||
| 1kΩ | Pull-up
|
||||
+-----+-----+
|
||||
|
|
||||
+--------+
|
||||
| |
|
||||
+-----+-----+ |
|
||||
| NPN BJT | | BC817 or 2N3904
|
||||
| Q1 |<-+
|
||||
+-----+-----+
|
||||
|C
|
||||
|
|
||||
+-----------+
|
||||
| |
|
||||
+----+----+ +---+---+
|
||||
| 10Ω | | 100pF | Snubber
|
||||
+----+----+ +---+---+
|
||||
| |
|
||||
+-----------+--------> To Track (via DCC cutout)
|
||||
|
|
||||
|
|
||||
+-----+-----+
|
||||
| 10kΩ | Pull-down
|
||||
+-----+-----+
|
||||
|
|
||||
v
|
||||
GND
|
||||
|
||||
RailCom Cutout Detection (Optional GPIO11)
|
||||
|
||||
Track Signal ---+
|
||||
|
|
||||
+---+---+
|
||||
|Voltage| Resistor divider
|
||||
|Divider| 22kΩ / 10kΩ
|
||||
+---+---+
|
||||
|
|
||||
+---> GPIO11 (Cutout Detect)
|
||||
|
|
||||
+---+---+
|
||||
| 0.1µF | Filter capacitor
|
||||
+---+---+
|
||||
|
|
||||
GND
|
||||
BIN
ESP32/DCC-Loco/Hardware/railcom.png
Normal file
BIN
ESP32/DCC-Loco/Hardware/railcom.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 30 KiB |
544
ESP32/DCC-Loco/README.md
Normal file
544
ESP32/DCC-Loco/README.md
Normal file
@@ -0,0 +1,544 @@
|
||||
# DCC Locomotive Decoder
|
||||
|
||||
ESP32-H2 based DCC locomotive decoder with advanced features for model railroading.
|
||||
|
||||
## Features
|
||||
|
||||
- **DCC Signal Decoding**: Full NMRA-compliant DCC decoder supporting short (1-127) and long (128-10239) addresses
|
||||
- **Motor Control**: TB67H450FNG H-bridge motor driver with:
|
||||
- 128-step speed control
|
||||
- Configurable acceleration/deceleration
|
||||
- Load compensation with PID control
|
||||
- Current sensing
|
||||
- **LED Control**: WS2812 addressable LED support
|
||||
- Multiple lighting effects (solid, blink, pulse, directional)
|
||||
- Function-mapped lighting
|
||||
- Adjustable brightness
|
||||
- **RailCom Feedback**: Bidirectional communication with command station
|
||||
- **Accessory Outputs**: 2x N-channel MOSFET outputs for accessories
|
||||
- Smoke generators
|
||||
- Sound modules
|
||||
- Other low-side switched loads
|
||||
- **Configuration**: WiFi/Bluetooth configuration via WebSocket
|
||||
- Web-based interface
|
||||
- CV (Configuration Variable) management
|
||||
- Real-time status monitoring
|
||||
|
||||
## Hardware Requirements
|
||||
|
||||
### Components
|
||||
|
||||
- **ESP32-H2 Development Board** (e.g., ESP32-H2-DevKitM-1)
|
||||
- **TB67H450FNG** Motor Driver IC
|
||||
- **WS2812** or compatible addressable LEDs
|
||||
- **N-channel MOSFETs** (2x) for accessory outputs (e.g., IRLZ44N)
|
||||
- **Optocoupler** for DCC signal isolation (e.g., 6N137 or PC817)
|
||||
- **Current sense resistor** (0.1Ω - 0.5Ω, 1W or higher)
|
||||
- **Capacitors**: 100µF electrolytic, 0.1µF ceramic
|
||||
- **Resistors**: Pull-ups/pull-downs as needed
|
||||
- **Push button** for configuration mode
|
||||
|
||||
### Power Supply
|
||||
|
||||
- **Track Power**: 12-18V DC from DCC track
|
||||
- **Logic Power**: 3.3V for ESP32-H2 (use onboard regulator or external LDO)
|
||||
- **Motor Power**: Same as track power (filtered)
|
||||
|
||||
## Wiring Diagram
|
||||
|
||||
### DCC Input
|
||||
```
|
||||
DCC Track Signal
|
||||
|
|
||||
+---[1kΩ]---+---[Optocoupler Anode]
|
||||
| |
|
||||
[10kΩ] [0.1µF]
|
||||
| |
|
||||
GND GND
|
||||
|
||||
Optocoupler Cathode ---[470Ω]--- 3.3V
|
||||
Optocoupler Output --- GPIO4 (PIN_DCC_INPUT)
|
||||
Optocoupler Ground --- GND
|
||||
```
|
||||
|
||||
### TB67H450FNG Motor Driver
|
||||
```
|
||||
ESP32-H2 TB67H450FNG Motor
|
||||
GPIO5 ----------- IN1
|
||||
GPIO6 ----------- IN2
|
||||
GPIO7 ----------- PWM
|
||||
OUT1 --------------- M+
|
||||
OUT2 --------------- M-
|
||||
VM ---------------- Track+ (12-18V)
|
||||
VCC ---------------- 3.3V
|
||||
GND ---------------- GND
|
||||
|
||||
Current Sensing:
|
||||
IPROPI ------------- GPIO8 (via voltage divider)
|
||||
[0.1Ω Rs between OUT2 and GND]
|
||||
```
|
||||
|
||||
**TB67H450FNG Pin Configuration:**
|
||||
| Pin | Connection | Description |
|
||||
|-----|------------|-------------|
|
||||
| VM | Track Power (12-18V) | Motor power supply |
|
||||
| VCC | 3.3V | Logic power supply |
|
||||
| IN1 | GPIO5 | Input 1 (phase A) |
|
||||
| IN2 | GPIO6 | Input 2 (phase B) |
|
||||
| PWM | GPIO7 | PWM speed control |
|
||||
| OUT1 | Motor+ | Motor output 1 |
|
||||
| OUT2 | Motor- | Motor output 2 |
|
||||
| IPROPI | GPIO8 | Current monitor output |
|
||||
| GND | GND | Ground |
|
||||
|
||||
**Motor Control Truth Table:**
|
||||
| IN1 | IN2 | PWM | Operation |
|
||||
|-----|-----|-----|-----------|
|
||||
| L | L | X | Brake (standby) |
|
||||
| H | L | PWM | Forward |
|
||||
| L | H | PWM | Reverse |
|
||||
| H | H | X | Brake (active) |
|
||||
|
||||
### WS2812 LED Strip
|
||||
```
|
||||
ESP32-H2 WS2812
|
||||
GPIO9 ----------- DIN (Data In)
|
||||
3.3V/5V --------- VCC (check LED voltage requirements)
|
||||
GND ------------- GND
|
||||
|
||||
Note: Add a 330-470Ω resistor in series with DIN
|
||||
Add a 100-1000µF capacitor across VCC and GND near LEDs
|
||||
```
|
||||
|
||||
### RailCom
|
||||
```
|
||||
ESP32-H2 Circuit
|
||||
GPIO10 ---------- UART TX (to RailCom transmitter)
|
||||
GPIO11 ---------- DCC Cutout Detection (optional)
|
||||
|
||||
RailCom Transmitter Circuit:
|
||||
UART TX --- [Transistor Driver] --- Track Signal
|
||||
(Sends during DCC cutout window)
|
||||
```
|
||||
|
||||
### Accessory Outputs (N-FETs)
|
||||
```
|
||||
ESP32-H2 N-FET (IRLZ44N) Load
|
||||
GPIO12 ---------- Gate 1
|
||||
Source 1 ---------- GND
|
||||
Drain 1 ----------- Load 1 (-)
|
||||
|
||||
GPIO13 ---------- Gate 2
|
||||
Source 2 ---------- GND
|
||||
Drain 2 ----------- Load 2 (-)
|
||||
|
||||
Load (+) connects to positive supply
|
||||
Add 10kΩ pull-down resistor from Gate to Source on each FET
|
||||
```
|
||||
|
||||
### Configuration Button
|
||||
```
|
||||
GPIO14 ---------- Button ---------- GND
|
||||
(Internal pull-up enabled)
|
||||
```
|
||||
|
||||
### Complete Pin Assignment Table
|
||||
|
||||
| Pin | Function | Connection | Notes |
|
||||
|-----|----------|------------|-------|
|
||||
| GPIO4 | DCC Input | Optocoupler output | DCC signal from track |
|
||||
| GPIO5 | Motor IN1 | TB67H450FNG IN1 | Motor phase A |
|
||||
| GPIO6 | Motor IN2 | TB67H450FNG IN2 | Motor phase B |
|
||||
| GPIO7 | Motor PWM | TB67H450FNG PWM | Speed control |
|
||||
| GPIO8 | Current Sense | TB67H450FNG IPROPI | ADC input |
|
||||
| GPIO9 | LED Data | WS2812 DIN | LED control |
|
||||
| GPIO10 | RailCom TX | UART1 TX | RailCom feedback |
|
||||
| GPIO11 | Cutout Detect | DCC cutout circuit | Optional |
|
||||
| GPIO12 | Accessory 1 | N-FET Gate | Output 1 |
|
||||
| GPIO13 | Accessory 2 | N-FET Gate | Output 2 |
|
||||
| GPIO14 | Config Button | Push button to GND | Enter config mode |
|
||||
|
||||
**Note:** Pin assignments can be modified in `src/main.cpp` (PIN DEFINITIONS section).
|
||||
|
||||
## Software Setup
|
||||
|
||||
### Prerequisites
|
||||
|
||||
- [PlatformIO](https://platformio.org/) installed
|
||||
- Git (optional)
|
||||
|
||||
### Installation
|
||||
|
||||
1. Clone or download this repository
|
||||
2. Open the `DCC-Loco` folder in PlatformIO (VS Code with PlatformIO extension)
|
||||
3. Build the project:
|
||||
```bash
|
||||
pio run
|
||||
```
|
||||
4. Upload to ESP32-H2:
|
||||
```bash
|
||||
pio run --target upload
|
||||
```
|
||||
5. Monitor serial output:
|
||||
```bash
|
||||
pio device monitor
|
||||
```
|
||||
|
||||
### Configuration
|
||||
|
||||
#### Initial Setup
|
||||
|
||||
On first boot, the decoder initializes with default values:
|
||||
- **Address**: 3 (short address)
|
||||
- **Acceleration**: 10
|
||||
- **Deceleration**: 10
|
||||
- **LED Brightness**: 128 (50%)
|
||||
- **RailCom**: Enabled
|
||||
- **Load Compensation**: Enabled
|
||||
|
||||
#### Configuration Mode
|
||||
|
||||
To enter configuration mode:
|
||||
1. Hold the configuration button (GPIO14) for 3 seconds
|
||||
2. The decoder creates a WiFi Access Point:
|
||||
- **SSID**: `DCC-Loco-XXXXXX` (XXXXXX = device ID)
|
||||
- **Password**: `dcc12345`
|
||||
3. Connect to the WiFi AP
|
||||
4. Open a web browser and navigate to `http://192.168.4.1`
|
||||
5. Use the web interface to:
|
||||
- Read/Write Configuration Variables (CVs)
|
||||
- Test outputs
|
||||
- Monitor decoder status
|
||||
- Reset to defaults
|
||||
6. Press the configuration button again to exit config mode
|
||||
|
||||
#### Configuration Variables (CVs)
|
||||
|
||||
Standard NMRA CVs:
|
||||
|
||||
| CV | Name | Default | Description |
|
||||
|----|------|---------|-------------|
|
||||
| 1 | Primary Address | 3 | Short address (1-127) |
|
||||
| 2 | Vstart | 1 | Start voltage |
|
||||
| 3 | Acceleration Rate | 10 | Acceleration rate (0-255) |
|
||||
| 4 | Deceleration Rate | 10 | Deceleration rate (0-255) |
|
||||
| 5 | Vhigh | 255 | Maximum voltage |
|
||||
| 6 | Vmid | 128 | Mid voltage |
|
||||
| 7 | Version ID | 1 | Decoder version |
|
||||
| 8 | Manufacturer ID | 13 | Manufacturer ID (DIY) |
|
||||
| 17-18 | Extended Address | - | Long address (128-10239) |
|
||||
| 29 | Configuration Data | 6 | Config bits (address mode, speed steps) |
|
||||
|
||||
Custom CVs:
|
||||
|
||||
| CV | Name | Default | Description |
|
||||
|----|------|---------|-------------|
|
||||
| 50 | Motor Kp | 50 | PID proportional gain (value/10) |
|
||||
| 51 | Motor Ki | 5 | PID integral gain (value/10) |
|
||||
| 52 | Motor Kd | 10 | PID derivative gain (value/10) |
|
||||
| 53 | RailCom Enable | 1 | Enable RailCom (0=off, 1=on) |
|
||||
| 54 | Load Comp Enable | 1 | Enable load compensation (0=off, 1=on) |
|
||||
| 55 | LED Brightness | 128 | LED brightness (0-255) |
|
||||
| 56 | Accessory 1 Mode | 2 | Accessory output 1 mode |
|
||||
| 57 | Accessory 2 Mode | 2 | Accessory output 2 mode |
|
||||
|
||||
Accessory Modes:
|
||||
- 0 = Always off
|
||||
- 1 = Always on
|
||||
- 2 = Function controlled
|
||||
- 3 = PWM control
|
||||
- 4 = Blinking
|
||||
- 5 = Speed dependent
|
||||
|
||||
### WebSocket Protocol
|
||||
|
||||
The configuration server uses WebSocket for real-time communication.
|
||||
|
||||
**Connect:** `ws://<decoder-ip>/ws`
|
||||
|
||||
**Commands:**
|
||||
|
||||
Read CV:
|
||||
```json
|
||||
{
|
||||
"command": "read_cv",
|
||||
"cv": 1
|
||||
}
|
||||
```
|
||||
|
||||
Write CV:
|
||||
```json
|
||||
{
|
||||
"command": "write_cv",
|
||||
"cv": 1,
|
||||
"value": 5
|
||||
}
|
||||
```
|
||||
|
||||
Get Status:
|
||||
```json
|
||||
{
|
||||
"command": "get_status"
|
||||
}
|
||||
```
|
||||
|
||||
Reset to Defaults:
|
||||
```json
|
||||
{
|
||||
"command": "reset"
|
||||
}
|
||||
```
|
||||
|
||||
**Responses:**
|
||||
|
||||
CV Read:
|
||||
```json
|
||||
{
|
||||
"type": "cv_read",
|
||||
"cv": 1,
|
||||
"value": 3
|
||||
}
|
||||
```
|
||||
|
||||
Status:
|
||||
```json
|
||||
{
|
||||
"type": "status",
|
||||
"address": 3,
|
||||
"speed": 0,
|
||||
"direction": true,
|
||||
"signal": true,
|
||||
"current": 150,
|
||||
"functions": [false, false, true, ...]
|
||||
}
|
||||
```
|
||||
|
||||
## Code Structure
|
||||
|
||||
```
|
||||
DCC-Loco/
|
||||
├── platformio.ini # PlatformIO configuration
|
||||
├── README.md # This file
|
||||
├── include/ # Header files
|
||||
│ ├── DCCDecoder.h # DCC signal decoding
|
||||
│ ├── CVManager.h # Configuration variable management
|
||||
│ ├── LEDController.h # WS2812 LED control
|
||||
│ ├── MotorDriver.h # TB67H450FNG motor control
|
||||
│ ├── RailCom.h # RailCom feedback
|
||||
│ ├── AccessoryOutputs.h # Accessory output control
|
||||
│ └── ConfigServer.h # WiFi/Bluetooth config server
|
||||
├── src/ # Implementation files
|
||||
│ ├── main.cpp # Main application
|
||||
│ ├── DCCDecoder.cpp
|
||||
│ ├── CVManager.cpp
|
||||
│ ├── LEDController.cpp
|
||||
│ ├── MotorDriver.cpp
|
||||
│ ├── RailCom.cpp
|
||||
│ ├── AccessoryOutputs.cpp
|
||||
│ └── ConfigServer.cpp
|
||||
├── lib/ # Custom libraries (if any)
|
||||
├── data/ # Web files (future use)
|
||||
└── Hardware/ # KiCad project files (future)
|
||||
```
|
||||
|
||||
## Module Descriptions
|
||||
|
||||
### DCCDecoder
|
||||
Decodes DCC packets using interrupt-driven bit detection. Supports:
|
||||
- Short and long addresses
|
||||
- 128-step speed control
|
||||
- Functions F0-F28
|
||||
- Emergency stop
|
||||
- Signal quality monitoring
|
||||
|
||||
### CVManager
|
||||
Manages Configuration Variables in non-volatile storage using ESP32 Preferences:
|
||||
- NMRA-compliant CV storage
|
||||
- Factory reset functionality
|
||||
- Address management (short/long)
|
||||
|
||||
### LEDController
|
||||
Controls WS2812 addressable LEDs with FastLED:
|
||||
- Multiple light modes (solid, blink, pulse, directional)
|
||||
- Function mapping to LEDs
|
||||
- Brightness control
|
||||
- Up to 16 LEDs
|
||||
|
||||
### MotorDriver
|
||||
Controls TB67H450FNG motor driver:
|
||||
- Forward/reverse control
|
||||
- PWM speed control
|
||||
- Acceleration/deceleration curves
|
||||
- Load compensation with PID
|
||||
- Current monitoring
|
||||
|
||||
### RailCom
|
||||
Implements RailCom feedback protocol:
|
||||
- Channel 1: Address broadcast
|
||||
- Channel 2: Status information
|
||||
- 250kbaud communication
|
||||
- Cutout detection
|
||||
|
||||
### AccessoryOutputs
|
||||
Controls 2x N-FET outputs:
|
||||
- Multiple modes (on/off, function, PWM, blink, speed-dependent)
|
||||
- Function mapping
|
||||
- Independent control
|
||||
|
||||
### ConfigServer
|
||||
Web-based configuration interface:
|
||||
- WiFi Access Point mode
|
||||
- WebSocket real-time communication
|
||||
- CV read/write
|
||||
- Status monitoring
|
||||
- Reset functionality
|
||||
|
||||
## Testing
|
||||
|
||||
### Basic Test Procedure
|
||||
|
||||
1. **Power Up Test**
|
||||
- Connect decoder to track power (12-18V DC)
|
||||
- Verify ESP32-H2 boots (check serial output)
|
||||
- Verify no smoke or excessive heat
|
||||
|
||||
2. **DCC Signal Test**
|
||||
- Apply DCC signal to track
|
||||
- Check serial monitor for "DCC OK" messages
|
||||
- Verify correct address detection
|
||||
|
||||
3. **Motor Test**
|
||||
- Send speed commands via DCC controller
|
||||
- Verify smooth acceleration/deceleration
|
||||
- Test forward and reverse
|
||||
- Check emergency stop
|
||||
|
||||
4. **LED Test**
|
||||
- Verify headlights change with direction
|
||||
- Test function-controlled LEDs (F1, F2, etc.)
|
||||
- Check brightness adjustment
|
||||
|
||||
5. **Accessory Test**
|
||||
- Activate mapped functions (F3, F4)
|
||||
- Verify N-FET outputs switch correctly
|
||||
- Test different output modes
|
||||
|
||||
6. **Configuration Test**
|
||||
- Enter configuration mode (hold button 3s)
|
||||
- Connect to WiFi AP
|
||||
- Read/write CVs via web interface
|
||||
- Verify changes take effect after exit
|
||||
|
||||
### Troubleshooting
|
||||
|
||||
**No DCC Signal:**
|
||||
- Check optocoupler wiring
|
||||
- Verify GPIO4 receives signal
|
||||
- Check for proper DCC track voltage
|
||||
|
||||
**Motor doesn't run:**
|
||||
- Verify TB67H450FNG connections
|
||||
- Check motor power supply (VM)
|
||||
- Verify PWM signal on GPIO7
|
||||
- Check motor connections
|
||||
|
||||
**LEDs don't light:**
|
||||
- Verify WS2812 data line connection
|
||||
- Check LED power supply voltage
|
||||
- Ensure correct NUM_LEDS setting
|
||||
- Check for loose connections
|
||||
|
||||
**Can't enter config mode:**
|
||||
- Verify button wiring (GPIO14 to GND)
|
||||
- Check serial monitor for messages
|
||||
- Try holding button longer (>3s)
|
||||
|
||||
**WiFi AP not visible:**
|
||||
- Check ESP32-H2 WiFi support
|
||||
- Verify sufficient power supply
|
||||
- Check for WiFi interference
|
||||
- Review serial output for errors
|
||||
|
||||
## Advanced Features
|
||||
|
||||
### Load Compensation
|
||||
|
||||
The decoder includes PID-based load compensation to maintain consistent speed under varying loads:
|
||||
- Monitors motor current
|
||||
- Adjusts PWM duty cycle
|
||||
- Tunable via CVs 50-52
|
||||
- Can be disabled via CV54
|
||||
|
||||
### Custom Function Mapping
|
||||
|
||||
Edit `src/main.cpp` to customize LED and accessory mappings:
|
||||
|
||||
```cpp
|
||||
// Example: Map F5 to LED 2 with pulse effect
|
||||
ledController.mapFunctionToLED(5, 2, LIGHT_PULSE);
|
||||
|
||||
// Example: Map F6 to accessory output 1
|
||||
accessories.mapFunction(1, 6);
|
||||
```
|
||||
|
||||
### RailCom Customization
|
||||
|
||||
Extend RailCom data transmission in `src/RailCom.cpp`:
|
||||
- Add more status information in Channel 2
|
||||
- Implement CV read-back
|
||||
- Add custom data fields
|
||||
|
||||
## Future Enhancements
|
||||
|
||||
- [ ] Sound decoder support
|
||||
- [ ] SUSI interface for external sound modules
|
||||
- [ ] Bluetooth configuration
|
||||
- [ ] Advanced lighting effects (mars light, ditch lights)
|
||||
- [ ] Function remapping via CV
|
||||
- [ ] Dual motor support
|
||||
- [ ] ABC brake support
|
||||
- [ ] Servo outputs
|
||||
|
||||
## Hardware Design Files
|
||||
|
||||
KiCad schematic and PCB files will be added to the `Hardware/` folder in future releases.
|
||||
|
||||
## License
|
||||
|
||||
This project is open-source and available under the MIT License.
|
||||
|
||||
## Contributing
|
||||
|
||||
Contributions are welcome! Please:
|
||||
1. Fork the repository
|
||||
2. Create a feature branch
|
||||
3. Commit your changes
|
||||
4. Submit a pull request
|
||||
|
||||
## Support
|
||||
|
||||
For issues, questions, or suggestions:
|
||||
- Open an issue on GitHub
|
||||
- Check documentation in `doc/` folder
|
||||
- Review source code comments
|
||||
|
||||
## Credits
|
||||
|
||||
- NMRA DCC specifications
|
||||
- ESP32-H2 Arduino core
|
||||
- FastLED library
|
||||
- AsyncWebServer library
|
||||
|
||||
## Version History
|
||||
|
||||
- **v1.0** (2026-01-15): Initial release
|
||||
- DCC decoding
|
||||
- Motor control with load compensation
|
||||
- WS2812 LED support
|
||||
- RailCom feedback
|
||||
- Accessory outputs
|
||||
- WiFi configuration
|
||||
|
||||
---
|
||||
|
||||
**Happy Model Railroading!** 🚂
|
||||
101
ESP32/DCC-Loco/include/AccessoryOutputs.h
Normal file
101
ESP32/DCC-Loco/include/AccessoryOutputs.h
Normal file
@@ -0,0 +1,101 @@
|
||||
/**
|
||||
* @file AccessoryOutputs.h
|
||||
* @brief Accessory Output Controller (N-channel MOSFETs)
|
||||
*
|
||||
* Controls 2 N-channel MOSFET outputs for accessories like smoke generators,
|
||||
* sound modules, or other low-side switched loads.
|
||||
*/
|
||||
|
||||
#ifndef ACCESSORY_OUTPUTS_H
|
||||
#define ACCESSORY_OUTPUTS_H
|
||||
|
||||
#include <Arduino.h>
|
||||
|
||||
enum AccessoryMode {
|
||||
ACC_OFF = 0, // Always off
|
||||
ACC_ON = 1, // Always on
|
||||
ACC_FUNCTION = 2, // Controlled by DCC function
|
||||
ACC_PWM = 3, // PWM control
|
||||
ACC_BLINK = 4, // Blinking mode
|
||||
ACC_SPEED_DEPENDENT = 5 // Output follows speed
|
||||
};
|
||||
|
||||
class AccessoryOutputs {
|
||||
public:
|
||||
AccessoryOutputs();
|
||||
|
||||
/**
|
||||
* @brief Initialize accessory outputs
|
||||
* @param output1Pin GPIO for accessory output 1 (N-FET gate)
|
||||
* @param output2Pin GPIO for accessory output 2 (N-FET gate)
|
||||
* @return true if successful
|
||||
*/
|
||||
bool begin(uint8_t output1Pin, uint8_t output2Pin);
|
||||
|
||||
/**
|
||||
* @brief Set output mode
|
||||
* @param outputNum Output number (1 or 2)
|
||||
* @param mode Accessory mode
|
||||
*/
|
||||
void setMode(uint8_t outputNum, AccessoryMode mode);
|
||||
|
||||
/**
|
||||
* @brief Set PWM duty cycle for output
|
||||
* @param outputNum Output number (1 or 2)
|
||||
* @param dutyCycle Duty cycle (0-255)
|
||||
*/
|
||||
void setPWM(uint8_t outputNum, uint8_t dutyCycle);
|
||||
|
||||
/**
|
||||
* @brief Map DCC function to output
|
||||
* @param outputNum Output number (1 or 2)
|
||||
* @param functionNum DCC function number (0-28)
|
||||
*/
|
||||
void mapFunction(uint8_t outputNum, uint8_t functionNum);
|
||||
|
||||
/**
|
||||
* @brief Update function state
|
||||
* @param functionNum Function number (0-28)
|
||||
* @param state Function state (true = on)
|
||||
*/
|
||||
void setFunctionState(uint8_t functionNum, bool state);
|
||||
|
||||
/**
|
||||
* @brief Set speed for speed-dependent mode
|
||||
* @param speed Speed value (0-126)
|
||||
*/
|
||||
void setSpeed(uint8_t speed);
|
||||
|
||||
/**
|
||||
* @brief Update outputs (call regularly from loop)
|
||||
*/
|
||||
void update();
|
||||
|
||||
/**
|
||||
* @brief Direct output control
|
||||
* @param outputNum Output number (1 or 2)
|
||||
* @param state Output state (true = on)
|
||||
*/
|
||||
void setOutput(uint8_t outputNum, bool state);
|
||||
|
||||
private:
|
||||
uint8_t pins[2];
|
||||
AccessoryMode modes[2];
|
||||
uint8_t pwmValues[2];
|
||||
uint8_t mappedFunctions[2];
|
||||
bool functionStates[29]; // F0-F28
|
||||
uint8_t currentSpeed;
|
||||
|
||||
// PWM channels
|
||||
const uint8_t pwmChannels[2] = {2, 3};
|
||||
const uint32_t pwmFrequency = 1000; // 1 kHz
|
||||
const uint8_t pwmResolution = 8;
|
||||
|
||||
// Blink timing
|
||||
unsigned long lastBlinkUpdate;
|
||||
bool blinkState;
|
||||
|
||||
void updateOutput(uint8_t outputNum);
|
||||
};
|
||||
|
||||
#endif // ACCESSORY_OUTPUTS_H
|
||||
100
ESP32/DCC-Loco/include/CVManager.h
Normal file
100
ESP32/DCC-Loco/include/CVManager.h
Normal file
@@ -0,0 +1,100 @@
|
||||
/**
|
||||
* @file CVManager.h
|
||||
* @brief Configuration Variable (CV) Manager
|
||||
*
|
||||
* Manages NMRA-compliant Configuration Variables stored in non-volatile memory.
|
||||
* Supports programming track operations and service mode programming.
|
||||
*/
|
||||
|
||||
#ifndef CV_MANAGER_H
|
||||
#define CV_MANAGER_H
|
||||
|
||||
#include <Arduino.h>
|
||||
#include <Preferences.h>
|
||||
|
||||
// Standard DCC CVs
|
||||
#define CV_PRIMARY_ADDRESS 1 // Short address (1-127)
|
||||
#define CV_VSTART 2 // Start voltage
|
||||
#define CV_ACCEL_RATE 3 // Acceleration rate
|
||||
#define CV_DECEL_RATE 4 // Deceleration rate
|
||||
#define CV_VHIGH 5 // Max voltage
|
||||
#define CV_VMID 6 // Mid voltage
|
||||
#define CV_VERSION_ID 7 // Manufacturer version
|
||||
#define CV_MANUFACTURER_ID 8 // Manufacturer ID
|
||||
#define CV_TOTAL_PWM_PERIOD 9 // PWM period
|
||||
#define CV_EMF_FEEDBACK_CUTOUT 10 // EMF feedback cutout
|
||||
#define CV_PACKET_TIMEOUT 11 // Packet timeout
|
||||
#define CV_EXTENDED_ADDRESS_HIGH 17 // Long address high byte
|
||||
#define CV_EXTENDED_ADDRESS_LOW 18 // Long address low byte
|
||||
#define CV_CONSIST_ADDRESS 19 // Consist address
|
||||
#define CV_CONFIG_DATA_1 29 // Configuration data
|
||||
|
||||
// Custom CVs for this decoder
|
||||
#define CV_MOTOR_KP 50 // Motor PID Kp
|
||||
#define CV_MOTOR_KI 51 // Motor PID Ki
|
||||
#define CV_MOTOR_KD 52 // Motor PID Kd
|
||||
#define CV_RAILCOM_ENABLE 53 // RailCom enable
|
||||
#define CV_LOAD_COMP_ENABLE 54 // Load compensation enable
|
||||
#define CV_LED_BRIGHTNESS 55 // LED brightness
|
||||
#define CV_ACCESSORY_1_MODE 56 // Accessory output 1 mode
|
||||
#define CV_ACCESSORY_2_MODE 57 // Accessory output 2 mode
|
||||
|
||||
#define MAX_CV_NUMBER 1024
|
||||
|
||||
class CVManager {
|
||||
public:
|
||||
CVManager();
|
||||
|
||||
/**
|
||||
* @brief Initialize CV manager and load from NVS
|
||||
* @return true if successful
|
||||
*/
|
||||
bool begin();
|
||||
|
||||
/**
|
||||
* @brief Read CV value
|
||||
* @param cvNumber CV number (1-1024)
|
||||
* @param defaultValue Default value if CV not set
|
||||
* @return CV value
|
||||
*/
|
||||
uint8_t readCV(uint16_t cvNumber, uint8_t defaultValue = 0);
|
||||
|
||||
/**
|
||||
* @brief Write CV value
|
||||
* @param cvNumber CV number (1-1024)
|
||||
* @param value Value to write
|
||||
* @return true if successful
|
||||
*/
|
||||
bool writeCV(uint16_t cvNumber, uint8_t value);
|
||||
|
||||
/**
|
||||
* @brief Reset all CVs to factory defaults
|
||||
*/
|
||||
void resetToDefaults();
|
||||
|
||||
/**
|
||||
* @brief Get locomotive address from CVs
|
||||
* @return Locomotive address (1-10239)
|
||||
*/
|
||||
uint16_t getLocoAddress();
|
||||
|
||||
/**
|
||||
* @brief Set locomotive address in CVs
|
||||
* @param address Address to set (1-10239)
|
||||
*/
|
||||
void setLocoAddress(uint16_t address);
|
||||
|
||||
/**
|
||||
* @brief Check if using extended (long) address
|
||||
* @return true if using long address
|
||||
*/
|
||||
bool isLongAddress();
|
||||
|
||||
private:
|
||||
Preferences preferences;
|
||||
|
||||
void setDefaultCVs();
|
||||
String getCVKey(uint16_t cvNumber);
|
||||
};
|
||||
|
||||
#endif // CV_MANAGER_H
|
||||
82
ESP32/DCC-Loco/include/ConfigServer.h
Normal file
82
ESP32/DCC-Loco/include/ConfigServer.h
Normal file
@@ -0,0 +1,82 @@
|
||||
/**
|
||||
* @file ConfigServer.h
|
||||
* @brief WiFi/Bluetooth Configuration Server
|
||||
*
|
||||
* Provides WebSocket-based configuration interface over WiFi or Bluetooth.
|
||||
* Allows reading/writing CVs, testing outputs, and monitoring decoder status.
|
||||
*/
|
||||
|
||||
#ifndef CONFIG_SERVER_H
|
||||
#define CONFIG_SERVER_H
|
||||
|
||||
#include <Arduino.h>
|
||||
#include <WiFi.h>
|
||||
#include <AsyncTCP.h>
|
||||
#include <ESPAsyncWebServer.h>
|
||||
#include <ArduinoJson.h>
|
||||
#include "CVManager.h"
|
||||
|
||||
class ConfigServer {
|
||||
public:
|
||||
ConfigServer(CVManager& cvManager);
|
||||
|
||||
/**
|
||||
* @brief Initialize configuration server
|
||||
* @param ssid WiFi SSID (nullptr for AP mode with default name)
|
||||
* @param password WiFi password
|
||||
* @param useAP true for AP mode, false for station mode
|
||||
* @return true if successful
|
||||
*/
|
||||
bool begin(const char* ssid = nullptr, const char* password = nullptr, bool useAP = true);
|
||||
|
||||
/**
|
||||
* @brief Stop configuration server
|
||||
*/
|
||||
void stop();
|
||||
|
||||
/**
|
||||
* @brief Check if configuration mode is active
|
||||
*/
|
||||
bool isActive() const { return active; }
|
||||
|
||||
/**
|
||||
* @brief Update server (call from loop)
|
||||
*/
|
||||
void update();
|
||||
|
||||
/**
|
||||
* @brief Set decoder status callback
|
||||
* Function signature: void callback(JsonObject& status)
|
||||
*/
|
||||
typedef void (*StatusCallback)(JsonObject& status);
|
||||
void setStatusCallback(StatusCallback callback);
|
||||
|
||||
private:
|
||||
CVManager& cvMgr;
|
||||
AsyncWebServer* server;
|
||||
AsyncWebSocket* ws;
|
||||
bool active;
|
||||
|
||||
StatusCallback statusCallback;
|
||||
unsigned long lastStatusUpdate;
|
||||
|
||||
void setupWebSocket();
|
||||
void setupHTTPRoutes();
|
||||
void handleWebSocketMessage(void* arg, uint8_t* data, size_t len);
|
||||
void handleWebSocketEvent(AsyncWebSocket* server, AsyncWebSocketClient* client,
|
||||
AwsEventType type, void* arg, uint8_t* data, size_t len);
|
||||
|
||||
void handleReadCV(AsyncWebSocketClient* client, JsonObject& json);
|
||||
void handleWriteCV(AsyncWebSocketClient* client, JsonObject& json);
|
||||
void handleGetStatus(AsyncWebSocketClient* client);
|
||||
void handleTestOutput(AsyncWebSocketClient* client, JsonObject& json);
|
||||
void handleReset(AsyncWebSocketClient* client);
|
||||
|
||||
void sendResponse(AsyncWebSocketClient* client, const char* type,
|
||||
bool success, const char* message = nullptr);
|
||||
void broadcastStatus();
|
||||
|
||||
String getDefaultAPName();
|
||||
};
|
||||
|
||||
#endif // CONFIG_SERVER_H
|
||||
100
ESP32/DCC-Loco/include/DCCDecoder.h
Normal file
100
ESP32/DCC-Loco/include/DCCDecoder.h
Normal file
@@ -0,0 +1,100 @@
|
||||
/**
|
||||
* @file DCCDecoder.h
|
||||
* @brief DCC Signal Decoder for locomotive control
|
||||
*
|
||||
* Decodes DCC packets from the track signal, extracts speed and function commands,
|
||||
* and manages locomotive addressing (short/long address support).
|
||||
*/
|
||||
|
||||
#ifndef DCC_DECODER_H
|
||||
#define DCC_DECODER_H
|
||||
|
||||
#include <Arduino.h>
|
||||
|
||||
// DCC Timing constants (in microseconds)
|
||||
#define DCC_ONE_BIT_MIN 52
|
||||
#define DCC_ONE_BIT_MAX 64
|
||||
#define DCC_ZERO_BIT_MIN 95
|
||||
#define DCC_ZERO_BIT_MAX 9900
|
||||
|
||||
// Maximum packet size
|
||||
#define MAX_DCC_PACKET_SIZE 6
|
||||
|
||||
class DCCDecoder {
|
||||
public:
|
||||
DCCDecoder();
|
||||
|
||||
/**
|
||||
* @brief Initialize the DCC decoder
|
||||
* @param dccPin GPIO pin for DCC signal input
|
||||
* @return true if initialization successful
|
||||
*/
|
||||
bool begin(uint8_t dccPin);
|
||||
|
||||
/**
|
||||
* @brief Process DCC signal (call frequently from loop or ISR)
|
||||
*/
|
||||
void process();
|
||||
|
||||
/**
|
||||
* @brief Get current speed value (0-126, 0=stop, 1=emergency stop)
|
||||
* @return Current speed
|
||||
*/
|
||||
uint8_t getSpeed() const { return currentSpeed; }
|
||||
|
||||
/**
|
||||
* @brief Get current direction
|
||||
* @return true = forward, false = reverse
|
||||
*/
|
||||
bool getDirection() const { return direction; }
|
||||
|
||||
/**
|
||||
* @brief Get function state (F0-F28)
|
||||
* @param functionNum Function number (0-28)
|
||||
* @return Function state (true = on)
|
||||
*/
|
||||
bool getFunction(uint8_t functionNum) const;
|
||||
|
||||
/**
|
||||
* @brief Check if decoder has received valid packets recently
|
||||
* @return true if signal is valid
|
||||
*/
|
||||
bool hasValidSignal() const;
|
||||
|
||||
/**
|
||||
* @brief Set locomotive address
|
||||
* @param address Locomotive address (1-10239)
|
||||
*/
|
||||
void setAddress(uint16_t address);
|
||||
|
||||
/**
|
||||
* @brief Get current locomotive address
|
||||
*/
|
||||
uint16_t getAddress() const { return locoAddress; }
|
||||
|
||||
private:
|
||||
static void IRAM_ATTR dccISR();
|
||||
static DCCDecoder* instance;
|
||||
|
||||
void decodeDCCPacket();
|
||||
void processSpeedPacket(uint8_t* data, uint8_t len);
|
||||
void processFunctionPacket(uint8_t* data, uint8_t len);
|
||||
|
||||
uint8_t dccInputPin;
|
||||
uint16_t locoAddress;
|
||||
uint8_t currentSpeed;
|
||||
bool direction;
|
||||
uint32_t functions; // Bit field for F0-F28
|
||||
|
||||
// Packet assembly
|
||||
uint8_t packetBuffer[MAX_DCC_PACKET_SIZE];
|
||||
uint8_t packetIndex;
|
||||
uint8_t bitCount;
|
||||
bool assemblingPacket;
|
||||
|
||||
// Timing
|
||||
unsigned long lastBitTime;
|
||||
unsigned long lastValidPacket;
|
||||
};
|
||||
|
||||
#endif // DCC_DECODER_H
|
||||
108
ESP32/DCC-Loco/include/LEDController.h
Normal file
108
ESP32/DCC-Loco/include/LEDController.h
Normal file
@@ -0,0 +1,108 @@
|
||||
/**
|
||||
* @file LEDController.h
|
||||
* @brief WS2812 LED Controller for lighting effects
|
||||
*
|
||||
* Controls WS2812 addressable LEDs for headlights, taillights, and other effects.
|
||||
* Supports direction-based lighting and function-controlled effects.
|
||||
*/
|
||||
|
||||
#ifndef LED_CONTROLLER_H
|
||||
#define LED_CONTROLLER_H
|
||||
|
||||
#include <Arduino.h>
|
||||
#include <FastLED.h>
|
||||
|
||||
#define MAX_LEDS 16
|
||||
#define DEFAULT_BRIGHTNESS 128
|
||||
|
||||
enum LightMode {
|
||||
LIGHT_OFF = 0,
|
||||
LIGHT_ON = 1,
|
||||
LIGHT_BLINK = 2,
|
||||
LIGHT_PULSE = 3,
|
||||
LIGHT_DIRECTION_FRONT = 4, // On when moving forward
|
||||
LIGHT_DIRECTION_REAR = 5 // On when moving backward
|
||||
};
|
||||
|
||||
class LEDController {
|
||||
public:
|
||||
LEDController();
|
||||
|
||||
/**
|
||||
* @brief Initialize LED controller
|
||||
* @param ledPin GPIO pin for WS2812 data
|
||||
* @param numLeds Number of LEDs in the strip
|
||||
* @return true if successful
|
||||
*/
|
||||
bool begin(uint8_t ledPin, uint8_t numLeds);
|
||||
|
||||
/**
|
||||
* @brief Update LED states (call regularly from loop)
|
||||
*/
|
||||
void update();
|
||||
|
||||
/**
|
||||
* @brief Set LED mode for a specific LED
|
||||
* @param ledIndex LED index (0-based)
|
||||
* @param mode Light mode
|
||||
*/
|
||||
void setLEDMode(uint8_t ledIndex, LightMode mode);
|
||||
|
||||
/**
|
||||
* @brief Set LED color
|
||||
* @param ledIndex LED index
|
||||
* @param r Red (0-255)
|
||||
* @param g Green (0-255)
|
||||
* @param b Blue (0-255)
|
||||
*/
|
||||
void setLEDColor(uint8_t ledIndex, uint8_t r, uint8_t g, uint8_t b);
|
||||
|
||||
/**
|
||||
* @brief Set global brightness
|
||||
* @param brightness Brightness (0-255)
|
||||
*/
|
||||
void setBrightness(uint8_t brightness);
|
||||
|
||||
/**
|
||||
* @brief Set direction for directional lights
|
||||
* @param forward true = forward, false = reverse
|
||||
*/
|
||||
void setDirection(bool forward);
|
||||
|
||||
/**
|
||||
* @brief Map function to LED
|
||||
* @param functionNum Function number (0-28)
|
||||
* @param ledIndex LED index
|
||||
* @param mode Light mode when function is active
|
||||
*/
|
||||
void mapFunctionToLED(uint8_t functionNum, uint8_t ledIndex, LightMode mode);
|
||||
|
||||
/**
|
||||
* @brief Update function state
|
||||
* @param functionNum Function number
|
||||
* @param state Function state (true = on)
|
||||
*/
|
||||
void setFunctionState(uint8_t functionNum, bool state);
|
||||
|
||||
private:
|
||||
CRGB leds[MAX_LEDS];
|
||||
uint8_t numLEDs;
|
||||
uint8_t dataPin;
|
||||
bool direction;
|
||||
|
||||
struct LEDConfig {
|
||||
LightMode mode;
|
||||
CRGB color;
|
||||
uint8_t mappedFunction; // 255 = no function mapping
|
||||
};
|
||||
|
||||
LEDConfig ledConfig[MAX_LEDS];
|
||||
bool functionStates[29]; // F0-F28
|
||||
|
||||
unsigned long lastUpdate;
|
||||
uint16_t effectCounter;
|
||||
|
||||
void updateLED(uint8_t ledIndex);
|
||||
};
|
||||
|
||||
#endif // LED_CONTROLLER_H
|
||||
114
ESP32/DCC-Loco/include/MotorDriver.h
Normal file
114
ESP32/DCC-Loco/include/MotorDriver.h
Normal file
@@ -0,0 +1,114 @@
|
||||
/**
|
||||
* @file MotorDriver.h
|
||||
* @brief TB67H450FNG Motor Driver Controller
|
||||
*
|
||||
* Controls the TB67H450FNG H-bridge motor driver with PWM speed control,
|
||||
* direction control, and optional load compensation/BEMF feedback.
|
||||
*/
|
||||
|
||||
#ifndef MOTOR_DRIVER_H
|
||||
#define MOTOR_DRIVER_H
|
||||
|
||||
#include <Arduino.h>
|
||||
|
||||
// TB67H450FNG control pins
|
||||
// IN1 and IN2 control direction and brake
|
||||
// PWM controls speed
|
||||
|
||||
class MotorDriver {
|
||||
public:
|
||||
MotorDriver();
|
||||
|
||||
/**
|
||||
* @brief Initialize motor driver
|
||||
* @param in1Pin GPIO for IN1 (Motor phase A)
|
||||
* @param in2Pin GPIO for IN2 (Motor phase B)
|
||||
* @param pwmPin GPIO for PWM speed control
|
||||
* @param currentSensePin ADC pin for current sensing (optional, 255 = disabled)
|
||||
* @return true if successful
|
||||
*/
|
||||
bool begin(uint8_t in1Pin, uint8_t in2Pin, uint8_t pwmPin, uint8_t currentSensePin = 255);
|
||||
|
||||
/**
|
||||
* @brief Set motor speed and direction
|
||||
* @param speed Speed value (0-126, DCC format: 0=stop, 1=emergency stop, 2-127=speed)
|
||||
* @param forward Direction (true=forward, false=reverse)
|
||||
*/
|
||||
void setSpeed(uint8_t speed, bool forward);
|
||||
|
||||
/**
|
||||
* @brief Emergency stop
|
||||
*/
|
||||
void emergencyStop();
|
||||
|
||||
/**
|
||||
* @brief Update motor control (call regularly for load compensation)
|
||||
*/
|
||||
void update();
|
||||
|
||||
/**
|
||||
* @brief Enable/disable load compensation
|
||||
* @param enable true to enable
|
||||
*/
|
||||
void setLoadCompensation(bool enable);
|
||||
|
||||
/**
|
||||
* @brief Get motor current (if current sensing enabled)
|
||||
* @return Current in mA
|
||||
*/
|
||||
uint16_t getMotorCurrent();
|
||||
|
||||
/**
|
||||
* @brief Set PID parameters for load compensation
|
||||
* @param kp Proportional gain
|
||||
* @param ki Integral gain
|
||||
* @param kd Derivative gain
|
||||
*/
|
||||
void setPIDParameters(float kp, float ki, float kd);
|
||||
|
||||
/**
|
||||
* @brief Set acceleration rate
|
||||
* @param rate Rate value (0-255, higher = faster)
|
||||
*/
|
||||
void setAccelRate(uint8_t rate);
|
||||
|
||||
/**
|
||||
* @brief Set deceleration rate
|
||||
* @param rate Rate value (0-255, higher = faster)
|
||||
*/
|
||||
void setDecelRate(uint8_t rate);
|
||||
|
||||
private:
|
||||
uint8_t pinIN1;
|
||||
uint8_t pinIN2;
|
||||
uint8_t pinPWM;
|
||||
uint8_t pinCurrentSense;
|
||||
|
||||
uint8_t targetSpeed;
|
||||
uint8_t currentSpeed;
|
||||
bool targetDirection;
|
||||
bool loadCompensationEnabled;
|
||||
|
||||
// Acceleration/deceleration
|
||||
uint8_t accelRate;
|
||||
uint8_t decelRate;
|
||||
unsigned long lastSpeedUpdate;
|
||||
|
||||
// Load compensation (PID)
|
||||
float Kp, Ki, Kd;
|
||||
float integral;
|
||||
float lastError;
|
||||
uint16_t targetCurrent;
|
||||
|
||||
// PWM settings
|
||||
const uint8_t pwmChannel = 0;
|
||||
const uint32_t pwmFrequency = 20000; // 20 kHz
|
||||
const uint8_t pwmResolution = 8; // 8-bit (0-255)
|
||||
|
||||
void applyMotorControl();
|
||||
void updateAcceleration();
|
||||
void updateLoadCompensation();
|
||||
uint16_t readCurrent();
|
||||
};
|
||||
|
||||
#endif // MOTOR_DRIVER_H
|
||||
93
ESP32/DCC-Loco/include/RailCom.h
Normal file
93
ESP32/DCC-Loco/include/RailCom.h
Normal file
@@ -0,0 +1,93 @@
|
||||
/**
|
||||
* @file RailCom.h
|
||||
* @brief RailCom Feedback Controller
|
||||
*
|
||||
* Implements RailCom channel 1 and 2 for bidirectional communication
|
||||
* with the command station. Sends locomotive address and status information.
|
||||
*/
|
||||
|
||||
#ifndef RAILCOM_H
|
||||
#define RAILCOM_H
|
||||
|
||||
#include <Arduino.h>
|
||||
|
||||
// RailCom timing (in microseconds)
|
||||
#define RAILCOM_CHANNEL1_START 26
|
||||
#define RAILCOM_CHANNEL1_END 177
|
||||
#define RAILCOM_CHANNEL2_START 193
|
||||
#define RAILCOM_CHANNEL2_END 454
|
||||
|
||||
// RailCom 4bit to 8bit encoding table
|
||||
#define RAILCOM_4BIT_TO_8BIT_SIZE 16
|
||||
|
||||
class RailCom {
|
||||
public:
|
||||
RailCom();
|
||||
|
||||
/**
|
||||
* @brief Initialize RailCom
|
||||
* @param txPin GPIO for RailCom transmit (UART TX)
|
||||
* @param cutoutDetectPin GPIO to detect DCC cutout (optional, 255 = disabled)
|
||||
* @return true if successful
|
||||
*/
|
||||
bool begin(uint8_t txPin, uint8_t cutoutDetectPin = 255);
|
||||
|
||||
/**
|
||||
* @brief Enable/disable RailCom
|
||||
* @param enable true to enable
|
||||
*/
|
||||
void setEnabled(bool enable);
|
||||
|
||||
/**
|
||||
* @brief Check if RailCom is enabled
|
||||
*/
|
||||
bool isEnabled() const { return enabled; }
|
||||
|
||||
/**
|
||||
* @brief Send RailCom data during cutout window
|
||||
* This should be called when DCC cutout is detected
|
||||
*/
|
||||
void sendRailComData();
|
||||
|
||||
/**
|
||||
* @brief Set locomotive address for RailCom reporting
|
||||
* @param address Locomotive address
|
||||
*/
|
||||
void setAddress(uint16_t address);
|
||||
|
||||
/**
|
||||
* @brief Set decoder state information
|
||||
* @param speed Current speed
|
||||
* @param direction Current direction
|
||||
*/
|
||||
void setDecoderState(uint8_t speed, bool direction);
|
||||
|
||||
/**
|
||||
* @brief Update RailCom (call regularly from loop)
|
||||
*/
|
||||
void update();
|
||||
|
||||
private:
|
||||
uint8_t txPin;
|
||||
uint8_t cutoutPin;
|
||||
bool enabled;
|
||||
|
||||
uint16_t locoAddress;
|
||||
uint8_t currentSpeed;
|
||||
bool currentDirection;
|
||||
|
||||
HardwareSerial* railcomSerial;
|
||||
|
||||
// RailCom encoding
|
||||
uint8_t encode4to8(uint8_t data);
|
||||
void sendChannel1();
|
||||
void sendChannel2();
|
||||
|
||||
// Timing
|
||||
unsigned long lastCutoutTime;
|
||||
bool inCutout;
|
||||
|
||||
static const uint8_t railcom4to8[16];
|
||||
};
|
||||
|
||||
#endif // RAILCOM_H
|
||||
43
ESP32/DCC-Loco/platformio.ini
Normal file
43
ESP32/DCC-Loco/platformio.ini
Normal file
@@ -0,0 +1,43 @@
|
||||
; PlatformIO Project Configuration File for DCC Locomotive Decoder
|
||||
; ESP32-H2 based DCC decoder with RailCom, motor control, and accessories
|
||||
|
||||
[env:esp32-h2-devkitm-1]
|
||||
platform = espressif32
|
||||
board = esp32-h2-devkitm-1
|
||||
framework = arduino
|
||||
|
||||
; Build flags
|
||||
build_flags =
|
||||
-DCORE_DEBUG_LEVEL=3
|
||||
-DBOARD_HAS_PSRAM
|
||||
-Os
|
||||
|
||||
; Monitor settings
|
||||
monitor_speed = 115200
|
||||
monitor_filters = esp32_exception_decoder
|
||||
|
||||
; Upload settings
|
||||
upload_speed = 921600
|
||||
|
||||
; Library dependencies
|
||||
lib_deps =
|
||||
; FastLED for WS2812 LED control
|
||||
fastled/FastLED@^3.6.0
|
||||
|
||||
; Async Web Server for WiFi configuration
|
||||
esphome/ESPAsyncWebServer-esphome@^3.1.0
|
||||
|
||||
; Async TCP
|
||||
esphome/AsyncTCP-esphome@^2.1.3
|
||||
|
||||
; ArduinoJson for config management
|
||||
bblanchon/ArduinoJson@^7.0.0
|
||||
|
||||
; Preferences/EEPROM for CV storage
|
||||
; (built-in ESP32 library)
|
||||
|
||||
; Filesystem
|
||||
board_build.filesystem = littlefs
|
||||
|
||||
; Partition scheme for OTA updates
|
||||
board_build.partitions = default.csv
|
||||
139
ESP32/DCC-Loco/src/AccessoryOutputs.cpp
Normal file
139
ESP32/DCC-Loco/src/AccessoryOutputs.cpp
Normal file
@@ -0,0 +1,139 @@
|
||||
/**
|
||||
* @file AccessoryOutputs.cpp
|
||||
* @brief Accessory Output Controller Implementation
|
||||
*/
|
||||
|
||||
#include "AccessoryOutputs.h"
|
||||
|
||||
AccessoryOutputs::AccessoryOutputs()
|
||||
: currentSpeed(0), lastBlinkUpdate(0), blinkState(false) {
|
||||
pins[0] = 0;
|
||||
pins[1] = 0;
|
||||
modes[0] = ACC_OFF;
|
||||
modes[1] = ACC_OFF;
|
||||
pwmValues[0] = 0;
|
||||
pwmValues[1] = 0;
|
||||
mappedFunctions[0] = 255;
|
||||
mappedFunctions[1] = 255;
|
||||
memset(functionStates, 0, sizeof(functionStates));
|
||||
}
|
||||
|
||||
bool AccessoryOutputs::begin(uint8_t output1Pin, uint8_t output2Pin) {
|
||||
pins[0] = output1Pin;
|
||||
pins[1] = output2Pin;
|
||||
|
||||
// Configure pins
|
||||
pinMode(pins[0], OUTPUT);
|
||||
pinMode(pins[1], OUTPUT);
|
||||
|
||||
// Setup PWM channels
|
||||
ledcSetup(pwmChannels[0], pwmFrequency, pwmResolution);
|
||||
ledcSetup(pwmChannels[1], pwmFrequency, pwmResolution);
|
||||
|
||||
ledcAttachPin(pins[0], pwmChannels[0]);
|
||||
ledcAttachPin(pins[1], pwmChannels[1]);
|
||||
|
||||
// Initialize outputs to off
|
||||
ledcWrite(pwmChannels[0], 0);
|
||||
ledcWrite(pwmChannels[1], 0);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void AccessoryOutputs::setMode(uint8_t outputNum, AccessoryMode mode) {
|
||||
if (outputNum >= 1 && outputNum <= 2) {
|
||||
modes[outputNum - 1] = mode;
|
||||
}
|
||||
}
|
||||
|
||||
void AccessoryOutputs::setPWM(uint8_t outputNum, uint8_t dutyCycle) {
|
||||
if (outputNum >= 1 && outputNum <= 2) {
|
||||
pwmValues[outputNum - 1] = dutyCycle;
|
||||
}
|
||||
}
|
||||
|
||||
void AccessoryOutputs::mapFunction(uint8_t outputNum, uint8_t functionNum) {
|
||||
if (outputNum >= 1 && outputNum <= 2 && functionNum <= 28) {
|
||||
mappedFunctions[outputNum - 1] = functionNum;
|
||||
}
|
||||
}
|
||||
|
||||
void AccessoryOutputs::setFunctionState(uint8_t functionNum, bool state) {
|
||||
if (functionNum <= 28) {
|
||||
functionStates[functionNum] = state;
|
||||
}
|
||||
}
|
||||
|
||||
void AccessoryOutputs::setSpeed(uint8_t speed) {
|
||||
currentSpeed = speed;
|
||||
}
|
||||
|
||||
void AccessoryOutputs::update() {
|
||||
unsigned long currentTime = millis();
|
||||
|
||||
// Update blink state (1 Hz)
|
||||
if (currentTime - lastBlinkUpdate >= 500) {
|
||||
lastBlinkUpdate = currentTime;
|
||||
blinkState = !blinkState;
|
||||
}
|
||||
|
||||
// Update each output
|
||||
updateOutput(0);
|
||||
updateOutput(1);
|
||||
}
|
||||
|
||||
void AccessoryOutputs::updateOutput(uint8_t outputNum) {
|
||||
if (outputNum >= 2) return;
|
||||
|
||||
bool shouldBeOn = false;
|
||||
uint8_t pwmValue = 255;
|
||||
|
||||
switch (modes[outputNum]) {
|
||||
case ACC_OFF:
|
||||
shouldBeOn = false;
|
||||
break;
|
||||
|
||||
case ACC_ON:
|
||||
shouldBeOn = true;
|
||||
break;
|
||||
|
||||
case ACC_FUNCTION:
|
||||
if (mappedFunctions[outputNum] != 255) {
|
||||
shouldBeOn = functionStates[mappedFunctions[outputNum]];
|
||||
}
|
||||
break;
|
||||
|
||||
case ACC_PWM:
|
||||
shouldBeOn = true;
|
||||
pwmValue = pwmValues[outputNum];
|
||||
break;
|
||||
|
||||
case ACC_BLINK:
|
||||
shouldBeOn = blinkState;
|
||||
break;
|
||||
|
||||
case ACC_SPEED_DEPENDENT:
|
||||
if (currentSpeed >= 2) {
|
||||
shouldBeOn = true;
|
||||
// Map speed (2-127) to PWM (0-255)
|
||||
pwmValue = map(currentSpeed, 2, 127, 0, 255);
|
||||
} else {
|
||||
shouldBeOn = false;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
// Apply output
|
||||
if (shouldBeOn) {
|
||||
ledcWrite(pwmChannels[outputNum], pwmValue);
|
||||
} else {
|
||||
ledcWrite(pwmChannels[outputNum], 0);
|
||||
}
|
||||
}
|
||||
|
||||
void AccessoryOutputs::setOutput(uint8_t outputNum, bool state) {
|
||||
if (outputNum >= 1 && outputNum <= 2) {
|
||||
uint8_t idx = outputNum - 1;
|
||||
ledcWrite(pwmChannels[idx], state ? 255 : 0);
|
||||
}
|
||||
}
|
||||
116
ESP32/DCC-Loco/src/CVManager.cpp
Normal file
116
ESP32/DCC-Loco/src/CVManager.cpp
Normal file
@@ -0,0 +1,116 @@
|
||||
/**
|
||||
* @file CVManager.cpp
|
||||
* @brief Configuration Variable Manager Implementation
|
||||
*/
|
||||
|
||||
#include "CVManager.h"
|
||||
|
||||
CVManager::CVManager() {}
|
||||
|
||||
bool CVManager::begin() {
|
||||
if (!preferences.begin("dcc-decoder", false)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check if this is first boot
|
||||
if (preferences.getUChar("initialized", 0) == 0) {
|
||||
setDefaultCVs();
|
||||
preferences.putUChar("initialized", 1);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
uint8_t CVManager::readCV(uint16_t cvNumber, uint8_t defaultValue) {
|
||||
if (cvNumber < 1 || cvNumber > MAX_CV_NUMBER) {
|
||||
return defaultValue;
|
||||
}
|
||||
|
||||
return preferences.getUChar(getCVKey(cvNumber).c_str(), defaultValue);
|
||||
}
|
||||
|
||||
bool CVManager::writeCV(uint16_t cvNumber, uint8_t value) {
|
||||
if (cvNumber < 1 || cvNumber > MAX_CV_NUMBER) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return preferences.putUChar(getCVKey(cvNumber).c_str(), value);
|
||||
}
|
||||
|
||||
void CVManager::resetToDefaults() {
|
||||
preferences.clear();
|
||||
setDefaultCVs();
|
||||
preferences.putUChar("initialized", 1);
|
||||
}
|
||||
|
||||
uint16_t CVManager::getLocoAddress() {
|
||||
// Check CV29 bit 5 to determine address mode
|
||||
uint8_t cv29 = readCV(CV_CONFIG_DATA_1, 0x06);
|
||||
|
||||
if (cv29 & 0x20) {
|
||||
// Long address mode
|
||||
uint8_t highByte = readCV(CV_EXTENDED_ADDRESS_HIGH, 0xC0);
|
||||
uint8_t lowByte = readCV(CV_EXTENDED_ADDRESS_LOW, 0x03);
|
||||
return ((highByte & 0x3F) << 8) | lowByte;
|
||||
} else {
|
||||
// Short address mode
|
||||
return readCV(CV_PRIMARY_ADDRESS, 3);
|
||||
}
|
||||
}
|
||||
|
||||
void CVManager::setLocoAddress(uint16_t address) {
|
||||
if (address >= 1 && address <= 127) {
|
||||
// Short address
|
||||
writeCV(CV_PRIMARY_ADDRESS, address);
|
||||
|
||||
// Clear long address bit in CV29
|
||||
uint8_t cv29 = readCV(CV_CONFIG_DATA_1, 0x06);
|
||||
cv29 &= ~0x20;
|
||||
writeCV(CV_CONFIG_DATA_1, cv29);
|
||||
} else if (address >= 128 && address <= 10239) {
|
||||
// Long address
|
||||
uint8_t highByte = 0xC0 | ((address >> 8) & 0x3F);
|
||||
uint8_t lowByte = address & 0xFF;
|
||||
|
||||
writeCV(CV_EXTENDED_ADDRESS_HIGH, highByte);
|
||||
writeCV(CV_EXTENDED_ADDRESS_LOW, lowByte);
|
||||
|
||||
// Set long address bit in CV29
|
||||
uint8_t cv29 = readCV(CV_CONFIG_DATA_1, 0x06);
|
||||
cv29 |= 0x20;
|
||||
writeCV(CV_CONFIG_DATA_1, cv29);
|
||||
}
|
||||
}
|
||||
|
||||
bool CVManager::isLongAddress() {
|
||||
uint8_t cv29 = readCV(CV_CONFIG_DATA_1, 0x06);
|
||||
return (cv29 & 0x20) != 0;
|
||||
}
|
||||
|
||||
void CVManager::setDefaultCVs() {
|
||||
// Standard CVs
|
||||
writeCV(CV_PRIMARY_ADDRESS, 3); // Default address 3
|
||||
writeCV(CV_VSTART, 1); // Start voltage
|
||||
writeCV(CV_ACCEL_RATE, 10); // Acceleration rate
|
||||
writeCV(CV_DECEL_RATE, 10); // Deceleration rate
|
||||
writeCV(CV_VHIGH, 255); // Max voltage
|
||||
writeCV(CV_VMID, 128); // Mid voltage
|
||||
writeCV(CV_VERSION_ID, 1); // Version 1
|
||||
writeCV(CV_MANUFACTURER_ID, 13); // DIY decoder
|
||||
writeCV(CV_TOTAL_PWM_PERIOD, 20); // 20ms PWM period
|
||||
writeCV(CV_CONFIG_DATA_1, 0x06); // 128 speed steps, short address
|
||||
|
||||
// Custom CVs
|
||||
writeCV(CV_MOTOR_KP, 50); // PID Kp = 5.0
|
||||
writeCV(CV_MOTOR_KI, 5); // PID Ki = 0.5
|
||||
writeCV(CV_MOTOR_KD, 10); // PID Kd = 1.0
|
||||
writeCV(CV_RAILCOM_ENABLE, 1); // RailCom enabled
|
||||
writeCV(CV_LOAD_COMP_ENABLE, 1); // Load compensation enabled
|
||||
writeCV(CV_LED_BRIGHTNESS, 128); // 50% brightness
|
||||
writeCV(CV_ACCESSORY_1_MODE, ACC_FUNCTION); // Function controlled
|
||||
writeCV(CV_ACCESSORY_2_MODE, ACC_FUNCTION); // Function controlled
|
||||
}
|
||||
|
||||
String CVManager::getCVKey(uint16_t cvNumber) {
|
||||
return "cv" + String(cvNumber);
|
||||
}
|
||||
309
ESP32/DCC-Loco/src/ConfigServer.cpp
Normal file
309
ESP32/DCC-Loco/src/ConfigServer.cpp
Normal file
@@ -0,0 +1,309 @@
|
||||
/**
|
||||
* @file ConfigServer.cpp
|
||||
* @brief WiFi/Bluetooth Configuration Server Implementation
|
||||
*/
|
||||
|
||||
#include "ConfigServer.h"
|
||||
|
||||
ConfigServer::ConfigServer(CVManager& cvManager)
|
||||
: cvMgr(cvManager), server(nullptr), ws(nullptr),
|
||||
active(false), statusCallback(nullptr), lastStatusUpdate(0) {}
|
||||
|
||||
bool ConfigServer::begin(const char* ssid, const char* password, bool useAP) {
|
||||
// Initialize WiFi
|
||||
if (useAP) {
|
||||
// Access Point mode
|
||||
String apName = ssid ? String(ssid) : getDefaultAPName();
|
||||
String apPass = password ? String(password) : "dcc12345";
|
||||
|
||||
WiFi.softAP(apName.c_str(), apPass.c_str());
|
||||
Serial.println("WiFi AP started");
|
||||
Serial.print("AP Name: ");
|
||||
Serial.println(apName);
|
||||
Serial.print("IP Address: ");
|
||||
Serial.println(WiFi.softAPIP());
|
||||
} else {
|
||||
// Station mode
|
||||
if (!ssid) return false;
|
||||
|
||||
WiFi.begin(ssid, password);
|
||||
|
||||
int attempts = 0;
|
||||
while (WiFi.status() != WL_CONNECTED && attempts < 20) {
|
||||
delay(500);
|
||||
Serial.print(".");
|
||||
attempts++;
|
||||
}
|
||||
|
||||
if (WiFi.status() != WL_CONNECTED) {
|
||||
Serial.println("\nWiFi connection failed");
|
||||
return false;
|
||||
}
|
||||
|
||||
Serial.println("\nWiFi connected");
|
||||
Serial.print("IP Address: ");
|
||||
Serial.println(WiFi.localIP());
|
||||
}
|
||||
|
||||
// Create web server
|
||||
server = new AsyncWebServer(80);
|
||||
ws = new AsyncWebSocket("/ws");
|
||||
|
||||
setupWebSocket();
|
||||
setupHTTPRoutes();
|
||||
|
||||
server->begin();
|
||||
active = true;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void ConfigServer::stop() {
|
||||
if (server) {
|
||||
server->end();
|
||||
delete server;
|
||||
server = nullptr;
|
||||
}
|
||||
|
||||
if (ws) {
|
||||
delete ws;
|
||||
ws = nullptr;
|
||||
}
|
||||
|
||||
WiFi.disconnect();
|
||||
active = false;
|
||||
}
|
||||
|
||||
void ConfigServer::update() {
|
||||
if (!active) return;
|
||||
|
||||
// Send periodic status updates
|
||||
unsigned long currentTime = millis();
|
||||
if (currentTime - lastStatusUpdate >= 1000) { // Every second
|
||||
lastStatusUpdate = currentTime;
|
||||
broadcastStatus();
|
||||
}
|
||||
}
|
||||
|
||||
void ConfigServer::setupWebSocket() {
|
||||
ws->onEvent([this](AsyncWebSocket* server, AsyncWebSocketClient* client,
|
||||
AwsEventType type, void* arg, uint8_t* data, size_t len) {
|
||||
handleWebSocketEvent(server, client, type, arg, data, len);
|
||||
});
|
||||
|
||||
server->addHandler(ws);
|
||||
}
|
||||
|
||||
void ConfigServer::setupHTTPRoutes() {
|
||||
// Serve basic HTML page
|
||||
server->on("/", HTTP_GET, [](AsyncWebServerRequest* request) {
|
||||
String html = R"html(
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>DCC Loco Decoder Config</title>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<style>
|
||||
body { font-family: Arial; margin: 20px; }
|
||||
.container { max-width: 600px; margin: 0 auto; }
|
||||
button { padding: 10px 20px; margin: 5px; }
|
||||
input { padding: 5px; margin: 5px; }
|
||||
.status { background: #f0f0f0; padding: 10px; margin: 10px 0; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<h1>DCC Locomotive Decoder</h1>
|
||||
<div class="status" id="status">Connecting...</div>
|
||||
|
||||
<h2>Configuration Variables</h2>
|
||||
<div>
|
||||
<label>CV Number: <input type="number" id="cvNum" min="1" max="1024" value="1"></label>
|
||||
<button onclick="readCV()">Read CV</button>
|
||||
</div>
|
||||
<div>
|
||||
<label>CV Value: <input type="number" id="cvVal" min="0" max="255" value="0"></label>
|
||||
<button onclick="writeCV()">Write CV</button>
|
||||
</div>
|
||||
<div id="cvResult"></div>
|
||||
|
||||
<h2>Actions</h2>
|
||||
<button onclick="resetDecoder()">Reset to Defaults</button>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const ws = new WebSocket('ws://' + location.host + '/ws');
|
||||
|
||||
ws.onmessage = (event) => {
|
||||
const msg = JSON.parse(event.data);
|
||||
console.log('Received:', msg);
|
||||
|
||||
if (msg.type === 'status') {
|
||||
document.getElementById('status').innerHTML =
|
||||
'Address: ' + msg.address + '<br>' +
|
||||
'Speed: ' + msg.speed + '<br>' +
|
||||
'Direction: ' + (msg.direction ? 'Forward' : 'Reverse');
|
||||
} else if (msg.type === 'cv_read') {
|
||||
document.getElementById('cvResult').innerHTML =
|
||||
'CV' + msg.cv + ' = ' + msg.value;
|
||||
} else if (msg.type === 'cv_write') {
|
||||
document.getElementById('cvResult').innerHTML =
|
||||
msg.success ? 'CV written successfully' : 'Write failed';
|
||||
}
|
||||
};
|
||||
|
||||
function readCV() {
|
||||
const cv = parseInt(document.getElementById('cvNum').value);
|
||||
ws.send(JSON.stringify({command: 'read_cv', cv: cv}));
|
||||
}
|
||||
|
||||
function writeCV() {
|
||||
const cv = parseInt(document.getElementById('cvNum').value);
|
||||
const value = parseInt(document.getElementById('cvVal').value);
|
||||
ws.send(JSON.stringify({command: 'write_cv', cv: cv, value: value}));
|
||||
}
|
||||
|
||||
function resetDecoder() {
|
||||
if (confirm('Reset all CVs to defaults?')) {
|
||||
ws.send(JSON.stringify({command: 'reset'}));
|
||||
}
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
)html";
|
||||
request->send(200, "text/html", html);
|
||||
});
|
||||
}
|
||||
|
||||
void ConfigServer::handleWebSocketEvent(AsyncWebSocket* server, AsyncWebSocketClient* client,
|
||||
AwsEventType type, void* arg, uint8_t* data, size_t len) {
|
||||
if (type == WS_EVT_CONNECT) {
|
||||
Serial.printf("WebSocket client #%u connected\n", client->id());
|
||||
handleGetStatus(client);
|
||||
} else if (type == WS_EVT_DISCONNECT) {
|
||||
Serial.printf("WebSocket client #%u disconnected\n", client->id());
|
||||
} else if (type == WS_EVT_DATA) {
|
||||
AwsFrameInfo* info = (AwsFrameInfo*)arg;
|
||||
if (info->final && info->index == 0 && info->len == len && info->opcode == WS_TEXT) {
|
||||
data[len] = 0;
|
||||
handleWebSocketMessage(client, data, len);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void ConfigServer::handleWebSocketMessage(void* clientPtr, uint8_t* data, size_t len) {
|
||||
AsyncWebSocketClient* client = (AsyncWebSocketClient*)clientPtr;
|
||||
|
||||
StaticJsonDocument<256> doc;
|
||||
DeserializationError error = deserializeJson(doc, data, len);
|
||||
|
||||
if (error) {
|
||||
Serial.println("JSON parse error");
|
||||
return;
|
||||
}
|
||||
|
||||
const char* command = doc["command"];
|
||||
|
||||
if (strcmp(command, "read_cv") == 0) {
|
||||
handleReadCV(client, doc.as<JsonObject>());
|
||||
} else if (strcmp(command, "write_cv") == 0) {
|
||||
handleWriteCV(client, doc.as<JsonObject>());
|
||||
} else if (strcmp(command, "get_status") == 0) {
|
||||
handleGetStatus(client);
|
||||
} else if (strcmp(command, "reset") == 0) {
|
||||
handleReset(client);
|
||||
}
|
||||
}
|
||||
|
||||
void ConfigServer::handleReadCV(AsyncWebSocketClient* client, JsonObject& json) {
|
||||
uint16_t cvNum = json["cv"];
|
||||
uint8_t value = cvMgr.readCV(cvNum, 0);
|
||||
|
||||
StaticJsonDocument<128> response;
|
||||
response["type"] = "cv_read";
|
||||
response["cv"] = cvNum;
|
||||
response["value"] = value;
|
||||
|
||||
String output;
|
||||
serializeJson(response, output);
|
||||
client->text(output);
|
||||
}
|
||||
|
||||
void ConfigServer::handleWriteCV(AsyncWebSocketClient* client, JsonObject& json) {
|
||||
uint16_t cvNum = json["cv"];
|
||||
uint8_t value = json["value"];
|
||||
bool success = cvMgr.writeCV(cvNum, value);
|
||||
|
||||
StaticJsonDocument<128> response;
|
||||
response["type"] = "cv_write";
|
||||
response["success"] = success;
|
||||
|
||||
String output;
|
||||
serializeJson(response, output);
|
||||
client->text(output);
|
||||
}
|
||||
|
||||
void ConfigServer::handleGetStatus(AsyncWebSocketClient* client) {
|
||||
StaticJsonDocument<256> response;
|
||||
JsonObject status = response.to<JsonObject>();
|
||||
status["type"] = "status";
|
||||
|
||||
if (statusCallback) {
|
||||
statusCallback(status);
|
||||
} else {
|
||||
status["address"] = cvMgr.getLocoAddress();
|
||||
status["speed"] = 0;
|
||||
status["direction"] = true;
|
||||
}
|
||||
|
||||
String output;
|
||||
serializeJson(response, output);
|
||||
client->text(output);
|
||||
}
|
||||
|
||||
void ConfigServer::handleReset(AsyncWebSocketClient* client) {
|
||||
cvMgr.resetToDefaults();
|
||||
sendResponse(client, "reset", true, "Decoder reset to defaults");
|
||||
}
|
||||
|
||||
void ConfigServer::sendResponse(AsyncWebSocketClient* client, const char* type,
|
||||
bool success, const char* message) {
|
||||
StaticJsonDocument<128> response;
|
||||
response["type"] = type;
|
||||
response["success"] = success;
|
||||
if (message) {
|
||||
response["message"] = message;
|
||||
}
|
||||
|
||||
String output;
|
||||
serializeJson(response, output);
|
||||
client->text(output);
|
||||
}
|
||||
|
||||
void ConfigServer::broadcastStatus() {
|
||||
if (!ws || ws->count() == 0) return;
|
||||
|
||||
StaticJsonDocument<256> response;
|
||||
JsonObject status = response.to<JsonObject>();
|
||||
status["type"] = "status";
|
||||
|
||||
if (statusCallback) {
|
||||
statusCallback(status);
|
||||
} else {
|
||||
status["address"] = cvMgr.getLocoAddress();
|
||||
}
|
||||
|
||||
String output;
|
||||
serializeJson(response, output);
|
||||
ws->textAll(output);
|
||||
}
|
||||
|
||||
void ConfigServer::setStatusCallback(StatusCallback callback) {
|
||||
statusCallback = callback;
|
||||
}
|
||||
|
||||
String ConfigServer::getDefaultAPName() {
|
||||
uint64_t chipid = ESP.getEfuseMac();
|
||||
return "DCC-Loco-" + String((uint32_t)(chipid >> 32), HEX);
|
||||
}
|
||||
232
ESP32/DCC-Loco/src/DCCDecoder.cpp
Normal file
232
ESP32/DCC-Loco/src/DCCDecoder.cpp
Normal file
@@ -0,0 +1,232 @@
|
||||
/**
|
||||
* @file DCCDecoder.cpp
|
||||
* @brief DCC Signal Decoder Implementation
|
||||
*/
|
||||
|
||||
#include "DCCDecoder.h"
|
||||
|
||||
DCCDecoder* DCCDecoder::instance = nullptr;
|
||||
|
||||
DCCDecoder::DCCDecoder()
|
||||
: dccInputPin(0), locoAddress(3), currentSpeed(0), direction(true),
|
||||
functions(0), packetIndex(0), bitCount(0), assemblingPacket(false),
|
||||
lastBitTime(0), lastValidPacket(0) {
|
||||
instance = this;
|
||||
}
|
||||
|
||||
bool DCCDecoder::begin(uint8_t dccPin) {
|
||||
dccInputPin = dccPin;
|
||||
pinMode(dccInputPin, INPUT);
|
||||
|
||||
// Attach interrupt for DCC signal
|
||||
attachInterrupt(digitalPinToInterrupt(dccInputPin), dccISR, CHANGE);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void IRAM_ATTR DCCDecoder::dccISR() {
|
||||
if (instance) {
|
||||
instance->process();
|
||||
}
|
||||
}
|
||||
|
||||
void DCCDecoder::process() {
|
||||
unsigned long currentTime = micros();
|
||||
unsigned long pulseDuration = currentTime - lastBitTime;
|
||||
lastBitTime = currentTime;
|
||||
|
||||
// Check for DCC ONE bit (52-64 µs)
|
||||
if (pulseDuration >= DCC_ONE_BIT_MIN && pulseDuration <= DCC_ONE_BIT_MAX) {
|
||||
if (assemblingPacket) {
|
||||
// Shift in a '1' bit
|
||||
packetBuffer[packetIndex] = (packetBuffer[packetIndex] << 1) | 1;
|
||||
bitCount++;
|
||||
|
||||
if (bitCount >= 8) {
|
||||
packetIndex++;
|
||||
bitCount = 0;
|
||||
|
||||
if (packetIndex >= MAX_DCC_PACKET_SIZE) {
|
||||
assemblingPacket = false;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Preamble bit
|
||||
if (pulseDuration >= DCC_ONE_BIT_MIN) {
|
||||
// Start of new packet after preamble
|
||||
packetIndex = 0;
|
||||
bitCount = 0;
|
||||
assemblingPacket = true;
|
||||
memset(packetBuffer, 0, sizeof(packetBuffer));
|
||||
}
|
||||
}
|
||||
}
|
||||
// Check for DCC ZERO bit (95-9900 µs)
|
||||
else if (pulseDuration >= DCC_ZERO_BIT_MIN && pulseDuration <= DCC_ZERO_BIT_MAX) {
|
||||
if (assemblingPacket) {
|
||||
// Shift in a '0' bit
|
||||
packetBuffer[packetIndex] = (packetBuffer[packetIndex] << 1);
|
||||
bitCount++;
|
||||
|
||||
if (bitCount >= 8) {
|
||||
packetIndex++;
|
||||
bitCount = 0;
|
||||
|
||||
// Check for end of packet (more than 3 bytes minimum)
|
||||
if (packetIndex >= 3) {
|
||||
decodeDCCPacket();
|
||||
assemblingPacket = false;
|
||||
}
|
||||
|
||||
if (packetIndex >= MAX_DCC_PACKET_SIZE) {
|
||||
assemblingPacket = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void DCCDecoder::decodeDCCPacket() {
|
||||
// Validate checksum
|
||||
uint8_t checksum = 0;
|
||||
for (uint8_t i = 0; i < packetIndex - 1; i++) {
|
||||
checksum ^= packetBuffer[i];
|
||||
}
|
||||
|
||||
if (checksum != packetBuffer[packetIndex - 1]) {
|
||||
return; // Invalid packet
|
||||
}
|
||||
|
||||
lastValidPacket = millis();
|
||||
|
||||
// Check address
|
||||
uint16_t packetAddress;
|
||||
uint8_t dataStart;
|
||||
|
||||
if (packetBuffer[0] == 0xFF) {
|
||||
// Idle packet
|
||||
return;
|
||||
} else if (packetBuffer[0] >= 0xC0) {
|
||||
// Long address (14-bit)
|
||||
packetAddress = ((packetBuffer[0] & 0x3F) << 8) | packetBuffer[1];
|
||||
dataStart = 2;
|
||||
} else if (packetBuffer[0] >= 1 && packetBuffer[0] <= 127) {
|
||||
// Short address (7-bit)
|
||||
packetAddress = packetBuffer[0];
|
||||
dataStart = 1;
|
||||
} else {
|
||||
return; // Invalid address
|
||||
}
|
||||
|
||||
// Check if packet is for this decoder
|
||||
if (packetAddress != locoAddress && packetAddress != 0) {
|
||||
return; // Not for us (0 = broadcast)
|
||||
}
|
||||
|
||||
// Process instruction byte
|
||||
uint8_t instruction = packetBuffer[dataStart];
|
||||
|
||||
if ((instruction & 0xC0) == 0x40) {
|
||||
// Speed and direction (01DCSSSS or 001DSSSS for 14/28 step)
|
||||
processSpeedPacket(&packetBuffer[dataStart], packetIndex - dataStart - 1);
|
||||
} else if ((instruction & 0xE0) == 0x80) {
|
||||
// Function group (100XXXXX)
|
||||
processFunctionPacket(&packetBuffer[dataStart], packetIndex - dataStart - 1);
|
||||
} else if ((instruction & 0xF0) == 0xA0) {
|
||||
// Function group (1011XXXX)
|
||||
processFunctionPacket(&packetBuffer[dataStart], packetIndex - dataStart - 1);
|
||||
} else if ((instruction & 0xE0) == 0xC0) {
|
||||
// Feature expansion
|
||||
processFunctionPacket(&packetBuffer[dataStart], packetIndex - dataStart - 1);
|
||||
}
|
||||
}
|
||||
|
||||
void DCCDecoder::processSpeedPacket(uint8_t* data, uint8_t len) {
|
||||
if (len < 1) return;
|
||||
|
||||
uint8_t instruction = data[0];
|
||||
|
||||
// Check for 128-step speed (0x3F = 00111111)
|
||||
if ((instruction & 0xC0) == 0x40) {
|
||||
// 01DCSSSS (14/28 step mode)
|
||||
direction = (instruction & 0x20) ? true : false;
|
||||
uint8_t speedBits = instruction & 0x0F;
|
||||
|
||||
if (len >= 2) {
|
||||
// 128 step mode: second byte contains speed
|
||||
currentSpeed = data[1] & 0x7F;
|
||||
} else {
|
||||
// 14/28 step mode
|
||||
currentSpeed = speedBits * 9; // Approximate scaling
|
||||
}
|
||||
} else if ((instruction & 0xE0) == 0x20) {
|
||||
// 001DSSSS (reverse operation control)
|
||||
direction = (instruction & 0x10) ? true : false;
|
||||
}
|
||||
}
|
||||
|
||||
void DCCDecoder::processFunctionPacket(uint8_t* data, uint8_t len) {
|
||||
if (len < 1) return;
|
||||
|
||||
uint8_t instruction = data[0];
|
||||
|
||||
if ((instruction & 0xF0) == 0x80) {
|
||||
// 100DDDDD - Function group 1 (F0-F4)
|
||||
if (instruction & 0x10) functions |= (1 << 0); else functions &= ~(1 << 0); // F0
|
||||
if (instruction & 0x01) functions |= (1 << 1); else functions &= ~(1 << 1); // F1
|
||||
if (instruction & 0x02) functions |= (1 << 2); else functions &= ~(1 << 2); // F2
|
||||
if (instruction & 0x04) functions |= (1 << 3); else functions &= ~(1 << 3); // F3
|
||||
if (instruction & 0x08) functions |= (1 << 4); else functions &= ~(1 << 4); // F4
|
||||
} else if ((instruction & 0xF0) == 0xB0) {
|
||||
// 1011DDDD - Function group 2 (F5-F8)
|
||||
if (instruction & 0x01) functions |= (1 << 5); else functions &= ~(1 << 5); // F5
|
||||
if (instruction & 0x02) functions |= (1 << 6); else functions &= ~(1 << 6); // F6
|
||||
if (instruction & 0x04) functions |= (1 << 7); else functions &= ~(1 << 7); // F7
|
||||
if (instruction & 0x08) functions |= (1 << 8); else functions &= ~(1 << 8); // F8
|
||||
} else if ((instruction & 0xF0) == 0xA0) {
|
||||
// 1010DDDD - Function group 3 (F9-F12)
|
||||
if (instruction & 0x01) functions |= (1 << 9); else functions &= ~(1 << 9); // F9
|
||||
if (instruction & 0x02) functions |= (1 << 10); else functions &= ~(1 << 10); // F10
|
||||
if (instruction & 0x04) functions |= (1 << 11); else functions &= ~(1 << 11); // F11
|
||||
if (instruction & 0x08) functions |= (1 << 12); else functions &= ~(1 << 12); // F12
|
||||
} else if (instruction == 0xDE) {
|
||||
// F13-F20
|
||||
if (len >= 2) {
|
||||
uint8_t funcByte = data[1];
|
||||
for (uint8_t i = 0; i < 8; i++) {
|
||||
if (funcByte & (1 << i)) {
|
||||
functions |= (1 << (13 + i));
|
||||
} else {
|
||||
functions &= ~(1 << (13 + i));
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (instruction == 0xDF) {
|
||||
// F21-F28
|
||||
if (len >= 2) {
|
||||
uint8_t funcByte = data[1];
|
||||
for (uint8_t i = 0; i < 8; i++) {
|
||||
if (funcByte & (1 << i)) {
|
||||
functions |= (1 << (21 + i));
|
||||
} else {
|
||||
functions &= ~(1 << (21 + i));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bool DCCDecoder::getFunction(uint8_t functionNum) const {
|
||||
if (functionNum > 28) return false;
|
||||
return (functions & (1 << functionNum)) != 0;
|
||||
}
|
||||
|
||||
bool DCCDecoder::hasValidSignal() const {
|
||||
return (millis() - lastValidPacket) < 1000; // Valid if packet within last second
|
||||
}
|
||||
|
||||
void DCCDecoder::setAddress(uint16_t address) {
|
||||
if (address >= 1 && address <= 10239) {
|
||||
locoAddress = address;
|
||||
}
|
||||
}
|
||||
132
ESP32/DCC-Loco/src/LEDController.cpp
Normal file
132
ESP32/DCC-Loco/src/LEDController.cpp
Normal file
@@ -0,0 +1,132 @@
|
||||
/**
|
||||
* @file LEDController.cpp
|
||||
* @brief WS2812 LED Controller Implementation
|
||||
*/
|
||||
|
||||
#include "LEDController.h"
|
||||
|
||||
LEDController::LEDController()
|
||||
: numLEDs(0), dataPin(0), direction(true), lastUpdate(0), effectCounter(0) {
|
||||
memset(functionStates, 0, sizeof(functionStates));
|
||||
|
||||
for (uint8_t i = 0; i < MAX_LEDS; i++) {
|
||||
ledConfig[i].mode = LIGHT_OFF;
|
||||
ledConfig[i].color = CRGB::White;
|
||||
ledConfig[i].mappedFunction = 255;
|
||||
}
|
||||
}
|
||||
|
||||
bool LEDController::begin(uint8_t ledPin, uint8_t numLeds) {
|
||||
if (numLeds > MAX_LEDS) {
|
||||
numLeds = MAX_LEDS;
|
||||
}
|
||||
|
||||
dataPin = ledPin;
|
||||
numLEDs = numLeds;
|
||||
|
||||
// Initialize FastLED
|
||||
FastLED.addLeds<WS2812, dataPin, GRB>(leds, numLEDs);
|
||||
FastLED.setBrightness(DEFAULT_BRIGHTNESS);
|
||||
FastLED.clear();
|
||||
FastLED.show();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void LEDController::update() {
|
||||
unsigned long currentTime = millis();
|
||||
|
||||
if (currentTime - lastUpdate >= 20) { // Update at ~50 Hz
|
||||
lastUpdate = currentTime;
|
||||
effectCounter++;
|
||||
|
||||
for (uint8_t i = 0; i < numLEDs; i++) {
|
||||
updateLED(i);
|
||||
}
|
||||
|
||||
FastLED.show();
|
||||
}
|
||||
}
|
||||
|
||||
void LEDController::updateLED(uint8_t ledIndex) {
|
||||
if (ledIndex >= numLEDs) return;
|
||||
|
||||
LEDConfig& config = ledConfig[ledIndex];
|
||||
bool shouldBeOn = false;
|
||||
|
||||
// Determine if LED should be on based on mode
|
||||
switch (config.mode) {
|
||||
case LIGHT_OFF:
|
||||
shouldBeOn = false;
|
||||
break;
|
||||
|
||||
case LIGHT_ON:
|
||||
shouldBeOn = true;
|
||||
break;
|
||||
|
||||
case LIGHT_BLINK:
|
||||
shouldBeOn = (effectCounter % 50) < 25; // ~1 Hz blink
|
||||
break;
|
||||
|
||||
case LIGHT_PULSE:
|
||||
{
|
||||
uint8_t brightness = (sin8(effectCounter * 5) >> 1) + 128;
|
||||
leds[ledIndex] = config.color;
|
||||
leds[ledIndex].fadeToBlackBy(255 - brightness);
|
||||
return;
|
||||
}
|
||||
|
||||
case LIGHT_DIRECTION_FRONT:
|
||||
shouldBeOn = direction;
|
||||
break;
|
||||
|
||||
case LIGHT_DIRECTION_REAR:
|
||||
shouldBeOn = !direction;
|
||||
break;
|
||||
}
|
||||
|
||||
// Check function mapping
|
||||
if (config.mappedFunction != 255) {
|
||||
shouldBeOn = shouldBeOn && functionStates[config.mappedFunction];
|
||||
}
|
||||
|
||||
// Set LED color
|
||||
if (shouldBeOn) {
|
||||
leds[ledIndex] = config.color;
|
||||
} else {
|
||||
leds[ledIndex] = CRGB::Black;
|
||||
}
|
||||
}
|
||||
|
||||
void LEDController::setLEDMode(uint8_t ledIndex, LightMode mode) {
|
||||
if (ledIndex < MAX_LEDS) {
|
||||
ledConfig[ledIndex].mode = mode;
|
||||
}
|
||||
}
|
||||
|
||||
void LEDController::setLEDColor(uint8_t ledIndex, uint8_t r, uint8_t g, uint8_t b) {
|
||||
if (ledIndex < MAX_LEDS) {
|
||||
ledConfig[ledIndex].color = CRGB(r, g, b);
|
||||
}
|
||||
}
|
||||
|
||||
void LEDController::setBrightness(uint8_t brightness) {
|
||||
FastLED.setBrightness(brightness);
|
||||
}
|
||||
|
||||
void LEDController::setDirection(bool forward) {
|
||||
direction = forward;
|
||||
}
|
||||
|
||||
void LEDController::mapFunctionToLED(uint8_t functionNum, uint8_t ledIndex, LightMode mode) {
|
||||
if (ledIndex < MAX_LEDS && functionNum <= 28) {
|
||||
ledConfig[ledIndex].mappedFunction = functionNum;
|
||||
ledConfig[ledIndex].mode = mode;
|
||||
}
|
||||
}
|
||||
|
||||
void LEDController::setFunctionState(uint8_t functionNum, bool state) {
|
||||
if (functionNum <= 28) {
|
||||
functionStates[functionNum] = state;
|
||||
}
|
||||
}
|
||||
213
ESP32/DCC-Loco/src/MotorDriver.cpp
Normal file
213
ESP32/DCC-Loco/src/MotorDriver.cpp
Normal file
@@ -0,0 +1,213 @@
|
||||
/**
|
||||
* @file MotorDriver.cpp
|
||||
* @brief TB67H450FNG Motor Driver Implementation
|
||||
*/
|
||||
|
||||
#include "MotorDriver.h"
|
||||
|
||||
MotorDriver::MotorDriver()
|
||||
: pinIN1(0), pinIN2(0), pinPWM(0), pinCurrentSense(255),
|
||||
targetSpeed(0), currentSpeed(0), targetDirection(true),
|
||||
loadCompensationEnabled(false), accelRate(10), decelRate(10),
|
||||
lastSpeedUpdate(0), Kp(1.0), Ki(0.1), Kd(0.5),
|
||||
integral(0), lastError(0), targetCurrent(0) {}
|
||||
|
||||
bool MotorDriver::begin(uint8_t in1Pin, uint8_t in2Pin, uint8_t pwmPin, uint8_t currentSensePin) {
|
||||
pinIN1 = in1Pin;
|
||||
pinIN2 = in2Pin;
|
||||
pinPWM = pwmPin;
|
||||
pinCurrentSense = currentSensePin;
|
||||
|
||||
// Configure pins
|
||||
pinMode(pinIN1, OUTPUT);
|
||||
pinMode(pinIN2, OUTPUT);
|
||||
pinMode(pinPWM, OUTPUT);
|
||||
|
||||
if (pinCurrentSense != 255) {
|
||||
pinMode(pinCurrentSense, INPUT);
|
||||
}
|
||||
|
||||
// Setup PWM
|
||||
ledcSetup(pwmChannel, pwmFrequency, pwmResolution);
|
||||
ledcAttachPin(pinPWM, pwmChannel);
|
||||
ledcWrite(pwmChannel, 0);
|
||||
|
||||
// Set initial state (brake)
|
||||
digitalWrite(pinIN1, LOW);
|
||||
digitalWrite(pinIN2, LOW);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void MotorDriver::setSpeed(uint8_t speed, bool forward) {
|
||||
targetSpeed = speed;
|
||||
targetDirection = forward;
|
||||
}
|
||||
|
||||
void MotorDriver::emergencyStop() {
|
||||
targetSpeed = 0;
|
||||
currentSpeed = 0;
|
||||
|
||||
// Apply brake
|
||||
digitalWrite(pinIN1, HIGH);
|
||||
digitalWrite(pinIN2, HIGH);
|
||||
ledcWrite(pwmChannel, 0);
|
||||
}
|
||||
|
||||
void MotorDriver::update() {
|
||||
updateAcceleration();
|
||||
|
||||
if (loadCompensationEnabled && pinCurrentSense != 255) {
|
||||
updateLoadCompensation();
|
||||
} else {
|
||||
applyMotorControl();
|
||||
}
|
||||
}
|
||||
|
||||
void MotorDriver::updateAcceleration() {
|
||||
unsigned long currentTime = millis();
|
||||
|
||||
if (currentTime - lastSpeedUpdate < 50) {
|
||||
return; // Update every 50ms
|
||||
}
|
||||
|
||||
lastSpeedUpdate = currentTime;
|
||||
|
||||
if (currentSpeed < targetSpeed) {
|
||||
// Accelerate
|
||||
uint8_t delta = (accelRate > 0) ? (255 / accelRate) : 1;
|
||||
if (currentSpeed + delta >= targetSpeed) {
|
||||
currentSpeed = targetSpeed;
|
||||
} else {
|
||||
currentSpeed += delta;
|
||||
}
|
||||
} else if (currentSpeed > targetSpeed) {
|
||||
// Decelerate
|
||||
uint8_t delta = (decelRate > 0) ? (255 / decelRate) : 1;
|
||||
if (currentSpeed <= delta || currentSpeed - delta <= targetSpeed) {
|
||||
currentSpeed = targetSpeed;
|
||||
} else {
|
||||
currentSpeed -= delta;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void MotorDriver::applyMotorControl() {
|
||||
if (currentSpeed == 0) {
|
||||
// Stop/brake
|
||||
digitalWrite(pinIN1, LOW);
|
||||
digitalWrite(pinIN2, LOW);
|
||||
ledcWrite(pwmChannel, 0);
|
||||
return;
|
||||
}
|
||||
|
||||
if (currentSpeed == 1) {
|
||||
// Emergency stop (brake)
|
||||
digitalWrite(pinIN1, HIGH);
|
||||
digitalWrite(pinIN2, HIGH);
|
||||
ledcWrite(pwmChannel, 0);
|
||||
return;
|
||||
}
|
||||
|
||||
// Map DCC speed (2-127) to PWM (0-255)
|
||||
uint16_t pwmValue = map(currentSpeed, 2, 127, 0, 255);
|
||||
|
||||
// Set direction
|
||||
if (targetDirection) {
|
||||
// Forward
|
||||
digitalWrite(pinIN1, HIGH);
|
||||
digitalWrite(pinIN2, LOW);
|
||||
} else {
|
||||
// Reverse
|
||||
digitalWrite(pinIN1, LOW);
|
||||
digitalWrite(pinIN2, HIGH);
|
||||
}
|
||||
|
||||
// Set PWM
|
||||
ledcWrite(pwmChannel, pwmValue);
|
||||
}
|
||||
|
||||
void MotorDriver::updateLoadCompensation() {
|
||||
// Read current
|
||||
uint16_t currentMa = readCurrent();
|
||||
|
||||
// PID control
|
||||
float error = targetCurrent - currentMa;
|
||||
integral += error;
|
||||
|
||||
// Anti-windup
|
||||
if (integral > 1000) integral = 1000;
|
||||
if (integral < -1000) integral = -1000;
|
||||
|
||||
float derivative = error - lastError;
|
||||
lastError = error;
|
||||
|
||||
float correction = (Kp * error) + (Ki * integral) + (Kd * derivative);
|
||||
|
||||
// Apply correction to PWM
|
||||
if (currentSpeed == 0 || currentSpeed == 1) {
|
||||
applyMotorControl();
|
||||
return;
|
||||
}
|
||||
|
||||
uint16_t basePwm = map(currentSpeed, 2, 127, 0, 255);
|
||||
int16_t adjustedPwm = basePwm + (int16_t)correction;
|
||||
|
||||
// Clamp PWM
|
||||
if (adjustedPwm < 0) adjustedPwm = 0;
|
||||
if (adjustedPwm > 255) adjustedPwm = 255;
|
||||
|
||||
// Set direction
|
||||
if (targetDirection) {
|
||||
digitalWrite(pinIN1, HIGH);
|
||||
digitalWrite(pinIN2, LOW);
|
||||
} else {
|
||||
digitalWrite(pinIN1, LOW);
|
||||
digitalWrite(pinIN2, HIGH);
|
||||
}
|
||||
|
||||
ledcWrite(pwmChannel, adjustedPwm);
|
||||
}
|
||||
|
||||
uint16_t MotorDriver::readCurrent() {
|
||||
if (pinCurrentSense == 255) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Read ADC value
|
||||
uint16_t adcValue = analogRead(pinCurrentSense);
|
||||
|
||||
// Convert to milliamps (this is hardware dependent)
|
||||
// Assuming 3.3V reference, 12-bit ADC, and current sense amplifier
|
||||
// Adjust this based on your actual hardware
|
||||
uint16_t currentMa = (adcValue * 3300) / 4096; // Simplified conversion
|
||||
|
||||
return currentMa;
|
||||
}
|
||||
|
||||
uint16_t MotorDriver::getMotorCurrent() {
|
||||
return readCurrent();
|
||||
}
|
||||
|
||||
void MotorDriver::setLoadCompensation(bool enable) {
|
||||
loadCompensationEnabled = enable;
|
||||
|
||||
if (enable) {
|
||||
integral = 0;
|
||||
lastError = 0;
|
||||
}
|
||||
}
|
||||
|
||||
void MotorDriver::setPIDParameters(float kp, float ki, float kd) {
|
||||
Kp = kp;
|
||||
Ki = ki;
|
||||
Kd = kd;
|
||||
}
|
||||
|
||||
void MotorDriver::setAccelRate(uint8_t rate) {
|
||||
accelRate = rate;
|
||||
}
|
||||
|
||||
void MotorDriver::setDecelRate(uint8_t rate) {
|
||||
decelRate = rate;
|
||||
}
|
||||
127
ESP32/DCC-Loco/src/RailCom.cpp
Normal file
127
ESP32/DCC-Loco/src/RailCom.cpp
Normal file
@@ -0,0 +1,127 @@
|
||||
/**
|
||||
* @file RailCom.cpp
|
||||
* @brief RailCom Feedback Implementation
|
||||
*/
|
||||
|
||||
#include "RailCom.h"
|
||||
|
||||
// RailCom 4-to-8 bit encoding table
|
||||
const uint8_t RailCom::railcom4to8[16] = {
|
||||
0xAC, 0xE5, 0xD3, 0xF0,
|
||||
0x99, 0xCC, 0xB4, 0x78,
|
||||
0xA3, 0xE1, 0xD5, 0xF2,
|
||||
0x9A, 0xCA, 0xB8, 0x71
|
||||
};
|
||||
|
||||
RailCom::RailCom()
|
||||
: txPin(0), cutoutPin(255), enabled(false), locoAddress(3),
|
||||
currentSpeed(0), currentDirection(true), railcomSerial(nullptr),
|
||||
lastCutoutTime(0), inCutout(false) {}
|
||||
|
||||
bool RailCom::begin(uint8_t txPinNum, uint8_t cutoutDetectPin) {
|
||||
txPin = txPinNum;
|
||||
cutoutPin = cutoutDetectPin;
|
||||
|
||||
// Initialize UART for RailCom
|
||||
// RailCom uses 250kbaud, 8N1
|
||||
railcomSerial = &Serial1;
|
||||
railcomSerial->begin(250000, SERIAL_8N1, -1, txPin); // RX not used
|
||||
|
||||
if (cutoutPin != 255) {
|
||||
pinMode(cutoutPin, INPUT);
|
||||
}
|
||||
|
||||
enabled = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
void RailCom::setEnabled(bool enable) {
|
||||
enabled = enable;
|
||||
}
|
||||
|
||||
void RailCom::setAddress(uint16_t address) {
|
||||
locoAddress = address;
|
||||
}
|
||||
|
||||
void RailCom::setDecoderState(uint8_t speed, bool direction) {
|
||||
currentSpeed = speed;
|
||||
currentDirection = direction;
|
||||
}
|
||||
|
||||
void RailCom::update() {
|
||||
if (!enabled) return;
|
||||
|
||||
// Check for cutout signal if pin is configured
|
||||
if (cutoutPin != 255) {
|
||||
bool cutoutDetected = digitalRead(cutoutPin) == LOW;
|
||||
|
||||
if (cutoutDetected && !inCutout) {
|
||||
// Rising edge of cutout
|
||||
inCutout = true;
|
||||
lastCutoutTime = micros();
|
||||
sendRailComData();
|
||||
} else if (!cutoutDetected && inCutout) {
|
||||
// Falling edge of cutout
|
||||
inCutout = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void RailCom::sendRailComData() {
|
||||
if (!enabled || !railcomSerial) return;
|
||||
|
||||
// Wait for Channel 1 window (26-177 µs)
|
||||
delayMicroseconds(RAILCOM_CHANNEL1_START);
|
||||
sendChannel1();
|
||||
|
||||
// Wait for Channel 2 window (193-454 µs)
|
||||
unsigned long elapsed = micros() - lastCutoutTime;
|
||||
if (elapsed < RAILCOM_CHANNEL2_START) {
|
||||
delayMicroseconds(RAILCOM_CHANNEL2_START - elapsed);
|
||||
}
|
||||
sendChannel2();
|
||||
}
|
||||
|
||||
void RailCom::sendChannel1() {
|
||||
// Channel 1: Send locomotive address (ID)
|
||||
// Format: 2 bytes encoded with 4-to-8 encoding
|
||||
|
||||
uint8_t addrLow = locoAddress & 0x0F;
|
||||
uint8_t addrHigh = (locoAddress >> 4) & 0x0F;
|
||||
|
||||
uint8_t byte1 = encode4to8(addrHigh);
|
||||
uint8_t byte2 = encode4to8(addrLow);
|
||||
|
||||
railcomSerial->write(byte1);
|
||||
railcomSerial->write(byte2);
|
||||
railcomSerial->flush();
|
||||
}
|
||||
|
||||
void RailCom::sendChannel2() {
|
||||
// Channel 2: Send status/data
|
||||
// We can send speed, function states, etc.
|
||||
|
||||
// For simplicity, send basic status
|
||||
// Bit 0-6: Speed (0-127)
|
||||
// Bit 7: Direction
|
||||
|
||||
uint8_t statusByte = currentSpeed & 0x7F;
|
||||
if (currentDirection) {
|
||||
statusByte |= 0x80;
|
||||
}
|
||||
|
||||
uint8_t dataLow = statusByte & 0x0F;
|
||||
uint8_t dataHigh = (statusByte >> 4) & 0x0F;
|
||||
|
||||
uint8_t byte1 = encode4to8(dataHigh);
|
||||
uint8_t byte2 = encode4to8(dataLow);
|
||||
|
||||
railcomSerial->write(byte1);
|
||||
railcomSerial->write(byte2);
|
||||
railcomSerial->flush();
|
||||
}
|
||||
|
||||
uint8_t RailCom::encode4to8(uint8_t data) {
|
||||
if (data >= 16) return 0xAC; // Invalid, return first code
|
||||
return railcom4to8[data];
|
||||
}
|
||||
344
ESP32/DCC-Loco/src/main.cpp
Normal file
344
ESP32/DCC-Loco/src/main.cpp
Normal file
@@ -0,0 +1,344 @@
|
||||
/**
|
||||
* @file main.cpp
|
||||
* @brief DCC Locomotive Decoder - Main Entry Point
|
||||
*
|
||||
* ESP32-H2 based DCC decoder with:
|
||||
* - DCC signal decoding
|
||||
* - TB67H450FNG motor control with load compensation
|
||||
* - WS2812 LED control
|
||||
* - RailCom feedback
|
||||
* - 2x N-FET accessory outputs
|
||||
* - WiFi/Bluetooth configuration via WebSocket
|
||||
*/
|
||||
|
||||
#include <Arduino.h>
|
||||
#include "DCCDecoder.h"
|
||||
#include "CVManager.h"
|
||||
#include "LEDController.h"
|
||||
#include "MotorDriver.h"
|
||||
#include "RailCom.h"
|
||||
#include "AccessoryOutputs.h"
|
||||
#include "ConfigServer.h"
|
||||
|
||||
// ==================== PIN DEFINITIONS ====================
|
||||
// Adjust these based on your hardware wiring
|
||||
|
||||
// DCC Input
|
||||
#define PIN_DCC_INPUT 4 // DCC signal input (with optocoupler)
|
||||
|
||||
// Motor Driver (TB67H450FNG)
|
||||
#define PIN_MOTOR_IN1 5 // Motor phase A
|
||||
#define PIN_MOTOR_IN2 6 // Motor phase B
|
||||
#define PIN_MOTOR_PWM 7 // PWM speed control
|
||||
#define PIN_MOTOR_CURRENT 8 // Current sense (ADC)
|
||||
|
||||
// WS2812 LEDs
|
||||
#define PIN_LED_DATA 9 // WS2812 data line
|
||||
#define NUM_LEDS 4 // Number of LEDs in strip
|
||||
|
||||
// RailCom
|
||||
#define PIN_RAILCOM_TX 10 // RailCom transmit (UART1 TX)
|
||||
#define PIN_RAILCOM_CUTOUT 11 // DCC cutout detection (optional)
|
||||
|
||||
// Accessory Outputs (N-FETs)
|
||||
#define PIN_ACCESSORY_1 12 // Accessory output 1
|
||||
#define PIN_ACCESSORY_2 13 // Accessory output 2
|
||||
|
||||
// Configuration Button
|
||||
#define PIN_CONFIG_BUTTON 14 // Hold to enter config mode
|
||||
|
||||
// ==================== GLOBAL OBJECTS ====================
|
||||
DCCDecoder dccDecoder;
|
||||
CVManager cvManager;
|
||||
LEDController ledController;
|
||||
MotorDriver motorDriver;
|
||||
RailCom railCom;
|
||||
AccessoryOutputs accessories;
|
||||
ConfigServer* configServer = nullptr;
|
||||
|
||||
// ==================== STATE VARIABLES ====================
|
||||
bool configMode = false;
|
||||
unsigned long configButtonPressTime = 0;
|
||||
const unsigned long CONFIG_HOLD_TIME = 3000; // 3 seconds to enter config mode
|
||||
|
||||
// ==================== FUNCTION DECLARATIONS ====================
|
||||
void checkConfigButton();
|
||||
void enterConfigMode();
|
||||
void exitConfigMode();
|
||||
void updateDecoderStatus(JsonObject& status);
|
||||
void syncStatesFromDecoder();
|
||||
|
||||
// ==================== SETUP ====================
|
||||
void setup() {
|
||||
Serial.begin(115200);
|
||||
delay(1000);
|
||||
|
||||
Serial.println("\n\n======================================");
|
||||
Serial.println("DCC Locomotive Decoder");
|
||||
Serial.println("ESP32-H2 Version 1.0");
|
||||
Serial.println("======================================\n");
|
||||
|
||||
// Initialize configuration button
|
||||
pinMode(PIN_CONFIG_BUTTON, INPUT_PULLUP);
|
||||
|
||||
// Initialize CV Manager
|
||||
Serial.print("Initializing CV Manager... ");
|
||||
if (cvManager.begin()) {
|
||||
Serial.println("OK");
|
||||
} else {
|
||||
Serial.println("FAILED");
|
||||
}
|
||||
|
||||
// Load configuration from CVs
|
||||
uint16_t locoAddress = cvManager.getLocoAddress();
|
||||
uint8_t accelRate = cvManager.readCV(CV_ACCEL_RATE, 10);
|
||||
uint8_t decelRate = cvManager.readCV(CV_DECEL_RATE, 10);
|
||||
uint8_t ledBrightness = cvManager.readCV(CV_LED_BRIGHTNESS, 128);
|
||||
bool railComEnabled = cvManager.readCV(CV_RAILCOM_ENABLE, 1) != 0;
|
||||
bool loadCompEnabled = cvManager.readCV(CV_LOAD_COMP_ENABLE, 1) != 0;
|
||||
|
||||
Serial.println("\nConfiguration:");
|
||||
Serial.printf(" Locomotive Address: %d %s\n", locoAddress,
|
||||
cvManager.isLongAddress() ? "(Long)" : "(Short)");
|
||||
Serial.printf(" Accel Rate: %d\n", accelRate);
|
||||
Serial.printf(" Decel Rate: %d\n", decelRate);
|
||||
Serial.printf(" RailCom: %s\n", railComEnabled ? "Enabled" : "Disabled");
|
||||
Serial.printf(" Load Compensation: %s\n", loadCompEnabled ? "Enabled" : "Disabled");
|
||||
|
||||
// Initialize DCC Decoder
|
||||
Serial.print("\nInitializing DCC Decoder... ");
|
||||
if (dccDecoder.begin(PIN_DCC_INPUT)) {
|
||||
dccDecoder.setAddress(locoAddress);
|
||||
Serial.println("OK");
|
||||
} else {
|
||||
Serial.println("FAILED");
|
||||
}
|
||||
|
||||
// Initialize Motor Driver
|
||||
Serial.print("Initializing Motor Driver... ");
|
||||
if (motorDriver.begin(PIN_MOTOR_IN1, PIN_MOTOR_IN2, PIN_MOTOR_PWM, PIN_MOTOR_CURRENT)) {
|
||||
motorDriver.setAccelRate(accelRate);
|
||||
motorDriver.setDecelRate(decelRate);
|
||||
motorDriver.setLoadCompensation(loadCompEnabled);
|
||||
|
||||
// Load PID parameters from CVs
|
||||
float kp = cvManager.readCV(CV_MOTOR_KP, 50) / 10.0;
|
||||
float ki = cvManager.readCV(CV_MOTOR_KI, 5) / 10.0;
|
||||
float kd = cvManager.readCV(CV_MOTOR_KD, 10) / 10.0;
|
||||
motorDriver.setPIDParameters(kp, ki, kd);
|
||||
|
||||
Serial.println("OK");
|
||||
} else {
|
||||
Serial.println("FAILED");
|
||||
}
|
||||
|
||||
// Initialize LED Controller
|
||||
Serial.print("Initializing LED Controller... ");
|
||||
if (ledController.begin(PIN_LED_DATA, NUM_LEDS)) {
|
||||
ledController.setBrightness(ledBrightness);
|
||||
|
||||
// Configure default LED mappings
|
||||
ledController.setLEDMode(0, LIGHT_DIRECTION_FRONT); // Front headlight
|
||||
ledController.setLEDColor(0, 255, 255, 200); // Warm white
|
||||
|
||||
ledController.setLEDMode(1, LIGHT_DIRECTION_REAR); // Rear headlight
|
||||
ledController.setLEDColor(1, 255, 0, 0); // Red
|
||||
|
||||
ledController.mapFunctionToLED(1, 2, LIGHT_ON); // F1 -> LED 2
|
||||
ledController.mapFunctionToLED(2, 3, LIGHT_BLINK); // F2 -> LED 3 (blink)
|
||||
|
||||
Serial.println("OK");
|
||||
} else {
|
||||
Serial.println("FAILED");
|
||||
}
|
||||
|
||||
// Initialize RailCom
|
||||
if (railComEnabled) {
|
||||
Serial.print("Initializing RailCom... ");
|
||||
if (railCom.begin(PIN_RAILCOM_TX, PIN_RAILCOM_CUTOUT)) {
|
||||
railCom.setAddress(locoAddress);
|
||||
Serial.println("OK");
|
||||
} else {
|
||||
Serial.println("FAILED");
|
||||
}
|
||||
} else {
|
||||
Serial.println("RailCom disabled");
|
||||
}
|
||||
|
||||
// Initialize Accessory Outputs
|
||||
Serial.print("Initializing Accessory Outputs... ");
|
||||
if (accessories.begin(PIN_ACCESSORY_1, PIN_ACCESSORY_2)) {
|
||||
// Load modes from CVs
|
||||
AccessoryMode mode1 = (AccessoryMode)cvManager.readCV(CV_ACCESSORY_1_MODE, ACC_FUNCTION);
|
||||
AccessoryMode mode2 = (AccessoryMode)cvManager.readCV(CV_ACCESSORY_2_MODE, ACC_FUNCTION);
|
||||
|
||||
accessories.setMode(1, mode1);
|
||||
accessories.setMode(2, mode2);
|
||||
|
||||
// Map to functions F3 and F4 by default
|
||||
accessories.mapFunction(1, 3); // F3 -> Output 1
|
||||
accessories.mapFunction(2, 4); // F4 -> Output 2
|
||||
|
||||
Serial.println("OK");
|
||||
} else {
|
||||
Serial.println("FAILED");
|
||||
}
|
||||
|
||||
Serial.println("\n======================================");
|
||||
Serial.println("Decoder Ready!");
|
||||
Serial.println("Waiting for DCC signal...");
|
||||
Serial.println("Hold CONFIG button for 3s to enter");
|
||||
Serial.println("configuration mode.");
|
||||
Serial.println("======================================\n");
|
||||
}
|
||||
|
||||
// ==================== MAIN LOOP ====================
|
||||
void loop() {
|
||||
// Check for configuration mode entry
|
||||
checkConfigButton();
|
||||
|
||||
if (configMode) {
|
||||
// Configuration mode - just update the config server
|
||||
if (configServer) {
|
||||
configServer->update();
|
||||
}
|
||||
delay(10);
|
||||
return;
|
||||
}
|
||||
|
||||
// ==================== NORMAL OPERATION MODE ====================
|
||||
|
||||
// Sync decoder states
|
||||
syncStatesFromDecoder();
|
||||
|
||||
// Update all modules
|
||||
motorDriver.update();
|
||||
ledController.update();
|
||||
railCom.update();
|
||||
accessories.update();
|
||||
|
||||
// Periodic status output (every 5 seconds)
|
||||
static unsigned long lastStatusPrint = 0;
|
||||
if (millis() - lastStatusPrint >= 5000) {
|
||||
lastStatusPrint = millis();
|
||||
|
||||
if (dccDecoder.hasValidSignal()) {
|
||||
Serial.printf("DCC OK | Addr:%d | Speed:%d | Dir:%s | Current:%dmA\n",
|
||||
dccDecoder.getAddress(),
|
||||
dccDecoder.getSpeed(),
|
||||
dccDecoder.getDirection() ? "FWD" : "REV",
|
||||
motorDriver.getMotorCurrent());
|
||||
} else {
|
||||
Serial.println("No DCC signal detected");
|
||||
}
|
||||
}
|
||||
|
||||
delay(1); // Small delay to prevent watchdog issues
|
||||
}
|
||||
|
||||
// ==================== HELPER FUNCTIONS ====================
|
||||
|
||||
void checkConfigButton() {
|
||||
bool buttonPressed = (digitalRead(PIN_CONFIG_BUTTON) == LOW);
|
||||
|
||||
if (buttonPressed) {
|
||||
if (configButtonPressTime == 0) {
|
||||
configButtonPressTime = millis();
|
||||
} else if (!configMode && (millis() - configButtonPressTime >= CONFIG_HOLD_TIME)) {
|
||||
enterConfigMode();
|
||||
}
|
||||
} else {
|
||||
// Button released
|
||||
if (configMode && configButtonPressTime > 0) {
|
||||
// Short press in config mode = exit
|
||||
exitConfigMode();
|
||||
}
|
||||
configButtonPressTime = 0;
|
||||
}
|
||||
}
|
||||
|
||||
void enterConfigMode() {
|
||||
configMode = true;
|
||||
Serial.println("\n======================================");
|
||||
Serial.println("ENTERING CONFIGURATION MODE");
|
||||
Serial.println("======================================");
|
||||
|
||||
// Stop motor
|
||||
motorDriver.emergencyStop();
|
||||
|
||||
// Create and start configuration server
|
||||
configServer = new ConfigServer(cvManager);
|
||||
configServer->setStatusCallback(updateDecoderStatus);
|
||||
|
||||
// Start in AP mode with default name
|
||||
if (configServer->begin(nullptr, nullptr, true)) {
|
||||
Serial.println("Configuration server started");
|
||||
Serial.println("Connect to WiFi AP to configure");
|
||||
Serial.println("Press CONFIG button again to exit");
|
||||
} else {
|
||||
Serial.println("Failed to start configuration server");
|
||||
delete configServer;
|
||||
configServer = nullptr;
|
||||
configMode = false;
|
||||
}
|
||||
}
|
||||
|
||||
void exitConfigMode() {
|
||||
Serial.println("\n======================================");
|
||||
Serial.println("EXITING CONFIGURATION MODE");
|
||||
Serial.println("======================================\n");
|
||||
|
||||
if (configServer) {
|
||||
configServer->stop();
|
||||
delete configServer;
|
||||
configServer = nullptr;
|
||||
}
|
||||
|
||||
// Reload configuration
|
||||
uint16_t newAddress = cvManager.getLocoAddress();
|
||||
dccDecoder.setAddress(newAddress);
|
||||
railCom.setAddress(newAddress);
|
||||
|
||||
uint8_t newBrightness = cvManager.readCV(CV_LED_BRIGHTNESS, 128);
|
||||
ledController.setBrightness(newBrightness);
|
||||
|
||||
configMode = false;
|
||||
Serial.println("Decoder ready - waiting for DCC signal");
|
||||
}
|
||||
|
||||
void updateDecoderStatus(JsonObject& status) {
|
||||
status["address"] = dccDecoder.getAddress();
|
||||
status["speed"] = dccDecoder.getSpeed();
|
||||
status["direction"] = dccDecoder.getDirection();
|
||||
status["signal"] = dccDecoder.hasValidSignal();
|
||||
status["current"] = motorDriver.getMotorCurrent();
|
||||
|
||||
// Add function states
|
||||
JsonArray functions = status.createNestedArray("functions");
|
||||
for (uint8_t i = 0; i <= 12; i++) {
|
||||
functions.add(dccDecoder.getFunction(i));
|
||||
}
|
||||
}
|
||||
|
||||
void syncStatesFromDecoder() {
|
||||
// Get current state from DCC decoder
|
||||
uint8_t speed = dccDecoder.getSpeed();
|
||||
bool direction = dccDecoder.getDirection();
|
||||
|
||||
// Update motor
|
||||
motorDriver.setSpeed(speed, direction);
|
||||
|
||||
// Update LEDs
|
||||
ledController.setDirection(direction);
|
||||
for (uint8_t i = 0; i <= 28; i++) {
|
||||
bool funcState = dccDecoder.getFunction(i);
|
||||
ledController.setFunctionState(i, funcState);
|
||||
accessories.setFunctionState(i, funcState);
|
||||
}
|
||||
|
||||
// Update accessories
|
||||
accessories.setSpeed(speed);
|
||||
|
||||
// Update RailCom
|
||||
railCom.setDecoderState(speed, direction);
|
||||
}
|
||||
Reference in New Issue
Block a user