×
#include <WiFi.h>
#include <WebServer.h>
#include <HTTPClient.h>
#include <ArduinoJson.h>
#include <DHT.h>

#include <driver/i2s.h>
#include <arduinoFFT.h>
#include <math.h>

// ============================================================
// Audio_Monitor_R3
// ESP32 Audio Status Monitor + INMP441 6-Band FFT
//
// [Integrated Functions]
// - WiFi Web Dashboard
// - Roon ROCK monitoring
// - DHT11 temperature / humidity
// - INMP441 6-band FFT spectrum monitor
//
// [Design policy]
// - Shared status via gState
// - Web reads only gState
// - FFT runs periodically and publishes averaged result
// - Calibration offset per band
// ============================================================

// ============================================================
// 0. USER SETTINGS
// ============================================================

// ---------- WiFi ----------
const char* ssid     = "<私のSSID>";
const char* password = "<私のパスワード>";

// ---------- Roon ROCK API ----------
const char* ROON_STATE_URL = "http://<私のROCK IPアドレス>/1/getstate";

// ---------- DHT11 ----------
#define DHTPIN  13
#define DHTTYPE DHT11

DHT dht(DHTPIN, DHTTYPE);

// ---------- Temperature thresholds ----------
const float TEMP_WARNING = 25.0;
const float TEMP_ERROR   = 35.0;

// ---------- Web refresh ----------
const uint32_t PAGE_REFRESH_SEC = 30;

// ---------- Update intervals ----------
const uint32_t ROON_UPDATE_MS     = 10000;
const uint32_t ENV_UPDATE_MS      = 5000;
const uint32_t FFT_BLOCK_MS       = 250;    // one FFT cycle trigger
const uint32_t SPECTRUM_UPDATE_MS = 2000;   // publish to dashboard every 2 sec

// ============================================================
// 1. FFT / INMP441 SETTINGS
// ============================================================

// ---------- I2S pin assignment ----------
static const int I2S_BCLK_PIN = 14;   // INMP441 SCK / BCLK
static const int I2S_WS_PIN   = 15;   // INMP441 WS / LRCLK
static const int I2S_SD_PIN   = 32;   // INMP441 SD

// ---------- audio settings ----------
static const uint16_t FFT_SIZE    = 1024;
static const double   SAMPLE_RATE = 16000.0;
static const double   BIN_WIDTH   = SAMPLE_RATE / FFT_SIZE;   // 15.625 Hz/bin

// ---------- low-frequency handling ----------
static const bool EXCLUDE_BIN_1 = true;          // ignore 15.625 Hz bin
static const bool INCLUDE_BIN2_IN_63HZ = false;  // include 31.25 Hz in 63Hz band?

// ---------- spectrum averaging ----------
static const uint8_t SPECTRUM_AVG_BLOCKS = 8;    // 250ms x 8 = approx 2 sec

// ---------- display meter range ----------
static const float METER_MIN_DB = 10.0;
static const float METER_MAX_DB = 80.0;

// ---------- calibration offsets ----------
float calibrationOffsetDb[6] = {
  -84.0,   // 63Hz
  -84.0,   // 160Hz
  -84.0,   // 380Hz
  -84.0,   // 1kHz
  -84.0,   // 2.5kHz
  -84.0    // 6.3kHz
};

// ---------- band names ----------
static const char* BAND_NAMES[6] = {
  "&nbsp;&nbsp;&nbsp;63Hz", "&nbsp;&nbsp;&nbsp;160Hz", "&nbsp;&nbsp;&nbsp;380Hz", "&nbsp;&nbsp;&nbsp;1kHz", "&nbsp;&nbsp;&nbsp;2.5kHz", "&nbsp;&nbsp;&nbsp;6.3kHz"
};

// ---------- band definition ----------
struct BandDef {
  const char* name;
  double fLow;
  double fHigh;
};

static const BandDef bands[6] = {
  {"63Hz",   31.25,   93.75},
  {"160Hz",  93.75,  250.00},
  {"380Hz", 250.00,  625.00},
  {"1kHz",  625.00, 1625.00},
  {"2.5kHz",1625.00,4000.00},
  {"6.3kHz",4000.00,8000.00}
};

