OreBolt-OS/modules/orebolt-proxalarm/radar_ui.c

370 lines
13 KiB
C
Executable File

/*
* 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 <string.h>
#include <math.h>
/* ------------------------------------------------------------------ */
/* 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;
}