feat(main): 添加CAN 1Hz反馈轮询任务和HTTP接口
- 新增monitor/can_feedback_task.cpp组件,实现CAN 1Hz反馈轮询功能 - 在main.cpp中注册CAN反馈轮询任务,依赖SPI初始化完成后启动 - 在OTA协调器和中继模块中集成CAN反馈任务暂停/恢复机制 - 扩展CANFD桥接层,添加0xC1反馈查询、0xC2模式切换、0xC3ID快照协议支持 - 新增HTTP接口/can/feedback提供三路CAN反馈快照数据 - 新增HTTP接口/can/mode支持查询和切换CAN接收过滤模式 - 更新版本号从0.3.34到0.35 - 扩大SPI缓冲区以支持更大帧传输需求 BREAKING CHANGE: 新增CAN反馈监控功能需要GD32固件支持0xC1/0xC2/0xC3协议扩展
This commit is contained in:
@@ -10,6 +10,7 @@ idf_component_register(
|
||||
"server/http_handlers.cpp" # HTTP 路由处理(H2 从 wifi_ota 提取)
|
||||
"server/ble_ota.cpp" # BLE GATT OTA 服务器
|
||||
"monitor/health_monitor.cpp" # 系统健康监控(温度/RSSI/堆内存)
|
||||
"monitor/can_feedback_task.cpp" # CAN 1Hz 反馈轮询任务(0xC1)
|
||||
"monitor/debug_log.cpp" # 三路调试日志(UART/UDP/环形缓冲)
|
||||
INCLUDE_DIRS
|
||||
"."
|
||||
|
||||
+9
-1
@@ -1,4 +1,4 @@
|
||||
/*
|
||||
/*
|
||||
* ESP32-C3FH4 OTA 中继 - 程序入口
|
||||
* 四个 FreeRTOS 任务:健康监控 / OTA Relay / WiFi Server / BLE GATT
|
||||
*
|
||||
@@ -23,6 +23,7 @@
|
||||
#include "monitor/debug_log.h"
|
||||
#include "app_config.h"
|
||||
#include "ota/esp32_ota_core.h"
|
||||
#include "monitor/can_feedback_task.h"
|
||||
|
||||
/**
|
||||
* @brief 程序主入口,初始化系统组件并创建各 FreeRTOS 任务。
|
||||
@@ -150,6 +151,13 @@ extern "C" void app_main(void)
|
||||
DEBUG_INFO("MAIN", "BLE OTA 任务已创建");
|
||||
}
|
||||
|
||||
// CAN 1Hz 反馈轮询任务(依赖 SPI 已初始化)
|
||||
if (startCanFeedbackTask()) {
|
||||
DEBUG_INFO("MAIN", "CAN 反馈轮询任务已创建");
|
||||
} else {
|
||||
DEBUG_WARN("MAIN", "CAN 反馈轮询任务创建失败(非致命,OTA 不受影响)");
|
||||
}
|
||||
|
||||
// ★ OTA 回滚保护:标记当前固件有效
|
||||
// 启用 CONFIG_APP_ROLLBACK_ENABLE 后,新固件首次启动必须调用此函数
|
||||
// 若未调用就崩溃/重启,bootloader 会自动回退到上一个有效分区
|
||||
|
||||
@@ -4,11 +4,11 @@
|
||||
// 固件版本
|
||||
#define FW_VERSION_MAJOR 0
|
||||
#define FW_VERSION_MINOR 3
|
||||
#define FW_VERSION_PATCH 34
|
||||
#define FW_VERSION_STRING "0.3.34"
|
||||
#define FW_VERSION_PATCH 35
|
||||
#define FW_VERSION_STRING "0.3.35"
|
||||
|
||||
// 编译时间
|
||||
#define BUILD_TIME "2026-06-28 04:26:17"
|
||||
#define BUILD_TIME "2026-06-29 01:50:43"
|
||||
|
||||
// Git 信息
|
||||
#define GIT_HASH "6ef9730"
|
||||
#define GIT_HASH "a93dba6"
|
||||
|
||||
@@ -0,0 +1,181 @@
|
||||
/*
|
||||
* can_feedback_task.cpp - 周期性轮询 GD32 三路 CAN 1Hz 反馈快照
|
||||
*
|
||||
* 通过 SPI 协议帧 0xC1 拉取 GD32 端的通道统计与最近一帧信息,
|
||||
* 用调试日志输出;其它消费者(Web/WebSocket)可直接读取
|
||||
* CanFeedbackTask::snapshot(channel) 拷贝最新一份数据。
|
||||
*/
|
||||
#include "freertos/FreeRTOS.h"
|
||||
#include "freertos/task.h"
|
||||
#include "freertos/semphr.h"
|
||||
|
||||
#include "transport/canfd_bridge.h"
|
||||
#include "monitor/debug_log.h"
|
||||
#include "monitor/can_feedback_task.h"
|
||||
|
||||
#include <string.h>
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr uint32_t kPollIntervalMs = 1000U; ///< 1Hz 拉取节拍
|
||||
constexpr uint32_t kPollTimeoutMs = 200U; ///< 单次 SPI 事务超时
|
||||
constexpr const char *kTag = "CAN_FB";
|
||||
|
||||
static can_feedback_result_t s_latest[CAN_CHANNEL_MAX];
|
||||
static bool s_valid [CAN_CHANNEL_MAX];
|
||||
static SemaphoreHandle_t s_mutex = nullptr;
|
||||
static volatile bool s_paused = false;
|
||||
static can_channel_feedback_t s_prev_stats[CAN_CHANNEL_MAX];
|
||||
static bool s_prev_valid[CAN_CHANNEL_MAX];
|
||||
static bool s_prev_poll_ok[CAN_CHANNEL_MAX];
|
||||
|
||||
static void publish(uint8_t ch, const can_feedback_result_t &fb)
|
||||
{
|
||||
if (s_mutex == nullptr) return;
|
||||
if (xSemaphoreTake(s_mutex, pdMS_TO_TICKS(50)) == pdTRUE) {
|
||||
s_latest[ch] = fb;
|
||||
s_valid[ch] = true;
|
||||
xSemaphoreGive(s_mutex);
|
||||
}
|
||||
}
|
||||
|
||||
static void taskLoop(void *)
|
||||
{
|
||||
DEBUG_INFO(kTag, "CAN 反馈轮询任务启动 (1Hz, 0xC1)");
|
||||
|
||||
can_feedback_result_t fb{};
|
||||
for (;;) {
|
||||
/* OTA 进行中时暂停 CAN 轮询,避免抢占 SPI 导致 OTA 握手失败 */
|
||||
if (s_paused) {
|
||||
vTaskDelay(pdMS_TO_TICKS(kPollIntervalMs));
|
||||
continue;
|
||||
}
|
||||
|
||||
for (uint8_t ch = 0U; ch < CAN_CHANNEL_MAX; ++ch) {
|
||||
const int rc = canFdPollFeedback(static_cast<can_channel_t>(ch),
|
||||
&fb, kPollTimeoutMs);
|
||||
if (rc == 0) {
|
||||
publish(ch, fb);
|
||||
|
||||
const bool prevOk = s_prev_poll_ok[ch];
|
||||
const bool changed = (!s_prev_valid[ch]) ||
|
||||
(fb.stats.rx_count != s_prev_stats[ch].rx_count) ||
|
||||
(fb.stats.tx_count != s_prev_stats[ch].tx_count) ||
|
||||
(fb.stats.rx_drop_count != s_prev_stats[ch].rx_drop_count) ||
|
||||
(fb.stats.error_count != s_prev_stats[ch].error_count) ||
|
||||
(fb.stats.mode != s_prev_stats[ch].mode) ||
|
||||
(fb.stats.snapshot_seq != s_prev_stats[ch].snapshot_seq);
|
||||
const bool abnormal = (fb.stats.rx_drop_count != 0U) ||
|
||||
(fb.stats.error_count != 0U) ||
|
||||
(fb.stats.mode != 0U);
|
||||
|
||||
if (!prevOk || changed || abnormal) {
|
||||
DEBUG_INFO(kTag,
|
||||
"CH%u seq=%lu mode=%u rx=%lu tx=%lu drop=%lu err=%lu ids=%u",
|
||||
static_cast<unsigned>(ch),
|
||||
static_cast<unsigned long>(fb.stats.snapshot_seq),
|
||||
static_cast<unsigned>(fb.stats.mode),
|
||||
static_cast<unsigned long>(fb.stats.rx_count),
|
||||
static_cast<unsigned long>(fb.stats.tx_count),
|
||||
static_cast<unsigned long>(fb.stats.rx_drop_count),
|
||||
static_cast<unsigned long>(fb.stats.error_count),
|
||||
static_cast<unsigned>(fb.valid));
|
||||
}
|
||||
|
||||
s_prev_stats[ch] = fb.stats;
|
||||
s_prev_valid[ch] = true;
|
||||
s_prev_poll_ok[ch] = true;
|
||||
} else {
|
||||
if (s_prev_poll_ok[ch]) {
|
||||
DEBUG_WARN(kTag, "CH%u 轮询失败", static_cast<unsigned>(ch));
|
||||
}
|
||||
s_prev_stats[ch] = {};
|
||||
s_prev_valid[ch] = false;
|
||||
s_prev_poll_ok[ch] = false;
|
||||
}
|
||||
}
|
||||
vTaskDelay(pdMS_TO_TICKS(kPollIntervalMs));
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
/**
|
||||
* @brief 启动 1Hz CAN 反馈轮询任务(幂等)。
|
||||
* @return true 表示任务已存在或创建成功。
|
||||
*/
|
||||
bool startCanFeedbackTask()
|
||||
{
|
||||
static bool s_started = false;
|
||||
if (s_started) return true;
|
||||
|
||||
if (s_mutex == nullptr) {
|
||||
s_mutex = xSemaphoreCreateMutex();
|
||||
if (s_mutex == nullptr) {
|
||||
DEBUG_ERR(kTag, "互斥量创建失败");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
memset(s_latest, 0, sizeof(s_latest));
|
||||
memset(s_valid, 0, sizeof(s_valid));
|
||||
|
||||
BaseType_t xRet = xTaskCreatePinnedToCore(
|
||||
taskLoop,
|
||||
"can_fb",
|
||||
4096,
|
||||
nullptr,
|
||||
4,
|
||||
nullptr,
|
||||
0);
|
||||
if (xRet != pdPASS) {
|
||||
DEBUG_ERR(kTag, "任务创建失败");
|
||||
return false;
|
||||
}
|
||||
s_started = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
void pauseCanFeedbackTask(bool pause)
|
||||
{
|
||||
s_paused = pause;
|
||||
if (pause) {
|
||||
DEBUG_INFO(kTag, "CAN 轮询已暂停(OTA 进行中)");
|
||||
} else {
|
||||
DEBUG_INFO(kTag, "CAN 轮询已恢复");
|
||||
}
|
||||
}
|
||||
|
||||
bool getLatestIdSnapshot(uint8_t channel, id_snapshot_cache_t *out)
|
||||
{
|
||||
if (channel >= CAN_CHANNEL_MAX || out == nullptr || s_mutex == nullptr) return false;
|
||||
bool ok = false;
|
||||
if (xSemaphoreTake(s_mutex, pdMS_TO_TICKS(100)) == pdTRUE) {
|
||||
if (s_latest[channel].valid) {
|
||||
out->count = s_latest[channel].valid;
|
||||
memcpy(out->entries, s_latest[channel].entries, sizeof(out->entries));
|
||||
ok = true;
|
||||
}
|
||||
xSemaphoreGive(s_mutex);
|
||||
}
|
||||
return ok;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 拷贝某通道的最新快照(线程安全)。
|
||||
* @return true 表示拿到一份有效数据。
|
||||
*/
|
||||
bool getLatestCanFeedback(uint8_t channel, can_feedback_result_t *out)
|
||||
{
|
||||
if (channel >= CAN_CHANNEL_MAX || out == nullptr || s_mutex == nullptr) {
|
||||
return false;
|
||||
}
|
||||
bool ok = false;
|
||||
if (xSemaphoreTake(s_mutex, pdMS_TO_TICKS(50)) == pdTRUE) {
|
||||
if (s_valid[channel]) {
|
||||
*out = s_latest[channel];
|
||||
ok = true;
|
||||
}
|
||||
xSemaphoreGive(s_mutex);
|
||||
}
|
||||
return ok;
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
/*
|
||||
* can_feedback_task.h - 周期性轮询 GD32 CAN 反馈快照的任务接口
|
||||
*/
|
||||
#ifndef CAN_FEEDBACK_TASK_H
|
||||
#define CAN_FEEDBACK_TASK_H
|
||||
|
||||
#include <stdint.h>
|
||||
#include "transport/canfd_bridge.h"
|
||||
|
||||
/** @brief ID 快照缓存结构。 */
|
||||
typedef struct {
|
||||
id_snapshot_entry_t entries[CANFD_ID_SNAPSHOT_MAX];
|
||||
uint8_t count;
|
||||
bool valid;
|
||||
} id_snapshot_cache_t;
|
||||
|
||||
/** @brief 暂停/恢复 CAN 反馈轮询(OTA 期间调用 pause=true)。 */
|
||||
void pauseCanFeedbackTask(bool pause);
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
/**
|
||||
* @brief 启动 1Hz CAN 反馈轮询任务(幂等)。
|
||||
* @return true 表示任务已存在或创建成功。
|
||||
*/
|
||||
bool startCanFeedbackTask();
|
||||
|
||||
/**
|
||||
* @brief 拷贝某通道的最新反馈快照(线程安全)。
|
||||
* @param channel 0/1/2。
|
||||
* @param out 输出缓冲。
|
||||
* @return true 表示拿到一份有效数据,false 表示通道无效或尚未拿到第一份。
|
||||
*/
|
||||
bool getLatestCanFeedback(uint8_t channel, can_feedback_result_t *out);
|
||||
|
||||
/**
|
||||
* @brief 获取指定通道的最新 ID 快照(线程安全)。
|
||||
* @param channel 0/1/2。
|
||||
* @param out 输出缓冲。
|
||||
* @return true 有有效数据。
|
||||
*/
|
||||
bool getLatestIdSnapshot(uint8_t channel, id_snapshot_cache_t *out);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif // CAN_FEEDBACK_TASK_H
|
||||
@@ -5,6 +5,7 @@
|
||||
* 传输层(WiFi/BLE)通过本协调器发起所有 OTA 操作。
|
||||
*/
|
||||
#include "ota_coordinator.h"
|
||||
#include "monitor/can_feedback_task.h"
|
||||
#include "esp32_ota_core.h"
|
||||
#include "relay/ota_relay.h"
|
||||
#include "storage/gd32_flash_store.h"
|
||||
@@ -313,6 +314,7 @@ esp_err_t OtaCoordinator::triggerGd32Relay()
|
||||
|
||||
m_sysState = OTA_SYS_GD32_RELAYING;
|
||||
m_busySinceTick = xTaskGetTickCount(); // ★ 记录忙状态起始时刻
|
||||
pauseCanFeedbackTask(true);
|
||||
DEBUG_INFO(TAG, "触发 GD32 中继, 系统状态 → GD32_RELAYING");
|
||||
|
||||
uint32_t cmd = 1;
|
||||
@@ -336,6 +338,7 @@ void OtaCoordinator::notifyRelayStarted()
|
||||
if (h && m_sysState != OTA_SYS_GD32_RELAYING) {
|
||||
m_sysState = OTA_SYS_GD32_RELAYING;
|
||||
m_busySinceTick = xTaskGetTickCount(); // ★ 记录起始时刻
|
||||
pauseCanFeedbackTask(true);
|
||||
DEBUG_INFO(TAG, "中继已开始, 系统状态 → GD32_RELAYING");
|
||||
}
|
||||
unlock(h);
|
||||
@@ -350,10 +353,12 @@ void OtaCoordinator::notifyRelayComplete(bool success)
|
||||
m_sysState = OTA_SYS_GD32_COMPLETE;
|
||||
m_gd32StorageDone = false;
|
||||
m_busySinceTick = 0; // ★ COMPLETE 非忙
|
||||
pauseCanFeedbackTask(false);
|
||||
DEBUG_INFO(TAG, "GD32 中继完成, 系统状态 → GD32_COMPLETE");
|
||||
} else {
|
||||
m_sysState = OTA_SYS_ERROR;
|
||||
m_busySinceTick = 0; // ★ ERROR 非忙(可被后续请求恢复)
|
||||
pauseCanFeedbackTask(false);
|
||||
DEBUG_ERR(TAG, "GD32 中继失败, 系统状态 → ERROR");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
* 协议子步骤与加密/Flash 工具方法保持不变。
|
||||
*/
|
||||
#include <string.h>
|
||||
#include "monitor/can_feedback_task.h"
|
||||
#include <stdlib.h>
|
||||
#include <inttypes.h>
|
||||
#include "freertos/FreeRTOS.h"
|
||||
@@ -1335,6 +1336,7 @@ void OtaRelay::taskEntry(void *pvParameters)
|
||||
DEBUG_INFO("OTA", "收到 OTA 调度信号: %" PRIu32 ",开始转发...", msg);
|
||||
// ★ H4: 通过 IRelayListener 接口回调通知协调器(不再直接依赖 OtaCoordinator)
|
||||
IRelayListener *listener = getRelayListener();
|
||||
pauseCanFeedbackTask(true);
|
||||
if (listener) listener->onRelayStarted();
|
||||
ota_result_t result = instance().forwardFirmware();
|
||||
|
||||
@@ -1353,5 +1355,6 @@ void OtaRelay::taskEntry(void *pvParameters)
|
||||
// ★ H4: 通过 IRelayListener 接口回调通知协调器中继结束
|
||||
listener = getRelayListener();
|
||||
if (listener) listener->onRelayComplete(result == OTA_RESULT_OK);
|
||||
pauseCanFeedbackTask(false);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
/*
|
||||
/*
|
||||
* http_handlers.cpp - HTTP 请求处理器实现
|
||||
* 从 wifi_ota.cpp 提取的 HTTP handler 函数
|
||||
*/
|
||||
@@ -28,6 +28,7 @@
|
||||
#include "monitor/debug_log.h"
|
||||
#include "spi_master.h"
|
||||
#include "canfd_bridge.h"
|
||||
#include "monitor/can_feedback_task.h"
|
||||
#include "ble_ota.h"
|
||||
|
||||
// OTA 准备状态:/ota/prepare 完成后设置,/ota/upload 消费后清除
|
||||
@@ -714,6 +715,140 @@ esp_err_t logs_handler(httpd_req_t *req)
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
|
||||
// ============================================================
|
||||
// GET /can/feedback — 三路 CAN 1Hz 反馈快照
|
||||
// ============================================================
|
||||
esp_err_t can_feedback_handler(httpd_req_t *req)
|
||||
{
|
||||
http_set_cors_headers(req);
|
||||
|
||||
can_feedback_result_t results[CAN_CHANNEL_MAX] = {};
|
||||
bool valid[CAN_CHANNEL_MAX] = {};
|
||||
for (uint8_t i = 0; i < CAN_CHANNEL_MAX; ++i) {
|
||||
valid[i] = getLatestCanFeedback(i, &results[i]);
|
||||
}
|
||||
|
||||
/* 估算缓冲大小:每通道 ~2KB (统计+ID条目) */
|
||||
char *resp = (char *)calloc(1, 8192);
|
||||
if (!resp) {
|
||||
httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, "no memory");
|
||||
return ESP_FAIL;
|
||||
}
|
||||
|
||||
char *p = resp;
|
||||
p += sprintf(p, "{\"channels\":[");
|
||||
for (uint8_t i = 0; i < CAN_CHANNEL_MAX; ++i) {
|
||||
const can_feedback_result_t &r = results[i];
|
||||
if (i > 0) p += sprintf(p, ",");
|
||||
p += sprintf(p, "{\"ch\":%u,\"valid\":%s", (unsigned)i,
|
||||
valid[i] ? "true" : "false");
|
||||
if (valid[i]) {
|
||||
p += sprintf(p,
|
||||
",\"mode\":%u,\"seq\":%lu,\"rx\":%lu,\"tx\":%lu,"
|
||||
"\"drop\":%lu,\"err\":%lu,\"count\":%u,\"entries\":[",
|
||||
(unsigned)r.stats.mode,
|
||||
(unsigned long)r.stats.snapshot_seq,
|
||||
(unsigned long)r.stats.rx_count,
|
||||
(unsigned long)r.stats.tx_count,
|
||||
(unsigned long)r.stats.rx_drop_count,
|
||||
(unsigned long)r.stats.error_count,
|
||||
(unsigned)r.total_count);
|
||||
for (uint8_t j = 0; j < r.total_count && j < CANFD_ID_SNAPSHOT_MAX; ++j) {
|
||||
const id_snapshot_entry_t &e = r.entries[j];
|
||||
if (j > 0) p += sprintf(p, ",");
|
||||
p += sprintf(p,
|
||||
"{\"id\":\"0x%08lX\",\"dlc\":%u,\"fd\":%u,"
|
||||
"\"brs\":%u,\"esi\":%u,\"data\":\"",
|
||||
(unsigned long)e.id,
|
||||
(unsigned)e.dlc,
|
||||
(unsigned)e.fd_flag,
|
||||
(unsigned)e.brs,
|
||||
(unsigned)e.esi);
|
||||
const uint8_t dataLen = (e.dlc < CANFD_ID_SNAPSHOT_DATA_MAX)
|
||||
? e.dlc : CANFD_ID_SNAPSHOT_DATA_MAX;
|
||||
for (uint8_t d = 0; d < dataLen; ++d) {
|
||||
p += sprintf(p, "%02X", e.data[d]);
|
||||
}
|
||||
p += sprintf(p, "\"}");
|
||||
}
|
||||
p += sprintf(p, "]");
|
||||
}
|
||||
p += sprintf(p, "}");
|
||||
}
|
||||
p += sprintf(p, "]}");
|
||||
|
||||
httpd_resp_set_type(req, "application/json");
|
||||
httpd_resp_send(req, resp, (int)(p - resp));
|
||||
free(resp);
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// GET/POST /can/mode — 查询或切换三路 CAN 接收模式
|
||||
// ============================================================
|
||||
esp_err_t can_mode_handler(httpd_req_t *req)
|
||||
{
|
||||
http_set_cors_headers(req);
|
||||
|
||||
if (req->method == HTTP_GET) {
|
||||
can_feedback_result_t fb{};
|
||||
if (getLatestCanFeedback(0, &fb)) {
|
||||
char resp[64];
|
||||
int len = snprintf(resp, sizeof(resp),
|
||||
"{\"mode\":%u}", (unsigned)fb.stats.mode);
|
||||
httpd_resp_set_type(req, "application/json");
|
||||
httpd_resp_send(req, resp, len);
|
||||
} else {
|
||||
can_rx_mode_t actual = CAN_RX_MODE_ALL_PASS;
|
||||
int rc = canFdSetRxMode(CAN_RX_MODE_ALL_PASS, &actual, 500);
|
||||
char resp[64];
|
||||
int len = snprintf(resp, sizeof(resp),
|
||||
"{\"mode\":%u,\"source\":\"spi\",\"ok\":%s}",
|
||||
(unsigned)actual, rc == 0 ? "true" : "false");
|
||||
httpd_resp_set_type(req, "application/json");
|
||||
httpd_resp_send(req, resp, len);
|
||||
}
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
char buf[128] = {0};
|
||||
int ret = httpd_req_recv(req, buf, sizeof(buf) - 1);
|
||||
if (ret <= 0) {
|
||||
httpd_resp_send_err(req, HTTPD_400_BAD_REQUEST, "接收数据失败");
|
||||
return ESP_FAIL;
|
||||
}
|
||||
buf[ret] = '\0';
|
||||
|
||||
cJSON *root = cJSON_Parse(buf);
|
||||
if (!root) {
|
||||
httpd_resp_send_err(req, HTTPD_400_BAD_REQUEST, "JSON 解析失败");
|
||||
return ESP_FAIL;
|
||||
}
|
||||
|
||||
cJSON *mode_item = cJSON_GetObjectItem(root, "mode");
|
||||
if (!mode_item || !cJSON_IsNumber(mode_item)) {
|
||||
cJSON_Delete(root);
|
||||
httpd_resp_send_err(req, HTTPD_400_BAD_REQUEST, "缺少 mode 字段");
|
||||
return ESP_FAIL;
|
||||
}
|
||||
|
||||
int requested = (int)cJSON_GetNumberValue(mode_item);
|
||||
cJSON_Delete(root);
|
||||
|
||||
can_rx_mode_t target = (requested == 1) ? CAN_RX_MODE_FILTER_ID
|
||||
: CAN_RX_MODE_ALL_PASS;
|
||||
can_rx_mode_t actual = CAN_RX_MODE_ALL_PASS;
|
||||
int rc = canFdSetRxMode(target, &actual, 500);
|
||||
|
||||
char resp[96];
|
||||
int len = snprintf(resp, sizeof(resp),
|
||||
"{\"ok\":%s,\"requested\":%d,\"actual\":%u}",
|
||||
rc == 0 ? "true" : "false", requested, (unsigned)actual);
|
||||
httpd_resp_set_type(req, "application/json");
|
||||
httpd_resp_send(req, resp, len);
|
||||
return ESP_OK;
|
||||
}
|
||||
esp_err_t can_send_handler(httpd_req_t *req)
|
||||
{
|
||||
http_set_cors_headers(req);
|
||||
|
||||
@@ -20,4 +20,8 @@ esp_err_t spi_send_handler(httpd_req_t *req);
|
||||
esp_err_t system_status_handler(httpd_req_t *req);
|
||||
esp_err_t system_probe_handler(httpd_req_t *req);
|
||||
|
||||
// CAN 1Hz 反馈快照与接收模式
|
||||
esp_err_t can_feedback_handler(httpd_req_t *req);
|
||||
esp_err_t can_mode_handler(httpd_req_t *req);
|
||||
|
||||
#endif // HTTP_HANDLERS_H
|
||||
|
||||
@@ -456,6 +456,9 @@ esp_err_t WifiOta::httpServerStart()
|
||||
{"/system/status", HTTP_GET, system_status_handler},
|
||||
{"/system/probe", HTTP_GET, system_probe_handler},
|
||||
{"/can/send", HTTP_POST, can_send_handler},
|
||||
{"/can/feedback", HTTP_GET, can_feedback_handler},
|
||||
{"/can/mode", HTTP_GET, can_mode_handler},
|
||||
{"/can/mode", HTTP_POST, can_mode_handler},
|
||||
{"/spi/send", HTTP_POST, spi_send_handler},
|
||||
};
|
||||
|
||||
@@ -475,7 +478,7 @@ esp_err_t WifiOta::httpServerStart()
|
||||
httpd_register_uri_handler(WifiOta::instance().m_httpServer, &opts_uri);
|
||||
}
|
||||
|
||||
DEBUG_INFO("WIFI", "注册端点: /ota/upload, /ota/relay, /ota/update, /restart, /logs, /system/status, /system/probe, /can/send, /spi/send");
|
||||
DEBUG_INFO("WIFI", "注册端点: /ota/upload, /ota/relay, /ota/update, /restart, /logs, /system/status, /system/probe, /can/send, /spi/send, /can/feedback, /can/mode");
|
||||
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
/*
|
||||
/*
|
||||
* canfd_bridge.cpp - CANFD 3通道通信桥接实现(M4: 从 SpiMaster 提取)
|
||||
*/
|
||||
#include <string.h>
|
||||
@@ -50,6 +50,18 @@ static int sendCanFrameOverSpi(can_channel_t channel, uint32_t ext_id,
|
||||
return static_cast<int>(copied_len);
|
||||
}
|
||||
|
||||
constexpr uint8_t kSpiCanFeedbackType = 0xC1U;
|
||||
constexpr uint8_t kSpiCanSetModeType = 0xC2U;
|
||||
constexpr uint8_t kSpiIdSnapshotType = 0xC3U;
|
||||
constexpr size_t kFeedbackPayloadLen = 123U; // 与 GD32 协议表对齐 (kTxBufSize=128 - overhead=5)
|
||||
|
||||
static inline uint32_t readU32Le(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);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
/**
|
||||
@@ -80,3 +92,182 @@ int canFdSendNoReply(can_channel_t channel, uint32_t ext_id,
|
||||
|
||||
return (sendCanFrameOverSpi(channel, ext_id, data, dlc, nullptr, 0U, 1000U) >= 0) ? 0 : -1;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @brief 拉取一路 CAN 通道的 1Hz 反馈 + ID 快照表(GD32 SPI 帧类型 0xC1,分页)。
|
||||
* 连续拉取 page=0 和 page=1,合并为完整结果。
|
||||
*/
|
||||
int canFdPollFeedback(can_channel_t channel,
|
||||
can_feedback_result_t *out,
|
||||
uint32_t timeout_ms)
|
||||
{
|
||||
if (channel >= CAN_CHANNEL_MAX || out == nullptr) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
out->valid = 0;
|
||||
uint8_t entryIdx = 0U;
|
||||
|
||||
constexpr uint8_t kFixedHeader = 25U; /* ch+page+total+cnt+mode+seq+rx+tx+drop+err */
|
||||
|
||||
for (uint8_t page = 0U; page < 2U; ++page) {
|
||||
uint8_t req[2] = { static_cast<uint8_t>(channel & CAN_CHANNEL_MASK), page };
|
||||
uint8_t resp_buf[kFeedbackPayloadLen] = {};
|
||||
uint8_t resp_type = 0U;
|
||||
size_t resp_len = 0U;
|
||||
|
||||
esp_err_t ret = SpiMaster::instance().otaTransfer(
|
||||
kSpiCanFeedbackType,
|
||||
req, 2U,
|
||||
&resp_type, resp_buf, sizeof(resp_buf), &resp_len, timeout_ms);
|
||||
if (ret != ESP_OK || resp_type != SPI_OTA_RSP_ACK || resp_len < kFixedHeader) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
/* 第一页时解析固定头 */
|
||||
if (page == 0U) {
|
||||
out->stats.channel = resp_buf[0];
|
||||
out->stats.mode = resp_buf[4];
|
||||
out->stats.snapshot_seq = readU32Le(&resp_buf[5]);
|
||||
out->stats.rx_count = readU32Le(&resp_buf[9]);
|
||||
out->stats.tx_count = readU32Le(&resp_buf[13]);
|
||||
out->stats.rx_drop_count = readU32Le(&resp_buf[17]);
|
||||
out->stats.error_count = readU32Le(&resp_buf[21]);
|
||||
out->total_count = resp_buf[2];
|
||||
}
|
||||
|
||||
const uint8_t entryCount = resp_buf[3];
|
||||
if (entryCount == 0U) break;
|
||||
|
||||
uint16_t offset = kFixedHeader;
|
||||
for (uint8_t i = 0U; i < entryCount && entryIdx < CANFD_ID_SNAPSHOT_MAX; ++i) {
|
||||
if (offset + 8U > resp_len) break;
|
||||
|
||||
id_snapshot_entry_t &e = out->entries[entryIdx];
|
||||
e.id = readU32Le(&resp_buf[offset]);
|
||||
e.dlc = resp_buf[offset + 4U];
|
||||
const uint8_t flags = resp_buf[offset + 5U];
|
||||
e.fd_flag = (flags & 0x01U) ? 1U : 0U;
|
||||
e.brs = (flags & 0x02U) ? 1U : 0U;
|
||||
e.esi = (flags & 0x04U) ? 1U : 0U;
|
||||
|
||||
const uint8_t dataLen = (e.dlc < CANFD_ID_SNAPSHOT_DATA_MAX)
|
||||
? e.dlc : static_cast<uint8_t>(CANFD_ID_SNAPSHOT_DATA_MAX);
|
||||
const uint16_t entrySize = static_cast<uint16_t>(8U + dataLen);
|
||||
if (offset + entrySize > resp_len) break;
|
||||
|
||||
memcpy(e.data, &resp_buf[offset + 8U], dataLen);
|
||||
for (uint8_t d = dataLen; d < CANFD_ID_SNAPSHOT_DATA_MAX; ++d) {
|
||||
e.data[d] = 0U;
|
||||
}
|
||||
|
||||
offset += entrySize;
|
||||
entryIdx++;
|
||||
}
|
||||
|
||||
if (entryIdx >= out->total_count) break;
|
||||
}
|
||||
out->valid = 1;
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 切换 GD32 三路 CAN 接收过滤模式(GD32 SPI 帧类型 0xC2)。
|
||||
*/
|
||||
|
||||
/**
|
||||
* @brief 拉取指定通道的不重复 ID 快照表(GD32 SPI 帧类型 0xC3)。
|
||||
* 分页拉取,每页 8 条,最多拉 2 页(16 条)。
|
||||
*/
|
||||
int canFdPullIdSnapshot(can_channel_t channel,
|
||||
id_snapshot_entry_t *out,
|
||||
uint8_t *out_count,
|
||||
uint32_t timeout_ms)
|
||||
{
|
||||
if (channel >= CAN_CHANNEL_MAX || out == nullptr || out_count == nullptr) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
*out_count = 0U;
|
||||
uint8_t total = 0U;
|
||||
|
||||
for (uint8_t page = 0U; page < 2U; ++page) {
|
||||
uint8_t req[2] = { static_cast<uint8_t>(channel), page };
|
||||
uint8_t resp_buf[kFeedbackPayloadLen] = {};
|
||||
uint8_t resp_type = 0U;
|
||||
size_t resp_len = 0U;
|
||||
|
||||
esp_err_t ret = SpiMaster::instance().otaTransfer(
|
||||
kSpiIdSnapshotType,
|
||||
req, 2U,
|
||||
&resp_type, resp_buf, sizeof(resp_buf), &resp_len, timeout_ms);
|
||||
if (ret != ESP_OK || resp_type != SPI_OTA_RSP_ACK || resp_len < 4U) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
/* resp: [ch][page][total_count][entry_count][entries...] */
|
||||
if (page == 0U) {
|
||||
total = resp_buf[2];
|
||||
}
|
||||
const uint8_t entryCount = resp_buf[3];
|
||||
if (entryCount == 0U) break;
|
||||
|
||||
uint16_t offset = 4U;
|
||||
for (uint8_t i = 0U; i < entryCount && *out_count < CANFD_ID_SNAPSHOT_MAX; ++i) {
|
||||
if (offset + 16U > resp_len) break;
|
||||
|
||||
id_snapshot_entry_t &e = out[*out_count];
|
||||
e.id = readU32Le(&resp_buf[offset]);
|
||||
e.seq = readU32Le(&resp_buf[offset + 4U]);
|
||||
e.rx_count = readU32Le(&resp_buf[offset + 8U]);
|
||||
e.dlc = resp_buf[offset + 12U];
|
||||
const uint8_t flags = resp_buf[offset + 13U];
|
||||
e.fd_flag = (flags & 0x01U) ? 1U : 0U;
|
||||
e.brs = (flags & 0x02U) ? 1U : 0U;
|
||||
e.esi = (flags & 0x04U) ? 1U : 0U;
|
||||
|
||||
const uint8_t dataLen = (e.dlc < CANFD_ID_SNAPSHOT_DATA_MAX)
|
||||
? e.dlc : static_cast<uint8_t>(CANFD_ID_SNAPSHOT_DATA_MAX);
|
||||
const uint16_t entrySize = 16U + dataLen;
|
||||
if (offset + entrySize > resp_len) break;
|
||||
|
||||
memcpy(e.data, &resp_buf[offset + 16U], dataLen);
|
||||
/* 清零剩余字节 */
|
||||
for (uint8_t d = dataLen; d < CANFD_ID_SNAPSHOT_DATA_MAX; ++d) {
|
||||
e.data[d] = 0U;
|
||||
}
|
||||
|
||||
offset += entrySize;
|
||||
(*out_count)++;
|
||||
}
|
||||
|
||||
if (*out_count >= total) break; /* 已拉完 */
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
int canFdSetRxMode(can_rx_mode_t mode,
|
||||
can_rx_mode_t *actual,
|
||||
uint32_t timeout_ms)
|
||||
{
|
||||
if (mode != CAN_RX_MODE_ALL_PASS && mode != CAN_RX_MODE_FILTER_ID) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
uint8_t req = static_cast<uint8_t>(mode);
|
||||
uint8_t resp_buf[1] = {0};
|
||||
uint8_t resp_type = 0U;
|
||||
size_t resp_len = 0U;
|
||||
|
||||
esp_err_t ret = SpiMaster::instance().otaTransfer(
|
||||
kSpiCanSetModeType,
|
||||
&req, 1U,
|
||||
&resp_type, resp_buf, sizeof(resp_buf), &resp_len, timeout_ms);
|
||||
if (ret != ESP_OK || resp_type != SPI_OTA_RSP_ACK || resp_len < 1U) {
|
||||
return -1;
|
||||
}
|
||||
if (actual != nullptr) {
|
||||
*actual = static_cast<can_rx_mode_t>(resp_buf[0]);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
/*
|
||||
/*
|
||||
* canfd_bridge.h - CANFD 3通道通信桥接(M4: 从 SpiMaster 提取)
|
||||
*
|
||||
* 封装 CANFD 帧格式构建 + SPI 传输,与 SpiMaster 的 OTA 协议职责解耦。
|
||||
@@ -61,4 +61,93 @@ int canFdSend(can_channel_t channel, uint32_t ext_id,
|
||||
int canFdSendNoReply(can_channel_t channel, uint32_t ext_id,
|
||||
const uint8_t *data, uint8_t dlc);
|
||||
|
||||
|
||||
// ============================================================
|
||||
// 不重复 ID 快照表(SPI 0xC3 协议配套)
|
||||
// ============================================================
|
||||
#define CANFD_ID_SNAPSHOT_MAX 16 ///< 每通道最大不重复 ID 条目数
|
||||
#define CANFD_ID_SNAPSHOT_DATA_MAX 64 ///< 快照条目最大数据字节数
|
||||
|
||||
/**
|
||||
* @brief 单个不重复 ID 的最新帧快照条目。
|
||||
*/
|
||||
typedef struct {
|
||||
uint32_t id;
|
||||
uint32_t seq;
|
||||
uint32_t rx_count;
|
||||
uint8_t dlc;
|
||||
uint8_t fd_flag;
|
||||
uint8_t brs;
|
||||
uint8_t esi;
|
||||
uint8_t data[CANFD_ID_SNAPSHOT_DATA_MAX];
|
||||
} id_snapshot_entry_t;
|
||||
|
||||
// ============================================================
|
||||
// CAN 接收过滤模式 & 1Hz 反馈快照
|
||||
// 与 GD32 DCU_APP SPI 协议帧 0xC1/0xC2 配套
|
||||
// ============================================================
|
||||
#define SPI_CAN_FEEDBACK_LAST_DATA_BYTES 93 ///< 反馈快照中最近一帧数据字节数(受 SPI 128B 应答帧约束(kTxBufSize=128 - overhead=5 - 前缀30 = 93))
|
||||
|
||||
typedef enum {
|
||||
CAN_RX_MODE_ALL_PASS = 0, ///< 默认:禁用 ID 过滤,三路全部接收
|
||||
CAN_RX_MODE_FILTER_ID = 1, ///< 按 CONFIG_CAN_RX_FILTER_ID/MASK 做标准帧过滤
|
||||
} can_rx_mode_t;
|
||||
|
||||
/**
|
||||
* @brief 单通道 1Hz 反馈统计。
|
||||
*/
|
||||
typedef struct {
|
||||
uint8_t channel;
|
||||
uint8_t mode;
|
||||
uint32_t snapshot_seq;
|
||||
uint32_t rx_count;
|
||||
uint32_t tx_count;
|
||||
uint32_t rx_drop_count;
|
||||
uint32_t error_count;
|
||||
} can_channel_feedback_t;
|
||||
|
||||
/**
|
||||
* @brief 单通道 0xC1 完整响应:统计 + ID 快照表。
|
||||
*/
|
||||
typedef struct {
|
||||
can_channel_feedback_t stats;
|
||||
id_snapshot_entry_t entries[CANFD_ID_SNAPSHOT_MAX];
|
||||
uint8_t total_count; /**< 该通道实际 ID 条目总数 */
|
||||
uint8_t valid; /**< 数据是否有效 */
|
||||
} can_feedback_result_t;
|
||||
|
||||
/**
|
||||
* @brief 拉取一路 CAN 通道的 1Hz 反馈快照。
|
||||
* @param channel 目标通道 0/1/2。
|
||||
* @param out 输出快照(不可为 nullptr)。
|
||||
* @param timeout_ms SPI 事务超时。
|
||||
* @return 0 成功,<0 失败(ERROR 应答、payload 长度不足或 SPI 错误)。
|
||||
*/
|
||||
int canFdPollFeedback(can_channel_t channel,
|
||||
can_feedback_result_t *out,
|
||||
uint32_t timeout_ms);
|
||||
|
||||
/**
|
||||
* @brief 切换 GD32 三路 CAN 的接收过滤模式。
|
||||
* @param mode 目标模式(0=全收,1=按 ID 过滤)。
|
||||
* @param actual 可选输出,GD32 实际生效模式。
|
||||
* @param timeout_ms SPI 事务超时。
|
||||
* @return 0 成功,<0 失败。
|
||||
*/
|
||||
int canFdSetRxMode(can_rx_mode_t mode,
|
||||
can_rx_mode_t *actual,
|
||||
uint32_t timeout_ms);
|
||||
|
||||
/**
|
||||
* @brief 拉取指定通道的不重复 ID 快照表(SPI 帧 0xC3,分页)。
|
||||
* @param channel 目标通道 0/1/2。
|
||||
* @param out 输出条目数组(至少 CANFD_ID_SNAPSHOT_MAX 个元素)。
|
||||
* @param out_count 输出实际条目数。
|
||||
* @param timeout_ms SPI 超时。
|
||||
* @return 0 成功,<0 失败。
|
||||
*/
|
||||
int canFdPullIdSnapshot(can_channel_t channel,
|
||||
id_snapshot_entry_t *out,
|
||||
uint8_t *out_count,
|
||||
uint32_t timeout_ms);
|
||||
#endif // CANFD_BRIDGE_H
|
||||
|
||||
@@ -452,7 +452,7 @@ esp_err_t SpiMaster::otaPollResponse(uint8_t *resp_type, uint8_t *resp_payload,
|
||||
esp_err_t SpiMaster::otaPollResponseUnlocked(uint8_t *resp_type, uint8_t *resp_payload,
|
||||
size_t resp_max, size_t *resp_len, uint32_t timeout_ms)
|
||||
{
|
||||
constexpr size_t kPollBytes = 96U;
|
||||
constexpr size_t kPollBytes = 160U;
|
||||
TickType_t deadline = xTaskGetTickCount() + pdMS_TO_TICKS(timeout_ms);
|
||||
|
||||
while (xTaskGetTickCount() <= deadline) {
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
#define SPI_CLOCK_HZ (4 * 1000 * 1000) // 4MHz
|
||||
#define SPI_MODE (0) // CPOL=0, CPHA=0
|
||||
#define SPI_QUEUE_SIZE (1) ///< SPI 事务队列深度
|
||||
#define SPI_OTA_FRAME_MAX (96) ///< SPI OTA 单帧最大字节数
|
||||
#define SPI_OTA_FRAME_MAX (133) ///< SPI OTA 单帧最大字节数
|
||||
|
||||
// GD32 SPI OTA 帧类型
|
||||
#define SPI_OTA_SOF (0xA5)
|
||||
|
||||
Reference in New Issue
Block a user