// ---------- I2S port ----------
static const i2s_port_t I2S_PORT = I2S_NUM_0;

// ---------- raw I2S buffer ----------
int32_t rawSamples[FFT_SIZE];

// ---------- FFT buffers ----------
double vReal[FFT_SIZE];
double vImag[FFT_SIZE];

ArduinoFFT<double> FFT = ArduinoFFT<double>(vReal, vImag, FFT_SIZE, SAMPLE_RATE);

// ============================================================
// 2. SYSTEM STATE
// ============================================================

struct SpectrumState {
  float bandDb[6];          // calibrated display dB
  float rawBandDb[6];       // raw relative dB before calibration
  float peakFreq;
  float peakDb;
  unsigned long lastUpdateMs;
  bool valid;
};

struct EnvironmentState {
  float temperature;
  float humidity;
  String tempStatus;
};

struct RoonState {
  String roonStatus;
  String roonVersion;
  String roonUptimeText;
  String rockVersion;
  String rockUptime;
  String storageFree;
  String rockIP;
};

struct SystemState {
  RoonState roon;
  EnvironmentState env;
  SpectrumState spectrum;
};

SystemState gState;

// ============================================================
// 3. GLOBALS
// ============================================================

WebServer server(80);

unsigned long lastRoonUpdate = 0;
unsigned long lastEnvUpdate  = 0;
unsigned long lastFftKick    = 0;

// averaging accumulators
float spectrumAccum[6] = {0, 0, 0, 0, 0, 0};
uint8_t spectrumAccumCount = 0;

float peakFreqAccum = 0.0;
float peakDbAccum   = 0.0;

// ============================================================
// 4. HELPER FUNCTIONS
// ============================================================

// ------------------------------------------------------------
// uptime sec -> "Xd Yh"
// ------------------------------------------------------------
String formatUptime(unsigned long sec)
{
  int days  = sec / 86400;
  int hours = (sec % 86400) / 3600;

  return String(days) + "d " + String(hours) + "h";
}

// ------------------------------------------------------------
// dB -> bar percent
// ------------------------------------------------------------
int dbToPercent(float db)
{
  if (db < METER_MIN_DB) db = METER_MIN_DB;
  if (db > METER_MAX_DB) db = METER_MAX_DB;

  return (int)(100.0f * (db - METER_MIN_DB) / (METER_MAX_DB - METER_MIN_DB));
}

// ------------------------------------------------------------
// power -> dB
// ------------------------------------------------------------
double powerToDb(double p)
{
  const double eps = 1e-20;
  return 10.0 * log10(p + eps);
}

// ------------------------------------------------------------
// bin -> frequency
// ------------------------------------------------------------
double binToFreq(uint16_t bin)
{
  return (double)bin * BIN_WIDTH;
}

// ------------------------------------------------------------
// inclusive lower / upper bin helpers
// ------------------------------------------------------------
uint16_t freqToBinFloor(double freq)
{
  int b = (int)floor(freq / BIN_WIDTH + 1e-9);
  if (b < 0) b = 0;
  if (b > (FFT_SIZE / 2 - 1)) b = FFT_SIZE / 2 - 1;
  return (uint16_t)b;
}

uint16_t freqToBinCeil(double freq)
{
  int b = (int)ceil(freq / BIN_WIDTH - 1e-9);
  if (b < 0) b = 0;
  if (b > (FFT_SIZE / 2 - 1)) b = FFT_SIZE / 2 - 1;
  return (uint16_t)b;
}

// ============================================================
// 5. ROON / ENVIRONMENT
// ============================================================

