This commit is contained in:
27
README.md
27
README.md
@@ -129,6 +129,33 @@ When heading to your Local Game Store (LGS), a friend's house, or a tournament,
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
### 🔋 Battery Life & Power Optimization (850 mAh LiPo)
|
||||||
|
|
||||||
|
The MTG Companion is engineered for long tabletop sessions powered by a standard **850 mAh 3.7V LiPo** cell (~3.15 Wh).
|
||||||
|
|
||||||
|
#### Expected Runtime
|
||||||
|
|
||||||
|
| Play Style | Average Current | Expected Battery Life | Notes |
|
||||||
|
| :--- | :---: | :---: | :--- |
|
||||||
|
| **Typical Commander / FNM Session** | **~60 – 70 mA** | **10 – 12 hours** | Screen ON, tracking life, token spawning, occasional card scans, Wi-Fi modem sleep active |
|
||||||
|
| **Offline Play (No Wi-Fi)** | **~50 – 60 mA** | **12 – 14 hours** | Wi-Fi idle/disconnected, all card art and Oracle data streamed from local LittleFS flash |
|
||||||
|
| **Heavy Wi-Fi Setup / Card Pairing** | **~130 – 160 mA** | **4 – 5.5 hours** | Continuous SoftAP web portal active, bulk Scryfall art downloads, OTA updates |
|
||||||
|
|
||||||
|
#### Firmware Power-Saving Features
|
||||||
|
|
||||||
|
* **💤 Automatic Wi-Fi Modem Sleep (`WIFI_PS_MIN_MODEM`):**
|
||||||
|
When connected to Wi-Fi but not actively transferring data (e.g. while tracking life or waiting for card taps), the Wi-Fi radio enters modem sleep between beacon intervals. This cuts ESP32 power consumption by **~30–40 mA**. The radio automatically boosts to full power (`WIFI_PS_NONE`) during active Scryfall downloads, OTA flashing, or SoftAP pairing mode.
|
||||||
|
* **📡 125 ms RFID Carrier Duty Cycling:**
|
||||||
|
The MFRC522 polling loop runs an RF carrier burst sweep every 125 ms instead of continuously pumping 13.56 MHz RF power. This reduces average RFID reader draw from 20 mA down to **~5 mA** without any perceptible tag detection latency.
|
||||||
|
|
||||||
|
#### ⚠️ Hardware Tip: LDO Voltage Regulator Selection
|
||||||
|
|
||||||
|
To extract the full capacity of a single-cell LiPo battery (discharging from 4.2V down to ~3.4V):
|
||||||
|
* **Recommended LDOs:** Use ultra-low-dropout regulators such as **ME6211**, **AP2112K**, or **XC6206** (dropout voltage ~100–250 mV). These maintain a solid 3.3V rail down to ~3.45V battery level, utilizing **>90%** of the battery's energy.
|
||||||
|
* **Avoid AMS1117-3.3:** The common AMS1117 has a large 1.1V–1.3V dropout. When powered from a 3.7V LiPo, it browns out before utilizing even 20% of the battery capacity.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## 🛠️ Part 2: Technical Specifications & Build Guide
|
## 🛠️ Part 2: Technical Specifications & Build Guide
|
||||||
|
|
||||||
### Hardware Architecture
|
### Hardware Architecture
|
||||||
|
|||||||
@@ -236,7 +236,9 @@ void rc522_task(void *arg)
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
rc522_delay_ms(task_delay_ms);
|
uint32_t delay_ms = (rc522->config->poll_interval_ms > 0) ? rc522->config->poll_interval_ms : task_delay_ms;
|
||||||
|
rc522_delay_ms(delay_ms);
|
||||||
|
|
||||||
|
|
||||||
if (rc522->config->task_mutex != NULL) {
|
if (rc522->config->task_mutex != NULL) {
|
||||||
if (xSemaphoreTake(rc522->config->task_mutex, pdMS_TO_TICKS(mutex_take_timeout_ms)) == pdTRUE) {
|
if (xSemaphoreTake(rc522->config->task_mutex, pdMS_TO_TICKS(mutex_take_timeout_ms)) == pdTRUE) {
|
||||||
|
|||||||
@@ -22,6 +22,7 @@
|
|||||||
#include "nvs_flash.h"
|
#include "nvs_flash.h"
|
||||||
#include "nvs.h"
|
#include "nvs.h"
|
||||||
#include "cJSON.h"
|
#include "cJSON.h"
|
||||||
|
#include "wifi_manager.h"
|
||||||
#include "ota_manager.h"
|
#include "ota_manager.h"
|
||||||
|
|
||||||
static const char *TAG = "ota";
|
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_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_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"
|
||||||
|
"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\n"
|
||||||
|
"-----END CERTIFICATE-----\n";
|
||||||
|
|
||||||
|
|
||||||
static SemaphoreHandle_t s_ota_mux = NULL;
|
static SemaphoreHandle_t s_ota_mux = NULL;
|
||||||
static ota_status_t s_status = OTA_STATUS_IDLE;
|
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);
|
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 = {
|
http_rx_buf_t rx = {
|
||||||
.buf = malloc(GITEA_API_BUF_SIZE),
|
.buf = malloc(GITEA_API_BUF_SIZE),
|
||||||
.len = 0,
|
.len = 0,
|
||||||
@@ -222,6 +266,7 @@ static void ota_check_task(void *arg)
|
|||||||
};
|
};
|
||||||
|
|
||||||
if (!rx.buf) {
|
if (!rx.buf) {
|
||||||
|
wifi_manager_release_net_lock();
|
||||||
set_state(OTA_STATUS_FAILED, "Out of memory");
|
set_state(OTA_STATUS_FAILED, "Out of memory");
|
||||||
s_ota_task_handle = NULL;
|
s_ota_task_handle = NULL;
|
||||||
vTaskDelete(NULL);
|
vTaskDelete(NULL);
|
||||||
@@ -232,15 +277,16 @@ static void ota_check_task(void *arg)
|
|||||||
esp_http_client_config_t cfg = {
|
esp_http_client_config_t cfg = {
|
||||||
.url = GITEA_RELEASE_API_URL,
|
.url = GITEA_RELEASE_API_URL,
|
||||||
.transport_type = HTTP_TRANSPORT_OVER_SSL,
|
.transport_type = HTTP_TRANSPORT_OVER_SSL,
|
||||||
.timeout_ms = 10000,
|
.timeout_ms = 15000,
|
||||||
.event_handler = http_rx_event_handler,
|
.event_handler = http_rx_event_handler,
|
||||||
.user_data = &rx,
|
.user_data = &rx,
|
||||||
.buffer_size = 2048,
|
.buffer_size = 1024,
|
||||||
.crt_bundle_attach = esp_crt_bundle_attach,
|
.cert_pem = s_isrg_root_x1_pem,
|
||||||
};
|
};
|
||||||
|
|
||||||
esp_http_client_handle_t client = esp_http_client_init(&cfg);
|
esp_http_client_handle_t client = esp_http_client_init(&cfg);
|
||||||
if (!client) {
|
if (!client) {
|
||||||
|
wifi_manager_release_net_lock();
|
||||||
free(rx.buf);
|
free(rx.buf);
|
||||||
set_state(OTA_STATUS_FAILED, "HTTP init failed");
|
set_state(OTA_STATUS_FAILED, "HTTP init failed");
|
||||||
s_ota_task_handle = NULL;
|
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);
|
esp_err_t ret = esp_http_client_perform(client);
|
||||||
int status_code = esp_http_client_get_status_code(client);
|
int status_code = esp_http_client_get_status_code(client);
|
||||||
esp_http_client_cleanup(client);
|
esp_http_client_cleanup(client);
|
||||||
|
wifi_manager_release_net_lock();
|
||||||
|
|
||||||
|
|
||||||
if (ret != ESP_OK || status_code != 200) {
|
if (ret != ESP_OK || status_code != 200) {
|
||||||
char err_msg[64];
|
char err_msg[64];
|
||||||
@@ -415,6 +463,14 @@ static void ota_download_task(void *arg)
|
|||||||
s_bytes_read = 0;
|
s_bytes_read = 0;
|
||||||
s_total_bytes = s_release.binary_size;
|
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...");
|
set_state(OTA_STATUS_DOWNLOADING, "Starting download...");
|
||||||
ESP_LOGI(TAG, "Starting HTTPS OTA from: %s", s_release.download_url);
|
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,
|
.timeout_ms = 30000,
|
||||||
.keep_alive_enable = true,
|
.keep_alive_enable = true,
|
||||||
.buffer_size = 2048,
|
.buffer_size = 2048,
|
||||||
.crt_bundle_attach = esp_crt_bundle_attach,
|
.cert_pem = s_isrg_root_x1_pem,
|
||||||
};
|
};
|
||||||
|
|
||||||
esp_https_ota_config_t ota_cfg = {
|
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_https_ota_handle_t ota_handle = NULL;
|
||||||
esp_err_t err = esp_https_ota_begin(&ota_cfg, &ota_handle);
|
esp_err_t err = esp_https_ota_begin(&ota_cfg, &ota_handle);
|
||||||
if (err != ESP_OK || ota_handle == NULL) {
|
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));
|
ESP_LOGE(TAG, "esp_https_ota_begin failed: %s", esp_err_to_name(err));
|
||||||
set_state(OTA_STATUS_FAILED, "Failed to connect to image");
|
set_state(OTA_STATUS_FAILED, "Failed to connect to image");
|
||||||
s_ota_task_handle = NULL;
|
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) {
|
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_LOGW(TAG, "Incoming image has identical ELF SHA-256 as running app - aborting flash write");
|
||||||
esp_https_ota_abort(ota_handle);
|
esp_https_ota_abort(ota_handle);
|
||||||
|
wifi_manager_release_net_lock();
|
||||||
set_state(OTA_STATUS_UP_TO_DATE, "Firmware is already up to date");
|
set_state(OTA_STATUS_UP_TO_DATE, "Firmware is already up to date");
|
||||||
s_ota_task_handle = NULL;
|
s_ota_task_handle = NULL;
|
||||||
vTaskDelete(NULL);
|
vTaskDelete(NULL);
|
||||||
@@ -459,6 +517,7 @@ static void ota_download_task(void *arg)
|
|||||||
if (s_cancel_requested) {
|
if (s_cancel_requested) {
|
||||||
ESP_LOGW(TAG, "OTA canceled by user");
|
ESP_LOGW(TAG, "OTA canceled by user");
|
||||||
esp_https_ota_abort(ota_handle);
|
esp_https_ota_abort(ota_handle);
|
||||||
|
wifi_manager_release_net_lock();
|
||||||
set_state(OTA_STATUS_IDLE, "Update canceled");
|
set_state(OTA_STATUS_IDLE, "Update canceled");
|
||||||
s_ota_task_handle = NULL;
|
s_ota_task_handle = NULL;
|
||||||
vTaskDelete(NULL);
|
vTaskDelete(NULL);
|
||||||
@@ -481,6 +540,7 @@ static void ota_download_task(void *arg)
|
|||||||
if (err == ESP_OK) {
|
if (err == ESP_OK) {
|
||||||
if (esp_https_ota_is_complete_data_received(ota_handle)) {
|
if (esp_https_ota_is_complete_data_received(ota_handle)) {
|
||||||
esp_err_t finish_err = esp_https_ota_finish(ota_handle);
|
esp_err_t finish_err = esp_https_ota_finish(ota_handle);
|
||||||
|
wifi_manager_release_net_lock();
|
||||||
if (finish_err == ESP_OK) {
|
if (finish_err == ESP_OK) {
|
||||||
/* Store installed release hash in NVS */
|
/* Store installed release hash in NVS */
|
||||||
nvs_handle_t nvs_h;
|
nvs_handle_t nvs_h;
|
||||||
@@ -502,11 +562,13 @@ static void ota_download_task(void *arg)
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
esp_https_ota_abort(ota_handle);
|
esp_https_ota_abort(ota_handle);
|
||||||
|
wifi_manager_release_net_lock();
|
||||||
set_state(OTA_STATUS_FAILED, "Incomplete download");
|
set_state(OTA_STATUS_FAILED, "Incomplete download");
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
ESP_LOGE(TAG, "esp_https_ota_perform failed: %s", esp_err_to_name(err));
|
ESP_LOGE(TAG, "esp_https_ota_perform failed: %s", esp_err_to_name(err));
|
||||||
esp_https_ota_abort(ota_handle);
|
esp_https_ota_abort(ota_handle);
|
||||||
|
wifi_manager_release_net_lock();
|
||||||
set_state(OTA_STATUS_FAILED, "Flash write failed");
|
set_state(OTA_STATUS_FAILED, "Flash write failed");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ static const char *TAG = "rfid";
|
|||||||
* Keep this <= 15 chars and in sync with the rc522 library's
|
* Keep this <= 15 chars and in sync with the rc522 library's
|
||||||
* xTaskCreate("rc522_poll", ...). */
|
* xTaskCreate("rc522_poll", ...). */
|
||||||
#define RC522_TASK_NAME "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_STACK_SIZE (6 * 1024)
|
||||||
#define RC522_TASK_PRIORITY (1)
|
#define RC522_TASK_PRIORITY (1)
|
||||||
|
|
||||||
|
|||||||
@@ -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)
|
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++) {
|
for (int attempt = 0; attempt < MAX_ATTEMPTS; attempt++) {
|
||||||
if (sink->file != NULL) {
|
if (sink->file != NULL) {
|
||||||
rewind(sink->file);
|
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);
|
esp_http_client_handle_t client = esp_http_client_init(&cfg);
|
||||||
if (client == NULL) {
|
if (client == NULL) {
|
||||||
return ESP_ERR_NO_MEM;
|
overall_ret = ESP_ERR_NO_MEM;
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
esp_http_client_set_method(client, HTTP_METHOD_GET);
|
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);
|
ESP_LOGD(TAG, "GET %s -> %s (status %d)", url, esp_err_to_name(ret), status);
|
||||||
if (ret == ESP_OK && status >= 200 && status < 300) {
|
if (ret == ESP_OK && status >= 200 && status < 300) {
|
||||||
return ESP_OK;
|
overall_ret = ESP_OK;
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
if (ret == ESP_OK && (status < 200 || status >= 300)) {
|
if (ret == ESP_OK && (status < 200 || status >= 300)) {
|
||||||
ESP_LOGW(TAG, "HTTP GET returned status %d for %s", status, url);
|
ESP_LOGW(TAG, "HTTP GET returned status %d for %s", status, url);
|
||||||
}
|
}
|
||||||
if (sink->err != ESP_OK) {
|
if (sink->err != ESP_OK) {
|
||||||
return sink->err;
|
overall_ret = sink->err;
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
vTaskDelay(pdMS_TO_TICKS(500));
|
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)
|
static esp_err_t http_get_json(const char *url, char **out_buf)
|
||||||
|
|||||||
@@ -17,20 +17,6 @@ static const char *TAG = "storage";
|
|||||||
|
|
||||||
#define STORAGE_PARTITION_LABEL "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)
|
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)
|
static esp_err_t seed_defaults(void)
|
||||||
{
|
{
|
||||||
/* Create /spiffs/cards directory */
|
/* Ensure /spiffs/cards directory exists */
|
||||||
mkdir(STORAGE_CARDS_DIR, 0755);
|
mkdir(STORAGE_CARDS_DIR, 0755);
|
||||||
|
|
||||||
if (!storage_file_exists(STORAGE_MAPPINGS)) {
|
if (!storage_file_exists(STORAGE_MAPPINGS)) {
|
||||||
ESP_LOGI(TAG, "seeding %s", STORAGE_MAPPINGS);
|
ESP_LOGI(TAG, "Creating empty %s", STORAGE_MAPPINGS);
|
||||||
esp_err_t ret = storage_write_text(STORAGE_MAPPINGS, SAMPLE_MAPPINGS);
|
esp_err_t ret = storage_write_text(STORAGE_MAPPINGS, "{\n}\n");
|
||||||
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);
|
|
||||||
if (ret != ESP_OK) {
|
if (ret != ESP_OK) {
|
||||||
return ret;
|
return ret;
|
||||||
}
|
}
|
||||||
|
|||||||
167
main/ui_task.c
167
main/ui_task.c
@@ -887,34 +887,45 @@ static void view_storage(void)
|
|||||||
lv_obj_set_style_pad_all(stat_box, 8, 0);
|
lv_obj_set_style_pad_all(stat_box, 8, 0);
|
||||||
lv_obj_set_scrollbar_mode(stat_box, LV_SCROLLBAR_MODE_OFF);
|
lv_obj_set_scrollbar_mode(stat_box, LV_SCROLLBAR_MODE_OFF);
|
||||||
|
|
||||||
lv_obj_t *key_lbl = lv_label_create(stat_box);
|
struct {
|
||||||
lv_obj_set_width(key_lbl, 125);
|
const char *key;
|
||||||
lv_obj_set_style_text_font(key_lbl, &lv_font_montserrat_14, 0);
|
char val[32];
|
||||||
lv_obj_set_style_text_color(key_lbl, lv_color_hex(0x00E5FF), 0);
|
} stat_rows[5];
|
||||||
lv_label_set_text(key_lbl,
|
|
||||||
"CACHED CARDS:\n"
|
snprintf(stat_rows[0].val, sizeof(stat_rows[0].val), "%d", json_c);
|
||||||
"ART CROPS:\n\n"
|
stat_rows[0].key = "CACHED CARDS:";
|
||||||
"STORAGE USED:\n"
|
|
||||||
"PERCENT USED:\n"
|
snprintf(stat_rows[1].val, sizeof(stat_rows[1].val), "%d", jpg_c);
|
||||||
"DECK CAPACITY:");
|
stat_rows[1].key = "ART CROPS:";
|
||||||
lv_obj_align(key_lbl, LV_ALIGN_TOP_LEFT, 0, 0);
|
|
||||||
|
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;
|
int percent = total_b > 0 ? (int)((used_b * 100) / total_b) : 0;
|
||||||
lv_label_set_text_fmt(val_lbl,
|
snprintf(stat_rows[3].val, sizeof(stat_rows[3].val), "%d%%", percent);
|
||||||
"%d\n"
|
stat_rows[3].key = "PERCENT USED:";
|
||||||
"%d\n\n"
|
|
||||||
"%d KB\n"
|
snprintf(stat_rows[4].val, sizeof(stat_rows[4].val), "~100 (EDH)");
|
||||||
"%d%%\n"
|
stat_rows[4].key = "DECK CAPACITY:";
|
||||||
"~120-140",
|
|
||||||
json_c, jpg_c,
|
for (int i = 0; i < 5; i++) {
|
||||||
(int)(used_b / 1024),
|
int y_pos = (i < 2) ? (i * 20) : (i * 20 + 8);
|
||||||
percent);
|
|
||||||
lv_obj_align(val_lbl, LV_ALIGN_TOP_RIGHT, 0, 0);
|
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 */
|
/* Action 0: Clear Cards Button */
|
||||||
lv_obj_t *btn0 = lv_obj_create(lv_scr_act());
|
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();
|
const char *ssid = wifi_manager_get_connected_ssid();
|
||||||
int rssi = wifi_manager_get_rssi();
|
int rssi = wifi_manager_get_rssi();
|
||||||
char wifi_line[32];
|
char wifi_line[64];
|
||||||
if (wifi_manager_connected() && ssid && strlen(ssid) > 0) {
|
if (wifi_manager_connected() && ssid && strlen(ssid) > 0) {
|
||||||
snprintf(wifi_line, sizeof(wifi_line), "%s (%ddB)", ssid, rssi);
|
snprintf(wifi_line, sizeof(wifi_line), "%s (%ddB)", ssid, rssi);
|
||||||
} else {
|
} else {
|
||||||
snprintf(wifi_line, sizeof(wifi_line), "Disconnected");
|
snprintf(wifi_line, sizeof(wifi_line), "Disconnected");
|
||||||
}
|
}
|
||||||
|
|
||||||
lv_obj_t *lbl_keys = lv_label_create(box);
|
struct {
|
||||||
lv_obj_set_width(lbl_keys, 75);
|
const char *key;
|
||||||
lv_obj_set_style_text_font(lbl_keys, &lv_font_montserrat_14, 0);
|
char val[64];
|
||||||
lv_obj_set_style_text_color(lbl_keys, lv_color_hex(0x00E5FF), 0);
|
} rows[10];
|
||||||
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);
|
|
||||||
|
|
||||||
lv_obj_t *lbl_vals = lv_label_create(box);
|
snprintf(rows[0].val, sizeof(rows[0].val), "%s", ota_manager_get_current_version());
|
||||||
lv_obj_set_width(lbl_vals, 117);
|
rows[0].key = "VERSION:";
|
||||||
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);
|
snprintf(rows[1].val, sizeof(rows[1].val), "%s", wifi_line);
|
||||||
lv_obj_set_style_text_align(lbl_vals, LV_TEXT_ALIGN_RIGHT, 0);
|
rows[1].key = "WI-FI:";
|
||||||
lv_label_set_text_fmt(lbl_vals,
|
|
||||||
"%s\n"
|
snprintf(rows[2].val, sizeof(rows[2].val), "%s", wifi_manager_get_ip());
|
||||||
"%s\n"
|
rows[2].key = "IP ADDR:";
|
||||||
"%s\n"
|
|
||||||
"192.168.4.1\n"
|
snprintf(rows[3].val, sizeof(rows[3].val), "192.168.4.1");
|
||||||
"%d / %d KB\n"
|
rows[3].key = "HOTSPOT:";
|
||||||
"%d cached\n"
|
|
||||||
"%d KB\n"
|
snprintf(rows[4].val, sizeof(rows[4].val), "%d / %d KB", (int)(used_b / 1024), (int)(total_b / 1024));
|
||||||
"%d steps\n"
|
rows[4].key = "STORAGE:";
|
||||||
"ESP32+ST7789\n"
|
|
||||||
"MFRC522",
|
snprintf(rows[5].val, sizeof(rows[5].val), "%d cached", json_c);
|
||||||
ota_manager_get_current_version(),
|
rows[5].key = "CARDS:";
|
||||||
wifi_line,
|
|
||||||
wifi_manager_get_ip(),
|
snprintf(rows[6].val, sizeof(rows[6].val), "%d KB", (int)(free_heap / 1024));
|
||||||
(int)(used_b / 1024), (int)(total_b / 1024),
|
rows[6].key = "FREE RAM:";
|
||||||
json_c,
|
|
||||||
(int)(free_heap / 1024),
|
snprintf(rows[7].val, sizeof(rows[7].val), "%d steps", (int)encoder_get_steps_per_detent());
|
||||||
(int)encoder_get_steps_per_detent());
|
rows[7].key = "ENCODER:";
|
||||||
lv_obj_align(lbl_vals, LV_ALIGN_TOP_RIGHT, 0, 0);
|
|
||||||
|
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_obj_t *hint = lv_label_create(lv_scr_act());
|
||||||
lv_label_set_text(hint, "CLICK: MENU");
|
lv_label_set_text(hint, "CLICK: MENU");
|
||||||
@@ -1561,6 +1584,7 @@ static void handle_new_card(const char *uid)
|
|||||||
char *name = NULL;
|
char *name = NULL;
|
||||||
esp_err_t ret = storage_manager_lookup_name(uid, &name);
|
esp_err_t ret = storage_manager_lookup_name(uid, &name);
|
||||||
if (ret != ESP_OK || name == NULL) {
|
if (ret != ESP_OK || name == NULL) {
|
||||||
|
wifi_manager_set_power_save(false);
|
||||||
app_set_view(APP_STATE_REGISTRATION);
|
app_set_view(APP_STATE_REGISTRATION);
|
||||||
view_unmapped(uid);
|
view_unmapped(uid);
|
||||||
buzzer_chirp(392, 80);
|
buzzer_chirp(392, 80);
|
||||||
@@ -1757,6 +1781,9 @@ static void handle_encoder_click(void)
|
|||||||
} else {
|
} else {
|
||||||
if (s_storage_option_idx == 0) {
|
if (s_storage_option_idx == 0) {
|
||||||
storage_manager_clear_all_cards(true);
|
storage_manager_clear_all_cards(true);
|
||||||
|
s_card_name[0] = '\0';
|
||||||
|
s_uid[0] = '\0';
|
||||||
|
s_waiting_fetch = false;
|
||||||
buzzer_chirp(1046, 100);
|
buzzer_chirp(1046, 100);
|
||||||
s_storage_confirm_dialog = false;
|
s_storage_confirm_dialog = false;
|
||||||
view_storage();
|
view_storage();
|
||||||
@@ -1778,6 +1805,9 @@ static void handle_encoder_click(void)
|
|||||||
case APP_STATE_SCANNER:
|
case APP_STATE_SCANNER:
|
||||||
case APP_STATE_CARD_VIEW:
|
case APP_STATE_CARD_VIEW:
|
||||||
case APP_STATE_REGISTRATION:
|
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) {
|
if (cur == APP_STATE_CARD_VIEW && s_card_name[0] != '\0' && !s_waiting_fetch) {
|
||||||
char jp[72];
|
char jp[72];
|
||||||
snprintf(jp, sizeof(jp), STORAGE_CARDS_DIR "/%s.json", s_uid);
|
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);
|
buzzer_chirp(1046, 50);
|
||||||
vTaskDelay(pdMS_TO_TICKS(40));
|
vTaskDelay(pdMS_TO_TICKS(40));
|
||||||
buzzer_chirp(1318, 70);
|
buzzer_chirp(1318, 70);
|
||||||
|
if (app_get_view() == APP_STATE_REGISTRATION) {
|
||||||
|
wifi_manager_set_power_save(true);
|
||||||
|
}
|
||||||
app_set_view(APP_STATE_MENU);
|
app_set_view(APP_STATE_MENU);
|
||||||
view_menu();
|
view_menu();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -31,6 +31,8 @@ static int s_retry_count = 0;
|
|||||||
static char s_sta_ip[20] = "No Wi-Fi";
|
static char s_sta_ip[20] = "No Wi-Fi";
|
||||||
static char s_active_ssid[WIFI_SSID_MAX_LEN + 1] = "Disconnected";
|
static char s_active_ssid[WIFI_SSID_MAX_LEN + 1] = "Disconnected";
|
||||||
static SemaphoreHandle_t s_wifi_mux = NULL;
|
static SemaphoreHandle_t s_wifi_mux = NULL;
|
||||||
|
static SemaphoreHandle_t s_net_lock = NULL;
|
||||||
|
|
||||||
|
|
||||||
/* ------------------------------------------------------------------ */
|
/* ------------------------------------------------------------------ */
|
||||||
/* NVS Storage Helpers */
|
/* 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) {
|
if (s_wifi_group) {
|
||||||
xEventGroupSetBits(s_wifi_group, WIFI_CONNECTED_BIT);
|
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_wifi_mux = xSemaphoreCreateMutex();
|
||||||
|
s_net_lock = xSemaphoreCreateMutex();
|
||||||
s_wifi_group = xEventGroupCreate();
|
s_wifi_group = xEventGroupCreate();
|
||||||
if (s_wifi_group == NULL) {
|
if (s_wifi_group == NULL || s_net_lock == NULL) {
|
||||||
return ESP_ERR_NO_MEM;
|
return ESP_ERR_NO_MEM;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
ESP_ERROR_CHECK(esp_netif_init());
|
ESP_ERROR_CHECK(esp_netif_init());
|
||||||
ESP_ERROR_CHECK(esp_event_loop_create_default());
|
ESP_ERROR_CHECK(esp_event_loop_create_default());
|
||||||
esp_netif_t *sta_netif = esp_netif_create_default_wifi_sta();
|
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;
|
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;
|
||||||
}
|
}
|
||||||
@@ -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);
|
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
|
#ifdef __cplusplus
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
@@ -28,4 +28,9 @@ CONFIG_JD_FORMAT_RGB565=y
|
|||||||
CONFIG_JD_FASTDECODE_32BIT=y
|
CONFIG_JD_FASTDECODE_32BIT=y
|
||||||
|
|
||||||
# Flash Chip Driver Optimization
|
# Flash Chip Driver Optimization
|
||||||
CONFIG_SPI_FLASH_SUPPORT_BOYA_CHIP=y
|
CONFIG_SPI_FLASH_SUPPORT_BOYA_CHIP=y
|
||||||
|
|
||||||
|
# mbedTLS Dynamic Buffers & Memory Optimization (Vital for ESP32 without PSRAM)
|
||||||
|
CONFIG_MBEDTLS_DYNAMIC_BUFFER=y
|
||||||
|
CONFIG_MBEDTLS_DYNAMIC_FREE_CONFIG_DATA=y
|
||||||
|
CONFIG_MBEDTLS_DYNAMIC_FREE_CA_CERT=y
|
||||||
0
storage/cards/.gitkeep
Normal file
0
storage/cards/.gitkeep
Normal file
Binary file not shown.
|
Before Width: | Height: | Size: 23 KiB |
@@ -1,7 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "Sol Ring",
|
|
||||||
"mana_cost": "{1}",
|
|
||||||
"type": "Artifact",
|
|
||||||
"oracle": "Tap: Add CC.",
|
|
||||||
"price_usd": "1.25"
|
|
||||||
}
|
|
||||||
@@ -1,6 +1 @@
|
|||||||
{
|
{}
|
||||||
"04A2B3C4": "Sol Ring",
|
|
||||||
"04E1F2A3": "Atraxa, Praetors' Voice",
|
|
||||||
"508152B2": "Workshop Assistant",
|
|
||||||
"B34DF005": "Black Lotus"
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user