diff --git a/components/sys_wifi/CMakeLists.txt b/components/sys_wifi/CMakeLists.txt new file mode 100644 index 0000000..ab3ec4e --- /dev/null +++ b/components/sys_wifi/CMakeLists.txt @@ -0,0 +1,15 @@ +idf_component_register( + SRCS + "sys_wifi.c" + INCLUDE_DIRS + "include" + REQUIRES + sys_core + esp_wifi + esp_event + esp_netif + nvs_flash + esp_http_server + freertos + log +) diff --git a/components/sys_wifi/Kconfig b/components/sys_wifi/Kconfig new file mode 100644 index 0000000..b4e5d1a --- /dev/null +++ b/components/sys_wifi/Kconfig @@ -0,0 +1,20 @@ +menu "System Wi-Fi" + +config SYS_WIFI_ENABLED + bool "Enable Wi-Fi support" + default y + +config SYS_WIFI_AP_SSID + string "Provisioning access point SSID" + default "Micro-OS Setup" + depends on SYS_WIFI_ENABLED + +config SYS_WIFI_AP_PASSWORD + string "Provisioning access point password" + default "12345678" + depends on SYS_WIFI_ENABLED + help + Password for the temporary provisioning access point. It must be at + least 8 characters long. + +endmenu diff --git a/components/sys_wifi/include/sys_wifi.h b/components/sys_wifi/include/sys_wifi.h new file mode 100644 index 0000000..983d4d5 --- /dev/null +++ b/components/sys_wifi/include/sys_wifi.h @@ -0,0 +1,17 @@ +#ifndef SYS_WIFI_H +#define SYS_WIFI_H + +#include "esp_err.h" + +#ifdef __cplusplus +extern "C" { +#endif + +esp_err_t sys_wifi_init(void); +esp_err_t sys_wifi_clear_credentials(void); + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/components/sys_wifi/sys_wifi.c b/components/sys_wifi/sys_wifi.c new file mode 100644 index 0000000..4e7028d --- /dev/null +++ b/components/sys_wifi/sys_wifi.c @@ -0,0 +1,338 @@ +#include "sys_wifi.h" +#include "os_event.h" +#include "os_status.h" +#include "sdkconfig.h" +#include "esp_event.h" +#include "esp_http_server.h" +#include "esp_log.h" +#include "esp_netif.h" +#include "esp_wifi.h" +#include "nvs.h" +#include "nvs_flash.h" +#include +#include +#include +#include + +static const char *TAG = "SYS_WIFI"; +static esp_netif_t *s_sta_netif; +static esp_netif_t *s_ap_netif; +static httpd_handle_t s_http_server; +static char s_sta_ssid[33]; +static char s_sta_password[65]; + +static void copy_wifi_string(uint8_t *destination, size_t destination_size, const char *source) +{ + size_t length = strlen(source); + if (length >= destination_size) length = destination_size - 1; + memcpy(destination, source, length); + destination[length] = '\0'; +} + +static void set_status(wifi_state_t state, int8_t rssi) +{ + sys_status_set_wifi(state, rssi); + sys_event_post(SYS_EVENT_WIFI_STATUS_CHANGED, (int32_t)state, NULL); +} + +static esp_err_t save_credentials(const char *ssid, const char *password) +{ + nvs_handle_t nvs; + esp_err_t err = nvs_open("wifi", NVS_READWRITE, &nvs); + if (err != ESP_OK) return err; + err = nvs_set_str(nvs, "ssid", ssid); + if (err == ESP_OK) err = nvs_set_str(nvs, "password", password); + if (err == ESP_OK) err = nvs_commit(nvs); + nvs_close(nvs); + return err; +} + +static esp_err_t load_credentials(void) +{ + nvs_handle_t nvs; + size_t ssid_len = sizeof(s_sta_ssid); + size_t password_len = sizeof(s_sta_password); + esp_err_t err = nvs_open("wifi", NVS_READONLY, &nvs); + if (err != ESP_OK) return err; + err = nvs_get_str(nvs, "ssid", s_sta_ssid, &ssid_len); + if (err == ESP_OK) err = nvs_get_str(nvs, "password", s_sta_password, &password_len); + nvs_close(nvs); + return err; +} + +static int hex_value(char value) +{ + if (value >= '0' && value <= '9') return value - '0'; + if (value >= 'a' && value <= 'f') return value - 'a' + 10; + if (value >= 'A' && value <= 'F') return value - 'A' + 10; + return -1; +} + +static bool form_value(const char *body, const char *key, char *output, size_t output_size) +{ + size_t key_len = strlen(key); + const char *cursor = body; + size_t output_len = 0; + while (cursor && *cursor) { + if ((cursor == body || cursor[-1] == '&') && strncmp(cursor, key, key_len) == 0 && cursor[key_len] == '=') { + cursor += key_len + 1; + while (*cursor && *cursor != '&') { + int value = 0; + if (*cursor == '+') { + value = ' '; + cursor++; + } else if (*cursor == '%' && hex_value(cursor[1]) >= 0 && hex_value(cursor[2]) >= 0) { + value = (hex_value(cursor[1]) << 4) | hex_value(cursor[2]); + cursor += 3; + } else { + value = (unsigned char)*cursor++; + } + if (output_len + 1 >= output_size) return false; + output[output_len++] = (char)value; + } + output[output_len] = '\0'; + return output_len > 0; + } + cursor = strchr(cursor, '&'); + if (cursor) cursor++; + } + return false; +} + +static void append_json_string(httpd_req_t *request, const uint8_t *value, size_t length) +{ + for (size_t i = 0; i < length && value[i] != '\0'; i++) { + char escaped[2] = {(char)value[i], '\0'}; + if (value[i] == '"' || value[i] == '\\') { + httpd_resp_send_chunk(request, "\\", 1); + } + httpd_resp_send_chunk(request, escaped, 1); + } +} + +static esp_err_t provisioning_scan(httpd_req_t *request) +{ + wifi_scan_config_t scan_config = { + .show_hidden = true, + .scan_type = WIFI_SCAN_TYPE_ACTIVE, + }; + esp_err_t err = esp_wifi_scan_start(&scan_config, true); + if (err != ESP_OK) { + httpd_resp_send_err(request, HTTPD_500_INTERNAL_SERVER_ERROR, "Wi-Fi scan failed"); + return err; + } + + uint16_t count = 0; + esp_wifi_scan_get_ap_num(&count); + if (count > 32) count = 32; + wifi_ap_record_t *records = calloc(count, sizeof(wifi_ap_record_t)); + if (count > 0 && !records) { + httpd_resp_send_err(request, HTTPD_500_INTERNAL_SERVER_ERROR, "Not enough memory for scan"); + return ESP_ERR_NO_MEM; + } + if (count > 0) { + err = esp_wifi_scan_get_ap_records(&count, records); + if (err != ESP_OK) { + httpd_resp_send_err(request, HTTPD_500_INTERNAL_SERVER_ERROR, "Could not read scan results"); + free(records); + return err; + } + } + + httpd_resp_set_type(request, "application/json"); + httpd_resp_send_chunk(request, "[", 1); + uint16_t visible_count = 0; + for (uint16_t i = 0; i < count; i++) { + if (records[i].ssid[0] == '\0') continue; + + char prefix[32]; + int prefix_length = snprintf(prefix, sizeof(prefix), "%s{\"ssid\":\"", + visible_count == 0 ? "" : ","); + httpd_resp_send_chunk(request, prefix, prefix_length); + append_json_string(request, records[i].ssid, sizeof(records[i].ssid)); + char suffix[48]; + int suffix_length = snprintf(suffix, sizeof(suffix), "\",\"rssi\":%d}", records[i].rssi); + httpd_resp_send_chunk(request, suffix, suffix_length); + visible_count++; + } + httpd_resp_send_chunk(request, "]", 1); + free(records); + return httpd_resp_send_chunk(request, NULL, 0); +} + +static esp_err_t provisioning_get(httpd_req_t *request) +{ + const char *page = + "" + "ABrobot Wi-Fi

