diff --git a/main/CMakeLists.txt b/main/CMakeLists.txt index 9bd74ac..bb49fa4 100644 --- a/main/CMakeLists.txt +++ b/main/CMakeLists.txt @@ -12,5 +12,6 @@ idf_component_register(SRCS "app_state.c" "ota_manager.c" "dns_server.c" + "serial_log.c" INCLUDE_DIRS "." ) \ No newline at end of file diff --git a/main/display_manager.c b/main/display_manager.c index 262f64a..a74a2db 100644 --- a/main/display_manager.c +++ b/main/display_manager.c @@ -1,4 +1,6 @@ +#include "freertos/FreeRTOS.h" +#include "freertos/task.h" #include "esp_log.h" #include "esp_lcd_panel_io.h" #include "esp_lcd_panel_ops.h" @@ -68,10 +70,13 @@ esp_err_t display_manager_init(void) } ESP_ERROR_CHECK(esp_lcd_panel_reset(s_panel)); + vTaskDelay(pdMS_TO_TICKS(150)); /* Allow ST7789 internal charge pump to fully stabilize */ ESP_ERROR_CHECK(esp_lcd_panel_init(s_panel)); + vTaskDelay(pdMS_TO_TICKS(20)); ESP_ERROR_CHECK(esp_lcd_panel_invert_color(s_panel, true)); ESP_ERROR_CHECK(esp_lcd_panel_mirror(s_panel, true, true)); ESP_ERROR_CHECK(esp_lcd_panel_disp_on_off(s_panel, true)); + vTaskDelay(pdMS_TO_TICKS(50)); /* 2. Initialize LVGL port on Core 1 */ lvgl_port_cfg_t port_cfg = ESP_LVGL_PORT_INIT_CONFIG(); diff --git a/main/main.c b/main/main.c index 1519924..9786a3d 100644 --- a/main/main.c +++ b/main/main.c @@ -10,11 +10,15 @@ #include "scryfall_client.h" #include "web_server.h" #include "ota_manager.h" +#include "serial_log.h" static const char *TAG = "MAIN"; void app_main(void) { + /* Initialize serial log ring buffer to capture all ESP_LOG output for Web UI */ + serial_log_init(); + ESP_LOGI(TAG, "MTG RFID Companion booting..."); /* Validate OTA partition & cancel rollback if just updated */ diff --git a/main/serial_log.c b/main/serial_log.c new file mode 100644 index 0000000..209a06a --- /dev/null +++ b/main/serial_log.c @@ -0,0 +1,152 @@ +/* + * MTG RFID Companion - In-memory Serial Log Ring Buffer + */ + +#include "serial_log.h" +#include +#include +#include +#include "esp_log.h" +#include "freertos/FreeRTOS.h" +#include "freertos/task.h" + +#define LOG_BUFFER_SIZE (16 * 1024) + +static vprintf_like_t s_prev_vprintf = NULL; +static char s_ring_buffer[LOG_BUFFER_SIZE]; +static uint32_t s_write_idx = 0; +static uint32_t s_total_bytes = 0; +static portMUX_TYPE s_log_mux = portMUX_INITIALIZER_UNLOCKED; + +static int serial_log_vprintf(const char *fmt, va_list args) +{ + int ret = 0; + + /* Maintain original console/UART output */ + if (s_prev_vprintf != NULL) { + va_list copy1; + va_copy(copy1, args); + ret = s_prev_vprintf(fmt, copy1); + va_end(copy1); + } else { + va_list copy1; + va_copy(copy1, args); + ret = vprintf(fmt, copy1); + va_end(copy1); + } + + /* Format message into local buffer */ + char line_buf[512]; + va_list copy2; + va_copy(copy2, args); + int written = vsnprintf(line_buf, sizeof(line_buf), fmt, copy2); + va_end(copy2); + + if (written > 0) { + size_t len = (size_t)written; + if (len >= sizeof(line_buf)) { + len = sizeof(line_buf) - 1; + } + + portENTER_CRITICAL(&s_log_mux); + size_t space_to_end = LOG_BUFFER_SIZE - s_write_idx; + if (len <= space_to_end) { + memcpy(&s_ring_buffer[s_write_idx], line_buf, len); + s_write_idx = (s_write_idx + len) % LOG_BUFFER_SIZE; + } else { + memcpy(&s_ring_buffer[s_write_idx], line_buf, space_to_end); + memcpy(&s_ring_buffer[0], line_buf + space_to_end, len - space_to_end); + s_write_idx = (uint32_t)(len - space_to_end); + } + s_total_bytes += (uint32_t)len; + portEXIT_CRITICAL(&s_log_mux); + } + + return ret; +} + +esp_err_t serial_log_init(void) +{ + portENTER_CRITICAL(&s_log_mux); + s_total_bytes = 0; + s_write_idx = 0; + memset(s_ring_buffer, 0, sizeof(s_ring_buffer)); + portEXIT_CRITICAL(&s_log_mux); + + s_prev_vprintf = esp_log_set_vprintf(serial_log_vprintf); + return ESP_OK; +} + +size_t serial_log_read(uint32_t from_offset, char *dest, size_t max_len, uint32_t *next_offset) +{ + if (!dest || max_len == 0) { + if (next_offset) { + *next_offset = from_offset; + } + return 0; + } + + size_t copied = 0; + + portENTER_CRITICAL(&s_log_mux); + + uint32_t total = s_total_bytes; + uint32_t oldest = (total > LOG_BUFFER_SIZE) ? (total - LOG_BUFFER_SIZE) : 0; + + if (from_offset < oldest || from_offset == 0) { + from_offset = oldest; + } + + if (from_offset >= total) { + if (next_offset) { + *next_offset = total; + } + dest[0] = '\0'; + portEXIT_CRITICAL(&s_log_mux); + return 0; + } + + uint32_t to_read = total - from_offset; + if (to_read > (uint32_t)(max_len - 1)) { + to_read = (uint32_t)(max_len - 1); + } + + size_t start_idx = from_offset % LOG_BUFFER_SIZE; + size_t chunk1 = LOG_BUFFER_SIZE - start_idx; + + if (to_read <= chunk1) { + memcpy(dest, &s_ring_buffer[start_idx], to_read); + } else { + memcpy(dest, &s_ring_buffer[start_idx], chunk1); + memcpy(dest + chunk1, &s_ring_buffer[0], to_read - chunk1); + } + + copied = to_read; + dest[copied] = '\0'; + + if (next_offset) { + *next_offset = from_offset + (uint32_t)to_read; + } + + portEXIT_CRITICAL(&s_log_mux); + + return copied; +} + +void serial_log_clear(void) +{ + portENTER_CRITICAL(&s_log_mux); + s_total_bytes = 0; + s_write_idx = 0; + memset(s_ring_buffer, 0, sizeof(s_ring_buffer)); + portEXIT_CRITICAL(&s_log_mux); +} + +uint32_t serial_log_get_total_bytes(void) +{ + uint32_t total; + portENTER_CRITICAL(&s_log_mux); + total = s_total_bytes; + portEXIT_CRITICAL(&s_log_mux); + return total; +} diff --git a/main/serial_log.h b/main/serial_log.h new file mode 100644 index 0000000..cca1db2 --- /dev/null +++ b/main/serial_log.h @@ -0,0 +1,47 @@ +/* + * MTG RFID Companion - In-memory Serial Log Ring Buffer + */ + +#pragma once + +#include +#include +#include "esp_err.h" + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * @brief Initialize the serial log ring buffer and hook into esp_log_set_vprintf. + * + * Hardware UART serial output remains active and unaffected. + */ +esp_err_t serial_log_init(void); + +/** + * @brief Read logs starting from a specific byte offset. + * + * @param from_offset Monotonically increasing byte sequence offset previously read. + * If 0 or if the offset has fallen outside the ring buffer window, + * the available recent buffer content is returned. + * @param dest Output buffer to receive log bytes. + * @param max_len Maximum bytes to write into dest. + * @param next_offset Pointer to store the new byte offset cursor. + * @return Number of bytes copied into dest. + */ +size_t serial_log_read(uint32_t from_offset, char *dest, size_t max_len, uint32_t *next_offset); + +/** + * @brief Clear the serial log buffer. + */ +void serial_log_clear(void); + +/** + * @brief Get total lifetime log bytes written. + */ +uint32_t serial_log_get_total_bytes(void); + +#ifdef __cplusplus +} +#endif diff --git a/main/ui_task.c b/main/ui_task.c index 3219e21..601455e 100644 --- a/main/ui_task.c +++ b/main/ui_task.c @@ -1094,6 +1094,90 @@ static void view_storage(void) lvgl_port_unlock(); } +static void truncate_text_to_fit(char *str, size_t max_buf_sz, lv_coord_t max_w, const lv_font_t *font) +{ + if (lv_txt_get_width(str, strlen(str), font, 0, LV_TEXT_FLAG_NONE) <= max_w) { + return; + } + + size_t len = strlen(str); + while (len > 0) { + while (len > 0 && (str[len - 1] & 0xC0) == 0x80) { + len--; + } + if (len > 0) { + len--; + } + str[len] = '\0'; + char candidate[96]; + snprintf(candidate, sizeof(candidate), "%s...", str); + if (lv_txt_get_width(candidate, strlen(candidate), font, 0, LV_TEXT_FLAG_NONE) <= max_w) { + snprintf(str, max_buf_sz, "%s", candidate); + return; + } + } + snprintf(str, max_buf_sz, "..."); +} + +static void get_fitted_wifi_text(char *out, size_t out_sz, const char *ssid, int rssi, lv_coord_t max_w) +{ + if (!wifi_manager_connected() || !ssid || strlen(ssid) == 0) { + snprintf(out, out_sz, "Disconnected"); + return; + } + + char suffix[24] = ""; + if (rssi != 0) { + snprintf(suffix, sizeof(suffix), " (%ddB)", rssi); + } + + /* Try full SSID + suffix */ + snprintf(out, out_sz, "%s%s", ssid, suffix); + if (lv_txt_get_width(out, strlen(out), &lv_font_montserrat_14, 0, LV_TEXT_FLAG_NONE) <= max_w) { + return; + } + + /* If it doesn't fit, truncate SSID with "..." while preserving the suffix */ + char temp_ssid[WIFI_SSID_MAX_LEN + 1]; + strncpy(temp_ssid, ssid, sizeof(temp_ssid) - 1); + temp_ssid[sizeof(temp_ssid) - 1] = '\0'; + size_t len = strlen(temp_ssid); + + while (len > 0) { + while (len > 0 && (temp_ssid[len - 1] & 0xC0) == 0x80) { + len--; + } + if (len > 0) { + len--; + } + temp_ssid[len] = '\0'; + snprintf(out, out_sz, "%s...%s", temp_ssid, suffix); + if (lv_txt_get_width(out, strlen(out), &lv_font_montserrat_14, 0, LV_TEXT_FLAG_NONE) <= max_w) { + return; + } + } + + /* If even "..." + suffix is too wide, drop suffix and truncate SSID */ + strncpy(temp_ssid, ssid, sizeof(temp_ssid) - 1); + temp_ssid[sizeof(temp_ssid) - 1] = '\0'; + len = strlen(temp_ssid); + while (len > 0) { + while (len > 0 && (temp_ssid[len - 1] & 0xC0) == 0x80) { + len--; + } + if (len > 0) { + len--; + } + temp_ssid[len] = '\0'; + snprintf(out, out_sz, "%s...", temp_ssid); + if (lv_txt_get_width(out, strlen(out), &lv_font_montserrat_14, 0, LV_TEXT_FLAG_NONE) <= max_w) { + return; + } + } + + snprintf(out, out_sz, "..."); +} + /* 7. Device & Wi-Fi Info */ static void view_device_info(void) { @@ -1110,6 +1194,8 @@ static void view_device_info(void) lv_obj_set_style_radius(box, 6, 0); lv_obj_set_style_pad_all(box, 8, 0); + const lv_coord_t inner_w = (DISP_W - 24) - 16; /* 200px usable width */ + size_t total_b = 0, used_b = 0; storage_manager_get_info(&total_b, &used_b); int json_c = 0, jpg_c = 0; @@ -1118,12 +1204,11 @@ static void view_device_info(void) const char *ssid = wifi_manager_get_connected_ssid(); int rssi = wifi_manager_get_rssi(); + lv_coord_t wifi_kw = lv_txt_get_width("WI-FI:", 6, &lv_font_montserrat_14, 0, LV_TEXT_FLAG_NONE); + lv_coord_t wifi_val_w = inner_w - wifi_kw - 6; + 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"); - } + get_fitted_wifi_text(wifi_line, sizeof(wifi_line), ssid, rssi, wifi_val_w); struct { const char *key; @@ -1169,10 +1254,14 @@ static void view_device_info(void) lv_obj_set_style_text_color(k, lv_color_hex(0x00E5FF), 0); lv_obj_align(k, LV_ALIGN_TOP_LEFT, 0, y_pos); + lv_coord_t k_w = lv_txt_get_width(rows[i].key, strlen(rows[i].key), &lv_font_montserrat_14, 0, LV_TEXT_FLAG_NONE); + lv_coord_t max_val_w = inner_w - k_w - 6; + truncate_text_to_fit(rows[i].val, sizeof(rows[i].val), max_val_w, &lv_font_montserrat_14); + 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_width(v, max_val_w); + lv_label_set_long_mode(v, LV_LABEL_LONG_CLIP); 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); diff --git a/main/web_server.c b/main/web_server.c index 2a38128..d1245da 100644 --- a/main/web_server.c +++ b/main/web_server.c @@ -16,6 +16,7 @@ #include "wifi_manager.h" #include "dns_server.h" #include "web_server.h" +#include "serial_log.h" static const char *TAG = "web"; @@ -67,6 +68,7 @@ static const char SPA_PAGE[] = "
" "" "" + "" "
" "" "
" @@ -125,13 +127,49 @@ static const char SPA_PAGE[] = "
" "

