フィジカルAI研究室を作る Log#12 | Physical Intelligence の π0.5 が初めてブロックを箱に入れた

机の上の小さなロボットアームが、AI の指示だけで赤いブロックをつかみ、持ち上げて、白い箱の中に落としました。人がお手本を見せて学ばせた AI でこのアームを動かし始めてから、ブロックが箱に入ったのはこれが初めてです。同じ日の朝、同じ AI は、つかんだブロックをマットに付けたまま引きずり、箱の壁にぶつけて箱ごと押していました。

朝と夕方で、AI そのものは何も変えていません。学習し直してもいません。変えたのは、AI の答えをアームに届ける側の2か所だけです。

原因は、アームの肩の小さな「届かなさ」でした。ブロックを持って腕を上げる指令を出すと、肩の関節は毎回、目標より約2°低いところで止まります。この日の記録を全部数えると、ブロックを持って肩を上げた指令43回のうち、43回とも目標に届いていませんでした(0.77〜2.80°、真ん中の値は 2.20°)。2°は目で見ても分からないほどの差です。ところが AI は、「いま腕が何度にあるか」を測った値を見て、次の1秒の動きを決めます。低いまま止まった角度を見て、そこからまた「これだけ上げる」と答えるので、足りなかった分は取り戻されず、毎回積み重なっていきます。0.5秒ずつ動かした回では、合計 41.2° 上げる指令に対して、実際に上がったのは 11.1° でした。

使った AI は、米国の Physical Intelligence 社が公開しているロボット用のモデル π0.5(パイレイテンゴ)です。上から見たカメラの画像と、6つの関節の角度を受け取り、次の1秒(10ステップ)の関節の角度を返します。人が手でアームを動かして教えた動きを19回録り、それを見せて学習させました(模倣学習)。

AI が悪いのか、アームが悪いのかを分けるために、お手本そのものをアームでもう一度再生して、横から撮りました。再生ではブロックは毎回箱に入り、ブロックは箱のふちより十分に高いところを通っていました。お手本の再生は、毎回「この角度へ行け」と決まった角度を送るので、2°の不足はその場かぎりで、次へ持ち越されません。AI で動かすときだけ、不足が持ち越されていたことになります。

そこで、AI に渡す「いまの角度」を、測った値ではなく、直前にアームへ送った目標の角度に変えました。AI に渡す角度を、測った値から直前に送った目標に変えると、肩の不足が積み重ならなくなり、ブロックが初めて箱に入りました。 あわせて、1秒分の答えのうち最後の姿勢だけを送っていたのを、途中の姿勢も順に送るように変え、「持ち上げてから回る」という順番を保つようにしています。

やったことの全体は次のとおりです。

やったことなぜ結果
お手本と同じ位置にブロックを置く道具を作った
AI が学習した場面と同じ条件で試すため
位置C・D・E のどれでも、お手本の最初のコマを重ねて置けるようになった
位置C で AI に動かさせた(朝)
前の回の「持ち上げずに引きずる」が、置き場所のせいかを確かめるため
お手本どおりの位置でも、引きずって箱の壁に当てた
肩を上げる指令と、実際に上がった角度を全部数えた
どこで高さを失っているかを知るため
43回とも目標より低く止まった。下げる向きは約1°行き過ぎる
同じ目標を送り直して詰める処理を足した
2°の不足を、その場で取り戻せるかを見るため
送り直しても縮まらなかった
1秒分の途中の姿勢も送るようにした
お手本は、つかんだあと1.5秒その場で持ち上げてから回っていたため
順番は保てたが、高さはまだ足りなかった
お手本の動きをそのまま実機で再生し、横から撮った
AI とアームのどちらに原因があるかを分けるため
4回とも箱に入った。ブロックは箱のふちより上を通った
AI に渡す角度を、直前に送った目標にした
不足が積み重なる仕組みを断つため
18:16 の回で、つかんで持ち上げ、箱に入れた
18:16 の回を横から撮った動画のコマ。ブロックの上で深くつかみ、持ち上げてから右へ回り、箱の上で離して、ブロックが箱の中に残っている

ただ、続けて試した回は安定していません。18:26 の回は箱の位置で爪を開きましたが、動画がなく、箱に入ったかは確かめられていません。18:30 と 18:32 の回はブロックをつかみ損ねました。そのあと、手首の関節 j4 が +45.87° のまま動かなくなり、この日の実機はここで終えています。お手本の他の置き場所(位置D・E)はまだ試せていません。

この回の作業も、Claude(この記事の手順とプログラムを一緒に組み立てた AI)と進めました。手順・分析・記録の集計は Claude、アームを動かすプログラムの変更は Claude Code、その確認は codex(別の AI)が行い、アームの操作と、目で見た判断・止める判断はこちらで行っています。ここから下は、使ったコマンド・コード・出力を全量で載せた作業ログです。ログの中の Pod(借りた GPU マシン)の IP アドレスとポート番号は、<POD_IP> と <SSH_PORT> に置き換えています。コミットの説明文や Claude Code の報告に出てくる「LOG359」は、記事の番号が決まる前に仮に付けた番号で、この記事(LOG361)のことです。

作業ログ ― 推論用の Pod を借り直し、学習済みのモデルを送ってサーバーを起動した

前の回(LOG358)で学習し直したモデル(pi05_mecharm_trim_lora・お手本 teach_v1_trim2・4,000 ステップ)は、Mac につないだ外付け SSD(KIOXIA)の checkpoints/pi05_mecharm_trim_lora_v1/3999/ に持ち帰ってあります。推論だけなら学習ほど GPU のメモリは要らないので、Runpod で推論用の Pod を借り直しました。朝の Pod は GPU の型番と料金を記録していません。夕方に借り直した Pod は A40(46GB)で、料金は記録していません。

Mac から、mechArm 用のスクリプト一式と学習設定の追加スクリプト、モデルを送りました。モデルは大きいファイルから順に4本同時に送り、送り終えたあとファイルの大きさを Pod 側と1つずつ照らし合わせています。

(base) shimazueiichi@shimadzueiichinoMacBook-Pro 3999 % P=<SSH_PORT>; IP=<POD_IP>; K=~/.ssh/id_ed25519
ssh -p $P -i $K root@$IP "mkdir -p /workspace/r2r2r /workspace/logs"
scp -P $P -i $K -r ~/r2r2r-scripts/pi05 root@$IP:/workspace/r2r2r/
scp -P $P -i $K /Volumes/KIOXIA/r2r2r-data/lab/Log11/pod/add_trim_config.py root@$IP:/workspace/r2r2r/
cd /Volumes/KIOXIA/r2r2r-data/checkpoints/pi05_mecharm_trim_lora_v1/3999
find . -type d | ssh -p $P -i $K root@$IP "mkdir -p /workspace/ckpt/3999 && cd /workspace/ckpt/3999 && xargs mkdir -p"
find . -type f -exec stat -f '%z %N' {} + | sort -rn | cut -d' ' -f2- | xargs -P 4 -I{} scp -q -P $P -i $K {} root@$IP:/workspace/ckpt/3999/{}
ssh -p $P -i $K root@$IP "cd /workspace/ckpt/3999 && find . -type f -printf '%s %p\n'" > /tmp/remote_ck.txt
find . -type f | while read f; do s=$(stat -f %z "$f"); grep -qx "$s $f" /tmp/remote_ck.txt || echo "不一致: $f"; done; echo "確認おわり"
eval_policy_offline.py                        100% 9921    35.6KB/s   00:00    
convert_to_lerobot.py                         100% 2873    10.5KB/s   00:00    
run_policy_client.py                          100%   24KB  88.5KB/s   00:00    
check_dataset.py                              100%   14KB  52.2KB/s   00:00    
build_demo_episodes.py.bak_pre_LOG352_2026101 100%   11KB  40.6KB/s   00:00    
RUNPOD_TRAIN.md                               100%   15KB  54.0KB/s   00:00    
install_addon.py                              100% 4786    17.4KB/s   00:00    
mecharm_policy.py                             100% 2321     8.5KB/s   00:00    
eval_policy_offline.cpython-312.pyc           100%   15KB  55.2KB/s   00:00    
build_demo_episodes.cpython-310.pyc           100%   11KB  39.5KB/s   00:00    
build_demo_episodes.cpython-312.pyc           100%   29KB 107.9KB/s   00:00    
check_dataset.cpython-312.pyc                 100%   25KB  93.5KB/s   00:00    
policy_ws.cpython-312.pyc                     100% 4580    16.4KB/s   00:00    
camera_align.cpython-312.pyc                  100%   18KB  68.5KB/s   00:00    
run_policy_client.cpython-312.pyc             100%   30KB 110.6KB/s   00:00    
build_demo_episodes.py                        100%   24KB  90.5KB/s   00:00    
policy_ws.py                                  100% 2778    10.1KB/s   00:00    
camera_align.py                               100%   13KB  49.1KB/s   00:00    
add_trim_config.py                            100% 1966     7.0KB/s   00:00    

Pod 側では、tmux(接続が切れても作業が続く画面)を先に1行だけで開き、その中で openpi(π0.5 を動かすための公開コード、コミット 215abfb)を取り直して uv sync し、mechArm 用の追加(install_addon.py)と学習設定の登録(add_trim_config.py)を入れてから、推論サーバーを起動しました。手順は前の回と同じで、設定名とお手本の名前だけが pi05_mecharm_trim_lora・mecharm_redblock_box_teach_v1_trim2 に変わります。Pod の画面の出力は残していないので、使ったコマンドだけを載せます。

tmux new -s p
python3 /workspace/r2r2r/pi05/openpi_addon/install_addon.py /workspace/openpi
python3 /workspace/r2r2r/add_trim_config.py /workspace/openpi
cd /workspace/openpi
mkdir -p assets/pi05_mecharm_trim_lora/local && cp -r /workspace/ckpt/3999/assets/local/mecharm_redblock_box_teach_v1_trim2 assets/pi05_mecharm_trim_lora/local/
uv run scripts/serve_policy.py policy:checkpoint --policy.config=pi05_mecharm_trim_lora --policy.dir=/workspace/ckpt/3999 2>&1 | tee /workspace/logs/serve.log

Mac 側からは、Pod の 8000 番を手元の 8000 番へつなぐ SSH のトンネルを張り、アームを動かすクライアントは ws://localhost:8000 に問い合わせます。

ssh -N -L 8000:localhost:8000 -p <SSH_PORT> -i ~/.ssh/id_ed25519 root@<POD_IP>

KIOXIA の lab/Log12/serve.log に持ち帰った、朝のサーバーのログです(10時28分に持ち帰った時点のもの)。時刻は Pod の時計(世界標準時)です。

INFO:root:Loading model...
INFO:2026-10-22 00:52:10,003:jax._src.xla_bridge:925: Unable to initialize backend 'rocm': module 'jaxlib.xla_extension' has no attribute 'GpuAllocatorConfig'
INFO:jax._src.xla_bridge:Unable to initialize backend 'rocm': module 'jaxlib.xla_extension' has no attribute 'GpuAllocatorConfig'
INFO:2026-10-22 00:52:10,005:jax._src.xla_bridge:925: Unable to initialize backend 'tpu': INTERNAL: Failed to open libtpu.so: libtpu.so: cannot open shared object file: No such file or directory
INFO:jax._src.xla_bridge:Unable to initialize backend 'tpu': INTERNAL: Failed to open libtpu.so: libtpu.so: cannot open shared object file: No such file or directory
INFO:absl:orbax-checkpoint version: 0.11.13
INFO:absl:Created BasePyTreeCheckpointHandler: use_ocdbt=True, use_zarr3=False, pytree_metadata_options=PyTreeMetadataOptions(support_rich_types=False), array_metadata_store=<orbax.checkpoint._src.metadata.array_metadata_store.Store object at 0x706101a9bb10>
INFO:absl:Restoring checkpoint from /workspace/ckpt/3999/params.
INFO:absl:[thread=MainThread] Failed to get flag value for EXPERIMENTAL_ORBAX_USE_DISTRIBUTED_PROCESS_ID.
INFO:absl:[process=0] /jax/checkpoint/read/bytes_per_sec: 1.3 GiB/s (total bytes: 6.3 GiB) (time elapsed: 5 seconds) (per-host)
INFO:absl:Finished restoring checkpoint in 5.07 seconds from /workspace/ckpt/3999/params.
INFO:openpi.shared.download:Downloading gs://big_vision/paligemma_tokenizer.model to /root/.cache/openpi/big_vision/paligemma_tokenizer.model
INFO:tqdm_loggable.tqdm_logging:Progress on: -/4264023 rate:- remaining:? elapsed:00:00 postfix:-
INFO:tqdm_loggable.tqdm_logging:Progress on: 4.07MiB/4.07MiB rate:4,259,565.7iB/s remaining:00:00 elapsed:00:01 postfix:-
INFO:root:Loaded norm stats from /workspace/openpi/assets/pi05_mecharm_trim_lora/local/mecharm_redblock_box_teach_v1_trim2
INFO:root:Loaded norm stats from /workspace/ckpt/3999/assets/local/mecharm_redblock_box_teach_v1_trim2
INFO:root:Creating server (host: 05c03cfb11fc, ip: 172.16.80.2)
INFO:websockets.server:server listening on 0.0.0.0:8000
INFO:websockets.server:connection open
INFO:openpi.serving.websocket_policy_server:Connection from ('127.0.0.1', 37440) opened
INFO:openpi.serving.websocket_policy_server:Connection from ('127.0.0.1', 37440) closed
INFO:websockets.server:connection open
INFO:openpi.serving.websocket_policy_server:Connection from ('127.0.0.1', 40204) opened
INFO:openpi.serving.websocket_policy_server:Connection from ('127.0.0.1', 40204) closed
INFO:websockets.server:connection open
INFO:openpi.serving.websocket_policy_server:Connection from ('127.0.0.1', 52988) opened
INFO:openpi.serving.websocket_policy_server:Connection from ('127.0.0.1', 52988) closed
INFO:websockets.server:connection open
INFO:openpi.serving.websocket_policy_server:Connection from ('127.0.0.1', 55516) opened
INFO:openpi.serving.websocket_policy_server:Connection from ('127.0.0.1', 55516) closed

Loaded norm stats from …/mecharm_redblock_box_teach_v1_trim2 の2行で、学習に使ったお手本(trim2)から計算した正規化の値を読んだことが分かります。connection open と closed の組は、Mac のクライアントが1回つなぐたびに出る行です。

お手本と同じ位置にブロックを置くため、camera_align.py に —episode を足した

この記事のコード・ログ・データは、1ファイルずつ下に置いています。全文はリンク先にあります。

ファイルの一覧(30本)

01_camera_align.py_--episode.txt ― お手本と同じ位置にブロックを置くため、camera_align.py に --episode を足した・4,937字

02_sec03.txt ― 位置C の朝の回は、つかんで運んだが、引きずって箱の壁に当てた・5,816字

03_sec03.txt ― 位置C の朝の回は、つかんで運んだが、引きずって箱の壁に当てた・9,661字

04_sec03.txt ― 位置C の朝の回は、つかんで運んだが、引きずって箱の壁に当てた・6,032字

