소스 검색

Initial Commit

Benjamin Harris 1 개월 전
커밋
62eec39037

+ 124 - 0
DSPWriter.cpp

@@ -0,0 +1,124 @@
+#include "DSPWriter.h"
+#include <Wire.h>
+#include "DataConversion.h"
+
+DSPWriter::DSPWriter() {}
+DSPWriter::~DSPWriter() {}
+
+void DSPWriter::downloadProgram()
+{
+    /*
+    DSPWriter::writeRegisterBlock(REG_COREREGISTER_IC_1_ADDR, REG_COREREGISTER_IC_1_BYTE, R0_COREREGISTER_IC_1_Default, CORE_REGISTER_R0_REGSIZE);
+    DSPWriter::writeRegisterBlock(PROGRAM_ADDR_IC_1, PROGRAM_SIZE_IC_1, Program_Data_IC_1, PROGRAM_REGSIZE);
+    DSPWriter::writeRegisterBlock(PARAM_ADDR_IC_1, PARAM_SIZE_IC_1, Param_Data_IC_1, PARAMETER_REGSIZE);
+    DSPWriter::writeRegisterBlock(REG_COREREGISTER_IC_1_ADDR, R3_HWCONFIGURATION_IC_1_SIZE, R3_HWCONFIGURATION_IC_1_Default, HARDWARE_CONF_REGSIZE);
+    DSPWriter::writeRegisterBlock(REG_COREREGISTER_IC_1_ADDR, REG_COREREGISTER_IC_1_BYTE, R4_COREREGISTER_IC_1_Default, CORE_REGISTER_R4_REGSIZE);*/
+}
+
+void DSPWriter::writeRegisterBlock(uint16_t subAddress, int dataLength, const uint8_t* pdata, uint8_t registerSize)
+{
+    uint16_t bytesSent = 0;
+
+    while (bytesSent < dataLength)
+    {
+        uint8_t MSByte = subAddress >> 8;
+        uint8_t LSByte = (uint8_t)(subAddress & 0xFF);
+
+        Wire.beginTransmission(DSP_I2C_ADDRESS);
+        Wire.write(MSByte);
+        Wire.write(LSByte);
+
+        uint8_t chunk = registerSize;
+        if ((bytesSent + chunk) > dataLength) {
+            chunk = dataLength - bytesSent;
+        }
+
+        for (uint8_t i = 0; i < chunk; i++) {
+            Wire.write(pdata[bytesSent++]);
+        }
+
+        uint8_t err = Wire.endTransmission();
+
+        if (err != 0) {
+            // 1 = data too long, 2 = NACK addr, 3 = NACK data, 4 = other
+            // Caller can check Serial output; silent continue to attempt remaining blocks
+        }
+
+        subAddress++;
+        delay(0);
+    }
+}
+
+void DSPWriter::writeRegister(uint16_t memoryAddress, uint8_t length, const uint8_t* data)
+{
+    uint8_t LSByte = (uint8_t)memoryAddress & 0xFF;
+    uint8_t MSByte = memoryAddress >> 8;
+
+    Wire.beginTransmission(DSP_I2C_ADDRESS); // Begin write
+
+    Wire.write(MSByte); // Send high address
+    Wire.write(LSByte); // Send low address
+
+    for (uint8_t i = 0; i < length; i++)
+        Wire.write(data[i]); // Send all bytes in passed array
+
+    Wire.endTransmission(); // Write out data to I2C and stop transmitting
+}
+
+// ---------------------------------------------------------------
+// Safeload counter — static so it survives across DSPWriter
+// instances, but exposed via resetSafeload() so the TCP bridge
+// can reset it cleanly at the start of each session.
+// ---------------------------------------------------------------
+static uint8_t s_safeload_count = 0;
+
+void DSPWriter::resetSafeload()
+{
+    s_safeload_count = 0;
+}
+
+void DSPWriter::safeload_writeRegister(uint16_t memoryAddress, uint8_t* data, bool finished)
+{
+    uint8_t addr[2]; // Address array
+
+    addr[0] = (memoryAddress >> 8) & 0xFF;
+    addr[1] = memoryAddress & 0xFF;
+
+    // Place the 16-bit memory address into the next safeload address slot
+    DSPWriter::writeRegister(dspRegister::SafeloadAddress0 + s_safeload_count, sizeof(addr), addr);
+
+    // Q: Why is the safeload register five bytes long for four-byte parameters?
+    // A: Safeload registers also support five-byte slew RAM writes. For normal
+    //    parameter RAM writes the first byte is always 0x00.
+    DSPWriter::writeRegister(dspRegister::SafeloadData0 + s_safeload_count, 5, data);
+
+    s_safeload_count++;
+
+    if (finished == true || s_safeload_count >= 5) // Max 5 safeload slots
+    {
+        addr[0] = 0x00;
+        addr[1] = 0x3C; // IST bit — initiate safeload transfer
+        DSPWriter::writeRegister(dspRegister::CoreRegister, sizeof(addr), addr);
+        s_safeload_count = 0;
+    }
+}
+
+void DSPWriter::safeload_writeRegister(uint16_t memoryAddress, int32_t data, bool finished)
+{
+    uint8_t dataArray[5];
+    DataConversion::intToFixed(data, dataArray);
+    safeload_writeRegister(memoryAddress, dataArray, finished);
+}
+
+void DSPWriter::safeload_writeRegister(uint16_t memoryAddress, float data, bool finished)
+{
+    uint8_t dataArray[5];
+    DataConversion::floatToFixed(data, dataArray);
+    safeload_writeRegister(memoryAddress, dataArray, finished);
+}
+
+void DSPWriter::safeload_writeRegister(uint16_t memoryAddress, int16_t data,  bool finished) { safeload_writeRegister(memoryAddress, (int32_t)data,  finished); }
+void DSPWriter::safeload_writeRegister(uint16_t memoryAddress, uint32_t data, bool finished) { safeload_writeRegister(memoryAddress, (int32_t)data,  finished); }
+void DSPWriter::safeload_writeRegister(uint16_t memoryAddress, uint16_t data, bool finished) { safeload_writeRegister(memoryAddress, (int32_t)data,  finished); }
+void DSPWriter::safeload_writeRegister(uint16_t memoryAddress, uint8_t data,  bool finished) { safeload_writeRegister(memoryAddress, (int32_t)data,  finished); }
+void DSPWriter::safeload_writeRegister(uint16_t memoryAddress, double data,   bool finished) { safeload_writeRegister(memoryAddress, (float)data,   finished); }

