mechArm 270 Pi 動的追従の限界を探る Log#41 | ブロックを掴んでいるのに落とす。原因を測定で剥がしていく
ブロックは掴めている。爪の間に収まって、持ち上がる。なのに、置く前に落ちる。落ちるのは決まって、写真を撮るためにもう一度アームを動かしたときだった。その瞬間、爪がわずかに緩む動きをする。
掴めているのに落とす、という症状は厄介だ。掴めていないなら掴み方を直せばいい。届いていないなら届かせればいい。だが「掴んでいるのに落とす」は、どこが悪いのか自分では言えない。だから測ることにした。落ちる瞬間に何が起きているのかを、推測ではなく数値で剥がしていく。
最初は撮影を疑った。次に関節を疑った。どちらも違った。最後に残ったのは、爪が「位置で止まる」だけで「圧力をかけ続けない」という問題だった。そこに辿り着くまでに、二度、見立てを捨てている。その捨てた過程も含めて残す。
撮影を疑う
最初の見立ては撮影だった。落ちるのが「写真を撮るために持ち上げる瞬間」に集中していたからだ。このパイプラインでは、PICK の後に結果を撮影して VLM で成否を判定する。撮影のたびにカメラ(cv2.VideoCapture)を開くのだが、これが Mac とラズパイ上の制御デーモンをつなぐ TCP 接続を切る。撮影が終わると再接続する。この「切断 → 撮影 → 再接続」のサイクルが、保持しているグリッパーを乱しているのではないか、という仮説を立てた。
測れる手段を先に用意する、という原則をこのプロジェクトでは守っている。理屈を先に立てて納得するのではなく、まず一つ測ってから考える。今回測りたいのはグリッパーの開き具合だ。get_gripper_value() という API があり、爪の位置を 0〜100 で返す。この値は大きいほど開き、小さいほど閉じている。これを撮影サイクルの前後で読めば、撮影で爪が緩んだかどうかが数値で出る。
ただし制約がある。get_gripper_value() はデーモン経由で読むため、撮影でその接続が切れている間は読めない。だから測定は「接続が生きている瞬間」に限る。撮影は接続を閉じる → カメラを開く → 撮影 → カメラを閉じる → 接続を開き直す、というブラケットで囲み、その前後で値を読む構造にした。
基準値を取るために、ブロックを掴まずに爪を開閉だけさせて値を読む。最初の二回はおかしな値が出た。
[CONTROL] no-block 基準値取得
[EVT ] M0 measure gv=-1 gm=True conn=True note=after_open
[EVT ] M2 measure gv=98 gm=True conn=True note=after_close_no_block # 1回目
[EVT ] M2 measure gv=-1 gm=True conn=True note=after_close_no_block # 2回目同じ空振りの開閉なのに、閉じたときの値が 98 と -1 でばらついた。この時点で一度、get_gripper_value() は保持の指標として使えない、と判断しかけた。指標が死んだなら測定設計ごと作り直しだ、と。
だが、その判断は早かった。連続でもう三回測ると、値が落ち着いた。
[EVT ] M0 measure gv=97 gm=False note=after_open # 開いた直後
[EVT ] M2 measure gv=6 gm=False note=after_close_no_block # 空振りで閉じ切った直後これが五回連続で一致した。開くと 97、何も挟まずに閉じ切ると 6。値が安定したときは gm=False(グリッパー静止)で、ばらついた最初の二回は gm=True(動作中)だった。爪が動いている最中に読むと無効値が返り、整定してから読めば安定する。指標が死んでいたのではなく、読むタイミングが死んでいた。撤回する。
基準が決まった。開=97、空振り閉=6。ブロックを挟んでいれば、その厚みのぶん閉じ切れないので、6 より上の中間値になるはずだ。撮影の前後でこの値が 6 へ近づけば「中身が抜けた」と読める。
撮影サイクルは無罪だった
ここで一つ壁にぶつかった。実際に PICK して撮影窓を再現するには、アームを所定の位置へ降ろして掴む必要があるのだが、採用済みの PICK 姿勢を読み込ませると姿勢の検証で弾かれた。降下姿勢と中間姿勢の扱いがコードの想定とずれていて、まともに掴ませる経路がその場では通らなかった。
回り道をやめて、もっと素直な方法に切り替えた。アームを HOME 姿勢に置いたまま、手でブロックを爪の間に差し込んで掴ませる。そのうえで撮影サイクルだけを通す。降下も持ち上げもない。撮影が保持を壊すかどうか、その一点だけを切り出す。
掴んだ直後の値は 38。空振り閉の 6 とははっきり違う。ブロックを挟めている。その状態で撮影サイクルを二回通した。全量で残す。
ts,phase,event,gripper_value,gripper_moving,daemon_connected,cmd_issued,note
2026-06-28T19:12:18.364,M0,measure,97,False,True,,home_open_before_grip
2026-06-28T19:12:18.364,grip,gripper_cmd,,,True,"set_gripper_state(1,100)",
2026-06-28T19:12:24.423,M2,measure,38,False,True,,home_after_grip
2026-06-28T19:12:24.425,capture1,daemon_close,,,False,,
2026-06-28T19:12:25.792,capture1,camera_open,,,False,,top_idx=2
2026-06-28T19:12:26.980,capture1,camera_release,,,False,,saved:homegrip_20260628_191206_homegrip_pre.jpg
2026-06-28T19:12:27.000,reconnect,daemon_open,,,True,,
2026-06-28T19:12:27.046,M3,measure,43,False,True,,home_after_capture1
2026-06-28T19:12:27.046,capture2,daemon_close,,,False,,
2026-06-28T19:12:27.115,capture2,camera_open,,,False,,top_idx=2
2026-06-28T19:12:28.340,capture2,camera_release,,,False,,saved:homegrip_20260628_191206_homegrip_post.jpg
2026-06-28T19:12:28.358,reconnect,daemon_open,,,True,,
2026-06-28T19:12:28.383,M4,measure,36,False,True,,home_after_capture2
2026-06-28T19:12:30.424,M5,measure,36,False,True,,home_after_M4_plus_2s掴んだ直後(M2)が 38、撮影一回目の後(M3)が 43、撮影二回目の後(M4)が 36、二秒後(M5)が 36。±数の揺れで、空振り閉の 6 へ向かう動きはない。撮影後の画像でも、赤いブロックは爪の間に収まったままだった。落ちていない。
ただ、撮影窓は接続を切るので、切っていない比較も取らないと公平でない。撮影を、同じ時間だけ「カメラを開かず接続を保ったまま待つ」処理に置き換えたモードを回した。これも全量で。
2026-06-28T19:17:40.644,M2,measure,38,False,True,,home_after_grip
2026-06-28T19:17:40.645,capture1,idle_sleep,,,True,,sleep=1.944s
2026-06-28T19:17:42.626,M3,measure,38,False,True,,home_after_capture1
2026-06-28T19:17:42.626,capture2,idle_sleep,,,True,,sleep=1.944s
2026-06-28T19:17:44.626,M4,measure,36,False,True,,home_after_capture2
2026-06-28T19:17:46.665,M5,measure,36,False,True,,home_after_M4_plus_2s撮影あり(38→43→36→36)と、撮影なし(38→38→36→36)。最終的な落ち着き先は同じ 36 で、どちらも 6 へ向かわない。両方ともブロックは落ちなかった。
この回で疑ったものと、その結果を先に並べておく。撮影はここまでで外れた。残りはこの下で一つずつ剥がす。
疑ったもの | 測ったこと | 結果 | 判断 |
|---|---|---|---|
撮影サイクル | daemon 切断・再接続前後の gripper 値 | 38→43→36→36 | 落下原因ではない |
撮影なし待機 | 同じ時間だけ接続を保って待つ | 38→38→36→36 | 撮影ありと同じ |
j4 の垂れ | 指令値と実測角の差 | ±4°以内 | 主因ではない |
関節全体 | 到達後の実測角 | 読みタイミングが怪しい | 測定設計に課題 |
爪の保持方式 | 手でブロックを動かす | 爪は締め直さない | 位置で止まるだけ |
そして、撮影窓のあいだにグリッパーへ送った命令を確認すると、開・閉・最後の解放の三つだけで、撮影の前後には一つも出ていない。命令ゼロ。撮影サイクルそのものは、保持を壊していなかった。最初の見立ては外れた。
動かすと落ちる、場所を選ばずに
撮影が無罪なら、最初に「撮影で落ちた」のは何だったのか。HOME に置いたまま掴んだだけでは症状が出ない。症状が出ていたのは「掴んで、腕を起こして、運ぶ」という姿勢の変化を伴う流れだった。HOME 掴みはその流れを消してしまっている。だから再現しなかった。
姿勢変化を入れる。掴んだまま、本番で通る四つの姿勢 ── APPROACH_PICK、PICK、APPROACH_PLACE、PLACE ── を順に巡らせて、各到達点で値を測る。撮影はしない。姿勢を変えることと保持の関係だけを見る。何周も回せるようにした。
ここで二つのことが同時に起きた。一つは目視。掴んだブロックが、特定の場所に限らず落ちる。APPROACH_PICK から PICK のあたりで緩むことが多い気はするが、決まった一箇所ではない。そしてアームを動かしているあいだ、爪がモゾモゾと細かく動いている。さらに、j4 という関節が緩んでアームの先が垂れることもある。
もう一つは数値。各到達点で関節の実測値を指令値と突き合わせ、ずれを出した。目視で「j4 が垂れる」と見えたので、j4 のずれが大きいはずだと思った。だが出てきた数字は逆だった。三周ぶんの j4 のずれを並べる。
M_grip APPROACH_PICK PICK APPROACH_PLACE PLACE
200457(d_j4) - -3.86 2.64 0.97 1.05
200627(d_j4) - -3.86 2.73 1.76 0.88
200737(d_j4) - -0.62 1.85 1.85 0.88
j4 のずれは全部 ±4 度以内。指令通りの場所にほぼ来ている。垂れていない。大きくずれていたのは別の関節(j1 で -74 度、j2 で -86 度など)で、これは「保持が緩んだ」のではなく「そもそもその角度まで動き切る前に読んでしまった」ずれだった。整定の判定を爪の動き基準にしていたので、関節がまだ動いている途中で実測を読んでいた。関節の数字自体、信用しきれない。
目視は「j4 が垂れる・場所を選ばず落ちる」と言い、数字は「j4 のずれは小さい」と言う。食い違っている。どちらかが嘘なのではなく、測定が、目に見えている現象を捉えられていない。少なくとも、撮影サイクルと j4 の到達ずれでは、この落下を説明できなかった。爪の値(gv)はこの巡回中ずっと無効値(-1)を返し続けていて、関節の値もタイミングが怪しい。ここで二度目の足踏みをした。測りに行ったはずの道具が、肝心の場面で測れていない。
位置で止まる爪
行き詰まりを抜けたのは、測定ではなく一つの見立てだった。
ブロックを掴んだ後に爪の位置が固定される。ただし爪自体に少しガタがあるため、固定された爪からガタの隙間ぶんブロックが落ちる。
理想は、掴んだ後もブロックに圧力をかけ続ける爪だ。今は違う。閉じる命令は「ある位置まで閉じて、そこで止まる」だけで、ブロックを押さえ続けてはいない。位置で止まっているから、爪とブロックのあいだにガタの隙間が残る。動かさなければ問題ないが、姿勢を変えて揺さぶると、その隙間からブロックが抜ける。
この見立ては、これまでの観察を一本につなぐ。爪のモゾモゾは、位置で止まった爪がガタの範囲で当て込みを繰り返している動きだ。命令ゼロでも動くのはそのため。HOME 掴みで落ちなかったのは、動かさなければガタが揺さぶられないからだ。巡回で落ちたのは、動かすたびにガタが励起されるから。場所を選ばないのは、ガタが姿勢に依らず励起されるから。j4 が垂れて見えたのは、ガタでブロックがずれた見た目だったのかもしれない。
確かめるのは簡単だった。HOME で掴ませて、その状態で手でブロックを押したり引いたりする。圧力で握っているなら、爪はブロックの動きを追って締め直すはずだ。位置で止まっているだけなら、追わない。
追わなかった。ブロックを動かしても、爪は握り直さない。位置で固定されたまま、隙間のぶんブロックが遊ぶ。見立ては実機で裏付けられた。症状の本体は、撮影でも関節でもなく、把持の方式そのものだった。爪は位置を保つが、圧力を保たない。
圧力をかけ続ける手はあるか
では、圧力をかけ続ける握り方ができるのか。グリッパー制御の API を調べた。
pymycobot のグリッパー制御は三系統ある。今使っている set_gripper_state(state, speed) は state を 0(開)か 1(閉)で渡し、閉じ切る位置まで動いて止まる。set_gripper_value(value, speed) は 0〜100 の開き量を数値で指定する。set_gripper_protection_current(current) は保護電流を 1〜500 で設定する。
正直なところ、「ブロックに力をかけ続ける」専用のモードは見当たらなかった。三つとも位置ベースで、保護電流も「閉じるときにこの電流を超えたら止める」安全閾値であって、握力を保ち続ける指令ではない。
ただ、位置制御の枠内で圧力に近い効果を出せそうな手は残っている。set_gripper_value で「ブロックの厚みに相当する値より、さらに閉じた位置」を指令する。爪はその目標に届こうとし続けるが、ブロックがあるので閉じ切れない。届かないぶん、閉じようとする力がブロックに残り続ける。本物の力制御ではない。だが、閉じ切れない目標位置を与えることで、位置サーボの押し込みを保持力として使える可能性がある。これはガタを圧力で潰せるかの直接の試しになる。
デーモンを確認すると、set_gripper_value はまだ実装されていなかった。あるのは state ベースの set_gripper だけだ。次は、このコマンドをデーモンに足し、閉じ込み量を変えて握らせ、巡回で落ちなくなるかを比べる。今の set_gripper_state(1) では落ちる巡回が、set_gripper_value の押し込みで落ちなくなれば、この手が効いたことになる。
撮影で落ちると思っていた。測ると、撮影ではなかった。j4 が垂れると思っていた。測ると、それでも説明できなかった。落ちる瞬間の一点を疑うところから始めて、撮影を外し、関節を外し、最後に把持の方式に行き着いた。閉じた位置で止まることと、握り続けることは違う。剥がし終えて残ったのは、閉じているが握り続けていない爪、という一行だった。
