/** * @file motor_state.cpp * @brief 多电机状态表实现 — CAN STATUS 帧识别、内存缓存、SPI 推送序列化。 * * 所有函数仅在主循环上下文调用(taskCanRxParse / taskHeartbeat / pushStep), * 无 ISR 竞态,不需要临界区保护。 */ #include "device/motor_state.h" #include "device/spi/app_spi_command_defs.h" #include "main.h" #include /* ── 全局状态表实例 ── */ static MotorStateTable s_motorTable = {}; namespace MotorState { MotorStateTable &table() { return s_motorTable; } void onCanFrame(uint8_t channel, uint32_t id, const uint8_t *data, uint8_t len) { /* BOOT 心跳 0x103(固定 ID,不带节点号):电机在 Bootloader IDLE 每 ~10ms 发一次。 * 记录最近收到时刻与通道,供网页显示独立 BOOT 状态(不归属任何 Nx)。 * 注意:必须在 STATUS 掩码过滤之前拦截,否则 0x103 会被下面的掩码判断丢弃。 */ if (id == kOtaBootHeartbeatId) { s_motorTable.boot_hb_sec = g_app_uptime_sec; s_motorTable.boot_channel = channel; s_motorTable.boot_active = 1U; /* 0x103 仅更新 boot_active 状态,不再串口打印(高频心跳会刷屏)。 */ return; } /* 掩码匹配 STATUS ID: 0x000N0100 */ if ((id & kMotorStatusIdMask) != kMotorStatusIdMatch) { return; } const uint8_t node = static_cast((id >> 16) & 0x0FU); if (node < 1U || node > kMotorMaxNodes) { return; } MotorSlot &slot = s_motorTable.slots[node - 1U]; slot.node_id = node; slot.channel = channel; slot.online = 1U; slot.dlc = (len <= kMotorStatusDlc) ? len : kMotorStatusDlc; slot.rx_count++; slot.last_rx_sec = g_app_uptime_sec; slot.seq = ++s_motorTable.global_seq; /* 拷贝原始 payload */ std::memcpy(slot.data, data, slot.dlc); if (slot.dlc < kMotorStatusDlc) { std::memset(&slot.data[slot.dlc], 0, kMotorStatusDlc - slot.dlc); } } void checkOffline() { const uint32_t now = g_app_uptime_sec; uint8_t active = 0U; for (uint8_t i = 0U; i < kMotorMaxNodes; ++i) { MotorSlot &slot = s_motorTable.slots[i]; if (slot.node_id == 0U) { continue; /* 空槽 */ } if ((now - slot.last_rx_sec) >= kMotorOfflineSec) { slot.online = 0U; } if (slot.online != 0U) { active++; } } s_motorTable.active_count = active; /* BOOT 心跳超时:>kBootHeartbeatTimeoutSec 无 0x103 → 电机已退出 BOOT。 */ if (s_motorTable.boot_active != 0U && (now - s_motorTable.boot_hb_sec) >= kBootHeartbeatTimeoutSec) { s_motorTable.boot_active = 0U; } } uint16_t buildPayload(uint8_t page, uint8_t *buf, uint16_t maxLen) { if (page >= kMotorTotalPages || buf == nullptr) { return 0U; } /* 需要的最小空间: header + 至少 1 entry */ const uint16_t needed = kMotorPageHeaderLen + kMotorEntrySize; if (maxLen < needed) { return 0U; } const uint8_t startIdx = static_cast(page * kMotorEntriesPerPage); uint8_t count = 0U; /* Header (4B) */ buf[0] = page; buf[1] = kMotorTotalPages; buf[2] = 0U; /* entry_count, 稍后回填 */ /* bit[6:0]=在线电机数,bit7=BOOT 心跳(0x103)活跃标志(供网页显示独立 BOOT 状态)。 * active_count 上限 9,bit7 空闲可复用,旧解析方读到会多算 128,故 ESP32 侧需掩 0x7F。 */ buf[3] = static_cast((s_motorTable.active_count & 0x7FU) | (s_motorTable.boot_active ? kMotorActiveBootBit : 0U)); uint16_t offset = kMotorPageHeaderLen; for (uint8_t i = 0U; i < kMotorEntriesPerPage; ++i) { const uint8_t slotIdx = static_cast(startIdx + i); if (slotIdx >= kMotorMaxNodes) { break; } const MotorSlot &slot = s_motorTable.slots[slotIdx]; if (slot.node_id == 0U) { continue; /* 跳过空槽 */ } /* 检查剩余空间 */ if (offset + kMotorEntrySize > maxLen) { break; } buf[offset + 0U] = slot.node_id; buf[offset + 1U] = slot.channel; buf[offset + 2U] = slot.online; buf[offset + 3U] = slot.dlc; writeU32Le(&buf[offset + 4U], slot.rx_count); writeU32Le(&buf[offset + 8U], slot.seq); std::memcpy(&buf[offset + 12U], slot.data, kMotorStatusDlc); offset += kMotorEntrySize; count++; } buf[2] = count; return offset; } } // namespace MotorState