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