feat(debug_log): 新增UART0 RX泵任务支持串口日志转发
新增UART0 RX泵任务功能,将GPIO20接收到的串口数据按切行、剥离后以tag=UART推入ring buffer,通过HTTP /logs接口显示在网页日志面板。 接线方式:任意串口源TX连接至H1/H2的ESP_RX引脚,波特率3M 8N1。 feat(motor_ota): GD32 OTA增加包级防呆机制防止错误固件刷入 中继触发前校验gd32_ota镜像向量表身份(Gd32FlashStore::validateStoredImageIdentity), 对Bootloader包或非APP包在WiFi /ota/relay与BLE 0xF7均拒绝中继, 避免GD32 APP区被错误包擦除。新增0x87完成码用于固件身份验证失败场景。 feat(motor_state): 扩展CAN状态数据结构支持CONFIG负载 扩展motor_slot_t结构体,新增config_rx_count、config_seq和config_data字段 用于存储CONFIG帧的相关信息。CAN状态数据包大小从44B扩展到76B, 页面总数从6调整为9,支持目标值回报的配置数据传输。 fix(motor_ota): 修复OTA取消流程中的信用度管理和任务清理问题 ★ 先复位credit再发送CANCEL命令,防止传输卡住时背压导致CANCEL被拒绝。 延长取消等待时间至10s覆盖最坏情况,确保feedTask正常退出而非强制删除。 取消完成后复位credit防止锁存死锁,确保后续通信正常进行。 fix(http_server): 修复HTTP请求处理中的边界检查和安全验证 拒绝chunked请求(content_len<=0)防止无限循环,添加通道参数范围验证(ch 0~2), 在OTA中继触发前验证固件身份防止错误包刷入,使用http_json_append避免缓冲区越界。 refactor(gd32_flash): 实现GD32固件身份验证算法确保刷写安全 基于Cortex-M向量表内容实现包级身份校验,通过MSP和复位向量位置 区分APP包、Bootloader包和无效固件,为OTA安全提供基础保障。 BREAKING CHANGE: 电机状态API数据格式变更,CAN通道参数验证更严格
This commit is contained in:
@@ -503,6 +503,35 @@ esp_err_t DebugLog::uartInit()
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// UART0 RX 泵:把 RX(GPIO20) 收到的串口行转发进 ring buffer,
|
||||
// 经 HTTP /logs 出现在网页日志面板(tag=UART)。
|
||||
// 接线:H1/H2 排针上任意串口源 TX → ESP_RX(GPIO20),3M 8N1。
|
||||
// ============================================================
|
||||
static void uartRxPumpTask(void *arg)
|
||||
{
|
||||
(void)arg;
|
||||
uint8_t chunk[64];
|
||||
char line[128];
|
||||
int pos = 0;
|
||||
|
||||
for (;;) {
|
||||
const int n = uart_read_bytes(UART_NUM_0, chunk, sizeof(chunk), portMAX_DELAY);
|
||||
for (int i = 0; i < n; i++) {
|
||||
const char c = (char)chunk[i];
|
||||
if (c == '\n' || pos >= (int)sizeof(line) - 1) {
|
||||
if (pos > 0) {
|
||||
line[pos] = '\0';
|
||||
DebugLog::instance().log(DEBUG_LOG_INFO, "UART", "%s", line);
|
||||
pos = 0;
|
||||
}
|
||||
} else if (c != '\r') {
|
||||
line[pos++] = c;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
esp_err_t DebugLog::uartSend(const uint8_t *data, size_t len)
|
||||
{
|
||||
if (!m_uartInitialized || !m_uartAvailable) {
|
||||
@@ -558,6 +587,15 @@ esp_err_t DebugLog::init()
|
||||
m_debugAddr.sin_port = htons(DEBUG_LOG_PORT);
|
||||
m_debugAddr.sin_addr.s_addr = inet_addr(DEBUG_LOG_IP);
|
||||
|
||||
/* UART0 RX 泵:串口行 → ring → /logs 网页面板 */
|
||||
if (m_uartAvailable) {
|
||||
if (xTaskCreate(uartRxPumpTask, "uart_rx", 3072, nullptr, 1, nullptr) == pdPASS) {
|
||||
ESP_LOGI(TAG, "UART0 RX 泵已启动 (GPIO%d → /logs)", UART_DEBUG_RX_GPIO);
|
||||
} else {
|
||||
ESP_LOGW(TAG, "UART0 RX 泵任务创建失败(堆不足?)");
|
||||
}
|
||||
}
|
||||
|
||||
m_initialized = true;
|
||||
ESP_LOGI(TAG, "调试日志已初始化,上位机 %s:%d", DEBUG_LOG_IP, DEBUG_LOG_PORT);
|
||||
return ESP_OK;
|
||||
|
||||
@@ -288,13 +288,16 @@ const uint32_t remaining = s_fw_size - s_fw_offset;
|
||||
|
||||
/* feedTask 字节喂完 ≠ GD32/电机已完成。suppress 期间 0xD2 被丢;
|
||||
* 末包 CAN ACK / 跳转 APP 还要数百 ms。只 QUERY 一次会把前端卡在 kTransfer。
|
||||
* 退出后轮询 QUERY,直到 GD32 进 FINISHED/ERROR/IDLE,或最多 ~8s。 */
|
||||
* 退出后轮询 QUERY,直到 GD32 进 FINISHED/ERROR/IDLE,或最多 ~8s。
|
||||
* 用户取消(s_feed_running 已被 motorOtaCancel 置 false)时跳过:
|
||||
* 设备端已进 IDLE、GD32 已 resetToIdle,无终态可等——立即收尾退出,
|
||||
* 把 cancel task 的等待窗口压到最短(也消除强删窗口)。 */
|
||||
{
|
||||
constexpr uint32_t kExitPollMs = 8000U;
|
||||
constexpr uint32_t kExitGapMs = 150U;
|
||||
const uint32_t exit_deadline = nowMs() + kExitPollMs;
|
||||
bool got_terminal = false;
|
||||
while (nowMs() < exit_deadline) {
|
||||
while (s_feed_running && nowMs() < exit_deadline) {
|
||||
/* 优先吃 ring 推送(已恢复 suppress) */
|
||||
{
|
||||
const motor_ota_status_t st = motorOtaGetStatus();
|
||||
@@ -395,6 +398,25 @@ int motorOtaStart(uint32_t fw_size, uint8_t channel)
|
||||
&resp_type, resp_payload, sizeof(resp_payload), &resp_len,
|
||||
300U); /* 300ms: fire-and-forget,让 worker 有足够时间完成 START。
|
||||
* 原 500ms 偏保守;GD32 收到 START 立即进 kPreCancel,<50ms 响应。 */
|
||||
if (ret == ESP_OK && resp_type != kRspAck && resp_len >= 1U &&
|
||||
resp_payload[0] == 0xE5U) {
|
||||
/* 0xE5 = GD32 端 relay 会话残留(上次 CANCEL 丢失/未达):不清会直接
|
||||
* 让本次 OTA 空转失败。补发 CANCEL(GD32 resetToIdle 归 kIdle)后
|
||||
* 重试一次 START,一次自愈即恢复,无需用户重试或刷新。 */
|
||||
DEBUG_WARN(TAG, "START 0xE5(GD32 残留会话)→ 补发 CANCEL 后重试 START");
|
||||
uint8_t ccmd[1] = {1U};
|
||||
uint8_t crt = 0;
|
||||
uint8_t crp[4] = {0};
|
||||
size_t crl = 0;
|
||||
SpiMaster::instance().resetCredit(); /* 解除背压,保证 CANCEL 发出 */
|
||||
(void)SpiMaster::instance().transact(
|
||||
kSpiMotorOtaCmd, ccmd, 1, &crt, crp, sizeof(crp), &crl, 500U);
|
||||
SpiMaster::instance().resetCredit(); /* GD32 已 resetToIdle,窗口对齐满 */
|
||||
ret = SpiMaster::instance().transact(
|
||||
kSpiMotorOtaCmd, cmd, 6,
|
||||
&resp_type, resp_payload, sizeof(resp_payload), &resp_len,
|
||||
300U);
|
||||
}
|
||||
if (ret != ESP_OK || resp_type != kRspAck) {
|
||||
DEBUG_WARN(TAG, "START SPI 未确认 ACK (ret=%s resp=0x%02X)——fire-and-forget 继续",
|
||||
esp_err_to_name(ret), resp_type);
|
||||
@@ -441,7 +463,7 @@ void motorOtaCancel(void)
|
||||
}
|
||||
|
||||
/* 通知 GD32 取消 relay 状态机 + 等 feedTask 退出是耗时操作
|
||||
*(SPI transact 最多 1s + feedTask 退出最多 2s),不能在 httpd 线程
|
||||
*(SPI transact 最多 1s + feedTask 退出最长 ~8.2s),不能在 httpd 线程
|
||||
* 同步阻塞,否则 cancel HTTP 请求本身会超时("Failed to fetch"),
|
||||
* 且 httpd 单线程会饿死其他端点。
|
||||
* 用独立 task 异步执行,HTTP handler 立即返回。 */
|
||||
@@ -453,7 +475,12 @@ void motorOtaCancel(void)
|
||||
}
|
||||
|
||||
auto do_cancel = [](void *) {
|
||||
/* 通知 GD32 取消 relay 状态机 */
|
||||
/* ★ 先复位 credit 再发 CANCEL:取消的典型时机恰是传输卡住(GD32 缓冲满,
|
||||
* 最后一帧 RESPONSE credit=0 已锁存进 m_remoteCredit)——不先解除背压,
|
||||
* CANCEL 自己会被 CREDIT 背压 refuse,GD32 端 relay 会话收不到取消而残留,
|
||||
* 下次 START 被 GD32 0xE5 拒 → "取消后重新开始没法开始"。
|
||||
* GD32 收到 CANCEL 后 resetToIdle 恢复满窗口,与复位值对齐。 */
|
||||
SpiMaster::instance().resetCredit();
|
||||
uint8_t cmd[1] = {1U}; /* cmd=CANCEL */
|
||||
uint8_t resp_type = 0;
|
||||
uint8_t resp_payload[4];
|
||||
@@ -462,22 +489,43 @@ void motorOtaCancel(void)
|
||||
kSpiMotorOtaCmd, cmd, 1,
|
||||
&resp_type, resp_payload, sizeof(resp_payload), &resp_len,
|
||||
1000U);
|
||||
if (ret != ESP_OK) {
|
||||
/* 重试一次:再复位 credit(防首次期间又被失配帧锁 0)后重发 */
|
||||
vTaskDelay(pdMS_TO_TICKS(50));
|
||||
SpiMaster::instance().resetCredit();
|
||||
ret = SpiMaster::instance().transact(
|
||||
kSpiMotorOtaCmd, cmd, 1,
|
||||
&resp_type, resp_payload, sizeof(resp_payload), &resp_len,
|
||||
1000U);
|
||||
}
|
||||
DEBUG_WARN(TAG, "cancel: GD32 resp ret=%s type=0x%02X", esp_err_to_name(ret), resp_type);
|
||||
|
||||
/* 等待 feedTask 真正退出(最多 2s),避免 s_feed_running 已清但 task
|
||||
* 仍卡在 SpiMaster::transact 阻塞中,导致 motorOtaIsActive() 瞬时仍 true。 */
|
||||
/* 等待 feedTask 真正退出。feedTask 最坏存活:主循环收尾 ≤0.2s +
|
||||
* 退出轮询 8s ≈ 8.2s(120s 整体超时路径同样有界),等待上限取 10s
|
||||
* 覆盖之——正常路径 feedTask 因 s_status 已被置 IDLE 在 ~0.3s 内退出,
|
||||
* 这里只是兜底;尽量不强删(强删会泄漏 job 槽 + worker 悬垂栈写)。 */
|
||||
TaskHandle_t t = s_feed_task;
|
||||
if (t) {
|
||||
for (int i = 0; i < 200 && s_feed_task != nullptr; i++) {
|
||||
for (int i = 0; i < 1000 && s_feed_task != nullptr; i++) {
|
||||
vTaskDelay(pdMS_TO_TICKS(10));
|
||||
}
|
||||
if (s_feed_task != nullptr) {
|
||||
DEBUG_ERR(TAG, "cancel: feedTask 未在 2s 内退出,强制删除");
|
||||
DEBUG_ERR(TAG, "cancel: feedTask 未在 10s 内退出,强制删除");
|
||||
vTaskDelete(s_feed_task);
|
||||
s_feed_task = nullptr;
|
||||
/* 强删绕过了 feedTask 收尾,suppress 必须在此恢复,
|
||||
* 否则 ring service 永久关闭 → GD32 推送全部收不到。 */
|
||||
SpiMaster::instance().setSuppressRingService(false);
|
||||
}
|
||||
}
|
||||
s_feed_running = false; /* 双保险 */
|
||||
/* credit 复位:取消的典型时机是传输卡住(GD32 缓冲满)——最后一帧
|
||||
* RESPONSE 的 credit=0(session 合法)被锁存进 m_remoteCredit。
|
||||
* GD32 resetToIdle 后转空闲不再发帧,relay 又跑在 APP(不重启、
|
||||
* 无 session=0 帧)→ 没有任何 resync 入口能把 credit 送回来,
|
||||
* 后续所有 transact 被 CREDIT 背压拒绝 → 网页所有消息收不到。
|
||||
* GD32 端 clearOtaSessionQueues 已把接收窗口恢复满,此处对齐复位。 */
|
||||
SpiMaster::instance().resetCredit();
|
||||
cancel_task = nullptr;
|
||||
vTaskDelete(NULL);
|
||||
};
|
||||
@@ -487,6 +535,7 @@ void motorOtaCancel(void)
|
||||
DEBUG_ERR(TAG, "cancel: failed to create cancel task, doing sync cancel");
|
||||
cancel_task = nullptr;
|
||||
/* 降级:同步执行(极端 OOM 情况) */
|
||||
SpiMaster::instance().resetCredit(); /* 先解除背压,保证 CANCEL 能发出 */
|
||||
uint8_t cmd[1] = {1U};
|
||||
uint8_t resp_type = 0;
|
||||
uint8_t resp_payload[4];
|
||||
@@ -503,9 +552,11 @@ void motorOtaCancel(void)
|
||||
if (s_feed_task != nullptr) {
|
||||
vTaskDelete(s_feed_task);
|
||||
s_feed_task = nullptr;
|
||||
SpiMaster::instance().setSuppressRingService(false);
|
||||
}
|
||||
}
|
||||
s_feed_running = false;
|
||||
SpiMaster::instance().resetCredit(); /* 同 do_cancel:防 credit=0 锁存死锁 */
|
||||
}
|
||||
DEBUG_WARN(TAG, "cancel: dispatched feed_running=%d", (int)s_feed_running);
|
||||
}
|
||||
|
||||
@@ -65,6 +65,9 @@ bool onMotorStateEvent(const uint8_t *payload, size_t len)
|
||||
slot.rx_count = rdU32Le(&e[4]);
|
||||
slot.seq = rdU32Le(&e[8]);
|
||||
memcpy(slot.data, &e[12], MOTOR_STATUS_DLC);
|
||||
slot.config_rx_count = rdU32Le(&e[44]);
|
||||
slot.config_seq = rdU32Le(&e[48]);
|
||||
memcpy(slot.config_data, &e[52], MOTOR_CONFIG_DLC);
|
||||
|
||||
offset += MOTOR_ENTRY_SIZE;
|
||||
}
|
||||
|
||||
@@ -6,10 +6,12 @@
|
||||
*
|
||||
* payload 格式(每页):
|
||||
* [0] page, [1] total_pages, [2] entry_count, [3] active_count
|
||||
* [4..] 每个 entry 44B:
|
||||
* [4..] 每个 entry 76B:
|
||||
* [0] node_id, [1] channel, [2] online, [3] dlc,
|
||||
* [4..7] rx_count (LE), [8..11] seq (LE),
|
||||
* [12..43] data[32] (原始 STATUS payload)
|
||||
* [44..47] config_rx_count (LE), [48..51] config_seq (LE),
|
||||
* [52..75] config_data[24] (原始 CONFIG payload, 目标值回报)
|
||||
*
|
||||
* 同一节点号可能同时挂在多个 CAN 通道(两台电机同号异线),
|
||||
* 缓存表与 GD32 端一致按 node×channel 双键存储(27 槽)。
|
||||
@@ -29,9 +31,10 @@ extern "C" {
|
||||
#define MOTOR_CHANNELS 3
|
||||
#define MOTOR_MAX_SLOTS (MOTOR_MAX_NODES * MOTOR_CHANNELS)
|
||||
#define MOTOR_STATUS_DLC 32
|
||||
#define MOTOR_ENTRY_SIZE 44
|
||||
#define MOTOR_CONFIG_DLC 24
|
||||
#define MOTOR_ENTRY_SIZE 76
|
||||
#define MOTOR_PAGE_HEADER 4
|
||||
#define MOTOR_TOTAL_PAGES 6
|
||||
#define MOTOR_TOTAL_PAGES 9
|
||||
|
||||
/** @brief 单电机状态快照。 */
|
||||
typedef struct {
|
||||
@@ -42,6 +45,9 @@ typedef struct {
|
||||
uint32_t rx_count; ///< 累计收到帧数
|
||||
uint32_t seq; ///< GD32 侧更新序号
|
||||
uint8_t data[MOTOR_STATUS_DLC]; ///< 原始 STATUS payload
|
||||
uint32_t config_rx_count; ///< 累计收到 CONFIG 帧数
|
||||
uint32_t config_seq; ///< CONFIG 更新序号
|
||||
uint8_t config_data[MOTOR_CONFIG_DLC]; ///< 原始 CONFIG payload (目标值回报)
|
||||
} motor_slot_t;
|
||||
|
||||
/** @brief 全局电机状态表。 */
|
||||
|
||||
Reference in New Issue
Block a user