OTA over AP mode instable (~25% success rate)

anonOmattie
Posts: 1
Joined: Thu Oct 16, 2025 2:55 pm

OTA over AP mode instable (~25% success rate)

Postby anonOmattie » Thu Oct 16, 2025 3:00 pm

Hey All! I am struggling to get OTA over AP mode working while trying to update my sketch using OTA. Right now I get about ~25% success rate when trying to update OTA over the AP mode created by my ESP.

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);
}

Who is online

Users browsing this forum: Amazon [Bot], Google [Bot] and 1 guest