TM Robot × Isaac Sim:Modbus SLAVE ポーリングで Pick & Place を外部制御する
はじめに
以前の記事では、Isaac Sim 内に Modbus TCP サーバー(スレーブ)を立て、TMFlow(マスター)からグリッパーを制御する構成を紹介しました。
今回はその逆のアーキテクチャです。TM Robot 自体を Modbus スレーブとして動作させ、外部の Python スクリプトがマスターとしてレジスタをポーリングしながら Pick & Place シーケンス全体を制御します。
このアプローチが有効なケース:
- ビジョンシステムや上位 MES から動作指令を直接投げたい
- TMFlow のフローとは別のロジック(Python での条件分岐・ループ)で動かしたい
- 複数ロボットを 1 つのマスタープロセスで調整したい
- シミュレーションと実機で同じ制御コードを使いたい
注意: 本記事のコードは概念実証(PoC)用の簡略版です。実運用では適切なエラーハンドリング・タイムアウト処理・ロギングを追加してください。
アーキテクチャ比較
| 前回記事 | 今回の記事 | |
|---|---|---|
| マスター(制御側) | TMFlow | 外部 Python スクリプト |
| スレーブ(被制御側) | Isaac Sim(グリッパー Modbus サーバー) | TM Robot(または Isaac Sim がスレーブを模倣) |
| シーケンス記述場所 | TMFlow フロー | Python ステートマシン |
| 向いているケース | ロボットプログラマーが主導 | 上位システム(MES・Python 制御器)が主導 |
システム構成
┌─────────────────────────────────┐
│ Python マスタースクリプト │
│ (Pick & Place ステートマシン) │
│ │
│ 1. 位置レジスタ書き込み │
│ 2. 動作コマンド送信 │
│ 3. 完了フラグをポーリング │
│ 4. 次ステップへ遷移 │
└──────────────┬──────────────────┘
│ Modbus TCP
│ (Port 502 / 5020)
▼
┌─────────────────────────────────┐
│ TM Robot (Modbus TCP スレーブ) │
│ または │
│ Isaac Sim スレーブシミュレーター │
│ │
│ - 状態レジスタ(ロボット状態) │
│ - 位置指令レジスタ │
│ - グリッパー制御レジスタ │
└─────────────────────────────────┘
TMFlow の Modbus スレーブ設定
TM Robot を Modbus スレーブとして動作させるには、TMFlow で Listen ノードを使います。
基本的なフロー構成
[Start]
↓
[Listen Node] ← ここで外部 Modbus 接続を待ち受ける
↓
(外部マスターからの指令を受けてロボットが動作)
↓
[End]
Listen ノードが有効な間、TM Robot は設定ポートで Modbus TCP 接続を受け付けます。外部マスターはレジスタへの書き込みでロボットを制御し、読み出しで状態を確認します。
主要なレジスタマップ(TM Robot Modbus スレーブ)
TM Robot の Modbus スレーブが公開する主要レジスタ群(詳細は TM Robot ユーザーマニュアルの「External Signal」章を参照):
ロボット状態(読み取り専用)
| アドレス(例) | 名称 | 値の意味 |
|---|---|---|
| 0x0000 | Robot Mode | 0=初期化中 / 5=自動運転 / 7=エラー |
| 0x0001 | Motion Status | 0=待機 / 1=動作中 |
| 0x0002 | Error Code | エラー番号(0=正常) |
TCP 現在位置(読み取り専用)
| アドレス(例) | 名称 | 単位 |
|---|---|---|
| 0x0010–0x0015 | TCP X/Y/Z/Rx/Ry/Rz | mm / deg(×100 で整数化) |
位置指令(書き込み)
| アドレス(例) | 名称 | 説明 |
|---|---|---|
| 0x0200–0x0205 | Target X/Y/Z/Rx/Ry/Rz | 目標 TCP 座標 |
| 0x0206 | Speed | 動作速度(%) |
| 0x0207 | Command | 0=なし / 1=PTP 動作 / 2=Line 動作 |
| 0x0210 | Gripper Command | 0=開く / 1=閉じる |
実装時の注意: 実際のアドレスは TM Robot のファームウェアバージョンと設定によって異なります。必ず使用機器のマニュアル(「TM Robot Expression Editor and Listen Node Reference Guide」)を参照してください。
Python マスター:ポーリングループの実装
ステートマシン設計
Pick & Place の状態遷移:
IDLE
→ MOVE_TO_APPROACH (ピック位置の上方へ移動)
→ MOVE_TO_PICK (ピック位置へ降下)
→ GRIPPER_CLOSE (グリッパーを閉じる)
→ CHECK_GRASP (把持確認:把持成功?)
→ MOVE_TO_LIFT (持ち上げ)
→ MOVE_TO_PLACE (プレース位置へ移動)
→ GRIPPER_OPEN (グリッパーを開く)
→ MOVE_TO_HOME (ホームへ戻る)
→ DONE / ERROR
基本実装
import time
from pymodbus.client import ModbusTcpClient
from enum import Enum, auto
class State(Enum):
IDLE = auto()
MOVE_TO_APPROACH = auto()
MOVE_TO_PICK = auto()
GRIPPER_CLOSE = auto()
CHECK_GRASP = auto()
MOVE_TO_LIFT = auto()
MOVE_TO_PLACE = auto()
GRIPPER_OPEN = auto()
MOVE_TO_HOME = auto()
DONE = auto()
ERROR = auto()
# ---- レジスタアドレス(環境に合わせて変更) ----
REG_MOTION_STATUS = 0x0001 # 0=待機, 1=動作中
REG_GRASP_STATUS = 0x0002 # 0=未把持, 1=把持
REG_TARGET_X = 0x0200
REG_TARGET_Y = 0x0201
REG_TARGET_Z = 0x0202
REG_TARGET_RX = 0x0203
REG_TARGET_RY = 0x0204
REG_TARGET_RZ = 0x0205
REG_SPEED = 0x0206
REG_COMMAND = 0x0207
REG_GRIPPER_CMD = 0x0210 # 0=開く, 1=閉じる
CMD_EXECUTE_PTP = 1
CMD_EXECUTE_LINE = 2
# ---- 作業座標(mm、deg) ----
APPROACH_POS = [300, -200, 200, 180, 0, 90] # ピック位置の上方
PICK_POS = [300, -200, 100, 180, 0, 90] # ピック位置
LIFT_POS = [300, -200, 250, 180, 0, 90] # 持ち上げ後
PLACE_POS = [300, 200, 200, 180, 0, 90] # プレース位置
HOME_POS = [ 0, 0, 300, 180, 0, 0] # ホーム位置
SPEED_NORMAL = 30 # %
SPEED_SLOW = 10 # %(ピック・プレース接近時)
POLL_INTERVAL = 0.05 # 50ms
MOTION_TIMEOUT = 30 # 秒
class PickAndPlaceController:
def __init__(self, host: str, port: int = 502):
self.client = ModbusTcpClient(host, port=port)
self.state = State.IDLE
def connect(self) -> bool:
return self.client.connect()
def disconnect(self):
self.client.close()
# ---- レジスタ操作 ----
def read_register(self, address: int) -> int:
result = self.client.read_holding_registers(address, count=1)
if result.isError():
raise IOError(f"Register read error: {address:#x}")
return result.registers[0]
def write_register(self, address: int, value: int):
result = self.client.write_register(address, value)
if result.isError():
raise IOError(f"Register write error: {address:#x}")
# ---- ポーリング:動作完了を待つ ----
def wait_motion_complete(self, timeout: float = MOTION_TIMEOUT) -> bool:
deadline = time.time() + timeout
while time.time() < deadline:
status = self.read_register(REG_MOTION_STATUS)
if status == 0: # 待機状態 = 動作完了
return True
time.sleep(POLL_INTERVAL)
return False # タイムアウト
# ---- 位置移動コマンド ----
def move_to(self, pos: list, speed: int, motion_type: int = CMD_EXECUTE_PTP):
# 座標を整数レジスタ(×100)に変換して書き込む
addrs = [REG_TARGET_X, REG_TARGET_Y, REG_TARGET_Z,
REG_TARGET_RX, REG_TARGET_RY, REG_TARGET_RZ]
for addr, val in zip(addrs, pos):
self.write_register(addr, int(val * 100))
self.write_register(REG_SPEED, speed)
self.write_register(REG_COMMAND, motion_type)
# ---- グリッパー制御 ----
def gripper_open(self):
self.write_register(REG_GRIPPER_CMD, 0)
def gripper_close(self):
self.write_register(REG_GRIPPER_CMD, 1)
def check_grasp_success(self) -> bool:
return self.read_register(REG_GRASP_STATUS) == 1
# ---- メインループ ----
def run(self):
self.state = State.MOVE_TO_APPROACH
print("[START] Pick & Place 開始")
while self.state not in (State.DONE, State.ERROR):
try:
self._step()
except IOError as e:
print(f"[ERROR] 通信エラー: {e}")
self.state = State.ERROR
print(f"[{self.state.name}] 終了")
def _step(self):
s = self.state
if s == State.MOVE_TO_APPROACH:
print(" → アプローチ位置へ移動")
self.move_to(APPROACH_POS, SPEED_NORMAL)
if not self.wait_motion_complete():
self.state = State.ERROR; return
self.state = State.MOVE_TO_PICK
elif s == State.MOVE_TO_PICK:
print(" → ピック位置へ降下")
self.move_to(PICK_POS, SPEED_SLOW, CMD_EXECUTE_LINE)
if not self.wait_motion_complete():
self.state = State.ERROR; return
self.state = State.GRIPPER_CLOSE
elif s == State.GRIPPER_CLOSE:
print(" → グリッパーを閉じる")
self.gripper_close()
time.sleep(0.8) # グリッパー動作待ち
self.state = State.CHECK_GRASP
elif s == State.CHECK_GRASP:
if self.check_grasp_success():
print(" ✓ 把持成功")
self.state = State.MOVE_TO_LIFT
else:
print(" ✗ 把持失敗")
self.state = State.ERROR
elif s == State.MOVE_TO_LIFT:
print(" → 持ち上げ")
self.move_to(LIFT_POS, SPEED_SLOW, CMD_EXECUTE_LINE)
if not self.wait_motion_complete():
self.state = State.ERROR; return
self.state = State.MOVE_TO_PLACE
elif s == State.MOVE_TO_PLACE:
print(" → プレース位置へ移動")
self.move_to(PLACE_POS, SPEED_NORMAL)
if not self.wait_motion_complete():
self.state = State.ERROR; return
self.state = State.GRIPPER_OPEN
elif s == State.GRIPPER_OPEN:
print(" → グリッパーを開く")
self.gripper_open()
time.sleep(0.5)
self.state = State.MOVE_TO_HOME
elif s == State.MOVE_TO_HOME:
print(" → ホームへ戻る")
self.move_to(HOME_POS, SPEED_NORMAL)
if not self.wait_motion_complete():
self.state = State.ERROR; return
self.state = State.DONE
if __name__ == "__main__":
ctrl = PickAndPlaceController(host="192.168.1.100") # TM Robot の IP
if ctrl.connect():
ctrl.run()
ctrl.disconnect()
else:
print("接続失敗")
Isaac Sim でのスレーブシミュレーション
実機の前に Isaac Sim でロジックを検証できます。Isaac Sim 側に TM Robot スレーブを模倣する Modbus サーバーを立て、上記の Python マスターコードをそのまま接続します。
Isaac Sim スレーブシミュレーター(骨格)
# isaac_sim_slave_server.py
# Isaac Sim の Script Editor で実行する
import threading
import time
from pymodbus.server import StartTcpServer
from pymodbus.datastore import (
ModbusSequentialDataBlock,
ModbusSlaveContext,
ModbusServerContext,
)
# レジスタストア
store = ModbusSlaveContext(
hr=ModbusSequentialDataBlock(0, [0] * 0x300),
)
context = ModbusServerContext(slaves=store, single=True)
# ---- Isaac Sim 制御ループ(50Hz)----
def control_loop():
"""マスターが書き込んだコマンドを Isaac Sim のアームに反映する"""
while True:
command = store.getValues(3, 0x0207, 1)[0] # REG_COMMAND
if command in (1, 2): # PTP / Line 実行
# ① コマンドレジスタをクリア
store.setValues(3, 0x0207, [0])
# ② 目標座標を読み出す
vals = store.getValues(3, 0x0200, 6)
target = [v / 100.0 for v in vals]
# ③ Isaac Sim のアームを目標位置へ動かす
# (Articulation API で IK を解いて joint positions を設定)
store.setValues(3, 0x0001, [1]) # Motion Status = 動作中
_move_arm_to(target)
_wait_arm_settle()
store.setValues(3, 0x0001, [0]) # Motion Status = 完了
# グリッパー制御
gripper_cmd = store.getValues(3, 0x0210, 1)[0]
_control_gripper(gripper_cmd)
time.sleep(0.02) # 50Hz
def _move_arm_to(target_tcp):
"""Isaac Sim の IK でアームを動かす(実装略)"""
pass
def _control_gripper(cmd):
"""グリッパーの開閉(既存 Modbus サーバー記事の実装を流用可能)"""
pass
def _wait_arm_settle():
"""アームが目標に到達するまで待つ(簡略版)"""
time.sleep(1.5)
# Modbus サーバーをバックグラウンドで起動
control_thread = threading.Thread(target=control_loop, daemon=True)
control_thread.start()
StartTcpServer(context=context, address=("0.0.0.0", 5020))
Python マスターの接続先を TM Robot の IP から 127.0.0.1:5020 に変えるだけで、Isaac Sim 上でロジックをまるごと検証できます。
ポーリング方式の実装ポイント
ポーリング間隔の選択
| 間隔 | 特性 |
|---|---|
| 10ms 以下 | CPU 負荷が高い。小さな PoC 以外は避ける |
| 50ms(推奨) | 制御応答性と負荷のバランスが良い |
| 100ms 以上 | 軽量だが、短い動作の完了検出が遅れることがある |
タイムアウトの設計
動作タイムアウトは余裕を持って設定します。ロボット動作時間 + 停止時間 + 通信往復の合計より 20〜30% 長め が目安です。
# 例:最大動作距離 500mm・速度 30% の場合
# 移動時間の上限 ≒ 8 秒
# タイムアウト設定 ≒ 12 秒
MOTION_TIMEOUT = 12
前回記事(TMFlow マスター方式)との使い分け
| TMFlow マスター方式 | ポーリング(Python マスター)方式 | |
|---|---|---|
| シーケンス記述 | TMFlow のノード | Python コード |
| 条件分岐 | TMFlow の If ノード | Python の if 文(柔軟) |
| 上位システム連携 | 難しい | 容易(REST API・DB 接続なども可) |
| デバッグ | TMFlow ログ | Python デバッガ・ログ |
| 向き不向き | ロボット SE 主導の開発 | ソフトウェア SE 主導・上位統合 |
Isaac Sim でのロボット検証・PoC 構築をご検討の方へ
Modbus SLAVE 連携から、ビジョン統合・上位システム連携を含む Pick & Place PoC の設計・実装までをサポートしています。
4週間・固定価格のクイックスタートパッケージもご用意しています。
まとめ
- TM Robot を Modbus スレーブとして動作させると、外部 Python スクリプトが Pick & Place シーケンスを完全にコントロールできる
- ステートマシン + ポーリングループの組み合わせは、TMFlow のノードフローでは表現しにくい複雑な条件分岐や上位連携に向いている
- Isaac Sim でスレーブを模倣するサーバーを立てれば、同じマスターコードをシミュレーション→実機の順で検証できる
- ポーリング間隔 50ms・タイムアウトは移動時間の 130% を目安に設計する
関連記事
- Isaac Sim × Modbus TCP:OnRobot 2FG7グリッパーをTMFlowからリモート制御する — 今回と逆構成(Isaac Sim がスレーブ、TMFlow がマスター)の実装
- TM Robot と Isaac Sim を ROS2 で連携させる — ROS2 を使ったアーム制御の別アプローチ
- 既存の Isaac Sim 環境に Isaac Lab を追加する — Pick & Place を強化学習で自動化する次のステップ
- ルールベース自動化 vs Physical AI — どちらを選ぶべきか — ルールベース(今回の記事)と学習ベースの使い分け
- 【2025年12月版】NVIDIA Isaac Sim 環境構築(Ubuntu 24.04) — Isaac Sim 環境の基礎セットアップ
