"""EtherCAT 电机控制台后端自测(不接硬件)。 用一个模拟 GD32 buildInput/processOutput 行为的假从站,验证: 1. DLC 32 的电机 CMD 能完整下发(旧 24B 布局会静默截断后 8 字节) 2. 后台 PDO 循环靠滚动窗口扫完 27 个槽 3. STATUS 完整 32B 回传(旧 8B 短快照根本装不下) 4. 命令队列按 seq 沿下发,不互相覆盖 5. 循环可干净停止 6. 【1.5.56】批量发送区:多帧在**同一个周期**发出(同步下发) 7. 【1.5.56】批量接收区:一周期回收多帧,含 DLC64 CANFD 8. 【1.5.56】兼容性:批量区为空时,字节布局与 1.5.55 完全一致 跑法: python test_motor_backend.py """ import struct import sys import time import types import os BASE = os.path.dirname(os.path.abspath(__file__)) sys.path.insert(0, BASE) # pysoem 依赖 Npcap 与真网卡,自测里用桩替掉 _fake = types.ModuleType("pysoem") _fake.find_adapters = lambda: [] _fake.INIT_STATE, _fake.PREOP_STATE = 1, 2 _fake.SAFEOP_STATE, _fake.OP_STATE = 4, 8 _fake.STATE_ACK = 0x10 _fake.Master = object sys.modules["pysoem"] = _fake import ecat_server as S # noqa: E402 SLOT = S.MOT_SLOT_SIZE NSLOT = S.MOT_SLOTS_PER_PDO class FakeSlave: """按 comm/ethercat_can_bridge.cpp 的语义模拟从站侧。""" def __init__(self): self._out = bytes(S.PDO_OUT_LEN) self.rx_frames = [] # 从站"发出"的帧 (ch,id,dlc,hexdata) self.tx_cycles = [] # 每个 seq 沿发出的帧数,用来证明"同一周期多帧" self.last_seq = None # 对应固件 s_lastSeq / s_seqPrimed self.tx_count = 0 self.pending_rx = [] # 待经批量接收区回给主站的帧 self.rx_lost = 0 @property def output(self): return self._out @output.setter def output(self, v): self._out = bytes(v) seq = self._out[0] # 固件语义:首次只记基准不发送;之后仅 seq 变化沿触发一次发送 if self.last_seq is not None and seq != self.last_seq: n = 0 # 段1 单帧区(dlc=0 表示本次只发批量) dlc = self._out[2] if dlc: self.rx_frames.append(( self._out[1], struct.unpack_from(" S.PDO_OUT_LEN: break d = self._out[off + S.FRM_OFF_DLC] if off + S.FRM_HDR_SIZE + d > S.PDO_OUT_LEN: break self.rx_frames.append(( self._out[off + S.FRM_OFF_CH], struct.unpack_from("= S.MOTOR_SLOT_COUNT: break off = S.MOTW_OFF_SLOTS + i * SLOT b[off] = idx // S.MOTOR_CHANNELS + 1 # node 1..9 b[off + 1] = idx % S.MOTOR_CHANNELS # ch 0..2 b[off + 2] = 1 # online b[off + 3] = 32 # dlc struct.pack_into(" S.PDO_IN_LEN: break self.pending_rx.pop(0) b[off + S.FRM_OFF_CH] = ch b[off + S.FRM_OFF_DLC] = dlc b[off + S.FRM_OFF_FLAGS] = 0x01 struct.pack_into(" 从站收到 {len(sent)} 帧," f"各 seq 沿帧数={slave.tx_cycles}") assert len(sent) == 9, f"批量下发丢帧: {len(sent)}/9" assert max(slave.tx_cycles) > 1, \ f"仍是每周期一帧({slave.tx_cycles}),批量区没生效" # 关键:9 帧 DLC32 应在 1 个周期内发完(单帧区1 + 批量区8),而非排队 9 个周期 assert len(slave.tx_cycles) <= 2, \ f"9 帧用了 {len(slave.tx_cycles)} 个周期,未达到同步下发" ids = [f"0x{f[1]:X}" for f in sent] print(f" 帧序 ={ids}") assert [f[2] for f in sent] == [32] * 9, "批量帧 DLC 有截断" print(f" [OK] 9 帧在 {len(slave.tx_cycles)} 个周期内发完(旧实现需 9 个周期)") print(f"\n=== 7. 批量接收:一周期回收多帧 (容量 {S.RXB_BYTES}B) ===") # 含一帧满长 DLC64,验证 CANFD 全量能过 slave.pending_rx = [ (0, 0x10100, 32, bytes(range(32))), (1, 0x20100, 32, bytes(range(32, 64))), (2, 0x30100, 64, bytes(range(64))), # CANFD 满长 (0, 0x40100, 8, bytes(range(8))), ] S._motor_rx_log.clear() S._motor_thread_start(period_ms=2) time.sleep(0.25) snap = S.api_motor_state({}) S._motor_thread_stop() got = S.api_motor_rx({}) frames = got["frames"] print(f" 回收 {len(frames)} 帧, rx_batch.count={snap['rx_batch']['count']}, " f"pending={snap['rx_batch']['pending']}, lost={snap['rx_batch']['lost']}") for f in frames: print(f" ch={f['ch']} id=0x{f['id']:X} dlc={f['dlc']}") assert len(frames) == 4, f"批量接收丢帧: {len(frames)}/4" assert [f["dlc"] for f in frames] == [32, 32, 64, 8], "接收 DLC 有错" fd = next(f for f in frames if f["dlc"] == 64) assert bytes.fromhex(fd["data"]) == bytes(range(64)), "DLC64 payload 不一致" assert got["remain"] == 0, "取走后缓冲应清空" print(" [OK] 4 帧含 DLC64 满长 CANFD 全部完整回收") print("\n=== 8. 兼容性:批量区为空时布局与 1.5.55 一致 ===") S._fill_can_out(1, 0x30200, 32, bytes(range(32))) out = bytes(S._can_out) assert out[1] == 1 and out[2] == 32 assert struct.unpack_from("