Phase 2: LittleFS storage manager on /spiffs + shared SPI2_HOST bus
This commit is contained in:
@@ -1,4 +1,6 @@
|
||||
idf_component_register(SRCS
|
||||
"main.c"
|
||||
"spi_bus_manager.c"
|
||||
"storage_manager.c"
|
||||
INCLUDE_DIRS "."
|
||||
)
|
||||
38
main/hw_pins.h
Normal file
38
main/hw_pins.h
Normal file
@@ -0,0 +1,38 @@
|
||||
/*
|
||||
* MTG RFID Companion - Hardware pin assignments
|
||||
*
|
||||
* Do NOT change these unless explicitly instructed. SPI2_HOST (VSPI) is
|
||||
* shared by the ST7789 display and the MFRC522 RFID reader.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "sdkconfig.h"
|
||||
#include "hal/gpio_types.h"
|
||||
#include "driver/spi_master.h"
|
||||
|
||||
/* Shared SPI2_HOST (VSPI) bus */
|
||||
#define HW_SPI_HOST (SPI2_HOST)
|
||||
#define HW_SPI_SCK_GPIO (GPIO_NUM_18)
|
||||
#define HW_SPI_MOSI_GPIO (GPIO_NUM_23)
|
||||
#define HW_SPI_MISO_GPIO (GPIO_NUM_19)
|
||||
|
||||
/* MFRC522 RFID reader */
|
||||
#define HW_RC522_CS_GPIO (GPIO_NUM_5)
|
||||
#define HW_RC522_RST_GPIO (GPIO_NUM_22)
|
||||
|
||||
/* ST7789 display */
|
||||
#define HW_LCD_CS_GPIO (GPIO_NUM_15)
|
||||
#define HW_LCD_DC_GPIO (GPIO_NUM_2)
|
||||
#define HW_LCD_RST_GPIO (GPIO_NUM_4)
|
||||
|
||||
/* EC11 rotary encoder */
|
||||
#define HW_ENC_CLK_GPIO (GPIO_NUM_32)
|
||||
#define HW_ENC_DT_GPIO (GPIO_NUM_33)
|
||||
#define HW_ENC_SW_GPIO (GPIO_NUM_25)
|
||||
|
||||
/* WS2812B Neopixel */
|
||||
#define HW_NEOPIXEL_GPIO (GPIO_NUM_27)
|
||||
|
||||
/* Piezo buzzer */
|
||||
#define HW_BUZZER_GPIO (GPIO_NUM_14)
|
||||
14
main/main.c
14
main/main.c
@@ -1,19 +1,21 @@
|
||||
/*
|
||||
* MTG RFID Companion - Entry point
|
||||
*
|
||||
* Phase 1: Boot scaffold. Subsequent phases attach managers here.
|
||||
*/
|
||||
|
||||
#include <stdio.h>
|
||||
#include "freertos/FreeRTOS.h"
|
||||
#include "freertos/task.h"
|
||||
#include "esp_log.h"
|
||||
#include "spi_bus_manager.h"
|
||||
#include "storage_manager.h"
|
||||
|
||||
static const char *TAG = "MAIN";
|
||||
|
||||
void app_main(void)
|
||||
{
|
||||
ESP_LOGI(TAG, "MTG RFID Companion booting...");
|
||||
|
||||
ESP_ERROR_CHECK(spi_bus_manager_init());
|
||||
ESP_ERROR_CHECK(storage_manager_init());
|
||||
|
||||
ESP_LOGI(TAG, "Boot sequence complete. Ready for Phase 3+.");
|
||||
|
||||
while (1) {
|
||||
vTaskDelay(pdMS_TO_TICKS(1000));
|
||||
}
|
||||
|
||||
35
main/spi_bus_manager.c
Normal file
35
main/spi_bus_manager.c
Normal file
@@ -0,0 +1,35 @@
|
||||
/*
|
||||
* MTG RFID Companion - Shared SPI bus manager
|
||||
*/
|
||||
|
||||
#include <string.h>
|
||||
#include "esp_log.h"
|
||||
#include "spi_bus_manager.h"
|
||||
#include "hw_pins.h"
|
||||
|
||||
static const char *TAG = "spi_bus";
|
||||
|
||||
/* 240 x 80 rows x 2 bytes per pixel. Largest contiguous DMA transfer the
|
||||
* display engine will issue. */
|
||||
#define HW_SPI_MAX_TRANSFER_SZ (240 * 80 * sizeof(uint16_t))
|
||||
|
||||
esp_err_t spi_bus_manager_init(void)
|
||||
{
|
||||
spi_bus_config_t buscfg = {
|
||||
.sclk_io_num = HW_SPI_SCK_GPIO,
|
||||
.mosi_io_num = HW_SPI_MOSI_GPIO,
|
||||
.miso_io_num = HW_SPI_MISO_GPIO,
|
||||
.quadwp_io_num = -1,
|
||||
.quadhd_io_num = -1,
|
||||
.max_transfer_sz = HW_SPI_MAX_TRANSFER_SZ,
|
||||
};
|
||||
|
||||
esp_err_t ret = spi_bus_initialize(HW_SPI_HOST, &buscfg, SPI_DMA_CH_AUTO);
|
||||
if (ret != ESP_OK) {
|
||||
ESP_LOGE(TAG, "spi_bus_initialize failed: %s", esp_err_to_name(ret));
|
||||
return ret;
|
||||
}
|
||||
ESP_LOGI(TAG, "SPI2_HOST ready: SCK=%d MOSI=%d MISO=%d",
|
||||
HW_SPI_SCK_GPIO, HW_SPI_MOSI_GPIO, HW_SPI_MISO_GPIO);
|
||||
return ESP_OK;
|
||||
}
|
||||
27
main/spi_bus_manager.h
Normal file
27
main/spi_bus_manager.h
Normal file
@@ -0,0 +1,27 @@
|
||||
/*
|
||||
* MTG RFID Companion - Shared SPI bus manager
|
||||
*
|
||||
* Phase 2 Step 2.3: Initializes the single SPI2_HOST (VSPI) bus shared by
|
||||
* the ST7789 display and the MFRC522 RFID reader.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "esp_err.h"
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
/**
|
||||
* @brief Initialize the shared SPI2_HOST bus.
|
||||
*
|
||||
* Must be called exactly once before the display and RFID driver attach
|
||||
* their SPI devices. MFRC522 is passed a NULL bus_config so that it adds
|
||||
* its device to this pre-existing bus instead of trying to own it.
|
||||
*/
|
||||
esp_err_t spi_bus_manager_init(void);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
200
main/storage_manager.c
Normal file
200
main/storage_manager.c
Normal file
@@ -0,0 +1,200 @@
|
||||
/*
|
||||
* MTG RFID Companion - LittleFS storage manager
|
||||
*/
|
||||
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
#include <stdlib.h>
|
||||
#include <sys/stat.h>
|
||||
#include <sys/unistd.h>
|
||||
#include "esp_log.h"
|
||||
#include "esp_littlefs.h"
|
||||
#include "storage_manager.h"
|
||||
|
||||
static const char *TAG = "storage";
|
||||
|
||||
#define STORAGE_PARTITION_LABEL "storage"
|
||||
#define STORAGE_BOOT_COUNT_FILE STORAGE_BASE_PATH "/boot_count.txt"
|
||||
#define STORAGE_TEST_FILE STORAGE_BASE_PATH "/test.txt"
|
||||
|
||||
static const char SAMPLE_MAPPINGS[] =
|
||||
"{\n"
|
||||
" \"04A2B3C4\": \"Sol Ring\",\n"
|
||||
" \"04E1F2A3\": \"Atraxa, Praetors' Voice\"\n"
|
||||
"}\n";
|
||||
|
||||
static const char SAMPLE_CARD[] =
|
||||
"{\n"
|
||||
" \"name\": \"Sol Ring\",\n"
|
||||
" \"mana_cost\": \"{1}\",\n"
|
||||
" \"type\": \"Artifact\",\n"
|
||||
" \"oracle\": \"Tap: Add CC.\",\n"
|
||||
" \"price_usd\": \"1.25\"\n"
|
||||
"}\n";
|
||||
|
||||
bool storage_file_exists(const char *path)
|
||||
{
|
||||
struct stat st;
|
||||
return (stat(path, &st) == 0) && S_ISREG(st.st_mode);
|
||||
}
|
||||
|
||||
esp_err_t storage_read_text(const char *path, char **out_buf, size_t *out_len)
|
||||
{
|
||||
if (out_buf == NULL || out_len == NULL) {
|
||||
return ESP_ERR_INVALID_ARG;
|
||||
}
|
||||
*out_buf = NULL;
|
||||
*out_len = 0;
|
||||
|
||||
FILE *f = fopen(path, "rb");
|
||||
if (f == NULL) {
|
||||
return ESP_ERR_NOT_FOUND;
|
||||
}
|
||||
|
||||
if (fseek(f, 0, SEEK_END) != 0) {
|
||||
fclose(f);
|
||||
return ESP_FAIL;
|
||||
}
|
||||
long size = ftell(f);
|
||||
if (size < 0) {
|
||||
fclose(f);
|
||||
return ESP_FAIL;
|
||||
}
|
||||
rewind(f);
|
||||
|
||||
char *buf = malloc((size_t)size + 1);
|
||||
if (buf == NULL) {
|
||||
fclose(f);
|
||||
return ESP_ERR_NO_MEM;
|
||||
}
|
||||
|
||||
size_t rd = fread(buf, 1, (size_t)size, f);
|
||||
fclose(f);
|
||||
if (rd != (size_t)size) {
|
||||
free(buf);
|
||||
return ESP_FAIL;
|
||||
}
|
||||
buf[rd] = '\0';
|
||||
*out_buf = buf;
|
||||
*out_len = rd;
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
esp_err_t storage_write_text(const char *path, const char *text)
|
||||
{
|
||||
char tmp[288];
|
||||
const size_t len = strlen(path);
|
||||
|
||||
if (len > sizeof(tmp) - 8) {
|
||||
return ESP_ERR_INVALID_ARG;
|
||||
}
|
||||
snprintf(tmp, sizeof(tmp), "%s.tmp", path);
|
||||
|
||||
FILE *f = fopen(tmp, "wb");
|
||||
if (f == NULL) {
|
||||
ESP_LOGE(TAG, "open %s failed", tmp);
|
||||
return ESP_FAIL;
|
||||
}
|
||||
size_t wr = fwrite(text, 1, strlen(text), f);
|
||||
if (wr != strlen(text) || fflush(f) != 0 || fclose(f) != 0) {
|
||||
ESP_LOGE(TAG, "write %s failed", tmp);
|
||||
remove(tmp);
|
||||
return ESP_FAIL;
|
||||
}
|
||||
if (rename(tmp, path) != 0) {
|
||||
ESP_LOGE(TAG, "rename %s -> %s failed", tmp, path);
|
||||
remove(tmp);
|
||||
return ESP_FAIL;
|
||||
}
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
static esp_err_t seed_defaults(void)
|
||||
{
|
||||
/* Create /spiffs/cards directory */
|
||||
mkdir(STORAGE_CARDS_DIR, 0755);
|
||||
|
||||
if (!storage_file_exists(STORAGE_MAPPINGS)) {
|
||||
ESP_LOGI(TAG, "seeding %s", STORAGE_MAPPINGS);
|
||||
esp_err_t ret = storage_write_text(STORAGE_MAPPINGS, SAMPLE_MAPPINGS);
|
||||
if (ret != ESP_OK) {
|
||||
return ret;
|
||||
}
|
||||
}
|
||||
|
||||
char sample_path[64];
|
||||
snprintf(sample_path, sizeof(sample_path), STORAGE_CARDS_DIR "/sample.json");
|
||||
if (!storage_file_exists(sample_path)) {
|
||||
ESP_LOGI(TAG, "seeding %s", sample_path);
|
||||
esp_err_t ret = storage_write_text(sample_path, SAMPLE_CARD);
|
||||
if (ret != ESP_OK) {
|
||||
return ret;
|
||||
}
|
||||
}
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
static void cross_reboot_persistence_test(void)
|
||||
{
|
||||
/* Persistent boot counter: proves writes survive a reset/reboot. */
|
||||
char buf[16] = {0};
|
||||
uint32_t count = 0;
|
||||
|
||||
FILE *f = fopen(STORAGE_BOOT_COUNT_FILE, "rb");
|
||||
if (f != NULL) {
|
||||
if (fread(buf, 1, sizeof(buf) - 1, f) > 0) {
|
||||
count = (uint32_t)strtoul(buf, NULL, 10);
|
||||
ESP_LOGI(TAG, "boot_count read back = %lu (persistence across reboots OK)", (unsigned long)count);
|
||||
}
|
||||
fclose(f);
|
||||
}
|
||||
|
||||
count++;
|
||||
snprintf(buf, sizeof(buf), "%lu\n", (unsigned long)count);
|
||||
storage_write_text(STORAGE_BOOT_COUNT_FILE, buf);
|
||||
ESP_LOGI(TAG, "boot_count updated to %lu", (unsigned long)count);
|
||||
|
||||
/* Sanity write/read within the same boot. */
|
||||
storage_write_text(STORAGE_TEST_FILE, "MTG-RFID-COMPANION persistence test\n");
|
||||
char *rd = NULL;
|
||||
size_t rd_len = 0;
|
||||
if (storage_read_text(STORAGE_TEST_FILE, &rd, &rd_len) == ESP_OK && rd != NULL) {
|
||||
ESP_LOGI(TAG, "test.txt echoed back: %s", rd);
|
||||
free(rd);
|
||||
} else {
|
||||
ESP_LOGE(TAG, "test.txt read-back failed");
|
||||
}
|
||||
}
|
||||
|
||||
esp_err_t storage_manager_init(void)
|
||||
{
|
||||
esp_vfs_littlefs_conf_t conf = {
|
||||
.base_path = STORAGE_BASE_PATH,
|
||||
.partition_label = STORAGE_PARTITION_LABEL,
|
||||
.format_if_mount_failed = true,
|
||||
};
|
||||
|
||||
esp_err_t ret = esp_vfs_littlefs_register(&conf);
|
||||
if (ret != ESP_OK) {
|
||||
ESP_LOGE(TAG, "LittleFS mount failed: %s", esp_err_to_name(ret));
|
||||
return ret;
|
||||
}
|
||||
|
||||
size_t total = 0;
|
||||
size_t used = 0;
|
||||
if (esp_littlefs_info(STORAGE_PARTITION_LABEL, &total, &used) == ESP_OK) {
|
||||
ESP_LOGI(TAG, "LittleFS mounted at %s: %d bytes total, %d used",
|
||||
STORAGE_BASE_PATH, (int)total, (int)used);
|
||||
} else {
|
||||
ESP_LOGI(TAG, "LittleFS mounted at %s", STORAGE_BASE_PATH);
|
||||
}
|
||||
|
||||
ret = seed_defaults();
|
||||
if (ret != ESP_OK) {
|
||||
ESP_LOGE(TAG, "seeding default files failed: %s", esp_err_to_name(ret));
|
||||
return ret;
|
||||
}
|
||||
|
||||
cross_reboot_persistence_test();
|
||||
return ESP_OK;
|
||||
}
|
||||
45
main/storage_manager.h
Normal file
45
main/storage_manager.h
Normal file
@@ -0,0 +1,45 @@
|
||||
/*
|
||||
* MTG RFID Companion - LittleFS storage manager
|
||||
*
|
||||
* Phase 2: Mounts LittleFS at /spiffs on the "storage" partition, seeds the
|
||||
* default database files on first boot, and exposes file helpers used by the
|
||||
* RFID/cache/web subsystems.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "esp_err.h"
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
#define STORAGE_BASE_PATH "/spiffs"
|
||||
#define STORAGE_MAPPINGS STORAGE_BASE_PATH "/mappings.json"
|
||||
#define STORAGE_CARDS_DIR STORAGE_BASE_PATH "/cards"
|
||||
|
||||
/**
|
||||
* @brief Mount LittleFS and seed default files.
|
||||
* @return ESP_OK on success.
|
||||
*/
|
||||
esp_err_t storage_manager_init(void);
|
||||
|
||||
/**
|
||||
* @brief Check whether a file exists on the filesystem.
|
||||
*/
|
||||
bool storage_file_exists(const char *path);
|
||||
|
||||
/**
|
||||
* @brief Read an entire file into a heap allocated buffer (caller frees).
|
||||
* @param[out] out_buf NULL terminated buffer; NULL and out_len=0 if missing.
|
||||
*/
|
||||
esp_err_t storage_read_text(const char *path, char **out_buf, size_t *out_len);
|
||||
|
||||
/**
|
||||
* @brief Write text (plus terminating newline) to a file atomically.
|
||||
*/
|
||||
esp_err_t storage_write_text(const char *path, const char *text);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
Reference in New Issue
Block a user