Ver Fonte

添加硬件固件

Moki há 2 semanas atrás
pai
commit
dc1c422c54
5 ficheiros alterados com 1919 adições e 1 exclusões
  1. 5 1
      README.md
  2. 62 0
      firmware/README.md
  3. 498 0
      firmware/ble/ai_light.ino
  4. 677 0
      firmware/mqtt/ai_light.ino
  5. 677 0
      firmware/mqtt/ai_light_v2.ino

+ 5 - 1
README.md

@@ -7,7 +7,7 @@ OpenCode 状态监控工具,支持实时监控多个 OpenCode 实例的状态
 - 🔍 **自动发现** - 自动扫描并发现运行中的 OpenCode 实例
 - 📊 **实时监控** - 通过 SSE 事件流实时获取状态变化
 - 📡 **MQTT 推送** - 支持将状态信息推送到 MQTT Broker
-- 🟢 **BLE 蓝牙推送** - 通过蓝牙将状态推送到 AI-Light 等 BLE 设备
+- 🟢 **BLE 蓝牙推送** - 通过蓝牙将状态推送到 [AI-Light](firmware/) 等 BLE 设备
 - 💾 **配置管理** - 使用 SQLite 存储 MQTT 和 BLE 配置
 - 🌐 **HTTP API** - 提供 RESTful API 接口管理配置(详见 [API 文档](docs/api.md))
 - 🔌 **WebSocket** - 支持通过 WebSocket 实时推送状态到网页(详见 [API 文档](docs/api.md))
@@ -212,6 +212,10 @@ AI-Status-Light/
 │   ├── event/            # 事件处理模块
 │   ├── monitor/          # 监控器核心模块
 │   └── mqtt/             # MQTT 客户端模块
+├── firmware/             # ESP32-C3 硬件固件
+│   ├── ble/              # BLE 蓝牙固件
+│   ├── mqtt/             # MQTT 固件
+│   └── README.md         # 硬件说明
 ├── scripts/
 │   ├── build.bat                 # Windows 构建脚本
 │   ├── build.sh                  # Linux/macOS 构建脚本

+ 62 - 0
firmware/README.md

