feat(esp32): 添加多电机OTA中继功能并优化调试日志系统
- 新增多电机状态、版本和OTA中继监控模块(monitor/motor_state.cpp, monitor/motor_version.cpp, monitor/motor_ota_relay.cpp) - 在CAN反馈任务中添加0xC5/0xC6/0xD2类型消息处理,支持多电机状态推送 - 重构调试日志系统,将一次性JSON分配改为512B栈缓冲流式发送,降低内存峰值 - 调试UART波特率从115200提升至30000 - 修改版本管理方式,从自动生version.h改为手动维护 - 更新VSCode设置以支持ESP-IDF开发环境配置 - 在OTA协调器中添加电机OTA互斥机制,防止并发冲突 - 完善中继状态机异常处理,添加远程取消通知机制
This commit is contained in:
@@ -22,6 +22,9 @@
|
||||
#include "monitor/debug_log.h"
|
||||
#include "monitor/can_feedback_task.h"
|
||||
#include "monitor/gd32_adc_feedback.h"
|
||||
#include "monitor/motor_state.h"
|
||||
#include "monitor/motor_version.h"
|
||||
#include "monitor/motor_ota_relay.h"
|
||||
#include "app_config.h"
|
||||
#include "esp_system.h"
|
||||
|
||||
@@ -33,6 +36,9 @@ constexpr uint32_t kWaitRingMs = 500U; ///< 单次等待 GD32 按铃超
|
||||
constexpr uint32_t kPauseDelayMs = 100U; ///< 暂停时的轮询间隔
|
||||
constexpr uint8_t kSpiCanFeedbackType = 0xC1U;
|
||||
constexpr uint8_t kSpiAdcFeedbackType = 0xC4U; ///< GD32 ADC 调试反馈
|
||||
constexpr uint8_t kSpiMotorStateType = 0xC5U; ///< GD32 多电机状态推送
|
||||
constexpr uint8_t kSpiMotorVersionType = 0xC6U; ///< GD32 多电机版本推送(事件驱动)
|
||||
constexpr uint8_t kSpiMotorOtaStatusType = 0xD2U; ///< GD32 电机 OTA relay 进度推送
|
||||
constexpr size_t kRingPayloadMax = SPI_OTA_FRAME_MAX; ///< 与变长读一致,避免截断
|
||||
constexpr const char *kTag = "CAN_FB";
|
||||
|
||||
@@ -102,6 +108,24 @@ static void taskLoop(void *)
|
||||
continue;
|
||||
}
|
||||
|
||||
/* 0xC5 多电机状态推送:GD32 250ms 分页推送,ESP32 解析入缓存 */
|
||||
if (ring_type == kSpiMotorStateType) {
|
||||
(void)onMotorStateEvent(ring_payload, ring_len);
|
||||
continue;
|
||||
}
|
||||
|
||||
/* 0xC6 多电机版本推送:GD32 收到电机版本应答(0x00FF)后事件驱动推送 */
|
||||
if (ring_type == kSpiMotorVersionType) {
|
||||
(void)onMotorVersionEvent(ring_payload, ring_len);
|
||||
continue;
|
||||
}
|
||||
|
||||
/* 0xD2 电机 OTA relay 进度推送 */
|
||||
if (ring_type == kSpiMotorOtaStatusType) {
|
||||
onMotorOtaStatusPush(ring_payload, ring_len);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (ring_type != kSpiCanFeedbackType || ring_len < 28U) {
|
||||
continue;
|
||||
}
|
||||
|
||||
+50
-97
@@ -6,7 +6,7 @@
|
||||
* 2. UDP 单播到指定上位机(独立 UDP 调试接收端可选)
|
||||
* 3. 内部环形缓冲区(供 HTTP /logs 拉取,WebUI 实际使用这一条)
|
||||
*
|
||||
* UART0: TX=GPIO21, RX=GPIO20, 115200 波特率
|
||||
* UART0: TX=GPIO21, RX=GPIO20, 3000000 波特率
|
||||
* UDP: <DEBUG_LOG_IP>:<DEBUG_LOG_PORT>
|
||||
*
|
||||
* 时序说明:
|
||||
@@ -68,17 +68,6 @@ bool DebugLog::isKeepAliveTag(const char *tag)
|
||||
return false;
|
||||
}
|
||||
|
||||
char *DebugLog::allocEmptyJson(int *out_len)
|
||||
{
|
||||
/* 返回 nullptr 让 caller 走"[]"字面量分支,避免空环时每秒 malloc(3)/free(3)。
|
||||
* ringDrainJson 的所有"返空"路径都调本函数,caller logs_handler 见 nullptr
|
||||
* 后直接发 "[]" 字面量,零分配。 */
|
||||
if (out_len) {
|
||||
*out_len = 0;
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
/**
|
||||
* JSON 字符串内容转义写入 buf[pos..];返回新 pos。
|
||||
* 控制字符除 \n\r\t 外写成 \u00XX;空间不足时截断并尽量写 "..."。
|
||||
@@ -384,97 +373,62 @@ void DebugLog::ringPush(const char *data, int len)
|
||||
}
|
||||
|
||||
/**
|
||||
* 单次持锁:peek 元数据 →(可选)按堆缩小条数 → pop+转义拼 JSON。
|
||||
* capacity 按真实 raw 长度估算,避免 48×320 虚高导致 OTA 期返空。
|
||||
* 流式导出:peek 快照条数 → 逐条 pop(仅 pop 期间持锁)转义拼 JSON →
|
||||
* 攒满 512B 栈缓冲即经 sink 发一个 chunk。替代原 ~4KB 一次性 malloc,
|
||||
* 把 /logs 发送路径的应用层瞬时峰值从几 KB 降到栈缓冲级别。
|
||||
*/
|
||||
char *DebugLog::ringDrainJson(int *out_len)
|
||||
int DebugLog::ringDrainJsonStream(LogJsonSink sink, void *ctx)
|
||||
{
|
||||
if (!out_len) {
|
||||
return nullptr;
|
||||
if (!sink) {
|
||||
return 0;
|
||||
}
|
||||
*out_len = 0;
|
||||
|
||||
const uint32_t free_now = esp_get_free_heap_size();
|
||||
if (free_now < kDrainMinFree || !m_logRingMutex) {
|
||||
return allocEmptyJson(out_len);
|
||||
return 0; /* 低堆/未就绪:caller 一次性发 "[]" */
|
||||
}
|
||||
|
||||
/* 先 peek 快照条数:无内容则让 caller 走 "[]" 一次性响应,不进 chunked 编码。 */
|
||||
xSemaphoreTake(m_logRingMutex, portMAX_DELAY);
|
||||
|
||||
int total_raw = 0;
|
||||
int drain_n = ringPeekDrainMetaLocked(LOG_DRAIN_MAX, &total_raw);
|
||||
if (drain_n <= 0) {
|
||||
xSemaphoreGive(m_logRingMutex);
|
||||
return allocEmptyJson(out_len);
|
||||
}
|
||||
|
||||
/* 转义最坏 ~2x + 每条 "" 与逗号 + 数组括号 */
|
||||
auto calc_capacity = [](int n, int raw) -> int {
|
||||
int cap = raw * 2 + n * 4 + 16;
|
||||
if (cap < 16) {
|
||||
cap = 16;
|
||||
}
|
||||
return cap;
|
||||
};
|
||||
|
||||
int capacity = calc_capacity(drain_n, total_raw);
|
||||
const size_t largest = heap_caps_get_largest_free_block(MALLOC_CAP_DEFAULT);
|
||||
|
||||
/* 堆不够则减条数重估(仍持锁,peek 只读;减 n 时从最旧开始仍一致) */
|
||||
while (drain_n > 0 && largest < (size_t)capacity) {
|
||||
drain_n = drain_n / 2;
|
||||
if (drain_n == 0) {
|
||||
break;
|
||||
}
|
||||
total_raw = 0;
|
||||
drain_n = ringPeekDrainMetaLocked(drain_n, &total_raw);
|
||||
capacity = calc_capacity(drain_n, total_raw);
|
||||
}
|
||||
|
||||
if (drain_n <= 0 || largest < (size_t)capacity) {
|
||||
xSemaphoreGive(m_logRingMutex);
|
||||
return allocEmptyJson(out_len);
|
||||
}
|
||||
|
||||
/* 持锁期间尽量短:只 pop 到栈上临时区会爆栈;改为直接写入 malloc 缓冲。
|
||||
* 先释放锁再 malloc,再二次持锁 pop——中间可能插入新日志,只 drain 最旧 N 条即可。 */
|
||||
xSemaphoreGive(m_logRingMutex);
|
||||
|
||||
char *buf = (char *)malloc((size_t)capacity);
|
||||
if (!buf) {
|
||||
return allocEmptyJson(out_len);
|
||||
if (drain_n <= 0) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
xSemaphoreTake(m_logRingMutex, portMAX_DELAY);
|
||||
|
||||
/* 重新核对:ring 可能变少 */
|
||||
if (m_logRingCount <= 0) {
|
||||
xSemaphoreGive(m_logRingMutex);
|
||||
free(buf);
|
||||
return allocEmptyJson(out_len);
|
||||
}
|
||||
if (drain_n > m_logRingCount) {
|
||||
drain_n = m_logRingCount;
|
||||
}
|
||||
if (drain_n > LOG_DRAIN_MAX) {
|
||||
drain_n = LOG_DRAIN_MAX;
|
||||
}
|
||||
/* 512B 栈缓冲替代原 ~4KB malloc。单条转义最坏 2×LOG_RING_SIZE,
|
||||
* flush 后空缓冲须能容纳单条 + 括号余量(见 static_assert)。 */
|
||||
static constexpr int STREAM_BUF_CAP = 512;
|
||||
static_assert(STREAM_BUF_CAP >= LOG_RING_SIZE * 2 + 8,
|
||||
"stream buf must hold one worst-case escaped entry");
|
||||
char buf[STREAM_BUF_CAP];
|
||||
char entry[LOG_RING_SIZE];
|
||||
|
||||
int pos = 0;
|
||||
buf[pos++] = '[';
|
||||
bool first = true;
|
||||
char entry[LOG_RING_SIZE];
|
||||
|
||||
for (int k = 0; k < drain_n; ++k) {
|
||||
/* 先估空间再 pop,避免 pop 后写不下导致丢条 */
|
||||
const int need_min = first ? 4 : 5; /* ,"x" 或 "x" 量级 + 闭合余量 */
|
||||
if (pos + need_min >= capacity - 2) {
|
||||
break;
|
||||
/* 逐条 pop:仅在 pop 期间持锁,绝不在 sink(网络 I/O)时持锁,
|
||||
* 避免阻塞其它任务写日志。 */
|
||||
int entry_len = 0;
|
||||
xSemaphoreTake(m_logRingMutex, portMAX_DELAY);
|
||||
bool popped = ringPopOldestLocked(entry, sizeof(entry), &entry_len) && entry_len > 0;
|
||||
xSemaphoreGive(m_logRingMutex);
|
||||
if (!popped) {
|
||||
break; /* 环已空(并发被别处 drain) */
|
||||
}
|
||||
|
||||
int entry_len = 0;
|
||||
if (!ringPopOldestLocked(entry, sizeof(entry), &entry_len) || entry_len <= 0) {
|
||||
break;
|
||||
/* 预留单条转义最坏占用再决定是否 flush:jsonEscapeAppend 内部把单条封顶在
|
||||
* LOG_RING_SIZE*2(控制符 \u00XX 会 6× 膨胀),故按该上限 + 逗号/引号/收尾预留,
|
||||
* 与 entry_len 无关;放不下先 flush,flush 后空缓冲必能完整容纳单条不被截断。 */
|
||||
const int need = LOG_RING_SIZE * 2 + 8;
|
||||
if (pos + need > STREAM_BUF_CAP) {
|
||||
if (!sink(ctx, buf, pos)) {
|
||||
return -1;
|
||||
}
|
||||
pos = 0;
|
||||
}
|
||||
|
||||
if (!first) {
|
||||
@@ -482,27 +436,26 @@ char *DebugLog::ringDrainJson(int *out_len)
|
||||
}
|
||||
first = false;
|
||||
buf[pos++] = '"';
|
||||
pos = jsonEscapeAppend(buf, pos, capacity, entry, entry_len);
|
||||
if (pos < capacity - 1) {
|
||||
pos = jsonEscapeAppend(buf, pos, STREAM_BUF_CAP, entry, entry_len);
|
||||
if (pos < STREAM_BUF_CAP - 1) {
|
||||
buf[pos++] = '"';
|
||||
} else if (pos < capacity) {
|
||||
buf[pos - 1] = '"'; /* 极端:覆盖末字节闭合 */
|
||||
} else {
|
||||
buf[pos - 1] = '"'; /* 极端兜底:覆盖末字节闭合 */
|
||||
}
|
||||
}
|
||||
|
||||
if (pos + 1 < capacity) {
|
||||
buf[pos++] = ']';
|
||||
buf[pos] = '\0';
|
||||
} else if (capacity >= 3) {
|
||||
buf[capacity - 2] = ']';
|
||||
buf[capacity - 1] = '\0';
|
||||
pos = capacity - 1;
|
||||
/* 收尾 ']':缓冲放不下先 flush,再单独发闭合。 */
|
||||
if (pos + 1 > STREAM_BUF_CAP) {
|
||||
if (!sink(ctx, buf, pos)) {
|
||||
return -1;
|
||||
}
|
||||
pos = 0;
|
||||
}
|
||||
|
||||
xSemaphoreGive(m_logRingMutex);
|
||||
|
||||
*out_len = pos;
|
||||
return buf;
|
||||
buf[pos++] = ']';
|
||||
if (!sink(ctx, buf, pos)) {
|
||||
return -1;
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
* 注意:当前 tools/WebUI 日志面板走的是 HTTP /logs,
|
||||
* 不是直接监听 UDP 6666。
|
||||
*
|
||||
* UART0: TX=GPIO21, RX=GPIO20, 115200 波特率
|
||||
* UART0: TX=GPIO21, RX=GPIO20, 3000000 波特率
|
||||
*
|
||||
* 线程模型:DEBUG_* / log() / send() 仅允许在任务上下文调用
|
||||
* (内部使用 portMAX_DELAY 互斥锁,禁止 ISR)。
|
||||
@@ -31,7 +31,7 @@
|
||||
// UART0 调试配置
|
||||
#define UART_DEBUG_TX_GPIO 21 ///< UART0 TX GPIO 引脚号
|
||||
#define UART_DEBUG_RX_GPIO 20 ///< UART0 RX GPIO 引脚号
|
||||
#define UART_DEBUG_BAUD_RATE 115200 ///< UART0 波特率
|
||||
#define UART_DEBUG_BAUD_RATE 3000000 ///< UART0 波特率
|
||||
|
||||
// 日志级别
|
||||
typedef enum {
|
||||
@@ -41,6 +41,12 @@ typedef enum {
|
||||
DEBUG_LOG_DBG = 4 // 调试
|
||||
} debug_log_level_t;
|
||||
|
||||
/**
|
||||
* @brief /logs 流式发送 sink:每个 chunk 回调一次;返回 false 表示发送失败需中止。
|
||||
* @param ctx 透传上下文(如 httpd_req_t*)。
|
||||
*/
|
||||
typedef bool (*LogJsonSink)(void *ctx, const char *data, int len);
|
||||
|
||||
/**
|
||||
* @brief 调试日志类(单例)
|
||||
*
|
||||
@@ -79,11 +85,15 @@ public:
|
||||
bool ringPopOldest(char *out, size_t out_size, int *out_len);
|
||||
|
||||
/**
|
||||
* @brief 将环形缓冲区日志导出为 JSON 数组字符串。
|
||||
* @param out_len 输出 JSON 字符串长度。
|
||||
* @return 动态分配的 JSON 字符串,调用者须 free();失败返回 NULL。
|
||||
* @brief 将环形日志以 chunked JSON 数组流式发送,消除一次性大块 malloc。
|
||||
* 按条 pop(短持锁),512B 栈缓冲攒满即经 sink 发一个 chunk;
|
||||
* 峰值从原 ~4KB malloc 降为栈缓冲 + 小 chunk。
|
||||
* @param sink 每个 chunk 的发送回调;返回 false 表示发送失败。
|
||||
* @param ctx 透传给 sink 的上下文。
|
||||
* @return 1=已流式发送完整数组(调用方需补发终止 chunk);
|
||||
* 0=无日志/堆门控(调用方一次性发 "[]");-1=中途发送失败。
|
||||
*/
|
||||
char* ringDrainJson(int *out_len);
|
||||
int ringDrainJsonStream(LogJsonSink sink, void *ctx);
|
||||
|
||||
void setLogLevel(debug_log_level_t level) { m_logLevel = level; }
|
||||
debug_log_level_t getLogLevel() const { return m_logLevel; }
|
||||
@@ -129,7 +139,7 @@ private:
|
||||
* free < kDropInfoFree → 非 keep-alive 丢 INFO/DBG;keep-alive 保留 INFO
|
||||
* free < kDropWarnFree → 全局丢 DBG
|
||||
* free < kSkipUdpFree → 不 sendto(仍 UART+ring)
|
||||
* free < kDrainMinFree → /logs 返 "[]" 避免大块 malloc
|
||||
* free < kDrainMinFree → /logs 返 "[]"(低堆时连流式小 chunk 的 pbuf 也省)
|
||||
* keep-alive tag:OTA 前缀、SPI、WIFI */
|
||||
static constexpr uint32_t kDropWarnFree = 6U * 1024U;
|
||||
static constexpr uint32_t kDropInfoFree = 10U * 1024U;
|
||||
@@ -151,7 +161,6 @@ private:
|
||||
int ringPeekDrainMetaLocked(int max_n, int *total_raw_len) const;
|
||||
|
||||
static bool isKeepAliveTag(const char *tag);
|
||||
static char *allocEmptyJson(int *out_len);
|
||||
static int jsonEscapeAppend(char *buf, int pos, int capacity, const char *src, int src_len);
|
||||
};
|
||||
|
||||
|
||||
@@ -0,0 +1,458 @@
|
||||
/*
|
||||
* motor_ota_relay.cpp - 电机 OTA relay ESP32 侧实现
|
||||
*/
|
||||
#include "monitor/motor_ota_relay.h"
|
||||
#include "monitor/debug_log.h"
|
||||
#include "transport/spi_master.h"
|
||||
#include "storage/gd32_flash_store.h"
|
||||
#include "esp_log.h"
|
||||
#include "esp_timer.h"
|
||||
#include "freertos/FreeRTOS.h"
|
||||
#include "freertos/task.h"
|
||||
#include "freertos/semphr.h"
|
||||
#include <string.h>
|
||||
|
||||
static const char *TAG = "MOTOR_OTA";
|
||||
|
||||
/* SPI 类型常量(与 GD32 app_spi_command_defs.h 对齐) */
|
||||
static constexpr uint8_t kSpiMotorOtaCmd = 0xD0U;
|
||||
static constexpr uint8_t kSpiMotorOtaData = 0xD1U;
|
||||
static constexpr uint8_t kSpiMotorOtaStatus = 0xD2U;
|
||||
static constexpr uint8_t kRspAck = 0x80U;
|
||||
static constexpr uint8_t kRspWait = 0x81U;
|
||||
|
||||
/* 内部状态 */
|
||||
static SemaphoreHandle_t s_mutex = nullptr;
|
||||
static motor_ota_status_t s_status = {};
|
||||
/* 固件不再整体缓冲在堆里:HTTP handler 流式写入 gd32_ota flash 分区,
|
||||
* feedTask 经 flashReadChunk 分块读取(见 feedTask)。 */
|
||||
static uint32_t s_fw_size = 0;
|
||||
static uint32_t s_fw_offset = 0; /* 已喂给 GD32 的偏移 */
|
||||
static TaskHandle_t s_feed_task = nullptr;
|
||||
static volatile bool s_feed_running = false;
|
||||
|
||||
static int64_t nowMs() {
|
||||
return esp_timer_get_time() / 1000;
|
||||
}
|
||||
|
||||
/* ═══════════════════ 喂数据 task ═══════════════════ */
|
||||
|
||||
static void feedTask(void *arg)
|
||||
{
|
||||
(void)arg;
|
||||
uint8_t resp_payload[16];
|
||||
uint8_t resp_type = 0;
|
||||
size_t resp_len = 0;
|
||||
/* 栈上 chunk:[offset:u32 LE][data…],避免整固件堆缓冲 */
|
||||
uint8_t chunk_buf[MOTOR_OTA_CHUNK_SIZE];
|
||||
|
||||
/* 整体超时保护:feedTask 最长存活 120s,超时自动退出。
|
||||
* 防止 GD32 侧不回 ACK 或持续回 WAIT 时,task 永久卡在循环里,
|
||||
* 导致 motorOtaIsActive() 一直为 true、阻塞其他 OTA。 */
|
||||
const int64_t deadline_ms = nowMs() + 120000LL;
|
||||
|
||||
/* 启动宽限期:GD32 需要时间完成 kPreCancel(800ms 残帧窗口) + 握手(~1s)。
|
||||
* 在这个窗口内不检查终态,避免上一次 OTA 残留的 ERROR 状态推送
|
||||
* (0xD2 ring) 抢先覆写 s_status 导致 feedTask 误判退出。
|
||||
* 原 3s 过保守:GD32 进握手后不再发 ERROR push,1500ms 足够覆盖
|
||||
* kPreCancel + 开始握手(kBootSettleMs 已降到 800ms)。 */
|
||||
const int64_t grace_end_ms = nowMs() + 1500LL;
|
||||
|
||||
/* 重新启用 suppressRingService:ring service 抢占 SPI 带宽导致吐量从 ~5 chunks/s 降到 ~1.5 chunks/s。
|
||||
* 进度显示由 feedTask 自己基于 s_fw_offset 更新(近似值),不依赖 GD32 push。 */
|
||||
SpiMaster::instance().setSuppressRingService(true);
|
||||
|
||||
/* suppress 期间 s_status 不被 0xD2 push 更新(worker 不读 ring),
|
||||
* feedTask 无法靠 motorOtaGetStatus() 检测 GD32 是否已进 ERROR/FINISHED 终态。
|
||||
* 连续 SPI 失败计数:超过阈值时主动 QUERY GD32 真实状态,避免盲重试空转到 120s。 */
|
||||
uint32_t consec_fail = 0U;
|
||||
constexpr uint32_t kQueryAfterFails = 8U; /* 连续 8 次失败(~2s)后 QUERY 一次 */
|
||||
uint32_t ack_n = 0U; /* 成功 ACK 计数,用于降频写 s_status */
|
||||
|
||||
while (s_feed_running && s_fw_offset < s_fw_size) {
|
||||
if (nowMs() >= deadline_ms) {
|
||||
ESP_LOGE(TAG, "feedTask 整体超时 (120s),强制退出 offset=%u/%u",
|
||||
(unsigned)s_fw_offset, (unsigned)s_fw_size);
|
||||
break;
|
||||
}
|
||||
|
||||
/* GD32 已进终态:停止喂,避免盲重试空转到 120s 超时。
|
||||
* 启动宽限期(3s)内跳过终态检查:避免上一次 OTA 残留的 ERROR 状态推送
|
||||
* (0xD2 ring 延迟到达) 覆写 s_status 导致 feedTask 误判退出发 CANCEL。
|
||||
* GD32 需 kPreCancel(500ms 沉淀)+握手(~1s) 才能进入传输态。 */
|
||||
const motor_ota_status_t st = motorOtaGetStatus();
|
||||
if (nowMs() >= grace_end_ms) {
|
||||
if (st.state == MOTOR_OTA_IDLE || st.state == MOTOR_OTA_FINISHED ||
|
||||
st.state == MOTOR_OTA_ERROR) {
|
||||
ESP_LOGW(TAG, "GD32 终态 0x%02X,停止喂数据 offset=%u/%u",
|
||||
(int)st.state, (unsigned)s_fw_offset, (unsigned)s_fw_size);
|
||||
break;
|
||||
}
|
||||
}
|
||||
/* 注:旧版有 "GD32 回到握手阶段→重置 offset" 的逻辑,
|
||||
* 但 v1.4.38+ 已移除握手重试(0x05 直接中止不重握手),
|
||||
* 且 v1.4.58 feedData 在握手期间就接受数据(预填充缓冲)。
|
||||
* 若在此重置 offset,会导致缓冲中数据重复。已移除。 */
|
||||
|
||||
const uint32_t remaining = s_fw_size - s_fw_offset;
|
||||
const uint32_t data_len = (remaining < MOTOR_OTA_CHUNK_DATA)
|
||||
? remaining : MOTOR_OTA_CHUNK_DATA;
|
||||
const uint32_t frame_len = MOTOR_OTA_CHUNK_HDR + data_len;
|
||||
|
||||
/* [0..3]=绝对 offset LE,[4..]=固件数据。重传同 offset 时 GD32 幂等。 */
|
||||
chunk_buf[0] = static_cast<uint8_t>(s_fw_offset & 0xFFU);
|
||||
chunk_buf[1] = static_cast<uint8_t>((s_fw_offset >> 8) & 0xFFU);
|
||||
chunk_buf[2] = static_cast<uint8_t>((s_fw_offset >> 16) & 0xFFU);
|
||||
chunk_buf[3] = static_cast<uint8_t>((s_fw_offset >> 24) & 0xFFU);
|
||||
esp_err_t rd = Gd32FlashStore::instance().flashReadChunk(
|
||||
s_fw_offset, chunk_buf + MOTOR_OTA_CHUNK_HDR, data_len);
|
||||
if (rd != ESP_OK) {
|
||||
ESP_LOGE(TAG, "flash read fail @%u: %s", (unsigned)s_fw_offset, esp_err_to_name(rd));
|
||||
break;
|
||||
}
|
||||
|
||||
esp_err_t ret = SpiMaster::instance().transact(
|
||||
kSpiMotorOtaData,
|
||||
chunk_buf, frame_len,
|
||||
&resp_type, resp_payload, sizeof(resp_payload), &resp_len,
|
||||
200U); /* 200ms: v1.4.59成功时的确切值。poll有~95ms 做多次盲读 */
|
||||
|
||||
if (ret != ESP_OK) {
|
||||
ESP_LOGW(TAG, "SPI data fail: %s offset=%u", esp_err_to_name(ret), (unsigned)s_fw_offset);
|
||||
vTaskDelay(pdMS_TO_TICKS(10)); /* 快速重试(原 100ms 太慢导致吐量低) */
|
||||
/* suppress 期间 s_status 不更新,连续失败时主动 QUERY GD32 真实状态,
|
||||
* 避免 GD32 已进 ERROR 终态但 feedTask 不知道、盲重试空转到 120s 超时。 */
|
||||
if (++consec_fail >= kQueryAfterFails) {
|
||||
consec_fail = 0U;
|
||||
uint8_t qcmd[1] = {2U}; /* cmd=QUERY */
|
||||
uint8_t qt = 0, qp[16] = {0};
|
||||
size_t ql = 0;
|
||||
if (SpiMaster::instance().transact(
|
||||
kSpiMotorOtaCmd, qcmd, 1, &qt, qp, sizeof(qp), &ql, 300U) == ESP_OK
|
||||
&& qt == kRspAck && ql >= 1U) {
|
||||
motor_ota_state_t qst = static_cast<motor_ota_state_t>(qp[0]);
|
||||
if (qst == MOTOR_OTA_IDLE || qst == MOTOR_OTA_FINISHED ||
|
||||
qst == MOTOR_OTA_ERROR) {
|
||||
ESP_LOGW(TAG, "QUERY 检测到 GD32 终态 0x%02X err=0x%02X,停止喂数据 offset=%u/%u",
|
||||
(int)qst, (unsigned)qp[1],
|
||||
(unsigned)s_fw_offset, (unsigned)s_fw_size);
|
||||
if (s_mutex && xSemaphoreTake(s_mutex, pdMS_TO_TICKS(50)) == pdTRUE) {
|
||||
s_status.state = qst;
|
||||
s_status.error_code = qp[1];
|
||||
if (ql >= 6U) {
|
||||
s_status.packets_acked = static_cast<uint16_t>(qp[2])
|
||||
| (static_cast<uint16_t>(qp[3]) << 8);
|
||||
s_status.total_packets = static_cast<uint16_t>(qp[4])
|
||||
| (static_cast<uint16_t>(qp[5]) << 8);
|
||||
}
|
||||
s_status.last_update_ms = nowMs();
|
||||
xSemaphoreGive(s_mutex);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
continue; /* 不推进 offset,同 offset 重发 */
|
||||
}
|
||||
consec_fail = 0U;
|
||||
|
||||
if (resp_type == kRspAck) {
|
||||
s_fw_offset += data_len;
|
||||
/* 进度:每 8 chunk 写 s_status;state 在 suppress 下不会收 0xD2,
|
||||
* 喂数一开始就标 TRANSFER,避免网页卡在「开始握手」。 */
|
||||
const bool touch_status = ((++ack_n & 7U) == 0U) || (s_fw_offset >= s_fw_size);
|
||||
if (touch_status || ack_n == 1U) {
|
||||
const uint16_t fed_pkts = static_cast<uint16_t>((s_fw_offset + 47U) / 48U);
|
||||
if (s_mutex && xSemaphoreTake(s_mutex, pdMS_TO_TICKS(2)) == pdTRUE) {
|
||||
if (s_status.state < MOTOR_OTA_TRANSFER ||
|
||||
s_status.state == MOTOR_OTA_PRE_CANCEL ||
|
||||
s_status.state == MOTOR_OTA_HANDSHAKE_REQ ||
|
||||
s_status.state == MOTOR_OTA_HANDSHAKE_NONCE ||
|
||||
s_status.state == MOTOR_OTA_HANDSHAKE_CONFIRM ||
|
||||
s_status.state == MOTOR_OTA_ERASE) {
|
||||
s_status.state = MOTOR_OTA_TRANSFER;
|
||||
}
|
||||
s_status.packets_acked = fed_pkts;
|
||||
s_status.last_update_ms = nowMs();
|
||||
xSemaphoreGive(s_mutex);
|
||||
}
|
||||
}
|
||||
} else if (resp_type == kRspWait) {
|
||||
/* 缓冲满:不推进 offset,稍候用新 TX_SEQ 再发同一 offset。
|
||||
* WAIT 路径 SPI 层已推进 seq(见 otaPollResponseUnlocked),
|
||||
* 避免同 seq 重发被 GD32 当重复请求死锁。 */
|
||||
vTaskDelay(pdMS_TO_TICKS(2));
|
||||
} else {
|
||||
/* 非 ACK/WAIT(含 ERROR/陈旧 type):不 break 整次 OTA。
|
||||
* 曾因 poll 读到 EVENT/错 type 直接 break → fed 卡首 chunk。 */
|
||||
ESP_LOGW(TAG, "SPI data unexpected type=0x%02X offset=%u,重试",
|
||||
resp_type, (unsigned)s_fw_offset);
|
||||
vTaskDelay(pdMS_TO_TICKS(5));
|
||||
}
|
||||
}
|
||||
|
||||
/* 恢复 ring service,让后续 GD32 push(FINISHED/ERROR) 正常到达 */
|
||||
SpiMaster::instance().setSuppressRingService(false);
|
||||
|
||||
/* feedTask 字节喂完 ≠ GD32/电机已完成。suppress 期间 0xD2 被丢;
|
||||
* 末包 CAN ACK / 跳转 APP 还要数百 ms。只 QUERY 一次会把前端卡在 kTransfer。
|
||||
* 退出后轮询 QUERY,直到 GD32 进 FINISHED/ERROR/IDLE,或最多 ~8s。 */
|
||||
{
|
||||
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) {
|
||||
/* 优先吃 ring 推送(已恢复 suppress) */
|
||||
{
|
||||
const motor_ota_status_t st = motorOtaGetStatus();
|
||||
if (st.state == MOTOR_OTA_FINISHED || st.state == MOTOR_OTA_ERROR ||
|
||||
st.state == MOTOR_OTA_IDLE) {
|
||||
ESP_LOGI(TAG, "feedTask 退出,ring 终态: state=%u err=%u pkt=%u/%u",
|
||||
(unsigned)st.state, (unsigned)st.error_code,
|
||||
(unsigned)st.packets_acked, (unsigned)st.total_packets);
|
||||
got_terminal = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
uint8_t qcmd[1] = {2U}; /* cmd=QUERY */
|
||||
uint8_t qresp_type = 0;
|
||||
uint8_t qresp[16] = {0};
|
||||
size_t qresp_len = 0;
|
||||
esp_err_t qr = SpiMaster::instance().transact(
|
||||
kSpiMotorOtaCmd, qcmd, 1,
|
||||
&qresp_type, qresp, sizeof(qresp), &qresp_len, 300U);
|
||||
if (qr == ESP_OK && qresp_type == kRspAck && qresp_len >= 10U) {
|
||||
const motor_ota_state_t qst = static_cast<motor_ota_state_t>(qresp[0]);
|
||||
if (s_mutex && xSemaphoreTake(s_mutex, pdMS_TO_TICKS(100)) == pdTRUE) {
|
||||
s_status.state = qst;
|
||||
s_status.error_code = qresp[1];
|
||||
s_status.packets_acked = static_cast<uint16_t>(qresp[2])
|
||||
| (static_cast<uint16_t>(qresp[3]) << 8);
|
||||
s_status.total_packets = static_cast<uint16_t>(qresp[4])
|
||||
| (static_cast<uint16_t>(qresp[5]) << 8);
|
||||
s_status.fw_size = static_cast<uint32_t>(qresp[6])
|
||||
| (static_cast<uint32_t>(qresp[7]) << 8)
|
||||
| (static_cast<uint32_t>(qresp[8]) << 16)
|
||||
| (static_cast<uint32_t>(qresp[9]) << 24);
|
||||
s_status.last_update_ms = nowMs();
|
||||
xSemaphoreGive(s_mutex);
|
||||
}
|
||||
if (qst == MOTOR_OTA_FINISHED || qst == MOTOR_OTA_ERROR ||
|
||||
qst == MOTOR_OTA_IDLE) {
|
||||
ESP_LOGI(TAG, "feedTask 退出,QUERY 终态: state=%u err=%u pkt=%u/%u",
|
||||
(unsigned)qst, (unsigned)qresp[1],
|
||||
(unsigned)(static_cast<uint16_t>(qresp[2])
|
||||
| (static_cast<uint16_t>(qresp[3]) << 8)),
|
||||
(unsigned)(static_cast<uint16_t>(qresp[4])
|
||||
| (static_cast<uint16_t>(qresp[5]) << 8)));
|
||||
got_terminal = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
vTaskDelay(pdMS_TO_TICKS(kExitGapMs));
|
||||
}
|
||||
if (!got_terminal) {
|
||||
ESP_LOGW(TAG, "feedTask 退出轮询超时,前端可能仍显示传输中 offset=%u/%u",
|
||||
(unsigned)s_fw_offset, (unsigned)s_fw_size);
|
||||
}
|
||||
}
|
||||
|
||||
s_feed_running = false;
|
||||
s_feed_task = nullptr;
|
||||
vTaskDelete(NULL);
|
||||
}
|
||||
|
||||
/* ═══════════════════ 公开接口 ═══════════════════ */
|
||||
|
||||
void motorOtaRelayInit(void)
|
||||
{
|
||||
if (!s_mutex) {
|
||||
s_mutex = xSemaphoreCreateMutex();
|
||||
}
|
||||
}
|
||||
|
||||
void onMotorOtaStatusPush(const uint8_t *payload, size_t len)
|
||||
{
|
||||
if (len < 12U || !s_mutex) return;
|
||||
if (xSemaphoreTake(s_mutex, pdMS_TO_TICKS(10)) != pdTRUE) return;
|
||||
|
||||
s_status.state = static_cast<motor_ota_state_t>(payload[0]);
|
||||
s_status.error_code = payload[1];
|
||||
s_status.packets_acked = static_cast<uint16_t>(payload[2]) | (static_cast<uint16_t>(payload[3]) << 8);
|
||||
s_status.total_packets = static_cast<uint16_t>(payload[4]) | (static_cast<uint16_t>(payload[5]) << 8);
|
||||
s_status.fw_size = static_cast<uint32_t>(payload[6])
|
||||
| (static_cast<uint32_t>(payload[7]) << 8)
|
||||
| (static_cast<uint32_t>(payload[8]) << 16)
|
||||
| (static_cast<uint32_t>(payload[9]) << 24);
|
||||
s_status.buf_used = static_cast<uint16_t>(payload[10]) | (static_cast<uint16_t>(payload[11]) << 8);
|
||||
s_status.last_update_ms = nowMs();
|
||||
|
||||
xSemaphoreGive(s_mutex);
|
||||
}
|
||||
|
||||
int motorOtaStart(uint32_t fw_size, uint8_t channel)
|
||||
{
|
||||
if (fw_size == 0 || fw_size > MOTOR_OTA_MAX_FW_SIZE || channel > 2) {
|
||||
return -1;
|
||||
}
|
||||
if (s_feed_running) {
|
||||
return -2;
|
||||
}
|
||||
|
||||
/* 固件已由 HTTP handler 流式写入 gd32_ota 分区;此处不再堆缓冲 */
|
||||
s_fw_size = fw_size;
|
||||
s_fw_offset = 0;
|
||||
|
||||
/* 发 SPI start 命令(best-effort,不阻塞等 ACK)。
|
||||
* SPI "延迟一拍"协议下,START 的 ACK 偶发被 send 阶段当作 stale 吃掉、
|
||||
* poll 反读空 → transact 报 TIMEOUT,但 GD32 实际已收到 START 进握手
|
||||
* (GD32 日志 "SPI 0xD0 START recv" + "START v150" 每次稳定出现)。
|
||||
* 重发 START 会被 GD32 拒(已在进行中→0xE5),QUERY 的 ACK 同样受
|
||||
* delayed-by-one 影响不可靠。改为 fire-and-forget:命令发出即继续,
|
||||
* 由 feedTask 靠 GD32 push 的状态 + 超时保护自决去留。 */
|
||||
uint8_t cmd[6];
|
||||
cmd[0] = 0U; /* cmd=START */
|
||||
cmd[1] = channel;
|
||||
cmd[2] = static_cast<uint8_t>(fw_size & 0xFF);
|
||||
cmd[3] = static_cast<uint8_t>((fw_size >> 8) & 0xFF);
|
||||
cmd[4] = static_cast<uint8_t>((fw_size >> 16) & 0xFF);
|
||||
cmd[5] = static_cast<uint8_t>((fw_size >> 24) & 0xFF);
|
||||
|
||||
uint8_t resp_type = 0;
|
||||
uint8_t resp_payload[16];
|
||||
size_t resp_len = 0;
|
||||
esp_err_t ret = SpiMaster::instance().transact(
|
||||
kSpiMotorOtaCmd, cmd, 6,
|
||||
&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) {
|
||||
DEBUG_WARN(TAG, "START SPI 未确认 ACK (ret=%s resp=0x%02X)——fire-and-forget 继续",
|
||||
esp_err_to_name(ret), resp_type);
|
||||
}
|
||||
|
||||
/* 初始化状态 */
|
||||
if (s_mutex && xSemaphoreTake(s_mutex, pdMS_TO_TICKS(100)) == pdTRUE) {
|
||||
memset(&s_status, 0, sizeof(s_status));
|
||||
s_status.state = MOTOR_OTA_HANDSHAKE_REQ;
|
||||
s_status.fw_size = fw_size;
|
||||
s_status.total_packets = static_cast<uint16_t>((fw_size + 47U) / 48U);
|
||||
s_status.last_update_ms = nowMs();
|
||||
xSemaphoreGive(s_mutex);
|
||||
}
|
||||
|
||||
/* 启动喂数据 task。prio=7 > spi_worker(6) > httpd(5):
|
||||
* 传输期优先抢 SPI 队列;实际总线访问仍经 worker 串行。 */
|
||||
s_feed_running = true;
|
||||
BaseType_t ok = xTaskCreate(feedTask, "ota_feed", 4096, nullptr, 7, &s_feed_task);
|
||||
if (ok != pdPASS) {
|
||||
s_feed_running = false;
|
||||
return -3;
|
||||
}
|
||||
|
||||
ESP_LOGI(TAG, "Motor OTA started: %lu B, ch=%u", (unsigned long)fw_size, channel);
|
||||
return 0;
|
||||
}
|
||||
|
||||
void motorOtaCancel(void)
|
||||
{
|
||||
DEBUG_WARN(TAG, "cancel: feed_running=%d task=%p offset=%u/%u",
|
||||
(int)s_feed_running, s_feed_task,
|
||||
(unsigned)s_fw_offset, (unsigned)s_fw_size);
|
||||
|
||||
/* 立即标记 false,让 feedTask 在下次循环检查时退出。
|
||||
* motorOtaIsActive() 依赖此标志,设 false 后后续 OTA 不再被阻塞。 */
|
||||
s_feed_running = false;
|
||||
|
||||
/* 标记状态为 IDLE,让前端 /motor/ota/status 立即看到空闲 */
|
||||
if (s_mutex && xSemaphoreTake(s_mutex, pdMS_TO_TICKS(100)) == pdTRUE) {
|
||||
s_status.state = MOTOR_OTA_IDLE;
|
||||
s_status.last_update_ms = nowMs();
|
||||
xSemaphoreGive(s_mutex);
|
||||
}
|
||||
|
||||
/* 通知 GD32 取消 relay 状态机 + 等 feedTask 退出是耗时操作
|
||||
*(SPI transact 最多 1s + feedTask 退出最多 2s),不能在 httpd 线程
|
||||
* 同步阻塞,否则 cancel HTTP 请求本身会超时("Failed to fetch"),
|
||||
* 且 httpd 单线程会饿死其他端点。
|
||||
* 用独立 task 异步执行,HTTP handler 立即返回。 */
|
||||
static TaskHandle_t cancel_task = nullptr;
|
||||
if (cancel_task != nullptr) {
|
||||
/* 上一次 cancel 还在跑,不重复创建 */
|
||||
DEBUG_WARN(TAG, "cancel: previous cancel task still running, skip");
|
||||
return;
|
||||
}
|
||||
|
||||
auto do_cancel = [](void *) {
|
||||
/* 通知 GD32 取消 relay 状态机 */
|
||||
uint8_t cmd[1] = {1U}; /* cmd=CANCEL */
|
||||
uint8_t resp_type = 0;
|
||||
uint8_t resp_payload[4];
|
||||
size_t resp_len = 0;
|
||||
esp_err_t 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。 */
|
||||
TaskHandle_t t = s_feed_task;
|
||||
if (t) {
|
||||
for (int i = 0; i < 200 && s_feed_task != nullptr; i++) {
|
||||
vTaskDelay(pdMS_TO_TICKS(10));
|
||||
}
|
||||
if (s_feed_task != nullptr) {
|
||||
DEBUG_ERR(TAG, "cancel: feedTask 未在 2s 内退出,强制删除");
|
||||
vTaskDelete(s_feed_task);
|
||||
s_feed_task = nullptr;
|
||||
}
|
||||
}
|
||||
s_feed_running = false; /* 双保险 */
|
||||
cancel_task = nullptr;
|
||||
vTaskDelete(NULL);
|
||||
};
|
||||
|
||||
BaseType_t ok = xTaskCreate(do_cancel, "ota_cancel", 3072, nullptr, 4, &cancel_task);
|
||||
if (ok != pdPASS) {
|
||||
DEBUG_ERR(TAG, "cancel: failed to create cancel task, doing sync cancel");
|
||||
cancel_task = nullptr;
|
||||
/* 降级:同步执行(极端 OOM 情况) */
|
||||
uint8_t cmd[1] = {1U};
|
||||
uint8_t resp_type = 0;
|
||||
uint8_t resp_payload[4];
|
||||
size_t resp_len = 0;
|
||||
(void)SpiMaster::instance().transact(
|
||||
kSpiMotorOtaCmd, cmd, 1,
|
||||
&resp_type, resp_payload, sizeof(resp_payload), &resp_len,
|
||||
1000U);
|
||||
TaskHandle_t t = s_feed_task;
|
||||
if (t) {
|
||||
for (int i = 0; i < 200 && s_feed_task != nullptr; i++) {
|
||||
vTaskDelay(pdMS_TO_TICKS(10));
|
||||
}
|
||||
if (s_feed_task != nullptr) {
|
||||
vTaskDelete(s_feed_task);
|
||||
s_feed_task = nullptr;
|
||||
}
|
||||
}
|
||||
s_feed_running = false;
|
||||
}
|
||||
DEBUG_WARN(TAG, "cancel: dispatched feed_running=%d", (int)s_feed_running);
|
||||
}
|
||||
|
||||
bool motorOtaIsActive(void)
|
||||
{
|
||||
return s_feed_running;
|
||||
}
|
||||
|
||||
motor_ota_status_t motorOtaGetStatus(void)
|
||||
{
|
||||
motor_ota_status_t out = {};
|
||||
if (s_mutex && xSemaphoreTake(s_mutex, pdMS_TO_TICKS(100)) == pdTRUE) {
|
||||
out = s_status;
|
||||
xSemaphoreGive(s_mutex);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
/*
|
||||
* motor_ota_relay.h - 电机 OTA relay ESP32 侧(固件缓冲 + SPI 喂数据 + 进度缓存)
|
||||
*
|
||||
* 两段式流程(与 BLE 刷 GD32 对齐,避免收固件期间与 GD32 按铃争抢单缓冲 MISO):
|
||||
* 1. 前端 POST /motor/ota 上传固件 + channel → 流式写 gd32_ota flash 分区,写 meta
|
||||
* 2. 前端 POST /motor/ota/relay?ch=X → motorOtaStart 发 SPI 0xD0(start) + 启动 feedTask
|
||||
* 3. feedTask 从 flash 分块读,经 SPI 0xD1(data) 喂 GD32
|
||||
* 4. GD32 通过 0xD2 ring push 报进度,ESP32 缓存
|
||||
* 5. 前端 GET /motor/ota/status 轮询进度
|
||||
*/
|
||||
#ifndef MOTOR_OTA_RELAY_H
|
||||
#define MOTOR_OTA_RELAY_H
|
||||
|
||||
#include <stdint.h>
|
||||
#include <stdbool.h>
|
||||
#include <stddef.h>
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
#define MOTOR_OTA_MAX_FW_SIZE 0xC000U /* 48 KB */
|
||||
/* SPI payload 上限 236 = 256 - V2_OVERHEAD(20)。前 4B 为绝对 offset LE,数据最多 232B。
|
||||
* 无 offset 时 SPI 超时重传会被 GD32 重复追加(BLF:前 236B 循环 → 刷坏)。 */
|
||||
#define MOTOR_OTA_CHUNK_HDR 4U
|
||||
#define MOTOR_OTA_CHUNK_DATA 232U
|
||||
#define MOTOR_OTA_CHUNK_SIZE (MOTOR_OTA_CHUNK_HDR + MOTOR_OTA_CHUNK_DATA)
|
||||
|
||||
typedef enum {
|
||||
MOTOR_OTA_IDLE = 0,
|
||||
MOTOR_OTA_HANDSHAKE_REQ,
|
||||
MOTOR_OTA_HANDSHAKE_NONCE,
|
||||
MOTOR_OTA_HANDSHAKE_CONFIRM,
|
||||
MOTOR_OTA_ERASE,
|
||||
MOTOR_OTA_TRANSFER,
|
||||
MOTOR_OTA_FINISH_OTA, /* 收到 0xF0F0F0F0,已发 FINISH_OTA,等跳转 ACK */
|
||||
MOTOR_OTA_FINISHED,
|
||||
MOTOR_OTA_ERROR,
|
||||
MOTOR_OTA_PRE_CANCEL = 9 /* 握手前预取消:发 CANCEL 清理电机残留会话(GD32 侧过渡态) */
|
||||
} motor_ota_state_t;
|
||||
|
||||
typedef struct {
|
||||
motor_ota_state_t state;
|
||||
uint8_t error_code;
|
||||
uint16_t packets_acked;
|
||||
uint16_t total_packets;
|
||||
uint32_t fw_size;
|
||||
uint16_t buf_used; /* GD32 侧缓冲占用 */
|
||||
int64_t last_update_ms; /* ESP32 收到状态的时间 */
|
||||
} motor_ota_status_t;
|
||||
|
||||
/**
|
||||
* @brief 处理 GD32 0xD2 ring push(由 can_feedback_task 分发)。
|
||||
*/
|
||||
void onMotorOtaStatusPush(const uint8_t *payload, size_t len);
|
||||
|
||||
/**
|
||||
* @brief 启动电机 OTA:发 SPI start + 启动喂数据 task。
|
||||
*
|
||||
* 固件须已由调用方(HTTP handler)流式写入 gd32_ota flash 分区
|
||||
* (Gd32FlashStore::flashWriteChunk,offset 0 起)。喂数据 task 从
|
||||
* flash 分块读取并经 SPI 0xD1 喂给 GD32,不在堆上缓冲整个固件
|
||||
* (48KB 整块 malloc 会超出 ESP32-C3 的 largest_free_block,导致 OOM)。
|
||||
*
|
||||
* @param fw_size 固件大小 (≤48KB),须与已写入 flash 的大小一致
|
||||
* @param channel CAN 通道 (0-2)
|
||||
* @return 0 成功, -1 参数错误, -2 已在进行中, -3 SPI 失败
|
||||
*/
|
||||
int motorOtaStart(uint32_t fw_size, uint8_t channel);
|
||||
|
||||
/**
|
||||
* @brief 电机 OTA 是否正在进行(喂数据 task 存活)。
|
||||
* 供 OtaCoordinator 在 beginGd32Storage/beginEsp32Ota 中检查:
|
||||
* 电机喂数期间禁止新的 GD32/ESP32 OTA,避免 gd32_ota 分区被并发覆写。
|
||||
*/
|
||||
bool motorOtaIsActive(void);
|
||||
|
||||
/**
|
||||
* @brief 取消电机 OTA。
|
||||
*/
|
||||
void motorOtaCancel(void);
|
||||
|
||||
/**
|
||||
* @brief 获取最新状态(线程安全)。
|
||||
*/
|
||||
motor_ota_status_t motorOtaGetStatus(void);
|
||||
|
||||
/**
|
||||
* @brief 模块初始化(创建 mutex 等)。
|
||||
*/
|
||||
void motorOtaRelayInit(void);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif /* MOTOR_OTA_RELAY_H */
|
||||
@@ -0,0 +1,137 @@
|
||||
/*
|
||||
* motor_state.cpp - GD32 多电机状态推送解析与缓存 (SPI TYPE 0xC5)
|
||||
*/
|
||||
#include "monitor/motor_state.h"
|
||||
#include "esp_timer.h"
|
||||
#include "freertos/FreeRTOS.h"
|
||||
#include "freertos/semphr.h"
|
||||
|
||||
#include <string.h>
|
||||
|
||||
namespace {
|
||||
|
||||
static SemaphoreHandle_t s_mux = nullptr;
|
||||
static motor_state_table_t s_table = {};
|
||||
|
||||
static uint32_t rdU32Le(const uint8_t *p)
|
||||
{
|
||||
return static_cast<uint32_t>(p[0])
|
||||
| (static_cast<uint32_t>(p[1]) << 8)
|
||||
| (static_cast<uint32_t>(p[2]) << 16)
|
||||
| (static_cast<uint32_t>(p[3]) << 24);
|
||||
}
|
||||
|
||||
static void ensureMux()
|
||||
{
|
||||
if (s_mux == nullptr) {
|
||||
s_mux = xSemaphoreCreateMutex();
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
bool onMotorStateEvent(const uint8_t *payload, size_t len)
|
||||
{
|
||||
if (payload == nullptr || len < MOTOR_PAGE_HEADER) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const uint8_t page = payload[0];
|
||||
const uint8_t total_pages = payload[1];
|
||||
const uint8_t entry_count = payload[2];
|
||||
const uint8_t active_cnt = payload[3];
|
||||
|
||||
if (page >= MOTOR_TOTAL_PAGES || total_pages == 0 || total_pages > MOTOR_TOTAL_PAGES) {
|
||||
return false;
|
||||
}
|
||||
|
||||
/* 验证长度足够容纳声明的 entry 数 */
|
||||
const size_t expected = MOTOR_PAGE_HEADER + static_cast<size_t>(entry_count) * MOTOR_ENTRY_SIZE;
|
||||
if (len < expected) {
|
||||
return false;
|
||||
}
|
||||
|
||||
ensureMux();
|
||||
if (s_mux == nullptr) return false;
|
||||
if (xSemaphoreTake(s_mux, pdMS_TO_TICKS(20)) != pdTRUE) return false;
|
||||
|
||||
/* page 0 时刷新全局字段。
|
||||
* active 字节 bit7 = boot_active(总线上有电机在 BOOT,0x103 心跳),
|
||||
* 低 7 位才是真实在线电机数。 */
|
||||
if (page == 0) {
|
||||
s_table.active_count = static_cast<uint8_t>(active_cnt & 0x7FU);
|
||||
s_table.boot_active = (active_cnt & 0x80U) ? 1U : 0U;
|
||||
}
|
||||
|
||||
/* 逐条解析 entry */
|
||||
size_t offset = MOTOR_PAGE_HEADER;
|
||||
for (uint8_t i = 0; i < entry_count; ++i) {
|
||||
const uint8_t *e = &payload[offset];
|
||||
const uint8_t node = e[0];
|
||||
if (node < 1 || node > MOTOR_MAX_NODES) {
|
||||
offset += MOTOR_ENTRY_SIZE;
|
||||
continue;
|
||||
}
|
||||
|
||||
motor_slot_t &slot = s_table.slots[node - 1];
|
||||
slot.node_id = node;
|
||||
slot.channel = e[1];
|
||||
slot.online = e[2];
|
||||
slot.dlc = e[3];
|
||||
slot.rx_count = rdU32Le(&e[4]);
|
||||
slot.seq = rdU32Le(&e[8]);
|
||||
memcpy(slot.data, &e[12], MOTOR_STATUS_DLC);
|
||||
|
||||
offset += MOTOR_ENTRY_SIZE;
|
||||
}
|
||||
|
||||
/* 统计有效槽位 */
|
||||
uint8_t valid = 0;
|
||||
for (uint8_t i = 0; i < MOTOR_MAX_NODES; ++i) {
|
||||
if (s_table.slots[i].node_id != 0) valid++;
|
||||
}
|
||||
s_table.valid_slots = valid;
|
||||
s_table.recv_ms = esp_timer_get_time() / 1000;
|
||||
s_table.have_data = true;
|
||||
|
||||
xSemaphoreGive(s_mux);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool getLatestMotorState(motor_state_table_t *out)
|
||||
{
|
||||
if (out == nullptr) return false;
|
||||
ensureMux();
|
||||
if (s_mux == nullptr) return false;
|
||||
|
||||
bool ok = false;
|
||||
if (xSemaphoreTake(s_mux, pdMS_TO_TICKS(20)) == pdTRUE) {
|
||||
if (s_table.have_data) {
|
||||
*out = s_table;
|
||||
ok = true;
|
||||
}
|
||||
xSemaphoreGive(s_mux);
|
||||
}
|
||||
return ok;
|
||||
}
|
||||
|
||||
uint32_t motorStateAgeMs(void)
|
||||
{
|
||||
ensureMux();
|
||||
if (s_mux == nullptr) return UINT32_MAX;
|
||||
|
||||
int64_t recv = 0;
|
||||
bool have = false;
|
||||
if (xSemaphoreTake(s_mux, pdMS_TO_TICKS(20)) == pdTRUE) {
|
||||
have = s_table.have_data;
|
||||
recv = s_table.recv_ms;
|
||||
xSemaphoreGive(s_mux);
|
||||
}
|
||||
if (!have || recv <= 0) return UINT32_MAX;
|
||||
|
||||
const int64_t now = esp_timer_get_time() / 1000;
|
||||
if (now < recv) return 0U;
|
||||
const int64_t age = now - recv;
|
||||
return (age > static_cast<int64_t>(UINT32_MAX)) ? UINT32_MAX
|
||||
: static_cast<uint32_t>(age);
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
/*
|
||||
* motor_state.h - 被动接收 GD32 多电机状态推送(SPI TYPE 0xC5)
|
||||
*
|
||||
* 与 CAN 0xC1 / ADC 0xC4 同模型:GD32 250ms 主动按铃推送(独立任务位),ESP32 takeEvent 侧解析入库。
|
||||
* 是否推送由 GD32 CONFIG_MOTOR_STATE_ENABLE 决定;ESP32 无本地开关,有帧即解析。
|
||||
*
|
||||
* payload 格式(每页):
|
||||
* [0] page, [1] total_pages, [2] entry_count, [3] active_count
|
||||
* [4..] 每个 entry 44B:
|
||||
* [0] node_id, [1] channel, [2] online, [3] dlc,
|
||||
* [4..7] rx_count (LE), [8..11] seq (LE),
|
||||
* [12..43] data[32] (原始 STATUS payload)
|
||||
*/
|
||||
#ifndef MOTOR_STATE_H
|
||||
#define MOTOR_STATE_H
|
||||
|
||||
#include <stdint.h>
|
||||
#include <stdbool.h>
|
||||
#include <stddef.h>
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
#define MOTOR_MAX_NODES 9
|
||||
#define MOTOR_STATUS_DLC 32
|
||||
#define MOTOR_ENTRY_SIZE 44
|
||||
#define MOTOR_PAGE_HEADER 4
|
||||
#define MOTOR_TOTAL_PAGES 2
|
||||
|
||||
/** @brief 单电机状态快照。 */
|
||||
typedef struct {
|
||||
uint8_t node_id; ///< 1-9, 0=空槽
|
||||
uint8_t channel; ///< CAN 通道 (0/1/2)
|
||||
uint8_t online; ///< 1=在线, 0=离线
|
||||
uint8_t dlc; ///< STATUS 帧 DLC (正常=32)
|
||||
uint32_t rx_count; ///< 累计收到帧数
|
||||
uint32_t seq; ///< GD32 侧更新序号
|
||||
uint8_t data[MOTOR_STATUS_DLC]; ///< 原始 STATUS payload
|
||||
} motor_slot_t;
|
||||
|
||||
/** @brief 全局电机状态表。 */
|
||||
typedef struct {
|
||||
motor_slot_t slots[MOTOR_MAX_NODES]; ///< 按 node_id-1 索引
|
||||
uint8_t active_count; ///< 在线电机数
|
||||
uint8_t boot_active; ///< 1=总线上有电机在 BOOT(0x103 心跳,来自 0xC5 头 bit7)
|
||||
uint8_t valid_slots; ///< 有效槽位数(node_id!=0)
|
||||
int64_t recv_ms; ///< ESP32 最近收到时 esp_timer ms
|
||||
bool have_data; ///< 至少收到过一帧
|
||||
} motor_state_table_t;
|
||||
|
||||
/**
|
||||
* @brief 处理一帧 0xC5 EVENT payload(由 can_fb 任务 takeEvent 分发)。
|
||||
* @return true 表示已接受并更新。
|
||||
*/
|
||||
bool onMotorStateEvent(const uint8_t *payload, size_t len);
|
||||
|
||||
/**
|
||||
* @brief 拷贝最新电机状态表(线程安全)。
|
||||
* @param[out] out 输出缓冲
|
||||
* @return true 表示至少收到过一帧
|
||||
*/
|
||||
bool getLatestMotorState(motor_state_table_t *out);
|
||||
|
||||
/** @brief 距离上次收到的毫秒数;无数据返回 UINT32_MAX。 */
|
||||
uint32_t motorStateAgeMs(void);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif /* MOTOR_STATE_H */
|
||||
@@ -0,0 +1,84 @@
|
||||
/*
|
||||
* motor_version.cpp - GD32 多电机版本推送解析与缓存 (SPI TYPE 0xC6)
|
||||
*
|
||||
* 镜像 motor_state.cpp 的互斥保护缓存模型。
|
||||
*/
|
||||
#include "monitor/motor_version.h"
|
||||
#include "esp_timer.h"
|
||||
#include "freertos/FreeRTOS.h"
|
||||
#include "freertos/semphr.h"
|
||||
|
||||
#include <string.h>
|
||||
|
||||
namespace {
|
||||
|
||||
static SemaphoreHandle_t s_mux = nullptr;
|
||||
static motor_version_table_t s_table = {};
|
||||
|
||||
static uint32_t rdU32Le(const uint8_t *p)
|
||||
{
|
||||
return static_cast<uint32_t>(p[0])
|
||||
| (static_cast<uint32_t>(p[1]) << 8)
|
||||
| (static_cast<uint32_t>(p[2]) << 16)
|
||||
| (static_cast<uint32_t>(p[3]) << 24);
|
||||
}
|
||||
|
||||
static void ensureMux()
|
||||
{
|
||||
if (s_mux == nullptr) {
|
||||
s_mux = xSemaphoreCreateMutex();
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
bool onMotorVersionEvent(const uint8_t *payload, size_t len)
|
||||
{
|
||||
if (payload == nullptr || len < MOTORVER_HEADER_LEN) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const uint8_t count = payload[0];
|
||||
|
||||
ensureMux();
|
||||
if (s_mux == nullptr) return false;
|
||||
if (xSemaphoreTake(s_mux, pdMS_TO_TICKS(20)) != pdTRUE) return false;
|
||||
|
||||
size_t offset = MOTORVER_HEADER_LEN;
|
||||
for (uint8_t i = 0; i < count; ++i) {
|
||||
if (offset + MOTORVER_ENTRY_SIZE > len) {
|
||||
break;
|
||||
}
|
||||
const uint8_t node = payload[offset];
|
||||
if (node >= 1 && node <= MOTORVER_MAX_NODES) {
|
||||
motor_version_slot_t &slot = s_table.slots[node - 1];
|
||||
slot.node_id = node;
|
||||
slot.seq = rdU32Le(&payload[offset + 1]);
|
||||
memcpy(slot.version, &payload[offset + 5], MOTORVER_DLC);
|
||||
slot.recv_ms = esp_timer_get_time() / 1000;
|
||||
slot.valid = true;
|
||||
s_table.have_data = true;
|
||||
}
|
||||
offset += MOTORVER_ENTRY_SIZE;
|
||||
}
|
||||
|
||||
xSemaphoreGive(s_mux);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool getLatestMotorVersion(motor_version_table_t *out)
|
||||
{
|
||||
if (out == nullptr) return false;
|
||||
ensureMux();
|
||||
if (s_mux == nullptr) return false;
|
||||
|
||||
bool ok = false;
|
||||
if (xSemaphoreTake(s_mux, pdMS_TO_TICKS(20)) == pdTRUE) {
|
||||
if (s_table.have_data) {
|
||||
*out = s_table;
|
||||
ok = true;
|
||||
}
|
||||
xSemaphoreGive(s_mux);
|
||||
}
|
||||
return ok;
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
/*
|
||||
* motor_version.h - 被动接收 GD32 多电机版本推送(SPI TYPE 0xC6)
|
||||
*
|
||||
* 与 0xC5 电机状态同模型:GD32 收到电机版本应答(0x00FF)后事件驱动按铃推送,
|
||||
* ESP32 takeEvent 侧解析入库。版本为按需查询的低频数据,独立于 0xC5 周期推送。
|
||||
* 是否推送由 GD32 CONFIG_MOTOR_VERSION_ENABLE 决定;ESP32 无本地开关,有帧即解析。
|
||||
*
|
||||
* payload 格式(单帧,Header 2B + 每 entry 29B):
|
||||
* [0] count 本帧节点数
|
||||
* [1] rsvd 保留
|
||||
* [2..] 每个 entry 29B:
|
||||
* [0] node_id, [1..4] seq (LE), [5..28] version[24] (原始版本 payload)
|
||||
*/
|
||||
#ifndef MOTOR_VERSION_H
|
||||
#define MOTOR_VERSION_H
|
||||
|
||||
#include <stdint.h>
|
||||
#include <stdbool.h>
|
||||
#include <stddef.h>
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
#define MOTORVER_MAX_NODES 9
|
||||
#define MOTORVER_DLC 24
|
||||
#define MOTORVER_ENTRY_SIZE 29 /* node(1) + seq(4) + version(24) */
|
||||
#define MOTORVER_HEADER_LEN 2
|
||||
|
||||
/** @brief 单电机版本快照。 */
|
||||
typedef struct {
|
||||
uint8_t node_id; ///< 1-9, 0=空槽
|
||||
uint32_t seq; ///< GD32 侧更新序号(判新鲜用)
|
||||
uint8_t version[MOTORVER_DLC]; ///< 原始版本 payload
|
||||
int64_t recv_ms; ///< ESP32 收到时 esp_timer ms
|
||||
bool valid; ///< 该槽已收到过版本
|
||||
} motor_version_slot_t;
|
||||
|
||||
/** @brief 全局电机版本表。 */
|
||||
typedef struct {
|
||||
motor_version_slot_t slots[MOTORVER_MAX_NODES]; ///< 按 node_id-1 索引
|
||||
bool have_data; ///< 至少收到过一帧
|
||||
} motor_version_table_t;
|
||||
|
||||
/**
|
||||
* @brief 处理一帧 0xC6 EVENT payload(由 can_fb 任务 takeEvent 分发)。
|
||||
* @return true 表示已接受并更新。
|
||||
*/
|
||||
bool onMotorVersionEvent(const uint8_t *payload, size_t len);
|
||||
|
||||
/**
|
||||
* @brief 拷贝最新电机版本表(线程安全)。
|
||||
* @param[out] out 输出缓冲
|
||||
* @return true 表示至少收到过一帧
|
||||
*/
|
||||
bool getLatestMotorVersion(motor_version_table_t *out);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif /* MOTOR_VERSION_H */
|
||||
Reference in New Issue
Block a user