initial game template
This commit is contained in:
72
README.md
72
README.md
@@ -1,9 +1,10 @@
|
|||||||
# RP2350 TFT Display with Touch and SD Card Demo
|
# RP2350 TFT Display with Touch and SD Card Demo
|
||||||
|
|
||||||
A modular embedded application for RP2350 microcontrollers featuring display, touch, and SD card support with hardware abstraction layers.
|
A modular embedded application for RP2350 microcontrollers featuring display, touch, and SD card support with hardware abstraction layers. **Now includes a reactive game template architecture!**
|
||||||
|
|
||||||
## Features
|
## Features
|
||||||
|
|
||||||
|
- **Reactive Game Template** - Event-driven architecture optimized for e-ink displays and power efficiency
|
||||||
- **Display Abstraction Layer** - Support for multiple display types (ST7796, ST7789, E-Paper)
|
- **Display Abstraction Layer** - Support for multiple display types (ST7796, ST7789, E-Paper)
|
||||||
- **Touch Abstraction Layer** - Extensible touch controller support (FT6336U)
|
- **Touch Abstraction Layer** - Extensible touch controller support (FT6336U)
|
||||||
- **SD Card with FatFS** - File system support with board-aware initialization
|
- **SD Card with FatFS** - File system support with board-aware initialization
|
||||||
@@ -12,6 +13,75 @@ A modular embedded application for RP2350 microcontrollers featuring display, to
|
|||||||
- **Automated Build Scripts** - Build for one board or all boards with single commands
|
- **Automated Build Scripts** - Build for one board or all boards with single commands
|
||||||
- **Hardware Abstraction** - Factory pattern for displays and touch controllers
|
- **Hardware Abstraction** - Factory pattern for displays and touch controllers
|
||||||
|
|
||||||
|
## 🎮 Reactive Game Template
|
||||||
|
|
||||||
|
The project now uses a **clean, event-driven architecture** perfect for building games and interactive applications:
|
||||||
|
|
||||||
|
### Key Features
|
||||||
|
- ⚡ **Event-Driven**: Display only updates when input is received
|
||||||
|
- 🔋 **Power Efficient**: Uses `__wfi()` to sleep between inputs (< 1mA idle)
|
||||||
|
- 📄 **E-ink Optimized**: Minimizes screen refreshes for e-paper displays
|
||||||
|
- 🎯 **Interrupt-Driven**: Touch and button handling via hardware interrupts
|
||||||
|
- 🧩 **Modular**: Clear separation of input processing, game logic, and rendering
|
||||||
|
|
||||||
|
### Architecture Highlights
|
||||||
|
|
||||||
|
```
|
||||||
|
Interrupt → Set Flag → Wake CPU → Process Input → Update Game → Draw → Refresh → Sleep
|
||||||
|
```
|
||||||
|
|
||||||
|
**Before (Polling Loop):**
|
||||||
|
```cpp
|
||||||
|
while(1) {
|
||||||
|
if (touch_interrupt_flag) {
|
||||||
|
// Read touch data
|
||||||
|
// Process coordinates
|
||||||
|
// Draw directly
|
||||||
|
// Handle gestures inline
|
||||||
|
refresh_screen();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**After (Reactive Template):**
|
||||||
|
```cpp
|
||||||
|
while(1) {
|
||||||
|
__wfi(); // Sleep until interrupt
|
||||||
|
|
||||||
|
InputEvent input = process_button_input(config);
|
||||||
|
if (!input.valid) {
|
||||||
|
input = process_touch_input(config, &last_touch_time);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (input.valid && game_update(&game_state, input, config, &renderer)) {
|
||||||
|
game_draw(&game_state, &renderer, &gui);
|
||||||
|
refresh_screen(bit_buffer, display);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Creating Your Own Game
|
||||||
|
|
||||||
|
1. **Modify GameState** - Define your game variables
|
||||||
|
2. **Implement game_init()** - Set initial values
|
||||||
|
3. **Implement game_update()** - Handle input and update state
|
||||||
|
4. **Implement game_draw()** - Render your game graphics
|
||||||
|
|
||||||
|
The reactive loop and input system work automatically!
|
||||||
|
|
||||||
|
**📖 [Read the Full Template Usage Guide](TEMPLATE_USAGE.md)** for detailed examples and patterns.
|
||||||
|
|
||||||
|
### Example Game (Included)
|
||||||
|
|
||||||
|
The template includes a **Button Navigation Game** demonstrating:
|
||||||
|
- Hardware button input handling (KEY0 switches focus, KEY1 clicks)
|
||||||
|
- GUI component usage (buttons, radio buttons, status bars)
|
||||||
|
- State management (click counters, focus tracking)
|
||||||
|
- Visual feedback (filled buttons show focus)
|
||||||
|
- E-ink optimized refreshes
|
||||||
|
|
||||||
|
Perfect starting point for your own game!
|
||||||
|
|
||||||
## Supported Hardware Configurations
|
## Supported Hardware Configurations
|
||||||
|
|
||||||
### Available Board Configurations
|
### Available Board Configurations
|
||||||
|
|||||||
@@ -1,438 +0,0 @@
|
|||||||
# Reactive Game Template Refactoring Plan
|
|
||||||
|
|
||||||
**Goal**: Transform `basic1.cpp` into a clean, event-driven reactive game template where:
|
|
||||||
- Display only updates when input is received (ideal for e-ink displays)
|
|
||||||
- All drawing happens inside dedicated functions
|
|
||||||
- Game state is separate from input handling
|
|
||||||
- Clear structure for developers to customize game logic
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Phase 1: Define Core Structures and Types
|
|
||||||
|
|
||||||
### ☐ Step 1.1: Create InputEvent structure
|
|
||||||
**Location**: After includes, before interrupt handlers
|
|
||||||
**Action**: Define a unified `InputEvent` structure to normalize all input types (touch, button, gesture)
|
|
||||||
```cpp
|
|
||||||
// Input event types
|
|
||||||
enum InputType {
|
|
||||||
INPUT_NONE = 0,
|
|
||||||
INPUT_TOUCH_DOWN,
|
|
||||||
INPUT_TOUCH_MOVE,
|
|
||||||
INPUT_TOUCH_UP,
|
|
||||||
INPUT_BUTTON_0,
|
|
||||||
INPUT_BUTTON_1,
|
|
||||||
INPUT_GESTURE
|
|
||||||
};
|
|
||||||
|
|
||||||
// Unified input event structure
|
|
||||||
struct InputEvent {
|
|
||||||
InputType type;
|
|
||||||
int16_t x;
|
|
||||||
int16_t y;
|
|
||||||
uint8_t gesture_code; // For gesture events
|
|
||||||
uint8_t button_id; // For button events
|
|
||||||
uint8_t pressure; // Touch pressure/weight
|
|
||||||
bool valid; // Set to true if event is valid
|
|
||||||
};
|
|
||||||
```
|
|
||||||
|
|
||||||
### ☐ Step 1.2: Create GameState structure
|
|
||||||
**Location**: After InputEvent definition
|
|
||||||
**Action**: Define a `GameState` structure to hold all game-specific data (example for drawing game)
|
|
||||||
```cpp
|
|
||||||
// Game state - customize this for your game
|
|
||||||
struct GameState {
|
|
||||||
// Drawing game state
|
|
||||||
int16_t last_x;
|
|
||||||
int16_t last_y;
|
|
||||||
bool is_drawing;
|
|
||||||
|
|
||||||
// General game state
|
|
||||||
uint32_t score;
|
|
||||||
bool game_over;
|
|
||||||
uint32_t frame_count;
|
|
||||||
|
|
||||||
// Statistics
|
|
||||||
uint32_t touch_success_count;
|
|
||||||
uint32_t touch_fail_count;
|
|
||||||
};
|
|
||||||
```
|
|
||||||
|
|
||||||
### ☐ Step 1.3: Create GameConfig structure
|
|
||||||
**Location**: After GameState definition
|
|
||||||
**Action**: Define configurable parameters for the game
|
|
||||||
```cpp
|
|
||||||
// Game configuration - adjust these for your game
|
|
||||||
struct GameConfig {
|
|
||||||
uint32_t touch_debounce_ms; // Touch polling rate
|
|
||||||
uint32_t button_debounce_ms; // Button debounce delay
|
|
||||||
bool enable_gestures; // Enable gesture recognition
|
|
||||||
bool enable_continuous_draw; // Allow continuous drawing while touched
|
|
||||||
bool debug_verbose; // Print debug messages
|
|
||||||
};
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Phase 2: Refactor Input System
|
|
||||||
|
|
||||||
### ☐ Step 2.1: Simplify touch interrupt handler
|
|
||||||
**Location**: `touch_interrupt_handler()` function (lines ~52-83)
|
|
||||||
**Action**: Remove all processing logic, keep only flag setting
|
|
||||||
- Remove `touch->read_touch()` call from ISR
|
|
||||||
- Remove printf statements from ISR
|
|
||||||
- Keep only `touch_interrupt_flag` and `touch_event_down` flag setting
|
|
||||||
|
|
||||||
### ☐ Step 2.2: Simplify button interrupt handler
|
|
||||||
**Location**: `button_interrupt_handler()` function (lines ~98-113)
|
|
||||||
**Action**: Remove printf from ISR, keep only flag setting
|
|
||||||
- Remove `printf()` calls from interrupt handler
|
|
||||||
- Keep only flag setting logic
|
|
||||||
|
|
||||||
### ☐ Step 2.3: Create process_touch_input() function
|
|
||||||
**Location**: After interrupt handlers, before refresh_screen()
|
|
||||||
**Action**: Create dedicated function to process touch events
|
|
||||||
```cpp
|
|
||||||
/**
|
|
||||||
* @brief Process touch input and convert to InputEvent
|
|
||||||
*
|
|
||||||
* Reads touch data from controller and creates appropriate InputEvent.
|
|
||||||
* Handles debouncing and filtering internally.
|
|
||||||
*
|
|
||||||
* @param config Game configuration
|
|
||||||
* @param last_time Pointer to last touch time for debouncing
|
|
||||||
* @return InputEvent structure (valid=false if no valid input)
|
|
||||||
*/
|
|
||||||
InputEvent process_touch_input(const GameConfig& config, uint32_t* last_time);
|
|
||||||
```
|
|
||||||
|
|
||||||
### ☐ Step 2.4: Create process_button_input() function
|
|
||||||
**Location**: After process_touch_input()
|
|
||||||
**Action**: Create dedicated function to process button events
|
|
||||||
```cpp
|
|
||||||
/**
|
|
||||||
* @brief Process button input and convert to InputEvent
|
|
||||||
*
|
|
||||||
* Checks button flags and verifies button state with debouncing.
|
|
||||||
* Clears flags after processing.
|
|
||||||
*
|
|
||||||
* @param config Game configuration
|
|
||||||
* @return InputEvent structure (valid=false if no valid input)
|
|
||||||
*/
|
|
||||||
InputEvent process_button_input(const GameConfig& config);
|
|
||||||
```
|
|
||||||
|
|
||||||
### ☐ Step 2.5: Create get_gesture_name() helper function
|
|
||||||
**Location**: After process_button_input()
|
|
||||||
**Action**: Extract gesture name lookup into helper function
|
|
||||||
```cpp
|
|
||||||
/**
|
|
||||||
* @brief Get human-readable gesture name
|
|
||||||
*
|
|
||||||
* @param gesture_code Gesture code from touch controller
|
|
||||||
* @return Constant string with gesture name
|
|
||||||
*/
|
|
||||||
const char* get_gesture_name(uint8_t gesture_code);
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Phase 3: Create Game Logic Functions
|
|
||||||
|
|
||||||
### ☐ Step 3.1: Create game_init() function
|
|
||||||
**Location**: After input processing functions
|
|
||||||
**Action**: Initialize game state
|
|
||||||
```cpp
|
|
||||||
/**
|
|
||||||
* @brief Initialize game state
|
|
||||||
*
|
|
||||||
* Called once at startup to set initial game values.
|
|
||||||
* Customize this for your game.
|
|
||||||
*
|
|
||||||
* @param state Pointer to GameState to initialize
|
|
||||||
*/
|
|
||||||
void game_init(GameState* state);
|
|
||||||
```
|
|
||||||
|
|
||||||
### ☐ Step 3.2: Create game_update() function
|
|
||||||
**Location**: After game_init()
|
|
||||||
**Action**: Update game state based on input
|
|
||||||
```cpp
|
|
||||||
/**
|
|
||||||
* @brief Update game state based on input event
|
|
||||||
*
|
|
||||||
* This is where your game logic goes.
|
|
||||||
* Called whenever an input event occurs.
|
|
||||||
*
|
|
||||||
* @param state Pointer to GameState to update
|
|
||||||
* @param input Input event to process
|
|
||||||
* @param config Game configuration
|
|
||||||
* @return true if screen needs refresh (drawing occurred)
|
|
||||||
*/
|
|
||||||
bool game_update(GameState* state, const InputEvent& input, const GameConfig& config);
|
|
||||||
```
|
|
||||||
|
|
||||||
### ☐ Step 3.3: Create game_draw() function
|
|
||||||
**Location**: After game_update()
|
|
||||||
**Action**: Draw game visuals based on state
|
|
||||||
```cpp
|
|
||||||
/**
|
|
||||||
* @brief Draw game graphics to screen buffer
|
|
||||||
*
|
|
||||||
* All drawing operations go here.
|
|
||||||
* Called only when game_update() returns true.
|
|
||||||
*
|
|
||||||
* @param state Pointer to current GameState
|
|
||||||
* @param renderer Renderer for drawing primitives
|
|
||||||
* @param gui GUI system for widgets (optional)
|
|
||||||
*/
|
|
||||||
void game_draw(const GameState* state, LowLevelRenderer* renderer, LowLevelGUI* gui);
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Phase 4: Refactor Main Function
|
|
||||||
|
|
||||||
### ☐ Step 4.1: Move global variables into main()
|
|
||||||
**Location**: main() function, beginning
|
|
||||||
**Action**: Move state variables into main as local variables
|
|
||||||
- Convert `last_x`, `last_y`, `was_touched` into GameState
|
|
||||||
- Convert `touch_fail_count`, `touch_success_count` into GameState
|
|
||||||
- Keep only interrupt flags as globals
|
|
||||||
|
|
||||||
### ☐ Step 4.2: Create and initialize GameConfig
|
|
||||||
**Location**: main() function, after display init
|
|
||||||
**Action**: Create GameConfig instance with sensible defaults
|
|
||||||
```cpp
|
|
||||||
GameConfig config = {
|
|
||||||
.touch_debounce_ms = 10,
|
|
||||||
.button_debounce_ms = 50,
|
|
||||||
.enable_gestures = true,
|
|
||||||
.enable_continuous_draw = true,
|
|
||||||
.debug_verbose = false
|
|
||||||
};
|
|
||||||
```
|
|
||||||
|
|
||||||
### ☐ Step 4.3: Create and initialize GameState
|
|
||||||
**Location**: main() function, after GameConfig
|
|
||||||
**Action**: Create GameState and call game_init()
|
|
||||||
```cpp
|
|
||||||
GameState game_state;
|
|
||||||
game_init(&game_state);
|
|
||||||
```
|
|
||||||
|
|
||||||
### ☐ Step 4.4: Call game_draw() for initial screen
|
|
||||||
**Location**: main() function, replace GUI setup code
|
|
||||||
**Action**: Replace hardcoded GUI drawing with game_draw() call
|
|
||||||
- Remove lines creating windows, status bars, gauges
|
|
||||||
- Call `game_draw(&game_state, &renderer, &gui);`
|
|
||||||
- Keep initial refresh_screen() call
|
|
||||||
|
|
||||||
### ☐ Step 4.5: Refactor main loop with reactive pattern
|
|
||||||
**Location**: main() while loop (lines ~321-423)
|
|
||||||
**Action**: Replace polling logic with clean reactive pattern
|
|
||||||
```cpp
|
|
||||||
// Reactive game loop pattern:
|
|
||||||
while (1) {
|
|
||||||
__wfi(); // Sleep until interrupt
|
|
||||||
|
|
||||||
InputEvent input = {0};
|
|
||||||
bool needs_refresh = false;
|
|
||||||
|
|
||||||
// 1. Process button input first (higher priority)
|
|
||||||
input = process_button_input(config);
|
|
||||||
if (input.valid) {
|
|
||||||
needs_refresh = game_update(&game_state, input, config);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 2. Process touch input
|
|
||||||
if (!input.valid) {
|
|
||||||
input = process_touch_input(config, &last_touch_time);
|
|
||||||
if (input.valid) {
|
|
||||||
needs_refresh = game_update(&game_state, input, config);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 3. Draw and refresh only if needed
|
|
||||||
if (needs_refresh) {
|
|
||||||
game_draw(&game_state, &renderer, &gui);
|
|
||||||
refresh_screen(bit_buffer, display);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Phase 5: Implement Function Bodies
|
|
||||||
|
|
||||||
### ☐ Step 5.1: Implement process_touch_input()
|
|
||||||
**Action**: Move touch reading logic from main loop into this function
|
|
||||||
- Check `touch_interrupt_flag`, return invalid event if not set
|
|
||||||
- Implement debouncing using `last_time` parameter
|
|
||||||
- Read `TouchData` from touch controller
|
|
||||||
- Convert to `InputEvent` structure
|
|
||||||
- Handle `touch_event_down` flag for TOUCH_DOWN vs TOUCH_MOVE vs TOUCH_UP
|
|
||||||
- Return populated InputEvent
|
|
||||||
|
|
||||||
### ☐ Step 5.2: Implement process_button_input()
|
|
||||||
**Action**: Move button handling logic from main loop into this function
|
|
||||||
- Check button flags (`button_key0_pressed`, `button_key1_pressed`)
|
|
||||||
- Implement debouncing with `sleep_ms()` and `gpio_get()` verification
|
|
||||||
- Clear flags after processing
|
|
||||||
- Return InputEvent with button_id set
|
|
||||||
|
|
||||||
### ☐ Step 5.3: Implement get_gesture_name()
|
|
||||||
**Action**: Move gesture switch statement into this function
|
|
||||||
- Simple switch on gesture_code
|
|
||||||
- Return const char* string
|
|
||||||
|
|
||||||
### ☐ Step 5.4: Implement game_init()
|
|
||||||
**Action**: Initialize GameState fields
|
|
||||||
```cpp
|
|
||||||
void game_init(GameState* state) {
|
|
||||||
state->last_x = -1;
|
|
||||||
state->last_y = -1;
|
|
||||||
state->is_drawing = false;
|
|
||||||
state->score = 0;
|
|
||||||
state->game_over = false;
|
|
||||||
state->frame_count = 0;
|
|
||||||
state->touch_success_count = 0;
|
|
||||||
state->touch_fail_count = 0;
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### ☐ Step 5.5: Implement game_update()
|
|
||||||
**Action**: Move game logic (drawing lines) into this function
|
|
||||||
- Handle different InputEvent types
|
|
||||||
- Update GameState based on input
|
|
||||||
- Return true if drawing occurred (needs refresh)
|
|
||||||
- Example: For drawing game, draw line from last_x/y to current x/y
|
|
||||||
|
|
||||||
### ☐ Step 5.6: Implement game_draw()
|
|
||||||
**Action**: Create initial UI or game graphics
|
|
||||||
- Can draw GUI elements (windows, gauges, etc.)
|
|
||||||
- Or draw game-specific graphics
|
|
||||||
- For drawing game, no additional drawing needed (done in game_update)
|
|
||||||
- Can add score display, game over screen, etc.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Phase 6: Documentation and Comments
|
|
||||||
|
|
||||||
### ☐ Step 6.1: Update file header comment
|
|
||||||
**Action**: Change description to reflect template nature
|
|
||||||
- Update title to "Reactive Game Template"
|
|
||||||
- Add description of architecture
|
|
||||||
- Add usage instructions
|
|
||||||
|
|
||||||
### ☐ Step 6.2: Add section separator comments
|
|
||||||
**Action**: Add clear comment blocks separating each section
|
|
||||||
```cpp
|
|
||||||
// ============================================================================
|
|
||||||
// INPUT EVENT STRUCTURES
|
|
||||||
// ============================================================================
|
|
||||||
|
|
||||||
// ============================================================================
|
|
||||||
// GAME STATE AND CONFIGURATION
|
|
||||||
// ============================================================================
|
|
||||||
|
|
||||||
// ============================================================================
|
|
||||||
// INTERRUPT HANDLERS (Keep these minimal!)
|
|
||||||
// ============================================================================
|
|
||||||
|
|
||||||
// ============================================================================
|
|
||||||
// INPUT PROCESSING
|
|
||||||
// ============================================================================
|
|
||||||
|
|
||||||
// ============================================================================
|
|
||||||
// GAME LOGIC (Customize this section for your game!)
|
|
||||||
// ============================================================================
|
|
||||||
|
|
||||||
// ============================================================================
|
|
||||||
// MAIN PROGRAM
|
|
||||||
// ============================================================================
|
|
||||||
```
|
|
||||||
|
|
||||||
### ☐ Step 6.3: Add customization guide comments
|
|
||||||
**Action**: Add comments explaining what to modify for new games
|
|
||||||
```cpp
|
|
||||||
// ============================================================================
|
|
||||||
// HOW TO CREATE YOUR OWN GAME:
|
|
||||||
// ============================================================================
|
|
||||||
// 1. Modify GameState structure with your game variables
|
|
||||||
// 2. Implement game_init() to set initial values
|
|
||||||
// 3. Implement game_update() to handle input and update state
|
|
||||||
// 4. Implement game_draw() to render your game graphics
|
|
||||||
// 5. Adjust GameConfig for your game's needs
|
|
||||||
// 6. The reactive loop and input system work automatically!
|
|
||||||
// ============================================================================
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Phase 7: Testing and Validation
|
|
||||||
|
|
||||||
### ☐ Step 7.1: Verify compilation
|
|
||||||
**Action**: Build the project and fix any compilation errors
|
|
||||||
- Run `./build_and_flash.sh`
|
|
||||||
- Fix any syntax errors or missing definitions
|
|
||||||
|
|
||||||
### ☐ Step 7.2: Test on hardware (if available)
|
|
||||||
**Action**: Flash to device and test input/drawing
|
|
||||||
- Verify touch input works
|
|
||||||
- Verify buttons work (on e-ink board)
|
|
||||||
- Verify display only updates on input
|
|
||||||
|
|
||||||
### ☐ Step 7.3: Create example game variations
|
|
||||||
**Action**: Create commented-out example implementations in the file
|
|
||||||
- Add example game_update() for different game types
|
|
||||||
- Add example game_draw() for different visualizations
|
|
||||||
- Keep default drawing game as primary example
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Phase 8: Create Template Documentation
|
|
||||||
|
|
||||||
### ☐ Step 8.1: Create TEMPLATE_USAGE.md
|
|
||||||
**Action**: Write comprehensive guide for using the template
|
|
||||||
- Architecture overview
|
|
||||||
- How to customize for different games
|
|
||||||
- Example game ideas (snake, pong, tic-tac-toe, etc.)
|
|
||||||
- Input handling best practices
|
|
||||||
- E-ink optimization tips
|
|
||||||
|
|
||||||
### ☐ Step 8.2: Update README.md
|
|
||||||
**Action**: Add section about the reactive game template
|
|
||||||
- Link to TEMPLATE_USAGE.md
|
|
||||||
- Highlight key features
|
|
||||||
- Show before/after code structure
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Completion Checklist
|
|
||||||
|
|
||||||
- [ ] All Phase 1 steps completed (Core Structures)
|
|
||||||
- [ ] All Phase 2 steps completed (Input System)
|
|
||||||
- [ ] All Phase 3 steps completed (Game Logic)
|
|
||||||
- [ ] All Phase 4 steps completed (Main Function)
|
|
||||||
- [ ] All Phase 5 steps completed (Function Bodies)
|
|
||||||
- [ ] All Phase 6 steps completed (Documentation)
|
|
||||||
- [ ] All Phase 7 steps completed (Testing)
|
|
||||||
- [ ] All Phase 8 steps completed (Template Documentation)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Notes for Implementation
|
|
||||||
|
|
||||||
**Work one step at a time**: Each checkbox should be completed and verified before moving to the next.
|
|
||||||
|
|
||||||
**Test incrementally**: After each phase, verify the code still compiles.
|
|
||||||
|
|
||||||
**Keep it simple**: The template should be easy to understand and modify.
|
|
||||||
|
|
||||||
**Optimize for e-ink**: Minimize refreshes, only redraw on input.
|
|
||||||
|
|
||||||
**Power efficiency**: Use `__wfi()` to sleep between inputs.
|
|
||||||
|
|
||||||
**Flexibility**: Template should work on all board types (TFT with touch, e-ink with buttons, etc.)
|
|
||||||
543
TEMPLATE_USAGE.md
Normal file
543
TEMPLATE_USAGE.md
Normal file
@@ -0,0 +1,543 @@
|
|||||||
|
# Reactive Game Template - Usage Guide
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
This template provides a clean, event-driven architecture for building games and interactive applications on Raspberry Pi Pico (RP2350) with displays. It's designed to be efficient, power-conscious, and optimized for e-ink displays.
|
||||||
|
|
||||||
|
## Architecture
|
||||||
|
|
||||||
|
### Event-Driven Design
|
||||||
|
|
||||||
|
```
|
||||||
|
Interrupt (Touch/Button) → Set Flag → Main Loop Wakes → Process Input → Update Game State → Refresh Display → Sleep
|
||||||
|
```
|
||||||
|
|
||||||
|
The template follows a reactive pattern:
|
||||||
|
1. **Sleep**: CPU waits for interrupts using `__wfi()` (very power efficient)
|
||||||
|
2. **Wake**: Interrupt handler sets a flag and wakes CPU
|
||||||
|
3. **Process**: Main loop processes the input event
|
||||||
|
4. **Update**: Game logic updates state based on input
|
||||||
|
5. **Render**: Display refreshes only if needed
|
||||||
|
6. **Repeat**: Back to sleep
|
||||||
|
|
||||||
|
### Key Components
|
||||||
|
|
||||||
|
#### 1. Input Event System
|
||||||
|
- **InputEvent Structure**: Unified representation of all inputs
|
||||||
|
- **InputType Enum**: Touch (down/move/up), buttons, gestures
|
||||||
|
- **Interrupt Handlers**: Minimal ISRs that only set flags
|
||||||
|
- **Processing Functions**: `process_touch_input()` and `process_button_input()`
|
||||||
|
|
||||||
|
#### 2. Game State Management
|
||||||
|
- **GameState Structure**: All game-specific data in one place
|
||||||
|
- **GameConfig Structure**: Configurable parameters (debounce, features, debug)
|
||||||
|
- **No Global Variables**: State is passed to functions explicitly
|
||||||
|
|
||||||
|
#### 3. Game Logic Functions
|
||||||
|
- **game_init()**: Initialize game state at startup
|
||||||
|
- **game_update()**: Handle input events and update state
|
||||||
|
- **game_draw()**: Render UI and game graphics
|
||||||
|
|
||||||
|
## Creating Your Own Game
|
||||||
|
|
||||||
|
### Step 1: Define Your Game State
|
||||||
|
|
||||||
|
Modify the `GameState` structure to hold your game-specific data:
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
struct GameState {
|
||||||
|
// Example for a Snake game
|
||||||
|
int snake_x[100];
|
||||||
|
int snake_y[100];
|
||||||
|
int snake_length;
|
||||||
|
int food_x;
|
||||||
|
int food_y;
|
||||||
|
int direction;
|
||||||
|
uint32_t score;
|
||||||
|
bool game_over;
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
### Step 2: Initialize Your Game
|
||||||
|
|
||||||
|
Implement `game_init()` to set initial values:
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
void game_init(GameState* state) {
|
||||||
|
// Snake starts in center
|
||||||
|
state->snake_x[0] = V_WIDTH / 2;
|
||||||
|
state->snake_y[0] = V_HEIGHT / 2;
|
||||||
|
state->snake_length = 3;
|
||||||
|
state->direction = 0; // Right
|
||||||
|
state->score = 0;
|
||||||
|
state->game_over = false;
|
||||||
|
// Place first food
|
||||||
|
place_random_food(state);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Step 3: Handle Input
|
||||||
|
|
||||||
|
Implement `game_update()` to respond to input events:
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
bool game_update(GameState* state, const InputEvent& input, const GameConfig& config, LowLevelRenderer* renderer) {
|
||||||
|
bool needs_refresh = false;
|
||||||
|
|
||||||
|
switch (input.type) {
|
||||||
|
case INPUT_BUTTON_0:
|
||||||
|
// Turn left
|
||||||
|
state->direction = (state->direction + 3) % 4;
|
||||||
|
needs_refresh = true;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case INPUT_BUTTON_1:
|
||||||
|
// Turn right
|
||||||
|
state->direction = (state->direction + 1) % 4;
|
||||||
|
needs_refresh = true;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case INPUT_TOUCH_DOWN:
|
||||||
|
// Touch to restart if game over
|
||||||
|
if (state->game_over) {
|
||||||
|
game_init(state);
|
||||||
|
needs_refresh = true;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update snake position
|
||||||
|
update_snake_position(state);
|
||||||
|
check_collisions(state);
|
||||||
|
|
||||||
|
return needs_refresh;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Step 4: Draw Your Game
|
||||||
|
|
||||||
|
Implement `game_draw()` to render graphics:
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
void game_draw(const GameState* state, LowLevelRenderer* renderer, LowLevelGUI* gui) {
|
||||||
|
// Draw game board
|
||||||
|
LowLevelWindow *w1 = gui->draw_new_window(10, 10, V_WIDTH - 20, V_HEIGHT - 20, "Snake Game");
|
||||||
|
|
||||||
|
// Draw snake
|
||||||
|
for (int i = 0; i < state->snake_length; i++) {
|
||||||
|
renderer->draw_filled_rectangle(state->snake_x[i], state->snake_y[i], 10, 10, true, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Draw food
|
||||||
|
renderer->draw_filled_circle(state->food_x, state->food_y, 5, true);
|
||||||
|
|
||||||
|
// Draw score
|
||||||
|
char score_text[20];
|
||||||
|
snprintf(score_text, sizeof(score_text), "Score: %d", state->score);
|
||||||
|
renderer->draw_string(20, 30, score_text, true);
|
||||||
|
|
||||||
|
// Game over message
|
||||||
|
if (state->game_over) {
|
||||||
|
renderer->draw_string(V_WIDTH/2 - 40, V_HEIGHT/2, "GAME OVER", true);
|
||||||
|
renderer->draw_string(V_WIDTH/2 - 50, V_HEIGHT/2 + 20, "Touch to restart", true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Step 5: Adjust Configuration
|
||||||
|
|
||||||
|
Modify `GameConfig` in main() for your game's needs:
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
GameConfig config = {
|
||||||
|
.touch_debounce_ms = 50, // Slower for menu navigation
|
||||||
|
.button_debounce_ms = 100, // Longer for game controls
|
||||||
|
.enable_gestures = false, // Not needed for snake
|
||||||
|
.enable_continuous_draw = false,
|
||||||
|
.debug_verbose = true // Enable during development
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
## Example Game Ideas
|
||||||
|
|
||||||
|
### 1. Tic-Tac-Toe
|
||||||
|
- **Input**: Touch to place X/O, buttons to switch players
|
||||||
|
- **State**: 3x3 grid, current player, win condition
|
||||||
|
- **Drawing**: Grid lines, X and O symbols
|
||||||
|
- **Ideal for**: E-ink displays (few updates)
|
||||||
|
|
||||||
|
### 2. Pong
|
||||||
|
- **Input**: Buttons to move paddle up/down
|
||||||
|
- **State**: Paddle positions, ball position/velocity, score
|
||||||
|
- **Drawing**: Paddles, ball, center line, score
|
||||||
|
- **Note**: May need timer-based updates for ball movement
|
||||||
|
|
||||||
|
### 3. Memory Card Game
|
||||||
|
- **Input**: Touch cards to flip, buttons to navigate
|
||||||
|
- **State**: Card positions, flipped states, matches found
|
||||||
|
- **Drawing**: Card grid, symbols when flipped
|
||||||
|
- **Ideal for**: E-ink displays (turn-based)
|
||||||
|
|
||||||
|
### 4. Calculator
|
||||||
|
- **Input**: Touch for number buttons, physical buttons for operations
|
||||||
|
- **State**: Current value, operation mode, history
|
||||||
|
- **Drawing**: Display, button grid
|
||||||
|
- **Perfect for**: E-ink displays (minimal updates)
|
||||||
|
|
||||||
|
### 5. Drawing Board (Current Example)
|
||||||
|
- **Input**: Touch to draw, buttons to clear/undo
|
||||||
|
- **State**: Last position, stroke history
|
||||||
|
- **Drawing**: Lines following touch movement
|
||||||
|
- **Ideal for**: TFT displays (frequent updates)
|
||||||
|
|
||||||
|
## Input Handling Best Practices
|
||||||
|
|
||||||
|
### Touch Input
|
||||||
|
```cpp
|
||||||
|
case INPUT_TOUCH_DOWN:
|
||||||
|
// First touch - capture position
|
||||||
|
state->start_x = input.x;
|
||||||
|
state->start_y = input.y;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case INPUT_TOUCH_MOVE:
|
||||||
|
// Continuous drawing/dragging
|
||||||
|
if (config.enable_continuous_draw) {
|
||||||
|
draw_line(state->last_x, state->last_y, input.x, input.y);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case INPUT_TOUCH_UP:
|
||||||
|
// Touch released - finalize action
|
||||||
|
calculate_gesture(state->start_x, state->start_y, input.x, input.y);
|
||||||
|
break;
|
||||||
|
```
|
||||||
|
|
||||||
|
### Button Input
|
||||||
|
```cpp
|
||||||
|
case INPUT_BUTTON_0:
|
||||||
|
// First button - navigation/cancel
|
||||||
|
navigate_menu_prev(state);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case INPUT_BUTTON_1:
|
||||||
|
// Second button - selection/confirm
|
||||||
|
select_menu_item(state);
|
||||||
|
break;
|
||||||
|
```
|
||||||
|
|
||||||
|
### Gesture Input
|
||||||
|
```cpp
|
||||||
|
case INPUT_GESTURE:
|
||||||
|
switch(input.gesture_code) {
|
||||||
|
case 0x10: // Move Up
|
||||||
|
scroll_up(state);
|
||||||
|
break;
|
||||||
|
case 0x18: // Move Down
|
||||||
|
scroll_down(state);
|
||||||
|
break;
|
||||||
|
case 0x48: // Zoom In
|
||||||
|
increase_scale(state);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
```
|
||||||
|
|
||||||
|
## E-Ink Display Optimization
|
||||||
|
|
||||||
|
### Minimize Refreshes
|
||||||
|
- Only return `true` from `game_update()` when display actually changes
|
||||||
|
- Batch updates: collect multiple changes before refreshing
|
||||||
|
- Use partial refresh when available
|
||||||
|
|
||||||
|
### Visual Design Tips
|
||||||
|
1. **High Contrast**: Use solid blacks and whites
|
||||||
|
2. **Clear Shapes**: Avoid thin lines (use 2px minimum)
|
||||||
|
3. **Large Text**: Use readable fonts (5x5 or larger)
|
||||||
|
4. **Simple Graphics**: Minimize complex patterns
|
||||||
|
5. **Static Elements**: Redraw only changed areas when possible
|
||||||
|
|
||||||
|
### Example Pattern
|
||||||
|
```cpp
|
||||||
|
bool game_update(GameState* state, const InputEvent& input, const GameConfig& config, LowLevelRenderer* renderer) {
|
||||||
|
bool needs_refresh = false;
|
||||||
|
|
||||||
|
// Collect all changes
|
||||||
|
if (input.type == INPUT_BUTTON_0) {
|
||||||
|
state->menu_index++;
|
||||||
|
needs_refresh = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Only redraw if something changed
|
||||||
|
return needs_refresh;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Power Efficiency
|
||||||
|
|
||||||
|
### Sleep Between Events
|
||||||
|
The template uses `__wfi()` to put CPU to sleep:
|
||||||
|
```cpp
|
||||||
|
while (1) {
|
||||||
|
__wfi(); // CPU sleeps here until interrupt
|
||||||
|
// Process input...
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Reduce Polling
|
||||||
|
- Use interrupt-driven input (already implemented)
|
||||||
|
- Avoid tight loops checking hardware
|
||||||
|
- Let ISRs wake the CPU only when needed
|
||||||
|
|
||||||
|
### Display Power
|
||||||
|
```cpp
|
||||||
|
// For e-ink: Turn off display after inactivity
|
||||||
|
if (time_since_last_input > SLEEP_TIMEOUT) {
|
||||||
|
display->sleep();
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## GUI Components Available
|
||||||
|
|
||||||
|
The template includes a full GUI system with these components:
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
// Windows
|
||||||
|
gui->draw_new_window(x, y, width, height, "Title");
|
||||||
|
|
||||||
|
// Buttons
|
||||||
|
gui->draw_button(window, x, y, "Label", pressed, rounded);
|
||||||
|
|
||||||
|
// Checkboxes
|
||||||
|
gui->draw_checkbox(window, x, y, "Label", checked);
|
||||||
|
|
||||||
|
// Radio Buttons
|
||||||
|
gui->draw_radio_button(window, x, y, "Label", selected);
|
||||||
|
|
||||||
|
// Sliders
|
||||||
|
gui->draw_slider(window, x, y, width, height, position, "Label");
|
||||||
|
|
||||||
|
// Status Bars
|
||||||
|
gui->draw_status_bar(window, x, y, width, "Label", "Sublabel", percentage, "Value");
|
||||||
|
|
||||||
|
// Gauges
|
||||||
|
gui->draw_circular_gauge(window, x, y, width, "Label", percentage);
|
||||||
|
|
||||||
|
// Text Boxes
|
||||||
|
gui->draw_textbox(window, x, y, width, height, "Content", focused);
|
||||||
|
|
||||||
|
// Tabs
|
||||||
|
gui->draw_tab(window, x, y, width, height, "Label", selected);
|
||||||
|
|
||||||
|
// Notifications
|
||||||
|
gui->draw_notification(window, x, y, width, "Time", "Message");
|
||||||
|
|
||||||
|
// Clock
|
||||||
|
gui->draw_large_clock(window, x, y, "12:30");
|
||||||
|
|
||||||
|
// Calendar
|
||||||
|
gui->draw_calendar(window, x, y, month, year);
|
||||||
|
```
|
||||||
|
|
||||||
|
## Debugging Tips
|
||||||
|
|
||||||
|
### Enable Verbose Mode
|
||||||
|
```cpp
|
||||||
|
GameConfig config = {
|
||||||
|
.debug_verbose = true // Print debug messages
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
### Monitor Input Events
|
||||||
|
```cpp
|
||||||
|
if (config.debug_verbose) {
|
||||||
|
printf("Input: type=%d x=%d y=%d\n", input.type, input.x, input.y);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Check State Changes
|
||||||
|
```cpp
|
||||||
|
if (config.debug_verbose) {
|
||||||
|
printf("Score: %d, Lives: %d\n", state->score, state->lives);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Serial Monitor
|
||||||
|
Connect via USB and monitor output:
|
||||||
|
```bash
|
||||||
|
screen /dev/cu.usbmodem101
|
||||||
|
```
|
||||||
|
|
||||||
|
## Multi-Board Support
|
||||||
|
|
||||||
|
The template works across different board configurations:
|
||||||
|
|
||||||
|
- **Pico 2 with TFT + Touch**: Full interactive drawing
|
||||||
|
- **Pico 2 with E-ink + Buttons**: Button-based navigation
|
||||||
|
- **Feather boards**: Various display combinations
|
||||||
|
|
||||||
|
Board-specific configuration is handled automatically through `board_config.h`.
|
||||||
|
|
||||||
|
## Advanced Patterns
|
||||||
|
|
||||||
|
### Timer-Based Updates
|
||||||
|
For games needing periodic updates (not just reactive):
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
// In main(), before loop:
|
||||||
|
uint32_t last_game_tick = 0;
|
||||||
|
const uint32_t TICK_INTERVAL_MS = 100;
|
||||||
|
|
||||||
|
// In main loop:
|
||||||
|
uint32_t now = to_ms_since_boot(get_absolute_time());
|
||||||
|
bool needs_tick = (now - last_game_tick >= TICK_INTERVAL_MS);
|
||||||
|
|
||||||
|
if (needs_tick) {
|
||||||
|
// Update game logic (physics, AI, etc.)
|
||||||
|
update_game_tick(&game_state);
|
||||||
|
last_game_tick = now;
|
||||||
|
refresh_screen(bit_buffer, display);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Animation
|
||||||
|
```cpp
|
||||||
|
// Smooth movement over multiple frames
|
||||||
|
void animate_sprite(GameState* state) {
|
||||||
|
state->sprite_x += state->velocity_x;
|
||||||
|
state->sprite_y += state->velocity_y;
|
||||||
|
state->frame_count++;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### State Machines
|
||||||
|
```cpp
|
||||||
|
enum GameMode {
|
||||||
|
MODE_MENU,
|
||||||
|
MODE_PLAYING,
|
||||||
|
MODE_PAUSED,
|
||||||
|
MODE_GAME_OVER
|
||||||
|
};
|
||||||
|
|
||||||
|
struct GameState {
|
||||||
|
GameMode mode;
|
||||||
|
// ... other fields
|
||||||
|
};
|
||||||
|
|
||||||
|
bool game_update(GameState* state, const InputEvent& input, ...) {
|
||||||
|
switch(state->mode) {
|
||||||
|
case MODE_MENU:
|
||||||
|
return handle_menu_input(state, input);
|
||||||
|
case MODE_PLAYING:
|
||||||
|
return handle_game_input(state, input);
|
||||||
|
case MODE_PAUSED:
|
||||||
|
return handle_pause_input(state, input);
|
||||||
|
case MODE_GAME_OVER:
|
||||||
|
return handle_gameover_input(state, input);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Common Pitfalls
|
||||||
|
|
||||||
|
### 1. Forgetting to Return True
|
||||||
|
```cpp
|
||||||
|
// Wrong:
|
||||||
|
bool game_update(...) {
|
||||||
|
state->score++;
|
||||||
|
// No return - screen won't refresh!
|
||||||
|
}
|
||||||
|
|
||||||
|
// Right:
|
||||||
|
bool game_update(...) {
|
||||||
|
state->score++;
|
||||||
|
return true; // Signal refresh needed
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Drawing in game_update()
|
||||||
|
```cpp
|
||||||
|
// Wrong:
|
||||||
|
bool game_update(...) {
|
||||||
|
renderer->draw_line(...); // Drawing here!
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Right:
|
||||||
|
bool game_update(...) {
|
||||||
|
state->line_end_x = input.x; // Update state only
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
// Drawing happens in main loop when refresh is needed
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Blocking in ISR
|
||||||
|
```cpp
|
||||||
|
// Wrong:
|
||||||
|
void touch_interrupt_handler(...) {
|
||||||
|
touch->read_touch(&data); // Slow I2C operation in ISR!
|
||||||
|
printf("Touch!\n"); // Serial output in ISR!
|
||||||
|
}
|
||||||
|
|
||||||
|
// Right:
|
||||||
|
void touch_interrupt_handler(...) {
|
||||||
|
touch_interrupt_flag = true; // Just set flag
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. Not Clearing Buffer Before Redraw
|
||||||
|
```cpp
|
||||||
|
// When redrawing entire UI:
|
||||||
|
memset(bit_buffer, 0, V_WIDTH * V_HEIGHT / 8); // Clear first
|
||||||
|
game_draw(&game_state, &renderer, &gui); // Then draw
|
||||||
|
```
|
||||||
|
|
||||||
|
## Performance Considerations
|
||||||
|
|
||||||
|
### Memory Usage
|
||||||
|
- Frame buffer: `V_WIDTH * V_HEIGHT / 8` bytes (e.g., 13KB for 296x128)
|
||||||
|
- Keep GameState small for fast copying
|
||||||
|
- Use `uint8_t` instead of `int` where possible
|
||||||
|
|
||||||
|
### CPU Usage
|
||||||
|
- Interrupt-driven design minimizes CPU usage
|
||||||
|
- `__wfi()` puts CPU to sleep between events
|
||||||
|
- Typical power draw: < 1mA while sleeping
|
||||||
|
|
||||||
|
### Display Refresh Times
|
||||||
|
- E-ink: 1-4 seconds for full refresh
|
||||||
|
- TFT: < 50ms for full screen
|
||||||
|
- Partial updates much faster
|
||||||
|
|
||||||
|
## Next Steps
|
||||||
|
|
||||||
|
1. **Start Simple**: Begin with basic button navigation
|
||||||
|
2. **Add Features**: Gradually add game mechanics
|
||||||
|
3. **Test on Hardware**: Verify on your target board
|
||||||
|
4. **Optimize**: Tune debounce, refresh strategy for your needs
|
||||||
|
5. **Polish**: Add animations, sounds, save states
|
||||||
|
|
||||||
|
## Example Projects
|
||||||
|
|
||||||
|
Check out these example implementations:
|
||||||
|
- **Button Game** (current): Focus switching and click counting
|
||||||
|
- **Drawing Board**: Touch-based freehand drawing
|
||||||
|
- **Snake Game**: Classic snake with button controls
|
||||||
|
- **Calculator**: Touch-based number pad with operations
|
||||||
|
|
||||||
|
## Resources
|
||||||
|
|
||||||
|
- **Board Configs**: `board_configs/` directory
|
||||||
|
- **Display Drivers**: `display/` directory
|
||||||
|
- **Font Files**: `fonts/` directory
|
||||||
|
- **Refactoring Plan**: `REFACTORING_PLAN.md` - Implementation details
|
||||||
|
|
||||||
|
## Support
|
||||||
|
|
||||||
|
For issues or questions:
|
||||||
|
1. Check serial output with `screen /dev/cu.usbmodem101`
|
||||||
|
2. Enable `debug_verbose` in GameConfig
|
||||||
|
3. Review the refactoring plan for architecture details
|
||||||
|
4. Test with minimal game logic first
|
||||||
|
|
||||||
|
Happy coding! 🎮
|
||||||
613
basic1.cpp
613
basic1.cpp
@@ -3,7 +3,35 @@
|
|||||||
*
|
*
|
||||||
* SPDX-License-Identifier: Apache-2.0
|
* SPDX-License-Identifier: Apache-2.0
|
||||||
*
|
*
|
||||||
* 4.0" TFT ST7796 with Touch Screen and SD Card Demo
|
* ============================================================================
|
||||||
|
* REACTIVE GAME TEMPLATE - Event-Driven Architecture for RP2350
|
||||||
|
* ============================================================================
|
||||||
|
*
|
||||||
|
* This template provides a clean, reactive architecture for building games
|
||||||
|
* and interactive applications on Raspberry Pi Pico with displays.
|
||||||
|
*
|
||||||
|
* KEY FEATURES:
|
||||||
|
* - Event-driven: Display only updates when input is received
|
||||||
|
* - Power efficient: Uses __wfi() to sleep between inputs
|
||||||
|
* - E-ink optimized: Minimizes screen refreshes
|
||||||
|
* - Interrupt-driven: Touch and button handling via interrupts
|
||||||
|
* - Modular: Clear separation of input, game logic, and rendering
|
||||||
|
*
|
||||||
|
* ARCHITECTURE:
|
||||||
|
* 1. Interrupt handlers set flags (kept minimal)
|
||||||
|
* 2. Main loop processes input events
|
||||||
|
* 3. Game logic updates state based on events
|
||||||
|
* 4. Screen refreshes only when changes occur
|
||||||
|
*
|
||||||
|
* HOW TO CREATE YOUR OWN GAME:
|
||||||
|
* ============================================================================
|
||||||
|
* 1. Modify GameState structure with your game variables
|
||||||
|
* 2. Implement game_init() to set initial values
|
||||||
|
* 3. Implement game_update() to handle input and update state
|
||||||
|
* 4. Implement game_draw() to render your game graphics
|
||||||
|
* 5. Adjust GameConfig for your game's needs
|
||||||
|
* 6. The reactive loop and input system work automatically!
|
||||||
|
* ============================================================================
|
||||||
*/
|
*/
|
||||||
|
|
||||||
#include "pico/stdlib.h"
|
#include "pico/stdlib.h"
|
||||||
@@ -26,6 +54,72 @@ bi_decl(bi_program_description("4.0\" TFT ST7796 with Touch and SD Card Demo"));
|
|||||||
bi_decl(bi_program_version_string("0.1"));
|
bi_decl(bi_program_version_string("0.1"));
|
||||||
bi_decl(bi_program_build_date_string(__DATE__));
|
bi_decl(bi_program_build_date_string(__DATE__));
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// INPUT EVENT STRUCTURES
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
// Input event types
|
||||||
|
enum InputType {
|
||||||
|
INPUT_NONE = 0,
|
||||||
|
INPUT_TOUCH_DOWN,
|
||||||
|
INPUT_TOUCH_MOVE,
|
||||||
|
INPUT_TOUCH_UP,
|
||||||
|
INPUT_BUTTON_0,
|
||||||
|
INPUT_BUTTON_1,
|
||||||
|
INPUT_GESTURE
|
||||||
|
};
|
||||||
|
|
||||||
|
// Unified input event structure
|
||||||
|
struct InputEvent {
|
||||||
|
InputType type;
|
||||||
|
int16_t x;
|
||||||
|
int16_t y;
|
||||||
|
uint8_t gesture_code; // For gesture events
|
||||||
|
uint8_t button_id; // For button events
|
||||||
|
uint8_t pressure; // Touch pressure/weight
|
||||||
|
bool valid; // Set to true if event is valid
|
||||||
|
};
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// GAME STATE AND CONFIGURATION
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
// Game state - customize this for your game
|
||||||
|
struct GameState {
|
||||||
|
// Drawing game state
|
||||||
|
int16_t last_x;
|
||||||
|
int16_t last_y;
|
||||||
|
bool is_drawing;
|
||||||
|
|
||||||
|
// General game state
|
||||||
|
uint32_t score;
|
||||||
|
bool game_over;
|
||||||
|
uint32_t frame_count;
|
||||||
|
|
||||||
|
// UI state
|
||||||
|
uint8_t progress_value; // Progress bar value (0-100)
|
||||||
|
uint8_t focused_button; // Which button has focus (0 or 1)
|
||||||
|
uint32_t button1_clicks; // Count clicks on button 1
|
||||||
|
uint32_t button2_clicks; // Count clicks on button 2
|
||||||
|
|
||||||
|
// Statistics
|
||||||
|
uint32_t touch_success_count;
|
||||||
|
uint32_t touch_fail_count;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Game configuration - adjust these for your game
|
||||||
|
struct GameConfig {
|
||||||
|
uint32_t touch_debounce_ms; // Touch polling rate
|
||||||
|
uint32_t button_debounce_ms; // Button debounce delay
|
||||||
|
bool enable_gestures; // Enable gesture recognition
|
||||||
|
bool enable_continuous_draw; // Allow continuous drawing while touched
|
||||||
|
bool debug_verbose; // Print debug messages
|
||||||
|
};
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// INTERRUPT HANDLERS (Keep these minimal!)
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
// Touch interrupt handling
|
// Touch interrupt handling
|
||||||
volatile bool touch_interrupt_flag = false;
|
volatile bool touch_interrupt_flag = false;
|
||||||
volatile bool touch_event_down = false;
|
volatile bool touch_event_down = false;
|
||||||
@@ -54,42 +148,12 @@ void touch_interrupt_handler(uint gpio, uint32_t events) {
|
|||||||
// Main loop will handle the actual touch reading
|
// Main loop will handle the actual touch reading
|
||||||
touch_interrupt_flag = true;
|
touch_interrupt_flag = true;
|
||||||
|
|
||||||
// Optional: track which edge triggered (for debugging)
|
// Track which edge triggered (down vs up)
|
||||||
if (events & GPIO_IRQ_EDGE_FALL) {
|
if (events & GPIO_IRQ_EDGE_FALL) {
|
||||||
touch_event_down = true;
|
touch_event_down = true;
|
||||||
printf("Touch DOWN event detected\n");
|
|
||||||
}
|
}
|
||||||
if (events & GPIO_IRQ_EDGE_RISE) {
|
if (events & GPIO_IRQ_EDGE_RISE) {
|
||||||
touch_event_down = false;
|
touch_event_down = false;
|
||||||
printf("Touch UP event detected\n");
|
|
||||||
}
|
|
||||||
TouchData touch_data;
|
|
||||||
touch->read_touch(&touch_data);
|
|
||||||
|
|
||||||
|
|
||||||
int16_t x = touch_data.points[0].x;
|
|
||||||
int16_t y = touch_data.points[0].y;
|
|
||||||
uint8_t event = touch_data.points[0].event;
|
|
||||||
uint8_t id = touch_data.points[0].id;
|
|
||||||
uint8_t weight = touch_data.points[0].pressure;
|
|
||||||
uint8_t gesture = touch_data.gesture;
|
|
||||||
|
|
||||||
// Display detailed touch information including weight and gesture
|
|
||||||
printf("Touch: X=%d Y=%d Event=%d ID=%d Weight=%d\n",
|
|
||||||
x, y, event, id, weight);
|
|
||||||
|
|
||||||
// Display gesture if detected (non-zero)
|
|
||||||
if (gesture != 0) {
|
|
||||||
const char* gesture_name = "Unknown";
|
|
||||||
switch(gesture) {
|
|
||||||
case 0x10: gesture_name = "Move Up"; break;
|
|
||||||
case 0x14: gesture_name = "Move Right"; break;
|
|
||||||
case 0x18: gesture_name = "Move Down"; break;
|
|
||||||
case 0x1C: gesture_name = "Move Left"; break;
|
|
||||||
case 0x48: gesture_name = "Zoom In"; break;
|
|
||||||
case 0x49: gesture_name = "Zoom Out"; break;
|
|
||||||
}
|
|
||||||
printf(" Gesture=0x%02X (%s)\n", gesture, gesture_name);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -110,12 +174,10 @@ void button_interrupt_handler(uint gpio, uint32_t events) {
|
|||||||
if (events & GPIO_IRQ_EDGE_FALL) {
|
if (events & GPIO_IRQ_EDGE_FALL) {
|
||||||
if (gpio == BUTTON_KEY0_PIN) {
|
if (gpio == BUTTON_KEY0_PIN) {
|
||||||
button_key0_pressed = true;
|
button_key0_pressed = true;
|
||||||
printf("Button KEY0 pressed\n");
|
|
||||||
}
|
}
|
||||||
#ifdef BUTTON_KEY1_PIN
|
#ifdef BUTTON_KEY1_PIN
|
||||||
else if (gpio == BUTTON_KEY1_PIN) {
|
else if (gpio == BUTTON_KEY1_PIN) {
|
||||||
button_key1_pressed = true;
|
button_key1_pressed = true;
|
||||||
printf("Button KEY1 pressed\n");
|
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
@@ -145,6 +207,322 @@ void refresh_screen(const uint8_t *buffer, LowLevelDisplay* display) {
|
|||||||
display->refresh();
|
display->refresh();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// INPUT PROCESSING
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Get human-readable gesture name
|
||||||
|
*
|
||||||
|
* @param gesture_code Gesture code from touch controller
|
||||||
|
* @return Constant string with gesture name
|
||||||
|
*/
|
||||||
|
const char* get_gesture_name(uint8_t gesture_code) {
|
||||||
|
switch(gesture_code) {
|
||||||
|
case 0x10: return "Move Up";
|
||||||
|
case 0x14: return "Move Right";
|
||||||
|
case 0x18: return "Move Down";
|
||||||
|
case 0x1C: return "Move Left";
|
||||||
|
case 0x48: return "Zoom In";
|
||||||
|
case 0x49: return "Zoom Out";
|
||||||
|
default: return "Unknown";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Process touch input and convert to InputEvent
|
||||||
|
*
|
||||||
|
* Reads touch data from controller and creates appropriate InputEvent.
|
||||||
|
* Handles debouncing and filtering internally.
|
||||||
|
*
|
||||||
|
* @param config Game configuration
|
||||||
|
* @param last_time Pointer to last touch time for debouncing
|
||||||
|
* @return InputEvent structure (valid=false if no valid input)
|
||||||
|
*/
|
||||||
|
InputEvent process_touch_input(const GameConfig& config, uint32_t* last_time) {
|
||||||
|
InputEvent event = {INPUT_NONE, 0, 0, 0, 0, 0, false};
|
||||||
|
|
||||||
|
// Check if touch interrupt flag is set
|
||||||
|
if (!touch_interrupt_flag) {
|
||||||
|
return event; // No touch event
|
||||||
|
}
|
||||||
|
|
||||||
|
// Don't clear the flag yet - we may still be processing continuous touch
|
||||||
|
|
||||||
|
// Check if touch is active
|
||||||
|
if (!touch_event_down) {
|
||||||
|
// Touch released
|
||||||
|
touch_interrupt_flag = false;
|
||||||
|
event.type = INPUT_TOUCH_UP;
|
||||||
|
event.valid = true;
|
||||||
|
return event;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Touch is down - check debounce timing
|
||||||
|
uint32_t now = to_ms_since_boot(get_absolute_time());
|
||||||
|
if (now - *last_time < config.touch_debounce_ms) {
|
||||||
|
return event; // Too soon, skip
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read touch data
|
||||||
|
TouchData touch_data;
|
||||||
|
if (!touch || !touch->read_touch(&touch_data)) {
|
||||||
|
return event; // Read failed
|
||||||
|
}
|
||||||
|
|
||||||
|
// Populate event structure
|
||||||
|
event.x = touch_data.points[0].x;
|
||||||
|
event.y = touch_data.points[0].y;
|
||||||
|
event.pressure = touch_data.points[0].pressure;
|
||||||
|
event.gesture_code = touch_data.gesture;
|
||||||
|
event.valid = true;
|
||||||
|
|
||||||
|
// Determine event type
|
||||||
|
if (*last_time == 0) {
|
||||||
|
event.type = INPUT_TOUCH_DOWN;
|
||||||
|
} else {
|
||||||
|
event.type = INPUT_TOUCH_MOVE;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle gesture events
|
||||||
|
if (config.enable_gestures && touch_data.gesture != 0) {
|
||||||
|
event.type = INPUT_GESTURE;
|
||||||
|
if (config.debug_verbose) {
|
||||||
|
printf("Gesture: 0x%02X (%s)\n", event.gesture_code, get_gesture_name(event.gesture_code));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
*last_time = now;
|
||||||
|
return event;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Process button input and convert to InputEvent
|
||||||
|
*
|
||||||
|
* Checks button flags and verifies button state with debouncing.
|
||||||
|
* Clears flags after processing.
|
||||||
|
*
|
||||||
|
* @param config Game configuration
|
||||||
|
* @return InputEvent structure (valid=false if no valid input)
|
||||||
|
*/
|
||||||
|
InputEvent process_button_input(const GameConfig& config) {
|
||||||
|
InputEvent event = {INPUT_NONE, 0, 0, 0, 0, 0, false};
|
||||||
|
|
||||||
|
#ifdef BUTTON_KEY0_PIN
|
||||||
|
// Check KEY0
|
||||||
|
if (button_key0_pressed) {
|
||||||
|
button_key0_pressed = false;
|
||||||
|
sleep_ms(config.button_debounce_ms);
|
||||||
|
|
||||||
|
if (gpio_get(BUTTON_KEY0_PIN) == 0) { // Verify still pressed
|
||||||
|
event.type = INPUT_BUTTON_0;
|
||||||
|
event.button_id = 0;
|
||||||
|
event.valid = true;
|
||||||
|
|
||||||
|
if (config.debug_verbose) {
|
||||||
|
printf("Button KEY0 action triggered\n");
|
||||||
|
}
|
||||||
|
return event;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#ifdef BUTTON_KEY1_PIN
|
||||||
|
// Check KEY1
|
||||||
|
if (button_key1_pressed) {
|
||||||
|
button_key1_pressed = false;
|
||||||
|
sleep_ms(config.button_debounce_ms);
|
||||||
|
|
||||||
|
if (gpio_get(BUTTON_KEY1_PIN) == 0) { // Verify still pressed
|
||||||
|
event.type = INPUT_BUTTON_1;
|
||||||
|
event.button_id = 1;
|
||||||
|
event.valid = true;
|
||||||
|
|
||||||
|
if (config.debug_verbose) {
|
||||||
|
printf("Button KEY1 action triggered\n");
|
||||||
|
}
|
||||||
|
return event;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
#endif
|
||||||
|
|
||||||
|
return event;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// GAME LOGIC (Customize this section for your game!)
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Initialize game state
|
||||||
|
*
|
||||||
|
* Called once at startup to set initial game values.
|
||||||
|
* Customize this for your game.
|
||||||
|
*
|
||||||
|
* @param state Pointer to GameState to initialize
|
||||||
|
*/
|
||||||
|
void game_init(GameState* state) {
|
||||||
|
state->last_x = -1;
|
||||||
|
state->last_y = -1;
|
||||||
|
state->is_drawing = false;
|
||||||
|
state->score = 0;
|
||||||
|
state->game_over = false;
|
||||||
|
state->frame_count = 0;
|
||||||
|
state->progress_value = 50; // Start at 50%
|
||||||
|
state->focused_button = 0; // Start with first button focused
|
||||||
|
state->button1_clicks = 0;
|
||||||
|
state->button2_clicks = 0;
|
||||||
|
state->touch_success_count = 0;
|
||||||
|
state->touch_fail_count = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Update game state based on input event
|
||||||
|
*
|
||||||
|
* This is where your game logic goes.
|
||||||
|
* Called whenever an input event occurs.
|
||||||
|
*
|
||||||
|
* @param state Pointer to GameState to update
|
||||||
|
* @param input Input event to process
|
||||||
|
* @param config Game configuration
|
||||||
|
* @param renderer Renderer for drawing operations
|
||||||
|
* @return true if screen needs refresh (drawing occurred)
|
||||||
|
*/
|
||||||
|
bool game_update(GameState* state, const InputEvent& input, const GameConfig& config, LowLevelRenderer* renderer) {
|
||||||
|
bool needs_refresh = false;
|
||||||
|
|
||||||
|
switch (input.type) {
|
||||||
|
case INPUT_TOUCH_DOWN:
|
||||||
|
// Start new drawing stroke
|
||||||
|
state->last_x = input.x;
|
||||||
|
state->last_y = input.y;
|
||||||
|
state->is_drawing = true;
|
||||||
|
state->touch_success_count++;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case INPUT_TOUCH_MOVE:
|
||||||
|
// Continue drawing stroke
|
||||||
|
if (config.enable_continuous_draw && state->is_drawing) {
|
||||||
|
if (state->last_x >= 0 && state->last_y >= 0) {
|
||||||
|
// Draw line from last position
|
||||||
|
renderer->draw_line(state->last_x, state->last_y, input.x, input.y, true);
|
||||||
|
needs_refresh = true;
|
||||||
|
}
|
||||||
|
state->last_x = input.x;
|
||||||
|
state->last_y = input.y;
|
||||||
|
state->touch_success_count++;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case INPUT_TOUCH_UP:
|
||||||
|
// End drawing stroke
|
||||||
|
state->is_drawing = false;
|
||||||
|
state->last_x = -1;
|
||||||
|
state->last_y = -1;
|
||||||
|
needs_refresh = true; // Final refresh to show complete stroke
|
||||||
|
break;
|
||||||
|
|
||||||
|
case INPUT_BUTTON_0:
|
||||||
|
// KEY0: Switch focus between buttons
|
||||||
|
state->focused_button = (state->focused_button == 0) ? 1 : 0;
|
||||||
|
needs_refresh = true;
|
||||||
|
if (config.debug_verbose) {
|
||||||
|
printf("Focus switched to button %d\n", state->focused_button);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case INPUT_BUTTON_1:
|
||||||
|
// KEY1: Activate the focused button
|
||||||
|
if (state->focused_button == 0) {
|
||||||
|
state->button1_clicks++;
|
||||||
|
if (config.debug_verbose) {
|
||||||
|
printf("Button 1 clicked! Total: %d\n", state->button1_clicks);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
state->button2_clicks++;
|
||||||
|
if (config.debug_verbose) {
|
||||||
|
printf("Button 2 clicked! Total: %d\n", state->button2_clicks);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
needs_refresh = true;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case INPUT_GESTURE:
|
||||||
|
// Handle gesture
|
||||||
|
if (config.debug_verbose) {
|
||||||
|
printf("Gesture detected: %s\n", get_gesture_name(input.gesture_code));
|
||||||
|
}
|
||||||
|
// Add gesture-specific actions here
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
state->frame_count++;
|
||||||
|
return needs_refresh;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Draw game graphics to screen buffer
|
||||||
|
*
|
||||||
|
* All initial UI drawing operations go here.
|
||||||
|
* Called once at startup to create the initial screen.
|
||||||
|
*
|
||||||
|
* @param state Pointer to current GameState
|
||||||
|
* @param renderer Renderer for drawing primitives
|
||||||
|
* @param gui GUI system for widgets (optional)
|
||||||
|
*/
|
||||||
|
void game_draw(const GameState* state, LowLevelRenderer* renderer, LowLevelGUI* gui) {
|
||||||
|
// Draw main window
|
||||||
|
LowLevelWindow *w1 = gui->draw_new_window(10, 10, V_WIDTH - 20, V_HEIGHT - 20, "Button Game");
|
||||||
|
|
||||||
|
// Draw instructions using text
|
||||||
|
renderer->set_font(&font_5x5_obj);
|
||||||
|
renderer->draw_string(20, 50, "KEY0: Switch Focus", true);
|
||||||
|
renderer->draw_string(20, 65, "KEY1: Click Button", true);
|
||||||
|
|
||||||
|
// Create button labels with click counts
|
||||||
|
char btn1_label[30];
|
||||||
|
snprintf(btn1_label, sizeof(btn1_label), "BTN 1 (%d)", state->button1_clicks);
|
||||||
|
|
||||||
|
char btn2_label[30];
|
||||||
|
snprintf(btn2_label, sizeof(btn2_label), "BTN 2 (%d)", state->button2_clicks);
|
||||||
|
|
||||||
|
// Draw Button 1 using GUI button element
|
||||||
|
// pressed=true shows it's focused/selected
|
||||||
|
gui->draw_button(w1, 10, 90, btn1_label, state->focused_button == 0, true);
|
||||||
|
|
||||||
|
// Draw Button 2 using GUI button element
|
||||||
|
gui->draw_button(w1, 10, 140, btn2_label, state->focused_button == 1, true);
|
||||||
|
|
||||||
|
// Draw status indicators using GUI elements
|
||||||
|
// Show which button is focused
|
||||||
|
if (state->focused_button == 0) {
|
||||||
|
gui->draw_radio_button(w1, 200, 100, "Active", true);
|
||||||
|
} else {
|
||||||
|
gui->draw_radio_button(w1, 200, 100, "Active", false);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (state->focused_button == 1) {
|
||||||
|
gui->draw_radio_button(w1, 200, 150, "Active", true);
|
||||||
|
} else {
|
||||||
|
gui->draw_radio_button(w1, 200, 150, "Active", false);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show total interactions with a status bar
|
||||||
|
uint32_t total_clicks = state->button1_clicks + state->button2_clicks;
|
||||||
|
int percentage = (total_clicks > 0) ? ((state->button1_clicks * 100) / total_clicks) : 50;
|
||||||
|
|
||||||
|
char total_str[20];
|
||||||
|
snprintf(total_str, sizeof(total_str), "%d", total_clicks);
|
||||||
|
gui->draw_status_bar(w1, 10, 200, 270, "TOTAL CLICKS", "BTN1 vs BTN2 Ratio", percentage, total_str);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// MAIN PROGRAM
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -191,13 +569,26 @@ int main()
|
|||||||
// Now clear to black for drawing
|
// Now clear to black for drawing
|
||||||
display->clear(false); // Clear to black
|
display->clear(false); // Clear to black
|
||||||
|
|
||||||
|
// Initialize renderer and GUI system
|
||||||
LowLevelRenderer renderer(bit_buffer, V_WIDTH, V_HEIGHT);
|
LowLevelRenderer renderer(bit_buffer, V_WIDTH, V_HEIGHT);
|
||||||
renderer.set_font(&font_5x5_obj);
|
renderer.set_font(&font_5x5_obj);
|
||||||
LowLevelGUI gui = LowLevelGUI(&renderer, font_BMplain_obj);
|
LowLevelGUI gui = LowLevelGUI(&renderer, font_BMplain_obj);
|
||||||
LowLevelWindow *w1 = gui.draw_new_window(15, 15, V_WIDTH - 30, V_HEIGHT - 30, "Main Window");
|
|
||||||
gui.draw_status_bar(w1, 10, 40, 200,
|
// Initialize game configuration
|
||||||
"PANELS", "Weekly Average Charge", 65, "190KWH");
|
GameConfig config = {
|
||||||
gui.draw_circular_gauge(w1, 10, 100 - 10, 200, "SYSTEM EFF.", 68);
|
.touch_debounce_ms = 10,
|
||||||
|
.button_debounce_ms = 50,
|
||||||
|
.enable_gestures = true,
|
||||||
|
.enable_continuous_draw = true,
|
||||||
|
.debug_verbose = false
|
||||||
|
};
|
||||||
|
|
||||||
|
// Initialize game state
|
||||||
|
GameState game_state;
|
||||||
|
game_init(&game_state);
|
||||||
|
|
||||||
|
// Draw initial game graphics
|
||||||
|
game_draw(&game_state, &renderer, &gui);
|
||||||
|
|
||||||
// Refresh the screen with the rendered GUI
|
// Refresh the screen with the rendered GUI
|
||||||
refresh_screen(bit_buffer, display);
|
refresh_screen(bit_buffer, display);
|
||||||
@@ -260,131 +651,47 @@ int main()
|
|||||||
// printf("SD Card initialization failed or no card present\n");
|
// printf("SD Card initialization failed or no card present\n");
|
||||||
// }
|
// }
|
||||||
|
|
||||||
// Main loop - handle touch events
|
// ========================================================================
|
||||||
int last_x = -1, last_y = -1;
|
// REACTIVE GAME LOOP
|
||||||
|
// ========================================================================
|
||||||
|
// The loop sleeps until an interrupt occurs, then:
|
||||||
|
// 1. Process input (button or touch)
|
||||||
|
// 2. Update game state based on input
|
||||||
|
// 3. Redraw only if game_update() indicates changes occurred
|
||||||
|
// This is ideal for e-ink displays (minimal refreshes) and power efficiency
|
||||||
|
// ========================================================================
|
||||||
|
|
||||||
// Touch debouncing
|
|
||||||
uint32_t last_touch_time = 0;
|
uint32_t last_touch_time = 0;
|
||||||
const uint32_t debounce_ms = 10; // Poll touch every 10ms (100 times per second)
|
|
||||||
bool was_touched = false;
|
|
||||||
int touch_fail_count = 0;
|
|
||||||
int touch_success_count = 0;
|
|
||||||
|
|
||||||
while (1) {
|
while (1) {
|
||||||
// Sleep until interrupt wakes us up (very power efficient!)
|
// Sleep until interrupt wakes us up (very power efficient!)
|
||||||
// Te(); // Wait For Event - CPU sleeps until interrupt or evenurs
|
__wfi(); // Wait For Interrupt - CPU sleeps until any interrupt occurs
|
||||||
__wfi(); // Wait For Interrupt - CPU sleeps until any interrupt
|
|
||||||
|
|
||||||
#ifdef BUTTON_KEY0_PIN
|
InputEvent input = {INPUT_NONE, 0, 0, 0, 0, 0, false};
|
||||||
// Handle button presses with debouncing
|
bool needs_refresh = false;
|
||||||
if (button_key0_pressed) {
|
|
||||||
button_key0_pressed = false;
|
// 1. Process button input first (higher priority)
|
||||||
sleep_ms(50); // Debounce delay
|
input = process_button_input(config);
|
||||||
if (gpio_get(BUTTON_KEY0_PIN) == 0) { // Verify button still pressed
|
if (input.valid) {
|
||||||
printf("Button KEY0 action triggered\n");
|
needs_refresh = game_update(&game_state, input, config, &renderer);
|
||||||
// TODO: Add your KEY0 action here (e.g., focus next widget)
|
}
|
||||||
|
|
||||||
|
// 2. Process touch input (if no button was pressed)
|
||||||
|
if (!input.valid) {
|
||||||
|
input = process_touch_input(config, &last_touch_time);
|
||||||
|
if (input.valid) {
|
||||||
|
needs_refresh = game_update(&game_state, input, config, &renderer);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (button_key1_pressed) {
|
// 3. Redraw and refresh screen only if needed
|
||||||
button_key1_pressed = false;
|
if (needs_refresh) {
|
||||||
sleep_ms(50); // Debounce delay
|
// For button presses or touch release, redraw entire UI
|
||||||
if (gpio_get(BUTTON_KEY1_PIN) == 0) { // Verify button still pressed
|
if (input.type == INPUT_BUTTON_0 || input.type == INPUT_BUTTON_1 || input.type == INPUT_TOUCH_UP) {
|
||||||
printf("Button KEY1 action triggered\n");
|
// Clear buffer and redraw entire UI with updated state
|
||||||
// TODO: Add your KEY1 action here (e.g., activate focused widget)
|
memset(bit_buffer, 0, V_WIDTH * V_HEIGHT / 8);
|
||||||
|
game_draw(&game_state, &renderer, &gui);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
#endif
|
|
||||||
|
|
||||||
// Check if our touch interrupt flag was set
|
|
||||||
if (!touch_interrupt_flag) {
|
|
||||||
continue; // Woken by different interrupt, go back to sleep
|
|
||||||
}
|
|
||||||
|
|
||||||
// Clear the flag
|
|
||||||
touch_interrupt_flag = false;
|
|
||||||
|
|
||||||
while(touch_event_down){
|
|
||||||
uint32_t now = to_ms_since_boot(get_absolute_time());
|
|
||||||
|
|
||||||
// Check if enough time has passed since last touch check (debounce)
|
|
||||||
if (now - last_touch_time < debounce_ms) {
|
|
||||||
//continue;
|
|
||||||
}
|
|
||||||
//printf("Touch interrupt event detected (event_down=%d)\n", touch_event_down);
|
|
||||||
|
|
||||||
// Touch interrupt occurred - read the data
|
|
||||||
// is_touched() will check INT pin and confirm via I2C if needed
|
|
||||||
//if (touch && touch->is_touched()) {
|
|
||||||
// Now read full touch data via I2C (already confirmed by INT pin)
|
|
||||||
TouchData touch_data;
|
|
||||||
if (!touch->read_touch(&touch_data)) {
|
|
||||||
// Read failed or no actual touch data
|
|
||||||
touch_fail_count++;
|
|
||||||
//was_touched = false;
|
|
||||||
//last_x = -1;
|
|
||||||
//last_y = -1;
|
|
||||||
//last_touch_time = now;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
touch_success_count++;
|
|
||||||
|
|
||||||
int16_t x = touch_data.points[0].x;
|
|
||||||
int16_t y = touch_data.points[0].y;
|
|
||||||
uint8_t event = touch_data.points[0].event;
|
|
||||||
uint8_t id = touch_data.points[0].id;
|
|
||||||
uint8_t weight = touch_data.points[0].pressure;
|
|
||||||
uint8_t gesture = touch_data.gesture;
|
|
||||||
|
|
||||||
// Display detailed touch information including weight and gesture
|
|
||||||
// printf("Touch: X=%d Y=%d Event=%d ID=%d Weight=%d",
|
|
||||||
// x, y, event, id, weight);
|
|
||||||
|
|
||||||
// Display gesture if detected (non-zero)
|
|
||||||
if (gesture != 0) {
|
|
||||||
const char* gesture_name = "Unknown";
|
|
||||||
switch(gesture) {
|
|
||||||
case 0x10: gesture_name = "Move Up"; break;
|
|
||||||
case 0x14: gesture_name = "Move Right"; break;
|
|
||||||
case 0x18: gesture_name = "Move Down"; break;
|
|
||||||
case 0x1C: gesture_name = "Move Left"; break;
|
|
||||||
case 0x48: gesture_name = "Zoom In"; break;
|
|
||||||
case 0x49: gesture_name = "Zoom Out"; break;
|
|
||||||
}
|
|
||||||
printf(" Gesture=0x%02X (%s)", gesture, gesture_name);
|
|
||||||
}
|
|
||||||
|
|
||||||
// printf(" [Success:%d Fail:%d]\n", touch_success_count, touch_fail_count);
|
|
||||||
|
|
||||||
|
|
||||||
// Check if touch is in title area to clear screen
|
|
||||||
|
|
||||||
// Draw line from last position (for smooth drawing)
|
|
||||||
if (last_x >= 0 && last_y >= 0) {
|
|
||||||
int dx = abs(x - last_x);
|
|
||||||
int dy = abs(y - last_y);
|
|
||||||
// Only draw line if movement is reasonable (filter noise)
|
|
||||||
//if (dx < 50 && dy < 50) {
|
|
||||||
renderer.draw_line(last_x, last_y, x, y, true);
|
|
||||||
|
|
||||||
//}
|
|
||||||
}
|
|
||||||
|
|
||||||
last_x = x;
|
|
||||||
last_y = y;
|
|
||||||
|
|
||||||
was_touched = true;
|
|
||||||
last_touch_time = now;
|
|
||||||
//} else {
|
|
||||||
// INT pin triggered but no touch data (likely release event)
|
|
||||||
|
|
||||||
//}
|
|
||||||
}
|
|
||||||
if (was_touched) {
|
|
||||||
last_x = -1;
|
|
||||||
last_y = -1;
|
|
||||||
was_touched = false;
|
|
||||||
refresh_screen(bit_buffer, display);
|
refresh_screen(bit_buffer, display);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -93,13 +93,13 @@ void LowLevelDisplayEPaper::refresh() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
printf("Refreshing e-paper display (partial refresh)...\n");
|
// printf("Refreshing e-paper display (partial refresh)...\n");
|
||||||
|
|
||||||
// Use partial refresh for fast updates (~1 second instead of ~15 seconds)
|
// Use partial refresh for fast updates (~1 second instead of ~15 seconds)
|
||||||
// Partial refresh updates only the changed pixels
|
// Partial refresh updates only the changed pixels
|
||||||
EPD_4IN2_V2_PartialDisplay(framebuffer, 0, 0, EPD_4IN2_V2_WIDTH, EPD_4IN2_V2_HEIGHT);
|
EPD_4IN2_V2_PartialDisplay(framebuffer, 0, 0, EPD_4IN2_V2_WIDTH, EPD_4IN2_V2_HEIGHT);
|
||||||
|
|
||||||
printf("E-paper partial refresh complete\n");
|
// printf("E-paper partial refresh complete\n");
|
||||||
dirty = false;
|
dirty = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user