// ------------------------------------------------------------
// Roon status update
// ------------------------------------------------------------
void updateRoonStatus()
{
  HTTPClient http;
  http.begin(ROON_STATE_URL);
  http.addHeader("Content-Type", "application/x-www-form-urlencoded");

  int httpCode = http.POST("");

  if (httpCode > 0)
  {
    String payload = http.getString();

    DynamicJsonDocument doc(8192);
    DeserializationError error = deserializeJson(doc, payload);

    if (!error)
    {
      bool running = doc["data"]["state"]["roon_running"];

      gState.roon.roonStatus =
        running ? "ONLINE" : "OFFLINE";

      gState.roon.roonVersion =
        doc["data"]["state"]["roon_version"].as<String>();

      gState.roon.rockVersion =
        doc["data"]["device_display_version"].as<String>();

      gState.roon.rockIP =
        doc["data"]["interfaces"][0]["state"]["ip"].as<String>();

      unsigned long rockSec =
        String(doc["data"]["state"]["uptime"]).toInt();

      unsigned long roonSec =
        String(doc["data"]["state"]["roon_uptime"]).toInt();

      gState.roon.rockUptime = formatUptime(rockSec);
      gState.roon.roonUptimeText = formatUptime(roonSec);

      uint64_t used =
        doc["data"]["state"]["internal_storage"]["data_used"];

      uint64_t total =
        doc["data"]["state"]["internal_storage"]["data_total"];

      float freeGB =
        (float)(total - used) / 1024.0 / 1024.0;

      float totalGB =
        (float)total / 1024.0 / 1024.0;

      int percent = 0;
      if (total > 0) {
        percent = ((total - used) * 100) / total;
      }

      gState.roon.storageFree =
        String((int)freeGB)
        + " GB / "
        + String((int)totalGB)
        + " GB ("
        + String(percent)
        + "% available)";
    }
    else
    {
      gState.roon.roonStatus = "JSON ERROR";
    }
  }
  else
  {
    gState.roon.roonStatus = "NO RESPONSE";
  }

  http.end();
}

// ------------------------------------------------------------
// DHT11 update
// ------------------------------------------------------------
void updateEnvironment()
{
  float h = dht.readHumidity();
  float t = dht.readTemperature();

  if (!isnan(h) && !isnan(t))
  {
    gState.env.humidity = h;
    gState.env.temperature = t;
  }

  if (gState.env.temperature >= TEMP_ERROR)
  {
    gState.env.tempStatus = "ERROR";
  }
  else if (gState.env.temperature >= TEMP_WARNING)
  {
    gState.env.tempStatus = "WARNING";
  }
  else
  {
    gState.env.tempStatus = "OK";
  }
}

// ============================================================
// 6. INMP441 / FFT
// ============================================================

// ------------------------------------------------------------
// I2S init
// ------------------------------------------------------------
void initI2SMic()
{
  const i2s_config_t i2s_config = {
    .mode = (i2s_mode_t)(I2S_MODE_MASTER | I2S_MODE_RX),
    .sample_rate = (uint32_t)SAMPLE_RATE,
    .bits_per_sample = I2S_BITS_PER_SAMPLE_32BIT,
    .channel_format = I2S_CHANNEL_FMT_ONLY_LEFT,
    .communication_format = I2S_COMM_FORMAT_STAND_I2S,
    .intr_alloc_flags = ESP_INTR_FLAG_LEVEL1,
    .dma_buf_count = 8,
    .dma_buf_len = 256,
    .use_apll = false,
    .tx_desc_auto_clear = false,
    .fixed_mclk = 0
  };

  const i2s_pin_config_t pin_config = {
    .bck_io_num = I2S_BCLK_PIN,
    .ws_io_num = I2S_WS_PIN,
    .data_out_num = I2S_PIN_NO_CHANGE,
    .data_in_num = I2S_SD_PIN
  };

  i2s_driver_uninstall(I2S_PORT);

  esp_err_t err;
  err = i2s_driver_install(I2S_PORT, &i2s_config, 0, NULL);
  if (err != ESP_OK) {
    Serial.printf("ERROR: i2s_driver_install failed (%d)\n", err);
    while (1) delay(1000);
  }

  err = i2s_set_pin(I2S_PORT, &pin_config);
  if (err != ESP_OK) {
    Serial.printf("ERROR: i2s_set_pin failed (%d)\n", err);
    while (1) delay(1000);
  }

  i2s_zero_dma_buffer(I2S_PORT);
  Serial.println("I2S microphone initialized.");
}

