ai_light.ino 24 KB

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