diff --git a/RSI Config Files/Changing Values/valuetests.rsi b/RSI Config Files/Changing Values/valuetests.rsi
new file mode 100644
index 0000000..0ef3dfb
--- /dev/null
+++ b/RSI Config Files/Changing Values/valuetests.rsi
@@ -0,0 +1,74 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/RSI Config Files/Changing Values/valuetests.rsi.diagram b/RSI Config Files/Changing Values/valuetests.rsi.diagram
new file mode 100644
index 0000000..6d2d98d
--- /dev/null
+++ b/RSI Config Files/Changing Values/valuetests.rsi.diagram
@@ -0,0 +1,126 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/RSI Config Files/Changing Values/valuetests.rsi.xml b/RSI Config Files/Changing Values/valuetests.rsi.xml
new file mode 100644
index 0000000..6b8f150
--- /dev/null
+++ b/RSI Config Files/Changing Values/valuetests.rsi.xml
@@ -0,0 +1,40 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/RSI Config Files/Changing Values/valuetests.xml b/RSI Config Files/Changing Values/valuetests.xml
new file mode 100644
index 0000000..808e46d
--- /dev/null
+++ b/RSI Config Files/Changing Values/valuetests.xml
@@ -0,0 +1,18 @@
+
+
+ 10.10.10.10
+ 64000
+ ImFree
+ FALSE
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/RSI Config Files/RSI Network/RSI_Ethernet.rsi b/RSI Config Files/RSI Network/RSI_Ethernet.rsi
new file mode 100644
index 0000000..0a5807e
--- /dev/null
+++ b/RSI Config Files/RSI Network/RSI_Ethernet.rsi
@@ -0,0 +1,201 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/RSI Config Files/RSI Network/RSI_Ethernet.rsi.diagram b/RSI Config Files/RSI Network/RSI_Ethernet.rsi.diagram
new file mode 100644
index 0000000..74ed22e
--- /dev/null
+++ b/RSI Config Files/RSI Network/RSI_Ethernet.rsi.diagram
@@ -0,0 +1,332 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/RSI Config Files/RSI Network/RSI_Ethernet.rsi.xml b/RSI Config Files/RSI Network/RSI_Ethernet.rsi.xml
new file mode 100644
index 0000000..254a028
--- /dev/null
+++ b/RSI Config Files/RSI Network/RSI_Ethernet.rsi.xml
@@ -0,0 +1,103 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/RSI Config Files/RSI Network/RSI_EthernetConfig.xml b/RSI Config Files/RSI Network/RSI_EthernetConfig.xml
new file mode 100644
index 0000000..3df7a58
--- /dev/null
+++ b/RSI Config Files/RSI Network/RSI_EthernetConfig.xml
@@ -0,0 +1,42 @@
+
+
+ 10.10.10.10
+ 64000
+ ImFree
+ FALSE
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/RSIPI/cli.py b/src/RSIPI/cli.py
new file mode 100644
index 0000000..cd3dd92
--- /dev/null
+++ b/src/RSIPI/cli.py
@@ -0,0 +1,53 @@
+import sys
+import time
+from src.RSIPI.rsi_api import RSIAPI
+
+class RSICommandLineInterface:
+ """Command-Line Interface for controlling RSI Client."""
+
+ def __init__(self, config_file):
+ """Initialize CLI with an RSI API instance."""
+ self.api = RSIAPI(config_file)
+ self.running = True
+
+ def run(self):
+ """Starts the CLI interaction loop."""
+ print("✅ RSI Command-Line Interface Started. Type 'help' for commands.")
+ while self.running:
+ command = input("RSI> ").strip().lower()
+ self.process_command(command)
+
+ def process_command(self, command):
+ """Processes user input commands."""
+ parts = command.split()
+ if not parts:
+ return
+
+ cmd = parts[0]
+
+ if cmd == "start":
+ self.api.start()
+ elif cmd == "stop":
+ self.api.stop()
+ elif cmd == "exit":
+ self.api.stop()
+ self.running = False
+ elif cmd == "help":
+ self.show_help()
+ else:
+ print("❌ Unknown command. Type 'help' for a list of commands.")
+
+ def show_help(self):
+ """Displays the list of available commands."""
+ print("""
+Available Commands:
+ start - Start the RSI client in the background
+ stop - Stop the RSI client
+ exit - Stop RSI and exit CLI
+ help - Show this command list
+ """)
+
+if __name__ == "__main__":
+ config_file = "RSI_EthernetConfig.xml"
+ cli = RSICommandLineInterface(config_file)
+ cli.run()
diff --git a/src/RSIPI/config_parser.py b/src/RSIPI/config_parser.py
new file mode 100644
index 0000000..f38d90a
--- /dev/null
+++ b/src/RSIPI/config_parser.py
@@ -0,0 +1,138 @@
+import xml.etree.ElementTree as ET
+import logging
+
+class ConfigParser:
+ """Handles parsing the RSI config file and generating structured send/receive variables."""
+
+ def __init__(self, config_file):
+ """Initialize with the configuration file and process send/receive structures."""
+ self.config_file = config_file
+ self.internal_structure = {
+ "ComStatus": "String",
+ "RIst": {"X": 0, "Y": 0, "Z": 0, "A": 0, "B": 0, "C": 0},
+ "RSol": {"X": 0, "Y": 0, "Z": 0, "A": 0, "B": 0, "C": 0},
+ "ASPos": {"A1": 0, "A2": 0, "A3": 0, "A4": 0, "A5": 0, "A6": 0},
+ "ELPos": {"E1": 0, "E2": 0, "E3": 0, "E4": 0, "E5": 0, "E6": 0},
+ "ESPos": {"E1": 0, "E2": 0, "E3": 0, "E4": 0, "E5": 0, "E6": 0},
+ "MaCur": {"A1": 0, "A2": 0, "A3": 0, "A4": 0, "A5": 0, "A6": 0},
+ "MECur": {"E1": 0, "E2": 0, "E3": 0, "E4": 0, "E5": 0, "E6": 0},
+ "IPOC": 0,
+ "BMode": "Status",
+ "IPOSTAT": "",
+ "Delay": {"D": 0},
+ "EStr": "EStr Test",
+ "Tech.T2": {"T21": 0, "T22": 0, "T23": 0, "T24": 0, "T25": 0, "T26": 0, "T27": 0, "T28": 0, "T29": 0, "T210": 0},
+ "RKorr": {"X": 0, "Y": 0, "Z": 0, "A": 0, "B": 0, "C": 0}
+ }
+ self.network_settings = {}
+ self.receive_variables,self.send_variables = self.process_config()
+
+ # ✅ Rename Tech.XX keys to Tech in send and receive variables at the last step
+ self.rename_tech_keys(self.send_variables)
+ self.rename_tech_keys(self.receive_variables)
+
+ # ✅ Ensure IPOC is always present in send variables
+ if "IPOC" not in self.send_variables:
+ self.send_variables.update({"IPOC": 0})
+
+ print(f"✅ Final Send Variables: {self.send_variables}")
+ print(f"✅ Final Receive Variables: {self.receive_variables}")
+
+ def process_config(self):
+ """Parse the config file and create structured send and receive variables."""
+ send_vars = {}
+ receive_vars = {}
+
+ try:
+ tree = ET.parse(self.config_file)
+ root = tree.getroot()
+
+ # ✅ Extract network settings
+ config = root.find("CONFIG")
+ self.network_settings = {
+ "ip": config.find("IP_NUMBER").text.strip(),
+ "port": int(config.find("PORT").text.strip()),
+ "sentype": config.find("SENTYPE").text.strip(),
+ "onlysend": config.find("ONLYSEND").text.strip().upper() == "TRUE",
+ }
+
+ # ✅ Parse SEND section (values received from RSI)
+ send_section = root.find("SEND/ELEMENTS")
+ for element in send_section.findall("ELEMENT"):
+ tag = element.get("TAG").replace("DEF_", "")
+ indx = element.get("INDX", "")
+ var_type = element.get("TYPE", "")
+ print(f"🔍 Processing SEND: {tag} | INDX: {indx} | TYPE: {var_type}")
+ if tag != "FREE":
+ self.process_variable_structure(send_vars, tag, var_type, indx)
+
+ # ✅ Parse RECEIVE section (values sent to RSI)
+ receive_section = root.find("RECEIVE/ELEMENTS")
+ for element in receive_section.findall("ELEMENT"):
+ tag = element.get("TAG").replace("DEF_", "")
+ indx = element.get("INDX", "")
+ var_type = element.get("TYPE", "")
+ print(f"🔍 Processing RECEIVE: {tag} | INDX: {indx} | TYPE: {var_type}")
+ if tag != "FREE":
+ self.process_variable_structure(receive_vars, tag, var_type, indx)
+
+ logging.info("✅ Config processed successfully.")
+ except Exception as e:
+ logging.error(f"❌ Error processing config file: {e}")
+
+ # ✅ Ensure IPOC is always in send variables
+ if "IPOC" not in send_vars:
+ send_vars["IPOC"] = 0
+
+ print(f"✅ Processed Send Variables: {send_vars}")
+ print(f"✅ Processed Receive Variables: {receive_vars}")
+ return send_vars, receive_vars
+
+ def process_variable_structure(self, var_dict, tag, var_type, indx=""):
+ """Handles structured and simple variables based on internal structure and TYPE attribute."""
+ tag = tag.replace("DEF_", "") # ✅ Strip "DEF_" prefix
+
+ print(f"🔍 Assigning {tag}: INDX={indx}, TYPE={var_type}")
+
+ if indx == "INTERNAL":
+ if tag in self.internal_structure:
+ internal_value = self.internal_structure[tag]
+ var_dict[tag] = internal_value.copy() if isinstance(internal_value, dict) else internal_value
+ print(f"✅ INTERNAL Match: {tag} -> {var_dict[tag]}")
+ else:
+ print(f"❌ INTERNAL variable '{tag}' not found in internal dictionary.")
+ elif "." in tag: # ✅ Handle elements in the format XXX.ZZ
+ parent, subkey = tag.split(".", 1)
+ if parent not in var_dict:
+ var_dict[parent] = {} # ✅ Create parent dictionary if not exists
+ var_dict[parent][subkey] = self.get_default_value(var_type)
+ print(f"📂 Assigned '{tag}' as nested dictionary under '{parent}': {var_dict[parent]}")
+ else:
+ var_dict[tag] = self.get_default_value(var_type)
+ print(f"📄 Assigned Standard Value: '{tag}' -> {var_dict[tag]}")
+
+ def rename_tech_keys(self, var_dict):
+ """Rename Tech.XX keys to Tech in send and receive variables at the last step."""
+ tech_data = {}
+ for key in list(var_dict.keys()):
+ if key.startswith("Tech."):
+ tech_data.update(var_dict.pop(key)) # ✅ Merge all Tech.XX keys into Tech
+ if tech_data:
+ var_dict["Tech"] = tech_data # ✅ Store under Tech
+ print(f"✅ Renamed Tech.XX keys to 'Tech': {var_dict['Tech']}")
+
+ def get_default_value(self, var_type):
+ """Returns a default value based on TYPE."""
+ if var_type == "BOOL":
+ return False
+ elif var_type == "STRING":
+ return ""
+ elif var_type == "LONG":
+ return 0
+ elif var_type == "DOUBLE":
+ return 0.0
+ return None # Unknown type fallback
+
+ def get_network_settings(self):
+ """Return the extracted network settings."""
+ return self.network_settings
diff --git a/src/RSIPI/echo_server.py b/src/RSIPI/echo_server.py
new file mode 100644
index 0000000..7f47710
--- /dev/null
+++ b/src/RSIPI/echo_server.py
@@ -0,0 +1,137 @@
+import sys
+import os
+import socket
+import time
+import xml.etree.ElementTree as ET
+import logging
+import threading
+
+sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "..")))
+
+from src.RSIPI.rsi_config import RSIConfig
+
+# ✅ Configure Logging
+LOGGING_ENABLED = True
+
+if LOGGING_ENABLED:
+ logging.basicConfig(
+ filename="echo_server.log",
+ level=logging.DEBUG,
+ format="%(asctime)s [%(levelname)s] %(message)s",
+ datefmt="%Y-%m-%d %H:%M:%S"
+ )
+
+class EchoServer:
+ """A UDP echo server that sends test messages to the RSI client and logs received messages."""
+
+ def __init__(self, config_file, delay_ms=4):
+ self.config = RSIConfig(config_file)
+ network_settings = self.config.get_network_settings()
+
+ self.server_address = ("0.0.0.0", 50000)
+ self.client_address = ("127.0.0.1", network_settings["port"])
+ self.udp_socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
+ self.udp_socket.bind(self.server_address)
+
+ self.ipoc_value = 123456
+ self.delay_ms = delay_ms / 1000 # Convert milliseconds to seconds
+
+ self.send_variables = self.config.get_send_variables()
+ self.receive_variables = self.config.get_receive_variables()
+
+ self.running = True # ✅ Control flag for graceful shutdown
+ self.thread = threading.Thread(target=self.send_message, daemon=True)
+
+ logging.info(f"✅ Echo Server started on {self.server_address}")
+ logging.info(f"📡 Sending messages to RSI Client on port {network_settings['port']}")
+ logging.info(f"⏳ Message delay set to {delay_ms}ms")
+ print("✅ Echo Server Started")
+ print(f"📡 Sending messages to RSI Client on port {network_settings['port']}")
+
+ def generate_message(self):
+ """Generate a properly formatted RSI message dynamically based on the config file."""
+ root = ET.Element("Rob", Type="KUKA") # ✅ Correct root element
+
+ for tag, value in self.send_variables.items():
+ if isinstance(value, dict):
+ element = ET.SubElement(root, tag.split(".")[0]) # Create main element
+ for sub_key, sub_value in value.items():
+ if isinstance(sub_value, bool):
+ sub_value = int(sub_value) # Convert booleans to 0/1
+ element.set(sub_key, f"{sub_value:.1f}" if isinstance(sub_value, float) else str(sub_value))
+ else:
+ element = ET.SubElement(root, tag)
+ if isinstance(value, bool):
+ value = int(value) # Convert booleans to 0/1
+ element.text = f"{value:.1f}" if isinstance(value, float) else str(value)
+
+ ET.SubElement(root, "IPOC").text = str(self.ipoc_value)
+
+ message = ET.tostring(root, encoding="utf-8").decode()
+ logging.debug(f"📤 Sent XML Message:\n{message}")
+ print(f"📤 Sent XML Message:\n{message}")
+ return message
+
+ def send_message(self):
+ """Send messages every 4ms or 12ms without waiting for a response."""
+ while self.running:
+ try:
+ message = self.generate_message()
+ logging.debug(f"📡 Sending message to RSI client with IPOC={self.ipoc_value}...")
+ print(f"📡 Sending message to RSI client with IPOC={self.ipoc_value}...")
+ self.udp_socket.sendto(message.encode(), self.client_address) # ✅ Send without waiting
+
+ # ✅ Attempt to receive a response
+ self.udp_socket.settimeout(self.delay_ms)
+ try:
+ response, addr = self.udp_socket.recvfrom(1024)
+ received_message = response.decode()
+
+ logging.debug(f"📩 Received XML Message:\n{received_message}")
+ print(f"📩 Received XML Message:\n{received_message}")
+
+ except socket.timeout:
+ print("⚠️ No response received before next send cycle.")
+
+ time.sleep(self.delay_ms) # ✅ Apply user-defined delay (4ms or 12ms)
+ self.increment_ipoc() # ✅ Increment IPOC for next message
+
+ except ConnectionResetError:
+ logging.warning("⚠️ RSI Client is not responding. Retrying...")
+ print("⚠️ RSI Client is not responding. Retrying in 2 seconds...")
+ time.sleep(2) # ✅ Instead of crashing, wait and retry
+
+ except Exception as e:
+ logging.error(f"❌ Unexpected error: {e}")
+ print(f"❌ Unexpected error: {e}")
+
+ def increment_ipoc(self):
+ """Increment the IPOC value by 4 milliseconds for the next message."""
+ self.ipoc_value += 4
+ logging.debug(f"🔄 IPOC incremented to {self.ipoc_value}")
+ print(f"🔄 IPOC incremented to {self.ipoc_value}")
+
+ def start(self):
+ """Start the Echo Server message loop in a separate thread."""
+ self.running = True
+ self.thread.start()
+
+ def stop(self):
+ """Gracefully stop the Echo Server."""
+ print("🛑 Stopping Echo Server...")
+ logging.info("🛑 Stopping Echo Server...")
+ self.running = False # ✅ Stop the loop
+ self.thread.join() # ✅ Wait for the thread to finish
+ self.udp_socket.close() # ✅ Close the socket
+ logging.info("✅ Echo Server Stopped")
+ print("✅ Echo Server Stopped")
+
+if __name__ == "__main__":
+ config_file = "RSI_EthernetConfig.xml"
+ echo_server = EchoServer(config_file, delay_ms=4)
+ try:
+ echo_server.start()
+ while True:
+ time.sleep(1) # Keep main thread alive
+ except KeyboardInterrupt:
+ echo_server.stop()
diff --git a/src/RSIPI/graphing.py b/src/RSIPI/graphing.py
new file mode 100644
index 0000000..68001f9
--- /dev/null
+++ b/src/RSIPI/graphing.py
@@ -0,0 +1,121 @@
+import time
+import logging
+import pandas as pd
+import matplotlib.pyplot as plt
+import matplotlib.animation as animation
+
+# ✅ Configure Logging for Graphing Module
+LOGGING_ENABLED = True
+
+if LOGGING_ENABLED:
+ logging.basicConfig(
+ filename="graphing.log",
+ level=logging.DEBUG,
+ format="%(asctime)s [%(levelname)s] %(message)s",
+ datefmt="%d-%m-%Y %H:%M:%S"
+ )
+
+class RSIGraphing:
+ """Handles real-time and CSV-based graphing for RSI analysis."""
+
+ def __init__(self, csv_file=None):
+ """Initialize graphing for real-time plotting or CSV replay."""
+ self.csv_file = csv_file
+ self.fig, self.ax = plt.subplots(1, 2, figsize=(12, 5)) # ✅ Two subplots: Time-Series & X-Y Trajectory
+
+ # ✅ Titles and Labels
+ self.ax[0].set_title("Real-Time Position Tracking")
+ self.ax[0].set_xlabel("Time")
+ self.ax[0].set_ylabel("Position (mm)")
+ self.ax[1].set_title("2D Trajectory (X-Y)")
+ self.ax[1].set_xlabel("X Position")
+ self.ax[1].set_ylabel("Y Position")
+
+ # ✅ Data storage for plotting
+ self.time_data = []
+ self.x_data, self.y_data, self.z_data = [], [], []
+ self.actual_x, self.actual_y, self.actual_z = [], [], []
+ self.trajectory_x, self.trajectory_y = [], []
+
+ self.plot_mode = "TCP" # ✅ Toggle between "TCP" and "Joints"
+
+ if csv_file:
+ self.load_csv_log(csv_file)
+ self.plot_csv_data()
+ else:
+ self.ani = animation.FuncAnimation(self.fig, self.update_graph, interval=500, save_count=100)
+
+ plt.show()
+
+ def update_graph(self, frame):
+ """Update the live graph with new position data."""
+ timestamp = time.strftime("%H:%M:%S")
+ self.time_data.append(timestamp)
+
+ # ✅ Simulated Data for Testing (Replace with real-time data)
+ self.x_data.append(len(self.time_data) * 10)
+ self.y_data.append(len(self.time_data) * 5)
+ self.z_data.append(len(self.time_data) * 2)
+
+ self.actual_x.append(len(self.time_data) * 9.8)
+ self.actual_y.append(len(self.time_data) * 4.9)
+ self.actual_z.append(len(self.time_data) * 1.8)
+
+ # ✅ 2D Trajectory (X-Y)
+ self.trajectory_x.append(len(self.time_data) * 9.8)
+ self.trajectory_y.append(len(self.time_data) * 4.9)
+
+ # ✅ Clear and replot data
+ self.ax[0].clear()
+ self.ax[0].plot(self.time_data, self.x_data, label="Planned X", linestyle="dashed")
+ self.ax[0].plot(self.time_data, self.y_data, label="Planned Y", linestyle="dashed")
+ self.ax[0].plot(self.time_data, self.z_data, label="Planned Z", linestyle="dashed")
+
+ self.ax[0].plot(self.time_data, self.actual_x, label="Actual X", marker="o")
+ self.ax[0].plot(self.time_data, self.actual_y, label="Actual Y", marker="o")
+ self.ax[0].plot(self.time_data, self.actual_z, label="Actual Z", marker="o")
+
+ self.ax[0].legend()
+ self.ax[0].set_title(f"Real-Time {self.plot_mode} Position")
+ self.ax[0].set_xlabel("Time")
+ self.ax[0].set_ylabel("Position (mm)")
+
+ # ✅ 2D Trajectory (X-Y)
+ self.ax[1].clear()
+ self.ax[1].plot(self.trajectory_x, self.trajectory_y, label="Actual Path", marker="o")
+ self.ax[1].set_title("2D Trajectory (X-Y)")
+ self.ax[1].set_xlabel("X Position")
+ self.ax[1].set_ylabel("Y Position")
+ self.ax[1].legend()
+
+ def load_csv_log(self, filename):
+ """Load a CSV log and replay motion."""
+ df = pd.read_csv(filename)
+ self.time_data = df["Timestamp"].tolist()
+ self.trajectory_x = df["Receive.RIst.X"].tolist()
+ self.trajectory_y = df["Receive.RIst.Y"].tolist()
+
+ logging.info(f"Loaded CSV log: {filename}")
+
+ def plot_csv_data(self):
+ """Plot data from a CSV file for post-analysis."""
+ plt.figure(figsize=(10, 5))
+ plt.plot(self.trajectory_x, self.trajectory_y, label="Planned Path", linestyle="dashed")
+ plt.scatter(self.trajectory_x, self.trajectory_y, color='red', label="Actual Path", marker="o")
+ plt.title("CSV Replay: 2D Trajectory (X-Y)")
+ plt.xlabel("X Position")
+ plt.ylabel("Y Position")
+ plt.legend()
+ plt.show()
+
+if __name__ == "__main__":
+ import argparse
+
+ parser = argparse.ArgumentParser(description="RSI Graphing Utility")
+ parser.add_argument("--csv", help="Path to CSV file for replay mode", type=str)
+ args = parser.parse_args()
+
+ if args.csv:
+ RSIGraphing(csv_file=args.csv)
+ else:
+ RSIGraphing() # Run in live mode if no CSV is provided
diff --git a/src/RSIPI/main.py b/src/RSIPI/main.py
new file mode 100644
index 0000000..0e2a503
--- /dev/null
+++ b/src/RSIPI/main.py
@@ -0,0 +1,11 @@
+from src.RSIPI.rsi_client import RSIClient
+import rsi_api
+from time import sleep
+
+if __name__ == "__main__":
+ config_file = "RSI_EthernetConfig.xml" # Ensure this file exists in the working directory
+ client = RSIClient(config_file)
+ client.start()
+ sleep(5)
+ # client.update_variable("EStr", "Testing 123 Testing")
+ print("done")
\ No newline at end of file
diff --git a/src/RSIPI/network_process.py b/src/RSIPI/network_process.py
new file mode 100644
index 0000000..3e80969
--- /dev/null
+++ b/src/RSIPI/network_process.py
@@ -0,0 +1,106 @@
+import multiprocessing
+import socket
+import time
+import logging
+from config_parser import ConfigParser
+import sys
+import os
+import socket
+import xml.etree.ElementTree as ET
+import multiprocessing
+from xml_handler import XMLGenerator # ✅ Import XML generator module
+from config_parser import ConfigParser # ✅ Import the updated config parser
+
+
+import multiprocessing
+import socket
+import logging
+from config_parser import ConfigParser
+from xml_handler import XMLGenerator
+
+class NetworkProcess(multiprocessing.Process):
+ """Handles UDP communication in a separate process."""
+
+ def __init__(self, ip, port, send_variables, receive_variables, stop_event, config_parser):
+ super().__init__()
+ self.send_variables = send_variables
+ self.receive_variables = receive_variables
+ self.stop_event = stop_event
+ self.config_parser = config_parser
+ self.udp_socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
+
+ # ✅ Fallback IP handling
+ self.client_address = (ip, port)
+ if not self.is_valid_ip(ip):
+ logging.warning(f"Invalid IP address '{ip}' detected. Falling back to '0.0.0.0'.")
+ print(f"⚠️ Invalid IP '{ip}', falling back to '0.0.0.0'.")
+ self.client_address = ('0.0.0.0', port)
+ else:
+ self.client_address = (ip, port)
+
+ try:
+ self.udp_socket.bind(self.client_address)
+ logging.info(f"✅ Network process initialized on {self.client_address}")
+ except OSError as e:
+ logging.error(f"❌ Failed to bind to {self.client_address}: {e}")
+ raise
+
+ self.controller_ip_and_port = None
+
+ def is_valid_ip(self, ip):
+ """Check if an IP address is valid and can be bound on this machine."""
+ try:
+ socket.inet_aton(ip) # Validate format
+ with socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as s:
+ s.bind((ip, 0)) # Try binding
+ return True
+ except (socket.error, OSError):
+ return False
+
+
+ def run(self):
+ """Start the network loop."""
+ print("[DEBUG] Network process started.")
+ while not self.stop_event.is_set():
+ try:
+ print("[DEBUG] Waiting for incoming message...")
+ self.udp_socket.settimeout(5)
+ data_received, self.controller_ip_and_port = self.udp_socket.recvfrom(1024)
+ message = data_received.decode()
+
+ print(f"[DEBUG] Received message: {message}")
+ self.process_received_data(message)
+
+ # ✅ Generate the send XML using the updated XMLGenerator
+ send_xml = XMLGenerator.generate_send_xml(self.send_variables, self.config_parser.network_settings)
+ print(f"[DEBUG] Sending response: {send_xml}")
+
+ self.udp_socket.sendto(send_xml.encode(), self.controller_ip_and_port)
+
+ except socket.timeout:
+ print("[WARNING] No message received within timeout period.")
+ except Exception as e:
+ print(f"[ERROR] Network process error: {e}")
+
+
+ def stop_network(self):
+ """Safely stop the network process."""
+ if self.udp_socket:
+ self.udp_socket.close()
+ print("✅ Network socket closed.")
+
+ def process_received_data(self, xml_string):
+ """Parse incoming XML and update shared variables."""
+ try:
+ root = ET.fromstring(xml_string)
+ for element in root:
+ if element.tag in self.receive_variables:
+ if len(element.attrib) > 0: # ✅ Handle structured data (dictionaries)
+ self.receive_variables[element.tag] = {k: float(v) for k, v in element.attrib.items()}
+ else:
+ self.receive_variables[element.tag] = element.text
+
+ print(f"[DEBUG] Updated received variables: {self.receive_variables}")
+ except Exception as e:
+ print(f"[ERROR] Error parsing received message: {e}")
+
diff --git a/src/RSIPI/rsi_api.py b/src/RSIPI/rsi_api.py
new file mode 100644
index 0000000..410f1fd
--- /dev/null
+++ b/src/RSIPI/rsi_api.py
@@ -0,0 +1,80 @@
+import threading
+import time
+import inspect
+
+
+class RSIAPI:
+ def __init__(self):
+ """Initialize RSIAPI with shared send variables."""
+ self.shared_send_variables = {} # Ensure this is always initialized
+ self.running = False # RSI status
+ print(f"✅ RSIAPI instance created from: {inspect.stack()[1].filename}")
+ print(f"✅ shared_send_variables initialized: {self.shared_send_variables}")
+
+ def start_rsi(self):
+ """Simulate RSI client startup."""
+ print("\n🚀 Starting RSI Client...")
+ self.running = True
+ self.shared_send_variables["EStr"] = "RSI Started" # Default value
+ print(f"✅ RSI Running: {self.running}")
+ print(f"📌 Initial shared_send_variables: {self.shared_send_variables}")
+
+ # Run a separate thread to simulate RSI process
+ rsi_thread = threading.Thread(target=self.rsi_loop, daemon=True)
+ rsi_thread.start()
+
+ def rsi_loop(self):
+ """Simulate RSI running in the background."""
+ while self.running:
+ print(f"🔄 RSI Loop Running... Current EStr: {self.shared_send_variables.get('EStr', 'N/A')}")
+ time.sleep(2) # Simulate 2-second update intervals
+
+ def update_variable(self, variable, value):
+ """Update a variable in shared_send_variables."""
+ print("\n🔍 Debugging update_variable()")
+ print(f"🔹 Checking if shared_send_variables exists: {hasattr(self, 'shared_send_variables')}")
+
+ if not hasattr(self, "shared_send_variables"):
+ print("❌ Error: shared_send_variables is missing!")
+ return
+
+ if variable in self.shared_send_variables:
+ self.shared_send_variables[variable] = value
+ print(f"✅ Updated {variable} to {value}")
+ else:
+ print(f"⚠️ Warning: Variable '{variable}' not found in shared_send_variables.")
+ print(f"📌 Available variables: {list(self.shared_send_variables.keys())}")
+
+ def stop_rsi(self):
+ """Stop the RSI process."""
+ print("\n🛑 Stopping RSI Client...")
+ self.running = False
+ self.shared_send_variables["EStr"] = "RSI Stopped"
+ print(f"✅ RSI Stopped. Final EStr: {self.shared_send_variables['EStr']}")
+
+
+# ==============================
+# ✅ TEST CODE: Start RSI & Change Variables
+# ==============================
+
+if __name__ == "__main__":
+ print("\n🚀 Starting RSIAPI Test...\n")
+
+ # Step 1: Create an instance of RSIAPI
+ api = RSIAPI()
+
+ # Step 2: Start RSI
+ api.start_rsi()
+
+ # Step 3: Wait 3 seconds and update `EStr`
+ time.sleep(3)
+ print("\n🛠 Updating 'EStr' variable to 'Testing 123'...")
+ api.update_variable("EStr", "Testing 123")
+
+ # Step 4: Wait another 3 seconds and update `EStr` again
+ time.sleep(3)
+ print("\n🛠 Updating 'EStr' variable to 'Final Test Value'...")
+ api.update_variable("EStr", "Final Test Value")
+
+ # Step 5: Stop RSI after 5 more seconds
+
diff --git a/src/RSIPI/rsi_client.py b/src/RSIPI/rsi_client.py
new file mode 100644
index 0000000..24b9a80
--- /dev/null
+++ b/src/RSIPI/rsi_client.py
@@ -0,0 +1,72 @@
+import logging
+import multiprocessing
+import time
+from src.RSIPI.config_parser import ConfigParser
+from src.RSIPI.network_process import NetworkProcess
+
+class RSIClient:
+ """Main RSI API class that integrates network, config handling, and message processing."""
+
+ def __init__(self, config_file):
+ """Initialize the RSI client and set up logging and networking."""
+ logging.info(f"📂 Loading RSI configuration from {config_file}...")
+
+ # Load configuration
+ self.config_parser = ConfigParser(config_file)
+ network_settings = self.config_parser.get_network_settings()
+
+ self.manager = multiprocessing.Manager()
+ self.send_variables = self.manager.dict(self.config_parser.send_variables)
+ self.receive_variables = self.manager.dict(self.config_parser.receive_variables)
+ self.stop_event = multiprocessing.Event()
+
+ logging.info(f"🚀 Starting network process on {network_settings['ip']}:{network_settings['port']}...")
+
+ # ✅ Corrected constructor call with all necessary parameters
+ self.network_process = NetworkProcess(
+ network_settings["ip"],
+ network_settings["port"],
+ self.send_variables,
+ self.receive_variables,
+ self.stop_event,
+ self.config_parser
+ )
+
+ self.network_process.start()
+
+ def start(self):
+ """Keep the client running and allow periodic debugging."""
+ logging.info("✅ RSI Client Started")
+ print("✅ RSI Client Started. Press CTRL+C to stop.")
+
+ try:
+ while not self.stop_event.is_set():
+ time.sleep(2)
+ except KeyboardInterrupt:
+ self.stop()
+ except Exception as e:
+ logging.error(f"❌ RSI Client encountered an error: {e}")
+ print(f"❌ RSI Client encountered an error: {e}")
+
+ def stop(self):
+ """Stop the network process safely and close resources."""
+ logging.info("🛑 Stopping RSI Client...")
+ print("🛑 Stopping RSI Client...")
+
+ self.stop_event.set() # ✅ Signal all processes to stop
+
+ if self.network_process.is_alive():
+ self.network_process.terminate()
+ self.network_process.join()
+
+ logging.info("✅ RSI Client Stopped")
+ print("✅ RSI Client Stopped")
+
+if __name__ == "__main__":
+ config_file = "RSI_EthernetConfig.xml"
+ client = RSIClient(config_file)
+
+ try:
+ client.start()
+ except KeyboardInterrupt:
+ client.stop()
diff --git a/src/RSIPI/rsi_config.py b/src/RSIPI/rsi_config.py
new file mode 100644
index 0000000..ad06c7b
--- /dev/null
+++ b/src/RSIPI/rsi_config.py
@@ -0,0 +1,130 @@
+import xml.etree.ElementTree as ET
+import logging
+
+# ✅ Configure Logging (Allow it to be turned off)
+LOGGING_ENABLED = True # Change to False to disable logging
+
+if LOGGING_ENABLED:
+ logging.basicConfig(
+ filename="rsi_config.log", # Save logs to file
+ level=logging.DEBUG, # Log everything for debugging
+ format="%(asctime)s [%(levelname)s] %(message)s",
+ datefmt="%Y-%m-%d %H:%M:%S"
+ )
+
+class RSIConfig:
+ """Processes the RSI config.xml file to determine communication parameters."""
+
+ internal = {
+ "ComStatus": "String",
+ "RIst": ["X", "Y", "Z", "A", "B", "C"],
+ "RSol": ["X", "Y", "Z", "A", "B", "C"],
+ "AIPos": ["A1", "A2", "A3", "A4", "A5", "A6"],
+ "ASPos": ["A1", "A2", "A3", "A4", "A5", "A6"],
+ "ELPos": ["E1", "E2", "E3", "E4", "E5", "E6"],
+ "ESPos": ["E1", "E2", "E3", "E4", "E5", "E6"],
+ "MaCur": ["A1", "A2", "A3", "A4", "A5", "A6"],
+ "MECur": ["E1", "E2", "E3", "E4", "E5", "E6"],
+ "IPOC": 0,
+ "BMode": "Status",
+ "IPOSTAT": "",
+ "Delay": ["D"],
+ "EStr": "EStr Test",
+ "Tech.C1": ["C11", "C12", "C13", "C14", "C15", "C16", "C17", "C18", "C19", "C110"],
+ "Tech.C2": ["C21", "C22", "C23", "C24", "C25", "C26", "C27", "C28", "C29", "C210"],
+ "Tech.T2": ["T21", "T22", "T23", "T24", "T25", "T26", "T27", "T28", "T29", "T210"],
+ }
+
+ def __init__(self, config_file):
+ self.config_file = config_file
+ self.network_settings = {}
+ self.send_variables = {}
+ self.receive_variables = {}
+ self.load_config()
+
+ def strip_def_prefix(self, tag):
+ """Remove 'DEF_' prefix from a variable name."""
+ return tag.replace("DEF_", "")
+
+ def process_internal_variable(self, tag):
+ """Process structured internal variables."""
+ if tag in self.internal:
+ if isinstance(self.internal[tag], list):
+ return {key: 0.0 for key in self.internal[tag]} # Store as dict
+ return self.internal[tag] # Store as default value
+ return None
+
+ def process_variable_structure(self, var_dict, tag, var_type):
+ """Handles structured variables (e.g., `Tech.T2`, `RKorr.X`)."""
+ if tag in self.internal:
+ var_dict[tag] = self.process_internal_variable(tag)
+ elif "." in tag:
+ base, subkey = tag.split(".", 1)
+ if base not in var_dict:
+ var_dict[base] = {}
+ var_dict[base][subkey] = self.get_default_value(var_type)
+ else:
+ var_dict[tag] = self.get_default_value(var_type)
+
+ def get_default_value(self, var_type):
+ """Returns a default value based on TYPE."""
+ if var_type == "BOOL":
+ return False
+ elif var_type == "STRING":
+ return ""
+ elif var_type == "LONG":
+ return 0
+ elif var_type == "DOUBLE":
+ return 0.0
+ return None # Unknown type fallback
+
+ def load_config(self):
+ """Load and parse the config.xml file, ensuring variables are structured correctly."""
+ try:
+ logging.info(f"Loading config file: {self.config_file}")
+ tree = ET.parse(self.config_file)
+ root = tree.getroot()
+
+ # Read Network Settings
+ config = root.find("CONFIG")
+ self.network_settings = {
+ "ip": config.find("IP_NUMBER").text.strip(),
+ "port": int(config.find("PORT").text.strip()),
+ "sentype": config.find("SENTYPE").text.strip(),
+ "onlysend": config.find("ONLYSEND").text.strip().upper() == "TRUE",
+ }
+ logging.info(f"Network settings loaded: {self.network_settings}")
+
+ # Read SEND Section (Values we send to RSI client)
+ send_section = root.find("SEND/ELEMENTS")
+ for element in send_section.findall("ELEMENT"):
+ tag = self.strip_def_prefix(element.get("TAG"))
+ var_type = element.get("TYPE")
+
+ if tag != "FREE": # ✅ Ignore `FREE` variables
+ self.process_variable_structure(self.send_variables, tag, var_type)
+
+ # Read RECEIVE Section (Values we receive from RSI client)
+ receive_section = root.find("RECEIVE/ELEMENTS")
+ for element in receive_section.findall("ELEMENT"):
+ tag = self.strip_def_prefix(element.get("TAG"))
+ var_type = element.get("TYPE")
+
+ if tag != "FREE": # ✅ Ignore `FREE` variables
+ self.process_variable_structure(self.receive_variables, tag, var_type)
+
+ logging.info("Configuration successfully loaded.")
+ logging.debug(f"Send Variables: {self.send_variables}")
+ logging.debug(f"Receive Variables: {self.receive_variables}")
+
+ except Exception as e:
+ logging.error(f"Error loading {self.config_file}: {e}")
+
+ def get_network_settings(self):
+ return self.network_settings
+
+ def get_send_variables(self):
+ return self.send_variables
+
+ def get_receive_variables(self):
+ return self.receive_variables
diff --git a/src/RSIPI/xml_handler.py b/src/RSIPI/xml_handler.py
new file mode 100644
index 0000000..8af2e09
--- /dev/null
+++ b/src/RSIPI/xml_handler.py
@@ -0,0 +1,35 @@
+import xml.etree.ElementTree as ET
+
+class XMLGenerator:
+ """Converts send and receive variables into properly formatted XML messages."""
+
+ @staticmethod
+ def generate_send_xml(send_variables, network_settings):
+ """Generate the send XML message dynamically based on send variables."""
+ root = ET.Element("Sen", Type=network_settings["sentype"]) # ✅ Root with Type from config
+
+ for key, value in send_variables.items():
+ if isinstance(value, dict): # ✅ Handle dictionaries as elements with attributes
+ element = ET.SubElement(root, key)
+ for sub_key, sub_value in value.items():
+ element.set(sub_key, f"{float(sub_value):.2f}")
+ else: # ✅ Handle standard elements with text values
+ ET.SubElement(root, key).text = str(value)
+
+ return ET.tostring(root, encoding="utf-8").decode()
+
+ @staticmethod
+ def generate_receive_xml(receive_variables):
+ """Generate the receive XML message dynamically based on receive variables."""
+ root = ET.Element("Rob", Type="KUKA") # ✅ Root with Type="KUKA"
+
+ for key, value in receive_variables.items():
+ if isinstance(value, dict): # ✅ Handle dictionaries as elements with attributes
+ element = ET.SubElement(root, key)
+ for sub_key, sub_value in value.items():
+ element.set(sub_key, f"{float(sub_value):.2f}")
+ else: # ✅ Handle standard elements with text values
+ ET.SubElement(root, key).text = str(value)
+
+ return ET.tostring(root, encoding="utf-8").decode()
+
diff --git a/src/__init__.py b/src/__init__.py
new file mode 100644
index 0000000..e69de29