Skip to content

Commit 3bc1ff0

Browse files
committed
examples: pose_recorder (space=torque toggle, s=save) + pose_sender (send pose by id)
1 parent 7f0b2ba commit 3bc1ff0

2 files changed

Lines changed: 194 additions & 0 deletions

File tree

examples/pose_recorder.py

Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
"""Record head poses by hand.
2+
3+
Workflow:
4+
- SPACE toggles torque. Torque OFF -> move the head by hand; torque ON ->
5+
it holds where you left it.
6+
- S saves the current pose to a JSON file, printing its numeric ID.
7+
- Q (or Ctrl-C) quits.
8+
9+
Saved poses accumulate in the file (IDs 1, 2, 3, ...) so you can record a few,
10+
note which ID is which, then replay any of them with pose_sender.py.
11+
12+
Note:
13+
The daemon must be running. By default this connects to the Lite robot on
14+
localhost; pass --robot wireless to reach reachy-mini.local.
15+
"""
16+
17+
import argparse
18+
import json
19+
import sys
20+
import termios
21+
import tty
22+
from pathlib import Path
23+
24+
import numpy as np
25+
26+
from reachy_mini import ReachyMini
27+
28+
29+
def read_key() -> str:
30+
"""Read a single keypress from the terminal (raw mode)."""
31+
fd = sys.stdin.fileno()
32+
old = termios.tcgetattr(fd)
33+
try:
34+
tty.setraw(fd)
35+
ch = sys.stdin.read(1)
36+
finally:
37+
termios.tcsetattr(fd, termios.TCSADRAIN, old)
38+
return ch
39+
40+
41+
def connect(robot: str) -> ReachyMini:
42+
"""Connect to the Lite (localhost) or wireless (reachy-mini.local) robot."""
43+
if robot == "wireless":
44+
return ReachyMini(
45+
media_backend="no_media",
46+
connection_mode="network",
47+
host="reachy-mini.local",
48+
)
49+
return ReachyMini(media_backend="no_media", connection_mode="localhost_only")
50+
51+
52+
def next_id(poses: list[dict]) -> int:
53+
"""Next sequential ID (1-based)."""
54+
return max((p["id"] for p in poses), default=0) + 1
55+
56+
57+
def main() -> None:
58+
"""Run the interactive pose recorder."""
59+
parser = argparse.ArgumentParser(description="Record head poses by hand.")
60+
parser.add_argument(
61+
"--robot",
62+
choices=["lite", "wireless"],
63+
default="lite",
64+
help="Which robot to connect to (default: lite / localhost).",
65+
)
66+
parser.add_argument(
67+
"--file",
68+
default="recorded_poses.json",
69+
help="JSON file to append poses to (default: recorded_poses.json).",
70+
)
71+
args = parser.parse_args()
72+
73+
path = Path(args.file).resolve()
74+
poses = json.loads(path.read_text()) if path.exists() else []
75+
76+
print(f"Saving to {path}")
77+
print("SPACE = toggle torque | S = save pose | Q = quit")
78+
79+
with connect(args.robot) as mini:
80+
torque_on = True
81+
mini.enable_motors()
82+
print("Torque ON (holding). Press SPACE to release and move the head.")
83+
try:
84+
while True:
85+
key = read_key()
86+
if key in ("q", "\x03"): # q or Ctrl-C
87+
break
88+
if key == " ":
89+
torque_on = not torque_on
90+
if torque_on:
91+
mini.enable_motors()
92+
print("Torque ON (holding current pose)")
93+
else:
94+
mini.disable_motors()
95+
print("Torque OFF (move the head by hand)")
96+
elif key in ("s", "S"):
97+
head = np.array(mini.get_current_head_pose(), dtype=float)
98+
antennas = list(mini.get_present_antenna_joint_positions())
99+
pid = next_id(poses)
100+
poses.append(
101+
{
102+
"id": pid,
103+
"head": head.tolist(),
104+
"antennas": antennas,
105+
}
106+
)
107+
path.write_text(json.dumps(poses, indent=2))
108+
print(
109+
f"Saved pose {pid}: "
110+
f"pos={np.round(head[:3, 3], 4).tolist()} "
111+
f"antennas={np.round(antennas, 3).tolist()}"
112+
)
113+
finally:
114+
mini.enable_motors()
115+
print("\nTorque ON. Bye.")
116+
117+
118+
if __name__ == "__main__":
119+
main()

examples/pose_sender.py

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
"""Send a recorded pose to a robot, then exit.
2+
3+
Reads a pose (by ID) from the JSON file written by pose_recorder.py, enables
4+
torque, moves there, and quits. Everything else the robot does (handshakes,
5+
etc.) keeps working after.
6+
7+
Note:
8+
The daemon must be running. By default this connects to the Lite robot on
9+
localhost; pass --robot wireless to reach reachy-mini.local (or run this
10+
script directly on the wireless robot, where localhost is the wireless one).
11+
"""
12+
13+
import argparse
14+
import json
15+
import sys
16+
from pathlib import Path
17+
18+
import numpy as np
19+
20+
from reachy_mini import ReachyMini
21+
22+
23+
def connect(robot: str) -> ReachyMini:
24+
"""Connect to the Lite (localhost) or wireless (reachy-mini.local) robot."""
25+
if robot == "wireless":
26+
return ReachyMini(
27+
media_backend="no_media",
28+
connection_mode="network",
29+
host="reachy-mini.local",
30+
)
31+
return ReachyMini(media_backend="no_media", connection_mode="localhost_only")
32+
33+
34+
def main() -> None:
35+
"""Send one recorded pose to the robot and exit."""
36+
parser = argparse.ArgumentParser(description="Send a recorded pose to a robot.")
37+
parser.add_argument("id", type=int, help="Pose ID to send (see pose_recorder.py).")
38+
parser.add_argument(
39+
"--robot",
40+
choices=["lite", "wireless"],
41+
default="lite",
42+
help="Which robot to send to (default: lite / localhost).",
43+
)
44+
parser.add_argument(
45+
"--file",
46+
default="recorded_poses.json",
47+
help="JSON file of recorded poses (default: recorded_poses.json).",
48+
)
49+
parser.add_argument(
50+
"--duration", type=float, default=1.0, help="Move duration in seconds."
51+
)
52+
parser.add_argument(
53+
"--no-antennas", action="store_true", help="Send only the head pose."
54+
)
55+
args = parser.parse_args()
56+
57+
path = Path(args.file).resolve()
58+
if not path.exists():
59+
sys.exit(f"No pose file at {path}")
60+
poses = {p["id"]: p for p in json.loads(path.read_text())}
61+
if args.id not in poses:
62+
sys.exit(f"No pose with ID {args.id} in {path}. Available: {sorted(poses)}")
63+
64+
pose = poses[args.id]
65+
head = np.array(pose["head"], dtype=float)
66+
antennas = None if args.no_antennas else pose.get("antennas")
67+
68+
with connect(args.robot) as mini:
69+
mini.enable_motors()
70+
mini.goto_target(head=head, antennas=antennas, duration=args.duration)
71+
print(f"Sent pose {args.id} to {args.robot}.")
72+
73+
74+
if __name__ == "__main__":
75+
main()

0 commit comments

Comments
 (0)