// Modulos ADAU DSP WiFi + EEPROM (HTTP uploader + Sigma TCP bridge) // Build: 260304_13 (YYMMDD_rev) #include #include #include #include #include "DSPWriter.h" #include #include #include #include "FS.h" #include #include #include #include "esp_rom_crc.h" //============================================================= // WiFi / UI //============================================================= const char* hostname = "modulos-dsp"; const char* version = "VER: 260304_13"; // Compile-time default credentials — used when NVS has no saved credentials. // Change these before flashing. If NVS credentials are saved (via the config // portal) they take priority over these on subsequent boots. static const char DEFAULT_SSID[] = "alfred"; static const char DEFAULT_PASS[] = "alfred16"; // Runtime WiFi credentials — populated from NVS or defaults at boot static char s_ssid[64] = ""; static char s_pass[64] = ""; #define I2C_SDA 13 #define I2C_SCL 12 WiFiServer tcpServer(8086); WebServer httpServer(80); #include "index_html.h" #include "ota_html.h" #include "ap_html.h" #include "params_html.h" hd44780_I2Cexp lcd; static bool s_lcdOk = false; //============================================================= // NeoPixel status LED (Waveshare ESP32-S3 Zero, GPIO 21) //============================================================= #define NEOPIXEL_PIN 21 #define NEOPIXEL_COUNT 1 #define NEOPIXEL_BRIGHT 40 Adafruit_NeoPixel statusLed(NEOPIXEL_COUNT, NEOPIXEL_PIN, NEO_GRB + NEO_KHZ800); static void ledSet(uint8_t r, uint8_t g, uint8_t b) { statusLed.setPixelColor(0, statusLed.Color(r, g, b)); statusLed.show(); } static void ledOff() { ledSet(0, 0, 0); } static void ledCyan() { ledSet(0, NEOPIXEL_BRIGHT/2, NEOPIXEL_BRIGHT/2); } static void ledGreen() { ledSet(0, NEOPIXEL_BRIGHT, 0); } static void ledBlue() { ledSet(0, 0, NEOPIXEL_BRIGHT); } static void ledYellow() { ledSet(NEOPIXEL_BRIGHT, NEOPIXEL_BRIGHT, 0); } static void ledMagenta() { ledSet(NEOPIXEL_BRIGHT, 0, NEOPIXEL_BRIGHT); } static void ledErrorFlash() { for (int i = 0; i < 2; i++) { ledSet(NEOPIXEL_BRIGHT, 0, 0); delay(80); ledOff(); delay(80); } } //============================================================= // TCP protocol buffer //============================================================= static uint8_t dataBuffer[50 * 1024]; #define STATE_START 0 #define STATE_READ_CMD 1 #define STATE_WRITE_CMD 2 #define CMD_WRITE 0x09 #define CMD_READ 0x0A #define TCP_IDLE_TIMEOUT_MS 30000 // drop session if silent for 30 s #define TCP_DEBUG 0 // set to 1 to log raw RX bytes to Serial constexpr int WRITE_HDR_LEN = 10; constexpr int READ_HDR_LEN = 8; struct adauWriteHeader { uint8_t command; uint8_t safeload; uint8_t placement; uint16_t totalLen; uint8_t chipAddr; uint16_t dataLen; uint16_t address; }; struct adauReadHeader { uint8_t command; uint16_t totalLen; uint8_t chipAddr; uint16_t dataLen; uint16_t address; }; static adauWriteHeader writeHeader; static adauReadHeader readHeader; static constexpr uint8_t DSP_7BIT = DSP_I2C_ADDRESS; // 0x34 static constexpr uint8_t EEPROM_7BIT = EEPROM_I2C_ADDRESS; // 0x50 //============================================================= // 24C256 EEPROM //============================================================= static constexpr uint32_t EEPROM_SIZE_BYTES = 32768; static constexpr uint16_t EEPROM_PAGE_SIZE = 64; static constexpr uint8_t I2C_MAX_DATA_PER_TX = 28; //============================================================= // Helpers //============================================================= static uint8_t chipAddrTo7bit(uint8_t chipAddr) { switch (chipAddr) { case 0x01: return 0x34; // chip index 1 = DSP case 0x02: return 0x50; // chip index 2 = EEPROM case 0x68: return 0x34; case 0xA0: return 0x50; case 0x34: return 0x34; case 0x50: return 0x50; default: if (chipAddr > 0x7F) return (uint8_t)(chipAddr >> 1); return chipAddr; } } static uint8_t registerSizeForAddress(uint16_t address, uint16_t dataLen) { if (address == dspRegister::CoreRegister) { if (dataLen == 2) return CORE_REGISTER_R0_REGSIZE; if (dataLen == 24) return HARDWARE_CONF_REGSIZE; return CORE_REGISTER_R0_REGSIZE; } if (address >= DSP_PROG_RAM_START && address <= DSP_PROG_RAM_END) return PROGRAM_REGSIZE; if (address < DSP_PROG_RAM_START) return PARAMETER_REGSIZE; return HARDWARE_CONF_REGSIZE; } static bool i2cAckPoll(uint8_t addr7, uint32_t timeoutMs = 80) { uint32_t start = millis(); while ((millis() - start) < timeoutMs) { Wire.beginTransmission(addr7); if (Wire.endTransmission() == 0) return true; delay(1); } return false; } static bool eepromWritePageChunk(uint16_t memAddr, const uint8_t* data, uint16_t len) { Wire.beginTransmission(EEPROM_7BIT); Wire.write((uint8_t)(memAddr >> 8)); Wire.write((uint8_t)(memAddr & 0xFF)); for (uint16_t i = 0; i < len; i++) Wire.write(data[i]); if (Wire.endTransmission() != 0) return false; return i2cAckPoll(EEPROM_7BIT, 120); } static bool eepromWriteBlock(uint16_t memAddr, const uint8_t* data, uint16_t len) { while (len) { uint16_t pageOff = memAddr % EEPROM_PAGE_SIZE; uint16_t spaceInPage = EEPROM_PAGE_SIZE - pageOff; uint16_t chunk = len; if (chunk > spaceInPage) chunk = spaceInPage; if (chunk > I2C_MAX_DATA_PER_TX) chunk = I2C_MAX_DATA_PER_TX; if (!eepromWritePageChunk(memAddr, data, chunk)) return false; memAddr += chunk; data += chunk; len -= chunk; delay(0); } return true; } static bool i2cReadBlock(uint8_t addr7, uint16_t memAddr, uint8_t* out, uint16_t len) { Wire.beginTransmission(addr7); Wire.write((uint8_t)(memAddr >> 8)); Wire.write((uint8_t)(memAddr & 0xFF)); if (Wire.endTransmission(false) != 0) return false; uint16_t got = 0; while (got < len) { uint8_t ask = (len - got) > 32 ? 32 : (len - got); if (Wire.requestFrom((int)addr7, (int)ask) != ask) return false; for (uint8_t i = 0; i < ask; i++) out[got++] = Wire.read(); } return true; } // SigmaTCP read response: 0x0B, totalLen_hi, totalLen_lo, status, dataLen_hi, dataLen_lo, [payload] static bool sendReadResponse(WiFiClient& client, const uint8_t* data, uint16_t dataLen, bool ok) { uint16_t totalLen = 6 + dataLen; uint8_t hdr[6]; hdr[0] = 0x0B; hdr[1] = (uint8_t)(totalLen >> 8); hdr[2] = (uint8_t)(totalLen & 0xFF); hdr[3] = ok ? 0x00 : 0x01; hdr[4] = (uint8_t)(dataLen >> 8); hdr[5] = (uint8_t)(dataLen & 0xFF); if (client.write(hdr, sizeof(hdr)) != sizeof(hdr)) return false; if (dataLen > 0) { if (ok && data) { if (client.write(data, dataLen) != dataLen) return false; } else { // send zeros so SigmaStudio doesn't stall on a failed read static uint8_t zeros[256]; uint16_t rem = dataLen; while (rem) { uint16_t chunk = rem > sizeof(zeros) ? sizeof(zeros) : rem; if (client.write(zeros, chunk) != chunk) return false; rem -= chunk; } } } return true; } // SigmaTCP write ack: 0x09, 0x00, 0x04, status static bool sendWriteAck(WiFiClient& client, bool ok) { uint8_t ack[4] = { 0x09, 0x00, 0x04, ok ? (uint8_t)0x00 : (uint8_t)0x01 }; return client.write(ack, sizeof(ack)) == sizeof(ack); } static void printHex(const char* label, const uint8_t* buf, uint16_t len, uint16_t maxPrint = 24) { Serial.print(label); uint16_t n = len < maxPrint ? len : maxPrint; for (uint16_t i = 0; i < n; i++) { if (buf[i] < 0x10) Serial.print("0"); Serial.print(buf[i], HEX); Serial.print(" "); } if (len > maxPrint) Serial.print("..."); Serial.println(); } //============================================================= // NVS credential helpers //============================================================= static Preferences s_prefs; static bool loadWifiCreds() { s_prefs.begin("wifi", true); String ssid = s_prefs.getString("ssid", ""); String pass = s_prefs.getString("pass", ""); s_prefs.end(); if (ssid.length() == 0) return false; ssid.toCharArray(s_ssid, sizeof(s_ssid)); pass.toCharArray(s_pass, sizeof(s_pass)); return true; } static void saveWifiCreds(const String& ssid, const String& pass) { s_prefs.begin("wifi", false); s_prefs.putString("ssid", ssid); s_prefs.putString("pass", pass); s_prefs.end(); } static void clearWifiCreds() { s_prefs.begin("wifi", false); s_prefs.clear(); s_prefs.end(); } //============================================================= // SoftAP config portal — runs when no credentials are stored // or when a previous connection attempt failed. // Serves a simple HTML form; never returns. //============================================================= static void startConfigAP() { Serial.println("Starting config AP: ModulosDSP-Setup"); WiFi.mode(WIFI_AP); WiFi.softAP("ModulosDSP-Setup"); delay(100); IPAddress apIP = WiFi.softAPIP(); Serial.print("AP IP: "); Serial.println(apIP); if (s_lcdOk) { lcd.clear(); lcd.setCursor(0, 0); lcd.print("WiFi Setup Mode"); lcd.setCursor(0, 1); lcd.print("ModulosDSP-Setup"); lcd.setCursor(0, 2); lcd.print(apIP); } httpServer.on("/", HTTP_GET, []() { httpServer.send_P(200, "text/html", AP_HTML); }); httpServer.on("/save", HTTP_POST, []() { if (!httpServer.hasArg("ssid") || httpServer.arg("ssid").length() == 0) { httpServer.send(400, "text/plain", "SSID is required."); return; } String newSsid = httpServer.arg("ssid"); String newPass = httpServer.arg("pass"); saveWifiCreds(newSsid, newPass); Serial.printf("Credentials saved for SSID: %s\n", newSsid.c_str()); httpServer.send(200, "text/html", "" "

