feat: implement the open fallback access point and captive portal
This commit is contained in:
+178
-15
@@ -20,6 +20,7 @@
|
||||
#include "freertos/portmacro.h"
|
||||
#include "http_api.h"
|
||||
#include "network_credential_store.h"
|
||||
#include "network_configuration.h"
|
||||
#include "network_state.h"
|
||||
#include "nvs_flash.h"
|
||||
#include "sync_service.h"
|
||||
@@ -34,6 +35,7 @@ enum { kHttpMaxOpenSockets = 12U, kFallbackMaxConnections = 10U, kDnsPort = 53U,
|
||||
typedef struct { bool configured; bool connected; bool fallback_active; } wifi_state_t;
|
||||
static portMUX_TYPE s_wifi_lock = portMUX_INITIALIZER_UNLOCKED;
|
||||
static portMUX_TYPE s_network_lock = portMUX_INITIALIZER_UNLOCKED;
|
||||
static portMUX_TYPE s_configuration_lock = portMUX_INITIALIZER_UNLOCKED;
|
||||
static portMUX_TYPE s_diagnostics_lock = portMUX_INITIALIZER_UNLOCKED;
|
||||
static wifi_state_t s_wifi_state = {0};
|
||||
static bool s_littlefs_mounted;
|
||||
@@ -46,10 +48,13 @@ static esp_timer_handle_t s_sync_timer;
|
||||
static esp_timer_handle_t s_bot_timer;
|
||||
static esp_timer_handle_t s_network_timer;
|
||||
static network_manager_t s_network_manager;
|
||||
static network_configuration_t s_configuration;
|
||||
static bool s_scan_pending;
|
||||
static bool s_scan_ready;
|
||||
static bool s_dns_running;
|
||||
|
||||
enum { kNetworkRequestBytes = 160U, kNetworkResponseBytes = 768U };
|
||||
|
||||
static uint32_t platform_random(void *unused) { (void)unused; return esp_random(); }
|
||||
|
||||
static void record_rejected_input(void) {
|
||||
@@ -300,6 +305,40 @@ static void disable_fallback_ap(void) {
|
||||
if (esp_wifi_set_mode(WIFI_MODE_STA) != ESP_OK) ESP_LOGW(kLogTag, "fallback access point stop failed");
|
||||
}
|
||||
|
||||
static esp_err_t configure_station_profile(const network_profile_t *profile) {
|
||||
wifi_config_t station_config = {0};
|
||||
if (profile != NULL) {
|
||||
memcpy(station_config.sta.ssid, profile->ssid, kNetworkSsidBytes);
|
||||
memcpy(station_config.sta.password, profile->password, kNetworkPasswordBytes);
|
||||
}
|
||||
return esp_wifi_set_config(WIFI_IF_STA, &station_config);
|
||||
}
|
||||
|
||||
static bool configuration_is_validating(void) {
|
||||
bool validating;
|
||||
portENTER_CRITICAL(&s_configuration_lock); validating = network_configuration_busy(&s_configuration); portEXIT_CRITICAL(&s_configuration_lock);
|
||||
return validating;
|
||||
}
|
||||
|
||||
static const char *configuration_state_name(void) {
|
||||
network_configuration_state_t state;
|
||||
portENTER_CRITICAL(&s_configuration_lock); state = s_configuration.state; portEXIT_CRITICAL(&s_configuration_lock);
|
||||
if (state == NETWORK_CONFIGURATION_VALIDATING) return "validating";
|
||||
if (state == NETWORK_CONFIGURATION_SUCCESS) return "success";
|
||||
if (state == NETWORK_CONFIGURATION_FAILED) return "failed";
|
||||
const wifi_state_t wifi = wifi_state_snapshot();
|
||||
return wifi.fallback_active ? "fallback" : wifi.connected ? "connected" : "connecting";
|
||||
}
|
||||
|
||||
static const char *configuration_message(void) {
|
||||
const char *state = configuration_state_name();
|
||||
if (strcmp(state, "validating") == 0) return "Проверяем подключение к сети…";
|
||||
if (strcmp(state, "success") == 0) return "Сеть сохранена. Подключите устройство к домашней сети: точка доступа скоро отключится.";
|
||||
if (strcmp(state, "failed") == 0) return "Не удалось подключиться. Сохранённая сеть не изменена.";
|
||||
if (strcmp(state, "fallback") == 0) return "Работает точка доступа Battleship-open.";
|
||||
return strcmp(state, "connected") == 0 ? "Подключено к сохранённой сети." : "Подключаемся к сохранённой сети…";
|
||||
}
|
||||
|
||||
static void apply_network_action(network_action_t action) {
|
||||
if (action == NETWORK_ACTION_FALLBACK) enable_fallback_ap();
|
||||
else if (action == NETWORK_ACTION_CONNECT && esp_wifi_connect() != ESP_OK) ESP_LOGW(kLogTag, "saved network connection request failed");
|
||||
@@ -315,9 +354,45 @@ static network_action_t network_event_action(bool connected) {
|
||||
|
||||
static void network_timer_callback(void *unused) {
|
||||
(void)unused;
|
||||
const uint32_t now_ms = (uint32_t)(esp_timer_get_time() / 1000U);
|
||||
network_action_t action;
|
||||
portENTER_CRITICAL(&s_network_lock); action = network_manager_tick(&s_network_manager, (uint32_t)(esp_timer_get_time() / 1000U)); portEXIT_CRITICAL(&s_network_lock);
|
||||
portENTER_CRITICAL(&s_network_lock); action = network_manager_tick(&s_network_manager, now_ms); portEXIT_CRITICAL(&s_network_lock);
|
||||
apply_network_action(action);
|
||||
bool validation_timed_out = false; bool success_notice_expired = false; network_profile_t previous = {0}; bool had_previous = false;
|
||||
portENTER_CRITICAL(&s_configuration_lock);
|
||||
validation_timed_out = network_configuration_validation_timed_out(&s_configuration, now_ms);
|
||||
success_notice_expired = network_configuration_success_notice_expired(&s_configuration, now_ms);
|
||||
if (validation_timed_out) { previous = s_configuration.previous_profile; had_previous = s_configuration.had_previous_profile; }
|
||||
if (success_notice_expired) s_configuration.state = NETWORK_CONFIGURATION_IDLE;
|
||||
portEXIT_CRITICAL(&s_configuration_lock);
|
||||
if (validation_timed_out) {
|
||||
portENTER_CRITICAL(&s_network_lock); portENTER_CRITICAL(&s_configuration_lock);
|
||||
network_configuration_finish(&s_configuration, &s_network_manager, false, now_ms);
|
||||
if (had_previous) network_manager_disconnected(&s_network_manager, now_ms);
|
||||
portEXIT_CRITICAL(&s_configuration_lock); portEXIT_CRITICAL(&s_network_lock);
|
||||
if (configure_station_profile(had_previous ? &previous : NULL) == ESP_OK && had_previous) esp_wifi_connect();
|
||||
}
|
||||
if (success_notice_expired && fallback_active()) disable_fallback_ap();
|
||||
}
|
||||
|
||||
static void validation_connected_work(void *unused) {
|
||||
(void)unused;
|
||||
if (!configuration_is_validating()) return;
|
||||
network_profile_t candidate = {0}; network_profile_t previous = {0}; bool had_previous = false;
|
||||
portENTER_CRITICAL(&s_network_lock); candidate = s_network_manager.candidate; portEXIT_CRITICAL(&s_network_lock);
|
||||
const bool persisted = network_credential_store_replace(&candidate);
|
||||
const uint32_t now_ms = (uint32_t)(esp_timer_get_time() / 1000U);
|
||||
portENTER_CRITICAL(&s_network_lock); portENTER_CRITICAL(&s_configuration_lock);
|
||||
previous = s_configuration.previous_profile; had_previous = s_configuration.had_previous_profile;
|
||||
network_configuration_finish(&s_configuration, &s_network_manager, persisted, now_ms);
|
||||
if (persisted) network_manager_connected(&s_network_manager);
|
||||
else if (had_previous) network_manager_disconnected(&s_network_manager, now_ms);
|
||||
portEXIT_CRITICAL(&s_configuration_lock); portEXIT_CRITICAL(&s_network_lock);
|
||||
if (!persisted) {
|
||||
ESP_LOGW(kLogTag, "network credential storage failed");
|
||||
esp_wifi_disconnect(); configure_station_profile(had_previous ? &previous : NULL);
|
||||
if (had_previous) esp_wifi_connect();
|
||||
}
|
||||
}
|
||||
|
||||
static void wifi_event_handler(void *argument, esp_event_base_t event_base, int32_t event_id, void *event_data) {
|
||||
@@ -325,12 +400,15 @@ static void wifi_event_handler(void *argument, esp_event_base_t event_base, int3
|
||||
if (event_base == WIFI_EVENT && event_id == WIFI_EVENT_STA_START) apply_network_action(network_event_action(false));
|
||||
else if (event_base == WIFI_EVENT && event_id == WIFI_EVENT_STA_DISCONNECTED) {
|
||||
portENTER_CRITICAL(&s_wifi_lock); s_wifi_state.connected = false; portEXIT_CRITICAL(&s_wifi_lock);
|
||||
apply_network_action(network_event_action(false));
|
||||
if (!configuration_is_validating()) apply_network_action(network_event_action(false));
|
||||
} else if (event_base == WIFI_EVENT && event_id == WIFI_EVENT_SCAN_DONE) { s_scan_pending = false; s_scan_ready = true; }
|
||||
else if (event_base == IP_EVENT && event_id == IP_EVENT_STA_GOT_IP) {
|
||||
network_event_action(true);
|
||||
bool validating = configuration_is_validating();
|
||||
if (validating) {
|
||||
if (s_server != NULL) httpd_queue_work(s_server, validation_connected_work, NULL);
|
||||
} else network_event_action(true);
|
||||
portENTER_CRITICAL(&s_wifi_lock); s_wifi_state.connected = true; portEXIT_CRITICAL(&s_wifi_lock);
|
||||
if (fallback_active()) disable_fallback_ap();
|
||||
if (!validating && fallback_active()) disable_fallback_ap();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -414,26 +492,106 @@ static esp_err_t setup_handler(httpd_req_t *request) {
|
||||
static size_t append_scan_name(char *output, size_t capacity, size_t length, const uint8_t *ssid) {
|
||||
if (length >= capacity) return length;
|
||||
for (size_t index = 0U; index < 32U && ssid[index] != '\0' && length + 1U < capacity; ++index) {
|
||||
const uint8_t value = ssid[index]; output[length++] = value >= 32U && value <= 126U ? (char)value : '?';
|
||||
const uint8_t value = ssid[index]; output[length++] = value >= 32U && value <= 126U && value != '"' && value != '\\' ? (char)value : '?';
|
||||
}
|
||||
output[length++] = '\n'; output[length] = '\0'; return length;
|
||||
}
|
||||
|
||||
static esp_err_t setup_scan_handler(httpd_req_t *request) {
|
||||
if (!fallback_active()) { httpd_resp_set_status(request, "409 Conflict"); return httpd_resp_send(request, "fallback access point is inactive", HTTPD_RESP_USE_STRLEN); }
|
||||
return httpd_resp_send_err(request, HTTPD_404_NOT_FOUND, "use /api/network/scan");
|
||||
}
|
||||
|
||||
static void network_response(httpd_req_t *request, uint16_t status, const char *body) {
|
||||
httpd_resp_set_status(request, status == 200U ? "200 OK" : status == 400U ? "400 Bad Request" : status == 409U ? "409 Conflict" : "503 Service Unavailable");
|
||||
httpd_resp_set_type(request, "application/json"); httpd_resp_set_hdr(request, "Cache-Control", "no-store");
|
||||
httpd_resp_send(request, body, HTTPD_RESP_USE_STRLEN);
|
||||
}
|
||||
|
||||
static void network_json_skip(const char **text) { while (**text == ' ' || **text == '\n' || **text == '\r' || **text == '\t') ++*text; }
|
||||
static bool network_json_string(const char **text, char *output, size_t output_size) {
|
||||
network_json_skip(text); if (**text != '"' || output_size == 0U) return false; ++*text; size_t length = 0U;
|
||||
while (**text != '\0' && **text != '"') { const unsigned char value = (unsigned char)*(*text)++; if (value < 0x20U || value == '\\' || length + 1U >= output_size) return false; output[length++] = (char)value; }
|
||||
if (**text != '"') return false;
|
||||
++*text; output[length] = '\0'; return true;
|
||||
}
|
||||
static bool parse_network_profile(const char *body, network_profile_t *profile) {
|
||||
if (body == NULL || profile == NULL) return false;
|
||||
const char *text = body; char key[16] = {0}; bool ssid = false; bool password = false; memset(profile, 0, sizeof(*profile));
|
||||
network_json_skip(&text); if (*text++ != '{') return false;
|
||||
for (;;) {
|
||||
network_json_skip(&text); if (*text == '}') { ++text; break; }
|
||||
if ((ssid || password) && *text++ != ',') return false;
|
||||
if (!network_json_string(&text, key, sizeof(key))) return false;
|
||||
network_json_skip(&text); if (*text++ != ':') return false;
|
||||
if (strcmp(key, "ssid") == 0 && !ssid) { if (!network_json_string(&text, profile->ssid, sizeof(profile->ssid))) return false; ssid = true; }
|
||||
else if (strcmp(key, "password") == 0 && !password) { if (!network_json_string(&text, profile->password, sizeof(profile->password))) return false; password = true; }
|
||||
else return false;
|
||||
}
|
||||
network_json_skip(&text); return *text == '\0' && ssid && password && network_profile_valid(profile);
|
||||
}
|
||||
|
||||
static esp_err_t network_status_handler(httpd_req_t *request) {
|
||||
bool saved;
|
||||
portENTER_CRITICAL(&s_network_lock); saved = s_network_manager.has_profile; portEXIT_CRITICAL(&s_network_lock);
|
||||
char body[320]; snprintf(body, sizeof(body), "{\"ok\":true,\"state\":\"%s\",\"hasSavedNetwork\":%s,\"message\":\"%s\"}", configuration_state_name(), saved ? "true" : "false", configuration_message());
|
||||
network_response(request, 200U, body); return ESP_OK;
|
||||
}
|
||||
|
||||
static esp_err_t network_scan_handler(httpd_req_t *request) {
|
||||
if (configuration_is_validating()) { network_response(request, 409U, "{\"ok\":false,\"code\":\"NETWORK_BUSY\",\"message\":\"Идёт проверка подключения.\"}"); return ESP_OK; }
|
||||
if (!s_scan_pending && !s_scan_ready) {
|
||||
const esp_err_t result = esp_wifi_scan_start(NULL, false);
|
||||
if (result != ESP_OK) { httpd_resp_set_status(request, "503 Service Unavailable"); return httpd_resp_send(request, "scan unavailable", HTTPD_RESP_USE_STRLEN); }
|
||||
if (esp_wifi_scan_start(NULL, false) != ESP_OK) { network_response(request, 503U, "{\"ok\":false,\"code\":\"SCAN_UNAVAILABLE\",\"message\":\"Поиск сетей сейчас недоступен.\"}"); return ESP_OK; }
|
||||
s_scan_pending = true;
|
||||
}
|
||||
httpd_resp_set_type(request, "text/plain; charset=utf-8"); httpd_resp_set_hdr(request, "Cache-Control", "no-store");
|
||||
if (s_scan_pending) return httpd_resp_send(request, "scanning\n", HTTPD_RESP_USE_STRLEN);
|
||||
if (s_scan_pending) { network_response(request, 200U, "{\"ok\":true,\"state\":\"scanning\",\"networks\":[]}"); return ESP_OK; }
|
||||
wifi_ap_record_t records[kScanResultLimit] = {0}; uint16_t count = kScanResultLimit;
|
||||
if (esp_wifi_scan_get_ap_records(&count, records) != ESP_OK) { httpd_resp_set_status(request, "503 Service Unavailable"); return httpd_resp_send(request, "scan unavailable", HTTPD_RESP_USE_STRLEN); }
|
||||
if (esp_wifi_scan_get_ap_records(&count, records) != ESP_OK) { network_response(request, 503U, "{\"ok\":false,\"code\":\"SCAN_UNAVAILABLE\",\"message\":\"Поиск сетей сейчас недоступен.\"}"); return ESP_OK; }
|
||||
s_scan_ready = false;
|
||||
char names[(kNetworkSsidBytes + 1U) * kScanResultLimit + 1U] = {0}; size_t length = 0U;
|
||||
for (uint16_t index = 0U; index < count; ++index) length = append_scan_name(names, sizeof(names), length, records[index].ssid);
|
||||
return httpd_resp_send(request, names, length);
|
||||
char body[kNetworkResponseBytes] = "{\"ok\":true,\"state\":\"ready\",\"networks\":["; size_t length = strlen(body);
|
||||
for (uint16_t index = 0U; index < count && length + kNetworkSsidBytes + 4U < sizeof(body); ++index) {
|
||||
char name[kNetworkSsidBytes + 1U] = {0}; append_scan_name(name, sizeof(name), 0U, records[index].ssid); name[strcspn(name, "\n")] = '\0';
|
||||
length += (size_t)snprintf(body + length, sizeof(body) - length, "%s\"%s\"", index == 0U ? "" : ",", name);
|
||||
}
|
||||
snprintf(body + length, sizeof(body) - length, "]}"); network_response(request, 200U, body); return ESP_OK;
|
||||
}
|
||||
|
||||
static esp_err_t receive_network_profile(httpd_req_t *request, network_profile_t *profile) {
|
||||
char body[kNetworkRequestBytes + 1U] = {0};
|
||||
if (request->content_len <= 0 || request->content_len > kNetworkRequestBytes || !request_has_json_content_type(request)) { record_rejected_input(); network_response(request, 400U, "{\"ok\":false,\"code\":\"MALFORMED_NETWORK\",\"message\":\"Укажите название сети и пароль.\"}"); return ESP_FAIL; }
|
||||
size_t length = 0U;
|
||||
while (length < (size_t)request->content_len) { const int received = httpd_req_recv(request, body + length, request->content_len - length); if (received <= 0) { record_rejected_input(); network_response(request, 400U, "{\"ok\":false,\"code\":\"MALFORMED_NETWORK\",\"message\":\"Некорректный запрос.\"}"); return ESP_FAIL; } length += (size_t)received; }
|
||||
body[length] = '\0';
|
||||
if (!parse_network_profile(body, profile)) { record_rejected_input(); network_response(request, 400U, "{\"ok\":false,\"code\":\"INVALID_NETWORK\",\"message\":\"Проверьте название сети и пароль.\"}"); return ESP_FAIL; }
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
static esp_err_t network_validate_handler(httpd_req_t *request) {
|
||||
network_profile_t candidate = {0}; if (receive_network_profile(request, &candidate) != ESP_OK) return ESP_OK;
|
||||
if (s_scan_pending || configuration_is_validating()) { network_response(request, 409U, "{\"ok\":false,\"code\":\"NETWORK_BUSY\",\"message\":\"Другая операция уже выполняется.\"}"); return ESP_OK; }
|
||||
const uint32_t now_ms = (uint32_t)(esp_timer_get_time() / 1000U); bool started;
|
||||
portENTER_CRITICAL(&s_network_lock); portENTER_CRITICAL(&s_configuration_lock);
|
||||
started = network_configuration_begin(&s_configuration, &s_network_manager, &candidate, now_ms);
|
||||
portEXIT_CRITICAL(&s_configuration_lock); portEXIT_CRITICAL(&s_network_lock);
|
||||
if (!started) { network_response(request, 409U, "{\"ok\":false,\"code\":\"NETWORK_BUSY\",\"message\":\"Другая операция уже выполняется.\"}"); return ESP_OK; }
|
||||
enable_fallback_ap(); esp_wifi_disconnect();
|
||||
if (configure_station_profile(&candidate) != ESP_OK || esp_wifi_connect() != ESP_OK) {
|
||||
portENTER_CRITICAL(&s_network_lock); portENTER_CRITICAL(&s_configuration_lock);
|
||||
network_configuration_finish(&s_configuration, &s_network_manager, false, now_ms);
|
||||
portEXIT_CRITICAL(&s_configuration_lock); portEXIT_CRITICAL(&s_network_lock);
|
||||
network_response(request, 503U, "{\"ok\":false,\"code\":\"CONNECT_UNAVAILABLE\",\"message\":\"Не удалось начать проверку сети.\"}"); return ESP_OK;
|
||||
}
|
||||
network_response(request, 200U, "{\"ok\":true,\"state\":\"validating\",\"message\":\"Проверяем подключение. Оставайтесь в Battleship-open до подтверждения.\"}"); return ESP_OK;
|
||||
}
|
||||
|
||||
static esp_err_t network_delete_handler(httpd_req_t *request) {
|
||||
if (request->content_len != 0 || configuration_is_validating() || s_scan_pending) { network_response(request, 409U, "{\"ok\":false,\"code\":\"NETWORK_BUSY\",\"message\":\"Операция сейчас недоступна.\"}"); return ESP_OK; }
|
||||
if (!network_credential_store_delete()) { network_response(request, 503U, "{\"ok\":false,\"code\":\"STORAGE_UNAVAILABLE\",\"message\":\"Не удалось удалить сохранённую сеть.\"}"); return ESP_OK; }
|
||||
const uint32_t now_ms = (uint32_t)(esp_timer_get_time() / 1000U);
|
||||
portENTER_CRITICAL(&s_network_lock); network_manager_init(&s_network_manager, NULL, now_ms); portEXIT_CRITICAL(&s_network_lock);
|
||||
portENTER_CRITICAL(&s_configuration_lock); network_configuration_init(&s_configuration); portEXIT_CRITICAL(&s_configuration_lock);
|
||||
portENTER_CRITICAL(&s_wifi_lock); s_wifi_state.configured = false; s_wifi_state.connected = false; portEXIT_CRITICAL(&s_wifi_lock);
|
||||
esp_wifi_disconnect(); configure_station_profile(NULL); enable_fallback_ap();
|
||||
network_response(request, 200U, "{\"ok\":true,\"state\":\"fallback\",\"message\":\"Сохранённая сеть удалена. Battleship-open остаётся доступной.\"}"); return ESP_OK;
|
||||
}
|
||||
|
||||
static esp_err_t root_handler(httpd_req_t *request) {
|
||||
@@ -458,12 +616,16 @@ static esp_err_t static_file_handler(httpd_req_t *request) {
|
||||
|
||||
static esp_err_t start_http_server(void) {
|
||||
httpd_config_t config = HTTPD_DEFAULT_CONFIG();
|
||||
config.max_uri_handlers = 21U; config.max_open_sockets = kHttpMaxOpenSockets; config.uri_match_fn = httpd_uri_match_wildcard; config.lru_purge_enable = true;
|
||||
config.max_uri_handlers = 25U; config.max_open_sockets = kHttpMaxOpenSockets; config.uri_match_fn = httpd_uri_match_wildcard; config.lru_purge_enable = true;
|
||||
ESP_RETURN_ON_ERROR(httpd_start(&s_server, &config), kLogTag, "http server start failed");
|
||||
const httpd_uri_t routes[] = {
|
||||
{.uri = "/", .method = HTTP_GET, .handler = root_handler},
|
||||
{.uri = "/setup", .method = HTTP_GET, .handler = setup_handler},
|
||||
{.uri = "/setup/scan", .method = HTTP_GET, .handler = setup_scan_handler},
|
||||
{.uri = "/api/network/status", .method = HTTP_GET, .handler = network_status_handler},
|
||||
{.uri = "/api/network/scan", .method = HTTP_GET, .handler = network_scan_handler},
|
||||
{.uri = "/api/network/validate", .method = HTTP_POST, .handler = network_validate_handler},
|
||||
{.uri = "/api/network/delete", .method = HTTP_POST, .handler = network_delete_handler},
|
||||
{.uri = "/api/info", .method = HTTP_GET, .handler = api_handler, .user_ctx = (void *)(uintptr_t)HTTP_API_ROUTE_INFO},
|
||||
{.uri = "/api/health", .method = HTTP_GET, .handler = api_handler, .user_ctx = (void *)(uintptr_t)HTTP_API_ROUTE_HEALTH},
|
||||
{.uri = "/api/session/join", .method = HTTP_POST, .handler = api_handler, .user_ctx = (void *)(uintptr_t)HTTP_API_ROUTE_JOIN},
|
||||
@@ -499,6 +661,7 @@ void app_main(void) {
|
||||
const esp_timer_create_args_t bot_timer_args = {.callback = bot_timer_callback, .name = "bot_turn"};
|
||||
ESP_ERROR_CHECK(esp_timer_create(&bot_timer_args, &s_bot_timer));
|
||||
application_set_bot_scheduler(&s_application, (scheduler_t){.schedule_after_ms = schedule_bot_turn, .context = NULL});
|
||||
network_configuration_init(&s_configuration);
|
||||
http_api_init(&s_api, &s_application);
|
||||
sync_service_init(&s_sync, &s_application);
|
||||
mount_littlefs();
|
||||
|
||||
Reference in New Issue
Block a user