namespace fs { class FS; }
using namespace fs;

#include <Wire.h>
#include <Adafruit_MLX90640.h>
#include <TFT_eSPI.h>
#include <FastLED.h>
#include <WiFi.h>
#include <WebServer.h>
#include <FS.h>
#include <SPIFFS.h>

#ifndef DISABLE_BLE
#include <BLEDevice.h>
#include <BLEServer.h>
#include <BLEUtils.h>
#include <BLE2902.h>
#endif

// ================= 📺 布局参数 =================
#define SCREEN_W  240
#define SCREEN_H  240
TFT_eSPI tft = TFT_eSPI(SCREEN_W, SCREEN_H);

#define IMG_W     144
#define IMG_H     108
#define IMG_X     40
#define IMG_Y     28

#define LEGEND_W  12
#define LEGEND_X  (SCREEN_W - LEGEND_W - 4)
#define LEGEND_Y  IMG_Y
#define LEGEND_H  IMG_H

#define STATUS_H  24
#define PANEL_H   32
#define PANEL_Y   (SCREEN_H - PANEL_H)
#define FONT_SM   1

// ================= 🔌 引脚 =================
const uint8_t btnPin = 1;
const uint8_t btnLedPin = 2;
const uint8_t ledPin = 42;
const uint8_t wsPin  = 48;
#define NUM_LEDS 2
#define I2C_SDA   8
#define I2C_SCL   9

// ================= 🌡️ 传感器 =================
Adafruit_MLX90640 mlx;
float frame[768], smooth[768], interp[IMG_W * IMG_H];
float tMin = 0, tMax = 0, tMinFixed = -10, tMaxFixed = 50;
bool autoRange = true;
float centerTemp = 0.0, hotSpotTemp = -999, coldSpotTemp = 999;
int hotSpotX = -1, hotSpotY = -1;
CRGB leds[NUM_LEDS];

// FPS
uint32_t lastFrameTime = 0;
float currentFPS = 0.0;
const float FPS_FILTER = 0.3f;

// ================= 📡 WiFi + WebServer =================
const char* WIFI_SSID = "WIFI";//默认无密码,连接自己WIFI添加密码即可
IPAddress localIP;
bool wifiConnected = false;
uint32_t lastWifiCheck = 0;
WebServer server(80);

// ================= 🔌 USB CDC =================
uint32_t lastSerialSend = 0;
const uint32_t SERIAL_SEND_INTERVAL = 1000;

// ================= 🔵 BLE =================
#ifndef DISABLE_BLE
#define BLE_SERVICE_UUID "4fafc201-1fb5-459e-8fcc-c5c9c331914b"
#define BLE_CHAR_UUID "beb5483e-36e1-4688-b7f5-ea07361b26a8"
#define BLE_DEVICE_NAME "Thermal-Pro"
BLEServer *pServer = nullptr;
BLECharacteristic *pCharacteristic = nullptr;
bool bleConnected = false;
uint32_t lastBleSend = 0;
const uint32_t BLE_SEND_INTERVAL = 1000;

class MyServerCallbacks : public BLEServerCallbacks {
  void onConnect(BLEServer* pServer) override { bleConnected = true; Serial.println("🔵 BLE Connected"); }
  void onDisconnect(BLEServer* pServer) override { bleConnected = false; Serial.println("🔵 BLE Disconnected"); pServer->startAdvertising(); }
};
#endif

// ================= 🚨 报警 =================
float alertThreshold = 40.0;
bool alertActive = false;
uint32_t lastAlertTime = 0;
const uint32_t ALERT_COOLDOWN = 3000;

// ================= 🎨 色板 =================
uint16_t ironbow565[256];
void buildIronbowTable() {
  for (int i = 0; i < 256; i++) {
    uint8_t r, g, b;
    if (i < 30) { r = 0; g = map(i, 0, 29, 0, 80); b = 255; }
    else if (i < 80) { r = map(i, 30, 79, 0, 200); g = map(i, 30, 79, 80, 255); b = map(i, 30, 79, 255, 50); }
    else if (i < 150) { r = 255; g = map(i, 80, 149, 255, 30); b = map(i, 80, 149, 50, 0); }
    else { r = 255; g = map(i, 150, 255, 30, 255); b = map(i, 150, 255, 0, 200); }
    ironbow565[i] = ((r & 0xF8) << 8) | ((g & 0xFC) << 3) | (b >> 3);
  }
}
inline uint8_t tempToIndex(float t, float minT, float maxT) {
  if (maxT - minT < 1.0f) maxT = minT + 10.0f;
  return (uint8_t)constrain((t - minT) / (maxT - minT) * 255.0f, 0, 255);
}

// ================= 🔧 温度处理 =================
float clampTemp(float t) { return constrain(t, -40, 150); }
void calcSmartRange(float* data, int len, float* outMin, float* outMax) {
  float minV = 999, maxV = -999;
  for (int i = 0; i < len; i++) { float t = clampTemp(data[i]); if (t < minV) minV = t; if (t > maxV) maxV = t; }
  float span = maxV - minV;
  if (span < 5.0f) { float mid = (minV + maxV) * 0.5f; *outMin = mid - 10.0f; *outMax = mid + 10.0f; }
  else { *outMin = minV - span * 0.05f; *outMax = maxV + span * 0.05f; }
}
void findHotColdSpots() {
  hotSpotTemp = -999; coldSpotTemp = 999; hotSpotX = -1; hotSpotY = -1;
  for (int i = 0; i < 768; i++) {
    float t = clampTemp(frame[i]);
    if (t > hotSpotTemp) { hotSpotTemp = t; hotSpotY = i / 32; hotSpotX = i % 32; }
    if (t < coldSpotTemp) coldSpotTemp = t;
  }
}
float avgCenterTemp() {
  float sum = 0; int cx = 16, cy = 12;
  for (int dy = -4; dy < 4; dy++) for (int dx = -4; dx < 4; dx++) sum += clampTemp(frame[(cy + dy) * 32 + (cx + dx)]);
  return sum / 64.0f;
}

// ================= 🖼️ 图像处理 =================
void smoothFrame() {
  const float w[3][3] = {{1,2,1},{2,4,2},{1,2,1}};
  for (int y = 1; y < 23; y++) for (int x = 1; x < 31; x++) {
    float sum = 0, wsum = 0;
    for (int dy = -1; dy <= 1; dy++) for (int dx = -1; dx <= 1; dx++) { sum += frame[(y + dy) * 32 + (x + dx)] * w[dy + 1][dx + 1]; wsum += w[dy + 1][dx + 1]; }
    smooth[y * 32 + x] = sum / wsum;
  }
  for (int i = 0; i < 32; i++) { smooth[i] = frame[i]; smooth[23 * 32 + i] = frame[23 * 32 + i]; smooth[i * 32] = frame[i * 32]; smooth[i * 32 + 31] = frame[i * 32 + 31]; }
}
void bicubicAdaptive() {
  const float colRatio = 23.0f / (IMG_W - 1), rowRatio = 31.0f / (IMG_H - 1), a = -0.5f;
  for (int sy = 0; sy < IMG_H; sy++) {
    float sc = sy * rowRatio;
    int c0 = constrain((int)sc - 1, 0, 30), c1 = constrain((int)sc, 0, 31), c2 = constrain((int)sc + 1, 0, 31), c3 = constrain((int)sc + 2, 0, 31);
    float fc = sc - (int)sc;
    float wc[4] = { a*fc*fc*fc - 2*a*fc*fc + a*fc, (a+2)*fc*fc*fc - (a+3)*fc*fc + 1, -(a+2)*fc*fc*fc + (2*a+3)*fc*fc - a*fc, -a*fc*fc*fc + a*fc*fc };
    for (int sx = 0; sx < IMG_W; sx++) {
      float sr = sx * colRatio;
      int r0 = constrain((int)sr - 1, 0, 22), r1 = constrain((int)sr, 0, 23), r2 = constrain((int)sr + 1, 0, 23), r3 = constrain((int)sr + 2, 0, 23);
      float fr = sr - (int)sr;
      float wr[4] = { a*fr*fr*fr - 2*a*fr*fr + a*fr, (a+2)*fr*fr*fr - (a+3)*fr*fr + 1, -(a+2)*fr*fr*fr + (2*a+3)*fr*fr - a*fr, -a*fr*fr*fr + a*fr*fr };
      bool edge = (r0==0||r3==23||c0==0||c3==31);
      if (edge) {
        int rr0 = constrain((int)sr, 0, 23), rr1 = min(rr0 + 1, 23), cc0 = constrain((int)sc, 0, 31), cc1 = min(cc0 + 1, 31);
        float fr2 = sr - rr0, fc2 = sc - cc0;
        float v00 = smooth[rr0*32+cc0], v01 = smooth[rr0*32+cc1], v10 = smooth[rr1*32+cc0], v11 = smooth[rr1*32+cc1];
        interp[sy*IMG_W+sx] = v00 + fc2*(v01-v00) + fr2*(v10-v00 + fc2*(v11-v10-v01+v00));
      } else {
        float result = 0;
        for (int m = 0; m < 4; m++) {
          float rowSum = 0;
          for (int n = 0; n < 4; n++) {
            int rr = (m==0)?r0:(m==1)?r1:(m==2)?r2:r3, cc = (n==0)?c0:(n==1)?c1:(n==2)?c2:c3;
            rowSum += smooth[rr*32+cc] * wc[n];
          }
          result += rowSum * wr[m];
        }
        interp[sy*IMG_W+sx] = result;
      }
    }
  }
}
void applySharpen(float strength = 0.25f) {
  static float sharp[IMG_W * IMG_H];
  for (int y = 1; y < IMG_H - 1; y++) for (int x = 1; x < IMG_W - 1; x++) {
    float c = interp[y*IMG_W+x], lap = 5*c - interp[(y-1)*IMG_W+x] - interp[(y+1)*IMG_W+x] - interp[y*IMG_W+(x-1)] - interp[y*IMG_W+(x+1)];
    sharp[y*IMG_W+x] = c + strength * (c - lap * 0.25f);
  }
  for (int i = 0; i < IMG_W; i++) { sharp[i] = interp[i]; sharp[(IMG_H-1)*IMG_W+i] = interp[(IMG_H-1)*IMG_W+i]; }
  for (int j = 0; j < IMG_H; j++) { sharp[j*IMG_W] = interp[j*IMG_W]; sharp[j*IMG_W+IMG_W-1] = interp[j*IMG_W+IMG_W-1]; }
  memcpy(interp, sharp, sizeof(interp));
}

