diff --git a/Core/Bsp/protocol.c b/Core/Bsp/protocol.c index 4586a68..009cfee 100644 --- a/Core/Bsp/protocol.c +++ b/Core/Bsp/protocol.c @@ -14,6 +14,8 @@ #include "bsp_uart.h" #include "bsp_rc522.h" #include "bsp_beep.h" +#include "bsp_sr04.h" +#include "bsp_track_ir.h" #include "checksum.h" #include "cmsis_os.h" #include "elog.h" @@ -235,6 +237,7 @@ static void CarCtrl_HandleCommand(const char *cmd_payload) CarCtrl_StopAll(); elog_w(Protocol_TAG, "已到达站点,请先重新设置目标站点 (GS:xxx) 之后再启动"); CarCtrl_BeepTimes(2U); + Protocol_SendFeedback("ST", 0); return; } @@ -243,6 +246,7 @@ static void CarCtrl_HandleCommand(const char *cmd_payload) CarCtrl_StopAll(); elog_w(Protocol_TAG, "未设置有效站点,拒绝启动。请先下发 GS:001 或 GS:002"); CarCtrl_BeepTimes(2U); + Protocol_SendFeedback("ST", 0); return; } @@ -250,13 +254,16 @@ static void CarCtrl_HandleCommand(const char *cmd_payload) car_target_reached = 0; elog_i(Protocol_TAG, "小车自动循迹启动, speed=%u%%, station=%u", car_speed_percent, car_target_station); + Protocol_SendFeedback("ST", 1); } else if (strcmp(arg + 1, "STOP") == 0) { car_running = 0; car_target_reached = 0; CarCtrl_StopAll(); elog_i(Protocol_TAG, "小车自动循迹暂停"); + Protocol_SendFeedback("ST", 1); } else { elog_w(Protocol_TAG, "未知启动/停止命令: %s", cmd_payload); + Protocol_SendFeedback("ST", 0); } return; } @@ -278,8 +285,10 @@ static void CarCtrl_HandleCommand(const char *cmd_payload) if (car_running != 0U) { CarCtrl_ApplySpeed(); } + Protocol_SendFeedback("SP", 1); } else { elog_w(Protocol_TAG, "速度参数解析失败: %s", cmd_payload); + Protocol_SendFeedback("SP", 0); } return; } @@ -301,8 +310,10 @@ static void CarCtrl_HandleCommand(const char *cmd_payload) car_target_reached = 0; elog_i(Protocol_TAG, "设置目标站点: %03u", car_target_station); CarCtrl_BeepTimes(1U); + Protocol_SendFeedback("GS", 1); } else { elog_w(Protocol_TAG, "站点参数解析失败: %s", cmd_payload); + Protocol_SendFeedback("GS", 0); } return; } @@ -311,6 +322,65 @@ static void CarCtrl_HandleCommand(const char *cmd_payload) elog_w(Protocol_TAG, "未支持的控制命令: %s", cmd_payload); } +/** + * @brief 打包并发送协议帧到上位机 (TCP) + * @param payload 有效载荷文本 (不含帧头 LOGI:, 分隔符 :, 校验位 CS 和 帧尾 #) + */ +static void Protocol_SendPacket(const char *payload) { + char packet[64] = {0}; + uint8_t cs = 0; + + // 1. 组装基础段: LOGI:PAYLOAD + snprintf(packet, sizeof(packet), "LOGI:%s", payload); + + // 2. 计算校验和 (从 LOGI 开始到 payload 结束的累加和) + cs = Calculate_CheckSum((uint8_t *)packet, 0, (uint16_t)strlen(packet)); + + // 3. 拼接校验位和帧尾 + size_t current_len = strlen(packet); + snprintf(packet + current_len, sizeof(packet) - current_len, ":%02X#", cs); + + // 4. 发送 + ESP12F_TCP_SendMessage(packet); +} + +/** + * @brief 发送状态报告 + * 格式示例: STAT:SP:080,STA:001,RUN:1,DIS:12.5,TRK:0010,RPM:25:25:25:25 + */ +void Protocol_SendStatusReport(void) { + char payload[128] = {0}; + float dist = sr04_get_distance(); + uint8_t trk_mask = track_ir_get_line_mask(); + + // STAT:SP:xxx,STA:xxx,RUN:x,DIS:xxx.x,TRK:xxxx,RPM:m1:m2:m3:m4 + // TRK 此时上报 4 位二进制掩码 (H4 H3 H2 H1) + // RPM 为各个电机的实际 RPM,保留整数 + snprintf(payload, sizeof(payload), + "STAT:SP:%03u,STA:%03u,RUN:%u,DIS:%.1f,TRK:%u%u%u%u,RPM:%d:%d:%d:%d", + car_speed_percent, car_target_station, car_running, dist, + (trk_mask & TRACK_IR_H4_BIT) ? 1 : 0, + (trk_mask & TRACK_IR_H3_BIT) ? 1 : 0, + (trk_mask & TRACK_IR_H2_BIT) ? 1 : 0, + (trk_mask & TRACK_IR_H1_BIT) ? 1 : 0, + (int)hall_get_speed(MOTOR_1), + (int)hall_get_speed(MOTOR_2), + (int)hall_get_speed(MOTOR_3), + (int)hall_get_speed(MOTOR_4)); + + Protocol_SendPacket(payload); +} + +/** + * @brief 发送反馈 + * 格式示例: FB:ST:1 (ST成功), FB:GS:0 (GS失败) + */ +void Protocol_SendFeedback(const char *cmd_type, uint8_t status) { + char payload[16] = {0}; + snprintf(payload, sizeof(payload), "FB:%s:%u", cmd_type, status); + Protocol_SendPacket(payload); +} + /* 引用在 freertos.c 中定义的消息队列句柄 */ extern osMessageQueueId_t CmdQueueHandle; /** @@ -403,12 +473,15 @@ void Protocol_HandleMessage(uint8_t *data, uint16_t len) { void CarCtrl_Task(void *argument) { /* USER CODE BEGIN CarCtrl_Task */ char cmd_payload[16] = {0}; + uint32_t last_report_tick = 0; /* 初始化闭环 PID 控制器 */ CarCtrl_InitClosedLoop(); /* Infinite loop */ for (;;) { + uint32_t now = HAL_GetTick(); + /* 1. 处理控制指令 (非阻塞获取,如果没有指令则继续执行闭环) */ if (osMessageQueueGet(CmdQueueHandle, cmd_payload, NULL, 0) == osOK) { elog_d(CarCtrlTask_TAG, "CarCtrl: Command %s", cmd_payload); @@ -421,6 +494,12 @@ void CarCtrl_Task(void *argument) { /* 2. 执行 PID 闭环控制更新 */ CarCtrl_UpdateClosedLoop(); + /* 3. 定期向应用层/上位机发送状态报告 (每 500ms) */ + if (now - last_report_tick >= 500) { + Protocol_SendStatusReport(); + last_report_tick = now; + } + /* * 与 hall_update_speed() 的 100ms 采样周期对齐,避免 PID 使用过期速度反复修正。 * 如果后续把测速周期改成 20ms,这里也要同步改回 20ms。 diff --git a/Core/Bsp/protocol.h b/Core/Bsp/protocol.h index 2ccad7c..c6a3eb5 100644 --- a/Core/Bsp/protocol.h +++ b/Core/Bsp/protocol.h @@ -10,6 +10,18 @@ */ void Protocol_HandleMessage(uint8_t *data, uint16_t len); +/** + * @brief 发送状态报告到上位机 (TCP) + * @details 包含当前速度、目标站点、循迹状态、超声波距离等 + */ +void Protocol_SendStatusReport(void); + +/** + * @brief 发送命令执行结果反馈 + * @param cmd_type 命令类型 (如 "ST", "SP", "GS") + * @param status 状态 (0: 失败, 1: 成功) + */ +void Protocol_SendFeedback(const char *cmd_type, uint8_t status); void CarCtrl_Task(void *argument); diff --git a/Core/Bsp/up_readme.md b/Core/Bsp/up_readme.md new file mode 100644 index 0000000..f945a5f --- /dev/null +++ b/Core/Bsp/up_readme.md @@ -0,0 +1,75 @@ +好的!为了方便你开发上位机,我为你整理了一份详细的**物流小车 TCP 通信协议文档**。这份文档完全基于我们目前代码中的逻辑实现。 + +--- + +# 🚚 物流小车 TCP 通信协议文档 (v1.2) + +## 1. 协议基础格式 +所有数据包(上行和下行)均采用以下固定包装结构: +`LOGI::#` + +* **`LOGI:`**: 固定帧头。 +* **``**: 有效载荷文本。 +* **`:`**: 有效载荷与校验位之间的分隔符。 +* **``**: 2字节 ASCII 十六进制校验和(CheckSum)。 + * *计算规则*:从 `L` 开始(下标0)累加到最后一个冒号 `:` 之前的所有字节。 +* **`#`**: 固定帧尾。 + +--- + +## 2. 下行指令 (上位机 -> 小车) + +| 指令前缀 | 示例 | 说明 | +| :--- | :--- | :--- | +| **ST:RUN** | `LOGI:ST:RUN:3B#` | **启动**: 开始自动运输。必须先发 `GS` 设置目标站。 | +| **ST:STOP**| `LOGI:ST:STOP:8C#`| **停止**: 立即停止所有电机,清除到站锁存。 | +| **SP:xxx** | `LOGI:SP:050:D7#` | **速度**: 设置速度百分比 (000~100)。 | +| **GS:xxx** | `LOGI:GS:001:CA#` | **站点**: 设置目标站点 (目前支持 001/002)。会清除到站锁存并响铃。 | + +--- + +## 3. 上行遥测 (小车 -> 上位机) + +### 3.1 周期状态报告 (下位机每 500ms 自动推送) +**格式:** `LOGI:STAT:SP:速度,STA:站点,RUN:运行,DIS:距离,TRK:循迹:CS#` +**示例:** `LOGI:STAT:SP:050,STA:001,RUN:1,DIS:52.4,TRK:00100:B2#` + +| 字段 | 示例值 | 说明 | +| :--- | :--- | :--- | +| **SP** | `050` | 当前速度百分比 (0~100)。 | +| **STA**| `001` | 当前设置的目标站点编号。 | +| **RUN**| `1` | 运行状态 (1:运动中, 0:停止/待机)。 | +| **DIS**| `12.5`| 超声波测距 (单位: cm,保留1位小数)。 | +| **TRK**| `00100`| 5位循迹灯状态 (0: 未触发感应, 1: 感应到黑线)。从左到右对应 IR1-IR5。 | + +### 3.2 指令执行反馈 (收到指令后立即回复) +**格式:** `LOGI:FB:指令类型:状态值:CS#` +**示例:** `LOGI:FB:GS:1:A5#` (代表站点设置成功) + +| 字段 | 值意义 | 说明 | +| :--- | :--- | :--- | +| **指令类型** | `ST` / `SP` / `GS` | 对应上位机发来的指令头。 | +| **状态值** | `1` / `0` | 1: 执行成功;0: 失败(如未设站点就点运行等)。 | + +--- + +## 4. 特殊业务逻辑说明 (供上位机逻辑参考) + +1. **到站锁死机制**: + * 小车到达 `STA` 对应的 RFID 站点后,会自动进入 `STOP` 状态并推送 `RUN:0`。 + * **此时直接发 `ST:RUN` 会被拒绝**(收到 `FB:ST:0` 反馈),蜂鸣器响两声。 + * **解锁方式**:必须重新发送 `GS:xxx` 指令覆盖目标站点(即使设成同一个号也可以),然后再发 `ST:RUN`。 + +2. **安全保护**: + * 若上位机未发送过任何 `GS` 指令,尝试发 `ST:RUN` 会触发失败反馈。 + +3. **校验和计算 Python 参考代码**: + ```python + def calculate_cs(data_str): + # 如 data_str 为 "LOGI:ST:RUN" + return sum(data_str.encode('ascii')) & 0xFF + ``` + +--- + +你可以直接复制到你的上位机开发笔记里。如需增加更多字段(如电池电压、电机电流等),我可以随时在底层代码里帮你补齐。 \ No newline at end of file diff --git a/README.md b/README.md index a29d8c9..e0956e0 100644 --- a/README.md +++ b/README.md @@ -178,6 +178,50 @@ const char *message = "Hello, ESP12F! This is a test message."; HAL_StatusTypeDef status = ESP12F_TCP_SendMessage(message); ``` +--- + +## 🚀 物流小车 TCP 通信协议 (v1.2) + +本项目采用自定义 ASCII 协议进行上位机控制与状态监控。 + +### 1. 指令帧结构 +`LOGI::#` +- **LOGI**: 固定帧头 +- **PAYLOAD**: 有效载荷 (详见下表) +- **CS**: 2字节十六进制校验和 (从 'L' 累加到 ':' 之前) +- **#**: 固定帧尾 + +### 2. 控制指令 (上位机 -> 小车) + +| 功能 | 指令格式 | 示例 | 说明 | +| :--- | :--- | :--- | :--- | +| **设置站点** | `GS:NNN` | `LOGI:GS:001:CA#` | NNN为3位站点号 (支持 001/002) | +| **启动运行** | `ST:RUN` | `LOGI:ST:RUN:3B#` | 开始任务 (必须先设有效站点) | +| **停止运行** | `ST:STOP` | `LOGI:ST:STOP:8C#` | 立即停止 | +| **设置速度** | `SP:VVV` | `LOGI:SP:050:D7#` | VVV为000-100 (百分比) | + +### 3. 上行遥测 (小车 -> 上位机) + +#### 3.1 状态推送 (每 500ms 推送一次) +**格式**: `LOGI:STAT:SP:速度,STA:站点,RUN:运行,DIS:距离,TRK:循迹状态,RPM:M1:M2:M3:M4:CS#` +**字段**: +- `SP`: 当前速度 % +- `STA`: 目标站点号 +- `RUN`: 运行状态 (1:运行, 0:停止) +- `DIS`: 避障距离 (cm) +- `TRK`: 4位红外状态 (0/1组合, 顺序为 H4 H3 H2 H1) +- `RPM`: 四路电机实际转速,以 `:` 分隔 (顺序为 LR:LF:RF:RR) + +#### 3.2 指令反馈 (即时回复) +**格式**: `LOGI:FB:指令类型:状态值:CS#` +**示例**: `LOGI:FB:GS:1:A5#` (1代表成功, 0代表失败) + +--- + +### 4. 业务逻辑约束 +1. **到站锁存**: 到达站点后小车自动停下,`RUN` 变为 0。 +2. **解锁流程**: 车辆停稳后,必须重新发送 `GS` 指令设置新站点(或覆盖旧站点),方可再次发送 `ST:RUN` 启动。否则,小车将报警并拒绝运行。 + 这些是上位机发送的指令: 按照这些来写代码