From eb2ae72d91a4b2ec0333a2e6d18eeeeee6b60464 Mon Sep 17 00:00:00 2001 From: Your Name Date: Mon, 18 May 2026 22:49:34 +0530 Subject: feat: add WiFi setup state machine + config_add_wifi with unit tests --- main/config.c | 57 ++++++++++++++++++++ main/config.h | 1 + main/wifi_setup.c | 89 +++++++++++++++++++++++++++++++ main/wifi_setup.h | 51 ++++++++++++++++++ tests/unit/Makefile | 5 +- tests/unit/test_wifi_setup.c | 121 +++++++++++++++++++++++++++++++++++++++++++ 6 files changed, 323 insertions(+), 1 deletion(-) create mode 100644 main/wifi_setup.c create mode 100644 main/wifi_setup.h create mode 100644 tests/unit/test_wifi_setup.c diff --git a/main/config.c b/main/config.c index 6753f40..32ae30e 100644 --- a/main/config.c +++ b/main/config.c @@ -320,3 +320,60 @@ void tollgate_config_derive_unique(tollgate_config_t *cfg) ESP_LOGI(TAG, "Unique config derived from nsec: SSID='%s', AP_IP=%s", cfg->ap_ssid, cfg->ap_ip_str); } + +esp_err_t tollgate_config_add_wifi(const char *ssid, const char *password) { + if (!ssid || !password) return ESP_ERR_INVALID_ARG; + if (g_config.network_count >= TOLLGATE_MAX_WIFI_NETWORKS) return ESP_ERR_NO_MEM; + + strncpy(g_config.networks[g_config.network_count].ssid, ssid, + sizeof(g_config.networks[g_config.network_count].ssid) - 1); + strncpy(g_config.networks[g_config.network_count].password, password, + sizeof(g_config.networks[g_config.network_count].password) - 1); + g_config.network_count++; + + cJSON *root = cJSON_CreateObject(); + cJSON *nsec = cJSON_CreateString(g_config.nsec); + cJSON_AddItemToObject(root, "nsec", nsec); + + cJSON *networks = cJSON_CreateArray(); + for (int i = 0; i < g_config.network_count; i++) { + cJSON *net = cJSON_CreateObject(); + cJSON_AddStringToObject(net, "ssid", g_config.networks[i].ssid); + cJSON_AddStringToObject(net, "password", g_config.networks[i].password); + cJSON_AddItemToArray(networks, net); + } + cJSON_AddItemToObject(root, "wifi_networks", networks); + + if (g_config.ap_password[0]) + cJSON_AddStringToObject(root, "ap_password", g_config.ap_password); + cJSON_AddStringToObject(root, "mint_url", g_config.mint_url); + cJSON_AddNumberToObject(root, "price_per_step", g_config.price_per_step); + cJSON_AddNumberToObject(root, "step_size_ms", g_config.step_size_ms); + + if (g_config.metric[0]) + cJSON_AddStringToObject(root, "metric", g_config.metric); + + cJSON_AddStringToObject(root, "nostr_geohash", g_config.nostr_geohash); + + cJSON *relays = cJSON_CreateArray(); + for (int i = 0; i < g_config.nostr_relay_count; i++) { + cJSON_AddItemToArray(relays, cJSON_CreateString(g_config.nostr_relays[i])); + } + cJSON_AddItemToObject(root, "nostr_relays", relays); + cJSON_AddNumberToObject(root, "nostr_publish_interval_s", g_config.nostr_publish_interval_s); + + char *json_str = cJSON_Print(root); + cJSON_Delete(root); + + FILE *f = fopen("/spiffs/config.json", "w"); + if (!f) { + free(json_str); + return ESP_FAIL; + } + fputs(json_str, f); + fclose(f); + free(json_str); + + ESP_LOGI(TAG, "Added WiFi network '%s' to config (total: %d)", ssid, g_config.network_count); + return ESP_OK; +} diff --git a/main/config.h b/main/config.h index fa4d95c..dad0ef0 100644 --- a/main/config.h +++ b/main/config.h @@ -71,5 +71,6 @@ esp_err_t tollgate_config_init(void); const tollgate_config_t *tollgate_config_get(void); esp_err_t tollgate_config_get_wifi(wifi_config_t *wifi_config); esp_err_t tollgate_config_get_next_wifi(wifi_config_t *wifi_config); +esp_err_t tollgate_config_add_wifi(const char *ssid, const char *password); #endif diff --git a/main/wifi_setup.c b/main/wifi_setup.c new file mode 100644 index 0000000..b2669e9 --- /dev/null +++ b/main/wifi_setup.c @@ -0,0 +1,89 @@ +#include "wifi_setup.h" +#include + +void wifi_setup_init(wifi_setup_t *setup) { + if (!setup) return; + memset(setup, 0, sizeof(*setup)); + setup->state = SETUP_SCAN; + setup->selected_ap = -1; +} + +void wifi_setup_set_aps(wifi_setup_t *setup, const wifi_ap_info_t *aps, int count) { + if (!setup || !aps) return; + if (count > WIFI_SETUP_MAX_APS) count = WIFI_SETUP_MAX_APS; + memcpy(setup->aps, aps, count * sizeof(wifi_ap_info_t)); + setup->ap_count = count; + setup->list_scroll = 0; + setup->state = SETUP_LIST; +} + +int wifi_setup_visible_count(const wifi_setup_t *setup) { + if (!setup) return 0; + int remaining = setup->ap_count - setup->list_scroll; + if (remaining > WIFI_SETUP_MAX_VISIBLE) remaining = WIFI_SETUP_MAX_VISIBLE; + return remaining < 0 ? 0 : remaining; +} + +const wifi_ap_info_t *wifi_setup_get_visible(const wifi_setup_t *setup, int idx) { + if (!setup || idx < 0 || idx >= wifi_setup_visible_count(setup)) return NULL; + return &setup->aps[setup->list_scroll + idx]; +} + +setup_state_t wifi_setup_handle_select(wifi_setup_t *setup, int list_idx) { + if (!setup || setup->state != SETUP_LIST) return setup ? setup->state : SETUP_CANCELLED; + if (list_idx < 0 || list_idx >= wifi_setup_visible_count(setup)) return setup->state; + + int real_idx = setup->list_scroll + list_idx; + setup->selected_ap = real_idx; + strncpy(setup->selected_ssid, setup->aps[real_idx].ssid, WIFI_SETUP_SSID_LEN - 1); + setup->selected_ssid[WIFI_SETUP_SSID_LEN - 1] = '\0'; + setup->state = SETUP_PASSWORD; + return setup->state; +} + +setup_state_t wifi_setup_handle_connect(wifi_setup_t *setup) { + if (!setup || setup->state != SETUP_PASSWORD) return setup ? setup->state : SETUP_CANCELLED; + setup->state = SETUP_CONNECTING; + return setup->state; +} + +setup_state_t wifi_setup_handle_connect_result(wifi_setup_t *setup, bool success, const char *ip) { + if (!setup) return SETUP_CANCELLED; + if (setup->state != SETUP_CONNECTING) return setup->state; + + if (success) { + setup->state = SETUP_SUCCESS; + if (ip) { + strncpy(setup->connect_ip, ip, sizeof(setup->connect_ip) - 1); + setup->connect_ip[sizeof(setup->connect_ip) - 1] = '\0'; + } + setup->connect_failed_auth = false; + } else { + setup->state = SETUP_FAILED; + setup->connect_failed_auth = true; + setup->connect_ip[0] = '\0'; + } + return setup->state; +} + +setup_state_t wifi_setup_handle_cancel(wifi_setup_t *setup) { + if (!setup) return SETUP_CANCELLED; + setup->state = SETUP_CANCELLED; + return setup->state; +} + +setup_state_t wifi_setup_handle_retry(wifi_setup_t *setup) { + if (!setup) return SETUP_CANCELLED; + if (setup->state != SETUP_FAILED) return setup->state; + setup->state = SETUP_PASSWORD; + setup->connect_failed_auth = false; + return setup->state; +} + +setup_state_t wifi_setup_handle_change_network(wifi_setup_t *setup) { + if (!setup) return SETUP_CANCELLED; + if (setup->state != SETUP_FAILED) return setup->state; + setup->state = SETUP_LIST; + setup->connect_failed_auth = false; + return setup->state; +} diff --git a/main/wifi_setup.h b/main/wifi_setup.h new file mode 100644 index 0000000..17712d5 --- /dev/null +++ b/main/wifi_setup.h @@ -0,0 +1,51 @@ +#ifndef WIFI_SETUP_H +#define WIFI_SETUP_H + +#include "esp_err.h" +#include +#include + +#define WIFI_SETUP_MAX_APS 20 +#define WIFI_SETUP_MAX_VISIBLE 8 +#define WIFI_SETUP_SSID_LEN 33 +#define WIFI_SETUP_PASS_LEN 64 + +typedef enum { + SETUP_SCAN, + SETUP_LIST, + SETUP_PASSWORD, + SETUP_CONNECTING, + SETUP_SUCCESS, + SETUP_FAILED, + SETUP_CANCELLED +} setup_state_t; + +typedef struct { + char ssid[WIFI_SETUP_SSID_LEN]; + int rssi; + bool secured; +} wifi_ap_info_t; + +typedef struct { + setup_state_t state; + wifi_ap_info_t aps[WIFI_SETUP_MAX_APS]; + int ap_count; + int list_scroll; + int selected_ap; + char selected_ssid[WIFI_SETUP_SSID_LEN]; + char connect_ip[16]; + bool connect_failed_auth; +} wifi_setup_t; + +void wifi_setup_init(wifi_setup_t *setup); +void wifi_setup_set_aps(wifi_setup_t *setup, const wifi_ap_info_t *aps, int count); +int wifi_setup_visible_count(const wifi_setup_t *setup); +const wifi_ap_info_t *wifi_setup_get_visible(const wifi_setup_t *setup, int idx); +setup_state_t wifi_setup_handle_select(wifi_setup_t *setup, int list_idx); +setup_state_t wifi_setup_handle_connect(wifi_setup_t *setup); +setup_state_t wifi_setup_handle_connect_result(wifi_setup_t *setup, bool success, const char *ip); +setup_state_t wifi_setup_handle_cancel(wifi_setup_t *setup); +setup_state_t wifi_setup_handle_retry(wifi_setup_t *setup); +setup_state_t wifi_setup_handle_change_network(wifi_setup_t *setup); + +#endif diff --git a/tests/unit/Makefile b/tests/unit/Makefile index b978891..0faf47e 100644 --- a/tests/unit/Makefile +++ b/tests/unit/Makefile @@ -22,7 +22,7 @@ LDFLAGS := -lmbedcrypto -lcjson -lm SECP256K1_OBJ := secp256k1.o precomputed_ecmult.o precomputed_ecmult_gen.o -TESTS := test_geohash test_identity test_nostr_event test_cashu test_session test_tollgate_client test_lnurl_pay test_lightning_payout test_mcp_handler test_nip04 test_cvm_server test_touch test_keyboard +TESTS := test_geohash test_identity test_nostr_event test_cashu test_session test_tollgate_client test_lnurl_pay test_lightning_payout test_mcp_handler test_nip04 test_cvm_server test_touch test_keyboard test_wifi_setup .PHONY: all test clean $(TESTS) @@ -87,5 +87,8 @@ test_touch: test_touch.c $(REPO_ROOT)/main/touch.c test_keyboard: test_keyboard.c $(REPO_ROOT)/main/keyboard.c $(CC) $(CFLAGS) $< $(REPO_ROOT)/main/keyboard.c -o $@ $(LDFLAGS) +test_wifi_setup: test_wifi_setup.c $(REPO_ROOT)/main/wifi_setup.c + $(CC) $(CFLAGS) $< $(REPO_ROOT)/main/wifi_setup.c -o $@ $(LDFLAGS) + clean: rm -f $(TESTS) $(SECP256K1_OBJ) diff --git a/tests/unit/test_wifi_setup.c b/tests/unit/test_wifi_setup.c new file mode 100644 index 0000000..5f1b8f0 --- /dev/null +++ b/tests/unit/test_wifi_setup.c @@ -0,0 +1,121 @@ +#include "test_framework.h" +#include "../../main/wifi_setup.h" +#include + +int main(void) +{ + printf("=== test_wifi_setup ===\n"); + + wifi_setup_t setup; + wifi_setup_init(&setup); + + ASSERT_EQ_INT(SETUP_SCAN, (int)setup.state, "Init state = SCAN"); + ASSERT_EQ_INT(0, setup.ap_count, "Init ap_count = 0"); + ASSERT_EQ_INT(-1, setup.selected_ap, "Init selected_ap = -1"); + + wifi_ap_info_t test_aps[3] = { + {"FastNet", -30, true}, + {"SlowNet", -70, true}, + {"OpenNet", -50, false}, + }; + wifi_setup_set_aps(&setup, test_aps, 3); + + ASSERT_EQ_INT(SETUP_LIST, (int)setup.state, "After set_aps: state = LIST"); + ASSERT_EQ_INT(3, setup.ap_count, "After set_aps: ap_count = 3"); + ASSERT_EQ_INT(3, wifi_setup_visible_count(&setup), "Visible count = 3"); + + { + const wifi_ap_info_t *ap = wifi_setup_get_visible(&setup, 0); + ASSERT(ap != NULL, "Visible AP 0 is not NULL"); + ASSERT_EQ_STR("FastNet", ap->ssid, "AP 0 = FastNet"); + ASSERT_EQ_INT(-30, ap->rssi, "AP 0 RSSI = -30"); + ASSERT(ap->secured, "AP 0 is secured"); + } + + { + const wifi_ap_info_t *ap = wifi_setup_get_visible(&setup, 2); + ASSERT(ap != NULL, "Visible AP 2 is not NULL"); + ASSERT_EQ_STR("OpenNet", ap->ssid, "AP 2 = OpenNet"); + ASSERT(!ap->secured, "AP 2 is open"); + } + + { + const wifi_ap_info_t *ap = wifi_setup_get_visible(&setup, 3); + ASSERT(ap == NULL, "Out of range returns NULL"); + } + + setup_state_t s = wifi_setup_handle_select(&setup, 0); + ASSERT_EQ_INT(SETUP_PASSWORD, (int)s, "Select AP 0: state = PASSWORD"); + ASSERT_EQ_INT(0, setup.selected_ap, "Selected AP index = 0"); + ASSERT_EQ_STR("FastNet", setup.selected_ssid, "Selected SSID = FastNet"); + + wifi_setup_handle_connect(&setup); + ASSERT_EQ_INT(SETUP_CONNECTING, (int)setup.state, "Connect: state = CONNECTING"); + + s = wifi_setup_handle_connect_result(&setup, true, "192.168.1.42"); + ASSERT_EQ_INT(SETUP_SUCCESS, (int)s, "Connect success: state = SUCCESS"); + ASSERT_EQ_STR("192.168.1.42", setup.connect_ip, "Connect IP stored"); + + wifi_setup_init(&setup); + wifi_setup_set_aps(&setup, test_aps, 3); + wifi_setup_handle_select(&setup, 1); + wifi_setup_handle_connect(&setup); + + s = wifi_setup_handle_connect_result(&setup, false, NULL); + ASSERT_EQ_INT(SETUP_FAILED, (int)s, "Connect fail: state = FAILED"); + ASSERT(setup.connect_failed_auth, "Failed auth flag set"); + + s = wifi_setup_handle_retry(&setup); + ASSERT_EQ_INT(SETUP_PASSWORD, (int)s, "Retry: state = PASSWORD"); + ASSERT(!setup.connect_failed_auth, "Retry clears auth flag"); + + wifi_setup_init(&setup); + wifi_setup_set_aps(&setup, test_aps, 3); + wifi_setup_handle_select(&setup, 0); + wifi_setup_handle_connect(&setup); + wifi_setup_handle_connect_result(&setup, false, NULL); + + s = wifi_setup_handle_change_network(&setup); + ASSERT_EQ_INT(SETUP_LIST, (int)s, "Change network: state = LIST"); + + wifi_setup_init(&setup); + s = wifi_setup_handle_cancel(&setup); + ASSERT_EQ_INT(SETUP_CANCELLED, (int)s, "Cancel: state = CANCELLED"); + + wifi_setup_init(&setup); + wifi_ap_info_t many_aps[12]; + for (int i = 0; i < 12; i++) { + snprintf(many_aps[i].ssid, sizeof(many_aps[i].ssid), "Net%d", i); + many_aps[i].rssi = -30 - i * 5; + many_aps[i].secured = true; + } + wifi_setup_set_aps(&setup, many_aps, 12); + ASSERT_EQ_INT(12, setup.ap_count, "12 APs stored"); + ASSERT_EQ_INT(8, wifi_setup_visible_count(&setup), "Only 8 visible"); + + { + const wifi_ap_info_t *ap = wifi_setup_get_visible(&setup, 7); + ASSERT(ap != NULL, "8th visible AP exists"); + ASSERT_EQ_STR("Net7", ap->ssid, "8th visible = Net7"); + } + + wifi_setup_init(&setup); + s = wifi_setup_handle_select(&setup, 0); + ASSERT_EQ_INT(SETUP_SCAN, (int)s, "Select in SCAN state = no change"); + + s = wifi_setup_handle_select(&setup, -1); + ASSERT_EQ_INT(SETUP_SCAN, (int)s, "Select invalid idx = no change"); + + wifi_setup_init(&setup); + wifi_setup_set_aps(&setup, test_aps, 3); + s = wifi_setup_handle_retry(&setup); + ASSERT_EQ_INT(SETUP_LIST, (int)s, "Retry in LIST state = no change"); + + s = wifi_setup_handle_change_network(&setup); + ASSERT_EQ_INT(SETUP_LIST, (int)s, "Change network in LIST state = no change"); + + ASSERT_EQ_INT(0, wifi_setup_visible_count(NULL), "NULL setup returns 0"); + ASSERT(NULL == wifi_setup_get_visible(NULL, 0), "NULL setup returns NULL AP"); + + TEST_SUMMARY(); +} -- cgit v1.2.3