Add mana menu

This commit is contained in:
shylie 2026-01-11 08:07:13 -05:00
parent 85dcf5f32e
commit b28f81404e
8 changed files with 369 additions and 49 deletions

View File

@ -13,6 +13,8 @@ add_executable(mtgcard
src/lib.cpp src/lib.cpp
src/flash.cpp src/flash.cpp
src/cardslot.cpp src/cardslot.cpp
src/icons.cpp
src/manamenu.cpp
src/usb_descriptors.c src/usb_descriptors.c

View File

@ -13,8 +13,11 @@ private:
public: public:
CardSlot(lib::Flash& flash, uint16_t card_index); 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_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 save_data(lib::Flash& flash);
void erase(lib::Flash& flash); void erase(lib::Flash& flash);
@ -42,8 +45,10 @@ private:
struct struct
{ {
bool in_use : 1; bool in_use : 1;
uint32_t times_erased : 26; uint8_t cmc : 5;
uint8_t colors : 5;
uint8_t name_length : NAME_BITS; uint8_t name_length : NAME_BITS;
uint32_t times_erased;
} status; } status;
uint8_t name[MAX_NAME_LENGTH]; uint8_t name[MAX_NAME_LENGTH];

View File

@ -1,6 +1,8 @@
#ifndef ICONS_H #ifndef ICONS_H
#define ICONS_H #define ICONS_H
#include <pico/stdlib.h>
extern unsigned char __0_svg_raw[]; extern unsigned char __0_svg_raw[];
extern int __0_svg_raw_len; extern int __0_svg_raw_len;
@ -82,4 +84,20 @@ extern int G_svg_raw_len;
extern unsigned char C_svg_raw[]; extern unsigned char C_svg_raw[];
extern int C_svg_raw_len; 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 #endif // ICONS_H

61
include/menu.h Normal file
View File

@ -0,0 +1,61 @@
#ifndef MENU_H
#define MENU_H
#include "display.h"
#include <pico/stdlib.h>
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

View File

@ -13,13 +13,6 @@ uint16_t CardSlot::get_image_start_page_index() const
return card_index * TOTAL_PAGE_COUNT + 1; 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) void CardSlot::set_name(const uint8_t* name, uint8_t name_length)
{ {
if (name_length > 32) 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; 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) void CardSlot::save_data(lib::Flash& flash)
{ {
// if we're saving data, the slot is in use // if we're saving data, the slot is in use
status.in_use = true; 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) 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(); auto& page = flash.page();
page[0] = (raw_status >> 0) & 0xFF; page[0] = (raw_status >> 0) & 0xFF;
@ -45,7 +53,12 @@ void CardSlot::save_data(lib::Flash& flash)
page[2] = (raw_status >> 16) & 0xFF; page[2] = (raw_status >> 16) & 0xFF;
page[3] = (raw_status >> 24) & 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); 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.in_use = raw_status & 0x01;
status.name_length = raw_status & 0x3E; 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) int32_t CardSlot::get_unused(lib::Flash& flash)
{ {
int min_unused = INT32_MAX; uint32_t min_unused = UINT32_MAX;
int min_unused_index = -1; int min_unused_index = -1;
for (int i = 0; i < MAX_CARDS; i++) for (int i = 0; i < MAX_CARDS; i++)
{ {
CardSlot c(flash, 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 = c.times_erased();
min_unused_index = i; min_unused_index = i;

29
src/icons.cpp Normal file
View File

@ -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;
}
}

View File

@ -1,9 +1,10 @@
#include "cardslot.h" #include "cardslot.h"
#include "display.h" #include "display.h"
#include "flash.h" #include "flash.h"
#include "icons.h" #include "menu.h"
#include "pixelstream.h" #include "pixelstream.h"
#include <new>
#include <pico/binary_info.h> #include <pico/binary_info.h>
#include <pico/stdlib.h> #include <pico/stdlib.h>
#include <tusb.h> #include <tusb.h>
@ -36,6 +37,9 @@ Flash flash(FLASH_SCK, FLASH_TX, FLASH_RX, FLASH_CS, 70000000);
enum class RX enum class RX
{ {
WAITING, WAITING,
GETTING_CMC,
GETTING_COLORS,
GETTING_NAME_LENGTH,
GETTING_NAME, GETTING_NAME,
GETTING_IMAGE GETTING_IMAGE
}; };
@ -50,6 +54,9 @@ struct
uint8_t written; uint8_t written;
} name; } name;
uint8_t cmc;
uint8_t colors;
struct struct
{ {
size_t written = 0; size_t written = 0;
@ -61,6 +68,27 @@ struct
int32_t card_index = 0; int32_t card_index = 0;
} rx; } 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*>(MENU_MEMORY
+ current_menu * menus::MAX_MENU_SIZE);
}
template <typename T> void push_menu()
{
current_menu += 1;
new (get_current_menu()) T(display);
}
void pop_menu()
{
get_current_menu()->~Menu();
current_menu -= 1;
}
} }
int main() int main()
@ -83,56 +111,43 @@ int main()
uint16_t card_index = 0; uint16_t card_index = 0;
absolute_time_t last_press = get_absolute_time(); absolute_time_t last_press = get_absolute_time();
absolute_time_t previous_time = last_press;
const auto& pixel_fn = [](void* pixels, uint8_t data) push_menu<menus::ManaMenu>();
{ static_cast<PixelStream*>(pixels)->write(data); };
{
auto pixels = display.pixels();
CardSlot(flash, 0).each_pixel(flash, pixel_fn, &pixels);
}
while (true) while (true)
{ {
absolute_time_t current = get_absolute_time(); absolute_time_t current = get_absolute_time();
float dt = absolute_time_diff_us(previous_time, current) / 1000000.0f;
if (!gpio_get(BUTTON_LEFT) if (!gpio_get(BUTTON_LEFT)
&& absolute_time_diff_us(last_press, current) > 1000 * 10) && absolute_time_diff_us(last_press, current) > 1000 * 150)
{ {
do get_current_menu()->onLeftPressed();
{
card_index -= 1;
if (card_index >= CardSlot::MAX_CARDS)
{
card_index = CardSlot::MAX_CARDS - 1;
}
} while (!CardSlot(flash, card_index).is_used());
auto pixels = display.pixels(); last_press = current;
CardSlot(flash, card_index).each_pixel(flash, pixel_fn, &pixels); }
if (!gpio_get(BUTTON_MIDDLE)
&& absolute_time_diff_us(last_press, current) > 1000 * 150)
{
get_current_menu()->onMenuPressed();
last_press = current; last_press = current;
} }
if (!gpio_get(BUTTON_RIGHT) if (!gpio_get(BUTTON_RIGHT)
&& absolute_time_diff_us(last_press, current) > 1000 * 10) && absolute_time_diff_us(last_press, current) > 1000 * 150)
{ {
do get_current_menu()->onRightPressed();
{
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);
last_press = current; last_press = current;
} }
tud_task(); 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; return;
} }
rx.state = RX::GETTING_NAME; rx.state = RX::GETTING_CMC;
rx.card_index = CardSlot::get_unused(flash); 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; rx.name.written = 0;
read += 1; 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); CardSlot slot(flash, rx.card_index);
slot.erase(flash); slot.erase(flash);
slot.set_name(rx.name.buffer, rx.name.size); slot.set_name(rx.name.buffer, rx.name.size);
slot.set_cmc(rx.cmc);
slot.set_colors(rx.colors);
slot.save_data(flash); slot.save_data(flash);
} }
} }
} }
else else if (rx.state == RX::GETTING_IMAGE)
{ {
size_t copy_size = bufsize - read < Flash::Page::SIZE - rx.image.written size_t copy_size = bufsize - read < Flash::Page::SIZE - rx.image.written
? bufsize - read ? bufsize - read

151
src/manamenu.cpp Normal file
View File

@ -0,0 +1,151 @@
#include "icons.h"
#include "menu.h"
#include "pixelstream.h"
#include <cmath>
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() {}