/* ota_display.c — OTA notification/progress UI on GC9A01 (bd-1yr8) * * Renders OTA state overlaid on the 240×240 round HUD display: * - BADGE: small dot on top-right when update available (idle state) * - UPDATE SCREEN: version compare, Update Balance / Update IO / Update All * - PROGRESS: arc around display perimeter + % + status text * - ERROR: red banner + "RETRY" prompt * * The display_draw_* primitives must be provided by the GC9A01 driver. * Actual SPI driver implementation is in a separate driver bead. */ #include "ota_display.h" #include "gc9a01.h" #include "gitea_ota.h" #include "vesc_can.h" #include "version.h" #include "esp_log.h" #include "freertos/FreeRTOS.h" #include "freertos/task.h" #include #include static const char *TAG = "ota_disp"; /* Display centre and radius for the 240×240 GC9A01 */ #define CX 120 #define CY 120 #define RAD 110 /* "SAULT" at scale=4: 5 chars × 24px wide = 120px, 28px tall */ #define SAULT_SCALE 4 #define SAULT_X 60 /* (240 - 120) / 2 */ #define SAULT_Y 86 /* centre vertically */ /* Battery voltage line at scale=2: "XX.XV" ≤ 5 chars × 12px = 60px */ #define VBAT_SCALE 2 #define VBAT_Y 130 static bool s_hud_dirty = true; /* set true whenever OTA overlay was active */ /* ── Idle HUD: SAULT title + battery voltage ── */ static void draw_hud_idle(void) { if (s_hud_dirty) { /* Clear full screen on first entry after OTA overlay */ display_fill_rect(0, 0, 240, 240, COL_BG); display_draw_string_s(SAULT_X, SAULT_Y, "SAULT", COL_WHITE, COL_BG, SAULT_SCALE); s_hud_dirty = false; } /* Update battery voltage every tick */ uint16_t vbat_mv = (uint16_t)((int32_t)g_vesc[0].voltage_x10 * 100); char vbuf[16]; if (vbat_mv == 0) { snprintf(vbuf, sizeof(vbuf), "--.-V"); } else { snprintf(vbuf, sizeof(vbuf), "%2u.%uV", vbat_mv / 1000u, (vbat_mv % 1000u) / 100u); } int vx = CX - (int)(strlen(vbuf) * 6 * VBAT_SCALE / 2); display_fill_rect(vx - 2, VBAT_Y - 2, (int)(strlen(vbuf) * 6 * VBAT_SCALE) + 4, 7 * VBAT_SCALE + 4, COL_BG); display_draw_string_s(vx, VBAT_Y, vbuf, COL_GREEN, COL_BG, VBAT_SCALE); } /* ── Availability badge: 8×8 dot at top-right of display ── */ static void draw_badge(bool balance_avail, bool io_avail) { uint16_t col = (balance_avail || io_avail) ? COL_ORANGE : COL_BG; display_fill_rect(200, 15, 12, 12, col); } /* ── Progress arc: sweeps 0→360° proportional to progress% ── */ static void draw_progress_arc(uint8_t pct, uint16_t color) { int end_deg = (int)(360 * pct / 100); display_draw_arc(CX, CY, RAD, 0, end_deg, 6, color); } /* ── Status banner: 2 lines of text centred on display ── */ static void draw_status(const char *line1, const char *line2, uint16_t fg, uint16_t bg) { display_fill_rect(20, 90, 200, 60, bg); if (line1 && line1[0]) display_draw_string(CX - (int)(strlen(line1) * 6 / 2), 96, line1, fg, bg); if (line2 && line2[0]) display_draw_string(CX - (int)(strlen(line2) * 6 / 2), 116, line2, fg, bg); } /* ── Main render logic ── */ void ota_display_update(void) { /* Determine dominant OTA state */ ota_self_state_t self = g_ota_self_state; uart_ota_send_state_t io_s = g_uart_ota_state; switch (self) { case OTA_SELF_DOWNLOADING: case OTA_SELF_VERIFYING: case OTA_SELF_APPLYING: { s_hud_dirty = true; char pct_str[16]; snprintf(pct_str, sizeof(pct_str), "%d%%", g_ota_self_progress); const char *phase = (self == OTA_SELF_VERIFYING) ? "Verifying..." : (self == OTA_SELF_APPLYING) ? "Applying..." : "Downloading..."; draw_progress_arc(g_ota_self_progress, COL_BLUE); draw_status("Updating Balance", pct_str, COL_WHITE, COL_BG); ESP_LOGD(TAG, "balance OTA %s %d%%", phase, g_ota_self_progress); return; } case OTA_SELF_REBOOTING: s_hud_dirty = true; draw_status("Update complete", "Rebooting...", COL_GREEN, COL_BG); return; case OTA_SELF_FAILED: s_hud_dirty = true; draw_progress_arc(0, COL_RED); draw_status("Balance update", "FAILED RETRY?", COL_RED, COL_BG); return; default: break; } switch (io_s) { case UART_OTA_S_DOWNLOADING: s_hud_dirty = true; draw_progress_arc(g_uart_ota_progress, COL_YELLOW); draw_status("Downloading IO", "firmware...", COL_WHITE, COL_BG); return; case UART_OTA_S_SENDING: { s_hud_dirty = true; char pct_str[16]; snprintf(pct_str, sizeof(pct_str), "%d%%", g_uart_ota_progress); draw_progress_arc(g_uart_ota_progress, COL_YELLOW); draw_status("Updating IO", pct_str, COL_WHITE, COL_BG); return; } case UART_OTA_S_DONE: s_hud_dirty = true; draw_status("IO update done", "", COL_GREEN, COL_BG); return; case UART_OTA_S_FAILED: s_hud_dirty = true; draw_progress_arc(0, COL_RED); draw_status("IO update", "FAILED RETRY?", COL_RED, COL_BG); return; default: break; } /* Idle — draw SAULT HUD + badge */ bool bal_avail = g_balance_update.available; bool io_avail = g_io_update.available; draw_hud_idle(); draw_badge(bal_avail, io_avail); if (bal_avail || io_avail) { char verline[48]; if (bal_avail) { snprintf(verline, sizeof(verline), "Bal v%s rdy", g_balance_update.version); draw_status(verline, io_avail ? "IO update rdy" : "", COL_ORANGE, COL_BG); } else if (io_avail) { snprintf(verline, sizeof(verline), "IO v%s rdy", g_io_update.version); draw_status(verline, "", COL_ORANGE, COL_BG); } } } /* ── Background display task (5 Hz) ── */ static void ota_display_task(void *arg) { for (;;) { vTaskDelay(pdMS_TO_TICKS(200)); ota_display_update(); } } void ota_display_init(void) { xTaskCreate(ota_display_task, "ota_disp", 2048, NULL, 3, NULL); ESP_LOGI(TAG, "OTA display task started"); }