---
title: "フィジカルAI研究室を作る Log#19 | ゲームパッドで mechArm 270 を直す準備ができた"
slug: gamepad-ready-to-correct-mecharm-270
date: 2026-11-02
day: 419
meta_description: "3,198円の互換ゲームパッドをMacにつなぐと、ボタン16個と軸6本が読め、mechArm 270の関節を1押し0.5°ずつ動かして爪を開け閉めし、操作を全部記録するスクリプトができた。実機はまだで、アームなしで172回確かめた。"
tags: [フィジカルAI研究室, mechArm 270, ゲームパッド, テレオペレーション, 人の補正, pygame, Figure, π0.5, Claude Code, codex]
series: "フィジカルAI研究室"
---

3,198円の互換品のゲームパッド（PS4 型）を Mac に Bluetooth でつなぐと、ボタン16個と軸6本がすべて読め、それを使って小型ロボットアーム mechArm 270 の関節を1押し0.5°ずつ動かし、爪を開け閉めし、押した操作をすべて記録するスクリプトまで作れました。ここまでは、アームに指令を送らないドライラン（画面の中の数字だけが動く試し）で172回の操作を確かめた段階で、実物のアームではまだ動かしていません。

この回でやったことで、次の3つが変わります。

- 良くなること：AI（Physical Intelligence の π0.5）が失敗しかけたときの人の直しが、キーボードで「c で補正に入り、関節の番号を選び、+ か − を押す」から、十字キーを押すだけになる。左右（根元の回転）はいつも十字キーの左右、上下は △ で選んだ関節
- もたらされること：**押した1回ごとに、送った角度と届いた角度が1行ずつ記録に残る。** 人の直しを、次の学習に使う「人が直した記録」として、そのまま集められる形になった
- 時間・費用：コントローラーは3,198円。Claude Code の実装は66分（外部のレビュー3回を含む）、アームなしの試しは3分33秒で172回の操作。キーボードで直していたときと比べて速くなるかは、まだ測っていない。実機で、1回の直しにかかる時間を両方で測って確かめる

きっかけは、人の家事の動画でロボットを学ばせている米国の Figure 社の話でした。人の目線の動画から学べるのは「どこへ手を伸ばすか」までで、力加減やロボット自身の体のくせは、ロボットのデータで合わせる必要があります。このプロジェクトでは、AI が mechArm 270 を動かし、失敗しかけたら人がキーで関節を直し、その直した記録で学び直す、ということを続けてきました（LOG417・LOG422）。その「人が直す」部分を、もっと手早く、もっと確実に記録に残せる形にするのが、この回のねらいです。

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

| 段階 | やったこと | 結果 |
|---|---|---|
| Figure の投稿を確かめる | 「Figure が一般の人の家事の動画で学習させ、知らない家での成功率が9%から56%になった」という投稿の数字を、報道と公開資料で確かめた | 数字はほぼ報道どおり。56% は Figure 自身の評価で、Tesla の求人の話は2024年のものだった |
| ゲームパッドを選ぶ | メーカーの公式品（129.99ドル）と、市販の互換品を比べた | 操作を自分のスクリプトで読めて記録できる互換品（3,198円）にした。mechArm の中の Raspberry Pi（Ubuntu 18.04）では互換品がつまずく例があるので、まず Mac につなぐ |
| 読み取りを確かめる | 値を表示するだけのスクリプト gamepad_check.py を作り、ボタンとスティックを全部押した | ボタン16個すべて反応、連射機能は入っていない、スティックは離すと毎回 +0.004 に戻る。値の更新は約0.05秒おき |
| 操作を決める | モードを2つに分ける案から、押した分だけそのまま動く1つの形にしぼった | 十字キー左右 = 根元、上下 = △ で選んだ関節、R1 で2°、× と ○ で爪、HOME 長押しで始めの姿勢、L2 と R2 の同時押しで停止 |
| スクリプトを作る | 依頼箱で Claude Code に gamepad_teleop.py を頼み、codex のレビューを3回通した | 1,048行とテスト46件。直すべき指摘は1回目7件・2回目2件・3回目0件 |
| アームなしで試す | ドライランで、ボタンを一通り押した | 172回の操作。範囲の守り・停止と再開・HOME が決めたとおりに動いた。前の動きが終わる前の押し25回は、ためずに捨てた |
| 残りを頼む | ドライランは記録しない・起動時の警告を直す、を依頼69で頼んだ | この記録の時点で作業中 |

【画像】LOG419_01_gamepad_and_mecharm.jpg 黒い PS4 型の互換ゲームパッドと、その奥に置いた mechArm 270 の爪（グリッパー）と赤いブロック

次は、止めてある実物のアームを動かせる状態に戻し、十字キーの左右の向きを確かめてから、AI の動きを人がゲームパッドで直す試しに入ります。

GPU を買うか借りるかの判断は、Runpod で実際に借りて学習させた費用をもとに、別のページにまとめています。

【バナー】gpu-buy-or-rent


## 作業ログ ― 互換ゲームパッドを Mac で読み、mechArm 270 を手で直すスクリプトをアームなしで試すまで

この回の作業は 11/1 の朝から 11/2 の夜まで。実物のアームは、前の回（LOG422）で手首の関節（j4）が指令に応えなくなったまま止めてあり、この回では一度も動かしていません。記事の中の「見込める効果」は、測った値ではなく見込みです。

### この回で使う言葉

| 言葉 | この記事での意味 |
|---|---|
| ゲームパッド | ゲーム機のコントローラー。この回は PS4 用の形をした互換品（純正ではない安い品）を使った |
| テレオペレーション | 人が手元の道具で、ロボットを離れた所から動かすこと。ここではゲームパッドで mechArm 270 の関節を動かすこと |
| ドライラン | 本物のアームに指令を送らず、送るはずだった角度を画面の数字だけで動かす試し |
| j0〜j5 | mechArm 270 の6つの関節。j0 は根元の回転、j1 は肩、j2 はひじ、j3 は腕のねじり、j4 は手首の曲げ、j5 は爪のねじり |
| command と reached | command はアームに送った角度、reached はアームが実際に届いた角度。2つの差が、アームの動きのずれになる |
| 基準の姿勢 | スクリプトを起動したときの姿勢。動かせる範囲（±15° や ±10°）は、この姿勢から数える |
| HOME | アームの始めの姿勢 [0, 0, 0, 0, 0, −30] |
| 依頼箱 | Claude が書いた依頼を Mac の見張り係が Claude Code に渡し、報告を書き戻す仕組み（iCloud のフォルダ） |
| codex | OpenAI のコードを読むAI。関節を動かすコードは、書いた Claude Code とは別のAIに読ませて確かめる |

### pygame は、Python でゲームの画面と入力を扱う道具

[pygame](https://www.pygame.org/) は、Python でゲームを作るための道具の集まりです。中では [SDL](https://www.libsdl.org/) という、画面・音・コントローラーを扱う部品が動いています。この回では、ゲームパッドのボタンとスティックの値を読むのと、今の関節の角度を出す小さな窓を作るのに使いました。

| pygame の言葉 | この記事での意味 |
|---|---|
| ボタン（button） | 押したか離したかの2通りの値。このゲームパッドでは0〜15の16個 |
| 軸（axis） | −1.0〜+1.0 の連続した値。スティックの横と縦、L2 と R2 の押し込み量で6本 |
| hat | 十字キーを1つの部品として読む形。このゲームパッドでは0個で、十字キーはボタン11〜14として読める |
| GUID | コントローラーの種類を表す番号。メーカーの番号と製品の番号が入っている |

使い方は、Mac のターミナルで `python3 -m pip install pygame` で入れ、スクリプトの中で `pygame.joystick` を読むだけです。

### Figure の Index は、人が家事を撮った動画をお金で集める仕組み

米国のヒューマノイド（人の形のロボット）の会社 [Figure](https://www.figure.ai/) は、2026年8月に Index というスマホのアプリを公開しました。一般の人が自分の家事を撮って送ると、撮った時間に応じてお金が払われます。集めた動画で、Figure のロボットを動かす AI（Helix）を学習させています。この回の調べで使った主な報道は [Forbes の記事](https://www.forbes.com/sites/johnkoetsier/2026/09/21/figures-billion-dollar-physical-ai-bet-delivered-a-6x-jump-in-robot-chore-success/) です。


## 11/1 朝：Figure の Index の投稿を確かめ、人の目線の動画で学べることを整理した

X で見た Martin（@0x_meden）の投稿のスクリーンショット3枚を Claude に見せ、事実かを確かめた。X のページは読み取りを断られたので、スクリーンショットの文字から読んだ。

結果は、数字はほぼ報道どおりだった。ただし、知らない家での成功率が9%から56%になったという数字は Figure 自身の評価で、第三者が同じ結果を出した記録はない。「ゼロショット」は、まったく初めての家事ができたという意味ではなく、家事ごとのデータは別にあって、学習に使っていない家でもできた、という意味だった。投稿の中の Tesla の求人（時給25〜48ドル）は2024年8月の話だった。

続けて「人の視点からの動画でほんとうにロボットを学習させることができるの？」と聞いた。答えは、人の動画で下地を作り、最後にロボット自身のデータで合わせる2段構え、だった。

| 人の目線の動画で学べること | 学べないこと |
|---|---|
| 何をどの順でやるか（タオルを広げてから畳む、など） | 力加減や、物が手の中でどう滑るか（映像に写らない） |
| 物のどこに手を伸ばすか、手首がどう動くか | ロボット自身の体の制約（関節の範囲・重さ・指の数の違い） |
| 見たことのない部屋や物への慣れ | 2本指のグリッパーと5本指の人の手の差。mechArm 270 に移せるのは「どこへ手を伸ばすか」まで |

「学べないこと」の側が、このプロジェクトで人がキーで直してきた所（根元の向きの数度のずれ、つかんだあとの持ち上げ）に当たる。直した記録はロボット自身のデータで、それを集めやすくすることが次の一手になる、という流れでゲームパッドの話に移った。調べた結果は、iCloud の research フォルダに調査メモとして残した。

### 調査メモ research_figure_index_human_video_20261101.md

````markdown
# Figure の Index（人の家事動画を買って学習）の事実確認と、人の目線の動画でロボットが学べるか

- 調べた日：2026-11-01
- きっかけ：エイイチが共有した X の投稿のスクリーンショット3枚（Martin @0x_meden の投稿、Grok の日本語訳）。「これは事実？」と「人の視点からの動画でほんとうにロボットを学習させることができるの？」の2つの問い
- 方針：投稿の数字を、報道・公式の資料・モデルの配布ページと突き合わせた。会社の発表は「会社の発表」と書き分ける
- 換算：1ドル＝150円で計算（円は目安）
- 注意：X の投稿ページはサイト側が読み取りを拒否したため開けなかった。投稿の中身はスクリーンショットから読んだ

---

## ① 要点

1. 投稿の数字はほぼ報道・発表と一致する。ただし 9%→56% は Figure 自身の評価で、第三者の再現はまだない
2. 「ゼロショット」は、まったく初めての家事ができたという意味ではない。家事ごとの学習データは別にあり、学習に使っていない家でもできた、という意味
3. Tesla の求人（時給25〜48ドル）は 2024年8月の報道で、今の動きではない
4. 人の目線の動画で学べるのは「何をどの順で」「どこへ手を伸ばすか」まで。力加減とロボット自身の体の制約は映像に写らないので、最後にロボットのデータで合わせる2段構えが今のやり方
5. 数字はどれも「自社の評価で」出ている。外から同じ結果を確かめる層（評価・再現）はまだ空いている

---

## ② 投稿の主張と確認結果

| 投稿の主張 | 確認結果 | 出典 |
|---|---|---|
| Figure が一般の人にお金を払い、家事を撮影してもらう Index というアプリ | 事実。2026-08-25 にステルスから公開。分単位で支払い | howaiworks・humanoidsdaily・pebblous |
| 108か国で 26.4万ダウンロード、週 4.4万人が投稿 | 一致 | howaiworks・Forbes |
| 1,600万本以上の動画 | 一致（審査で落ちた分も含む数） | pebblous |
| 支払い済み 1,500万ドル（約22.5億円） | 一致。1本あたり1ドル未満になるが、支払いは本数でなく撮影時間に対してなので、単価は出せない | pebblous |
| 今後12か月でデータと計算に10億ドル以上 | 一致（報道は「10億ドルを約束」） | explainx・runtimewire |
| 毎秒30分の映像 | 新しい発表では毎秒およそ35分 | Black Scarab・Forbes |
| 1日あたり4.9年分の人の仕事 | 確認できなかった | — |
| Helix 2.5 を Index で事前学習、学習に使っていないベイエリアの30軒で試験 | 一致。借りた家30軒 | Forbes・Black Scarab |
| Index なし 9% → あり 56% | 一致。最後までやり切った試行だけを成功と数え、420回中237回。すべての家で1回以上成功 | aixploria・Black Scarab |
| NVIDIA は GR00T N1.7 を EgoScale の2万時間で訓練 | 人の映像 20,854時間を事前学習に入れたのは事実。ただしそれだけで学習したのではなく、ロボットのデータと混ぜている | NVIDIA の配布ページ（GitHub）・aiwiki |
| Tesla が時給25〜48ドルでモーションキャプチャーのデータ収集係を雇用 | 2024年8月の報道。今の話ではない | Decrypt・AI Business |
| Mecka が Sequoia 主導と「報じられる」6,000万ドルのシリーズB | もう発表済み（2026-10-07）。NVIDIA・Microsoft の M12 なども参加。評価額は約5億ドル | af.net（TechCrunch ほかの要約）・valueaddvc |

---

## ③ 「ゼロショット」の意味

- Helix 2.5 は、新しい家事を言葉で頼まれて動きを編み出したのではない
- 家事ごとのデータ（タスクのデータ）は別にあり、その量が前の版（Helix 02）の半分で同じ成功率になった、というのが Figure の発表
- 評価の家事が Index の事前学習データに占める割合は、どれも 1.90% 以下（会社の発表）
- 変えたのは「Index で事前学習したかどうか」の1点だけ、という比べ方

---

## ④ 人の目線の動画でロボットが学べるか

**学べること**
- 何をどの順でやるか（タオルを広げてから畳む、など）
- 物のどこに手を伸ばすか、手首がどう動くか
- 見たことのない部屋や物への慣れ

**学べないこと**
- 力加減や、物が手の中でどう滑るか（映像に写らない）
- ロボット自身の体の制約（関節の範囲、重さ、指の数の違い）

**今のやり方は2段構え**
- NVIDIA の EgoScale：人の映像で大規模に事前学習し、ロボット固有の調整は約4時間だけ。人の映像を 1,000時間から 2万時間に増やすと、作業の達成度が平均で2倍以上になった（NVIDIA の研究者の報告、第三者の再現はまだ）
- 動画からカメラと手の姿勢を推定し、手首の相対的な動きを取り出している。動画から「動きの指示」に近いものを作っている
- Figure の 56% も、家事ごとのデータが半分で済んだという話で、ゼロになったわけではない

**mechArm に当てはめると**
- 2本指のグリッパーは5本指の人の手との差が大きい。人の動画から移せるのは「どこへ手を伸ばすか」まで、という見立て
- 関連：research_human_wearable_data_20261027.md（安いアームでは人のデータだけだと置くのを失敗した報告）、research_human_to_robot_conversion_20261027.md（人のデータをロボットに合わせる4分類）

---

## ⑤ Pixels to Motors から見た意味

- 数字はどれも自社の評価。評価と再現を外から担う層はまだ空いている（主指標「再現可能な環境数」と同じ方向）
- 同じ日の会話で出た方向：実機の動きとシミュレーションを重ねた記録を増やす。1回の試行ごとに「シミュレーションの計画（関節角の列）」「実機の結果（command と reached、映像）」「両者の差」の3点を揃える

---

## 出典

- [Forbes: Figure's Billion-Dollar Physical AI Bet Delivered A 6X Jump In Robot Chore Success](https://www.forbes.com/sites/johnkoetsier/2026/09/21/figures-billion-dollar-physical-ai-bet-delivered-a-6x-jump-in-robot-chore-success/)
- [Black Scarab: Figure takes Helix 2.5 into 30 unfamiliar homes](https://www.blackscarab.ai/news/figure-helix-2-5-zero-shot-homes)
- [aixploria: Helix 2.5](https://www.aixploria.com/en/helix-2-5-figure-ai-robot/)
- [howaiworks: Figure Launches Index](https://howaiworks.ai/blog/figure-index-robot-training-dataset-2026)
- [pebblous: Figure AI Index — Buying Humanoid Training Video](https://blog.pebblous.ai/blog/figure-index-robot-training-video-gig/en/)
- [explainx: Figure Index](https://explainx.ai/blog/figure-index-robot-dataset-august-2026)
- [GitHub: Isaac GR00T N1.7](https://github.com/precognitionlab/Isaac-GR00T-N1.7)
- [aiwiki: GR00T N1.7](https://aiwiki.ai/wiki/groot_n1_7/raw)
- [af.net: Mecka AI Series B](https://af.net/realtime/mecka-ai-raises-60m-series-b-led-by-sequoia-capital-for-robot-training-data/)
- [valueaddvc: Mecka](https://valueaddvc.com/pulse/company/mecka)
- [Decrypt: Tesla motion capture job](https://decrypt.co/245399/tesla-bot-robotics-job-motion-capture-suit?amp=1)
- [AI Business: Tesla Pays $48 an Hour](https://aibusiness.com/ml/tesla-pays-48-an-hour-for-motion-capture-to-develop-humanoid-robot)
````


## 11/1 朝：メーカーの公式ゲームパッドと互換品を比べ、3,198円の互換品にした

> 別件でmecharmの動きを補正するとき、コントローラーをゲーム用のをつけて操作したりは可能ですか？

mechArm 270 を作っている Elephant Robotics には、公式の Bluetooth ゲームパッド（129.99ドル、mechArm 270 Pi 対応）がある。ただ、押した操作を自分のスクリプトで受け取って記録できるか、今ある守り（J4_SAFE などの角度の範囲）を通るかが、商品ページのどこにも書かれていなかった。目的は「直した操作を記録に残す」ことなので、市販のゲームパッドを自分のスクリプトで読む形にした。

もう1つ調べたのは、つなぐ先。mechArm 270 の中の Raspberry Pi（Ubuntu 18.04）に直接つなぐと、互換品は「最初はつながるのに、電源を切ると再接続できない」「純正を名乗るのに Linux の PS4 用の部品に弾かれる」といった例が見つかった。Mac から使った報告があった Yommor の互換品（3,198円・USB-C・有線でも使える）を選び、まず Mac につなぐことにした。

| くらべたこと | メーカーの公式品 | 市販の互換品（Yommor） |
|---|---|---|
| 値段 | 129.99ドル（送料・関税は別） | 3,198円 |
| 操作を自分で記録できるか | 書かれていない | 自分のスクリプトで読むので、すべて記録できる |
| 今の守り（角度の範囲）を通るか | 分からない | 自分のスクリプトで通す |
| Mac での報告 | — | MacBook Air・iPad Air で使えたというレビュー |
| 気をつける点 | — | TURBO 連射（ボタン1回が連続入力になる）を切る。SHARE ボタンは押すと画面が開くというレビューがあり、使わない |

### 調査メモ research_gamepad_teleop_mecharm_20261101.md

````markdown
# ゲーム用コントローラーで mechArm を手で補正する：買うものと、つなぎ方の調査

- 調べた日：2026-11-01
- きっかけ：エイイチの問い「mechArm の動きを補正するとき、ゲーム用のコントローラーをつけて操作したりは可能ですか？」
- 方針：メーカーの商品ページ・掲示板・不具合の報告を開いて確かめたことだけを書く
- 換算：1ドル＝150円で計算（円は目安）
- 状態：Yommor の互換コントローラーを購入済み（2026-11-02 着）。動きの確認はまだ

---

## ① 要点

1. 可能。やり方は2つ。公式のゲームパッドを使うか、市販のゲーム用コントローラーを自分のスクリプトで読むか
2. 市販品を自分のスクリプトで読む方を選んだ。理由は、今ある保護（J4_SAFE・joint_limit）を必ず通せることと、操作した指令と届いた角度（command / reached）を台帳に残せること。補正の操作がそのまま「人が直した記録」になる
3. 最初は Mac につなぎ、Mac のスクリプトから SSH 越しに mechArm を動かす。mechArm の Raspberry Pi（Ubuntu 18.04）に直接つなぐのは、遅れや止まり方に不安が出てから
4. 互換品は Linux でつまずく例がはっきりある（再接続できない・USB でも弾かれる）。Mac で動いた報告がある品を選んだ

---

## ② 公式のゲームパッド（Elephant Robotics）

| 項目 | 内容 |
|---|---|
| 価格 | 129.99ドル（約2万円、送料・関税別） |
| 対応 | mechArm 270 Pi を含む |
| つなぎ方 | Bluetooth |
| できること | 位置・速度・姿勢など、いくつかの動かし方を選んで操作 |
| 分からないこと | 操作した内容を自分で記録できるか、自分のスクリプトで入力を受け取れるか、保護を通るか（どこにも書かれていない） |

レビュー4件のうち1件はペンホルダーの感想で、レビューは当てにならない。「とりあえず手で動かせればいい」場合の選択肢。

---

## ③ 買ったもの：Yommor P4 コントローラー（Amazon、3,198円）

| 項目 | 内容 | 出典 |
|---|---|---|
| つなぎ方 | Bluetooth。USB ケーブルでつなぐと有線としても使える | 商品ページ |
| 端子 | USB-C | Amazon の Rufus（AI）の回答とレビュー2件 |
| 公式の対応 | P3 / P4 / PC（Windows 7/8/10）。Mac・Linux は書かれていない | 商品ページ |
| Apple 機器での報告 | MacBook Air で問題なく接続、iPad Air M5 で問題なく接続 | レビュー |
| ペアリング | SHARE を押したまま HOME を押す（純正と同じ） | 商品ページ |
| 気をつける点 | TURBO 連射（ボタン1回が連続入力になる）は必ず切る。SHARE を押すと画面が立ち上がるというレビューがあり、ロボット操作のボタンには使わない | 商品ページ・レビュー |

**他の候補との違い**
- PS4 型の互換品はどれも中身がほぼ同じで、違いは見た目・ブランド・レビュー数・数百円の差
- Smalife（有線のみ）：Bluetooth の不具合とは無縁だが、Mac での報告がなく、USB 接続でも互換品が Linux で弾かれた例があるため選ばなかった
- Leadjoy（8,780円、2.4G の USB 受信機）：受信機は普通の USB コントローラーとして見えるので、将来 Pi に直接つなぐときに Bluetooth の不具合を避けられる可能性がある。今は要らない

---

## ④ Linux（mechArm の Raspberry Pi）で互換品を使う難しさ

| 例 | 内容 | 出典 |
|---|---|---|
| 再接続できない | Ubuntu 24.04 系でも、互換品は最初のペアリングは通るのに、電源を切ると再接続できない（2026年に報告、未解決） | BlueZ Issue #2380 |
| USB でも弾かれる | 純正の Sony を名乗る互換品が、Linux の PS4 用ドライバーで「情報が取れない」と弾かれ、修正パッチが必要だった | Arch Linux Forums |
| つながっても入力が来ない | Raspberry Pi でペアリングはできたのに動かず、USB の無線アダプターを買って解決 | RetroPie forum |
| Mac でも | 互換品は無線より有線のほうが安定しやすいという声 | Cycling '74 forum |

mechArm 270 Pi の出荷時の OS は Ubuntu 18.04（CNX Software）。古いので、新しい Linux で直った不具合も残っている可能性が高い。Yommor 自体の Linux での動作報告は見つからなかった。

---

## ⑤ Pi に直接つなぐ場合（今はやらない）

**必要なもの**
1. Mac から SSH で Pi に入り、bluetoothctl でペアリング（モニター・キーボードは要らない）
2. `/dev/input/js0` ができて、ボタンで値が出ることの確認
3. 読み取り用のライブラリ。Pi の Python が古いので、最新の pygame が入らないかもしれない。その場合は evdev か古い版
4. 操作スクリプトと保護を Pi 側に置く
5. ログは Pi の SD カードに書かれるので、KIOXIA に移す手順
6. 無線がダメなときは USB ケーブルで直挿しか、USB の無線アダプター

**良いところ**
- 押してから動くまでが短くなる（SSH 越しのひと手間がない）
- Mac と Pi の通信が途切れても「離したら止まる」が Pi の中で完結する（安全面でいちばん大きい）
- Mac なしで、アームとコントローラーだけで要所の姿勢を教えられる

**今やらない理由**
カメラ・台帳・KIOXIA が Mac 側にそろっている。Mac で始め、遅れや止まり方に不安が出たら Pi 直結に移る。

---

## ⑥ ロボットにつなぐ前の確認と、作るときの決まり

**届いたら（ロボットにはつながない）**
1. TURBO 連射が切れているか
2. Mac につながるか（Bluetooth、ダメなら USB-C ケーブル。充電専用ケーブルだと入力が届かない）
3. `~/r2r2r-scripts/tools/gamepad_check.py` で、ボタン・スティックの値が出るか。スティックを離したときに値が 0 付近（目安 ±0.05 以内）に戻るか。ずれが大きいと「触っていないのに動く」原因になる

**操作スクリプトを作るとき**
- まず「何を補正したいか」（PICK の位置合わせか、姿勢の微調整か）を決める。関節ごとに動かすか座標で動かすかがここで決まる
- スティックで連続的に指令を送ると、サーボの応答が追いつかずカクつきやすい。1回押すごとに少しだけ動かす方式から始める（pymycobot の jog 系の命令）
- ボタンを押している間だけ動く（離したら止まる）仕組みと、低速の上限を最初から入れる
- 関節を動かすコードなので、設計 → Claude Code で実装 → 外部レビュー → dry-run の順を飛ばさない

---

## 出典

- [Elephant Robotics ショップ：myCobot Bluetooth ゲームパッド](https://shop.elephantrobotics.com/ja/products/mycobot-gamepad)
- [openelab: myCobot Bluetooth Gamepad](https://openelab.com/products/mycobot-wireless-controller)
- [WisteriaHill：ラズパイ4B ＋ PS4 コントローラ（2021年、Raspberry Pi OS と pyPS4Controller の手順）](https://wisteriahill.sakura.ne.jp/CMS/WordPress/2021/11/11/raspberry-pi-4-b-ps4-controller/)
- [CNX Software: MechArm Pi 270](https://cnx-software.com/?p=99769)
- [BlueZ Issue #2380: Generic PS4 controller fails to reconnect](https://github.com/bluez/bluez/issues/2380)
- [Arch Linux Forums: Can't get input from DualShock 4](https://bbs.archlinux.org/viewtopic.php?id=262451)
- [RetroPie forum: Cannot connect PS4 Controller](https://retropie.org.uk/forum/post/10895)
- [Cycling '74 forum: gamepad object](https://cycling74.com/forums/gamepad-object-supported-controllers-and-non-cable-connection)
````


## 11/2 午後：gamepad_check.py で、ボタン16個と軸6本がすべて読めた

11/2 にゲームパッドが届いた。Mac の Bluetooth の設定で、SHARE を押したまま HOME を長押ししてつなぐと、「DUALSHOCK 4 Wireless Controller」と出た。Mac からは、ソニーの純正のコントローラーとして見えている。

【画像】LOG419_02_bluetooth_dualshock4.png Mac の Bluetooth の設定。自分のデバイスの一番上に DUALSHOCK 4 Wireless Controller が接続済みで並んでいる

アームにはつながず、値を表示するだけのスクリプト gamepad_check.py を Claude が書いた。ウィンドウの文字は、日本語のフォントがなくても化けないように英数字だけにしてある。

【画像】LOG419_03_gamepad_check_window.png gamepad_check.py の窓。PS4 Controller の6本の軸の値が並び、スティックの4本は +0.004、L2 と R2 の2本は −1.000 で左端まで緑の棒が伸びている

ボタンを1つずつ押し、スティックを端まで倒し、L2 と R2 を押し込んだ結果は次のとおり。

| 確かめたこと | 結果 |
|---|---|
| ボタン 0〜15 | 16個すべて、押した・離したが1回ずつ出た。押しっぱなしで繰り返す行がないので、TURBO 連射は入っていない |
| 左スティック（軸0・1） | −1.000〜+1.000 まで届き、離すと毎回 +0.004 に戻る |
| 右スティック（軸2・3） | 軸2 は −0.984〜+0.976、軸3 は −1.000〜+1.000。離すと毎回 +0.004 に戻る |
| L2・R2（軸4・5） | 離して −1.0、押し込んで +1.0。途中の値（−0.286、+0.404 など）も出る |
| 十字キー | ボタン11〜14として出る（hat ではない） |
| 値の更新の間隔 | スティックを回している間、出力の時刻はおよそ0.05秒（1秒に約20回）刻み |
| 気づいたこと | 左スティックを強く回している間に、左スティックの押し込み（ボタン7）が0.03〜0.05秒だけ入ることが3回あった |

ボタンの番号は、□ → × → ○ → △ の順に押して 2 → 0 → 1 → 3 と出たことで、0 = ×・1 = ○・2 = □・3 = △ と決まった。

### gamepad_check.py

```python
#!/usr/bin/env python3
"""
gamepad_check.py — ゲーム用コントローラーの入力を読んで表示するだけの確認スクリプト

ロボットには一切つながない。ファイルにも何も書き込まない。

使い方（Mac）:
    python3 -m pip install pygame
    python3 gamepad_check.py

見ること:
    1. ボタンを押す・離すと、番号と押した/離したが出るか
    2. スティックを倒すと、軸の値が -1.0〜+1.0 の間で動くか
    3. スティックを離したとき、値が 0 付近に戻るか（終了時の一覧で「最後の値」を見る）

終了: ウィンドウを閉じる、または ESC キー、またはターミナルで Ctrl+C
"""

import os
import sys
import time

# ウィンドウが手前にないときもコントローラーの入力を受け取る
os.environ.setdefault("SDL_JOYSTICK_ALLOW_BACKGROUND_EVENTS", "1")

import pygame  # noqa: E402

AXIS_PRINT_STEP = 0.05   # 軸の値がこれ以上変わったらターミナルに出す
WINDOW_SIZE = (560, 420)


def describe(js):
    return (f"[{js.get_instance_id()}] {js.get_name()}  "
            f"軸={js.get_numaxes()}  ボタン={js.get_numbuttons()}  "
            f"十字キー={js.get_numhats()}  GUID={js.get_guid()}")


def main():
    pygame.init()
    pygame.joystick.init()
    screen = pygame.display.set_mode(WINDOW_SIZE)
    pygame.display.set_caption("gamepad_check")
    # ウィンドウの文字は英数字だけにする（日本語フォントがなくても化けないように）
    font = pygame.font.SysFont("menlo,consolas,dejavusansmono", 16)
    clock = pygame.time.Clock()

    sticks = {}          # instance_id -> Joystick
    last_axis = {}       # (id, axis) -> 最後に表示した値
    axis_stats = {}      # (id, axis) -> [最小, 最大, 最後の値]
    t0 = time.monotonic()

    def ts():
        return f"{time.monotonic() - t0:8.3f}s"

    print("コントローラーを待っています（つないでいなければ、今つないでください）")

    running = True
    try:
        while running:
            for ev in pygame.event.get():
                if ev.type == pygame.QUIT:
                    running = False
                elif ev.type == pygame.KEYDOWN and ev.key == pygame.K_ESCAPE:
                    running = False

                elif ev.type == pygame.JOYDEVICEADDED:
                    js = pygame.joystick.Joystick(ev.device_index)
                    sticks[js.get_instance_id()] = js
                    print(f"{ts()}  接続: {describe(js)}")
                elif ev.type == pygame.JOYDEVICEREMOVED:
                    js = sticks.pop(ev.instance_id, None)
                    name = js.get_name() if js else "?"
                    print(f"{ts()}  切断: [{ev.instance_id}] {name}")

                elif ev.type == pygame.JOYBUTTONDOWN:
                    print(f"{ts()}  ボタン {ev.button:2d}  押した")
                elif ev.type == pygame.JOYBUTTONUP:
                    print(f"{ts()}  ボタン {ev.button:2d}  離した")

                elif ev.type == pygame.JOYHATMOTION:
                    print(f"{ts()}  十字キー {ev.hat}  {ev.value}")

                elif ev.type == pygame.JOYAXISMOTION:
                    key = (ev.instance_id, ev.axis)
                    v = ev.value
                    st = axis_stats.setdefault(key, [v, v, v])
                    st[0] = min(st[0], v)
                    st[1] = max(st[1], v)
                    st[2] = v
                    prev = last_axis.get(key)
                    if prev is None or abs(v - prev) >= AXIS_PRINT_STEP:
                        last_axis[key] = v
                        print(f"{ts()}  軸 {ev.axis}  {v:+.3f}")

            # ウィンドウに今の値を並べる
            screen.fill((20, 20, 24))
            y = 10
            if not sticks:
                screen.blit(font.render("no controller", True, (230, 180, 80)), (10, y))
            for js in sticks.values():
                screen.blit(font.render(js.get_name()[:50], True, (200, 220, 255)), (10, y))
                y += 24
                for a in range(js.get_numaxes()):
                    v = js.get_axis(a)
                    screen.blit(font.render(f"axis {a}: {v:+.3f}", True, (230, 230, 230)), (10, y))
                    # 値の棒（中央が0）
                    cx, w = 200, 150
                    pygame.draw.rect(screen, (70, 70, 80), (cx - w, y + 4, 2 * w, 10), 1)
                    pygame.draw.rect(screen, (90, 200, 120), (cx, y + 4, int(v * w), 10) if v >= 0
                                     else (cx + int(v * w), y + 4, int(-v * w), 10))
                    y += 20
                pressed = [str(b) for b in range(js.get_numbuttons()) if js.get_button(b)]
                screen.blit(font.render("buttons: " + (" ".join(pressed) or "-"),
                                        True, (230, 230, 230)), (10, y))
                y += 20
                for h in range(js.get_numhats()):
                    screen.blit(font.render(f"hat {h}: {js.get_hat(h)}", True, (230, 230, 230)), (10, y))
                    y += 20
                y += 10
            pygame.display.flip()
            clock.tick(60)
    except KeyboardInterrupt:
        pass
    finally:
        if axis_stats:
            print("\n--- 軸の一覧（スティックを離した状態で終えると、最後の値＝戻り位置） ---")
            for (iid, axis), (lo, hi, last) in sorted(axis_stats.items()):
                print(f"[{iid}] 軸 {axis}:  最小 {lo:+.3f}  最大 {hi:+.3f}  最後の値 {last:+.3f}")
        pygame.quit()


if __name__ == "__main__":
    sys.exit(main())
```

### gamepad_check.py を動かしたターミナルの出力（1〜613行目）

pygame を入れたところから、ボタン・スティック・L2・R2 を一通り押し、番号を確かめ、つなぎ直しまで。

```text
Last login: Sun Nov  1 18:58:26 on ttys001
(base) shimazueiichi@shimadzueiichinoMacBook-Pro ~ %    cd ~/r2r2r-scripts/tools 
   python3 -m pip install pygame
   python3 gamepad_check.py
Collecting pygame
  Downloading pygame-2.6.1-cp312-cp312-macosx_11_0_arm64.whl.metadata (12 kB)
Downloading pygame-2.6.1-cp312-cp312-macosx_11_0_arm64.whl (12.4 MB)
   ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 12.4/12.4 MB 15.2 MB/s  0:00:00
Installing collected packages: pygame
Successfully installed pygame-2.6.1
/opt/homebrew/Caskroom/miniforge/base/lib/python3.12/site-packages/pygame/pkgdata.py:25: UserWarning: pkg_resources is deprecated as an API. See https://setuptools.pypa.io/en/latest/pkg_resources.html. The pkg_resources package is slated for removal as early as 2025-11-30. Refrain from using this package or pin to Setuptools<81.
  from pkg_resources import resource_stream, resource_exists
pygame 2.6.1 (SDL 2.28.4, Python 3.12.12)
Hello from the pygame community. https://www.pygame.org/contribute.html
コントローラーを待っています（つないでいなければ、今つないでください）
   0.059s  接続: [0] PS4 Controller  軸=6  ボタン=16  十字キー=0  GUID=03008fe54c050000cc09000000016800
  58.133s  ボタン  2  押した
  58.402s  ボタン  2  離した
  58.964s  ボタン  3  押した
  59.185s  ボタン  3  離した
  59.625s  ボタン  0  押した
  59.863s  ボタン  0  離した
  60.163s  ボタン  1  押した
  60.398s  ボタン  1  離した
  64.875s  ボタン  4  押した
  65.111s  ボタン  4  離した
  65.755s  ボタン  5  押した
  66.174s  ボタン  5  離した
  66.612s  ボタン  6  押した
  66.870s  ボタン  6  離した
  69.982s  軸 1  +0.004
  69.982s  軸 1  -0.326
  70.027s  軸 1  -1.000
  70.129s  軸 0  +0.004
  70.569s  軸 0  +0.067
  70.569s  軸 1  -0.796
  70.622s  軸 0  +0.004
  70.622s  軸 1  +0.004
  70.976s  ボタン  7  押した
  71.529s  ボタン  7  離した
  72.320s  ボタン  8  押した
  72.321s  軸 2  +0.004
  72.321s  軸 2  +0.137
  72.321s  軸 3  +0.004
  72.321s  軸 3  -0.122
  72.372s  軸 2  +0.686
  72.372s  軸 3  -0.733
  72.475s  軸 2  +0.004
  72.475s  軸 3  +0.349
  72.526s  軸 2  -0.584
  72.526s  軸 3  +0.820
  72.577s  軸 2  -0.647
  72.577s  軸 3  +0.765
  72.764s  軸 2  -0.482
  72.764s  軸 3  +0.882
  72.829s  軸 2  -0.278
  72.829s  軸 3  +0.725
  72.896s  軸 2  -0.145
  72.896s  軸 3  +0.537
  72.997s  軸 2  -0.012
  72.997s  軸 3  +0.271
  73.032s  軸 3  +0.082
  73.083s  軸 3  +0.012
  73.168s  ボタン  8  離した
  73.168s  軸 3  +0.090
  73.217s  軸 3  +0.004
  83.634s  ボタン 13  押した
  83.935s  ボタン 13  離した
  84.221s  ボタン 14  押した
  84.540s  ボタン 14  離した
  84.680s  ボタン 12  押した
  84.915s  ボタン 12  離した
  84.915s  ボタン 14  押した
  84.981s  ボタン 14  離した
  85.282s  ボタン 11  押した
  85.630s  ボタン 11  離した
  86.099s  ボタン 11  押した
  86.368s  ボタン 11  離した
  86.620s  ボタン 13  押した
  86.819s  ボタン 13  離した
  87.072s  ボタン 14  押した
  87.274s  ボタン 14  離した
  87.475s  ボタン 12  押した
  87.756s  ボタン 12  離した
  89.080s  ボタン 15  押した
  89.366s  ボタン 15  離した
  89.722s  ボタン 15  押した
  90.293s  軸 0  -0.075
  90.293s  軸 1  +0.114
  90.327s  軸 0  -0.129
  90.377s  軸 0  -0.035
  90.378s  軸 1  +0.012
  91.341s  ボタン 15  離した
  93.321s  ボタン  9  押した
  93.856s  ボタン  9  離した
  94.223s  ボタン 10  押した
  94.723s  ボタン 10  離した
  96.063s  軸 4  -1.000
  96.064s  軸 4  -0.286
  96.112s  軸 4  +1.000
  96.466s  軸 4  +0.404
  96.534s  軸 4  -1.000
  96.885s  軸 5  -1.000
  96.885s  軸 5  +1.000
  97.238s  軸 5  -1.000
 117.682s  軸 4  -0.114
 117.716s  軸 4  +1.000
 118.001s  軸 4  -1.000
 118.220s  軸 5  +1.000
 118.422s  軸 5  -1.000
 118.930s  ボタン  0  押した
 119.148s  ボタン  0  離した
 119.384s  ボタン  2  押した
 119.570s  ボタン  2  離した
 119.769s  ボタン  3  押した
 119.969s  ボタン  3  離した
 120.188s  ボタン  1  押した
 120.438s  ボタン  1  離した
 121.330s  ボタン  9  押した
 121.634s  ボタン  9  離した
 121.887s  ボタン 10  押した
 122.174s  ボタン 10  離した
 122.728s  軸 2  +0.976
 122.728s  軸 3  -0.247
 122.779s  軸 3  -0.122
 122.829s  軸 3  -0.051
 122.981s  軸 3  +0.114
 123.013s  軸 2  +0.600
 123.013s  軸 3  +0.804
 123.081s  軸 2  +0.004
 123.081s  軸 3  +1.000
 123.130s  軸 2  -0.796
 123.130s  軸 3  +0.616
 123.181s  軸 2  -0.961
 123.181s  軸 3  +0.302
 123.230s  軸 3  -0.012
 123.264s  軸 3  -0.255
 123.331s  軸 2  -0.898
 123.331s  軸 3  -0.443
 123.380s  軸 2  -0.647
 123.380s  軸 3  -0.765
 123.432s  軸 2  -0.561
 123.432s  軸 3  -0.827
 123.468s  軸 2  -0.420
 123.468s  軸 3  -0.914
 123.535s  軸 2  -0.263
 123.535s  軸 3  -0.969
 123.569s  軸 2  -0.153
 123.619s  軸 2  -0.067
 123.820s  軸 2  -0.216
 123.871s  軸 2  -0.584
 123.871s  軸 3  -0.820
 123.922s  軸 2  -0.882
 123.922s  軸 3  -0.482
 123.973s  軸 2  -0.984
 123.973s  軸 3  -0.192
 124.024s  軸 3  +0.090
 124.073s  軸 2  -0.914
 124.073s  軸 3  +0.412
 124.123s  軸 2  -0.812
 124.123s  軸 3  +0.592
 124.173s  軸 2  -0.639
 124.173s  軸 3  +0.773
 124.240s  軸 2  -0.522
 124.240s  軸 3  +0.859
 124.273s  軸 2  -0.122
 124.273s  軸 3  +1.000
 124.322s  軸 2  +0.333
 124.322s  軸 3  +0.945
 124.373s  軸 2  +0.851
 124.373s  軸 3  +0.537
 124.424s  軸 2  +0.961
 124.424s  軸 3  +0.302
 124.476s  軸 3  +0.129
 124.526s  軸 3  +0.004
 124.577s  軸 3  -0.106
 124.627s  軸 2  +0.004
 124.627s  軸 3  +0.004
 124.826s  軸 1  -0.137
 124.876s  軸 0  -0.208
 124.876s  軸 1  -0.984
 124.926s  軸 0  -0.153
 124.975s  軸 0  +0.373
 125.031s  軸 0  +0.631
 125.031s  軸 1  -0.780
 125.064s  軸 0  +0.780
 125.065s  軸 1  -0.631
 125.116s  軸 0  +0.835
 125.116s  軸 1  -0.553
 125.182s  軸 0  +0.953
 125.183s  軸 1  -0.326
 125.216s  軸 1  -0.043
 125.283s  軸 1  +0.184
 125.333s  軸 0  +0.843
 125.333s  軸 1  +0.545
 125.366s  軸 0  +0.631
 125.366s  軸 1  +0.780
 125.416s  ボタン  7  押した
 125.417s  軸 0  +0.459
 125.417s  軸 1  +0.890
 125.481s  軸 0  +0.255
 125.481s  軸 1  +0.969
 125.531s  軸 0  -0.129
 125.564s  軸 0  -0.498
 125.564s  軸 1  +0.875
 125.635s  軸 0  -0.812
 125.635s  軸 1  +0.592
 125.667s  軸 0  -0.898
 125.667s  軸 1  +0.451
 125.716s  軸 0  -0.977
 125.716s  軸 1  +0.224
 125.784s  軸 1  -0.035
 125.816s  軸 0  -0.906
 125.816s  軸 1  -0.435
 125.866s  軸 0  -0.498
 125.867s  軸 1  -0.875
 125.933s  軸 0  -0.216
 125.933s  軸 1  -0.984
 125.968s  軸 0  -0.075
 126.017s  軸 0  +0.004
 126.084s  軸 0  +0.388
 126.084s  軸 1  -0.922
 126.117s  軸 0  +0.843
 126.117s  軸 1  -0.545
 126.167s  軸 0  +0.992
 126.167s  軸 1  -0.153
 126.234s  軸 1  +0.004
 126.268s  軸 1  +0.161
 126.337s  軸 0  +0.914
 126.337s  軸 1  +0.412
 126.387s  軸 0  +0.827
 126.387s  軸 1  +0.561
 126.420s  軸 0  +0.725
 126.420s  軸 1  +0.694
 126.471s  軸 0  +0.655
 126.471s  軸 1  +0.757
 126.523s  ボタン  7  離した
 126.523s  軸 0  +0.592
 126.523s  軸 1  +0.812
 126.571s  軸 0  +0.451
 126.572s  軸 1  +0.898
 126.623s  軸 0  +0.004
 126.623s  軸 1  +0.004
 127.125s  軸 0  -0.945
 127.125s  軸 1  -0.333
 127.176s  軸 1  -0.176
 127.228s  軸 0  -1.000
 127.228s  軸 1  -0.035
 127.515s  軸 0  +0.004
 127.581s  軸 0  +1.000
 127.781s  軸 1  +0.090
 127.814s  ボタン  7  押した
 127.814s  軸 0  +0.922
 127.814s  軸 1  +0.161
 127.880s  軸 0  +0.655
 127.880s  軸 1  +0.004
 127.929s  軸 0  +0.333
 127.963s  軸 0  -0.600
 127.963s  軸 1  -0.224
 128.029s  軸 0  -1.000
 128.029s  軸 1  -0.035
 128.180s  ボタン  7  離した
 128.264s  軸 1  -0.216
 128.331s  軸 0  -0.145
 128.331s  軸 1  -0.004
 128.381s  軸 0  +0.004
 128.565s  軸 3  -0.278
 128.616s  軸 3  -1.000
 128.834s  軸 2  -0.059
 128.902s  軸 2  -0.192
 128.902s  軸 3  +0.051
 128.936s  軸 0  -0.137
 128.936s  軸 1  -0.992
 128.936s  軸 2  -0.506
 128.936s  軸 3  +0.867
 128.985s  軸 0  -0.067
 128.986s  軸 2  -0.365
 128.986s  軸 3  +0.937
 129.086s  軸 0  -0.012
 129.168s  ボタン  7  押した
 129.168s  軸 0  -0.075
 129.168s  軸 1  -0.820
 129.168s  軸 2  -0.592
 129.168s  軸 3  +0.631
 129.219s  ボタン  7  離した
 129.219s  軸 0  -0.012
 129.219s  軸 1  +0.843
 129.219s  軸 2  +0.027
 129.219s  軸 3  -1.000
 129.269s  軸 0  -0.098
 129.269s  軸 1  +1.000
 129.518s  軸 3  +0.004
 129.552s  軸 0  +0.004
 129.552s  軸 1  +0.004
 130.274s  軸 2  +0.976
 130.274s  軸 3  -0.224
 130.325s  軸 3  -0.035
 130.698s  軸 2  +0.004
 130.983s  軸 2  +0.953
 130.983s  軸 3  -0.302
 131.032s  軸 3  -0.129
 131.065s  軸 3  -0.075
 131.217s  軸 2  +0.694
 131.269s  軸 2  +0.004
 131.269s  軸 3  +0.004
 131.481s  軸 2  -0.608
 131.530s  軸 2  -0.984
 131.530s  軸 3  -0.192
 131.697s  軸 3  -0.247
 131.897s  軸 2  +0.004
 131.898s  軸 3  +0.004
 132.133s  軸 1  -1.000
 132.372s  軸 0  +0.098
 132.422s  軸 0  +0.608
 132.422s  軸 1  -0.420
 132.473s  軸 0  +0.984
 132.473s  軸 1  +0.184
 132.660s  軸 1  +0.294
 132.710s  軸 0  +0.875
 132.710s  軸 1  +0.490
 132.763s  軸 0  +0.608
 132.763s  軸 1  +0.796
 132.797s  軸 0  +0.161
 132.797s  軸 1  +0.992
 132.848s  ボタン  7  押した
 132.849s  軸 0  -0.318
 132.880s  ボタン  7  離した
 132.880s  軸 0  -0.757
 132.880s  軸 1  +0.663
 132.931s  軸 0  -0.890
 132.931s  軸 1  +0.467
 132.981s  軸 0  -0.961
 132.981s  軸 1  +0.286
 133.014s  軸 1  -0.357
 133.066s  軸 0  -0.576
 133.066s  軸 1  -0.820
 133.168s  軸 0  -0.765
 133.168s  軸 1  -0.608
 133.219s  軸 0  -0.310
 133.219s  軸 1  -0.145
 133.270s  軸 0  +0.004
 133.270s  軸 1  +0.004
 133.471s  軸 0  -0.749
 133.471s  軸 1  -0.161
 133.520s  軸 0  -1.000
 133.572s  軸 1  -0.059
 133.808s  軸 1  -0.137
 133.842s  軸 0  +0.004
 133.842s  軸 1  +0.004
 133.995s  軸 0  +0.192
 134.029s  軸 0  +1.000
 134.029s  軸 1  -0.051
 134.215s  軸 0  +0.749
 134.266s  軸 0  +0.004
 134.266s  軸 1  +0.004
 188.169s  ボタン  2  押した
 188.421s  ボタン  2  離した
 188.773s  ボタン  0  押した
 188.975s  ボタン  0  離した
 189.291s  ボタン  1  押した
 189.441s  ボタン  1  離した
 189.845s  ボタン  3  押した
 189.977s  ボタン  3  離した
 494.313s  ボタン 13  押した
 494.551s  ボタン 13  離した
 494.984s  ボタン 14  押した
 495.017s  ボタン 12  押した
 495.136s  ボタン 12  離した
 495.168s  ボタン 14  離した
 495.976s  ボタン 11  押した
 496.268s  ボタン 11  離した
 497.078s  ボタン 12  押した
 497.332s  ボタン 12  離した
 498.435s  ボタン 11  押した
 498.653s  ボタン 11  離した
 499.227s  ボタン 12  押した
 499.396s  ボタン 12  離した
 511.089s  ボタン  9  押した
 511.531s  ボタン  9  離した
 516.885s  ボタン  9  押した
 519.276s  ボタン  9  離した
 519.647s  ボタン  9  押した
 520.134s  ボタン  9  離した
 523.976s  ボタン  1  押した
 524.227s  ボタン  1  離した
 524.580s  ボタン  0  押した
 524.864s  ボタン  0  離した
 525.287s  ボタン  1  押した
 525.474s  ボタン  1  離した
 525.860s  ボタン  0  押した
 526.047s  ボタン  0  離した
 526.481s  ボタン  1  押した
 526.633s  ボタン  1  離した
 526.915s  ボタン  0  押した
 527.097s  ボタン  0  離した
 528.667s  ボタン  2  押した
 529.015s  ボタン  2  離した
 536.089s  ボタン  2  押した
 536.361s  ボタン  2  離した
 540.284s  ボタン  3  押した
 540.435s  ボタン 10  押した
 540.587s  ボタン 10  離した
 541.830s  ボタン  3  離した
 546.669s  ボタン 10  押した
 546.969s  ボタン 10  離した
 547.973s  軸 5  +1.000
 548.244s  軸 5  +0.608
 548.296s  軸 5  -1.000
 548.530s  軸 5  +1.000
 548.733s  軸 5  +0.702
 548.766s  軸 5  -1.000
 550.520s  軸 5  +1.000
 552.194s  軸 5  -0.655
 552.245s  軸 5  -1.000
 554.259s  軸 5  +0.906
 554.309s  軸 5  +1.000
 554.445s  軸 5  -1.000
 554.616s  軸 5  +1.000
 554.732s  軸 5  +0.255
 554.766s  軸 5  -1.000
 555.201s  ボタン 10  押した
 555.424s  ボタン 10  離した
 555.522s  ボタン 10  押した
 555.742s  ボタン 10  離した
 555.927s  ボタン 10  押した
 556.076s  ボタン 10  離した
 568.569s  軸 4  +1.000
 568.569s  軸 5  +0.074
 568.636s  軸 5  +1.000
 569.119s  軸 5  -1.000
 569.168s  軸 4  -1.000
 569.538s  軸 4  +1.000
 569.538s  軸 5  +1.000
 569.722s  軸 5  -1.000
 569.773s  軸 4  -1.000
 593.911s  軸 4  -0.161
 594.014s  軸 4  +1.000
 594.014s  軸 5  +1.000
 594.370s  軸 4  -1.000
 594.370s  軸 5  -1.000
 629.088s  軸 4  +1.000
 629.088s  軸 5  +1.000
 629.422s  軸 4  -0.647
 629.422s  軸 5  -0.114
 629.472s  軸 4  -1.000
 629.472s  軸 5  -1.000
 629.808s  軸 4  +0.286
 629.842s  軸 4  +1.000
 629.842s  軸 5  +0.310
 629.892s  軸 5  +1.000
 630.132s  軸 5  +0.631
 630.183s  軸 4  -1.000
 630.184s  軸 5  -1.000
 630.522s  軸 4  +0.435
 630.573s  軸 4  +1.000
 630.573s  軸 5  +1.000
 630.946s  軸 4  -0.608
 630.946s  軸 5  -0.451
 630.980s  軸 4  -1.000
 630.980s  軸 5  -1.000
 631.331s  軸 4  +1.000
 631.331s  軸 5  +1.000
 631.585s  軸 5  -1.000
 631.636s  軸 4  -1.000
 631.953s  軸 4  -0.490
 631.954s  軸 5  +0.545
 631.986s  軸 4  +1.000
 631.986s  軸 5  +1.000
 632.123s  軸 4  +0.443
 632.123s  軸 5  -1.000
 632.173s  軸 4  -1.000
 632.377s  軸 4  +1.000
 632.377s  軸 5  +0.537
 632.428s  軸 5  +1.000
 632.580s  軸 5  -1.000
 632.613s  軸 4  -1.000
 795.942s  ボタン 10  押した
 796.127s  ボタン 10  離した
 796.362s  ボタン 10  押した
 796.584s  ボタン 10  離した
 796.820s  ボタン 10  押した
 796.971s  ボタン 10  離した
 917.588s  ボタン 13  押した
 917.877s  ボタン 13  離した
 918.534s  ボタン 14  押した
 918.788s  ボタン 14  離した
 919.444s  ボタン 11  押した
 919.777s  ボタン 11  離した
 920.164s  ボタン 12  押した
 920.448s  ボタン 12  離した
 920.918s  ボタン 11  押した
 921.169s  ボタン 11  離した
 921.441s  ボタン 12  押した
 921.627s  ボタン 12  離した
1139.347s  ボタン  9  押した
1140.057s  ボタン 14  押した
1140.595s  ボタン 11  押した
1140.679s  ボタン 11  離した
1140.679s  ボタン 14  離した
1140.928s  ボタン 13  押した
1142.494s  ボタン 13  離した
1142.830s  ボタン 14  押した
1143.031s  ボタン 14  離した
1143.284s  ボタン  9  離した
1143.637s  ボタン 10  押した
1144.075s  ボタン 13  押した
1144.281s  ボタン 13  離した
1144.434s  ボタン 14  押した
1144.653s  ボタン 11  押した
1144.653s  ボタン 14  離した
1144.785s  ボタン 13  押した
1145.152s  ボタン 11  離した
1145.152s  ボタン 13  離した
1145.186s  ボタン 14  押した
1145.383s  ボタン 11  押した
1145.434s  ボタン 14  離した
1145.550s  ボタン 13  押した
1145.838s  ボタン 11  離した
1145.838s  ボタン 13  離した
1145.888s  ボタン 12  押した
1146.138s  ボタン 12  離した
1146.138s  ボタン 11  押した
1146.376s  ボタン 10  離した
1146.442s  ボタン 11  離した
1191.280s  ボタン 10  押した
1191.722s  ボタン 14  押した
1191.855s  ボタン 11  押した
1191.905s  ボタン 14  離した
1191.956s  ボタン 11  離した
1191.956s  ボタン 13  押した
1192.124s  ボタン 14  押した
1192.124s  ボタン 13  離した
1192.324s  ボタン 11  押した
1192.390s  ボタン 14  離した
1192.474s  ボタン 13  押した
1192.576s  ボタン 13  離した
1192.861s  ボタン 14  押した
1192.911s  ボタン 12  押した
1192.911s  ボタン 11  離した
1192.911s  ボタン 14  離した
1193.178s  ボタン 14  押した
1193.228s  ボタン 12  離した
1193.278s  ボタン 11  押した
1193.432s  ボタン 14  離した
1193.584s  ボタン 11  離した
1193.584s  ボタン 14  押した
1193.633s  ボタン 12  押した
1193.633s  ボタン 14  離した
1193.915s  ボタン 12  離した
1194.046s  ボタン 10  離した
1194.330s  軸 2  -0.467
1194.330s  軸 3  -0.396
1194.381s  軸 2  -0.749
1194.381s  軸 3  -0.671
1194.415s  軸 2  +0.004
1194.415s  軸 3  +0.004
1194.531s  ボタン  5  押した
1195.922s  ボタン  5  離した
1196.547s  軸 4  +0.388
1196.547s  軸 5  +1.000
1196.580s  軸 4  +1.000
1197.180s  軸 5  +0.051
1197.229s  軸 4  -1.000
1197.230s  軸 5  -1.000
1207.772s  ボタン  0  押した
1207.923s  ボタン  0  離した
1208.234s  ボタン  1  押した
1208.489s  ボタン  1  離した
1208.777s  ボタン  0  押した
1208.928s  ボタン  0  離した
1209.210s  ボタン  1  押した
1209.447s  ボタン  1  離した
1211.082s  ボタン 12  押した
1211.252s  ボタン 12  離した
1211.600s  ボタン 11  押した
1211.683s  ボタン 13  押した
1211.785s  ボタン 13  離した
1211.818s  ボタン 11  離した
1212.425s  ボタン 14  押した
1212.659s  ボタン 14  離した
1212.895s  ボタン 13  押した
1213.079s  ボタン 13  離した
1264.171s  ボタン  3  押した
1264.376s  ボタン  3  離した
1264.581s  ボタン  3  押した
1264.730s  ボタン  3  離した
1264.932s  ボタン  3  押した
1265.101s  ボタン  3  離した
1265.235s  ボタン  3  押した
1265.436s  ボタン  3  離した
1266.687s  ボタン  3  押した
1266.821s  ボタン  3  離した
1266.989s  ボタン  3  押した
1267.173s  ボタン  3  離した
1267.374s  ボタン  3  押した
1267.507s  ボタン  3  離した
1287.505s  ボタン  3  押した
1287.589s  ボタン  3  離した
1287.791s  ボタン  3  押した
1287.874s  ボタン  3  離した
1288.027s  ボタン  3  押した
1288.210s  ボタン  3  離した
1288.342s  ボタン  3  押した
1288.428s  ボタン  3  離した
2208.380s  切断: [0] PS4 Controller
^C
--- 軸の一覧（スティックを離した状態で終えると、最後の値＝戻り位置） ---
[0] 軸 0:  最小 -1.000  最大 +1.000  最後の値 +0.004
[0] 軸 1:  最小 -1.000  最大 +1.000  最後の値 +0.004
[0] 軸 2:  最小 -1.000  最大 +1.000  最後の値 +0.004
[0] 軸 3:  最小 -1.000  最大 +1.000  最後の値 +0.004
[0] 軸 4:  最小 -1.000  最大 +1.000  最後の値 -1.000
[0] 軸 5:  最小 -1.000  最大 +1.000  最後の値 -1.000
```

### KIOXIA の diag/gamepad_check_20261102/README.md

```markdown
# ゲームパッドの読み取り確認（2026-11-02）

- 目的：買った互換コントローラーの入力を Mac で読めるか、ロボットにつなぐ前に確かめる（ロボットにはつないでいない）
- 調査メモ：iCloud の `research/research_gamepad_teleop_mecharm_20261101.md`
- 中身
  - `terminal_output.txt`：ターミナルの出力そのまま（エイイチが貼ったもの。ESC での終了と最後の「軸の一覧」は含まない）
  - `gamepad_check.py`：使ったスクリプトの写し（原本は `~/r2r2r-scripts/tools/gamepad_check.py`）

## 条件

| 項目 | 値 |
|---|---|
| コントローラー | Yommor P4 コントローラー（Amazon、3,198円、互換品、USB-C） |
| つなぎ方 | Bluetooth（Mac の設定では「DUALSHOCK 4 Wireless Controller」と表示） |
| Mac | MacBook Pro（Apple シリコン） |
| Python / pygame / SDL | 3.12.12（miniforge）/ 2.6.1 / 2.28.4 |
| pygame が見た名前 | PS4 Controller、軸6・ボタン16・十字キー（hat）0 |
| GUID | 03008fe54c050000cc09000000016800（Sony 054C・DUALSHOCK 4 2代目 09CC と同じ番号） |

## 結果

| 確認 | 結果 |
|---|---|
| ボタン 0〜15 | 16個すべて、押した・離したが1回ずつ出た |
| TURBO 連射 | 入っていない（押しっぱなしで繰り返す行がない）。SHARE（4）を1回押しても変化なし |
| 左スティック（軸0・1） | −1.000〜+1.000 まで届く。離すと毎回 +0.004 に戻る |
| 右スティック（軸2・3） | 軸2 −0.984〜+0.976、軸3 −1.000〜+1.000。離すと毎回 +0.004 に戻る |
| L2・R2（軸4・5） | 離して −1.0、押し込んで +1.0。途中の値も出る（−0.286、+0.404 など） |
| 十字キー | ボタン 11〜14 として出る（hat ではない） |
| 値の更新の間隔 | スティックを回している間、出力の時刻はおよそ 0.05 秒（約20回/秒）刻み。この間隔がコントローラー側か読み取り側かは未確認 |

## 気づいたこと

- 左スティックを強く回している間に、左スティックの押し込み（ボタン7）が 0.03〜0.05 秒だけ入ることが3回あった（129.168s・132.848s ほか）。スティックの押し込み（7・8）には、つかむ・離すなどの大事な操作を割り当てない
- タッチパッド（15）を押している間に左スティックが少し動いた（軸0 −0.129）。親指が触れたと見られる。その後の操作では毎回 +0.004 に戻っている

## ボタン番号（SDL の PS4 の並び。押した場所との対応はエイイチの確認待ち）

| 番号 | ボタン |
|---|---|
| 0 / 1 / 2 / 3 | × / ○ / □ / △ |
| 4 / 5 / 6 | SHARE / HOME / OPTIONS |
| 7 / 8 | 左スティック押し込み / 右スティック押し込み |
| 9 / 10 | L1 / R1 |
| 11 / 12 / 13 / 14 | 十字キー 上 / 下 / 左 / 右 |
| 15 | タッチパッド押し込み |

| 軸 | 中身 |
|---|---|
| 0 / 1 | 左スティック 横 / 縦（上に倒すと −） |
| 2 / 3 | 右スティック 横 / 縦（上に倒すと −） |
| 4 / 5 | L2 / R2（離して −1.0） |
```

### KIOXIA の diag/gamepad_check_20261102/button_map_confirmed.md

````markdown
# ボタン番号の確定（2026-11-02 15:48）

README.md の「ボタン番号」の表のうち、エイイチの確認待ちだった 0〜3 を確定した。README.md は書き換えず、このファイルを足す。

- 確かめ方：エイイチが 左（□）→ 下（×）→ 右（○）→ 上（△）の順に押し、ターミナルに 2 → 0 → 1 → 3 の順で出た
- 確定：0 = ×、1 = ○、2 = □、3 = △（README.md の表のとおり）

確認したときのターミナルの出力（そのまま）：

```
 188.169s  ボタン  2  押した
 188.421s  ボタン  2  離した
 188.773s  ボタン  0  押した
 188.975s  ボタン  0  離した
 189.291s  ボタン  1  押した
 189.441s  ボタン  1  離した
 189.845s  ボタン  3  押した
 189.977s  ボタン  3  離した
```
````


## 11/2 午後：操作は、押した分だけそのまま動く1つの形にしぼった

直したい動きは3つだった。

> 今のところあるのはPICK の位置合わせ（アームの左右）と姿勢の微調整（アームの上げ下げ）
> 爪の握り直しとかが多いかな
> HOME姿勢に戻すとか、停止ボタンも必要

Claude の最初の案は、上空の姿勢（APPROACH_PICK）と床ぎりぎりの姿勢（PICK）の間を1/10ずつ行き来する上げ下げや、「開く → 2秒待つ → 閉じる」の握り直しボタン、関節を選んで動かす別のモードを持つ形だった。話しながら、次のように変わった。

| 最初の案 | 決まった形 | 決めた理由 |
|---|---|---|
| 停止は R1 | L2 と R2 の両方を半分以上押し込む | 停止は片方だと間違って押しやすい。Claude は「片方でも止まるほうが、とっさのときに確実」と勧めたが、両方にした |
| 上げ下げは2つの姿勢の間を行き来 | 選んだ関節をそのまま動かす | 「キーパッドの動きがそのまま出ればいい」。動きの中身が見えない形は使いにくい |
| 位置合わせのモードと関節のモードの2つ | モードは1つ。△ で上下の関節を切り替える | 「迷いそうだけど」。いつ押しても同じ動きになる |
| 握り直しボタン（開く → 待つ → 閉じる） | なし。× と ○ を自分で押す | 「オートの要素は無くしたい」 |
| HOME は △ の長押し | HOME ボタンの1秒長押し | 「HOMEボタンでいいんじゃないの？」。長押しは、6つの関節がまとめて大きく動くので、触れただけで動かないように残した |
| 2° の刻みは L1 を押しながら | R1 を押しながら | 「十字キーを操作しながらL1を押しずらいから、R1にして」 |

- ねらい：AI が失敗しかけた数秒のうちに、迷わずに直せること
- 変わったこと：押すボタンと動く関節が、いつも同じ1対1になった。自動で動く要素（握り直し・上げ下げの道筋・失敗したときの自動の HOME）はなくなった
- 見込める効果：直した操作が「どの関節を何度」の形で残るので、記録をそのまま並べて比べられる。直すのにかかる時間が縮むかは、実機で測る

### 決まった操作と守り

| ボタン | 動き |
|---|---|
| 十字キー 左 / 右 | j0（根元の回転）を0.5°ずつ。どの関節を選んでいても、いつも j0 |
| 十字キー 上 / 下 | 選んでいる関節を0.5°ずつ。上 = −、下 = ＋。始めは j1（肩）。PICK の上空から床へ下りるとき j1・j2・j4 はどれも角度が増えるので、下を押すと爪が下がる向きにそろう |
| △ | 上下で動かす関節を切り替える（j1 → j2 → j3 → j4 → j5 → j1） |
| R1 を押しながら | 1回2°ずつ |
| × / ○ | 爪を閉じる / 開く |
| HOME を1秒長押し | HOME の姿勢に戻る |
| L2 と R2 を両方半分以上 | 停止。OPTIONS で再開 |

守りは数字で決めた。j1 は 90.87°（PICK の高さ。arm_kinematics.md の PICK_J1_FIXED）より下げない。j0 は基準の姿勢から ±15°、j1〜j5 は ±10° まで。j4 は J4_SAFE（−90〜+60°）の中だけ。コントローラーの接続が切れたら停止にする。既存のコードには、j1 の安全の上限として 88.0° という値（fk_lookup.J1_SAFE_MAX）もあるが、それだと PICK の高さまで下ろせないので、ここでは 90.87° にした。

j1 の止めは、PICK より下を指令しないだけで、床に当たらないことまでは保証しない。PICK の姿勢そのものが爪先が床ぎりぎりで、j2（ひじ）や j4（手首）を下げる方向に動かすと、j1 の止めとは関係なく爪先が床に近づく。最後の確かめは人の目と、L2+R2 の停止になる。


## 11/2 16:09〜17:15：依頼68で、Claude Code が gamepad_teleop.py を作った

決まった操作を依頼文にして、依頼箱（iCloud の 依頼箱/新規/）に置いた。Mac の見張り係が16:09に Claude Code へ渡し、17:15に報告が戻った。関節を動かすコードなので、codex のレビューを3回通している。

| codex のレビュー | 直すべき指摘 | 中身 |
|---|---|---|
| 1回目 | 7件 | 停止と送信の取り合い、範囲の外の姿勢から大きく動く、古い目標から動く、HOME の長押しの持ち越し、同じコマでボタンを停止より先に処理する、読み取りの失敗の扱い、記録に書けなくなったあとの再開 |
| 2回目 | 2件 | 送る前の待ちが10秒で切れたあとに送ってしまう穴、停止を押すと画面が止まる |
| 3回目 | 0件 | 勧めるだけの2件（終わるときに送ると決めていたかを残す・終わり方のテスト）も直した |

codex のレビューの全文は、Mac の一時フォルダ（/tmp/req68/）に書かれていて、この記録には残っていない。上の表は Claude Code の作業の記録（logs/20261102_1730.md）から写した。

**L2 と R2 で止めても、もう送った1回分は最後まで動く。** 調べると、Raspberry Pi の上で動いている指令の受け口（mecharm_daemon.py）に、動いている途中で止める命令がなかった。ふだんの1回の動きは最大2°なので困るのは HOME のような大きい動きだけで、止める命令を足すかは、実機で使ってから決めることにした。

### 依頼文 20261102_68_ゲームパッドでアームを手で動かすスクリプトを作る.md

````markdown
---
作業フォルダ: /Users/shimazueiichi/r2r2r-scripts
上限時間: 180
---
# 依頼68：ゲームパッドで mechArm を手で動かすスクリプト（tools/gamepad_teleop.py）を作る。実機には流さない（2026-11-02）

## ねらい

PICK で爪の位置がずれたときに、エイイチがゲームパッドで左右・上げ下げ・爪の開閉を直す。押した分だけそのまま動く単純な作りにする。**自動で動く要素は入れない**（自動の握り直し・失敗時の自動 HOME などは作らない）。押した操作と、送った角度・届いた角度をすべて記録し、あとで「人が直した記録」として使えるようにする。

## 前提（確かめ済み）

- コントローラー：Yommor の互換品。Mac に Bluetooth でつなぐと pygame では `PS4 Controller`（軸6・ボタン16・hat 0）として見える
- 読み取りの確認は `tools/gamepad_check.py` で済み。記録は `/Volumes/KIOXIA/r2r2r-data/diag/gamepad_check_20261102/`（README.md と button_map_confirmed.md にボタン番号の表がある）
- ボタン番号：0=× 1=○ 2=□ 3=△ 4=SHARE 5=HOME 6=OPTIONS 7=L3 8=R3 9=L1 10=R1 11=十字上 12=十字下 13=十字左 14=十字右 15=タッチパッド押し込み
- 軸：0/1 左スティック、2/3 右スティック、4=L2、5=R2（L2・R2 は離して −1.0、押し込んで +1.0）
- 左スティックを強く回すと L3（7）が一瞬入ることがある。スティックと L3・R3 は使わない

## 操作（エイイチが決めた。**変えない**）

| ボタン | 動き |
|---|---|
| 十字キー 左 / 右（13 / 14） | j0 を 0.5°ずつ。どの関節を選んでいても、いつも j0 |
| 十字キー 上 / 下（11 / 12） | 選んでいる関節を 0.5°ずつ。上 = −、下 = ＋（PICK の上空 → 床で j1・j2・j4 が増えるのと同じ向き。j1 では下を押すと爪が下がる）。始めは j1 |
| △（3） | 上下で動かす関節を切り替える：j1 → j2 → j3 → j4 → j5 → j1 |
| R1（10）を押しながら | 1回 2°ずつ（十字キーの左右・上下とも） |
| ×（0） / ○（1） | 爪を閉じる / 開く |
| HOME（5）を1秒長押し | HOME `[0, 0, 0, 0, 0, -30]` に戻る。1秒続けて押したときに1回だけ送る。1秒に満たなければ何もしない |
| L2 と R2（軸4・5）の両方が 0.0 を超えたとき | 停止。以後の動く操作を受け付けない。OPTIONS（6）で再開。片方だけでは止めない |

割り当てのないボタン（□・SHARE・L1・L3・R3・タッチパッド・スティック）は何もしない。

## 守り（数字で決めたもの）

- j1 は 90.87（arm_kinematics.md の PICK_J1_FIXED）を超える指令を送らない。※ `fk_lookup.J1_SAFE_MAX`（88.0）は別の用途の値なので、ここでは使わない（エイイチと 90.87 で決めた）
- j0 は「基準の姿勢」から ±15° まで。j1〜j5 は ±10° まで
- j4 は J4_SAFE（−90〜+60）の中だけ。各関節の物理的な可動域（今のコードにある値）も通す
- 基準の姿勢 = 起動したときに読んだ姿勢。HOME に戻ったあとは HOME を基準にし直す
- 範囲で切られたら、切られた理由を画面とログに出す。勝手に別の関節で補わない
- HOME だけは範囲の検査を通さずに送れる（run_policy_client.py の h と同じ扱い。範囲の外にいるアームを戻す手段として残す）

## 送り方

- `send_angles` だけを使う（`send_coords` 禁止）。speed 20
- daemon への送り方・読み方・爪の開閉は、今あるものを使う：`visual_servo_pick` の `get_daemon_socket` / `daemon_send_angles` / `daemon_get_angles`、爪は `pi05/run_policy_client.py` の `gripper_cmd`（閉 = `daemon_set_gripper_value(20)`、開 = `daemon_set_gripper(0, ...)`）と同じ。他のスクリプトと同時に daemon を使わないための仕組み（`install_daemon_lock` など）があれば同じように使う
- 1回押すごとに目標を1つ作って送り、届いたか（3° 以内）を確かめる。`pi05/run_policy_client.py` の補正（`plan_correction_step`・`apply_limits`・`check_send_safe`）の作りを参考にしてよい
- 送ってから届くまで（または待ちの上限まで）は、次の動く操作を受け付けない（ためておかない）。停止だけはいつでも受け付ける
- 爪の開閉のあとは、run_policy_client.py と同じ待ち（GRIPPER_SLEEP）を置き、その間は動く操作を受け付けない

## 停止について

- この依頼でやるのは「新しい指令を送らない＋ OPTIONS まで受け付けない」まで
- **動いている途中（特に HOME のような大きい動き）を止める方法は作らずに調べて報告する**：Pi で動いている `/home/ubuntu/mecharm_daemon.py`（リポジトリの写しには stop の命令がない）に止める命令があるか、pymycobot の `stop()`、今の角度を読んで送り直す案の得失。daemon は変えない
- 次のときも停止と同じ状態にする：コントローラーの接続が切れたとき（JOYDEVICEREMOVED）、読み取りに失敗したとき
- Ctrl+C・ウィンドウを閉じたときは、新しい指令を送らずに終わる。終わるときにアームを動かさない（HOME 復帰もサーボ解放もしない。run_policy_client.py の finally と同じ）

## 画面

- pygame のウィンドウに大きく出す：今動かす関節（j1 など）・刻み（0.5° / 2°）・状態（待機 / 動いている / 停止中）・6関節の今の角度・爪の状態・直前に範囲で切られた理由
- ウィンドウの文字は英数字だけ（日本語フォントがなくても化けないように。gamepad_check.py と同じ）。ターミナルには日本語で1行ずつ出す。関節を切り替えたときもターミナルに出す

## 記録

- 置き場所：`/Volumes/KIOXIA/r2r2r-data/runs/experiments/gamepad_teleop/{run_stem}/`（MASTER の保存先テーブルの「実機の単体実験」。通しの完走ではないので台帳には書かない）
- 書く前に KIOXIA のマウントを確かめる（`ensure_ssd_mounted()` などの今ある仕組み）。マウントされていなければ止めて知らせる。ローカルに逃がさない
- `log.jsonl`：押した操作1つにつき1行。時刻・ボタン・操作の種類・選んでいる関節・刻み・送る前に読んだ角度・送った角度（command）・届いた角度（reached）・届いたか・爪の状態・範囲で切られた中身・停止の状態
- `meta.json`：コントローラーの名前と GUID・Python / pygame / SDL の版・起動したときの姿勢・守りの値・git のコミット・`--execute` の有無
- 測定データと対になるコードとして、使ったスクリプトの写しを同じフォルダに置く
- 1行書くたびに flush する（途中で止まっても残るように）

## 実行のしかた

- 既定はドライラン：daemon にもアームにも触らず、HOME から始まる仮の姿勢を動かす。エイイチがアームなしでコントローラーの操作を試せるようにする
- `--execute` を付けたときだけ daemon に送る

## 確かめ（実機なし）

テストを足す：
- ボタンと操作の対応が上の表のとおりか（番号の取り違えがないか）
- 十字キー上下の向き（上 = −、下 = ＋）と、△ で j1 → j5 → j1 と回ること
- 守り：j1 が 90.87 を超えない、j0 ±15°・j1〜j5 ±10°、J4_SAFE。HOME のあと基準が HOME になること
- 停止：L2・R2 の両方が 0.0 を超えたら止まる。片方だけでは止まらない。停止中は動く操作を受け付けず、OPTIONS で戻る。接続が切れたら停止になる
- HOME：1秒に満たない長押しでは送らない。1秒続けたら1回だけ送る
- 動いている間の押しは受け付けない（ためない）
- ドライランで流れがひととおり動くこと

## 変えないもの

- daemon（Pi 側も写しも）、run_policy_client.py・visual_servo_pick.py などの既存のスクリプトと定数、tools/gamepad_check.py
- 既存のデータ

## 終わったら

- 関節を動かすコードなので、codex のレビューを通してから報告する（code_review_request_template.md の形）。指摘は直すか、直さない理由を書く
- **実機には流さない**。報告に、エイイチが最初に試す手順を書く：①ドライランでコントローラーの操作を一通り試す ②アームを HOME に置いて `--execute`、j0 を 0.5° ずつ数回 ③停止（L2+R2）と OPTIONS での再開 ④爪の開閉 ⑤HOME の長押し
- 停止の調べた結果（上の「停止について」）を「決めてほしいこと」に書く
- 個別のファイルを指定して add・commit する（`git add .` は使わない）
````

### Claude Code の作業の記録 logs/20261102_1730.md

```markdown
# 20261102_1730 依頼68：ゲームパッドで mechArm を手で動かす tools/gamepad_teleop.py

- 依頼ファイル: 20261102_68_ゲームパッドでアームを手で動かすスクリプトを作る.md
- 実機には流していない（daemon にもアームにも触っていない）。Pi への SSH（読み取りだけ）は「Operation timed out」でつながらなかった

## やったこと
- `tools/gamepad_teleop.py` を新しく作った（新規 1 本）。`tools/test_gamepad_teleop.py`（テスト 46 件）を足した
- 操作は依頼の表どおり。十字キー左右 = j0（左 = −、右 = +。依頼に向きの指定がないので `J0_LEFT_SIGN = -1.0` に置いた）、上下 = 選んでいる関節（上 = −、下 = +、始め j1）、△ = j1→j2→j3→j4→j5→j1、R1 押しながら 2°、× 閉・○ 開、HOME 1 秒長押しで HOME を 1 回、L2・R2 の両方 > 0.0 で停止・OPTIONS で再開
- 守り: j1 ≤ 90.87、j0 は基準 ±15°、j1〜j5 は ±10°、j4 は J4_SAFE、物理可動域（wr.JOINT_LIMITS）。基準 = 起動時の姿勢、HOME を送ったら HOME。動かす関節は範囲の端で切って送る（切った理由を画面・ログ・ターミナルへ）。端にいれば送らない。出発点の 6 関節のどれかが範囲の外なら送らない（HOME の長押しで戻す）
- 送り方: `daemon_send_angles`（speed 20、wait 0、timeout_override 8 秒）で 1 回だけ送り、`read_angles` で 3° 以内かを読む。送り直さない（`_send_and_wait_arrival` は届かないと 5 秒後に送り直すので、停止のあとに送ってしまう。使わなかった）。爪は `gripper_cmd` と同じ中身（閉 = `daemon_set_gripper_value(20)`、開 = `daemon_set_gripper(0, wait=0.0)`、そのあと 0.8 秒）。`install_daemon_lock` は --execute のときに呼ぶ
- 1 回の出発点: 直前の目標に届いていて、読んだ角度とも 3° 以内ならその目標、そうでなければ読んだ角度
- 停止: 送る仕事は別スレッド。仕事ごとの SendGate で「送ると決める」と「停止の取り消し」を並べる。停止のあとに送ると決める指令はない。止める前の仕事は OPTIONS で生き返らない。コントローラーの切断・読み取りの失敗・到着確認中に関節角を読めない・送信の失敗・log.jsonl に書けない（このときは再開させない）も停止
- 終わり方: Ctrl+C・ウィンドウを閉じる → 新しい指令は送らない。HOME 復帰もサーボ解放もしない
- 記録: `/Volumes/KIOXIA/r2r2r-data/runs/experiments/gamepad_teleop/teleop_YYYYMMDD_HHMMSS[_dry]/` に log.jsonl（1 操作 1 行・flush）、meta.json、スクリプトの写し。KIOXIA が未マウントなら止める
- codex レビュー 3 回（/tmp/req68/codex_r1.md・r2・r3）
  - 1 回目: 必須 7 件（停止と送信の競合、範囲の外の出発点を切ってから大きく動く、古い目標から動く、HOME 長押しの持ち越し、同じコマでボタンを停止より先に処理、読み取り失敗の扱い、記録失敗のあとの再開）→ 全部直した
  - 2 回目: 必須 2 件（鍵の待ちが切れたあとの送信、停止で画面が止まる）→ 鍵を「送ると決める」一瞬だけにし、送信に通信の上限を付けた
  - 3 回目: 必須なし。推奨 2 件（終了時に送ると決めていたかを残す、終了のテスト）→ 直した

## 停止の調べ（実機を止める方法は作っていない）
- リポジトリの写し `mecharm_daemon.py` の命令は ping / send_angles / send_angle / get_angles / set_fresh_mode / set_gripper だけ。stop はない（写しには set_gripper_value もないので、Pi で動いているものとは違う）。Pi の本物は SSH がつながらず確かめられなかった
- daemon は 1 本の接続を順に処理する（listen(1)、accept して serve_client が終わるまで次を受けない）。別のプロセスから止める命令を割り込ませることはできない。同じ接続なら send_angles は送った直後に返るので、動いている間に次の命令を送れる
- pymycobot 4.0.5（Mac）に `stop()`（ProtocolCode.STOP 0x29）・`pause()` / `resume()` / `is_paused()`・`is_moving()` がある。使うには daemon に命令を足す必要がある
- 今の角度を読んで送り直す案: 新しい命令が要らない。ただし読むのに最大 3.7 秒かかる（LOG352）ので、その間も動きが進む。送る角度は動きの途中の値なので、止まるまでに少し行き過ぎる。speed 20 で行き先を変えることになるので、急に止まるわけではない

## 変えたファイル
- tools/gamepad_teleop.py（新規）
- tools/test_gamepad_teleop.py（新規）
- logs/20261102_1730.md（この記録）

## 変えていないもの
- daemon（Pi・写し）、run_policy_client.py、visual_servo_pick.py（作業前から未コミットの差分があるが、今回は触っていない）、j0_sweep_scan.py、tools/gamepad_check.py、既存のデータ
```

### 依頼箱の報告 20261102_68_ゲームパッドでアームを手で動かすスクリプトを作る_20261102_1609.md

```markdown
# 結果：20261102_68_ゲームパッドでアームを手で動かすスクリプトを作る

- 状態：完了
- 始めた時刻：2026-11-02 16:09
- 終わった時刻：2026-11-02 17:15（66分）
- 作業フォルダ：/Users/shimazueiichi/r2r2r-scripts
- この続きを Claude Code で話すとき：作業フォルダで `claude --resume 0dc0bf5d-b87f-4756-959e-0b36dbe3081e`

## Claude Code の報告

この起こしは2回目の codex レビューの確認用でしたが、その作業はもう終わっています。

## やったこと
- 2回目のレビュー結果（/tmp/req68/codex_r2.md）を見直しました。指摘は2件で、どちらも先に直してあります。
  - 停止の待ちが10秒で切れたあとに送られる穴
  - 停止を押すと画面が止まる問題
- 3回目のレビュー（/tmp/req68/codex_r3.md）は「必須の修正なし」でした。推奨2件も直しています。

## 結果
- テストは46件すべて通っています。
- commit は 78bd62e（スクリプトとテスト）と 1185747（作業の記録）で、今回足した作業はありません。
- 実機には流していません。

## 変えたファイル
なし（今回の起こしでは何も変えていません）

## 決めてほしいこと
前の報告と同じ3点です。
1. **十字キー左右の向き**：今は「右 = +、左 = −」です。アームの動く向きと合っているかを見て決めてください。
2. **動いている途中を止める方法**：daemon に stop の命令を足すかどうか。足さない場合は、今の角度を読んで送り直す案になります。こちらは少し行き過ぎてから止まります。
3. **ドライランの記録**：今はドライランでも KIOXIA に記録しています。このままでよいか決めてください。
```

### gamepad_teleop.py（commit 78bd62e）

```python
#!/usr/bin/env python3
"""gamepad_teleop.py — ゲームパッドで mechArm 270 を手で動かす (依頼 68. 2026-11-02)

PICK で爪の位置がずれたときに, エイイチがゲームパッドで左右・上げ下げ・爪の開閉を直す.
押した分だけそのまま動く. 自動で動く要素は入れない (自動の握り直し・失敗時の自動 HOME などはない).
押した操作と, 送った角度・届いた角度をすべて log.jsonl に残す (あとで「人が直した記録」として使う).

操作 (エイイチが決めた. 変えない):
  十字キー 左 / 右 (13 / 14)  j0 を 0.5° ずつ. どの関節を選んでいても いつも j0
                              (向き: 左 = J0_LEFT_SIGN. 依頼に向きの指定がないので, 右 = +, 左 = − にしてある)
  十字キー 上 / 下 (11 / 12)  選んでいる関節を 0.5° ずつ. 上 = −, 下 = +. 始めは j1
  △ (3)                       上下で動かす関節を切り替える: j1 → j2 → j3 → j4 → j5 → j1
  R1 (10) を押しながら        1 回 2° ずつ (十字キーの左右・上下とも)
  × (0) / ○ (1)              爪を閉じる / 開く
  HOME (5) を 1 秒長押し      HOME [0, 0, 0, 0, 0, -30] に戻る. 1 秒続けて押したときに 1 回だけ送る
  L2 と R2 (軸 4・5) の両方が 0.0 を超えたとき  停止. 以後の動く操作を受け付けない. OPTIONS (6) で再開
  ほかのボタン (□・SHARE・L1・L3・R3・タッチパッド・スティック) は何もしない

守り:
  - j1 は 90.87 (arm_kinematics.md の PICK_J1_FIXED) を超える指令を送らない
    (fk_lookup.J1_SAFE_MAX = 88.0 は別の用途の値なので, ここでは使わない)
  - j0 は 基準の姿勢 ±15°, j1〜j5 は ±10°. j4 は J4_SAFE (−90〜+60). 物理可動域 (wr.JOINT_LIMITS) も通す
  - 基準の姿勢 = 起動したときに読んだ姿勢. HOME を送ったあとは HOME を基準にし直す
  - 範囲で切られたら理由を画面とログに出す. 別の関節で補わない
  - HOME だけは範囲の検査を通さずに送れる (run_policy_client.py の h と同じ扱い)

送り方:
  - send_angles だけ (send_coords は使わない). speed 20 (j0_sweep_scan.SPEED)
  - 1 回押すごとに目標を 1 つ作って 1 回だけ送り, 3° 以内に届いたかを読んで確かめる.
    送り直しはしない (_send_and_wait_arrival は届かないと 5 秒後に同じ目標を送り直すので,
    その間に停止が押されても送ってしまう. ここでは使わない)
  - 送ってから届くまで (または待ちの上限まで) と, 爪のあとの GRIPPER_SLEEP の間は
    次の動く操作を受け付けない (ためない). 停止はいつでも受け付ける
    (送る仕事は別のスレッドで行い, 画面とコントローラーの読み取りは止めない)
  - 停止が入ったあとは新しい指令を送らない. **動いている途中の動きは止められない**
    (daemon に止める命令がない. 依頼 68 の報告を参照)

1 回の出発点:
  直前に送った目標に届いていて, 送る前に読んだ角度とも 3° 以内なら その目標. そうでなければ 読んだ角度.
  (j1 の重さによる 2〜3° の不足があっても, 0.5° の刻みが読んだ値に引きずられないように.
   手で動かされたなど 3° を超えて離れていたら, 古い目標から大きく動かさない)
  出発点の 6 関節のどれかが範囲の外なら送らない (範囲の端へ切って 1 回で大きく動かさない).

停止と送信の順番 (codex R1):
  「送ると決める」と「停止・終了の取り消し」を 仕事ごとの鍵 (SendGate) で並べる (codex R1・R2).
  停止のあとに送ると決める指令はない. 停止は待たされない (画面も止まらない). 停止より前に決めた送信は
  通信の上限 SEND_RPC_TIMEOUT_SEC (8 秒) の中で終わる. 止める前に作った仕事は OPTIONS で再開しても生き返らない.
  1 コマの中では L2+R2 と読み取りの失敗を ボタンより先に見る. 送ったあと確かめる途中で関節角を
  読めなければ停止にする. log.jsonl に書けなくなったら停止し, 再開させない.

終わり方:
  Ctrl+C / ウィンドウを閉じる. 新しい指令は送らずに終わる. アームは動かさない
  (HOME 復帰もサーボ解放もしない. run_policy_client.py の finally と同じ)

Usage:
  python3 tools/gamepad_teleop.py              # ドライラン (daemon にもアームにも触らない. HOME から始まる仮の姿勢)
  python3 tools/gamepad_teleop.py --execute    # daemon に送る

記録: /Volumes/KIOXIA/r2r2r-data/runs/experiments/gamepad_teleop/{run_stem}/
      log.jsonl (押した操作 1 つにつき 1 行) / meta.json / gamepad_teleop.py (使ったスクリプトの写し)
      通しの完走ではないので台帳には書かない.
"""
import argparse
import hashlib
import json
import os
import platform
import queue
import shutil
import subprocess
import sys
import threading
import time
from datetime import datetime
from pathlib import Path

# ウィンドウが手前にないときもコントローラーの入力を受け取る (gamepad_check.py と同じ)
os.environ.setdefault("SDL_JOYSTICK_ALLOW_BACKGROUND_EVENTS", "1")
os.environ.setdefault("PYGAME_HIDE_SUPPORT_PROMPT", "1")

import numpy as np  # noqa: E402
import pygame  # noqa: E402

_HERE = Path(__file__).resolve().parent
_ROOT = _HERE.parent
sys.path.insert(0, str(_ROOT / "pi05"))

# run_policy_client を読むと, パス (place_modules / drag_teaching) と pymycobot のスタブが入る.
# 爪 (gripper_cmd)・HOME・J4_SAFE・関節角の読み方 (read_angles) は それと同じものを使う
import run_policy_client as rpc  # noqa: E402
from j0_sweep_scan import POLL_SLEEP, REACH_TIMEOUT_SEC, SPEED  # noqa: E402  20 / 0.3 / 8.0
from visual_servo_pick import (  # noqa: E402
    close_daemon_socket, daemon_send_angles, daemon_set_gripper, daemon_set_gripper_value, get_daemon_socket,
)

wr = rpc.wr

# ---- ボタンと軸 (/Volumes/KIOXIA/r2r2r-data/diag/gamepad_check_20261102/ で確かめた番号) ----
BTN_CROSS = 0
BTN_CIRCLE = 1
BTN_SQUARE = 2
BTN_TRIANGLE = 3
BTN_SHARE = 4
BTN_HOME = 5
BTN_OPTIONS = 6
BTN_L3 = 7
BTN_R3 = 8
BTN_L1 = 9
BTN_R1 = 10
BTN_UP = 11
BTN_DOWN = 12
BTN_LEFT = 13
BTN_RIGHT = 14
BTN_TOUCHPAD = 15
BUTTON_NAMES = {
    BTN_CROSS: "cross", BTN_CIRCLE: "circle", BTN_SQUARE: "square", BTN_TRIANGLE: "triangle",
    BTN_SHARE: "share", BTN_HOME: "home", BTN_OPTIONS: "options", BTN_L3: "l3", BTN_R3: "r3",
    BTN_L1: "l1", BTN_R1: "r1", BTN_UP: "dpad_up", BTN_DOWN: "dpad_down", BTN_LEFT: "dpad_left",
    BTN_RIGHT: "dpad_right", BTN_TOUCHPAD: "touchpad",
}
AXIS_L2 = 4
AXIS_R2 = 5
TRIGGER_STOP_ABOVE = 0.0      # L2 と R2 の両方がこれを超えたら停止 (離して -1.0, 押し込んで +1.0)

# ---- 動かし方 ----
STEP_FINE_DEG = 0.5
STEP_COARSE_DEG = 2.0          # R1 を押しながら
J0_LEFT_SIGN = -1.0            # 十字キー左で j0 を動かす向き (右はその逆). 依頼に向きの指定がない
UP_SIGN = -1.0                 # 十字キー上 = −, 下 = + (PICK の上空 → 床で j1・j2・j4 が増える向き)
SELECT_ORDER = (1, 2, 3, 4, 5)  # △ で回す順. 始めは j1
HOME_HOLD_SEC = 1.0
HOME = [float(v) for v in rpc.HOME]   # [0, 0, 0, 0, 0, -30]

# ---- 守り ----
J1_MAX_DEG = 90.87             # arm_kinematics.md の PICK_J1_FIXED (エイイチと決めた. J1_SAFE_MAX 88 は使わない)
J0_RANGE_DEG = 15.0            # 基準の姿勢から j0 は ±15°
JOINT_RANGE_DEG = 10.0         # j1〜j5 は ±10°
J4_SAFE = tuple(float(v) for v in rpc.J4_SAFE)   # (-90, 60)
JOINT_LIMITS = [tuple(float(v) for v in lim) for lim in wr.JOINT_LIMITS]   # 物理可動域
LIMIT_EPS_DEG = 1e-6
REACH_TOL_DEG = 3.0            # 届いたかの判定 (推論の到着確認と同じ 3°)

DRY_MOVE_SEC = 0.3             # ドライランで「動いている」状態を見せる時間
SEND_RPC_TIMEOUT_SEC = 8.0     # send_angles 1 回の通信 (接続・ping・送信・返事) の上限. 通信が詰まった
                               # 送信が 停止のずっとあとに届かないように (daemon の返事は送信後の読み直し込みで最長 3.7 秒. LOG352)
WORKER_JOIN_SEC = 12.0         # 終わるとき, 送っている途中の仕事が戻るのを待つ上限 (読み取り 1 回は最長 3.7 秒)

DATA_ROOT = Path("/Volumes/KIOXIA/r2r2r-data")
LOG_ROOT = DATA_ROOT / "runs" / "experiments" / "gamepad_teleop"
SSD_MOUNT = "/Volumes/KIOXIA"

STATE_IDLE = "IDLE"
STATE_MOVING = "MOVING"
STATE_STOPPED = "STOPPED"


def ensure_ssd_mounted():
    if not os.path.ismount(SSD_MOUNT):
        sys.exit("[ABORT] KIOXIA がマウントされていません。記録をローカルには逃がさず、ここで止めます。")


def _r(a):
    return None if a is None else [round(float(v), 2) for v in a]


def _fmt(a):
    return "None" if a is None else " ".join(f"{float(v):+.2f}" for v in a)


# =============================================================================
# 守り (純関数. テストで直接呼ぶ)
# =============================================================================

def joint_window(base):
    """基準の姿勢 base から, 各関節に送ってよい範囲 (lo, hi, 各端の出どころ) を作る.
    出どころ: BASE (基準 ±15/±10) / PHYS (物理可動域) / J4_SAFE / J1_MAX. 狭い方が勝つ."""
    lo, hi, lo_src, hi_src = [], [], [], []
    for k in range(6):
        r = J0_RANGE_DEG if k == 0 else JOINT_RANGE_DEG
        cands_lo = [(float(base[k]) - r, "BASE"), (JOINT_LIMITS[k][0], "PHYS")]
        cands_hi = [(float(base[k]) + r, "BASE"), (JOINT_LIMITS[k][1], "PHYS")]
        if k == 4:
            cands_lo.append((J4_SAFE[0], "J4_SAFE"))
            cands_hi.append((J4_SAFE[1], "J4_SAFE"))
        if k == 1:
            cands_hi.append((J1_MAX_DEG, "J1_MAX"))
        v_lo, s_lo = max(cands_lo, key=lambda c: c[0])
        v_hi, s_hi = min(cands_hi, key=lambda c: c[0])
        lo.append(v_lo); hi.append(v_hi); lo_src.append(s_lo); hi_src.append(s_hi)
    return np.array(lo), np.array(hi), lo_src, hi_src


LIMIT_JA = {"BASE": "基準の姿勢からの範囲", "PHYS": "物理可動域", "J4_SAFE": "J4_SAFE",
            "J1_MAX": "j1 の上限 90.87 (PICK_J1_FIXED)"}


def plan_step(start, joint, delta, base):
    """start (今回の出発点) の joint を delta 動かした目標を作り, 範囲を通す.

    戻り値 dict:
      target   送る目標 (6) or None (送らない)
      clip     切られた中身 [{joint, want, cmd, limit, bound}] (切られて端まで送るとき)
      refused  送らない理由 [{joint, value, limit, lo, hi, ...}] (空なら送ってよい)
    動かした関節は範囲の端で切る (端までは送る). 端にいて押した向きに 1 ° も進めないときは送らない.
    ほかの関節は変えない (補わない). 6 関節のどれかが範囲の外なら送らない.
    """
    lo, hi, lo_src, hi_src = joint_window(base)
    want = np.asarray(start, dtype=float)[:6].copy()
    out = {"target": None, "clip": [], "refused": []}
    if not np.all(np.isfinite(want)):
        out["refused"].append({"joint": None, "why": "nonfinite_start", "value": _r(want)})
        return out
    # 出発点の 6 関節がどれか範囲の外なら送らない (切ってから送ると, 範囲の外から 1 回で
    # 刻みより大きく動く. 例: j1=100 から −0.5 を押すと 90.87 へ −9.13° になる. codex 必須 2)
    for k in range(6):
        if want[k] < lo[k] - LIMIT_EPS_DEG or want[k] > hi[k] + LIMIT_EPS_DEG:
            src = hi_src[k] if want[k] > hi[k] else lo_src[k]
            out["refused"].append({"joint": k, "why": "start_out_of_window", "value": round(want[k], 2),
                                   "lo": round(lo[k], 2), "hi": round(hi[k], 2), "limit": src})
    if out["refused"]:
        return out
    v = want[joint] + delta
    if lo[joint] > hi[joint]:
        out["refused"].append({"joint": joint, "why": "empty_window", "value": round(v, 2),
                               "lo": round(lo[joint], 2), "hi": round(hi[joint], 2),
                               "limit": f"{lo_src[joint]}/{hi_src[joint]}"})
        return out
    c = min(max(v, lo[joint]), hi[joint])
    if abs(c - v) > LIMIT_EPS_DEG:
        src = hi_src[joint] if v > hi[joint] else lo_src[joint]
        bound = hi[joint] if v > hi[joint] else lo[joint]
        out["clip"].append({"joint": joint, "want": round(v, 2), "cmd": round(c, 2),
                            "limit": src, "bound": round(float(bound), 2)})
    if (c - want[joint]) * np.sign(delta) <= LIMIT_EPS_DEG:
        # 端にいる (または出発点が範囲の外で, 押した向きに進めない)
        src = hi_src[joint] if delta > 0 else lo_src[joint]
        out["refused"].append({"joint": joint, "why": "at_limit", "value": round(v, 2),
                               "lo": round(lo[joint], 2), "hi": round(hi[joint], 2), "limit": src})
        return out
    want[joint] = c
    for k in range(6):
        if want[k] < lo[k] - LIMIT_EPS_DEG or want[k] > hi[k] + LIMIT_EPS_DEG:
            src = hi_src[k] if want[k] > hi[k] else lo_src[k]
            out["refused"].append({"joint": k, "why": "out_of_window", "value": round(want[k], 2),
                                   "lo": round(lo[k], 2), "hi": round(hi[k], 2), "limit": src})
    if not out["refused"]:
        out["target"] = want
    return out


def limit_text_ascii(plan):
    """画面用 (英数字だけ)."""
    parts = []
    for c in plan.get("clip", []):
        parts.append(f"j{c['joint']} clipped {c['want']:+.2f}->{c['cmd']:+.2f} ({c['limit']} {c['bound']:+.2f})")
    for r in plan.get("refused", []):
        if r.get("joint") is None:
            parts.append(f"refused: {r['why']}")
        else:
            parts.append(f"j{r['joint']} refused {r['why']} {r['value']:+.2f} "
                         f"[{r['lo']:+.2f}, {r['hi']:+.2f}] ({r['limit']})")
    return " / ".join(parts)


def limit_text_ja(plan):
    """ターミナル用 (日本語)."""
    parts = []
    for c in plan.get("clip", []):
        parts.append(f"j{c['joint']} を {c['want']:+.2f}° → {c['cmd']:+.2f}° に切った"
                     f"（{LIMIT_JA.get(c['limit'], c['limit'])} {c['bound']:+.2f}°）")
    for r in plan.get("refused", []):
        if r.get("joint") is None:
            parts.append(f"送らない: {r['why']}")
            continue
        why = {"at_limit": "範囲の端にいて、押した向きに進めない",
               "out_of_window": "範囲の外",
               "start_out_of_window": "出発点が範囲の外（HOME の長押しで戻せます）",
               "empty_window": "範囲が空（基準の姿勢が守りの外）"}.get(r["why"], r["why"])
        parts.append(f"j{r['joint']} {r['value']:+.2f}° は送らない：{why}"
                     f"（範囲 [{r['lo']:+.2f}, {r['hi']:+.2f}]・{LIMIT_JA.get(r['limit'], r['limit'])}）")
    return " / ".join(parts)


# =============================================================================
# アームへの送り口
# =============================================================================

class SendGate:
    """1 つの仕事の「送ると決める」と「停止・終了の取り消し」を順番に並べる鍵 (codex R1 必須 1 / R2).

    送る側は 鍵の中で取り消しを確かめて「送ると決めた」(committed) を立て, 鍵の外で送る.
    停止・終了は 鍵の中で取り消しを立て, そのときに送ると決めていたかを受け取る.
    鍵を持つのは確かめる一瞬だけなので, 停止は待たされない (画面も止まらない).
    なので「停止のあとに送ると決めた指令」はない. 停止より前に決めた送信は, 通信が遅れても
    daemon_send_angles の timeout_override (SEND_RPC_TIMEOUT_SEC) の中で終わる.
    取り消しは仕事ごとに作り, OPTIONS で再開しても戻さない (止める前の仕事が生き返らない).
    """

    def __init__(self):
        self.lock = threading.Lock()
        self.cancel = threading.Event()
        self.committed = False

    def run(self, fn):
        """取り消されていなければ「送ると決めた」を立ててから fn() を呼ぶ. 戻り値: 呼んだか."""
        with self.lock:
            if self.cancel.is_set():
                return False
            self.committed = True
        fn()
        return True

    def cancel_now(self):
        """取り消しを立てる. 戻り値: それより前に送ると決めていたか (送信中・送信済み)."""
        with self.lock:
            self.cancel.set()
            return self.committed


class DaemonArm:
    """--execute: daemon に送る. 送り方・読み方は visual_servo_pick / run_policy_client と同じ."""

    execute = True

    def read(self):
        return rpc.read_angles()          # 5 回まで読み直す. 読めなければ None

    def send_and_check(self, target, gate, quitting):
        """target を 1 回だけ send_angles (speed 20) で送り, 3° 以内に届くかを読んで確かめる.
        gate が取り消されていれば送らない. 送り直しはしない. 確かめる途中で読めなければ error."""
        target = np.asarray(target, dtype=float)
        pre = self.read()
        if pre is None:
            return {"sent": False, "error": "read_failed_before_send"}
        pre_max = float(np.max(np.abs(target - pre)))
        timeout = max(REACH_TIMEOUT_SEC, wr._compute_arrival_timeout(pre_max))
        t_send = time.time()
        try:
            sent = gate.run(lambda: daemon_send_angles([float(v) for v in target], speed=SPEED,
                                                       wait=rpc.SEND_WAIT,
                                                       timeout_override=SEND_RPC_TIMEOUT_SEC))
        except Exception as e:
            # 届いたかどうか分からない. 呼び出し側で停止にする
            return {"sent": "unknown", "error": f"send_failed: {type(e).__name__}: {e}",
                    "measured_before_send": _r(pre), "t_send": t_send}
        if not sent:
            return {"sent": False, "why": "halted_before_send", "measured_before_send": _r(pre)}
        t0 = time.monotonic()
        last, err = None, None
        base = {"sent": True, "timeout_sec": round(timeout, 2), "t_send": t_send, "measured_before_send": _r(pre)}
        while time.monotonic() - t0 < timeout:
            if quitting.is_set():
                return dict(base, reached=None, interrupted=True, reached_angles=_r(last),
                            reach_err=None if err is None else round(err, 3))
            a = self.read()
            if a is None:
                # 動いているはずの間に読めない: 届いたか分からないまま次を受け付けない (codex 必須 6)
                return dict(base, reached=None, reached_angles=_r(last),
                            reach_err=None if err is None else round(err, 3),
                            error="read_failed_during_check", elapsed_sec=round(time.monotonic() - t0, 2))
            last = a
            err = float(np.max(np.abs(a - target)))          # 丸める前の値で判定する (codex 推奨 3)
            if err <= REACH_TOL_DEG:
                return dict(base, reached=True, reached_angles=_r(a), reach_err=round(err, 3),
                            elapsed_sec=round(time.monotonic() - t0, 2))
            time.sleep(POLL_SLEEP)
        return dict(base, reached=False, reached_angles=_r(last),
                    reach_err=None if err is None else round(err, 3),
                    elapsed_sec=round(time.monotonic() - t0, 2))

    def gripper(self, state, gate):
        """run_policy_client.gripper_cmd と同じ中身 (閉 = daemon_set_gripper_value(GRIP_VALUE) /
        開 = daemon_set_gripper(0, wait=GRIP_OPEN_DAEMON_WAIT). そのあと GRIPPER_SLEEP 待つ).
        指令だけを gate の中で送り, 待ちは gate の外で行う (停止の受け付けを待ちの分だけ遅らせない).
        戻り値: 送ったか."""
        if state == 1:
            sent = gate.run(lambda: daemon_set_gripper_value(rpc.GRIP_VALUE))
        else:
            sent = gate.run(lambda: daemon_set_gripper(0, wait=rpc.GRIP_OPEN_DAEMON_WAIT))
        if sent:
            time.sleep(rpc.GRIPPER_SLEEP)
        return sent


class DryArm:
    """ドライラン: daemon にもアームにも触らない. HOME から始まる仮の姿勢を動かす."""

    execute = False

    def __init__(self, start=HOME):
        self.q = np.asarray(start, dtype=float).copy()

    def read(self):
        return self.q.copy()

    def send_and_check(self, target, gate, quitting):
        t_send = time.time()
        if not gate.run(lambda: None):
            return {"sent": False, "why": "halted_before_send", "measured_before_send": _r(self.q)}
        time.sleep(DRY_MOVE_SEC)
        self.q = np.asarray(target, dtype=float).copy()
        return {"sent": True, "reached": True, "reached_angles": _r(self.q), "reach_err": 0.0,
                "t_send": t_send, "dry": True}

    def gripper(self, state, gate):
        if not gate.run(lambda: None):
            return False
        time.sleep(rpc.GRIPPER_SLEEP)     # 実機と同じ間だけ受け付けない
        return True


# =============================================================================
# 記録
# =============================================================================

class JsonlLog:
    def __init__(self, path):
        self.path = Path(path)
        self.f = open(self.path, "x", encoding="utf-8")

    def write(self, rec):
        self.f.write(json.dumps(rec, ensure_ascii=False) + "\n")
        self.f.flush()                    # 途中で止まっても残るように 1 行ごとに

    def close(self):
        if not self.f.closed:
            self.f.close()


def write_json_atomic(path, obj):
    tmp = Path(str(path) + ".tmp")
    with open(tmp, "w", encoding="utf-8") as f:
        json.dump(obj, f, ensure_ascii=False, indent=2)
        f.flush()
        os.fsync(f.fileno())
    os.replace(tmp, path)


# =============================================================================
# 操作の状態 (pygame に依存しない. テストで直接呼ぶ)
# =============================================================================

class Teleop:
    """押したボタンを受けて, 送る仕事を別のスレッドで 1 つずつ行う.

    送っている間 (busy) の動く操作は受け付けない (ためない). 停止はいつでも受け付ける.
    状態を書き換えるのは呼び出し側のスレッド (pygame のループ) だけ. 送る仕事の結果は
    results に入り, poll() で受け取って状態とログに反映する.
    送る直前の確かめは 仕事ごとの SendGate で行い, 停止・終了はその鍵を取ってから取り消す.
    """

    def __init__(self, arm, start_pose, log, out=print, spawn=None):
        self.arm = arm
        self.log = log
        self.out = out
        self.spawn = spawn or self._spawn_thread
        self.base = np.asarray(start_pose, dtype=float).copy()       # 基準の姿勢
        self.cmd = np.asarray(start_pose, dtype=float).copy()        # 直前の目標 (始めは起動時の姿勢)
        self.cmd_reached = True
        self.measured = np.asarray(start_pose, dtype=float).copy()   # 最後に読めた角度
        self.sel = 0                       # SELECT_ORDER の何番目 (始めは j1)
        self.r1 = False
        self.stopped = False
        self.stop_reason = None
        self.busy = False
        self.pending = None                # 送っている操作のログ行
        self.thread = None
        self.grip = "unknown"
        self.last_limit = ""               # 直前に範囲で切られた理由 (画面用. 英数字)
        self.last_msg = ""
        self.gate = None                   # 今の仕事の SendGate (停止・終了で取り消す)
        self.log_failed = False            # log.jsonl に書けなくなったら 再開させない (終わらせる)
        self.quitting = threading.Event()
        self.results = queue.Queue()
        self.home_down_at = None
        self.home_handled = False
        self.triggers = [-1.0, -1.0]
        self.both_triggers = False
        self.read_error = False
        self.seq = 0
        self.n_sent = 0

    # ---- 見せる値 ----
    @property
    def joint(self):
        return SELECT_ORDER[self.sel]

    @property
    def step(self):
        return STEP_COARSE_DEG if self.r1 else STEP_FINE_DEG

    @property
    def state(self):
        if self.stopped:
            return STATE_STOPPED
        return STATE_MOVING if self.busy else STATE_IDLE

    # ---- 記録 ----
    def _entry(self, button, kind, **kw):
        self.seq += 1
        e = {"seq": self.seq, "t": datetime.now().isoformat(timespec="milliseconds"),
             "t_unix": round(time.time(), 3), "button": button,
             "button_name": BUTTON_NAMES.get(button) if isinstance(button, int) else button,
             "kind": kind, "selected_joint": self.joint, "step_deg": self.step,
             "execute": bool(self.arm.execute)}
        e.update(kw)
        return e

    def _finish(self, e):
        e.update({"grip_state": self.grip, "stopped": self.stopped, "stop_reason": self.stop_reason,
                  "base_pose": _r(self.base)})
        try:
            self.log.write(e)
        except Exception as ex:          # SSD が外れたなど. ローカルには逃がさない
            first = not self.log_failed
            self.log_failed = True
            if first:
                self.out(f"[記録エラー] log.jsonl に書けません: {ex}。停止にします"
                         "（再開はできません。終わらせて KIOXIA を確かめてください）")
            if not self.stopped:
                self.stop_reason = "log_write_failed"
            self.stopped = True
            self._cancel_gate()
            self._drop_home_hold()

    # ---- 停止 ----
    def _cancel_gate(self):
        """今の仕事をまだ送ると決めていなければ, もう送らせない (待たない).
        戻り値: 停止より前に送ると決めていたか (None = 仕事なし)."""
        if self.gate is None:
            return None
        return self.gate.cancel_now()

    def _drop_home_hold(self):
        """HOME の長押しを無効にする (押し直しを求める. codex 必須 4)."""
        if self.home_down_at is not None and not self.home_handled:
            self.home_handled = True
            self.out("[HOME] 長押しの途中で受け付けられなくなったので取り消しました（押し直してください）")
            self._finish(self._entry(BTN_HOME, "home", accepted=False, reason="hold_cancelled"))

    def enter_stop(self, reason, button=None, kind="stop"):
        was = self.stopped
        self.stopped = True
        committed = self._cancel_gate()
        if not was:
            self.stop_reason = reason
        self.out(f"[停止] {reason}。新しい指令は送りません（OPTIONS で再開）"
                 + ("。止める前に送った動きは止まらずに最後まで行きます" if committed else "")
                 + ("。まだ送っていなかった操作は送りません" if committed is False else ""))
        self._finish(self._entry(button, kind, accepted=True, reason=reason, already_stopped=was,
                                 busy=self.busy, send_committed_before_stop=committed,
                                 triggers=[round(v, 3) for v in self.triggers]))
        self._drop_home_hold()

    def set_triggers(self, l2, r2):
        self.triggers = [float(l2), float(r2)]
        both = l2 > TRIGGER_STOP_ABOVE and r2 > TRIGGER_STOP_ABOVE
        if both and not self.both_triggers:
            self.both_triggers = True
            self.enter_stop("L2+R2", button="L2+R2")
        self.both_triggers = both

    def controller_lost(self, reason):
        self.enter_stop(reason, button=None, kind="controller_lost")
        self.r1 = False                      # 押しっぱなしの状態を持ち越さない
        self.home_down_at = None
        self.home_handled = False
        self.both_triggers = False

    def controller_read_failed(self, msg):
        if not self.read_error:
            self.read_error = True
            self.enter_stop(f"read_error: {msg}", button=None, kind="read_error")

    def controller_read_ok(self):
        self.read_error = False

    def _resume(self, e):
        if not self.stopped:
            e.update(accepted=False, reason="not_stopped")
            self.out("[再開] 止まっていないので何もしません")
        elif self.both_triggers:
            e.update(accepted=False, reason="triggers_still_pressed")
            self.out("[再開] L2 と R2 を離してから OPTIONS を押してください")
        elif self.read_error:
            e.update(accepted=False, reason="controller_read_error")
            self.out("[再開] コントローラーを読めていないので再開しません")
        elif self.log_failed:
            e.update(accepted=False, reason="log_write_failed")
            self.out("[再開] 記録が書けないので再開しません（終わらせて KIOXIA を確かめてください）")
        else:
            # 止める前に作った仕事の取り消しは戻さない (仕事ごとの SendGate. 再開で生き返らない)
            self.stopped = False
            self.stop_reason = None
            e.update(accepted=True)
            self.out("[再開] 操作を受け付けます")
        self._finish(e)

    # ---- ボタン ----
    def press(self, button, now):
        if button == BTN_R1:
            self.r1 = True
            self._finish(self._entry(button, "modifier", accepted=True))
            return
        if button == BTN_TRIANGLE:
            self.sel = (self.sel + 1) % len(SELECT_ORDER)
            self.out(f"[関節] 上下で動かす関節を j{self.joint} にしました")
            self._finish(self._entry(button, "select_joint", accepted=True))
            return
        if button == BTN_OPTIONS:
            self._resume(self._entry(button, "resume"))
            return
        if button == BTN_HOME:
            self.home_down_at = now
            self.home_handled = False
            if self.stopped or self.busy:
                # 押し始めに受け付けられないなら, 長押しを続けても送らない (押し直しを求める)
                self.home_handled = True
                why = "stopped" if self.stopped else "busy"
                self.out("[受付なし] HOME: " + ("停止中です" if self.stopped else "前の操作が終わっていません")
                         + "（押し直してください）")
                self._finish(self._entry(button, "home", accepted=False, reason=why))
            return                           # 1 秒続いたとき (tick) か 離したとき (release) に 1 行書く
        if button in (BTN_LEFT, BTN_RIGHT):
            sign = J0_LEFT_SIGN if button == BTN_LEFT else -J0_LEFT_SIGN
            self._request(self._entry(button, "move", joint=0, delta=sign * self.step),
                          ("move", 0, sign * self.step))
            return
        if button in (BTN_UP, BTN_DOWN):
            sign = UP_SIGN if button == BTN_UP else -UP_SIGN
            j = self.joint
            self._request(self._entry(button, "move", joint=j, delta=sign * self.step),
                          ("move", j, sign * self.step))
            return
        if button in (BTN_CROSS, BTN_CIRCLE):
            state = 1 if button == BTN_CROSS else 0
            self._request(self._entry(button, "grip_close" if state else "grip_open", grip_cmd=state),
                          ("grip", state))
            return
        self._finish(self._entry(button, "ignored", accepted=False, reason="unassigned"))

    def release(self, button, now):
        if button == BTN_R1:
            self.r1 = False
        elif button == BTN_HOME:
            if self.home_down_at is not None and not self.home_handled:
                held = now - self.home_down_at
                self.out(f"[HOME] {held:.2f} 秒で離したので送りません（{HOME_HOLD_SEC:.0f} 秒の長押しで送ります）")
                self._finish(self._entry(button, "home_short", accepted=False, reason="held_too_short",
                                         held_sec=round(held, 3)))
            self.home_down_at = None
            self.home_handled = False

    def tick(self, now):
        if self.home_down_at is not None and not self.home_handled and (self.busy or self.stopped):
            self._drop_home_hold()           # 長押しの途中で別の操作が動いた・止まった
        if (self.home_down_at is not None and not self.home_handled
                and now - self.home_down_at >= HOME_HOLD_SEC):
            self.home_handled = True
            self._request(self._entry(BTN_HOME, "home", held_sec=round(now - self.home_down_at, 3)),
                          ("home",))

    # ---- 送る仕事 ----
    def _request(self, e, job):
        if self.stopped:
            e.update(accepted=False, reason="stopped")
            self.out("[受付なし] 停止中です（OPTIONS で再開）")
            self._finish(e)
            return
        if self.busy:
            e.update(accepted=False, reason="busy")
            self.out("[受付なし] 前の操作が終わっていません（ためません）")
            self._finish(e)
            return
        e["accepted"] = True
        self.busy = True
        self.pending = e
        gate = SendGate()
        self.gate = gate
        snap = {"cmd": self.cmd.copy(), "cmd_reached": self.cmd_reached, "base": self.base.copy()}
        self.spawn(lambda: self._run_job(job, snap, gate))

    def _spawn_thread(self, fn):
        self.thread = threading.Thread(target=fn, daemon=True)
        self.thread.start()

    def _run_job(self, job, snap, gate):
        res = {}
        try:
            if job[0] == "move":
                _, j, d = job
                q = self.arm.read()
                res["measured_before"] = _r(q)
                if q is None:
                    res["error"] = "read_failed"
                    return
                # 出発点: 直前の目標に届いていて, 今読んだ角度とも 3° 以内ならその目標.
                # そうでなければ今読んだ角度 (手で動かされたなど. 古い目標から大きく動かさない. codex 必須 3)
                gap = float(np.max(np.abs(np.asarray(snap["cmd"]) - q)))
                use_cmd = snap["cmd_reached"] and gap <= REACH_TOL_DEG
                start = snap["cmd"] if use_cmd else q
                res["start_source"] = "command" if use_cmd else "measured"
                res["cmd_gap_deg"] = round(gap, 3)
                plan = plan_step(start, j, d, snap["base"])
                res["clip"] = plan["clip"]
                res["refused"] = plan["refused"]
                res["limit_ja"] = limit_text_ja(plan)
                res["limit_ascii"] = limit_text_ascii(plan)
                if plan["target"] is None:
                    res["sent"] = False
                    return
                res["command"] = _r(plan["target"])
                res["_target"] = plan["target"]
                res.update(self.arm.send_and_check(plan["target"], gate, self.quitting))
            elif job[0] == "home":
                q = self.arm.read()
                res["measured_before"] = _r(q)
                if q is None:                # 読めなければ送らない (codex 必須 6)
                    res["error"] = "read_failed"
                    return
                res["command"] = _r(HOME)
                res["_target"] = np.array(HOME)
                res["range_check"] = "skipped_for_home"
                res.update(self.arm.send_and_check(np.array(HOME), gate, self.quitting))
            elif job[0] == "grip":
                res["measured_before"] = None
                sent = self.arm.gripper(job[1], gate)
                res["sent"] = bool(sent)
                if not sent:
                    res["why"] = "halted_before_send"
        except Exception as ex:
            res["error"] = f"{type(ex).__name__}: {ex}"
        finally:
            self.results.put((job, res))

    def poll(self):
        """送る仕事の結果を受け取り, 状態とログに反映する (pygame のループから毎回呼ぶ)."""
        while True:
            try:
                job, res = self.results.get_nowait()
            except queue.Empty:
                return
            self._apply(job, res)

    def _apply(self, job, res):
        e = self.pending or self._entry(None, "orphan_result")
        self.pending = None
        self.busy = False
        target = res.pop("_target", None)
        e.update(res)
        if res.get("reached_angles") is not None:
            self.measured = np.asarray(res["reached_angles"], dtype=float)
        elif res.get("measured_before") is not None:
            self.measured = np.asarray(res["measured_before"], dtype=float)
        sent = res.get("sent")
        if job[0] in ("move", "home") and sent is True:
            self.n_sent += 1
            self.cmd = np.asarray(target, dtype=float)
            self.cmd_reached = bool(res.get("reached"))
            if job[0] == "home":
                self.base = np.array(HOME, dtype=float)    # HOME を送ったあとは HOME を基準にし直す
        elif sent == "unknown":
            self.cmd_reached = False                      # 次は読んだ角度から
        if job[0] == "grip" and res.get("error"):
            self.grip = "unknown"
        if job[0] == "grip" and sent is True:
            self.grip = "closed" if job[1] == 1 else "open"
        if job[0] == "move":
            self.last_limit = res.get("limit_ascii", "")
        e["reached"] = res.get("reached") if sent is True else None
        # ---- ターミナル ----
        if job[0] == "move":
            label = f"j{job[1]} {job[2]:+.1f}°"
        elif job[0] == "home":
            label = "HOME"
        else:
            label = "爪を" + ("閉じる" if job[1] == 1 else "開く")
        if res.get("limit_ja"):
            self.out(f"[範囲] {res['limit_ja']}")
        if res.get("error"):
            self.out(f"[エラー] {label}: {res['error']}。停止にします")
        elif sent is True and job[0] == "grip":
            self.out(f"[爪] {label}（待ち {rpc.GRIPPER_SLEEP:.1f} 秒のあと受付）")
        elif sent is True:
            r = res.get("reached")
            how = "届いた" if r else ("確かめる途中で終了" if r is None else "届かなかった")
            self.out(f"[送信] {label} → {_fmt(res.get('command'))}  {how}"
                     f"（ずれ {res.get('reach_err')}°）  届いた角度 {_fmt(res.get('reached_angles'))}")
        elif res.get("why") == "halted_before_send":
            self.out(f"[送らない] {label}: 停止が入ったので送りませんでした")
        else:
            self.out(f"[送らない] {label}")
        self.gate = None
        self._finish(e)
        if res.get("error"):
            self.enter_stop(f"arm_error: {res['error']}", kind="arm_error")

    # ---- 終わり ----
    def shutdown(self):
        """新しい指令を送らずに終わる. 送っている途中の仕事が戻るのを WORKER_JOIN_SEC まで待つ.
        戻り値: 仕事が終わっているか (終わっていなければ daemon のソケットを閉じない)."""
        self.quitting.set()
        committed = self._cancel_gate()
        if self.pending is not None:
            self.pending["send_committed_before_exit"] = committed
        if self.thread is not None:
            self.thread.join(WORKER_JOIN_SEC)
        self.poll()
        done = self.thread is None or not self.thread.is_alive()
        if self.pending is not None:
            e = self.pending
            self.pending = None
            e.update(reached=None, interrupted_at_exit=True)
            self._finish(e)
        return done

    def status_lines(self, q_now=None):
        """画面の文字 (英数字だけ)."""
        q = self.measured if q_now is None else q_now
        lines = [
            ("big", f"JOINT  j{self.joint}    STEP {self.step:.1f} deg"),
            ("big", f"STATE  {self.state}" + ("  (moving)" if self.stopped and self.busy else "")),
            ("big", f"GRIP   {self.grip}"),
        ]
        for k in range(6):
            lines.append(("mono", f"j{k}  {q[k]:+8.2f}   cmd {self.cmd[k]:+8.2f}   base {self.base[k]:+8.2f}"))
        lines.append(("small", "last limit: " + (self.last_limit or "-")))
        if self.stopped:
            lines.append(("small", f"stop reason: {self.stop_reason}  (OPTIONS = resume)"))
        return lines


# =============================================================================
# pygame のループ
# =============================================================================

WINDOW_SIZE = (900, 560)


def controller_info(js):
    return {"name": js.get_name(), "guid": js.get_guid(), "instance_id": js.get_instance_id(),
            "axes": js.get_numaxes(), "buttons": js.get_numbuttons(), "hats": js.get_numhats(),
            "connected_at": datetime.now().isoformat(timespec="seconds")}


def run_loop(tp, screen, fonts, mode_text, on_controller=None, on_frame=None):
    """ウィンドウを閉じるまで回す. 戻り値: 終わった理由."""
    js = None
    clock = pygame.time.Clock()
    frame = 0
    while True:
        if on_frame is not None:
            on_frame(frame, tp)
        frame += 1
        now = time.monotonic()
        events = pygame.event.get()
        buttons = []
        for ev in events:
            if ev.type == pygame.QUIT:
                return "window_closed"
            if ev.type == pygame.JOYDEVICEADDED:
                if js is None:
                    js = pygame.joystick.Joystick(ev.device_index)
                    info = controller_info(js)
                    tp.out(f"[接続] {info['name']}  GUID={info['guid']}  軸={info['axes']}  ボタン={info['buttons']}")
                    if on_controller is not None:
                        on_controller(info)
            elif ev.type == pygame.JOYDEVICEREMOVED:
                if js is not None and ev.instance_id == js.get_instance_id():
                    js = None
                    tp.controller_lost("controller_disconnected")
            elif ev.type in (pygame.JOYBUTTONDOWN, pygame.JOYBUTTONUP):
                if js is not None and ev.instance_id == js.get_instance_id():
                    buttons.append(ev)
        # 停止 (L2+R2) と 読み取りの失敗を ボタンより先に見る (同じコマのボタンで送らない. codex 必須 5)
        read_ok = js is not None
        if js is not None:
            try:
                l2, r2 = js.get_axis(AXIS_L2), js.get_axis(AXIS_R2)
                tp.controller_read_ok()
                tp.set_triggers(l2, r2)
            except Exception as ex:          # pygame.error など
                read_ok = False
                tp.controller_read_failed(f"{type(ex).__name__}: {ex}")
        if read_ok:
            for ev in buttons:
                if ev.type == pygame.JOYBUTTONDOWN:
                    tp.press(ev.button, now)
                else:
                    tp.release(ev.button, now)
        tp.tick(now)
        tp.poll()
        draw(screen, fonts, tp, mode_text, js)
        clock.tick(60)


def draw(screen, fonts, tp, mode_text, js):
    screen.fill((20, 20, 24))
    color_state = {STATE_IDLE: (120, 220, 140), STATE_MOVING: (240, 200, 80),
                   STATE_STOPPED: (240, 90, 90)}[tp.state]
    y = 10
    screen.blit(fonts["small"].render(f"{mode_text}   controller: "
                                      + (js.get_name()[:40] if js is not None else "none"),
                                      True, (200, 220, 255)), (10, y))
    y += 28
    for kind, text in tp.status_lines():
        col = color_state if text.startswith("STATE") else (235, 235, 235)
        screen.blit(fonts[kind].render(text[:110], True, col), (10, y))
        y += {"big": 44, "mono": 26, "small": 24}[kind]
    y += 6
    for t in ("dpad L/R: j0   dpad U/D: selected joint (up = -, down = +)   triangle: next joint",
              "R1 hold: 2 deg   cross: close   circle: open   HOME hold 1s: home",
              "L2+R2: stop   OPTIONS: resume   window close / Ctrl+C: quit (arm is not moved)"):
        screen.blit(fonts["small"].render(t, True, (150, 150, 160)), (10, y))
        y += 22
    pygame.display.flip()


# =============================================================================
# main
# =============================================================================

def git_info(path):
    def run(*a):
        try:
            return subprocess.run(["git", *a], cwd=_ROOT, capture_output=True, text=True,
                                  timeout=5).stdout.strip()
        except Exception:
            return None
    rel = os.path.relpath(path, _ROOT)
    return {"commit": run("rev-parse", "HEAD"),
            "script_status": run("status", "--porcelain", "--", rel) or "clean"}


def main(argv=None, on_frame=None, start_pose_dry=None):
    ap = argparse.ArgumentParser(description="ゲームパッドで mechArm を手で動かす (既定はドライラン)")
    ap.add_argument("--execute", action="store_true", help="daemon に送る (付けなければドライラン)")
    args = ap.parse_args(argv)

    ensure_ssd_mounted()
    stamp = datetime.now().strftime("%Y%m%d_%H%M%S")
    run_stem = f"teleop_{stamp}" + ("" if args.execute else "_dry")
    run_dir = LOG_ROOT / run_stem
    if run_dir.exists():
        sys.exit(f"[ABORT] 同じ名前のフォルダがあります: {run_dir}")
    run_dir.mkdir(parents=True)
    script_copy = run_dir / Path(__file__).name
    shutil.copy2(__file__, script_copy)
    log = JsonlLog(run_dir / "log.jsonl")
    print(f"[記録] {run_dir}")

    tp = None
    meta = None
    end_reason = None
    daemon_used = False
    try:
        if args.execute:
            get_daemon_socket()
            daemon_used = True
            rpc.install_daemon_lock()
            arm = DaemonArm()
            start = arm.read()
            if start is None:
                end_reason = "start_read_failed"
                sys.exit("[ABORT] 起動したときの関節角を読めません（daemon を確かめてください）。何も送らずに終わります")
            print("[実機] daemon に送ります。起動したときの姿勢: " + _fmt(start))
        else:
            start = np.asarray(start_pose_dry if start_pose_dry is not None else HOME, dtype=float)
            arm = DryArm(start)
            print("[ドライラン] daemon にもアームにも触りません。HOME から始まる仮の姿勢を動かします")
        lo, hi, _, _ = joint_window(start)
        bad = [k for k in range(6) if not (lo[k] - LIMIT_EPS_DEG <= start[k] <= hi[k] + LIMIT_EPS_DEG)]
        if bad:
            print("[注意] 起動したときの姿勢が守りの外: " + ", ".join(f"j{k}={start[k]:+.2f}" for k in bad)
                  + "。この関節を含む操作は送りません（HOME の長押しは送れます）")

        meta = {
            "run_stem": run_stem, "started_at": datetime.now().isoformat(timespec="seconds"),
            "execute": bool(args.execute), "argv": sys.argv,
            "controllers": [],
            "python": sys.version, "pygame": pygame.version.ver,
            "sdl": ".".join(str(v) for v in pygame.get_sdl_version()), "platform": platform.platform(),
            "start_pose": _r(start),
            "guards": {"J1_MAX_DEG": J1_MAX_DEG, "J0_RANGE_DEG": J0_RANGE_DEG, "JOINT_RANGE_DEG": JOINT_RANGE_DEG,
                       "J4_SAFE": list(J4_SAFE), "JOINT_LIMITS": [list(x) for x in JOINT_LIMITS],
                       "window_at_start": {"lo": _r(lo), "hi": _r(hi)},
                       "home_bypasses_range_check": True},
            "motion": {"speed": SPEED, "step_fine_deg": STEP_FINE_DEG, "step_coarse_deg": STEP_COARSE_DEG,
                       "reach_tol_deg": REACH_TOL_DEG, "reach_timeout_min_sec": REACH_TIMEOUT_SEC,
                       "poll_sec": POLL_SLEEP, "gripper_sleep_sec": rpc.GRIPPER_SLEEP,
                       "grip_close_value": rpc.GRIP_VALUE, "send_wait": rpc.SEND_WAIT,
                       "home": HOME, "home_hold_sec": HOME_HOLD_SEC, "trigger_stop_above": TRIGGER_STOP_ABOVE,
                       "j0_left_sign": J0_LEFT_SIGN, "up_sign": UP_SIGN, "select_order": list(SELECT_ORDER)},
            "button_map": {str(k): v for k, v in BUTTON_NAMES.items()},
            "git": git_info(__file__),
            "script_copy": script_copy.name,
            "script_sha256": hashlib.sha256(script_copy.read_bytes()).hexdigest(),
        }
        write_json_atomic(run_dir / "meta.json", meta)

        def on_controller(info):
            meta["controllers"].append(info)
            try:
                write_json_atomic(run_dir / "meta.json", meta)
            except Exception as ex:
                print(f"[記録エラー] meta.json に書けません: {ex}")

        tp = Teleop(arm, start, log)
        pygame.init()
        pygame.joystick.init()
        screen = pygame.display.set_mode(WINDOW_SIZE)
        pygame.display.set_caption("gamepad_teleop" + (" [EXECUTE]" if args.execute else " [DRY RUN]"))
        mono = "menlo,consolas,dejavusansmono"
        fonts = {"big": pygame.font.SysFont(mono, 34, bold=True), "mono": pygame.font.SysFont(mono, 20),
                 "small": pygame.font.SysFont(mono, 16)}
        print("コントローラーを待っています（つないでいなければ、今つないでください）")
        end_reason = run_loop(tp, screen, fonts, "EXECUTE" if args.execute else "DRY RUN",
                              on_controller=on_controller, on_frame=on_frame)
    except KeyboardInterrupt:
        end_reason = "ctrl_c"
        print("\n[終了] Ctrl+C")
    finally:
        # 終わるときにアームを動かさない (HOME 復帰もサーボ解放もしない). 新しい指令も送らない
        worker_done = True
        if tp is not None:
            worker_done = tp.shutdown()
            if not worker_done:
                print("[終了] 送っている途中の仕事が戻らないまま終わります（ソケットは閉じません）")
        if meta is not None:
            meta.update(ended_at=datetime.now().isoformat(timespec="seconds"), end_reason=end_reason or "error",
                        n_sent=(tp.n_sent if tp is not None else 0))
            try:
                write_json_atomic(run_dir / "meta.json", meta)
            except Exception as ex:
                print(f"[記録エラー] meta.json に書けません: {ex}")
        log.close()
        if daemon_used and worker_done:
            try:
                close_daemon_socket()
            except Exception:
                pass
        pygame.quit()
        print(f"[終了] アームは動かしていません。記録: {run_dir}")
    return 0


if __name__ == "__main__":
    sys.exit(main())
```

### test_gamepad_teleop.py（テスト46件）

```python
#!/usr/bin/env python3
"""test_gamepad_teleop.py — gamepad_teleop.py の確かめ (アームなし・コントローラーなし. 依頼 68).

  python3 tools/test_gamepad_teleop.py -v

中身:
  - ボタン番号と操作の対応 (依頼 68 の表) / 十字キー上下の向き / △ で j1 → j5 → j1
  - 守り: j1 は 90.87 を超えない / j0 ±15°・j1〜j5 ±10° / J4_SAFE / HOME のあと基準が HOME
  - 停止: L2・R2 の両方 > 0.0 で止まる・片方では止まらない / 停止中は受け付けない / OPTIONS で戻る / 接続が切れたら停止
  - HOME: 1 秒に満たない長押しでは送らない / 1 秒続けたら 1 回だけ送る
  - 動いている間の押しは受け付けない (ためない)
  - daemon への送り方: 停止が立っていれば送らない / 届かなくても送り直さない
  - ドライランで main() がひととおり動く (偽のコントローラー・ダミーの画面・一時フォルダ)
  - codex R1 の指摘: 範囲外の出発点 / 目標と読んだ角度のずれ / 長押しの持ち越し / 停止と送信の順番 /
    再開で古い仕事が生き返らない / 読み取り失敗 / 記録の失敗 / ボタンより先に L2+R2 を見る
KIOXIA にもアームにも触らない.
"""
import json
import os
import sys
import tempfile
import threading
import unittest
from pathlib import Path
from unittest import mock

os.environ.setdefault("SDL_VIDEODRIVER", "dummy")
os.environ.setdefault("SDL_AUDIODRIVER", "dummy")

import numpy as np  # noqa: E402

sys.path.insert(0, str(Path(__file__).resolve().parent))
import gamepad_teleop as gt  # noqa: E402

HOME = [0.0, 0.0, 0.0, 0.0, 0.0, -30.0]


class ListLog:
    def __init__(self):
        self.rows = []

    def write(self, rec):
        json.dumps(rec, ensure_ascii=False)      # JSON にできること
        self.rows.append(rec)


class FakeArm:
    execute = False

    def __init__(self, start=HOME, reach=True):
        self.q = np.asarray(start, dtype=float).copy()
        self.sent = []
        self.grips = []
        self.reach = reach

    def read(self):
        return self.q.copy()

    def send_and_check(self, target, gate, quitting):
        if not gate.run(lambda: self.sent.append([float(v) for v in target])):
            return {"sent": False, "why": "halted_before_send"}
        if self.reach:
            self.q = np.asarray(target, dtype=float).copy()
        return {"sent": True, "reached": self.reach, "reached_angles": gt._r(self.q), "reach_err": 0.0}

    def gripper(self, state, gate):
        return gate.run(lambda: self.grips.append(state))


def make(start=HOME, reach=True, sync=True):
    arm = FakeArm(start, reach)
    log = ListLog()
    spawned = []
    spawn = (lambda fn: fn()) if sync else spawned.append
    tp = gt.Teleop(arm, start, log, out=lambda *a: None, spawn=spawn)
    return tp, arm, log, spawned


def press(tp, b, now=0.0):
    tp.press(b, now)
    tp.release(b, now)
    tp.poll()


class TestButtonMap(unittest.TestCase):
    def test_numbers(self):
        self.assertEqual((gt.BTN_CROSS, gt.BTN_CIRCLE, gt.BTN_SQUARE, gt.BTN_TRIANGLE), (0, 1, 2, 3))
        self.assertEqual((gt.BTN_SHARE, gt.BTN_HOME, gt.BTN_OPTIONS, gt.BTN_L3, gt.BTN_R3), (4, 5, 6, 7, 8))
        self.assertEqual((gt.BTN_L1, gt.BTN_R1), (9, 10))
        self.assertEqual((gt.BTN_UP, gt.BTN_DOWN, gt.BTN_LEFT, gt.BTN_RIGHT, gt.BTN_TOUCHPAD), (11, 12, 13, 14, 15))
        self.assertEqual((gt.AXIS_L2, gt.AXIS_R2), (4, 5))

    def test_dpad_left_right_always_j0(self):
        for sel in range(5):
            tp, arm, log, _ = make()
            tp.sel = sel
            press(tp, gt.BTN_RIGHT)
            self.assertEqual(log.rows[-1]["joint"], 0)
            self.assertAlmostEqual(arm.sent[-1][0], 0.5)
            press(tp, gt.BTN_LEFT)
            press(tp, gt.BTN_LEFT)
            self.assertAlmostEqual(arm.sent[-1][0], -0.5)
            self.assertEqual(arm.sent[-1][1:], HOME[1:])

    def test_up_minus_down_plus_starts_j1(self):
        tp, arm, log, _ = make()
        self.assertEqual(tp.joint, 1)
        press(tp, gt.BTN_DOWN)
        self.assertAlmostEqual(arm.sent[-1][1], 0.5)
        press(tp, gt.BTN_UP)
        press(tp, gt.BTN_UP)
        self.assertAlmostEqual(arm.sent[-1][1], -0.5)

    def test_triangle_cycles(self):
        tp, arm, log, _ = make()
        seen = [tp.joint]
        for _ in range(5):
            press(tp, gt.BTN_TRIANGLE)
            seen.append(tp.joint)
        self.assertEqual(seen, [1, 2, 3, 4, 5, 1])
        self.assertEqual(arm.sent, [])                      # 切り替えだけでは送らない
        tp.sel = 3                                           # j4
        press(tp, gt.BTN_DOWN)
        self.assertAlmostEqual(arm.sent[-1][4], 0.5)

    def test_r1_coarse_step(self):
        tp, arm, log, _ = make()
        tp.press(gt.BTN_R1, 0.0)
        press(tp, gt.BTN_RIGHT)
        self.assertAlmostEqual(arm.sent[-1][0], 2.0)
        press(tp, gt.BTN_DOWN)
        self.assertAlmostEqual(arm.sent[-1][1], 2.0)
        tp.release(gt.BTN_R1, 0.0)
        press(tp, gt.BTN_RIGHT)
        self.assertAlmostEqual(arm.sent[-1][0], 2.5)

    def test_gripper(self):
        tp, arm, log, _ = make()
        press(tp, gt.BTN_CROSS)
        self.assertEqual(arm.grips, [1])
        self.assertEqual(tp.grip, "closed")
        press(tp, gt.BTN_CIRCLE)
        self.assertEqual(arm.grips, [1, 0])
        self.assertEqual(tp.grip, "open")

    def test_unassigned_do_nothing(self):
        tp, arm, log, _ = make()
        for b in (gt.BTN_SQUARE, gt.BTN_SHARE, gt.BTN_L1, gt.BTN_L3, gt.BTN_R3, gt.BTN_TOUCHPAD):
            press(tp, b)
        self.assertEqual(arm.sent, [])
        self.assertEqual(arm.grips, [])
        self.assertTrue(all(r["kind"] == "ignored" for r in log.rows))
        self.assertEqual(tp.joint, 1)


class TestGuards(unittest.TestCase):
    def test_j1_never_above_9087(self):
        start = [0.0, 85.0, 0.0, 0.0, 0.0, -30.0]
        tp, arm, log, _ = make(start)
        tp.r1 = True
        for _ in range(6):
            press(tp, gt.BTN_DOWN)
        self.assertTrue(all(s[1] <= 90.87 + 1e-9 for s in arm.sent))
        self.assertAlmostEqual(arm.sent[-1][1], 90.87)
        clipped = [r for r in log.rows if r.get("clip")]
        self.assertEqual(clipped[0]["clip"][0]["limit"], "J1_MAX")
        refused = [r for r in log.rows if r.get("refused")]
        self.assertTrue(refused and refused[-1]["refused"][0]["why"] == "at_limit")
        self.assertIn("J1_MAX", tp.last_limit)

    def test_j0_15_and_others_10(self):
        tp, arm, log, _ = make()
        tp.r1 = True
        for _ in range(12):
            press(tp, gt.BTN_RIGHT)
        self.assertAlmostEqual(max(s[0] for s in arm.sent), 15.0)
        for _ in range(20):
            press(tp, gt.BTN_LEFT)
        self.assertAlmostEqual(min(s[0] for s in arm.sent), -15.0)
        tp.sel = 1                                          # j2
        for _ in range(10):
            press(tp, gt.BTN_UP)
        self.assertAlmostEqual(min(s[2] for s in arm.sent), -10.0)
        tp.sel = 4                                          # j5 (基準 -30)
        for _ in range(10):
            press(tp, gt.BTN_DOWN)
        self.assertAlmostEqual(max(s[5] for s in arm.sent), -20.0)

    def test_j4_safe(self):
        start = [0.0, 0.0, 0.0, 0.0, 55.0, -30.0]
        tp, arm, log, _ = make(start)
        tp.sel = 3
        tp.r1 = True
        for _ in range(5):
            press(tp, gt.BTN_DOWN)
        self.assertAlmostEqual(max(s[4] for s in arm.sent), 60.0)
        self.assertEqual([r for r in log.rows if r.get("clip")][0]["clip"][0]["limit"], "J4_SAFE")

    def test_window_function(self):
        lo, hi, lo_src, hi_src = gt.joint_window([0, 85, 0, 0, 55, -30])
        self.assertAlmostEqual(hi[1], 90.87)
        self.assertEqual(hi_src[1], "J1_MAX")
        self.assertAlmostEqual(hi[4], 60.0)
        self.assertEqual(hi_src[4], "J4_SAFE")
        self.assertAlmostEqual(lo[0], -15.0)
        self.assertAlmostEqual(hi[2], 10.0)

    def test_no_compensation_other_joints(self):
        start = [0.0, 0.0, 0.0, 0.0, 0.0, -30.0]
        p = gt.plan_step(start, 1, 20.0, start)
        self.assertAlmostEqual(p["target"][1], 10.0)
        self.assertEqual(list(p["target"][[0, 2, 3, 4, 5]]), [0.0, 0.0, 0.0, 0.0, -30.0])

    def test_out_of_window_start_refuses(self):
        # 起動したときに j1 が 91 (守りの外): j1 を下げる (−) 押しも, 他の関節の押しも送らない
        start = [0.0, 91.0, 0.0, 0.0, 0.0, -30.0]
        p = gt.plan_step(start, 0, 0.5, start)
        self.assertIsNone(p["target"])
        self.assertEqual(p["refused"][0]["joint"], 1)

    def test_home_rebases(self):
        start = [10.0, 5.0, 0.0, 0.0, 0.0, -30.0]
        tp, arm, log, _ = make(start)
        tp.press(gt.BTN_HOME, 0.0)
        tp.tick(1.01)
        tp.poll()
        self.assertEqual(arm.sent[-1], HOME)
        self.assertEqual(list(tp.base), HOME)
        tp.r1 = True
        for _ in range(10):
            press(tp, gt.BTN_LEFT)
        self.assertAlmostEqual(arm.sent[-1][0], -15.0)       # 基準が HOME (j0=0) になった

    def test_home_bypasses_range(self):
        # 範囲の外 (j1=95) にいても HOME は送れる
        start = [0.0, 95.0, 0.0, 0.0, 0.0, -30.0]
        tp, arm, log, _ = make(start)
        press(tp, gt.BTN_DOWN)
        self.assertEqual(arm.sent, [])
        tp.press(gt.BTN_HOME, 0.0)
        tp.tick(1.0)
        tp.poll()
        self.assertEqual(arm.sent, [HOME])

    def test_start_from_measured_when_not_reached(self):
        tp, arm, log, _ = make(reach=False)
        press(tp, gt.BTN_RIGHT)                    # 届かない (仮の姿勢は HOME のまま)
        self.assertFalse(tp.cmd_reached)
        press(tp, gt.BTN_RIGHT)                    # 次は読んだ角度 (0) から
        self.assertAlmostEqual(arm.sent[-1][0], 0.5)
        self.assertEqual(log.rows[-1]["start_source"], "measured")


class TestCodexR1(unittest.TestCase):
    def test_selected_joint_out_of_window_not_clamped(self):
        # 基準 j1=100 (守りの外) から −0.5 を押しても 90.87 へ 1 回で動かさない
        start = [0.0, 100.0, 0.0, 0.0, 0.0, -30.0]
        p = gt.plan_step(start, 1, -0.5, start)
        self.assertIsNone(p["target"])
        self.assertEqual(p["refused"][0]["why"], "start_out_of_window")
        tp, arm, log, _ = make(start)
        press(tp, gt.BTN_UP)
        self.assertEqual(arm.sent, [])

    def test_physical_limit_ends(self):
        base = [0.0, 0.0, 170.0, 0.0, 0.0, -30.0]       # j2 の物理可動域は ±177
        p = gt.plan_step(base, 2, 20.0, base)
        self.assertAlmostEqual(p["target"][2], 177.0)
        self.assertEqual(p["clip"][0]["limit"], "PHYS")

    def test_gap_between_command_and_measured(self):
        tp, arm, log, _ = make()
        press(tp, gt.BTN_RIGHT)                         # 目標 j0=0.5 に届いた
        arm.q[0] = 10.0                                 # 手で動かされた
        press(tp, gt.BTN_RIGHT)
        self.assertAlmostEqual(arm.sent[-1][0], 10.5)   # 今読んだ角度から 0.5
        self.assertEqual(log.rows[-1]["start_source"], "measured")
        arm.q[0] = -20.0                                # 基準から ±15 の外 → 送らない
        press(tp, gt.BTN_RIGHT)
        self.assertEqual(len(arm.sent), 2)

    def test_home_hold_across_busy_is_dropped(self):
        tp, arm, log, spawned = make(sync=False)
        press(tp, gt.BTN_RIGHT)                         # 動いている
        tp.press(gt.BTN_HOME, 0.0)                      # busy 中に押し始め
        spawned.pop()()
        tp.poll()                                       # 0.5 秒で空く
        tp.tick(1.5)
        self.assertEqual(spawned, [])
        self.assertIn(("home", False, "busy"), [(r["kind"], r.get("accepted"), r.get("reason")) for r in log.rows])

    def test_home_hold_dropped_when_other_op_starts(self):
        tp, arm, log, spawned = make(sync=False)
        tp.press(gt.BTN_HOME, 0.0)
        press(tp, gt.BTN_RIGHT)                         # 長押しの途中で j0 を動かした
        tp.tick(0.2)
        spawned.pop()()
        tp.poll()
        tp.tick(1.5)
        self.assertEqual(spawned, [])
        self.assertIn("hold_cancelled", [r.get("reason") for r in log.rows])

    def test_home_hold_dropped_by_stop_and_disconnect(self):
        tp, arm, log, _ = make()
        tp.press(gt.BTN_HOME, 0.0)
        tp.set_triggers(1, 1)
        tp.set_triggers(-1, -1)
        press(tp, gt.BTN_OPTIONS)
        tp.tick(1.5)
        self.assertEqual(arm.sent, [])
        tp.r1 = True
        tp.press(gt.BTN_HOME, 2.0)
        tp.controller_lost("controller_disconnected")
        self.assertFalse(tp.r1)
        press(tp, gt.BTN_OPTIONS)
        tp.tick(5.0)
        self.assertEqual(arm.sent, [])

    def test_resume_does_not_revive_old_job(self):
        tp, arm, log, spawned = make(sync=False)
        press(tp, gt.BTN_RIGHT)
        tp.set_triggers(1, 1)
        tp.set_triggers(-1, -1)
        press(tp, gt.BTN_OPTIONS)                       # すぐ再開
        self.assertFalse(tp.stopped)
        spawned.pop()()                                 # 止める前の仕事が走る
        tp.poll()
        self.assertEqual(arm.sent, [])

    def test_home_read_failure_does_not_send(self):
        tp, arm, log, _ = make([5.0, 0, 0, 0, 0, -30])
        reads = iter([None, np.array(HOME)])
        arm.read = lambda: next(reads)
        tp.press(gt.BTN_HOME, 0.0)
        tp.tick(1.0)
        tp.poll()
        self.assertEqual(arm.sent, [])
        self.assertTrue(tp.stopped)

    def test_log_failure_blocks_resume(self):
        tp, arm, log, _ = make()

        def bad(rec):
            raise OSError("disk gone")
        log.write = bad
        press(tp, gt.BTN_TRIANGLE)
        self.assertTrue(tp.stopped)
        press(tp, gt.BTN_OPTIONS)
        self.assertTrue(tp.stopped)
        press(tp, gt.BTN_RIGHT)
        self.assertEqual(arm.sent, [])

    def test_loop_reads_triggers_before_buttons(self):
        import pygame
        pygame.init()
        screen = pygame.display.set_mode((200, 100))
        fonts = {k: pygame.font.SysFont("menlo", 12) for k in ("big", "mono", "small")}
        js = FakeJoystick(0)
        tp, arm, log, _ = make()
        frames = {
            0: lambda: pygame.event.post(pygame.event.Event(pygame.JOYDEVICEADDED, device_index=0)),
            2: lambda: (js.axes.__setitem__(slice(4, 6), [1.0, 1.0]),
                        pygame.event.post(pygame.event.Event(pygame.JOYBUTTONDOWN, instance_id=7, button=gt.BTN_RIGHT, joy=0))),
            4: lambda: pygame.event.post(pygame.event.Event(pygame.QUIT)),
        }
        with mock.patch.object(gt.pygame.joystick, "Joystick", lambda i: js):
            gt.run_loop(tp, screen, fonts, "TEST", on_frame=lambda n, t: frames.get(n, lambda: None)())
        self.assertEqual(arm.sent, [])
        self.assertTrue(tp.stopped)
        # 読み取りに失敗したコマのボタンも処理しない
        js2 = FakeJoystick(0)
        js2.get_axis = lambda a: (_ for _ in ()).throw(pygame.error("gone"))
        tp2, arm2, log2, _ = make()
        frames2 = {
            0: lambda: pygame.event.post(pygame.event.Event(pygame.JOYDEVICEADDED, device_index=0)),
            2: lambda: pygame.event.post(pygame.event.Event(pygame.JOYBUTTONDOWN, instance_id=7, button=gt.BTN_RIGHT, joy=0)),
            4: lambda: pygame.event.post(pygame.event.Event(pygame.QUIT)),
        }
        with mock.patch.object(gt.pygame.joystick, "Joystick", lambda i: js2):
            gt.run_loop(tp2, screen, fonts, "TEST", on_frame=lambda n, t: frames2.get(n, lambda: None)())
        self.assertEqual(arm2.sent, [])
        self.assertTrue(tp2.stopped)
        pygame.quit()


class TestShutdown(unittest.TestCase):
    def test_uncommitted_job_is_not_sent_at_exit(self):
        tp, arm, log, spawned = make(sync=False)
        press(tp, gt.BTN_RIGHT)
        tp.shutdown()                                # 仕事はまだ走っていない
        spawned.pop()()                              # 終了のあとに走っても送らない
        self.assertEqual(arm.sent, [])
        last = [r for r in log.rows if r["kind"] == "move"][0]
        self.assertIs(last["send_committed_before_exit"], False)
        self.assertTrue(last["interrupted_at_exit"])

    def test_committed_send_is_waited_and_recorded(self):
        tp, arm, log, _ = make(sync=False)
        release = threading.Event()
        entered = threading.Event()
        orig = arm.send_and_check

        def slow(target, gate, quitting):
            def fn():
                entered.set()
                release.wait(2.0)
                arm.sent.append([float(v) for v in target])
            if not gate.run(fn):
                return {"sent": False, "why": "halted_before_send"}
            return {"sent": True, "reached": None, "interrupted": True}
        arm.send_and_check = slow
        tp.spawn = tp._spawn_thread
        press(tp, gt.BTN_RIGHT)
        entered.wait(1.0)
        threading.Timer(0.2, release.set).start()
        done = tp.shutdown()                          # 送ると決めた送信が戻るまで待つ
        self.assertTrue(done)
        self.assertEqual(len(arm.sent), 1)
        move = [r for r in log.rows if r["kind"] == "move"][0]
        self.assertIs(move["send_committed_before_exit"], True)

    def test_worker_not_done_returns_false(self):
        tp, arm, log, _ = make(sync=False)
        entered = threading.Event()

        def stuck(target, gate, quitting):
            gate.run(lambda: (entered.set(), threading.Event().wait(1.0)))
            return {"sent": True}
        arm.send_and_check = stuck
        tp.spawn = tp._spawn_thread
        press(tp, gt.BTN_RIGHT)
        entered.wait(1.0)
        with mock.patch.object(gt, "WORKER_JOIN_SEC", 0.1):
            self.assertFalse(tp.shutdown())           # 戻らない → main はソケットを閉じない


class TestStop(unittest.TestCase):
    def test_both_triggers(self):
        tp, arm, log, _ = make()
        tp.set_triggers(0.5, -1.0)
        self.assertFalse(tp.stopped)
        tp.set_triggers(-1.0, 0.9)
        self.assertFalse(tp.stopped)
        tp.set_triggers(0.0, 0.0)                  # ちょうど 0.0 は超えていない
        self.assertFalse(tp.stopped)
        tp.set_triggers(0.01, 0.01)
        self.assertTrue(tp.stopped)
        self.assertEqual(tp.state, gt.STATE_STOPPED)

    def test_stopped_rejects_and_options_resumes(self):
        tp, arm, log, _ = make()
        tp.set_triggers(1.0, 1.0)
        for b in (gt.BTN_RIGHT, gt.BTN_DOWN, gt.BTN_CROSS, gt.BTN_CIRCLE):
            press(tp, b)
        tp.press(gt.BTN_HOME, 0.0)
        tp.tick(2.0)
        tp.release(gt.BTN_HOME, 2.0)
        self.assertEqual(arm.sent, [])
        self.assertEqual(arm.grips, [])
        self.assertTrue(all(r.get("reason") == "stopped" for r in log.rows
                            if r["kind"] in ("move", "grip_close", "grip_open", "home")))
        press(tp, gt.BTN_OPTIONS)                  # まだ L2+R2 を押している → 戻らない
        self.assertTrue(tp.stopped)
        tp.set_triggers(-1.0, -1.0)
        press(tp, gt.BTN_OPTIONS)
        self.assertFalse(tp.stopped)
        press(tp, gt.BTN_RIGHT)
        self.assertEqual(len(arm.sent), 1)

    def test_disconnect_and_read_error(self):
        tp, arm, log, _ = make()
        tp.controller_lost("controller_disconnected")
        self.assertTrue(tp.stopped)
        self.assertEqual(log.rows[-1]["kind"], "controller_lost")
        tp2, arm2, log2, _ = make()
        tp2.controller_read_failed("x")
        tp2.controller_read_failed("x")
        self.assertTrue(tp2.stopped)
        self.assertEqual(sum(r["kind"] == "read_error" for r in log2.rows), 1)
        press(tp2, gt.BTN_OPTIONS)                 # 読めないうちは戻らない
        self.assertTrue(tp2.stopped)
        tp2.controller_read_ok()
        press(tp2, gt.BTN_OPTIONS)
        self.assertFalse(tp2.stopped)

    def test_stop_while_moving_blocks_send(self):
        tp, arm, log, spawned = make(sync=False)
        press(tp, gt.BTN_RIGHT)                    # 仕事を作ったが, まだ走らせていない
        self.assertTrue(tp.busy)
        tp.set_triggers(1.0, 1.0)                  # 動いている間でも停止は受け付ける
        self.assertTrue(tp.stopped)
        spawned.pop()()                            # 仕事が走る: 送る直前に停止を見て送らない
        tp.poll()
        self.assertEqual(arm.sent, [])
        self.assertFalse(tp.busy)
        move = [r for r in log.rows if r["kind"] == "move"][0]
        self.assertEqual(move["why"], "halted_before_send")


class TestHomeHold(unittest.TestCase):
    def test_short_press_does_not_send(self):
        tp, arm, log, _ = make()
        tp.press(gt.BTN_HOME, 10.0)
        tp.tick(10.5)
        tp.tick(10.99)
        tp.release(gt.BTN_HOME, 10.99)
        tp.poll()
        self.assertEqual(arm.sent, [])
        self.assertEqual(log.rows[-1]["kind"], "home_short")

    def test_hold_sends_once(self):
        tp, arm, log, _ = make([5.0, 0, 0, 0, 0, -30])
        tp.press(gt.BTN_HOME, 0.0)
        for t in (0.5, 1.0, 1.5, 3.0, 5.0):
            tp.tick(t)
            tp.poll()
        tp.release(gt.BTN_HOME, 5.0)
        self.assertEqual(arm.sent, [HOME])
        self.assertEqual(sum(r["kind"] == "home" for r in log.rows), 1)
        self.assertFalse(any(r["kind"] == "home_short" for r in log.rows))
        tp.press(gt.BTN_HOME, 6.0)                 # 押し直せば, また 1 回
        tp.tick(7.0)
        tp.poll()
        self.assertEqual(len(arm.sent), 2)


class TestBusy(unittest.TestCase):
    def test_presses_while_moving_are_dropped(self):
        tp, arm, log, spawned = make(sync=False)
        press(tp, gt.BTN_RIGHT)
        for b in (gt.BTN_RIGHT, gt.BTN_DOWN, gt.BTN_CROSS):
            press(tp, b)
        tp.press(gt.BTN_HOME, 0.0)
        tp.tick(1.5)
        self.assertEqual(len(spawned), 1)           # 仕事は 1 つだけ
        spawned.pop()()
        tp.poll()
        tp.tick(3.0)                                # 長押しのまま空いても, ためた HOME は出ない
        self.assertEqual(spawned, [])
        self.assertEqual(arm.sent, [[0.5, 0, 0, 0, 0, -30]])
        busy = [r for r in log.rows if r.get("reason") == "busy"]
        self.assertEqual(len(busy), 4)


class TestDaemonArm(unittest.TestCase):
    def _arm(self, reads):
        it = iter(reads)
        last = [None]

        def read():
            try:
                last[0] = next(it)
            except StopIteration:
                pass
            return None if last[0] is None else np.asarray(last[0], dtype=float)
        return read

    def test_halt_blocks_send(self):
        sends = []
        with mock.patch.object(gt.rpc, "read_angles", self._arm([HOME])), \
                mock.patch.object(gt, "daemon_send_angles", lambda *a, **k: sends.append(a)):
            gate, quit_ = gt.SendGate(), threading.Event()
            gate.cancel_now()
            r = gt.DaemonArm().send_and_check(np.array(HOME), gate, quit_)
        self.assertEqual(sends, [])
        self.assertFalse(r["sent"])

    def test_single_send_no_retry(self):
        sends = []
        with mock.patch.object(gt.rpc, "read_angles", self._arm([HOME])), \
                mock.patch.object(gt, "daemon_send_angles", lambda a, **k: sends.append((a, k))), \
                mock.patch.object(gt, "REACH_TIMEOUT_SEC", 0.3), \
                mock.patch.object(gt.wr, "_compute_arrival_timeout", lambda d: 0.1), \
                mock.patch.object(gt, "POLL_SLEEP", 0.05):
            r = gt.DaemonArm().send_and_check(np.array([5.0, 0, 0, 0, 0, -30]),
                                              gt.SendGate(), threading.Event())
        self.assertEqual(len(sends), 1)
        self.assertEqual(sends[0][1]["speed"], 20)
        self.assertFalse(r["reached"])
        self.assertTrue(r["sent"])

    def test_reached(self):
        target = [1.0, 0, 0, 0, 0, -30]
        with mock.patch.object(gt.rpc, "read_angles", self._arm([HOME, [0.5, 0, 0, 0, 0, -30]])), \
                mock.patch.object(gt, "daemon_send_angles", lambda a, **k: None):
            r = gt.DaemonArm().send_and_check(np.array(target), gt.SendGate(), threading.Event())
        self.assertTrue(r["reached"])
        self.assertEqual(r["reach_err"], 0.5)

    def test_reach_uses_unrounded_error(self):
        target = [3.004, 0, 0, 0, 0, -30]
        with mock.patch.object(gt.rpc, "read_angles", self._arm([HOME])), \
                mock.patch.object(gt, "daemon_send_angles", lambda a, **k: None), \
                mock.patch.object(gt, "REACH_TIMEOUT_SEC", 0.2), \
                mock.patch.object(gt.wr, "_compute_arrival_timeout", lambda d: 0.1), \
                mock.patch.object(gt, "POLL_SLEEP", 0.05):
            r = gt.DaemonArm().send_and_check(np.array(target), gt.SendGate(), threading.Event())
        self.assertFalse(r["reached"])

    def test_read_none_during_check_is_error(self):
        with mock.patch.object(gt.rpc, "read_angles", self._arm([HOME, None])), \
                mock.patch.object(gt, "daemon_send_angles", lambda a, **k: None):
            r = gt.DaemonArm().send_and_check(np.array([5.0, 0, 0, 0, 0, -30]), gt.SendGate(), threading.Event())
        self.assertTrue(r["sent"])
        self.assertEqual(r["error"], "read_failed_during_check")
        self.assertIsNone(r["reached"])

    def test_stop_does_not_wait_and_reports_commit(self):
        # 送ると決めて送信 (通信) の最中に停止が来ても, 停止は待たない. 決めていたことを返す.
        # 停止のあとに「送ると決める」ことはない
        gate = gt.SendGate()
        entered, release = threading.Event(), threading.Event()

        def slow_send():
            entered.set()
            release.wait(2.0)
        t = threading.Thread(target=lambda: gate.run(slow_send))
        t.start()
        entered.wait(1.0)
        got = []
        stopper = threading.Thread(target=lambda: got.append(gate.cancel_now()))
        stopper.start()
        stopper.join(0.5)
        self.assertFalse(stopper.is_alive())         # 待たされない
        self.assertEqual(got, [True])                # 停止より前に送ると決めていた
        release.set()
        t.join(2.0)
        self.assertFalse(gate.run(lambda: self.fail("取り消しのあとに送った")))
        g2 = gt.SendGate()
        self.assertFalse(g2.cancel_now())            # まだ決めていない仕事は送らない
        self.assertFalse(g2.run(lambda: self.fail("取り消しのあとに送った")))

    def test_send_uses_rpc_timeout(self):
        seen = []
        with mock.patch.object(gt.rpc, "read_angles", self._arm([HOME])), \
                mock.patch.object(gt, "daemon_send_angles", lambda a, **k: seen.append(k)):
            gt.DaemonArm().send_and_check(np.array(HOME), gt.SendGate(), threading.Event())
        self.assertEqual(seen[0]["timeout_override"], gt.SEND_RPC_TIMEOUT_SEC)

    def test_stop_log_says_whether_send_was_committed(self):
        tp, arm, log, spawned = make(sync=False)
        press(tp, gt.BTN_RIGHT)
        tp.set_triggers(1, 1)                       # まだ送ると決めていない
        stop = [r for r in log.rows if r["kind"] == "stop"][0]
        self.assertIs(stop["send_committed_before_stop"], False)

    def test_gripper_gate(self):
        calls = []
        with mock.patch.object(gt, "daemon_set_gripper_value", lambda v: calls.append(("close", v))), \
                mock.patch.object(gt, "daemon_set_gripper", lambda s, wait: calls.append(("open", s, wait))), \
                mock.patch.object(gt.rpc, "GRIPPER_SLEEP", 0.0):
            arm = gt.DaemonArm()
            self.assertTrue(arm.gripper(1, gt.SendGate()))
            self.assertTrue(arm.gripper(0, gt.SendGate()))
            g = gt.SendGate()
            g.cancel_now()
            self.assertFalse(arm.gripper(1, g))
        self.assertEqual(calls, [("close", 20), ("open", 0, gt.rpc.GRIP_OPEN_DAEMON_WAIT)])


class FakeJoystick:
    def __init__(self, idx):
        self.axes = [0.0, 0.0, 0.0, 0.0, -1.0, -1.0]

    def get_instance_id(self):
        return 7

    def get_name(self):
        return "PS4 Controller"

    def get_guid(self):
        return "fake-guid"

    def get_numaxes(self):
        return 6

    def get_numbuttons(self):
        return 16

    def get_numhats(self):
        return 0

    def get_axis(self, a):
        return self.axes[a]


class TestDryRunMain(unittest.TestCase):
    def test_flow(self):
        import pygame
        js_holder = {}

        def make_js(idx):
            js_holder["js"] = FakeJoystick(idx)
            return js_holder["js"]

        def btn(b, down=True):
            t = pygame.JOYBUTTONDOWN if down else pygame.JOYBUTTONUP
            pygame.event.post(pygame.event.Event(t, instance_id=7, button=b, joy=0))

        # フレームごとの台本 (ドライランは 1 回 0.3 秒動いている扱い = 60fps で 18 コマ程度)
        script = {
            1: lambda: pygame.event.post(pygame.event.Event(pygame.JOYDEVICEADDED, device_index=0)),
            3: lambda: (btn(gt.BTN_RIGHT), btn(gt.BTN_RIGHT, False)),
            5: lambda: (btn(gt.BTN_RIGHT), btn(gt.BTN_RIGHT, False)),       # 動いている間 → 受け付けない
            40: lambda: btn(gt.BTN_TRIANGLE),
            42: lambda: (btn(gt.BTN_DOWN), btn(gt.BTN_DOWN, False)),
            80: lambda: btn(gt.BTN_CROSS),
            82: lambda: js_holder["js"].axes.__setitem__(slice(4, 6), [0.5, 0.5]),   # 停止
            85: lambda: (btn(gt.BTN_LEFT), btn(gt.BTN_LEFT, False)),
            86: lambda: js_holder["js"].axes.__setitem__(slice(4, 6), [-1.0, -1.0]),
            88: lambda: btn(gt.BTN_OPTIONS),
            150: lambda: btn(gt.BTN_HOME),
            230: lambda: btn(gt.BTN_HOME, False),
            300: lambda: pygame.event.post(pygame.event.Event(pygame.QUIT)),
        }

        def on_frame(n, tp):
            if n in script:
                script[n]()

        with tempfile.TemporaryDirectory() as td, \
                mock.patch.object(gt, "LOG_ROOT", Path(td)), \
                mock.patch.object(gt, "ensure_ssd_mounted", lambda: None), \
                mock.patch.object(gt.pygame.joystick, "Joystick", make_js), \
                mock.patch.object(gt.rpc, "GRIPPER_SLEEP", 0.2), \
                mock.patch.object(gt, "get_daemon_socket", side_effect=AssertionError("daemon に触った")), \
                mock.patch.object(gt, "daemon_send_angles", side_effect=AssertionError("daemon に送った")):
            gt.main([], on_frame=on_frame)
            runs = list(Path(td).iterdir())
            self.assertEqual(len(runs), 1)
            run = runs[0]
            self.assertTrue(run.name.endswith("_dry"))
            rows = [json.loads(x) for x in (run / "log.jsonl").read_text().splitlines()]
            meta = json.loads((run / "meta.json").read_text())
            self.assertTrue((run / "gamepad_teleop.py").exists())
        kinds = [(r["kind"], r.get("accepted"), r.get("reason")) for r in rows]
        # 受け付けた 1 回目の行は届いてから書くので, 動いている間に断った行 (busy) が先に来る
        self.assertEqual(kinds[0], ("move", False, "busy"))
        self.assertEqual(kinds[1][:2], ("move", True))
        self.assertIn(("move", False, "busy"), kinds)
        self.assertIn(("select_joint", True, None), kinds)
        self.assertIn(("stop", True, "L2+R2"), kinds)
        self.assertIn(("move", False, "stopped"), kinds)
        self.assertIn(("resume", True, None), kinds)
        home = [r for r in rows if r["kind"] == "home"]
        self.assertEqual(len(home), 1)
        self.assertEqual(home[0]["command"], HOME)
        moves = [r for r in rows if r["kind"] == "move" and r.get("sent") is True]
        self.assertEqual(moves[0]["command"], [0.5, 0, 0, 0, 0, -30])
        self.assertEqual(moves[1]["command"], [0.5, 0.0, 0.5, 0, 0, -30])    # △ で j2 にしてから下
        for r in moves:
            for key in ("t", "button", "kind", "selected_joint", "step_deg", "measured_before",
                        "command", "reached_angles", "reached", "grip_state", "clip", "stopped"):
                self.assertIn(key, r)
        self.assertEqual(meta["controllers"][0]["name"], "PS4 Controller")
        self.assertEqual(meta["controllers"][0]["guid"], "fake-guid")
        self.assertFalse(meta["execute"])
        self.assertEqual(meta["end_reason"], "window_closed")
        self.assertEqual(meta["guards"]["J1_MAX_DEG"], 90.87)
        for key in ("python", "pygame", "sdl", "start_pose", "git"):
            self.assertIn(key, meta)


if __name__ == "__main__":
    unittest.main()
```


## 11/2 19:26〜19:34：アームなしのドライランで、172回の操作が決めたとおりに動いた

Mac で `python3 ~/r2r2r-scripts/tools/gamepad_teleop.py` を、何も付けずに起動した。何も付けないとドライランになり、HOME から始まる仮の姿勢の数字だけが動く。mechArm 270 と Raspberry Pi の電源は入れていない。

【画像】LOG419_04_gamepad_teleop_dry_run.png gamepad_teleop.py の窓。DRY RUN、動かす関節 j1、刻み 0.5 deg、状態 IDLE が大きく出て、その下に6関節の今の角度・送った角度・基準の角度と、ボタンの割り当ての一覧が並ぶ

19:27:17 から 19:30:50 までの3分33秒で、172回の操作が記録された。

| 操作の種類 | 回数 | 中身 |
|---|---|---|
| 関節を動かす（十字キー） | 137回 | 受け付けて送ったのは107回。j0 が35回、j3 が34回、j5 が23回、j4 が8回、j2 が5回、j1 が2回 |
| 関節の切り替え（△） | 10回 | j1 → j2 → j3 → j4 → j5 → j1 と回った |
| 爪を開く / 閉じる（○ / ×） | 7回 / 6回 | 1回ごとに0.8秒の待ちが入る |
| R1（2°の刻み） | 3回 | 2° ずつ動いた（j0 +2.0° が6回、j5 +2.0° が10回など） |
| HOME の長押し | 2回 | 19:29:42 の1回は送られ、角度が HOME に戻った。もう1回は停止中で受け付けなかった |
| 停止 / 再開 | 1回 / 1回 | L2+R2 で停止し、停止中の押しはすべて受け付けず、OPTIONS で戻った |
| 割り当てのないボタン（□） | 5回 | 何もしなかった |

受け付けなかった34回の内わけは、前の動きや爪の待ちが終わる前に押した分が25回、割り当てのない □ が5回、停止中が4回。範囲の守りは j5 で3回効いた。j5 は始めが −30° なので、動かせるのは ±10° の [−40, −20]。−41° と −42° を送ろうとした所で −40° に切られ、−18° を送ろうとした所で送らなかった。j1 の 90.87° の止めは、HOME から始めると j1 が ±10° までしか動かないので、この試しでは効く場面がなかった。

| ドライランで気づいたこと | 次にどうするか |
|---|---|
| 前の動きが終わる前の押しを、ためずに捨てた分が25回あった。実機では1回の動きに約1秒かかるので、もっと増える | 実機で押した感触を見てから、そのままでよいか決める |
| 起動したとき、画面の部品（SDL）が2つ読み込まれているという警告が十数行出た。gamepad_check.py では出なかった | 依頼69で、どこで2つ目が読み込まれているかを調べて直す |
| ドライランでも KIOXIA に記録を書き、KIOXIA がつながっていないと止まる | 依頼69で、ドライランは記録しない形にする |
| 十字キーの右で j0 が＋になるが、＋がアームのどちら向きかは実機で動かさないと分からない | 実機で0.5°動かして確かめる |

### gamepad_teleop.py のドライランのターミナルの出力（614〜805行目）

```text
(base) shimazueiichi@shimadzueiichinoMacBook-Pro tools % python3 ~/r2r2r-scripts/tools/gamepad_teleop.py
/opt/homebrew/Caskroom/miniforge/base/lib/python3.12/site-packages/pygame/pkgdata.py:25: UserWarning: pkg_resources is deprecated as an API. See https://setuptools.pypa.io/en/latest/pkg_resources.html. The pkg_resources package is slated for removal as early as 2025-11-30. Refrain from using this package or pin to Setuptools<81.
  from pkg_resources import resource_stream, resource_exists
objc[86737]: Class SDLApplication is implemented in both /opt/homebrew/Caskroom/miniforge/base/lib/python3.12/site-packages/pygame/.dylibs/libSDL2-2.0.0.dylib (0x1050612c8) and /opt/homebrew/Caskroom/miniforge/base/lib/python3.12/site-packages/cv2/.dylibs/libSDL2-2.0.0.dylib (0x1275a8890). This may cause spurious casting failures and mysterious crashes. One of the duplicates must be removed or renamed.
objc[86737]: Class SDLAppDelegate is implemented in both /opt/homebrew/Caskroom/miniforge/base/lib/python3.12/site-packages/pygame/.dylibs/libSDL2-2.0.0.dylib (0x105061318) and /opt/homebrew/Caskroom/miniforge/base/lib/python3.12/site-packages/cv2/.dylibs/libSDL2-2.0.0.dylib (0x1275a88e0). This may cause spurious casting failures and mysterious crashes. One of the duplicates must be removed or renamed.
objc[86737]: Class SDLTranslatorResponder is implemented in both /opt/homebrew/Caskroom/miniforge/base/lib/python3.12/site-packages/pygame/.dylibs/libSDL2-2.0.0.dylib (0x105061390) and /opt/homebrew/Caskroom/miniforge/base/lib/python3.12/site-packages/cv2/.dylibs/libSDL2-2.0.0.dylib (0x1275a8958). This may cause spurious casting failures and mysterious crashes. One of the duplicates must be removed or renamed.
objc[86737]: Class SDLMessageBoxPresenter is implemented in both /opt/homebrew/Caskroom/miniforge/base/lib/python3.12/site-packages/pygame/.dylibs/libSDL2-2.0.0.dylib (0x1050613b8) and /opt/homebrew/Caskroom/miniforge/base/lib/python3.12/site-packages/cv2/.dylibs/libSDL2-2.0.0.dylib (0x1275a8980). This may cause spurious casting failures and mysterious crashes. One of the duplicates must be removed or renamed.
objc[86737]: Class SDL_cocoametalview is implemented in both /opt/homebrew/Caskroom/miniforge/base/lib/python3.12/site-packages/pygame/.dylibs/libSDL2-2.0.0.dylib (0x105061408) and /opt/homebrew/Caskroom/miniforge/base/lib/python3.12/site-packages/cv2/.dylibs/libSDL2-2.0.0.dylib (0x1275a89d0). This may cause spurious casting failures and mysterious crashes. One of the duplicates must be removed or renamed.
objc[86737]: Class SDLOpenGLContext is implemented in both /opt/homebrew/Caskroom/miniforge/base/lib/python3.12/site-packages/pygame/.dylibs/libSDL2-2.0.0.dylib (0x105061458) and /opt/homebrew/Caskroom/miniforge/base/lib/python3.12/site-packages/cv2/.dylibs/libSDL2-2.0.0.dylib (0x1275a8a20). This may cause spurious casting failures and mysterious crashes. One of the duplicates must be removed or renamed.
objc[86737]: Class SDL_ShapeData is implemented in both /opt/homebrew/Caskroom/miniforge/base/lib/python3.12/site-packages/pygame/.dylibs/libSDL2-2.0.0.dylib (0x1050614d0) and /opt/homebrew/Caskroom/miniforge/base/lib/python3.12/site-packages/cv2/.dylibs/libSDL2-2.0.0.dylib (0x1275a8a98). This may cause spurious casting failures and mysterious crashes. One of the duplicates must be removed or renamed.
objc[86737]: Class SDL_CocoaClosure is implemented in both /opt/homebrew/Caskroom/miniforge/base/lib/python3.12/site-packages/pygame/.dylibs/libSDL2-2.0.0.dylib (0x105061520) and /opt/homebrew/Caskroom/miniforge/base/lib/python3.12/site-packages/cv2/.dylibs/libSDL2-2.0.0.dylib (0x1275a8ae8). This may cause spurious casting failures and mysterious crashes. One of the duplicates must be removed or renamed.
objc[86737]: Class SDL_VideoData is implemented in both /opt/homebrew/Caskroom/miniforge/base/lib/python3.12/site-packages/pygame/.dylibs/libSDL2-2.0.0.dylib (0x105061570) and /opt/homebrew/Caskroom/miniforge/base/lib/python3.12/site-packages/cv2/.dylibs/libSDL2-2.0.0.dylib (0x1275a8b38). This may cause spurious casting failures and mysterious crashes. One of the duplicates must be removed or renamed.
objc[86737]: Class SDL_WindowData is implemented in both /opt/homebrew/Caskroom/miniforge/base/lib/python3.12/site-packages/pygame/.dylibs/libSDL2-2.0.0.dylib (0x1050615c0) and /opt/homebrew/Caskroom/miniforge/base/lib/python3.12/site-packages/cv2/.dylibs/libSDL2-2.0.0.dylib (0x1275a8b88). This may cause spurious casting failures and mysterious crashes. One of the duplicates must be removed or renamed.
objc[86737]: Class SDLWindow is implemented in both /opt/homebrew/Caskroom/miniforge/base/lib/python3.12/site-packages/pygame/.dylibs/libSDL2-2.0.0.dylib (0x1050615e8) and /opt/homebrew/Caskroom/miniforge/base/lib/python3.12/site-packages/cv2/.dylibs/libSDL2-2.0.0.dylib (0x1275a8bb0). This may cause spurious casting failures and mysterious crashes. One of the duplicates must be removed or renamed.
objc[86737]: Class Cocoa_WindowListener is implemented in both /opt/homebrew/Caskroom/miniforge/base/lib/python3.12/site-packages/pygame/.dylibs/libSDL2-2.0.0.dylib (0x105061610) and /opt/homebrew/Caskroom/miniforge/base/lib/python3.12/site-packages/cv2/.dylibs/libSDL2-2.0.0.dylib (0x1275a8bd8). This may cause spurious casting failures and mysterious crashes. One of the duplicates must be removed or renamed.
objc[86737]: Class SDLView is implemented in both /opt/homebrew/Caskroom/miniforge/base/lib/python3.12/site-packages/pygame/.dylibs/libSDL2-2.0.0.dylib (0x105061688) and /opt/homebrew/Caskroom/miniforge/base/lib/python3.12/site-packages/cv2/.dylibs/libSDL2-2.0.0.dylib (0x1275a8c50). This may cause spurious casting failures and mysterious crashes. One of the duplicates must be removed or renamed.
objc[86737]: Class METAL_RenderData is implemented in both /opt/homebrew/Caskroom/miniforge/base/lib/python3.12/site-packages/pygame/.dylibs/libSDL2-2.0.0.dylib (0x105061700) and /opt/homebrew/Caskroom/miniforge/base/lib/python3.12/site-packages/cv2/.dylibs/libSDL2-2.0.0.dylib (0x1275a8cc8). This may cause spurious casting failures and mysterious crashes. One of the duplicates must be removed or renamed.
objc[86737]: Class METAL_TextureData is implemented in both /opt/homebrew/Caskroom/miniforge/base/lib/python3.12/site-packages/pygame/.dylibs/libSDL2-2.0.0.dylib (0x105061750) and /opt/homebrew/Caskroom/miniforge/base/lib/python3.12/site-packages/cv2/.dylibs/libSDL2-2.0.0.dylib (0x1275a8d18). This may cause spurious casting failures and mysterious crashes. One of the duplicates must be removed or renamed.
objc[86737]: Class SDL_RumbleMotor is implemented in both /opt/homebrew/Caskroom/miniforge/base/lib/python3.12/site-packages/pygame/.dylibs/libSDL2-2.0.0.dylib (0x105061778) and /opt/homebrew/Caskroom/miniforge/base/lib/python3.12/site-packages/cv2/.dylibs/libSDL2-2.0.0.dylib (0x1275a8d40). This may cause spurious casting failures and mysterious crashes. One of the duplicates must be removed or renamed.
objc[86737]: Class SDL_RumbleContext is implemented in both /opt/homebrew/Caskroom/miniforge/base/lib/python3.12/site-packages/pygame/.dylibs/libSDL2-2.0.0.dylib (0x1050617c8) and /opt/homebrew/Caskroom/miniforge/base/lib/python3.12/site-packages/cv2/.dylibs/libSDL2-2.0.0.dylib (0x1275a8d90). This may cause spurious casting failures and mysterious crashes. One of the duplicates must be removed or renamed.
[記録] /Volumes/KIOXIA/r2r2r-data/runs/experiments/gamepad_teleop/teleop_20261102_192657_dry
[ドライラン] daemon にもアームにも触りません。HOME から始まる仮の姿勢を動かします
コントローラーを待っています（つないでいなければ、今つないでください）
[接続] PS4 Controller  GUID=03008fe54c050000cc09000000016800  軸=6  ボタン=16
[送信] j0 -0.5° → -0.50 +0.00 +0.00 +0.00 +0.00 -30.00  届いた（ずれ 0.0°）  届いた角度 -0.50 +0.00 +0.00 +0.00 +0.00 -30.00
[送信] j0 -0.5° → -1.00 +0.00 +0.00 +0.00 +0.00 -30.00  届いた（ずれ 0.0°）  届いた角度 -1.00 +0.00 +0.00 +0.00 +0.00 -30.00
[送信] j0 +0.5° → -0.50 +0.00 +0.00 +0.00 +0.00 -30.00  届いた（ずれ 0.0°）  届いた角度 -0.50 +0.00 +0.00 +0.00 +0.00 -30.00
[送信] j0 +0.5° → +0.00 +0.00 +0.00 +0.00 +0.00 -30.00  届いた（ずれ 0.0°）  届いた角度 +0.00 +0.00 +0.00 +0.00 +0.00 -30.00
[送信] j1 +0.5° → +0.00 +0.50 +0.00 +0.00 +0.00 -30.00  届いた（ずれ 0.0°）  届いた角度 +0.00 +0.50 +0.00 +0.00 +0.00 -30.00
[送信] j1 +0.5° → +0.00 +1.00 +0.00 +0.00 +0.00 -30.00  届いた（ずれ 0.0°）  届いた角度 +0.00 +1.00 +0.00 +0.00 +0.00 -30.00
[送信] j0 +0.5° → +0.50 +1.00 +0.00 +0.00 +0.00 -30.00  届いた（ずれ 0.0°）  届いた角度 +0.50 +1.00 +0.00 +0.00 +0.00 -30.00
[送信] j0 +0.5° → +1.00 +1.00 +0.00 +0.00 +0.00 -30.00  届いた（ずれ 0.0°）  届いた角度 +1.00 +1.00 +0.00 +0.00 +0.00 -30.00
[送信] j0 +0.5° → +1.50 +1.00 +0.00 +0.00 +0.00 -30.00  届いた（ずれ 0.0°）  届いた角度 +1.50 +1.00 +0.00 +0.00 +0.00 -30.00
[関節] 上下で動かす関節を j2 にしました
[送信] j0 +0.5° → +2.00 +1.00 +0.00 +0.00 +0.00 -30.00  届いた（ずれ 0.0°）  届いた角度 +2.00 +1.00 +0.00 +0.00 +0.00 -30.00
[送信] j0 +0.5° → +2.50 +1.00 +0.00 +0.00 +0.00 -30.00  届いた（ずれ 0.0°）  届いた角度 +2.50 +1.00 +0.00 +0.00 +0.00 -30.00
[送信] j0 +0.5° → +3.00 +1.00 +0.00 +0.00 +0.00 -30.00  届いた（ずれ 0.0°）  届いた角度 +3.00 +1.00 +0.00 +0.00 +0.00 -30.00
[送信] j0 +0.5° → +3.50 +1.00 +0.00 +0.00 +0.00 -30.00  届いた（ずれ 0.0°）  届いた角度 +3.50 +1.00 +0.00 +0.00 +0.00 -30.00
[送信] j0 +0.5° → +4.00 +1.00 +0.00 +0.00 +0.00 -30.00  届いた（ずれ 0.0°）  届いた角度 +4.00 +1.00 +0.00 +0.00 +0.00 -30.00
[関節] 上下で動かす関節を j3 にしました
[送信] j0 +0.5° → +4.50 +1.00 +0.00 +0.00 +0.00 -30.00  届いた（ずれ 0.0°）  届いた角度 +4.50 +1.00 +0.00 +0.00 +0.00 -30.00
[送信] j3 +0.5° → +4.50 +1.00 +0.00 +0.50 +0.00 -30.00  届いた（ずれ 0.0°）  届いた角度 +4.50 +1.00 +0.00 +0.50 +0.00 -30.00
[送信] j3 +0.5° → +4.50 +1.00 +0.00 +1.00 +0.00 -30.00  届いた（ずれ 0.0°）  届いた角度 +4.50 +1.00 +0.00 +1.00 +0.00 -30.00
[送信] j3 -0.5° → +4.50 +1.00 +0.00 +0.50 +0.00 -30.00  届いた（ずれ 0.0°）  届いた角度 +4.50 +1.00 +0.00 +0.50 +0.00 -30.00
[送信] j3 -0.5° → +4.50 +1.00 +0.00 +0.00 +0.00 -30.00  届いた（ずれ 0.0°）  届いた角度 +4.50 +1.00 +0.00 +0.00 +0.00 -30.00
[送信] j3 -0.5° → +4.50 +1.00 +0.00 -0.50 +0.00 -30.00  届いた（ずれ 0.0°）  届いた角度 +4.50 +1.00 +0.00 -0.50 +0.00 -30.00
[送信] j3 -0.5° → +4.50 +1.00 +0.00 -1.00 +0.00 -30.00  届いた（ずれ 0.0°）  届いた角度 +4.50 +1.00 +0.00 -1.00 +0.00 -30.00
[送信] j3 -0.5° → +4.50 +1.00 +0.00 -1.50 +0.00 -30.00  届いた（ずれ 0.0°）  届いた角度 +4.50 +1.00 +0.00 -1.50 +0.00 -30.00
[受付なし] 前の操作が終わっていません（ためません）
[送信] j3 -0.5° → +4.50 +1.00 +0.00 -2.00 +0.00 -30.00  届いた（ずれ 0.0°）  届いた角度 +4.50 +1.00 +0.00 -2.00 +0.00 -30.00
[送信] j3 -0.5° → +4.50 +1.00 +0.00 -2.50 +0.00 -30.00  届いた（ずれ 0.0°）  届いた角度 +4.50 +1.00 +0.00 -2.50 +0.00 -30.00
[受付なし] 前の操作が終わっていません（ためません）
[送信] j3 +0.5° → +4.50 +1.00 +0.00 -2.00 +0.00 -30.00  届いた（ずれ 0.0°）  届いた角度 +4.50 +1.00 +0.00 -2.00 +0.00 -30.00
[受付なし] 前の操作が終わっていません（ためません）
[送信] j3 +0.5° → +4.50 +1.00 +0.00 -1.50 +0.00 -30.00  届いた（ずれ 0.0°）  届いた角度 +4.50 +1.00 +0.00 -1.50 +0.00 -30.00
[送信] j3 +0.5° → +4.50 +1.00 +0.00 -1.00 +0.00 -30.00  届いた（ずれ 0.0°）  届いた角度 +4.50 +1.00 +0.00 -1.00 +0.00 -30.00
[受付なし] 前の操作が終わっていません（ためません）
[送信] j3 +0.5° → +4.50 +1.00 +0.00 -0.50 +0.00 -30.00  届いた（ずれ 0.0°）  届いた角度 +4.50 +1.00 +0.00 -0.50 +0.00 -30.00
[受付なし] 前の操作が終わっていません（ためません）
[送信] j3 +0.5° → +4.50 +1.00 +0.00 +0.00 +0.00 -30.00  届いた（ずれ 0.0°）  届いた角度 +4.50 +1.00 +0.00 +0.00 +0.00 -30.00
[受付なし] 前の操作が終わっていません（ためません）
[送信] j3 +0.5° → +4.50 +1.00 +0.00 +0.50 +0.00 -30.00  届いた（ずれ 0.0°）  届いた角度 +4.50 +1.00 +0.00 +0.50 +0.00 -30.00
[受付なし] 前の操作が終わっていません（ためません）
[送信] j3 +0.5° → +4.50 +1.00 +0.00 +1.00 +0.00 -30.00  届いた（ずれ 0.0°）  届いた角度 +4.50 +1.00 +0.00 +1.00 +0.00 -30.00
[受付なし] 前の操作が終わっていません（ためません）
[送信] j3 +0.5° → +4.50 +1.00 +0.00 +1.50 +0.00 -30.00  届いた（ずれ 0.0°）  届いた角度 +4.50 +1.00 +0.00 +1.50 +0.00 -30.00
[爪] 爪を開く（待ち 0.8 秒のあと受付）
[送信] j3 +0.5° → +4.50 +1.00 +0.00 +2.00 +0.00 -30.00  届いた（ずれ 0.0°）  届いた角度 +4.50 +1.00 +0.00 +2.00 +0.00 -30.00
[送信] j3 +0.5° → +4.50 +1.00 +0.00 +2.50 +0.00 -30.00  届いた（ずれ 0.0°）  届いた角度 +4.50 +1.00 +0.00 +2.50 +0.00 -30.00
[送信] j3 +0.5° → +4.50 +1.00 +0.00 +3.00 +0.00 -30.00  届いた（ずれ 0.0°）  届いた角度 +4.50 +1.00 +0.00 +3.00 +0.00 -30.00
[送信] j3 +0.5° → +4.50 +1.00 +0.00 +3.50 +0.00 -30.00  届いた（ずれ 0.0°）  届いた角度 +4.50 +1.00 +0.00 +3.50 +0.00 -30.00
[受付なし] 前の操作が終わっていません（ためません）
[爪] 爪を開く（待ち 0.8 秒のあと受付）
[送信] j3 +0.5° → +4.50 +1.00 +0.00 +4.00 +0.00 -30.00  届いた（ずれ 0.0°）  届いた角度 +4.50 +1.00 +0.00 +4.00 +0.00 -30.00
[送信] j3 +0.5° → +4.50 +1.00 +0.00 +4.50 +0.00 -30.00  届いた（ずれ 0.0°）  届いた角度 +4.50 +1.00 +0.00 +4.50 +0.00 -30.00
[送信] j3 -0.5° → +4.50 +1.00 +0.00 +4.00 +0.00 -30.00  届いた（ずれ 0.0°）  届いた角度 +4.50 +1.00 +0.00 +4.00 +0.00 -30.00
[爪] 爪を開く（待ち 0.8 秒のあと受付）
[送信] j0 -0.5° → +4.00 +1.00 +0.00 +4.00 +0.00 -30.00  届いた（ずれ 0.0°）  届いた角度 +4.00 +1.00 +0.00 +4.00 +0.00 -30.00
[送信] j0 -0.5° → +3.50 +1.00 +0.00 +4.00 +0.00 -30.00  届いた（ずれ 0.0°）  届いた角度 +3.50 +1.00 +0.00 +4.00 +0.00 -30.00
[送信] j0 +0.5° → +4.00 +1.00 +0.00 +4.00 +0.00 -30.00  届いた（ずれ 0.0°）  届いた角度 +4.00 +1.00 +0.00 +4.00 +0.00 -30.00
[送信] j0 +0.5° → +4.50 +1.00 +0.00 +4.00 +0.00 -30.00  届いた（ずれ 0.0°）  届いた角度 +4.50 +1.00 +0.00 +4.00 +0.00 -30.00
[送信] j0 +2.0° → +6.50 +1.00 +0.00 +4.00 +0.00 -30.00  届いた（ずれ 0.0°）  届いた角度 +6.50 +1.00 +0.00 +4.00 +0.00 -30.00
[送信] j0 +2.0° → +8.50 +1.00 +0.00 +4.00 +0.00 -30.00  届いた（ずれ 0.0°）  届いた角度 +8.50 +1.00 +0.00 +4.00 +0.00 -30.00
[送信] j0 -2.0° → +6.50 +1.00 +0.00 +4.00 +0.00 -30.00  届いた（ずれ 0.0°）  届いた角度 +6.50 +1.00 +0.00 +4.00 +0.00 -30.00
[送信] j0 -2.0° → +4.50 +1.00 +0.00 +4.00 +0.00 -30.00  届いた（ずれ 0.0°）  届いた角度 +4.50 +1.00 +0.00 +4.00 +0.00 -30.00
[送信] j0 +0.5° → +5.00 +1.00 +0.00 +4.00 +0.00 -30.00  届いた（ずれ 0.0°）  届いた角度 +5.00 +1.00 +0.00 +4.00 +0.00 -30.00
[送信] j0 +0.5° → +5.50 +1.00 +0.00 +4.00 +0.00 -30.00  届いた（ずれ 0.0°）  届いた角度 +5.50 +1.00 +0.00 +4.00 +0.00 -30.00
[送信] j0 +0.5° → +6.00 +1.00 +0.00 +4.00 +0.00 -30.00  届いた（ずれ 0.0°）  届いた角度 +6.00 +1.00 +0.00 +4.00 +0.00 -30.00
[送信] j3 +0.5° → +6.00 +1.00 +0.00 +4.50 +0.00 -30.00  届いた（ずれ 0.0°）  届いた角度 +6.00 +1.00 +0.00 +4.50 +0.00 -30.00
[爪] 爪を閉じる（待ち 0.8 秒のあと受付）
[爪] 爪を閉じる（待ち 0.8 秒のあと受付）
[爪] 爪を開く（待ち 0.8 秒のあと受付）
[関節] 上下で動かす関節を j4 にしました
[関節] 上下で動かす関節を j5 にしました
[関節] 上下で動かす関節を j1 にしました
[関節] 上下で動かす関節を j2 にしました
[送信] j0 +0.5° → +6.50 +1.00 +0.00 +4.50 +0.00 -30.00  届いた（ずれ 0.0°）  届いた角度 +6.50 +1.00 +0.00 +4.50 +0.00 -30.00
[送信] j0 +0.5° → +7.00 +1.00 +0.00 +4.50 +0.00 -30.00  届いた（ずれ 0.0°）  届いた角度 +7.00 +1.00 +0.00 +4.50 +0.00 -30.00
[送信] j0 -0.5° → +6.50 +1.00 +0.00 +4.50 +0.00 -30.00  届いた（ずれ 0.0°）  届いた角度 +6.50 +1.00 +0.00 +4.50 +0.00 -30.00
[送信] j2 +0.5° → +6.50 +1.00 +0.50 +4.50 +0.00 -30.00  届いた（ずれ 0.0°）  届いた角度 +6.50 +1.00 +0.50 +4.50 +0.00 -30.00
[送信] j2 +0.5° → +6.50 +1.00 +1.00 +4.50 +0.00 -30.00  届いた（ずれ 0.0°）  届いた角度 +6.50 +1.00 +1.00 +4.50 +0.00 -30.00
[送信] j2 +0.5° → +6.50 +1.00 +1.50 +4.50 +0.00 -30.00  届いた（ずれ 0.0°）  届いた角度 +6.50 +1.00 +1.50 +4.50 +0.00 -30.00
[送信] j2 -0.5° → +6.50 +1.00 +1.00 +4.50 +0.00 -30.00  届いた（ずれ 0.0°）  届いた角度 +6.50 +1.00 +1.00 +4.50 +0.00 -30.00
[送信] j2 -0.5° → +6.50 +1.00 +0.50 +4.50 +0.00 -30.00  届いた（ずれ 0.0°）  届いた角度 +6.50 +1.00 +0.50 +4.50 +0.00 -30.00
[関節] 上下で動かす関節を j3 にしました
[送信] j3 -0.5° → +6.50 +1.00 +0.50 +4.00 +0.00 -30.00  届いた（ずれ 0.0°）  届いた角度 +6.50 +1.00 +0.50 +4.00 +0.00 -30.00
[送信] j3 -0.5° → +6.50 +1.00 +0.50 +3.50 +0.00 -30.00  届いた（ずれ 0.0°）  届いた角度 +6.50 +1.00 +0.50 +3.50 +0.00 -30.00
[送信] j3 -0.5° → +6.50 +1.00 +0.50 +3.00 +0.00 -30.00  届いた（ずれ 0.0°）  届いた角度 +6.50 +1.00 +0.50 +3.00 +0.00 -30.00
[送信] j3 -0.5° → +6.50 +1.00 +0.50 +2.50 +0.00 -30.00  届いた（ずれ 0.0°）  届いた角度 +6.50 +1.00 +0.50 +2.50 +0.00 -30.00
[受付なし] 前の操作が終わっていません（ためません）
[送信] j3 -0.5° → +6.50 +1.00 +0.50 +2.00 +0.00 -30.00  届いた（ずれ 0.0°）  届いた角度 +6.50 +1.00 +0.50 +2.00 +0.00 -30.00
[受付なし] 前の操作が終わっていません（ためません）
[送信] j3 -0.5° → +6.50 +1.00 +0.50 +1.50 +0.00 -30.00  届いた（ずれ 0.0°）  届いた角度 +6.50 +1.00 +0.50 +1.50 +0.00 -30.00
[送信] j3 -0.5° → +6.50 +1.00 +0.50 +1.00 +0.00 -30.00  届いた（ずれ 0.0°）  届いた角度 +6.50 +1.00 +0.50 +1.00 +0.00 -30.00
[送信] j3 -0.5° → +6.50 +1.00 +0.50 +0.50 +0.00 -30.00  届いた（ずれ 0.0°）  届いた角度 +6.50 +1.00 +0.50 +0.50 +0.00 -30.00
[関節] 上下で動かす関節を j4 にしました
[送信] j3 -0.5° → +6.50 +1.00 +0.50 +0.00 +0.00 -30.00  届いた（ずれ 0.0°）  届いた角度 +6.50 +1.00 +0.50 +0.00 +0.00 -30.00
[送信] j4 -0.5° → +6.50 +1.00 +0.50 +0.00 -0.50 -30.00  届いた（ずれ 0.0°）  届いた角度 +6.50 +1.00 +0.50 +0.00 -0.50 -30.00
[送信] j4 -0.5° → +6.50 +1.00 +0.50 +0.00 -1.00 -30.00  届いた（ずれ 0.0°）  届いた角度 +6.50 +1.00 +0.50 +0.00 -1.00 -30.00
[送信] j4 -0.5° → +6.50 +1.00 +0.50 +0.00 -1.50 -30.00  届いた（ずれ 0.0°）  届いた角度 +6.50 +1.00 +0.50 +0.00 -1.50 -30.00
[送信] j4 -0.5° → +6.50 +1.00 +0.50 +0.00 -2.00 -30.00  届いた（ずれ 0.0°）  届いた角度 +6.50 +1.00 +0.50 +0.00 -2.00 -30.00
[送信] j4 -0.5° → +6.50 +1.00 +0.50 +0.00 -2.50 -30.00  届いた（ずれ 0.0°）  届いた角度 +6.50 +1.00 +0.50 +0.00 -2.50 -30.00
[受付なし] 前の操作が終わっていません（ためません）
[送信] j4 -0.5° → +6.50 +1.00 +0.50 +0.00 -3.00 -30.00  届いた（ずれ 0.0°）  届いた角度 +6.50 +1.00 +0.50 +0.00 -3.00 -30.00
[受付なし] 前の操作が終わっていません（ためません）
[送信] j4 -0.5° → +6.50 +1.00 +0.50 +0.00 -3.50 -30.00  届いた（ずれ 0.0°）  届いた角度 +6.50 +1.00 +0.50 +0.00 -3.50 -30.00
[送信] j4 -0.5° → +6.50 +1.00 +0.50 +0.00 -4.00 -30.00  届いた（ずれ 0.0°）  届いた角度 +6.50 +1.00 +0.50 +0.00 -4.00 -30.00
[関節] 上下で動かす関節を j5 にしました
[送信] j5 -0.5° → +6.50 +1.00 +0.50 +0.00 -4.00 -30.50  届いた（ずれ 0.0°）  届いた角度 +6.50 +1.00 +0.50 +0.00 -4.00 -30.50
[受付なし] 前の操作が終わっていません（ためません）
[送信] j5 -0.5° → +6.50 +1.00 +0.50 +0.00 -4.00 -31.00  届いた（ずれ 0.0°）  届いた角度 +6.50 +1.00 +0.50 +0.00 -4.00 -31.00
[受付なし] 前の操作が終わっていません（ためません）
[送信] j5 -0.5° → +6.50 +1.00 +0.50 +0.00 -4.00 -31.50  届いた（ずれ 0.0°）  届いた角度 +6.50 +1.00 +0.50 +0.00 -4.00 -31.50
[受付なし] 前の操作が終わっていません（ためません）
[送信] j5 -0.5° → +6.50 +1.00 +0.50 +0.00 -4.00 -32.00  届いた（ずれ 0.0°）  届いた角度 +6.50 +1.00 +0.50 +0.00 -4.00 -32.00
[受付なし] 前の操作が終わっていません（ためません）
[送信] j5 -0.5° → +6.50 +1.00 +0.50 +0.00 -4.00 -32.50  届いた（ずれ 0.0°）  届いた角度 +6.50 +1.00 +0.50 +0.00 -4.00 -32.50
[受付なし] 前の操作が終わっていません（ためません）
[送信] j5 -0.5° → +6.50 +1.00 +0.50 +0.00 -4.00 -33.00  届いた（ずれ 0.0°）  届いた角度 +6.50 +1.00 +0.50 +0.00 -4.00 -33.00
[受付なし] 前の操作が終わっていません（ためません）
[送信] j5 -0.5° → +6.50 +1.00 +0.50 +0.00 -4.00 -33.50  届いた（ずれ 0.0°）  届いた角度 +6.50 +1.00 +0.50 +0.00 -4.00 -33.50
[送信] j5 -0.5° → +6.50 +1.00 +0.50 +0.00 -4.00 -34.00  届いた（ずれ 0.0°）  届いた角度 +6.50 +1.00 +0.50 +0.00 -4.00 -34.00
[爪] 爪を開く（待ち 0.8 秒のあと受付）
[送信] j5 -0.5° → +6.50 +1.00 +0.50 +0.00 -4.00 -34.50  届いた（ずれ 0.0°）  届いた角度 +6.50 +1.00 +0.50 +0.00 -4.00 -34.50
[送信] j5 -0.5° → +6.50 +1.00 +0.50 +0.00 -4.00 -35.00  届いた（ずれ 0.0°）  届いた角度 +6.50 +1.00 +0.50 +0.00 -4.00 -35.00
[送信] j5 -2.0° → +6.50 +1.00 +0.50 +0.00 -4.00 -37.00  届いた（ずれ 0.0°）  届いた角度 +6.50 +1.00 +0.50 +0.00 -4.00 -37.00
[送信] j5 -2.0° → +6.50 +1.00 +0.50 +0.00 -4.00 -39.00  届いた（ずれ 0.0°）  届いた角度 +6.50 +1.00 +0.50 +0.00 -4.00 -39.00
[範囲] j5 を -41.00° → -40.00° に切った（基準の姿勢からの範囲 -40.00°）
[送信] j5 -2.0° → +6.50 +1.00 +0.50 +0.00 -4.00 -40.00  届いた（ずれ 0.0°）  届いた角度 +6.50 +1.00 +0.50 +0.00 -4.00 -40.00
[範囲] j5 を -42.00° → -40.00° に切った（基準の姿勢からの範囲 -40.00°） / j5 -42.00° は送らない：範囲の端にいて、押した向きに進めない（範囲 [-40.00, -20.00]・基準の姿勢からの範囲）
[送らない] j5 -2.0°
[送信] j5 +2.0° → +6.50 +1.00 +0.50 +0.00 -4.00 -38.00  届いた（ずれ 0.0°）  届いた角度 +6.50 +1.00 +0.50 +0.00 -4.00 -38.00
[送信] j5 +2.0° → +6.50 +1.00 +0.50 +0.00 -4.00 -36.00  届いた（ずれ 0.0°）  届いた角度 +6.50 +1.00 +0.50 +0.00 -4.00 -36.00
[送信] j5 +2.0° → +6.50 +1.00 +0.50 +0.00 -4.00 -34.00  届いた（ずれ 0.0°）  届いた角度 +6.50 +1.00 +0.50 +0.00 -4.00 -34.00
[送信] j5 +2.0° → +6.50 +1.00 +0.50 +0.00 -4.00 -32.00  届いた（ずれ 0.0°）  届いた角度 +6.50 +1.00 +0.50 +0.00 -4.00 -32.00
[送信] j5 +2.0° → +6.50 +1.00 +0.50 +0.00 -4.00 -30.00  届いた（ずれ 0.0°）  届いた角度 +6.50 +1.00 +0.50 +0.00 -4.00 -30.00
[受付なし] 前の操作が終わっていません（ためません）
[送信] j5 +2.0° → +6.50 +1.00 +0.50 +0.00 -4.00 -28.00  届いた（ずれ 0.0°）  届いた角度 +6.50 +1.00 +0.50 +0.00 -4.00 -28.00
[受付なし] 前の操作が終わっていません（ためません）
[送信] j5 +2.0° → +6.50 +1.00 +0.50 +0.00 -4.00 -26.00  届いた（ずれ 0.0°）  届いた角度 +6.50 +1.00 +0.50 +0.00 -4.00 -26.00
[受付なし] 前の操作が終わっていません（ためません）
[送信] j5 +2.0° → +6.50 +1.00 +0.50 +0.00 -4.00 -24.00  届いた（ずれ 0.0°）  届いた角度 +6.50 +1.00 +0.50 +0.00 -4.00 -24.00
[送信] j5 +2.0° → +6.50 +1.00 +0.50 +0.00 -4.00 -22.00  届いた（ずれ 0.0°）  届いた角度 +6.50 +1.00 +0.50 +0.00 -4.00 -22.00
[受付なし] 前の操作が終わっていません（ためません）
[送信] j5 +2.0° → +6.50 +1.00 +0.50 +0.00 -4.00 -20.00  届いた（ずれ 0.0°）  届いた角度 +6.50 +1.00 +0.50 +0.00 -4.00 -20.00
[範囲] j5 を -18.00° → -20.00° に切った（基準の姿勢からの範囲 -20.00°） / j5 -18.00° は送らない：範囲の端にいて、押した向きに進めない（範囲 [-40.00, -20.00]・基準の姿勢からの範囲）
[送らない] j5 +2.0°
[送信] HOME → +0.00 +0.00 +0.00 +0.00 +0.00 -30.00  届いた（ずれ 0.0°）  届いた角度 +0.00 +0.00 +0.00 +0.00 +0.00 -30.00
[爪] 爪を閉じる（待ち 0.8 秒のあと受付）
[爪] 爪を閉じる（待ち 0.8 秒のあと受付）
[爪] 爪を開く（待ち 0.8 秒のあと受付）
[爪] 爪を閉じる（待ち 0.8 秒のあと受付）
[爪] 爪を開く（待ち 0.8 秒のあと受付）
[爪] 爪を閉じる（待ち 0.8 秒のあと受付）
[送信] j0 +0.5° → +0.50 +0.00 +0.00 +0.00 +0.00 -30.00  届いた（ずれ 0.0°）  届いた角度 +0.50 +0.00 +0.00 +0.00 +0.00 -30.00
[受付なし] 前の操作が終わっていません（ためません）
[送信] j0 +0.5° → +1.00 +0.00 +0.00 +0.00 +0.00 -30.00  届いた（ずれ 0.0°）  届いた角度 +1.00 +0.00 +0.00 +0.00 +0.00 -30.00
[受付なし] 前の操作が終わっていません（ためません）
[送信] j0 +0.5° → +1.50 +0.00 +0.00 +0.00 +0.00 -30.00  届いた（ずれ 0.0°）  届いた角度 +1.50 +0.00 +0.00 +0.00 +0.00 -30.00
[送信] j0 +0.5° → +2.00 +0.00 +0.00 +0.00 +0.00 -30.00  届いた（ずれ 0.0°）  届いた角度 +2.00 +0.00 +0.00 +0.00 +0.00 -30.00
[関節] 上下で動かす関節を j1 にしました
[送信] j0 +2.0° → +4.00 +0.00 +0.00 +0.00 +0.00 -30.00  届いた（ずれ 0.0°）  届いた角度 +4.00 +0.00 +0.00 +0.00 +0.00 -30.00
[送信] j0 +2.0° → +6.00 +0.00 +0.00 +0.00 +0.00 -30.00  届いた（ずれ 0.0°）  届いた角度 +6.00 +0.00 +0.00 +0.00 +0.00 -30.00
[送信] j0 +2.0° → +8.00 +0.00 +0.00 +0.00 +0.00 -30.00  届いた（ずれ 0.0°）  届いた角度 +8.00 +0.00 +0.00 +0.00 +0.00 -30.00
[送信] j0 +2.0° → +10.00 +0.00 +0.00 +0.00 +0.00 -30.00  届いた（ずれ 0.0°）  届いた角度 +10.00 +0.00 +0.00 +0.00 +0.00 -30.00
[停止] L2+R2。新しい指令は送りません（OPTIONS で再開）
[受付なし] HOME: 停止中です（押し直してください）
[受付なし] 停止中です（OPTIONS で再開）
[受付なし] 停止中です（OPTIONS で再開）
[受付なし] 停止中です（OPTIONS で再開）
[再開] 操作を受け付けます
```

### runs/experiments/gamepad_teleop/teleop_20261102_192657_dry/meta.json

```json
{
  "run_stem": "teleop_20261102_192657_dry",
  "started_at": "2026-11-02T19:26:57",
  "execute": false,
  "argv": [
    "/Users/shimazueiichi/r2r2r-scripts/tools/gamepad_teleop.py"
  ],
  "controllers": [
    {
      "name": "PS4 Controller",
      "guid": "03008fe54c050000cc09000000016800",
      "instance_id": 0,
      "axes": 6,
      "buttons": 16,
      "hats": 0,
      "connected_at": "2026-11-02T19:27:01"
    }
  ],
  "python": "3.12.12 | packaged by conda-forge | (main, Oct 22 2025, 23:34:53) [Clang 19.1.7 ]",
  "pygame": "2.6.1",
  "sdl": "2.28.4",
  "platform": "macOS-26.6.2-arm64-arm-64bit",
  "start_pose": [
    0.0,
    0.0,
    0.0,
    0.0,
    0.0,
    -30.0
  ],
  "guards": {
    "J1_MAX_DEG": 90.87,
    "J0_RANGE_DEG": 15.0,
    "JOINT_RANGE_DEG": 10.0,
    "J4_SAFE": [
      -90.0,
      60.0
    ],
    "JOINT_LIMITS": [
      [
        -156.0,
        161.0
      ],
      [
        -79.0,
        123.0
      ],
      [
        -177.0,
        177.0
      ],
      [
        -156.0,
        162.0
      ],
      [
        -118.0,
        118.0
      ],
      [
        -177.0,
        178.0
      ]
    ],
    "window_at_start": {
      "lo": [
        -15.0,
        -10.0,
        -10.0,
        -10.0,
        -10.0,
        -40.0
      ],
      "hi": [
        15.0,
        10.0,
        10.0,
        10.0,
        10.0,
        -20.0
      ]
    },
    "home_bypasses_range_check": true
  },
  "motion": {
    "speed": 20,
    "step_fine_deg": 0.5,
    "step_coarse_deg": 2.0,
    "reach_tol_deg": 3.0,
    "reach_timeout_min_sec": 8.0,
    "poll_sec": 0.3,
    "gripper_sleep_sec": 0.8,
    "grip_close_value": 20,
    "send_wait": 0.0,
    "home": [
      0.0,
      0.0,
      0.0,
      0.0,
      0.0,
      -30.0
    ],
    "home_hold_sec": 1.0,
    "trigger_stop_above": 0.0,
    "j0_left_sign": -1.0,
    "up_sign": -1.0,
    "select_order": [
      1,
      2,
      3,
      4,
      5
    ]
  },
  "button_map": {
    "0": "cross",
    "1": "circle",
    "2": "square",
    "3": "triangle",
    "4": "share",
    "5": "home",
    "6": "options",
    "7": "l3",
    "8": "r3",
    "9": "l1",
    "10": "r1",
    "11": "dpad_up",
    "12": "dpad_down",
    "13": "dpad_left",
    "14": "dpad_right",
    "15": "touchpad"
  },
  "git": {
    "commit": "1185747a39e3442acffa89a8afd537f02ed14116",
    "script_status": "clean"
  },
  "script_copy": "gamepad_teleop.py",
  "script_sha256": "630228e45c596001c331612bbf6fa00838cfb5129844070aa9eeeeed45e99086",
  "ended_at": "2026-11-02T19:34:24",
  "end_reason": "ctrl_c",
  "n_sent": 108
}
```

### runs/experiments/gamepad_teleop/teleop_20261102_192657_dry/log.jsonl（172行）

```json
{"seq": 1, "t": "2026-11-02T19:27:17.144", "t_unix": 1793615237.144, "button": 13, "button_name": "dpad_left", "kind": "move", "selected_joint": 1, "step_deg": 0.5, "execute": false, "joint": 0, "delta": -0.5, "accepted": true, "measured_before": [0.0, 0.0, 0.0, 0.0, 0.0, -30.0], "start_source": "command", "cmd_gap_deg": 0.0, "clip": [], "refused": [], "limit_ja": "", "limit_ascii": "", "command": [-0.5, 0.0, 0.0, 0.0, 0.0, -30.0], "sent": true, "reached": true, "reached_angles": [-0.5, 0.0, 0.0, 0.0, 0.0, -30.0], "reach_err": 0.0, "t_send": 1793615237.14743, "dry": true, "grip_state": "unknown", "stopped": false, "stop_reason": null, "base_pose": [0.0, 0.0, 0.0, 0.0, 0.0, -30.0]}
{"seq": 2, "t": "2026-11-02T19:27:17.962", "t_unix": 1793615237.962, "button": 13, "button_name": "dpad_left", "kind": "move", "selected_joint": 1, "step_deg": 0.5, "execute": false, "joint": 0, "delta": -0.5, "accepted": true, "measured_before": [-0.5, 0.0, 0.0, 0.0, 0.0, -30.0], "start_source": "command", "cmd_gap_deg": 0.0, "clip": [], "refused": [], "limit_ja": "", "limit_ascii": "", "command": [-1.0, 0.0, 0.0, 0.0, 0.0, -30.0], "sent": true, "reached": true, "reached_angles": [-1.0, 0.0, 0.0, 0.0, 0.0, -30.0], "reach_err": 0.0, "t_send": 1793615237.963496, "dry": true, "grip_state": "unknown", "stopped": false, "stop_reason": null, "base_pose": [0.0, 0.0, 0.0, 0.0, 0.0, -30.0]}
{"seq": 3, "t": "2026-11-02T19:27:18.584", "t_unix": 1793615238.585, "button": 14, "button_name": "dpad_right", "kind": "move", "selected_joint": 1, "step_deg": 0.5, "execute": false, "joint": 0, "delta": 0.5, "accepted": true, "measured_before": [-1.0, 0.0, 0.0, 0.0, 0.0, -30.0], "start_source": "command", "cmd_gap_deg": 0.0, "clip": [], "refused": [], "limit_ja": "", "limit_ascii": "", "command": [-0.5, 0.0, 0.0, 0.0, 0.0, -30.0], "sent": true, "reached": true, "reached_angles": [-0.5, 0.0, 0.0, 0.0, 0.0, -30.0], "reach_err": 0.0, "t_send": 1793615238.586001, "dry": true, "grip_state": "unknown", "stopped": false, "stop_reason": null, "base_pose": [0.0, 0.0, 0.0, 0.0, 0.0, -30.0]}
{"seq": 4, "t": "2026-11-02T19:27:19.102", "t_unix": 1793615239.102, "button": 14, "button_name": "dpad_right", "kind": "move", "selected_joint": 1, "step_deg": 0.5, "execute": false, "joint": 0, "delta": 0.5, "accepted": true, "measured_before": [-0.5, 0.0, 0.0, 0.0, 0.0, -30.0], "start_source": "command", "cmd_gap_deg": 0.0, "clip": [], "refused": [], "limit_ja": "", "limit_ascii": "", "command": [0.0, 0.0, 0.0, 0.0, 0.0, -30.0], "sent": true, "reached": true, "reached_angles": [0.0, 0.0, 0.0, 0.0, 0.0, -30.0], "reach_err": 0.0, "t_send": 1793615239.1033611, "dry": true, "grip_state": "unknown", "stopped": false, "stop_reason": null, "base_pose": [0.0, 0.0, 0.0, 0.0, 0.0, -30.0]}
{"seq": 5, "t": "2026-11-02T19:27:19.899", "t_unix": 1793615239.899, "button": 12, "button_name": "dpad_down", "kind": "move", "selected_joint": 1, "step_deg": 0.5, "execute": false, "joint": 1, "delta": 0.5, "accepted": true, "measured_before": [0.0, 0.0, 0.0, 0.0, 0.0, -30.0], "start_source": "command", "cmd_gap_deg": 0.0, "clip": [], "refused": [], "limit_ja": "", "limit_ascii": "", "command": [0.0, 0.5, 0.0, 0.0, 0.0, -30.0], "sent": true, "reached": true, "reached_angles": [0.0, 0.5, 0.0, 0.0, 0.0, -30.0], "reach_err": 0.0, "t_send": 1793615239.900151, "dry": true, "grip_state": "unknown", "stopped": false, "stop_reason": null, "base_pose": [0.0, 0.0, 0.0, 0.0, 0.0, -30.0]}
{"seq": 6, "t": "2026-11-02T19:27:20.349", "t_unix": 1793615240.35, "button": 12, "button_name": "dpad_down", "kind": "move", "selected_joint": 1, "step_deg": 0.5, "execute": false, "joint": 1, "delta": 0.5, "accepted": true, "measured_before": [0.0, 0.5, 0.0, 0.0, 0.0, -30.0], "start_source": "command", "cmd_gap_deg": 0.0, "clip": [], "refused": [], "limit_ja": "", "limit_ascii": "", "command": [0.0, 1.0, 0.0, 0.0, 0.0, -30.0], "sent": true, "reached": true, "reached_angles": [0.0, 1.0, 0.0, 0.0, 0.0, -30.0], "reach_err": 0.0, "t_send": 1793615240.350843, "dry": true, "grip_state": "unknown", "stopped": false, "stop_reason": null, "base_pose": [0.0, 0.0, 0.0, 0.0, 0.0, -30.0]}
{"seq": 7, "t": "2026-11-02T19:27:24.208", "t_unix": 1793615244.209, "button": 2, "button_name": "square", "kind": "ignored", "selected_joint": 1, "step_deg": 0.5, "execute": false, "accepted": false, "reason": "unassigned", "grip_state": "unknown", "stopped": false, "stop_reason": null, "base_pose": [0.0, 0.0, 0.0, 0.0, 0.0, -30.0]}
{"seq": 8, "t": "2026-11-02T19:27:25.294", "t_unix": 1793615245.294, "button": 14, "button_name": "dpad_right", "kind": "move", "selected_joint": 1, "step_deg": 0.5, "execute": false, "joint": 0, "delta": 0.5, "accepted": true, "measured_before": [0.0, 1.0, 0.0, 0.0, 0.0, -30.0], "start_source": "command", "cmd_gap_deg": 0.0, "clip": [], "refused": [], "limit_ja": "", "limit_ascii": "", "command": [0.5, 1.0, 0.0, 0.0, 0.0, -30.0], "sent": true, "reached": true, "reached_angles": [0.5, 1.0, 0.0, 0.0, 0.0, -30.0], "reach_err": 0.0, "t_send": 1793615245.295143, "dry": true, "grip_state": "unknown", "stopped": false, "stop_reason": null, "base_pose": [0.0, 0.0, 0.0, 0.0, 0.0, -30.0]}
{"seq": 9, "t": "2026-11-02T19:27:26.121", "t_unix": 1793615246.122, "button": 14, "button_name": "dpad_right", "kind": "move", "selected_joint": 1, "step_deg": 0.5, "execute": false, "joint": 0, "delta": 0.5, "accepted": true, "measured_before": [0.5, 1.0, 0.0, 0.0, 0.0, -30.0], "start_source": "command", "cmd_gap_deg": 0.0, "clip": [], "refused": [], "limit_ja": "", "limit_ascii": "", "command": [1.0, 1.0, 0.0, 0.0, 0.0, -30.0], "sent": true, "reached": true, "reached_angles": [1.0, 1.0, 0.0, 0.0, 0.0, -30.0], "reach_err": 0.0, "t_send": 1793615246.123486, "dry": true, "grip_state": "unknown", "stopped": false, "stop_reason": null, "base_pose": [0.0, 0.0, 0.0, 0.0, 0.0, -30.0]}
{"seq": 10, "t": "2026-11-02T19:27:26.613", "t_unix": 1793615246.614, "button": 14, "button_name": "dpad_right", "kind": "move", "selected_joint": 1, "step_deg": 0.5, "execute": false, "joint": 0, "delta": 0.5, "accepted": true, "measured_before": [1.0, 1.0, 0.0, 0.0, 0.0, -30.0], "start_source": "command", "cmd_gap_deg": 0.0, "clip": [], "refused": [], "limit_ja": "", "limit_ascii": "", "command": [1.5, 1.0, 0.0, 0.0, 0.0, -30.0], "sent": true, "reached": true, "reached_angles": [1.5, 1.0, 0.0, 0.0, 0.0, -30.0], "reach_err": 0.0, "t_send": 1793615246.614913, "dry": true, "grip_state": "unknown", "stopped": false, "stop_reason": null, "base_pose": [0.0, 0.0, 0.0, 0.0, 0.0, -30.0]}
{"seq": 11, "t": "2026-11-02T19:27:28.788", "t_unix": 1793615248.789, "button": 3, "button_name": "triangle", "kind": "select_joint", "selected_joint": 2, "step_deg": 0.5, "execute": false, "accepted": true, "grip_state": "unknown", "stopped": false, "stop_reason": null, "base_pose": [0.0, 0.0, 0.0, 0.0, 0.0, -30.0]}
{"seq": 12, "t": "2026-11-02T19:27:29.640", "t_unix": 1793615249.64, "button": 14, "button_name": "dpad_right", "kind": "move", "selected_joint": 2, "step_deg": 0.5, "execute": false, "joint": 0, "delta": 0.5, "accepted": true, "measured_before": [1.5, 1.0, 0.0, 0.0, 0.0, -30.0], "start_source": "command", "cmd_gap_deg": 0.0, "clip": [], "refused": [], "limit_ja": "", "limit_ascii": "", "command": [2.0, 1.0, 0.0, 0.0, 0.0, -30.0], "sent": true, "reached": true, "reached_angles": [2.0, 1.0, 0.0, 0.0, 0.0, -30.0], "reach_err": 0.0, "t_send": 1793615249.640985, "dry": true, "grip_state": "unknown", "stopped": false, "stop_reason": null, "base_pose": [0.0, 0.0, 0.0, 0.0, 0.0, -30.0]}
{"seq": 13, "t": "2026-11-02T19:27:30.239", "t_unix": 1793615250.239, "button": 14, "button_name": "dpad_right", "kind": "move", "selected_joint": 2, "step_deg": 0.5, "execute": false, "joint": 0, "delta": 0.5, "accepted": true, "measured_before": [2.0, 1.0, 0.0, 0.0, 0.0, -30.0], "start_source": "command", "cmd_gap_deg": 0.0, "clip": [], "refused": [], "limit_ja": "", "limit_ascii": "", "command": [2.5, 1.0, 0.0, 0.0, 0.0, -30.0], "sent": true, "reached": true, "reached_angles": [2.5, 1.0, 0.0, 0.0, 0.0, -30.0], "reach_err": 0.0, "t_send": 1793615250.2401779, "dry": true, "grip_state": "unknown", "stopped": false, "stop_reason": null, "base_pose": [0.0, 0.0, 0.0, 0.0, 0.0, -30.0]}
{"seq": 14, "t": "2026-11-02T19:27:30.846", "t_unix": 1793615250.846, "button": 14, "button_name": "dpad_right", "kind": "move", "selected_joint": 2, "step_deg": 0.5, "execute": false, "joint": 0, "delta": 0.5, "accepted": true, "measured_before": [2.5, 1.0, 0.0, 0.0, 0.0, -30.0], "start_source": "command", "cmd_gap_deg": 0.0, "clip": [], "refused": [], "limit_ja": "", "limit_ascii": "", "command": [3.0, 1.0, 0.0, 0.0, 0.0, -30.0], "sent": true, "reached": true, "reached_angles": [3.0, 1.0, 0.0, 0.0, 0.0, -30.0], "reach_err": 0.0, "t_send": 1793615250.847351, "dry": true, "grip_state": "unknown", "stopped": false, "stop_reason": null, "base_pose": [0.0, 0.0, 0.0, 0.0, 0.0, -30.0]}
{"seq": 15, "t": "2026-11-02T19:27:31.393", "t_unix": 1793615251.393, "button": 14, "button_name": "dpad_right", "kind": "move", "selected_joint": 2, "step_deg": 0.5, "execute": false, "joint": 0, "delta": 0.5, "accepted": true, "measured_before": [3.0, 1.0, 0.0, 0.0, 0.0, -30.0], "start_source": "command", "cmd_gap_deg": 0.0, "clip": [], "refused": [], "limit_ja": "", "limit_ascii": "", "command": [3.5, 1.0, 0.0, 0.0, 0.0, -30.0], "sent": true, "reached": true, "reached_angles": [3.5, 1.0, 0.0, 0.0, 0.0, -30.0], "reach_err": 0.0, "t_send": 1793615251.394119, "dry": true, "grip_state": "unknown", "stopped": false, "stop_reason": null, "base_pose": [0.0, 0.0, 0.0, 0.0, 0.0, -30.0]}
{"seq": 16, "t": "2026-11-02T19:27:31.945", "t_unix": 1793615251.946, "button": 14, "button_name": "dpad_right", "kind": "move", "selected_joint": 2, "step_deg": 0.5, "execute": false, "joint": 0, "delta": 0.5, "accepted": true, "measured_before": [3.5, 1.0, 0.0, 0.0, 0.0, -30.0], "start_source": "command", "cmd_gap_deg": 0.0, "clip": [], "refused": [], "limit_ja": "", "limit_ascii": "", "command": [4.0, 1.0, 0.0, 0.0, 0.0, -30.0], "sent": true, "reached": true, "reached_angles": [4.0, 1.0, 0.0, 0.0, 0.0, -30.0], "reach_err": 0.0, "t_send": 1793615251.946812, "dry": true, "grip_state": "unknown", "stopped": false, "stop_reason": null, "base_pose": [0.0, 0.0, 0.0, 0.0, 0.0, -30.0]}
{"seq": 17, "t": "2026-11-02T19:27:33.388", "t_unix": 1793615253.389, "button": 3, "button_name": "triangle", "kind": "select_joint", "selected_joint": 3, "step_deg": 0.5, "execute": false, "accepted": true, "grip_state": "unknown", "stopped": false, "stop_reason": null, "base_pose": [0.0, 0.0, 0.0, 0.0, 0.0, -30.0]}
{"seq": 18, "t": "2026-11-02T19:27:35.742", "t_unix": 1793615255.742, "button": 14, "button_name": "dpad_right", "kind": "move", "selected_joint": 3, "step_deg": 0.5, "execute": false, "joint": 0, "delta": 0.5, "accepted": true, "measured_before": [4.0, 1.0, 0.0, 0.0, 0.0, -30.0], "start_source": "command", "cmd_gap_deg": 0.0, "clip": [], "refused": [], "limit_ja": "", "limit_ascii": "", "command": [4.5, 1.0, 0.0, 0.0, 0.0, -30.0], "sent": true, "reached": true, "reached_angles": [4.5, 1.0, 0.0, 0.0, 0.0, -30.0], "reach_err": 0.0, "t_send": 1793615255.743019, "dry": true, "grip_state": "unknown", "stopped": false, "stop_reason": null, "base_pose": [0.0, 0.0, 0.0, 0.0, 0.0, -30.0]}
{"seq": 19, "t": "2026-11-02T19:27:36.498", "t_unix": 1793615256.499, "button": 12, "button_name": "dpad_down", "kind": "move", "selected_joint": 3, "step_deg": 0.5, "execute": false, "joint": 3, "delta": 0.5, "accepted": true, "measured_before": [4.5, 1.0, 0.0, 0.0, 0.0, -30.0], "start_source": "command", "cmd_gap_deg": 0.0, "clip": [], "refused": [], "limit_ja": "", "limit_ascii": "", "command": [4.5, 1.0, 0.0, 0.5, 0.0, -30.0], "sent": true, "reached": true, "reached_angles": [4.5, 1.0, 0.0, 0.5, 0.0, -30.0], "reach_err": 0.0, "t_send": 1793615256.499892, "dry": true, "grip_state": "unknown", "stopped": false, "stop_reason": null, "base_pose": [0.0, 0.0, 0.0, 0.0, 0.0, -30.0]}
{"seq": 20, "t": "2026-11-02T19:27:36.950", "t_unix": 1793615256.95, "button": 12, "button_name": "dpad_down", "kind": "move", "selected_joint": 3, "step_deg": 0.5, "execute": false, "joint": 3, "delta": 0.5, "accepted": true, "measured_before": [4.5, 1.0, 0.0, 0.5, 0.0, -30.0], "start_source": "command", "cmd_gap_deg": 0.0, "clip": [], "refused": [], "limit_ja": "", "limit_ascii": "", "command": [4.5, 1.0, 0.0, 1.0, 0.0, -30.0], "sent": true, "reached": true, "reached_angles": [4.5, 1.0, 0.0, 1.0, 0.0, -30.0], "reach_err": 0.0, "t_send": 1793615256.9513521, "dry": true, "grip_state": "unknown", "stopped": false, "stop_reason": null, "base_pose": [0.0, 0.0, 0.0, 0.0, 0.0, -30.0]}
{"seq": 21, "t": "2026-11-02T19:27:37.644", "t_unix": 1793615257.645, "button": 11, "button_name": "dpad_up", "kind": "move", "selected_joint": 3, "step_deg": 0.5, "execute": false, "joint": 3, "delta": -0.5, "accepted": true, "measured_before": [4.5, 1.0, 0.0, 1.0, 0.0, -30.0], "start_source": "command", "cmd_gap_deg": 0.0, "clip": [], "refused": [], "limit_ja": "", "limit_ascii": "", "command": [4.5, 1.0, 0.0, 0.5, 0.0, -30.0], "sent": true, "reached": true, "reached_angles": [4.5, 1.0, 0.0, 0.5, 0.0, -30.0], "reach_err": 0.0, "t_send": 1793615257.645942, "dry": true, "grip_state": "unknown", "stopped": false, "stop_reason": null, "base_pose": [0.0, 0.0, 0.0, 0.0, 0.0, -30.0]}
{"seq": 22, "t": "2026-11-02T19:27:38.011", "t_unix": 1793615258.011, "button": 11, "button_name": "dpad_up", "kind": "move", "selected_joint": 3, "step_deg": 0.5, "execute": false, "joint": 3, "delta": -0.5, "accepted": true, "measured_before": [4.5, 1.0, 0.0, 0.5, 0.0, -30.0], "start_source": "command", "cmd_gap_deg": 0.0, "clip": [], "refused": [], "limit_ja": "", "limit_ascii": "", "command": [4.5, 1.0, 0.0, 0.0, 0.0, -30.0], "sent": true, "reached": true, "reached_angles": [4.5, 1.0, 0.0, 0.0, 0.0, -30.0], "reach_err": 0.0, "t_send": 1793615258.011798, "dry": true, "grip_state": "unknown", "stopped": false, "stop_reason": null, "base_pose": [0.0, 0.0, 0.0, 0.0, 0.0, -30.0]}
{"seq": 23, "t": "2026-11-02T19:27:38.496", "t_unix": 1793615258.497, "button": 11, "button_name": "dpad_up", "kind": "move", "selected_joint": 3, "step_deg": 0.5, "execute": false, "joint": 3, "delta": -0.5, "accepted": true, "measured_before": [4.5, 1.0, 0.0, 0.0, 0.0, -30.0], "start_source": "command", "cmd_gap_deg": 0.0, "clip": [], "refused": [], "limit_ja": "", "limit_ascii": "", "command": [4.5, 1.0, 0.0, -0.5, 0.0, -30.0], "sent": true, "reached": true, "reached_angles": [4.5, 1.0, 0.0, -0.5, 0.0, -30.0], "reach_err": 0.0, "t_send": 1793615258.497492, "dry": true, "grip_state": "unknown", "stopped": false, "stop_reason": null, "base_pose": [0.0, 0.0, 0.0, 0.0, 0.0, -30.0]}
{"seq": 24, "t": "2026-11-02T19:27:39.434", "t_unix": 1793615259.435, "button": 11, "button_name": "dpad_up", "kind": "move", "selected_joint": 3, "step_deg": 0.5, "execute": false, "joint": 3, "delta": -0.5, "accepted": true, "measured_before": [4.5, 1.0, 0.0, -0.5, 0.0, -30.0], "start_source": "command", "cmd_gap_deg": 0.0, "clip": [], "refused": [], "limit_ja": "", "limit_ascii": "", "command": [4.5, 1.0, 0.0, -1.0, 0.0, -30.0], "sent": true, "reached": true, "reached_angles": [4.5, 1.0, 0.0, -1.0, 0.0, -30.0], "reach_err": 0.0, "t_send": 1793615259.4358559, "dry": true, "grip_state": "unknown", "stopped": false, "stop_reason": null, "base_pose": [0.0, 0.0, 0.0, 0.0, 0.0, -30.0]}
{"seq": 25, "t": "2026-11-02T19:27:40.160", "t_unix": 1793615260.161, "button": 11, "button_name": "dpad_up", "kind": "move", "selected_joint": 3, "step_deg": 0.5, "execute": false, "joint": 3, "delta": -0.5, "accepted": true, "measured_before": [4.5, 1.0, 0.0, -1.0, 0.0, -30.0], "start_source": "command", "cmd_gap_deg": 0.0, "clip": [], "refused": [], "limit_ja": "", "limit_ascii": "", "command": [4.5, 1.0, 0.0, -1.5, 0.0, -30.0], "sent": true, "reached": true, "reached_angles": [4.5, 1.0, 0.0, -1.5, 0.0, -30.0], "reach_err": 0.0, "t_send": 1793615260.16168, "dry": true, "grip_state": "unknown", "stopped": false, "stop_reason": null, "base_pose": [0.0, 0.0, 0.0, 0.0, 0.0, -30.0]}
{"seq": 27, "t": "2026-11-02T19:27:41.250", "t_unix": 1793615261.25, "button": 11, "button_name": "dpad_up", "kind": "move", "selected_joint": 3, "step_deg": 0.5, "execute": false, "joint": 3, "delta": -0.5, "accepted": false, "reason": "busy", "grip_state": "unknown", "stopped": false, "stop_reason": null, "base_pose": [0.0, 0.0, 0.0, 0.0, 0.0, -30.0]}
{"seq": 26, "t": "2026-11-02T19:27:40.930", "t_unix": 1793615260.93, "button": 11, "button_name": "dpad_up", "kind": "move", "selected_joint": 3, "step_deg": 0.5, "execute": false, "joint": 3, "delta": -0.5, "accepted": true, "measured_before": [4.5, 1.0, 0.0, -1.5, 0.0, -30.0], "start_source": "command", "cmd_gap_deg": 0.0, "clip": [], "refused": [], "limit_ja": "", "limit_ascii": "", "command": [4.5, 1.0, 0.0, -2.0, 0.0, -30.0], "sent": true, "reached": true, "reached_angles": [4.5, 1.0, 0.0, -2.0, 0.0, -30.0], "reach_err": 0.0, "t_send": 1793615260.931148, "dry": true, "grip_state": "unknown", "stopped": false, "stop_reason": null, "base_pose": [0.0, 0.0, 0.0, 0.0, 0.0, -30.0]}
{"seq": 28, "t": "2026-11-02T19:27:41.802", "t_unix": 1793615261.803, "button": 11, "button_name": "dpad_up", "kind": "move", "selected_joint": 3, "step_deg": 0.5, "execute": false, "joint": 3, "delta": -0.5, "accepted": true, "measured_before": [4.5, 1.0, 0.0, -2.0, 0.0, -30.0], "start_source": "command", "cmd_gap_deg": 0.0, "clip": [], "refused": [], "limit_ja": "", "limit_ascii": "", "command": [4.5, 1.0, 0.0, -2.5, 0.0, -30.0], "sent": true, "reached": true, "reached_angles": [4.5, 1.0, 0.0, -2.5, 0.0, -30.0], "reach_err": 0.0, "t_send": 1793615261.803781, "dry": true, "grip_state": "unknown", "stopped": false, "stop_reason": null, "base_pose": [0.0, 0.0, 0.0, 0.0, 0.0, -30.0]}
{"seq": 30, "t": "2026-11-02T19:27:42.898", "t_unix": 1793615262.898, "button": 12, "button_name": "dpad_down", "kind": "move", "selected_joint": 3, "step_deg": 0.5, "execute": false, "joint": 3, "delta": 0.5, "accepted": false, "reason": "busy", "grip_state": "unknown", "stopped": false, "stop_reason": null, "base_pose": [0.0, 0.0, 0.0, 0.0, 0.0, -30.0]}
{"seq": 29, "t": "2026-11-02T19:27:42.645", "t_unix": 1793615262.645, "button": 12, "button_name": "dpad_down", "kind": "move", "selected_joint": 3, "step_deg": 0.5, "execute": false, "joint": 3, "delta": 0.5, "accepted": true, "measured_before": [4.5, 1.0, 0.0, -2.5, 0.0, -30.0], "start_source": "command", "cmd_gap_deg": 0.0, "clip": [], "refused": [], "limit_ja": "", "limit_ascii": "", "command": [4.5, 1.0, 0.0, -2.0, 0.0, -30.0], "sent": true, "reached": true, "reached_angles": [4.5, 1.0, 0.0, -2.0, 0.0, -30.0], "reach_err": 0.0, "t_send": 1793615262.646216, "dry": true, "grip_state": "unknown", "stopped": false, "stop_reason": null, "base_pose": [0.0, 0.0, 0.0, 0.0, 0.0, -30.0]}
{"seq": 32, "t": "2026-11-02T19:27:44.649", "t_unix": 1793615264.65, "button": 12, "button_name": "dpad_down", "kind": "move", "selected_joint": 3, "step_deg": 0.5, "execute": false, "joint": 3, "delta": 0.5, "accepted": false, "reason": "busy", "grip_state": "unknown", "stopped": false, "stop_reason": null, "base_pose": [0.0, 0.0, 0.0, 0.0, 0.0, -30.0]}
{"seq": 31, "t": "2026-11-02T19:27:44.395", "t_unix": 1793615264.395, "button": 12, "button_name": "dpad_down", "kind": "move", "selected_joint": 3, "step_deg": 0.5, "execute": false, "joint": 3, "delta": 0.5, "accepted": true, "measured_before": [4.5, 1.0, 0.0, -2.0, 0.0, -30.0], "start_source": "command", "cmd_gap_deg": 0.0, "clip": [], "refused": [], "limit_ja": "", "limit_ascii": "", "command": [4.5, 1.0, 0.0, -1.5, 0.0, -30.0], "sent": true, "reached": true, "reached_angles": [4.5, 1.0, 0.0, -1.5, 0.0, -30.0], "reach_err": 0.0, "t_send": 1793615264.396405, "dry": true, "grip_state": "unknown", "stopped": false, "stop_reason": null, "base_pose": [0.0, 0.0, 0.0, 0.0, 0.0, -30.0]}
{"seq": 33, "t": "2026-11-02T19:27:44.836", "t_unix": 1793615264.836, "button": 12, "button_name": "dpad_down", "kind": "move", "selected_joint": 3, "step_deg": 0.5, "execute": false, "joint": 3, "delta": 0.5, "accepted": true, "measured_before": [4.5, 1.0, 0.0, -1.5, 0.0, -30.0], "start_source": "command", "cmd_gap_deg": 0.0, "clip": [], "refused": [], "limit_ja": "", "limit_ascii": "", "command": [4.5, 1.0, 0.0, -1.0, 0.0, -30.0], "sent": true, "reached": true, "reached_angles": [4.5, 1.0, 0.0, -1.0, 0.0, -30.0], "reach_err": 0.0, "t_send": 1793615264.837267, "dry": true, "grip_state": "unknown", "stopped": false, "stop_reason": null, "base_pose": [0.0, 0.0, 0.0, 0.0, 0.0, -30.0]}
{"seq": 35, "t": "2026-11-02T19:27:45.357", "t_unix": 1793615265.357, "button": 12, "button_name": "dpad_down", "kind": "move", "selected_joint": 3, "step_deg": 0.5, "execute": false, "joint": 3, "delta": 0.5, "accepted": false, "reason": "busy", "grip_state": "unknown", "stopped": false, "stop_reason": null, "base_pose": [0.0, 0.0, 0.0, 0.0, 0.0, -30.0]}
{"seq": 34, "t": "2026-11-02T19:27:45.171", "t_unix": 1793615265.172, "button": 12, "button_name": "dpad_down", "kind": "move", "selected_joint": 3, "step_deg": 0.5, "execute": false, "joint": 3, "delta": 0.5, "accepted": true, "measured_before": [4.5, 1.0, 0.0, -1.0, 0.0, -30.0], "start_source": "command", "cmd_gap_deg": 0.0, "clip": [], "refused": [], "limit_ja": "", "limit_ascii": "", "command": [4.5, 1.0, 0.0, -0.5, 0.0, -30.0], "sent": true, "reached": true, "reached_angles": [4.5, 1.0, 0.0, -0.5, 0.0, -30.0], "reach_err": 0.0, "t_send": 1793615265.172242, "dry": true, "grip_state": "unknown", "stopped": false, "stop_reason": null, "base_pose": [0.0, 0.0, 0.0, 0.0, 0.0, -30.0]}
{"seq": 37, "t": "2026-11-02T19:27:45.944", "t_unix": 1793615265.945, "button": 12, "button_name": "dpad_down", "kind": "move", "selected_joint": 3, "step_deg": 0.5, "execute": false, "joint": 3, "delta": 0.5, "accepted": false, "reason": "busy", "grip_state": "unknown", "stopped": false, "stop_reason": null, "base_pose": [0.0, 0.0, 0.0, 0.0, 0.0, -30.0]}
{"seq": 36, "t": "2026-11-02T19:27:45.693", "t_unix": 1793615265.694, "button": 12, "button_name": "dpad_down", "kind": "move", "selected_joint": 3, "step_deg": 0.5, "execute": false, "joint": 3, "delta": 0.5, "accepted": true, "measured_before": [4.5, 1.0, 0.0, -0.5, 0.0, -30.0], "start_source": "command", "cmd_gap_deg": 0.0, "clip": [], "refused": [], "limit_ja": "", "limit_ascii": "", "command": [4.5, 1.0, 0.0, 0.0, 0.0, -30.0], "sent": true, "reached": true, "reached_angles": [4.5, 1.0, 0.0, 0.0, 0.0, -30.0], "reach_err": 0.0, "t_send": 1793615265.694686, "dry": true, "grip_state": "unknown", "stopped": false, "stop_reason": null, "base_pose": [0.0, 0.0, 0.0, 0.0, 0.0, -30.0]}
{"seq": 39, "t": "2026-11-02T19:27:46.544", "t_unix": 1793615266.544, "button": 12, "button_name": "dpad_down", "kind": "move", "selected_joint": 3, "step_deg": 0.5, "execute": false, "joint": 3, "delta": 0.5, "accepted": false, "reason": "busy", "grip_state": "unknown", "stopped": false, "stop_reason": null, "base_pose": [0.0, 0.0, 0.0, 0.0, 0.0, -30.0]}
{"seq": 38, "t": "2026-11-02T19:27:46.280", "t_unix": 1793615266.28, "button": 12, "button_name": "dpad_down", "kind": "move", "selected_joint": 3, "step_deg": 0.5, "execute": false, "joint": 3, "delta": 0.5, "accepted": true, "measured_before": [4.5, 1.0, 0.0, 0.0, 0.0, -30.0], "start_source": "command", "cmd_gap_deg": 0.0, "clip": [], "refused": [], "limit_ja": "", "limit_ascii": "", "command": [4.5, 1.0, 0.0, 0.5, 0.0, -30.0], "sent": true, "reached": true, "reached_angles": [4.5, 1.0, 0.0, 0.5, 0.0, -30.0], "reach_err": 0.0, "t_send": 1793615266.2813098, "dry": true, "grip_state": "unknown", "stopped": false, "stop_reason": null, "base_pose": [0.0, 0.0, 0.0, 0.0, 0.0, -30.0]}
{"seq": 41, "t": "2026-11-02T19:27:47.269", "t_unix": 1793615267.27, "button": 12, "button_name": "dpad_down", "kind": "move", "selected_joint": 3, "step_deg": 0.5, "execute": false, "joint": 3, "delta": 0.5, "accepted": false, "reason": "busy", "grip_state": "unknown", "stopped": false, "stop_reason": null, "base_pose": [0.0, 0.0, 0.0, 0.0, 0.0, -30.0]}
{"seq": 40, "t": "2026-11-02T19:27:46.947", "t_unix": 1793615266.948, "button": 12, "button_name": "dpad_down", "kind": "move", "selected_joint": 3, "step_deg": 0.5, "execute": false, "joint": 3, "delta": 0.5, "accepted": true, "measured_before": [4.5, 1.0, 0.0, 0.5, 0.0, -30.0], "start_source": "command", "cmd_gap_deg": 0.0, "clip": [], "refused": [], "limit_ja": "", "limit_ascii": "", "command": [4.5, 1.0, 0.0, 1.0, 0.0, -30.0], "sent": true, "reached": true, "reached_angles": [4.5, 1.0, 0.0, 1.0, 0.0, -30.0], "reach_err": 0.0, "t_send": 1793615266.948599, "dry": true, "grip_state": "unknown", "stopped": false, "stop_reason": null, "base_pose": [0.0, 0.0, 0.0, 0.0, 0.0, -30.0]}
{"seq": 43, "t": "2026-11-02T19:27:47.789", "t_unix": 1793615267.79, "button": 12, "button_name": "dpad_down", "kind": "move", "selected_joint": 3, "step_deg": 0.5, "execute": false, "joint": 3, "delta": 0.5, "accepted": false, "reason": "busy", "grip_state": "unknown", "stopped": false, "stop_reason": null, "base_pose": [0.0, 0.0, 0.0, 0.0, 0.0, -30.0]}
{"seq": 42, "t": "2026-11-02T19:27:47.486", "t_unix": 1793615267.487, "button": 12, "button_name": "dpad_down", "kind": "move", "selected_joint": 3, "step_deg": 0.5, "execute": false, "joint": 3, "delta": 0.5, "accepted": true, "measured_before": [4.5, 1.0, 0.0, 1.0, 0.0, -30.0], "start_source": "command", "cmd_gap_deg": 0.0, "clip": [], "refused": [], "limit_ja": "", "limit_ascii": "", "command": [4.5, 1.0, 0.0, 1.5, 0.0, -30.0], "sent": true, "reached": true, "reached_angles": [4.5, 1.0, 0.0, 1.5, 0.0, -30.0], "reach_err": 0.0, "t_send": 1793615267.48761, "dry": true, "grip_state": "unknown", "stopped": false, "stop_reason": null, "base_pose": [0.0, 0.0, 0.0, 0.0, 0.0, -30.0]}
{"seq": 44, "t": "2026-11-02T19:27:48.490", "t_unix": 1793615268.491, "button": 1, "button_name": "circle", "kind": "grip_open", "selected_joint": 3, "step_deg": 0.5, "execute": false, "grip_cmd": 0, "accepted": true, "measured_before": null, "sent": true, "reached": null, "grip_state": "open", "stopped": false, "stop_reason": null, "base_pose": [0.0, 0.0, 0.0, 0.0, 0.0, -30.0]}
{"seq": 45, "t": "2026-11-02T19:27:50.942", "t_unix": 1793615270.943, "button": 12, "button_name": "dpad_down", "kind": "move", "selected_joint": 3, "step_deg": 0.5, "execute": false, "joint": 3, "delta": 0.5, "accepted": true, "measured_before": [4.5, 1.0, 0.0, 1.5, 0.0, -30.0], "start_source": "command", "cmd_gap_deg": 0.0, "clip": [], "refused": [], "limit_ja": "", "limit_ascii": "", "command": [4.5, 1.0, 0.0, 2.0, 0.0, -30.0], "sent": true, "reached": true, "reached_angles": [4.5, 1.0, 0.0, 2.0, 0.0, -30.0], "reach_err": 0.0, "t_send": 1793615270.9430451, "dry": true, "grip_state": "open", "stopped": false, "stop_reason": null, "base_pose": [0.0, 0.0, 0.0, 0.0, 0.0, -30.0]}
{"seq": 46, "t": "2026-11-02T19:27:52.032", "t_unix": 1793615272.033, "button": 12, "button_name": "dpad_down", "kind": "move", "selected_joint": 3, "step_deg": 0.5, "execute": false, "joint": 3, "delta": 0.5, "accepted": true, "measured_before": [4.5, 1.0, 0.0, 2.0, 0.0, -30.0], "start_source": "command", "cmd_gap_deg": 0.0, "clip": [], "refused": [], "limit_ja": "", "limit_ascii": "", "command": [4.5, 1.0, 0.0, 2.5, 0.0, -30.0], "sent": true, "reached": true, "reached_angles": [4.5, 1.0, 0.0, 2.5, 0.0, -30.0], "reach_err": 0.0, "t_send": 1793615272.03394, "dry": true, "grip_state": "open", "stopped": false, "stop_reason": null, "base_pose": [0.0, 0.0, 0.0, 0.0, 0.0, -30.0]}
{"seq": 47, "t": "2026-11-02T19:27:53.090", "t_unix": 1793615273.091, "button": 12, "button_name": "dpad_down", "kind": "move", "selected_joint": 3, "step_deg": 0.5, "execute": false, "joint": 3, "delta": 0.5, "accepted": true, "measured_before": [4.5, 1.0, 0.0, 2.5, 0.0, -30.0], "start_source": "command", "cmd_gap_deg": 0.0, "clip": [], "refused": [], "limit_ja": "", "limit_ascii": "", "command": [4.5, 1.0, 0.0, 3.0, 0.0, -30.0], "sent": true, "reached": true, "reached_angles": [4.5, 1.0, 0.0, 3.0, 0.0, -30.0], "reach_err": 0.0, "t_send": 1793615273.0916228, "dry": true, "grip_state": "open", "stopped": false, "stop_reason": null, "base_pose": [0.0, 0.0, 0.0, 0.0, 0.0, -30.0]}
{"seq": 48, "t": "2026-11-02T19:27:54.146", "t_unix": 1793615274.146, "button": 12, "button_name": "dpad_down", "kind": "move", "selected_joint": 3, "step_deg": 0.5, "execute": false, "joint": 3, "delta": 0.5, "accepted": true, "measured_before": [4.5, 1.0, 0.0, 3.0, 0.0, -30.0], "start_source": "command", "cmd_gap_deg": 0.0, "clip": [], "refused": [], "limit_ja": "", "limit_ascii": "", "command": [4.5, 1.0, 0.0, 3.5, 0.0, -30.0], "sent": true, "reached": true, "reached_angles": [4.5, 1.0, 0.0, 3.5, 0.0, -30.0], "reach_err": 0.0, "t_send": 1793615274.146797, "dry": true, "grip_state": "open", "stopped": false, "stop_reason": null, "base_pose": [0.0, 0.0, 0.0, 0.0, 0.0, -30.0]}
{"seq": 50, "t": "2026-11-02T19:27:55.991", "t_unix": 1793615275.992, "button": 12, "button_name": "dpad_down", "kind": "move", "selected_joint": 3, "step_deg": 0.5, "execute": false, "joint": 3, "delta": 0.5, "accepted": false, "reason": "busy", "grip_state": "open", "stopped": false, "stop_reason": null, "base_pose": [0.0, 0.0, 0.0, 0.0, 0.0, -30.0]}
{"seq": 49, "t": "2026-11-02T19:27:55.354", "t_unix": 1793615275.354, "button": 1, "button_name": "circle", "kind": "grip_open", "selected_joint": 3, "step_deg": 0.5, "execute": false, "grip_cmd": 0, "accepted": true, "measured_before": null, "sent": true, "reached": null, "grip_state": "open", "stopped": false, "stop_reason": null, "base_pose": [0.0, 0.0, 0.0, 0.0, 0.0, -30.0]}
{"seq": 51, "t": "2026-11-02T19:27:57.121", "t_unix": 1793615277.121, "button": 12, "button_name": "dpad_down", "kind": "move", "selected_joint": 3, "step_deg": 0.5, "execute": false, "joint": 3, "delta": 0.5, "accepted": true, "measured_before": [4.5, 1.0, 0.0, 3.5, 0.0, -30.0], "start_source": "command", "cmd_gap_deg": 0.0, "clip": [], "refused": [], "limit_ja": "", "limit_ascii": "", "command": [4.5, 1.0, 0.0, 4.0, 0.0, -30.0], "sent": true, "reached": true, "reached_angles": [4.5, 1.0, 0.0, 4.0, 0.0, -30.0], "reach_err": 0.0, "t_send": 1793615277.1223638, "dry": true, "grip_state": "open", "stopped": false, "stop_reason": null, "base_pose": [0.0, 0.0, 0.0, 0.0, 0.0, -30.0]}
{"seq": 52, "t": "2026-11-02T19:27:57.593", "t_unix": 1793615277.593, "button": 12, "button_name": "dpad_down", "kind": "move", "selected_joint": 3, "step_deg": 0.5, "execute": false, "joint": 3, "delta": 0.5, "accepted": true, "measured_before": [4.5, 1.0, 0.0, 4.0, 0.0, -30.0], "start_source": "command", "cmd_gap_deg": 0.0, "clip": [], "refused": [], "limit_ja": "", "limit_ascii": "", "command": [4.5, 1.0, 0.0, 4.5, 0.0, -30.0], "sent": true, "reached": true, "reached_angles": [4.5, 1.0, 0.0, 4.5, 0.0, -30.0], "reach_err": 0.0, "t_send": 1793615277.5946538, "dry": true, "grip_state": "open", "stopped": false, "stop_reason": null, "base_pose": [0.0, 0.0, 0.0, 0.0, 0.0, -30.0]}
{"seq": 53, "t": "2026-11-02T19:27:58.484", "t_unix": 1793615278.485, "button": 11, "button_name": "dpad_up", "kind": "move", "selected_joint": 3, "step_deg": 0.5, "execute": false, "joint": 3, "delta": -0.5, "accepted": true, "measured_before": [4.5, 1.0, 0.0, 4.5, 0.0, -30.0], "start_source": "command", "cmd_gap_deg": 0.0, "clip": [], "refused": [], "limit_ja": "", "limit_ascii": "", "command": [4.5, 1.0, 0.0, 4.0, 0.0, -30.0], "sent": true, "reached": true, "reached_angles": [4.5, 1.0, 0.0, 4.0, 0.0, -30.0], "reach_err": 0.0, "t_send": 1793615278.485688, "dry": true, "grip_state": "open", "stopped": false, "stop_reason": null, "base_pose": [0.0, 0.0, 0.0, 0.0, 0.0, -30.0]}
{"seq": 54, "t": "2026-11-02T19:27:59.302", "t_unix": 1793615279.303, "button": 1, "button_name": "circle", "kind": "grip_open", "selected_joint": 3, "step_deg": 0.5, "execute": false, "grip_cmd": 0, "accepted": true, "measured_before": null, "sent": true, "reached": null, "grip_state": "open", "stopped": false, "stop_reason": null, "base_pose": [0.0, 0.0, 0.0, 0.0, 0.0, -30.0]}
{"seq": 55, "t": "2026-11-02T19:28:12.801", "t_unix": 1793615292.802, "button": 13, "button_name": "dpad_left", "kind": "move", "selected_joint": 3, "step_deg": 0.5, "execute": false, "joint": 0, "delta": -0.5, "accepted": true, "measured_before": [4.5, 1.0, 0.0, 4.0, 0.0, -30.0], "start_source": "command", "cmd_gap_deg": 0.0, "clip": [], "refused": [], "limit_ja": "", "limit_ascii": "", "command": [4.0, 1.0, 0.0, 4.0, 0.0, -30.0], "sent": true, "reached": true, "reached_angles": [4.0, 1.0, 0.0, 4.0, 0.0, -30.0], "reach_err": 0.0, "t_send": 1793615292.802093, "dry": true, "grip_state": "open", "stopped": false, "stop_reason": null, "base_pose": [0.0, 0.0, 0.0, 0.0, 0.0, -30.0]}
{"seq": 56, "t": "2026-11-02T19:28:13.645", "t_unix": 1793615293.645, "button": 13, "button_name": "dpad_left", "kind": "move", "selected_joint": 3, "step_deg": 0.5, "execute": false, "joint": 0, "delta": -0.5, "accepted": true, "measured_before": [4.0, 1.0, 0.0, 4.0, 0.0, -30.0], "start_source": "command", "cmd_gap_deg": 0.0, "clip": [], "refused": [], "limit_ja": "", "limit_ascii": "", "command": [3.5, 1.0, 0.0, 4.0, 0.0, -30.0], "sent": true, "reached": true, "reached_angles": [3.5, 1.0, 0.0, 4.0, 0.0, -30.0], "reach_err": 0.0, "t_send": 1793615293.646611, "dry": true, "grip_state": "open", "stopped": false, "stop_reason": null, "base_pose": [0.0, 0.0, 0.0, 0.0, 0.0, -30.0]}
{"seq": 57, "t": "2026-11-02T19:28:14.611", "t_unix": 1793615294.611, "button": 14, "button_name": "dpad_right", "kind": "move", "selected_joint": 3, "step_deg": 0.5, "execute": false, "joint": 0, "delta": 0.5, "accepted": true, "measured_before": [3.5, 1.0, 0.0, 4.0, 0.0, -30.0], "start_source": "command", "cmd_gap_deg": 0.0, "clip": [], "refused": [], "limit_ja": "", "limit_ascii": "", "command": [4.0, 1.0, 0.0, 4.0, 0.0, -30.0], "sent": true, "reached": true, "reached_angles": [4.0, 1.0, 0.0, 4.0, 0.0, -30.0], "reach_err": 0.0, "t_send": 1793615294.6116009, "dry": true, "grip_state": "open", "stopped": false, "stop_reason": null, "base_pose": [0.0, 0.0, 0.0, 0.0, 0.0, -30.0]}
{"seq": 58, "t": "2026-11-02T19:28:15.396", "t_unix": 1793615295.396, "button": 14, "button_name": "dpad_right", "kind": "move", "selected_joint": 3, "step_deg": 0.5, "execute": false, "joint": 0, "delta": 0.5, "accepted": true, "measured_before": [4.0, 1.0, 0.0, 4.0, 0.0, -30.0], "start_source": "command", "cmd_gap_deg": 0.0, "clip": [], "refused": [], "limit_ja": "", "limit_ascii": "", "command": [4.5, 1.0, 0.0, 4.0, 0.0, -30.0], "sent": true, "reached": true, "reached_angles": [4.5, 1.0, 0.0, 4.0, 0.0, -30.0], "reach_err": 0.0, "t_send": 1793615295.397482, "dry": true, "grip_state": "open", "stopped": false, "stop_reason": null, "base_pose": [0.0, 0.0, 0.0, 0.0, 0.0, -30.0]}
{"seq": 59, "t": "2026-11-02T19:28:25.088", "t_unix": 1793615305.088, "button": 10, "button_name": "r1", "kind": "modifier", "selected_joint": 3, "step_deg": 2.0, "execute": false, "accepted": true, "grip_state": "open", "stopped": false, "stop_reason": null, "base_pose": [0.0, 0.0, 0.0, 0.0, 0.0, -30.0]}
{"seq": 60, "t": "2026-11-02T19:28:25.596", "t_unix": 1793615305.596, "button": 14, "button_name": "dpad_right", "kind": "move", "selected_joint": 3, "step_deg": 2.0, "execute": false, "joint": 0, "delta": 2.0, "accepted": true, "measured_before": [4.5, 1.0, 0.0, 4.0, 0.0, -30.0], "start_source": "command", "cmd_gap_deg": 0.0, "clip": [], "refused": [], "limit_ja": "", "limit_ascii": "", "command": [6.5, 1.0, 0.0, 4.0, 0.0, -30.0], "sent": true, "reached": true, "reached_angles": [6.5, 1.0, 0.0, 4.0, 0.0, -30.0], "reach_err": 0.0, "t_send": 1793615305.596456, "dry": true, "grip_state": "open", "stopped": false, "stop_reason": null, "base_pose": [0.0, 0.0, 0.0, 0.0, 0.0, -30.0]}
{"seq": 61, "t": "2026-11-02T19:28:26.748", "t_unix": 1793615306.748, "button": 14, "button_name": "dpad_right", "kind": "move", "selected_joint": 3, "step_deg": 2.0, "execute": false, "joint": 0, "delta": 2.0, "accepted": true, "measured_before": [6.5, 1.0, 0.0, 4.0, 0.0, -30.0], "start_source": "command", "cmd_gap_deg": 0.0, "clip": [], "refused": [], "limit_ja": "", "limit_ascii": "", "command": [8.5, 1.0, 0.0, 4.0, 0.0, -30.0], "sent": true, "reached": true, "reached_angles": [8.5, 1.0, 0.0, 4.0, 0.0, -30.0], "reach_err": 0.0, "t_send": 1793615306.749216, "dry": true, "grip_state": "open", "stopped": false, "stop_reason": null, "base_pose": [0.0, 0.0, 0.0, 0.0, 0.0, -30.0]}
{"seq": 62, "t": "2026-11-02T19:28:27.552", "t_unix": 1793615307.552, "button": 13, "button_name": "dpad_left", "kind": "move", "selected_joint": 3, "step_deg": 2.0, "execute": false, "joint": 0, "delta": -2.0, "accepted": true, "measured_before": [8.5, 1.0, 0.0, 4.0, 0.0, -30.0], "start_source": "command", "cmd_gap_deg": 0.0, "clip": [], "refused": [], "limit_ja": "", "limit_ascii": "", "command": [6.5, 1.0, 0.0, 4.0, 0.0, -30.0], "sent": true, "reached": true, "reached_angles": [6.5, 1.0, 0.0, 4.0, 0.0, -30.0], "reach_err": 0.0, "t_send": 1793615307.552995, "dry": true, "grip_state": "open", "stopped": false, "stop_reason": null, "base_pose": [0.0, 0.0, 0.0, 0.0, 0.0, -30.0]}
{"seq": 63, "t": "2026-11-02T19:28:28.441", "t_unix": 1793615308.442, "button": 13, "button_name": "dpad_left", "kind": "move", "selected_joint": 3, "step_deg": 2.0, "execute": false, "joint": 0, "delta": -2.0, "accepted": true, "measured_before": [6.5, 1.0, 0.0, 4.0, 0.0, -30.0], "start_source": "command", "cmd_gap_deg": 0.0, "clip": [], "refused": [], "limit_ja": "", "limit_ascii": "", "command": [4.5, 1.0, 0.0, 4.0, 0.0, -30.0], "sent": true, "reached": true, "reached_angles": [4.5, 1.0, 0.0, 4.0, 0.0, -30.0], "reach_err": 0.0, "t_send": 1793615308.443362, "dry": true, "grip_state": "open", "stopped": false, "stop_reason": null, "base_pose": [0.0, 0.0, 0.0, 0.0, 0.0, -30.0]}
{"seq": 64, "t": "2026-11-02T19:28:29.759", "t_unix": 1793615309.759, "button": 14, "button_name": "dpad_right", "kind": "move", "selected_joint": 3, "step_deg": 0.5, "execute": false, "joint": 0, "delta": 0.5, "accepted": true, "measured_before": [4.5, 1.0, 0.0, 4.0, 0.0, -30.0], "start_source": "command", "cmd_gap_deg": 0.0, "clip": [], "refused": [], "limit_ja": "", "limit_ascii": "", "command": [5.0, 1.0, 0.0, 4.0, 0.0, -30.0], "sent": true, "reached": true, "reached_angles": [5.0, 1.0, 0.0, 4.0, 0.0, -30.0], "reach_err": 0.0, "t_send": 1793615309.7603269, "dry": true, "grip_state": "open", "stopped": false, "stop_reason": null, "base_pose": [0.0, 0.0, 0.0, 0.0, 0.0, -30.0]}
{"seq": 65, "t": "2026-11-02T19:28:30.945", "t_unix": 1793615310.945, "button": 14, "button_name": "dpad_right", "kind": "move", "selected_joint": 3, "step_deg": 0.5, "execute": false, "joint": 0, "delta": 0.5, "accepted": true, "measured_before": [5.0, 1.0, 0.0, 4.0, 0.0, -30.0], "start_source": "command", "cmd_gap_deg": 0.0, "clip": [], "refused": [], "limit_ja": "", "limit_ascii": "", "command": [5.5, 1.0, 0.0, 4.0, 0.0, -30.0], "sent": true, "reached": true, "reached_angles": [5.5, 1.0, 0.0, 4.0, 0.0, -30.0], "reach_err": 0.0, "t_send": 1793615310.946372, "dry": true, "grip_state": "open", "stopped": false, "stop_reason": null, "base_pose": [0.0, 0.0, 0.0, 0.0, 0.0, -30.0]}
{"seq": 66, "t": "2026-11-02T19:28:31.753", "t_unix": 1793615311.754, "button": 14, "button_name": "dpad_right", "kind": "move", "selected_joint": 3, "step_deg": 0.5, "execute": false, "joint": 0, "delta": 0.5, "accepted": true, "measured_before": [5.5, 1.0, 0.0, 4.0, 0.0, -30.0], "start_source": "command", "cmd_gap_deg": 0.0, "clip": [], "refused": [], "limit_ja": "", "limit_ascii": "", "command": [6.0, 1.0, 0.0, 4.0, 0.0, -30.0], "sent": true, "reached": true, "reached_angles": [6.0, 1.0, 0.0, 4.0, 0.0, -30.0], "reach_err": 0.0, "t_send": 1793615311.7546618, "dry": true, "grip_state": "open", "stopped": false, "stop_reason": null, "base_pose": [0.0, 0.0, 0.0, 0.0, 0.0, -30.0]}
{"seq": 67, "t": "2026-11-02T19:28:33.748", "t_unix": 1793615313.749, "button": 12, "button_name": "dpad_down", "kind": "move", "selected_joint": 3, "step_deg": 0.5, "execute": false, "joint": 3, "delta": 0.5, "accepted": true, "measured_before": [6.0, 1.0, 0.0, 4.0, 0.0, -30.0], "start_source": "command", "cmd_gap_deg": 0.0, "clip": [], "refused": [], "limit_ja": "", "limit_ascii": "", "command": [6.0, 1.0, 0.0, 4.5, 0.0, -30.0], "sent": true, "reached": true, "reached_angles": [6.0, 1.0, 0.0, 4.5, 0.0, -30.0], "reach_err": 0.0, "t_send": 1793615313.7499819, "dry": true, "grip_state": "open", "stopped": false, "stop_reason": null, "base_pose": [0.0, 0.0, 0.0, 0.0, 0.0, -30.0]}
{"seq": 68, "t": "2026-11-02T19:28:36.048", "t_unix": 1793615316.049, "button": 0, "button_name": "cross", "kind": "grip_close", "selected_joint": 3, "step_deg": 0.5, "execute": false, "grip_cmd": 1, "accepted": true, "measured_before": null, "sent": true, "reached": null, "grip_state": "closed", "stopped": false, "stop_reason": null, "base_pose": [0.0, 0.0, 0.0, 0.0, 0.0, -30.0]}
{"seq": 69, "t": "2026-11-02T19:28:38.453", "t_unix": 1793615318.453, "button": 0, "button_name": "cross", "kind": "grip_close", "selected_joint": 3, "step_deg": 0.5, "execute": false, "grip_cmd": 1, "accepted": true, "measured_before": null, "sent": true, "reached": null, "grip_state": "closed", "stopped": false, "stop_reason": null, "base_pose": [0.0, 0.0, 0.0, 0.0, 0.0, -30.0]}
{"seq": 70, "t": "2026-11-02T19:28:39.701", "t_unix": 1793615319.701, "button": 2, "button_name": "square", "kind": "ignored", "selected_joint": 3, "step_deg": 0.5, "execute": false, "accepted": false, "reason": "unassigned", "grip_state": "closed", "stopped": false, "stop_reason": null, "base_pose": [0.0, 0.0, 0.0, 0.0, 0.0, -30.0]}
{"seq": 71, "t": "2026-11-02T19:28:42.347", "t_unix": 1793615322.347, "button": 1, "button_name": "circle", "kind": "grip_open", "selected_joint": 3, "step_deg": 0.5, "execute": false, "grip_cmd": 0, "accepted": true, "measured_before": null, "sent": true, "reached": null, "grip_state": "open", "stopped": false, "stop_reason": null, "base_pose": [0.0, 0.0, 0.0, 0.0, 0.0, -30.0]}
{"seq": 72, "t": "2026-11-02T19:28:58.709", "t_unix": 1793615338.71, "button": 3, "button_name": "triangle", "kind": "select_joint", "selected_joint": 4, "step_deg": 0.5, "execute": false, "accepted": true, "grip_state": "open", "stopped": false, "stop_reason": null, "base_pose": [0.0, 0.0, 0.0, 0.0, 0.0, -30.0]}
{"seq": 73, "t": "2026-11-02T19:29:00.886", "t_unix": 1793615340.887, "button": 3, "button_name": "triangle", "kind": "select_joint", "selected_joint": 5, "step_deg": 0.5, "execute": false, "accepted": true, "grip_state": "open", "stopped": false, "stop_reason": null, "base_pose": [0.0, 0.0, 0.0, 0.0, 0.0, -30.0]}
{"seq": 74, "t": "2026-11-02T19:29:01.759", "t_unix": 1793615341.759, "button": 3, "button_name": "triangle", "kind": "select_joint", "selected_joint": 1, "step_deg": 0.5, "execute": false, "accepted": true, "grip_state": "open", "stopped": false, "stop_reason": null, "base_pose": [0.0, 0.0, 0.0, 0.0, 0.0, -30.0]}
{"seq": 75, "t": "2026-11-02T19:29:02.906", "t_unix": 1793615342.906, "button": 3, "button_name": "triangle", "kind": "select_joint", "selected_joint": 2, "step_deg": 0.5, "execute": false, "accepted": true, "grip_state": "open", "stopped": false, "stop_reason": null, "base_pose": [0.0, 0.0, 0.0, 0.0, 0.0, -30.0]}
{"seq": 76, "t": "2026-11-02T19:29:04.644", "t_unix": 1793615344.645, "button": 14, "button_name": "dpad_right", "kind": "move", "selected_joint": 2, "step_deg": 0.5, "execute": false, "joint": 0, "delta": 0.5, "accepted": true, "measured_before": [6.0, 1.0, 0.0, 4.5, 0.0, -30.0], "start_source": "command", "cmd_gap_deg": 0.0, "clip": [], "refused": [], "limit_ja": "", "limit_ascii": "", "command": [6.5, 1.0, 0.0, 4.5, 0.0, -30.0], "sent": true, "reached": true, "reached_angles": [6.5, 1.0, 0.0, 4.5, 0.0, -30.0], "reach_err": 0.0, "t_send": 1793615344.64776, "dry": true, "grip_state": "open", "stopped": false, "stop_reason": null, "base_pose": [0.0, 0.0, 0.0, 0.0, 0.0, -30.0]}
{"seq": 77, "t": "2026-11-02T19:29:05.594", "t_unix": 1793615345.595, "button": 14, "button_name": "dpad_right", "kind": "move", "selected_joint": 2, "step_deg": 0.5, "execute": false, "joint": 0, "delta": 0.5, "accepted": true, "measured_before": [6.5, 1.0, 0.0, 4.5, 0.0, -30.0], "start_source": "command", "cmd_gap_deg": 0.0, "clip": [], "refused": [], "limit_ja": "", "limit_ascii": "", "command": [7.0, 1.0, 0.0, 4.5, 0.0, -30.0], "sent": true, "reached": true, "reached_angles": [7.0, 1.0, 0.0, 4.5, 0.0, -30.0], "reach_err": 0.0, "t_send": 1793615345.597534, "dry": true, "grip_state": "open", "stopped": false, "stop_reason": null, "base_pose": [0.0, 0.0, 0.0, 0.0, 0.0, -30.0]}
{"seq": 78, "t": "2026-11-02T19:29:06.644", "t_unix": 1793615346.645, "button": 13, "button_name": "dpad_left", "kind": "move", "selected_joint": 2, "step_deg": 0.5, "execute": false, "joint": 0, "delta": -0.5, "accepted": true, "measured_before": [7.0, 1.0, 0.0, 4.5, 0.0, -30.0], "start_source": "command", "cmd_gap_deg": 0.0, "clip": [], "refused": [], "limit_ja": "", "limit_ascii": "", "command": [6.5, 1.0, 0.0, 4.5, 0.0, -30.0], "sent": true, "reached": true, "reached_angles": [6.5, 1.0, 0.0, 4.5, 0.0, -30.0], "reach_err": 0.0, "t_send": 1793615346.645689, "dry": true, "grip_state": "open", "stopped": false, "stop_reason": null, "base_pose": [0.0, 0.0, 0.0, 0.0, 0.0, -30.0]}
{"seq": 79, "t": "2026-11-02T19:29:08.022", "t_unix": 1793615348.023, "button": 12, "button_name": "dpad_down", "kind": "move", "selected_joint": 2, "step_deg": 0.5, "execute": false, "joint": 2, "delta": 0.5, "accepted": true, "measured_before": [6.5, 1.0, 0.0, 4.5, 0.0, -30.0], "start_source": "command", "cmd_gap_deg": 0.0, "clip": [], "refused": [], "limit_ja": "", "limit_ascii": "", "command": [6.5, 1.0, 0.5, 4.5, 0.0, -30.0], "sent": true, "reached": true, "reached_angles": [6.5, 1.0, 0.5, 4.5, 0.0, -30.0], "reach_err": 0.0, "t_send": 1793615348.023799, "dry": true, "grip_state": "open", "stopped": false, "stop_reason": null, "base_pose": [0.0, 0.0, 0.0, 0.0, 0.0, -30.0]}
{"seq": 80, "t": "2026-11-02T19:29:08.848", "t_unix": 1793615348.848, "button": 12, "button_name": "dpad_down", "kind": "move", "selected_joint": 2, "step_deg": 0.5, "execute": false, "joint": 2, "delta": 0.5, "accepted": true, "measured_before": [6.5, 1.0, 0.5, 4.5, 0.0, -30.0], "start_source": "command", "cmd_gap_deg": 0.0, "clip": [], "refused": [], "limit_ja": "", "limit_ascii": "", "command": [6.5, 1.0, 1.0, 4.5, 0.0, -30.0], "sent": true, "reached": true, "reached_angles": [6.5, 1.0, 1.0, 4.5, 0.0, -30.0], "reach_err": 0.0, "t_send": 1793615348.848504, "dry": true, "grip_state": "open", "stopped": false, "stop_reason": null, "base_pose": [0.0, 0.0, 0.0, 0.0, 0.0, -30.0]}
{"seq": 81, "t": "2026-11-02T19:29:09.538", "t_unix": 1793615349.539, "button": 12, "button_name": "dpad_down", "kind": "move", "selected_joint": 2, "step_deg": 0.5, "execute": false, "joint": 2, "delta": 0.5, "accepted": true, "measured_before": [6.5, 1.0, 1.0, 4.5, 0.0, -30.0], "start_source": "command", "cmd_gap_deg": 0.0, "clip": [], "refused": [], "limit_ja": "", "limit_ascii": "", "command": [6.5, 1.0, 1.5, 4.5, 0.0, -30.0], "sent": true, "reached": true, "reached_angles": [6.5, 1.0, 1.5, 4.5, 0.0, -30.0], "reach_err": 0.0, "t_send": 1793615349.539597, "dry": true, "grip_state": "open", "stopped": false, "stop_reason": null, "base_pose": [0.0, 0.0, 0.0, 0.0, 0.0, -30.0]}
{"seq": 82, "t": "2026-11-02T19:29:10.292", "t_unix": 1793615350.293, "button": 11, "button_name": "dpad_up", "kind": "move", "selected_joint": 2, "step_deg": 0.5, "execute": false, "joint": 2, "delta": -0.5, "accepted": true, "measured_before": [6.5, 1.0, 1.5, 4.5, 0.0, -30.0], "start_source": "command", "cmd_gap_deg": 0.0, "clip": [], "refused": [], "limit_ja": "", "limit_ascii": "", "command": [6.5, 1.0, 1.0, 4.5, 0.0, -30.0], "sent": true, "reached": true, "reached_angles": [6.5, 1.0, 1.0, 4.5, 0.0, -30.0], "reach_err": 0.0, "t_send": 1793615350.294013, "dry": true, "grip_state": "open", "stopped": false, "stop_reason": null, "base_pose": [0.0, 0.0, 0.0, 0.0, 0.0, -30.0]}
{"seq": 83, "t": "2026-11-02T19:29:11.053", "t_unix": 1793615351.053, "button": 11, "button_name": "dpad_up", "kind": "move", "selected_joint": 2, "step_deg": 0.5, "execute": false, "joint": 2, "delta": -0.5, "accepted": true, "measured_before": [6.5, 1.0, 1.0, 4.5, 0.0, -30.0], "start_source": "command", "cmd_gap_deg": 0.0, "clip": [], "refused": [], "limit_ja": "", "limit_ascii": "", "command": [6.5, 1.0, 0.5, 4.5, 0.0, -30.0], "sent": true, "reached": true, "reached_angles": [6.5, 1.0, 0.5, 4.5, 0.0, -30.0], "reach_err": 0.0, "t_send": 1793615351.0542111, "dry": true, "grip_state": "open", "stopped": false, "stop_reason": null, "base_pose": [0.0, 0.0, 0.0, 0.0, 0.0, -30.0]}
{"seq": 84, "t": "2026-11-02T19:29:11.801", "t_unix": 1793615351.802, "button": 3, "button_name": "triangle", "kind": "select_joint", "selected_joint": 3, "step_deg": 0.5, "execute": false, "accepted": true, "grip_state": "open", "stopped": false, "stop_reason": null, "base_pose": [0.0, 0.0, 0.0, 0.0, 0.0, -30.0]}
{"seq": 85, "t": "2026-11-02T19:29:12.657", "t_unix": 1793615352.657, "button": 11, "button_name": "dpad_up", "kind": "move", "selected_joint": 3, "step_deg": 0.5, "execute": false, "joint": 3, "delta": -0.5, "accepted": true, "measured_before": [6.5, 1.0, 0.5, 4.5, 0.0, -30.0], "start_source": "command", "cmd_gap_deg": 0.0, "clip": [], "refused": [], "limit_ja": "", "limit_ascii": "", "command": [6.5, 1.0, 0.5, 4.0, 0.0, -30.0], "sent": true, "reached": true, "reached_angles": [6.5, 1.0, 0.5, 4.0, 0.0, -30.0], "reach_err": 0.0, "t_send": 1793615352.6583, "dry": true, "grip_state": "open", "stopped": false, "stop_reason": null, "base_pose": [0.0, 0.0, 0.0, 0.0, 0.0, -30.0]}
{"seq": 86, "t": "2026-11-02T19:29:13.378", "t_unix": 1793615353.378, "button": 11, "button_name": "dpad_up", "kind": "move", "selected_joint": 3, "step_deg": 0.5, "execute": false, "joint": 3, "delta": -0.5, "accepted": true, "measured_before": [6.5, 1.0, 0.5, 4.0, 0.0, -30.0], "start_source": "command", "cmd_gap_deg": 0.0, "clip": [], "refused": [], "limit_ja": "", "limit_ascii": "", "command": [6.5, 1.0, 0.5, 3.5, 0.0, -30.0], "sent": true, "reached": true, "reached_angles": [6.5, 1.0, 0.5, 3.5, 0.0, -30.0], "reach_err": 0.0, "t_send": 1793615353.379213, "dry": true, "grip_state": "open", "stopped": false, "stop_reason": null, "base_pose": [0.0, 0.0, 0.0, 0.0, 0.0, -30.0]}
{"seq": 87, "t": "2026-11-02T19:29:14.145", "t_unix": 1793615354.145, "button": 11, "button_name": "dpad_up", "kind": "move", "selected_joint": 3, "step_deg": 0.5, "execute": false, "joint": 3, "delta": -0.5, "accepted": true, "measured_before": [6.5, 1.0, 0.5, 3.5, 0.0, -30.0], "start_source": "command", "cmd_gap_deg": 0.0, "clip": [], "refused": [], "limit_ja": "", "limit_ascii": "", "command": [6.5, 1.0, 0.5, 3.0, 0.0, -30.0], "sent": true, "reached": true, "reached_angles": [6.5, 1.0, 0.5, 3.0, 0.0, -30.0], "reach_err": 0.0, "t_send": 1793615354.145959, "dry": true, "grip_state": "open", "stopped": false, "stop_reason": null, "base_pose": [0.0, 0.0, 0.0, 0.0, 0.0, -30.0]}
{"seq": 88, "t": "2026-11-02T19:29:14.904", "t_unix": 1793615354.905, "button": 11, "button_name": "dpad_up", "kind": "move", "selected_joint": 3, "step_deg": 0.5, "execute": false, "joint": 3, "delta": -0.5, "accepted": true, "measured_before": [6.5, 1.0, 0.5, 3.0, 0.0, -30.0], "start_source": "command", "cmd_gap_deg": 0.0, "clip": [], "refused": [], "limit_ja": "", "limit_ascii": "", "command": [6.5, 1.0, 0.5, 2.5, 0.0, -30.0], "sent": true, "reached": true, "reached_angles": [6.5, 1.0, 0.5, 2.5, 0.0, -30.0], "reach_err": 0.0, "t_send": 1793615354.9053261, "dry": true, "grip_state": "open", "stopped": false, "stop_reason": null, "base_pose": [0.0, 0.0, 0.0, 0.0, 0.0, -30.0]}
{"seq": 90, "t": "2026-11-02T19:29:15.693", "t_unix": 1793615355.694, "button": 11, "button_name": "dpad_up", "kind": "move", "selected_joint": 3, "step_deg": 0.5, "execute": false, "joint": 3, "delta": -0.5, "accepted": false, "reason": "busy", "grip_state": "open", "stopped": false, "stop_reason": null, "base_pose": [0.0, 0.0, 0.0, 0.0, 0.0, -30.0]}
{"seq": 89, "t": "2026-11-02T19:29:15.493", "t_unix": 1793615355.494, "button": 11, "button_name": "dpad_up", "kind": "move", "selected_joint": 3, "step_deg": 0.5, "execute": false, "joint": 3, "delta": -0.5, "accepted": true, "measured_before": [6.5, 1.0, 0.5, 2.5, 0.0, -30.0], "start_source": "command", "cmd_gap_deg": 0.0, "clip": [], "refused": [], "limit_ja": "", "limit_ascii": "", "command": [6.5, 1.0, 0.5, 2.0, 0.0, -30.0], "sent": true, "reached": true, "reached_angles": [6.5, 1.0, 0.5, 2.0, 0.0, -30.0], "reach_err": 0.0, "t_send": 1793615355.495326, "dry": true, "grip_state": "open", "stopped": false, "stop_reason": null, "base_pose": [0.0, 0.0, 0.0, 0.0, 0.0, -30.0]}
{"seq": 92, "t": "2026-11-02T19:29:16.251", "t_unix": 1793615356.251, "button": 11, "button_name": "dpad_up", "kind": "move", "selected_joint": 3, "step_deg": 0.5, "execute": false, "joint": 3, "delta": -0.5, "accepted": false, "reason": "busy", "grip_state": "open", "stopped": false, "stop_reason": null, "base_pose": [0.0, 0.0, 0.0, 0.0, 0.0, -30.0]}
{"seq": 91, "t": "2026-11-02T19:29:15.944", "t_unix": 1793615355.944, "button": 11, "button_name": "dpad_up", "kind": "move", "selected_joint": 3, "step_deg": 0.5, "execute": false, "joint": 3, "delta": -0.5, "accepted": true, "measured_before": [6.5, 1.0, 0.5, 2.0, 0.0, -30.0], "start_source": "command", "cmd_gap_deg": 0.0, "clip": [], "refused": [], "limit_ja": "", "limit_ascii": "", "command": [6.5, 1.0, 0.5, 1.5, 0.0, -30.0], "sent": true, "reached": true, "reached_angles": [6.5, 1.0, 0.5, 1.5, 0.0, -30.0], "reach_err": 0.0, "t_send": 1793615355.944788, "dry": true, "grip_state": "open", "stopped": false, "stop_reason": null, "base_pose": [0.0, 0.0, 0.0, 0.0, 0.0, -30.0]}
{"seq": 93, "t": "2026-11-02T19:29:16.632", "t_unix": 1793615356.633, "button": 11, "button_name": "dpad_up", "kind": "move", "selected_joint": 3, "step_deg": 0.5, "execute": false, "joint": 3, "delta": -0.5, "accepted": true, "measured_before": [6.5, 1.0, 0.5, 1.5, 0.0, -30.0], "start_source": "command", "cmd_gap_deg": 0.0, "clip": [], "refused": [], "limit_ja": "", "limit_ascii": "", "command": [6.5, 1.0, 0.5, 1.0, 0.0, -30.0], "sent": true, "reached": true, "reached_angles": [6.5, 1.0, 0.5, 1.0, 0.0, -30.0], "reach_err": 0.0, "t_send": 1793615356.634006, "dry": true, "grip_state": "open", "stopped": false, "stop_reason": null, "base_pose": [0.0, 0.0, 0.0, 0.0, 0.0, -30.0]}
{"seq": 94, "t": "2026-11-02T19:29:17.051", "t_unix": 1793615357.051, "button": 11, "button_name": "dpad_up", "kind": "move", "selected_joint": 3, "step_deg": 0.5, "execute": false, "joint": 3, "delta": -0.5, "accepted": true, "measured_before": [6.5, 1.0, 0.5, 1.0, 0.0, -30.0], "start_source": "command", "cmd_gap_deg": 0.0, "clip": [], "refused": [], "limit_ja": "", "limit_ascii": "", "command": [6.5, 1.0, 0.5, 0.5, 0.0, -30.0], "sent": true, "reached": true, "reached_angles": [6.5, 1.0, 0.5, 0.5, 0.0, -30.0], "reach_err": 0.0, "t_send": 1793615357.0524302, "dry": true, "grip_state": "open", "stopped": false, "stop_reason": null, "base_pose": [0.0, 0.0, 0.0, 0.0, 0.0, -30.0]}
{"seq": 96, "t": "2026-11-02T19:29:17.796", "t_unix": 1793615357.797, "button": 3, "button_name": "triangle", "kind": "select_joint", "selected_joint": 4, "step_deg": 0.5, "execute": false, "accepted": true, "grip_state": "open", "stopped": false, "stop_reason": null, "base_pose": [0.0, 0.0, 0.0, 0.0, 0.0, -30.0]}
{"seq": 95, "t": "2026-11-02T19:29:17.511", "t_unix": 1793615357.511, "button": 11, "button_name": "dpad_up", "kind": "move", "selected_joint": 3, "step_deg": 0.5, "execute": false, "joint": 3, "delta": -0.5, "accepted": true, "measured_before": [6.5, 1.0, 0.5, 0.5, 0.0, -30.0], "start_source": "command", "cmd_gap_deg": 0.0, "clip": [], "refused": [], "limit_ja": "", "limit_ascii": "", "command": [6.5, 1.0, 0.5, 0.0, 0.0, -30.0], "sent": true, "reached": true, "reached_angles": [6.5, 1.0, 0.5, 0.0, 0.0, -30.0], "reach_err": 0.0, "t_send": 1793615357.512304, "dry": true, "grip_state": "open", "stopped": false, "stop_reason": null, "base_pose": [0.0, 0.0, 0.0, 0.0, 0.0, -30.0]}
{"seq": 97, "t": "2026-11-02T19:29:18.530", "t_unix": 1793615358.53, "button": 11, "button_name": "dpad_up", "kind": "move", "selected_joint": 4, "step_deg": 0.5, "execute": false, "joint": 4, "delta": -0.5, "accepted": true, "measured_before": [6.5, 1.0, 0.5, 0.0, 0.0, -30.0], "start_source": "command", "cmd_gap_deg": 0.0, "clip": [], "refused": [], "limit_ja": "", "limit_ascii": "", "command": [6.5, 1.0, 0.5, 0.0, -0.5, -30.0], "sent": true, "reached": true, "reached_angles": [6.5, 1.0, 0.5, 0.0, -0.5, -30.0], "reach_err": 0.0, "t_send": 1793615358.5311701, "dry": true, "grip_state": "open", "stopped": false, "stop_reason": null, "base_pose": [0.0, 0.0, 0.0, 0.0, 0.0, -30.0]}
{"seq": 98, "t": "2026-11-02T19:29:19.003", "t_unix": 1793615359.003, "button": 11, "button_name": "dpad_up", "kind": "move", "selected_joint": 4, "step_deg": 0.5, "execute": false, "joint": 4, "delta": -0.5, "accepted": true, "measured_before": [6.5, 1.0, 0.5, 0.0, -0.5, -30.0], "start_source": "command", "cmd_gap_deg": 0.0, "clip": [], "refused": [], "limit_ja": "", "limit_ascii": "", "command": [6.5, 1.0, 0.5, 0.0, -1.0, -30.0], "sent": true, "reached": true, "reached_angles": [6.5, 1.0, 0.5, 0.0, -1.0, -30.0], "reach_err": 0.0, "t_send": 1793615359.008142, "dry": true, "grip_state": "open", "stopped": false, "stop_reason": null, "base_pose": [0.0, 0.0, 0.0, 0.0, 0.0, -30.0]}
{"seq": 99, "t": "2026-11-02T19:29:19.508", "t_unix": 1793615359.509, "button": 11, "button_name": "dpad_up", "kind": "move", "selected_joint": 4, "step_deg": 0.5, "execute": false, "joint": 4, "delta": -0.5, "accepted": true, "measured_before": [6.5, 1.0, 0.5, 0.0, -1.0, -30.0], "start_source": "command", "cmd_gap_deg": 0.0, "clip": [], "refused": [], "limit_ja": "", "limit_ascii": "", "command": [6.5, 1.0, 0.5, 0.0, -1.5, -30.0], "sent": true, "reached": true, "reached_angles": [6.5, 1.0, 0.5, 0.0, -1.5, -30.0], "reach_err": 0.0, "t_send": 1793615359.5098188, "dry": true, "grip_state": "open", "stopped": false, "stop_reason": null, "base_pose": [0.0, 0.0, 0.0, 0.0, 0.0, -30.0]}
{"seq": 100, "t": "2026-11-02T19:29:19.905", "t_unix": 1793615359.905, "button": 11, "button_name": "dpad_up", "kind": "move", "selected_joint": 4, "step_deg": 0.5, "execute": false, "joint": 4, "delta": -0.5, "accepted": true, "measured_before": [6.5, 1.0, 0.5, 0.0, -1.5, -30.0], "start_source": "command", "cmd_gap_deg": 0.0, "clip": [], "refused": [], "limit_ja": "", "limit_ascii": "", "command": [6.5, 1.0, 0.5, 0.0, -2.0, -30.0], "sent": true, "reached": true, "reached_angles": [6.5, 1.0, 0.5, 0.0, -2.0, -30.0], "reach_err": 0.0, "t_send": 1793615359.906163, "dry": true, "grip_state": "open", "stopped": false, "stop_reason": null, "base_pose": [0.0, 0.0, 0.0, 0.0, 0.0, -30.0]}
{"seq": 101, "t": "2026-11-02T19:29:20.293", "t_unix": 1793615360.293, "button": 11, "button_name": "dpad_up", "kind": "move", "selected_joint": 4, "step_deg": 0.5, "execute": false, "joint": 4, "delta": -0.5, "accepted": true, "measured_before": [6.5, 1.0, 0.5, 0.0, -2.0, -30.0], "start_source": "command", "cmd_gap_deg": 0.0, "clip": [], "refused": [], "limit_ja": "", "limit_ascii": "", "command": [6.5, 1.0, 0.5, 0.0, -2.5, -30.0], "sent": true, "reached": true, "reached_angles": [6.5, 1.0, 0.5, 0.0, -2.5, -30.0], "reach_err": 0.0, "t_send": 1793615360.293468, "dry": true, "grip_state": "open", "stopped": false, "stop_reason": null, "base_pose": [0.0, 0.0, 0.0, 0.0, 0.0, -30.0]}
{"seq": 103, "t": "2026-11-02T19:29:20.897", "t_unix": 1793615360.898, "button": 11, "button_name": "dpad_up", "kind": "move", "selected_joint": 4, "step_deg": 0.5, "execute": false, "joint": 4, "delta": -0.5, "accepted": false, "reason": "busy", "grip_state": "open", "stopped": false, "stop_reason": null, "base_pose": [0.0, 0.0, 0.0, 0.0, 0.0, -30.0]}
{"seq": 102, "t": "2026-11-02T19:29:20.628", "t_unix": 1793615360.628, "button": 11, "button_name": "dpad_up", "kind": "move", "selected_joint": 4, "step_deg": 0.5, "execute": false, "joint": 4, "delta": -0.5, "accepted": true, "measured_before": [6.5, 1.0, 0.5, 0.0, -2.5, -30.0], "start_source": "command", "cmd_gap_deg": 0.0, "clip": [], "refused": [], "limit_ja": "", "limit_ascii": "", "command": [6.5, 1.0, 0.5, 0.0, -3.0, -30.0], "sent": true, "reached": true, "reached_angles": [6.5, 1.0, 0.5, 0.0, -3.0, -30.0], "reach_err": 0.0, "t_send": 1793615360.62906, "dry": true, "grip_state": "open", "stopped": false, "stop_reason": null, "base_pose": [0.0, 0.0, 0.0, 0.0, 0.0, -30.0]}
{"seq": 105, "t": "2026-11-02T19:29:21.419", "t_unix": 1793615361.419, "button": 11, "button_name": "dpad_up", "kind": "move", "selected_joint": 4, "step_deg": 0.5, "execute": false, "joint": 4, "delta": -0.5, "accepted": false, "reason": "busy", "grip_state": "open", "stopped": false, "stop_reason": null, "base_pose": [0.0, 0.0, 0.0, 0.0, 0.0, -30.0]}
{"seq": 104, "t": "2026-11-02T19:29:21.132", "t_unix": 1793615361.132, "button": 11, "button_name": "dpad_up", "kind": "move", "selected_joint": 4, "step_deg": 0.5, "execute": false, "joint": 4, "delta": -0.5, "accepted": true, "measured_before": [6.5, 1.0, 0.5, 0.0, -3.0, -30.0], "start_source": "command", "cmd_gap_deg": 0.0, "clip": [], "refused": [], "limit_ja": "", "limit_ascii": "", "command": [6.5, 1.0, 0.5, 0.0, -3.5, -30.0], "sent": true, "reached": true, "reached_angles": [6.5, 1.0, 0.5, 0.0, -3.5, -30.0], "reach_err": 0.0, "t_send": 1793615361.1327739, "dry": true, "grip_state": "open", "stopped": false, "stop_reason": null, "base_pose": [0.0, 0.0, 0.0, 0.0, 0.0, -30.0]}
{"seq": 106, "t": "2026-11-02T19:29:21.653", "t_unix": 1793615361.653, "button": 11, "button_name": "dpad_up", "kind": "move", "selected_joint": 4, "step_deg": 0.5, "execute": false, "joint": 4, "delta": -0.5, "accepted": true, "measured_before": [6.5, 1.0, 0.5, 0.0, -3.5, -30.0], "start_source": "command", "cmd_gap_deg": 0.0, "clip": [], "refused": [], "limit_ja": "", "limit_ascii": "", "command": [6.5, 1.0, 0.5, 0.0, -4.0, -30.0], "sent": true, "reached": true, "reached_angles": [6.5, 1.0, 0.5, 0.0, -4.0, -30.0], "reach_err": 0.0, "t_send": 1793615361.654449, "dry": true, "grip_state": "open", "stopped": false, "stop_reason": null, "base_pose": [0.0, 0.0, 0.0, 0.0, 0.0, -30.0]}
{"seq": 107, "t": "2026-11-02T19:29:22.177", "t_unix": 1793615362.178, "button": 3, "button_name": "triangle", "kind": "select_joint", "selected_joint": 5, "step_deg": 0.5, "execute": false, "accepted": true, "grip_state": "open", "stopped": false, "stop_reason": null, "base_pose": [0.0, 0.0, 0.0, 0.0, 0.0, -30.0]}
{"seq": 108, "t": "2026-11-02T19:29:23.049", "t_unix": 1793615363.05, "button": 11, "button_name": "dpad_up", "kind": "move", "selected_joint": 5, "step_deg": 0.5, "execute": false, "joint": 5, "delta": -0.5, "accepted": true, "measured_before": [6.5, 1.0, 0.5, 0.0, -4.0, -30.0], "start_source": "command", "cmd_gap_deg": 0.0, "clip": [], "refused": [], "limit_ja": "", "limit_ascii": "", "command": [6.5, 1.0, 0.5, 0.0, -4.0, -30.5], "sent": true, "reached": true, "reached_angles": [6.5, 1.0, 0.5, 0.0, -4.0, -30.5], "reach_err": 0.0, "t_send": 1793615363.0512562, "dry": true, "grip_state": "open", "stopped": false, "stop_reason": null, "base_pose": [0.0, 0.0, 0.0, 0.0, 0.0, -30.0]}
{"seq": 110, "t": "2026-11-02T19:29:23.996", "t_unix": 1793615363.996, "button": 11, "button_name": "dpad_up", "kind": "move", "selected_joint": 5, "step_deg": 0.5, "execute": false, "joint": 5, "delta": -0.5, "accepted": false, "reason": "busy", "grip_state": "open", "stopped": false, "stop_reason": null, "base_pose": [0.0, 0.0, 0.0, 0.0, 0.0, -30.0]}
{"seq": 109, "t": "2026-11-02T19:29:23.677", "t_unix": 1793615363.678, "button": 11, "button_name": "dpad_up", "kind": "move", "selected_joint": 5, "step_deg": 0.5, "execute": false, "joint": 5, "delta": -0.5, "accepted": true, "measured_before": [6.5, 1.0, 0.5, 0.0, -4.0, -30.5], "start_source": "command", "cmd_gap_deg": 0.0, "clip": [], "refused": [], "limit_ja": "", "limit_ascii": "", "command": [6.5, 1.0, 0.5, 0.0, -4.0, -31.0], "sent": true, "reached": true, "reached_angles": [6.5, 1.0, 0.5, 0.0, -4.0, -31.0], "reach_err": 0.0, "t_send": 1793615363.678486, "dry": true, "grip_state": "open", "stopped": false, "stop_reason": null, "base_pose": [0.0, 0.0, 0.0, 0.0, 0.0, -30.0]}
{"seq": 112, "t": "2026-11-02T19:29:24.501", "t_unix": 1793615364.502, "button": 11, "button_name": "dpad_up", "kind": "move", "selected_joint": 5, "step_deg": 0.5, "execute": false, "joint": 5, "delta": -0.5, "accepted": false, "reason": "busy", "grip_state": "open", "stopped": false, "stop_reason": null, "base_pose": [0.0, 0.0, 0.0, 0.0, 0.0, -30.0]}
{"seq": 111, "t": "2026-11-02T19:29:24.247", "t_unix": 1793615364.248, "button": 11, "button_name": "dpad_up", "kind": "move", "selected_joint": 5, "step_deg": 0.5, "execute": false, "joint": 5, "delta": -0.5, "accepted": true, "measured_before": [6.5, 1.0, 0.5, 0.0, -4.0, -31.0], "start_source": "command", "cmd_gap_deg": 0.0, "clip": [], "refused": [], "limit_ja": "", "limit_ascii": "", "command": [6.5, 1.0, 0.5, 0.0, -4.0, -31.5], "sent": true, "reached": true, "reached_angles": [6.5, 1.0, 0.5, 0.0, -4.0, -31.5], "reach_err": 0.0, "t_send": 1793615364.248802, "dry": true, "grip_state": "open", "stopped": false, "stop_reason": null, "base_pose": [0.0, 0.0, 0.0, 0.0, 0.0, -30.0]}
{"seq": 114, "t": "2026-11-02T19:29:24.952", "t_unix": 1793615364.953, "button": 11, "button_name": "dpad_up", "kind": "move", "selected_joint": 5, "step_deg": 0.5, "execute": false, "joint": 5, "delta": -0.5, "accepted": false, "reason": "busy", "grip_state": "open", "stopped": false, "stop_reason": null, "base_pose": [0.0, 0.0, 0.0, 0.0, 0.0, -30.0]}
{"seq": 113, "t": "2026-11-02T19:29:24.720", "t_unix": 1793615364.721, "button": 11, "button_name": "dpad_up", "kind": "move", "selected_joint": 5, "step_deg": 0.5, "execute": false, "joint": 5, "delta": -0.5, "accepted": true, "measured_before": [6.5, 1.0, 0.5, 0.0, -4.0, -31.5], "start_source": "command", "cmd_gap_deg": 0.0, "clip": [], "refused": [], "limit_ja": "", "limit_ascii": "", "command": [6.5, 1.0, 0.5, 0.0, -4.0, -32.0], "sent": true, "reached": true, "reached_angles": [6.5, 1.0, 0.5, 0.0, -4.0, -32.0], "reach_err": 0.0, "t_send": 1793615364.7220821, "dry": true, "grip_state": "open", "stopped": false, "stop_reason": null, "base_pose": [0.0, 0.0, 0.0, 0.0, 0.0, -30.0]}
{"seq": 116, "t": "2026-11-02T19:29:25.407", "t_unix": 1793615365.407, "button": 11, "button_name": "dpad_up", "kind": "move", "selected_joint": 5, "step_deg": 0.5, "execute": false, "joint": 5, "delta": -0.5, "accepted": false, "reason": "busy", "grip_state": "open", "stopped": false, "stop_reason": null, "base_pose": [0.0, 0.0, 0.0, 0.0, 0.0, -30.0]}
{"seq": 115, "t": "2026-11-02T19:29:25.137", "t_unix": 1793615365.137, "button": 11, "button_name": "dpad_up", "kind": "move", "selected_joint": 5, "step_deg": 0.5, "execute": false, "joint": 5, "delta": -0.5, "accepted": true, "measured_before": [6.5, 1.0, 0.5, 0.0, -4.0, -32.0], "start_source": "command", "cmd_gap_deg": 0.0, "clip": [], "refused": [], "limit_ja": "", "limit_ascii": "", "command": [6.5, 1.0, 0.5, 0.0, -4.0, -32.5], "sent": true, "reached": true, "reached_angles": [6.5, 1.0, 0.5, 0.0, -4.0, -32.5], "reach_err": 0.0, "t_send": 1793615365.137384, "dry": true, "grip_state": "open", "stopped": false, "stop_reason": null, "base_pose": [0.0, 0.0, 0.0, 0.0, 0.0, -30.0]}
{"seq": 118, "t": "2026-11-02T19:29:25.927", "t_unix": 1793615365.927, "button": 11, "button_name": "dpad_up", "kind": "move", "selected_joint": 5, "step_deg": 0.5, "execute": false, "joint": 5, "delta": -0.5, "accepted": false, "reason": "busy", "grip_state": "open", "stopped": false, "stop_reason": null, "base_pose": [0.0, 0.0, 0.0, 0.0, 0.0, -30.0]}
{"seq": 117, "t": "2026-11-02T19:29:25.644", "t_unix": 1793615365.644, "button": 11, "button_name": "dpad_up", "kind": "move", "selected_joint": 5, "step_deg": 0.5, "execute": false, "joint": 5, "delta": -0.5, "accepted": true, "measured_before": [6.5, 1.0, 0.5, 0.0, -4.0, -32.5], "start_source": "command", "cmd_gap_deg": 0.0, "clip": [], "refused": [], "limit_ja": "", "limit_ascii": "", "command": [6.5, 1.0, 0.5, 0.0, -4.0, -33.0], "sent": true, "reached": true, "reached_angles": [6.5, 1.0, 0.5, 0.0, -4.0, -33.0], "reach_err": 0.0, "t_send": 1793615365.645656, "dry": true, "grip_state": "open", "stopped": false, "stop_reason": null, "base_pose": [0.0, 0.0, 0.0, 0.0, 0.0, -30.0]}
{"seq": 120, "t": "2026-11-02T19:29:26.344", "t_unix": 1793615366.344, "button": 11, "button_name": "dpad_up", "kind": "move", "selected_joint": 5, "step_deg": 0.5, "execute": false, "joint": 5, "delta": -0.5, "accepted": false, "reason": "busy", "grip_state": "open", "stopped": false, "stop_reason": null, "base_pose": [0.0, 0.0, 0.0, 0.0, 0.0, -30.0]}
{"seq": 119, "t": "2026-11-02T19:29:26.158", "t_unix": 1793615366.158, "button": 11, "button_name": "dpad_up", "kind": "move", "selected_joint": 5, "step_deg": 0.5, "execute": false, "joint": 5, "delta": -0.5, "accepted": true, "measured_before": [6.5, 1.0, 0.5, 0.0, -4.0, -33.0], "start_source": "command", "cmd_gap_deg": 0.0, "clip": [], "refused": [], "limit_ja": "", "limit_ascii": "", "command": [6.5, 1.0, 0.5, 0.0, -4.0, -33.5], "sent": true, "reached": true, "reached_angles": [6.5, 1.0, 0.5, 0.0, -4.0, -33.5], "reach_err": 0.0, "t_send": 1793615366.159463, "dry": true, "grip_state": "open", "stopped": false, "stop_reason": null, "base_pose": [0.0, 0.0, 0.0, 0.0, 0.0, -30.0]}
{"seq": 121, "t": "2026-11-02T19:29:26.596", "t_unix": 1793615366.597, "button": 11, "button_name": "dpad_up", "kind": "move", "selected_joint": 5, "step_deg": 0.5, "execute": false, "joint": 5, "delta": -0.5, "accepted": true, "measured_before": [6.5, 1.0, 0.5, 0.0, -4.0, -33.5], "start_source": "command", "cmd_gap_deg": 0.0, "clip": [], "refused": [], "limit_ja": "", "limit_ascii": "", "command": [6.5, 1.0, 0.5, 0.0, -4.0, -34.0], "sent": true, "reached": true, "reached_angles": [6.5, 1.0, 0.5, 0.0, -4.0, -34.0], "reach_err": 0.0, "t_send": 1793615366.5980968, "dry": true, "grip_state": "open", "stopped": false, "stop_reason": null, "base_pose": [0.0, 0.0, 0.0, 0.0, 0.0, -30.0]}
{"seq": 122, "t": "2026-11-02T19:29:27.397", "t_unix": 1793615367.398, "button": 1, "button_name": "circle", "kind": "grip_open", "selected_joint": 5, "step_deg": 0.5, "execute": false, "grip_cmd": 0, "accepted": true, "measured_before": null, "sent": true, "reached": null, "grip_state": "open", "stopped": false, "stop_reason": null, "base_pose": [0.0, 0.0, 0.0, 0.0, 0.0, -30.0]}
{"seq": 123, "t": "2026-11-02T19:29:29.238", "t_unix": 1793615369.238, "button": 11, "button_name": "dpad_up", "kind": "move", "selected_joint": 5, "step_deg": 0.5, "execute": false, "joint": 5, "delta": -0.5, "accepted": true, "measured_before": [6.5, 1.0, 0.5, 0.0, -4.0, -34.0], "start_source": "command", "cmd_gap_deg": 0.0, "clip": [], "refused": [], "limit_ja": "", "limit_ascii": "", "command": [6.5, 1.0, 0.5, 0.0, -4.0, -34.5], "sent": true, "reached": true, "reached_angles": [6.5, 1.0, 0.5, 0.0, -4.0, -34.5], "reach_err": 0.0, "t_send": 1793615369.239279, "dry": true, "grip_state": "open", "stopped": false, "stop_reason": null, "base_pose": [0.0, 0.0, 0.0, 0.0, 0.0, -30.0]}
{"seq": 124, "t": "2026-11-02T19:29:29.695", "t_unix": 1793615369.696, "button": 11, "button_name": "dpad_up", "kind": "move", "selected_joint": 5, "step_deg": 0.5, "execute": false, "joint": 5, "delta": -0.5, "accepted": true, "measured_before": [6.5, 1.0, 0.5, 0.0, -4.0, -34.5], "start_source": "command", "cmd_gap_deg": 0.0, "clip": [], "refused": [], "limit_ja": "", "limit_ascii": "", "command": [6.5, 1.0, 0.5, 0.0, -4.0, -35.0], "sent": true, "reached": true, "reached_angles": [6.5, 1.0, 0.5, 0.0, -4.0, -35.0], "reach_err": 0.0, "t_send": 1793615369.69733, "dry": true, "grip_state": "open", "stopped": false, "stop_reason": null, "base_pose": [0.0, 0.0, 0.0, 0.0, 0.0, -30.0]}
{"seq": 125, "t": "2026-11-02T19:29:30.740", "t_unix": 1793615370.741, "button": 10, "button_name": "r1", "kind": "modifier", "selected_joint": 5, "step_deg": 2.0, "execute": false, "accepted": true, "grip_state": "open", "stopped": false, "stop_reason": null, "base_pose": [0.0, 0.0, 0.0, 0.0, 0.0, -30.0]}
{"seq": 126, "t": "2026-11-02T19:29:31.158", "t_unix": 1793615371.159, "button": 11, "button_name": "dpad_up", "kind": "move", "selected_joint": 5, "step_deg": 2.0, "execute": false, "joint": 5, "delta": -2.0, "accepted": true, "measured_before": [6.5, 1.0, 0.5, 0.0, -4.0, -35.0], "start_source": "command", "cmd_gap_deg": 0.0, "clip": [], "refused": [], "limit_ja": "", "limit_ascii": "", "command": [6.5, 1.0, 0.5, 0.0, -4.0, -37.0], "sent": true, "reached": true, "reached_angles": [6.5, 1.0, 0.5, 0.0, -4.0, -37.0], "reach_err": 0.0, "t_send": 1793615371.159596, "dry": true, "grip_state": "open", "stopped": false, "stop_reason": null, "base_pose": [0.0, 0.0, 0.0, 0.0, 0.0, -30.0]}
{"seq": 127, "t": "2026-11-02T19:29:31.895", "t_unix": 1793615371.895, "button": 11, "button_name": "dpad_up", "kind": "move", "selected_joint": 5, "step_deg": 2.0, "execute": false, "joint": 5, "delta": -2.0, "accepted": true, "measured_before": [6.5, 1.0, 0.5, 0.0, -4.0, -37.0], "start_source": "command", "cmd_gap_deg": 0.0, "clip": [], "refused": [], "limit_ja": "", "limit_ascii": "", "command": [6.5, 1.0, 0.5, 0.0, -4.0, -39.0], "sent": true, "reached": true, "reached_angles": [6.5, 1.0, 0.5, 0.0, -4.0, -39.0], "reach_err": 0.0, "t_send": 1793615371.896183, "dry": true, "grip_state": "open", "stopped": false, "stop_reason": null, "base_pose": [0.0, 0.0, 0.0, 0.0, 0.0, -30.0]}
{"seq": 128, "t": "2026-11-02T19:29:32.599", "t_unix": 1793615372.6, "button": 11, "button_name": "dpad_up", "kind": "move", "selected_joint": 5, "step_deg": 2.0, "execute": false, "joint": 5, "delta": -2.0, "accepted": true, "measured_before": [6.5, 1.0, 0.5, 0.0, -4.0, -39.0], "start_source": "command", "cmd_gap_deg": 0.0, "clip": [{"joint": 5, "want": -41.0, "cmd": -40.0, "limit": "BASE", "bound": -40.0}], "refused": [], "limit_ja": "j5 を -41.00° → -40.00° に切った（基準の姿勢からの範囲 -40.00°）", "limit_ascii": "j5 clipped -41.00->-40.00 (BASE -40.00)", "command": [6.5, 1.0, 0.5, 0.0, -4.0, -40.0], "sent": true, "reached": true, "reached_angles": [6.5, 1.0, 0.5, 0.0, -4.0, -40.0], "reach_err": 0.0, "t_send": 1793615372.6020322, "dry": true, "grip_state": "open", "stopped": false, "stop_reason": null, "base_pose": [0.0, 0.0, 0.0, 0.0, 0.0, -30.0]}
{"seq": 129, "t": "2026-11-02T19:29:33.540", "t_unix": 1793615373.54, "button": 11, "button_name": "dpad_up", "kind": "move", "selected_joint": 5, "step_deg": 2.0, "execute": false, "joint": 5, "delta": -2.0, "accepted": true, "measured_before": [6.5, 1.0, 0.5, 0.0, -4.0, -40.0], "start_source": "command", "cmd_gap_deg": 0.0, "clip": [{"joint": 5, "want": -42.0, "cmd": -40.0, "limit": "BASE", "bound": -40.0}], "refused": [{"joint": 5, "why": "at_limit", "value": -42.0, "lo": -40.0, "hi": -20.0, "limit": "BASE"}], "limit_ja": "j5 を -42.00° → -40.00° に切った（基準の姿勢からの範囲 -40.00°） / j5 -42.00° は送らない：範囲の端にいて、押した向きに進めない（範囲 [-40.00, -20.00]・基準の姿勢からの範囲）", "limit_ascii": "j5 clipped -42.00->-40.00 (BASE -40.00) / j5 refused at_limit -42.00 [-40.00, -20.00] (BASE)", "sent": false, "reached": null, "grip_state": "open", "stopped": false, "stop_reason": null, "base_pose": [0.0, 0.0, 0.0, 0.0, 0.0, -30.0]}
{"seq": 130, "t": "2026-11-02T19:29:34.450", "t_unix": 1793615374.451, "button": 12, "button_name": "dpad_down", "kind": "move", "selected_joint": 5, "step_deg": 2.0, "execute": false, "joint": 5, "delta": 2.0, "accepted": true, "measured_before": [6.5, 1.0, 0.5, 0.0, -4.0, -40.0], "start_source": "command", "cmd_gap_deg": 0.0, "clip": [], "refused": [], "limit_ja": "", "limit_ascii": "", "command": [6.5, 1.0, 0.5, 0.0, -4.0, -38.0], "sent": true, "reached": true, "reached_angles": [6.5, 1.0, 0.5, 0.0, -4.0, -38.0], "reach_err": 0.0, "t_send": 1793615374.452723, "dry": true, "grip_state": "open", "stopped": false, "stop_reason": null, "base_pose": [0.0, 0.0, 0.0, 0.0, 0.0, -30.0]}
{"seq": 131, "t": "2026-11-02T19:29:35.090", "t_unix": 1793615375.09, "button": 12, "button_name": "dpad_down", "kind": "move", "selected_joint": 5, "step_deg": 2.0, "execute": false, "joint": 5, "delta": 2.0, "accepted": true, "measured_before": [6.5, 1.0, 0.5, 0.0, -4.0, -38.0], "start_source": "command", "cmd_gap_deg": 0.0, "clip": [], "refused": [], "limit_ja": "", "limit_ascii": "", "command": [6.5, 1.0, 0.5, 0.0, -4.0, -36.0], "sent": true, "reached": true, "reached_angles": [6.5, 1.0, 0.5, 0.0, -4.0, -36.0], "reach_err": 0.0, "t_send": 1793615375.091098, "dry": true, "grip_state": "open", "stopped": false, "stop_reason": null, "base_pose": [0.0, 0.0, 0.0, 0.0, 0.0, -30.0]}
{"seq": 132, "t": "2026-11-02T19:29:35.545", "t_unix": 1793615375.546, "button": 12, "button_name": "dpad_down", "kind": "move", "selected_joint": 5, "step_deg": 2.0, "execute": false, "joint": 5, "delta": 2.0, "accepted": true, "measured_before": [6.5, 1.0, 0.5, 0.0, -4.0, -36.0], "start_source": "command", "cmd_gap_deg": 0.0, "clip": [], "refused": [], "limit_ja": "", "limit_ascii": "", "command": [6.5, 1.0, 0.5, 0.0, -4.0, -34.0], "sent": true, "reached": true, "reached_angles": [6.5, 1.0, 0.5, 0.0, -4.0, -34.0], "reach_err": 0.0, "t_send": 1793615375.546286, "dry": true, "grip_state": "open", "stopped": false, "stop_reason": null, "base_pose": [0.0, 0.0, 0.0, 0.0, 0.0, -30.0]}
{"seq": 133, "t": "2026-11-02T19:29:35.993", "t_unix": 1793615375.993, "button": 12, "button_name": "dpad_down", "kind": "move", "selected_joint": 5, "step_deg": 2.0, "execute": false, "joint": 5, "delta": 2.0, "accepted": true, "measured_before": [6.5, 1.0, 0.5, 0.0, -4.0, -34.0], "start_source": "command", "cmd_gap_deg": 0.0, "clip": [], "refused": [], "limit_ja": "", "limit_ascii": "", "command": [6.5, 1.0, 0.5, 0.0, -4.0, -32.0], "sent": true, "reached": true, "reached_angles": [6.5, 1.0, 0.5, 0.0, -4.0, -32.0], "reach_err": 0.0, "t_send": 1793615375.994407, "dry": true, "grip_state": "open", "stopped": false, "stop_reason": null, "base_pose": [0.0, 0.0, 0.0, 0.0, 0.0, -30.0]}
{"seq": 134, "t": "2026-11-02T19:29:36.414", "t_unix": 1793615376.415, "button": 12, "button_name": "dpad_down", "kind": "move", "selected_joint": 5, "step_deg": 2.0, "execute": false, "joint": 5, "delta": 2.0, "accepted": true, "measured_before": [6.5, 1.0, 0.5, 0.0, -4.0, -32.0], "start_source": "command", "cmd_gap_deg": 0.0, "clip": [], "refused": [], "limit_ja": "", "limit_ascii": "", "command": [6.5, 1.0, 0.5, 0.0, -4.0, -30.0], "sent": true, "reached": true, "reached_angles": [6.5, 1.0, 0.5, 0.0, -4.0, -30.0], "reach_err": 0.0, "t_send": 1793615376.415506, "dry": true, "grip_state": "open", "stopped": false, "stop_reason": null, "base_pose": [0.0, 0.0, 0.0, 0.0, 0.0, -30.0]}
{"seq": 136, "t": "2026-11-02T19:29:36.949", "t_unix": 1793615376.949, "button": 12, "button_name": "dpad_down", "kind": "move", "selected_joint": 5, "step_deg": 2.0, "execute": false, "joint": 5, "delta": 2.0, "accepted": false, "reason": "busy", "grip_state": "open", "stopped": false, "stop_reason": null, "base_pose": [0.0, 0.0, 0.0, 0.0, 0.0, -30.0]}
{"seq": 135, "t": "2026-11-02T19:29:36.748", "t_unix": 1793615376.748, "button": 12, "button_name": "dpad_down", "kind": "move", "selected_joint": 5, "step_deg": 2.0, "execute": false, "joint": 5, "delta": 2.0, "accepted": true, "measured_before": [6.5, 1.0, 0.5, 0.0, -4.0, -30.0], "start_source": "command", "cmd_gap_deg": 0.0, "clip": [], "refused": [], "limit_ja": "", "limit_ascii": "", "command": [6.5, 1.0, 0.5, 0.0, -4.0, -28.0], "sent": true, "reached": true, "reached_angles": [6.5, 1.0, 0.5, 0.0, -4.0, -28.0], "reach_err": 0.0, "t_send": 1793615376.748905, "dry": true, "grip_state": "open", "stopped": false, "stop_reason": null, "base_pose": [0.0, 0.0, 0.0, 0.0, 0.0, -30.0]}
{"seq": 138, "t": "2026-11-02T19:29:37.432", "t_unix": 1793615377.432, "button": 12, "button_name": "dpad_down", "kind": "move", "selected_joint": 5, "step_deg": 2.0, "execute": false, "joint": 5, "delta": 2.0, "accepted": false, "reason": "busy", "grip_state": "open", "stopped": false, "stop_reason": null, "base_pose": [0.0, 0.0, 0.0, 0.0, 0.0, -30.0]}
{"seq": 137, "t": "2026-11-02T19:29:37.151", "t_unix": 1793615377.152, "button": 12, "button_name": "dpad_down", "kind": "move", "selected_joint": 5, "step_deg": 2.0, "execute": false, "joint": 5, "delta": 2.0, "accepted": true, "measured_before": [6.5, 1.0, 0.5, 0.0, -4.0, -28.0], "start_source": "command", "cmd_gap_deg": 0.0, "clip": [], "refused": [], "limit_ja": "", "limit_ascii": "", "command": [6.5, 1.0, 0.5, 0.0, -4.0, -26.0], "sent": true, "reached": true, "reached_angles": [6.5, 1.0, 0.5, 0.0, -4.0, -26.0], "reach_err": 0.0, "t_send": 1793615377.152193, "dry": true, "grip_state": "open", "stopped": false, "stop_reason": null, "base_pose": [0.0, 0.0, 0.0, 0.0, 0.0, -30.0]}
{"seq": 140, "t": "2026-11-02T19:29:37.783", "t_unix": 1793615377.784, "button": 12, "button_name": "dpad_down", "kind": "move", "selected_joint": 5, "step_deg": 2.0, "execute": false, "joint": 5, "delta": 2.0, "accepted": false, "reason": "busy", "grip_state": "open", "stopped": false, "stop_reason": null, "base_pose": [0.0, 0.0, 0.0, 0.0, 0.0, -30.0]}
{"seq": 139, "t": "2026-11-02T19:29:37.617", "t_unix": 1793615377.617, "button": 12, "button_name": "dpad_down", "kind": "move", "selected_joint": 5, "step_deg": 2.0, "execute": false, "joint": 5, "delta": 2.0, "accepted": true, "measured_before": [6.5, 1.0, 0.5, 0.0, -4.0, -26.0], "start_source": "command", "cmd_gap_deg": 0.0, "clip": [], "refused": [], "limit_ja": "", "limit_ascii": "", "command": [6.5, 1.0, 0.5, 0.0, -4.0, -24.0], "sent": true, "reached": true, "reached_angles": [6.5, 1.0, 0.5, 0.0, -4.0, -24.0], "reach_err": 0.0, "t_send": 1793615377.619, "dry": true, "grip_state": "open", "stopped": false, "stop_reason": null, "base_pose": [0.0, 0.0, 0.0, 0.0, 0.0, -30.0]}
{"seq": 141, "t": "2026-11-02T19:29:38.053", "t_unix": 1793615378.053, "button": 12, "button_name": "dpad_down", "kind": "move", "selected_joint": 5, "step_deg": 2.0, "execute": false, "joint": 5, "delta": 2.0, "accepted": true, "measured_before": [6.5, 1.0, 0.5, 0.0, -4.0, -24.0], "start_source": "command", "cmd_gap_deg": 0.0, "clip": [], "refused": [], "limit_ja": "", "limit_ascii": "", "command": [6.5, 1.0, 0.5, 0.0, -4.0, -22.0], "sent": true, "reached": true, "reached_angles": [6.5, 1.0, 0.5, 0.0, -4.0, -22.0], "reach_err": 0.0, "t_send": 1793615378.053596, "dry": true, "grip_state": "open", "stopped": false, "stop_reason": null, "base_pose": [0.0, 0.0, 0.0, 0.0, 0.0, -30.0]}
{"seq": 143, "t": "2026-11-02T19:29:38.614", "t_unix": 1793615378.614, "button": 12, "button_name": "dpad_down", "kind": "move", "selected_joint": 5, "step_deg": 2.0, "execute": false, "joint": 5, "delta": 2.0, "accepted": false, "reason": "busy", "grip_state": "open", "stopped": false, "stop_reason": null, "base_pose": [0.0, 0.0, 0.0, 0.0, 0.0, -30.0]}
{"seq": 142, "t": "2026-11-02T19:29:38.380", "t_unix": 1793615378.38, "button": 12, "button_name": "dpad_down", "kind": "move", "selected_joint": 5, "step_deg": 2.0, "execute": false, "joint": 5, "delta": 2.0, "accepted": true, "measured_before": [6.5, 1.0, 0.5, 0.0, -4.0, -22.0], "start_source": "command", "cmd_gap_deg": 0.0, "clip": [], "refused": [], "limit_ja": "", "limit_ascii": "", "command": [6.5, 1.0, 0.5, 0.0, -4.0, -20.0], "sent": true, "reached": true, "reached_angles": [6.5, 1.0, 0.5, 0.0, -4.0, -20.0], "reach_err": 0.0, "t_send": 1793615378.380851, "dry": true, "grip_state": "open", "stopped": false, "stop_reason": null, "base_pose": [0.0, 0.0, 0.0, 0.0, 0.0, -30.0]}
{"seq": 144, "t": "2026-11-02T19:29:38.802", "t_unix": 1793615378.802, "button": 12, "button_name": "dpad_down", "kind": "move", "selected_joint": 5, "step_deg": 2.0, "execute": false, "joint": 5, "delta": 2.0, "accepted": true, "measured_before": [6.5, 1.0, 0.5, 0.0, -4.0, -20.0], "start_source": "command", "cmd_gap_deg": 0.0, "clip": [{"joint": 5, "want": -18.0, "cmd": -20.0, "limit": "BASE", "bound": -20.0}], "refused": [{"joint": 5, "why": "at_limit", "value": -18.0, "lo": -40.0, "hi": -20.0, "limit": "BASE"}], "limit_ja": "j5 を -18.00° → -20.00° に切った（基準の姿勢からの範囲 -20.00°） / j5 -18.00° は送らない：範囲の端にいて、押した向きに進めない（範囲 [-40.00, -20.00]・基準の姿勢からの範囲）", "limit_ascii": "j5 clipped -18.00->-20.00 (BASE -20.00) / j5 refused at_limit -18.00 [-40.00, -20.00] (BASE)", "sent": false, "reached": null, "grip_state": "open", "stopped": false, "stop_reason": null, "base_pose": [0.0, 0.0, 0.0, 0.0, 0.0, -30.0]}
{"seq": 145, "t": "2026-11-02T19:29:42.307", "t_unix": 1793615382.307, "button": 5, "button_name": "home", "kind": "home", "selected_joint": 5, "step_deg": 0.5, "execute": false, "held_sec": 1.007, "accepted": true, "measured_before": [6.5, 1.0, 0.5, 0.0, -4.0, -20.0], "command": [0.0, 0.0, 0.0, 0.0, 0.0, -30.0], "range_check": "skipped_for_home", "sent": true, "reached": true, "reached_angles": [0.0, 0.0, 0.0, 0.0, 0.0, -30.0], "reach_err": 0.0, "t_send": 1793615382.3079748, "dry": true, "grip_state": "open", "stopped": false, "stop_reason": null, "base_pose": [0.0, 0.0, 0.0, 0.0, 0.0, -30.0]}
{"seq": 146, "t": "2026-11-02T19:29:57.892", "t_unix": 1793615397.892, "button": 2, "button_name": "square", "kind": "ignored", "selected_joint": 5, "step_deg": 0.5, "execute": false, "accepted": false, "reason": "unassigned", "grip_state": "open", "stopped": false, "stop_reason": null, "base_pose": [0.0, 0.0, 0.0, 0.0, 0.0, -30.0]}
{"seq": 147, "t": "2026-11-02T19:29:59.341", "t_unix": 1793615399.342, "button": 0, "button_name": "cross", "kind": "grip_close", "selected_joint": 5, "step_deg": 0.5, "execute": false, "grip_cmd": 1, "accepted": true, "measured_before": null, "sent": true, "reached": null, "grip_state": "closed", "stopped": false, "stop_reason": null, "base_pose": [0.0, 0.0, 0.0, 0.0, 0.0, -30.0]}
{"seq": 148, "t": "2026-11-02T19:30:01.189", "t_unix": 1793615401.189, "button": 2, "button_name": "square", "kind": "ignored", "selected_joint": 5, "step_deg": 0.5, "execute": false, "accepted": false, "reason": "unassigned", "grip_state": "closed", "stopped": false, "stop_reason": null, "base_pose": [0.0, 0.0, 0.0, 0.0, 0.0, -30.0]}
{"seq": 149, "t": "2026-11-02T19:30:03.745", "t_unix": 1793615403.746, "button": 2, "button_name": "square", "kind": "ignored", "selected_joint": 5, "step_deg": 0.5, "execute": false, "accepted": false, "reason": "unassigned", "grip_state": "closed", "stopped": false, "stop_reason": null, "base_pose": [0.0, 0.0, 0.0, 0.0, 0.0, -30.0]}
{"seq": 150, "t": "2026-11-02T19:30:04.764", "t_unix": 1793615404.765, "button": 0, "button_name": "cross", "kind": "grip_close", "selected_joint": 5, "step_deg": 0.5, "execute": false, "grip_cmd": 1, "accepted": true, "measured_before": null, "sent": true, "reached": null, "grip_state": "closed", "stopped": false, "stop_reason": null, "base_pose": [0.0, 0.0, 0.0, 0.0, 0.0, -30.0]}
{"seq": 151, "t": "2026-11-02T19:30:07.613", "t_unix": 1793615407.613, "button": 1, "button_name": "circle", "kind": "grip_open", "selected_joint": 5, "step_deg": 0.5, "execute": false, "grip_cmd": 0, "accepted": true, "measured_before": null, "sent": true, "reached": null, "grip_state": "open", "stopped": false, "stop_reason": null, "base_pose": [0.0, 0.0, 0.0, 0.0, 0.0, -30.0]}
{"seq": 152, "t": "2026-11-02T19:30:09.579", "t_unix": 1793615409.579, "button": 0, "button_name": "cross", "kind": "grip_close", "selected_joint": 5, "step_deg": 0.5, "execute": false, "grip_cmd": 1, "accepted": true, "measured_before": null, "sent": true, "reached": null, "grip_state": "closed", "stopped": false, "stop_reason": null, "base_pose": [0.0, 0.0, 0.0, 0.0, 0.0, -30.0]}
{"seq": 153, "t": "2026-11-02T19:30:11.055", "t_unix": 1793615411.055, "button": 1, "button_name": "circle", "kind": "grip_open", "selected_joint": 5, "step_deg": 0.5, "execute": false, "grip_cmd": 0, "accepted": true, "measured_before": null, "sent": true, "reached": null, "grip_state": "open", "stopped": false, "stop_reason": null, "base_pose": [0.0, 0.0, 0.0, 0.0, 0.0, -30.0]}
{"seq": 154, "t": "2026-11-02T19:30:12.601", "t_unix": 1793615412.602, "button": 0, "button_name": "cross", "kind": "grip_close", "selected_joint": 5, "step_deg": 0.5, "execute": false, "grip_cmd": 1, "accepted": true, "measured_before": null, "sent": true, "reached": null, "grip_state": "closed", "stopped": false, "stop_reason": null, "base_pose": [0.0, 0.0, 0.0, 0.0, 0.0, -30.0]}
{"seq": 155, "t": "2026-11-02T19:30:16.216", "t_unix": 1793615416.216, "button": 14, "button_name": "dpad_right", "kind": "move", "selected_joint": 5, "step_deg": 0.5, "execute": false, "joint": 0, "delta": 0.5, "accepted": true, "measured_before": [0.0, 0.0, 0.0, 0.0, 0.0, -30.0], "start_source": "command", "cmd_gap_deg": 0.0, "clip": [], "refused": [], "limit_ja": "", "limit_ascii": "", "command": [0.5, 0.0, 0.0, 0.0, 0.0, -30.0], "sent": true, "reached": true, "reached_angles": [0.5, 0.0, 0.0, 0.0, 0.0, -30.0], "reach_err": 0.0, "t_send": 1793615416.217888, "dry": true, "grip_state": "closed", "stopped": false, "stop_reason": null, "base_pose": [0.0, 0.0, 0.0, 0.0, 0.0, -30.0]}
{"seq": 157, "t": "2026-11-02T19:30:17.205", "t_unix": 1793615417.205, "button": 14, "button_name": "dpad_right", "kind": "move", "selected_joint": 5, "step_deg": 0.5, "execute": false, "joint": 0, "delta": 0.5, "accepted": false, "reason": "busy", "grip_state": "closed", "stopped": false, "stop_reason": null, "base_pose": [0.0, 0.0, 0.0, 0.0, 0.0, -30.0]}
{"seq": 156, "t": "2026-11-02T19:30:16.949", "t_unix": 1793615416.949, "button": 14, "button_name": "dpad_right", "kind": "move", "selected_joint": 5, "step_deg": 0.5, "execute": false, "joint": 0, "delta": 0.5, "accepted": true, "measured_before": [0.5, 0.0, 0.0, 0.0, 0.0, -30.0], "start_source": "command", "cmd_gap_deg": 0.0, "clip": [], "refused": [], "limit_ja": "", "limit_ascii": "", "command": [1.0, 0.0, 0.0, 0.0, 0.0, -30.0], "sent": true, "reached": true, "reached_angles": [1.0, 0.0, 0.0, 0.0, 0.0, -30.0], "reach_err": 0.0, "t_send": 1793615416.950113, "dry": true, "grip_state": "closed", "stopped": false, "stop_reason": null, "base_pose": [0.0, 0.0, 0.0, 0.0, 0.0, -30.0]}
{"seq": 159, "t": "2026-11-02T19:30:17.747", "t_unix": 1793615417.747, "button": 14, "button_name": "dpad_right", "kind": "move", "selected_joint": 5, "step_deg": 0.5, "execute": false, "joint": 0, "delta": 0.5, "accepted": false, "reason": "busy", "grip_state": "closed", "stopped": false, "stop_reason": null, "base_pose": [0.0, 0.0, 0.0, 0.0, 0.0, -30.0]}
{"seq": 158, "t": "2026-11-02T19:30:17.494", "t_unix": 1793615417.494, "button": 14, "button_name": "dpad_right", "kind": "move", "selected_joint": 5, "step_deg": 0.5, "execute": false, "joint": 0, "delta": 0.5, "accepted": true, "measured_before": [1.0, 0.0, 0.0, 0.0, 0.0, -30.0], "start_source": "command", "cmd_gap_deg": 0.0, "clip": [], "refused": [], "limit_ja": "", "limit_ascii": "", "command": [1.5, 0.0, 0.0, 0.0, 0.0, -30.0], "sent": true, "reached": true, "reached_angles": [1.5, 0.0, 0.0, 0.0, 0.0, -30.0], "reach_err": 0.0, "t_send": 1793615417.49533, "dry": true, "grip_state": "closed", "stopped": false, "stop_reason": null, "base_pose": [0.0, 0.0, 0.0, 0.0, 0.0, -30.0]}
{"seq": 160, "t": "2026-11-02T19:30:19.098", "t_unix": 1793615419.099, "button": 14, "button_name": "dpad_right", "kind": "move", "selected_joint": 5, "step_deg": 0.5, "execute": false, "joint": 0, "delta": 0.5, "accepted": true, "measured_before": [1.5, 0.0, 0.0, 0.0, 0.0, -30.0], "start_source": "command", "cmd_gap_deg": 0.0, "clip": [], "refused": [], "limit_ja": "", "limit_ascii": "", "command": [2.0, 0.0, 0.0, 0.0, 0.0, -30.0], "sent": true, "reached": true, "reached_angles": [2.0, 0.0, 0.0, 0.0, 0.0, -30.0], "reach_err": 0.0, "t_send": 1793615419.100053, "dry": true, "grip_state": "closed", "stopped": false, "stop_reason": null, "base_pose": [0.0, 0.0, 0.0, 0.0, 0.0, -30.0]}
{"seq": 161, "t": "2026-11-02T19:30:20.552", "t_unix": 1793615420.552, "button": 3, "button_name": "triangle", "kind": "select_joint", "selected_joint": 1, "step_deg": 0.5, "execute": false, "accepted": true, "grip_state": "closed", "stopped": false, "stop_reason": null, "base_pose": [0.0, 0.0, 0.0, 0.0, 0.0, -30.0]}
{"seq": 162, "t": "2026-11-02T19:30:23.567", "t_unix": 1793615423.567, "button": 10, "button_name": "r1", "kind": "modifier", "selected_joint": 1, "step_deg": 2.0, "execute": false, "accepted": true, "grip_state": "closed", "stopped": false, "stop_reason": null, "base_pose": [0.0, 0.0, 0.0, 0.0, 0.0, -30.0]}
{"seq": 163, "t": "2026-11-02T19:30:23.897", "t_unix": 1793615423.898, "button": 14, "button_name": "dpad_right", "kind": "move", "selected_joint": 1, "step_deg": 2.0, "execute": false, "joint": 0, "delta": 2.0, "accepted": true, "measured_before": [2.0, 0.0, 0.0, 0.0, 0.0, -30.0], "start_source": "command", "cmd_gap_deg": 0.0, "clip": [], "refused": [], "limit_ja": "", "limit_ascii": "", "command": [4.0, 0.0, 0.0, 0.0, 0.0, -30.0], "sent": true, "reached": true, "reached_angles": [4.0, 0.0, 0.0, 0.0, 0.0, -30.0], "reach_err": 0.0, "t_send": 1793615423.899039, "dry": true, "grip_state": "closed", "stopped": false, "stop_reason": null, "base_pose": [0.0, 0.0, 0.0, 0.0, 0.0, -30.0]}
{"seq": 164, "t": "2026-11-02T19:30:24.650", "t_unix": 1793615424.65, "button": 14, "button_name": "dpad_right", "kind": "move", "selected_joint": 1, "step_deg": 2.0, "execute": false, "joint": 0, "delta": 2.0, "accepted": true, "measured_before": [4.0, 0.0, 0.0, 0.0, 0.0, -30.0], "start_source": "command", "cmd_gap_deg": 0.0, "clip": [], "refused": [], "limit_ja": "", "limit_ascii": "", "command": [6.0, 0.0, 0.0, 0.0, 0.0, -30.0], "sent": true, "reached": true, "reached_angles": [6.0, 0.0, 0.0, 0.0, 0.0, -30.0], "reach_err": 0.0, "t_send": 1793615424.650733, "dry": true, "grip_state": "closed", "stopped": false, "stop_reason": null, "base_pose": [0.0, 0.0, 0.0, 0.0, 0.0, -30.0]}
{"seq": 165, "t": "2026-11-02T19:30:25.342", "t_unix": 1793615425.342, "button": 14, "button_name": "dpad_right", "kind": "move", "selected_joint": 1, "step_deg": 2.0, "execute": false, "joint": 0, "delta": 2.0, "accepted": true, "measured_before": [6.0, 0.0, 0.0, 0.0, 0.0, -30.0], "start_source": "command", "cmd_gap_deg": 0.0, "clip": [], "refused": [], "limit_ja": "", "limit_ascii": "", "command": [8.0, 0.0, 0.0, 0.0, 0.0, -30.0], "sent": true, "reached": true, "reached_angles": [8.0, 0.0, 0.0, 0.0, 0.0, -30.0], "reach_err": 0.0, "t_send": 1793615425.3432372, "dry": true, "grip_state": "closed", "stopped": false, "stop_reason": null, "base_pose": [0.0, 0.0, 0.0, 0.0, 0.0, -30.0]}
{"seq": 166, "t": "2026-11-02T19:30:26.144", "t_unix": 1793615426.145, "button": 14, "button_name": "dpad_right", "kind": "move", "selected_joint": 1, "step_deg": 2.0, "execute": false, "joint": 0, "delta": 2.0, "accepted": true, "measured_before": [8.0, 0.0, 0.0, 0.0, 0.0, -30.0], "start_source": "command", "cmd_gap_deg": 0.0, "clip": [], "refused": [], "limit_ja": "", "limit_ascii": "", "command": [10.0, 0.0, 0.0, 0.0, 0.0, -30.0], "sent": true, "reached": true, "reached_angles": [10.0, 0.0, 0.0, 0.0, 0.0, -30.0], "reach_err": 0.0, "t_send": 1793615426.145644, "dry": true, "grip_state": "closed", "stopped": false, "stop_reason": null, "base_pose": [0.0, 0.0, 0.0, 0.0, 0.0, -30.0]}
{"seq": 167, "t": "2026-11-02T19:30:27.392", "t_unix": 1793615427.392, "button": "L2+R2", "button_name": "L2+R2", "kind": "stop", "selected_joint": 1, "step_deg": 0.5, "execute": false, "accepted": true, "reason": "L2+R2", "already_stopped": false, "busy": false, "send_committed_before_stop": null, "triggers": [1.0, 1.0], "grip_state": "closed", "stopped": true, "stop_reason": "L2+R2", "base_pose": [0.0, 0.0, 0.0, 0.0, 0.0, -30.0]}
{"seq": 168, "t": "2026-11-02T19:30:30.507", "t_unix": 1793615430.507, "button": 5, "button_name": "home", "kind": "home", "selected_joint": 1, "step_deg": 0.5, "execute": false, "accepted": false, "reason": "stopped", "grip_state": "closed", "stopped": true, "stop_reason": "L2+R2", "base_pose": [0.0, 0.0, 0.0, 0.0, 0.0, -30.0]}
{"seq": 169, "t": "2026-11-02T19:30:39.657", "t_unix": 1793615439.657, "button": 13, "button_name": "dpad_left", "kind": "move", "selected_joint": 1, "step_deg": 0.5, "execute": false, "joint": 0, "delta": -0.5, "accepted": false, "reason": "stopped", "grip_state": "closed", "stopped": true, "stop_reason": "L2+R2", "base_pose": [0.0, 0.0, 0.0, 0.0, 0.0, -30.0]}
{"seq": 170, "t": "2026-11-02T19:30:41.192", "t_unix": 1793615441.193, "button": 13, "button_name": "dpad_left", "kind": "move", "selected_joint": 1, "step_deg": 0.5, "execute": false, "joint": 0, "delta": -0.5, "accepted": false, "reason": "stopped", "grip_state": "closed", "stopped": true, "stop_reason": "L2+R2", "base_pose": [0.0, 0.0, 0.0, 0.0, 0.0, -30.0]}
{"seq": 171, "t": "2026-11-02T19:30:41.750", "t_unix": 1793615441.75, "button": 14, "button_name": "dpad_right", "kind": "move", "selected_joint": 1, "step_deg": 0.5, "execute": false, "joint": 0, "delta": 0.5, "accepted": false, "reason": "stopped", "grip_state": "closed", "stopped": true, "stop_reason": "L2+R2", "base_pose": [0.0, 0.0, 0.0, 0.0, 0.0, -30.0]}
{"seq": 172, "t": "2026-11-02T19:30:50.768", "t_unix": 1793615450.769, "button": 6, "button_name": "options", "kind": "resume", "selected_joint": 1, "step_deg": 0.5, "execute": false, "accepted": true, "grip_state": "closed", "stopped": false, "stop_reason": null, "base_pose": [0.0, 0.0, 0.0, 0.0, 0.0, -30.0]}
```

【図解】LOG419_gamepad_controls.html


## ゲームパッドの HOME ボタンを押すと、Mac の Apple Games が開いた

ゲームパッドの電源を入れるには HOME ボタンを押す。そのたびに、Mac で Apple Games のアプリが開いた。HOME ボタンは、アームを HOME の姿勢に戻すボタンにも使うので、止めておきたい。

【画像】LOG419_05_apple_games_opened.png ゲームパッドの HOME ボタンを押したときに Mac に出た「Apple Games へようこそ」の画面

Mac（macOS 26.6.2）では、システム設定 →「ゲームコントローラ」→「コントローラのショートカット」に、HOME ボタンの設定があった。「コントローラショートカットを有効にする」が入っていて、「ホームボタンを押して開く」が「ゲーム」になっている。コントローラーを選んだ先の、ボタンごとの割り当ての画面にはなく、1つ前の画面にあった。

【画像】LOG419_06_controller_shortcut_setting.png システム設定のゲームコントローラの中の「コントローラのショートカット」。コントローラショートカットを有効にするが入っていて、ホームボタンを押して開く先がゲームになっている

「コントローラショートカットを有効にする」を切ると、HOME ボタンを押しても Games は開かなくなる。切ったあとも、スクリプトからは HOME ボタンをボタン5として読める見込みで、次に gamepad_check.py を動かしたときに確かめる。

同じ設定の中の、コントローラーを選んだ先の画面の一番下には「ジェスチャ共有」があり、SHARE ボタンの2回押しが「収録を開始/停止」になっていた。スクリプトでは SHARE を使わないが、誤って押すと画面の収録が始まるので、こちらも「オフ」にしておく。


## 依頼69：ドライランは記録しない・起動時の SDL の警告を直す

残っていた3つを決めて、依頼69を置いた。19:39 に作業が始まり、この記録の時点では作業中。

| 残っていたこと | 決めたこと |
|---|---|
| ドライランの記録 | 記録しない。KIOXIA がつながっていなくても動くようにする。11/2 のドライランの記録は消さない |
| 動いている途中で止める方法 | 今は足さない。スクリプトの説明に「送った1回分は最後まで動く」と書く |
| 十字キーの左右の向き | 実機で確かめてから決める |
| SDL が2つ読み込まれる警告 | どこで読み込まれているかを調べ、gamepad_teleop.py の側で避ける。Python の環境は変えない |

### 依頼文 20261102_69_ゲームパッドのドライランは記録しない・SDLの重複を直す.md

````markdown
---
作業フォルダ: /Users/shimazueiichi/r2r2r-scripts
上限時間: 120
---
# 依頼69：tools/gamepad_teleop.py のドライランは記録しない・SDL が2つ読み込まれる警告を直す。実機には流さない（2026-11-02）

依頼68（commit 78bd62e）の「決めてほしいこと」に、エイイチが決めた（2026-11-02 19:36）。
**操作（ボタンの割り当て・刻み・守りの値）は変えない。**

## エイイチが決めたこと

1. **ドライランは記録しない**：`--execute` がないときは、KIOXIA に何も書かない（フォルダも作らない）。KIOXIA がつながっていなくてもドライランは動くようにする。ターミナルと画面の表示は今のまま
   - 今ある `runs/experiments/gamepad_teleop/teleop_20261102_192657_dry/` は測定データなので、消さない・動かさない
2. **動いている途中で止める方法は、今は足さない**：daemon も送り方も変えない。スクリプトの先頭の説明に「L2+R2 は新しい指令を送らないだけで、送った1回分（HOME を含む）は最後まで動く」と書く
3. **十字キー左右の向き（今は右 = +）**：明日の実機で確かめてから決める。この依頼では変えない

## 直すこと：SDL が2つ読み込まれる警告

2026-11-02 19:26 にドライランを起動すると、次の警告が十数行出た（動きには影響なし）。

```
objc[86737]: Class SDLApplication is implemented in both /opt/homebrew/Caskroom/miniforge/base/lib/python3.12/site-packages/pygame/.dylibs/libSDL2-2.0.0.dylib (...) and ...
objc[86737]: Class SDLAppDelegate is implemented in both ...
（SDLTranslatorResponder・SDLMessageBoxPresenter・SDL_cocoametalview・SDLOpenGLContext・SDL_ShapeData・SDL_CocoaClosure・SDL_VideoData・SDL_WindowData・SDLWindow・Cocoa_WindowListener・SDLView・METAL_RenderData・METAL_TextureData・SDL_RumbleMotor・SDL_RumbleContext も同じ）
```

`tools/gamepad_check.py`（pygame だけを読む）では出なかった。gamepad_teleop.py が読み込む別のモジュール（visual_servo_pick など）の先で、2つ目の SDL が読み込まれていると見られる。

- どのモジュールが2つ目の SDL を読み込んでいるか（どのファイル・どのパッケージか）を突き止めて、報告に書く
- 直し方：gamepad_teleop.py の側で、その読み込みを避ける（例：要る関数だけを読む・読み込みを `--execute` のときだけにする・読み込む順番を変える）。**ほかのスクリプトと、Python の環境（パッケージの入れ替え・削除）は変えない。** 環境を変えないと直らない場合は、直さずに「決めてほしいこと」に書く
- 直したあと、ドライランで警告が出ないことを確かめる

## 確かめ（実機なし）

- 今のテスト46件が通ること。ドライランで KIOXIA に書かないことのテストを足す（`--execute` のときは今までどおり書く）
- ドライランを起動して、警告が出ないことと、画面とターミナルの表示が今までと同じことを確かめる

## 変えないもの

- 操作の割り当て・刻み・守りの値・送り方・停止の作り（上の1〜3以外）
- daemon、run_policy_client.py・visual_servo_pick.py などの既存のスクリプト、tools/gamepad_check.py
- 既存のデータ

## 終わったら

- 関節を動かすスクリプトの変更なので、codex のレビューを通してから報告する（code_review_request_template.md の形）。指摘は直すか、直さない理由を書く
- 実機には流さない
- 個別のファイルを指定して add・commit する（`git add .` は使わない）
````


## 次に実機で確かめること

| 順番 | 確かめること | 見る数字 |
|---|---|---|
| 1 | 手首（j4）が指令に応えるかを点検する（LOG422 で止まったまま） | 指令と実測の差 |
| 2 | アームを HOME に置いて `--execute` で起動し、j0 を0.5°ずつ数回 | 十字キーの右で、根元がどちらに回るか |
| 3 | L2+R2 の停止と OPTIONS での再開、爪の開け閉め、HOME の長押し | 停止のあとに指令が出ていないこと |
| 4 | AI（π0.5）の動きを、キーボードの代わりにゲームパッドで直す | 1回の直しにかかる時間（キーボードのときと比べる）・command と reached の差 |
