Initialisation depot
This commit is contained in:
110
LocomotiveTestBench/data/README.md
Normal file
110
LocomotiveTestBench/data/README.md
Normal file
@@ -0,0 +1,110 @@
|
||||
# Web Interface Files Setup
|
||||
|
||||
This directory contains the web interface files that will be uploaded to the ESP32's LittleFS filesystem.
|
||||
|
||||
## Structure
|
||||
|
||||
```
|
||||
data/
|
||||
├── index.html # Main HTML page
|
||||
├── css/
|
||||
│ ├── bootstrap.min.css # Bootstrap CSS (local copy)
|
||||
│ └── style.css # Custom styles
|
||||
└── js/
|
||||
├── bootstrap.bundle.min.js # Bootstrap JS (local copy)
|
||||
└── app.js # Application JavaScript
|
||||
```
|
||||
|
||||
## How to Upload Files to ESP32
|
||||
|
||||
### Method 1: Using PlatformIO (Recommended)
|
||||
|
||||
1. **Install LittleFS Uploader**:
|
||||
- In VS Code, open PlatformIO Home
|
||||
- Go to Platforms → Espressif 32
|
||||
- Make sure it's installed/updated
|
||||
|
||||
2. **Download Bootstrap Files** (if not already present):
|
||||
|
||||
Download these files and place them in the appropriate directories:
|
||||
|
||||
- **Bootstrap CSS** (v5.3.0):
|
||||
- URL: https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css
|
||||
- Save to: `data/css/bootstrap.min.css`
|
||||
|
||||
- **Bootstrap JS** (v5.3.0):
|
||||
- URL: https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js
|
||||
- Save to: `data/js/bootstrap.bundle.min.js`
|
||||
|
||||
3. **Upload Filesystem**:
|
||||
- In VS Code, click PlatformIO icon
|
||||
- Under PROJECT TASKS → env:wemos_d1_mini32
|
||||
- Click "Upload Filesystem Image"
|
||||
- Wait for upload to complete
|
||||
|
||||
### Method 2: Manual Download and Upload
|
||||
|
||||
If you need to download Bootstrap files manually:
|
||||
|
||||
```bash
|
||||
# Navigate to data directories
|
||||
cd data/css
|
||||
wget https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css
|
||||
|
||||
cd ../js
|
||||
wget https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js
|
||||
```
|
||||
|
||||
Or use curl:
|
||||
|
||||
```bash
|
||||
cd data/css
|
||||
curl -o bootstrap.min.css https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css
|
||||
|
||||
cd ../js
|
||||
curl -o bootstrap.bundle.min.js https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js
|
||||
```
|
||||
|
||||
### Verifying Upload
|
||||
|
||||
After uploading, you can verify the files are present by:
|
||||
|
||||
1. Opening Serial Monitor (115200 baud)
|
||||
2. Look for "LittleFS mounted successfully" message
|
||||
3. Access the web interface and check browser console for any 404 errors
|
||||
|
||||
## File Sizes (Approximate)
|
||||
|
||||
- `bootstrap.min.css`: ~190 KB
|
||||
- `bootstrap.bundle.min.js`: ~220 KB
|
||||
- `style.css`: ~1 KB
|
||||
- `app.js`: ~4 KB
|
||||
- `index.html`: ~4 KB
|
||||
|
||||
**Total**: ~420 KB (ESP32 has 1.5MB+ available for LittleFS)
|
||||
|
||||
## Benefits of LittleFS Approach
|
||||
|
||||
✅ **Better Organization**: Separate HTML, CSS, and JS files
|
||||
✅ **Offline Operation**: Works without internet connection
|
||||
✅ **Easier Maintenance**: Edit files without recompiling firmware
|
||||
✅ **Faster Updates**: Only upload filesystem when web files change
|
||||
✅ **Better Performance**: No need to parse embedded strings
|
||||
✅ **Standard Development**: Use familiar web development workflow
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### LittleFS Mount Failed
|
||||
- Ensure filesystem is properly formatted
|
||||
- Try uploading filesystem image again
|
||||
- Check serial output for detailed error messages
|
||||
|
||||
### 404 Errors on Static Files
|
||||
- Verify files are in correct directories
|
||||
- Check file names match exactly (case-sensitive)
|
||||
- Re-upload filesystem image
|
||||
|
||||
### Bootstrap Not Loading
|
||||
- Download Bootstrap files to `data/css/` and `data/js/`
|
||||
- Upload filesystem image
|
||||
- Clear browser cache and reload
|
||||
6
LocomotiveTestBench/data/css/bootstrap.min.css
vendored
Normal file
6
LocomotiveTestBench/data/css/bootstrap.min.css
vendored
Normal file
File diff suppressed because one or more lines are too long
44
LocomotiveTestBench/data/css/style.css
Normal file
44
LocomotiveTestBench/data/css/style.css
Normal file
@@ -0,0 +1,44 @@
|
||||
body {
|
||||
padding: 20px;
|
||||
background-color: #f5f5f5;
|
||||
}
|
||||
|
||||
.container {
|
||||
max-width: 800px;
|
||||
background: white;
|
||||
padding: 30px;
|
||||
border-radius: 10px;
|
||||
box-shadow: 0 0 20px rgba(0,0,0,0.1);
|
||||
}
|
||||
|
||||
.status-indicator {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
border-radius: 50%;
|
||||
display: inline-block;
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
.status-connected {
|
||||
background-color: #28a745;
|
||||
}
|
||||
|
||||
.status-disconnected {
|
||||
background-color: #dc3545;
|
||||
}
|
||||
|
||||
.speed-value {
|
||||
font-size: 2em;
|
||||
font-weight: bold;
|
||||
text-align: center;
|
||||
margin: 20px 0;
|
||||
}
|
||||
|
||||
.function-btn {
|
||||
margin: 5px;
|
||||
}
|
||||
|
||||
.direction-indicator {
|
||||
font-size: 1.2em;
|
||||
margin-left: 10px;
|
||||
}
|
||||
27
LocomotiveTestBench/data/download_bootstrap.sh
Executable file
27
LocomotiveTestBench/data/download_bootstrap.sh
Executable file
@@ -0,0 +1,27 @@
|
||||
#!/bin/bash
|
||||
# Script to download Bootstrap files for offline use
|
||||
|
||||
echo "Downloading Bootstrap 5.3.0 files..."
|
||||
|
||||
# Create directories if they don't exist
|
||||
mkdir -p css
|
||||
mkdir -p js
|
||||
|
||||
# Download Bootstrap CSS
|
||||
echo "Downloading Bootstrap CSS..."
|
||||
curl -L -o css/bootstrap.min.css https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css
|
||||
|
||||
# Download Bootstrap JS Bundle (includes Popper)
|
||||
echo "Downloading Bootstrap JS Bundle..."
|
||||
curl -L -o js/bootstrap.bundle.min.js https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js
|
||||
|
||||
echo ""
|
||||
echo "Download complete!"
|
||||
echo ""
|
||||
echo "Files downloaded:"
|
||||
echo " - css/bootstrap.min.css ($(du -h css/bootstrap.min.css | cut -f1))"
|
||||
echo " - js/bootstrap.bundle.min.js ($(du -h js/bootstrap.bundle.min.js | cut -f1))"
|
||||
echo ""
|
||||
echo "Now you can upload the filesystem to your ESP32:"
|
||||
echo " 1. In VS Code, open PlatformIO"
|
||||
echo " 2. Click 'Upload Filesystem Image' under PROJECT TASKS"
|
||||
124
LocomotiveTestBench/data/index.html
Normal file
124
LocomotiveTestBench/data/index.html
Normal file
@@ -0,0 +1,124 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Locomotive Test Bench</title>
|
||||
<link href="/css/bootstrap.min.css" rel="stylesheet">
|
||||
<link href="/css/style.css" rel="stylesheet">
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<h1 class="text-center mb-4">🚂 Locomotive Test Bench</h1>
|
||||
|
||||
<!-- Status Section -->
|
||||
<div class="card mb-3">
|
||||
<div class="card-body">
|
||||
<h5 class="card-title">Status</h5>
|
||||
<p><span class="status-indicator" id="statusIndicator"></span>
|
||||
<span id="statusText">Connecting...</span></p>
|
||||
<p class="mb-0"><small>IP: <span id="ipAddress">-</span></small></p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Control Mode Section -->
|
||||
<div class="card mb-3">
|
||||
<div class="card-body">
|
||||
<h5 class="card-title">Control Mode</h5>
|
||||
<div class="btn-group w-100" role="group">
|
||||
<input type="radio" class="btn-check" name="mode" id="modeAnalog" value="analog" autocomplete="off">
|
||||
<label class="btn btn-outline-primary" for="modeAnalog">DC Analog</label>
|
||||
|
||||
<input type="radio" class="btn-check" name="mode" id="modeDCC" value="dcc" autocomplete="off">
|
||||
<label class="btn btn-outline-primary" for="modeDCC">DCC Digital</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- DCC Address Section -->
|
||||
<div class="card mb-3" id="dccSection" style="display: none;">
|
||||
<div class="card-body">
|
||||
<h5 class="card-title">DCC Configuration</h5>
|
||||
<div class="row">
|
||||
<div class="col-md-8">
|
||||
<label for="dccAddress" class="form-label">Locomotive Address</label>
|
||||
<input type="number" class="form-control" id="dccAddress" min="1" max="10239" value="3">
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<label class="form-label"> </label>
|
||||
<button class="btn btn-primary w-100" onclick="setDCCAddress()">Set</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Speed Control Section -->
|
||||
<div class="card mb-3">
|
||||
<div class="card-body">
|
||||
<h5 class="card-title">Speed Control
|
||||
<span class="direction-indicator" id="directionIndicator">→</span>
|
||||
</h5>
|
||||
<div class="speed-value" id="speedValue">0%</div>
|
||||
<input type="range" class="form-range" id="speedSlider" min="0" max="100" value="0">
|
||||
<div class="d-flex justify-content-between mt-3">
|
||||
<button class="btn btn-danger" onclick="emergencyStop()">⏹ STOP</button>
|
||||
<button class="btn btn-secondary" onclick="reverseDirection()">🔄 Reverse</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- DCC Functions Section -->
|
||||
<div class="card mb-3" id="functionsSection" style="display: none;">
|
||||
<div class="card-body">
|
||||
<h5 class="card-title">DCC Functions</h5>
|
||||
<div id="functionButtons" class="d-flex flex-wrap">
|
||||
<!-- Function buttons will be generated dynamically -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- WiFi Configuration Section -->
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<h5 class="card-title">WiFi Configuration</h5>
|
||||
<button class="btn btn-info w-100 mb-3" type="button" data-bs-toggle="collapse" data-bs-target="#wifiConfig">
|
||||
Show WiFi Settings
|
||||
</button>
|
||||
<div class="collapse" id="wifiConfig">
|
||||
<div class="mb-3">
|
||||
<label class="form-label">WiFi Mode</label>
|
||||
<select class="form-select" id="wifiMode">
|
||||
<option value="ap">Access Point</option>
|
||||
<option value="client">Client (Connect to Network)</option>
|
||||
</select>
|
||||
</div>
|
||||
<div id="apSettings">
|
||||
<div class="mb-3">
|
||||
<label for="apSSID" class="form-label">AP SSID</label>
|
||||
<input type="text" class="form-control" id="apSSID" value="LocoTestBench">
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="apPassword" class="form-label">AP Password</label>
|
||||
<input type="password" class="form-control" id="apPassword" value="12345678">
|
||||
</div>
|
||||
</div>
|
||||
<div id="clientSettings" style="display: none;">
|
||||
<div class="mb-3">
|
||||
<label for="wifiSSID" class="form-label">Network SSID</label>
|
||||
<input type="text" class="form-control" id="wifiSSID">
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="wifiPassword" class="form-label">Network Password</label>
|
||||
<input type="password" class="form-control" id="wifiPassword">
|
||||
</div>
|
||||
</div>
|
||||
<button class="btn btn-warning w-100" onclick="saveWiFiSettings()">Save & Restart</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="/js/bootstrap.bundle.min.js"></script>
|
||||
<script src="/js/app.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
182
LocomotiveTestBench/data/js/app.js
Normal file
182
LocomotiveTestBench/data/js/app.js
Normal file
@@ -0,0 +1,182 @@
|
||||
let currentMode = 'analog';
|
||||
let currentDirection = 1;
|
||||
let dccFunctions = 0;
|
||||
|
||||
// Initialize
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
loadStatus();
|
||||
setupEventListeners();
|
||||
generateFunctionButtons();
|
||||
setInterval(loadStatus, 2000);
|
||||
});
|
||||
|
||||
function setupEventListeners() {
|
||||
document.getElementById('speedSlider').addEventListener('input', function(e) {
|
||||
updateSpeed(e.target.value);
|
||||
});
|
||||
|
||||
document.querySelectorAll('input[name="mode"]').forEach(radio => {
|
||||
radio.addEventListener('change', function(e) {
|
||||
setMode(e.target.value);
|
||||
});
|
||||
});
|
||||
|
||||
document.getElementById('wifiMode').addEventListener('change', function(e) {
|
||||
toggleWiFiSettings(e.target.value);
|
||||
});
|
||||
}
|
||||
|
||||
function generateFunctionButtons() {
|
||||
const container = document.getElementById('functionButtons');
|
||||
for (let i = 0; i <= 12; i++) {
|
||||
const btn = document.createElement('button');
|
||||
btn.className = 'btn btn-outline-secondary function-btn';
|
||||
btn.id = 'f' + i;
|
||||
btn.textContent = 'F' + i;
|
||||
btn.onclick = () => toggleFunction(i);
|
||||
container.appendChild(btn);
|
||||
}
|
||||
}
|
||||
|
||||
async function loadStatus() {
|
||||
try {
|
||||
const response = await fetch('/api/status');
|
||||
const data = await response.json();
|
||||
|
||||
document.getElementById('statusIndicator').className = 'status-indicator status-connected';
|
||||
document.getElementById('statusText').textContent = 'Connected';
|
||||
document.getElementById('ipAddress').textContent = data.ip || '-';
|
||||
|
||||
currentMode = data.mode;
|
||||
currentDirection = data.direction;
|
||||
document.getElementById(data.mode === 'dcc' ? 'modeDCC' : 'modeAnalog').checked = true;
|
||||
document.getElementById('speedSlider').value = data.speed;
|
||||
document.getElementById('speedValue').textContent = data.speed + '%';
|
||||
document.getElementById('dccAddress').value = data.dccAddress;
|
||||
|
||||
updateUIForMode(data.mode);
|
||||
updateDirectionIndicator();
|
||||
} catch (error) {
|
||||
document.getElementById('statusIndicator').className = 'status-indicator status-disconnected';
|
||||
document.getElementById('statusText').textContent = 'Disconnected';
|
||||
}
|
||||
}
|
||||
|
||||
function updateUIForMode(mode) {
|
||||
currentMode = mode;
|
||||
document.getElementById('dccSection').style.display = mode === 'dcc' ? 'block' : 'none';
|
||||
document.getElementById('functionsSection').style.display = mode === 'dcc' ? 'block' : 'none';
|
||||
}
|
||||
|
||||
async function setMode(mode) {
|
||||
try {
|
||||
await fetch('/api/mode', {
|
||||
method: 'POST',
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
body: JSON.stringify({mode: mode})
|
||||
});
|
||||
updateUIForMode(mode);
|
||||
} catch (error) {
|
||||
console.error('Error setting mode:', error);
|
||||
}
|
||||
}
|
||||
|
||||
async function updateSpeed(speed) {
|
||||
document.getElementById('speedValue').textContent = speed + '%';
|
||||
try {
|
||||
await fetch('/api/speed', {
|
||||
method: 'POST',
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
body: JSON.stringify({
|
||||
speed: parseInt(speed),
|
||||
direction: currentDirection
|
||||
})
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error setting speed:', error);
|
||||
}
|
||||
}
|
||||
|
||||
async function emergencyStop() {
|
||||
document.getElementById('speedSlider').value = 0;
|
||||
updateSpeed(0);
|
||||
}
|
||||
|
||||
async function reverseDirection() {
|
||||
currentDirection = currentDirection === 1 ? 0 : 1;
|
||||
updateDirectionIndicator();
|
||||
const speed = document.getElementById('speedSlider').value;
|
||||
updateSpeed(speed);
|
||||
}
|
||||
|
||||
function updateDirectionIndicator() {
|
||||
document.getElementById('directionIndicator').textContent = currentDirection === 1 ? '→' : '←';
|
||||
}
|
||||
|
||||
async function setDCCAddress() {
|
||||
const address = document.getElementById('dccAddress').value;
|
||||
try {
|
||||
await fetch('/api/dcc/address', {
|
||||
method: 'POST',
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
body: JSON.stringify({address: parseInt(address)})
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error setting DCC address:', error);
|
||||
}
|
||||
}
|
||||
|
||||
async function toggleFunction(fn) {
|
||||
const btn = document.getElementById('f' + fn);
|
||||
const isActive = btn.classList.contains('btn-secondary');
|
||||
|
||||
if (isActive) {
|
||||
btn.classList.remove('btn-secondary');
|
||||
btn.classList.add('btn-outline-secondary');
|
||||
} else {
|
||||
btn.classList.remove('btn-outline-secondary');
|
||||
btn.classList.add('btn-secondary');
|
||||
}
|
||||
|
||||
try {
|
||||
await fetch('/api/dcc/function', {
|
||||
method: 'POST',
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
body: JSON.stringify({
|
||||
function: fn,
|
||||
state: !isActive
|
||||
})
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error setting function:', error);
|
||||
}
|
||||
}
|
||||
|
||||
function toggleWiFiSettings(mode) {
|
||||
document.getElementById('apSettings').style.display = mode === 'ap' ? 'block' : 'none';
|
||||
document.getElementById('clientSettings').style.display = mode === 'client' ? 'block' : 'none';
|
||||
}
|
||||
|
||||
async function saveWiFiSettings() {
|
||||
const mode = document.getElementById('wifiMode').value;
|
||||
const config = {
|
||||
isAPMode: mode === 'ap',
|
||||
apSSID: document.getElementById('apSSID').value,
|
||||
apPassword: document.getElementById('apPassword').value,
|
||||
ssid: document.getElementById('wifiSSID').value,
|
||||
password: document.getElementById('wifiPassword').value
|
||||
};
|
||||
|
||||
if (confirm('Save WiFi settings and restart?')) {
|
||||
try {
|
||||
await fetch('/api/wifi', {
|
||||
method: 'POST',
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
body: JSON.stringify(config)
|
||||
});
|
||||
alert('Settings saved. Device will restart...');
|
||||
} catch (error) {
|
||||
console.error('Error saving WiFi settings:', error);
|
||||
}
|
||||
}
|
||||
}
|
||||
7
LocomotiveTestBench/data/js/bootstrap.bundle.min.js
vendored
Normal file
7
LocomotiveTestBench/data/js/bootstrap.bundle.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user