August 10, 2026
Ken Suzuki
Technology

TM Robot × Isaac Sim: Externally Controlling Pick & Place via Modbus SLAVE Polling

A walkthrough of an implementation pattern where the TM Robot runs as a Modbus TCP slave and a Python master script polls its registers to drive a full Pick & Place sequence — from validating the logic in Isaac Sim through to deploying it on the real robot.

Isaac SimModbus TCPTM RobotTMFlowPythonPick & PlaceRobot Control
TM Robot × Isaac Sim: Externally Controlling Pick & Place via Modbus SLAVE Polling

TM Robot × Isaac Sim: Externally Controlling Pick & Place via Modbus SLAVE Polling

Introduction

In a previous article, we set up a Modbus TCP server (slave) inside Isaac Sim and controlled a gripper from TMFlow (the master).

This time we flip the architecture around. The TM Robot itself runs as a Modbus slave, and an external Python script acts as the master, polling registers to drive the entire Pick & Place sequence.

This approach is useful when:

  • You want a vision system or a higher-level MES to issue motion commands directly
  • You want to drive the robot with logic (conditional branching, loops) written in Python rather than a TMFlow flow
  • You want to coordinate multiple robots from a single master process
  • You want to use the same control code in both simulation and on the real hardware

Note: The code in this article is a simplified proof-of-concept (PoC) version. For production use, please add proper error handling, timeout handling, and logging.


Architecture Comparison

Previous Article This Article
Master (controller) TMFlow External Python script
Slave (controlled device) Isaac Sim (gripper Modbus server) TM Robot (or Isaac Sim emulating the slave)
Where the sequence lives TMFlow flow Python state machine
Best suited for Robot programmer-led development Higher-level system (MES / Python controller)-led development

System Overview

┌─────────────────────────────────┐
│  Python master script            │
│  (Pick & Place state machine)    │
│                                 │
│  1. Write position registers     │
│  2. Send motion command           │
│  3. Poll completion flag          │
│  4. Transition to next step       │
└──────────────┬──────────────────┘
               │ Modbus TCP
               │ (Port 502 / 5020)
               ▼
┌─────────────────────────────────┐
│  TM Robot (Modbus TCP slave)     │
│  or                              │
│  Isaac Sim slave simulator        │
│                                 │
│  - Status registers (robot state) │
│  - Target position registers      │
│  - Gripper control registers       │
└─────────────────────────────────┘

Configuring the Modbus Slave in TMFlow

To make the TM Robot behave as a Modbus slave, you use a Listen Node in TMFlow.

Basic Flow Structure

[Start]
  ↓
[Listen Node]  ← waits for an external Modbus connection here
  ↓
(the robot moves based on commands from the external master)
  ↓
[End]

While the Listen Node is active, the TM Robot accepts Modbus TCP connections on the configured port. The external master controls the robot by writing to registers and confirms its state by reading them.

Key Register Map (TM Robot Modbus Slave)

The main register groups exposed by the TM Robot's Modbus slave (see the "External Signal" chapter of the TM Robot user manual for full details):

Robot Status (Read-Only)

Address (example) Name Meaning
0x0000 Robot Mode 0=Initializing / 5=Auto Run / 7=Error
0x0001 Motion Status 0=Idle / 1=Moving
0x0002 Error Code Error number (0=normal)

TCP Current Position (Read-Only)

Address (example) Name Unit
0x0010–0x0015 TCP X/Y/Z/Rx/Ry/Rz mm / deg (scaled by ×100 to an integer)

Position Command (Write)

Address (example) Name Description
0x0200–0x0205 Target X/Y/Z/Rx/Ry/Rz Target TCP coordinates
0x0206 Speed Motion speed (%)
0x0207 Command 0=None / 1=PTP motion / 2=Line motion
0x0210 Gripper Command 0=Open / 1=Close

Implementation note: The actual addresses vary depending on the TM Robot's firmware version and configuration. Always check the manual for your specific unit (the "TM Robot Expression Editor and Listen Node Reference Guide").


