mechArm 270 Pi 動的追従の限界を探る Log#42 | グリッパーの握り方を位置から押し込みに変えたら、ブロックが落ちなくなった
LOG261R2R2R2026/6/29

mechArm 270 Pi 動的追従の限界を探る Log#42 | グリッパーの握り方を位置から押し込みに変えたら、ブロックが落ちなくなった

前回、ブロックが落ちる原因がやっと割れた。爪が「位置で止まる」だけで、握り直さない。手でブロックを動かしても爪は追ってこない。位置で固定されているから、ガタの隙間ぶん、姿勢が変わると抜ける。撮影サイクルでもなく、特定の関節でもなく、握り方そのものが原因だった。

原因が分かったので、今回は対策が効くかを測る。閉じ切って位置で止めるのをやめて、閉じ切らない目標値で握る。閉じ切れないぶん、爪は目標に向かって押し込み続ける。その押し込み力でブロックを保持できないか、という仮説を実機で確かめた。

閉じ切る握りと、閉じ切らない握り

mechArm のグリッパーには2系統の制御がある。state ベースの set_gripper_state(state, speed) は state=0 で開き、state=1 で閉じる。これまで把持に使ってきたのはこれで、state=1 は「閉じ切る」指示になる。閉じ切ったあとは位置で止まる。これが前回突き止めた落下の正体だった。

もう一系統が set_gripper_value(value, speed) で、0〜100 の目標開度を指定する。ここで「ブロックの厚みより小さい目標値」を渡すと、爪はそこまで閉じようとするがブロックに当たって到達できない。到達できないぶん、押し込みが残る。前回の測定で、グリッパーの開度値(gv)は開いた状態で 97、空振りで閉じ切ると 6、ブロックを挟んだ状態で 36 前後だと分かっている。だから 36 より小さい目標値を渡せば、閉じ切れずに押し込み力が残るはずだった。

この set_gripper_value は、これまで使ってきたデーモンに入っていなかった。あるのは state ベースの set_gripper だけ。なので、まずデーモンに値ベースの把持コマンドを足すところから始めた。

デーモンに値ベース把持を足す

Pi 側で常駐しているデーモンに、set_gripper_value のコマンドを1つ追加した。既存の set_gripper(state)の隣に置く純追記で、既存のコマンドには一切触っていない。値域ガードを必須にして、value が 0〜100、speed が 1〜100 の範囲内のときだけハードに送る。範囲外やキャスト失敗、未対応環境(AttributeError)は error を返してデーモンは落とさない。これは物理リスクのある変更なので、グリッパーに変な値を送らないための一次防衛をデーモン側に置いた。

デーモン全体のうち、追加した set_gripper_value 分岐は次の通り。既存の set_gripper(state)と並べて載せる。

    elif cmd == "set_gripper":
        state = int(cmd_obj["state"])
        speed = int(cmd_obj.get("speed", 100))
        wait = float(cmd_obj.get("wait", 0))
        # DAY175 対策: 連続送信時の動作不具合回避のため事前 sleep
        time.sleep(2)
        result = _mc.set_gripper_state(state, speed)
        if wait > 0:
            time.sleep(wait)
        return {"ok": True, "result": result}

    # DAY261: 値ベース把持 cmd (純追記).
    # state ベースの set_gripper とは独立。閉じ切らない目標値で握って押し込み力を
    # 残す検証用。値域ガード必須 (value 0-100 / speed 1-100)。範囲外は
    # ハードに送らず error を返す。応答フォーマットは set_gripper と同形 {ok, result}。
    elif cmd == "set_gripper_value":
        try:
            value = int(cmd_obj["value"])
            speed = int(cmd_obj.get("speed", 100))
        except (KeyError, TypeError, ValueError) as e:
            return {"ok": False, "error": f"bad args: {type(e).__name__}: {e}"}
        if not (0 <= value <= 100):
            return {"ok": False, "error": f"value must be 0..100, got {value}"}
        if not (1 <= speed <= 100):
            return {"ok": False, "error": f"speed must be 1..100, got {speed}"}
        try:
            result = _mc.set_gripper_value(value, speed)
        except AttributeError:
            return {"ok": False,
                    "error": "set_gripper_value is not available in this pymycobot environment"}
        except Exception as e:
            return {"ok": False, "error": f"{type(e).__name__}: {e}"}
        log(f"set_gripper_value(value={value}, speed={speed}) result={result}")
        return {"ok": True, "result": result}

Mac 側にも、このコマンドを TCP で送るヘルパを足した。既存の daemon_set_gripper(state)は触らず、独立した daemon_set_gripper_value を追加する。対比のため両方載せる。

