ModulosDSP_101.ino 37 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054
  1. // Modulos ADAU DSP WiFi + EEPROM (HTTP uploader + Sigma TCP bridge)
  2. // Build: 260304_13 (YYMMDD_rev)
  3. #include <WiFi.h>
  4. #include <Wire.h>
  5. #include <WebServer.h>
  6. #include <Preferences.h>
  7. #include "DSPWriter.h"
  8. #include <hd44780.h>
  9. #include <hd44780ioClass/hd44780_I2Cexp.h>
  10. #include <Adafruit_NeoPixel.h>
  11. #include "FS.h"
  12. #include <LittleFS.h>
  13. #include <Update.h>
  14. #include <ESPmDNS.h>
  15. //=============================================================
  16. // WiFi / UI
  17. //=============================================================
  18. const char* hostname = "modulos-dsp";
  19. const char* version = "VER: 260304_13";
  20. // Compile-time default credentials — used when NVS has no saved credentials.
  21. // Change these before flashing. If NVS credentials are saved (via the config
  22. // portal) they take priority over these on subsequent boots.
  23. static const char DEFAULT_SSID[] = "alfred";
  24. static const char DEFAULT_PASS[] = "alfred16";
  25. // Runtime WiFi credentials — populated from NVS or defaults at boot
  26. static char s_ssid[64] = "";
  27. static char s_pass[64] = "";
  28. #define I2C_SDA 13
  29. #define I2C_SCL 12
  30. WiFiServer tcpServer(8086);
  31. WebServer httpServer(80);
  32. #include "index_html.h"
  33. #include "ota_html.h"
  34. #include "ap_html.h"
  35. #include "params_html.h"
  36. hd44780_I2Cexp lcd;
  37. static bool s_lcdOk = false;
  38. //=============================================================
  39. // NeoPixel status LED (Waveshare ESP32-S3 Zero, GPIO 21)
  40. //=============================================================
  41. #define NEOPIXEL_PIN 21
  42. #define NEOPIXEL_COUNT 1
  43. #define NEOPIXEL_BRIGHT 40
  44. Adafruit_NeoPixel statusLed(NEOPIXEL_COUNT, NEOPIXEL_PIN, NEO_GRB + NEO_KHZ800);
  45. static void ledSet(uint8_t r, uint8_t g, uint8_t b)
  46. {
  47. statusLed.setPixelColor(0, statusLed.Color(r, g, b));
  48. statusLed.show();
  49. }
  50. static void ledOff() { ledSet(0, 0, 0); }
  51. static void ledCyan() { ledSet(0, NEOPIXEL_BRIGHT/2, NEOPIXEL_BRIGHT/2); }
  52. static void ledGreen() { ledSet(0, NEOPIXEL_BRIGHT, 0); }
  53. static void ledBlue() { ledSet(0, 0, NEOPIXEL_BRIGHT); }
  54. static void ledYellow() { ledSet(NEOPIXEL_BRIGHT, NEOPIXEL_BRIGHT, 0); }
  55. static void ledMagenta() { ledSet(NEOPIXEL_BRIGHT, 0, NEOPIXEL_BRIGHT); }
  56. static void ledErrorFlash()
  57. {
  58. for (int i = 0; i < 2; i++) {
  59. ledSet(NEOPIXEL_BRIGHT, 0, 0); delay(80);
  60. ledOff(); delay(80);
  61. }
  62. }
  63. //=============================================================
  64. // TCP protocol buffer
  65. //=============================================================
  66. static uint8_t dataBuffer[50 * 1024];
  67. #define STATE_START 0
  68. #define STATE_READ_CMD 1
  69. #define STATE_WRITE_CMD 2
  70. #define CMD_WRITE 0x09
  71. #define CMD_READ 0x0A
  72. #define TCP_IDLE_TIMEOUT_MS 30000 // drop session if silent for 30 s
  73. #define TCP_DEBUG 0 // set to 1 to log raw RX bytes to Serial
  74. constexpr int WRITE_HDR_LEN = 10;
  75. constexpr int READ_HDR_LEN = 8;
  76. struct adauWriteHeader {
  77. uint8_t command;
  78. uint8_t safeload;
  79. uint8_t placement;
  80. uint16_t totalLen;
  81. uint8_t chipAddr;
  82. uint16_t dataLen;
  83. uint16_t address;
  84. };
  85. struct adauReadHeader {
  86. uint8_t command;
  87. uint16_t totalLen;
  88. uint8_t chipAddr;
  89. uint16_t dataLen;
  90. uint16_t address;
  91. };
  92. static adauWriteHeader writeHeader;
  93. static adauReadHeader readHeader;
  94. static constexpr uint8_t DSP_7BIT = DSP_I2C_ADDRESS; // 0x34
  95. static constexpr uint8_t EEPROM_7BIT = EEPROM_I2C_ADDRESS; // 0x50
  96. //=============================================================
  97. // 24C256 EEPROM
  98. //=============================================================
  99. static constexpr uint32_t EEPROM_SIZE_BYTES = 32768;
  100. static constexpr uint16_t EEPROM_PAGE_SIZE = 64;
  101. static constexpr uint8_t I2C_MAX_DATA_PER_TX = 28;
  102. //=============================================================
  103. // CRC32
  104. //=============================================================
  105. static uint32_t crc32_update(uint32_t crc, const uint8_t* data, size_t len)
  106. {
  107. crc = ~crc;
  108. for (size_t i = 0; i < len; i++) {
  109. crc ^= data[i];
  110. for (int b = 0; b < 8; b++) {
  111. uint32_t mask = -(crc & 1u);
  112. crc = (crc >> 1) ^ (0xEDB88320u & mask);
  113. }
  114. }
  115. return ~crc;
  116. }
  117. //=============================================================
  118. // Helpers
  119. //=============================================================
  120. static uint8_t chipAddrTo7bit(uint8_t chipAddr)
  121. {
  122. switch (chipAddr) {
  123. case 0x01: return 0x34; // chip index 1 = DSP
  124. case 0x02: return 0x50; // chip index 2 = EEPROM
  125. case 0x68: return 0x34;
  126. case 0xA0: return 0x50;
  127. case 0x34: return 0x34;
  128. case 0x50: return 0x50;
  129. default:
  130. if (chipAddr > 0x7F) return (uint8_t)(chipAddr >> 1);
  131. return chipAddr;
  132. }
  133. }
  134. static uint8_t registerSizeForAddress(uint16_t address, uint16_t dataLen)
  135. {
  136. if (address == dspRegister::CoreRegister) {
  137. if (dataLen == 2) return CORE_REGISTER_R0_REGSIZE;
  138. if (dataLen == 24) return HARDWARE_CONF_REGSIZE;
  139. return CORE_REGISTER_R0_REGSIZE;
  140. }
  141. if (address >= DSP_PROG_RAM_START && address <= DSP_PROG_RAM_END) return PROGRAM_REGSIZE;
  142. if (address < DSP_PROG_RAM_START) return PARAMETER_REGSIZE;
  143. return HARDWARE_CONF_REGSIZE;
  144. }
  145. static bool i2cAckPoll(uint8_t addr7, uint32_t timeoutMs = 80)
  146. {
  147. uint32_t start = millis();
  148. while ((millis() - start) < timeoutMs) {
  149. Wire.beginTransmission(addr7);
  150. if (Wire.endTransmission() == 0) return true;
  151. delay(1);
  152. }
  153. return false;
  154. }
  155. static bool eepromWritePageChunk(uint16_t memAddr, const uint8_t* data, uint16_t len)
  156. {
  157. Wire.beginTransmission(EEPROM_7BIT);
  158. Wire.write((uint8_t)(memAddr >> 8));
  159. Wire.write((uint8_t)(memAddr & 0xFF));
  160. for (uint16_t i = 0; i < len; i++) Wire.write(data[i]);
  161. if (Wire.endTransmission() != 0) return false;
  162. return i2cAckPoll(EEPROM_7BIT, 120);
  163. }
  164. static bool eepromWriteBlock(uint16_t memAddr, const uint8_t* data, uint16_t len)
  165. {
  166. while (len) {
  167. uint16_t pageOff = memAddr % EEPROM_PAGE_SIZE;
  168. uint16_t spaceInPage = EEPROM_PAGE_SIZE - pageOff;
  169. uint16_t chunk = len;
  170. if (chunk > spaceInPage) chunk = spaceInPage;
  171. if (chunk > I2C_MAX_DATA_PER_TX) chunk = I2C_MAX_DATA_PER_TX;
  172. if (!eepromWritePageChunk(memAddr, data, chunk)) return false;
  173. memAddr += chunk; data += chunk; len -= chunk;
  174. delay(0);
  175. }
  176. return true;
  177. }
  178. static bool i2cReadBlock(uint8_t addr7, uint16_t memAddr, uint8_t* out, uint16_t len)
  179. {
  180. Wire.beginTransmission(addr7);
  181. Wire.write((uint8_t)(memAddr >> 8));
  182. Wire.write((uint8_t)(memAddr & 0xFF));
  183. if (Wire.endTransmission(false) != 0) return false;
  184. uint16_t got = 0;
  185. while (got < len) {
  186. uint8_t ask = (len - got) > 32 ? 32 : (len - got);
  187. if (Wire.requestFrom((int)addr7, (int)ask) != ask) return false;
  188. for (uint8_t i = 0; i < ask; i++) out[got++] = Wire.read();
  189. }
  190. return true;
  191. }
  192. // SigmaTCP read response: 0x0B, totalLen_hi, totalLen_lo, status, dataLen_hi, dataLen_lo, [payload]
  193. static bool sendReadResponse(WiFiClient& client, const uint8_t* data, uint16_t dataLen, bool ok)
  194. {
  195. uint16_t totalLen = 6 + dataLen;
  196. uint8_t hdr[6];
  197. hdr[0] = 0x0B;
  198. hdr[1] = (uint8_t)(totalLen >> 8);
  199. hdr[2] = (uint8_t)(totalLen & 0xFF);
  200. hdr[3] = ok ? 0x00 : 0x01;
  201. hdr[4] = (uint8_t)(dataLen >> 8);
  202. hdr[5] = (uint8_t)(dataLen & 0xFF);
  203. if (client.write(hdr, sizeof(hdr)) != sizeof(hdr)) return false;
  204. if (dataLen > 0) {
  205. if (ok && data) {
  206. if (client.write(data, dataLen) != dataLen) return false;
  207. } else {
  208. // send zeros so SigmaStudio doesn't stall on a failed read
  209. static uint8_t zeros[256];
  210. uint16_t rem = dataLen;
  211. while (rem) {
  212. uint16_t chunk = rem > sizeof(zeros) ? sizeof(zeros) : rem;
  213. if (client.write(zeros, chunk) != chunk) return false;
  214. rem -= chunk;
  215. }
  216. }
  217. }
  218. return true;
  219. }
  220. // SigmaTCP write ack: 0x09, 0x00, 0x04, status
  221. static bool sendWriteAck(WiFiClient& client, bool ok)
  222. {
  223. uint8_t ack[4] = { 0x09, 0x00, 0x04, ok ? (uint8_t)0x00 : (uint8_t)0x01 };
  224. return client.write(ack, sizeof(ack)) == sizeof(ack);
  225. }
  226. static void printHex(const char* label, const uint8_t* buf, uint16_t len, uint16_t maxPrint = 24)
  227. {
  228. Serial.print(label);
  229. uint16_t n = len < maxPrint ? len : maxPrint;
  230. for (uint16_t i = 0; i < n; i++) {
  231. if (buf[i] < 0x10) Serial.print("0");
  232. Serial.print(buf[i], HEX);
  233. Serial.print(" ");
  234. }
  235. if (len > maxPrint) Serial.print("...");
  236. Serial.println();
  237. }
  238. //=============================================================
  239. // NVS credential helpers
  240. //=============================================================
  241. static Preferences s_prefs;
  242. static bool loadWifiCreds()
  243. {
  244. s_prefs.begin("wifi", true);
  245. String ssid = s_prefs.getString("ssid", "");
  246. String pass = s_prefs.getString("pass", "");
  247. s_prefs.end();
  248. if (ssid.length() == 0) return false;
  249. ssid.toCharArray(s_ssid, sizeof(s_ssid));
  250. pass.toCharArray(s_pass, sizeof(s_pass));
  251. return true;
  252. }
  253. static void saveWifiCreds(const String& ssid, const String& pass)
  254. {
  255. s_prefs.begin("wifi", false);
  256. s_prefs.putString("ssid", ssid);
  257. s_prefs.putString("pass", pass);
  258. s_prefs.end();
  259. }
  260. static void clearWifiCreds()
  261. {
  262. s_prefs.begin("wifi", false);
  263. s_prefs.clear();
  264. s_prefs.end();
  265. }
  266. //=============================================================
  267. // SoftAP config portal — runs when no credentials are stored
  268. // or when a previous connection attempt failed.
  269. // Serves a simple HTML form; never returns.
  270. //=============================================================
  271. static void startConfigAP()
  272. {
  273. Serial.println("Starting config AP: ModulosDSP-Setup");
  274. WiFi.mode(WIFI_AP);
  275. WiFi.softAP("ModulosDSP-Setup");
  276. delay(100);
  277. IPAddress apIP = WiFi.softAPIP();
  278. Serial.print("AP IP: "); Serial.println(apIP);
  279. if (s_lcdOk) {
  280. lcd.clear();
  281. lcd.setCursor(0, 0); lcd.print("WiFi Setup Mode");
  282. lcd.setCursor(0, 1); lcd.print("ModulosDSP-Setup");
  283. lcd.setCursor(0, 2); lcd.print(apIP);
  284. }
  285. httpServer.on("/", HTTP_GET, []() {
  286. httpServer.send_P(200, "text/html", AP_HTML);
  287. });
  288. httpServer.on("/save", HTTP_POST, []() {
  289. if (!httpServer.hasArg("ssid") || httpServer.arg("ssid").length() == 0) {
  290. httpServer.send(400, "text/plain", "SSID is required.");
  291. return;
  292. }
  293. String newSsid = httpServer.arg("ssid");
  294. String newPass = httpServer.arg("pass");
  295. saveWifiCreds(newSsid, newPass);
  296. Serial.printf("Credentials saved for SSID: %s\n", newSsid.c_str());
  297. httpServer.send(200, "text/html",
  298. "<html><body style='font-family:sans-serif;max-width:380px;margin:60px auto;padding:0 20px'>"
  299. "<h3>Saved!</h3><p>Connecting to <strong>" + newSsid +
  300. "</strong>&hellip; The device will reboot now.</p></body></html>");
  301. delay(1000);
  302. ESP.restart();
  303. });
  304. httpServer.begin();
  305. Serial.println("Config portal active — waiting for credentials");
  306. while (true) {
  307. httpServer.handleClient();
  308. delay(1);
  309. }
  310. }
  311. static void printWifiInfo()
  312. {
  313. Serial.println();
  314. Serial.println("WiFi connected.");
  315. Serial.print("WiFi IP: "); Serial.println(WiFi.localIP());
  316. Serial.printf("Hostname: http://%s.local/\n", hostname);
  317. Serial.print("MAC: "); Serial.println(WiFi.macAddress());
  318. Serial.println("Modulos AudioDSP");
  319. Serial.println(version);
  320. if (s_lcdOk) {
  321. lcd.setCursor(4, 3);
  322. lcd.print(WiFi.localIP());
  323. }
  324. }
  325. //=============================================================
  326. // HTTP EEPROM uploader state
  327. //=============================================================
  328. static volatile bool uploadActive = false;
  329. static volatile bool uploadVerify = false;
  330. static volatile bool uploadFailed = false;
  331. static volatile uint32_t uploadBytes = 0;
  332. static volatile uint32_t uploadCrc = 0;
  333. //=============================================================
  334. // HTTP handlers
  335. //=============================================================
  336. static String contentTypeFor(const String& path) {
  337. if (path.endsWith(".html")) return "text/html";
  338. if (path.endsWith(".css")) return "text/css";
  339. if (path.endsWith(".js")) return "application/javascript";
  340. if (path.endsWith(".png")) return "image/png";
  341. if (path.endsWith(".jpg") || path.endsWith(".jpeg")) return "image/jpeg";
  342. if (path.endsWith(".webp")) return "image/webp";
  343. if (path.endsWith(".svg")) return "image/svg+xml";
  344. if (path.endsWith(".ico")) return "image/x-icon";
  345. if (path.endsWith(".woff")) return "font/woff";
  346. if (path.endsWith(".woff2"))return "font/woff2";
  347. return "application/octet-stream";
  348. }
  349. static bool streamFromFS(String path) {
  350. if (!LittleFS.exists(path)) {
  351. if (path.startsWith("/")) {
  352. String alt = path.substring(1);
  353. if (LittleFS.exists(alt)) path = alt; else return false;
  354. } else {
  355. String alt = "/" + path;
  356. if (LittleFS.exists(alt)) path = alt; else return false;
  357. }
  358. }
  359. File f = LittleFS.open(path, "r");
  360. if (!f) return false;
  361. httpServer.streamFile(f, contentTypeFor(path));
  362. f.close();
  363. return true;
  364. }
  365. static void handleRoot() {
  366. String html = FPSTR(INDEX_HTML);
  367. html.replace("{{IP}}", WiFi.localIP().toString());
  368. httpServer.send(200, "text/html; charset=utf-8", html);
  369. }
  370. static void handleStatus() {
  371. String s;
  372. s += "uploadActive="; s += (uploadActive ? "1" : "0"); s += "\n";
  373. s += "uploadFailed="; s += (uploadFailed ? "1" : "0"); s += "\n";
  374. s += "uploadBytes="; s += String((uint32_t)uploadBytes); s += "\n";
  375. s += "uploadCRC32=0x"; s += String((uint32_t)uploadCrc, HEX); s += "\n";
  376. httpServer.send(200, "text/plain", s);
  377. }
  378. static void handleUploadDone() {
  379. if (uploadFailed) {
  380. httpServer.send(500, "text/plain", "Upload failed.\nCheck Serial log.\n");
  381. return;
  382. }
  383. String msg = "OK\nBytes written: " + String((uint32_t)uploadBytes) +
  384. "\nCRC32: 0x" + String((uint32_t)uploadCrc, HEX) + "\n";
  385. httpServer.send(200, "text/plain", msg);
  386. }
  387. static void handleUploadStream() {
  388. HTTPUpload& up = httpServer.upload();
  389. if (up.status == UPLOAD_FILE_START) {
  390. uploadActive = true; uploadFailed = false; uploadBytes = 0; uploadCrc = 0;
  391. uploadVerify = httpServer.hasArg("verify");
  392. Serial.println(); Serial.print("HTTP upload start: "); Serial.println(up.filename);
  393. Serial.print("Verify: "); Serial.println(uploadVerify ? "yes" : "no");
  394. Wire.beginTransmission(EEPROM_7BIT);
  395. Serial.print("EEPROM probe err: "); Serial.println(Wire.endTransmission());
  396. }
  397. else if (up.status == UPLOAD_FILE_WRITE) {
  398. if (uploadFailed) return;
  399. if ((uploadBytes + up.currentSize) > EEPROM_SIZE_BYTES) {
  400. Serial.println("Upload too large for 24C256"); uploadFailed = true; return;
  401. }
  402. if (!eepromWriteBlock((uint16_t)uploadBytes, up.buf, (uint16_t)up.currentSize)) {
  403. Serial.println("EEPROM write failed"); uploadFailed = true; return;
  404. }
  405. uploadCrc = crc32_update(uploadCrc, up.buf, up.currentSize);
  406. uploadBytes += up.currentSize;
  407. ledMagenta();
  408. }
  409. else if (up.status == UPLOAD_FILE_END) {
  410. Serial.print("HTTP upload end, bytes="); Serial.println((uint32_t)uploadBytes);
  411. if (uploadVerify && !uploadFailed) {
  412. Serial.println("Verify start (CRC32)...");
  413. uint32_t crc = 0;
  414. static uint8_t tmp[256];
  415. uint32_t remaining = uploadBytes; uint16_t addr = 0;
  416. while (remaining) {
  417. uint16_t n = remaining > sizeof(tmp) ? sizeof(tmp) : (uint16_t)remaining;
  418. if (!i2cReadBlock(EEPROM_7BIT, addr, tmp, n)) { Serial.println("EEPROM read failed"); uploadFailed = true; break; }
  419. crc = crc32_update(crc, tmp, n);
  420. addr += n; remaining -= n; delay(0);
  421. }
  422. Serial.print("Verify CRC32: 0x"); Serial.println(crc, HEX);
  423. if (!uploadFailed && crc != uploadCrc) { Serial.println("CRC mismatch"); uploadFailed = true; }
  424. }
  425. uploadActive = false; ledOff();
  426. Serial.println(uploadFailed ? "HTTP upload result: FAIL" : "HTTP upload result: OK");
  427. }
  428. else if (up.status == UPLOAD_FILE_ABORTED) {
  429. Serial.println("HTTP upload aborted"); uploadActive = false; uploadFailed = true; ledOff();
  430. }
  431. }
  432. //=============================================================
  433. // HTTP DSP reset handler
  434. //=============================================================
  435. static void handleDspReset() {
  436. Serial.println("DSP soft reset requested");
  437. // Stop DSP execution, brief pause, restart. Program and parameter RAM
  438. // are preserved — this restarts execution without reloading from EEPROM.
  439. uint8_t stop[2] = { 0x00, 0x00 };
  440. uint8_t run[2] = { 0x00, 0x01 };
  441. DSPWriter::writeRegister(dspRegister::CoreRegister, sizeof(stop), stop);
  442. delay(100);
  443. DSPWriter::writeRegister(dspRegister::CoreRegister, sizeof(run), run);
  444. Serial.println("DSP soft reset complete");
  445. httpServer.send(200, "text/plain", "DSP soft reset complete.");
  446. }
  447. //=============================================================
  448. // HTTP DSP status handler GET /dsp_status
  449. //=============================================================
  450. static void handleDspStatus() {
  451. // Core Register (0x081C) — 2 bytes, bit 0 = Run
  452. uint8_t coreRaw[2] = {0, 0};
  453. bool coreOk = i2cReadBlock(DSP_7BIT, dspRegister::CoreRegister, coreRaw, 2);
  454. uint16_t coreVal = ((uint16_t)coreRaw[0] << 8) | coreRaw[1];
  455. // GPIO All Register (0x0808) — 1 byte
  456. uint8_t gpio = 0;
  457. bool gpioOk = i2cReadBlock(DSP_7BIT, dspRegister::GpioAllRegister, &gpio, 1);
  458. // ADC0–3 (0x0809–0x080C) — 1 byte each, read as a 4-byte burst
  459. uint8_t adc[4] = {0, 0, 0, 0};
  460. bool adcOk = i2cReadBlock(DSP_7BIT, dspRegister::Adc0, adc, 4);
  461. bool allOk = coreOk && gpioOk && adcOk;
  462. char buf[200];
  463. snprintf(buf, sizeof(buf),
  464. "{\"ok\":%s,\"running\":%s,"
  465. "\"coreReg\":\"0x%04X\","
  466. "\"gpio\":\"0x%02X\","
  467. "\"adc\":[\"0x%02X\",\"0x%02X\",\"0x%02X\",\"0x%02X\"]}",
  468. allOk ? "true" : "false",
  469. (coreOk && (coreVal & 0x01)) ? "true" : "false",
  470. coreVal, gpio,
  471. adc[0], adc[1], adc[2], adc[3]);
  472. httpServer.send(allOk ? 200 : 500, "application/json", buf);
  473. }
  474. //=============================================================
  475. // HTTP parameter tuner GET /params GET /param POST /param
  476. //=============================================================
  477. static void handleParamsPage() {
  478. String html = FPSTR(PARAMS_HTML);
  479. html.replace("{{IP}}", WiFi.localIP().toString());
  480. httpServer.send(200, "text/html; charset=utf-8", html);
  481. }
  482. static void handleParamGet() {
  483. if (!httpServer.hasArg("addr")) {
  484. httpServer.send(400, "text/plain", "Missing 'addr'.");
  485. return;
  486. }
  487. long addr = strtol(httpServer.arg("addr").c_str(), nullptr, 0);
  488. if (addr < 0 || addr > (long)DSP_PARAM_RAM_END) {
  489. httpServer.send(400, "text/plain", "addr must be 0x0000-0x03FF.");
  490. return;
  491. }
  492. uint8_t raw[4] = {0, 0, 0, 0};
  493. bool ok = i2cReadBlock(DSP_7BIT, (uint16_t)addr, raw, 4);
  494. // 5.23 fixed-point → float
  495. int32_t fixed = ((int32_t)raw[0] << 24) | ((int32_t)raw[1] << 16) |
  496. ((int32_t)raw[2] << 8) | (int32_t)raw[3];
  497. float fval = (float)fixed / 8388608.0f;
  498. char buf[128];
  499. snprintf(buf, sizeof(buf),
  500. "{\"ok\":%s,\"addr\":%ld,\"hex\":\"0x%02X%02X%02X%02X\",\"float\":%.7f}",
  501. ok ? "true" : "false", addr,
  502. raw[0], raw[1], raw[2], raw[3], fval);
  503. httpServer.send(ok ? 200 : 500, "application/json", buf);
  504. }
  505. static void handleParamSet() {
  506. if (!httpServer.hasArg("addr") || !httpServer.hasArg("value")) {
  507. httpServer.send(400, "text/plain", "Missing 'addr' or 'value'.");
  508. return;
  509. }
  510. long addr = strtol(httpServer.arg("addr").c_str(), nullptr, 0);
  511. if (addr < 0 || addr > (long)DSP_PARAM_RAM_END) {
  512. httpServer.send(400, "text/plain", "addr must be 0x0000-0x03FF.");
  513. return;
  514. }
  515. String mode = httpServer.arg("mode");
  516. DSPWriter dspWriter;
  517. if (mode == "hex") {
  518. String hexStr = httpServer.arg("value");
  519. if (hexStr.startsWith("0x") || hexStr.startsWith("0X")) hexStr = hexStr.substring(2);
  520. hexStr.replace(" ", "");
  521. hexStr.replace("_", "");
  522. if (hexStr.length() != 8) {
  523. httpServer.send(400, "text/plain", "Hex value must be exactly 8 hex characters.");
  524. return;
  525. }
  526. uint32_t v = strtoul(hexStr.c_str(), nullptr, 16);
  527. uint8_t sl[5] = { 0x00,
  528. (uint8_t)((v >> 24) & 0xFF),
  529. (uint8_t)((v >> 16) & 0xFF),
  530. (uint8_t)((v >> 8) & 0xFF),
  531. (uint8_t)( v & 0xFF) };
  532. dspWriter.safeload_writeRegister((uint16_t)addr, sl, true);
  533. Serial.printf("Param write (hex) addr=0x%04lX val=0x%08X\n", addr, v);
  534. } else {
  535. // Float mode — safeload_writeRegister handles 5.23 conversion internally
  536. float fval = httpServer.arg("value").toFloat();
  537. dspWriter.safeload_writeRegister((uint16_t)addr, fval, true);
  538. Serial.printf("Param write (float) addr=0x%04lX val=%.7f\n", addr, fval);
  539. }
  540. httpServer.send(200, "text/plain", "OK");
  541. }
  542. //=============================================================
  543. // HTTP WiFi reset handler POST /wifi_reset
  544. //=============================================================
  545. static void handleWifiReset() {
  546. Serial.println("WiFi credentials cleared — rebooting to AP setup mode");
  547. clearWifiCreds();
  548. httpServer.send(200, "text/plain",
  549. "Credentials cleared. Rebooting into setup mode.\n"
  550. "Connect to 'ModulosDSP-Setup' and open 192.168.4.1.");
  551. delay(500);
  552. ESP.restart();
  553. }
  554. //=============================================================
  555. // HTTP GPIO handlers GET /gpio POST /gpio
  556. //=============================================================
  557. static void handleGpioGet() {
  558. uint8_t gpio = 0;
  559. bool gpioOk = i2cReadBlock(DSP_7BIT, dspRegister::GpioAllRegister, &gpio, 1);
  560. uint8_t mpcfg[2] = {0, 0};
  561. bool cfgOk = i2cReadBlock(DSP_7BIT, dspRegister::MpCfg0, mpcfg, 2);
  562. bool ok = gpioOk && cfgOk;
  563. char buf[100];
  564. snprintf(buf, sizeof(buf),
  565. "{\"ok\":%s,\"gpio\":%u,\"mpcfg0\":%u,\"mpcfg1\":%u}",
  566. ok ? "true" : "false", gpio, mpcfg[0], mpcfg[1]);
  567. httpServer.send(ok ? 200 : 500, "application/json", buf);
  568. }
  569. static void handleGpioSet() {
  570. if (!httpServer.hasArg("value")) {
  571. httpServer.send(400, "text/plain", "Missing 'value' parameter.");
  572. return;
  573. }
  574. long val = httpServer.arg("value").toInt();
  575. if (val < 0 || val > 255) {
  576. httpServer.send(400, "text/plain", "value must be 0-255.");
  577. return;
  578. }
  579. uint8_t b = (uint8_t)val;
  580. DSPWriter::writeRegister(dspRegister::GpioAllRegister, 1, &b);
  581. Serial.printf("GPIO set: 0x%02X\n", b);
  582. httpServer.send(200, "text/plain", "OK");
  583. }
  584. //=============================================================
  585. // HTTP OTA handlers
  586. //=============================================================
  587. static void handleOtaPage() {
  588. String html = FPSTR(OTA_HTML);
  589. html.replace("{{IP}}", WiFi.localIP().toString());
  590. httpServer.send(200, "text/html; charset=utf-8", html);
  591. }
  592. static void handleOtaDone() {
  593. if (Update.hasError()) {
  594. String err = "OTA failed: " + String(Update.errorString());
  595. Serial.println(err);
  596. httpServer.send(500, "text/plain", err);
  597. } else {
  598. httpServer.send(200, "text/plain", "Firmware updated — rebooting now.");
  599. Serial.println("OTA success — rebooting");
  600. delay(500);
  601. ESP.restart();
  602. }
  603. }
  604. static void handleOtaStream() {
  605. HTTPUpload& up = httpServer.upload();
  606. if (up.status == UPLOAD_FILE_START) {
  607. Serial.printf("OTA start: %s\n", up.filename.c_str());
  608. ledCyan();
  609. if (!Update.begin(UPDATE_SIZE_UNKNOWN)) {
  610. Serial.print("OTA begin failed: ");
  611. Update.printError(Serial);
  612. }
  613. }
  614. else if (up.status == UPLOAD_FILE_WRITE) {
  615. if (Update.write(up.buf, up.currentSize) != up.currentSize) {
  616. Serial.print("OTA write failed: ");
  617. Update.printError(Serial);
  618. ledErrorFlash();
  619. }
  620. }
  621. else if (up.status == UPLOAD_FILE_END) {
  622. if (Update.end(true)) {
  623. Serial.printf("OTA end: %u bytes written\n", up.totalSize);
  624. } else {
  625. Serial.print("OTA end failed: ");
  626. Update.printError(Serial);
  627. ledErrorFlash();
  628. }
  629. ledOff();
  630. }
  631. else if (up.status == UPLOAD_FILE_ABORTED) {
  632. Update.abort();
  633. Serial.println("OTA aborted");
  634. ledOff();
  635. }
  636. }
  637. //=============================================================
  638. // Setup
  639. //=============================================================
  640. void setup() {
  641. Wire.begin(I2C_SDA, I2C_SCL);
  642. Wire.setClock(400000);
  643. statusLed.begin();
  644. statusLed.setBrightness(NEOPIXEL_BRIGHT);
  645. statusLed.show();
  646. s_lcdOk = (lcd.begin(20, 4) == 0);
  647. if (s_lcdOk) {
  648. lcd.display(); lcd.backlight();
  649. lcd.setCursor(2, 0); lcd.print("Modulos AudioDSP"); delay(1000);
  650. lcd.setCursor(5, 1); lcd.print("Booting..."); delay(1000);
  651. } else {
  652. Serial.println("LCD not found - continuing without display");
  653. }
  654. Serial.begin(115200); delay(1500);
  655. Serial.println(); Serial.println("Booting...");
  656. Serial.printf("Reset reason: %d\n", (int)esp_reset_reason());
  657. if (!loadWifiCreds()) {
  658. // NVS empty — fall back to compile-time defaults so an unattended
  659. // device can reach the network without needing the config portal.
  660. strlcpy(s_ssid, DEFAULT_SSID, sizeof(s_ssid));
  661. strlcpy(s_pass, DEFAULT_PASS, sizeof(s_pass));
  662. Serial.println("No NVS credentials — using defaults");
  663. }
  664. Serial.printf("Connecting to %s", s_ssid);
  665. WiFi.mode(WIFI_STA);
  666. WiFi.setAutoReconnect(true);
  667. WiFi.begin(s_ssid, s_pass);
  668. uint32_t t0 = millis();
  669. while (WiFi.status() != WL_CONNECTED && millis() - t0 < 15000) {
  670. delay(500); Serial.print(".");
  671. }
  672. Serial.println();
  673. if (WiFi.status() != WL_CONNECTED) {
  674. Serial.println("WiFi connect failed — entering setup mode");
  675. startConfigAP(); // never returns
  676. }
  677. if (MDNS.begin(hostname)) {
  678. MDNS.addService("http", "tcp", 80);
  679. Serial.printf("mDNS: http://%s.local/\n", hostname);
  680. } else {
  681. Serial.println("mDNS start failed");
  682. }
  683. if (!LittleFS.begin(false)) {
  684. Serial.println("LittleFS mount failed, formatting...");
  685. if (!LittleFS.begin(true)) { Serial.println("LittleFS mount failed even after format"); return; }
  686. }
  687. Serial.println("LittleFS mounted OK");
  688. File root = LittleFS.open("/"); File f = root.openNextFile();
  689. while (f) { Serial.print("LittleFS: "); Serial.println(f.name()); f = root.openNextFile(); }
  690. if (s_lcdOk) { lcd.setCursor(3, 2); lcd.print("File System OK"); delay(1000); }
  691. tcpServer.begin();
  692. httpServer.on("/", HTTP_GET, handleRoot);
  693. httpServer.on("/status", HTTP_GET, handleStatus);
  694. httpServer.on("/upload", HTTP_POST, handleUploadDone, handleUploadStream);
  695. httpServer.on("/ota", HTTP_GET, handleOtaPage);
  696. httpServer.on("/ota_do", HTTP_POST, handleOtaDone, handleOtaStream);
  697. httpServer.on("/dsp_reset", HTTP_POST, handleDspReset);
  698. httpServer.on("/dsp_status", HTTP_GET, handleDspStatus);
  699. httpServer.on("/gpio", HTTP_GET, handleGpioGet);
  700. httpServer.on("/gpio", HTTP_POST, handleGpioSet);
  701. httpServer.on("/wifi_reset", HTTP_POST, handleWifiReset);
  702. httpServer.on("/params", HTTP_GET, handleParamsPage);
  703. httpServer.on("/param", HTTP_GET, handleParamGet);
  704. httpServer.on("/param", HTTP_POST, handleParamSet);
  705. httpServer.onNotFound([]() {
  706. String uri = httpServer.uri();
  707. if (streamFromFS(uri)) return;
  708. Serial.print("HTTP 404: "); Serial.println(uri);
  709. httpServer.send(404, "text/plain", "Not found: " + uri);
  710. });
  711. httpServer.begin();
  712. if (s_lcdOk) {
  713. lcd.setCursor(4, 3); lcd.print("System Ready"); delay(1000);
  714. lcd.clear();
  715. lcd.setCursor(2, 0); lcd.print("Modulos AudioDSP");
  716. lcd.setCursor(3, 1); lcd.print(version); delay(500);
  717. }
  718. printWifiInfo();
  719. Serial.print("HTTP uploader: http://"); Serial.print(WiFi.localIP()); Serial.println("/");
  720. }
  721. //=============================================================
  722. // TCP bridge
  723. //=============================================================
  724. //=============================================================
  725. // TCP bridge
  726. //=============================================================
  727. static void handleTcpBridgeClient(WiFiClient& client)
  728. {
  729. Serial.println("TCP new connection");
  730. DSPWriter::resetSafeload();
  731. int writeIndex = 0; // next free slot in dataBuffer
  732. int readIndex = 0; // start of current unprocessed command
  733. int receivedByteCount = 0; // total bytes written into dataBuffer
  734. int currentState = STATE_START;
  735. uint32_t lastActivityMs = millis(); // idle watchdog
  736. while (client.connected()) {
  737. httpServer.handleClient();
  738. delay(0);
  739. // ------------------------------------------------------------------
  740. // STEP 1: Always drain the TCP stack into dataBuffer.
  741. // Do this unconditionally every loop iteration — this is what keeps
  742. // the TCP receive window open. If we only drain when we feel like it,
  743. // the window goes to zero and SigmaStudio stops sending (ZeroWindow).
  744. // ------------------------------------------------------------------
  745. int avail = client.available();
  746. if (avail > 0) {
  747. int space = (int)sizeof(dataBuffer) - writeIndex;
  748. if (avail > space) {
  749. Serial.println("TCP RX overflow");
  750. ledErrorFlash(); client.stop(); return;
  751. }
  752. int got = client.read(&dataBuffer[writeIndex], avail);
  753. if (got > 0) {
  754. writeIndex += got;
  755. receivedByteCount += got;
  756. lastActivityMs = millis();
  757. }
  758. }
  759. // ------------------------------------------------------------------
  760. // STEP 2: Process whatever is in the buffer.
  761. // This is driven purely by buffer contents, not by client.available().
  762. // We loop here processing commands until we run out of buffered data.
  763. // ------------------------------------------------------------------
  764. bool processedSomething = true;
  765. while (processedSomething && client.connected()) {
  766. processedSomething = false;
  767. // --- STATE_START: identify opcode ---
  768. if (currentState == STATE_START) {
  769. if (receivedByteCount <= readIndex) {
  770. // Buffer empty — reset for next command
  771. writeIndex = readIndex = receivedByteCount = 0;
  772. ledOff();
  773. break; // nothing to process, go back to receive loop
  774. }
  775. #if TCP_DEBUG
  776. printHex("TCP RX: ", &dataBuffer[readIndex], (uint16_t)(receivedByteCount - readIndex));
  777. #endif
  778. uint8_t op = dataBuffer[readIndex];
  779. if (op == CMD_WRITE) { currentState = STATE_WRITE_CMD; processedSomething = true; }
  780. else if (op == CMD_READ) { currentState = STATE_READ_CMD; processedSomething = true; }
  781. else {
  782. Serial.printf("TCP invalid opcode: 0x%02X\n", op);
  783. ledErrorFlash();
  784. client.stop(); return;
  785. }
  786. }
  787. // --- STATE_WRITE_CMD ---
  788. if (currentState == STATE_WRITE_CMD) {
  789. // Need full header first
  790. if (receivedByteCount < (readIndex + WRITE_HDR_LEN)) break;
  791. writeHeader.safeload = dataBuffer[readIndex + 1];
  792. writeHeader.placement = dataBuffer[readIndex + 2];
  793. writeHeader.totalLen = (uint16_t)((dataBuffer[readIndex + 3] << 8) | dataBuffer[readIndex + 4]);
  794. writeHeader.chipAddr = dataBuffer[readIndex + 5];
  795. writeHeader.dataLen = (uint16_t)((dataBuffer[readIndex + 6] << 8) | dataBuffer[readIndex + 7]);
  796. writeHeader.address = (uint16_t)((dataBuffer[readIndex + 8] << 8) | dataBuffer[readIndex + 9]);
  797. if (writeHeader.totalLen != WRITE_HDR_LEN + writeHeader.dataLen) {
  798. Serial.printf("TCP WRITE bad totalLen: got %u expected %u\n",
  799. writeHeader.totalLen, WRITE_HDR_LEN + writeHeader.dataLen);
  800. ledErrorFlash(); client.stop(); return;
  801. }
  802. // Need full payload — if not here yet, break back to receive loop
  803. if (receivedByteCount < (readIndex + WRITE_HDR_LEN + (int)writeHeader.dataLen)) {
  804. Serial.printf("TCP WRITE buffering: have %d need %d bytes\n",
  805. receivedByteCount - readIndex,
  806. WRITE_HDR_LEN + (int)writeHeader.dataLen);
  807. break;
  808. }
  809. readIndex += WRITE_HDR_LEN;
  810. uint8_t target7 = chipAddrTo7bit(writeHeader.chipAddr);
  811. if (target7 == EEPROM_7BIT) {
  812. ledYellow();
  813. bool ok = eepromWriteBlock(writeHeader.address, &dataBuffer[readIndex], writeHeader.dataLen);
  814. readIndex += writeHeader.dataLen;
  815. sendWriteAck(client, ok);
  816. if (!ok) { Serial.println("TCP EEPROM write failed"); ledErrorFlash(); }
  817. else ledOff();
  818. currentState = STATE_START;
  819. processedSomething = true;
  820. continue;
  821. }
  822. if (target7 != DSP_7BIT) {
  823. Serial.printf("TCP unknown chipAddr: 0x%02X\n", writeHeader.chipAddr);
  824. ledErrorFlash(); client.stop(); return;
  825. }
  826. ledGreen();
  827. uint8_t registerSize = registerSizeForAddress(writeHeader.address, writeHeader.dataLen);
  828. uint16_t regAddress = writeHeader.address;
  829. Serial.printf("TCP WRITE addr=0x%04X len=%u regSz=%u safeload=%u\n",
  830. writeHeader.address, writeHeader.dataLen, registerSize, writeHeader.safeload);
  831. bool writeOk = true;
  832. if (writeHeader.safeload == 1) {
  833. if (writeHeader.dataLen % 4 != 0) {
  834. Serial.printf("TCP safeload dataLen %u not multiple of 4\n", writeHeader.dataLen);
  835. ledErrorFlash(); client.stop(); return;
  836. }
  837. int writeCount = writeHeader.dataLen / 4;
  838. int slri = readIndex;
  839. DSPWriter dspWriter;
  840. while (writeCount > 0) {
  841. uint8_t da[5] = { 0x00, dataBuffer[slri], dataBuffer[slri+1],
  842. dataBuffer[slri+2], dataBuffer[slri+3] };
  843. dspWriter.safeload_writeRegister(regAddress, da, writeCount == 1);
  844. regAddress++; slri += 4; writeCount--; delay(0);
  845. }
  846. } else {
  847. writeOk = DSPWriter::writeRegisterBlock(regAddress, writeHeader.dataLen,
  848. &dataBuffer[readIndex], registerSize);
  849. if (!writeOk) Serial.println("TCP DSP block write failed");
  850. }
  851. readIndex += writeHeader.dataLen;
  852. if (!writeOk) ledErrorFlash(); else ledOff();
  853. sendWriteAck(client, writeOk);
  854. currentState = STATE_START;
  855. processedSomething = true;
  856. continue;
  857. }
  858. // --- STATE_READ_CMD ---
  859. if (currentState == STATE_READ_CMD) {
  860. if (receivedByteCount < (readIndex + READ_HDR_LEN)) break;
  861. readHeader.totalLen = (uint16_t)((dataBuffer[readIndex + 1] << 8) | dataBuffer[readIndex + 2]);
  862. readHeader.chipAddr = dataBuffer[readIndex + 3];
  863. readHeader.dataLen = (uint16_t)((dataBuffer[readIndex + 4] << 8) | dataBuffer[readIndex + 5]);
  864. readHeader.address = (uint16_t)((dataBuffer[readIndex + 6] << 8) | dataBuffer[readIndex + 7]);
  865. readIndex += READ_HDR_LEN;
  866. uint8_t target7 = chipAddrTo7bit(readHeader.chipAddr);
  867. Serial.printf("TCP READ chip=0x%02X addr=0x%04X len=%u\n",
  868. readHeader.chipAddr, readHeader.address, readHeader.dataLen);
  869. if (readHeader.dataLen > 4096) { readHeader.dataLen = 4096; }
  870. static uint8_t readOut[4096];
  871. bool ok = false;
  872. ledBlue();
  873. if (target7 == EEPROM_7BIT) ok = i2cReadBlock(EEPROM_7BIT, readHeader.address, readOut, readHeader.dataLen);
  874. else if (target7 == DSP_7BIT) ok = i2cReadBlock(DSP_7BIT, readHeader.address, readOut, readHeader.dataLen);
  875. else {
  876. Serial.printf("TCP unknown chipAddr (READ): 0x%02X\n", readHeader.chipAddr);
  877. }
  878. if (!ok) Serial.println("TCP READ I2C failed - sending zeros");
  879. if (!sendReadResponse(client, ok ? readOut : nullptr, readHeader.dataLen, ok)) {
  880. Serial.println("TCP READ send failed"); ledErrorFlash(); client.stop(); return;
  881. }
  882. ledOff();
  883. currentState = STATE_START;
  884. processedSomething = true;
  885. continue;
  886. }
  887. } // end process loop
  888. // Idle path — no data waiting, no command in progress.
  889. if (currentState == STATE_START && receivedByteCount == readIndex) {
  890. if (!client.available()) {
  891. if (millis() - lastActivityMs > TCP_IDLE_TIMEOUT_MS) {
  892. Serial.println("TCP idle timeout — closing session");
  893. ledErrorFlash();
  894. break;
  895. }
  896. httpServer.handleClient();
  897. delay(10);
  898. }
  899. }
  900. } // end main while loop
  901. DSPWriter::resetSafeload(); // flush any mid-session safeload before disconnect
  902. client.stop();
  903. Serial.println("TCP disconnected");
  904. ledOff();
  905. }
  906. //=============================================================
  907. // WiFi watchdog
  908. //=============================================================
  909. static uint32_t s_wifiLastCheck = 0;
  910. static bool s_wifiLost = false;
  911. static void maintainWifi()
  912. {
  913. if (millis() - s_wifiLastCheck < 5000) return;
  914. s_wifiLastCheck = millis();
  915. if (WiFi.status() != WL_CONNECTED) {
  916. if (!s_wifiLost) {
  917. Serial.println("WiFi lost");
  918. if (s_lcdOk) { lcd.setCursor(0, 3); lcd.print("WiFi lost... "); }
  919. s_wifiLost = true;
  920. }
  921. WiFi.reconnect();
  922. } else if (s_wifiLost) {
  923. Serial.print("WiFi reconnected, IP: "); Serial.println(WiFi.localIP());
  924. MDNS.begin(hostname);
  925. MDNS.addService("http", "tcp", 80);
  926. tcpServer.begin(); // re-register listening socket with recovered stack
  927. printWifiInfo(); // update LCD with current IP
  928. s_wifiLost = false;
  929. }
  930. }
  931. //=============================================================
  932. // Loop
  933. //=============================================================
  934. void loop() {
  935. maintainWifi();
  936. httpServer.handleClient();
  937. if (uploadActive) { delay(1); return; }
  938. WiFiClient client = tcpServer.available();
  939. if (client) handleTcpBridgeClient(client);
  940. delay(1);
  941. }