2 Commits
video ... main

Author SHA1 Message Date
7d8e2a37af fix(ci): track dependencies.lock and define CONFIG_LV_USE_FREETYPE
Some checks failed
ESP32 Build & Release / build (push) Failing after 13s
2026-09-16 17:27:40 +02:00
9c5f62b6de serial log on webserver
Some checks failed
ESP32 Build & Release / build (push) Failing after 15s
2026-09-16 17:08:26 +02:00
11 changed files with 527 additions and 11 deletions

1
.gitignore vendored
View File

@@ -2,7 +2,6 @@
build/ build/
sdkconfig.old sdkconfig.old
sdkconfig.defaults.bak sdkconfig.defaults.bak
dependencies.lock
# ESP Component Manager downloads # ESP Component Manager downloads
managed_components/ managed_components/

55
dependencies.lock Normal file
View File

@@ -0,0 +1,55 @@
dependencies:
espressif/esp_jpeg:
component_hash: defb83669293cbf86d0fa86b475ba5517aceed04ed70db435388c151ab37b5d7
dependencies:
- name: idf
require: private
version: '>=5.0'
source:
registry_url: https://components.espressif.com/
type: service
version: 1.3.1
espressif/esp_lvgl_port:
component_hash: d3c020b45c3dfc0d1706a83b63a1c34a6eb444a6e935f9afff79f92b7b94dcef
dependencies:
- name: idf
require: private
version: '>=5.2'
- name: lvgl/lvgl
registry_url: https://components.espressif.com
require: public
version: '>=8,<10'
source:
registry_url: https://components.espressif.com/
type: service
version: 2.9.0
idf:
source:
type: idf
version: 5.5.5
joltwallet/littlefs:
component_hash: 3af20a3d75c49db9dc2286fd50862c34055c9f578419ad1f2bf7ed3ed01391fe
dependencies:
- name: idf
require: private
version: '>=5.0'
source:
registry_url: https://components.espressif.com/
type: service
version: 1.22.3
lvgl/lvgl:
component_hash: d7c1ac037ae6e85d94897f807d6e7ba0946a83e720074fc95a4f6241da9f9f53
dependencies: []
source:
registry_url: https://components.espressif.com/
type: service
version: 8.4.0
direct_dependencies:
- espressif/esp_jpeg
- espressif/esp_lvgl_port
- idf
- joltwallet/littlefs
- lvgl/lvgl
manifest_hash: 2386ea3980a579a41642d521494a1377dbeef5b30a665d77640d0c1d1c7aba30
target: esp32
version: 2.0.0

View File

@@ -12,5 +12,6 @@ idf_component_register(SRCS
"app_state.c" "app_state.c"
"ota_manager.c" "ota_manager.c"
"dns_server.c" "dns_server.c"
"serial_log.c"
INCLUDE_DIRS "." INCLUDE_DIRS "."
) )

View File