def daemon_set_gripper(state, speed=100, wait=2):
    """グリッパー開閉。state=0:開く / state=1:閉じる。"""
    resp = _daemon_request({
        "cmd": "set_gripper",
        "state": state,
        "speed": speed,
        "wait": wait,
    })
    result = resp.get("result")
    print(f"  [Pi] GRIPPER: {'OPEN' if state == 0 else 'CLOSE'} result= {result}")


# DAY261: 値ベース把持ヘルパ (純追記).
# 既存 daemon_set_gripper (state) と独立。push-grip 検証用に閉じ切らない目標値で握る。
# daemon 側 cmd "set_gripper_value" が値域ガード (0-100 / 1-100) を持つ。
def daemon_set_gripper_value(value, speed=100):
    """グリッパーを指定値 (0-100) で握る。押し込み力を残すため閉じ切らない目標値を渡す。
    state ベースの daemon_set_gripper は触らない (独立ヘルパ)。"""
    resp = _daemon_request({
        "cmd": "set_gripper_value",
        "value": int(value),
        "speed": int(speed),
    })
    result = resp.get("result")
    print(f"  [Pi] GRIPPER: VALUE={int(value)} speed={int(speed)} result= {result}")
    return result

デーモンの追加とヘルパの追加は、どちらもグリッパーの挙動を変える変更にあたる。実機に流す前に codex と gemini の外部レビューを通した。観点は3つ。既存コマンドを壊していないか、値域ガード(0〜100)が効いているか、既存挙動の回帰がないか。レビューは全項目で問題なしの判定だった。

値を振って、握れる範囲を探す

次に、診断スクリプトに --grip-value フラグを足した。これは HOME でブロックを手で爪に挟ませてから、4つの姿勢(APPROACH_PICK / PICK / APPROACH_PLACE / PLACE)を巡回するモードで、巡回中にブロックが落ちるかを目視で見る。フラグ未指定なら従来通り state=1 で閉じ切り、指定するとその値で値ベース把持に差し替える。既存のロジックは触らず、GRIP の1箇所だけを分岐させた。

値ベース把持のラッパーと、GRIP の差し替え部、フラグ定義は次の通り。

# DAY261: 値ベース把持ラッパー (純追記). 既存 _wrapped_set_gripper (state) は無変更.
def _wrapped_set_gripper_value(log, value, speed=100, phase="grip"):
    """daemon_set_gripper_value を呼ぶ前後に cmd_issued をログ.
    --home-grip-tour --grip-value N からのみ呼ばれる (フラグ無し時は呼ばれない)."""
    cmd = "set_gripper_value({},{})".format(value, speed)
    log.emit(phase, "gripper_cmd", daemon_connected=True, cmd_issued=cmd)
    try:
        daemon_set_gripper_value(value, speed=speed)
    except Exception as e:
        log.emit(phase, "gripper_cmd", daemon_connected=True,
                 cmd_issued=cmd, note="exception:{}".format(e))
        # 6. GRIP 閉 (手差しブロックを掴む).
        # DAY261: --grip-value 指定時は値ベース把持に差し替え (押し込み力検証).
        # フラグ未指定時は従来通り state=1 (位置で閉じ切る) のまま.
        grip_value = getattr(args, "grip_value", None)
        if grip_value is not None:
            _wrapped_set_gripper_value(log, value=grip_value, speed=100,
                                       phase="grip")
            meta["grip_value_target"] = int(grip_value)
        else:
            _wrapped_set_gripper(log, state=1, speed=100, phase="grip")
        time.sleep(GRIPPER_SLEEP)
    # DAY261: --grip-value (純追記). 未指定なら従来の state=1 閉じ. 指定時は
    # _home_grip_tour の GRIP を値ベース把持に差し替える (押し込み力検証).
    ap.add_argument("--grip-value", type=int, default=None,
                    help="--home-grip-tour 用: GRIP を set_gripper_value(N) に "
                         "差し替え (N=0..100). 未指定は従来の state=1 閉じ.")
    args = ap.parse_args()

    # DAY261: --grip-value の値域検証 (daemon 側にもガードあり・二重チェック).
    if args.grip_value is not None and not (0 <= args.grip_value <= 100):
        print("[ABORT] --grip-value は 0..100 の範囲で指定. got {}".format(
            args.grip_value), file=sys.stderr)
        sys.exit(2)

まず接続確認として value=50 を1回叩いた。その実行ログが次。

[Mac] python3 grip_hold_capture_diag.py --grip-value 50 --home-grip-tour --loops 1
================================================================================
DAY259 grip_hold_capture_diag  run_stem=homegriptour_20260629_155632  mode=home_grip_tour
  out_dir = /Volumes/KIOXIA/r2r2r-data/runs/experiments/grip_hold_capture_diag/homegriptour_20260629_155632
================================================================================

