2026-07-26 20:39:59 +08:00
|
|
|
|
/**
|
|
|
|
|
|
* @file motor_ota_relay.cpp
|
|
|
|
|
|
* @brief 电机 OTA relay 状态机实现。
|
|
|
|
|
|
*/
|
|
|
|
|
|
#include "sys/motor_ota_relay.h"
|
|
|
|
|
|
#include "sys/aes128.h"
|
|
|
|
|
|
#include "sys/config.h"
|
|
|
|
|
|
#include "sys/systick.h"
|
|
|
|
|
|
#include "sys/task_manager.h"
|
|
|
|
|
|
#include "main.h"
|
|
|
|
|
|
#include "device/canfd.h"
|
|
|
|
|
|
#include "device/spi/app_spi_command.h"
|
|
|
|
|
|
#include "device/spi/app_spi_command_defs.h"
|
|
|
|
|
|
#include <cstring>
|
|
|
|
|
|
#include <cstdio>
|
|
|
|
|
|
|
|
|
|
|
|
namespace MotorOtaRelay {
|
|
|
|
|
|
|
|
|
|
|
|
/* ═══════════════════ 内部常量 ═══════════════════ */
|
|
|
|
|
|
|
|
|
|
|
|
/* 与电机 BOOT 工程一致的 K_master(预置主密钥) */
|
|
|
|
|
|
static const uint8_t K_MASTER[16] = {
|
|
|
|
|
|
0x01,0x23,0x45,0x67,0x89,0xAB,0xCD,0xEF,
|
|
|
|
|
|
0xFE,0xDC,0xBA,0x98,0x76,0x54,0x32,0x10
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
/* 固件环形缓冲:ESP32 喂入明文,状态机消耗加密后发 CAN。
|
|
|
|
|
|
* 2KB≈42 包:CAN 5ms/包时约 210ms 余量;ESP32 SPI 欠载时会出现 ~500ms CAN 空洞。
|
|
|
|
|
|
* 提到 4KB≈85 包(~420ms)给 WiFi/HTTP 抖动更大缓冲。 */
|
|
|
|
|
|
static constexpr uint16_t kBufSize = 4096U;
|
|
|
|
|
|
|
|
|
|
|
|
/* ═══════════════════ 内部状态 ═══════════════════ */
|
|
|
|
|
|
|
|
|
|
|
|
static State s_state = State::kIdle;
|
|
|
|
|
|
static uint8_t s_error = 0;
|
|
|
|
|
|
static uint8_t s_channel = 0;
|
|
|
|
|
|
static uint32_t s_fw_size = 0;
|
|
|
|
|
|
static uint32_t s_fw_fed = 0; /* 已接受的固件字节数(绝对流位置,防 0xD1 重传重复追加) */
|
|
|
|
|
|
static uint16_t s_total_packets = 0;
|
|
|
|
|
|
static uint16_t s_packets_acked = 0;
|
|
|
|
|
|
static uint32_t s_session_num = 0;
|
|
|
|
|
|
static bool s_boot_heartbeat_seen = false; /* kPreCancel 期间是否收到 0x103 心跳 */
|
2026-08-01 04:15:10 +08:00
|
|
|
|
static uint32_t s_last_feed_ms = 0; /* 上次 feedData 成功接受新数据的时刻(SPI 断流检测) */
|
|
|
|
|
|
static uint8_t s_auto_retry = 0; /* 传输中途自动重试计数(SPI 断流 / 电机会话丢失) */
|
|
|
|
|
|
static uint16_t s_last_acked_pkt = 0xFFFFU; /* 上次确认的包号,用于识别重复 ACK */
|
|
|
|
|
|
static uint8_t s_crc_retry_count = 0; /* 当前包 CRC 重试计数 */
|
2026-07-26 20:39:59 +08:00
|
|
|
|
|
|
|
|
|
|
/* AES */
|
|
|
|
|
|
static aes128_context s_aes_ctx;
|
|
|
|
|
|
static uint8_t s_k_session[16];
|
|
|
|
|
|
static uint8_t s_nonce[16];
|
|
|
|
|
|
|
|
|
|
|
|
/* ACK 捕获(由 onCanFrame 在 taskCanRxParse 上下文写入) */
|
|
|
|
|
|
static volatile bool s_ack_pending = false;
|
|
|
|
|
|
static uint8_t s_ack_data[64];
|
|
|
|
|
|
static uint8_t s_ack_len = 0;
|
|
|
|
|
|
|
|
|
|
|
|
/* 超时 / 发送节奏 */
|
|
|
|
|
|
static uint32_t s_state_enter_ms = 0;
|
|
|
|
|
|
/* 上次发送时刻;0 = 当前步骤尚未首发(enterState 置 0,process() 负责首发)。 */
|
|
|
|
|
|
static uint32_t s_last_send_ms = 0;
|
|
|
|
|
|
/* 当前步骤已补发次数(进入新步骤 / 弹出新数据包时清零)。 */
|
|
|
|
|
|
static uint8_t s_retry_count = 0;
|
|
|
|
|
|
/* CRC 错误后待重发标志:置位后由 onTxTick() 按 kTransferGapMs 间隔重发当前包(不立即重发)。 */
|
|
|
|
|
|
static bool s_crc_resend_pending = false;
|
|
|
|
|
|
/* 数据发送节奏完全由主循环 onTxTick() + SysTick 钟表(s_last_send_ms)驱动,
|
|
|
|
|
|
* 不再用跨 ISR/主循环的计数器(消除 s_gap_div_100us 竞争)。 */
|
|
|
|
|
|
|
|
|
|
|
|
/* 固件环形缓冲 */
|
|
|
|
|
|
static uint8_t s_buf[kBufSize];
|
|
|
|
|
|
static volatile uint16_t s_buf_head = 0; /* 写入位置(ESP32 feed) */
|
|
|
|
|
|
static volatile uint16_t s_buf_tail = 0; /* 读取位置(状态机消耗) */
|
|
|
|
|
|
|
|
|
|
|
|
/* 当前待发包的明文暂存 + "当前包是否已从缓冲弹出"标志。
|
|
|
|
|
|
* CRC 错误时 BOOT 要求重发同一包,不能再 bufPop 新数据。
|
|
|
|
|
|
* s_pkt_pending=false 表示当前没有待发包数据(需弹新包);
|
|
|
|
|
|
* s_pkt_pending=true 表示 s_pkt_plain 已含待发数据,可重发。 */
|
|
|
|
|
|
static uint8_t s_pkt_plain[kPacketPayload];
|
|
|
|
|
|
static bool s_pkt_pending = false;
|
|
|
|
|
|
/* 末包 padding 诊断:sendNextPacket 弹末包时记录有效字节数(0xFFFF=非末包/已打印),
|
|
|
|
|
|
* 供 sendCurrentPacket 在 TX 日志前一次性打印,避免 padding 日志被紧接的 TX 行吞掉。 */
|
|
|
|
|
|
static uint16_t s_last_pad_valid = 0xFFFFU;
|
|
|
|
|
|
|
|
|
|
|
|
/* ═══════════════════ 工具函数 ═══════════════════ */
|
|
|
|
|
|
|
|
|
|
|
|
static inline uint16_t bufUsed() {
|
|
|
|
|
|
return static_cast<uint16_t>((s_buf_head - s_buf_tail + kBufSize) % kBufSize);
|
|
|
|
|
|
}
|
|
|
|
|
|
static inline uint16_t bufFree() {
|
|
|
|
|
|
return static_cast<uint16_t>(kBufSize - 1U - bufUsed());
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
static void bufPush(const uint8_t *data, uint16_t len) {
|
|
|
|
|
|
for (uint16_t i = 0; i < len; i++) {
|
|
|
|
|
|
s_buf[s_buf_head] = data[i];
|
|
|
|
|
|
s_buf_head = static_cast<uint16_t>((s_buf_head + 1U) % kBufSize);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
static bool bufPop(uint8_t *out, uint16_t len) {
|
|
|
|
|
|
if (bufUsed() < len) return false;
|
|
|
|
|
|
for (uint16_t i = 0; i < len; i++) {
|
|
|
|
|
|
out[i] = s_buf[s_buf_tail];
|
|
|
|
|
|
s_buf_tail = static_cast<uint16_t>((s_buf_tail + 1U) % kBufSize);
|
|
|
|
|
|
}
|
|
|
|
|
|
return true;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/* 简单伪随机 nonce(SysTick + 未初始化 ADC 噪声足够区分会话) */
|
|
|
|
|
|
static void generateNonce(uint8_t *out) {
|
|
|
|
|
|
uint32_t seed = SysTickTimer::getMs();
|
|
|
|
|
|
for (uint8_t i = 0; i < 16; i++) {
|
|
|
|
|
|
seed = seed * 1103515245U + 12345U;
|
|
|
|
|
|
out[i] = static_cast<uint8_t>((seed >> 16) & 0xFF);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/* keyed-CRC32:与电机 BOOT calculate_crc32_with_key 查表实现完全一致。
|
|
|
|
|
|
* ★ 必须用查表法:BOOT 侧 (crc ^ key[i]) & 0xFF(8位 XOR 再查表),
|
|
|
|
|
|
* bit-by-bit 用 crc ^= key[i](32位 XOR),两者不等价,会导致每包 CRC 校验失败。 */
|
|
|
|
|
|
/* CRC32 查找表(标准多项式 0xEDB88320)。 */
|
|
|
|
|
|
static const uint32_t kCrcTable[256] = {
|
|
|
|
|
|
0x00000000, 0x77073096, 0xEE0E612C, 0x990951BA, 0x076DC419, 0x706AF48F,
|
|
|
|
|
|
0xE963A535, 0x9E6495A3, 0x0EDB8832, 0x79DCB8A4, 0xE0D5E91E, 0x97D2D988,
|
|
|
|
|
|
0x09B64C2B, 0x7EB17CBD, 0xE7B82D07, 0x90BF1D91, 0x1DB71064, 0x6AB020F2,
|
|
|
|
|
|
0xF3B97148, 0x84BE41DE, 0x1ADAD47D, 0x6DDDE4EB, 0xF4D4B551, 0x83D385C7,
|
|
|
|
|
|
0x136C9856, 0x646BA8C0, 0xFD62F97A, 0x8A65C9EC, 0x14015C4F, 0x63066CD9,
|
|
|
|
|
|
0xFA0F3D63, 0x8D080DF5, 0x3B6E20C8, 0x4C69105E, 0xD56041E4, 0xA2677172,
|
|
|
|
|
|
0x3C03E4D1, 0x4B04D447, 0xD20D85FD, 0xA50AB56B, 0x35B5A8FA, 0x42B2986C,
|
|
|
|
|
|
0xDBBBC9D6, 0xACBCF940, 0x32D86CE3, 0x45DF5C75, 0xDCD60DCF, 0xABD13D59,
|
|
|
|
|
|
0x26D930AC, 0x51DE003A, 0xC8D75180, 0xBFD06116, 0x21B4F4B5, 0x56B3C423,
|
|
|
|
|
|
0xCFBA9599, 0xB8BDA50F, 0x2802B89E, 0x5F058808, 0xC60CD9B2, 0xB10BE924,
|
|
|
|
|
|
0x2F6F7C87, 0x58684C11, 0xC1611DAB, 0xB6662D3D, 0x76DC4190, 0x01DB7106,
|
|
|
|
|
|
0x98D220BC, 0xEFD5102A, 0x71B18589, 0x06B6B51F, 0x9FBFE4A5, 0xE8B8D433,
|
|
|
|
|
|
0x7807C9A2, 0x0F00F934, 0x9609A88E, 0xE10E9818, 0x7F6A0DBB, 0x086D3D2D,
|
|
|
|
|
|
0x91646C97, 0xE6635C01, 0x6B6B51F4, 0x1C6C6162, 0x856530D8, 0xF262004E,
|
|
|
|
|
|
0x6C0695ED, 0x1B01A57B, 0x8208F4C1, 0xF50FC457, 0x65B0D9C6, 0x12B7E950,
|
|
|
|
|
|
0x8BBEB8EA, 0xFCB9887C, 0x62DD1DDF, 0x15DA2D49, 0x8CD37CF3, 0xFBD44C65,
|
|
|
|
|
|
0x4DB26158, 0x3AB551CE, 0xA3BC0074, 0xD4BB30E2, 0x4ADFA541, 0x3DD895D7,
|
|
|
|
|
|
0xA4D1C46D, 0xD3D6F4FB, 0x4369E96A, 0x346ED9FC, 0xAD678846, 0xDA60B8D0,
|
|
|
|
|
|
0x44042D73, 0x33031DE5, 0xAA0A4C5F, 0xDD0D7CC9, 0x5005713C, 0x270241AA,
|
|
|
|
|
|
0xBE0B1010, 0xC90C2086, 0x5768B525, 0x206F85B3, 0xB966D409, 0xCE61E49F,
|
|
|
|
|
|
0x5EDEF90E, 0x29D9C998, 0xB0D09822, 0xC7D7A8B4, 0x59B33D17, 0x2EB40D81,
|
|
|
|
|
|
0xB7BD5C3B, 0xC0BA6CAD, 0xEDB88320, 0x9ABFB3B6, 0x03B6E20C, 0x74B1D29A,
|
|
|
|
|
|
0xEAD54739, 0x9DD277AF, 0x04DB2615, 0x73DC1683, 0xE3630B12, 0x94643B84,
|
|
|
|
|
|
0x0D6D6A3E, 0x7A6A5AA8, 0xE40ECF0B, 0x9309FF9D, 0x0A00AE27, 0x7D079EB1,
|
|
|
|
|
|
0xF00F9344, 0x8708A3D2, 0x1E01F268, 0x6906C2FE, 0xF762575D, 0x806567CB,
|
|
|
|
|
|
0x196C3671, 0x6E6B06E7, 0xFED41B76, 0x89D32BE0, 0x10DA7A5A, 0x67DD4ACC,
|
|
|
|
|
|
0xF9B9DF6F, 0x8EBEEFF9, 0x17B7BE43, 0x60B08ED5, 0xD6D6A3E8, 0xA1D1937E,
|
|
|
|
|
|
0x38D8C2C4, 0x4FDFF252, 0xD1BB67F1, 0xA6BC5767, 0x3FB506DD, 0x48B2364B,
|
|
|
|
|
|
0xD80D2BDA, 0xAF0A1B4C, 0x36034AF6, 0x41047A60, 0xDF60EFC3, 0xA867DF55,
|
|
|
|
|
|
0x316E8EEF, 0x4669BE79, 0xCB61B38C, 0xBC66831A, 0x256FD2A0, 0x5268E236,
|
|
|
|
|
|
0xCC0C7795, 0xBB0B4703, 0x220216B9, 0x5505262F, 0xC5BA3BBE, 0xB2BD0B28,
|
|
|
|
|
|
0x2BB45A92, 0x5CB36A04, 0xC2D7FFA7, 0xB5D0CF31, 0x2CD99E8B, 0x5BDEAE1D,
|
|
|
|
|
|
0x9B64C2B0, 0xEC63F226, 0x756AA39C, 0x026D930A, 0x9C0906A9, 0xEB0E363F,
|
|
|
|
|
|
0x72076785, 0x05005713, 0x95BF4A82, 0xE2B87A14, 0x7BB12BAE, 0x0CB61B38,
|
|
|
|
|
|
0x92D28E9B, 0xE5D5BE0D, 0x7CDCEFB7, 0x0BDBDF21, 0x86D3D2D4, 0xF1D4E242,
|
|
|
|
|
|
0x68DDB3F8, 0x1FDA836E, 0x81BE16CD, 0xF6B9265B, 0x6FB077E1, 0x18B74777,
|
|
|
|
|
|
0x88085AE6, 0xFF0F6A70, 0x66063BCA, 0x11010B5C, 0x8F659EFF, 0xF862AE69,
|
|
|
|
|
|
0x616BFFD3, 0x166CCF45, 0xA00AE278, 0xD70DD2EE, 0x4E048354, 0x3903B3C2,
|
|
|
|
|
|
0xA7672661, 0xD06016F7, 0x4969474D, 0x3E6E77DB, 0xAED16A4A, 0xD9D65ADC,
|
|
|
|
|
|
0x40DF0B66, 0x37D83BF0, 0xA9BCAE53, 0xDEBB9EC5, 0x47B2CF7F, 0x30B5FFE9,
|
|
|
|
|
|
0xBDBDF21C, 0xCABAC28A, 0x53B39330, 0x24B4A3A6, 0xBAD03605, 0xCDD70693,
|
|
|
|
|
|
0x54DE5729, 0x23D967BF, 0xB3667A2E, 0xC4614AB8, 0x5D681B02, 0x2A6F2B94,
|
|
|
|
|
|
0xB40BBE37, 0xC30C8EA1, 0x5A05DF1B, 0x2D02EF8D
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
static void keyedCrc32(const uint8_t *key, const uint8_t *data, uint8_t *output) {
|
|
|
|
|
|
uint32_t crc = 0xFFFFFFFFU;
|
|
|
|
|
|
|
|
|
|
|
|
/* 16 字节会话密钥先喂入(与 BOOT 完全一致:查表法) */
|
|
|
|
|
|
for (int i = 0; i < 16; ++i) {
|
|
|
|
|
|
crc = kCrcTable[(crc ^ key[i]) & 0xFFU] ^ (crc >> 8);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/* 48 字节有效数据再喂入 */
|
|
|
|
|
|
for (int i = 0; i < (int)kPacketPayload; ++i) {
|
|
|
|
|
|
crc = kCrcTable[(crc ^ data[i]) & 0xFFU] ^ (crc >> 8);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/* 取反得到最终结果 */
|
|
|
|
|
|
crc = ~crc;
|
|
|
|
|
|
|
|
|
|
|
|
/* 小端格式输出 */
|
|
|
|
|
|
output[0] = static_cast<uint8_t>(crc & 0xFFU);
|
|
|
|
|
|
output[1] = static_cast<uint8_t>((crc >> 8) & 0xFFU);
|
|
|
|
|
|
output[2] = static_cast<uint8_t>((crc >> 16) & 0xFFU);
|
|
|
|
|
|
output[3] = static_cast<uint8_t>((crc >> 24) & 0xFFU);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/* ═══════════════════ CAN 发送辅助 ═══════════════════ */
|
|
|
|
|
|
|
|
|
|
|
|
/* OTA 帧发送:periodic 控制是否允许硬件自动重传。
|
|
|
|
|
|
* 所有帧统一用 periodic=false:发一次即止,不靠 CAN 硬件重传。
|
|
|
|
|
|
* - 握手/控制帧:重传由 relay 软件 process() 按 kStepTimeoutMs 超时补发。
|
|
|
|
|
|
* - 数据帧:靠 relay 软件 stop-and-wait(onTxTick)补发。
|
|
|
|
|
|
* 否则电机跳 BOOT 期间 CAN 短暂断开时硬件持续重传,恢复后重复帧到达 BOOT
|
|
|
|
|
|
* 被判为非法握手 → INVALID_HANDSHAKE(0x05),或数据帧重复 → CRC 错误(0x06)。 */
|
|
|
|
|
|
static bool canSend(uint32_t id, const uint8_t *data, uint8_t dlc, bool periodic = false) {
|
|
|
|
|
|
/* 诊断:kTransfer 期间只应发 0x101,若出现 0x100 则为幽灵帧 */
|
|
|
|
|
|
if (s_state == State::kTransfer && id == kOtaReqId) {
|
2026-07-27 01:49:18 +08:00
|
|
|
|
LOG_ERR("[MOTOR-OTA] BUG! canSend(0x100) in kTransfer! dlc=%u\r\n", (unsigned)dlc);
|
2026-07-26 20:39:59 +08:00
|
|
|
|
}
|
|
|
|
|
|
const CanFd::CanTxResult r = CanFd::transmitFrame(s_channel, id, data, dlc, periodic);
|
|
|
|
|
|
return (r == CanFd::CanTxResult::kOk);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
static void enterState(State st) {
|
|
|
|
|
|
const State prev = s_state;
|
|
|
|
|
|
s_state = st;
|
|
|
|
|
|
s_state_enter_ms = SysTickTimer::getMs();
|
|
|
|
|
|
/* 进入新步骤:清补发计数;s_last_send_ms=0 令 process() 先“首发”再进入等待/补发节奏。 */
|
|
|
|
|
|
s_retry_count = 0;
|
|
|
|
|
|
s_last_send_ms = 0;
|
|
|
|
|
|
s_crc_resend_pending = false;
|
|
|
|
|
|
|
|
|
|
|
|
/* auto_retrans 切换:OTA 是应用层停等协议,硬件自动重发会打穿电机单缓冲 RX
|
|
|
|
|
|
* (重发的前一帧被当成新帧处理 → receive_packet_num 错位 → CRC 错)。
|
|
|
|
|
|
* 进入 kHandshakeReq(start() 唯一入口)即关 ARD 切单发;
|
|
|
|
|
|
* 回到 kIdle/kFinished/kError 即恢复 ARD,避免异常路径漏恢复导致全局卡在单发。
|
|
|
|
|
|
* 注:setChannelAutoRetrans 内部按 init-mode 改 ARD 位,开销在几十 µs 量级。 */
|
|
|
|
|
|
const bool otaActive = (st == State::kPreCancel || st == State::kHandshakeReq
|
|
|
|
|
|
|| st == State::kHandshakeNonce || st == State::kHandshakeConfirm
|
|
|
|
|
|
|| st == State::kErase || st == State::kTransfer
|
|
|
|
|
|
|| st == State::kFinishOta);
|
|
|
|
|
|
const bool wasActive = (prev == State::kPreCancel || prev == State::kHandshakeReq
|
|
|
|
|
|
|| prev == State::kHandshakeNonce || prev == State::kHandshakeConfirm
|
|
|
|
|
|
|| prev == State::kErase || prev == State::kTransfer
|
|
|
|
|
|
|| prev == State::kFinishOta);
|
|
|
|
|
|
if (otaActive && !wasActive) {
|
|
|
|
|
|
CanFd::setChannelAutoRetrans(s_channel, false); /* OTA 开始:单发 */
|
|
|
|
|
|
} else if (!otaActive && wasActive) {
|
|
|
|
|
|
CanFd::setChannelAutoRetrans(s_channel, true); /* OTA 结束:恢复重发 */
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
static bool stateTimedOut(uint32_t timeout_ms) {
|
|
|
|
|
|
return (SysTickTimer::getMs() - s_state_enter_ms) >= timeout_ms;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
static void pushStatus(); /* 前置声明:setError 里要用,定义在下方 */
|
|
|
|
|
|
|
|
|
|
|
|
static void setError(uint8_t code) {
|
|
|
|
|
|
s_error = code;
|
|
|
|
|
|
enterState(State::kError);
|
|
|
|
|
|
/* 任何失败都立即把 kError 推给 ESP32:否则 ESP32 feedTask 看不到错误态,
|
|
|
|
|
|
* 会一直喂到 120s 超时才退出。"失败了就报错退出"要求即时上报。 */
|
|
|
|
|
|
pushStatus();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/* 进度推送给 ESP32(0xD2 ring push)。
|
|
|
|
|
|
* 传输期 ESP32 suppress ring:push 只会占 PB6/m_txBuf 50ms,干扰 0xD1 喂数。
|
|
|
|
|
|
* kTransfer 内禁止 push;终态/错误/握手阶段仍推。 */
|
|
|
|
|
|
static void pushStatus() {
|
|
|
|
|
|
if (s_state == State::kTransfer) {
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
uint8_t payload[12];
|
|
|
|
|
|
payload[0] = static_cast<uint8_t>(s_state);
|
|
|
|
|
|
payload[1] = s_error;
|
|
|
|
|
|
payload[2] = static_cast<uint8_t>(s_packets_acked & 0xFF);
|
|
|
|
|
|
payload[3] = static_cast<uint8_t>(s_packets_acked >> 8);
|
|
|
|
|
|
payload[4] = static_cast<uint8_t>(s_total_packets & 0xFF);
|
|
|
|
|
|
payload[5] = static_cast<uint8_t>(s_total_packets >> 8);
|
|
|
|
|
|
payload[6] = static_cast<uint8_t>(s_fw_size & 0xFF);
|
|
|
|
|
|
payload[7] = static_cast<uint8_t>((s_fw_size >> 8) & 0xFF);
|
|
|
|
|
|
payload[8] = static_cast<uint8_t>((s_fw_size >> 16) & 0xFF);
|
|
|
|
|
|
payload[9] = static_cast<uint8_t>((s_fw_size >> 24) & 0xFF);
|
|
|
|
|
|
payload[10] = static_cast<uint8_t>(bufUsed() & 0xFF);
|
|
|
|
|
|
payload[11] = static_cast<uint8_t>(bufUsed() >> 8);
|
|
|
|
|
|
AppSpiCommandBus.requestEsp32Read(SpiDefs::kTypeMotorOtaStatus, payload, 12);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-08-01 04:15:10 +08:00
|
|
|
|
/* 传输中途自动重试:电机会话已死(超时退 IDLE)或 SPI 断流,但仍有重试预算。
|
|
|
|
|
|
* 流程:CANCEL 电机 → 清环缓 → 重置喂数/包计数 → 回 kPreCancel 重新握手。
|
|
|
|
|
|
* error_code 暂置 0xF0 推给 ESP32:含义="relay 正在重试,请从 offset 0 重新喂数"。
|
|
|
|
|
|
* ESP32 feedTask 看到 state 回退到 kPreCancel + err=0xF0 → 重置 s_fw_offset=0。 */
|
|
|
|
|
|
static void tryAutoRetry(const char *reason) {
|
|
|
|
|
|
s_auto_retry++;
|
|
|
|
|
|
LOG_ERR("[MOTOR-OTA] AUTO-RETRY #%u (%s) pkt=%u/%u fed=%lu/%lu\r\n",
|
|
|
|
|
|
(unsigned)s_auto_retry, reason,
|
|
|
|
|
|
(unsigned)s_packets_acked, (unsigned)s_total_packets,
|
|
|
|
|
|
(unsigned long)s_fw_fed, (unsigned long)s_fw_size);
|
|
|
|
|
|
|
|
|
|
|
|
/* 通知电机 BOOT 中止当前会话(若已退 IDLE 则 CANCEL 无副作用) */
|
|
|
|
|
|
canSend(kOtaCancelId, nullptr, 0);
|
|
|
|
|
|
|
|
|
|
|
|
/* 清环缓 + 重置喂数位置:电机重新擦除后需要完整固件,旧缓冲数据已无效 */
|
|
|
|
|
|
s_buf_head = 0;
|
|
|
|
|
|
s_buf_tail = 0;
|
|
|
|
|
|
s_fw_fed = 0U;
|
|
|
|
|
|
s_packets_acked = 0;
|
|
|
|
|
|
s_pkt_pending = false;
|
|
|
|
|
|
s_crc_resend_pending = false;
|
|
|
|
|
|
s_crc_retry_count = 0;
|
|
|
|
|
|
s_last_acked_pkt = 0xFFFFU;
|
|
|
|
|
|
s_ack_pending = false;
|
|
|
|
|
|
s_last_pad_valid = 0xFFFFU;
|
|
|
|
|
|
s_last_feed_ms = SysTickTimer::getMs(); /* 重试后重新起算 SPI 超时 */
|
|
|
|
|
|
|
|
|
|
|
|
/* 回 kPreCancel 等电机 0x103 心跳 → 重新握手 */
|
|
|
|
|
|
s_boot_heartbeat_seen = false;
|
|
|
|
|
|
s_error = 0xF0U; /* 信号:非致命,正在重试 */
|
|
|
|
|
|
enterState(State::kPreCancel);
|
|
|
|
|
|
pushStatus();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-07-26 20:39:59 +08:00
|
|
|
|
/* ═══════════════════ 握手子步骤 ═══════════════════ */
|
|
|
|
|
|
|
|
|
|
|
|
/* 单步等对应反馈的超时:到点仍无反馈就补发。100ms 加快握手节奏(电机 BOOT 主循环~10ms 足够响应)。 */
|
|
|
|
|
|
static constexpr uint32_t kStepTimeoutMs = 100U;
|
|
|
|
|
|
/* 补发次数上限:首发后最多再补发 kMaxResends 次;仍无反馈则判该步失败并打印。 */
|
|
|
|
|
|
static constexpr uint8_t kMaxResends = 2U;
|
2026-07-27 00:02:14 +08:00
|
|
|
|
/* 数据阶段 ACK 超时重发间隔(ms):正常 ACK 往返 ~11ms(电机 BOOT 主循环 ~10ms),
|
|
|
|
|
|
* 100ms 留 10 倍余量且避开电机偶发 flash 写慢。原 500ms 硬编码:11 次丢帧盲等
|
|
|
|
|
|
* 共 ~5.5s,占整场传输过半时间。电机对同包号幂等(只 ACK 不重复写 flash),
|
|
|
|
|
|
* 早发导致的双 ACK 由 processTransferAck 的重复 ACK 过滤兜底。 */
|
|
|
|
|
|
static constexpr uint32_t kAckResendMs = 100U;
|
|
|
|
|
|
/* 数据阶段每包补发上限:随 kAckResendMs 500→100 同步 2→4,保持总容错时间窗;
|
|
|
|
|
|
* 握手阶段仍用 kMaxResends,不动 v1.4.51 抗干扰逻辑。超限后由 process() 整体超时兜底。 */
|
|
|
|
|
|
static constexpr uint8_t kAckMaxResends = 4U;
|
2026-07-26 20:39:59 +08:00
|
|
|
|
/* 数据传输每包间隔(ms):正式发固件时每包也留间隔,防电机单缓冲 RX 高速下丢包/CRC 错。
|
|
|
|
|
|
* 现行:5ms/包(用户指定)。SPI 喂数必须能跟上;跟不上时靠水位门控暂停 CAN。 */
|
|
|
|
|
|
static constexpr uint32_t kTransferGapMs = 5U;
|
|
|
|
|
|
/* 开传前环缓最低水位:至少约 1KB(~21 包 / ~100ms)再发首包,
|
|
|
|
|
|
* 避免擦除后立刻按 5ms 抽干仅有的 232B 首 chunk 导致电机超时。 */
|
|
|
|
|
|
static constexpr uint16_t kStartWaterBytes = 1024U;
|
|
|
|
|
|
/* 运行中低水位:低于此值暂停发新包,等 ESP32 回填(已在途包仍等 ACK)。 */
|
|
|
|
|
|
static constexpr uint16_t kRunWaterBytes = 256U;
|
|
|
|
|
|
/* 擦除后首包等待(ms):电机发 erase ACK = 擦除已完成,
|
|
|
|
|
|
* 100ms 仅为电机内部状态转换留余量(BOOT 主循环~10ms)。
|
|
|
|
|
|
* 之前 500ms 导致电机 RECEIVING_FIRMWARE 超时(~2s)预算被压缩。 */
|
|
|
|
|
|
static constexpr uint32_t kEraseWaitMs = 100U;
|
2026-07-30 02:11:49 +08:00
|
|
|
|
/* 等新 APP 运行确认(ms):尾包 ACK 后电机复位跳 APP,新 APP 上电初始化完 CAN 会
|
|
|
|
|
|
* 自主发 [0A AC](刷写结束/APP 运行有效,见协议表 15/16 行)。
|
|
|
|
|
|
* 复位+初始化实测 <1s,3s 留足余量;超时 = 新 APP 未启动 → error 0x25。 */
|
|
|
|
|
|
static constexpr uint32_t kAppRunTimeoutMs = 3000U;
|
2026-08-01 04:15:10 +08:00
|
|
|
|
/* SPI 喂数断流超时(ms):ESP32 停止 feedData 超过此时限且固件尚未喂完,
|
|
|
|
|
|
* 判定 SPI 链路故障,主动报错退出,避免环缓耗尽后与电机 0x103 心跳形成死锁。
|
|
|
|
|
|
* 8s 留足 WiFi/HTTP 抖动余量(正常 SPI 喂数间隔 <100ms)。 */
|
|
|
|
|
|
static constexpr uint32_t kSpiFeedTimeoutMs = 8000U;
|
|
|
|
|
|
/* 传输中途 SPI 断流 / 电机会话丢失时的自动重试次数:
|
|
|
|
|
|
* 发 CANCEL 通知电机 → 清环缓 → 重新握手 → 推状态让 ESP32 从 offset 0 重喂。
|
|
|
|
|
|
* 1 次重试覆盖"SPI 短暂抖动 + 电机超时退 IDLE"场景;仍失败则报错由上位机决策。 */
|
|
|
|
|
|
static constexpr uint8_t kMaxAutoRetry = 1U;
|
2026-07-30 02:11:49 +08:00
|
|
|
|
/* 残帧窗口 kBootSettleMs 已移除(2026-07-27):收到 0x103 即直接进握手。
|
|
|
|
|
|
* 旧担忧:电机 APP 跳转前 TX 队列残留 [00 FA] 帧延迟上总线被 BOOT catch-all 命中 → 0x05。
|
|
|
|
|
|
* 现行握手阶段对垃圾帧一律忽略,靠 kStepTimeoutMs=100ms 步进重发(≤2次)自愈,
|
|
|
|
|
|
* 窗口不再必要;若 0x05 复现优先怀疑此处。 */
|
2026-07-26 20:39:59 +08:00
|
|
|
|
|
|
|
|
|
|
/* 步骤名,用于“xxx 未收到”日志,让用户/上位机知道卡在协议哪一步。 */
|
|
|
|
|
|
static const char *stepName(State st) {
|
|
|
|
|
|
switch (st) {
|
|
|
|
|
|
case State::kPreCancel: return "WAIT_BOOT(0x103)";
|
|
|
|
|
|
case State::kHandshakeReq: return "HANDSHAKE_REQ(0x100:00AC)";
|
|
|
|
|
|
case State::kHandshakeNonce: return "HANDSHAKE_NONCE(0x100:16B)";
|
|
|
|
|
|
case State::kHandshakeConfirm: return "HANDSHAKE_CONFIRM(0x100:16B)";
|
|
|
|
|
|
case State::kErase: return "ERASE(0x101:fw_size)";
|
|
|
|
|
|
case State::kTransfer: return "TRANSFER(0x101:data)";
|
|
|
|
|
|
case State::kFinishOta: return "FINISH_OTA(0x100:0AAC)";
|
2026-07-30 02:11:49 +08:00
|
|
|
|
case State::kWaitAppRun: return "WAIT_APP_RUN(0x102:0AAC)";
|
2026-07-26 20:39:59 +08:00
|
|
|
|
default: return "?";
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
static void sendNonce() {
|
|
|
|
|
|
/* 生成 nonce 并切状态;首发由 ACK handler 触发(不走 process 首发路径)。 */
|
|
|
|
|
|
generateNonce(s_nonce);
|
|
|
|
|
|
enterState(State::kHandshakeNonce);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/* 统一重发当前步骤的帧(超时重发 + kHandshakeReq 首发) */
|
|
|
|
|
|
static void sendCurrentStep(uint32_t now_ms) {
|
2026-07-27 01:49:18 +08:00
|
|
|
|
OTA_LOGI("[MOTOR-OTA] TX %s try=%u t=%lu\r\n",
|
2026-07-26 20:39:59 +08:00
|
|
|
|
stepName(s_state), (unsigned)s_retry_count, (unsigned long)now_ms);
|
|
|
|
|
|
switch (s_state) {
|
|
|
|
|
|
case State::kHandshakeReq: {
|
|
|
|
|
|
const uint8_t req[2] = {0x00, 0xAC};
|
|
|
|
|
|
canSend(kOtaReqId, req, 2);
|
|
|
|
|
|
break;
|
|
|
|
|
|
}
|
|
|
|
|
|
case State::kHandshakeNonce:
|
|
|
|
|
|
canSend(kOtaReqId, s_nonce, 16);
|
|
|
|
|
|
break;
|
|
|
|
|
|
case State::kHandshakeConfirm: {
|
|
|
|
|
|
uint8_t plain[16];
|
|
|
|
|
|
for (uint8_t i = 0; i < 16; i++) plain[i] = s_nonce[i] ^ 0x55;
|
|
|
|
|
|
uint8_t confirm[16];
|
|
|
|
|
|
aes128_set_key(&s_aes_ctx, s_k_session);
|
|
|
|
|
|
aes128_encrypt_block(&s_aes_ctx, plain, confirm);
|
|
|
|
|
|
canSend(kOtaReqId, confirm, 16);
|
|
|
|
|
|
break;
|
|
|
|
|
|
}
|
|
|
|
|
|
case State::kErase: {
|
|
|
|
|
|
uint8_t size_le[4];
|
|
|
|
|
|
size_le[0] = static_cast<uint8_t>(s_fw_size & 0xFF);
|
|
|
|
|
|
size_le[1] = static_cast<uint8_t>((s_fw_size >> 8) & 0xFF);
|
|
|
|
|
|
size_le[2] = static_cast<uint8_t>((s_fw_size >> 16) & 0xFF);
|
|
|
|
|
|
size_le[3] = static_cast<uint8_t>((s_fw_size >> 24) & 0xFF);
|
|
|
|
|
|
canSend(kOtaDataId, size_le, 4);
|
|
|
|
|
|
break;
|
|
|
|
|
|
}
|
|
|
|
|
|
case State::kFinishOta: {
|
|
|
|
|
|
const uint8_t finish_cmd[2] = {0x0A, 0xAC};
|
|
|
|
|
|
canSend(kOtaReqId, finish_cmd, 2);
|
|
|
|
|
|
break;
|
|
|
|
|
|
}
|
|
|
|
|
|
default:
|
|
|
|
|
|
break;
|
|
|
|
|
|
}
|
|
|
|
|
|
s_last_send_ms = now_ms;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/* ═══════════════════ 数据传输 ═══════════════════ */
|
|
|
|
|
|
|
|
|
|
|
|
/* 将 s_pkt_plain 组装成 64B CAN 帧并发送。
|
|
|
|
|
|
* 不弹缓冲,只做加密+CRC+组帧+CAN 发送。
|
|
|
|
|
|
* 供 sendNextPacket(弹新包后发)和 CRC 重发(发同一包)共用。 */
|
|
|
|
|
|
static void sendCurrentPacket() {
|
|
|
|
|
|
const uint32_t pkt_num = s_packets_acked;
|
|
|
|
|
|
const bool is_last = (pkt_num + 1U >= s_total_packets);
|
|
|
|
|
|
|
|
|
|
|
|
/* AES-ECB 加密 48B */
|
|
|
|
|
|
uint8_t encrypted[kPacketPayload];
|
|
|
|
|
|
aes128_set_key(&s_aes_ctx, s_k_session);
|
|
|
|
|
|
aes128_ecb_encrypt_48bytes(&s_aes_ctx, s_pkt_plain, encrypted);
|
|
|
|
|
|
|
|
|
|
|
|
/* keyed CRC32 */
|
|
|
|
|
|
uint8_t crc[4];
|
|
|
|
|
|
keyedCrc32(s_k_session, s_pkt_plain, crc);
|
|
|
|
|
|
|
|
|
|
|
|
/* 组装 64B 帧: session(4) + pkt_num(4) + encrypted(48) + IFEND(4) + CRC(4)
|
|
|
|
|
|
* 帧布局对齐 BOOT 工程 OTA.cpp:
|
|
|
|
|
|
* data[0-3] session, [4-7] pkt_num, [8-55] encrypted(48B),
|
|
|
|
|
|
* [56-59] IFEND (extract_u32_from_4bytes(data+56)),
|
|
|
|
|
|
* [60-63] CRC (data+60) */
|
|
|
|
|
|
uint8_t frame[kFrameDlc];
|
|
|
|
|
|
memset(frame, 0, sizeof(frame));
|
|
|
|
|
|
/* session_num LE */
|
|
|
|
|
|
frame[0] = static_cast<uint8_t>(s_session_num & 0xFF);
|
|
|
|
|
|
frame[1] = static_cast<uint8_t>((s_session_num >> 8) & 0xFF);
|
|
|
|
|
|
frame[2] = static_cast<uint8_t>((s_session_num >> 16) & 0xFF);
|
|
|
|
|
|
frame[3] = static_cast<uint8_t>((s_session_num >> 24) & 0xFF);
|
|
|
|
|
|
/* packet_num LE */
|
|
|
|
|
|
frame[4] = static_cast<uint8_t>(pkt_num & 0xFF);
|
|
|
|
|
|
frame[5] = static_cast<uint8_t>((pkt_num >> 8) & 0xFF);
|
|
|
|
|
|
frame[6] = static_cast<uint8_t>((pkt_num >> 16) & 0xFF);
|
|
|
|
|
|
frame[7] = static_cast<uint8_t>((pkt_num >> 24) & 0xFF);
|
|
|
|
|
|
/* encrypted payload */
|
|
|
|
|
|
memcpy(&frame[8], encrypted, kPacketPayload);
|
|
|
|
|
|
/* IFEND @56 (BOOT 在 data+56 读 IFEND) */
|
|
|
|
|
|
if (is_last) {
|
|
|
|
|
|
frame[56] = 0xFF; frame[57] = 0xFF; frame[58] = 0xFF; frame[59] = 0xFF;
|
|
|
|
|
|
}
|
|
|
|
|
|
/* CRC @60 (BOOT 在 data+60 读 CRC) */
|
|
|
|
|
|
memcpy(&frame[60], crc, 4);
|
|
|
|
|
|
|
|
|
|
|
|
s_ack_pending = false;
|
|
|
|
|
|
/* periodic=false:数据帧只发一次,不靠 CAN 硬件自动重传。
|
|
|
|
|
|
* 硬件重传会在 10ms 间隔下持续重发同一包,电机 BOOT 处理期间重复收到
|
|
|
|
|
|
* 同一包 → 时序错乱 → CRC 错误(06)/握手错误(05)。
|
|
|
|
|
|
* 超时重发由 onTxTick()(CRC 错误置 s_crc_resend_pending)负责。 */
|
|
|
|
|
|
/* 末包 padding 日志在 TX 前打印(sendNextPacket 设 s_last_pad_valid)。
|
|
|
|
|
|
* 用最短格式 + 一次性打印(打印后置 0xFFFF 防重发重复打印)。 */
|
|
|
|
|
|
if (s_last_pad_valid != 0xFFFFU) {
|
2026-07-27 01:49:18 +08:00
|
|
|
|
OTA_LOGI("[MOTOR-OTA] PAD v=%u p=%u\r\n",
|
2026-07-26 20:39:59 +08:00
|
|
|
|
(unsigned)s_last_pad_valid,
|
|
|
|
|
|
(unsigned)(kPacketPayload - s_last_pad_valid));
|
|
|
|
|
|
s_last_pad_valid = 0xFFFFU;
|
|
|
|
|
|
}
|
|
|
|
|
|
/* 逐包 TX 日志在 5ms 节奏下会拖主循环;仅末包/每 64 包打一行。 */
|
|
|
|
|
|
if (is_last || ((pkt_num & 0x3FU) == 0U)) {
|
2026-07-27 01:49:18 +08:00
|
|
|
|
OTA_LOGI("[MOTOR-OTA] TX pkt=%lu/%u%s t=%lu\r\n",
|
2026-07-26 20:39:59 +08:00
|
|
|
|
(unsigned long)pkt_num, (unsigned)s_total_packets,
|
|
|
|
|
|
is_last ? " LAST" : "", (unsigned long)SysTickTimer::getMs());
|
|
|
|
|
|
}
|
|
|
|
|
|
canSend(kOtaDataId, frame, kFrameDlc, /*periodic=*/false);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
static void sendNextPacket() {
|
|
|
|
|
|
/* 从缓冲取明文到 s_pkt_plain */
|
|
|
|
|
|
const uint16_t used = bufUsed();
|
|
|
|
|
|
const bool is_last = (s_packets_acked + 1U >= s_total_packets);
|
|
|
|
|
|
/* 末包真实有效字节:fw_size % 48;整除时末包也是满 48B。 */
|
|
|
|
|
|
const uint16_t last_valid = static_cast<uint16_t>(
|
|
|
|
|
|
(s_fw_size % kPacketPayload) == 0U
|
|
|
|
|
|
? kPacketPayload
|
|
|
|
|
|
: (s_fw_size % kPacketPayload));
|
|
|
|
|
|
|
|
|
|
|
|
if (is_last) {
|
|
|
|
|
|
/* ★ 末包无论 used 是否 ≥48,都只取 last_valid 再 0xFF pad。
|
|
|
|
|
|
* 旧逻辑 used≥48 时 bufPop(48) 会把环缓里多喂的重复字节一并发出
|
|
|
|
|
|
* (BLF:末包 padding 变成向量表而非 0xFF)。 */
|
|
|
|
|
|
if (used < last_valid) {
|
|
|
|
|
|
static uint32_t s_last_tail_ms = 0;
|
|
|
|
|
|
const uint32_t now = SysTickTimer::getMs();
|
|
|
|
|
|
if (now - s_last_tail_ms >= 200U) {
|
2026-07-27 01:49:18 +08:00
|
|
|
|
OTA_LOGI("[MOTOR-OTA] tail-wait pkt=%u used=%u need=%u fed=%lu t=%lu\r\n",
|
2026-07-26 20:39:59 +08:00
|
|
|
|
(unsigned)s_packets_acked, (unsigned)used,
|
|
|
|
|
|
(unsigned)last_valid, (unsigned long)s_fw_fed,
|
|
|
|
|
|
(unsigned long)now);
|
|
|
|
|
|
s_last_tail_ms = now;
|
|
|
|
|
|
}
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
for (uint16_t i = 0U; i < last_valid; ++i) {
|
|
|
|
|
|
s_pkt_plain[i] = s_buf[s_buf_tail];
|
|
|
|
|
|
s_buf_tail = static_cast<uint16_t>((s_buf_tail + 1U) % kBufSize);
|
|
|
|
|
|
}
|
|
|
|
|
|
for (uint16_t i = last_valid; i < kPacketPayload; ++i) {
|
|
|
|
|
|
s_pkt_plain[i] = 0xFFU;
|
|
|
|
|
|
}
|
|
|
|
|
|
s_last_pad_valid = last_valid;
|
|
|
|
|
|
} else {
|
|
|
|
|
|
if (used < kPacketPayload) {
|
|
|
|
|
|
static uint32_t s_last_empty_ms = 0;
|
|
|
|
|
|
const uint32_t now = SysTickTimer::getMs();
|
|
|
|
|
|
if (now - s_last_empty_ms >= 200U) {
|
2026-07-27 01:49:18 +08:00
|
|
|
|
OTA_LOGI("[MOTOR-OTA] buf-empty wait-feed pkt=%u used=%u fed=%lu/%lu t=%lu\r\n",
|
2026-07-26 20:39:59 +08:00
|
|
|
|
(unsigned)s_packets_acked, (unsigned)used,
|
|
|
|
|
|
(unsigned long)s_fw_fed, (unsigned long)s_fw_size,
|
|
|
|
|
|
(unsigned long)now);
|
|
|
|
|
|
s_last_empty_ms = now;
|
|
|
|
|
|
}
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
if (!bufPop(s_pkt_plain, kPacketPayload)) {
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
s_last_pad_valid = 0xFFFFU;
|
|
|
|
|
|
}
|
|
|
|
|
|
s_pkt_pending = true;
|
|
|
|
|
|
s_retry_count = 0; /* 新数据包:重置补发计数 */
|
|
|
|
|
|
s_state_enter_ms = SysTickTimer::getMs(); /* 新数据包:重置整体超时 backstop */
|
2026-07-27 00:02:14 +08:00
|
|
|
|
/* 丢弃上一包/握手阶段残留的 TX 错误事件,避免新包刚发就被陈旧标志误触快速重发。 */
|
|
|
|
|
|
(void)CanFd::consumeTxError(s_channel);
|
2026-07-26 20:39:59 +08:00
|
|
|
|
/* 末包 padding 日志在 sendCurrentPacket 的 TX 行之前打印,且用最短格式,
|
|
|
|
|
|
* 避免 10ms 高频 TX 期间被 UART 吞掉(之前放在这里会被紧接的 TX pkt= 行挤掉)。 */
|
|
|
|
|
|
sendCurrentPacket();
|
|
|
|
|
|
s_last_send_ms = SysTickTimer::getMs();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/* BOOT 错误码(与 OTA.h ota_error_t 对齐,仅列出实际处理的) */
|
|
|
|
|
|
static constexpr uint8_t kBootErrHandshake = 0x05; /* INVALID_HANDSHAKE:直接中止(不重握手) */
|
|
|
|
|
|
static constexpr uint8_t kBootErrCrc = 0x06; /* CRC 错误:重发同一包 */
|
|
|
|
|
|
|
|
|
|
|
|
/* CRC 错误重试上限:BOOT 对 CRC 失败的包做 receive_packet_num -= 1 并
|
|
|
|
|
|
* 发 1 字节 error frame(0x06),期望上位机重发同一包。超过此限判定为
|
|
|
|
|
|
* 真实数据损坏,报错中止,避免无限循环。 */
|
|
|
|
|
|
static constexpr uint8_t kMaxCrcRetries = 3;
|
|
|
|
|
|
static uint32_t s_dup_ack_count = 0; /* 累计忽略的重复/未前进 ACK 帧数(诊断重传风暴) */
|
|
|
|
|
|
static uint32_t s_crc_err_count = 0; /* 累计收到的 0x06 CRC 错误帧数(含电机重传) */
|
2026-07-27 00:02:14 +08:00
|
|
|
|
static uint32_t s_ack_resend_total = 0; /* 累计 ACK 超时重发次数(整场不清零,丢帧率基线) */
|
|
|
|
|
|
static uint32_t s_fast_resend_total = 0; /* 累计 TX 错误快速重发次数(MTE 事件驱动,免等超时) */
|
2026-07-26 20:39:59 +08:00
|
|
|
|
|
|
|
|
|
|
static void processTransferAck(const uint8_t *data, uint8_t len) {
|
|
|
|
|
|
/* 擦除等待窗口(首包尚未发出:s_packets_acked==0 且 !s_pkt_pending):电机重传的旧
|
|
|
|
|
|
* [BA C0] / 杂散帧一律忽略,绝不能推进 s_packets_acked——否则 onTxTick 的
|
|
|
|
|
|
* need=(s_packets_acked==0?kEraseWaitMs:kTransferGapMs) 会提前用 30ms 抢跑首包,
|
|
|
|
|
|
* 且首包被错标成非 0 号(实测首包变 pkt=5) → 从一开始就包号/数据错位,每包 0x06。 */
|
|
|
|
|
|
if (s_packets_acked == 0U && !s_pkt_pending) {
|
|
|
|
|
|
s_dup_ack_count++;
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
/* BOOT 错误帧:1 字节 [error_code],发到 0x102 ID。
|
|
|
|
|
|
* CRC 错误(0x06):BOOT 做 receive_packet_num -= 1,期望重发同一包,
|
|
|
|
|
|
* 不改变 ota_state,仍处于 RECEIVING_FIRMWARE。
|
|
|
|
|
|
* 其他错误码:BOOT 已进 IDLE,不可恢复。 */
|
|
|
|
|
|
if (len == 1) {
|
|
|
|
|
|
if (data[0] == kBootErrCrc) {
|
|
|
|
|
|
s_crc_err_count++;
|
|
|
|
|
|
/* 去重:已排定重发时,电机 auto_retrans 重传的重复 0x06 不再计数,
|
|
|
|
|
|
* 避免一次 CRC 错的多份重传帧把重试计数提前耗尽 → 假性 0x23。 */
|
|
|
|
|
|
if (s_crc_resend_pending) {
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
if (s_crc_retry_count < kMaxCrcRetries) {
|
|
|
|
|
|
s_crc_retry_count++;
|
|
|
|
|
|
/* 不立即重发:置标志,由 onTxTick() 按 kTransferGapMs 间隔重发当前包,
|
|
|
|
|
|
* 避免"挨太近→又 CRC 错"死循环。s_pkt_plain 仍含上次明文,s_pkt_pending 保持。 */
|
|
|
|
|
|
s_crc_resend_pending = true;
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
2026-07-27 01:49:18 +08:00
|
|
|
|
LOG_ERR("[MOTOR-OTA] CRC retry exhausted pkt=%u/%u dup=%lu crc=%lu\r\n",
|
2026-07-26 20:39:59 +08:00
|
|
|
|
(unsigned)s_packets_acked, (unsigned)s_total_packets,
|
|
|
|
|
|
(unsigned long)s_dup_ack_count, (unsigned long)s_crc_err_count);
|
|
|
|
|
|
setError(0x23); /* CRC 重试耗尽 */
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
if (data[0] == kBootErrHandshake) {
|
|
|
|
|
|
/* INVALID_HANDSHAKE(0x05):电机 BOOT 已退回 IDLE。按用户要求不再做
|
|
|
|
|
|
* 「发 CANCEL + 从包 0 重握手」的自动重试(会白扔进度、坏总线下反复失败),
|
|
|
|
|
|
* 直接中止,由上位机决定是否整体重刷。 */
|
2026-07-27 01:49:18 +08:00
|
|
|
|
LOG_ERR("[MOTOR-OTA] HANDSHAKE err(0x05) abort pkt=%u/%u\r\n",
|
2026-07-26 20:39:59 +08:00
|
|
|
|
(unsigned)s_packets_acked, (unsigned)s_total_packets);
|
|
|
|
|
|
setError(0x05);
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
/* 其他错误码:BOOT 已中止,relay 也报错 */
|
|
|
|
|
|
setError(0x24); /* BOOT 报致命错误 */
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (len < 6) { setError(0x21); return; }
|
|
|
|
|
|
/* ACK: [0xBA, 0xC0, next_pkt(4B LE)] */
|
|
|
|
|
|
if (data[0] != 0xBA || data[1] != 0xC0) { setError(0x22); return; }
|
|
|
|
|
|
|
|
|
|
|
|
const uint32_t next_pkt = static_cast<uint32_t>(data[2])
|
|
|
|
|
|
| (static_cast<uint32_t>(data[3]) << 8)
|
|
|
|
|
|
| (static_cast<uint32_t>(data[4]) << 16)
|
|
|
|
|
|
| (static_cast<uint32_t>(data[5]) << 24);
|
|
|
|
|
|
|
|
|
|
|
|
/* BOOT 协议:最后一包(IFEND=0xFFFFFFFF)后,BOOT 进 FINISHED 并回
|
|
|
|
|
|
* ACK(0xF0F0F0F0) 表示"全部写入完成,等 FINISH_OTA"。
|
|
|
|
|
|
* 上位机须再发 [0x0A, 0xAC] 到 REQ ID,BOOT 才真正跳转 APP。
|
|
|
|
|
|
* 旧实现误将 0xF0F0F0F0 当普通包号处理,导致 relay 永远等不到 0xFFFFFFFF,
|
|
|
|
|
|
* 既不进 kFinished 也不发 FINISH_OTA,BOOT 卡在 FINISHED 状态不跳转。 */
|
|
|
|
|
|
/* BOOT 协议(已对照电机 BOOT OTA.cpp line 430-436 确认):
|
|
|
|
|
|
* 最后一包(IFEND=0xFFFFFFFF)后,BOOT 发 ACK(0xFFFFFFFF) 并立即 jumpToApplication()
|
|
|
|
|
|
* 自行跳转 APP,不需要上位机再发 FINISH_OTA。
|
|
|
|
|
|
* 旧注释里"0xF0F0F0F0 表示等 FINISH_OTA"是错误假设——电机 BOOT 代码里根本
|
|
|
|
|
|
* 没有 0xF0F0F0F0,尾帧 ACK 就是 0xFFFFFFFF,发完即跳转。
|
2026-07-30 02:11:49 +08:00
|
|
|
|
* 收到 0xFFFFFFFF = 电机已跳转,但不证明新 APP 能跑 → 进 kWaitAppRun
|
|
|
|
|
|
* 等新 APP 上电自主发的 [0A AC] 运行确认(协议表 15/16 行),收到才 kFinished。
|
|
|
|
|
|
* 此阶段绝不能主动发 [0x0A,0xAC]——会被已运行的电机 APP 当作握手开始信号干扰。 */
|
2026-07-26 20:39:59 +08:00
|
|
|
|
if (next_pkt == 0xFFFFFFFFU) {
|
2026-07-30 02:11:49 +08:00
|
|
|
|
/* 末包已被电机确认:把进度钉到 total,便于 QUERY/网页识别 100%。 */
|
2026-07-26 20:39:59 +08:00
|
|
|
|
s_packets_acked = s_total_packets;
|
|
|
|
|
|
s_last_acked_pkt = s_total_packets;
|
|
|
|
|
|
s_pkt_pending = false;
|
2026-07-30 02:11:49 +08:00
|
|
|
|
LOG_ERR("[MOTOR-OTA] tail-ACK pkt=%u/%u dup=%lu crc=%lu resend=%lu fast=%lu t=%lu, wait APP-run 0AAC\r\n",
|
2026-07-26 20:39:59 +08:00
|
|
|
|
(unsigned)s_packets_acked, (unsigned)s_total_packets,
|
|
|
|
|
|
(unsigned long)s_dup_ack_count, (unsigned long)s_crc_err_count,
|
2026-07-27 00:02:14 +08:00
|
|
|
|
(unsigned long)s_ack_resend_total, (unsigned long)s_fast_resend_total,
|
2026-07-26 20:39:59 +08:00
|
|
|
|
(unsigned long)SysTickTimer::getMs());
|
2026-07-30 02:11:49 +08:00
|
|
|
|
enterState(State::kWaitAppRun);
|
2026-07-26 20:39:59 +08:00
|
|
|
|
pushStatus();
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/* 兼容:若未来 BOOT 改为回 0xF0F0F0F0 等待 FINISH_OTA,再发 [0x0A,0xAC]。
|
|
|
|
|
|
* 当前电机 BOOT 不会走到这里,保留分支以防协议变体。 */
|
|
|
|
|
|
if (next_pkt == 0xF0F0F0F0U) {
|
|
|
|
|
|
const uint8_t finish_cmd[2] = {0x0A, 0xAC};
|
|
|
|
|
|
canSend(kOtaReqId, finish_cmd, 2);
|
|
|
|
|
|
s_last_send_ms = SysTickTimer::getMs();
|
|
|
|
|
|
enterState(State::kFinishOta);
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const uint16_t next16 = static_cast<uint16_t>(next_pkt);
|
|
|
|
|
|
|
|
|
|
|
|
/* 重复/未前进 ACK:BOOT auto_retrans=ENABLE,relay 侧 CAN 层没回 ACK 时会重传
|
|
|
|
|
|
* “同一个 next_pkt”的 ACK 帧。next_pkt 未前进时必须整帧忽略——绝不能清
|
|
|
|
|
|
* s_pkt_pending:否则会把刚发出、尚未真正确认的在途包误判为已确认,下一次
|
|
|
|
|
|
* onTxTick 多 bufPop 一包却仍沿用旧包号发送 → 包号/数据错位、CRC 错累积,
|
|
|
|
|
|
* 正是“看着有很多重复发送”的根源。忽略后在途包的真确认(next_pkt 前进)自然到来。 */
|
|
|
|
|
|
if (s_last_acked_pkt != 0xFFFFU && next16 <= s_last_acked_pkt) {
|
|
|
|
|
|
/* 重复/未前进 ACK:只累计计数,不逐帧打印(量极大会打爆 UART 扰乱时序)。
|
|
|
|
|
|
* 汇总见每 32 包进度行的 dup=。 */
|
|
|
|
|
|
s_dup_ack_count++;
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
s_packets_acked = next16;
|
|
|
|
|
|
s_last_acked_pkt = next16;
|
|
|
|
|
|
s_crc_retry_count = 0; /* 正常前进 ACK,清零 CRC 重试计数 */
|
|
|
|
|
|
s_crc_resend_pending = false;
|
|
|
|
|
|
s_pkt_pending = false; /* 本包确认;下一包由主循环 onTxTick 按钟表间隔发出 */
|
|
|
|
|
|
|
|
|
|
|
|
/* 每 32 包只打串口汇总,不再 pushStatus(0xD2)。
|
|
|
|
|
|
* 传输期 ESP32 suppress ring,0xD2 读不到还占 spi_ready/ring,
|
|
|
|
|
|
* 与 0xD1 喂数抢总线,加重缓冲欠载(CANlog 可见周期性 ~500ms 空洞)。
|
|
|
|
|
|
* 进度由 ESP32 feedTask 按已喂字节估算;终态再 push。 */
|
|
|
|
|
|
if ((s_packets_acked & 0x1F) == 0) {
|
2026-07-27 01:49:18 +08:00
|
|
|
|
OTA_LOGI("[MOTOR-OTA] progress pkt=%u/%u dup=%lu crc=%lu resend=%lu fast=%lu t=%lu\r\n",
|
2026-07-26 20:39:59 +08:00
|
|
|
|
(unsigned)s_packets_acked, (unsigned)s_total_packets,
|
|
|
|
|
|
(unsigned long)s_dup_ack_count, (unsigned long)s_crc_err_count,
|
2026-07-27 00:02:14 +08:00
|
|
|
|
(unsigned long)s_ack_resend_total, (unsigned long)s_fast_resend_total,
|
2026-07-26 20:39:59 +08:00
|
|
|
|
(unsigned long)SysTickTimer::getMs());
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/* ═══════════════════ 公开接口 ═══════════════════ */
|
|
|
|
|
|
|
|
|
|
|
|
void init() {
|
|
|
|
|
|
/* 无额外硬件初始化;CAN 已由 CanFd 模块管理 */
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/* 数据发送推进:由主循环 TaskRunner 每圈调用(不再走 ISR 计时 + TASK 标志)。
|
|
|
|
|
|
* 全部间隔判定用 SysTick 钟表(s_last_send_ms),与任务调度器的定时器分频解耦,
|
|
|
|
|
|
* 从根上消除 s_gap_div_100us 被 ISR(++)与主循环(=0)同时读写的竞争。
|
|
|
|
|
|
* 首包等 BOOT 擦除完成(kEraseWaitMs,从进 kTransfer 起算),后续包按 kTransferGapMs。 */
|
|
|
|
|
|
void onTxTick() {
|
|
|
|
|
|
if (s_state != State::kTransfer) return;
|
|
|
|
|
|
|
|
|
|
|
|
const uint32_t now = SysTickTimer::getMs();
|
|
|
|
|
|
|
2026-07-27 00:02:14 +08:00
|
|
|
|
/* ACK 超时重发:包已发出(s_pkt_pending=true)但超过 kAckResendMs 未收到 ACK,重发。
|
|
|
|
|
|
* 场景:OTA 期间 ARD=1 单发,总线错误(如 STUFF)直接击落数据帧/ACK 丢失,
|
2026-07-26 20:39:59 +08:00
|
|
|
|
* GD32 等 ACK、电机等下一包 → 双方死锁。
|
|
|
|
|
|
* ★ 注意:检查 s_pkt_pending(包已发等ACK),不是 s_ack_pending(ACK已到等处理)!
|
|
|
|
|
|
* 重发后电机幂等处理(同包号只 ACK 不重复写 flash)。 */
|
|
|
|
|
|
if (s_pkt_pending && !s_ack_pending && !s_crc_resend_pending) {
|
2026-07-27 00:02:14 +08:00
|
|
|
|
/* 快速路径:TX 完成中断已确认当前帧被总线错误击落(MTE),免等
|
|
|
|
|
|
* kAckResendMs 盲等,按 kTransferGapMs 节流立即重发。先判 gap 再 consume,
|
|
|
|
|
|
* 避免过早清标志丢事件;重发次数与超时路径共用 kAckMaxResends 上限。 */
|
|
|
|
|
|
if ((now - s_last_send_ms) >= kTransferGapMs
|
|
|
|
|
|
&& s_retry_count < kAckMaxResends
|
|
|
|
|
|
&& CanFd::consumeTxError(s_channel)) {
|
|
|
|
|
|
s_retry_count++;
|
|
|
|
|
|
s_fast_resend_total++;
|
|
|
|
|
|
const uint32_t err = CanFd::readChannelErrorReg(s_channel);
|
2026-07-27 01:49:18 +08:00
|
|
|
|
LOG_ERR("[MOTOR-OTA] TX-err fast-resend pkt=%u retry=%u tec=%lu rec=%lu errn=%lu\r\n",
|
2026-07-27 00:02:14 +08:00
|
|
|
|
(unsigned)s_packets_acked, (unsigned)s_retry_count,
|
|
|
|
|
|
(unsigned long)((err >> 16) & 0xFFU),
|
|
|
|
|
|
(unsigned long)((err >> 24) & 0xFFU),
|
|
|
|
|
|
(unsigned long)((err >> 4) & 0x07U));
|
|
|
|
|
|
sendCurrentPacket();
|
|
|
|
|
|
s_last_send_ms = SysTickTimer::getMs();
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
if (now - s_last_send_ms >= kAckResendMs) {
|
|
|
|
|
|
if (s_retry_count < kAckMaxResends) {
|
2026-07-26 20:39:59 +08:00
|
|
|
|
s_retry_count++;
|
2026-07-27 00:02:14 +08:00
|
|
|
|
s_ack_resend_total++;
|
|
|
|
|
|
/* 事件时刻 CAN_ERR 快照:钉死丢帧时总线状态(TEC 涨=数据帧发丢,
|
|
|
|
|
|
* REC 高=接收方向损坏),摆脱 1Hz [CAN-BUS] 采样混叠。仅日志,不做发送门控。 */
|
|
|
|
|
|
const uint32_t err = CanFd::readChannelErrorReg(s_channel);
|
2026-07-27 01:49:18 +08:00
|
|
|
|
LOG_ERR("[MOTOR-OTA] ACK timeout resend pkt=%u retry=%u tec=%lu rec=%lu errn=%lu\r\n",
|
2026-07-27 00:02:14 +08:00
|
|
|
|
(unsigned)s_packets_acked, (unsigned)s_retry_count,
|
|
|
|
|
|
(unsigned long)((err >> 16) & 0xFFU),
|
|
|
|
|
|
(unsigned long)((err >> 24) & 0xFFU),
|
|
|
|
|
|
(unsigned long)((err >> 4) & 0x07U));
|
2026-07-26 20:39:59 +08:00
|
|
|
|
sendCurrentPacket();
|
|
|
|
|
|
s_last_send_ms = SysTickTimer::getMs();
|
|
|
|
|
|
}
|
|
|
|
|
|
/* 超过最大重试次数后由 process() 的整体超时处理 */
|
|
|
|
|
|
}
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/* CRC 错误重发:发同一包(s_pkt_plain 不变、不 bufPop),按 kTransferGapMs 节流;
|
|
|
|
|
|
* 优先于 s_pkt_pending 门,否则忽略重复 ACK 后 s_pkt_pending 长期为真会堵死重发。 */
|
|
|
|
|
|
if (s_crc_resend_pending) {
|
|
|
|
|
|
if (now - s_last_send_ms < kTransferGapMs) return;
|
|
|
|
|
|
s_crc_resend_pending = false;
|
|
|
|
|
|
sendCurrentPacket();
|
|
|
|
|
|
s_last_send_ms = SysTickTimer::getMs();
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (s_pkt_pending) return; /* 当前包待确认,不发新包 */
|
|
|
|
|
|
|
|
|
|
|
|
/* 首包(s_packets_acked==0)等 kEraseWaitMs 让 BOOT 擦完,后续包按 kTransferGapMs。 */
|
|
|
|
|
|
const uint32_t need = (s_packets_acked == 0U) ? kEraseWaitMs : kTransferGapMs;
|
|
|
|
|
|
if (now - s_last_send_ms < need) return;
|
|
|
|
|
|
|
|
|
|
|
|
/* 水位门控:SPI 喂数跟不上时暂停 CAN,避免抽干环缓后电机 1s 超时。
|
|
|
|
|
|
* 首包要求更高水位(开传缓冲);运行中维持低水位即可。 */
|
|
|
|
|
|
const uint16_t water = (s_packets_acked == 0U) ? kStartWaterBytes : kRunWaterBytes;
|
|
|
|
|
|
const uint16_t used = bufUsed();
|
|
|
|
|
|
/* 末包:只剩 last_valid 字节时放行 */
|
|
|
|
|
|
const bool is_last = (s_packets_acked + 1U >= s_total_packets);
|
|
|
|
|
|
const uint16_t last_need = static_cast<uint16_t>(
|
|
|
|
|
|
(s_fw_size % kPacketPayload) == 0U ? kPacketPayload
|
|
|
|
|
|
: (s_fw_size % kPacketPayload));
|
|
|
|
|
|
/* 尾段:SPI 已喂完全部固件(fed>=fw_size)时缓冲里就是剩余全部数据,
|
|
|
|
|
|
* 剩余字节可能 < water(如 248<256),必须旁路水位门控,否则尾段死锁
|
|
|
|
|
|
* → 电机 BOOT 1s 无包超时。 */
|
|
|
|
|
|
const bool all_fed = (s_fw_fed >= s_fw_size);
|
|
|
|
|
|
if (!is_last && !all_fed && used < water) {
|
|
|
|
|
|
static uint32_t s_last_water_ms = 0;
|
|
|
|
|
|
if (now - s_last_water_ms >= 200U) {
|
2026-07-27 01:49:18 +08:00
|
|
|
|
OTA_LOGI("[MOTOR-OTA] wait-SPI-level used=%u need=%u fed=%lu pkt=%u\r\n",
|
2026-07-26 20:39:59 +08:00
|
|
|
|
(unsigned)used, (unsigned)water,
|
|
|
|
|
|
(unsigned long)s_fw_fed, (unsigned)s_packets_acked);
|
|
|
|
|
|
s_last_water_ms = now;
|
|
|
|
|
|
}
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
if (is_last && used < last_need) {
|
|
|
|
|
|
return; /* sendNextPacket 内也会等,这里省一次调用 */
|
|
|
|
|
|
}
|
|
|
|
|
|
sendNextPacket(); /* 内部设 s_last_send_ms */
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
void process() {
|
|
|
|
|
|
if (s_state == State::kIdle || s_state == State::kFinished || s_state == State::kError) {
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/* kPreCancel:等待电机 BOOT 心跳(0x103)确认已进入 Bootloader。
|
2026-07-30 02:11:49 +08:00
|
|
|
|
* 电机 BOOT IDLE 每 ~10ms 发一次 0x103;收到即证明电机已在 BOOT IDLE,
|
|
|
|
|
|
* 直接进握手(残帧窗口已移除,握手阶段对干扰帧忽略+步进重发兜底)。 */
|
2026-07-26 20:39:59 +08:00
|
|
|
|
if (s_state == State::kPreCancel) {
|
2026-07-30 02:11:49 +08:00
|
|
|
|
if (s_boot_heartbeat_seen) {
|
|
|
|
|
|
OTA_LOGI("[MOTOR-OTA] 0x103 ok, handshake\r\n");
|
2026-07-26 20:39:59 +08:00
|
|
|
|
enterState(State::kHandshakeReq);
|
|
|
|
|
|
pushStatus();
|
|
|
|
|
|
} else if (stateTimedOut(kHandshakeTimeoutMs)) {
|
2026-07-27 01:49:18 +08:00
|
|
|
|
LOG_ERR("[MOTOR-OTA] wait 0x103 timeout, motor not in BOOT\r\n");
|
2026-07-26 20:39:59 +08:00
|
|
|
|
setError(0x02); /* 0x02 = 电机未进入 BOOT */
|
|
|
|
|
|
}
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-07-30 02:11:49 +08:00
|
|
|
|
/* kWaitAppRun:尾包 ACK 后电机已复位跳 APP,等新 APP 自主发的 [0A AC] 运行确认。
|
|
|
|
|
|
* GD32 此阶段不发任何帧(不能走下方首发/补发路径,否则 sendCurrentStep 空转
|
|
|
|
|
|
* kMaxResends 次后误报 0x3A);启动期杂帧一律忽略,只认 [0A AC]。
|
|
|
|
|
|
* kAppRunTimeoutMs 无确认 → error 0x25(新 APP 未启动)。 */
|
|
|
|
|
|
if (s_state == State::kWaitAppRun) {
|
|
|
|
|
|
if (s_ack_pending) {
|
|
|
|
|
|
s_ack_pending = false;
|
|
|
|
|
|
uint8_t ack_data[64];
|
|
|
|
|
|
memcpy(ack_data, s_ack_data, 64);
|
|
|
|
|
|
const uint8_t ack_len = s_ack_len;
|
|
|
|
|
|
if (ack_len >= 2 && ack_data[0] == 0x0A && ack_data[1] == 0xAC) {
|
|
|
|
|
|
LOG_ERR("[MOTOR-OTA] APP-run 0AAC ok, FINISHED t=%lu\r\n",
|
|
|
|
|
|
(unsigned long)SysTickTimer::getMs());
|
|
|
|
|
|
enterState(State::kFinished);
|
|
|
|
|
|
pushStatus();
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
/* 非 [0A AC](APP 启动期其他帧)——忽略继续等 */
|
|
|
|
|
|
}
|
|
|
|
|
|
if (stateTimedOut(kAppRunTimeoutMs)) {
|
|
|
|
|
|
LOG_ERR("[MOTOR-OTA] no APP-run 0AAC in %lums, new APP not running\r\n",
|
|
|
|
|
|
(unsigned long)kAppRunTimeoutMs);
|
|
|
|
|
|
setError(0x25); /* 0x25 = 刷写传完但新 APP 未发运行确认 */
|
|
|
|
|
|
}
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-07-26 20:39:59 +08:00
|
|
|
|
/* 超时检查 */
|
|
|
|
|
|
const uint32_t timeout = (s_state == State::kTransfer || s_state == State::kFinishOta)
|
|
|
|
|
|
? kAckTimeoutMs : kHandshakeTimeoutMs;
|
|
|
|
|
|
if (stateTimedOut(timeout)) {
|
2026-08-01 04:15:10 +08:00
|
|
|
|
/* kTransfer 超时(电机无 ACK / 已退 IDLE):优先自动重试 */
|
|
|
|
|
|
if (s_state == State::kTransfer && s_auto_retry < kMaxAutoRetry) {
|
|
|
|
|
|
tryAutoRetry("transfer-timeout");
|
|
|
|
|
|
} else {
|
|
|
|
|
|
setError(0x01); /* 超时 */
|
|
|
|
|
|
pushStatus();
|
|
|
|
|
|
}
|
2026-07-26 20:39:59 +08:00
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-08-01 04:15:10 +08:00
|
|
|
|
/* SPI 喂数断流检测(kTransfer 专用):固件尚未喂完但 ESP32 已长时间无新数据。
|
|
|
|
|
|
* 优先自动重试(重新握手 + 等 ESP32 重喂);重试耗尽才报错 0x26。 */
|
|
|
|
|
|
if (s_state == State::kTransfer && s_fw_fed < s_fw_size) {
|
|
|
|
|
|
const uint32_t feed_silence = SysTickTimer::getMs() - s_last_feed_ms;
|
|
|
|
|
|
if (feed_silence >= kSpiFeedTimeoutMs) {
|
|
|
|
|
|
if (s_auto_retry < kMaxAutoRetry) {
|
|
|
|
|
|
tryAutoRetry("SPI-feed-starvation");
|
|
|
|
|
|
} else {
|
|
|
|
|
|
LOG_ERR("[MOTOR-OTA] SPI feed starvation: no data for %lums, fed=%lu/%lu pkt=%u/%u retry-exhausted\r\n",
|
|
|
|
|
|
(unsigned long)feed_silence,
|
|
|
|
|
|
(unsigned long)s_fw_fed, (unsigned long)s_fw_size,
|
|
|
|
|
|
(unsigned)s_packets_acked, (unsigned)s_total_packets);
|
|
|
|
|
|
setError(0x26); /* 0x26 = SPI 喂数超时(重试耗尽) */
|
|
|
|
|
|
pushStatus();
|
|
|
|
|
|
}
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-07-26 20:39:59 +08:00
|
|
|
|
/* ===== ACK 优先处理 ===== */
|
|
|
|
|
|
if (s_ack_pending) {
|
|
|
|
|
|
s_ack_pending = false;
|
|
|
|
|
|
uint8_t ack_data[64];
|
|
|
|
|
|
uint8_t ack_len;
|
|
|
|
|
|
memcpy(ack_data, s_ack_data, 64);
|
|
|
|
|
|
ack_len = s_ack_len;
|
|
|
|
|
|
|
|
|
|
|
|
/* 日志:握手步骤全打印;传输期错误帧限频 */
|
|
|
|
|
|
bool rx_do_log = (s_state != State::kTransfer);
|
|
|
|
|
|
if (s_state == State::kTransfer && ack_len == 1) {
|
|
|
|
|
|
static uint32_t s_last_rxerr_ms = 0;
|
|
|
|
|
|
const uint32_t rnow = SysTickTimer::getMs();
|
|
|
|
|
|
if (rnow - s_last_rxerr_ms >= 200U) { rx_do_log = true; s_last_rxerr_ms = rnow; }
|
|
|
|
|
|
}
|
|
|
|
|
|
if (rx_do_log) {
|
2026-07-27 01:49:18 +08:00
|
|
|
|
OTA_LOGI("[MOTOR-OTA] RX %s pkt=%u/%u len=%u b0=%02X b1=%02X t=%lu\r\n",
|
2026-07-26 20:39:59 +08:00
|
|
|
|
stepName(s_state), (unsigned)s_packets_acked, (unsigned)s_total_packets,
|
|
|
|
|
|
(unsigned)ack_len,
|
|
|
|
|
|
(ack_len > 0) ? ack_data[0] : 0xFFU, (ack_len > 1) ? ack_data[1] : 0xFFU,
|
|
|
|
|
|
(unsigned long)SysTickTimer::getMs());
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
switch (s_state) {
|
|
|
|
|
|
case State::kHandshakeReq:
|
|
|
|
|
|
if (ack_len >= 2 && ack_data[0] == 0x00 && ack_data[1] == 0xAC) {
|
|
|
|
|
|
/* echo 正确 —— 立即发 nonce(与 PC 工具一致,不给干扰帧窗口) */
|
|
|
|
|
|
generateNonce(s_nonce);
|
|
|
|
|
|
enterState(State::kHandshakeNonce);
|
|
|
|
|
|
canSend(kOtaReqId, s_nonce, 16);
|
|
|
|
|
|
s_last_send_ms = SysTickTimer::getMs();
|
2026-07-27 01:49:18 +08:00
|
|
|
|
OTA_LOGI("[MOTOR-OTA] TX HANDSHAKE_NONCE(0x100:16B) [ACK-driven] t=%lu\r\n",
|
2026-07-26 20:39:59 +08:00
|
|
|
|
(unsigned long)s_last_send_ms);
|
|
|
|
|
|
}
|
|
|
|
|
|
/* 不匹配的 ACK 靠掉 —— 忽略,等超时重发 REQ */
|
|
|
|
|
|
break;
|
|
|
|
|
|
|
|
|
|
|
|
case State::kHandshakeNonce:
|
|
|
|
|
|
if (ack_len < 36) {
|
|
|
|
|
|
/* 短帧(0x05/干扰) —— 忽略,等真正 48B 或超时重发 */
|
2026-07-27 01:49:18 +08:00
|
|
|
|
LOG_ERR("[MOTOR-OTA] NONCE ACK ignore short frame len=%u b0=0x%02X\r\n",
|
2026-07-26 20:39:59 +08:00
|
|
|
|
(unsigned)ack_len, (ack_len > 0) ? ack_data[0] : 0xFFU);
|
|
|
|
|
|
break;
|
|
|
|
|
|
}
|
|
|
|
|
|
if (memcmp(ack_data + 12, s_nonce + 8, 8) != 0) {
|
|
|
|
|
|
/* nonce echo 不匹配 —— 电机收到了错误的帧当 nonce */
|
2026-07-27 01:49:18 +08:00
|
|
|
|
LOG_ERR("[MOTOR-OTA] NONCE ACK echo mismatch, ignore\r\n");
|
2026-07-26 20:39:59 +08:00
|
|
|
|
break;
|
|
|
|
|
|
}
|
|
|
|
|
|
/* ✔ nonce 响应正确 —— 派生 K_session,立即发 confirm */
|
|
|
|
|
|
aes128_set_key(&s_aes_ctx, K_MASTER);
|
|
|
|
|
|
aes128_encrypt_block(&s_aes_ctx, s_nonce, s_k_session);
|
|
|
|
|
|
s_session_num = static_cast<uint32_t>(s_nonce[0])
|
|
|
|
|
|
| (static_cast<uint32_t>(s_nonce[1]) << 8)
|
|
|
|
|
|
| (static_cast<uint32_t>(s_nonce[2]) << 16)
|
|
|
|
|
|
| (static_cast<uint32_t>(s_nonce[3]) << 24);
|
|
|
|
|
|
{
|
|
|
|
|
|
enterState(State::kHandshakeConfirm);
|
|
|
|
|
|
uint8_t plain[16];
|
|
|
|
|
|
for (uint8_t i = 0; i < 16; i++) plain[i] = s_nonce[i] ^ 0x55;
|
|
|
|
|
|
uint8_t confirm[16];
|
|
|
|
|
|
aes128_set_key(&s_aes_ctx, s_k_session);
|
|
|
|
|
|
aes128_encrypt_block(&s_aes_ctx, plain, confirm);
|
|
|
|
|
|
canSend(kOtaReqId, confirm, 16);
|
|
|
|
|
|
s_last_send_ms = SysTickTimer::getMs();
|
2026-07-27 01:49:18 +08:00
|
|
|
|
OTA_LOGI("[MOTOR-OTA] TX HANDSHAKE_CONFIRM(0x100:16B) [ACK-driven] t=%lu\r\n",
|
2026-07-26 20:39:59 +08:00
|
|
|
|
(unsigned long)s_last_send_ms);
|
|
|
|
|
|
}
|
|
|
|
|
|
break;
|
|
|
|
|
|
|
|
|
|
|
|
case State::kHandshakeConfirm:
|
|
|
|
|
|
if (ack_len >= 2 && ack_data[0] == 0xBA && ack_data[1] == 0xC0) {
|
|
|
|
|
|
/* ✔ 握手成功 —— 立即发 fw_size */
|
|
|
|
|
|
enterState(State::kErase);
|
|
|
|
|
|
uint8_t size_le[4];
|
|
|
|
|
|
size_le[0] = static_cast<uint8_t>(s_fw_size & 0xFF);
|
|
|
|
|
|
size_le[1] = static_cast<uint8_t>((s_fw_size >> 8) & 0xFF);
|
|
|
|
|
|
size_le[2] = static_cast<uint8_t>((s_fw_size >> 16) & 0xFF);
|
|
|
|
|
|
size_le[3] = static_cast<uint8_t>((s_fw_size >> 24) & 0xFF);
|
|
|
|
|
|
canSend(kOtaDataId, size_le, 4);
|
|
|
|
|
|
s_last_send_ms = SysTickTimer::getMs();
|
2026-07-27 01:49:18 +08:00
|
|
|
|
OTA_LOGI("[MOTOR-OTA] TX ERASE(0x101:fw_size) [ACK-driven] t=%lu\r\n",
|
2026-07-26 20:39:59 +08:00
|
|
|
|
(unsigned long)s_last_send_ms);
|
|
|
|
|
|
}
|
|
|
|
|
|
/* 非 [BA C0] —— 忽略(可能是 0x03/0x05 干扰帧) */
|
|
|
|
|
|
break;
|
|
|
|
|
|
|
|
|
|
|
|
case State::kErase:
|
|
|
|
|
|
if (ack_len >= 6 && ack_data[0] == 0xBA && ack_data[1] == 0xC0) {
|
|
|
|
|
|
/* ✔ 擦除完成 —— 进入传输态,等 kEraseWaitMs 后由 onTxTick 发首包 */
|
|
|
|
|
|
s_packets_acked = 0;
|
|
|
|
|
|
enterState(State::kTransfer);
|
|
|
|
|
|
pushStatus();
|
|
|
|
|
|
/* 清空 CAN TX 邮箱:握手期间快速连发4帧(REQ+nonce+confirm+fw_size),
|
|
|
|
|
|
* 可能有帧卡在邮箱未发出,进入 kTransfer 后才发出 → 电机收到
|
|
|
|
|
|
* 过时的 0x100 帧 → catch-all → [05] 杀死 OTA。强制 abort 降低风险。 */
|
|
|
|
|
|
CanFd::abortPendingTx(s_channel);
|
|
|
|
|
|
s_last_send_ms = SysTickTimer::getMs();
|
2026-07-27 01:49:18 +08:00
|
|
|
|
OTA_LOGI("[MOTOR-OTA] ERASE ACK ok, wait %ums then first pkt\r\n",
|
2026-07-26 20:39:59 +08:00
|
|
|
|
(unsigned)kEraseWaitMs);
|
|
|
|
|
|
}
|
|
|
|
|
|
/* 非 [BA C0 ...] —— 忽略,等超时重发 fw_size */
|
|
|
|
|
|
break;
|
|
|
|
|
|
|
|
|
|
|
|
case State::kTransfer:
|
|
|
|
|
|
processTransferAck(ack_data, ack_len);
|
|
|
|
|
|
break;
|
|
|
|
|
|
|
|
|
|
|
|
case State::kFinishOta:
|
|
|
|
|
|
if (ack_len >= 2 && ack_data[0] == 0x0A && ack_data[1] == 0xAC) {
|
|
|
|
|
|
enterState(State::kFinished);
|
|
|
|
|
|
pushStatus();
|
|
|
|
|
|
} else {
|
|
|
|
|
|
/* BOOT 可能回 WAIT 表示还在处理,重置超时 */
|
|
|
|
|
|
s_state_enter_ms = SysTickTimer::getMs();
|
|
|
|
|
|
}
|
|
|
|
|
|
break;
|
|
|
|
|
|
|
|
|
|
|
|
default:
|
|
|
|
|
|
break;
|
|
|
|
|
|
}
|
|
|
|
|
|
return; /* ACK 已处理,本轮结束 */
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/* ===== 无 ACK —— 首发或超时重发 ===== */
|
|
|
|
|
|
if (s_state == State::kTransfer) {
|
|
|
|
|
|
return; /* kTransfer 由 onTxTick 驱动 */
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const uint32_t now_ms = SysTickTimer::getMs();
|
|
|
|
|
|
|
|
|
|
|
|
if (s_last_send_ms == 0U) {
|
|
|
|
|
|
/* 首发(仅 kHandshakeReq 初始进入时走这里,其余步骤由 ACK handler 触发发帧) */
|
|
|
|
|
|
sendCurrentStep(now_ms);
|
|
|
|
|
|
} else if ((now_ms - s_last_send_ms) >= kStepTimeoutMs) {
|
|
|
|
|
|
/* 超时重发 */
|
|
|
|
|
|
if (s_retry_count >= kMaxResends) {
|
2026-07-27 01:49:18 +08:00
|
|
|
|
LOG_ERR("[MOTOR-OTA] %s no-ACK after %u tries, abort\r\n",
|
2026-07-26 20:39:59 +08:00
|
|
|
|
stepName(s_state), (unsigned)(kMaxResends + 1U));
|
|
|
|
|
|
setError(static_cast<uint8_t>(0x30U + static_cast<uint8_t>(s_state)));
|
|
|
|
|
|
pushStatus();
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
s_retry_count++;
|
|
|
|
|
|
sendCurrentStep(now_ms);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
bool onCanFrame(uint8_t channel, uint32_t id, const uint8_t *data, uint8_t len) {
|
|
|
|
|
|
if (s_state == State::kIdle || s_state == State::kFinished || s_state == State::kError) {
|
|
|
|
|
|
return false;
|
|
|
|
|
|
}
|
|
|
|
|
|
if (channel != s_channel) return false;
|
|
|
|
|
|
|
|
|
|
|
|
/* BOOT 的 0x103 帧语义修正:
|
|
|
|
|
|
* 本电机 BOOT 仅在 IDLE 状态每 ~10ms 发一次 0x103 心跳(见 OTA.cpp process()),
|
|
|
|
|
|
* 擦除/传输忙时并不发 0x103。握手阶段(kHandshakeReq/Nonce/Confirm)收到 0x103
|
|
|
|
|
|
* 只说明电机仍停在 IDLE、尚未接受握手,此时绝不能重置超时——否则握手失败时
|
|
|
|
|
|
* 超时会被 IDLE 心跳无限重置 → relay 静默死循环、UI 永远卡在"握手中"且无错误码。
|
|
|
|
|
|
* 仅在 kErase/kTransfer/kFinishOta(电机确在忙、WAIT 合法)才重置超时,
|
|
|
|
|
|
* 使握手失败能在 kHandshakeTimeoutMs 后正常超时报错、可被取消/重试。 */
|
|
|
|
|
|
if (id == kOtaWaitId) {
|
|
|
|
|
|
/* kPreCancel 期间:0x103 = 电机 BOOT IDLE 心跳,证明电机已进入 Bootloader */
|
|
|
|
|
|
if (s_state == State::kPreCancel) {
|
|
|
|
|
|
s_boot_heartbeat_seen = true;
|
|
|
|
|
|
}
|
2026-08-01 04:15:10 +08:00
|
|
|
|
if (s_state == State::kErase || s_state == State::kFinishOta) {
|
|
|
|
|
|
s_state_enter_ms = SysTickTimer::getMs();
|
|
|
|
|
|
}
|
|
|
|
|
|
/* kTransfer 特殊处理:仅在有包在途(s_pkt_pending=true)时重置超时。
|
|
|
|
|
|
* 电机 BOOT 在 RECEIVING_FIRMWARE 超时(~1-2s)后退回 IDLE 并恢复 0x103 心跳;
|
|
|
|
|
|
* 此时 GD32 若无包在途(环缓耗尽等 SPI 喂数),0x103 的含义是"电机已放弃传输"
|
|
|
|
|
|
* 而非"电机还在忙"。无条件重置超时 → 5s backstop 永远到不了 → 永久卡死。
|
|
|
|
|
|
* 有包在途时重置仍合法:电机 flash 写慢确实会回 WAIT。 */
|
|
|
|
|
|
if (s_state == State::kTransfer && s_pkt_pending) {
|
2026-07-26 20:39:59 +08:00
|
|
|
|
s_state_enter_ms = SysTickTimer::getMs();
|
|
|
|
|
|
}
|
|
|
|
|
|
return true; /* 已消费,不再传递给其他 handler */
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (id != kOtaAckId) return false;
|
|
|
|
|
|
|
|
|
|
|
|
/* kPreCancel 期间丢弃残留 ACK(电机可能对上次 OTA 的帧补发 ACK),
|
|
|
|
|
|
* 不置 s_ack_pending,确保进入 kHandshakeReq 时从干净状态开始。 */
|
|
|
|
|
|
if (s_state == State::kPreCancel) return true;
|
|
|
|
|
|
|
|
|
|
|
|
/* 捕获 ACK:总是覆盖(处理最新帧)。
|
|
|
|
|
|
* 无需 s_last_send_ms guard——新模型里 ACK handler 触发后立即发下一帧,
|
|
|
|
|
|
* s_last_send_ms 在任何 ACK 到达前已被设置。 */
|
|
|
|
|
|
|
|
|
|
|
|
/* 捕获 ACK */
|
|
|
|
|
|
const uint8_t copy_len = (len <= 64) ? len : 64;
|
|
|
|
|
|
memcpy(s_ack_data, data, copy_len);
|
|
|
|
|
|
s_ack_len = copy_len;
|
|
|
|
|
|
s_ack_pending = true;
|
|
|
|
|
|
return true; /* 已消费,不再传递给其他 handler */
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
bool start(uint8_t can_channel, uint32_t fw_size) {
|
|
|
|
|
|
if (s_state != State::kIdle && s_state != State::kFinished && s_state != State::kError) {
|
|
|
|
|
|
return false; /* 已在进行中 */
|
|
|
|
|
|
}
|
|
|
|
|
|
if (fw_size == 0 || fw_size > kMaxFwSize) return false;
|
|
|
|
|
|
if (can_channel >= 3) return false;
|
|
|
|
|
|
|
|
|
|
|
|
s_channel = can_channel;
|
|
|
|
|
|
s_fw_size = fw_size;
|
|
|
|
|
|
s_fw_fed = 0U;
|
|
|
|
|
|
s_total_packets = static_cast<uint16_t>((fw_size + kPacketPayload - 1U) / kPacketPayload);
|
|
|
|
|
|
s_packets_acked = 0;
|
|
|
|
|
|
s_error = 0;
|
|
|
|
|
|
s_session_num = 0; /* 由 processNonceAck 根据 s_nonce 前4字节设置 */
|
|
|
|
|
|
s_buf_head = 0;
|
|
|
|
|
|
s_buf_tail = 0;
|
|
|
|
|
|
s_ack_pending = false;
|
|
|
|
|
|
s_last_send_ms = 0; /* 0=尚未首发,enterState(kHandshakeReq) 也会置 0,由 process() 首发 00AC */
|
|
|
|
|
|
s_crc_retry_count = 0;
|
|
|
|
|
|
s_last_acked_pkt = 0xFFFFU;
|
|
|
|
|
|
s_pkt_pending = false;
|
|
|
|
|
|
s_dup_ack_count = 0;
|
|
|
|
|
|
s_crc_err_count = 0;
|
2026-07-27 00:02:14 +08:00
|
|
|
|
s_ack_resend_total = 0;
|
|
|
|
|
|
s_fast_resend_total = 0;
|
2026-07-26 20:39:59 +08:00
|
|
|
|
s_last_pad_valid = 0xFFFFU;
|
2026-08-01 04:15:10 +08:00
|
|
|
|
s_last_feed_ms = SysTickTimer::getMs(); /* 从 start 起算 SPI 喂数超时 */
|
|
|
|
|
|
s_auto_retry = 0;
|
2026-07-26 20:39:59 +08:00
|
|
|
|
|
|
|
|
|
|
/* 不在 SPI 中断里同步发握手帧(电机跳 BOOT 期间 CAN 可能短暂断开)。
|
|
|
|
|
|
* 先进 kPreCancel 等待电机 0x103 心跳确认已进 BOOT,再由 process() 异步推进到握手。 */
|
2026-07-27 01:49:18 +08:00
|
|
|
|
LOG_ERR("[MOTOR-OTA] START ch=%u fw=%lu total=%u\r\n",
|
2026-07-26 20:39:59 +08:00
|
|
|
|
(unsigned)can_channel, (unsigned long)fw_size, (unsigned)s_total_packets);
|
|
|
|
|
|
s_boot_heartbeat_seen = false;
|
|
|
|
|
|
enterState(State::kPreCancel);
|
|
|
|
|
|
return true;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
bool feedData(const uint8_t *data, uint16_t len) {
|
|
|
|
|
|
/* 接受任何活跃状态的数据(包括握手期间)——让 ESP32 在 kPreCancel/握手
|
|
|
|
|
|
* 期间就能预填充缓冲,进入 kTransfer 后立即有数据可发。
|
|
|
|
|
|
*
|
|
|
|
|
|
* payload: [offset:u32 LE][chunk...]。offset 为固件绝对字节偏移。
|
|
|
|
|
|
* ★ BLF 根因:无 offset 纯流时,SPI 超时重传同一 chunk,GD32 再次追加,
|
|
|
|
|
|
* CAN 上出现「前 236B 循环 185 次」→ CRC 全过但固件内容全错。 */
|
|
|
|
|
|
if (s_state == State::kIdle || s_state == State::kError ||
|
|
|
|
|
|
s_state == State::kFinished) {
|
|
|
|
|
|
return false;
|
|
|
|
|
|
}
|
|
|
|
|
|
if (data == nullptr || len < 4U) {
|
|
|
|
|
|
return false;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const uint32_t offset = static_cast<uint32_t>(data[0])
|
|
|
|
|
|
| (static_cast<uint32_t>(data[1]) << 8)
|
|
|
|
|
|
| (static_cast<uint32_t>(data[2]) << 16)
|
|
|
|
|
|
| (static_cast<uint32_t>(data[3]) << 24);
|
|
|
|
|
|
const uint8_t *chunk = data + 4U;
|
|
|
|
|
|
uint16_t chunk_len = static_cast<uint16_t>(len - 4U);
|
|
|
|
|
|
if (chunk_len == 0U) {
|
|
|
|
|
|
return true;
|
|
|
|
|
|
}
|
|
|
|
|
|
if (s_fw_size == 0U) {
|
|
|
|
|
|
return false;
|
|
|
|
|
|
}
|
|
|
|
|
|
if (offset >= s_fw_size) {
|
|
|
|
|
|
return true; /* 越界尾:幂等 ACK */
|
|
|
|
|
|
}
|
|
|
|
|
|
/* 截到固件末尾 */
|
|
|
|
|
|
if (offset + chunk_len > s_fw_size) {
|
|
|
|
|
|
chunk_len = static_cast<uint16_t>(s_fw_size - offset);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (offset + chunk_len <= s_fw_fed) {
|
|
|
|
|
|
/* 完全已收过:幂等 ACK,不写环缓 */
|
|
|
|
|
|
return true;
|
|
|
|
|
|
}
|
|
|
|
|
|
if (offset > s_fw_fed) {
|
|
|
|
|
|
/* 缺口:拒收,ESP32 按 s_fw_offset 重发正确位置 */
|
|
|
|
|
|
static uint32_t s_last_gap_ms = 0;
|
|
|
|
|
|
const uint32_t now = SysTickTimer::getMs();
|
|
|
|
|
|
if (now - s_last_gap_ms >= 200U) {
|
2026-07-27 01:49:18 +08:00
|
|
|
|
LOG_ERR("[MOTOR-OTA] feed gap off=%lu fed=%lu len=%u\r\n",
|
2026-07-26 20:39:59 +08:00
|
|
|
|
(unsigned long)offset, (unsigned long)s_fw_fed,
|
|
|
|
|
|
(unsigned)chunk_len);
|
|
|
|
|
|
s_last_gap_ms = now;
|
|
|
|
|
|
}
|
|
|
|
|
|
return false;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/* 部分重叠:只追加 [s_fw_fed, offset+chunk_len) */
|
|
|
|
|
|
const uint32_t skip = s_fw_fed - offset;
|
|
|
|
|
|
const uint16_t add_len = static_cast<uint16_t>(chunk_len - skip);
|
|
|
|
|
|
if (bufFree() < add_len) {
|
|
|
|
|
|
return false; /* 环缓满 → SPI WAIT */
|
|
|
|
|
|
}
|
|
|
|
|
|
bufPush(chunk + skip, add_len);
|
|
|
|
|
|
s_fw_fed += add_len;
|
2026-08-01 04:15:10 +08:00
|
|
|
|
s_last_feed_ms = SysTickTimer::getMs(); /* SPI 断流检测:记录有效喂数时刻 */
|
2026-07-26 20:39:59 +08:00
|
|
|
|
return true;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
void cancel() {
|
|
|
|
|
|
if (s_state == State::kIdle) return;
|
2026-07-27 01:49:18 +08:00
|
|
|
|
LOG_ERR("[MOTOR-OTA] CANCEL received in state %s\r\n", stepName(s_state));
|
2026-07-30 02:11:49 +08:00
|
|
|
|
/* ★ kFinished/kError/kWaitAppRun 时电机已跳转 APP 或已中止,绝不能再发 CAN 取消帧!
|
2026-07-26 20:39:59 +08:00
|
|
|
|
* kOtaCancelId=0x00000000 与电机 APP 的 CANFD_BOOT_ID(base_id=0x0000) 冲突,
|
|
|
|
|
|
* 电机 APP 收到会当作 BOOT 命令 → enter_silent_mode 10s,APP 看似卡死。
|
2026-07-30 02:11:49 +08:00
|
|
|
|
* kWaitAppRun = 尾包已确认、电机已复位跳 APP,同样不能发。
|
2026-07-26 20:39:59 +08:00
|
|
|
|
* 仅在活跃传输态(kPreCancel..kFinishOta)发取消帧通知电机 BOOT 中止。 */
|
2026-07-30 02:11:49 +08:00
|
|
|
|
const bool was_active = (s_state != State::kFinished && s_state != State::kError &&
|
|
|
|
|
|
s_state != State::kWaitAppRun);
|
2026-07-26 20:39:59 +08:00
|
|
|
|
if (was_active) {
|
|
|
|
|
|
canSend(kOtaCancelId, nullptr, 0);
|
|
|
|
|
|
}
|
|
|
|
|
|
enterState(State::kIdle);
|
|
|
|
|
|
s_buf_head = 0;
|
|
|
|
|
|
s_buf_tail = 0;
|
|
|
|
|
|
s_fw_fed = 0U;
|
|
|
|
|
|
s_pkt_pending = false;
|
|
|
|
|
|
s_crc_retry_count = 0;
|
|
|
|
|
|
pushStatus();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
Status getStatus() {
|
|
|
|
|
|
Status st;
|
|
|
|
|
|
st.state = s_state;
|
|
|
|
|
|
st.error_code = s_error;
|
|
|
|
|
|
st.packets_acked = s_packets_acked;
|
|
|
|
|
|
st.total_packets = s_total_packets;
|
|
|
|
|
|
st.fw_size = s_fw_size;
|
|
|
|
|
|
return st;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
bool isActive() {
|
|
|
|
|
|
return (s_state != State::kIdle && s_state != State::kFinished && s_state != State::kError);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
} /* namespace MotorOtaRelay */
|