diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..8493c22 --- /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 02aaeba..29e9102 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 55440f4..ca39670 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 0000000..d71dbed --- /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 0000000..48cbfe5 --- /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 b2ec122..5848988 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 9e1eaf8..853d8b8 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 8693eb2..c1049a1 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 db05ca3..5b3c642 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 0000000..b233392 --- /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