#include <stdio.h> #include <string.h> #include "freertos/FreeRTOS.h" #include "freertos/task.h" #include "freertos/semphr.h" #include "esp_log.h" #include "esp_check.h" #include "driver/i2c_master.h" #include "ssd1306.h" #include "font8x8_basic.h" #include "display_task.h" // Now includes game_framework.h indirectly #include "game/jump_bird_game_input_queue.h" // Still needed for input queue, but could be abstracted further #define TAG "DISPLAY_TASK" #define I2C_SDA_GPIO 19 #define I2C_SCL_GPIO 18 #define OLED_I2C_ADDRESS 0x3C #define I2C_PORT I2C_NUM_1 #define I2C_SPEED_HZ 400000 // OLED_WIDTH, OLED_HEIGHT, SCORE_DISPLAY_HEIGHT etc. are now from game_framework.h #define N_DISPLAY_LINES (OLED_HEIGHT / 8) #define MAX_LINE_LENGTH (OLED_WIDTH / 8) + 1 SSD1306_t dev; SemaphoreHandle_t xDisplayMutex = NULL; static char display_lines[N_DISPLAY_LINES][MAX_LINE_LENGTH]; static bool dirty_lines[N_DISPLAY_LINES] = { true }; // Track which lines changed typedef enum { DISPLAY_STATE_INITIAL_MESSAGE, DISPLAY_STATE_GAME_RUNNING, DISPLAY_STATE_GAME_OVER, DISPLAY_STATE_OTHER_INFO } display_state_t; static display_state_t s_current_display_state = DISPLAY_STATE_INITIAL_MESSAGE; static int s_game_over_score = 0; static int s_displayed_score = -1; // Tracks the score currently drawn on the display // Pointer to the currently active game instance static const game_t *s_current_game = NULL; static esp_err_t display_i2c_ng_init(void) { i2c_master_bus_config_t bus_config = { .clk_source = I2C_CLK_SRC_DEFAULT, .i2c_port = I2C_PORT, .scl_io_num = I2C_SCL_GPIO, .sda_io_num = I2C_SDA_GPIO, .flags = { .enable_internal_pullup = true, } }; ESP_RETURN_ON_ERROR(i2c_new_master_bus(&bus_config, &dev._i2c_bus_handle), TAG, "Failed to create I2C bus"); i2c_device_config_t dev_cfg = { .dev_addr_length = I2C_ADDR_BIT_LEN_7, .device_address = OLED_I2C_ADDRESS, .scl_speed_hz = I2C_SPEED_HZ, }; ESP_RETURN_ON_ERROR(i2c_master_bus_add_device(dev._i2c_bus_handle, &dev_cfg, &dev._i2c_dev_handle), TAG, "Failed to add I2C OLED device"); return ESP_OK; } static void display_manager_task(void *pvParameters) { ESP_LOGI(TAG, "Display Manager Task started."); if (display_i2c_ng_init() != ESP_OK) { ESP_LOGE(TAG, "I2C init failed, halting task."); vTaskDelete(NULL); } ssd1306_init(&dev, OLED_WIDTH, OLED_HEIGHT); ssd1306_clear_screen(&dev, false); ssd1306_show_buffer(&dev); while (1) { if (xSemaphoreTake(xDisplayMutex, portMAX_DELAY) == pdTRUE) { switch (s_current_display_state) { case DISPLAY_STATE_INITIAL_MESSAGE: { bool jump_signal_peek = false; // Check if s_current_game is valid before trying to access its input queue if (s_current_game && s_current_game->get_input_queue && s_current_game->get_input_queue() != NULL && xQueuePeek(s_current_game->get_input_queue(), &jump_signal_peek, 0) == pdTRUE) { if (jump_signal_peek) { bool jump_signal_start = false; if (xQueueReceive(s_current_game->get_input_queue(), &jump_signal_start, 0) == pdTRUE) { if (jump_signal_start) { s_current_display_state = DISPLAY_STATE_GAME_RUNNING; if (s_current_game && s_current_game->init_game) { s_current_game->init_game(); } if (s_current_game && s_current_game->set_active) { s_current_game->set_active(true); } // No full screen clear here, game drawing will handle its area s_displayed_score = -1; // Reset score display tracker for new game } } } } for (int i = 0; i < N_DISPLAY_LINES; i++) { if (dirty_lines[i] && strlen(display_lines[i]) > 0) { ssd1306_display_text(&dev, i, display_lines[i], strlen(display_lines[i]), false); dirty_lines[i] = false; } } break; } case DISPLAY_STATE_GAME_RUNNING: { if (s_current_game && s_current_game->is_active && s_current_game->is_active()) { if (s_current_game->handle_input) { s_current_game->handle_input(); } bool game_over = false; if (s_current_game->update_game_state) { game_over = s_current_game->update_game_state(); } if (s_current_game->draw_game) { s_current_game->draw_game(&dev); // This now only draws game elements } // Update score display only if it has changed int current_game_score = 0; if (s_current_game && s_current_game->get_score) { current_game_score = s_current_game->get_score(); } if (current_game_score != s_displayed_score) { char score_str[MAX_LINE_LENGTH]; snprintf(score_str, sizeof(score_str), "Score: %d", current_game_score); // Clear the entire line 0 before drawing the new score to avoid artifacts ssd1306_display_text(&dev, 0, " ", strlen(" "), false); // Clear line 0 ssd1306_display_text(&dev, 0, score_str, strlen(score_str), false); s_displayed_score = current_game_score; } if (game_over) { s_game_over_score = current_game_score; // Use the final score s_current_display_state = DISPLAY_STATE_GAME_OVER; if (s_current_game && s_current_game->set_active) { s_current_game->set_active(false); } ssd1306_clear_screen(&dev, false); // Keep this clear for game over screen // Mark all lines dirty for the game over screen for (int i = 0; i < N_DISPLAY_LINES; i++) { dirty_lines[i] = true; } s_displayed_score = -1; // Reset score display tracker } } else { // This block handles cases where the game might become inactive externally // or if is_active() becomes false for other reasons. if (s_current_game && s_current_game->get_score) { s_game_over_score = s_current_game->get_score(); // Get final score } else { s_game_over_score = 0; // Default if no game or get_score not implemented } s_current_display_state = DISPLAY_STATE_GAME_OVER; ssd1306_clear_screen(&dev, false); // Keep this clear for game over screen for (int i = 0; i < N_DISPLAY_LINES; i++) { dirty_lines[i] = true; } s_displayed_score = -1; // Reset score display tracker } break; } case DISPLAY_STATE_GAME_OVER: { char score_display[MAX_LINE_LENGTH]; snprintf(score_display, sizeof(score_display), "Score: %d", s_game_over_score); const char *game_over_messages[] = { " GAME OVER! ", " ", score_display, " ", " Press button ", " to Restart ", NULL, NULL }; for (int i = 0; i < N_DISPLAY_LINES; i++) { if (game_over_messages[i] && dirty_lines[i]) { ssd1306_display_text(&dev, i, game_over_messages[i], strlen(game_over_messages[i]), false); dirty_lines[i] = false; } } bool jump_signal_restart = false; // Check if s_current_game is valid before trying to access its input queue if (s_current_game && s_current_game->get_input_queue && s_current_game->get_input_queue() != NULL && xQueueReceive(s_current_game->get_input_queue(), &jump_signal_restart, 0) == pdTRUE) { if (jump_signal_restart) { s_current_display_state = DISPLAY_STATE_GAME_RUNNING; if (s_current_game && s_current_game->init_game) { s_current_game->init_game(); } if (s_current_game && s_current_game->set_active) { s_current_game->set_active(true); } // No full screen clear here, game drawing will handle its area s_displayed_score = -1; // Reset score display tracker for new game } } break; } case DISPLAY_STATE_OTHER_INFO: { for (int i = 0; i < N_DISPLAY_LINES; i++) { if (dirty_lines[i] && strlen(display_lines[i]) > 0) { ssd1306_display_text(&dev, i, display_lines[i], strlen(display_lines[i]), false); dirty_lines[i] = false; } } break; } } ssd1306_show_buffer(&dev); // Push the entire buffer to the display xSemaphoreGive(xDisplayMutex); } vTaskDelay(pdMS_TO_TICKS(5)); } } void display_update_text(const char *lines[], size_t num_lines) { if (xDisplayMutex == NULL) return; if (xSemaphoreTake(xDisplayMutex, portMAX_DELAY) == pdTRUE) { for (size_t i = 0; i < N_DISPLAY_LINES; i++) { const char *new_line = (i < num_lines && lines[i]) ? lines[i] : ""; if (strncmp(display_lines[i], new_line, MAX_LINE_LENGTH) != 0) { strncpy(display_lines[i], new_line, MAX_LINE_LENGTH - 1); display_lines[i][MAX_LINE_LENGTH - 1] = '\0'; dirty_lines[i] = true; } } xSemaphoreGive(xDisplayMutex); } } // Updated function signature to accept a game instance esp_err_t start_display_task(const game_t *game_instance) { if (game_instance == NULL) { ESP_LOGE(TAG, "Game instance provided to start_display_task is NULL!"); return ESP_FAIL; } s_current_game = game_instance; // Store the game instance xDisplayMutex = xSemaphoreCreateMutex(); if (xDisplayMutex == NULL) return ESP_FAIL; xSemaphoreGive(xDisplayMutex); // Initialize the display with "Score: 0" const char *initial_lines[] = {"Score: 0"}; display_update_text(initial_lines, 1); if (xTaskCreate(display_manager_task, "Display_Manager_Task", 4096, NULL, 5, NULL) != pdPASS) { vSemaphoreDelete(xDisplayMutex); return ESP_FAIL; } return ESP_OK; }