ai_light.ino 24 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810
  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. #include <HTTPClient.h>
  10. #include <Update.h>
  11. // =====================================================
  12. // ESP32-C3 SuperMini + 原玩具公共正极灯板:BLE + MQTT 双模式
  13. //
  14. // 功能:
  15. // - 开机始终启动 BLE,支持灯效控制 + WiFi/MQTT 配置
  16. // - MQTT 模式下同时连接 WiFi/MQTT
  17. // - 运行时长按 BOOT 按钮 3 秒 → 切换 BLE/MQTT 模式并重启
  18. //
  19. // BLE 配置:
  20. // Service: b8b7e001-7a6b-4f4f-9a8b-11c0ffee0001
  21. // Mode: b8b7e002-... (读写/通知 - 灯效模式)
  22. // Config: b8b7e003-... (读写 - WiFi/MQTT 配置 JSON)
  23. //
  24. // 配置 JSON 格式(写入 Config 特征):
  25. // {
  26. // "wifi_ssid": "xxx",
  27. // "wifi_pass": "xxx",
  28. // "mqtt_broker": "192.168.1.100",
  29. // "mqtt_port": 1883,
  30. // "mqtt_user": "user",
  31. // "mqtt_pass": "pass",
  32. // "mqtt_client": "AI-Light",
  33. // "mqtt_topic": "opencode/status",
  34. // "mqtt_status": "openCodeLight/status",
  35. // "ota_url": "https://example.com/firmware.bin"
  36. // }
  37. //
  38. // 接线方式(V3 版本):
  39. // ESP32 3.3V -> 原灯板 + / 原电池正极
  40. // ESP32 IO2 -> 220Ω -> L1 控制点 = 黄灯
  41. // ESP32 IO3 -> 220Ω -> L2 控制点 = 绿灯
  42. // ESP32 IO4 -> 220Ω -> L3 控制点 = 红灯
  43. // =====================================================
  44. // =====================================================
  45. // BLE 配置
  46. // =====================================================
  47. const char* BLE_DEVICE_NAME = "AI-Light";
  48. const char* FW_VERSION = "1.0.0";
  49. #define SERVICE_UUID "b8b7e001-7a6b-4f4f-9a8b-11c0ffee0001"
  50. #define MODE_CHAR_UUID "b8b7e002-7a6b-4f4f-9a8b-11c0ffee0001"
  51. #define CONFIG_CHAR_UUID "b8b7e003-7a6b-4f4f-9a8b-11c0ffee0001"
  52. // =====================================================
  53. // 引脚定义(红绿黄可通过配置动态修改,状态灯和按钮固定)
  54. // =====================================================
  55. int redPin = 4;
  56. int greenPin = 3;
  57. int yellowPin = 2;
  58. const int STATUS_PIN = 8;
  59. const int BUTTON_PIN = 9;
  60. const int PWM_FREQ = 5000;
  61. const int PWM_RESOLUTION = 8;
  62. const int RED_MAX = 255;
  63. const int YELLOW_MAX = 220;
  64. const int GREEN_MAX = 220;
  65. const unsigned long NORMAL_MODE_TIMEOUT_MS = 5UL * 60UL * 1000UL;
  66. const unsigned long TRAFFIC_MODE_TIMEOUT_MS = 10UL * 60UL * 1000UL;
  67. const unsigned long LONG_PRESS_MS = 3000;
  68. // =====================================================
  69. // 全局状态
  70. // =====================================================
  71. String currentMode = "init";
  72. unsigned long modeStart = 0;
  73. bool useMQTT = true;
  74. WiFiClient wifiClient;
  75. PubSubClient mqttClient(wifiClient);
  76. BLEServer* pServer = nullptr;
  77. BLECharacteristic* pModeCharacteristic = nullptr;
  78. BLECharacteristic* pConfigCharacteristic = nullptr;
  79. bool bleDeviceConnected = false;
  80. bool wifiConnected = false;
  81. Preferences preferences;
  82. String cfgWifiSsid = "";
  83. String cfgWifiPass = "";
  84. String cfgMqttBroker = "";
  85. uint16_t cfgMqttPort = 1883;
  86. String cfgMqttUser = "";
  87. String cfgMqttPass = "";
  88. String cfgMqttClient = "AI-Light";
  89. String cfgMqttTopic = "opencode/status";
  90. String cfgMqttStatus = "openCodeLight/status";
  91. String cfgOtaUrl = "";
  92. bool otaInProgress = false;
  93. // =====================================================
  94. // NVS 配置读写
  95. // =====================================================
  96. void loadConfig() {
  97. cfgWifiSsid = preferences.getString("wifi_ssid", "");
  98. cfgWifiPass = preferences.getString("wifi_pass", "");
  99. cfgMqttBroker = preferences.getString("mqtt_broker", "");
  100. cfgMqttPort = preferences.getUInt("mqtt_port", 1883);
  101. cfgMqttUser = preferences.getString("mqtt_user", "");
  102. cfgMqttPass = preferences.getString("mqtt_pass", "");
  103. cfgMqttClient = preferences.getString("mqtt_client", "AI-Light");
  104. cfgMqttTopic = preferences.getString("mqtt_topic", "opencode/status");
  105. cfgMqttStatus = preferences.getString("mqtt_status", "openCodeLight/status");
  106. redPin = preferences.getUInt("pin_red", 4);
  107. greenPin = preferences.getUInt("pin_green", 3);
  108. yellowPin = preferences.getUInt("pin_yellow", 2);
  109. cfgOtaUrl = preferences.getString("ota_url", "");
  110. }
  111. bool isConfigComplete() {
  112. return cfgWifiSsid.length() > 0 && cfgMqttBroker.length() > 0;
  113. }
  114. String getConfigJson() {
  115. JsonDocument doc;
  116. doc["fw_version"] = FW_VERSION;
  117. doc["wifi_ssid"] = cfgWifiSsid;
  118. doc["mqtt_broker"] = cfgMqttBroker;
  119. doc["mqtt_port"] = cfgMqttPort;
  120. doc["mqtt_user"] = cfgMqttUser;
  121. doc["mqtt_client"] = cfgMqttClient;
  122. doc["mqtt_topic"] = cfgMqttTopic;
  123. doc["mqtt_status"] = cfgMqttStatus;
  124. doc["comm_mode"] = useMQTT ? 1 : 0;
  125. doc["pin_red"] = redPin;
  126. doc["pin_green"] = greenPin;
  127. doc["pin_yellow"] = yellowPin;
  128. doc["ota_url"] = cfgOtaUrl;
  129. String out;
  130. serializeJson(doc, out);
  131. return out;
  132. }
  133. void saveConfigFromJson(const String& json) {
  134. JsonDocument doc;
  135. DeserializationError err = deserializeJson(doc, json);
  136. if (err) {
  137. Serial.print("Config JSON parse error: ");
  138. Serial.println(err.c_str());
  139. return;
  140. }
  141. if (doc.containsKey("wifi_ssid"))
  142. preferences.putString("wifi_ssid", doc["wifi_ssid"].as<String>());
  143. if (doc.containsKey("wifi_pass"))
  144. preferences.putString("wifi_pass", doc["wifi_pass"].as<String>());
  145. if (doc.containsKey("mqtt_broker"))
  146. preferences.putString("mqtt_broker", doc["mqtt_broker"].as<String>());
  147. if (doc.containsKey("mqtt_port"))
  148. preferences.putUInt("mqtt_port", doc["mqtt_port"].as<uint16_t>());
  149. if (doc.containsKey("mqtt_user"))
  150. preferences.putString("mqtt_user", doc["mqtt_user"].as<String>());
  151. if (doc.containsKey("mqtt_pass"))
  152. preferences.putString("mqtt_pass", doc["mqtt_pass"].as<String>());
  153. if (doc.containsKey("mqtt_client"))
  154. preferences.putString("mqtt_client", doc["mqtt_client"].as<String>());
  155. if (doc.containsKey("mqtt_topic"))
  156. preferences.putString("mqtt_topic", doc["mqtt_topic"].as<String>());
  157. if (doc.containsKey("mqtt_status"))
  158. preferences.putString("mqtt_status", doc["mqtt_status"].as<String>());
  159. if (doc.containsKey("pin_red"))
  160. preferences.putUInt("pin_red", doc["pin_red"].as<uint8_t>());
  161. if (doc.containsKey("pin_green"))
  162. preferences.putUInt("pin_green", doc["pin_green"].as<uint8_t>());
  163. if (doc.containsKey("pin_yellow"))
  164. preferences.putUInt("pin_yellow", doc["pin_yellow"].as<uint8_t>());
  165. if (doc.containsKey("ota_url"))
  166. preferences.putString("ota_url", doc["ota_url"].as<String>());
  167. Serial.println("Config saved. Restarting...");
  168. delay(500);
  169. ESP.restart();
  170. }
  171. // =====================================================
  172. // 基础工具函数:公共正极反相输出
  173. // =====================================================
  174. void writeLed(int pin, int value) {
  175. value = constrain(value, 0, 255);
  176. int pwmValue = 255 - value;
  177. ledcWrite(pin, pwmValue);
  178. }
  179. void allOff() {
  180. writeLed(redPin, 0);
  181. writeLed(yellowPin, 0);
  182. writeLed(greenPin, 0);
  183. }
  184. void setOnly(int red, int yellow, int green) {
  185. writeLed(redPin, constrain(red, 0, RED_MAX));
  186. writeLed(yellowPin, constrain(yellow, 0, YELLOW_MAX));
  187. writeLed(greenPin, constrain(green, 0, GREEN_MAX));
  188. }
  189. int triWave(unsigned long t, unsigned long period, int maxValue) {
  190. unsigned long x = t % period;
  191. if (x < period / 2) return map(x, 0, period / 2, 0, maxValue);
  192. return map(x, period / 2, period, maxValue, 0);
  193. }
  194. int fadeInOutBrightness(
  195. unsigned long t, unsigned long fadeIn, unsigned long hold,
  196. unsigned long fadeOut, unsigned long offTime, int maxValue
  197. ) {
  198. unsigned long total = fadeIn + hold + fadeOut + offTime;
  199. unsigned long x = t % total;
  200. if (x < fadeIn) return map(x, 0, fadeIn, 0, maxValue);
  201. x -= fadeIn;
  202. if (x < hold) return maxValue;
  203. x -= hold;
  204. if (x < fadeOut) return map(x, 0, fadeOut, maxValue, 0);
  205. return 0;
  206. }
  207. void fadeToStatic(int targetRed, int targetYellow, int targetGreen, int fadeMs = 80) {
  208. allOff();
  209. int steps = 12;
  210. int delayPerStep = max(1, fadeMs / steps);
  211. for (int i = 0; i <= steps; i++) {
  212. float p = (float)i / steps;
  213. setOnly(targetRed * p, targetYellow * p, targetGreen * p);
  214. delay(delayPerStep);
  215. }
  216. }
  217. // =====================================================
  218. // 模式处理
  219. // =====================================================
  220. bool isValidMode(String mode) {
  221. return (
  222. mode == "red" || mode == "yellow" || mode == "green" ||
  223. mode == "busy" || mode == "error" || mode == "thinking" ||
  224. mode == "ai" || mode == "success" || mode == "traffic" ||
  225. mode == "alarm" || mode == "init" || mode == "off" || mode == "idle"
  226. );
  227. }
  228. void publishStatus() {
  229. if (useMQTT && mqttClient.connected()) {
  230. mqttClient.publish(cfgMqttStatus.c_str(), currentMode.c_str(), true);
  231. }
  232. }
  233. void notifyMode() {
  234. if (pModeCharacteristic) {
  235. pModeCharacteristic->setValue(currentMode.c_str());
  236. if (bleDeviceConnected) pModeCharacteristic->notify();
  237. }
  238. }
  239. void setMode(String mode) {
  240. mode.trim();
  241. mode.toLowerCase();
  242. if (!isValidMode(mode)) {
  243. Serial.print("Unknown mode: ");
  244. Serial.println(mode);
  245. return;
  246. }
  247. if (mode == "idle") mode = "traffic";
  248. if (mode == currentMode) return;
  249. currentMode = mode;
  250. modeStart = millis();
  251. Serial.print("Mode: ");
  252. Serial.println(currentMode);
  253. if (mode == "red") fadeToStatic(RED_MAX, 0, 0, 80);
  254. else if (mode == "yellow") fadeToStatic(0, YELLOW_MAX, 0, 80);
  255. else if (mode == "green") fadeToStatic(0, 0, GREEN_MAX, 80);
  256. else if (mode == "success") setOnly(0, 0, GREEN_MAX);
  257. else if (mode == "off") allOff();
  258. publishStatus();
  259. notifyMode();
  260. }
  261. void autoTimeoutCheck() {
  262. unsigned long elapsed = millis() - modeStart;
  263. if (currentMode == "off") return;
  264. if (currentMode == "traffic") {
  265. if (elapsed >= TRAFFIC_MODE_TIMEOUT_MS) setMode("off");
  266. return;
  267. }
  268. if (elapsed >= NORMAL_MODE_TIMEOUT_MS) setMode("traffic");
  269. }
  270. // =====================================================
  271. // 灯效模式
  272. // =====================================================
  273. void updateBusy() {
  274. unsigned long t = millis() - modeStart;
  275. int y = fadeInOutBrightness(t, 80, 500, 120, 500, YELLOW_MAX);
  276. setOnly(0, y, 0);
  277. }
  278. void updateError() {
  279. unsigned long t = millis() - modeStart;
  280. int r = fadeInOutBrightness(t, 40, 180, 80, 180, RED_MAX);
  281. setOnly(r, 0, 0);
  282. }
  283. void updateThinking() {
  284. unsigned long t = millis() - modeStart;
  285. const unsigned long period = 1050;
  286. unsigned long x = t % period;
  287. int g = 0, y = 0, r = 0;
  288. if (x < 350) {
  289. g = map(x, 0, 350, GREEN_MAX, 70);
  290. y = map(x, 0, 350, 20, YELLOW_MAX);
  291. } else if (x < 700) {
  292. unsigned long p = x - 350;
  293. g = map(p, 0, 350, 70, 0);
  294. y = map(p, 0, 350, YELLOW_MAX, 70);
  295. r = map(p, 0, 350, 20, RED_MAX);
  296. } else {
  297. unsigned long p = x - 700;
  298. g = map(p, 0, 350, 20, GREEN_MAX);
  299. y = map(p, 0, 350, 70, 0);
  300. r = map(p, 0, 350, RED_MAX, 70);
  301. }
  302. setOnly(r, y, g);
  303. }
  304. void updateAi() {
  305. unsigned long t = millis() - modeStart;
  306. const unsigned long period = 1800;
  307. unsigned long x = t % period;
  308. int g = triWave((x + 0) % period, period, 150);
  309. int y = triWave((x + period / 3) % period, period, 140);
  310. int r = triWave((x + 2 * period / 3) % period, period, 170);
  311. setOnly(r, y, g);
  312. }
  313. void updateSuccess() { setOnly(0, 0, GREEN_MAX); }
  314. void updateAlarm() {
  315. unsigned long t = millis() - modeStart;
  316. const unsigned long phaseMs = 260;
  317. int phase = (t / phaseMs) % 2;
  318. unsigned long inside = t % phaseMs;
  319. int brightness;
  320. if (inside < 60) brightness = map(inside, 0, 60, 0, 255);
  321. else if (inside < 180) brightness = 255;
  322. else brightness = map(inside, 180, phaseMs, 255, 0);
  323. if (phase == 0) setOnly(brightness, 0, 0);
  324. else setOnly(0, min(brightness, YELLOW_MAX), 0);
  325. }
  326. void updateTraffic() {
  327. unsigned long t = (millis() - modeStart) % 15000;
  328. if (t < 5000) {
  329. setOnly(0, 0, GREEN_MAX);
  330. } else if (t < 6500) {
  331. unsigned long phase = (t - 5000) % 500;
  332. int g = 0;
  333. if (phase < 60) g = map(phase, 0, 60, 0, GREEN_MAX);
  334. else if (phase < 230) g = GREEN_MAX;
  335. else if (phase < 320) g = map(phase, 230, 320, GREEN_MAX, 0);
  336. setOnly(0, 0, g);
  337. } else if (t < 8500) {
  338. setOnly(0, YELLOW_MAX, 0);
  339. } else if (t < 13500) {
  340. setOnly(RED_MAX, 0, 0);
  341. } else {
  342. unsigned long phase = (t - 13500) % 500;
  343. int r = 0;
  344. if (phase < 60) r = map(phase, 0, 60, 0, RED_MAX);
  345. else if (phase < 230) r = RED_MAX;
  346. else if (phase < 320) r = map(phase, 230, 320, RED_MAX, 0);
  347. setOnly(r, 0, 0);
  348. }
  349. }
  350. void updateInit() {
  351. unsigned long t = millis() - modeStart;
  352. const unsigned long period = 2500;
  353. unsigned long x = t % period;
  354. int brightness;
  355. if (x < 800) brightness = map(x, 0, 800, 0, 200);
  356. else if (x < 1200) brightness = 200;
  357. else if (x < 2000) brightness = map(x, 1200, 2000, 200, 0);
  358. else brightness = 0;
  359. setOnly(brightness, brightness, brightness);
  360. }
  361. void breathingGreen(int times) {
  362. const unsigned long period = 2500;
  363. for (int i = 0; i < times; i++) {
  364. unsigned long start = millis();
  365. while (millis() - start < period) {
  366. unsigned long t = millis() - start;
  367. int brightness;
  368. if (t < 800) brightness = map(t, 0, 800, 0, GREEN_MAX);
  369. else if (t < 1200) brightness = GREEN_MAX;
  370. else if (t < 2000) brightness = map(t, 1200, 2000, GREEN_MAX, 0);
  371. else brightness = 0;
  372. setOnly(0, 0, brightness);
  373. delay(5);
  374. }
  375. }
  376. allOff();
  377. }
  378. // =====================================================
  379. // BOOT 按钮处理
  380. // =====================================================
  381. unsigned long bootPressStart = 0;
  382. bool bootWasPressed = false;
  383. bool switchTriggered = false;
  384. void checkBootButton() {
  385. bool pressed = (digitalRead(BUTTON_PIN) == LOW);
  386. if (pressed && !bootWasPressed) {
  387. bootPressStart = millis();
  388. bootWasPressed = true;
  389. }
  390. if (pressed && bootWasPressed && !switchTriggered) {
  391. if (millis() - bootPressStart >= LONG_PRESS_MS) {
  392. switchTriggered = true;
  393. Serial.println("BOOT long press -> switching mode...");
  394. allOff();
  395. for (int i = 0; i < 3; i++) {
  396. setOnly(100, 100, 100); delay(100);
  397. allOff(); delay(100);
  398. }
  399. useMQTT = !useMQTT;
  400. preferences.putUInt("comm_mode", useMQTT ? 1 : 0);
  401. delay(200);
  402. ESP.restart();
  403. }
  404. }
  405. if (!pressed) {
  406. bootWasPressed = false;
  407. switchTriggered = false;
  408. }
  409. }
  410. // =====================================================
  411. // 状态 LED
  412. // =====================================================
  413. unsigned long lastStatusLedToggle = 0;
  414. bool statusLedState = false;
  415. void updateStatusLed() {
  416. bool connected = (useMQTT && wifiConnected) ? (WiFi.status() == WL_CONNECTED) : bleDeviceConnected;
  417. if (connected) {
  418. digitalWrite(STATUS_PIN, LOW);
  419. return;
  420. }
  421. if (millis() - lastStatusLedToggle >= 500) {
  422. statusLedState = !statusLedState;
  423. digitalWrite(STATUS_PIN, statusLedState ? LOW : HIGH);
  424. lastStatusLedToggle = millis();
  425. }
  426. }
  427. // =====================================================
  428. // BLE
  429. // =====================================================
  430. class ServerCallbacks : public BLEServerCallbacks {
  431. void onConnect(BLEServer* s) { bleDeviceConnected = true; Serial.println("BLE connected."); }
  432. void onDisconnect(BLEServer* s) { bleDeviceConnected = false; Serial.println("BLE disconnected."); delay(100); BLEDevice::startAdvertising(); }
  433. };
  434. class ModeCharCallbacks : public BLECharacteristicCallbacks {
  435. void onWrite(BLECharacteristic* c) {
  436. String val = c->getValue();
  437. if (val == "ota") {
  438. performOTA();
  439. return;
  440. }
  441. setMode(val);
  442. }
  443. void onRead(BLECharacteristic* c) { c->setValue(currentMode.c_str()); }
  444. };
  445. class ConfigCharCallbacks : public BLECharacteristicCallbacks {
  446. void onWrite(BLECharacteristic* c) { saveConfigFromJson(c->getValue()); }
  447. void onRead(BLECharacteristic* c) { c->setValue(getConfigJson().c_str()); }
  448. };
  449. void setupBLE() {
  450. Serial.println("Starting BLE...");
  451. BLEDevice::init(BLE_DEVICE_NAME);
  452. pServer = BLEDevice::createServer();
  453. pServer->setCallbacks(new ServerCallbacks());
  454. BLEService* pService = pServer->createService(BLEUUID(SERVICE_UUID), 20);
  455. pModeCharacteristic = pService->createCharacteristic(MODE_CHAR_UUID,
  456. BLECharacteristic::PROPERTY_READ | BLECharacteristic::PROPERTY_WRITE | BLECharacteristic::PROPERTY_NOTIFY);
  457. pModeCharacteristic->setCallbacks(new ModeCharCallbacks());
  458. pModeCharacteristic->setValue(currentMode.c_str());
  459. pModeCharacteristic->addDescriptor(new BLE2902());
  460. pConfigCharacteristic = pService->createCharacteristic(CONFIG_CHAR_UUID,
  461. BLECharacteristic::PROPERTY_READ | BLECharacteristic::PROPERTY_WRITE);
  462. pConfigCharacteristic->setCallbacks(new ConfigCharCallbacks());
  463. pService->start();
  464. BLEAdvertising* pAdvertising = BLEDevice::getAdvertising();
  465. pAdvertising->addServiceUUID(SERVICE_UUID);
  466. pAdvertising->setScanResponse(true);
  467. pAdvertising->setMinPreferred(0x0c);
  468. pAdvertising->setMaxPreferred(0x18);
  469. BLEDevice::startAdvertising();
  470. Serial.println("BLE advertising.");
  471. }
  472. // =====================================================
  473. // MQTT
  474. // =====================================================
  475. void mqttCallback(char* topic, byte* payload, unsigned int length) {
  476. String message = "";
  477. for (unsigned int i = 0; i < length; i++) message += (char)payload[i];
  478. Serial.print("MQTT: "); Serial.print(topic); Serial.print(" -> "); Serial.println(message);
  479. JsonDocument doc;
  480. if (deserializeJson(doc, message)) { setMode(message); return; }
  481. const char* code = doc["code"];
  482. if (code) {
  483. String c = String(code);
  484. if (c == "idle") setMode("idle");
  485. else if (c == "busy" || c == "running") setMode("busy");
  486. else if (c == "retry" || c == "permission") setMode("alarm");
  487. else if (c == "pending") setMode("yellow");
  488. else if (c == "reasoning") setMode("thinking");
  489. else if (c == "using_tool") setMode("ai");
  490. else if (c == "error") setMode("error");
  491. else if (c == "ota") {
  492. performOTA();
  493. return;
  494. }
  495. else setMode(c);
  496. }
  497. }
  498. bool connectWiFi() {
  499. Serial.print("Connecting WiFi");
  500. WiFi.mode(WIFI_STA);
  501. WiFi.begin(cfgWifiSsid.c_str(), cfgWifiPass.c_str());
  502. for (int retry = 1; retry <= 5; retry++) {
  503. Serial.printf("\nWiFi %d/5", retry);
  504. int attempts = 0;
  505. while (WiFi.status() != WL_CONNECTED && attempts < 60) {
  506. delay(5); setOnly(100, 100, 100); updateStatusLed(); Serial.print("."); attempts++;
  507. }
  508. if (WiFi.status() == WL_CONNECTED) {
  509. Serial.printf("\nWiFi OK. IP: %s\n", WiFi.localIP().toString().c_str());
  510. digitalWrite(STATUS_PIN, LOW);
  511. return true;
  512. }
  513. Serial.println("\nFailed, retrying..."); delay(2000);
  514. }
  515. Serial.println("\nWiFi failed. Turning off WiFi.");
  516. WiFi.disconnect(true);
  517. WiFi.mode(WIFI_OFF);
  518. return false;
  519. }
  520. bool connectMQTT() {
  521. mqttClient.setServer(cfgMqttBroker.c_str(), cfgMqttPort);
  522. mqttClient.setCallback(mqttCallback);
  523. Serial.print("MQTT connecting");
  524. int attempts = 0;
  525. while (!mqttClient.connected()) {
  526. if (mqttClient.connect(cfgMqttClient.c_str(), cfgMqttUser.c_str(), cfgMqttPass.c_str())) {
  527. Serial.println("\nMQTT OK.");
  528. mqttClient.subscribe(cfgMqttTopic.c_str());
  529. allOff(); breathingGreen(3); publishStatus();
  530. return true;
  531. }
  532. Serial.print("."); delay(500);
  533. if (++attempts > 60) { Serial.println("\nMQTT failed!"); return false; }
  534. }
  535. return false;
  536. }
  537. void checkMQTTConnection() {
  538. if (useMQTT && !mqttClient.connected()) { Serial.println("MQTT reconnecting..."); connectMQTT(); }
  539. }
  540. // =====================================================
  541. // OTA 固件更新
  542. // =====================================================
  543. void notifyOtaStatus(const String& status) {
  544. if (pModeCharacteristic && bleDeviceConnected) {
  545. pModeCharacteristic->setValue(status.c_str());
  546. pModeCharacteristic->notify();
  547. }
  548. if (useMQTT && mqttClient.connected()) {
  549. mqttClient.publish(cfgMqttStatus.c_str(), status.c_str(), true);
  550. }
  551. }
  552. void performOTA() {
  553. Serial.println("=== OTA Start ===");
  554. if (otaInProgress) {
  555. Serial.println("OTA already in progress");
  556. return;
  557. }
  558. if (cfgOtaUrl.length() == 0) {
  559. Serial.println("OTA URL not configured");
  560. notifyOtaStatus("ota:no_url");
  561. return;
  562. }
  563. if (WiFi.status() != WL_CONNECTED) {
  564. Serial.println("WiFi not connected");
  565. notifyOtaStatus("ota:no_wifi");
  566. return;
  567. }
  568. otaInProgress = true;
  569. notifyOtaStatus("ota:downloading");
  570. HTTPClient http;
  571. http.begin(cfgOtaUrl);
  572. http.setTimeout(30000);
  573. int httpCode = http.GET();
  574. Serial.printf("HTTP GET: %d\n", httpCode);
  575. if (httpCode != 200) {
  576. Serial.printf("HTTP error: %d\n", httpCode);
  577. notifyOtaStatus("ota:error");
  578. http.end();
  579. otaInProgress = false;
  580. return;
  581. }
  582. int contentLength = http.getSize();
  583. Serial.printf("Firmware size: %d bytes\n", contentLength);
  584. if (contentLength <= 0) {
  585. Serial.println("Invalid content length");
  586. notifyOtaStatus("ota:error");
  587. http.end();
  588. otaInProgress = false;
  589. return;
  590. }
  591. if (!Update.begin(contentLength)) {
  592. Serial.println("Update.begin failed");
  593. notifyOtaStatus("ota:error");
  594. http.end();
  595. otaInProgress = false;
  596. return;
  597. }
  598. WiFiClient* stream = http.getStreamPtr();
  599. uint8_t buf[1024];
  600. int written = 0;
  601. unsigned long lastProgress = millis();
  602. while (http.connected() && written < contentLength) {
  603. size_t size = stream->available();
  604. if (size) {
  605. int bytesRead = stream->readBytes(buf, min(size, sizeof(buf)));
  606. size_t bytesWritten = Update.write(buf, bytesRead);
  607. written += bytesWritten;
  608. if (millis() - lastProgress >= 1000) {
  609. Serial.printf("Progress: %d/%d (%.1f%%)\n", written, contentLength, (float)written / contentLength * 100);
  610. lastProgress = millis();
  611. }
  612. }
  613. delay(1);
  614. }
  615. Serial.printf("Written: %d/%d\n", written, contentLength);
  616. if (Update.end(true)) {
  617. Serial.println("OTA success!");
  618. notifyOtaStatus("ota:success");
  619. delay(1000);
  620. ESP.restart();
  621. } else {
  622. Serial.printf("OTA error: %s\n", Update.errorString());
  623. notifyOtaStatus("ota:error");
  624. }
  625. http.end();
  626. otaInProgress = false;
  627. }
  628. // =====================================================
  629. // 初始化
  630. // =====================================================
  631. void setup() {
  632. Serial.begin(115200);
  633. delay(500);
  634. preferences.begin("ai-light", false);
  635. useMQTT = (preferences.getUInt("comm_mode", 1) != 0);
  636. loadConfig();
  637. ledcAttach(redPin, PWM_FREQ, PWM_RESOLUTION);
  638. ledcAttach(yellowPin, PWM_FREQ, PWM_RESOLUTION);
  639. ledcAttach(greenPin, PWM_FREQ, PWM_RESOLUTION);
  640. pinMode(STATUS_PIN, OUTPUT);
  641. pinMode(BUTTON_PIN, INPUT_PULLUP);
  642. digitalWrite(STATUS_PIN, HIGH);
  643. allOff();
  644. Serial.println();
  645. Serial.println("=== AI-Light ===");
  646. Serial.printf("Mode: %s\n", useMQTT ? "MQTT" : "BLE-only");
  647. Serial.printf("WiFi: %s\n", cfgWifiSsid.length() > 0 ? cfgWifiSsid.c_str() : "(not set)");
  648. Serial.printf("MQTT: %s\n", cfgMqttBroker.length() > 0 ? cfgMqttBroker.c_str() : "(not set)");
  649. Serial.printf("Pins: R=%d G=%d Y=%d\n", redPin, greenPin, yellowPin);
  650. currentMode = "init";
  651. modeStart = millis();
  652. if (useMQTT && isConfigComplete()) {
  653. wifiConnected = connectWiFi();
  654. if (wifiConnected) {
  655. connectMQTT();
  656. setMode("traffic");
  657. Serial.println("WiFi/MQTT mode. Long press BOOT (3s) to switch.");
  658. return;
  659. }
  660. Serial.println("WiFi failed. Entering BLE config mode.");
  661. }
  662. setupBLE();
  663. delay(100);
  664. setMode("traffic");
  665. Serial.println("BLE config mode. Long press BOOT (3s) to switch.");
  666. }
  667. // =====================================================
  668. // 主循环
  669. // =====================================================
  670. void loop() {
  671. updateStatusLed();
  672. checkBootButton();
  673. if (useMQTT && wifiConnected) {
  674. if (WiFi.status() != WL_CONNECTED) {
  675. Serial.println("WiFi lost, reconnecting...");
  676. wifiConnected = connectWiFi();
  677. if (!wifiConnected) {
  678. Serial.println("WiFi failed. BLE config mode active.");
  679. }
  680. }
  681. if (wifiConnected) {
  682. checkMQTTConnection();
  683. mqttClient.loop();
  684. }
  685. }
  686. autoTimeoutCheck();
  687. if (currentMode == "busy") updateBusy();
  688. else if (currentMode == "error") updateError();
  689. else if (currentMode == "thinking") updateThinking();
  690. else if (currentMode == "ai") updateAi();
  691. else if (currentMode == "success") updateSuccess();
  692. else if (currentMode == "traffic") updateTraffic();
  693. else if (currentMode == "alarm") updateAlarm();
  694. else if (currentMode == "init") updateInit();
  695. else if (currentMode == "off") allOff();
  696. delay(5);
  697. }