698 lines
21 KiB
Python
698 lines
21 KiB
Python
#!/usr/bin/env python3
|
|
"""
|
|
g1_mode_controller.py
|
|
|
|
Interactive terminal controller for Unitree G1 locomotion + basic upper body (waist/arms).
|
|
|
|
Key features:
|
|
- Menu is generated from a single OPTIONS dictionary (easy to extend).
|
|
- Ctrl+C / Quit = StopMove only (keep current posture; NO prep/damp).
|
|
- Status prints LIVE joints (deg) from LowState.
|
|
- WALK/RUN teleop uses the "old working" keyboard control (pynput).
|
|
- PREP mode = stand + balance (NO Start/FSM200).
|
|
- READY/START mode = PREP + Start (FSM 200).
|
|
- StandUp/Squat/Sit/Lie-to-Stand use SetFsmId() fallback for compatibility.
|
|
|
|
Run:
|
|
python3 g1_mode_controller.py --iface enp3s0
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
import sys
|
|
import time
|
|
from dataclasses import dataclass
|
|
from typing import Optional, Dict, Tuple, Callable, Any
|
|
from threading import Lock, Event
|
|
from collections import OrderedDict
|
|
|
|
# ---------- Unitree SDK2 ----------
|
|
from unitree_sdk2py.core.channel import ChannelFactoryInitialize, ChannelSubscriber
|
|
from unitree_sdk2py.g1.loco.g1_loco_client import LocoClient
|
|
from unitree_sdk2py.idl.unitree_hg.msg.dds_ import LowState_ # type: ignore
|
|
from unitree_sdk2py.comm.motion_switcher.motion_switcher_client import MotionSwitcherClient
|
|
|
|
# Optional teleop keyboard:
|
|
try:
|
|
from pynput.keyboard import Listener, Key, KeyCode # type: ignore
|
|
HAVE_PYNPUT = True
|
|
except Exception:
|
|
HAVE_PYNPUT = False
|
|
|
|
|
|
# ---------------- Joint map (for status) ----------------
|
|
JOINT_NAMES = {
|
|
12: "WAIST_YAW",
|
|
13: "WAIST_ROLL",
|
|
14: "WAIST_PITCH",
|
|
15: "L_SHOULDER_PITCH",
|
|
16: "L_SHOULDER_ROLL",
|
|
17: "L_SHOULDER_YAW",
|
|
18: "L_ELBOW",
|
|
19: "L_WRIST_ROLL",
|
|
20: "L_WRIST_PITCH",
|
|
21: "L_WRIST_YAW",
|
|
22: "R_SHOULDER_PITCH",
|
|
23: "R_SHOULDER_ROLL",
|
|
24: "R_SHOULDER_YAW",
|
|
25: "R_ELBOW",
|
|
26: "R_WRIST_ROLL",
|
|
27: "R_WRIST_PITCH",
|
|
28: "R_WRIST_YAW",
|
|
}
|
|
|
|
|
|
def rad2deg(x: float) -> float:
|
|
return float(x) * 180.0 / 3.141592653589793
|
|
|
|
|
|
# ---------------- Live State Monitor ----------------
|
|
|
|
class LiveStateMonitor:
|
|
"""Subscribes LowState and keeps last joint positions."""
|
|
def __init__(self):
|
|
self.lock = Lock()
|
|
self.last_q_rad: Dict[int, float] = {}
|
|
self.msg_count = 0
|
|
self.last_rx_time = 0.0
|
|
self.first_msg_evt = Event()
|
|
|
|
def cb(self, msg: LowState_):
|
|
motor_state = getattr(msg, "motor_state", None)
|
|
if motor_state is None:
|
|
return
|
|
|
|
with self.lock:
|
|
self.msg_count += 1
|
|
self.last_rx_time = time.time()
|
|
|
|
for idx in JOINT_NAMES.keys():
|
|
try:
|
|
self.last_q_rad[idx] = float(motor_state[idx].q)
|
|
except Exception:
|
|
pass
|
|
|
|
self.first_msg_evt.set()
|
|
|
|
def snapshot_deg(self) -> Tuple[int, float, Dict[int, float]]:
|
|
with self.lock:
|
|
age = (time.time() - self.last_rx_time) if self.last_rx_time else 1e9
|
|
qdeg = {i: rad2deg(q) for i, q in self.last_q_rad.items()}
|
|
return self.msg_count, age, qdeg
|
|
|
|
|
|
# ---------------- Upper body (waist/arms) publisher ----------------
|
|
|
|
@dataclass
|
|
class ArmRig:
|
|
pub: object
|
|
cmd: object
|
|
crc: object
|
|
waist_idx: int = 12
|
|
|
|
def set_joint(self, idx: int, q: float, kp: float = 60.0, kd: float = 1.5, tau: float = 0.0):
|
|
mc = self.cmd.motor_cmd[idx]
|
|
mc.q = float(q)
|
|
mc.dq = 0.0
|
|
mc.tau = float(tau)
|
|
mc.kp = float(kp)
|
|
mc.kd = float(kd)
|
|
|
|
def send(self):
|
|
self.cmd.crc = self.crc.Crc(self.cmd)
|
|
self.pub.Write(self.cmd)
|
|
|
|
|
|
def try_init_arm_sdk() -> Optional[ArmRig]:
|
|
try:
|
|
from unitree_sdk2py.core.channel import ChannelPublisher # type: ignore
|
|
from unitree_sdk2py.idl.unitree_hg.msg.dds_ import LowCmd_ # type: ignore
|
|
from unitree_sdk2py.idl.default import unitree_hg_msg_dds__LowCmd_ # type: ignore
|
|
from unitree_sdk2py.utils.crc import CRC # type: ignore
|
|
|
|
pub = ChannelPublisher("rt/arm_sdk", LowCmd_)
|
|
pub.Init()
|
|
|
|
cmd = unitree_hg_msg_dds__LowCmd_()
|
|
crc = CRC()
|
|
|
|
# enable arm_sdk mode
|
|
cmd.motor_cmd[29].q = 1
|
|
return ArmRig(pub=pub, cmd=cmd, crc=crc)
|
|
except Exception as e:
|
|
print("[arm_sdk] disabled:", e)
|
|
return None
|
|
|
|
|
|
# ---------------- Core helpers ----------------
|
|
|
|
def init_loco(timeout: float = 10.0) -> LocoClient:
|
|
bot = LocoClient()
|
|
bot.SetTimeout(timeout)
|
|
bot.Init()
|
|
return bot
|
|
|
|
|
|
def init_msc(timeout: float = 5.0) -> MotionSwitcherClient:
|
|
msc = MotionSwitcherClient()
|
|
msc.SetTimeout(timeout)
|
|
msc.Init()
|
|
return msc
|
|
|
|
|
|
def safe_call(name: str, fn: Callable[..., Any], *args, **kwargs) -> Any:
|
|
"""
|
|
Unitree python wrappers often return None on success.
|
|
This helper prints [OK] on no-exception; prints [ERR] on exception.
|
|
"""
|
|
try:
|
|
ret = fn(*args, **kwargs)
|
|
print(f"[OK] {name}: success (ret={ret})")
|
|
return ret
|
|
except Exception as e:
|
|
code = getattr(e, "code", None)
|
|
payload = getattr(e, "payload", None)
|
|
print(f"[ERR] {name} failed: {repr(e)} code={code} payload={payload}")
|
|
return None
|
|
|
|
|
|
def stop_only(bot: Optional[LocoClient]):
|
|
"""Stop walking/teleop velocity but do NOT change mode/posture."""
|
|
if bot is None:
|
|
return
|
|
safe_call("StopMove", bot.StopMove)
|
|
|
|
|
|
def move_cmd(bot: LocoClient, vx: float, vy: float, om: float):
|
|
"""
|
|
SDK variations exist. We try a safe call order.
|
|
"""
|
|
try:
|
|
bot.Move(vx, vy, om, True)
|
|
return
|
|
except Exception:
|
|
pass
|
|
|
|
try:
|
|
bot.Move(vx, vy, om)
|
|
return
|
|
except Exception:
|
|
pass
|
|
|
|
try:
|
|
bot.Move(vx, vy, om, continuous_move=True) # type: ignore
|
|
return
|
|
except Exception as e:
|
|
print("[ERR] Move failed in all signatures:", repr(e))
|
|
|
|
|
|
def fsm_set(bot: LocoClient, fsm_id: int, label: str):
|
|
"""
|
|
Prefer direct methods if present; fallback to SetFsmId(fsm_id).
|
|
"""
|
|
if hasattr(bot, label):
|
|
fn = getattr(bot, label)
|
|
return safe_call(label, fn)
|
|
|
|
# common fallback in some sdk builds
|
|
if hasattr(bot, "SetFsmId"):
|
|
return safe_call(f"{label} (FSM {fsm_id}) via SetFsmId({fsm_id})", bot.SetFsmId, fsm_id)
|
|
|
|
print(f"[ERR] Neither {label} nor SetFsmId exists in this SDK build.")
|
|
return None
|
|
|
|
|
|
def balance_stand(bot: LocoClient, mode: int = 0):
|
|
"""
|
|
Some builds require BalanceStand(balance_mode).
|
|
Others expose SetBalanceMode(0/1).
|
|
"""
|
|
if hasattr(bot, "BalanceStand"):
|
|
try:
|
|
return safe_call(f"BalanceStand({mode})", bot.BalanceStand, mode)
|
|
except TypeError:
|
|
return safe_call("BalanceStand()", bot.BalanceStand)
|
|
|
|
if hasattr(bot, "SetBalanceMode"):
|
|
return safe_call(f"SetBalanceMode({mode})", bot.SetBalanceMode, mode)
|
|
|
|
print("[WARN] No BalanceStand/SetBalanceMode available in this SDK.")
|
|
return None
|
|
|
|
|
|
# ---------------- PREP / READY sequences ----------------
|
|
|
|
def prep_mode(bot: LocoClient):
|
|
"""
|
|
PREP = stand + balance (NO Start/FSM200).
|
|
Matches your tested behavior:
|
|
StopMove -> Damp -> StandUp(FSM4) -> ramp stand height -> BalanceStand(0) -> set final height.
|
|
"""
|
|
print("[PREP] stand + balance (NO Start/FSM200) ...")
|
|
safe_call("StopMove", bot.StopMove)
|
|
safe_call("Damp", bot.Damp)
|
|
|
|
# StandUp FSM 4
|
|
fsm_set(bot, 4, "StandUp")
|
|
|
|
# Stand height ramp (0.02 .. 0.50)
|
|
if hasattr(bot, "SetStandHeight"):
|
|
for h in [x / 100.0 for x in range(2, 51, 2)]:
|
|
safe_call(f"SetStandHeight({h:.2f}m)", bot.SetStandHeight, float(h))
|
|
time.sleep(0.03)
|
|
|
|
# set a comfortable final height before balancing
|
|
safe_call("SetStandHeight(0.22m)", bot.SetStandHeight, 0.22)
|
|
else:
|
|
print("[WARN] bot.SetStandHeight not available in this SDK build.")
|
|
|
|
# Balance (static stand)
|
|
balance_stand(bot, mode=0)
|
|
|
|
# Re-send final height (common trick)
|
|
if hasattr(bot, "SetStandHeight"):
|
|
safe_call("SetStandHeight(0.22m)", bot.SetStandHeight, 0.22)
|
|
|
|
print("[PREP] Done. (NO Start)")
|
|
|
|
|
|
def ready_start_mode(bot: LocoClient):
|
|
"""
|
|
READY/START = PREP + Start (FSM200).
|
|
"""
|
|
print("[READY] stand + balance + start (FSM 200) ...")
|
|
prep_mode(bot)
|
|
if hasattr(bot, "Start"):
|
|
safe_call("Start (FSM 200)", bot.Start)
|
|
else:
|
|
# some builds: SetFsmId(200)
|
|
fsm_set(bot, 200, "Start")
|
|
print("[READY] Done. (FSM200 expected)")
|
|
|
|
|
|
# ---------------- Teleop (old keyboard function) ----------------
|
|
|
|
def teleop_loop(bot: LocoClient, speed_limit: float):
|
|
if not HAVE_PYNPUT:
|
|
print("[ERR] pynput not installed. Install: pip install pynput")
|
|
return
|
|
|
|
LIN_STEP = 0.05
|
|
ANG_STEP = 0.2
|
|
SEND_PERIOD = 0.10 # 10 Hz
|
|
|
|
pressed = set()
|
|
|
|
def on_press(k):
|
|
if isinstance(k, KeyCode) and k.char:
|
|
pressed.add(k.char.lower())
|
|
else:
|
|
pressed.add(k)
|
|
|
|
def on_release(k):
|
|
if isinstance(k, KeyCode) and k.char:
|
|
pressed.discard(k.char.lower())
|
|
else:
|
|
pressed.discard(k)
|
|
|
|
def key(name: str) -> bool:
|
|
if name == "space":
|
|
return Key.space in pressed
|
|
if name == "esc":
|
|
return Key.esc in pressed
|
|
return name in pressed
|
|
|
|
def clamp(v: float) -> float:
|
|
return max(-speed_limit, min(speed_limit, v))
|
|
|
|
vx = vy = om = 0.0
|
|
last_send = 0.0
|
|
|
|
print("\n--- TELEOP ---")
|
|
print("Hold keys: W/S (vx), Q/E (vy), A/D (yaw), Space (stop). ESC to exit.\n")
|
|
|
|
listener = Listener(on_press=on_press, on_release=on_release)
|
|
listener.start()
|
|
|
|
try:
|
|
while True:
|
|
if key("esc"):
|
|
stop_only(bot)
|
|
break
|
|
|
|
if key("w") and not key("s"):
|
|
vx = clamp(vx + LIN_STEP)
|
|
elif key("s") and not key("w"):
|
|
vx = clamp(vx - LIN_STEP)
|
|
else:
|
|
vx = 0.0
|
|
|
|
if key("q") and not key("e"):
|
|
vy = clamp(vy + LIN_STEP)
|
|
elif key("e") and not key("q"):
|
|
vy = clamp(vy - LIN_STEP)
|
|
else:
|
|
vy = 0.0
|
|
|
|
if key("a") and not key("d"):
|
|
om = clamp(om + ANG_STEP)
|
|
elif key("d") and not key("a"):
|
|
om = clamp(om - ANG_STEP)
|
|
else:
|
|
om = 0.0
|
|
|
|
if key("space"):
|
|
vx = vy = om = 0.0
|
|
|
|
now = time.time()
|
|
if now - last_send >= SEND_PERIOD:
|
|
# enable gait mode if supported (some SDKs require it for walking)
|
|
if hasattr(bot, "SetBalanceMode"):
|
|
try:
|
|
bot.SetBalanceMode(1)
|
|
except Exception:
|
|
pass
|
|
|
|
move_cmd(bot, vx, vy, om)
|
|
last_send = now
|
|
sys.stdout.write(f"\rvx={vx:+.2f} vy={vy:+.2f} yaw={om:+.2f} ")
|
|
sys.stdout.flush()
|
|
|
|
time.sleep(0.005)
|
|
|
|
except KeyboardInterrupt:
|
|
stop_only(bot)
|
|
print("\n--- teleop interrupted (StopMove only) ---\n")
|
|
finally:
|
|
listener.stop()
|
|
print("\n--- teleop ended ---\n")
|
|
|
|
|
|
# ---------------- Presets for arms ----------------
|
|
|
|
LEFT_DOWN = [
|
|
(12, 0.0),
|
|
(15, +0.211),
|
|
(16, +0.181),
|
|
(17, -0.284),
|
|
(18, +0.672),
|
|
(19, -0.379),
|
|
(20, -0.852),
|
|
(21, -0.019),
|
|
]
|
|
RIGHT_DOWN = [
|
|
(12, 0.0),
|
|
(22, +0.087),
|
|
(23, -0.271),
|
|
(24, +0.323),
|
|
(25, +0.691),
|
|
(26, +0.240),
|
|
(27, -0.771),
|
|
(28, -0.176),
|
|
]
|
|
|
|
def apply_pose(arm: ArmRig, pose, kp: float = 60.0, kd: float = 1.5):
|
|
for j, q in pose:
|
|
arm.set_joint(j, q, kp=kp, kd=kd, tau=0.0)
|
|
arm.send()
|
|
|
|
|
|
# ---------------- Main ----------------
|
|
|
|
def main():
|
|
ap = argparse.ArgumentParser()
|
|
ap.add_argument("--iface", required=True, help="NIC connected to G1 (example: enp3s0)")
|
|
ap.add_argument("--timeout", type=float, default=10.0)
|
|
ap.add_argument("--topic", default="", help="LowState topic override (default tries rt/lf/lowstate then rt/lowstate)")
|
|
args = ap.parse_args()
|
|
|
|
# DDS init ONCE
|
|
ChannelFactoryInitialize(0, args.iface)
|
|
|
|
# Live monitor
|
|
monitor = LiveStateMonitor()
|
|
topics = [args.topic] if args.topic else ["rt/lf/lowstate", "rt/lowstate"]
|
|
sub = None
|
|
last_err = None
|
|
for t in topics:
|
|
try:
|
|
sub = ChannelSubscriber(t, LowState_)
|
|
sub.Init(monitor.cb, 200)
|
|
print(f"[OK] LowState subscribed: {t}")
|
|
break
|
|
except Exception as e:
|
|
last_err = e
|
|
print(f"[WARN] LowState subscribe failed for {t}: {e}", file=sys.stderr)
|
|
if sub is None:
|
|
print(f"[WARN] Could not subscribe LowState. Last error: {last_err}")
|
|
|
|
# Init clients
|
|
print("Initializing LocoClient...")
|
|
bot: Optional[LocoClient] = None
|
|
try:
|
|
bot = init_loco(timeout=args.timeout)
|
|
except Exception as e:
|
|
print("[ERR] Loco init failed:", e)
|
|
|
|
print("Initializing MotionSwitcherClient...")
|
|
msc: Optional[MotionSwitcherClient] = None
|
|
try:
|
|
msc = init_msc(timeout=5.0)
|
|
status, result = msc.CheckMode()
|
|
print(f"[MSC] Current mode: {result.get('name') or '(none)'}")
|
|
except Exception as e:
|
|
print("[WARN] MotionSwitcher init failed:", e)
|
|
|
|
arm = try_init_arm_sdk()
|
|
|
|
# Snapshot on start
|
|
if sub is not None and monitor.first_msg_evt.wait(timeout=2.0):
|
|
cnt, age, qdeg = monitor.snapshot_deg()
|
|
print("\n[STATE] Current joint snapshot (deg):")
|
|
print(f" lowstate_msgs={cnt} age={age:.2f}s")
|
|
for idx in sorted(qdeg.keys()):
|
|
print(f" [{idx:02d}] {JOINT_NAMES.get(idx,'?'):<18} {qdeg[idx]:+8.2f}°")
|
|
print()
|
|
|
|
def need_bot() -> bool:
|
|
if bot is None:
|
|
print("[ERR] No LocoClient available.")
|
|
return False
|
|
return True
|
|
|
|
def need_arm() -> bool:
|
|
if arm is None:
|
|
print("[ERR] arm_sdk not available.")
|
|
return False
|
|
return True
|
|
|
|
# ----- Dictionary-driven menu -----
|
|
OPTIONS: "OrderedDict[int, Dict[str, Any]]" = OrderedDict()
|
|
|
|
OPTIONS[1] = {"label": "PREP mode (stand + balance) [NO start/FSM200]",
|
|
"fn": lambda: prep_mode(bot) if need_bot() else None}
|
|
|
|
OPTIONS[2] = {"label": "READY/START mode (stand + balance + start, FSM 200)",
|
|
"fn": lambda: ready_start_mode(bot) if need_bot() else None}
|
|
|
|
OPTIONS[3] = {"label": "ZeroTorque (FSM 0)",
|
|
"fn": lambda: fsm_set(bot, 0, "ZeroTorque") if need_bot() else None}
|
|
|
|
OPTIONS[4] = {"label": "Damp (FSM 1)",
|
|
"fn": lambda: fsm_set(bot, 1, "Damp") if need_bot() else None}
|
|
|
|
OPTIONS[5] = {"label": "StandUp (FSM 4)",
|
|
"fn": lambda: fsm_set(bot, 4, "StandUp") if need_bot() else None}
|
|
|
|
OPTIONS[6] = {"label": "Squat (FSM 2)",
|
|
"fn": lambda: fsm_set(bot, 2, "Squat") if need_bot() else None}
|
|
|
|
OPTIONS[7] = {"label": "Sit (FSM 3)",
|
|
"fn": lambda: fsm_set(bot, 3, "Sit") if need_bot() else None}
|
|
|
|
OPTIONS[8] = {"label": "LowStand",
|
|
"fn": lambda: safe_call("LowStand", bot.LowStand) if (need_bot() and hasattr(bot, "LowStand")) else print("[ERR] LowStand not available")}
|
|
|
|
OPTIONS[9] = {"label": "HighStand",
|
|
"fn": lambda: safe_call("HighStand", bot.HighStand) if (need_bot() and hasattr(bot, "HighStand")) else print("[ERR] HighStand not available")}
|
|
|
|
OPTIONS[10] = {"label": "Lie-to-Stand (FSM 702) (if supported)",
|
|
"fn": lambda: fsm_set(bot, 702, "Lie2StandUp") if need_bot() else None}
|
|
|
|
OPTIONS[11] = {"label": "Walk teleop (keyboard: W/S Q/E A/D, ESC exit)",
|
|
"fn": lambda: teleop_loop(bot, speed_limit=0.6) if need_bot() else None}
|
|
|
|
OPTIONS[12] = {"label": "Run teleop (keyboard: W/S Q/E A/D, ESC exit)",
|
|
"fn": lambda: teleop_loop(bot, speed_limit=1.2) if need_bot() else None}
|
|
|
|
OPTIONS[16] = {"label": "Status (LIVE joints)",
|
|
"fn": lambda: show_status(bot, arm, msc, sub, monitor)}
|
|
|
|
OPTIONS[17] = {"label": "MotionSwitcher: Select mode 'ai' (supported)",
|
|
"fn": lambda: msc_select_ai(msc)}
|
|
|
|
OPTIONS[18] = {"label": "MotionSwitcher: Release current mode",
|
|
"fn": lambda: msc_release(msc)}
|
|
|
|
OPTIONS[19] = {"label": "MotionSwitcher: Show current mode",
|
|
"fn": lambda: msc_show(msc)}
|
|
|
|
OPTIONS[20] = {"label": "Seated mode ON (Sit)",
|
|
"fn": lambda: fsm_set(bot, 3, "Sit") if need_bot() else None}
|
|
|
|
OPTIONS[21] = {"label": "Seated mode OFF (StandUp + PREP)",
|
|
"fn": lambda: seated_off(bot) if need_bot() else None}
|
|
|
|
OPTIONS[22] = {"label": "Reconnect (re-init Loco + MotionSwitcher)",
|
|
"fn": lambda: reconnect(args, lambda b: set_bot_ref(b), lambda m: set_msc_ref(m))}
|
|
|
|
# Helpers need access to outer bot/msc refs:
|
|
def set_bot_ref(new_bot: Optional[LocoClient]):
|
|
nonlocal bot
|
|
bot = new_bot
|
|
|
|
def set_msc_ref(new_msc: Optional[MotionSwitcherClient]):
|
|
nonlocal msc
|
|
msc = new_msc
|
|
|
|
|
|
def print_menu():
|
|
print("\n" + "=" * 70)
|
|
print("G1 MODE CONTROLLER - select option:")
|
|
print("=" * 70)
|
|
for k, v in OPTIONS.items():
|
|
print(f"{k:>3}) {v['label']}")
|
|
print(" 0) Quit (StopMove only)")
|
|
print("=" * 70)
|
|
|
|
print_menu()
|
|
|
|
while True:
|
|
try:
|
|
choice = input("\nSelect option (0-22): ").strip()
|
|
except KeyboardInterrupt:
|
|
stop_only(bot)
|
|
print("\n[EXIT] Ctrl+C → StopMove only. Keeping current posture. Bye.")
|
|
break
|
|
except EOFError:
|
|
stop_only(bot)
|
|
print("\n[EXIT] EOF → StopMove only. Keeping current posture. Bye.")
|
|
break
|
|
|
|
if not choice:
|
|
continue
|
|
|
|
if choice == "0":
|
|
stop_only(bot)
|
|
print("\n[EXIT] Quit → StopMove only. Keeping current posture. Bye.")
|
|
break
|
|
|
|
try:
|
|
opt = int(choice)
|
|
except ValueError:
|
|
print("Please enter a number.")
|
|
continue
|
|
|
|
if opt not in OPTIONS:
|
|
print("Unknown option.")
|
|
print_menu()
|
|
continue
|
|
|
|
try:
|
|
OPTIONS[opt]["fn"]()
|
|
except KeyboardInterrupt:
|
|
stop_only(bot)
|
|
print("\n[INTERRUPT] Ctrl+C → StopMove only. Keeping current posture.")
|
|
except Exception as e:
|
|
print("[ERR] Action failed:", repr(e))
|
|
|
|
print_menu()
|
|
|
|
|
|
# --------- extra action helpers that need current refs ---------
|
|
|
|
def show_status(bot, arm, msc, sub, monitor):
|
|
print("Status:")
|
|
print(f" LocoClient: {'OK' if bot is not None else 'NOT INITIALISED'}")
|
|
print(f" MotionSwitcher: {'OK' if msc is not None else 'DISABLED'}")
|
|
if msc is not None:
|
|
try:
|
|
_s, r = msc.CheckMode()
|
|
print(f" MSC mode: {r.get('name') or '(none)'}")
|
|
except Exception:
|
|
print(" MSC mode: (error reading)")
|
|
print(f" arm_sdk: {'OK' if arm is not None else 'DISABLED'}")
|
|
print(f" pynput: {'OK' if HAVE_PYNPUT else 'NOT INSTALLED'}")
|
|
|
|
if sub is None:
|
|
print(" LowState: NOT SUBSCRIBED")
|
|
return
|
|
|
|
cnt, age, qdeg = monitor.snapshot_deg()
|
|
print(f" LowState: OK msgs={cnt} age={age:.2f}s")
|
|
print(" Current joints (deg):")
|
|
for idx in sorted(qdeg.keys()):
|
|
print(f" [{idx:02d}] {JOINT_NAMES.get(idx,'?'):<18} {qdeg[idx]:+8.2f}°")
|
|
|
|
|
|
def msc_select_ai(msc: Optional[MotionSwitcherClient]):
|
|
if msc is None:
|
|
print("[ERR] MotionSwitcher not available.")
|
|
return
|
|
ret = safe_call("SelectMode('ai')", msc.SelectMode, "ai")
|
|
# ret could be (7004,None) etc; we just print it from safe_call.
|
|
|
|
|
|
def msc_release(msc: Optional[MotionSwitcherClient]):
|
|
if msc is None:
|
|
print("[ERR] MotionSwitcher not available.")
|
|
return
|
|
safe_call("ReleaseMode()", msc.ReleaseMode)
|
|
|
|
|
|
def msc_show(msc: Optional[MotionSwitcherClient]):
|
|
if msc is None:
|
|
print("[ERR] MotionSwitcher not available.")
|
|
return
|
|
try:
|
|
s, r = msc.CheckMode()
|
|
print(f"[MSC] Current mode: {r.get('name') or '(none)'}")
|
|
except Exception as e:
|
|
print("[ERR] CheckMode failed:", repr(e))
|
|
|
|
|
|
def seated_off(bot: LocoClient):
|
|
print("[SEATED] Leaving seated: StandUp + PREP (no Start)...")
|
|
fsm_set(bot, 4, "StandUp")
|
|
prep_mode(bot)
|
|
|
|
|
|
def reconnect(args, set_bot, set_msc):
|
|
print("Reconnecting LocoClient...")
|
|
try:
|
|
b = init_loco(timeout=args.timeout)
|
|
set_bot(b)
|
|
print("[OK] LocoClient reconnected.")
|
|
except Exception as e:
|
|
print("[ERR] Loco reconnect failed:", repr(e))
|
|
set_bot(None)
|
|
|
|
print("Reconnecting MotionSwitcherClient...")
|
|
try:
|
|
m = init_msc(timeout=5.0)
|
|
set_msc(m)
|
|
try:
|
|
_s, r = m.CheckMode()
|
|
print(f"[MSC] Current mode: {r.get('name') or '(none)'}")
|
|
except Exception:
|
|
pass
|
|
print("[OK] MotionSwitcher reconnected.")
|
|
except Exception as e:
|
|
print("[ERR] MotionSwitcher reconnect failed:", repr(e))
|
|
set_msc(None)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|