#include #include #include #include "display_cfg.h" #include "data.h" #include "ui.h" #include "ble.h" #include "power.h" #include "imu.h" #include "splash.h" #include "usage_rate.h" // Physical buttons (global, screen-independent): // BTN_BACK (GPIO 0) — left, send Space (Claude Code voice mode push-to-talk) // BTN_FWD (GPIO 18) — right, send Shift+Tab (Claude Code mode toggle) // AXP PWR (PMU) — middle, cycle screens; on splash, cycle animations #define BTN_BACK 0 #define BTN_FWD 18 // ---- Hardware objects ---- Arduino_DataBus *bus = new Arduino_ESP32QSPI( LCD_CS, LCD_SCLK, LCD_SDIO0, LCD_SDIO1, LCD_SDIO2, LCD_SDIO3); Arduino_CO5300 *gfx = new Arduino_CO5300( bus, LCD_RESET, 0 /* rotation */, LCD_WIDTH, LCD_HEIGHT, 0, 0, 0, 0); TouchDrvCST92xx touch; XPowersPMU pmu; SensorQMI8658 imu; static UsageData usage = {}; // ---- Touch interrupt + shared state ---- static volatile bool touch_pressed = false; static volatile uint16_t touch_x = 0; static volatile uint16_t touch_y = 0; static volatile bool touch_data_ready = false; static void IRAM_ATTR touch_isr(void) { touch_data_ready = true; } static void touch_read() { if (!touch_data_ready) return; touch_data_ready = false; int16_t tx[5], ty[5]; uint8_t n = touch.getPoint(tx, ty, touch.getSupportTouchPoint()); if (n > 0) { touch_pressed = true; touch_x = (uint16_t)tx[0]; touch_y = (uint16_t)ty[0]; } else { touch_pressed = false; } } // ---- LVGL draw buffers (PSRAM-backed, partial render) ---- #define BUF_LINES 40 static uint16_t *buf1 = nullptr; static uint16_t *buf2 = nullptr; // rot_buf for strip rotation — max size is 480×480 (full invalidation case) // but typical partial strips are much smaller static uint16_t *rot_buf = nullptr; // LVGL tick callback static uint32_t my_tick(void) { return millis(); } // Rotate a w×h strip and compute destination coordinates on the 480×480 display. // src pixels are in row-major order for the rectangle (sx, sy, w, h). // Output goes to rot_buf in row-major order for the destination rectangle. static void rotate_strip(const uint16_t *src, int32_t w, int32_t h, int32_t sx, int32_t sy, uint8_t r, int32_t *dx, int32_t *dy, int32_t *dw, int32_t *dh) { const int S = LCD_WIDTH; // 480 switch (r) { case 1: { // 90° CW: (x,y) -> (S-1-y, x) *dw = h; *dh = w; *dx = S - sy - h; *dy = sx; for (int32_t y = 0; y < h; y++) { for (int32_t x = 0; x < w; x++) { // src(x,y) -> dst(h-1-y, x) rot_buf[x * h + (h - 1 - y)] = src[y * w + x]; } } break; } case 2: { // 180°: (x,y) -> (S-1-x, S-1-y) *dw = w; *dh = h; *dx = S - sx - w; *dy = S - sy - h; for (int32_t y = 0; y < h; y++) { for (int32_t x = 0; x < w; x++) { rot_buf[(h - 1 - y) * w + (w - 1 - x)] = src[y * w + x]; } } break; } case 3: { // 270° CW: (x,y) -> (y, S-1-x) *dw = h; *dh = w; *dx = sy; *dy = S - sx - w; for (int32_t y = 0; y < h; y++) { for (int32_t x = 0; x < w; x++) { // src(x,y) -> dst(y, w-1-x) rot_buf[(w - 1 - x) * h + y] = src[y * w + x]; } } break; } default: *dx = sx; *dy = sy; *dw = w; *dh = h; break; } } // LVGL flush callback — rotates partial strips and writes to display static void my_flush_cb(lv_display_t* disp, const lv_area_t* area, uint8_t* px_map) { int32_t w = area->x2 - area->x1 + 1; int32_t h = area->y2 - area->y1 + 1; uint16_t *src = (uint16_t*)px_map; uint8_t r = imu_get_rotation(); if (r == 0) { gfx->draw16bitRGBBitmap(area->x1, area->y1, src, w, h); } else { int32_t dx, dy, dw, dh; rotate_strip(src, w, h, area->x1, area->y1, r, &dx, &dy, &dw, &dh); gfx->draw16bitRGBBitmap(dx, dy, rot_buf, dw, dh); } lv_display_flush_ready(disp); } // CO5300 requires even-aligned flush regions static void rounder_cb(lv_event_t* e) { lv_area_t *area = (lv_area_t*)lv_event_get_param(e); area->x1 = area->x1 & ~1; area->y1 = area->y1 & ~1; area->x2 = area->x2 | 1; area->y2 = area->y2 | 1; } // LVGL touch callback static void my_touch_cb(lv_indev_t* indev, lv_indev_data_t* data) { if (touch_pressed) { data->point.x = touch_x; data->point.y = touch_y; data->state = LV_INDEV_STATE_PRESSED; } else { data->state = LV_INDEV_STATE_RELEASED; } } // Parse a JSON line into UsageData static bool parse_json(const char* json, UsageData* out) { JsonDocument doc; DeserializationError err = deserializeJson(doc, json); if (err) { Serial.printf("JSON parse error: %s\n", err.c_str()); return false; } out->session_pct = doc["s"] | 0.0f; out->session_reset_mins = doc["sr"] | -1; out->weekly_pct = doc["w"] | 0.0f; out->weekly_reset_mins = doc["wr"] | -1; strlcpy(out->status, doc["st"] | "unknown", sizeof(out->status)); out->ok = doc["ok"] | false; out->valid = true; return true; } // Serial command buffer #define CMD_BUF_SIZE 64 static char cmd_buf[CMD_BUF_SIZE]; static int cmd_pos = 0; static void send_screenshot() { const uint32_t w = LCD_WIDTH, h = LCD_HEIGHT; const uint32_t row_bytes = w * 2; const uint32_t buf_size = row_bytes * h; uint8_t* sbuf = (uint8_t*)heap_caps_malloc(buf_size, MALLOC_CAP_SPIRAM); if (!sbuf) { Serial.println("SCREENSHOT_ERR"); return; } lv_draw_buf_t draw_buf; lv_draw_buf_init(&draw_buf, w, h, LV_COLOR_FORMAT_RGB565, row_bytes, sbuf, buf_size); lv_result_t res = lv_snapshot_take_to_draw_buf(lv_screen_active(), LV_COLOR_FORMAT_RGB565, &draw_buf); if (res != LV_RESULT_OK) { heap_caps_free(sbuf); Serial.println("SCREENSHOT_ERR"); return; } Serial.printf("SCREENSHOT_START %lu %lu %lu\n", (unsigned long)w, (unsigned long)h, (unsigned long)buf_size); Serial.flush(); Serial.write(sbuf, buf_size); Serial.flush(); Serial.println(); Serial.println("SCREENSHOT_END"); heap_caps_free(sbuf); } static void check_serial_cmd() { while (Serial.available()) { char c = Serial.read(); if (c == '\n' || c == '\r') { cmd_buf[cmd_pos] = '\0'; if (strcmp(cmd_buf, "screenshot") == 0) { send_screenshot(); } cmd_pos = 0; } else if (cmd_pos < CMD_BUF_SIZE - 1) { cmd_buf[cmd_pos++] = c; } } } void setup() { Serial.begin(115200); delay(300); Serial.println("{\"ready\":true}"); // Init I2C (shared by touch + PMU) Wire.begin(IIC_SDA, IIC_SCL); // Init display gfx->begin(); gfx->fillScreen(0x0000); gfx->setBrightness(200); // Init PMU power_init(); // Init IMU (accelerometer for auto-rotation) imu_init(); // Init touch touch.setPins(TP_RST, TP_INT); if (!touch.begin(Wire, CST9220_ADDR, IIC_SDA, IIC_SCL)) { Serial.println("Touch init failed"); } else { touch.setMaxCoordinates(LCD_WIDTH, LCD_HEIGHT); touch.setSwapXY(true); touch.setMirrorXY(true, false); attachInterrupt(TP_INT, touch_isr, FALLING); Serial.println("Touch init OK"); } // Init LVGL lv_init(); lv_tick_set_cb(my_tick); // Allocate PSRAM-backed partial render buffers buf1 = (uint16_t*)heap_caps_malloc(LCD_WIDTH * BUF_LINES * 2, MALLOC_CAP_SPIRAM); buf2 = (uint16_t*)heap_caps_malloc(LCD_WIDTH * BUF_LINES * 2, MALLOC_CAP_SPIRAM); // rot_buf needs to hold the largest possible strip after rotation // A 480×40 strip rotated 90° becomes 40×480, same pixel count rot_buf = (uint16_t*)heap_caps_malloc(LCD_WIDTH * BUF_LINES * 2, MALLOC_CAP_SPIRAM); lv_display_t* disp = lv_display_create(LCD_WIDTH, LCD_HEIGHT); lv_display_set_color_format(disp, LV_COLOR_FORMAT_RGB565); lv_display_set_flush_cb(disp, my_flush_cb); lv_display_set_buffers(disp, buf1, buf2, LCD_WIDTH * BUF_LINES * 2, LV_DISPLAY_RENDER_MODE_PARTIAL); // CO5300 even-alignment rounder lv_display_add_event_cb(disp, rounder_cb, LV_EVENT_INVALIDATE_AREA, NULL); lv_indev_t* indev = lv_indev_create(); lv_indev_set_type(indev, LV_INDEV_TYPE_POINTER); lv_indev_set_read_cb(indev, my_touch_cb); // Init BLE data channel ble_init(); // Physical buttons: back (GPIO 0) and forward (GPIO 18) pinMode(BTN_BACK, INPUT_PULLUP); pinMode(BTN_FWD, INPUT_PULLUP); // Build dashboard ui_init(); // Show initial BLE status on Bluetooth screen ui_update_ble_status(ble_get_state(), ble_get_device_name(), ble_get_mac_address()); // Show initial battery status ui_update_battery(power_battery_pct(), power_is_charging()); ui_show_screen(SCREEN_SPLASH); Serial.println("Dashboard ready, waiting for data on BLE..."); } static ble_state_t last_ble_state = BLE_STATE_INIT; // Brightness ramp state for rotation transition // On rotation change we blank the panel, force a full LVGL redraw at the // new orientation, then ramp brightness back up over ~125ms so the // transition reads as deliberate instead of as a glitch. static void handle_rotation_change(void) { static uint8_t last_rotation = 0; static uint8_t ramp_step = 0; // 0=idle, 1-4=ramping static uint32_t ramp_last = 0; uint8_t rot = imu_get_rotation(); if (rot != last_rotation) { gfx->setBrightness(0); last_rotation = rot; lv_obj_invalidate(lv_screen_active()); ramp_step = 1; return; } if (ramp_step == 0) return; uint32_t now = millis(); if (now - ramp_last < 25) return; ramp_last = now; static const uint8_t levels[] = {60, 120, 170, 200}; gfx->setBrightness(levels[ramp_step - 1]); if (ramp_step >= 4) ramp_step = 0; else ramp_step++; } void loop() { touch_read(); lv_timer_handler(); ui_tick_anim(); ble_tick(); power_tick(); imu_tick(); splash_tick(); // Three-button input (global, screen-independent): // LEFT (GPIO 0) → Space (voice-mode push-to-talk; press & release tracked) // RIGHT (GPIO 18) → Shift+Tab (Claude Code mode toggle) // PWR (AXP) → cycle screens; on splash, cycle animations { static bool back_was = false, fwd_was = false; bool back_now = (digitalRead(BTN_BACK) == LOW); bool fwd_now = (digitalRead(BTN_FWD) == LOW); if (back_now != back_was) { if (back_now) ble_keyboard_press(0x2C, 0); // HID Space, no mods else ble_keyboard_release(); back_was = back_now; } if (fwd_now != fwd_was) { if (fwd_now) ble_keyboard_press(0x2B, 0x02); // HID Tab + LEFT_SHIFT else ble_keyboard_release(); fwd_was = fwd_now; } if (power_pwr_pressed()) { if (ui_get_current_screen() == SCREEN_SPLASH) splash_next(); else ui_cycle_screen(); } } handle_rotation_change(); // Update BLE status on screen when state changes ble_state_t bs = ble_get_state(); if (bs != last_ble_state) { last_ble_state = bs; ui_update_ble_status(bs, ble_get_device_name(), ble_get_mac_address()); } // Update battery indicator static int last_pct = -2; static bool last_charging = false; int pct = power_battery_pct(); bool charging = power_is_charging(); if (pct != last_pct || charging != last_charging) { last_pct = pct; last_charging = charging; ui_update_battery(pct, charging); } // Check for serial commands (screenshot, etc.) check_serial_cmd(); // Process incoming BLE data if (ble_has_data()) { if (parse_json(ble_get_data(), &usage)) { int g_before = usage_rate_group(); usage_rate_sample(usage.session_pct); int g_after = usage_rate_group(); if (g_after != g_before) { Serial.printf("usage rate: group %d -> %d (s=%.2f%%)\n", g_before, g_after, usage.session_pct); if (splash_is_active()) splash_pick_for_current_rate(); } ui_update(&usage); ble_send_ack(); } else { ble_send_nack(); } } delay(5); }