diff --git a/boards/sdkconfig.base_defaults b/boards/sdkconfig.base_defaults index a624d2a..3d85c97 100644 --- a/boards/sdkconfig.base_defaults +++ b/boards/sdkconfig.base_defaults @@ -588,6 +588,7 @@ CONFIG_CAMERA_WIFI_XCLK_FREQ=16500000 # CONFIG_WIFI_MDNS_HOSTNAME="openiristracker" CONFIG_WIFI_SSID="" +CONFIG_WIFI_BSSID="" CONFIG_WIFI_PASSWORD="" CONFIG_WIFI_AP_SSID="EyeTrackVR" CONFIG_WIFI_AP_PASSWORD="12345678" diff --git a/components/CommandManager/CommandManager/CommandSchema.cpp b/components/CommandManager/CommandManager/CommandSchema.cpp index 236f0ca..8655520 100644 --- a/components/CommandManager/CommandManager/CommandSchema.cpp +++ b/components/CommandManager/CommandManager/CommandSchema.cpp @@ -1,8 +1,33 @@ #include "CommandSchema.hpp" +void to_json(nlohmann::json& j, const WifiPayload& payload) +{ + j = nlohmann::json{ + {"name", payload.name}, {"ssid", payload.ssid}, {"bssid", payload.bssid}, + {"password", payload.password}, {"channel", payload.channel}, {"power", payload.power}, + }; +} + +void from_json(const nlohmann::json& j, WifiPayload& payload) +{ + payload.name = j.at("name").get(); + payload.ssid = j.at("ssid").get(); + payload.password = j.at("password").get(); + payload.channel = j.at("channel").get(); + payload.power = j.at("power").get(); + + if (j.contains("bssid")) + { + payload.bssid = j.at("bssid").get(); + } +} + void to_json(nlohmann::json& j, const UpdateWifiPayload& payload) { - j = nlohmann::json{{"name", payload.name}, {"ssid", payload.ssid}, {"password", payload.password}, {"channel", payload.channel}, {"power", payload.power}}; + j = nlohmann::json{ + {"name", payload.name}, {"ssid", payload.ssid}, {"bssid", payload.bssid}, + {"password", payload.password}, {"channel", payload.channel}, {"power", payload.power}, + }; } void from_json(const nlohmann::json& j, UpdateWifiPayload& payload) @@ -13,6 +38,11 @@ void from_json(const nlohmann::json& j, UpdateWifiPayload& payload) payload.ssid = j.at("ssid").get(); } + if (j.contains("bssid")) + { + payload.bssid = j.at("bssid").get(); + } + if (j.contains("password")) { payload.password = j.at("password").get(); @@ -54,7 +84,8 @@ void from_json(const nlohmann::json& j, UpdateAPWiFiPayload& payload) void to_json(nlohmann::json& j, const UpdateCameraConfigPayload& payload) { j = nlohmann::json{ - {"vflip", payload.vflip}, {"href", payload.href}, {"framesize", payload.framesize}, {"quality", payload.quality}, {"brightness", payload.brightness}}; + {"vflip", payload.vflip}, {"href", payload.href}, {"framesize", payload.framesize}, {"quality", payload.quality}, {"brightness", payload.brightness}, + }; } void from_json(const nlohmann::json& j, UpdateCameraConfigPayload& payload) diff --git a/components/CommandManager/CommandManager/CommandSchema.hpp b/components/CommandManager/CommandManager/CommandSchema.hpp index 171a5f7..8dc508c 100644 --- a/components/CommandManager/CommandManager/CommandSchema.hpp +++ b/components/CommandManager/CommandManager/CommandSchema.hpp @@ -10,17 +10,20 @@ struct WifiPayload : BasePayload { std::string name; std::string ssid; + std::optional bssid; std::string password; uint8_t channel; uint8_t power; }; -NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE(WifiPayload, name, ssid, password, channel, power) +void to_json(nlohmann::json& j, const WifiPayload& payload); +void from_json(const nlohmann::json& j, WifiPayload& payload); struct UpdateWifiPayload : BasePayload { std::string name; std::optional ssid; + std::optional bssid; std::optional password; std::optional channel; std::optional power; diff --git a/components/CommandManager/CommandManager/commands/wifi_commands.cpp b/components/CommandManager/CommandManager/commands/wifi_commands.cpp index 2b789d6..02a9502 100644 --- a/components/CommandManager/CommandManager/commands/wifi_commands.cpp +++ b/components/CommandManager/CommandManager/commands/wifi_commands.cpp @@ -19,8 +19,16 @@ CommandResult setWiFiCommand(std::shared_ptr registry, const return CommandResult::getErrorResult("Invalid payload: missing SSID"); } + // format is XX:XX:XX:XX:XX:XX + const std::string bssid = payload.bssid.has_value() ? payload.bssid.value() : ""; + const auto bssid_len = bssid.length(); + if (bssid_len > 0 && bssid_len != 17) + { + return CommandResult::getErrorResult("BSSID is malformed"); + } + std::shared_ptr projectConfig = registry->resolve(DependencyType::project_config); - projectConfig->setWifiConfig(payload.name, payload.ssid, payload.password, payload.channel, payload.power); + projectConfig->setWifiConfig(payload.name, payload.ssid, bssid, payload.password, payload.channel, payload.power); return CommandResult::getSuccessResult("Config updated"); } @@ -53,12 +61,22 @@ CommandResult updateWiFiCommand(std::shared_ptr registry, co return CommandResult::getErrorResult("Invalid payload - missing network name"); } + if (payload.bssid.has_value()) + { + auto bssid_len = payload.bssid.value().length(); + if (bssid_len > 0 && bssid_len != 11) + { + return CommandResult::getErrorResult("BSSID is malformed"); + } + } + auto projectConfig = registry->resolve(DependencyType::project_config); auto storedNetworks = projectConfig->getWifiConfigs(); if (const auto networkToUpdate = std::ranges::find_if(storedNetworks, [&](auto& network) { return network.name == payload.name; }); networkToUpdate != storedNetworks.end()) { projectConfig->setWifiConfig(payload.name, payload.ssid.has_value() ? payload.ssid.value() : networkToUpdate->ssid, + payload.bssid.has_value() ? payload.bssid.value() : networkToUpdate->bssid, payload.password.has_value() ? payload.password.value() : networkToUpdate->password, payload.channel.has_value() ? payload.channel.value() : networkToUpdate->channel, payload.power.has_value() ? payload.power.value() : networkToUpdate->power); diff --git a/components/ProjectConfig/ProjectConfig/Models.hpp b/components/ProjectConfig/ProjectConfig/Models.hpp index f7c9a8f..5d6a598 100644 --- a/components/ProjectConfig/ProjectConfig/Models.hpp +++ b/components/ProjectConfig/ProjectConfig/Models.hpp @@ -170,14 +170,23 @@ struct WiFiConfig_t : BaseConfigModel // default constructor used for loading WiFiConfig_t(Preferences* pref) : BaseConfigModel(pref) {} - WiFiConfig_t(Preferences* pref, const uint8_t index, std::string name, std::string ssid, std::string password, const uint8_t channel, const uint8_t power) - : BaseConfigModel(pref), index(index), name(std::move(name)), ssid(std::move(ssid)), password(std::move(password)), channel(channel), power(power) + WiFiConfig_t(Preferences* pref, const uint8_t index, std::string name, std::string ssid, std::string bssid, std::string password, const uint8_t channel, + const uint8_t power) + : BaseConfigModel(pref), + index(index), + name(std::move(name)), + ssid(std::move(ssid)), + bssid(std::move(bssid)), + password(std::move(password)), + channel(channel), + power(power) { } uint8_t index; std::string name; std::string ssid; + std::string bssid; std::string password; uint8_t channel; uint8_t power; @@ -190,6 +199,7 @@ struct WiFiConfig_t : BaseConfigModel auto const iter_str = std::string(Helpers::itoa(index, buffer, 10)); this->name = this->pref->getString(("name" + iter_str).c_str(), ""); this->ssid = this->pref->getString(("ssid" + iter_str).c_str(), ""); + this->bssid = this->pref->getString(("bssid" + iter_str).c_str(), ""); this->password = this->pref->getString(("password" + iter_str).c_str(), ""); this->channel = this->pref->getUInt(("channel" + iter_str).c_str()); this->power = this->pref->getUInt(("power" + iter_str).c_str()); @@ -204,6 +214,7 @@ struct WiFiConfig_t : BaseConfigModel this->pref->putString(("name" + iter_str).c_str(), this->name.c_str()); this->pref->putString(("ssid" + iter_str).c_str(), this->ssid.c_str()); + this->pref->putString(("bssid" + iter_str).c_str(), this->bssid.c_str()); this->pref->putString(("password" + iter_str).c_str(), this->password.c_str()); this->pref->putUInt(("channel" + iter_str).c_str(), this->channel); this->pref->putUInt(("power" + iter_str).c_str(), this->power); @@ -213,8 +224,8 @@ struct WiFiConfig_t : BaseConfigModel std::string toRepresentation() { - return Helpers::format_string("{\"name\": \"%s\", \"ssid\": \"%s\", \"password\": \"%s\", \"channel\": %u, \"power\": %u}", this->name.c_str(), - this->ssid.c_str(), this->password.c_str(), this->channel, this->power); + return Helpers::format_string("{\"name\": \"%s\", \"ssid\": \"%s\", \"bssid\": \"%s\", \"password\": \"%s\", \"channel\": %u, \"power\": %u}", + this->name.c_str(), this->ssid.c_str(), this->bssid.c_str(), this->password.c_str(), this->channel, this->power); }; }; diff --git a/components/ProjectConfig/ProjectConfig/ProjectConfig.cpp b/components/ProjectConfig/ProjectConfig/ProjectConfig.cpp index 5e030ed..560e260 100644 --- a/components/ProjectConfig/ProjectConfig/ProjectConfig.cpp +++ b/components/ProjectConfig/ProjectConfig/ProjectConfig.cpp @@ -127,7 +127,8 @@ void ProjectConfig::setCameraConfig(const uint8_t vflip, const uint8_t framesize ESP_LOGD(CONFIGURATION_TAG, "Updating Camera config"); } -void ProjectConfig::setWifiConfig(const std::string& networkName, const std::string& ssid, const std::string& password, uint8_t channel, uint8_t power) +void ProjectConfig::setWifiConfig(const std::string& networkName, const std::string& ssid, const std::string& bssid, const std::string& password, + uint8_t channel, uint8_t power) { const auto size = this->config.networks.size(); @@ -139,6 +140,7 @@ void ProjectConfig::setWifiConfig(const std::string& networkName, const std::str it->name = networkName; it->ssid = ssid; + it->bssid = bssid; it->password = password; it->channel = channel; it->power = power; @@ -150,7 +152,7 @@ void ProjectConfig::setWifiConfig(const std::string& networkName, const std::str if (size == 0) { ESP_LOGI(CONFIGURATION_TAG, "No networks, We're adding a new network"); - this->config.networks.emplace_back(this->pref, static_cast(0), networkName, ssid, password, channel, power); + this->config.networks.emplace_back(this->pref, static_cast(0), networkName, ssid, bssid, password, channel, power); // Save the new network immediately this->config.networks.back().save(); saveNetworkCount(this->pref, 1); @@ -162,10 +164,10 @@ void ProjectConfig::setWifiConfig(const std::string& networkName, const std::str { ESP_LOGI(CONFIGURATION_TAG, "We're adding a new network"); // we don't have that network yet, we can add it as we still have some - // space we're using emplace_back as push_back will create a copy of it, + // space, we're using emplace_back as push_back will create a copy of it, // we want to avoid that uint8_t last_index = getNetworkCount(this->pref); - this->config.networks.emplace_back(this->pref, last_index, networkName, ssid, password, channel, power); + this->config.networks.emplace_back(this->pref, last_index, networkName, ssid, bssid, password, channel, power); // Save the new network immediately this->config.networks.back().save(); saveNetworkCount(this->pref, static_cast(this->config.networks.size())); diff --git a/components/ProjectConfig/ProjectConfig/ProjectConfig.hpp b/components/ProjectConfig/ProjectConfig/ProjectConfig.hpp index af83ac6..33f128d 100644 --- a/components/ProjectConfig/ProjectConfig/ProjectConfig.hpp +++ b/components/ProjectConfig/ProjectConfig/ProjectConfig.hpp @@ -37,7 +37,8 @@ class ProjectConfig void setLEDDUtyCycleConfig(int led_external_pwm_duty_cycle); void setMDNSConfig(const std::string& hostname); void setCameraConfig(uint8_t vflip, uint8_t framesize, uint8_t href, uint8_t quality, uint8_t brightness); - void setWifiConfig(const std::string& networkName, const std::string& ssid, const std::string& password, uint8_t channel, uint8_t power); + void setWifiConfig(const std::string& networkName, const std::string& ssid, const std::string& bssid, const std::string& password, uint8_t channel, + uint8_t power); void deleteWifiConfig(const std::string& networkName); diff --git a/components/wifiManager/wifiManager/WiFiScanner.cpp b/components/wifiManager/wifiManager/WiFiScanner.cpp index 6e15663..7057dac 100644 --- a/components/wifiManager/wifiManager/WiFiScanner.cpp +++ b/components/wifiManager/wifiManager/WiFiScanner.cpp @@ -6,42 +6,6 @@ static const char* TAG = "WiFiScanner"; WiFiScanner::WiFiScanner() {} -void WiFiScanner::scanResultCallback(void* arg, esp_event_base_t event_base, int32_t event_id, void* event_data) -{ - auto* scanner = static_cast(arg); - if (event_base == WIFI_EVENT && event_id == WIFI_EVENT_SCAN_DONE) - { - uint16_t ap_count = 0; - esp_wifi_scan_get_ap_num(&ap_count); - - if (ap_count == 0) - { - ESP_LOGI(TAG, "No access points found"); - return; - } - - wifi_ap_record_t* ap_records = new wifi_ap_record_t[ap_count]; - ESP_ERROR_CHECK(esp_wifi_scan_get_ap_records(&ap_count, ap_records)); - - scanner->networks.clear(); - for (uint16_t i = 0; i < ap_count; i++) - { - WiFiNetwork network; - network.ssid = std::string(reinterpret_cast(ap_records[i].ssid)); - network.channel = ap_records[i].primary; - network.rssi = ap_records[i].rssi; - memcpy(network.mac, ap_records[i].bssid, 6); - network.auth_mode = ap_records[i].authmode; - - scanner->networks.push_back(network); - } - - delete[] ap_records; - ESP_LOGI(TAG, "Found %d access points", ap_count); - } -} - -// todo this is garbage std::vector WiFiScanner::scanNetworks(int timeout_ms) { std::vector scan_results; @@ -61,176 +25,67 @@ std::vector WiFiScanner::scanNetworks(int timeout_ms) // Stop any ongoing scan esp_wifi_scan_stop(); - // Try sequential channel scanning as a workaround - bool try_sequential_scan = true; // Enable sequential scan + // Sequential channel scan - scan each channel individually with timeout tracking + std::vector all_records; + int64_t start_time = esp_timer_get_time() / 1000; // Convert to ms - if (!try_sequential_scan) + for (uint8_t ch = 1; ch <= 13; ch++) { - // Normal all-channel scan - wifi_scan_config_t scan_config = { - .ssid = nullptr, - .bssid = nullptr, - .channel = 0, // 0 means scan all channels - .show_hidden = true, - .scan_type = WIFI_SCAN_TYPE_ACTIVE, // Active scan - .scan_time = {.active = - { - .min = 120, // Min per channel - .max = 300 // Max per channel - }, - .passive = 360}, - .home_chan_dwell_time = 0, // 0 for default - .channel_bitmap = 0 // 0 for all channels - }; + // Check if we've exceeded the timeout + int64_t current_time = esp_timer_get_time() / 1000; + int64_t elapsed = current_time - start_time; - err = esp_wifi_scan_start(&scan_config, false); - if (err != ESP_OK) + if (elapsed >= timeout_ms) { - ESP_LOGE(TAG, "Failed to start scan: %s", esp_err_to_name(err)); - return scan_results; + ESP_LOGW(TAG, "Sequential scan timeout after %lld ms at channel %d", elapsed, ch); + break; } - } - else - { - // Sequential channel scan - scan each channel individually with timeout tracking - std::vector all_records; - int64_t start_time = esp_timer_get_time() / 1000; // Convert to ms - for (uint8_t ch = 1; ch <= 13; ch++) + wifi_scan_config_t scan_config = {.ssid = nullptr, + .bssid = nullptr, + .channel = ch, + .show_hidden = true, + .scan_type = WIFI_SCAN_TYPE_ACTIVE, + .scan_time = {.active = {.min = 100, .max = 200}, .passive = 300}, + .home_chan_dwell_time = 0, + .channel_bitmap = 0}; + + err = esp_wifi_scan_start(&scan_config, true); // Blocking scan + if (err == ESP_OK) { - // Check if we've exceeded the timeout - int64_t current_time = esp_timer_get_time() / 1000; - int64_t elapsed = current_time - start_time; - - if (elapsed >= timeout_ms) - { - ESP_LOGW(TAG, "Sequential scan timeout after %lld ms at channel %d", elapsed, ch); - break; - } - - wifi_scan_config_t scan_config = {.ssid = nullptr, - .bssid = nullptr, - .channel = ch, - .show_hidden = true, - .scan_type = WIFI_SCAN_TYPE_ACTIVE, - .scan_time = {.active = {.min = 100, .max = 200}, .passive = 300}, - .home_chan_dwell_time = 0, - .channel_bitmap = 0}; - - err = esp_wifi_scan_start(&scan_config, true); // Blocking scan - if (err == ESP_OK) + uint16_t ch_count = 0; + esp_wifi_scan_get_ap_num(&ch_count); + if (ch_count > 0) { - uint16_t ch_count = 0; - esp_wifi_scan_get_ap_num(&ch_count); - if (ch_count > 0) + wifi_ap_record_t* ch_records = new wifi_ap_record_t[ch_count]; + if (esp_wifi_scan_get_ap_records(&ch_count, ch_records) == ESP_OK) { - wifi_ap_record_t* ch_records = new wifi_ap_record_t[ch_count]; - if (esp_wifi_scan_get_ap_records(&ch_count, ch_records) == ESP_OK) + for (uint16_t i = 0; i < ch_count; i++) { - for (uint16_t i = 0; i < ch_count; i++) - { - all_records.push_back(ch_records[i]); - } + all_records.push_back(ch_records[i]); } - delete[] ch_records; } + delete[] ch_records; } - vTaskDelay(pdMS_TO_TICKS(50)); - } - - // Process all collected records - for (const auto& record : all_records) - { - WiFiNetwork network; - network.ssid = std::string(reinterpret_cast(record.ssid)); - network.channel = record.primary; - network.rssi = record.rssi; - memcpy(network.mac, record.bssid, 6); - network.auth_mode = record.authmode; - scan_results.push_back(network); - } - - int64_t total_time = (esp_timer_get_time() / 1000) - start_time; - ESP_LOGI(TAG, "Sequential scan completed in %lld ms, found %d APs", total_time, scan_results.size()); - - // Skip the normal result processing - return scan_results; - } - - // Wait for scan completion with timeout - int64_t start_time = esp_timer_get_time() / 1000; // Convert to ms - int64_t elapsed_ms = 0; - bool scan_done = false; - - while (elapsed_ms < timeout_ms) - { - // Check if scan is actually complete by trying to get AP count - // When scan is done, this will return ESP_OK with a valid count - uint16_t temp_count = 0; - esp_err_t count_err = esp_wifi_scan_get_ap_num(&temp_count); - - // If we can successfully get the AP count, the scan is likely complete - // However, we should still wait for the scan to fully finish - if (count_err == ESP_OK && temp_count > 0) - { - // Give it a bit more time to ensure all channels are scanned - vTaskDelay(pdMS_TO_TICKS(500)); - scan_done = true; - break; } - - vTaskDelay(pdMS_TO_TICKS(200)); - elapsed_ms = (esp_timer_get_time() / 1000) - start_time; - } - - if (!scan_done && elapsed_ms >= timeout_ms) - { - ESP_LOGE(TAG, "Scan timeout after %lld ms", elapsed_ms); - esp_wifi_scan_stop(); - return scan_results; + vTaskDelay(pdMS_TO_TICKS(50)); } - // Get scan results - uint16_t ap_count = 0; - esp_wifi_scan_get_ap_num(&ap_count); - - if (ap_count == 0) - { - ESP_LOGI(TAG, "No access points found"); - return scan_results; - } - - wifi_ap_record_t* ap_records = new wifi_ap_record_t[ap_count]; - err = esp_wifi_scan_get_ap_records(&ap_count, ap_records); - if (err != ESP_OK) - { - ESP_LOGE(TAG, "Failed to get scan records: %s", esp_err_to_name(err)); - delete[] ap_records; - return scan_results; - } - - // Build the results vector and track channels found - bool channels_found[15] = {false}; // Track channels 0-14 - - for (uint16_t i = 0; i < ap_count; i++) + // Process all collected records + for (const auto& record : all_records) { WiFiNetwork network; - network.ssid = std::string(reinterpret_cast(ap_records[i].ssid)); - network.channel = ap_records[i].primary; - network.rssi = ap_records[i].rssi; - memcpy(network.mac, ap_records[i].bssid, 6); - network.auth_mode = ap_records[i].authmode; - - if (network.channel <= 14) - { - channels_found[network.channel] = true; - } - + network.ssid = std::string(reinterpret_cast(record.ssid)); + network.channel = record.primary; + network.rssi = record.rssi; + memcpy(network.mac, record.bssid, 6); + network.auth_mode = record.authmode; scan_results.push_back(network); } - delete[] ap_records; - ESP_LOGI(TAG, "Found %d access points", ap_count); + int64_t total_time = (esp_timer_get_time() / 1000) - start_time; + ESP_LOGI(TAG, "Sequential scan completed in %lld ms, found %d APs", total_time, scan_results.size()); + // Skip the normal result processing return scan_results; } \ No newline at end of file diff --git a/components/wifiManager/wifiManager/WiFiScanner.hpp b/components/wifiManager/wifiManager/WiFiScanner.hpp index 837c551..043c815 100644 --- a/components/wifiManager/wifiManager/WiFiScanner.hpp +++ b/components/wifiManager/wifiManager/WiFiScanner.hpp @@ -21,7 +21,6 @@ class WiFiScanner public: WiFiScanner(); std::vector scanNetworks(int timeout_ms = 15000); - static void scanResultCallback(void* arg, esp_event_base_t event_base, int32_t event_id, void* event_data); private: std::vector networks; diff --git a/components/wifiManager/wifiManager/wifiManager.cpp b/components/wifiManager/wifiManager/wifiManager.cpp index 8886a93..a63670a 100644 --- a/components/wifiManager/wifiManager/wifiManager.cpp +++ b/components/wifiManager/wifiManager/wifiManager.cpp @@ -1,4 +1,8 @@ #include "wifiManager.hpp" +#include +#include +#include +#include static auto WIFI_MANAGER_TAG = "[WIFI_MANAGER]"; @@ -47,7 +51,25 @@ WiFiManager::WiFiManager(std::shared_ptr deviceConfig, QueueHandl { } -void WiFiManager::SetCredentials(const char* ssid, const char* password) +std::vector WiFiManager::ParseBSSID(std::string_view bssid_string) +{ + return bssid_string + // We format the bssid/mac address as XX:XX:XX:XX:XX:XX + | std::views::split(':') + // Once we have that, we can convert each sub range into a proper uint8_t value + | std::views::transform( + [](auto&& subrange) -> uint8_t + { + auto view = std::string_view(subrange); + uint8_t result{}; + std::from_chars(view.begin(), view.end(), result, 16); + return result; + }) + // and now group them into the vector we need + | std::ranges::to>(); +} + +void WiFiManager::SetCredentials(const char* ssid, const std::vector bssid, const char* password, bool use_bssid) { // Clear the config first memset(&_wifi_cfg, 0, sizeof(_wifi_cfg)); @@ -62,6 +84,13 @@ void WiFiManager::SetCredentials(const char* ssid, const char* password) memcpy(_wifi_cfg.sta.password, password, pass_len); _wifi_cfg.sta.password[pass_len] = '\0'; + // if we can use bssid, just copy it. Parser makes sure we do not exceed 6 elements so we should be safe here + // if we fail to parse, the vec will be empty, so use_bssid won't be set + if (use_bssid) + { + std::copy(bssid.begin(), bssid.end(), _wifi_cfg.sta.bssid); + } + // Set other required fields // Use open auth if no password, otherwise allow any WPA variant if (strlen(password) == 0) @@ -81,8 +110,8 @@ void WiFiManager::SetCredentials(const char* ssid, const char* password) // OPTIMIZATION: Use fast scan instead of all channel scan for quicker connection _wifi_cfg.sta.scan_method = WIFI_FAST_SCAN; - _wifi_cfg.sta.bssid_set = 0; // Don't use specific BSSID - _wifi_cfg.sta.channel = 0; // Auto channel detection + _wifi_cfg.sta.bssid_set = use_bssid; // Don't use specific BSSID + _wifi_cfg.sta.channel = 0; // Auto channel detection // Additional settings that might help with compatibility _wifi_cfg.sta.listen_interval = 0; // Default listen interval @@ -101,7 +130,8 @@ void WiFiManager::SetCredentials(const char* ssid, const char* password) void WiFiManager::ConnectWithHardcodedCredentials() { SystemEvent event = {EventSource::WIFI, WiFiState_e::WiFiState_ReadyToConnect}; - this->SetCredentials(CONFIG_WIFI_SSID, CONFIG_WIFI_PASSWORD); + const auto bssid = this->ParseBSSID(std::string_view(CONFIG_WIFI_BSSID)); + this->SetCredentials(CONFIG_WIFI_SSID, bssid, CONFIG_WIFI_PASSWORD, bssid.size()); wifi_mode_t mode; if (esp_wifi_get_mode(&mode) == ESP_OK) @@ -170,7 +200,8 @@ void WiFiManager::ConnectWithStoredCredentials() // Reset retry counter for each network attempt s_retry_num = 0; xEventGroupClearBits(s_wifi_event_group, WIFI_FAIL_BIT | WIFI_CONNECTED_BIT); - this->SetCredentials(network.ssid.c_str(), network.password.c_str()); + auto bssid = this->ParseBSSID(std::string_view(network.bssid)); + this->SetCredentials(network.ssid.c_str(), bssid, network.password.c_str(), bssid.size()); // Update config without stopping WiFi again ESP_LOGI(WIFI_MANAGER_TAG, "Attempting to connect to SSID: '%s'", network.ssid.c_str()); diff --git a/components/wifiManager/wifiManager/wifiManager.hpp b/components/wifiManager/wifiManager/wifiManager.hpp index d819364..eab5a93 100644 --- a/components/wifiManager/wifiManager/wifiManager.hpp +++ b/components/wifiManager/wifiManager/wifiManager.hpp @@ -7,6 +7,7 @@ #include #include #include +#include #include "WiFiScanner.hpp" #include "esp_event.h" @@ -40,10 +41,11 @@ class WiFiManager int8_t power; - void SetCredentials(const char* ssid, const char* password); + void SetCredentials(const char* ssid, const std::vector bssid, const char* password, bool use_bssid); void ConnectWithHardcodedCredentials(); void ConnectWithStoredCredentials(); void SetupAccessPoint(); + std::vector ParseBSSID(std::string_view bssid_string); public: WiFiManager(std::shared_ptr deviceConfig, QueueHandle_t eventQueue, StateManager* stateManager); diff --git a/main/Kconfig.projbuild b/main/Kconfig.projbuild index 1a81688..d555808 100644 --- a/main/Kconfig.projbuild +++ b/main/Kconfig.projbuild @@ -93,6 +93,10 @@ menu "OpenIris: WiFi Configuration" string "WiFi network name (SSID)" default "" + config WIFI_BSSID + string "WiFi network MAC Address (BSSID), completely optional, used in special cases" + default "" + config WIFI_PASSWORD string "WiFi password" default "" diff --git a/sdkconfig b/sdkconfig index 1ee1e52..3edd547 100644 --- a/sdkconfig +++ b/sdkconfig @@ -590,6 +590,7 @@ CONFIG_CAMERA_WIFI_XCLK_FREQ=16500000 # OpenIris: WiFi Configuration # CONFIG_WIFI_SSID="" +CONFIG_WIFI_BSSID="" CONFIG_WIFI_PASSWORD="" CONFIG_WIFI_AP_SSID="EyeTrackVR" CONFIG_WIFI_AP_PASSWORD="12345678" diff --git a/tests/.env.example b/tests/.env.example index d59b7c2..a084981 100644 --- a/tests/.env.example +++ b/tests/.env.example @@ -1,4 +1,5 @@ WIFI_SSID= +WIFI_BSSID= WIFI_PASS= SWITCH_MODE_REBOOT_TIME=5 WIFI_CONNECTION_TIMEOUT=5 diff --git a/tests/conftest.py b/tests/conftest.py index 2a5a485..f946cb9 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -37,7 +37,8 @@ def pytest_addoption(parser): def pytest_configure(config): config.addinivalue_line( - "markers", "has_capability(caps): skip if the board does not have the capability" + "markers", + "has_capability(caps): skip if the board does not have the capability", ) config.addinivalue_line( "markers", "lacks_capability(caps): skip if the board DOES have the capability" @@ -60,7 +61,7 @@ def check_capability_marker(request, board_lacks_capability): "has_capability marker must be provided with a capability to check" ) - for capability in marker.args: + for capability in marker.args: if board_lacks_capability(capability): pytest.skip(f"Board does not have capability {capability}") @@ -72,7 +73,7 @@ def check_lacks_capability_marker(request, board_lacks_capability): raise ValueError( "lacks_capability marker must be provided with a capability to check" ) - + for capability in lacks_capability_marker.args: if not board_lacks_capability(capability): pytest.skip( @@ -119,6 +120,7 @@ def board_connection(request): @dataclass class TestConfig: WIFI_SSID: str + WIFI_BSSID: str WIFI_PASS: str SWITCH_MODE_REBOOT_TIME: int WIFI_CONNECTION_TIMEOUT: int @@ -127,12 +129,14 @@ class TestConfig: def __init__( self, WIFI_SSID: str, + WIFI_BSSID: str, WIFI_PASS: str, SWITCH_MODE_REBOOT_TIME: int, WIFI_CONNECTION_TIMEOUT: int, INVALID_WIFI_CONNECTION_TIMEOUT: int, ): self.WIFI_SSID = WIFI_SSID + self.WIFI_BSSID = WIFI_BSSID self.WIFI_PASS = WIFI_PASS self.SWITCH_MODE_REBOOT_TIME = int(SWITCH_MODE_REBOOT_TIME) self.WIFI_CONNECTION_TIMEOUT = int(WIFI_CONNECTION_TIMEOUT) diff --git a/tests/test_commands.py b/tests/test_commands.py index 886432c..59bddae 100644 --- a/tests/test_commands.py +++ b/tests/test_commands.py @@ -255,8 +255,39 @@ def test_reset_config(get_openiris_device, config): @pytest.mark.has_capability("wireless") -def test_set_wifi(get_openiris_device, ensure_board_in_mode, config): - # since we want to test actual connection to the network, let's reset the device and reboot it +def test_set_wifi_no_bssid_in_payload( + get_openiris_device, ensure_board_in_mode, config +): + device = get_openiris_device() + reset_command = device.send_command("reset_config", {"section": "all"}) + assert not has_command_failed(reset_command) + + with DetectPortChange(): + device.send_command("restart_device") + time.sleep(config.SWITCH_MODE_REBOOT_TIME) + + device = ensure_board_in_mode("wifi", device) + params = { + "name": "main", + "ssid": config.WIFI_SSID, + "password": config.WIFI_PASS, + "channel": 0, + "power": 0, + } + set_wifi_result = device.send_command("set_wifi", params) + assert not has_command_failed(set_wifi_result) + + connect_wifi_result = device.send_command("connect_wifi") + assert not -has_command_failed(connect_wifi_result) + time.sleep(config.WIFI_CONNECTION_TIMEOUT) # and let it try to for some time + + wifi_status_command = device.send_command("get_wifi_status") + assert not has_command_failed(wifi_status_command) + assert wifi_status_command["results"][0]["result"]["data"]["status"] == "connected" + + +@pytest.mark.has_capability("wireless") +def test_set_wifi_no_bssid(get_openiris_device, ensure_board_in_mode, config): device = get_openiris_device() reset_command = device.send_command("reset_config", {"section": "all"}) assert not has_command_failed(reset_command) @@ -270,6 +301,7 @@ def test_set_wifi(get_openiris_device, ensure_board_in_mode, config): params = { "name": "main", "ssid": config.WIFI_SSID, + "bssid": "", "password": config.WIFI_PASS, "channel": 0, "power": 0, @@ -287,12 +319,76 @@ def test_set_wifi(get_openiris_device, ensure_board_in_mode, config): assert wifi_status_command["results"][0]["result"]["data"]["status"] == "connected" +@pytest.mark.has_capability("wireless") +def test_set_wifi_correct_bssid(get_openiris_device, ensure_board_in_mode, config): + device = get_openiris_device() + reset_command = device.send_command("reset_config", {"section": "all"}) + assert not has_command_failed(reset_command) + + with DetectPortChange(): + device.send_command("restart_device") + time.sleep(config.SWITCH_MODE_REBOOT_TIME) + + device = ensure_board_in_mode("wifi", device) + params = { + "name": "main", + "ssid": config.WIFI_SSID, + "bssid": config.WIFI_BSSID, + "password": config.WIFI_PASS, + "channel": 0, + "power": 0, + } + set_wifi_result = device.send_command("set_wifi", params) + assert not has_command_failed(set_wifi_result) + + connect_wifi_result = device.send_command("connect_wifi") + assert not -has_command_failed(connect_wifi_result) + time.sleep(config.WIFI_CONNECTION_TIMEOUT) + + wifi_status_command = device.send_command("get_wifi_status") + assert not has_command_failed(wifi_status_command) + assert wifi_status_command["results"][0]["result"]["data"]["status"] == "connected" + + +@pytest.mark.has_capability("wireless") +def test_set_wifi_nonexitant_bssid(get_openiris_device, ensure_board_in_mode, config): + device = get_openiris_device() + reset_command = device.send_command("reset_config", {"section": "all"}) + assert not has_command_failed(reset_command) + + with DetectPortChange(): + device.send_command("restart_device") + time.sleep(config.SWITCH_MODE_REBOOT_TIME) + + device = ensure_board_in_mode("wifi", device) + params = { + "name": "main", + "ssid": config.WIFI_SSID, + "bssid": "99:99:99:99:99:99", # a completely wrong BSSID, just to test that we fail to connect + "password": config.WIFI_PASS, + "channel": 0, + "power": 0, + } + + set_wifi_result = device.send_command("set_wifi", params) + assert not has_command_failed(set_wifi_result) + + connect_wifi_result = device.send_command("connect_wifi") + assert not -has_command_failed(connect_wifi_result) + time.sleep(config.WIFI_CONNECTION_TIMEOUT) + + wifi_status_command = device.send_command("get_wifi_status") + assert not has_command_failed(wifi_status_command) + assert wifi_status_command["results"][0]["result"]["data"]["status"] == "error" + + @pytest.mark.has_capability("wireless") def test_set_wifi_invalid_network(get_openiris_device, ensure_board_in_mode, config): device = ensure_board_in_mode("wifi", get_openiris_device()) params = { "name": "main", "ssid": "PleaseDontBeARealNetwork", + "bssid": "", "password": "AndThePasswordIsFake", "channel": 0, "power": 0, @@ -351,6 +447,7 @@ def test_update_main_wifi_network(ensure_board_in_mode, get_openiris_device, con params1 = { "name": "main", "ssid": "Nada", + "bssid": "", "password": "Nuuh", "channel": 0, "power": 0, @@ -377,6 +474,7 @@ def test_set_wifi_add_another_network(ensure_board_in_mode, get_openiris_device) params = { "name": "anotherNetwork", "ssid": "PleaseDontBeARealNetwork", + "bssid": "", "password": "AndThePassowrdIsFake", "channel": 0, "power": 0, @@ -475,6 +573,7 @@ def test_update_wifi_command(ensure_board_in_mode, get_openiris_device, payload) params = { "name": "anotherNetwork", "ssid": "PleaseDontBeARealNetwork", + "bssid": "", "password": "AndThePasswordIsFake", "channel": 0, "power": 0, diff --git a/tools/setup_openiris.py b/tools/setup_openiris.py index da22d54..c619046 100644 --- a/tools/setup_openiris.py +++ b/tools/setup_openiris.py @@ -82,6 +82,7 @@ def __init__(self, title, context=None, parent_menu=None): @dataclass class WiFiNetwork: ssid: str + bssid: str channel: int rssi: int mac_address: str @@ -119,7 +120,7 @@ def scan_networks(self, timeout: int = 30): "scan_networks", params={"timeout_ms": timeout_ms}, timeout=timeout ) if has_command_failed(response): - print(f"❌ Scan failed: {response['error']}") + print(f"❌ Scan failed: {get_response_error(response)}") return channels_found = set() @@ -130,6 +131,7 @@ def scan_networks(self, timeout: int = 30): for net in networks: network = WiFiNetwork( ssid=net["ssid"], + bssid=net["mac_address"], channel=net["channel"], rssi=net["rssi"], mac_address=net["mac_address"], @@ -144,7 +146,7 @@ def scan_networks(self, timeout: int = 30): f"✅ Found {len(self.networks)} networks on channels: {sorted(channels_found)}" ) - def get_networks(self) -> list: + def get_networks(self) -> list[WiFiNetwork]: return self.networks @@ -152,6 +154,10 @@ def has_command_failed(result) -> bool: return "error" in result or result["results"][0]["result"]["status"] != "success" +def get_response_error(result) -> str: + return result["results"][0]["result"]["data"] + + def get_device_mode(device: OpenIrisDevice) -> dict: command_result = device.send_command("get_device_mode") if has_command_failed(command_result): @@ -181,7 +187,7 @@ def get_led_duty_cycle(device: OpenIrisDevice) -> dict: def get_mdns_name(device: OpenIrisDevice) -> dict: response = device.send_command("get_mdns_name") if "error" in response: - print(f"❌ Failed to get device name: {response['error']}") + print(f"❌ Failed to get device name: {get_response_error(response)}") return {"name": "unknown"} return {"name": response["results"][0]["result"]["data"]["hostname"]} @@ -190,7 +196,7 @@ def get_mdns_name(device: OpenIrisDevice) -> dict: def get_serial_info(device: OpenIrisDevice) -> dict: response = device.send_command("get_serial") if has_command_failed(response): - print(f"❌ Failed to get serial/MAC: {response['error']}") + print(f"❌ Failed to get serial/MAC: {get_response_error(response)}") return {"serial": None, "mac": None} return { @@ -203,7 +209,7 @@ def get_device_info(device: OpenIrisDevice) -> dict: response = device.send_command("get_who_am_i") if has_command_failed(response): - print(f"❌ Failed to get device info: {response['error']}") + print(f"❌ Failed to get device info: {get_response_error(response)}") return {"who_am_i": None, "version": None} return { @@ -215,7 +221,7 @@ def get_device_info(device: OpenIrisDevice) -> dict: def get_wifi_status(device: OpenIrisDevice) -> dict: response = device.send_command("get_wifi_status") if has_command_failed(response): - print(f"❌ Failed to get wifi status: {response['error']}") + print(f"❌ Failed to get wifi status: {get_response_error(response)}") return {"wifi_status": {"status": "unknown"}} return {"wifi_status": response["results"][0]["result"]["data"]} @@ -267,7 +273,7 @@ def configure_device_name(device: OpenIrisDevice, *args, **kwargs): response = device.send_command("set_mdns", {"hostname": name_choice}) if "error" in response: - print(f"❌ MDNS name setup failed: {response['error']}") + print(f"❌ MDNS name setup failed: {get_response_error(response)}") return print("✅ MDNS name set successfully") @@ -278,7 +284,7 @@ def start_streaming(device: OpenIrisDevice, *args, **kwargs): response = device.send_command("start_streaming") if "error" in response: - print(f"❌ Failed to start streaming: {response['error']}") + print(f"❌ Failed to start streaming: {get_response_error(response)}") return print("✅ Streaming mode started") @@ -336,7 +342,7 @@ def set_led_duty_cycle(device: OpenIrisDevice, *args, **kwargs): "set_led_duty_cycle", {"dutyCycle": duty_cycle} ) if has_command_failed(response): - print(f"❌ Failed to set LED duty cycle: {response['error']}") + print(f"❌ Failed to set LED duty cycle: {get_response_error(response)}") return False print("✅ LED duty cycle set successfully") @@ -400,7 +406,7 @@ def restart_device_command(device: OpenIrisDevice, *args, **kwargs): print("🔄 Restarting device...") response = device.send_command("restart_device") if has_command_failed(response): - print(f"❌ Failed to restart device: {response['error']}") + print(f"❌ Failed to restart device: {get_response_error(response)}") return print("✅ Device restart command sent successfully") @@ -439,9 +445,9 @@ def display_networks(wifi_scanner: WiFiScanner, *args, **kwargs): return print("\n📡 Available WiFi Networks:") - print("-" * 85) - print(f"{'#':<3} {'SSID':<32} {'Channel':<8} {'Signal':<20} {'Security':<15}") - print("-" * 85) + print("-" * 110) + print(f"{'#':<3} {'SSID':<32} {'Channel':<8} {'Signal':<20} {'BSSID':<22} {'Security':<15}") + print("-" * 110) channels = {} for idx, network in enumerate(networks, 1): @@ -452,10 +458,10 @@ def display_networks(wifi_scanner: WiFiScanner, *args, **kwargs): ssid_display = network.ssid if network.ssid else "" print( - f"{idx:<3} {ssid_display:<32} {network.channel:<8} {signal_str:<20} {network.security_type:<15}" + f"{idx:<3} {ssid_display:<32} {network.channel:<8} {signal_str:<20} {network.bssid:<22} {network.security_type:<15}" ) - print("-" * 85) + print("-" * 110) print("\n📊 Channel distribution: ", end="") for channel in sorted(channels.keys()): @@ -483,7 +489,7 @@ def configure_wifi(device: OpenIrisDevice, wifi_scanner: WiFiScanner, *args, **k sorted_networks = wifi_scanner.get_networks() if 0 <= net_idx < len(sorted_networks): - selected_network = sorted_networks[net_idx] + selected_network = sorted_networks[net_idx] ssid = selected_network.ssid print(f"\n🔐 Selected: {ssid}") @@ -502,6 +508,7 @@ def configure_wifi(device: OpenIrisDevice, wifi_scanner: WiFiScanner, *args, **k params = { "name": "main", "ssid": ssid, + "bssid": selected_network.bssid, "password": password, "channel": 0, "power": 0, @@ -509,7 +516,7 @@ def configure_wifi(device: OpenIrisDevice, wifi_scanner: WiFiScanner, *args, **k response = device.send_command("set_wifi", params) if has_command_failed(response): - print(f"❌ WiFi setup failed: {response['error']}") + print(f"❌ WiFi setup failed: {get_response_error(response)}") break print("✅ WiFi configured successfully!") @@ -557,7 +564,7 @@ def attempt_wifi_connection(device: OpenIrisDevice, *args, **kwargs): print("🔗 Attempting WiFi connection...") response = device.send_command("connect_wifi") if has_command_failed(response): - print(f"❌ WiFi connection failed: {response['error']}") + print(f"❌ WiFi connection failed: {get_response_error(response)}") return print("✅ WiFi connection attempt started")