diff --git a/main/CMakeLists.txt b/main/CMakeLists.txt index c15e956..13698bb 100644 --- a/main/CMakeLists.txt +++ b/main/CMakeLists.txt @@ -1,4 +1,6 @@ idf_component_register(SRCS "main.c" + "spi_bus_manager.c" + "storage_manager.c" INCLUDE_DIRS "." ) \ No newline at end of file diff --git a/main/hw_pins.h b/main/hw_pins.h new file mode 100644 index 0000000..994ecf0 --- /dev/null +++ b/main/hw_pins.h @@ -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) \ No newline at end of file diff --git a/main/main.c b/main/main.c index ba112ca..ac45d1c 100644 --- a/main/main.c +++ b/main/main.c @@ -1,19 +1,21 @@ -/* - * MTG RFID Companion - Entry point - * - * Phase 1: Boot scaffold. Subsequent phases attach managers here. - */ - #include #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)); } diff --git a/main/spi_bus_manager.c b/main/spi_bus_manager.c new file mode 100644 index 0000000..dac121e --- /dev/null +++ b/main/spi_bus_manager.c @@ -0,0 +1,35 @@ +/* + * MTG RFID Companion - Shared SPI bus manager + */ + +#include +#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; +} \ No newline at end of file diff --git a/main/spi_bus_manager.h b/main/spi_bus_manager.h new file mode 100644 index 0000000..ab3078b --- /dev/null +++ b/main/spi_bus_manager.h @@ -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 \ No newline at end of file diff --git a/main/storage_manager.c b/main/storage_manager.c new file mode 100644 index 0000000..1f212c3 --- /dev/null +++ b/main/storage_manager.c @@ -0,0 +1,200 @@ +/* + * MTG RFID Companion - LittleFS storage manager + */ + +#include +#include +#include +#include +#include +#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; +} \ No newline at end of file diff --git a/main/storage_manager.h b/main/storage_manager.h new file mode 100644 index 0000000..0169441 --- /dev/null +++ b/main/storage_manager.h @@ -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 \ No newline at end of file