ABrobot Wi-Fi setup

" + "
" + "

" + "" + "

" + "

" + "

" + ""; + httpd_resp_set_type(request, "text/html"); + return httpd_resp_send(request, page, HTTPD_RESP_USE_STRLEN); +} + +static esp_err_t provisioning_post(httpd_req_t *request) +{ + char body[160] = {0}; + char ssid[sizeof(s_sta_ssid)] = {0}; + char password[sizeof(s_sta_password)] = {0}; + int received = httpd_req_recv(request, body, sizeof(body) - 1); + if (received <= 0 || !form_value(body, "ssid", ssid, sizeof(ssid)) || + !form_value(body, "password", password, sizeof(password))) { + httpd_resp_send_err(request, HTTPD_400_BAD_REQUEST, "SSID and password are required"); + return ESP_FAIL; + } + + esp_err_t err = save_credentials(ssid, password); + if (err != ESP_OK) { + httpd_resp_send_err(request, HTTPD_500_INTERNAL_SERVER_ERROR, "Could not save credentials"); + return err; + } + + snprintf(s_sta_ssid, sizeof(s_sta_ssid), "%s", ssid); + snprintf(s_sta_password, sizeof(s_sta_password), "%s", password); + httpd_resp_sendstr(request, "Credentials saved. The device is connecting; you can close this page."); + set_status(WIFI_STATE_CONNECTING, 0); + err = esp_wifi_set_mode(WIFI_MODE_STA); + if (err != ESP_OK) { + ESP_LOGE(TAG, "Failed to switch to station mode: %s", esp_err_to_name(err)); + return err; + } + wifi_config_t config = {0}; + copy_wifi_string(config.sta.ssid, sizeof(config.sta.ssid), s_sta_ssid); + copy_wifi_string(config.sta.password, sizeof(config.sta.password), s_sta_password); + err = esp_wifi_set_config(WIFI_IF_STA, &config); + if (err != ESP_OK) { + ESP_LOGE(TAG, "Failed to apply station credentials: %s", esp_err_to_name(err)); + return err; + } + err = esp_wifi_connect(); + if (err != ESP_OK) { + ESP_LOGE(TAG, "Failed to start station connection: %s", esp_err_to_name(err)); + return err; + } + ESP_LOGI(TAG, "Station connection started for '%s'", s_sta_ssid); + return ESP_OK; +} + +static void start_provisioning_server(void) +{ + httpd_config_t config = HTTPD_DEFAULT_CONFIG(); + if (httpd_start(&s_http_server, &config) != ESP_OK) { + ESP_LOGE(TAG, "Failed to start provisioning web server"); + return; + } + const httpd_uri_t get_uri = {.uri = "/", .method = HTTP_GET, .handler = provisioning_get}; + const httpd_uri_t scan_uri = {.uri = "/scan", .method = HTTP_GET, .handler = provisioning_scan}; + const httpd_uri_t post_uri = {.uri = "/configure", .method = HTTP_POST, .handler = provisioning_post}; + httpd_register_uri_handler(s_http_server, &get_uri); + httpd_register_uri_handler(s_http_server, &scan_uri); + httpd_register_uri_handler(s_http_server, &post_uri); +} + +static void start_softap(void) +{ + wifi_config_t ap_config = {0}; + copy_wifi_string(ap_config.ap.ssid, sizeof(ap_config.ap.ssid), CONFIG_SYS_WIFI_AP_SSID); + copy_wifi_string(ap_config.ap.password, sizeof(ap_config.ap.password), CONFIG_SYS_WIFI_AP_PASSWORD); + ap_config.ap.authmode = WIFI_AUTH_WPA2_PSK; + ap_config.ap.max_connection = 2; + esp_wifi_set_mode(WIFI_MODE_APSTA); + esp_wifi_set_config(WIFI_IF_AP, &ap_config); + esp_wifi_start(); + set_status(WIFI_STATE_AP_MODE, 0); + start_provisioning_server(); + ESP_LOGI(TAG, "Provisioning AP '%s', browse to http://192.168.4.1", CONFIG_SYS_WIFI_AP_SSID); +} + +static void start_station(void) +{ + wifi_config_t config = {0}; + copy_wifi_string(config.sta.ssid, sizeof(config.sta.ssid), s_sta_ssid); + copy_wifi_string(config.sta.password, sizeof(config.sta.password), s_sta_password); + esp_wifi_set_mode(WIFI_MODE_STA); + esp_wifi_set_config(WIFI_IF_STA, &config); + esp_wifi_start(); + set_status(WIFI_STATE_CONNECTING, 0); + esp_wifi_connect(); + ESP_LOGI(TAG, "Connecting to Wi-Fi network '%s'", s_sta_ssid); +} + +static void wifi_event_handler(void *arg, esp_event_base_t event_base, int32_t event_id, void *event_data) +{ + if (event_base == WIFI_EVENT && event_id == WIFI_EVENT_STA_DISCONNECTED) { + set_status(WIFI_STATE_CONNECTING, 0); + esp_wifi_connect(); + } else if (event_base == IP_EVENT && event_id == IP_EVENT_STA_GOT_IP) { + wifi_ap_record_t record; + int8_t rssi = 0; + if (esp_wifi_sta_get_ap_info(&record) == ESP_OK) rssi = record.rssi; + set_status(WIFI_STATE_CONNECTED, rssi); + ESP_LOGI(TAG, "Wi-Fi connected"); + } +} + +esp_err_t sys_wifi_clear_credentials(void) +{ + nvs_handle_t nvs; + esp_err_t err = nvs_open("wifi", NVS_READWRITE, &nvs); + if (err != ESP_OK) return err; + err = nvs_erase_all(nvs); + if (err == ESP_OK) err = nvs_commit(nvs); + nvs_close(nvs); + return err; +} + +esp_err_t sys_wifi_init(void) +{ +#if !CONFIG_SYS_WIFI_ENABLED + set_status(WIFI_STATE_OFF, 0); + return ESP_OK; +#else + esp_err_t err = nvs_flash_init(); + if (err == ESP_ERR_NVS_NO_FREE_PAGES || err == ESP_ERR_NVS_NEW_VERSION_FOUND) { + ESP_ERROR_CHECK(nvs_flash_erase()); + err = nvs_flash_init(); + } + if (err != ESP_OK) return err; + + err = esp_netif_init(); + if (err != ESP_OK && err != ESP_ERR_INVALID_STATE) return err; + err = esp_event_loop_create_default(); + if (err != ESP_OK && err != ESP_ERR_INVALID_STATE) return err; + s_sta_netif = esp_netif_create_default_wifi_sta(); + s_ap_netif = esp_netif_create_default_wifi_ap(); + if (!s_sta_netif || !s_ap_netif) return ESP_ERR_NO_MEM; + + wifi_init_config_t wifi_config = WIFI_INIT_CONFIG_DEFAULT(); + err = esp_wifi_init(&wifi_config); + if (err != ESP_OK) return err; + ESP_ERROR_CHECK(esp_event_handler_register(WIFI_EVENT, ESP_EVENT_ANY_ID, &wifi_event_handler, NULL)); + ESP_ERROR_CHECK(esp_event_handler_register(IP_EVENT, IP_EVENT_STA_GOT_IP, &wifi_event_handler, NULL)); + + if (load_credentials() == ESP_OK && s_sta_ssid[0] != '\0' && s_sta_password[0] != '\0') { + start_station(); + } else { + start_softap(); + } + return ESP_OK; +#endif +} diff --git a/main/CMakeLists.txt b/main/CMakeLists.txt index f4158c2..77c2377 100644 --- a/main/CMakeLists.txt +++ b/main/CMakeLists.txt @@ -14,6 +14,7 @@ idf_component_register( sys_gfx sys_app sys_cli + sys_wifi driver esp_driver_gpio esp_driver_i2c diff --git a/main/main.c b/main/main.c index 54af2ed..527f706 100644 --- a/main/main.c +++ b/main/main.c @@ -16,6 +16,7 @@ #include "os_input.h" #include "os_app.h" #include "os_cli.h" +#include "sys_wifi.h" #include "apps.h" static const char *TAG = "MICRO_OS"; @@ -85,9 +86,16 @@ void app_main(void) // 1. Core Services & Telemetry sys_event_init(); sys_status_init(); + esp_err_t err; + + // 1b. Wi-Fi and provisioning services + err = sys_wifi_init(); + if (err != ESP_OK) { + ESP_LOGE(TAG, "Failed to initialize Wi-Fi subsystem: %s", esp_err_to_name(err)); + } // 2. Display & Graphics Subsystem - esp_err_t err = sys_gfx_init(); + err = sys_gfx_init(); if (err != ESP_OK) { ESP_LOGE(TAG, "Failed to initialize graphics subsystem: %s", esp_err_to_name(err)); } diff --git a/sdkconfig.defaults b/sdkconfig.defaults index c124d7b..2e30ed7 100644 --- a/sdkconfig.defaults +++ b/sdkconfig.defaults @@ -9,6 +9,9 @@ CONFIG_ESP_DEFAULT_CPU_FREQ_MHZ=160 CONFIG_ESP_CONSOLE_USB_SERIAL_JTAG=y CONFIG_ESP_CONSOLE_SECONDARY_NONE=y +# Wi-Fi station/provisioning support +CONFIG_SYS_WIFI_ENABLED=y + # FreeRTOS configuration CONFIG_FREERTOS_HZ=1000 CONFIG_FREERTOS_USE_TRACE_FACILITY=y