Compare commits

..

4 Commits

Author SHA1 Message Date
yangzhijia
08a055b611 fix:修补蓝牙配网bug 2026-08-12 09:22:28 +08:00
yangzhijia
93112dcdfd feat:增加蓝牙配网功能 2026-08-12 09:20:54 +08:00
yangzhijia
2ce4ac0b9c 版本1.2 2026-08-06 13:58:45 +08:00
yangzhijia
69e927c5f0 feat: 按钮去抖+配网界面美化+水浸离线+LED独立任务+报警全拓展(人身优先)+有人才检测 2026-07-31 15:58:09 +08:00
41 changed files with 15014 additions and 907 deletions

View File

@@ -7,7 +7,24 @@
"WebSearch", "WebSearch",
"WebFetch(domain:boardor.com)", "WebFetch(domain:boardor.com)",
"Read(//d//**)", "Read(//d//**)",
"PowerShell(Get-Content \"D:\\\\R60AFD1_数据手册_V1.2.pdf\" -Raw -Encoding Byte | Out-Null; Write-Host \"File exists, size:\" \\(\\(Get-Item \"D:\\\\R60AFD1_数据手册_V1.2.pdf\"\\).Length\\);)" "PowerShell(Get-Content \"D:\\\\R60AFD1_数据手册_V1.2.pdf\" -Raw -Encoding Byte | Out-Null; Write-Host \"File exists, size:\" \\(\\(Get-Item \"D:\\\\R60AFD1_数据手册_V1.2.pdf\"\\).Length\\);)",
"Bash(py -m pip install openpyxl -q)",
"Read(//d/PlatformIO1/Projects/**)",
"Bash(pio pkg *)",
"Bash(git stash *)",
"Bash(git checkout *)",
"Bash(sed -i 's/#define OLED_SDA_PIN 14/#define OLED_SDA_PIN 15/' include/config.h)",
"Bash(sed -i 's/#define OLED_SCL_PIN 15/#define OLED_SCL_PIN 16/' include/config.h)",
"Bash(sed -i 's/#define BTN_PROVISION_PIN 16/#define BTN_PROVISION_PIN 14/' include/config.h)",
"Bash(sed -i 's/#define BTN_PAGE_SWITCH_PIN 10/#define BTN_PAGE_SWITCH_PIN 5/' include/config.h)",
"Bash(sed -i 's/#define BTN_LONG_PRESS_MS 3000/#define BTN_LONG_PRESS_MS 1500/' include/config.h)",
"Bash(sed -i 's/#define WATER_SENSOR_PIN 6 /#define WATER_SENSOR_PIN 45/' include/config.h)",
"Bash(sed -i 's/#define BUZZER_PIN 7 /#define BUZZER_PIN 21/' include/config.h)",
"PowerShell(Get-ChildItem \"$env:USERPROFILE\\\\.claude\" -ErrorAction SilentlyContinue; if \\(-not $?\\) { Write-Host \"No .claude at USERPROFILE\" })",
"Read(//c/Users/杨志佳/.claude/**)",
"Read(//d/ClaudeData/.claude/**)",
"Read(//c/Users/杨志佳/.claude/projects/d--PlatformIO1-Projects-Healthv2-0/memory/**)",
"Read(//d/PlatformIO1/Projects/Healthv2.0/ai/sessions/memory/**)"
] ]
} }
} }

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1 @@
{"agentType":"Explore","description":"Research U8g2 Chinese font options","toolUseId":"call_01_W4SVsAkWKYYPD3AllArZ6173","spawnDepth":1}

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1 @@
{"agentType":"Explore","description":"Explore existing patterns and GPIO","toolUseId":"call_00_wiJSGewBjcUTBxzRBDKV0217","spawnDepth":1}

View File

