311 lines
14 KiB
Python
311 lines
14 KiB
Python
"""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)
|