diff --git a/RemoteMapper-Flasher/bin/RemoteMapper_ESP32S3_N16R8_app.bin b/RemoteMapper-Flasher/bin/RemoteMapper_ESP32S3_N16R8_app.bin index fcc3b4b..12309b2 100644 Binary files a/RemoteMapper-Flasher/bin/RemoteMapper_ESP32S3_N16R8_app.bin and b/RemoteMapper-Flasher/bin/RemoteMapper_ESP32S3_N16R8_app.bin differ diff --git a/RemoteMapper-Flasher/bin/RemoteMapper_ESP32S3_N16R8_full.bin b/RemoteMapper-Flasher/bin/RemoteMapper_ESP32S3_N16R8_full.bin index 02c548e..eb48d1b 100644 Binary files a/RemoteMapper-Flasher/bin/RemoteMapper_ESP32S3_N16R8_full.bin and b/RemoteMapper-Flasher/bin/RemoteMapper_ESP32S3_N16R8_full.bin differ diff --git a/RemoteMapper-Flasher/bin/RemoteMapper_ESP32S3_N4R2_app.bin b/RemoteMapper-Flasher/bin/RemoteMapper_ESP32S3_N4R2_app.bin index fcb9346..5c101c2 100644 Binary files a/RemoteMapper-Flasher/bin/RemoteMapper_ESP32S3_N4R2_app.bin and b/RemoteMapper-Flasher/bin/RemoteMapper_ESP32S3_N4R2_app.bin differ diff --git a/RemoteMapper-Flasher/bin/RemoteMapper_ESP32S3_N4R2_full.bin b/RemoteMapper-Flasher/bin/RemoteMapper_ESP32S3_N4R2_full.bin index f8b1f03..2d70262 100644 Binary files a/RemoteMapper-Flasher/bin/RemoteMapper_ESP32S3_N4R2_full.bin and b/RemoteMapper-Flasher/bin/RemoteMapper_ESP32S3_N4R2_full.bin differ diff --git a/RemoteMapper-Flasher/bin/RemoteMapper_ESP32S3_N8R2_app.bin b/RemoteMapper-Flasher/bin/RemoteMapper_ESP32S3_N8R2_app.bin index 43b59c2..7a541d6 100644 Binary files a/RemoteMapper-Flasher/bin/RemoteMapper_ESP32S3_N8R2_app.bin and b/RemoteMapper-Flasher/bin/RemoteMapper_ESP32S3_N8R2_app.bin differ diff --git a/RemoteMapper-Flasher/bin/RemoteMapper_ESP32S3_N8R2_full.bin b/RemoteMapper-Flasher/bin/RemoteMapper_ESP32S3_N8R2_full.bin index 070a267..cb17332 100644 Binary files a/RemoteMapper-Flasher/bin/RemoteMapper_ESP32S3_N8R2_full.bin and b/RemoteMapper-Flasher/bin/RemoteMapper_ESP32S3_N8R2_full.bin differ diff --git a/src/cli/cli_manager.cpp b/src/cli/cli_manager.cpp index d824412..e78f273 100644 --- a/src/cli/cli_manager.cpp +++ b/src/cli/cli_manager.cpp @@ -1,20 +1,118 @@ #include "cli_manager.h" #include "version.h" +#include "wifi/wifi_manager.h" +#include "log/app_log.h" #include "ble/ble_remote_client.h" #include "audio/audio_pipeline.h" #include "keymap/key_state_machine.h" #include #include +#include + +#if !ARDUINO_USB_CDC_ON_BOOT +extern USBCDC USBSerial; +#endif extern key_mapper_engine_t g_key_engine; static String s_input_buffer = ""; +static void handle_command(const String& line); + +static void cli_write_line(const String& line) { + Serial.println(line); +#if !ARDUINO_USB_CDC_ON_BOOT + if (USBSerial) { + USBSerial.println(line); + } +#endif +} + +static void cli_feed_char(char c) { + if (c == '\r' || c == '\n') { + if (s_input_buffer.length() > 0) { + handle_command(s_input_buffer); + s_input_buffer = ""; + } + } else { + s_input_buffer += c; + if (s_input_buffer.length() > 250) { + s_input_buffer = ""; + } + } +} + +static void handle_wifi_command(const String& arg) { + bool want_on = arg.equalsIgnoreCase("on"); + bool want_off = arg.equalsIgnoreCase("off"); + if (want_on || want_off) { + if (wifi_manager_get_enabled() == want_on) { + cli_write_line(want_on + ? "{\"status\":\"nochange\",\"wifi_enabled\":true}" + : "{\"status\":\"nochange\",\"wifi_enabled\":false}"); + return; + } + wifi_manager_set_enabled(want_on); + cli_write_line(want_on + ? "{\"status\":\"ok\",\"wifi_enabled\":true,\"rebooting\":true}" + : "{\"status\":\"ok\",\"wifi_enabled\":false,\"rebooting\":true}"); + cli_write_line(want_on + ? "Wi-Fi 将在重启后开启(AP 热点 RemoteMapper-AP 恢复)..." + : "Wi-Fi 将在重启后关闭(后台改用 USB CDC/UART 的 'wifi on' 恢复)..."); + delay(300); + ESP.restart(); + } else { + JsonDocument doc; + doc["wifi_enabled"] = wifi_manager_get_enabled(); + doc["ap_running"] = wifi_manager_is_ap_running(); + doc["ap_ip"] = wifi_manager_get_ap_ip(); + doc["sta_connected"] = wifi_manager_is_sta_connected(); + doc["sta_ip"] = wifi_manager_get_sta_ip(); + String out; + serializeJson(doc, out); + cli_write_line(out); + } +} + +static void handle_log_command(const String& arg) { + if (arg.equalsIgnoreCase("on")) { + app_log_set_cdc_enabled(true); + cli_write_line("{\"status\":\"ok\",\"cdc_log_enabled\":true}"); + cli_write_line("CDC 日志镜像已开启(注意:语音会话期间日志较密集)"); + } else if (arg.equalsIgnoreCase("off")) { + app_log_set_cdc_enabled(false); + cli_write_line("{\"status\":\"ok\",\"cdc_log_enabled\":false}"); + cli_write_line("CDC 日志镜像已关闭(UART 日志不受影响)"); + } else { + JsonDocument doc; + doc["cdc_log_enabled"] = app_log_get_cdc_enabled(); + String out; + serializeJson(doc, out); + cli_write_line(out); + } +} + static void handle_command(const String& line) { String cmd = line; cmd.trim(); if (cmd.length() == 0) return; + // Split into "head [tail]" so wifi on/off/status can share one dispatcher + int sp = cmd.indexOf(' '); + String head = (sp < 0) ? cmd : cmd.substring(0, sp); + String tail = (sp < 0) ? String("") : cmd.substring(sp + 1); + tail.trim(); + + if (head.equalsIgnoreCase("wifi")) { + handle_wifi_command(tail); + return; + } + + if (head.equalsIgnoreCase("log")) { + handle_log_command(tail); + return; + } + if (cmd.equalsIgnoreCase("status") || cmd.equalsIgnoreCase("info")) { JsonDocument doc; doc["firmware"] = FIRMWARE_NAME; @@ -26,28 +124,33 @@ static void handle_command(const String& line) { doc["samples_pushed"] = g_audio_pipeline.total_samples_pushed; doc["free_heap"] = ESP.getFreeHeap(); doc["free_psram"] = ESP.getFreePsram(); + doc["wifi_enabled"] = wifi_manager_get_enabled(); String out; serializeJson(doc, out); - Serial.println(out); + cli_write_line(out); } else if (cmd.equalsIgnoreCase("reconnect")) { - Serial.println("{\"status\":\"reconnecting\"}"); + cli_write_line("{\"status\":\"reconnecting\"}"); ble_remote_trigger_reconnect(); } else if (cmd.equalsIgnoreCase("reset_keys")) { key_engine_load_defaults(&g_key_engine); - Serial.println("{\"status\":\"keymap_reset_to_defaults\"}"); + cli_write_line("{\"status\":\"keymap_reset_to_defaults\"}"); } else if (cmd.equalsIgnoreCase("help")) { - Serial.println("Commands:"); - Serial.println(" status - Display system info & runtime statistics (JSON)"); - Serial.println(" reconnect - Trigger BLE remote re-scan"); - Serial.println(" reset_keys - Reset key bindings to factory defaults"); - Serial.println(" help - Show available commands"); + cli_write_line("Commands:"); + cli_write_line(" status - Display system info & runtime statistics (JSON)"); + cli_write_line(" reconnect - Trigger BLE remote re-scan"); + cli_write_line(" reset_keys - Reset key bindings to factory defaults"); + cli_write_line(" wifi on|off - Enable/disable the whole Wi-Fi radio (persisted)"); + cli_write_line(" wifi status - Show Wi-Fi radio & connection status (JSON)"); + cli_write_line(" log on|off - Mirror full logs to USB CDC (default: off)"); + cli_write_line(" log status - Show CDC log mirror state (JSON)"); + cli_write_line(" help - Show available commands"); } else { - Serial.println("{\"error\":\"unknown_command\",\"hint\":\"type help\"}"); + cli_write_line("{\"error\":\"unknown_command\",\"hint\":\"type help\"}"); } } @@ -60,18 +163,16 @@ void cli_manager_init(void) { void cli_manager_task(void) { while (Serial.available() > 0) { char c = (char)Serial.read(); - if (c == '\r' || c == '\n') { - if (s_input_buffer.length() > 0) { - handle_command(s_input_buffer); - s_input_buffer = ""; - } - } else { - s_input_buffer += c; - if (s_input_buffer.length() > 250) { - s_input_buffer = ""; - } + cli_feed_char(c); + } +#if !ARDUINO_USB_CDC_ON_BOOT + if (USBSerial) { + while (USBSerial.available() > 0) { + char c = (char)USBSerial.read(); + cli_feed_char(c); } } +#endif } } // extern "C" diff --git a/src/log/app_log.cpp b/src/log/app_log.cpp index 4ee2aee..2d51a61 100644 --- a/src/log/app_log.cpp +++ b/src/log/app_log.cpp @@ -1,9 +1,16 @@ #include "app_log.h" #include +#include #include #include #include +#if !ARDUINO_USB_CDC_ON_BOOT +extern USBCDC USBSerial; +#endif + +static bool s_cdc_log_enabled = false; + #define STATIC_LOG_LINES 250 #define LOG_LINE_MAX_LEN 160 @@ -36,6 +43,11 @@ void app_log(const char* tag, const char* format, ...) { if (Serial) { Serial.println(full_line); } +#if !ARDUINO_USB_CDC_ON_BOOT + if (s_cdc_log_enabled && USBSerial) { + USBSerial.println(full_line); + } +#endif // 2. Store to circular buffer with spinlock protection taskENTER_CRITICAL(&s_log_mux); @@ -71,3 +83,11 @@ void app_log_clear(void) { s_log_count = 0; taskEXIT_CRITICAL(&s_log_mux); } + +void app_log_set_cdc_enabled(bool enabled) { + s_cdc_log_enabled = enabled; +} + +bool app_log_get_cdc_enabled(void) { + return s_cdc_log_enabled; +} diff --git a/src/log/app_log.h b/src/log/app_log.h index 6b51f86..5102cf8 100644 --- a/src/log/app_log.h +++ b/src/log/app_log.h @@ -12,6 +12,8 @@ void app_log_init(void); void app_log(const char* tag, const char* format, ...); String app_log_get_json(void); void app_log_clear(void); +void app_log_set_cdc_enabled(bool enabled); +bool app_log_get_cdc_enabled(void); #ifdef __cplusplus } diff --git a/src/main.cpp b/src/main.cpp index bf0cb89..010eb8b 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -69,8 +69,14 @@ void setup() { // 6. Initialize Wi-Fi AP + STA & Captive Portal wifi_manager_init(); - // 7. Initialize Embedded Web Server & REST APIs - web_server_init(); + // 7. Initialize Embedded Web Server & REST APIs (Wi-Fi only). + // lwIP/esp_netif are initialized inside the Wi-Fi library; starting a TCP + // server while Wi-Fi is disabled triggers a lwIP assert and reboots the chip. + if (wifi_manager_get_enabled()) { + web_server_init(); + } else { + app_log("SYSTEM", "Wi-Fi disabled: web server not started"); + } // 8. Launch BLE Central Task pinned to Core 0 xTaskCreatePinnedToCore( @@ -83,7 +89,11 @@ void setup() { TASK_CORE_BLE ); - app_log("SYSTEM", "System initialization complete. Web available at http://192.168.4.1 or http://remotemapper.local"); + if (wifi_manager_get_enabled()) { + app_log("SYSTEM", "System initialization complete. Web available at http://192.168.4.1 or http://remotemapper.local"); + } else { + app_log("SYSTEM", "System initialization complete. Wi-Fi radio off (use CDC/UART: 'wifi on')"); + } } void loop() { @@ -95,8 +105,10 @@ void loop() { // 2. Service Wi-Fi & DNS tasks wifi_manager_task(); - // 3. Service HTTP Web Server - web_server_task(); + // 3. Service HTTP Web Server (only when Wi-Fi is enabled) + if (wifi_manager_get_enabled()) { + web_server_task(); + } // 4. Service Serial / WebSerial CLI commands cli_manager_task(); diff --git a/src/nvs/nvs_manager.cpp b/src/nvs/nvs_manager.cpp index c04c54e..f2a70ae 100644 --- a/src/nvs/nvs_manager.cpp +++ b/src/nvs/nvs_manager.cpp @@ -116,6 +116,7 @@ String nvs_manager_dump_json(void) { if (prefs.isKey("sta_pass")) wifiObj["sta_pass"] = prefs.getString("sta_pass", ""); if (prefs.isKey("ap_ssid")) wifiObj["ap_ssid"] = prefs.getString("ap_ssid", ""); if (prefs.isKey("ap_pass")) wifiObj["ap_pass"] = prefs.getString("ap_pass", ""); + if (prefs.isKey("wifi_enabled")) wifiObj["wifi_enabled"] = prefs.getBool("wifi_enabled", true); prefs.end(); } if (prefs.begin("ble_conf", true)) { diff --git a/src/web/web_server.cpp b/src/web/web_server.cpp index e7e1ad3..bce284d 100644 --- a/src/web/web_server.cpp +++ b/src/web/web_server.cpp @@ -33,6 +33,7 @@ static void handle_status() { doc["sta_ip"] = wifi_manager_get_sta_ip(); doc["sta_connected"] = wifi_manager_is_sta_connected(); doc["ap_ssid"] = AP_SSID; + doc["wifi_enabled"] = wifi_manager_get_enabled(); String ap_pass = wifi_manager_get_ap_pass(); doc["ap_secured"] = (ap_pass.length() >= 8); doc["ap_pass"] = ap_pass; diff --git a/src/wifi/wifi_manager.cpp b/src/wifi/wifi_manager.cpp index d02b47a..3c606a2 100644 --- a/src/wifi/wifi_manager.cpp +++ b/src/wifi/wifi_manager.cpp @@ -16,17 +16,15 @@ static IPAddress s_ap_netmask(255, 255, 255, 0); static bool s_sta_configured = false; static uint32_t s_last_sta_check = 0; +static bool s_wifi_enabled = true; +static bool s_ap_running = false; +static bool s_mdns_started = false; -void wifi_manager_init(void) { - s_prefs.begin("wifi_conf", false); - String sta_ssid = s_prefs.getString("ssid", ""); - String sta_pass = s_prefs.getString("pass", ""); - String ap_pass = s_prefs.getString("ap_pass", ""); - - // Set Wi-Fi Mode - WiFi.mode(WIFI_AP_STA); - - // 1. Configure and start AP Mode +static void wifi_start_ap(const String& ap_pass) { + if (s_ap_running) { + s_dns_server.stop(); + WiFi.softAPdisconnect(false); + } WiFi.softAPConfig(s_ap_ip, s_ap_ip, s_ap_netmask); if (ap_pass.length() >= 8) { WiFi.softAP(AP_SSID, ap_pass.c_str()); @@ -35,12 +33,23 @@ void wifi_manager_init(void) { WiFi.softAP(AP_SSID, ""); app_log("WIFI", "AP Started: %s (Open Network, IP: 192.168.4.1)", AP_SSID); } - - // 2. Start Captive Portal DNS s_dns_server.setErrorReplyCode(DNSReplyCode::NoError); s_dns_server.start(DNS_PORT, "*", s_ap_ip); + s_ap_running = true; +} - // 3. Connect to Home Wi-Fi if saved +static void wifi_apply_config(void) { + String sta_ssid = s_prefs.getString("ssid", ""); + String sta_pass = s_prefs.getString("pass", ""); + String ap_pass = s_prefs.getString("ap_pass", ""); + + // Set Wi-Fi Mode (AP + STA) + WiFi.mode(WIFI_AP_STA); + + // 1. Configure and start AP Mode + wifi_start_ap(ap_pass); + + // 2. Connect to Home Wi-Fi if saved if (sta_ssid.length() > 0) { s_sta_configured = true; app_log("WIFI", "Connecting to Home Wi-Fi: %s...", sta_ssid.c_str()); @@ -49,15 +58,59 @@ void wifi_manager_init(void) { app_log("WIFI", "No Home Wi-Fi configured, running in AP Setup mode"); } - // 4. Start mDNS - if (MDNS.begin(MDNS_HOSTNAME)) { + // 3. Start mDNS (once per boot) + if (!s_mdns_started && MDNS.begin(MDNS_HOSTNAME)) { MDNS.addService("http", "tcp", 80); app_log("WIFI", "mDNS responder started: http://%s.local", MDNS_HOSTNAME); + s_mdns_started = true; } } +void wifi_manager_init(void) { + s_prefs.begin("wifi_conf", false); + s_wifi_enabled = s_prefs.getBool("wifi_enabled", true); + + if (!s_wifi_enabled) { + // Never touch the WiFi stack when disabled: de-initializing WiFi before BLE + // starts (or tearing it down at runtime) breaks 2.4GHz coexistence and can + // stop the BLE link from coming up. The radio simply stays uninitialized. + app_log("WIFI", "Wi-Fi disabled by config; radio left uninitialized (USB CDC/UART: 'wifi on')"); + return; + } + + // 1. Start AP + STA + mDNS + wifi_apply_config(); +} + +bool wifi_manager_get_enabled(void) { + return s_wifi_enabled; +} + +bool wifi_manager_is_ap_running(void) { + return s_ap_running; +} + +bool wifi_manager_set_enabled(bool enabled) { + if (s_wifi_enabled == enabled) { + return true; + } + + s_prefs.putBool("wifi_enabled", enabled); + s_wifi_enabled = enabled; + + // Applying the change requires a reboot: the caller (CLI/web) must restart. + // Switching the WiFi stack on/off at runtime is unsafe while BLE is running. + app_log("WIFI", "Wi-Fi %s saved; reboot required to apply", enabled ? "enable" : "disable"); + return true; +} + void wifi_manager_task(void) { - s_dns_server.processNextRequest(); + if (!s_wifi_enabled) { + return; + } + if (s_ap_running) { + s_dns_server.processNextRequest(); + } uint32_t now = millis(); if (s_sta_configured && (now - s_last_sta_check > 5000)) { @@ -73,10 +126,12 @@ void wifi_manager_task(void) { } String wifi_manager_get_ap_ip(void) { + if (!s_wifi_enabled) return "Disabled"; return WiFi.softAPIP().toString(); } String wifi_manager_get_sta_ip(void) { + if (!s_wifi_enabled) return "Disabled"; if (WiFi.status() == WL_CONNECTED) { return WiFi.localIP().toString(); } @@ -84,10 +139,12 @@ String wifi_manager_get_sta_ip(void) { } bool wifi_manager_is_sta_connected(void) { + if (!s_wifi_enabled) return false; return WiFi.status() == WL_CONNECTED; } int8_t wifi_manager_get_sta_rssi(void) { + if (!s_wifi_enabled) return 0; if (WiFi.status() == WL_CONNECTED) { return WiFi.RSSI(); } @@ -95,6 +152,9 @@ int8_t wifi_manager_get_sta_rssi(void) { } String wifi_manager_scan_json(void) { + if (!s_wifi_enabled) { + return "{\"networks\":[],\"error\":\"wifi_disabled\"}"; + } app_log("WIFI", "Scanning for 2.4GHz Wi-Fi networks..."); int n = WiFi.scanNetworks(); JsonDocument doc; @@ -114,6 +174,7 @@ String wifi_manager_scan_json(void) { } bool wifi_manager_save_sta_config(const String& ssid, const String& password) { + if (!s_wifi_enabled) return false; if (ssid.length() == 0) return false; s_prefs.putString("ssid", ssid); @@ -131,6 +192,7 @@ String wifi_manager_get_ap_pass(void) { } bool wifi_manager_save_ap_config(const String& ap_password) { + if (!s_wifi_enabled) return false; String p = ap_password; p.trim(); if (p.length() > 0 && p.length() < 8) { @@ -148,4 +210,3 @@ bool wifi_manager_save_ap_config(const String& ap_password) { } return true; } - diff --git a/src/wifi/wifi_manager.h b/src/wifi/wifi_manager.h index d600889..9f00aac 100644 --- a/src/wifi/wifi_manager.h +++ b/src/wifi/wifi_manager.h @@ -12,6 +12,9 @@ extern "C" { void wifi_manager_init(void); void wifi_manager_task(void); +bool wifi_manager_get_enabled(void); +bool wifi_manager_set_enabled(bool enabled); +bool wifi_manager_is_ap_running(void); String wifi_manager_get_ap_ip(void); String wifi_manager_get_sta_ip(void); bool wifi_manager_is_sta_connected(void); @@ -24,4 +27,3 @@ bool wifi_manager_save_ap_config(const String& ap_password); #ifdef __cplusplus } #endif -