feat: implement Wi-Fi provisioning and persistent configuration
This commit is contained in:
@@ -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;
|
||||
}
|
||||
Reference in New Issue
Block a user