Files
RC_WheelLeg/05_software/real/sim2real/interface/motor_driver.py
T

311 lines
14 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""RobStride 电机驱动包装。
职责:
- 封装 ik_real 中 RobStrideDriver 的 enable/disable/clear/control_mit 调用
- **真实的丢包检测**:旧版用「value=0 启发式」会误判(电机回机械零位时也是 0)。
新方案:
1. 调用 process_messages 前快照所有电机的 (pos, vel, torque)
2. 调用后比较:状态变了 → 这一帧有新反馈;状态完全没变 → 累计 stale_count
3. stale_count 超过阈值才沿用上一帧(方法论 3.4.2)
仍然不完美(电机长时间静止确实会有连续多帧 state 不变),但比 0 启发式可靠。
- 通过 driver_factory 由调用方注入:远程 Linux 主机用 RobStrideDriver
本地 Windows 调试可用 Mock。
"""
from dataclasses import dataclass
import threading
from typing import Callable, Dict, List, Optional, Tuple
import numpy as np
from interface.motor_mapping import MotorMapping
@dataclass
class MotorReading:
position: float
velocity: float
torque: float = 0.0
fresh: bool = False # True 表示本帧驱动板有新反馈
class HardwareIO:
"""统一的电机+IMU总线接口(不含策略),主控调用这一层。
Args:
driver_factory: () -> (drv1, drv2),由调用方注入;返回的对象需要满足:
connect()/disconnect()/disable(name)/enable(name)/clear_warnings(name)
add_motor(name, mid, model)/process_messages()
control_mit(name, q, dq, kp, kd, tau)
.motors: dict[name -> motor], motor.state.position / .velocity / .torque
config: yaml 解析后的字典
"""
def __init__(self, driver_factory: Callable[[str, str, bool], Tuple[object, object]],
motor_model: str, can1_port: str, can2_port: str, debug: bool = False,
stale_frames_to_holdover: int = 2):
self.mapper = MotorMapping()
drv1, drv2 = driver_factory(can1_port, can2_port, debug)
self.driver_can1 = drv1
self.driver_can2 = drv2
self.motor_model = motor_model
self.stale_frames_to_holdover = stale_frames_to_holdover
# 上一帧反馈(按 (bus, can_id) 索引),用于丢包兜底
self._last_pos: Dict[Tuple[int, int], float] = {}
self._last_vel: Dict[Tuple[int, int], float] = {}
self._last_torque: Dict[Tuple[int, int], float] = {}
# 每个电机连续多少帧没收到新反馈
self._stale_counts: Dict[Tuple[int, int], int] = {}
# 第一次必须读到才能解锁,避免初始化时直接用零位发送大力矩
self._initialized = False
self.lock = threading.Lock()
# 累计诊断
self.holdover_total = 0 # 累计被沿用上一帧的次数
# ---- 总线管理 ----
def connect(self):
self.driver_can1.connect()
self.driver_can2.connect()
for jk in self.mapper.SIM_JOINT_ORDER:
leg, joint = jk
bus, mid = self.mapper.CAN_ID_MAP[jk]
name = f"{leg}_{joint}"
drv = self.driver_can1 if bus == 1 else self.driver_can2
drv.add_motor(name, mid, self.motor_model)
self._stale_counts[(bus, mid)] = 0
def disconnect(self):
try:
self.driver_can1.disconnect()
finally:
self.driver_can2.disconnect()
def enable_all(self):
for drv in (self.driver_can1, self.driver_can2):
for name in drv.motors:
drv.clear_warnings(name)
drv.enable(name)
def disable_all(self):
for drv in (self.driver_can1, self.driver_can2):
for name in drv.motors:
drv.disable(name)
# ---- 状态读取 ----
def _snapshot_state(self) -> Dict[Tuple[int, int], Tuple[float, float, float, int]]:
"""快照所有电机的 (pos, vel, torque, update_count)process_messages 前后比较即可判 fresh。"""
snap: Dict[Tuple[int, int], Tuple[float, float, float, int]] = {}
for drv_idx, drv in enumerate((self.driver_can1, self.driver_can2)):
bus = drv_idx + 1
for name, motor in drv.motors.items():
parts = name.split("_", 1)
if len(parts) != 2:
continue
key = (parts[0], parts[1])
if key not in self.mapper.CAN_ID_MAP:
continue
_, mid = self.mapper.CAN_ID_MAP[key]
s = motor.state
snap[(bus, mid)] = (s.position, s.velocity, s.torque, getattr(s, "update_count", 0))
return snap
def read_state(self) -> Tuple[np.ndarray, np.ndarray, np.ndarray, Dict[str, object]]:
"""返回 (sim_joint_pos[16], sim_joint_vel[16], sim_joint_torque[16], debug_info)。"""
with self.lock:
# 1) 抓取上一次的状态作为「pre」快照(基线)
pre = self._snapshot_state()
# 2) 拉取本帧反馈
self.driver_can1.process_messages()
self.driver_can2.process_messages()
# 3) 抓取「post」快照
post = self._snapshot_state()
# 4) 比较:state 元组变了 → 本帧有新反馈,stale_count 清零;否则 stale_count++
per_motor_fresh: Dict[Tuple[int, int], bool] = {}
for key in post:
fresh = (pre.get(key) != post[key])
per_motor_fresh[key] = fresh
if fresh:
self._stale_counts[key] = 0
else:
self._stale_counts[key] += 1
# 5) 取出本帧 pos/vel;若该电机连续多帧没刷新,沿用上一帧(方法论 3.4.2)
real_pos: Dict[Tuple[int, int], float] = {}
real_vel: Dict[Tuple[int, int], float] = {}
real_torque: Dict[Tuple[int, int], float] = {}
holdover_this_frame = 0
for key, (pos, vel, tor, _) in post.items():
if (not per_motor_fresh[key]) and self._stale_counts[key] >= self.stale_frames_to_holdover:
# 长时间不刷新视作丢包:沿用上一帧
if key in self._last_pos:
real_pos[key] = self._last_pos[key]
real_vel[key] = self._last_vel[key]
real_torque[key] = self._last_torque[key]
holdover_this_frame += 1
else:
real_pos[key] = pos
real_vel[key] = vel
real_torque[key] = tor
else:
real_pos[key] = pos
real_vel[key] = vel
real_torque[key] = tor
self.holdover_total += holdover_this_frame
# 缓存本帧(即便部分是 holdover 也缓存)
self._last_pos = real_pos.copy()
self._last_vel = real_vel.copy()
self._last_torque = real_torque.copy()
if not self._initialized:
self._initialized = True
cur_pos = self.mapper.real_to_sim(real_pos)
cur_vel = self.mapper.real_vel_to_sim(real_vel)
cur_torque = self.mapper.real_vel_to_sim(real_torque)
# 诊断信息
stale_max = max(self._stale_counts.values()) if self._stale_counts else 0
n_stale_motors = sum(1 for c in self._stale_counts.values()
if c >= self.stale_frames_to_holdover)
# 按 SIM_JOINT_ORDER 排列的每个电机连续丢帧数
per_motor_stale = [
self._stale_counts.get(self.mapper.CAN_ID_MAP[jk], 99)
for jk in self.mapper.SIM_JOINT_ORDER
]
return cur_pos, cur_vel, cur_torque, {
"holdover_this_frame": holdover_this_frame,
"stale_max": stale_max,
"n_stale_motors": n_stale_motors,
"fresh_count": sum(1 for v in per_motor_fresh.values() if v),
"per_motor_stale": per_motor_stale,
}
def passive_poll(self):
"""发送全 0 (0刚度0阻尼0力矩) 的 MIT 指令给所有电机。
目的:在 ENABLED 状态下,不产生力矩地索要反馈(因为 RobStride 在 MIT 模式下必须有指令才反馈)。"""
with self.lock:
for jk in self.mapper.SIM_JOINT_ORDER:
bus, mid = self.mapper.CAN_ID_MAP[jk]
name = f"{jk[0]}_{jk[1]}"
drv = self.driver_can1 if bus == 1 else self.driver_can2
if name in drv.motors:
drv.control_mit(name, 0.0, 0.0, 0.0, 0.0, 0.0)
# ---- 控制下发 ----
def send_control(self, target_angles: np.ndarray, kp_leg: float, kd_leg: float,
kd_wheel: float):
"""与 sim2sim 的 PD 模型对齐:
- 腿: position 控制,目标角度由 target_angles[:12] 给出,kp/kd 来自配置
- 轮: velocity 控制,目标速度由 target_angles[12:] 给出,kd 阻尼
"""
with self.lock:
if target_angles.shape != (16,):
raise ValueError("target_angles must be (16,)")
real_targets = self.mapper.sim_to_real(target_angles.astype(np.float32))
# 轮毂速度目标暂且用 0,如果 target_angles 里包含了速度,就在 policy 那里处理,
# 这里的 target_angles 是 pose 目标,轮毂作为连续旋转关节其实位置控制没有意义。
# 为了兼容旧代码,这里构造一个 16 维的 velocity array,只有后 4 个是目标(如果当作速度的话)。
vel_targets = np.zeros(16, dtype=np.float32)
vel_targets[12:] = target_angles[12:].astype(np.float32)
real_wheel = self.mapper.sim_vel_to_real(vel_targets)
for jk in self.mapper.SIM_JOINT_ORDER:
leg, joint = jk
bus, mid = self.mapper.CAN_ID_MAP[jk]
name = f"{leg}_{joint}"
drv = self.driver_can1 if bus == 1 else self.driver_can2
if name not in drv.motors:
continue
if joint == "wheel":
v = real_wheel[(bus, mid)]
drv.control_mit(name, 0.0, v, 0.0, kd_wheel, 0.0)
else:
q = real_targets[(bus, mid)]
drv.control_mit(name, q, 0.0, kp_leg, kd_leg, 0.0)
def damping_brake(self, kd_leg: float, kd_wheel: float):
"""急停模式:所有关节卸载刚度,仅保留阻尼。
对应 270_SimToReal 方法论 97.11 Level 2 "刹车"。
"""
with self.lock:
for jk in self.mapper.SIM_JOINT_ORDER:
leg, joint = jk
bus, _ = self.mapper.CAN_ID_MAP[jk]
name = f"{leg}_{joint}"
drv = self.driver_can1 if bus == 1 else self.driver_can2
if name not in drv.motors:
continue
kd = kd_wheel if joint == "wheel" else kd_leg
drv.control_mit(name, 0.0, 0.0, 0.0, kd, 0.0)
def wait_feedback_ready(self, max_attempts: int = 20,
poll_interval: float = 0.05) -> Tuple[bool, list]:
"""enable 后调用:尝试 max_attempts 次读总线,等所有 16 个电机
都至少给出一帧反馈。
返回 (all_ready, missing_motors)missing_motors 是 (bus, mid, name) 列表。
"""
import time
seen: Dict[Tuple[int, int], bool] = {
self.mapper.CAN_ID_MAP[jk]: False for jk in self.mapper.SIM_JOINT_ORDER
}
# 用第一次读到的 (pos, vel, torque) 三元组的"非零"或"已变化"作为反馈到达的判据。
# 启动瞬间所有 motor.state 默认全 0,要么收到反馈让其变化,要么收到反馈但值确实是 0。
# 退化情况下电机静止时 vel=0 且 pos=机械零位也=0,那种情况只能等多帧确认。
snap_prev = self._snapshot_state()
for attempt in range(max_attempts):
with self.lock:
self.driver_can1.process_messages()
self.driver_can2.process_messages()
snap_cur = self._snapshot_state()
for key, fields_cur in snap_cur.items():
if seen[key]:
continue
fields_prev = snap_prev.get(key)
# 任一字段不为 0 → 一定有反馈(因为初始值都是 0)
if any(v != 0.0 for v in fields_cur):
seen[key] = True
# 与上一次快照不同 → 一定有反馈(即便都很小)
elif fields_prev is not None and fields_cur != fields_prev:
seen[key] = True
snap_prev = snap_cur
if all(seen.values()):
return True, []
time.sleep(poll_interval)
# 超时:列出仍未反馈的电机
missing = []
rev_can = {v: k for k, v in self.mapper.CAN_ID_MAP.items()}
for key, ok in seen.items():
if not ok:
leg, joint = rev_can[key]
missing.append((key[0], key[1], f"{leg}_{joint}"))
return False, missing
def read_measured_pose(self) -> np.ndarray:
"""返回 (16,) 当前实测 sim 坐标系下的关节位置。
会先 process_messages 一次保证拿到本帧。
"""
self.driver_can1.process_messages()
self.driver_can2.process_messages()
real_pos: Dict[Tuple[int, int], float] = {}
for drv_idx, drv in enumerate((self.driver_can1, self.driver_can2)):
bus = drv_idx + 1
for name, motor in drv.motors.items():
parts = name.split("_", 1)
if len(parts) != 2:
continue
key = (parts[0], parts[1])
if key not in self.mapper.CAN_ID_MAP:
continue
_, mid = self.mapper.CAN_ID_MAP[key]
real_pos[(bus, mid)] = motor.state.position
return self.mapper.real_to_sim(real_pos)