// ------------------------------------------------------------
// read one mic block
// ------------------------------------------------------------
bool readMicBlock(int32_t* dst, size_t count)
{
  size_t bytesRead = 0;
  esp_err_t err = i2s_read(
    I2S_PORT,
    (void*)dst,
    count * sizeof(int32_t),
    &bytesRead,
    portMAX_DELAY
  );

  if (err != ESP_OK) {
    Serial.printf("ERROR: i2s_read failed (%d)\n", err);
    return false;
  }

  size_t samplesRead = bytesRead / sizeof(int32_t);
  return (samplesRead == count);
}

// ------------------------------------------------------------
// Prepare FFT input
// 1) convert raw sample
// 2) mean
// 3) DC remove
// 4) Hann window
// ------------------------------------------------------------
double prepareFFTInput(const int32_t* src, uint16_t n)
{
  double mean = 0.0;

  // pass 1 : mean
  for (uint16_t i = 0; i < n; i++) {
    double s = (double)(src[i] >> 8);
    mean += s;
  }
  mean /= (double)n;

  // pass 2 : DC remove + Hann
  for (uint16_t i = 0; i < n; i++) {
    double s = (double)(src[i] >> 8);
    s -= mean;

    double w = 0.5 * (1.0 - cos((2.0 * PI * i) / (n - 1)));
    vReal[i] = s * w;
    vImag[i] = 0.0;
  }

  return mean;
}

// ------------------------------------------------------------
// Find peak bin
// ------------------------------------------------------------
void findPeakBin(uint16_t& peakBin, double& peakMag)
{
  uint16_t startBin = EXCLUDE_BIN_1 ? 2 : 1;

  peakBin = startBin;
  peakMag = vReal[startBin];

  for (uint16_t i = startBin + 1; i < FFT_SIZE / 2; i++) {
    if (vReal[i] > peakMag) {
      peakMag = vReal[i];
      peakBin = i;
    }
  }
}

// ------------------------------------------------------------
// Integrate one band by power sum
// ------------------------------------------------------------
double integrateBandPower(double fLow, double fHigh, bool is63Band)
{
  uint16_t binStart = freqToBinCeil(fLow);
  uint16_t binEnd   = freqToBinFloor(fHigh);

  if (binEnd < binStart) return 0.0;

  double sumPower = 0.0;

  for (uint16_t bin = binStart; bin <= binEnd; bin++) {

    if (bin == 0) continue;
    if (EXCLUDE_BIN_1 && bin == 1) continue;
    if (is63Band && !INCLUDE_BIN2_IN_63HZ && bin == 2) continue;

    double mag = vReal[bin];
    sumPower += mag * mag;
  }

  return sumPower;
}

// ------------------------------------------------------------
// Publish averaged spectrum to gState
// ------------------------------------------------------------
void publishSpectrumAverage()
{
  if (spectrumAccumCount == 0) return;

  for (int i = 0; i < 6; i++) {
    float avgRaw = spectrumAccum[i] / spectrumAccumCount;

    gState.spectrum.rawBandDb[i] = avgRaw;
    gState.spectrum.bandDb[i]    = avgRaw + calibrationOffsetDb[i];

    spectrumAccum[i] = 0.0;
  }

  gState.spectrum.peakFreq = peakFreqAccum / spectrumAccumCount;
  gState.spectrum.peakDb   = peakDbAccum   / spectrumAccumCount;

  peakFreqAccum = 0.0;
  peakDbAccum   = 0.0;

  spectrumAccumCount = 0;
  gState.spectrum.lastUpdateMs = millis();
  gState.spectrum.valid = true;
}

