#!/usr/bin/env python3 # -*- coding: utf-8 -*- import time import struct import threading from dataclasses import dataclass from typing import Dict, List, Tuple, Optional import can TORQUE_CMD_ID = 0x280 CMD_A1 = 0xA1 def _clamp_int16(x: int) -> int: return max(-32768, min(32767, int(x))) def _pack_i16_le(x: int) -> Tuple[int, int]: x = _clamp_int16(x) u = x & 0xFFFF return (u & 0xFF, (u >> 8) & 0xFF) def build_torque_broadcast(t1: int, t2: int, t3: int, t4: int) -> can.Message: d0, d1 = _pack_i16_le(t1) d2, d3 = _pack_i16_le(t2) d4, d5 = _pack_i16_le(t3) d6, d7 = _pack_i16_le(t4) return can.Message( arbitration_id=TORQUE_CMD_ID, is_extended_id=False, data=[d0, d1, d2, d3, d4, d5, d6, d7], ) def _arb_id_single(motor_id: int) -> int: return 0x140 + int(motor_id) def parse_a1(data8: bytes) -> Dict: # A1 回包(你实测) # [0]=0xA1 [1]=temp(int8) [2..3]=iq/power(int16 LE) [4..5]=speed(int16 LE, dps) [6..7]=encoder(uint16 LE) if len(data8) != 8 or data8[0] != CMD_A1: raise ValueError("not A1") temperature = struct.unpack(" str: if fr.t <= 0: return f"{tag} M{mid}: (no reply yet)" d = fr.data arb = fr.arb cmd = d[0] if cmd == CMD_A1: s = parse_a1(d) return (f"{tag} M{mid}: arb=0x{arb:X} " f"T={s['temperature_C']}C iq/p={s['iq_or_power_raw']} " f"spd={s['speed_dps']}dps enc={s['encoder']}") return f"{tag} M{mid}: arb=0x{arb:X} cmd=0x{cmd:02X} raw={[hex(x) for x in d]}" @dataclass class RxFrame: t: float = 0.0 arb: int = 0 data: bytes = b"" class RxCache4Ch(can.Listener): """ 一个 Notifier 监听一个 “设备Bus(包含两个channel)”,按 msg.channel 分流到 ch0/ch1 的缓存。 """ def __init__(self, ch_to_motor_ids: Dict[int, List[int]]): super().__init__() self.ch_to_motor_ids = {int(ch): set(mids) for ch, mids in ch_to_motor_ids.items()} self._lock = threading.Lock() self.latest: Dict[Tuple[int, int], RxFrame] = {} for ch, mids in self.ch_to_motor_ids.items(): for mid in mids: self.latest[(ch, mid)] = RxFrame() def on_message_received(self, msg: can.Message): # python-can Message.channel:对 canalystii 多通道会填 0/1 ch = getattr(msg, "channel", None) if ch is None: # 没有 channel 信息就无法分流,直接忽略或当作 ch0 ch = 0 try: ch = int(ch) except Exception: ch = 0 if ch not in self.ch_to_motor_ids: return arb = msg.arbitration_id if arb < 0x141 or arb > 0x160: return mid = arb - 0x140 if mid not in self.ch_to_motor_ids[ch]: return with self._lock: self.latest[(ch, mid)] = RxFrame(time.time(), arb, bytes(msg.data)) def snapshot(self) -> Dict[Tuple[int, int], RxFrame]: with self._lock: return dict(self.latest) class SenderThread(threading.Thread): """ 每个“逻辑channel(共4个)”一个发送线程: - 等主线程 tick_event - barrier 同步放行 - 加 bus_lock 后 send(同一个设备的两个channel共享一个bus对象,必须加锁) """ def __init__( self, name: str, bus: can.BusABC, bus_lock: threading.Lock, channel_index: int, # 0 or 1 tick_event: threading.Event, barrier: threading.Barrier, stop_event: threading.Event, ): super().__init__(name=name) self.bus = bus self.bus_lock = bus_lock self.channel_index = int(channel_index) self.tick_event = tick_event self.barrier = barrier self.stop_event = stop_event self.torque = (0, 0, 0, 0) def run(self): while not self.stop_event.is_set(): if not self.tick_event.wait(timeout=0.5): continue if self.stop_event.is_set(): break try: self.barrier.wait(timeout=0.05) except threading.BrokenBarrierError: continue t1, t2, t3, t4 = self.torque msg = build_torque_broadcast(t1, t2, t3, t4) msg.channel = self.channel_index # ★ 指定发到该设备的哪个channel try: with self.bus_lock: self.bus.send(msg) except can.CanError: pass # 等主线程清 tick_event,避免同tick重复发 while self.tick_event.is_set() and not self.stop_event.is_set(): time.sleep(0) def open_device_bus(device_idx: int, bitrate: int) -> can.BusABC: """ 每块 CANalyst-II 只打开一次 Bus,channel=[0,1] """ # canalystii 后端一般支持 device=0/1;如果你环境不支持会 TypeError,这里做兼容 try: return can.Bus(interface="canalystii", channel=[0, 1], bitrate=bitrate, device=device_idx) except TypeError: # 某些版本 canalystii 不暴露 device 参数(只能看到一块设备) return can.Bus(interface="canalystii", channel=[0, 1], bitrate=bitrate) def main(): bitrate = 1000000 hz = 500 dt = 1.0 / hz # ========= 你需要按实际接线修改:每个“设备-channel”上有哪些电机ID ========= # 逻辑4路:dev0-ch0, dev0-ch1, dev1-ch0, dev1-ch1 motor_map = { ("dev0", 0): [1, 2, 3, 4], ("dev0", 1): [1, 2, 3, 4], ("dev1", 0): [1, 2, 3, 4], ("dev1", 1): [1, 2, 3, 4], } # ========= 打开两块设备(每块一次性打开2路channel) ========= bus0 = open_device_bus(device_idx=0, bitrate=bitrate) bus1 = open_device_bus(device_idx=1, bitrate=bitrate) # 过滤:只收我们关心的 arb_id(两设备相同过滤即可) def set_filters_for(bus: can.BusABC, all_motor_ids: List[int]): try: bus.set_filters([{"can_id": _arb_id_single(i), "can_mask": 0x7FF, "extended": False} for i in all_motor_ids]) except Exception: pass all_ids_dev0 = sorted(set(motor_map[("dev0", 0)] + motor_map[("dev0", 1)])) all_ids_dev1 = sorted(set(motor_map[("dev1", 0)] + motor_map[("dev1", 1)])) set_filters_for(bus0, all_ids_dev0) set_filters_for(bus1, all_ids_dev1) # ========= 接收缓存:每个设备一个 Notifier,一个 cache(按 channel 分流) ========= cache0 = RxCache4Ch({0: motor_map[("dev0", 0)], 1: motor_map[("dev0", 1)]}) cache1 = RxCache4Ch({0: motor_map[("dev1", 0)], 1: motor_map[("dev1", 1)]}) notifier0 = can.Notifier(bus0, [cache0], timeout=0.01) notifier1 = can.Notifier(bus1, [cache1], timeout=0.01) # ========= 发送线程:4个逻辑channel ========= stop_event = threading.Event() tick_event = threading.Event() barrier = threading.Barrier(4 + 1) # 4个sender + 主线程 bus0_lock = threading.Lock() bus1_lock = threading.Lock() sender_dev0_ch0 = SenderThread("sender-dev0-ch0", bus0, bus0_lock, 0, tick_event, barrier, stop_event) sender_dev0_ch1 = SenderThread("sender-dev0-ch1", bus0, bus0_lock, 1, tick_event, barrier, stop_event) sender_dev1_ch0 = SenderThread("sender-dev1-ch0", bus1, bus1_lock, 0, tick_event, barrier, stop_event) sender_dev1_ch1 = SenderThread("sender-dev1-ch1", bus1, bus1_lock, 1, tick_event, barrier, stop_event) senders = [sender_dev0_ch0, sender_dev0_ch1, sender_dev1_ch0, sender_dev1_ch1] for th in senders: th.start() # ========= 你的控制输出:这里演示固定 torque ========= def torque_for(tag: str, ch: int) -> Tuple[int, int, int, int]: # TODO:替换为你的控制算法输出 return (30, 30, 30, 30) k = 0 next_tick = time.perf_counter() try: while True: # 1) 写入4路 torque(主线程写共享变量) sender_dev0_ch0.torque = torque_for("dev0", 0) sender_dev0_ch1.torque = torque_for("dev0", 1) sender_dev1_ch0.torque = torque_for("dev1", 0) sender_dev1_ch1.torque = torque_for("dev1", 1) # 2) 同步tick发布 + barrier放行(尽量同时发送) tick_event.set() try: barrier.wait(timeout=0.05) except threading.BrokenBarrierError: pass tick_event.clear() # 3) 打印(每100ms一次) if k % int(hz * 0.1) == 0: print(f"\n==== tick {k} t={time.time():.3f} ====") snap0 = cache0.snapshot() print("[dev0-ch0]") for mid in motor_map[("dev0", 0)]: print(" " + dump_frame("[dev0-ch0]", mid, snap0[(0, mid)])) print("[dev0-ch1]") for mid in motor_map[("dev0", 1)]: print(" " + dump_frame("[dev0-ch1]", mid, snap0[(1, mid)])) snap1 = cache1.snapshot() print("[dev1-ch0]") for mid in motor_map[("dev1", 0)]: print(" " + dump_frame("[dev1-ch0]", mid, snap1[(0, mid)])) print("[dev1-ch1]") for mid in motor_map[("dev1", 1)]: print(" " + dump_frame("[dev1-ch1]", mid, snap1[(1, mid)])) k += 1 # 4) 500Hz 固定周期(绝对时间推进) next_tick += dt now = time.perf_counter() if next_tick > now: time.sleep(next_tick - now) else: next_tick = now except KeyboardInterrupt: pass finally: # 停止发送线程 stop_event.set() tick_event.set() for th in senders: th.join(timeout=0.5) # 停止 Notifier(避免你之前那种 interpreter shutdown 崩溃) try: notifier0.stop() notifier1.stop() except Exception: pass # 安全清零力矩(给每个设备两个channel都发一次) try: for ch in [0, 1]: m = build_torque_broadcast(0, 0, 0, 0) m.channel = ch with bus0_lock: bus0.send(m) with bus1_lock: bus1.send(m) except Exception: pass try: bus0.shutdown() bus1.shutdown() except Exception: pass if __name__ == "__main__": main()