RSI-PI/src/RSIPI/tools_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

295 lines
10 KiB
Python

"""Utility tools API namespace for RSIPI."""
import logging
import os
import json
from typing import Dict, Any, Union, TYPE_CHECKING
import pandas as pd
import matplotlib.pyplot as plt
if TYPE_CHECKING:
from .rsi_client import RSIClient
class ToolsAPI:
"""
Utility tools interface for KUKA RSI robot control.
Provides debugging, inspection, data comparison, and reporting utilities
for analyzing RSI performance and robot behavior.
"""
def __init__(self, client: 'RSIClient') -> None:
"""
Initialize ToolsAPI namespace.
Args:
client: RSIClient instance for variable access
"""
self.client = client
def update_variable(self, name: str, value: Union[float, int]) -> str:
"""
Low-level variable update with safety validation.
Direct access to send_variables for advanced users. Most users should
use higher-level methods like api.motion.update_cartesian() instead.
Args:
name: Variable name (e.g., 'IPOC', 'RKorr.X', 'Tech.C11')
value: New value to set
Returns:
Status message
Raises:
RSIVariableError: If variable not found
RSISafetyViolation: If value violates safety limits
Example:
>>> api.tools.update_variable('RKorr.X', 10.0)
'Updated RKorr.X to 10.0'
>>> api.tools.update_variable('Tech.C11', 42)
'Updated Tech.C11 to 42.0'
Note:
This bypasses higher-level abstractions and directly modifies
send_variables. Use with caution and prefer namespace-specific
methods when available.
"""
from .exceptions import RSIVariableError
# receive_variables = what the robot receives from us (RKorr, AKorr, DiO, Tech.C, etc.)
# send_variables = what the robot sends to us (RIst, RSol, IPOC, etc.)
# User corrections go into receive_variables.
target = self.client.receive_variables
if "." in name:
parent, child = name.split(".", 1)
full_path = f"{parent}.{child}"
if parent in target:
current = dict(target[parent])
safe_value = self.client.safety_manager.validate(full_path, float(value))
current[child] = safe_value
target[parent] = current
logging.debug(f"Updated {name} to {safe_value}")
return f"Updated {name} to {safe_value}"
else:
raise RSIVariableError(f"Parent variable '{parent}' not found in receive_variables")
else:
safe_value = self.client.safety_manager.validate(name, float(value))
target[name] = safe_value
logging.debug(f"Updated {name} to {safe_value}")
return f"Updated {name} to {safe_value}"
def show_variables(self) -> None:
"""
Print available send/receive variables to console.
Displays a formatted list of all configured RSI variables with their
nested structure. Useful for debugging and discovering available data.
Example:
>>> api.tools.show_variables()
Send Variables:
- IPOC
- RKorr: X, Y, Z, A, B, C
- AKorr: A1, A2, A3, A4, A5, A6
- Tech: C11, C12, C13, ... T11, T12, ...
Receive Variables:
- IPOC
- RIst: X, Y, Z, A, B, C
- RSol: X, Y, Z, A, B, C
- ASPos: A1, A2, A3, A4, A5, A6
- MaCur: A1, A2, A3, A4, A5, A6
"""
def format_grouped(var_dict):
output = []
for var, val in var_dict.items():
if isinstance(val, dict):
sub_vars = ', '.join(val.keys())
output.append(f"{var}: {sub_vars}")
else:
output.append(var)
return output
send_vars = format_grouped(self.client.send_variables)
receive_vars = format_grouped(self.client.receive_variables)
print("\nSend Variables:")
for item in send_vars:
print(f" - {item}")
print("\nReceive Variables:")
for item in receive_vars:
print(f" - {item}")
print()
def show_config(self) -> Dict[str, Any]:
"""
Retrieve configuration information.
Returns network settings and current variable structure from the
active RSI configuration.
Returns:
Dictionary containing:
- Network: IP, port, sentype, onlysend settings
- Send variables: Current send variable structure
- Receive variables: Current receive variable structure
Example:
>>> config = api.tools.show_config()
>>> print(config['Network'])
{'ip': '192.168.1.100', 'port': 49152, 'sentype': 'ImFree', 'onlysend': False}
>>> print(config['Send variables'].keys())
dict_keys(['IPOC', 'RKorr', 'AKorr', 'Tech'])
"""
return {
"Network": self.client.config_parser.get_network_settings(),
"Send variables": dict(self.client.send_variables),
"Receive variables": dict(self.client.receive_variables)
}
def reset_variables(self) -> str:
"""
Reset send variables to default values.
Calls the client's reset_send_variables() method if available,
otherwise returns a not-implemented message.
Returns:
Status message
Example:
>>> api.tools.reset_variables()
'Send variables reset to default values'
Note:
This typically resets correction values (RKorr, AKorr) to zero
and restores default Tech variable values. IPOC is not affected.
"""
if hasattr(self.client, 'reset_send_variables'):
self.client.reset_send_variables()
logging.info("Send variables reset to defaults")
return "Send variables reset to default values"
else:
return "reset_send_variables() not implemented on client"
@staticmethod
def generate_report(filename: str, format_type: str = "csv") -> str:
"""
Generate statistical report from CSV log file.
Analyzes recorded RSI data and produces summary statistics for
position, velocity, and other metrics.
Args:
filename: Path to CSV log file (with or without .csv extension)
format_type: Output format - 'csv', 'json', or 'pdf'
Returns:
Path to generated report file
Raises:
FileNotFoundError: If CSV file doesn't exist
ValueError: If format_type is unsupported or CSV has no position data
Example:
>>> api.tools.generate_report('logs/test_run.csv', 'pdf')
'Report saved as logs/test_run_report.pdf'
Note:
PDF reports include bar charts of max/mean position values.
CSV and JSON formats provide tabular statistical data.
"""
# Ensure filename ends with .csv
if not filename.endswith(".csv"):
filename += ".csv"
if not os.path.exists(filename):
raise FileNotFoundError(f"File not found: {filename}")
df = pd.read_csv(filename)
# Extract position columns
position_cols = [col for col in df.columns if col.startswith("Receive.RIst.")]
if not position_cols:
raise ValueError("No 'Receive.RIst' position columns found in CSV")
report_data = {
"Max Position": df[position_cols].max().to_dict(),
"Mean Position": df[position_cols].mean().to_dict(),
}
report_base = filename.replace(".csv", "")
output_path = f"{report_base}_report.{format_type.lower()}"
if format_type == "csv":
pd.DataFrame(report_data).T.to_csv(output_path)
elif format_type == "json":
with open(output_path, "w") as f:
json.dump(report_data, f, indent=4)
elif format_type == "pdf":
fig, ax = plt.subplots()
pd.DataFrame(report_data).T.plot(kind='bar', ax=ax)
ax.set_title("RSI Position Report")
plt.tight_layout()
plt.savefig(output_path)
plt.close(fig)
else:
raise ValueError(f"Unsupported format: {format_type}. Use 'csv', 'json', or 'pdf'.")
logging.info(f"Report generated: {output_path}")
return f"Report saved as {output_path}"
@staticmethod
def compare_runs(file1: str, file2: str) -> Dict[str, Dict[str, float]]:
"""
Compare two test run CSV files.
Calculates mean and max deviation between corresponding position
columns in two log files. Useful for repeatability analysis.
Args:
file1: Path to first CSV log file
file2: Path to second CSV log file
Returns:
Dictionary mapping column names to deviation statistics:
- mean_diff: Average absolute difference
- max_diff: Maximum absolute difference
Raises:
FileNotFoundError: If either file doesn't exist
Example:
>>> diffs = api.tools.compare_runs('run1.csv', 'run2.csv')
>>> for col, stats in diffs.items():
... print(f"{col}: mean={stats['mean_diff']:.3f}, max={stats['max_diff']:.3f}")
Receive.RIst.X: mean=0.234, max=1.456
Receive.RIst.Y: mean=0.178, max=0.892
Receive.RIst.Z: mean=0.312, max=1.023
Note:
Only compares columns present in both files. Typically used for
comparing repeatability of the same motion program.
"""
df1 = pd.read_csv(file1)
df2 = pd.read_csv(file2)
shared_cols = [col for col in df1.columns if col in df2.columns and col.startswith("Receive.RIst")]
diffs = {}
for col in shared_cols:
delta = abs(df1[col] - df2[col])
diffs[col] = {
"mean_diff": float(delta.mean()),
"max_diff": float(delta.max()),
}
logging.info(f"Compared {len(shared_cols)} position columns between runs")
return diffs