#!/usr/bin/env python3 # -*- coding: utf-8 -*- """jog_probe_pi.py — 根元 j0 で 4 つの動かし方を測り, mechArm がどこまでなめらかに動くかを見る (2026-11-03) Mac の tools/jog_probe.sh から呼ぶ (デーモンを止めてから, シリアルに直接つなぐ)。 Pi の python3 は 3.6, pymycobot 2.9.6 (PYTHONPATH=/usr/local/lib/python2.7/dist-packages)。 測るもの (どれも j0 だけを動かす. 始める前に HOME 近くにいること. 各テストのあと始めの角度へ戻す): 0. 読み取り: get_angles を 40 回. 1 回の時間と, 空で返る割合 A. 1 回の send_angles (+10°, speed 20): 着くまでの時間と最高速度 (アーム自体の速さ) B. 送り直し (今のゲームパッドの長押しと同じ): 0.1 秒おきに「読んだ角度 + 3°」を send_angles. 1 秒 C. B と同じ. ただし set_fresh_mode(1) (前の動きを待たずに上書き) D. jog_angle(1, 向き, speed) を 1 秒 → jog_stop. speed 10 と 30 各テストで: 進んだ角度・平均の速さ・0.1 秒ごとの速さのばらつき (なめらかさ)・止まった区間の割合・ 止める命令のあとに進んだ角度 (行きすぎ) を出す。生の記録 (時刻と角度) は --out の JSON に入る。 守り: j0 が始めの角度から JOG_GUARD_DEG (25°) 離れたらそのテストを止める (jog_stop・stop)。 q と Enter (または Ctrl-C・ssh が切れたとき) で止める。終わるとき (止めたときも) jog_stop と set_fresh_mode(0)。 send_coords() は使わない。release 系は呼ばない。 """ from __future__ import print_function import argparse import json import select import signal import statistics import sys import time SERIAL_PORT = "/dev/ttyAMA0" BAUDRATE = 1000000 HOME = [0, 0, 0, 0, 0, -30] SPEED = 20 HOME_TOL_DEG = 5.0 JOG_GUARD_DEG = 25.0 STEP_A_DEG = 10.0 STREAM_SEC = 1.0 STREAM_INTERVAL_SEC = 0.1 STREAM_AHEAD_DEG = 3.0 JOG_SEC = 1.0 JOG_SPEEDS = (10, 30) AFTER_STOP_SEC = 1.0 RETURN_TIMEOUT_SEC = 10.0 BIN_SEC = 0.1 GET_ANGLES_RETRIES = 12 class Quit(Exception): pass def now(): return time.time() def stdin_quit(): r, _w, _x = select.select([sys.stdin], [], [], 0) if r: line = sys.stdin.readline() if line.strip().lower() == "q": raise Quit() def ask(msg): print(msg + " (Enter で進む / q と Enter で終わる)", flush=True) line = sys.stdin.readline() if not line or line.strip().lower() == "q": raise Quit() def read_angles(mc, tries=GET_ANGLES_RETRIES): for _ in range(tries): a = mc.get_angles() if a and len(a) == 6: return [float(x) for x in a] time.sleep(0.02) return None def wait_arrive(mc, target, timeout): t0 = now() a = None while now() - t0 < timeout: stdin_quit() a = read_angles(mc) if a is not None and max(abs(x - y) for x, y in zip(a, target)) <= 2.0: return a, True time.sleep(0.1) return a, False def summarize(samples, t_cmd0, t_stop, sign): """samples: [(t, j0)]. t_cmd0 = 動かし始めた時刻, t_stop = 止める命令 (なければ None).""" act = [(t, q) for t, q in samples if t >= t_cmd0 and (t_stop is None or t <= t_stop)] out = {"n_samples": len(samples)} if len(act) < 2: return out travel = (act[-1][1] - act[0][1]) * sign dur = act[-1][0] - act[0][0] out.update(travel_deg=round(travel, 2), active_sec=round(dur, 2), mean_speed=round(travel / dur, 2) if dur > 0 else None) # 0.1 秒ごとの速さ (最初の 0.2 秒は加速として除く) bins = [] t = act[0][0] + 0.2 while t + BIN_SEC <= act[-1][0]: seg = [(ts, q) for ts, q in act if t <= ts <= t + BIN_SEC] if len(seg) >= 2: bins.append((seg[-1][1] - seg[0][1]) * sign / (seg[-1][0] - seg[0][0]) if seg[-1][0] > seg[0][0] else 0.0) t += BIN_SEC if bins: med = statistics.median(bins) out.update(bin_speeds=[round(b, 1) for b in bins], median_speed=round(med, 2), max_speed=round(max(bins), 2), speed_cv=round(statistics.pstdev(bins) / med, 2) if med > 0.5 else None, stall_ratio=round(sum(1 for b in bins if b < 0.25 * med) / len(bins), 2) if med > 0.5 else None) if t_stop is not None: before = [q for ts, q in samples if ts <= t_stop] after = [(ts, q) for ts, q in samples if ts > t_stop] if before and after: q_stop = before[-1] out["overshoot_deg"] = round((after[-1][1] - q_stop) * sign, 2) moving = [ts for (ts, q), (ts2, q2) in zip(after, after[1:]) if abs(q2 - q) > 0.1] out["stop_latency_sec"] = round(moving[-1] - t_stop, 2) if moving else 0.0 return out def run_sampling(mc, start, sec, sign, on_tick=None, label=""): """sec 秒, できるだけ速く j0 を読む. on_tick(t, angles) を毎回呼ぶ. 守りを超えたら止める.""" samples = [] t0 = now() while now() - t0 < sec: stdin_quit() a = mc.get_angles() t = now() if a and len(a) == 6: samples.append((t, float(a[0]))) if abs(float(a[0]) - start[0]) > JOG_GUARD_DEG: print(" 守り: j0 が始めから {:.1f}° 離れたので止めます".format(float(a[0]) - start[0]), flush=True) return samples, True if on_tick is not None: on_tick(t, [float(x) for x in a]) return samples, False def back_to_start(mc, start): mc.send_angles(start, SPEED) a, ok = wait_arrive(mc, start, RETURN_TIMEOUT_SEC) print(" 始めの角度へ戻した: " + ("着いた" if ok else "着かない") + " j0={}".format(None if a is None else round(a[0], 2)), flush=True) time.sleep(0.5) def test_read(mc): lat, empty = [], 0 for _ in range(40): t = now() a = mc.get_angles() lat.append(now() - t) if not (a and len(a) == 6): empty += 1 r = {"median_ms": round(statistics.median(lat) * 1000, 1), "max_ms": round(max(lat) * 1000, 1), "empty_ratio": round(empty / 40.0, 2)} print(" 読み取り 1 回: 中央 {median_ms} ms / 最大 {max_ms} ms / 空で返った割合 {empty_ratio}".format(**r), flush=True) return r def test_step(mc, start): target = list(start) target[0] = start[0] + STEP_A_DEG t_cmd = now() mc.send_angles(target, SPEED) samples, guard = run_sampling(mc, start, 2.0, +1) s = summarize(samples, t_cmd, None, +1) reached = [t for t, q in samples if abs(q - target[0]) <= 1.0] s["arrive_sec"] = round(reached[0] - t_cmd, 2) if reached else None return s, samples, t_cmd, None def test_stream(mc, start, sign, fresh): if fresh: mc.set_fresh_mode(1) st = {"last": 0.0, "n": 0} t_cmd = now() def tick(t, a): if t - st["last"] >= STREAM_INTERVAL_SEC: target = list(start) target[0] = a[0] + sign * STREAM_AHEAD_DEG mc.send_angles(target, SPEED) st["last"] = t st["n"] += 1 try: samples, guard = run_sampling(mc, start, STREAM_SEC, sign, on_tick=tick) t_stop = now() after, _ = run_sampling(mc, start, AFTER_STOP_SEC, sign) finally: if fresh: mc.set_fresh_mode(0) s = summarize(samples + after, t_cmd, t_stop, sign) s["n_sends"] = st["n"] return s, samples + after, t_cmd, t_stop def test_jog(mc, start, sign, speed): direction = 1 if sign > 0 else 0 t_cmd = now() try: mc.jog_angle(1, direction, speed) samples, guard = run_sampling(mc, start, JOG_SEC, sign) finally: mc.jog_stop() t_stop = now() after, _ = run_sampling(mc, start, AFTER_STOP_SEC, sign) s = summarize(samples + after, t_cmd, t_stop, sign) moved = abs(samples[-1][1] - samples[0][1]) if len(samples) >= 2 else 0.0 s["jog_moved"] = moved > 1.0 return s, samples + after, t_cmd, t_stop def show(name, s): keys = ["travel_deg", "mean_speed", "median_speed", "max_speed", "speed_cv", "stall_ratio", "overshoot_deg", "stop_latency_sec", "arrive_sec", "n_sends", "jog_moved"] print(" [{}] ".format(name) + " ".join("{}={}".format(k, s[k]) for k in keys if k in s), flush=True) if "bin_speeds" in s: print(" 0.1 秒ごとの速さ (°/秒): " + " ".join(str(v) for v in s["bin_speeds"]), flush=True) def main(argv=None): ap = argparse.ArgumentParser() ap.add_argument("--out", required=True) args = ap.parse_args(argv) def on_hup(signum, frame): raise Quit() signal.signal(signal.SIGHUP, on_hup) import pymycobot from pymycobot.mycobot import MyCobot print("pymycobot {} ({})".format(getattr(pymycobot, "__version__", "?"), pymycobot.__file__), flush=True) mc = MyCobot(SERIAL_PORT, BAUDRATE) time.sleep(0.5) result = {"started_at": time.strftime("%Y-%m-%d %H:%M:%S"), "pymycobot": getattr(pymycobot, "__version__", "?"), "params": {"speed": SPEED, "stream_interval": STREAM_INTERVAL_SEC, "stream_ahead": STREAM_AHEAD_DEG, "jog_speeds": list(JOG_SPEEDS), "guard_deg": JOG_GUARD_DEG}, "tests": {}} try: start = read_angles(mc) print("始めの角度: {}".format(start), flush=True) if start is None: print("角度が読めないので終わります", flush=True) return 1 if max(abs(x - y) for x, y in zip(start, HOME)) > HOME_TOL_DEG: ask("HOME から離れています。HOME [0,0,0,0,0,-30] へ送ります") mc.send_angles(HOME, SPEED) start, ok = wait_arrive(mc, HOME, 15.0) if not ok: print("HOME に着かないので終わります", flush=True) return 1 time.sleep(0.5) start = read_angles(mc) result["start"] = start print("根元 j0 だけを、始めの角度から 25° 以内で動かします。毎回始めの角度へ戻します。", flush=True) print("\n0. 読み取りの速さ (動かさない)", flush=True) result["tests"]["read"] = test_read(mc) plan = [("A_step_10deg", lambda: test_step(mc, start)), ("B_stream", lambda: test_stream(mc, start, +1, False)), ("C_stream_fresh", lambda: test_stream(mc, start, -1, True)), ("D_jog_speed{}".format(JOG_SPEEDS[0]), lambda: test_jog(mc, start, +1, JOG_SPEEDS[0])), ("D_jog_speed{}".format(JOG_SPEEDS[1]), lambda: test_jog(mc, start, -1, JOG_SPEEDS[1]))] for name, fn in plan: ask("\n{} を始めます".format(name)) s, samples, t_cmd, t_stop = fn() show(name, s) result["tests"][name] = dict(s, samples=[[round(t - t_cmd, 4), round(q, 2)] for t, q in samples], t_stop=None if t_stop is None else round(t_stop - t_cmd, 4)) back_to_start(mc, start) except Quit: print("\n止めました (このあとは動かす命令を送りません)", flush=True) result["quit"] = True finally: for f in (getattr(mc, "jog_stop", None),): try: if f is not None: f() except Exception: pass try: mc.set_fresh_mode(0) except Exception: pass with open(args.out, "w") as fp: json.dump(result, fp, ensure_ascii=False) print("結果: " + args.out, flush=True) return 0 if __name__ == "__main__": sys.exit(main())