#!/usr/bin/env python3
"""
FORCE-FOLLOW — shape the Inspire hand by pushing its fingers, like posing a limp hand.

The RH56 cannot be back-driven: its worm drive self-locks, so pushing a finger moves it
0.000 rad no matter how hard you press (measured: 233 g of push, 0.000-0.005 of rotation).
Kinesthetic teaching by physically bending the fingers is therefore impossible.

This makes it FEEL back-drivable instead, with admittance control — the standard technique
for exactly this class of device (non-backdrivable, high-friction, force-sensed):

    push the pad  ->  force sensor reads it  ->  motor drives the finger that way
    stop pushing  ->  it holds where it is

So you shape the hand with your hands, and the pose sticks. Then record it.

    conda activate g1_env
    cd ~/Robotics_workspace/yslootahtech/G1/G1_Lootah/Manual_Recorder
    python3 hand_follow.py enp3s0                  # push fingers to close them
    python3 hand_follow.py enp3s0 --invert         # if a push OPENS instead of closes
    python3 hand_follow.py enp3s0 --gain 0.0009    # faster / slower response

Ctrl-C prints the shape you ended on, ready to paste into the dashboard or a recording.

SAFETY: a finger you push closes WHILE you keep pushing. If it closes onto your fingertip
the force stays high and it keeps going, so it is speed-limited (full travel ~3s) and the
hand's own grip-force limit still applies. Push with a knuckle or the back of a pen if you
would rather not have it close on your skin.
"""
import sys, time, argparse
from unitree_sdk2py.core.channel import ChannelPublisher, ChannelSubscriber, ChannelFactoryInitialize
from unitree_sdk2py.idl.unitree_go.msg.dds_ import MotorCmds_, MotorStates_
from unitree_sdk2py.idl.default import unitree_go_msg_dds__MotorCmd_

NAMES = ["R.pinky", "R.ring", "R.mid", "R.index", "R.thumbB", "R.thumbR",
         "L.pinky", "L.ring", "L.mid", "L.index", "L.thumbB", "L.thumbR"]
THUMB_ROT = [5, 11]

ap = argparse.ArgumentParser()
ap.add_argument("iface", nargs="?", default="enp3s0")
ap.add_argument("--gain", type=float, default=0.0006,
                help="travel per gram per second. higher = more eager (default 0.0006)")
ap.add_argument("--deadband", type=float, default=30.0,
                help="grams of push before a finger reacts, ignores drift/noise")
ap.add_argument("--maxrate", type=float, default=0.35,
                help="max travel per second (0.35 = full range in ~3s)")
ap.add_argument("--force", type=float, default=400.0, help="grip force limit while shaping")
ap.add_argument("--invert", action="store_true", help="flip which way a push moves the finger")
ap.add_argument("--only", default="", help="comma list to enable, e.g. R.index,R.mid (default all)")
a = ap.parse_args()

enabled = set(range(12))
if a.only.strip():
    enabled = set()
    for tok in a.only.split(","):
        tok = tok.strip().lower()
        hit = [i for i, n in enumerate(NAMES) if n.lower() == tok]
        if not hit:
            sys.exit(f"--only: unknown finger {tok!r}. Names: {', '.join(NAMES)}")
        enabled.add(hit[0])

ChannelFactoryInitialize(0, a.iface)
st = {"q": [1.0] * 12, "f": [0.0] * 12, "n": 0}
def on_state(m):
    st["q"] = [float(m.states[i].q) for i in range(12)]
    st["f"] = [float(m.states[i].tau_est) for i in range(12)]
    st["n"] += 1
ChannelSubscriber("rt/inspire/state", MotorStates_).Init(on_state, 10)
pub = ChannelPublisher("rt/inspire/cmd", MotorCmds_); pub.Init()
msg = MotorCmds_(); msg.cmds = [unitree_go_msg_dds__MotorCmd_() for _ in range(12)]

