This commit is contained in:
Summer 2025-04-11 20:42:43 +00:00 committed by GitHub
commit 176a85a403
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
12 changed files with 636 additions and 30 deletions

1
.gitignore vendored Normal file
View File

@ -0,0 +1 @@
PythonExamples/__pycache__/mode_switch_test.cpython-311.pyc

View File

@ -1,10 +1,12 @@
#include "CommandManager.hpp" #include "CommandManager.hpp"
#include "tasks/tasks.hpp"
CommandManager::CommandManager(ProjectConfig* deviceConfig) CommandManager::CommandManager(ProjectConfig* deviceConfig)
: deviceConfig(deviceConfig) {} : deviceConfig(deviceConfig) {}
const CommandType CommandManager::getCommandType(JsonVariant& command) { const CommandType CommandManager::getCommandType(JsonVariant& command) {
if (!command.containsKey("command")) if (!command["command"].is<const char*>())
return CommandType::None; return CommandType::None;
if (auto search = commandMap.find(command["command"]); if (auto search = commandMap.find(command["command"]);
@ -15,11 +17,11 @@ const CommandType CommandManager::getCommandType(JsonVariant& command) {
} }
bool CommandManager::hasDataField(JsonVariant& command) { bool CommandManager::hasDataField(JsonVariant& command) {
return command.containsKey("data"); return command["data"].is<JsonObject>();
} }
void CommandManager::handleCommands(CommandsPayload commandsPayload) { void CommandManager::handleCommands(CommandsPayload commandsPayload) {
if (!commandsPayload.data.containsKey("commands")) { if (!commandsPayload.data["commands"].is<JsonArray>()) {
log_e("Json data sent not supported, lacks commands field"); log_e("Json data sent not supported, lacks commands field");
return; return;
} }
@ -41,12 +43,12 @@ void CommandManager::handleCommand(JsonVariant command) {
// malformed command, lacked data field // malformed command, lacked data field
break; break;
if (!command["data"].containsKey("ssid") || if (!command["data"]["ssid"].is<const char*>() ||
!command["data"].containsKey("password")) !command["data"]["password"].is<const char*>())
break; break;
std::string customNetworkName = "main"; std::string customNetworkName = "main";
if (command["data"].containsKey("network_name")) if (command["data"]["network_name"].is<const char*>())
customNetworkName = command["data"]["network_name"].as<std::string>(); customNetworkName = command["data"]["network_name"].as<std::string>();
this->deviceConfig->setWifiConfig(customNetworkName, this->deviceConfig->setWifiConfig(customNetworkName,
@ -56,13 +58,17 @@ void CommandManager::handleCommand(JsonVariant command) {
0, // power, should this be zero? 0, // power, should this be zero?
false, false); false, false);
this->deviceConfig->setHasWiFiCredentials(true, false);
this->deviceConfig->setDeviceMode(DeviceMode::WIFI_MODE, true);
log_i("[CommandManager] Switching to WiFi mode after receiving credentials");
break; break;
} }
case CommandType::SET_MDNS: { case CommandType::SET_MDNS: {
if (!this->hasDataField(command)) if (!this->hasDataField(command))
break; break;
if (!command["data"].containsKey("hostname") || if (!command["data"]["hostname"].is<const char*>() ||
!strlen(command["data"]["hostname"])) !strlen(command["data"]["hostname"]))
break; break;
@ -75,6 +81,55 @@ void CommandManager::handleCommand(JsonVariant command) {
Serial.println("PONG \n\r"); Serial.println("PONG \n\r");
break; break;
} }
case CommandType::SWITCH_MODE: {
if (!this->hasDataField(command))
break;
if (!command["data"]["mode"].is<int>())
break;
int modeValue = command["data"]["mode"];
DeviceMode newMode = static_cast<DeviceMode>(modeValue);
DeviceMode currentMode = this->deviceConfig->getDeviceModeConfig().mode;
// 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: {
auto networks = this->deviceConfig->getWifiConfigs();
for (auto& network : networks) {
this->deviceConfig->deleteWifiConfig(network.name, false);
}
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: default:
break; break;
} }

View File

@ -10,6 +10,9 @@ enum CommandType {
PING, PING,
SET_WIFI, SET_WIFI,
SET_MDNS, SET_MDNS,
SWITCH_MODE,
WIPE_WIFI_CREDS,
RESTART_DEVICE,
}; };
struct CommandsPayload { struct CommandsPayload {
@ -22,6 +25,9 @@ class CommandManager {
{"ping", CommandType::PING}, {"ping", CommandType::PING},
{"set_wifi", CommandType::SET_WIFI}, {"set_wifi", CommandType::SET_WIFI},
{"set_mdns", CommandType::SET_MDNS}, {"set_mdns", CommandType::SET_MDNS},
{"switch_mode", CommandType::SWITCH_MODE},
{"wipe_wifi_creds", CommandType::WIPE_WIFI_CREDS},
{"restart_device", CommandType::RESTART_DEVICE},
}; };
ProjectConfig* deviceConfig; ProjectConfig* deviceConfig;
@ -33,6 +39,7 @@ class CommandManager {
public: public:
CommandManager(ProjectConfig* deviceConfig); CommandManager(ProjectConfig* deviceConfig);
void handleCommands(CommandsPayload commandsPayload); void handleCommands(CommandsPayload commandsPayload);
ProjectConfig* getDeviceConfig() { return deviceConfig; }
}; };
#endif #endif

View File

@ -28,7 +28,8 @@ struct DeviceStates {
networksConfigUpdated, networksConfigUpdated,
apConfigUpdated, apConfigUpdated,
wifiTxPowerUpdated, wifiTxPowerUpdated,
cameraConfigUpdated cameraConfigUpdated,
deviceModeUpdated
}; };
enum WiFiState_e { enum WiFiState_e {

View File

@ -58,6 +58,12 @@ void ProjectConfig::initConfig() {
.quality = 7, .quality = 7,
.brightness = 2, .brightness = 2,
}; };
// Initialize device mode with default values
this->config.deviceMode = {
.mode = DeviceMode::AUTO_MODE,
.hasWiFiCredentials = false,
};
} }
void ProjectConfig::save() { void ProjectConfig::save() {
@ -67,11 +73,13 @@ void ProjectConfig::save() {
cameraConfigSave(); cameraConfigSave();
wifiConfigSave(); wifiConfigSave();
wifiTxPowerConfigSave(); wifiTxPowerConfigSave();
end(); // we call end() here to close the connection to the NVS partition, we deviceModeConfigSave();
// only do this because we call ESP.restart() next. end(); // we call end() here to close the connection to the NVS partition
OpenIrisTasks::ScheduleRestart(2000); // Removed automatic restart to allow explicit control via RESTART_DEVICE command
// OpenIrisTasks::ScheduleRestart(2000);
} }
void ProjectConfig::wifiConfigSave() { void ProjectConfig::wifiConfigSave() {
log_d("Saving wifi config"); log_d("Saving wifi config");
@ -126,6 +134,15 @@ void ProjectConfig::wifiTxPowerConfigSave() {
putInt("txpower", this->config.txpower.power); putInt("txpower", this->config.txpower.power);
} }
void ProjectConfig::deviceModeConfigSave() {
/* Device Mode Config */
putInt(MODE_KEY, static_cast<int>(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<int>(this->config.deviceMode.mode),
this->config.deviceMode.hasWiFiCredentials);
}
void ProjectConfig::cameraConfigSave() { void ProjectConfig::cameraConfigSave() {
/* Camera Config */ /* Camera Config */
putInt("vflip", this->config.camera.vflip); putInt("vflip", this->config.camera.vflip);
@ -205,6 +222,18 @@ void ProjectConfig::load() {
this->config.camera.quality = getInt("quality", 7); this->config.camera.quality = getInt("quality", 7);
this->config.camera.brightness = getInt("brightness", 2); this->config.camera.brightness = getInt("brightness", 2);
int savedMode = getInt(MODE_KEY, static_cast<int>(DeviceMode::AUTO_MODE));
this->config.deviceMode.mode = static_cast<DeviceMode>(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<int>(this->config.deviceMode.mode),
this->config.deviceMode.hasWiFiCredentials);
this->_already_loaded = true; this->_already_loaded = true;
this->notifyAll(ConfigState_e::configLoaded); this->notifyAll(ConfigState_e::configLoaded);
} }
@ -419,6 +448,14 @@ std::string ProjectConfig::WiFiTxPower_t::toRepresentation() {
return json; return json;
} }
std::string ProjectConfig::DeviceModeConfig_t::toRepresentation() {
std::string json = Helpers::format_string(
"\"device_mode\": {\"mode\": %d, \"hasWiFiCredentials\": %s}",
static_cast<int>(this->mode),
this->hasWiFiCredentials ? "true" : "false");
return json;
}
//********************************************************************************************************************** //**********************************************************************************************************************
//* //*
//! Get Methods //! Get Methods
@ -443,3 +480,32 @@ ProjectConfig::MDNSConfig_t& ProjectConfig::getMDNSConfig() {
ProjectConfig::WiFiTxPower_t& ProjectConfig::getWiFiTxPowerConfig() { ProjectConfig::WiFiTxPower_t& ProjectConfig::getWiFiTxPowerConfig() {
return this->config.txpower; 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<int>(mode));
log_i("[ProjectConfig] Mode set to: %d", static_cast<int>(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;
}

View File

@ -13,6 +13,14 @@
#include "data/utilities/network_utilities.hpp" #include "data/utilities/network_utilities.hpp"
#include "tasks/tasks.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<ConfigState_e> { class ProjectConfig : public Preferences, public ISubject<ConfigState_e> {
public: public:
ProjectConfig(const std::string& name = std::string(), ProjectConfig(const std::string& name = std::string(),
@ -88,6 +96,12 @@ class ProjectConfig : public Preferences, public ISubject<ConfigState_e> {
std::string toRepresentation(); std::string toRepresentation();
}; };
struct DeviceModeConfig_t {
DeviceMode mode;
bool hasWiFiCredentials;
std::string toRepresentation();
};
struct TrackerConfig_t { struct TrackerConfig_t {
DeviceConfig_t device; DeviceConfig_t device;
CameraConfig_t camera; CameraConfig_t camera;
@ -95,6 +109,7 @@ class ProjectConfig : public Preferences, public ISubject<ConfigState_e> {
AP_WiFiConfig_t ap_network; AP_WiFiConfig_t ap_network;
MDNSConfig_t mdns; MDNSConfig_t mdns;
WiFiTxPower_t txpower; WiFiTxPower_t txpower;
DeviceModeConfig_t deviceMode;
}; };
DeviceConfig_t& getDeviceConfig(); DeviceConfig_t& getDeviceConfig();
@ -103,6 +118,7 @@ class ProjectConfig : public Preferences, public ISubject<ConfigState_e> {
AP_WiFiConfig_t& getAPWifiConfig(); AP_WiFiConfig_t& getAPWifiConfig();
MDNSConfig_t& getMDNSConfig(); MDNSConfig_t& getMDNSConfig();
WiFiTxPower_t& getWiFiTxPowerConfig(); WiFiTxPower_t& getWiFiTxPowerConfig();
DeviceModeConfig_t& getDeviceModeConfig();
void setDeviceConfig(const std::string& OTALogin, void setDeviceConfig(const std::string& OTALogin,
const std::string& OTAPassword, const std::string& OTAPassword,
@ -133,11 +149,23 @@ class ProjectConfig : public Preferences, public ISubject<ConfigState_e> {
void deleteWifiConfig(const std::string& networkName, 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: private:
TrackerConfig_t config; TrackerConfig_t config;
std::string _name; std::string _name;
std::string _mdnsName; std::string _mdnsName;
bool _already_loaded; 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 #endif // PROJECT_CONFIG_HPP

View File

@ -3,7 +3,30 @@
SerialManager::SerialManager(CommandManager* commandManager) SerialManager::SerialManager(CommandManager* commandManager)
: commandManager(commandManager) {} : commandManager(commandManager) {}
#ifdef ETVR_EYE_TRACKER_USB_API void SerialManager::sendQuery(QueryAction action,
QueryStatus status,
std::string additional_info) {
JsonDocument doc;
doc["action"] = queryActionMap.at(action);
doc["status"] = static_cast<int>(status);
if (!additional_info.empty()) {
doc["info"] = additional_info;
}
String output;
serializeJson(doc, output);
Serial.println(output);
}
void SerialManager::checkUSBMode() {
// 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");
}
}
void SerialManager::send_frame() { void SerialManager::send_frame() {
if (!last_frame) if (!last_frame)
last_frame = esp_timer_get_time(); 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, log_d("Size: %uKB, Time: %ums (%ifps)\n", len / 1024, latency,
1000 / latency); 1000 / latency);
} }
#endif
void SerialManager::init() { void SerialManager::init() {
#ifdef SERIAL_MANAGER_USE_HIGHER_FREQUENCY #ifdef SERIAL_MANAGER_USE_HIGHER_FREQUENCY
@ -58,25 +80,27 @@ void SerialManager::init() {
if (SERIAL_FLUSH_ENABLED) { if (SERIAL_FLUSH_ENABLED) {
Serial.flush(); Serial.flush();
} }
// Check if we're in USB mode and set up accordingly
checkUSBMode();
} }
void SerialManager::run() { void SerialManager::run() {
// Process any available commands first to ensure mode changes are detected immediately
if (Serial.available()) { if (Serial.available()) {
JsonDocument doc; JsonDocument doc;
DeserializationError deserializationError = deserializeJson(doc, Serial); DeserializationError deserializationError = deserializeJson(doc, Serial);
if (deserializationError) { if (deserializationError) {
log_e("Command deserialization failed: %s", deserializationError.c_str()); log_e("Command deserialization failed: %s", deserializationError.c_str());
} else {
return;
}
CommandsPayload commands = {doc}; CommandsPayload commands = {doc};
this->commandManager->handleCommands(commands); this->commandManager->handleCommands(commands);
} }
#ifdef ETVR_EYE_TRACKER_USB_API }
else {
DeviceMode currentMode = this->commandManager->getDeviceConfig()->getDeviceModeConfig().mode;
if (currentMode == DeviceMode::USB_MODE) {
this->send_frame(); this->send_frame();
} }
#endif
} }

View File

@ -35,12 +35,10 @@ class SerialManager {
esp_err_t err = ESP_OK; esp_err_t err = ESP_OK;
CommandManager* commandManager; CommandManager* commandManager;
#ifdef ETVR_EYE_TRACKER_USB_API
int64_t last_frame = 0; int64_t last_frame = 0;
long last_request_time = 0; long last_request_time = 0;
void send_frame(); void send_frame();
#endif
public: public:
SerialManager(CommandManager* commandManager); SerialManager(CommandManager* commandManager);
@ -49,6 +47,7 @@ class SerialManager {
std::string additional_info); std::string additional_info);
void init(); void init();
void run(); void run();
void checkUSBMode();
}; };
#endif #endif

View File

@ -36,6 +36,8 @@ void BaseAPI::begin() {
DefaultHeaders::Instance().addHeader("Access-Control-Allow-Origin", "*"); 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); // std::bind(&BaseAPI::notFound, &std::placeholders::_1);
server.onNotFound([&](AsyncWebServerRequest* request) { notFound(request); }); server.onNotFound([&](AsyncWebServerRequest* request) { notFound(request); });
} }
@ -214,7 +216,15 @@ void BaseAPI::rebootDevice(AsyncWebServerRequest* request) {
switch (_networkMethodsMap_enum[request->method()]) { switch (_networkMethodsMap_enum[request->method()]) {
case GET: { case GET: {
request->send(200, MIMETYPE_JSON, "{\"msg\":\"Rebooting Device\"}"); request->send(200, MIMETYPE_JSON, "{\"msg\":\"Rebooting Device\"}");
OpenIrisTasks::ScheduleRestart(2000); OpenIrisTasks::ScheduleRestart(2000);
break;
}
case POST: {
request->send(200, MIMETYPE_JSON, "{\"msg\":\"Rebooting Device\"}");
OpenIrisTasks::ScheduleRestart(2000);
break;
} }
default: { default: {
request->send(400, MIMETYPE_JSON, "{\"msg\":\"Invalid Request\"}"); request->send(400, MIMETYPE_JSON, "{\"msg\":\"Invalid Request\"}");
@ -381,7 +391,7 @@ void BaseAPI::beginOTA() {
esp_camera_deinit(); // deinitialize the camera driver esp_camera_deinit(); // deinitialize the camera driver
digitalWrite(PWDN_GPIO_NUM, HIGH); // turn power off to camera module 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); 200, "text/html", ELEGANT_HTML, ELEGANT_HTML_SIZE);
response->addHeader("Content-Encoding", "gzip"); response->addHeader("Content-Encoding", "gzip");
request->send(response); request->send(response);

View File

@ -169,6 +169,19 @@ bool WiFiHandler::iniSTA(const std::string& ssid,
currentMillis = millis(); currentMillis = millis();
log_i("."); log_i(".");
log_d("Progress: %d \n\r", progress); log_d("Progress: %d \n\r", progress);
// Check if mode has been changed to USB mode during connection attempt
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);
return false;
}
if (Serial.available()) {
yield(); // Allow other processes to run
}
if ((currentMillis - startingMillis) >= connectionTimeout) { if ((currentMillis - startingMillis) >= connectionTimeout) {
wifiStateManager.setState(WiFiState_e::WiFiState_Error); wifiStateManager.setState(WiFiState_e::WiFiState_Error);
log_e("Connection to: %s TIMEOUT \n\r", ssid.c_str()); log_e("Connection to: %s TIMEOUT \n\r", ssid.c_str());

View File

@ -38,10 +38,33 @@ StreamServer streamServer;
#endif // SIM_ENABLED #endif // SIM_ENABLED
void etvr_eye_tracker_web_init() { void etvr_eye_tracker_web_init() {
// Check if mode has been changed to USB mode before starting network initialization
if (deviceConfig.getDeviceModeConfig().mode == DeviceMode::USB_MODE) {
log_i("[SETUP]: Mode changed to USB before network initialization, aborting");
WiFi.disconnect(true);
return;
}
log_d("[SETUP]: Starting Network Handler"); log_d("[SETUP]: Starting Network Handler");
deviceConfig.attach(mdnsHandler); deviceConfig.attach(mdnsHandler);
// Check mode again before starting WiFi
if (deviceConfig.getDeviceModeConfig().mode == DeviceMode::USB_MODE) {
log_i("[SETUP]: Mode changed to USB before WiFi initialization, aborting");
WiFi.disconnect(true);
return;
}
log_d("[SETUP]: Starting WiFi Handler"); log_d("[SETUP]: Starting WiFi Handler");
wifiHandler.begin(); wifiHandler.begin();
// Check mode again before starting MDNS
if (deviceConfig.getDeviceModeConfig().mode == DeviceMode::USB_MODE) {
log_i("[SETUP]: Mode changed to USB before MDNS initialization, aborting");
WiFi.disconnect(true);
return;
}
log_d("[SETUP]: Starting MDNS Handler"); log_d("[SETUP]: Starting MDNS Handler");
mdnsHandler.startMDNS(); mdnsHandler.startMDNS();
@ -86,6 +109,8 @@ void setup() {
Logo::printASCII(); Logo::printASCII();
ledManager.begin(); ledManager.begin();
// Device mode is now managed by ProjectConfig
#ifdef CONFIG_CAMERA_MODULE_SWROOM_BABBLE_S3 // Set IR emitter strength to 100%. #ifdef CONFIG_CAMERA_MODULE_SWROOM_BABBLE_S3 // Set IR emitter strength to 100%.
const int ledPin = 1; // Replace this with a command endpoint eventually. const int ledPin = 1; // Replace this with a command endpoint eventually.
const int freq = 5000; const int freq = 5000;
@ -104,11 +129,20 @@ void setup() {
serialManager.init(); serialManager.init();
#ifndef ETVR_EYE_TRACKER_USB_API DeviceMode currentMode = deviceConfig.getDeviceModeConfig().mode;
if (currentMode == DeviceMode::WIFI_MODE) {
// Initialize WiFi mode
etvr_eye_tracker_web_init(); etvr_eye_tracker_web_init();
#else // ETVR_EYE_TRACKER_WEB_API 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); WiFi.disconnect(true);
#endif // ETVR_EYE_TRACKER_WEB_API log_i("[SETUP]: Initialized in USB mode");
}
} }
void loop() { void loop() {

View File

@ -0,0 +1,368 @@
#!/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
self.last_boot_time = 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 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, 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():
print("❌ Serial connection failed. Aborting test.")
return test_results
test_results["serial_connection"] = True
print("✅ Serial connection successful")
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 3: Discovering device on network ===")
if tester.discover_device():
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")
# 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 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")
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 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")
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}")
return test_results
finally:
tester.close()
if __name__ == "__main__":
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()
# 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)