Robot Communication track

Lesson 02 · Lessons

I/O Communication

Pick-and-place with a $DI/$DO handshake

Lessons7 min readFree lesson

I/O communication is the most basic communication in which a robot and other equipment (PLC, machine, sensor) exchange signals by turning ON/OFF one wire. When the robot turns on output ($DO), the other party reads it as input, and when the other party turns on response, the robot reads it as input ($DI). This handshake is the basic pattern of all automation.

In this part, we summarize the five types of I/O of the HIWIN robot and reproduce the Grip/Ungrip handshake of a pick and place cycle as a pure Python state machine. Learn logic with a single PC without physical wiring, simulator, or serial port.

Learning Objectives

  • Distinguish between the 5 types of I/O (DI/O·SI/O·FI/O·RI/O·MI/O) of the HIWIN robot and know the number of channels and variables.
  • Design the handshake with $DO[n]=TRUE output and WAIT FOR $DI[n]==TRUE standby.
  • Read FI/O basic mapping (Start/Hold/Stop/Enable/RSR, Run/Held/Fault/Ready/ACK).
  • Reproduce and execute the two pick-and-place handshakes (grip $DI[1], ungrip $DI[4]) with a Python state machine.

Target environment

  • Tools: Python 3.10+ (only uses standard libraries — no additional installation required)
  • Platform: Windows PC (simulation) — No actual Robot/PLC/Gripper port required
  • Verification: CLI automatic execution — python src/io_handshake/main.py

Key concepts

I/O communication = "1 wire = 1 signal". When the robot turns on the output ($DO), the other party reads it as input, and when the other party turns on the response, the robot reads it as input ($DI). This handshake is key.

1) Five types of I/O at a glance

Type Full name Channel (based on 6 axes) variable Remarks
DI/O Digital I/O 24 I / 24 O $DI / $DO PLC, machine, sensor and general digital signals (SCARA is 16I/8O)
SI/O Fieldbus I/O 128 I / 128 O $SI / $SO CC-Link / Profinet / EtherNet/IP Option Card Required
FI/O Functional I/O 8 I / 8 O (function fixed) Start·Hold·Stop·Enable·RSR, etc. Function signal
RI/O Robot I/O 6 I / 4 O $RI / $RO Wiring at the end of the robot body. SCARA/Delta not allowed
MI/O Module I/O 32 I / 32 O $MI / $MO Bundle multiple I/O and monitor them as modules (specify Start/End I/O)

⚠️ The number of channels varies depending on the model. The above numbers are based on a 6-axis joint robot (RA605, etc.). Be sure to check the corresponding Robot/Controller manual. Some models can add extension I/O.

2) Input/output commands

Type input(read) output(write) range
Digital WAIT FOR $DI[n]==TRUE / ==FALSE $DO[n]=TRUE / =FALSE n=1~24
Fieldbus WAIT FOR $SI[n]==TRUE / ==FALSE $SO[n]=TRUE / =FALSE n=1~128
Function N/A (higher level is directly connected to input) N/A (Output is directly connected to the upper level) 1~8
Robot WAIT FOR $RI[m]==TRUE / ==FALSE $RO[n]=TRUE / =FALSE m=16, n=14
Module WAIT FOR $MI[n]==TRUE / ==FALSE $MO[n]=TRUE / =FALSE n=1~32

WAIT FOR is a blocking command that stops and waits on the line until the condition becomes true. This is the “wait for response” part of the handshake.

3) FI/O basic mapping

Function I/O-is pin number has fixed function. (If you use SI/O, $SI[1]~$SI[8] / $SO[1]~$SO[8] plays that role.)

input $SI[n] Features output $SO[n] Features
$SI[1] Start $SO[1] Run
$SI[2] Hold $SO[2] Held (suspended)
$SI[3] Stop $SO[3] Fault
$SI[4] Enable (operating permission) $SO[4] Ready
$SI[5~8] RSR1~4 (Robot service request) $SO[5~8] ACK1~4 (request response)

4) CN6 port wiring (NPN) concept

  • It exits from the CN6 port of the actual digital I/O-is controller to the terminal block and is connected to the PLC/sensor.
  • Both input and output NPN (sink) type: Recognized as “ON” when the signal line falls to 0V(GND). (It will not work if mixed with PNP equipment, so make sure to match the wiring method.)
  • When wiring, the key is to correctly connect the common line (COM) and GND of the external 24V power supply. Incorrect polarity will cause the input to always be fixed.

See as example

Example — src/io_handshake/main.py: Pick and place I/O handshake state machine

