From b28f81404e1ef2c7fb8cb96e1489663b0770a802 Mon Sep 17 00:00:00 2001 From: shylie Date: Sun, 11 Jan 2026 08:07:13 -0500 Subject: [PATCH] Add mana menu --- CMakeLists.txt | 2 + include/cardslot.h | 9 ++- include/icons.h | 18 ++++++ include/menu.h | 61 ++++++++++++++++++ src/cardslot.cpp | 43 +++++++++---- src/icons.cpp | 29 +++++++++ src/main.cpp | 105 +++++++++++++++++++++---------- src/manamenu.cpp | 151 +++++++++++++++++++++++++++++++++++++++++++++ 8 files changed, 369 insertions(+), 49 deletions(-) create mode 100644 include/menu.h create mode 100644 src/icons.cpp create mode 100644 src/manamenu.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 99738fa..6b82cd1 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -13,6 +13,8 @@ add_executable(mtgcard src/lib.cpp src/flash.cpp src/cardslot.cpp + src/icons.cpp + src/manamenu.cpp src/usb_descriptors.c diff --git a/include/cardslot.h b/include/cardslot.h index 2568722..55cf74b 100644 --- a/include/cardslot.h +++ b/include/cardslot.h @@ -13,8 +13,11 @@ private: public: CardSlot(lib::Flash& flash, uint16_t card_index); - void mark_unused(lib::Flash& flash); void set_name(const uint8_t* name, uint8_t length); + void set_cmc(uint8_t cmc); + void set_colors(uint8_t colors); + + void mark_unused(lib::Flash& flash); void save_data(lib::Flash& flash); void erase(lib::Flash& flash); @@ -42,8 +45,10 @@ private: struct { bool in_use : 1; - uint32_t times_erased : 26; + uint8_t cmc : 5; + uint8_t colors : 5; uint8_t name_length : NAME_BITS; + uint32_t times_erased; } status; uint8_t name[MAX_NAME_LENGTH]; diff --git a/include/icons.h b/include/icons.h index 441ce0c..d885d0b 100644 --- a/include/icons.h +++ b/include/icons.h @@ -1,6 +1,8 @@ #ifndef ICONS_H #define ICONS_H +#include + extern unsigned char __0_svg_raw[]; extern int __0_svg_raw_len; @@ -82,4 +84,20 @@ extern int G_svg_raw_len; extern unsigned char C_svg_raw[]; extern int C_svg_raw_len; +namespace icon +{ + +constexpr const unsigned char* NUMBER_ICONS[21] + = { __0_svg_raw, __1_svg_raw, __2_svg_raw, __3_svg_raw, __4_svg_raw, + __5_svg_raw, __6_svg_raw, __7_svg_raw, __8_svg_raw, __9_svg_raw, + __10_svg_raw, __11_svg_raw, __12_svg_raw, __13_svg_raw, __14_svg_raw, + __15_svg_raw, __16_svg_raw, __17_svg_raw, __18_svg_raw, __19_svg_raw, + __20_svg_raw }; + +constexpr int LENGTH = 32 * 32 * 3; + +const unsigned char* wubrgc(uint8_t value); + +} + #endif // ICONS_H diff --git a/include/menu.h b/include/menu.h new file mode 100644 index 0000000..6397b63 --- /dev/null +++ b/include/menu.h @@ -0,0 +1,61 @@ +#ifndef MENU_H +#define MENU_H + +#include "display.h" + +#include + +class Menu +{ +public: + static constexpr int HOLD_MS_THRESHOLD = 250; + + explicit Menu(lib::Display& display); + virtual ~Menu() = default; + + virtual void onTick(float dt) = 0; + + virtual void onLeftPressed() = 0; + virtual void onLeftHeld() = 0; + + virtual void onMenuPressed() = 0; + virtual void onMenuHeld() = 0; + + virtual void onRightPressed() = 0; + virtual void onRightHeld() = 0; + +protected: + lib::Display* const display; +}; + +namespace menus +{ + +class ManaMenu : public Menu +{ +public: + explicit ManaMenu(lib::Display& display); + ~ManaMenu() override = default; + + void onTick(float dt) override; + + void onLeftPressed() override; + void onLeftHeld() override; + + void onMenuPressed() override; + void onMenuHeld() override; + + void onRightPressed() override; + void onRightHeld() override; + +private: + uint8_t current; + bool going_left; + float progress; +}; + +constexpr size_t MAX_MENU_SIZE = sizeof(ManaMenu); + +} + +#endif // MENU_H diff --git a/src/cardslot.cpp b/src/cardslot.cpp index 330b0f1..3416f56 100644 --- a/src/cardslot.cpp +++ b/src/cardslot.cpp @@ -13,13 +13,6 @@ uint16_t CardSlot::get_image_start_page_index() const return card_index * TOTAL_PAGE_COUNT + 1; } -void CardSlot::mark_unused(lib::Flash& flash) -{ - flash.read_page(card_index * TOTAL_PAGE_COUNT); - flash.page()[0] &= 0xFE; - flash.write_page(card_index * TOTAL_PAGE_COUNT); -} - void CardSlot::set_name(const uint8_t* name, uint8_t name_length) { if (name_length > 32) @@ -31,13 +24,28 @@ void CardSlot::set_name(const uint8_t* name, uint8_t name_length) status.name_length = name_length; } +void CardSlot::set_cmc(uint8_t cmc) { status.cmc = cmc > 20 ? 20 : cmc; } + +void CardSlot::set_colors(uint8_t colors) { status.colors = colors & 0b11111; } + +void CardSlot::mark_unused(lib::Flash& flash) +{ + flash.read_page(card_index * TOTAL_PAGE_COUNT); + flash.page()[0] &= 0xFE; + flash.write_page(card_index * TOTAL_PAGE_COUNT); +} + void CardSlot::save_data(lib::Flash& flash) { // if we're saving data, the slot is in use status.in_use = true; + // and the erase count should be updated + status.times_erased += 1; + uint32_t raw_status = (status.in_use << 0) | (status.name_length << 1) - | (status.times_erased << (NAME_BITS + 1)); + | (status.cmc << (NAME_BITS + 1)) + | (status.colors << (NAME_BITS + 6)); auto& page = flash.page(); page[0] = (raw_status >> 0) & 0xFF; @@ -45,7 +53,12 @@ void CardSlot::save_data(lib::Flash& flash) page[2] = (raw_status >> 16) & 0xFF; page[3] = (raw_status >> 24) & 0xFF; - memcpy(page + 4, name, status.name_length); + page[4] = (status.times_erased >> 0) & 0xFF; + page[5] = (status.times_erased >> 8) & 0xFF; + page[6] = (status.times_erased >> 16) & 0xFF; + page[7] = (status.times_erased >> 24) & 0xFF; + + memcpy(page + 8, name, status.name_length); flash.write_page(card_index * TOTAL_PAGE_COUNT); } @@ -101,20 +114,24 @@ void CardSlot::load_data(lib::Flash& flash) status.in_use = raw_status & 0x01; status.name_length = raw_status & 0x3E; - status.times_erased = raw_status & 0xFFFFFFC0; + status.cmc = raw_status & 0x7C0; + status.colors = raw_status & 0xF800; - memcpy(name, page + 4, status.name_length); + status.times_erased + = page[4] | (page[5] << 8) | (page[6] << 16) | (page[7] << 24); + + memcpy(name, page + 8, status.name_length); } int32_t CardSlot::get_unused(lib::Flash& flash) { - int min_unused = INT32_MAX; + uint32_t min_unused = UINT32_MAX; int min_unused_index = -1; for (int i = 0; i < MAX_CARDS; i++) { CardSlot c(flash, i); - if (!c.is_used() && c.times_erased() < min_unused) + if (!c.is_used() && c.times_erased() <= min_unused) { min_unused = c.times_erased(); min_unused_index = i; diff --git a/src/icons.cpp b/src/icons.cpp new file mode 100644 index 0000000..0ef5cf0 --- /dev/null +++ b/src/icons.cpp @@ -0,0 +1,29 @@ +#include "icons.h" + +const unsigned char* icon::wubrgc(uint8_t value) +{ + if (value & 0b10000) + { + return W_svg_raw; + } + else if (value & 0b1000) + { + return U_svg_raw; + } + else if (value & 0b100) + { + return B_svg_raw; + } + else if (value & 0b10) + { + return R_svg_raw; + } + else if (value & 0b1) + { + return G_svg_raw; + } + else + { + return C_svg_raw; + } +} diff --git a/src/main.cpp b/src/main.cpp index 45ddd04..b348db4 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -1,9 +1,10 @@ #include "cardslot.h" #include "display.h" #include "flash.h" -#include "icons.h" +#include "menu.h" #include "pixelstream.h" +#include #include #include #include @@ -36,6 +37,9 @@ Flash flash(FLASH_SCK, FLASH_TX, FLASH_RX, FLASH_CS, 70000000); enum class RX { WAITING, + GETTING_CMC, + GETTING_COLORS, + GETTING_NAME_LENGTH, GETTING_NAME, GETTING_IMAGE }; @@ -50,6 +54,9 @@ struct uint8_t written; } name; + uint8_t cmc; + uint8_t colors; + struct { size_t written = 0; @@ -61,6 +68,27 @@ struct int32_t card_index = 0; } rx; +uint8_t + MENU_MEMORY[menus::MAX_MENU_SIZE * 5]; // menu stack is at most 5 menus deep +int8_t current_menu = -1; + +Menu* get_current_menu() +{ + return reinterpret_cast(MENU_MEMORY + + current_menu * menus::MAX_MENU_SIZE); +} + +template void push_menu() +{ + current_menu += 1; + new (get_current_menu()) T(display); +} +void pop_menu() +{ + get_current_menu()->~Menu(); + current_menu -= 1; +} + } int main() @@ -83,56 +111,43 @@ int main() uint16_t card_index = 0; absolute_time_t last_press = get_absolute_time(); + absolute_time_t previous_time = last_press; - const auto& pixel_fn = [](void* pixels, uint8_t data) - { static_cast(pixels)->write(data); }; - - { - auto pixels = display.pixels(); - CardSlot(flash, 0).each_pixel(flash, pixel_fn, &pixels); - } + push_menu(); while (true) { absolute_time_t current = get_absolute_time(); + float dt = absolute_time_diff_us(previous_time, current) / 1000000.0f; if (!gpio_get(BUTTON_LEFT) - && absolute_time_diff_us(last_press, current) > 1000 * 10) + && absolute_time_diff_us(last_press, current) > 1000 * 150) { - do - { - card_index -= 1; - if (card_index >= CardSlot::MAX_CARDS) - { - card_index = CardSlot::MAX_CARDS - 1; - } - } while (!CardSlot(flash, card_index).is_used()); + get_current_menu()->onLeftPressed(); - auto pixels = display.pixels(); - CardSlot(flash, card_index).each_pixel(flash, pixel_fn, &pixels); + last_press = current; + } + + if (!gpio_get(BUTTON_MIDDLE) + && absolute_time_diff_us(last_press, current) > 1000 * 150) + { + get_current_menu()->onMenuPressed(); last_press = current; } if (!gpio_get(BUTTON_RIGHT) - && absolute_time_diff_us(last_press, current) > 1000 * 10) + && absolute_time_diff_us(last_press, current) > 1000 * 150) { - do - { - card_index += 1; - if (card_index >= CardSlot::MAX_CARDS) - { - card_index = 0; - } - } while (!CardSlot(flash, card_index).is_used()); - - auto pixels = display.pixels(); - CardSlot(flash, card_index).each_pixel(flash, pixel_fn, &pixels); + get_current_menu()->onRightPressed(); last_press = current; } tud_task(); + get_current_menu()->onTick(dt); + + previous_time = current; } } @@ -154,11 +169,31 @@ void tud_vendor_rx_cb(uint8_t itf, const uint8_t* buffer, uint16_t bufsize) return; } - rx.state = RX::GETTING_NAME; + rx.state = RX::GETTING_CMC; rx.card_index = CardSlot::get_unused(flash); + } + else if (rx.state == RX::GETTING_CMC) + { + rx.state = RX::GETTING_COLORS; - rx.name.size = buffer[0] > 32 ? 32 : buffer[0]; + rx.cmc = buffer[read] > 20 ? 20 : buffer[read]; + + read += 1; + } + else if (rx.state == RX::GETTING_COLORS) + { + rx.state = RX::GETTING_NAME_LENGTH; + + rx.colors = buffer[read] & 0b11111; + + read += 1; + } + else if (rx.state == RX::GETTING_NAME_LENGTH) + { + rx.state = RX::GETTING_NAME; + + rx.name.size = buffer[read] > 32 ? 32 : buffer[read]; rx.name.written = 0; read += 1; @@ -186,11 +221,13 @@ void tud_vendor_rx_cb(uint8_t itf, const uint8_t* buffer, uint16_t bufsize) CardSlot slot(flash, rx.card_index); slot.erase(flash); slot.set_name(rx.name.buffer, rx.name.size); + slot.set_cmc(rx.cmc); + slot.set_colors(rx.colors); slot.save_data(flash); } } } - else + else if (rx.state == RX::GETTING_IMAGE) { size_t copy_size = bufsize - read < Flash::Page::SIZE - rx.image.written ? bufsize - read diff --git a/src/manamenu.cpp b/src/manamenu.cpp new file mode 100644 index 0000000..b4ec396 --- /dev/null +++ b/src/manamenu.cpp @@ -0,0 +1,151 @@ +#include "icons.h" +#include "menu.h" +#include "pixelstream.h" + +#include + +namespace +{ + +constexpr uint8_t COLORS[] = { 0, 0b1, 0b10, 0b100, 0b1000, 0b10000 }; + +} + +using namespace menus; + +Menu::Menu(lib::Display& display) : + display(&display) +{ +} + +ManaMenu::ManaMenu(lib::Display& display) : + Menu(display), + current(0), + going_left(false), + progress(0.999f) +{ +} + +void ManaMenu::onTick(float dt) +{ + constexpr float PI = 3.1415926f; + constexpr float STEP = 2 * PI / 6.0f; + constexpr float RADIUS = 80; + + if (progress < 1.0f) + { + const float offset = (going_left ? -1 : 1) * STEP * progress + STEP / 2.0f; + + // erase previous + for (int i = 0; i < sizeof(COLORS) / sizeof(*COLORS); i++) + { + const float angle = (i - current) * STEP - offset; + + const float x = cosf(angle) * RADIUS; + const float y = sinf(angle) * RADIUS; + + const int left = x - 20 + 120; + const int top = y - 20 + 160; + + // top rectangle + { + display->set_update_area(left, top, 38, 5); + auto pixels = display->pixels(); + for (int pixel = 0; pixel < 38 * 5; pixel++) + { + pixels.write(0, 0, 0); + } + } + + // bottom rectangle + { + display->set_update_area(left, top + 36, 38, 5); + auto pixels = display->pixels(); + for (int pixel = 0; pixel < 38 * 5; pixel++) + { + pixels.write(0, 0, 0); + } + } + + // left rectangle + { + display->set_update_area(left, top + 3, 5, 38); + auto pixels = display->pixels(); + for (int pixel = 0; pixel < 5 * 38; pixel++) + { + pixels.write(0, 0, 0); + } + } + + // right rectangle + { + display->set_update_area(left + 36, top + 3, 5, 38); + auto pixels = display->pixels(); + for (int pixel = 0; pixel < 5 * 38; pixel++) + { + pixels.write(0, 0, 0); + } + } + } + + progress += dt * 1.2f; + if (progress > 1.0f) + { + progress = 1.0f; + } + + // write new + for (int i = 0; i < sizeof(COLORS) / sizeof(*COLORS); i++) + { + const float angle = (i - current) * STEP - offset; + + const float x = cosf(angle) * RADIUS; + const float y = sinf(angle) * RADIUS; + + const int left = x - 15 + 120; + const int top = y - 15 + 160; + + display->set_update_area(left, top, 31, 32); + auto pixels = display->pixels(); + for (int pixel = 0; pixel < icon::LENGTH; pixel++) + { + pixels.write(icon::wubrgc(COLORS[i])[pixel]); + } + } + + sleep_ms(1); + } +} + +void ManaMenu::onLeftPressed() +{ + if (current == 0) + { + current = 5; + } + else + { + current -= 1; + } + + progress = 0.0f; +} +void ManaMenu::onLeftHeld() {} + +void ManaMenu::onMenuPressed() {} +void ManaMenu::onMenuHeld() {} + +void ManaMenu::onRightPressed() +{ + if (current == 5) + { + current = 0; + } + else + { + current += 1; + } + + progress = 0.0f; +} +void ManaMenu::onRightHeld() {}