+ 144 - 0
DSPWriter.h

@@ -0,0 +1,144 @@
+
+#pragma once
+
+#include <stdint.h>
+
+#define CORE_REGISTER_R0_REGSIZE 2
+#define HARDWARE_CONF_REGSIZE 1
+#define CORE_REGISTER_R4_REGSIZE 2
+#define PARAMETER_REGSIZE 4
+#define PROGRAM_REGSIZE 5
+
+/* 7-bit i2c addresses */
+#define DSP_I2C_ADDRESS 0x34 //(0x68 >> 1) & 0xFE
+#define EEPROM_I2C_ADDRESS 0x50 //(0xa0 >> 1) & 0xFE
+
+// ADAU1401 address space boundaries
+#define DSP_PARAM_RAM_START  0x0000
+#define DSP_PARAM_RAM_END    0x03FF
+#define DSP_PROG_RAM_START   0x0400
+#define DSP_PROG_RAM_END     0x07FF
+
+// Hardware register constants
+typedef enum
+{
+    InterfaceRegister0 = 0x0800,
+    InterfaceRegister1 = 0x0801,
+    InterfaceRegister2 = 0x0802,
+    InterfaceRegister3 = 0x0803,
+    InterfaceRegister4 = 0x0804,
+    InterfaceRegister5 = 0x0805,
+    InterfaceRegister6 = 0x0806,
+    InterfaceRegister7 = 0x0807,
+    GpioAllRegister = 0x0808,
+    Adc0 = 0x0809,
+    Adc1 = 0x080A,
+    Adc2 = 0x080B,
+    Adc3 = 0x080C,
+    SafeloadData0 = 0x0810,
+    SafeloadData1 = 0x0811,
+    SafeloadData2 = 0x0812,
+    SafeloadData3 = 0x0813,
+    SafeloadData4 = 0x0814,
+    SafeloadAddress0 = 0x0815,
+    SafeloadAddress1 = 0x0816,
+    SafeloadAddress2 = 0x0817,
+    SafeloadAddress3 = 0x0818,
+    SafeloadAddress4 = 0x0819,
+    DataCapture0 = 0x081A,
+    DataCpature1 = 0x081B,
+    CoreRegister = 0x081C,
+    RAMRegister = 0x081D,
+    SerialOutRegister1 = 0x081E,
+    SerialInputRegister = 0x081F,
+    MpCfg0 = 0x0820,
+    MpCfg1 = 0x0821,
+    AnalogPowerDownRegister = 0x0822,
+    AnalogInterfaceRegister0 = 0x0824
+} dspRegister;
+
+class DSPWriter
+{
+public:
+    DSPWriter();
+    ~DSPWriter();
+
+    // DSP data write methods
+
+    static void downloadProgram();
+
+    /***************************************
+    Function: writeRegisterBlock()
+    Inputs:
+      uint16_t subAddress;               DSP memory start address
+      int dataLength;                    Number of bytes to write
+      const uint8_t *pdata;             Data array to write
+      uint8_t registerSize;             Number of bytes each register can hold
+    ***************************************/
+    static void writeRegisterBlock(uint16_t subAddress, int dataLength, const uint8_t* pdata, uint8_t registerSize);
+
+    /***************************************
+    Function: writeRegister()
+    Purpose:  Writes data to the DSP
+              (max 32 bytes due to i2c buffer size)
+    Inputs:   uint16_t startMemoryAddress;   DSP memory address
+              uint8_t length;                Number of bytes to write
+              uint8_t *data;                 Data array to write
+    Returns:  None
+    ***************************************/
+    static void writeRegister(uint16_t memoryAddress, uint8_t length, const uint8_t* data);
+
+    /***************************************
+    Function: resetSafeload()
+    Purpose:  Resets the internal safeload counter. Must be called at the
+              start of each TCP session to prevent counter corruption from
+              a previously interrupted safeload sequence.
+    ***************************************/
+    static void resetSafeload();
+
+    // Template wrapper for safeload_write
+    template <typename Address, typename Data1, typename... DataN>
+    void safeload_write(const Address& address, const Data1& data1, const DataN &...dataN)
+    {
+        // Store passed address
+        _dspRegAddr = address;
+        safeload_write_wrapper(data1, dataN...);
+    }
+
+    /***************************************
+    Function: safeload_writeRegister()
+    Purpose:  Writes 5 bytes of data to the parameter memory of the DSP, the first byte is 0x00
+    Inputs:   uint16_t startMemoryAddress;   DSP memory address
+              *data;                 Data array to write
+              bool finished;                 Indicates if this is the last packet or not
+    Returns:  None
+    ***************************************/
+    void safeload_writeRegister(uint16_t memoryAddress, uint8_t* data, bool finished);
+    void safeload_writeRegister(uint16_t memoryAddress, int32_t data, bool finished);
+    void safeload_writeRegister(uint16_t memoryAddress, float data, bool finished);
+    void safeload_writeRegister(uint16_t memoryAddress, int16_t data, bool finished);
+    void safeload_writeRegister(uint16_t memoryAddress, uint32_t data, bool finished);
+    void safeload_writeRegister(uint16_t memoryAddress, uint16_t data, bool finished);
+    void safeload_writeRegister(uint16_t memoryAddress, uint8_t data, bool finished);
+    void safeload_writeRegister(uint16_t memoryAddress, double data, bool finished);
+
+
+private:
+    // Wrapper template functions for safeload template
+    template <typename Data1, typename... DataN>
+    void safeload_write_wrapper(const Data1& data1, const DataN &...dataN)
+    {
+        safeload_writeRegister(_dspRegAddr, data1, false);
+        _dspRegAddr++;
+        safeload_write_wrapper(dataN...);  // Recursive call using pack expansion syntax
+    }
+    // Handles last argument
+    template <typename Data1>
+    void safeload_write_wrapper(const Data1& data1)
+    {
+        safeload_writeRegister(_dspRegAddr, data1, true);
+    }
+
+    // Private variables
+    uint16_t _dspRegAddr;      // Used by template safeload functions
+};