[RUN] mode=home_grip_tour loops=1 pause_each=False
[TOUR] APPROACH_PICK  <- approach_pick_NA_20pt_adopted_DAY235_01.json
            base_angles = [0.0, 87.74, -38.33, 3.6, -70.11, -30.0]
[TOUR] PICK           <- pick_NA_20pt_adopted_DAY235_01.json
            base_angles = [-36.03, 88.0, -21.44, 1.4, -70.48, -32.87]
[TOUR] APPROACH_PLACE <- approach_place_NA_20pt_adopted_DAY235_01.json
            base_angles = [38.4, 57.39, 17.49, 0.08, -89.2, -29.44]
[TOUR] PLACE          <- place_withblock_20pt_adopted_DAY235_01.json
            base_angles = [40.6, 59.15, 12.83, -1.4, -78.04, -29.53]
[EVT  ] home       daemon_open    gv= gm= conn=True cmd=- note=
[EVT  ] home       measure        gv= gm= conn=True cmd=- note=HOME sent
[EVT  ] grip       gripper_cmd    gv= gm= conn=True cmd=set_gripper_state(0,100) note=
  [Pi] GRIPPER: OPEN result= None

[HOME-GRIP-TOUR] 爪の間にブロックを差し込んで Enter (掴ませます)
                  press Enter to proceed >
[EVT  ] M0         measure        gv=97 gm=False conn=True cmd=- note=tour_open_before_grip
[EVT  ] grip       gripper_cmd    gv= gm= conn=True cmd=set_gripper_value(50,100) note=
  [Pi] GRIPPER: VALUE=50 speed=100 result= None
[EVT  ] M_grip     measure        gv=51 gm=False conn=True cmd=- note=tour_after_grip

[TOUR] === loop 1/1 ===
[EVT  ] L0_APPROACH_PICK measure        gv= gm= conn=True cmd=- note=send APPROACH_PICK [0.0, 87.74, -38.33, 3.6, -70.11, -30.0]
[EVT  ] L0_APPROACH_PICK measure        gv=-1 gm=False conn=True cmd=- note=after_move_to_APPROACH_PICK gv_invalid
[EVT  ] L0_PICK    measure        gv= gm= conn=True cmd=- note=send PICK [-36.03, 88.0, -21.44, 1.4, -70.48, -32.87]
[EVT  ] L0_PICK    measure        gv=-1 gm=False conn=True cmd=- note=after_move_to_PICK gv_invalid
[EVT  ] L0_APPROACH_PLACE measure        gv= gm= conn=True cmd=- note=send APPROACH_PLACE [38.4, 57.39, 17.49, 0.08, -89.2, -29.44]
[EVT  ] L0_APPROACH_PLACE measure        gv=-1 gm=False conn=True cmd=- note=after_move_to_APPROACH_PLACE gv_invalid
[EVT  ] L0_PLACE   measure        gv= gm= conn=True cmd=- note=send PLACE [40.6, 59.15, 12.83, -1.4, -78.04, -29.53]
[EVT  ] L0_PLACE   measure        gv=50 gm=False conn=True cmd=- note=after_move_to_PLACE
[EVT  ] loop_end   measure        gv= gm= conn=True cmd=- note=loop 0 done
[EVT  ] home       measure        gv= gm= conn=True cmd=- note=HOME return after tour
[EVT  ] grip       gripper_cmd    gv= gm= conn=True cmd=set_gripper_state(0,100) note=
  [Pi] GRIPPER: OPEN result= None
[EVT  ] disconnect daemon_close   gv= gm= conn=False cmd=- note=

value 50 では、ブロックを挟ませようとしても緩くて掴めなかった。gv が grip 直後で 51、目標 50 に対してそのまま開度が止まっている。これは「閉じ切れていない」のではなく「閉じが浅すぎて、まだブロックに届いていない」状態。gv は開で 97、閉じるほど小さくなるので、50 はまだかなり開き気味で、ブロック厚み相当の 36 まで閉じていない。50 は掴める範囲の外側、ということが1点踏めた。

次に 36、20、10 と振った。実行ログを全量で載せる。

[Mac] python3 grip_hold_capture_diag.py --grip-value 36 --home-grip-tour --loops 1
================================================================================
DAY259 grip_hold_capture_diag  run_stem=homegriptour_20260629_155841  mode=home_grip_tour
  out_dir = /Volumes/KIOXIA/r2r2r-data/runs/experiments/grip_hold_capture_diag/homegriptour_20260629_155841
================================================================================
... (TOUR ロード省略せず・上と同一の4姿勢) ...
[EVT  ] M0         measure        gv=-1 gm=True conn=True cmd=- note=tour_open_before_grip
[EVT  ] grip       gripper_cmd    gv= gm= conn=True cmd=set_gripper_value(36,100) note=
  [Pi] GRIPPER: VALUE=36 speed=100 result= None
