174 lines
6.3 KiB
Python
174 lines
6.3 KiB
Python
import socket
|
|
import time
|
|
import xml.etree.ElementTree as ET
|
|
import logging
|
|
import threading
|
|
from src.RSIPI.rsi_config import RSIConfig
|
|
|
|
# ✅ Toggle logging for debugging purposes
|
|
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:
|
|
"""
|
|
Simulates a KUKA RSI UDP server for testing.
|
|
|
|
- Responds to incoming RSI correction commands.
|
|
- Updates internal position state (absolute/relative).
|
|
- Returns structured XML messages (like a real robot).
|
|
"""
|
|
|
|
def __init__(self, config_file, delay_ms=4, mode="relative"):
|
|
"""
|
|
Initialise the echo server.
|
|
|
|
Args:
|
|
config_file (str): Path to RSI EthernetConfig.xml.
|
|
delay_ms (int): Delay between messages in milliseconds.
|
|
mode (str): Correction mode ("relative" or "absolute").
|
|
"""
|
|
self.config = RSIConfig(config_file)
|
|
network_settings = self.config.get_network_settings()
|
|
|
|
self.server_address = ("0.0.0.0", 50000) # Local bind
|
|
self.client_address = ("127.0.0.1", network_settings["port"]) # Client to echo back to
|
|
self.udp_socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
|
self.udp_socket.bind(self.server_address)
|
|
|
|
self.last_received = None
|
|
self.ipoc_value = 123456
|
|
self.delay_ms = delay_ms / 1000 # Convert to seconds
|
|
self.mode = mode.lower()
|
|
|
|
# Internal state to simulate robot values
|
|
self.state = {
|
|
"RIst": {k: 0.0 for k in ["X", "Y", "Z", "A", "B", "C"]},
|
|
"AIPos": {f"A{i}": 0.0 for i in range(1, 7)},
|
|
"ELPos": {f"E{i}": 0.0 for i in range(1, 7)},
|
|
"DiO": 0,
|
|
"DiL": 0
|
|
}
|
|
|
|
self.running = True
|
|
self.thread = threading.Thread(target=self.send_message, daemon=True)
|
|
|
|
logging.info(f"Echo Server started on {self.server_address}")
|
|
print(f"Echo Server started in {self.mode.upper()} mode.")
|
|
|
|
def receive_and_process(self):
|
|
"""
|
|
Handles one incoming UDP message and updates the internal state accordingly.
|
|
Supports RKorr, AKorr, DiO, DiL, and IPOC updates.
|
|
"""
|
|
try:
|
|
self.udp_socket.settimeout(self.delay_ms)
|
|
data, addr = self.udp_socket.recvfrom(1024)
|
|
xml_string = data.decode()
|
|
root = ET.fromstring(xml_string)
|
|
self.last_received = xml_string
|
|
|
|
for elem in root:
|
|
tag = elem.tag
|
|
if tag in ["RKorr", "AKorr"]:
|
|
for axis, value in elem.attrib.items():
|
|
value = float(value)
|
|
if tag == "RKorr" and axis in self.state["RIst"]:
|
|
# Apply Cartesian correction
|
|
if self.mode == "relative":
|
|
self.state["RIst"][axis] += value
|
|
else:
|
|
self.state["RIst"][axis] = value
|
|
elif tag == "AKorr" and axis in self.state["AIPos"]:
|
|
# Apply joint correction
|
|
if self.mode == "relative":
|
|
self.state["AIPos"][axis] += value
|
|
else:
|
|
self.state["AIPos"][axis] = value
|
|
elif tag in ["DiO", "DiL"]:
|
|
if tag in self.state:
|
|
self.state[tag] = int(elem.text.strip())
|
|
elif tag == "IPOC":
|
|
self.ipoc_value = int(elem.text.strip())
|
|
|
|
logging.debug(f"Processed input: {ET.tostring(root).decode()}")
|
|
except socket.timeout:
|
|
pass # No data within delay window
|
|
except ConnectionResetError:
|
|
print("⚠️ Connection was reset by client. Waiting before retry...")
|
|
time.sleep(0.5)
|
|
except Exception as e:
|
|
print(f"[ERROR] Failed to process input: {e}")
|
|
|
|
def generate_message(self):
|
|
"""
|
|
Creates a reply XML message based on current state.
|
|
Format matches KUKA RSI's expected response structure.
|
|
"""
|
|
root = ET.Element("Rob", Type="KUKA")
|
|
|
|
for key in ["RIst", "AIPos", "ELPos"]:
|
|
element = ET.SubElement(root, key)
|
|
for sub_key, value in self.state[key].items():
|
|
element.set(sub_key, f"{value:.2f}")
|
|
|
|
for key in ["DiO", "DiL"]:
|
|
ET.SubElement(root, key).text = str(self.state[key])
|
|
|
|
ET.SubElement(root, "IPOC").text = str(self.ipoc_value)
|
|
return ET.tostring(root, encoding="utf-8").decode()
|
|
|
|
def send_message(self):
|
|
"""
|
|
Main loop to receive input, update state, and send reply.
|
|
Runs in a background thread until stopped.
|
|
"""
|
|
while self.running:
|
|
try:
|
|
self.receive_and_process()
|
|
response = self.generate_message()
|
|
self.udp_socket.sendto(response.encode(), self.client_address)
|
|
self.ipoc_value += 4
|
|
time.sleep(self.delay_ms)
|
|
except Exception as e:
|
|
print(f"[ERROR] EchoServer error: {e}")
|
|
time.sleep(1)
|
|
|
|
def start(self):
|
|
"""Starts the echo server loop in a background thread."""
|
|
self.running = True
|
|
self.thread.start()
|
|
|
|
def stop(self):
|
|
"""Stops the echo server and cleans up the socket."""
|
|
print("Stopping Echo Server...")
|
|
self.running = False
|
|
self.thread.join()
|
|
self.udp_socket.close()
|
|
print("✅ Echo Server Stopped.")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
import argparse
|
|
parser = argparse.ArgumentParser(description="Run Echo Server for RSI Simulation")
|
|
parser.add_argument("--config", type=str, default="RSI_EthernetConfig.xml", help="Path to RSI config file")
|
|
parser.add_argument("--mode", type=str, choices=["relative", "absolute"], default="relative", help="Correction mode")
|
|
parser.add_argument("--delay", type=int, default=4, help="Delay between messages in ms")
|
|
|
|
args = parser.parse_args()
|
|
server = EchoServer(config_file=args.config, delay_ms=args.delay, mode=args.mode)
|
|
|
|
try:
|
|
server.start()
|
|
while True:
|
|
time.sleep(1)
|
|
except KeyboardInterrupt:
|
|
server.stop()
|