feat(ui): 新增裁判系统自定义UI(0x0301)并打通键鼠→底盘链路

键鼠控制(此前 WASD 无反应):
- 云台 robot_cmd_task:0x0304 与 USART3 帧内键鼠合并为共享视图 keyboard_merged(0x0304 优先);
  键鼠模式由帧内键鼠动作进入,动云台左摇杆退出
- c_comm_task/c_comm:键鼠在控时底盘摇杆由 WASD+鼠标合成,改读共享视图(此前只认 0x0304)
- dr16/调试输出:新增帧内键鼠原始字节诊断(key_lo/key_hi/mouse_*/s1/frames)

自定义UI(底盘板经裁判系统 USART6 绘制到选手端):
- 新增 底盘/modules/comms/referee_ui.c|h:15 字节图形组包(带编译期断言),
  v5 卡片式 HUD = 俯仰梯(pitch) + 罗盘刻度带(底盘朝向) + 预估底盘功率 + 超级电容
  + 摩擦轮 L/R 转速 + 小陀螺状态,yaw 在刻度带右侧;5Hz 刷新,约 50 图元/8 包
- referee.c 新增 referee_send_frame()(cmd 0x0301 机器人交互数据,CRC8+CRC16,USART6 TX DMA)
- 新增 CToC 0x1B9:云台把 pitch 与摩擦轮转速带给底盘(底盘本地没有这两个量)
- 版式避开官方保留区:顶部比分条/左下小地图/右下战车模块/中部两块官方保留

验证:云台 Code=99540 RO=1764 RW=420 ZI=62572;底盘 Code=99452 RO=1928 RW=416 ZI=62584;均 0 错误 0 警告。
未做实机验证(当前无板/无图传);UI 协议常量(UI_RECEIVER_ID/UI_SEND_HZ)与版式坐标集中在 referee_ui.h 宏区,便于按协议 PDF 一处校正。
This commit is contained in:
lyf
2026-10-16 04:24:32 +08:00
parent 370d4519b5
commit 267a659242
21 changed files with 867 additions and 58 deletions
+243
View File
@@ -0,0 +1,243 @@
/* =============================================================================
* 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 <string.h>
/* 协议要求:图形数据恰好 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;
}
}