[EVT  ] M_grip     measure        gv=-1 gm=False conn=True cmd=- note=tour_after_grip gv_invalid
[EVT  ] L0_APPROACH_PICK measure        gv=-1 gm=False conn=True cmd=- note=after_move_to_APPROACH_PICK gv_invalid
[EVT  ] L0_PICK    measure        gv=-1 gm=False conn=True cmd=- note=after_move_to_PICK gv_invalid
[EVT  ] L0_APPROACH_PLACE measure        gv=-1 gm=False conn=True cmd=- note=after_move_to_APPROACH_PLACE gv_invalid
[EVT  ] L0_PLACE   measure        gv=42 gm=False conn=True cmd=- note=after_move_to_PLACE
[EVT  ] loop_end   measure        gv= gm= conn=True cmd=- note=loop 0 done

value 36 では、PLACE 到達後の gv が 42 になった。目標は 36 なのに 42 で止まっている。これは 36 まで閉じ切れず、ブロック厚みに当たって 42 で止まったということ。爪がブロックに当たって止まっている挙動で、握れている。

[Mac] python3 grip_hold_capture_diag.py --grip-value 20 --home-grip-tour --loops 1
================================================================================
DAY259 grip_hold_capture_diag  run_stem=homegriptour_20260629_160010  mode=home_grip_tour
================================================================================
... (TOUR ロード・4姿勢同一) ...
[EVT  ] M0         measure        gv=-1 gm=True conn=True cmd=- note=tour_open_before_grip
[EVT  ] grip       gripper_cmd    gv= gm= conn=True cmd=set_gripper_value(20,100) note=
  [Pi] GRIPPER: VALUE=20 speed=100 result= None
[EVT  ] M_grip     measure        gv=-1 gm=False conn=True cmd=- note=tour_after_grip gv_invalid
[EVT  ] L0_APPROACH_PICK measure        gv=-1 gm=False conn=True cmd=- note=after_move_to_APPROACH_PICK gv_invalid
[EVT  ] L0_PICK    measure        gv=-1 gm=False conn=True cmd=- note=after_move_to_PICK gv_invalid
[EVT  ] L0_APPROACH_PLACE measure        gv=-1 gm=False conn=True cmd=- note=after_move_to_APPROACH_PLACE gv_invalid
[EVT  ] L0_PLACE   measure        gv=-1 gm=False conn=True cmd=- note=after_move_to_PLACE gv_invalid
[EVT  ] loop_end   measure        gv= gm= conn=True cmd=- note=loop 0 done
[Mac] python3 grip_hold_capture_diag.py --grip-value 10 --home-grip-tour --loops 1
================================================================================
DAY259 grip_hold_capture_diag  run_stem=homegriptour_20260629_160203  mode=home_grip_tour
================================================================================
... (TOUR ロード・4姿勢同一) ...
[EVT  ] M0         measure        gv=-1 gm=True conn=True cmd=- note=tour_open_before_grip
[EVT  ] grip       gripper_cmd    gv= gm= conn=True cmd=set_gripper_value(10,100) note=
  [Pi] GRIPPER: VALUE=10 speed=100 result= None
[EVT  ] M_grip     measure        gv=-1 gm=False conn=True cmd=- note=tour_after_grip gv_invalid
[EVT  ] L0_APPROACH_PICK measure        gv=-1 gm=False conn=True cmd=- note=after_move_to_APPROACH_PICK gv_invalid
[EVT  ] L0_PICK    measure        gv=-1 gm=False conn=True cmd=- note=after_move_to_PICK gv_invalid
[EVT  ] L0_APPROACH_PLACE measure        gv=-1 gm=False conn=True cmd=- note=after_move_to_APPROACH_PLACE gv_invalid
[EVT  ] L0_PLACE   measure        gv=-1 gm=False conn=True cmd=- note=after_move_to_PLACE gv_invalid
[EVT  ] loop_end   measure        gv= gm= conn=True cmd=- note=loop 0 done

目視の結果はこうだった。value 50 は緩くて掴めない。36 は掴める。20 は一番しっかり掴んだ。10 は 20 より少し緩い。ただし 36 / 20 / 10 のどれも、4姿勢を巡回する間にブロックは落ちなかった。落ちたのは「そもそも掴めなかった」50 だけで、握れたケースは全部、巡回を通して保持できた。

20 と 10 の関係には引っかかりが残った。目標が小さいほど押し込みが深くなるなら、10 は 20 より強く握るはずなのに、10 のほうが緩かった。gv と実際の握り強さは単純な比例ではない。この逆転の原因はこの回では確定できていない。

