# RSIPI: Robot Sensor Interface for Python [![Python 3.10+](https://img.shields.io/badge/python-3.10+-blue.svg)](https://www.python.org/downloads/) [![License: MIT](https://img.shields.io/badge/License-MIT-green.svg)](LICENSE) RSIPI is a Python library for real-time control of KUKA industrial robots via the Robot Sensor Interface (RSI) protocol. The robot controller sends its state over UDP at a configurable cycle rate (4ms at 250Hz or 12ms at 83Hz), and RSIPI sends back position corrections, I/O commands, and Tech parameters. Communication uses XML packets over a dedicated Ethernet link, managed in a separate process so your control logic never blocks the real-time loop. --- ## Safety Notice RSIPI directly controls industrial robot motion. Misuse can cause damage or injury. - **Test offline first** using the built-in echo server before connecting to a real robot. - **Hardware E-stops** must be present and functional. RSIPI's software E-stop is not safety-rated. - **Limit correction ranges** via `api.safety.set_limit()` and KUKA Workspaces. - **Isolate the RSI network** -- use a dedicated Ethernet interface with no external access. - **Never run unattended** without proper risk assessment and safety measures. --- ## Installation Requires Python 3.10+. ```bash # Development install (editable) pip install -e . # Or install dependencies directly pip install pandas>=2.0 numpy>=1.22 matplotlib>=3.5 lxml>=4.9 scipy>=1.8 ``` --- ## Quick Start ```python from RSIPI import RSIAPI # Context manager handles cleanup on exit with RSIAPI("RSI_EthernetConfig.xml") as api: api.start() if api.wait_for_connection(timeout=10.0): # Send a 10mm correction in X api.motion.update_cartesian(X=10.0) # Read current TCP position pose = api.motion.get_current_pose() print(f"TCP: X={pose['X']}, Y={pose['Y']}, Z={pose['Z']}") else: print("No robot connection within 10s") # api.stop() called automatically ``` Without the context manager: ```python api = RSIAPI("RSI_EthernetConfig.xml", rsi_mode="relative") api.start() api.wait_for_connection() api.motion.update_cartesian(X=5.0, Y=-3.0) api.stop() ``` --- ## Configuration RSIPI reads `RSI_EthernetConfig.xml` to determine network settings and which variables are exchanged with the robot. ```xml 10.10.10.10 64000 ImFree FALSE ``` Key points: - `DEF_` prefixed tags are expanded internally (e.g., `DEF_RIst` becomes `RIst: {X, Y, Z, A, B, C}`). - `HOLDON="1"` means the last value is held if no new value is sent. - SEND variables are read via `api.monitoring`, RECEIVE variables are written via `api.motion`, `api.io`, etc. - The config must match the RSI object configuration on the KUKA controller. --- ## API Reference ### Core Lifecycle ```python api = RSIAPI( config_file="RSI_EthernetConfig.xml", rsi_mode="relative", # "absolute" or "relative" -- must match KRL max_cartesian_rate=0.5, # Max mm/cycle for RKorr (0 = unlimited) max_joint_rate=0.1, # Max deg/cycle for AKorr (0 = unlimited) cycle_time=0.004 # 0.004 = 4ms/250Hz, 0.012 = 12ms/83Hz ) api.start() # Start UDP listener in background thread api.wait_for_connection(10.0) # Block until first robot packet (returns bool) api.is_running() # Check if communication is active api.stop() # Graceful shutdown api.reconnect() # Restart network with fresh resources ``` ### `api.motion` -- Motion Control ```python # Cartesian corrections (RKorr) -- mm for XYZ, degrees for ABC api.motion.update_cartesian(X=10.0, Y=-5.0, Z=0.0) api.motion.update_cartesian(A=2.5, B=0.0, C=0.0) # Joint corrections (AKorr) -- degrees api.motion.update_joints(A1=5.0, A2=-3.0) # Read current state pose = api.motion.get_current_pose() # {X, Y, Z, A, B, C} joints = api.motion.get_current_joints() # {A1, A2, A3, A4, A5, A6} # External axes api.motion.move_external_axis("E1", 500.0) # Tech parameters (runtime motion adjustment) api.motion.adjust_speed("Tech.T21", 0.5) ``` #### Trajectories ```python # Generate linear trajectory traj = api.motion.generate_trajectory( start={"X": 0, "Y": 0, "Z": 500}, end={"X": 100, "Y": 0, "Z": 500}, steps=50, space="cartesian" ) # Execute (blocking) api.motion.execute_trajectory(traj, space="cartesian", rate=0.012) # Or generate + execute in one call api.motion.move_cartesian_trajectory( {"X": 0, "Y": 0, "Z": 500}, {"X": 100, "Y": 0, "Z": 500}, steps=50, rate=0.02 ) api.motion.move_joint_trajectory( {"A1": 0, "A2": 0, "A3": 0, "A4": 0, "A5": 0, "A6": 0}, {"A1": 30, "A2": -15, "A3": 45, "A4": 0, "A5": 30, "A6": 0}, steps=100, rate=0.4 ) # Cancel a running trajectory from another thread api.motion.cancel_trajectory() ``` #### Trajectory Queue ```python api.motion.queue_cartesian_trajectory(p0, p1, steps=50) api.motion.queue_cartesian_trajectory(p1, p2, steps=50) api.motion.queue_joint_trajectory(j0, j1, steps=100, rate=0.4) print(api.motion.get_queue()) # Metadata for queued items api.motion.execute_queued_trajectories() # Run all in sequence api.motion.clear_queue() # Discard without executing ``` #### Geometric Primitives ```python # Circular arc arc = api.motion.generate_arc( center={"X": 100, "Y": 0, "Z": 500}, radius=50.0, start_angle=0, end_angle=90, steps=50, plane="XY" ) # Full circle circle = api.motion.generate_circle( center={"X": 100, "Y": 0, "Z": 500}, radius=50.0, steps=100, plane="XY" ) # Spiral spiral = api.motion.generate_spiral( center={"X": 100, "Y": 0, "Z": 500}, start_radius=10.0, end_radius=50.0, pitch=5.0, revolutions=5, steps=200, plane="XY", axis="Z" ) api.motion.execute_trajectory(arc, space="cartesian") ``` #### Velocity Profiles ```python traj = api.motion.generate_trajectory(p0, p1, steps=100) # Trapezoidal (bang-bang acceleration) profiled = api.motion.generate_velocity_profile( traj, max_velocity=200.0, max_acceleration=500.0, profile="trapezoidal" ) # S-curve (jerk-limited, smoother) profiled = api.motion.generate_velocity_profile( traj, max_velocity=200.0, max_acceleration=500.0, profile="s-curve" ) # Each element is (waypoint_dict, velocity_float) for waypoint, velocity in profiled: print(f"Velocity: {velocity:.2f} mm/s") ``` #### Path Blending ```python traj1 = api.motion.generate_trajectory(p0, p1, 50) traj2 = api.motion.generate_trajectory(p1, p2, 50) blended = api.motion.blend_trajectories( traj1, traj2, blend_radius=10.0, # mm from junction blend_steps=20 ) api.motion.execute_trajectory(blended) ``` #### Coordinate Transforms ```python world_pose = api.motion.transform_coordinates( pose={"X": 100, "Y": 0, "Z": 500}, from_frame="BASE", to_frame="WORLD", frame_offset={"X": 500, "Y": 200, "Z": 0} ) ``` ### `api.io` -- Digital I/O ```python # Set output by channel number api.io.set_output(1, True) # Digout.o1 = ON api.io.set_output(3, False) # Digout.o3 = OFF # Generic toggle (any group) api.io.toggle("DiO", "1", True) # Read input if api.io.get_input(1): # Digin.i1 print("Sensor triggered") # Timed pulse (blocking) api.io.pulse(2, duration=0.1) # 100ms pulse on Digout.o2 ``` ### `api.krl` -- KRL Coordination ```python # Wait for KRL to set a digital input (synchronization) if api.krl.wait_for_signal(3, timeout=10.0): print("KRL ready") # Signal KRL that Python is done api.krl.signal_complete(2) # Sets Digout.o2 = HIGH # Pass data to KRL via Tech.C variables (slots 11-199) api.krl.write_param("C12", 120.0) # KRL reads $TECH.C[12] api.krl.write_param(13, -50.0) # Read data from KRL via Tech.T variables force = api.krl.read_param("T11") # KRL writes $TECH.T[11] actual_x = api.krl.read_param(12) # Parse KRL .src/.dat files to CSV api.krl.parse_to_csv("robot_prog.src", "robot_prog.dat", "output.csv") # Inject RSI commands into existing KRL program api.krl.inject_rsi("robot_prog.src", "robot_prog_rsi.src") ``` ### `api.safety` -- Safety Management ```python # Emergency stop (software-level, NOT safety-rated) api.safety.stop() api.safety.is_stopped() # True # Reset E-stop api.safety.reset() # Configure correction limits api.safety.set_limit("RKorr.X", -50.0, 50.0) api.safety.set_limit("AKorr.A1", -10.0, 10.0) # View all limits limits = api.safety.get_limits() for var, (lo, hi) in limits.items(): print(f"{var}: [{lo}, {hi}]") # Full status status = api.safety.status() # {"emergency_stop": False, "safety_override": False, "limits": {...}} # Override limits (use with extreme caution) api.safety.override(True) # ... calibration work ... api.safety.override(False) ``` ### `api.monitoring` -- Live Data ```python # Comprehensive snapshot data = api.monitoring.get_live_data() # {"position": {X,Y,Z,A,B,C}, "velocity": {...}, "force": {...}, "ipoc": 123456} # Individual reads pos = api.monitoring.get_position() # {X, Y, Z, A, B, C} force = api.monitoring.get_force() # {A1, A2, A3, A4, A5, A6} motor currents ipoc = api.monitoring.get_ipoc() # Interrupt point counter # NumPy/Pandas formats arr = api.monitoring.get_live_data_as_numpy() # shape (4, 6) df = api.monitoring.get_live_data_as_dataframe() # single-row DataFrame # Console watch (blocking, Ctrl+C to stop) api.monitoring.watch_network(duration=10, rate=0.2) ``` ### `api.diagnostics` -- Network Health ```python stats = api.diagnostics.get_stats() timing = api.diagnostics.get_timing() # cycle time, jitter quality = api.diagnostics.get_network_quality() # packet loss, IPOC gaps if not api.diagnostics.is_healthy(): for w in api.diagnostics.get_warnings(): print(f"Warning: {w}") if api.diagnostics.check_watchdog(): api.reconnect() print(api.diagnostics.format_stats()) # Network Diagnostics: # Cycle Time: 4.01ms (+/-0.12ms jitter) # Packet Loss: 0.05% # ... ``` ### `api.logging` -- CSV Logging ```python # Start logging (auto-generates filename in logs/) path = api.logging.start() print(path) # logs/17-04-2026_14-32-45.csv # Or specify filename api.logging.start("my_experiment.csv") api.logging.is_active() # True api.logging.stop() ``` Logs include British-format timestamps, all send/receive variables per cycle. Logging runs in a separate process to avoid interfering with the 4 ms control loop. ### `api.viz` -- Visualization ```python # Static plots from CSV logs api.viz.plot_static("logs/test.csv", "3d") api.viz.plot_static("logs/test.csv", "position") # Position vs time api.viz.plot_static("logs/test.csv", "velocity") api.viz.plot_static("logs/test.csv", "joints") api.viz.plot_static("logs/test.csv", "force") api.viz.plot_static("logs/test.csv", "2d_xy") # 2D projections # Deviation from planned path api.viz.plot_static("logs/actual.csv", "deviation", overlay_path="logs/planned.csv") # Comprehensive multi-plot visualization api.viz.visualize_csv_log("logs/test.csv") api.viz.visualize_csv_log("logs/test.csv", export=True) # Save to disk # Compare two runs api.viz.compare_runs("run1.csv", "run2.csv") # Live plotting (runs in background thread) api.viz.start_live_plot("3d", interval=100) # 100ms update api.viz.change_live_plot_mode("position") api.viz.stop_live_plot() ``` ### `api.tools` -- Utilities ```python # Low-level variable access api.tools.update_variable("RKorr.X", 10.0) api.tools.show_variables() # Print all available variables api.tools.show_config() # Network settings + variable structure api.tools.reset_variables() # Zero out corrections # Reports and comparison api.tools.generate_report("logs/test.csv", "pdf") diffs = api.tools.compare_runs("run1.csv", "run2.csv") ``` --- ## RSI Mode and Rate Limiting ### Absolute vs Relative Mode The `rsi_mode` parameter must match what your KRL program uses with `RSI_MOVECORR()`: | Mode | Behavior | Use Case | |------|----------|----------| | `"relative"` | Corrections are **added** to the programmed path each cycle. Sending `X=1.0` every cycle moves 1mm/cycle continuously. | Continuous adjustments, sensor feedback | | `"absolute"` | Corrections specify **total offset** from programmed path. Sending `X=10.0` holds 10mm offset regardless of how many cycles. | Target position offsets | ### Cycle Time KUKA RSI supports two cycle rates, configured on the robot controller side. RSIPI's network loop is reactive (it responds to whatever the robot sends), but the `cycle_time` parameter ensures diagnostics, health checks, and jitter warnings use the correct baseline: ```python # 4ms cycle / 250Hz (default) api = RSIAPI("RSI_EthernetConfig.xml", cycle_time=0.004) # 12ms cycle / 83Hz api = RSIAPI("RSI_EthernetConfig.xml", cycle_time=0.012) ``` | Cycle Time | Frequency | Use Case | |------------|-----------|----------| | `0.004` (4ms) | 250 Hz | High-frequency corrections, sensor feedback loops | | `0.012` (12ms) | 83 Hz | Standard motion corrections, less demanding applications | ### Rate Limiting Rate limiting caps the per-cycle change to prevent sudden jumps: ```python api = RSIAPI( "RSI_EthernetConfig.xml", rsi_mode="relative", max_cartesian_rate=0.5, # Max 0.5 mm per cycle max_joint_rate=0.1, # Max 0.1 deg per cycle cycle_time=0.004 # 4ms cycle ) ``` Set rates to `0.0` (default) to disable rate limiting. Clamping is applied in the network process right before the response is sent to the robot. --- ## Testing ### Echo Server RSIPI includes an echo server that simulates a KUKA controller for offline development: ```bash python -m RSIPI.rsi_echo_server ``` The echo server binds to the same UDP port as a real robot, sends XML state packets at 250 Hz, and accepts correction responses. Use it to test your control logic without hardware. ### Running with pytest ```bash pip install -e ".[dev]" pytest ``` Test files go in the `tests/` directory. The project uses `src` layout with `pythonpath = ["src"]` configured in `pyproject.toml`. --- ## Architecture ``` KUKA Robot Controller | UDP/XML (4ms cycle) | NetworkProcess <- multiprocessing.Process, owns the socket | Manager().dict() <- shared send_variables / receive_variables | RSIClient <- orchestrator: config, safety, network | RSIAPI <- runs RSIClient in daemon thread / | \ \ motion io krl safety monitoring logging viz diagnostics tools ``` - **NetworkProcess** runs in a separate OS process. It receives XML from the robot, parses it into `send_variables` (what the robot tells us), and builds the response XML from `receive_variables` (what we tell the robot). IPOC synchronization (`IPOC + 4`) is handled automatically. - **RSIClient** creates the `multiprocessing.Manager` dicts for cross-process variable sharing, initializes the `ConfigParser` and `SafetyManager`, and manages the network process lifecycle. - **RSIAPI** wraps RSIClient in a daemon thread and exposes the namespaced sub-APIs (`motion`, `io`, `krl`, etc.). The 4 ms cycle is driven by the robot controller, not by RSIPI. If a response is not sent within the cycle window, the robot uses the last held values (for `HOLDON="1"` variables) or drops to zero. --- ## Examples The `examples/` directory contains runnable scripts: | Script | Description | |--------|-------------| | `example_01_start_stop.py` | Basic lifecycle: connect, wait, disconnect | | `example_02_send_cartesian.py` | Send Cartesian corrections (RKorr) | | `example_03_send_joint.py` | Send joint corrections (AKorr) | | `example_04_external_axes.py` | Control external axes (E1, E2, ...) | | `example_05_digital_io.py` | Digital I/O: set outputs, read inputs, pulse | | `example_06_logging_csv.py` | Start/stop CSV logging | | `example_07_graphing_live.py` | Live 3D plot during operation | | `example_08_safety_limits.py` | Configure and test safety limits | | `example_09_trajectory_cartesian.py` | Generate and execute Cartesian trajectory | | `example_10_shutdown_safe.py` | Graceful shutdown pattern | Advanced examples: | Directory | Scripts | |-----------|---------| | `examples/advanced_motion/` | Velocity profiles, arcs/circles/spirals, path blending, coordinate transforms | | `examples/coordination/` | Python-KRL handshake, parameter passing via Tech variables, state machine coordination | --- ## CLI Start the interactive command-line interface: ```bash python -m RSIPI.rsi_cli --config RSI_EthernetConfig.xml ``` The CLI provides the same capabilities as the Python API through text commands: `start`, `stop`, `set `, `move_cartesian`, `log start`, `safety-stop`, etc. --- ## License MIT