05_sec04.txt ― 0.5 秒ずつ動かした 10:20 の回は、深くつかみ、持ち上がらないまま31回問い合わせた・3,052字

06_sec04.txt ― 0.5 秒ずつ動かした 10:20 の回は、深くつかみ、持ち上がらないまま31回問い合わせた・7,857字

07_settle.txt ― 同じ目標を送り直して詰める settle を足したが、ずれは縮まらなかった・3,307字

08_settle.txt ― 同じ目標を送り直して詰める settle を足したが、ずれは縮まらなかった・4,249字

09_settle.txt ― 同じ目標を送り直して詰める settle を足したが、ずれは縮まらなかった・4,291字

10_settle.txt ― 同じ目標を送り直して詰める settle を足したが、ずれは縮まらなかった・5,896字

11_1_--via-stride.txt ― 1秒分の途中の姿勢も送る --via-stride を足した・4,019字

12_1_--via-stride.txt ― 1秒分の途中の姿勢も送る --via-stride を足した・3,735字

13_1_--via-stride.txt ― 1秒分の途中の姿勢も送る --via-stride を足した・13,091字

14_sec10.txt ― お手本の動きを実機でそのまま再生すると、ブロックは4回とも箱に入った・5,202字

15_sec10.txt ― お手本の動きを実機でそのまま再生すると、ブロックは4回とも箱に入った・6,659字

16_sec10.txt ― お手本の動きを実機でそのまま再生すると、ブロックは4回とも箱に入った・11,491字

17_sec10.txt ― お手本の動きを実機でそのまま再生すると、ブロックは4回とも箱に入った・3,822字

18_AI_--state-from-target.txt ― AI に渡す角度を、直前に送った目標にする --state-from-target を足した・3,590字

19_sec12.txt ― 18:16 の回で、π0.5 がブロックを初めて箱に入れた・8,128字

20_sec12.txt ― 18:16 の回で、π0.5 がブロックを初めて箱に入れた・23,898字

21_sec13.txt ― 続く4回は、2回を1チャンクで止め、1回は箱の位置で離し、2回はつかみ損ねた・6,231字

22_sec13.txt ― 続く4回は、2回を1チャンクで止め、1回は箱の位置で離し、2回はつかみ損ねた・19,858字

23_sec13.txt ― 続く4回は、2回を1チャンクで止め、1回は箱の位置で離し、2回はつかみ損ねた・21,597字

24_sec14.txt ― 位置D に移ろうとしたところで、手首 j4 が +45.87° のまま動かなくなった・14,855字

25_sec14.txt ― 位置D に移ろうとしたところで、手首 j4 が +45.87° のまま動かなくなった・7,010字

26_sec14.txt ― 位置D に移ろうとしたところで、手首 j4 が +45.87° のまま動かなくなった・3,580字

27_sec15.txt ― 実機の回すべてのチャンクの記録・30,101字

28_sec15.txt ― 実機の回すべてのチャンクの記録・6,789字

29_sec16.py.txt ― この回で変えた2つのファイルの全文(run_policy_client.py・camera_align.py)・35,559字

30_sec16.py.txt ― この回で変えた2つのファイルの全文(run_policy_client.py・camera_align.py)・10,409字

前の回の夕方は、ブロックをお手本の置き場所と置き場所の間に置いていました。今回は、AI が学習した場面と同じ条件で試すため、お手本とまったく同じ位置に置くことにしました。お手本は5か所(A〜E)で録っていて、今回は位置C から試します。

上のカメラの写り方を確かめる camera_align.py は、ライブの映像にお手本の最初のコマを半透明で重ねて表示します。これまでは重ねるお手本が位置A の1本に決まっていたので、どのお手本を重ねるかを選べるようにしました(Claude Code の実装)。重なって見えるお手本のブロックに、実物のブロックを合わせれば、同じ位置に置けます。Claude Code の報告です。

01_camera_align.py_—episode.txt4,937字・92行・text
別のタブで開く保存

コミットの説明文です。

commit b5b093c700893257a9c65ac87093299debb0d715
Date:   Wed Oct 21 20:12:38 2026 +0900

camera_align.py: --episode で重ねるお手本を選べるようにする (LOG359)

LOG359 の実機テストで, ブロックをお手本と同じ位置 (C / D / E) に置くため.
これまで重ねる基準は --raw-dir の最初のエピソード (20261019_183454・位置A) に
固定だったので, 位置A のブロックにしか合わせられなかった.
重なった画像のブロックに実物のブロックを合わせれば, そのお手本と同じ位置に置ける.

- pick_episode(raw_dir, episode=None) を新設. episode=None なら最初のエピソード
  (従来どおり). 完全一致 -> 1 本だけに当たる部分一致 (192343 など) の順で探し,
  見つからない / 複数当たるときはエピソード一覧を出して ABORT する.
- load_reference(raw_dir, episode=None) に第 2 引数を追加. 既定は None なので
  run_policy_client.py の呼び出し (写り方の検査) は最初のエピソードのまま変わらない.
- grasp_j0() を新設し, [REF] 行と画面左下に お手本名 と つかんだ瞬間の j0 を出す.
  どの位置のお手本を重ねているかが分かるように (check_dataset.py の項目 5 と同じ見方).
- 画面の文字を黒で縁取る (_put). 薄いグレーのままだと机の白に重なった行が読めなかった.

判定 (dy / dx / response) としきい値は変更なし. アームに送る処理は触っていない.

確認 (カメラを開く手前まで・実データ):
  --episode なし                     -> 183454 (位置A) つかんだ j0=-36.4°
  --episode run_0001_20261019_192343 -> 192343 (位置C) つかんだ j0=-27.7°
  --episode run_0001_20261019_193404 -> 193404 (位置D) つかんだ j0=-61.6°
  --episode run_0001_20261019_194818 -> 194818 (位置E) つかんだ j0=+13.2°
  --episode 192343 (部分一致)        -> 192343
  存在しない名前 -> 19 本の一覧を出して終了 / run_0001 (19 本該当) -> 複数一致で終了

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

位置C・D・E のお手本の最初のコマです。位置E は、たたんだ姿勢のアームの右の爪の下にブロックが隠れます。

上カメラで見たお手本の最初のコマ。左から位置C(192343)・位置D(193404)・位置E(194818)。位置E のブロックは右の爪の陰にある

お手本の上カメラの画像から、ブロックがマットのどこにあるかを mm で出しました。マットの四隅を画像で見つけ、赤い部分の中心と向きを測るスクリプトです。

# お手本の最初のコマで、赤いブロックの中心をマットの端からの距離 (mm) にする (LOG361)
#  マットの横幅 620mm を画像の左右の端の間の画素数で割って 1 画素の大きさを出す
import os
import cv2, numpy as np
ROOT = os.environ.get("R2R2R_DATA", "/Volumes/KIOXIA/r2r2r-data"); os.chdir(ROOT)
R = "datasets/mecharm_redblock_box_teach_v1_trim2/raw_episodes"
def mat_edges(im):
    g = cv2.cvtColor(im, cv2.COLOR_BGR2GRAY); dark = g < 90
    L, Rr = [], []
    for y in range(120, 180):
        xs = np.where(dark[y])[0]; L.append(xs.min()); Rr.append(xs.max())
    yb = np.median([np.where(dark[:, x])[0].max() for x in range(70, 110)])
    return np.median(L), np.median(Rr), yb
def block(im, s_min, v_min):
    hsv = cv2.cvtColor(im, cv2.COLOR_BGR2HSV)
    m = (((hsv[..., 0] < 10) | (hsv[..., 0] > 165)) & (hsv[..., 1] > s_min) & (hsv[..., 2] > v_min)).astype(np.uint8)
    m[:60] = 0
    n, lab, st, cen = cv2.connectedComponentsWithStats(m); k = 1 + np.argmax(st[1:, 4])
    pts = np.column_stack(np.where(lab == k))[:, ::-1].astype(np.float32)
    (_, _), (bw, bh), ang = cv2.minAreaRect(pts)
    return cen[k], st[k, 4], ang, bw, bh
for name, e, s_min, v_min in [("C", "192343", 70, 50), ("C", "192438", 70, 50), ("C", "192539", 70, 50), ("C", "192633", 70, 50),
                               ("D", "193404", 70, 50), ("D", "193459", 70, 50), ("D", "193617", 70, 50), ("D", "193825", 70, 50), ("D", "193932", 70, 50),
                               ("E", "194818", 120, 80), ("E", "194913", 120, 80), ("E", "195023", 120, 80), ("E", "195120", 120, 80)]:
    im = cv2.imread(f"{R}/run_0001_20261019_{e}/frames/000000.jpg")
    xl, xr, yb = mat_edges(im); mm = 620 / (xr - xl)
    (cx, cy), area, ang, bw, bh = block(im, s_min, v_min)
    print(f"{name} {e}  1画素 {mm:.2f}mm  画素数 {area:3d}  左の端から {(cx-xl)*mm:4.0f}mm  下の端から {(yb-cy)*mm:4.0f}mm  向き {ang:5.0f}°  {bw*mm:.0f}x{bh*mm:.0f}mm")

出力です。位置C は、マットの左の端から約 185mm、下の端から約 122mm、45〜53° 回した向きです。位置E は爪の陰で赤い部分が数画素しか見えないので、位置の数字は当てになりません。

C 192343  1画素 2.24mm  画素数 191  左の端から  180mm  下の端から  120mm  向き   -45°  32x33mm
C 192438  1画素 2.24mm  画素数 188  左の端から  184mm  下の端から  121mm  向き   -53°  31x32mm
C 192539  1画素 2.24mm  画素数 180  左の端から  186mm  下の端から  122mm  向き   -53°  33x32mm
C 192633  1画素 2.24mm  画素数 191  左の端から  187mm  下の端から  120mm  向き   -51°  33x32mm
D 193404  1画素 2.23mm  画素数 253  左の端から   63mm  下の端から  244mm  向き   -22°  37x38mm
D 193459  1画素 2.24mm  画素数 252  左の端から   66mm  下の端から  239mm  向き   -20°  38x38mm
D 193617  1画素 2.24mm  画素数 249  左の端から   68mm  下の端から  240mm  向き   -27°  35x37mm
D 193825  1画素 2.24mm  画素数 256  左の端から   68mm  下の端から  239mm  向き   -22°  38x39mm
D 193932  1画素 2.24mm  画素数 253  左の端から   62mm  下の端から  243mm  向き   -31°  37x39mm
E 194818  1画素 2.23mm  画素数  18  左の端から  378mm  下の端から  103mm  向き   -90°  11x7mm
E 194913  1画素 2.24mm  画素数   7  左の端から  356mm  下の端から  120mm  向き   -90°  4x4mm
E 195023  1画素 2.23mm  画素数  12  左の端から  373mm  下の端から  100mm  向き   -90°  11x4mm
E 195120  1画素 2.24mm  画素数  14  左の端から  373mm  下の端から   99mm  向き   -76°  11x7mm

位置C に合わせたときの camera_align.py の画面です。左上に OK と、写り方のずれ(dy・dx)が出ます。

camera_align.py の画面。上カメラのライブ映像にお手本 192343(位置C)の最初のコマを重ね、左上に OK と dy −1.1・dx +0.1 が出ている。ブロックは重なった位置に置いてある

位置C の朝の回は、つかんで運んだが、引きずって箱の壁に当てた

アームを動かす常駐プログラム(daemon。Raspberry Pi の上で動き、Mac からの角度の指令を受けてアームへ送る)が動いているかを確かめ、アームをたたんだ姿勢(HOME)に戻して爪を開いてから、π0.5 で動かしました。1回目(9:52)です。1回の問い合わせで返ってくる10ステップ(1秒分)のうち、最後の姿勢だけを送る、これまでのやり方です。最初の問い合わせは、サーバー側のコンパイルで 52 秒かかっています。

02_sec03.txt5,816字・78行・text
Last login: Thu Oct 22 09:07:18 on ttys006
(base) shimazueiichi@shimadzueiichinoMacBook-Pro ~ % ssh -i ~/.ssh/id_rsa_pi ubuntu@192.168.2.1 'ss -ltn | grep 5555' || { ssh -i ~/.ssh/id_rsa_pi ubuntu@192.168.2.1 "setsid nohup python3 /home/ubuntu/mecharm_daemon.py </dev/null >>/home/ubuntu/daemon.log 2>&1 &"; sleep 5; ssh -i ~/.ssh/id_rsa_pi ubuntu@192.168.2.1 'tail -3 ~/daemon.log; ss -ltn | grep 5555'; }
[DAEMON 14:50:28] ✓ MyCobot 初期化完了
[DAEMON 14:50:28] startup_warmup angles=[-0.35, 93.25, -7.99, 1.84, -86.83, -29.7]
[DAEMON 14:50:28] ポート 5555 で待ち受け中...
LISTEN   0         1                   0.0.0.0:5555             0.0.0.0:*       
(base) shimazueiichi@shimadzueiichinoMacBook-Pro ~ % cd ~/r2r2r-scripts
python3 dynamic_tracking_limits/place/place_modules/08_home_from_anywhere.py
別のタブで開く保存

チャンク03(問い合わせの番号)でつかみ、04〜07 で箱の方へ回り、08 で「爪を開く」と返ってきたところで、送らずに h(HOME に戻して終了)で止めました。

続けて動かそうとすると、上カメラの写り方の確認で3回止まりました。response(お手本の画像との似かたの強さ)が 0.2 を下回り、「測れない」になっています。カメラを開き直してから camera_align.py で確かめ、写り方が OK に戻ってから動かしました。

03_sec03.txt9,661字・118行・text
(base) shimazueiichi@shimadzueiichinoMacBook-Pro r2r2r-scripts % cd ~/r2r2r-scripts
RAW=/Volumes/KIOXIA/r2r2r-data/datasets/mecharm_redblock_box_teach_v1_trim2/raw_episodes
python3 pi05/run_policy_client.py --raw-dir $RAW --steps-per-chunk 10 --execute
[SAFE  ] 安全範囲 (学習 19 本 ±10°): j0[-72,+55]  j1[-9,+88]  j2[-46,+41]  j3[-18,+27]  j4[-90,+10]  j5[-51,-14]
[SAFE  ] 肩 j1 の上限は J1_SAFE_MAX=88° で切った (これを超える指令はアームが黙って捨てる / LOG352)  元データ: /Volumes/KIOXIA/r2r2r-data/datasets/mecharm_redblock_box_teach_v1_trim2/raw_episodes
[MODE  ] EXECUTE  steps/chunk=10  max_move=20.0°  log=/Volumes/KIOXIA/r2r2r-data/runs/pi05_policy/pi05_20261022_095832
[CAMERA] 写り方 dy=-6.7 dx=+7.2 response=-0.059 → 測れない (相関が弱く dx/dy が当てにならない (response=-0.059 < 0.20))
[CAMERA] 基準: /Volumes/KIOXIA/r2r2r-data/datasets/mecharm_redblock_box_teach_v1_trim2/raw_episodes/run_0001_20261019_183454/frames/000000.jpg
別のタブで開く保存