gv の値そのものにも差が出ている。36 では PLACE 到達後に 42 と読めたが、20 と 10 では巡回を通してずっと -1 のまま読めなかった。gm(グリッパー動作中フラグ)は False なのに gv が -1 になる。これは前回の測定で見えていた「押し込み中は爪が当て込み続けて gv が読みにくい」がそのまま出ている。20 と 10 は目標が深いぶん、爪がブロックに当て込み続けて開度値が安定しない。だから判定は gv ではなく目視を主に置く、という前回の割り切りが、ここで正しかったことになる。

4つの値の結果を整理する。

grip-value

掴めたか

巡回中の落下

gv(PLACE到達後)

50

緩くて掴めない

―(掴めず)

50(開き気味で止まる)

36

掴める

落ちない

42(厚みに当たって止まる)

20

しっかり掴む

落ちない

-1(当て込み継続で読めず)

10

やや緩い

落ちない

-1(当て込み継続で読めず)

この検証で、value 20 を暫定の保持値に置いた。一番しっかり掴んだ値で、巡回中も落ちなかった。

通しの把持を値ベースに切り替える

ここまでは HOME でブロックを手で挟ませる検証だった。通しの Pick & Place は条件が違う。実際にブロックに爪を降ろして掴む(手で挟ませない)。掴んだあと PLACE まで保持が続くので、巡回より移動が長い。掴んだあとに爪先とブロックの差分測定や、撮影による成否判定が挟まる。手で挟ませる検証で 20 が効いたからといって、降ろして掴む本番でも効くとは限らない。

通しの把持を、閉じ切り state=1 から値ベース 20 に切り替えた。把持は _do_gripper という1つの関数に集約されている。この関数の close(掴む)のときだけ値ベースに変え、open(離す)は従来の state=0 のまま残す。close を呼ぶのは PICK の握りの1箇所だけで、開放(最初のグリッパー開、掴み直しの開、PLACE での離し)はすべて open なので、close だけが切り替わる構造になっている。

切り替えは、閉じ切る state=1 の経路を本体から外す判断とセットにした。今回の方針として、把持はこれから押し込み保持で固定する。戻したいときは目標値を振るか、変更を戻す。

GRIP_VALUE 定数と、切り替え後の _do_gripper 全量。

GRIP_VALUE = 20                   # close の push 保持目標値(DAY261 tour 実測の暫定値)。
                                  # 閉じ切り state(1) は「位置で止まり握り直さず落ちる」(DAY259)。
                                  # set_gripper_value(20) で閉じ切らず押し込み力を残す。
                                  # gv は 開=97 / 把持厚み相当≈36 / これより小さいほど押し込み深い。
                                  # 効かなければこの値のみ振る(descend 把持は tour と条件が違う点に留意)。
def _do_gripper(action, stage_label, log_record, dry_run):
    """グリッパー操作。戻り値(result=-1 含む)は DAY175 仕様で判定に使わない。
    一方、daemon TCP 例外は本物の失敗として bool False を返す(close 未遂のまま
    LIFT などの事故を防ぐ)。

    DAY261: close は set_gripper_value(GRIP_VALUE) で push 保持に切替(位置で
    止まらず押し込み力を残す)。open は従来どおり set_gripper_state(0)。

    戻り値: True = 正常 or dry_run / False = daemon 例外(呼び出し側で ABORT 連動)
    """
    state = 1 if action == "close" else 0
    is_close = (action == "close")
    print("[GRIP  ] {}  action={}  method={}  sleep={:.1f}s  dry_run={}".format(
        stage_label, action,
        "value({})".format(GRIP_VALUE) if is_close else "state(0)",
        GRIPPER_SLEEP, dry_run))
    rec = {
        "stage": stage_label, "action": action, "state": state,
        "sleep_sec": GRIPPER_SLEEP, "dry_run": dry_run,
        # DAY261: 把持方式の判別用。dry-run でも残るよう rec 構築時に書く。
        "grip_method": "value" if is_close else "state",
    }
    if is_close:
        rec["grip_value"] = GRIP_VALUE
    ok = True
    if not dry_run:
        try:
            if is_close:
                rec["daemon_return"] = daemon_set_gripper_value(GRIP_VALUE)
            else:
                rec["daemon_return"] = daemon_set_gripper(state)
            # 戻り値(result=-1 等)は判定に使わない(DAY175・成否無関係)
        except Exception as e:
            rec["error"] = str(e)
            ok = False
            print("[GRIP  ] [ERROR] {}: daemon 例外: {}".format(stage_label, e))
        time.sleep(GRIPPER_SLEEP)
    log_record.setdefault("gripper", []).append(rec)
    return ok

この変更も把持の挙動を本線の制御フローで変えるので、外部レビューを通した。重点的に見てほしいと依頼したのは、掴んだあとに挟まる測定や判定が gv の戻り値に依存していないか、という点だった。押し込み保持では gv が -1 になりうるので、もし判定がそれに依存していたら通しが壊れる。レビューの結論は、本体フローに gv や gm を読む箇所がないこと、掴んだあとの判定は撮影画像と握る前に取った差分だけで決まること、だった。押し込み中に gv が -1 になっても判定は壊れない。tour では見えなかった、通しに特有の副作用が構造的に刺さらないことが確認できた。

