Files
mujoco_linkerbot/tools/hand_curve_recorder_standalone.py
2026-07-23 17:55:52 +08:00

242 lines
7.6 KiB
Python
Executable File
Raw Permalink 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.
#!/usr/bin/env python3
"""手部曲线录制 + 画图(单文件,仿真/真机通用)。
依赖(两边都要):
- 已 source ROS2jazzy
- pip: numpy matplotlib
- ros: rclpy sensor_msgs std_msgs
用法示例:
python3 hand_curve_recorder_standalone.py --hand left --channel 3 --label sim
# 对端发 /cb_left_hand_control_cmd
# Ctrl+C → 当前目录下 reports/hand_curves/ 生成 csv + png
"""
from __future__ import annotations
import argparse
import csv
import os
import time
from datetime import datetime
from pathlib import Path
import numpy as np
CHANNEL_NAMES = [
"thumb_bend",
"thumb_yaw",
"index",
"middle",
"ring",
"pinky",
]
def _pad(seq, n, fill=float("nan")):
out = [fill] * n
for i, v in enumerate(list(seq)[:n]):
out[i] = float(v)
return out
def plot_curves(rows, ch, ch_name, label, have_state, have_current, png_path):
import matplotlib
matplotlib.use("Agg")
import matplotlib.pyplot as plt
t = np.array([r["t_s"] for r in rows], dtype=float)
cmd = np.array([r["cmd"][ch] for r in rows], dtype=float)
joint = np.array([r["joint"][ch] for r in rows], dtype=float)
current = np.array([r["current"][ch] for r in rows], dtype=float)
y = joint.copy()
if np.all(np.isnan(y)):
y = cmd.copy()
v = np.gradient(y, t)
if len(v) >= 5:
v = np.convolve(v, np.ones(5) / 5.0, mode="same")
fig, axes = plt.subplots(3, 1, figsize=(10, 8), sharex=True)
src = "joint" if have_state else "cmd(as joint)"
fig.suptitle(
f"recorder ch={ch}:{ch_name} label={label} src={src}",
fontsize=12,
)
axes[0].plot(t, cmd, "k--", lw=1.2, label="command_u8")
axes[0].plot(t, joint, "C0", lw=1.6, label="joint_u8")
axes[0].set_ylabel("position (0-255)")
axes[0].legend(loc="best")
axes[0].grid(True, alpha=0.3)
axes[1].plot(t, v, "C1", lw=1.6, label="d(joint)/dt")
axes[1].set_ylabel("velocity (u8/s)")
axes[1].legend(loc="best")
axes[1].grid(True, alpha=0.3)
if have_current:
axes[2].plot(t, current, "C3", lw=1.6, label="current/effort")
else:
axes[2].text(
0.5,
0.5,
"no current / state.effort",
ha="center",
va="center",
transform=axes[2].transAxes,
)
axes[2].set_ylabel("current / effort")
axes[2].set_xlabel("time (s)")
axes[2].legend(loc="best")
axes[2].grid(True, alpha=0.3)
fig.tight_layout()
fig.savefig(png_path, dpi=140)
plt.close(fig)
def main():
parser = argparse.ArgumentParser(description="Hand curve recorder (ROS2, single file)")
parser.add_argument("--hand", default="left", choices=["left", "right"])
parser.add_argument("--channel", type=int, default=3, help="0拇指弯..3中指..5小指")
parser.add_argument("--label", default="run", help="sim / real / ...")
parser.add_argument("--hz", type=float, default=100.0)
parser.add_argument("--n", type=int, default=6)
parser.add_argument("--out", default="reports/hand_curves")
parser.add_argument("--cmd-topic", default="", help="空则 /cb_{hand}_hand_control_cmd")
parser.add_argument("--state-topic", default="", help="空则 /cb_{hand}_hand_state")
parser.add_argument(
"--current-topic",
default="",
help="可选 Float32MultiArray;空则用 state.effort",
)
parser.add_argument(
"--no-cmd-as-joint",
action="store_true",
help="没有 state 时不要用 cmd 代替 joint",
)
args = parser.parse_args()
os.environ.setdefault("MPLCONFIGDIR", str(Path.cwd() / ".mplconfig"))
Path(os.environ["MPLCONFIGDIR"]).mkdir(parents=True, exist_ok=True)
import rclpy
from rclpy.node import Node
from sensor_msgs.msg import JointState
from std_msgs.msg import Float32MultiArray
cmd_topic = args.cmd_topic or f"/cb_{args.hand}_hand_control_cmd"
state_topic = args.state_topic or f"/cb_{args.hand}_hand_state"
out_dir = Path(args.out).expanduser()
if not out_dir.is_absolute():
out_dir = Path.cwd() / out_dir
out_dir.mkdir(parents=True, exist_ok=True)
n = args.n
ch = args.channel
use_cmd_as_joint = not args.no_cmd_as_joint
class Recorder(Node):
def __init__(self):
super().__init__("hand_curve_recorder_py")
self.cmd = [float("nan")] * n
self.joint = [float("nan")] * n
self.current = [float("nan")] * n
self.have_state = False
self.have_current = False
self.rows = []
self.t0 = time.perf_counter()
self.create_subscription(JointState, cmd_topic, self.on_cmd, 50)
self.create_subscription(JointState, state_topic, self.on_state, 50)
if args.current_topic:
self.create_subscription(
Float32MultiArray, args.current_topic, self.on_current, 50
)
self.create_timer(1.0 / max(1.0, args.hz), self.on_timer)
self.get_logger().info(
f"recording | cmd={cmd_topic} | state={state_topic} | "
f"current={args.current_topic or 'state.effort'} | "
f"ch={ch} | out={out_dir}"
)
self.get_logger().info("Ctrl+C to stop and plot")
def on_cmd(self, msg: JointState):
self.cmd = _pad(msg.position, n)
def on_state(self, msg: JointState):
self.have_state = True
self.joint = _pad(msg.position, n)
if msg.effort:
self.have_current = True
self.current = _pad(msg.effort, n)
def on_current(self, msg: Float32MultiArray):
self.have_current = True
self.current = _pad(msg.data, n)
def on_timer(self):
joint = list(self.joint)
if (not self.have_state) and use_cmd_as_joint:
joint = list(self.cmd)
self.rows.append(
{
"t_s": time.perf_counter() - self.t0,
"cmd": list(self.cmd),
"joint": joint,
"current": list(self.current),
}
)
rclpy.init()
node = Recorder()
try:
rclpy.spin(node)
except KeyboardInterrupt:
pass
rows = node.rows
have_state = node.have_state
have_current = node.have_current
node.destroy_node()
if rclpy.ok():
rclpy.shutdown()
if not rows:
print("no samples recorded")
return
ch_name = CHANNEL_NAMES[ch] if 0 <= ch < len(CHANNEL_NAMES) else f"ch{ch}"
stamp = datetime.now().strftime("%Y%m%d_%H%M%S")
stem = f"{args.label}_{ch_name}_{stamp}"
csv_path = out_dir / f"{stem}.csv"
png_path = out_dir / f"{stem}_qvt.png"
with csv_path.open("w", newline="") as f:
w = csv.writer(f)
header = ["t_s"]
for i in range(n):
name = CHANNEL_NAMES[i] if i < len(CHANNEL_NAMES) else f"ch{i}"
header += [f"cmd_{name}", f"joint_{name}", f"current_{name}"]
w.writerow(header)
for row in rows:
line = [f"{row['t_s']:.6f}"]
for i in range(n):
line += [
f"{row['cmd'][i]:.6f}",
f"{row['joint'][i]:.6f}",
f"{row['current'][i]:.6f}",
]
w.writerow(line)
plot_curves(rows, ch, ch_name, args.label, have_state, have_current, png_path)
print(f"CSV -> {csv_path} ({len(rows)} samples)")
print(f"plot -> {png_path}")
print(f"state={'yes' if have_state else 'no'} current={'yes' if have_current else 'no'}")
if __name__ == "__main__":
main()