10:06 の回は、動き出しのあとで h で止めています。10:07 の回が、この朝いちばん先まで進んだ回です。

04_sec03.txt6,032字・80行・text
(base) shimazueiichi@shimadzueiichinoMacBook-Pro r2r2r-scripts % python3 pi05/run_policy_client.py --raw-dir $RAW --steps-per-chunk 10 --execute
[SAFE  ] 安全範囲 (学習 19 本 ±10°): j0[-72,+55]  j1[-9,+88]  j2[-46,+41]  j3[-18,+27]  j4[-90,+10]  j5[-51,-14]
[SAFE  ] 肩 j1 の上限は J1_SAFE_MAX=88° で切った (これを超える指令はアームが黙って捨てる / LOG352)  元データ: /Volumes/KIOXIA/r2r2r-data/datasets/mecharm_redblock_box_teach_v1_trim2/raw_episodes
[MODE  ] EXECUTE  steps/chunk=10  max_move=20.0°  log=/Volumes/KIOXIA/r2r2r-data/runs/pi05_policy/pi05_20261022_100713
[CAMERA] 写り方 dy=-6.9 dx=+2.7 response=0.542 → OK (|dy|<=20 / |dx|<=20 / response>=0.20)
[CAMERA] 基準: /Volumes/KIOXIA/r2r2r-data/datasets/mecharm_redblock_box_teach_v1_trim2/raw_episodes/run_0001_20261019_183454/frames/000000.jpg
[POLICY] 接続 ws://localhost:8000  metadata={}
[GRIP  ] 開始時のグリッパー開放は, 最初の目標が検査を通ってから行います (爪が閉じていたら手で開けておく)
別のタブで開く保存

チャンク03 でつかみ、08 で箱の位置まで回り、09〜13 で爪の「開く・閉じる」を繰り返しました。横から撮った動画では、ブロックはマットに付いたまま引きずられ、箱の壁に当たって箱を押しています。最後に h で HOME へ戻すと、ブロックを持ったまま戻りました。

10:07 の回を斜め上から撮った動画のコマ。0:29 でつかみ、0:54 でブロックを箱の壁に当てて箱を押し、1:44 でブロックを持ったまま HOME に戻っている

0.5 秒ずつ動かした 10:20 の回は、深くつかみ、持ち上がらないまま31回問い合わせた

1回に動かす長さを 0.5 秒(5ステップ)にして、細かく問い合わせる回も試しました(--steps-per-chunk 5)。この回の画面の出力は残っていないので、記録(log.jsonl)から作った表を「実機の回すべてのチャンクの記録」の節に載せています。

この回は、閉じる前に肩を少しずつ下げ続け、お手本より約5°深いところ(肩 j1 = 69°、お手本は 64°)でつかみました。そのあと、肩を上げる指令が17回続きましたが、指令の合計 41.2° に対して、実際に上がったのは 11.1° です。上のカメラの画像を、お手本(位置C)の同じ場面と並べました。

上カメラの画像。左が 10:20 の回のチャンク08・11・13・18・22・27、右がお手本 192343 の同じ場面。左は肩 j1 が 68〜58° のまま、右は 64° から 49° へ上がっている

この回までの記録をまとめ、学習と実機で、モデルへの入力や答えの使い方が食い違っていないかの点検を codex に頼みました。依頼文です。

05_sec04.txt3,052字・51行・text
# 確認依頼:π0.5 が実機でブロックを持ち上げない原因(学習と実機の食い違いの点検)

対象(読むだけ。コードの変更はしないでください)
- `~/r2r2r-scripts/pi05/run_policy_client.py`(実機クライアント。とくに grab_rgb / 状態の作り方 / plan_chunk / apply_limits / グリッパーの扱い)
- `~/r2r2r-scripts/pi05/build_demo_episodes.py`(お手本 → raw_episodes。--trim-mid-idle を含む版で teach_v1_trim2 を作った)
- `~/r2r2r-scripts/pi05/convert_to_lerobot.py`(raw_episodes → LeRobot 形式)
- `~/r2r2r-scripts/pi05/openpi_addon/`(mecharm_policy.py と config の追加。入力・出力の変換、正規化、delta かどうか)
- `~/r2r2r-scripts/pi05/eval_policy_offline.py`(お手本の画像と角度でサーバーに問い合わせる答え合わせ)
別のタブで開く保存

codex の点検結果です。画像の作り方や正規化の値に、持ち上げ不足を説明する食い違いは見つからず、いちばん疑わしいのは「チャンクの最後の姿勢だけを送り、3°以内なら着いたとみなして次へ進む」実行のしかただ、という結論でした。

06_sec04.txt7,857字・153行・text
# π0.5 持ち上げ不足:学習・実機の食い違い点検

2026-10-22。コード・19本のraw_episodes・指定のオフラインCSV・実機3回のlog.jsonl・serve.log・FK比較表を読み取り専用で確認した。アーム、グリッパー、カメラ、推論サーバーへの操作・接続は行っていない。対象コード、HOME、SPEED、J4_SAFEは変更していない。以下の数値集計は保存済みデータだけを使った。

## 結論

**最有力は、チャンク末尾の姿勢だけを送る再計画方式と、3°の到達許容が、小さい持ち上げ指令を十分に実現できないまま次の予測へ進めていること。** それに、つかむ時点で学習軌道より深い姿勢へ外れる問題が重なっている。
別のタブで開く保存

肩を上げる指令43回は、すべて目標より 0.77〜2.80° 低いところで止まっていた

codex が数えた3回分を、この日の実機の回すべてに広げて数え直しました。夕方の回まで含めて、あとから同じスクリプトで出したものです。ブロックを持っている(爪が閉じている)ときに、肩 j1 を上げる向き(角度を小さくする向き)の指令を送り、次に測った角度が指令からどれだけずれていたかを数えます。

# 肩 j1 の「送った目標」と「次のチャンクの実測」の差を数える (LOG361)
#  + は腕が目標より低いまま止まった (j1 が大きい = 下向き)
#  中継点 (--via-stride) の回は、チャンクの最後に送った目標で比べる
import os, json
import numpy as np
ROOT = os.environ.get("R2R2R_DATA", "/Volumes/KIOXIA/r2r2r-data"); os.chdir(ROOT)
RUNS = ["20261021_183245", "20261022_095251", "20261022_100713", "20261022_102053",
        "20261022_114643", "20261022_131036", "20261022_181602", "20261022_182650"]
allup = []
for r in RUNS:
    L = [json.loads(l) for l in open(f"runs/pi05_policy/pi05_{r}/log.jsonl")]
    L = [x for x in L if "chunk" in x]
    up, dn = [], []
    for a, b in zip(L, L[1:]):
        if not a.get("executed"):
            continue
        s = np.array(a.get("measured_state", a["state"])[:6]); t = np.array(a["target"])
        n = np.array(b.get("measured_state", b["state"])[:6])
        cmd = t[1] - s[1]; res = n[1] - t[1]
        (up if cmd < 0 else dn).append((cmd, res, int(a["state"][6])))
    allup += [u for u in up if u[2] == 1]
    print(f"== {r}")
    print("  肩を上げる指令 (指令°, 指令からのずれ°, 爪):", " ".join(f"({c:+.1f},{e:+.2f},{g})" for c, e, g in up))
    print("  肩を下げる指令 (指令°, 指令からのずれ°, 爪):", " ".join(f"({c:+.1f},{e:+.2f},{g})" for c, e, g in dn))
    held = [(c, e) for c, e, g in up if g == 1]
    if held:
        print(f"  ブロックを持って上げた指令の合計 {sum(c for c,_ in held):+.1f}° / 実際に上がった量 {sum(c + e for c, e in held):+.1f}°")
e = np.array([u[1] for u in allup])
print(f"\n全体: ブロックを持って肩を上げる指令 {len(e)} 回  ずれ 最小 {e.min():+.2f}° 中央 {np.median(e):+.2f}° 最大 {e.max():+.2f}°  低いまま止まった回 {int((e > 0).sum())}")
== 20261021_183245
  肩を上げる指令 (指令°, 指令からのずれ°, 爪): (-5.8,+2.58,1) (-9.6,+2.52,1) (-9.8,+2.47,1) (-4.6,+2.64,1)
  肩を下げる指令 (指令°, 指令からのずれ°, 爪): (+20.0,+1.01,0) (+20.0,+1.00,0) (+20.0,+1.10,0) (+1.2,+1.05,0) (+10.1,+1.21,1) (+7.3,+0.53,1) (+4.4,-0.41,0) (+1.0,+0.21,1)
  ブロックを持って上げた指令の合計 -29.9° / 実際に上がった量 -19.7°
== 20261022_095251
  肩を上げる指令 (指令°, 指令からのずれ°, 爪): (-5.2,+1.92,1) (-5.7,+2.39,1) (-7.9,+2.65,1)
  肩を下げる指令 (指令°, 指令からのずれ°, 爪): (+20.0,+1.00,0) (+20.0,+0.92,0) (+16.5,+0.72,0) (+1.8,+0.40,0) (+1.4,+0.98,1)
  ブロックを持って上げた指令の合計 -18.8° / 実際に上がった量 -11.9°
== 20261022_100713
  肩を上げる指令 (指令°, 指令からのずれ°, 爪): (-5.8,+1.49,1) (-6.4,+2.50,1) (-6.7,+2.80,1) (-1.5,+1.48,1) (-0.1,+0.69,0)
  肩を下げる指令 (指令°, 指令からのずれ°, 爪): (+20.0,+1.10,0) (+20.0,+1.09,0) (+17.1,+0.44,0) (+2.5,-0.80,0) (+13.6,+0.63,1) (+0.5,+0.55,1) (+0.1,-0.07,1) (+0.4,+0.32,0)
  ブロックを持って上げた指令の合計 -20.3° / 実際に上がった量 -12.0°
== 20261022_102053
  肩を上げる指令 (指令°, 指令からのずれ°, 爪): (-2.3,+1.70,1) (-2.0,+1.50,1) (-1.9,+0.77,1) (-2.1,+1.43,1) (-2.6,+1.62,1) (-3.4,+1.95,1) (-3.6,+1.93,1) (-3.3,+2.20,1) (-2.9,+1.94,1) (-3.1,+2.12,1) (-1.9,+1.92,1) (-1.1,+1.09,1) (-2.4,+2.05,1) (-1.1,+1.05,1) (-2.9,+2.24,1) (-2.3,+2.25,1) (-2.5,+2.37,1)
  肩を下げる指令 (指令°, 指令からのずれ°, 爪): (+8.2,+0.99,0) (+11.5,+0.97,0) (+12.4,+1.08,0) (+11.3,+0.94,0) (+8.8,+0.83,0) (+2.1,+1.02,0) (+1.7,+0.45,0) (+2.5,-0.34,0) (+1.9,-0.52,0) (+2.0,-0.88,0) (+0.5,+0.38,0) (+3.2,+0.58,1) (+0.2,-0.19,1)
  ブロックを持って上げた指令の合計 -41.2° / 実際に上がった量 -11.1°
== 20261022_114643
  肩を上げる指令 (指令°, 指令からのずれ°, 爪): (-5.6,+1.94,1) (-6.3,+2.32,1) (-6.1,+2.65,1)
  肩を下げる指令 (指令°, 指令からのずれ°, 爪): (+20.0,+1.10,0) (+20.0,+1.09,0) (+17.4,+0.58,0) (+1.5,-0.48,0) (+2.7,+1.00,1)
  ブロックを持って上げた指令の合計 -18.0° / 実際に上がった量 -11.1°
== 20261022_131036
  肩を上げる指令 (指令°, 指令からのずれ°, 爪): (-4.2,+2.37,1) (-7.3,+2.61,1) (-8.1,+2.63,1)
  肩を下げる指令 (指令°, 指令からのずれ°, 爪): (+19.3,+0.97,0) (+24.2,+1.11,0) (+13.2,+0.87,0) (+3.7,+0.94,0) (+0.3,+1.16,1)
  ブロックを持って上げた指令の合計 -19.6° / 実際に上がった量 -11.9°
== 20261022_181602
  肩を上げる指令 (指令°, 指令からのずれ°, 爪): (-13.6,+2.54,1) (-14.3,+2.44,1) (-14.4,+2.42,1) (-8.5,+1.22,1) (-21.1,+2.33,0) (-22.6,+1.98,0) (-19.0,+1.73,0) (-3.1,+1.43,0) (-1.9,+1.60,0) (-1.6,+1.60,0)
  肩を下げる指令 (指令°, 指令からのずれ°, 爪): (+21.0,+0.96,0) (+21.0,+1.01,0) (+17.2,+0.99,0) (+5.3,+0.42,0) (+9.5,+0.93,0) (+0.7,+1.01,0) (+20.4,+1.06,1) (+1.2,+1.17,1)
  ブロックを持って上げた指令の合計 -50.8° / 実際に上がった量 -42.2°
== 20261022_182650
  肩を上げる指令 (指令°, 指令からのずれ°, 爪): (-3.9,+2.00,1) (-7.6,+2.37,1) (-9.5,+2.47,1) (-2.8,+1.22,1) (-0.9,+0.95,1) (-18.7,+2.44,0) (-26.5,+1.96,0) (-19.4,+0.90,0) (-1.7,+1.73,0)
  肩を下げる指令 (指令°, 指令からのずれ°, 爪): (+20.3,+0.93,0) (+23.0,+0.98,0) (+11.1,+1.16,0) (+2.9,+0.19,0) (+2.4,+0.63,0) (+13.6,+0.91,1)
  ブロックを持って上げた指令の合計 -24.6° / 実際に上がった量 -15.6°

全体: ブロックを持って肩を上げる指令 43 回  ずれ 最小 +0.77° 中央 +2.20° 最大 +2.80°  低いまま止まった回 43

ブロックを持って肩を上げる指令は、43回とも目標に届かずに止まっていました。 下げる向きの指令は、逆に約1°行き過ぎています。届かない分はどれも、到達の判定(3°以内なら着いた)より小さいので、クライアントは「着いた」として次の問い合わせへ進みます。AI はその低い角度を「いまの姿勢」として受け取り、そこからまた持ち上げの答えを返すので、足りない分は戻ってきません。

同じ目標を送り直して詰める settle を足したが、ずれは縮まらなかった

最初に試したのは、届かなかったときに、同じ目標をもう一度送って詰めるやり方です。新しい目標は作らず、安全の検査を通った目標をそのまま送り直すだけにしました(Claude Code の実装)。Claude Code の報告です。

07_settle.txt3,307字・88行・text
「送り直して詰める」を入れました。変更は pi05/run_policy_client.py の 1
  ファイルのみ(未コミット)。

  変えた行

  ┌─────────┬─────────────────────────────────────────────────────────────────┐
  │   行    │                              変更                               │
  ├─────────┼─────────────────────────────────────────────────────────────────┤
別のタブで開く保存

codex のレビューは「実機に進む前に直す点がある」でした。送り直しのあと動きが止まったかを確かめていないこと、呼び出し先が中で1回送り直すので、送る回数が上限を超えることなどです。

**現状では、settle有効の実機テストへ進む前に修正が必要です。** 同じtargetを再送する点は守られていますが、動作完了の確認、実際の再送回数、失敗の記録に抜けがあります。コード変更・アーム操作はしていません。