Since the physics I/O-is cannot be created directly on a PC, the logic that exchanges signals between the robot and the PLC is reproduced using a pure Python state machine. Signal states are imitated by dictionaries. (see file for full code)

#Core part — $DO output → PLC response → WAIT FOR $DI one handshake
DO = {6: False}            #Robot output: $DO[6] = Gripper CLOSE/OPEN command
DI = {1: False, 4: False}  #Robot input: $DI[1]=Grip completed, $DI[4]=Ungrip completed

def handshake(title, do_pin, do_value, di_pin, note=""):
    robot_set_do(do_pin, do_value, note)   # $DO[n]=TRUE/FALSE
    plc_respond(do_pin, di_pin)            #After a while, respond with $DI[n]=TRUE
    robot_wait_for(di_pin)                 # WAIT FOR $DI[n]==TRUE

handshake("GRIP",   do_pin=6, do_value=True,  di_pin=1)  #Pick → $DI[1]
handshake("UNGRIP", do_pin=6, do_value=False, di_pin=4)  #Place → $DI[4]

🤖 Real Equipment (HRSS) — The above two handshakes are actually part of the following HRSS pick-and-place program: It is one cycle to pick up the workpiece from one place and place it from another place. Open and close the gripper with $DO[6] and wait for Grip/Ungrip to complete with WAIT FOR $DI. (P1/P3-is +50mm access point above P2/P4, vertical approach to the workpiece to prevent collision or dragging.)

PTP P0 FINE=1 Vel=100% Acc=75% TOOL[0] BASE[0] ; Start from HOME
PTP P1 FINE=1 Vel=100% Acc=75% TOOL[0] BASE[0] ; PICK up approach
LIN P2 FINE=1 Vel=100mm/s Acc=75% TOOL[0] BASE[0] ; point of vertical descent
$DO[6]=TRUE ; Gripper close (pick up) command
WAIT FOR $DI[1]==TRUE ; Wait for grip completion response
LIN P1 FINE=1 Vel=100mm/s Acc=75% TOOL[0] BASE[0] ; Vertical rise (carrying work)
PTP P3 FINE=1 Vel=100% Acc=75% TOOL[0] BASE[0] ; PLACE Move up
LIN P4 FINE=1 Vel=100mm/s Acc=75% TOOL[0] BASE[0] ; point that falls vertically
$DO[6]=FALSE ; Gripper open (release) command
WAIT FOR $DI[4]==TRUE ; Waiting for ungrip completion response
LIN P3 FINE=1 Vel=100mm/s Acc=75% TOOL[0] BASE[0] ; Vertical rise (empty hand)
PTP P0 FINE=1 Vel=100% Acc=75% TOOL[0] BASE[0] ; HOME Return

PTP (point-to-point) is used for fast free movement, LIN (straight line) is used for precise vertical approach near the workpiece.

View Run/verify

cd lecture/02_io/02_io
$env:PYTHONUTF8=1        #Guaranteed Korean/emoji output
python src/io_handshake/main.py

Expected output:

Start pick and place I/O handshake simulation

== GRIP: $DO[6] ON, $DI[1] Wait for grip completion ==
[ROBO] $DO[6]=TRUE output
[PLC] $DO[6]=ON detected → Actuator in operation...
[PLC] Operation completed → $DI[1]=TRUE response
   [ROBO] WAIT FOR $DI[1]==TRUE ...
[ROBO] Check $DI[1]==TRUE → Proceed to next action

== UNGRIP (place): $DO[6] OFF, $DI[4] Wait for ungrip completion ==
[ROBO] $DO[6]=FALSE output (gripper OPEN)
[PLC] $DO[6]=OFF detected → Actuator in operation...
[PLC] Operation completed → $DI[4]=TRUE response
   [ROBO] WAIT FOR $DI[4]==TRUE ...
[ROBO] Check $DI[4]==TRUE → Proceed with next action

Cycle completed ✅ (all handshakes completed normally)

✅ Checkpoint: If the sequence [ROBO] $DO ... output → [PLC] ... response → [ROBO] WAIT FOR...OK is repeated twice and Cycle completed ✅ is visible at the end, it is successful.

Common Mistakes

Q. It passes straight through WAIT FOR without stopping.

A. This is the case when the input is always fixed to ON (usually opposite to NPN/PNP wiring). Set the CN6 wiring to NPN (sink) and check the COM/GND polarity of the external 24V. The code will have the same symptom if the corresponding $DI bit is not initialized to False before the next handshake.

Q. Even if I turn on $DO, the other person does not respond.