レビューは全項目で問題なし。実機の通しに進んだ。

通しを1本流す

切り替えたコードで通しを1本流した。実行ログを全量で載せる。

[Mac] python3 run_pick_place.py
======================================================================
[RUN] run_pick_place.py  dry_run=False  log=run_0001_20260629_163524.json
======================================================================
  [Pi] fresh_mode=1
[INIT  ] fresh_mode=1
[STAGE 1] HOME 復帰 + グリッパー開
[SEND] stage1_to_home: send_angles (single) [  +0.00,   +0.00,   +0.00,   +0.00,   +0.00,  -30.00]  speed=20 wait=1.0s  (pre max|Δ|=78.92°, post-send timeout 13.8s)
[SEND] arrival OK: max|Δ|=1.93° (tol 2.5°)
[MOVE  ] stage1_to_home  arrived=True retried=False post_max|Δ|=1.930°
[GRIP  ] stage1_grip_open  action=open  method=state(0)  sleep=2.0s  dry_run=False
  [Pi] GRIPPER: OPEN result= None
[STAGE 2] 赤ブロック + 白箱 同時検出(block と box は同回・同 homography)
[CAM] 接続中カメラ一覧:
  index=0  name=FaceTime HDカメラ
  index=1  name=Iriun Camera
  index=2  name=‎iPhone14 Pro MAXのカメラ
[CAM] 上カメラ(iPhone)  index=2
[CAM] 横カメラ(Iriun)   index=1
[DETECT] resolved top index: 2
[Mac] マット四隅検出中... (10フレーム)
[Mac] ✓ マット四隅検出: 10/10 フレーム成功
[Mac] ✓ 動的homography計算完了
[Mac] ✓ マット範囲: X=[369, 1596] Y=[106, 989]
[06f] TEMPLATES_DIR overridden to: /Users/shimazueiichi/r2r2r-scripts/dynamic_tracking_limits/place/templates
[06f] ROTATION_ANGLES_DEG overridden to: [0, 15, 30, 45, 60, 75]
[06f] SCALES overridden to: [0.85, 1.0, 1.15]
[テンプレ] 計 8 枚 (day165=8, auto=0)
[06f] DETECT: center=(1420,657) score=0.656 angle=0° scale=1.0 size=(197x184) template=white_box_20260520_163838_005.png (eval 4.42s)
[DETECT][1/3] box Unity=(-0.222,+0.194) score=0.6557058095932007
  赤ブロック pixel=(519, 727) → Unity=(0.2358, 0.2293)
[DETECT][1/3] block Unity=(+0.236,+0.229) score=null
[06f] DETECT: center=(1420,657) score=0.656 angle=0° scale=1.0 size=(197x184) template=white_box_20260520_163838_005.png (eval 4.46s)
[DETECT][2/3] box Unity=(-0.222,+0.194) score=0.6556802988052368
  赤ブロック pixel=(519, 727) → Unity=(0.2358, 0.2293)
[DETECT][2/3] block Unity=(+0.236,+0.229) score=null
[06f] DETECT: center=(1420,657) score=0.655 angle=0° scale=1.0 size=(197x184) template=white_box_20260520_163838_005.png (eval 4.55s)
[DETECT][3/3] box Unity=(-0.222,+0.194) score=0.6551604270935059
  赤ブロック pixel=(519, 727) → Unity=(0.2358, 0.2293)
[DETECT][3/3] block Unity=(+0.236,+0.229) score=null
[DETECT] block_xz=[0.23581591796875, 0.22931744384765626] block_score=None
[DETECT] box_xz=[-0.22202584838867187, 0.19393476867675782] box_score=0.6556802988052368
[CORR ] key=x240_z220 dx=-7.49mm dz=-9.77mm n=2  target (+235.8,+229.3) -> (+243.3,+239.1) [mm]
[LOOKUP] stage3_approach_pick  phase=approach_pick  target_xz=(+0.2433,+0.2391)  → j0=-38.679°  method=interp  x_resid=0.00000m  z_resid=0.00123m  joint_ok=True
[SEND] stage3_approach_pick: send_angles (single) [ -38.68,  +87.74,  -38.33,   +3.60,  -70.11,  -30.00]  speed=20 wait=1.0s  (pre max|Δ|=86.25°, post-send timeout 14.9s)
[SEND] arrival OK: max|Δ|=1.42° (tol 2.0°)
[MOVE  ] stage3_approach_pick  arrived=True retried=False post_max|Δ|=1.420°
[LOOKUP] stage4_pick  phase=pick  target_xz=(+0.2433,+0.2391)  → j0=-41.930°  method=fk_nearest  x_resid=0.01334m  z_resid=0.01106m  joint_ok=True
[SEND] stage4_pick: send_angles (single) [ -41.93,  +87.74,  -33.33,   +8.60,  -50.11,  -30.00]  speed=20 wait=1.0s  (pre max|Δ|=19.49°, post-send timeout 8.0s)
[SEND] arrival OK: max|Δ|=1.92° (tol 3.0°)
[MOVE  ] stage4_pick  arrived=True retried=False post_max|Δ|=1.920°
[CAM] 接続中カメラ一覧:
  index=0  name=FaceTime HDカメラ
  index=1  name=Iriun Camera
  index=2  name=‎iPhone14 Pro MAXのカメラ