// ================= 🎨 UI 绘制 =================
static uint16_t rowBuf[IMG_W];
void drawThermal(float mn, float mx) {
  tft.setAddrWindow(IMG_X, IMG_Y, IMG_W, IMG_H);
  for (int y = 0; y < IMG_H; y++) { for (int x = 0; x < IMG_W; x++) rowBuf[x] = ironbow565[tempToIndex(interp[y*IMG_W+x], mn, mx)]; tft.pushColors(rowBuf, IMG_W, false); }
  tft.endWrite();
}
void drawLegend(float mn, float mx) {
  tft.fillRect(LEGEND_X - 3, LEGEND_Y - 3, LEGEND_W + 6, LEGEND_H + 6, 0x18E3);
  tft.fillRect(LEGEND_X, LEGEND_Y, LEGEND_W, LEGEND_H, TFT_BLACK);
  for (int i = 0; i < LEGEND_H - 2; i++) { uint8_t idx = map(i, 0, LEGEND_H - 3, 255, 0); tft.drawFastHLine(LEGEND_X + 2, LEGEND_Y + 2 + i, LEGEND_W - 4, ironbow565[idx]); }
  tft.setTextColor(TFT_WHITE); tft.setTextSize(FONT_SM); tft.setTextDatum(TR_DATUM);
  tft.drawFastHLine(LEGEND_X - 2, LEGEND_Y + 2, 4, TFT_WHITE); tft.drawFloat(mx, 0, LEGEND_X - 6, LEGEND_Y + 4);
  tft.drawFastHLine(LEGEND_X - 2, LEGEND_Y + LEGEND_H/2, 4, TFT_WHITE); tft.drawFloat((mn+mx)*0.5f, 0, LEGEND_X - 6, LEGEND_Y + LEGEND_H/2 - 2);
  tft.drawFastHLine(LEGEND_X - 2, LEGEND_Y + LEGEND_H - 3, 4, TFT_WHITE); tft.drawFloat(mn, 0, LEGEND_X - 6, LEGEND_Y + LEGEND_H - 8);
  tft.setTextDatum(TL_DATUM);
}
void drawTopBar() {
  tft.fillRect(0, 0, SCREEN_W, STATUS_H, 0x1082);
  tft.drawLine(0, STATUS_H, SCREEN_W, STATUS_H, TFT_DARKGREY);
  tft.setTextColor(TFT_WHITE); tft.setTextSize(2); tft.setTextDatum(TC_DATUM);
  tft.drawString("THERMAL PRO", SCREEN_W / 2, 5);
  tft.setTextSize(FONT_SM); tft.setTextDatum(TR_DATUM);
  tft.setTextColor(autoRange ? TFT_GREEN : TFT_YELLOW);
  tft.drawString(autoRange ? "AUTO" : "MANUAL", SCREEN_W - 4, 8);
  tft.setTextDatum(TL_DATUM);
}
void drawHotMarker() {
  if (hotSpotX < 0) return;
  int sx = IMG_X + (int)(hotSpotY * (IMG_W - 1) / 23.0f + 0.5f), sy = IMG_Y + (int)(hotSpotX * (IMG_H - 1) / 31.0f + 0.5f);
  sx = constrain(sx, IMG_X + 6, IMG_X + IMG_W - 6); sy = constrain(sy, IMG_Y + 6, IMG_Y + IMG_H - 6);
  tft.drawLine(sx - 5, sy, sx + 5, sy, TFT_RED); tft.drawLine(sx, sy - 5, sx, sy + 5, TFT_RED); tft.fillCircle(sx, sy, 2, TFT_RED);
  tft.setTextColor(TFT_WHITE); tft.setTextSize(FONT_SM); tft.setTextDatum(TL_DATUM);
  int tx = sx + 8, ty = sy - 10; if (tx + 32 > IMG_X + IMG_W) tx = sx - 34; if (ty < IMG_Y + 4) ty = sy + 8;
  tft.drawFloat(hotSpotTemp, 1, tx, ty); tft.drawString("C", tx + 28, ty);
}
void drawCrosshair() {
  int cx = IMG_X + IMG_W / 2, cy = IMG_Y + IMG_H / 2, arm = 10;
  tft.drawLine(max(cx - arm, IMG_X), cy, cx - 2, cy, TFT_WHITE); tft.drawLine(cx + 2, cy, min(cx + arm, IMG_X + IMG_W - 1), cy, TFT_WHITE);
  tft.drawLine(cx, max(cy - arm, IMG_Y), cx, cy - 2, TFT_WHITE); tft.drawLine(cx, cy + 2, cx, min(cy + arm, IMG_Y + IMG_H - 1), TFT_WHITE);
  tft.fillCircle(cx, cy, 2, TFT_WHITE);
}
void drawAvgTemp() {
  int y = IMG_Y + IMG_H + 8, centerX = IMG_X + IMG_W / 2;
  tft.fillRect(centerX - 35, y - 2, 70, 20, TFT_BLACK);
  tft.setTextColor(TFT_GREEN, TFT_BLACK); tft.setTextSize(2); tft.setTextDatum(TC_DATUM);
  tft.drawFloat(centerTemp, 1, centerX, y); tft.drawString("C", centerX + 54, y); tft.setTextDatum(TL_DATUM);
}
void drawBottomPanel() {
  tft.fillRect(0, PANEL_Y, SCREEN_W, PANEL_H, 0x0841); tft.drawLine(0, PANEL_Y, SCREEN_W, PANEL_Y, TFT_DARKGREY);
  int line1 = PANEL_Y + 6, line2 = PANEL_Y + 20;
  tft.setTextColor(TFT_RED); tft.setTextSize(FONT_SM); tft.setTextDatum(TL_DATUM);
  tft.drawString("MAX ", 6, line1); tft.drawFloat(hotSpotTemp, 1, 42, line1); tft.drawString("C", 78, line1);
  tft.setTextColor(TFT_CYAN); tft.setTextDatum(TR_DATUM);
  tft.drawString(" MIN", SCREEN_W - 6, line1); tft.drawFloat(coldSpotTemp, 1, SCREEN_W - 42, line1); tft.drawString("C", SCREEN_W - 6, line1); tft.setTextDatum(TL_DATUM);
  tft.setTextColor(currentFPS >= 15 ? TFT_GREEN : TFT_YELLOW);
  tft.drawString("FPS:", 6, line2); tft.drawFloat(currentFPS, 0, 30, line2);
  tft.setTextColor(wifiConnected ? TFT_WHITE : TFT_DARKGREY); tft.setTextDatum(TR_DATUM);
  if (wifiConnected) { tft.drawString("IP:", SCREEN_W - 90, line2); tft.setTextColor(TFT_CYAN); tft.drawString(localIP.toString().c_str(), SCREEN_W - 6, line2); }
  else { tft.drawString("CCIT-WLAN", SCREEN_W - 6, line2); }
  tft.setTextDatum(TL_DATUM);
}
void drawFrame() { tft.drawRect(IMG_X - 2, IMG_Y - 2, IMG_W + 4, IMG_H + 4, TFT_DARKGREY); tft.drawRect(IMG_X - 1, IMG_Y - 1, IMG_W + 2, IMG_H + 2, TFT_WHITE); }