A. This is a typo in the output channel/number or the external 24V power is not applied. Recheck the channel number and check the terminal block power.

Q. I get an error saying that $RI/$RO or $SI/$SO does not exist.

A. RI/O Not supported in SCARA·Delta model, so replace DI/O or To-MI/O. SI/O Fieldbus option card (CC-Link/Profinet/EtherNet/IP) must be installed to operate.

Q. Hangul/part✅ is broken on the console.

A. This is a Windows console default encoding (cp949) issue. Run set PYTHONUTF8=1 ($env:PYTHONUTF8=1 for PowerShell). (main.py is also defended once more with stdout.reconfigure(encoding="utf-8").)

Cleanup

  • I learned the number of channels, variables, and input/output commands for 5 types of I/O (DI/O·SI/O·FI/O·RI/O·MI/O).
  • We confirmed through pick and place that the handshake called $DO output → counterpart response → WAIT FOR $DI is the basic pattern for all automation.
  • I/O used to communicate with a single physical signal, but later on in Modbus, software is actually exchanged using coil/register bits.

Try it yourself

Solve the assignment below. The reference solution is shown below.

📖 Manual Source

  • HIWIN Robot Communication User Manual, Chapter 2 Input/Output (I/O) communication method (p.20~29).
  • Table 1 (5 types of I/O), Table 2 (input/output code), Step 1~4 pick and place example, Table 4·5 (DI/O·SI/O handshake table).
  • The PC simulation code is self-organized in this tutorial.

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/io_handshake/main.pyPython
"""02. I/O communication — Pick and place I/O handshake state machine (io_handshake)

When the robot outputs the gripper motion with $DO[6], the upper PLC responds with $DI a moment later.
Outputs the handshake flow step by step to the console. Instead of actual physical I/O (CN6 terminal voltage)
It mimics signal states using Python dictionaries and uses only standard libraries.
(No simulator/serial port required. _shared bootstrap is placed for track consistency.)

Handshake definition:
    $DO[6]=TRUE -> Gripper CLOSE command, PLC responds with $DI[1]=TRUE (grip complete)
    $DO[6]=FALSE -> Gripper OPEN (place) command, PLC responds with $DI[4]=TRUE (ungrip complete)

Run: python src/io_handshake/main.py"""
import os
import sys
import time

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

#Signal table imitating physical I/O (actually voltage ON/OFF of CN6 terminal block)
DO = {6: False}            #Robot output: $DO[6] = Gripper CLOSE/OPEN command
DI = {1: False, 4: False}  #Robot input: $DI[1]=Grip completed, $DI[4]=Ungrip completed

#Demo speed (seconds). If set to 0, it proceeds immediately.
RESPOND_DELAY = 1.0
POLL_INTERVAL = 0.05


def plc_respond(do_pin, di_pin, delay=RESPOND_DELAY):
    """Upper PLC role: Watches the robot's $DO change and responds with $DI after delay."""
    level = "ON" if DO[do_pin] else "OFF"
    print(f"[PLC ] $DO[{do_pin}]={level} detected → Actuator in operation...")
    time.sleep(delay)
    DI[di_pin] = True
    print(f"[PLC] Operation completed → $DI[{di_pin}]=TRUE response")


def robot_wait_for(di_pin):
    """Imitate the robot command WAIT FOR $DI[n]==TRUE (blocking wait until response)."""
    print(f"   [ROBO] WAIT FOR $DI[{di_pin}]==TRUE ...")
    while not DI[di_pin]:
        time.sleep(POLL_INTERVAL)
    print(f"[ROBO] Confirm $DI[{di_pin}]==TRUE → Proceed with next action\n")


def robot_set_do(do_pin, value, note=""):
    """Imitates the robot command $DO[n]=TRUE/FALSE (output bit ON/OFF)."""
    DO[do_pin] = value
    level = "TRUE" if value else "FALSE"
    tail = f" ({note})" if note else ""
    print(f"[ROBO] $DO[{do_pin}]={level} output{tail}")


def handshake(title, do_pin, do_value, di_pin, note=""):
    """$DO output → PLC response → One handshake called WAIT FOR $DI."""
    print(f"== {title} ==")
    robot_set_do(do_pin, do_value, note)
    plc_respond(do_pin, di_pin)   #In reality, PLCs operate simultaneously in separate devices.
    robot_wait_for(di_pin)