+ 44 - 0
DataConversion.cpp

@@ -0,0 +1,44 @@
+
+#include "DataConversion.h"
+
+void DataConversion::floatToFixed(float value, uint8_t* buffer)
+{
+    // Convert float to 4 byte hex
+    int32_t fixedval = (value * ((int32_t)1 << 23));
+
+    // Store the 4 bytes in the passed buffer
+    buffer[0] = 0x00; // First must be empty
+    buffer[1] = (fixedval >> 24) & 0xFF;
+    buffer[2] = (fixedval >> 16) & 0xFF;
+    buffer[3] = (fixedval >> 8) & 0xFF;
+    buffer[4] = fixedval & 0xFF;
+}
+
+/***************************************
+Function: intToFixed()
+Purpose:  Converts a 28.0 integer value to 5-byte HEX and stores it to a buffer
+Inputs:   int32_t value;      Value to convert
+          uint8_t *buffer;    Buffer to store the converted data to
+Returns:  None
+***************************************/
+void DataConversion::intToFixed(int32_t value, uint8_t* buffer)
+{
+    // Store the 4 bytes in the passed buffer
+    buffer[0] = 0x00; // First must be empty
+    buffer[1] = (value >> 24) & 0xFF;
+    buffer[2] = (value >> 16) & 0xFF;
+    buffer[3] = (value >> 8) & 0xFF;
+    buffer[4] = value & 0xFF;
+}
+
+/***************************************
+Function: floatToInt()
+Purpose:  Converts a 5.23 float value to int 28.0
+Inputs:   float value;    Value to convert
+Returns:  int32_t;        Converted value
+***************************************/
+int32_t DataConversion::floatToInt(float value)
+{
+    // Convert float 5.23 to int 28.0
+    return (value * ((int32_t)1 << 23));
+}

