AP menu
All checks were successful
ESP32 Build & Release / build (push) Successful in 6m40s

This commit is contained in:
2026-09-12 13:47:34 +02:00
parent d691febfe4
commit 3708674bbb
14 changed files with 289 additions and 120 deletions

View File

@@ -22,6 +22,7 @@
#include "nvs_flash.h"
#include "nvs.h"
#include "cJSON.h"
#include "wifi_manager.h"
#include "ota_manager.h"
static const char *TAG = "ota";
@@ -30,7 +31,42 @@ static const char *TAG = "ota";
#define GITEA_RELEASE_API_URL "https://git.rasmusbendtsen.dk/api/v1/repos/rasmus/MTGcompanion/releases/latest"
#define GITEA_FIRMWARE_FALLBACK_URL "https://git.rasmusbendtsen.dk/rasmus/MTGcompanion/releases/download/latest/mtg-rfid-companion.bin"
#define GITEA_API_BUF_SIZE (8192)
#define GITEA_API_BUF_SIZE (5120)
/* Let's Encrypt ISRG Root X1 CA Certificate (valid through 2035) */
static const char s_isrg_root_x1_pem[] =
"-----BEGIN CERTIFICATE-----\n"
"MIIFazCCA1OgAwIBAgIRAIIQz7DSQONZRGPgu2OCiwAwDQYJKoZIhvcNAQELBQAw\n"
"TzELMAkGA1UEBhMCVVMxKTAnBgNVBAoTIEludGVybmV0IFNlY3VyaXR5IFJlc2Vh\n"
"cmNoIEdyb3VwMRUwEwYDVQQDEwxJU1JHIFJvb3QgWDEwHhcNMTUwNjA0MTEwNDM4\n"
"WhcNMzUwNjA0MTEwNDM4WjBPMQswCQYDVQQGEwJVUzEpMCcGA1UEChMgSW50ZXJu\n"
"ZXQgU2VjdXJpdHkgUmVzZWFyY2ggR3JvdXAxFTATBgNVBAMTDElTUkcgUm9vdCBY\n"
"MTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAK3oJHP0FDfzm54rVygc\n"
"h77ct984kIxuPOZXoHj3dcKi/vVqbvYATyjb3miGbESTtrFj/RQSa78f0uoxmyF+\n"
"0TM8ukj13Xnfs7j/EvEhmkvBioZxaUpmZmyPfjxwv60pIgbz5MDmgK7iS4+3mX6U\n"
"A5/TR5d8mUgjU+g4rk8Kb4Mu0UlXjIB0ttov0DiNewNwIRt18jA8+o+u3dpjq+sW\n"
"T8KOEUt+zwvo/7V3LvSye0rgTBIlDHCAymg4VMk7BPZ7hm/ELNKjD+Jo2FR3qyHB\n"
"5T0Y3HsLuJvW5iB4YlcNHlsdu87kGJ55tukmi8mxdAQ4Q7e2RCOFvu396j3x+UC\n"
"B5iPNgiV5+I3lg02dZ77DnKxHZu8A/lJBdiB3QW0KtZB6awBdpUKD9jf1b0SHzUv\n"
"KBds0pjBqAlkd25HN7rOrFleaJ1/ctaJxQZBKT5ZPt0m9STJEadao0xAH0ahmbWn\n"
"OlFuhjuefXKnEgV4We0+UXgVCwOPjdAvBbI+e0ocS3MFEvzG6uBQE3xDk3SzynTn\n"
"jh8BCNAw1FtxNrQHusEwMFxIt4I7mKZ9YIqioymCzLq9gwQbooMDQaHWBfEbwrbw\n"
"qHyGO0aoSCqI3Haadr8faqU9GY/rOPNk3sgrDQoo//fb4hVC1CLQJ13hef4Y53CI\n"
"rU7m2Ys6xt0nUW7/vGT1M0NPAgMBAAGjQjBAMA4GA1UdDwEB/wQEAwIBBjAPBgNV\n"
"HRMBAf8EBTADAQH/MB0GA1UdDgQWBBR5tFnme7bl5AFzgAiIyBpY9umbbjANBgkq\n"
"hkiG9w0BAQsFAAOCAgEAVR9YqbyyqFDQDLHYGmkgJykIrGF1XIpu+ILlaS/V9lZL\n"
"ubhzEFnTIZd+50xx+7LSYK05qAvqFyFWhfFQDlnrzuBZ6brJFe+GnY+EgPbk6ZGQ\n"
"3BebYhtF8GaV0nxvwuo77x/Py9auJ/GpsMiu/X1+mvoiBOv/2X/qkSsisRcOj/KK\n"
"NFtY2PwByVS5uCbMiogzihDyC3+6WVwW6LLv3xLfHTjuCvjHIInNzktHCgKQ5ORA\n"
"zI4JMPJ+GslWYHb4phowim57iaztXOoJwTdwJx4nLCgdNbOhdjsnvzqvHu7UrTkX\n"
"WStAmzOVyyghqpZXjFaH3pO3JLF+l+/+sKAIuvtd7u+Nxe5AW0wdeRlN8NwdCjNP\n"
"ElpzVmbUq4JUagEiuTDkHzsxHpFKVK7q4+63SM1N95R1NbdWhscdCb+ZAJzVcoyi\n"
"3B43njTOQ5yOf+1CceWxG1bQVs5ZufpsMljq4Ui0/1lvh+wjChP4kqKOJ2qxq4Rg\n"
"qsahDYVvTH9w7jXbyLeiNdd8XM2w9U/t7y0Ff/9yi0GE44Za4rF2LN9d11TPAmRG\n"
"unUHBcnWEvgJBQl9nJEiU0Zsnvgc/ubhPgXRR4Xq37Z0j4r7g1SgEEzwxA57demy\n"
"PxgcYxn/eR44/KJ4EBs+lVDR3veyJm+kXQ99b21/+jh5Xos1AnX5iItreGCc=\n"
"-----END CERTIFICATE-----\n";
static SemaphoreHandle_t s_ota_mux = NULL;
static ota_status_t s_status = OTA_STATUS_IDLE;
@@ -215,6 +251,14 @@ static void ota_check_task(void *arg)
ESP_LOGI(TAG, "Querying Gitea release API: %s", GITEA_RELEASE_API_URL);
if (wifi_manager_acquire_net_lock(pdMS_TO_TICKS(15000)) != ESP_OK) {
ESP_LOGW(TAG, "Network lock busy, cannot start OTA check");
set_state(OTA_STATUS_FAILED, "Network busy");
s_ota_task_handle = NULL;
vTaskDelete(NULL);
return;
}
http_rx_buf_t rx = {
.buf = malloc(GITEA_API_BUF_SIZE),
.len = 0,
@@ -222,6 +266,7 @@ static void ota_check_task(void *arg)
};
if (!rx.buf) {
wifi_manager_release_net_lock();
set_state(OTA_STATUS_FAILED, "Out of memory");
s_ota_task_handle = NULL;
vTaskDelete(NULL);
@@ -232,15 +277,16 @@ static void ota_check_task(void *arg)
esp_http_client_config_t cfg = {
.url = GITEA_RELEASE_API_URL,
.transport_type = HTTP_TRANSPORT_OVER_SSL,
.timeout_ms = 10000,
.timeout_ms = 15000,
.event_handler = http_rx_event_handler,
.user_data = &rx,
.buffer_size = 2048,
.crt_bundle_attach = esp_crt_bundle_attach,
.buffer_size = 1024,
.cert_pem = s_isrg_root_x1_pem,
};
esp_http_client_handle_t client = esp_http_client_init(&cfg);
if (!client) {
wifi_manager_release_net_lock();
free(rx.buf);
set_state(OTA_STATUS_FAILED, "HTTP init failed");
s_ota_task_handle = NULL;
@@ -255,6 +301,8 @@ static void ota_check_task(void *arg)
esp_err_t ret = esp_http_client_perform(client);
int status_code = esp_http_client_get_status_code(client);
esp_http_client_cleanup(client);
wifi_manager_release_net_lock();
if (ret != ESP_OK || status_code != 200) {
char err_msg[64];
@@ -415,6 +463,14 @@ static void ota_download_task(void *arg)
s_bytes_read = 0;
s_total_bytes = s_release.binary_size;
if (wifi_manager_acquire_net_lock(pdMS_TO_TICKS(15000)) != ESP_OK) {
ESP_LOGW(TAG, "Network lock busy, cannot start OTA download");
set_state(OTA_STATUS_FAILED, "Network busy");
s_ota_task_handle = NULL;
vTaskDelete(NULL);
return;
}
set_state(OTA_STATUS_DOWNLOADING, "Starting download...");
ESP_LOGI(TAG, "Starting HTTPS OTA from: %s", s_release.download_url);
@@ -424,7 +480,7 @@ static void ota_download_task(void *arg)
.timeout_ms = 30000,
.keep_alive_enable = true,
.buffer_size = 2048,
.crt_bundle_attach = esp_crt_bundle_attach,
.cert_pem = s_isrg_root_x1_pem,
};
esp_https_ota_config_t ota_cfg = {
@@ -434,6 +490,7 @@ static void ota_download_task(void *arg)
esp_https_ota_handle_t ota_handle = NULL;
esp_err_t err = esp_https_ota_begin(&ota_cfg, &ota_handle);
if (err != ESP_OK || ota_handle == NULL) {
wifi_manager_release_net_lock();
ESP_LOGE(TAG, "esp_https_ota_begin failed: %s", esp_err_to_name(err));
set_state(OTA_STATUS_FAILED, "Failed to connect to image");
s_ota_task_handle = NULL;
@@ -448,6 +505,7 @@ static void ota_download_task(void *arg)
if (memcmp(new_app_info.app_elf_sha256, running->app_elf_sha256, sizeof(new_app_info.app_elf_sha256)) == 0) {
ESP_LOGW(TAG, "Incoming image has identical ELF SHA-256 as running app - aborting flash write");
esp_https_ota_abort(ota_handle);
wifi_manager_release_net_lock();
set_state(OTA_STATUS_UP_TO_DATE, "Firmware is already up to date");
s_ota_task_handle = NULL;
vTaskDelete(NULL);
@@ -459,6 +517,7 @@ static void ota_download_task(void *arg)
if (s_cancel_requested) {
ESP_LOGW(TAG, "OTA canceled by user");
esp_https_ota_abort(ota_handle);
wifi_manager_release_net_lock();
set_state(OTA_STATUS_IDLE, "Update canceled");
s_ota_task_handle = NULL;
vTaskDelete(NULL);
@@ -481,6 +540,7 @@ static void ota_download_task(void *arg)
if (err == ESP_OK) {
if (esp_https_ota_is_complete_data_received(ota_handle)) {
esp_err_t finish_err = esp_https_ota_finish(ota_handle);
wifi_manager_release_net_lock();
if (finish_err == ESP_OK) {
/* Store installed release hash in NVS */
nvs_handle_t nvs_h;
@@ -502,11 +562,13 @@ static void ota_download_task(void *arg)
}
} else {
esp_https_ota_abort(ota_handle);
wifi_manager_release_net_lock();
set_state(OTA_STATUS_FAILED, "Incomplete download");
}
} else {
ESP_LOGE(TAG, "esp_https_ota_perform failed: %s", esp_err_to_name(err));
esp_https_ota_abort(ota_handle);
wifi_manager_release_net_lock();
set_state(OTA_STATUS_FAILED, "Flash write failed");
}

View File

@@ -20,7 +20,7 @@ static const char *TAG = "rfid";
* Keep this <= 15 chars and in sync with the rc522 library's
* xTaskCreate("rc522_poll", ...). */
#define RC522_TASK_NAME "rc522_poll"
#define RC522_POLL_INTERVAL_MS (100)
#define RC522_POLL_INTERVAL_MS (125)
#define RC522_TASK_STACK_SIZE (6 * 1024)
#define RC522_TASK_PRIORITY (1)

View File

@@ -148,6 +148,12 @@ static esp_err_t http_event_handler(esp_http_client_event_t *evt)
static esp_err_t http_request(const char *url, http_sink_t *sink)
{
if (wifi_manager_acquire_net_lock(pdMS_TO_TICKS(15000)) != ESP_OK) {
ESP_LOGW(TAG, "Network lock busy (e.g. OTA update in progress), deferring fetch %s", url);
return ESP_ERR_TIMEOUT;
}
esp_err_t overall_ret = ESP_FAIL;
for (int attempt = 0; attempt < MAX_ATTEMPTS; attempt++) {
if (sink->file != NULL) {
rewind(sink->file);
@@ -168,7 +174,8 @@ static esp_err_t http_request(const char *url, http_sink_t *sink)
};
esp_http_client_handle_t client = esp_http_client_init(&cfg);
if (client == NULL) {
return ESP_ERR_NO_MEM;
overall_ret = ESP_ERR_NO_MEM;
break;
}
esp_http_client_set_method(client, HTTP_METHOD_GET);
@@ -181,17 +188,21 @@ static esp_err_t http_request(const char *url, http_sink_t *sink)
ESP_LOGD(TAG, "GET %s -> %s (status %d)", url, esp_err_to_name(ret), status);
if (ret == ESP_OK && status >= 200 && status < 300) {
return ESP_OK;
overall_ret = ESP_OK;
break;
}
if (ret == ESP_OK && (status < 200 || status >= 300)) {
ESP_LOGW(TAG, "HTTP GET returned status %d for %s", status, url);
}
if (sink->err != ESP_OK) {
return sink->err;
overall_ret = sink->err;
break;
}
vTaskDelay(pdMS_TO_TICKS(500));
}
return ESP_FAIL;
wifi_manager_release_net_lock();
return overall_ret;
}
static esp_err_t http_get_json(const char *url, char **out_buf)

View File

@@ -17,20 +17,6 @@ static const char *TAG = "storage";
#define STORAGE_PARTITION_LABEL "storage"
static const char SAMPLE_MAPPINGS[] =
"{\n"
" \"04A2B3C4\": \"Sol Ring\",\n"
" \"04E1F2A3\": \"Atraxa, Praetors' Voice\"\n"
"}\n";
static const char SAMPLE_CARD[] =
"{\n"
" \"name\": \"Sol Ring\",\n"
" \"mana_cost\": \"{1}\",\n"
" \"type\": \"Artifact\",\n"
" \"oracle\": \"Tap: Add CC.\",\n"
" \"price_usd\": \"1.25\"\n"
"}\n";
bool storage_file_exists(const char *path)
{
@@ -222,22 +208,12 @@ esp_err_t storage_manager_lookup_name(const char *uid, char **out)
static esp_err_t seed_defaults(void)
{
/* Create /spiffs/cards directory */
/* Ensure /spiffs/cards directory exists */
mkdir(STORAGE_CARDS_DIR, 0755);
if (!storage_file_exists(STORAGE_MAPPINGS)) {
ESP_LOGI(TAG, "seeding %s", STORAGE_MAPPINGS);
esp_err_t ret = storage_write_text(STORAGE_MAPPINGS, SAMPLE_MAPPINGS);
if (ret != ESP_OK) {
return ret;
}
}
char sample_path[64];
snprintf(sample_path, sizeof(sample_path), STORAGE_CARDS_DIR "/sample.json");
if (!storage_file_exists(sample_path)) {
ESP_LOGI(TAG, "seeding %s", sample_path);
esp_err_t ret = storage_write_text(sample_path, SAMPLE_CARD);
ESP_LOGI(TAG, "Creating empty %s", STORAGE_MAPPINGS);
esp_err_t ret = storage_write_text(STORAGE_MAPPINGS, "{\n}\n");
if (ret != ESP_OK) {
return ret;
}

View File

@@ -887,34 +887,45 @@ static void view_storage(void)
lv_obj_set_style_pad_all(stat_box, 8, 0);
lv_obj_set_scrollbar_mode(stat_box, LV_SCROLLBAR_MODE_OFF);
lv_obj_t *key_lbl = lv_label_create(stat_box);
lv_obj_set_width(key_lbl, 125);
lv_obj_set_style_text_font(key_lbl, &lv_font_montserrat_14, 0);
lv_obj_set_style_text_color(key_lbl, lv_color_hex(0x00E5FF), 0);
lv_label_set_text(key_lbl,
"CACHED CARDS:\n"
"ART CROPS:\n\n"
"STORAGE USED:\n"
"PERCENT USED:\n"
"DECK CAPACITY:");
lv_obj_align(key_lbl, LV_ALIGN_TOP_LEFT, 0, 0);
struct {
const char *key;
char val[32];
} stat_rows[5];
snprintf(stat_rows[0].val, sizeof(stat_rows[0].val), "%d", json_c);
stat_rows[0].key = "CACHED CARDS:";
snprintf(stat_rows[1].val, sizeof(stat_rows[1].val), "%d", jpg_c);
stat_rows[1].key = "ART CROPS:";
snprintf(stat_rows[2].val, sizeof(stat_rows[2].val), "%d / %d KB", (int)(used_b / 1024), (int)(total_b / 1024));
stat_rows[2].key = "STORAGE USED:";
lv_obj_t *val_lbl = lv_label_create(stat_box);
lv_obj_set_width(val_lbl, 70);
lv_obj_set_style_text_font(val_lbl, &lv_font_montserrat_14, 0);
lv_obj_set_style_text_color(val_lbl, lv_color_hex(COLOR_AMBER), 0);
lv_obj_set_style_text_align(val_lbl, LV_TEXT_ALIGN_RIGHT, 0);
int percent = total_b > 0 ? (int)((used_b * 100) / total_b) : 0;
lv_label_set_text_fmt(val_lbl,
"%d\n"
"%d\n\n"
"%d KB\n"
"%d%%\n"
"~120-140",
json_c, jpg_c,
(int)(used_b / 1024),
percent);
lv_obj_align(val_lbl, LV_ALIGN_TOP_RIGHT, 0, 0);
snprintf(stat_rows[3].val, sizeof(stat_rows[3].val), "%d%%", percent);
stat_rows[3].key = "PERCENT USED:";
snprintf(stat_rows[4].val, sizeof(stat_rows[4].val), "~100 (EDH)");
stat_rows[4].key = "DECK CAPACITY:";
for (int i = 0; i < 5; i++) {
int y_pos = (i < 2) ? (i * 20) : (i * 20 + 8);
lv_obj_t *k = lv_label_create(stat_box);
lv_label_set_text(k, stat_rows[i].key);
lv_obj_set_style_text_font(k, &lv_font_montserrat_14, 0);
lv_obj_set_style_text_color(k, lv_color_hex(0x00E5FF), 0);
lv_obj_align(k, LV_ALIGN_TOP_LEFT, 0, y_pos);
lv_obj_t *v = lv_label_create(stat_box);
lv_label_set_text(v, stat_rows[i].val);
lv_obj_set_width(v, 90);
lv_label_set_long_mode(v, LV_LABEL_LONG_DOT);
lv_obj_set_style_text_font(v, &lv_font_montserrat_14, 0);
lv_obj_set_style_text_color(v, lv_color_hex(COLOR_AMBER), 0);
lv_obj_set_style_text_align(v, LV_TEXT_ALIGN_RIGHT, 0);
lv_obj_align(v, LV_ALIGN_TOP_RIGHT, 0, y_pos);
}
/* Action 0: Clear Cards Button */
lv_obj_t *btn0 = lv_obj_create(lv_scr_act());
@@ -990,54 +1001,66 @@ static void view_device_info(void)
const char *ssid = wifi_manager_get_connected_ssid();
int rssi = wifi_manager_get_rssi();
char wifi_line[32];
char wifi_line[64];
if (wifi_manager_connected() && ssid && strlen(ssid) > 0) {
snprintf(wifi_line, sizeof(wifi_line), "%s (%ddB)", ssid, rssi);
} else {
snprintf(wifi_line, sizeof(wifi_line), "Disconnected");
}
lv_obj_t *lbl_keys = lv_label_create(box);
lv_obj_set_width(lbl_keys, 75);
lv_obj_set_style_text_font(lbl_keys, &lv_font_montserrat_14, 0);
lv_obj_set_style_text_color(lbl_keys, lv_color_hex(0x00E5FF), 0);
lv_label_set_text(lbl_keys,
"VERSION:\n"
"WI-FI:\n"
"IP ADDR:\n"
"HOTSPOT:\n"
"STORAGE:\n"
"CARDS:\n"
"FREE RAM:\n"
"ENCODER:\n"
"HARDWARE:\n"
"RFID:");
lv_obj_align(lbl_keys, LV_ALIGN_TOP_LEFT, 0, 0);
struct {
const char *key;
char val[64];
} rows[10];
lv_obj_t *lbl_vals = lv_label_create(box);
lv_obj_set_width(lbl_vals, 117);
lv_obj_set_style_text_font(lbl_vals, &lv_font_montserrat_14, 0);
lv_obj_set_style_text_color(lbl_vals, lv_color_hex(COLOR_AMBER), 0);
lv_obj_set_style_text_align(lbl_vals, LV_TEXT_ALIGN_RIGHT, 0);
lv_label_set_text_fmt(lbl_vals,
"%s\n"
"%s\n"
"%s\n"
"192.168.4.1\n"
"%d / %d KB\n"
"%d cached\n"
"%d KB\n"
"%d steps\n"
"ESP32+ST7789\n"
"MFRC522",
ota_manager_get_current_version(),
wifi_line,
wifi_manager_get_ip(),
(int)(used_b / 1024), (int)(total_b / 1024),
json_c,
(int)(free_heap / 1024),
(int)encoder_get_steps_per_detent());
lv_obj_align(lbl_vals, LV_ALIGN_TOP_RIGHT, 0, 0);
snprintf(rows[0].val, sizeof(rows[0].val), "%s", ota_manager_get_current_version());
rows[0].key = "VERSION:";
snprintf(rows[1].val, sizeof(rows[1].val), "%s", wifi_line);
rows[1].key = "WI-FI:";
snprintf(rows[2].val, sizeof(rows[2].val), "%s", wifi_manager_get_ip());
rows[2].key = "IP ADDR:";
snprintf(rows[3].val, sizeof(rows[3].val), "192.168.4.1");
rows[3].key = "HOTSPOT:";
snprintf(rows[4].val, sizeof(rows[4].val), "%d / %d KB", (int)(used_b / 1024), (int)(total_b / 1024));
rows[4].key = "STORAGE:";
snprintf(rows[5].val, sizeof(rows[5].val), "%d cached", json_c);
rows[5].key = "CARDS:";
snprintf(rows[6].val, sizeof(rows[6].val), "%d KB", (int)(free_heap / 1024));
rows[6].key = "FREE RAM:";
snprintf(rows[7].val, sizeof(rows[7].val), "%d steps", (int)encoder_get_steps_per_detent());
rows[7].key = "ENCODER:";
snprintf(rows[8].val, sizeof(rows[8].val), "ESP32+ST7789");
rows[8].key = "HARDWARE:";
snprintf(rows[9].val, sizeof(rows[9].val), "MFRC522");
rows[9].key = "RFID:";
for (int i = 0; i < 10; i++) {
int y_pos = i * 20;
lv_obj_t *k = lv_label_create(box);
lv_label_set_text(k, rows[i].key);
lv_obj_set_style_text_font(k, &lv_font_montserrat_14, 0);
lv_obj_set_style_text_color(k, lv_color_hex(0x00E5FF), 0);
lv_obj_align(k, LV_ALIGN_TOP_LEFT, 0, y_pos);
lv_obj_t *v = lv_label_create(box);
lv_label_set_text(v, rows[i].val);
lv_obj_set_width(v, 120);
lv_label_set_long_mode(v, LV_LABEL_LONG_DOT);
lv_obj_set_style_text_font(v, &lv_font_montserrat_14, 0);
lv_obj_set_style_text_color(v, lv_color_hex(COLOR_AMBER), 0);
lv_obj_set_style_text_align(v, LV_TEXT_ALIGN_RIGHT, 0);
lv_obj_align(v, LV_ALIGN_TOP_RIGHT, 0, y_pos);
}
lv_obj_t *hint = lv_label_create(lv_scr_act());
lv_label_set_text(hint, "CLICK: MENU");
@@ -1561,6 +1584,7 @@ static void handle_new_card(const char *uid)
char *name = NULL;
esp_err_t ret = storage_manager_lookup_name(uid, &name);
if (ret != ESP_OK || name == NULL) {
wifi_manager_set_power_save(false);
app_set_view(APP_STATE_REGISTRATION);
view_unmapped(uid);
buzzer_chirp(392, 80);
@@ -1757,6 +1781,9 @@ static void handle_encoder_click(void)
} else {
if (s_storage_option_idx == 0) {
storage_manager_clear_all_cards(true);
s_card_name[0] = '\0';
s_uid[0] = '\0';
s_waiting_fetch = false;
buzzer_chirp(1046, 100);
s_storage_confirm_dialog = false;
view_storage();
@@ -1778,6 +1805,9 @@ static void handle_encoder_click(void)
case APP_STATE_SCANNER:
case APP_STATE_CARD_VIEW:
case APP_STATE_REGISTRATION:
if (cur == APP_STATE_REGISTRATION) {
wifi_manager_set_power_save(true);
}
if (cur == APP_STATE_CARD_VIEW && s_card_name[0] != '\0' && !s_waiting_fetch) {
char jp[72];
snprintf(jp, sizeof(jp), STORAGE_CARDS_DIR "/%s.json", s_uid);
@@ -1961,6 +1991,9 @@ static void ui_task(void *arg)
buzzer_chirp(1046, 50);
vTaskDelay(pdMS_TO_TICKS(40));
buzzer_chirp(1318, 70);
if (app_get_view() == APP_STATE_REGISTRATION) {
wifi_manager_set_power_save(true);
}
app_set_view(APP_STATE_MENU);
view_menu();
}

View File

@@ -31,6 +31,8 @@ static int s_retry_count = 0;
static char s_sta_ip[20] = "No Wi-Fi";
static char s_active_ssid[WIFI_SSID_MAX_LEN + 1] = "Disconnected";
static SemaphoreHandle_t s_wifi_mux = NULL;
static SemaphoreHandle_t s_net_lock = NULL;
/* ------------------------------------------------------------------ */
/* NVS Storage Helpers */
@@ -203,6 +205,9 @@ static void wifi_event_handler(void *arg, esp_event_base_t base, int32_t id, voi
if (s_wifi_group) {
xEventGroupSetBits(s_wifi_group, WIFI_CONNECTED_BIT);
}
/* Enable modem power saving when idle on station network */
esp_wifi_set_ps(WIFI_PS_MIN_MODEM);
ESP_LOGI(TAG, "Wi-Fi modem sleep enabled (WIFI_PS_MIN_MODEM: ~30-40 mA saved)");
}
}
@@ -361,11 +366,13 @@ esp_err_t wifi_manager_init(void)
}
s_wifi_mux = xSemaphoreCreateMutex();
s_net_lock = xSemaphoreCreateMutex();
s_wifi_group = xEventGroupCreate();
if (s_wifi_group == NULL) {
if (s_wifi_group == NULL || s_net_lock == NULL) {
return ESP_ERR_NO_MEM;
}
ESP_ERROR_CHECK(esp_netif_init());
ESP_ERROR_CHECK(esp_event_loop_create_default());
esp_netif_t *sta_netif = esp_netif_create_default_wifi_sta();
@@ -467,4 +474,45 @@ esp_err_t wifi_manager_start(void)
}
return ESP_OK;
}
static bool s_power_save_enabled = true;
esp_err_t wifi_manager_acquire_net_lock(TickType_t timeout_ticks)
{
if (!s_net_lock) {
s_net_lock = xSemaphoreCreateMutex();
}
if (!s_net_lock) {
return ESP_ERR_NO_MEM;
}
BaseType_t taken = xSemaphoreTake(s_net_lock, timeout_ticks);
if (taken == pdTRUE) {
/* Full RF performance for active TLS data transfers / OTA */
esp_wifi_set_ps(WIFI_PS_NONE);
return ESP_OK;
}
return ESP_ERR_TIMEOUT;
}
void wifi_manager_release_net_lock(void)
{
if (s_net_lock) {
/* Revert to low-power modem sleep when idle */
if (s_power_save_enabled && wifi_manager_connected()) {
esp_wifi_set_ps(WIFI_PS_MIN_MODEM);
}
xSemaphoreGive(s_net_lock);
}
}
esp_err_t wifi_manager_set_power_save(bool enable)
{
s_power_save_enabled = enable;
wifi_ps_type_t ps_type = enable ? WIFI_PS_MIN_MODEM : WIFI_PS_NONE;
esp_err_t err = esp_wifi_set_ps(ps_type);
ESP_LOGI(TAG, "Wi-Fi power save %s (%s)",
enable ? "enabled" : "disabled",
enable ? "WIFI_PS_MIN_MODEM" : "WIFI_PS_NONE");
return err;
}

View File

@@ -99,6 +99,23 @@ int wifi_manager_get_saved_profiles(wifi_profile_t *out_profiles, int max_count)
*/
esp_err_t wifi_manager_connect_new(const char *ssid, const char *password, bool save_to_nvs);
/**
* @brief Acquire exclusive network lock for heavy TLS / HTTPS operations.
* Prevents simultaneous TLS handshakes (OTA vs Scryfall) from exhausting SRAM.
*/
esp_err_t wifi_manager_acquire_net_lock(TickType_t timeout_ticks);
/**
* @brief Release exclusive network lock.
*/
void wifi_manager_release_net_lock(void);
/**
* @brief Set Wi-Fi modem power saving mode.
* @param enable True for WIFI_PS_MIN_MODEM (saves ~30-40 mA), False for WIFI_PS_NONE.
*/
esp_err_t wifi_manager_set_power_save(bool enable);
#ifdef __cplusplus
}
#endif