def main():
    print("Start pick and place I/O handshake simulation\n")

    #① GRIP: $DO[6] ON → $DI[1] Grip completed
    handshake("GRIP: $DO[6] ON, $DI[1] Wait for grip completion",
              do_pin=6, do_value=True, di_pin=1)

    #② UNGRIP: $DO[6] OFF → $DI[4] Ungrip completed
    handshake("UNGRIP (place): $DO[6] OFF, $DI[4] Wait for ungrip completion",
              do_pin=6, do_value=False, di_pin=4, note="Gripper OPEN")

    print("Cycle completed ✅ (all handshakes completed normally)")
    return 0


if __name__ == "__main__":
    sys.exit(main())

Assignment

Problem 1 — 3-station continuous handshake

  • File name: homework_01/main.py
  • Key concepts: handshake generalization, data/flow separation, $DI response initialization

Requirements

  • Expand the single GRIP/UNGRIP handshake in the text example to a continuous sequence of 3 stations. (Example: Pick up ST1 → Place ST2 → Pick up ST3)
  • Each station is set as data (list) in the form of (Title, $DO pin, $DO value, $DI response pin) and processed with a loop.
  • For each station, the following sequence is output: $DO output → PLC response → WAIT FOR $DI, and at the end, All 3 stations completed ✅ is output.

Expected output (partial)

3-station continuous I/O handshake begins

[1] == ST1 PICK (Pick): $DO[6] ON, $DI[1] Wait for grip completion ==
[ROBO] $DO[6]=TRUE output
[PLC] $DO[6]=ON detection → operating...
[PLC] Operation completed → $DI[1]=TRUE response
    [ROBO] WAIT FOR $DI[1]==TRUE ...
[ROBO] Check $DI[1]==TRUE → Proceed to next action
...
All 3 stations completed ✅

Hint

  • It is easy to Add/delete if you put the station information in a list. for title, do, val, di in STATIONS:
  • Before the next WAIT FOR, the corresponding $DI bit must be initialized to False so that it does not pass immediately. If this is omitted, the second time passes without waiting.

Problem 2 — Monitoring handshake timeout

  • File name: homework_02/main.py
  • Key concepts: risk of blocking wait, timeout, time.monotonic()

Requirements

  • Set a timeout (e.g. 2 seconds) to prevent WAIT FOR $DI from falling into an infinite wait.
  • If the response comes within the deadline, normal progress is displayed along with the elapsed time. If the response does not arrive in the end, a ⚠️ Time out! warning is output.
  • Shows both normal case (no response) and fault case (no response).

Expected output

Start handshake timeout monitoring demo

== CASE A: Normal response ($DO[6] ON → $DI[1]) ==
[ROBO] $DO[6]=TRUE output
   [ROBO] WAIT FOR $DI[1]==TRUE ... (timeout 2.0s)
[ROBO] Confirm $DI[1]==TRUE (0.50s) → Proceed

== CASE B: No response ($DO[6] OFF → $DI[4]) ==
[ROBO] $DO[6]=FALSE output
   [ROBO] WAIT FOR $DI[4]==TRUE ... (timeout 2.0s)
[WARN] ⚠️ $DI[4] No response — 2.0s timeout! (Cable/PLC needs to be checked)

Result: CASE A=OK, CASE B=TIMEOUT
Timeout occurs ❌ — Check the items above ⚠️

Hint

  • Elapsed time is measured as start = time.monotonic() followed by time.monotonic() - start (not affected by system clock changes).
  • Since the non-response case is a situation in which “the PLC does not respond forever,” the response time is set to None and simulated.
  • If a timeout occurs, the step is treated as failure and stopped (continuing in the field will lead to malfunction). CASE B is an intended timeout, so the exit code is 1.

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 — 3-station continuous handshake

Key points:
- Generalize the single handshake (GRIP/UNGRIP) in Part 1, main.py, to repetition of multiple stations.
- Each station is expressed with 4 elements (name, $DO pin, $DO value, $DI pin), and if set as data (list)
  Easy to add/delete stations — a pattern that separates data from operational flow.
- The previous response ($DI) must be initialized so that the next WAIT FOR does not pass immediately.

Common mistakes:
- The DI bit is not reset, so the next handshake passes immediately without waiting.
- Confusing $DO ON/OFF and $DI pin numbers in output messages.

