ai_light.ino 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649
  1. #include <WiFi.h>
  2. #include <PubSubClient.h>
  3. #include <ArduinoJson.h>
  4. #include <BLEDevice.h>
  5. #include <BLEServer.h>
  6. #include <BLEUtils.h>
  7. #include <BLE2902.h>
  8. #include <Preferences.h>
  9. // =====================================================
  10. // ESP32-C3 SuperMini + 原玩具公共正极灯板:BLE + MQTT 双模式
  11. //
  12. // 功能:
  13. // - 开机始终启动 BLE,支持灯效控制 + WiFi/MQTT 配置
  14. // - MQTT 模式下同时连接 WiFi/MQTT
  15. // - 运行时长按 BOOT 按钮 3 秒 → 切换 BLE/MQTT 模式并重启
  16. //
  17. // BLE 配置:
  18. // Service: b8b7e001-7a6b-4f4f-9a8b-11c0ffee0001
  19. // Mode: b8b7e002-... (读写/通知 - 灯效模式)
  20. // Config: b8b7e003-... (读写 - WiFi/MQTT 配置 JSON)
  21. //
  22. // 配置 JSON 格式(写入 Config 特征):
  23. // {
  24. // "wifi_ssid": "xxx",
  25. // "wifi_pass": "xxx",
  26. // "mqtt_broker": "192.168.1.100",
  27. // "mqtt_port": 1883,
  28. // "mqtt_user": "user",
  29. // "mqtt_pass": "pass",
  30. // "mqtt_client": "AI-Light",
  31. // "mqtt_topic": "opencode/status",
  32. // "mqtt_status": "openCodeLight/status"
  33. // }
  34. //
  35. // 接线方式(V2 版本:红黄互换 + 黄绿互换):
  36. // ESP32 3.3V -> 原灯板 + / 原电池正极
  37. // ESP32 IO2 -> 220Ω -> L1 控制点 = 绿灯
  38. // ESP32 IO3 -> 220Ω -> L2 控制点 = 红灯
  39. // ESP32 IO4 -> 220Ω -> L3 控制点 = 黄灯
  40. // =====================================================
  41. // =====================================================
  42. // BLE 配置
  43. // =====================================================
  44. const char* BLE_DEVICE_NAME = "AI-Light";
  45. #define SERVICE_UUID "b8b7e001-7a6b-4f4f-9a8b-11c0ffee0001"
  46. #define MODE_CHAR_UUID "b8b7e002-7a6b-4f4f-9a8b-11c0ffee0001"
  47. #define CONFIG_CHAR_UUID "b8b7e003-7a6b-4f4f-9a8b-11c0ffee0001"
  48. // =====================================================
  49. // 引脚定义(V2 版本:红黄互换 + 黄绿互换)
  50. // =====================================================
  51. const int GREEN_PIN = 2;
  52. const int RED_PIN = 3;
  53. const int YELLOW_PIN = 4;
  54. const int WIFI_LED_PIN = 8;
  55. const int BOOT_PIN = 9;
  56. const int PWM_FREQ = 5000;
  57. const int PWM_RESOLUTION = 8;
  58. const int RED_MAX = 255;
  59. const int YELLOW_MAX = 220;
  60. const int GREEN_MAX = 220;
  61. const unsigned long NORMAL_MODE_TIMEOUT_MS = 5UL * 60UL * 1000UL;
  62. const unsigned long TRAFFIC_MODE_TIMEOUT_MS = 10UL * 60UL * 1000UL;
  63. const unsigned long LONG_PRESS_MS = 3000;
  64. // =====================================================
  65. // 全局状态
  66. // =====================================================
  67. String currentMode = "init";
  68. unsigned long modeStart = 0;
  69. bool useMQTT = true;
  70. WiFiClient wifiClient;
  71. PubSubClient mqttClient(wifiClient);
  72. BLEServer* pServer = nullptr;
  73. BLECharacteristic* pModeCharacteristic = nullptr;
  74. BLECharacteristic* pConfigCharacteristic = nullptr;
  75. bool bleDeviceConnected = false;
  76. Preferences preferences;
  77. String cfgWifiSsid = "";
  78. String cfgWifiPass = "";
  79. String cfgMqttBroker = "";
  80. uint16_t cfgMqttPort = 1883;
  81. String cfgMqttUser = "";
  82. String cfgMqttPass = "";
  83. String cfgMqttClient = "AI-Light";
  84. String cfgMqttTopic = "opencode/status";
  85. String cfgMqttStatus = "openCodeLight/status";
  86. // =====================================================
  87. // NVS 配置读写
  88. // =====================================================
  89. void loadConfig() {
  90. cfgWifiSsid = preferences.getString("wifi_ssid", "");
  91. cfgWifiPass = preferences.getString("wifi_pass", "");
  92. cfgMqttBroker = preferences.getString("mqtt_broker", "");
  93. cfgMqttPort = preferences.getUInt("mqtt_port", 1883);
  94. cfgMqttUser = preferences.getString("mqtt_user", "");
  95. cfgMqttPass = preferences.getString("mqtt_pass", "");
  96. cfgMqttClient = preferences.getString("mqtt_client", "AI-Light");
  97. cfgMqttTopic = preferences.getString("mqtt_topic", "opencode/status");
  98. cfgMqttStatus = preferences.getString("mqtt_status", "openCodeLight/status");
  99. }
  100. bool isConfigComplete() {
  101. return cfgWifiSsid.length() > 0 && cfgMqttBroker.length() > 0;
  102. }
  103. String getConfigJson() {
  104. JsonDocument doc;
  105. doc["wifi_ssid"] = cfgWifiSsid;
  106. doc["mqtt_broker"] = cfgMqttBroker;
  107. doc["mqtt_port"] = cfgMqttPort;
  108. doc["mqtt_user"] = cfgMqttUser;
  109. doc["mqtt_client"] = cfgMqttClient;
  110. doc["mqtt_topic"] = cfgMqttTopic;
  111. doc["mqtt_status"] = cfgMqttStatus;
  112. doc["comm_mode"] = useMQTT ? 1 : 0;
  113. String out;
  114. serializeJson(doc, out);
  115. return out;
  116. }
  117. void saveConfigFromJson(const String& json) {
  118. JsonDocument doc;
  119. DeserializationError err = deserializeJson(doc, json);
  120. if (err) {
  121. Serial.print("Config JSON parse error: ");
  122. Serial.println(err.c_str());
  123. return;
  124. }
  125. if (doc.containsKey("wifi_ssid"))
  126. preferences.putString("wifi_ssid", doc["wifi_ssid"].as<String>());
  127. if (doc.containsKey("wifi_pass"))
  128. preferences.putString("wifi_pass", doc["wifi_pass"].as<String>());
  129. if (doc.containsKey("mqtt_broker"))
  130. preferences.putString("mqtt_broker", doc["mqtt_broker"].as<String>());
  131. if (doc.containsKey("mqtt_port"))
  132. preferences.putUInt("mqtt_port", doc["mqtt_port"].as<uint16_t>());
  133. if (doc.containsKey("mqtt_user"))
  134. preferences.putString("mqtt_user", doc["mqtt_user"].as<String>());
  135. if (doc.containsKey("mqtt_pass"))
  136. preferences.putString("mqtt_pass", doc["mqtt_pass"].as<String>());
  137. if (doc.containsKey("mqtt_client"))
  138. preferences.putString("mqtt_client", doc["mqtt_client"].as<String>());
  139. if (doc.containsKey("mqtt_topic"))
  140. preferences.putString("mqtt_topic", doc["mqtt_topic"].as<String>());
  141. if (doc.containsKey("mqtt_status"))
  142. preferences.putString("mqtt_status", doc["mqtt_status"].as<String>());
  143. Serial.println("Config saved. Restarting...");
  144. delay(500);
  145. ESP.restart();
  146. }
  147. // =====================================================
  148. // 基础工具函数:公共正极反相输出
  149. // =====================================================
  150. void writeLed(int pin, int value) {
  151. value = constrain(value, 0, 255);
  152. int pwmValue = 255 - value;
  153. ledcWrite(pin, pwmValue);
  154. }
  155. void allOff() {
  156. writeLed(RED_PIN, 0);
  157. writeLed(YELLOW_PIN, 0);
  158. writeLed(GREEN_PIN, 0);
  159. }
  160. void setOnly(int red, int yellow, int green) {
  161. writeLed(RED_PIN, constrain(red, 0, RED_MAX));
  162. writeLed(YELLOW_PIN, constrain(yellow, 0, YELLOW_MAX));
  163. writeLed(GREEN_PIN, constrain(green, 0, GREEN_MAX));
  164. }
  165. int triWave(unsigned long t, unsigned long period, int maxValue) {
  166. unsigned long x = t % period;
  167. if (x < period / 2) return map(x, 0, period / 2, 0, maxValue);
  168. return map(x, period / 2, period, maxValue, 0);
  169. }
  170. int fadeInOutBrightness(
  171. unsigned long t, unsigned long fadeIn, unsigned long hold,
  172. unsigned long fadeOut, unsigned long offTime, int maxValue
  173. ) {
  174. unsigned long total = fadeIn + hold + fadeOut + offTime;
  175. unsigned long x = t % total;
  176. if (x < fadeIn) return map(x, 0, fadeIn, 0, maxValue);
  177. x -= fadeIn;
  178. if (x < hold) return maxValue;
  179. x -= hold;
  180. if (x < fadeOut) return map(x, 0, fadeOut, maxValue, 0);
  181. return 0;
  182. }
  183. void fadeToStatic(int targetRed, int targetYellow, int targetGreen, int fadeMs = 80) {
  184. allOff();
  185. int steps = 12;
  186. int delayPerStep = max(1, fadeMs / steps);
  187. for (int i = 0; i <= steps; i++) {
  188. float p = (float)i / steps;
  189. setOnly(targetRed * p, targetYellow * p, targetGreen * p);
  190. delay(delayPerStep);
  191. }
  192. }
  193. // =====================================================
  194. // 模式处理
  195. // =====================================================
  196. bool isValidMode(String mode) {
  197. return (
  198. mode == "red" || mode == "yellow" || mode == "green" ||
  199. mode == "busy" || mode == "error" || mode == "thinking" ||
  200. mode == "ai" || mode == "success" || mode == "traffic" ||
  201. mode == "alarm" || mode == "init" || mode == "off" || mode == "idle"
  202. );
  203. }
  204. void publishStatus() {
  205. if (useMQTT && mqttClient.connected()) {
  206. mqttClient.publish(cfgMqttStatus.c_str(), currentMode.c_str(), true);
  207. }
  208. }
  209. void notifyMode() {
  210. if (pModeCharacteristic) {
  211. pModeCharacteristic->setValue(currentMode.c_str());
  212. if (bleDeviceConnected) pModeCharacteristic->notify();
  213. }
  214. }
  215. void setMode(String mode) {
  216. mode.trim();
  217. mode.toLowerCase();
  218. if (!isValidMode(mode)) {
  219. Serial.print("Unknown mode: ");
  220. Serial.println(mode);
  221. return;
  222. }
  223. if (mode == "idle") mode = "traffic";
  224. currentMode = mode;
  225. modeStart = millis();
  226. Serial.print("Mode: ");
  227. Serial.println(currentMode);
  228. if (mode == "red") fadeToStatic(RED_MAX, 0, 0, 80);
  229. else if (mode == "yellow") fadeToStatic(0, YELLOW_MAX, 0, 80);
  230. else if (mode == "green") fadeToStatic(0, 0, GREEN_MAX, 80);
  231. else if (mode == "success") setOnly(0, 0, GREEN_MAX);
  232. else if (mode == "off") allOff();
  233. publishStatus();
  234. notifyMode();
  235. }
  236. void autoTimeoutCheck() {
  237. unsigned long elapsed = millis() - modeStart;
  238. if (currentMode == "off") return;
  239. if (currentMode == "traffic") {
  240. if (elapsed >= TRAFFIC_MODE_TIMEOUT_MS) setMode("off");
  241. return;
  242. }
  243. if (elapsed >= NORMAL_MODE_TIMEOUT_MS) setMode("traffic");
  244. }
  245. // =====================================================
  246. // 灯效模式
  247. // =====================================================
  248. void updateBusy() {
  249. unsigned long t = millis() - modeStart;
  250. int y = fadeInOutBrightness(t, 80, 500, 120, 500, YELLOW_MAX);
  251. setOnly(0, y, 0);
  252. }
  253. void updateError() {
  254. unsigned long t = millis() - modeStart;
  255. int r = fadeInOutBrightness(t, 40, 180, 80, 180, RED_MAX);
  256. setOnly(r, 0, 0);
  257. }
  258. void updateThinking() {
  259. unsigned long t = millis() - modeStart;
  260. const unsigned long period = 1050;
  261. unsigned long x = t % period;
  262. int g = 0, y = 0, r = 0;
  263. if (x < 350) {
  264. g = map(x, 0, 350, GREEN_MAX, 70);
  265. y = map(x, 0, 350, 20, YELLOW_MAX);
  266. } else if (x < 700) {
  267. unsigned long p = x - 350;
  268. g = map(p, 0, 350, 70, 0);
  269. y = map(p, 0, 350, YELLOW_MAX, 70);
  270. r = map(p, 0, 350, 20, RED_MAX);
  271. } else {
  272. unsigned long p = x - 700;
  273. g = map(p, 0, 350, 20, GREEN_MAX);
  274. y = map(p, 0, 350, 70, 0);
  275. r = map(p, 0, 350, RED_MAX, 70);
  276. }
  277. setOnly(r, y, g);
  278. }
  279. void updateAi() {
  280. unsigned long t = millis() - modeStart;
  281. const unsigned long period = 1800;
  282. unsigned long x = t % period;
  283. int g = triWave((x + 0) % period, period, 150);
  284. int y = triWave((x + period / 3) % period, period, 140);
  285. int r = triWave((x + 2 * period / 3) % period, period, 170);
  286. setOnly(r, y, g);
  287. }
  288. void updateSuccess() { setOnly(0, 0, GREEN_MAX); }
  289. void updateAlarm() {
  290. unsigned long t = millis() - modeStart;
  291. const unsigned long phaseMs = 260;
  292. int phase = (t / phaseMs) % 2;
  293. unsigned long inside = t % phaseMs;
  294. int brightness;
  295. if (inside < 60) brightness = map(inside, 0, 60, 0, 255);
  296. else if (inside < 180) brightness = 255;
  297. else brightness = map(inside, 180, phaseMs, 255, 0);
  298. if (phase == 0) setOnly(brightness, 0, 0);
  299. else setOnly(0, min(brightness, YELLOW_MAX), 0);
  300. }
  301. void updateTraffic() {
  302. unsigned long t = (millis() - modeStart) % 15000;
  303. if (t < 5000) {
  304. setOnly(0, 0, GREEN_MAX);
  305. } else if (t < 6500) {
  306. unsigned long phase = (t - 5000) % 500;
  307. int g = 0;
  308. if (phase < 60) g = map(phase, 0, 60, 0, GREEN_MAX);
  309. else if (phase < 230) g = GREEN_MAX;
  310. else if (phase < 320) g = map(phase, 230, 320, GREEN_MAX, 0);
  311. setOnly(0, 0, g);
  312. } else if (t < 8500) {
  313. setOnly(0, YELLOW_MAX, 0);
  314. } else if (t < 13500) {
  315. setOnly(RED_MAX, 0, 0);
  316. } else {
  317. unsigned long phase = (t - 13500) % 500;
  318. int r = 0;
  319. if (phase < 60) r = map(phase, 0, 60, 0, RED_MAX);
  320. else if (phase < 230) r = RED_MAX;
  321. else if (phase < 320) r = map(phase, 230, 320, RED_MAX, 0);
  322. setOnly(r, 0, 0);
  323. }
  324. }
  325. void updateInit() {
  326. unsigned long t = millis() - modeStart;
  327. const unsigned long period = 2500;
  328. unsigned long x = t % period;
  329. int brightness;
  330. if (x < 800) brightness = map(x, 0, 800, 0, 200);
  331. else if (x < 1200) brightness = 200;
  332. else if (x < 2000) brightness = map(x, 1200, 2000, 200, 0);
  333. else brightness = 0;
  334. setOnly(brightness, brightness, brightness);
  335. }
  336. void breathingGreen(int times) {
  337. const unsigned long period = 2500;
  338. for (int i = 0; i < times; i++) {
  339. unsigned long start = millis();
  340. while (millis() - start < period) {
  341. unsigned long t = millis() - start;
  342. int brightness;
  343. if (t < 800) brightness = map(t, 0, 800, 0, GREEN_MAX);
  344. else if (t < 1200) brightness = GREEN_MAX;
  345. else if (t < 2000) brightness = map(t, 1200, 2000, GREEN_MAX, 0);
  346. else brightness = 0;
  347. setOnly(0, 0, brightness);
  348. delay(5);
  349. }
  350. }
  351. allOff();
  352. }
  353. // =====================================================
  354. // BOOT 按钮处理
  355. // =====================================================
  356. unsigned long bootPressStart = 0;
  357. bool bootWasPressed = false;
  358. bool switchTriggered = false;
  359. void checkBootButton() {
  360. bool pressed = (digitalRead(BOOT_PIN) == LOW);
  361. if (pressed && !bootWasPressed) {
  362. bootPressStart = millis();
  363. bootWasPressed = true;
  364. }
  365. if (pressed && bootWasPressed && !switchTriggered) {
  366. if (millis() - bootPressStart >= LONG_PRESS_MS) {
  367. switchTriggered = true;
  368. Serial.println("BOOT long press -> switching mode...");
  369. allOff();
  370. for (int i = 0; i < 3; i++) {
  371. setOnly(100, 100, 100); delay(100);
  372. allOff(); delay(100);
  373. }
  374. useMQTT = !useMQTT;
  375. preferences.putUInt("comm_mode", useMQTT ? 1 : 0);
  376. delay(200);
  377. ESP.restart();
  378. }
  379. }
  380. if (!pressed) {
  381. bootWasPressed = false;
  382. switchTriggered = false;
  383. }
  384. }
  385. // =====================================================
  386. // 状态 LED
  387. // =====================================================
  388. unsigned long lastStatusLedToggle = 0;
  389. bool statusLedState = false;
  390. void updateStatusLed() {
  391. bool connected = useMQTT ? (WiFi.status() == WL_CONNECTED) : bleDeviceConnected;
  392. if (connected) {
  393. digitalWrite(WIFI_LED_PIN, LOW);
  394. return;
  395. }
  396. if (millis() - lastStatusLedToggle >= 500) {
  397. statusLedState = !statusLedState;
  398. digitalWrite(WIFI_LED_PIN, statusLedState ? LOW : HIGH);
  399. lastStatusLedToggle = millis();
  400. }
  401. }
  402. // =====================================================
  403. // BLE
  404. // =====================================================
  405. class ServerCallbacks : public BLEServerCallbacks {
  406. void onConnect(BLEServer* s) { bleDeviceConnected = true; Serial.println("BLE connected."); }
  407. void onDisconnect(BLEServer* s) { bleDeviceConnected = false; Serial.println("BLE disconnected."); BLEDevice::startAdvertising(); }
  408. };
  409. class ModeCharCallbacks : public BLECharacteristicCallbacks {
  410. void onWrite(BLECharacteristic* c) { setMode(c->getValue()); }
  411. void onRead(BLECharacteristic* c) { c->setValue(currentMode.c_str()); }
  412. };
  413. class ConfigCharCallbacks : public BLECharacteristicCallbacks {
  414. void onWrite(BLECharacteristic* c) { saveConfigFromJson(c->getValue()); }
  415. void onRead(BLECharacteristic* c) { c->setValue(getConfigJson().c_str()); }
  416. };
  417. void setupBLE() {
  418. Serial.println("Starting BLE...");
  419. BLEDevice::init(BLE_DEVICE_NAME);
  420. pServer = BLEDevice::createServer();
  421. pServer->setCallbacks(new ServerCallbacks());
  422. BLEService* pService = pServer->createService(SERVICE_UUID);
  423. pModeCharacteristic = pService->createCharacteristic(MODE_CHAR_UUID,
  424. BLECharacteristic::PROPERTY_READ | BLECharacteristic::PROPERTY_WRITE | BLECharacteristic::PROPERTY_NOTIFY);
  425. pModeCharacteristic->setCallbacks(new ModeCharCallbacks());
  426. pModeCharacteristic->setValue(currentMode.c_str());
  427. pModeCharacteristic->addDescriptor(new BLE2902());
  428. pConfigCharacteristic = pService->createCharacteristic(CONFIG_CHAR_UUID,
  429. BLECharacteristic::PROPERTY_READ | BLECharacteristic::PROPERTY_WRITE);
  430. pConfigCharacteristic->setCallbacks(new ConfigCharCallbacks());
  431. pService->start();
  432. BLEAdvertising* pAdvertising = BLEDevice::getAdvertising();
  433. pAdvertising->addServiceUUID(SERVICE_UUID);
  434. pAdvertising->setScanResponse(true);
  435. pAdvertising->setMinPreferred(0x06);
  436. pAdvertising->setMinPreferred(0x12);
  437. BLEDevice::startAdvertising();
  438. Serial.println("BLE advertising.");
  439. }
  440. // =====================================================
  441. // MQTT
  442. // =====================================================
  443. void mqttCallback(char* topic, byte* payload, unsigned int length) {
  444. String message = "";
  445. for (unsigned int i = 0; i < length; i++) message += (char)payload[i];
  446. Serial.print("MQTT: "); Serial.print(topic); Serial.print(" -> "); Serial.println(message);
  447. JsonDocument doc;
  448. if (deserializeJson(doc, message)) { setMode(message); return; }
  449. const char* code = doc["code"];
  450. if (code) {
  451. String c = String(code);
  452. if (c == "idle") setMode("idle");
  453. else if (c == "busy" || c == "running") setMode("busy");
  454. else if (c == "retry" || c == "permission") setMode("alarm");
  455. else if (c == "pending") setMode("yellow");
  456. else if (c == "reasoning") setMode("thinking");
  457. else if (c == "using_tool") setMode("ai");
  458. else if (c == "error") setMode("error");
  459. else setMode(c);
  460. }
  461. }
  462. void connectWiFi() {
  463. Serial.print("Connecting WiFi");
  464. WiFi.mode(WIFI_STA);
  465. WiFi.begin(cfgWifiSsid.c_str(), cfgWifiPass.c_str());
  466. for (int retry = 1; retry <= 5; retry++) {
  467. Serial.printf("\nWiFi %d/5", retry);
  468. int attempts = 0;
  469. while (WiFi.status() != WL_CONNECTED && attempts < 60) {
  470. delay(5); setOnly(100, 100, 100); updateStatusLed(); Serial.print("."); attempts++;
  471. }
  472. if (WiFi.status() == WL_CONNECTED) break;
  473. Serial.println("\nFailed, retrying..."); delay(2000);
  474. }
  475. if (WiFi.status() != WL_CONNECTED) {
  476. Serial.println("\nWiFi failed. Restarting...");
  477. delay(1000);
  478. ESP.restart();
  479. }
  480. Serial.printf("\nWiFi OK. IP: %s\n", WiFi.localIP().toString().c_str());
  481. digitalWrite(WIFI_LED_PIN, LOW);
  482. }
  483. void connectMQTT() {
  484. mqttClient.setServer(cfgMqttBroker.c_str(), cfgMqttPort);
  485. mqttClient.setCallback(mqttCallback);
  486. Serial.print("MQTT connecting");
  487. int attempts = 0;
  488. while (!mqttClient.connected()) {
  489. if (mqttClient.connect(cfgMqttClient.c_str(), cfgMqttUser.c_str(), cfgMqttPass.c_str())) {
  490. Serial.println("\nMQTT OK.");
  491. mqttClient.subscribe(cfgMqttTopic.c_str());
  492. allOff(); breathingGreen(3); publishStatus();
  493. return;
  494. }
  495. Serial.print("."); delay(500);
  496. if (++attempts > 60) { Serial.println("\nMQTT failed!"); attempts = 0; }
  497. }
  498. }
  499. void checkMQTTConnection() {
  500. if (useMQTT && !mqttClient.connected()) { Serial.println("MQTT reconnecting..."); connectMQTT(); }
  501. }
  502. // =====================================================
  503. // 初始化
  504. // =====================================================
  505. void setup() {
  506. Serial.begin(115200);
  507. delay(500);
  508. ledcAttach(RED_PIN, PWM_FREQ, PWM_RESOLUTION);
  509. ledcAttach(YELLOW_PIN, PWM_FREQ, PWM_RESOLUTION);
  510. ledcAttach(GREEN_PIN, PWM_FREQ, PWM_RESOLUTION);
  511. pinMode(WIFI_LED_PIN, OUTPUT);
  512. pinMode(BOOT_PIN, INPUT_PULLUP);
  513. digitalWrite(WIFI_LED_PIN, HIGH);
  514. allOff();
  515. preferences.begin("ai-light", false);
  516. useMQTT = (preferences.getUInt("comm_mode", 1) != 0);
  517. loadConfig();
  518. Serial.println();
  519. Serial.println("=== AI-Light ===");
  520. Serial.printf("Mode: %s\n", useMQTT ? "MQTT" : "BLE-only");
  521. Serial.printf("WiFi: %s\n", cfgWifiSsid.length() > 0 ? cfgWifiSsid.c_str() : "(not set)");
  522. Serial.printf("MQTT: %s\n", cfgMqttBroker.length() > 0 ? cfgMqttBroker.c_str() : "(not set)");
  523. currentMode = "init";
  524. modeStart = millis();
  525. setupBLE();
  526. if (useMQTT && isConfigComplete()) {
  527. connectWiFi();
  528. connectMQTT();
  529. } else if (useMQTT) {
  530. Serial.println("Config incomplete. Use BLE to configure.");
  531. }
  532. setMode("traffic");
  533. Serial.println("Long press BOOT (3s) to switch mode.");
  534. }
  535. // =====================================================
  536. // 主循环
  537. // =====================================================
  538. void loop() {
  539. updateStatusLed();
  540. checkBootButton();
  541. if (useMQTT) {
  542. checkMQTTConnection();
  543. mqttClient.loop();
  544. }
  545. autoTimeoutCheck();
  546. if (currentMode == "busy") updateBusy();
  547. else if (currentMode == "error") updateError();
  548. else if (currentMode == "thinking") updateThinking();
  549. else if (currentMode == "ai") updateAi();
  550. else if (currentMode == "success") updateSuccess();
  551. else if (currentMode == "traffic") updateTraffic();
  552. else if (currentMode == "alarm") updateAlarm();
  553. else if (currentMode == "init") updateInit();
  554. else if (currentMode == "off") allOff();
  555. delay(5);
  556. }