行番号は現在の [run_policy_client.py](/Users/shimazueiichi/r2r2r-scripts/pi05/run_policy_client.py) に対応します。

|項目|判定|確認結果・修正案|
|---|---|---|
|A1 同じ目標だけを再送|✅ OK|217行でtargetを取り出し、230行でそのまま送ります。加算・補正はありません。呼び出し元も安全検査済みtargetを渡しています。|
|A2 動作終了後に読む|❌ NG|**230〜233行**。再送でも許容3°の関数を呼ぶため、settle-tol=1°への収束も、停止も確認しません。後述のとおり1秒待機はありますが、動作完了保証ではありません。再送後は期限付きの読角で収束・安定を確認してから次の再送を判断する必要があります。|
|A3 同一目標が無視される可能性|⚠️ 要注意|ローカルのMacラッパーとdaemonには同一targetを省略する処理はなく、毎回`_mc.send_angles`へ渡します。ただし稼働Piのpymycobot・ファームウェアが同じ指令をどう扱うかは未確認です。**再送すれば詰まるとはコードだけでは証明できません。**|
|A4 詰めてから閉じる順序|✅ OK|458〜459行のsettle後に、460〜465行でグリッパー操作します。ただしsettle未収束・読角失敗でも、仕様どおりその後に閉じます。「順番は先」でも「詰まったこと」は保証しません。|
|A5 h・q・auto・二重送信|❌ NG|h/q/sはsettle前に分岐し、HOMEにsettleはかかりません。ただし**230行の呼び出し先自身が再送を最大1回行う**ため、settle-retries=2でも追加の実送信は最大4回になり得ます。「最大2回」とは不一致です。再送予算を実送信回数で管理する必要があります。|
|A6 読角失敗|✅/⚠️|222〜225行、233〜241行で読めなければ追加再送をやめます。HOME等を勝手に送りません。ただし最初の読角失敗はsettle記録が空になり、許容内で再送不要だった場合と区別できません。失敗理由を記録すべきです。|
|B1 既存経路の維持|✅ OK|retries=0は215〜216行で読角・送信なし。ドライラン、h/q/s、開閉指令そのものは変更されていません。有効時に開閉が遅れるのは追加処理の結果です。|
|B2 呼び出し先の変更|✅ OK|`j0_sweep_scan.py`に未コミット差分はありません。|
|C1 振動|⚠️ 要注意|同じ目標・有限回数なので、補正値を反転し続ける無限振動ロジックはありません。ただし動作途中の再指令による再始動や物理的な揺れは排除できません。A2の安定確認が必要です。|
|C2 関節制限|✅ OK|通常経路では405行の安全検査を通った同じtargetだけを再送します。今回の変更でJ4_SAFE・J1_SAFE_MAX外の**目標角**を生成する経路は見つかりません。実角度のオーバーシュート保証とは別です。|
|D1 send_coords|✅ OK|追加処理に使用はありません。|
|D2 固定値|✅ OK|HOMEは変更なし。呼び出し先のSPEED=20、SEND_WAIT_SEC=1.0も変更されていません。|

特に直すべき点は次の3つです。

1. **再送後の待ち方をsettle用にする。**  
   `_send_and_wait_arrival`からのreturn後、直ちに1回読むだけでは不十分です。ただし「送信直後に無待機で読む」という実装ではありません。ローカルdaemonはsend後に**1秒sleep**してから応答します(`mecharm_daemon.py:75–81`)。問題は、その1秒と3°判定で**動作終了・1°収束を保証できないこと**です。

2. **再送の失敗・内部再送を扱う。**  
   **230行で戻り値 `(success, retried)` を捨てています。** 呼び出し先が部分移動/ストール疑いでFalseを返しても、外側ループが再び送る場合があります。少なくとも失敗時はそのsettle内の追加再送をやめて警告し、戻り値・内部再送の有無を記録してください。「プログラム全体を止める条件を増やさない」という仕様とも両立できます。

3. **ログを実態に合わせる。**  
   現在の`settle_retries_used`は関数の呼び出し回数で、実送信回数ではありません。初回読角失敗、許容内、送信失敗、未収束を区別し、設定tol・最終状態も残す必要があります。

直した版の報告です。

08_settle.txt4,249字・102行・text
 3 点を反映しました。変更は pi05/run_policy_client.py の settle_target
  周りのみ(未コミット)。

  変えた行
     
  ┌───────┬──────────────────────────────────────────────────────────────────┐
  │  行   │                               変更                               │
  ├───────┼──────────────────────────────────────────────────────────────────┤
別のタブで開く保存

2回目のレビューです。

**R1は一部未解消、R2・R3の既存経路は維持されています。** 修正箇所だけ読み取り確認しました。コード変更・アーム操作はしていません。

### R1 ❌ 未解消の点があります

**A2:⚠️ 待機は追加されていますが、停止未確認でも再送します。**  
[203行以降](/Users/shimazueiichi/r2r2r-scripts/pi05/run_policy_client.py:203)で初回・再送後とも読角を待つようになりました。ただし257行の`_still`は無視され、再送後の`still=False`も記録するだけです。タイムアウト時に動作途中の値で判断する経路は残ります。

タイムアウト時も続行する仕様なら、その点は明示したうえで、**そのsettle内の追加再送はやめて警告で戻る**のが適切です。なお「2回の差が0.2°未満」は停止の近似判定で、低速移動や同じ読角値の再取得を排除するものではありません。

**A5:❌ 実送信回数の上限を超える経路があります。**  
[271〜277行](/Users/shimazueiichi/r2r2r-scripts/pi05/run_policy_client.py:271)は呼び出す前に`sends >= retries`を確認しますが、内部再送分は**送った後**に加算します。

例:

- 上限2回、最初の呼び出しで1回送信
- 残り1回で再び呼び出す
- 今回は内部再送され2回送信
- **合計3回となり上限超過**

残り予算が1回なら、内部再送し得る関数をそのまま呼べません。残り予算を守れる送信経路が必要です。

また、`retried=True`は厳密な実送信回数ではありません。呼び出し先の[302〜305行](/Users/shimazueiichi/r2r2r-scripts/dynamic_tracking_limits/place/place_modules/j0_sweep_scan.py:302)では、再送前の読角失敗でも`False, True`を返します。この場合、実際には2回目を送っていません。`success=False`で外側の再送をやめる修正自体はできています。

**A6・ログ:❌ 途中の読角失敗を見逃します。**  
[219〜221行](/Users/shimazueiichi/r2r2r-scripts/pi05/run_policy_client.py:219)で読角に失敗すると、`None`ではなく直前の角度を返します。呼び出し側はこれを有効値として扱うため、**読めなくなった後に古い値で再送したり、within_tol/convergedと記録したりできます。**

読角失敗と時間切れを別の状態で返し、読角失敗なら追加再送をやめて`read_failed`にしてください。直前の有効値は「最終確認値」として別途保存できます。

なお、disabledの[253〜254行](/Users/shimazueiichi/r2r2r-scripts/pi05/run_policy_client.py:253)だけは`final_err/joint/final_actual/tries`がありません。全reasonで同じログ形式にするなら、`None`と空配列を追加してください。

### R2 ✅ 既存の分岐・停止条件は維持

[503〜538行](/Users/shimazueiichi/r2r2r-scripts/pi05/run_policy_client.py:503)を確認しました。

- h・q・sはsettle前に分岐します。
- ドライランではsettleを呼びません。
- retries=0では追加の読角・送信がありません。
- グリッパー操作はsettle後です。
- settle失敗によるプログラム全体の停止条件は増えていません。
- 初回送信失敗時の`--auto`停止は従来どおりです。

ただし、R1の読角失敗を直前値に置き換える処理は、新しく入った問題です。

### R3 ✅ 安全範囲外の目標を新たに作る経路はなし

[255行・275行](/Users/shimazueiichi/r2r2r-scripts/pi05/run_policy_client.py:255)では、[477行](/Users/shimazueiichi/r2r2r-scripts/pi05/run_policy_client.py:477)の安全検査を通ったtargetを、そのまま再送しています。加算・差し替えはなく、J4_SAFE・J1_SAFE_MAX外の**指令値を生成する経路**は見つかりません。

もう一度直した版の報告です。このあと3回目のレビューを通し、11時24分にコミットしました(3回目のレビューの文は残していません)。

09_settle.txt4,291字・94行・text
3 点を反映しました。変更は pi05/run_policy_client.py のみ(未コミット)。

  変えた行

  ┌─────────┬─────────────────────────────────────────────────────────────────┐
  │   行    │                              変更                               │
  ├─────────┼─────────────────────────────────────────────────────────────────┤
  │ 23-26   │ docstring: 「動きが止まらない / 読めない /                      │
別のタブで開く保存

コミットの説明文です。

commit c7e726aac6473cedfc5248e8dcfc583850cb3465
Date:   Thu Oct 22 11:24:03 2026 +0900

送った目標とのずれを同じ目標の送り直しで詰める (LOG359)

LOG359: ブロックを持って肩を持ち上げる指令 (j1 を小さくする向き) が毎回 約 2°
届かないまま, 次の予測に進んでいた. 到達判定は _send_and_wait_arrival(..., 3.0) の
3° なので, このずれは「着いた」と見なされる. モデルは実際の姿勢から次を予測するため,
持ち上げが毎回 2° ずつ失われ, ブロックがマットから離れない.
実機 4 回 (pi05_20261021_183245 / 20261022_095251 / 100713 / 102053) の log.jsonl で,
肩を上げる指令 28 回すべてが 0.77〜2.80° (中央 2.00°) 届かず, 28 回とも 3° 未満だった
(102053 は上げる指令の合計 41° に対して実際は 11°). 逆向き (下げる) は 0.5〜1° 行き過ぎる.

- settle_target(): チャンクの目標を送ったあと, 送った目標との |目標 - 実際| の最大が
  --settle-tol (既定 1.0°) を超えていたら, **同じ目標を** 最大 --settle-retries
  (既定 2) 回まで送り直して詰める. 新しい目標は作らない (apply_limits と
  check_send_safe を通った target をそのまま送り直すだけ). --settle-retries 0 で
  従来の動き (読みも送りもしない).
- 送り直しは _send_and_wait_arrival ではなく, その中で使っている 1 回だけ送る層
  daemon_send_angles(..., speed=SPEED, wait=SEND_WAIT_SEC) を直接呼ぶ (codex A5).
  中で再送しないので送信回数が上限を超えない. SPEED / SEND_WAIT_SEC は j0_sweep_scan
  から import して本線と同じ値 (20 / 1.0) を使う.
- read_until_still(): 0.2 秒おきに読み, 続けて読んだ 2 回の差が 6 関節すべて 0.2° 未満で
  「止まった」とみなしてから測る (codex A2). 2.0 秒で打ち切り. 読み取りに失敗したら
  古い値を返さず, 最後に読めた値は別に返す (codex A6).
- 動きが止まらない (not_still) / 読めない (read_failed) / 送信が例外 (send_failed) の
  ときは, その settle の送り直しをやめて [WARN] を出す. **止める条件は増やしていない**
  (どの終わり方でもチャンクループは続き, 終了コードも変えない).
- log.jsonl のチャンクの行に settle (reason / tol / sends / final_err / joint /
  final_actual / last_valid_actual / tries) と settle_retries_used を残す.
  reason は within_tol / converged / not_converged / not_still / send_failed /
  read_failed / disabled のいずれかで, どれでも同じ形.

変更なし: _send_and_wait_arrival 本体 (j0_sweep_scan), チャンク本体の送信,
HOME 復帰 (h) / q / グリッパーの開閉 / ドライラン, J4_SAFE, J1_SAFE_MAX, apply_limits.

偽の daemon で確認 (アームには送っていない. 10 ケースすべて期待どおり):
  +2.0° → 送り直して +0.4°        converged / settle の送信 1 回
  +2.0° のまま                    not_converged / 送信 2 回で打ち切り
  --settle-retries 0              disabled / settle の読み 0 回・送信 0 回
  2.0 秒で止まらない              not_still / 1 回目のあと送り直しをやめる
  送信が例外                      send_failed / try に sent=false と error
  読み取りが失敗                  read_failed / final_* は null, last_valid_actual に退避
  ドライラン                      送信 0 回・settle キーなし

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

11:46 に、送り直しを入れて動かしました。

10_settle.txt5,896字・85行・text
(base) shimazueiichi@shimadzueiichinoMacBook-Pro r2r2r-scripts % python3 pi05/run_policy_client.py --raw-dir $RAW --steps-per-chunk 10 --execute
[SAFE  ] 安全範囲 (学習 19 本 ±10°): j0[-72,+55]  j1[-9,+88]  j2[-46,+41]  j3[-18,+27]  j4[-90,+10]  j5[-51,-14]
[SAFE  ] 肩 j1 の上限は J1_SAFE_MAX=88° で切った (これを超える指令はアームが黙って捨てる / LOG352)  元データ: /Volumes/KIOXIA/r2r2r-data/datasets/mecharm_redblock_box_teach_v1_trim2/raw_episodes
[MODE  ] EXECUTE  steps/chunk=10  max_move=20.0°  log=/Volumes/KIOXIA/r2r2r-data/runs/pi05_policy/pi05_20261022_114643
[CAMERA] 写り方 dy=-6.5 dx=+3.0 response=0.553 → OK (|dy|<=20 / |dx|<=20 / response>=0.20)
[CAMERA] 基準: /Volumes/KIOXIA/r2r2r-data/datasets/mecharm_redblock_box_teach_v1_trim2/raw_episodes/run_0001_20261019_183454/frames/000000.jpg
[POLICY] 接続 ws://localhost:8000  metadata={}
[GRIP  ] 開始時のグリッパー開放は, 最初の目標が検査を通ってから行います (爪が閉じていたら手で開けておく)
別のタブで開く保存

送り直しても、ずれはほとんど縮みませんでした。チャンク05 と 06 では、2回送り直しても j1 のずれが 2.32°・2.65° のまま、1つも動いていません。同じ目標を送っても、アームは「もう着いている」として動かないようです。チャンク07 では、daemon への送信が4秒で時間切れになっています。

上カメラの写り方は、macOS のセンターフレームで数秒後に変わっていた

朝、写り方の確認で続けて止まったとき、カメラを開いた直後と3秒後のコマを保存して比べました。上カメラは iPhone を連係カメラ(Continuity Camera)で Mac につないで使っています。

# 動かすコマンド (run_policy_client.py) と同じ方法で上カメラを開き, 開いた直後と3秒後のコマを保存する (LOG359)
# 使い方: cd ~/r2r2r-scripts && python3 <このファイル>
import sys, time, os
R = os.path.expanduser("~/r2r2r-scripts")
for sub in ["", "pi05", "dynamic_tracking_limits/place/place_modules", "dynamic_tracking_limits/drag_teaching"]:
    sys.path.insert(0, os.path.join(R, sub))
