feat: 添加状态报告和反馈功能,更新协议文档

This commit is contained in:
2026-04-15 00:54:14 +08:00
parent 9313675630
commit e5b2ad20a3
4 changed files with 210 additions and 0 deletions

View File

@@ -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。

View File

@@ -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);

75
Core/Bsp/up_readme.md Normal file
View File

@@ -0,0 +1,75 @@
好的!为了方便你开发上位机,我为你整理了一份详细的**物流小车 TCP 通信协议文档**。这份文档完全基于我们目前代码中的逻辑实现。
---
# 🚚 物流小车 TCP 通信协议文档 (v1.2)
## 1. 协议基础格式
所有数据包(上行和下行)均采用以下固定包装结构:
`LOGI:<PAYLOAD>:<CS>#`
* **`LOGI:`**: 固定帧头。
* **`<PAYLOAD>`**: 有效载荷文本。
* **`:`**: 有效载荷与校验位之间的分隔符。
* **`<CS>`**: 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
```
---
你可以直接复制到你的上位机开发笔记里。如需增加更多字段(如电池电压、电机电流等),我可以随时在底层代码里帮你补齐。

View File

@@ -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:<PAYLOAD>:<CS>#`
- **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` 启动。否则,小车将报警并拒绝运行。
这些是上位机发送的指令:
按照这些来写代码