Files
Simba-OmniInfantry/底盘/modules/comms/referee_ui.c
T
lyf 267a659242 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 一处校正。
2026-10-16 04:24:32 +08:00

244 lines
12 KiB
C
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/* =============================================================================
* 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;
}
}