various bug fixes
This commit is contained in:
@@ -16,9 +16,15 @@ const char *app_state_name(app_state_t state)
|
||||
{
|
||||
switch (state) {
|
||||
case APP_STATE_BOOT: return "BOOT";
|
||||
case APP_STATE_MENU: return "MENU";
|
||||
case APP_STATE_SCANNER: return "SCANNER";
|
||||
case APP_STATE_CARD_VIEW: return "CARD_VIEW";
|
||||
case APP_STATE_LIFE_COUNTER: return "LIFE_COUNTER";
|
||||
case APP_STATE_DICE_ROLLER: return "DICE_ROLLER";
|
||||
case APP_STATE_COMMANDER_TRACKER: return "COMMANDER_TRACKER";
|
||||
case APP_STATE_TOKEN_SPAWNER: return "TOKEN_SPAWNER";
|
||||
case APP_STATE_STORAGE_MANAGER: return "STORAGE_MANAGER";
|
||||
case APP_STATE_DEVICE_INFO: return "DEVICE_INFO";
|
||||
case APP_STATE_REGISTRATION: return "REGISTRATION";
|
||||
}
|
||||
return "UNKNOWN";
|
||||
|
||||
@@ -16,9 +16,15 @@ extern "C" {
|
||||
|
||||
typedef enum {
|
||||
APP_STATE_BOOT = 0,
|
||||
APP_STATE_MENU,
|
||||
APP_STATE_SCANNER,
|
||||
APP_STATE_CARD_VIEW,
|
||||
APP_STATE_LIFE_COUNTER,
|
||||
APP_STATE_DICE_ROLLER,
|
||||
APP_STATE_COMMANDER_TRACKER,
|
||||
APP_STATE_TOKEN_SPAWNER,
|
||||
APP_STATE_STORAGE_MANAGER,
|
||||
APP_STATE_DEVICE_INFO,
|
||||
APP_STATE_REGISTRATION,
|
||||
} app_state_t;
|
||||
|
||||
|
||||
@@ -9,12 +9,71 @@
|
||||
#include "hw_pins.h"
|
||||
#include "buzzer.h"
|
||||
|
||||
#include "freertos/queue.h"
|
||||
|
||||
static const char *TAG = "buzzer";
|
||||
|
||||
#define BUZZER_SPEED_MODE LEDC_LOW_SPEED_MODE
|
||||
#define BUZZER_TIMER_NUM LEDC_TIMER_0
|
||||
#define BUZZER_CHANNEL LEDC_CHANNEL_0
|
||||
|
||||
static QueueHandle_t s_sound_queue = NULL;
|
||||
|
||||
static const buzzer_note_t MELODY_VICTORY[] = {
|
||||
{NOTE_C5, 90}, {NOTE_C5, 90}, {NOTE_C5, 90}, {NOTE_C5, 220},
|
||||
{NOTE_GS4, 220}, {NOTE_AS4, 220}, {NOTE_C5, 140}, {NOTE_REST, 35},
|
||||
{NOTE_AS4, 90}, {NOTE_C5, 400}
|
||||
};
|
||||
|
||||
static const buzzer_note_t MELODY_LEGENDARY[] = {
|
||||
{NOTE_G4, 65}, {NOTE_C5, 65}, {NOTE_E5, 65}, {NOTE_G5, 90}, {NOTE_C6, 280}
|
||||
};
|
||||
|
||||
static const buzzer_note_t MELODY_LOSS[] = {
|
||||
{NOTE_D4, 130}, {NOTE_CS4, 130}, {NOTE_C4, 130}, {NOTE_B3, 260},
|
||||
{NOTE_REST, 35}, {NOTE_AS3, 400}
|
||||
};
|
||||
|
||||
esp_err_t buzzer_play_notes(const buzzer_note_t *notes, size_t count)
|
||||
{
|
||||
if (notes == NULL || count == 0) return ESP_OK;
|
||||
for (size_t i = 0; i < count; i++) {
|
||||
if (notes[i].freq_hz > 0) {
|
||||
buzzer_start(notes[i].freq_hz, 30);
|
||||
vTaskDelay(pdMS_TO_TICKS(notes[i].duration_ms));
|
||||
buzzer_stop();
|
||||
} else {
|
||||
vTaskDelay(pdMS_TO_TICKS(notes[i].duration_ms));
|
||||
}
|
||||
/* 12ms articulation gap between notes for distinct 8-bit chiptune feel */
|
||||
vTaskDelay(pdMS_TO_TICKS(12));
|
||||
}
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
static void buzzer_task(void *arg)
|
||||
{
|
||||
(void)arg;
|
||||
buzzer_sound_t sound;
|
||||
while (1) {
|
||||
if (xQueueReceive(s_sound_queue, &sound, portMAX_DELAY) == pdTRUE) {
|
||||
switch (sound) {
|
||||
case BUZZER_SOUND_VICTORY:
|
||||
buzzer_play_notes(MELODY_VICTORY, sizeof(MELODY_VICTORY) / sizeof(MELODY_VICTORY[0]));
|
||||
break;
|
||||
case BUZZER_SOUND_LEGENDARY:
|
||||
buzzer_play_notes(MELODY_LEGENDARY, sizeof(MELODY_LEGENDARY) / sizeof(MELODY_LEGENDARY[0]));
|
||||
break;
|
||||
case BUZZER_SOUND_LOSS:
|
||||
buzzer_play_notes(MELODY_LOSS, sizeof(MELODY_LOSS) / sizeof(MELODY_LOSS[0]));
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
esp_err_t buzzer_init(void)
|
||||
{
|
||||
ledc_timer_config_t timer_cfg = {
|
||||
@@ -42,7 +101,12 @@ esp_err_t buzzer_init(void)
|
||||
return ret;
|
||||
}
|
||||
|
||||
ESP_LOGI(TAG, "buzzer ready on GPIO %d", HW_BUZZER_GPIO);
|
||||
if (s_sound_queue == NULL) {
|
||||
s_sound_queue = xQueueCreate(4, sizeof(buzzer_sound_t));
|
||||
xTaskCreate(buzzer_task, "buzzer_task", 2048, NULL, 3, NULL);
|
||||
}
|
||||
|
||||
ESP_LOGI(TAG, "buzzer ready on GPIO %d (chiptune synth active)", HW_BUZZER_GPIO);
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
@@ -81,4 +145,12 @@ esp_err_t buzzer_chirp(uint32_t freq_hz, uint32_t duration_ms)
|
||||
}
|
||||
vTaskDelay(pdMS_TO_TICKS(duration_ms));
|
||||
return buzzer_stop();
|
||||
}
|
||||
|
||||
esp_err_t buzzer_play_sound(buzzer_sound_t sound)
|
||||
{
|
||||
if (s_sound_queue != NULL) {
|
||||
xQueueSend(s_sound_queue, &sound, 0);
|
||||
}
|
||||
return ESP_OK;
|
||||
}
|
||||
@@ -12,8 +12,56 @@
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
/* Musical note frequencies (Hz) */
|
||||
#define NOTE_REST 0
|
||||
#define NOTE_G3 196
|
||||
#define NOTE_GS3 208
|
||||
#define NOTE_A3 220
|
||||
#define NOTE_AS3 233
|
||||
#define NOTE_B3 247
|
||||
#define NOTE_C4 262
|
||||
#define NOTE_CS4 277
|
||||
#define NOTE_D4 294
|
||||
#define NOTE_DS4 311
|
||||
#define NOTE_E4 330
|
||||
#define NOTE_F4 349
|
||||
#define NOTE_FS4 370
|
||||
#define NOTE_G4 392
|
||||
#define NOTE_GS4 415
|
||||
#define NOTE_A4 440
|
||||
#define NOTE_AS4 466
|
||||
#define NOTE_B4 494
|
||||
#define NOTE_C5 523
|
||||
#define NOTE_CS5 554
|
||||
#define NOTE_D5 587
|
||||
#define NOTE_DS5 622
|
||||
#define NOTE_E5 659
|
||||
#define NOTE_F5 698
|
||||
#define NOTE_FS5 740
|
||||
#define NOTE_G5 784
|
||||
#define NOTE_GS5 831
|
||||
#define NOTE_A5 880
|
||||
#define NOTE_AS5 932
|
||||
#define NOTE_B5 988
|
||||
#define NOTE_C6 1047
|
||||
#define NOTE_D6 1175
|
||||
#define NOTE_E6 1319
|
||||
#define NOTE_G6 1568
|
||||
|
||||
typedef struct {
|
||||
uint16_t freq_hz;
|
||||
uint16_t duration_ms;
|
||||
} buzzer_note_t;
|
||||
|
||||
typedef enum {
|
||||
BUZZER_SOUND_NONE = 0,
|
||||
BUZZER_SOUND_VICTORY,
|
||||
BUZZER_SOUND_LEGENDARY,
|
||||
BUZZER_SOUND_LOSS,
|
||||
} buzzer_sound_t;
|
||||
|
||||
/**
|
||||
* @brief Initialize LEDC PWM channel for the buzzer.
|
||||
* @brief Initialize LEDC PWM channel for the buzzer and starts worker task.
|
||||
*/
|
||||
esp_err_t buzzer_init(void);
|
||||
|
||||
@@ -34,6 +82,16 @@ esp_err_t buzzer_stop(void);
|
||||
*/
|
||||
esp_err_t buzzer_chirp(uint32_t freq_hz, uint32_t duration_ms);
|
||||
|
||||
/**
|
||||
* @brief Play an 8-bit melody asynchronously (non-blocking).
|
||||
*/
|
||||
esp_err_t buzzer_play_sound(buzzer_sound_t sound);
|
||||
|
||||
/**
|
||||
* @brief Play a custom sequence of notes synchronously.
|
||||
*/
|
||||
esp_err_t buzzer_play_notes(const buzzer_note_t *notes, size_t count);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
@@ -1,38 +1,34 @@
|
||||
/*
|
||||
* MTG RFID Companion - ST7789 display manager (Core 1)
|
||||
* MTG RFID Companion - ST7789 display manager with LVGL (ponytail: native library)
|
||||
*/
|
||||
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <stdio.h>
|
||||
#include "esp_log.h"
|
||||
#include "esp_lcd_panel_io.h"
|
||||
#include "esp_lcd_panel_ops.h"
|
||||
#include "esp_lcd_panel_vendor.h"
|
||||
#include "esp_lcd_panel_st7789.h"
|
||||
#include "driver/spi_master.h"
|
||||
#include "tjpgd.h"
|
||||
#include "hw_pins.h"
|
||||
#include "spi_bus_manager.h"
|
||||
#include "display_manager.h"
|
||||
#include "ui_font.h"
|
||||
|
||||
static const char *TAG = "display";
|
||||
|
||||
#define LCD_HOST HW_SPI_HOST
|
||||
#define LCD_PIXEL_CLOCK_HZ (20 * 1000 * 1000)
|
||||
#define LCD_PIXEL_CLOCK_HZ (16 * 1000 * 1000)
|
||||
#define LCD_CMD_BITS 8
|
||||
#define LCD_PARAM_BITS 8
|
||||
#define LCD_TRANS_QUEUE 10
|
||||
|
||||
#define JPEG_POOL_SIZE (10 * 1024)
|
||||
#define JPEG_SBUF (512) /* must match CONFIG_JD_SZBUF */
|
||||
static esp_lcd_panel_handle_t s_panel = NULL;
|
||||
static lv_disp_t *s_disp = NULL;
|
||||
|
||||
static esp_lcd_panel_handle_t s_panel;
|
||||
static uint16_t *s_staging = NULL; /* 240 px line staging buffer for JPEG */
|
||||
|
||||
static esp_err_t panel_io_init(esp_lcd_panel_io_handle_t *io)
|
||||
esp_err_t display_manager_init(void)
|
||||
{
|
||||
if (s_disp != NULL) {
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
/* 1. Initialize ST7789 panel via esp_lcd */
|
||||
esp_lcd_panel_io_handle_t io = NULL;
|
||||
esp_lcd_panel_io_spi_config_t io_config = {
|
||||
.dc_gpio_num = HW_LCD_DC_GPIO,
|
||||
.cs_gpio_num = HW_LCD_CS_GPIO,
|
||||
@@ -40,19 +36,9 @@ static esp_err_t panel_io_init(esp_lcd_panel_io_handle_t *io)
|
||||
.lcd_cmd_bits = LCD_CMD_BITS,
|
||||
.lcd_param_bits = LCD_PARAM_BITS,
|
||||
.spi_mode = 0,
|
||||
.trans_queue_depth = LCD_TRANS_QUEUE,
|
||||
.trans_queue_depth = 10,
|
||||
};
|
||||
return esp_lcd_new_panel_io_spi((esp_lcd_spi_bus_handle_t)LCD_HOST, &io_config, io);
|
||||
}
|
||||
|
||||
esp_err_t display_manager_init(void)
|
||||
{
|
||||
if (s_panel != NULL) {
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
esp_lcd_panel_io_handle_t io = NULL;
|
||||
esp_err_t ret = panel_io_init(&io);
|
||||
esp_err_t ret = esp_lcd_new_panel_io_spi((esp_lcd_spi_bus_handle_t)LCD_HOST, &io_config, &io);
|
||||
if (ret != ESP_OK) {
|
||||
ESP_LOGE(TAG, "panel io init failed: %s", esp_err_to_name(ret));
|
||||
return ret;
|
||||
@@ -72,214 +58,48 @@ esp_err_t display_manager_init(void)
|
||||
ESP_ERROR_CHECK(esp_lcd_panel_reset(s_panel));
|
||||
ESP_ERROR_CHECK(esp_lcd_panel_init(s_panel));
|
||||
ESP_ERROR_CHECK(esp_lcd_panel_invert_color(s_panel, true));
|
||||
/* No panel mirror: the font bit-order fix in display_put_text already
|
||||
* yields correct (non-mirrored) glyphs. Mirroring here would flip the
|
||||
* whole frame and re-break the text. */
|
||||
ESP_ERROR_CHECK(esp_lcd_panel_mirror(s_panel, false, false));
|
||||
ESP_ERROR_CHECK(esp_lcd_panel_disp_on_off(s_panel, true));
|
||||
|
||||
s_staging = malloc(DISP_W * sizeof(uint16_t));
|
||||
if (s_staging == NULL) {
|
||||
ESP_LOGE(TAG, "staging buffer alloc failed");
|
||||
return ESP_ERR_NO_MEM;
|
||||
/* 2. Initialize LVGL port on Core 1 */
|
||||
lvgl_port_cfg_t port_cfg = ESP_LVGL_PORT_INIT_CONFIG();
|
||||
port_cfg.task_affinity = 1;
|
||||
ret = lvgl_port_init(&port_cfg);
|
||||
if (ret != ESP_OK) {
|
||||
ESP_LOGE(TAG, "lvgl port init failed: %s", esp_err_to_name(ret));
|
||||
return ret;
|
||||
}
|
||||
|
||||
ESP_LOGI(TAG, "ST7789 %dx%d ready (CS=%d DC=%d RST=%d)",
|
||||
/* 20 lines buffer (240 * 20 * 2 = 9.6 KB) - lean SRAM footprint */
|
||||
lvgl_port_display_cfg_t disp_cfg = {
|
||||
.io_handle = io,
|
||||
.panel_handle = s_panel,
|
||||
.buffer_size = DISP_W * 20,
|
||||
.double_buffer = false,
|
||||
.hres = DISP_W,
|
||||
.vres = DISP_H,
|
||||
.monochrome = false,
|
||||
.rotation = {
|
||||
.swap_xy = false,
|
||||
.mirror_x = false,
|
||||
.mirror_y = false,
|
||||
},
|
||||
.flags = {
|
||||
.buff_dma = true,
|
||||
},
|
||||
};
|
||||
s_disp = lvgl_port_add_disp(&disp_cfg);
|
||||
if (s_disp == NULL) {
|
||||
ESP_LOGE(TAG, "lvgl_port_add_disp failed");
|
||||
return ESP_FAIL;
|
||||
}
|
||||
|
||||
ESP_LOGI(TAG, "ST7789 %dx%d with LVGL ready (CS=%d DC=%d RST=%d)",
|
||||
DISP_W, DISP_H, HW_LCD_CS_GPIO, HW_LCD_DC_GPIO, HW_LCD_RST_GPIO);
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
void display_draw_bitmap(int x0, int y0, int x1, int y1, const uint16_t *pixels)
|
||||
lv_disp_t *display_get_disp(void)
|
||||
{
|
||||
if (s_panel == NULL || pixels == NULL) {
|
||||
return;
|
||||
}
|
||||
esp_lcd_panel_draw_bitmap(s_panel, x0, y0, x1, y1, pixels);
|
||||
}
|
||||
|
||||
void display_fill_rect(int x0, int y0, int x1, int y1, uint16_t color)
|
||||
{
|
||||
if (x0 < 0) x0 = 0;
|
||||
if (y0 < 0) y0 = 0;
|
||||
if (x1 > DISP_W) x1 = DISP_W;
|
||||
if (y1 > DISP_H) y1 = DISP_H;
|
||||
if (x1 <= x0 || y1 <= y0) {
|
||||
return;
|
||||
}
|
||||
|
||||
int width = x1 - x0;
|
||||
int height = y1 - y0;
|
||||
|
||||
static uint16_t line[DISP_W];
|
||||
for (int i = 0; i < width; i++) {
|
||||
line[i] = color;
|
||||
}
|
||||
for (int r = 0; r < height; r++) {
|
||||
display_draw_bitmap(x0, y0 + r, x1, y0 + r + 1, line);
|
||||
}
|
||||
}
|
||||
|
||||
void display_fill(uint16_t color)
|
||||
{
|
||||
display_fill_rect(0, 0, DISP_W, DISP_H, color);
|
||||
}
|
||||
|
||||
int display_put_text(int x, int y, const char *str, uint16_t fg, uint16_t bg)
|
||||
{
|
||||
int x_start = x;
|
||||
|
||||
while (str != NULL && *str != '\0') {
|
||||
char c = *str++;
|
||||
if (c >= FONT5X7_FIRST_CHAR && c <= FONT5X7_LAST_CHAR) {
|
||||
const uint8_t *glyph = font5x7[c - FONT5X7_FIRST_CHAR];
|
||||
/* Row-major [row][col]. The font table stores each row with the
|
||||
* MSB as the LEFT column, so read bit (W-1-gx) for column gx. */
|
||||
uint16_t cell[FONT5X7_H][FONT5X7_W];
|
||||
|
||||
for (int gy = 0; gy < FONT5X7_H; gy++) {
|
||||
for (int gx = 0; gx < FONT5X7_W; gx++) {
|
||||
cell[gy][gx] = (glyph[gy] >> (FONT5X7_W - 1 - gx)) & 1 ? fg : bg;
|
||||
}
|
||||
}
|
||||
if (x + FONT5X7_W <= DISP_W && y + FONT5X7_H <= DISP_H) {
|
||||
display_draw_bitmap(x, y, x + FONT5X7_W, y + FONT5X7_H, &cell[0][0]);
|
||||
}
|
||||
x += FONT5X7_W + 1;
|
||||
} else if (c == ' ') {
|
||||
x += FONT5X7_W + 1;
|
||||
}
|
||||
}
|
||||
return x - x_start;
|
||||
}
|
||||
|
||||
void display_put_text_center(int y, const char *str, uint16_t fg, uint16_t bg)
|
||||
{
|
||||
int len = (int)strlen(str);
|
||||
int width = len * (FONT5X7_W + 1) - 1;
|
||||
int x = (DISP_W - width) / 2;
|
||||
if (x < 0) x = 0;
|
||||
display_put_text(x, y, str, fg, bg);
|
||||
}
|
||||
|
||||
void display_draw_big_text(int x, int y, const char *str, int scale, uint16_t fg, uint16_t bg)
|
||||
{
|
||||
int cell_w = FONT5X7_W * scale;
|
||||
int cell_h = FONT5X7_H * scale;
|
||||
int num = (int)strlen(str);
|
||||
if (num <= 0) {
|
||||
return;
|
||||
}
|
||||
if (x + num * (cell_w + scale) - scale > DISP_W || y + cell_h > DISP_H) {
|
||||
return;
|
||||
}
|
||||
|
||||
/* Staging buffer for one glyph, scaled */
|
||||
uint16_t *glyph_buf = malloc((size_t)cell_w * cell_h * sizeof(uint16_t));
|
||||
if (glyph_buf == NULL) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (int i = 0; i < num; i++) {
|
||||
char c = str[i];
|
||||
if (c < FONT5X7_FIRST_CHAR || c > FONT5X7_LAST_CHAR) {
|
||||
x += cell_w + scale;
|
||||
continue;
|
||||
}
|
||||
const uint8_t *g = font5x7[c - FONT5X7_FIRST_CHAR];
|
||||
|
||||
for (int gy = 0; gy < FONT5X7_H; gy++) {
|
||||
for (int gx = 0; gx < FONT5X7_W; gx++) {
|
||||
uint16_t color = ((g[gy] >> gx) & 1) ? fg : bg;
|
||||
for (int sy = 0; sy < scale; sy++) {
|
||||
for (int sx = 0; sx < scale; sx++) {
|
||||
glyph_buf[(gy * scale + sy) * cell_w + (gx * scale + sx)] = color;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
display_draw_bitmap(x, y, x + cell_w, y + cell_h, glyph_buf);
|
||||
x += cell_w + scale;
|
||||
}
|
||||
free(glyph_buf);
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* tjpgd streaming decode */
|
||||
/* ------------------------------------------------------------------ */
|
||||
|
||||
typedef struct {
|
||||
FILE *f;
|
||||
} jpeg_io_t;
|
||||
|
||||
static size_t jpeg_input_cb(JDEC *jd, uint8_t *buf, size_t ndata)
|
||||
{
|
||||
jpeg_io_t *io = (jpeg_io_t *)jd->device;
|
||||
if (io == NULL || io->f == NULL) {
|
||||
return 0;
|
||||
}
|
||||
if (buf == NULL) {
|
||||
/* tjpgd requests skipping bytes during header scan */
|
||||
if (fseek(io->f, (long)ndata, SEEK_CUR) == 0) {
|
||||
return ndata;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
return fread(buf, 1, ndata, io->f);
|
||||
}
|
||||
|
||||
static int jpeg_output_cb(JDEC *jd, void *bitmap, JRECT *rect)
|
||||
{
|
||||
(void)jd;
|
||||
uint16_t *pix = (uint16_t *)bitmap;
|
||||
int w = rect->right - rect->left + 1;
|
||||
int h = rect->bottom - rect->top + 1;
|
||||
|
||||
for (int row = 0; row < h; row++) {
|
||||
/* Convert little-endian RGB565 words (tjpgd) to big-endian wire order */
|
||||
for (int col = 0; col < w; col++) {
|
||||
uint16_t v = pix[row * w + col];
|
||||
s_staging[col] = (uint16_t)((v << 8) | (v >> 8));
|
||||
}
|
||||
display_draw_bitmap(rect->left, rect->top + row, rect->left + w, rect->top + row + 1, s_staging);
|
||||
}
|
||||
return 1; /* continue decoding */
|
||||
}
|
||||
|
||||
esp_err_t display_show_jpeg_file(const char *path)
|
||||
{
|
||||
esp_err_t ret = ESP_FAIL;
|
||||
FILE *f = fopen(path, "rb");
|
||||
if (f == NULL) {
|
||||
ESP_LOGE(TAG, "cannot open %s", path);
|
||||
return ESP_ERR_NOT_FOUND;
|
||||
}
|
||||
|
||||
jpeg_io_t io = { .f = f };
|
||||
JDEC jdec;
|
||||
void *pool = malloc(JPEG_POOL_SIZE);
|
||||
if (pool == NULL) {
|
||||
ESP_LOGE(TAG, "jpeg pool alloc failed");
|
||||
fclose(f);
|
||||
return ESP_ERR_NO_MEM;
|
||||
}
|
||||
|
||||
JRESULT jr = jd_prepare(&jdec, jpeg_input_cb, pool, JPEG_POOL_SIZE, &io);
|
||||
if (jr != JDR_OK) {
|
||||
ESP_LOGE(TAG, "jd_prepare failed (%d)", jr);
|
||||
goto done;
|
||||
}
|
||||
|
||||
ESP_LOGI(TAG, "decoding %s: %ux%u", path, (unsigned)jdec.width, (unsigned)jdec.height);
|
||||
jr = jd_decomp(&jdec, jpeg_output_cb, 0);
|
||||
if (jr != JDR_OK) {
|
||||
ESP_LOGE(TAG, "jd_decomp failed (%d)", jr);
|
||||
goto done;
|
||||
}
|
||||
ESP_LOGI(TAG, "JPEG displayed: %s", path);
|
||||
ret = ESP_OK;
|
||||
|
||||
done:
|
||||
free(pool);
|
||||
fclose(f);
|
||||
return ret;
|
||||
return s_disp;
|
||||
}
|
||||
@@ -1,15 +1,12 @@
|
||||
/*
|
||||
* MTG RFID Companion - ST7789 display manager (Core 1)
|
||||
*
|
||||
* Phase 4: Initializes the ST7789 240x320 panel via esp_lcd as a device on
|
||||
* the shared SPI2_HOST bus, provides RGB565 rendering primitives, glyph text,
|
||||
* and streaming tjpgd JPEG decode into the panel.
|
||||
* MTG RFID Companion - ST7789 display manager with LVGL (ponytail: native library)
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "esp_err.h"
|
||||
#include <stdint.h>
|
||||
#include "esp_lvgl_port.h"
|
||||
#include "lvgl.h"
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
@@ -18,59 +15,8 @@ extern "C" {
|
||||
#define DISP_W (240)
|
||||
#define DISP_H (320)
|
||||
|
||||
#define RGB565(r, g, b) ((uint16_t)((((r)&0xF8) << 8) | (((g)&0xFC) << 3) | (((b)&0xF8) >> 3)))
|
||||
|
||||
#define COLOR_BLACK RGB565(0, 0, 0)
|
||||
#define COLOR_WHITE RGB565(255, 255, 255)
|
||||
#define COLOR_RED RGB565(255, 0, 0)
|
||||
#define COLOR_GREEN RGB565(0, 255, 0)
|
||||
#define COLOR_BLUE RGB565(0, 0, 255)
|
||||
#define COLOR_YELLOW RGB565(255, 255, 0)
|
||||
#define COLOR_CYAN RGB565(0, 255, 255)
|
||||
#define COLOR_MAGENTA RGB565(255, 0, 255)
|
||||
#define COLOR_GRAY RGB565(128, 128, 128)
|
||||
|
||||
/**
|
||||
* @brief Initialize the ST7789 panel. Requires the shared SPI bus to exist.
|
||||
*/
|
||||
esp_err_t display_manager_init(void);
|
||||
|
||||
/**
|
||||
* @brief Fill the whole screen with a color.
|
||||
*/
|
||||
void display_fill(uint16_t color);
|
||||
|
||||
/**
|
||||
* @brief Fill a rectangle. x1/y1 are exclusive.
|
||||
*/
|
||||
void display_fill_rect(int x0, int y0, int x1, int y1, uint16_t color);
|
||||
|
||||
/**
|
||||
* @brief Draw pixels. x1/y1 exclusive, buffer length = (x1-x0)*(y1-y0) RGB565 words.
|
||||
*/
|
||||
void display_draw_bitmap(int x0, int y0, int x1, int y1, const uint16_t *pixels);
|
||||
|
||||
/**
|
||||
* @brief Draw text with a 5x7 font. Returns pixel width consumed.
|
||||
*/
|
||||
int display_put_text(int x, int y, const char *str, uint16_t fg, uint16_t bg);
|
||||
|
||||
/**
|
||||
* @brief Draw text centered horizontally on the given row.
|
||||
*/
|
||||
void display_put_text_center(int y, const char *str, uint16_t fg, uint16_t bg);
|
||||
|
||||
/**
|
||||
* @brief Draw large digits (life counter style) using a scaled 5x7 pattern.
|
||||
* @param x,y Top-left corner; scale = pixel size of one 5x7 cell.
|
||||
*/
|
||||
void display_draw_big_text(int x, int y, const char *str, int scale, uint16_t fg, uint16_t bg);
|
||||
|
||||
/**
|
||||
* @brief Stream-decode a JPEG from LittleFS and draw it at the top-left.
|
||||
* Uses tjpgd with a line-by-line output into the panel DMA path.
|
||||
*/
|
||||
esp_err_t display_show_jpeg_file(const char *path);
|
||||
lv_disp_t *display_get_disp(void);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
|
||||
121
main/encoder.c
121
main/encoder.c
@@ -1,5 +1,11 @@
|
||||
/*
|
||||
* MTG RFID Companion - EC11 rotary encoder driver
|
||||
*
|
||||
* Implements a true Gray Code quadrature state machine.
|
||||
* Both CLK and DT pins generate interrupts on ANYEDGE.
|
||||
* Contact bounce is inherently rejected by valid transition filtering.
|
||||
* Each mechanical detent ("tick") advances through a fixed number of valid
|
||||
* quadrature transitions (default 4 for standard full-step EC11, or 2 for half-step).
|
||||
*/
|
||||
|
||||
#include "freertos/FreeRTOS.h"
|
||||
@@ -12,38 +18,87 @@
|
||||
static const char *TAG = "encoder";
|
||||
|
||||
static volatile int32_t s_delta = 0;
|
||||
static volatile int8_t s_accum = 0;
|
||||
static volatile uint8_t s_prev_state = 0x03;
|
||||
static volatile int8_t s_steps_per_detent = 4; /* 4 = full-step, 2 = half-step */
|
||||
|
||||
static volatile bool s_button_pressed = false;
|
||||
static volatile uint32_t s_last_sw_tick = 0;
|
||||
|
||||
#define SW_DEBOUNCE_MS (35)
|
||||
|
||||
/* Quadrature decode: on each CLK edge, sample DT.
|
||||
* DT == CLK level => one direction, != => other.
|
||||
* Physical wiring is corrected here if a knob feels inverted. */
|
||||
#define DIR_CLK_HIGH (1)
|
||||
/*
|
||||
* If the direction comes out reversed, flip this to -1.
|
||||
* Quadrature Gray-Code Transition Table
|
||||
* Index: (old_state << 2) | new_state (4-bit, 0..15)
|
||||
* State bits: bit 1 = CLK (GPIO 32), bit 0 = DT (GPIO 33)
|
||||
*
|
||||
* Clockwise sequence: 11 (3) -> 01 (1) -> 00 (0) -> 10 (2) -> 11 (3)
|
||||
* 11->01 (13): +1
|
||||
* 01->00 (4): +1
|
||||
* 00->10 (2): +1
|
||||
* 10->11 (11): +1
|
||||
*
|
||||
* Counter-Clockwise sequence: 11 (3) -> 10 (2) -> 00 (0) -> 01 (1) -> 11 (3)
|
||||
* 11->10 (14): -1
|
||||
* 10->00 (8): -1
|
||||
* 00->01 (1): -1
|
||||
* 01->11 (7): -1
|
||||
*
|
||||
* All bounces (e.g. 11 <-> 01) cancel out: +1 + (-1) = 0.
|
||||
* Invalid diagonal skips (00 <-> 11, 01 <-> 10): 0.
|
||||
*/
|
||||
#define ENC_DIRECTION (1)
|
||||
static const int8_t s_quad_table[16] = {
|
||||
[0] = 0,
|
||||
[1] = -1, // 00 -> 01 (CCW)
|
||||
[2] = 1, // 00 -> 10 (CW)
|
||||
[3] = 0, // 00 -> 11 (invalid)
|
||||
[4] = 1, // 01 -> 00 (CW)
|
||||
[5] = 0,
|
||||
[6] = 0, // 01 -> 10 (invalid)
|
||||
[7] = -1, // 01 -> 11 (CCW)
|
||||
[8] = -1, // 10 -> 00 (CCW)
|
||||
[9] = 0, // 10 -> 01 (invalid)
|
||||
[10] = 0,
|
||||
[11] = 1, // 10 -> 11 (CW)
|
||||
[12] = 0, // 11 -> 00 (invalid)
|
||||
[13] = 1, // 11 -> 01 (CW)
|
||||
[14] = -1, // 11 -> 10 (CCW)
|
||||
[15] = 0,
|
||||
};
|
||||
|
||||
static void IRAM_ATTR encoder_isr(void *arg)
|
||||
{
|
||||
uint32_t gpio = (uint32_t)(uintptr_t)arg;
|
||||
|
||||
if (gpio == HW_ENC_CLK_GPIO) {
|
||||
int dt = gpio_get_level(HW_ENC_DT_GPIO);
|
||||
int clk = gpio_get_level(HW_ENC_CLK_GPIO);
|
||||
/* Increment on one edge per detent; using level comparison on both edges
|
||||
* doubles the count, so we only act when CLK transitions are detected as
|
||||
* ticks. To keep this robust we count on every edge and divide the
|
||||
* sense: signed direction * 1. */
|
||||
s_delta += (dt == clk) ? (ENC_DIRECTION) : (-ENC_DIRECTION);
|
||||
} else if (gpio == HW_ENC_SW_GPIO) {
|
||||
if (gpio == HW_ENC_SW_GPIO) {
|
||||
unsigned int now = (unsigned int)xTaskGetTickCountFromISR();
|
||||
if ((now - s_last_sw_tick) > pdMS_TO_TICKS(SW_DEBOUNCE_MS)) {
|
||||
s_last_sw_tick = now;
|
||||
s_button_pressed = true;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
/* Read current levels of both encoder channels */
|
||||
uint8_t clk = (uint8_t)gpio_get_level(HW_ENC_CLK_GPIO);
|
||||
uint8_t dt = (uint8_t)gpio_get_level(HW_ENC_DT_GPIO);
|
||||
uint8_t curr = (clk << 1) | dt;
|
||||
|
||||
if (curr != s_prev_state) {
|
||||
uint8_t idx = ((s_prev_state & 0x03) << 2) | (curr & 0x03);
|
||||
int8_t step = s_quad_table[idx];
|
||||
s_prev_state = curr;
|
||||
|
||||
if (step != 0) {
|
||||
s_accum += step;
|
||||
if (s_accum >= s_steps_per_detent) {
|
||||
s_delta += 1;
|
||||
s_accum = 0;
|
||||
} else if (s_accum <= -s_steps_per_detent) {
|
||||
s_delta -= 1;
|
||||
s_accum = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -61,20 +116,46 @@ static esp_err_t config_pin(gpio_num_t pin, gpio_int_type_t intr_type)
|
||||
|
||||
esp_err_t encoder_init(void)
|
||||
{
|
||||
gpio_install_isr_service(ESP_INTR_FLAG_LEVEL1);
|
||||
esp_err_t err = gpio_install_isr_service(ESP_INTR_FLAG_LEVEL1);
|
||||
if (err != ESP_OK && err != ESP_ERR_INVALID_STATE) {
|
||||
ESP_ERROR_CHECK(err);
|
||||
}
|
||||
|
||||
ESP_ERROR_CHECK(config_pin(HW_ENC_CLK_GPIO, GPIO_INTR_POSEDGE));
|
||||
ESP_ERROR_CHECK(config_pin(HW_ENC_DT_GPIO, GPIO_INTR_DISABLE));
|
||||
ESP_ERROR_CHECK(config_pin(HW_ENC_CLK_GPIO, GPIO_INTR_ANYEDGE));
|
||||
ESP_ERROR_CHECK(config_pin(HW_ENC_DT_GPIO, GPIO_INTR_ANYEDGE));
|
||||
ESP_ERROR_CHECK(config_pin(HW_ENC_SW_GPIO, GPIO_INTR_NEGEDGE));
|
||||
|
||||
/* Initialize initial state */
|
||||
uint8_t clk = (uint8_t)gpio_get_level(HW_ENC_CLK_GPIO);
|
||||
uint8_t dt = (uint8_t)gpio_get_level(HW_ENC_DT_GPIO);
|
||||
s_prev_state = (clk << 1) | dt;
|
||||
s_accum = 0;
|
||||
s_delta = 0;
|
||||
|
||||
ESP_ERROR_CHECK(gpio_isr_handler_add(HW_ENC_CLK_GPIO, encoder_isr, (void *)(uintptr_t)HW_ENC_CLK_GPIO));
|
||||
ESP_ERROR_CHECK(gpio_isr_handler_add(HW_ENC_DT_GPIO, encoder_isr, (void *)(uintptr_t)HW_ENC_DT_GPIO));
|
||||
ESP_ERROR_CHECK(gpio_isr_handler_add(HW_ENC_SW_GPIO, encoder_isr, (void *)(uintptr_t)HW_ENC_SW_GPIO));
|
||||
|
||||
ESP_LOGI(TAG, "EC11 encoder ready (CLK=%d DT=%d SW=%d)",
|
||||
HW_ENC_CLK_GPIO, HW_ENC_DT_GPIO, HW_ENC_SW_GPIO);
|
||||
ESP_LOGI(TAG, "EC11 quadrature encoder ready (CLK=%d DT=%d SW=%d steps/tick=%d)",
|
||||
HW_ENC_CLK_GPIO, HW_ENC_DT_GPIO, HW_ENC_SW_GPIO, s_steps_per_detent);
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
int8_t encoder_get_steps_per_detent(void)
|
||||
{
|
||||
return s_steps_per_detent;
|
||||
}
|
||||
|
||||
void encoder_set_steps_per_detent(int8_t steps)
|
||||
{
|
||||
if (steps != 2 && steps != 4) {
|
||||
steps = 4;
|
||||
}
|
||||
s_steps_per_detent = steps;
|
||||
s_accum = 0;
|
||||
ESP_LOGI(TAG, "Encoder resolution set to %d steps/detent", steps);
|
||||
}
|
||||
|
||||
bool encoder_has_delta(void)
|
||||
{
|
||||
return s_delta != 0;
|
||||
|
||||
@@ -46,6 +46,16 @@ bool encoder_button_pressed(void);
|
||||
*/
|
||||
void encoder_clear_button(void);
|
||||
|
||||
/**
|
||||
* @brief Get current quadrature steps per mechanical detent tick (2 or 4).
|
||||
*/
|
||||
int8_t encoder_get_steps_per_detent(void);
|
||||
|
||||
/**
|
||||
* @brief Set quadrature steps per mechanical detent tick (2 or 4).
|
||||
*/
|
||||
void encoder_set_steps_per_detent(int8_t steps);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
@@ -2,6 +2,7 @@
|
||||
## NOTE: espressif/esp_lcd is a built-in IDF component - do NOT declare it here.
|
||||
dependencies:
|
||||
idf: ">=5.0"
|
||||
abobija/rc522: "*"
|
||||
joltwallet/littlefs: "*"
|
||||
esp_jpeg: ">=1.0.2"
|
||||
esp_jpeg: ">=1.0.2"
|
||||
lvgl/lvgl: "^8.3.11"
|
||||
espressif/esp_lvgl_port: "^2.0.0"
|
||||
@@ -20,9 +20,9 @@ void app_main(void)
|
||||
ESP_ERROR_CHECK(spi_bus_manager_init());
|
||||
ESP_ERROR_CHECK(storage_manager_init());
|
||||
|
||||
/* RFID comes BEFORE the display: the reader lives on its own SPI3 bus now
|
||||
/* RFID comes BEFORE the display: the reader lives on its own SPI3 bus
|
||||
* (display owns SPI2), but initializing it before the panel's init traffic
|
||||
* still gives the self-test the quietest possible bus timing. */
|
||||
* gives the startup the quietest possible bus timing. */
|
||||
esp_err_t ret = rfid_manager_init();
|
||||
if (ret != ESP_OK || rfid_manager_start() != ESP_OK) {
|
||||
ESP_LOGW(TAG, "RFID unavailable (%s) - continuing without card scanning", esp_err_to_name(ret));
|
||||
|
||||
@@ -23,11 +23,11 @@ static const char *TAG = "rfid";
|
||||
#define RC522_TASK_NAME "rc522_poll"
|
||||
#define RC522_POLL_INTERVAL_MS (100)
|
||||
#define RC522_TASK_STACK_SIZE (6 * 1024)
|
||||
#define RC522_TASK_PRIORITY (4)
|
||||
#define RC522_TASK_PRIORITY (1)
|
||||
|
||||
/* Conservative clock for MFRC522 clones. If the FIFO self-test still
|
||||
* complains, lower further or check power/wiring. */
|
||||
#define RC522_SPI_CLK_HZ (1000000)
|
||||
#define RC522_SPI_CLK_HZ (5000000)
|
||||
#define RC522_START_ATTEMPTS (3)
|
||||
|
||||
/* Some clone MFRC522 modules (esp. those wired with RST left floating and
|
||||
@@ -76,40 +76,12 @@ static void on_picc_state_changed(void *arg, esp_event_base_t base, int32_t even
|
||||
* hardware-CS device. Any plausible value (0x88/0x91/0x92) means the chip is
|
||||
* alive and MISO works; 0x00/0xFF means MISO/power/wiring is dead. This is a
|
||||
* pure diagnostic and does not depend on the rc522 driver's reset path. */
|
||||
static void rfid_probe_version(void)
|
||||
{
|
||||
spi_device_handle_t probe = NULL;
|
||||
spi_device_interface_config_t devcfg = {
|
||||
.clock_speed_hz = RC522_SPI_CLK_HZ,
|
||||
.mode = 0,
|
||||
.spics_io_num = HW_RC522_CS_GPIO,
|
||||
.queue_size = 1,
|
||||
.command_bits = 1, /* read/write select bit */
|
||||
.address_bits = 6, /* register address */
|
||||
.dummy_bits = 1,
|
||||
.flags = SPI_DEVICE_HALFDUPLEX, /* RX-only reads like the rc522 driver */
|
||||
};
|
||||
if (spi_bus_add_device(HW_RFID_SPI_HOST, &devcfg, &probe) != ESP_OK || probe == NULL) {
|
||||
ESP_LOGW(TAG, "probe: could not add temp SPI device on SPI3");
|
||||
return;
|
||||
}
|
||||
|
||||
uint8_t version = 0;
|
||||
spi_transaction_t t = {
|
||||
.cmd = 1, /* read */
|
||||
.addr = 0x37, /* VersionReg */
|
||||
.rxlength = 8,
|
||||
.rx_buffer = &version,
|
||||
};
|
||||
esp_err_t ret = spi_device_polling_transmit(probe, &t);
|
||||
ESP_LOGI(TAG, "MFRC522 VersionReg=0x%02X (%s). 0x88/0x91/0x92=alive; 0x00/0xFF=MISO/power/wiring",
|
||||
version, esp_err_to_name(ret));
|
||||
|
||||
spi_bus_remove_device(probe);
|
||||
}
|
||||
|
||||
esp_err_t rfid_manager_init(void)
|
||||
{
|
||||
esp_log_level_set("rc522", ESP_LOG_INFO);
|
||||
|
||||
/* Dedicated SPI3_HOST (HSPI) bus for the reader - independent controller
|
||||
* so the display (SPI2_HOST) and the RFID never share bus timing. We
|
||||
* initialize SPI3 here (with a real MISO) and hand the already-initialized
|
||||
@@ -124,14 +96,12 @@ esp_err_t rfid_manager_init(void)
|
||||
ESP_LOGI(TAG, "initializing MFRC522 on SPI3_HOST (SCK=%d MOSI=%d MISO=%d CS=%d)",
|
||||
HW_RFID_SCK_GPIO, HW_RFID_MOSI_GPIO, HW_RFID_MISO_GPIO, HW_RC522_CS_GPIO);
|
||||
|
||||
esp_err_t ret = spi_bus_initialize(HW_RFID_SPI_HOST, &rfid_bus_cfg, SPI_DMA_CH_AUTO);
|
||||
esp_err_t ret = spi_bus_initialize(HW_RFID_SPI_HOST, &rfid_bus_cfg, SPI_DMA_DISABLED);
|
||||
if (ret != ESP_OK) {
|
||||
ESP_LOGE(TAG, "spi_bus_initialize(SPI3) failed: %s", esp_err_to_name(ret));
|
||||
return ret;
|
||||
}
|
||||
|
||||
rfid_probe_version();
|
||||
|
||||
rc522_spi_config_t driver_config = {
|
||||
.host_id = HW_RFID_SPI_HOST,
|
||||
.bus_config = NULL, /* SPI3 already initialized above */
|
||||
@@ -139,11 +109,8 @@ esp_err_t rfid_manager_init(void)
|
||||
.spics_io_num = HW_RC522_CS_GPIO,
|
||||
.clock_speed_hz = RC522_SPI_CLK_HZ,
|
||||
},
|
||||
.dma_chan = SPI_DMA_CH_AUTO,
|
||||
/* RST left unconnected (-1): the module comes up on its own power-on
|
||||
* reset and uses the library soft-reset. Wire RST=GPIO22 and set this
|
||||
* to HW_RC522_RST_GPIO if your module needs the hard-reset line. */
|
||||
.rst_io_num = -1,
|
||||
.dma_chan = SPI_DMA_DISABLED,
|
||||
.rst_io_num = HW_RC522_RST_GPIO,
|
||||
};
|
||||
|
||||
ret = rc522_spi_create(&driver_config, &s_driver);
|
||||
|
||||
@@ -29,7 +29,7 @@ static const char *TAG = "scryfall";
|
||||
#define MAX_ATTEMPTS (2)
|
||||
|
||||
#define JSON_BUF_MAX (128 * 1024)
|
||||
#define NAME_MAX (128)
|
||||
#define CARD_NAME_MAX (128)
|
||||
#define UID_MAX (24)
|
||||
#define FIELD_STR_MAX (256)
|
||||
#define ORACLE_MAX (768)
|
||||
@@ -38,7 +38,7 @@ static const char *TAG = "scryfall";
|
||||
#define EVT_CARD_CACHED (1U << 0)
|
||||
|
||||
typedef struct {
|
||||
char name[NAME_MAX];
|
||||
char name[CARD_NAME_MAX];
|
||||
char uid[UID_MAX];
|
||||
} fetch_request_t;
|
||||
|
||||
@@ -163,13 +163,20 @@ static esp_err_t http_request(const char *url, http_sink_t *sink)
|
||||
}
|
||||
|
||||
esp_http_client_set_method(client, HTTP_METHOD_GET);
|
||||
esp_http_client_set_header(client, "User-Agent", "MTGCompanion/1.0");
|
||||
esp_http_client_set_header(client, "Accept", "*/*");
|
||||
|
||||
esp_err_t ret = esp_http_client_perform(client);
|
||||
int status = esp_http_client_get_status_code(client);
|
||||
esp_http_client_cleanup(client);
|
||||
|
||||
ESP_LOGD(TAG, "GET %s -> %s", url, esp_err_to_name(ret));
|
||||
if (ret == ESP_OK) {
|
||||
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;
|
||||
}
|
||||
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;
|
||||
}
|
||||
@@ -227,14 +234,48 @@ esp_err_t scryfall_ensure_cached(const char *card_name, const char *uid)
|
||||
{
|
||||
char json_path[64];
|
||||
snprintf(json_path, sizeof(json_path), STORAGE_CARDS_DIR "/%s.json", uid);
|
||||
char jpg_path[64];
|
||||
snprintf(jpg_path, sizeof(jpg_path), STORAGE_CARDS_DIR "/%s.jpg", uid);
|
||||
|
||||
if (storage_file_exists(json_path)) {
|
||||
ESP_LOGI(TAG, "cache hit %s", json_path);
|
||||
return ESP_OK;
|
||||
char *chk = NULL;
|
||||
size_t chk_len = 0;
|
||||
bool valid = false;
|
||||
if (storage_read_text(json_path, &chk, &chk_len) == ESP_OK && chk != NULL) {
|
||||
cJSON *cj = cJSON_Parse(chk);
|
||||
if (cj) {
|
||||
cJSON *n = cJSON_GetObjectItem(cj, "name");
|
||||
cJSON *t = cJSON_GetObjectItem(cj, "type");
|
||||
cJSON *o = cJSON_GetObjectItem(cj, "oracle");
|
||||
/* Verify name matches the requested card name */
|
||||
if (n && n->valuestring && strcasecmp(n->valuestring, card_name) == 0) {
|
||||
if ((t && t->valuestring && strlen(t->valuestring) > 0) ||
|
||||
(o && o->valuestring && strlen(o->valuestring) > 0)) {
|
||||
valid = true;
|
||||
}
|
||||
}
|
||||
cJSON_Delete(cj);
|
||||
}
|
||||
free(chk);
|
||||
}
|
||||
if (valid) {
|
||||
if (storage_file_exists(jpg_path)) {
|
||||
ESP_LOGI(TAG, "cache hit %s and %s", json_path, jpg_path);
|
||||
s_result_bits |= EVT_CARD_CACHED;
|
||||
snprintf(s_last_uid, sizeof(s_last_uid), "%s", uid);
|
||||
return ESP_OK;
|
||||
}
|
||||
ESP_LOGI(TAG, "json valid but %s missing, fetching art...", jpg_path);
|
||||
} else {
|
||||
ESP_LOGW(TAG, "cache entry %s is invalid or card mismatch (expected '%s'), re-fetching...", json_path, card_name);
|
||||
remove(json_path);
|
||||
remove(jpg_path);
|
||||
}
|
||||
}
|
||||
|
||||
/* 1. GET card JSON */
|
||||
char url[512];
|
||||
char enc[NAME_MAX * 3];
|
||||
char enc[CARD_NAME_MAX * 3];
|
||||
url_encode(card_name, enc, sizeof(enc));
|
||||
snprintf(url, sizeof(url), SCRYFALL_API SCRYFALL_CARD_ARTS_URL "%s", enc);
|
||||
|
||||
@@ -253,6 +294,14 @@ esp_err_t scryfall_ensure_cached(const char *card_name, const char *uid)
|
||||
return ESP_FAIL;
|
||||
}
|
||||
|
||||
cJSON *obj = cJSON_GetObjectItem(root, "object");
|
||||
if (obj && cJSON_IsString(obj) && strcmp(obj->valuestring, "error") == 0) {
|
||||
ESP_LOGE(TAG, "Scryfall API error: %s",
|
||||
get_string(cJSON_GetObjectItem(root, "details"), "unknown error"));
|
||||
cJSON_Delete(root);
|
||||
return ESP_FAIL;
|
||||
}
|
||||
|
||||
/* 2. Extract fields */
|
||||
const char *name = get_string(cJSON_GetObjectItem(root, "name"), card_name);
|
||||
const char *mana = NULL;
|
||||
@@ -265,40 +314,79 @@ esp_err_t scryfall_ensure_cached(const char *card_name, const char *uid)
|
||||
const char *type_line = get_string(cJSON_GetObjectItem(root, "type_line"), "");
|
||||
const char *power = get_string(cJSON_GetObjectItem(root, "power"), "");
|
||||
const char *toughness = get_string(cJSON_GetObjectItem(root, "toughness"), "");
|
||||
const char *loyalty = get_string(cJSON_GetObjectItem(root, "loyalty"), "");
|
||||
const char *oracle = get_string(cJSON_GetObjectItem(root, "oracle_text"), "");
|
||||
const char *image_url = NULL;
|
||||
cJSON *uris = cJSON_GetObjectItem(root, "image_uris");
|
||||
|
||||
cJSON *faces = cJSON_GetObjectItem(root, "card_faces");
|
||||
if (faces && cJSON_IsArray(faces) && cJSON_GetArraySize(faces) > 0) {
|
||||
cJSON *face0 = cJSON_GetArrayItem(faces, 0);
|
||||
if (face0) {
|
||||
if (uris == NULL) {
|
||||
uris = cJSON_GetObjectItem(face0, "image_uris");
|
||||
}
|
||||
if (strlen(oracle) == 0) {
|
||||
oracle = get_string(cJSON_GetObjectItem(face0, "oracle_text"), "");
|
||||
}
|
||||
if (strlen(mana) == 0) {
|
||||
mana = get_string(cJSON_GetObjectItem(face0, "mana_cost"), "");
|
||||
}
|
||||
if (strlen(type_line) == 0) {
|
||||
type_line = get_string(cJSON_GetObjectItem(face0, "type_line"), "");
|
||||
}
|
||||
}
|
||||
}
|
||||
if (uris) {
|
||||
image_url = get_string(cJSON_GetObjectItem(uris, "normal"), "");
|
||||
image_url = get_string(cJSON_GetObjectItem(uris, "art_crop"), "");
|
||||
if (strlen(image_url) == 0) {
|
||||
image_url = get_string(cJSON_GetObjectItem(uris, "normal"), "");
|
||||
}
|
||||
}
|
||||
const char *price_usd = NULL;
|
||||
cJSON *prices = cJSON_GetObjectItem(root, "prices");
|
||||
if (prices) {
|
||||
price_usd = get_string(cJSON_GetObjectItem(prices, "usd"), "");
|
||||
}
|
||||
const char *rarity = get_string(cJSON_GetObjectItem(root, "rarity"), "");
|
||||
|
||||
/* 3. Persist metadata */
|
||||
char meta[1536];
|
||||
snprintf(meta, sizeof(meta),
|
||||
"{\n"
|
||||
" \"name\": \"%s\",\n"
|
||||
" \"mana_cost\": \"%s\",\n"
|
||||
" \"type\": \"%s\",\n"
|
||||
" \"power\": \"%s\",\n"
|
||||
" \"toughness\": \"%s\",\n"
|
||||
" \"oracle\": \"%s\",\n"
|
||||
" \"price_usd\": \"%s\"\n"
|
||||
"}\n",
|
||||
name, mana, type_line, power, toughness, oracle,
|
||||
price_usd ? price_usd : "");
|
||||
/* 3. Persist metadata with proper JSON escaping */
|
||||
cJSON *card_obj = cJSON_CreateObject();
|
||||
if (card_obj) {
|
||||
cJSON_AddStringToObject(card_obj, "name", name);
|
||||
cJSON_AddStringToObject(card_obj, "mana_cost", mana);
|
||||
cJSON_AddStringToObject(card_obj, "type", type_line);
|
||||
cJSON_AddStringToObject(card_obj, "rarity", rarity);
|
||||
cJSON_AddStringToObject(card_obj, "power", power);
|
||||
cJSON_AddStringToObject(card_obj, "toughness", toughness);
|
||||
cJSON_AddStringToObject(card_obj, "loyalty", loyalty);
|
||||
cJSON_AddStringToObject(card_obj, "oracle", oracle);
|
||||
cJSON_AddStringToObject(card_obj, "price_usd", price_usd ? price_usd : "");
|
||||
|
||||
ret = storage_write_text(json_path, meta);
|
||||
char *meta_str = cJSON_PrintUnformatted(card_obj);
|
||||
cJSON_Delete(card_obj);
|
||||
if (meta_str != NULL) {
|
||||
ret = storage_write_text(json_path, meta_str);
|
||||
free(meta_str);
|
||||
} else {
|
||||
ret = ESP_ERR_NO_MEM;
|
||||
}
|
||||
} else {
|
||||
ret = ESP_ERR_NO_MEM;
|
||||
}
|
||||
|
||||
/* 4. Download cover art (best effort) */
|
||||
if (image_url && strlen(image_url) > 0) {
|
||||
char jpg_path[64];
|
||||
snprintf(jpg_path, sizeof(jpg_path), STORAGE_CARDS_DIR "/%s.jpg", uid);
|
||||
esp_err_t dl = http_download_file(image_url, jpg_path);
|
||||
char proxy_url[600];
|
||||
/* wsrv.nl converts Scryfall's progressive JPEG to a lightweight 200x110 baseline JPEG */
|
||||
snprintf(proxy_url, sizeof(proxy_url), "https://wsrv.nl/?url=%s&w=200&h=110&fit=cover&output=jpg", image_url);
|
||||
esp_err_t dl = http_download_file(proxy_url, jpg_path);
|
||||
if (dl != ESP_OK) {
|
||||
ESP_LOGW(TAG, "proxied image download failed, trying direct: %s", image_url);
|
||||
dl = http_download_file(image_url, jpg_path);
|
||||
}
|
||||
if (dl != ESP_OK) {
|
||||
ESP_LOGW(TAG, "image download failed: %s", image_url);
|
||||
} else {
|
||||
@@ -341,13 +429,8 @@ static void prefetch_mappings(void)
|
||||
cJSON *item = NULL;
|
||||
cJSON_ArrayForEach(item, root) {
|
||||
uid = item->string;
|
||||
if (uid == NULL) continue;
|
||||
char path[64];
|
||||
snprintf(path, sizeof(path), STORAGE_CARDS_DIR "/%s.json", uid);
|
||||
if (!storage_file_exists(path)) {
|
||||
ESP_LOGI(TAG, "prefetching '%s' for %s", item->valuestring, uid);
|
||||
scryfall_ensure_cached(item->valuestring, uid);
|
||||
}
|
||||
if (uid == NULL || item->valuestring == NULL) continue;
|
||||
scryfall_ensure_cached(item->valuestring, uid);
|
||||
}
|
||||
cJSON_Delete(root);
|
||||
}
|
||||
@@ -397,7 +480,7 @@ esp_err_t scryfall_fetch_request(const char *card_name, const char *uid)
|
||||
if (s_fetch_queue == NULL) {
|
||||
return ESP_ERR_INVALID_STATE;
|
||||
}
|
||||
if (strlen(card_name) >= NAME_MAX || strlen(uid) >= UID_MAX || strlen(card_name) == 0) {
|
||||
if (strlen(card_name) >= CARD_NAME_MAX || strlen(uid) >= UID_MAX || strlen(card_name) == 0) {
|
||||
return ESP_ERR_INVALID_ARG;
|
||||
}
|
||||
fetch_request_t req;
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
#include <stdlib.h>
|
||||
#include <sys/stat.h>
|
||||
#include <sys/unistd.h>
|
||||
#include <dirent.h>
|
||||
#include "esp_log.h"
|
||||
#include "esp_littlefs.h"
|
||||
#include "cJSON.h"
|
||||
@@ -129,6 +130,18 @@ esp_err_t storage_manager_bind_uid(const char *uid, const char *card_name)
|
||||
return ESP_ERR_NO_MEM;
|
||||
}
|
||||
|
||||
/* If previous mapping was for a different card, purge cached files so new card fetches clean */
|
||||
cJSON *prev = cJSON_GetObjectItem(root, uid);
|
||||
if (prev == NULL || prev->valuestring == NULL || strcasecmp(prev->valuestring, card_name) != 0) {
|
||||
char old_json[64];
|
||||
char old_jpg[64];
|
||||
snprintf(old_json, sizeof(old_json), STORAGE_CARDS_DIR "/%s.json", uid);
|
||||
snprintf(old_jpg, sizeof(old_jpg), STORAGE_CARDS_DIR "/%s.jpg", uid);
|
||||
remove(old_json);
|
||||
remove(old_jpg);
|
||||
ESP_LOGI(TAG, "purged old card cache for %s due to new binding -> '%s'", uid, card_name);
|
||||
}
|
||||
|
||||
cJSON_DeleteItemFromObject(root, uid);
|
||||
cJSON_AddItemToObject(root, uid, cJSON_CreateString(card_name));
|
||||
|
||||
@@ -139,6 +152,47 @@ esp_err_t storage_manager_bind_uid(const char *uid, const char *card_name)
|
||||
return ret;
|
||||
}
|
||||
|
||||
esp_err_t storage_manager_unbind_uid(const char *uid)
|
||||
{
|
||||
if (uid == NULL || uid[0] == '\0') {
|
||||
return ESP_ERR_INVALID_ARG;
|
||||
}
|
||||
char *existing = NULL;
|
||||
size_t existing_len = 0;
|
||||
cJSON *root = NULL;
|
||||
|
||||
if (storage_read_text(STORAGE_MAPPINGS, &existing, &existing_len) == ESP_OK && existing != NULL) {
|
||||
root = cJSON_Parse(existing);
|
||||
}
|
||||
free(existing);
|
||||
|
||||
if (root == NULL || !cJSON_IsObject(root)) {
|
||||
if (root) cJSON_Delete(root);
|
||||
return ESP_ERR_NOT_FOUND;
|
||||
}
|
||||
|
||||
if (!cJSON_HasObjectItem(root, uid)) {
|
||||
cJSON_Delete(root);
|
||||
return ESP_ERR_NOT_FOUND;
|
||||
}
|
||||
|
||||
cJSON_DeleteItemFromObject(root, uid);
|
||||
|
||||
char old_json[64];
|
||||
char old_jpg[64];
|
||||
snprintf(old_json, sizeof(old_json), STORAGE_CARDS_DIR "/%s.json", uid);
|
||||
snprintf(old_jpg, sizeof(old_jpg), STORAGE_CARDS_DIR "/%s.jpg", uid);
|
||||
remove(old_json);
|
||||
remove(old_jpg);
|
||||
ESP_LOGI(TAG, "unbound UID %s and removed cached files", uid);
|
||||
|
||||
char *out = cJSON_Print(root);
|
||||
esp_err_t ret = out ? storage_write_text(STORAGE_MAPPINGS, out) : ESP_ERR_NO_MEM;
|
||||
free(out);
|
||||
cJSON_Delete(root);
|
||||
return ret;
|
||||
}
|
||||
|
||||
esp_err_t storage_manager_lookup_name(const char *uid, char **out)
|
||||
{
|
||||
*out = NULL;
|
||||
@@ -157,6 +211,17 @@ esp_err_t storage_manager_lookup_name(const char *uid, char **out)
|
||||
}
|
||||
|
||||
cJSON *item = cJSON_GetObjectItem(root, uid);
|
||||
if (item == NULL) {
|
||||
char prefix[48];
|
||||
snprintf(prefix, sizeof(prefix), "%s&name=", uid);
|
||||
cJSON *child = NULL;
|
||||
cJSON_ArrayForEach(child, root) {
|
||||
if (child->string && strncmp(child->string, prefix, strlen(prefix)) == 0) {
|
||||
item = child;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
esp_err_t ret = (item && cJSON_IsString(item) && item->valuestring) ? ESP_OK : ESP_ERR_NOT_FOUND;
|
||||
if (ret == ESP_OK) {
|
||||
*out = strdup(item->valuestring);
|
||||
@@ -256,4 +321,63 @@ esp_err_t storage_manager_init(void)
|
||||
|
||||
cross_reboot_persistence_test();
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
esp_err_t storage_manager_get_info(size_t *total_bytes, size_t *used_bytes)
|
||||
{
|
||||
if (total_bytes == NULL || used_bytes == NULL) {
|
||||
return ESP_ERR_INVALID_ARG;
|
||||
}
|
||||
return esp_littlefs_info(STORAGE_PARTITION_LABEL, total_bytes, used_bytes);
|
||||
}
|
||||
|
||||
esp_err_t storage_manager_count_cached(int *json_count, int *jpg_count)
|
||||
{
|
||||
if (json_count == NULL || jpg_count == NULL) {
|
||||
return ESP_ERR_INVALID_ARG;
|
||||
}
|
||||
*json_count = 0;
|
||||
*jpg_count = 0;
|
||||
|
||||
DIR *d = opendir(STORAGE_CARDS_DIR);
|
||||
if (d == NULL) {
|
||||
return ESP_OK;
|
||||
}
|
||||
struct dirent *entry;
|
||||
while ((entry = readdir(d)) != NULL) {
|
||||
if (entry->d_type == DT_REG || entry->d_type == DT_UNKNOWN) {
|
||||
size_t nlen = strlen(entry->d_name);
|
||||
if (nlen > 5 && strcmp(entry->d_name + nlen - 5, ".json") == 0) {
|
||||
(*json_count)++;
|
||||
} else if (nlen > 4 && strcmp(entry->d_name + nlen - 4, ".jpg") == 0) {
|
||||
(*jpg_count)++;
|
||||
}
|
||||
}
|
||||
}
|
||||
closedir(d);
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
esp_err_t storage_manager_clear_all_cards(bool reset_mappings)
|
||||
{
|
||||
DIR *d = opendir(STORAGE_CARDS_DIR);
|
||||
if (d != NULL) {
|
||||
struct dirent *entry;
|
||||
while ((entry = readdir(d)) != NULL) {
|
||||
if (entry->d_name[0] == '.') {
|
||||
continue;
|
||||
}
|
||||
char path[300];
|
||||
snprintf(path, sizeof(path), STORAGE_CARDS_DIR "/%s", entry->d_name);
|
||||
unlink(path);
|
||||
}
|
||||
closedir(d);
|
||||
ESP_LOGI(TAG, "Cleared all files in %s", STORAGE_CARDS_DIR);
|
||||
}
|
||||
|
||||
if (reset_mappings) {
|
||||
storage_write_text(STORAGE_MAPPINGS, "{\n}\n");
|
||||
ESP_LOGI(TAG, "Reset %s to empty", STORAGE_MAPPINGS);
|
||||
}
|
||||
return ESP_OK;
|
||||
}
|
||||
@@ -45,12 +45,32 @@ esp_err_t storage_write_text(const char *path, const char *text);
|
||||
*/
|
||||
esp_err_t storage_manager_bind_uid(const char *uid, const char *card_name);
|
||||
|
||||
/**
|
||||
* @brief Remove a UID mapping and its cached card JSON/JPG files.
|
||||
*/
|
||||
esp_err_t storage_manager_unbind_uid(const char *uid);
|
||||
|
||||
/**
|
||||
* @brief Lookup a card name for a UID in mappings.json. Caller frees *out.
|
||||
* @return ESP_OK and non-NULL *out on success; ESP_ERR_NOT_FOUND otherwise.
|
||||
*/
|
||||
esp_err_t storage_manager_lookup_name(const char *uid, char **out);
|
||||
|
||||
/**
|
||||
* @brief Get partition usage information.
|
||||
*/
|
||||
esp_err_t storage_manager_get_info(size_t *total_bytes, size_t *used_bytes);
|
||||
|
||||
/**
|
||||
* @brief Count number of cached cards (.json and .jpg) in /spiffs/cards.
|
||||
*/
|
||||
esp_err_t storage_manager_count_cached(int *json_count, int *jpg_count);
|
||||
|
||||
/**
|
||||
* @brief Wipe all cached cards and optionally reset mappings.json for a new deck.
|
||||
*/
|
||||
esp_err_t storage_manager_clear_all_cards(bool reset_mappings);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
114
main/ui_font.h
114
main/ui_font.h
@@ -1,114 +0,0 @@
|
||||
/*
|
||||
* MTG RFID Companion - 5x7 bitmap font (uppercase, digits, punctuation)
|
||||
*
|
||||
* Row-major glyph table. Each glyph is 7 bytes; one byte per pixel row,
|
||||
* low 5 bits map columns 0..4 from left to right (bit0 = left column).
|
||||
* Index = ASCII code - 32.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#define FONT5X7_W (5)
|
||||
#define FONT5X7_H (7)
|
||||
#define FONT5X7_FIRST_CHAR (32)
|
||||
#define FONT5X7_LAST_CHAR (126)
|
||||
#define FONT5X7_NUM_CHARS (FONT5X7_LAST_CHAR - FONT5X7_FIRST_CHAR + 1)
|
||||
|
||||
/* (char - 32) -> glyph rows */
|
||||
static const uint8_t font5x7[FONT5X7_NUM_CHARS][FONT5X7_H] = {
|
||||
/* 0x20 ' ' */ { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 },
|
||||
/* 0x21 '!' */ { 0x04, 0x04, 0x04, 0x04, 0x00, 0x04, 0x04 },
|
||||
/* 0x22 '"' */ { 0x0A, 0x0A, 0x00, 0x00, 0x00, 0x00, 0x00 },
|
||||
/* 0x23 '#' */ { 0x0A, 0x15, 0x1F, 0x15, 0x1F, 0x0A, 0x0A },
|
||||
/* 0x24 '$' */ { 0x0E, 0x11, 0x0F, 0x14, 0x0F, 0x11, 0x0E },
|
||||
/* 0x25 '%' */ { 0x19, 0x1A, 0x02, 0x04, 0x08, 0x0B, 0x13 },
|
||||
/* 0x26 '&' */ { 0x0E, 0x11, 0x09, 0x06, 0x13, 0x11, 0x0E },
|
||||
/* 0x27 '\'' */ { 0x06, 0x04, 0x08, 0x00, 0x00, 0x00, 0x00 },
|
||||
/* 0x28 '(' */ { 0x02, 0x04, 0x08, 0x08, 0x08, 0x04, 0x02 },
|
||||
/* 0x29 ')' */ { 0x08, 0x04, 0x02, 0x02, 0x02, 0x04, 0x08 },
|
||||
/* 0x2A '*' */ { 0x04, 0x15, 0x1F, 0x04, 0x04, 0x00, 0x00 },
|
||||
/* 0x2B '+' */ { 0x00, 0x04, 0x04, 0x1F, 0x04, 0x04, 0x00 },
|
||||
/* 0x2C ',' */ { 0x00, 0x00, 0x00, 0x00, 0x0C, 0x04, 0x08 },
|
||||
/* 0x2D '-' */ { 0x00, 0x00, 0x00, 0x1F, 0x00, 0x00, 0x00 },
|
||||
/* 0x2E '.' */ { 0x00, 0x00, 0x00, 0x00, 0x00, 0x0C, 0x0C },
|
||||
/* 0x2F '/' */ { 0x01, 0x01, 0x02, 0x04, 0x08, 0x10, 0x10 },
|
||||
/* 0x30 '0' */ { 0x0E, 0x11, 0x11, 0x11, 0x11, 0x11, 0x0E },
|
||||
/* 0x31 '1' */ { 0x04, 0x0C, 0x04, 0x04, 0x04, 0x04, 0x1F },
|
||||
/* 0x32 '2' */ { 0x0E, 0x11, 0x01, 0x02, 0x04, 0x08, 0x1F },
|
||||
/* 0x33 '3' */ { 0x0E, 0x01, 0x01, 0x0E, 0x01, 0x01, 0x0E },
|
||||
/* 0x34 '4' */ { 0x11, 0x11, 0x11, 0x1F, 0x01, 0x01, 0x01 },
|
||||
/* 0x35 '5' */ { 0x1F, 0x10, 0x10, 0x1E, 0x01, 0x01, 0x0E },
|
||||
/* 0x36 '6' */ { 0x0E, 0x11, 0x10, 0x1E, 0x11, 0x11, 0x0E },
|
||||
/* 0x37 '7' */ { 0x1F, 0x01, 0x02, 0x04, 0x04, 0x04, 0x04 },
|
||||
/* 0x38 '8' */ { 0x0E, 0x11, 0x11, 0x0E, 0x11, 0x11, 0x0E },
|
||||
/* 0x39 '9' */ { 0x0E, 0x11, 0x11, 0x0F, 0x01, 0x01, 0x0E },
|
||||
/* 0x3A ':' */ { 0x00, 0x06, 0x06, 0x00, 0x06, 0x06, 0x00 },
|
||||
/* 0x3B ';' */ { 0x00, 0x06, 0x06, 0x00, 0x06, 0x02, 0x04 },
|
||||
/* 0x3C '<' */ { 0x02, 0x06, 0x0A, 0x1E, 0x08, 0x08, 0x00 },
|
||||
/* 0x3D '=' */ { 0x00, 0x1F, 0x00, 0x1F, 0x00, 0x00, 0x00 },
|
||||
/* 0x3E '>' */ { 0x08, 0x0C, 0x0A, 0x0F, 0x02, 0x02, 0x00 },
|
||||
/* 0x3F '?' */ { 0x0E, 0x11, 0x01, 0x06, 0x04, 0x00, 0x04 },
|
||||
/* 0x40 '@' */ { 0x0E, 0x11, 0x15, 0x1F, 0x10, 0x10, 0x0F },
|
||||
/* 0x41 'A' */ { 0x0E, 0x11, 0x11, 0x1F, 0x11, 0x11, 0x11 },
|
||||
/* 0x42 'B' */ { 0x1E, 0x11, 0x11, 0x1E, 0x11, 0x11, 0x1E },
|
||||
/* 0x43 'C' */ { 0x0E, 0x11, 0x10, 0x10, 0x10, 0x11, 0x0E },
|
||||
/* 0x44 'D' */ { 0x18, 0x11, 0x11, 0x11, 0x11, 0x11, 0x18 },
|
||||
/* 0x45 'E' */ { 0x1F, 0x10, 0x10, 0x1F, 0x10, 0x10, 0x1F },
|
||||
/* 0x46 'F' */ { 0x1F, 0x10, 0x10, 0x1E, 0x10, 0x10, 0x10 },
|
||||
/* 0x47 'G' */ { 0x0E, 0x11, 0x10, 0x13, 0x11, 0x11, 0x0F },
|
||||
/* 0x48 'H' */ { 0x11, 0x11, 0x11, 0x1F, 0x11, 0x11, 0x11 },
|
||||
/* 0x49 'I' */ { 0x1F, 0x04, 0x04, 0x04, 0x04, 0x04, 0x1F },
|
||||
/* 0x4A 'J' */ { 0x01, 0x01, 0x01, 0x01, 0x11, 0x11, 0x0E },
|
||||
/* 0x4B 'K' */ { 0x11, 0x12, 0x14, 0x18, 0x14, 0x12, 0x11 },
|
||||
/* 0x4C 'L' */ { 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x1F },
|
||||
/* 0x4D 'M' */ { 0x11, 0x19, 0x15, 0x13, 0x11, 0x11, 0x11 },
|
||||
/* 0x4E 'N' */ { 0x11, 0x19, 0x15, 0x13, 0x11, 0x11, 0x11 },
|
||||
/* 0x4F 'O' */ { 0x0E, 0x11, 0x11, 0x11, 0x11, 0x11, 0x0E },
|
||||
/* 0x50 'P' */ { 0x1E, 0x11, 0x11, 0x1E, 0x10, 0x10, 0x10 },
|
||||
/* 0x51 'Q' */ { 0x0E, 0x11, 0x11, 0x11, 0x15, 0x12, 0x0D },
|
||||
/* 0x52 'R' */ { 0x1E, 0x11, 0x11, 0x1E, 0x14, 0x12, 0x11 },
|
||||
/* 0x53 'S' */ { 0x0E, 0x11, 0x10, 0x0E, 0x01, 0x11, 0x0E },
|
||||
/* 0x54 'T' */ { 0x1F, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04 },
|
||||
/* 0x55 'U' */ { 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x0E },
|
||||
/* 0x56 'V' */ { 0x11, 0x11, 0x11, 0x11, 0x11, 0x0A, 0x04 },
|
||||
/* 0x57 'W' */ { 0x11, 0x11, 0x11, 0x13, 0x15, 0x19, 0x11 },
|
||||
/* 0x58 'X' */ { 0x11, 0x11, 0x0A, 0x04, 0x0A, 0x11, 0x11 },
|
||||
/* 0x59 'Y' */ { 0x11, 0x11, 0x0A, 0x04, 0x04, 0x04, 0x04 },
|
||||
/* 0x5A 'Z' */ { 0x1F, 0x01, 0x02, 0x04, 0x08, 0x10, 0x1F },
|
||||
/* 0x5B '[' */ { 0x08, 0x0C, 0x08, 0x08, 0x08, 0x08, 0x0E },
|
||||
/* 0x5C '\\' */ { 0x10, 0x10, 0x08, 0x04, 0x02, 0x01, 0x01 },
|
||||
/* 0x5D ']' */ { 0x0E, 0x04, 0x04, 0x04, 0x04, 0x0C, 0x04 },
|
||||
/* 0x5E '^' */ { 0x04, 0x0A, 0x15, 0x04, 0x04, 0x00, 0x00 },
|
||||
/* 0x5F '_' */ { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x1F },
|
||||
/* 0x60 '`' */ { 0x06, 0x08, 0x04, 0x00, 0x00, 0x00, 0x00 },
|
||||
/* 0x61 'a' */ { 0x0E, 0x11, 0x11, 0x1F, 0x11, 0x11, 0x11 },
|
||||
/* 0x62 'b' */ { 0x1E, 0x11, 0x11, 0x1E, 0x11, 0x11, 0x1E },
|
||||
/* 0x63 'c' */ { 0x0E, 0x11, 0x10, 0x10, 0x10, 0x11, 0x0E },
|
||||
/* 0x64 'd' */ { 0x18, 0x11, 0x11, 0x11, 0x11, 0x11, 0x18 },
|
||||
/* 0x65 'e' */ { 0x1F, 0x10, 0x10, 0x1F, 0x10, 0x10, 0x1F },
|
||||
/* 0x66 'f' */ { 0x1F, 0x10, 0x10, 0x1E, 0x10, 0x10, 0x10 },
|
||||
/* 0x67 'g' */ { 0x0E, 0x11, 0x10, 0x13, 0x11, 0x11, 0x0F },
|
||||
/* 0x68 'h' */ { 0x11, 0x11, 0x11, 0x1F, 0x11, 0x11, 0x11 },
|
||||
/* 0x69 'i' */ { 0x1F, 0x04, 0x04, 0x04, 0x04, 0x04, 0x1F },
|
||||
/* 0x6A 'j' */ { 0x01, 0x01, 0x01, 0x01, 0x11, 0x11, 0x0E },
|
||||
/* 0x6B 'k' */ { 0x11, 0x12, 0x14, 0x18, 0x14, 0x12, 0x11 },
|
||||
/* 0x6C 'l' */ { 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x1F },
|
||||
/* 0x6D 'm' */ { 0x11, 0x19, 0x15, 0x13, 0x11, 0x11, 0x11 },
|
||||
/* 0x6E 'n' */ { 0x11, 0x19, 0x15, 0x13, 0x11, 0x11, 0x11 },
|
||||
/* 0x6F 'o' */ { 0x0E, 0x11, 0x11, 0x11, 0x11, 0x11, 0x0E },
|
||||
/* 0x70 'p' */ { 0x1E, 0x11, 0x11, 0x1E, 0x10, 0x10, 0x10 },
|
||||
/* 0x71 'q' */ { 0x0E, 0x11, 0x11, 0x11, 0x15, 0x12, 0x0D },
|
||||
/* 0x72 'r' */ { 0x1E, 0x11, 0x11, 0x1E, 0x14, 0x12, 0x11 },
|
||||
/* 0x73 's' */ { 0x0E, 0x11, 0x10, 0x0E, 0x01, 0x11, 0x0E },
|
||||
/* 0x74 't' */ { 0x1F, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04 },
|
||||
/* 0x75 'u' */ { 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x0E },
|
||||
/* 0x76 'v' */ { 0x11, 0x11, 0x11, 0x11, 0x11, 0x0A, 0x04 },
|
||||
/* 0x77 'w' */ { 0x11, 0x11, 0x11, 0x13, 0x15, 0x19, 0x11 },
|
||||
/* 0x78 'x' */ { 0x11, 0x11, 0x0A, 0x04, 0x0A, 0x11, 0x11 },
|
||||
/* 0x79 'y' */ { 0x11, 0x11, 0x0A, 0x04, 0x04, 0x04, 0x04 },
|
||||
/* 0x7A 'z' */ { 0x1F, 0x01, 0x02, 0x04, 0x08, 0x10, 0x1F },
|
||||
/* 0x7B '{' */ { 0x06, 0x04, 0x08, 0x0C, 0x08, 0x04, 0x18 },
|
||||
/* 0x7C '|' */ { 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04 },
|
||||
/* 0x7D '}' */ { 0x0C, 0x04, 0x02, 0x06, 0x02, 0x04, 0x06 },
|
||||
/* 0x7E '~' */ { 0x00, 0x1B, 0x16, 0x00, 0x00, 0x00, 0x00 },
|
||||
};
|
||||
1707
main/ui_task.c
1707
main/ui_task.c
File diff suppressed because it is too large
Load Diff
@@ -2,6 +2,7 @@
|
||||
* MTG RFID Companion - Registration web server (SoftAP)
|
||||
*/
|
||||
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include "esp_log.h"
|
||||
@@ -25,28 +26,52 @@ static const char SPA_PAGE[] =
|
||||
"<!DOCTYPE html><html><head><meta charset=\"utf-8\">"
|
||||
"<meta name=\"viewport\" content=\"width=device-width,initial-scale=1\">"
|
||||
"<title>MTG RFID Companion</title><style>"
|
||||
"body{font-family:system-ui;background:#1a1a2e;color:#eee;max-width:560px;margin:2em auto;padding:1em}"
|
||||
"h1{color:#ffd75a}.uid{font-size:1.6em;letter-spacing:2px;color:#7fd77f;word-break:break-all}"
|
||||
"input,.btn{font-size:1em;padding:.5em;border-radius:6px;border:1px solid #555;background:#222;color:#eee}"
|
||||
"button{margin:.4em}.sug{list-style:none;padding:0}.sug li{cursor:pointer;padding:.5em;border-bottom:1px solid #444}"
|
||||
"body{font-family:system-ui,-apple-system,sans-serif;background:#141424;color:#eee;max-width:600px;margin:1.5em auto;padding:1em}"
|
||||
"h1{color:#ffd75a;margin-bottom:0.2em}h2{color:#ffd75a;font-size:1.15em;margin-top:1.2em}"
|
||||
".uid{font-size:1.5em;font-family:monospace;letter-spacing:2px;color:#7fd77f;word-break:break-all;background:#1e1e32;padding:.4em .6em;border-radius:6px;border:1px solid #333}"
|
||||
"input{font-size:1em;padding:.5em .8em;border-radius:6px;border:1px solid #555;background:#222;color:#eee;width:calc(100% - 1.6em);box-sizing:border-box}"
|
||||
"button,.btn{font-size:.95em;padding:.5em 1em;border-radius:6px;border:1px solid #555;background:#2a2a44;color:#eee;cursor:pointer;margin:.3em 0}"
|
||||
"button:hover,.btn:hover{background:#3a3a5c}"
|
||||
".btn-primary{background:#ffd75a;color:#111;border:none;font-weight:bold}"
|
||||
".btn-primary:hover{background:#ffe380}"
|
||||
".btn-del{background:#4a1e1e;color:#ff9999;border:1px solid #772222;font-size:.85em;padding:.4em .8em;cursor:pointer;border-radius:6px}"
|
||||
".btn-del:hover{background:#772222;color:#fff}"
|
||||
".sug{list-style:none;padding:0;margin:.4em 0;background:#1f1f33;border-radius:6px;border:1px solid #333}"
|
||||
".sug li{cursor:pointer;padding:.5em .8em;border-bottom:1px solid #333}"
|
||||
".sug li:hover{background:#2a2a48;color:#ffd75a}"
|
||||
".ok{color:#7fd77f}.bad{color:#ff6b6b}"
|
||||
".card-item{display:flex;align-items:center;gap:12px;background:#1e1e32;border:1px solid #33334c;border-radius:8px;padding:8px 12px;margin-bottom:8px}"
|
||||
".card-thumb{width:52px;height:52px;object-fit:cover;border-radius:6px;border:1px solid #444;background:#111;flex-shrink:0}"
|
||||
".card-info{flex:1;min-width:0}"
|
||||
".card-title{font-weight:bold;color:#ffd75a;font-size:1.02em;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}"
|
||||
".card-uid{font-family:monospace;color:#7fd77f;font-size:.85em;margin-top:2px}"
|
||||
".card-none{color:#777;font-style:italic;padding:.8em 0}"
|
||||
".header-row{display:flex;align-items:center;justify-content:space-between}"
|
||||
"</style></head><body>"
|
||||
"<h1> MTG RFID Companion</h1>"
|
||||
"<p>Pair a scanned card with its Scryfall name.</p>"
|
||||
"<h2>Last scanned UID</h2><p class=\"uid\" id=\"uid\">waiting...</p>"
|
||||
"<h2>Card name</h2>"
|
||||
"<input id=\"name\" placeholder=\"Type a card name\" autocomplete=\"off\">"
|
||||
"<button onclick=\"bindCard()\">Pair UID with card</button>"
|
||||
"<h1>MTG RFID Companion</h1>"
|
||||
"<p style=\"color:#aaa;margin-top:0\">Pair card sleeves and manage your scanned deck.</p>"
|
||||
"<div style=\"background:#1a1a2e;padding:1em;border-radius:8px;border:1px solid #2d2d48;margin-bottom:1.5em\">"
|
||||
"<h2 style=\"margin-top:0\">Last Scanned Tag</h2>"
|
||||
"<p class=\"uid\" id=\"uid\">waiting...</p>"
|
||||
"<h2>Pair With Card Name</h2>"
|
||||
"<input id=\"name\" placeholder=\"Type card name (e.g. Black Lotus)\" autocomplete=\"off\">"
|
||||
"<div style=\"margin-top:.6em\"><button class=\"btn-primary\" onclick=\"bindCard()\">Pair Card</button></div>"
|
||||
"<ul class=\"sug\" id=\"res\"></ul>"
|
||||
"<p id=\"status\"></p>"
|
||||
"</div>"
|
||||
"<div class=\"header-row\">"
|
||||
"<h2>Paired Cards (<span id=\"count\">0</span>)</h2>"
|
||||
"<button class=\"btn\" style=\"font-size:.8em\" onclick=\"loadCards()\">Refresh</button>"
|
||||
"</div>"
|
||||
"<div id=\"cards-box\"><p class=\"card-none\">Loading paired cards...</p></div>"
|
||||
"<script>"
|
||||
"var lastUid='';var suggestions=[];"
|
||||
"function poll(){fetch('/api/last_uid').then(function(r){return r.json();}).then(function(j){"
|
||||
" if(j.uid&&j.uid!==lastUid){lastUid=j.uid;document.getElementById('uid').textContent=j.uid;"
|
||||
" suggestions=[];document.getElementById('res').innerHTML='';show('','');}})"
|
||||
" .catch(function(){});setTimeout(poll,1000);}"
|
||||
"function esc(s){return s.replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>');}"
|
||||
"function show(cls,msg){document.getElementById('status').className=cls;document.getElementById('status').textContent=msg;}"
|
||||
"function poll(){fetch('/api/last_uid').then(function(r){return r.json();}).then(function(j){"
|
||||
" if(j.uid&&j.uid!==lastUid){lastUid=j.uid;document.getElementById('uid').textContent=j.uid;"
|
||||
" suggestions=[];document.getElementById('res').innerHTML='';show('','');}"
|
||||
"}).catch(function(){});setTimeout(poll,1000);}"
|
||||
"function autocomplete(){var q=document.getElementById('name').value;"
|
||||
" if(q.length<3){suggestions=[];document.getElementById('res').innerHTML='';return;}"
|
||||
" fetch('https://api.scryfall.com/cards/autocomplete?q='+encodeURIComponent(q))"
|
||||
@@ -58,13 +83,38 @@ static const char SPA_PAGE[] =
|
||||
"document.getElementById('name').oninput=autocomplete;"
|
||||
"function bindCard(){var n=document.getElementById('name').value.trim();"
|
||||
" if(!lastUid||!n){show('bad','Scan a card and enter a name first');return;}"
|
||||
" show('','Pairing...');"
|
||||
" fetch('/api/bind',{method:'POST',headers:{'Content-Type':'application/x-www-form-urlencoded'},"
|
||||
" body:'uid='+encodeURIComponent(lastUid)+'&name='+encodeURIComponent(n)})"
|
||||
" .then(function(r){return r.json();}).then(function(j){"
|
||||
" if(j.ok){show('ok','Paired '+j.uid+' -> '+j.name);document.getElementById('name').value='';"
|
||||
" document.getElementById('res').innerHTML='';}"
|
||||
" document.getElementById('res').innerHTML='';loadCards();}"
|
||||
" else{show('bad','Error: '+j.error);}}).catch(function(e){show('bad','Network: '+e);});}"
|
||||
"poll();"
|
||||
"function unbindCard(u){if(!confirm('Unbind card '+u+'?'))return;"
|
||||
" fetch('/api/unbind',{method:'POST',headers:{'Content-Type':'application/x-www-form-urlencoded'},"
|
||||
" body:'uid='+encodeURIComponent(u)})"
|
||||
" .then(function(r){return r.json();}).then(function(j){"
|
||||
" if(j.ok){loadCards();show('ok','Unbound '+u);}"
|
||||
" else{show('bad','Error unbinding '+u);}}).catch(function(e){show('bad','Network: '+e);});}"
|
||||
"function loadCards(){"
|
||||
" fetch('/api/cards').then(function(r){return r.json();}).then(function(data){"
|
||||
" var box=document.getElementById('cards-box');var uids=Object.keys(data);"
|
||||
" document.getElementById('count').textContent=uids.length;"
|
||||
" if(uids.length===0){box.innerHTML='<p class=\"card-none\">No cards paired yet. Scan a card above to begin!</p>';return;}"
|
||||
" uids.sort(function(a,b){return data[a].localeCompare(data[b]);});"
|
||||
" var html='';"
|
||||
" uids.forEach(function(u){"
|
||||
" var nm=esc(data[u]);"
|
||||
" html+='<div class=\"card-item\">';"
|
||||
" html+='<img class=\"card-thumb\" src=\"/api/art?uid='+u+'\" alt=\"art\" onerror=\"this.style.display=\\'none\\'\">';"
|
||||
" html+='<div class=\"card-info\"><div class=\"card-title\">'+nm+'</div><div class=\"card-uid\">'+u+'</div></div>';"
|
||||
" html+='<button class=\"btn-del\" onclick=\"unbindCard(\\''+u+'\\')\">Unbind</button>';"
|
||||
" html+='</div>';"
|
||||
" });"
|
||||
" box.innerHTML=html;"
|
||||
" }).catch(function(e){document.getElementById('cards-box').innerHTML='<p class=\"bad\">Failed to load cards: '+e+'</p>';});"
|
||||
"}"
|
||||
"poll();loadCards();"
|
||||
"</script></body></html>";
|
||||
|
||||
static httpd_handle_t s_server = NULL;
|
||||
@@ -72,6 +122,9 @@ static httpd_handle_t s_server = NULL;
|
||||
static esp_err_t handle_root(httpd_req_t *req);
|
||||
static esp_err_t handle_last_uid(httpd_req_t *req);
|
||||
static esp_err_t handle_bind(httpd_req_t *req);
|
||||
static esp_err_t handle_cards(httpd_req_t *req);
|
||||
static esp_err_t handle_art(httpd_req_t *req);
|
||||
static esp_err_t handle_unbind(httpd_req_t *req);
|
||||
|
||||
static const httpd_uri_t URI_ROOT = {
|
||||
.uri = "/", .method = HTTP_GET, .handler = handle_root, .user_ctx = NULL
|
||||
@@ -82,6 +135,15 @@ static const httpd_uri_t URI_LAST_UID = {
|
||||
static const httpd_uri_t URI_BIND = {
|
||||
.uri = "/api/bind", .method = HTTP_POST, .handler = handle_bind, .user_ctx = NULL
|
||||
};
|
||||
static const httpd_uri_t URI_CARDS = {
|
||||
.uri = "/api/cards", .method = HTTP_GET, .handler = handle_cards, .user_ctx = NULL
|
||||
};
|
||||
static const httpd_uri_t URI_ART = {
|
||||
.uri = "/api/art", .method = HTTP_GET, .handler = handle_art, .user_ctx = NULL
|
||||
};
|
||||
static const httpd_uri_t URI_UNBIND = {
|
||||
.uri = "/api/unbind", .method = HTTP_POST, .handler = handle_unbind, .user_ctx = NULL
|
||||
};
|
||||
|
||||
static void send_json(httpd_req_t *req, int status, const char *body)
|
||||
{
|
||||
@@ -174,18 +236,29 @@ static esp_err_t handle_bind(httpd_req_t *req)
|
||||
|
||||
char *uid = NULL;
|
||||
char *name = NULL;
|
||||
const char *u = strstr(form, "uid=");
|
||||
const char *n = strstr(form, "name=");
|
||||
char *u = strstr(form, "uid=");
|
||||
char *n = strstr(form, "name=");
|
||||
if (u) {
|
||||
char *end = strchr(u, '&');
|
||||
(void)end;
|
||||
if (end) *end = '\0';
|
||||
uid = url_decode(u + 4);
|
||||
if (end) *end = '&';
|
||||
}
|
||||
if (n) {
|
||||
char *end = strchr(n, '&');
|
||||
if (end) *end = '\0';
|
||||
name = url_decode(n + 5);
|
||||
if (end) *end = '&';
|
||||
}
|
||||
free(form);
|
||||
|
||||
if (uid) {
|
||||
size_t ulen = strlen(uid);
|
||||
while (ulen > 0 && (uid[ulen - 1] == ' ' || uid[ulen - 1] == '\r' || uid[ulen - 1] == '\n')) {
|
||||
uid[--ulen] = '\0';
|
||||
}
|
||||
}
|
||||
|
||||
if (uid == NULL || name == NULL || strlen(uid) == 0 || strlen(name) == 0) {
|
||||
free(uid);
|
||||
free(name);
|
||||
@@ -212,6 +285,114 @@ static esp_err_t handle_bind(httpd_req_t *req)
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
static esp_err_t handle_cards(httpd_req_t *req)
|
||||
{
|
||||
char *text = NULL;
|
||||
size_t len = 0;
|
||||
if (storage_read_text(STORAGE_MAPPINGS, &text, &len) == ESP_OK && text != NULL) {
|
||||
httpd_resp_set_type(req, "application/json");
|
||||
httpd_resp_send(req, text, (ssize_t)len);
|
||||
free(text);
|
||||
} else {
|
||||
send_json(req, 200, "{}\n");
|
||||
}
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
static esp_err_t handle_art(httpd_req_t *req)
|
||||
{
|
||||
char query[64] = {0};
|
||||
char uid[32] = {0};
|
||||
if (httpd_req_get_url_query_str(req, query, sizeof(query)) == ESP_OK) {
|
||||
httpd_query_key_value(query, "uid", uid, sizeof(uid));
|
||||
}
|
||||
if (uid[0] == '\0') {
|
||||
httpd_resp_send_err(req, HTTPD_400_BAD_REQUEST, "Missing uid");
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
char path[64];
|
||||
snprintf(path, sizeof(path), STORAGE_CARDS_DIR "/%s.jpg", uid);
|
||||
FILE *f = fopen(path, "rb");
|
||||
if (!f) {
|
||||
httpd_resp_send_err(req, HTTPD_404_NOT_FOUND, "Art not found");
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
httpd_resp_set_type(req, "image/jpeg");
|
||||
httpd_resp_set_hdr(req, "Cache-Control", "public, max-age=86400");
|
||||
|
||||
char chunk[1024];
|
||||
size_t n = 0;
|
||||
while ((n = fread(chunk, 1, sizeof(chunk), f)) > 0) {
|
||||
if (httpd_resp_send_chunk(req, chunk, (ssize_t)n) != ESP_OK) {
|
||||
fclose(f);
|
||||
return ESP_FAIL;
|
||||
}
|
||||
}
|
||||
fclose(f);
|
||||
httpd_resp_send_chunk(req, NULL, 0);
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
static esp_err_t handle_unbind(httpd_req_t *req)
|
||||
{
|
||||
if (req->content_len <= 0 || req->content_len >= HTTPD_BODY_MAX) {
|
||||
send_json(req, 400, "{\"ok\":false,\"error\":\"bad body\"}");
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
char *form = malloc((size_t)req->content_len + 1);
|
||||
if (form == NULL) {
|
||||
send_json(req, 500, "{\"ok\":false,\"error\":\"no mem\"}");
|
||||
return ESP_OK;
|
||||
}
|
||||
int got = httpd_req_recv(req, form, (size_t)req->content_len);
|
||||
if (got != req->content_len) {
|
||||
free(form);
|
||||
send_json(req, 400, "{\"ok\":false,\"error\":\"short body\"}");
|
||||
return ESP_OK;
|
||||
}
|
||||
form[got] = '\0';
|
||||
|
||||
char *uid = NULL;
|
||||
char *u = strstr(form, "uid=");
|
||||
if (u) {
|
||||
char *end = strchr(u, '&');
|
||||
if (end) *end = '\0';
|
||||
uid = url_decode(u + 4);
|
||||
if (end) *end = '&';
|
||||
}
|
||||
free(form);
|
||||
|
||||
if (uid) {
|
||||
size_t ulen = strlen(uid);
|
||||
while (ulen > 0 && (uid[ulen - 1] == ' ' || uid[ulen - 1] == '\r' || uid[ulen - 1] == '\n')) {
|
||||
uid[--ulen] = '\0';
|
||||
}
|
||||
}
|
||||
|
||||
if (uid == NULL || strlen(uid) == 0) {
|
||||
free(uid);
|
||||
send_json(req, 400, "{\"ok\":false,\"error\":\"missing uid\"}");
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
esp_err_t ret = storage_manager_unbind_uid(uid);
|
||||
if (ret != ESP_OK) {
|
||||
free(uid);
|
||||
send_json(req, 404, "{\"ok\":false,\"error\":\"uid not found\"}");
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
char ok[128];
|
||||
snprintf(ok, sizeof(ok), "{\"ok\":true,\"uid\":\"%s\"}\n", uid);
|
||||
ESP_LOGI(TAG, "unbound UID %s via web", uid);
|
||||
send_json(req, 200, ok);
|
||||
free(uid);
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* SoftAP + server */
|
||||
/* ------------------------------------------------------------------ */
|
||||
@@ -286,7 +467,7 @@ esp_err_t web_server_start(void)
|
||||
|
||||
httpd_config_t config = HTTPD_DEFAULT_CONFIG();
|
||||
config.stack_size = 6 * 1024;
|
||||
config.max_uri_handlers = 8;
|
||||
config.max_uri_handlers = 12;
|
||||
config.lru_purge_enable = true;
|
||||
config.keep_alive_enable = true;
|
||||
config.max_open_sockets = 4;
|
||||
@@ -300,6 +481,9 @@ esp_err_t web_server_start(void)
|
||||
ESP_ERROR_CHECK(httpd_register_uri_handler(s_server, &URI_ROOT));
|
||||
ESP_ERROR_CHECK(httpd_register_uri_handler(s_server, &URI_LAST_UID));
|
||||
ESP_ERROR_CHECK(httpd_register_uri_handler(s_server, &URI_BIND));
|
||||
ESP_ERROR_CHECK(httpd_register_uri_handler(s_server, &URI_CARDS));
|
||||
ESP_ERROR_CHECK(httpd_register_uri_handler(s_server, &URI_ART));
|
||||
ESP_ERROR_CHECK(httpd_register_uri_handler(s_server, &URI_UNBIND));
|
||||
|
||||
ESP_LOGI(TAG, "SoftAP '%s' at 192.168.4.1, web server ready", AP_SSID);
|
||||
return ESP_OK;
|
||||
|
||||
@@ -21,6 +21,7 @@ static EventGroupHandle_t s_wifi_group = NULL;
|
||||
#define WIFI_CONNECTED_BIT BIT0
|
||||
|
||||
static int s_retry_count = 0;
|
||||
static char s_sta_ip[20] = "No Wi-Fi";
|
||||
|
||||
static void wifi_event_handler(void *arg, esp_event_base_t base, int32_t id, void *data)
|
||||
{
|
||||
@@ -30,6 +31,7 @@ static void wifi_event_handler(void *arg, esp_event_base_t base, int32_t id, voi
|
||||
ESP_LOGI(TAG, "station started, connecting...");
|
||||
esp_wifi_connect();
|
||||
} else if (base == WIFI_EVENT && id == WIFI_EVENT_STA_DISCONNECTED) {
|
||||
strncpy(s_sta_ip, "No Wi-Fi", sizeof(s_sta_ip));
|
||||
if (s_retry_count < WIFI_RETRY_MAX) {
|
||||
ESP_LOGW(TAG, "disconnected (%d), retrying in %d ms", s_retry_count, WIFI_RECONNECT_DELAY_MS);
|
||||
xEventGroupClearBits(s_wifi_group, WIFI_CONNECTED_BIT);
|
||||
@@ -39,12 +41,21 @@ static void wifi_event_handler(void *arg, esp_event_base_t base, int32_t id, voi
|
||||
}
|
||||
} else if (base == IP_EVENT && id == IP_EVENT_STA_GOT_IP) {
|
||||
ip_event_got_ip_t *event = (ip_event_got_ip_t *)data;
|
||||
ESP_LOGI(TAG, "connected, IP: " IPSTR, IP2STR(&event->ip_info.ip));
|
||||
snprintf(s_sta_ip, sizeof(s_sta_ip), IPSTR, IP2STR(&event->ip_info.ip));
|
||||
ESP_LOGI(TAG, "connected, IP: %s", s_sta_ip);
|
||||
s_retry_count = 0;
|
||||
xEventGroupSetBits(s_wifi_group, WIFI_CONNECTED_BIT);
|
||||
}
|
||||
}
|
||||
|
||||
const char *wifi_manager_get_ip(void)
|
||||
{
|
||||
if (!wifi_manager_connected()) {
|
||||
return "No Wi-Fi";
|
||||
}
|
||||
return s_sta_ip;
|
||||
}
|
||||
|
||||
esp_err_t wifi_manager_init(void)
|
||||
{
|
||||
esp_err_t ret = nvs_flash_init();
|
||||
|
||||
@@ -41,6 +41,11 @@ bool wifi_manager_connected(void);
|
||||
*/
|
||||
esp_err_t wifi_manager_wait_connected(TickType_t timeout_ticks);
|
||||
|
||||
/**
|
||||
* @brief Get the STA IP address as a string (or "No Wi-Fi" if disconnected).
|
||||
*/
|
||||
const char *wifi_manager_get_ip(void);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
Reference in New Issue
Block a user