feat: implement Wi-Fi provisioning and persistent configuration

This commit is contained in:
2026-09-03 22:58:57 +03:00
parent 8ffd6dabcf
commit b10c7f5972
29 changed files with 1713 additions and 16 deletions
+5
View File
@@ -0,0 +1,5 @@
idf_component_register(
SRCS "maze32_network.c" "maze32_provisioning.c" "maze32_provisioning_protocol.c"
INCLUDE_DIRS "include"
REQUIRES esp_event esp_http_server esp_netif esp_wifi maze32_config
)
@@ -0,0 +1,73 @@
#ifndef MAZE32_NETWORK_H
#define MAZE32_NETWORK_H
#include <stddef.h>
#include <stdint.h>
#include "esp_err.h"
#include "esp_http_server.h"
#include "esp_netif.h"
#include "freertos/FreeRTOS.h"
#include "freertos/event_groups.h"
#include "freertos/semphr.h"
#include "maze32_wifi_config.h"
#ifdef __cplusplus
extern "C" {
#endif
#define MAZE32_WIFI_SCAN_LIMIT 8U
typedef enum {
MAZE32_NETWORK_BOOTING = 0,
MAZE32_NETWORK_STATION_CONNECTING,
MAZE32_NETWORK_STATION_CONNECTED,
MAZE32_NETWORK_PROVISIONING,
MAZE32_NETWORK_ERROR,
} maze32_network_state_t;
typedef struct {
char ssid[MAZE32_WIFI_SSID_MAX_BYTES + 1U];
int8_t rssi;
uint8_t auth_mode;
} maze32_wifi_scan_result_t;
typedef struct {
EventGroupHandle_t events;
SemaphoreHandle_t operation_mutex;
esp_netif_t *station_netif;
esp_netif_t *access_point_netif;
volatile maze32_network_state_t state;
unsigned int retry_count;
char access_point_ssid[24];
maze32_reconfiguration_guard_fn reconfiguration_guard;
void *guard_context;
httpd_handle_t server;
} maze32_network_t;
esp_err_t maze32_network_init(maze32_network_t *network,
maze32_reconfiguration_guard_fn guard,
void *guard_context);
esp_err_t maze32_network_start(maze32_network_t *network);
esp_err_t maze32_network_apply_credentials(maze32_network_t *network,
const maze32_wifi_credentials_t *credentials);
esp_err_t maze32_network_reset_credentials(maze32_network_t *network);
void maze32_network_retry_saved_credentials(void *context);
void maze32_network_return_to_provisioning(void *context);
esp_err_t maze32_network_scan(maze32_network_t *network,
maze32_wifi_scan_result_t *results,
size_t capacity,
size_t *count);
maze32_network_state_t maze32_network_state(const maze32_network_t *network);
const char *maze32_network_state_name(maze32_network_state_t state);
const char *maze32_network_access_point_ssid(const maze32_network_t *network);
esp_err_t maze32_network_ip_address(const maze32_network_t *network,
char *output,
size_t output_size);
esp_err_t maze32_provisioning_register(httpd_handle_t server, maze32_network_t *network);
#ifdef __cplusplus
}
#endif
#endif
@@ -0,0 +1,20 @@
#ifndef MAZE32_PROVISIONING_PROTOCOL_H
#define MAZE32_PROVISIONING_PROTOCOL_H
#include <stddef.h>
#include "maze32_wifi_config.h"
#ifdef __cplusplus
extern "C" {
#endif
int maze32_provisioning_parse_credentials(const char *body,
size_t length,
maze32_wifi_credentials_t *credentials);
#ifdef __cplusplus
}
#endif
#endif
+352
View File
@@ -0,0 +1,352 @@
#include "maze32_network.h"
#include <stdio.h>
#include <string.h>
#include "esp_log.h"
#include "esp_mac.h"
#include "esp_wifi.h"
#include "maze32_config_store.h"
#define MAZE32_CONNECTED_BIT BIT0
#define MAZE32_FAILED_BIT BIT1
#define MAZE32_STATION_RETRY_LIMIT 3U
#define MAZE32_STATION_TIMEOUT_MS 12000U
static const char *TAG = "maze32_network";
static maze32_network_t *active_network;
static void set_state(maze32_network_t *network, maze32_network_state_t state)
{
network->state = state;
ESP_LOGI(TAG, "Network state: %s", maze32_network_state_name(state));
}
static void network_event(void *argument,
esp_event_base_t event_base,
int32_t event_id,
void *event_data)
{
(void)event_data;
maze32_network_t *network = argument;
if (event_base == WIFI_EVENT && event_id == WIFI_EVENT_STA_START &&
network->state == MAZE32_NETWORK_STATION_CONNECTING) {
esp_wifi_connect();
} else if (event_base == WIFI_EVENT && event_id == WIFI_EVENT_STA_DISCONNECTED &&
network->state == MAZE32_NETWORK_STATION_CONNECTING) {
if (network->retry_count < MAZE32_STATION_RETRY_LIMIT) {
++network->retry_count;
ESP_LOGW(TAG, "Station connection failed; retry %u/%u",
network->retry_count, MAZE32_STATION_RETRY_LIMIT);
esp_wifi_connect();
} else {
xEventGroupSetBits(network->events, MAZE32_FAILED_BIT);
}
} else if (event_base == IP_EVENT && event_id == IP_EVENT_STA_GOT_IP) {
xEventGroupSetBits(network->events, MAZE32_CONNECTED_BIT);
}
}
static esp_err_t stop_wifi(void)
{
esp_err_t result = esp_wifi_stop();
if (result == ESP_ERR_WIFI_NOT_STARTED || result == ESP_ERR_WIFI_NOT_INIT) {
return ESP_OK;
}
return result;
}
static esp_err_t start_provisioning(maze32_network_t *network)
{
set_state(network, MAZE32_NETWORK_PROVISIONING);
esp_err_t result = stop_wifi();
if (result != ESP_OK) {
return result;
}
wifi_config_t access_point = {0};
size_t ssid_length = strlen(network->access_point_ssid);
memcpy(access_point.ap.ssid, network->access_point_ssid, ssid_length);
access_point.ap.ssid_len = ssid_length;
access_point.ap.channel = 1U;
access_point.ap.max_connection = 4U;
access_point.ap.authmode = WIFI_AUTH_OPEN;
access_point.ap.pmf_cfg.required = false;
result = esp_wifi_set_mode(WIFI_MODE_APSTA);
if (result == ESP_OK) {
result = esp_wifi_set_config(WIFI_IF_AP, &access_point);
}
if (result == ESP_OK) {
result = esp_wifi_start();
}
if (result == ESP_OK) {
ESP_LOGI(TAG, "Fallback access point ready: SSID=%s, URL=http://192.168.4.1/provision",
network->access_point_ssid);
} else {
set_state(network, MAZE32_NETWORK_ERROR);
}
return result;
}
static esp_err_t connect_station(maze32_network_t *network,
const maze32_wifi_credentials_t *credentials)
{
esp_err_t result = stop_wifi();
if (result != ESP_OK) {
return result;
}
wifi_config_t station = {0};
memcpy(station.sta.ssid, credentials->ssid, strlen(credentials->ssid));
memcpy(station.sta.password, credentials->password, strlen(credentials->password));
station.sta.threshold.authmode = WIFI_AUTH_OPEN;
station.sta.sae_pwe_h2e = WPA3_SAE_PWE_BOTH;
network->retry_count = 0U;
xEventGroupClearBits(network->events, MAZE32_CONNECTED_BIT | MAZE32_FAILED_BIT);
set_state(network, MAZE32_NETWORK_STATION_CONNECTING);
ESP_LOGI(TAG, "Connecting to configured station SSID: %s", credentials->ssid);
result = esp_wifi_set_mode(WIFI_MODE_STA);
if (result == ESP_OK) {
result = esp_wifi_set_config(WIFI_IF_STA, &station);
}
if (result == ESP_OK) {
result = esp_wifi_start();
}
if (result != ESP_OK) {
ESP_LOGE(TAG, "Station startup failed: %s", esp_err_to_name(result));
return start_provisioning(network);
}
EventBits_t bits = xEventGroupWaitBits(
network->events,
MAZE32_CONNECTED_BIT | MAZE32_FAILED_BIT,
pdTRUE,
pdFALSE,
pdMS_TO_TICKS(MAZE32_STATION_TIMEOUT_MS));
if ((bits & MAZE32_CONNECTED_BIT) != 0U) {
set_state(network, MAZE32_NETWORK_STATION_CONNECTED);
char address[16];
if (maze32_network_ip_address(network, address, sizeof(address)) == ESP_OK) {
ESP_LOGI(TAG, "Station connected: IP=%s", address);
}
return ESP_OK;
}
ESP_LOGW(TAG, "Station connection timed out or exhausted retries; enabling fallback");
return start_provisioning(network);
}
static int save_credentials(const maze32_wifi_credentials_t *credentials, void *context)
{
(void)context;
return maze32_config_store_save(credentials) == ESP_OK ? 0 : -1;
}
static int clear_credentials(void *context)
{
(void)context;
return maze32_config_store_clear() == ESP_OK ? 0 : -1;
}
esp_err_t maze32_network_init(maze32_network_t *network,
maze32_reconfiguration_guard_fn guard,
void *guard_context)
{
if (network == NULL || guard == NULL || active_network != NULL) {
return ESP_ERR_INVALID_ARG;
}
memset(network, 0, sizeof(*network));
network->state = MAZE32_NETWORK_BOOTING;
network->reconfiguration_guard = guard;
network->guard_context = guard_context;
network->events = xEventGroupCreate();
network->operation_mutex = xSemaphoreCreateMutex();
if (network->events == NULL || network->operation_mutex == NULL) {
return ESP_ERR_NO_MEM;
}
uint8_t mac[6];
esp_err_t result = esp_read_mac(mac, ESP_MAC_WIFI_SOFTAP);
if (result != ESP_OK) {
return result;
}
snprintf(network->access_point_ssid, sizeof(network->access_point_ssid),
"maze32-%02X%02X", mac[4], mac[5]);
wifi_init_config_t wifi_configuration = WIFI_INIT_CONFIG_DEFAULT();
result = esp_wifi_init(&wifi_configuration);
if (result != ESP_OK) {
return result;
}
network->station_netif = esp_netif_create_default_wifi_sta();
network->access_point_netif = esp_netif_create_default_wifi_ap();
if (network->station_netif == NULL || network->access_point_netif == NULL) {
return ESP_ERR_NO_MEM;
}
result = esp_event_handler_register(WIFI_EVENT, ESP_EVENT_ANY_ID, network_event, network);
if (result == ESP_OK) {
result = esp_event_handler_register(IP_EVENT, IP_EVENT_STA_GOT_IP, network_event, network);
}
if (result == ESP_OK) {
active_network = network;
}
return result;
}
esp_err_t maze32_network_start(maze32_network_t *network)
{
if (network == NULL || xSemaphoreTake(network->operation_mutex, portMAX_DELAY) != pdTRUE) {
return ESP_ERR_INVALID_ARG;
}
maze32_wifi_credentials_t credentials = {0};
esp_err_t result = maze32_config_store_load(&credentials);
if (result == ESP_OK) {
result = connect_station(network, &credentials);
} else if (result == ESP_ERR_NOT_FOUND || result == ESP_ERR_INVALID_VERSION ||
result == ESP_ERR_INVALID_STATE) {
ESP_LOGI(TAG, "No usable saved station configuration; enabling fallback");
result = start_provisioning(network);
}
xSemaphoreGive(network->operation_mutex);
return result;
}
esp_err_t maze32_network_apply_credentials(maze32_network_t *network,
const maze32_wifi_credentials_t *credentials)
{
if (network == NULL) {
return ESP_ERR_INVALID_ARG;
}
int result = maze32_wifi_config_apply(credentials,
network->reconfiguration_guard,
save_credentials,
network->guard_context);
if (result == -1) {
return ESP_ERR_INVALID_ARG;
}
if (result == -2) {
return ESP_ERR_INVALID_STATE;
}
return result == 0 ? ESP_OK : ESP_FAIL;
}
esp_err_t maze32_network_reset_credentials(maze32_network_t *network)
{
if (network == NULL) {
return ESP_ERR_INVALID_ARG;
}
int result = maze32_wifi_config_reset(network->reconfiguration_guard,
clear_credentials,
network->guard_context);
if (result == -2) {
return ESP_ERR_INVALID_STATE;
}
return result == 0 ? ESP_OK : ESP_FAIL;
}
void maze32_network_retry_saved_credentials(void *context)
{
maze32_network_t *network = context;
if (network == NULL || xSemaphoreTake(network->operation_mutex, portMAX_DELAY) != pdTRUE) {
return;
}
maze32_wifi_credentials_t credentials = {0};
esp_err_t result = maze32_config_store_load(&credentials);
if (result == ESP_OK) {
result = connect_station(network, &credentials);
}
if (result != ESP_OK) {
ESP_LOGE(TAG, "Saved station retry failed: %s", esp_err_to_name(result));
start_provisioning(network);
}
xSemaphoreGive(network->operation_mutex);
}
void maze32_network_return_to_provisioning(void *context)
{
maze32_network_t *network = context;
if (network == NULL || xSemaphoreTake(network->operation_mutex, portMAX_DELAY) != pdTRUE) {
return;
}
esp_err_t result = start_provisioning(network);
if (result != ESP_OK) {
ESP_LOGE(TAG, "Failed to restore provisioning access point: %s", esp_err_to_name(result));
}
xSemaphoreGive(network->operation_mutex);
}
esp_err_t maze32_network_scan(maze32_network_t *network,
maze32_wifi_scan_result_t *results,
size_t capacity,
size_t *count)
{
if (network == NULL || results == NULL || count == NULL || capacity == 0U ||
capacity > MAZE32_WIFI_SCAN_LIMIT ||
xSemaphoreTake(network->operation_mutex, pdMS_TO_TICKS(1000U)) != pdTRUE) {
return ESP_ERR_INVALID_ARG;
}
if (network->state != MAZE32_NETWORK_PROVISIONING) {
xSemaphoreGive(network->operation_mutex);
return ESP_ERR_INVALID_STATE;
}
wifi_scan_config_t scan_configuration = {0};
esp_err_t result = esp_wifi_scan_start(&scan_configuration, true);
uint16_t found = (uint16_t)capacity;
wifi_ap_record_t records[MAZE32_WIFI_SCAN_LIMIT];
if (result == ESP_OK) {
result = esp_wifi_scan_get_ap_records(&found, records);
}
if (result == ESP_OK) {
*count = found;
for (size_t index = 0U; index < found; ++index) {
memcpy(results[index].ssid, records[index].ssid, MAZE32_WIFI_SSID_MAX_BYTES);
results[index].ssid[MAZE32_WIFI_SSID_MAX_BYTES] = '\0';
results[index].rssi = records[index].rssi;
results[index].auth_mode = (uint8_t)records[index].authmode;
}
}
xSemaphoreGive(network->operation_mutex);
return result;
}
maze32_network_state_t maze32_network_state(const maze32_network_t *network)
{
return network == NULL ? MAZE32_NETWORK_ERROR : network->state;
}
const char *maze32_network_state_name(maze32_network_state_t state)
{
switch (state) {
case MAZE32_NETWORK_BOOTING: return "booting";
case MAZE32_NETWORK_STATION_CONNECTING: return "station_connecting";
case MAZE32_NETWORK_STATION_CONNECTED: return "station_connected";
case MAZE32_NETWORK_PROVISIONING: return "provisioning";
case MAZE32_NETWORK_ERROR: return "error";
default: return "unknown";
}
}
const char *maze32_network_access_point_ssid(const maze32_network_t *network)
{
return network == NULL ? "" : network->access_point_ssid;
}
esp_err_t maze32_network_ip_address(const maze32_network_t *network,
char *output,
size_t output_size)
{
if (network == NULL || output == NULL || output_size < 16U ||
network->station_netif == NULL) {
return ESP_ERR_INVALID_ARG;
}
esp_netif_ip_info_t information;
esp_err_t result = esp_netif_get_ip_info(network->station_netif, &information);
if (result == ESP_OK) {
snprintf(output, output_size, IPSTR, IP2STR(&information.ip));
}
return result;
}
@@ -0,0 +1,261 @@
#include "maze32_network.h"
#include <stdio.h>
#include <string.h>
#include "esp_log.h"
#include "maze32_provisioning_protocol.h"
#define MAZE32_PROVISION_BODY_LIMIT 256U
#define MAZE32_PROVISION_RESPONSE_LIMIT 2048U
static const char *TAG = "maze32_provision";
static const char *PROVISION_PATH = "/littlefs/provision.html.gz";
static int append_json_string(char *output,
size_t capacity,
size_t *length,
const char *value)
{
if (*length + 1U >= capacity) {
return 0;
}
output[(*length)++] = '"';
for (const unsigned char *cursor = (const unsigned char *)value;
*cursor != '\0'; ++cursor) {
const char *escape = NULL;
if (*cursor == '"') {
escape = "\\\"";
} else if (*cursor == '\\') {
escape = "\\\\";
} else if (*cursor == '\b') {
escape = "\\b";
} else if (*cursor == '\f') {
escape = "\\f";
} else if (*cursor == '\n') {
escape = "\\n";
} else if (*cursor == '\r') {
escape = "\\r";
} else if (*cursor == '\t') {
escape = "\\t";
}
if (escape != NULL) {
if (*length + 2U >= capacity) {
return 0;
}
output[(*length)++] = escape[0];
output[(*length)++] = escape[1];
} else {
if (*cursor < 0x20U) {
if (*length + 6U >= capacity) {
return 0;
}
int written = snprintf(output + *length, capacity - *length,
"\\u%04x", *cursor);
if (written != 6) {
return 0;
}
*length += 6U;
continue;
}
if (*length + 1U >= capacity) {
return 0;
}
output[(*length)++] = (char)*cursor;
}
}
if (*length + 1U >= capacity) {
return 0;
}
output[(*length)++] = '"';
output[*length] = '\0';
return 1;
}
static esp_err_t send_json(httpd_req_t *request, const char *status, const char *body)
{
httpd_resp_set_status(request, status);
httpd_resp_set_type(request, "application/json");
httpd_resp_set_hdr(request, "Cache-Control", "no-store");
return httpd_resp_sendstr(request, body);
}
static esp_err_t send_provision_page(httpd_req_t *request)
{
FILE *file = fopen(PROVISION_PATH, "rb");
if (file == NULL) {
ESP_LOGE(TAG, "Provisioning asset is unavailable: %s", PROVISION_PATH);
return httpd_resp_send_err(request, HTTPD_500_INTERNAL_SERVER_ERROR,
"Provisioning application is unavailable");
}
httpd_resp_set_type(request, "text/html; charset=utf-8");
httpd_resp_set_hdr(request, "Content-Encoding", "gzip");
httpd_resp_set_hdr(request, "Cache-Control", "no-cache");
char buffer[1024];
esp_err_t result = ESP_OK;
size_t bytes_read;
while ((bytes_read = fread(buffer, 1, sizeof(buffer), file)) > 0U) {
result = httpd_resp_send_chunk(request, buffer, bytes_read);
if (result != ESP_OK) {
break;
}
}
if (ferror(file) != 0) {
result = ESP_FAIL;
}
fclose(file);
return result == ESP_OK ? httpd_resp_send_chunk(request, NULL, 0) : result;
}
static esp_err_t send_status(httpd_req_t *request)
{
maze32_network_t *network = request->user_ctx;
char address[16] = "";
if (maze32_network_state(network) == MAZE32_NETWORK_STATION_CONNECTED) {
maze32_network_ip_address(network, address, sizeof(address));
}
char response[256];
int length = snprintf(response, sizeof(response),
"{\"state\":\"%s\",\"accessPointSsid\":\"%s\",\"ip\":\"%s\"}",
maze32_network_state_name(maze32_network_state(network)),
maze32_network_access_point_ssid(network), address);
if (length < 0 || (size_t)length >= sizeof(response)) {
return ESP_FAIL;
}
return send_json(request, "200 OK", response);
}
static esp_err_t send_scan(httpd_req_t *request)
{
maze32_network_t *network = request->user_ctx;
maze32_wifi_scan_result_t results[MAZE32_WIFI_SCAN_LIMIT];
size_t count = 0U;
esp_err_t result = maze32_network_scan(network, results, MAZE32_WIFI_SCAN_LIMIT, &count);
if (result != ESP_OK) {
return send_json(request, "503 Service Unavailable",
"{\"error\":\"Сканирование сейчас недоступно\"}");
}
char response[MAZE32_PROVISION_RESPONSE_LIMIT] = "{\"networks\":[";
size_t length = strlen(response);
for (size_t index = 0U; index < count; ++index) {
const char *prefix = index == 0U ? "{" : ",{";
int written = snprintf(response + length, sizeof(response) - length,
"%s\"ssid\":", prefix);
if (written < 0 || (size_t)written >= sizeof(response) - length) {
return ESP_ERR_NO_MEM;
}
length += (size_t)written;
if (!append_json_string(response, sizeof(response), &length, results[index].ssid)) {
return ESP_ERR_NO_MEM;
}
written = snprintf(response + length, sizeof(response) - length,
",\"rssi\":%d,\"secure\":%s}",
results[index].rssi,
results[index].auth_mode == 0U ? "false" : "true");
if (written < 0 || (size_t)written >= sizeof(response) - length) {
return ESP_ERR_NO_MEM;
}
length += (size_t)written;
}
int written = snprintf(response + length, sizeof(response) - length, "]}");
if (written < 0 || (size_t)written >= sizeof(response) - length) {
return ESP_ERR_NO_MEM;
}
return send_json(request, "200 OK", response);
}
static esp_err_t read_credentials(httpd_req_t *request,
maze32_wifi_credentials_t *credentials)
{
if (request->content_len == 0U || request->content_len > MAZE32_PROVISION_BODY_LIMIT) {
return ESP_ERR_INVALID_SIZE;
}
char body[MAZE32_PROVISION_BODY_LIMIT + 1U];
size_t received = 0U;
while (received < request->content_len) {
int current = httpd_req_recv(request, body + received, request->content_len - received);
if (current <= 0) {
return ESP_FAIL;
}
received += (size_t)current;
}
body[received] = '\0';
if (!maze32_provisioning_parse_credentials(body, received, credentials)) {
return ESP_ERR_INVALID_ARG;
}
return ESP_OK;
}
static esp_err_t apply_credentials(httpd_req_t *request)
{
maze32_network_t *network = request->user_ctx;
maze32_wifi_credentials_t credentials;
esp_err_t result = read_credentials(request, &credentials);
if (result != ESP_OK) {
return send_json(request, "400 Bad Request",
"{\"error\":\"Проверьте имя сети и пароль\"}");
}
result = maze32_network_apply_credentials(network, &credentials);
memset(&credentials, 0, sizeof(credentials));
if (result == ESP_ERR_INVALID_STATE) {
return send_json(request, "409 Conflict",
"{\"error\":\"Сначала завершите текущую игру\"}");
}
if (result != ESP_OK) {
return send_json(request, "500 Internal Server Error",
"{\"error\":\"Не удалось сохранить настройки\"}");
}
result = httpd_queue_work(network->server, maze32_network_retry_saved_credentials, network);
if (result != ESP_OK) {
return send_json(request, "500 Internal Server Error",
"{\"error\":\"Настройки сохранены; перезагрузите устройство\"}");
}
return send_json(request, "202 Accepted",
"{\"status\":\"saved\",\"message\":\"Подключаемся к сети…\"}");
}
static esp_err_t reset_credentials(httpd_req_t *request)
{
maze32_network_t *network = request->user_ctx;
esp_err_t result = maze32_network_reset_credentials(network);
if (result == ESP_ERR_INVALID_STATE) {
return send_json(request, "409 Conflict",
"{\"error\":\"Сначала завершите текущую игру\"}");
}
if (result != ESP_OK) {
return send_json(request, "500 Internal Server Error",
"{\"error\":\"Не удалось сбросить настройки\"}");
}
result = httpd_queue_work(network->server, maze32_network_return_to_provisioning, network);
if (result != ESP_OK) {
return ESP_FAIL;
}
return send_json(request, "202 Accepted",
"{\"status\":\"reset\",\"message\":\"Настройки сети удалены\"}");
}
esp_err_t maze32_provisioning_register(httpd_handle_t server, maze32_network_t *network)
{
if (server == NULL || network == NULL) {
return ESP_ERR_INVALID_ARG;
}
network->server = server;
const httpd_uri_t routes[] = {
{.uri = "/provision", .method = HTTP_GET, .handler = send_provision_page, .user_ctx = network},
{.uri = "/api/wifi/status", .method = HTTP_GET, .handler = send_status, .user_ctx = network},
{.uri = "/api/wifi/scan", .method = HTTP_GET, .handler = send_scan, .user_ctx = network},
{.uri = "/api/wifi/config", .method = HTTP_POST, .handler = apply_credentials, .user_ctx = network},
{.uri = "/api/wifi/reset", .method = HTTP_POST, .handler = reset_credentials, .user_ctx = network},
};
for (size_t index = 0U; index < sizeof(routes) / sizeof(routes[0]); ++index) {
esp_err_t result = httpd_register_uri_handler(server, &routes[index]);
if (result != ESP_OK) {
return result;
}
}
ESP_LOGI(TAG, "Provisioning endpoints ready");
return ESP_OK;
}
@@ -0,0 +1,113 @@
#include "maze32_provisioning_protocol.h"
#include <ctype.h>
#include <string.h>
static const char *skip_space(const char *cursor, const char *end)
{
while (cursor < end && isspace((unsigned char)*cursor)) {
++cursor;
}
return cursor;
}
static int parse_json_string(const char **cursor_pointer,
const char *end,
char *output,
size_t capacity)
{
const char *cursor = *cursor_pointer;
size_t length = 0U;
if (cursor >= end || *cursor++ != '"') {
return 0;
}
while (cursor < end && *cursor != '"') {
unsigned char value = (unsigned char)*cursor++;
if (value == '\\') {
if (cursor >= end) {
return 0;
}
char escaped = *cursor++;
if (escaped == '"' || escaped == '\\' || escaped == '/') {
value = (unsigned char)escaped;
} else if (escaped == 'b') {
value = '\b';
} else if (escaped == 'f') {
value = '\f';
} else if (escaped == 'n') {
value = '\n';
} else if (escaped == 'r') {
value = '\r';
} else if (escaped == 't') {
value = '\t';
} else {
return 0;
}
}
if (value < 0x20U || length + 1U >= capacity) {
return 0;
}
output[length++] = (char)value;
}
if (cursor >= end || *cursor++ != '"') {
return 0;
}
output[length] = '\0';
*cursor_pointer = cursor;
return 1;
}
int maze32_provisioning_parse_credentials(const char *body,
size_t length,
maze32_wifi_credentials_t *credentials)
{
if (body == NULL || credentials == NULL) {
return 0;
}
const char *cursor = body;
const char *end = body + length;
int have_ssid = 0;
int have_password = 0;
memset(credentials, 0, sizeof(*credentials));
cursor = skip_space(cursor, end);
if (cursor >= end || *cursor++ != '{') {
return 0;
}
for (int field_index = 0; field_index < 2; ++field_index) {
char key[12];
char value[MAZE32_WIFI_PASSWORD_MAX_BYTES + 1U];
cursor = skip_space(cursor, end);
if (!parse_json_string(&cursor, end, key, sizeof(key))) {
return 0;
}
cursor = skip_space(cursor, end);
if (cursor >= end || *cursor++ != ':') {
return 0;
}
cursor = skip_space(cursor, end);
if (!parse_json_string(&cursor, end, value, sizeof(value))) {
return 0;
}
if (strcmp(key, "ssid") == 0 && !have_ssid &&
strlen(value) <= MAZE32_WIFI_SSID_MAX_BYTES) {
memcpy(credentials->ssid, value, strlen(value) + 1U);
have_ssid = 1;
} else if (strcmp(key, "password") == 0 && !have_password) {
memcpy(credentials->password, value, strlen(value) + 1U);
have_password = 1;
} else {
return 0;
}
cursor = skip_space(cursor, end);
if (field_index == 0 && (cursor >= end || *cursor++ != ',')) {
return 0;
}
}
cursor = skip_space(cursor, end);
if (cursor >= end || *cursor++ != '}') {
return 0;
}
cursor = skip_space(cursor, end);
return cursor == end && have_ssid && have_password &&
maze32_wifi_credentials_validate(credentials) == MAZE32_WIFI_CONFIG_VALID;
}