// ------------------------------------------------------------
// One FFT update cycle
// ------------------------------------------------------------
void updateSpectrum()
{
  // 1) read one block
  if (!readMicBlock(rawSamples, FFT_SIZE)) {
    return;
  }

  // 2) prepare input
  prepareFFTInput(rawSamples, FFT_SIZE);

  // 3) FFT
  FFT.compute(FFT_FORWARD);
  FFT.complexToMagnitude();

  // 4) peak
  uint16_t peakBin;
  double peakMag;
  findPeakBin(peakBin, peakMag);

  float peakFreq = (float)binToFreq(peakBin);
  float peakDb   = (float)(20.0 * log10(peakMag + 1e-12));

  // 5) 6-band integration
  float bandRawDb[6];

  for (uint8_t i = 0; i < 6; i++) {
    bool is63Band = (i == 0);
    double p = integrateBandPower(bands[i].fLow, bands[i].fHigh, is63Band);
    bandRawDb[i] = (float)powerToDb(p);
  }

  // 6) accumulate
  for (int i = 0; i < 6; i++) {
    spectrumAccum[i] += bandRawDb[i];
  }

  peakFreqAccum += peakFreq;
  peakDbAccum   += peakDb;
  spectrumAccumCount++;

  // 7) publish every N blocks
  if (spectrumAccumCount >= SPECTRUM_AVG_BLOCKS) {
    publishSpectrumAverage();
  }
}

// ============================================================
// 7. HTML HELPERS
// ============================================================

// ------------------------------------------------------------
// Return health label for Roon system
// ------------------------------------------------------------
String buildSystemHealthHtml()
{
  if (gState.roon.roonStatus == "ONLINE") {
    return "<span class='ok'>OK</span>";
  }
  return "<span class='ng'>ERROR</span>";
}

// ------------------------------------------------------------
// Return temp status label
// ------------------------------------------------------------
String buildTempStatusHtml()
{
  if (gState.env.tempStatus == "OK") {
    return "<span class='ok'>OK</span>";
  }
  else if (gState.env.tempStatus == "WARNING") {
    return "<span class='warning'>WARNING</span>";
  }
  else {
    return "<span class='ng'>NG</span>";
  }
}

// ------------------------------------------------------------
// Build one spectrum row
// ------------------------------------------------------------
String buildSpectrumRow(const char* label, float db)
{
  String html;
  int width = dbToPercent(db);

  html += "<div class='spec-row'>";
  html += "<div class='spec-label'>";
  html += label;
  html += "</div>";

  html += "<div class='spec-bar-wrap'>";
  html += "<div class='spec-bar' style='width:";
  html += String(width);
  html += "%;'></div>";
  html += "</div>";

  html += "<div class='spec-value'>";
  html += String(db, 1);
  html += " dB</div>";

  html += "</div>";

  return html;
}

// ============================================================
// 8. WEB PAGE
// ============================================================