Saved!

Connecting to " + newSsid + "… The device will reboot now.

"); delay(1000); ESP.restart(); }); httpServer.begin(); Serial.println("Config portal active — waiting for credentials"); while (true) { httpServer.handleClient(); delay(1); } } static void printWifiInfo() { Serial.println(); Serial.println("WiFi connected."); Serial.print("WiFi IP: "); Serial.println(WiFi.localIP()); Serial.printf("Hostname: http://%s.local/\n", hostname); Serial.print("MAC: "); Serial.println(WiFi.macAddress()); Serial.println("Modulos AudioDSP"); Serial.println(version); if (s_lcdOk) { lcd.setCursor(4, 3); lcd.print(WiFi.localIP()); } } //============================================================= // HTTP EEPROM uploader state //============================================================= static volatile bool uploadActive = false; static volatile bool uploadVerify = false; static volatile bool uploadFailed = false; static volatile uint32_t uploadBytes = 0; static volatile uint32_t uploadCrc = 0; //============================================================= // HTTP handlers //============================================================= static String contentTypeFor(const String& path) { if (path.endsWith(".html")) return "text/html"; if (path.endsWith(".css")) return "text/css"; if (path.endsWith(".js")) return "application/javascript"; if (path.endsWith(".png")) return "image/png"; if (path.endsWith(".jpg") || path.endsWith(".jpeg")) return "image/jpeg"; if (path.endsWith(".webp")) return "image/webp"; if (path.endsWith(".svg")) return "image/svg+xml"; if (path.endsWith(".ico")) return "image/x-icon"; if (path.endsWith(".woff")) return "font/woff"; if (path.endsWith(".woff2"))return "font/woff2"; return "application/octet-stream"; } static bool streamFromFS(String path) { if (!LittleFS.exists(path)) { if (path.startsWith("/")) { String alt = path.substring(1); if (LittleFS.exists(alt)) path = alt; else return false; } else { String alt = "/" + path; if (LittleFS.exists(alt)) path = alt; else return false; } } File f = LittleFS.open(path, "r"); if (!f) return false; httpServer.streamFile(f, contentTypeFor(path)); f.close(); return true; } static void handleRoot() { String html = FPSTR(INDEX_HTML); html.replace("{{IP}}", WiFi.localIP().toString()); httpServer.send(200, "text/html; charset=utf-8", html); } static void handleStatus() { String s; s += "uploadActive="; s += (uploadActive ? "1" : "0"); s += "\n"; s += "uploadFailed="; s += (uploadFailed ? "1" : "0"); s += "\n"; s += "uploadBytes="; s += String((uint32_t)uploadBytes); s += "\n"; s += "uploadCRC32=0x"; s += String((uint32_t)uploadCrc, HEX); s += "\n"; httpServer.send(200, "text/plain", s); } static void handleUploadDone() { if (uploadFailed) { httpServer.send(500, "text/plain", "Upload failed.\nCheck Serial log.\n"); return; } String msg = "OK\nBytes written: " + String((uint32_t)uploadBytes) + "\nCRC32: 0x" + String((uint32_t)uploadCrc, HEX) + "\n"; httpServer.send(200, "text/plain", msg); } static void handleUploadStream() { HTTPUpload& up = httpServer.upload(); if (up.status == UPLOAD_FILE_START) { uploadActive = true; uploadFailed = false; uploadBytes = 0; uploadCrc = 0; uploadVerify = httpServer.hasArg("verify"); Serial.println(); Serial.print("HTTP upload start: "); Serial.println(up.filename); Serial.print("Verify: "); Serial.println(uploadVerify ? "yes" : "no"); Wire.beginTransmission(EEPROM_7BIT); Serial.print("EEPROM probe err: "); Serial.println(Wire.endTransmission()); } else if (up.status == UPLOAD_FILE_WRITE) { if (uploadFailed) return; if ((uploadBytes + up.currentSize) > EEPROM_SIZE_BYTES) { Serial.println("Upload too large for 24C256"); uploadFailed = true; return; } if (!eepromWriteBlock((uint16_t)uploadBytes, up.buf, (uint16_t)up.currentSize)) { Serial.println("EEPROM write failed"); uploadFailed = true; return; } uploadCrc = esp_rom_crc32_le(uploadCrc, up.buf, up.currentSize); uploadBytes += up.currentSize; ledMagenta(); } else if (up.status == UPLOAD_FILE_END) { Serial.print("HTTP upload end, bytes="); Serial.println((uint32_t)uploadBytes); if (uploadVerify && !uploadFailed) { Serial.println("Verify start (CRC32)..."); uint32_t crc = 0; static uint8_t tmp[256]; uint32_t remaining = uploadBytes; uint16_t addr = 0; while (remaining) { uint16_t n = remaining > sizeof(tmp) ? sizeof(tmp) : (uint16_t)remaining; if (!i2cReadBlock(EEPROM_7BIT, addr, tmp, n)) { Serial.println("EEPROM read failed"); uploadFailed = true; break; } crc = esp_rom_crc32_le(crc, tmp, n); addr += n; remaining -= n; delay(0); } Serial.print("Verify CRC32: 0x"); Serial.println(crc, HEX); if (!uploadFailed && crc != uploadCrc) { Serial.println("CRC mismatch"); uploadFailed = true; } } uploadActive = false; ledOff(); Serial.println(uploadFailed ? "HTTP upload result: FAIL" : "HTTP upload result: OK"); } else if (up.status == UPLOAD_FILE_ABORTED) { Serial.println("HTTP upload aborted"); uploadActive = false; uploadFailed = true; ledOff(); } } //============================================================= // HTTP DSP reset handler //============================================================= static void handleDspReset() { Serial.println("DSP soft reset requested"); // Stop DSP execution, brief pause, restart. Program and parameter RAM // are preserved — this restarts execution without reloading from EEPROM. uint8_t stop[2] = { 0x00, 0x00 }; uint8_t run[2] = { 0x00, 0x01 }; DSPWriter::writeRegister(dspRegister::CoreRegister, sizeof(stop), stop); delay(100); DSPWriter::writeRegister(dspRegister::CoreRegister, sizeof(run), run); Serial.println("DSP soft reset complete"); httpServer.send(200, "text/plain", "DSP soft reset complete."); } //============================================================= // HTTP DSP status handler GET /dsp_status //============================================================= static void handleDspStatus() { // Core Register (0x081C) — 2 bytes, bit 0 = Run uint8_t coreRaw[2] = {0, 0}; bool coreOk = i2cReadBlock(DSP_7BIT, dspRegister::CoreRegister, coreRaw, 2); uint16_t coreVal = ((uint16_t)coreRaw[0] << 8) | coreRaw[1]; // GPIO All Register (0x0808) — 1 byte uint8_t gpio = 0; bool gpioOk = i2cReadBlock(DSP_7BIT, dspRegister::GpioAllRegister, &gpio, 1); // ADC0–3 (0x0809–0x080C) — 1 byte each, read as a 4-byte burst uint8_t adc[4] = {0, 0, 0, 0}; bool adcOk = i2cReadBlock(DSP_7BIT, dspRegister::Adc0, adc, 4); bool allOk = coreOk && gpioOk && adcOk; char buf[200]; snprintf(buf, sizeof(buf), "{\"ok\":%s,\"running\":%s," "\"coreReg\":\"0x%04X\"," "\"gpio\":\"0x%02X\"," "\"adc\":[\"0x%02X\",\"0x%02X\",\"0x%02X\",\"0x%02X\"]}", allOk ? "true" : "false", (coreOk && (coreVal & 0x01)) ? "true" : "false", coreVal, gpio, adc[0], adc[1], adc[2], adc[3]); httpServer.send(allOk ? 200 : 500, "application/json", buf); } //============================================================= // HTTP parameter tuner GET /params GET /param POST /param //============================================================= static void handleParamsPage() { String html = FPSTR(PARAMS_HTML); html.replace("{{IP}}", WiFi.localIP().toString()); httpServer.send(200, "text/html; charset=utf-8", html); } static void handleParamGet() { if (!httpServer.hasArg("addr")) { httpServer.send(400, "text/plain", "Missing 'addr'."); return; } long addr = strtol(httpServer.arg("addr").c_str(), nullptr, 0); if (addr < 0 || addr > (long)DSP_PARAM_RAM_END) { httpServer.send(400, "text/plain", "addr must be 0x0000-0x03FF."); return; } uint8_t raw[4] = {0, 0, 0, 0}; bool ok = i2cReadBlock(DSP_7BIT, (uint16_t)addr, raw, 4); // 5.23 fixed-point → float int32_t fixed = ((int32_t)raw[0] << 24) | ((int32_t)raw[1] << 16) | ((int32_t)raw[2] << 8) | (int32_t)raw[3]; float fval = (float)fixed / 8388608.0f; char buf[128]; snprintf(buf, sizeof(buf), "{\"ok\":%s,\"addr\":%ld,\"hex\":\"0x%02X%02X%02X%02X\",\"float\":%.7f}", ok ? "true" : "false", addr, raw[0], raw[1], raw[2], raw[3], fval); httpServer.send(ok ? 200 : 500, "application/json", buf); } static void handleParamSet() { if (!httpServer.hasArg("addr") || !httpServer.hasArg("value")) { httpServer.send(400, "text/plain", "Missing 'addr' or 'value'."); return; } long addr = strtol(httpServer.arg("addr").c_str(), nullptr, 0); if (addr < 0 || addr > (long)DSP_PARAM_RAM_END) { httpServer.send(400, "text/plain", "addr must be 0x0000-0x03FF."); return; } String mode = httpServer.arg("mode"); DSPWriter dspWriter; if (mode == "hex") { String hexStr = httpServer.arg("value"); if (hexStr.startsWith("0x") || hexStr.startsWith("0X")) hexStr = hexStr.substring(2); hexStr.replace(" ", ""); hexStr.replace("_", ""); if (hexStr.length() != 8) { httpServer.send(400, "text/plain", "Hex value must be exactly 8 hex characters."); return; } uint32_t v = strtoul(hexStr.c_str(), nullptr, 16); uint8_t sl[5] = { 0x00, (uint8_t)((v >> 24) & 0xFF), (uint8_t)((v >> 16) & 0xFF), (uint8_t)((v >> 8) & 0xFF), (uint8_t)( v & 0xFF) }; dspWriter.safeload_writeRegister((uint16_t)addr, sl, true); Serial.printf("Param write (hex) addr=0x%04lX val=0x%08X\n", addr, v); } else { // Float mode — safeload_writeRegister handles 5.23 conversion internally float fval = httpServer.arg("value").toFloat(); dspWriter.safeload_writeRegister((uint16_t)addr, fval, true); Serial.printf("Param write (float) addr=0x%04lX val=%.7f\n", addr, fval); } httpServer.send(200, "text/plain", "OK"); } //============================================================= // HTTP WiFi reset handler POST /wifi_reset //============================================================= static void handleWifiReset() { Serial.println("WiFi credentials cleared — rebooting to AP setup mode"); clearWifiCreds(); httpServer.send(200, "text/plain", "Credentials cleared. Rebooting into setup mode.\n" "Connect to 'ModulosDSP-Setup' and open 192.168.4.1."); delay(500); ESP.restart(); } //============================================================= // HTTP GPIO handlers GET /gpio POST /gpio //============================================================= static void handleGpioGet() { uint8_t gpio = 0; bool gpioOk = i2cReadBlock(DSP_7BIT, dspRegister::GpioAllRegister, &gpio, 1); uint8_t mpcfg[2] = {0, 0}; bool cfgOk = i2cReadBlock(DSP_7BIT, dspRegister::MpCfg0, mpcfg, 2); bool ok = gpioOk && cfgOk; char buf[100]; snprintf(buf, sizeof(buf), "{\"ok\":%s,\"gpio\":%u,\"mpcfg0\":%u,\"mpcfg1\":%u}", ok ? "true" : "false", gpio, mpcfg[0], mpcfg[1]); httpServer.send(ok ? 200 : 500, "application/json", buf); } static void handleGpioSet() { if (!httpServer.hasArg("value")) { httpServer.send(400, "text/plain", "Missing 'value' parameter."); return; } long val = httpServer.arg("value").toInt(); if (val < 0 || val > 255) { httpServer.send(400, "text/plain", "value must be 0-255."); return; } uint8_t b = (uint8_t)val; DSPWriter::writeRegister(dspRegister::GpioAllRegister, 1, &b); Serial.printf("GPIO set: 0x%02X\n", b); httpServer.send(200, "text/plain", "OK"); } //============================================================= // HTTP OTA handlers //============================================================= static void handleOtaPage() { String html = FPSTR(OTA_HTML); html.replace("{{IP}}", WiFi.localIP().toString()); httpServer.send(200, "text/html; charset=utf-8", html); } static void handleOtaDone() { if (Update.hasError()) { String err = "OTA failed: " + String(Update.errorString()); Serial.println(err); httpServer.send(500, "text/plain", err); } else { httpServer.send(200, "text/plain", "Firmware updated — rebooting now."); Serial.println("OTA success — rebooting"); delay(500); ESP.restart(); } } static void handleOtaStream() { HTTPUpload& up = httpServer.upload(); if (up.status == UPLOAD_FILE_START) { Serial.printf("OTA start: %s\n", up.filename.c_str()); ledCyan(); if (!Update.begin(UPDATE_SIZE_UNKNOWN)) { Serial.print("OTA begin failed: "); Update.printError(Serial); } } else if (up.status == UPLOAD_FILE_WRITE) { if (Update.write(up.buf, up.currentSize) != up.currentSize) { Serial.print("OTA write failed: "); Update.printError(Serial); ledErrorFlash(); } } else if (up.status == UPLOAD_FILE_END) { if (Update.end(true)) { Serial.printf("OTA end: %u bytes written\n", up.totalSize); } else { Serial.print("OTA end failed: "); Update.printError(Serial); ledErrorFlash(); } ledOff(); } else if (up.status == UPLOAD_FILE_ABORTED) { Update.abort(); Serial.println("OTA aborted"); ledOff(); } } //============================================================= // Setup //============================================================= void setup() { Wire.begin(I2C_SDA, I2C_SCL); Wire.setClock(400000); statusLed.begin(); statusLed.setBrightness(NEOPIXEL_BRIGHT); statusLed.show(); s_lcdOk = (lcd.begin(20, 4) == 0); if (s_lcdOk) { lcd.display(); lcd.backlight(); lcd.setCursor(2, 0); lcd.print("Modulos AudioDSP"); delay(1000); lcd.setCursor(5, 1); lcd.print("Booting..."); delay(1000); } else { Serial.println("LCD not found - continuing without display"); } Serial.begin(115200); delay(1500); Serial.println(); Serial.println("Booting..."); Serial.printf("Reset reason: %d\n", (int)esp_reset_reason()); if (!loadWifiCreds()) { // NVS empty — fall back to compile-time defaults so an unattended // device can reach the network without needing the config portal. strlcpy(s_ssid, DEFAULT_SSID, sizeof(s_ssid)); strlcpy(s_pass, DEFAULT_PASS, sizeof(s_pass)); Serial.println("No NVS credentials — using defaults"); } Serial.printf("Connecting to %s", s_ssid); WiFi.mode(WIFI_STA); WiFi.setAutoReconnect(true); WiFi.begin(s_ssid, s_pass); uint32_t t0 = millis(); while (WiFi.status() != WL_CONNECTED && millis() - t0 < 15000) { delay(500); Serial.print("."); } Serial.println(); if (WiFi.status() != WL_CONNECTED) { Serial.println("WiFi connect failed — entering setup mode"); startConfigAP(); // never returns } if (MDNS.begin(hostname)) { MDNS.addService("http", "tcp", 80); Serial.printf("mDNS: http://%s.local/\n", hostname); } else { Serial.println("mDNS start failed"); } if (!LittleFS.begin(false)) { Serial.println("LittleFS mount failed, formatting..."); if (!LittleFS.begin(true)) { Serial.println("LittleFS mount failed even after format"); return; } } Serial.println("LittleFS mounted OK"); File root = LittleFS.open("/"); File f = root.openNextFile(); while (f) { Serial.print("LittleFS: "); Serial.println(f.name()); f = root.openNextFile(); } if (s_lcdOk) { lcd.setCursor(3, 2); lcd.print("File System OK"); delay(1000); } tcpServer.begin(); httpServer.on("/", HTTP_GET, handleRoot); httpServer.on("/status", HTTP_GET, handleStatus); httpServer.on("/upload", HTTP_POST, handleUploadDone, handleUploadStream); httpServer.on("/ota", HTTP_GET, handleOtaPage); httpServer.on("/ota_do", HTTP_POST, handleOtaDone, handleOtaStream); httpServer.on("/dsp_reset", HTTP_POST, handleDspReset); httpServer.on("/dsp_status", HTTP_GET, handleDspStatus); httpServer.on("/gpio", HTTP_GET, handleGpioGet); httpServer.on("/gpio", HTTP_POST, handleGpioSet); httpServer.on("/wifi_reset", HTTP_POST, handleWifiReset); httpServer.on("/params", HTTP_GET, handleParamsPage); httpServer.on("/param", HTTP_GET, handleParamGet); httpServer.on("/param", HTTP_POST, handleParamSet); httpServer.onNotFound([]() { String uri = httpServer.uri(); if (streamFromFS(uri)) return; Serial.print("HTTP 404: "); Serial.println(uri); httpServer.send(404, "text/plain", "Not found: " + uri); }); httpServer.begin(); if (s_lcdOk) { lcd.setCursor(4, 3); lcd.print("System Ready"); delay(1000); lcd.clear(); lcd.setCursor(2, 0); lcd.print("Modulos AudioDSP"); lcd.setCursor(3, 1); lcd.print(version); delay(500); } printWifiInfo(); Serial.print("HTTP uploader: http://"); Serial.print(WiFi.localIP()); Serial.println("/"); } //============================================================= // TCP bridge //============================================================= //============================================================= // TCP bridge //============================================================= static void handleTcpBridgeClient(WiFiClient& client) { Serial.println("TCP new connection"); DSPWriter::resetSafeload(); int writeIndex = 0; // next free slot in dataBuffer int readIndex = 0; // start of current unprocessed command int receivedByteCount = 0; // total bytes written into dataBuffer int currentState = STATE_START; uint32_t lastActivityMs = millis(); // idle watchdog while (client.connected()) { httpServer.handleClient(); delay(0); // ------------------------------------------------------------------ // STEP 1: Always drain the TCP stack into dataBuffer. // Do this unconditionally every loop iteration — this is what keeps // the TCP receive window open. If we only drain when we feel like it, // the window goes to zero and SigmaStudio stops sending (ZeroWindow). // ------------------------------------------------------------------ int avail = client.available(); if (avail > 0) { int space = (int)sizeof(dataBuffer) - writeIndex; if (avail > space) { Serial.println("TCP RX overflow"); ledErrorFlash(); client.stop(); return; } int got = client.read(&dataBuffer[writeIndex], avail); if (got > 0) { writeIndex += got; receivedByteCount += got; lastActivityMs = millis(); } } // ------------------------------------------------------------------ // STEP 2: Process whatever is in the buffer. // This is driven purely by buffer contents, not by client.available(). // We loop here processing commands until we run out of buffered data. // ------------------------------------------------------------------ bool processedSomething = true; while (processedSomething && client.connected()) { processedSomething = false; // --- STATE_START: identify opcode --- if (currentState == STATE_START) { if (receivedByteCount <= readIndex) { // Buffer empty — reset for next command writeIndex = readIndex = receivedByteCount = 0; ledOff(); break; // nothing to process, go back to receive loop } #if TCP_DEBUG printHex("TCP RX: ", &dataBuffer[readIndex], (uint16_t)(receivedByteCount - readIndex)); #endif uint8_t op = dataBuffer[readIndex]; if (op == CMD_WRITE) { currentState = STATE_WRITE_CMD; processedSomething = true; } else if (op == CMD_READ) { currentState = STATE_READ_CMD; processedSomething = true; } else { Serial.printf("TCP invalid opcode: 0x%02X\n", op); ledErrorFlash(); client.stop(); return; } } // --- STATE_WRITE_CMD --- if (currentState == STATE_WRITE_CMD) { // Need full header first if (receivedByteCount < (readIndex + WRITE_HDR_LEN)) break; writeHeader.safeload = dataBuffer[readIndex + 1]; writeHeader.placement = dataBuffer[readIndex + 2]; writeHeader.totalLen = (uint16_t)((dataBuffer[readIndex + 3] << 8) | dataBuffer[readIndex + 4]); writeHeader.chipAddr = dataBuffer[readIndex + 5]; writeHeader.dataLen = (uint16_t)((dataBuffer[readIndex + 6] << 8) | dataBuffer[readIndex + 7]); writeHeader.address = (uint16_t)((dataBuffer[readIndex + 8] << 8) | dataBuffer[readIndex + 9]); if (writeHeader.totalLen != WRITE_HDR_LEN + writeHeader.dataLen) { Serial.printf("TCP WRITE bad totalLen: got %u expected %u\n", writeHeader.totalLen, WRITE_HDR_LEN + writeHeader.dataLen); ledErrorFlash(); client.stop(); return; } // Need full payload — if not here yet, break back to receive loop if (receivedByteCount < (readIndex + WRITE_HDR_LEN + (int)writeHeader.dataLen)) { Serial.printf("TCP WRITE buffering: have %d need %d bytes\n", receivedByteCount - readIndex, WRITE_HDR_LEN + (int)writeHeader.dataLen); break; } readIndex += WRITE_HDR_LEN; uint8_t target7 = chipAddrTo7bit(writeHeader.chipAddr); if (target7 == EEPROM_7BIT) { ledYellow(); bool ok = eepromWriteBlock(writeHeader.address, &dataBuffer[readIndex], writeHeader.dataLen); readIndex += writeHeader.dataLen; sendWriteAck(client, ok); if (!ok) { Serial.println("TCP EEPROM write failed"); ledErrorFlash(); } else ledOff(); currentState = STATE_START; processedSomething = true; continue; } if (target7 != DSP_7BIT) { Serial.printf("TCP unknown chipAddr: 0x%02X\n", writeHeader.chipAddr); ledErrorFlash(); client.stop(); return; } ledGreen(); uint8_t registerSize = registerSizeForAddress(writeHeader.address, writeHeader.dataLen); uint16_t regAddress = writeHeader.address; Serial.printf("TCP WRITE addr=0x%04X len=%u regSz=%u safeload=%u\n", writeHeader.address, writeHeader.dataLen, registerSize, writeHeader.safeload); bool writeOk = true; if (writeHeader.safeload == 1) { if (writeHeader.dataLen % 4 != 0) { Serial.printf("TCP safeload dataLen %u not multiple of 4\n", writeHeader.dataLen); ledErrorFlash(); client.stop(); return; } int writeCount = writeHeader.dataLen / 4; int slri = readIndex; DSPWriter dspWriter; while (writeCount > 0) { uint8_t da[5] = { 0x00, dataBuffer[slri], dataBuffer[slri+1], dataBuffer[slri+2], dataBuffer[slri+3] }; dspWriter.safeload_writeRegister(regAddress, da, writeCount == 1); regAddress++; slri += 4; writeCount--; delay(0); } } else { writeOk = DSPWriter::writeRegisterBlock(regAddress, writeHeader.dataLen, &dataBuffer[readIndex], registerSize); if (!writeOk) Serial.println("TCP DSP block write failed"); } readIndex += writeHeader.dataLen; if (!writeOk) ledErrorFlash(); else ledOff(); sendWriteAck(client, writeOk); currentState = STATE_START; processedSomething = true; continue; } // --- STATE_READ_CMD --- if (currentState == STATE_READ_CMD) { if (receivedByteCount < (readIndex + READ_HDR_LEN)) break; readHeader.totalLen = (uint16_t)((dataBuffer[readIndex + 1] << 8) | dataBuffer[readIndex + 2]); readHeader.chipAddr = dataBuffer[readIndex + 3]; readHeader.dataLen = (uint16_t)((dataBuffer[readIndex + 4] << 8) | dataBuffer[readIndex + 5]); readHeader.address = (uint16_t)((dataBuffer[readIndex + 6] << 8) | dataBuffer[readIndex + 7]); readIndex += READ_HDR_LEN; uint8_t target7 = chipAddrTo7bit(readHeader.chipAddr); Serial.printf("TCP READ chip=0x%02X addr=0x%04X len=%u\n", readHeader.chipAddr, readHeader.address, readHeader.dataLen); if (readHeader.dataLen > 4096) { readHeader.dataLen = 4096; } static uint8_t readOut[4096]; bool ok = false; ledBlue(); if (target7 == EEPROM_7BIT) ok = i2cReadBlock(EEPROM_7BIT, readHeader.address, readOut, readHeader.dataLen); else if (target7 == DSP_7BIT) ok = i2cReadBlock(DSP_7BIT, readHeader.address, readOut, readHeader.dataLen); else { Serial.printf("TCP unknown chipAddr (READ): 0x%02X\n", readHeader.chipAddr); } if (!ok) Serial.println("TCP READ I2C failed - sending zeros"); if (!sendReadResponse(client, ok ? readOut : nullptr, readHeader.dataLen, ok)) { Serial.println("TCP READ send failed"); ledErrorFlash(); client.stop(); return; } ledOff(); currentState = STATE_START; processedSomething = true; continue; } } // end process loop // Idle path — no data waiting, no command in progress. if (currentState == STATE_START && receivedByteCount == readIndex) { if (!client.available()) { if (millis() - lastActivityMs > TCP_IDLE_TIMEOUT_MS) { Serial.println("TCP idle timeout — closing session"); ledErrorFlash(); break; } httpServer.handleClient(); delay(10); } } } // end main while loop DSPWriter::resetSafeload(); // flush any mid-session safeload before disconnect client.stop(); Serial.println("TCP disconnected"); ledOff(); } //============================================================= // WiFi watchdog //============================================================= static uint32_t s_wifiLastCheck = 0; static bool s_wifiLost = false; static void maintainWifi() { if (millis() - s_wifiLastCheck < 5000) return; s_wifiLastCheck = millis(); if (WiFi.status() != WL_CONNECTED) { if (!s_wifiLost) { Serial.println("WiFi lost"); if (s_lcdOk) { lcd.setCursor(0, 3); lcd.print("WiFi lost... "); } s_wifiLost = true; } WiFi.reconnect(); } else if (s_wifiLost) { Serial.print("WiFi reconnected, IP: "); Serial.println(WiFi.localIP()); MDNS.begin(hostname); MDNS.addService("http", "tcp", 80); tcpServer.begin(); // re-register listening socket with recovered stack printWifiInfo(); // update LCD with current IP s_wifiLost = false; } } //============================================================= // Loop //============================================================= void loop() { maintainWifi(); httpServer.handleClient(); if (uploadActive) { delay(1); return; } WiFiClient client = tcpServer.available(); if (client) handleTcpBridgeClient(client); delay(1); }