Add mode switching without reflash

This commit is contained in:
Summer 2025-03-15 11:10:56 -07:00
parent 5da262c8da
commit c5906f4758
10 changed files with 444 additions and 25 deletions

1
.gitignore vendored Normal file
View File

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

View File

@ -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<const char*>())
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<JsonObject>();
}
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");
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<const char*>() ||
!command["data"]["password"].is<const char*>())
break;
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>();
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<const char*>() ||
!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<int>())
break;
int modeValue = command["data"]["mode"];
DeviceMode newMode = static_cast<DeviceMode>(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;
}

View File

@ -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;

View File

@ -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<int>(DeviceMode::AUTO_MODE));
currentMode = static_cast<DeviceMode>(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<int>(currentMode));
}
DeviceMode DeviceModeManager::getMode() {
return currentMode;
}
void DeviceModeManager::setMode(DeviceMode mode) {
currentMode = mode;
preferences.putInt(MODE_KEY, static_cast<int>(mode));
log_i("[DeviceModeManager] Mode set to: %d", static_cast<int>(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();
}
}

View File

@ -0,0 +1,47 @@
#pragma once
#ifndef DEVICE_MODE_HPP
#define DEVICE_MODE_HPP
#include <Arduino.h>
#include <Preferences.h>
#include <string>
// 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

View File

@ -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<int>(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
}

View File

@ -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

View File

@ -2,6 +2,7 @@
#include <WiFi.h>
#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());

View File

@ -1,4 +1,5 @@
#include <openiris.hpp>
#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() {

View File

@ -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)