+ 36 - 0
DataConversion.h

@@ -0,0 +1,36 @@
+
+#pragma once
+
+#include <stdint.h>
+
+class DataConversion
+{
+public:
+
+    /***************************************
+    Function: floatTofixed()
+    Purpose:  Converts a 5.23 float value to 5-byte HEX and stores it to a buffer
+    Inputs:   float value;      Value to convert
+              uint8_t *buffer;  Buffer to store the converted data to
+    Returns:  None
+    ***************************************/
+    static void floatToFixed(float value, uint8_t* buffer);
+
+    /***************************************
+    Function: intToFixed()
+    Purpose:  Converts a 28.0 integer value to 5-byte HEX and stores it to a buffer
+    Inputs:   int32_t value;      Value to convert
+              uint8_t *buffer;    Buffer to store the converted data to
+    Returns:  None
+    ***************************************/
+    static void intToFixed(int32_t value, uint8_t* buffer);
+
+    /***************************************
+    Function: floatToInt()
+    Purpose:  Converts a 5.23 float value to int 28.0
+    Inputs:   float value;    Value to convert
+    Returns:  int32_t;        Converted value
+    ***************************************/
+    static int32_t floatToInt(float value);
+
+};

+ 701 - 0
ModulosDSP_101.ino

@@ -0,0 +1,701 @@
+// Modulos ADAU DSP WiFi + EEPROM (HTTP uploader + Sigma TCP bridge)
+// Ver 1.3.1
+// March 2026
+//
+// This is a clean restore to the last known good state (v1.3.0/_04)
+// with the chipAddr 0x01 fix applied. The receive loop is the original
+// simple byte-at-a-time approach which correctly handled large packets.
+//
+// Changes vs original:
+//  - sendReadResponse() corrected to 6-byte SigmaTCP header format
+//  - sendWriteAck() added after every DSP/EEPROM write
+//  - chipAddrTo7bit() handles 0x01=DSP, 0x02=EEPROM chip indexes
+//  - I2C read failure returns zeros instead of dropping connection
+//  - registerSize derived from ADAU1401 address map
+//  - DSPWriter::resetSafeload() at start of each TCP session
+//  - WS2812 NeoPixel on GPIO 21 (Waveshare ESP32-S3 Zero)
+//
+// Changelog v1.3.0:
+//  - FIX: sendReadResponse() header corrected to 6-byte SigmaTCP format
+//         (was 4 bytes; SigmaStudio expects: 0x0B, totalLen_hi, totalLen_lo, status, dataLen_hi, dataLen_lo)
+//  - ADD: sendWriteAck() — SigmaStudio expects a 4-byte ACK after every write
+//         (was missing; caused immediate disconnect after first write)
+//  - FIX: I2C read failure now returns zeros instead of dropping TCP connection
+//         (SigmaStudio can probe/read a DSP that isn't responding yet without aborting)
+//  - ADD: Hex dump of first bytes of each received command to Serial for diagnostics
+//  - ADD: printHex() debug helper
+//
+// Changelog v1.4.0:
+//  - FIX: TCP receive loop replaced with client.readBytes() + 3s per-packet timeout
+//         Previous byte-at-a-time loop timed out mid-transfer on large program blocks
+//         (e.g. 1490-byte program download) because client.available() returns 0
+//         between TCP segments even when more data is in flight. Now we block-read
+//         exactly the bytes needed to complete the current packet, so a large program
+//         download can span multiple TCP segments without triggering a false idle timeout.
+//  - FIX: Idle timeout now only applies between commands, not during active receive
+//
+// Changelog v1.2.0:
+//  - ADD: WS2812 NeoPixel status LED (Waveshare ESP32-S3 Zero, GPIO 21)
+//         OFF        = idle / no TCP client
+//         GREEN      = DSP write in progress
+//         BLUE       = DSP read in progress
+//         YELLOW     = EEPROM write via TCP
+//         MAGENTA    = HTTP EEPROM upload in progress
+//         RED flash  = error (I2C fail, overflow, bad packet)
+//
+// Changelog v1.1.0:
+//  - FIX: Buffer overflow check moved to BEFORE write
+//  - FIX: chipAddrTo7bit() replaced with explicit lookup table
+//  - FIX: registerSize now derived from ADAU1401 address range
+//  - FIX: DSPWriter::resetSafeload() called at TCP session start
+//  - FIX: Safeload dataLen validated as multiple of 4
+//  - FIX: totalLen vs dataLen cross-validated on WRITE packets
+
+#include <WiFi.h>
+#include <Wire.h>
+#include <WebServer.h>
+#include "DSPWriter.h"
+#include <hd44780.h>
+#include <hd44780ioClass/hd44780_I2Cexp.h>
+#include <Adafruit_NeoPixel.h>
+#include "FS.h"
+#include <LittleFS.h>
+
+//=============================================================
+// WiFi / UI
+//=============================================================
+const char* ssid     = "alfred";
+const char* password = "alfred16";
+const char* version  = "VER: 260304_13";
+
+#define I2C_SDA 13
+#define I2C_SCL 12
+
+WiFiServer tcpServer(8086);
+WebServer  httpServer(80);
+
+#include "index_html.h"
+
+hd44780_I2Cexp lcd;
+
+//=============================================================
+// 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
+
+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;
+
+//=============================================================
+// CRC32
+//=============================================================
+static uint32_t crc32_update(uint32_t crc, const uint8_t* data, size_t len)
+{
+  crc = ~crc;
+  for (size_t i = 0; i < len; i++) {
+    crc ^= data[i];
+    for (int b = 0; b < 8; b++) {
+      uint32_t mask = -(crc & 1u);
+      crc = (crc >> 1) ^ (0xEDB88320u & mask);
+    }
+  }
+  return ~crc;
+}
+
+//=============================================================
+// 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 eepromReadBlock(uint16_t memAddr, uint8_t* out, uint16_t len)
+{
+  Wire.beginTransmission(EEPROM_7BIT);
+  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)EEPROM_7BIT, (int)ask) != ask) return false;
+    for (uint8_t i = 0; i < ask; i++) out[got++] = Wire.read();
+  }
+  return true;
+}
+
+static bool dspReadBlock(uint16_t memAddr, uint8_t* out, uint16_t len)
+{
+  Wire.beginTransmission(DSP_7BIT);
+  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)DSP_7BIT, (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();
+}
+
+static void printWifiInfo()
+{
+  Serial.println();
+  Serial.println("WiFi connected.");
+  Serial.print("WiFi IP: "); Serial.println(WiFi.localIP());
+  Serial.print("MAC: ");     Serial.println(WiFi.macAddress());
+  Serial.println("Modulos AudioDSP");
+  Serial.println(version);
+  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 = crc32_update(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 (!eepromReadBlock(addr, tmp, n)) { Serial.println("EEPROM read failed"); uploadFailed = true; break; }
+        crc = crc32_update(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();
+  }
+}
+
+//=============================================================
+// Setup
+//=============================================================
+void setup() {
+  Wire.begin(I2C_SDA, I2C_SCL);
+  Wire.setClock(400000);
+
+  statusLed.begin();
+  statusLed.setBrightness(NEOPIXEL_BRIGHT);
+  statusLed.show();
+
+  lcd.begin(20, 4); lcd.display(); lcd.backlight();
+  lcd.setCursor(2, 0); lcd.print("Modulos AudioDSP"); delay(1000);
+  lcd.setCursor(5, 1); lcd.print("Booting..."); delay(1000);
+
+  Serial.begin(115200); delay(1500);
+  Serial.println(); Serial.println("Booting...");
+  Serial.printf("Reset reason: %d\n", (int)esp_reset_reason());
+
+  WiFi.mode(WIFI_STA);
+  WiFi.begin(ssid, password);
+  while (WiFi.waitForConnectResult() != WL_CONNECTED) {
+    Serial.println("Connection Failed! Rebooting...");
+    delay(5000); ESP.restart();
+  }
+
+  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(); }
+
+  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.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();
+
+  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;
+
+  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).
+    // ------------------------------------------------------------------
+    while (client.available()) {
+      if (writeIndex >= (int)sizeof(dataBuffer)) {
+        Serial.println("TCP RX overflow");
+        ledErrorFlash(); client.stop(); return;
+      }
+      int b = client.read();
+      if (b < 0) break;
+      dataBuffer[writeIndex++] = (uint8_t)b;
+      receivedByteCount++;
+    }
+
+    // ------------------------------------------------------------------
+    // 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
+        }
+        printHex("TCP RX: ", &dataBuffer[readIndex], (uint16_t)(receivedByteCount - readIndex));
+        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]);
+
+        // 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);
+
+        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 {
+          DSPWriter::writeRegisterBlock(regAddress, writeHeader.dataLen,
+                                        &dataBuffer[readIndex], registerSize);
+        }
+
+        readIndex += writeHeader.dataLen;
+        sendWriteAck(client, true);
+        ledOff();
+        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 = eepromReadBlock(readHeader.address, readOut, readHeader.dataLen);
+        else if (target7 == DSP_7BIT)    ok = dspReadBlock   (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
+
+    // No idle timeout — stay connected until SigmaStudio disconnects.
+    // client.connected() will return false when the TCP connection drops.
+    if (currentState == STATE_START && receivedByteCount == readIndex) {
+      if (!client.available()) {
+        httpServer.handleClient();
+        delay(10);
+      }
+    }
+
+  } // end main while loop
+
+  client.stop();
+  Serial.println("TCP disconnected");
+  ledOff();
+}
+
+//=============================================================
+// Loop
+//=============================================================
+void loop() {
+  httpServer.handleClient();
+  if (uploadActive) { delay(1); return; }
+  WiFiClient client = tcpServer.available();
+  if (client) handleTcpBridgeClient(client);
+  delay(1);
+}

