マルチロボットHRIシステム開発全記録。Unity HUD × ROS 2 × Pythonで2台のmechArm 270を制御する
プロジェクト概要
UnityをGUI(顔)、ROS 2/Pythonをコントローラー(脳)として、2台のロボットの動作状態と「確信度(Confidence)」をリアルタイムに監視・操作するHUD(Head-Up Display)を構築。
※本プロジェクトの目的:実機「mechArm 270 M5」の制御を通じて、AIの判断をいかに物理的な動きへ変換するか。そのロボット制御の仕組みを理解するプロセスを記録・検証するものです。
開発プロセスと試行錯誤の軌跡
①:Unity UIの基本設計
内容: Robot 1/2 それぞれに、スライダー(確信度)、丸いランプ(ステータス)、テキスト(関節情報)、アクティブ枠(操作対象)を配置。
苦労した点: 階層構造(Hierarchy)が複雑になり、どのImageがどの役割を持つか整理するのに時間を要した。
②:ROS 2通信インフラの構築
内容:
ROS-TCP Endpointを起動し、PythonとUnity間のメッセージ通信路を確保。失敗と解決: ターミナルで
Package not foundエラーが発生。原因はワークスペース(unity_ws)の環境設定(source)忘れ。正しいディレクトリへ移動し、環境をロードすることで解決した。
③:Pythonコントローラーの実装と「保存」の罠
内容:
pynputを使ったキーボード監視と、Tabキーによる操作ロボットの切り替えロジックを実装。失敗と解決: Cursorで記述したコードが
Untitled-1のまま未保存だったり、誤って.ini拡張子で保存され、実行不能に。ファイルを~/unity_ws/scripts/multi_robot_controller.pyとして正しく保存し直すことで「脳」が動き出した。
④:双方向の信号同期
内容: Python側から「今どちらを操作しているか(1 or 2)」を
Int32型で送信。Unity側のHudManager.csでこれを受け取り、枠線の表示・非表示を切り替える機能を実装。失敗と解決: 当初、枠線が切り替わらず苦戦。Unity側の
Start()内に購読(Subscribe)コードが不足していたことが判明し、修正後に同期に成功。
⑤:デザインのブラッシュアップと透明度の戦い
内容: 青い「背景」を「枠線(Outline)」に変更し、SF風のHUDデザインへ。
失敗と解決: 背景を透明にしても青い色が消えない問題が発生。親オブジェクト(
robot1_info)にも背景画像が設定されていたことが原因。親子のAlphaを適切に操作し、洗練された「透過型枠線」を実現した。
最終版ソースコード
【Python】操作ロジックと状態送信
キー入力に応じて、操作対象の切り替え、確信度の計算(0.8:操作中 / 1.0:静止)、ロボットIDの送信を行います。
Python
import rclpy
from rclpy.node import Node
from std_msgs.msg import Int32, Float32
from sensor_msgs.msg import JointState
from pynput import keyboard
class MultiController(Node):
def __init__(self):
super().__init__('multi_controller')
self.active_id = 1
self.is_moving = False
# 配信窓口の設定
self.active_pub = self.create_publisher(Int32, 'active_robot_id', 10)
self.conf1_pub = self.create_publisher(Float32, 'robot1/confidence', 10)
self.conf2_pub = self.create_publisher(Float32, 'robot2/confidence', 10)
# 10Hzで状態を更新
self.create_timer(0.1, self.publish_status)
print("=== Multi-Robot HRI Controller Initialized ===")
def publish_status(self):
# 1. 操作対象IDの送信
self.active_pub.publish(Int32(data=self.active_id))
# 2. 確信度の計算(操作中は0.8[黄]、停止中は1.0[緑])
val = 0.8 if self.is_moving else 1.0
if self.active_id == 1:
self.conf1_pub.publish(Float32(data=val))
self.conf2_pub.publish(Float32(data=1.0))
else:
self.conf1_pub.publish(Float32(data=1.0))
self.conf2_pub.publish(Float32(data=val))
def on_press(self, key):
if key == keyboard.Key.tab:
self.active_id = 2 if self.active_id == 1 else 1
print(f"Active Robot: {self.active_id}")
elif hasattr(key, 'char') and key.char in '123456':
self.is_moving = True
def on_release(self, key):
self.is_moving = False
def main():
rclpy.init()
node = MultiController()
with keyboard.Listener(on_press=node.on_press, on_release=node.on_release) as listener:
rclpy.spin(node)
if __name__ == '__main__':
main()【Unity/C#】HUD管理スクリプト
Pythonからの信号を受け取り、UIの色・文字・枠線を瞬時に切り替えます。
C#
using UnityEngine;
using UnityEngine.UI;
using Unity.Robotics.ROSTCPConnector;
using RosMessageTypes.Std;
using TMPro;
public class HudManager : MonoBehaviour
{
[Header("Robot 1 UI")]
public TextMeshProUGUI r1TitleText;
public Image r1ActiveIndicator;
public Image r1StatusLamp;
public Slider r1ConfidenceGauge;
[Header("Robot 2 UI")]
public TextMeshProUGUI r2TitleText;
public Image r2ActiveIndicator;
public Image r2StatusLamp;
public Slider r2ConfidenceGauge;
void Start()
{
var ros = ROSConnection.GetOrCreateInstance();
ros.Subscribe<Float32Msg>("robot1/confidence", msg => UpdateConfidence(msg.data, 1));
ros.Subscribe<Float32Msg>("robot2/confidence", msg => UpdateConfidence(msg.data, 2));
ros.Subscribe<Int32Msg>("active_robot_id", msg => SetActiveRobot(msg.data));
}
private void UpdateConfidence(float value, int robotId)
{
Slider targetSlider = (robotId == 1) ? r1ConfidenceGauge : r2ConfidenceGauge;
Image targetLamp = (robotId == 1) ? r1StatusLamp : r2StatusLamp;
if (targetSlider != null) targetSlider.value = value;
if (targetLamp != null) targetLamp.color = GetHriColor(value);
}
private Color GetHriColor(float value)
{
if (value >= 0.9f) return Color.green;
if (value >= 0.6f) return Color.yellow;
return Color.red;
}
public void SetActiveRobot(int robotId)
{
// 枠線の制御
if (r1ActiveIndicator != null) r1ActiveIndicator.enabled = (robotId == 1);
if (r2ActiveIndicator != null) r2ActiveIndicator.enabled = (robotId == 2);
// ラベルの更新
if (r1TitleText != null) {
r1TitleText.text = (robotId == 1) ? "ROBOT 1 [ACTIVE]" : "ROBOT 1 [IDLE]";
r1TitleText.color = (robotId == 1) ? Color.cyan : Color.gray;
}
if (r2TitleText != null) {
r2TitleText.text = (robotId == 2) ? "ROBOT 2 [ACTIVE]" : "ROBOT 2 [IDLE]";
r2TitleText.color = (robotId == 2) ? Color.cyan : Color.gray;
}
}
}プロジェクトの成果
直感的なUI: 操作対象を「枠線と色」で瞬時に理解できるようになった。
動的なフィードバック: キー入力という物理的な操作が、HUD上の「確信度」という心理的な情報に変換され、ランプの色で表現される仕組みを構築した。
堅牢な通信: エラーへの対処法を確立し、安定して2台を制御できるようになった。
フェーズ3 完遂の結び
ロボティクスデザイナーとしての「架け橋」の構築
マルチロボット制御システムの基礎構築を終え、フェーズ3を無事にクローズします。このフェーズは、決して平坦な道のりではありませんでした。
通信の壁: 「Connection refused」という赤い文字を前に、ROS-TCP Endpointの重要性と、環境構築(source)の繊細さを学びました。
実装の壁: Cursorでのファイル保存ミスや、拡張子の取り違えといった「開発現場のリアルな洗礼」を受けながらも、一つずつ「住所(パス)」を特定し、プログラムを確実に動かす忍耐力を身につけました。
デザインの壁: 単なる塗りつぶしのパネルを、Outlineコンポーネントや透明度(Alpha)の調整によって、SFのような透過型HUDへと昇華させ、情報の優先度を視覚的に整理する術を習得しました。
最終的に完成したこのシステムは、単に2台のロボットを動かすためのツールに留まりません。
オペレーターが「Tabキー」ひとつで意思を伝え、それに応えてUIが鮮やかな水色に輝き、ロボットの「迷い(確信度)」が直感的な色としてフィードバックされる。そこにあるのは、一方的な命令ではなく、人間と機械が互いの状態を理解し合う「対話のデザイン」——すなわちHRI(Human-Robot Interaction)の核心そのものでした。
ロボティクスの世界は深淵です。しかし、このフェーズを通じて習得した「UnityとROS 2を自在に繋ぐ力」という架け橋は、もはや単なる技術的な知識ではありません。それは、バーチャルな思考をフィジカルな動力へと変換し、現実の世界をより良く、より確かなものへと書き換えていくための「変革の力」です。
基礎を固めた今、視線はすでにその先の未来を捉えています(実機「mechArm 270 M5」の制御はロードマップでのフェーズ5になりますので少し先です)確かな手応えを糧に現実を動かす次なるプロジェクトへと踏み出します。
