Initial kesp32_app_base component

This commit is contained in:
2026-10-04 11:42:27 +09:00
commit e4e4d78b26
25 changed files with 2073 additions and 0 deletions
+6
View File
@@ -0,0 +1,6 @@
# ESP-IDF generated output and local configuration
build/
managed_components/
sdkconfig
sdkconfig.old
*.log
+16
View File
@@ -0,0 +1,16 @@
idf_component_register(
SRCS
"src/esp32_app_base.c"
"src/wifi_store.c"
"src/web_provisioning.c"
"src/portal_self_test.c"
"src/time_sync.c"
"src/ota_update.c"
"src/ota_version.c"
"src/mqtt_transport.c"
"src/dns_server/dns_server.c"
INCLUDE_DIRS "include" "src" "src/dns_server/include"
EMBED_FILES "src/provisioning.html"
REQUIRES app_update esp_driver_gpio esp_event esp_http_server esp_https_ota esp_netif esp_wifi mqtt nvs_flash
PRIV_REQUIRES espressif__cjson
)
+82
View File
@@ -0,0 +1,82 @@
menu "SESE Controller"
config SESE_NTP_SERVER
string "NTP server"
default "pool.ntp.org"
help
Hostname of the SNTP server used to set the system clock after the
station receives an IP address.
config SESE_TIMEZONE
string "POSIX timezone"
default "KST-9"
help
POSIX TZ string used when formatting local timestamps. KST-9 means
Korea Standard Time (UTC+09:00). The system epoch remains UTC.
config SESE_NTP_INITIAL_WAIT_SECONDS
int "Initial NTP synchronization wait (seconds)"
default 30
range 5 120
help
Maximum boot-time wait for the first NTP response. A timeout does
not stop the SNTP client's periodic background retries.
config SESE_MQTT_ENABLE
bool "Enable MQTT client"
default y
help
Connect to the configured broker after Wi-Fi and initial NTP setup.
The client publishes retained online/offline state, periodic
heartbeats, and implements a bounded ping/pong test command.
config SESE_MQTT_BROKER_URI
string "MQTT broker URI"
default ""
depends on SESE_MQTT_ENABLE
help
Broker URI such as mqtt://host:1883 or mqtts://host:8883. Leave
empty to keep the client disabled. Do not embed usernames,
passwords, tokens, or other credentials in this build setting.
config SESE_MQTT_HEARTBEAT_SECONDS
int "MQTT heartbeat interval (seconds)"
default 60
range 5 86400
depends on SESE_MQTT_ENABLE
config SESE_OTA_ENABLE
bool "Enable automatic HTTPS OTA"
default y
help
Check a configured HTTPS endpoint for a newer firmware image after
Wi-Fi connects and then periodically while the device is running.
config SESE_OTA_URL
string "HTTPS OTA firmware URL"
default ""
depends on SESE_OTA_ENABLE
help
Direct HTTPS URL to an ESP-IDF application .bin image. Leave empty
to disable network checks. Do not place credentials or secret query
parameters in this build-time URL.
config SESE_OTA_INITIAL_DELAY_SECONDS
int "Delay before first OTA check (seconds)"
default 30
range 5 3600
depends on SESE_OTA_ENABLE
config SESE_OTA_CHECK_INTERVAL_MINUTES
int "Successful/no-update check interval (minutes)"
default 360
range 5 10080
depends on SESE_OTA_ENABLE
config SESE_OTA_RETRY_INTERVAL_MINUTES
int "Failed-check retry interval (minutes)"
default 15
range 1 1440
depends on SESE_OTA_ENABLE
endmenu
+9
View File
@@ -0,0 +1,9 @@
# kesp32_app_base
Reusable ESP-IDF application-base component for SESE consumers. The source keeps
its public compatibility API, including `esp32_app_base_start()` and the
`SESE_*` Kconfig symbols; the repository/component name is `kesp32_app_base`.
This checkout is consumed locally by `sese-controller` as a Git submodule. The
parent project supplies product policy, board settings, partitions, and build
configuration.
+5
View File
@@ -0,0 +1,5 @@
dependencies:
idf:
version: '>=6.0.2'
espressif/mqtt: ^1.1.0
espressif/cjson: ^1.7.19
+34
View File
@@ -0,0 +1,34 @@
#pragma once
#include "esp_err.h"
#include <stdbool.h>
#include <stdint.h>
#include "freertos/FreeRTOS.h"
#include "freertos/event_groups.h"
typedef esp_err_t (*esp32_app_base_product_start_fn_t)(void);
typedef esp_err_t (*esp32_app_base_health_check_fn_t)(void);
typedef struct {
const char *service_prefix;
int reset_gpio;
bool reset_active_low;
uint32_t reset_hold_ms;
esp32_app_base_product_start_fn_t product_start;
esp32_app_base_health_check_fn_t health_check;
const char *ntp_server;
const char *timezone;
uint32_t ntp_initial_wait_seconds;
bool ota_enabled;
const char *ota_url;
uint32_t ota_initial_delay_seconds;
uint32_t ota_check_interval_minutes;
uint32_t ota_retry_interval_minutes;
} esp32_app_base_config_t;
/** Owns NVS, Wi-Fi browser provisioning, NTP, OTA rollback and reset button. */
esp_err_t esp32_app_base_start(const esp32_app_base_config_t *config);
/** Network readiness event group for optional product services. */
EventGroupHandle_t esp32_app_base_network_events(void);
EventBits_t esp32_app_base_network_ready_bit(void);
+25
View File
@@ -0,0 +1,25 @@
#pragma once
#include "esp_err.h"
#include <stdbool.h>
#include <stddef.h>
#include <stdint.h>
typedef void (*mqtt_transport_connected_fn_t)(void *ctx);
typedef void (*mqtt_transport_message_fn_t)(const char *topic, size_t topic_len,
const char *data, size_t data_len, void *ctx);
typedef void (*mqtt_transport_heartbeat_fn_t)(void *ctx);
typedef struct {
const char *broker_uri;
const char *client_id;
const char *last_will_topic;
const char *last_will_payload;
uint32_t heartbeat_interval_seconds;
mqtt_transport_connected_fn_t on_connected;
mqtt_transport_message_fn_t on_message;
mqtt_transport_heartbeat_fn_t on_heartbeat;
void *ctx;
} mqtt_transport_config_t;
esp_err_t mqtt_transport_start(const mqtt_transport_config_t *config);
int mqtt_transport_publish(const char *topic, const char *payload, int qos, bool retain);
int mqtt_transport_subscribe(const char *topic, int qos);
+11
View File
@@ -0,0 +1,11 @@
#pragma once
#include "esp_err.h"
#include "freertos/FreeRTOS.h"
#include "freertos/event_groups.h"
/** Mark an OTA image valid after application-level network diagnostics pass. */
esp_err_t ota_mark_running_app_valid(void);
/** Start the periodic HTTPS OTA worker when OTA is configured. */
esp_err_t ota_start_task(EventGroupHandle_t network_events, EventBits_t connected_bit);
+6
View File
@@ -0,0 +1,6 @@
#pragma once
#include <stdbool.h>
/** Compare strict major.minor.patch versions. Returns false for invalid input. */
bool ota_version_compare(const char *left, const char *right, int *order);
+6
View File
@@ -0,0 +1,6 @@
#pragma once
#include "esp_err.h"
/** Run one local HTTP health check against the captive portal. */
esp_err_t portal_self_test_start(void);
+10
View File
@@ -0,0 +1,10 @@
#pragma once
#include "esp_err.h"
#include "freertos/FreeRTOS.h"
/** Initialize and start periodic SNTP synchronization. */
esp_err_t time_sync_start(void);
/** Wait for the first successful synchronization and log local time. */
esp_err_t time_sync_wait(TickType_t timeout);
+31
View File
@@ -0,0 +1,31 @@
#pragma once
#include <stdint.h>
#include "esp_err.h"
#include "wifi_store.h"
typedef esp_err_t (*web_provisioning_apply_cb_t)(const wifi_store_credentials_t *credentials);
typedef void (*web_provisioning_expired_cb_t)(void);
typedef enum {
WEB_PROV_IDLE,
WEB_PROV_SCANNING,
WEB_PROV_CONNECTING,
WEB_PROV_CONNECTED,
WEB_PROV_FAILED,
} web_provisioning_state_t;
typedef struct {
const char *service_name;
uint32_t timeout_seconds;
web_provisioning_apply_cb_t apply_credentials;
web_provisioning_expired_cb_t expired;
} web_provisioning_config_t;
esp_err_t web_provisioning_start(const web_provisioning_config_t *config);
void web_provisioning_stop(void);
bool web_provisioning_is_active(void);
bool web_provisioning_is_connecting(void);
void web_provisioning_report_connected(const char *ip_address);
void web_provisioning_report_failure(const char *reason);
+19
View File
@@ -0,0 +1,19 @@
#pragma once
#include <stdbool.h>
#include <stddef.h>
#include <stdint.h>
#include "esp_err.h"
#define WIFI_STORE_SSID_MAX_LEN 32
#define WIFI_STORE_PASSWORD_MAX_LEN 64
typedef struct {
char ssid[WIFI_STORE_SSID_MAX_LEN + 1];
char password[WIFI_STORE_PASSWORD_MAX_LEN + 1];
} wifi_store_credentials_t;
esp_err_t wifi_store_load(wifi_store_credentials_t *credentials, bool *found);
esp_err_t wifi_store_save(const wifi_store_credentials_t *credentials);
esp_err_t wifi_store_erase(void);
+3
View File
@@ -0,0 +1,3 @@
idf_component_register(SRCS dns_server.c
INCLUDE_DIRS include
PRIV_REQUIRES esp_netif)
+290
View File
@@ -0,0 +1,290 @@
/*
* SPDX-FileCopyrightText: 2021-2023 Espressif Systems (Shanghai) CO LTD
*
* SPDX-License-Identifier: Unlicense OR CC0-1.0
*/
#include <sys/param.h>
#include <inttypes.h>
#include "esp_log.h"
#include "esp_system.h"
#include "esp_check.h"
#include "esp_netif.h"
#include "lwip/err.h"
#include "lwip/sockets.h"
#include "lwip/sys.h"
#include "lwip/netdb.h"
#include "dns_server.h"
#define DNS_PORT (53)
#define DNS_MAX_LEN (256)
#define OPCODE_MASK (0x7800)
#define QR_FLAG (1 << 7)
#define QD_TYPE_A (0x0001)
#define ANS_TTL_SEC (300)
static const char *TAG = "example_dns_redirect_server";
// DNS Header Packet
typedef struct __attribute__((__packed__))
{
uint16_t id;
uint16_t flags;
uint16_t qd_count;
uint16_t an_count;
uint16_t ns_count;
uint16_t ar_count;
} dns_header_t;
// DNS Question Packet
typedef struct {
uint16_t type;
uint16_t class;
} dns_question_t;
// DNS Answer Packet
typedef struct __attribute__((__packed__))
{
uint16_t ptr_offset;
uint16_t type;
uint16_t class;
uint32_t ttl;
uint16_t addr_len;
uint32_t ip_addr;
} dns_answer_t;
// DNS server handle
struct dns_server_handle {
bool started;
TaskHandle_t task;
int num_of_entries;
dns_entry_pair_t entry[];
};
/*
Parse the name from the packet from the DNS name format to a regular .-seperated name
returns the pointer to the next part of the packet
*/
static char *parse_dns_name(char *raw_name, char *parsed_name, size_t parsed_name_max_len)
{
char *label = raw_name;
char *name_itr = parsed_name;
int name_len = 0;
do {
int sub_name_len = *label;
// (len + 1) since we are adding a '.'
name_len += (sub_name_len + 1);
if (name_len > parsed_name_max_len) {
return NULL;
}
// Copy the sub name that follows the the label
memcpy(name_itr, label + 1, sub_name_len);
name_itr[sub_name_len] = '.';
name_itr += (sub_name_len + 1);
label += sub_name_len + 1;
} while (*label != 0);
// Terminate the final string, replacing the last '.'
parsed_name[name_len - 1] = '\0';
// Return pointer to first char after the name
return label + 1;
}
// Parses the DNS request and prepares a DNS response with the IP of the softAP
static int parse_dns_request(char *req, size_t req_len, char *dns_reply, size_t dns_reply_max_len, dns_server_handle_t h)
{
if (req_len > dns_reply_max_len) {
return -1;
}
// Prepare the reply
memset(dns_reply, 0, dns_reply_max_len);
memcpy(dns_reply, req, req_len);
// Endianess of NW packet different from chip
dns_header_t *header = (dns_header_t *)dns_reply;
ESP_LOGD(TAG, "DNS query with header id: 0x%X, flags: 0x%X, qd_count: %d",
ntohs(header->id), ntohs(header->flags), ntohs(header->qd_count));
// Not a standard query
if ((header->flags & OPCODE_MASK) != 0) {
return 0;
}
// Set question response flag
header->flags |= QR_FLAG;
uint16_t qd_count = ntohs(header->qd_count);
header->an_count = htons(qd_count);
int reply_len = qd_count * sizeof(dns_answer_t) + req_len;
if (reply_len > dns_reply_max_len) {
return -1;
}
// Pointer to current answer and question
char *cur_ans_ptr = dns_reply + req_len;
char *cur_qd_ptr = dns_reply + sizeof(dns_header_t);
char name[128];
// Respond to all questions based on configured rules
for (int qd_i = 0; qd_i < qd_count; qd_i++) {
char *name_end_ptr = parse_dns_name(cur_qd_ptr, name, sizeof(name));
if (name_end_ptr == NULL) {
ESP_LOGE(TAG, "Failed to parse DNS question: %s", cur_qd_ptr);
return -1;
}
dns_question_t *question = (dns_question_t *)(name_end_ptr);
uint16_t qd_type = ntohs(question->type);
uint16_t qd_class = ntohs(question->class);
ESP_LOGD(TAG, "Received type: %d | Class: %d | Question for: %s", qd_type, qd_class, name);
if (qd_type == QD_TYPE_A) {
esp_ip4_addr_t ip = { .addr = IPADDR_ANY };
// Check the configured rules to decide whether to answer this question or not
for (int i = 0; i < h->num_of_entries; ++i) {
// check if the name either corresponds to the entry, or if we should answer to all queries ("*")
if (strcmp(h->entry[i].name, "*") == 0 || strcmp(h->entry[i].name, name) == 0) {
if (h->entry[i].if_key) {
esp_netif_ip_info_t ip_info;
esp_netif_get_ip_info(esp_netif_get_handle_from_ifkey(h->entry[i].if_key), &ip_info);
ip.addr = ip_info.ip.addr;
break;
} else if (h->entry->ip.addr != IPADDR_ANY) {
ip.addr = h->entry[i].ip.addr;
break;
}
}
}
if (ip.addr == IPADDR_ANY) { // no rule applies, continue with another question
continue;
}
dns_answer_t *answer = (dns_answer_t *)cur_ans_ptr;
answer->ptr_offset = htons(0xC000 | (cur_qd_ptr - dns_reply));
answer->type = htons(qd_type);
answer->class = htons(qd_class);
answer->ttl = htonl(ANS_TTL_SEC);
ESP_LOGD(TAG, "Answer with PTR offset: 0x%" PRIX16 " and IP 0x%" PRIX32, ntohs(answer->ptr_offset), ip.addr);
answer->addr_len = htons(sizeof(ip.addr));
answer->ip_addr = ip.addr;
}
}
return reply_len;
}
/*
Sets up a socket and listen for DNS queries,
replies to all type A queries with the IP of the softAP
*/
void dns_server_task(void *pvParameters)
{
char rx_buffer[128];
char addr_str[128];
int addr_family;
int ip_protocol;
dns_server_handle_t handle = pvParameters;
while (handle->started) {
struct sockaddr_in dest_addr;
dest_addr.sin_addr.s_addr = htonl(INADDR_ANY);
dest_addr.sin_family = AF_INET;
dest_addr.sin_port = htons(DNS_PORT);
addr_family = AF_INET;
ip_protocol = IPPROTO_IP;
inet_ntoa_r(dest_addr.sin_addr, addr_str, sizeof(addr_str) - 1);
int sock = socket(addr_family, SOCK_DGRAM, ip_protocol);
if (sock < 0) {
ESP_LOGE(TAG, "Unable to create socket: errno %d", errno);
break;
}
ESP_LOGI(TAG, "Socket created");
int err = bind(sock, (struct sockaddr *)&dest_addr, sizeof(dest_addr));
if (err < 0) {
ESP_LOGE(TAG, "Socket unable to bind: errno %d", errno);
}
ESP_LOGI(TAG, "Socket bound, port %d", DNS_PORT);
while (handle->started) {
ESP_LOGI(TAG, "Waiting for data");
struct sockaddr_in6 source_addr; // Large enough for both IPv4 or IPv6
socklen_t socklen = sizeof(source_addr);
int len = recvfrom(sock, rx_buffer, sizeof(rx_buffer) - 1, 0, (struct sockaddr *)&source_addr, &socklen);
// Error occurred during receiving
if (len < 0) {
ESP_LOGE(TAG, "recvfrom failed: errno %d", errno);
close(sock);
break;
}
// Data received
else {
// Get the sender's ip address as string
if (source_addr.sin6_family == PF_INET) {
inet_ntoa_r(((struct sockaddr_in *)&source_addr)->sin_addr.s_addr, addr_str, sizeof(addr_str) - 1);
} else if (source_addr.sin6_family == PF_INET6) {
inet6_ntoa_r(source_addr.sin6_addr, addr_str, sizeof(addr_str) - 1);
}
// Null-terminate whatever we received and treat like a string...
rx_buffer[len] = 0;
char reply[DNS_MAX_LEN];
int reply_len = parse_dns_request(rx_buffer, len, reply, DNS_MAX_LEN, handle);
ESP_LOGI(TAG, "Received %d bytes from %s | DNS reply with len: %d", len, addr_str, reply_len);
if (reply_len <= 0) {
ESP_LOGE(TAG, "Failed to prepare a DNS reply");
} else {
int err = sendto(sock, reply, reply_len, 0, (struct sockaddr *)&source_addr, sizeof(source_addr));
if (err < 0) {
ESP_LOGE(TAG, "Error occurred during sending: errno %d", errno);
break;
}
}
}
}
if (sock != -1) {
ESP_LOGE(TAG, "Shutting down socket");
shutdown(sock, 0);
close(sock);
}
}
vTaskDelete(NULL);
}
dns_server_handle_t start_dns_server(dns_server_config_t *config)
{
dns_server_handle_t handle = calloc(1, sizeof(struct dns_server_handle) + config->num_of_entries * sizeof(dns_entry_pair_t));
ESP_RETURN_ON_FALSE(handle, NULL, TAG, "Failed to allocate dns server handle");
handle->started = true;
handle->num_of_entries = config->num_of_entries;
memcpy(handle->entry, config->item, config->num_of_entries * sizeof(dns_entry_pair_t));
xTaskCreate(dns_server_task, "dns_server", 4096, handle, 5, &handle->task);
return handle;
}
void stop_dns_server(dns_server_handle_t handle)
{
if (handle) {
handle->started = false;
vTaskDelete(handle->task);
free(handle);
}
}
+80
View File
@@ -0,0 +1,80 @@
/*
* SPDX-FileCopyrightText: 2021-2023 Espressif Systems (Shanghai) CO LTD
*
* SPDX-License-Identifier: Unlicense OR CC0-1.0
*/
#pragma once
#include "esp_netif.h"
#ifdef __cplusplus
extern "C" {
#endif
#ifndef DNS_SERVER_MAX_ITEMS
#define DNS_SERVER_MAX_ITEMS 1
#endif
#define DNS_SERVER_CONFIG_SINGLE(queried_name, netif_key) { \
.num_of_entries = 1, \
.item = { { .name = queried_name, .if_key = netif_key } } \
}
/**
* @brief Definition of one DNS entry: NAME - IP (or the netif whose IP to answer)
*
* @note Please use string literals (or ensure they are valid during dns_server lifetime) as names, since
* we don't take copies of the config values `name` and `if_key`
*/
typedef struct dns_entry_pair {
const char* name; /**<! Exact match of the name field of the DNS query to answer */
const char* if_key; /**<! Use this network interface IP to answer, only if NULL, use the static IP below */
esp_ip4_addr_t ip; /**<! Constant IP address to answer this query, if "if_key==NULL" */
} dns_entry_pair_t;
/**
* @brief DNS server config struct defining the rules for answering DNS (A type) queries
*
* @note If you want to define more rules, you can set `DNS_SERVER_MAX_ITEMS` before including this header
* Example of using 2 entries with constant IP addresses
* \code{.c}
* #define DNS_SERVER_MAX_ITEMS 2
* #include "dns_server.h"
*
* dns_server_config_t config = {
* .num_of_entries = 2,
* .item = { {.name = "my-esp32.com", .ip = { .addr = ESP_IP4TOADDR( 192, 168, 4, 1) } } ,
* {.name = "my-utils.com", .ip = { .addr = ESP_IP4TOADDR( 192, 168, 4, 100) } } } };
* start_dns_server(&config);
* \endcode
*/
typedef struct dns_server_config {
int num_of_entries; /**<! Number of rules specified in the config struct */
dns_entry_pair_t item[DNS_SERVER_MAX_ITEMS]; /**<! Array of pairs */
} dns_server_config_t;
/**
* @brief DNS server handle
*/
typedef struct dns_server_handle *dns_server_handle_t;
/**
* @brief Set ups and starts a simple DNS server that will respond to all A queries (IPv4)
* based on configured rules, pairs of name and either IPv4 address or a netif ID (to respond by it's IPv4 add)
*
* @param config Configuration structure listing the pairs of (name, IP/netif-id)
* @return dns_server's handle on success, NULL on failure
*/
dns_server_handle_t start_dns_server(dns_server_config_t *config);
/**
* @brief Stops and destroys DNS server's task and structs
* @param handle DNS server's handle to destroy
*/
void stop_dns_server(dns_server_handle_t handle);
#ifdef __cplusplus
}
#endif
+451
View File
@@ -0,0 +1,451 @@
#include <inttypes.h>
#include <stdbool.h>
#include <stdint.h>
#include <stdio.h>
#include <string.h>
#include "driver/gpio.h"
#include "esp_event.h"
#include "esp_log.h"
#include "esp_mac.h"
#include "esp_netif.h"
#include "esp_system.h"
#include "esp_timer.h"
#include "esp_wifi.h"
#include "freertos/FreeRTOS.h"
#include "freertos/event_groups.h"
#include "freertos/semphr.h"
#include "freertos/task.h"
#include "nvs_flash.h"
#include "ota_update.h"
#include "esp32_app_base.h"
#include "portal_self_test.h"
#include "time_sync.h"
#include "web_provisioning.h"
#include "wifi_store.h"
#define WIFI_CONNECTED_BIT BIT0
#define RECONNECT_MAX_DELAY_SECONDS 30
#define RECOVERY_PORTAL_AFTER_FAILURES 5
#define PORTAL_TIMEOUT_SECONDS 600
#define PORTAL_SUCCESS_DISPLAY_SECONDS 5
#if CONFIG_IDF_TARGET_ESP32C3
#define RESET_BUTTON_GPIO GPIO_NUM_9
#else
#define RESET_BUTTON_GPIO GPIO_NUM_0
#endif
#define RESET_BUTTON_HOLD_MS 5000
static const char *TAG = "esp32_app_base";
static EventGroupHandle_t s_wifi_events;
static SemaphoreHandle_t s_credentials_mutex;
static esp_timer_handle_t s_reconnect_timer;
static bool s_wifi_started;
static bool s_portal_mode;
static bool s_recovery_requested;
static unsigned int s_reconnect_attempt;
static char s_service_name[16];
static wifi_store_credentials_t s_saved_credentials;
static bool s_saved_credentials_found;
static wifi_store_credentials_t s_pending_credentials;
static int s_reset_gpio = RESET_BUTTON_GPIO;
static bool s_reset_active_low = true;
static uint32_t s_reset_hold_ms = RESET_BUTTON_HOLD_MS;
static void start_browser_portal(void);
static void clear_pending_credentials(void)
{
xSemaphoreTake(s_credentials_mutex, portMAX_DELAY);
memset(&s_pending_credentials, 0, sizeof(s_pending_credentials));
xSemaphoreGive(s_credentials_mutex);
}
static void reconnect_timer_callback(void *arg)
{
(void)arg;
if (!s_portal_mode) {
esp_err_t err = esp_wifi_connect();
if (err != ESP_OK) {
ESP_LOGW(TAG, "Wi-Fi reconnect request failed: %s", esp_err_to_name(err));
}
}
}
static void recovery_portal_task(void *arg)
{
(void)arg;
start_browser_portal();
s_recovery_requested = false;
vTaskDelete(NULL);
}
static void schedule_reconnect(void)
{
if (s_reconnect_attempt >= RECOVERY_PORTAL_AFTER_FAILURES && !s_recovery_requested) {
s_recovery_requested = true;
ESP_LOGW(TAG, "Repeated Wi-Fi failures; starting browser recovery portal");
if (xTaskCreate(recovery_portal_task, "recovery_portal", 6144, NULL, 5, NULL) != pdPASS) {
s_recovery_requested = false;
ESP_LOGE(TAG, "Could not create recovery portal task");
}
return;
}
uint32_t delay_seconds = 1U << (s_reconnect_attempt < 5U ? s_reconnect_attempt : 5U);
if (delay_seconds > RECONNECT_MAX_DELAY_SECONDS) {
delay_seconds = RECONNECT_MAX_DELAY_SECONDS;
}
s_reconnect_attempt++;
esp_err_t err = esp_timer_stop(s_reconnect_timer);
if (err != ESP_OK && err != ESP_ERR_INVALID_STATE) {
ESP_LOGW(TAG, "Could not stop reconnect timer: %s", esp_err_to_name(err));
}
ESP_LOGW(TAG, "Wi-Fi disconnected; reconnecting in %" PRIu32 " seconds", delay_seconds);
ESP_ERROR_CHECK(esp_timer_start_once(s_reconnect_timer, delay_seconds * 1000000ULL));
}
static const char *disconnect_message(uint8_t reason)
{
switch (reason) {
case WIFI_REASON_AUTH_FAIL:
case WIFI_REASON_4WAY_HANDSHAKE_TIMEOUT:
case WIFI_REASON_HANDSHAKE_TIMEOUT:
return "Wi-Fi 인증에 실패했습니다. 비밀번호를 확인해 주세요.";
case WIFI_REASON_NO_AP_FOUND:
return "선택한 Wi-Fi를 찾지 못했습니다.";
default:
return "Wi-Fi에 연결하지 못했습니다. 다시 시도해 주세요.";
}
}
static void portal_shutdown_task(void *arg)
{
(void)arg;
vTaskDelay(pdMS_TO_TICKS(PORTAL_SUCCESS_DISPLAY_SECONDS * 1000U));
web_provisioning_stop();
s_portal_mode = false;
ESP_ERROR_CHECK(esp_wifi_set_mode(WIFI_MODE_STA));
ESP_LOGI(TAG, "Provisioning SoftAP stopped; station mode remains active");
vTaskDelete(NULL);
}
static void event_handler(void *arg, esp_event_base_t event_base,
int32_t event_id, void *event_data)
{
(void)arg;
if (event_base == WIFI_EVENT) {
if (event_id == WIFI_EVENT_STA_START && !s_portal_mode) {
ESP_ERROR_CHECK(esp_wifi_connect());
} else if (event_id == WIFI_EVENT_STA_DISCONNECTED) {
xEventGroupClearBits(s_wifi_events, WIFI_CONNECTED_BIT);
const wifi_event_sta_disconnected_t *event = event_data;
if (web_provisioning_is_connecting()) {
ESP_LOGW(TAG, "Browser-provided Wi-Fi connection failed, reason=%u", event->reason);
clear_pending_credentials();
web_provisioning_report_failure(disconnect_message(event->reason));
} else if (!s_portal_mode && s_saved_credentials_found) {
schedule_reconnect();
}
}
return;
}
if (event_base == IP_EVENT && event_id == IP_EVENT_STA_GOT_IP) {
const ip_event_got_ip_t *event = event_data;
s_reconnect_attempt = 0;
esp_err_t err = esp_timer_stop(s_reconnect_timer);
if (err != ESP_OK && err != ESP_ERR_INVALID_STATE) {
ESP_LOGW(TAG, "Could not stop reconnect timer: %s", esp_err_to_name(err));
}
ESP_LOGI(TAG, "Wi-Fi connected, IPv4 address: " IPSTR, IP2STR(&event->ip_info.ip));
if (web_provisioning_is_connecting()) {
xSemaphoreTake(s_credentials_mutex, portMAX_DELAY);
wifi_store_credentials_t credentials = s_pending_credentials;
memset(&s_pending_credentials, 0, sizeof(s_pending_credentials));
xSemaphoreGive(s_credentials_mutex);
err = wifi_store_save(&credentials);
if (err != ESP_OK) {
memset(&credentials, 0, sizeof(credentials));
ESP_LOGE(TAG, "Could not persist verified Wi-Fi settings: %s", esp_err_to_name(err));
web_provisioning_report_failure("설정을 저장하지 못했습니다.");
return;
}
s_saved_credentials = credentials;
s_saved_credentials_found = true;
memset(&credentials, 0, sizeof(credentials));
char ip_address[16];
snprintf(ip_address, sizeof(ip_address), IPSTR, IP2STR(&event->ip_info.ip));
web_provisioning_report_connected(ip_address);
ESP_LOGI(TAG, "Verified Wi-Fi settings stored in NVS");
if (xTaskCreate(portal_shutdown_task, "portal_shutdown", 4096, NULL, 5, NULL) != pdPASS) {
ESP_LOGE(TAG, "Could not create portal shutdown task");
}
}
xEventGroupSetBits(s_wifi_events, WIFI_CONNECTED_BIT);
}
}
static void get_service_name(void)
{
uint8_t mac[6];
ESP_ERROR_CHECK(esp_read_mac(mac, ESP_MAC_WIFI_STA));
snprintf(s_service_name, sizeof(s_service_name), "APP_%02X%02X%02X",
mac[3], mac[4], mac[5]);
}
static void init_nvs(void)
{
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();
}
ESP_ERROR_CHECK(err);
}
static void init_network_stack(void)
{
ESP_ERROR_CHECK(esp_netif_init());
ESP_ERROR_CHECK(esp_event_loop_create_default());
s_wifi_events = xEventGroupCreate();
s_credentials_mutex = xSemaphoreCreateMutex();
ESP_ERROR_CHECK(s_wifi_events == NULL || s_credentials_mutex == NULL ? ESP_ERR_NO_MEM : ESP_OK);
const esp_timer_create_args_t timer_args = {
.callback = reconnect_timer_callback,
.name = "wifi_reconnect",
};
ESP_ERROR_CHECK(esp_timer_create(&timer_args, &s_reconnect_timer));
ESP_ERROR_CHECK(esp_event_handler_register(WIFI_EVENT, ESP_EVENT_ANY_ID,
event_handler, NULL));
ESP_ERROR_CHECK(esp_event_handler_register(IP_EVENT, IP_EVENT_STA_GOT_IP,
event_handler, NULL));
ESP_ERROR_CHECK(esp_netif_create_default_wifi_sta() == NULL ? ESP_FAIL : ESP_OK);
ESP_ERROR_CHECK(esp_netif_create_default_wifi_ap() == NULL ? ESP_FAIL : ESP_OK);
wifi_init_config_t wifi_config = WIFI_INIT_CONFIG_DEFAULT();
ESP_ERROR_CHECK(esp_wifi_init(&wifi_config));
ESP_ERROR_CHECK(esp_wifi_set_storage(WIFI_STORAGE_RAM));
}
static esp_err_t make_station_config(const wifi_store_credentials_t *credentials,
wifi_config_t *config)
{
if (credentials == NULL || config == NULL) {
return ESP_ERR_INVALID_ARG;
}
size_t ssid_len = strnlen(credentials->ssid, sizeof(credentials->ssid));
size_t password_len = strnlen(credentials->password, sizeof(credentials->password));
if (ssid_len == 0 || ssid_len > sizeof(config->sta.ssid) ||
password_len > sizeof(config->sta.password)) {
return ESP_ERR_INVALID_ARG;
}
memset(config, 0, sizeof(*config));
memcpy(config->sta.ssid, credentials->ssid, ssid_len);
memcpy(config->sta.password, credentials->password, password_len);
config->sta.scan_method = WIFI_ALL_CHANNEL_SCAN;
config->sta.sort_method = WIFI_CONNECT_AP_BY_SIGNAL;
config->sta.threshold.authmode = WIFI_AUTH_OPEN;
return ESP_OK;
}
static esp_err_t verify_station_config_boundaries(void)
{
wifi_store_credentials_t credentials = {0};
memset(credentials.ssid, 'S', WIFI_STORE_SSID_MAX_LEN);
memset(credentials.password, 'a', WIFI_STORE_PASSWORD_MAX_LEN);
wifi_config_t config;
esp_err_t err = make_station_config(&credentials, &config);
if (err != ESP_OK ||
memcmp(config.sta.ssid, credentials.ssid, sizeof(config.sta.ssid)) != 0 ||
memcmp(config.sta.password, credentials.password, sizeof(config.sta.password)) != 0) {
memset(&credentials, 0, sizeof(credentials));
memset(&config, 0, sizeof(config));
return ESP_FAIL;
}
memset(&credentials, 0, sizeof(credentials));
memset(&config, 0, sizeof(config));
ESP_LOGI(TAG, "32-byte SSID and 64-byte PSK boundary self-test passed");
return ESP_OK;
}
static esp_err_t apply_browser_credentials(const wifi_store_credentials_t *credentials)
{
if (credentials == NULL) {
return ESP_ERR_INVALID_ARG;
}
xSemaphoreTake(s_credentials_mutex, portMAX_DELAY);
s_pending_credentials = *credentials;
xSemaphoreGive(s_credentials_mutex);
wifi_config_t station_config;
esp_err_t err = make_station_config(credentials, &station_config);
if (err == ESP_OK) {
err = esp_wifi_set_config(WIFI_IF_STA, &station_config);
}
memset(&station_config, 0, sizeof(station_config));
if (err == ESP_OK) {
err = esp_wifi_connect();
}
if (err != ESP_OK) {
clear_pending_credentials();
}
return err;
}
static void portal_expired(void)
{
web_provisioning_stop();
s_portal_mode = false;
if (s_saved_credentials_found) {
ESP_LOGI(TAG, "Restoring station connection after portal timeout");
wifi_config_t station_config;
ESP_ERROR_CHECK(make_station_config(&s_saved_credentials, &station_config));
ESP_ERROR_CHECK(esp_wifi_set_mode(WIFI_MODE_STA));
ESP_ERROR_CHECK(esp_wifi_set_config(WIFI_IF_STA, &station_config));
memset(&station_config, 0, sizeof(station_config));
ESP_ERROR_CHECK(esp_wifi_connect());
} else {
ESP_LOGW(TAG, "No Wi-Fi settings; SoftAP disabled until reboot or BOOT long-press");
ESP_ERROR_CHECK(esp_wifi_set_mode(WIFI_MODE_NULL));
}
}
static void start_browser_portal(void)
{
get_service_name();
s_portal_mode = true;
wifi_config_t ap_config = {0};
strlcpy((char *)ap_config.ap.ssid, s_service_name, sizeof(ap_config.ap.ssid));
ap_config.ap.ssid_len = strlen(s_service_name);
ap_config.ap.channel = 1;
ap_config.ap.max_connection = 1;
ap_config.ap.authmode = WIFI_AUTH_OPEN;
ESP_ERROR_CHECK(esp_wifi_set_mode(WIFI_MODE_APSTA));
ESP_ERROR_CHECK(esp_wifi_set_config(WIFI_IF_AP, &ap_config));
if (!s_wifi_started) {
ESP_ERROR_CHECK(esp_wifi_start());
s_wifi_started = true;
}
const web_provisioning_config_t portal_config = {
.service_name = s_service_name,
.timeout_seconds = PORTAL_TIMEOUT_SECONDS,
.apply_credentials = apply_browser_credentials,
.expired = portal_expired,
};
ESP_ERROR_CHECK(web_provisioning_start(&portal_config));
ESP_ERROR_CHECK(portal_self_test_start());
wifi_config_t running_ap = {0};
ESP_ERROR_CHECK(esp_wifi_get_config(WIFI_IF_AP, &running_ap));
ESP_ERROR_CHECK(running_ap.ap.authmode == WIFI_AUTH_OPEN ? ESP_OK : ESP_FAIL);
ESP_LOGW(TAG, "Browser Wi-Fi setup required");
ESP_LOGW(TAG, "Provisioning SoftAP SSID: %s", s_service_name);
ESP_LOGW(TAG, "Provisioning SoftAP security: open (no Wi-Fi password)");
ESP_LOGW(TAG, "Open http://192.168.4.1 if the setup page does not appear automatically");
}
static void start_wifi_station(void)
{
wifi_config_t station_config;
ESP_ERROR_CHECK(make_station_config(&s_saved_credentials, &station_config));
ESP_ERROR_CHECK(esp_wifi_set_mode(WIFI_MODE_STA));
ESP_ERROR_CHECK(esp_wifi_set_config(WIFI_IF_STA, &station_config));
memset(&station_config, 0, sizeof(station_config));
ESP_ERROR_CHECK(esp_wifi_start());
s_wifi_started = true;
}
static void reset_button_task(void *arg)
{
(void)arg;
const gpio_config_t config = {
.pin_bit_mask = 1ULL << RESET_BUTTON_GPIO,
.mode = GPIO_MODE_INPUT,
.pull_up_en = GPIO_PULLUP_DISABLE,
.pull_down_en = GPIO_PULLDOWN_DISABLE,
.intr_type = GPIO_INTR_DISABLE,
};
ESP_ERROR_CHECK(gpio_config(&config));
uint32_t held_ms = 0;
while (true) {
bool pressed = gpio_get_level(s_reset_gpio) == (s_reset_active_low ? 0 : 1);
if (pressed) {
held_ms += 100;
if (held_ms >= s_reset_hold_ms) {
ESP_LOGW(TAG, "BOOT long-press detected; erasing Wi-Fi settings and restarting");
ESP_ERROR_CHECK(wifi_store_erase());
vTaskDelay(pdMS_TO_TICKS(200));
esp_restart();
}
} else {
held_ms = 0;
}
vTaskDelay(pdMS_TO_TICKS(100));
}
}
EventGroupHandle_t esp32_app_base_network_events(void) { return s_wifi_events; }
EventBits_t esp32_app_base_network_ready_bit(void) { return WIFI_CONNECTED_BIT; }
esp_err_t esp32_app_base_start(const esp32_app_base_config_t *config)
{
if (config != NULL) {
s_reset_gpio = config->reset_gpio >= 0 ? config->reset_gpio : RESET_BUTTON_GPIO;
s_reset_active_low = config->reset_active_low;
s_reset_hold_ms = config->reset_hold_ms != 0 ? config->reset_hold_ms : RESET_BUTTON_HOLD_MS;
}
init_nvs();
init_network_stack();
ESP_ERROR_CHECK(verify_station_config_boundaries());
ESP_ERROR_CHECK(wifi_store_load(&s_saved_credentials, &s_saved_credentials_found));
ESP_ERROR_CHECK(xTaskCreate(reset_button_task, "wifi_reset_button", 3072, NULL, 4, NULL) == pdPASS
? ESP_OK : ESP_ERR_NO_MEM);
if (s_saved_credentials_found) {
ESP_LOGI(TAG, "Stored Wi-Fi settings found; starting station mode");
start_wifi_station();
} else {
ESP_LOGI(TAG, "No stored Wi-Fi settings; starting browser provisioning");
start_browser_portal();
}
xEventGroupWaitBits(s_wifi_events, WIFI_CONNECTED_BIT, pdFALSE, pdTRUE, portMAX_DELAY);
ESP_LOGI(TAG, "network is ready (service=%s reset_gpio=%d hold_ms=%lu)",
config != NULL && config->service_prefix != NULL ? config->service_prefix : "default",
config != NULL ? config->reset_gpio : RESET_BUTTON_GPIO,
(unsigned long)(config != NULL ? config->reset_hold_ms : RESET_BUTTON_HOLD_MS));
ESP_ERROR_CHECK(ota_mark_running_app_valid());
ESP_ERROR_CHECK(time_sync_start());
(void)time_sync_wait(pdMS_TO_TICKS((config && config->ntp_initial_wait_seconds ? config->ntp_initial_wait_seconds : 30) * 1000U));
if (config != NULL && config->product_start != NULL) {
ESP_ERROR_CHECK(config->product_start());
}
if (config == NULL || config->health_check == NULL || config->health_check() == ESP_OK) {
ESP_ERROR_CHECK(ota_start_task(s_wifi_events, WIFI_CONNECTED_BIT));
}
while (true) {
vTaskDelay(pdMS_TO_TICKS(30000));
}
}
+65
View File
@@ -0,0 +1,65 @@
#include "mqtt_transport.h"
#include <stdbool.h>
#include <string.h>
#include "esp_crt_bundle.h"
#include "esp_log.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "mqtt_client.h"
static const char *TAG = "mqtt_transport";
static esp_mqtt_client_handle_t s_client;
static mqtt_transport_config_t s_config;
static volatile bool s_connected;
static void handler(void *args, esp_event_base_t base, int32_t id, void *data)
{
(void)args; (void)base;
esp_mqtt_event_handle_t e = data;
switch ((esp_mqtt_event_id_t)id) {
case MQTT_EVENT_CONNECTED:
s_connected = true;
if (s_config.on_connected) s_config.on_connected(s_config.ctx);
break;
case MQTT_EVENT_DISCONNECTED: s_connected = false; break;
case MQTT_EVENT_DATA:
if (s_config.on_message) s_config.on_message(e->topic, e->topic_len, e->data, e->data_len, s_config.ctx);
break;
default: break;
}
}
static void heartbeat_task(void *arg)
{
(void)arg;
while (true) {
vTaskDelay(pdMS_TO_TICKS((s_config.heartbeat_interval_seconds ? s_config.heartbeat_interval_seconds : 60) * 1000U));
if (s_connected && s_config.on_heartbeat) s_config.on_heartbeat(s_config.ctx);
}
}
int mqtt_transport_publish(const char *topic, const char *payload, int qos, bool retain)
{ return s_connected ? esp_mqtt_client_publish(s_client, topic, payload, 0, qos, retain ? 1 : 0) : -1; }
int mqtt_transport_subscribe(const char *topic, int qos)
{ return s_connected ? esp_mqtt_client_subscribe(s_client, topic, qos) : -1; }
esp_err_t mqtt_transport_start(const mqtt_transport_config_t *config)
{
if (!config || !config->broker_uri || !config->broker_uri[0] || s_client) return ESP_OK;
s_config = *config;
bool tls = strncmp(config->broker_uri, "mqtts://", 8) == 0 || strncmp(config->broker_uri, "wss://", 6) == 0;
esp_mqtt_client_config_t mc = {
.broker.address.uri = config->broker_uri,
.broker.verification.crt_bundle_attach = tls ? esp_crt_bundle_attach : NULL,
.credentials.client_id = config->client_id,
.session.last_will.topic = config->last_will_topic,
.session.last_will.msg = config->last_will_payload,
.session.last_will.qos = 1, .session.last_will.retain = 1,
.network.reconnect_timeout_ms = 5000,
};
s_client = esp_mqtt_client_init(&mc);
if (!s_client) return ESP_ERR_NO_MEM;
esp_err_t err = esp_mqtt_client_register_event(s_client, ESP_EVENT_ANY_ID, handler, NULL);
if (err == ESP_OK) err = esp_mqtt_client_start(s_client);
if (err != ESP_OK) { esp_mqtt_client_destroy(s_client); s_client = NULL; return err; }
if (xTaskCreate(heartbeat_task, "mqtt_heartbeat", 4096, NULL, 4, NULL) != pdPASS) return ESP_ERR_NO_MEM;
ESP_LOGI(TAG, "generic MQTT transport started");
return ESP_OK;
}
+190
View File
@@ -0,0 +1,190 @@
#include "ota_update.h"
#include <stdbool.h>
#include <stdint.h>
#include <stdio.h>
#include <string.h>
#include "esp_check.h"
#include "esp_crt_bundle.h"
#include "esp_http_client.h"
#include "esp_https_ota.h"
#include "esp_log.h"
#include "esp_ota_ops.h"
#include "esp_system.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "sdkconfig.h"
#include "ota_version.h"
#define OTA_TASK_STACK_SIZE 8192
#define OTA_TASK_PRIORITY 5
#define OTA_PROGRESS_LOG_STEP (128 * 1024)
static const char *TAG = "sese_ota";
static EventGroupHandle_t s_network_events;
static EventBits_t s_connected_bit;
static TaskHandle_t s_ota_task_handle;
typedef enum {
OTA_CHECK_NO_UPDATE,
OTA_CHECK_FAILED,
OTA_CHECK_INSTALLED,
} ota_check_result_t;
static esp_err_t validate_candidate(const esp_app_desc_t *candidate)
{
const esp_partition_t *running_partition = esp_ota_get_running_partition();
esp_app_desc_t running;
ESP_RETURN_ON_ERROR(esp_ota_get_partition_description(running_partition, &running),
TAG, "Could not read running image descriptor");
if (strncmp(candidate->project_name, running.project_name,
sizeof(candidate->project_name)) != 0) {
ESP_LOGE(TAG, "Rejecting image for project '%s'; expected '%s'",
candidate->project_name, running.project_name);
return ESP_ERR_INVALID_ARG;
}
int version_order = 0;
if (!ota_version_compare(candidate->version, running.version, &version_order)) {
ESP_LOGE(TAG, "OTA versions must use major.minor.patch: running='%s', candidate='%s'",
running.version, candidate->version);
return ESP_ERR_INVALID_VERSION;
}
if (version_order <= 0) {
ESP_LOGI(TAG, "No update: running=%s, candidate=%s", running.version, candidate->version);
return ESP_ERR_INVALID_VERSION;
}
ESP_LOGI(TAG, "New firmware accepted: %s -> %s", running.version, candidate->version);
return ESP_OK;
}
static ota_check_result_t perform_ota_check(void)
{
esp_http_client_config_t http_config = {
.url = CONFIG_SESE_OTA_URL,
.crt_bundle_attach = esp_crt_bundle_attach,
.timeout_ms = 15000,
.keep_alive_enable = true,
};
esp_https_ota_config_t ota_config = {
.http_config = &http_config,
};
ESP_LOGI(TAG, "Checking HTTPS OTA endpoint");
esp_https_ota_handle_t handle = NULL;
esp_err_t err = esp_https_ota_begin(&ota_config, &handle);
if (err != ESP_OK) {
ESP_LOGE(TAG, "OTA connection failed: %s", esp_err_to_name(err));
return OTA_CHECK_FAILED;
}
esp_app_desc_t candidate = {0};
err = esp_https_ota_get_img_desc(handle, &candidate);
if (err != ESP_OK) {
ESP_LOGE(TAG, "Could not read OTA image descriptor: %s", esp_err_to_name(err));
esp_https_ota_abort(handle);
return OTA_CHECK_FAILED;
}
err = validate_candidate(&candidate);
if (err == ESP_ERR_INVALID_VERSION) {
esp_https_ota_abort(handle);
return OTA_CHECK_NO_UPDATE;
}
if (err != ESP_OK) {
esp_https_ota_abort(handle);
return OTA_CHECK_FAILED;
}
int next_progress_log = OTA_PROGRESS_LOG_STEP;
do {
err = esp_https_ota_perform(handle);
int bytes_read = esp_https_ota_get_image_len_read(handle);
if (bytes_read >= next_progress_log) {
ESP_LOGI(TAG, "OTA download progress: %d bytes", bytes_read);
next_progress_log += OTA_PROGRESS_LOG_STEP;
}
} while (err == ESP_ERR_HTTPS_OTA_IN_PROGRESS);
if (err != ESP_OK || !esp_https_ota_is_complete_data_received(handle)) {
ESP_LOGE(TAG, "OTA download incomplete: %s", esp_err_to_name(err));
esp_https_ota_abort(handle);
return OTA_CHECK_FAILED;
}
err = esp_https_ota_finish(handle);
if (err != ESP_OK) {
ESP_LOGE(TAG, "OTA image validation/activation failed: %s", esp_err_to_name(err));
return OTA_CHECK_FAILED;
}
ESP_LOGI(TAG, "OTA update installed; rebooting");
vTaskDelay(pdMS_TO_TICKS(1000));
esp_restart();
return OTA_CHECK_INSTALLED;
}
static void ota_task(void *arg)
{
(void)arg;
vTaskDelay(pdMS_TO_TICKS(CONFIG_SESE_OTA_INITIAL_DELAY_SECONDS * 1000));
while (true) {
xEventGroupWaitBits(s_network_events, s_connected_bit, pdFALSE, pdTRUE, portMAX_DELAY);
ota_check_result_t result = perform_ota_check();
uint32_t delay_minutes = result == OTA_CHECK_FAILED
? CONFIG_SESE_OTA_RETRY_INTERVAL_MINUTES
: CONFIG_SESE_OTA_CHECK_INTERVAL_MINUTES;
vTaskDelay(pdMS_TO_TICKS(delay_minutes * 60U * 1000U));
}
}
esp_err_t ota_mark_running_app_valid(void)
{
#if CONFIG_BOOTLOADER_APP_ROLLBACK_ENABLE
const esp_partition_t *running = esp_ota_get_running_partition();
esp_ota_img_states_t state;
esp_err_t err = esp_ota_get_state_partition(running, &state);
if (err != ESP_OK) {
return err;
}
if (state == ESP_OTA_IMG_PENDING_VERIFY) {
ESP_LOGI(TAG, "Network diagnostic passed; confirming OTA image");
return esp_ota_mark_app_valid_cancel_rollback();
}
#endif
return ESP_OK;
}
esp_err_t ota_start_task(EventGroupHandle_t network_events, EventBits_t connected_bit)
{
#if !CONFIG_SESE_OTA_ENABLE
ESP_LOGI(TAG, "Automatic OTA is disabled by configuration");
return ESP_OK;
#else
if (network_events == NULL || connected_bit == 0) {
return ESP_ERR_INVALID_ARG;
}
if (CONFIG_SESE_OTA_URL[0] == '\0') {
ESP_LOGW(TAG, "Automatic OTA is enabled but SESE_OTA_URL is empty; checks are disabled");
return ESP_OK;
}
if (strncmp(CONFIG_SESE_OTA_URL, "https://", strlen("https://")) != 0) {
ESP_LOGE(TAG, "SESE_OTA_URL must use HTTPS");
return ESP_ERR_INVALID_ARG;
}
if (s_ota_task_handle != NULL) {
return ESP_ERR_INVALID_STATE;
}
s_network_events = network_events;
s_connected_bit = connected_bit;
BaseType_t created = xTaskCreate(ota_task, "https_ota", OTA_TASK_STACK_SIZE,
NULL, OTA_TASK_PRIORITY, &s_ota_task_handle);
return created == pdPASS ? ESP_OK : ESP_ERR_NO_MEM;
#endif
}
+23
View File
@@ -0,0 +1,23 @@
#include "ota_version.h"
#include <stdint.h>
#include <stdio.h>
static bool parse_version(const char *version, uint32_t parsed[3])
{
char trailing;
return sscanf(version, "%u.%u.%u%c", &parsed[0], &parsed[1], &parsed[2], &trailing) == 3;
}
bool ota_version_compare(const char *left, const char *right, int *order)
{
if (!left || !right || !order) return false;
uint32_t a[3], b[3];
if (!parse_version(left, a) || !parse_version(right, b)) return false;
*order = 0;
for (int i = 0; i < 3; ++i) {
if (a[i] > b[i]) { *order = 1; return true; }
if (a[i] < b[i]) { *order = -1; return true; }
}
return true;
}
+68
View File
@@ -0,0 +1,68 @@
#include "portal_self_test.h"
#include <string.h>
#include "esp_http_client.h"
#include "esp_log.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
static const char *TAG = "portal_self_test";
static esp_err_t check_endpoint(const char *url, const char *sentinel)
{
esp_http_client_config_t config = {
.url = url,
.timeout_ms = 3000,
.disable_auto_redirect = true,
};
esp_http_client_handle_t client = esp_http_client_init(&config);
if (client == NULL) {
return ESP_ERR_NO_MEM;
}
esp_err_t err = esp_http_client_open(client, 0);
if (err != ESP_OK) {
esp_http_client_cleanup(client);
return err;
}
int64_t content_length = esp_http_client_fetch_headers(client);
if (content_length < 0 || esp_http_client_get_status_code(client) != 200) {
esp_http_client_close(client);
esp_http_client_cleanup(client);
return ESP_FAIL;
}
char response[512] = {0};
int read = esp_http_client_read_response(client, response, sizeof(response) - 1);
esp_http_client_close(client);
esp_http_client_cleanup(client);
if (read <= 0 || strstr(response, sentinel) == NULL) {
return ESP_FAIL;
}
return ESP_OK;
}
static void self_test_task(void *arg)
{
(void)arg;
vTaskDelay(pdMS_TO_TICKS(500));
esp_err_t root_result = check_endpoint("http://192.168.4.1/", "hidden-ssid");
esp_err_t status_result = check_endpoint("http://192.168.4.1/api/v1/status", "\"state\":\"idle\"");
esp_err_t networks_result = check_endpoint("http://192.168.4.1/api/v1/networks", "\"networks\"");
if (root_result == ESP_OK && status_result == ESP_OK && networks_result == ESP_OK) {
ESP_LOGI(TAG, "Captive portal HTTP and Wi-Fi scan API self-test passed");
} else {
ESP_LOGE(TAG, "Captive portal self-test failed: root=%s status=%s networks=%s",
esp_err_to_name(root_result), esp_err_to_name(status_result),
esp_err_to_name(networks_result));
}
vTaskDelete(NULL);
}
esp_err_t portal_self_test_start(void)
{
return xTaskCreate(self_test_task, "portal_self_test", 4096, NULL, 3, NULL) == pdPASS
? ESP_OK : ESP_ERR_NO_MEM;
}
+39
View File
@@ -0,0 +1,39 @@
<!doctype html>
<html lang="ko">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<meta name="sese-features" content="hidden-ssid">
<title>SESE Controller Wi-Fi 설정</title>
<style>
:root{color-scheme:light;font-family:system-ui,sans-serif}body{margin:0;background:#f3f5f7;color:#17202a}.card{max-width:480px;margin:24px auto;padding:24px;background:#fff;border-radius:16px;box-shadow:0 4px 18px #0002}h1{font-size:1.45rem;margin:0 0 8px}.muted{color:#5d6d7e;font-size:.93rem}label{display:block;margin:18px 0 6px;font-weight:650}select,input,button{box-sizing:border-box;width:100%;font-size:1rem;padding:12px;border-radius:9px;border:1px solid #aeb6bf}button{margin-top:16px;border:0;background:#0866c6;color:white;font-weight:700}button:disabled{background:#9aa5b1}.row{display:flex;gap:8px}.row select{flex:1}.row button{width:auto;margin:0}.status{margin-top:18px;padding:12px;border-radius:9px;background:#eef4fa;white-space:pre-line}.error{background:#fdecec;color:#922b21}.success{background:#e9f7ef;color:#196f3d}.hidden{display:none}</style>
</head>
<body><main class="card">
<h1>SESE Controller Wi-Fi 설정</h1>
<p id="device" class="muted">장치 정보를 확인하고 있습니다.</p>
<label for="network">연결할 2.4GHz Wi-Fi</label>
<div class="row"><select id="network"><option>검색 중…</option></select><button id="scan" type="button">재검색</button></div>
<label><input id="hidden-network" type="checkbox" style="width:auto"> 숨겨진 Wi-Fi 직접 입력</label>
<div id="hidden-fields" class="hidden">
<label for="hidden-ssid">숨겨진 Wi-Fi 이름(SSID)</label>
<input id="hidden-ssid" type="text" maxlength="32" autocomplete="off" autocapitalize="none" spellcheck="false" placeholder="대소문자를 정확히 입력">
<p class="muted">검색 목록에 나타나지 않는 SSID를 정확히 입력해 주세요.</p>
</div>
<label for="password">Wi-Fi 비밀번호</label>
<input id="password" type="password" maxlength="64" autocomplete="new-password">
<label><input id="show" type="checkbox" style="width:auto"> 비밀번호 표시</label>
<button id="connect" type="button">연결</button>
<div id="status" class="status">Wi-Fi를 선택해 주세요.</div>
<p class="muted">이 페이지는 인터넷이 아닌 SESE Controller에서 직접 제공됩니다.</p>
</main>
<script>
let nonce='';let pollTimer=null;
const q=id=>document.getElementById(id);
function setStatus(text,kind=''){const e=q('status');e.textContent=text;e.className='status '+kind}
async function json(url,opt){const r=await fetch(url,opt);const d=await r.json().catch(()=>({}));if(!r.ok)throw new Error(d.error||'요청에 실패했습니다.');return d}
async function status(){try{const d=await json('/api/v1/status');nonce=d.nonce||nonce;q('device').textContent='설정 대상: '+d.device;if(d.state==='connecting'){setStatus('Wi-Fi 연결을 확인하고 있습니다…');q('connect').disabled=true}else if(d.state==='connected'){setStatus('설정이 완료되었습니다.\n이 창은 닫아도 됩니다.','success');q('connect').disabled=true;clearInterval(pollTimer)}else if(d.state==='failed'){setStatus(d.message||'연결하지 못했습니다. 다시 시도해 주세요.','error');q('connect').disabled=false}}catch(e){}}
async function scan(){q('scan').disabled=true;setStatus('주변 Wi-Fi를 검색하고 있습니다…');try{const d=await json('/api/v1/networks');const s=q('network');s.textContent='';for(const n of d.networks){const o=document.createElement('option');o.value=n.ssid;o.textContent=n.ssid+' '+n.rssi+' dBm'+(n.secure?' 🔒':'');s.appendChild(o)}if(!d.networks.length){const o=document.createElement('option');o.value='';o.disabled=true;o.textContent='검색된 Wi-Fi가 없습니다';s.appendChild(o)}setStatus('Wi-Fi를 선택하거나 숨겨진 Wi-Fi를 직접 입력해 주세요.')}catch(e){setStatus(e.message,'error')}finally{q('scan').disabled=q('hidden-network').checked}}
async function connect(){const manual=q('hidden-network').checked;const ssid=manual?q('hidden-ssid').value:q('network').value;const bytes=new TextEncoder().encode(ssid).length;if(!ssid){setStatus(manual?'숨겨진 Wi-Fi 이름을 입력해 주세요.':'Wi-Fi를 선택해 주세요.','error');return}if(bytes>32){setStatus('Wi-Fi 이름은 UTF-8 기준 32바이트 이하여야 합니다.','error');return}q('connect').disabled=true;setStatus('설정을 전송하고 있습니다…');try{await json('/api/v1/config',{method:'POST',headers:{'Content-Type':'application/json','X-SESE-Nonce':nonce},body:JSON.stringify({ssid,password:q('password').value})});q('password').value='';setStatus('Wi-Fi 연결을 확인하고 있습니다…')}catch(e){setStatus(e.message,'error');q('connect').disabled=false}}
q('hidden-network').onchange=()=>{const manual=q('hidden-network').checked;q('hidden-fields').classList.toggle('hidden',!manual);q('network').disabled=manual;q('scan').disabled=manual;if(manual)q('hidden-ssid').focus()};q('show').onchange=()=>q('password').type=q('show').checked?'text':'password';q('scan').onclick=scan;q('connect').onclick=connect;
(async()=>{await status();await scan();pollTimer=setInterval(status,1000)})();
</script></body></html>
+74
View File
@@ -0,0 +1,74 @@
#include "time_sync.h"
#include <stdbool.h>
#include <stdlib.h>
#include <time.h>
#include "esp_log.h"
#include "esp_netif_sntp.h"
#include "sdkconfig.h"
static const char *TAG = "sese_time";
static bool s_initialized;
static void log_local_time(const char *prefix)
{
time_t now;
struct tm local_time;
char timestamp[40];
time(&now);
localtime_r(&now, &local_time);
if (strftime(timestamp, sizeof(timestamp), "%Y-%m-%dT%H:%M:%S%z", &local_time) == 0) {
ESP_LOGW(TAG, "%s, but timestamp formatting failed", prefix);
return;
}
ESP_LOGI(TAG, "%s: %s", prefix, timestamp);
}
static void time_sync_notification(struct timeval *time_value)
{
(void)time_value;
log_local_time("NTP time synchronized");
}
esp_err_t time_sync_start(void)
{
if (s_initialized) {
return ESP_OK;
}
if (setenv("TZ", CONFIG_SESE_TIMEZONE, 1) != 0) {
return ESP_FAIL;
}
tzset();
esp_sntp_config_t config = ESP_NETIF_SNTP_DEFAULT_CONFIG(CONFIG_SESE_NTP_SERVER);
config.sync_cb = time_sync_notification;
esp_err_t err = esp_netif_sntp_init(&config);
if (err != ESP_OK) {
return err;
}
s_initialized = true;
ESP_LOGI(TAG, "NTP started with server '%s' and timezone '%s'",
CONFIG_SESE_NTP_SERVER, CONFIG_SESE_TIMEZONE);
return ESP_OK;
}
esp_err_t time_sync_wait(TickType_t timeout)
{
if (!s_initialized) {
return ESP_ERR_INVALID_STATE;
}
esp_err_t err = esp_netif_sntp_sync_wait(timeout);
if (err == ESP_OK) {
log_local_time("Initial system time ready");
} else if (err == ESP_ERR_TIMEOUT) {
ESP_LOGW(TAG, "Initial NTP synchronization timed out; periodic retries remain active");
} else {
ESP_LOGW(TAG, "Initial NTP synchronization did not complete: %s", esp_err_to_name(err));
}
return err;
}
+416
View File
@@ -0,0 +1,416 @@
#include "web_provisioning.h"
#include <inttypes.h>
#include <stdbool.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "cJSON.h"
#include "dns_server.h"
#include "esp_check.h"
#include "esp_http_server.h"
#include "esp_log.h"
#include "esp_netif.h"
#include "esp_random.h"
#include "esp_wifi.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "lwip/inet.h"
#define REQUEST_BODY_MAX 256
#define NONCE_HEX_LENGTH 32
#define MAX_SCAN_RESULTS 20
extern const char provisioning_start[] asm("_binary_provisioning_html_start");
extern const char provisioning_end[] asm("_binary_provisioning_html_end");
static const char *TAG = "web_provisioning";
static httpd_handle_t s_http_server;
static dns_server_handle_t s_dns_server;
static TaskHandle_t s_timeout_task;
static web_provisioning_config_t s_config;
static portMUX_TYPE s_lock = portMUX_INITIALIZER_UNLOCKED;
static bool s_active;
static web_provisioning_state_t s_state = WEB_PROV_IDLE;
static char s_nonce[NONCE_HEX_LENGTH + 1];
static char s_message[96];
static char s_ip_address[16];
static char s_captive_uri[32];
static const char *state_name(web_provisioning_state_t state)
{
switch (state) {
case WEB_PROV_SCANNING: return "scanning";
case WEB_PROV_CONNECTING: return "connecting";
case WEB_PROV_CONNECTED: return "connected";
case WEB_PROV_FAILED: return "failed";
case WEB_PROV_IDLE:
default: return "idle";
}
}
static void set_state(web_provisioning_state_t state, const char *message)
{
taskENTER_CRITICAL(&s_lock);
s_state = state;
strlcpy(s_message, message == NULL ? "" : message, sizeof(s_message));
taskEXIT_CRITICAL(&s_lock);
}
static void send_json(httpd_req_t *req, cJSON *root, const char *status)
{
char *payload = cJSON_PrintUnformatted(root);
httpd_resp_set_status(req, status);
httpd_resp_set_type(req, "application/json");
httpd_resp_set_hdr(req, "Cache-Control", "no-store");
if (payload == NULL) {
httpd_resp_send(req, "{\"error\":\"out_of_memory\"}", HTTPD_RESP_USE_STRLEN);
} else {
httpd_resp_send(req, payload, HTTPD_RESP_USE_STRLEN);
cJSON_free(payload);
}
}
static esp_err_t root_get_handler(httpd_req_t *req)
{
httpd_resp_set_type(req, "text/html; charset=utf-8");
httpd_resp_set_hdr(req, "Cache-Control", "no-store");
return httpd_resp_send(req, provisioning_start, provisioning_end - provisioning_start);
}
static esp_err_t status_get_handler(httpd_req_t *req)
{
web_provisioning_state_t state;
char message[sizeof(s_message)];
char ip_address[sizeof(s_ip_address)];
taskENTER_CRITICAL(&s_lock);
state = s_state;
strlcpy(message, s_message, sizeof(message));
strlcpy(ip_address, s_ip_address, sizeof(ip_address));
taskEXIT_CRITICAL(&s_lock);
cJSON *root = cJSON_CreateObject();
ESP_RETURN_ON_FALSE(root, ESP_ERR_NO_MEM, TAG, "Could not allocate status JSON");
cJSON_AddStringToObject(root, "state", state_name(state));
cJSON_AddStringToObject(root, "device", s_config.service_name);
cJSON_AddStringToObject(root, "nonce", s_nonce);
if (message[0] != '\0') {
cJSON_AddStringToObject(root, "message", message);
}
if (ip_address[0] != '\0') {
cJSON_AddStringToObject(root, "ip", ip_address);
}
send_json(req, root, "200 OK");
cJSON_Delete(root);
return ESP_OK;
}
static int compare_ap(const void *left, const void *right)
{
const wifi_ap_record_t *a = left;
const wifi_ap_record_t *b = right;
return b->rssi - a->rssi;
}
static esp_err_t networks_get_handler(httpd_req_t *req)
{
if (web_provisioning_is_connecting()) {
httpd_resp_set_status(req, "409 Conflict");
httpd_resp_send(req, "Connection attempt in progress", HTTPD_RESP_USE_STRLEN);
return ESP_OK;
}
set_state(WEB_PROV_SCANNING, "");
esp_err_t err = esp_wifi_scan_start(NULL, true);
if (err != ESP_OK) {
set_state(WEB_PROV_FAILED, "Wi-Fi 검색을 시작하지 못했습니다.");
httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, "Wi-Fi scan failed");
return ESP_OK;
}
uint16_t count = 0;
ESP_ERROR_CHECK_WITHOUT_ABORT(esp_wifi_scan_get_ap_num(&count));
if (count > MAX_SCAN_RESULTS) {
count = MAX_SCAN_RESULTS;
}
wifi_ap_record_t records[MAX_SCAN_RESULTS] = {0};
if (count > 0) {
err = esp_wifi_scan_get_ap_records(&count, records);
if (err != ESP_OK) {
set_state(WEB_PROV_FAILED, "Wi-Fi 검색 결과를 읽지 못했습니다.");
httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, "Wi-Fi scan result failed");
return ESP_OK;
}
qsort(records, count, sizeof(records[0]), compare_ap);
}
cJSON *root = cJSON_CreateObject();
cJSON *networks = cJSON_AddArrayToObject(root, "networks");
ESP_RETURN_ON_FALSE(root && networks, ESP_ERR_NO_MEM, TAG, "Could not allocate scan JSON");
for (uint16_t i = 0; i < count; ++i) {
if (records[i].ssid[0] == '\0') {
continue;
}
bool duplicate = false;
for (uint16_t j = 0; j < i; ++j) {
if (strcmp((const char *)records[i].ssid, (const char *)records[j].ssid) == 0) {
duplicate = true;
break;
}
}
if (duplicate) {
continue;
}
cJSON *network = cJSON_CreateObject();
cJSON_AddStringToObject(network, "ssid", (const char *)records[i].ssid);
cJSON_AddNumberToObject(network, "rssi", records[i].rssi);
cJSON_AddBoolToObject(network, "secure", records[i].authmode != WIFI_AUTH_OPEN);
cJSON_AddItemToArray(networks, network);
}
set_state(WEB_PROV_IDLE, "");
send_json(req, root, "200 OK");
cJSON_Delete(root);
return ESP_OK;
}
static bool nonce_is_valid(httpd_req_t *req)
{
size_t length = httpd_req_get_hdr_value_len(req, "X-SESE-Nonce");
if (length != NONCE_HEX_LENGTH) {
return false;
}
char value[NONCE_HEX_LENGTH + 1];
return httpd_req_get_hdr_value_str(req, "X-SESE-Nonce", value, sizeof(value)) == ESP_OK &&
strcmp(value, s_nonce) == 0;
}
static esp_err_t config_post_handler(httpd_req_t *req)
{
if (!nonce_is_valid(req)) {
httpd_resp_send_err(req, HTTPD_403_FORBIDDEN, "Invalid session");
return ESP_OK;
}
if (req->content_len <= 0 || req->content_len > REQUEST_BODY_MAX) {
httpd_resp_send_err(req, HTTPD_400_BAD_REQUEST, "Invalid request size");
return ESP_OK;
}
if (web_provisioning_is_connecting()) {
httpd_resp_set_status(req, "409 Conflict");
httpd_resp_send(req, "Connection attempt in progress", HTTPD_RESP_USE_STRLEN);
return ESP_OK;
}
char body[REQUEST_BODY_MAX + 1];
size_t received = 0;
while (received < req->content_len) {
int result = httpd_req_recv(req, body + received, req->content_len - received);
if (result <= 0) {
memset(body, 0, sizeof(body));
httpd_resp_send_err(req, HTTPD_400_BAD_REQUEST, "Incomplete request");
return ESP_OK;
}
received += result;
}
body[received] = '\0';
cJSON *root = cJSON_ParseWithLength(body, received);
memset(body, 0, sizeof(body));
if (root == NULL) {
httpd_resp_send_err(req, HTTPD_400_BAD_REQUEST, "Invalid JSON");
return ESP_OK;
}
cJSON *ssid = cJSON_GetObjectItemCaseSensitive(root, "ssid");
cJSON *password = cJSON_GetObjectItemCaseSensitive(root, "password");
if (!cJSON_IsString(ssid) || !cJSON_IsString(password)) {
cJSON_Delete(root);
httpd_resp_send_err(req, HTTPD_400_BAD_REQUEST, "SSID and password are required");
return ESP_OK;
}
size_t ssid_len = strlen(ssid->valuestring);
size_t password_len = strlen(password->valuestring);
if (ssid_len == 0 || ssid_len > WIFI_STORE_SSID_MAX_LEN ||
password_len > WIFI_STORE_PASSWORD_MAX_LEN) {
cJSON_Delete(root);
httpd_resp_send_err(req, HTTPD_400_BAD_REQUEST, "Credential length is invalid");
return ESP_OK;
}
wifi_store_credentials_t credentials = {0};
strlcpy(credentials.ssid, ssid->valuestring, sizeof(credentials.ssid));
strlcpy(credentials.password, password->valuestring, sizeof(credentials.password));
cJSON_Delete(root);
ESP_LOGI(TAG, "Received browser Wi-Fi configuration for SSID '%s' (password not logged)",
credentials.ssid);
set_state(WEB_PROV_CONNECTING, "");
esp_err_t err = s_config.apply_credentials(&credentials);
memset(&credentials, 0, sizeof(credentials));
if (err != ESP_OK) {
set_state(WEB_PROV_FAILED, "연결 요청을 시작하지 못했습니다.");
httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, "Could not start connection");
return ESP_OK;
}
cJSON *response = cJSON_CreateObject();
cJSON_AddStringToObject(response, "state", "connecting");
send_json(req, response, "202 Accepted");
cJSON_Delete(response);
return ESP_OK;
}
static esp_err_t not_found_handler(httpd_req_t *req, httpd_err_code_t error)
{
(void)error;
httpd_resp_set_status(req, "303 See Other");
httpd_resp_set_hdr(req, "Location", "/");
return httpd_resp_send(req, "Open SESE Controller setup", HTTPD_RESP_USE_STRLEN);
}
static void generate_nonce(void)
{
for (size_t i = 0; i < NONCE_HEX_LENGTH / 8; ++i) {
snprintf(s_nonce + (i * 8), 9, "%08" PRIx32, esp_random());
}
s_nonce[NONCE_HEX_LENGTH] = '\0';
}
static esp_err_t configure_captive_portal_dhcp(void)
{
esp_netif_t *ap = esp_netif_get_handle_from_ifkey("WIFI_AP_DEF");
ESP_RETURN_ON_FALSE(ap, ESP_ERR_NOT_FOUND, TAG, "SoftAP network interface missing");
esp_netif_ip_info_t ip_info;
ESP_RETURN_ON_ERROR(esp_netif_get_ip_info(ap, &ip_info), TAG, "Could not read SoftAP IP");
snprintf(s_captive_uri, sizeof(s_captive_uri), "http://" IPSTR, IP2STR(&ip_info.ip));
ESP_ERROR_CHECK_WITHOUT_ABORT(esp_netif_dhcps_stop(ap));
esp_err_t err = esp_netif_dhcps_option(ap, ESP_NETIF_OP_SET,
ESP_NETIF_CAPTIVEPORTAL_URI,
s_captive_uri, strlen(s_captive_uri));
ESP_ERROR_CHECK_WITHOUT_ABORT(esp_netif_dhcps_start(ap));
return err;
}
static void timeout_task(void *argument)
{
(void)argument;
vTaskDelay(pdMS_TO_TICKS(s_config.timeout_seconds * 1000U));
if (web_provisioning_is_active()) {
ESP_LOGW(TAG, "Browser provisioning window expired");
if (s_config.expired != NULL) {
s_config.expired();
}
}
s_timeout_task = NULL;
vTaskDelete(NULL);
}
esp_err_t web_provisioning_start(const web_provisioning_config_t *config)
{
if (config == NULL || config->service_name == NULL ||
config->apply_credentials == NULL || config->timeout_seconds == 0 || s_active) {
return ESP_ERR_INVALID_ARG;
}
s_config = *config;
generate_nonce();
memset(s_message, 0, sizeof(s_message));
memset(s_ip_address, 0, sizeof(s_ip_address));
set_state(WEB_PROV_IDLE, "");
httpd_config_t http_config = HTTPD_DEFAULT_CONFIG();
http_config.max_open_sockets = 4;
http_config.lru_purge_enable = true;
http_config.max_uri_handlers = 4;
ESP_RETURN_ON_ERROR(httpd_start(&s_http_server, &http_config), TAG, "HTTP server start failed");
const httpd_uri_t root = {.uri = "/", .method = HTTP_GET, .handler = root_get_handler};
const httpd_uri_t status = {.uri = "/api/v1/status", .method = HTTP_GET, .handler = status_get_handler};
const httpd_uri_t networks = {.uri = "/api/v1/networks", .method = HTTP_GET, .handler = networks_get_handler};
const httpd_uri_t apply = {.uri = "/api/v1/config", .method = HTTP_POST, .handler = config_post_handler};
ESP_ERROR_CHECK(httpd_register_uri_handler(s_http_server, &root));
ESP_ERROR_CHECK(httpd_register_uri_handler(s_http_server, &status));
ESP_ERROR_CHECK(httpd_register_uri_handler(s_http_server, &networks));
ESP_ERROR_CHECK(httpd_register_uri_handler(s_http_server, &apply));
ESP_ERROR_CHECK(httpd_register_err_handler(s_http_server, HTTPD_404_NOT_FOUND, not_found_handler));
ESP_RETURN_ON_ERROR(configure_captive_portal_dhcp(), TAG, "DHCP captive portal setup failed");
dns_server_config_t dns_config = DNS_SERVER_CONFIG_SINGLE("*", "WIFI_AP_DEF");
s_dns_server = start_dns_server(&dns_config);
if (s_dns_server == NULL) {
httpd_stop(s_http_server);
s_http_server = NULL;
return ESP_FAIL;
}
taskENTER_CRITICAL(&s_lock);
s_active = true;
taskEXIT_CRITICAL(&s_lock);
if (xTaskCreate(timeout_task, "portal_timeout", 3072, NULL, 4, &s_timeout_task) != pdPASS) {
web_provisioning_stop();
return ESP_ERR_NO_MEM;
}
ESP_LOGI(TAG, "Browser provisioning portal ready at %s", s_captive_uri);
return ESP_OK;
}
void web_provisioning_stop(void)
{
taskENTER_CRITICAL(&s_lock);
bool was_active = s_active;
s_active = false;
taskEXIT_CRITICAL(&s_lock);
if (!was_active) {
return;
}
if (s_timeout_task != NULL && s_timeout_task != xTaskGetCurrentTaskHandle()) {
vTaskDelete(s_timeout_task);
s_timeout_task = NULL;
}
if (s_dns_server != NULL) {
stop_dns_server(s_dns_server);
s_dns_server = NULL;
}
if (s_http_server != NULL) {
httpd_stop(s_http_server);
s_http_server = NULL;
}
ESP_LOGI(TAG, "Browser provisioning portal stopped");
}
bool web_provisioning_is_active(void)
{
taskENTER_CRITICAL(&s_lock);
bool active = s_active;
taskEXIT_CRITICAL(&s_lock);
return active;
}
bool web_provisioning_is_connecting(void)
{
taskENTER_CRITICAL(&s_lock);
bool connecting = s_state == WEB_PROV_CONNECTING;
taskEXIT_CRITICAL(&s_lock);
return connecting;
}
void web_provisioning_report_connected(const char *ip_address)
{
taskENTER_CRITICAL(&s_lock);
strlcpy(s_ip_address, ip_address == NULL ? "" : ip_address, sizeof(s_ip_address));
taskEXIT_CRITICAL(&s_lock);
set_state(WEB_PROV_CONNECTED, "설정이 완료되었습니다. 이 창은 닫아도 됩니다.");
}
void web_provisioning_report_failure(const char *reason)
{
set_state(WEB_PROV_FAILED, reason == NULL ? "Wi-Fi에 연결하지 못했습니다." : reason);
}
+114
View File
@@ -0,0 +1,114 @@
#include "wifi_store.h"
#include <string.h>
#include "nvs.h"
#define WIFI_STORE_NAMESPACE "wifi_cfg"
#define WIFI_STORE_KEY "record"
#define WIFI_STORE_MAGIC 0x53455345U
#define WIFI_STORE_SCHEMA_VERSION 1U
typedef struct {
uint32_t magic;
uint16_t schema_version;
uint16_t reserved;
wifi_store_credentials_t credentials;
} wifi_store_record_t;
static bool credentials_valid(const wifi_store_credentials_t *credentials)
{
if (credentials == NULL) {
return false;
}
size_t ssid_len = strnlen(credentials->ssid, sizeof(credentials->ssid));
size_t password_len = strnlen(credentials->password, sizeof(credentials->password));
return ssid_len > 0 && ssid_len <= WIFI_STORE_SSID_MAX_LEN &&
password_len <= WIFI_STORE_PASSWORD_MAX_LEN;
}
esp_err_t wifi_store_load(wifi_store_credentials_t *credentials, bool *found)
{
if (credentials == NULL || found == NULL) {
return ESP_ERR_INVALID_ARG;
}
*found = false;
memset(credentials, 0, sizeof(*credentials));
nvs_handle_t handle;
esp_err_t err = nvs_open(WIFI_STORE_NAMESPACE, NVS_READONLY, &handle);
if (err == ESP_ERR_NVS_NOT_FOUND) {
return ESP_OK;
}
if (err != ESP_OK) {
return err;
}
wifi_store_record_t record = {0};
size_t size = sizeof(record);
err = nvs_get_blob(handle, WIFI_STORE_KEY, &record, &size);
nvs_close(handle);
if (err == ESP_ERR_NVS_NOT_FOUND) {
return ESP_OK;
}
if (err != ESP_OK) {
return err;
}
if (size != sizeof(record) || record.magic != WIFI_STORE_MAGIC ||
record.schema_version != WIFI_STORE_SCHEMA_VERSION ||
!credentials_valid(&record.credentials)) {
return ESP_ERR_INVALID_STATE;
}
*credentials = record.credentials;
*found = true;
return ESP_OK;
}
esp_err_t wifi_store_save(const wifi_store_credentials_t *credentials)
{
if (!credentials_valid(credentials)) {
return ESP_ERR_INVALID_ARG;
}
wifi_store_record_t record = {
.magic = WIFI_STORE_MAGIC,
.schema_version = WIFI_STORE_SCHEMA_VERSION,
.credentials = *credentials,
};
nvs_handle_t handle = 0;
esp_err_t err = nvs_open(WIFI_STORE_NAMESPACE, NVS_READWRITE, &handle);
if (err == ESP_OK) {
err = nvs_set_blob(handle, WIFI_STORE_KEY, &record, sizeof(record));
}
if (err == ESP_OK) {
err = nvs_commit(handle);
}
if (handle) {
nvs_close(handle);
}
return err;
}
esp_err_t wifi_store_erase(void)
{
nvs_handle_t handle = 0;
esp_err_t err = nvs_open(WIFI_STORE_NAMESPACE, NVS_READWRITE, &handle);
if (err == ESP_ERR_NVS_NOT_FOUND) {
return ESP_OK;
}
if (err != ESP_OK) {
return err;
}
err = nvs_erase_all(handle);
if (err == ESP_OK) {
err = nvs_commit(handle);
}
nvs_close(handle);
return err;
}