From 8b6463d0603ddef53425cb6bebd311d0714a002c Mon Sep 17 00:00:00 2001 From: Ken Van Hoeylandt Date: Tue, 12 Nov 2024 21:36:23 +0100 Subject: [PATCH] Wi-Fi improvements (#77) Refactoring, new features and stability improvements. --- tactility-core/src/hash.c | 13 + tactility-core/src/hash.h | 11 +- tactility-core/src/secure.c | 16 +- tactility-core/src/secure.h | 10 +- tactility-headless/src/esp_init.c | 4 +- tactility-headless/src/services/wifi/wifi.h | 5 +- .../src/services/wifi/wifi_credentials.h | 25 -- .../src/services/wifi/wifi_credentials_esp.c | 235 ------------------ .../src/services/wifi/wifi_credentials_mock.c | 30 --- .../src/services/wifi/wifi_esp.c | 182 +++++++++++--- .../src/services/wifi/wifi_globals.h | 17 ++ .../src/services/wifi/wifi_mock.c | 3 +- .../src/services/wifi/wifi_settings.h | 34 +++ .../src/services/wifi/wifi_settings_esp.c | 147 +++++++++++ .../src/services/wifi/wifi_settings_mock.c | 28 +++ .../apps/wifi_connect/wifi_connect_bindings.h | 2 +- .../src/apps/wifi_connect/wifi_connect_view.c | 28 ++- tactility/src/apps/wifi_manage/wifi_manage.c | 24 +- 18 files changed, 450 insertions(+), 364 deletions(-) delete mode 100644 tactility-headless/src/services/wifi/wifi_credentials.h delete mode 100644 tactility-headless/src/services/wifi/wifi_credentials_esp.c delete mode 100644 tactility-headless/src/services/wifi/wifi_credentials_mock.c create mode 100644 tactility-headless/src/services/wifi/wifi_globals.h create mode 100644 tactility-headless/src/services/wifi/wifi_settings.h create mode 100644 tactility-headless/src/services/wifi/wifi_settings_esp.c create mode 100644 tactility-headless/src/services/wifi/wifi_settings_mock.c diff --git a/tactility-core/src/hash.c b/tactility-core/src/hash.c index 36cedf0f..2b66ae99 100644 --- a/tactility-core/src/hash.c +++ b/tactility-core/src/hash.c @@ -9,3 +9,16 @@ uint32_t tt_hash_string_djb2(const char* str) { } return hash; } + +uint32_t tt_hash_bytes_djb2(const void* data, size_t length) { + uint32_t hash = 5381; + uint8_t* data_bytes = (uint8_t*)data; + uint8_t c = *data_bytes++; + size_t index = 0; + while (index < length) { + hash = ((hash << 5) + hash) + (uint32_t)c; // hash * 33 + c + c = *data_bytes++; + index++; + } + return hash; +} diff --git a/tactility-core/src/hash.h b/tactility-core/src/hash.h index b90d60d8..b3edf7e8 100644 --- a/tactility-core/src/hash.h +++ b/tactility-core/src/hash.h @@ -1,5 +1,6 @@ #pragma once +#include #include #ifdef __cplusplus @@ -7,13 +8,19 @@ extern "C" { #endif /** - * This is quicker than the m-string.h hashing, as the latter - * operates on raw memory blocks and thus a strlen() call is required first. + * Implementation of DJB2 hashing algorithm. * @param[in] str the string to calculate the hash for * @return the hash */ uint32_t tt_hash_string_djb2(const char* str); +/** + * Implementation of DJB2 hashing algorithm. + * @param[in] data the bytes to calculate the hash for + * @return the hash + */ +uint32_t tt_hash_bytes_djb2(const void* data, size_t length); + #ifdef __cplusplus } #endif \ No newline at end of file diff --git a/tactility-core/src/secure.c b/tactility-core/src/secure.c index 1938ba0c..2a094be1 100644 --- a/tactility-core/src/secure.c +++ b/tactility-core/src/secure.c @@ -120,17 +120,19 @@ static void get_key(uint8_t key[32]) { #endif } -void tt_secure_get_iv_from_string(const char* input, uint8_t iv[16]) { +void tt_secure_get_iv_from_data(const void* data, size_t data_length, uint8_t iv[16]) { memset((void*)iv, 0, 16); - char c = *input++; - int index = 0; - while (c) { - iv[index] = c; - index++; - c = *input++; + uint8_t* data_bytes = (uint8_t*)data; + for (int i = 0; i < data_length; ++i) { + size_t safe_index = i % 16; + iv[safe_index] %= data_bytes[i]; } } +void tt_secure_get_iv_from_string(const char* input, uint8_t iv[16]) { + tt_secure_get_iv_from_data((const void*)input, strlen(input), iv); +} + static int tt_aes256_crypt_cbc( const uint8_t key[32], int mode, diff --git a/tactility-core/src/secure.h b/tactility-core/src/secure.h index f56c79ba..a260239e 100644 --- a/tactility-core/src/secure.h +++ b/tactility-core/src/secure.h @@ -26,7 +26,15 @@ extern "C" { #endif /** - * @brief Fills the IV with zeros and then copies up to 16 characters of the string into the IV. + * @brief Fills the IV with zeros and then creates an IV based on the input data. + * @param data input data + * @param data_length input data length + * @param iv output IV + */ +void tt_secure_get_iv_from_data(const void* data, size_t data_length, uint8_t iv[16]); + +/** +* @brief Fills the IV with zeros and then creates an IV based on the input data. * @param input input text * @param iv output IV */ diff --git a/tactility-headless/src/esp_init.c b/tactility-headless/src/esp_init.c index 5a186aa6..0a15f0a3 100644 --- a/tactility-headless/src/esp_init.c +++ b/tactility-headless/src/esp_init.c @@ -3,7 +3,7 @@ #ifdef ESP_TARGET #include "esp_partitions.h" -#include "services/wifi/wifi_credentials.h" +#include "services/wifi/wifi_settings.h" #include "esp_event.h" #include "esp_netif.h" @@ -34,7 +34,7 @@ void tt_esp_init() { tt_esp_network_init(); - tt_wifi_credentials_init(); + tt_wifi_settings_init(); } #endif \ No newline at end of file diff --git a/tactility-headless/src/services/wifi/wifi.h b/tactility-headless/src/services/wifi/wifi.h index 4fef308b..829676de 100644 --- a/tactility-headless/src/services/wifi/wifi.h +++ b/tactility-headless/src/services/wifi/wifi.h @@ -7,6 +7,7 @@ extern "C" { #include "pubsub.h" #include #include +#include "wifi_globals.h" #ifdef ESP_PLATFORM #include "esp_wifi.h" @@ -65,7 +66,7 @@ typedef struct { } WifiEvent; typedef struct { - uint8_t ssid[33]; + uint8_t ssid[TT_WIFI_SSID_LIMIT + 1]; int8_t rssi; wifi_auth_mode_t auth_mode; } WifiApRecord; @@ -112,7 +113,7 @@ void wifi_set_enabled(bool enabled); * @param ssid * @param password */ -void wifi_connect(const char* ssid, const char _Nullable password[64]); +void wifi_connect(const char* ssid, _Nullable const char* password); /** * @brief Disconnect from the access point. Doesn't have any effect when not connected. diff --git a/tactility-headless/src/services/wifi/wifi_credentials.h b/tactility-headless/src/services/wifi/wifi_credentials.h deleted file mode 100644 index 133b1db6..00000000 --- a/tactility-headless/src/services/wifi/wifi_credentials.h +++ /dev/null @@ -1,25 +0,0 @@ -#pragma once - -#include - -#ifdef __cplusplus -extern "C" { -#endif - -#define TT_WIFI_CREDENTIALS_PASSWORD_LIMIT 64 // Should be equal to wifi_sta_config_t.password -// TODO: Move to config file -#define TT_WIFI_CREDENTIALS_LIMIT 16 - -void tt_wifi_credentials_init(); - -bool tt_wifi_credentials_contains(const char* ssid); - -bool tt_wifi_credentials_get(const char* ssid, char password[TT_WIFI_CREDENTIALS_PASSWORD_LIMIT]); - -bool tt_wifi_credentials_set(const char* ssid, char password[TT_WIFI_CREDENTIALS_PASSWORD_LIMIT]); - -bool tt_wifi_credentials_remove(const char* ssid); - -#ifdef __cplusplus -} -#endif diff --git a/tactility-headless/src/services/wifi/wifi_credentials_esp.c b/tactility-headless/src/services/wifi/wifi_credentials_esp.c deleted file mode 100644 index 333c1ba7..00000000 --- a/tactility-headless/src/services/wifi/wifi_credentials_esp.c +++ /dev/null @@ -1,235 +0,0 @@ -#ifdef ESP_TARGET - -#include "wifi_credentials.h" - -#include "nvs_flash.h" -#include "log.h" -#include "hash.h" -#include "check.h" -#include "mutex.h" -#include "secure.h" - -#define TAG "wifi_credentials" -#define TT_NVS_NAMESPACE "tt_wifi_cred" // limited by NVS_KEY_NAME_MAX_SIZE -#define TT_NVS_PARTITION "nvs" - -static void hash_reset_all(); - -// region Hash - -static Mutex* hash_mutex = NULL; -static int8_t hash_index = -1; -static uint32_t hashes[TT_WIFI_CREDENTIALS_LIMIT] = { 0 }; - -static void hash_init() { - tt_assert(hash_mutex == NULL); - hash_mutex = tt_mutex_alloc(MutexTypeNormal); - hash_reset_all(); -} - -static void tt_hash_mutex_lock() { - tt_assert(tt_mutex_acquire(hash_mutex, 100) == TtStatusOk); -} - -static void tt_hash_mutex_unlock() { - tt_assert(tt_mutex_release(hash_mutex) == TtStatusOk); -} - -static int hash_find_value(uint32_t hash) { - tt_hash_mutex_lock(); - for (int i = 0; i < hash_index; ++i) { - if (hashes[i] == hash) { - tt_hash_mutex_unlock(); - return i; - } - } - tt_hash_mutex_unlock(); - return -1; -} - -static bool hash_contains_value(uint32_t value) { - return hash_find_value(value) != -1; -} - -static void hash_add(const char* ssid) { - uint32_t hash = tt_hash_string_djb2(ssid); - if (!hash_contains_value(hash)) { - tt_hash_mutex_lock(); - tt_check((hash_index + 1) < TT_WIFI_CREDENTIALS_LIMIT, "exceeding wifi credentials list size"); - hash_index++; - hashes[hash_index] = hash; - tt_hash_mutex_unlock(); - } -} - -static void hash_reset_all() { - hash_index = -1; -} - -// endregion Hash - -// region Wi-Fi Credentials - static - -static esp_err_t tt_wifi_credentials_nvs_open(nvs_handle_t* handle, nvs_open_mode_t mode) { - return nvs_open(TT_NVS_NAMESPACE, NVS_READWRITE, handle); -} - -static void tt_wifi_credentials_nvs_close(nvs_handle_t handle) { - nvs_close(handle); -} - -static bool tt_wifi_credentials_contains_in_flash(const char* ssid) { - nvs_handle_t handle; - esp_err_t result = tt_wifi_credentials_nvs_open(&handle, NVS_READONLY); - if (result != ESP_OK) { - TT_LOG_E(TAG, "Failed to open NVS handle: %s", esp_err_to_name(result)); - return false; - } - - hash_reset_all(); - - nvs_iterator_t iterator; - result = nvs_entry_find(TT_NVS_PARTITION, TT_NVS_NAMESPACE, NVS_TYPE_BLOB, &iterator); - bool contains_ssid = false; - while (result == ESP_OK) { - nvs_entry_info_t info; - nvs_entry_info(iterator, &info); // Can omit error check if parameters are guaranteed to be non-NULL - if (strcmp(info.key, ssid) == 0) { - contains_ssid = true; - break; - } - result = nvs_entry_next(&iterator); - } - - nvs_release_iterator(iterator); - tt_wifi_credentials_nvs_close(handle); - - return contains_ssid; -} - -// endregion Wi-Fi Credentials - static - -// region Wi-Fi Credentials - public - -bool tt_wifi_credentials_contains(const char* ssid) { - uint32_t hash = tt_hash_string_djb2(ssid); - if (hash_contains_value(hash)) { - return tt_wifi_credentials_contains_in_flash(ssid); - } else { - return false; - } -} - -void tt_wifi_credentials_init() { - TT_LOG_I(TAG, "init started"); - hash_init(); - - nvs_handle_t handle; - esp_err_t result = tt_wifi_credentials_nvs_open(&handle, NVS_READWRITE); - if (result != ESP_OK) { - TT_LOG_E(TAG, "Failed to open NVS handle for init: %s", esp_err_to_name(result)); - return; - } - - nvs_iterator_t iterator; - result = nvs_entry_find(TT_NVS_PARTITION, TT_NVS_NAMESPACE, NVS_TYPE_BLOB, &iterator); - while (result == ESP_OK) { - nvs_entry_info_t info; - nvs_entry_info(iterator, &info); // Can omit error check if parameters are guaranteed to be non-NULL - hash_add(info.key); - result = nvs_entry_next(&iterator); - } - nvs_release_iterator(iterator); - - tt_wifi_credentials_nvs_close(handle); - TT_LOG_I(TAG, "init finished"); -} - -bool tt_wifi_credentials_get(const char* ssid, char password[TT_WIFI_CREDENTIALS_PASSWORD_LIMIT]) { - nvs_handle_t handle; - esp_err_t result = tt_wifi_credentials_nvs_open(&handle, NVS_READONLY); - if (result != ESP_OK) { - TT_LOG_E(TAG, "Failed to open NVS handle: %s", esp_err_to_name(result)); - return false; - } - - char password_encrypted[TT_WIFI_CREDENTIALS_PASSWORD_LIMIT]; - size_t length = TT_WIFI_CREDENTIALS_PASSWORD_LIMIT; - result = nvs_get_blob(handle, ssid, password_encrypted, &length); - - uint8_t iv[16]; - tt_secure_get_iv_from_string(ssid, iv); - int decrypt_result = tt_secure_decrypt( - iv, - (uint8_t*)password_encrypted, - (uint8_t*)password, - TT_WIFI_CREDENTIALS_PASSWORD_LIMIT - ); - - if (decrypt_result != 0) { - result = ESP_FAIL; - TT_LOG_E(TAG, "Failed to decrypt credentials for \"%s\": %d", ssid, decrypt_result); - } - - if (result != ESP_OK && result != ESP_ERR_NVS_NOT_FOUND) { - TT_LOG_E(TAG, "Failed to get credentials for \"%s\": %s", ssid, esp_err_to_name(result)); - } - - tt_wifi_credentials_nvs_close(handle); - return result == ESP_OK; -} - -bool tt_wifi_credentials_set(const char* ssid, char password[TT_WIFI_CREDENTIALS_PASSWORD_LIMIT]) { - nvs_handle_t handle; - esp_err_t result = tt_wifi_credentials_nvs_open(&handle, NVS_READWRITE); - if (result != ESP_OK) { - TT_LOG_E(TAG, "Failed to open NVS handle: %s", esp_err_to_name(result)); - return false; - } - - char password_encrypted[TT_WIFI_CREDENTIALS_PASSWORD_LIMIT]; - uint8_t iv[16]; - tt_secure_get_iv_from_string(ssid, iv); - int encrypt_result = tt_secure_encrypt( - iv, - (uint8_t*)password, - (uint8_t*)password_encrypted, - TT_WIFI_CREDENTIALS_PASSWORD_LIMIT - ); - - if (encrypt_result != 0) { - result = ESP_FAIL; - TT_LOG_E(TAG, "Failed to encrypt credentials for \"%s\": %d", ssid, encrypt_result); - } - - if (result == ESP_OK) { - result = nvs_set_blob(handle, ssid, password_encrypted, TT_WIFI_CREDENTIALS_PASSWORD_LIMIT); - if (result != ESP_OK) { - TT_LOG_E(TAG, "Failed to get credentials for \"%s\": %s", ssid, esp_err_to_name(result)); - } - } - - tt_wifi_credentials_nvs_close(handle); - return result == ESP_OK; -} - -bool tt_wifi_credentials_remove(const char* ssid) { - nvs_handle_t handle; - esp_err_t result = tt_wifi_credentials_nvs_open(&handle, NVS_READWRITE); - if (result != ESP_OK) { - TT_LOG_E(TAG, "Failed to open NVS handle to store \"%s\": %s", ssid, esp_err_to_name(result)); - return false; - } - - result = nvs_erase_key(handle, ssid); - if (result != ESP_OK) { - TT_LOG_E(TAG, "Failed to erase credentials for \"%s\": %s", ssid, esp_err_to_name(result)); - } - - tt_wifi_credentials_nvs_close(handle); - return result == ESP_OK; -} - -// end region Wi-Fi Credentials - public - -#endif // ESP_TARGET \ No newline at end of file diff --git a/tactility-headless/src/services/wifi/wifi_credentials_mock.c b/tactility-headless/src/services/wifi/wifi_credentials_mock.c deleted file mode 100644 index 7d1b065e..00000000 --- a/tactility-headless/src/services/wifi/wifi_credentials_mock.c +++ /dev/null @@ -1,30 +0,0 @@ -#ifndef ESP_TARGET - -#include "wifi_credentials.h" -#include "log.h" - -#define TAG "wifi_credentials_mock" - -static void hash_reset_all(); - -bool tt_wifi_credentials_contains(const char* ssid) { - return false; -} - -void tt_wifi_credentials_init() { - TT_LOG_I(TAG, "init"); -} - -bool tt_wifi_credentials_get(const char* ssid, char password[TT_WIFI_CREDENTIALS_PASSWORD_LIMIT]) { - return false; -} - -bool tt_wifi_credentials_set(const char* ssid, char password[TT_WIFI_CREDENTIALS_PASSWORD_LIMIT]) { - return false; -} - -bool tt_wifi_credentials_remove(const char* ssid) { - return false; -} - -#endif // ESP_TARGET \ No newline at end of file diff --git a/tactility-headless/src/services/wifi/wifi_esp.c b/tactility-headless/src/services/wifi/wifi_esp.c index a975a1db..ff0e0898 100644 --- a/tactility-headless/src/services/wifi/wifi_esp.c +++ b/tactility-headless/src/services/wifi/wifi_esp.c @@ -11,10 +11,10 @@ #include "mutex.h" #include "pubsub.h" #include "service.h" +#include "wifi_settings.h" #include #define TAG "wifi" -#define WIFI_SCAN_RECORD_LIMIT 16 // default, can be overridden #define WIFI_CONNECTED_BIT BIT0 #define WIFI_FAIL_BIT BIT1 @@ -50,8 +50,8 @@ typedef enum { } WifiMessageType; typedef struct { - uint8_t ssid[32]; - uint8_t password[64]; + uint8_t ssid[TT_WIFI_SSID_LIMIT]; + uint8_t password[TT_WIFI_CREDENTIALS_PASSWORD_LIMIT]; } WifiConnectMessage; typedef struct { @@ -83,7 +83,7 @@ static Wifi* wifi_alloc() { instance->scan_active = false; instance->scan_list = NULL; instance->scan_list_count = 0; - instance->scan_list_limit = WIFI_SCAN_RECORD_LIMIT; + instance->scan_list_limit = TT_WIFI_SCAN_RECORD_LIMIT; instance->event_handler_any_id = NULL; instance->event_handler_got_ip = NULL; instance->event_group = xEventGroupCreate(); @@ -109,52 +109,67 @@ PubSub* wifi_get_pubsub() { } WifiRadioState wifi_get_radio_state() { - return wifi_singleton->radio_state; + tt_assert(wifi_singleton); + wifi_lock(wifi_singleton); + WifiRadioState state = wifi_singleton->radio_state; + wifi_unlock(wifi_singleton); + return state; } void wifi_scan() { tt_assert(wifi_singleton); + wifi_lock(wifi_singleton); WifiMessage message = {.type = WifiMessageTypeScan}; // No need to lock for queue tt_message_queue_put(wifi_singleton->queue, &message, 100 / portTICK_PERIOD_MS); + wifi_unlock(wifi_singleton); } bool wifi_is_scanning() { tt_assert(wifi_singleton); - return wifi_singleton->scan_active; + wifi_lock(wifi_singleton); + bool is_scanning = wifi_singleton->scan_active; + wifi_unlock(wifi_singleton); + return is_scanning; } -void wifi_connect(const char* ssid, const char _Nullable password[64]) { +void wifi_connect(const char* ssid, _Nullable const char* password) { tt_assert(wifi_singleton); - tt_check(strlen(ssid) <= 32); WifiMessage message = {.type = WifiMessageTypeConnect}; - memcpy(message.connect_message.ssid, ssid, 32); + wifi_lock(wifi_singleton); + memcpy(message.connect_message.ssid, ssid, TT_WIFI_SSID_LIMIT); if (password != NULL) { - memcpy(message.connect_message.password, password, 64); + memcpy(message.connect_message.password, password, TT_WIFI_CREDENTIALS_PASSWORD_LIMIT); } else { message.connect_message.password[0] = 0; } tt_message_queue_put(wifi_singleton->queue, &message, 100 / portTICK_PERIOD_MS); + wifi_unlock(wifi_singleton); } void wifi_disconnect() { tt_assert(wifi_singleton); WifiMessage message = {.type = WifiMessageTypeDisconnect}; + wifi_lock(wifi_singleton); tt_message_queue_put(wifi_singleton->queue, &message, 100 / portTICK_PERIOD_MS); + wifi_unlock(wifi_singleton); } void wifi_set_scan_records(uint16_t records) { tt_assert(wifi_singleton); + wifi_lock(wifi_singleton); if (records != wifi_singleton->scan_list_limit) { wifi_scan_list_free_safely(wifi_singleton); wifi_singleton->scan_list_limit = records; } + wifi_unlock(wifi_singleton); } void wifi_get_scan_results(WifiApRecord records[], uint16_t limit, uint16_t* result_count) { tt_check(wifi_singleton); tt_check(result_count); + wifi_lock(wifi_singleton); if (wifi_singleton->scan_list_count == 0) { *result_count = 0; } else { @@ -170,9 +185,12 @@ void wifi_get_scan_results(WifiApRecord records[], uint16_t limit, uint16_t* res // so it effectively became the list count: *result_count = i; } + wifi_unlock(wifi_singleton); } void wifi_set_enabled(bool enabled) { + tt_check(wifi_singleton); + wifi_lock(wifi_singleton); if (enabled) { WifiMessage message = {.type = WifiMessageTypeRadioOn}; // No need to lock for queue @@ -182,14 +200,19 @@ void wifi_set_enabled(bool enabled) { // No need to lock for queue tt_message_queue_put(wifi_singleton->queue, &message, 100 / portTICK_PERIOD_MS); } + wifi_unlock(wifi_singleton); } bool wifi_is_connection_secure() { tt_check(wifi_singleton); - return wifi_singleton->secure_connection; + wifi_lock(wifi_singleton); + bool is_secure = wifi_singleton->secure_connection; + wifi_unlock(wifi_singleton); + return is_secure; } int wifi_get_rssi() { + tt_check(wifi_singleton); static int rssi = 0; if (esp_wifi_sta_get_rssi(&rssi) == ESP_OK) { return rssi; @@ -201,16 +224,15 @@ int wifi_get_rssi() { // endregion Public functions static void wifi_lock(Wifi* wifi) { - tt_crash("this fails for now"); // TODO: Fix tt_assert(wifi); tt_assert(wifi->mutex); - tt_check(xSemaphoreTakeRecursive(wifi->mutex, portMAX_DELAY) == pdPASS); + tt_mutex_acquire(wifi->mutex, tt_ms_to_ticks(100)); } static void wifi_unlock(Wifi* wifi) { tt_assert(wifi); tt_assert(wifi->mutex); - tt_check(xSemaphoreGiveRecursive(wifi->mutex) == pdPASS); + tt_mutex_release(wifi->mutex); } static void wifi_scan_list_alloc(Wifi* wifi) { @@ -243,7 +265,42 @@ static void wifi_publish_event_simple(Wifi* wifi, WifiEventType type) { tt_pubsub_publish(wifi->pubsub, &turning_on_event); } +static void wifi_copy_scan_list(Wifi* wifi) { + // Create scan list if it does not exist + wifi_scan_list_alloc_safely(wifi); + wifi->scan_list_count = 0; + uint16_t record_count = wifi->scan_list_limit; + + ESP_ERROR_CHECK(esp_wifi_scan_get_ap_records(&record_count, wifi->scan_list)); + uint16_t safe_record_count = TT_MIN(wifi->scan_list_limit, record_count); + wifi->scan_list_count = safe_record_count; + TT_LOG_I(TAG, "Scanned %u APs. Showing %u:", record_count, safe_record_count); + for (uint16_t i = 0; i < safe_record_count; i++) { + wifi_ap_record_t* record = &wifi->scan_list[i]; + TT_LOG_I(TAG, " - SSID %s (RSSI %d, channel %d)", record->ssid, record->rssi, record->primary); + } +} + +static void wifi_auto_connect(Wifi* wifi) { + for (int i = 0; i < wifi->scan_list_count; ++i) { + const char* ssid = (const char*)wifi->scan_list[i].ssid; + if (tt_wifi_settings_contains(ssid)) { + static_assert(sizeof(wifi->scan_list[i].ssid) == (TT_WIFI_SSID_LIMIT + 1), "SSID size mismatch"); + WifiApSettings ap_settings; + if (tt_wifi_settings_load(ssid, &ap_settings)) { + if (ap_settings.auto_connect) { + wifi_connect(ssid, ap_settings.secret); + } + } else { + TT_LOG_E(TAG, "Failed to load credentials for ssid %s", ssid); + } + break; + } + } +} + static void event_handler(TT_UNUSED void* arg, esp_event_base_t event_base, int32_t event_id, void* event_data) { + wifi_lock(wifi_singleton); if (event_base == WIFI_EVENT && event_id == WIFI_EVENT_STA_START) { TT_LOG_I(TAG, "event_handler: sta start"); if (wifi_singleton->radio_state == WIFI_RADIO_CONNECTION_PENDING) { @@ -258,7 +315,37 @@ static void event_handler(TT_UNUSED void* arg, esp_event_base_t event_base, int3 ip_event_got_ip_t* event = (ip_event_got_ip_t*)event_data; TT_LOG_I(TAG, "event_handler: got ip:" IPSTR, IP2STR(&event->ip_info.ip)); xEventGroupSetBits(wifi_singleton->event_group, WIFI_CONNECTED_BIT); + } else if (event_base == WIFI_EVENT && event_id == WIFI_EVENT_SCAN_DONE) { + wifi_event_sta_scan_done_t* event = (wifi_event_sta_scan_done_t*)event_data; + TT_LOG_I(TAG, "event_handler: wifi scanning done (scan id %u)", event->scan_id); + bool copied_list; + if ( + wifi_singleton->radio_state == WIFI_RADIO_ON || + wifi_singleton->radio_state == WIFI_RADIO_CONNECTION_ACTIVE || + wifi_singleton->radio_state == WIFI_RADIO_CONNECTION_PENDING + ) { + wifi_copy_scan_list(wifi_singleton); + copied_list = true; + } else { + copied_list = false; + } + + if ( + wifi_singleton->radio_state != WIFI_RADIO_OFF && + wifi_singleton->radio_state != WIFI_RADIO_OFF_PENDING + ) { + esp_wifi_scan_stop(); + } + + wifi_publish_event_simple(wifi_singleton, WifiEventTypeScanFinished); + wifi_singleton->scan_active = false; + TT_LOG_I(TAG, "Finished scan"); + + if (copied_list) { + wifi_auto_connect(wifi_singleton); + } } + wifi_unlock(wifi_singleton); } static void wifi_enable(Wifi* wifi) { @@ -353,6 +440,7 @@ static void wifi_disable(Wifi* wifi) { } TT_LOG_I(TAG, "Disabling"); + xEventGroupClearBits(wifi_singleton->event_group, WIFI_FAIL_BIT | WIFI_CONNECTED_BIT); wifi->radio_state = WIFI_RADIO_OFF_PENDING; wifi_publish_event_simple(wifi, WifiEventTypeRadioStateOffPending); @@ -401,35 +489,22 @@ static void wifi_disable(Wifi* wifi) { static void wifi_scan_internal(Wifi* wifi) { WifiRadioState state = wifi->radio_state; - if (state != WIFI_RADIO_ON && state != WIFI_RADIO_CONNECTION_ACTIVE) { + if (state != WIFI_RADIO_ON && state != WIFI_RADIO_CONNECTION_ACTIVE && state != WIFI_RADIO_CONNECTION_PENDING) { TT_LOG_W(TAG, "Scan unavailable: wifi not enabled"); return; } - TT_LOG_I(TAG, "Starting scan"); - wifi->scan_active = true; - wifi_publish_event_simple(wifi, WifiEventTypeScanStarted); - - // Create scan list if it does not exist - wifi_scan_list_alloc_safely(wifi); - wifi->scan_list_count = 0; - - esp_wifi_scan_start(NULL, true); - uint16_t record_count = wifi->scan_list_limit; - ESP_ERROR_CHECK(esp_wifi_scan_get_ap_records(&record_count, wifi->scan_list)); - uint16_t safe_record_count = TT_MIN(wifi->scan_list_limit, record_count); - wifi->scan_list_count = safe_record_count; - TT_LOG_I(TAG, "Scanned %u APs. Showing %u:", record_count, safe_record_count); - for (uint16_t i = 0; i < safe_record_count; i++) { - wifi_ap_record_t* record = &wifi->scan_list[i]; - TT_LOG_I(TAG, " - SSID %s (RSSI %d, channel %d)", record->ssid, record->rssi, record->primary); + if (!wifi->scan_active) { + if (esp_wifi_scan_start(NULL, false) == ESP_OK) { + TT_LOG_I(TAG, "Starting scan"); + wifi->scan_active = true; + wifi_publish_event_simple(wifi, WifiEventTypeScanStarted); + } else { + TT_LOG_I(TAG, "Can't start scan"); + } + } else { + TT_LOG_W(TAG, "Scan already pending"); } - - esp_wifi_scan_stop(); - - wifi_publish_event_simple(wifi, WifiEventTypeScanFinished); - wifi->scan_active = false; - TT_LOG_I(TAG, "Finished scan"); } static void wifi_connect_internal(Wifi* wifi, WifiConnectMessage* connect_message) { @@ -452,8 +527,10 @@ static void wifi_connect_internal(Wifi* wifi, WifiConnectMessage* connect_messag .sae_h2e_identifier = {0}, }, }; - memcpy(wifi_config.sta.ssid, connect_message->ssid, 32); - memcpy(wifi_config.sta.password, connect_message->password, 64); + + static_assert(sizeof(wifi_config.sta.ssid) == sizeof(connect_message->ssid), "SSID size mismatch"); + memcpy(wifi_config.sta.ssid, connect_message->ssid, sizeof(wifi_config.sta.ssid)); + memcpy(wifi_config.sta.password, connect_message->password, sizeof(wifi_config.sta.password)); wifi->secure_connection = (wifi_config.sta.password[0] != 0x00); @@ -558,28 +635,51 @@ _Noreturn int32_t wifi_main(TT_UNUSED void* parameter) { Wifi* wifi = wifi_singleton; MessageQueue* queue = wifi->queue; + if (TT_WIFI_AUTO_ENABLE) { + wifi_enable(wifi); + wifi_scan_internal(wifi); + } + WifiMessage message; while (true) { - if (tt_message_queue_get(queue, &message, 1000 / portTICK_PERIOD_MS) == TtStatusOk) { + if (tt_message_queue_get(queue, &message, 5000 / portTICK_PERIOD_MS) == TtStatusOk) { TT_LOG_I(TAG, "Processing message of type %d", message.type); switch (message.type) { case WifiMessageTypeRadioOn: + wifi_lock(wifi); wifi_enable(wifi); + wifi_unlock(wifi); break; case WifiMessageTypeRadioOff: + wifi_lock(wifi); wifi_disable(wifi); + wifi_unlock(wifi); break; case WifiMessageTypeScan: + wifi_lock(wifi); wifi_scan_internal(wifi); + wifi_unlock(wifi); break; case WifiMessageTypeConnect: + wifi_lock(wifi); wifi_connect_internal(wifi, &message.connect_message); + wifi_unlock(wifi); break; case WifiMessageTypeDisconnect: + wifi_lock(wifi); wifi_disconnect_internal_but_keep_active(wifi); + wifi_unlock(wifi); break; } } + + // Automatic scanning is done so we can automatically connect to access points + wifi_lock(wifi); + bool should_start_scan = wifi->radio_state == WIFI_RADIO_ON && !wifi->scan_active; + wifi_unlock(wifi); + if (should_start_scan) { + wifi_scan_internal(wifi); + } } } diff --git a/tactility-headless/src/services/wifi/wifi_globals.h b/tactility-headless/src/services/wifi/wifi_globals.h new file mode 100644 index 00000000..7e7c37e9 --- /dev/null +++ b/tactility-headless/src/services/wifi/wifi_globals.h @@ -0,0 +1,17 @@ +#pragma once + +#ifdef __cplusplus +extern "C" { +#endif + +#define TT_WIFI_AUTO_CONNECT true // Default setting for new Wi-Fi entries +#define TT_WIFI_AUTO_ENABLE false + +#define TT_WIFI_SCAN_RECORD_LIMIT 16 // default, can be overridden + +#define TT_WIFI_SSID_LIMIT 32 // 32 characters/octets, according to IEEE 802.11-2020 spec +#define TT_WIFI_CREDENTIALS_PASSWORD_LIMIT 64 // 64 characters/octets, according to IEEE 802.11-2020 spec + +#ifdef __cplusplus +} +#endif diff --git a/tactility-headless/src/services/wifi/wifi_mock.c b/tactility-headless/src/services/wifi/wifi_mock.c index 34f73371..c5cbff4c 100644 --- a/tactility-headless/src/services/wifi/wifi_mock.c +++ b/tactility-headless/src/services/wifi/wifi_mock.c @@ -12,7 +12,6 @@ #include #define TAG "wifi" -#define WIFI_SCAN_RECORD_LIMIT 16 // default, can be overridden #define WIFI_CONNECTED_BIT BIT0 #define WIFI_FAIL_BIT BIT1 @@ -77,7 +76,7 @@ bool wifi_is_scanning() { return wifi_singleton->scan_active; } -void wifi_connect(const char* ssid, const char _Nullable password[64]) { +void wifi_connect(const char* ssid, _Nullable const char* password) { tt_assert(wifi_singleton); tt_check(strlen(ssid) <= 32); // TODO: implement diff --git a/tactility-headless/src/services/wifi/wifi_settings.h b/tactility-headless/src/services/wifi/wifi_settings.h new file mode 100644 index 00000000..6ace9d69 --- /dev/null +++ b/tactility-headless/src/services/wifi/wifi_settings.h @@ -0,0 +1,34 @@ +#pragma once + +#include +#include "wifi_globals.h" + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * This struct is stored as-is into NVS flash. + * + * The SSID and secret are increased by 1 byte to facilitate string null termination. + * This makes it easier to use the char array as a string in various places. + */ +typedef struct { + char ssid[TT_WIFI_SSID_LIMIT + 1]; + char secret[TT_WIFI_CREDENTIALS_PASSWORD_LIMIT + 1]; + bool auto_connect; +} WifiApSettings; + +void tt_wifi_settings_init(); + +bool tt_wifi_settings_contains(const char* ssid); + +bool tt_wifi_settings_load(const char* ssid, WifiApSettings* settings); + +bool tt_wifi_settings_save(const WifiApSettings* settings); + +bool tt_wifi_settings_remove(const char* ssid); + +#ifdef __cplusplus +} +#endif diff --git a/tactility-headless/src/services/wifi/wifi_settings_esp.c b/tactility-headless/src/services/wifi/wifi_settings_esp.c new file mode 100644 index 00000000..9fe2cd92 --- /dev/null +++ b/tactility-headless/src/services/wifi/wifi_settings_esp.c @@ -0,0 +1,147 @@ +#ifdef ESP_TARGET + +#include "wifi_globals.h" +#include "wifi_settings.h" + +#include "nvs_flash.h" +#include "log.h" +#include "hash.h" +#include "check.h" +#include "secure.h" + +#define TAG "wifi_settings" +#define TT_NVS_NAMESPACE "wifi_settings" // limited by NVS_KEY_NAME_MAX_SIZE + +// region Wi-Fi Credentials - static + +static esp_err_t tt_wifi_credentials_nvs_open(nvs_handle_t* handle, nvs_open_mode_t mode) { + return nvs_open(TT_NVS_NAMESPACE, NVS_READWRITE, handle); +} + +static void tt_wifi_credentials_nvs_close(nvs_handle_t handle) { + nvs_close(handle); +} + +// endregion Wi-Fi Credentials - static + +// region Wi-Fi Credentials - public + +bool tt_wifi_settings_contains(const char* ssid) { + nvs_handle_t handle; + esp_err_t result = tt_wifi_credentials_nvs_open(&handle, NVS_READONLY); + if (result != ESP_OK) { + TT_LOG_E(TAG, "Failed to open NVS handle: %s", esp_err_to_name(result)); + return false; + } + + bool key_exists = nvs_find_key(handle, ssid, NULL) == ESP_OK; + tt_wifi_credentials_nvs_close(handle); + + return key_exists; +} + +void tt_wifi_settings_init() { + TT_LOG_I(TAG, "init"); + static_assert(strlen(TT_NVS_NAMESPACE) <= NVS_KEY_NAME_MAX_SIZE, "Namespace name too long"); +} + +bool tt_wifi_settings_load(const char* ssid, WifiApSettings* settings) { + nvs_handle_t handle; + esp_err_t result = tt_wifi_credentials_nvs_open(&handle, NVS_READONLY); + if (result != ESP_OK) { + TT_LOG_E(TAG, "Failed to open NVS handle: %s", esp_err_to_name(result)); + return false; + } + + WifiApSettings encrypted_settings; + size_t length = sizeof(WifiApSettings); + result = nvs_get_blob(handle, ssid, &encrypted_settings, &length); + + uint8_t iv[16]; + tt_secure_get_iv_from_string(ssid, iv); + int decrypt_result = tt_secure_decrypt( + iv, + (uint8_t*)encrypted_settings.secret, + (uint8_t*)settings->secret, + TT_WIFI_CREDENTIALS_PASSWORD_LIMIT + ); + // Manually ensure null termination, because encryption must be a multiple of 16 bytes + encrypted_settings.secret[TT_WIFI_CREDENTIALS_PASSWORD_LIMIT] = 0; + + if (decrypt_result != 0) { + result = ESP_FAIL; + TT_LOG_E(TAG, "Failed to decrypt credentials for \"%s\": %d", ssid, decrypt_result); + } + + if (result != ESP_OK && result != ESP_ERR_NVS_NOT_FOUND) { + TT_LOG_E(TAG, "Failed to get credentials for \"%s\": %s", ssid, esp_err_to_name(result)); + } + + tt_wifi_credentials_nvs_close(handle); + + settings->auto_connect = encrypted_settings.auto_connect; + strcpy((char*)settings->ssid, encrypted_settings.ssid); + + return result == ESP_OK; +} + +bool tt_wifi_settings_save(const WifiApSettings* settings) { + nvs_handle_t handle; + esp_err_t result = tt_wifi_credentials_nvs_open(&handle, NVS_READWRITE); + if (result != ESP_OK) { + TT_LOG_E(TAG, "Failed to open NVS handle: %s", esp_err_to_name(result)); + return false; + } + + WifiApSettings encrypted_settings = { + .auto_connect = settings->auto_connect, + }; + strcpy((char*)encrypted_settings.ssid, settings->ssid); + // We only decrypt multiples of 16, so we have to ensure the last byte is set to 0 + encrypted_settings.secret[TT_WIFI_CREDENTIALS_PASSWORD_LIMIT] = 0; + + uint8_t iv[16]; + tt_secure_get_iv_from_data(settings->ssid, strlen(settings->ssid), iv); + int encrypt_result = tt_secure_encrypt( + iv, + (uint8_t*)settings->secret, + (uint8_t*)encrypted_settings.secret, + TT_WIFI_CREDENTIALS_PASSWORD_LIMIT + ); + + if (encrypt_result != 0) { + result = ESP_FAIL; + TT_LOG_E(TAG, "Failed to encrypt credentials \"%s\": %d", settings->ssid, encrypt_result); + } + + if (result == ESP_OK) { + result = nvs_set_blob(handle, settings->ssid, &encrypted_settings, sizeof(WifiApSettings)); + if (result != ESP_OK) { + TT_LOG_E(TAG, "Failed to get credentials for \"%s\": %s", settings->ssid, esp_err_to_name(result)); + } + } + + tt_wifi_credentials_nvs_close(handle); + return result == ESP_OK; +} + +bool tt_wifi_settings_remove(const char* ssid) { + nvs_handle_t handle; + esp_err_t result = tt_wifi_credentials_nvs_open(&handle, NVS_READWRITE); + if (result != ESP_OK) { + TT_LOG_E(TAG, "Failed to open NVS handle to store \"%s\": %s", ssid, esp_err_to_name(result)); + return false; + } + + result = nvs_erase_key(handle, ssid); + if (result != ESP_OK) { + TT_LOG_E(TAG, "Failed to erase credentials for \"%s\": %s", ssid, esp_err_to_name(result)); + } + + tt_wifi_credentials_nvs_close(handle); + return result == ESP_OK; +} + +// end region Wi-Fi Credentials - public + +#endif // ESP_TARGET \ No newline at end of file diff --git a/tactility-headless/src/services/wifi/wifi_settings_mock.c b/tactility-headless/src/services/wifi/wifi_settings_mock.c new file mode 100644 index 00000000..7fe65018 --- /dev/null +++ b/tactility-headless/src/services/wifi/wifi_settings_mock.c @@ -0,0 +1,28 @@ +#ifndef ESP_TARGET + +#include "wifi_settings.h" +#include "log.h" + +#define TAG "wifi_settings_mock" + +bool tt_wifi_settings_contains(const char* ssid) { + return false; +} + +void tt_wifi_settings_init() { + TT_LOG_I(TAG, "init"); +} + +bool tt_wifi_settings_load(const char* ssid, WifiApSettings* settings) { + return false; +} + +bool tt_wifi_settings_save(const WifiApSettings* settings) { + return false; +} + +bool tt_wifi_settings_remove(const char* ssid) { + return false; +} + +#endif // ESP_TARGET \ No newline at end of file diff --git a/tactility/src/apps/wifi_connect/wifi_connect_bindings.h b/tactility/src/apps/wifi_connect/wifi_connect_bindings.h index e3020075..2250e678 100644 --- a/tactility/src/apps/wifi_connect/wifi_connect_bindings.h +++ b/tactility/src/apps/wifi_connect/wifi_connect_bindings.h @@ -2,7 +2,7 @@ #include -typedef void (*OnConnectSsid)(const char* ssid, const char password[64], void* context); +typedef void (*OnConnectSsid)(const char ssid[TT_WIFI_SSID_LIMIT], const char password[TT_WIFI_CREDENTIALS_PASSWORD_LIMIT], void* context); typedef struct { OnConnectSsid on_connect_ssid; diff --git a/tactility/src/apps/wifi_connect/wifi_connect_view.c b/tactility/src/apps/wifi_connect/wifi_connect_view.c index d2f2235d..5b674628 100644 --- a/tactility/src/apps/wifi_connect/wifi_connect_view.c +++ b/tactility/src/apps/wifi_connect/wifi_connect_view.c @@ -3,7 +3,7 @@ #include "log.h" #include "lvgl.h" #include "services/gui/gui.h" -#include "services/wifi/wifi_credentials.h" +#include "services/wifi/wifi_settings.h" #include "ui/spacer.h" #include "ui/style.h" #include "ui/toolbar.h" @@ -19,23 +19,37 @@ static void on_connect(lv_event_t* event) { const char* ssid = lv_textarea_get_text(view->ssid_textarea); const char* password = lv_textarea_get_text(view->password_textarea); - if (strlen(password) > 63) { + size_t password_len = strlen(password); + if (password_len > TT_WIFI_CREDENTIALS_PASSWORD_LIMIT) { // TODO: UI feedback TT_LOG_E(TAG, "Password too long"); return; } - char password_buffer[64]; - strcpy(password_buffer, password); + + size_t ssid_len = strlen(ssid); + if (ssid_len > TT_WIFI_SSID_LIMIT) { + // TODO: UI feedback + TT_LOG_E(TAG, "SSID too long"); + return; + } + + WifiApSettings settings; + strcpy((char*)settings.secret, password); + strcpy((char*)settings.ssid, ssid); + settings.auto_connect = TT_WIFI_AUTO_CONNECT; // No UI yet, so use global setting:w + WifiConnectBindings* bindings = &wifi->bindings; bindings->on_connect_ssid( - ssid, - password_buffer, + settings.ssid, + settings.secret, bindings->on_connect_ssid_context ); if (lv_obj_get_state(view->remember_switch) == LV_STATE_CHECKED) { - tt_wifi_credentials_set(ssid, password_buffer); + if (!tt_wifi_settings_save(&settings)) { + TT_LOG_E(TAG, "Failed to store credentials"); + } } } diff --git a/tactility/src/apps/wifi_manage/wifi_manage.c b/tactility/src/apps/wifi_manage/wifi_manage.c index e335cb2d..b1592ce8 100644 --- a/tactility/src/apps/wifi_manage/wifi_manage.c +++ b/tactility/src/apps/wifi_manage/wifi_manage.c @@ -3,11 +3,11 @@ #include "app.h" #include "apps/wifi_connect/wifi_connect_bundle.h" #include "services/loader/loader.h" +#include "services/wifi/wifi_settings.h" #include "tactility_core.h" #include "ui/lvgl_sync.h" #include "wifi_manage_state_updating.h" #include "wifi_manage_view.h" -#include "services/wifi/wifi_credentials.h" #define TAG "wifi_manage" @@ -15,10 +15,10 @@ static void wifi_manage_event_callback(const void* message, void* context); static void on_connect(const char* ssid) { - char password[TT_WIFI_CREDENTIALS_PASSWORD_LIMIT]; - if (tt_wifi_credentials_get(ssid, password)) { + WifiApSettings settings; + if (tt_wifi_settings_load(ssid, &settings)) { TT_LOG_I(TAG, "Connecting with known credentials"); - wifi_connect(ssid, password); + wifi_connect(ssid, settings.secret); } else { TT_LOG_I(TAG, "Starting connection dialog"); Bundle bundle = tt_bundle_alloc(); @@ -39,8 +39,7 @@ static void on_wifi_toggled(bool enabled) { static WifiManage* wifi_manage_alloc() { WifiManage* wifi = malloc(sizeof(WifiManage)); - PubSub* wifi_pubsub = wifi_get_pubsub(); - wifi->wifi_subscription = tt_pubsub_subscribe(wifi_pubsub, &wifi_manage_event_callback, wifi); + wifi->wifi_subscription = NULL; wifi->mutex = tt_mutex_alloc(MutexTypeNormal); wifi->state = (WifiManageState) { .scanning = wifi_is_scanning(), @@ -57,8 +56,6 @@ static WifiManage* wifi_manage_alloc() { } static void wifi_manage_free(WifiManage* wifi) { - PubSub* wifi_pubsub = wifi_get_pubsub(); - tt_pubsub_unsubscribe(wifi_pubsub, wifi->wifi_subscription); tt_mutex_free(wifi->mutex); free(wifi); @@ -116,6 +113,9 @@ static void wifi_manage_event_callback(const void* message, void* context) { static void app_show(App app, lv_obj_t* parent) { WifiManage* wifi = (WifiManage*)tt_app_get_data(app); + PubSub* wifi_pubsub = wifi_get_pubsub(); + wifi->wifi_subscription = tt_pubsub_subscribe(wifi_pubsub, &wifi_manage_event_callback, wifi); + // State update (it has its own locking) wifi_manage_state_set_radio_state(wifi, wifi_get_radio_state()); wifi_manage_state_set_scanning(wifi, wifi_is_scanning()); @@ -130,7 +130,10 @@ static void app_show(App app, lv_obj_t* parent) { wifi_manage_unlock(wifi); WifiRadioState radio_state = wifi_get_radio_state(); - if (radio_state == WIFI_RADIO_ON && !wifi_is_scanning()) { + bool can_scan = radio_state == WIFI_RADIO_ON || + radio_state == WIFI_RADIO_CONNECTION_PENDING || + radio_state == WIFI_RADIO_CONNECTION_ACTIVE; + if (can_scan && !wifi_is_scanning()) { wifi_scan(); } } @@ -138,6 +141,9 @@ static void app_show(App app, lv_obj_t* parent) { static void app_hide(App app) { WifiManage* wifi = (WifiManage*)tt_app_get_data(app); wifi_manage_lock(wifi); + PubSub* wifi_pubsub = wifi_get_pubsub(); + tt_pubsub_unsubscribe(wifi_pubsub, wifi->wifi_subscription); + wifi->wifi_subscription = NULL; wifi->view_enabled = false; wifi_manage_unlock(wifi); }