2026年8月10日
Ken Suzuki
技術

TM Robot × Isaac Sim:Modbus SLAVE ポーリングで Pick & Place を外部制御する

TM Robot を Modbus TCP スレーブとして動作させ、Python マスタースクリプトがレジスタをポーリングしながら Pick & Place シーケンスを制御する実装パターンを解説。Isaac Sim でシミュレーション検証してから実機に展開するまでの流れをまとめます。

Isaac SimModbus TCPTM RobotTMFlowPythonPick & Placeロボット制御
TM Robot × Isaac Sim:Modbus SLAVE ポーリングで Pick & Place を外部制御する

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% を目安に設計する

関連記事

TM Robot × Isaac Sim:Modbus SLAVE ポーリングで Pick & Place を外部制御する | Shirokuma.online