Skip to content

Commit 9a33901

Browse files
committed
feat: HUD polish — annunciators, adaptive numpad, STATUSTEXT ticker
- Parse ULog logging messages ('L' type) into per-drone ring buffer - Color-coded STATUSTEXT ticker (N key toggle): severity colors from theme drone palette, gradient-faded edges, persists until pushed out - Console: warnings take over ticker zone above HUD bar - Tactical: warnings show in bottom-right notification panel - Adaptive numpad: 3x3 (≤9), 3x4 (10-12), 4x4 (13-16) with smaller font for two-digit numbers - Font scaling floors: minimum readable sizes at small windows, label alpha reduced for better contrast - Interpolation toast: I key now shows toast instead of printf - Annunciator system (hud_annunciators.c/h): - Console tab fade: per-drone color bar double-pulses on marker crossing - Gimbal ring bounce: pinned drone cell bounces on marker (tactical) - Radar droplet wave: two expanding rings from drone blip (tactical) - Ticker warning flash: background brightens, text inverts on arrival - Ring shake: gimbal cell oscillates on STATUSTEXT warning (tactical)
1 parent ef12090 commit 9a33901

19 files changed

Lines changed: 656 additions & 80 deletions

CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@ if(NOT BUILD_TESTING_ONLY)
5656
src/asset_path.c
5757
src/theme.c
5858
src/tactical_hud.c
59+
src/hud_annunciators.c
5960
)
6061

