RSI-PI/src/RSIPI/krl_api.py
Adam 098f53aed2 Fix send/receive variable inversion and network loop performance
The variable naming follows KUKA convention (robot's perspective) where
send_variables = what the robot sends us (RIst, RSol) and
receive_variables = what the robot receives from us (RKorr, DiO).
All APIs were using them backwards — writing corrections to
send_variables and building response XML from them, meaning the robot
never received actual corrections.

- network_handler: parse incoming XML into send_variables, build
  response XML from receive_variables, use local dict snapshots
  to avoid per-key Manager IPC within the 4ms cycle
- motion_api: check receive_variables for RKorr/AKorr
- tools_api: write user corrections to receive_variables
- monitoring_api: read robot state from send_variables
- io_api: read digital inputs from send_variables
- krl_api: read Tech.T params from send_variables
- rsi_cli/rsi_graphing: add --config arg, remove hardcoded paths
- main.py: test runner with all examples and multiprocessing guard
2026-04-17 18:54:10 +01:00

383 lines
14 KiB
Python

"""KRL program manipulation API namespace for RSIPI."""
import logging
import time
from typing import Optional, Union, TYPE_CHECKING
if TYPE_CHECKING:
from .rsi_client import RSIClient
class KRLAPI:
"""
KUKA Robot Language (KRL) program manipulation interface.
Provides utilities for parsing KRL programs, extracting coordinate data,
and injecting RSI control commands into existing KRL workflows.
"""
def __init__(self, client: 'RSIClient') -> None:
"""
Initialize KRLAPI namespace.
Args:
client: RSIClient instance (currently unused, reserved for future features)
"""
self.client = client
@staticmethod
def parse_to_csv(src_file: str, dat_file: str, output_file: str) -> str:
"""
Parse KRL source and data files, extract coordinates to CSV.
Reads .src (program logic) and .dat (position data) files, extracts
point definitions and movement commands, and exports to CSV format.
Args:
src_file: Path to KRL .src program file
dat_file: Path to KRL .dat data file
output_file: Path for output CSV file
Returns:
Status message indicating success or failure
Raises:
FileNotFoundError: If source or data files don't exist
Exception: If parsing fails
Example:
>>> api.krl.parse_to_csv('robot_prog.src', 'robot_prog.dat', 'output.csv')
'KRL data successfully exported to output.csv'
Note:
The parser extracts position data (E6POS, E6AXIS, FRAME) from the
.dat file and matches them with movement commands (PTP, LIN, CIRC)
from the .src file.
"""
try:
from .krl_to_csv_parser import KRLParser
parser = KRLParser(src_file, dat_file)
parser.parse_src()
parser.parse_dat()
parser.export_csv(output_file)
logging.info(f"KRL data exported to {output_file}")
return f"KRL data successfully exported to {output_file}"
except Exception as e:
logging.error(f"KRL parsing failed: {e}")
return f"Error parsing KRL files: {e}"
@staticmethod
def inject_rsi(input_krl: str, output_krl: Optional[str] = None, rsi_config: str = "RSIGatewayv1.rsi") -> str:
"""
Inject RSI control commands into a KRL program.
Automatically modifies a KRL .src file to include RSI initialization,
sensor communication, and cleanup code. This allows adding real-time
external control to existing robot programs.
Args:
input_krl: Path to input KRL .src file
output_krl: Optional output path (defaults to overwriting input)
rsi_config: RSI configuration file name (default: 'RSIGatewayv1.rsi')
Returns:
Status message indicating success or failure
Raises:
FileNotFoundError: If input KRL file doesn't exist
Exception: If injection fails
Example:
>>> # Modify in place
>>> api.krl.inject_rsi('robot_prog.src')
'RSI successfully injected into robot_prog.src'
>>> # Create new file
>>> api.krl.inject_rsi('robot_prog.src', 'robot_prog_rsi.src')
'RSI successfully injected into robot_prog_rsi.src'
Note:
The injection adds:
- RSI_CREATE() at program start
- RSI_ON() before motion commands
- RSI_MOVECORR() during movement
- RSI_OFF() after motion
This allows Python to send corrections during program execution.
"""
try:
from .inject_rsi_to_krl import inject_rsi_to_krl
inject_rsi_to_krl(input_krl, output_krl, rsi_config)
output_path = output_krl if output_krl else input_krl
logging.info(f"RSI injected into {output_path}")
return f"RSI successfully injected into {output_path}"
except Exception as e:
logging.error(f"RSI injection failed: {e}")
return f"RSI injection failed: {e}"
def wait_for_signal(
self,
channel: int,
timeout: float = 5.0,
check_interval: float = 0.01,
group: str = 'Digin'
) -> bool:
"""
Wait for KRL to set a specific I/O signal.
Blocks until the specified digital input becomes HIGH, or timeout occurs.
Commonly used for synchronization where Python waits for KRL to signal
completion of a robot operation before proceeding.
Args:
channel: Input channel number to monitor (1-based)
timeout: Maximum wait time in seconds (default: 5.0)
check_interval: Polling interval in seconds (default: 0.01 = 10ms)
group: I/O group name (default: 'Digin')
Returns:
True if signal received, False if timeout occurred
Example:
>>> # Wait for KRL to signal ready on input 3
>>> if api.krl.wait_for_signal(3, timeout=10.0):
... print("KRL signaled ready!")
... # Proceed with Python-side processing
... else:
... print("Timeout waiting for KRL signal")
>>> # Handshake pattern: Python waits → KRL signals → Python continues
>>> api.motion.update_cartesian(X=100) # Send correction
>>> api.krl.wait_for_signal(1) # Wait for KRL to acknowledge
>>> # KRL has processed the correction, safe to continue
Note:
This is a blocking operation. The check_interval determines polling
frequency - lower values provide faster response but higher CPU usage.
For typical RSI applications, 10-50ms intervals are appropriate.
Warning:
Ensure the KRL program actually sets the signal, otherwise this will
block until timeout. Consider using try/except for timeout handling.
"""
from .io_api import IOAPI
io_api = IOAPI(self.client)
start_time = time.time()
logging.debug(f"Waiting for signal on {group}.i{channel} (timeout: {timeout}s)")
while (time.time() - start_time) < timeout:
if io_api.get_input(channel, group=group):
elapsed = time.time() - start_time
logging.info(f"Signal received on {group}.i{channel} after {elapsed:.3f}s")
return True
time.sleep(check_interval)
logging.warning(f"Timeout waiting for signal on {group}.i{channel} after {timeout}s")
return False
def signal_complete(self, channel: int, group: str = 'Digout') -> str:
"""
Signal to KRL that Python-side operation is complete.
Sets the specified digital output HIGH to notify the KRL program that
Python has finished processing and KRL can proceed.
Args:
channel: Output channel number to signal on (1-based)
group: I/O group name (default: 'Digout')
Returns:
Status message indicating success
Raises:
RSIVariableError: If the output channel doesn't exist
RSISafetyViolation: If safety checks prevent the operation
Example:
>>> # Signal KRL that data processing is complete
>>> api.krl.signal_complete(2)
'Signaled complete on Digout.o2'
>>> # Typical coordination pattern:
>>> # 1. KRL sends data via Tech variables
>>> # 2. Python processes data
>>> result = api.krl.read_param('T11') # Read from KRL
>>> processed = result * 2.0 # Process
>>> api.krl.write_param('C11', processed) # Write result
>>> api.krl.signal_complete(1) # Tell KRL we're done
Note:
This sets the output and leaves it HIGH. If you need to reset the
signal after KRL acknowledges, use api.io.pulse() instead or manually
call api.io.set_output(channel, False) after KRL reads the signal.
See Also:
wait_for_signal() - Complementary method for waiting on inputs
api.io.pulse() - For temporary signal pulses
"""
from .io_api import IOAPI
io_api = IOAPI(self.client)
io_api.set_output(channel, True, group=group)
logging.info(f"Signaled complete on {group}.o{channel}")
return f"Signaled complete on {group}.o{channel}"
def write_param(self, slot: Union[int, str], value: float) -> str:
"""
Write parameter to Tech.C variable for KRL to read.
Tech.C variables (C11-C199) are "Control" parameters written by Python
and read by KRL programs. Used for passing numerical data from Python
to the robot controller.
Args:
slot: Tech.C slot number (11-199) or string like 'C11', 'c15'
value: Numerical value to write
Returns:
Status message indicating success
Raises:
ValueError: If slot number is invalid (must be 11-199)
RSIVariableError: If Tech variable group doesn't exist
RSISafetyViolation: If safety checks prevent the operation
Example:
>>> # Send target position to KRL
>>> api.krl.write_param(11, 650.5) # Tech.C11 = 650.5
'Updated Tech.C11 to 650.5'
>>> # Send multiple parameters
>>> api.krl.write_param('C12', 120.0) # X coordinate
>>> api.krl.write_param('C13', -50.0) # Y coordinate
>>> api.krl.write_param('C14', 800.0) # Z coordinate
>>> # KRL side reads with: target_x = $TECH.C[12]
Note:
KUKA RSI Tech variables support slots 11-199. Slots 1-10 are reserved.
The KRL program must read from $TECH.C[n] to access these values.
KRL Example:
```krl
DEF my_program()
DECL REAL target_x, target_y, target_z
; Python writes to C12, C13, C14
target_x = $TECH.C[12]
target_y = $TECH.C[13]
target_z = $TECH.C[14]
; Use coordinates...
END
```
See Also:
read_param() - Read Tech.T variables written by KRL
"""
# Normalize slot to integer
if isinstance(slot, str):
slot_str = slot.upper().strip()
if slot_str.startswith('C'):
slot_num = int(slot_str[1:])
else:
slot_num = int(slot_str)
else:
slot_num = int(slot)
# Validate slot range (KUKA reserves 1-10, usable range is 11-199)
if not (11 <= slot_num <= 199):
raise ValueError(f"Tech slot must be between 11-199, got {slot_num}")
from .tools_api import ToolsAPI
tools = ToolsAPI(self.client)
var_name = f"Tech.C{slot_num}"
result = tools.update_variable(var_name, value)
logging.debug(f"Wrote {value} to {var_name}")
return result
def read_param(self, slot: Union[int, str]) -> float:
"""
Read parameter from Tech.T variable written by KRL.
Tech.T variables (T11-T199) are "Transfer" parameters written by KRL
programs and read by Python. Used for passing numerical data from the
robot controller to Python.
Args:
slot: Tech.T slot number (11-199) or string like 'T11', 't15'
Returns:
Numerical value from the Tech.T slot
Raises:
ValueError: If slot number is invalid (must be 11-199)
RSIVariableError: If Tech variable group doesn't exist or slot not found
Example:
>>> # Read sensor value from KRL
>>> force = api.krl.read_param(11) # Read Tech.T11
>>> print(f"Force reading: {force}")
Force reading: 125.5
>>> # Read multiple parameters
>>> actual_x = api.krl.read_param('T12')
>>> actual_y = api.krl.read_param('T13')
>>> actual_z = api.krl.read_param('T14')
>>> # KRL side writes with: $TECH.T[12] = actual_pos.X
Note:
Tech.T variables are updated every RSI cycle (~4ms) from the robot
controller. Values reflect the KRL program's last write operation.
KRL Example:
```krl
DEF my_program()
DECL E6POS actual_pos
actual_pos = $POS_ACT
; Write to Tech.T for Python to read
$TECH.T[12] = actual_pos.X
$TECH.T[13] = actual_pos.Y
$TECH.T[14] = actual_pos.Z
END
```
Warning:
Ensure the KRL program has written to the Tech.T slot before reading,
otherwise you'll receive the default value (typically 0.0).
See Also:
write_param() - Write Tech.C variables for KRL to read
"""
from .exceptions import RSIVariableError
# Normalize slot to integer
if isinstance(slot, str):
slot_str = slot.upper().strip()
if slot_str.startswith('T'):
slot_num = int(slot_str[1:])
else:
slot_num = int(slot_str)
else:
slot_num = int(slot)
# Validate slot range
if not (11 <= slot_num <= 199):
raise ValueError(f"Tech slot must be between 11-199, got {slot_num}")
# Tech.T variables are written by KRL and sent to us (send_variables)
if 'Tech' in self.client.send_variables:
tech_dict = self.client.send_variables.get('Tech', {})
var_name = f"T{slot_num}"
if isinstance(tech_dict, dict) and var_name in tech_dict:
value = tech_dict[var_name]
logging.debug(f"Read {value} from Tech.{var_name}")
return float(value)
else:
raise RSIVariableError(f"Tech.{var_name} not found in send_variables")
else:
raise RSIVariableError("Tech variable group not found in send_variables")