|
|
@@ -6,8 +6,6 @@
|
|
|
#include <BLEUtils.h>
|
|
|
#include <BLE2902.h>
|
|
|
#include <Preferences.h>
|
|
|
-#include <HTTPClient.h>
|
|
|
-#include <Update.h>
|
|
|
|
|
|
// =====================================================
|
|
|
// ESP32-C3 SuperMini + 原玩具公共正极灯板:BLE + MQTT 双模式
|
|
|
@@ -15,14 +13,15 @@
|
|
|
// 功能:
|
|
|
// - 开机始终启动 BLE,支持灯效控制 + WiFi/MQTT 配置
|
|
|
// - MQTT 模式下同时连接 WiFi/MQTT
|
|
|
-// - 运行时长按 BOOT 按钮 3 秒 → 切换 BLE/MQTT 模式并重启
|
|
|
+// - 运行长按 BOOT 按钮 3 秒 → 切换 BLE/MQTT 模式并重启
|
|
|
+// - 连续短按 BOOT 按钮 3 下 → 恢复出厂设置并重启
|
|
|
//
|
|
|
// BLE 配置:
|
|
|
// Service: b8b7e001-7a6b-4f4f-9a8b-11c0ffee0001
|
|
|
// Mode: b8b7e002-... (读写/通知 - 灯效模式)
|
|
|
// Config: b8b7e003-... (读写 - WiFi/MQTT 配置 JSON)
|
|
|
//
|
|
|
-// 配置 JSON 格式(写入 Config 特征):
|
|
|
+// 配置 JSON 格式(写入 Config 特征或 MQTT config topic):
|
|
|
// {
|
|
|
// "wifi_ssid": "xxx",
|
|
|
// "wifi_pass": "xxx",
|
|
|
@@ -31,12 +30,12 @@
|
|
|
// "mqtt_user": "user",
|
|
|
// "mqtt_pass": "pass",
|
|
|
// "mqtt_client": "AI-Light",
|
|
|
-// "mqtt_topic": "opencode/status",
|
|
|
-// "mqtt_status": "openCodeLight/status",
|
|
|
+// "mqtt_topic": "agent/status",
|
|
|
+// "mqtt_status": "agentLight/status",
|
|
|
+// "mqtt_topic_config": "agent/status/config",
|
|
|
// "pin_red": 4,
|
|
|
// "pin_green": 3,
|
|
|
-// "pin_yellow": 2,
|
|
|
-// "ota_url": "https://example.com/firmware.bin"
|
|
|
+// "pin_yellow": 2
|
|
|
// }
|
|
|
//
|
|
|
// 接线方式(默认引脚,可通过配置修改):
|
|
|
@@ -76,6 +75,8 @@ const int GREEN_MAX = 220;
|
|
|
const unsigned long NORMAL_MODE_TIMEOUT_MS = 5UL * 60UL * 1000UL;
|
|
|
const unsigned long TRAFFIC_MODE_TIMEOUT_MS = 10UL * 60UL * 1000UL;
|
|
|
const unsigned long LONG_PRESS_MS = 3000;
|
|
|
+const unsigned long TRIPLE_PRESS_WINDOW_MS = 1500;
|
|
|
+const int TRIPLE_PRESS_COUNT = 3;
|
|
|
|
|
|
// =====================================================
|
|
|
// 全局状态
|
|
|
@@ -102,10 +103,9 @@ uint16_t cfgMqttPort = 1883;
|
|
|
String cfgMqttUser = "";
|
|
|
String cfgMqttPass = "";
|
|
|
String cfgMqttClient = "AI-Light";
|
|
|
-String cfgMqttTopic = "opencode/status";
|
|
|
-String cfgMqttStatus = "openCodeLight/status";
|
|
|
-String cfgOtaUrl = "";
|
|
|
-bool otaInProgress = false;
|
|
|
+String cfgMqttTopic = "agent/status";
|
|
|
+String cfgMqttStatus = "agentLight/status";
|
|
|
+String cfgMqttTopicConfig = "agent/status/config";
|
|
|
|
|
|
|
|
|
// =====================================================
|
|
|
@@ -120,12 +120,12 @@ void loadConfig() {
|
|
|
cfgMqttUser = preferences.getString("mqtt_user", "");
|
|
|
cfgMqttPass = preferences.getString("mqtt_pass", "");
|
|
|
cfgMqttClient = preferences.getString("mqtt_client", "AI-Light");
|
|
|
- cfgMqttTopic = preferences.getString("mqtt_topic", "opencode/status");
|
|
|
- cfgMqttStatus = preferences.getString("mqtt_status", "openCodeLight/status");
|
|
|
+ cfgMqttTopic = preferences.getString("mqtt_topic", "agent/status");
|
|
|
+ cfgMqttStatus = preferences.getString("mqtt_status", "agentLight/status");
|
|
|
+ cfgMqttTopicConfig = preferences.getString("mqtt_topic_cfg", "agent/status/config");
|
|
|
redPin = preferences.getUInt("pin_red", 4);
|
|
|
greenPin = preferences.getUInt("pin_green", 3);
|
|
|
yellowPin = preferences.getUInt("pin_yellow", 2);
|
|
|
- cfgOtaUrl = preferences.getString("ota_url", "");
|
|
|
}
|
|
|
|
|
|
bool isConfigComplete() {
|
|
|
@@ -142,11 +142,11 @@ String getConfigJson() {
|
|
|
doc["mqtt_client"] = cfgMqttClient;
|
|
|
doc["mqtt_topic"] = cfgMqttTopic;
|
|
|
doc["mqtt_status"] = cfgMqttStatus;
|
|
|
+ doc["mqtt_topic_config"] = cfgMqttTopicConfig;
|
|
|
doc["comm_mode"] = useMQTT ? 1 : 0;
|
|
|
doc["pin_red"] = redPin;
|
|
|
doc["pin_green"] = greenPin;
|
|
|
doc["pin_yellow"] = yellowPin;
|
|
|
- doc["ota_url"] = cfgOtaUrl;
|
|
|
String out;
|
|
|
serializeJson(doc, out);
|
|
|
return out;
|
|
|
@@ -179,14 +179,14 @@ void saveConfigFromJson(const String& json) {
|
|
|
preferences.putString("mqtt_topic", doc["mqtt_topic"].as<String>());
|
|
|
if (doc.containsKey("mqtt_status"))
|
|
|
preferences.putString("mqtt_status", doc["mqtt_status"].as<String>());
|
|
|
+ if (doc.containsKey("mqtt_topic_config"))
|
|
|
+ preferences.putString("mqtt_topic_cfg", doc["mqtt_topic_config"].as<String>());
|
|
|
if (doc.containsKey("pin_red"))
|
|
|
preferences.putUInt("pin_red", doc["pin_red"].as<uint8_t>());
|
|
|
if (doc.containsKey("pin_green"))
|
|
|
preferences.putUInt("pin_green", doc["pin_green"].as<uint8_t>());
|
|
|
if (doc.containsKey("pin_yellow"))
|
|
|
preferences.putUInt("pin_yellow", doc["pin_yellow"].as<uint8_t>());
|
|
|
- if (doc.containsKey("ota_url"))
|
|
|
- preferences.putString("ota_url", doc["ota_url"].as<String>());
|
|
|
|
|
|
Serial.println("Config saved. Restarting...");
|
|
|
delay(500);
|
|
|
@@ -439,13 +439,31 @@ void breathingGreen(int times) {
|
|
|
unsigned long bootPressStart = 0;
|
|
|
bool bootWasPressed = false;
|
|
|
bool switchTriggered = false;
|
|
|
+unsigned long lastPressEnd = 0;
|
|
|
+int pressCount = 0;
|
|
|
+
|
|
|
+void factoryReset() {
|
|
|
+ Serial.println("Factory reset! Clearing all config...");
|
|
|
+ allOff();
|
|
|
+ for (int i = 0; i < 5; i++) {
|
|
|
+ setOnly(255, 0, 0); delay(200);
|
|
|
+ allOff(); delay(200);
|
|
|
+ }
|
|
|
+ preferences.clear();
|
|
|
+ Serial.println("NVS cleared. Restarting...");
|
|
|
+ delay(500);
|
|
|
+ ESP.restart();
|
|
|
+}
|
|
|
|
|
|
void checkBootButton() {
|
|
|
bool pressed = (digitalRead(BUTTON_PIN) == LOW);
|
|
|
+
|
|
|
if (pressed && !bootWasPressed) {
|
|
|
bootPressStart = millis();
|
|
|
bootWasPressed = true;
|
|
|
+ switchTriggered = false;
|
|
|
}
|
|
|
+
|
|
|
if (pressed && bootWasPressed && !switchTriggered) {
|
|
|
if (millis() - bootPressStart >= LONG_PRESS_MS) {
|
|
|
switchTriggered = true;
|
|
|
@@ -461,9 +479,21 @@ void checkBootButton() {
|
|
|
ESP.restart();
|
|
|
}
|
|
|
}
|
|
|
- if (!pressed) {
|
|
|
+
|
|
|
+ if (!pressed && bootWasPressed) {
|
|
|
bootWasPressed = false;
|
|
|
- switchTriggered = false;
|
|
|
+ unsigned long pressDuration = millis() - bootPressStart;
|
|
|
+ if (pressDuration < LONG_PRESS_MS) {
|
|
|
+ if (millis() - lastPressEnd > TRIPLE_PRESS_WINDOW_MS) {
|
|
|
+ pressCount = 0;
|
|
|
+ }
|
|
|
+ pressCount++;
|
|
|
+ lastPressEnd = millis();
|
|
|
+ if (pressCount >= TRIPLE_PRESS_COUNT) {
|
|
|
+ pressCount = 0;
|
|
|
+ factoryReset();
|
|
|
+ }
|
|
|
+ }
|
|
|
}
|
|
|
}
|
|
|
|
|
|
@@ -494,17 +524,22 @@ void updateStatusLed() {
|
|
|
// =====================================================
|
|
|
|
|
|
class ServerCallbacks : public BLEServerCallbacks {
|
|
|
- void onConnect(BLEServer* s) { bleDeviceConnected = true; Serial.println("BLE connected."); }
|
|
|
- void onDisconnect(BLEServer* s) { bleDeviceConnected = false; Serial.println("BLE disconnected."); delay(100); BLEDevice::startAdvertising(); }
|
|
|
+ void onConnect(BLEServer* s) {
|
|
|
+ bleDeviceConnected = true;
|
|
|
+ Serial.println("BLE connected.");
|
|
|
+ if (currentMode == "init") setMode("traffic");
|
|
|
+ }
|
|
|
+ void onDisconnect(BLEServer* s) {
|
|
|
+ bleDeviceConnected = false;
|
|
|
+ Serial.println("BLE disconnected.");
|
|
|
+ delay(100);
|
|
|
+ BLEDevice::startAdvertising();
|
|
|
+ }
|
|
|
};
|
|
|
|
|
|
class ModeCharCallbacks : public BLECharacteristicCallbacks {
|
|
|
void onWrite(BLECharacteristic* c) {
|
|
|
String val = c->getValue();
|
|
|
- if (val == "ota") {
|
|
|
- performOTA();
|
|
|
- return;
|
|
|
- }
|
|
|
setMode(val);
|
|
|
}
|
|
|
void onRead(BLECharacteristic* c) { c->setValue(currentMode.c_str()); }
|
|
|
@@ -552,6 +587,11 @@ void mqttCallback(char* topic, byte* payload, unsigned int length) {
|
|
|
for (unsigned int i = 0; i < length; i++) message += (char)payload[i];
|
|
|
Serial.print("MQTT: "); Serial.print(topic); Serial.print(" -> "); Serial.println(message);
|
|
|
|
|
|
+ if (String(topic) == cfgMqttTopicConfig) {
|
|
|
+ saveConfigFromJson(message);
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
JsonDocument doc;
|
|
|
if (deserializeJson(doc, message)) { setMode(message); return; }
|
|
|
const char* code = doc["code"];
|
|
|
@@ -564,10 +604,6 @@ void mqttCallback(char* topic, byte* payload, unsigned int length) {
|
|
|
else if (c == "reasoning") setMode("thinking");
|
|
|
else if (c == "using_tool") setMode("ai");
|
|
|
else if (c == "error") setMode("error");
|
|
|
- else if (c == "ota") {
|
|
|
- performOTA();
|
|
|
- return;
|
|
|
- }
|
|
|
else setMode(c);
|
|
|
}
|
|
|
}
|
|
|
@@ -581,7 +617,11 @@ bool connectWiFi() {
|
|
|
Serial.printf("\nWiFi %d/5", retry);
|
|
|
int attempts = 0;
|
|
|
while (WiFi.status() != WL_CONNECTED && attempts < 60) {
|
|
|
- delay(5); setOnly(100, 100, 100); updateStatusLed(); Serial.print("."); attempts++;
|
|
|
+ delay(5);
|
|
|
+ updateInit();
|
|
|
+ updateStatusLed();
|
|
|
+ Serial.print(".");
|
|
|
+ attempts++;
|
|
|
}
|
|
|
if (WiFi.status() == WL_CONNECTED) {
|
|
|
Serial.printf("\nWiFi OK. IP: %s\n", WiFi.localIP().toString().c_str());
|
|
|
@@ -606,6 +646,7 @@ bool connectMQTT() {
|
|
|
if (mqttClient.connect(cfgMqttClient.c_str(), cfgMqttUser.c_str(), cfgMqttPass.c_str())) {
|
|
|
Serial.println("\nMQTT OK.");
|
|
|
mqttClient.subscribe(cfgMqttTopic.c_str());
|
|
|
+ mqttClient.subscribe(cfgMqttTopicConfig.c_str());
|
|
|
allOff(); breathingGreen(3); publishStatus();
|
|
|
return true;
|
|
|
}
|
|
|
@@ -620,114 +661,6 @@ void checkMQTTConnection() {
|
|
|
}
|
|
|
|
|
|
|
|
|
-// =====================================================
|
|
|
-// OTA 固件更新
|
|
|
-// =====================================================
|
|
|
-
|
|
|
-void notifyOtaStatus(const String& status) {
|
|
|
- if (pModeCharacteristic && bleDeviceConnected) {
|
|
|
- pModeCharacteristic->setValue(status.c_str());
|
|
|
- pModeCharacteristic->notify();
|
|
|
- }
|
|
|
- if (useMQTT && mqttClient.connected()) {
|
|
|
- mqttClient.publish(cfgMqttStatus.c_str(), status.c_str(), true);
|
|
|
- }
|
|
|
-}
|
|
|
-
|
|
|
-void performOTA() {
|
|
|
- Serial.println("=== OTA Start ===");
|
|
|
-
|
|
|
- if (otaInProgress) {
|
|
|
- Serial.println("OTA already in progress");
|
|
|
- return;
|
|
|
- }
|
|
|
-
|
|
|
- if (cfgOtaUrl.length() == 0) {
|
|
|
- Serial.println("OTA URL not configured");
|
|
|
- notifyOtaStatus("ota:no_url");
|
|
|
- return;
|
|
|
- }
|
|
|
-
|
|
|
- if (WiFi.status() != WL_CONNECTED) {
|
|
|
- Serial.println("WiFi not connected");
|
|
|
- notifyOtaStatus("ota:no_wifi");
|
|
|
- return;
|
|
|
- }
|
|
|
-
|
|
|
- otaInProgress = true;
|
|
|
- notifyOtaStatus("ota:downloading");
|
|
|
-
|
|
|
- HTTPClient http;
|
|
|
- http.begin(cfgOtaUrl);
|
|
|
- http.setTimeout(30000);
|
|
|
-
|
|
|
- int httpCode = http.GET();
|
|
|
- Serial.printf("HTTP GET: %d\n", httpCode);
|
|
|
-
|
|
|
- if (httpCode != 200) {
|
|
|
- Serial.printf("HTTP error: %d\n", httpCode);
|
|
|
- notifyOtaStatus("ota:error");
|
|
|
- http.end();
|
|
|
- otaInProgress = false;
|
|
|
- return;
|
|
|
- }
|
|
|
-
|
|
|
- int contentLength = http.getSize();
|
|
|
- Serial.printf("Firmware size: %d bytes\n", contentLength);
|
|
|
-
|
|
|
- if (contentLength <= 0) {
|
|
|
- Serial.println("Invalid content length");
|
|
|
- notifyOtaStatus("ota:error");
|
|
|
- http.end();
|
|
|
- otaInProgress = false;
|
|
|
- return;
|
|
|
- }
|
|
|
-
|
|
|
- if (!Update.begin(contentLength)) {
|
|
|
- Serial.println("Update.begin failed");
|
|
|
- notifyOtaStatus("ota:error");
|
|
|
- http.end();
|
|
|
- otaInProgress = false;
|
|
|
- return;
|
|
|
- }
|
|
|
-
|
|
|
- WiFiClient* stream = http.getStreamPtr();
|
|
|
- uint8_t buf[1024];
|
|
|
- int written = 0;
|
|
|
- unsigned long lastProgress = millis();
|
|
|
-
|
|
|
- while (http.connected() && written < contentLength) {
|
|
|
- size_t size = stream->available();
|
|
|
- if (size) {
|
|
|
- int bytesRead = stream->readBytes(buf, min(size, sizeof(buf)));
|
|
|
- size_t bytesWritten = Update.write(buf, bytesRead);
|
|
|
- written += bytesWritten;
|
|
|
-
|
|
|
- if (millis() - lastProgress >= 1000) {
|
|
|
- Serial.printf("Progress: %d/%d (%.1f%%)\n", written, contentLength, (float)written / contentLength * 100);
|
|
|
- lastProgress = millis();
|
|
|
- }
|
|
|
- }
|
|
|
- delay(1);
|
|
|
- }
|
|
|
-
|
|
|
- Serial.printf("Written: %d/%d\n", written, contentLength);
|
|
|
-
|
|
|
- if (Update.end(true)) {
|
|
|
- Serial.println("OTA success!");
|
|
|
- notifyOtaStatus("ota:success");
|
|
|
- delay(1000);
|
|
|
- ESP.restart();
|
|
|
- } else {
|
|
|
- Serial.printf("OTA error: %s\n", Update.errorString());
|
|
|
- notifyOtaStatus("ota:error");
|
|
|
- }
|
|
|
-
|
|
|
- http.end();
|
|
|
- otaInProgress = false;
|
|
|
-}
|
|
|
-
|
|
|
-
|
|
|
// =====================================================
|
|
|
// 初始化
|
|
|
// =====================================================
|
|
|
@@ -746,7 +679,9 @@ void setup() {
|
|
|
pinMode(STATUS_PIN, OUTPUT);
|
|
|
pinMode(BUTTON_PIN, INPUT_PULLUP);
|
|
|
digitalWrite(STATUS_PIN, HIGH);
|
|
|
- allOff();
|
|
|
+
|
|
|
+ currentMode = "init";
|
|
|
+ modeStart = millis();
|
|
|
|
|
|
Serial.println();
|
|
|
Serial.println("=== AI-Light ===");
|
|
|
@@ -755,9 +690,6 @@ void setup() {
|
|
|
Serial.printf("MQTT: %s\n", cfgMqttBroker.length() > 0 ? cfgMqttBroker.c_str() : "(not set)");
|
|
|
Serial.printf("Pins: R=%d G=%d Y=%d\n", redPin, greenPin, yellowPin);
|
|
|
|
|
|
- currentMode = "init";
|
|
|
- modeStart = millis();
|
|
|
-
|
|
|
if (useMQTT && isConfigComplete()) {
|
|
|
wifiConnected = connectWiFi();
|
|
|
if (wifiConnected) {
|
|
|
@@ -770,10 +702,7 @@ void setup() {
|
|
|
}
|
|
|
|
|
|
setupBLE();
|
|
|
- delay(100);
|
|
|
-
|
|
|
- setMode("traffic");
|
|
|
- Serial.println("BLE config mode. Long press BOOT (3s) to switch.");
|
|
|
+ Serial.println("BLE advertising. Waiting for connection...");
|
|
|
}
|
|
|
|
|
|
|