// ================= 🌐 精简网页 =================
const char* WEB_PAGE = R"rawliteral(
<!DOCTYPE html><html lang="zh-CN"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1.0">
<title>Thermal Pro</title>
<style>
body{background:#0b0d10;color:#c8ccd4;font-family:system-ui,sans-serif;padding:10px;margin:0}
.header{display:flex;justify-content:space-between;padding:8px 12px;background:#15181e;border-radius:8px;margin-bottom:10px}
.title{font-weight:600;color:#00e5ff}.badge{padding:2px 6px;border-radius:12px;background:rgba(0,229,255,0.1);color:#00e5ff;font-size:0.8rem}
.grid{display:grid;grid-template-columns:1fr 1fr;gap:10px}
.card{background:#15181e;border-radius:8px;padding:12px}
.card h3{margin:0 0 8px 0;color:#00e5ff;font-size:1rem}
.temp-grid{display:grid;grid-template-columns:repeat(3,1fr);gap:4px;text-align:center}
.temp-val{font-size:1.3rem;font-weight:700}.temp-max{color:#ff3b30}.temp-min{color:#7dd3fc}.temp-avg{color:#34c759}
.threshold{display:flex;gap:6px;margin-top:6px}
.threshold input{flex:1;padding:4px 8px;background:#0f1216;border:1px solid #2a2e36;border-radius:4px;color:#fff}
.threshold button{padding:4px 10px;background:#00e5ff;color:#000;border:none;border-radius:4px;font-weight:600;cursor:pointer}
.alert{padding:6px 10px;background:rgba(255,59,48,0.15);border:1px solid #ff3b30;border-radius:4px;color:#ff3b30;margin-top:8px;display:none}
.alert.show{display:block}
.info-list{list-style:none;font-size:0.85rem;margin:0;padding:0}
.info-list li{display:flex;justify-content:space-between;padding:3px 0;border-bottom:1px dashed rgba(255,255,255,0.1)}
</style></head><body>
<div class="header"><div class="title">🔥 Thermal Pro</div><div><span class="badge" id="wifiBadge">📡</span><span class="badge" id="fpsBadge">FPS:--</span></div></div>
<div class="grid">
<div class="card"><h3>🌡️ Temperature</h3><div class="temp-grid">
<div><div style="color:#888;font-size:0.8rem">MAX</div><div class="temp-val temp-max" id="maxTemp">--.-</div><div style="color:#888;font-size:0.8rem">C</div></div>
<div><div style="color:#888;font-size:0.8rem">AVG</div><div class="temp-val temp-avg" id="avgTemp">--.-</div><div style="color:#888;font-size:0.8rem">C</div></div>
<div><div style="color:#888;font-size:0.8rem">MIN</div><div class="temp-val temp-min" id="minTemp">--.-</div><div style="color:#888;font-size:0.8rem">C</div></div>
</div><div class="threshold"><input type="number" id="thresholdInput" step="0.1" value="40.0"><button onclick="setThreshold()">Set</button></div><div class="alert" id="alertBox">⚠️ TEMP EXCEEDED!</div></div>
<div class="card"><h3>📊 System</h3><ul class="info-list">
<li><span>IP</span><span id="devIp">--.--.--.--</span></li><li><span>WiFi</span><span id="wifiStatus">--</span></li>
<li><span>USB</span><span style="color:#34c759">● Active</span></li><li><span>FPS</span><span id="frameRate">--</span></li>
<li><span>Mode</span><span id="rangeMode">AUTO</span></li><li><span>Threshold</span><span id="currentThreshold">40.0</span>C</li>
</ul></div></div><div style="text-align:center;padding:10px;color:#666;font-size:0.8rem">Thermal Pro | Auto-refresh 1s</div>
<script>
const API='/api';let alertActive=false;
async function fetchData(){try{const r=await fetch(API+'/data',{cache:'no-store'});const d=await r.json();
document.getElementById('maxTemp').textContent=d.hot.toFixed(1);
document.getElementById('avgTemp').textContent=d.avg.toFixed(1);
document.getElementById('minTemp').textContent=d.cold.toFixed(1);
document.getElementById('fpsBadge').textContent='FPS:'+d.fps.toFixed(0);
document.getElementById('frameRate').textContent=d.fps.toFixed(1)+' FPS';
document.getElementById('devIp').textContent=d.ip||'--';
document.getElementById('wifiStatus').textContent=d.wifi?'Connected':'Disconnected';
document.getElementById('rangeMode').textContent=d.auto?'AUTO':'MANUAL';
if(d.hot>d.threshold&&!alertActive){alertActive=true;document.getElementById('alertBox').classList.add('show');
fetch(API+'/alert',{method:'POST',body:JSON.stringify({msg:'ALERT:'+d.hot.toFixed(1)+'C'})}).catch(()=>{});
}else if(d.hot<=d.threshold&&alertActive){alertActive=false;document.getElementById('alertBox').classList.remove('show');}
}catch(e){console.warn('Error:',e)}}
function setThreshold(){const v=parseFloat(document.getElementById('thresholdInput').value);
if(!isNaN(v)&&v>=-40&&v<=150){fetch(API+'/threshold',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({threshold:v})})
.then(()=>{document.getElementById('currentThreshold').textContent=v}).catch(()=>{});}}
fetchData();setInterval(fetchData,1000);
</script></body></html>
)rawliteral";

// ================= 📡 API 接口 =================
void sendCorsHeaders() {
  server.sendHeader("Access-Control-Allow-Origin", "*");
  server.sendHeader("Access-Control-Allow-Methods", "GET, POST, OPTIONS");
  server.sendHeader("Access-Control-Allow-Headers", "Content-Type");
  server.sendHeader("Connection", "close");
  server.sendHeader("Cache-Control", "no-cache, no-store, must-revalidate");
}

void handleRoot() {
  Serial.println("🌐 [HTTP] GET /");
  sendCorsHeaders();
  server.send(200, "text/html", WEB_PAGE);
}

void handleApiData() {
  Serial.println("🌐 [HTTP] GET /api/data");
  sendCorsHeaders();
  float h = isnan(hotSpotTemp) ? 0.0 : hotSpotTemp;
  float a = isnan(centerTemp) ? 0.0 : centerTemp;
  float c = isnan(coldSpotTemp) ? 0.0 : coldSpotTemp;
  float f = isnan(currentFPS) ? 0.0 : currentFPS;
  String json = "{";
  json += "\"hot\":" + String(h, 1) + ",";
  json += "\"avg\":" + String(a, 1) + ",";
  json += "\"cold\":" + String(c, 1) + ",";
  json += "\"fps\":" + String(f, 1) + ",";
  json += "\"ip\":\"" + (wifiConnected ? localIP.toString() : "0.0.0.0") + "\",";
  json += "\"wifi\":" + String(wifiConnected ? "true" : "false") + ",";
#ifndef DISABLE_BLE
  json += "\"ble\":" + String(bleConnected ? "true" : "false") + ",";
#else
  json += "\"ble\":\"disabled\",";
#endif
  json += "\"auto\":" + String(autoRange ? "true" : "false") + ",";
  json += "\"threshold\":" + String(alertThreshold, 1);
  json += "}";
  Serial.printf("📤 JSON: %s\n", json.c_str());
  server.send(200, "application/json", json);
}

void handleSetThreshold() {
  Serial.println("🌐 [HTTP] POST /api/threshold");
  sendCorsHeaders();
  if (server.hasArg("plain")) {
    String body = server.arg("plain");
    int start = body.indexOf("threshold");
    if (start >= 0) {
      int valStart = body.indexOf(":", start) + 1;
      int valEnd = body.indexOf(",", valStart); if (valEnd < 0) valEnd = body.indexOf("}", valStart);
      if (valStart < valEnd) {
        float newThresh = body.substring(valStart, valEnd).toFloat();
        if (newThresh >= -40 && newThresh <= 150) { alertThreshold = newThresh; Serial.printf("✅ Threshold: %.1f\n", alertThreshold); server.send(200, "application/json", "{\"ok\":true}"); return; }
      }
    }
  }
  server.send(400, "application/json", "{\"ok\":false}");
}

void handleAlert() {
  Serial.println("🌐 [HTTP] POST /api/alert");
  sendCorsHeaders();
  if (server.hasArg("plain")) {
    String body = server.arg("plain");
    int msgStart = body.indexOf("msg");
    if (msgStart >= 0) {
      int valStart = body.indexOf(":", msgStart) + 2;
      int valEnd = body.indexOf("\"", valStart);
      if (valStart < valEnd) {
        String alertMsg = body.substring(valStart, valEnd);
        Serial.println("[ALERT] " + alertMsg);
#ifndef DISABLE_BLE
        if (bleConnected && pCharacteristic) { pCharacteristic->setValue(("[ALERT] " + alertMsg).c_str()); pCharacteristic->notify(); }
#endif
      }
    }
  }
  server.send(200, "application/json", "{\"ok\":true}");
}

void setupWebServer() {
  server.on("/", handleRoot);
  server.on("/api/data", handleApiData);
  server.on("/api/threshold", HTTP_POST, handleSetThreshold);
  server.on("/api/alert", HTTP_POST, handleAlert);
  server.begin();
  Serial.println("🌐 WebServer started on port 80");
}

// ================= 🔌 USB CDC =================
void sendSerialData() {
  if (millis() - lastSerialSend >= SERIAL_SEND_INTERVAL) {
    lastSerialSend = millis();
    Serial.printf("T:%.1f,%.1f,%.1f,%d,%d,%.1f,%d\n", centerTemp, hotSpotTemp, coldSpotTemp, hotSpotX, hotSpotY, alertThreshold, (hotSpotTemp > alertThreshold) ? 1 : 0);
  }
}

// ================= 🔵 BLE =================
#ifndef DISABLE_BLE
void sendBleData() {
  if (bleConnected && pCharacteristic && millis() - lastBleSend >= BLE_SEND_INTERVAL) {
    lastBleSend = millis();
    String msg = "T:" + String(centerTemp,1) + "," + String(hotSpotTemp,1) + "," + String(coldSpotTemp,1) + "," + hotSpotX + "," + hotSpotY + "," + String(alertThreshold,1) + "," + ((hotSpotTemp > alertThreshold) ? "1" : "0") + "\n";
    pCharacteristic->setValue(msg.c_str());
    pCharacteristic->notify();
  }
}
void setupBLE() {
  Serial.println("🔵 BLE: Initializing...");
  delay(100);
  BLEDevice::init(BLE_DEVICE_NAME);
  pServer = BLEDevice::createServer();
  pServer->setCallbacks(new MyServerCallbacks());
  BLEService *pService = pServer->createService(BLE_SERVICE_UUID);
  pCharacteristic = pService->createCharacteristic(BLE_CHAR_UUID, BLECharacteristic::PROPERTY_READ | BLECharacteristic::PROPERTY_WRITE | BLECharacteristic::PROPERTY_NOTIFY);
  pCharacteristic->addDescriptor(new BLE2902());
  pService->start();
  BLEAdvertising *pAdvertising = BLEDevice::getAdvertising();
  pAdvertising->addServiceUUID(BLE_SERVICE_UUID);
  pAdvertising->setScanResponse(true);
  BLEDevice::startAdvertising();
  Serial.println("🔵 BLE: Advertising '" + String(BLE_DEVICE_NAME) + "'");
}
#endif

// ================= 🚨 报警 =================
void checkAlert() {
  if (hotSpotTemp > alertThreshold && millis() - lastAlertTime > ALERT_COOLDOWN) {
    lastAlertTime = millis(); alertActive = true;
    String alertMsg = "[ALERT] TEMP:" + String(hotSpotTemp,1) + "C > " + String(alertThreshold,1) + "C @ (" + hotSpotX + "," + hotSpotY + ")";
    Serial.println(alertMsg);
#ifndef DISABLE_BLE
    if (bleConnected && pCharacteristic) { pCharacteristic->setValue(alertMsg.c_str()); pCharacteristic->notify(); }
#endif
  } else if (hotSpotTemp <= alertThreshold) { alertActive = false; }
}

// ================= 🚀 初始化 =================
void setup() {
  Serial.begin(115200);
  while(!Serial) delay(10);
  Serial.println("\n🔥 Thermal Pro v4.1 | ESP32-S3");

  // 🔌 WiFi
  WiFi.mode(WIFI_STA);
  WiFi.begin(WIFI_SSID);
  Serial.print("📡 WiFi: Connecting to "); Serial.println(WIFI_SSID);
  uint32_t wifiStart = millis();
  while (WiFi.status() != WL_CONNECTED && millis() - wifiStart < 8000) { delay(250); Serial.print("."); }
  if (WiFi.status() == WL_CONNECTED) { localIP = WiFi.localIP(); wifiConnected = true; Serial.printf("\n✅ WiFi: %s\n", localIP.toString().c_str()); }
  else { wifiConnected = false; Serial.println("\n⚠️ WiFi timeout"); }

  setupWebServer();

#ifndef DISABLE_BLE
  setupBLE();
#endif

  // 传感器
  Wire.begin(I2C_SDA, I2C_SCL, 400000);
  Wire.setClock(1000000);
  if (!mlx.begin(MLX90640_I2CADDR_DEFAULT, &Wire)) { while(1) { delay(100); tft.fillScreen(TFT_RED); } }
  mlx.setRefreshRate(MLX90640_16_HZ);
  Serial.println("🌡️ MLX90640 OK");

  // 引脚
  pinMode(btnPin, INPUT_PULLUP);
  pinMode(btnLedPin, INPUT_PULLUP);
  pinMode(ledPin, OUTPUT);
  digitalWrite(ledPin, HIGH);
  FastLED.addLeds<WS2812, wsPin, GRB>(leds, NUM_LEDS);
  FastLED.setBrightness(100);

  // 屏幕
  tft.init(); tft.setRotation(2); tft.fillScreen(TFT_BLACK);
  buildIronbowTable();
  drawTopBar(); drawFrame(); drawLegend(tMinFixed, tMaxFixed);
  Serial.println("🎨 UI Ready");
}

// ================= 🔄 主循环 =================
void loop() {
  server.handleClient();

  if (millis() - lastWifiCheck > 5000) {
    lastWifiCheck = millis();
    if (WiFi.status() != WL_CONNECTED) { wifiConnected = false; WiFi.begin(WIFI_SSID); }
    else if (!wifiConnected) { localIP = WiFi.localIP(); wifiConnected = true; }
  }

  if (mlx.getFrame(frame) == 0) {
    uint32_t now = millis();
    float dt = (now - lastFrameTime) / 1000.0f;
    lastFrameTime = now;
    if (dt > 0.01f && dt < 1.0f) currentFPS = currentFPS * (1-FPS_FILTER) + (1.0f/dt) * FPS_FILTER;

    if (autoRange) calcSmartRange(frame, 768, &tMin, &tMax);
    else { tMin = tMinFixed; tMax = tMaxFixed; }
    findHotColdSpots();
    centerTemp = avgCenterTemp();
    smoothFrame(); bicubicAdaptive(); applySharpen(0.25f);

    drawThermal(tMin, tMax);
    drawCrosshair();
    drawHotMarker();
    drawAvgTemp();
    drawBottomPanel();

    uint8_t hue = map(constrain(centerTemp, 15, 45), 15, 45, 180, 0);
    leds[0] = leds[1] = CHSV(hue, 255, 150);
    FastLED.show();

    static bool ledState = true, lastBtnLed = HIGH;
    static uint32_t btnLedDown = 0;
    bool bl = digitalRead(btnLedPin);
    if (bl == LOW && lastBtnLed == HIGH) btnLedDown = millis();
    if (bl == HIGH && lastBtnLed == LOW && millis() - btnLedDown < 400) { ledState = !ledState; digitalWrite(ledPin, ledState ? HIGH : LOW); }
    lastBtnLed = bl;

    checkAlert();
    sendSerialData();
#ifndef DISABLE_BLE
    sendBleData();
#endif
  }

  static bool lastBtn = HIGH;
  static uint32_t btnDown = 0;
  bool b = digitalRead(btnPin);
  if (b == LOW && lastBtn == HIGH) btnDown = millis();
  if (b == HIGH && lastBtn == LOW && millis() - btnDown < 500) { autoRange = !autoRange; drawTopBar(); }
  if (b == LOW && millis() - btnDown > 800) {
    #ifdef TFT_BL
    static bool bl = true; bl = !bl;
    digitalWrite(TFT_BL, bl ? TFT_BACKLIGHT_ON : !TFT_BACKLIGHT_ON);
    btnDown = millis();
    #endif
  }
  lastBtn = b;
}

优化颜色校准


namespace fs { class FS; }
using namespace fs;

#include <Wire.h>
#include <Adafruit_MLX90640.h>
#include <TFT_eSPI.h>
#include <FastLED.h>
#include <WiFi.h>
#include <WebServer.h>
#include <FS.h>
#include <SPIFFS.h>

#ifndef DISABLE_BLE
#include <BLEDevice.h>
#include <BLEServer.h>
#include <BLEUtils.h>
#include <BLE2902.h>
#endif

// ================= 📺 布局参数 =================
#define SCREEN_W  240
#define SCREEN_H  240
TFT_eSPI tft = TFT_eSPI(SCREEN_W, SCREEN_H);

#define IMG_W     144
#define IMG_H     108
#define IMG_X     40
#define IMG_Y     28

#define LEGEND_W  12
#define LEGEND_X  (SCREEN_W - LEGEND_W - 4)
#define LEGEND_Y  IMG_Y
#define LEGEND_H  IMG_H

#define STATUS_H  24
#define PANEL_H   32
#define PANEL_Y   (SCREEN_H - PANEL_H)
#define FONT_SM   1

// ================= 🔌 引脚 =================
const uint8_t btnPin = 1;
const uint8_t btnLedPin = 2;
const uint8_t ledPin = 42;
const uint8_t wsPin  = 48;
#define NUM_LEDS 2
#define I2C_SDA   8
#define I2C_SCL   9

// ================= 🌡️ 传感器 =================
Adafruit_MLX90640 mlx;
float frame[768], smooth[768], interp[IMG_W * IMG_H];
float tMin = 0, tMax = 0, tMinFixed = -10, tMaxFixed = 60;
bool autoRange = true;
float centerTemp = 0.0, hotSpotTemp = -999, coldSpotTemp = 999;
int hotSpotX = -1, hotSpotY = -1;
CRGB leds[NUM_LEDS];

// FPS
uint32_t lastFrameTime = 0;
float currentFPS = 0.0;
const float FPS_FILTER = 0.3f;

// ================= 📡 WiFi + WebServer =================
const char* WIFI_SSID = "WIFI";
IPAddress localIP;
bool wifiConnected = false;
uint32_t lastWifiCheck = 0;
WebServer server(80);

// ================= 🔌 USB CDC =================
uint32_t lastSerialSend = 0;
const uint32_t SERIAL_SEND_INTERVAL = 1000;

// ================= 🔵 BLE =================
#ifndef DISABLE_BLE
#define BLE_SERVICE_UUID "4fafc201-1fb5-459e-8fcc-c5c9c331914b"
#define BLE_CHAR_UUID "beb5483e-36e1-4688-b7f5-ea07361b26a8"
#define BLE_DEVICE_NAME "Thermal-Pro"
BLEServer *pServer = nullptr;
BLECharacteristic *pCharacteristic = nullptr;
bool bleConnected = false;
uint32_t lastBleSend = 0;
const uint32_t BLE_SEND_INTERVAL = 1000;

class MyServerCallbacks : public BLEServerCallbacks {
  void onConnect(BLEServer* pServer) override { bleConnected = true; Serial.println("🔵 BLE Connected"); }
  void onDisconnect(BLEServer* pServer) override { bleConnected = false; Serial.println("🔵 BLE Disconnected"); pServer->startAdvertising(); }
};
#endif

// ================= 🚨 报警 =================
float alertThreshold = 40.0;
bool alertActive = false;
uint32_t lastAlertTime = 0;
const uint32_t ALERT_COOLDOWN = 3000;

// ================= 铁色热像仪配色================
uint16_t ironbow565[256];
void buildIronbowTable() {
  for (int i = 0; i < 256; i++) {
    uint8_t r, g, b;

    if (i < 60) {
      // 冷:深蓝
      r = 0;
      g = 0;
      b = 255;
    }
    else if (i < 120) {
      // 中冷:青
      r = 0;
      g = map(i, 60, 119, 0, 255);
      b = 255;
    }
    else if (i < 180) {
      // 常温:青 → 黄
      r = map(i, 120, 179, 0, 255);
      g = 255;
      b = map(i, 120, 179, 255, 0);
    }
    else {
      // 高温:黄 → 橙 → 红
      r = 255;
      g = map(i, 180, 255, 255, 40);
      b = 0;
    }

    ironbow565[i] = ((r & 0xF8) << 8) | ((g & 0xFC) << 3) | (b >> 3);
  }
}

// 温度映射
inline uint8_t tempToIndex(float t, float minT, float maxT) {
  float range = maxT - minT;
  if (range < 10.0f) range = 10.0f;
  float val = (t - minT) / range;
  val = val * 255.0f;
  return (uint8_t)constrain(val, 0, 255);
}


// ================= 🔧 温度处理 =================
float clampTemp(float t) { return constrain(t, -40, 150); }
void calcSmartRange(float* data, int len, float* outMin, float* outMax) {
  float minV = 999, maxV = -999;
  for (int i = 0; i < len; i++) { float t = clampTemp(data[i]); if (t < minV) minV = t; if (t > maxV) maxV = t; }
  float span = maxV - minV;
  if (span < 5.0f) { float mid = (minV + maxV) * 0.5f; *outMin = mid - 10.0f; *outMax = mid + 10.0f; }
  else { *outMin = minV - span * 0.05f; *outMax = maxV + span * 0.05f; }
}
void findHotColdSpots() {
  hotSpotTemp = -999; coldSpotTemp = 999; hotSpotX = -1; hotSpotY = -1;
  for (int i = 0; i < 768; i++) {
    float t = clampTemp(frame[i]);
    if (t > hotSpotTemp) { hotSpotTemp = t; hotSpotY = i / 32; hotSpotX = i % 32; }
    if (t < coldSpotTemp) coldSpotTemp = t;
  }
}
float avgCenterTemp() {
  float sum = 0; 
  int cx = 16, cy = 12;
  // 8x8=64个点,除以 64.0f
  for (int dy = -4; dy < 4; dy++) {
    for (int dx = -4; dx < 4; dx++) {
      sum += clampTemp(frame[(cy + dy) * 32 + (cx + dx)]);
    }
  }
  return sum / 64.0f; 
}

// ================= 🖼️ 图像处理 =================
void smoothFrame() {
  const float w[3][3] = {{1,2,1},{2,4,2},{1,2,1}};
  for (int y = 1; y < 23; y++) for (int x = 1; x < 31; x++) {
    float sum = 0, wsum = 0;
    for (int dy = -1; dy <= 1; dy++) for (int dx = -1; dx <= 1; dx++) { sum += frame[(y + dy) * 32 + (x + dx)] * w[dy + 1][dx + 1]; wsum += w[dy + 1][dx + 1]; }
    smooth[y * 32 + x] = sum / wsum;
  }
  for (int i = 0; i < 32; i++) { smooth[i] = frame[i]; smooth[23 * 32 + i] = frame[23 * 32 + i]; smooth[i * 32] = frame[i * 32]; smooth[i * 32 + 31] = frame[i * 32 + 31]; }
}
void bicubicAdaptive() {
  const float colRatio = 23.0f / (IMG_W - 1), rowRatio = 31.0f / (IMG_H - 1), a = -0.5f;
  for (int sy = 0; sy < IMG_H; sy++) {
    float sc = sy * rowRatio;
    int c0 = constrain((int)sc - 1, 0, 30), c1 = constrain((int)sc, 0, 31), c2 = constrain((int)sc + 1, 0, 31), c3 = constrain((int)sc + 2, 0, 31);
    float fc = sc - (int)sc;
    float wc[4] = { a*fc*fc*fc - 2*a*fc*fc + a*fc, (a+2)*fc*fc*fc - (a+3)*fc*fc + 1, -(a+2)*fc*fc*fc + (2*a+3)*fc*fc - a*fc, -a*fc*fc*fc + a*fc*fc };
    for (int sx = 0; sx < IMG_W; sx++) {
      float sr = sx * colRatio;
      int r0 = constrain((int)sr - 1, 0, 22), r1 = constrain((int)sr, 0, 23), r2 = constrain((int)sr + 1, 0, 23), r3 = constrain((int)sr + 2, 0, 23);
      float fr = sr - (int)sr;
      float wr[4] = { a*fr*fr*fr - 2*a*fr*fr + a*fr, (a+2)*fr*fr*fr - (a+3)*fr*fr + 1, -(a+2)*fr*fr*fr + (2*a+3)*fr*fr - a*fr, -a*fr*fr*fr + a*fr*fr };
      bool edge = (r0==0||r3==23||c0==0||c3==31);
      if (edge) {
        int rr0 = constrain((int)sr, 0, 23), rr1 = min(rr0 + 1, 23), cc0 = constrain((int)sc, 0, 31), cc1 = min(cc0 + 1, 31);
        float fr2 = sr - rr0, fc2 = sc - cc0;
        float v00 = smooth[rr0*32+cc0], v01 = smooth[rr0*32+cc1], v10 = smooth[rr1*32+cc0], v11 = smooth[rr1*32+cc1];
        interp[sy*IMG_W+sx] = v00 + fc2*(v01-v00) + fr2*(v10-v00 + fc2*(v11-v10-v01+v00));
      } else {
        float result = 0;
        for (int m = 0; m < 4; m++) {
          float rowSum = 0;
          for (int n = 0; n < 4; n++) {
            int rr = (m==0)?r0:(m==1)?r1:(m==2)?r2:r3, cc = (n==0)?c0:(n==1)?c1:(n==2)?c2:c3;
            rowSum += smooth[rr*32+cc] * wc[n];
          }
          result += rowSum * wr[m];
        }
        interp[sy*IMG_W+sx] = result;
      }
    }
  }
}

void applySharpen(float strength = 0.25f) {
  static float sharp[IMG_W * IMG_H];
  for (int y = 1; y < IMG_H - 1; y++) for (int x = 1; x < IMG_W - 1; x++) {
    float c = interp[y*IMG_W+x], lap = 5*c - interp[(y-1)*IMG_W+x] - interp[(y+1)*IMG_W+x] - interp[y*IMG_W+(x-1)] - interp[y*IMG_W+(x+1)];
    sharp[y*IMG_W+x] = c + strength * (c - lap * 0.25f);
  }
  for (int i = 0; i < IMG_W; i++) { sharp[i] = interp[i]; sharp[(IMG_H-1)*IMG_W+i] = interp[(IMG_H-1)*IMG_W+i]; }
  for (int j = 0; j < IMG_H; j++) { sharp[j*IMG_W] = interp[j*IMG_W]; sharp[j*IMG_W+IMG_W-1] = interp[j*IMG_W+IMG_W-1]; }
  memcpy(interp, sharp, sizeof(interp));
}

// ================= 🎨 UI 绘制 =================
static uint16_t rowBuf[IMG_W];
void drawThermal(float mn, float mx) {
  tft.setAddrWindow(IMG_X, IMG_Y, IMG_W, IMG_H);
  for (int y = 0; y < IMG_H; y++) { for (int x = 0; x < IMG_W; x++) rowBuf[x] = ironbow565[tempToIndex(interp[y*IMG_W+x], mn, mx)]; tft.pushColors(rowBuf, IMG_W, false); }
  tft.endWrite();
}
void drawLegend(float mn, float mx) {
  tft.fillRect(LEGEND_X - 3, LEGEND_Y - 3, LEGEND_W + 6, LEGEND_H + 6, 0x18E3);
  tft.fillRect(LEGEND_X, LEGEND_Y, LEGEND_W, IMG_H, TFT_BLACK);
  for (int i = 0; i < LEGEND_H - 2; i++) { uint8_t idx = map(i, 0, LEGEND_H - 3, 255, 0); tft.drawFastHLine(LEGEND_X + 2, LEGEND_Y + 2 + i, LEGEND_W - 4, ironbow565[idx]); }
  tft.setTextColor(TFT_WHITE); tft.setTextSize(FONT_SM); tft.setTextDatum(TR_DATUM);
  tft.drawFastHLine(LEGEND_X - 2, LEGEND_Y + 2, 4, TFT_WHITE); tft.drawFloat(mx, 0, LEGEND_X - 6, LEGEND_Y + 4);
  tft.drawFastHLine(LEGEND_X - 2, LEGEND_Y + LEGEND_H/2, 4, TFT_WHITE); tft.drawFloat((mn+mx)*0.5f, 0, LEGEND_X - 6, LEGEND_Y + LEGEND_H/2 - 2);
  tft.drawFastHLine(LEGEND_X - 2, LEGEND_Y + LEGEND_H - 3, 4, TFT_WHITE); tft.drawFloat(mn, 0, LEGEND_X - 6, LEGEND_Y + LEGEND_H - 8);
  tft.setTextDatum(TL_DATUM);
}
void drawTopBar() {
  tft.fillRect(0, 0, SCREEN_W, STATUS_H, 0x1082);
  tft.drawLine(0, STATUS_H, SCREEN_W, STATUS_H, TFT_DARKGREY);
  tft.setTextColor(TFT_WHITE); tft.setTextSize(2); tft.setTextDatum(TC_DATUM);
  tft.drawString("THERMAL PRO", SCREEN_W / 2, 5);
  tft.setTextSize(FONT_SM); tft.setTextDatum(TR_DATUM);
  tft.setTextColor(autoRange ? TFT_GREEN : TFT_YELLOW);
  tft.drawString(autoRange ? "AUTO" : "MANUAL", SCREEN_W - 4, 8);
  tft.setTextDatum(TL_DATUM);
}
void drawHotMarker() {
  if (hotSpotX < 0) return;
  int sx = IMG_X + (int)(hotSpotY * (IMG_W - 1) / 23.0f + 0.5f);
  int sy = IMG_Y + (int)(hotSpotX * (IMG_H - 1) / 31.0f + 0.5f);
  sx = constrain(sx, IMG_X + 6, IMG_X + IMG_W - 6);
  sy = constrain(sy, IMG_Y + 6, IMG_Y + IMG_H - 6);
  tft.drawLine(sx - 5, sy, sx + 5, sy, TFT_RED);
  tft.drawLine(sx, sy - 5, sx, sy + 5, TFT_RED);
  tft.fillCircle(sx, sy, 2, TFT_RED);
  tft.setTextColor(TFT_WHITE); tft.setTextSize(FONT_SM); tft.setTextDatum(TL_DATUM);
  int tx = sx + 8, ty = sy - 10; if (tx + 32 > IMG_X + IMG_W) tx = sx - 34; if (ty < IMG_Y + 4) ty = sy + 8;
  tft.drawFloat(hotSpotTemp, 1, tx, ty); tft.drawString("C", tx + 28, ty);
}
void drawCrosshair() {
  int cx = IMG_X + IMG_W / 2;
  int cy = IMG_Y + IMG_H / 2;
  int arm = 10;
  tft.drawLine(cx, max(cy - arm, IMG_Y), cx, cy - 2, TFT_WHITE);
  tft.drawLine(cx, cy + 2, cx, min(cy + arm, IMG_Y + IMG_H - 1), TFT_WHITE);
  tft.drawLine(max(cx - arm, IMG_X), cy, cx - 2, cy, TFT_WHITE);
  tft.drawLine(cx + 2, cy, min(cx + arm, IMG_X + IMG_W - 1), cy, TFT_WHITE);
  tft.fillCircle(cx, cy, 2, TFT_WHITE);
}
void drawAvgTemp() {
  int y = IMG_Y + IMG_H + 8;
  int centerX = IMG_X + IMG_W / 2;
  // 彻底清空温度区域背景,消除残影
  tft.fillRect(centerX - 40, y - 5, 80, 26, TFT_BLACK);  
  tft.setTextColor(TFT_GREEN, TFT_BLACK); 
  tft.setTextSize(2); 
  tft.setTextDatum(TC_DATUM);
  tft.drawFloat(centerTemp, 1, centerX, y); 
  tft.drawString("C", centerX + 54, y); 
  tft.setTextDatum(TL_DATUM);
}

void drawBottomPanel() {
  tft.fillRect(0, PANEL_Y, SCREEN_W, PANEL_H, 0x0841); tft.drawLine(0, PANEL_Y, SCREEN_W, PANEL_Y, TFT_DARKGREY);
  int line1 = PANEL_Y + 6, line2 = PANEL_Y + 20;
  tft.setTextColor(TFT_RED); tft.setTextSize(FONT_SM); tft.setTextDatum(TL_DATUM);
  tft.drawString("MAX ", 6, line1); tft.drawFloat(hotSpotTemp, 1, 42, line1); tft.drawString("C", 78, line1);
  tft.setTextColor(TFT_CYAN); tft.setTextDatum(TR_DATUM);
  tft.drawString(" MIN", SCREEN_W - 6, line1); tft.drawFloat(coldSpotTemp, 1, SCREEN_W - 42, line1); tft.drawString("C", SCREEN_W - 6, line1); tft.setTextDatum(TL_DATUM);
  tft.setTextColor(currentFPS >= 15 ? TFT_GREEN : TFT_YELLOW);
  tft.drawString("FPS:", 6, line2); tft.drawFloat(currentFPS, 0, 30, line2);
  tft.setTextColor(wifiConnected ? TFT_WHITE : TFT_DARKGREY); tft.setTextDatum(TR_DATUM);
  if (wifiConnected) { tft.drawString("IP:", SCREEN_W - 90, line2); tft.setTextColor(TFT_CYAN); tft.drawString(localIP.toString().c_str(), SCREEN_W - 6, line2); }
  else { tft.drawString("CCIT-WLAN", SCREEN_W - 6, line2); }
  tft.setTextDatum(TL_DATUM);
}
void drawFrame() {
  tft.drawRect(IMG_X - 2, IMG_Y - 2, IMG_W + 4, IMG_H + 4, TFT_DARKGREY);
  tft.drawRect(IMG_X - 1, IMG_Y - 1, IMG_W + 2, IMG_H + 2, TFT_WHITE);
}

// ================= 🌐 精简网页 =================
const char* WEB_PAGE = R"rawliteral(
<!DOCTYPE html><html lang="zh-CN"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1.0">
<title>Thermal Pro</title>
<style>
body{background:#0b0d10;color:#c8ccd4;font-family:system-ui,sans-serif;padding:10px;margin:0}
.header{display:flex;justify-content:space-between;padding:8px 12px;background:#15181e;border-radius:8px;margin-bottom:10px}
.title{font-weight:600;color:#00e5ff}.badge{padding:2px 6px;border-radius:12px;background:rgba(0,229,255,0.1);color:#00e5ff;font-size:0.8rem}
.grid{display:grid;grid-template-columns:1fr 1fr;gap:10px}
.card{background:#15181e;border-radius:8px;padding:12px}
.card h3{margin:0 0 8px 0;color:#00e5ff;font-size:1rem}
.temp-grid{display:grid;grid-template-columns:repeat(3,1fr);gap:4px;text-align:center}
.temp-val{font-size:1.3rem;font-weight:700}.temp-max{color:#ff3b30}.temp-min{color:#7dd3fc}.temp-avg{color:#34c759}
.threshold{display:flex;gap:6px;margin-top:6px}
.threshold input{flex:1;padding:4px 8px;background:#0f1216;border:1px solid #2a2e36;border-radius:4px;color:#fff}
.threshold button{padding:4px 10px;background:#00e5ff;color:#000;border:none;border-radius:4px;font-weight:600;cursor:pointer}
.alert{padding:6px 10px;background:rgba(255,59,48,0.15);border:1px solid #ff3b30;border-radius:4px;color:#ff3b30;margin-top:8px;display:none}
.alert.show{display:block}
.info-list{list-style:none;font-size:0.85rem;margin:0;padding:0}
.info-list li{display:flex;justify-content:space-between;padding:3px 0;border-bottom:1px dashed rgba(255,255,255,0.1)}
</style></head><body>
<div class="header"><div class="title">🔥 Thermal Pro</div><div><span class="badge" id="wifiBadge">📡</span><span class="badge" id="fpsBadge">FPS:--</span></div></div>
<div class="grid">
<div class="card"><h3>🌡️ Temperature</h3><div class="temp-grid">
<div><div style="color:#888;font-size:0.8rem">MAX</div><div class="temp-val temp-max" id="maxTemp">--.-</div><div style="color:#888;font-size:0.8rem">C</div></div>
<div><div style="color:#888;font-size:0.8rem">AVG</div><div class="temp-val temp-avg" id="avgTemp">--.-</div><div style="color:#888;font-size:0.8rem">C</div></div>
<div><div style="color:#888;font-size:0.8rem">MIN</div><div class="temp-val temp-min" id="minTemp">--.-</div><div style="color:#888;font-size:0.8rem">C</div></div>
</div><div class="threshold"><input type="number" id="thresholdInput" step="0.1" value="40.0"><button onclick="setThreshold()">Set</button></div><div class="alert" id="alertBox">⚠️ TEMP EXCEEDED!</div></div>
<div class="card"><h3>📊 System</h3><ul class="info-list">
<li><span>IP</span><span id="devIp">--.--.--.--</span></li><li><span>WiFi</span><span id="wifiStatus">--</span></li>
<li><span>USB</span><span style="color:#34c759">● Active</span></li><li><span>FPS</span><span id="frameRate">--</span></li>
<li><span>Mode</span><span id="rangeMode">AUTO</span></li><li><span>Threshold</span><span id="currentThreshold">40.0</span>C</li>
</ul></div></div><div style="text-align:center;padding:10px;color:#666;font-size:0.8rem">Thermal Pro | Auto-refresh 1s</div>
<script>
const API='/api';let alertActive=false;
async function fetchData(){try{const r=await fetch(API+'/data',{cache:'no-store'});const d=await r.json();
document.getElementById('maxTemp').textContent=d.hot.toFixed(1);
document.getElementById('avgTemp').textContent=d.avg.toFixed(1);
document.getElementById('minTemp').textContent=d.cold.toFixed(1);
document.getElementById('fpsBadge').textContent='FPS:'+d.fps.toFixed(0);
document.getElementById('frameRate').textContent=d.fps.toFixed(1)+' FPS';
document.getElementById('devIp').textContent=d.ip||'--';
document.getElementById('wifiStatus').textContent=d.wifi?'Connected':'Disconnected';
document.getElementById('rangeMode').textContent=d.auto?'AUTO':'MANUAL';
if(d.hot>d.threshold&&!alertActive){alertActive=true;document.getElementById('alertBox').classList.add('show');
fetch(API+'/alert',{method:'POST',body:JSON.stringify({msg:'ALERT:'+d.hot.toFixed(1)+'C'})}).catch(()=>{});
}else if(d.hot<=d.threshold&&alertActive){alertActive=false;document.getElementById('alertBox').classList.remove('show');}
}catch(e){console.warn('Error:',e)}}
function setThreshold(){const v=parseFloat(document.getElementById('thresholdInput').value);
if(!isNaN(v)&&v>=-40&&v<=150){fetch(API+'/threshold',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({threshold:v})})
.then(()=>{document.getElementById('currentThreshold').textContent=v}).catch(()=>{});}}
fetchData();setInterval(fetchData,1000);
</script></body></html>
)rawliteral";

// ================= 📡 API 接口 =================
void sendCorsHeaders() {
  server.sendHeader("Access-Control-Allow-Origin", "*");
  server.sendHeader("Access-Control-Allow-Methods", "GET, POST, OPTIONS");
  server.sendHeader("Access-Control-Allow-Headers", "Content-Type");
  server.sendHeader("Connection", "close");
  server.sendHeader("Cache-Control", "no-cache, no-store, must-revalidate");
}

void handleRoot() {
  Serial.println("🌐 [HTTP] GET /");
  sendCorsHeaders();
  server.send(200, "text/html", WEB_PAGE);
}

void handleApiData() {
  Serial.println("🌐 [HTTP] GET /api/data");
  sendCorsHeaders();
  float h = isnan(hotSpotTemp) ? 0.0 : hotSpotTemp;
  float a = isnan(centerTemp) ? 0.0 : centerTemp;
  float c = isnan(coldSpotTemp) ? 0.0 : coldSpotTemp;
  float f = isnan(currentFPS) ? 0.0 : currentFPS;
  String json = "{";
  json += "\"hot\":" + String(h, 1) + ",";
  json += "\"avg\":" + String(a, 1) + ",";
  json += "\"cold\":" + String(c, 1) + ",";
  json += "\"fps\":" + String(f, 1) + ",";
  json += "\"ip\":\"" + (wifiConnected ? localIP.toString() : "0.0.0.0") + "\",";
  json += "\"wifi\":" + String(wifiConnected ? "true" : "false") + ",";
#ifndef DISABLE_BLE
  json += "\"ble\":" + String(bleConnected ? "true" : "false") + ",";
#else
  json += "\"ble\":\"disabled\",";
#endif
  json += "\"auto\":" + String(autoRange ? "true" : "false") + ",";
  json += "\"threshold\":" + String(alertThreshold, 1);
  json += "}";
  Serial.printf("📤 JSON: %s\n", json.c_str());
  server.send(200, "application/json", json);
}

void handleSetThreshold() {
  Serial.println("🌐 [HTTP] POST /api/threshold");
  sendCorsHeaders();
  if (server.hasArg("plain")) {
    String body = server.arg("plain");
    int start = body.indexOf("threshold");
    if (start >= 0) {
      int valStart = body.indexOf(":", start) + 1;
      int valEnd = body.indexOf(",", valStart); if (valEnd < 0) valEnd = body.indexOf("}", valStart);
      if (valStart < valEnd) {
        float newThresh = body.substring(valStart, valEnd).toFloat();
        if (newThresh >= -40 && newThresh <= 150) { alertThreshold = newThresh; Serial.printf("✅ Threshold: %.1f\n", alertThreshold); server.send(200, "application/json", "{\"ok\":true}"); return; }
      }
    }
  }
  server.send(400, "application/json", "{\"ok\":false}");
}

void handleAlert() {
  Serial.println("🌐 [HTTP] POST /api/alert");
  sendCorsHeaders();
  if (server.hasArg("plain")) {
    String body = server.arg("plain");
    int msgStart = body.indexOf("msg");
    if (msgStart >= 0) {
      int valStart = body.indexOf(":", msgStart) + 2;
      int valEnd = body.indexOf("\"", valStart);
      if (valStart < valEnd) {
        String alertMsg = body.substring(valStart, valEnd);
        Serial.println("[ALERT] " + alertMsg);
#ifndef DISABLE_BLE
        if (bleConnected && pCharacteristic) { pCharacteristic->setValue(("[ALERT] " + alertMsg).c_str()); pCharacteristic->notify(); }
#endif
      }
    }
  }
  server.send(200, "application/json", "{\"ok\":true}");
}

void setupWebServer() {
  server.on("/", handleRoot);
  server.on("/api/data", handleApiData);
  server.on("/api/threshold", HTTP_POST, handleSetThreshold);
  server.on("/api/alert", HTTP_POST, handleAlert);
  server.begin();
  Serial.println("🌐 WebServer started on port 80");
}

// ================= 🔌 USB CDC =================
void sendSerialData() {
  if (millis() - lastSerialSend >= SERIAL_SEND_INTERVAL) {
    lastSerialSend = millis();
    Serial.printf("T:%.1f,%.1f,%.1f,%d,%d,%.1f,%d\n", centerTemp, hotSpotTemp, coldSpotTemp, hotSpotX, hotSpotY, alertThreshold, (hotSpotTemp > alertThreshold) ? 1 : 0);
  }
}

// ================= 🔵 BLE =================
#ifndef DISABLE_BLE
void sendBleData() {
  if (bleConnected && pCharacteristic && millis() - lastBleSend >= BLE_SEND_INTERVAL) {
    lastBleSend = millis();
    String msg = "T:" + String(centerTemp,1) + "," + String(hotSpotTemp,1) + "," + String(coldSpotTemp,1) + "," + hotSpotX + "," + hotSpotY + "," + String(alertThreshold,1) + "," + ((hotSpotTemp > alertThreshold) ? "1" : "0") + "\n";
    pCharacteristic->setValue(msg.c_str());
    pCharacteristic->notify();
  }
}
void setupBLE() {
  Serial.println("🔵 BLE: Initializing...");
  delay(10);
  BLEDevice::init(BLE_DEVICE_NAME);
  pServer = BLEDevice::createServer();
  pServer->setCallbacks(new MyServerCallbacks());
  BLEService *pService = pServer->createService(BLE_SERVICE_UUID);
  pCharacteristic = pService->createCharacteristic(BLE_CHAR_UUID, BLECharacteristic::PROPERTY_READ | BLECharacteristic::PROPERTY_WRITE | BLECharacteristic::PROPERTY_NOTIFY);
  pCharacteristic->addDescriptor(new BLE2902());
  pService->start();
  BLEAdvertising *pAdvertising = BLEDevice::getAdvertising();
  pAdvertising->addServiceUUID(BLE_SERVICE_UUID);
  pAdvertising->setScanResponse(true);
  BLEDevice::startAdvertising();
  Serial.println("🔵 BLE: Advertising '" + String(BLE_DEVICE_NAME) + "'");
}
#endif

// ================= 🚨 报警 =================
void checkAlert() {
  if (hotSpotTemp > alertThreshold && millis() - lastAlertTime > ALERT_COOLDOWN) {
    lastAlertTime = millis();
    alertActive = true;
    leds[0] = leds[1] = CRGB::Red;
    FastLED.show();
    
    String alertMsg = "[ALERT] TEMP:" + String(hotSpotTemp,1) + "C > " + String(alertThreshold,1) + "C";
    Serial.println(alertMsg);
#ifndef DISABLE_BLE
    if (bleConnected && pCharacteristic) { pCharacteristic->setValue(alertMsg.c_str()); pCharacteristic->notify(); }
#endif
  } 
  else if (hotSpotTemp <= alertThreshold) {
    alertActive = false;
    uint8_t hue = map(constrain(centerTemp, 15, 45), 15, 45, 180, 0);
    leds[0] = leds[1] = CHSV(hue, 255, 150);
    FastLED.show();
  }
}

// ================= 🚀 初始化 =================
void setup() {
  Serial.begin(115200);
  while(!Serial) delay(10);
  Serial.println("\n🔥 Thermal Pro v4.1 | ESP32-S3");

  // 🔌 WiFi
  WiFi.mode(WIFI_STA);
  WiFi.begin(WIFI_SSID);
  Serial.print("📡 WiFi: Connecting to "); Serial.println(WIFI_SSID);
  uint32_t wifiStart = millis();
  while (WiFi.status() != WL_CONNECTED && millis() - wifiStart < 8000) { delay(250); Serial.print("."); }
  if (WiFi.status() == WL_CONNECTED) { localIP = WiFi.localIP(); wifiConnected = true; Serial.printf("\n✅ WiFi: %s\n", localIP.toString().c_str()); }
  else { wifiConnected = false; Serial.println("\n⚠️ WiFi timeout"); }

  setupWebServer();

#ifndef DISABLE_BLE
  setupBLE();
#endif

  // 传感器
  Wire.begin(I2C_SDA, I2C_SCL, 400000);
  Wire.setClock(1000000);
  if (!mlx.begin(MLX90640_I2CADDR_DEFAULT, &Wire)) { while(1) { delay(100); tft.fillScreen(TFT_RED); } }
  mlx.setRefreshRate(MLX90640_16_HZ);
  Serial.println("🌡️ MLX90640 OK");

  // 引脚
  pinMode(btnPin, INPUT_PULLUP);
  pinMode(btnLedPin, INPUT_PULLUP);
  pinMode(ledPin, OUTPUT);
  digitalWrite(ledPin, HIGH);
  FastLED.addLeds<WS2812, wsPin, GRB>(leds, NUM_LEDS);
  FastLED.setBrightness(100);

  // 屏幕
  tft.init(); tft.setRotation(2); tft.fillScreen(TFT_BLACK);
  buildIronbowTable();
  drawTopBar(); drawFrame(); drawLegend(tMinFixed, tMaxFixed);
  Serial.println("🎨 UI Ready");
}

// ================= 🔄 主循环 =================
void loop() {
  server.handleClient();

  if (millis() - lastWifiCheck > 5000) {
    lastWifiCheck = millis();
    if (WiFi.status() != WL_CONNECTED) { wifiConnected = false; WiFi.begin(WIFI_SSID); }
    else if (!wifiConnected) { localIP = WiFi.localIP(); wifiConnected = true; }
  }

  if (mlx.getFrame(frame) == 0) {
    uint32_t now = millis();
    float dt = (now - lastFrameTime) / 1000.0f;
    lastFrameTime = now;
    if (dt > 0.01f && dt < 1.0f) currentFPS = currentFPS * (1-FPS_FILTER) + (1.0f/dt) * FPS_FILTER;

    if (autoRange) calcSmartRange(frame, 768, &tMin, &tMax);
    else { tMin = tMinFixed; tMax = tMaxFixed; }
    findHotColdSpots();
    centerTemp = avgCenterTemp();
    smoothFrame(); bicubicAdaptive(); applySharpen(0.25f);

    drawThermal(tMin, tMax);
    drawCrosshair();
    drawHotMarker();
    drawAvgTemp();
    drawBottomPanel();

    uint8_t hue = map(constrain(centerTemp, 15, 45), 15, 45, 180, 0);
    leds[0] = leds[1] = CHSV(hue, 255, 150);
    FastLED.show();

    static bool ledState = true, lastBtnLed = HIGH;
    static uint32_t btnLedDown = 0;
    bool bl = digitalRead(btnLedPin);
    if (bl == LOW && lastBtnLed == HIGH) btnLedDown = millis();
    if (bl == HIGH && lastBtnLed == LOW && millis() - btnLedDown < 400) { ledState = !ledState; digitalWrite(ledPin, ledState ? HIGH : LOW); }
    lastBtnLed = bl;

    checkAlert();
    sendSerialData();
#ifndef DISABLE_BLE
    sendBleData();
#endif
  }

  static bool lastBtn = HIGH;
  static uint32_t btnDown = 0;
  bool b = digitalRead(btnPin);
  if (b == LOW && lastBtn == HIGH) btnDown = millis();
  if (b == HIGH && lastBtn == LOW && millis() - btnDown < 500) { autoRange = !autoRange; drawTopBar(); }
  if (b == LOW && millis() - btnDown > 800) {
    #ifdef TFT_BL
    static bool bl = true; bl = !bl;
    digitalWrite(TFT_BL, bl ? TFT_BACKLIGHT_ON : !TFT_BACKLIGHT_ON);
    btnDown = millis();
    #endif
  }
  lastBtn = b;
}

更多推荐