Python Master: Implementing the Polling Loop

State Machine Design

The Pick & Place state transitions:

IDLE
  → MOVE_TO_APPROACH    (move above the pick position)
  → MOVE_TO_PICK        (descend to the pick position)
  → GRIPPER_CLOSE       (close the gripper)
  → CHECK_GRASP         (verify the grasp succeeded)
  → MOVE_TO_LIFT        (lift the object)
  → MOVE_TO_PLACE       (move to the place position)
  → GRIPPER_OPEN        (open the gripper)
  → MOVE_TO_HOME        (return home)
  → DONE / ERROR

Basic Implementation

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()

# ---- Register addresses (adjust to your environment) ----
REG_MOTION_STATUS   = 0x0001  # 0=idle, 1=moving
REG_GRASP_STATUS    = 0x0002  # 0=not grasped, 1=grasped
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=open, 1=close

CMD_EXECUTE_PTP  = 1
CMD_EXECUTE_LINE = 2

# ---- Task coordinates (mm, deg) ----
APPROACH_POS = [300, -200, 200, 180, 0, 90]  # above the pick position
PICK_POS     = [300, -200, 100, 180, 0, 90]  # pick position
LIFT_POS     = [300, -200, 250, 180, 0, 90]  # after lifting
PLACE_POS    = [300,  200, 200, 180, 0, 90]  # place position
HOME_POS     = [  0,    0, 300, 180, 0,  0]  # home position
SPEED_NORMAL = 30   # %
SPEED_SLOW   = 10   # % (for approaching pick/place)
POLL_INTERVAL = 0.05  # 50ms
MOTION_TIMEOUT = 30   # seconds


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()

    # ---- Register access ----
    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}")

    # ---- Polling: wait for motion completion ----
    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:  # idle = motion complete
                return True
            time.sleep(POLL_INTERVAL)
        return False  # timeout

    # ---- Motion command ----
    def move_to(self, pos: list, speed: int, motion_type: int = CMD_EXECUTE_PTP):
        # Convert coordinates to integer registers (×100) and write them
        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)

    # ---- Gripper control ----
    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

    # ---- Main loop ----
    def run(self):
        self.state = State.MOVE_TO_APPROACH
        print("[START] Starting Pick & Place")

        while self.state not in (State.DONE, State.ERROR):
            try:
                self._step()
            except IOError as e:
                print(f"[ERROR] Communication error: {e}")
                self.state = State.ERROR

        print(f"[{self.state.name}] Finished")

    def _step(self):
        s = self.state

        if s == State.MOVE_TO_APPROACH:
            print("  → Moving to approach position")
            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("  → Descending to pick position")
            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("  → Closing gripper")
            self.gripper_close()
            time.sleep(0.8)  # wait for gripper motion
            self.state = State.CHECK_GRASP

        elif s == State.CHECK_GRASP:
            if self.check_grasp_success():
                print("  ✓ Grasp successful")
                self.state = State.MOVE_TO_LIFT
            else:
                print("  ✗ Grasp failed")
                self.state = State.ERROR

        elif s == State.MOVE_TO_LIFT:
            print("  → Lifting")
            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("  → Moving to place position")
            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("  → Opening gripper")
            self.gripper_open()
            time.sleep(0.5)
            self.state = State.MOVE_TO_HOME

        elif s == State.MOVE_TO_HOME:
            print("  → Returning home")
            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's IP
    if ctrl.connect():
        ctrl.run()
        ctrl.disconnect()
    else:
        print("Connection failed")

Simulating the Slave in Isaac Sim

You can validate the logic in Isaac Sim before touching the real robot. Set up a Modbus server inside Isaac Sim that emulates the TM Robot slave, and connect the Python master code above to it as-is.

Isaac Sim Slave Simulator (Skeleton)

# isaac_sim_slave_server.py
# Run this from Isaac Sim's Script Editor