@@ -0,0 +1,252 @@
.pio\libdeps\esp32-s3-devkitc-1\WiFiManager\WiFiManager.cpp:31:WiFiManagerParameter::WiFiManagerParameter(const char *custom) {
.pio\libdeps\esp32-s3-devkitc-1\WiFiManager\WiFiManager.cpp:37: _customHTML = custom;
.pio\libdeps\esp32-s3-devkitc-1\WiFiManager\WiFiManager.cpp:48:WiFiManagerParameter::WiFiManagerParameter(const char *id, const char *label, const char *defaultValue, int length, const char *custom) {
.pio\libdeps\esp32-s3-devkitc-1\WiFiManager\WiFiManager.cpp:49: init(id, label, defaultValue, length, custom, WFM_LABEL_DEFAULT);
.pio\libdeps\esp32-s3-devkitc-1\WiFiManager\WiFiManager.cpp:52:WiFiManagerParameter::WiFiManagerParameter(const char *id, const char *label, const char *defaultValue, int length, const char *custom, int labelPlacement) {
.pio\libdeps\esp32-s3-devkitc-1\WiFiManager\WiFiManager.cpp:53: init(id, label, defaultValue, length, custom, labelPlacement);
.pio\libdeps\esp32-s3-devkitc-1\WiFiManager\WiFiManager.cpp:56:void WiFiManagerParameter::init(const char *id, const char *label, const char *defaultValue, int length, const char *custom, int labelPlacement) {
.pio\libdeps\esp32-s3-devkitc-1\WiFiManager\WiFiManager.cpp:60: _customHTML = custom;
.pio\libdeps\esp32-s3-devkitc-1\WiFiManager\WiFiManager.cpp:124:const char* WiFiManagerParameter::getCustomHTML() const {
.pio\libdeps\esp32-s3-devkitc-1\WiFiManager\WiFiManager.cpp:125: return _customHTML;
.pio\libdeps\esp32-s3-devkitc-1\WiFiManager\WiFiManager.cpp:159: DEBUG_WM(WM_DEBUG_DEV,F("Updated _max_params:"),_max_params);
.pio\libdeps\esp32-s3-devkitc-1\WiFiManager\WiFiManager.cpp:220: if(_debug && _debugLevel >= WM_DEBUG_DEV) debugPlatformInfo();
.pio\libdeps\esp32-s3-devkitc-1\WiFiManager\WiFiManager.cpp:254: WiFi.persistent(false); // disable persistent so scannetworks and mode switching do not cause overwrites
.pio\libdeps\esp32-s3-devkitc-1\WiFiManager\WiFiManager.cpp:283: // bool wifiIsSaved = getWiFiIsSaved();
.pio\libdeps\esp32-s3-devkitc-1\WiFiManager\WiFiManager.cpp:284: bool wifiIsSaved = true; // workaround until I can check esp32 wifiisinit and has nvs
.pio\libdeps\esp32-s3-devkitc-1\WiFiManager\WiFiManager.cpp:287: setupHostname(true);
.pio\libdeps\esp32-s3-devkitc-1\WiFiManager\WiFiManager.cpp:302: // check if wifi is saved, (has autoconnect) to speed up cp start
.pio\libdeps\esp32-s3-devkitc-1\WiFiManager\WiFiManager.cpp:304: if(wifiIsSaved){
.pio\libdeps\esp32-s3-devkitc-1\WiFiManager\WiFiManager.cpp:308: // attempt to connect using saved settings, on fail fallback to AP config portal
.pio\libdeps\esp32-s3-devkitc-1\WiFiManager\WiFiManager.cpp:320: if(esp32persistent) WiFi.persistent(false); // disable persistent for esp32 after esp_wifi_start or else saves wont work
.pio\libdeps\esp32-s3-devkitc-1\WiFiManager\WiFiManager.cpp:332: setupHostname(true);
.pio\libdeps\esp32-s3-devkitc-1\WiFiManager\WiFiManager.cpp:338: // @todo check if correct ssid == saved ssid when already connected
.pio\libdeps\esp32-s3-devkitc-1\WiFiManager\WiFiManager.cpp:374: DEBUG_WM(F("No Credentials are Saved, skipping connect"));
.pio\libdeps\esp32-s3-devkitc-1\WiFiManager\WiFiManager.cpp:392:bool WiFiManager::setupHostname(bool restart){
.pio\libdeps\esp32-s3-devkitc-1\WiFiManager\WiFiManager.cpp:455: // WiFi.reconnect(); // This does not reset dhcp
.pio\libdeps\esp32-s3-devkitc-1\WiFiManager\WiFiManager.cpp:457: delay(200); // do not remove, need a delay for disconnect to change status()
.pio\libdeps\esp32-s3-devkitc-1\WiFiManager\WiFiManager.cpp:481: // setup optional soft AP static ip config
.pio\libdeps\esp32-s3-devkitc-1\WiFiManager\WiFiManager.cpp:484: DEBUG_WM(F("Custom AP IP/GW/Subnet:"));
.pio\libdeps\esp32-s3-devkitc-1\WiFiManager\WiFiManager.cpp:560: setupConfigPortal();
.pio\libdeps\esp32-s3-devkitc-1\WiFiManager\WiFiManager.cpp:618:void WiFiManager::setupHTTPServer(){
.pio\libdeps\esp32-s3-devkitc-1\WiFiManager\WiFiManager.cpp:626: DEBUG_WM(WM_DEBUG_VERBOSE,F("http server started with custom port: "),_httpPort); // @todo not showing ip
.pio\libdeps\esp32-s3-devkitc-1\WiFiManager\WiFiManager.cpp:630: server.reset(new WM_WebServer(_httpPort));
.pio\libdeps\esp32-s3-devkitc-1\WiFiManager\WiFiManager.cpp:631: // This is not the safest way to reset the webserver, it can cause crashes on callbacks initilized before this and since its a shared pointer...
.pio\libdeps\esp32-s3-devkitc-1\WiFiManager\WiFiManager.cpp:641: /* Setup httpd callbacks, web pages: root, wifi config pages, SO captive portal detectors and not found. */
.pio\libdeps\esp32-s3-devkitc-1\WiFiManager\WiFiManager.cpp:646: server->on(WM_G(R_wifinoscan), std::bind(&WiFiManager::handleWifi, this, false));
.pio\libdeps\esp32-s3-devkitc-1\WiFiManager\WiFiManager.cpp:647: server->on(WM_G(R_wifisave), std::bind(&WiFiManager::handleWifiSave, this));
.pio\libdeps\esp32-s3-devkitc-1\WiFiManager\WiFiManager.cpp:648: server->on(WM_G(R_info), std::bind(&WiFiManager::handleInfo, this));
.pio\libdeps\esp32-s3-devkitc-1\WiFiManager\WiFiManager.cpp:650: server->on(WM_G(R_paramsave), std::bind(&WiFiManager::handleParamSave, this));
.pio\libdeps\esp32-s3-devkitc-1\WiFiManager\WiFiManager.cpp:651: server->on(WM_G(R_restart), std::bind(&WiFiManager::handleReset, this));
.pio\libdeps\esp32-s3-devkitc-1\WiFiManager\WiFiManager.cpp:652: server->on(WM_G(R_exit), std::bind(&WiFiManager::handleExit, this));
.pio\libdeps\esp32-s3-devkitc-1\WiFiManager\WiFiManager.cpp:653: server->on(WM_G(R_close), std::bind(&WiFiManager::handleClose, this));
.pio\libdeps\esp32-s3-devkitc-1\WiFiManager\WiFiManager.cpp:658: server->on(WM_G(R_update), std::bind(&WiFiManager::handleUpdate, this));
.pio\libdeps\esp32-s3-devkitc-1\WiFiManager\WiFiManager.cpp:659: server->on(WM_G(R_updatedone), HTTP_POST, std::bind(&WiFiManager::handleUpdateDone, this), std::bind(&WiFiManager::handleUpdating, this));
.pio\libdeps\esp32-s3-devkitc-1\WiFiManager\WiFiManager.cpp:667:void WiFiManager::setupDNSD(){
.pio\libdeps\esp32-s3-devkitc-1\WiFiManager\WiFiManager.cpp:668: dnsServer.reset(new DNSServer());
.pio\libdeps\esp32-s3-devkitc-1\WiFiManager\WiFiManager.cpp:670: /* Setup the DNS server redirecting all the domains to the apIP */
.pio\libdeps\esp32-s3-devkitc-1\WiFiManager\WiFiManager.cpp:679:void WiFiManager::setupConfigPortal() {
.pio\libdeps\esp32-s3-devkitc-1\WiFiManager\WiFiManager.cpp:680: setupHTTPServer();
.pio\libdeps\esp32-s3-devkitc-1\WiFiManager\WiFiManager.cpp:681: _lastscan = 0; // reset network scan cache
.pio\libdeps\esp32-s3-devkitc-1\WiFiManager\WiFiManager.cpp:682: if(_preloadwifiscan) WiFi_scanNetworks(true,true); // preload wifiscan , async
.pio\libdeps\esp32-s3-devkitc-1\WiFiManager\WiFiManager.cpp:707: //setup AP
.pio\libdeps\esp32-s3-devkitc-1\WiFiManager\WiFiManager.cpp:719: // HANDLE issues with STA connections, shutdown sta if not connected, or else this will hang channel scanning and softap will not respond
.pio\libdeps\esp32-s3-devkitc-1\WiFiManager\WiFiManager.cpp:759: DEBUG_WM(WM_DEBUG_DEV,F("setupConfigPortal"));
.pio\libdeps\esp32-s3-devkitc-1\WiFiManager\WiFiManager.cpp:761: setupConfigPortal();
.pio\libdeps\esp32-s3-devkitc-1\WiFiManager\WiFiManager.cpp:764: DEBUG_WM(WM_DEBUG_DEV,F("setupDNSD"));
.pio\libdeps\esp32-s3-devkitc-1\WiFiManager\WiFiManager.cpp:766: setupDNSD();
.pio\libdeps\esp32-s3-devkitc-1\WiFiManager\WiFiManager.cpp:819: DEBUG_WM(WM_DEBUG_NOTIFY,F("config portal exiting"));
.pio\libdeps\esp32-s3-devkitc-1\WiFiManager\WiFiManager.cpp:832: MDNS.update();
.pio\libdeps\esp32-s3-devkitc-1\WiFiManager\WiFiManager.cpp:837: if(_allowExit && (configPortalHasTimeout() || abort)){
.pio\libdeps\esp32-s3-devkitc-1\WiFiManager\WiFiManager.cpp:861: * returns WL_IDLE_STATUS or WL_CONNECTED/WL_CONNECT_FAILED upon connect/save flag
.pio\libdeps\esp32-s3-devkitc-1\WiFiManager\WiFiManager.cpp:874: // Waiting for save...
.pio\libdeps\esp32-s3-devkitc-1\WiFiManager\WiFiManager.cpp:878: DEBUG_WM(WM_DEBUG_VERBOSE,F("processing save"));
.pio\libdeps\esp32-s3-devkitc-1\WiFiManager\WiFiManager.cpp:880: if(_enableCaptivePortal) delay(_cpclosedelay); // keeps the captiveportal from closing to fast.
.pio\libdeps\esp32-s3-devkitc-1\WiFiManager\WiFiManager.cpp:885: DEBUG_WM(WM_DEBUG_VERBOSE,F("No ssid, skipping wifi save"));
.pio\libdeps\esp32-s3-devkitc-1\WiFiManager\WiFiManager.cpp:890: uint8_t res = connectWifi(_ssid, _pass, _connectonsave) == WL_CONNECTED;
.pio\libdeps\esp32-s3-devkitc-1\WiFiManager\WiFiManager.cpp:891: if (res || (!_connectonsave)) {
.pio\libdeps\esp32-s3-devkitc-1\WiFiManager\WiFiManager.cpp:893: if(!_connectonsave){
.pio\libdeps\esp32-s3-devkitc-1\WiFiManager\WiFiManager.cpp:894: DEBUG_WM(F("SAVED with no connect to new AP"));
.pio\libdeps\esp32-s3-devkitc-1\WiFiManager\WiFiManager.cpp:896: DEBUG_WM(F("Connect to new AP [SUCCESS]"));
.pio\libdeps\esp32-s3-devkitc-1\WiFiManager\WiFiManager.cpp:902: if ( _savewificallback != NULL) {
.pio\libdeps\esp32-s3-devkitc-1\WiFiManager\WiFiManager.cpp:904: DEBUG_WM(WM_DEBUG_VERBOSE,F("[CB] _savewificallback calling"));
.pio\libdeps\esp32-s3-devkitc-1\WiFiManager\WiFiManager.cpp:906: _savewificallback(); // @CALLBACK
.pio\libdeps\esp32-s3-devkitc-1\WiFiManager\WiFiManager.cpp:908: if(!_connectonsave) return WL_IDLE_STATUS;
.pio\libdeps\esp32-s3-devkitc-1\WiFiManager\WiFiManager.cpp:913: DEBUG_WM(WM_DEBUG_ERROR,F("[ERROR] Connect to new AP Failed"));
.pio\libdeps\esp32-s3-devkitc-1\WiFiManager\WiFiManager.cpp:919: // do save callback
.pio\libdeps\esp32-s3-devkitc-1\WiFiManager\WiFiManager.cpp:920: // @todo this is more of an exiting callback than a save, clarify when this should actually occur
.pio\libdeps\esp32-s3-devkitc-1\WiFiManager\WiFiManager.cpp:921: // confirm or verify data was saved to make this more accurate callback
.pio\libdeps\esp32-s3-devkitc-1\WiFiManager\WiFiManager.cpp:922: if ( _savewificallback != NULL) {
.pio\libdeps\esp32-s3-devkitc-1\WiFiManager\WiFiManager.cpp:924: DEBUG_WM(WM_DEBUG_VERBOSE,F("[CB] WiFi/Param save callback"));
.pio\libdeps\esp32-s3-devkitc-1\WiFiManager\WiFiManager.cpp:926: _savewificallback(); // @CALLBACK
.pio\libdeps\esp32-s3-devkitc-1\WiFiManager\WiFiManager.cpp:932: // clear save strings
.pio\libdeps\esp32-s3-devkitc-1\WiFiManager\WiFiManager.cpp:975: server.reset();
.pio\libdeps\esp32-s3-devkitc-1\WiFiManager\WiFiManager.cpp:977: WiFi.scanDelete(); // free wifi scan results
.pio\libdeps\esp32-s3-devkitc-1\WiFiManager\WiFiManager.cpp:982: dnsServer.reset();
.pio\libdeps\esp32-s3-devkitc-1\WiFiManager\WiFiManager.cpp:1010: DEBUG_WM(WM_DEBUG_VERBOSE,F("configportal closed"));
.pio\libdeps\esp32-s3-devkitc-1\WiFiManager\WiFiManager.cpp:1028: // make sure sta is on before `begin` so it does not call enablesta->mode while persistent is ON ( which would save WM AP state to eeprom !)
.pio\libdeps\esp32-s3-devkitc-1\WiFiManager\WiFiManager.cpp:1044: // if ssid argument provided connect to that
.pio\libdeps\esp32-s3-devkitc-1\WiFiManager\WiFiManager.cpp:1048: // @todo connect=false seems to disconnect sta in begin() so not sure if _connectonsave is useful at all
.pio\libdeps\esp32-s3-devkitc-1\WiFiManager\WiFiManager.cpp:1051: if(_saveTimeout > 0){
.pio\libdeps\esp32-s3-devkitc-1\WiFiManager\WiFiManager.cpp:1052: connRes = waitForConnectResult(_saveTimeout); // use default save timeout for saves to prevent bugs in esp->waitforconnectresult loop
.pio\libdeps\esp32-s3-devkitc-1\WiFiManager\WiFiManager.cpp:1060: // connect using saved ssid if there is one
.pio\libdeps\esp32-s3-devkitc-1\WiFiManager\WiFiManager.cpp:1067: DEBUG_WM(F("No wifi saved, skipping"));
.pio\libdeps\esp32-s3-devkitc-1\WiFiManager\WiFiManager.cpp:1089: if(connRes != WL_SCAN_COMPLETED){
.pio\libdeps\esp32-s3-devkitc-1\WiFiManager\WiFiManager.cpp:1090: updateConxResult(connRes);
.pio\libdeps\esp32-s3-devkitc-1\WiFiManager\WiFiManager.cpp:1097: * connect to a new wifi ap
.pio\libdeps\esp32-s3-devkitc-1\WiFiManager\WiFiManager.cpp:1102: * @return connect only save if false
.pio\libdeps\esp32-s3-devkitc-1\WiFiManager\WiFiManager.cpp:1122: * connect to stored wifi
.pio\libdeps\esp32-s3-devkitc-1\WiFiManager\WiFiManager.cpp:1130: DEBUG_WM(F("Connecting to SAVED AP:"),WiFi_SSID(true));
.pio\libdeps\esp32-s3-devkitc-1\WiFiManager\WiFiManager.cpp:1164: DEBUG_WM(WM_DEBUG_VERBOSE,F("Custom static IP/GW/Subnet/DNS"));
.pio\libdeps\esp32-s3-devkitc-1\WiFiManager\WiFiManager.cpp:1168: DEBUG_WM(WM_DEBUG_VERBOSE,F("Custom static DNS"));
.pio\libdeps\esp32-s3-devkitc-1\WiFiManager\WiFiManager.cpp:1174: DEBUG_WM(WM_DEBUG_VERBOSE,F("Custom STA IP/GW/Subnet"));
.pio\libdeps\esp32-s3-devkitc-1\WiFiManager\WiFiManager.cpp:1193:void WiFiManager::updateConxResult(uint8_t status){
.pio\libdeps\esp32-s3-devkitc-1\WiFiManager\WiFiManager.cpp:1281: page += _customHeadElement;
.pio\libdeps\esp32-s3-devkitc-1\WiFiManager\WiFiManager.cpp:1336: String str = FPSTR(HTTP_ROOT_MAIN); // @todo custom title
.pio\libdeps\esp32-s3-devkitc-1\WiFiManager\WiFiManager.cpp:1346: if(_preloadwifiscan) WiFi_scanNetworks(_scancachetime,true); // preload wifiscan throttled, async
.pio\libdeps\esp32-s3-devkitc-1\WiFiManager\WiFiManager.cpp:1355:void WiFiManager::handleWifi(boolean scan) {
.pio\libdeps\esp32-s3-devkitc-1\WiFiManager\WiFiManager.cpp:1361: if (scan) {
.pio\libdeps\esp32-s3-devkitc-1\WiFiManager\WiFiManager.cpp:1365: WiFi_scanNetworks(server->hasArg(F("refresh")),false); //wifiscan, force if arg refresh
.pio\libdeps\esp32-s3-devkitc-1\WiFiManager\WiFiManager.cpp:1366: page += getScanItemOut();
.pio\libdeps\esp32-s3-devkitc-1\WiFiManager\WiFiManager.cpp:1371: pitem.replace(FPSTR(T_v), F("wifisave")); // set form action
.pio\libdeps\esp32-s3-devkitc-1\WiFiManager\WiFiManager.cpp:1396: page += FPSTR(HTTP_SCAN_LINK);
.pio\libdeps\esp32-s3-devkitc-1\WiFiManager\WiFiManager.cpp:1421: pitem.replace(FPSTR(T_v), F("paramsave"));
.pio\libdeps\esp32-s3-devkitc-1\WiFiManager\WiFiManager.cpp:1443: if((String)_menutokens[menuId] == "custom" && _customMenuHTML!=NULL){
.pio\libdeps\esp32-s3-devkitc-1\WiFiManager\WiFiManager.cpp:1444: page += _customMenuHTML;
.pio\libdeps\esp32-s3-devkitc-1\WiFiManager\WiFiManager.cpp:1454:// // is it possible in softap mode to detect aps without scanning
.pio\libdeps\esp32-s3-devkitc-1\WiFiManager\WiFiManager.cpp:1455:// bool WiFiManager::WiFi_scanNetworksForAP(bool force){
.pio\libdeps\esp32-s3-devkitc-1\WiFiManager\WiFiManager.cpp:1456:// WiFi_scanNetworks(force);
.pio\libdeps\esp32-s3-devkitc-1\WiFiManager\WiFiManager.cpp:1459:void WiFiManager::WiFi_scanComplete(int networksFound){
.pio\libdeps\esp32-s3-devkitc-1\WiFiManager\WiFiManager.cpp:1460: _lastscan = millis();
.pio\libdeps\esp32-s3-devkitc-1\WiFiManager\WiFiManager.cpp:1463: DEBUG_WM(WM_DEBUG_VERBOSE,F("WiFi Scan ASYNC completed"), "in "+(String)(_lastscan - _startscan)+" ms");
.pio\libdeps\esp32-s3-devkitc-1\WiFiManager\WiFiManager.cpp:1464: DEBUG_WM(WM_DEBUG_VERBOSE,F("WiFi Scan ASYNC found:"),_numNetworks);
.pio\libdeps\esp32-s3-devkitc-1\WiFiManager\WiFiManager.cpp:1468:bool WiFiManager::WiFi_scanNetworks(){
.pio\libdeps\esp32-s3-devkitc-1\WiFiManager\WiFiManager.cpp:1469: return WiFi_scanNetworks(false,false);
.pio\libdeps\esp32-s3-devkitc-1\WiFiManager\WiFiManager.cpp:1472:bool WiFiManager::WiFi_scanNetworks(unsigned int cachetime,bool async){
.pio\libdeps\esp32-s3-devkitc-1\WiFiManager\WiFiManager.cpp:1473: return WiFi_scanNetworks(millis()-_lastscan > cachetime,async);
.pio\libdeps\esp32-s3-devkitc-1\WiFiManager\WiFiManager.cpp:1475:bool WiFiManager::WiFi_scanNetworks(unsigned int cachetime){
.pio\libdeps\esp32-s3-devkitc-1\WiFiManager\WiFiManager.cpp:1476: return WiFi_scanNetworks(millis()-_lastscan > cachetime,false);
.pio\libdeps\esp32-s3-devkitc-1\WiFiManager\WiFiManager.cpp:1478:bool WiFiManager::WiFi_scanNetworks(bool force,bool async){
.pio\libdeps\esp32-s3-devkitc-1\WiFiManager\WiFiManager.cpp:1480: // DEBUG_WM(WM_DEBUG_DEV,"scanNetworks async:",async == true);
.pio\libdeps\esp32-s3-devkitc-1\WiFiManager\WiFiManager.cpp:1481: // DEBUG_WM(WM_DEBUG_DEV,_numNetworks,(millis()-_lastscan ));
.pio\libdeps\esp32-s3-devkitc-1\WiFiManager\WiFiManager.cpp:1482: // DEBUG_WM(WM_DEBUG_DEV,"scanNetworks force:",force == true);
.pio\libdeps\esp32-s3-devkitc-1\WiFiManager\WiFiManager.cpp:1485: // if 0 networks, rescan @note this was a kludge, now disabling to test real cause ( maybe wifi not init etc)
.pio\libdeps\esp32-s3-devkitc-1\WiFiManager\WiFiManager.cpp:1487: if(_numNetworks == 0 && _autoforcerescan){
.pio\libdeps\esp32-s3-devkitc-1\WiFiManager\WiFiManager.cpp:1488: DEBUG_WM(WM_DEBUG_DEV,"NO APs found forcing new scan");
.pio\libdeps\esp32-s3-devkitc-1\WiFiManager\WiFiManager.cpp:1492: // if scan is empty or stale (last scantime > _scancachetime), this avoids fast reloading wifi page and constant scan delayed page loads appearing to freeze.
.pio\libdeps\esp32-s3-devkitc-1\WiFiManager\WiFiManager.cpp:1493: if(!_lastscan || (_lastscan>0 && (millis()-_lastscan > _scancachetime))){
.pio\libdeps\esp32-s3-devkitc-1\WiFiManager\WiFiManager.cpp:1499: _startscan = millis();
.pio\libdeps\esp32-s3-devkitc-1\WiFiManager\WiFiManager.cpp:1500: if(async && _asyncScan){
.pio\libdeps\esp32-s3-devkitc-1\WiFiManager\WiFiManager.cpp:1504: DEBUG_WM(WM_DEBUG_VERBOSE,F("WiFi Scan ASYNC started"));
.pio\libdeps\esp32-s3-devkitc-1\WiFiManager\WiFiManager.cpp:1507: WiFi.scanNetworksAsync(std::bind(&WiFiManager::WiFi_scanComplete,this,_1));
.pio\libdeps\esp32-s3-devkitc-1\WiFiManager\WiFiManager.cpp:1509: DEBUG_WM(WM_DEBUG_VERBOSE,F("WiFi Scan SYNC started"));
.pio\libdeps\esp32-s3-devkitc-1\WiFiManager\WiFiManager.cpp:1510: res = WiFi.scanNetworks();
.pio\libdeps\esp32-s3-devkitc-1\WiFiManager\WiFiManager.cpp:1514: DEBUG_WM(WM_DEBUG_VERBOSE,F("WiFi Scan ASYNC started"));
.pio\libdeps\esp32-s3-devkitc-1\WiFiManager\WiFiManager.cpp:1516: res = WiFi.scanNetworks(true);
.pio\libdeps\esp32-s3-devkitc-1\WiFiManager\WiFiManager.cpp:1521: DEBUG_WM(WM_DEBUG_VERBOSE,F("WiFi Scan SYNC started"));
.pio\libdeps\esp32-s3-devkitc-1\WiFiManager\WiFiManager.cpp:1522: res = WiFi.scanNetworks();
.pio\libdeps\esp32-s3-devkitc-1\WiFiManager\WiFiManager.cpp:1524: if(res == WIFI_SCAN_FAILED){
.pio\libdeps\esp32-s3-devkitc-1\WiFiManager\WiFiManager.cpp:1526: DEBUG_WM(WM_DEBUG_ERROR,F("[ERROR] scan failed"));
.pio\libdeps\esp32-s3-devkitc-1\WiFiManager\WiFiManager.cpp:1529: else if(res == WIFI_SCAN_RUNNING){
.pio\libdeps\esp32-s3-devkitc-1\WiFiManager\WiFiManager.cpp:1531: DEBUG_WM(WM_DEBUG_ERROR,F("[ERROR] scan waiting"));
.pio\libdeps\esp32-s3-devkitc-1\WiFiManager\WiFiManager.cpp:1533: while(WiFi.scanComplete() == WIFI_SCAN_RUNNING){
.pio\libdeps\esp32-s3-devkitc-1\WiFiManager\WiFiManager.cpp:1539: _numNetworks = WiFi.scanComplete();
.pio\libdeps\esp32-s3-devkitc-1\WiFiManager\WiFiManager.cpp:1542: _lastscan = millis();
.pio\libdeps\esp32-s3-devkitc-1\WiFiManager\WiFiManager.cpp:1544: DEBUG_WM(WM_DEBUG_VERBOSE,F("WiFi Scan completed"), "in "+(String)(_lastscan - _startscan)+" ms");
.pio\libdeps\esp32-s3-devkitc-1\WiFiManager\WiFiManager.cpp:1550: DEBUG_WM(WM_DEBUG_VERBOSE,F("Scan is cached"),(String)(millis()-_lastscan )+" ms ago");
.pio\libdeps\esp32-s3-devkitc-1\WiFiManager\WiFiManager.cpp:1556:String WiFiManager::WiFiManager::getScanItemOut(){
.pio\libdeps\esp32-s3-devkitc-1\WiFiManager\WiFiManager.cpp:1559: if(!_numNetworks) WiFi_scanNetworks(); // scan in case this gets called before any scans
.pio\libdeps\esp32-s3-devkitc-1\WiFiManager\WiFiManager.cpp:1564: DEBUG_WM(F("No networks found"));
.pio\libdeps\esp32-s3-devkitc-1\WiFiManager\WiFiManager.cpp:1617: HTTP_ITEM_STR.replace("{h}",_scanDispOptions ? "" : "h");
.pio\libdeps\esp32-s3-devkitc-1\WiFiManager\WiFiManager.cpp:1619: HTTP_ITEM_STR.replace("{h}",_scanDispOptions ? "h" : "");
.pio\libdeps\esp32-s3-devkitc-1\WiFiManager\WiFiManager.cpp:1697: // @todo how can we get these accurate settings from memory , wifi_get_ip_info does not seem to reveal if struct ip_info is static or not
.pio\libdeps\esp32-s3-devkitc-1\WiFiManager\WiFiManager.cpp:1769: // if no ID use customhtml for item, else generate from param string
.pio\libdeps\esp32-s3-devkitc-1\WiFiManager\WiFiManager.cpp:1779: if(tok_c)pitem.replace(FPSTR(T_c), _params[i]->getCustomHTML()); // T_c meant for additional attributes, not html, but can stuff
.pio\libdeps\esp32-s3-devkitc-1\WiFiManager\WiFiManager.cpp:1781: pitem = _params[i]->getCustomHTML();
.pio\libdeps\esp32-s3-devkitc-1\WiFiManager\WiFiManager.cpp:1805: * HTTPD CALLBACK save form and redirect to WLAN config page again
.pio\libdeps\esp32-s3-devkitc-1\WiFiManager\WiFiManager.cpp:1807:void WiFiManager::handleWifiSave() {
.pio\libdeps\esp32-s3-devkitc-1\WiFiManager\WiFiManager.cpp:1809: DEBUG_WM(WM_DEBUG_VERBOSE,F("<- HTTP WiFi save "));
.pio\libdeps\esp32-s3-devkitc-1\WiFiManager\WiFiManager.cpp:1814: //SAVE/connect here
.pio\libdeps\esp32-s3-devkitc-1\WiFiManager\WiFiManager.cpp:1821: DEBUG_WM(WM_DEBUG_VERBOSE,F("Detected WiFi password change"));
.pio\libdeps\esp32-s3-devkitc-1\WiFiManager\WiFiManager.cpp:1826: String requestinfo = "SERVER_REQUEST\n----------------\n";
.pio\libdeps\esp32-s3-devkitc-1\WiFiManager\WiFiManager.cpp:1827: requestinfo += "URI: ";
.pio\libdeps\esp32-s3-devkitc-1\WiFiManager\WiFiManager.cpp:1828: requestinfo += server->uri();
.pio\libdeps\esp32-s3-devkitc-1\WiFiManager\WiFiManager.cpp:1829: requestinfo += "\nMethod: ";
.pio\libdeps\esp32-s3-devkitc-1\WiFiManager\WiFiManager.cpp:1830: requestinfo += (server->method() == HTTP_GET) ? "GET" : "POST";
.pio\libdeps\esp32-s3-devkitc-1\WiFiManager\WiFiManager.cpp:1831: requestinfo += "\nArguments: ";
.pio\libdeps\esp32-s3-devkitc-1\WiFiManager\WiFiManager.cpp:1832: requestinfo += server->args();
.pio\libdeps\esp32-s3-devkitc-1\WiFiManager\WiFiManager.cpp:1833: requestinfo += "\n";
.pio\libdeps\esp32-s3-devkitc-1\WiFiManager\WiFiManager.cpp:1835: requestinfo += " " + server->argName(i) + ": " + server->arg(i) + "\n";
.pio\libdeps\esp32-s3-devkitc-1\WiFiManager\WiFiManager.cpp:1838: DEBUG_WM(WM_DEBUG_MAX,requestinfo);
.pio\libdeps\esp32-s3-devkitc-1\WiFiManager\WiFiManager.cpp:1872: if (_presavewificallback != NULL) {
.pio\libdeps\esp32-s3-devkitc-1\WiFiManager\WiFiManager.cpp:1873: _presavewificallback(); // @CALLBACK
.pio\libdeps\esp32-s3-devkitc-1\WiFiManager\WiFiManager.cpp:1876: if(_paramsInWifi) doParamSave();
.pio\libdeps\esp32-s3-devkitc-1\WiFiManager\WiFiManager.cpp:1881: page = getHTTPHead(FPSTR(S_titlewifisettings)); // @token titleparamsaved
.pio\libdeps\esp32-s3-devkitc-1\WiFiManager\WiFiManager.cpp:1882: page += FPSTR(HTTP_PARAMSAVED);
.pio\libdeps\esp32-s3-devkitc-1\WiFiManager\WiFiManager.cpp:1885: page = getHTTPHead(FPSTR(S_titlewifisaved)); // @token titlewifisaved
.pio\libdeps\esp32-s3-devkitc-1\WiFiManager\WiFiManager.cpp:1886: page += FPSTR(HTTP_SAVED);
.pio\libdeps\esp32-s3-devkitc-1\WiFiManager\WiFiManager.cpp:1896: DEBUG_WM(WM_DEBUG_DEV,F("Sent wifi save page"));
.pio\libdeps\esp32-s3-devkitc-1\WiFiManager\WiFiManager.cpp:1899: connect = true; //signal ready to connect/reset process in processConfigPortal
.pio\libdeps\esp32-s3-devkitc-1\WiFiManager\WiFiManager.cpp:1902:void WiFiManager::handleParamSave() {
.pio\libdeps\esp32-s3-devkitc-1\WiFiManager\WiFiManager.cpp:1905: DEBUG_WM(WM_DEBUG_VERBOSE,F("<- HTTP Param save "));
.pio\libdeps\esp32-s3-devkitc-1\WiFiManager\WiFiManager.cpp:1912: doParamSave();
.pio\libdeps\esp32-s3-devkitc-1\WiFiManager\WiFiManager.cpp:1914: String page = getHTTPHead(FPSTR(S_titleparamsaved)); // @token titleparamsaved
.pio\libdeps\esp32-s3-devkitc-1\WiFiManager\WiFiManager.cpp:1915: page += FPSTR(HTTP_PARAMSAVED);
.pio\libdeps\esp32-s3-devkitc-1\WiFiManager\WiFiManager.cpp:1922: DEBUG_WM(WM_DEBUG_DEV,F("Sent param save page"));
.pio\libdeps\esp32-s3-devkitc-1\WiFiManager\WiFiManager.cpp:1926:void WiFiManager::doParamSave(){
.pio\libdeps\esp32-s3-devkitc-1\WiFiManager\WiFiManager.cpp:1927: // @todo use new callback for before paramsaves, is this really needed?
.pio\libdeps\esp32-s3-devkitc-1\WiFiManager\WiFiManager.cpp:1928: if ( _presaveparamscallback != NULL) {
.pio\libdeps\esp32-s3-devkitc-1\WiFiManager\WiFiManager.cpp:1929: _presaveparamscallback(); // @CALLBACK
.pio\libdeps\esp32-s3-devkitc-1\WiFiManager\WiFiManager.cpp:1966: if ( _saveparamscallback != NULL) {
.pio\libdeps\esp32-s3-devkitc-1\WiFiManager\WiFiManager.cpp:1967: _saveparamscallback(); // @CALLBACK
.pio\libdeps\esp32-s3-devkitc-1\WiFiManager\WiFiManager.cpp:1973: * HTTPD CALLBACK info page
.pio\libdeps\esp32-s3-devkitc-1\WiFiManager\WiFiManager.cpp:1975:void WiFiManager::handleInfo() {
.pio\libdeps\esp32-s3-devkitc-1\WiFiManager\WiFiManager.cpp:1977: DEBUG_WM(WM_DEBUG_VERBOSE,F("<- HTTP Info"));
.pio\libdeps\esp32-s3-devkitc-1\WiFiManager\WiFiManager.cpp:1980: String page = getHTTPHead(FPSTR(S_titleinfo)); // @token titleinfo
.pio\libdeps\esp32-s3-devkitc-1\WiFiManager\WiFiManager.cpp:1983: uint16_t infos = 0;
.pio\libdeps\esp32-s3-devkitc-1\WiFiManager\WiFiManager.cpp:1986: //@todo wrap in build flag to remove all info code for memory saving
.pio\libdeps\esp32-s3-devkitc-1\WiFiManager\WiFiManager.cpp:1988: infos = 28;
.pio\libdeps\esp32-s3-devkitc-1\WiFiManager\WiFiManager.cpp:1989: String infoids[] = {
.pio\libdeps\esp32-s3-devkitc-1\WiFiManager\WiFiManager.cpp:2002: F("lastreset"),
.pio\libdeps\esp32-s3-devkitc-1\WiFiManager\WiFiManager.cpp:2021: // add esp_chip_info ?
.pio\libdeps\esp32-s3-devkitc-1\WiFiManager\WiFiManager.cpp:2022: infos = 27;
.pio\libdeps\esp32-s3-devkitc-1\WiFiManager\WiFiManager.cpp:2023: String infoids[] = {
.pio\libdeps\esp32-s3-devkitc-1\WiFiManager\WiFiManager.cpp:2034: F("lastreset"),
.pio\libdeps\esp32-s3-devkitc-1\WiFiManager\WiFiManager.cpp:2055: for(size_t i=0; i<infos;i++){
.pio\libdeps\esp32-s3-devkitc-1\WiFiManager\WiFiManager.cpp:2056: if(infoids[i] != NULL) page += getInfoData(infoids[i]);
.pio\libdeps\esp32-s3-devkitc-1\WiFiManager\WiFiManager.cpp:2061: page += getInfoData("aboutver");
.pio\libdeps\esp32-s3-devkitc-1\WiFiManager\WiFiManager.cpp:2062: page += getInfoData("aboutarduinover");
.pio\libdeps\esp32-s3-devkitc-1\WiFiManager\WiFiManager.cpp:2063: page += getInfoData("aboutidfver");
.pio\libdeps\esp32-s3-devkitc-1\WiFiManager\WiFiManager.cpp:2064: page += getInfoData("aboutdate");
.pio\libdeps\esp32-s3-devkitc-1\WiFiManager\WiFiManager.cpp:2067: if(_showInfoUpdate){
.pio\libdeps\esp32-s3-devkitc-1\WiFiManager\WiFiManager.cpp:2071: if(_showInfoErase) page += FPSTR(HTTP_ERASEBTN);
.pio\libdeps\esp32-s3-devkitc-1\WiFiManager\WiFiManager.cpp:2079: DEBUG_WM(WM_DEBUG_DEV,F("Sent info page"));
.pio\libdeps\esp32-s3-devkitc-1\WiFiManager\WiFiManager.cpp:2083:String WiFiManager::getInfoData(String id){
.pio\libdeps\esp32-s3-devkitc-1\WiFiManager\WiFiManager.cpp:2087: p = FPSTR(HTTP_INFO_esphead);
.pio\libdeps\esp32-s3-devkitc-1\WiFiManager\WiFiManager.cpp:2093: p = FPSTR(HTTP_INFO_wifihead);
.pio\libdeps\esp32-s3-devkitc-1\WiFiManager\WiFiManager.cpp:2098: p = FPSTR(HTTP_INFO_uptime);
.pio\libdeps\esp32-s3-devkitc-1\WiFiManager\WiFiManager.cpp:2103: p = FPSTR(HTTP_INFO_chipid);
.pio\libdeps\esp32-s3-devkitc-1\WiFiManager\WiFiManager.cpp:2108: p = FPSTR(HTTP_INFO_chiprev);
.pio\libdeps\esp32-s3-devkitc-1\WiFiManager\WiFiManager.cpp:2120: p = FPSTR(HTTP_INFO_fchipid);
.pio\libdeps\esp32-s3-devkitc-1\WiFiManager\WiFiManager.cpp:2125: p = FPSTR(HTTP_INFO_idesize);
.pio\libdeps\esp32-s3-devkitc-1\WiFiManager\WiFiManager.cpp:2130: p = FPSTR(HTTP_INFO_flashsize);
.pio\libdeps\esp32-s3-devkitc-1\WiFiManager\WiFiManager.cpp:2133: p = FPSTR(HTTP_INFO_psrsize);
.pio\libdeps\esp32-s3-devkitc-1\WiFiManager\WiFiManager.cpp:2139: p = FPSTR(HTTP_INFO_corever);
.pio\libdeps\esp32-s3-devkitc-1\WiFiManager\WiFiManager.cpp:2145: p = FPSTR(HTTP_INFO_bootver);
.pio\libdeps\esp32-s3-devkitc-1\WiFiManager\WiFiManager.cpp:2150: p = FPSTR(HTTP_INFO_cpufreq);
.pio\libdeps\esp32-s3-devkitc-1\WiFiManager\WiFiManager.cpp:2154: p = FPSTR(HTTP_INFO_freeheap);
.pio\libdeps\esp32-s3-devkitc-1\WiFiManager\WiFiManager.cpp:2158: p = FPSTR(HTTP_INFO_memsketch);
.pio\libdeps\esp32-s3-devkitc-1\WiFiManager\WiFiManager.cpp:2163: p = FPSTR(HTTP_INFO_memsmeter);
.pio\libdeps\esp32-s3-devkitc-1\WiFiManager\WiFiManager.cpp:2167: else if(id==F("lastreset")){
.pio\libdeps\esp32-s3-devkitc-1\WiFiManager\WiFiManager.cpp:2169: p = FPSTR(HTTP_INFO_lastreset);
.pio\libdeps\esp32-s3-devkitc-1\WiFiManager\WiFiManager.cpp:2170: p.replace(FPSTR(T_1),(String)ESP.getResetReason());
.pio\libdeps\esp32-s3-devkitc-1\WiFiManager\WiFiManager.cpp:2173: p = FPSTR(HTTP_INFO_lastreset);
.pio\libdeps\esp32-s3-devkitc-1\WiFiManager\WiFiManager.cpp:2175: int reason = rtc_get_reset_reason(i);
.pio\libdeps\esp32-s3-devkitc-1\WiFiManager\WiFiManager.cpp:2180: case 1 : p.replace(tok,F("Vbat power on reset"));break;
.pio\libdeps\esp32-s3-devkitc-1\WiFiManager\WiFiManager.cpp:2181: case 3 : p.replace(tok,F("Software reset digital core"));break;
.pio\libdeps\esp32-s3-devkitc-1\WiFiManager\WiFiManager.cpp:2182: case 4 : p.replace(tok,F("Legacy watch dog reset digital core"));break;
.pio\libdeps\esp32-s3-devkitc-1\WiFiManager\WiFiManager.cpp:2183: case 5 : p.replace(tok,F("Deep Sleep reset digital core"));break;
.pio\libdeps\esp32-s3-devkitc-1\WiFiManager\WiFiManager.cpp:2184: case 6 : p.replace(tok,F("Reset by SLC module, reset digital core"));break;
.pio\libdeps\esp32-s3-devkitc-1\WiFiManager\WiFiManager.cpp:2185: case 7 : p.replace(tok,F("Timer Group0 Watch dog reset digital core"));break;
.pio\libdeps\esp32-s3-devkitc-1\WiFiManager\WiFiManager.cpp:2186: case 8 : p.replace(tok,F("Timer Group1 Watch dog reset digital core"));break;
[Showing results with pagination = limit: 250]

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1 @@
{"agentType":"Explore","description":"查找报警和水浸逻辑","toolUseId":"call_00_wPSOk3gDw0mP3Mj0s8Xb0494","spawnDepth":1}

File diff suppressed because one or more lines are too long

View File

@@ -1,9 +1,12 @@
/** /**
* @file alarm_manager.h * @file alarm_manager.h
* @brief 报警管理器 - 双模式 (本地判定 + 服务端指令) * @brief 报警管理器 - 多类型分优先级报警 (本地判定 + 服务端指令)
* *
* 本地模式: 离线也能用, 跌倒/水浸/心率异常/呼吸异常 本地判定触发 * 优先级: 跌倒 > 水浸 > 心率异常 > 呼吸异常 > 温湿度异常 > 传感器故障
*
* 本地模式: 离线也能用, 全部类型本地判定触发
* 服务端模式: 收到 cmd/{gateid}/alarm, alarm/restore 执行联动 * 服务端模式: 收到 cmd/{gateid}/alarm, alarm/restore 执行联动
* 防抖: 传感器故障需连续 N 次确认才触发, 避免误报
*/ */
#ifndef ALARM_MANAGER_H #ifndef ALARM_MANAGER_H
@@ -12,16 +15,23 @@
#include <Arduino.h> #include <Arduino.h>
#include "config.h" #include "config.h"
// 报警回调 — 通知外部 (如 MQTT 上报)
typedef void (*AlarmCallback)(uint8_t type, bool triggered, const char* desc);
class AlarmManager { class AlarmManager {
public: public:
// 告警类型 (本地 + 服务端共用) // 告警类型 (按优先级从高到低)
enum AlarmType { enum AlarmType : uint8_t {
ALARM_NONE = 0, ALARM_NONE = 0,
ALARM_FALL, // 跌倒 (本地 + 服务端) ALARM_FALL, // 跌倒 (本地 + 服务端) ★★★★★
ALARM_WATER_LEAK, // 水浸 (本地 + 服务端) ALARM_WATER_LEAK, // 水浸 (本地 + 服务端) ★★★★
ALARM_HEART_ABNORMAL, // 心率异常 (本地) ALARM_HEART_ABNORMAL, // 心率异常 (本地) ★★★
ALARM_BREATH_ABNORMAL, // 呼吸异常 (本地) ALARM_BREATH_ABNORMAL, // 呼吸异常 (本地) ★★★
ALARM_TEMP_ABNORMAL, // 温度异常 (本地) ★★
ALARM_HUMI_ABNORMAL, // 湿度异常 (本地) ★★
ALARM_SENSOR_FAULT, // 传感器故障 (本地) ★
ALARM_SERVER, // 服务端下发 (通用) ALARM_SERVER, // 服务端下发 (通用)
ALARM_COUNT
}; };
AlarmManager(); AlarmManager();
@@ -34,11 +44,11 @@ public:
// 本地报警接口 (离线可用) // 本地报警接口 (离线可用)
// ============================================================ // ============================================================
/** 触发本地报警 */ /** 触发本地报警 (防抖由调用方处理) */
void triggerLocal(AlarmType type); void triggerLocal(AlarmType type, const char* detail = nullptr);
/** 清除本地报警 */ /** 清除本地报警 */
void clearLocal(AlarmType type); void clearLocal(AlarmType type);
/** 清除所有本地报警 */ /** 清除所有本地报警 (含防抖计数器) */
void clearAllLocal(); void clearAllLocal();
// ============================================================ // ============================================================
@@ -53,33 +63,58 @@ public:
void buzzerTest(uint32_t durationSec); void buzzerTest(uint32_t durationSec);
// ============================================================ // ============================================================
// 状态查询 (上报用) // 回调注册
// ============================================================
/** 报警状态变化回调 (触发/清除时调用, 用于MQTT上报) */
void onAlarmChange(AlarmCallback cb) { _alarmCb = cb; }
// ============================================================
// 状态查询
// ============================================================ // ============================================================
bool isBuzzerOn() const { return _buzzerOn; } bool isBuzzerOn() const { return _buzzerOn; }
bool hasActiveAlarm() const { return _activeCount > 0; } bool hasActiveAlarm() const { return _activeCount > 0; }
const char* getActiveAlarmType() const; uint8_t getActiveCount() const { return _activeCount; }
AlarmType getHighestAlarm() const;
const char* getAlarmName(AlarmType type) const;
const char* getActiveAlarmStr() const;
private: private:
// 报警优先级 (越小越紧急)
static uint8_t _priority(AlarmType t);
void _setBuzzer(bool on); void _setBuzzer(bool on);
void _setLED(bool on); void _setLED(bool on);
void _updateBuzzer(); void _updateBuzzer();
void _updateLED(); void _updateLED();
uint32_t _buzzerIntervalFor(AlarmType type);
// 蜂鸣器/LED 状态 // 蜂鸣器/LED 状态
bool _buzzerOn; bool _buzzerOn;
bool _ledOn; bool _ledOn;
uint32_t _buzzerOffTime; // 定时关闭时刻 (0=手动) uint32_t _buzzerOffTime; // 定时关闭时刻 (0=手动/无限)
uint32_t _buzzerToggleTime; // 间歇切换时刻 uint32_t _buzzerToggleTime; // 间歇切换时刻
bool _buzzerBeepState; // 间歇状态 (响/停) bool _buzzerBeepState; // 间歇状态 (响/停)
uint8_t _beepCount; // 当前周期哔了几声 uint8_t _beepCount; // 当前周期哔了几声
uint8_t _beepsPerCycle; // 当前报警类型每周期几声
uint32_t _beepOnMs; // 每声持续 ms
uint32_t _beepOffMs; // 每声间隔 ms
uint32_t _cyclePauseMs; // 周期间停顿 ms
// 活动报警列表 (最多 4 个同时) // 活动报警列表 (最多 5 个同时)
AlarmType _activeAlarms[4]; static const uint8_t MAX_ACTIVE = 5;
AlarmType _activeAlarms[MAX_ACTIVE];
uint8_t _activeCount; uint8_t _activeCount;
// 当前详情 (用于上报)
char _activeDetail[64];
// LED 闪烁 // LED 闪烁
uint32_t _lastLEDToggle; uint32_t _lastLEDToggle;
// 回调
AlarmCallback _alarmCb;
}; };
#endif // ALARM_MANAGER_H #endif // ALARM_MANAGER_H

View File

@@ -1,10 +1,13 @@
/** /**
* @file button_manager.h * @file button_manager.h
* @brief 三按钮管理器 - 去抖 + 长按/短按识别 * @brief 三按钮管理器 - 双向去抖 + 长按/短按识别 + 超时自愈
* *
* 按钮1 (GPIO14): 长按3秒 → 配网 * 按钮1 (GPIO14): 长按1.5秒 → 配网
* 按钮2 (GPIO15): 短按 → 解除报警 * 按钮2 (GPIO13): 短按 → 解除报警
* 按钮3 (GPIO16): 短按 → 翻页 * 按钮3 (GPIO46): 短按 → 翻页
*
* 去抖策略: 状态变化需持续稳定 BTN_DEBOUNCE_MS 才确认
* 自愈策略: 按钮按下超过 10 秒强制复位,防止卡死
*/ */
#ifndef BUTTON_MANAGER_H #ifndef BUTTON_MANAGER_H
@@ -13,6 +16,8 @@
#include <Arduino.h> #include <Arduino.h>
#include "config.h" #include "config.h"
#define BTN_STUCK_TIMEOUT_MS 10000 // 按钮卡死超时 (10秒强制复位)
typedef void (*ButtonCallback)(void); typedef void (*ButtonCallback)(void);
class ButtonManager { class ButtonManager {
@@ -31,17 +36,32 @@ public:
void onPageSwitch(ButtonCallback cb) { _cbPage = cb; } void onPageSwitch(ButtonCallback cb) { _cbPage = cb; }
private: private:
/**
* 按钮状态机 (基于稳定读数而非边沿)
*
* 状态转换:
* IDLE ──[raw稳定LOW×BTN_DEBOUNCE_MS]──→ PRESSED (记录pressTime)
* PRESSED ──[hold≥BTN_LONG_PRESS_MS]──→ 触发长按回调 (仅限hasLong)
* PRESSED ──[raw稳定HIGH×BTN_DEBOUNCE_MS]──→ 检查hold时长触发短按 → IDLE
* PRESSED ──[hold≥BTN_STUCK_TIMEOUT_MS]──→ 强制复位 → IDLE (防卡死)
*/
struct ButtonState { struct ButtonState {
uint8_t pin; uint8_t pin;
bool pressed; // 当前是否在按下状态 bool stableState; // 当前确认的稳定状态 (true=按下)
uint32_t pressTime; // 按下时刻 (ms) bool rawState; // 上一次 raw 读值
bool longTriggered; // 防止长按时重复触发短按 uint32_t changeTime; // raw 状态变化时刻
uint32_t pressTime; // 稳定按下时刻 (ms)
bool longTriggered; // 长按已触发
bool shortPending; // 等待释放以判定短按
}; };
/** 处理单个按钮的状态机 */ /** 处理单个按钮 (基于稳定状态而非瞬时边沿) */
void _processButton(ButtonState& btn, bool hasShort, bool hasLong); void _processButton(ButtonState& btn, bool hasShort, bool hasLong);
bool _readPin(uint8_t pin); bool _readPin(uint8_t pin);
/** 强制复位单个按钮 */
void _resetButton(ButtonState& btn);
ButtonState _btnProvision; ButtonState _btnProvision;
ButtonState _btnDismiss; ButtonState _btnDismiss;
ButtonState _btnPage; ButtonState _btnPage;

View File

@@ -16,10 +16,15 @@
// 1. 硬件引脚定义 (ESP32-S3) // 1. 硬件引脚定义 (ESP32-S3)
// ============================================================ // ============================================================
// --- R60ABD1 毫米波雷达 --- // --- 雷达型号选择 ---
#define RADAR_TYPE_ABD1 0 // R60ABD1: 心率/呼吸精准, 跌倒一般
#define RADAR_TYPE_AFD1 1 // R60AFD1: 跌倒/姿态精准, 无心率呼吸
#define RADAR_TYPE RADAR_TYPE_AFD1 // AFD1 (跌倒/姿态精准)
// --- 毫米波雷达 ---
#define RADAR_UART_PORT Serial2 #define RADAR_UART_PORT Serial2
#define RADAR_RX_PIN 17 #define RADAR_RX_PIN 17
#define RADAR_TX_PIN 1 // UART 发送 (从18挪到1) #define RADAR_TX_PIN 1
#define RADAR_BAUD_RATE 115200 #define RADAR_BAUD_RATE 115200
// --- 雷达备用 GPIO --- // --- 雷达备用 GPIO ---
@@ -30,10 +35,10 @@
#define DHT11_PIN 4 // 温湿度传感器 #define DHT11_PIN 4 // 温湿度传感器
// --- 水浸传感器 --- // --- 水浸传感器 ---
#define WATER_SENSOR_PIN 6 // 数字输入, 低电平=有水 #define WATER_SENSOR_PIN 45 // 数字输入, 低电平=有水
// --- 蜂鸣器 (PWM) --- // --- 蜂鸣器 (PWM) ---
#define BUZZER_PIN 7 #define BUZZER_PIN 21
#define BUZZER_PWM_CHANNEL 0 #define BUZZER_PWM_CHANNEL 0
#define BUZZER_PWM_FREQ 2000 #define BUZZER_PWM_FREQ 2000
#define BUZZER_PWM_RESOLUTION 8 #define BUZZER_PWM_RESOLUTION 8
@@ -47,15 +52,15 @@
#define LED_ALARM_PIN 9 // 红色 - 报警指示 #define LED_ALARM_PIN 9 // 红色 - 报警指示
// --- OLED 显示屏 (SSD1306 128x64 I2C) --- // --- OLED 显示屏 (SSD1306 128x64 I2C) ---
#define OLED_SDA_PIN 14 // I2C 数据线 #define OLED_SDA_PIN 15 // I2C 数据线
#define OLED_SCL_PIN 15 // I2C 时钟线 #define OLED_SCL_PIN 16 // I2C 时钟线
// --- 交互按钮 --- // --- 交互按钮 ---
#define BTN_PROVISION_PIN 16 // 擦除WiFi配网按钮 (长按3秒) #define BTN_PROVISION_PIN 14 // 擦除WiFi配网按钮 (长按3秒)
#define BTN_ALARM_DISMISS_PIN 13 // 解除报警按钮 #define BTN_ALARM_DISMISS_PIN 13 // 解除报警按钮
#define BTN_PAGE_SWITCH_PIN 10 // 翻页按钮 #define BTN_PAGE_SWITCH_PIN 46 // 翻页按钮
#define BTN_DEBOUNCE_MS 50 // 去抖时间 #define BTN_DEBOUNCE_MS 50 // 去抖时间
#define BTN_LONG_PRESS_MS 3000 // 长按判定 #define BTN_LONG_PRESS_MS 1500 // 长按判定
// ============================================================ // ============================================================
// 2. 设备标识 (协议规范 §2) // 2. 设备标识 (协议规范 §2)
@@ -72,8 +77,8 @@
// ============================================================ // ============================================================
#define WIFI_AP_SSID "HealthMonitor-Setup" #define WIFI_AP_SSID "HealthMonitor-Setup"
#define WIFI_AP_PASSWORD "12345678" #define WIFI_AP_PASSWORD "1234567890"
#define WIFI_CONFIG_TIMEOUT 300 #define WIFI_CONFIG_TIMEOUT 0 // 0=不超时, 一直等待配置
#define WIFI_CONNECT_TIMEOUT 15 #define WIFI_CONNECT_TIMEOUT 15
#define WIFI_RETRY_INTERVAL 10000 #define WIFI_RETRY_INTERVAL 10000
@@ -115,8 +120,9 @@
// 6. 上报配置 (协议规范 §4.1, §7.2) // 6. 上报配置 (协议规范 §4.1, §7.2)
// ============================================================ // ============================================================
#define REPORT_INTERVAL_MS 1000 // 遥测上报周期 1秒 #define REPORT_INTERVAL_MS 1000 // 遥测上报周期 1秒 (有人时)
#define REPORT_QOS 0 // 遥测 QoS 0 #define REPORT_QOS 0 // 遥测 QoS 0
#define PRESENCE_HEARTBEAT_MS 60000 // 无人心跳间隔 60秒 (4G省流量)
#define STATUS_QOS 1 // 状态/响应 QoS 1 #define STATUS_QOS 1 // 状态/响应 QoS 1
// ============================================================ // ============================================================
@@ -130,6 +136,7 @@
#define TEMP_LOW 5.0f #define TEMP_LOW 5.0f
#define TEMP_HIGH 45.0f #define TEMP_HIGH 45.0f
#define HUMI_LOW 15.0f #define HUMI_LOW 15.0f
#define PRESENCE_HYSTERESIS 8 // 有人无人防抖 (PCB信号抖动时过滤)
#define HUMI_HIGH 90.0f #define HUMI_HIGH 90.0f
#define WATER_DEBOUNCE_MS 50 #define WATER_DEBOUNCE_MS 50
@@ -167,10 +174,11 @@
struct TelemetryData { struct TelemetryData {
// --- 设备标识 --- // --- 设备标识 ---
char saleid[12]; // 站点编号 "10100001" char saleid[12]; // 站点编号 (已弃用)
char gateid[16]; // 网关编号 "10100001001" char gateid[16]; // 网关编号
char meterId[8]; // 节点ID "ENV001" char meterId[8]; // 节点ID (已弃用, 新用 gateid+后缀)
char meterName[16]; // 设备型号 "RadarDHT11" char meterName[16]; // 设备型号 "RadarDHT11"
char deviceType[16]; // 设备类型枚举 bedroom/bathroom/...
// --- 环境数据 (DHT11) --- // --- 环境数据 (DHT11) ---
float temp; // 温度 ℃ float temp; // 温度 ℃
@@ -187,6 +195,8 @@ struct TelemetryData {
uint8_t heartRate; // 心率 bpm (无效=0) uint8_t heartRate; // 心率 bpm (无效=0)
uint8_t breathRate; // 呼吸 次/分 (无效=0) uint8_t breathRate; // 呼吸 次/分 (无效=0)
uint8_t body; // 人体能量/体征值 uint8_t body; // 人体能量/体征值
uint8_t sleepState; // 睡眠状态 0=清醒 1=浅睡 2=深睡
uint8_t sleepScore; // 睡眠评分 0~100
bool radarValid; // 雷达数据有效性 bool radarValid; // 雷达数据有效性
bool vitalValid; // 心率呼吸有效性 bool vitalValid; // 心率呼吸有效性

View File

@@ -29,8 +29,11 @@ public:
* @param data 遥测数据 * @param data 遥测数据
* @param wifiOk WiFi 是否已连接 * @param wifiOk WiFi 是否已连接
* @param buzzerOn 蜂鸣器是否在响 * @param buzzerOn 蜂鸣器是否在响
* @param hasAlarm 是否有活跃报警
* @param alarmName 报警名称 (如 "跌倒"/"心率异常")
*/ */
void update(const TelemetryData& data, bool wifiOk, bool buzzerOn); void update(const TelemetryData& data, bool wifiOk, bool buzzerOn,
bool hasAlarm = false, const char* alarmName = "");
/** 切换到下一页 (0→1→2→3→0 循环) */ /** 切换到下一页 (0→1→2→3→0 循环) */
void nextPage(); void nextPage();
@@ -39,10 +42,11 @@ public:
uint8_t getPage() const { return _pageIndex; } uint8_t getPage() const { return _pageIndex; }
private: private:
void _drawPage0(const TelemetryData& data, bool wifiOk, bool buzzerOn); void _drawPage0(const TelemetryData& data, bool wifiOk, bool buzzerOn, bool hasAlarm);
void _drawPage1(const TelemetryData& data); void _drawPage1(const TelemetryData& data, bool hasAlarm);
void _drawPage2(const TelemetryData& data); void _drawPage2(const TelemetryData& data, bool hasAlarm);
void _drawPage3(const TelemetryData& data, bool buzzerOn, bool wifiOk); void _drawPage3(const TelemetryData& data, bool buzzerOn, bool wifiOk, bool hasAlarm);
void _drawAlarmBanner(const char* name);
/** 格式化秒数为 HH:MM:SS */ /** 格式化秒数为 HH:MM:SS */
String _fmtUptime(uint32_t secs) const; String _fmtUptime(uint32_t secs) const;

View File

@@ -80,6 +80,12 @@ public:
const char* errorCode, const char* errorMsg, const char* errorCode, const char* errorMsg,
const char* wifiSignal = ""); const char* wifiSignal = "");
/** 本地报警上报 status/{gateid}/alarm (§4.7 扩展) */
void publishLocalAlarm(uint8_t alarmType, bool triggered, const char* desc);
/** 无人极简报文 (仅 Presence+RadarValid, 省4G流量) */
void publishPresenceOnly(bool presence, bool radarValid, uint32_t sequence);
// ============================================================ // ============================================================
// 回调注册 (协议规范 §5) // 回调注册 (协议规范 §5)
// ============================================================ // ============================================================

View File

@@ -21,6 +21,8 @@ struct RadarRawData {
uint8_t breathRate; // 呼吸 uint8_t breathRate; // 呼吸
uint8_t body; // 体动/能量值 uint8_t body; // 体动/能量值
uint16_t distance; // 距离 cm uint16_t distance; // 距离 cm
uint8_t sleepState; // 睡眠状态: 0=清醒 1=浅睡 2=深睡
uint8_t sleepScore; // 睡眠评分: 0~100
bool radarValid; // 雷达有效性 bool radarValid; // 雷达有效性
bool vitalValid; // 生命体征有效性 bool vitalValid; // 生命体征有效性
@@ -68,6 +70,7 @@ private:
void _extractPresence(const RadarFrame& frame); void _extractPresence(const RadarFrame& frame);
void _extractBreathing(const RadarFrame& frame); void _extractBreathing(const RadarFrame& frame);
void _extractHeartRate(const RadarFrame& frame); void _extractHeartRate(const RadarFrame& frame);
void _extractFall(const RadarFrame& frame); // AFD1 跌倒
void _extractSleep(const RadarFrame& frame); void _extractSleep(const RadarFrame& frame);
void _extractWorkStatus(const RadarFrame& frame); void _extractWorkStatus(const RadarFrame& frame);
void _extractDetectRange(const RadarFrame& frame); void _extractDetectRange(const RadarFrame& frame);
@@ -85,6 +88,13 @@ private:
RadarCallback _callback; RadarCallback _callback;
uint32_t _frameCount; uint32_t _frameCount;
uint32_t _errorCount; uint32_t _errorCount;
uint32_t _lastHeartBeat; // 上次心率数据时间
uint32_t _lastBreathBeat; // 上次呼吸数据时间
// 有人无人防抖 (PCB 信号不稳定时过滤跳变)
bool _stablePresence; // 防抖后的稳定有人/无人
uint8_t _presenceCnt; // 连续一致计数
bool _lastRawPresence; // 上一次原始值
}; };
#endif // RADAR_MANAGER_H #endif // RADAR_MANAGER_H

View File

@@ -33,9 +33,10 @@ enum RadarControl : uint8_t {
CTRL_WORK_STATUS = 0x05, // 工作状态 CTRL_WORK_STATUS = 0x05, // 工作状态
CTRL_DETECT_RANGE = 0x07, // 探测范围 CTRL_DETECT_RANGE = 0x07, // 探测范围
CTRL_PRESENCE = 0x80, // 人体存在 CTRL_PRESENCE = 0x80, // 人体存在
CTRL_BREATHING = 0x81, // 呼吸检测 CTRL_BREATHING = 0x81, // 呼吸检测 (仅 ABD1)
CTRL_SLEEP = 0x84, // 睡眠监测 CTRL_FALL = 0x83, // 跌倒检测 (仅 AFD1)
CTRL_HEART_RATE = 0x85, // 心率监测 CTRL_SLEEP = 0x84, // 睡眠监测 (仅 ABD1)
CTRL_HEART_RATE = 0x85, // 心率监测 (仅 ABD1)
}; };
// ============================================================ // ============================================================
@@ -73,6 +74,7 @@ enum SleepStatus : uint8_t {
SLEEP_AWAKE = 0, // 清醒 SLEEP_AWAKE = 0, // 清醒
SLEEP_LIGHT = 1, // 浅睡 SLEEP_LIGHT = 1, // 浅睡
SLEEP_DEEP = 2, // 深睡 SLEEP_DEEP = 2, // 深睡
SLEEP_ABSENT = 3, // 离床/无人/无效
}; };
// ============================================================ // ============================================================

View File

@@ -1,10 +1,10 @@
/** /**
* @file wifi_manager.h * @file wifi_manager.h
* @brief WiFi 配网管理器 * @brief WiFi 配网管理器 (AP + WebServer)
* *
* 功能: * 功能:
* - 自动连接已保存的 WiFi * - 自动连接已保存的 WiFi
* - 配网失败/首次启动 → AP 模式 + 强制门户页面 * - 配网失败/首次启动 → AP 模式 + Captive Portal
* - 自定义参数: MQTT 服务器/端口/用户/密码、设备名称、环境备注 * - 自定义参数: MQTT 服务器/端口/用户/密码、设备名称、环境备注
* - 长按按钮重置配置 * - 长按按钮重置配置
* - LED 状态指示 * - LED 状态指示
@@ -15,8 +15,14 @@
#define WIFI_MANAGER_H #define WIFI_MANAGER_H
#include <Arduino.h> #include <Arduino.h>
#include <WiFiManager.h> #include <WiFi.h>
#include <WebServer.h>
#include <DNSServer.h>
#include <LittleFS.h> #include <LittleFS.h>
#include <BLEDevice.h>
#include <BLEServer.h>
#include <BLEUtils.h>
#include <BLE2902.h>
#include <ArduinoJson.h> #include <ArduinoJson.h>
#include "config.h" #include "config.h"
@@ -27,7 +33,10 @@ struct DeviceConfig {
char mqtt_user[20]; char mqtt_user[20];
char mqtt_pass[20]; char mqtt_pass[20];
char device_id[20]; char device_id[20];
char env_note[20]; // 环境备注 (如: "主卧", "客厅", "老人房") char env_note[20]; // 环境备注 (房间名)
char env_type[16]; // 环境类型 bedroom/bathroom/...
char wifi_ssid[33]; // WiFi SSID
char wifi_pass[65]; // WiFi 密码
}; };
class WiFiMgmt { class WiFiMgmt {
@@ -38,95 +47,72 @@ public:
// 单例指针 (用于静态回调) // 单例指针 (用于静态回调)
static WiFiMgmt* _instance; static WiFiMgmt* _instance;
/**
* @brief 初始化 WiFi 管理器
* @return true=成功
*/
bool begin(); bool begin();
/**
* @brief 主循环, 处理配网流程和按钮检测
* @return true=WiFi 已连接
*/
bool loop(); bool loop();
/**
* @brief WiFi 是否已连接
*/
bool isConnected() const; bool isConnected() const;
bool justProvisioned(); // 刚配网成功 (读取后自动清除)
/**
* @brief 获取设备配置
*/
DeviceConfig getConfig() const; DeviceConfig getConfig() const;
/**
* @brief 获取设备 ID (用于 MQTT client_id)
*/
const char* getDeviceId() const; const char* getDeviceId() const;
/**
* @brief 获取环境备注
*/
const char* getEnvNote() const; const char* getEnvNote() const;
/**
* @brief 强制进入配网模式
*/
void startConfigPortal(); void startConfigPortal();
void handleBLEProvision(const char* json); // BLE 配网回调
/** void enqueueBLEJson(const char* json); // BLE 线程安全入队 (仅暂存)
* @brief 清除所有配置并重启
*/
void resetSettings(); void resetSettings();
private: private:
/** bool _connectWiFi(); // 仅尝试已保存凭据
* @brief 尝试连接 WiFi bool _tryConnect(const char* ssid, const char* pass); // 尝试连接指定 WiFi
*/
bool _connectWiFi();
/**
* @brief 设置 LED 状态
* @param mode 0=慢闪(未连接) 1=快闪(配网) 2=常亮(已连接)
*/
void _setLEDMode(uint8_t mode);
/**
* @brief 检测按钮长按
*/
bool _checkButtonLongPress();
/**
* @brief 加载/保存配置到 LittleFS
*/
bool _loadConfig(); bool _loadConfig();
bool _saveConfig(); bool _saveConfig();
void _setLEDMode(uint8_t mode);
static void _ledTaskFunc(void* param);
bool _checkButtonLongPress();
/** // Web 服务器
* @brief 保存 WiFi 参数回调 (WiFiManager 的回调) void _startPortal();
*/ void _stopPortal();
static void _saveParamsCallback(); void _handleRoot();
static void _apCallback(WiFiManager* wm); void _handleSave();
void _handleReset();
void _updateWifiScan(); // WiFi 扫描缓存
String _htmlPage(); // 返回配网 HTML 页面
WiFiManager _wm; // BLE 蓝牙配网
void _startBLE();
void _stopBLE();
WebServer* _server;
DNSServer* _dns;
DeviceConfig _config; DeviceConfig _config;
bool _connected; bool _connected;
bool _portalActive;
bool _portalStopFlag; // 延迟关闭标记
uint32_t _portalStartTime;
String _pendingSSID;
String _pendingPass;
bool _provisionSuccess; // 刚配网成功标记
uint32_t _lastCheckTime; uint32_t _lastCheckTime;
uint32_t _buttonPressTime; uint32_t _buttonPressTime;
bool _buttonPressed; bool _buttonPressed;
// LED 闪烁状态 // WiFi 扫描缓存 (避免每次打开页面都扫描3-8秒)
uint32_t _ledLastToggle; String _wifiScanCache;
bool _ledState; uint32_t _lastScanTime;
// WiFiManager 自定义参数 // LED 独立任务
WiFiManagerParameter* _param_mqtt_server; TaskHandle_t _ledTaskHandle;
WiFiManagerParameter* _param_mqtt_port; volatile uint8_t _ledMode; // 0=灭 1=慢闪 2=快闪 3=常亮
WiFiManagerParameter* _param_mqtt_user;
WiFiManagerParameter* _param_mqtt_pass; // BLE
WiFiManagerParameter* _param_device_id; BLEServer* _bleServer;
WiFiManagerParameter* _param_env_note; BLECharacteristic* _bleInfoChar; // READ: 设备信息
BLECharacteristic* _bleChar; // WRITE+NOTIFY: 配网
BLEAdvertising* _bleAdv;
// BLE 回调保护: 不在 BTC_TASK 栈里做 Flash 写操作
String _blePendingJson; // 待处理的 BLE 配网 JSON
bool _blePendingFlag; // 有未处理的 BLE 数据
void _processBLEPending(); // 在 loop() 上下文处理
}; };
#endif // WIFI_MANAGER_H #endif // WIFI_MANAGER_H

View File

@@ -1,262 +0,0 @@
/**
* wm_strings_zh.h
* 中文语言包 for WiFiManager
* 基于 wm_strings_en.h 翻译
*
* WiFiManager, a library for the ESP8266/Arduino platform
* for configuration of WiFi credentials using a Captive Portal
*
* @author Creator tzapu
* @author tablatronix
* @version 0.0.0
* @license MIT
*/
#ifndef _WM_STRINGS_ZH_H_
#define _WM_STRINGS_ZH_H_
// strings files must include a consts file!
#include "wm_consts_en.h" // include constants, tokens, routes
const char WM_LANGUAGE[] PROGMEM = "zh-CN"; // i18n lang code
const char HTTP_HEAD_START[] PROGMEM = "<!DOCTYPE html>"
"<html lang='zh-CN'><head>"
"<meta name='format-detection' content='telephone=no'>"
"<meta charset='UTF-8'>"
"<meta name='viewport' content='width=device-width,initial-scale=1,user-scalable=no'/>"
"<title>{v}</title>";
const char HTTP_SCRIPT[] PROGMEM = "<script>function c(l){"
"document.getElementById('s').value=l.getAttribute('data-ssid')||l.innerText||l.textContent;"
"p = l.nextElementSibling.classList.contains('l');"
"document.getElementById('p').disabled = !p;"
"if(p)document.getElementById('p').focus();};"
"function f() {var x = document.getElementById('p');x.type==='password'?x.type='text':x.type='password';}"
"</script>";
const char HTTP_HEAD_END[] PROGMEM = "</head><body class='{c}'><div class='wrap'>"; // {c} = _bodyclass
const char HTTP_ROOT_MAIN[] PROGMEM = "<h1>{t}</h1><h3>{v}</h3>";
const char * const HTTP_PORTAL_MENU[] PROGMEM = {
"<form action='/wifi' method='get'><button>配置 WiFi</button></form><br/>\n", // MENU_WIFI
"<form action='/0wifi' method='get'><button>配置 WiFi (不扫描)</button></form><br/>\n", // MENU_WIFINOSCAN
"<form action='/info' method='get'><button>系统信息</button></form><br/>\n", // MENU_INFO
"<form action='/param' method='get'><button>参数设置</button></form><br/>\n", // MENU_PARAM
"<form action='/close' method='get'><button>关闭页面</button></form><br/>\n", // MENU_CLOSE
"<form action='/restart' method='get'><button>重启设备</button></form><br/>\n", // MENU_RESTART
"<form action='/exit' method='get'><button>退出配网</button></form><br/>\n", // MENU_EXIT
"<form action='/erase' method='get'><button class='D'>清除配置</button></form><br/>\n", // MENU_ERASE
"<form action='/update' method='get'><button>固件升级</button></form><br/>\n", // MENU_UPDATE
"<hr><br/>" // MENU_SEP
};
const char HTTP_PORTAL_OPTIONS[] PROGMEM = "";
const char HTTP_ITEM_QI[] PROGMEM = "<div role='img' aria-label='{r}%' title='{r}%' class='q q-{q} {i} {h}'></div>";
const char HTTP_ITEM_QP[] PROGMEM = "<div class='q {h}'>{r}%</div>";
const char HTTP_ITEM[] PROGMEM = "<div><a href='#p' onclick='c(this)' data-ssid='{V}'>{v}</a>{qi}{qp}</div>";
const char HTTP_FORM_START[] PROGMEM = "<form method='POST' action='{v}'>";
const char HTTP_FORM_WIFI[] PROGMEM = "<label for='s'>WiFi 名称 (SSID)</label><input id='s' name='s' maxlength='32' autocorrect='off' autocapitalize='none' placeholder='{v}'><br/><label for='p'>WiFi 密码</label><input id='p' name='p' maxlength='64' type='password' placeholder='{p}'><input type='checkbox' id='showpass' onclick='f()'> <label for='showpass'>显示密码</label><br/>";
const char HTTP_FORM_WIFI_END[] PROGMEM = "";
const char HTTP_FORM_STATIC_HEAD[] PROGMEM = "<hr><br/>";
const char HTTP_FORM_END[] PROGMEM = "<br/><br/><button type='submit'>保存配置</button></form>";
const char HTTP_FORM_LABEL[] PROGMEM = "<label for='{i}'>{t}</label>";
const char HTTP_FORM_PARAM_HEAD[] PROGMEM = "<hr><br/>";
const char HTTP_FORM_PARAM[] PROGMEM = "<br/><input id='{i}' name='{n}' maxlength='{l}' value='{v}' {c}>\n";
const char HTTP_SCAN_LINK[] PROGMEM = "<br/><form action='/wifi?refresh=1' method='POST'><button name='refresh' value='1'>重新扫描</button></form>";
const char HTTP_SAVED[] PROGMEM = "<div class='msg'>正在保存配置<br/>尝试连接 WiFi...<br/>如果连接失败,请重新连接 AP 再试一次</div>";
const char HTTP_PARAMSAVED[] PROGMEM = "<div class='msg S'>配置已保存<br/></div>";
const char HTTP_END[] PROGMEM = "</div></body></html>";
const char HTTP_ERASEBTN[] PROGMEM = "<br/><form action='/erase' method='get'><button class='D'>清除 WiFi 配置</button></form>";
const char HTTP_UPDATEBTN[] PROGMEM = "<br/><form action='/update' method='get'><button>固件升级</button></form>";
const char HTTP_BACKBTN[] PROGMEM = "<hr><br/><form action='/' method='get'><button>返回首页</button></form>";
const char HTTP_STATUS_ON[] PROGMEM = "<div class='msg S'><strong>已连接</strong> {v}<br/><em><small>IP 地址: {i}</small></em></div>";
const char HTTP_STATUS_OFF[] PROGMEM = "<div class='msg {c}'><strong>未连接</strong> {v}{r}</div>";
const char HTTP_STATUS_OFFPW[] PROGMEM = "<br/>WiFi 密码错误";
const char HTTP_STATUS_OFFNOAP[] PROGMEM = "<br/>未找到该 WiFi";
const char HTTP_STATUS_OFFFAIL[] PROGMEM = "<br/>无法连接";
const char HTTP_STATUS_NONE[] PROGMEM = "<div class='msg'>未设置 WiFi</div>";
const char HTTP_BR[] PROGMEM = "<br/>";
const char HTTP_STYLE[] PROGMEM = "<style>"
".c,body{text-align:center;font-family:'PingFang SC','Microsoft YaHei','Helvetica Neue',sans-serif}div,input,select{padding:5px;font-size:1em;margin:5px 0;box-sizing:border-box}"
"input,button,select,.msg{border-radius:.3rem;width: 100%}input[type=radio],input[type=checkbox]{width:auto}"
"button,input[type='button'],input[type='submit']{cursor:pointer;border:0;background-color:#1fa3ec;color:#fff;line-height:2.4rem;font-size:1.2rem;width:100%}"
"input[type='file']{border:1px solid #1fa3ec}"
".wrap {text-align:left;display:inline-block;min-width:260px;max-width:500px}"
"a{color:#000;font-weight:700;text-decoration:none}a:hover{color:#1fa3ec;text-decoration:underline}"
".q{height:16px;margin:0;padding:0 5px;text-align:right;min-width:38px;float:right}.q.q-0:after{background-position-x:0}.q.q-1:after{background-position-x:-16px}.q.q-2:after{background-position-x:-32px}.q.q-3:after{background-position-x:-48px}.q.q-4:after{background-position-x:-64px}.q.l:before{background-position-x:-80px;padding-right:5px}.ql .q{float:left}.q:after,.q:before{content:'';width:16px;height:16px;display:inline-block;background-repeat:no-repeat;background-position: 16px 0;"
"background-image:url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGAAAAAQCAMAAADeZIrLAAAAJFBMVEX///8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADHJj5lAAAAC3RSTlMAIjN3iJmqu8zd7vF8pzcAAABsSURBVHja7Y1BCsAwCASNSVo3/v+/BUEiXnIoXkoX5jAQMxTHzK9cVSnvDxwD8bFx8PhZ9q8FmghXBhqA1faxk92PsxvRc2CCCFdhQCbRkLoAQ3q/wWUBqG35ZxtVzW4Ed6LngPyBU2CobdIDQ5oPWI5nCUwAAAAASUVORK5CYII=');}"
"@media (-webkit-min-device-pixel-ratio: 2),(min-resolution: 192dpi){.q:before,.q:after {"
"background-image:url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAALwAAAAgCAMAAACfM+KhAAAALVBMVEX///8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADAOrOgAAAADnRSTlMAESIzRGZ3iJmqu8zd7gKjCLQAAACmSURBVHgB7dDBCoMwEEXRmKlVY3L//3NLhyzqIqSUggy8uxnhCR5Mo8xLt+14aZ7wwgsvvPA/ofv9+44334UXXngvb6XsFhO/VoC2RsSv9J7x8BnYLW+AjT56ud/uePMdb7IP8Bsc/e7h8Cfk912ghsNXWPpDC4hvN+D1560A1QPORyh84VKLjjdvfPFm++i9EWq0348XXnjhhT+4dIbCW+WjZim9AKk4UZMnnCEuAAAAAElFTkSuQmCC');"
"background-size: 95px 16px;}}"
".msg{padding:20px;margin:20px 0;border:1px solid #eee;border-left-width:5px;border-left-color:#777}.msg h4{margin-top:0;margin-bottom:5px}.msg.P{border-left-color:#1fa3ec}.msg.P h4{color:#1fa3ec}.msg.D{border-left-color:#dc3630}.msg.D h4{color:#dc3630}.msg.S{border-left-color: #5cb85c}.msg.S h4{color: #5cb85c}"
"dt{font-weight:bold}dd{margin:0;padding:0 0 0.5em 0;min-height:12px}"
"td{vertical-align: top;}"
".h{display:none}"
"button{transition: 0s opacity;transition-delay: 3s;transition-duration: 0s;cursor: pointer}"
"button.D{background-color:#dc3630}"
"button:active{opacity:50% !important;cursor:wait;transition-delay: 0s}"
"body.invert,body.invert a,body.invert h1 {background-color:#060606;color:#fff;}"
"body.invert .msg{color:#fff;background-color:#282828;border-top:1px solid #555;border-right:1px solid #555;border-bottom:1px solid #555;}"
"body.invert .q[role=img]{-webkit-filter:invert(1);filter:invert(1);}"
":disabled {opacity: 0.5;}"
"</style>";
#ifndef WM_NOHELP
const char HTTP_HELP[] PROGMEM =
"<br/><h3>可用页面</h3><hr>"
"<table class='table'>"
"<thead><tr><th>页面</th><th>功能</th></tr></thead><tbody>"
"<tr><td><a href='/'>/</a></td>"
"<td>主菜单页面</td></tr>"
"<tr><td><a href='/wifi'>/wifi</a></td>"
"<td>显示 WiFi 扫描结果并配置连接 (/0wifi 不扫描)</td></tr>"
"<tr><td><a href='/wifisave'>/wifisave</a></td>"
"<td>保存 WiFi 配置并连接设备</td></tr>"
"<tr><td><a href='/param'>/param</a></td>"
"<td>参数设置页面</td></tr>"
"<tr><td><a href='/info'>/info</a></td>"
"<td>系统信息页面</td></tr>"
"<tr><td><a href='/u'>/u</a></td>"
"<td>固件 OTA 升级</td></tr>"
"<tr><td><a href='/close'>/close</a></td>"
"<td>关闭弹出窗口,配网服务保持运行</td></tr>"
"<tr><td>/exit</td>"
"<td>退出配网模式,配网服务将关闭</td></tr>"
"<tr><td>/restart</td>"
"<td>重启设备</td></tr>"
"<tr><td>/erase</td>"
"<td>清除 WiFi 配置并重启。清除后设备不会自动连接,需重新配网</td></tr>"
"</table>"
"<p/>GitHub <a href='https://github.com/tzapu/WiFiManager'>https://github.com/tzapu/WiFiManager</a>.";
#else
const char HTTP_HELP[] PROGMEM = "";
#endif
const char HTTP_UPDATE[] PROGMEM = "上传新固件<br/><form method='POST' action='u' enctype='multipart/form-data' onchange=\"(function(el){document.getElementById('uploadbin').style.display = el.value=='' ? 'none' : 'initial';})(this)\"><input type='file' name='update' accept='.bin,application/octet-stream'><button id='uploadbin' type='submit' class='h D'>开始升级</button></form><small><a href='http://192.168.4.1/update' target='_blank'>* 如果在配网页面内无法使用,请在浏览器中打开 http://192.168.4.1</a><small>";
const char HTTP_UPDATE_FAIL[] PROGMEM = "<div class='msg D'><strong>升级失败!</strong><Br/>请重启设备后重试</div>";
const char HTTP_UPDATE_SUCCESS[] PROGMEM = "<div class='msg S'><strong>升级成功!</strong><Br/>设备正在重启...</div>";
#ifdef WM_JSTEST
const char HTTP_JS[] PROGMEM =
"<script>function postAjax(url, data, success) {"
" var params = typeof data == 'string' ? data : Object.keys(data).map("
" function(k){ return encodeURIComponent(k) + '=' + encodeURIComponent(data[k]) }"
" ).join('&');"
" var xhr = window.XMLHttpRequest ? new XMLHttpRequest() : new ActiveXObject(\"Microsoft.XMLHTTP\");"
" xhr.open('POST', url);"
" xhr.onreadystatechange = function() {"
" if (xhr.readyState>3 && xhr.status==200) { success(xhr.responseText); }"
" };"
" xhr.setRequestHeader('X-Requested-With', 'XMLHttpRequest');"
" xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');"
" xhr.send(params);"
" return xhr;}"
"postAjax('/status', 'p1=1&p2=Hello+World', function(data){ console.log(data); });"
"postAjax('/status', { p1: 1, p2: 'Hello World' }, function(data){ console.log(data); });"
"</script>";
#endif
// Info html
#ifdef ESP32
const char HTTP_INFO_esphead[] PROGMEM = "<h3>ESP32 信息</h3><hr><dl>";
const char HTTP_INFO_chiprev[] PROGMEM = "<dt>芯片版本</dt><dd>{1}</dd>";
const char HTTP_INFO_lastreset[] PROGMEM = "<dt>上次复位原因</dt><dd>CPU0: {1}<br/>CPU1: {2}</dd>";
const char HTTP_INFO_aphost[] PROGMEM = "<dt>AP 主机名</dt><dd>{1}</dd>";
const char HTTP_INFO_psrsize[] PROGMEM = "<dt>PSRAM 大小</dt><dd>{1} 字节</dd>";
const char HTTP_INFO_temp[] PROGMEM = "<dt>芯片温度</dt><dd>{1}°C / {2}°F</dd>";
const char HTTP_INFO_hall[] PROGMEM = "<dt>霍尔传感器</dt><dd>{1}</dd>";
#else
const char HTTP_INFO_esphead[] PROGMEM = "<h3>ESP8266 信息</h3><hr><dl>";
const char HTTP_INFO_fchipid[] PROGMEM = "<dt>Flash 芯片 ID</dt><dd>{1}</dd>";
const char HTTP_INFO_corever[] PROGMEM = "<dt>Core 版本</dt><dd>{1}</dd>";
const char HTTP_INFO_bootver[] PROGMEM = "<dt>Boot 版本</dt><dd>{1}</dd>";
const char HTTP_INFO_lastreset[] PROGMEM = "<dt>上次复位原因</dt><dd>{1}</dd>";
const char HTTP_INFO_flashsize[] PROGMEM = "<dt>实际 Flash 大小</dt><dd>{1} 字节</dd>";
#endif
const char HTTP_INFO_memsmeter[] PROGMEM = "<br/><progress value='{1}' max='{2}'></progress></dd>";
const char HTTP_INFO_memsketch[] PROGMEM = "<dt>内存 - 程序大小</dt><dd>已用 / 总计<br/>{1} / {2} 字节";
const char HTTP_INFO_freeheap[] PROGMEM = "<dt>内存 - 可用堆</dt><dd>{1} 字节</dd>";
const char HTTP_INFO_wifihead[] PROGMEM = "<br/><h3>WiFi 信息</h3><hr>";
const char HTTP_INFO_uptime[] PROGMEM = "<dt>运行时间</dt><dd>{1} 分 {2} 秒</dd>";
const char HTTP_INFO_chipid[] PROGMEM = "<dt>芯片 ID</dt><dd>{1}</dd>";
const char HTTP_INFO_idesize[] PROGMEM = "<dt>Flash 大小</dt><dd>{1} 字节</dd>";
const char HTTP_INFO_sdkver[] PROGMEM = "<dt>SDK 版本</dt><dd>{1}</dd>";
const char HTTP_INFO_cpufreq[] PROGMEM = "<dt>CPU 频率</dt><dd>{1}MHz</dd>";
const char HTTP_INFO_apip[] PROGMEM = "<dt>AP IP 地址</dt><dd>{1}</dd>";
const char HTTP_INFO_apmac[] PROGMEM = "<dt>AP MAC 地址</dt><dd>{1}</dd>";
const char HTTP_INFO_apssid[] PROGMEM = "<dt>AP SSID</dt><dd>{1}</dd>";
const char HTTP_INFO_apbssid[] PROGMEM = "<dt>BSSID</dt><dd>{1}</dd>";
const char HTTP_INFO_stassid[] PROGMEM = "<dt>Station SSID</dt><dd>{1}</dd>";
const char HTTP_INFO_staip[] PROGMEM = "<dt>Station IP</dt><dd>{1}</dd>";
const char HTTP_INFO_stagw[] PROGMEM = "<dt>网关地址</dt><dd>{1}</dd>";
const char HTTP_INFO_stasub[] PROGMEM = "<dt>子网掩码</dt><dd>{1}</dd>";
const char HTTP_INFO_dnss[] PROGMEM = "<dt>DNS 服务器</dt><dd>{1}</dd>";
const char HTTP_INFO_host[] PROGMEM = "<dt>主机名</dt><dd>{1}</dd>";
const char HTTP_INFO_stamac[] PROGMEM = "<dt>Station MAC</dt><dd>{1}</dd>";
const char HTTP_INFO_conx[] PROGMEM = "<dt>连接状态</dt><dd>{1}</dd>";
const char HTTP_INFO_autoconx[] PROGMEM = "<dt>自动连接</dt><dd>{1}</dd>";
const char HTTP_INFO_aboutver[] PROGMEM = "<dt>WiFiManager 版本</dt><dd>{1}</dd>";
const char HTTP_INFO_aboutarduino[] PROGMEM = "<dt>Arduino 版本</dt><dd>{1}</dd>";
const char HTTP_INFO_aboutsdk[] PROGMEM = "<dt>ESP-SDK/IDF 版本</dt><dd>{1}</dd>";
const char HTTP_INFO_aboutdate[] PROGMEM = "<dt>构建日期</dt><dd>{1}</dd>";
// ============================================================
// 文本常量
// ============================================================
const char S_brand[] PROGMEM = "健康监测配网";
const char S_debugPrefix[] PROGMEM = "*wm:";
const char S_y[] PROGMEM = "";
const char S_n[] PROGMEM = "";
const char S_enable[] PROGMEM = "已启用";
const char S_disable[] PROGMEM = "已禁用";
const char S_GET[] PROGMEM = "GET";
const char S_POST[] PROGMEM = "POST";
const char S_NA[] PROGMEM = "未知";
const char S_passph[] PROGMEM = "********";
const char S_titlewifisaved[] PROGMEM = "WiFi 已保存";
const char S_titlewifisettings[] PROGMEM = "设置已保存";
const char S_titlewifi[] PROGMEM = "配置 WiFi";
const char S_titleinfo[] PROGMEM = "系统信息";
const char S_titleparam[] PROGMEM = "参数设置";
const char S_titleparamsaved[] PROGMEM = "设置已保存";
const char S_titleexit[] PROGMEM = "退出配网";
const char S_titlereset[] PROGMEM = "重置";
const char S_titleerase[] PROGMEM = "清除配置";
const char S_titleclose[] PROGMEM = "关闭页面";
const char S_options[] PROGMEM = "选项";
const char S_nonetworks[] PROGMEM = "未发现 WiFi 网络,请点击刷新重新扫描。";
const char S_staticip[] PROGMEM = "静态 IP";
const char S_staticgw[] PROGMEM = "静态网关";
const char S_staticdns[] PROGMEM = "静态 DNS";
const char S_subnet[] PROGMEM = "子网掩码";
const char S_exiting[] PROGMEM = "正在退出配网模式";
const char S_resetting[] PROGMEM = "设备将在几秒后重启";
const char S_closing[] PROGMEM = "可以关闭此页面,配网服务将继续运行";
const char S_error[] PROGMEM = "发生错误";
const char S_notfound[] PROGMEM = "页面未找到\n\n";
const char S_uri[] PROGMEM = "URI: ";
const char S_method[] PROGMEM = "\n请求方法: ";
const char S_args[] PROGMEM = "\n参数: ";
const char S_parampre[] PROGMEM = "param_";
// debug strings
const char D_HR[] PROGMEM = "--------------------";
// softap ssid default prefix
#ifdef ESP8266
const char S_ssidpre[] PROGMEM = "ESP";
#elif defined(ESP32)
const char S_ssidpre[] PROGMEM = "ESP32";
#else
const char S_ssidpre[] PROGMEM = "WM";
#endif
#endif

View File

@@ -9,6 +9,8 @@ framework = arduino
; 串口监视器 ; 串口监视器
monitor_speed = 115200 monitor_speed = 115200
monitor_filters = direct monitor_filters = direct
monitor_rts = 0
monitor_dtr = 0
; Flash 配置 ; Flash 配置
board_build.flash_mode = qio board_build.flash_mode = qio
@@ -25,9 +27,6 @@ build_flags =
; 库依赖 ; 库依赖
lib_deps = lib_deps =
; WiFi 配网 (自带 captive portal)
tzapu/WiFiManager@^2.0.17
; MQTT 客户端 ; MQTT 客户端
knolleary/PubSubClient@^2.8 knolleary/PubSubClient@^2.8
@@ -43,4 +42,4 @@ lib_deps =
; 上传配置 ; 上传配置
upload_speed = 921600 upload_speed = 921600
upload_port = COM15 upload_port = COM19

View File

@@ -1,13 +1,43 @@
/** /**
* @file alarm_manager.cpp * @file alarm_manager.cpp
* @brief 报警管理器 - 双模式实现 * @brief 报警管理器 - 多类型分优先级实现
* *
* 本地: 跌倒/水浸/心率异常/呼吸异常 → 立即蜂鸣器+LED * 蜂鸣模式 (人身安全 > 环境 > 设备):
* 服务端: cmd/alarm, cmd/alarm/restore → 执行联动 + 回复响应 * 跌倒: 连速哔 (60ms/60ms) — 最急 ★★★★★
* 心率/呼吸: 5连急哔 (80ms/80ms) — 次急 ★★★★★
* 水浸: 3连哔 (120ms/100ms) — 中等 ★★★
* 温湿度: 2声长停 (200ms/400ms) — 温和 ★★
* 传感器: 单声长哔 (300ms/3s) — 提示 ★
*/ */
#include "alarm_manager.h" #include "alarm_manager.h"
// ============================================================
// 优先级定义 — 人身安全 > 环境安全 > 设备故障
// ============================================================
uint8_t AlarmManager::_priority(AlarmType t) {
switch (t) {
// ★★★★★ 人身安全 — 直接威胁生命
case ALARM_FALL: return 0;
case ALARM_HEART_ABNORMAL: return 1;
case ALARM_BREATH_ABNORMAL: return 1;
// ★★★ 环境安全 — 潜在危险
case ALARM_WATER_LEAK: return 2;
// ★★ 环境舒适 — 非紧急
case ALARM_TEMP_ABNORMAL: return 3;
case ALARM_HUMI_ABNORMAL: return 3;
// ★ 设备状态 — 仅提示
case ALARM_SENSOR_FAULT: return 4;
case ALARM_SERVER: return 5;
default: return 9;
}
}
// ============================================================
// 构造 & 析构
// ============================================================
AlarmManager::AlarmManager() AlarmManager::AlarmManager()
: _buzzerOn(false) : _buzzerOn(false)
, _ledOn(false) , _ledOn(false)
@@ -15,10 +45,16 @@ AlarmManager::AlarmManager()
, _buzzerToggleTime(0) , _buzzerToggleTime(0)
, _buzzerBeepState(false) , _buzzerBeepState(false)
, _beepCount(0) , _beepCount(0)
, _beepsPerCycle(3)
, _beepOnMs(150)
, _beepOffMs(100)
, _cyclePauseMs(600)
, _activeCount(0) , _activeCount(0)
, _lastLEDToggle(0) , _lastLEDToggle(0)
, _alarmCb(nullptr)
{ {
for (int i = 0; i < 4; i++) _activeAlarms[i] = ALARM_NONE; for (uint8_t i = 0; i < MAX_ACTIVE; i++) _activeAlarms[i] = ALARM_NONE;
memset(_activeDetail, 0, sizeof(_activeDetail));
} }
AlarmManager::~AlarmManager() {} AlarmManager::~AlarmManager() {}
@@ -28,18 +64,14 @@ AlarmManager::~AlarmManager() {}
// ============================================================ // ============================================================
bool AlarmManager::begin() { bool AlarmManager::begin() {
ledcSetup(BUZZER_PWM_CHANNEL, BUZZER_PWM_FREQ, BUZZER_PWM_RESOLUTION); // 有源蜂鸣器: 用 digitalWrite, 不用 PWM
ledcAttachPin(BUZZER_PIN, BUZZER_PWM_CHANNEL); pinMode(BUZZER_PIN, OUTPUT);
ledcWrite(BUZZER_PWM_CHANNEL, 0); // 关 digitalWrite(BUZZER_PIN, LOW);
pinMode(LED_ALARM_PIN, OUTPUT); pinMode(LED_ALARM_PIN, OUTPUT);
digitalWrite(LED_ALARM_PIN, LOW); digitalWrite(LED_ALARM_PIN, LOW);
_buzzerToggleTime = 0; Serial.println(F("[告警] ✅ 多类型分优先级报警就绪 (7种类型)"));
_buzzerBeepState = false;
_beepCount = 0;
Serial.println(F("[告警] ✅ 双模式报警 (本地+服务端)"));
return true; return true;
} }
@@ -53,33 +85,129 @@ void AlarmManager::loop() {
} }
// ============================================================ // ============================================================
// 本地报警触发 (离线可用) // 报警名称映射
// ============================================================ // ============================================================
void AlarmManager::triggerLocal(AlarmType type) { const char* AlarmManager::getAlarmName(AlarmType type) const {
switch (type) {
case ALARM_FALL: return "跌倒";
case ALARM_WATER_LEAK: return "水浸";
case ALARM_HEART_ABNORMAL: return "心率异常";
case ALARM_BREATH_ABNORMAL: return "呼吸异常";
case ALARM_TEMP_ABNORMAL: return "温度异常";
case ALARM_HUMI_ABNORMAL: return "湿度异常";
case ALARM_SENSOR_FAULT: return "传感器故障";
case ALARM_SERVER: return "云端指令";
default: return "未知";
}
}
// ============================================================
// 最高优先级报警
// ============================================================
AlarmManager::AlarmType AlarmManager::getHighestAlarm() const {
if (_activeCount == 0) return ALARM_NONE;
AlarmType best = ALARM_NONE;
uint8_t bestP = 99;
for (uint8_t i = 0; i < _activeCount; i++) {
uint8_t p = _priority(_activeAlarms[i]);
if (p < bestP) { bestP = p; best = _activeAlarms[i]; }
}
return best;
}
const char* AlarmManager::getActiveAlarmStr() const {
AlarmType t = getHighestAlarm();
if (t == ALARM_NONE) return "";
return getAlarmName(t);
}
// ============================================================
// 蜂鸣参数表 (按最高优先级报警驱动)
// ============================================================
uint32_t AlarmManager::_buzzerIntervalFor(AlarmType type) {
switch (type) {
case ALARM_FALL: return 200; // LED 快闪
case ALARM_WATER_LEAK: return 300;
case ALARM_HEART_ABNORMAL: return 500;
case ALARM_BREATH_ABNORMAL: return 500;
case ALARM_TEMP_ABNORMAL: return 800;
case ALARM_HUMI_ABNORMAL: return 800;
case ALARM_SENSOR_FAULT: return 1000;
default: return 500;
}
}
// ============================================================
// 更新蜂鸣参数 (根据当前最高优先级报警)
// ============================================================
static void _calcBuzzerParams(AlarmManager::AlarmType t,
uint8_t& beeps, uint32_t& onMs,
uint32_t& offMs, uint32_t& pauseMs) {
switch (t) {
// 人身安全 — 连速急哔
case AlarmManager::ALARM_FALL:
beeps = 0; onMs = 200; offMs = 200; pauseMs = 0; // 跌倒: 急促哔哔
break;
case AlarmManager::ALARM_HEART_ABNORMAL:
case AlarmManager::ALARM_BREATH_ABNORMAL:
beeps = 5; onMs = 200; offMs = 150; pauseMs = 500; // 心率/呼吸: 5连哔
break;
// 环境安全
case AlarmManager::ALARM_WATER_LEAK:
beeps = 3; onMs = 250; offMs = 200; pauseMs = 600; // 水浸: 3连哔
break;
// 环境舒适
case AlarmManager::ALARM_TEMP_ABNORMAL:
case AlarmManager::ALARM_HUMI_ABNORMAL:
beeps = 2; onMs = 300; offMs = 500; pauseMs = 1500; // 温湿度: 2声长停
break;
// 设备状态
case AlarmManager::ALARM_SENSOR_FAULT:
beeps = 1; onMs = 500; offMs = 3000; pauseMs = 0; // 传感器: 单声提醒
break;
default:
beeps = 0; onMs = 500; offMs = 500; pauseMs = 0; // 持续
break;
}
}
// ============================================================
// 本地报警触发
// ============================================================
void AlarmManager::triggerLocal(AlarmType type, const char* detail) {
// 去重 // 去重
for (uint8_t i = 0; i < _activeCount; i++) { for (uint8_t i = 0; i < _activeCount; i++) {
if (_activeAlarms[i] == type) return; if (_activeAlarms[i] == type) return;
} }
if (_activeCount < 4) { if (_activeCount < MAX_ACTIVE) {
_activeAlarms[_activeCount++] = type; _activeAlarms[_activeCount++] = type;
} }
const char* name = ""; const char* name = getAlarmName(type);
switch (type) { Serial.printf("[告警] 🚨 触发: %s (活动:%d)", name, _activeCount);
case ALARM_FALL: name = "跌倒"; break; if (detail && detail[0]) Serial.printf(" | %s", detail);
case ALARM_WATER_LEAK: name = "水浸"; break; Serial.println();
case ALARM_HEART_ABNORMAL: name = "心率异常"; break;
case ALARM_BREATH_ABNORMAL: name = "呼吸异常"; break; // 更新蜂鸣参数 (按最高优先级)
default: name = "未知"; break; AlarmType highest = getHighestAlarm();
_calcBuzzerParams(highest, _beepsPerCycle, _beepOnMs, _beepOffMs, _cyclePauseMs);
// 保存详情
if (detail) {
strncpy(_activeDetail, detail, sizeof(_activeDetail) - 1);
} }
Serial.printf("[告警] 🚨 本地报警触发: %s (活动报警:%d)\n", name, _activeCount);
// 立即开启蜂鸣器和 LED
_setBuzzer(true); _setBuzzer(true);
_setLED(true); _setLED(true);
// 通知外部
if (_alarmCb) _alarmCb((uint8_t)type, true, detail ? detail : name);
} }
void AlarmManager::clearLocal(AlarmType type) { void AlarmManager::clearLocal(AlarmType type) {
@@ -94,21 +222,29 @@ void AlarmManager::clearLocal(AlarmType type) {
} }
} }
Serial.printf("[告警] 本地报警清除, 剩余活动:%d\n", _activeCount); const char* name = getAlarmName(type);
Serial.printf("[告警] ✅ 清除: %s (剩余:%d)\n", name, _activeCount);
// 没有活动报警时关闭
if (_activeCount == 0) { if (_activeCount == 0) {
_setBuzzer(false); _setBuzzer(false);
_setLED(false); _setLED(false);
_activeDetail[0] = '\0';
} else {
// 降级到次高优先级报警的蜂鸣参数
AlarmType highest = getHighestAlarm();
_calcBuzzerParams(highest, _beepsPerCycle, _beepOnMs, _beepOffMs, _cyclePauseMs);
} }
if (_alarmCb) _alarmCb((uint8_t)type, false, name);
} }
void AlarmManager::clearAllLocal() { void AlarmManager::clearAllLocal() {
_activeCount = 0; _activeCount = 0;
for (int i = 0; i < 4; i++) _activeAlarms[i] = ALARM_NONE; for (uint8_t i = 0; i < MAX_ACTIVE; i++) _activeAlarms[i] = ALARM_NONE;
_activeDetail[0] = '\0';
_setBuzzer(false); _setBuzzer(false);
_setLED(false); _setLED(false);
Serial.println(F("[告警] 清除所有本地报警")); Serial.println(F("[告警] 清除所有报警"));
} }
// ============================================================ // ============================================================
@@ -116,16 +252,15 @@ void AlarmManager::clearAllLocal() {
// ============================================================ // ============================================================
void AlarmManager::executeAlarm(const AlarmCommand& cmd) { void AlarmManager::executeAlarm(const AlarmCommand& cmd) {
Serial.printf("[告警] ☁️ 服务端告警: %s (等级=%s, 蜂鸣=%d)\n", Serial.printf("[告警] ☁️ 服务端: %s (等级=%s 蜂鸣=%d)\n",
cmd.alarmType, cmd.alarmLevel, cmd.buzzerOn); cmd.alarmType, cmd.alarmLevel, cmd.buzzerOn);
// 映射 alarmType 到本地类型
AlarmType localType = ALARM_SERVER; AlarmType localType = ALARM_SERVER;
if (strcmp(cmd.alarmType, "fall") == 0) localType = ALARM_FALL; if (strcmp(cmd.alarmType, "fall") == 0) localType = ALARM_FALL;
else if (strcmp(cmd.alarmType, "water_leak") == 0) localType = ALARM_WATER_LEAK; else if (strcmp(cmd.alarmType, "water_leak") == 0) localType = ALARM_WATER_LEAK;
if (cmd.buzzerOn) { if (cmd.buzzerOn) {
triggerLocal(localType); triggerLocal(localType, cmd.alarmDesc);
if (cmd.buzzerDuration > 0) { if (cmd.buzzerDuration > 0) {
_buzzerOffTime = millis() + (uint32_t)cmd.buzzerDuration * 1000; _buzzerOffTime = millis() + (uint32_t)cmd.buzzerDuration * 1000;
} else { } else {
@@ -138,25 +273,22 @@ void AlarmManager::executeAlarm(const AlarmCommand& cmd) {
} }
} }
// ============================================================
// 服务端恢复指令 (§5.2)
// ============================================================
void AlarmManager::executeRestore(const AlarmCommand& cmd) { void AlarmManager::executeRestore(const AlarmCommand& cmd) {
Serial.printf("[告警] ☁️ 服务端恢复: %s\n", cmd.alarmType); Serial.printf("[告警] ☁️ 恢复: %s\n", cmd.alarmType);
AlarmType localType = ALARM_SERVER; AlarmType localType = ALARM_SERVER;
if (strcmp(cmd.alarmType, "fall") == 0) localType = ALARM_FALL; if (strcmp(cmd.alarmType, "fall") == 0) localType = ALARM_FALL;
else if (strcmp(cmd.alarmType, "water_leak") == 0) localType = ALARM_WATER_LEAK; else if (strcmp(cmd.alarmType, "water_leak") == 0) localType = ALARM_WATER_LEAK;
else if (strcmp(cmd.alarmType, "heart_abnormal") == 0) localType = ALARM_HEART_ABNORMAL;
else if (strcmp(cmd.alarmType, "breath_abnormal") == 0) localType = ALARM_BREATH_ABNORMAL;
else if (strcmp(cmd.alarmType, "temp_abnormal") == 0) localType = ALARM_TEMP_ABNORMAL;
else if (strcmp(cmd.alarmType, "humi_abnormal") == 0) localType = ALARM_HUMI_ABNORMAL;
else if (strcmp(cmd.alarmType, "sensor_fault") == 0) localType = ALARM_SENSOR_FAULT;
clearLocal(localType); clearLocal(localType);
if (!cmd.buzzerOn) { if (!cmd.buzzerOn) _setBuzzer(false);
_setBuzzer(false); if (!cmd.nightLight) _setLED(false);
}
if (!cmd.nightLight) {
_setLED(false);
}
} }
// ============================================================ // ============================================================
@@ -164,27 +296,11 @@ void AlarmManager::executeRestore(const AlarmCommand& cmd) {
// ============================================================ // ============================================================
void AlarmManager::buzzerTest(uint32_t durationSec) { void AlarmManager::buzzerTest(uint32_t durationSec) {
Serial.printf("[告警] 🔔 蜂鸣器测试 %d\n", durationSec); Serial.printf("[告警] 🔔 蜂鸣器测试 %ds\n", durationSec);
_setBuzzer(true); _setBuzzer(true);
_buzzerOffTime = millis() + durationSec * 1000; _buzzerOffTime = millis() + durationSec * 1000;
} }
// ============================================================
// 状态查询
// ============================================================
const char* AlarmManager::getActiveAlarmType() const {
if (_activeCount == 0) return "";
switch (_activeAlarms[0]) {
case ALARM_FALL: return "fall";
case ALARM_WATER_LEAK: return "water_leak";
case ALARM_HEART_ABNORMAL: return "heart_abnormal";
case ALARM_BREATH_ABNORMAL: return "breath_abnormal";
case ALARM_SERVER: return "server";
default: return "";
}
}
// ============================================================ // ============================================================
// 内部控制 // 内部控制
// ============================================================ // ============================================================
@@ -195,9 +311,9 @@ void AlarmManager::_setBuzzer(bool on) {
_buzzerToggleTime = 0; _buzzerToggleTime = 0;
_beepCount = 0; _beepCount = 0;
if (on) { if (on) {
_buzzerOffTime = 0; // 本地报警=无限哔哔 _buzzerOffTime = 0;
} else { } else {
ledcWrite(BUZZER_PWM_CHANNEL, 0); digitalWrite(BUZZER_PIN, LOW);
_buzzerOffTime = 0; _buzzerOffTime = 0;
} }
} }
@@ -207,57 +323,76 @@ void AlarmManager::_setLED(bool on) {
if (!on) digitalWrite(LED_ALARM_PIN, LOW); if (!on) digitalWrite(LED_ALARM_PIN, LOW);
} }
// ============================================================
// 蜂鸣器更新 (优先级驱动模式)
// ============================================================
void AlarmManager::_updateBuzzer() { void AlarmManager::_updateBuzzer() {
// 定时关闭检查 if (!_buzzerOn) return;
if (_buzzerOn && _buzzerOffTime > 0 && millis() >= _buzzerOffTime) {
// 定时关闭
if (_buzzerOffTime > 0 && millis() >= _buzzerOffTime) {
_buzzerOn = false; _buzzerOn = false;
ledcWrite(BUZZER_PWM_CHANNEL, 0); digitalWrite(BUZZER_PIN, LOW);
Serial.println(F("[告警] ⏰ 蜂鸣器定时关闭")); Serial.println(F("[告警] ⏰ 蜂鸣器定时关闭"));
return; return;
} }
// 本地报警: 三连哔 停 — 循环 (哔哔哔...哔哔哔...)
if (_buzzerOn && _buzzerOffTime == 0) {
uint32_t now = millis(); uint32_t now = millis();
// 周期: 3声哔 (每声150ms响+100ms停) + 600ms大停顿
// 云端定时: 哔哔声 (500ms响/500ms停), 不是长鸣
if (_buzzerOffTime > 0) {
if (now - _buzzerToggleTime >= 500) {
_buzzerToggleTime = now;
_buzzerBeepState = !_buzzerBeepState;
digitalWrite(BUZZER_PIN, _buzzerBeepState ? HIGH : LOW);
}
return;
}
// 本地报警: 按优先级模式 — 哔声/停顿交替
if (_beepsPerCycle == 0) {
// 连速哔 (跌倒): 简单翻转
if (now - _buzzerToggleTime >= (_buzzerBeepState ? _beepOnMs : _beepOffMs)) {
_buzzerToggleTime = now;
_buzzerBeepState = !_buzzerBeepState;
digitalWrite(BUZZER_PIN, _buzzerBeepState ? HIGH : LOW);
}
} else {
// 多声一组: N声哔 + 长停顿
if (!_buzzerBeepState) { if (!_buzzerBeepState) {
// 停顿中,检查是否该下一声 // 停顿中
uint32_t pause = (_beepCount >= 3) ? 600 : 100; uint32_t pause = (_beepCount >= _beepsPerCycle) ? _cyclePauseMs : _beepOffMs;
if (now - _buzzerToggleTime >= pause) { if (now - _buzzerToggleTime >= pause) {
_buzzerToggleTime = now; _buzzerToggleTime = now;
if (_beepCount >= 3) _beepCount = 0; // 重置周期 if (_beepCount >= _beepsPerCycle) _beepCount = 0;
_buzzerBeepState = true; _buzzerBeepState = true;
ledcWrite(BUZZER_PWM_CHANNEL, 128); digitalWrite(BUZZER_PIN, HIGH);
_beepCount++; _beepCount++;
} }
} else { } else {
// 在响中150ms后停下 // 响声中
if (now - _buzzerToggleTime >= 150) { if (now - _buzzerToggleTime >= _beepOnMs) {
_buzzerToggleTime = now; _buzzerToggleTime = now;
_buzzerBeepState = false; _buzzerBeepState = false;
ledcWrite(BUZZER_PWM_CHANNEL, 0); digitalWrite(BUZZER_PIN, LOW);
} }
} }
} }
// 云端定时: 持续响
else if (_buzzerOn && _buzzerOffTime > 0) {
ledcWrite(BUZZER_PWM_CHANNEL, 128);
}
} }
// ============================================================
// LED 更新 (按最高优先级决定闪烁频率)
// ============================================================
void AlarmManager::_updateLED() { void AlarmManager::_updateLED() {
if (!_ledOn) { if (!_ledOn) {
digitalWrite(LED_ALARM_PIN, LOW); digitalWrite(LED_ALARM_PIN, LOW);
return; return;
} }
// 根据告警类型调整闪烁频率 AlarmType highest = getHighestAlarm();
uint32_t interval = 500; uint32_t interval = _buzzerIntervalFor(highest);
for (uint8_t i = 0; i < _activeCount; i++) {
if (_activeAlarms[i] == ALARM_FALL) {
interval = 200; // 跌倒快闪
break;
}
}
if (millis() - _lastLEDToggle >= interval) { if (millis() - _lastLEDToggle >= interval) {
_lastLEDToggle = millis(); _lastLEDToggle = millis();

View File

@@ -1,6 +1,12 @@
/** /**
* @file button_manager.cpp * @file button_manager.cpp
* @brief 三按钮管理器实现 - 去抖 + 长按/短按 * @brief 三按钮管理器实现 - 双向去抖 + 长按/短按 + 超时自愈
*
* 核心改进 (v2):
* 1. 按下沿和释放沿都经过 BTN_DEBOUNCE_MS 稳定确认
* 2. 长按期间持续检测 (不再等到松手才触发)
* 3. 按下超过 BTN_STUCK_TIMEOUT_MS 自动复位, 防止状态机卡死
* 4. 用 stableState 替代瞬时 raw 驱动状态, 消除噪声误触发
*/ */
#include "button_manager.h" #include "button_manager.h"
@@ -14,9 +20,19 @@ ButtonManager::ButtonManager()
, _cbDismiss(nullptr) , _cbDismiss(nullptr)
, _cbPage(nullptr) , _cbPage(nullptr)
{ {
_btnProvision = { BTN_PROVISION_PIN, false, 0, false }; auto initBtn = [](ButtonState& s, uint8_t pin) {
_btnDismiss = { BTN_ALARM_DISMISS_PIN, false, 0, false }; s.pin = pin;
_btnPage = { BTN_PAGE_SWITCH_PIN, false, 0, false }; s.stableState = false;
s.rawState = false;
s.changeTime = 0;
s.pressTime = 0;
s.longTriggered = false;
s.shortPending = false;
};
initBtn(_btnProvision, BTN_PROVISION_PIN);
initBtn(_btnDismiss, BTN_ALARM_DISMISS_PIN);
initBtn(_btnPage, BTN_PAGE_SWITCH_PIN);
} }
// ============================================================ // ============================================================
@@ -28,9 +44,19 @@ void ButtonManager::begin() {
pinMode(BTN_ALARM_DISMISS_PIN, INPUT_PULLUP); pinMode(BTN_ALARM_DISMISS_PIN, INPUT_PULLUP);
pinMode(BTN_PAGE_SWITCH_PIN, INPUT_PULLUP); pinMode(BTN_PAGE_SWITCH_PIN, INPUT_PULLUP);
Serial.println(F("[按钮] ✅ 三按钮初始化完成")); // 强制初始化为未按下 (PCB 上电 GPIO 不稳, 忽略初始状态)
_btnProvision.rawState = false;
_btnProvision.stableState = false;
_btnDismiss.rawState = false;
_btnDismiss.stableState = false;
_btnPage.rawState = false;
_btnPage.stableState = false;
Serial.println(F("[按钮] ✅ 三按钮初始化完成 (双向去抖+超时自愈)"));
Serial.printf(" 配网: GPIO%d | 解除报警: GPIO%d | 翻页: GPIO%d\n", Serial.printf(" 配网: GPIO%d | 解除报警: GPIO%d | 翻页: GPIO%d\n",
BTN_PROVISION_PIN, BTN_ALARM_DISMISS_PIN, BTN_PAGE_SWITCH_PIN); BTN_PROVISION_PIN, BTN_ALARM_DISMISS_PIN, BTN_PAGE_SWITCH_PIN);
Serial.printf(" 去抖:%dms | 长按:%dms | 卡死超时:%dms\n",
BTN_DEBOUNCE_MS, BTN_LONG_PRESS_MS, BTN_STUCK_TIMEOUT_MS);
} }
// ============================================================ // ============================================================
@@ -38,6 +64,9 @@ void ButtonManager::begin() {
// ============================================================ // ============================================================
void ButtonManager::loop() { void ButtonManager::loop() {
// 开机 10 秒静默 (PCB 上电 GPIO 不稳 + 强制初始未按下)
if (millis() < 10000) return;
// 按钮1: 配网 (仅长按) // 按钮1: 配网 (仅长按)
_processButton(_btnProvision, false, true); _processButton(_btnProvision, false, true);
@@ -49,40 +78,91 @@ void ButtonManager::loop() {
} }
// ============================================================ // ============================================================
// 单按钮状态机 // 单按钮状态机 (基于稳定状态, 非瞬时边沿)
// ============================================================ // ============================================================
void ButtonManager::_processButton(ButtonState& btn, bool hasShort, bool hasLong) { void ButtonManager::_processButton(ButtonState& btn, bool hasShort, bool hasLong) {
bool raw = _readPin(btn.pin); // LOW = 按下 uint32_t now = millis();
bool raw = _readPin(btn.pin); // true = 按下 (LOW)
if (raw && !btn.pressed) { // ── 第1步: raw 状态变化时启动去抖计时 ──
// 按下沿 if (raw != btn.rawState) {
btn.pressed = true; btn.rawState = raw;
btn.pressTime = millis(); btn.changeTime = now;
// 不立即改变 stableState — 等稳定确认
}
// ── 第2步: 去抖确认 — raw 保持稳定超过 BTN_DEBOUNCE_MS 才生效 ──
bool debounced = (now - btn.changeTime >= BTN_DEBOUNCE_MS);
bool stateChanged = (btn.rawState != btn.stableState);
if (debounced && stateChanged) {
// 状态正式切换
btn.stableState = btn.rawState;
if (btn.stableState) {
// ── 确认按下 ──
btn.pressTime = now;
btn.longTriggered = false;
btn.shortPending = hasShort; // 只有支持短按的按钮才标记待判定
} else {
// ── 确认释放 ──
uint32_t holdTime = now - btn.pressTime;
// 短按判定: 按下时长在去抖和长按阈值之间
if (btn.shortPending && holdTime >= BTN_DEBOUNCE_MS
&& holdTime < BTN_LONG_PRESS_MS && !btn.longTriggered) {
// 根据引脚分发短按回调
if (btn.pin == BTN_ALARM_DISMISS_PIN && _cbDismiss) {
_cbDismiss();
}
if (btn.pin == BTN_PAGE_SWITCH_PIN && _cbPage) {
_cbPage();
}
}
btn.shortPending = false;
btn.longTriggered = false; btn.longTriggered = false;
} }
else if (!raw && btn.pressed) {
// 松开沿
btn.pressed = false;
uint32_t holdTime = millis() - btn.pressTime;
if (holdTime < BTN_DEBOUNCE_MS) {
return; // 抖动脉冲,忽略
} }
if (hasLong && holdTime >= BTN_LONG_PRESS_MS && !btn.longTriggered) { // ── 第3步: 长按检测 (在按下稳定期间持续检查) ──
// 长按触发 if (hasLong && btn.stableState && !btn.longTriggered) {
uint32_t holdTime = now - btn.pressTime;
if (holdTime >= BTN_LONG_PRESS_MS) {
btn.longTriggered = true; btn.longTriggered = true;
if (_cbProvision) _cbProvision(); btn.shortPending = false; // 长按已触发, 不再判定短按
if (_cbProvision) {
_cbProvision();
} }
else if (hasShort && holdTime >= BTN_DEBOUNCE_MS && holdTime < BTN_LONG_PRESS_MS) {
// 短按触发
if (btn.pin == BTN_ALARM_DISMISS_PIN && _cbDismiss) _cbDismiss();
if (btn.pin == BTN_PAGE_SWITCH_PIN && _cbPage) _cbPage();
} }
} }
// ── 第4步: 卡死自愈 — 按下超时强制复位 ──
if (btn.stableState && (now - btn.pressTime >= BTN_STUCK_TIMEOUT_MS)) {
Serial.printf("[按钮] ⚠️ GPIO%d 按下超时 %lums, 强制复位\n",
btn.pin, now - btn.pressTime);
_resetButton(btn);
}
} }
// ============================================================
// 强制复位
// ============================================================
void ButtonManager::_resetButton(ButtonState& btn) {
btn.stableState = false;
btn.rawState = _readPin(btn.pin); // 重新同步真实硬件状态
btn.changeTime = 0;
btn.pressTime = 0;
btn.longTriggered = false;
btn.shortPending = false;
}
// ============================================================
// 读引脚 (INPUT_PULLUP → LOW = 按下)
// ============================================================
bool ButtonManager::_readPin(uint8_t pin) { bool ButtonManager::_readPin(uint8_t pin) {
return digitalRead(pin) == LOW; // INPUT_PULLUP: LOW = 按下 return digitalRead(pin) == LOW;
} }

View File

@@ -71,37 +71,63 @@ void DisplayManager::nextPage() {
// 主更新入口 // 主更新入口
// ============================================================ // ============================================================
void DisplayManager::update(const TelemetryData& data, bool wifiOk, bool buzzerOn) { void DisplayManager::update(const TelemetryData& data, bool wifiOk, bool buzzerOn,
bool hasAlarm, const char* alarmName) {
if (!_initialized) return; if (!_initialized) return;
// 每 5 秒刷新一次 (防止 OLED 烧屏,也避免 SPI 占用过多) // 有报警时每秒刷新 (闪烁提示),否则 5 秒
if (millis() - _lastDrawTime < 5000) return; uint32_t interval = hasAlarm ? 1000 : 5000;
if (millis() - _lastDrawTime < interval) return;
_lastDrawTime = millis(); _lastDrawTime = millis();
_u8g2.clearBuffer(); _u8g2.clearBuffer();
// 活跃报警 — 所有页面顶部闪烁提示
if (hasAlarm && alarmName && alarmName[0]) {
_drawAlarmBanner(alarmName);
}
switch (_pageIndex) { switch (_pageIndex) {
case 0: _drawPage0(data, wifiOk, buzzerOn); break; case 0: _drawPage0(data, wifiOk, buzzerOn, hasAlarm); break;
case 1: _drawPage1(data); break; case 1: _drawPage1(data, hasAlarm); break;
case 2: _drawPage2(data); break; case 2: _drawPage2(data, hasAlarm); break;
case 3: _drawPage3(data, buzzerOn, wifiOk); break; case 3: _drawPage3(data, buzzerOn, wifiOk, hasAlarm); break;
default: _drawPage0(data, wifiOk, buzzerOn); break; default: _drawPage0(data, wifiOk, buzzerOn, hasAlarm); break;
} }
_u8g2.sendBuffer(); _u8g2.sendBuffer();
} }
// ============================================================
// 报警横幅 (顶部闪烁)
// ============================================================
void DisplayManager::_drawAlarmBanner(const char* name) {
// 每秒翻转,实现闪烁效果
bool blink = ((millis() / 1000) % 2 == 0);
_u8g2.setFont(u8g2_font_wqy12_t_gb2312);
if (blink) {
_u8g2.drawUTF8(2, 12, "🚨");
_u8g2.drawUTF8(26, 12, name);
} else {
_u8g2.drawUTF8(2, 12, " ");
}
_u8g2.drawHLine(0, 15, 128);
}
// ============================================================ // ============================================================
// Page 0: 环境数据 // Page 0: 环境数据
// ============================================================ // ============================================================
void DisplayManager::_drawPage0(const TelemetryData& data, bool wifiOk, bool buzzerOn) { void DisplayManager::_drawPage0(const TelemetryData& data, bool wifiOk,
// 标题栏 bool buzzerOn, bool hasAlarm) {
// 标题栏 (报警横幅已占顶部时跳过)
if (!hasAlarm) {
_u8g2.setFont(u8g2_font_wqy12_t_gb2312); _u8g2.setFont(u8g2_font_wqy12_t_gb2312);
_u8g2.drawUTF8(2, 12, "🏥 健康监测 V1.0"); _u8g2.drawUTF8(2, 12, "🏥 健康监测 V1.0");
// 分隔线
_u8g2.drawHLine(0, 15, 128); _u8g2.drawHLine(0, 15, 128);
}
// 温湿度 // 温湿度
_u8g2.setFont(u8g2_font_wqy12_t_gb2312); _u8g2.setFont(u8g2_font_wqy12_t_gb2312);
@@ -144,11 +170,13 @@ void DisplayManager::_drawPage0(const TelemetryData& data, bool wifiOk, bool buz
// Page 1: 生命体征 // Page 1: 生命体征
// ============================================================ // ============================================================
void DisplayManager::_drawPage1(const TelemetryData& data) { void DisplayManager::_drawPage1(const TelemetryData& data, bool hasAlarm) {
// 标题栏 // 标题栏
if (!hasAlarm) {
_u8g2.setFont(u8g2_font_wqy12_t_gb2312); _u8g2.setFont(u8g2_font_wqy12_t_gb2312);
_u8g2.drawUTF8(2, 12, "🫀 生命体征"); _u8g2.drawUTF8(2, 12, "🫀 生命体征");
_u8g2.drawHLine(0, 15, 128); _u8g2.drawHLine(0, 15, 128);
}
// 心率 // 心率
_u8g2.drawUTF8(4, 32, "心率:"); _u8g2.drawUTF8(4, 32, "心率:");
@@ -182,10 +210,12 @@ void DisplayManager::_drawPage1(const TelemetryData& data) {
// Page 2: 人体状态 // Page 2: 人体状态
// ============================================================ // ============================================================
void DisplayManager::_drawPage2(const TelemetryData& data) { void DisplayManager::_drawPage2(const TelemetryData& data, bool hasAlarm) {
if (!hasAlarm) {
_u8g2.setFont(u8g2_font_wqy12_t_gb2312); _u8g2.setFont(u8g2_font_wqy12_t_gb2312);
_u8g2.drawUTF8(2, 12, "人体状态"); _u8g2.drawUTF8(2, 12, "人体状态");
_u8g2.drawHLine(0, 15, 128); _u8g2.drawHLine(0, 15, 128);
}
_u8g2.drawUTF8(4, 30, "有人:"); _u8g2.drawUTF8(4, 30, "有人:");
_u8g2.drawUTF8(60, 30, data.presence ? "V" : "X"); _u8g2.drawUTF8(60, 30, data.presence ? "V" : "X");
@@ -210,13 +240,20 @@ void DisplayManager::_drawPage2(const TelemetryData& data) {
// Page 3: 安全信息 // Page 3: 安全信息
// ============================================================ // ============================================================
void DisplayManager::_drawPage3(const TelemetryData& data, bool buzzerOn, bool wifiOk) { void DisplayManager::_drawPage3(const TelemetryData& data, bool buzzerOn,
bool wifiOk, bool hasAlarm) {
if (!hasAlarm) {
_u8g2.setFont(u8g2_font_wqy12_t_gb2312); _u8g2.setFont(u8g2_font_wqy12_t_gb2312);
_u8g2.drawUTF8(2, 12, "安全信息"); _u8g2.drawUTF8(2, 12, "安全信息");
_u8g2.drawHLine(0, 15, 128); _u8g2.drawHLine(0, 15, 128);
}
_u8g2.drawUTF8(4, 30, "水浸:"); _u8g2.drawUTF8(4, 30, "水浸:");
if (!data.rainValid) {
_u8g2.drawUTF8(60, 30, "离线");
} else {
_u8g2.drawUTF8(60, 30, data.rainDO ? "正常" : "!!报警"); _u8g2.drawUTF8(60, 30, data.rainDO ? "正常" : "!!报警");
}
_u8g2.drawUTF8(4, 44, "蜂鸣器:"); _u8g2.drawUTF8(4, 44, "蜂鸣器:");
_u8g2.drawUTF8(60, 44, buzzerOn ? "" : ""); _u8g2.drawUTF8(60, 44, buzzerOn ? "" : "");

View File

@@ -40,6 +40,23 @@ bool g_localFallActive = false;
bool g_localWaterActive = false; bool g_localWaterActive = false;
bool g_localHeartAbnActive = false; bool g_localHeartAbnActive = false;
bool g_localBreathAbnActive = false; bool g_localBreathAbnActive = false;
bool g_localTempAbnActive = false;
bool g_localHumiAbnActive = false;
bool g_localRadarFaultActive = false;
bool g_localDhtFaultActive = false;
// 报警防抖计数器 (连续N次确认才触发)
uint8_t g_tempAbnCnt = 0;
uint8_t g_humiAbnCnt = 0;
uint8_t g_radarTimeoutCnt = 0;
uint8_t g_heartAbnCnt = 0; // 心率异常防抖
uint8_t g_breathAbnCnt = 0; // 呼吸异常防抖
uint32_t g_fallDetectMs = 0; // 跌倒检测计时 (ms)
uint32_t g_fallClearMs = 0; // 跌倒清除计时 (ms)
bool g_fallDismissed = false; // 已手动解除, 不重复报警
#define ALARM_HYSTERESIS 3 // 连续3次确认才触发
#define FALL_DETECT_MS 2000 // 跌倒持续2秒才触发 (房顶安装, 过滤坐下)
#define FALL_CLEAR_MS 5000 // 跌倒消失5秒后才清除 (防误消)
// ============================================================ // ============================================================
// 时间同步 // 时间同步
@@ -74,14 +91,28 @@ void syncTime() {
void handleAlarmCommand(const AlarmCommand& cmd) { void handleAlarmCommand(const AlarmCommand& cmd) {
if (strcmp(cmd.type, "alarm_restore") == 0) { if (strcmp(cmd.type, "alarm_restore") == 0) {
// 告警恢复 // 告警恢复 — 远程指令与手动按钮同等优先级
g_alarm.executeRestore(cmd); g_alarm.executeRestore(cmd);
// 清除对应本地告警状态 // 清除全部本地告警状态 (远程恢复=手动解除)
if (strcmp(cmd.alarmType, "fall") == 0) g_localFallActive = false; g_localFallActive = false;
else if (strcmp(cmd.alarmType, "water_leak") == 0) g_localWaterActive = false; g_localWaterActive = false;
else if (strcmp(cmd.alarmType, "heart_abnormal") == 0) g_localHeartAbnActive = false; g_localHeartAbnActive = false;
else if (strcmp(cmd.alarmType, "breath_abnormal") == 0) g_localBreathAbnActive = false; g_localBreathAbnActive = false;
g_localTempAbnActive = false;
g_localHumiAbnActive = false;
g_localRadarFaultActive = false;
g_localDhtFaultActive = false;
// 防抖计数器也复位
g_tempAbnCnt = 0;
g_humiAbnCnt = 0;
g_radarTimeoutCnt = 0;
g_heartAbnCnt = 0;
g_breathAbnCnt = 0;
g_fallDetectMs = 0;
g_fallClearMs = 0;
g_fallDismissed = false;
} else { } else {
// 告警触发 // 告警触发
g_alarm.executeAlarm(cmd); g_alarm.executeAlarm(cmd);
@@ -191,11 +222,13 @@ TelemetryData buildTelemetry(const RadarRawData& radar) {
memset(&t, 0, sizeof(t)); memset(&t, 0, sizeof(t));
// 设备标识 // 设备标识
strncpy(t.saleid, SALEID_DEFAULT, sizeof(t.saleid) - 1);
strncpy(t.gateid, g_mqtt.getGateId(), sizeof(t.gateid) - 1); strncpy(t.gateid, g_mqtt.getGateId(), sizeof(t.gateid) - 1);
strncpy(t.meterId, METER_ID_DEFAULT, sizeof(t.meterId) - 1);
strncpy(t.meterName, METER_NAME_DEFAULT, sizeof(t.meterName) - 1); strncpy(t.meterName, METER_NAME_DEFAULT, sizeof(t.meterName) - 1);
// 环境类型: 从配网配置读取 (默认 bedroom)
DeviceConfig cfg = g_wifi.getConfig();
strncpy(t.deviceType, cfg.env_type[0] ? cfg.env_type : "bedroom", sizeof(t.deviceType)-1);
// DHT11 // DHT11
t.temp = g_sensor.getTemp(); t.temp = g_sensor.getTemp();
t.humi = g_sensor.getHumi(); t.humi = g_sensor.getHumi();
@@ -209,6 +242,8 @@ TelemetryData buildTelemetry(const RadarRawData& radar) {
t.heartRate = radar.heartRate; t.heartRate = radar.heartRate;
t.breathRate = radar.breathRate; t.breathRate = radar.breathRate;
t.body = radar.body; t.body = radar.body;
t.sleepState = radar.sleepState;
t.sleepScore = radar.sleepScore;
t.radarValid = radar.radarValid; t.radarValid = radar.radarValid;
t.vitalValid = radar.vitalValid; t.vitalValid = radar.vitalValid;
@@ -226,54 +261,189 @@ TelemetryData buildTelemetry(const RadarRawData& radar) {
} }
// ============================================================ // ============================================================
// 本地报警检查 (离线可用) // 本地报警回调 → MQTT 上报 (连接时)
// ============================================================
void onLocalAlarmChange(uint8_t type, bool triggered, const char* desc) {
if (g_mqtt.isConnected()) {
g_mqtt.publishLocalAlarm(type, triggered, desc);
}
}
// ============================================================
// 本地报警检查 (离线可用) — 人身安全 > 环境 > 设备
// ============================================================ // ============================================================
void checkLocalAlarms(const RadarRawData& radar) { void checkLocalAlarms(const RadarRawData& radar) {
uint32_t now = millis(); // 环境类型 → 报警开关
const char* env = g_wifi.getConfig().env_type;
bool enFall = (strcmp(env, "bathroom") == 0 || strcmp(env, "hallway") == 0 || strcmp(env, "livingroom") == 0);
bool enVital = (strcmp(env, "bedroom") == 0);
bool enWater = (strcmp(env, "bathroom") == 0);
bool enEnv = (strcmp(env, "bedroom") == 0 || strcmp(env, "balcony") == 0 || strcmp(env, "kitchen") == 0);
// --- 跌倒检测 (本地紧急报警, 立即触发) --- // ─── 前置条件: 有人才检测人体生命体征 ───
if (radar.fall && radar.radarValid && !g_localFallActive) { bool humanPresent = radar.presence && radar.radarValid;
// ─── ★★★★★ 人身安全: 跌倒 (仅浴室/走廊/客厅) ───
if (enFall && radar.fall && humanPresent && !g_fallDismissed && millis() > 30000) {
if (!g_localFallActive) {
// 持续检测中: 必须满 FALL_DETECT_MS 才报警
if (g_fallDetectMs == 0) {
g_fallDetectMs = millis();
} else if (millis() - g_fallDetectMs >= FALL_DETECT_MS) {
g_localFallActive = true; g_localFallActive = true;
g_alarm.triggerLocal(AlarmManager::ALARM_FALL); g_fallDetectMs = 0;
} else if (!radar.fall && g_localFallActive) { g_fallClearMs = 0;
g_alarm.triggerLocal(AlarmManager::ALARM_FALL, "雷达检测到跌倒");
}
} else {
g_fallClearMs = 0; // 还在跌倒, 重置清除计时
}
} else {
g_fallDetectMs = 0;
// 解除抑制需要 fall 持续消失 FALL_CLEAR_MS (防 AFD1 频繁跳变)
if (g_fallDismissed && g_fallClearMs == 0) {
g_fallClearMs = millis();
}
if (!g_fallDismissed || millis() - g_fallClearMs >= FALL_CLEAR_MS) {
g_fallDismissed = false;
g_fallClearMs = 0;
}
if (g_localFallActive) {
// 跌倒已消失 → 延迟清除
if (g_fallClearMs == 0) {
g_fallClearMs = millis();
} else if (millis() - g_fallClearMs >= FALL_CLEAR_MS) {
g_localFallActive = false; g_localFallActive = false;
g_fallClearMs = 0;
g_alarm.clearLocal(AlarmManager::ALARM_FALL); g_alarm.clearLocal(AlarmManager::ALARM_FALL);
} }
}
// --- 水浸检测 ---
bool waterLeak = !g_sensor.getRainDO() && g_sensor.isRainValid();
if (waterLeak && !g_localWaterActive) {
g_localWaterActive = true;
g_alarm.triggerLocal(AlarmManager::ALARM_WATER_LEAK);
} else if (!waterLeak && g_localWaterActive) {
g_localWaterActive = false;
g_alarm.clearLocal(AlarmManager::ALARM_WATER_LEAK);
} }
// --- 心率异常 (离线本地判定, 阈值预留后续调整) --- // ─── ★★★★★ 人身安全: 心率异常 (仅卧室+ABD1, 防抖3次, 30s) ───
if (radar.vitalValid && radar.heartRate > 0) { #if RADAR_TYPE != RADAR_TYPE_AFD1
if (enVital && millis() > 30000 && humanPresent && radar.vitalValid && radar.heartRate > 0) {
bool heartAbn = (radar.heartRate < HEART_RATE_MIN || radar.heartRate > HEART_RATE_MAX); bool heartAbn = (radar.heartRate < HEART_RATE_MIN || radar.heartRate > HEART_RATE_MAX);
if (heartAbn && !g_localHeartAbnActive) { if (heartAbn) {
if (++g_heartAbnCnt >= ALARM_HYSTERESIS && !g_localHeartAbnActive) {
g_localHeartAbnActive = true; g_localHeartAbnActive = true;
g_alarm.triggerLocal(AlarmManager::ALARM_HEART_ABNORMAL); char buf[32];
} else if (!heartAbn && g_localHeartAbnActive) { snprintf(buf, sizeof(buf), "心率异常: %d bpm", radar.heartRate);
g_alarm.triggerLocal(AlarmManager::ALARM_HEART_ABNORMAL, buf);
}
} else {
g_heartAbnCnt = 0;
if (g_localHeartAbnActive) {
g_localHeartAbnActive = false; g_localHeartAbnActive = false;
g_alarm.clearLocal(AlarmManager::ALARM_HEART_ABNORMAL); g_alarm.clearLocal(AlarmManager::ALARM_HEART_ABNORMAL);
} }
} }
} else if (!humanPresent && g_localHeartAbnActive) {
g_localHeartAbnActive = false;
g_heartAbnCnt = 0;
g_alarm.clearLocal(AlarmManager::ALARM_HEART_ABNORMAL);
}
#endif // RADAR_TYPE != AFD1
// --- 呼吸异常 --- // ─── ★★★★★ 人身安全: 呼吸异常 (仅卧室+ABD1, 防抖3次, 30秒后生效) ───
if (radar.vitalValid && radar.breathRate > 0) { #if RADAR_TYPE != RADAR_TYPE_AFD1
if (enVital && millis() > 30000 && humanPresent && radar.vitalValid && radar.breathRate > 0) {
bool breathAbn = (radar.breathRate < BREATH_RATE_MIN || radar.breathRate > BREATH_RATE_MAX); bool breathAbn = (radar.breathRate < BREATH_RATE_MIN || radar.breathRate > BREATH_RATE_MAX);
if (breathAbn && !g_localBreathAbnActive) { if (breathAbn) {
if (++g_breathAbnCnt >= ALARM_HYSTERESIS && !g_localBreathAbnActive) {
g_localBreathAbnActive = true; g_localBreathAbnActive = true;
g_alarm.triggerLocal(AlarmManager::ALARM_BREATH_ABNORMAL); char buf[32];
} else if (!breathAbn && g_localBreathAbnActive) { snprintf(buf, sizeof(buf), "呼吸异常: %d 次/分", radar.breathRate);
g_alarm.triggerLocal(AlarmManager::ALARM_BREATH_ABNORMAL, buf);
}
} else {
g_breathAbnCnt = 0;
if (g_localBreathAbnActive) {
g_localBreathAbnActive = false; g_localBreathAbnActive = false;
g_alarm.clearLocal(AlarmManager::ALARM_BREATH_ABNORMAL); g_alarm.clearLocal(AlarmManager::ALARM_BREATH_ABNORMAL);
} }
} }
} else if (!humanPresent && g_localBreathAbnActive) {
g_localBreathAbnActive = false;
g_breathAbnCnt = 0;
g_alarm.clearLocal(AlarmManager::ALARM_BREATH_ABNORMAL);
}
#endif // RADAR_TYPE != AFD1
// ─── ★★★ 环境安全: 水浸 (仅浴室) ───
if (enWater) {
bool waterLeak = !g_sensor.getRainDO() && g_sensor.isRainValid();
if (waterLeak && !g_localWaterActive) {
g_localWaterActive = true;
g_alarm.triggerLocal(AlarmManager::ALARM_WATER_LEAK, "水浸传感器触发");
} else if (!waterLeak && g_localWaterActive) {
g_localWaterActive = false;
g_alarm.clearLocal(AlarmManager::ALARM_WATER_LEAK);
}
} // enWater
// ─── ★★ 环境舒适: 温湿度异常 (仅卧室/阳台/厨房, 防抖3次, 30s) ───
if (enEnv && millis() > 30000 && g_sensor.isDhtValid()) {
float temp = g_sensor.getTemp();
bool tempAbn = (temp < TEMP_LOW || temp > TEMP_HIGH);
if (tempAbn) {
if (++g_tempAbnCnt >= ALARM_HYSTERESIS && !g_localTempAbnActive) {
g_localTempAbnActive = true;
char buf[32];
snprintf(buf, sizeof(buf), "温度%.1f°C 超出范围", temp);
g_alarm.triggerLocal(AlarmManager::ALARM_TEMP_ABNORMAL, buf);
}
} else {
g_tempAbnCnt = 0;
if (g_localTempAbnActive) {
g_localTempAbnActive = false;
g_alarm.clearLocal(AlarmManager::ALARM_TEMP_ABNORMAL);
}
}
// ─── ★★ 环境舒适: 湿度异常 (防抖3次) ───
float humi = g_sensor.getHumi();
bool humiAbn = (humi < HUMI_LOW || humi > HUMI_HIGH);
if (humiAbn) {
if (++g_humiAbnCnt >= ALARM_HYSTERESIS && !g_localHumiAbnActive) {
g_localHumiAbnActive = true;
char buf[32];
snprintf(buf, sizeof(buf), "湿度%.1f%% 超出范围", humi);
g_alarm.triggerLocal(AlarmManager::ALARM_HUMI_ABNORMAL, buf);
}
} else {
g_humiAbnCnt = 0;
if (g_localHumiAbnActive) {
g_localHumiAbnActive = false;
g_alarm.clearLocal(AlarmManager::ALARM_HUMI_ABNORMAL);
}
}
}
// ─── 设备:雷达超时 (连续5秒无数据, 仅标记, 不报警) ───
if (g_radar.isTimeout()) {
if (++g_radarTimeoutCnt >= 5 && !g_localRadarFaultActive) {
g_localRadarFaultActive = true;
Serial.println(F("[系统] ⚠️ 雷达超时无数据"));
}
} else {
g_radarTimeoutCnt = 0;
if (g_localRadarFaultActive) {
g_localRadarFaultActive = false;
Serial.println(F("[系统] ✅ 雷达数据恢复"));
}
}
// ─── 设备DHT 持续故障 (仅标记, 不报警) ───
if (g_sensor.isDhtFault() && !g_localDhtFaultActive && millis() > 60000) {
g_localDhtFaultActive = true;
Serial.println(F("[系统] ⚠️ DHT11 温湿度传感器故障"));
} else if (!g_sensor.isDhtFault() && g_localDhtFaultActive) {
g_localDhtFaultActive = false;
Serial.println(F("[系统] ✅ DHT11 传感器恢复"));
}
} }
// ============================================================ // ============================================================
@@ -346,6 +516,20 @@ void setup() {
}); });
g_buttons.onAlarmDismiss([]() { g_buttons.onAlarmDismiss([]() {
g_alarm.clearAllLocal(); g_alarm.clearAllLocal();
// 同步清除所有本地状态标记
g_localFallActive = false;
g_localWaterActive = false;
g_localHeartAbnActive = false;
g_localBreathAbnActive = false;
g_localTempAbnActive = false;
g_localHumiAbnActive = false;
g_localRadarFaultActive = false;
g_localDhtFaultActive = false;
g_tempAbnCnt = 0;
g_humiAbnCnt = 0;
g_radarTimeoutCnt = 0;
g_fallClearMs = 0;
g_fallDismissed = true; // 抑制重复报警, 等跌倒信号消了再恢复
}); });
g_buttons.onProvisionLongPress([]() { g_buttons.onProvisionLongPress([]() {
g_wifi.startConfigPortal(); g_wifi.startConfigPortal();
@@ -370,6 +554,9 @@ void setup() {
g_mqtt.onControlCommand(handleControlCommand); g_mqtt.onControlCommand(handleControlCommand);
g_mqtt.onProvisionCommand(handleProvisionCommand); g_mqtt.onProvisionCommand(handleProvisionCommand);
// 注册本地报警回调 → MQTT 上报
g_alarm.onAlarmChange(onLocalAlarmChange);
// 连接 MQTT // 连接 MQTT
if (g_wifi.isConnected()) { if (g_wifi.isConnected()) {
g_mqtt.connect(); g_mqtt.connect();
@@ -378,6 +565,9 @@ void setup() {
// 8. 传感器 // 8. 传感器
g_sensor.begin(); g_sensor.begin();
// 8. 等雷达 + 电源稳定
delay(8000);
// 9. 雷达任务 (核心0) // 9. 雷达任务 (核心0)
xTaskCreatePinnedToCore( xTaskCreatePinnedToCore(
radarTask, "RadarTask", radarTask, "RadarTask",
@@ -411,13 +601,22 @@ void loop() {
static bool wasDisconnected = true; static bool wasDisconnected = true;
if (g_mqtt.isConnected()) { if (g_mqtt.isConnected()) {
if (wasDisconnected) { if (wasDisconnected) {
// 刚恢复连接, 同步时间
syncTime(); syncTime();
wasDisconnected = false; wasDisconnected = false;
} }
} else { } else {
wasDisconnected = true; wasDisconnected = true;
} }
// 配网成功后: 先更新 MQTT 配置, 再强制重连
if (g_wifi.justProvisioned()) {
DeviceConfig cfg = g_wifi.getConfig();
g_mqtt.disconnect();
g_mqtt.setServer(cfg.mqtt_server, atoi(cfg.mqtt_port),
cfg.mqtt_user, cfg.mqtt_pass);
g_mqtt.connect();
g_mqtt.publishProvisionStatus("success", "");
}
} }
// --- 3. 传感器 (独立,任何传感器故障不影响其他) --- // --- 3. 传感器 (独立,任何传感器故障不影响其他) ---
@@ -427,33 +626,57 @@ void loop() {
RadarRawData radar = g_radar.getData(); RadarRawData radar = g_radar.getData();
checkLocalAlarms(radar); checkLocalAlarms(radar);
// --- 传感器故障检测 (每30秒检查一次避免刷屏) --- // --- 传感器状态日志 (仅记录, 不报警) ---
static uint32_t lastFaultCheck = 0; static uint32_t lastFaultLog = 0;
if (now - lastFaultCheck > 30000) { if (now - lastFaultLog > 60000) {
lastFaultCheck = now; lastFaultLog = now;
if (!g_sensor.isDhtValid() && millis() > 60000) { if (!g_sensor.isDhtValid() && millis() > 60000) {
Serial.println(F("[系统] ⚠️ DHT11 未检测到 (不影响运行)")); Serial.println(F("[系统] ⚠️ DHT11 读数失效"));
} }
if (g_radar.isTimeout()) { if (g_radar.isTimeout()) {
Serial.println(F("[系统] ⚠️ 雷达超时无数据 (不影响运行)")); Serial.println(F("[系统] ⚠️ 雷达数据超时"));
} }
} }
// --- 5. 报警模块更新 (定时器/闪烁) --- // --- 5. 报警模块更新 (定时器/闪烁) ---
g_alarm.loop(); g_alarm.loop();
// --- 6. 遥测上报 (1秒周期) --- // --- 6. 遥测上报 ---
// 有人时: 每秒全量上报
// 无人时: 仅状态变化时发极简报文 + 60秒心跳 (省4G流量)
static bool lastPresence = false;
static uint32_t lastHeartbeat = 0; // 无人心跳计时
if (radar.presence) {
// ── 有人: 每秒完整上报 ──
if (now - g_lastReportTime >= REPORT_INTERVAL_MS) { if (now - g_lastReportTime >= REPORT_INTERVAL_MS) {
g_lastReportTime = now; g_lastReportTime = now;
TelemetryData tele = buildTelemetry(radar); TelemetryData tele = buildTelemetry(radar);
g_mqtt.publishAllPoints(tele, g_sequence); g_mqtt.publishAllPoints(tele, g_sequence);
g_sequence++; g_sequence++;
lastHeartbeat = now; // 有人时重置心跳计时
// 调试输出 + OLED 显示
printDebug(tele); printDebug(tele);
g_display.update(tele, wifiOk, g_alarm.isBuzzerOn()); g_display.update(tele, wifiOk, g_alarm.isBuzzerOn(),
g_alarm.hasActiveAlarm(), g_alarm.getActiveAlarmStr());
} }
} else {
// ── 无人: 状态翻转发一次极简报文, 之后每60秒心跳 ──
if (lastPresence != radar.presence || now - lastHeartbeat >= PRESENCE_HEARTBEAT_MS) {
g_mqtt.publishPresenceOnly(radar.presence, radar.radarValid, g_sequence);
g_sequence++;
lastHeartbeat = now;
// 调试输出
Serial.printf("[上报] 无人极简报文 (Presence=0) 序号:%d\n", g_sequence - 1);
}
// OLED 还是照常刷新 (显示无人状态)
TelemetryData tele = buildTelemetry(radar);
g_display.update(tele, wifiOk, g_alarm.isBuzzerOn(),
g_alarm.hasActiveAlarm(), g_alarm.getActiveAlarmStr());
}
lastPresence = radar.presence;
// --- 7. MQTT 配网状态 (WiFiManager 触发时) --- // --- 7. MQTT 配网状态 (WiFiManager 触发时) ---
// WiFiManager 的 captive portal 启动/关闭时更新状态 // WiFiManager 的 captive portal 启动/关闭时更新状态

View File

@@ -178,20 +178,23 @@ void MQTTManager::publishAllPoints(const TelemetryData& d, uint32_t sequence) {
snprintf(timeStr, sizeof(timeStr), "2026-01-01 00:00:00"); snprintf(timeStr, sizeof(timeStr), "2026-01-01 00:00:00");
} }
// 构建 JSON (协议规范 §4.1) // 构建 JSON (协议规范 V0.08)
JsonDocument doc; JsonDocument doc;
doc["saleid"] = d.saleid;
doc["gateid"] = d.gateid; doc["gateid"] = d.gateid;
doc["type"] = "report"; doc["type"] = "report";
doc["time"] = timeStr; doc["time"] = timeStr;
doc["sequence"] = String(sequence); doc["sequence"] = String(sequence);
doc["source"] = "da"; doc["source"] = "da";
// meter.id = gateid + 后缀 (如 10100001001 → 10100001001001)
String meterId = String(d.gateid) + "001";
JsonArray meter = doc["meter"].to<JsonArray>(); JsonArray meter = doc["meter"].to<JsonArray>();
JsonObject m = meter.add<JsonObject>(); JsonObject m = meter.add<JsonObject>();
m["id"] = d.meterId; m["id"] = meterId;
m["status"] = "1"; m["status"] = "1";
m["name"] = d.meterName; m["name"] = d.meterName;
m["type"] = d.deviceType; // 设备类型枚举
JsonObject v = m["values"].to<JsonObject>(); JsonObject v = m["values"].to<JsonObject>();
// 环境数据 // 环境数据
@@ -205,6 +208,8 @@ void MQTTManager::publishAllPoints(const TelemetryData& d, uint32_t sequence) {
v["Resident"] = d.resident ? "1" : "0"; v["Resident"] = d.resident ? "1" : "0";
// 生命体征 // 生命体征
v["Body"] = String(d.body); v["Body"] = String(d.body);
v["SleepState"] = String(d.sleepState);
v["SleepScore"] = String(d.sleepScore);
v["HeartRate"] = String(d.heartRate); v["HeartRate"] = String(d.heartRate);
v["BreathRate"] = String(d.breathRate); v["BreathRate"] = String(d.breathRate);
v["RadarValid"] = d.radarValid ? "1" : "0"; v["RadarValid"] = d.radarValid ? "1" : "0";
@@ -371,6 +376,92 @@ void MQTTManager::publishProvisionResponse(const char* cmdId, bool success,
_publish(MQTT_TOPIC_RESPONSE_PROVISION, payload.c_str(), STATUS_QOS); _publish(MQTT_TOPIC_RESPONSE_PROVISION, payload.c_str(), STATUS_QOS);
} }
// ============================================================
// 上行: 本地报警上报 (§4.7 扩展)
// 设备本地判定报警后立即上报,格式兼容服务端告警协议
// ============================================================
void MQTTManager::publishLocalAlarm(uint8_t alarmType, bool triggered, const char* desc) {
if (!isConnected()) return;
struct tm timeinfo;
char timeStr[24];
if (getLocalTime(&timeinfo)) {
strftime(timeStr, sizeof(timeStr), "%Y-%m-%d %H:%M:%S", &timeinfo);
} else {
snprintf(timeStr, sizeof(timeStr), "2026-01-01 00:00:00");
}
// 类型映射
const char* typeStr = "unknown";
switch (alarmType) {
case 1: typeStr = "fall"; break; // ALARM_FALL
case 2: typeStr = "water_leak"; break; // ALARM_WATER_LEAK
case 3: typeStr = "heart_abnormal"; break; // ALARM_HEART_ABNORMAL
case 4: typeStr = "breath_abnormal"; break; // ALARM_BREATH_ABNORMAL
case 5: typeStr = "temp_abnormal"; break; // ALARM_TEMP_ABNORMAL
case 6: typeStr = "humi_abnormal"; break; // ALARM_HUMI_ABNORMAL
case 7: typeStr = "sensor_fault"; break; // ALARM_SENSOR_FAULT
default: typeStr = "unknown"; break;
}
JsonDocument doc;
doc["gateid"] = _gateid;
doc["type"] = triggered ? "alarm" : "alarm_restore";
doc["time"] = timeStr;
doc["alarmType"] = typeStr;
doc["source"] = "local"; // 标识本地判定
doc["desc"] = desc ? desc : "";
String payload;
serializeJson(doc, payload);
// 通过 status 通道上报本地报警
String topic = _buildTopic("status/%s/alarm");
_mqtt.publish(topic.c_str(), payload.c_str());
}
// ============================================================
// 上行: 无人极简报文 (省4G流量, 仅 Presence + RadarValid)
// ============================================================
void MQTTManager::publishPresenceOnly(bool presence, bool radarValid, uint32_t sequence) {
if (!isConnected()) return;
struct tm timeinfo;
char timeStr[24];
if (getLocalTime(&timeinfo)) {
strftime(timeStr, sizeof(timeStr), "%Y-%m-%d %H:%M:%S", &timeinfo);
} else {
snprintf(timeStr, sizeof(timeStr), "2026-01-01 00:00:00");
}
// 极简 JSON: ~120 字节 vs 完整报文 ~500 字节
JsonDocument doc;
doc["gateid"] = _gateid;
doc["type"] = "report";
doc["time"] = timeStr;
doc["sequence"] = String(sequence);
doc["source"] = "da";
String meterId = String(_gateid) + "001";
JsonArray meter = doc["meter"].to<JsonArray>();
JsonObject m = meter.add<JsonObject>();
m["id"] = meterId;
m["status"] = "1";
m["name"] = METER_NAME_DEFAULT;
JsonObject v = m["values"].to<JsonObject>();
v["Presence"] = presence ? "1" : "0";
v["RadarValid"] = radarValid ? "1" : "0";
String payload;
serializeJson(doc, payload);
_mqtt.publish("report/allpoints", payload.c_str());
}
// ============================================================ // ============================================================
// 回调注册 // 回调注册
// ============================================================ // ============================================================

View File

@@ -19,6 +19,11 @@ RadarManager::RadarManager()
, _callback(nullptr) , _callback(nullptr)
, _frameCount(0) , _frameCount(0)
, _errorCount(0) , _errorCount(0)
, _lastHeartBeat(0)
, _lastBreathBeat(0)
, _stablePresence(false)
, _presenceCnt(0)
, _lastRawPresence(false)
{ {
memset(&_currentFrame, 0, sizeof(_currentFrame)); memset(&_currentFrame, 0, sizeof(_currentFrame));
memset(&_data, 0, sizeof(_data)); memset(&_data, 0, sizeof(_data));
@@ -43,6 +48,9 @@ bool RadarManager::begin() {
_resetParser(); _resetParser();
memset(&_data, 0, sizeof(_data)); memset(&_data, 0, sizeof(_data));
_data.lastUpdate = millis(); _data.lastUpdate = millis();
_stablePresence = false;
_presenceCnt = 0;
_lastRawPresence = false;
// 不给雷达发任何命令,让它自己初始化并开始自动上报 // 不给雷达发任何命令,让它自己初始化并开始自动上报
// R60ABD1 上电后通常需要 15-30 秒自检,之后自动输出数据 // R60ABD1 上电后通常需要 15-30 秒自检,之后自动输出数据
@@ -68,6 +76,16 @@ void RadarManager::loop() {
Serial.printf("[雷达] ✅ 开始接收数据 (首帧)\n"); Serial.printf("[雷达] ✅ 开始接收数据 (首帧)\n");
} }
// 心率/呼吸数据过期 (>10秒无更新 → 标记无效)
if (millis() - _lastHeartBeat > 10000) {
_data.vitalValid = false;
_data.heartRate = 0;
}
if (millis() - _lastBreathBeat > 10000) {
_data.vitalValid = false;
_data.breathRate = 0;
}
// 超时检测:如果 10 秒还没收到任何数据,打印一次 // 超时检测:如果 10 秒还没收到任何数据,打印一次
static uint32_t noDataWarn = 0; static uint32_t noDataWarn = 0;
if (_frameCount == 0 && millis() - _data.lastUpdate > 10000) { if (_frameCount == 0 && millis() - _data.lastUpdate > 10000) {
@@ -222,6 +240,9 @@ void RadarManager::_processCompleteFrame() {
case CTRL_HEART_RATE: case CTRL_HEART_RATE:
_extractHeartRate(_currentFrame); _extractHeartRate(_currentFrame);
break; break;
case CTRL_FALL:
_extractFall(_currentFrame); // AFD1 专用跌倒
break;
case CTRL_SLEEP: case CTRL_SLEEP:
_extractSleep(_currentFrame); _extractSleep(_currentFrame);
break; break;
@@ -250,47 +271,60 @@ void RadarManager::_processCompleteFrame() {
void RadarManager::_extractPresence(const RadarFrame& frame) { void RadarManager::_extractPresence(const RadarFrame& frame) {
uint8_t raw = frame.data_length > 0 ? frame.data[0] : 0; uint8_t raw = frame.data_length > 0 ? frame.data[0] : 0;
bool rawPresence = false;
// cmd 0x00 (len=1): 有人状态 (1=有人 0=无人) // cmd 0x00 (len=1): 有人状态 (1=有人 0=无人)
if (frame.command == 0x00 && frame.data_length >= 1) { if (frame.command == 0x00 && frame.data_length >= 1) {
_data.presence = (raw == 1); rawPresence = (raw == 1);
_data.radarValid = true; _data.radarValid = true;
_data.lastUpdate = millis(); _data.lastUpdate = millis();
_notifyUpdate();
} }
// cmd 0x01 (len=1): 运动状态/体动 (值越大越活跃) // cmd 0x01 (len=1): 运动状态/体动 (值越大越活跃)
else if (frame.command == 0x01 && frame.data_length >= 1) { else if (frame.command == 0x01 && frame.data_length >= 1) {
_data.body = raw; // 0=静止, 1+=有体动 _data.body = raw;
_data.motion = (raw >= 1); // >=1 有运动 _data.motion = (raw >= 1);
_data.radarValid = true; _data.radarValid = true;
_data.lastUpdate = millis(); _data.lastUpdate = millis();
_notifyUpdate(); _notifyUpdate();
return; // 运动帧不含有人信息, 跳过防抖
} }
// cmd 0x03 (len=1): 有人状态备用 (非0=有人) // cmd 0x03 (len=1): 有人状态备用 (非0=有人)
else if (frame.command == 0x03 && frame.data_length >= 1) { else if (frame.command == 0x03 && frame.data_length >= 1) {
_data.presence = (raw != 0); rawPresence = (raw != 0);
_data.radarValid = true; _data.radarValid = true;
_data.lastUpdate = millis(); _data.lastUpdate = millis();
_notifyUpdate();
} }
// cmd 0x04 (len=2): [0]=presence [1]=motion // cmd 0x04 (len=2): [0]=presence [1]=motion
else if (frame.command == 0x04 && frame.data_length >= 2) { else if (frame.command == 0x04 && frame.data_length >= 2) {
_data.presence = (frame.data[0] != 0); rawPresence = (frame.data[0] != 0);
_data.motion = (frame.data[1] == 0); _data.motion = (frame.data[1] == 0);
_data.radarValid = true; _data.radarValid = true;
_data.lastUpdate = millis(); _data.lastUpdate = millis();
_notifyUpdate();
} }
// cmd 0x05 (len=6): 完整包 // cmd 0x05 (len=6): 完整包
else if (frame.command == 0x05 && frame.data_length >= 6) { else if (frame.command == 0x05 && frame.data_length >= 6) {
_data.presence = (frame.data[0] != 0); rawPresence = (frame.data[0] != 0);
_data.motion = (frame.data[1] == 0); _data.motion = (frame.data[1] == 0);
_data.distance = ((uint16_t)frame.data[2] << 8) | frame.data[3]; _data.distance = ((uint16_t)frame.data[2] << 8) | frame.data[3];
_data.body = frame.data[4]; _data.body = frame.data[4];
_data.radarValid = true; _data.radarValid = true;
_data.lastUpdate = millis(); _data.lastUpdate = millis();
_notifyUpdate(); } else {
return; // 未知命令
} }
// 防抖: 连续 PRESENCE_HYSTERESIS 次一致才更新
if (rawPresence == _lastRawPresence) {
if (_presenceCnt < PRESENCE_HYSTERESIS) _presenceCnt++;
} else {
_presenceCnt = 0;
_lastRawPresence = rawPresence;
}
if (_presenceCnt >= PRESENCE_HYSTERESIS && rawPresence != _data.presence) {
_data.presence = rawPresence;
_stablePresence = rawPresence;
}
_notifyUpdate();
} }
void RadarManager::_extractBreathing(const RadarFrame& frame) { void RadarManager::_extractBreathing(const RadarFrame& frame) {
@@ -299,8 +333,9 @@ void RadarManager::_extractBreathing(const RadarFrame& frame) {
if (rate != 0x80 && rate != 0x00 && rate <= 40) { if (rate != 0x80 && rate != 0x00 && rate <= 40) {
_data.breathRate = rate; _data.breathRate = rate;
_data.vitalValid = true; _data.vitalValid = true;
_data.presence = true; // 有呼吸=有人 _data.presence = true;
_data.radarValid = true; _data.radarValid = true;
_lastBreathBeat = millis();
} }
_data.lastUpdate = millis(); _data.lastUpdate = millis();
_notifyUpdate(); _notifyUpdate();
@@ -313,14 +348,25 @@ void RadarManager::_extractHeartRate(const RadarFrame& frame) {
if (rate != 0x80 && rate != 0x00 && rate >= 40) { if (rate != 0x80 && rate != 0x00 && rate >= 40) {
_data.heartRate = rate; _data.heartRate = rate;
_data.vitalValid = true; _data.vitalValid = true;
_data.presence = true; // 有心跳=有人 _data.presence = true;
_data.radarValid = true; _data.radarValid = true;
_lastHeartBeat = millis();
} }
_data.lastUpdate = millis(); _data.lastUpdate = millis();
_notifyUpdate(); _notifyUpdate();
} }
} }
// AFD1 专用: 跌倒检测 (控制字 0x83)
void RadarManager::_extractFall(const RadarFrame& frame) {
if (frame.data_length >= 1) {
_data.fall = (frame.data[0] != 0);
_data.radarValid = true;
_data.lastUpdate = millis();
_notifyUpdate();
}
}
void RadarManager::_extractSleep(const RadarFrame& frame) { void RadarManager::_extractSleep(const RadarFrame& frame) {
// 从睡眠数据提取: cmd=0x00/0x01 为入床离床状态 // 从睡眠数据提取: cmd=0x00/0x01 为入床离床状态
if (frame.data_length >= 1) { if (frame.data_length >= 1) {
@@ -333,16 +379,15 @@ void RadarManager::_extractSleep(const RadarFrame& frame) {
} }
if (frame.data_length >= 3) { if (frame.data_length >= 3) {
// data[0]: 入床/离床 _data.sleepState = frame.data[1]; // 0=清醒 1=浅睡 2=深睡
// data[1]: 睡眠状态 _data.sleepScore = frame.data[2]; // 0~100
// data[2]: 评分 _data.lastUpdate = millis();
_notifyUpdate();
}
// 跌倒检测: 可能在睡眠异常标志中
if (frame.data_length >= 4) { if (frame.data_length >= 4) {
_data.fall = (frame.data[3] & 0x01) != 0; _data.fall = (frame.data[3] & 0x01) != 0;
_data.resident = (frame.data[3] & 0x02) != 0; _data.resident = (frame.data[3] & 0x02) != 0;
}
_data.lastUpdate = millis(); _data.lastUpdate = millis();
_notifyUpdate(); _notifyUpdate();
} }

View File

@@ -39,7 +39,7 @@ bool SensorManager::begin() {
pinMode(WATER_SENSOR_PIN, INPUT_PULLUP); pinMode(WATER_SENSOR_PIN, INPUT_PULLUP);
_lastRainRaw = (digitalRead(WATER_SENSOR_PIN) == HIGH); // HIGH=正常 _lastRainRaw = (digitalRead(WATER_SENSOR_PIN) == HIGH); // HIGH=正常
_rainDO = _lastRainRaw; _rainDO = _lastRainRaw;
_rainValid = true; _rainValid = false; // 默认离线,直到检测到真正的状态变化才设为有效
// 首次读取 (DHT11 可能失败) // 首次读取 (DHT11 可能失败)
read(); read();

View File

@@ -1,36 +1,42 @@
/** /**
* @file wifi_manager.cpp * @file wifi_manager.cpp
* @brief WiFi 配网管理器实现 * @brief WiFi 配网管理器 — AP + WebServer
*/ */
#include "wifi_manager.h" #include "wifi_manager.h"
// 单例指针 // BLE 配网回调: 小程序写入 JSON → 仅在 BLE 栈中暂存, 不直接处理
class BLEProvisionCB : public BLECharacteristicCallbacks {
void onWrite(BLECharacteristic* ch) override {
std::string val = ch->getValue();
if (val.length() > 0 && WiFiMgmt::_instance) {
Serial.printf("[BLE] 收到配网数据 (%d 字节)\n", val.length());
// 仅暂存 → 在 loop() 里处理 (BTC_TASK 栈太小, 写 Flash 会栈溢出)
WiFiMgmt::_instance->enqueueBLEJson(val.c_str());
}
}
};
WiFiMgmt* WiFiMgmt::_instance = nullptr; WiFiMgmt* WiFiMgmt::_instance = nullptr;
// ============================================================
// 构造函数 & 析构函数
// ============================================================
WiFiMgmt::WiFiMgmt() WiFiMgmt::WiFiMgmt()
: _connected(false) : _server(nullptr), _dns(nullptr)
, _lastCheckTime(0) , _connected(false)
, _buttonPressTime(0) , _portalActive(false), _portalStopFlag(false)
, _buttonPressed(false) , _portalStartTime(0)
, _ledLastToggle(0) , _lastCheckTime(0), _buttonPressTime(0), _buttonPressed(false)
, _ledState(false) , _lastScanTime(0)
, _param_mqtt_server(nullptr) , _provisionSuccess(false)
, _param_mqtt_port(nullptr) , _ledTaskHandle(nullptr), _ledMode(0)
, _param_mqtt_user(nullptr) , _bleServer(nullptr), _bleInfoChar(nullptr), _bleChar(nullptr), _bleAdv(nullptr)
, _param_mqtt_pass(nullptr) , _blePendingFlag(false)
, _param_device_id(nullptr)
, _param_env_note(nullptr)
{ {
memset(&_config, 0, sizeof(_config)); memset(&_config, 0, sizeof(_config));
_instance = this; // 设置单例 _instance = this;
} }
WiFiMgmt::~WiFiMgmt() { WiFiMgmt::~WiFiMgmt() {
_stopPortal();
_instance = nullptr; _instance = nullptr;
} }
@@ -39,93 +45,53 @@ WiFiMgmt::~WiFiMgmt() {
// ============================================================ // ============================================================
bool WiFiMgmt::begin() { bool WiFiMgmt::begin() {
Serial.println(F("\n========================================")); Serial.println(("\n=== WiFi 配网管理器 (SmartConfig) ==="));
Serial.println(F(" WiFi 配网管理器 初始化"));
Serial.println(F("========================================"));
// 初始化引脚
pinMode(CONFIG_BUTTON_PIN, INPUT_PULLUP); pinMode(CONFIG_BUTTON_PIN, INPUT_PULLUP);
pinMode(LED_WIFI_PIN, OUTPUT); pinMode(LED_WIFI_PIN, OUTPUT);
digitalWrite(LED_WIFI_PIN, LOW); digitalWrite(LED_WIFI_PIN, LOW);
// 初始化 LittleFS xTaskCreate(_ledTaskFunc, "WiFiLed", 1024, nullptr, 1, &_ledTaskHandle);
// 冷启动延迟, 等 Flash 稳定
delay(500);
if (!LittleFS.begin(true)) { if (!LittleFS.begin(true)) {
Serial.println(F("[WiFi] LittleFS 挂载失败!")); Serial.println(("[WiFi] LittleFS 挂载失败!"));
return false; return false;
} }
Serial.println(F("[WiFi] LittleFS 已挂载")); Serial.println(("[WiFi] LittleFS 就绪"));
// 加载配置
if (!_loadConfig()) { if (!_loadConfig()) {
Serial.println(F("[WiFi] 使用默认配置")); Serial.println(("[WiFi] 使用默认配置"));
// 设置默认值 strncpy(_config.mqtt_server, MQTT_DEFAULT_SERVER, sizeof(_config.mqtt_server)-1);
strncpy(_config.mqtt_server, MQTT_DEFAULT_SERVER, sizeof(_config.mqtt_server) - 1); strncpy(_config.mqtt_port, MQTT_DEFAULT_PORT_STR, sizeof(_config.mqtt_port)-1);
strncpy(_config.mqtt_port, MQTT_DEFAULT_PORT_STR, sizeof(_config.mqtt_port) - 1); strncpy(_config.mqtt_user, MQTT_DEFAULT_USER, sizeof(_config.mqtt_user)-1);
strncpy(_config.mqtt_user, MQTT_DEFAULT_USER, sizeof(_config.mqtt_user) - 1); strncpy(_config.mqtt_pass, MQTT_DEFAULT_PASS, sizeof(_config.mqtt_pass)-1);
strncpy(_config.mqtt_pass, MQTT_DEFAULT_PASS, sizeof(_config.mqtt_pass) - 1); strncpy(_config.device_id, MQTT_DEFAULT_DEVICE, sizeof(_config.device_id)-1);
strncpy(_config.device_id, MQTT_DEFAULT_DEVICE, sizeof(_config.device_id) - 1); strncpy(_config.env_note, MQTT_DEFAULT_ENV, sizeof(_config.env_note)-1);
strncpy(_config.env_note, MQTT_DEFAULT_ENV, sizeof(_config.env_note) - 1); strncpy(_config.env_type, "bedroom", sizeof(_config.env_type)-1);
memset(_config.wifi_ssid, 0, sizeof(_config.wifi_ssid));
memset(_config.wifi_pass, 0, sizeof(_config.wifi_pass));
} }
// 配置 WiFiManager WiFi.setAutoReconnect(true);
_wm.setDebugOutput(false); // 关闭调试输出 (避免干扰)
_wm.setConfigPortalTimeout(WIFI_CONFIG_TIMEOUT);
_wm.setConnectTimeout(WIFI_CONNECT_TIMEOUT);
_wm.setTitle("🏥 五劳无感康养监测 - 设备配网"); // 配网页面的中文标题
_wm.setCustomHeadElement(
"<style>"
"body { font-family: 'Microsoft YaHei', 'PingFang SC', sans-serif; }"
".msg .info { font-size: 14px; }"
"</style>"
);
WiFi.setAutoReconnect(true); // ESP32 自动重连
// 注册保存回调 (必须使用静态函数, 不能使用 lambda) // 有配置 → 直接连
_wm.setSaveConfigCallback(_saveParamsCallback); bool hasConfig = LittleFS.exists(CONFIG_FILE_DEVICE);
Serial.printf("[WiFi] 配置文件: %s\n", hasConfig ? "存在" : "不存在");
// 创建自定义参数 if (hasConfig) {
_param_mqtt_server = new WiFiManagerParameter(
"mqtt_server", "MQTT 服务器地址",
_config.mqtt_server, 40);
_param_mqtt_port = new WiFiManagerParameter(
"mqtt_port", "MQTT 端口",
_config.mqtt_port, 6);
_param_mqtt_user = new WiFiManagerParameter(
"mqtt_user", "MQTT 用户名",
_config.mqtt_user, 20);
_param_mqtt_pass = new WiFiManagerParameter(
"mqtt_pass", "MQTT 密码",
_config.mqtt_pass, 20);
_param_device_id = new WiFiManagerParameter(
"device_id", "设备名称",
_config.device_id, 20);
_param_env_note = new WiFiManagerParameter(
"env_note", "🏠 环境备注 (如: 主卧/客厅/老人房)",
_config.env_note, 20);
// 注册自定义参数
_wm.addParameter(_param_mqtt_server);
_wm.addParameter(_param_mqtt_port);
_wm.addParameter(_param_mqtt_user);
_wm.addParameter(_param_mqtt_pass);
_wm.addParameter(_param_device_id);
_wm.addParameter(_param_env_note);
// 设置 AP 模式回调 (静态函数)
_wm.setAPCallback(_apCallback);
// 尝试连接 WiFi
_connected = _connectWiFi(); _connected = _connectWiFi();
Serial.printf("[WiFi] 自动连接: %s\n", _connected ? "成功" : "失败");
} else {
Serial.println(("[WiFi] 无配置, 跳过连接"));
_connected = false;
}
if (!_connected) { if (!_connected) {
// 如果自动连接失败, 启动配网页面 _setLEDMode(2); // 快闪 = 配网模式
Serial.println(F("[WiFi] 自动连接失败, 启动配网模式...")); _startPortal(); // 只开 AP
_setLEDMode(1); // 快闪 = 配网模式 } else {
_connected = _connectWiFi(); // 内部会启动配网页面 _setLEDMode(3); // 常亮
}
if (_connected) {
_setLEDMode(2); // 常亮 = 已连接
} }
_lastCheckTime = millis(); _lastCheckTime = millis();
@@ -137,35 +103,64 @@ bool WiFiMgmt::begin() {
// ============================================================ // ============================================================
bool WiFiMgmt::loop() { bool WiFiMgmt::loop() {
// 检测按钮长按 // --- BLE 待处理数据 (从 loop 上下文安全处理, 不占 BTC_TASK 栈) ---
_processBLEPending();
// --- 配网完成 → WiFi 连接 (Portal 内/外 统一处理) ---
if (_portalStopFlag) {
if (_portalActive) {
_stopPortal();
}
_portalStopFlag = false;
if (_pendingSSID.length() > 0) {
Serial.printf("[WiFi] 配网连接: %s\n", _pendingSSID.c_str());
_connected = _tryConnect(_pendingSSID.c_str(), _pendingPass.c_str());
_pendingSSID = ""; _pendingPass = "";
if (_connected) {
_setLEDMode(3);
_provisionSuccess = true; // 标记配网成功 → main 里触发 MQTT 重连
} else {
_setLEDMode(2);
if (!_portalActive) {
_startPortal(); // 连接失败 → 开 AP 重试
}
}
}
return _connected;
}
// --- Portal 模式 ---
if (_portalActive) {
_dns->processNextRequest();
_server->handleClient();
delay(10);
return _connected;
}
// --- 正常运行 ---
if (_checkButtonLongPress()) { if (_checkButtonLongPress()) {
Serial.println(F("[WiFi] ⚠️ 检测到长按, 清除配置并重启...")); Serial.println(("[WiFi] 长按, 进配网模式"));
resetSettings(); _setLEDMode(2);
WiFi.disconnect();
_startPortal();
_connected = false;
return false; return false;
} }
// 检查 WiFi 状态
if (millis() - _lastCheckTime > WIFI_RETRY_INTERVAL) { if (millis() - _lastCheckTime > WIFI_RETRY_INTERVAL) {
_lastCheckTime = millis(); _lastCheckTime = millis();
wl_status_t s = WiFi.status();
if (WiFi.status() != WL_CONNECTED) { if (s != WL_CONNECTED) {
if (_connected) {
_connected = false; _connected = false;
_setLEDMode(0); // 慢闪 = 未连接 _setLEDMode(1);
Serial.println(F("[WiFi] ⚠️ WiFi 断开, 尝试重连...")); Serial.printf("[WiFi] 断开! status=%d RSSI=%d\n", s, WiFi.RSSI());
_connectWiFi(); }
} else { // 不手动 reconnect(), 让 WiFi.setAutoReconnect(true) 自动处理
} else if (!_connected) {
_connected = true; _connected = true;
_setLEDMode(2); // 常亮 _setLEDMode(3);
} Serial.printf("[WiFi] 已连接 RSSI=%d\n", WiFi.RSSI());
}
// LED 闪烁处理
if (!_connected) {
uint32_t interval = (_wm.getConfigPortalActive()) ? 300 : 1000;
if (millis() - _ledLastToggle > interval) {
_ledLastToggle = millis();
_ledState = !_ledState;
digitalWrite(LED_WIFI_PIN, _ledState ? HIGH : LOW);
} }
} }
@@ -177,128 +172,433 @@ bool WiFiMgmt::loop() {
// ============================================================ // ============================================================
bool WiFiMgmt::_connectWiFi() { bool WiFiMgmt::_connectWiFi() {
// 检查是否有已保存的配置 if (_config.wifi_ssid[0] == '\0') {
// autoConnect 会先尝试连接, 失败后启动 AP 配网页面 Serial.println(("[WiFi] 无已存WiFi密码"));
_connected = _wm.autoConnect(WIFI_AP_SSID, WIFI_AP_PASSWORD); return false;
if (_connected) {
Serial.println(F("[WiFi] ✅ 已连接!"));
Serial.printf(" WiFi名称: %s\n", WiFi.SSID().c_str());
Serial.printf(" IP地址: %s\n", WiFi.localIP().toString().c_str());
Serial.printf(" MAC地址: %s\n", WiFi.macAddress().c_str());
// 重新加载配置 (确保获取最新值)
// autoConnect 过程中如果有保存, 回调已写入 LittleFS
_loadConfig();
// 同步自定义参数 (如果配置是从 LittleFS 加载的)
_param_mqtt_server->setValue(_config.mqtt_server, sizeof(_config.mqtt_server));
_param_mqtt_port->setValue(_config.mqtt_port, sizeof(_config.mqtt_port));
_param_mqtt_user->setValue(_config.mqtt_user, sizeof(_config.mqtt_user));
_param_mqtt_pass->setValue(_config.mqtt_pass, sizeof(_config.mqtt_pass));
_param_device_id->setValue(_config.device_id, sizeof(_config.device_id));
_param_env_note->setValue(_config.env_note, sizeof(_config.env_note));
Serial.printf(" 设备名: %s\n", _config.device_id);
Serial.printf(" 环境: %s\n", _config.env_note);
Serial.printf(" MQTT服务器: %s:%s\n", _config.mqtt_server, _config.mqtt_port);
} }
Serial.printf("[WiFi] 连接: %s\n", _config.wifi_ssid);
// 崩溃重启后 WiFi 模块可能残留旧状态, 彻底重置
WiFi.disconnect(true);
delay(200);
WiFi.mode(WIFI_STA);
delay(100);
WiFi.begin(_config.wifi_ssid, _config.wifi_pass);
int retry = 0;
while (WiFi.status() != WL_CONNECTED && retry < 40) { // 20s
delay(500); Serial.print("."); yield(); retry++;
}
Serial.println();
_connected = (WiFi.status() == WL_CONNECTED);
Serial.printf("[WiFi] 连接结果: %s status=%d\n", _connected ? "成功" : "失败", WiFi.status());
if (_connected) Serial.printf("[WiFi] SSID=%s IP=%s\n", WiFi.SSID().c_str(), WiFi.localIP().toString().c_str());
return _connected; return _connected;
} }
// ============================================================ bool WiFiMgmt::_tryConnect(const char* ssid, const char* pass) {
// 配网页面 // ★ 先从 AP 模式彻底断开再切 STA和 _connectWiFi() 保持一致
// ============================================================ WiFi.disconnect(true);
delay(200);
void WiFiMgmt::startConfigPortal() { WiFi.mode(WIFI_STA);
Serial.println(F("[WiFi] 🔧 手动启动配网页面...")); delay(100);
_setLEDMode(1); WiFi.persistent(true); // 确保持久化 WiFi 密码到 NVS (断电不丢)
_wm.startConfigPortal(WIFI_AP_SSID, WIFI_AP_PASSWORD); WiFi.begin(ssid, pass);
_connected = (WiFi.status() == WL_CONNECTED); int retry = 0;
if (_connected) { while (WiFi.status() != WL_CONNECTED && retry < 40) { // 20s
_setLEDMode(2); delay(500); Serial.print("."); retry++;
} }
bool ok = (WiFi.status() == WL_CONNECTED);
Serial.printf("\n[WiFi] 连接结果: %s status=%d\n", ok ? "成功!" : "失败", WiFi.status());
return ok;
} }
// ============================================================ // ============================================================
// 重置设置 // 配网 Portal (AP + WebServer)
// ============================================================
void WiFiMgmt::startConfigPortal() {
_setLEDMode(2);
_startPortal();
}
void WiFiMgmt::_startPortal() {
if (_portalActive) return;
WiFi.disconnect(true);
delay(200);
WiFi.mode(WIFI_AP);
WiFi.softAPConfig(IPAddress(192,168,4,1), IPAddress(192,168,4,1), IPAddress(255,255,255,0));
WiFi.softAP(WIFI_AP_SSID, WIFI_AP_PASSWORD);
Serial.printf("[WiFi] AP: %s IP: 192.168.4.1\n", WIFI_AP_SSID);
// DNS 劫持: 所有域名 → 192.168.4.1 → 触发手机强制门户弹窗
_dns = new DNSServer();
_dns->start(53, "*", WiFi.softAPIP());
_server = new WebServer(80);
_server->on("/", HTTP_GET, [this]() { _handleRoot(); });
_server->on("/save", HTTP_POST, [this]() { _handleSave(); });
_server->on("/reset", HTTP_GET, [this]() { _handleReset(); });
_server->onNotFound( [this]() { _handleRoot(); });
_server->begin();
_portalActive = true;
_portalStartTime = millis();
Serial.println(("[WiFi] 手机连 AP 后浏览器打开 http://192.168.4.1"));
_startBLE(); // 同时启动 BLE 配网广播
}
void WiFiMgmt::_stopPortal() {
if (!_portalActive) return;
_stopBLE();
if (_server) { _server->stop(); delete _server; _server = nullptr; }
if (_dns) { _dns->stop(); delete _dns; _dns = nullptr; }
WiFi.softAPdisconnect(true);
_portalActive = false;
}
// ============================================================
// Web 处理器
// ============================================================
void WiFiMgmt::_handleRoot() {
_server->send(200, "text/html; charset=utf-8", _htmlPage());
}
void WiFiMgmt::_handleSave() {
if (_server->hasArg("s")) {
_pendingSSID = _server->arg("s");
_pendingPass = _server->arg("p");
// 持久化 WiFi 密码到 LittleFS
strncpy(_config.wifi_ssid, _pendingSSID.c_str(), sizeof(_config.wifi_ssid)-1);
strncpy(_config.wifi_pass, _pendingPass.c_str(), sizeof(_config.wifi_pass)-1);
if (_server->hasArg("device_id")) strncpy(_config.device_id, _server->arg("device_id").c_str(), sizeof(_config.device_id)-1);
if (_server->hasArg("env_type")) strncpy(_config.env_type, _server->arg("env_type").c_str(), sizeof(_config.env_type)-1);
if (_server->hasArg("env_note")) strncpy(_config.env_note, _server->arg("env_note").c_str(), sizeof(_config.env_note)-1);
if (_server->hasArg("mqtt_server")) strncpy(_config.mqtt_server, _server->arg("mqtt_server").c_str(), sizeof(_config.mqtt_server)-1);
if (_server->hasArg("mqtt_port")) strncpy(_config.mqtt_port, _server->arg("mqtt_port").c_str(), sizeof(_config.mqtt_port)-1);
if (_server->hasArg("mqtt_user")) strncpy(_config.mqtt_user, _server->arg("mqtt_user").c_str(), sizeof(_config.mqtt_user)-1);
if (_server->hasArg("mqtt_pass")) strncpy(_config.mqtt_pass, _server->arg("mqtt_pass").c_str(), sizeof(_config.mqtt_pass)-1);
_saveConfig();
_server->send(200, "text/html; charset=utf-8",
"<!DOCTYPE html><html lang='zh-CN'><head><meta charset='UTF-8'><meta name='viewport' content='width=device-width,initial-scale=1'>"
"<title>配置完成</title><style>body{font-family:sans-serif;background:#f3faf8;display:flex;align-items:center;justify-content:center;height:100vh;margin:0}"
".box{text-align:center;background:#fff;padding:32px;border-radius:16px;box-shadow:0 2px 12px rgba(0,0,0,0.08);max-width:340px}"
"h1{color:#12a889;font-size:20px}.ok{font-size:48px;margin:16px}"
"p{color:#8aa09a;font-size:14px;line-height:1.8}"
"</style></head><body><div class='box'><div class='ok' id='icon'>⏳</div>"
"<h1 id='title'>配置已保存</h1><p id='msg'>设备正在连接 WiFi...<br>等待指示灯<b>常亮</b></p>"
"</div><script>"
"var n=0;setInterval(function(){n++;"
"if(n>8){document.getElementById('icon').textContent='✅';"
"document.getElementById('msg').innerHTML='请切换回您的 WiFi 网络<br><b>指示灯常亮</b>即连接成功';clearInterval()}"
"},2000);</script></body></html>");
_portalStopFlag = true;
return;
}
_handleRoot();
}
void WiFiMgmt::_handleReset() {
_server->send(200, "text/html; charset=utf-8",
"<!DOCTYPE html><html lang='zh-CN'><head><meta charset='UTF-8'><meta name='viewport' content='width=device-width,initial-scale=1'><title>已重置</title>"
"<style>body{font-family:sans-serif;background:#f3faf8;display:flex;align-items:center;justify-content:center;height:100vh;margin:0}"
".box{text-align:center;background:#fff;padding:32px;border-radius:16px;box-shadow:0 2px 12px rgba(0,0,0,0.08)}"
"h1{color:#f53f3f;font-size:18px}p{color:#8aa09a;font-size:14px}</style></head><body><div class='box'><h1>配置已清除</h1><p>设备正在重启...</p></div></body></html>");
delay(500);
_stopPortal();
resetSettings();
}
// ============================================================
// 配网 HTML 页面 (精简版)
// ============================================================
void WiFiMgmt::_updateWifiScan() {
_wifiScanCache = "";
int n = WiFi.scanNetworks(false, true);
for (int i = 0; i < n; i++) {
_wifiScanCache += "<option value='" + WiFi.SSID(i) + "'>" + WiFi.SSID(i) + " (" + String(WiFi.RSSI(i)) + "dBm)</option>";
}
if (n == 0) _wifiScanCache = "<option value=''>未发现 WiFi</option>";
WiFi.scanDelete();
_lastScanTime = millis();
}
String WiFiMgmt::_htmlPage() {
if (_wifiScanCache.length() == 0) _updateWifiScan();
String html;
html.reserve(4096);
html += "<!DOCTYPE html><html lang='zh-CN'><head><meta charset='UTF-8'><meta name='viewport' content='width=device-width,initial-scale=1,user-scalable=no'>";
html += "<meta name='theme-color' content='#12a889'><title>设备配网</title><style>";
html += ":root{--p:#12a889;--bg:#f3faf8;--cb:#fff;--ch:#eef8f5;--cg:#e9fff8;--t:#15201d;--t2:#4e6660;--t3:#8aa09a}";
html += "*{box-sizing:border-box;margin:0;padding:0}body{font-family:-apple-system,BlinkMacSystemFont,'PingFang SC','Microsoft YaHei',sans-serif;background:var(--bg);min-height:100vh;padding:12px 16px 24px;color:var(--t);font-size:14px;line-height:1.5}";
html += ".wrap{max-width:420px;margin:0 auto}.hdr{display:flex;align-items:center;justify-content:center;position:relative;padding:20px 0 16px}";
html += ".hdricon{width:56px;height:56px;border-radius:50%;background:var(--cg);display:flex;align-items:center;justify-content:center;font-size:28px;margin-right:14px;box-shadow:0 4px 12px rgba(18,168,137,.12)}";
html += "h1{font-size:20px;font-weight:700;color:var(--t)}h3{font-size:12px;font-weight:400;color:var(--t2);margin-top:4px}";
html += ".card{background:var(--cb);border-radius:16px;padding:20px;margin-bottom:16px;box-shadow:0 2px 12px rgba(0,0,0,.06)}";
html += ".chdr{display:flex;align-items:center;gap:12px;margin-bottom:18px}.chicon{width:36px;height:36px;border-radius:10px;background:var(--cg);display:flex;align-items:center;justify-content:center;font-size:18px;flex-shrink:0}";
html += ".ctitle{font-size:16px;font-weight:600;color:var(--t)}.csub{font-size:12px;color:var(--t3);margin-top:2px}";
html += ".field{margin-top:14px}.field:first-child{margin-top:0}label{display:block;font-size:14px;font-weight:500;color:var(--t2);margin-bottom:8px}";
html += ".iwrap{display:flex;align-items:center;background:var(--ch);border-radius:10px;border:1px solid transparent;transition:border-color .2s}";
html += ".iicon{padding:0 10px;font-size:16px;color:var(--t3)}input,select{flex:1;width:100%;padding:12px 14px;font-size:14px;border:none;background:transparent;color:var(--t);outline:none}";
html += ".iwrap:focus-within{border-color:var(--p);background:#fff}select{-webkit-appearance:none;padding-right:30px}";
html += ".gate{display:flex;align-items:center;justify-content:space-between;padding:12px 14px;border-radius:12px;background:var(--cg);margin-bottom:14px;font-size:13px}";
html += ".glabel{color:var(--t2)}.gid{color:var(--p);font-weight:600}";
html += ".btns{display:flex;flex-direction:column;gap:12px;margin-top:8px}";
html += "button{width:100%;height:48px;border:none;border-radius:24px;font-size:15px;font-weight:600;color:#fff;background:var(--p);box-shadow:0 2px 6px rgba(18,168,137,.2);cursor:pointer}";
html += "button.danger{background:#fff;color:#f53f3f;border:1px solid #ffd6d6;box-shadow:none}";
html += ".ft{text-align:center;font-size:11px;color:var(--t3);padding:20px 0 6px}";
html += "@media(prefers-color-scheme:dark){body{background:#0f1f1c;color:#e6f0ee}.card{background:#182b27}.iwrap{background:#1f3530}.iwrap:focus-within{background:#243d38}}";
html += "</style></head><body><div class='wrap'>";
// Header + 提示
html += "<div class='hdr'><div class='hdricon'>📡</div><div><h1>观照-五劳无感 设备配网</h1><h3>WiFi 联网与参数配置</h3></div></div>";
html += "<div class='gate' style='justify-content:center'><span style='color:var(--p)'>💡 也可用 <b>ESP-Touch</b> App 一键配网</span></div>";
html += "<form method='POST' action='/save'>";
html += "<div class='card'><div class='chdr'><div class='chicon'>📶</div><div><div class='ctitle'>无线网络</div><div class='csub'>选择 WiFi 或使用 SmartConfig</div></div></div>";
html += "<div class='field'><label>WiFi 名称</label><div class='iwrap'><span class='iicon'>📡</span><select name='s'>";
html += _wifiScanCache;
html += "</select></div></div>";
html += "<div class='field'><label>WiFi 密码</label><div class='iwrap'><span class='iicon'>🔒</span><input type='password' name='p' placeholder='请输入 WiFi 密码'></div></div></div>";
html += "<div class='card'><div class='chdr'><div class='chicon'>⚙️</div><div><div class='ctitle'>设备基础信息</div><div class='csub'>唯一标识与安装位置</div></div></div>";
html += "<div class='field'><label>设备编号</label><div class='iwrap'><span class='iicon'>#</span><input type='text' name='device_id' value='";
html += _config.device_id;
html += "'></div></div>";
html += "<div class='field'><label>环境类型</label><div class='iwrap'><span class='iicon'>🏷️</span><select name='env_type'>";
const char* envTypes[] = {"bedroom","livingroom","bathroom","hallway","balcony","kitchen"};
const char* envLabels[] = {"🛏️ 卧室","🛋️ 客厅","🛁 浴室/卫生间","🚶 走廊","🌿 阳台","🍳 厨房"};
for (int i = 0; i < 6; i++) {
html += "<option value='";
html += envTypes[i];
html += "'";
if (strcmp(_config.env_type, envTypes[i]) == 0 || (strlen(_config.env_type) == 0 && i == 0))
html += " selected";
html += ">";
html += envLabels[i];
html += "</option>";
}
html += "</select></div></div>";
html += "<div class='field'><label>位置备注</label><div class='iwrap'><span class='iicon'>🏠</span><input type='text' name='env_note' placeholder='如: 主卧 / 客厅 / 老人房' value='";
html += _config.env_note;
html += "'></div></div></div>";
html += "<input type='hidden' name='mqtt_server' value='";
html += _config.mqtt_server;
html += "'><input type='hidden' name='mqtt_port' value='";
html += _config.mqtt_port;
html += "'><input type='hidden' name='mqtt_user' value='";
html += _config.mqtt_user;
html += "'><input type='hidden' name='mqtt_pass' value='";
html += _config.mqtt_pass;
html += "'>";
html += "<div class='btns'><button type='submit'>保存配置并重启连接</button></div></form>";
html += "<button class='danger' onclick=\"if(confirm('确定恢复出厂?'))location.href='/reset'\">恢复出厂配置</button>";
html += "<div class='ft'>Health Monitor · Device Provisioning</div></div></body></html>";
return html;
}
// ============================================================
// BLE 蓝牙配网
// ============================================================
void WiFiMgmt::_startBLE() {
// 如果 BLE 栈已在运行, 仅重启广播 (避免 deinit→init 失败)
if (_bleServer != nullptr) {
if (_bleAdv) {
// 重设广告数据 (stop 后 start 需要重新配置)
BLEAdvertisementData advData;
advData.setCompleteServices(BLEUUID("4fafc201-1fb5-459e-8fcc-c5c9c331914b"));
advData.setShortName("HealthMon");
_bleAdv->setAdvertisementData(advData);
_bleAdv->setScanResponse(false);
_bleAdv->start();
}
Serial.println("[BLE] ✅ 广播已重开");
return;
}
BLEDevice::init("HealthMonitor");
_bleServer = BLEDevice::createServer();
BLEService* svc = _bleServer->createService("4fafc201-1fb5-459e-8fcc-c5c9c331914b");
// 特征1: 设备信息 (READ) — 小程序连接后读取设备编号等
_bleInfoChar = svc->createCharacteristic(
"4fafc202-1fb5-459e-8fcc-c5c9c331914b",
BLECharacteristic::PROPERTY_READ
);
// 设备信息 JSON (最多 512 字节)
JsonDocument info;
info["device_id"] = _config.device_id;
info["mqtt_server"] = _config.mqtt_server;
info["mqtt_port"] = _config.mqtt_port;
info["fw_version"] = FW_VERSION;
String infoJson;
serializeJson(info, infoJson);
_bleInfoChar->setValue(infoJson.c_str());
// 特征2: 配网写入 (WRITE + NOTIFY) — 小程序发配网数据, 设备回复
_bleChar = svc->createCharacteristic(
"4fafc203-1fb5-459e-8fcc-c5c9c331914b",
BLECharacteristic::PROPERTY_WRITE | BLECharacteristic::PROPERTY_NOTIFY
);
_bleChar->setCallbacks(new BLEProvisionCB());
_bleChar->addDescriptor(new BLE2902()); // 启用 notify
_bleChar->setValue(""); // 初始为空
svc->start();
_bleAdv = _bleServer->getAdvertising();
BLEAdvertisementData advData;
advData.setCompleteServices(BLEUUID("4fafc201-1fb5-459e-8fcc-c5c9c331914b"));
advData.setShortName("HealthMon");
_bleAdv->setAdvertisementData(advData);
_bleAdv->setScanResponse(false); // 不需要 scan response, 全在主包
_bleAdv->start();
Serial.printf("[BLE] ✅ 配网广播: HealthMonitor device_id=%s\n", _config.device_id);
}
void WiFiMgmt::_stopBLE() {
if (!_bleServer) return;
// ★ 核心修复: 先断开所有 BLE 客户端连接,
// 否则手机端缓存连接状态, 下次扫描不到设备
size_t connCount = _bleServer->getConnectedCount();
if (connCount > 0) {
Serial.printf("[BLE] 断开 %d 个已连接客户端\n", connCount);
for (size_t i = 0; i < connCount; i++) {
_bleServer->disconnect(i);
}
delay(300); // 等待 BLE 协议栈完成断开流程
}
if (_bleAdv) { _bleAdv->stop(); }
Serial.println("[BLE] 广播已暂停");
}
// BLE 线程安全入队 (仅暂存, 不在 BTC_TASK 回调里处理)
void WiFiMgmt::enqueueBLEJson(const char* json) {
_blePendingJson = json;
_blePendingFlag = true;
}
// 在 loop() 上下文中处理 BLE 配网数据 (安全 — 不在 BTC_TASK 栈中)
void WiFiMgmt::_processBLEPending() {
if (!_blePendingFlag) return;
_blePendingFlag = false;
Serial.printf("[BLE] loop 处理配网: %s\n", _blePendingJson.c_str());
handleBLEProvision(_blePendingJson.c_str());
_blePendingJson = "";
}
void WiFiMgmt::handleBLEProvision(const char* json) {
JsonDocument doc;
DeserializationError err = deserializeJson(doc, json);
if (err) {
Serial.printf("[BLE] JSON 解析失败: %s\n", err.c_str());
if (_bleChar) { _bleChar->setValue("ERR_JSON"); _bleChar->notify(); }
return;
}
// 提取 WiFi 凭证
const char* ssid = doc["s"] | "";
const char* pass = doc["p"] | "";
if (strlen(ssid) == 0) {
Serial.println("[BLE] 未提供 WiFi SSID");
if (_bleChar) { _bleChar->setValue("ERR_NO_SSID"); _bleChar->notify(); }
return;
}
_pendingSSID = ssid;
_pendingPass = pass;
// 保存到 LittleFS (和 Web 配网完全相同的逻辑)
strncpy(_config.wifi_ssid, ssid, sizeof(_config.wifi_ssid) - 1);
strncpy(_config.wifi_pass, pass, sizeof(_config.wifi_pass) - 1);
if (doc.containsKey("device_id")) strncpy(_config.device_id, doc["device_id"] | "", sizeof(_config.device_id) - 1);
if (doc.containsKey("env_note")) strncpy(_config.env_note, doc["env_note"] | "", sizeof(_config.env_note) - 1);
if (doc.containsKey("env_type")) strncpy(_config.env_type, doc["env_type"] | "", sizeof(_config.env_type) - 1);
if (doc.containsKey("mqtt_server")) strncpy(_config.mqtt_server, doc["mqtt_server"] | "", sizeof(_config.mqtt_server) - 1);
if (doc.containsKey("mqtt_port")) strncpy(_config.mqtt_port, doc["mqtt_port"] | "", sizeof(_config.mqtt_port) - 1);
if (doc.containsKey("mqtt_user")) strncpy(_config.mqtt_user, doc["mqtt_user"] | "", sizeof(_config.mqtt_user) - 1);
if (doc.containsKey("mqtt_pass")) strncpy(_config.mqtt_pass, doc["mqtt_pass"] | "", sizeof(_config.mqtt_pass) - 1);
_saveConfig();
_portalStopFlag = true; // 触发延迟关闭 AP+BLE → 连接 WiFi
Serial.printf("[BLE] ✅ 配网完成, SSID=%s\n", ssid);
// 回复 OK
if (_bleChar) { _bleChar->setValue("OK"); _bleChar->notify(); }
delay(100);
}
// ============================================================
// 重置
// ============================================================ // ============================================================
void WiFiMgmt::resetSettings() { void WiFiMgmt::resetSettings() {
Serial.println(F("[WiFi] 清除所有配置...")); Serial.println(("[WiFi] 清除所有配置..."));
// 删除 LittleFS 中的配置文件
if (LittleFS.exists(CONFIG_FILE_WIFI)) LittleFS.remove(CONFIG_FILE_WIFI); if (LittleFS.exists(CONFIG_FILE_WIFI)) LittleFS.remove(CONFIG_FILE_WIFI);
if (LittleFS.exists(CONFIG_FILE_MQTT)) LittleFS.remove(CONFIG_FILE_MQTT); if (LittleFS.exists(CONFIG_FILE_MQTT)) LittleFS.remove(CONFIG_FILE_MQTT);
if (LittleFS.exists(CONFIG_FILE_DEVICE)) LittleFS.remove(CONFIG_FILE_DEVICE); if (LittleFS.exists(CONFIG_FILE_DEVICE)) LittleFS.remove(CONFIG_FILE_DEVICE);
WiFi.disconnect(true, true);
// 清除 WiFiManager 的配置 for (int i = 0; i < 10; i++) { digitalWrite(LED_WIFI_PIN, !digitalRead(LED_WIFI_PIN)); delay(100); }
_wm.resetSettings(); Serial.println(("[WiFi] 正在重启..."));
// LED 快闪提示
for (int i = 0; i < 10; i++) {
digitalWrite(LED_WIFI_PIN, !digitalRead(LED_WIFI_PIN));
delay(100);
}
Serial.println(F("[WiFi] 🔄 正在重启..."));
delay(500); delay(500);
ESP.restart(); ESP.restart();
} }
// ============================================================ // ============================================================
// 按钮长按检测 // 按钮
// ============================================================ // ============================================================
bool WiFiMgmt::_checkButtonLongPress() { bool WiFiMgmt::_checkButtonLongPress() {
bool pressed = (digitalRead(CONFIG_BUTTON_PIN) == LOW); bool pressed = (digitalRead(CONFIG_BUTTON_PIN) == LOW);
if (pressed && !_buttonPressed) { if (pressed && !_buttonPressed) {
// 按下 _buttonPressed = true; _buttonPressTime = millis();
_buttonPressed = true;
_buttonPressTime = millis();
} else if (!pressed && _buttonPressed) { } else if (!pressed && _buttonPressed) {
// 松开
_buttonPressed = false; _buttonPressed = false;
if (millis() - _buttonPressTime >= BUTTON_LONG_PRESS_MS) { if (millis() - _buttonPressTime >= BUTTON_LONG_PRESS_MS) return true;
return true; // 长按确认
} }
}
return false; return false;
} }
// ============================================================ // ============================================================
// LittleFS 配置持久化 // LittleFS
// ============================================================ // ============================================================
bool WiFiMgmt::_loadConfig() { bool WiFiMgmt::_loadConfig() {
if (!LittleFS.exists(CONFIG_FILE_DEVICE)) { if (!LittleFS.exists(CONFIG_FILE_DEVICE)) return false;
return false;
}
File file = LittleFS.open(CONFIG_FILE_DEVICE, "r"); File file = LittleFS.open(CONFIG_FILE_DEVICE, "r");
if (!file) return false; if (!file) return false;
JsonDocument doc; JsonDocument doc;
DeserializationError err = deserializeJson(doc, file); DeserializationError err = deserializeJson(doc, file);
file.close(); file.close();
if (err) return false;
if (err) { strncpy(_config.mqtt_server, doc["mqtt_server"] | MQTT_DEFAULT_SERVER, sizeof(_config.mqtt_server)-1);
Serial.printf("[WiFi] JSON 解析失败: %s\n", err.c_str()); strncpy(_config.mqtt_port, doc["mqtt_port"] | MQTT_DEFAULT_PORT_STR, sizeof(_config.mqtt_port)-1);
return false; strncpy(_config.mqtt_user, doc["mqtt_user"] | MQTT_DEFAULT_USER, sizeof(_config.mqtt_user)-1);
} strncpy(_config.mqtt_pass, doc["mqtt_pass"] | MQTT_DEFAULT_PASS, sizeof(_config.mqtt_pass)-1);
strncpy(_config.device_id, doc["device_id"] | MQTT_DEFAULT_DEVICE, sizeof(_config.device_id)-1);
strncpy(_config.mqtt_server, doc["mqtt_server"] | MQTT_DEFAULT_SERVER, sizeof(_config.mqtt_server) - 1); strncpy(_config.env_note, doc["env_note"] | MQTT_DEFAULT_ENV, sizeof(_config.env_note)-1);
strncpy(_config.mqtt_port, doc["mqtt_port"] | MQTT_DEFAULT_PORT_STR, sizeof(_config.mqtt_port) - 1); strncpy(_config.env_type, doc["env_type"] | "bedroom", sizeof(_config.env_type)-1);
strncpy(_config.mqtt_user, doc["mqtt_user"] | MQTT_DEFAULT_USER, sizeof(_config.mqtt_user) - 1); strncpy(_config.wifi_ssid, doc["wifi_ssid"] | "", sizeof(_config.wifi_ssid)-1);
strncpy(_config.mqtt_pass, doc["mqtt_pass"] | MQTT_DEFAULT_PASS, sizeof(_config.mqtt_pass) - 1); strncpy(_config.wifi_pass, doc["wifi_pass"] | "", sizeof(_config.wifi_pass)-1);
strncpy(_config.device_id, doc["device_id"] | MQTT_DEFAULT_DEVICE, sizeof(_config.device_id) - 1); Serial.printf("[WiFi] 配置加载, 已存WiFi: %s\n", _config.wifi_ssid[0] ? _config.wifi_ssid : "(空)");
strncpy(_config.env_note, doc["env_note"] | MQTT_DEFAULT_ENV, sizeof(_config.env_note) - 1);
Serial.println(F("[WiFi] ✅ 设备配置已加载"));
Serial.printf(" 设备名: %s, 环境: %s\n", _config.device_id, _config.env_note);
return true; return true;
} }
@@ -310,103 +610,45 @@ bool WiFiMgmt::_saveConfig() {
doc["mqtt_pass"] = _config.mqtt_pass; doc["mqtt_pass"] = _config.mqtt_pass;
doc["device_id"] = _config.device_id; doc["device_id"] = _config.device_id;
doc["env_note"] = _config.env_note; doc["env_note"] = _config.env_note;
doc["env_type"] = _config.env_type;
doc["wifi_ssid"] = _config.wifi_ssid;
doc["wifi_pass"] = _config.wifi_pass;
File file = LittleFS.open(CONFIG_FILE_DEVICE, "w"); File file = LittleFS.open(CONFIG_FILE_DEVICE, "w");
if (!file) { if (!file) return false;
Serial.println(F("[WiFi] ❌ 无法创建配置文件")); if (serializeJson(doc, file) == 0) { file.close(); return false; }
return false;
}
if (serializeJson(doc, file) == 0) {
Serial.println(F("[WiFi] ❌ 配置写入失败"));
file.close(); file.close();
return false;
}
file.close();
Serial.println(F("[WiFi] ✅ 设备配置已保存"));
return true; return true;
} }
// ============================================================ // ============================================================
// WiFiManager 保存参数回调 // Getter
// ============================================================ // ============================================================
// ============================================================ bool WiFiMgmt::isConnected() const { return _connected && (WiFi.status() == WL_CONNECTED); }
// 静态回调函数 (WiFiManager 要求 C 函数指针) bool WiFiMgmt::justProvisioned() { bool v = _provisionSuccess; _provisionSuccess = false; return v; }
// ============================================================ DeviceConfig WiFiMgmt::getConfig() const { return _config; }
const char* WiFiMgmt::getDeviceId() const { return _config.device_id; }
void WiFiMgmt::_apCallback(WiFiManager* wm) { const char* WiFiMgmt::getEnvNote() const { return _config.env_note; }
Serial.println(F("[WiFi] 🔧 进入配网模式"));
Serial.printf(" AP WiFi名称: %s\n", WIFI_AP_SSID);
Serial.printf(" AP 密码: %s\n", WIFI_AP_PASSWORD);
Serial.println(F(" 手机连接此 WiFi 即可打开配网页面 (自动弹出)"));
}
void WiFiMgmt::_saveParamsCallback() {
if (!_instance) return;
Serial.println(F("[WiFi] 💾 参数已保存"));
// 从 WiFiManager 参数中读取值
strncpy(_instance->_config.mqtt_server, _instance->_param_mqtt_server->getValue(),
sizeof(_instance->_config.mqtt_server) - 1);
strncpy(_instance->_config.mqtt_port, _instance->_param_mqtt_port->getValue(),
sizeof(_instance->_config.mqtt_port) - 1);
strncpy(_instance->_config.mqtt_user, _instance->_param_mqtt_user->getValue(),
sizeof(_instance->_config.mqtt_user) - 1);
strncpy(_instance->_config.mqtt_pass, _instance->_param_mqtt_pass->getValue(),
sizeof(_instance->_config.mqtt_pass) - 1);
strncpy(_instance->_config.device_id, _instance->_param_device_id->getValue(),
sizeof(_instance->_config.device_id) - 1);
strncpy(_instance->_config.env_note, _instance->_param_env_note->getValue(),
sizeof(_instance->_config.env_note) - 1);
_instance->_saveConfig();
Serial.println(F("[WiFi] 📋 新配置:"));
Serial.printf(" MQTT: %s:%s\n",
_instance->_config.mqtt_server,
_instance->_config.mqtt_port);
Serial.printf(" 设备: %s\n", _instance->_config.device_id);
Serial.printf(" 环境: %s\n", _instance->_config.env_note);
}
// ============================================================ // ============================================================
// Getter 方法 // LED
// ============================================================ // ============================================================
bool WiFiMgmt::isConnected() const { void WiFiMgmt::_setLEDMode(uint8_t mode) { _ledMode = mode; }
return _connected && (WiFi.status() == WL_CONNECTED);
}
DeviceConfig WiFiMgmt::getConfig() const { void WiFiMgmt::_ledTaskFunc(void* param) {
return _config; const TickType_t fastMs = pdMS_TO_TICKS(300);
} const TickType_t slowMs = pdMS_TO_TICKS(1000);
while (true) {
const char* WiFiMgmt::getDeviceId() const { if (!_instance) { vTaskDelay(slowMs); continue; }
return _config.device_id; uint8_t mode = _instance->_ledMode;
}
const char* WiFiMgmt::getEnvNote() const {
return _config.env_note;
}
// ============================================================
// LED 模式控制
// ============================================================
void WiFiMgmt::_setLEDMode(uint8_t mode) {
// mode 由 loop() 中的闪烁逻辑处理
// 0=慢闪 1=快闪 2=常亮
switch (mode) { switch (mode) {
case 0: case 0: digitalWrite(LED_WIFI_PIN, LOW); vTaskDelay(slowMs); break;
case 1: case 1: digitalWrite(LED_WIFI_PIN, HIGH); vTaskDelay(slowMs);
// 闪烁由 loop 处理 digitalWrite(LED_WIFI_PIN, LOW); vTaskDelay(slowMs); break;
break; case 2: digitalWrite(LED_WIFI_PIN, HIGH); vTaskDelay(fastMs);
case 2: digitalWrite(LED_WIFI_PIN, LOW); vTaskDelay(fastMs); break;
digitalWrite(LED_WIFI_PIN, HIGH); case 3: digitalWrite(LED_WIFI_PIN, HIGH); vTaskDelay(slowMs); break;
_ledState = true; }
break;
} }
} }