1025 lines
41 KiB
C++
1025 lines
41 KiB
C++
/*
|
|
* Copyright (c) 2021 Arm Limited and Contributors. All rights reserved.
|
|
*
|
|
* SPDX-License-Identifier: Apache-2.0
|
|
*
|
|
* ============================================================================
|
|
* 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
|
|
* - Hybrid input handling: IRQ wake-up plus active-touch sampling
|
|
* - 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/binary_info.h"
|
|
#include "pico/multicore.h"
|
|
#include "board_config.h" // Board-specific pin configuration
|
|
#include "sd_card.h"
|
|
extern "C" {
|
|
#include "ff.h" // FatFS
|
|
}
|
|
#include <stdlib.h>
|
|
#include <stdio.h>
|
|
#include <string.h>
|
|
#include "display/low_level_render.h"
|
|
#include "display/low_level_display.h"
|
|
#include "display/low_level_display_epaper.h"
|
|
#include "display/low_level_display_st7796.h"
|
|
#include "display/low_level_touch.h"
|
|
#include "input_manager.h"
|
|
#include "game.h"
|
|
#include "game_launcher.h"
|
|
#include "tic_tac_toe.h"
|
|
#include "demo_game.h"
|
|
#include "monopoly_game.h"
|
|
#include "lua_game_loader.h"
|
|
#include "serial_uploader.h"
|
|
#include "scene_stack.h"
|
|
|
|
|
|
// Binary info for RP2350 - ensures proper boot image structure
|
|
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_build_date_string(__DATE__));
|
|
|
|
// ============================================================================
|
|
// DUAL-CORE DISPLAY REFRESH SYSTEM
|
|
// ============================================================================
|
|
|
|
// Shared variables for core communication
|
|
volatile bool refresh_requested = false;
|
|
volatile bool refresh_in_progress = false;
|
|
const uint8_t* volatile refresh_buffer = nullptr;
|
|
LowLevelDisplay* volatile refresh_display = nullptr;
|
|
|
|
/**
|
|
* @brief Core 1 entry point - handles display refresh operations
|
|
*
|
|
* Runs on the second core, waiting for refresh requests.
|
|
* This keeps Core 0 responsive while display updates happen in background.
|
|
*/
|
|
void core1_entry() {
|
|
printf("Core 1 started - handling display refreshes\n");
|
|
|
|
while (1) {
|
|
// Wait for refresh request
|
|
if (refresh_requested && refresh_buffer && refresh_display) {
|
|
// refresh_in_progress is already set by Core 0 to lock the buffer
|
|
|
|
// Get local copies for safe access
|
|
LowLevelDisplay* display = refresh_display;
|
|
const uint8_t* buffer = refresh_buffer;
|
|
|
|
// Perform the refresh operation (may be slow for e-ink)
|
|
display->draw_buffer(buffer);
|
|
display->refresh();
|
|
|
|
// Clear flags
|
|
refresh_requested = false;
|
|
refresh_in_progress = false; // Unlock buffer for Core 0
|
|
}
|
|
|
|
// Small delay to avoid busy-waiting
|
|
sleep_us(100);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @brief Request a screen refresh (non-blocking)
|
|
*
|
|
* Queues the refresh on Core 1, keeping Core 0 responsive.
|
|
*
|
|
* @param buffer Pointer to 1-bit framebuffer
|
|
* @param display Pointer to display abstraction
|
|
* @return true if refresh started, false if already in progress
|
|
*/
|
|
bool refresh_screen_async(const uint8_t *buffer, LowLevelDisplay* display) {
|
|
// Check if Core 1 is busy with previous refresh
|
|
if (refresh_in_progress) {
|
|
// Still refreshing previous frame, skip this one
|
|
return false;
|
|
}
|
|
|
|
// Lock the buffer immediately on Core 0 to prevent race condition
|
|
// Core 1 will unlock it (set to false) when done
|
|
refresh_in_progress = true;
|
|
|
|
// Queue refresh on Core 1
|
|
refresh_buffer = buffer;
|
|
refresh_display = display;
|
|
refresh_requested = true;
|
|
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* @brief Check if a refresh is currently in progress
|
|
* @return true if Core 1 is still refreshing
|
|
*/
|
|
bool is_refresh_in_progress() {
|
|
return refresh_in_progress;
|
|
}
|
|
|
|
// ============================================================================
|
|
// GAME CONFIGURATION
|
|
// ============================================================================
|
|
|
|
// 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
|
|
};
|
|
|
|
// ============================================================================
|
|
// DISPLAY DIMMING CONFIGURATION
|
|
// ============================================================================
|
|
|
|
// Display dimming settings
|
|
#define DEFAULT_DIM_TIMEOUT_MS (2 * 60 * 1000) // 2 minutes to dim
|
|
#define DEFAULT_SLEEP_TIMEOUT_MS (10 * 60 * 1000) // 10 minutes to sleep
|
|
#define DIM_CHECK_INTERVAL_MS 10000 // Check every 10 seconds
|
|
|
|
// Display dimming state
|
|
static uint32_t last_interaction_time = 0; // Last time user interacted
|
|
static bool is_idle_2min_triggered = false; // Flag for 2min trigger
|
|
static bool is_idle_10min_triggered = false; // Flag for 10min trigger
|
|
static volatile bool dim_check_flag = false; // Flag set by timer to check dimming
|
|
static uint32_t dim_timeout_ms = DEFAULT_DIM_TIMEOUT_MS;
|
|
static uint32_t sleep_timeout_ms = DEFAULT_SLEEP_TIMEOUT_MS;
|
|
|
|
/**
|
|
* @brief Update last interaction time and notify display driver
|
|
*
|
|
* Call this whenever the user interacts with the device (touch, button press).
|
|
* The display driver handles specific wake/restore logic.
|
|
*
|
|
* @param display Pointer to display interface
|
|
*/
|
|
static inline void record_user_interaction(LowLevelDisplay* display) {
|
|
last_interaction_time = to_ms_since_boot(get_absolute_time());
|
|
|
|
// Reset idle flags
|
|
is_idle_2min_triggered = false;
|
|
is_idle_10min_triggered = false;
|
|
|
|
// Notify display driver of interaction
|
|
display->on_user_interaction();
|
|
}
|
|
|
|
/**
|
|
* @brief Timer callback to periodically check dimming status
|
|
*
|
|
* This alarm callback fires every DIM_CHECK_INTERVAL_MS milliseconds
|
|
* to wake the CPU from __wfi() and check if dimming should occur.
|
|
* Running in interrupt context, so just sets a flag for main loop.
|
|
*
|
|
* @param id Alarm ID (unused)
|
|
* @param user_data User data pointer (unused)
|
|
* @return Next alarm time (relative to current time)
|
|
*/
|
|
static int64_t dim_check_alarm_callback(alarm_id_t id, void *user_data) {
|
|
// Set flag to check dimming in main loop
|
|
dim_check_flag = true;
|
|
|
|
// Return interval in microseconds for next alarm
|
|
// Negative value means schedule relative to now
|
|
return -(DIM_CHECK_INTERVAL_MS * 1000);
|
|
}
|
|
|
|
/**
|
|
* @brief Check if idle thresholds have been met and notify display driver
|
|
*
|
|
* Checks elapsed time since last interaction and calls the appropriate
|
|
* display driver methods (on_idle_2min or on_idle_10min).
|
|
*
|
|
* @param display Pointer to display interface
|
|
*/
|
|
static inline void check_and_apply_dimming(LowLevelDisplay* display) {
|
|
uint32_t current_time = to_ms_since_boot(get_absolute_time());
|
|
uint32_t elapsed = current_time - last_interaction_time;
|
|
|
|
// Check sleep timeout (if enabled)
|
|
if (sleep_timeout_ms > 0 && !is_idle_10min_triggered && elapsed >= sleep_timeout_ms) {
|
|
display->on_idle_10min();
|
|
is_idle_10min_triggered = true;
|
|
is_idle_2min_triggered = true; // Implicitly triggered
|
|
}
|
|
// Check dim timeout (if enabled)
|
|
else if (dim_timeout_ms > 0 && !is_idle_2min_triggered && elapsed >= dim_timeout_ms) {
|
|
display->on_idle_2min();
|
|
is_idle_2min_triggered = true;
|
|
}
|
|
}
|
|
|
|
// ============================================================================
|
|
// INTERRUPT HANDLERS (Keep these minimal!)
|
|
// ============================================================================
|
|
|
|
// Touch interrupt handling
|
|
volatile bool touch_interrupt_flag = false;
|
|
volatile bool touch_event_down = false;
|
|
LowLevelTouch* touch = nullptr;
|
|
|
|
// Button interrupt handling
|
|
#ifdef BUTTON_KEY0_PIN
|
|
volatile bool button_key0_pressed = false;
|
|
volatile bool button_key1_pressed = false;
|
|
#endif
|
|
|
|
/**
|
|
* @brief Returns true when an application-level wake source is pending.
|
|
*
|
|
* __wfi() can wake on unrelated interrupts (e.g. USB/background IRQs). This
|
|
* guard prevents running full input/game logic unless one of our expected
|
|
* event sources actually fired.
|
|
*/
|
|
static inline bool has_pending_wake_source() {
|
|
if (touch_interrupt_flag) return true;
|
|
if (touch_event_down) return true;
|
|
if (dim_check_flag) return true;
|
|
#ifdef BUTTON_KEY0_PIN
|
|
if (button_key0_pressed || button_key1_pressed) return true;
|
|
#endif
|
|
return false;
|
|
}
|
|
|
|
struct SleepOption {
|
|
const char* label;
|
|
uint32_t sleep_ms;
|
|
};
|
|
|
|
static constexpr SleepOption kSleepOptions[] = {
|
|
{"Never", 0},
|
|
{"30 sec", 30 * 1000},
|
|
{"1 min", 60 * 1000},
|
|
{"2 min", 2 * 60 * 1000},
|
|
{"5 min", 5 * 60 * 1000},
|
|
{"10 min", 10 * 60 * 1000}
|
|
};
|
|
static constexpr int kSleepOptionCount = sizeof(kSleepOptions) / sizeof(kSleepOptions[0]);
|
|
|
|
static inline void apply_sleep_option(int option_index) {
|
|
if (option_index < 0 || option_index >= kSleepOptionCount) {
|
|
return;
|
|
}
|
|
|
|
sleep_timeout_ms = kSleepOptions[option_index].sleep_ms;
|
|
if (sleep_timeout_ms == 0) {
|
|
dim_timeout_ms = 0;
|
|
return;
|
|
}
|
|
|
|
uint32_t candidate_dim = sleep_timeout_ms / 5;
|
|
if (candidate_dim < 30 * 1000) {
|
|
candidate_dim = 30 * 1000;
|
|
}
|
|
if (candidate_dim > 2 * 60 * 1000) {
|
|
candidate_dim = 2 * 60 * 1000;
|
|
}
|
|
if (candidate_dim >= sleep_timeout_ms && sleep_timeout_ms > 5000) {
|
|
candidate_dim = sleep_timeout_ms - 5000;
|
|
}
|
|
dim_timeout_ms = candidate_dim;
|
|
}
|
|
|
|
static inline bool in_rect(int16_t x, int16_t y, int rx, int ry, int rw, int rh) {
|
|
return x >= rx && x < (rx + rw) && y >= ry && y < (ry + rh);
|
|
}
|
|
|
|
static inline bool is_top_right_start(int16_t x, int16_t y, int width, int height) {
|
|
return x >= (width * 3 / 4) && y <= (height / 4);
|
|
}
|
|
|
|
static inline bool is_open_menu_swipe(int16_t sx, int16_t sy, int16_t ex, int16_t ey, int width, int height) {
|
|
if (!is_top_right_start(sx, sy, width, height)) {
|
|
return false;
|
|
}
|
|
|
|
const bool released_near_center = in_rect(ex, ey, width / 4, height / 4, width / 2, height / 2);
|
|
const int dx = ex - sx;
|
|
const int dy = ey - sy;
|
|
const bool moved_left_and_down = (dx <= -(width / 5)) && (dy >= (height / 10));
|
|
|
|
printf("Swipe from (%d, %d) to (%d, %d) - released_near_center=%d, moved_left_and_down=%d\n",
|
|
sx, sy, ex, ey, released_near_center, moved_left_and_down);
|
|
|
|
return released_near_center && moved_left_and_down;
|
|
}
|
|
|
|
static void draw_in_game_menu(LowLevelRenderer* renderer, LowLevelGUI* gui, int width, int height, const char* sleep_label) {
|
|
const int menu_w = width - 40;
|
|
const int menu_h = 190;
|
|
const int menu_x = 20;
|
|
const int menu_y = (height - menu_h) / 2;
|
|
const int row_h = 34;
|
|
const int row_x = menu_x + 16;
|
|
const int row_w = menu_w - 32;
|
|
const int row_start_y = menu_y + 48;
|
|
|
|
renderer->draw_filled_rectangle(0, 0, width, height, false, 1);
|
|
renderer->set_font(&font_5x5_obj);
|
|
renderer->set_text_color(true);
|
|
|
|
LowLevelWindow* win = gui->draw_new_window(menu_x, menu_y, menu_w, menu_h, "Game Menu");
|
|
gui->draw_button(win, row_x, row_start_y + (0 * row_h), "Restart Game", false, true);
|
|
gui->draw_button(win, row_x, row_start_y + (1 * row_h), "Back to Game Selection", false, true);
|
|
|
|
char sleep_label_text[64];
|
|
snprintf(sleep_label_text, sizeof(sleep_label_text), "Auto Sleep: %s", sleep_label);
|
|
gui->draw_button(win, row_x, row_start_y + (2 * row_h), sleep_label_text, false, true);
|
|
}
|
|
|
|
/**
|
|
* @brief Returns true when the currently selected game needs frame ticks.
|
|
*/
|
|
static inline bool game_wants_frame_updates(GameLauncher& launcher) {
|
|
if (!launcher.is_game_selected()) {
|
|
return false;
|
|
}
|
|
|
|
Game* game = launcher.get_selected_game();
|
|
return game && game->wants_frame_updates();
|
|
}
|
|
|
|
/**
|
|
* @brief Touch interrupt callback handler
|
|
*
|
|
* Called automatically by hardware when INT pin changes state:
|
|
* - Falling edge: Touch detected (INT goes LOW)
|
|
* - Rising edge: Touch released (INT goes HIGH)
|
|
*
|
|
* This runs in interrupt context, so keep it fast - just set a flag
|
|
*
|
|
* @param gpio GPIO pin number that triggered the interrupt
|
|
* @param events Event mask (GPIO_IRQ_EDGE_FALL and/or GPIO_IRQ_EDGE_RISE)
|
|
*/
|
|
void touch_interrupt_handler(uint gpio, uint32_t events) {
|
|
(void)gpio;
|
|
|
|
// Track which edge triggered (down vs up).
|
|
// Keep ISR minimal: do not log/print from interrupt context.
|
|
if (events & GPIO_IRQ_EDGE_FALL) {
|
|
touch_event_down = true;
|
|
touch_interrupt_flag = true;
|
|
}
|
|
if (events & GPIO_IRQ_EDGE_RISE) {
|
|
touch_event_down = false;
|
|
touch_interrupt_flag = true;
|
|
}
|
|
}
|
|
|
|
#ifdef BUTTON_KEY0_PIN
|
|
/**
|
|
* @brief Button interrupt callback handler
|
|
*
|
|
* Called automatically by hardware when button pins change state.
|
|
* Buttons are active LOW (pressed = 0, released = 1) with pull-ups.
|
|
*
|
|
* This runs in interrupt context, so keep it fast - just set flags.
|
|
*
|
|
* @param gpio GPIO pin number that triggered the interrupt
|
|
* @param events Event mask (GPIO_IRQ_EDGE_FALL and/or GPIO_IRQ_EDGE_RISE)
|
|
*/
|
|
void button_interrupt_handler(uint gpio, uint32_t events) {
|
|
// Only respond to falling edge (button press)
|
|
if (events & GPIO_IRQ_EDGE_FALL) {
|
|
if (gpio == BUTTON_KEY0_PIN) {
|
|
button_key0_pressed = true;
|
|
}
|
|
#ifdef BUTTON_KEY1_PIN
|
|
else if (gpio == BUTTON_KEY1_PIN) {
|
|
button_key1_pressed = true;
|
|
}
|
|
#endif
|
|
}
|
|
}
|
|
#endif
|
|
|
|
// Screen dimensions and configuration from board_config.h
|
|
const int V_WIDTH = DISPLAY_WIDTH;
|
|
const int V_HEIGHT = DISPLAY_HEIGHT;
|
|
|
|
uint8_t bit_buffer[V_WIDTH * V_HEIGHT / 8];
|
|
|
|
/**
|
|
* @brief Refresh the screen with the 1-bit buffer
|
|
*
|
|
* Displays work directly with 1-bit monochrome buffers.
|
|
* The display driver internally converts to its native format (RGB565, etc.)
|
|
*
|
|
* @param buffer Pointer to 1-bit framebuffer (width*height/8 bytes)
|
|
* @param display Pointer to display abstraction layer
|
|
*/
|
|
void refresh_screen(const uint8_t *buffer, LowLevelDisplay* display) {
|
|
display->draw_buffer(buffer);
|
|
display->refresh();
|
|
}
|
|
|
|
// ============================================================================
|
|
// INPUT PROCESSING
|
|
// ============================================================================
|
|
|
|
|
|
|
|
|
|
|
|
// ============================================================================
|
|
// MAIN PROGRAM
|
|
// ============================================================================
|
|
|
|
|
|
|
|
|
|
int main()
|
|
{
|
|
// Initialize standard I/O for debugging with timeout
|
|
// This prevents hanging when USB is not connected
|
|
stdio_init_all();
|
|
sleep_ms(100); // Wait for USB connection (if present)
|
|
|
|
printf("\n=== %s Demo ===\n", BOARD_NAME);
|
|
printf("Starting dual-core system...\n");
|
|
|
|
// Create display abstraction using factory method
|
|
// The factory handles all board-specific configuration internally
|
|
LowLevelDisplay* display = LowLevelDisplay::create((DisplayType)DISPLAY_TYPE_SELECTED, V_WIDTH, V_HEIGHT);
|
|
|
|
if (!display) {
|
|
printf("Failed to create display!\n");
|
|
return -1;
|
|
}
|
|
|
|
printf("Initializing display...\n");
|
|
|
|
// Initialize the display
|
|
if (!display->init()) {
|
|
printf("Display initialization failed!\n");
|
|
delete display;
|
|
return -1;
|
|
}
|
|
|
|
// Enable dirty rectangle optimization for ST7796 displays
|
|
if (display->get_type() == DISPLAY_TYPE_ST7796) {
|
|
LowLevelDisplayST7796* st7796_display = static_cast<LowLevelDisplayST7796*>(display);
|
|
st7796_display->enable_dirty_rect(true);
|
|
printf("Dirty rectangle optimization enabled (4 quadrants: TL/TR/BL/BR split)\n");
|
|
}
|
|
|
|
// Launch Core 1 for display refresh handling
|
|
printf("Launching Core 1 for display refresh...\n");
|
|
multicore_launch_core1(core1_entry);
|
|
sleep_ms(100); // Give Core 1 time to start
|
|
|
|
// Do a full refresh with white screen first (removes ghosting on e-paper)
|
|
printf("Performing initial full refresh to white...\n");
|
|
display->clear(true); // Clear to white
|
|
|
|
// For e-paper, do a full refresh to ensure clean display
|
|
if (display->get_type() == DISPLAY_TYPE_EPAPER) {
|
|
LowLevelDisplayEPaper* epaper = static_cast<LowLevelDisplayEPaper*>(display);
|
|
epaper->full_refresh(); // Full refresh removes ghosting
|
|
printf("Full refresh complete\n");
|
|
} else {
|
|
refresh_screen(bit_buffer, display); // For TFT, just refresh normally
|
|
}
|
|
|
|
// Now clear to black for drawing
|
|
display->clear(false); // Clear to black
|
|
|
|
// Initialize renderer and GUI system
|
|
LowLevelRenderer renderer(bit_buffer, V_WIDTH, V_HEIGHT);
|
|
renderer.set_font(&font_homespun_obj);
|
|
LowLevelGUI gui = LowLevelGUI(&renderer, font_homespun_obj);
|
|
|
|
// Initialize touch screen using abstraction FIRST (before InputManager needs it)
|
|
touch = LowLevelTouch::create((TouchType)TOUCH_TYPE_SELECTED, V_WIDTH, V_HEIGHT,
|
|
TOUCH_SWAP_XY, TOUCH_INVERT_X, TOUCH_INVERT_Y);
|
|
|
|
if (touch) {
|
|
printf("Touch initialized successfully\n");
|
|
|
|
// Set up touch IRQ wake-up (InputManager handles active-touch sampling)
|
|
printf("Setting up touch interrupt callback...\n");
|
|
touch->set_interrupt_callback(touch_interrupt_handler);
|
|
printf("Touch interrupt enabled on INT pin (falling and rising edges)\n");
|
|
|
|
// Run communication test if available
|
|
// Note: Commented out as it may hang on some hardware configurations
|
|
printf("\nRunning touch reliability test...\n");
|
|
touch->test_communication();
|
|
printf("...\n");
|
|
} else {
|
|
printf("Touch initialization failed or not configured\n");
|
|
}
|
|
|
|
// Initialize game configuration
|
|
GameConfig config = {
|
|
.touch_debounce_ms = 10,
|
|
.button_debounce_ms = 20,
|
|
.enable_gestures = true,
|
|
.enable_continuous_draw = true,
|
|
.debug_verbose = false
|
|
};
|
|
|
|
// Create InputManager for processing inputs (touch must be initialized first!)
|
|
InputManager input_manager(touch, &config);
|
|
|
|
// Create GameLauncher
|
|
GameLauncher launcher(V_WIDTH, V_HEIGHT, &renderer, &gui, &input_manager);
|
|
|
|
// Create SerialUploader for rapid game iteration
|
|
SerialUploader serial_uploader(&launcher);
|
|
printf("Serial uploader initialized\n");
|
|
|
|
// Register available games
|
|
launcher.register_game("Tic-Tac-Toe", "Classic 2-player game",
|
|
[](uint16_t w, uint16_t h, LowLevelRenderer* r, LowLevelGUI* g, InputManager* im) -> Game* {
|
|
return new TicTacToeGame(w, h, r, g, im);
|
|
});
|
|
|
|
launcher.register_game("Monopoly", "Classic property trading game",
|
|
[](uint16_t w, uint16_t h, LowLevelRenderer* r, LowLevelGUI* g, InputManager* im) -> Game* {
|
|
// For Feather TFT (480x320), reduce width to 430 to make room for sidebar buttons
|
|
uint16_t game_w = (w == 480) ? 430 : w;
|
|
return new MonopolyGame(game_w, h, r, g, im);
|
|
});
|
|
|
|
launcher.register_game("Demo Game", "Simple test game",
|
|
[](uint16_t w, uint16_t h, LowLevelRenderer* r, LowLevelGUI* g, InputManager* im) -> Game* {
|
|
return new DemoGame(w, h, r, g, im);
|
|
});
|
|
|
|
// Initialize SD card and mount filesystem
|
|
printf("\nInitializing SD card...\n");
|
|
bool sd_available = sd_card_init_with_board_config();
|
|
if (sd_available) {
|
|
printf("SD card initialized successfully\n");
|
|
|
|
// Mount FatFS filesystem
|
|
static FATFS fs;
|
|
FRESULT res = f_mount(&fs, "0:", 1); // Mount drive 0 immediately
|
|
if (res == FR_OK) {
|
|
printf("FatFS mounted successfully\n");
|
|
|
|
// Register Lua games from SD card /games directory
|
|
printf("Scanning for Lua games on SD card...\n");
|
|
int lua_games_found = LuaGameLoader::register_all_games(&launcher);
|
|
printf("Found %d Lua game(s)\n", lua_games_found);
|
|
} else {
|
|
printf("FatFS mount failed (error %d) - SD card may not be formatted\n", res);
|
|
}
|
|
|
|
// Restore display SPI speed (SD card shares same SPI bus)
|
|
// SD card init sets SPI to 12.5 MHz, but display needs 32 MHz for fast refresh
|
|
spi_set_baudrate(DISPLAY_SPI_PORT, SPI_BAUDRATE);
|
|
printf("Display SPI speed restored to %d MHz\n", SPI_BAUDRATE / 1000000);
|
|
} else {
|
|
printf("SD card not available - skipping Lua game scan\n");
|
|
}
|
|
|
|
// Draw launcher menu
|
|
launcher.draw();
|
|
|
|
// Refresh the screen with the launcher menu (async on Core 1)
|
|
refresh_screen_async(bit_buffer, display);
|
|
printf("Initial screen refresh queued on Core 1\n");
|
|
|
|
#ifdef BUTTON_KEY0_PIN
|
|
// Initialize hardware buttons (e-ink board only)
|
|
printf("\nInitializing hardware buttons...\n");
|
|
|
|
// Initialize KEY0 button
|
|
gpio_init(BUTTON_KEY0_PIN);
|
|
gpio_set_dir(BUTTON_KEY0_PIN, GPIO_IN);
|
|
gpio_pull_up(BUTTON_KEY0_PIN); // Active LOW with pull-up
|
|
printf(" KEY0 initialized on GP%d (active LOW)\n", BUTTON_KEY0_PIN);
|
|
|
|
#ifdef BUTTON_KEY1_PIN
|
|
// Initialize KEY1 button
|
|
gpio_init(BUTTON_KEY1_PIN);
|
|
gpio_set_dir(BUTTON_KEY1_PIN, GPIO_IN);
|
|
gpio_pull_up(BUTTON_KEY1_PIN); // Active LOW with pull-up
|
|
printf(" KEY1 initialized on GP%d (active LOW)\n", BUTTON_KEY1_PIN);
|
|
#endif
|
|
|
|
// Enable interrupts on falling edge (button press)
|
|
gpio_set_irq_enabled_with_callback(BUTTON_KEY0_PIN,
|
|
GPIO_IRQ_EDGE_FALL,
|
|
true,
|
|
&button_interrupt_handler);
|
|
#ifdef BUTTON_KEY1_PIN
|
|
gpio_set_irq_enabled(BUTTON_KEY1_PIN, GPIO_IRQ_EDGE_FALL, true);
|
|
#endif
|
|
|
|
printf("Button interrupts enabled (falling edge = press)\n");
|
|
#endif
|
|
|
|
// ========================================================================
|
|
// REACTIVE GAME LOOP WITH DUAL-CORE REFRESH
|
|
// ========================================================================
|
|
// Core 0 (this loop): Handles input and game logic - stays responsive
|
|
// Core 1: Handles display refresh - can take 1-2 seconds for e-ink
|
|
//
|
|
// The loop primarily sleeps on __wfi(), and wakes to:
|
|
// 1. Process input (button or touch)
|
|
// 2. Update game state based on input
|
|
// 3. Queue refresh on Core 1 (non-blocking)
|
|
// While touch is active or a game needs frame ticks, the loop stays awake.
|
|
// This keeps Core 0 responsive even during slow e-ink refreshes
|
|
// ========================================================================
|
|
|
|
uint32_t last_touch_time = 0;
|
|
bool pending_refresh = false; // Track if we have a pending refresh
|
|
|
|
// Initialize last interaction time to current time
|
|
last_interaction_time = to_ms_since_boot(get_absolute_time());
|
|
// Set up repeating alarm to periodically check dimming status
|
|
// This wakes the CPU from __wfi() every DIM_CHECK_INTERVAL_MS
|
|
add_alarm_in_ms(DIM_CHECK_INTERVAL_MS, dim_check_alarm_callback, nullptr, true);
|
|
|
|
int sleep_option_index = 5; // Default: 10 min
|
|
apply_sleep_option(sleep_option_index);
|
|
|
|
if (display->get_type() == DISPLAY_TYPE_ST7796) {
|
|
if (sleep_timeout_ms == 0) {
|
|
printf("Power saving: auto sleep disabled\n");
|
|
} else {
|
|
printf("Power saving: Dim at %lus, Sleep at %lus\n",
|
|
(unsigned long)(dim_timeout_ms / 1000),
|
|
(unsigned long)(sleep_timeout_ms / 1000));
|
|
}
|
|
} else {
|
|
if (sleep_timeout_ms == 0) {
|
|
printf("Power saving: auto sleep disabled\n");
|
|
} else {
|
|
printf("Power saving: Sleep at %lus\n", (unsigned long)(sleep_timeout_ms / 1000));
|
|
}
|
|
}
|
|
printf("Dimming check timer set to %d seconds\n", DIM_CHECK_INTERVAL_MS / 1000);
|
|
|
|
printf("\nEntering reactive game loop (Core 0 - input & logic)\n");
|
|
printf("Display refreshes handled by Core 1\n");
|
|
printf("Frame rate limited to 30 FPS (33.3ms per frame)\n\n");
|
|
|
|
Game* current_game = nullptr;
|
|
uint32_t game_start_time = 0;
|
|
|
|
// Frame rate limiting (30 FPS = 33.33ms per frame)
|
|
const uint32_t TARGET_FRAME_TIME_MS = 33; // 1000ms / 30fps ≈ 33ms
|
|
uint32_t last_frame_time = 0;
|
|
|
|
bool needs_refresh = false; // Track if screen needs redraw
|
|
bool dirty_rect_opt_state = (display->get_type() == DISPLAY_TYPE_ST7796);
|
|
SceneStack scene_stack;
|
|
bool swipe_candidate_active = false;
|
|
int16_t swipe_start_x = 0;
|
|
int16_t swipe_start_y = 0;
|
|
int16_t swipe_last_x = 0;
|
|
int16_t swipe_last_y = 0;
|
|
|
|
while (1) {
|
|
// 0. Process serial uploads (for rapid game iteration)
|
|
serial_uploader.process(is_refresh_in_progress());
|
|
|
|
// If serial uploader wants to launch a game, wait until it's safe (no display refresh)
|
|
if (serial_uploader.wants_to_launch_game() && !is_refresh_in_progress()) {
|
|
// Safe to launch now - no SPI conflict with display
|
|
bool game_launched = serial_uploader.complete_launch();
|
|
if (game_launched) {
|
|
// A new game was uploaded and launched - trigger redraw
|
|
needs_refresh = true;
|
|
current_game = launcher.get_selected_game();
|
|
scene_stack.clear_to_launcher();
|
|
scene_stack.push(SceneId::GAME);
|
|
// Note: game is already initialized by select_game_by_name()
|
|
}
|
|
}
|
|
|
|
// Determine if we should sleep or stay awake for updates
|
|
bool stay_awake = false;
|
|
if (needs_refresh) stay_awake = true;
|
|
if (pending_refresh) stay_awake = true;
|
|
if (serial_uploader.wants_to_launch_game()) stay_awake = true; // Don't sleep while waiting to launch
|
|
if (touch_event_down) stay_awake = true; // Keep sampling while finger is down
|
|
if (last_touch_time != 0) stay_awake = true; // Keep sampling during active touch session
|
|
|
|
if (scene_stack.is(SceneId::GAME) && game_wants_frame_updates(launcher)) stay_awake = true;
|
|
|
|
if (!stay_awake) {
|
|
// Sleep until interrupt wakes us up (very power efficient!)
|
|
__wfi(); // Wait For Interrupt - CPU sleeps until any interrupt occurs
|
|
|
|
// Ignore unrelated interrupts (USB/background/timer noise).
|
|
// Only continue loop work when one of our wake sources is pending.
|
|
if (!has_pending_wake_source()) {
|
|
continue;
|
|
}
|
|
}
|
|
|
|
InputEvent input = {INPUT_NONE, 0, 0, 0, 0, 0, false};
|
|
|
|
// 1. Process button input first (higher priority)
|
|
input = input_manager.process_button_input();
|
|
|
|
// 2. Process touch input (if no button was pressed)
|
|
if (!input.valid) {
|
|
input = input_manager.process_touch_input(&last_touch_time);
|
|
// if debugging enabled, print touch event
|
|
if (input.valid && config.debug_verbose) {
|
|
printf("Touch Event: type=%d, x=%d, y=%d, pressure=%d, gesture=0x%02X\n",
|
|
input.type, input.x, input.y, input.pressure, input.gesture_code);
|
|
}
|
|
}
|
|
|
|
// 3. Process input based on current state
|
|
if (input.valid) {
|
|
// Record user interaction for dimming timer
|
|
record_user_interaction(display);
|
|
|
|
// if debugging enabled, print input event
|
|
if (config.debug_verbose) {
|
|
printf("Input Event: type=%d, x=%d, y=%d, gesture=0x%02X, button=%d, pressure=%d\n",
|
|
input.type, input.x, input.y, input.gesture_code,
|
|
input.button_id, input.pressure);
|
|
}
|
|
|
|
SceneId scene = scene_stack.current();
|
|
|
|
if (scene == SceneId::LAUNCHER) {
|
|
swipe_candidate_active = false;
|
|
|
|
// Wait for any active display refresh to finish before potentially loading a game (SD Card I/O)
|
|
// This prevents SPI bus conflicts between Core 0 (SD Card) and Core 1 (Display)
|
|
while (is_refresh_in_progress()) {
|
|
sleep_us(100);
|
|
}
|
|
|
|
bool game_selected = launcher.update(input);
|
|
if (game_selected) {
|
|
printf("Game launched successfully\n");
|
|
game_start_time = 0;
|
|
scene_stack.push(SceneId::GAME);
|
|
}
|
|
needs_refresh = true;
|
|
} else if (scene == SceneId::GAME) {
|
|
current_game = launcher.get_selected_game();
|
|
if (!current_game) {
|
|
scene_stack.clear_to_launcher();
|
|
needs_refresh = true;
|
|
} else {
|
|
bool consumed_by_scene = false;
|
|
|
|
// Swipe gesture candidate for opening menu (evaluated on touch release).
|
|
if (input.type == INPUT_TOUCH_DOWN &&
|
|
is_top_right_start(input.x, input.y, V_WIDTH, V_HEIGHT)) {
|
|
swipe_candidate_active = true;
|
|
swipe_start_x = input.x;
|
|
swipe_start_y = input.y;
|
|
swipe_last_x = input.x;
|
|
swipe_last_y = input.y;
|
|
consumed_by_scene = true;
|
|
}
|
|
|
|
if (swipe_candidate_active && (input.type == INPUT_TOUCH_MOVE || input.type == INPUT_TOUCH_UP)) {
|
|
consumed_by_scene = true;
|
|
if (input.type == INPUT_TOUCH_MOVE) {
|
|
swipe_last_x = input.x;
|
|
swipe_last_y = input.y;
|
|
}
|
|
if (input.type == INPUT_TOUCH_UP) {
|
|
if (is_open_menu_swipe(swipe_start_x, swipe_start_y, swipe_last_x, swipe_last_y, V_WIDTH, V_HEIGHT)) {
|
|
scene_stack.push(SceneId::IN_GAME_MENU);
|
|
needs_refresh = true;
|
|
printf("Opened in-game menu\n");
|
|
}
|
|
swipe_candidate_active = false;
|
|
}
|
|
}
|
|
|
|
if (!consumed_by_scene) {
|
|
needs_refresh = current_game->update(input) || needs_refresh;
|
|
|
|
if (current_game->wants_to_exit()) {
|
|
printf("Game requested exit - returning to launcher\n");
|
|
swipe_candidate_active = false;
|
|
launcher.reset();
|
|
scene_stack.clear_to_launcher();
|
|
needs_refresh = true;
|
|
if (display->get_type() == DISPLAY_TYPE_EPAPER) {
|
|
LowLevelDisplayEPaper* epaper = static_cast<LowLevelDisplayEPaper*>(display);
|
|
epaper->full_refresh();
|
|
}
|
|
}
|
|
|
|
if (input.type == INPUT_TOUCH_DOWN) {
|
|
if (game_start_time == 0) {
|
|
game_start_time = to_ms_since_boot(get_absolute_time());
|
|
}
|
|
} else if (input.type == INPUT_TOUCH_UP) {
|
|
uint32_t now = to_ms_since_boot(get_absolute_time());
|
|
if (game_start_time > 0 && (now - game_start_time) > 10000) {
|
|
printf("Long press detected - returning to launcher\n");
|
|
swipe_candidate_active = false;
|
|
launcher.reset();
|
|
scene_stack.clear_to_launcher();
|
|
needs_refresh = true;
|
|
if (display->get_type() == DISPLAY_TYPE_EPAPER) {
|
|
LowLevelDisplayEPaper* epaper = static_cast<LowLevelDisplayEPaper*>(display);
|
|
epaper->full_refresh();
|
|
}
|
|
}
|
|
game_start_time = 0;
|
|
}
|
|
}
|
|
}
|
|
} else if (scene == SceneId::IN_GAME_MENU) {
|
|
current_game = launcher.get_selected_game();
|
|
if (!current_game) {
|
|
launcher.return_to_menu();
|
|
scene_stack.clear_to_launcher();
|
|
needs_refresh = true;
|
|
} else {
|
|
const int menu_w = V_WIDTH - 40;
|
|
const int menu_h = 190;
|
|
const int menu_x = 20;
|
|
const int menu_y = (V_HEIGHT - menu_h) / 2;
|
|
const int row_h = 34;
|
|
const int row_x = menu_x + 16;
|
|
const int row_w = menu_w - 32;
|
|
const int row_start_y = menu_y + 48;
|
|
|
|
// Menu tap handling on TOUCH_DOWN because TOUCH_UP coordinates can be unreliable (often 0,0).
|
|
if (input.type == INPUT_TOUCH_DOWN) {
|
|
if (in_rect(input.x, input.y, row_x, row_start_y + (0 * row_h), row_w, row_h)) {
|
|
if (launcher.restart_selected_game()) {
|
|
scene_stack.pop(); // Back to GAME
|
|
needs_refresh = true;
|
|
printf("Restarted current game from global menu\n");
|
|
}
|
|
} else if (in_rect(input.x, input.y, row_x, row_start_y + (1 * row_h), row_w, row_h)) {
|
|
swipe_candidate_active = false;
|
|
launcher.return_to_menu();
|
|
scene_stack.clear_to_launcher();
|
|
needs_refresh = true;
|
|
printf("Returned to game launcher from global menu\n");
|
|
if (display->get_type() == DISPLAY_TYPE_EPAPER) {
|
|
LowLevelDisplayEPaper* epaper = static_cast<LowLevelDisplayEPaper*>(display);
|
|
epaper->full_refresh();
|
|
}
|
|
} else if (in_rect(input.x, input.y, row_x, row_start_y + (2 * row_h), row_w, row_h)) {
|
|
sleep_option_index = (sleep_option_index + 1) % kSleepOptionCount;
|
|
apply_sleep_option(sleep_option_index);
|
|
needs_refresh = true;
|
|
if (sleep_timeout_ms == 0) {
|
|
printf("Auto sleep disabled\n");
|
|
} else {
|
|
printf("Auto sleep set to %s\n", kSleepOptions[sleep_option_index].label);
|
|
}
|
|
} else if (!in_rect(input.x, input.y, menu_x, menu_y, menu_w, menu_h)) {
|
|
scene_stack.pop(); // Close menu, resume game scene
|
|
needs_refresh = true;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if (scene_stack.is(SceneId::GAME) && game_wants_frame_updates(launcher)) {
|
|
// No input, but check if game wants continuous updates
|
|
current_game = launcher.get_selected_game();
|
|
if (current_game) {
|
|
// Only send frame tick if we're ready to draw the next frame
|
|
if (!is_refresh_in_progress()) {
|
|
InputEvent frame_tick = {INPUT_FRAME_TICK, 0, 0, 0, 0, 0, true};
|
|
needs_refresh = current_game->update(frame_tick) || needs_refresh;
|
|
}
|
|
}
|
|
}
|
|
|
|
// 4. Redraw and queue async refresh on Core 1 (with 30 FPS limiting)
|
|
if (needs_refresh || pending_refresh) {
|
|
// Check frame rate limiting
|
|
uint32_t current_time = to_ms_since_boot(get_absolute_time());
|
|
uint32_t time_since_last_frame = current_time - last_frame_time;
|
|
|
|
// Only proceed if enough time has passed since last frame
|
|
if (time_since_last_frame >= TARGET_FRAME_TIME_MS) {
|
|
// Only draw if Core 1 is finished with the buffer
|
|
if (!is_refresh_in_progress()) {
|
|
// Update dirty rectangle optimization based on continuous updates
|
|
if (display->get_type() == DISPLAY_TYPE_ST7796) {
|
|
bool wants_opt = false;
|
|
if (scene_stack.is(SceneId::GAME)) {
|
|
Game* g = launcher.get_selected_game();
|
|
if (g && g->wants_frame_updates()) {
|
|
wants_opt = true;
|
|
}
|
|
}
|
|
|
|
if (dirty_rect_opt_state != wants_opt) {
|
|
LowLevelDisplayST7796* st7796_display = static_cast<LowLevelDisplayST7796*>(display);
|
|
st7796_display->enable_dirty_rect(wants_opt);
|
|
dirty_rect_opt_state = wants_opt;
|
|
}
|
|
}
|
|
|
|
// Clear buffer and redraw entire UI with updated state
|
|
memset(bit_buffer, 0, V_WIDTH * V_HEIGHT / 8);
|
|
// Reset renderer state so one scene/game cannot leak clip/text/font settings
|
|
// into subsequent scenes (e.g. Lua games using clip rects).
|
|
renderer.reset_clip_rect();
|
|
renderer.set_text_color(true);
|
|
renderer.set_font(&font_homespun_obj);
|
|
|
|
if (scene_stack.is(SceneId::LAUNCHER)) {
|
|
launcher.draw();
|
|
} else if (scene_stack.is(SceneId::GAME)) {
|
|
current_game = launcher.get_selected_game();
|
|
if (current_game) {
|
|
current_game->draw();
|
|
} else {
|
|
launcher.draw();
|
|
scene_stack.clear_to_launcher();
|
|
}
|
|
} else if (scene_stack.is(SceneId::IN_GAME_MENU)) {
|
|
current_game = launcher.get_selected_game();
|
|
if (current_game) {
|
|
current_game->draw();
|
|
draw_in_game_menu(&renderer, &gui, V_WIDTH, V_HEIGHT, kSleepOptions[sleep_option_index].label);
|
|
} else {
|
|
launcher.draw();
|
|
scene_stack.clear_to_launcher();
|
|
}
|
|
}
|
|
|
|
// TFT fix:
|
|
// Run refresh synchronously on Core 0 for ST7796/ST7789.
|
|
// We observed intermittent frozen screens with async Core 1 refresh
|
|
// after SD/Lua activity even though scene/game logic continued.
|
|
// E-paper keeps async refresh because its refresh latency is high.
|
|
bool refresh_started = false;
|
|
if (display->get_type() == DISPLAY_TYPE_ST7796 || display->get_type() == DISPLAY_TYPE_ST7789) {
|
|
refresh_screen(bit_buffer, display);
|
|
refresh_started = true;
|
|
} else {
|
|
refresh_started = refresh_screen_async(bit_buffer, display);
|
|
}
|
|
|
|
if (refresh_started) {
|
|
needs_refresh = false;
|
|
pending_refresh = false; // Refresh queued successfully
|
|
last_frame_time = current_time; // Update frame time
|
|
} else {
|
|
pending_refresh = true;
|
|
}
|
|
} else {
|
|
pending_refresh = true;
|
|
}
|
|
} else {
|
|
// Frame rate limit: skip this frame, wait for next opportunity
|
|
// Sleep for the remaining time to reach target frame time
|
|
uint32_t remaining_time = TARGET_FRAME_TIME_MS - time_since_last_frame;
|
|
if (remaining_time > 1) {
|
|
sleep_ms(remaining_time - 1); // -1 to account for overhead
|
|
}
|
|
}
|
|
}
|
|
|
|
// 5. Check if display should be dimmed due to inactivity
|
|
// This flag is set by timer alarm every DIM_CHECK_INTERVAL_MS
|
|
if (dim_check_flag) {
|
|
dim_check_flag = false;
|
|
check_and_apply_dimming(display);
|
|
}
|
|
}
|
|
|
|
return 0;
|
|
}
|