// Copyright 2024 splitkb.com (support@splitkb.com) // SPDX-License-Identifier: GPL-2.0-or-later #include QMK_KEYBOARD_H #include "halcyon.h" #include "hlc_tft_display.h" #include "config.h" #include "qp.h" #include "qp_comms.h" #include "qp_surface.h" #include "qp_st77xx_opcodes.h" #include "split_util.h" #include // Fonts mono2 #include "graphics/fonts/Retron2000-27.qff.h" #include "graphics/fonts/Retron2000-underline-27.qff.h" // Numbers mono2 #include "graphics/numbers/0.qgf.h" #include "graphics/numbers/1.qgf.h" #include "graphics/numbers/2.qgf.h" #include "graphics/numbers/3.qgf.h" #include "graphics/numbers/4.qgf.h" #include "graphics/numbers/5.qgf.h" #include "graphics/numbers/6.qgf.h" #include "graphics/numbers/7.qgf.h" #include "graphics/numbers/8.qgf.h" #include "graphics/numbers/9.qgf.h" #include "graphics/numbers/undef.qgf.h" static const char *caps = "Caps"; static const char *num = "Num"; static const char *scroll = "Scroll"; static painter_font_handle_t Retron27; static painter_font_handle_t Retron27_underline; static painter_image_handle_t layer_number; backlight_config_t backlight_config; static uint16_t lcd_surface_fb[135*240]; bool backlight_off = false; int color_value = 0; led_t last_led_usb_state = {0}; layer_state_t last_layer_state = {0}; #define GRID_WIDTH 27 #define GRID_HEIGHT 48 #define CELL_SIZE 4 // Cell size excluding outline #define OUTLINE_SIZE 1 // Define the probability factor for initial alive cells #define INITIAL_ALIVE_PROBABILITY 0.2 // 20% chance of being alive bool grid[GRID_HEIGHT][GRID_WIDTH]; // Current state bool new_grid[GRID_HEIGHT][GRID_WIDTH]; // Next state bool changed_grid[GRID_HEIGHT][GRID_WIDTH]; // Tracks changed cells void init_grid() { // Initialize grid with alive cells for (int y = 0; y < GRID_HEIGHT; y++) { for (int x = 0; x < GRID_WIDTH; x++) { grid[y][x] = (rand() < INITIAL_ALIVE_PROBABILITY * RAND_MAX); // Use probability factor changed_grid[y][x] = true; // Mark all as changed initially } } } void draw_grid() { uint8_t hue = 0; // Hue for alive cells uint8_t sat = 0; // Saturation for alive cells uint8_t val_dead = 0; // Brightness for dead cells for (int y = 0; y < GRID_HEIGHT; y++) { for (int x = 0; x < GRID_WIDTH; x++) { if (changed_grid[y][x]) { // Only update changed cells uint16_t left = x * (CELL_SIZE + OUTLINE_SIZE); uint16_t top = y * (CELL_SIZE + OUTLINE_SIZE); uint16_t right = left + CELL_SIZE + OUTLINE_SIZE; uint16_t bottom = top + CELL_SIZE + OUTLINE_SIZE; // Draw the outline qp_rect(lcd_surface, left, top, right, bottom, hue, sat, val_dead, true); // Draw the filled cell inside the outline if it's alive if (grid[y][x]) { switch (color_value) { case 0: qp_rect(lcd_surface, left + OUTLINE_SIZE, top + OUTLINE_SIZE, right - OUTLINE_SIZE, bottom - OUTLINE_SIZE, HSV_LAYER_0, true); break; case 1: qp_rect(lcd_surface, left + OUTLINE_SIZE, top + OUTLINE_SIZE, right - OUTLINE_SIZE, bottom - OUTLINE_SIZE, HSV_LAYER_1, true); break; case 2: qp_rect(lcd_surface, left + OUTLINE_SIZE, top + OUTLINE_SIZE, right - OUTLINE_SIZE, bottom - OUTLINE_SIZE, HSV_LAYER_2, true); break; case 3: qp_rect(lcd_surface, left + OUTLINE_SIZE, top + OUTLINE_SIZE, right - OUTLINE_SIZE, bottom - OUTLINE_SIZE, HSV_LAYER_3, true); break; case 4: qp_rect(lcd_surface, left + OUTLINE_SIZE, top + OUTLINE_SIZE, right - OUTLINE_SIZE, bottom - OUTLINE_SIZE, HSV_LAYER_4, true); break; case 5: qp_rect(lcd_surface, left + OUTLINE_SIZE, top + OUTLINE_SIZE, right - OUTLINE_SIZE, bottom - OUTLINE_SIZE, HSV_LAYER_5, true); break; case 6: qp_rect(lcd_surface, left + OUTLINE_SIZE, top + OUTLINE_SIZE, right - OUTLINE_SIZE, bottom - OUTLINE_SIZE, HSV_LAYER_6, true); break; case 7: qp_rect(lcd_surface, left + OUTLINE_SIZE, top + OUTLINE_SIZE, right - OUTLINE_SIZE, bottom - OUTLINE_SIZE, HSV_LAYER_7, true); break; default: qp_rect(lcd_surface, left + OUTLINE_SIZE, top + OUTLINE_SIZE, right - OUTLINE_SIZE, bottom - OUTLINE_SIZE, HSV_LAYER_UNDEF, true); } } } } } } void update_grid() { for (int y = 0; y < GRID_HEIGHT; y++) { for (int x = 0; x < GRID_WIDTH; x++) { int alive_neighbors = 0; // Count alive neighbors for (int dy = -1; dy <= 1; dy++) { for (int dx = -1; dx <= 1; dx++) { if (dy == 0 && dx == 0) continue; // Skip the current cell int ny = y + dy; int nx = x + dx; if (ny >= 0 && ny < GRID_HEIGHT && nx >= 0 && nx < GRID_WIDTH) { alive_neighbors += grid[ny][nx]; } } } // Apply the rules of the Game of Life if (grid[y][x]) { // Any live cell with two or three live neighbours survives. new_grid[y][x] = (alive_neighbors == 2 || alive_neighbors == 3); } else { // Any dead cell with exactly three live neighbours becomes a live cell. new_grid[y][x] = (alive_neighbors == 3); } // Track changed cells changed_grid[y][x] = (grid[y][x] != new_grid[y][x]); } } // Copy new grid state to current grid for (int y = 0; y < GRID_HEIGHT; y++) { for (int x = 0; x < GRID_WIDTH; x++) { grid[y][x] = new_grid[y][x]; } } } // Function to add a cluster of cells at a random position void add_cell_cluster() { int cluster_size = 3; // Size of the cluster (3x3) int x = rand() % (GRID_WIDTH - cluster_size); int y = rand() % (GRID_HEIGHT - cluster_size); for (int dy = 0; dy < cluster_size; dy++) { for (int dx = 0; dx < cluster_size; dx++) { bool is_alive = rand() % 2; // Randomly choose between 0 and 1 grid[y + dy][x + dx] = is_alive; // Set the cell to be alive changed_grid[y + dy][x + dx] = true; // Mark the cell as changed } } } void update_display(void) { static bool first_run_led = false; static bool first_run_layer = false; if( first_run_layer == false) { // Load fonts Retron27 = qp_load_font_mem(font_Retron2000_27); Retron27_underline = qp_load_font_mem(font_Retron2000_underline_27); } if(last_led_usb_state.raw != host_keyboard_led_state().raw || first_run_led == false) { led_t led_usb_state = host_keyboard_led_state(); led_usb_state.caps_lock ? qp_drawtext_recolor(lcd_surface, 5, LCD_HEIGHT - Retron27->line_height * 3 - 15, Retron27_underline, caps, HSV_CAPS_ON, HSV_BLACK) : qp_drawtext_recolor(lcd_surface, 5, LCD_HEIGHT - Retron27->line_height * 3 - 15, Retron27, caps, HSV_CAPS_OFF, HSV_BLACK); led_usb_state.num_lock ? qp_drawtext_recolor(lcd_surface, 5, LCD_HEIGHT - Retron27->line_height * 2 - 10, Retron27_underline, num, HSV_NUM_ON, HSV_BLACK) : qp_drawtext_recolor(lcd_surface, 5, LCD_HEIGHT - Retron27->line_height * 2 - 10, Retron27, num, HSV_NUM_OFF, HSV_BLACK); led_usb_state.scroll_lock ? qp_drawtext_recolor(lcd_surface, 5, LCD_HEIGHT - Retron27->line_height - 5, Retron27_underline, scroll, HSV_SCROLL_ON, HSV_BLACK) : qp_drawtext_recolor(lcd_surface, 5, LCD_HEIGHT - Retron27->line_height - 5, Retron27, scroll, HSV_SCROLL_OFF, HSV_BLACK); last_led_usb_state = led_usb_state; first_run_led = true; } if(last_layer_state != layer_state || first_run_layer == false) { switch (get_highest_layer(layer_state|default_layer_state)) { case 0: layer_number = qp_load_image_mem(gfx_0); qp_drawimage_recolor(lcd_surface, 5, 5, layer_number, HSV_LAYER_0, HSV_BLACK); break; case 1: layer_number = qp_load_image_mem(gfx_1); qp_drawimage_recolor(lcd_surface, 5, 5, layer_number, HSV_LAYER_1, HSV_BLACK); break; case 2: layer_number = qp_load_image_mem(gfx_2); qp_drawimage_recolor(lcd_surface, 5, 5, layer_number, HSV_LAYER_2, HSV_BLACK); break; case 3: layer_number = qp_load_image_mem(gfx_3); qp_drawimage_recolor(lcd_surface, 5, 5, layer_number, HSV_LAYER_3, HSV_BLACK); break; case 4: layer_number = qp_load_image_mem(gfx_4); qp_drawimage_recolor(lcd_surface, 5, 5, layer_number, HSV_LAYER_4, HSV_BLACK); break; case 5: layer_number = qp_load_image_mem(gfx_5); qp_drawimage_recolor(lcd_surface, 5, 5, layer_number, HSV_LAYER_5, HSV_BLACK); break; case 6: layer_number = qp_load_image_mem(gfx_6); qp_drawimage_recolor(lcd_surface, 5, 5, layer_number, HSV_LAYER_6, HSV_BLACK); break; case 7: layer_number = qp_load_image_mem(gfx_7); qp_drawimage_recolor(lcd_surface, 5, 5, layer_number, HSV_LAYER_7, HSV_BLACK); break; default: layer_number = qp_load_image_mem(gfx_undef); qp_drawimage_recolor(lcd_surface, 5, 5, layer_number, HSV_LAYER_UNDEF, HSV_BLACK); } qp_close_image(layer_number); last_layer_state = layer_state; first_run_layer = true; } } // Quantum function void suspend_power_down_kb(void) { // backlight_suspend(); Disabled as it gives some weird behavior when rebooting the host qp_power(lcd, false); suspend_power_down_user(); } // Quantum function void suspend_wakeup_init_kb(void) { qp_power(lcd, true); // backlight_wakeup(); Disabled as it gives some weird behavior when rebooting the host suspend_wakeup_init_user(); } // Timeout handling void backlight_wakeup(void) { backlight_off = false; backlight_enable(); } // Timeout handling void backlight_suspend(void) { backlight_off = true; backlight_disable(); } // Called from halcyon.c bool module_post_init_kb(void) { setPinOutput(LCD_RST_PIN); writePinHigh(LCD_RST_PIN); // Initialise the LCD lcd = qp_st7789_make_spi_device(LCD_WIDTH, LCD_HEIGHT, LCD_CS_PIN, LCD_DC_PIN, LCD_RST_PIN, LCD_SPI_DIVISOR, LCD_SPI_MODE); qp_init(lcd, LCD_ROTATION); qp_set_viewport_offsets(lcd, LCD_OFFSET_X, LCD_OFFSET_Y); // Initialise surface lcd_surface = qp_make_rgb565_surface(LCD_WIDTH, LCD_HEIGHT, lcd_surface_fb); qp_init(lcd_surface, LCD_ROTATION); // Turn on the LCD and clear the display qp_power(lcd, true); qp_rect(lcd, 0, 0, LCD_WIDTH - 1, LCD_HEIGHT - 1, HSV_BLACK, true); qp_flush(lcd); // Turn on backlight backlight_enable(); if(!module_post_init_user()) { return false; } return true; } // Called from halcyon.c bool display_module_housekeeping_task_kb(bool second_display) { // Backlight feature if (backlight_off && last_input_activity_elapsed() <= QUANTUM_PAINTER_DISPLAY_TIMEOUT) { backlight_wakeup(); } if (!backlight_off && last_input_activity_elapsed() > QUANTUM_PAINTER_DISPLAY_TIMEOUT) { backlight_suspend(); } if(!display_module_housekeeping_task_user(second_display)) { return false; } if(second_display) { static uint32_t last_draw = 0; static bool second_display_set = false; static uint32_t previous_matrix_activity_time = 0; if(!second_display_set) { srand(time(NULL)); init_grid(); color_value = rand() % 8; second_display_set = true; } if (timer_elapsed32(last_draw) >= 100) { // Throttle to 10 fps draw_grid(); update_grid(); if (previous_matrix_activity_time != last_matrix_activity_time()) { color_value = rand() % 8; add_cell_cluster(); previous_matrix_activity_time = last_matrix_activity_time(); } last_draw = timer_read32(); } } // Update display information (layers, numlock, etc.) if(!second_display) { update_display(); } // Move surface to lcd qp_surface_draw(lcd_surface, lcd, 0, 0, 0); return true; }