6162
target_include_directories(hawkeye PRIVATE

src/data_source.h

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,9 @@
55
#include <stdint.h>
66
#include "mavlink_receiver.h" // hil_state_t, home_position_t
77

8+
// Forward declaration for STATUSTEXT ring buffer (defined in ulog_replay.h)
9+
struct statustext_ring;
10+
811
// Flight mode change event for timeline markers
912
typedef struct {
1013
float time_s; // seconds from log start
@@ -30,6 +33,7 @@ typedef struct {
3033
float correlation; // Pearson r vs reference drone (NAN = N/A)
3134
float rmse; // RMS position error vs reference (m) (NAN = N/A)
3235
float time_offset_s; // alignment offset for display
36+
const struct statustext_ring *statustext; // STATUSTEXT ring (NULL for MAVLink)
3337
} playback_state_t;
3438

3539
typedef struct data_source data_source_t;

src/data_source_ulog.c

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ static void ulog_poll(data_source_t *ds, float dt) {
2828
ds->playback.time_offset_s = (float)ctx->time_offset_s;
2929
ds->playback.mode_changes = (const playback_mode_change_t *)ctx->mode_changes;
3030
ds->playback.mode_change_count = ctx->mode_change_count;
31+
ds->playback.statustext = &ctx->statustext;
3132

3233
// Update playback progress
3334
uint64_t range = ctx->parser.end_timestamp - ctx->parser.start_timestamp;

src/hud.c

Lines changed: 169 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,8 @@ void hud_init(hud_t *h) {
4343
printf("Warning: could not load fonts/JetBrainsMono-Medium.ttf, using default font\n");
4444
if (h->font_label.glyphCount == 0)
4545
printf("Warning: could not load fonts/Inter-Medium.ttf, using default font\n");
46+
47+
annunc_init(&h->annunciators);
4648
}
4749

4850
void hud_update(hud_t *h, uint64_t time_usec, bool connected, float dt) {
@@ -54,6 +56,13 @@ void hud_update(hud_t *h, uint64_t time_usec, bool connected, float dt) {
5456
// Tick toast timer
5557
if (h->toast_timer > 0.0f)
5658
h->toast_timer -= dt;
59+
// Tick annunciator timers
60+
annunc_update(&h->annunciators, dt);
61+
// Tick STATUSTEXT ticker flash timers (entries persist, only flash fades)
62+
for (int i = 0; i < h->ticker_count; i++) {
63+
if (h->ticker[i].timer > 0.0f)
64+
h->ticker[i].timer -= dt;
65+
}
5766
}
5867

5968
void hud_toast(hud_t *h, const char *text, float duration_s) {
@@ -70,15 +79,71 @@ void hud_toast_color(hud_t *h, const char *text, float duration_s, Color color)
7079
h->toast_color = color;
7180
}
7281

82+
static Color severity_color(uint8_t sev, const theme_t *theme) {
83+
// Source severity colors from the drone palette warm tones
84+
if (sev <= 2) return theme->drone_palette[2]; // critical — palette red/pink
85+
if (sev == 3) return theme->drone_palette[4]; // error — palette orange/warm
86+
if (sev == 4) return theme->hud_warn; // warning — theme warn color
87+
return theme->hud_dim; // info+
88+
}
89+
90+
void hud_feed_statustext(hud_t *h, const struct statustext_ring *ring, int drone_idx) {
91+
if (!ring || drone_idx < 0 || drone_idx >= 16) return;
92+
int last = h->ticker_consumed[drone_idx];
93+
if (ring->head == last) return; // no new entries
94+
95+
// Walk from last consumed to current head
96+
int count = ring->count;
97+
int head = ring->head;
98+
int start = last;
99+
// How many new entries?
100+
int new_count = head - start;
101+
if (new_count < 0) new_count += STATUSTEXT_RING_SIZE;
102+
if (new_count > count) new_count = count;
103+
104+
for (int n = 0; n < new_count; n++) {
105+
int idx = (start + n) % STATUSTEXT_RING_SIZE;
106+
const statustext_entry_t *e = &ring->entries[idx];
107+
// Severity may be raw (0-7) or ASCII ('0'-'7')
108+
uint8_t sev = e->severity;
109+
if (sev >= '0') sev -= '0';
110+
if (sev > 4) continue; // only WARNING or worse
111+
112+
// Shift existing ticker entries down
113+
if (h->ticker_count < HUD_TICKER_MAX) h->ticker_count++;
114+
for (int j = h->ticker_count - 1; j > 0; j--)
115+
h->ticker[j] = h->ticker[j - 1];
116+
117+
// Insert at top
118+
snprintf(h->ticker[0].text, sizeof(h->ticker[0].text), "%s", e->text);
119+
h->ticker[0].severity = sev;
120+
h->ticker[0].timer = 8.0f;
121+
h->ticker[0].total = 8.0f;
122+
h->ticker[0].drone_idx = drone_idx;
123+
124+
// Trigger annunciators
125+
annunc_trigger_ticker_flash(&h->annunciators, 0);
126+
annunc_trigger_ring_shake(&h->annunciators, drone_idx);
127+
}
128+
h->ticker_consumed[drone_idx] = head;
129+
}
73130

74131
static void draw_numpad(const hud_t *h, const vehicle_t *vehicles,
75132
const data_source_t *sources, int vehicle_count,
76133
int selected, float numpad_x, float numpad_y,
77-
Font font_label, float btn_size, float gap, float scale) {
134+
Font font_label, float btn_size, float gap, float scale,
135+
int *out_cols, int *out_rows) {
136+
// Dynamic grid: up to 3 cols, rows sized to fit vehicle_count
137+
int cols = (vehicle_count <= 9) ? 3 : (vehicle_count <= 12) ? 3 : 4;
138+
int rows = (vehicle_count <= 9) ? 3 : (vehicle_count <= 12) ? 4 : 4;
139+
int total_slots = cols * rows;
140+
if (out_cols) *out_cols = cols;
141+
if (out_rows) *out_rows = rows;
142+
78143
float fs = 12 * scale;
79-
for (int i = 0; i < 9; i++) {
80-
int row = i / 3;
81-
int col = i % 3;
144+
for (int i = 0; i < total_slots; i++) {
145+
int row = i / cols;
146+
int col = i % cols;
82147
float bx = numpad_x + col * (btn_size + gap);
83148
float by = numpad_y + row * (btn_size + gap);
84149
int veh_idx = i;
@@ -116,11 +181,13 @@ static void draw_numpad(const hud_t *h, const vehicle_t *vehicles,
116181
vehicles[veh_idx].color.b, 80});
117182
}
118183

119-
char nb[2] = { '1' + i, '\0' };
120-
Vector2 nw = MeasureTextEx(font_label, nb, fs, 0.5f);
184+
char nb[4];
185+
snprintf(nb, sizeof(nb), "%d", i + 1);
186+
float lfs = (i >= 9) ? fs * 0.8f : fs;
187+
Vector2 nw = MeasureTextEx(font_label, nb, lfs, 0.5f);
121188
DrawTextEx(font_label, nb,
122-
(Vector2){bx + btn_size/2 - nw.x/2, by + btn_size/2 - fs/2},
123-
fs, 0.5f, btn_text);
189+
(Vector2){bx + btn_size/2 - nw.x/2, by + btn_size/2 - lfs/2},
190+
lfs, 0.5f, btn_text);
124191
}
125192
}
126193

@@ -141,8 +208,13 @@ static void draw_secondary_row(const hud_t *h, const vehicle_t *pv, int pidx,
141208
(Vector2){(float)screen_w, (float)row_y},
142209
1.0f, (Color){255, 255, 255, 20});
143210

144-
// Color bar
145-
DrawRectangle(0, row_y, (int)(3 * scale), secondary_h, pv->color);
211+
// Color bar (fades on marker crossing for this drone)
212+
{
213+
float tab_a = annunc_tab_fade_alpha(&h->annunciators, pidx);
214+
Color tc = pv->color;
215+
tc.a = (unsigned char)(tc.a * tab_a);
216+
DrawRectangle(0, row_y, (int)(3 * scale), secondary_h, tc);
217+
}
146218

147219
// Vehicle number
148220
char vnum[4];
@@ -247,7 +319,7 @@ void hud_draw(const hud_t *h, const vehicle_t *vehicles,
247319
Color bg = theme->hud_bg;
248320
Color border = theme->hud_border;
249321
Color warn = theme->hud_warn;
250-
Color label_color = accent_dim;
322+
Color label_color = (Color){accent_dim.r, accent_dim.g, accent_dim.b, (unsigned char)(accent_dim.a * 0.7f)};
251323
Color value_color = theme->hud_value;
252324
Color dim_color = theme->hud_dim;
253325
Color climb_color = theme->hud_climb;
@@ -258,12 +330,12 @@ void hud_draw(const hud_t *h, const vehicle_t *vehicles,
258330
if (s < 1.0f) s = 1.0f;
259331

260332
// Scaled font sizes
261-
float fs_label = 16 * s;
262-
float fs_value = 23 * s;
263-
float fs_unit = 15 * s;
264-
float fs_dim = 15 * s;
265-
float fs_sec_label = 14 * s;
266-
float fs_sec_value = 18 * s;
333+
float fs_label = fmaxf(16 * s, 10.0f);
334+
float fs_value = fmaxf(23 * s, 14.0f);
335+
float fs_unit = fmaxf(15 * s, 9.0f);
336+
float fs_dim = fmaxf(15 * s, 9.0f);
337+
float fs_sec_label = fmaxf(14 * s, 9.0f);
338+
float fs_sec_value = fmaxf(18 * s, 12.0f);
267339

268340
// Dynamic bar height
269341
int primary_h = (int)(120 * s);
@@ -279,12 +351,70 @@ void hud_draw(const hud_t *h, const vehicle_t *vehicles,
279351
DrawRectangle(0, bar_y, screen_w, total_bar_h, bg);
280352
DrawLineEx((Vector2){0, (float)bar_y}, (Vector2){(float)screen_w, (float)bar_y}, 1.0f, border);
281353

282-
// Toast notification (fades in/out, always above other notices)
354+
// Ticker zone: STATUSTEXT warnings take priority, toast shows when no warnings
283355
float toast_h_used = 0.0f;
284-
if (h->toast_timer > 0.0f) {
285-
float toast_fs = 14 * s;
286-
Vector2 tw = MeasureTextEx(h->font_label, h->toast_text, toast_fs, 0.5f);
287-
// Fade: full opacity for most of duration, fade out in last 0.5s
356+
float ticker_fs = fmaxf(14 * s, 10.0f);
357+
float ticker_y = (float)bar_y - 8 * s;
358+
359+
if (h->ticker_count > 0 && h->show_notifications) {
360+
// STATUSTEXT warnings — take over the ticker zone
361+
for (int i = 0; i < h->ticker_count; i++) {
362+
float fade = 1.0f;
363+
if (h->ticker[i].timer < 1.0f)
364+
fade = h->ticker[i].timer;
365+
Color sc = severity_color(h->ticker[i].severity, theme);
366+
367+
char tag[8];
368+
snprintf(tag, sizeof(tag), "D%d", h->ticker[i].drone_idx + 1);
369+
Vector2 tag_w = MeasureTextEx(h->font_label, tag, ticker_fs * 0.8f, 0.5f);
370+
Vector2 msg_w = MeasureTextEx(h->font_label, h->ticker[i].text, ticker_fs, 0.5f);
371+
float total_w = tag_w.x + 8 * s + msg_w.x;
372+
float tx = (float)(screen_w / 2) - total_w / 2.0f;
373+
float ty = ticker_y - msg_w.y;
374+
375+
// Gradient background with faded edges
376+
float bg_pad = 40 * s;
377+
float flash_a = annunc_ticker_flash_alpha(&h->annunciators, i);
378+
float bg_alpha = fade * (25 + flash_a * 120);
379+
Color bg_c = (Color){sc.r, sc.g, sc.b, (unsigned char)bg_alpha};
380+
// Center solid, edges fade to transparent
381+
float bg_x = tx - bg_pad;
382+
float bg_w = total_w + bg_pad * 2;
383+
float bg_h = msg_w.y + 4 * s;
384+
float edge = bg_pad;
385+
// Left fade
386+
for (int px = 0; px < (int)edge; px++) {
387+
float a = (float)px / edge;
388+
Color fc = {bg_c.r, bg_c.g, bg_c.b, (unsigned char)(bg_c.a * a)};
389+
DrawRectangle((int)(bg_x + px), (int)ty, 1, (int)bg_h, fc);
390+
}
391+
// Center solid
392+
DrawRectangle((int)(bg_x + edge), (int)ty, (int)(bg_w - edge * 2), (int)bg_h, bg_c);
393+
// Right fade
394+
for (int px = 0; px < (int)edge; px++) {
395+
float a = 1.0f - (float)px / edge;
396+
Color fc = {bg_c.r, bg_c.g, bg_c.b, (unsigned char)(bg_c.a * a)};
397+
DrawRectangle((int)(bg_x + bg_w - edge + px), (int)ty, 1, (int)bg_h, fc);
398+
}
399+
400+
// Drone tag
401+
Color tag_c = (Color){sc.r, sc.g, sc.b, (unsigned char)(fade * 140)};
402+
DrawTextEx(h->font_label, tag, (Vector2){tx, ty + 1}, ticker_fs * 0.8f, 0.5f, tag_c);
403+
404+
// Message text — invert to black during flash
405+
sc.a = (unsigned char)(fade * 255);
406+
Color text_c = (flash_a > 0.3f)
407+
? (Color){0, 0, 0, (unsigned char)(fade * 255)}
408+
: sc;
409+
DrawTextEx(h->font_label, h->ticker[i].text,
410+
(Vector2){tx + tag_w.x + 8 * s, ty}, ticker_fs, 0.5f, text_c);
411+
412+
ticker_y = ty - 2 * s;
413+
}
414+
toast_h_used = (float)bar_y - 8 * s - ticker_y;
415+
} else if (h->toast_timer > 0.0f) {
416+
// Regular toast when no STATUSTEXT warnings active
417+
Vector2 tw = MeasureTextEx(h->font_label, h->toast_text, ticker_fs, 0.5f);
288418
float fade = 1.0f;
289419
if (h->toast_timer < 0.5f)
290420
fade = h->toast_timer / 0.5f;
@@ -294,7 +424,7 @@ void hud_draw(const hud_t *h, const vehicle_t *vehicles,
294424
float toast_y = (float)bar_y - tw.y - 8 * s;
295425
float toast_x = (float)(screen_w / 2) - tw.x / 2.0f;
296426
DrawTextEx(h->font_label, h->toast_text, (Vector2){toast_x, toast_y},
297-
toast_fs, 0.5f, toast_c);
427+
ticker_fs, 0.5f, toast_c);
298428
toast_h_used = tw.y + 8 * s;
299429
}
300430

@@ -333,8 +463,13 @@ void hud_draw(const hud_t *h, const vehicle_t *vehicles,
333463
// Offset the main HUD content below the transport row
334464
bar_y += transport_h;
335465

336-
// Primary color bar on left edge
337-
DrawRectangle(0, bar_y, (int)(3 * s), primary_h, v->color);
466+
// Primary color bar on left edge (fades on marker crossing)
467+
{
468+
float tab_a = annunc_tab_fade_alpha(&h->annunciators, selected);
469+
Color tab_c = v->color;
470+
tab_c.a = (unsigned char)(tab_c.a * tab_a);
471+
DrawRectangle(0, bar_y, (int)(3 * s), primary_h, tab_c);
472+
}
338473

339474
// Instruments -- centered vertically in primary area
340475
float inst_radius = INSTRUMENT_RADIUS * s;
@@ -356,7 +491,8 @@ void hud_draw(const hud_t *h, const vehicle_t *vehicles,
356491
float np_btn = NUMPAD_BTN_SIZE * s;
357492
float np_gap = NUMPAD_GAP * s;
358493
float status_x = (float)(screen_w - 16 * s - 110 * s);
359-
float numpad_total_w = 3 * (np_btn + np_gap) - np_gap;
494+
int np_cols = (vehicle_count <= 9) ? 3 : (vehicle_count <= 12) ? 3 : 4;
495+
float numpad_total_w = np_cols * (np_btn + np_gap) - np_gap;
360496
float numpad_x = status_x - 20 * s - numpad_total_w;
361497
float timer_x = numpad_x - 24 * s - 60 * s;
362498

@@ -401,15 +537,20 @@ void hud_draw(const hud_t *h, const vehicle_t *vehicles,
401537
.label_color = label_color, .value_color = value_color,
402538
.dim_color = dim_color, .warn = warn,
403539
.climb_color = climb_color, .connected_color = connected_color,
540+
.selected = selected,
404541
};
405542

406543
hud_draw_telemetry(h, v, &tlay);
407544

408545
// Numpad (only when vehicle_count > 1)
409546
if (vehicle_count > 1) {
410-
float np_y = bar_y + (primary_h / 2.0f) - (3 * (np_btn + np_gap)) / 2.0f;
547+
int np_r = (vehicle_count <= 9) ? 3 : 4;
548+
float np_grid_h = np_r * (np_btn + np_gap) - np_gap;
549+
float np_y = bar_y + (primary_h / 2.0f) - np_grid_h / 2.0f;
550+
int np_c_out, np_r_out;
411551
draw_numpad(h, vehicles, sources, vehicle_count, selected,
412-
numpad_x, np_y, h->font_label, np_btn, np_gap, s);
552+
numpad_x, np_y, h->font_label, np_btn, np_gap, s,
553+
&np_c_out, &np_r_out);
413554
}
414555

415556
hud_draw_status(h, v, &sources[selected], &tlay, ghost_mode);

src/hud.h

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
#include "data_source.h"
66
#include "theme.h"
77
#include <stdbool.h>
8+
#include "hud_annunciators.h"
89

910
#define HUD_MAX_PINNED 15
1011
#define HUD_MARKER_LABEL_MAX 48
@@ -46,6 +47,24 @@ typedef struct {
4647
float toast_timer; // seconds remaining (0 = hidden)
4748
float toast_total; // total duration for fade calc
4849
Color toast_color; // custom color (0,0,0,0 = use default)
50+
51+
// STATUSTEXT ticker (severity-colored warning messages)
52+
#define HUD_TICKER_MAX 4
53+
struct {
54+
char text[128];
55+
uint8_t severity;
56+
float timer;
57+
float total;
58+
int drone_idx;
59+
} ticker[4];
60+
int ticker_count;
61+
int ticker_consumed[16]; // per-drone: last consumed ring head
62+
63+
// STATUSTEXT notification toggle
64+
bool show_notifications;
65+
66+
// Annunciator animation state
67+
hud_annunciators_t annunciators;
4968
} hud_t;
5069

5170
void hud_init(hud_t *h);
@@ -62,6 +81,9 @@ void hud_cleanup(hud_t *h);
6281
void hud_toast(hud_t *h, const char *text, float duration_s);
6382
void hud_toast_color(hud_t *h, const char *text, float duration_s, Color color);
6483

84+
// Feed STATUSTEXT messages from a drone's ring buffer into the ticker.
85+
void hud_feed_statustext(hud_t *h, const struct statustext_ring *ring, int drone_idx);
86+
6587
// Returns the total height of the HUD bar in pixels (for layout by other panels).
6688
int hud_bar_height(const hud_t *h, int screen_h);
6789

0 commit comments

Comments
 (0)