File size: 10,920 Bytes
4d58ef2
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
#!/usr/bin/env python3
"""Drive the flamingo-cycle policy on a physical Microduck through robotd's IPC socket.

One file, standard library only. A TEST AID, not a product: no gamepad, no supervision beyond
what robotd reports. NOT yet run on hardware — keep a hand near the robot for the first lift.

Robot config (deploy/robotd.toml, then `sudo systemctl restart robotd`). robotd writes
`robot.move {vx, vy, vyaw}` verbatim into the network's twist slots (EMA + 500 ms deadman only),
so the policy runs under the `walk` role with the standing switch disabled:

    [control]
    cmd_alpha = 1.0                # pass the flag through unsmoothed (default 0.2 = a 0.4 s ramp)

    [policy]
    walk = "/home/radxa/policies/flamingo/policy.onnx"
    stand = "none"                 # no "twist <= 0.05 -> standing network": this policy stands by itself at [0,0,0]
    sitstand = "none"              # no skill may take the command block over during the test
    ground_pick = "none"
    kick_left = "none"
    kick_right = "none"
    roulade = "none"

    [safety]
    limp_fall_tilt_z = -0.80       # fall predictor's "already tilted" gate is -0.90 (~26 deg); the hold leans ~24 deg

Falls stay the daemon's: the fall predictor makes the robot go limp, waits for the gyro to settle,
ramps to the default pose and hands back with the twist at zero, which for this policy is "stand".
For a permanent integration the daemon wants a `flamingo` role (a Net variant writing
twist = [flag, side, 0], like sit writes [1, 0, 0]) so the walk/stand policies stay loaded.

The policy reads the twist slots as [flag, side, 0]:
    flag  0 = stand on two feet, 1 = stand on one foot
    side +1 = right foot down / left leg lifted, -1 = left foot down

    # on the laptop, after:  ssh -L /tmp/robotd.sock:/run/robotd.sock radxa@<robot>
    python3 control.py --socket /tmp/robotd.sock                       # interactive: f = lift/lower, c = side, q = quit
    python3 control.py --socket /tmp/robotd.sock --timeline "0:0,1;3.6:1,1;11.6:0,1;18.3:1,-1;26:0,-1"   # scripted t:flag,side
    python3 control.py --socket /tmp/robotd.sock --no-enable           # don't send robot.enable (already driving)
    python3 control.py --socket /tmp/robotd.sock --init                # power the joints first (robot.init) if the robot is relaxed

If it exits immediately it tells you why (no socket / forward, permission, daemon refusal). Please
report the exact message when asking for help.

What it does: opens the socket, `robot.enable {on: true}` (a request, answered), then sends
`robot.move {vx: flag, vy: side, vyaw: 0}` notifications at 10 Hz (robotd zeroes a twist older
than its 500 ms deadman) while printing robotd's state (policy label, fallen/limp, gravity,
applied twist) at 2 Hz. On exit it sends flag 0 for 3 s so the robot is back on two feet before the
deadman takes over, then `robot.stop`. It never sends `robot.relax` (that cuts motor power).
"""

import argparse
import json
import select
import socket
import sys
import termios
import time
import tty

RESEND_HZ = 10.0
LOWER_S = 3.0          # time given to the policy to come back down before exiting


class Robotd:
    def __init__(self, path):
        self.sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
        try:
            self.sock.connect(path)
        except FileNotFoundError:
            sys.exit(f"no socket at {path}.\n  on the robot: is robotd running?  (systemctl status robotd)\n"
                     f"  from a laptop: forward it first — ssh -L {path}:/run/robotd.sock radxa@<robot>\n"
                     f"  (ssh refuses to bind if {path} already exists: rm it, then reconnect)")
        except PermissionError:
            sys.exit(f"no permission on {path} — on the robot run as the user that owns it (or with sudo)")
        except ConnectionRefusedError:
            sys.exit(f"{path} exists but nothing answers — a stale socket file or a dead ssh forward; remove it and reconnect")
        self.sock.setblocking(False)
        self.buf = b""
        self.next_id = 1
        self.state = None

    def notify(self, method, params=None):
        msg = {"jsonrpc": "2.0", "method": method}
        if params is not None:
            msg["params"] = params
        self.sock.sendall((json.dumps(msg) + "\n").encode())

    def request(self, method, params=None, timeout=2.0):
        rid = self.next_id
        self.next_id += 1
        msg = {"jsonrpc": "2.0", "id": rid, "method": method}
        if params is not None:
            msg["params"] = params
        self.sock.sendall((json.dumps(msg) + "\n").encode())
        t0 = time.time()
        while time.time() - t0 < timeout:
            for m in self.poll():
                if m.get("id") == rid:
                    if "error" in m:
                        raise RuntimeError(f"{method}: {m['error']}")
                    return m.get("result")
            time.sleep(0.01)
        raise TimeoutError(f"{method}: no answer in {timeout}s")

    def poll(self):
        """Read whatever arrived; keep the latest robot.state; return all messages."""
        out = []
        try:
            while True:
                chunk = self.sock.recv(65536)
                if not chunk:
                    raise ConnectionError("robotd closed the socket")
                self.buf += chunk
        except BlockingIOError:
            pass
        while b"\n" in self.buf:
            line, self.buf = self.buf.split(b"\n", 1)
            if not line.strip():
                continue
            try:
                m = json.loads(line)
            except json.JSONDecodeError:
                continue
            if m.get("method") == "robot.state":
                self.state = m.get("params", {})
            out.append(m)
        return out

    def move(self, flag, side):
        self.notify("robot.move", {"vx": float(flag), "vy": float(side), "vyaw": 0.0})

    def status(self):
        s = self.state
        if not s:
            return "no state yet"
        saf = s.get("safety", {})
        mv = s.get("movement", {})
        g = saf.get("gravity", [0, 0, 0])
        return (f"policy={s.get('policy')} fallen={saf.get('fallen')} limp={saf.get('limp')} "
                f"gz={g[2]:+.2f} applied_twist={[round(x, 2) for x in mv.get('applied', [])]} "
                f"limited_by={mv.get('limited_by')}")