@@ -0,0 +1,62 @@
+# AI-Light 固件
+
+基于 **ESP32-C3 SuperMini** 的桌面状态灯固件,通过 BLE 蓝牙或 MQTT 接收状态指令,控制红绿灯挂件显示 AI 工作状态。
+
+## 固件版本
+
+| 版本 | 文件 | 通信方式 | 说明 |
+|------|------|----------|------|
+| BLE | `ble/ai_light.ino` | BLE 蓝牙 | 无需 WiFi,适合本地使用 |
+| MQTT v1 | `mqtt/ai_light.ino` | WiFi + MQTT | 远程控制 |
+| MQTT v2 | `mqtt/ai_light_v2.ino` | WiFi + MQTT | 改进版 |
+
+## BLE 参数
+
+```
+Device Name:          AI-Light
+Service UUID:         b8b7e001-7a6b-4f4f-9a8b-11c0ffee0001
+Characteristic UUID:  b8b7e002-7a6b-4f4f-9a8b-11c0ffee0001
+```
+
+写入 UTF-8 字符串即可控制灯效,支持的模式:
+
+| 模式 | 灯效 | 典型场景 |
+|------|------|----------|
+| `init` | 开机初始化 | 启动 |
+| `thinking` | 跑马灯 | AI 分析中 |
+| `ai` | 慢速跑马灯 | AI 生成中 |
+| `busy` | 黄灯慢闪 | 命令执行中 |
+| `success` | 绿灯常亮 | 任务成功 |
+| `error` | 红灯快闪 | 任务失败 |
+| `alarm` | 红黄交替警灯 | 严重异常 |
+| `traffic` | 模拟红绿灯 | 展示模式 |
+| `off` | 全灭 | 关闭 |
+
+## 硬件接线
+
+基于公共正极灯板:
+
+```
+ESP32 3.3V  -> 灯板正极
+ESP32 IO2   -> 220Ω -> 绿灯
+ESP32 IO3   -> 220Ω -> 红灯
+ESP32 IO4   -> 220Ω -> 黄灯
+```
+
+公共正极逻辑:GPIO LOW = 灯亮,GPIO HIGH = 灯灭
+
+## 烧录方法
+
+1. 安装 [Arduino IDE 2.x](https://www.arduino.cc/en/software)
+2. 安装 ESP32 开发板包:`esp32 by Espressif Systems`
+3. 选择开发板:`ESP32C3 Dev Module`
+4. 打开 `.ino` 文件,点击 Upload
+5. 串口监视器波特率:`115200`
+
+## 依赖库(MQTT 版)
+
+- `WiFi.h`
+- `PubSubClient.h`
+- `ArduinoJson.h`
+
+通过 Arduino IDE → Sketch → Include Library → Manage Libraries 安装。

+ 498 - 0
firmware/ble/ai_light.ino

@@ -0,0 +1,498 @@
+#include <BLEDevice.h>
+#include <BLEServer.h>
+#include <BLEUtils.h>
+#include <BLE2902.h>
+
+// =====================================================
+// ESP32-C3 SuperMini + 原玩具公共正极灯板:BLE 蓝牙控制增强版
+//
+// 接线方式:
+// ESP32 3.3V  -> 原灯板 + / 原电池正极
+// ESP32 IO2   -> 220Ω -> L1 控制点 = 绿灯
+// ESP32 IO3   -> 220Ω -> L2 控制点 = 红灯
+// ESP32 IO4   -> 220Ω -> L3 控制点 = 黄灯
+//
+// 注意:
+// 1. 原灯板 - / 原电池负极 第一版先不要接。
+// 2. 公共正极:GPIO LOW = 灯亮,GPIO HIGH = 灯灭。
+// 3. 默认开机模式:init
+// 4. 除 off、traffic 外,其他模式最多运行 5 分钟,然后自动进入 traffic。
+// 5. traffic 最多运行 10 分钟,然后自动 off。
+// =====================================================
+
+const char* BLE_DEVICE_NAME = "AI-Light";
+
+#define SERVICE_UUID        "b8b7e001-7a6b-4f4f-9a8b-11c0ffee0001"
+#define MODE_CHAR_UUID      "b8b7e002-7a6b-4f4f-9a8b-11c0ffee0001"
+
+// 你的实测:L1=绿灯,L2=红灯,L3=黄灯
+const int GREEN_PIN = 2;   // IO2 -> L1 绿灯
+const int RED_PIN = 3;     // IO3 -> L2 红灯
+const int YELLOW_PIN = 4;  // IO4 -> L3 黄灯
+const int WIFI_LED_PIN = 8; // IO8 -> 板载LED
+
+const int PWM_FREQ = 5000;
+const int PWM_RESOLUTION = 8;
+
+// 红灯偏弱,所以红灯单独增强
+const int RED_MAX = 255;
+const int YELLOW_MAX = 220;
+const int GREEN_MAX = 220;
+
+const unsigned long NORMAL_MODE_TIMEOUT_MS = 5UL * 60UL * 1000UL;   // 5 分钟
+const unsigned long TRAFFIC_MODE_TIMEOUT_MS = 10UL * 60UL * 1000UL; // 10 分钟
+
+String currentMode = "init";
+unsigned long modeStart = 0;
+
+BLEServer* pServer = nullptr;
+BLECharacteristic* pModeCharacteristic = nullptr;
+bool deviceConnected = false;
+
+
+// =====================================================
+// 基础工具函数:公共正极反相输出
+// =====================================================
+
+void writeLed(int pin, int value) {
+  value = constrain(value, 0, 255);
+  int pwmValue = 255 - value; // 公共正极反相
+  ledcWrite(pin, pwmValue);
+}
+
+void allOff() {
+  writeLed(RED_PIN, 0);
+  writeLed(YELLOW_PIN, 0);
+  writeLed(GREEN_PIN, 0);
+}
+
+void setOnly(int red, int yellow, int green) {
+  writeLed(RED_PIN, constrain(red, 0, RED_MAX));
+  writeLed(YELLOW_PIN, constrain(yellow, 0, YELLOW_MAX));
+  writeLed(GREEN_PIN, constrain(green, 0, GREEN_MAX));
+}
+
+int triWave(unsigned long t, unsigned long period, int maxValue) {
+  unsigned long x = t % period;
+  if (x < period / 2) {
+    return map(x, 0, period / 2, 0, maxValue);
+  } else {
+    return map(x, period / 2, period, maxValue, 0);
+  }
+}
+
+int fadeInOutBrightness(
+  unsigned long t,
+  unsigned long fadeIn,
+  unsigned long hold,
+  unsigned long fadeOut,
+  unsigned long offTime,
+  int maxValue
+) {
+  unsigned long total = fadeIn + hold + fadeOut + offTime;
+  unsigned long x = t % total;
+
+  if (x < fadeIn) {
+    return map(x, 0, fadeIn, 0, maxValue);
+  }
+
+  x -= fadeIn;
+  if (x < hold) {
+    return maxValue;
+  }
+
+  x -= hold;
+  if (x < fadeOut) {
+    return map(x, 0, fadeOut, maxValue, 0);
+  }
+
+  return 0;
+}
+
+void fadeToStatic(int targetRed, int targetYellow, int targetGreen, int fadeMs = 80) {
+  allOff();
+
+  int steps = 12;
+  int delayPerStep = max(1, fadeMs / steps);
+
+  for (int i = 0; i <= steps; i++) {
+    float p = (float)i / steps;
+    setOnly(targetRed * p, targetYellow * p, targetGreen * p);
+    delay(delayPerStep);
+  }
+}
+
+
+// =====================================================
+// 模式处理
+// =====================================================
+
+bool isValidMode(String mode) {
+  return (
+    mode == "red" ||
+    mode == "yellow" ||
+    mode == "green" ||
+    mode == "busy" ||
+    mode == "error" ||
+    mode == "thinking" ||
+    mode == "ai" ||
+    mode == "success" ||
+    mode == "traffic" ||
+    mode == "alarm" ||
+    mode == "init" ||
+    mode == "idle" ||
+    mode == "off"
+  );
+}
+
+void notifyMode() {
+  if (pModeCharacteristic) {
+    pModeCharacteristic->setValue(currentMode.c_str());
+    if (deviceConnected) {
+      pModeCharacteristic->notify();
+    }
+  }
+}
+
+void setMode(String mode) {
+  mode.trim();
+  mode.toLowerCase();
+
+  if (!isValidMode(mode)) {
+    Serial.print("Unknown mode: ");
+    Serial.println(mode);
+    return;
+  }
+
+  if (mode == "idle") {
+    mode = "traffic";
+  }
+
+  currentMode = mode;
+  modeStart = millis();
+
+  Serial.print("Mode changed to: ");
+  Serial.println(currentMode);
+
+  if (mode == "red") {
+    fadeToStatic(RED_MAX, 0, 0, 80);
+  } else if (mode == "yellow") {
+    fadeToStatic(0, YELLOW_MAX, 0, 80);
+  } else if (mode == "green") {
+    fadeToStatic(0, 0, GREEN_MAX, 80);
+  } else if (mode == "success") {
+    setOnly(0, 0, GREEN_MAX);
+  } else if (mode == "off") {
+    allOff();
+  }
+
+  notifyMode();
+}
+
+void autoTimeoutCheck() {
+  unsigned long elapsed = millis() - modeStart;
+
+  if (currentMode == "off") {
+    return;
+  }
+
+  if (currentMode == "traffic") {
+    if (elapsed >= TRAFFIC_MODE_TIMEOUT_MS) {
+      Serial.println("Traffic timeout -> off");
+      setMode("off");
+    }
+    return;
+  }
+
+  if (elapsed >= NORMAL_MODE_TIMEOUT_MS) {
+    Serial.println("Normal mode timeout -> traffic");
+    setMode("traffic");
+  }
+}
+
+
+// =====================================================
+// 灯效模式
+// =====================================================
+
+void updateBusy() {
+  unsigned long t = millis() - modeStart;
+  int y = fadeInOutBrightness(t, 80, 500, 120, 500, YELLOW_MAX);
+  setOnly(0, y, 0);
+}
+
+void updateError() {
+  unsigned long t = millis() - modeStart;
+  int r = fadeInOutBrightness(t, 40, 180, 80, 180, RED_MAX);
+  setOnly(r, 0, 0);
+}
+
+// thinking:连贯跑马灯,按实物从上到下:L1绿 -> L2黄 -> L3红
+void updateThinking() {
+  unsigned long t = millis() - modeStart;
+  const unsigned long period = 1050;
+  unsigned long x = t % period;
+
+  int g = 0;
+  int y = 0;
+  int r = 0;
+
+  if (x < 350) {
+    g = map(x, 0, 350, GREEN_MAX, 70);
+    y = map(x, 0, 350, 20, YELLOW_MAX);
+    r = 0;
+  } else if (x < 700) {
+    unsigned long p = x - 350;
+    g = map(p, 0, 350, 70, 0);
+    y = map(p, 0, 350, YELLOW_MAX, 70);
+    r = map(p, 0, 350, 20, RED_MAX);
+  } else {
+    unsigned long p = x - 700;
+    g = map(p, 0, 350, 20, GREEN_MAX);
+    y = map(p, 0, 350, 70, 0);
+    r = map(p, 0, 350, RED_MAX, 70);
+  }
+
+  setOnly(r, y, g);
+}
+
+// ai:柔和版跑马灯,比 thinking 更慢、更柔和、亮度低一点
+void updateAi() {
+  unsigned long t = millis() - modeStart;
+  const unsigned long period = 1800;
+  unsigned long x = t % period;
+
+  unsigned long gx = (x + 0) % period;
+  unsigned long yx = (x + period / 3) % period;
+  unsigned long rx = (x + 2 * period / 3) % period;
+
+  int g = triWave(gx, period, 150);
+  int y = triWave(yx, period, 140);
+  int r = triWave(rx, period, 170);
+
+  setOnly(r, y, g);
+}
+
+void updateSuccess() {
+  setOnly(0, 0, GREEN_MAX);
+}
+
+// alarm:红黄交替警灯,带短渐变
+void updateAlarm() {
+  unsigned long t = millis() - modeStart;
+  const unsigned long phaseMs = 260;
+  int phase = (t / phaseMs) % 2;
+  unsigned long inside = t % phaseMs;
+
+  int brightness;
+  if (inside < 60) {
+    brightness = map(inside, 0, 60, 0, 255);
+  } else if (inside < 180) {
+    brightness = 255;
+  } else {
+    brightness = map(inside, 180, phaseMs, 255, 0);
+  }
+
+  if (phase == 0) {
+    setOnly(brightness, 0, 0);
+  } else {
+    setOnly(0, min(brightness, YELLOW_MAX), 0);
+  }
+}
+
+// traffic:绿灯变黄前绿闪;黄灯;红灯变绿前红闪
+void updateTraffic() {
+  unsigned long t = (millis() - modeStart) % 15000;
+
+  if (t < 5000) {
+    setOnly(0, 0, GREEN_MAX);
+  }
+
+  else if (t < 6500) {
+    unsigned long phase = (t - 5000) % 500;
+    int g = 0;
+    if (phase < 60) g = map(phase, 0, 60, 0, GREEN_MAX);
+    else if (phase < 230) g = GREEN_MAX;
+    else if (phase < 320) g = map(phase, 230, 320, GREEN_MAX, 0);
+    else g = 0;
+    setOnly(0, 0, g);
+  }
+
+  else if (t < 8500) {
+    setOnly(0, YELLOW_MAX, 0);
+  }
+
+  else if (t < 13500) {
+    setOnly(RED_MAX, 0, 0);
+  }
+
+  else {
+    unsigned long phase = (t - 13500) % 500;
+    int r = 0;
+    if (phase < 60) r = map(phase, 0, 60, 0, RED_MAX);
+    else if (phase < 230) r = RED_MAX;
+    else if (phase < 320) r = map(phase, 230, 320, RED_MAX, 0);
+    else r = 0;
+    setOnly(r, 0, 0);
+  }
+}
+
+// init:三色一起呼吸
+void updateInit() {
+  unsigned long t = millis() - modeStart;
+  const unsigned long period = 2500;
+  unsigned long x = t % period;
+  
+  int brightness;
+  if (x < 800) {
+    brightness = map(x, 0, 800, 0, 200);
+  } else if (x < 1200) {
+    brightness = 200;
+  } else if (x < 2000) {
+    brightness = map(x, 1200, 2000, 200, 0);
+  } else {
+    brightness = 0;
+  }
+  
+  setOnly(brightness, brightness, brightness);
+}
+
+
+// =====================================================
+// BLE 回调
+// =====================================================
+
+class ServerCallbacks : public BLEServerCallbacks {
+  void onConnect(BLEServer* pServer) {
+    deviceConnected = true;
+    Serial.println("BLE client connected.");
+  }
+
+  void onDisconnect(BLEServer* pServer) {
+    deviceConnected = false;
+    Serial.println("BLE client disconnected. Restart advertising.");
+    BLEDevice::startAdvertising();
+  }
+};
+
+class ModeCharacteristicCallbacks : public BLECharacteristicCallbacks {
+  void onWrite(BLECharacteristic* pCharacteristic) {
+    String value = pCharacteristic->getValue();
+    value.trim();
+
+    Serial.print("BLE write: ");
+    Serial.println(value);
+
+    setMode(value);
+  }
+
+  void onRead(BLECharacteristic* pCharacteristic) {
+    pCharacteristic->setValue(currentMode.c_str());
+  }
+};
+
+
+// =====================================================
+// 初始化
+// =====================================================
+
+void setup() {
+  Serial.begin(115200);
+  delay(500);
+
+  ledcAttach(RED_PIN, PWM_FREQ, PWM_RESOLUTION);
+  ledcAttach(YELLOW_PIN, PWM_FREQ, PWM_RESOLUTION);
+  ledcAttach(GREEN_PIN, PWM_FREQ, PWM_RESOLUTION);
+
+  pinMode(WIFI_LED_PIN, OUTPUT);
+  digitalWrite(WIFI_LED_PIN, HIGH);
+
+  allOff();
+
+  currentMode = "init";
+  modeStart = millis();
+
+  Serial.println();
+  Serial.println("Power on. Default mode: init");
+  Serial.println("Common anode BLE enhanced version.");
+  Serial.println("BLE device name: AI-Light");
+
+  BLEDevice::init(BLE_DEVICE_NAME);
+
+  pServer = BLEDevice::createServer();
+  pServer->setCallbacks(new ServerCallbacks());
+
+  BLEService* pService = pServer->createService(SERVICE_UUID);
+
+  pModeCharacteristic = pService->createCharacteristic(
+    MODE_CHAR_UUID,
+    BLECharacteristic::PROPERTY_READ |
+    BLECharacteristic::PROPERTY_WRITE |
+    BLECharacteristic::PROPERTY_NOTIFY
+  );
+
+  pModeCharacteristic->setCallbacks(new ModeCharacteristicCallbacks());
+  pModeCharacteristic->setValue(currentMode.c_str());
+  pModeCharacteristic->addDescriptor(new BLE2902());
+
+  pService->start();
+
+  BLEAdvertising* pAdvertising = BLEDevice::getAdvertising();
+  pAdvertising->addServiceUUID(SERVICE_UUID);
+  pAdvertising->setScanResponse(true);
+  pAdvertising->setMinPreferred(0x06);
+  pAdvertising->setMinPreferred(0x12);
+
+  BLEDevice::startAdvertising();
+
+  Serial.println("BLE advertising started.");
+  Serial.println("Supported modes:");
+  Serial.println("init / thinking / ai / busy / success / error / alarm / traffic / off / red / yellow / green");
+}
+
+
+// =====================================================
+// 主循环
+// =====================================================
+
+unsigned long lastBleLedToggle = 0;
+bool bleLedState = false;
+
+void updateBleLed() {
+  if (deviceConnected) {
+    digitalWrite(WIFI_LED_PIN, LOW);
+  } else {
+    if (millis() - lastBleLedToggle >= 500) {
+      bleLedState = !bleLedState;
+      digitalWrite(WIFI_LED_PIN, bleLedState ? LOW : HIGH);
+      lastBleLedToggle = millis();
+    }
+  }
+}
+
+void loop() {
+  updateBleLed();
+  autoTimeoutCheck();
+
+  if (currentMode == "busy") {
+    updateBusy();
+  } else if (currentMode == "error") {
+    updateError();
+  } else if (currentMode == "thinking") {
+    updateThinking();
+  } else if (currentMode == "ai") {
+    updateAi();
+  } else if (currentMode == "success") {
+    updateSuccess();
+  } else if (currentMode == "traffic") {
+    updateTraffic();
+  } else if (currentMode == "alarm") {
+    updateAlarm();
+  } else if (currentMode == "init") {
+    updateInit();
+  } else if (currentMode == "off") {
+    allOff();
+  }
+
+  delay(5);
+}

+ 677 - 0
firmware/mqtt/ai_light.ino

@@ -0,0 +1,677 @@
+#include <WiFi.h>
+#include <PubSubClient.h>
+#include <ArduinoJson.h>
+
+// =====================================================
+// ESP32-C3 SuperMini + 原玩具公共正极灯板:MQTT 蓝牙控制增强版
+//
+// 接线方式:
+// ESP32 3.3V  -> 原灯板 + / 原电池正极
+// ESP32 IO2   -> 220Ω -> L1 控制点 = 绿灯
+// ESP32 IO3   -> 220Ω -> L2 控制点 = 黄灯
+// ESP32 IO4   -> 220Ω -> L3 控制点 = 红灯
+//
+// 注意:
+// 1. 原灯板 - / 原电池负极 第一版先不要接。
+// 2. 公共正极:GPIO LOW = 灯亮,GPIO HIGH = 灯灭。
+// 3. 默认开机模式:init
+// 4. 除 off、traffic 外,其他模式最多运行 5 分钟,然后自动进入 traffic。
+// 5. traffic 最多运行 10 分钟,然后自动 off。
+// =====================================================
+
+// =====================================================
+// WiFi & MQTT 配置(请根据你的网络修改)
+// =====================================================
+#define WIFI_SSID        "MokiBox-IoT"
+#define WIFI_PASSWORD    "Moki@886."
+#define MQTT_BROKER      "47.92.50.210"
+#define MQTT_PORT        9883
+#define MQTT_USERNAME    "moki"
+#define MQTT_PASSWORD    "Moki@886."
+#define MQTT_CLIENT_ID   "AI-Light"
+#define MQTT_TOPIC       "opencode/status"
+#define MQTT_STATUS_TOPIC "openCodeLight/status"
+
+// 你的实测:L1=绿灯,L2=黄灯,L3=红灯
+const int GREEN_PIN = 2;   // IO2 -> L1 绿灯
+const int YELLOW_PIN = 3;  // IO3 -> L2 黄灯
+const int RED_PIN = 4;     // IO4 -> L3 红灯
+const int WIFI_LED_PIN = 8; // IO8 -> 板载LED
+
+const int PWM_FREQ = 5000;
+const int PWM_RESOLUTION = 8;
+
+// 红灯偏弱,所以红灯单独增强
+const int RED_MAX = 255;
+const int YELLOW_MAX = 220;
+const int GREEN_MAX = 220;
+
+const unsigned long NORMAL_MODE_TIMEOUT_MS = 5UL * 60UL * 1000UL;   // 5 分钟
+const unsigned long TRAFFIC_MODE_TIMEOUT_MS = 10UL * 60UL * 1000UL; // 10 分钟
+
+String currentMode = "init";
+unsigned long modeStart = 0;
+
+WiFiClient wifiClient;
+PubSubClient mqttClient(wifiClient);
+
+
+// =====================================================
+// 基础工具函数:公共正极反相输出
+// =====================================================
+
+void writeLed(int pin, int value) {
+  value = constrain(value, 0, 255);
+  int pwmValue = 255 - value; // 公共正极反相
+  ledcWrite(pin, pwmValue);
+}
+
+void allOff() {
+  writeLed(RED_PIN, 0);
+  writeLed(YELLOW_PIN, 0);
+  writeLed(GREEN_PIN, 0);
+}
+
+void setOnly(int red, int yellow, int green) {
+  writeLed(RED_PIN, constrain(red, 0, RED_MAX));
+  writeLed(YELLOW_PIN, constrain(yellow, 0, YELLOW_MAX));
+  writeLed(GREEN_PIN, constrain(green, 0, GREEN_MAX));
+}
+
+int triWave(unsigned long t, unsigned long period, int maxValue) {
+  unsigned long x = t % period;
+  if (x < period / 2) {
+    return map(x, 0, period / 2, 0, maxValue);
+  } else {
+    return map(x, period / 2, period, maxValue, 0);
+  }
+}
+
+int fadeInOutBrightness(
+  unsigned long t,
+  unsigned long fadeIn,
+  unsigned long hold,
+  unsigned long fadeOut,
+  unsigned long offTime,
+  int maxValue
+) {
+  unsigned long total = fadeIn + hold + fadeOut + offTime;
+  unsigned long x = t % total;
+
+  if (x < fadeIn) {
+    return map(x, 0, fadeIn, 0, maxValue);
+  }
+
+  x -= fadeIn;
+  if (x < hold) {
+    return maxValue;
+  }
+
+  x -= hold;
+  if (x < fadeOut) {
+    return map(x, 0, fadeOut, maxValue, 0);
+  }
+
+  return 0;
+}
+
+void blinkLed(int pin, int times = 3, int intervalMs = 200) {
+  for (int i = 0; i < times; i++) {
+    writeLed(pin, 200);
+    delay(intervalMs);
+    writeLed(pin, 0);
+    delay(intervalMs);
+  }
+}
+
+void fadeToStatic(int targetRed, int targetYellow, int targetGreen, int fadeMs = 80) {
+  allOff();
+
+  int steps = 12;
+  int delayPerStep = max(1, fadeMs / steps);
+
+  for (int i = 0; i <= steps; i++) {
+    float p = (float)i / steps;
+    setOnly(targetRed * p, targetYellow * p, targetGreen * p);
+    delay(delayPerStep);
+  }
+}
+
+
+// =====================================================
+// 模式处理
+// =====================================================
+
+bool isValidMode(String mode) {
+  return (
+    mode == "red" ||
+    mode == "yellow" ||
+    mode == "green" ||
+    mode == "busy" ||
+    mode == "error" ||
+    mode == "thinking" ||
+    mode == "ai" ||
+    mode == "success" ||
+    mode == "traffic" ||
+    mode == "alarm" ||
+    mode == "init" ||
+    mode == "off" ||
+    mode == "idle"
+  );
+}
+
+void publishStatus() {
+  mqttClient.publish(MQTT_STATUS_TOPIC, currentMode.c_str(), true);
+}
+
+void setMode(String mode) {
+  mode.trim();
+  mode.toLowerCase();
+
+  if (!isValidMode(mode)) {
+    Serial.print("Unknown mode: ");
+    Serial.println(mode);
+    return;
+  }
+
+  if (mode == "idle") {
+    mode = "traffic";
+  }
+
+  currentMode = mode;
+  modeStart = millis();
+
+  Serial.print("Mode changed to: ");
+  Serial.println(currentMode);
+
+  if (mode == "red") {
+    fadeToStatic(RED_MAX, 0, 0, 80);
+  } else if (mode == "yellow") {
+    fadeToStatic(0, YELLOW_MAX, 0, 80);
+  } else if (mode == "green") {
+    fadeToStatic(0, 0, GREEN_MAX, 80);
+  } else if (mode == "success") {
+    setOnly(0, 0, GREEN_MAX);
+  } else if (mode == "off") {
+    allOff();
+  }
+
+  publishStatus();
+}
+
+void autoTimeoutCheck() {
+  unsigned long elapsed = millis() - modeStart;
+
+  if (currentMode == "off") {
+    return;
+  }
+
+  if (currentMode == "traffic") {
+    if (elapsed >= TRAFFIC_MODE_TIMEOUT_MS) {
+      Serial.println("Traffic timeout -> off");
+      setMode("off");
+    }
+    return;
+  }
+
+  if (elapsed >= NORMAL_MODE_TIMEOUT_MS) {
+    Serial.println("Normal mode timeout -> traffic");
+    setMode("traffic");
+  }
+}
+
+
+// =====================================================
+// 灯效模式
+// =====================================================
+
+void updateBusy() {
+  unsigned long t = millis() - modeStart;
+  int y = fadeInOutBrightness(t, 80, 500, 120, 500, YELLOW_MAX);
+  setOnly(0, y, 0);
+}
+
+void updateError() {
+  unsigned long t = millis() - modeStart;
+  int r = fadeInOutBrightness(t, 40, 180, 80, 180, RED_MAX);
+  setOnly(r, 0, 0);
+}
+
+// thinking:连贯跑马灯,按实物从上到下:L1绿 -> L2黄 -> L3红
+void updateThinking() {
+  unsigned long t = millis() - modeStart;
+  const unsigned long period = 1050;
+  unsigned long x = t % period;
+
+  int g = 0;
+  int y = 0;
+  int r = 0;
+
+  if (x < 350) {
+    g = map(x, 0, 350, GREEN_MAX, 70);
+    y = map(x, 0, 350, 20, YELLOW_MAX);
+    r = 0;
+  } else if (x < 700) {
+    unsigned long p = x - 350;
+    g = map(p, 0, 350, 70, 0);
+    y = map(p, 0, 350, YELLOW_MAX, 70);
+    r = map(p, 0, 350, 20, RED_MAX);
+  } else {
+    unsigned long p = x - 700;
+    g = map(p, 0, 350, 20, GREEN_MAX);
+    y = map(p, 0, 350, 70, 0);
+    r = map(p, 0, 350, RED_MAX, 70);
+  }
+
+  setOnly(r, y, g);
+}
+
+// ai:柔和版跑马灯,比 thinking 更慢、更柔和、亮度低一点
+void updateAi() {
+  unsigned long t = millis() - modeStart;
+  const unsigned long period = 1800;
+  unsigned long x = t % period;
+
+  unsigned long gx = (x + 0) % period;
+  unsigned long yx = (x + period / 3) % period;
+  unsigned long rx = (x + 2 * period / 3) % period;
+
+  int g = triWave(gx, period, 150);
+  int y = triWave(yx, period, 140);
+  int r = triWave(rx, period, 170);
+
+  setOnly(r, y, g);
+}
+
+void updateSuccess() {
+  setOnly(0, 0, GREEN_MAX);
+}
+
+// alarm:红黄交替警灯,带短渐变
+void updateAlarm() {
+  unsigned long t = millis() - modeStart;
+  const unsigned long phaseMs = 260;
+  int phase = (t / phaseMs) % 2;
+  unsigned long inside = t % phaseMs;
+
+  int brightness;
+  if (inside < 60) {
+    brightness = map(inside, 0, 60, 0, 255);
+  } else if (inside < 180) {
+    brightness = 255;
+  } else {
+    brightness = map(inside, 180, phaseMs, 255, 0);
+  }
+
+  if (phase == 0) {
+    setOnly(brightness, 0, 0);
+  } else {
+    setOnly(0, min(brightness, YELLOW_MAX), 0);
+  }
+}
+
+// traffic:绿灯变黄前绿闪;黄灯;红灯变绿前红闪
+void updateTraffic() {
+  unsigned long t = (millis() - modeStart) % 15000;
+
+  if (t < 5000) {
+    setOnly(0, 0, GREEN_MAX);
+  }
+
+  else if (t < 6500) {
+    unsigned long phase = (t - 5000) % 500;
+    int g = 0;
+    if (phase < 60) g = map(phase, 0, 60, 0, GREEN_MAX);
+    else if (phase < 230) g = GREEN_MAX;
+    else if (phase < 320) g = map(phase, 230, 320, GREEN_MAX, 0);
+    else g = 0;
+    setOnly(0, 0, g);
+  }
+
+  else if (t < 8500) {
+    setOnly(0, YELLOW_MAX, 0);
+  }
+
+  else if (t < 13500) {
+    setOnly(RED_MAX, 0, 0);
+  }
+
+  else {
+    unsigned long phase = (t - 13500) % 500;
+    int r = 0;
+    if (phase < 60) r = map(phase, 0, 60, 0, RED_MAX);
+    else if (phase < 230) r = RED_MAX;
+    else if (phase < 320) r = map(phase, 230, 320, RED_MAX, 0);
+    else r = 0;
+    setOnly(r, 0, 0);
+  }
+}
+
+// init:三色一起呼吸
+void updateInit() {
+  unsigned long t = millis() - modeStart;
+  const unsigned long period = 2500;
+  unsigned long x = t % period;
+  
+  int brightness;
+  if (x < 800) {
+    brightness = map(x, 0, 800, 0, 200);
+  } else if (x < 1200) {
+    brightness = 200;
+  } else if (x < 2000) {
+    brightness = map(x, 1200, 2000, 200, 0);
+  } else {
+    brightness = 0;
+  }
+  
+  setOnly(brightness, brightness, brightness);
+}
+
+// 工具执行完成:绿灯快闪三次
+void updateCompletedFlash() {
+  unsigned long t = millis() - modeStart;
+  const int flashCount = 3;
+  const unsigned long flashDuration = 150;  // 每次闪烁150ms
+  const unsigned long totalDuration = flashCount * 2 * flashDuration;  // 3次闪烁总时长
+  
+  if (t >= totalDuration) {
+    // 闪烁完成,保持灭灯
+    setOnly(0, 0, 0);
+    return;
+  }
+  
+  // 计算当前闪烁状态
+  unsigned long flashPhase = t % (2 * flashDuration);
+  if (flashPhase < flashDuration) {
+    // 亮灯阶段
+    setOnly(0, 0, GREEN_MAX);
+  } else {
+    // 灭灯阶段
+    setOnly(0, 0, 0);
+  }
+}
+
+// 会话完成:绿灯呼吸闪烁(3次后自动切换到idle)
+void updateBreathing() {
+  unsigned long t = millis() - modeStart;
+  const unsigned long period = 2500;  // 2.5秒一个呼吸周期
+  const int breathCount = 3;  // 呼吸3次
+  const unsigned long totalDuration = breathCount * period;  // 总时长
+  
+  // 检查是否完成3次呼吸
+  if (t >= totalDuration) {
+    // 3次呼吸完成,自动切换到idle
+    setMode("idle");
+    return;
+  }
+  
+  unsigned long x = t % period;
+  
+  int brightness;
+  if (x < 800) {
+    // 吸入阶段:0 -> 最大
+    brightness = map(x, 0, 800, 0, GREEN_MAX);
+  } else if (x < 1200) {
+    // 保持阶段:最大亮度
+    brightness = GREEN_MAX;
+  } else if (x < 2000) {
+    // 呼出阶段:最大 -> 0
+    brightness = map(x, 1200, 2000, GREEN_MAX, 0);
+  } else {
+    // 停顿阶段:0亮度
+    brightness = 0;
+  }
+  
+  setOnly(0, 0, brightness);
+}
+
+
+// =====================================================
+// MQTT 回调
+// =====================================================
+
+void mqttCallback(char* topic, byte* payload, unsigned int length) {
+  String message = "";
+  for (unsigned int i = 0; i < length; i++) {
+    message += (char)payload[i];
+  }
+
+  Serial.print("MQTT message on ");
+  Serial.print(topic);
+  Serial.print(": ");
+  Serial.println(message);
+
+  JsonDocument doc;
+  DeserializationError error = deserializeJson(doc, message);
+
+  if (error) {
+    Serial.print("JSON parse failed, treating as raw mode: ");
+    Serial.println(error.c_str());
+    setMode(message);
+    return;
+  }
+
+  const char* code = doc["code"];
+  if (code) {
+    String codeStr = String(code);
+    
+    // 状态码映射到灯效
+    if (codeStr == "idle") {
+      setMode("idle");
+    } else if (codeStr == "busy" || codeStr == "running") {
+      setMode("busy");
+    } else if (codeStr == "retry" || codeStr == "permission") {
+      setMode("alarm");
+    } else if (codeStr == "pending") {
+      setMode("yellow");
+    } else if (codeStr == "reasoning") {
+      setMode("thinking");
+    } else if (codeStr == "using_tool") {
+      setMode("ai");
+    } else if (codeStr == "error") {
+      setMode("error");
+    } else {
+      // 直接使用原始模式名
+      setMode(codeStr);
+    }
+  } else {
+    Serial.println("MQTT message missing 'code' field");
+  }
+}
+
+unsigned long lastWifiLedToggle = 0;
+bool wifiLedState = false;
+
+void updateWifiLed() {
+  if (WiFi.status() == WL_CONNECTED) {
+    digitalWrite(WIFI_LED_PIN, LOW);
+  } else {
+    if (millis() - lastWifiLedToggle >= 500) {
+      wifiLedState = !wifiLedState;
+      digitalWrite(WIFI_LED_PIN, wifiLedState ? LOW : HIGH);
+      lastWifiLedToggle = millis();
+    }
+  }
+}
+
+void connectWiFi() {
+  Serial.print("Connecting to WiFi");
+  WiFi.mode(WIFI_STA);
+  WiFi.begin(WIFI_SSID, WIFI_PASSWORD);
+
+  int maxRetries = 5;
+  int attemptsPerRetry = 60;
+
+  for (int retry = 1; retry <= maxRetries; retry++) {
+    Serial.print("\nWiFi attempt ");
+    Serial.print(retry);
+    Serial.print("/");
+    Serial.println(maxRetries);
+
+    int attempts = 0;
+    while (WiFi.status() != WL_CONNECTED && attempts < attemptsPerRetry) {
+      delay(5);
+      setOnly(100, 100, 100);
+      updateWifiLed();
+      Serial.print(".");
+      attempts++;
+    }
+
+    if (WiFi.status() == WL_CONNECTED) {
+      break;
+    }
+
+    Serial.println("\nWiFi connection failed, retrying...");
+    delay(2000);
+  }
+
+  if (WiFi.status() != WL_CONNECTED) {
+    Serial.println("WiFi failed after 5 retries. Restarting...");
+    delay(1000);
+    ESP.restart();
+  }
+
+  Serial.println();
+  Serial.print("WiFi connected. IP: ");
+  Serial.println(WiFi.localIP());
+  digitalWrite(WIFI_LED_PIN, LOW);
+}
+
+void breathingGreen(int times) {
+  const unsigned long period = 2500;
+  for (int i = 0; i < times; i++) {
+    unsigned long start = millis();
+    while (millis() - start < period) {
+      unsigned long t = millis() - start;
+      int brightness;
+      if (t < 800) {
+        brightness = map(t, 0, 800, 0, GREEN_MAX);
+      } else if (t < 1200) {
+        brightness = GREEN_MAX;
+      } else if (t < 2000) {
+        brightness = map(t, 1200, 2000, GREEN_MAX, 0);
+      } else {
+        brightness = 0;
+      }
+      setOnly(0, 0, brightness);
+      delay(5);
+    }
+  }
+  allOff();
+}
+
+void connectMQTT() {
+  mqttClient.setServer(MQTT_BROKER, MQTT_PORT);
+  mqttClient.setCallback(mqttCallback);
+
+  Serial.print("Connecting to MQTT broker");
+  int attempts = 0;
+  bool ledState = false;
+  while (!mqttClient.connected()) {
+    ledState = !ledState;
+    if (ledState) {
+      setOnly(RED_MAX, 0, 0);
+    } else {
+      setOnly(0, YELLOW_MAX, 0);
+    }
+
+    if (mqttClient.connect(MQTT_CLIENT_ID, MQTT_USERNAME, MQTT_PASSWORD)) {
+      Serial.println();
+      Serial.println("MQTT connected.");
+
+      mqttClient.subscribe(MQTT_TOPIC);
+      Serial.print("Subscribed to: ");
+      Serial.println(MQTT_TOPIC);
+
+      allOff();
+      breathingGreen(3);
+      publishStatus();
+    } else {
+      Serial.print(".");
+      delay(500);
+      attempts++;
+      if (attempts > 60) {
+        Serial.println("\nMQTT connection failed!");
+        attempts = 0;
+      }
+    }
+  }
+}
+
+void checkMQTTConnection() {
+  if (!mqttClient.connected()) {
+    Serial.println("MQTT disconnected. Reconnecting...");
+    connectMQTT();
+  }
+}
+
+
+// =====================================================
+// 初始化
+// =====================================================
+
+void setup() {
+  Serial.begin(115200);
+  delay(500);
+
+  ledcAttach(RED_PIN, PWM_FREQ, PWM_RESOLUTION);
+  ledcAttach(YELLOW_PIN, PWM_FREQ, PWM_RESOLUTION);
+  ledcAttach(GREEN_PIN, PWM_FREQ, PWM_RESOLUTION);
+
+  pinMode(WIFI_LED_PIN, OUTPUT);
+  digitalWrite(WIFI_LED_PIN, LOW);  // 初始状态关闭
+
+  allOff();
+
+  Serial.println();
+  Serial.println("Power on.");
+  Serial.println("Common anode MQTT enhanced version.");
+  Serial.print("Device name: ");
+  Serial.println(MQTT_CLIENT_ID);
+
+  currentMode = "init";
+  modeStart = millis();
+
+  connectWiFi();
+  connectMQTT();
+
+  setMode("traffic");
+
+  Serial.println("Supported modes:");
+  Serial.println("init / thinking / ai / busy / success / error / alarm / traffic / off / red / yellow / green");
+}
+
+
+// =====================================================
+// 主循环
+// =====================================================
+
+void loop() {
+  updateWifiLed();
+  checkMQTTConnection();
+  mqttClient.loop();
+
+  autoTimeoutCheck();
+
+  if (currentMode == "busy") {
+    updateBusy();
+  } else if (currentMode == "error") {
+    updateError();
+  } else if (currentMode == "thinking") {
+    updateThinking();
+  } else if (currentMode == "ai") {
+    updateAi();
+  } else if (currentMode == "success") {
+    updateSuccess();
+  } else if (currentMode == "traffic") {
+    updateTraffic();
+  } else if (currentMode == "alarm") {
+    updateAlarm();
+  } else if (currentMode == "init") {
+    updateInit();
+  } else if (currentMode == "off") {
+    allOff();
+  }
+
+  delay(5);
+}

+ 677 - 0
firmware/mqtt/ai_light_v2.ino

@@ -0,0 +1,677 @@
+#include <WiFi.h>
+#include <PubSubClient.h>
+#include <ArduinoJson.h>
+
+// =====================================================
+// ESP32-C3 SuperMini + 原玩具公共正极灯板:MQTT 蓝牙控制增强版 V2
+//
+// 接线方式(V2 版本:红黄互换 + 黄绿互换):
+// ESP32 3.3V  -> 原灯板 + / 原电池正极
+// ESP32 IO2   -> 220Ω -> L1 控制点 = 黄灯
+// ESP32 IO3   -> 220Ω -> L2 控制点 = 红灯
+// ESP32 IO4   -> 220Ω -> L3 控制点 = 绿灯
+//
+// 注意:
+// 1. 原灯板 - / 原电池负极 第一版先不要接。
+// 2. 公共正极:GPIO LOW = 灯亮,GPIO HIGH = 灯灭。
+// 3. 默认开机模式:init
+// 4. 除 off、traffic 外,其他模式最多运行 5 分钟,然后自动进入 traffic。
+// 5. traffic 最多运行 10 分钟,然后自动 off。
+// =====================================================
+
+// =====================================================
+// WiFi & MQTT 配置(请根据你的网络修改)
+// =====================================================
+#define WIFI_SSID        "MokiBox-IoT"
+#define WIFI_PASSWORD    "Moki@886."
+#define MQTT_BROKER      "47.92.50.210"
+#define MQTT_PORT        9883
+#define MQTT_USERNAME    "moki"
+#define MQTT_PASSWORD    "Moki@886."
+#define MQTT_CLIENT_ID   "AI-Light"
+#define MQTT_TOPIC       "opencode/status"
+#define MQTT_STATUS_TOPIC "openCodeLight/status"
+
+// V2 版本:红黄互换 + 黄绿互换
+const int GREEN_PIN = 2;   // IO2 -> L1 绿灯
+const int RED_PIN = 3;     // IO3 -> L2 红灯
+const int YELLOW_PIN = 4;  // IO4 -> L3 黄灯
+const int WIFI_LED_PIN = 8; // IO8 -> 板载LED
+
+const int PWM_FREQ = 5000;
+const int PWM_RESOLUTION = 8;
+
+// 红灯偏弱,所以红灯单独增强
+const int RED_MAX = 255;
+const int YELLOW_MAX = 220;
+const int GREEN_MAX = 220;
+
+const unsigned long NORMAL_MODE_TIMEOUT_MS = 5UL * 60UL * 1000UL;   // 5 分钟
+const unsigned long TRAFFIC_MODE_TIMEOUT_MS = 10UL * 60UL * 1000UL; // 10 分钟
+
+String currentMode = "init";
+unsigned long modeStart = 0;
+
+WiFiClient wifiClient;
+PubSubClient mqttClient(wifiClient);
+
+
+// =====================================================
+// 基础工具函数:公共正极反相输出
+// =====================================================
+
+void writeLed(int pin, int value) {
+  value = constrain(value, 0, 255);
+  int pwmValue = 255 - value; // 公共正极反相
+  ledcWrite(pin, pwmValue);
+}
+
+void allOff() {
+  writeLed(RED_PIN, 0);
+  writeLed(YELLOW_PIN, 0);
+  writeLed(GREEN_PIN, 0);
+}
+
+void setOnly(int red, int yellow, int green) {
+  writeLed(RED_PIN, constrain(red, 0, RED_MAX));
+  writeLed(YELLOW_PIN, constrain(yellow, 0, YELLOW_MAX));
+  writeLed(GREEN_PIN, constrain(green, 0, GREEN_MAX));
+}
+
+int triWave(unsigned long t, unsigned long period, int maxValue) {
+  unsigned long x = t % period;
+  if (x < period / 2) {
+    return map(x, 0, period / 2, 0, maxValue);
+  } else {
+    return map(x, period / 2, period, maxValue, 0);
+  }
+}
+
+int fadeInOutBrightness(
+  unsigned long t,
+  unsigned long fadeIn,
+  unsigned long hold,
+  unsigned long fadeOut,
+  unsigned long offTime,
+  int maxValue
+) {
+  unsigned long total = fadeIn + hold + fadeOut + offTime;
+  unsigned long x = t % total;
+
+  if (x < fadeIn) {
+    return map(x, 0, fadeIn, 0, maxValue);
+  }
+
+  x -= fadeIn;
+  if (x < hold) {
+    return maxValue;
+  }
+
+  x -= hold;
+  if (x < fadeOut) {
+    return map(x, 0, fadeOut, maxValue, 0);
+  }
+
+  return 0;
+}
+
+void blinkLed(int pin, int times = 3, int intervalMs = 200) {
+  for (int i = 0; i < times; i++) {
+    writeLed(pin, 200);
+    delay(intervalMs);
+    writeLed(pin, 0);
+    delay(intervalMs);
+  }
+}
+
+void fadeToStatic(int targetRed, int targetYellow, int targetGreen, int fadeMs = 80) {
+  allOff();
+
+  int steps = 12;
+  int delayPerStep = max(1, fadeMs / steps);
+
+  for (int i = 0; i <= steps; i++) {
+    float p = (float)i / steps;
+    setOnly(targetRed * p, targetYellow * p, targetGreen * p);
+    delay(delayPerStep);
+  }
+}
+
+
+// =====================================================
+// 模式处理
+// =====================================================
+
+bool isValidMode(String mode) {
+  return (
+    mode == "red" ||
+    mode == "yellow" ||
+    mode == "green" ||
+    mode == "busy" ||
+    mode == "error" ||
+    mode == "thinking" ||
+    mode == "ai" ||
+    mode == "success" ||
+    mode == "traffic" ||
+    mode == "alarm" ||
+    mode == "init" ||
+    mode == "off" ||
+    mode == "idle"
+  );
+}
+
+void publishStatus() {
+  mqttClient.publish(MQTT_STATUS_TOPIC, currentMode.c_str(), true);
+}
+
+void setMode(String mode) {
+  mode.trim();
+  mode.toLowerCase();
+
+  if (!isValidMode(mode)) {
+    Serial.print("Unknown mode: ");
+    Serial.println(mode);
+    return;
+  }
+
+  if (mode == "idle") {
+    mode = "traffic";
+  }
+
+  currentMode = mode;
+  modeStart = millis();
+
+  Serial.print("Mode changed to: ");
+  Serial.println(currentMode);
+
+  if (mode == "red") {
+    fadeToStatic(RED_MAX, 0, 0, 80);
+  } else if (mode == "yellow") {
+    fadeToStatic(0, YELLOW_MAX, 0, 80);
+  } else if (mode == "green") {
+    fadeToStatic(0, 0, GREEN_MAX, 80);
+  } else if (mode == "success") {
+    setOnly(0, 0, GREEN_MAX);
+  } else if (mode == "off") {
+    allOff();
+  }
+
+  publishStatus();
+}
+
+void autoTimeoutCheck() {
+  unsigned long elapsed = millis() - modeStart;
+
+  if (currentMode == "off") {
+    return;
+  }
+
+  if (currentMode == "traffic") {
+    if (elapsed >= TRAFFIC_MODE_TIMEOUT_MS) {
+      Serial.println("Traffic timeout -> off");
+      setMode("off");
+    }
+    return;
+  }
+
+  if (elapsed >= NORMAL_MODE_TIMEOUT_MS) {
+    Serial.println("Normal mode timeout -> traffic");
+    setMode("traffic");
+  }
+}
+
+
+// =====================================================
+// 灯效模式
+// =====================================================
+
+void updateBusy() {
+  unsigned long t = millis() - modeStart;
+  int y = fadeInOutBrightness(t, 80, 500, 120, 500, YELLOW_MAX);
+  setOnly(0, y, 0);
+}
+
+void updateError() {
+  unsigned long t = millis() - modeStart;
+  int r = fadeInOutBrightness(t, 40, 180, 80, 180, RED_MAX);
+  setOnly(r, 0, 0);
+}
+
+// thinking:连贯跑马灯,按实物从上到下:L1绿 -> L2黄 -> L3红
+void updateThinking() {
+  unsigned long t = millis() - modeStart;
+  const unsigned long period = 1050;
+  unsigned long x = t % period;
+
+  int g = 0;
+  int y = 0;
+  int r = 0;
+
+  if (x < 350) {
+    g = map(x, 0, 350, GREEN_MAX, 70);
+    y = map(x, 0, 350, 20, YELLOW_MAX);
+    r = 0;
+  } else if (x < 700) {
+    unsigned long p = x - 350;
+    g = map(p, 0, 350, 70, 0);
+    y = map(p, 0, 350, YELLOW_MAX, 70);
+    r = map(p, 0, 350, 20, RED_MAX);
+  } else {
+    unsigned long p = x - 700;
+    g = map(p, 0, 350, 20, GREEN_MAX);
+    y = map(p, 0, 350, 70, 0);
+    r = map(p, 0, 350, RED_MAX, 70);
+  }
+
+  setOnly(r, y, g);
+}
+
+// ai:柔和版跑马灯,比 thinking 更慢、更柔和、亮度低一点
+void updateAi() {
+  unsigned long t = millis() - modeStart;
+  const unsigned long period = 1800;
+  unsigned long x = t % period;
+
+  unsigned long gx = (x + 0) % period;
+  unsigned long yx = (x + period / 3) % period;
+  unsigned long rx = (x + 2 * period / 3) % period;
+
+  int g = triWave(gx, period, 150);
+  int y = triWave(yx, period, 140);
+  int r = triWave(rx, period, 170);
+
+  setOnly(r, y, g);
+}
+
+void updateSuccess() {
+  setOnly(0, 0, GREEN_MAX);
+}
+
+// alarm:红黄交替警灯,带短渐变
+void updateAlarm() {
+  unsigned long t = millis() - modeStart;
+  const unsigned long phaseMs = 260;
+  int phase = (t / phaseMs) % 2;
+  unsigned long inside = t % phaseMs;
+
+  int brightness;
+  if (inside < 60) {
+    brightness = map(inside, 0, 60, 0, 255);
+  } else if (inside < 180) {
+    brightness = 255;
+  } else {
+    brightness = map(inside, 180, phaseMs, 255, 0);
+  }
+
+  if (phase == 0) {
+    setOnly(brightness, 0, 0);
+  } else {
+    setOnly(0, min(brightness, YELLOW_MAX), 0);
+  }
+}
+
+// traffic:绿灯变黄前绿闪;黄灯;红灯变绿前红闪
+void updateTraffic() {
+  unsigned long t = (millis() - modeStart) % 15000;
+
+  if (t < 5000) {
+    setOnly(0, 0, GREEN_MAX);
+  }
+
+  else if (t < 6500) {
+    unsigned long phase = (t - 5000) % 500;
+    int g = 0;
+    if (phase < 60) g = map(phase, 0, 60, 0, GREEN_MAX);
+    else if (phase < 230) g = GREEN_MAX;
+    else if (phase < 320) g = map(phase, 230, 320, GREEN_MAX, 0);
+    else g = 0;
+    setOnly(0, 0, g);
+  }
+
+  else if (t < 8500) {
+    setOnly(0, YELLOW_MAX, 0);
+  }
+
+  else if (t < 13500) {
+    setOnly(RED_MAX, 0, 0);
+  }
+
+  else {
+    unsigned long phase = (t - 13500) % 500;
+    int r = 0;
+    if (phase < 60) r = map(phase, 0, 60, 0, RED_MAX);
+    else if (phase < 230) r = RED_MAX;
+    else if (phase < 320) r = map(phase, 230, 320, RED_MAX, 0);
+    else r = 0;
+    setOnly(r, 0, 0);
+  }
+}
+
+// init:三色一起呼吸
+void updateInit() {
+  unsigned long t = millis() - modeStart;
+  const unsigned long period = 2500;
+  unsigned long x = t % period;
+  
+  int brightness;
+  if (x < 800) {
+    brightness = map(x, 0, 800, 0, 200);
+  } else if (x < 1200) {
+    brightness = 200;
+  } else if (x < 2000) {
+    brightness = map(x, 1200, 2000, 200, 0);
+  } else {
+    brightness = 0;
+  }
+  
+  setOnly(brightness, brightness, brightness);
+}
+
+// 工具执行完成:绿灯快闪三次
+void updateCompletedFlash() {
+  unsigned long t = millis() - modeStart;
+  const int flashCount = 3;
+  const unsigned long flashDuration = 150;  // 每次闪烁150ms
+  const unsigned long totalDuration = flashCount * 2 * flashDuration;  // 3次闪烁总时长
+  
+  if (t >= totalDuration) {
+    // 闪烁完成,保持灭灯
+    setOnly(0, 0, 0);
+    return;
+  }
+  
+  // 计算当前闪烁状态
+  unsigned long flashPhase = t % (2 * flashDuration);
+  if (flashPhase < flashDuration) {
+    // 亮灯阶段
+    setOnly(0, 0, GREEN_MAX);
+  } else {
+    // 灭灯阶段
+    setOnly(0, 0, 0);
+  }
+}
+
+// 会话完成:绿灯呼吸闪烁(3次后自动切换到idle)
+void updateBreathing() {
+  unsigned long t = millis() - modeStart;
+  const unsigned long period = 2500;  // 2.5秒一个呼吸周期
+  const int breathCount = 3;  // 呼吸3次
+  const unsigned long totalDuration = breathCount * period;  // 总时长
+  
+  // 检查是否完成3次呼吸
+  if (t >= totalDuration) {
+    // 3次呼吸完成,自动切换到idle
+    setMode("idle");
+    return;
+  }
+  
+  unsigned long x = t % period;
+  
+  int brightness;
+  if (x < 800) {
+    // 吸入阶段:0 -> 最大
+    brightness = map(x, 0, 800, 0, GREEN_MAX);
+  } else if (x < 1200) {
+    // 保持阶段:最大亮度
+    brightness = GREEN_MAX;
+  } else if (x < 2000) {
+    // 呼出阶段:最大 -> 0
+    brightness = map(x, 1200, 2000, GREEN_MAX, 0);
+  } else {
+    // 停顿阶段:0亮度
+    brightness = 0;
+  }
+  
+  setOnly(0, 0, brightness);
+}
+
+
+// =====================================================
+// MQTT 回调
+// =====================================================
+
+void mqttCallback(char* topic, byte* payload, unsigned int length) {
+  String message = "";
+  for (unsigned int i = 0; i < length; i++) {
+    message += (char)payload[i];
+  }
+
+  Serial.print("MQTT message on ");
+  Serial.print(topic);
+  Serial.print(": ");
+  Serial.println(message);
+
+  JsonDocument doc;
+  DeserializationError error = deserializeJson(doc, message);
+
+  if (error) {
+    Serial.print("JSON parse failed, treating as raw mode: ");
+    Serial.println(error.c_str());
+    setMode(message);
+    return;
+  }
+
+  const char* code = doc["code"];
+  if (code) {
+    String codeStr = String(code);
+    
+    // 状态码映射到灯效
+    if (codeStr == "idle") {
+      setMode("idle");
+    } else if (codeStr == "busy" || codeStr == "running") {
+      setMode("busy");
+    } else if (codeStr == "retry" || codeStr == "permission") {
+      setMode("alarm");
+    } else if (codeStr == "pending") {
+      setMode("yellow");
+    } else if (codeStr == "reasoning") {
+      setMode("thinking");
+    } else if (codeStr == "using_tool") {
+      setMode("ai");
+    } else if (codeStr == "error") {
+      setMode("error");
+    } else {
+      // 直接使用原始模式名
+      setMode(codeStr);
+    }
+  } else {
+    Serial.println("MQTT message missing 'code' field");
+  }
+}
+
+unsigned long lastWifiLedToggle = 0;
+bool wifiLedState = false;
+
+void updateWifiLed() {
+  if (WiFi.status() == WL_CONNECTED) {
+    digitalWrite(WIFI_LED_PIN, LOW);
+  } else {
+    if (millis() - lastWifiLedToggle >= 500) {
+      wifiLedState = !wifiLedState;
+      digitalWrite(WIFI_LED_PIN, wifiLedState ? LOW : HIGH);
+      lastWifiLedToggle = millis();
+    }
+  }
+}
+
+void connectWiFi() {
+  Serial.print("Connecting to WiFi");
+  WiFi.mode(WIFI_STA);
+  WiFi.begin(WIFI_SSID, WIFI_PASSWORD);
+
+  int maxRetries = 5;
+  int attemptsPerRetry = 60;
+
+  for (int retry = 1; retry <= maxRetries; retry++) {
+    Serial.print("\nWiFi attempt ");
+    Serial.print(retry);
+    Serial.print("/");
+    Serial.println(maxRetries);
+
+    int attempts = 0;
+    while (WiFi.status() != WL_CONNECTED && attempts < attemptsPerRetry) {
+      delay(5);
+      setOnly(100, 100, 100);
+      updateWifiLed();
+      Serial.print(".");
+      attempts++;
+    }
+
+    if (WiFi.status() == WL_CONNECTED) {
+      break;
+    }
+
+    Serial.println("\nWiFi connection failed, retrying...");
+    delay(2000);
+  }
+
+  if (WiFi.status() != WL_CONNECTED) {
+    Serial.println("WiFi failed after 5 retries. Restarting...");
+    delay(1000);
+    ESP.restart();
+  }
+
+  Serial.println();
+  Serial.print("WiFi connected. IP: ");
+  Serial.println(WiFi.localIP());
+  digitalWrite(WIFI_LED_PIN, LOW);
+}
+
+void breathingGreen(int times) {
+  const unsigned long period = 2500;
+  for (int i = 0; i < times; i++) {
+    unsigned long start = millis();
+    while (millis() - start < period) {
+      unsigned long t = millis() - start;
+      int brightness;
+      if (t < 800) {
+        brightness = map(t, 0, 800, 0, GREEN_MAX);
+      } else if (t < 1200) {
+        brightness = GREEN_MAX;
+      } else if (t < 2000) {
+        brightness = map(t, 1200, 2000, GREEN_MAX, 0);
+      } else {
+        brightness = 0;
+      }
+      setOnly(0, 0, brightness);
+      delay(5);
+    }
+  }
+  allOff();
+}
+
+void connectMQTT() {
+  mqttClient.setServer(MQTT_BROKER, MQTT_PORT);
+  mqttClient.setCallback(mqttCallback);
+
+  Serial.print("Connecting to MQTT broker");
+  int attempts = 0;
+  bool ledState = false;
+  while (!mqttClient.connected()) {
+    ledState = !ledState;
+    if (ledState) {
+      setOnly(RED_MAX, 0, 0);
+    } else {
+      setOnly(0, YELLOW_MAX, 0);
+    }
+
+    if (mqttClient.connect(MQTT_CLIENT_ID, MQTT_USERNAME, MQTT_PASSWORD)) {
+      Serial.println();
+      Serial.println("MQTT connected.");
+
+      mqttClient.subscribe(MQTT_TOPIC);
+      Serial.print("Subscribed to: ");
+      Serial.println(MQTT_TOPIC);
+
+      allOff();
+      breathingGreen(3);
+      publishStatus();
+    } else {
+      Serial.print(".");
+      delay(500);
+      attempts++;
+      if (attempts > 60) {
+        Serial.println("\nMQTT connection failed!");
+        attempts = 0;
+      }
+    }
+  }
+}
+
+void checkMQTTConnection() {
+  if (!mqttClient.connected()) {
+    Serial.println("MQTT disconnected. Reconnecting...");
+    connectMQTT();
+  }
+}
+
+
+// =====================================================
+// 初始化
+// =====================================================
+
+void setup() {
+  Serial.begin(115200);
+  delay(500);
+
+  ledcAttach(RED_PIN, PWM_FREQ, PWM_RESOLUTION);
+  ledcAttach(YELLOW_PIN, PWM_FREQ, PWM_RESOLUTION);
+  ledcAttach(GREEN_PIN, PWM_FREQ, PWM_RESOLUTION);
+
+  pinMode(WIFI_LED_PIN, OUTPUT);
+  digitalWrite(WIFI_LED_PIN, LOW);  // 初始状态关闭
+
+  allOff();
+
+  Serial.println();
+  Serial.println("Power on.");
+  Serial.println("Common anode MQTT enhanced version V2 (Red/Yellow + Yellow/Green swapped).");
+  Serial.print("Device name: ");
+  Serial.println(MQTT_CLIENT_ID);
+
+  currentMode = "init";
+  modeStart = millis();
+
+  connectWiFi();
+  connectMQTT();
+
+  setMode("traffic");
+
+  Serial.println("Supported modes:");
+  Serial.println("init / thinking / ai / busy / success / error / alarm / traffic / off / red / yellow / green");
+}
+
+
+// =====================================================
+// 主循环
+// =====================================================
+
+void loop() {
+  updateWifiLed();
+  checkMQTTConnection();
+  mqttClient.loop();
+
+  autoTimeoutCheck();
+
+  if (currentMode == "busy") {
+    updateBusy();
+  } else if (currentMode == "error") {
+    updateError();
+  } else if (currentMode == "thinking") {
+    updateThinking();
+  } else if (currentMode == "ai") {
+    updateAi();
+  } else if (currentMode == "success") {
+    updateSuccess();
+  } else if (currentMode == "traffic") {
+    updateTraffic();
+  } else if (currentMode == "alarm") {
+    updateAlarm();
+  } else if (currentMode == "init") {
+    updateInit();
+  } else if (currentMode == "off") {
+    allOff();
+  }
+
+  delay(5);
+}