[CAM] 上カメラ(iPhone)  index=2
[CAM] 横カメラ(Iriun)   index=1
[CAP-VLM] stage4_pick_tip 保存: /Volumes/KIOXIA/r2r2r-data/runs/run_pick_place/run_0001_20260629_163524/run_0001_20260629_163524_stage4_pick_tip.jpg
[TIP   ] attempt=1 dist=7.2mm (dx=-3.8 dz=-6.2)
[GRIP  ] stage5_grip_close  action=close  method=value(20)  sleep=2.0s  dry_run=False
  [Pi] GRIPPER: VALUE=20 speed=100 result= None
[SEND] stage6_lift: send_angles (single) [ -38.68,  +87.74,  -38.33,   +3.60,  -70.11,  -30.00]  speed=20 wait=1.0s  (pre max|Δ|=18.08°, post-send timeout 8.0s)
[SEND] arrival OK: max|Δ|=1.60° (tol 2.0°)
[MOVE  ] stage6_lift  arrived=True retried=False post_max|Δ|=1.600°
[CAM] 接続中カメラ一覧:
  index=0  name=FaceTime HDカメラ
  index=1  name=Iriun Camera
  index=2  name=‎iPhone14 Pro MAXのカメラ
[CAM] 上カメラ(iPhone)  index=2
[CAM] 横カメラ(Iriun)   index=1
[CAP-VLM] pick 保存: /Volumes/KIOXIA/r2r2r-data/runs/run_pick_place/run_0001_20260629_163524/run_0001_20260629_163524_pick.jpg
[JUDGE ] pick attempt=1 adopted=yes (vlm=yes human=None dist_override=False) confidence=0.82 reason=2枚目の画像でグリッパーの爪の間に赤ブロックが挟まれており、1枚目と比較してブロックの位置がグリッパーと共に移動・上昇しているように見える。床面との接触が確認できない。
[CORR ] mem key=x240_z220 dx=-5.63 dz=-7.97 n=3  (EMA α=0.5・最終 yes のみ更新)
[LOOKUP] stage7_approach_place  phase=approach_place  target_xz=(-0.2220,+0.1939)  → j0=+44.637°  method=interp  x_resid=0.00000m  z_resid=0.01374m  joint_ok=True
[SEND] stage7_approach_place: send_angles (single) [ +44.64,  +57.39,  +17.49,   +0.08,  -89.20,  -29.44]  speed=20 wait=1.0s  (pre max|Δ|=83.57°, post-send timeout 14.5s)
[SEND] arrival OK: max|Δ|=2.72° (tol 3.0°)
[MOVE  ] stage7_approach_place  arrived=True retried=False post_max|Δ|=2.720°
[LOOKUP] stage8_place  phase=place_withblock  target_xz=(-0.2220,+0.1939)  → j0=+44.828°  method=interp  x_resid=0.00000m  z_resid=0.01898m  joint_ok=True
[SEND] stage8_place: send_angles (single) [ +44.83,  +59.15,  +12.83,   -1.40,  -78.04,  -29.53]  speed=20 wait=1.0s  (pre max|Δ|=10.11°, post-send timeout 8.0s)
[SEND] arrival OK: max|Δ|=1.40° (tol 3.0°)
[MOVE  ] stage8_place  arrived=True retried=False post_max|Δ|=1.400°
[GRIP  ] stage9_release  action=open  pre_sleep=2.0s  attempts=2  sleep_each=2.0s  dry_run=False
  [Pi] GRIPPER: OPEN result= None
  [Pi] GRIPPER: OPEN result= None
[SEND] stage9b_swept_retreat: send_angles (single) [ +85.00,  +57.39,  +17.49,   +0.08,  -89.20,  -29.44]  speed=20 wait=1.0s  (pre max|Δ|=40.44°, post-send timeout 8.1s)
[SEND] arrival OK: max|Δ|=2.72° (tol 3.0°)
[MOVE  ] stage9b_swept_retreat  arrived=True retried=False post_max|Δ|=2.720°
[CAM] 接続中カメラ一覧:
  index=0  name=FaceTime HDカメラ
  index=1  name=Iriun Camera
  index=2  name=‎iPhone14 Pro MAXのカメラ