def send(q):
    for i in range(12):
        msg.cmds[i].q = float(max(0.0, min(1.0, q[i])))
        msg.cmds[i].kp = a.force
    pub.Write(msg)

time.sleep(1.0)
if st["n"] == 0:
    sys.exit("no hand state — is inspire_g1 running?")

# Start from wherever the hand already is, so it does not jump when you begin.
target = list(st["q"])
send(target)

print("calibrating resting force — DON'T touch (2s)...")
acc = [0.0] * 12; n = 0
t0 = time.time()
while time.time() - t0 < 2.0:
    send(target)
    for i in range(12): acc[i] += st["f"][i]
    n += 1
    time.sleep(0.03)
base = [acc[i] / n for i in range(12)]

print("\n" + "=" * 66)
print("PUSH A FINGER — it follows while you push, and holds when you stop")
print(f"  gain {a.gain}  deadband {a.deadband:.0f}g  max {a.maxrate}/s"
      + ("  INVERTED" if a.invert else ""))
if a.only.strip():
    print("  enabled: " + ", ".join(NAMES[i] for i in sorted(enabled)))
print("  Ctrl-C to stop and print the shape")
print("=" * 66 + "\n")

sign = 1.0 if a.invert else -1.0     # push on the pad (+force) should CLOSE (q toward 0)
last = time.time()
try:
    while True:
        now = time.time(); dt = now - last; last = now
        dt = min(dt, 0.1)
        moving = []
        for i in range(12):
            if i not in enabled:
                continue
            dev = st["f"][i] - base[i]
            # Measured: idle force drift over 90s untouched is 0g on all 12 fingers, so the
            # baseline is stable and needs no re-centring. The deadband only has to reject
            # incidental brushes, which the integrator would otherwise accumulate.
            if abs(dev) <= a.deadband:
                continue
            # push harder -> move faster, but never faster than maxrate
            step = sign * a.gain * (dev - (a.deadband if dev > 0 else -a.deadband)) * dt
            step = max(-a.maxrate * dt, min(a.maxrate * dt, step))
            target[i] = max(0.0, min(1.0, target[i] + step))
            moving.append(f"{NAMES[i].split('.')[1][:3]}{dev:+.0f}g→{target[i]:.2f}")
        send(target)
        # ALWAYS show the live force on every finger, not just ones over the deadband. Three
        # separate background measurements read 0g simply because they were not running while
        # a push happened; a live readout removes that whole class of confusion — you can see
        # immediately whether a push registers, how many grams it is, and which sign.
        top = sorted(((abs(st["f"][i] - base[i]), i) for i in range(12)), reverse=True)[:6]
        live = "  ".join(
            f"{NAMES[i].split('.')[1][:3]}{st['f'][i]-base[i]:+.0f}"
            + ("*" if abs(st["f"][i]-base[i]) > a.deadband else "")
            for v, i in top if v > 1)
        act = " ".join(moving)
        print("\r  " + (f"MOVING {act}" if act else f"force: {live or 'all quiet'}   "
              f"(need >{a.deadband:.0f}g)")[:110] + "\x1b[K", end="", flush=True)
        time.sleep(0.03)
except KeyboardInterrupt:
    print("\n\nshape you ended on:")
    print("  " + "  ".join(f"{NAMES[i].split('.')[1][:3]}={st['q'][i]:.2f}" for i in range(6)))
    print("  " + "  ".join(f"{NAMES[i].split('.')[1][:3]}={st['q'][i]:.2f}" for i in range(6, 12)))
    print("\n  as 12 values (paste into the dashboard / a pose file):")
    print("  " + " ".join(f"{st['q'][i]:.3f}" for i in range(12)))
    print("\n  holding this shape. Ctrl-C again to exit.")
    try:
        while True:
            send(target); time.sleep(0.05)
    except KeyboardInterrupt:
        print("done.")
