Previous 06. MODBUS SERVER, the robot was in a controlled position (Slave Server). This episode reverses the position. The robot becomes the Master Client and actively controls the electric gripper, other robots and PLC. With the MBC_* command in the HIWIN robot language, one robot connects to up to 4 servers simultaneously and reads and writes 4 types of data (Discrete Input / Coil / Input Register / Holding Register). Reproduce the entire process on one PC without the actual gripper, with _shared/gripper_sim.py (gripper = Server) and RobotMaster (robot = Client) from _shared/modbus_master.py.
Learning Objectives
Explains how the robot actively controls the gripper, PLC, and other robots in the Robot = Master Client structure.
Knows the structure in which one robot connects to up to 4 servers simultaneously and the meaning of connection numbers (1 to 4).
Write the arguments of connection commands such as MBC_RTU_OPEN/MBC_TCP_OPEN and data commands of MBC_READ_*/MBC_WRITE_* separately.
Explains the difference between Bit command (Discrete Input / Coil) and Word command (Input / Holding Register, 'W'/'D' type).
Know why you needSWAP_WORD/IEEE754_ENCODE/IEEE754_DECODE (Modbus cannot directly transmit real numbers, byte order differences for each device) and convert them directly.
Robot (Master/Client) sends the request first. Write “Open” to the gripper’s Holding Register and read the sensor value from the PLC’s Input Register.
The other device (Slave/Server) only responds with the requested value and does not speak first.
This is the exact opposite of Episode 06. Part 06: Robot=Server (Passive) / Part 07: Robot=Client (Active). Even for the same robot, the communication role varies depending on the settings.
2) One robot = simultaneous connection of up to 4 servers
One robot can control the gripper (connection 1), PLC (connection 2), vision (connection 3), and another robot (connection 4) simultaneously. Each connection is identified by connection number 1~4, and this number is the first argument of all MBC_* data commands.
Robot ──(Connection 1)── Electric Gripper (RTU)
──(Connection 2)── PLC (TCP)
──(Connection 3)── Vision System (TCP)
──(Connection 4)── Other Robot B (TCP)
(connection number, start address, data variable, length 1~48)
MBC_READ_COIL
Read Coil
(connection number, start address, data variable, length 1~48)
Write Bit
MBC_WRITE_COIL
Coil writing
(connection number, start address, data variable, length 1~48)
Read Word
MBC_READ_INPUT
Read Input Register
(connection number, starting address, data variable, type 'W'/'D', length)
MBC_READ_HOLDING
Read Holding Register
(connection number, starting address, data variable, type 'W'/'D', length)
Write Word
MBC_WRITE_HOLDING
Write Holding Register
(connection number, starting address, data variable, type 'W'/'D', length)
Starting address is 0-base (starting from 1 in HRSS/Caterpillar screens). Data commands will not work unless you call the Connect command first.
Bit command vs Word command — The key is the data type argument that only exists in the Word command.
Category
target area
data unit
length range
Bit
Discrete Input(R), Coil(R/W)
0 or 1
1~48
Word
Input Register(R), Holding Register(R/W)
16/32bit integer
'W' Read 1125·Write 1123 / 'D' Read 162·Write 161
'W' (Word) — One 16-bit register remains. Small values like model number, speed %.
'D' (Double Word) — Two consecutive registers combined into 32 bits. Values exceeding 16 bits, such as coordinates and angles. Save order is Low word first, High word last.
4) HRSS MBC_* ↔ RobotMaster correspondence table
RobotMaster takes over the role of robot (client) on the PC. One line of MBC_* in the actual equipment is one line of the method below.
HRSS robot command (actual equipment)
RobotMaster Method (💻 PC)
MBC_TCP_OPEN(1,"127.0.0.1",502)
with RobotMaster('127.0.0.1', 1503) as m:
MBC_TCP_CLOSE(1)
with Automatic at end of block close()
MBC_WRITE_HOLDING(1, addr, var, 'W', n)
m.write_holding(addr, [..])
MBC_READ_INPUT(1, addr, var, 'W', n)
m.read_input(addr, n)
MBC_READ_HOLDING(1, addr, var, 'W', n)
m.read_holding(addr, n)
MBC_WRITE_COIL(1, addr, var, n)
m.write_coil(addr, v) / m.write_coils(addr, [..])
MBC_READ_DINPUT(1, addr, var, n)
m.read_discrete(addr, n)
MBC_READ_COIL(1, addr, var, n)
m.read_coils(addr, n)
SWAP_WORD(v)
word_tools.swap_word(v)
IEEE754_ENCODE(f)
word_tools.ieee754_encode(f)
IEEE754_DECODE(i)
word_tools.ieee754_decode(i)
💻 The actual equipment connects the gripper to RS485 (RTU, port 502), but on the PC, since there is no cable, it imitates TCP + random port (1503). Data model/register address is the same as RTU.
5) Why is the conversion command needed?
Modbus registers only carry 16-bit integers. Neither real numbers (floating point numbers) nor integers exceeding 16 bits can be sent directly. Additionally, the byte order (High/Low word order) is different for each device. So:
command
Role
SWAP_WORD
Correct the byte order of the other device by exchanging the upper and lower word positions of the 32-bit value
IEEE754_ENCODE
Since you cannot send a real number (10.5), convert it to real number → 32bit integer (IEEE754 single precision) and send it
IEEE754_DECODE
Restore the received integer back to 32bit integer → real number
Verification values: ieee754_encode(10.5)=1093140480, ieee754_decode(1093140480)=10.5, swap_word(10)=655360. Integer coordinates exceeding 16 bits are processed as 'D' type (Part 06 split_word), and real numbers are processed as IEEE754_*. For more information, see Appendix B (32bit·IEEE754 conversion).
6) Gripper register map (Manual Table 55, XEG-64)
area
Address
Meaning
value
Holding Register (R/W)
1536
Gripper model
2624 = XEG-64
1600
direction
0=Close / 1=Open
1601
mobile administration
Unit 0.01 mm (50.00mm = 5000)
1602
Movement speed
Unit 0.01 mm/s
1606
Confirmation of execution
1 When written, it operates → After completion, 0 automatically returns
⚠️ Completion Judgment Trap: Just look at IR[769]==2(Pos) and the status of the previous action will latch and pass immediately. Be sure to check togetherIR[769]==2andHR[1606]==0 (the run flag automatically resets to 0).
7) Overview of 3 application examples (Manual 5.4.5)
Example
Target (Slave)
connection
What robots do
① Gripper
XEG-64 Electric Gripper
RTU
Write direction, administration, and execution with the Holding Register, and read the status and location with the Input Register. Open/Close — Practice in this section
② Other robots
Robot B
TCP
Robot A commands the speed·DO·PTP of robot B (Part 06 register map in opposite direction)
③ Read PLC
Mitsubishi FX5U
TCP
The robot reads and writes the PLC's M(Coil/DInput)·D(I/H Register) device
Acting as a robot (Master Client), it checks the gripper model and opens and closes 50.00mm. This is the flow of example ① in manual 5.4.5.1. (see file for full code)
from modbus_master import RobotMaster
def run_gripper(m, direction, stroke_001mm=5000, speed_001mm=5000):
m.write_holding(1600, direction) #① Direction (0=close / 1=open)
m.write_holding(1601, stroke_001mm) #② Movement stroke (0.01mm)
m.write_holding(1602, speed_001mm) #③ Movement speed
m.write_holding(1606, 1) #④ Execution trigger
#⑤ Check IR769==2 and HR1606==0 together (avoid latch trap)
while not (m.read_input(769, 1)[0] == 2 and m.read_holding(1606, 1)[0] == 0):
time.sleep(0.05)
return m.read_input(769, 1)[0], m.read_input(770, 1)[0]
with RobotMaster(args.host, args.port) as m: # MBC_TCP_OPEN(1,"...",502)
model = m.read_holding(1536, 1)[0] # MBC_READ_HOLDING ... 'W',1 -> 2624
run_gripper(m, 1, 5000) #Open 50.00mm
run_gripper(m, 0, 5000) #close
View Run/verify
Terminal A — Launch Gripper Server
Leave this window on.
cd lecture/_shared
set PYTHONUTF8=1
python gripper_sim.py --port 1503 --host 127.0.0.1
============================================================
XEG Gripper Modbus SERVER Simulator 127.0.0.1:1503
Try controlling it using 1600/1601/1606 with modbus_master.py instead of the robot (Client).
Exit: Ctrl+C
============================================================
Starting gripper logic thread.
Terminal B — Robot (master) cycle execution
cd lecture/06_client/07_modbus_client
set PYTHONUTF8=1
python src/client_gripper/main.py --port 1503
Expected output:
=== Gripper Control Cycle (Robot = Master Client) ===
[①] Check model HR[1536]=2624 -> XEG-64
[②] Open command: HR[1600]=1, HR[1601]=5000, HR[1606]=1
[③] Open completed IR[769]=2(Pos), IR[770]=5000(= 50.00mm), HR[1606]=0(automatic reset)
[④] Close command: HR[1600]=0, HR[1606]=1
[⑤] Close completed IR[769]=2(Pos), IR[770]=0(= 0.00mm)
Gripper control cycle completed ✅
An operation log is recorded on terminal A (server).
▶ Open gripper (target 50.00 mm)
✔ Action completed: Status=2(Pos), Position=50.00 mm
▶ Close gripper (target 0.00 mm)
✔ Action completed: Status=2(Pos), Position=0.00 mm
✅ Checkpoint: Check HR[1536]=2624(XEG-64) → After opening, IR[769]=2(Pos), IR[770]=5000(= 50.00mm), HR[1606] automatically return to 0 → After closing (direction=0), IR[770]=0. The robot has completed one cycle, actively controlling the gripper in both directions and confirming completion by status polling.
Common Mistakes
Q. I checked completion with IR[769]==2, but it passes before the operation even starts.
A. It is a latch trap. After the previous action, IR[769]remains at 2 (Pos). It is 2 for a moment even after sending the next command, so if it only sees 2, it passes immediately. Look at HR[1606], which sets itself to 0 when the gripper operates, and judge it as complete only when IR[769]==2 and HR[1606]==0.
Q. I wrote the real coordinates (10.5) to the register and it is broken.
A. Modbus cannot send real numbers directly. Before sending, change IEEE754_ENCODE(10.5) to an integer (1093140480) and restore it to IEEE754_DECODE on the receiving end. Integer coordinates exceeding 16 bits are processed as 'D' type (or Part 06 split_word).
Q. The gripper does not move or moves to the wrong position.
A. The writing order is wrong. Direction (HR1600), stroke (HR1601), and speed (HR1602) are written first, and the execution trigger (HR1606=1) is written last. The value at the moment the trigger is raised is used.
Q. I have ConnectionError: Failed to connect to slave.
A. Gripper server not running or port mismatch. Check that gripper_sim.py --port 1503 is displayed in terminal A and that the --port parameters match. (Ports below 1024, such as 502, use 1503 due to permission issues.)
Cleanup
Robot = Master Client. Actively reads and writes four types of registers of gripper, PLC, and other robots. One robot connects to up to 4 servers simultaneously with connection numbers 1 to 4.
The connection is first opened with MBC_RTU_OPEN/MBC_TCP_OPEN, and the data is Bit command (READ_DINPUT/READ_COIL/WRITE_COIL) and Word command (READ_INPUT/READ_HOLDING/WRITE_HOLDING, 'W'/'D').
Gripper cycle: Direction/stroke writing → HR[1606]=1 trigger → IR[769]==2 and HR[1606]==0 polling (latch trap avoidance) → Position confirmation with IR[770].
Real numbers cannot be sent directly via Modbus: Send after IEEE754_ENCODE (real number → integer, 10.5→1093140480), restore as IEEE754_DECODE. If the byte order is reversed, SWAP_WORD(10→655360).
Try it yourself
Solve the assignment below. The reference solution is shown below.
📖 Manual Source
HIWIN Robot Communication User Manual (C25UE801-2211)
Ch.5.4.1 Functional Application Notes (p.81, Robot = Client, up to 4 Servers, 4 types of data + conversion)
Ch.5.4.2 Function Interface Setting (p.82~85, 4 types of RTU·TCP Client connection)
Ch.5.4.4 Data Processing (p.102~105, SWAP_WORD / IEEE754_ENCODE / IEEE754_DECODE)
Ch.5.4.5 Example Programs (p.106116, Example① XEG Gripper / Example② Other Robot / Example③ FX5U PLC, Table 5556)
32bit·IEEE754 conversion practice: Appendix B · MBC_* cheat sheet: Appendix A
Example files
The runnable files used in this lesson. Copy them into your own project and experiment.
_shared/gripper_sim.pyPython
"""gripper_sim.py — XEG electric gripper = Modbus SERVER(Slave) simulator
Manual 5.4.5.1 (p.106-111). The robot becomes a Modbus CLIENT (master) and controls the gripper.
In this situation, the PC takes over the ‘gripper’ role. Try controlling it with modbus_master.py instead of the robot.
Register map (XEG-64 standard, decimal address, zero_mode):
Holding Register (Write)
1536 gripper models (2624 = XEG-64)
1552 HOME Reset (Write 1 to reset)
1600 directions (0=close / 1=open)
1601 travel stroke [0.01mm]
1602 Movement speed [0.01mm/s]
1603 Grip stroke [0.01mm]
1604 grip speed [0.01mm/s]
1605 Grip force [%]
1606 Confirm/execute settings (write 1 to start operation → automatically return to 0 after completion)
Input Register (Read)
769 (0x301) Gripper status (0 Idle, 1 Busy, 2 Pos reached, 3 Hold, 4~7 Alarm)
770 Current position [0.01mm]
771 (0x303) Firmware version major
772 (0x304) Firmware version minor
Run: python gripper_sim.py # 0.0.0.0:502 (separate port when running simultaneously with robot server)
python gripper_sim.py --port 1503
Exit: Ctrl+C"""
import argparse
import sys
import threading
import time
from pymodbus.datastore import (
ModbusSequentialDataBlock,
ModbusServerContext,
ModbusSlaveContext,
)
from pymodbus.server import StartTcpServer
CO, DI, HR, IR = 1, 2, 3, 4
ST_IDLE, ST_BUSY, ST_POS, ST_HOLD = 0, 1, 2, 3
REG_STATUS = 769
REG_CUR_POS = 770
class GripperLogic(threading.Thread):
def __init__(self, context, period=0.05):
super().__init__(daemon=True)
self.ctx = context[0]
self.period = period
self.busy = False
self.ticks = 0
self.move_ticks = 8
#Initial values: model, firmware, status Idle
self.ctx.setValues(HR, 1536, [2624])
self.ctx.setValues(IR, REG_STATUS, [ST_IDLE])
self.ctx.setValues(IR, REG_CUR_POS, [0])
self.ctx.setValues(IR, 771, [1, 0]) # FW 1.0
def run(self):
print("Starting gripper logic thread.")
while True:
confirm = self.ctx.getValues(HR, 1606, count=1)[0]
if not self.busy and confirm == 1:
direction = self.ctx.getValues(HR, 1600, count=1)[0]
stroke = self.ctx.getValues(HR, 1601, count=1)[0]
self.busy = True
self.ticks = 0
self.target = stroke if direction == 1 else 0
self.ctx.setValues(IR, REG_STATUS, [ST_BUSY])
print(f"▶ Gripper {'open' if direction == 1 else 'close'}"
f"(target {self.target/100:.2f} mm)")
elif self.busy:
self.ticks += 1
if self.ticks >= self.move_ticks:
self.ctx.setValues(IR, REG_CUR_POS, [self.target])
self.ctx.setValues(IR, REG_STATUS, [ST_POS])
self.ctx.setValues(HR, 1606, [0]) #Confirmation flag automatic reset
self.busy = False
print(f"✔ Action completed: state=2(Pos), position={self.target/100:.2f} mm")
time.sleep(self.period)
def build_context():
store = ModbusSlaveContext(
di=ModbusSequentialDataBlock(0, [0] * 100),
co=ModbusSequentialDataBlock(0, [0] * 100),
hr=ModbusSequentialDataBlock(0, [0] * 1700),
ir=ModbusSequentialDataBlock(0, [0] * 800),
zero_mode=True,
)
return ModbusServerContext(slaves=store, single=True)
def main():
ap = argparse.ArgumentParser()
ap.add_argument("--host", default="0.0.0.0")
ap.add_argument("--port", type=int, default=502)
args = ap.parse_args()
#cp949 Prevent logic thread from dying when outputting ▶/✔ in console.
try:
sys.stdout.reconfigure(encoding="utf-8", errors="replace")
except Exception:
pass
context = build_context()
GripperLogic(context).start()
print("=" * 60)
print(f"XEG Gripper Modbus SERVER Simulator {args.host}:{args.port}")
print("Try controlling it using 1600/1601/1606 with modbus_master.py instead of the robot (Client).")
print("Exit: Ctrl+C")
print("=" * 60)
try:
StartTcpServer(context=context, address=(args.host, args.port))
except KeyboardInterrupt:
print("\nServer shutdown.")
if __name__ == "__main__":
main()
_shared/modbus_master.pyPython
"""modbus_master.py — Modbus Master (Client) practice tool
It is used for two purposes.
(1) Command line CLI: Learn 4 data types by directly reading/writing registers of the simulator (slave)
(2) import module: Import and use the RobotMaster class from another practice script
Based on pymodbus 3.6.x. Install: python -m pip install pymodbus==3.6.9
CLI example:
# Assume the robot server simulator (robot_server_sim.py) is running on port 502
python modbus_master.py read-hr 100 1 # Read speed settings
python modbus_master.py write-hr 100 50 # Write at 50% speed
python modbus_master.py read-ir 524 1 # Read operation status
python modbus_master.py read-di 0 8 # SO1~SO8 status
python modbus_master.py write-coil 0 1 # SI1(Start) trigger
python modbus_master.py read-coil 0 8"""
import argparse
import sys
from pymodbus.client import ModbusTcpClient
class RobotMaster:
"""Modbus TCP master wrapper to control robots/slaves."""
def __init__(self, host="127.0.0.1", port=502, unit=1):
self.client = ModbusTcpClient(host, port=port)
self.unit = unit
def __enter__(self):
if not self.client.connect():
raise ConnectionError(f"Failed to connect to slave: {self.client}")
return self
def __exit__(self, *exc):
self.client.close()
#--- Read ---------------------------------------------------------------
def read_holding(self, addr, count=1):
rr = self.client.read_holding_registers(addr, count=count, slave=self.unit)
self._check(rr)
return rr.registers
def read_input(self, addr, count=1):
rr = self.client.read_input_registers(addr, count=count, slave=self.unit)
self._check(rr)
return rr.registers
def read_coils(self, addr, count=1):
rr = self.client.read_coils(addr, count=count, slave=self.unit)
self._check(rr)
return rr.bits[:count]
def read_discrete(self, addr, count=1):
rr = self.client.read_discrete_inputs(addr, count=count, slave=self.unit)
self._check(rr)
return rr.bits[:count]
#---Writing-------------------------------------------------------------
def write_holding(self, addr, values):
if isinstance(values, int):
values = [values]
#Modbus registers are transmitted as 16bit unsigned. returned by split_word()
#Negative words (e.g. High=-5) are also accepted and encoded as 0~65535 (& 0xFFFF).
#The slave interprets it again as a signed value.
values = [v & 0xFFFF for v in values]
rr = self.client.write_registers(addr, values, slave=self.unit)
self._check(rr)
def write_coil(self, addr, value):
rr = self.client.write_coil(addr, bool(value), slave=self.unit)
self._check(rr)
def write_coils(self, addr, values):
rr = self.client.write_coils(addr, [bool(v) for v in values], slave=self.unit)
self._check(rr)
@staticmethod
def _check(rr):
if rr.isError():
raise IOError(f"Modbus error response: {rr}")
def main():
ap = argparse.ArgumentParser(description="Modbus TCP Master CLI")
ap.add_argument("--host", default="127.0.0.1")
ap.add_argument("--port", type=int, default=502)
ap.add_argument("--unit", type=int, default=1, help="Slave ID")
ap.add_argument("op", choices=[
"read-hr", "write-hr", "read-ir",
"read-coil", "write-coil", "read-di",
])
ap.add_argument("addr", type=int)
ap.add_argument("value", type=int, nargs="?", default=1,
help="The read series is the count, and the write series is the value to write.")
args = ap.parse_args()
try:
with RobotMaster(args.host, args.port, args.unit) as m:
if args.op == "read-hr":
print(m.read_holding(args.addr, args.value))
elif args.op == "write-hr":
m.write_holding(args.addr, args.value)
print(f"HR[{args.addr}] <- {args.value} done")
elif args.op == "read-ir":
print(m.read_input(args.addr, args.value))
elif args.op == "read-coil":
print([int(b) for b in m.read_coils(args.addr, args.value)])
elif args.op == "write-coil":
m.write_coil(args.addr, args.value)
print(f"Coil[{args.addr}] <- {bool(args.value)} done")
elif args.op == "read-di":
print([int(b) for b in m.read_discrete(args.addr, args.value)])
except Exception as e:
print(f"[Error] {e}", file=sys.stderr)
sys.exit(1)
if __name__ == "__main__":
main()
_shared/README.mdMarkdown
# Shared library (`_shared/`)
This is a Python tool/simulator shared by all parties that reproduces HIWIN robot communication using only a PC.
Instead of duplicating large simulators from piece to piece, we manage them here in one place. (Based on pymodbus 3.6.9 / pyserial 3.5, all execution verification completed)
## Installation
```powershell
cd lecture/_shared
python -m pip install -r requirements.txt
#If Hangul is broken in the Windows console:
set PYTHONUTF8=1 # PowerShell: $env:PYTHONUTF8=1
```
## How to use it in each part
- Run the **standalone simulator** (Robot/Gripper/Vision) directly in a separate terminal, and connect the convenience `main.py` to With-TCP/Socket (no import required).
```powershell
python lecture/_shared/robot_server_sim.py --port 1502 --host 127.0.0.1
```
- **Helper module** (`word_tools`, `modbus_master`) is imported by convenience `main.py`. Path bootstrap:
```python
import os, sys
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "..", "..", "..", "_shared"))
from word_tools import split_word
from modbus_master import RobotMaster
```
(Because the edited file location is `GROUP/LEC/src/PROJECT/main.py`, it is 4 levels higher up to `_shared`.)
## File list
| file | Role | How to use |
|------|------|---------|
| `word_tools.py` | 32bit Low/High word segmentation/restoration, SWAP_WORD, IEEE754 conversion | 05, 07, Appendix B |
| `modbus_master.py` | Modbus Master CLI + `RobotMaster` Class | 05, 06, 07 |
| `robot_server_sim.py` | Robot = Modbus slave (implementation of command cycle) | 06, 09 |
| `gripper_sim.py` | XEG electric gripper = Modbus slave | 07, 09 |
| `tcp_echo_server.py` | TCP/IP high-level system (vision) simulator, `{data}` format | 03, 09 |
| `serial_echo.py` | RS-232 serial echo server (virtual COM) | 04 |
| `serial_client.py` | RS-232 serial client (virtual COM) | 04 |
> I/O handshake (Part 02) and integrated controller (Part 09) are included in each part of `src/<project>/main.py`.
## Check quick operation
```powershell
#1) Conversion tool (no equipment required)
python word_tools.py
#2) Launch the robot server (one terminal)
python robot_server_sim.py --port 1502 --host 127.0.0.1
#3) Control as master from another terminal
python modbus_master.py --port 1502 read-di 0 8 # SO1~SO8 (SO4=Ready)
python modbus_master.py --port 1502 read-ir 524 1 #Operation status (1=Idle)
```
> **Port 502 Permissions**: On Windows, port 502 may require administrator privileges. In practice, like `--port 1502`
> If you use port 1024 or higher, the process will proceed without permission issues. It actually uses Robots/equipment 502.
> **Running two simulators simultaneously**: Disconnect the ports. Example) Robot `--port 1502`, Gripper `--port 1503`.
_shared/requirements.txtText
# This lecture practice code dependency
# Install: python -m pip install -r requirements.txt
#
# The API of pymodbus varies depending on the version. This lecture is based on 3.6.x Written/Verified.
pymodbus==3.6.9
pyserial==3.5
_shared/robot_server_sim.pyPython
"""robot_server_sim.py — Robot = Modbus SERVER(Slave) simulator
Manual Ch.5.3 (p.41~80). The situation where the PLC/higher master controls the robot is reproduced on the PC.
Leave this program open and open modbus_master.py or QModMaster (GUI) in another window.
Practice the “instruction cycle” by reading/writing registers.
Command execution procedure (Manual 5.3.3.5):
1) HR[201] <- Command number (e.g. 0=PTP, 1=LIN, 3=JOG, 4=GO HOME)
2) HR[202..] <- parameter
3) HR[200] <- 1 (execution trigger)
4) Robot operates → IR[524] (operation status) is 2(Running) → 1(Idle), IR[200]=1(Success)
5) HR[200] <- 0 (Master resets, prepares for next command)
※ The parameter arrangement of this simulator has been simplified for learning purposes (refer to the PTP annotation below).
For the full parameter table of the actual robot, refer to Table 16 in the manual.
Register summary (zero_mode, address is 0-base):
Discrete Input (read only): 0~ SO1..SOn (SO1=Run, SO2=Held, SO3=Fault, SO4=Ready ...)
Coil (read/write): 0~ SI1..SIn (SI1=Start, SI2=Hold, SI3=Stop, SI4=Enable ...),
300~DO1..DOOn
Input Register (read only): 100 speed%, 200 command status, 201 current command, 300~ joint A1..A6(L/H),
400~ Orthogonal
Holding Reg (read/write): 100 speed setting, 200 execution trigger, 201 command number, 202 ~ parameter
Run: python robot_server_sim.py # 0.0.0.0:502, Slave ID 1
python robot_server_sim.py --port 1502 --unit 1
Exit: Ctrl+C"""
import argparse
import os
import sys
import threading
import time
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
from word_tools import split_word, combine_word # noqa: E402
from pymodbus.datastore import ( # noqa: E402
ModbusSequentialDataBlock,
ModbusServerContext,
ModbusSlaveContext,
)
from pymodbus.server import StartTcpServer # noqa: E402
#Use Modbus function code as register type selector (pymodbus convention)
CO, DI, HR, IR = 1, 2, 3, 4
CMD_NAMES = {0: "PTP", 1: "LIN", 2: "CIRC", 3: "JOG", 4: "GO HOME", 5: "T_STOP", 6: "STOP"}
class RobotLogic(threading.Thread):
"""Monitors command triggers in the background and operates the virtual robot."""
def __init__(self, context, unit=1, period=0.05):
super().__init__(daemon=True)
self.ctx = context[unit if not context.single else 0]
self.period = period
self.busy = False
self.ack_wait = False #Wait for the master to reset to HR[200]=0
self.ticks = 0
self.move_ticks = 10 #Approximately 0.5 second virtual travel time
self._init_registers()
def _init_registers(self):
#SO default state: SO4=Ready ON
self.ctx.setValues(DI, 0, [0, 0, 0, 1, 0, 0, 0, 0])
#IR State Reset
self.ctx.setValues(IR, 100, [100]) #speed 100%
self.ctx.setValues(IR, 200, [0, 0]) #Command status=0, current command=0
self.ctx.setValues(IR, 524, [1]) #Operation status = Idle
self.ctx.setValues(IR, 522, [1]) #Motor excitation ON
#Current position (joint/orthogonal) to 0
self.ctx.setValues(IR, 300, [0] * 12)
self.ctx.setValues(IR, 400, [0] * 12)
#--- Helper -------------------------------------------------------------
def _get(self, fc, addr, count=1):
return self.ctx.getValues(fc, addr, count=count)
def _set(self, fc, addr, values):
self.ctx.setValues(fc, addr, values)
def _set_so(self, idx, on):
self._set(DI, idx, [1 if on else 0])
#--- Execute command --------------------------------------------------------
def _start_command(self):
cmd = self._get(HR, 201)[0]
self.current_cmd = cmd
self.busy = True
self.ticks = 0
self._set(IR, 524, [2]) # Running
self._set(IR, 201, [cmd]) #current command
self._set(IR, 200, [0]) #status in progress
self._set_so(0, 1) # SO1=Run ON
print(f"▶ Start command: {cmd} ({CMD_NAMES.get(cmd, '?')})")
def _finish_command(self):
cmd = self.current_cmd
params = self._get(HR, 200, count=20) # HR[200..219]
#params[0]=trigger, [1]=command number, [2]=type, [3..14]=6 axis L/H, [15]=speed...
if cmd in (0, 1): # PTP / LIN
mtype = params[2] # 0=Joint, 1=Cartesian
axis_words = params[3:15] #12 words = 6 axes (L, H)
target = self.ctx_in_eng(axis_words)
if mtype == 0:
self._set(IR, 300, axis_words) #Current joint position = target
print(f"Joint movement completed A1~A6 = {target}")
else:
self._set(IR, 400, axis_words) #Current Cartesian Position = Target
print(f"Orthogonal movement completed X..C = {target}")
spd = params[15] if params[15] else 100
self._set(IR, 100, [spd])
elif cmd == 4: # GO HOME
self._set(IR, 300, [0] * 12)
self._set(IR, 400, [0] * 12)
print("Return to origin completed")
#common finish
self._set(IR, 524, [1]) # Idle
self._set(IR, 200, [1]) # Success
self._set_so(0, 0) # SO1=Run OFF
self.busy = False
self.ack_wait = True
print(f"✔ Command completion: IR[200]=1(Success). Reset the master to HR[200]<-0.")
@staticmethod
def ctx_in_eng(words):
"""[L,H,L,H,...] 12 words -> List of 6 engineering values (0.001 scale)."""
out = []
for i in range(0, 12, 2):
low = words[i] if words[i] < 32768 else words[i] - 65536
high = words[i + 1] if words[i + 1] < 32768 else words[i + 1] - 65536
out.append(round(combine_word(low, high) * 0.001, 3))
return out
#--- Main Loop --------------------------------------------------------
def run(self):
print("Starting the robot logic thread.")
while True:
trig = self._get(HR, 200)[0]
if self.ack_wait:
if trig == 0:
self.ack_wait = False #Confirm master reset → Next commands possible
elif self.busy:
self.ticks += 1
if self.ticks >= self.move_ticks:
self._finish_command()
elif trig == 1:
self._start_command()
time.sleep(self.period)
def build_context():
store = ModbusSlaveContext(
di=ModbusSequentialDataBlock(0, [0] * 600),
co=ModbusSequentialDataBlock(0, [0] * 600),
hr=ModbusSequentialDataBlock(0, [0] * 2100),
ir=ModbusSequentialDataBlock(0, [0] * 600),
zero_mode=True,
)
return ModbusServerContext(slaves=store, single=True)
def main():
ap = argparse.ArgumentParser()
ap.add_argument("--host", default="0.0.0.0")
ap.add_argument("--port", type=int, default=502)
ap.add_argument("--unit", type=int, default=1)
args = ap.parse_args()
#When outputting ▶/✔ from the Windows default console (cp949), UnicodeEncodeError occurs.
#Prevent logic threads from dying (compared to environments where PYTHONUTF8 is not set)
try:
sys.stdout.reconfigure(encoding="utf-8", errors="replace")
except Exception:
pass
context = build_context()
logic = RobotLogic(context, unit=args.unit)
logic.start()
print("=" * 60)
print(f"Robot Modbus SERVER Simulator {args.host}:{args.port} (Slave ID {args.unit})")
print("Connect to the master (modbus_master.py / QModMaster) and practice the command cycle.")
print("Exit: Ctrl+C")
print("=" * 60)
try:
StartTcpServer(context=context, address=(args.host, args.port))
except KeyboardInterrupt:
print("\nServer shutdown.")
if __name__ == "__main__":
main()
_shared/serial_client.pyPython
"""serial_client.py — RS-232 robot client (PC simulation, chapter 03-rs232)
Open the other side (COM5) of the com0com virtual COM port pair to transmit packets and
Receives and outputs the response from the echo server (serial_echo.py, COM6).
In an actual robot, this flow is written as COPEN(SER,HANDLE) -> CWRITE -> CREAD.
Run (serial_echo.py should be floating in another terminal):
python serial_client.py --port COM5 --baud 9600 --value 123"""
import argparse
import serial # pyserial
def main():
ap = argparse.ArgumentParser(description="RS-232 client (send packet and receive response)")
ap.add_argument("--port", default="COM5", help="Open virtual COM port (default COM5)")
ap.add_argument("--baud", type=int, default=9600, help="baud rate (must be the same on both sides)")
ap.add_argument("--value", default="123", help="Value to send (default 123)")
args = ap.parse_args()
ser = serial.Serial(args.port, args.baud, timeout=1)
packet = ("{" + args.value + "}").encode("ascii") #Example: b'{123}'
ser.write(packet)
print(f"[client] {args.port} @ {args.baud}bps -> send {packet!r}")
resp = ser.read(64) #Receive echo server response
print(f"[client] receive {resp!r} (decode: {resp.decode('ascii', errors='replace')})")
ser.close()
if __name__ == "__main__":
main()
_shared/serial_echo.pyPython
"""serial_echo.py — RS-232 echo server (PC simulation, chapter 03-rs232)
Open one side (COM6) of the com0com virtual COM port pair (e.g. COM5 <-> COM6),
The received data is returned by wrapping it in curly braces { } as in the HIWIN packet format.
The PC imitates the RS-232 response flow (role of the upper system) of a real robot.
Run:
python serial_echo.py --port COM6 --baud 9600
Stop: Ctrl+C"""
import argparse
import serial # pyserial
def main():
ap = argparse.ArgumentParser(description="RS-232 echo server (serial reception -> {data} response)")
ap.add_argument("--port", default="COM6", help="Open virtual COM port (default COM6)")
ap.add_argument("--baud", type=int, default=9600, help="baud rate (must be the same on both sides)")
args = ap.parse_args()
#timeout=1: prevents infinite waiting by making read() wait for data only for 1 second.
ser = serial.Serial(args.port, args.baud, timeout=1)
print(f"[echo] {args.port} @ {args.baud}bps opened. Waiting to receive... (Ctrl+C Exit)")
try:
while True:
data = ser.read(64) #Receive up to 64 bytes (if none, b'' after 1 second)
if not data:
continue
text = data.decode("ascii", errors="replace")
reply = "{" + text + "}" #Wrap it in { } like HIWIN packet format
ser.write(reply.encode("ascii"))
print(f"[echo] Receive {data!r} -> Reply {reply!r}")
except KeyboardInterrupt:
print("\n[echo] End")
finally:
ser.close()
if __name__ == "__main__":
main()
_shared/tcp_echo_server.pyPython
"""tcp_echo_server.py — High-level system (vision) simulator for Ethernet TCP/IP communication practice
Manual Ch.3 (p.30~33). The robot operates as a client, and the vision/higher system operates as a server.
In accordance with the HRSS communication protocol, packets are surrounded by curly braces { } and separated by commas.
Sent by robot: {TRIG} (request to shoot)
Server responds: {123.45,67.89,30.0} (X, Y, rotation angle)
Run:
python tcp_echo_server.py # vision mode, 0.0.0.0:6000
python tcp_echo_server.py --mode echo # Returns the received value as is.
python tcp_echo_server.py --port 6000 --host 0.0.0.0
Exit: Ctrl+C"""
import argparse
import socket
def make_vision_reply(request: str) -> str:
"""Simple fake vision results. In reality, the coordinates detected by the camera are included."""
#Different coordinates may be given depending on the request text (e.g. TRIG, COUNT, etc.).
samples = {
"TRIG": "123.45,67.89,30.0",
"TRIG2": "-45.10,210.00,-15.0",
}
return samples.get(request.strip(), "0.00,0.00,0.0")
def handle(conn: socket.socket, addr, mode: str):
print(f"[connection] {addr} connection")
with conn:
buf = ""
while True:
data = conn.recv(1024)
if not data:
break
buf += data.decode("ascii", errors="replace")
#Processed as packet terminator '}'
while "}" in buf:
pkt, buf = buf.split("}", 1)
payload = pkt.replace("{", "").strip()
print(f"Receive: {{{payload}}}")
if mode == "echo":
reply = payload
else:
reply = make_vision_reply(payload)
msg = "{" + reply + "}"
conn.sendall(msg.encode("ascii"))
print(f"Sent from: {msg}")
print(f"[End] {addr}")
def main():
ap = argparse.ArgumentParser()
ap.add_argument("--host", default="0.0.0.0")
ap.add_argument("--port", type=int, default=6000)
ap.add_argument("--mode", choices=["vision", "echo"], default="vision")
args = ap.parse_args()
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as srv:
srv.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
srv.bind((args.host, args.port))
srv.listen(1)
print(f"Waiting for TCP server {args.host}:{args.port} (mode={args.mode})")
print("Wait for the robot (client) to connect to COPEN (ETH, ...). Exit: Ctrl+C")
try:
while True:
conn, addr = srv.accept()
handle(conn, addr, args.mode)
except KeyboardInterrupt:
print("\nShutting down the server.")
if __name__ == "__main__":
main()
_shared/word_tools.pyPython
"""word_tools.py — HIWIN robot Modbus data conversion practice tool
Refer to manual Appendix II (32bit split storage) and 5.4.4 (SWAP_WORD / IEEE754).
This is a learning module that you can check directly with Python. No need for pymodbus.
Run:
python word_tools.py # Demo (verification of manual example)
python word_tools.py 90.83 # Check split/restore with random real values"""
import struct
import sys
# ---------------------------------------------------------------------------
#1) 32bit value ↔ Low/High word division (Manual Appendix II, p.131~133)
#The robot stores values exceeding 16 bits (coordinates, speed, etc.) divided into two registers.
#Each word is signed 16 bits (-32768~32767).
# ---------------------------------------------------------------------------
def split_word(value: int):
"""signed integer -> (low_word, high_word). Both signed 16bit.
>>> split_word(90830) # A1 = 90.830° (= 90.83 * 1000)
(25294, 1)
>>> split_word(300000) #
(-27680, 4)
>>> split_word(-300000) #
(27680, -5)"""
high = value // 65536 #Quotient (round down if negative → consistent with manual rule)
rem = value % 65536 #Rest: always 0 to 65535
low = rem - 65536 if rem > 32767 else rem #Expressed as signed 16bit
return low, high
def combine_word(low: int, high: int) -> int:
"""(low_word, high_word) -> Original signed integer.
>>> combine_word(25294, 1)
90830
>>> combine_word(-6742, -7) # X = -399.958 mm (Manual example)
-399958"""
return high * 65536 + (low & 0xFFFF) #High maintains sign, low is treated as unsigned
def to_engineering(low: int, high: int, scale: float = 0.001) -> float:
"""Low/High words to actual engineering values. (Coordinates/angle scale=0.001)"""
return combine_word(low, high) * scale
# ---------------------------------------------------------------------------
#2) SWAP_WORD (Manual 5.4.4.1, p.102)
#Exchange the upper/lower word positions of the 32-bit value. (Correct byte order difference by equipment)
# ---------------------------------------------------------------------------
def swap_word(v: int) -> int:
"""High word ↔ Low word exchange.
>>> swap_word(10) # 0x0000000A -> 0x000A0000
655360"""
v &= 0xFFFFFFFF
return ((v & 0xFFFF) << 16) | ((v >> 16) & 0xFFFF)
# ---------------------------------------------------------------------------
#3) IEEE754 single precision conversion (Manual 5.4.4.2 / 5.4.4.3, p.104~105)
#Since Modbus cannot transmit real numbers directly, they are encoded and received as 32-bit integers.
# ---------------------------------------------------------------------------
def ieee754_encode(f: float) -> int:
"""Real number -> 32bit integer (IEEE754 single).
>>> ieee754_encode(10.5)
1093140480"""
return struct.unpack(">I", struct.pack(">f", f))[0]
def ieee754_decode(i: int) -> float:
"""32bit integer (IEEE754 single) -> real number.
>>> ieee754_decode(1093140480)
10.5"""
return struct.unpack(">f", struct.pack(">I", i & 0xFFFFFFFF))[0]
def reg_to_int32(reg_low: int, reg_high: int) -> int:
"""2 registers (unsigned 16bit) -> unsigned 32bit. For IEEE754 decoding preprocessing."""
return ((reg_high & 0xFFFF) << 16) | (reg_low & 0xFFFF)
# ---------------------------------------------------------------------------
#demo
# ---------------------------------------------------------------------------
def _demo():
print("=== 32bit partition/restore (Appendix II) ===")
for eng in (90.830, 300.000, -399.958):
raw = round(eng * 1000)
low, high = split_word(raw)
back = to_engineering(low, high)
print(f" {eng:>10.3f} -> raw={raw:>8} Low={low:>7} High={high:>3}"
f"-> Restore={back:>10.3f} {'OK' if abs(back-eng) < 1e-6 else 'FAIL'}")
print("\n=== SWAP_WORD (5.4.4.1) ===")
print(f"swap_word(10) = {swap_word(10)} (expecting 655360)")
print("\n=== IEEE754 (5.4.4.2/3) ===")
print(f"ieee754_encode(10.5) = {ieee754_encode(10.5)} (expecting 1093140480)")
print(f"ieee754_decode(...) = {ieee754_decode(1093140480)} (expecting 10.5)")
if __name__ == "__main__":
if len(sys.argv) > 1:
val = float(sys.argv[1])
raw = round(val * 1000)
low, high = split_word(raw)
print(f"input {val} (×1000 = {raw})")
print(f" Low word = {low} (0x{low & 0xFFFF:04X})")
print(f" High word = {high} (0x{high & 0xFFFF:04X})")
print(f"restore = {to_engineering(low, high)}")
print(f" IEEE754 = {ieee754_encode(val)}")
else:
_demo()
src/client_gripper/main.pyPython
"""07. MODBUS CLIENT (Robot = Master) — Gripper open/close cycle (client_gripper)
Controls the electric gripper (Slave Server) in the role of a robot (Master Client).
Reproduce the flow of manual 5.4.5.1 example ① (XEG gripper RTU control) with a PC simulator.
Flow:
① Model confirmation: Read HR[1536] → 2624(XEG-64)
② Open: HR[1600]=1 (open), HR[1601]=5000(50.00mm), HR[1606]=1 (run)
③ Wait for completion: IR[769]==2(Pos) and HR[1606]==0(execution flag automatic reset)
④ Close: HR[1600]=0 (close), HR[1606]=1 (execute)
⑤ Wait for completion: IR[769]==2 and HR[1606]==0
⚠️ Completion judgment trap: If you only see IR[769]==2, the state of the previous action is latched and passes immediately.
Be sure to check IR[769]==2 and HR[1606]==0 (automatic reset of execution flag).
Prerequisite: Launch the gripper server in another terminal.
python ../../_shared/gripper_sim.py --port 1503 --host 127.0.0.1
Run:
python src/client_gripper/main.py --port 1503"""
import os
import sys
#--- Add _shared shared library path (Search for _shared from the top regardless of editing position or depth) ---
_d = os.path.dirname(os.path.abspath(__file__))
while _d != os.path.dirname(_d) and not os.path.isdir(os.path.join(_d, "_shared")):
_d = os.path.dirname(_d)
sys.path.insert(0, os.path.join(_d, "_shared"))
#Prevent Hangul/symbols from breaking or dying in the cp949 console.
try:
sys.stdout.reconfigure(encoding="utf-8", errors="replace")
except Exception:
pass
import argparse # noqa: E402
import time # noqa: E402
from modbus_master import RobotMaster # noqa: E402
def run_gripper(m, direction, stroke_001mm=5000, speed_001mm=5000):
"""Sends one action (open/close) to the gripper and polls until completion.
direction: 1=open / 0=close
Completion judgment: IR[769]==2(Pos) **and** HR[1606]==0 (automatic reset of execution flag).
If you only look at IR[769], the previous operation state is latched and passes incorrectly."""
m.write_holding(1600, direction) #① Direction (0=close / 1=open)
m.write_holding(1601, stroke_001mm) #② Movement stroke (0.01mm unit)
m.write_holding(1602, speed_001mm) #③ Movement speed (unit of 0.01mm/s)
m.write_holding(1606, 1) #④ Execution trigger
#⑤ Check IR769==2 and HR1606==0 together (avoid latch trap)
while not (m.read_input(769, 1)[0] == 2 and m.read_holding(1606, 1)[0] == 0):
time.sleep(0.05)
return m.read_input(769, 1)[0], m.read_input(770, 1)[0]
def main():
ap = argparse.ArgumentParser(description="Gripper open/close cycle (robot=master)")
ap.add_argument("--host", default="127.0.0.1")
ap.add_argument("--port", type=int, default=1503)
args = ap.parse_args()
print("=== Gripper Control Cycle (Robot = Master Client) ===")
try:
with RobotMaster(args.host, args.port) as m: # MBC_TCP_OPEN(1,"...",502)
#① Check the model
model = m.read_holding(1536, 1)[0]
name = "XEG-64" if model == 2624 else f"unknown({model})"
print(f"[①] Check model HR[1536]={model} -> {name}")
#② Open 50.00mm
print("[②] Open command: HR[1600]=1, HR[1601]=5000, HR[1606]=1")
st, pos = run_gripper(m, 1, 5000)
print(f"[③] Open completed IR[769]={st}(Pos), IR[770]={pos}(= {pos/100:.2f}mm),"
f"HR[1606]={m.read_holding(1606, 1)[0]}(automatic reset)")
#④ Close
print("[④] Close command: HR[1600]=0, HR[1606]=1")
st, pos = run_gripper(m, 0, 5000)
print(f"[⑤] Close Complete IR[769]={st}(Pos), IR[770]={pos}(= {pos/100:.2f}mm)")
print("Gripper control cycle completed ✅")
return 0
except Exception as e:
print(f"[Error] {e}", file=sys.stderr)
print("Verify that the gripper server is up:"
"python ../../_shared/gripper_sim.py --port "
f"{args.port} --host {args.host}", file=sys.stderr)
return 1
if __name__ == "__main__":
sys.exit(main())
Assignment
Precedence: For number 1 of the two tasks, first launch the gripper server in another terminal. (No server required for number 2)
cd lecture/_shared
set PYTHONUTF8=1
python gripper_sim.py --port 1503 --host 127.0.0.1
Problem 1 — Monitoring gripper status (IR769) as a human-readable string
The robot (Master Client) reads the gripper's IR[769] (state) and outputs the code (0~3) as a human-readable string (Idle/Busy/Pos/Hold).
Send an open command (HR[1600]=1, HR[1601]=5000, HR[1606]=1), pollingIR[769] until completion, and output whenever the status changes.
IR[769]==2 alone is not enough to determine completion. Since the status of the previous operation is latched and passes immediately, check HR[1606]==0 (automatic reset of execution flag) together.
Supports argparse --host(127.0.0.1) --port(1503).
Expected output
=== Gripper status monitoring (IR[769]) ===
State before operation IR[769]=0 -> Idle
Send open command -> start polling for status
IR[769]=0 -> Idle
IR[769]=1 -> Busy
IR[769]=2 -> Pos (position IR[770]=5000 = 50.00mm)
Observed state flow: Idle -> Busy -> Pos
Status monitoring complete ✅
If the gripper is already in the Pos(2) state from the previous exercise, the first line may appear as Pos instead of Idle (latch). This is why you should not judge completion based on IR[769]==2 alone.
Hint
Mapping to STATUS = {0: "Idle", 1: "Busy", 2: "Pos", 3: "Hold"} dictionary.
If you output only when it is different from the previous value, only the change in status is recorded clearly.
Problem 2 — IEEE754 round-trip conversion / SWAP_WORD verification
File name: homework_02/main.py
Key concepts: Modbus transmission of real numbers, IEEE754 Encoding/Decoding, word order correction
Requirements
Load ieee754_encode / ieee754_decode / swap_word of _shared/word_tools.py.
After encoding (real number → integer) the real number 10.5, decode (integer → real number) again and check whether OK/FAIL is the same as the original value. (Other values such as 50.0 as well)
Verify that swap_word(10) is 655360 and swap_word(65535) is 4294901760.
Operates only with word_tools without pymodbus (no server required).
Expected output
=== IEEE754 round-trip conversion (real -> integer -> real) ===
10.5 -> encode -> 1093140480 -> decode -> 10.5 OK
50.0 -> encode -> 1112014848 -> decode -> 50 OK
-12.25 -> encode -> 3242459136 -> decode -> -12.25 OK
=== SWAP_WORD (High/Low word exchange) ===
swap_word(10) = 655360 (expected 655360) OK
swap_word(65535) = 4294901760 (expect 4294901760) OK
Floating point comparison is abs(dec - value) < 1e-6.
swap_word(v) is ((v & 0xFFFF) << 16) | ((v >> 16) & 0xFFFF) — exchange of upper and lower word positions.
Check the correct answer
After solving it yourself, compare it with the reference solution below. The reference solution is annotated with key points and common mistakes.
Show the reference solution
Try the assignment yourself first — comparing your attempt with a working solution is where most of the learning happens.
homework_01/main.pyPython
"""Answer 1 — Monitor gripper status (IR769) as a human-readable string
Key points:
- The robot (Master Client) monitors the status by polling the gripper’s Input Register IR[769].
- Map IR[769] values (0~3) to Idle/Busy/Pos/Hold strings and output them for humans to read.
- IR[769]==2 alone is not enough to determine completion. The status of the previous action is latched and immediately
Since it passes, you must also check HR[1606]==0 (execution flag automatic reset).
Common mistakes:
- It is judged as completed by only looking at IR[769]==2 -> It immediately passes the previous latch value.
- Missing bootstrap -> ModuleNotFoundError: modbus_master"""
import os
import sys
#Add _shared shared library path (search _shared from parent regardless of depth)
_d = os.path.dirname(os.path.abspath(__file__))
while _d != os.path.dirname(_d) and not os.path.isdir(os.path.join(_d, "_shared")):
_d = os.path.dirname(_d)
sys.path.insert(0, os.path.join(_d, "_shared"))
try:
sys.stdout.reconfigure(encoding="utf-8", errors="replace")
except Exception:
pass
import argparse # noqa: E402
import time # noqa: E402
from modbus_master import RobotMaster # noqa: E402
#IR[769] status code -> human-readable string
STATUS = {0: "Idle", 1: "Busy", 2: "Pos", 3: "Hold"}
def status_str(code):
return STATUS.get(code, f"Alarm({code})")
def main():
ap = argparse.ArgumentParser(description="Gripper condition monitoring (IR769)")
ap.add_argument("--host", default="127.0.0.1")
ap.add_argument("--port", type=int, default=1503)
args = ap.parse_args()
print("=== Gripper status monitoring (IR[769]) ===")
try:
with RobotMaster(args.host, args.port) as m:
code = m.read_input(769, 1)[0]
print(f"Status before operation IR[769]={code} -> {status_str(code)}")
#Sends an open command, polls for status changes, and prints them.
m.write_holding(1600, 1) #open
m.write_holding(1601, 5000) # 50.00mm
m.write_holding(1606, 1) #run
print("Send open command -> start polling for status")
seen = []
#Check IR769==2 and HR1606==0 together (avoid latch trap).
#Since Pos(2) is a completion signal, only the in-progress status (0/1) is recorded within the loop.
#The final Pos arrival is output only once after the loop ends.
while not (m.read_input(769, 1)[0] == 2 and m.read_holding(1606, 1)[0] == 0):
code = m.read_input(769, 1)[0]
if code != 2 and (not seen or seen[-1] != code):
seen.append(code)
print(f" IR[769]={code} -> {status_str(code)}")
time.sleep(0.02)
code = m.read_input(769, 1)[0]
pos = m.read_input(770, 1)[0]
if not seen or seen[-1] != code:
seen.append(code)
print(f"IR[769]={code} -> {status_str(code)} (position IR[770]={pos} = {pos/100:.2f}mm)")
print(f"Observed status flow: {' -> '.join(status_str(c) for c in seen)}")
print("Status monitoring complete ✅")
return 0
except Exception as e:
print(f"[Error] {e}", file=sys.stderr)
print("Check gripper server: python ../../_shared/gripper_sim.py --port"
f"{args.port} --host {args.host}", file=sys.stderr)
return 1
if __name__ == "__main__":
sys.exit(main())
homework_02/main.pyPython
"""Answer 2 — IEEE754 round-trip conversion / SWAP_WORD verification
Key points:
- Modbus registers carry only 16-bit integers. Real numbers (floating point numbers) cannot be sent directly.
IEEE754_ENCODE (real number -> 32bit integer) before transmission, IEEE754_DECODE (integer -> real number) after reception.
- If the High/Low word order is reversed between devices, correct by swapping places with SWAP_WORD.
- Verify hand calculation using only _shared/word_tools.py without pymodbus.
Verification values (Manual 5.4.4):
- ieee754_encode(10.5) = 1093140480, ieee754_decode(1093140480) = 10.5
- swap_word(10) = 655360 (0x0000000A -> 0x000A0000)
Common mistakes:
- If you try to write a mistake directly into the register, it will be broken. Be sure to transmit after IEEE754 encoding.
- Missing bootstrap -> ModuleNotFoundError: word_tools"""
import os
import sys
#Add _shared shared library path (search _shared from parent regardless of depth)
_d = os.path.dirname(os.path.abspath(__file__))
while _d != os.path.dirname(_d) and not os.path.isdir(os.path.join(_d, "_shared")):
_d = os.path.dirname(_d)
sys.path.insert(0, os.path.join(_d, "_shared"))
try:
sys.stdout.reconfigure(encoding="utf-8", errors="replace")
except Exception:
pass
from word_tools import ieee754_encode, ieee754_decode, swap_word # noqa: E402
def roundtrip(value):
"""Real -> Integer (encode) -> Real (decode) round trip verification."""
enc = ieee754_encode(value)
dec = ieee754_decode(enc)
ok = abs(dec - value) < 1e-6
print(f"{value:>8} -> encode -> {enc:>12} -> decode -> {dec:<8g} {'OK' if ok else 'FAIL'}")
def check_swap(v, expect):
s = swap_word(v)
ok = s == expect
print(f"swap_word({v}) = {s} (expect {expect}) {'OK' if ok else 'FAIL'}")
def main():
print("=== IEEE754 round-trip conversion (real -> integer -> real) ===")
roundtrip(10.5) # 1093140480
roundtrip(50.0) # 1112014848
roundtrip(-12.25)
print("\n=== SWAP_WORD (High/Low word exchange) ===")
check_swap(10, 655360) # 0x0000000A -> 0x000A0000
check_swap(65535, 4294901760) # 0x0000FFFF -> 0xFFFF0000
print("\n=== Application: Scenario for sending real coordinates to a device in reverse byte order ===")
coord = 10.5
enc = ieee754_encode(coord) #Encoding before transmission
swapped = swap_word(enc) #Opposite equipment word order reverse -> correction
restored = ieee754_decode(swap_word(swapped)) #Receiving side: Revert and decode
ok = abs(restored - coord) < 1e-6
print(f"{coord} -> encode {enc} -> swap {swapped} -> swap-back -> decode {restored} "
f"{'OK' if ok else 'FAIL'}")
return 0
if __name__ == "__main__":
sys.exit(main())