void handleRoot()
{
  String html;

  html += "<html>";
  html += "<head>";

  html += "<title>Audio System Monitoring</title>";
  html += "<meta http-equiv='refresh' content='";
  html += String(PAGE_REFRESH_SEC);
  html += "'>";

  html += "<meta name='viewport' content='width=device-width, initial-scale=1'>";

  html += "<style>";
  html += "body{font-family:Arial,Helvetica,sans-serif;margin:20px;background:#ffffff;color:#222;}";
  html += "h1{margin-bottom:8px;}";
  html += "h2{margin-top:28px;margin-bottom:10px;}";
  html += "table{width:100%;border-collapse:collapse;margin-bottom:10px;}";
  html += "td{padding:8px;border-bottom:1px solid #cccccc;vertical-align:top;}";
  html += ".ok{color:green;font-weight:bold;}";
  html += ".ng{color:red;font-weight:bold;}";
  html += ".warning{color:orange;font-weight:bold;}";
  html += ".spec-row{display:flex;align-items:center;margin:10px 0;}";
  html += ".spec-label{width:130px;font-weight:bold;}";
  html += ".spec-bar-wrap{flex:1;height:20px;background:#e0e0e0;border-radius:4px;overflow:hidden;margin-right:32px;}";
  html += ".spec-bar{height:100%;background:#4caf50;}";
  html += ".spec-value{width:90px;text-align:right;font-family:monospace;}";
  html += ".note{color:#666;font-size:12px;}";
  html += ".section-box{border:1px solid #dddddd;border-radius:8px;padding:16px;margin-top:16px;}";
  html += "</style>";

  html += "</head>";
  html += "<body>";

  // --------------------------------------------------------
  // title
  // --------------------------------------------------------
  html += "<h1>Audio System Monitoring</h1>";
  html += "<hr>";

  // Optional image
  html += "<img src='https://keroyon-audio.com/wp-content/uploads/2026/06/audio_dashboard.jpg' width='100%'>";

  // --------------------------------------------------------
  // system health
  // --------------------------------------------------------
  html += "<h2>System Health : ";
  html += buildSystemHealthHtml();
  html += "</h2>";

  html += "<p>Audit since update : ";
  html += String(millis() / 60000);
  html += " min.</p>";

  // --------------------------------------------------------
  // Roon ROCK section
  // --------------------------------------------------------
  html += "<div class='section-box'>";
  html += "<h2>Roon ROCK</h2>";
  html += "<table>";

  html += "<tr>";
  html += "<td>Roon OS</td>";
  html += "<td class='ok'>OK</td>";
  html += "<td>Version : ";
  html += gState.roon.rockVersion;
  html += " Running : ";
  html += gState.roon.rockUptime;
  html += "</td>";
  html += "</tr>";

  html += "<tr>";
  html += "<td>Roon Server</td>";
  if (gState.roon.roonStatus == "ONLINE") {
    html += "<td class='ok'>OK</td>";
  } else {
    html += "<td class='ng'>NG</td>";
  }
  html += "<td>Version : ";
  html += gState.roon.roonVersion;
  html += " Running : ";
  html += gState.roon.roonUptimeText;
  html += "</td>";
  html += "</tr>";

  html += "<tr>";
  html += "<td>Music Storage</td>";
  html += "<td class='ok'>OK</td>";
  html += "<td>";
  html += gState.roon.storageFree;
  html += "</td>";
  html += "</tr>";

  html += "<tr>";
  html += "<td>ROCK IP Address</td>";
  html += "<td></td>";
  html += "<td>";
  html += gState.roon.rockIP;
  html += "</td>";
  html += "</tr>";

  html += "<tr>";
  html += "<td>ROCK-side Temp.</td>";
  html += "<td>";
  html += buildTempStatusHtml();
  html += "</td>";
  html += "<td>";
  html += String(gState.env.temperature, 1);
  html += " degrees";
  html += "</td>";
  html += "</tr>";

  html += "<tr>";
  html += "<td>ROCK-side Humidity</td>";
  html += "<td class='ok'>OK</td>";
  html += "<td>";
  html += String(gState.env.humidity, 1);
  html += " %";
  html += "</td>";
  html += "</tr>";

  html += "<tr>";
  html += "<td>Playback Now</td>";
  html += "<td></td>";
  html += "<td>Not Implemented</td>";
  html += "</tr>";

  html += "</table>";
  html += "</div>";

  // --------------------------------------------------------
  // Audio Spectrum section
  // --------------------------------------------------------
  html += "<div class='section-box'>";
  html += "<h2>Fan Nose Spectrum</h2>";

  if (!gState.spectrum.valid)
  {
    html += "<p>Initializing spectrum monitor...</p>";
  }
  else
  {
    for (int i = 0; i < 6; i++) {
      html += buildSpectrumRow(BAND_NAMES[i], gState.spectrum.bandDb[i]);
    }

    html += "<p class='note'>";
    html += "Peak : ";
    html += String(gState.spectrum.peakFreq, 1);
    html += " Hz / ";
    html += String(gState.spectrum.peakDb, 1);
    html += " dB raw";
    html += "</p>";

    html += "<p class='note'>";
    html += "Spectrum update : every ";
    html += String(SPECTRUM_UPDATE_MS / 1000.0, 1);
    html += " sec, averaged over ";
    html += String(SPECTRUM_AVG_BLOCKS);
    html += " FFT blocks";
    html += "</p>";
  }

  html += "</div>";

  // --------------------------------------------------------
  // footer
  // --------------------------------------------------------
  html += "<hr>";
  html += "<small>";
  html += "ESP32 : ";
  html += WiFi.localIP().toString();
  html += " | RSSI ";
  html += String(WiFi.RSSI());
  html += " dBm";
  html += " | Heap ";
  html += String(ESP.getFreeHeap());
  html += " byte";
  html += "</small>";

  html += "</body>";
  html += "</html>";

  server.send(200, "text/html", html);
}

