import pandas as pd import numpy as np import json import matplotlib.pyplot as plt from .rsi_client import RSIClient from .rsi_graphing import RSIGraphing from .kuka_visualiser import Kukarsivisualiser from .krl_to_csv_parser import KRLParser from .inject_rsi_to_krl import inject_rsi_to_krl import threading from threading import Thread from .trajectory_planner import generate_trajectory, execute_trajectory import datetime class RSIAPI: """RSI API for programmatic control, including alerts, logging, graphing, and data retrieval.""" def __init__(self, config_file="RSI_EthernetConfig.xml"): """Initialize RSIAPI with an RSI client instance.""" self.thread = None self.client = RSIClient(config_file) self.graph_process = None # Store graphing process self.graphing_instance = None self.graph_thread = None# self.trajectory_queue = [] def start_rsi(self): """Start the RSI client in a background thread.""" self.thread = threading.Thread(target=self.client.start, daemon=True) self.thread.start() return "✅ RSI started in background." def stop_rsi(self): """Stop the RSI client.""" self.client.stop() return "✅ RSI stopped." def update_variable(self, variable, value): """Dynamically update an RSI variable.""" try: if isinstance(value, str) and str(value).replace('.', '', 1).isdigit(): value = float(value) if '.' in value else int(value) self.client.update_send_variable(variable, value) return f"✅ Updated {variable} to {value}" except Exception as e: return f"❌ Failed to update {variable}: {e}" def show_variables(self, group: str = "all"): """ Prints current values of send/receive variable groups. """ import pprint live_data = self.get_live_data() if group == "all": pprint.pprint(live_data) else: found = False for key in live_data: if key.lower() == group.lower(): pprint.pprint({key: live_data[key]}) found = True break if not found: print(f"⚠️ Group '{group}' not found. Try one of: {', '.join(live_data.keys())}") def get_variables(self): """Retrieve current send and receive variables.""" return { "send_variables": dict(self.client.send_variables), "receive_variables": dict(self.client.receive_variables) } def get_live_data(self): """Retrieve real-time RSI data for external processing.""" return { "position": self.client.receive_variables.get("RIst", {"X": 0, "Y": 0, "Z": 0}), "velocity": self.client.receive_variables.get("Velocity", {"X": 0, "Y": 0, "Z": 0}), "acceleration": self.client.receive_variables.get("Acceleration", {"X": 0, "Y": 0, "Z": 0}), "force": self.client.receive_variables.get("MaCur", {"A1": 0, "A2": 0, "A3": 0, "A4": 0, "A5": 0, "A6": 0}), "ipoc": self.client.receive_variables.get("IPOC", "N/A") } def get_live_data_as_numpy(self): data = self.get_live_data() flat = [] for section in ["position", "velocity", "acceleration", "force"]: values = list(data[section].values()) flat.append(values) max_len = max(len(row) for row in flat) for row in flat: row.extend([0] * (max_len - len(row))) # Pad missing values return np.array(flat) def get_live_data_as_dataframe(self): """Retrieve live RSI data as a Pandas DataFrame.""" data = self.get_live_data() return pd.DataFrame([data]) def get_ipoc(self): """Retrieve the latest IPOC value.""" return self.client.receive_variables.get("IPOC", "N/A") def reconnect(self): """Restart the network connection without stopping RSI.""" self.client.reconnect() return "✅ Network connection restarted." def toggle_digital_io(self, io, value): """Toggle digital I/O states.""" self.client.update_send_variable(io, int(value)) return f"✅ {io} set to {value}" def move_external_axis(self, axis, value): """Move an external axis.""" self.client.update_send_variable(f"ELPos.{axis}", float(value)) return f"✅ Moved {axis} to {value}" def correct_position(self, correction_type, axis, value): """Apply correction to RKorr or AKorr.""" self.client.update_send_variable(f"{correction_type}.{axis}", float(value)) return f"✅ Applied correction: {correction_type}.{axis} = {value}" def adjust_speed(self, tech_param, value): """Adjust speed settings.""" self.client.update_send_variable(tech_param, float(value)) return f"✅ Set {tech_param} to {value}" def reset_variables(self): """Reset send variables to default values.""" self.client.reset_send_variables() return "✅ Send variables reset to default values." def get_status(self): """Retrieve full RSI system status.""" return { "network": self.client.config_parser.get_network_settings(), "send_variables": dict(self.client.send_variables), "receive_variables": dict(self.client.receive_variables) } def start_logging(self, filename=None): if not filename: timestamp = datetime.datetime.now().strftime("%d-%m-%Y_%H-%M-%S") filename = f"logs/{timestamp}.csv" self.client.start_logging(filename) return filename def stop_logging(self): """Stop logging RSI data.""" self.client.stop_logging() return "🛑 CSV Logging stopped." def is_logging_active(self): """Return logging status.""" return self.client.is_logging_active() def start_graphing(self, mode="position", overlay=False, plan_file=None): if self.graph_thread and self.graph_thread.is_alive(): return "⚠️ Graphing is already running." def graph_runner(): self.graphing_instance = RSIGraphing(self.client, mode=mode, overlay=overlay, plan_file=plan_file) self.graph_thread = Thread(target=graph_runner, daemon=True) self.graph_thread.start() return f"✅ Graphing started in {mode} mode" def stop_graphing(self): if self.graphing_instance: self.graphing_instance.stop() return "🛑 Graphing stopped" return "⚠️ Graphing not running." # ✅ ALERT METHODS def enable_alerts(self, enable): """Enable or disable real-time alerts.""" self.client.enable_alerts(enable) return f"✅ Alerts {'enabled' if enable else 'disabled'}." def override_safety(self, enabled: bool): self.client.safety_manager.override_safety(enabled) def is_safety_overridden(self) -> bool: return self.client.safety_manager.is_safety_overridden() def set_alert_threshold(self, alert_type, value): """Set threshold for deviation or force alerts.""" if alert_type in ["deviation", "force"]: self.client.set_alert_threshold(alert_type, value) return f"✅ {alert_type.capitalize()} alert threshold set to {value}" return "❌ Invalid alert type. Use 'deviation' or 'force'." def generate_report(self, filename, format_type): """Generate a statistical report from movement data.""" data = self.client.get_movement_data() df = pd.DataFrame(data) report = { "Max Position Deviation": df.iloc[:, 1:].max().to_dict(), "Mean Position Deviation": df.iloc[:, 1:].mean().to_dict(), } path = f"{filename}.{format_type.lower()}" if format_type == "csv": df.to_csv(path, index=False) elif format_type == "json": with open(path, "w") as f: f.write(json.dumps(report)) elif format_type == "pdf": fig, ax = plt.subplots() df.plot(ax=ax) plt.savefig(path) else: raise ValueError(f"❌ Unsupported report format: {format_type}") return f"✅ Report saved as {path}" @staticmethod def visualise_csv_log(csv_file, export=False): """ Visualize CSV log file directly via RSIAPI. Args: csv_file (str): Path to CSV log file. export (bool): Whether to export the plots. """ visualizer = Kukarsivisualiser(csv_file) visualizer.plot_trajectory() visualizer.plot_joint_positions() visualizer.plot_force_trends() if export: visualizer.export_graphs() @staticmethod def parse_krl_to_csv(src_file, dat_file, output_file): """ Parses KRL files (.src, .dat) and exports coordinates to CSV. Args: src_file (str): Path to KRL .src file. dat_file (str): Path to KRL .dat file. output_file (str): Path for output CSV file. """ try: parser = KRLParser(src_file, dat_file) parser.parse_src() parser.parse_dat() parser.export_csv(output_file) return f"✅ KRL data successfully exported to {output_file}" except Exception as e: return f"❌ Error parsing KRL files: {e}" @staticmethod def inject_rsi(input_krl, output_krl=None, rsi_config="RSIGatewayv1.rsi"): """ Inject RSI commands into a KRL (.src) program file. Args: input_krl (str): Path to the input KRL file. output_krl (str, optional): Path to the output file (defaults to overwriting input). rsi_config (str, optional): RSI configuration file name. """ try: inject_rsi_to_krl(input_krl, output_krl, rsi_config) output_path = output_krl if output_krl else input_krl return f"✅ RSI successfully injected into {output_path}" except Exception as e: return f"❌ RSI injection failed: {e}" @staticmethod def generate_trajectory(start, end, steps=100, space="cartesian"): """Generates a linear trajectory (Cartesian or Joint).""" return generate_trajectory(start, end, steps, space) def execute_trajectory(self, trajectory, space="cartesian", rate=0.012): """Executes a trajectory using live updates.""" return execute_trajectory(self, trajectory, space, rate) def queue_trajectory(self, trajectory, space="cartesian", rate=0.012): """Adds a trajectory to the internal queue.""" self.trajectory_queue.append({ "trajectory": trajectory, "space": space, "rate": rate, }) def clear_trajectory_queue(self): """Clears all queued trajectories.""" self.trajectory_queue.clear() def get_trajectory_queue(self): """Returns current queued trajectories (metadata only).""" return [ {"space": item["space"], "steps": len(item["trajectory"]), "rate": item["rate"]} for item in self.trajectory_queue ] def execute_queued_trajectories(self): """Executes all queued trajectories in order.""" for item in self.trajectory_queue: self.execute_trajectory(item["trajectory"], item["space"], item["rate"]) self.clear_trajectory_queue() def export_movement_data(self, filename="movement_log.csv"): """ Exports recorded movement data (if available) to a CSV file. Assumes self.client.logger has stored entries. """ if not hasattr(self.client, "logger") or self.client.logger is None: raise RuntimeError("No logger attached to RSI client.") data = self.client.get_movement_data() if not data: raise RuntimeError("No data available to export.") df = pd.DataFrame(data) df.to_csv(filename, index=False) return f"✅ Movement data exported to {filename}" @staticmethod def compare_test_runs(file1, file2): """ Compares two test run CSV files. Returns a summary of average and max deviation for each axis. """ import pandas as pd 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": delta.mean(), "max_diff": delta.max(), } return diffs def update_cartesian(self, **kwargs): """ Update Cartesian correction values (RKorr). Example: update_cartesian(X=10.0, Y=0.0, Z=0.0) """ for axis, value in kwargs.items(): self.update_variable(f"RKorr.{axis}", float(value)) def update_joints(self, **kwargs): for axis, value in kwargs.items(): self.update_variable(f"AKorr.{axis}", float(value)) def watch_network(self, duration: float = None, rate: float = 0.2): """ Continuously prints current receive variables (and IPOC). If duration is None, runs until interrupted. """ import time import datetime print("📡 Watching network... Press Ctrl+C to stop.\n") start_time = time.time() try: while True: live_data = self.get_live_data() ipoc = live_data.get("IPOC", "N/A") rpos = live_data.get("RIst", {}) print(f"[{datetime.datetime.now().strftime('%H:%M:%S')}] IPOC: {ipoc} | RIst: {rpos}") time.sleep(rate) if duration and (time.time() - start_time) >= duration: break except KeyboardInterrupt: print("\n🛑 Stopped network watch.")