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:
@@ -76,6 +76,18 @@ int ble_ota_ctrl_access(uint16_t conn_handle, uint16_t attr_handle,
|
||||
|
||||
DEBUG_INFO(TAG, "GD32就绪 SHA匹配 %" PRIu32 "/%" PRIu32 ",触发中继",
|
||||
meta.stored_size, meta.total_size);
|
||||
|
||||
/* ★ 包级防呆:SHA 匹配只证明传输无误。BOOT 包/非 APP 包在此拒绝,
|
||||
* 不进入 SPI 中继,GD32 APP 区不会被擦除。 */
|
||||
const Gd32ImageIdentity identity = Gd32FlashStore::instance().validateStoredImageIdentity();
|
||||
if (identity != Gd32ImageIdentity::kApp) {
|
||||
DEBUG_ERR(TAG, "GD32 固件身份校验失败 identity=%d,拒中继", (int)identity);
|
||||
sendFinishEvent(BLE_OTA_FINISH_ERR_FW_IDENTITY,
|
||||
packBleFinishValue(session_id,
|
||||
(identity == Gd32ImageIdentity::kBootloader) ? 1 : 2));
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (OtaRelay::getResultQueue()) {
|
||||
uint32_t stale = 0;
|
||||
while (xQueueReceive(OtaRelay::getResultQueue(), &stale, 0) == pdTRUE) {
|
||||
@@ -83,6 +95,13 @@ int ble_ota_ctrl_access(uint16_t conn_handle, uint16_t attr_handle,
|
||||
}
|
||||
}
|
||||
esp_err_t trig_err = OtaCoordinator::instance().triggerGd32Relay();
|
||||
if (trig_err == ESP_ERR_INVALID_SIZE) {
|
||||
/* 兜底:triggerGd32Relay 内部身份校验拒绝(与上方同判据) */
|
||||
DEBUG_ERR(TAG, "GD32 固件身份校验失败(trigger 兜底),拒中继");
|
||||
sendFinishEvent(BLE_OTA_FINISH_ERR_FW_IDENTITY,
|
||||
packBleFinishValue(session_id, 2));
|
||||
return 0;
|
||||
}
|
||||
if (trig_err != ESP_OK) {
|
||||
DEBUG_ERR(TAG, "GD32 中继触发失败: %s", esp_err_to_name(trig_err));
|
||||
sendFinishEvent(BLE_OTA_FINISH_ERR_GD32_RELAY,
|
||||
|
||||
@@ -57,6 +57,7 @@
|
||||
#define BLE_OTA_FINISH_ERR_GD32_SIZE 0x84
|
||||
#define BLE_OTA_FINISH_ERR_SHA_MISMATCH 0x85
|
||||
#define BLE_OTA_FINISH_ERR_GD32_RELAY 0x86
|
||||
#define BLE_OTA_FINISH_ERR_FW_IDENTITY 0x87 ///< 包身份不符(疑似 Bootloader/非 APP 包),拒绝中继
|
||||
|
||||
struct BleOtaDataBatch {
|
||||
uint8_t *data;
|
||||
|
||||
@@ -230,6 +230,15 @@ esp_err_t ota_upload_handler(httpd_req_t *req)
|
||||
{
|
||||
http_set_cors_headers(req);
|
||||
|
||||
/* chunked 请求(无 Content-Length 头)时 IDF 置 content_len=-1:
|
||||
* ESP32 分支无符号比较使收流循环失去长度上界,GD32 分支则一字节
|
||||
* 不写却返回 progress:100 假成功——必须前置拒绝 */
|
||||
if (req->content_len <= 0) {
|
||||
return http_send_json_error(req, "400 Bad Request",
|
||||
"bad_content_len",
|
||||
"missing or invalid Content-Length");
|
||||
}
|
||||
|
||||
uint8_t *recv_buf = s_ota_upload_recv;
|
||||
DEBUG_WARN("WIFI", "POST /ota/upload clen=%d free=%lu big=%lu",
|
||||
req->content_len,
|
||||
@@ -580,7 +589,27 @@ esp_err_t ota_relay_trigger_handler(httpd_req_t *req)
|
||||
"sha_compute_failed", "SHA compute failed");
|
||||
}
|
||||
|
||||
/* ★ 包级防呆:SHA 匹配只证明传输无误,不证明包内容正确。
|
||||
* 上传 BOOT 包(或其他非 APP 包)时在此拒绝,GD32 不会被擦除 APP 区。 */
|
||||
const Gd32ImageIdentity identity = Gd32FlashStore::instance().validateStoredImageIdentity();
|
||||
if (identity != Gd32ImageIdentity::kApp) {
|
||||
(void)BleOta::instance().resumeStack();
|
||||
const char *msg = (identity == Gd32ImageIdentity::kBootloader)
|
||||
? "检测到 Bootloader 固件(复位向量在 0x08000000 区),已拒绝刷入 APP 区,请选择 APP 固件"
|
||||
: (identity == Gd32ImageIdentity::kInvalid)
|
||||
? "固件向量表非法,疑似非 GD32 APP 固件,已拒绝刷写"
|
||||
: "固件存储读取失败,无法校验身份,请重新上传";
|
||||
DEBUG_ERR("OTA", "GD32 固件身份校验失败 identity=%d,拒绝中继", (int)identity);
|
||||
return http_send_json_error(req, "400 Bad Request", "firmware_identity_mismatch", msg);
|
||||
}
|
||||
|
||||
esp_err_t trigger_err = OtaCoordinator::instance().triggerGd32Relay();
|
||||
if (trigger_err == ESP_ERR_INVALID_SIZE) {
|
||||
/* 兜底:triggerGd32Relay 内部身份校验拒绝(与上方同判据,正常不会走到) */
|
||||
(void)BleOta::instance().resumeStack();
|
||||
return http_send_json_error(req, "400 Bad Request", "firmware_identity_mismatch",
|
||||
"固件身份校验失败,已拒绝刷写");
|
||||
}
|
||||
if (trigger_err != ESP_OK) {
|
||||
(void)BleOta::instance().resumeStack();
|
||||
return http_send_json_error(req, "400 Bad Request",
|
||||
@@ -1145,6 +1174,9 @@ esp_err_t can_send_batch_handler(httpd_req_t *req)
|
||||
return http_send_json_error(req, "400 Bad Request", "bad_args", "ch 字段格式错误");
|
||||
}
|
||||
long ch_val = readInt();
|
||||
if (ch_val < 0 || ch_val >= CAN_CHANNEL_MAX) {
|
||||
return http_send_json_error(req, "400 Bad Request", "bad_args", "ch 非法(0~2)");
|
||||
}
|
||||
if (!findKey("\"frames\"")) {
|
||||
return http_send_json_error(req, "400 Bad Request", "bad_args", "缺少 frames 字段");
|
||||
}
|
||||
@@ -2184,8 +2216,9 @@ esp_err_t motor_state_handler(httpd_req_t *req)
|
||||
* count u8 (有效 entry 数)
|
||||
* rsvd u16 = 0
|
||||
* Entry: node u8, ch u8, online u8, dlc u8,
|
||||
* rx_count u32, seq u32, data[32]
|
||||
* 27 槽(9 node × 3 ch,node×channel 双键)最大 12 + 27*44 = 1200B。
|
||||
* rx_count u32, seq u32, data[32],
|
||||
* config_rx_count u32, config_seq u32, config_data[24]
|
||||
* 27 槽(9 node × 3 ch,node×channel 双键)最大 12 + 27*76 = 2064B。
|
||||
* 无 have_data 时仍发 header(count=0),让前端区分「无数据」与「空总线」。 */
|
||||
bool want_bin = http_query_bool(req, "bin");
|
||||
if (want_bin) {
|
||||
@@ -2234,6 +2267,21 @@ esp_err_t motor_state_handler(httpd_req_t *req)
|
||||
buf[off++] = (uint8_t)((sq >> 24) & 0xFF);
|
||||
memcpy(&buf[off], s.data, MOTOR_STATUS_DLC);
|
||||
off += MOTOR_STATUS_DLC;
|
||||
/* config_rx_count LE */
|
||||
uint32_t cr = s.config_rx_count;
|
||||
buf[off++] = (uint8_t)(cr & 0xFF);
|
||||
buf[off++] = (uint8_t)((cr >> 8) & 0xFF);
|
||||
buf[off++] = (uint8_t)((cr >> 16) & 0xFF);
|
||||
buf[off++] = (uint8_t)((cr >> 24) & 0xFF);
|
||||
/* config_seq LE */
|
||||
uint32_t csq = s.config_seq;
|
||||
buf[off++] = (uint8_t)(csq & 0xFF);
|
||||
buf[off++] = (uint8_t)((csq >> 8) & 0xFF);
|
||||
buf[off++] = (uint8_t)((csq >> 16) & 0xFF);
|
||||
buf[off++] = (uint8_t)((csq >> 24) & 0xFF);
|
||||
/* config_data[24] */
|
||||
memcpy(&buf[off], s.config_data, MOTOR_CONFIG_DLC);
|
||||
off += MOTOR_CONFIG_DLC;
|
||||
count++;
|
||||
}
|
||||
}
|
||||
@@ -2257,7 +2305,9 @@ esp_err_t motor_state_handler(httpd_req_t *req)
|
||||
}
|
||||
|
||||
char *w = s_http_json_work;
|
||||
int off = snprintf(w, HTTP_JSON_WORK_CAP,
|
||||
/* http_json_append 截断时钳制在 cap-1;snprintf 截断返回期望长度,
|
||||
* off += snprintf 会越过 cap 导致 resp_send 越界读 */
|
||||
int off = http_json_append(w, HTTP_JSON_WORK_CAP, 0,
|
||||
"{\"have_data\":true,\"active_count\":%u,\"boot_active\":%s,\"age_ms\":%lu,\"motors\":[",
|
||||
(unsigned)tbl.active_count, tbl.boot_active ? "true" : "false", (unsigned long)age);
|
||||
|
||||
@@ -2269,22 +2319,28 @@ esp_err_t motor_state_handler(httpd_req_t *req)
|
||||
/* data hex */
|
||||
char hex[MOTOR_STATUS_DLC * 2 + 1];
|
||||
http_bytes_to_hex_upper(s.data, MOTOR_STATUS_DLC, hex);
|
||||
char chex[MOTOR_CONFIG_DLC * 2 + 1];
|
||||
http_bytes_to_hex_upper(s.config_data, MOTOR_CONFIG_DLC, chex);
|
||||
|
||||
off += snprintf(w + off, HTTP_JSON_WORK_CAP - off,
|
||||
off = http_json_append(w, HTTP_JSON_WORK_CAP, off,
|
||||
"%s{\"node\":%u,\"ch\":%u,\"online\":%s,\"dlc\":%u,"
|
||||
"\"rx_count\":%lu,\"seq\":%lu,\"data\":\"%s\"}",
|
||||
"\"rx_count\":%lu,\"seq\":%lu,\"data\":\"%s\","
|
||||
"\"config_rx_count\":%lu,\"config_seq\":%lu,\"config_data\":\"%s\"}",
|
||||
first ? "" : ",",
|
||||
(unsigned)s.node_id, (unsigned)s.channel,
|
||||
s.online ? "true" : "false",
|
||||
(unsigned)s.dlc,
|
||||
(unsigned long)s.rx_count,
|
||||
(unsigned long)s.seq,
|
||||
hex);
|
||||
hex,
|
||||
(unsigned long)s.config_rx_count,
|
||||
(unsigned long)s.config_seq,
|
||||
chex);
|
||||
first = false;
|
||||
}
|
||||
|
||||
if (off > 0 && (size_t)off < HTTP_JSON_WORK_CAP - 2) {
|
||||
off += snprintf(w + off, HTTP_JSON_WORK_CAP - off, "]}");
|
||||
if (off > 0) {
|
||||
off = http_json_append(w, HTTP_JSON_WORK_CAP, off, "]}");
|
||||
}
|
||||
|
||||
esp_err_t ret = httpd_resp_send(req, w, off > 0 ? off : 0);
|
||||
@@ -2325,7 +2381,8 @@ esp_err_t motor_version_handler(httpd_req_t *req)
|
||||
|
||||
const int64_t now_ms = esp_timer_get_time() / 1000;
|
||||
char *w = s_http_json_work;
|
||||
int off = snprintf(w, HTTP_JSON_WORK_CAP, "{\"have_data\":true,\"nodes\":[");
|
||||
int off = http_json_append(w, HTTP_JSON_WORK_CAP, 0,
|
||||
"{\"have_data\":true,\"nodes\":[");
|
||||
|
||||
bool first = true;
|
||||
for (uint8_t i = 0; i < MOTORVER_MAX_NODES && off > 0 && (size_t)off < HTTP_JSON_WORK_CAP - 128; ++i) {
|
||||
@@ -2337,7 +2394,7 @@ esp_err_t motor_version_handler(httpd_req_t *req)
|
||||
const long age = (s.recv_ms > 0 && now_ms >= s.recv_ms)
|
||||
? static_cast<long>(now_ms - s.recv_ms) : 0L;
|
||||
|
||||
off += snprintf(w + off, HTTP_JSON_WORK_CAP - off,
|
||||
off = http_json_append(w, HTTP_JSON_WORK_CAP, off,
|
||||
"%s{\"node\":%u,\"seq\":%lu,\"age_ms\":%ld,\"version\":\"%s\"}",
|
||||
first ? "" : ",",
|
||||
(unsigned)s.node_id,
|
||||
@@ -2347,8 +2404,8 @@ esp_err_t motor_version_handler(httpd_req_t *req)
|
||||
first = false;
|
||||
}
|
||||
|
||||
if (off > 0 && (size_t)off < HTTP_JSON_WORK_CAP - 2) {
|
||||
off += snprintf(w + off, HTTP_JSON_WORK_CAP - off, "]}");
|
||||
if (off > 0) {
|
||||
off = http_json_append(w, HTTP_JSON_WORK_CAP, off, "]}");
|
||||
}
|
||||
|
||||
esp_err_t ret = httpd_resp_send(req, w, off > 0 ? off : 0);
|
||||
@@ -2364,7 +2421,14 @@ esp_err_t motor_ota_upload_handler(httpd_req_t *req)
|
||||
{
|
||||
http_set_cors_headers(req);
|
||||
|
||||
uint8_t channel = static_cast<uint8_t>(http_query_u32(req, "ch", 0));
|
||||
const uint32_t ch_q = http_query_u32(req, "ch", 0);
|
||||
if (ch_q >= CAN_CHANNEL_MAX) {
|
||||
httpd_resp_set_status(req, "400 Bad Request");
|
||||
httpd_resp_set_type(req, "application/json");
|
||||
httpd_resp_sendstr(req, "{\"error\":\"bad channel (0~2)\"}");
|
||||
return ESP_FAIL;
|
||||
}
|
||||
uint8_t channel = static_cast<uint8_t>(ch_q);
|
||||
|
||||
const int content_len = req->content_len;
|
||||
if (content_len <= 0 || content_len > (int)MOTOR_OTA_MAX_FW_SIZE) {
|
||||
@@ -2480,7 +2544,14 @@ esp_err_t motor_ota_relay_handler(httpd_req_t *req)
|
||||
http_set_keepalive_hdr(req);
|
||||
DEBUG_INFO("MOTOR_OTA", "=== HTTP: POST /motor/ota/relay ===");
|
||||
|
||||
uint8_t channel = static_cast<uint8_t>(http_query_u32(req, "ch", 0));
|
||||
const uint32_t ch_q = http_query_u32(req, "ch", 0);
|
||||
if (ch_q >= CAN_CHANNEL_MAX) {
|
||||
httpd_resp_set_status(req, "400 Bad Request");
|
||||
httpd_resp_set_type(req, "application/json");
|
||||
httpd_resp_sendstr(req, "{\"error\":\"bad channel (0~2)\"}");
|
||||
return ESP_FAIL;
|
||||
}
|
||||
uint8_t channel = static_cast<uint8_t>(ch_q);
|
||||
|
||||
/* 读 meta 拿 fw_size(motorOtaStart 参数的唯一来源)。
|
||||
* total_size==0 = 无固件可刷(未上传过,或上次 relay 成功后 metaClear 已消费)。
|
||||
|
||||
Reference in New Issue
Block a user