[CAM] 上カメラ(iPhone)  index=2
[CAM] 横カメラ(Iriun)   index=1
[CAP-VLM] place 保存: /Volumes/KIOXIA/r2r2r-data/runs/run_pick_place/run_0001_20260629_163524/run_0001_20260629_163524_place.jpg
[JUDGE ] place: adopted=yes (vlm=yes human=None) confidence=0.97 reason=白い箱の内部に赤い四角いブロックが明確に見える
[SEND] stage10_lift_back: send_angles (single) [ +44.64,  +57.39,  +17.49,   +0.08,  -89.20,  -29.44]  speed=20 wait=1.0s  (pre max|Δ|=40.08°, post-send timeout 8.0s)
[SEND] arrival OK: max|Δ|=2.72° (tol 3.0°)
[MOVE  ] stage10_lift_back  arrived=True retried=False post_max|Δ|=2.720°
[SEND] stage10_to_home: send_angles (single) [  +0.00,   +0.00,   +0.00,   +0.00,   +0.00,  -30.00]  speed=20 wait=1.0s  (pre max|Δ|=88.50°, post-send timeout 15.3s)
[SEND] arrival OK: max|Δ|=2.37° (tol 2.5°)
[MOVE  ] stage10_to_home  arrived=True retried=False post_max|Δ|=2.280°
[RESULT] 完走
  [Pi] fresh_mode=0
[LOG   ] /Volumes/KIOXIA/r2r2r-data/runs/run_pick_place/run_0001_20260629_163524/run_0001_20260629_163524.json
[LEDGER] append OK: /Volumes/KIOXIA/r2r2r-data/runs/pick_place_ledger_v3.csv (in_box_geom=True dist=23.9)

stage5 で value 20 で握り、PICK 判定が yes(確信度 0.82)。判定理由は「爪の間に赤ブロックが挟まれ、ブロックがグリッパーと共に移動・上昇し、床面との接触が確認できない」。持ち上げて保持できている。そのまま stage6 lift から stage7 approach_place、stage8 place まで移動して、この間にブロックは落ちなかった。落ちていれば PLACE の箱内判定は出ない。PLACE 判定も yes(確信度 0.97)で、箱中心からの距離 23.9mm。手で挟ませる検証で見えた効き目が、実際に降ろして掴んで長い距離を運ぶ本番でも成立した。

同じ握りで何本か回す

把持が1本だけ通っても、まぐれかどうかは分からない。値ベース 20 に切り替えたあと、続けて何本か回した。台帳から、切り替え後の試行を拾う。

時刻

pick

place

in_box

箱中心距離(mm)

16:35:24

yes 0.82

yes 0.97

True

23.9

16:38:11

中断

―

―

―

16:38:52

yes 0.82

yes 0.98

False

66.9

16:40:21

yes 0.82

yes 0.97

False

62.0

16:41:48

中断

―

―

―

16:43:05

yes 0.82

中断

―

―

判定が出た回は、すべて pick が yes だった。確信度は 0.82 で揃っている。降ろして掴む把持が、値ベース 20 で繰り返し成立している。把持に限れば、1本目はまぐれではなかった。

一方で、PLACE は箱に入りきらない回があった。16:38:52 と 16:40:21 は pick も place も yes なのに、箱中心からの距離が 66.9mm と 62.0mm で、箱内判定の閾値 52mm を超えている。掴めて運べているのに、置く位置が箱の縁の外にずれている。これは爪と白箱の位置が合っていないためで、把持の問題ではなく PLACE 側の位置精度の問題になる。今回切り替えた押し込み保持とは別の軸の課題で、ここは次回に回す。

今回やったこと

前回、原因を測定で剥がして「位置で止まる握り」に行き着いた。今回はその対策が効くかを測った。閉じ切らない目標値で握って押し込み力を残す方式を、デーモンに足し、手で挟ませる検証で値を振り、通しの把持に入れて、実機で確かめた。

握り方を位置から押し込みに変えたら、ブロックは落ちなくなった。

残っているのは置く位置のずれ。掴んで運ぶところは押し込み保持で通るようになったので、次は爪と箱の位置を合わせる側に入る。

※ プロジェクト名「R2R2R」は、UC Berkeley・Toyota Research Instituteの研究「Real2Render2Real」(Yu, Fu et al., 2025)の概念に触発されています。現実空間をデジタルツイン化し、仮想空間での試行から得た知見を実世界のモーター駆動へ還元する——その設計と実装過程を毎日記録しています。技術アプローチおよび目的は独自のものです。