/* * radar_ui.c -- LVGL radar presentation layer implementation * * SPDX-License-Identifier: GPL-2.0-or-later * * Renders the proximity alarm radar display. Uses an LVGL canvas * for the radar scope (direct pixel access for the grid, range rings, * blips, and sweep line) and LVGL labels for the status bar and * alarm text overlay. * * The radar circle is inscribed in the smaller LCD dimension minus * a bottom margin for the status bar. On the H2 (320x240) this * gives a circle of radius ~96px centered at (160, 108). */ #include "radar_ui.h" #include "h2_ui.h" #include "log_manager.h" #include #include /* ------------------------------------------------------------------ */ /* Derived layout values */ /* ------------------------------------------------------------------ */ /* Compute these from the LCD constants so we adapt to any panel. */ #define RADAR_CX (H2_LCD_WIDTH / 2) #define RADAR_CY ((H2_LCD_HEIGHT - RADAR_UI_MARGIN_BOTTOM) / 2) #define RADAR_MAX_R (RADAR_CY - 12) /* leave margin at top */ /* ------------------------------------------------------------------ */ /* Internal state */ /* ------------------------------------------------------------------ */ /* Canvas buffer: RGB565, one pixel per uint16_t */ static uint16_t canvas_buf[H2_LCD_WIDTH * H2_LCD_HEIGHT]; static lv_obj_t *canvas = NULL; static lv_obj_t *lbl_status = NULL; static lv_obj_t *lbl_alert = NULL; static lv_obj_t *lbl_sens = NULL; static lv_obj_t *lbl_source = NULL; static int sweep_angle = 0; static int alarm_flash = 0; static lv_timer_t *sweep_timer = NULL; static lv_timer_t *flash_timer = NULL; /* Pre-computed grid-to-pixel scale factors */ static int grid_scale_x = 1; static int grid_scale_y = 1; /* Colors (RGB565) */ #define C_BG 0x0000 #define C_GRID 0x2104 /* dark green-gray */ #define C_RING 0x4208 /* range ring color */ #define C_SWEEP 0x07E0 /* bright green sweep line */ #define C_SWEEP_DIM 0x0340 /* dim green (sweep trail) */ #define C_BLIP_NEAR 0xF800 /* red: alarm zone */ #define C_BLIP_MID 0xFD20 /* orange: tracking zone */ #define C_BLIP_FAR 0x07E0 /* green: detection zone */ #define C_CENTER 0xF800 /* red center dot */ #define C_TEXT 0x18E0 /* dim teal for labels */ #define C_ALARM_BG 0xF800 /* red flash background */ /* ------------------------------------------------------------------ */ /* Helpers */ /* ------------------------------------------------------------------ */ static inline void px_set(int x, int y, uint16_t color) { if (x >= 0 && x < H2_LCD_WIDTH && y >= 0 && y < H2_LCD_HEIGHT) canvas_buf[y * H2_LCD_WIDTH + x] = color; } /* Bresenham line drawing */ static void draw_line(int x0, int y0, int x1, int y1, uint16_t color) { int dx = abs(x1 - x0), sx = x0 < x1 ? 1 : -1; int dy = -abs(y1 - y0), sy = y0 < y1 ? 1 : -1; int err = dx + dy, e2; while (1) { px_set(x0, y0, color); if (x0 == x1 && y0 == y1) break; e2 = 2 * err; if (e2 >= dy) { err += dy; x0 += sx; } if (e2 <= dx) { err += dx; y0 += sy; } } } /* Circle using midpoint algorithm */ static void draw_circle(int cx, int cy, int r, uint16_t color) { int x = r, y = 0, d = 1 - r; while (x >= y) { px_set(cx + x, cy + y, color); px_set(cx - x, cy + y, color); px_set(cx + x, cy - y, color); px_set(cx - x, cy - y, color); px_set(cx + y, cy + x, color); px_set(cx - y, cy + x, color); px_set(cx + y, cy - x, color); px_set(cx - y, cy - x, color); y++; if (d <= 0) { d += 2 * y + 1; } else { x--; d += 2 * (y - x) + 1; } } } /* Filled circle for blips */ static void draw_blip(int cx, int cy, int r, uint16_t color) { for (int dy = -r; dy <= r; dy++) { for (int dx = -r; dx <= r; dx++) { if (dx * dx + dy * dy <= r * r) px_set(cx + dx, cy + dy, color); } } } /* Map proxvec grid coordinates to pixel coordinates */ static void grid_to_px(int gx, int gy, int *px_out, int *py_out) { int half = PROXVEC_GRID_SIZE / 2; int dx = gx - half; int dy = gy - half; *px_out = RADAR_CX + dx * RADAR_MAX_R / half; *py_out = RADAR_CY + dy * RADAR_MAX_R / half; } /* Map grid radius to pixel radius */ static int grid_r_to_px_r(int grid_r) { return grid_r * RADAR_MAX_R / (PROXVEC_GRID_SIZE / 2); } /* Get blip color based on alarm state and distance */ static uint16_t blip_color(const proxvec_target_t *t) { if (t->alarm) return C_BLIP_NEAR; if (t->radius <= grid_r_to_px_r(PROXVEC_ALARM_RADIUS) * 2) return C_BLIP_MID; return C_BLIP_FAR; } /* Get blip color for state machine */ static uint16_t state_color(track_state_t state) { switch (state) { case TRACK_APPROACHING: return C_BLIP_MID; case TRACK_NEW: return C_BLIP_FAR; default: return C_BLIP_FAR; } } /* ------------------------------------------------------------------ */ /* Sweep timer callback */ /* ------------------------------------------------------------------ */ static void sweep_cb(lv_timer_t *timer) { (void)timer; sweep_angle = (sweep_angle + RADAR_UI_SWEEP_SPEED) % 360; lv_obj_invalidate(canvas); } /* Flash timer callback -- toggles the alarm overlay */ static void flash_cb(lv_timer_t *timer) { (void)timer; alarm_flash = !alarm_flash; if (alarm_flash && lbl_alert) { lv_obj_clear_flag(lbl_alert, LV_OBJ_FLAG_HIDDEN); } else if (lbl_alert) { lv_obj_add_flag(lbl_alert, LV_OBJ_FLAG_HIDDEN); } } /* ------------------------------------------------------------------ */ /* Public API */ /* ------------------------------------------------------------------ */ void radar_ui_init(void) { lv_obj_t *scr = lv_scr_act(); lv_obj_set_style_bg_color(scr, COLOR_BG, LV_PART_MAIN); /* Create canvas (full screen) */ canvas = lv_canvas_create(scr); lv_canvas_set_buffer(canvas, canvas_buf, H2_LCD_WIDTH, H2_LCD_HEIGHT, LV_IMG_CF_TRUE_COLOR); lv_obj_align(canvas, LV_ALIGN_TOP_LEFT, 0, 0); /* Source label (top-left) */ lbl_source = lv_label_create(scr); lv_label_set_text(lbl_source, "BLE"); lv_obj_set_style_text_color(lbl_source, COLOR_MUTED, LV_PART_MAIN); lv_obj_set_style_text_font(lbl_source, &lv_font_montserrat_10, LV_PART_MAIN); lv_obj_align(lbl_source, LV_ALIGN_TOP_LEFT, 4, 2); /* Sensitivity label (top-right) */ lbl_sens = lv_label_create(scr); lv_label_set_text(lbl_sens, "NEAR: -50dBm"); lv_obj_set_style_text_color(lbl_sens, COLOR_MUTED, LV_PART_MAIN); lv_obj_set_style_text_font(lbl_sens, &lv_font_montserrat_10, LV_PART_MAIN); lv_obj_align(lbl_sens, LV_ALIGN_TOP_RIGHT, -4, 2); /* Status bar label (bottom) */ lbl_status = lv_label_create(scr); lv_label_set_text(lbl_status, "0 targets | 0 alarms"); lv_obj_set_style_bg_color(lbl_status, lv_color_make(10, 14, 20), LV_PART_MAIN); lv_obj_set_style_text_color(lbl_status, COLOR_MUTED, LV_PART_MAIN); lv_obj_set_style_text_font(lbl_status, &lv_font_montserrat_10, LV_PART_MAIN); lv_obj_align(lbl_status, LV_ALIGN_BOTTOM_MID, 0, 0); lv_obj_set_width(lbl_status, H2_LCD_WIDTH); /* Alarm overlay (hidden by default) */ lbl_alert = lv_label_create(scr); lv_label_set_text(lbl_alert, "!! ALERT !!"); lv_obj_set_style_text_color(lbl_alert, lv_color_make(255, 50, 50), LV_PART_MAIN); lv_obj_set_style_text_font(lbl_alert, &lv_font_montserrat_14, LV_PART_MAIN); lv_obj_align(lbl_alert, LV_ALIGN_CENTER, 0, -RADAR_MAX_R - 16); lv_obj_add_flag(lbl_alert, LV_OBJ_FLAG_HIDDEN); /* Sweep animation timer (~50ms per tick = ~20fps) */ sweep_timer = lv_timer_create(sweep_cb, 50, NULL); /* Flash timer (disabled until alarm triggers) */ flash_timer = lv_timer_create(flash_cb, RADAR_UI_FLASH_PERIOD_MS / 2, NULL); lv_timer_pause(flash_timer); /* Compute grid-to-pixel scale */ grid_scale_x = RADAR_MAX_R / (PROXVEC_GRID_SIZE / 2); grid_scale_y = RADAR_MAX_R / (PROXVEC_GRID_SIZE / 2); LOG_INF("radar_ui: initialised (display %dx%d, radar r=%d)", H2_LCD_WIDTH, H2_LCD_HEIGHT, RADAR_MAX_R); } void radar_ui_refresh(const proxvec_target_t *targets, int count, int alarm_count) { /* Clear canvas */ memset(canvas_buf, 0, sizeof(canvas_buf)); /* Draw range rings */ for (int ring = 1; ring <= RADAR_UI_RING_COUNT; ring++) { int r = RADAR_MAX_R * ring / RADAR_UI_RING_COUNT; draw_circle(RADAR_CX, RADAR_CY, r, C_RING); } /* Draw crosshairs */ draw_line(RADAR_CX - RADAR_MAX_R, RADAR_CY, RADAR_CX + RADAR_MAX_R, RADAR_CY, C_GRID); draw_line(RADAR_CX, RADAR_CY - RADAR_MAX_R, RADAR_CX, RADAR_CY + RADAR_MAX_R, C_GRID); /* Draw sweep line */ { double rad = (double)sweep_angle * 3.14159265 / 180.0; int ex = RADAR_CX + (int)((double)RADAR_MAX_R * cos(rad)); int ey = RADAR_CY + (int)((double)RADAR_MAX_R * sin(rad)); draw_line(RADAR_CX, RADAR_CY, ex, ey, C_SWEEP); /* Draw a dim trail arc (~30 degrees behind sweep) */ for (int a = 5; a <= 30; a += 5) { double trail_rad = (double)(sweep_angle - a) * 3.14159265 / 180.0; int tx = RADAR_CX + (int)((double)RADAR_MAX_R * cos(trail_rad)); int ty = RADAR_CY + (int)((double)RADAR_MAX_R * sin(trail_rad)); draw_line(RADAR_CX, RADAR_CY, tx, ty, C_SWEEP_DIM); } } /* Draw alarm radius circle */ int alarm_px_r = grid_r_to_px_r(proxvec_get_alarm_radius()); draw_circle(RADAR_CX, RADAR_CY, alarm_px_r, C_BLIP_NEAR); /* Draw center dot (the holder) */ draw_blip(RADAR_CX, RADAR_CY, 3, C_CENTER); /* Draw target blips */ for (int i = 0; i < count; i++) { int px, py; grid_to_px(targets[i].x, targets[i].y, &px, &py); /* Blip size based on intensity */ int blip_r = 2 + targets[i].intensity / 80; if (blip_r > 6) blip_r = 6; uint16_t color = blip_color(&targets[i]); draw_blip(px, py, blip_r, color); /* Draw a small velocity vector for approaching targets */ if (targets[i].state == TRACK_APPROACHING && targets[i].velocity < -3) { /* Draw a line from blip toward center */ int dx = RADAR_CX - px; int dy = RADAR_CY - py; int len = (int)sqrt((double)(dx * dx + dy * dy)); if (len > 0) { int arrow_len = 12; int ax = px + dx * arrow_len / len; int ay = py + dy * arrow_len / len; draw_line(px, py, ax, ay, C_BLIP_NEAR); } } } /* Redraw alarm radius label at bottom of circle */ /* (Position is implicit from the circle) */ /* Update status label */ { char buf[48]; lv_snprintf(buf, sizeof(buf), "%d target%s | %d ALARM%s", count, count == 1 ? "" : "s", alarm_count, alarm_count == 1 ? "" : "S"); lv_label_set_text(lbl_status, buf); if (alarm_count > 0) { lv_obj_set_style_text_color(lbl_status, lv_color_make(255, 80, 80), LV_PART_MAIN); } else { lv_obj_set_style_text_color(lbl_status, COLOR_MUTED, LV_PART_MAIN); } } /* Mark canvas dirty */ lv_obj_invalidate(canvas); } void radar_ui_set_alarm_flash(int enabled) { if (enabled) { alarm_flash = 0; lv_timer_ready(flash_timer); lv_timer_resume(flash_timer); } else { lv_timer_pause(flash_timer); if (lbl_alert) lv_obj_add_flag(lbl_alert, LV_OBJ_FLAG_HIDDEN); alarm_flash = 0; } } void radar_ui_set_sensitivity_label(const char *text) { if (lbl_sens) lv_label_set_text(lbl_sens, text); } void radar_ui_set_source_label(const char *text) { if (lbl_source) lv_label_set_text(lbl_source, text); } void radar_ui_destroy(void) { if (sweep_timer) { lv_timer_del(sweep_timer); sweep_timer = NULL; } if (flash_timer) { lv_timer_del(flash_timer); flash_timer = NULL; } canvas = NULL; lbl_status = NULL; lbl_alert = NULL; lbl_sens = NULL; lbl_source = NULL; }