import cv2, camera_resolver
cams = camera_resolver.resolve_cameras(require_top=True, require_side=False, verbose=True)
print("cams:", cams)
cap = cv2.VideoCapture(cams["top"])
out = os.path.dirname(os.path.abspath(__file__))
t0 = time.time(); n = 0; saved0 = False
while time.time() - t0 < 3.0:
    ok, f = cap.read()
    if ok and f is not None:
        n += 1
        if not saved0:
            cv2.imwrite(os.path.join(out, "snap_first.jpg"), f); saved0 = True
            print("first frame", f.shape, f"{time.time()-t0:.2f}s")
cv2.imwrite(os.path.join(out, "snap_3s.jpg"), f)
print("frames", n, "saved to", out)

macOS のセンターフレーム(人の顔に合わせて画角を自動で寄せる機能)が入っていると、カメラを開いて数秒のうちに画角が寄り、マットが大きく写ります。お手本を録ったときと写り方が変わるので、センターフレームは切って使うことにしました。

上カメラのコマを3枚並べたもの。左と中はお手本と同じ写り方、右はセンターフレームで画角が寄り、マットとアームが大きく写っている

11:27 には、写り方の確認が NO MATCH になりました。このときはアームが HOME の姿勢に戻っていませんでした。この日の午前には、手首 j4 が +51.94° のまま動かなくなったこともあり、そのときはアームの電源を入れ直すと動くようになっています。

camera_align.py の画面。左上に NO MATCH と dy −24.6・dx −3.8 が出ている。手首が曲がったままで、重ねたお手本の爪の位置と合っていない

つかんだあと、お手本は 1.5 秒その場で持ち上げてから回っていた

送り直しでは詰まらなかったので、つかんだ直後の動きをお手本と比べました。お手本(位置C)で爪を閉じたステップから、1ステップ(0.1 秒)ごとに各関節がどれだけ動いたかと、実機で閉じた直後のチャンクの予測を並べるスクリプトです。

# つかんだ直後の動きを、お手本 (位置C) と比べる (LOG361)
#  お手本: 閉じたステップから 15 ステップ (1.5 秒) の 6 関節の変化
#  実機: 閉じた直後のチャンクで、モデルが予測した 10 ステップ (1 秒) の変化
import os, json
import numpy as np
ROOT = os.environ.get("R2R2R_DATA", "/Volumes/KIOXIA/r2r2r-data"); os.chdir(ROOT)
z = np.load("datasets/mecharm_redblock_box_teach_v1_trim2/raw_episodes/run_0001_20261019_192343/episode.npz")["state"]
c = np.where(np.diff(z[:, 6]) > 0.5)[0][0] + 1
print("お手本 192343  閉じたステップ t =", c, " 姿勢", np.round(z[c, :6], 1).tolist())
for k in range(0, 16):
    print(f"  +{k:2d} ステップ  Δ " + " ".join(f"{x:+5.1f}" for x in z[c + k, :6] - z[c, :6]))
for r in ["20261022_114643", "20261022_181602"]:
    L = [json.loads(l) for l in open(f"runs/pi05_policy/pi05_{r}/log.jsonl")]
    L = [x for x in L if "chunk" in x]
    i = next(k for k, x in enumerate(L) if x["state"][6] == 1)
    d = L[i]; s = np.array(d["state"]); a = np.array(d["actions"])
    print(f"\n実機 {r}  閉じた直後のチャンク {d['chunk']:02d}  state", np.round(s[:6], 1).tolist())
    for k in range(10):
        print(f"  step {k+1:2d}  Δ " + " ".join(f"{x:+5.1f}" for x in a[k, :6] - s[:6]))
お手本 192343  閉じたステップ t = 56  姿勢 [-27.700000762939453, 63.70000076293945, 14.100000381469727, -7.599999904632568, -61.20000076293945, -27.899999618530273]
  + 0 ステップ  Δ  +0.0  +0.0  +0.0  +0.0  +0.0  +0.0
  + 1 ステップ  Δ  -0.0  +0.0  -0.0  -0.0  -0.0  +0.0
  + 2 ステップ  Δ  -0.1  +0.0  -0.2  -0.1  -0.5  +0.0
  + 3 ステップ  Δ  -0.1  +0.0  -0.2  -0.1  -0.6  +0.0
  + 4 ステップ  Δ  -0.1  -0.0  -0.2  -0.1  -0.9  +0.0
  + 5 ステップ  Δ  -0.1  -0.4  +0.4  +0.2  -1.6  +0.1
  + 6 ステップ  Δ  -0.1  -0.8  +0.9  +0.5  -2.2  +0.1
  + 7 ステップ  Δ  -0.1  -1.2  +1.5  +0.9  -2.9  +0.2
  + 8 ステップ  Δ  -0.1  -1.6  +2.1  +1.2  -3.5  +0.2
  + 9 ステップ  Δ  -0.1  -2.0  +2.6  +1.5  -4.1  +0.3
  +10 ステップ  Δ  -0.1  -2.4  +3.2  +1.9  -4.8  +0.3
  +11 ステップ  Δ  -0.1  -2.8  +3.8  +2.2  -5.4  +0.4
  +12 ステップ  Δ  -0.1  -3.2  +4.3  +2.5  -6.1  +0.4
  +13 ステップ  Δ  -0.1  -3.6  +4.9  +2.9  -6.7  +0.5
  +14 ステップ  Δ  -0.1  -4.0  +5.5  +3.2  -7.3  +0.5
  +15 ステップ  Δ  +0.9  -4.5  +5.5  +3.5  -7.4  +0.4

実機 20261022_114643  閉じた直後のチャンク 04  state [-33.5, 62.5, 17.1, -3.2, -60.9, -28.3]
  step  1  Δ  -0.1  -0.0  +0.1  +0.1  -0.4  -0.0
  step  2  Δ  -0.3  -0.1  +0.3  +0.2  -0.8  -0.0
  step  3  Δ  -0.3  -0.1  +0.6  +0.4  -1.3  -0.1
  step  4  Δ  +0.1  -0.5  +0.8  +0.6  -1.6  -0.2
  step  5  Δ  +1.5  -1.0  +0.8  +1.0  -1.8  -0.4
  step  6  Δ  +4.1  -2.0  +0.5  +1.5  -1.5  -0.7
  step  7  Δ  +7.1  -2.8  +0.2  +2.0  -1.2  -1.0
  step  8  Δ  +9.9  -3.8  -0.2  +2.5  -0.8  -1.4
  step  9  Δ +12.9  -4.7  -0.8  +3.0  -0.3  -1.7
  step 10  Δ +15.5  -5.6  -1.2  +3.5  +0.2  -1.9

実機 20261022_181602  閉じた直後のチャンク 06  state [-39.6, 80.6, 7.5, 3.4, -89.5, -29.8]
  step  1  Δ  -0.1  -0.7  +0.7  +0.0  -0.1  -0.1
  step  2  Δ  -0.1  -1.9  +1.5  +0.0  -0.1  -0.1
  step  3  Δ  -0.0  -3.6  +2.4  +0.0  -0.1  -0.1
  step  4  Δ  -0.1  -5.1  +3.4  +0.0  +0.0  -0.1
  step  5  Δ  -0.1  -6.4  +4.4  +0.0  +0.1  -0.1
  step  6  Δ  -0.2  -7.5  +5.2  +0.0  +0.2  -0.1
  step  7  Δ  -0.2  -8.7  +6.1  +0.0  +0.2  -0.1
  step  8  Δ  -0.3  -9.9  +7.1  +0.1  +0.5  -0.1
  step  9  Δ  -0.0 -11.4  +8.1  +0.1  +0.7  -0.1
  step 10  Δ  +0.7 -12.6  +8.9  +0.2  +1.1  -0.1

お手本は、閉じてから 1.5 秒(15ステップ)の間、土台 j0 をほぼ動かさず(−0.1°)、肩 j1 を −4.5° 上げています。11:46 の回の予測は、0.5 秒後には土台を回し始め、1秒で +15.5° 回っています。最後の姿勢だけを送るやり方だと、「持ち上げながら回る」1本の動きになり、持ち上げきる前に回り始めます。

1秒ごとの姿勢(j0・j1・j4)を、お手本4本と実機の回で並べたものです。

# お手本 (位置C の 4 本) と実機の、つかんでから離すまでを 1 秒ごとに並べる (j0 / j1 / j4) (LOG361)
import os, json
import numpy as np
ROOT = os.environ.get("R2R2R_DATA", "/Volumes/KIOXIA/r2r2r-data"); os.chdir(ROOT)
RAW = "datasets/mecharm_redblock_box_teach_v1_trim2/raw_episodes"
for e in ["192343", "192438", "192539", "192633"]:
    C = np.load(f"{RAW}/run_0001_20261019_{e}/episode.npz")["state"]
    c = np.where(np.diff(C[:, 6]) > 0.5)[0][0] + 1; o = np.where(np.diff(C[:, 6]) < -0.5)[0]; o = o[o > c][0] + 1
    print(f"demo {e} " + " ".join(f"[{C[t,0]:+5.0f} {C[t,1]:4.0f} {C[t,4]:+4.0f}]" for t in range(c, o + 1, 10)))
for r in ["20261022_095251", "20261022_100713", "20261022_102053", "20261022_181602"]:
    L = [json.loads(l) for l in open(f"runs/pi05_policy/pi05_{r}/log.jsonl")][1:]
    S = [x.get("measured_state", x["state"]) for x in L if "chunk" in x and x["state"][6] == 1]
    print(f"run  {r} " + " ".join(f"[{s[0]:+5.0f} {s[1]:4.0f} {s[4]:+4.0f}]" for s in S))
demo 192343 [  -28   64  -61] [  -28   61  -66] [  -13   56  -66] [  +16   49  -60] [  +41   47  -54] [  +41   63  -68]
demo 192438 [  -28   64  -61] [  -28   61  -66] [  -22   58  -68] [   +6   51  -62] [  +36   45  -54] [  +41   59  -65]
demo 192539 [  -27   64  -61] [  -28   62  -65] [  -27   59  -70] [   +0   53  -64] [  +30   45  -55] [  +41   55  -61]
demo 192633 [  -27   64  -61] [  -28   61  -66] [  -28   60  -70] [   -2   53  -64] [  +28   46  -56] [  +41   54  -61]
run  20261022_095251 [  -33   63  -62] [  -20   59  -58] [   +0   56  -56] [  +20   51  -49] [  +39   53  -46]
run  20261022_100713 [  -37   63  -61] [  -24   58  -59] [   -4   55  -53] [  +16   51  -47] [  +36   51  -44] [  +40   65  -52] [  +40   67  -54] [  +41   67  -53]
run  20261022_102053 [  -31   69  -68] [  -31   69  -69] [  -30   68  -70] [  -30   67  -68] [  -29   66  -69] [  -24   65  -68] [  -15   64  -66] [   -5   62  -64] [   +5   61  -62] [   +8   60  -62] [  +11   59  -62] [  +12   59  -64] [  +12   59  -64] [  +15   59  -66] [  +16   59  -66] [  +21   58  -66] [  +23   58  -66] [  +29   58  -66] [  +30   62  -69]
run  20261022_181602 [  -39   82  -90] [  -38   71  -90] [  -16   59  -78] [  +13   47  -68] [  +40   39  -60] [  +43   61  -72]

モデルの予測そのものがお手本からずれていないかも、お手本の画像と角度をサーバーに渡して確かめました(前の回の答え合わせの記録 lab/Log11/offline_eval/20261021_180539/rows.csv の位置C の部分を並べ直したもの)。

# 10/21 のお手本との答え合わせ (eval_policy_offline.py の rows.csv) から、位置C のつかむ前後〜運ぶ間を抜き出す (LOG361)
import os, csv
ROOT = os.environ.get("R2R2R_DATA", "/Volumes/KIOXIA/r2r2r-data"); os.chdir(ROOT)
R = [r for r in csv.DictReader(open("lab/Log11/offline_eval/20261021_180539/rows.csv")) if "192343" in r["episode"]]
print(" t     j0     j1     j4  g  ずれ平均  ずれ最大(関節)  お手本1秒  予測1秒")
for r in R:
    t = int(r["t"])
    if 36 <= t <= 110:
        print(f"{t:3d} {float(r['j0']):+6.1f} {float(r['j1']):+6.1f} {float(r['j4']):+6.1f}  {r['grip']}  {r['err_mean']:>6}°  {r['err_max']:>6}°(j{r['err_max_joint']})  {r['demo_move_1s']:>6}°  {r['pred_move_1s']:>6}°")
 t     j0     j1     j4  g  ずれ平均  ずれ最大(関節)  お手本1秒  予測1秒
 36  -26.4  +54.5  -67.3  0    0.24°    1.03°(j4)    5.59°    5.21°
 39  -27.6  +58.1  -69.3  0    0.29°    1.09°(j1)    4.72°    4.49°
 42  -27.6  +58.1  -70.3  0    0.24°    0.78°(j3)    8.32°    7.64°
 45  -27.6  +59.6  -68.1  0    0.29°     1.1°(j2)     6.9°    7.43°
 48  -27.6  +61.2  -65.5  0    0.16°    0.47°(j0)    3.78°    3.73°
 51  -27.7  +62.8  -62.9  0    0.21°    0.86°(j4)    0.71°    0.82°
 54  -27.7  +63.7  -61.3  0    0.24°    0.93°(j2)    3.35°    2.42°
 57  -27.7  +63.7  -61.2  1    0.16°    0.52°(j4)    5.38°    5.67°
 60  -27.8  +63.7  -62.1  1    0.55°    1.13°(j2)    6.41°    5.32°
 63  -27.8  +62.5  -64.0  1    0.18°    0.89°(j1)    6.45°    5.56°
 66  -27.8  +61.3  -66.0  1    0.31°    1.18°(j0)   14.63°   13.99°
 69  -27.8  +60.1  -67.9  1    2.28°    9.91°(j0)   22.82°    13.1°
 72  -24.1  +58.6  -68.1  1    0.34°    1.47°(j0)   27.47°   28.42°
 75  -15.9  +56.6  -66.6  1    0.12°    0.57°(j1)   28.55°   28.18°
 78   -7.7  +54.5  -65.1  1    0.29°    1.04°(j0)   29.49°   28.45°
 81   +0.5  +52.5  -63.6  1    0.27°    1.16°(j0)   30.08°   28.92°
 84   +9.6  +50.3  -61.2  1    0.17°    0.65°(j0)   29.89°   29.25°
 87  +18.8  +48.1  -58.6  1    0.21°    0.79°(j0)   22.29°   23.09°
 90  +27.7  +45.8  -56.1  1     0.4°    1.47°(j4)   13.43°   12.11°
 93  +36.5  +44.9  -53.6  1    0.81°    2.04°(j0)   14.26°   15.52°
 96  +41.0  +47.2  -54.2  1    0.12°    0.92°(j1)   15.73°   14.93°
 99  +41.1  +52.3  -58.6  1    0.13°    0.69°(j4)    10.6°   10.15°
102  +41.1  +57.5  -63.1  1    0.11°    0.88°(j4)    5.61°    5.81°
105  +41.1  +62.6  -67.6  1    0.27°    1.52°(j4)    5.94°    7.15°
108  +41.1  +62.9  -68.5  1    0.52°    2.69°(j4)   11.52°    9.58°

