This document outlines the process for porting old hackathon branches to the new card architecture in DeskHog.
The main branch introduced a new extensible card system that allows dynamic card management through a web UI. Old branches need to be updated to use this architecture.
The new system uses:
- CardType enum - Defines available card types
- CardDefinition - Metadata for each card type
- Factory pattern - Lambda functions create card instances
- InputHandler interface - Base class for cards that handle input
- Dynamic card management - Cards can be added/removed via web UI
For the Flappy Hog example:
- Original:
FlappyBirdGameclass with direct integration in main.cpp - Had custom button handling and game loop in the LVGL task
- Direct references in CardController
Create a wrapper that implements InputHandler:
class FlappyHogCard : public InputHandler {
public:
FlappyHogCard(lv_obj_t* parent);
~FlappyHogCard();
lv_obj_t* getCard();
bool handleButtonPress(uint8_t button_index) override;
void prepareForRemoval() override;
bool update() override; // For game loop
private:
FlappyBirdGame* game;
lv_obj_t* cardContainer;
bool markedForRemoval;
};In src/config/CardConfig.h:
- Add new card type to enum
- Update
cardTypeToString()function - Update
stringToCardType()function
In CardController::initializeCardTypes():
CardDefinition flappyDef;
flappyDef.type = CardType::FLAPPY_HOG;
flappyDef.name = "Flappy Hog";
flappyDef.allowMultiple = false;
flappyDef.needsConfigInput = false;
flappyDef.uiDescription = "One button. Endless frustration. Infinite glory.";
flappyDef.factory = [this](const String& configValue) -> lv_obj_t* {
FlappyHogCard* newCard = new FlappyHogCard(screen);
if (newCard && newCard->getCard()) {
CardInstance instance{newCard, newCard->getCard()};
dynamicCards[CardType::FLAPPY_HOG].push_back(instance);
cardStack->registerInputHandler(newCard->getCard(), newCard);
return newCard->getCard();
}
delete newCard;
return nullptr;
};
registerCardType(flappyDef);For cards needing regular updates:
- Extend
InputHandlerinterface withvirtual bool update() { return false; } - Add
updateActiveCard()toCardNavigationStack - Call it from
CardController::processUIQueue()
This keeps the game loop encapsulated and running at the same frequency as UI updates.
Clean up:
- Direct references in main.cpp
- Card-specific methods in CardController
- Manual card creation in initialization
Verify:
- Card appears in web UI
- Can be added/removed dynamically
- Game functions properly
- No memory leaks or crashes
- Proper cleanup on removal
- Threading issues - Keep all UI updates on the LVGL thread
- Memory management - Use
prepareForRemoval()to prevent double-deletion - Button handling - Return false from
handleButtonPress()to allow navigation - Update frequency - Game updates run at UI refresh rate (~60 FPS)
- Extensible - Easy to add new card types
- Dynamic - Users control which cards are shown
- Encapsulated - No card-specific code in core systems
- Consistent - All cards follow the same pattern
- Thread-safe - Updates happen through the UI queue
- Commit your changes on the feature branch
- Merge main into your branch (expect conflicts)
- Resolve conflicts keeping the new architecture
- Test thoroughly before creating PR
The key is to wrap the existing functionality in the new card system rather than trying to preserve the old integration approach. Do not accept changes that alter the architecture of the project as a whole.