Loading saved networks...

" "" + "" + "
" + "
" + "
" + "
" + "● Live" + "0 lines" + "
" + "
" + "" + "" + "" + "" + "
" + "
" + "
" + "" + "
" + "Filter:" + "" + "
" + "
" + "
" + "
" + "
" ""; @@ -288,6 +393,8 @@ static esp_err_t handle_wifi_scan(httpd_req_t *req); static esp_err_t handle_wifi_saved(httpd_req_t *req); static esp_err_t handle_wifi_connect(httpd_req_t *req); static esp_err_t handle_wifi_forget(httpd_req_t *req); +static esp_err_t handle_logs(httpd_req_t *req); +static esp_err_t handle_logs_clear(httpd_req_t *req); static const httpd_uri_t URI_ROOT = { .uri = "/", .method = HTTP_GET, .handler = handle_root, .user_ctx = NULL @@ -325,6 +432,14 @@ static const httpd_uri_t URI_WIFI_FORGET = { .uri = "/api/wifi/forget", .method = HTTP_POST, .handler = handle_wifi_forget, .user_ctx = NULL }; +/* Serial Log URIs */ +static const httpd_uri_t URI_LOGS = { + .uri = "/api/logs", .method = HTTP_GET, .handler = handle_logs, .user_ctx = NULL +}; +static const httpd_uri_t URI_LOGS_CLEAR = { + .uri = "/api/logs/clear", .method = HTTP_POST, .handler = handle_logs_clear, .user_ctx = NULL +}; + /* Captive Portal Probe URIs */ static const httpd_uri_t URI_CP_APPLE = { .uri = "/hotspot-detect.html", .method = HTTP_GET, .handler = handle_captive_portal, .user_ctx = NULL @@ -803,6 +918,50 @@ static esp_err_t handle_wifi_forget(httpd_req_t *req) return ESP_OK; } +/* ------------------------------------------------------------------ */ +/* Serial Log endpoints */ +/* ------------------------------------------------------------------ */ + +static esp_err_t handle_logs(httpd_req_t *req) +{ + uint32_t offset = 0; + char query[64] = {0}; + if (httpd_req_get_url_query_str(req, query, sizeof(query)) == ESP_OK) { + char val[24] = {0}; + if (httpd_query_key_value(query, "offset", val, sizeof(val)) == ESP_OK) { + offset = (uint32_t)strtoul(val, NULL, 10); + } + } + + size_t chunk_size = 4096; + char *buf = malloc(chunk_size); + if (buf == NULL) { + httpd_resp_send_500(req); + return ESP_FAIL; + } + + uint32_t next_offset = offset; + size_t bytes_read = serial_log_read(offset, buf, chunk_size, &next_offset); + + char off_str[24]; + snprintf(off_str, sizeof(off_str), "%lu", (unsigned long)next_offset); + + httpd_resp_set_type(req, "text/plain"); + httpd_resp_set_hdr(req, "X-Log-Offset", off_str); + httpd_resp_set_hdr(req, "Access-Control-Expose-Headers", "X-Log-Offset"); + + esp_err_t ret = httpd_resp_send(req, buf, (ssize_t)bytes_read); + free(buf); + return ret; +} + +static esp_err_t handle_logs_clear(httpd_req_t *req) +{ + serial_log_clear(); + send_json(req, 200, "{\"ok\":true}\n"); + return ESP_OK; +} + /* ------------------------------------------------------------------ */ /* SoftAP + server */ /* ------------------------------------------------------------------ */ @@ -841,7 +1000,7 @@ esp_err_t web_server_start(void) httpd_config_t config = HTTPD_DEFAULT_CONFIG(); config.stack_size = 8 * 1024; - config.max_uri_handlers = 24; + config.max_uri_handlers = 32; config.lru_purge_enable = true; config.keep_alive_enable = true; config.max_open_sockets = 4; @@ -867,6 +1026,10 @@ esp_err_t web_server_start(void) ESP_ERROR_CHECK(httpd_register_uri_handler(s_server, &URI_WIFI_CONNECT)); ESP_ERROR_CHECK(httpd_register_uri_handler(s_server, &URI_WIFI_FORGET)); + /* Serial Log APIs */ + ESP_ERROR_CHECK(httpd_register_uri_handler(s_server, &URI_LOGS)); + ESP_ERROR_CHECK(httpd_register_uri_handler(s_server, &URI_LOGS_CLEAR)); + /* Captive Portal OS Probes */ ESP_ERROR_CHECK(httpd_register_uri_handler(s_server, &URI_CP_APPLE)); ESP_ERROR_CHECK(httpd_register_uri_handler(s_server, &URI_CP_204_1));