What I use;
Update.h
ESPNATIVEUSBMIDI lbrary (https://github.com/iamthesoundman/ESPNATIVEUSBMIDI)
> ESP32 S3 N16R8, (arduino IDE is set to esp32 s3 dev).
What i tried:
I tried both update.h from arduino and elegantOTA. Both do not allow me to update over OTA by using the AP mode of the esp. I enable AP mode by holding BOOT button for 5sec. When using the update.h library I very often get a succesfull flash, but most of the times my browser returns a failed upload error, with a
Code: Select all
[171467][E][Parsing.cpp:514] _parseForm(): Error: line:Flash size is 4MB, Minimal SPIFFS partition scheme, and my Sketch uses 1081669 bytes (55%) of program storage space. Maximum is 1966080 bytes.
The following code is what I try to use;
Code: Select all
/*********************************************************************
Xbox One Controller to MIDI Interface with Web Configuration
Press BOOT button (GPIO0) for 5 seconds to enter AP mode
Connect to WiFi: "Xbox-MIDI-Config" (password: "midi1234")
Navigate to: 192.168.4.1
*********************************************************************/
// Add near the top of your file with other includes
#ifndef CONFIG_ESP_MAIN_TASK_STACK_SIZE
#define CONFIG_ESP_MAIN_TASK_STACK_SIZE 8192
#endif
#include <Arduino.h>
#include <USB.h>
#include <ESPNATIVEUSBMIDI.h>
#include <MIDI.h>
#include <Bluepad32.h>
#include <Adafruit_NeoPixel.h>
#include <WiFi.h>
#include <WebServer.h>
#include <Preferences.h>
#include <Update.h>
#include <esp_task_wdt.h>
// Firmware version
const char* FIRMWARE_VERSION = "1.0.0";
// Pin Configuration
#define RGB_LED_PIN 48
#define NUM_PIXELS 1
#define BOOT_BUTTON 0
#define DEBUG_ENABLED true
// Timing Configuration
#define STICK_THRESHOLD 10
#define TRIGGER_THRESHOLD 20
#define RUMBLE_INTERVAL 30
#define BUTTON_HOLD_TIME 5000 // 5 seconds to enter config mode
// Debug macro
#if DEBUG_ENABLED
#define DEBUG_PRINT(x) Serial.print(x)
#define DEBUG_PRINTF(x, ...) Serial.printf(x, __VA_ARGS__)
#define DEBUG_PRINTLN(x) Serial.println(x)
#else
#define DEBUG_PRINT(x)
#define DEBUG_PRINTF(x, ...)
#define DEBUG_PRINTLN(x)
#endif
// Create objects
Adafruit_NeoPixel pixels(NUM_PIXELS, RGB_LED_PIN, NEO_GRB + NEO_KHZ800);
ESPNATIVEUSBMIDI espnativeusbmidi;
MIDI_CREATE_INSTANCE(ESPNATIVEUSBMIDI, espnativeusbmidi, MIDI)
ControllerPtr myControllers[BP32_MAX_GAMEPADS];
WebServer server(80);
Preferences preferences;
// Configuration structure
struct Config {
bool faceButtons = true;
bool dpadButtons = true;
bool bumperButtons = true;
bool stickButtons = true;
bool leftStick = true;
bool rightStick = false;
bool triggers = true;
bool rumbleOnButtons = true;
bool rumbleOnDpad = true;
bool rumbleOnBumpers = true;
bool rumbleOnTriggers = true;
bool rumbleOnStickButtons = true;
int rumbleDuration = 100;
int ledBrightness = 128;
int stickDeadzone = 50;
char midiName[32] = "Xbox Controller MIDI";
} config;
// Button mapping structure
struct ButtonGroup {
const byte* notes;
bool* states;
int count;
bool enabled;
bool rumbleEnabled;
uint8_t rumbleWeak;
uint8_t rumbleStrong;
};
// Controller state for visualization
struct ControllerState {
bool a, b, x, y;
bool dpadUp, dpadDown, dpadLeft, dpadRight;
bool l1, r1, l3, r3;
int leftX, leftY, rightX, rightY;
int leftTrigger, rightTrigger;
unsigned long lastUpdate;
} currentState = {0};
// MIDI note definitions
const byte BUTTON_NOTES[] = {60, 62, 64, 65}; // A, B, X, Y -> C4, D4, E4, F4
const byte DPAD_NOTES[] = {67, 69, 71, 72}; // Up, Down, Left, Right -> G4, A4, B4, C5
const byte BUMPER_NOTES[] = {48, 50}; // L1, R1 -> C3, D3
const byte STICK_BUTTON_NOTES[] = {45, 47}; // L3, R3 -> A2, B2 (add this line)
// State tracking
bool buttonStates[4] = {false};
bool dpadStates[4] = {false};
bool bumperStates[2] = {false};
bool stickButtonStates[2] = {false};
int lastAnalog[6] = {0};
unsigned long lastRumbleTime = 0;
unsigned long bootButtonPressTime = 0;
bool bootButtonPressed = false;
bool apModeActive = false;
bool otaInProgress = false;
// Button groups configuration
ButtonGroup buttonGroups[4];
// HTML for configuration page
const char CONFIG_HTML[] PROGMEM = R"rawliteral(
<!DOCTYPE html>
<html>
<head>
<title>Xbox MIDI Config</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<style>
body { font-family: Arial; margin: 20px; background: #1a1a1a; color: #fff; }
h1 { color: #4CAF50; }
.section { background: #2a2a2a; padding: 20px; margin: 15px 0; border-radius: 8px; }
.section h2 { margin-top: 0; color: #64B5F6; }
label { display: block; margin: 12px 0; font-size: 16px; }
input[type="checkbox"] { margin-right: 10px; width: 20px; height: 20px; cursor: pointer; }
input[type="number"], input[type="text"] {
padding: 8px; font-size: 16px; width: 100px;
background: #3a3a3a; color: #fff; border: 1px solid #555; border-radius: 4px;
}
input[type="text"] { width: 250px; }
input[type="range"] { width: 200px; vertical-align: middle; }
.value { display: inline-block; width: 40px; text-align: center; }
button {
background: #4CAF50; color: white; padding: 15px 30px;
font-size: 18px; border: none; border-radius: 5px;
cursor: pointer; margin: 10px 5px;
}
button:hover { background: #45a049; }
.exit-btn { background: #f44336; }
.exit-btn:hover { background: #da190b; }
.ota-btn { background: #FF9800; }
.ota-btn:hover { background: #F57C00; }
.status { padding: 15px; background: #333; border-radius: 5px; margin: 15px 0; }
/* Controller Visualization */
.controller-viz { position: relative; width: 600px; height: 330px; margin: 20px auto; }
.controller-body { width: 100%; height: 100%; background: #444; border-radius: 50px; position: relative; }
.btn {
position: absolute; width: 40px; height: 40px; border-radius: 50%;
background: #666; border: 2px solid #888; transition: all 0.1s;
display: flex; align-items: center; justify-content: center;
font-weight: bold; font-size: 18px;
}
.btn.active { background: #4CAF50; transform: scale(0.9); box-shadow: 0 0 20px #4CAF50; }
.btn-a { bottom: 80px; right: 120px; }
.btn-b { bottom: 120px; right: 80px; }
.btn-x { bottom: 120px; right: 160px; }
.btn-y { bottom: 160px; right: 120px; }
.dpad { position: absolute; left: 120px; bottom: 100px; width: 80px; height: 80px; }
.dpad-btn { position: absolute; width: 25px; height: 25px; background: #666; border: 2px solid #888; }
.dpad-btn.active { background: #4CAF50; box-shadow: 0 0 15px #4CAF50; }
.dpad-up { top: 0; left: 27px; }
.dpad-down { bottom: 0; left: 27px; }
.dpad-left { left: 0; top: 27px; }
.dpad-right { right: 0; top: 27px; }
.bumper {
position: absolute; width: 80px; height: 20px; background: #666;
border: 2px solid #888; border-radius: 10px;
display: flex; align-items: center; justify-content: center;
font-size: 12px; font-weight: bold;
}
.bumper.active { background: #4CAF50; box-shadow: 0 0 15px #4CAF50; }
.bumper-l { top: 20px; left: 100px; }
.bumper-r { top: 20px; right: 100px; }
.stick {
position: absolute; width: 60px; height: 60px; border-radius: 50%;
background: #555; border: 3px solid #777;
}
.stick.active { border-color: #4CAF50; box-shadow: 0 0 15px #4CAF50; }
.stick-left { bottom: 200px; left: 200px; }
.stick-right { bottom: 200px; right: 200px; }
.stick-indicator {
position: absolute; width: 15px; height: 15px; border-radius: 50%;
background: #FF5722; top: 50%; left: 50%;
transform: translate(-50%, -50%); transition: all 0.05s;
}
.trigger {
position: absolute; width: 60px; height: 15px; background: #666;
border: 2px solid #888; border-radius: 5px; overflow: hidden;
}
.trigger-l { top: 50px; left: 100px; }
.trigger-r { top: 50px; right: 100px; }
.trigger-fill {
height: 100%; background: linear-gradient(90deg, #4CAF50, #8BC34A);
width: 0%; transition: width 0.05s;
}
.info-text { text-align: center; margin-top: 10px; font-size: 14px; color: #aaa; }
</style>
</head>
<body>
<h1>Xbox Controller MIDI Configuration</h1>
<div class="status">
<strong>Firmware Version:</strong> %FIRMWARE_VERSION%<br>
<strong>Connection Status:</strong> Connected to 192.168.4.1<br>
<strong>Device:</strong> ESP32-S3 Xbox MIDI Interface
</div>
<!-- Live Controller Visualization -->
<div class="section">
<h2>Live Controller View</h2>
<div class="controller-viz">
<div class="controller-body">
<!-- Face Buttons -->
<div class="btn btn-a" id="btn-a">A</div>
<div class="btn btn-b" id="btn-b">B</div>
<div class="btn btn-x" id="btn-x">X</div>
<div class="btn btn-y" id="btn-y">Y</div>
<!-- D-Pad -->
<div class="dpad">
<div class="dpad-btn dpad-up" id="dpad-up"></div>
<div class="dpad-btn dpad-down" id="dpad-down"></div>
<div class="dpad-btn dpad-left" id="dpad-left"></div>
<div class="dpad-btn dpad-right" id="dpad-right"></div>
</div>
<!-- Bumpers -->
<div class="bumper bumper-l" id="bumper-l">L1</div>
<div class="bumper bumper-r" id="bumper-r">R1</div>
<!-- Triggers -->
<div class="trigger trigger-l"><div class="trigger-fill" id="trigger-l-fill"></div></div>
<div class="trigger trigger-r"><div class="trigger-fill" id="trigger-r-fill"></div></div>
<!-- Analog Sticks -->
<div class="stick stick-left" id="stick-l">
<div class="stick-indicator" id="stick-l-indicator"></div>
</div>
<div class="stick stick-right" id="stick-r">
<div class="stick-indicator" id="stick-r-indicator"></div>
</div>
</div>
</div>
<div class="info-text">Real-time controller input visualization - Updates every 100ms</div>
</div>
<form action="/save" method="POST">
<div class="section">
<h2>MIDI Settings</h2>
<label>
Device Name:<br>
<input type="text" name="midiName" value="%MIDI_NAME%" maxlength="31">
</label>
</div>
<div class="section">
<h2>Button Controls</h2>
<label><input type="checkbox" name="faceButtons" %FACE_BTN%> Enable Face Buttons (A, B, X, Y)</label>
<label><input type="checkbox" name="dpadButtons" %DPAD_BTN%> Enable D-Pad</label>
<label><input type="checkbox" name="bumperButtons" %BUMP_BTN%> Enable Bumpers (L1, R1)</label>
<label><input type="checkbox" name="stickButtons" %STICK_BTN%> Enable Stick Buttons (L3, R3)</label>
</div>
<div class="section">
<h2>Analog Controls</h2>
<label><input type="checkbox" name="leftStick" %LEFT_STICK%> Enable Left Stick (CC1 Mod, CC7 Vol)</label>
<label><input type="checkbox" name="rightStick" %RIGHT_STICK%> Enable Right Stick (CC10 Pan, CC11 Expr)</label>
<label><input type="checkbox" name="triggers" %TRIGGERS%> Enable Triggers (CC64 Sustain, CC65 Port)</label>
<label>
Joystick Deadzone:
<input type="number" name="stickDeadzone" value="%STICK_DZ%" min="0" max="200" step="5">
<small>(0-200, prevents drift)</small>
</label>
</div>
<div class="section">
<h2>Rumble Feedback</h2>
<label><input type="checkbox" name="rumbleButtons" %RUMBLE_BTN%> Rumble on Face Buttons</label>
<label><input type="checkbox" name="rumbleDpad" %RUMBLE_DPAD%> Rumble on D-Pad</label>
<label><input type="checkbox" name="rumbleBumpers" %RUMBLE_BUMP%> Rumble on Bumpers</label>
<label><input type="checkbox" name="rumbleStickButtons" %RUMBLE_STICK%> Rumble on Stick Buttons</label>
<label><input type="checkbox" name="rumbleTriggers" %RUMBLE_TRIG%> Rumble on Triggers</label>
<label>
Rumble Duration:
<input type="number" name="rumbleDuration" value="%RUMBLE_DUR%" min="20" max="500" step="10"> ms
</label>
</div>
<div class="section">
<h2>LED Settings</h2>
<label>
LED Brightness:
<input type="range" name="ledBrightness" value="%LED_BRIGHT%" min="0" max="255"
oninput="this.nextElementSibling.textContent = this.value">
<span class="value">%LED_BRIGHT%</span>
</label>
</div>
<button type="submit">Save Configuration</button>
<button type="button" class="ota-btn" onclick="location.href='/ota'">OTA Update</button>
<button type="button" class="exit-btn" onclick="location.href='/exit'">Exit Config Mode</button>
</form>
<div class="section">
<h2>MIDI Mapping Reference</h2>
<strong>Buttons:</strong><br>
- A, B, X, Y > C4, D4, E4, F4<br>
- D-pad > G4, A4, B4, C5<br>
- Bumpers > C3, D3<br>
- Stick Buttons > A2, B2<br><br>
<strong>Controllers:</strong><br>
- Left Stick X/Y > CC1 (Mod) / CC7 (Vol)<br>
- Right Stick X/Y > CC10 (Pan) / CC11 (Expr)<br>
- Triggers L/R > CC64 (Sus) / CC65 (Port)
</div>
<script>
// Live controller visualization update
function updateController() {
fetch('/state')
.then(response => response.json())
.then(data => {
// Face buttons
document.getElementById('btn-a').classList.toggle('active', data.a);
document.getElementById('btn-b').classList.toggle('active', data.b);
document.getElementById('btn-x').classList.toggle('active', data.x);
document.getElementById('btn-y').classList.toggle('active', data.y);
// D-pad
document.getElementById('dpad-up').classList.toggle('active', data.dpadUp);
document.getElementById('dpad-down').classList.toggle('active', data.dpadDown);
document.getElementById('dpad-left').classList.toggle('active', data.dpadLeft);
document.getElementById('dpad-right').classList.toggle('active', data.dpadRight);
// Bumpers
document.getElementById('bumper-l').classList.toggle('active', data.l1);
document.getElementById('bumper-r').classList.toggle('active', data.r1);
// Sticks (L3/R3 click)
document.getElementById('stick-l').classList.toggle('active', data.l3);
document.getElementById('stick-r').classList.toggle('active', data.r3);
// Analog sticks position
let leftX = (data.leftX / 512) * 20; // -20 to +20 pixels
let leftY = (data.leftY / 512) * 20;
let rightX = (data.rightX / 512) * 20;
let rightY = (data.rightY / 512) * 20;
document.getElementById('stick-l-indicator').style.transform =
`translate(calc(-50% + ${leftX}px), calc(-50% + ${-leftY}px))`;
document.getElementById('stick-r-indicator').style.transform =
`translate(calc(-50% + ${rightX}px), calc(-50% + ${-rightY}px))`;
// Triggers
let leftTrigger = (data.leftTrigger / 1023) * 100;
let rightTrigger = (data.rightTrigger / 1023) * 100;
document.getElementById('trigger-l-fill').style.width = leftTrigger + '%';
document.getElementById('trigger-r-fill').style.width = rightTrigger + '%';
})
.catch(err => console.error('Error fetching controller state:', err));
}
// Update every 100ms
setInterval(updateController, 100);
updateController(); // Initial update
</script>
</body>
</html>
)rawliteral";
// Replace placeholders in HTML
String getConfigHTML() {
String html = CONFIG_HTML;
html.replace("%FIRMWARE_VERSION%", String(FIRMWARE_VERSION));
html.replace("%MIDI_NAME%", String(config.midiName));
html.replace("%FACE_BTN%", config.faceButtons ? "checked" : "");
html.replace("%DPAD_BTN%", config.dpadButtons ? "checked" : "");
html.replace("%BUMP_BTN%", config.bumperButtons ? "checked" : "");
html.replace("%STICK_BTN%", config.stickButtons ? "checked" : "");
html.replace("%LEFT_STICK%", config.leftStick ? "checked" : "");
html.replace("%RIGHT_STICK%", config.rightStick ? "checked" : "");
html.replace("%TRIGGERS%", config.triggers ? "checked" : "");
html.replace("%RUMBLE_BTN%", config.rumbleOnButtons ? "checked" : "");
html.replace("%RUMBLE_DPAD%", config.rumbleOnDpad ? "checked" : "");
html.replace("%RUMBLE_BUMP%", config.rumbleOnBumpers ? "checked" : "");
html.replace("%RUMBLE_STICK%", config.rumbleOnStickButtons ? "checked" : "");
html.replace("%RUMBLE_TRIG%", config.rumbleOnTriggers ? "checked" : "");
html.replace("%RUMBLE_DUR%", String(config.rumbleDuration));
html.replace("%LED_BRIGHT%", String(config.ledBrightness));
html.replace("%STICK_DZ%", String(config.stickDeadzone));
return html;
}
// Handler for OTA upload processing
void handleOTAUpload() {
HTTPUpload& upload = server.upload();
if (upload.status == UPLOAD_FILE_START) {
Serial.printf("OTA Update Start: %s\n", upload.filename.c_str());
otaInProgress = true;
// Stop MIDI
MIDI.turnThruOff();
// CRITICAL: Disable BluePad32 new connections during OTA
BP32.enableNewBluetoothConnections(false);
// Disconnect existing controllers
for (int i = 0; i < BP32_MAX_GAMEPADS; i++) {
if (myControllers[i] != nullptr && myControllers[i]->isConnected()) {
myControllers[i]->disconnect();
}
}
delay(500);
Serial.printf("Free heap before update: %d\n", ESP.getFreeHeap());
if (!Update.begin(UPDATE_SIZE_UNKNOWN)) {
Update.printError(Serial);
otaInProgress = false;
}
}
else if (upload.status == UPLOAD_FILE_WRITE) {
// Feed watchdog to prevent timeout
esp_task_wdt_reset(); // ADD THIS LINE
size_t written = Update.write(upload.buf, upload.currentSize);
if (written != upload.currentSize) {
Update.printError(Serial);
Serial.printf("Write error: expected %d, wrote %d\n", upload.currentSize, written);
}
// Progress logging
if (Update.progress() % 65536 == 0 || Update.progress() < 65536) {
Serial.printf("Uploaded: %u bytes\n", Update.progress());
}
yield();
}
else if (upload.status == UPLOAD_FILE_END) {
Serial.printf("Upload complete: %u bytes\n", upload.totalSize);
if (Update.end(true)) {
Serial.println("Update SUCCESS");
BP32.enableNewBluetoothConnections(true);
} else {
Update.printError(Serial);
Serial.printf("Update FAILED: %s\n", Update.errorString());
BP32.enableNewBluetoothConnections(true);
}
otaInProgress = false;
}
else if (upload.status == UPLOAD_FILE_ABORTED) {
Update.abort();
BP32.enableNewBluetoothConnections(true);
otaInProgress = false;
Serial.println("Update aborted");
}
}
// Handler for OTA response page (called AFTER upload completes)
void handleOTAComplete() {
Serial.printf("handleOTAcomplete called");
if (Update.hasError()) {
server.send(500, "text/html",
"<html><body style='font-family:Arial;text-align:center;padding:50px;background:#1a1a1a;color:#fff;'>"
"<h1 style='color:#f44336;'>Update Failed!</h1>"
"<p>Please try again.</p>"
"<p><a href='/ota'>Return to update page</a></p>"
"</body></html>");
} else {
server.send(200, "text/html",
"<html><body style='font-family:Arial;text-align:center;padding:50px;background:#1a1a1a;color:#fff;'>"
"<h1 style='color:#4CAF50;'>Update Successful!</h1>"
"<p>Device will restart in 3 seconds...</p>"
"</body></html>");
delay(3000);
ESP.restart();
}
}
// Handler for OTA update page
void handleOTA() {
String html = R"rawliteral(
<!DOCTYPE html>
<html>
<head>
<title>OTA Update</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<style>
body { font-family: Arial; margin: 20px; background: #1a1a1a; color: #fff; text-align: center; }
h1 { color: #FF9800; }
.container { max-width: 600px; margin: 50px auto; background: #2a2a2a; padding: 30px; border-radius: 10px; }
input[type="file"] { margin: 20px 0; padding: 10px; color: #fff; }
button {
background: #FF9800; color: white; padding: 15px 30px;
font-size: 18px; border: none; border-radius: 5px; cursor: pointer; margin: 10px;
}
button:hover { background: #F57C00; }
.back-btn { background: #666; }
.back-btn:hover { background: #555; }
#progress { width: 100%; height: 30px; background: #3a3a3a; border-radius: 5px; margin: 20px 0; overflow: hidden; display: none; }
#progress-bar { height: 100%; background: linear-gradient(90deg, #FF9800, #FFC107); width: 0%; transition: width 0.3s; }
#status { margin: 20px 0; font-size: 16px; }
</style>
</head>
<body>
<div class="container">
<h1>OTA Firmware Update</h1>
<p>Current Version: )rawliteral" + String(FIRMWARE_VERSION) + R"rawliteral(</p>
<p style="color: #FF5722;"><strong>Warning:</strong> Do not disconnect power during update!</p>
<form method='POST' action='/ota-upload' enctype='multipart/form-data' id='upload-form'>
<input type='file' name='update' accept='.bin' id='file-input' required>
<br><br>
<button type='submit'>Upload Firmware</button>
<button type='button' class='back-btn' onclick="location.href='/'">Back</button>
</form>
<div id='progress'><div id='progress-bar'></div></div>
<div id='status'></div>
</div>
<script>
const form = document.getElementById('upload-form');
const fileInput = document.getElementById('file-input');
const progressDiv = document.getElementById('progress');
const progressBar = document.getElementById('progress-bar');
const statusDiv = document.getElementById('status');
const uploadBtn = form.querySelector('button[type="submit"]');
form.addEventListener('submit', function(e) {
e.preventDefault();
const file = fileInput.files[0];
if (!file) {
statusDiv.innerHTML = '<span style="color:#f44336;">Please select a file</span>';
return;
}
// Show UI feedback
progressDiv.style.display = 'block';
statusDiv.textContent = 'Preparing upload...';
uploadBtn.disabled = true;
uploadFirmware(file, 0); // Start upload with retry attempt 0
});
function uploadFirmware(file, retryCount) {
const maxRetries = 3;
const formData = new FormData();
formData.append('update', file);
const xhr = new XMLHttpRequest();
// CRITICAL: Set long timeout for large files
xhr.timeout = 180000; // 3 minutes timeout
// Progress monitoring
xhr.upload.addEventListener('progress', function(e) {
if (e.lengthComputable) {
const percentComplete = Math.round((e.loaded / e.total) * 100);
progressBar.style.width = percentComplete + '%';
statusDiv.textContent = 'Uploading: ' + percentComplete + '% (' +
Math.round(e.loaded / 1024) + ' KB / ' +
Math.round(e.total / 1024) + ' KB)';
}
});
// Success handler
xhr.addEventListener('load', function() {
if (xhr.status === 200) {
progressBar.style.width = '100%';
statusDiv.innerHTML = '<span style="color:#4CAF50;">✅ Upload complete! Device restarting...</span>';
// Wait and redirect
setTimeout(function() {
statusDiv.innerHTML += '<br><span style="color:#aaa;">Waiting for device to restart...</span>';
// Try to reconnect after 10 seconds
setTimeout(function() {
window.location.href = '/';
}, 10000);
}, 2000);
} else {
handleError('Server returned error: ' + xhr.status, file, retryCount);
}
});
// Network error handler
xhr.addEventListener('error', function() {
handleError('Network error during upload', file, retryCount);
});
// Timeout handler
xhr.addEventListener('timeout', function() {
handleError('Upload timeout (file too large or connection too slow)', file, retryCount);
});
// Abort handler
xhr.addEventListener('abort', function() {
statusDiv.innerHTML = '<span style="color:#f44336;">Upload cancelled</span>';
uploadBtn.disabled = false;
});
// Error handler function with retry logic
function handleError(errorMsg, file, attempt) {
console.error('Upload error:', errorMsg, 'Attempt:', attempt + 1);
if (attempt < maxRetries) {
const nextAttempt = attempt + 1;
statusDiv.innerHTML = '<span style="color:#FF9800;">⚠️ ' + errorMsg +
'<br>Retrying... (Attempt ' + nextAttempt + ' of ' + maxRetries + ')</span>';
// Wait 2 seconds before retry
setTimeout(function() {
uploadFirmware(file, nextAttempt);
}, 2000);
} else {
statusDiv.innerHTML = '<span style="color:#f44336;">❌ Upload failed: ' + errorMsg +
'<br>All ' + maxRetries + ' attempts failed.<br>' +
'<a href="/ota" style="color:#FF9800;">Click here to try again</a></span>';
uploadBtn.disabled = false;
progressBar.style.width = '0%';
}
}
// Send the request
try {
xhr.open('POST', '/ota-upload', true);
xhr.send(formData);
statusDiv.textContent = 'Starting upload... (Attempt ' + (retryCount + 1) + ')';
} catch (err) {
console.error('XHR send error:', err);
handleError('Failed to send request: ' + err.message, file, retryCount);
}
}
</script>
</body>
</html>
)rawliteral";
server.send(200, "text/html", html);
}
// Load configuration from preferences
void loadConfig() {
preferences.begin("xbox-midi", false);
config.faceButtons = preferences.getBool("faceBtn", true);
config.dpadButtons = preferences.getBool("dpadBtn", true);
config.bumperButtons = preferences.getBool("bumperBtn", true);
config.leftStick = preferences.getBool("leftStick", true);
config.rightStick = preferences.getBool("rightStick", false);
config.triggers = preferences.getBool("triggers", true);
config.rumbleOnButtons = preferences.getBool("rumbleBtn", true);
config.rumbleOnDpad = preferences.getBool("rumbleDpad", true);
config.rumbleOnBumpers = preferences.getBool("rumbleBump", true);
config.rumbleOnTriggers = preferences.getBool("rumbleTrig", true);
config.rumbleDuration = preferences.getInt("rumbleDur", 100);
config.ledBrightness = preferences.getInt("ledBright", 128);
config.stickDeadzone = preferences.getInt("stickDeadzone", 50);
config.stickButtons = preferences.getBool("stickBtn", true);
config.rumbleOnStickButtons = preferences.getBool("rumbleStick", true);
preferences.getString("midiName", config.midiName, sizeof(config.midiName));
preferences.end();
// Apply brightness
pixels.setBrightness(config.ledBrightness);
DEBUG_PRINTLN("Configuration loaded from preferences");
}
// Save configuration to preferences
void saveConfig() {
preferences.begin("xbox-midi", false);
preferences.putBool("faceBtn", config.faceButtons);
preferences.putBool("dpadBtn", config.dpadButtons);
preferences.putBool("bumperBtn", config.bumperButtons);
preferences.putBool("leftStick", config.leftStick);
preferences.putBool("rightStick", config.rightStick);
preferences.putBool("triggers", config.triggers);
preferences.putBool("rumbleBtn", config.rumbleOnButtons);
preferences.putBool("rumbleDpad", config.rumbleOnDpad);
preferences.putBool("rumbleBump", config.rumbleOnBumpers);
preferences.putBool("rumbleTrig", config.rumbleOnTriggers);
preferences.putInt("rumbleDur", config.rumbleDuration);
preferences.putInt("ledBright", config.ledBrightness);
preferences.putInt("stickDeadzone", config.stickDeadzone);
preferences.putString("midiName", config.midiName);
preferences.putBool("stickBtn", config.stickButtons);
preferences.putBool("rumbleStick", config.rumbleOnStickButtons);
preferences.end();
// Apply brightness immediately
pixels.setBrightness(config.ledBrightness);
DEBUG_PRINTLN("Configuration saved to preferences");
}
// Web server handlers
void handleRoot() {
server.send(200, "text/html", getConfigHTML());
}
void handleSave() {
// Update configuration from form
config.faceButtons = server.hasArg("faceButtons");
config.dpadButtons = server.hasArg("dpadButtons");
config.bumperButtons = server.hasArg("bumperButtons");
config.stickButtons = server.hasArg("stickButtons");
config.leftStick = server.hasArg("leftStick");
config.rightStick = server.hasArg("rightStick");
config.triggers = server.hasArg("triggers");
config.rumbleOnButtons = server.hasArg("rumbleButtons");
config.rumbleOnDpad = server.hasArg("rumbleDpad");
config.rumbleOnBumpers = server.hasArg("rumbleBumpers");
config.rumbleOnStickButtons = server.hasArg("rumbleStickButtons");
config.rumbleOnTriggers = server.hasArg("rumbleTriggers");
if (server.hasArg("rumbleDuration")) {
config.rumbleDuration = server.arg("rumbleDuration").toInt();
}
if (server.hasArg("ledBrightness")) {
config.ledBrightness = server.arg("ledBrightness").toInt();
}
if (server.hasArg("stickDeadzone")) {
config.stickDeadzone = server.arg("stickDeadzone").toInt();
}
if (server.hasArg("midiName")) {
strncpy(config.midiName, server.arg("midiName").c_str(), sizeof(config.midiName) - 1);
config.midiName[sizeof(config.midiName) - 1] = '\0';
}
saveConfig();
updateButtonGroups();
// Send success page
String html = "<html><head><meta http-equiv='refresh' content='3;url=/'></head><body style='font-family:Arial;text-align:center;padding:50px;background:#1a1a1a;color:#fff;'>";
html += "<h1 style='color:#4CAF50;'>Configuration Saved!</h1>";
html += "<p>Settings have been saved to flash memory.</p>";
html += "<p>Redirecting back to configuration page...</p>";
html += "</body></html>";
server.send(200, "text/html", html);
DEBUG_PRINTLN("Configuration updated and saved");
}
void handleExit() {
String html = "<html><body style='font-family:Arial;text-align:center;padding:50px;background:#1a1a1a;color:#fff;'>";
html += "<h1 style='color:#4CAF50;'>Exiting Configuration Mode</h1>";
html += "<p>AP mode will shut down in 3 seconds...</p>";
html += "<p>You can reconnect your MIDI software now.</p>";
html += "</body></html>";
server.send(200, "text/html", html);
delay(3000);
stopAPMode();
}
// Start AP mode
void startAPMode() {
if (apModeActive) return;
DEBUG_PRINTLN("Starting AP mode...");
setRGBLED(255, 128, 0); // Orange for config mode
// Ensure WiFi is completely off first
WiFi.mode(WIFI_OFF);
delay(500); // Critical: Wait for WiFi to fully shut down
// Now start AP mode
WiFi.mode(WIFI_AP);
delay(100); // Wait for mode change
WiFi.softAP("Xbox-MIDI-Config"); // Added password from your comments
delay(500); // Wait for AP to fully initialize
IPAddress IP = WiFi.softAPIP();
DEBUG_PRINT("AP IP address: ");
DEBUG_PRINTLN(IP);
server.on("/", handleRoot);
server.on("/save", HTTP_POST, handleSave);
server.on("/exit", handleExit);
server.on("/state", handleControllerState);
server.on("/ota", handleOTA);
server.on("/ota-upload", HTTP_POST, handleOTAComplete, handleOTAUpload);
server.enableCORS(true);
server.enableCrossOrigin(true);
server.begin();
apModeActive = true;
DEBUG_PRINTLN("Web server started on http://192.168.4.1");
}
// Stop AP mode
void stopAPMode() {
if (!apModeActive) return;
DEBUG_PRINTLN("Stopping AP mode...");
server.stop();
delay(100); // Give server time to stop
WiFi.softAPdisconnect(true);
delay(500); // Critical: Wait for AP to fully disconnect
WiFi.mode(WIFI_OFF);
delay(100); // Wait for mode change to complete
apModeActive = false;
setRGBLED(255, 0, 0); // Back to red (no controller)
DEBUG_PRINTLN("AP mode stopped");
}
// Check boot button for config mode
void checkBootButton() {
bool currentState = digitalRead(BOOT_BUTTON) == LOW; // Button is active LOW
if (currentState && !bootButtonPressed) {
// Button just pressed
bootButtonPressed = true;
bootButtonPressTime = millis();
DEBUG_PRINTLN("Boot button pressed");
} else if (!currentState && bootButtonPressed) {
// Button released
bootButtonPressed = false;
DEBUG_PRINTLN("Boot button released");
} else if (currentState && bootButtonPressed) {
// Button being held
unsigned long holdTime = millis() - bootButtonPressTime;
if (holdTime >= BUTTON_HOLD_TIME) {
if (!apModeActive) {
DEBUG_PRINTLN("5 seconds hold detected - entering config mode");
startAPMode();
} else {
DEBUG_PRINTLN("5 seconds hold detected - exiting config mode");
stopAPMode();
}
bootButtonPressed = false; // Reset to prevent retriggering
}
}
}
// Utility functions
void setRGBLED(uint8_t r, uint8_t g, uint8_t b) {
pixels.setPixelColor(0, pixels.Color(r, g, b));
pixels.show();
}
// Update controller state for visualization
void updateControllerState(ControllerPtr ctl) {
currentState.a = ctl->a();
currentState.b = ctl->b();
currentState.x = ctl->x();
currentState.y = ctl->y();
uint8_t dpad = ctl->dpad();
currentState.dpadUp = (dpad & DPAD_UP) != 0;
currentState.dpadDown = (dpad & DPAD_DOWN) != 0;
currentState.dpadLeft = (dpad & DPAD_LEFT) != 0;
currentState.dpadRight = (dpad & DPAD_RIGHT) != 0;
currentState.l1 = ctl->l1();
currentState.r1 = ctl->r1();
currentState.l3 = ctl->thumbL();
currentState.r3 = ctl->thumbR();
currentState.leftX = ctl->axisX();
currentState.leftY = ctl->axisY();
currentState.rightX = ctl->axisRX();
currentState.rightY = ctl->axisRY();
currentState.leftTrigger = ctl->brake();
currentState.rightTrigger = ctl->throttle();
currentState.lastUpdate = millis();
}
// Handler for live controller state (JSON API)
void handleControllerState() {
String json = "{";
json += "\"a\":" + String(currentState.a ? "true" : "false") + ",";
json += "\"b\":" + String(currentState.b ? "true" : "false") + ",";
json += "\"x\":" + String(currentState.x ? "true" : "false") + ",";
json += "\"y\":" + String(currentState.y ? "true" : "false") + ",";
json += "\"dpadUp\":" + String(currentState.dpadUp ? "true" : "false") + ",";
json += "\"dpadDown\":" + String(currentState.dpadDown ? "true" : "false") + ",";
json += "\"dpadLeft\":" + String(currentState.dpadLeft ? "true" : "false") + ",";
json += "\"dpadRight\":" + String(currentState.dpadRight ? "true" : "false") + ",";
json += "\"l1\":" + String(currentState.l1 ? "true" : "false") + ",";
json += "\"r1\":" + String(currentState.r1 ? "true" : "false") + ",";
json += "\"l3\":" + String(currentState.l3 ? "true" : "false") + ",";
json += "\"r3\":" + String(currentState.r3 ? "true" : "false") + ",";
json += "\"leftX\":" + String(currentState.leftX) + ",";
json += "\"leftY\":" + String(currentState.leftY) + ",";
json += "\"rightX\":" + String(currentState.rightX) + ",";
json += "\"rightY\":" + String(currentState.rightY) + ",";
json += "\"leftTrigger\":" + String(currentState.leftTrigger) + ",";
json += "\"rightTrigger\":" + String(currentState.rightTrigger) + ",";
json += "\"lastUpdate\":" + String(currentState.lastUpdate);
json += "}";
server.send(200, "application/json", json);
}
// Update button groups with current config
void updateButtonGroups() {
buttonGroups[0] = {BUTTON_NOTES, buttonStates, 4, config.faceButtons, config.rumbleOnButtons, 90, 60};
buttonGroups[1] = {DPAD_NOTES, dpadStates, 4, config.dpadButtons, config.rumbleOnDpad, 60, 45};
buttonGroups[2] = {BUMPER_NOTES, bumperStates, 2, config.bumperButtons, config.rumbleOnBumpers, 75, 50};
buttonGroups[3] = {STICK_BUTTON_NOTES, stickButtonStates, 2, config.stickButtons, config.rumbleOnStickButtons, 70, 55}; // Add this line
}
bool isControllerConnected() {
for (int i = 0; i < BP32_MAX_GAMEPADS; i++) {
if (myControllers[i] != nullptr && myControllers[i]->isConnected()) {
return true;
}
}
return false;
}
int analogToMIDI(int analogValue) {
return map(analogValue, -511, 512, 0, 127);
}
int triggerToMIDI(int triggerValue) {
return map(triggerValue, 0, 1023, 0, 127);
}
// Generic button processing
void processButtonGroup(bool* currentStates, ButtonGroup& group) {
if (!group.enabled) return;
for (int i = 0; i < group.count; i++) {
if (currentStates[i] && !group.states[i]) {
MIDI.sendNoteOn(group.notes[i], 127, 1);
DEBUG_PRINTF("Note %d ON\n", group.notes[i]);
} else if (!currentStates[i] && group.states[i]) {
MIDI.sendNoteOff(group.notes[i], 0, 1);
DEBUG_PRINTF("Note %d OFF\n", group.notes[i]);
}
group.states[i] = currentStates[i];
}
}
// Generic analog control processing
void processAnalogControl(int currentValue, int& lastValue, int threshold,
byte ccNumber, bool inverted = false) {
// Apply deadzone
int adjustedValue = currentValue;
if (abs(currentValue) < config.stickDeadzone) {
adjustedValue = 0; // Within deadzone, treat as centered
}
if (abs(adjustedValue - lastValue) > threshold) {
int midiValue = inverted ? analogToMIDI(-adjustedValue) : analogToMIDI(adjustedValue);
MIDI.sendControlChange(ccNumber, midiValue, 1);
DEBUG_PRINTF("CC%d: %d\n", ccNumber, midiValue);
lastValue = adjustedValue;
}
}
void processTriggerControl(int currentValue, int& lastValue, byte ccNumber) {
if (abs(currentValue - lastValue) > TRIGGER_THRESHOLD) {
int midiValue = triggerToMIDI(currentValue);
MIDI.sendControlChange(ccNumber, midiValue, 1);
DEBUG_PRINTF("CC%d: %d\n", ccNumber, midiValue);
lastValue = currentValue;
}
}
// Count active inputs for LED visualization
int countActiveInputs(ControllerPtr ctl) {
int count = 0;
if (config.faceButtons && (ctl->a() || ctl->b() || ctl->x() || ctl->y())) count += 2;
if (config.dpadButtons && ctl->dpad() != 0) count += 2;
if (config.bumperButtons && (ctl->l1() || ctl->r1())) count += 2;
if (config.triggers && (ctl->brake() > 100 || ctl->throttle() > 100)) count += 2;
return count;
}
// Update LED based on activity
void updateLED(ControllerPtr ctl) {
if (apModeActive) {
return; // Don't change LED in config mode
}
if (!isControllerConnected()) {
setRGBLED(255, 0, 0); // Red = no controller
return;
}
int activity = countActiveInputs(ctl);
uint8_t green = activity > 0 ? map(activity, 1, 8, 50, 255) : 0;
setRGBLED(0, green, 50); // Blue + variable green
}
// Unified rumble processing
void processRumble(ControllerPtr ctl) {
unsigned long currentTime = millis();
if (currentTime - lastRumbleTime < RUMBLE_INTERVAL) return;
uint8_t weak = 0, strong = 0;
// Face buttons
if (config.rumbleOnButtons && (ctl->a() || ctl->b() || ctl->x() || ctl->y())) {
weak = max(weak, (uint8_t)90);
strong = max(strong, (uint8_t)60);
}
// D-pad
if (config.rumbleOnDpad && ctl->dpad() != 0) {
weak = max(weak, (uint8_t)60);
strong = max(strong, (uint8_t)45);
}
// Bumpers
if (config.rumbleOnBumpers && (ctl->l1() || ctl->r1())) {
weak = max(weak, (uint8_t)75);
strong = max(strong, (uint8_t)50);
}
if (config.rumbleOnStickButtons && (ctl->thumbL() || ctl->thumbR())) {
weak = max(weak, (uint8_t)70);
strong = max(strong, (uint8_t)55);
}
// Triggers (progressive)
if (config.rumbleOnTriggers) {
int lt = ctl->brake(), rt = ctl->throttle();
if (lt > 50) {
uint8_t intensity = map(lt, 50, 1023, 20, 255);
weak = max(weak, intensity);
strong = max(strong, (uint8_t)(intensity * 0.7));
}
if (rt > 50) {
uint8_t intensity = map(rt, 50, 1023, 20, 255);
weak = max(weak, (uint8_t)(intensity * 0.7));
strong = max(strong, intensity);
}
}
if (weak > 0 || strong > 0) {
ctl->playDualRumble(0, config.rumbleDuration, weak, strong);
}
lastRumbleTime = currentTime;
}
// Main gamepad processing
void processGamepad(ControllerPtr ctl) {
// Process button groups
bool faceButtons[4] = {ctl->a(), ctl->b(), ctl->x(), ctl->y()};
processButtonGroup(faceButtons, buttonGroups[0]);
uint8_t dpad = ctl->dpad();
bool dpadButtons[4] = {
(dpad & DPAD_UP) != 0,
(dpad & DPAD_DOWN) != 0,
(dpad & DPAD_LEFT) != 0,
(dpad & DPAD_RIGHT) != 0
};
processButtonGroup(dpadButtons, buttonGroups[1]);
bool bumpers[2] = {ctl->l1(), ctl->r1()};
processButtonGroup(bumpers, buttonGroups[2]);
bool stickButtons[2] = {ctl->thumbL(), ctl->thumbR()};
processButtonGroup(stickButtons, buttonGroups[3]);
// Process analog controls
if (config.leftStick) {
processAnalogControl(ctl->axisX(), lastAnalog[0], STICK_THRESHOLD, 1);
processAnalogControl(ctl->axisY(), lastAnalog[1], STICK_THRESHOLD, 7, true);
}
if (config.rightStick) {
processAnalogControl(ctl->axisRX(), lastAnalog[2], STICK_THRESHOLD, 10);
processAnalogControl(ctl->axisRY(), lastAnalog[3], STICK_THRESHOLD, 11, true);
}
if (config.triggers) {
processTriggerControl(ctl->brake(), lastAnalog[4], 64);
processTriggerControl(ctl->throttle(), lastAnalog[5], 65);
}
// Update feedback
processRumble(ctl);
updateLED(ctl);
}
void processControllers() {
for (auto ctl : myControllers) {
if (ctl && ctl->isConnected() && ctl->hasData() && ctl->isGamepad()) {
processGamepad(ctl);
}
}
}
// Connection callbacks
void onConnectedController(ControllerPtr ctl) {
for (int i = 0; i < BP32_MAX_GAMEPADS; i++) {
if (myControllers[i] == nullptr) {
DEBUG_PRINTF("Controller connected, index=%d\n", i);
myControllers[i] = ctl;
if (!apModeActive) {
setRGBLED(0, 0, 150); // Blue
}
return;
}
}
DEBUG_PRINTLN("No empty slot for controller");
}
void onDisconnectedController(ControllerPtr ctl) {
for (int i = 0; i < BP32_MAX_GAMEPADS; i++) {
if (myControllers[i] == ctl) {
DEBUG_PRINTF("Controller disconnected, index=%d\n", i);
myControllers[i] = nullptr;
break;
}
}
if (!isControllerConnected() && !apModeActive) {
setRGBLED(255, 0, 0); // Red
}
}
// Print configuration helper
void printConfig(const char* name, bool enabled) {
Serial.printf("%s: %s\n", name, enabled ? "ENABLED" : "DISABLED");
}
void setup() {
Serial.begin(115200);
Serial.println("\n\n=================================");
Serial.println("Xbox Controller MIDI Interface");
Serial.println("=================================\n");
// Initialize boot button
pinMode(BOOT_BUTTON, INPUT_PULLUP);
// Load configuration from flash
loadConfig();
// Initialize button groups with loaded config
updateButtonGroups();
// Initialize RGB LED
pixels.begin();
pixels.clear();
pixels.setBrightness(config.ledBrightness);
setRGBLED(255, 0, 0); // Start with red (no controller)
// Initialize MIDI
espnativeusbmidi.begin();
USB.productName(config.midiName);
USB.begin();
MIDI.begin(MIDI_CHANNEL_OMNI);
// Initialize Bluepad32
Serial.printf("Firmware: %s\n", BP32.firmwareVersion());
const uint8_t* addr = BP32.localBdAddress();
Serial.printf("BD Addr: %2X:%2X:%2X:%2X:%2X:%2X\n",
addr[0], addr[1], addr[2], addr[3], addr[4], addr[5]);
BP32.setup(&onConnectedController, &onDisconnectedController);
BP32.forgetBluetoothKeys();
BP32.enableVirtualDevice(false);
// Print current configuration
Serial.println("\n=== CURRENT CONFIGURATION ===");
printConfig("Face buttons", config.faceButtons);
printConfig("D-pad", config.dpadButtons);
printConfig("Bumpers", config.bumperButtons);
printConfig("Left stick", config.leftStick);
printConfig("Right stick", config.rightStick);
printConfig("Triggers", config.triggers);
Serial.printf("MIDI Name: %s\n", config.midiName);
Serial.printf("LED Brightness: %d/255\n", config.ledBrightness);
Serial.println("\n=== RUMBLE CONFIGURATION ===");
printConfig("Button rumble", config.rumbleOnButtons);
printConfig("D-pad rumble", config.rumbleOnDpad);
printConfig("Bumper rumble", config.rumbleOnBumpers);
printConfig("Trigger rumble", config.rumbleOnTriggers);
Serial.printf("Rumble Duration: %d ms\n", config.rumbleDuration);
Serial.println("\n=================================");
Serial.println("Ready! Connect Xbox controller via Bluetooth");
Serial.println("Hold BOOT button for 5 seconds to enter config mode");
Serial.println("=================================\n");
}
void loop() {
checkBootButton();
if (apModeActive) {
// Exit immediately during OTA - ONLY handle web server
if (otaInProgress) {
server.handleClient();
yield(); // Just yield, don't call BP32.update()
return; // Skip everything else
}
// Normal AP mode operation - only when NOT doing OTA
server.handleClient();
if (BP32.update()) {
processControllers();
for (auto ctl : myControllers) {
if (ctl && ctl->isConnected() && ctl->hasData() && ctl->isGamepad()) {
updateControllerState(ctl);
}
}
}
} else {
if (BP32.update()) {
processControllers();
}
MIDI.read();
}
delay(1);
}