お手本の画像と角度を渡すと、持ち上げの場面のずれは平均 0.1〜0.5° で、予測はお手本に合っています。モデルはお手本どおりに答えられるのに、実機では答えの届け方で高さを失っている、ということになります。

爪先の高さでも比べました。関節の角度から爪先の位置を求める表(FK テーブル fk_tables/fk_scan_motion_based_v2.csv、読むだけ)で、近い40点から局所的に直線で当てはめて高さを出しています。

# お手本(位置C)と実機の各回で、爪先の高さ (FK 表 fk_scan_motion_based_v2.csv の ee_y) を比べる (LOG359)
# 高さは j0 によらないので、(j1,j2,j3,j4) の近い 40 点で局所線形に当てはめて求める
import os, json, glob
import numpy as np, pandas as pd
ROOT = os.environ.get("R2R2R_DATA", "/Volumes/KIOXIA/r2r2r-data"); os.chdir(ROOT)
d = pd.read_csv("fk_tables/fk_scan_motion_based_v2.csv")
X = d[["j1", "j2", "j3", "j4"]].to_numpy(float); Y = d["ee_y"].to_numpy(float)
def h(q):
    q = np.asarray(q, float)[[1, 2, 3, 4]]
    dd = ((X - q) ** 2).sum(1); idx = np.argpartition(dd, 40)[:40]
    A = np.c_[np.ones(len(idx)), X[idx] - q]
    coef, *_ = np.linalg.lstsq(A, Y[idx], rcond=None)
    return coef[0] * 1000  # mm
def phases(states):
    g = states[:, 6]
    c = np.where(np.diff(g) > 0.5)[0][0] + 1
    o = np.where(np.diff(g) < -0.5)[0]; o = o[o > c][0] + 1 if (o > c).any() else len(states) - 1
    return c, o
print("== お手本 位置C (10 ステップ = 1 秒ごと, 閉じた瞬間から開くまで) 爪先の高さ mm")
for e in ["192343", "192438", "192539", "192633"]:
    z = np.load(f"datasets/mecharm_redblock_box_teach_v1_trim2/raw_episodes/run_0001_20261019_{e}/episode.npz")["state"]
    c, o = phases(z)
    hs = [h(z[t]) for t in range(c, o + 1, 10)]
    print(f"demo {e}  閉じた時 {h(z[c]):6.1f}  最高 {max(h(z[t]) for t in range(c, o+1)):6.1f}  1秒ごと " + " ".join(f"{v:5.0f}" for v in hs))
print("== 実機 (チャンクごと, 閉じたあと) 爪先の高さ mm")
for r in ["095251", "100713", "102053", "114643", "131036", "181602", "182650"]:
    L = [json.loads(l) for l in open(f"runs/pi05_policy/pi05_20261022_{r}/log.jsonl")]
    S = [x.get("measured_state", x["state"]) for x in L if "chunk" in x and x["state"][6] == 1]  # --state-from-target の回は実測 (measured_state) を使う
    first_closed = S[0]
    print(f"run  {r}  閉じた直後 {h(first_closed):6.1f}  最高 {max(h(s) for s in S):6.1f}  チャンクごと " + " ".join(f"{h(s):5.0f}" for s in S))
== お手本 位置C (10 ステップ = 1 秒ごと, 閉じた瞬間から開くまで) 爪先の高さ mm
demo 192343  閉じた時   41.4  最高  135.2  1秒ごと    41    40    51   111   103    86
demo 192438  閉じた時   41.7  最高  124.9  1秒ごと    42    39    56    96   122    96
demo 192539  閉じた時   41.6  最高  134.5  1秒ごと    42    44    23    89   120    82
demo 192633  閉じた時   41.3  最高  129.8  1秒ごと    41    38    22    86   120   110
== 実機 (チャンクごと, 閉じたあと) 爪先の高さ mm
run  095251  閉じた直後   28.6  最高   71.6  チャンクごと    29    33    48    72    71
run  100713  閉じた直後   25.7  最高   50.8  チャンクごと    26    34    41    51    21    35    34    26
run  102053  閉じた直後    9.8  最高   36.5  チャンクごと    10     4    15     4     7     6    32    28     9    32    12    37    34    34    34    35    35    35    26
run  114643  閉じた直後   27.0  最高   74.3  チャンクごと    27    39    49    74    63
run  131036  閉じた直後   33.9  最高   67.4  チャンクごと    34    35    50    67    67
run  181602  閉じた直後    2.1  最高   86.7  チャンクごと     2    47     7    80    87    82
run  182650  閉じた直後   11.8  最高   93.8  チャンクごと    12    35    56    94    75    44

お手本は約 41mm の高さでつかみ、125〜135mm まで持ち上げています。朝の回は、どれも 37〜74mm までしか上がっていません。

1秒分の途中の姿勢も送る —via-stride を足した

「持ち上げてから回る」順番を保つため、1秒分の予測のうち、最後の姿勢だけでなく途中の姿勢も3ステップおきに順に送るようにしました(--via-stride 3 で 2・5・8・9 ステップ目を送る)。Claude Code の報告です。

11_1_—via-stride.txt4,019字・90行・text
別のタブで開く保存

codex のレビューです。小さい中継点がすべて「0.5° 以下なので送らない」に当たって消えること、最後の点を送らなかったときに到達の確認まで飛ばされることなど、直す点が出ました。

**`--via-stride > 0`の実機テスト前に修正すべき点があります。** 特に、小さい中継点の累積移動が消えることと、最後の点がスキップされるとsettleも実行されないことです。差分の読み取りのみ行いました。

行番号は現在の [run_policy_client.py](/Users/shimazueiichi/r2r2r-scripts/pi05/run_policy_client.py) です。

|項目|判定|確認結果・修正案|
|---|---|---|
|A1 選択順・開閉前の姿勢|✅/⚠️|[259〜265行](/Users/shimazueiichi/r2r2r-scripts/pi05/run_policy_client.py:259)は指定どおり昇順で、usedを超えません。開閉時の最終姿勢も`plan_chunk`が返す直前姿勢を使います。ただし開閉時のusedは切替ステップf、実際の最終rawはactions[f−1]なので、ログのstepとrawの元インデックスは一致しません。|
|A2 apply_limits・移動量|❌ NG|[262〜268行](/Users/shimazueiichi/r2r2r-scripts/pi05/run_policy_client.py:262)は指定どおり直前の**計画target**基準です。ただし送信時に実測との差を検査しないため、「各送信が実測から20°以内」は保証されません。直前target=20°、実測17°、次target=40°なら実測から23°です。送信直前に実測との差を確認し、超過時の扱いを定義してください。|
|A3 事前検査後のずれ|✅/⚠️|[552〜555行](/Users/shimazueiichi/r2r2r-scripts/pi05/run_policy_client.py:552)で全targetを送信前に検査します。途中の実測ずれで固定targetが安全角度範囲外へ変わることはありません。ただし実測からの移動量と、意図した持ち上げ経路は保証されません。|
|A4 途中未到達|✅/⚠️|[618〜630行](/Users/shimazueiichi/r2r2r-scripts/pi05/run_policy_client.py:618)。autoは次の点・グリッパーへ進まず停止します。手動は警告して次へ進むため従来方針どおりですが、未到達の持ち上げ点を飛ばして旋回点へ進めます。持ち上げ優先を保証するものではありません。|
|A5 h/q/s・dry-run・stride=0|✅ OK|確認入力はチャンクに1回。h/q/sは中継点送信前に分岐し、dry-runは送信しません。stride=0の1点計画は従来のapply_limits・送信ガードと同じです。|
|A6 viasの記録|❌ NG|[609〜612行](/Users/shimazueiichi/r2r2r-scripts/pi05/run_policy_client.py:609)。呼出先が**送信前の読角失敗**で戻った場合も`sent=True`になります。`sent`は送信関数を呼んだ意味と、実送信を分けてください。`actual_after`読取失敗を`None`にする点、スキップ点を`sent=False`にする点は正しいです。|
|C1 関節角度制限|✅ OK|検査済みtargetだけを送るため、今回の変更でJ4_SAFE・J1_SAFE_MAX外の指令値を作る経路は見つかりません。A2の移動量超過とは別です。|
|D1 send_coords|✅ OK|追加処理で使っていません。|
|D2 固定値|✅ OK|HOME、SPEED=20、SEND_WAIT_SEC=1.0、max_move既定20°は変更されていません。ただし20°制限の基準は中継点の計画targetへ変わっています。|

追加で、送信ガードに2つの不具合があります。

**❌ 小さい変位を積み重ねる軌道が、全点スキップされます。**  
[604〜608行](/Users/shimazueiichi/r2r2r-scripts/pi05/run_policy_client.py:604)は、送らなかった点も含む「直前の計画targetとの差」で判断します。

例えば現在0°、中継点0.4°→0.8°→1.2°なら、各deltaは0.4°なので**全部送られません**。従来なら最終1.2°を送ります。ガードは現在実測、または少なくとも最後に実行した点を基準にしてください。

**❌ 最終点のスキップでsettleまで消えます。**  
同じ`continue`が[625〜628行](/Users/shimazueiichi/r2r2r-scripts/pi05/run_policy_client.py:625)のsettleを飛ばします。特にstride=1で開閉がf番目に予測された場合、最後の中継点actions[f−1]と、最終raw_targetが重複し、最終delta=0になります。その結果、**最後に実送信した姿勢をsettleせず、そのまま爪を閉じる**経路になります。

重複する送信を省く処理と、チャンク最終姿勢の到達確認・settleは分離してください。

直した版の報告です。

12_1_—via-stride.txt3,735字・107行・text
別のタブで開く保存

13:10 に、中継点を送るやり方で動かしました(送り直しは切っています)。

13_1_—via-stride.txt13,091字・161行・text
別のタブで開く保存

チャンク04 で閉じたあと、05 の途中の姿勢で肩を上げ、そのあと回る順番になりました。ただ、爪先の高さはいちばん上がったところで 67mm で、朝の回と変わっていません。持ち上げの順番は直せても、肩の不足は積み重なったままです。

お手本の動きを実機でそのまま再生すると、ブロックは4回とも箱に入った

AI が悪いのか、アームが悪いのかを分けるため、お手本の動き(手でアームを動かして教えた要所の姿勢)を、同じアームでそのまま再生して、斜め上と横から撮りました。

daemon を起動し直し、上カメラでブロックと箱の位置を測ってから、まずドライラン(アームに何も送らない)で計画を確かめました。

14_sec10.txt5,202字・75行・text
(base) shimazueiichi@shimadzueiichinoMacBook-Pro ~ % ssh -i ~/.ssh/id_rsa_pi ubuntu@192.168.2.1 "setsid nohup python3 /home/ubuntu/mecharm_daemon.py </dev/null >>/home/ubuntu/daemon.log 2>&1 &"; sleep 8; ssh -i ~/.ssh/id_rsa_pi ubuntu@192.168.2.1 'tail -3 ~/daemon.log; ss -ltn | grep 5555'
[DAEMON 14:36:57] ✓ MyCobot 初期化完了
[DAEMON 14:36:57] startup_warmup angles=[1.14, 94.04, 9.05, -0.7, -104.5, -29.53]
[DAEMON 14:36:57] ポート 5555 で待ち受け中...
LISTEN0      1                                 0.0.0.0:5555        0.0.0.0:*    
(base) shimazueiichi@shimadzueiichinoMacBook-Pro ~ % cd ~/r2r2r-scripts/dynamic_tracking_limits/drag_teaching
python3 record_teach_demo.py measure --log 359
[RTD   ] 到達許容差は本線 run_pick_place.py の stage 値を使います(区間ごと。一律 8.0° は 2026-10-19 にやめた):
別のタブで開く保存
15_sec10.txt6,659字・82行・text
(base) shimazueiichi@shimadzueiichinoMacBook-Pro drag_teaching % python3 record_teach_demo.py run --teach drag_v2_20221013_231249.json
[RTD   ] 到達許容差は本線 run_pick_place.py の stage 値を使います(区間ごと。一律 8.0° は 2026-10-19 にやめた):
[RTD   ]   HOME                       2.5°  ← rpp.TOL_HOME(stage1_to_home / HOME 復帰)
[RTD   ]   APPROACH_PICK              2.0°  ← rpp.TOL_APPROACH(approach 系)
[RTD   ]   PICK                       3.0°  ← rpp.TOL_PICK_PLACE(stage4_pick)
[RTD   ]   APPROACH_PICK (lift)       3.0°  ← rpp.TOL_LIFT_BACK(lift の残差 2.19° を吸収)
[RTD   ]   APPROACH_PLACE             3.0°  ← rpp.TOL_APPROACH_PLACE(stage7)
[RTD   ]   PLACE                      3.0°  ← rpp.TOL_PICK_PLACE(stage8_place)
別のタブで開く保存

実機で再生した回です(16:46)。

16_sec10.txt11,491字・151行・text
(base) shimazueiichi@shimadzueiichinoMacBook-Pro drag_teaching % python3 record_teach_demo.py run --teach drag_v2_20221013_231249.json --execute --no-record
[RTD   ] 到達許容差は本線 run_pick_place.py の stage 値を使います(区間ごと。一律 8.0° は 2026-10-19 にやめた):
[RTD   ]   HOME                       2.5°  ← rpp.TOL_HOME(stage1_to_home / HOME 復帰)
[RTD   ]   APPROACH_PICK              2.0°  ← rpp.TOL_APPROACH(approach 系)
[RTD   ]   PICK                       3.0°  ← rpp.TOL_PICK_PLACE(stage4_pick)
[RTD   ]   APPROACH_PICK (lift)       3.0°  ← rpp.TOL_LIFT_BACK(lift の残差 2.19° を吸収)
[RTD   ]   APPROACH_PLACE             3.0°  ← rpp.TOL_APPROACH_PLACE(stage7)
[RTD   ]   PLACE                      3.0°  ← rpp.TOL_PICK_PLACE(stage8_place)
別のタブで開く保存

16:45〜17:06 に4回再生し、4回とも区間ごとの到達の判定は OK で、最後まで進みました。4回の区間ごとの動きを、試行の記録から並べたスクリプトと出力です。

# お手本の再生 (record_teach_demo.py run --execute) 4 回の、区間ごとの記録を並べる (LOG361)
import os, json, glob
ROOT = os.environ.get("R2R2R_DATA", "/Volumes/KIOXIA/r2r2r-data"); os.chdir(ROOT)
for p in sorted(glob.glob("runs/run_pick_place/run_0001_20261022_1[67]*/run_*.json")):
    d = json.load(open(p))
    print(f"== {d['run_stem']}  result={d['result']}  始まり {d['started_at']}")
    for s in d.get("segments", []):
        if s["kind"] == "move":
            print(f"  STEP {s['step']:2d}  {s['from_name']:>15s} → {s['to_name']:<26s} max|Δ|={s['max_delta_deg']:6.2f}°  arrival_ok={s['arrival_ok']}")
        else:
            print(f"  STEP {s['step']:2d}  gripper {s['action']:<5s}  daemon 往復 {s['daemon_roundtrip_sec']:.2f}s")
    fa = d.get("final_actual")
    print("  最後の実測:", fa)