@@ -1,4 +1,6 @@
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "esp_log.h" #include "esp_log.h"
#include "esp_lcd_panel_io.h" #include "esp_lcd_panel_io.h"
#include "esp_lcd_panel_ops.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)); 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)); 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_invert_color(s_panel, true));
ESP_ERROR_CHECK(esp_lcd_panel_mirror(s_panel, true, true)); ESP_ERROR_CHECK(esp_lcd_panel_mirror(s_panel, true, true));
ESP_ERROR_CHECK(esp_lcd_panel_disp_on_off(s_panel, 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 */ /* 2. Initialize LVGL port on Core 1 */
lvgl_port_cfg_t port_cfg = ESP_LVGL_PORT_INIT_CONFIG(); lvgl_port_cfg_t port_cfg = ESP_LVGL_PORT_INIT_CONFIG();

View File

@@ -4,5 +4,5 @@ dependencies:
idf: ">=5.0" idf: ">=5.0"
joltwallet/littlefs: "*" joltwallet/littlefs: "*"
esp_jpeg: ">=1.0.2" esp_jpeg: ">=1.0.2"
lvgl/lvgl: "^8.3.11" lvgl/lvgl: "~8.4.0"
espressif/esp_lvgl_port: "^2.0.0" espressif/esp_lvgl_port: "~2.9.0"

View File

@@ -10,11 +10,15 @@
#include "scryfall_client.h" #include "scryfall_client.h"
#include "web_server.h" #include "web_server.h"
#include "ota_manager.h" #include "ota_manager.h"
#include "serial_log.h"
static const char *TAG = "MAIN"; static const char *TAG = "MAIN";
void app_main(void) 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..."); ESP_LOGI(TAG, "MTG RFID Companion booting...");
/* Validate OTA partition & cancel rollback if just updated */ /* Validate OTA partition & cancel rollback if just updated */

152
main/serial_log.c Normal file
View File

@@ -0,0 +1,152 @@
/*
* MTG RFID Companion - In-memory Serial Log Ring Buffer
*/
#include "serial_log.h"
#include <stdio.h>
#include <string.h>
#include <stdarg.h>
#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;
}

47
main/serial_log.h Normal file
View File

@@ -0,0 +1,47 @@
/*
* MTG RFID Companion - In-memory Serial Log Ring Buffer
*/
#pragma once
#include <stddef.h>
#include <stdint.h>
#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

View File

@@ -1094,6 +1094,90 @@ static void view_storage(void)
lvgl_port_unlock(); 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 */ /* 7. Device & Wi-Fi Info */
static void view_device_info(void) 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_radius(box, 6, 0);
lv_obj_set_style_pad_all(box, 8, 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; size_t total_b = 0, used_b = 0;
storage_manager_get_info(&total_b, &used_b); storage_manager_get_info(&total_b, &used_b);
int json_c = 0, jpg_c = 0; 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(); const char *ssid = wifi_manager_get_connected_ssid();
int rssi = wifi_manager_get_rssi(); 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]; char wifi_line[64];
if (wifi_manager_connected() && ssid && strlen(ssid) > 0) { get_fitted_wifi_text(wifi_line, sizeof(wifi_line), ssid, rssi, wifi_val_w);
snprintf(wifi_line, sizeof(wifi_line), "%s (%ddB)", ssid, rssi);
} else {
snprintf(wifi_line, sizeof(wifi_line), "Disconnected");
}
struct { struct {
const char *key; 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_set_style_text_color(k, lv_color_hex(0x00E5FF), 0);
lv_obj_align(k, LV_ALIGN_TOP_LEFT, 0, y_pos); 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_obj_t *v = lv_label_create(box);
lv_label_set_text(v, rows[i].val); lv_label_set_text(v, rows[i].val);
lv_obj_set_width(v, 120); lv_obj_set_width(v, max_val_w);
lv_label_set_long_mode(v, LV_LABEL_LONG_DOT); 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_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_color(v, lv_color_hex(COLOR_AMBER), 0);
lv_obj_set_style_text_align(v, LV_TEXT_ALIGN_RIGHT, 0); lv_obj_set_style_text_align(v, LV_TEXT_ALIGN_RIGHT, 0);

View File

@@ -16,6 +16,7 @@
#include "wifi_manager.h" #include "wifi_manager.h"
#include "dns_server.h" #include "dns_server.h"
#include "web_server.h" #include "web_server.h"
#include "serial_log.h"
static const char *TAG = "web"; static const char *TAG = "web";
@@ -67,6 +68,7 @@ static const char SPA_PAGE[] =
"<div class=\"tabs\">" "<div class=\"tabs\">"
"<button id=\"tb-cards\" class=\"tab-btn active\" onclick=\"tab('cards')\">&#127183; Card Pairing</button>" "<button id=\"tb-cards\" class=\"tab-btn active\" onclick=\"tab('cards')\">&#127183; Card Pairing</button>"
"<button id=\"tb-wifi\" class=\"tab-btn\" onclick=\"tab('wifi')\">&#128246; Wi-Fi Settings</button>" "<button id=\"tb-wifi\" class=\"tab-btn\" onclick=\"tab('wifi')\">&#128246; Wi-Fi Settings</button>"
"<button id=\"tb-serial\" class=\"tab-btn\" onclick=\"tab('serial')\">&#128187; Serial Log</button>"
"</div>" "</div>"
"<!-- Tab: Cards -->" "<!-- Tab: Cards -->"
"<div id=\"pane-cards\" class=\"tab-pane active\">" "<div id=\"pane-cards\" class=\"tab-pane active\">"
@@ -125,13 +127,49 @@ static const char SPA_PAGE[] =
"</div>" "</div>"
"<div id=\"saved-box\"><p class=\"card-none\">Loading saved networks...</p></div>" "<div id=\"saved-box\"><p class=\"card-none\">Loading saved networks...</p></div>"
"</div>" "</div>"
"<!-- Tab: Serial Log -->"
"<div id=\"pane-serial\" class=\"tab-pane\">"
"<div style=\"background:#1a1a2e;padding:1em;border-radius:8px;border:1px solid #2d2d48;margin-bottom:1.5em\">"
"<div class=\"header-row\" style=\"margin-bottom:.8em;flex-wrap:wrap;gap:8px\">"
"<div style=\"display:flex;align-items:center;gap:8px\">"
"<span id=\"log-badge\" class=\"status-badge badge-ok\" style=\"margin-bottom:0\">● Live</span>"
"<span id=\"log-info\" style=\"font-size:.85em;color:#aaa\">0 lines</span>"
"</div>"
"<div style=\"display:flex;gap:6px;flex-wrap:wrap\">"
"<button class=\"btn\" id=\"btn-pause\" onclick=\"toggleLogPause()\" style=\"margin:0;font-size:.85em\">Pause</button>"
"<button class=\"btn\" onclick=\"clearLogs()\" style=\"margin:0;font-size:.85em\">Clear</button>"
"<button class=\"btn\" onclick=\"copyLogs()\" style=\"margin:0;font-size:.85em\">Copy</button>"
"<button class=\"btn\" onclick=\"downloadLogs()\" style=\"margin:0;font-size:.85em\">Save</button>"
"</div>"
"</div>"
"<div style=\"display:flex;justify-content:space-between;align-items:center;margin-bottom:.6em;font-size:.85em;color:#ccc;flex-wrap:wrap;gap:8px\">"
"<label style=\"display:flex;align-items:center;gap:6px;cursor:pointer\">"
"<input type=\"checkbox\" id=\"log-scroll\" checked style=\"width:auto;margin:0\"> Auto-scroll"
"</label>"
"<div style=\"display:flex;align-items:center;gap:6px\">"
"<span>Filter:</span>"
"<select id=\"log-filter\" onchange=\"renderLogs()\" style=\"width:auto;padding:.3em .6em;font-size:.85em\">"
"<option value=\"\">All levels</option>"
"<option value=\"E\">Errors (E)</option>"
"<option value=\"W\">Warnings &amp; Errors (W/E)</option>"
"<option value=\"I\">Info, Warn, Error (I/W/E)</option>"
"</select>"
"</div>"
"</div>"
"<div id=\"term\" style=\"background:#0d0d17;border:1px solid #28283f;border-radius:6px;padding:.75em;font-family:ui-monospace,SFMono-Regular,Consolas,monospace;font-size:12px;line-height:1.45;height:420px;overflow-y:auto;white-space:pre-wrap;word-break:break-all;color:#ddd\"></div>"
"</div>"
"</div>"
"<script>" "<script>"
"function tab(t){" "function tab(t){"
"document.getElementById('tb-cards').className='tab-btn'+(t==='cards'?' active':'');" "document.getElementById('tb-cards').className='tab-btn'+(t==='cards'?' active':'');"
"document.getElementById('tb-wifi').className='tab-btn'+(t==='wifi'?' active':'');" "document.getElementById('tb-wifi').className='tab-btn'+(t==='wifi'?' active':'');"
"document.getElementById('tb-serial').className='tab-btn'+(t==='serial'?' active':'');"
"document.getElementById('pane-cards').className='tab-pane'+(t==='cards'?' active':'');" "document.getElementById('pane-cards').className='tab-pane'+(t==='cards'?' active':'');"
"document.getElementById('pane-wifi').className='tab-pane'+(t==='wifi'?' active':'');" "document.getElementById('pane-wifi').className='tab-pane'+(t==='wifi'?' active':'');"
"document.getElementById('pane-serial').className='tab-pane'+(t==='serial'?' active':'');"
"if(t==='wifi'){refreshWifiStatus();loadSavedWifi();}" "if(t==='wifi'){refreshWifiStatus();loadSavedWifi();}"
"if(t==='serial'){if(!logPolling)startLogPoll();}"
"else{stopLogPoll();}"
"}" "}"
"var lastUid='';var suggestions=[];" "var lastUid='';var suggestions=[];"
"function esc(s){return s.replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;');}" "function esc(s){return s.replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;');}"
@@ -271,6 +309,73 @@ static const char SPA_PAGE[] =
" else{alert('Failed to forget '+s);}" " else{alert('Failed to forget '+s);}"
" }).catch(function(e){alert('Error: '+e);});" " }).catch(function(e){alert('Error: '+e);});"
"}" "}"
"var logOffset=0;var logLines=[];var logPolling=false;var logPaused=false;var logTimer=null;"
"function parseLogLine(str){"
" var clean=str.replace(/\\x1b\\[[0-9;]*m/g,'');"
" var lvl='I';var m=clean.match(/^([EWIDV])\\s+\\(/);"
" if(m)lvl=m[1];"
" var html=esc(clean);"
" if(lvl==='E')html='<span style=\"color:#ff6b6b;font-weight:600\">'+html+'</span>';"
" else if(lvl==='W')html='<span style=\"color:#ffd75a\">'+html+'</span>';"
" else if(lvl==='I')html='<span style=\"color:#7fd77f\">'+html+'</span>';"
" else if(lvl==='D')html='<span style=\"color:#6be5ff\">'+html+'</span>';"
" else html='<span style=\"color:#aaa\">'+html+'</span>';"
" return {raw:clean,lvl:lvl,html:html};"
"}"
"function renderLogs(){"
" var filter=document.getElementById('log-filter').value;"
" var term=document.getElementById('term');"
" var auto=document.getElementById('log-scroll').checked;"
" var filtered=logLines.filter(function(item){"
" if(!filter)return true;"
" if(filter==='E')return item.lvl==='E';"
" if(filter==='W')return item.lvl==='W'||item.lvl==='E';"
" if(filter==='I')return item.lvl==='I'||item.lvl==='W'||item.lvl==='E';"
" return true;"
" });"
" term.innerHTML=filtered.map(function(item){return item.html;}).join('\\n');"
" document.getElementById('log-info').textContent=logLines.length+' lines';"
" if(auto)term.scrollTop=term.scrollHeight;"
"}"
"function fetchLogs(){"
" if(logPaused)return;"
" fetch('/api/logs?offset='+logOffset).then(function(r){"
" var next=r.headers.get('X-Log-Offset');"
" if(next)logOffset=parseInt(next,10);"
" return r.text();"
" }).then(function(txt){"
" if(!txt||txt.length===0)return;"
" var lines=txt.split('\\n');var added=false;"
" for(var i=0;i<lines.length;i++){"
" var l=lines[i];"
" if(l.length>0||i<lines.length-1){logLines.push(parseLogLine(l));added=true;}"
" }"
" if(logLines.length>1200)logLines=logLines.slice(logLines.length-1000);"
" if(added)renderLogs();"
" }).catch(function(){});"
"}"
"function startLogPoll(){logPolling=true;fetchLogs();if(logTimer)clearInterval(logTimer);logTimer=setInterval(fetchLogs,1200);}"
"function stopLogPoll(){logPolling=false;if(logTimer){clearInterval(logTimer);logTimer=null;}}"
"function toggleLogPause(){"
" logPaused=!logPaused;var btn=document.getElementById('btn-pause');var badge=document.getElementById('log-badge');"
" if(logPaused){btn.textContent='Resume';badge.className='status-badge badge-bad';badge.textContent='⏸ Paused';}"
" else{btn.textContent='Pause';badge.className='status-badge badge-ok';badge.textContent='● Live';fetchLogs();}"
"}"
"function clearLogs(){"
" fetch('/api/logs/clear',{method:'POST'}).catch(function(){});"
" logLines=[];document.getElementById('term').innerHTML='';"
" document.getElementById('log-info').textContent='0 lines';"
"}"
"function copyLogs(){"
" var t=logLines.map(function(item){return item.raw;}).join('\\n');"
" navigator.clipboard.writeText(t).then(function(){alert('Logs copied to clipboard');}).catch(function(){alert('Copy failed');});"
"}"
"function downloadLogs(){"
" var t=logLines.map(function(item){return item.raw;}).join('\\n');"
" var b=new Blob([t],{type:'text/plain'});var u=URL.createObjectURL(b);"
" var a=document.createElement('a');a.href=u;a.download='mtg-companion-serial.log';"
" document.body.appendChild(a);a.click();document.body.removeChild(a);URL.revokeObjectURL(u);"
"}"
"poll();loadCards();refreshWifiStatus();" "poll();loadCards();refreshWifiStatus();"
"</script></body></html>"; "</script></body></html>";
@@ -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_saved(httpd_req_t *req);
static esp_err_t handle_wifi_connect(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_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 = { static const httpd_uri_t URI_ROOT = {
.uri = "/", .method = HTTP_GET, .handler = handle_root, .user_ctx = NULL .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 .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 */ /* Captive Portal Probe URIs */
static const httpd_uri_t URI_CP_APPLE = { static const httpd_uri_t URI_CP_APPLE = {
.uri = "/hotspot-detect.html", .method = HTTP_GET, .handler = handle_captive_portal, .user_ctx = NULL .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; 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 */ /* SoftAP + server */
/* ------------------------------------------------------------------ */ /* ------------------------------------------------------------------ */
@@ -841,7 +1000,7 @@ esp_err_t web_server_start(void)
httpd_config_t config = HTTPD_DEFAULT_CONFIG(); httpd_config_t config = HTTPD_DEFAULT_CONFIG();
config.stack_size = 8 * 1024; config.stack_size = 8 * 1024;
config.max_uri_handlers = 24; config.max_uri_handlers = 32;
config.lru_purge_enable = true; config.lru_purge_enable = true;
config.keep_alive_enable = true; config.keep_alive_enable = true;
config.max_open_sockets = 4; 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_CONNECT));
ESP_ERROR_CHECK(httpd_register_uri_handler(s_server, &URI_WIFI_FORGET)); 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 */ /* 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_APPLE));
ESP_ERROR_CHECK(httpd_register_uri_handler(s_server, &URI_CP_204_1)); ESP_ERROR_CHECK(httpd_register_uri_handler(s_server, &URI_CP_204_1));

View File

@@ -21,6 +21,7 @@ CONFIG_LV_FONT_MONTSERRAT_12=y
CONFIG_LV_FONT_MONTSERRAT_14=y CONFIG_LV_FONT_MONTSERRAT_14=y
CONFIG_LV_FONT_MONTSERRAT_20=y CONFIG_LV_FONT_MONTSERRAT_20=y
CONFIG_LV_FONT_MONTSERRAT_48=y CONFIG_LV_FONT_MONTSERRAT_48=y
CONFIG_LV_USE_FREETYPE=n
# tjpgd JPEG Decoder (RGB565 output) # tjpgd JPEG Decoder (RGB565 output)
CONFIG_JD_USE_ROM=n CONFIG_JD_USE_ROM=n