From c5906f47581655df6a831762c5dab54a665ade1d Mon Sep 17 00:00:00 2001 From: Summer <71572746+SummerSigh@users.noreply.github.com> Date: Sat, 15 Mar 2025 11:10:56 -0700 Subject: [PATCH 1/2] Add mode switching without reflash --- .gitignore | 1 + .../data/CommandManager/CommandManager.cpp | 81 ++++++++- .../data/CommandManager/CommandManager.hpp | 4 + ESP/lib/src/data/DeviceMode/DeviceMode.cpp | 62 +++++++ ESP/lib/src/data/DeviceMode/DeviceMode.hpp | 47 +++++ ESP/lib/src/io/Serial/SerialManager.cpp | 47 +++-- ESP/lib/src/io/Serial/SerialManager.hpp | 3 +- .../src/network/wifihandler/wifihandler.cpp | 16 ++ ESP/src/main.cpp | 47 ++++- PythonExamples/mode_switch_test.py | 161 ++++++++++++++++++ 10 files changed, 444 insertions(+), 25 deletions(-) create mode 100644 .gitignore create mode 100644 ESP/lib/src/data/DeviceMode/DeviceMode.cpp create mode 100644 ESP/lib/src/data/DeviceMode/DeviceMode.hpp create mode 100644 PythonExamples/mode_switch_test.py diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..8493c220 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +PythonExamples/__pycache__/mode_switch_test.cpython-311.pyc diff --git a/ESP/lib/src/data/CommandManager/CommandManager.cpp b/ESP/lib/src/data/CommandManager/CommandManager.cpp index 02aaeba3..29e91022 100644 --- a/ESP/lib/src/data/CommandManager/CommandManager.cpp +++ b/ESP/lib/src/data/CommandManager/CommandManager.cpp @@ -1,10 +1,13 @@ #include "CommandManager.hpp" +#include "data/DeviceMode/DeviceMode.hpp" +#include "tasks/tasks.hpp" + CommandManager::CommandManager(ProjectConfig* deviceConfig) : deviceConfig(deviceConfig) {} const CommandType CommandManager::getCommandType(JsonVariant& command) { - if (!command.containsKey("command")) + if (!command["command"].is()) return CommandType::None; if (auto search = commandMap.find(command["command"]); @@ -15,11 +18,11 @@ const CommandType CommandManager::getCommandType(JsonVariant& command) { } bool CommandManager::hasDataField(JsonVariant& command) { - return command.containsKey("data"); + return command["data"].is(); } void CommandManager::handleCommands(CommandsPayload commandsPayload) { - if (!commandsPayload.data.containsKey("commands")) { + if (!commandsPayload.data["commands"].is()) { log_e("Json data sent not supported, lacks commands field"); return; } @@ -41,12 +44,12 @@ void CommandManager::handleCommand(JsonVariant command) { // malformed command, lacked data field break; - if (!command["data"].containsKey("ssid") || - !command["data"].containsKey("password")) + if (!command["data"]["ssid"].is() || + !command["data"]["password"].is()) break; std::string customNetworkName = "main"; - if (command["data"].containsKey("network_name")) + if (command["data"]["network_name"].is()) customNetworkName = command["data"]["network_name"].as(); this->deviceConfig->setWifiConfig(customNetworkName, @@ -55,6 +58,16 @@ void CommandManager::handleCommand(JsonVariant command) { 0, // channel, should this be zero? 0, // power, should this be zero? false, false); + + DeviceModeManager* deviceModeManager = DeviceModeManager::getInstance(); + if (deviceModeManager) { + deviceModeManager->setHasWiFiCredentials(true); + + deviceModeManager->setMode(DeviceMode::WIFI_MODE); + log_i("[CommandManager] Switching to WiFi mode after receiving credentials"); + + OpenIrisTasks::ScheduleRestart(2000); + } break; } @@ -62,7 +75,7 @@ void CommandManager::handleCommand(JsonVariant command) { if (!this->hasDataField(command)) break; - if (!command["data"].containsKey("hostname") || + if (!command["data"]["hostname"].is() || !strlen(command["data"]["hostname"])) break; @@ -75,6 +88,60 @@ void CommandManager::handleCommand(JsonVariant command) { Serial.println("PONG \n\r"); break; } + case CommandType::SWITCH_MODE: { + if (!this->hasDataField(command)) + break; + + if (!command["data"]["mode"].is()) + break; + + int modeValue = command["data"]["mode"]; + DeviceMode newMode = static_cast(modeValue); + DeviceMode currentMode; + + DeviceModeManager* deviceModeManager = DeviceModeManager::getInstance(); + if (deviceModeManager) { + currentMode = deviceModeManager->getMode(); + + // If switching to USB mode from WiFi or AP mode, disconnect WiFi immediately + if (newMode == DeviceMode::USB_MODE && + (currentMode == DeviceMode::WIFI_MODE || currentMode == DeviceMode::AP_MODE)) { + log_i("[CommandManager] Immediately switching to USB mode"); + WiFi.disconnect(true); + } + + deviceModeManager->setMode(newMode); + log_i("[CommandManager] Switching to mode: %d", modeValue); + + // Only schedule a restart if not switching to USB mode during WiFi/AP initialization + if (!(newMode == DeviceMode::USB_MODE && + (currentMode == DeviceMode::WIFI_MODE || currentMode == DeviceMode::AP_MODE) && + wifiStateManager.getCurrentState() == WiFiState_e::WiFiState_Connecting)) { + OpenIrisTasks::ScheduleRestart(2000); + } + } + + break; + } + case CommandType::WIPE_WIFI_CREDS: { + + auto networks = this->deviceConfig->getWifiConfigs(); + for (auto& network : networks) { + this->deviceConfig->deleteWifiConfig(network.name, false); + } + + DeviceModeManager* deviceModeManager = DeviceModeManager::getInstance(); + if (deviceModeManager) { + deviceModeManager->setHasWiFiCredentials(false); + + deviceModeManager->setMode(DeviceMode::USB_MODE); + log_i("[CommandManager] Switching to USB mode after wiping credentials"); + + OpenIrisTasks::ScheduleRestart(2000); + } + + break; + } default: break; } diff --git a/ESP/lib/src/data/CommandManager/CommandManager.hpp b/ESP/lib/src/data/CommandManager/CommandManager.hpp index 55440f47..ca396701 100644 --- a/ESP/lib/src/data/CommandManager/CommandManager.hpp +++ b/ESP/lib/src/data/CommandManager/CommandManager.hpp @@ -10,6 +10,8 @@ enum CommandType { PING, SET_WIFI, SET_MDNS, + SWITCH_MODE, + WIPE_WIFI_CREDS, }; struct CommandsPayload { @@ -22,6 +24,8 @@ class CommandManager { {"ping", CommandType::PING}, {"set_wifi", CommandType::SET_WIFI}, {"set_mdns", CommandType::SET_MDNS}, + {"switch_mode", CommandType::SWITCH_MODE}, + {"wipe_wifi_creds", CommandType::WIPE_WIFI_CREDS}, }; ProjectConfig* deviceConfig; diff --git a/ESP/lib/src/data/DeviceMode/DeviceMode.cpp b/ESP/lib/src/data/DeviceMode/DeviceMode.cpp new file mode 100644 index 00000000..d71dbede --- /dev/null +++ b/ESP/lib/src/data/DeviceMode/DeviceMode.cpp @@ -0,0 +1,62 @@ +#include "DeviceMode.hpp" + +DeviceModeManager* DeviceModeManager::instance = nullptr; + +DeviceModeManager::DeviceModeManager() : currentMode(DeviceMode::USB_MODE) {} + +DeviceModeManager::~DeviceModeManager() { + preferences.end(); +} + +void DeviceModeManager::init() { + preferences.begin(PREF_NAMESPACE, false); + + // Load the saved mode or use default (USB_MODE) + int savedMode = preferences.getInt(MODE_KEY, static_cast(DeviceMode::AUTO_MODE)); + currentMode = static_cast(savedMode); + + // If in AUTO_MODE, determine the appropriate mode based on saved credentials + if (currentMode == DeviceMode::AUTO_MODE) { + currentMode = determineMode(); + } + + log_i("[DeviceModeManager] Initialized with mode: %d", static_cast(currentMode)); +} + +DeviceMode DeviceModeManager::getMode() { + return currentMode; +} + +void DeviceModeManager::setMode(DeviceMode mode) { + currentMode = mode; + preferences.putInt(MODE_KEY, static_cast(mode)); + log_i("[DeviceModeManager] Mode set to: %d", static_cast(mode)); +} + +bool DeviceModeManager::hasWiFiCredentials() { + return preferences.getBool(HAS_WIFI_CREDS_KEY, false); +} + +void DeviceModeManager::setHasWiFiCredentials(bool hasCredentials) { + preferences.putBool(HAS_WIFI_CREDS_KEY, hasCredentials); + log_i("[DeviceModeManager] WiFi credentials status set to: %d", hasCredentials); +} + +DeviceMode DeviceModeManager::determineMode() { + // If WiFi credentials are saved, use WiFi mode, otherwise use AP mode + return hasWiFiCredentials() ? DeviceMode::WIFI_MODE : DeviceMode::AP_MODE; +} + +DeviceModeManager* DeviceModeManager::getInstance() { + if (instance == nullptr) { + createInstance(); + } + return instance; +} + +void DeviceModeManager::createInstance() { + if (instance == nullptr) { + instance = new DeviceModeManager(); + instance->init(); + } +} \ No newline at end of file diff --git a/ESP/lib/src/data/DeviceMode/DeviceMode.hpp b/ESP/lib/src/data/DeviceMode/DeviceMode.hpp new file mode 100644 index 00000000..48cbfe53 --- /dev/null +++ b/ESP/lib/src/data/DeviceMode/DeviceMode.hpp @@ -0,0 +1,47 @@ +#pragma once +#ifndef DEVICE_MODE_HPP +#define DEVICE_MODE_HPP + +#include +#include +#include + +// Enum to represent the device operating mode +enum class DeviceMode { + USB_MODE, // Device operates in USB mode only + WIFI_MODE, // Device operates in WiFi mode only + AP_MODE, // Device operates in AP mode with serial commands enabled + AUTO_MODE // Device automatically selects mode based on saved credentials +}; + +class DeviceModeManager { +private: + static DeviceModeManager* instance; + Preferences preferences; + DeviceMode currentMode; + const char* PREF_NAMESPACE = "device_mode"; + const char* MODE_KEY = "mode"; + const char* HAS_WIFI_CREDS_KEY = "has_wifi_creds"; + +public: + DeviceModeManager(); + ~DeviceModeManager(); + + static DeviceModeManager* getInstance(); + + static void createInstance(); + + void init(); + + DeviceMode getMode(); + + void setMode(DeviceMode mode); + + bool hasWiFiCredentials(); + + void setHasWiFiCredentials(bool hasCredentials); + + DeviceMode determineMode(); +}; + +#endif // DEVICE_MODE_HPP \ No newline at end of file diff --git a/ESP/lib/src/io/Serial/SerialManager.cpp b/ESP/lib/src/io/Serial/SerialManager.cpp index b2ec122a..58489887 100644 --- a/ESP/lib/src/io/Serial/SerialManager.cpp +++ b/ESP/lib/src/io/Serial/SerialManager.cpp @@ -1,9 +1,32 @@ #include "SerialManager.hpp" +#include "data/DeviceMode/DeviceMode.hpp" SerialManager::SerialManager(CommandManager* commandManager) - : commandManager(commandManager) {} + : commandManager(commandManager) {} + +void SerialManager::sendQuery(QueryAction action, + QueryStatus status, + std::string additional_info) { + JsonDocument doc; + doc["action"] = queryActionMap.at(action); + doc["status"] = static_cast(status); + if (!additional_info.empty()) { + doc["info"] = additional_info; + } + + String output; + serializeJson(doc, output); + Serial.println(output); +} + +void SerialManager::checkUSBMode() { + DeviceMode currentMode = DeviceModeManager::getInstance()->getMode(); + if (currentMode == DeviceMode::USB_MODE) { + log_i("[SerialManager] USB mode active - auto-streaming enabled"); + + } +} -#ifdef ETVR_EYE_TRACKER_USB_API void SerialManager::send_frame() { if (!last_frame) last_frame = esp_timer_get_time(); @@ -49,7 +72,6 @@ void SerialManager::send_frame() { log_d("Size: %uKB, Time: %ums (%ifps)\n", len / 1024, latency, 1000 / latency); } -#endif void SerialManager::init() { #ifdef SERIAL_MANAGER_USE_HIGHER_FREQUENCY @@ -58,25 +80,28 @@ void SerialManager::init() { if (SERIAL_FLUSH_ENABLED) { Serial.flush(); } + + // Check if we're in USB mode and set up accordingly + checkUSBMode(); } void SerialManager::run() { + // Process any available commands first to ensure mode changes are detected immediately if (Serial.available()) { JsonDocument doc; DeserializationError deserializationError = deserializeJson(doc, Serial); if (deserializationError) { log_e("Command deserialization failed: %s", deserializationError.c_str()); - - return; + } else { + CommandsPayload commands = {doc}; + this->commandManager->handleCommands(commands); } - - CommandsPayload commands = {doc}; - this->commandManager->handleCommands(commands); } -#ifdef ETVR_EYE_TRACKER_USB_API - else { + + // Check if we're in USB mode and automatically send frames + DeviceMode currentMode = DeviceModeManager::getInstance()->getMode(); + if (currentMode == DeviceMode::USB_MODE) { this->send_frame(); } -#endif } diff --git a/ESP/lib/src/io/Serial/SerialManager.hpp b/ESP/lib/src/io/Serial/SerialManager.hpp index 9e1eaf8e..853d8b89 100644 --- a/ESP/lib/src/io/Serial/SerialManager.hpp +++ b/ESP/lib/src/io/Serial/SerialManager.hpp @@ -35,12 +35,10 @@ class SerialManager { esp_err_t err = ESP_OK; CommandManager* commandManager; -#ifdef ETVR_EYE_TRACKER_USB_API int64_t last_frame = 0; long last_request_time = 0; void send_frame(); -#endif public: SerialManager(CommandManager* commandManager); @@ -49,6 +47,7 @@ class SerialManager { std::string additional_info); void init(); void run(); + void checkUSBMode(); }; #endif \ No newline at end of file diff --git a/ESP/lib/src/network/wifihandler/wifihandler.cpp b/ESP/lib/src/network/wifihandler/wifihandler.cpp index 8693eb20..c1049a1e 100644 --- a/ESP/lib/src/network/wifihandler/wifihandler.cpp +++ b/ESP/lib/src/network/wifihandler/wifihandler.cpp @@ -2,6 +2,7 @@ #include #include "data/StateManager/StateManager.hpp" #include "data/utilities/helpers.hpp" +#include "data/DeviceMode/DeviceMode.hpp" WiFiHandler::WiFiHandler(ProjectConfig& configManager, const std::string& ssid, @@ -92,6 +93,7 @@ void WiFiHandler::begin() { void WiFiHandler::adhoc(const std::string& ssid, uint8_t channel, const std::string& password) { + wifiStateManager.setState(WiFiState_e::WiFiState_ADHOC); log_i("\n[INFO]: Configuring access point...\n"); @@ -169,6 +171,20 @@ bool WiFiHandler::iniSTA(const std::string& ssid, currentMillis = millis(); log_i("."); log_d("Progress: %d \n\r", progress); + + // Check if mode has been changed to USB mode during connection attempt + DeviceModeManager* deviceModeManager = DeviceModeManager::getInstance(); + if (deviceModeManager && deviceModeManager->getMode() == DeviceMode::USB_MODE) { + log_i("[WiFiHandler] Mode changed to USB during connection, aborting WiFi setup"); + WiFi.disconnect(true); + wifiStateManager.setState(WiFiState_e::WiFiState_Disconnected); + return false; + } + + if (Serial.available()) { + yield(); + } + if ((currentMillis - startingMillis) >= connectionTimeout) { wifiStateManager.setState(WiFiState_e::WiFiState_Error); log_e("Connection to: %s TIMEOUT \n\r", ssid.c_str()); diff --git a/ESP/src/main.cpp b/ESP/src/main.cpp index db05ca32..5b3c6423 100644 --- a/ESP/src/main.cpp +++ b/ESP/src/main.cpp @@ -1,4 +1,5 @@ #include +#include "data/DeviceMode/DeviceMode.hpp" /** * @brief ProjectConfig object * @brief This is the main configuration object for the project @@ -38,10 +39,34 @@ StreamServer streamServer; #endif // SIM_ENABLED void etvr_eye_tracker_web_init() { + // Check if mode has been changed to USB mode before starting network initialization + DeviceModeManager* deviceModeManager = DeviceModeManager::getInstance(); + if (deviceModeManager && deviceModeManager->getMode() == DeviceMode::USB_MODE) { + log_i("[SETUP]: Mode changed to USB before network initialization, aborting"); + WiFi.disconnect(true); + return; + } + log_d("[SETUP]: Starting Network Handler"); deviceConfig.attach(mdnsHandler); + + // Check mode again before starting WiFi + if (deviceModeManager && deviceModeManager->getMode() == DeviceMode::USB_MODE) { + log_i("[SETUP]: Mode changed to USB before WiFi initialization, aborting"); + WiFi.disconnect(true); + return; + } + log_d("[SETUP]: Starting WiFi Handler"); wifiHandler.begin(); + + // Check mode again before starting MDNS + if (deviceModeManager && deviceModeManager->getMode() == DeviceMode::USB_MODE) { + log_i("[SETUP]: Mode changed to USB before MDNS initialization, aborting"); + WiFi.disconnect(true); + return; + } + log_d("[SETUP]: Starting MDNS Handler"); mdnsHandler.startMDNS(); @@ -85,6 +110,9 @@ void setup() { Serial.begin(115200); Logo::printASCII(); ledManager.begin(); + + DeviceModeManager::createInstance(); + DeviceModeManager* deviceModeManager = DeviceModeManager::getInstance(); #ifdef CONFIG_CAMERA_MODULE_SWROOM_BABBLE_S3 // Set IR emitter strength to 100%. const int ledPin = 1; // Replace this with a command endpoint eventually. @@ -104,11 +132,20 @@ void setup() { serialManager.init(); -#ifndef ETVR_EYE_TRACKER_USB_API - etvr_eye_tracker_web_init(); -#else // ETVR_EYE_TRACKER_WEB_API - WiFi.disconnect(true); -#endif // ETVR_EYE_TRACKER_WEB_API + DeviceMode currentMode = deviceModeManager->getMode(); + + if (currentMode == DeviceMode::WIFI_MODE) { + // Initialize WiFi mode + etvr_eye_tracker_web_init(); + log_i("[SETUP]: Initialized in WiFi mode"); + } else if (currentMode == DeviceMode::AP_MODE) { + // Initialize AP mode with serial commands enabled + etvr_eye_tracker_web_init(); + log_i("[SETUP]: Initialized in AP mode with serial commands enabled"); + } else { + WiFi.disconnect(true); + log_i("[SETUP]: Initialized in USB mode"); + } } void loop() { diff --git a/PythonExamples/mode_switch_test.py b/PythonExamples/mode_switch_test.py new file mode 100644 index 00000000..b2333928 --- /dev/null +++ b/PythonExamples/mode_switch_test.py @@ -0,0 +1,161 @@ +#!/usr/bin/env python3 +import serial +import json +import time +import requests +import argparse +import socket + +class OpenIrisModeTester: + def __init__(self, port, baudrate=115200, timeout=5): + self.port = port + self.baudrate = baudrate + self.timeout = timeout + self.serial_conn = None + self.device_ip = None + self.device_port = None + + def connect_serial(self): + try: + self.serial_conn = serial.Serial(self.port, self.baudrate, timeout=self.timeout) + self.flush_serial_logs() + print(f"Connected to {self.port} at {self.baudrate} baud") + return True + except Exception as e: + print(f"Error connecting to serial port: {e}") + return False + + def flush_serial_logs(self): + if self.serial_conn and self.serial_conn.is_open: + self.serial_conn.reset_input_buffer() + self.serial_conn.reset_output_buffer() + print("Serial logs flushed.") + + def send_command(self, command_obj): + if not self.serial_conn: + print("Serial connection not established") + return False + + try: + command_json = json.dumps(command_obj) + self.serial_conn.write(command_json.encode() + b'\n') + time.sleep(0.5) + + response = "" + start_time = time.time() + while (time.time() - start_time) < self.timeout: + if self.serial_conn.in_waiting: + line = self.serial_conn.readline().decode('utf-8', errors='replace').strip() + if line: + response += line + try: + json.loads(response) + return response + except json.JSONDecodeError: + pass + time.sleep(0.1) + + return response if response else None + except Exception as e: + print(f"Error sending command: {e}") + return None + + def set_wifi_credentials(self, ssid, password): + command = {"commands": [{"command": "set_wifi", "data": {"ssid": ssid, "password": password, "network_name": "main"}}]} + print("Sending WiFi credentials...") + response = self.send_command(command) + print(f"Response: {response}") + return response + + def wipe_wifi_credentials(self): + command = {"commands": [{"command": "wipe_wifi_creds"}]} + print("Wiping WiFi credentials...") + response = self.send_command(command) + print(f"Response: {response}") + return response + + def discover_device(self, hostname="openiristracker.local"): + try: + self.device_ip = socket.gethostbyname(hostname) + self.device_port = 80 + print(f"Device found at {self.device_ip}:{self.device_port}") + return True + except socket.error as e: + print(f"Hostname resolution failed: {e}") + return False + + def test_wifi_api(self): + if not self.device_ip: + print("Device IP not available. Discover device first.") + return False + + try: + url = f"http://{self.device_ip}:81/control/builtin/command/ping" + response = requests.get(url, timeout=5) + if response.status_code == 200: + print("WiFi API test successful!") + return True + else: + print(f"WiFi API test failed with status code: {response.status_code}") + return False + except Exception as e: + print(f"Error testing WiFi API: {e}") + return False + + def close(self): + if self.serial_conn and self.serial_conn.is_open: + self.serial_conn.close() + + +def run_test(port, ssid, password): + tester = OpenIrisModeTester(port) + + try: + if not tester.connect_serial(): + return + + print("\n=== Step 1: Setting WiFi credentials ===") + tester.set_wifi_credentials(ssid, password) + print("Device will restart. Waiting 10 seconds...") + time.sleep(10) + + tester.connect_serial() + tester.flush_serial_logs() + + print("\n=== Step 2: Discovering device on network ===") + if tester.discover_device(): + print("\n=== Step 3: Testing WiFi API ===") + tester.test_wifi_api() + + tester.flush_serial_logs() + + print("\n=== Step 4: Wiping WiFi credentials ===") + tester.wipe_wifi_credentials() + print("Device will restart in USB mode. Waiting 10 seconds...") + time.sleep(10) + + tester.connect_serial() + tester.flush_serial_logs() + + print("\n=== Step 5: Verifying USB mode ===") + ping_command = {"commands": [{"command": "ping"}]} + print("Device is most likely booting, waiting 10 seconds...") + time.sleep(10) + + response = tester.send_command(ping_command) + print(f"Response: {response}") + + tester.flush_serial_logs() + + finally: + tester.close() + + +if __name__ == "__main__": + parser = argparse.ArgumentParser(description="Test OpenIris mode switching functionality") + parser.add_argument("--port", required=True, help="Serial port (e.g., COM3 or /dev/ttyUSB0)") + parser.add_argument("--ssid", required=True, help="WiFi SSID") + parser.add_argument("--password", required=True, help="WiFi password") + + args = parser.parse_args() + run_test(args.port, args.ssid, args.password) \ No newline at end of file From fe09dd0c62ecf0cfcc5023f1005362d33a3b67e2 Mon Sep 17 00:00:00 2001 From: Summer <71572746+SummerSigh@users.noreply.github.com> Date: Fri, 11 Apr 2025 11:49:19 -0700 Subject: [PATCH 2/2] refactor(DeviceMode): migrate device mode management to ProjectConfig Removed DeviceModeManager and integrated device mode handling into ProjectConfig for better maintainability. Added RESTART_DEVICE command for explicit device reboots and updated related tests to verify reboot functionality. Simplified mode checks across the codebase by using ProjectConfig directly. --- .../data/CommandManager/CommandManager.cpp | 68 ++--- .../data/CommandManager/CommandManager.hpp | 3 + ESP/lib/src/data/DeviceMode/DeviceMode.cpp | 62 ----- ESP/lib/src/data/DeviceMode/DeviceMode.hpp | 47 ---- .../src/data/StateManager/StateManager.hpp | 3 +- ESP/lib/src/data/config/project_config.cpp | 72 ++++- ESP/lib/src/data/config/project_config.hpp | 28 ++ ESP/lib/src/io/Serial/SerialManager.cpp | 7 +- ESP/lib/src/network/api/baseAPI/baseAPI.cpp | 12 +- .../src/network/wifihandler/wifihandler.cpp | 7 +- ESP/src/main.cpp | 13 +- PythonExamples/mode_switch_test.py | 263 ++++++++++++++++-- 12 files changed, 386 insertions(+), 199 deletions(-) delete mode 100644 ESP/lib/src/data/DeviceMode/DeviceMode.cpp delete mode 100644 ESP/lib/src/data/DeviceMode/DeviceMode.hpp diff --git a/ESP/lib/src/data/CommandManager/CommandManager.cpp b/ESP/lib/src/data/CommandManager/CommandManager.cpp index 29e91022..e7be0bf6 100644 --- a/ESP/lib/src/data/CommandManager/CommandManager.cpp +++ b/ESP/lib/src/data/CommandManager/CommandManager.cpp @@ -1,5 +1,4 @@ #include "CommandManager.hpp" -#include "data/DeviceMode/DeviceMode.hpp" #include "tasks/tasks.hpp" @@ -59,15 +58,9 @@ void CommandManager::handleCommand(JsonVariant command) { 0, // power, should this be zero? false, false); - DeviceModeManager* deviceModeManager = DeviceModeManager::getInstance(); - if (deviceModeManager) { - deviceModeManager->setHasWiFiCredentials(true); - - deviceModeManager->setMode(DeviceMode::WIFI_MODE); - log_i("[CommandManager] Switching to WiFi mode after receiving credentials"); - - OpenIrisTasks::ScheduleRestart(2000); - } + this->deviceConfig->setHasWiFiCredentials(true, false); + this->deviceConfig->setDeviceMode(DeviceMode::WIFI_MODE, true); + log_i("[CommandManager] Switching to WiFi mode after receiving credentials"); break; } @@ -97,30 +90,25 @@ void CommandManager::handleCommand(JsonVariant command) { int modeValue = command["data"]["mode"]; DeviceMode newMode = static_cast(modeValue); - DeviceMode currentMode; + DeviceMode currentMode = this->deviceConfig->getDeviceModeConfig().mode; - DeviceModeManager* deviceModeManager = DeviceModeManager::getInstance(); - if (deviceModeManager) { - currentMode = deviceModeManager->getMode(); - - // If switching to USB mode from WiFi or AP mode, disconnect WiFi immediately - if (newMode == DeviceMode::USB_MODE && - (currentMode == DeviceMode::WIFI_MODE || currentMode == DeviceMode::AP_MODE)) { - log_i("[CommandManager] Immediately switching to USB mode"); - WiFi.disconnect(true); - } - - deviceModeManager->setMode(newMode); - log_i("[CommandManager] Switching to mode: %d", modeValue); - - // Only schedule a restart if not switching to USB mode during WiFi/AP initialization - if (!(newMode == DeviceMode::USB_MODE && - (currentMode == DeviceMode::WIFI_MODE || currentMode == DeviceMode::AP_MODE) && - wifiStateManager.getCurrentState() == WiFiState_e::WiFiState_Connecting)) { - OpenIrisTasks::ScheduleRestart(2000); - } + // If switching to USB mode from WiFi or AP mode, disconnect WiFi immediately + if (newMode == DeviceMode::USB_MODE && + (currentMode == DeviceMode::WIFI_MODE || currentMode == DeviceMode::AP_MODE)) { + log_i("[CommandManager] Immediately switching to USB mode"); + WiFi.disconnect(true); } + this->deviceConfig->setDeviceMode(newMode, true); + log_i("[CommandManager] Switching to mode: %d", modeValue); + + // Removed automatic restart to allow explicit control via RESTART_DEVICE command + // if (!(newMode == DeviceMode::USB_MODE && + // (currentMode == DeviceMode::WIFI_MODE || currentMode == DeviceMode::AP_MODE) && + // wifiStateManager.getCurrentState() == WiFiState_e::WiFiState_Connecting)) { + // OpenIrisTasks::ScheduleRestart(2000); + // } + break; } case CommandType::WIPE_WIFI_CREDS: { @@ -130,18 +118,18 @@ void CommandManager::handleCommand(JsonVariant command) { this->deviceConfig->deleteWifiConfig(network.name, false); } - DeviceModeManager* deviceModeManager = DeviceModeManager::getInstance(); - if (deviceModeManager) { - deviceModeManager->setHasWiFiCredentials(false); - - deviceModeManager->setMode(DeviceMode::USB_MODE); - log_i("[CommandManager] Switching to USB mode after wiping credentials"); - - OpenIrisTasks::ScheduleRestart(2000); - } + this->deviceConfig->setHasWiFiCredentials(false, false); + this->deviceConfig->setDeviceMode(DeviceMode::USB_MODE, true); + log_i("[CommandManager] Switching to USB mode after wiping credentials"); + // Removed automatic restart to allow processing of all commands in payload break; } + case CommandType::RESTART_DEVICE: { + log_i("[CommandManager] Explicit restart requested"); + OpenIrisTasks::ScheduleRestart(2000); + break; + } default: break; } diff --git a/ESP/lib/src/data/CommandManager/CommandManager.hpp b/ESP/lib/src/data/CommandManager/CommandManager.hpp index ca396701..cf3a2f00 100644 --- a/ESP/lib/src/data/CommandManager/CommandManager.hpp +++ b/ESP/lib/src/data/CommandManager/CommandManager.hpp @@ -12,6 +12,7 @@ enum CommandType { SET_MDNS, SWITCH_MODE, WIPE_WIFI_CREDS, + RESTART_DEVICE, }; struct CommandsPayload { @@ -26,6 +27,7 @@ class CommandManager { {"set_mdns", CommandType::SET_MDNS}, {"switch_mode", CommandType::SWITCH_MODE}, {"wipe_wifi_creds", CommandType::WIPE_WIFI_CREDS}, + {"restart_device", CommandType::RESTART_DEVICE}, }; ProjectConfig* deviceConfig; @@ -37,6 +39,7 @@ class CommandManager { public: CommandManager(ProjectConfig* deviceConfig); void handleCommands(CommandsPayload commandsPayload); + ProjectConfig* getDeviceConfig() { return deviceConfig; } }; #endif \ No newline at end of file diff --git a/ESP/lib/src/data/DeviceMode/DeviceMode.cpp b/ESP/lib/src/data/DeviceMode/DeviceMode.cpp deleted file mode 100644 index d71dbede..00000000 --- a/ESP/lib/src/data/DeviceMode/DeviceMode.cpp +++ /dev/null @@ -1,62 +0,0 @@ -#include "DeviceMode.hpp" - -DeviceModeManager* DeviceModeManager::instance = nullptr; - -DeviceModeManager::DeviceModeManager() : currentMode(DeviceMode::USB_MODE) {} - -DeviceModeManager::~DeviceModeManager() { - preferences.end(); -} - -void DeviceModeManager::init() { - preferences.begin(PREF_NAMESPACE, false); - - // Load the saved mode or use default (USB_MODE) - int savedMode = preferences.getInt(MODE_KEY, static_cast(DeviceMode::AUTO_MODE)); - currentMode = static_cast(savedMode); - - // If in AUTO_MODE, determine the appropriate mode based on saved credentials - if (currentMode == DeviceMode::AUTO_MODE) { - currentMode = determineMode(); - } - - log_i("[DeviceModeManager] Initialized with mode: %d", static_cast(currentMode)); -} - -DeviceMode DeviceModeManager::getMode() { - return currentMode; -} - -void DeviceModeManager::setMode(DeviceMode mode) { - currentMode = mode; - preferences.putInt(MODE_KEY, static_cast(mode)); - log_i("[DeviceModeManager] Mode set to: %d", static_cast(mode)); -} - -bool DeviceModeManager::hasWiFiCredentials() { - return preferences.getBool(HAS_WIFI_CREDS_KEY, false); -} - -void DeviceModeManager::setHasWiFiCredentials(bool hasCredentials) { - preferences.putBool(HAS_WIFI_CREDS_KEY, hasCredentials); - log_i("[DeviceModeManager] WiFi credentials status set to: %d", hasCredentials); -} - -DeviceMode DeviceModeManager::determineMode() { - // If WiFi credentials are saved, use WiFi mode, otherwise use AP mode - return hasWiFiCredentials() ? DeviceMode::WIFI_MODE : DeviceMode::AP_MODE; -} - -DeviceModeManager* DeviceModeManager::getInstance() { - if (instance == nullptr) { - createInstance(); - } - return instance; -} - -void DeviceModeManager::createInstance() { - if (instance == nullptr) { - instance = new DeviceModeManager(); - instance->init(); - } -} \ No newline at end of file diff --git a/ESP/lib/src/data/DeviceMode/DeviceMode.hpp b/ESP/lib/src/data/DeviceMode/DeviceMode.hpp deleted file mode 100644 index 48cbfe53..00000000 --- a/ESP/lib/src/data/DeviceMode/DeviceMode.hpp +++ /dev/null @@ -1,47 +0,0 @@ -#pragma once -#ifndef DEVICE_MODE_HPP -#define DEVICE_MODE_HPP - -#include -#include -#include - -// Enum to represent the device operating mode -enum class DeviceMode { - USB_MODE, // Device operates in USB mode only - WIFI_MODE, // Device operates in WiFi mode only - AP_MODE, // Device operates in AP mode with serial commands enabled - AUTO_MODE // Device automatically selects mode based on saved credentials -}; - -class DeviceModeManager { -private: - static DeviceModeManager* instance; - Preferences preferences; - DeviceMode currentMode; - const char* PREF_NAMESPACE = "device_mode"; - const char* MODE_KEY = "mode"; - const char* HAS_WIFI_CREDS_KEY = "has_wifi_creds"; - -public: - DeviceModeManager(); - ~DeviceModeManager(); - - static DeviceModeManager* getInstance(); - - static void createInstance(); - - void init(); - - DeviceMode getMode(); - - void setMode(DeviceMode mode); - - bool hasWiFiCredentials(); - - void setHasWiFiCredentials(bool hasCredentials); - - DeviceMode determineMode(); -}; - -#endif // DEVICE_MODE_HPP \ No newline at end of file diff --git a/ESP/lib/src/data/StateManager/StateManager.hpp b/ESP/lib/src/data/StateManager/StateManager.hpp index f2b207df..ee8c60c6 100644 --- a/ESP/lib/src/data/StateManager/StateManager.hpp +++ b/ESP/lib/src/data/StateManager/StateManager.hpp @@ -28,7 +28,8 @@ struct DeviceStates { networksConfigUpdated, apConfigUpdated, wifiTxPowerUpdated, - cameraConfigUpdated + cameraConfigUpdated, + deviceModeUpdated }; enum WiFiState_e { diff --git a/ESP/lib/src/data/config/project_config.cpp b/ESP/lib/src/data/config/project_config.cpp index 05ea095b..860e1675 100644 --- a/ESP/lib/src/data/config/project_config.cpp +++ b/ESP/lib/src/data/config/project_config.cpp @@ -58,6 +58,12 @@ void ProjectConfig::initConfig() { .quality = 7, .brightness = 2, }; + + // Initialize device mode with default values + this->config.deviceMode = { + .mode = DeviceMode::AUTO_MODE, + .hasWiFiCredentials = false, + }; } void ProjectConfig::save() { @@ -67,11 +73,13 @@ void ProjectConfig::save() { cameraConfigSave(); wifiConfigSave(); wifiTxPowerConfigSave(); - end(); // we call end() here to close the connection to the NVS partition, we - // only do this because we call ESP.restart() next. - OpenIrisTasks::ScheduleRestart(2000); + deviceModeConfigSave(); + end(); // we call end() here to close the connection to the NVS partition + // Removed automatic restart to allow explicit control via RESTART_DEVICE command + // OpenIrisTasks::ScheduleRestart(2000); } + void ProjectConfig::wifiConfigSave() { log_d("Saving wifi config"); @@ -126,6 +134,15 @@ void ProjectConfig::wifiTxPowerConfigSave() { putInt("txpower", this->config.txpower.power); } +void ProjectConfig::deviceModeConfigSave() { + /* Device Mode Config */ + putInt(MODE_KEY, static_cast(this->config.deviceMode.mode)); + putBool(HAS_WIFI_CREDS_KEY, this->config.deviceMode.hasWiFiCredentials); + log_i("[ProjectConfig] Device mode config saved: mode=%d, hasWiFiCredentials=%d", + static_cast(this->config.deviceMode.mode), + this->config.deviceMode.hasWiFiCredentials); +} + void ProjectConfig::cameraConfigSave() { /* Camera Config */ putInt("vflip", this->config.camera.vflip); @@ -204,6 +221,18 @@ void ProjectConfig::load() { this->config.camera.framesize = getInt("framesize", (uint8_t)CAM_RESOLUTION); this->config.camera.quality = getInt("quality", 7); this->config.camera.brightness = getInt("brightness", 2); + + int savedMode = getInt(MODE_KEY, static_cast(DeviceMode::AUTO_MODE)); + this->config.deviceMode.mode = static_cast(savedMode); + this->config.deviceMode.hasWiFiCredentials = getBool(HAS_WIFI_CREDS_KEY, false); + + if (this->config.deviceMode.mode == DeviceMode::AUTO_MODE) { + this->config.deviceMode.mode = determineMode(); + } + + log_i("[ProjectConfig] Loaded device mode: %d, hasWiFiCredentials: %d", + static_cast(this->config.deviceMode.mode), + this->config.deviceMode.hasWiFiCredentials); this->_already_loaded = true; this->notifyAll(ConfigState_e::configLoaded); @@ -419,6 +448,14 @@ std::string ProjectConfig::WiFiTxPower_t::toRepresentation() { return json; } +std::string ProjectConfig::DeviceModeConfig_t::toRepresentation() { + std::string json = Helpers::format_string( + "\"device_mode\": {\"mode\": %d, \"hasWiFiCredentials\": %s}", + static_cast(this->mode), + this->hasWiFiCredentials ? "true" : "false"); + return json; +} + //********************************************************************************************************************** //* //! Get Methods @@ -443,3 +480,32 @@ ProjectConfig::MDNSConfig_t& ProjectConfig::getMDNSConfig() { ProjectConfig::WiFiTxPower_t& ProjectConfig::getWiFiTxPowerConfig() { return this->config.txpower; } + +ProjectConfig::DeviceModeConfig_t& ProjectConfig::getDeviceModeConfig() { + return this->config.deviceMode; +} + +void ProjectConfig::setDeviceMode(DeviceMode mode, bool shouldNotify) { + this->config.deviceMode.mode = mode; + putInt(MODE_KEY, static_cast(mode)); + log_i("[ProjectConfig] Mode set to: %d", static_cast(mode)); + + if (shouldNotify) { + this->notifyAll(ConfigState_e::deviceModeUpdated); + } +} + +void ProjectConfig::setHasWiFiCredentials(bool hasCredentials, bool shouldNotify) { + this->config.deviceMode.hasWiFiCredentials = hasCredentials; + putBool(HAS_WIFI_CREDS_KEY, hasCredentials); + log_i("[ProjectConfig] WiFi credentials status set to: %d", hasCredentials); + + if (shouldNotify) { + this->notifyAll(ConfigState_e::deviceModeUpdated); + } +} + +DeviceMode ProjectConfig::determineMode() { + // If WiFi credentials are saved, use WiFi mode, otherwise use AP mode + return this->config.deviceMode.hasWiFiCredentials ? DeviceMode::WIFI_MODE : DeviceMode::AP_MODE; +} diff --git a/ESP/lib/src/data/config/project_config.hpp b/ESP/lib/src/data/config/project_config.hpp index a2a5c99a..025d0c4b 100644 --- a/ESP/lib/src/data/config/project_config.hpp +++ b/ESP/lib/src/data/config/project_config.hpp @@ -13,6 +13,14 @@ #include "data/utilities/network_utilities.hpp" #include "tasks/tasks.hpp" +// Enum to represent the device operating mode +enum class DeviceMode { + USB_MODE, // Device operates in USB mode only + WIFI_MODE, // Device operates in WiFi mode only + AP_MODE, // Device operates in AP mode with serial commands enabled + AUTO_MODE // Device automatically selects mode based on saved credentials +}; + class ProjectConfig : public Preferences, public ISubject { public: ProjectConfig(const std::string& name = std::string(), @@ -88,6 +96,12 @@ class ProjectConfig : public Preferences, public ISubject { std::string toRepresentation(); }; + struct DeviceModeConfig_t { + DeviceMode mode; + bool hasWiFiCredentials; + std::string toRepresentation(); + }; + struct TrackerConfig_t { DeviceConfig_t device; CameraConfig_t camera; @@ -95,6 +109,7 @@ class ProjectConfig : public Preferences, public ISubject { AP_WiFiConfig_t ap_network; MDNSConfig_t mdns; WiFiTxPower_t txpower; + DeviceModeConfig_t deviceMode; }; DeviceConfig_t& getDeviceConfig(); @@ -103,6 +118,7 @@ class ProjectConfig : public Preferences, public ISubject { AP_WiFiConfig_t& getAPWifiConfig(); MDNSConfig_t& getMDNSConfig(); WiFiTxPower_t& getWiFiTxPowerConfig(); + DeviceModeConfig_t& getDeviceModeConfig(); void setDeviceConfig(const std::string& OTALogin, const std::string& OTAPassword, @@ -132,12 +148,24 @@ class ProjectConfig : public Preferences, public ISubject { void setWiFiTxPower(uint8_t power, bool shouldNotify); void deleteWifiConfig(const std::string& networkName, bool shouldNotify); + + void setDeviceMode(DeviceMode mode, bool shouldNotify); + + void setHasWiFiCredentials(bool hasCredentials, bool shouldNotify); + + DeviceMode determineMode(); + + void deviceModeConfigSave(); private: TrackerConfig_t config; std::string _name; std::string _mdnsName; bool _already_loaded; + + // Device mode related constants + const char* MODE_KEY = "mode"; + const char* HAS_WIFI_CREDS_KEY = "has_wifi_creds"; }; #endif // PROJECT_CONFIG_HPP diff --git a/ESP/lib/src/io/Serial/SerialManager.cpp b/ESP/lib/src/io/Serial/SerialManager.cpp index 58489887..cb259c7f 100644 --- a/ESP/lib/src/io/Serial/SerialManager.cpp +++ b/ESP/lib/src/io/Serial/SerialManager.cpp @@ -1,5 +1,4 @@ #include "SerialManager.hpp" -#include "data/DeviceMode/DeviceMode.hpp" SerialManager::SerialManager(CommandManager* commandManager) : commandManager(commandManager) {} @@ -20,7 +19,8 @@ void SerialManager::sendQuery(QueryAction action, } void SerialManager::checkUSBMode() { - DeviceMode currentMode = DeviceModeManager::getInstance()->getMode(); + // Get device mode from ProjectConfig via CommandManager + DeviceMode currentMode = this->commandManager->getDeviceConfig()->getDeviceModeConfig().mode; if (currentMode == DeviceMode::USB_MODE) { log_i("[SerialManager] USB mode active - auto-streaming enabled"); @@ -99,8 +99,7 @@ void SerialManager::run() { } } - // Check if we're in USB mode and automatically send frames - DeviceMode currentMode = DeviceModeManager::getInstance()->getMode(); + DeviceMode currentMode = this->commandManager->getDeviceConfig()->getDeviceModeConfig().mode; if (currentMode == DeviceMode::USB_MODE) { this->send_frame(); } diff --git a/ESP/lib/src/network/api/baseAPI/baseAPI.cpp b/ESP/lib/src/network/api/baseAPI/baseAPI.cpp index d8461aed..4e799e89 100644 --- a/ESP/lib/src/network/api/baseAPI/baseAPI.cpp +++ b/ESP/lib/src/network/api/baseAPI/baseAPI.cpp @@ -36,6 +36,8 @@ void BaseAPI::begin() { DefaultHeaders::Instance().addHeader("Access-Control-Allow-Origin", "*"); + // The restart_device endpoint has been removed in favor of using rebootDevice through POST + // std::bind(&BaseAPI::notFound, &std::placeholders::_1); server.onNotFound([&](AsyncWebServerRequest* request) { notFound(request); }); } @@ -214,7 +216,15 @@ void BaseAPI::rebootDevice(AsyncWebServerRequest* request) { switch (_networkMethodsMap_enum[request->method()]) { case GET: { request->send(200, MIMETYPE_JSON, "{\"msg\":\"Rebooting Device\"}"); + OpenIrisTasks::ScheduleRestart(2000); + break; + } + case POST: { + request->send(200, MIMETYPE_JSON, "{\"msg\":\"Rebooting Device\"}"); + + OpenIrisTasks::ScheduleRestart(2000); + break; } default: { request->send(400, MIMETYPE_JSON, "{\"msg\":\"Invalid Request\"}"); @@ -381,7 +391,7 @@ void BaseAPI::beginOTA() { esp_camera_deinit(); // deinitialize the camera driver digitalWrite(PWDN_GPIO_NUM, HIGH); // turn power off to camera module - AsyncWebServerResponse* response = request->beginResponse_P( + AsyncWebServerResponse* response = request->beginResponse( 200, "text/html", ELEGANT_HTML, ELEGANT_HTML_SIZE); response->addHeader("Content-Encoding", "gzip"); request->send(response); diff --git a/ESP/lib/src/network/wifihandler/wifihandler.cpp b/ESP/lib/src/network/wifihandler/wifihandler.cpp index c1049a1e..98b51419 100644 --- a/ESP/lib/src/network/wifihandler/wifihandler.cpp +++ b/ESP/lib/src/network/wifihandler/wifihandler.cpp @@ -2,7 +2,6 @@ #include #include "data/StateManager/StateManager.hpp" #include "data/utilities/helpers.hpp" -#include "data/DeviceMode/DeviceMode.hpp" WiFiHandler::WiFiHandler(ProjectConfig& configManager, const std::string& ssid, @@ -93,7 +92,6 @@ void WiFiHandler::begin() { void WiFiHandler::adhoc(const std::string& ssid, uint8_t channel, const std::string& password) { - wifiStateManager.setState(WiFiState_e::WiFiState_ADHOC); log_i("\n[INFO]: Configuring access point...\n"); @@ -173,8 +171,7 @@ bool WiFiHandler::iniSTA(const std::string& ssid, log_d("Progress: %d \n\r", progress); // Check if mode has been changed to USB mode during connection attempt - DeviceModeManager* deviceModeManager = DeviceModeManager::getInstance(); - if (deviceModeManager && deviceModeManager->getMode() == DeviceMode::USB_MODE) { + if (configManager.getDeviceModeConfig().mode == DeviceMode::USB_MODE) { log_i("[WiFiHandler] Mode changed to USB during connection, aborting WiFi setup"); WiFi.disconnect(true); wifiStateManager.setState(WiFiState_e::WiFiState_Disconnected); @@ -182,7 +179,7 @@ bool WiFiHandler::iniSTA(const std::string& ssid, } if (Serial.available()) { - yield(); + yield(); // Allow other processes to run } if ((currentMillis - startingMillis) >= connectionTimeout) { diff --git a/ESP/src/main.cpp b/ESP/src/main.cpp index 5b3c6423..e1f1c077 100644 --- a/ESP/src/main.cpp +++ b/ESP/src/main.cpp @@ -1,5 +1,4 @@ #include -#include "data/DeviceMode/DeviceMode.hpp" /** * @brief ProjectConfig object * @brief This is the main configuration object for the project @@ -40,8 +39,7 @@ StreamServer streamServer; void etvr_eye_tracker_web_init() { // Check if mode has been changed to USB mode before starting network initialization - DeviceModeManager* deviceModeManager = DeviceModeManager::getInstance(); - if (deviceModeManager && deviceModeManager->getMode() == DeviceMode::USB_MODE) { + if (deviceConfig.getDeviceModeConfig().mode == DeviceMode::USB_MODE) { log_i("[SETUP]: Mode changed to USB before network initialization, aborting"); WiFi.disconnect(true); return; @@ -51,7 +49,7 @@ void etvr_eye_tracker_web_init() { deviceConfig.attach(mdnsHandler); // Check mode again before starting WiFi - if (deviceModeManager && deviceModeManager->getMode() == DeviceMode::USB_MODE) { + if (deviceConfig.getDeviceModeConfig().mode == DeviceMode::USB_MODE) { log_i("[SETUP]: Mode changed to USB before WiFi initialization, aborting"); WiFi.disconnect(true); return; @@ -61,7 +59,7 @@ void etvr_eye_tracker_web_init() { wifiHandler.begin(); // Check mode again before starting MDNS - if (deviceModeManager && deviceModeManager->getMode() == DeviceMode::USB_MODE) { + if (deviceConfig.getDeviceModeConfig().mode == DeviceMode::USB_MODE) { log_i("[SETUP]: Mode changed to USB before MDNS initialization, aborting"); WiFi.disconnect(true); return; @@ -111,8 +109,7 @@ void setup() { Logo::printASCII(); ledManager.begin(); - DeviceModeManager::createInstance(); - DeviceModeManager* deviceModeManager = DeviceModeManager::getInstance(); + // Device mode is now managed by ProjectConfig #ifdef CONFIG_CAMERA_MODULE_SWROOM_BABBLE_S3 // Set IR emitter strength to 100%. const int ledPin = 1; // Replace this with a command endpoint eventually. @@ -132,7 +129,7 @@ void setup() { serialManager.init(); - DeviceMode currentMode = deviceModeManager->getMode(); + DeviceMode currentMode = deviceConfig.getDeviceModeConfig().mode; if (currentMode == DeviceMode::WIFI_MODE) { // Initialize WiFi mode diff --git a/PythonExamples/mode_switch_test.py b/PythonExamples/mode_switch_test.py index b2333928..afaf2373 100644 --- a/PythonExamples/mode_switch_test.py +++ b/PythonExamples/mode_switch_test.py @@ -14,6 +14,7 @@ def __init__(self, port, baudrate=115200, timeout=5): self.serial_conn = None self.device_ip = None self.device_port = None + self.last_boot_time = None def connect_serial(self): try: @@ -101,61 +102,267 @@ def test_wifi_api(self): except Exception as e: print(f"Error testing WiFi API: {e}") return False + + def restart_device_serial(self): + """Send reboot command via serial connection""" + command = {"commands": [{"command": "restart_device"}]} + print("Sending reboot command via serial...") + response = self.send_command(command) + print(f"Response: {response}") + self.last_boot_time = time.time() + + # Close the serial connection after sending reboot command + # This helps prevent permission issues when reconnecting + if self.serial_conn and self.serial_conn.is_open: + try: + self.serial_conn.close() + print("Serial connection closed after reboot command") + except Exception as e: + print(f"Error closing serial connection: {e}") + + return response + + def restart_device_wifi(self): + """Send reboot command via WiFi API""" + if not self.device_ip: + print("Device IP not available. Cannot restart via WiFi.") + return False + + try: + # First verify the device is responsive via WiFi + ping_url = f"http://{self.device_ip}:81/control/builtin/command/ping" + print(f"Testing device responsiveness with: {ping_url}") + ping_response = requests.get(ping_url, timeout=5) + if ping_response.status_code != 200: + print(f"Device not responsive via WiFi before restart attempt: {ping_response.status_code}") + return False + + # Use rebootDevice endpoint with POST method + url = f"http://{self.device_ip}:81/update/rebootDevice" + print(f"Sending reboot command to: {url}") + + # Use POST method as recommended + try: + response = requests.post(url, timeout=5) + if response.status_code == 200: + print("Reboot command via WiFi (POST) sent successfully!") + self.last_boot_time = time.time() + return True + else: + print(f"Reboot command via WiFi (POST) failed with status code: {response.status_code}") + return False + except requests.exceptions.ConnectionError: + print("Connection error during POST: Device may have already started restarting") + self.last_boot_time = time.time() + return True + except Exception as e: + print(f"Error sending POST reboot command: {e}") + return False + except requests.exceptions.ConnectionError: + print("Connection error: Device may have already started restarting") + self.last_boot_time = time.time() + return True + except Exception as e: + print(f"Error sending reboot command via WiFi: {e}") + return False + + def verify_restart(self, wait_time=60, retry_count=3): + """Verify that the device has rebooted by checking uptime""" + if not self.last_boot_time: + print("No reboot command was previously sent") + return False + + # Calculate elapsed time since reboot command + elapsed = time.time() - self.last_boot_time + if elapsed < wait_time: + # Wait remaining time if needed + remaining = wait_time - elapsed + print(f"Waiting {remaining:.1f} more seconds for device to reboot...") + time.sleep(remaining) + else: + print(f"Already waited {elapsed:.1f} seconds since reboot command") + + # Close the serial connection before attempting to reconnect + if self.serial_conn and self.serial_conn.is_open: + try: + self.serial_conn.close() + time.sleep(1) # Give the OS time to release the port + except Exception as e: + print(f"Error closing serial connection: {e}") + + # Try to reconnect serial with retries + for attempt in range(retry_count): + try: + if self.connect_serial(): + break + except Exception as e: + print(f"Error during reconnection attempt {attempt+1}: {e}") + + if attempt < retry_count - 1: + print(f"Reconnection attempt {attempt+1} failed, retrying in 2 seconds...") + time.sleep(2) + else: + print(f"Failed to reconnect to device after {retry_count} attempts") + return False + + # Send ping command to check if device is responsive + for attempt in range(retry_count): + ping_command = {"commands": [{"command": "ping"}]} + response = self.send_command(ping_command) + + if response and "pong" in response.lower(): + print(f"Device responded after restart (attempt {attempt+1})") + return True + elif attempt < retry_count - 1: + print(f"Ping attempt {attempt+1} failed, retrying in 2 seconds...") + time.sleep(2) + + print(f"Device did not respond properly after {retry_count} ping attempts") + return False def close(self): if self.serial_conn and self.serial_conn.is_open: self.serial_conn.close() -def run_test(port, ssid, password): +def run_test(port, ssid, password, wait_time=15, retry_count=3, skip_wifi_test=False): tester = OpenIrisModeTester(port) + test_results = { + "serial_connection": False, + "wifi_credentials": False, + "device_discovery": False, + "wifi_api": False, + "serial_restart": False, + "wifi_restart": False, + "usb_mode": False + } try: + print("\n=== Step 1: Testing Serial Connection ===") if not tester.connect_serial(): - return - - print("\n=== Step 1: Setting WiFi credentials ===") - tester.set_wifi_credentials(ssid, password) - print("Device will restart. Waiting 10 seconds...") - time.sleep(10) + print("❌ Serial connection failed. Aborting test.") + return test_results + test_results["serial_connection"] = True + print("✅ Serial connection successful") - tester.connect_serial() - tester.flush_serial_logs() + print("\n=== Step 2: Setting WiFi credentials ===") + response = tester.set_wifi_credentials(ssid, password) + if response and "error" not in response.lower(): + test_results["wifi_credentials"] = True + print("✅ WiFi credentials set successfully") + + # Explicitly send reboot command after setting WiFi credentials + print("Sending reboot command after setting WiFi credentials...") + tester.restart_device_serial() + + # Wait specifically 30 seconds for the device to restart + print("Waiting 30 seconds for device to restart...") + time.sleep(30) + else: + print("❌ Failed to set WiFi credentials") + + # Verify device reboots after setting WiFi credentials + if not tester.verify_restart(wait_time=wait_time, retry_count=retry_count): + print("❌ Device did not reboot properly after setting WiFi credentials") + else: + print("✅ Device rebooted successfully after setting WiFi credentials") - print("\n=== Step 2: Discovering device on network ===") + print("\n=== Step 3: Discovering device on network ===") if tester.discover_device(): - print("\n=== Step 3: Testing WiFi API ===") - tester.test_wifi_api() + test_results["device_discovery"] = True + print("✅ Device discovered on network") + + print("\n=== Step 4: Testing WiFi API ===") + if tester.test_wifi_api(): + test_results["wifi_api"] = True + print("✅ WiFi API test successful") + + print("\n=== Step 5: Testing reboot via WiFi ===") + if tester.restart_device_wifi(): + # Wait for device to reboot + if tester.verify_restart(wait_time=wait_time, retry_count=retry_count): + test_results["wifi_restart"] = True + print("✅ Device rebooted successfully via WiFi") + else: + print("❌ Device did not reboot properly via WiFi") + else: + print("❌ Failed to send reboot command via WiFi") + else: + print("❌ WiFi API test failed") + else: + print("❌ Failed to discover device on network") - tester.flush_serial_logs() + # Reconnect serial if needed + if not tester.serial_conn or not tester.serial_conn.is_open: + tester.connect_serial() + tester.flush_serial_logs() - print("\n=== Step 4: Wiping WiFi credentials ===") - tester.wipe_wifi_credentials() - print("Device will restart in USB mode. Waiting 10 seconds...") - time.sleep(10) + print("\n=== Step 6: Testing reboot via Serial ===") + tester.restart_device_serial() + if tester.verify_restart(wait_time=wait_time, retry_count=retry_count): + test_results["serial_restart"] = True + print("✅ Device rebooted successfully via Serial") + else: + print("❌ Device did not reboot properly via Serial") - tester.connect_serial() - tester.flush_serial_logs() + print("\n=== Step 7: Wiping WiFi credentials ===") + tester.wipe_wifi_credentials() + if not tester.verify_restart(wait_time=wait_time, retry_count=retry_count): + print("❌ Device did not reboot properly after wiping WiFi credentials") + else: + print("✅ Device rebooted successfully after wiping WiFi credentials") - print("\n=== Step 5: Verifying USB mode ===") - ping_command = {"commands": [{"command": "ping"}]} - print("Device is most likely booting, waiting 10 seconds...") - time.sleep(10) + print("\n=== Step 8: Verifying USB mode ===") + ping_command = {"commands": [{"command": "ping"}]} + response = tester.send_command(ping_command) + if response and "pong" in response.lower(): + test_results["usb_mode"] = True + print("✅ Device is in USB mode") + else: + print("❌ Failed to verify USB mode") - response = tester.send_command(ping_command) - print(f"Response: {response}") + tester.flush_serial_logs() + + # Print summary + print("\n=== Test Summary ===") + for test, result in test_results.items(): + status = "✅ PASS" if result else "❌ FAIL" + print(f"{test.replace('_', ' ').title()}: {status}") - tester.flush_serial_logs() + return test_results finally: tester.close() if __name__ == "__main__": - parser = argparse.ArgumentParser(description="Test OpenIris mode switching functionality") + parser = argparse.ArgumentParser(description="Test OpenIris mode switching and reboot functionality") parser.add_argument("--port", required=True, help="Serial port (e.g., COM3 or /dev/ttyUSB0)") parser.add_argument("--ssid", required=True, help="WiFi SSID") parser.add_argument("--password", required=True, help="WiFi password") + parser.add_argument("--wait-time", type=int, default=60, help="Wait time in seconds for device reboot (default: 15)") + parser.add_argument("--retry-count", type=int, default=3, help="Number of retry attempts for reconnection (default: 3)") + parser.add_argument("--skip-wifi-test", action="store_true", help="Skip WiFi-related tests") args = parser.parse_args() - run_test(args.port, args.ssid, args.password) \ No newline at end of file + + # Configure test parameters + test_config = { + "port": args.port, + "ssid": args.ssid, + "password": args.password, + "wait_time": args.wait_time, + "retry_count": args.retry_count, + "skip_wifi_test": args.skip_wifi_test + } + + # Run the test and get results + results = run_test(**test_config) + + # Exit with appropriate status code + if all(results.values()): + print("\n✅ All tests passed successfully!") + exit(0) + else: + print("\n❌ Some tests failed. See details above.") + exit(1) \ No newline at end of file