import threading
import time
from pymodbus.server import StartTcpServer
from pymodbus.datastore import (
    ModbusSequentialDataBlock,
    ModbusSlaveContext,
    ModbusServerContext,
)

# Register store
store = ModbusSlaveContext(
    hr=ModbusSequentialDataBlock(0, [0] * 0x300),
)
context = ModbusServerContext(slaves=store, single=True)

# ---- Isaac Sim control loop (50Hz) ----
def control_loop():
    """Apply the commands written by the master to the Isaac Sim arm"""
    while True:
        command = store.getValues(3, 0x0207, 1)[0]  # REG_COMMAND
        if command in (1, 2):  # execute PTP / Line
            # ① Clear the command register
            store.setValues(3, 0x0207, [0])
            # ② Read the target coordinates
            vals = store.getValues(3, 0x0200, 6)
            target = [v / 100.0 for v in vals]
            # ③ Move the Isaac Sim arm to the target position
            #    (solve IK via the Articulation API and set joint positions)
            store.setValues(3, 0x0001, [1])   # Motion Status = moving
            _move_arm_to(target)
            _wait_arm_settle()
            store.setValues(3, 0x0001, [0])   # Motion Status = complete

        # Gripper control
        gripper_cmd = store.getValues(3, 0x0210, 1)[0]
        _control_gripper(gripper_cmd)

        time.sleep(0.02)  # 50Hz

def _move_arm_to(target_tcp):
    """Move the arm using Isaac Sim's IK (implementation omitted)"""
    pass

def _control_gripper(cmd):
    """Open/close the gripper (can reuse the implementation from the existing Modbus server article)"""
    pass

def _wait_arm_settle():
    """Wait for the arm to reach the target (simplified)"""
    time.sleep(1.5)

# Start the Modbus server in the background
control_thread = threading.Thread(target=control_loop, daemon=True)
control_thread.start()

StartTcpServer(context=context, address=("0.0.0.0", 5020))

Just change the Python master's connection target from the TM Robot's IP to 127.0.0.1:5020 and you can validate the entire logic inside Isaac Sim.


Implementation Notes for the Polling Approach

Choosing the Polling Interval

Interval Characteristics
Under 10ms High CPU load. Avoid outside small PoCs
50ms (recommended) Good balance between control responsiveness and load
Over 100ms Lightweight, but can delay detecting completion of short motions

Designing Timeouts

Set motion timeouts with a comfortable margin. A good rule of thumb is 20–30% longer than the sum of robot motion time, dwell time, and round-trip communication time.

# Example: 500mm max travel distance at 30% speed
# Estimated motion time upper bound ≈ 8 seconds
# Timeout setting ≈ 12 seconds
MOTION_TIMEOUT = 12

When to Use This vs. the Previous Article's TMFlow-Master Approach

TMFlow Master Approach Polling (Python Master) Approach
Sequence definition TMFlow nodes Python code
Conditional branching TMFlow If node Python if statements (flexible)
Higher-level system integration Difficult Easy (REST APIs, DB connections, etc.)
Debugging TMFlow logs Python debugger / logging
Best fit Robot SE-led development Software SE-led development, higher-level integration

Planning a Robot Validation or PoC with Isaac Sim?

We support everything from Modbus SLAVE integration to Pick & Place PoC design and implementation, including vision integration and higher-level system connectivity.

A 4-week, fixed-price Quick Start Package is also available.

Learn more about our Robotics Simulation Service →


Summary

  • Running the TM Robot as a Modbus slave lets an external Python script take full control of the Pick & Place sequence
  • The state machine + polling loop combination is well suited to complex conditional branching and higher-level system integration that's hard to express with TMFlow's node-based flows
  • By emulating the slave with a server in Isaac Sim, you can validate the same master code first in simulation, then on the real robot
  • Design around a 50ms polling interval and a timeout of roughly 130% of the expected motion time

TM Robot × Isaac Sim: Externally Controlling Pick & Place via Modbus SLAVE Polling | Shirokuma.online