17_sec10.txt3,822字・52行・text
== run_0001_20261022_164511  result=completed  始まり 2026-10-22T16:45:11.399023
  STEP  1          current → HOME                       max|Δ|=  1.66°  arrival_ok=True
  STEP  2  gripper open   daemon 往復 3.57s
  STEP  3             HOME → APPROACH_PICK              max|Δ|= 71.80°  arrival_ok=True
  STEP  4    APPROACH_PICK → PICK                       max|Δ|= 12.04°  arrival_ok=True
  STEP  5  gripper close  daemon 往復 1.56s
  STEP  6             PICK → APPROACH_PICK (lift)       max|Δ|= 12.04°  arrival_ok=True
  STEP  7    APPROACH_PICK → APPROACH_PLACE             max|Δ|= 69.16°  arrival_ok=True
別のタブで開く保存
お手本を再生した回を横から撮った動画のコマ。0:24 でブロックを持ち上げ、0:25〜0:27 で箱のふちより上を通って箱の上へ回り、0:33 で離している

横から見ると、ブロックは箱のふちより十分高いところを通っています。お手本の高さに余裕がなかったわけではありません。再生では、つかむ区間(PICK)から持ち上げる区間(APPROACH_PICK (lift))へ、決まった絶対の角度を目標に送ります。持ち上げでも肩は少し届かずに止まりますが、その不足は次の区間に持ち越されず、次の区間はまた決まった角度を目標にします。AI で動かすときとの違いはここでした。

AI に渡す角度を、直前に送った目標にする —state-from-target を足した

AI に渡す「いまの角度」を、測った値ではなく、直前のチャンクで最後に送った目標にするオプションを足しました。グリッパーの値は今までどおり測った値です。目標と測った値の差が 6° を超えたら(腕がどこかに当たって止まっているときなど)、その回だけ測った値に戻します。安全の検査と移動量の制限は、今までどおり測った値を基準にしています。Claude Code の報告です。

18_AI_—state-from-target.txt3,590字・83行・text
別のタブで開く保存

コミットの説明文です。中継点を送る --via-stride も、このコミットで一緒に入っています。

commit 483a82ce6d8ac6a71569bbfba5a1cdbe0e9abd2c
Date:   Thu Oct 22 17:22:45 2026 +0900

モデルに渡す state を直前に送った目標にできるようにする (--state-from-target / LOG359)

LOG359: ブロックを持つと肩 j1 が毎チャンク 2.4〜2.6° 指令より低いところで止まる
(持つ前は約 1°). 同じ目標を送り直しても縮まらない (settle で確認済み). モデルは
実測の state から次を予測するので, この不足がチャンクごとに積み重わる. つかんだあとの
3 チャンクで, 予測した持ち上げ 約 20° に対して実際は 約 12° しか上がらず, ブロックが
箱の壁に当たった. お手本の再生 (record_teach_demo.py) は毎回 絶対の角度を目標に送るので
不足は 2〜3° のまま積み重ならず, 箱に入る (2026-10-22 の動画で確認).

- --state-from-target (既定 off = 今までどおり実測を渡す) を追加. on のとき, モデルに
  渡す state の 6 関節を「直前のチャンクで最後に送った目標」(中継点を使う場合は最後の
  中継点の目標) にする. グリッパーの値は今までどおり実測.
- 最初のチャンク / 直前のチャンクを送らなかった場合 (s で飛ばした・送信なし・中断・
  ドライラン) は実測を渡す.
- 送った目標と実測の差が どれかの関節で STATE_TARGET_MAX_GAP_DEG (6°) を超えていたら,
  そのチャンクは実測を渡して [WARN] を出す (腕がどこかに当たって止まっているときに
  目標だけが先へ進むのを防ぐ). 止めはしない.
- apply_limits / check_send_safe / 中継点の「実測からのかけ直し」は今までどおり実測が
  基準. 変えるのは **モデルに渡す state だけ**.
- 記録: log.jsonl のチャンクの行に state_source ("measured" / "target") と
  measured_state (実測) を追加. state はモデルに渡したほう. 画面にも [STATE] で
  どちらを渡したかと 目標と実測の最大差を 1 行出す.

変更なし: _send_and_wait_arrival 本体, HOME, SPEED, SEND_WAIT_SEC, J4_SAFE,
J1_SAFE_MAX, max_move の値, plan_chunk, --via-stride, settle, h / q / s, グリッパー.

偽の daemon で確認 (アームには送っていない. 5 ケースすべて期待どおり):
  off                 : 渡した state j1 60.00 → 56.50 → 53.00 / 送った目標 54.0 → 50.5 → 47.0
                        (1 チャンクごとに 2.5° の不足が積み重なる)
  on                  : 渡した state j1 60.00 → 54.00 → 48.00 / 送った目標 54.0 → 48.0 → 42.0
                        (開始 60° から 3 チャンクでちょうど -18°)
  差が 6° 超          : その回だけ実測に戻して [WARN] (state_source=measured)
  s で飛ばした次      : 実測に戻る
  ドライラン          : 送信 0 回・毎チャンク実測

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

偽の腕(送ると j1 が目標より 2.5° 高いところで止まる)での確認では、測った値を渡すと3チャンクで −18° のはずが −13° になり、目標を渡すと −18° ちょうどになっています。

18:16 の回で、π0.5 がブロックを初めて箱に入れた

夕方、Pod を借り直してサーバーを起動し、目標を渡すやり方(--state-from-target)と中継点(--via-stride 3)を合わせて動かしました。1回目(18:13)は、最初の問い合わせがコンパイルで約60秒かかり、途中で止めています。ターミナルに貼り付けた行がくずれたところも、そのまま載せています。

19_sec12.txt8,128字・100行・text
Last login: Thu Oct 22 09:18:22 on ttys008
(base) shimazueiichi@shimadzueiichinoMacBook-Pro ~ % python3 pi05/run_policy_client.py --raw-dir $RAW --steps-per-chunk 10 --via-stride 3 --settle-retries 0 --state-from-target --execute
python3: can't open file '/Users/shimazueiichi/pi05/run_policy_client.py': [Errno 2] No such file or directory
(base) shimazueiichi@shimadzueiichinoMacBook-Pro ~ % cd ~/r2r2r-scripts
RAW=/Volumes/KIOXIA/r2r2r-data/datasets/mecharm_redblock_box_teach_v1_trim2/raw_episodes
python3 pi05/run_policy_client.py --raw-dir $RAW --steps-per-chunk 10 --via-stride 3 --settle-retries 0 --state-from-target --execute
[SAFE  ] 安全範囲 (学習 19 本 ±10°): j0[-72,+55]  j1[-9,+88]  j2[-46,+41]  j3[-18,+27]  j4[-90,+10]  j5[-51,-14]
[SAFE  ] 肩 j1 の上限は J1_SAFE_MAX=88° で切った (これを超える指令はアームが黙って捨てる / LOG352)  元データ: /Volumes/KIOXIA/r2r2r-data/datasets/mecharm_redblock_box_teach_v1_trim2/raw_episodes
別のタブで開く保存

2回目(18:16)です。

20_sec12.txt23,898字・295行・text
(base) shimazueiichi@shimadzueiichinoMacBook-Pro r2r2r-scripts % cd ~/r2r2r-scripts
RAW=/Volumes/KIOXIA/r2r2r-data/datasets/mecharm_redblock_box_teach_v1_trim2/raw_episodes
python3 pi05/run_policy_client.py --raw-dir $RAW --steps-per-chunk 10 --via-stride 3 --settle-retries 0 --state-from-target --execute
[SAFE  ] 安全範囲 (学習 19 本 ±10°): j0[-72,+55]  j1[-9,+88]  j2[-46,+41]  j3[-18,+27]  j4[-90,+10]  j5[-51,-14]
[SAFE  ] 肩 j1 の上限は J1_SAFE_MAX=88° で切った (これを超える指令はアームが黙って捨てる / LOG352)  元データ: /Volumes/KIOXIA/r2r2r-data/datasets/mecharm_redblock_box_teach_v1_trim2/raw_episodes
[MODE  ] EXECUTE  steps/chunk=10  max_move=20.0°  log=/Volumes/KIOXIA/r2r2r-data/runs/pi05_policy/pi05_20261022_181602
[CAMERA] 写り方 dy=-5.5 dx=+0.3 response=0.533 → OK (|dy|<=20 / |dx|<=20 / response>=0.20)
[CAMERA] 基準: /Volumes/KIOXIA/r2r2r-data/datasets/mecharm_redblock_box_teach_v1_trim2/raw_episodes/run_0001_20261019_183454/frames/000000.jpg
別のタブで開く保存

チャンク05 で、お手本より深いところ(肩 j1 ≈ 80°)で爪を閉じ、06 で回る前に肩を上げ、07〜09 で箱の方へ回り、10 で少し下げて、11 で爪を開きました。チャンク01 以降は、モデルに渡した角度が「直前に送った目標」になっていて([STATE ] 目標を渡す)、目標と測った値の差は最大 2.82° に収まっています。爪先の高さは、閉じたとき 2.1mm、いちばん上がったところで 86.7mm です。お手本の 125〜135mm には届いていませんが、横から撮った動画では、ブロックは箱のふちを越えて箱の中に落ちています。

モデルに渡した角度の出どころごとに、肩を上げる指令の合計と実際に上がった量を比べると、次のようになります(「肩を上げる指令43回」の節の出力から)。

記録モデルに渡した角度
ブロックを持って上げた指令の合計
実際に上がった量
10:07(100713)測った値−20.3°−12.0°
10:20(102053)測った値−41.2°−11.1°
13:10(131036)測った値−19.6°−11.9°
18:16(181602)直前の目標−50.8°−42.2°
18:26(182650)直前の目標−24.6°−15.6°

続く4回は、2回を1チャンクで止め、1回は箱の位置で離し、2回はつかみ損ねた

18:24 と 18:25 の回は、最初のチャンクを送ったあと h で止めました。

21_sec13.txt6,231字・77行・text
(base) shimazueiichi@shimadzueiichinoMacBook-Pro r2r2r-scripts % cd ~/r2r2r-scripts
RAW=/Volumes/KIOXIA/r2r2r-data/datasets/mecharm_redblock_box_teach_v1_trim2/raw_episodes
python3 pi05/run_policy_client.py --raw-dir $RAW --steps-per-chunk 10 --via-stride 3 --settle-retries 0 --state-from-target --execute
[SAFE  ] 安全範囲 (学習 19 本 ±10°): j0[-72,+55]  j1[-9,+88]  j2[-46,+41]  j3[-18,+27]  j4[-90,+10]  j5[-51,-14]
[SAFE  ] 肩 j1 の上限は J1_SAFE_MAX=88° で切った (これを超える指令はアームが黙って捨てる / LOG352)  元データ: /Volumes/KIOXIA/r2r2r-data/datasets/mecharm_redblock_box_teach_v1_trim2/raw_episodes
[MODE  ] EXECUTE  steps/chunk=10  max_move=20.0°  log=/Volumes/KIOXIA/r2r2r-data/runs/pi05_policy/pi05_20261022_182419
[CAMERA] 写り方 dy=-5.3 dx=+2.6 response=0.452 → OK (|dy|<=20 / |dx|<=20 / response>=0.20)
[CAMERA] 基準: /Volumes/KIOXIA/r2r2r-data/datasets/mecharm_redblock_box_teach_v1_trim2/raw_episodes/run_0001_20261019_183454/frames/000000.jpg
別のタブで開く保存

18:26 の回は、チャンク04 でつかみ、09 で箱の位置(土台 j0 ≈ +41°)まで回り、10 で爪を開きました。爪先の高さはいちばん上で 93.8mm です。この回は動画を撮っていないので、ブロックが箱に入ったかは確かめていません。

22_sec13.txt19,858字・245行・text
(base) shimazueiichi@shimadzueiichinoMacBook-Pro r2r2r-scripts % python3 pi05/run_policy_client.py --raw-dir $RAW --steps-per-chunk 10 --via-stride 3 --settle-retries 0 --state-from-target --execute
[SAFE  ] 安全範囲 (学習 19 本 ±10°): j0[-72,+55]  j1[-9,+88]  j2[-46,+41]  j3[-18,+27]  j4[-90,+10]  j5[-51,-14]
[SAFE  ] 肩 j1 の上限は J1_SAFE_MAX=88° で切った (これを超える指令はアームが黙って捨てる / LOG352)  元データ: /Volumes/KIOXIA/r2r2r-data/datasets/mecharm_redblock_box_teach_v1_trim2/raw_episodes
[MODE  ] EXECUTE  steps/chunk=10  max_move=20.0°  log=/Volumes/KIOXIA/r2r2r-data/runs/pi05_policy/pi05_20261022_182650
[CAMERA] 写り方 dy=-5.4 dx=-0.2 response=0.599 → OK (|dy|<=20 / |dx|<=20 / response>=0.20)
[CAMERA] 基準: /Volumes/KIOXIA/r2r2r-data/datasets/mecharm_redblock_box_teach_v1_trim2/raw_episodes/run_0001_20261019_183454/frames/000000.jpg
[POLICY] 接続 ws://localhost:8000  metadata={}
[GRIP  ] 開始時のグリッパー開放は, 最初の目標が検査を通ってから行います (爪が閉じていたら手で開けておく)
別のタブで開く保存

18:30 と 18:32 の回は、爪を閉じたときにブロックが爪の間に入っておらず、つかみ損ねたので途中で h で止めました。動画では、爪の先がブロックの手前に下り、ブロックを押して位置をずらしています。

23_sec13.txt21,597字・265行・text
(base) shimazueiichi@shimadzueiichinoMacBook-Pro r2r2r-scripts % python3 pi05/run_policy_client.py --raw-dir $RAW --steps-per-chunk 10 --via-stride 3 --settle-retries 0 --state-from-target --execute
[SAFE  ] 安全範囲 (学習 19 本 ±10°): j0[-72,+55]  j1[-9,+88]  j2[-46,+41]  j3[-18,+27]  j4[-90,+10]  j5[-51,-14]
[SAFE  ] 肩 j1 の上限は J1_SAFE_MAX=88° で切った (これを超える指令はアームが黙って捨てる / LOG352)  元データ: /Volumes/KIOXIA/r2r2r-data/datasets/mecharm_redblock_box_teach_v1_trim2/raw_episodes
[MODE  ] EXECUTE  steps/chunk=10  max_move=20.0°  log=/Volumes/KIOXIA/r2r2r-data/runs/pi05_policy/pi05_20261022_183027
[CAMERA] 写り方 dy=-5.1 dx=+0.3 response=0.502 → OK (|dy|<=20 / |dx|<=20 / response>=0.20)
[CAMERA] 基準: /Volumes/KIOXIA/r2r2r-data/datasets/mecharm_redblock_box_teach_v1_trim2/raw_episodes/run_0001_20261019_183454/frames/000000.jpg
[POLICY] 接続 ws://localhost:8000  metadata={}
[GRIP  ] 開始時のグリッパー開放は, 最初の目標が検査を通ってから行います (爪が閉じていたら手で開けておく)
別のタブで開く保存
つかみ損ねた回(18:30 か 18:32 の回)を横から撮った動画のコマ。0:52 で爪がブロックに当たり、1:02 でブロックが押されて位置がずれ、1:10 で何もない所で爪を閉じている