Use only standard libraries. No simulator/port required.
Run: python main.py"""
import os
import sys
import time

#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

DELAY = 0.3  #PLC response delay (seconds). If set to 0, proceed immediately.

#Signal table (mimicking physical I/O)
DO = {6: False}
DI = {1: False, 4: False}

#3-station sequence: (title, $DO pin, $DO value, $DI response pin)
STATIONS = [
    ("ST1 PICK: $DO[6] ON, $DI[1] Wait for grip completion",   6, True,  1),
    ("ST2 PLACE: $DO[6] OFF, $DI[4] Wait for ungrip completion", 6, False, 4),
    ("ST3 PICK: $DO[6] ON, $DI[1] Wait for grip completion",   6, True,  1),
]


def handshake(idx, title, do_pin, do_value, di_pin):
    print(f"[{idx}] == {title} ==")
    DO[do_pin] = do_value
    DI[di_pin] = False  #Initialize previous response → make this WAIT FOR actually wait
    level = "TRUE" if do_value else "FALSE"
    print(f"[ROBO] $DO[{do_pin}]={level} output")
    #PLC response
    print(f"[PLC ] $DO[{do_pin}]={'ON' if do_value else 'OFF'} Detected → Operating...")
    time.sleep(DELAY)
    DI[di_pin] = True
    print(f"[PLC] Operation completed → $DI[{di_pin}]=TRUE response")
    #robot waiting
    print(f"    [ROBO] WAIT FOR $DI[{di_pin}]==TRUE ...")
    while not DI[di_pin]:
        time.sleep(0.05)
    print(f"[ROBO] Confirm $DI[{di_pin}]==TRUE → Proceed with next action\n")


def main():
    print("Start 3-station continuous I/O handshake\n")
    for i, (title, do_pin, do_value, di_pin) in enumerate(STATIONS, start=1):
        handshake(i, title, do_pin, do_value, di_pin)
    print(f"All {len(STATIONS)} stations completed ✅")
    return 0


if __name__ == "__main__":
    sys.exit(main())
homework_02/main.pyPython
"""Answer 2 — Handshake timeout monitoring

Key points:
- If the $DI response never comes in the actual field (cable disconnected, PLC stopped, etc.), WAIT FOR is used.
  Wait infinitely. So, if there is no response within a “timeout”, a warning is issued.
- Elapsed time is measured with time.monotonic() (unaffected by changes to the system clock).
- Shows both normal cases and timeout cases.

Common mistakes:
- Instead of using time.time(), the time is counted only by the number of sleeps, making it inaccurate.
- Continuing the wrong action by continuing even after the timeout → Here, only a warning is issued.
  The step is treated as a failure.

Use only standard libraries. No simulator/port required.
Run: python main.py"""
import os
import sys
import time

#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

DI = {1: False, 4: False}
TIMEOUT = 2.0      #Maximum number of seconds to wait for a response
POLL = 0.05


def wait_for_di(di_pin, respond_after):
    """$DI[di_pin] Waits for response within TIMEOUT.
    Assume that the PLC responds after respond_after seconds (if None, it does not respond at all).
    Returns True if a response is received, False if a timeout occurs."""
    print(f"   [ROBO] WAIT FOR $DI[{di_pin}]==TRUE ... (timeout {TIMEOUT:.1f}s)")
    start = time.monotonic()
    while True:
        elapsed = time.monotonic() - start
        #Simulate that the PLC responded at the respond_after point
        if respond_after is not None and elapsed >= respond_after:
            DI[di_pin] = True
        if DI[di_pin]:
            print(f"[ROBO] Check $DI[{di_pin}]==TRUE ({elapsed:.2f}s) → Proceed\n")
            return True
        if elapsed >= TIMEOUT:
            print(f"[WARN] ⚠️ $DI[{di_pin}] not responding — {TIMEOUT:.1f}s timeout!"
                  f"(Cable/PLC inspection required)\n")
            return False
        time.sleep(POLL)


def main():
    print("Start handshake timeout monitoring demo\n")

    #Case A: Normal — PLC responds after 0.5s
    print("== CASE A: Normal response ($DO[6] ON → $DI[1]) ==")
    print("[ROBO] $DO[6]=TRUE output")
    ok_a = wait_for_di(1, respond_after=0.5)

    #Case B: Failure — PLC never responds
    print("== CASE B: No response ($DO[6] OFF → $DI[4]) ==")
    print("[ROBO] $DO[6]=FALSE output")
    ok_b = wait_for_di(4, respond_after=None)

    print(f"Result: CASE A={'OK' if ok_a else 'TIMEOUT'},"
          f"CASE B={'OK' if ok_b else 'TIMEOUT'}")
    print("Normal shutdown ✅" if ok_a and ok_b else "Timeout occurs ❌ — Check the items above ⚠️")
    return 0 if (ok_a and ok_b) else 1


if __name__ == "__main__":
    sys.exit(main())

Learn this lesson with a mentor

Work through the Robot Communication track on your own, or join a guided cohort at our Pitampura campus and online.

Call NowEnroll Now