Phase 4: ST7789 240x320 panel (esp_lcd), 5x7 font, Core 1 UI task, streaming tjpgd JPEG decode + baked storage image
This commit is contained in:
@@ -4,4 +4,7 @@ include($ENV{IDF_PATH}/tools/cmake/project.cmake)
|
|||||||
project(mtg-rfid-companion)
|
project(mtg-rfid-companion)
|
||||||
|
|
||||||
idf_build_set_property(PROJECT_DESCRIPTION "MTG RFID Companion")
|
idf_build_set_property(PROJECT_DESCRIPTION "MTG RFID Companion")
|
||||||
idf_build_set_property(PROJECT_HOMEPAGE_URL "https://scryfall.com")
|
idf_build_set_property(PROJECT_HOMEPAGE_URL "https://scryfall.com")
|
||||||
|
|
||||||
|
# Bake the seeded LittleFS contents (from ./storage) into the flash image
|
||||||
|
littlefs_create_partition_image(storage storage FLASH_IN_PROJECT)
|
||||||
@@ -3,5 +3,7 @@ idf_component_register(SRCS
|
|||||||
"spi_bus_manager.c"
|
"spi_bus_manager.c"
|
||||||
"storage_manager.c"
|
"storage_manager.c"
|
||||||
"rfid_manager.c"
|
"rfid_manager.c"
|
||||||
|
"display_manager.c"
|
||||||
|
"ui_task.c"
|
||||||
INCLUDE_DIRS "."
|
INCLUDE_DIRS "."
|
||||||
)
|
)
|
||||||
237
main/display_manager.c
Normal file
237
main/display_manager.c
Normal file
@@ -0,0 +1,237 @@
|
|||||||
|
/*
|
||||||
|
* MTG RFID Companion - ST7789 display manager (Core 1)
|
||||||
|
*/
|
||||||
|
|
||||||
|
#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_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;
|
||||||
|
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_lcd_panel_io_spi_config_t io_config = {
|
||||||
|
.dc_gpio_num = HW_LCD_DC_GPIO,
|
||||||
|
.cs_gpio_num = HW_LCD_CS_GPIO,
|
||||||
|
.pclk_hz = LCD_PIXEL_CLOCK_HZ,
|
||||||
|
.lcd_cmd_bits = LCD_CMD_BITS,
|
||||||
|
.lcd_param_bits = LCD_PARAM_BITS,
|
||||||
|
.spi_mode = 0,
|
||||||
|
.trans_queue_depth = LCD_TRANS_QUEUE,
|
||||||
|
};
|
||||||
|
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);
|
||||||
|
if (ret != ESP_OK) {
|
||||||
|
ESP_LOGE(TAG, "panel io init failed: %s", esp_err_to_name(ret));
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
esp_lcd_panel_dev_config_t panel_config = {
|
||||||
|
.reset_gpio_num = HW_LCD_RST_GPIO,
|
||||||
|
.rgb_ele_order = LCD_RGB_ELEMENT_ORDER_RGB,
|
||||||
|
.bits_per_pixel = 16,
|
||||||
|
};
|
||||||
|
ret = esp_lcd_new_panel_st7789(io, &panel_config, &s_panel);
|
||||||
|
if (ret != ESP_OK) {
|
||||||
|
ESP_LOGE(TAG, "st7789 panel create failed: %s", esp_err_to_name(ret));
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
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));
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
ESP_LOGI(TAG, "ST7789 %dx%d 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)
|
||||||
|
{
|
||||||
|
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];
|
||||||
|
uint16_t cell[FONT5X7_W][FONT5X7_H];
|
||||||
|
|
||||||
|
for (int gy = 0; gy < FONT5X7_H; gy++) {
|
||||||
|
for (int gx = 0; gx < FONT5X7_W; gx++) {
|
||||||
|
cell[gx][gy] = (glyph[gy] >> 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);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ------------------------------------------------------------------ */
|
||||||
|
/* 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;
|
||||||
|
}
|
||||||
71
main/display_manager.h
Normal file
71
main/display_manager.h
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
/*
|
||||||
|
* 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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "esp_err.h"
|
||||||
|
#include <stdint.h>
|
||||||
|
|
||||||
|
#ifdef __cplusplus
|
||||||
|
extern "C" {
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#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 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);
|
||||||
|
|
||||||
|
#ifdef __cplusplus
|
||||||
|
}
|
||||||
|
#endif
|
||||||
@@ -3,4 +3,5 @@
|
|||||||
dependencies:
|
dependencies:
|
||||||
idf: ">=5.0"
|
idf: ">=5.0"
|
||||||
abobija/rc522: "*"
|
abobija/rc522: "*"
|
||||||
joltwallet/littlefs: "*"
|
joltwallet/littlefs: "*"
|
||||||
|
esp_jpeg: ">=1.0.2"
|
||||||
@@ -5,6 +5,8 @@
|
|||||||
#include "spi_bus_manager.h"
|
#include "spi_bus_manager.h"
|
||||||
#include "storage_manager.h"
|
#include "storage_manager.h"
|
||||||
#include "rfid_manager.h"
|
#include "rfid_manager.h"
|
||||||
|
#include "display_manager.h"
|
||||||
|
#include "ui_task.h"
|
||||||
|
|
||||||
static const char *TAG = "MAIN";
|
static const char *TAG = "MAIN";
|
||||||
|
|
||||||
@@ -14,11 +16,14 @@ void app_main(void)
|
|||||||
|
|
||||||
ESP_ERROR_CHECK(spi_bus_manager_init());
|
ESP_ERROR_CHECK(spi_bus_manager_init());
|
||||||
ESP_ERROR_CHECK(storage_manager_init());
|
ESP_ERROR_CHECK(storage_manager_init());
|
||||||
|
ESP_ERROR_CHECK(display_manager_init());
|
||||||
|
|
||||||
ESP_ERROR_CHECK(rfid_manager_init());
|
ESP_ERROR_CHECK(rfid_manager_init());
|
||||||
ESP_ERROR_CHECK(rfid_manager_start());
|
ESP_ERROR_CHECK(rfid_manager_start());
|
||||||
|
|
||||||
ESP_LOGI(TAG, "Boot sequence complete. Ready for Phase 4+.");
|
ESP_ERROR_CHECK(ui_task_start());
|
||||||
|
|
||||||
|
ESP_LOGI(TAG, "Boot sequence complete. Ready for Phase 5+.");
|
||||||
|
|
||||||
while (1) {
|
while (1) {
|
||||||
vTaskDelay(pdMS_TO_TICKS(1000));
|
vTaskDelay(pdMS_TO_TICKS(1000));
|
||||||
|
|||||||
114
main/ui_font.h
Normal file
114
main/ui_font.h
Normal file
@@ -0,0 +1,114 @@
|
|||||||
|
/*
|
||||||
|
* 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 },
|
||||||
|
};
|
||||||
75
main/ui_task.c
Normal file
75
main/ui_task.c
Normal file
@@ -0,0 +1,75 @@
|
|||||||
|
/*
|
||||||
|
* MTG RFID Companion - UI task (Core 1)
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include <string.h>
|
||||||
|
#include "freertos/FreeRTOS.h"
|
||||||
|
#include "freertos/task.h"
|
||||||
|
#include "esp_log.h"
|
||||||
|
#include "display_manager.h"
|
||||||
|
#include "ui_task.h"
|
||||||
|
|
||||||
|
static const char *TAG = "ui";
|
||||||
|
|
||||||
|
#define UI_TASK_STACK (8 * 1024)
|
||||||
|
#define UI_TASK_PRIO (5)
|
||||||
|
#define UI_CORE (1)
|
||||||
|
|
||||||
|
static TaskHandle_t s_ui_task = NULL;
|
||||||
|
|
||||||
|
static void draw_color_bars(void)
|
||||||
|
{
|
||||||
|
static const uint16_t bars[] = {
|
||||||
|
COLOR_WHITE, COLOR_YELLOW, COLOR_CYAN, COLOR_GREEN,
|
||||||
|
COLOR_MAGENTA, COLOR_RED, COLOR_BLUE, COLOR_BLACK
|
||||||
|
};
|
||||||
|
const int band = DISP_W / 8;
|
||||||
|
for (int i = 0; i < 8; i++) {
|
||||||
|
display_fill_rect(i * band, 0, (i + 1) * band, 64, bars[i]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static void ui_boot_demo(void)
|
||||||
|
{
|
||||||
|
ESP_LOGI(TAG, "UI demo: color bars");
|
||||||
|
draw_color_bars();
|
||||||
|
vTaskDelay(pdMS_TO_TICKS(1500));
|
||||||
|
|
||||||
|
ESP_LOGI(TAG, "UI demo: text rendering");
|
||||||
|
display_fill(COLOR_BLACK);
|
||||||
|
display_put_text_center(8, "MTG RFID COMPANION", COLOR_YELLOW, COLOR_BLACK);
|
||||||
|
display_put_text(16, 40, "UID: 04A2B3C4", COLOR_GREEN, COLOR_BLACK);
|
||||||
|
display_put_text(16, 56, "SOL RING", COLOR_WHITE, COLOR_BLACK);
|
||||||
|
display_put_text(16, 72, "MANA {1}", COLOR_CYAN, COLOR_BLACK);
|
||||||
|
display_put_text(16, 88, "PRICE $1.25", COLOR_MAGENTA, COLOR_BLACK);
|
||||||
|
vTaskDelay(pdMS_TO_TICKS(2000));
|
||||||
|
|
||||||
|
ESP_LOGI(TAG, "UI demo: JPEG decode from LittleFS");
|
||||||
|
display_show_jpeg_file("/spiffs/cards/sample.jpg");
|
||||||
|
vTaskDelay(pdMS_TO_TICKS(3000));
|
||||||
|
|
||||||
|
ESP_LOGI(TAG, "UI demo complete");
|
||||||
|
}
|
||||||
|
|
||||||
|
static void ui_task(void *arg)
|
||||||
|
{
|
||||||
|
(void)arg;
|
||||||
|
ESP_LOGI(TAG, "UI task on core %d", xPortGetCoreID());
|
||||||
|
ui_boot_demo();
|
||||||
|
ESP_LOGI(TAG, "UI task entering idle render loop");
|
||||||
|
while (1) {
|
||||||
|
/* Placeholder idle loop; state-driven rendering lands in Phase 7. */
|
||||||
|
vTaskDelay(pdMS_TO_TICKS(100));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
esp_err_t ui_task_start(void)
|
||||||
|
{
|
||||||
|
xTaskCreatePinnedToCore(ui_task, "ui_task", UI_TASK_STACK, NULL, UI_TASK_PRIO, &s_ui_task, UI_CORE);
|
||||||
|
return ESP_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
void ui_task_wait_boot(void)
|
||||||
|
{
|
||||||
|
/* Expose for later phases; currently no-op before state machine lands. */
|
||||||
|
}
|
||||||
29
main/ui_task.h
Normal file
29
main/ui_task.h
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
/*
|
||||||
|
* MTG RFID Companion - UI task (Core 1)
|
||||||
|
*
|
||||||
|
* Phase 4: Dedicated renderer task pinned to Core 1. Runs the boot-time
|
||||||
|
* visual validation sequence (color bars, text, JPEG decode).
|
||||||
|
*/
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "esp_err.h"
|
||||||
|
#include <stdbool.h>
|
||||||
|
|
||||||
|
#ifdef __cplusplus
|
||||||
|
extern "C" {
|
||||||
|
#endif
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Create and start the UI renderer task pinned to Core 1.
|
||||||
|
*/
|
||||||
|
esp_err_t ui_task_start(void);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Block until the Core-1 renderer task has finished its boot demo.
|
||||||
|
*/
|
||||||
|
void ui_task_wait_boot(void);
|
||||||
|
|
||||||
|
#ifdef __cplusplus
|
||||||
|
}
|
||||||
|
#endif
|
||||||
17
sdkconfig
17
sdkconfig
@@ -2053,6 +2053,23 @@ CONFIG_WIFI_PROV_STA_ALL_CHANNEL_SCAN=y
|
|||||||
CONFIG_RC522_PREVENT_SECTOR_TRAILER_WRITE=y
|
CONFIG_RC522_PREVENT_SECTOR_TRAILER_WRITE=y
|
||||||
# end of RC522
|
# end of RC522
|
||||||
|
|
||||||
|
#
|
||||||
|
# JPEG Decoder
|
||||||
|
#
|
||||||
|
# CONFIG_JD_USE_ROM is not set
|
||||||
|
CONFIG_JD_SZBUF=512
|
||||||
|
CONFIG_JD_FORMAT=1
|
||||||
|
# CONFIG_JD_FORMAT_RGB888 is not set
|
||||||
|
CONFIG_JD_FORMAT_RGB565=y
|
||||||
|
CONFIG_JD_USE_SCALE=y
|
||||||
|
CONFIG_JD_TBLCLIP=y
|
||||||
|
CONFIG_JD_FASTDECODE=1
|
||||||
|
# CONFIG_JD_FASTDECODE_BASIC is not set
|
||||||
|
CONFIG_JD_FASTDECODE_32BIT=y
|
||||||
|
# CONFIG_JD_FASTDECODE_TABLE is not set
|
||||||
|
# CONFIG_JD_DEFAULT_HUFFMAN is not set
|
||||||
|
# end of JPEG Decoder
|
||||||
|
|
||||||
#
|
#
|
||||||
# LittleFS
|
# LittleFS
|
||||||
#
|
#
|
||||||
|
|||||||
@@ -5,4 +5,10 @@ CONFIG_PARTITION_TABLE_CUSTOM=y
|
|||||||
CONFIG_PARTITION_TABLE_CUSTOM_FILENAME="partitions.csv"
|
CONFIG_PARTITION_TABLE_CUSTOM_FILENAME="partitions.csv"
|
||||||
CONFIG_ESPTOOLPY_FLASHMODE_DIO=y
|
CONFIG_ESPTOOLPY_FLASHMODE_DIO=y
|
||||||
CONFIG_VFS_SUPPORT_DIR=y
|
CONFIG_VFS_SUPPORT_DIR=y
|
||||||
CONFIG_FREERTOS_SMP=y
|
CONFIG_FREERTOS_SMP=y
|
||||||
|
|
||||||
|
# tjpgd / esp_jpeg: use source tjpgd (granular line-by-line API), RGB565 out
|
||||||
|
CONFIG_JD_USE_ROM=n
|
||||||
|
CONFIG_JD_SZBUF=512
|
||||||
|
CONFIG_JD_FORMAT_RGB565=y
|
||||||
|
CONFIG_JD_FASTDECODE_32BIT=y
|
||||||
BIN
storage/cards/sample.jpg
Normal file
BIN
storage/cards/sample.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 23 KiB |
7
storage/cards/sample.json
Normal file
7
storage/cards/sample.json
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"name": "Sol Ring",
|
||||||
|
"mana_cost": "{1}",
|
||||||
|
"type": "Artifact",
|
||||||
|
"oracle": "Tap: Add CC.",
|
||||||
|
"price_usd": "1.25"
|
||||||
|
}
|
||||||
4
storage/mappings.json
Normal file
4
storage/mappings.json
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
{
|
||||||
|
"04A2B3C4": "Sol Ring",
|
||||||
|
"04E1F2A3": "Atraxa, Praetors' Voice"
|
||||||
|
}
|
||||||
56
tools/make_sample_jpg.ps1
Normal file
56
tools/make_sample_jpg.ps1
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
$ErrorActionPreference = "Stop"
|
||||||
|
Add-Type -AssemblyName System.Drawing
|
||||||
|
|
||||||
|
$dir = "storage\cards"
|
||||||
|
New-Item -ItemType Directory -Force -Path $dir | Out-Null
|
||||||
|
New-Item -ItemType Directory -Force -Path "storage" | Out-Null
|
||||||
|
|
||||||
|
$w = 240
|
||||||
|
$h = 320
|
||||||
|
$bmp = New-Object System.Drawing.Bitmap($w, $h)
|
||||||
|
$g = [System.Drawing.Graphics]::FromImage($bmp)
|
||||||
|
$g.SmoothingMode = [System.Drawing.Drawing2D.SmoothingMode]::AntiAlias
|
||||||
|
|
||||||
|
# Vertical gradient: one fast FillRectangle per row
|
||||||
|
for ($y = 0; $y -lt $h; $y++) {
|
||||||
|
$blue = [byte]([int]((255 * $y) / ($h - 1)))
|
||||||
|
$green = [byte]([int]((255 * (($y * 7) % $h)) / $h))
|
||||||
|
$red = [byte]([int]((255 * (($y * 3) % $h)) / $h))
|
||||||
|
$b = [System.Drawing.SolidBrush]::new([System.Drawing.Color]::FromArgb($red, $green, $blue))
|
||||||
|
$g.FillRectangle($b, 0, $y, $w, 1)
|
||||||
|
$b.Dispose()
|
||||||
|
}
|
||||||
|
|
||||||
|
# Diagonal stripe for visual aliasing check
|
||||||
|
$stripe = [System.Drawing.SolidBrush]::new([System.Drawing.Color]::FromArgb(255, 255, 255))
|
||||||
|
for ($x = -$h; $x -lt $w; $x += 40) {
|
||||||
|
$pts = [System.Drawing.Point[]]@(
|
||||||
|
[System.Drawing.Point]::new($x, 0),
|
||||||
|
[System.Drawing.Point]::new($x + 20, 0),
|
||||||
|
[System.Drawing.Point]::new($x + 20 + $h, $h),
|
||||||
|
[System.Drawing.Point]::new($x + $h, $h)
|
||||||
|
)
|
||||||
|
$g.FillPolygon($stripe, $pts)
|
||||||
|
}
|
||||||
|
$stripe.Dispose()
|
||||||
|
|
||||||
|
$font = [System.Drawing.Font]::new([System.Drawing.FontFamily]::GenericSansSerif, 22, [System.Drawing.FontStyle]::Bold)
|
||||||
|
$font2 = [System.Drawing.Font]::new([System.Drawing.FontFamily]::GenericSansSerif, 14, [System.Drawing.FontStyle]::Bold)
|
||||||
|
$white = [System.Drawing.Brushes]::White
|
||||||
|
$black = [System.Drawing.Brushes]::Black
|
||||||
|
|
||||||
|
# Text with outline
|
||||||
|
$txt1 = "MTG RFID"
|
||||||
|
$txt2 = "COMPANION"
|
||||||
|
$sz1 = $g.MeasureString($txt1, $font)
|
||||||
|
$g.DrawString($txt1, $font, $black, 28 - 2, 40 - 2)
|
||||||
|
$g.DrawString($txt1, $font, $white, 28, 40)
|
||||||
|
$g.DrawString($txt2, $font, $black, 28 - 2, 40 + $sz1.Height - 2)
|
||||||
|
$g.DrawString($txt2, $font, $white, 28, 40 + $sz1.Height)
|
||||||
|
$g.DrawString("240x320 RGB565", $font2, $white, 28, 260)
|
||||||
|
$g.DrawString("ST7789 TJPGD", $font2, $white, 28, 285)
|
||||||
|
|
||||||
|
$bmp.Save("$dir\sample.jpg", [System.Drawing.Imaging.ImageFormat]::Jpeg)
|
||||||
|
$g.Dispose()
|
||||||
|
$bmp.Dispose()
|
||||||
|
Get-Item "$dir\sample.jpg" | Select-Object FullName, Length
|
||||||
Reference in New Issue
Block a user