位置D に移ろうとしたところで、手首 j4 が +45.87° のまま動かなくなった

次に位置D を試そうとすると、写り方の確認で止まりました。HOME に戻す前の姿勢を見ると、手首 j4 が +54.93° になっていました。

24_sec14.txt14,855字・164行・text
(base) shimazueiichi@shimadzueiichinoMacBook-Pro r2r2r-scripts % python3 pi05/run_policy_client.py --raw-dir $RAW --steps-per-chunk 10 --via-stride 3 --settle-retries 0 --state-from-target --execute
[SAFE  ] 安全範囲 (学習 19 本 ±10°): j0[-72,+55]  j1[-9,+88]  j2[-46,+41]  j3[-18,+27]  j4[-90,+10]  j5[-51,-14]
[SAFE  ] 肩 j1 の上限は J1_SAFE_MAX=88° で切った (これを超える指令はアームが黙って捨てる / LOG352)  元データ: /Volumes/KIOXIA/r2r2r-data/datasets/mecharm_redblock_box_teach_v1_trim2/raw_episodes
[MODE  ] EXECUTE  steps/chunk=10  max_move=20.0°  log=/Volumes/KIOXIA/r2r2r-data/runs/pi05_policy/pi05_20261022_183456
[CAMERA] 写り方 dy=-10.2 dx=+2.5 response=-0.027 → 測れない (相関が弱く dx/dy が当てにならない (response=-0.027 < 0.20))
[CAMERA] 基準: /Volumes/KIOXIA/r2r2r-data/datasets/mecharm_redblock_box_teach_v1_trim2/raw_episodes/run_0001_20261019_183454/frames/000000.jpg
[DONE  ] 記録: /Volumes/KIOXIA/r2r2r-data/runs/pi05_policy/pi05_20261022_183456
[ABORT ] 上カメラの写り方が『測れない』. アームには何も送らずに終了しました.
別のタブで開く保存

HOME に戻すと一度は戻りましたが、その後、手首 j4 が +45.87° のまま、指令を送っても動かなくなりました。daemon を起動し直しても、アームの電源を入れ直しても変わりません。手首を触っても熱くはなっていませんでした。この日の実機はここで終えています。

25_sec14.txt7,010字・132行・text
(base) shimazueiichi@shimadzueiichinoMacBook-Pro r2r2r-scripts % python3 dynamic_tracking_limits/place/place_modules/08_home_from_anywhere.py
python3 -c "from visual_servo_pick import get_daemon_socket, daemon_set_gripper, close_daemon_socket; get_daemon_socket(); daemon_set_gripper(0); close_daemon_socket()"
[INIT] daemon 接続中...
  [WARN] fresh_mode切替不可: daemon error: set_fresh_mode is not available in this pymycobot environment
[INIT] fresh_mode=1
[PRE]  現在姿勢: [-0.61, 1.49, 1.93, -0.17, 45.87, -30.23]
[SEND] HOME へ移動指令
[WAIT] t=2.0s  max_err=45.87°
別のタブで開く保存

最後に、夕方の Pod のサーバーのログを KIOXIA に持ち帰りました。

(base) shimazueiichi@shimadzueiichinoMacBook-Pro r2r2r-scripts % scp -P <SSH_PORT> -i ~/.ssh/id_ed25519 root@<POD_IP>:/workspace/logs/serve.log /Volumes/KIOXIA/r2r2r-data/lab/Log12/serve_evening.log
serve.log                                     100% 3581     9.3KB/s   00:00    
(base) shimazueiichi@shimadzueiichinoMacBook-Pro r2r2r-scripts % 
26_sec14.txt3,580字・39行・text
INFO:root:Loading model...
INFO:2026-10-22 09:12:17,659:jax._src.xla_bridge:925: Unable to initialize backend 'rocm': module 'jaxlib.xla_extension' has no attribute 'GpuAllocatorConfig'
INFO:jax._src.xla_bridge:Unable to initialize backend 'rocm': module 'jaxlib.xla_extension' has no attribute 'GpuAllocatorConfig'
INFO:2026-10-22 09:12:17,662:jax._src.xla_bridge:925: Unable to initialize backend 'tpu': INTERNAL: Failed to open libtpu.so: libtpu.so: cannot open shared object file: No such file or directory
INFO:jax._src.xla_bridge:Unable to initialize backend 'tpu': INTERNAL: Failed to open libtpu.so: libtpu.so: cannot open shared object file: No such file or directory
INFO:absl:orbax-checkpoint version: 0.11.13
INFO:absl:Created BasePyTreeCheckpointHandler: use_ocdbt=True, use_zarr3=False, pytree_metadata_options=PyTreeMetadataOptions(support_rich_types=False), array_metadata_store=<orbax.checkpoint._src.metadata.array_metadata_store.Store object at 0x7b3c425f5150>
INFO:absl:Restoring checkpoint from /workspace/ckpt/3999/params.
別のタブで開く保存

connection open が7回あり、夕方に AI へ問い合わせた7回(18:13・18:16・18:24・18:25・18:26・18:30・18:32)と数が合います。

実機の回すべてのチャンクの記録

この日の π0.5 の回すべてについて、log.jsonl からチャンクごとの表を作りました。推論にかかった時間、使ったステップ、モデルに渡した角度(state)とその出どころ、送った目標、爪の指令、送ったか・着いたか、中継点の数、送り直しの結果です。モデルに渡した角度が目標だった行には、そのとき測った角度(measured)を次の行に出しています。

"""LOG361: 2026-10-22 の π0.5 実機の回を, チャンクごとの表にする (読むだけ).
runs/pi05_policy/pi05_20261022_*/log.jsonl を読み, 1 行 = 1 チャンクで
推論時間 / 使ったステップ / モデルに渡した state / 送った目標 / グリッパー /
送ったか / 着いたか / state の出どころ / 中継点の数 / settle の結果 を出す.
"""
import json, glob, os
ROOT = os.environ.get("PI05_RUNS", "/Volumes/KIOXIA/r2r2r-data/runs/pi05_policy")
def f6(v):
    return "[" + " ".join(f"{x:+6.1f}" for x in v[:6]) + "]" if v else "-"
for d in sorted(glob.glob(ROOT + "/pi05_20261022_*")):
    L = [json.loads(l) for l in open(d + "/log.jsonl")]
    fr = [l for l in L if l.get("type") == "framing"]
    ch = [l for l in L if "chunk" in l]
    print(f"== {os.path.basename(d)}  チャンク {len(ch)}")
    for f in fr:
        print(f"  framing dy={f['dy']} dx={f['dx']} response={f['response']} -> {f['verdict']}")
    for c in ch:
        src = c.get("state_source", "measured")
        vias = len(c.get("vias") or [])
        st = c.get("settle") or {}
        print(f"  {c['chunk']:02d} {c['latency_s']*1000:6.0f}ms used={c.get('used_step')} "
              f"state={f6(c['state'])} src={src:8s} target={f6(c.get('target'))} "
              f"grip={c.get('grip_cmd')} exec={c.get('executed')} arrived={c.get('arrived')} "
              f"vias={vias} settle={st.get('reason','-')}")
        if src == "target" and c.get("measured_state"):
            print(f"       measured={f6(c['measured_state'])}")
27_sec15.txt30,101字・227行・text
== pi05_20261022_095251  チャンク 9
  framing dy=-0.49 dx=0.62 response=0.524 -> OK
  00  52684ms used=9 state=[  -0.3   +1.2   +0.7   +0.3   -0.3  -29.8] src=measured target=[ -10.7  +21.2   +5.7   -2.8  -20.3  -27.7] grip=None exec=True arrived=True vias=0 settle=-
  01    861ms used=9 state=[ -10.7  +22.2   +6.4   -3.1  -19.2  -28.3] src=measured target=[ -23.2  +42.2  +12.7   -2.9  -39.2  -27.8] grip=None exec=True arrived=True vias=0 settle=-
  02    804ms used=9 state=[ -23.5  +43.2  +13.3   -2.2  -39.0  -27.9] src=measured target=[ -33.0  +59.7  +18.8   -2.2  -59.0  -27.8] grip=None exec=True arrived=True vias=0 settle=-
  03   2310ms used=6 state=[ -33.0  +60.4  +18.4   -2.0  -60.5  -27.9] src=measured target=[ -32.8  +62.2  +16.8   -3.1  -59.5  -27.9] grip=1 exec=True arrived=True vias=0 settle=-
  04    804ms used=9 state=[ -33.0  +62.6  +16.5   -2.5  -61.6  -28.3] src=measured target=[ -19.5  +57.4  +15.3   +0.1  -61.4  -30.0] grip=None exec=True arrived=True vias=0 settle=-
  05    806ms used=9 state=[ -19.6  +59.3  +16.1   +0.2  -58.4  -29.3] src=measured target=[  +0.4  +53.6  +11.2   +3.7  -54.9  -31.2] grip=None exec=True arrived=True vias=0 settle=-
別のタブで開く保存

チャンクごとの姿勢を、お手本(位置C)からの距離と、移動量の上限で削られた関節と一緒に並べた表です。

# 実機の各回を、お手本 19 本と比べる (LOG361)
#  1) 各チャンクの状態と、お手本でいちばん近い状態との距離 (6 関節の差の大きさ, 同じグリッパー状態の中で)
#  2) 送った目標 (上限・検査のあと) と予測 (actions の使った行) の差 = 上限で削られた量
import os, json, glob
import numpy as np
ROOT = os.environ.get("R2R2R_DATA", "/Volumes/KIOXIA/r2r2r-data"); os.chdir(ROOT)
RAW = "datasets/mecharm_redblock_box_teach_v1_trim2/raw_episodes"
A = np.concatenate([np.load(p)["state"] for p in sorted(glob.glob(RAW + "/*/episode.npz"))])
RUNS = ["20261021_183245", "20261022_095251", "20261022_100713", "20261022_102053",
        "20261022_114643", "20261022_131036", "20261022_181602", "20261022_182650"]
for r in RUNS:
    L = [json.loads(l) for l in open(f"runs/pi05_policy/pi05_{r}/log.jsonl")]
    L = [x for x in L if "chunk" in x]
    print(f"== {r}  チャンク数 {len(L)}")
    print(" ch  g |     j0     j1     j2     j4 | お手本との距離 | 上限で削られた関節")
    for d in L:
        s = np.array(d.get("measured_state", d["state"]))
        m = A[:, 6] == s[6]
        dist = np.linalg.norm(A[m][:, :6] - s[:6], axis=1).min()
        notes = " / ".join(n.split(" ")[0] for n in d.get("notes", []))
        print(f" {d['chunk']:02d}  {int(s[6])} | {s[0]:+6.1f} {s[1]:+6.1f} {s[2]:+6.1f} {s[4]:+6.1f} | {dist:6.1f}° | {notes}")
28_sec15.txt6,789字・136行・text
== 20261021_183245  チャンク数 13
 ch  g |     j0     j1     j2     j4 | お手本との距離 | 上限で削られた関節
 00  0 |   -0.2   +1.0   +0.9   +1.0 |    1.0° | j1 / j4
 01  0 |  -11.3  +22.1   +0.9  -17.9 |    4.8° | j1 / j4
 02  0 |  -27.2  +43.1   +0.9  -37.7 |   11.1° | j1 / j4
 03  0 |  -39.6  +64.2   +1.8  -57.4 |   16.9° | 
 04  1 |  -39.7  +66.4   +2.6  -57.6 |   18.7° | 
 05  1 |  -29.9  +63.2   +4.4  -55.7 |   19.1° | j0
別のタブで開く保存

この回で変えた2つのファイルの全文(run_policy_client.py・camera_align.py)

アームを動かすクライアント pi05/run_policy_client.py の、この回の最後(コミット 483a82c)の全文です。

29_sec16.py.txt35,559字・757行・python
#!/usr/bin/env python3
"""run_policy_client.py — π0.5 (Runpod の serve_policy) の出力で mechArm 270 を動かす Mac 側クライアント.

流れ (1 チャンクごと):
  上カメラ画像 + 実測 6 軸 + グリッパー状態 → π0.5 → 行動チャンク (10 x 7, 0.1 秒刻みの絶対角)
  → 先頭 N ステップ先 (既定 5 = 0.5 秒先) の姿勢へ単発 send_angles (学習データと同じ速度 20)
  → --via-stride N を付けると, 途中の姿勢 (N-1, 2N-1, ... ステップ) も中継点として
     順に送る (チャンクの中の順番を残すため. 既定 0 = 1 点だけ送る従来どおり)
別のタブで開く保存

上カメラの写り方を確かめる pi05/camera_align.py の全文です(コミット b5b093c)。

30_sec16.py.txt10,409字・258行・python
#!/usr/bin/env python3
"""camera_align.py — 上カメラの写り方をお手本に合わせる画面 (Mac 側・アームは動かさない).

上カメラの iPhone は普段使いのもので, 毎回付け外しする. 付けるたびに写り方が変わり,
2026-10-21 の実機では お手本より下に大きくずれたまま動かしてしまった
(runs/pi05_policy/pi05_20261021_082645 は dy=+37.5. 1 分前のドライランは dy=-7.8 で正常).
iPhone を付けたあと, この画面で写り方を合わせてから実機に進む.
別のタブで開く保存

コミットごとの変更の大きさです。

b5b093c camera_align.py: --episode で重ねるお手本を選べるようにする (LOG359)
 pi05/camera_align.py | 94 ++++++++++++++++++++++++++++++++++++++++++++--------
 1 file changed, 80 insertions(+), 14 deletions(-)
c7e726a 送った目標とのずれを同じ目標の送り直しで詰める (LOG359)
 pi05/run_policy_client.py | 155 +++++++++++++++++++++++++++++++++++++++++++++-
 1 file changed, 153 insertions(+), 2 deletions(-)
483a82c モデルに渡す state を直前に送った目標にできるようにする (--state-from-target / LOG359)
 pi05/run_policy_client.py | 260 ++++++++++++++++++++++++++++++++++++++--------
 1 file changed, 215 insertions(+), 45 deletions(-)

次は、手首 j4 を直してから、位置D・E と 18:26 の回の再現を動画で確かめる

次にやることは3つです。

  1. 手首 j4 が +45.87° から動かない原因を調べる。daemon の起動し直しと電源の入れ直しでは戻っていない
  2. 位置C で、目標を渡すやり方の回を、横からの動画付きで何回か続けて動かし、箱に入る割合を数える。18:26 の回のように動画がない回を作らない
  3. 位置D・E で同じやり方を試す。位置E はブロックが爪の陰に入るので、camera_align.py を --alpha 0.7 で重ねて置く

まだ確かめていないことも残っています。目標を渡すやり方で、つかむ位置がお手本より深くなった(肩 j1 ≈ 80°、お手本は 64°)理由は分かっていません。つかみ損ねた2回が、深くつかもうとしたことと関係があるのかも、切り分けられていません。