583 lines
18 KiB
Markdown
583 lines
18 KiB
Markdown
# RSIPI: Robot Sensor Interface for Python
|
|
|
|
[](https://www.python.org/downloads/)
|
|
[](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
|
|
<ROOT>
|
|
<CONFIG>
|
|
<IP_NUMBER>10.10.10.10</IP_NUMBER> <!-- External PC IP -->
|
|
<PORT>64000</PORT> <!-- UDP port -->
|
|
<SENTYPE>ImFree</SENTYPE> <!-- XML root element name -->
|
|
<ONLYSEND>FALSE</ONLYSEND> <!-- FALSE = bidirectional -->
|
|
</CONFIG>
|
|
|
|
<!-- SEND: What the robot sends TO us (read-only from Python) -->
|
|
<SEND>
|
|
<ELEMENTS>
|
|
<ELEMENT TAG="DEF_RIst" TYPE="DOUBLE" INDX="INTERNAL" /> <!-- TCP position -->
|
|
<ELEMENT TAG="DEF_RSol" TYPE="DOUBLE" INDX="INTERNAL" /> <!-- Commanded position -->
|
|
<ELEMENT TAG="DEF_Delay" TYPE="LONG" INDX="INTERNAL" /> <!-- Packet delay count -->
|
|
<ELEMENT TAG="Digout.o1" TYPE="BOOL" INDX="2" /> <!-- Digital output state -->
|
|
</ELEMENTS>
|
|
</SEND>
|
|
|
|
<!-- RECEIVE: What the robot receives FROM us (writable from Python) -->
|
|
<RECEIVE>
|
|
<ELEMENTS>
|
|
<ELEMENT TAG="RKorr.X" TYPE="DOUBLE" INDX="1" HOLDON="1" /> <!-- Cartesian correction -->
|
|
<ELEMENT TAG="RKorr.Y" TYPE="DOUBLE" INDX="2" HOLDON="1" />
|
|
<ELEMENT TAG="RKorr.Z" TYPE="DOUBLE" INDX="3" HOLDON="1" />
|
|
<ELEMENT TAG="RKorr.A" TYPE="DOUBLE" INDX="4" HOLDON="1" />
|
|
<ELEMENT TAG="RKorr.B" TYPE="DOUBLE" INDX="5" HOLDON="1" />
|
|
<ELEMENT TAG="RKorr.C" TYPE="DOUBLE" INDX="6" HOLDON="1" />
|
|
<ELEMENT TAG="DiO" TYPE="LONG" INDX="8" HOLDON="1" /> <!-- Digital I/O -->
|
|
</ELEMENTS>
|
|
</RECEIVE>
|
|
</ROOT>
|
|
```
|
|
|
|
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 <var> <value>`, `move_cartesian`, `log start`, `safety-stop`, etc.
|
|
|
|
---
|
|
|
|
## License
|
|
|
|
MIT
|