// ============================================================
// 9. SETUP
// ============================================================

void setup()
{
  Serial.begin(115200);
  delay(2000);

  Serial.println();
  Serial.println("============================================================");
  Serial.println("Audio_Monitor_R3 : boot start");
  Serial.println("============================================================");

  // ---------- initialize state ----------
  gState.roon.roonStatus     = "UNKNOWN";
  gState.roon.roonVersion    = "-";
  gState.roon.roonUptimeText = "-";
  gState.roon.rockVersion    = "-";
  gState.roon.rockUptime     = "-";
  gState.roon.storageFree    = "-";
  gState.roon.rockIP         = "-";

  gState.env.temperature = 0.0;
  gState.env.humidity    = 0.0;
  gState.env.tempStatus  = "OK";

  gState.spectrum.peakFreq = 0.0;
  gState.spectrum.peakDb   = 0.0;
  gState.spectrum.lastUpdateMs = 0;
  gState.spectrum.valid = false;

  for (int i = 0; i < 6; i++) {
    gState.spectrum.bandDb[i] = 0.0;
    gState.spectrum.rawBandDb[i] = 0.0;
  }

  // ---------- DHT ----------
  dht.begin();
  Serial.println("DHT11 READY");

  // ---------- WiFi ----------
  WiFi.mode(WIFI_STA);
  Serial.println("CONNECTING TO WIFI...");
  WiFi.begin(ssid, password);

  while (WiFi.status() != WL_CONNECTED)
  {
    delay(500);
    Serial.print(".");
  }

  Serial.println();
  Serial.println("WiFi CONNECTED");
  Serial.print("IP ADDRESS: ");
  Serial.println(WiFi.localIP());

  // ---------- I2S Mic ----------
  initI2SMic();

  // ---------- initial update ----------
  updateEnvironment();
  updateRoonStatus();

  // ---------- Web server ----------
  server.on("/", handleRoot);
  server.begin();
  Serial.println("WEB SERVER STARTED");

  Serial.println("BOOT COMPLETE");
}

// ============================================================
// 10. LOOP
// ============================================================

void loop()
{
  server.handleClient();

  unsigned long now = millis();

  // ---------- Roon ----------
  if (now - lastRoonUpdate >= ROON_UPDATE_MS)
  {
    lastRoonUpdate = now;
    updateRoonStatus();

    Serial.println("[ROON] status updated");
    Serial.println(gState.roon.roonStatus);
    Serial.println(gState.roon.roonVersion);
  }

  // ---------- Environment ----------
  if (now - lastEnvUpdate >= ENV_UPDATE_MS)
  {
    lastEnvUpdate = now;
    updateEnvironment();

    Serial.print("[ENV] Temp = ");
    Serial.print(gState.env.temperature, 1);
    Serial.print(" C, Humidity = ");
    Serial.print(gState.env.humidity, 1);
    Serial.println(" %");
  }

  // ---------- FFT ----------
  if (now - lastFftKick >= FFT_BLOCK_MS)
  {
    lastFftKick = now;
    updateSpectrum();

    if (gState.spectrum.valid) {
      Serial.print("[FFT] ");
      for (int i = 0; i < 6; i++) {
        Serial.print(BAND_NAMES[i]);
        Serial.print(":");
        Serial.print(gState.spectrum.bandDb[i], 1);
        if (i < 5) Serial.print(" / ");
      }
      Serial.println();
    }
  }
}

投稿者

KeroYon