def parse_timeline(spec):
    steps = []
    for part in spec.split(";"):
        t, v = part.split(":")
        flag, side = (float(x) for x in v.split(","))
        steps.append((float(t), flag, side))
    return sorted(steps)


class Keys:
    """Raw single-key reads from the terminal (interactive mode)."""

    def __enter__(self):
        self.fd = sys.stdin.fileno()
        self.old = termios.tcgetattr(self.fd)
        tty.setcbreak(self.fd)
        return self

    def __exit__(self, *exc):
        termios.tcsetattr(self.fd, termios.TCSADRAIN, self.old)

    def get(self):
        if select.select([sys.stdin], [], [], 0)[0]:
            return sys.stdin.read(1).lower()
        return None


def main():
    ap = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
    ap.add_argument("--socket", default="/run/robotd.sock", help="robotd IPC socket (forwarded: /tmp/robotd.sock)")
    ap.add_argument("--timeline", default=None, help="scripted 't:flag,side;...' instead of the keyboard")
    ap.add_argument("--no-enable", action="store_true", help="skip robot.enable (the robot is already driving)")
    ap.add_argument("--state-hz", type=float, default=2.0, help="robot.state subscription rate for the status line")
    ap.add_argument("--init", action="store_true", help="send robot.init first (powers the joints and ramps to the home pose)")
    a = ap.parse_args()
    if a.timeline is None and not sys.stdin.isatty():
        sys.exit("interactive mode needs a terminal (keys are read from stdin); use --timeline \"t:flag,side;...\" instead")

    rd = Robotd(a.socket)
    try:
        print("robot.subscribe ->", rd.request("robot.subscribe", {"hz": int(a.state_hz)}))
    except Exception as e:  # the status line is a convenience, not a requirement
        print("subscribe failed (continuing without the status line):", e)
    if a.init:
        try:
            print("robot.init ->", rd.request("robot.init", timeout=10.0))
        except Exception as e:
            sys.exit(f"robot.init refused: {e}")
    if not a.no_enable:
        try:
            print("robot.enable ->", rd.request("robot.enable", {"on": True}))
        except Exception as e:
            sys.exit(f"robot.enable refused: {e}\n  (is the policy loaded? robotd reports 'policy unavailable: ...' in robot.health; "
                     f"is the robot initialised? try --init)")

    flag, side = 0.0, 1.0
    period = 1.0 / RESEND_HZ
    t0 = time.time()
    last_print = 0.0
    timeline = parse_timeline(a.timeline) if a.timeline else None
    keys = Keys() if timeline is None else None
    if keys:
        keys.__enter__()
        print("f = lift / lower the foot   c = choose the side (while standing)   q = quit (lowers first)")
    try:
        while True:
            now = time.time() - t0
            if timeline is not None:
                for t, f, s_ in timeline:
                    if now >= t:
                        flag, side = f, s_
                if now > timeline[-1][0] + LOWER_S and flag == 0.0:
                    break
            else:
                k = keys.get()
                if k == "q":
                    break
                if k == "f":
                    flag = 0.0 if flag else 1.0
                    print(f"\nflag -> {flag:.0f} ({'lifting the ' + ('LEFT' if side > 0 else 'RIGHT') + ' foot' if flag else 'coming down'})")
                if k == "c":
                    if flag:
                        print("\ncome down first (f) before changing the side")
                    else:
                        side = -side
                        print(f"\nside -> {side:+.0f} ({'right foot down' if side > 0 else 'left foot down'})")
            rd.move(flag, side)
            rd.poll()
            if now - last_print >= 1.0 / a.state_hz:
                last_print = now
                print(f"\r{now:6.1f}s  cmd=[{flag:.0f},{side:+.0f},0]  {rd.status()}", end="", flush=True)
            time.sleep(period)
    except KeyboardInterrupt:
        print("\ninterrupted")
    finally:
        if keys:
            keys.__exit__(None, None, None)
        print("\nlowering (flag 0) for %.0f s ..." % LOWER_S)
        t1 = time.time()
        while time.time() - t1 < LOWER_S:
            rd.move(0.0, side)
            rd.poll()
            time.sleep(period)
        try:
            rd.request("robot.stop")
        except Exception as e:
            print("robot.stop:", e)
        print("done:", rd.status())


if __name__ == "__main__":
    main()