Newer
Older
ESP32-RetroPlay / main / tasks / display_task.c
#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"
#include "game/jump_bird_game.h"
#include "game/jump_bird_game_input_queue.h" // For xJumpBirdGameInputQueue

#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

#define OLED_WIDTH 128
#define OLED_HEIGHT 64

#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; // New: Tracks the score currently drawn on the display

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;
                if (xJumpBirdGameInputQueue && xQueuePeek(xJumpBirdGameInputQueue, &jump_signal_peek, 0) == pdTRUE) {
                    if (jump_signal_peek) {
                        bool jump_signal_start = false;
                        xQueueReceive(xJumpBirdGameInputQueue, &jump_signal_start, 0);
                        if (jump_signal_start) {
                            s_current_display_state = DISPLAY_STATE_GAME_RUNNING;
                            jump_bird_init_game();
                            jump_bird_set_active(true);
                            // Removed ssd1306_clear_screen here.
                            // The score will be drawn due to s_displayed_score = -1,
                            // and jump_bird_draw_game will clear the game 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 (jump_bird_is_active()) {
                    jump_bird_handle_input();
                    bool game_over = jump_bird_update_game_state();
                    jump_bird_draw_game(&dev); // This now only draws game elements

                    // Update score display only if it has changed
                    int current_game_score = jump_bird_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;
                        jump_bird_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 jump_bird_is_active() becomes false for other reasons.
                    s_game_over_score = jump_bird_get_score(); // Get final score
                    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;
                if (xJumpBirdGameInputQueue && xQueueReceive(xJumpBirdGameInputQueue, &jump_signal_restart, 0) == pdTRUE) {
                    if (jump_signal_restart) {
                        s_current_display_state = DISPLAY_STATE_GAME_RUNNING;
                        jump_bird_init_game();
                        jump_bird_set_active(true);
                        // Removed ssd1306_clear_screen here.
                        // The score will be drawn due to s_displayed_score = -1,
                        // and jump_bird_draw_game will clear the game 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);
    }
}

esp_err_t start_display_task(void) {
    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;
}