파일 크기가 너무 크기때문에 변경 상태를 표시하지 않습니다.
+ 5 - 0
data/bootstrap.bundle.min.js


BIN
data/desktop.ini


BIN
data/fa-solid-900.woff2


BIN
data/favicon.ico


파일 크기가 너무 크기때문에 변경 상태를 표시하지 않습니다.
+ 18 - 0
data/font-awesome.min.css


파일 크기가 너무 크기때문에 변경 상태를 표시하지 않습니다.
+ 1 - 0
data/jquery-3.7.1.slim.min.js


BIN
data/logo-horizontal.webp


BIN
data/nasalization-rg.woff2


파일 크기가 너무 크기때문에 변경 상태를 표시하지 않습니다.
+ 12 - 0
data/yeti-bootstrap.min.css


BIN
desktop.ini


+ 87 - 0
index_html.h

@@ -0,0 +1,87 @@
+#pragma once
+#include <pgmspace.h>
+
+static const char INDEX_HTML[] PROGMEM = R"HTML(
+<!doctype html>
+<html lang="en-AU">
+<head>
+  <meta charset="utf-8">
+  <meta name="viewport" content="width=device-width, initial-scale=1">
+  <link rel="icon" type="image/x-icon" href="/favicon.ico">
+  <link rel="stylesheet" href="yeti-bootstrap.min.css">
+  <link rel="stylesheet" href="font-awesome.min.css">
+  <script src="jquery-3.7.1.slim.min.js" crossorigin="anonymous"></script>
+
+  <title>Modulos EEPROM Uploader</title>
+  <style>
+    @font-face {
+      font-family: 'nasalization';
+      src: url('/nasalization-rg.woff2') format('woff2');
+    }
+    .nasalization { font-family: 'nasalization'; }
+  </style>
+</head>
+
+<body class="bg-light" style="padding-top: 5rem;">
+  <nav class="navbar navbar-expand-md navbar-dark bg-dark fixed-top">
+    <div class="container">
+      <a class="navbar-brand nasalization text-uppercase" href="#">
+        <img src="/logo-horizontal.webp" height="30" class="d-inline-block align-top" loading="lazy">
+      </a>
+
+      <button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarText" aria-controls="navbarText" aria-expanded="false" aria-label="Toggle navigation">
+        <span class="navbar-toggler-icon"></span>
+      </button>
+
+      <div class="collapse navbar-collapse" id="navbarText">
+        <span class="col align-self-end text-end text-white">
+          <span>{{IP}}</span>
+          <i class="text-success fas fa-wifi"></i>
+          <!--<i class="fas fa-network-wired"></i>-->
+          <!--<i class="fas fa-diagram-project"></i>-->
+        </span>
+      </div>
+    </div>
+  </nav>
+
+  <div class="container">
+    <div class="row">
+      <div class="col-sm">
+        <h2 class="text-center pt-2 font-weight-bold" id="title">Modulos EEPROM Uploader (24C256)</h2>
+      </div>
+    </div>
+
+    <div class="row font-weight-bold border-top border-bottom">
+      <p>EEPROM I2C: 0x50, Size: 32768 bytes, Page: 64 bytes</p>
+
+      <form method="POST" action="/upload" enctype="multipart/form-data">
+        <div class="mb-3">
+          <input class="form-control" type="file" name="bin" required>
+        </div>
+
+        <div class="mb-3 form-check">
+          <input class="form-check-input" type="checkbox" name="verify" value="1" checked>
+          <label class="form-check-label">Verify after write (CRC32)</label>
+        </div>
+
+        <button style="background-color:#114378;border-color:#114378;color:#e8b434;"
+                type="submit" class="btn">Upload and Program</button>
+      </form>
+
+      <p><a href="/status">Status</a></p>
+    </div>
+  </div>
+
+  <footer class="fixed-bottom mt-auto py-3 border-top bg-dark">
+    <div class="container">
+      <p class="text-white text-center mb-0">
+        Copyright &copy; <span id="year"></span> - Modulos Audio - DSP Controller - All Rights Reserved
+      </p>
+      <script>document.getElementById('year').innerHTML = new Date().getFullYear();</script>
+    </div>
+  </footer>
+
+  <script src="/bootstrap.bundle.min.js" crossorigin="anonymous"></script>
+</body>
+</html>
+)HTML";

이 변경점에서 너무 많은 파일들이 변경되어 몇몇 파일들은 표시되지 않았습니다.