#!/usr/bin/env python3 """grip_current.py — 1 本の記録 (run のフォルダ) で, 爪を閉じたあとの爪の電流の値を 1 行で出す (2026-11-08 依頼 121). 入力 (run のフォルダ. runs/run_pick_place/{run}/ (教えた動きの再生) でも runs/pi05_policy/{run}/ (実機推論) でもよい): recording/currents.csv gripper_current 列 (依頼 116 の符号付きの値. 爪 7 の番地 69/70) recording/states.csv 爪の命令 (gripper_cmd) の時刻. 閉じる命令は grasp_depth.close_time と同じ選び方 見る範囲: 最初に閉じた命令の HELD_FROM_SEC 秒後から, 次に開く命令まで (なければ記録の終わりまで). 教えた動きの再生では, 閉じてから待ち (2 秒) が終わるまで電流を読まない (依頼 121) ので, 持ち上げてから先の値になる. 目安 (依頼 109・116 の測り. まだ再生の記録では実機で確かめていない): 空で閉じる: 15 前後 / ブロックをつかむ: 数百〜990 / 放す (開く): −50 前後 GRASP_LINE (100) 以上なら「つかめていそう」と出す. **目安の表示だけ**で, 学習データから外すかどうかは 今までどおり grasp_depth.py の深さ (閉じて 1.0 秒後のはみ出し 24px) で決める. python3 pi05/grip_current.py RUN_DIR [RUN_DIR ...] (grasp_depth.py も同じ run で深さの行の下にこの 1 行を出す) 読むだけ (記録には何も書かない). """ import argparse import csv import json import sys from pathlib import Path import grasp_depth as gd GRASP_LINE = 100 # これ以上なら「つかめていそう」(空 15 前後と つかんだ 数百 の間. 目安) HELD_FROM_SEC = 1.0 # 閉じる命令からこの秒数より後の値を見る (閉じている途中の値を混ぜない) def summarize(run_dir): """戻り値 dict: status (ok / no_currents / no_close / no_values) と, ok なら max・median・n・grasped.""" rec = Path(run_dir) / "recording" out = {"status": None, "max": None, "median": None, "n": 0, "grasped": None, "line": GRASP_LINE, "currents_mode": None, "reason": None} for name in ("currents_meta.json", "top_meta.json"): try: out["currents_mode"] = json.loads((rec / name).read_text()).get("currents_mode") if out["currents_mode"]: break except Exception: pass cur = rec / "currents.csv" if not cur.exists(): out.update(status="no_currents", reason="currents.csv がない (依頼 116・121 より前の記録)") return out try: gt, gs = gd.load_gripper_cmds(rec / "states.csv") except Exception as e: out.update(status="no_close", reason=f"states.csv を読めない ({type(e).__name__})") return out t_c = gd.close_time(gt, gs) if t_c is None: out.update(status="no_close", reason="閉じる命令がない") return out opens = [float(t) for t, s in zip(gt, gs) if s == 0 and t > t_c] t_end = min(opens) if opens else float("inf") vals = [] try: with cur.open(newline="") as f: for row in csv.DictReader(f): try: t, v = float(row["ts"]), row.get("gripper_current") or "" except Exception: continue if v == "" or not (t_c + HELD_FROM_SEC <= t < t_end): continue try: vals.append(int(v)) except ValueError: continue except Exception as e: # 読めない currents.csv でも 1 行で返す (codex R1 推奨) out.update(status="no_values", reason=f"currents.csv を読めない ({type(e).__name__})") return out out.update(t_close=round(t_c, 3), held_sec=(round(t_end - t_c, 1) if opens else None)) if not vals: out.update(status="no_values", reason="閉じたあとの値がない" + (f" (currents_mode={out['currents_mode']})" if out["currents_mode"] not in (None, "read_servo") else "")) return out vs = sorted(vals) out.update(status="ok", max=vs[-1], median=vs[len(vs) // 2], n=len(vs), grasped=vs[-1] >= GRASP_LINE) return out def line(run_dir, s=None): """表示の 1 行.""" s = s if s is not None else summarize(run_dir) name = Path(run_dir).name if s["status"] != "ok": return f"[爪 値なし] {name} {s['reason']}" flag = "つかめていそう" if s["grasped"] else "空で閉じた?" return (f"[爪 {flag}] {name} 閉じたあとの爪の値 最大 {s['max']} 中央値 {s['median']} ({s['n']} 行) " f"(目安の線 {GRASP_LINE}. 空 15 前後・つかむ 数百〜990)") def main(): ap = argparse.ArgumentParser(description=__doc__.split("\n")[0]) ap.add_argument("run_dir", nargs="+", help="run のフォルダ (recording/ がある所)") args = ap.parse_args() for d in args.run_dir: print(line(d)) return 0 if __name__ == "__main__": sys.exit(main())