From 267a65924247f1787af8da28c921f703387b0a0a Mon Sep 17 00:00:00 2001 From: lyf <169361657@qq.com> Date: Fri, 16 Oct 2026 04:24:32 +0800 Subject: [PATCH] =?UTF-8?q?feat(ui):=20=E6=96=B0=E5=A2=9E=E8=A3=81?= =?UTF-8?q?=E5=88=A4=E7=B3=BB=E7=BB=9F=E8=87=AA=E5=AE=9A=E4=B9=89UI(0x0301?= =?UTF-8?q?)=E5=B9=B6=E6=89=93=E9=80=9A=E9=94=AE=E9=BC=A0=E2=86=92?= =?UTF-8?q?=E5=BA=95=E7=9B=98=E9=93=BE=E8=B7=AF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 键鼠控制(此前 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 一处校正。 --- 云台/Application/Task/Inc/robot_cmd_task.h | 9 + 云台/Application/Task/Src/c_comm_task.c | 40 ++-- 云台/Application/Task/Src/debug_task.c | 85 ++++++- 云台/Application/Task/Src/robot_cmd_task.c | 115 ++++++++-- 云台/Application/robot_param.h | 1 + 云台/MDK-ARM/Simba-ELCT-FrameWork.uvoptx | 9 +- 云台/modules/comms/c_comm.c | 20 ++ 云台/modules/comms/c_comm.h | 5 + 云台/modules/comms/dr16.c | 6 + 云台/modules/comms/dr16.h | 7 + 底盘/Application/Task/Src/debug_task.c | 61 +++++- 底盘/Application/Task/Src/referee_task.c | 34 +++ 底盘/Application/robot_param.h | 1 + 底盘/MDK-ARM/Simba-ELCT-FrameWork.uvoptx | 40 ++-- 底盘/MDK-ARM/Simba-ELCT-FrameWork.uvprojx | 5 + 底盘/modules/comms/c_comm.c | 22 ++ 底盘/modules/comms/c_comm.h | 12 + 底盘/modules/comms/referee.c | 45 ++++ 底盘/modules/comms/referee.h | 6 + 底盘/modules/comms/referee_ui.c | 243 +++++++++++++++++++++ 底盘/modules/comms/referee_ui.h | 159 ++++++++++++++ 21 files changed, 867 insertions(+), 58 deletions(-) create mode 100644 底盘/modules/comms/referee_ui.c create mode 100644 底盘/modules/comms/referee_ui.h diff --git a/云台/Application/Task/Inc/robot_cmd_task.h b/云台/Application/Task/Inc/robot_cmd_task.h index da0e910..fe41813 100644 --- a/云台/Application/Task/Inc/robot_cmd_task.h +++ b/云台/Application/Task/Inc/robot_cmd_task.h @@ -24,4 +24,13 @@ typedef struct extern keyboard_data_t keyboard_data; +/* ---- 键鼠来源合并视图(2026-10-16) ---- + * 客户端键鼠既可能从裁判系统 0x0304 进来,也可能从 USART3 那 18 字节帧进来 + * (官方 18 字节控制帧里就带鼠标与按键,由 dr16 解析成 dr16.mouse_rl / dr16.key_w 等)。 + * robot_cmd_task 每周期把两路合并到下面这个视图: + * 云台/发射控制 与 c_comm_task 的底盘摇杆合成 都读它, + * 避免两处各判一次出现"云台跟鼠标、底盘却还在收遥控器原始摇杆"。 */ +extern keyboard_data_t keyboard_merged; /* 合并后的键鼠数据(0x0304 优先) */ +extern uint8_t keyboard_merged_online; /* 1=键鼠在控(0x0304 在线 或 帧内键鼠有动作) */ + #endif diff --git a/云台/Application/Task/Src/c_comm_task.c b/云台/Application/Task/Src/c_comm_task.c index 585bf85..d4c5db6 100644 --- a/云台/Application/Task/Src/c_comm_task.c +++ b/云台/Application/Task/Src/c_comm_task.c @@ -33,28 +33,38 @@ void c_comm_task(void const * argument) for (;;) { #if defined(ROBOT_TYPE_GIMBAL) - uint8_t kb_online = (keyboard_data.last_tick != 0 && - HAL_GetTick() - keyboard_data.last_tick < DEVICE_OFFLINE_TIME_MS * 10); + /* 键鼠来源已由 robot_cmd_task 合并(0x0304 优先,否则 USART3 帧内键鼠)。 + * 直接用共享视图,保证"云台怎么控、底盘就怎么收"——否则 WASD 合成的 + * 底盘摇杆会因为这里只认 0x0304 而被整段跳过(客户端键鼠走帧内时平移失效)。 */ + uint8_t kb_online = keyboard_merged_online; uint8_t kb[12]; - kb[0] = (uint8_t)(keyboard_data.mouse_rl & 0xFF); - kb[1] = (uint8_t)((uint16_t)keyboard_data.mouse_rl >> 8); - kb[2] = (uint8_t)(keyboard_data.mouse_du & 0xFF); - kb[3] = (uint8_t)((uint16_t)keyboard_data.mouse_du >> 8); - kb[4] = (uint8_t)(keyboard_data.mouse_wheel & 0xFF); - kb[5] = (uint8_t)((uint16_t)keyboard_data.mouse_wheel >> 8); - kb[6] = keyboard_data.mouse_key_l; - kb[7] = keyboard_data.mouse_key_r; - kb[8] = (uint8_t)(keyboard_data.key_w | (keyboard_data.key_s << 1) | - (keyboard_data.key_a << 2) | (keyboard_data.key_d << 3) | - (keyboard_data.key_shift << 4) | (keyboard_data.key_ctrl << 5) | - (keyboard_data.key_q << 6) | (keyboard_data.key_e << 7)); - kb[9] = (uint8_t)(keyboard_data.key_alpha1 | (keyboard_data.key_alpha2 << 1)); + kb[0] = (uint8_t)(keyboard_merged.mouse_rl & 0xFF); + kb[1] = (uint8_t)((uint16_t)keyboard_merged.mouse_rl >> 8); + kb[2] = (uint8_t)(keyboard_merged.mouse_du & 0xFF); + kb[3] = (uint8_t)((uint16_t)keyboard_merged.mouse_du >> 8); + kb[4] = (uint8_t)(keyboard_merged.mouse_wheel & 0xFF); + kb[5] = (uint8_t)((uint16_t)keyboard_merged.mouse_wheel >> 8); + kb[6] = keyboard_merged.mouse_key_l; + kb[7] = keyboard_merged.mouse_key_r; + kb[8] = (uint8_t)(keyboard_merged.key_w | (keyboard_merged.key_s << 1) | + (keyboard_merged.key_a << 2) | (keyboard_merged.key_d << 3) | + (keyboard_merged.key_shift << 4) | (keyboard_merged.key_ctrl << 5) | + (keyboard_merged.key_q << 6) | (keyboard_merged.key_e << 7)); + kb[9] = (uint8_t)(keyboard_merged.key_alpha1 | (keyboard_merged.key_alpha2 << 1)); kb[10] = 0; kb[11] = 0; c_comm_master_send(kb_online, kb, gimbal_motor_offline(), cmd_robot.spin_flag); // 故障位+小陀螺使能位 + /* 0x1B9 自定义UI 专用量:底盘本地没有 pitch 与摩擦轮转速, + * 由云台随板间通讯带给它(见 底盘/modules/comms/referee_ui.c)。 + * pitch 取云台 pitch 环反馈 -att_deg_roll;摩擦轮转速取电机反馈 rpm。 */ + c_comm_master_send_ui(-att_deg_roll, + (float)motor_friction_l.fdb.rotor_speed, + (float)motor_friction_r.fdb.rotor_speed, + cmd_robot.spin_flag); + /* 0x1A9 姿态帧:跟随符号自动综合的数据源 * flags:bit0=att_ready bit1=tick增向CCW bit2=bit1有效 */ { diff --git a/云台/Application/Task/Src/debug_task.c b/云台/Application/Task/Src/debug_task.c index 12c8ade..ab81c2f 100644 --- a/云台/Application/Task/Src/debug_task.c +++ b/云台/Application/Task/Src/debug_task.c @@ -1,5 +1,5 @@ /* ============================================================================= - * debug_task.c - USB 虚拟串口(CDC)姿态输出(欧拉角 + 姿态链路诊断) + * debug_task.c - USB 虚拟串口(CDC)姿态输出(欧拉角 + 姿态链路诊断 + 控制量) * 每 DEBUG_OUT_PERIOD_MS 输出一行。单位: 角度 ° / 加速度 m/s² / 角速度 rad/s * * ---- 欧拉角(QuaternionEKF 解算结果,见 modules/ins) ---- @@ -25,7 +25,30 @@ * se bmi088_spi_errors:SPI/DMA 出错次数;st=bmi088_spi_state() * (1=READY,非 1 说明 SPI 被卡住,数据路会因此停摆) * - * 开关: DEBUG_ATT_HEALTH=0 只输出三个欧拉角;DEBUG_ATT_DIAG=0 去掉诊断位。 + * ---- 控制量(DEBUG_CMD_OUT=1 时追加;用于排查 控制源→cmd_robot→执行机构) ---- + * online 1=**遥控器(DR16)在线** —— 本板这一位只反映 DR16,键鼠不置位! + * 没有遥控器时整段控制分支都不执行(见 robot_cmd_task.c 的 online 门控), + * 所以"键鼠测试"也必须有遥控器在线,否则这里恒为 0。 + * src 0=遥控器 1=键鼠(判据:0x0304 在 1s 内到过 keyboard_data.last_tick) + * spin 小陀螺使能(云台侧检测,随 0x189 下发给底盘) + * yaw_d/pitch_d 本周期云台角度增量(°/2ms 任务周期,×500 即 °/s) + * fric/fire/eject 发射机构开关 reset 云台角度复位请求 + * s2/ctrl 遥控器 S2 档位与 Ctrl 按下沿 —— 底盘就是靠这两个量推出 + * chassis_mode,所以本板不输出模式本身(本板该字段恒为 OFF) + * wheel 遥控拨轮原始值(1000~1050 为中立) + * --- 以下为 DR16 帧内容诊断(判断"键鼠数据到底有没有到机器人") --- + * s1 左侧拨杆(1上 2下 3中)。注意官方 RoboRTS 的 dbus.c 与本工程一致: + * sw2=bit44-45→s2, sw1=bit46-47→s1;若与实物相反,用本列与 s2 对照即可判定 + * mouse_rl/mouse_du/mouse_wheel 帧里 data[6..11] 解析出的鼠标三轴 + * mouse_l/mouse_r 帧里 data[12]/data[13](鼠标左右键) + * key_lo/key_hi 帧里 data[14]/data[15] **原始字节**(不做位拆分) + * ★ 按下 W 时 key_lo 应变 0x01;若恒为 0,说明帧里根本没有键鼠数据 + * frames dr16_frame_count;持续增长说明 USART3 一直在收帧 + * 无健康位(DEBUG_ATT_HEALTH=0)时输出为纯数字 CSV,列序即上面顺序, + * 可被 Lissio FireWater 直接解析成通道绘图。 + * + * 开关: DEBUG_ATT_HEALTH=0 只输出三个欧拉角;DEBUG_ATT_DIAG=0 去掉诊断位; + * DEBUG_CMD_OUT=1 追加控制量字段。 * ========================================================================== */ #include "debug_task.h" #include "cmsis_os.h" @@ -37,6 +60,7 @@ #include "bmi088.h" #include "gimbal.h" #include "dr16.h" +#include "robot_typedef.h" #include #include #include @@ -60,6 +84,8 @@ #define DEBUG_ATT_DIAG 0 /* 1-仅输出 Pitch 调参行(便于用 VOFA+/串口示波器绘图); 0-同时输出姿态行与 Pitch 调参行 */ #define DEBUG_PITCH_ONLY 0 +/* 1-追加控制量字段(见文件头); 0-保持原姿态输出不变 */ +#define DEBUG_CMD_OUT 1 /* 输出周期(ms)。姿态观测建议 100ms(10Hz) */ #define DEBUG_OUT_PERIOD_MS 100 @@ -114,17 +140,68 @@ void debug_task(void const * argument) #endif #if DEBUG_ATT_HEALTH +#if DEBUG_CMD_OUT + /* 健康位分支是文本格式,控制量就按 key=value 追加,便于人读 */ + char ctl[160]; + snprintf(ctl, sizeof(ctl), + " online=%u src=%u spin=%u yaw_d=%.4f pitch_d=%.4f fric=%u fire=%u" + " eject=%u reset=%u s2=%u ctrl=%u wheel=%d", + (unsigned)cmd_robot.online, (unsigned)cmd_robot.source, + (unsigned)cmd_robot.spin_flag, + cmd_robot.gimbal_yaw_delta, cmd_robot.gimbal_pitch_delta, + (unsigned)cmd_robot.friction_on, (unsigned)cmd_robot.fire, + (unsigned)cmd_robot.eject, (unsigned)cmd_robot.gimbal_reset, + (unsigned)dr16.s2, (unsigned)dr16.key_push_ctrl, + (int)cmd_robot.thumb_wheel); +#else + const char * ctl = ""; +#endif int len = snprintf(line, sizeof(line), - "ecd=%u yaw=%.2f pitch=%.2f roll=%.2f cvg=%u rdy=%u imu=%u an=%.2f gn=%.3f%s\r\n", + "ecd=%u yaw=%.2f pitch=%.2f roll=%.2f cvg=%u rdy=%u imu=%u an=%.2f gn=%.3f%s%s\r\n", (unsigned)motor_yaw_gimbal.fdb.raw_angle, att_deg_yaw, att_deg_pitch, att_deg_roll, (unsigned)INS.ConvergeFlag, (unsigned)att_ready, - (unsigned)bmi088_online, an, gn, tail); + (unsigned)bmi088_online, an, gn, tail, ctl); +#else +#if DEBUG_CMD_OUT + /* 纯数字 CSV。前 3 列保持原样(pitch_ecd, pitch_current, yaw_ecd),其余: + * online,src,spin, yaw_d,pitch_d, fric,fire,eject,reset, s2,ctrl,wheel, + * s1, mouse_rl,mouse_du,mouse_wheel,mouse_l,mouse_r, key_lo,key_hi, + * dr16_frame_count + * 末尾这 9 列是 DR16 帧内容诊断:key_lo/key_hi 就是帧里的 data[14]/data[15]。 + * 按住 W 时若它们仍为 0,说明帧里本就没有键鼠数据(不是解析问题)。 */ + int len = snprintf(line, sizeof(line), + "%u,%.1f,%u,%u,%u,%u,%.4f,%.4f,%u,%u,%u,%u,%u,%u,%d,%u,%d,%d,%d,%u,%u,%u,%u,%lu\r\n", + (unsigned)motor_pitch_gimbal.fdb.raw_angle, + pitch_current, + (unsigned)motor_yaw_gimbal.fdb.raw_angle, + (unsigned)cmd_robot.online, + (unsigned)cmd_robot.source, + (unsigned)cmd_robot.spin_flag, + cmd_robot.gimbal_yaw_delta, + cmd_robot.gimbal_pitch_delta, + (unsigned)cmd_robot.friction_on, + (unsigned)cmd_robot.fire, + (unsigned)cmd_robot.eject, + (unsigned)cmd_robot.gimbal_reset, + (unsigned)dr16.s2, + (unsigned)dr16.key_push_ctrl, + (int)cmd_robot.thumb_wheel, + (unsigned)dr16.s1, + (int)dr16.mouse_rl, + (int)dr16.mouse_du, + (int)dr16.mouse_wheel, + (unsigned)dr16.mouse_key_l, + (unsigned)dr16.mouse_key_r, + (unsigned)dr16.key_code_lo, + (unsigned)dr16.key_code_hi, + (unsigned long)dr16_frame_count); #else int len = snprintf(line, sizeof(line), "%u,%.1f,%u\r\n", (unsigned)motor_pitch_gimbal.fdb.raw_angle, pitch_current,(unsigned)motor_yaw_gimbal.fdb.raw_angle); #endif +#endif #endif diff --git a/云台/Application/Task/Src/robot_cmd_task.c b/云台/Application/Task/Src/robot_cmd_task.c index 63ae575..0b5f18b 100644 --- a/云台/Application/Task/Src/robot_cmd_task.c +++ b/云台/Application/Task/Src/robot_cmd_task.c @@ -1,6 +1,6 @@ /* ============================================================================= * robot_cmd_task.c - 控制源汇总(2ms),发布 cmd_robot_t(robot_typedef.h) - * 云台板: DR16 遥控器 + 图传键鼠(0x0304,referee 键盘钩子填充 keyboard_data) + * 云台板: DR16 遥控器 + 键鼠(0x0304 优先;USART3 的 18 字节帧里同样带键鼠,两路已合并,见 KB_FROM_DBUS_FRAME) * 键鼠/遥控互斥(与 legacy 一致);摇杆->云台增量与限位门控; * Q 摩擦轮/左键发射/E 退弹/拨轮;首帧触发 gimbal_reset * 底盘板: 从 ctoc_remote(0x149/0x189 解析结果)生成: @@ -31,6 +31,9 @@ volatile cmd_robot_t cmd_robot; #if defined(ROBOT_TYPE_GIMBAL) keyboard_data_t keyboard_data; +/* 键鼠来源合并视图(声明见 robot_cmd_task.h):云台控制与 c_comm_task 底盘摇杆合成共用 */ +keyboard_data_t keyboard_merged; +uint8_t keyboard_merged_online = 0; static uint8_t keyboard_online = 0; void dr16_frame_callback(const uint8_t * frame, uint16_t len) @@ -74,6 +77,15 @@ static void referee_keyboard_hook(const uint8_t data[12]) keyboard_data.last_tick = HAL_GetTick(); } +/* ---- 键鼠输入来源(2026-10-16 修) ---- + * 背景:客户端键鼠是从 USART3 那 18 字节帧进来的(data[6..15]),dr16_parse 早就 + * 解析进 dr16.mouse_rl / dr16.key_w 等字段,但控制逻辑只读 0x0304 填的 + * keyboard_data,于是"键鼠控制完全没用"。此开关让两路合并。 + * 1 = 除 0x0304 外同时接受帧内键鼠:见到键鼠动作即进入键鼠模式并保持, + * 直到操作手去动云台左摇杆才交还遥控器(右摇杆在底盘,键鼠模式下仍要能用)。 + * 0 = 旧行为:只有 0x0304 在线才走键鼠。 */ +#define KB_FROM_DBUS_FRAME 1 + void robot_cmd_task(void const * argument) { referee_set_keyboard_hook(referee_keyboard_hook); @@ -81,13 +93,80 @@ void robot_cmd_task(void const * argument) while (!app_ready) osDelay(2); + /* 键鼠模式状态:帧内键鼠动作置 1,左摇杆动作清 0 */ + static uint8_t kb_mode = 0; + for (;;) { keyboard_online = (keyboard_data.last_tick != 0 && HAL_GetTick() - keyboard_data.last_tick < DEVICE_OFFLINE_TIME_MS * 10); +#if KB_FROM_DBUS_FRAME + { + uint8_t kb_act = 0, rc_act = 0; + + /* 键鼠动作:鼠标移动 / 滚轮 / 左右键 / 任意按键(data[14]或data[15]非 0) */ + if (dr16.mouse_rl > 1 || dr16.mouse_rl < -1) kb_act = 1; + if (dr16.mouse_du > 1 || dr16.mouse_du < -1) kb_act = 1; + if (dr16.mouse_wheel != 0) kb_act = 1; + if (dr16.mouse_key_l || dr16.mouse_key_r) kb_act = 1; + if (dr16.key_code_lo || dr16.key_code_hi) kb_act = 1; + + /* 遥控器动作:只认云台左摇杆 */ + if (dr16.rc_l_UD > 1050 || dr16.rc_l_UD < 1000) rc_act = 1; + if (dr16.rc_l_RL > 1050 || dr16.rc_l_RL < 1000) rc_act = 1; + + if (kb_act) kb_mode = 1; + if (rc_act) kb_mode = 0; + } +#endif + + /* 是否键鼠在控:0x0304 在线时始终算键鼠(保持原有优先级) */ + uint8_t kb_active = (keyboard_online || kb_mode) ? 1 : 0; + + /* ---- 合并成共享视图 keyboard_merged ---- + * 0x0304 在线 → 整份用 keyboard_data;否则用 USART3 帧里那一套。 + * 云台控制、发射机构、以及 c_comm_task 里 WASD→底盘摇杆的合成都读这个 + * 视图,避免两处各判一次导致"云台跟鼠标了、底盘却还在收遥控器原始摇杆"。 */ + if (keyboard_online) + { + keyboard_merged = keyboard_data; + } + else + { + keyboard_merged.mouse_rl = dr16.mouse_rl; + keyboard_merged.mouse_du = dr16.mouse_du; + keyboard_merged.mouse_wheel = dr16.mouse_wheel; + keyboard_merged.mouse_key_l = dr16.mouse_key_l; + keyboard_merged.mouse_key_r = dr16.mouse_key_r; + keyboard_merged.key_w = dr16.key_w; + keyboard_merged.key_s = dr16.key_s; + keyboard_merged.key_a = dr16.key_a; + keyboard_merged.key_d = dr16.key_d; + keyboard_merged.key_shift = dr16.key_shift; + keyboard_merged.key_ctrl = dr16.key_ctrl; + keyboard_merged.key_q = dr16.key_q; + keyboard_merged.key_e = dr16.key_e; + /* alpha 键:dr16 侧那两位恒为 0(见 dr16.c 的 >>9/>>10),照搬保持语义 */ + keyboard_merged.key_alpha1 = dr16.key_alpha1; + keyboard_merged.key_alpha2 = dr16.key_alpha2; + keyboard_merged.key_push_q = dr16.key_push_q; + keyboard_merged.key_push_e = dr16.key_push_e; + keyboard_merged.key_push_shift = dr16.key_push_shift; + keyboard_merged.key_push_ctrl = dr16.key_push_ctrl; + keyboard_merged.last_tick = dr16_last_tick; + } + keyboard_merged_online = kb_active; + + /* 控制逻辑用的简写(取自共享视图) */ + int16_t kb_mouse_rl = keyboard_merged.mouse_rl; + int16_t kb_mouse_du = keyboard_merged.mouse_du; + uint8_t kb_mouse_l = keyboard_merged.mouse_key_l; + uint8_t kb_push_q = keyboard_merged.key_push_q; + uint8_t kb_push_e = keyboard_merged.key_push_e; + cmd_robot.online = (dr16_status == 1 && !dr16_offline()); - cmd_robot.source = keyboard_online ? 1 : 0; + cmd_robot.source = kb_active ? 1 : 0; if (cmd_robot.online) { @@ -95,7 +174,7 @@ void robot_cmd_task(void const * argument) float yaw_delta = 0, pitch_delta = 0; uint16_t pos = motor_pitch_gimbal.fdb.raw_angle; - if (!keyboard_online) + if (!kb_active) { /* 遥控器:左摇杆向上(>1050)为抬头(pitch_delta减小,pos向2260减小),门控须 pos > UPPER_LIMIT(2260); * 左摇杆向下(<1000)为低头(pitch_delta增大,pos向3950增大),门控须 pos < LOWER_LIMIT(3950) */ @@ -114,23 +193,26 @@ void robot_cmd_task(void const * argument) else { /* 键鼠:鼠标前推(>8)为抬头(pitch_delta减小),门控须 pos > UPPER_LIMIT;鼠标后拉(<-8)为低头(pitch_delta增大),门控须 pos < LOWER_LIMIT */ - if (keyboard_data.mouse_du > 8 && pos > PITCH_GM6020_POSITION_UPPER_LIMIT) + if (kb_mouse_du > 8 && pos > PITCH_GM6020_POSITION_UPPER_LIMIT) pitch_delta -= GIMBAL_PITCH_LEVER_RATE / 8192.0f * 360.0f * - ((float)keyboard_data.mouse_du * PC_MOUSE_DU_SENSITIVITY) / 660.0f; - else if (keyboard_data.mouse_du < -8 && pos < PITCH_GM6020_POSITION_LOWER_LIMIT) + ((float)kb_mouse_du * PC_MOUSE_DU_SENSITIVITY) / 660.0f; + else if (kb_mouse_du < -8 && pos < PITCH_GM6020_POSITION_LOWER_LIMIT) pitch_delta += GIMBAL_PITCH_LEVER_RATE / 8192.0f * 360.0f * - (-(float)keyboard_data.mouse_du * PC_MOUSE_DU_SENSITIVITY) / 660.0f; + (-(float)kb_mouse_du * PC_MOUSE_DU_SENSITIVITY) / 660.0f; - if (keyboard_data.mouse_rl != 0) + if (kb_mouse_rl != 0) yaw_delta -= GIMBAL_LEVER_SPEED_MAP_RATE * GIMBAL_YAW_PITCH_SPEED_RATE * GIMBAL_YAW_PITCH_SPEED_RATE * 0.0439453125f * - ((float)keyboard_data.mouse_rl * PC_MOUSE_RL_SENSITIVITY / 660.0f * 2.0f); + ((float)kb_mouse_rl * PC_MOUSE_RL_SENSITIVITY / 660.0f * 2.0f); } cmd_robot.gimbal_yaw_delta = yaw_delta; cmd_robot.gimbal_pitch_delta = pitch_delta; - /* ---- 发射机构 ---- */ - if (!keyboard_online) + /* ---- 发射机构 ---- + * 键鼠模式用 Q / 鼠标左键 / E(两路来源已合并进 kb_*,0x0304 优先); + * 遥控模式用 S1 与拨轮。kb_push_* 是"按下翻转"的常驻状态, + * 且键鼠模式只在动左摇杆时才退出,所以摩擦轮/E 的状态不会自己掉。 */ + if (!kb_active) { cmd_robot.friction_on = (dr16.s1 == 1); cmd_robot.fire = (dr16.thumb_wheel < 1000); @@ -138,9 +220,9 @@ void robot_cmd_task(void const * argument) } else { - cmd_robot.friction_on = keyboard_data.key_push_q; - cmd_robot.fire = keyboard_data.mouse_key_l; - cmd_robot.eject = keyboard_data.key_push_e; + cmd_robot.friction_on = kb_push_q; + cmd_robot.fire = kb_mouse_l; + cmd_robot.eject = kb_push_e; } cmd_robot.thumb_wheel = dr16.thumb_wheel; } @@ -153,12 +235,13 @@ void robot_cmd_task(void const * argument) cmd_robot.eject = 0; } - /* 小陀螺使能:Ctrl 沿翻转(仅在遥控源检测,键鼠 TODO); + /* 小陀螺使能:Ctrl 沿翻转。键鼠模式下取键鼠那一路的 Ctrl(0x0304 的 + * keyboard_data 或帧内 dr16.key_push_ctrl),遥控模式用遥控器 Ctrl。 * 云台侧是数据源头,底盘只收状态位——重连瞬间保持态跳变不会在 * 底盘误触发沿(修复"重连误开小陀螺疯转") */ { static uint8_t spin_flag = 0, last_ctrl = 0; - uint8_t ctrl_now = (!keyboard_online) ? dr16.key_push_ctrl : 0; + uint8_t ctrl_now = keyboard_online ? keyboard_data.key_push_ctrl : dr16.key_push_ctrl; if (last_ctrl == 0 && ctrl_now == 1) spin_flag = !spin_flag; last_ctrl = ctrl_now; diff --git a/云台/Application/robot_param.h b/云台/Application/robot_param.h index 656ea12..df427eb 100644 --- a/云台/Application/robot_param.h +++ b/云台/Application/robot_param.h @@ -139,6 +139,7 @@ #define CTOC_SLAVE_ID1 0x149 // 云台->底盘:摇杆数据 #define CTOC_SLAVE_ID2 0x189 // 云台->底盘:控制状态+故障/陀螺使能位 #define CTOC_SLAVE_ID3 0x1A9 // 云台->底盘:云台世界系 yaw(float)+att_ready +#define CTOC_SLAVE_ID4 0x1B9 // 云台->底盘:自定义UI 专用(pitch + 摩擦轮 L/R 转速) /* ============================ CAN ID ============================ */ /* 云台板:CAN1 = Pitch GM6020(0x206,0x1FE)+摩擦轮 M3508(0x201/0x202,0x200) diff --git a/云台/MDK-ARM/Simba-ELCT-FrameWork.uvoptx b/云台/MDK-ARM/Simba-ELCT-FrameWork.uvoptx index e198856..e292a1d 100644 --- a/云台/MDK-ARM/Simba-ELCT-FrameWork.uvoptx +++ b/云台/MDK-ARM/Simba-ELCT-FrameWork.uvoptx @@ -103,7 +103,7 @@ 1 0 0 - 2 + 3 @@ -114,9 +114,14 @@ - BIN\CMSIS_AGDI.dll + Segger\JL2CM3.dll + + 0 + JL2CM3 + -U20090928 -O78 -S2 -ZTIFSpeedSel5000 -A0 -C0 -JU1 -JI127.0.0.1 -JP0 -RST0 -N00("ARM CoreSight SW-DP") -D00(2BA01477) -L00(0) -TO18 -TC10000000 -TP21 -TDS8007 -TDT0 -TDC1F -TIEFFFFFFFF -TIP8 -TB1 -TFE0 -FO7 -FD20000000 -FC1000 -FN1 -FF0STM32F4xx_1024.FLM -FS08000000 -FL0100000 -FP0($$Device:STM32F407IGHx$CMSIS\Flash\STM32F4xx_1024.FLM) + 0 ARMRTXEVENTFLAGS diff --git a/云台/modules/comms/c_comm.c b/云台/modules/comms/c_comm.c index 376007f..dedcf74 100644 --- a/云台/modules/comms/c_comm.c +++ b/云台/modules/comms/c_comm.c @@ -136,6 +136,26 @@ void c_comm_master_send(uint8_t keyboard_online, const uint8_t kb[12], uint8_t m c_comm_send_frame(&hcan2, CTOC_SLAVE_ID2, data); } +/* 0x1B9 云台->底盘:自定义UI 专用量(底盘本地没有,见 referee_ui) + * [0..1] pitch(0.01°) [2..3] 左摩擦轮(rpm/100) [4..5] 右摩擦轮(rpm/100) + * [6] bit0=小陀螺使能 */ +void c_comm_master_send_ui(float pitch_deg, float fric_l_rpm, float fric_r_rpm, uint8_t spin_on) +{ + uint8_t data[8] = {0}; + int16_t p = (int16_t)(pitch_deg * 100.0f); + int16_t fl = (int16_t)(fric_l_rpm / 100.0f); + int16_t fr = (int16_t)(fric_r_rpm / 100.0f); + + data[0] = (uint8_t)(p & 0xFF); + data[1] = (uint8_t)((uint16_t)p >> 8); + data[2] = (uint8_t)(fl & 0xFF); + data[3] = (uint8_t)((uint16_t)fl >> 8); + data[4] = (uint8_t)(fr & 0xFF); + data[5] = (uint8_t)((uint16_t)fr >> 8); + data[6] = spin_on ? 0x01 : 0; + c_comm_send_frame(&hcan2, CTOC_SLAVE_ID4, data); +} + #elif defined(ROBOT_TYPE_CHASSIS) /* ============================ 底盘板(从机) ============================ */ diff --git a/云台/modules/comms/c_comm.h b/云台/modules/comms/c_comm.h index 996640d..92391a7 100644 --- a/云台/modules/comms/c_comm.h +++ b/云台/modules/comms/c_comm.h @@ -63,6 +63,11 @@ extern volatile int8_t ctoc_encoder_ccw; /* 底盘板:0x1A9 帧是否新鲜(掉线检测) */ extern uint8_t ctoc_att_fresh(void); +/* ---- 0x1B9 云台->底盘:自定义UI 专用数据 --------------------------------- + * 底盘本地没有、但裁判系统自定义UI 需要的量(见 底盘/modules/comms/referee_ui): + * pitch 角度与左右摩擦轮转速。接收侧解析见底盘 c_comm.c(c_comm_rx_0x1b9)。 */ +extern void c_comm_master_send_ui(float pitch_deg, float fric_l_rpm, float fric_r_rpm, uint8_t spin_on); + /* 注册 CAN 接收回调(can_start() 之后调用) */ extern void c_comm_init(void); diff --git a/云台/modules/comms/dr16.c b/云台/modules/comms/dr16.c index 32ec90f..b405456 100644 --- a/云台/modules/comms/dr16.c +++ b/云台/modules/comms/dr16.c @@ -48,6 +48,12 @@ void dr16_parse(const uint8_t * data) dr16.key_alpha1 = (data[14] >> 9) & 0x01; dr16.key_alpha2 = (data[14] >> 10) & 0x01; + /* 原始按键字节。注意 data[14] 是 uint8_t:上面两条 >>9/>>10 恒为 0 + * (整型提升后 byte 值 <256,右移 9 位必然为 0),即 alpha 键从未被真正解析。 + * 官方 16 位 key_code 的高字节在 data[15],这里先原样存下来供诊断/后续解析。 */ + dr16.key_code_lo = data[14]; + dr16.key_code_hi = data[15]; + dr16.thumb_wheel = (int16_t)(((uint16_t)data[17] << 8) | data[16]); // B[120:135] /* 按键边沿检测(legacy 语义:Q/E/Shift/Ctrl 按下翻转,鼠标右键按下瞬间置 1) */ diff --git a/云台/modules/comms/dr16.h b/云台/modules/comms/dr16.h index 67ec4b6..f1faaa8 100644 --- a/云台/modules/comms/dr16.h +++ b/云台/modules/comms/dr16.h @@ -37,6 +37,13 @@ typedef struct uint8_t key_alpha1; // 图传扩展键 uint8_t key_alpha2; + /* 原始按键字节(诊断用,不做位拆分) —— 用来判断"帧里到底有没有键鼠数据": + * 官方 dbus.h 的 key_code 是 16 位:bit0..7 = W/S/A/D/Shift/Ctrl/Q/E(=data[14]), + * bit8..15 = R/F/G/Z/X/C/V/B(=data[15])。本工程此前只读了 data[14]。 + * 拨键盘时这两个字节若非 0 说明数据确实到了;全 0 说明帧里就是空的。 */ + uint8_t key_code_lo; // data[14] + uint8_t key_code_hi; // data[15] + int16_t thumb_wheel; // 遥控器拨轮 /* 按键边沿检测(legacy 语义) */ diff --git a/底盘/Application/Task/Src/debug_task.c b/底盘/Application/Task/Src/debug_task.c index 2112b4e..372264d 100644 --- a/底盘/Application/Task/Src/debug_task.c +++ b/底盘/Application/Task/Src/debug_task.c @@ -1,5 +1,5 @@ /* ============================================================================= - * debug_task.c - USB 虚拟串口(CDC)姿态输出(欧拉角 + 姿态链路诊断) + * debug_task.c - USB 虚拟串口(CDC)姿态输出(欧拉角 + 姿态链路诊断 + 控制量/模式) * 每 DEBUG_OUT_PERIOD_MS 输出一行。单位: 角度 ° / 加速度 m/s² / 角速度 rad/s * * ---- 欧拉角(QuaternionEKF 解算结果,见 modules/ins) ---- @@ -25,7 +25,20 @@ * se bmi088_spi_errors:SPI/DMA 出错次数;st=bmi088_spi_state() * (1=READY,非 1 说明 SPI 被卡住,数据路会因此停摆) * - * 开关: DEBUG_ATT_HEALTH=0 只输出三个欧拉角;DEBUG_ATT_DIAG=0 去掉诊断位。 + * ---- 控制量 / 控制模式(DEBUG_CMD_OUT=1 时追加) ---- + * mode 底盘模式 0=OFF 1=MANUAL 2=FOLLOW 3=SPIN_SLOW 4=SPIN_FAST + * (由本板依据收到的 s2/spin 推出;模式逻辑见 robot_cmd_task.c) + * online 1=板间通讯(CToC)在线 —— 本板唯一的"控制源在线"判据 + * vx/vy 底盘平移速度指令(世界系映射值) wz [已废弃,恒 0] + * spin **收到的**小陀螺使能(0x189 bit2,云台侧做 Ctrl 沿检测) + * s2 **收到的**遥控 S2 档位(小陀螺为真时它被覆盖) + * reset 跟随朝向重锁请求(跟随模式重连时置位) + * 注意:本板拿不到"遥控器还是键鼠"这一信息——0x149/0x189 不带该位, + * robot_cmd_task 里 cmd_robot.source 恒被置 0,故这里不输出假字段。 + * 与姿态同源(cmd_robot/ctoc_remote),一起看才能判断"模式对不对、量给没给"。 + * + * 开关: DEBUG_ATT_HEALTH=0 只输出三个欧拉角;DEBUG_ATT_DIAG=0 去掉诊断位; + * DEBUG_CMD_OUT=1 追加控制量/控制模式。 * ========================================================================== */ #include "debug_task.h" #include "cmsis_os.h" @@ -35,6 +48,8 @@ #include "attitude.h" #include "ins.h" #include "bmi088.h" +#include "robot_typedef.h" +#include "c_comm.h" #include #include @@ -42,13 +57,31 @@ #define DEBUG_ATT_HEALTH 1 /* 1-再附加原始陀螺/零偏/采样计数(排障用); 0-只留上面的通用健康位 */ #define DEBUG_ATT_DIAG 1 +/* 1-追加控制量/控制模式(见文件头); 0-保持原姿态输出不变 */ +#define DEBUG_CMD_OUT 1 /* 输出周期(ms)。姿态观测建议 100ms(10Hz) */ #define DEBUG_OUT_PERIOD_MS 100 +#if DEBUG_CMD_OUT +/* 底盘模式名(便于人读;数值定义见 robot_typedef.h 的 chassis_mode_e) */ +static const char * cmd_mode_name(chassis_mode_e m) +{ + switch (m) + { + case CHASSIS_MODE_OFF: return "OFF"; + case CHASSIS_MODE_MANUAL: return "MANUAL"; + case CHASSIS_MODE_FOLLOW: return "FOLLOW"; + case CHASSIS_MODE_SPIN_SLOW: return "SPIN_SLOW"; + case CHASSIS_MODE_SPIN_FAST: return "SPIN_FAST"; + default: return "?"; + } +} +#endif + void debug_task(void const * argument) { - char line[192]; + char line[448]; char tail[112]; for (;;) @@ -70,15 +103,29 @@ void debug_task(void const * argument) (unsigned long)bmi088_spi_errors, (unsigned)bmi088_spi_state()); #endif +#if DEBUG_CMD_OUT + char ctl[192]; + snprintf(ctl, sizeof(ctl), + " mode=%s online=%u vx=%.3f vy=%.3f wz=%.3f spin=%u s2=%u reset=%u", + cmd_mode_name(cmd_robot.chassis_mode), + (unsigned)cmd_robot.online, + cmd_robot.vx, cmd_robot.vy, cmd_robot.wz_manual, + (unsigned)ctoc_remote.spin_flag, + (unsigned)ctoc_remote.s2, + (unsigned)cmd_robot.gimbal_reset); +#else + const char * ctl = ""; +#endif + #if DEBUG_ATT_HEALTH int len = snprintf(line, sizeof(line), - "yaw=%.2f pitch=%.2f roll=%.2f cvg=%u rdy=%u imu=%u an=%.2f gn=%.3f%s\r\n", + "yaw=%.2f pitch=%.2f roll=%.2f cvg=%u rdy=%u imu=%u an=%.2f gn=%.3f%s%s\r\n", att_deg_yaw, att_deg_pitch, att_deg_roll, (unsigned)INS.ConvergeFlag, (unsigned)att_ready, - (unsigned)bmi088_online, an, gn, tail); + (unsigned)bmi088_online, an, gn, tail, ctl); #else - int len = snprintf(line, sizeof(line), "yaw=%.2f pitch=%.2f roll=%.2f\r\n", - att_deg_yaw, att_deg_pitch, att_deg_roll); + int len = snprintf(line, sizeof(line), "yaw=%.2f pitch=%.2f roll=%.2f%s\r\n", + att_deg_yaw, att_deg_pitch, att_deg_roll, ctl); #endif if (len > 0) diff --git a/底盘/Application/Task/Src/referee_task.c b/底盘/Application/Task/Src/referee_task.c index 70a2fe6..cd66ec3 100644 --- a/底盘/Application/Task/Src/referee_task.c +++ b/底盘/Application/Task/Src/referee_task.c @@ -7,6 +7,9 @@ #include "cmsis_os.h" #include "bsp_usart.h" #include "referee.h" +#include "referee_ui.h" +#include "c_comm.h" +#include "attitude.h" #include "robot_param.h" /* referee_task:裁判系统链路监测 @@ -29,6 +32,37 @@ void referee_task(void const * argument) HAL_GetTick() - referee.last_update_tick > DEVICE_OFFLINE_TIME_MS * 10) referee.link_status = 0; + /* ---- 自定义UI:机器人 -> 裁判系统 -> 选手端 ------------------------ + * 本任务 100ms 一轮,内部按 UI_SEND_HZ 分频(5Hz:约 40 图形/帧, + * 115200 链路上约 3.8KB/s)。数据来源: + * 底盘本地 = att_deg_yaw(朝向) + referee(功率/电容) + * 云台 CToC = pitch + 摩擦轮 L/R 转速(0x1B9) + 云台 yaw(0x1A9) + 陀螺位(0x189) + * ------------------------------------------------------------------ */ + { + static uint8_t ui_div = 0; + if (++ui_div >= (uint8_t)(10 / UI_SEND_HZ)) + { + ui_div = 0; + + ui_data_t ui; + ui.pitch_deg = ctoc_ui_pitch_deg; + ui.yaw_deg = ctoc_gimbal_yaw; + ui.hdg_deg = att_deg_yaw; + ui.pwr_w = referee.chassis_power; + ui.pwr_limit_w = (float)referee.chassis_power_limit; + ui.buf_j = referee.buffer_energy; + ui.cap_pct = (uint8_t)(referee.buffer_energy >= 60.0f + ? 100 + : (referee.buffer_energy * 100.0f / 60.0f)); + ui.fric_l_rpm = ctoc_ui_fric_l_rpm; + ui.fric_r_rpm = ctoc_ui_fric_r_rpm; + ui.spin_on = ctoc_remote.spin_flag; + ui.ref_online = referee.link_status; + + referee_ui_update(&ui); + } + } + monitor_heartbeat(HB_REFEREE); osDelay(100); } diff --git a/底盘/Application/robot_param.h b/底盘/Application/robot_param.h index 07a3a89..888807e 100644 --- a/底盘/Application/robot_param.h +++ b/底盘/Application/robot_param.h @@ -124,6 +124,7 @@ #define CTOC_SLAVE_ID1 0x149 // 云台->底盘:摇杆数据 #define CTOC_SLAVE_ID2 0x189 // 云台->底盘:控制状态+故障/陀螺使能位 #define CTOC_SLAVE_ID3 0x1A9 // 云台->底盘:云台世界系 yaw(float)+att_ready +#define CTOC_SLAVE_ID4 0x1B9 // 云台->底盘:UI 数据(pitch + 摩擦轮 L/R 转速) /* ============================ CAN ID ============================ */ /* 云台板:CAN1 = Pitch GM6020(0x206,0x1FE)+摩擦轮 M3508(0x201/0x202,0x200) diff --git a/底盘/MDK-ARM/Simba-ELCT-FrameWork.uvoptx b/底盘/MDK-ARM/Simba-ELCT-FrameWork.uvoptx index 528fa6a..db90d3b 100644 --- a/底盘/MDK-ARM/Simba-ELCT-FrameWork.uvoptx +++ b/底盘/MDK-ARM/Simba-ELCT-FrameWork.uvoptx @@ -1246,6 +1246,18 @@ 0 0 0 + ..\modules\comms\referee_ui.c + referee_ui.c + 0 + 0 + + + 10 + 82 + 1 + 0 + 0 + 0 ..\modules\dev\led.c led.c 0 @@ -1253,7 +1265,7 @@ 10 - 82 + 83 1 0 0 @@ -1265,7 +1277,7 @@ 10 - 83 + 84 1 0 0 @@ -1277,7 +1289,7 @@ 10 - 84 + 85 1 0 0 @@ -1289,7 +1301,7 @@ 10 - 85 + 86 1 0 0 @@ -1301,7 +1313,7 @@ 10 - 86 + 87 1 0 0 @@ -1313,7 +1325,7 @@ 10 - 87 + 88 1 0 0 @@ -1325,7 +1337,7 @@ 10 - 88 + 89 1 0 0 @@ -1337,7 +1349,7 @@ 10 - 89 + 90 1 0 0 @@ -1349,7 +1361,7 @@ 10 - 90 + 91 1 0 0 @@ -1369,7 +1381,7 @@ 0 11 - 91 + 92 1 0 0 @@ -1381,7 +1393,7 @@ 11 - 92 + 93 1 0 0 @@ -1393,7 +1405,7 @@ 11 - 93 + 94 5 0 0 @@ -1405,7 +1417,7 @@ 11 - 94 + 95 5 0 0 @@ -1425,7 +1437,7 @@ 0 12 - 95 + 96 4 0 0 diff --git a/底盘/MDK-ARM/Simba-ELCT-FrameWork.uvprojx b/底盘/MDK-ARM/Simba-ELCT-FrameWork.uvprojx index f666c48..30c95a7 100644 --- a/底盘/MDK-ARM/Simba-ELCT-FrameWork.uvprojx +++ b/底盘/MDK-ARM/Simba-ELCT-FrameWork.uvprojx @@ -2363,6 +2363,11 @@ 1 ..\modules\comms\referee.c + + referee_ui.c + 1 + ..\modules\comms\referee_ui.c + led.c 1 diff --git a/底盘/modules/comms/c_comm.c b/底盘/modules/comms/c_comm.c index 376007f..8b27b52 100644 --- a/底盘/modules/comms/c_comm.c +++ b/底盘/modules/comms/c_comm.c @@ -23,6 +23,13 @@ static volatile uint32_t ctoc_att_last_tick = 0; /* 云台自检下发的编码器方向:+1=tick增向CCW -1=CW 0=未检测(0x1A9 flags) */ volatile int8_t ctoc_encoder_ccw = 0; +/* ---- 0x1B9 自定义UI 专用量(云台提供,底盘本地没有) ---- */ +volatile float ctoc_ui_pitch_deg = 0.0f; +volatile float ctoc_ui_fric_l_rpm = 0.0f; +volatile float ctoc_ui_fric_r_rpm = 0.0f; +volatile uint8_t ctoc_ui_spin_flag = 0; +volatile uint32_t ctoc_ui_last_tick = 0; + /* 0x1A9 帧新鲜判定(两板公共:底盘跟随用它门控,云台留作诊断) */ uint8_t ctoc_att_fresh(void) { @@ -179,11 +186,26 @@ static void c_comm_rx_0x1a9(uint32_t std_id, const uint8_t rx_data[8], uint32_t ctoc_att_last_tick = HAL_GetTick(); } +/* 0x1B9 云台->底盘:自定义UI 需要而底盘本地没有的量 + * [0..1] pitch(0.01°) [2..3] 左摩擦轮(rpm/100) [4..5] 右摩擦轮(rpm/100) + * [6] bit0=小陀螺使能 */ +static void c_comm_rx_0x1b9(uint32_t std_id, const uint8_t rx_data[8], uint32_t dlc) +{ + (void)std_id; + (void)dlc; + ctoc_ui_pitch_deg = (float)(int16_t)(((uint16_t)rx_data[1] << 8) | rx_data[0]) * 0.01f; + ctoc_ui_fric_l_rpm = (float)(int16_t)(((uint16_t)rx_data[3] << 8) | rx_data[2]) * 100.0f; + ctoc_ui_fric_r_rpm = (float)(int16_t)(((uint16_t)rx_data[5] << 8) | rx_data[4]) * 100.0f; + ctoc_ui_spin_flag = rx_data[6] & 0x01; + ctoc_ui_last_tick = HAL_GetTick(); +} + void c_comm_init(void) { can_rx_register(&hcan2, CTOC_SLAVE_ID1, c_comm_rx_0x149); can_rx_register(&hcan2, CTOC_SLAVE_ID2, c_comm_rx_0x189); can_rx_register(&hcan2, CTOC_SLAVE_ID3, c_comm_rx_0x1a9); + can_rx_register(&hcan2, CTOC_SLAVE_ID4, c_comm_rx_0x1b9); } void c_comm_slave_send(uint8_t motor_fault) diff --git a/底盘/modules/comms/c_comm.h b/底盘/modules/comms/c_comm.h index 996640d..60f72d1 100644 --- a/底盘/modules/comms/c_comm.h +++ b/底盘/modules/comms/c_comm.h @@ -63,6 +63,18 @@ extern volatile int8_t ctoc_encoder_ccw; /* 底盘板:0x1A9 帧是否新鲜(掉线检测) */ extern uint8_t ctoc_att_fresh(void); +/* ---- 0x1B9 云台->底盘:自定义UI 专用数据 --------------------------------- + * 底盘本地没有、但 UI 要显示的量(见 referee_ui):pitch 角度与左右摩擦轮转速。 + * 单位:0.01° / rpm(×100 压缩成 int16 传输) */ +extern volatile float ctoc_ui_pitch_deg; +extern volatile float ctoc_ui_fric_l_rpm; +extern volatile float ctoc_ui_fric_r_rpm; +extern volatile uint8_t ctoc_ui_spin_flag; +extern volatile uint32_t ctoc_ui_last_tick; + +/* 云台板:发送 0x1B9 UI 数据帧(2ms 周期或按需,c_comm_task 调用) */ +extern void c_comm_master_send_ui(float pitch_deg, float fric_l_rpm, float fric_r_rpm, uint8_t spin_on); + /* 注册 CAN 接收回调(can_start() 之后调用) */ extern void c_comm_init(void); diff --git a/底盘/modules/comms/referee.c b/底盘/modules/comms/referee.c index b0bfa56..704443f 100644 --- a/底盘/modules/comms/referee.c +++ b/底盘/modules/comms/referee.c @@ -216,3 +216,48 @@ void referee_parse_bytes(const uint8_t * data, uint16_t len) while (len--) referee_parse_byte(*data++); } + +/* ============================================================================ + * 发送:裁判系统帧(机器人 -> 裁判系统 -> 选手端) + * 帧: A5 | data_length(2,LE,=payload 字节数,不含 cmd_id) | seq | CRC8 + * | cmd_id(2,LE) | data | CRC16(2,LE,低字节先) + * CRC8 覆盖前 4 字节;CRC16 覆盖除末 2 字节外全部。 + * 自定义UI 走 cmd 0x0301(payload 首 6 字节为 data_cmd_id/sender_ID/receiver_ID)。 + * USART6 TX DMA:静态缓冲,发送期间不得被覆盖;UI 刷新率 5Hz 远低于链路能力。 + * ==========================================================================*/ +#include "bsp_usart.h" + +static uint8_t ref_tx_seq = 0; + +void referee_send_frame(uint16_t cmd_id, const uint8_t * data, uint16_t len) +{ + static uint8_t frame[REF_FRAME_MAX_LEN]; + uint16_t i = 0; + + if (len > (uint16_t)(REF_FRAME_MAX_LEN - 9) || data == NULL) + return; + + frame[i++] = 0xA5; + frame[i++] = (uint8_t)(len & 0xFF); + frame[i++] = (uint8_t)(len >> 8); + frame[i++] = ref_tx_seq++; + + uint8_t crc8 = 0xFF; + for (uint16_t k = 0; k < 4; k++) + crc8 = crc8_update(crc8, frame[k]); + frame[i++] = crc8; + + frame[i++] = (uint8_t)(cmd_id & 0xFF); + frame[i++] = (uint8_t)(cmd_id >> 8); + + for (uint16_t k = 0; k < len; k++) + frame[i++] = data[k]; + + uint16_t crc16 = 0xFFFF; + for (uint16_t k = 0; k < i; k++) + crc16 = crc16_update(crc16, frame[k]); + frame[i++] = (uint8_t)(crc16 & 0xFF); + frame[i++] = (uint8_t)(crc16 >> 8); + + usart6_tx_dma_enable(frame, i); +} diff --git a/底盘/modules/comms/referee.h b/底盘/modules/comms/referee.h index e1479f4..673a9bc 100644 --- a/底盘/modules/comms/referee.h +++ b/底盘/modules/comms/referee.h @@ -30,4 +30,10 @@ extern void referee_parse_bytes(const uint8_t * data, uint16_t len); typedef void (*referee_keyboard_hook_f)(const uint8_t data[12]); extern void referee_set_keyboard_hook(referee_keyboard_hook_f hook); +/* ---- 发送(机器人 -> 裁判系统/选手端) ------------------------------------- + * 用于自定义UI(0x0301 机器人交互数据)。data_length 取 payload 字节数(不含 cmd_id, + * 与解析器一致);CRC8 覆盖前 4 字节,CRC16 覆盖除末 2 字节外全部(低字节先)。 + * 内部静态缓冲 + USART6 TX DMA,立即返回,勿高频调用(链路 115200)。 */ +extern void referee_send_frame(uint16_t cmd_id, const uint8_t * data, uint16_t len); + #endif diff --git a/底盘/modules/comms/referee_ui.c b/底盘/modules/comms/referee_ui.c new file mode 100644 index 0000000..407d808 --- /dev/null +++ b/底盘/modules/comms/referee_ui.c @@ -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 + +/* 协议要求:图形数据恰好 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; + } +} diff --git a/底盘/modules/comms/referee_ui.h b/底盘/modules/comms/referee_ui.h new file mode 100644 index 0000000..6484b10 --- /dev/null +++ b/底盘/modules/comms/referee_ui.h @@ -0,0 +1,159 @@ +#ifndef REFEREE_UI_H +#define REFEREE_UI_H + +#include + +/* ============================================================================ + * referee_ui.h - 裁判系统"自定义UI"(机器人 -> 选手端 图形绘制) + * + * 通道: 底盘板 USART6(裁判系统) --cmd 0x0301 机器人交互数据--> 选手端叠加显示 + * 依据: 裁判系统串口协议 自定义UI 章节 + ARUW Taproot / Triton TR-mbed 两套实现 + * 版式: 详见 E:\\ELCT\\_mcp_recon\\hud_mock5.png(坐标与本文 UI_* 宏一一对应) + * + * 硬约束(协议): + * 1. 一条消息最多携带 7 个图形(0x0101/0x0102/0x0103/0x0104 = 1/2/5/7 个) + * 2. 图形总数有限,且图形用"名称"标识 -> 首轮用 ADD,之后一律 MODIFY, + * 否则会在选手端把图形表撑爆 + * 3. 图层数 0~9;坐标原点在屏幕左上角,x 向右、y 向下,画布 1920x1080 + * 4. 不得绘制官方保留区(顶部比分条 / 左下小地图 / 右下战车模块 / 中部两块保留) + * 5. 协议无"圆角矩形/渐变/图标"原语,只能用 线/矩形/圆/椭圆/弧 + 数值/字符 + * ==========================================================================*/ + +/* ---- 图形数据 ------------------------------------------------------------- + * 绘制类(线/矩形/圆/椭圆/弧) 与 数值类(浮点/整型/字符) 都是 15 字节: + * 头 11 字节(所有类型共用): name[3] + operate:3 type:3 layer:4 color:4 + * sa:9 ea:9 width:10 x0:11 y0:11 + * 后 4 字节: 绘制类 = radius:10 x1:11 y1:11;数值类 = 一个 float/int32/char[4] + * 所以用"共用头 + 联合体"表达,两种视图都是 15 字节。 + * 必须 pack(1):否则位域会对齐到 4 字节,整体变 16 字节。 */ +#pragma pack(push, 1) +typedef struct +{ + uint8_t name[3]; /* 图形名(3 字节 ASCII) */ + uint32_t operate : 3; /* UI_OP_* 空/加/改/删 */ + uint32_t type : 3; /* UI_T_* 图形类型 */ + uint32_t layer : 4; /* UI_L_* 图层 0~9 */ + uint32_t color : 4; /* UI_C_* 颜色 */ + uint32_t sa : 9; /* 起始角(弧/椭圆) */ + uint32_t ea : 9; /* 结束角 */ + uint32_t width : 10; /* 线宽 / 小数位 / 字符串长度 */ + uint32_t x0 : 11; /* 起点 x */ + uint32_t y0 : 11; /* 起点 y */ +} ui_head_t; /* 3 + 8 = 11 字节 */ + +typedef struct { ui_head_t h; uint32_t radius : 10, x1 : 11, y1 : 11; } ui_draw_t; +typedef struct { ui_head_t h; union { float f; int32_t i; char s[4]; } val; } ui_num_t; + +typedef union +{ + ui_draw_t d; /* 绘制类视图 */ + ui_num_t n; /* 数值类视图 */ + uint8_t raw[15]; +} ui_graphic_t; +#pragma pack(pop) + +/* ---- 命令 / 操作码 / 图形类型 -------------------------------------------- */ +#define UI_CMD_BASE 0x0301 /* 机器人交互数据 */ +#define UI_CMD_1 0x0101 +#define UI_CMD_2 0x0102 +#define UI_CMD_5 0x0103 +#define UI_CMD_7 0x0104 +#define UI_MAX_PER_PACKET 7 + +#define UI_OP_NOP 0 +#define UI_OP_ADD 1 +#define UI_OP_MOD 2 /* 稳态刷新一律用它 */ +#define UI_OP_DEL 3 + +#define UI_T_LINE 0 +#define UI_T_RECT 1 +#define UI_T_CIRCLE 2 +#define UI_T_ELLIPSE 3 +#define UI_T_ARC 4 +#define UI_T_FLOAT 5 +#define UI_T_INT 6 +#define UI_T_CHAR 7 + +/* ---- 颜色(官方 0~8) ------------------------------------------------------ */ +#define UI_C_RED 0 +#define UI_C_YELLOW 1 +#define UI_C_GREEN 2 +#define UI_C_ORANGE 3 +#define UI_C_PURPLE 4 +#define UI_C_PINK 5 +#define UI_C_CYAN 6 +#define UI_C_BLACK 7 +#define UI_C_WHITE 8 + +/* ---- 图层(协议 0~9) ------------------------------------------------------ */ +#define UI_L_FRAME 0 /* 卡片边框/刻度 */ +#define UI_L_GAUGE 1 /* 能量条/进度条 */ +#define UI_L_TEXT 2 /* 数值/字符 */ + +/* ---- 刷新率:5Hz 约 3.8KB/s(115200 链路),协议允许再调高 ---------------- */ +#define UI_SEND_HZ 5 + +/* ---- 发送/接收方 ID ----------------------------------------------------- + * sender = 本机机器人 ID(referee.robot_id,无裁判系统时用 0) + * receiver= 选手端/客户端(官方 UI 约定 0x8080);若你的协议版本要求填机器人 ID, + * 改这里一处即可。 */ +#define UI_RECEIVER_ID 0x8080 + +/* ---- 画布 ---------------------------------------------------------------- */ +#define UI_SCREEN_W 1920 +#define UI_SCREEN_H 1080 + +/* ---- 版式坐标(与 hud_mock5.png 对应,全部避开官方保留区) ---------------- */ +/* 左上:预估底盘功率 */ /* 左上 x56..520 y176..386 */ +#define UI_PWR_X0 56 +#define UI_PWR_Y0 176 +#define UI_PWR_X1 520 +#define UI_PWR_Y1 386 +/* 左中:超级电容 */ /* x56..520 y404..566 */ +#define UI_CAP_X0 56 +#define UI_CAP_Y0 404 +#define UI_CAP_X1 520 +#define UI_CAP_Y1 566 +/* 顶部:罗盘刻度带(底盘正方向) */ /* x626..1258 y186..270 */ +#define UI_HDG_X0 626 +#define UI_HDG_Y0 186 +#define UI_HDG_X1 1258 +#define UI_HDG_Y1 270 +#define UI_HDG_TAPE_HALF 216 /* 刻度带半宽(像素,对应 ±30°) */ +/* 中央:俯仰梯 */ /* 以屏幕中心为基准 */ +#define UI_LAD_CX 960 +#define UI_LAD_CY 540 +#define UI_LAD_PX_PER_DEG 11 /* 每度 11 像素 */ +#define UI_LAD_HALF_LONG 96 +#define UI_LAD_HALF_SHORT 62 +/* 右上:摩擦轮速度 */ /* x1420..1864 y176..520 */ +#define UI_FRIC_X0 1420 +#define UI_FRIC_Y0 176 +#define UI_FRIC_X1 1864 +#define UI_FRIC_Y1 520 +/* 底部:小陀螺状态 */ /* x700..1220 y874..1000 */ +#define UI_SPIN_X0 700 +#define UI_SPIN_Y0 874 +#define UI_SPIN_X1 1220 +#define UI_SPIN_Y1 1000 + +/* ---- 输入数据(referee_task 每周期填) ----------------------------------- */ +typedef struct +{ + float pitch_deg; /* 云台 pitch(来自 CToC) */ + float yaw_deg; /* 云台 yaw(来自 CToC 姿态帧) */ + float hdg_deg; /* 底盘正方向朝向(本板 att_deg_yaw) */ + float pwr_w; /* 底盘实时功率(裁判 0x0202) */ + float pwr_limit_w; /* 功率上限(裁判 0x0201) */ + float buf_j; /* 缓冲能量/超级电容(J) */ + uint8_t cap_pct; /* 电容百分比(0~100) */ + float fric_l_rpm; /* 左摩擦轮转速(来自 CToC) */ + float fric_r_rpm; /* 右摩擦轮转速 */ + uint8_t spin_on; /* 小陀螺使能 */ + uint8_t ref_online; /* 裁判系统在线(离线时整屏删图形) */ +} ui_data_t; + +/* 周期调用(建议 5Hz):组包并发送;裁判系统离线时发一次"删除全部图层" */ +extern void referee_ui_update(const ui_data_t * d); + +#endif