Added trajectory planning to cli and api. Updated readme.
This commit is contained in:
parent
259dd55c84
commit
af090b507b
25
README.md
25
README.md
@ -4,6 +4,31 @@ RSIPI is a high-performance, Python-based communication and control system desig
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
🛡️ Safety Notice
|
||||||
|
RSIPI is a powerful tool that directly interfaces with industrial robotic systems. Improper use can lead to dangerous movements, property damage, or personal injury.
|
||||||
|
|
||||||
|
⚠️ Safety Guidelines
|
||||||
|
Test in Simulation First
|
||||||
|
Always verify your RSI communication and trajectories using simulation tools before deploying to a live robot.
|
||||||
|
|
||||||
|
Enable Emergency Stops
|
||||||
|
Ensure all safety hardware (E-Stop, fencing, light curtains) is active and functioning correctly.
|
||||||
|
|
||||||
|
Supervised Operation Only
|
||||||
|
Run RSIPI only in supervised environments with trained personnel present.
|
||||||
|
|
||||||
|
Limit Movement Ranges
|
||||||
|
Use KUKA Workspaces or software limits to constrain movement, especially when testing new code.
|
||||||
|
|
||||||
|
Use Logging for Debugging
|
||||||
|
Avoid debugging while RSI is active; instead, enable CSV logging and review logs post-run.
|
||||||
|
|
||||||
|
Secure Network Configuration
|
||||||
|
Ensure your RSI network is on a closed, isolated interface to avoid external interference or spoofing.
|
||||||
|
|
||||||
|
Never Rely on RSIPI for Safety
|
||||||
|
RSIPI is not a safety-rated system. Do not use it in applications where failure could result in harm.
|
||||||
|
|
||||||
## 📄 Description
|
## 📄 Description
|
||||||
|
|
||||||
RSIPI allows users to:
|
RSIPI allows users to:
|
||||||
|
|||||||
@ -3,8 +3,11 @@ import xml.etree.ElementTree as ET
|
|||||||
class ConfigParser:
|
class ConfigParser:
|
||||||
"""Handles parsing the RSI config file and generating structured send/receive variables."""
|
"""Handles parsing the RSI config file and generating structured send/receive variables."""
|
||||||
|
|
||||||
def __init__(self, config_file):
|
def __init__(self, config_file, rsi_limits_file=None):
|
||||||
"""Initialize with the configuration file and process send/receive structures."""
|
"""Initialize with the configuration file and process send/receive structures."""
|
||||||
|
from src.RSIPI.rsi_limit_parser import parse_rsi_limits
|
||||||
|
self.rsi_limits_file = rsi_limits_file
|
||||||
|
self.safety_limits = {}
|
||||||
self.config_file = config_file
|
self.config_file = config_file
|
||||||
self.internal_structure = {
|
self.internal_structure = {
|
||||||
"ComStatus": "String",
|
"ComStatus": "String",
|
||||||
@ -44,6 +47,14 @@ class ConfigParser:
|
|||||||
if "IPOC" not in self.send_variables:
|
if "IPOC" not in self.send_variables:
|
||||||
self.send_variables.update({"IPOC": 0})
|
self.send_variables.update({"IPOC": 0})
|
||||||
|
|
||||||
|
# Load safety limits from .rsi.xml if provided
|
||||||
|
if self.rsi_limits_file:
|
||||||
|
try:
|
||||||
|
self.safety_limits = parse_rsi_limits(self.rsi_limits_file)
|
||||||
|
except Exception as e:
|
||||||
|
print(f"[WARNING] Failed to load .rsi.xml safety limits: {e}")
|
||||||
|
self.safety_limits = {}
|
||||||
|
|
||||||
print(f"✅ Final Send Variables: {self.send_variables}")
|
print(f"✅ Final Send Variables: {self.send_variables}")
|
||||||
print(f"✅ Final Receive Variables: {self.receive_variables}")
|
print(f"✅ Final Receive Variables: {self.receive_variables}")
|
||||||
|
|
||||||
|
|||||||
@ -5,8 +5,9 @@ import os
|
|||||||
|
|
||||||
|
|
||||||
class KukaRSIVisualizer:
|
class KukaRSIVisualizer:
|
||||||
def __init__(self, csv_file):
|
def __init__(self, csv_file, safety_limits=None):
|
||||||
self.csv_file = csv_file
|
self.csv_file = csv_file
|
||||||
|
self.safety_limits = safety_limits or {}
|
||||||
if not os.path.exists(csv_file):
|
if not os.path.exists(csv_file):
|
||||||
raise FileNotFoundError(f"CSV file {csv_file} not found.")
|
raise FileNotFoundError(f"CSV file {csv_file} not found.")
|
||||||
self.df = pd.read_csv(csv_file)
|
self.df = pd.read_csv(csv_file)
|
||||||
@ -32,7 +33,7 @@ class KukaRSIVisualizer:
|
|||||||
plt.show()
|
plt.show()
|
||||||
|
|
||||||
def plot_joint_positions(self, save_path=None):
|
def plot_joint_positions(self, save_path=None):
|
||||||
"""Plots joint positions over time."""
|
"""Plots joint positions over time with safety limit overlays."""
|
||||||
plt.figure()
|
plt.figure()
|
||||||
time_series = range(len(self.df))
|
time_series = range(len(self.df))
|
||||||
|
|
||||||
@ -40,6 +41,11 @@ class KukaRSIVisualizer:
|
|||||||
if col in self.df.columns:
|
if col in self.df.columns:
|
||||||
plt.plot(time_series, self.df[col], label=col)
|
plt.plot(time_series, self.df[col], label=col)
|
||||||
|
|
||||||
|
# 🔴 Overlay safety band
|
||||||
|
if col in self.safety_limits:
|
||||||
|
low, high = self.safety_limits[col]
|
||||||
|
plt.axhspan(low, high, color='red', alpha=0.1, label=f"{col} Safe Zone")
|
||||||
|
|
||||||
plt.xlabel("Time Steps")
|
plt.xlabel("Time Steps")
|
||||||
plt.ylabel("Joint Position (Degrees)")
|
plt.ylabel("Joint Position (Degrees)")
|
||||||
plt.title("Joint Positions Over Time")
|
plt.title("Joint Positions Over Time")
|
||||||
@ -59,6 +65,10 @@ class KukaRSIVisualizer:
|
|||||||
if col in self.df.columns:
|
if col in self.df.columns:
|
||||||
plt.plot(time_series, self.df[col], label=col)
|
plt.plot(time_series, self.df[col], label=col)
|
||||||
|
|
||||||
|
if col in self.safety_limits:
|
||||||
|
low, high = self.safety_limits[col]
|
||||||
|
plt.axhspan(low, high, color='red', alpha=0.1, label=f"{col} Safe Zone")
|
||||||
|
|
||||||
plt.xlabel("Time Steps")
|
plt.xlabel("Time Steps")
|
||||||
plt.ylabel("Force Correction (N)")
|
plt.ylabel("Force Correction (N)")
|
||||||
plt.title("Force Trends Over Time")
|
plt.title("Force Trends Over Time")
|
||||||
@ -81,8 +91,15 @@ if __name__ == "__main__":
|
|||||||
parser = argparse.ArgumentParser(description="Visualize RSI data logs.")
|
parser = argparse.ArgumentParser(description="Visualize RSI data logs.")
|
||||||
parser.add_argument("csv_file", type=str, help="Path to the RSI CSV log file.")
|
parser.add_argument("csv_file", type=str, help="Path to the RSI CSV log file.")
|
||||||
parser.add_argument("--export", action="store_true", help="Export graphs as PNG/PDF.")
|
parser.add_argument("--export", action="store_true", help="Export graphs as PNG/PDF.")
|
||||||
|
parser.add_argument("--limits", type=str, help="Optional .rsi.xml file to overlay safety bands")
|
||||||
|
|
||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
if args.limits:
|
||||||
|
from src.RSIPI.rsi_limit_parser import parse_rsi_limits
|
||||||
|
limits = parse_rsi_limits(args.limits)
|
||||||
|
visualizer = KukaRSIVisualizer(args.csv_file, safety_limits=limits)
|
||||||
|
else:
|
||||||
visualizer = KukaRSIVisualizer(args.csv_file)
|
visualizer = KukaRSIVisualizer(args.csv_file)
|
||||||
|
|
||||||
visualizer.plot_trajectory()
|
visualizer.plot_trajectory()
|
||||||
|
|||||||
@ -5,7 +5,7 @@ import csv
|
|||||||
import logging
|
import logging
|
||||||
import xml.etree.ElementTree as ET # ✅ FIX: Import ElementTree
|
import xml.etree.ElementTree as ET # ✅ FIX: Import ElementTree
|
||||||
from .xml_handler import XMLGenerator
|
from .xml_handler import XMLGenerator
|
||||||
|
from .safety_manager import SafetyManager
|
||||||
|
|
||||||
class NetworkProcess(multiprocessing.Process):
|
class NetworkProcess(multiprocessing.Process):
|
||||||
"""Handles UDP communication and optional CSV logging in a separate process."""
|
"""Handles UDP communication and optional CSV logging in a separate process."""
|
||||||
@ -17,6 +17,7 @@ class NetworkProcess(multiprocessing.Process):
|
|||||||
self.stop_event = stop_event
|
self.stop_event = stop_event
|
||||||
self.config_parser = config_parser
|
self.config_parser = config_parser
|
||||||
self.udp_socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
self.udp_socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
||||||
|
self.safety_manager = SafetyManager(config_parser.safety_limits)
|
||||||
|
|
||||||
self.client_address = (ip, port)
|
self.client_address = (ip, port)
|
||||||
|
|
||||||
@ -98,7 +99,7 @@ class NetworkProcess(multiprocessing.Process):
|
|||||||
print(f"[ERROR] Error parsing received message: {e}")
|
print(f"[ERROR] Error parsing received message: {e}")
|
||||||
|
|
||||||
def log_to_csv(self):
|
def log_to_csv(self):
|
||||||
"""Write send/receive variables to the CSV log."""
|
"""Write send/receive variables to the CSV log with safety flags."""
|
||||||
filename = self.log_filename.value.decode().strip()
|
filename = self.log_filename.value.decode().strip()
|
||||||
if not filename:
|
if not filename:
|
||||||
return
|
return
|
||||||
@ -112,14 +113,36 @@ class NetworkProcess(multiprocessing.Process):
|
|||||||
headers = ["Timestamp", "IPOC"]
|
headers = ["Timestamp", "IPOC"]
|
||||||
headers += [f"Send.{k}" for k in self.send_variables.keys()]
|
headers += [f"Send.{k}" for k in self.send_variables.keys()]
|
||||||
headers += [f"Receive.{k}" for k in self.receive_variables.keys()]
|
headers += [f"Receive.{k}" for k in self.receive_variables.keys()]
|
||||||
|
headers += ["SafetyViolation"]
|
||||||
writer.writerow(headers)
|
writer.writerow(headers)
|
||||||
|
|
||||||
# Write current data
|
# Gather values
|
||||||
timestamp = time.strftime("%d-%m-%Y %H:%M:%S")
|
timestamp = time.strftime("%d-%m-%Y %H:%M:%S")
|
||||||
ipoc = self.receive_variables.get("IPOC", 0)
|
ipoc = self.receive_variables.get("IPOC", 0)
|
||||||
send_data = [self.send_variables.get(k, "") for k in self.send_variables.keys()]
|
send_data = [self.send_variables.get(k, "") for k in self.send_variables.keys()]
|
||||||
receive_data = [self.receive_variables.get(k, "") for k in self.receive_variables.keys()]
|
receive_data = [self.receive_variables.get(k, "") for k in self.receive_variables.keys()]
|
||||||
writer.writerow([timestamp, ipoc] + send_data + receive_data)
|
|
||||||
|
# 🔴 Check for safety violations
|
||||||
|
violation = False
|
||||||
|
for var in self.send_variables:
|
||||||
|
value = self.send_variables[var]
|
||||||
|
# Check structured variables
|
||||||
|
if isinstance(value, dict):
|
||||||
|
for subkey, subval in value.items():
|
||||||
|
path = f"{var}.{subkey}"
|
||||||
|
try:
|
||||||
|
self.safety_manager.validate(path, subval)
|
||||||
|
except Exception as e:
|
||||||
|
violation = str(e)
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
try:
|
||||||
|
self.safety_manager.validate(var, value)
|
||||||
|
except Exception as e:
|
||||||
|
violation = str(e)
|
||||||
|
break
|
||||||
|
|
||||||
|
writer.writerow([timestamp, ipoc] + send_data + receive_data + [violation or "False"])
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"[ERROR] Failed to log data to CSV: {e}")
|
print(f"[ERROR] Failed to log data to CSV: {e}")
|
||||||
|
|||||||
@ -13,6 +13,16 @@ from threading import Thread
|
|||||||
from .trajectory_planner import generate_trajectory, execute_trajectory
|
from .trajectory_planner import generate_trajectory, execute_trajectory
|
||||||
|
|
||||||
|
|
||||||
|
def compare_test_runs(file1, file2):
|
||||||
|
"""Compare two movement logs."""
|
||||||
|
df1 = pd.read_csv(file1)
|
||||||
|
df2 = pd.read_csv(file2)
|
||||||
|
|
||||||
|
diff = abs(df1 - df2)
|
||||||
|
max_deviation = diff.max()
|
||||||
|
return f"📊 Max Deviation: {max_deviation}"
|
||||||
|
|
||||||
|
|
||||||
class RSIAPI:
|
class RSIAPI:
|
||||||
"""RSI API for programmatic control, including alerts, logging, graphing, and data retrieval."""
|
"""RSI API for programmatic control, including alerts, logging, graphing, and data retrieval."""
|
||||||
|
|
||||||
@ -182,15 +192,6 @@ class RSIAPI:
|
|||||||
df.to_csv(filename, index=False)
|
df.to_csv(filename, index=False)
|
||||||
return f"✅ Data exported to {filename}"
|
return f"✅ Data exported to {filename}"
|
||||||
|
|
||||||
def compare_test_runs(self, file1, file2):
|
|
||||||
"""Compare two movement logs."""
|
|
||||||
df1 = pd.read_csv(file1)
|
|
||||||
df2 = pd.read_csv(file2)
|
|
||||||
|
|
||||||
diff = abs(df1 - df2)
|
|
||||||
max_deviation = diff.max()
|
|
||||||
return f"📊 Max Deviation: {max_deviation}"
|
|
||||||
|
|
||||||
def generate_report(self, filename, format_type):
|
def generate_report(self, filename, format_type):
|
||||||
"""Generate a statistical report from movement data."""
|
"""Generate a statistical report from movement data."""
|
||||||
data = self.client.get_movement_data()
|
data = self.client.get_movement_data()
|
||||||
|
|||||||
@ -160,6 +160,30 @@ class RSICommandLineInterface:
|
|||||||
output = parts[2] if len(parts) == 3 else "report.txt"
|
output = parts[2] if len(parts) == 3 else "report.txt"
|
||||||
result = self.client.generate_report(parts[1], output)
|
result = self.client.generate_report(parts[1], output)
|
||||||
print(result)
|
print(result)
|
||||||
|
elif cmd == "safety-stop":
|
||||||
|
self.client.safety_manager.emergency_stop()
|
||||||
|
print("🛑 Safety: Emergency Stop activated.")
|
||||||
|
|
||||||
|
elif cmd == "safety-reset":
|
||||||
|
self.client.safety_manager.reset_stop()
|
||||||
|
print("✅ Safety: Emergency Stop reset. Motion allowed.")
|
||||||
|
|
||||||
|
elif cmd == "safety-status":
|
||||||
|
sm = self.client.safety_manager
|
||||||
|
print("🧱 Safety Status: " + ("STOPPED" if sm.is_stopped() else "ACTIVE"))
|
||||||
|
print("📏 Enforced Limits:")
|
||||||
|
for var, (lo, hi) in sm.get_limits().items():
|
||||||
|
print(f" {var}: {lo} → {hi}")
|
||||||
|
|
||||||
|
elif cmd == "safety-set-limit" and len(parts) == 4:
|
||||||
|
var, lo, hi = parts[1], parts[2], parts[3]
|
||||||
|
try:
|
||||||
|
lo = float(lo)
|
||||||
|
hi = float(hi)
|
||||||
|
self.client.safety_manager.set_limit(var, lo, hi)
|
||||||
|
print(f"✅ Set limit for {var}: {lo} to {hi}")
|
||||||
|
except ValueError:
|
||||||
|
print("❌ Invalid numbers for limit. Usage: safety-set-limit RKorr.X -5 5")
|
||||||
|
|
||||||
else:
|
else:
|
||||||
print("❌ Unknown command. Type 'help' for a list of commands.")
|
print("❌ Unknown command. Type 'help' for a list of commands.")
|
||||||
|
|||||||
@ -3,16 +3,17 @@ import multiprocessing
|
|||||||
import time
|
import time
|
||||||
from .config_parser import ConfigParser
|
from .config_parser import ConfigParser
|
||||||
from .network_handler import NetworkProcess
|
from .network_handler import NetworkProcess
|
||||||
|
from .safety_manager import SafetyManager
|
||||||
|
|
||||||
class RSIClient:
|
class RSIClient:
|
||||||
"""Main RSI API class that integrates network, config handling, and message processing."""
|
"""Main RSI API class that integrates network, config handling, and message processing."""
|
||||||
|
|
||||||
def __init__(self, config_file):
|
def __init__(self, config_file, rsi_limits_file=None):
|
||||||
"""Initialize the RSI client and set up logging and networking."""
|
"""Initialize the RSI client and set up logging and networking."""
|
||||||
logging.info(f"📂 Loading RSI configuration from {config_file}...")
|
logging.info(f"📂 Loading RSI configuration from {config_file}...")
|
||||||
|
|
||||||
# Load configuration
|
# Load configuration
|
||||||
self.config_parser = ConfigParser(config_file)
|
self.config_parser = ConfigParser(config_file, rsi_limits_file)
|
||||||
network_settings = self.config_parser.get_network_settings()
|
network_settings = self.config_parser.get_network_settings()
|
||||||
|
|
||||||
self.manager = multiprocessing.Manager()
|
self.manager = multiprocessing.Manager()
|
||||||
@ -20,6 +21,9 @@ class RSIClient:
|
|||||||
self.receive_variables = self.manager.dict(self.config_parser.receive_variables)
|
self.receive_variables = self.manager.dict(self.config_parser.receive_variables)
|
||||||
self.stop_event = multiprocessing.Event()
|
self.stop_event = multiprocessing.Event()
|
||||||
|
|
||||||
|
# ✅ Initialise safety manager from limits
|
||||||
|
self.safety_manager = SafetyManager(self.config_parser.safety_limits)
|
||||||
|
|
||||||
logging.info(f"🚀 Starting network process on {network_settings['ip']}:{network_settings['port']}...")
|
logging.info(f"🚀 Starting network process on {network_settings['ip']}:{network_settings['port']}...")
|
||||||
|
|
||||||
# ✅ Corrected constructor call with all necessary parameters
|
# ✅ Corrected constructor call with all necessary parameters
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
import xml.etree.ElementTree as ET
|
import xml.etree.ElementTree as ET
|
||||||
import logging
|
import logging
|
||||||
|
from src.RSIPI.rsi_limit_parser import parse_rsi_limits
|
||||||
|
|
||||||
# ✅ Configure Logging (Allow it to be turned off)
|
# ✅ Configure Logging (Allow it to be turned off)
|
||||||
LOGGING_ENABLED = True # Change to False to disable logging
|
LOGGING_ENABLED = True # Change to False to disable logging
|
||||||
@ -35,12 +36,26 @@ class RSIConfig:
|
|||||||
"Tech.T2": ["T21", "T22", "T23", "T24", "T25", "T26", "T27", "T28", "T29", "T210"],
|
"Tech.T2": ["T21", "T22", "T23", "T24", "T25", "T26", "T27", "T28", "T29", "T210"],
|
||||||
}
|
}
|
||||||
|
|
||||||
def __init__(self, config_file):
|
def __init__(self, config_file, rsi_limits_file=None):
|
||||||
self.config_file = config_file
|
self.config_file = config_file
|
||||||
|
self.rsi_limits_file = rsi_limits_file
|
||||||
|
self.safety_limits = {}
|
||||||
|
|
||||||
self.network_settings = {}
|
self.network_settings = {}
|
||||||
self.send_variables = {}
|
self.send_variables = {}
|
||||||
self.receive_variables = {}
|
self.receive_variables = {}
|
||||||
|
|
||||||
self.load_config()
|
self.load_config()
|
||||||
|
self.load_safety_limits() # ✅ NEW STEP
|
||||||
|
|
||||||
|
def load_safety_limits(self):
|
||||||
|
if self.rsi_limits_file:
|
||||||
|
try:
|
||||||
|
self.safety_limits = parse_rsi_limits(self.rsi_limits_file)
|
||||||
|
logging.info(f"✅ Loaded safety limits from {self.rsi_limits_file}")
|
||||||
|
except Exception as e:
|
||||||
|
logging.warning(f"⚠️ Failed to load RSI safety limits: {e}")
|
||||||
|
self.safety_limits = {}
|
||||||
|
|
||||||
def strip_def_prefix(self, tag):
|
def strip_def_prefix(self, tag):
|
||||||
"""Remove 'DEF_' prefix from a variable name."""
|
"""Remove 'DEF_' prefix from a variable name."""
|
||||||
|
|||||||
65
src/RSIPI/rsi_limit_parser.py
Normal file
65
src/RSIPI/rsi_limit_parser.py
Normal file
@ -0,0 +1,65 @@
|
|||||||
|
import xml.etree.ElementTree as ET
|
||||||
|
|
||||||
|
def parse_rsi_limits(xml_path):
|
||||||
|
tree = ET.parse(xml_path)
|
||||||
|
root = tree.getroot()
|
||||||
|
|
||||||
|
raw_limits = {}
|
||||||
|
|
||||||
|
for rsi_object in root.findall("RSIObject"):
|
||||||
|
obj_type = rsi_object.attrib.get("ObjType", "")
|
||||||
|
params = rsi_object.find("Parameters")
|
||||||
|
|
||||||
|
if params is None:
|
||||||
|
continue
|
||||||
|
|
||||||
|
if obj_type == "POSCORR":
|
||||||
|
for param in params.findall("Parameter"):
|
||||||
|
name = param.attrib["Name"]
|
||||||
|
value = float(param.attrib["ParamValue"])
|
||||||
|
if name == "LowerLimX":
|
||||||
|
raw_limits["RKorr.X_min"] = value
|
||||||
|
elif name == "UpperLimX":
|
||||||
|
raw_limits["RKorr.X_max"] = value
|
||||||
|
elif name == "LowerLimY":
|
||||||
|
raw_limits["RKorr.Y_min"] = value
|
||||||
|
elif name == "UpperLimY":
|
||||||
|
raw_limits["RKorr.Y_max"] = value
|
||||||
|
elif name == "LowerLimZ":
|
||||||
|
raw_limits["RKorr.Z_min"] = value
|
||||||
|
elif name == "UpperLimZ":
|
||||||
|
raw_limits["RKorr.Z_max"] = value
|
||||||
|
elif name == "MaxRotAngle":
|
||||||
|
for axis in ["A", "B", "C"]:
|
||||||
|
raw_limits[f"RKorr.{axis}_min"] = -value
|
||||||
|
raw_limits[f"RKorr.{axis}_max"] = value
|
||||||
|
|
||||||
|
elif obj_type == "AXISCORR":
|
||||||
|
for param in params.findall("Parameter"):
|
||||||
|
name = param.attrib["Name"]
|
||||||
|
value = float(param.attrib["ParamValue"])
|
||||||
|
if name.startswith("LowerLimA") or name.startswith("UpperLimA"):
|
||||||
|
axis = name[-1]
|
||||||
|
key = f"AKorr.A{axis}_{'min' if 'Lower' in name else 'max'}"
|
||||||
|
raw_limits[key] = value
|
||||||
|
|
||||||
|
elif obj_type == "AXISCORREXT":
|
||||||
|
for param in params.findall("Parameter"):
|
||||||
|
name = param.attrib["Name"]
|
||||||
|
value = float(param.attrib["ParamValue"])
|
||||||
|
if name.startswith("LowerLimE") or name.startswith("UpperLimE"):
|
||||||
|
axis = name[-1]
|
||||||
|
key = f"AKorr.E{axis}_{'min' if 'Lower' in name else 'max'}"
|
||||||
|
raw_limits[key] = value
|
||||||
|
|
||||||
|
# Build structured dictionary with only valid (min, max) pairs
|
||||||
|
structured_limits = {}
|
||||||
|
for key in list(raw_limits.keys()):
|
||||||
|
if key.endswith("_min"):
|
||||||
|
base = key[:-4]
|
||||||
|
min_val = raw_limits.get(f"{base}_min")
|
||||||
|
max_val = raw_limits.get(f"{base}_max")
|
||||||
|
if min_val is not None and max_val is not None:
|
||||||
|
structured_limits[base] = (min_val, max_val)
|
||||||
|
|
||||||
|
return structured_limits
|
||||||
66
src/RSIPI/safety_manager.py
Normal file
66
src/RSIPI/safety_manager.py
Normal file
@ -0,0 +1,66 @@
|
|||||||
|
import logging
|
||||||
|
|
||||||
|
|
||||||
|
class SafetyManager:
|
||||||
|
def __init__(self, limits=None):
|
||||||
|
"""
|
||||||
|
limits: Optional dictionary of variable limits in the form:
|
||||||
|
{
|
||||||
|
'RKorr.X': (-5.0, 5.0),
|
||||||
|
'AKorr.A1': (-6.0, 6.0),
|
||||||
|
...
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
self.limits = limits if limits is not None else {}
|
||||||
|
self.e_stop = False
|
||||||
|
self.last_values = {}
|
||||||
|
|
||||||
|
def validate(self, path: str, value: float) -> float:
|
||||||
|
"""
|
||||||
|
Validate a single variable update against safety rules.
|
||||||
|
Raises RuntimeError if E-Stop is active.
|
||||||
|
Raises ValueError if out of allowed bounds.
|
||||||
|
"""
|
||||||
|
if self.e_stop:
|
||||||
|
raise RuntimeError(f"SafetyManager: E-STOP active, motion blocked for {path}.")
|
||||||
|
|
||||||
|
if path in self.limits:
|
||||||
|
min_val, max_val = self.limits[path]
|
||||||
|
if not (min_val <= value <= max_val):
|
||||||
|
raise ValueError(
|
||||||
|
f"SafetyManager: {path}={value} is out of bounds ({min_val} to {max_val})"
|
||||||
|
)
|
||||||
|
|
||||||
|
return value
|
||||||
|
|
||||||
|
def emergency_stop(self):
|
||||||
|
"""Activate emergency stop mode (blocks all motion)."""
|
||||||
|
self.e_stop = True
|
||||||
|
|
||||||
|
def reset_stop(self):
|
||||||
|
"""Reset emergency stop (motion allowed again)."""
|
||||||
|
self.e_stop = False
|
||||||
|
|
||||||
|
def set_limit(self, path: str, min_val: float, max_val: float):
|
||||||
|
"""Manually set or override a safety limit at runtime."""
|
||||||
|
self.limits[path] = (min_val, max_val)
|
||||||
|
|
||||||
|
def get_limits(self):
|
||||||
|
"""Return a copy of current limits."""
|
||||||
|
return self.limits.copy()
|
||||||
|
|
||||||
|
def is_stopped(self):
|
||||||
|
return self.e_stop
|
||||||
|
|
||||||
|
def validate(self, path: str, value: float) -> float:
|
||||||
|
if self.e_stop:
|
||||||
|
logging.warning(f"Safety violation: {path} update blocked (E-STOP active)")
|
||||||
|
raise RuntimeError(f"E-STOP active. Motion blocked for {path}.")
|
||||||
|
|
||||||
|
if path in self.limits:
|
||||||
|
min_val, max_val = self.limits[path]
|
||||||
|
if not (min_val <= value <= max_val):
|
||||||
|
logging.warning(f"Safety violation: {path}={value} blocked (out of bounds {min_val} to {max_val})")
|
||||||
|
raise ValueError(f"{path}={value} is out of bounds ({min_val} to {max_val})")
|
||||||
|
|
||||||
|
return value
|
||||||
Loading…
Reference in New Issue
Block a user