/* ============================================================================= * referee_ui.c - 裁判系统自定义UI组包与发送(版式见 referee_ui.h / hud_mock5.png) * * 组包: 每包 = ext_student_interactive_header_data_t{data_cmd_id, sender_ID, * receiver_ID} + N×15 字节图形数据,N 由 data_cmd_id 决定(1/2/5/7)。 * 图形名用 3 字符自增计数器(base36)保证唯一;首轮 ADD,之后 MODIFY。 * 发送: referee_send_frame(0x0301, payload, 6 + N*15) -> USART6 DMA * ==========================================================================*/ #include "referee_ui.h" #include "referee.h" #include "robot_param.h" #include /* 协议要求:图形数据恰好 15 字节(头 11 + 尾 4)。若 pack(1) 失效或位域被重排, * 这里直接编译失败,而不是把错位的图形发给选手端。 */ _Static_assert(sizeof(ui_head_t) == 11, "ui_head_t 必须是 11 字节"); _Static_assert(sizeof(ui_draw_t) == 15, "ui_draw_t 必须是 15 字节"); _Static_assert(sizeof(ui_num_t) == 15, "ui_num_t 必须是 15 字节"); _Static_assert(sizeof(ui_graphic_t) == 15, "ui_graphic_t 必须是 15 字节"); /* ---- 图形名生成:base26 三字符(17576 个组合,足够) ---- */ static uint16_t ui_name_seq = 0; static void ui_next_name(uint8_t name[3]) { uint16_t v = ui_name_seq++; name[0] = (uint8_t)('A' + (v / 676) % 26); name[1] = (uint8_t)('A' + (v / 26) % 26); name[2] = (uint8_t)('A' + v % 26); } /* ---- 打包缓冲 ---- */ #define UI_PAYLOAD_MAX (6 + UI_MAX_PER_PACKET * sizeof(ui_graphic_t)) static uint8_t ui_pkt[UI_PAYLOAD_MAX]; static uint8_t ui_pkt_n; /* 当前包里的图形个数 */ static uint8_t ui_op = UI_OP_ADD; /* 首轮 ADD,之后 MOD */ static uint8_t ui_ready = 0; /* 首轮是否已发完 */ static void ui_flush(void) { if (ui_pkt_n == 0) return; uint16_t cmd; switch (ui_pkt_n) { case 1: cmd = UI_CMD_1; break; case 2: cmd = UI_CMD_2; break; case 5: cmd = UI_CMD_5; break; case 7: cmd = UI_CMD_7; break; default: return; /* 协议只支持 1/2/5/7,不该发生 */ } /* header: data_cmd_id / sender_ID / receiver_ID,均小端 */ ui_pkt[0] = (uint8_t)(cmd & 0xFF); ui_pkt[1] = (uint8_t)(cmd >> 8); uint16_t sid = (uint16_t)referee.robot_id; ui_pkt[2] = (uint8_t)(sid & 0xFF); ui_pkt[3] = (uint8_t)(sid >> 8); ui_pkt[4] = (uint8_t)(UI_RECEIVER_ID & 0xFF); ui_pkt[5] = (uint8_t)(UI_RECEIVER_ID >> 8); referee_send_frame(UI_CMD_BASE, ui_pkt, (uint16_t)(6 + ui_pkt_n * sizeof(ui_graphic_t))); ui_pkt_n = 0; } static ui_graphic_t * ui_new(uint8_t type, uint8_t layer, uint8_t color) { if (ui_pkt_n >= UI_MAX_PER_PACKET) ui_flush(); ui_graphic_t * g = (ui_graphic_t *)&ui_pkt[6 + ui_pkt_n * sizeof(ui_graphic_t)]; memset(g, 0, sizeof(*g)); ui_next_name(g->d.h.name); /* d.h 与 n.h 同址 */ g->d.h.type = type; g->d.h.layer = layer; g->d.h.color = color; g->d.h.operate = ui_op; ui_pkt_n++; return g; } /* ---- 图元封装(绘制类写 d.*,数值类写 n.val) ---- */ static void ui_line(uint8_t layer, uint8_t color, int x0, int y0, int x1, int y1, int w) { ui_graphic_t * g = ui_new(UI_T_LINE, layer, color); g->d.h.x0 = (uint32_t)x0; g->d.h.y0 = (uint32_t)y0; g->d.x1 = (uint32_t)x1; g->d.y1 = (uint32_t)y1; g->d.h.width = (uint32_t)w; } static void ui_rect(uint8_t layer, uint8_t color, int x0, int y0, int x1, int y1) { ui_graphic_t * g = ui_new(UI_T_RECT, layer, color); g->d.h.x0 = (uint32_t)x0; g->d.h.y0 = (uint32_t)y0; g->d.x1 = (uint32_t)x1; g->d.y1 = (uint32_t)y1; } static void ui_fval(uint8_t layer, uint8_t color, int x, int y, float v, int digits) { ui_graphic_t * g = ui_new(UI_T_FLOAT, layer, color); g->n.h.x0 = (uint32_t)x; g->n.h.y0 = (uint32_t)y; g->n.h.width = (uint32_t)digits; /* 浮点:小数位数 */ g->n.val.f = v; } static void ui_ival(uint8_t layer, uint8_t color, int x, int y, int32_t v) { ui_graphic_t * g = ui_new(UI_T_INT, layer, color); g->n.h.x0 = (uint32_t)x; g->n.h.y0 = (uint32_t)y; g->n.val.i = v; } static void ui_char(uint8_t layer, uint8_t color, int x, int y, const char * s) { ui_graphic_t * g = ui_new(UI_T_CHAR, layer, color); g->n.h.x0 = (uint32_t)x; g->n.h.y0 = (uint32_t)y; g->n.h.width = (uint32_t)strlen(s); for (int i = 0; i < 4; i++) /* 最多 4 字节 ASCII */ g->n.val.s[i] = s[i]; } /* 角度归一到 [0,360) */ static float ui_norm360(float a) { while (a < 0.0f) a += 360.0f; while (a >= 360.0f) a -= 360.0f; return a; } void referee_ui_update(const ui_data_t * d) { if (d == NULL) return; /* 裁判系统离线:发一轮删除图层,然后停发(避免选手端残留最后一帧) */ if (!d->ref_online) { if (ui_ready) { ui_pkt_n = 0; ui_graphic_t * g = ui_new(UI_T_RECT, UI_L_FRAME, UI_C_BLACK); g->d.h.operate = UI_OP_DEL; g->d.h.x0 = 0; g->d.h.y0 = 0; g->d.x1 = 0; g->d.y1 = 0; ui_flush(); ui_ready = 0; ui_name_seq = 0; ui_op = UI_OP_ADD; } return; } /* ---------------- 左上:预估底盘功率 ---------------- */ uint8_t pw_col = (d->pwr_w > d->pwr_limit_w) ? UI_C_RED : (d->pwr_w > d->pwr_limit_w * 0.8f) ? UI_C_ORANGE : UI_C_GREEN; ui_rect(UI_L_FRAME, UI_C_CYAN, UI_PWR_X0, UI_PWR_Y0, UI_PWR_X1, UI_PWR_Y0 + 4); ui_char(UI_L_TEXT, UI_C_CYAN, UI_PWR_X0 + 20, UI_PWR_Y0 + 30, "PWR"); ui_fval(UI_L_TEXT, UI_C_WHITE, UI_PWR_X0 + 20, UI_PWR_Y0 + 64, d->pwr_w, 1); ui_char(UI_L_TEXT, UI_C_WHITE, UI_PWR_X0 + 210, UI_PWR_Y0 + 84, "W"); int pw_w_px = (int)((UI_PWR_X1 - UI_PWR_X0 - 40) * (d->pwr_w / (d->pwr_limit_w > 1.0f ? d->pwr_limit_w : 1.0f))); if (pw_w_px > UI_PWR_X1 - UI_PWR_X0 - 40) pw_w_px = UI_PWR_X1 - UI_PWR_X0 - 40; if (pw_w_px < 0) pw_w_px = 0; ui_rect(UI_L_GAUGE, pw_col, UI_PWR_X0 + 20, UI_PWR_Y0 + 120, UI_PWR_X0 + 20 + pw_w_px, UI_PWR_Y0 + 142); ui_line(UI_L_GAUGE, UI_C_WHITE, UI_PWR_X0 + 20 + (UI_PWR_X1 - UI_PWR_X0 - 40) - 3, UI_PWR_Y0 + 116, UI_PWR_X0 + 20 + (UI_PWR_X1 - UI_PWR_X0 - 40) - 3, UI_PWR_Y0 + 146, 2); /* ---------------- 左中:超级电容 ---------------- */ uint8_t cap_col = (d->cap_pct < 15) ? UI_C_RED : (d->cap_pct < 40) ? UI_C_ORANGE : UI_C_CYAN; ui_rect(UI_L_FRAME, cap_col, UI_CAP_X0, UI_CAP_Y0, UI_CAP_X1, UI_CAP_Y0 + 4); ui_char(UI_L_TEXT, cap_col, UI_CAP_X0 + 20, UI_CAP_Y0 + 30, "CAP"); ui_ival(UI_L_TEXT, UI_C_WHITE, UI_CAP_X0 + 20, UI_CAP_Y0 + 64, (int32_t)d->cap_pct); ui_char(UI_L_TEXT, UI_C_WHITE, UI_CAP_X0 + 130, UI_CAP_Y0 + 84, "%"); int cap_px = (int)((UI_CAP_X1 - UI_CAP_X0 - 40) * (d->cap_pct / 100.0f)); ui_rect(UI_L_GAUGE, cap_col, UI_CAP_X0 + 20, UI_CAP_Y0 + 118, UI_CAP_X0 + 20 + cap_px, UI_CAP_Y0 + 140); /* ---------------- 顶部:罗盘刻度带(底盘正方向朝向) ---------------- */ { float h = ui_norm360(d->hdg_deg); ui_rect(UI_L_FRAME, UI_C_CYAN, UI_HDG_X0, UI_HDG_Y0, UI_HDG_X1, UI_HDG_Y0 + 4); ui_line(UI_L_FRAME, UI_C_CYAN, UI_HDG_X0 + 20, UI_HDG_Y0 + 62, UI_HDG_X1 - 20, UI_HDG_Y0 + 62, 2); int cx = (UI_HDG_X0 + UI_HDG_X1) / 2; for (int k = -1; k <= 1; k++) /* 3 条刻度 */ { int x = cx + k * (UI_HDG_TAPE_HALF / 2); ui_line(UI_L_FRAME, UI_C_CYAN, x, UI_HDG_Y0 + 40, x, UI_HDG_Y0 + 62, k == 0 ? 3 : 2); } ui_ival(UI_L_TEXT, UI_C_CYAN, cx - UI_HDG_TAPE_HALF / 2 - 24, UI_HDG_Y0 + 18, (int32_t)ui_norm360(h - 30.0f)); ui_ival(UI_L_TEXT, UI_C_CYAN, cx + UI_HDG_TAPE_HALF / 2 - 24, UI_HDG_Y0 + 18, (int32_t)ui_norm360(h + 30.0f)); ui_line(UI_L_FRAME, UI_C_WHITE, cx, UI_HDG_Y0 + 62, cx - 8, UI_HDG_Y0 + 50, 2); ui_line(UI_L_FRAME, UI_C_WHITE, cx, UI_HDG_Y0 + 62, cx + 8, UI_HDG_Y0 + 50, 2); ui_char(UI_L_TEXT, UI_C_CYAN, UI_HDG_X0 + 24, UI_HDG_Y0 + 70, "HDG"); ui_ival(UI_L_TEXT, UI_C_WHITE, UI_HDG_X0 + 90, UI_HDG_Y0 + 66, (int32_t)h); ui_char(UI_L_TEXT, UI_C_CYAN, UI_HDG_X1 - 170, UI_HDG_Y0 + 70, "YAW"); ui_ival(UI_L_TEXT, UI_C_WHITE, UI_HDG_X1 - 104, UI_HDG_Y0 + 66, (int32_t)ui_norm360(d->yaw_deg)); } /* ---------------- 中央:俯仰梯 ---------------- */ ui_line(UI_L_FRAME, UI_C_WHITE, UI_LAD_CX - 44, UI_LAD_CY, UI_LAD_CX - 14, UI_LAD_CY, 4); ui_line(UI_L_FRAME, UI_C_WHITE, UI_LAD_CX + 14, UI_LAD_CY, UI_LAD_CX + 44, UI_LAD_CY, 4); for (int i = -2; i <= 2; i++) /* ±10/±5/0 五条梯线 */ { if (i == 0) continue; int v = i * 5; int yy = UI_LAD_CY - v * UI_LAD_PX_PER_DEG; int half = (i % 2 == 0) ? UI_LAD_HALF_LONG : UI_LAD_HALF_SHORT; ui_line(UI_L_GAUGE, UI_C_CYAN, UI_LAD_CX - half, yy, UI_LAD_CX - 18, yy, 2); ui_line(UI_L_GAUGE, UI_C_CYAN, UI_LAD_CX + 18, yy, UI_LAD_CX + half, yy, 2); } ui_ival(UI_L_TEXT, UI_C_CYAN, UI_LAD_CX - UI_LAD_HALF_SHORT - 40, UI_LAD_CY - 10 * UI_LAD_PX_PER_DEG - 10, 10); ui_ival(UI_L_TEXT, UI_C_CYAN, UI_LAD_CX + UI_LAD_HALF_SHORT + 12, UI_LAD_CY - 10 * UI_LAD_PX_PER_DEG - 10, 10); { int py = UI_LAD_CY - (int)(d->pitch_deg * UI_LAD_PX_PER_DEG); ui_line(UI_L_GAUGE, UI_C_ORANGE, UI_LAD_CX - UI_LAD_HALF_LONG - 20, py, UI_LAD_CX - UI_LAD_HALF_LONG - 4, py, 4); ui_char(UI_L_TEXT, UI_C_ORANGE, UI_LAD_CX - UI_LAD_HALF_LONG - 132, py - 10, "PIT"); ui_fval(UI_L_TEXT, UI_C_WHITE, UI_LAD_CX - UI_LAD_HALF_LONG - 76, py - 14, d->pitch_deg, 1); } /* ---------------- 右上:摩擦轮速度 ---------------- */ { uint8_t f_col = (d->fric_l_rpm > 1000.0f && d->fric_r_rpm > 1000.0f) ? UI_C_GREEN : UI_C_ORANGE; ui_rect(UI_L_FRAME, f_col, UI_FRIC_X0, UI_FRIC_Y0, UI_FRIC_X1, UI_FRIC_Y0 + 4); ui_char(UI_L_TEXT, UI_C_CYAN, UI_FRIC_X0 + 24, UI_FRIC_Y0 + 34, "FRIC"); ui_ival(UI_L_TEXT, UI_C_WHITE, UI_FRIC_X0 + 24, UI_FRIC_Y0 + 74, (int32_t)d->fric_l_rpm); ui_ival(UI_L_TEXT, UI_C_WHITE, UI_FRIC_X0 + 250, UI_FRIC_Y0 + 74, (int32_t)d->fric_r_rpm); ui_char(UI_L_TEXT, f_col, UI_FRIC_X0 + 24, UI_FRIC_Y1 - 60, (d->fric_l_rpm > 1000.0f && d->fric_r_rpm > 1000.0f) ? "ARMED" : "IDLE"); int bl = (int)((UI_FRIC_Y1 - UI_FRIC_Y0 - 140) * (d->fric_l_rpm / 7000.0f)); int br = (int)((UI_FRIC_Y1 - UI_FRIC_Y0 - 140) * (d->fric_r_rpm / 7000.0f)); if (bl < 0) bl = 0; if (bl > UI_FRIC_Y1 - UI_FRIC_Y0 - 140) bl = UI_FRIC_Y1 - UI_FRIC_Y0 - 140; if (br < 0) br = 0; if (br > UI_FRIC_Y1 - UI_FRIC_Y0 - 140) br = UI_FRIC_Y1 - UI_FRIC_Y0 - 140; ui_rect(UI_L_GAUGE, UI_C_GREEN, UI_FRIC_X0 + 30, UI_FRIC_Y1 - 40 - bl, UI_FRIC_X0 + 74, UI_FRIC_Y1 - 40); ui_rect(UI_L_GAUGE, UI_C_GREEN, UI_FRIC_X0 + 256, UI_FRIC_Y1 - 40 - br, UI_FRIC_X0 + 300, UI_FRIC_Y1 - 40); } /* ---------------- 底部:小陀螺状态 ---------------- */ ui_rect(UI_L_FRAME, d->spin_on ? UI_C_GREEN : UI_C_CYAN, UI_SPIN_X0, UI_SPIN_Y0, UI_SPIN_X1, UI_SPIN_Y0 + 4); ui_char(UI_L_TEXT, UI_C_CYAN, UI_SPIN_X0 + 24, UI_SPIN_Y0 + 34, "SPIN"); if (d->spin_on) ui_rect(UI_L_GAUGE, UI_C_GREEN, UI_SPIN_X0 + 160, UI_SPIN_Y0 + 26, UI_SPIN_X0 + 268, UI_SPIN_Y0 + 66); ui_char(UI_L_TEXT, d->spin_on ? UI_C_BLACK : UI_C_WHITE, UI_SPIN_X0 + 184, UI_SPIN_Y0 + 36, d->spin_on ? "ON" : "OFF"); /* 收尾:最后一包;首轮发完切换为 MODIFY */ ui_flush(); if (!ui_ready) { ui_ready = 1; ui_op = UI_OP_MOD; } }