ModulosDSP_101.ino 36 KB

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