mechArm 270のリアル動作がUnityシミュレーションで再現された|R2R2Rループ検証
完全自動ループの初回実行。そして実機の関節角度データがUnityのロボットを動かすまで。
💡 Key Takeaway
「Real → Render」が繋がった。実機mechArmが動いた軌跡のCSVを、Unityのロボットがそのまま再生できる。指令値と実測値のズレがデータとして見えた瞬間、Phase 2の本当の作業が始まった。
やったこと | 起きたこと | 判断 |
|---|---|---|
r2r2r_loop.pyを実行した | paramikoが未インストールだった | pip installして即解決 |
SSH経由でPiにスクリプトを転送した |
|
|
バックグラウンドスレッドで角度をロギングした | pymycobotがシリアル競合で全サンプル失敗(0行) | メインスレッドでステップ後に1回取得する方式に切り替えた |
11ステップ分の関節角度を取得してUnityで再生した | 実機の動きがシミュレーションで再現された | 指令値と実測値のズレがデータとして見えた |
完全自動ループ、初回実行
— コマンドを1本打てば、ロボットが動いてカメラが判定して結果が記録される。
有線Ethernetを永続化して、paramikoによるSSH自動実行を実装した。それを初めて本番で動かす日だった。
cd ~/r2r2r-scripts && python3 r2r2r_loop.py最初のエラーは想定内だった。
ModuleNotFoundError: No module named 'paramiko'pip install paramiko で即解決。次のエラーはもう少し込み入っていた。
× ファイル転送エラー: [Errno 2] No such file原因はREMOTE_SCRIPTの設定だった。r2r2r_loop.py の中でこう書いていた。
REMOTE_SCRIPT = "~/pick_and_place_fixed.py"paramikoのSFTPはシェルを介さないため、チルダを展開しない。フルパスに修正した。
REMOTE_SCRIPT = "/home/ubuntu/pick_and_place_fixed.py"その後、カメラエラーも出た。IriunがTP-Link_9824に接続されていなかった。接続して再実行した。
Step3: APPROACH_PICK位置へ移動 → 目標角度: [-54.22, 81.91, -30.14, -8.08, -50.27, -29.44] ✓ 完了
Step4: PICK位置へ移動 → 目標角度: [-46.93, 102.74, -43.33, 3.6, -60.11, -31.9] ✓ 完了
Step5: グリッパーを閉じる ✓ 完了
Step6: APPROACH_PICK位置へ戻る → 目標角度: [-54.22, 81.91, -30.14, -8.08, -50.27, -29.44] ✓ 完了
Step7: APPROACH_PLACE位置へ移動 → 目標角度: [59.85, 16.87, 27.94, 10.01, -45.17, -35.33] ✓ 完了
Step8: PLACE位置へ移動 → 目標角度: [54.49, 45.17, 26.63, 2.9, -70.57, -29.26] ✓ 完了
Step9: グリッパーを開く ✓ 完了
Step10: APPROACH_PLACE位置へ戻る → 目標角度: [59.85, 16.87, 27.94, 10.01, -45.17, -35.33] ✓ 完了
Step11: ホーム位置へ戻る → 目標角度: [0, 0, 0, 0, 0, -30] ✓ 完了
[Attempt 1/3] Red ratio: 18.3% → OBJECT DETECTED ✓
📝 ログ記録: trial=1 result=SUCCESS fail_step=None red_ratio=18.3%
=== Loop Complete: SUCCESS ===初回実行で成功した。Mac → SSH(有線Ethernet)→ Pi → mechArm → 動作実行 → カメラ判定 → CSVログ記録。このパイプラインが1本のコマンドで動いた。
実機の関節角度を記録する
— ループが動くなら、その中にロギングを追加するだけだ。
完全自動ループが成立した時点で、次のミッションが見えた。実機の関節角度を時系列で記録して、UnityのArticulationBodyに渡す。Real → Renderのデータパイプラインだ。
失敗:バックグラウンドスレッド方式(0行)
最初の実装はバックグラウンドスレッドで50msごとにget_angles()をポーリングする方式だった。
_logging_active = False
_log_data = []
def start_logging(arm):
global _logging_active, _log_data
_log_data = []
_logging_active = True
def _loop():
while _logging_active:
try:
angles = arm.get_angles() # ← シリアル競合で全部失敗
ts = datetime.now().strftime("%Y-%m-%d %H:%M:%S.%f")
_log_data.append([ts] + angles)
except:
pass # 無音で失敗し続ける
time.sleep(0.05)
t = threading.Thread(target=_loop, daemon=True)
t.start()結果:
✓ ログ保存: motion_log_20221013_144912.csv(0行)原因はpymycobotのシリアル通信とスレッドの競合だった。メインスレッドがsend_angles()でシリアルポートを占有している間、バックグラウンドのget_angles()は全部失敗して、except: pass で無音になっていた。
解決:メインスレッドでステップ後に1回取得
スレッドをやめて、各ステップのtime.sleep()の直後に同期的に1回だけ取得する方式に切り替えた。
_log_data = []
def log_angles(label):
try:
angles = mc.get_angles()
ts = datetime.now().strftime("%Y-%m-%d %H:%M:%S.%f")
_log_data.append([ts, label] + angles)
except Exception as e:
print(f" (ログ取得失敗: {e})")
def save_log(filepath):
with open(filepath, "w", newline="") as f:
writer = csv.writer(f)
writer.writerow(["timestamp","label","J1","J2","J3","J4","J5","J6"])
writer.writerows(_log_data)
print(f" ✓ ログ保存: {filepath} ({len(_log_data)}行)")各ステップの後にこう呼ぶ。
# Step3: APPROACH_PICK
mc.send_angles(APPROACH_PICK, SPEED)
time.sleep(3)
log_angles("APPROACH_PICK") # send_anglesが完了してから取得
# Step4: PICK
mc.send_angles(PICK, SPEED)
time.sleep(3)
log_angles("PICK")
# Step5: グリッパーを閉じる
mc.set_gripper_state(1, 70)
time.sleep(2)
log_angles("GRIPPER_CLOSE")最後にPiのホームディレクトリに保存して、ファイルパスを標準出力する。Mac側がこの行を正規表現で読む。
log_path = f"/home/ubuntu/motion_log_{datetime.now().strftime('%Y%m%d_%H%M%S')}.csv"
save_log(log_path)
print(f" LOG_FILE={log_path}") # r2r2r_loop.pyがこの行をキャッチするr2r2r_loop.py 側でSFTP取得する。
import re
# SSH実行後のoutputからログパスを正規表現で取得
match = re.search(r"LOG_FILE=(.+\.csv)", output)
if match:
remote_log = match.group(1).strip()
local_log = os.path.join(os.path.dirname(__file__), f"motion_log_{attempt}.csv")
try:
client = paramiko.SSHClient()
client.set_missing_host_key_policy(paramiko.AutoAddPolicy())
client.connect(SSH_HOST, username=SSH_USER, key_filename=SSH_KEY_PATH, timeout=10)
sftp = client.open_sftp()
sftp.get(remote_log, local_log) # Pi → Mac にダウンロード
sftp.close()
client.close()
print(f" ✓ モーションログ取得: {local_log}")
except Exception as e:
print(f" × ログ取得失敗: {e}")結果:
✓ ログ保存: /home/ubuntu/motion_log_20221013_145630.csv(11行)
✓ モーションログ取得: /Users/shimazueiichi/r2r2r-scripts/motion_log_1.csv11ステップ分、取れた。
CSVの中身が語ること
— 指令値と実測値が一致していない。これがデータだ。
MacにSFTPで自動転送されたCSVを全行確認した。
timestamp, label, J1, J2, J3, J4, J5, J6
2022-10-13 14:55:41.439277, HOME, 0.17, 1.58, 1.75, 0.26, -0.26, -30.23
2022-10-13 14:55:45.010662, GRIPPER_OPEN, 0.17, 1.58, 1.75, 0.26, -0.26, -30.23
2022-10-13 14:55:49.597778, APPROACH_PICK, -53.7, 83.05, -28.74, -8.34, -48.6, -29.53
2022-10-13 14:55:54.179388, PICK, -51.24, 90.7, -33.83, -4.3, -53.52, -29.7
2022-10-13 14:55:57.751868, GRIPPER_CLOSE, -51.15, 90.7, -33.83, -4.13, -54.22, -29.7
2022-10-13 14:56:02.314592, APPROACH_PICK_RETURN, -53.61, 84.81, -29.35, -7.29, -51.06, -29.7
2022-10-13 14:56:06.981955, APPROACH_PLACE, 59.67, 18.89, 28.47, 9.93, -45.7, -34.71
2022-10-13 14:56:11.540778, PLACE, 54.31, 46.14, 28.3, 3.42, -69.08, -29.79
2022-10-13 14:56:21.665881, GRIPPER_OPEN_PLACE, 59.5, 31.9, 28.3, 9.58, -45.79, -34.8
2022-10-13 14:56:26.238152, APPROACH_PLACE_RETURN, 59.5, 18.45, 28.3, 9.75, -45.52, -34.98
2022-10-13 14:56:30.772345, HOME_RETURN, 0.26, 1.58, 1.75, 0.7, -0.35, -30.76PICK姿勢で指令値と実測値を比較する。
関節 | 指令値 | 実測値 | ズレ |
|---|---|---|---|
J1 | -46.93° | -51.24° | 4.31° |
J2 | 102.74° | 90.7° | 12.04° |
J3 | -43.33° | -33.83° | 9.5° |
J4 | 3.6° | -4.3° | 7.9° |
J5 | -60.11° | -53.52° | 6.59° |
J6 | -31.9° | -29.7° | 2.2° |
J2で約12度、J3で約9.5度のズレがある。
これが「シミュレーションと現実の差」の正体だ。Unityで設計した指令値通りには動いていない。重力・イナーシャ・摩擦・モーター応答遅延、すべてが積み重なって、実機は指令値とは異なる位置で止まる。
このズレを計測して記録することが、Phase 2の本質だ。
Unityで実機の動きを再生した
— CSVをStreamingAssetsに置いて、MotionLogPlayerでステップを順番に再生する。
MotionLogPlayer.cs をUnityに追加した。
using System.Collections;
using System.Collections.Generic;
using System.IO;
using UnityEngine;
public class MotionLogPlayer : MonoBehaviour
{
[Header("CSV設定")]
public string csvFileName = "motion_log_1.csv";
[Header("関節(J1〜J6順に設定)")]
public ArticulationBody[] joints;
[Header("再生設定")]
public float stepInterval = 1.5f;
public bool playOnStart = false;
private List<float[]> _frames = new List<float[]>();
private List<string> _labels = new List<string>();
void Start()
{
LoadCSV();
if (playOnStart) StartCoroutine(PlayMotion());
}
void LoadCSV()
{
string path = Path.Combine(Application.streamingAssetsPath, csvFileName);
if (!File.Exists(path)) { Debug.LogError($"CSV not found: {path}"); return; }
string[] lines = File.ReadAllLines(path);
for (int i = 1; i < lines.Length; i++)
{
string[] cols = lines[i].Split(',');
if (cols.Length < 8) continue;
_labels.Add(cols[1]);
float[] angles = new float[6];
for (int j = 0; j < 6; j++)
float.TryParse(cols[j + 2], out angles[j]);
_frames.Add(angles);
}
Debug.Log($"MotionLogPlayer: {_frames.Count}フレーム読み込み完了");
}
[ContextMenu("Play Motion")]
public void Play() => StartCoroutine(PlayMotion());
IEnumerator PlayMotion()
{
for (int i = 0; i < _frames.Count; i++)
{
Debug.Log($"Step {i+1}: {_labels[i]}");
SetJointAngles(_frames[i]);
yield return new WaitForSeconds(stepInterval);
}
Debug.Log("再生完了");
}
void SetJointAngles(float[] angles)
{
for (int i = 0; i < joints.Length && i < angles.Length; i++)
{
if (joints[i] == null) continue;
var drive = joints[i].xDrive;
drive.target = angles[i]; // 実測値をdrive.targetにセット
joints[i].xDrive = drive;
}
}
}CSVをStreamingAssetsにコピーしてUnityに渡す。
mkdir -p "/Users/shimazueiichi/Robotics_UX/Robotics_UX_New/Assets/StreamingAssets"
cp ~/r2r2r-scripts/motion_log_1.csv \
"/Users/shimazueiichi/Robotics_UX/Robotics_UX_New/Assets/StreamingAssets/motion_log_1.csv"mecharm270mpi_0_0にアタッチ。Joints配列にlink1〜link6をセット。既存のTrajectoryFollowerのチェックを外してInspector右クリック → Play Motionを実行した。
アームが動いた。
実機のmechArmが今日描いた軌跡を、Unityのロボットがなぞった。
Real → Renderが繋がった
今までのR2R2Rのフローはこうだった。
Render(Unity設計)→ Real(mechArm実行)これから、逆方向が動く。
Real(mechArm実測)→ CSV → Render(Unity再生)これは単なるデータの記録ではない。「現実で何が起きたか」をシミュレーション空間で再現できる、という意味だ。
指令値と実測値のズレが可視化されると、次の問いが立つ。Unityのパラメータを実測値に合わせて補正したら、次の実行はもっと精度が上がるか。補正したUnityで再設計した指令値で実機を動かしたら、ズレは縮まるか。
このループがR2R2Rの本体だ。
そのループが初めて両方向で動いた。
