#include "ui.h" #include #include #include #include "lvgl.h" #define MAIN_WIDTH 610 #define SIDEBAR_WIDTH 190 #define RADAR_TOP 52 #define RADAR_HEIGHT 428 #define RADAR_CENTER_X 300 #define RADAR_CENTER_Y 420 #define RADAR_RADIUS 340 #define RADAR_MAX_MM 8000.0f #define DEGREES_TO_RADIANS 0.01745329251994329577f static const uint32_t COLOR_BACKGROUND = 0x071015; static const uint32_t COLOR_PANEL = 0x0D191F; static const uint32_t COLOR_CARD = 0x13232A; static const uint32_t COLOR_GRID = 0x1F4145; static const uint32_t COLOR_GRID_MAJOR = 0x2C6665; static const uint32_t COLOR_ACCENT = 0x4BE1C1; static const uint32_t COLOR_TEXT = 0xEAF3F2; static const uint32_t COLOR_MUTED = 0x70858A; static const uint32_t COLOR_DANGER = 0xFF6B6B; static const uint32_t TARGET_COLORS[3] = {0xFF6B6B, 0xFFB454, 0x52C9F3}; static ui_config_t s_config; static radar_snapshot_t s_snapshot; static lv_obj_t *s_radar; static lv_obj_t *s_status_dot; static lv_obj_t *s_status_label; static lv_obj_t *s_frame_age_label; static lv_obj_t *s_nearest_value; static lv_obj_t *s_empty_label; static lv_obj_t *s_sound_button; static lv_obj_t *s_sound_label; static lv_obj_t *s_brightness_slider; static lv_obj_t *s_target_rows[3]; static lv_obj_t *s_target_row_dots[3]; static lv_obj_t *s_target_row_labels[3]; static lv_obj_t *s_target_tags[3]; static bool s_sound_enabled; static lv_color_t color(uint32_t hex) { return lv_color_hex(hex); } static void remove_default_style(lv_obj_t *object) { lv_obj_remove_style_all(object); lv_obj_clear_flag(object, LV_OBJ_FLAG_SCROLLABLE); } static lv_obj_t *make_label(lv_obj_t *parent, const char *text, const lv_font_t *font, uint32_t text_color) { lv_obj_t *label = lv_label_create(parent); lv_label_set_text(label, text); lv_obj_set_style_text_font(label, font, 0); lv_obj_set_style_text_color(label, color(text_color), 0); return label; } static float distance_to_radius(float distance_mm) { if (distance_mm <= 0.0f) { return 0.0f; } if (distance_mm <= 2000.0f) { return (distance_mm / 2000.0f) * 0.50f * RADAR_RADIUS; } if (distance_mm <= 4000.0f) { return (0.50f + ((distance_mm - 2000.0f) / 2000.0f) * 0.25f) * RADAR_RADIUS; } if (distance_mm <= 6000.0f) { return (0.75f + ((distance_mm - 4000.0f) / 2000.0f) * 0.15f) * RADAR_RADIUS; } if (distance_mm >= RADAR_MAX_MM) { return RADAR_RADIUS; } return (0.90f + ((distance_mm - 6000.0f) / 2000.0f) * 0.10f) * RADAR_RADIUS; } static uint8_t target_color_index(uint32_t id) { return id == 0U ? 0U : (uint8_t)((id - 1U) % 3U); } static bool target_to_point(const radar_target_view_t *target, lv_coord_t origin_x, lv_coord_t origin_y, lv_point_t *point) { if (!target->valid || target->y_mm <= 0.0f) { return false; } const float distance = sqrtf(target->x_mm * target->x_mm + target->y_mm * target->y_mm); if (distance < 100.0f || distance > RADAR_MAX_MM) { return false; } const float angle = atan2f(target->x_mm, target->y_mm); const float radius = distance_to_radius(distance); point->x = origin_x + RADAR_CENTER_X + (lv_coord_t)lroundf(sinf(angle) * radius); point->y = origin_y + RADAR_CENTER_Y - (lv_coord_t)lroundf(cosf(angle) * radius); return true; } static void draw_line(lv_draw_ctx_t *ctx, lv_color_t line_color, lv_coord_t width, lv_point_t start, lv_point_t end) { lv_draw_line_dsc_t descriptor; lv_draw_line_dsc_init(&descriptor); descriptor.color = line_color; descriptor.width = width; descriptor.opa = LV_OPA_COVER; lv_draw_line(ctx, &descriptor, &start, &end); } static void draw_radar_event(lv_event_t *event) { if (lv_event_get_code(event) != LV_EVENT_DRAW_MAIN) { return; } lv_obj_t *object = lv_event_get_target(event); lv_draw_ctx_t *ctx = lv_event_get_draw_ctx(event); lv_area_t coordinates; lv_obj_get_coords(object, &coordinates); const lv_point_t center = { .x = coordinates.x1 + RADAR_CENTER_X, .y = coordinates.y1 + RADAR_CENTER_Y, }; const uint16_t distances[] = {2000, 4000, 6000, 8000}; for (size_t i = 0; i < 4; ++i) { lv_draw_arc_dsc_t arc; lv_draw_arc_dsc_init(&arc); arc.color = color(i == 3 ? COLOR_GRID_MAJOR : COLOR_GRID); arc.width = i == 3 ? 2 : 1; arc.opa = LV_OPA_COVER; lv_draw_arc(ctx, &arc, ¢er, (uint16_t)lroundf(distance_to_radius(distances[i])), 210, 330); } const int16_t angles[] = {-60, -30, 0, 30, 60}; for (size_t i = 0; i < 5; ++i) { const float radians = (float)angles[i] * DEGREES_TO_RADIANS; const lv_point_t edge = { .x = center.x + (lv_coord_t)lroundf(sinf(radians) * RADAR_RADIUS), .y = center.y - (lv_coord_t)lroundf(cosf(radians) * RADAR_RADIUS), }; draw_line(ctx, color(angles[i] == 0 ? COLOR_GRID_MAJOR : COLOR_GRID), angles[i] == 0 ? 2 : 1, center, edge); } lv_draw_rect_dsc_t sensor; lv_draw_rect_dsc_init(&sensor); sensor.radius = LV_RADIUS_CIRCLE; sensor.bg_color = color(s_snapshot.sensor_online ? COLOR_ACCENT : COLOR_MUTED); sensor.bg_opa = LV_OPA_COVER; const lv_area_t sensor_area = { .x1 = center.x - 6, .y1 = center.y - 6, .x2 = center.x + 6, .y2 = center.y + 6, }; lv_draw_rect(ctx, &sensor, &sensor_area); for (size_t i = 0; i < RADAR_SERVICE_MAX_TARGETS; ++i) { const radar_target_view_t *target = &s_snapshot.targets[i]; lv_point_t point; if (!target_to_point(target, coordinates.x1, coordinates.y1, &point)) { continue; } const lv_color_t target_color = color(TARGET_COLORS[target_color_index(target->id)]); lv_draw_rect_dsc_t halo; lv_draw_rect_dsc_init(&halo); halo.radius = LV_RADIUS_CIRCLE; halo.bg_opa = LV_OPA_TRANSP; halo.border_color = target_color; halo.border_opa = LV_OPA_COVER; halo.border_width = 2; const lv_area_t halo_area = { .x1 = point.x - 13, .y1 = point.y - 13, .x2 = point.x + 13, .y2 = point.y + 13, }; lv_draw_rect(ctx, &halo, &halo_area); lv_draw_rect_dsc_t dot; lv_draw_rect_dsc_init(&dot); dot.radius = LV_RADIUS_CIRCLE; dot.bg_color = target_color; dot.bg_opa = LV_OPA_COVER; const lv_area_t dot_area = { .x1 = point.x - 5, .y1 = point.y - 5, .x2 = point.x + 5, .y2 = point.y + 5, }; lv_draw_rect(ctx, &dot, &dot_area); } } static void update_sound_button(void) { lv_label_set_text(s_sound_label, s_sound_enabled ? "SOUND ON" : "SOUND OFF"); lv_obj_set_style_bg_color(s_sound_button, color(s_sound_enabled ? COLOR_ACCENT : COLOR_CARD), 0); lv_obj_set_style_text_color(s_sound_label, color(s_sound_enabled ? COLOR_BACKGROUND : COLOR_MUTED), 0); } static void sound_button_event(lv_event_t *event) { if (lv_event_get_code(event) != LV_EVENT_CLICKED) { return; } s_sound_enabled = !s_sound_enabled; update_sound_button(); if (s_config.sound_changed != NULL) { s_config.sound_changed(s_sound_enabled); } } static void brightness_event(lv_event_t *event) { const lv_event_code_t code = lv_event_get_code(event); if (code != LV_EVENT_VALUE_CHANGED && code != LV_EVENT_RELEASED) { return; } if (s_config.brightness_changed != NULL) { const uint8_t value = (uint8_t)lv_slider_get_value(s_brightness_slider); s_config.brightness_changed(value, code == LV_EVENT_RELEASED); } } static void create_header(lv_obj_t *root) { lv_obj_t *title = make_label(root, "RADICK", &lv_font_montserrat_24, COLOR_TEXT); lv_obj_set_pos(title, 22, 13); lv_obj_t *subtitle = make_label(root, "RD-03D / 24 GHz / 3 TARGET", &lv_font_montserrat_12, COLOR_MUTED); lv_obj_set_pos(subtitle, 140, 22); lv_obj_t *line = lv_obj_create(root); remove_default_style(line); lv_obj_set_size(line, MAIN_WIDTH - 40, 1); lv_obj_set_pos(line, 20, 50); lv_obj_set_style_bg_color(line, color(COLOR_GRID), 0); lv_obj_set_style_bg_opa(line, LV_OPA_COVER, 0); } static void create_radar(lv_obj_t *root) { s_radar = lv_obj_create(root); remove_default_style(s_radar); lv_obj_set_pos(s_radar, 0, RADAR_TOP); lv_obj_set_size(s_radar, MAIN_WIDTH, RADAR_HEIGHT); lv_obj_add_event_cb(s_radar, draw_radar_event, LV_EVENT_DRAW_MAIN, NULL); const uint16_t distances[] = {2000, 4000, 6000, 8000}; for (size_t i = 0; i < 4; ++i) { char text[8]; snprintf(text, sizeof(text), "%um", (unsigned)(distances[i] / 1000U)); lv_obj_t *label = make_label(s_radar, text, &lv_font_montserrat_12, i == 3 ? COLOR_GRID_MAJOR : COLOR_MUTED); lv_obj_set_pos(label, RADAR_CENTER_X + 7, RADAR_CENTER_Y - (lv_coord_t)distance_to_radius(distances[i]) - 8); } s_empty_label = make_label(s_radar, "WAITING FOR RADAR", &lv_font_montserrat_16, COLOR_MUTED); lv_obj_align(s_empty_label, LV_ALIGN_TOP_MID, 0, 32); for (size_t i = 0; i < RADAR_SERVICE_MAX_TARGETS; ++i) { s_target_tags[i] = make_label(s_radar, "", &lv_font_montserrat_12, COLOR_TEXT); lv_obj_set_style_bg_color(s_target_tags[i], color(COLOR_BACKGROUND), 0); lv_obj_set_style_bg_opa(s_target_tags[i], LV_OPA_70, 0); lv_obj_set_style_pad_hor(s_target_tags[i], 5, 0); lv_obj_set_style_pad_ver(s_target_tags[i], 3, 0); lv_obj_set_style_radius(s_target_tags[i], 4, 0); lv_obj_add_flag(s_target_tags[i], LV_OBJ_FLAG_HIDDEN); } } static lv_obj_t *create_card(lv_obj_t *parent, lv_coord_t y, lv_coord_t height) { lv_obj_t *card = lv_obj_create(parent); lv_obj_clear_flag(card, LV_OBJ_FLAG_SCROLLABLE); lv_obj_set_pos(card, 12, y); lv_obj_set_size(card, SIDEBAR_WIDTH - 24, height); lv_obj_set_style_bg_color(card, color(COLOR_CARD), 0); lv_obj_set_style_bg_opa(card, LV_OPA_COVER, 0); lv_obj_set_style_border_width(card, 0, 0); lv_obj_set_style_radius(card, 10, 0); lv_obj_set_style_pad_all(card, 0, 0); return card; } static void create_sidebar(lv_obj_t *root) { lv_obj_t *sidebar = lv_obj_create(root); remove_default_style(sidebar); lv_obj_set_pos(sidebar, MAIN_WIDTH, 0); lv_obj_set_size(sidebar, SIDEBAR_WIDTH, 480); lv_obj_set_style_bg_color(sidebar, color(COLOR_PANEL), 0); lv_obj_set_style_bg_opa(sidebar, LV_OPA_COVER, 0); s_status_dot = lv_obj_create(sidebar); remove_default_style(s_status_dot); lv_obj_set_size(s_status_dot, 9, 9); lv_obj_set_pos(s_status_dot, 14, 19); lv_obj_set_style_radius(s_status_dot, LV_RADIUS_CIRCLE, 0); lv_obj_set_style_bg_opa(s_status_dot, LV_OPA_COVER, 0); s_status_label = make_label(sidebar, "RADAR OFFLINE", &lv_font_montserrat_14, COLOR_MUTED); lv_obj_set_pos(s_status_label, 31, 14); s_frame_age_label = make_label(sidebar, "NO DATA", &lv_font_montserrat_12, COLOR_MUTED); lv_obj_set_pos(s_frame_age_label, 31, 31); lv_obj_t *nearest_card = create_card(sidebar, 58, 94); lv_obj_t *nearest_title = make_label(nearest_card, "NEAREST", &lv_font_montserrat_12, COLOR_MUTED); lv_obj_set_pos(nearest_title, 12, 10); s_nearest_value = make_label(nearest_card, "--.- m", &lv_font_montserrat_40, COLOR_TEXT); lv_obj_set_pos(s_nearest_value, 10, 33); lv_obj_t *targets_title = make_label(sidebar, "TARGETS", &lv_font_montserrat_12, COLOR_MUTED); lv_obj_set_pos(targets_title, 14, 167); for (size_t i = 0; i < 3; ++i) { s_target_rows[i] = create_card(sidebar, 186 + (lv_coord_t)i * 53, 45); s_target_row_dots[i] = lv_obj_create(s_target_rows[i]); remove_default_style(s_target_row_dots[i]); lv_obj_set_size(s_target_row_dots[i], 8, 8); lv_obj_set_pos(s_target_row_dots[i], 11, 18); lv_obj_set_style_radius(s_target_row_dots[i], LV_RADIUS_CIRCLE, 0); lv_obj_set_style_bg_color(s_target_row_dots[i], color(TARGET_COLORS[i]), 0); lv_obj_set_style_bg_opa(s_target_row_dots[i], LV_OPA_COVER, 0); s_target_row_labels[i] = make_label(s_target_rows[i], "--", &lv_font_montserrat_12, COLOR_MUTED); lv_obj_set_pos(s_target_row_labels[i], 29, 8); } s_sound_button = lv_btn_create(sidebar); lv_obj_set_pos(s_sound_button, 12, 351); lv_obj_set_size(s_sound_button, SIDEBAR_WIDTH - 24, 42); lv_obj_set_style_shadow_width(s_sound_button, 0, 0); lv_obj_set_style_border_width(s_sound_button, 0, 0); lv_obj_set_style_radius(s_sound_button, 9, 0); lv_obj_add_event_cb(s_sound_button, sound_button_event, LV_EVENT_CLICKED, NULL); s_sound_label = make_label(s_sound_button, "", &lv_font_montserrat_14, COLOR_TEXT); lv_obj_center(s_sound_label); update_sound_button(); lv_obj_t *brightness_title = make_label(sidebar, "BRIGHTNESS", &lv_font_montserrat_12, COLOR_MUTED); lv_obj_set_pos(brightness_title, 14, 411); s_brightness_slider = lv_slider_create(sidebar); lv_obj_set_pos(s_brightness_slider, 14, 442); lv_obj_set_size(s_brightness_slider, SIDEBAR_WIDTH - 28, 8); lv_slider_set_range(s_brightness_slider, 10, 100); lv_slider_set_value(s_brightness_slider, s_config.brightness_percent, LV_ANIM_OFF); lv_obj_set_style_bg_color(s_brightness_slider, color(COLOR_CARD), LV_PART_MAIN); lv_obj_set_style_bg_color(s_brightness_slider, color(COLOR_ACCENT), LV_PART_INDICATOR); lv_obj_set_style_bg_color(s_brightness_slider, color(COLOR_TEXT), LV_PART_KNOB); lv_obj_set_style_pad_all(s_brightness_slider, 6, LV_PART_KNOB); lv_obj_set_ext_click_area(s_brightness_slider, 12); lv_obj_add_event_cb(s_brightness_slider, brightness_event, LV_EVENT_VALUE_CHANGED, NULL); lv_obj_add_event_cb(s_brightness_slider, brightness_event, LV_EVENT_RELEASED, NULL); if (!s_config.touch_available) { lv_obj_t *touch_warning = make_label(sidebar, "TOUCH OFFLINE", &lv_font_montserrat_12, COLOR_DANGER); lv_obj_align(touch_warning, LV_ALIGN_BOTTOM_MID, 0, -5); } } void ui_init(const ui_config_t *config) { memset(&s_snapshot, 0, sizeof(s_snapshot)); if (config != NULL) { s_config = *config; } else { memset(&s_config, 0, sizeof(s_config)); s_config.brightness_percent = 75; } s_sound_enabled = s_config.sound_enabled; lv_obj_t *root = lv_scr_act(); lv_obj_clear_flag(root, LV_OBJ_FLAG_SCROLLABLE); lv_obj_set_style_bg_color(root, color(COLOR_BACKGROUND), 0); lv_obj_set_style_bg_opa(root, LV_OPA_COVER, 0); lv_obj_set_style_pad_all(root, 0, 0); create_header(root); create_radar(root); create_sidebar(root); } void ui_update(const radar_snapshot_t *snapshot) { if (snapshot == NULL || s_radar == NULL) { return; } const bool online = snapshot->sensor_online; const bool radar_changed = snapshot->frame_count != s_snapshot.frame_count || snapshot->target_count != s_snapshot.target_count || online != s_snapshot.sensor_online; s_snapshot = *snapshot; lv_obj_set_style_bg_color(s_status_dot, color(online ? COLOR_ACCENT : COLOR_DANGER), 0); lv_label_set_text(s_status_label, online ? "RADAR ONLINE" : "RADAR OFFLINE"); lv_obj_set_style_text_color(s_status_label, color(online ? COLOR_TEXT : COLOR_DANGER), 0); if (online) { char age_text[24]; snprintf(age_text, sizeof(age_text), "%lu FR / %lums", (unsigned long)snapshot->frame_count, (unsigned long)snapshot->last_frame_age_ms); lv_label_set_text(s_frame_age_label, age_text); } else { lv_label_set_text(s_frame_age_label, "NO SENSOR DATA"); } float nearest = INFINITY; for (size_t i = 0; i < RADAR_SERVICE_MAX_TARGETS; ++i) { const radar_target_view_t *target = &snapshot->targets[i]; if (target->valid) { const float distance = sqrtf(target->x_mm * target->x_mm + target->y_mm * target->y_mm); if (distance < nearest) { nearest = distance; } char row_text[44]; snprintf(row_text, sizeof(row_text), "T%lu %.2fm\n%+.0f cm/s", (unsigned long)target->id, distance / 1000.0f, target->speed_cm_s); lv_label_set_text(s_target_row_labels[i], row_text); const uint8_t color_index = target_color_index(target->id); lv_obj_set_style_text_color(s_target_row_labels[i], color(COLOR_TEXT), 0); lv_obj_set_style_bg_color(s_target_row_dots[i], color(TARGET_COLORS[color_index]), 0); lv_obj_clear_flag(s_target_row_dots[i], LV_OBJ_FLAG_HIDDEN); lv_point_t point; if (target_to_point(target, 0, 0, &point)) { char tag[24]; snprintf(tag, sizeof(tag), "T%lu %.1fm", (unsigned long)target->id, distance / 1000.0f); lv_label_set_text(s_target_tags[i], tag); lv_obj_set_style_text_color(s_target_tags[i], color(TARGET_COLORS[color_index]), 0); lv_coord_t tag_x = point.x + 15; if (tag_x > MAIN_WIDTH - 82) { tag_x = point.x - 78; } lv_coord_t tag_y = point.y - 12; if (tag_y < 0) { tag_y = 0; } lv_obj_set_pos(s_target_tags[i], tag_x, tag_y); lv_obj_clear_flag(s_target_tags[i], LV_OBJ_FLAG_HIDDEN); } else { lv_obj_add_flag(s_target_tags[i], LV_OBJ_FLAG_HIDDEN); } } else { lv_label_set_text(s_target_row_labels[i], "--"); lv_obj_set_style_text_color(s_target_row_labels[i], color(COLOR_MUTED), 0); lv_obj_add_flag(s_target_row_dots[i], LV_OBJ_FLAG_HIDDEN); lv_obj_add_flag(s_target_tags[i], LV_OBJ_FLAG_HIDDEN); } } if (isfinite(nearest)) { char nearest_text[16]; snprintf(nearest_text, sizeof(nearest_text), "%.1f m", nearest / 1000.0f); lv_label_set_text(s_nearest_value, nearest_text); } else { lv_label_set_text(s_nearest_value, "--.- m"); } if (!online) { lv_label_set_text(s_empty_label, "WAITING FOR RADAR"); lv_obj_clear_flag(s_empty_label, LV_OBJ_FLAG_HIDDEN); } else if (snapshot->target_count == 0U) { lv_label_set_text(s_empty_label, "AREA CLEAR"); lv_obj_clear_flag(s_empty_label, LV_OBJ_FLAG_HIDDEN); } else { lv_obj_add_flag(s_empty_label, LV_OBJ_FLAG_HIDDEN); } if (radar_changed) { lv_obj_invalidate(s_radar); } }