updated and fixed safety manager, updated trajectory code.
This commit is contained in:
parent
2750952e26
commit
82b01cc7e8
@ -1,66 +1,62 @@
|
|||||||
|
from src.RSIPI.rsi_api import RSIAPI
|
||||||
|
from src.RSIPI.live_plotter import LivePlotter
|
||||||
|
import math
|
||||||
import time
|
import time
|
||||||
import threading
|
import threading
|
||||||
from winreg import REG_CREATED_NEW_KEY
|
|
||||||
|
|
||||||
from RSIPI.rsi_api import RSIAPI
|
|
||||||
|
def generate_figure_8_z(start_xyz, radius=100, loops=1, steps=100):
|
||||||
|
trajectory = []
|
||||||
|
for i in range(steps):
|
||||||
|
t = 2 * math.pi * loops * (i / steps)
|
||||||
|
x = radius * math.sin(t)
|
||||||
|
y = radius * math.sin(t) * math.cos(t)
|
||||||
|
z = radius * math.sin(2 * t) / 2
|
||||||
|
trajectory.append({
|
||||||
|
"X": start_xyz["X"] + x,
|
||||||
|
"Y": start_xyz["Y"] + y,
|
||||||
|
"Z": start_xyz["Z"] + z
|
||||||
|
})
|
||||||
|
return trajectory
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
from src.RSIPI.rsi_api import RSIAPI
|
|
||||||
|
|
||||||
api = RSIAPI("RSI_EthernetConfig.xml")
|
api = RSIAPI("RSI_EthernetConfig.xml")
|
||||||
|
|
||||||
print(api.set_alert_threshold("deviation", 1.0))
|
print("🛜 Starting RSI client...")
|
||||||
print(api.set_alert_threshold("force", 5.0))
|
api.start_rsi()
|
||||||
print(api.enable_alerts(True))
|
time.sleep(2)
|
||||||
|
|
||||||
api.client.safety_manager.set_limit("RKorr.X", -2.0, 2.0)
|
start_xyz = {"X": 500, "Y": 0, "Z": 500}
|
||||||
print("✅ Limit set for RKorr.X: -2.0 to 2.0")
|
trajectory = api.generate_trajectory(
|
||||||
|
start=start_xyz,
|
||||||
|
end=generate_figure_8_z(start_xyz, radius=100, loops=1, steps=1)[0], # endpoint unused
|
||||||
|
steps=100,
|
||||||
|
space="cartesian",
|
||||||
|
mode="absolute"
|
||||||
|
)
|
||||||
|
# Overwrite with full 8-curve, skipping internal interpolation
|
||||||
|
trajectory = generate_figure_8_z(start_xyz, radius=100, loops=1, steps=100)
|
||||||
|
|
||||||
# === Try safe update ===
|
print(f"✅ Generated figure-8 trajectory with {len(trajectory)} steps.")
|
||||||
try:
|
|
||||||
result = api.update_variable("RKorr.X", 1.0)
|
|
||||||
print(result)
|
|
||||||
print("✅ Safe value accepted.")
|
|
||||||
except Exception as e:
|
|
||||||
print(f"❌ Safe value was rejected unexpectedly: {e}")
|
|
||||||
|
|
||||||
# === Try unsafe update ===
|
# 🧠 Start live plotting in a thread (in mode 3d)
|
||||||
try:
|
def start_plot():
|
||||||
result = api.update_variable("RKorr.X", 5.0)
|
plotter = LivePlotter(api.client, mode="3d", interval=50)
|
||||||
print(result)
|
plotter.start()
|
||||||
print("❌ Unsafe value was accepted unexpectedly!")
|
|
||||||
except Exception as e:
|
|
||||||
print(f"✅ Correctly blocked unsafe value: {e}")
|
|
||||||
|
|
||||||
# === Emergency Stop ===
|
threading.Thread(target=start_plot, daemon=True).start()
|
||||||
api.client.safety_manager.emergency_stop()
|
|
||||||
try:
|
|
||||||
api.update_variable("RKorr.X", 0.0)
|
|
||||||
print("❌ Update passed during E-STOP (this is wrong)")
|
|
||||||
except Exception as e:
|
|
||||||
print(f"✅ Correctly blocked by E-STOP: {e}")
|
|
||||||
|
|
||||||
# === Reset E-Stop ===
|
print("🟢 Executing trajectory...")
|
||||||
api.client.safety_manager.reset_stop()
|
api.execute_trajectory(trajectory, space="cartesian", rate=0.02)
|
||||||
try:
|
|
||||||
result = api.update_variable("RKorr.X", 0.0)
|
|
||||||
print(result)
|
|
||||||
print("✅ Motion resumed after E-STOP reset.")
|
|
||||||
except Exception as e:
|
|
||||||
print(f"❌ Unexpected error after E-STOP reset: {e}")
|
|
||||||
|
|
||||||
# === Enable Override ===
|
time.sleep(len(trajectory) * 0.02 + 2.0)
|
||||||
api.override_safety(True)
|
print("✅ Trajectory execution complete.")
|
||||||
try:
|
|
||||||
result = api.update_variable("RKorr.X", 999.0)
|
|
||||||
print(result)
|
|
||||||
print("✅ Override allowed unsafe motion as expected.")
|
|
||||||
except Exception as e:
|
|
||||||
print(f"❌ Override failed: {e}")
|
|
||||||
|
|
||||||
# === Check override status ===
|
|
||||||
print("🛡️ Override status:", "ACTIVE" if api.is_safety_overridden() else "INACTIVE")
|
|
||||||
|
|
||||||
|
print("🛑 Stopping RSI...")
|
||||||
api.stop_rsi()
|
api.stop_rsi()
|
||||||
|
print("✅ RSI client stopped.")
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
main()
|
main()
|
||||||
|
|||||||
@ -13,6 +13,7 @@ from src.RSIPI.static_plotter import StaticPlotter # Make sure this file exists
|
|||||||
import os
|
import os
|
||||||
from src.RSIPI.live_plotter import LivePlotter
|
from src.RSIPI.live_plotter import LivePlotter
|
||||||
from threading import Thread
|
from threading import Thread
|
||||||
|
import asyncio
|
||||||
|
|
||||||
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."""
|
||||||
@ -274,7 +275,7 @@ class RSIAPI:
|
|||||||
return f"✅ {alert_type.capitalize()} alert threshold set to {value}"
|
return f"✅ {alert_type.capitalize()} alert threshold set to {value}"
|
||||||
return "❌ Invalid alert type. Use 'deviation' or 'force'."
|
return "❌ Invalid alert type. Use 'deviation' or 'force'."
|
||||||
|
|
||||||
## TODO Testing to here.
|
## TODO Test from here.
|
||||||
|
|
||||||
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."""
|
||||||
@ -356,14 +357,40 @@ class RSIAPI:
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
return f"❌ RSI injection failed: {e}"
|
return f"❌ RSI injection failed: {e}"
|
||||||
|
|
||||||
|
## TODO here
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def generate_trajectory(start, end, steps=100, space="cartesian"):
|
def generate_trajectory(start, end, steps=100, space="cartesian", mode="absolute", include_resets=False):
|
||||||
"""Generates a linear trajectory (Cartesian or Joint)."""
|
"""Generates a linear trajectory (Cartesian or Joint)."""
|
||||||
return generate_trajectory(start, end, steps, space)
|
return generate_trajectory(start, end, steps, space, mode, include_resets)
|
||||||
|
|
||||||
|
import asyncio
|
||||||
|
|
||||||
def execute_trajectory(self, trajectory, space="cartesian", rate=0.012):
|
def execute_trajectory(self, trajectory, space="cartesian", rate=0.012):
|
||||||
"""Executes a trajectory using live updates."""
|
"""
|
||||||
return execute_trajectory(self, trajectory, space, rate)
|
Executes a trajectory intelligently:
|
||||||
|
- If already inside an asyncio loop -> schedules task in background
|
||||||
|
- If no loop -> creates one and runs blocking
|
||||||
|
"""
|
||||||
|
|
||||||
|
async def runner():
|
||||||
|
for idx, point in enumerate(trajectory):
|
||||||
|
if space == "cartesian":
|
||||||
|
self.update_cartesian(**point)
|
||||||
|
elif space == "joint":
|
||||||
|
self.update_joints(**point)
|
||||||
|
else:
|
||||||
|
raise ValueError("space must be 'cartesian' or 'joint'")
|
||||||
|
print(f"🚀 Step {idx + 1}/{len(trajectory)} sent")
|
||||||
|
await asyncio.sleep(rate)
|
||||||
|
|
||||||
|
try:
|
||||||
|
loop = asyncio.get_running_loop()
|
||||||
|
# If inside event loop, schedule runner as background task
|
||||||
|
asyncio.create_task(runner())
|
||||||
|
except RuntimeError:
|
||||||
|
# If no event loop is running, create and run one
|
||||||
|
asyncio.run(runner())
|
||||||
|
|
||||||
def queue_trajectory(self, trajectory, space="cartesian", rate=0.012):
|
def queue_trajectory(self, trajectory, space="cartesian", rate=0.012):
|
||||||
"""Adds a trajectory to the internal queue."""
|
"""Adds a trajectory to the internal queue."""
|
||||||
|
|||||||
@ -63,7 +63,15 @@ class RSICommandLineInterface:
|
|||||||
elif cmd == "speed" and len(parts) == 3:
|
elif cmd == "speed" and len(parts) == 3:
|
||||||
self.client.adjust_speed(parts[1], parts[2])
|
self.client.adjust_speed(parts[1], parts[2])
|
||||||
elif cmd == "override" and len(parts) == 2:
|
elif cmd == "override" and len(parts) == 2:
|
||||||
self.client.override_safety(parts[1])
|
state = parts[1]
|
||||||
|
if state in ["on", "true", "1"]:
|
||||||
|
self.client.override_safety(True)
|
||||||
|
print("🛡️ Safety override ENABLED.")
|
||||||
|
elif state in ["off", "false", "0"]:
|
||||||
|
self.client.override_safety(False)
|
||||||
|
print("🛡️ Safety override DISABLED.")
|
||||||
|
else:
|
||||||
|
print("❌ Usage: override on | off")
|
||||||
elif cmd == "log":
|
elif cmd == "log":
|
||||||
if len(parts) < 1:
|
if len(parts) < 1:
|
||||||
print("⚠️ Usage: log start|stop|status")
|
print("⚠️ Usage: log start|stop|status")
|
||||||
@ -200,13 +208,15 @@ class RSICommandLineInterface:
|
|||||||
elif cmd == "safety-reset":
|
elif cmd == "safety-reset":
|
||||||
self.client.safety_manager.reset_stop()
|
self.client.safety_manager.reset_stop()
|
||||||
print("✅ Safety: Emergency Stop reset. Motion allowed.")
|
print("✅ Safety: Emergency Stop reset. Motion allowed.")
|
||||||
|
|
||||||
elif cmd == "safety-status":
|
elif cmd == "safety-status":
|
||||||
sm = self.client.safety_manager
|
sm = self.client.safety_manager
|
||||||
print("🧱 Safety Status: " + ("STOPPED" if sm.is_stopped() else "ACTIVE"))
|
override_active = self.client.is_safety_overridden()
|
||||||
print("📏 Enforced Limits:")
|
print("🛡️ Safety System Status:")
|
||||||
|
print(" 🧱 Emergency Stop:", "ACTIVE 🛑" if sm.is_stopped() else "CLEARED ✅")
|
||||||
|
print(" 🛡️ Safety Override:", "ENABLED" if override_active else "DISABLED")
|
||||||
|
print(" 📏 Enforced Limits:")
|
||||||
for var, (lo, hi) in sm.get_limits().items():
|
for var, (lo, hi) in sm.get_limits().items():
|
||||||
print(f" {var}: {lo} → {hi}")
|
print(f" - {var}: {lo} → {hi}")
|
||||||
elif cmd == "plot" and len(parts) >= 2:
|
elif cmd == "plot" and len(parts) >= 2:
|
||||||
plot_type = parts[1]
|
plot_type = parts[1]
|
||||||
if len(parts) < 3:
|
if len(parts) < 3:
|
||||||
@ -305,7 +315,12 @@ Available Commands:
|
|||||||
export_movement_data <csvfile> - Export logged motion data to CSV
|
export_movement_data <csvfile> - Export logged motion data to CSV
|
||||||
compare_test_runs <file1> <file2> - Compare 2 test logs (e.g. deviation)
|
compare_test_runs <file1> <file2> - Compare 2 test logs (e.g. deviation)
|
||||||
generate_report <input.csv> [out.txt] - Create a movement analysis report
|
generate_report <input.csv> [out.txt] - Create a movement analysis report
|
||||||
|
safety-stop - Emergency stop: block motion
|
||||||
|
safety-reset - Reset emergency stop
|
||||||
|
safety-status - Show safety and override status
|
||||||
|
override on/off - Enable or disable safety override
|
||||||
|
alerts on/off
|
||||||
|
set_alert_threshold <deviation|force> <value>
|
||||||
""")
|
""")
|
||||||
|
|
||||||
def visualize(self, csv_file, export=False):
|
def visualize(self, csv_file, export=False):
|
||||||
|
|||||||
@ -69,4 +69,42 @@ class SafetyManager:
|
|||||||
|
|
||||||
def is_safety_overridden(self) -> bool:
|
def is_safety_overridden(self) -> bool:
|
||||||
"""Returns whether safety override is active."""
|
"""Returns whether safety override is active."""
|
||||||
return self.override
|
return self.override
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def check_cartesian_limits(pose):
|
||||||
|
"""
|
||||||
|
Check if a Cartesian pose is within general robot limits.
|
||||||
|
Typical bounds: ±1500 mm in XYZ, ±360° in orientation.
|
||||||
|
"""
|
||||||
|
limits = {
|
||||||
|
"X": (-1500, 1500),
|
||||||
|
"Y": (-1500, 1500),
|
||||||
|
"Z": (0, 2000),
|
||||||
|
"A": (-360, 360),
|
||||||
|
"B": (-360, 360),
|
||||||
|
"C": (-360, 360),
|
||||||
|
}
|
||||||
|
for key, (lo, hi) in limits.items():
|
||||||
|
if key in pose and not (lo <= pose[key] <= hi):
|
||||||
|
return False
|
||||||
|
return True
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def check_joint_limits(pose):
|
||||||
|
"""
|
||||||
|
Check if a joint-space pose is within KUKA limits.
|
||||||
|
Typical KUKA ranges: A1–A6 in defined degrees.
|
||||||
|
"""
|
||||||
|
limits = {
|
||||||
|
"A1": (-185, 185),
|
||||||
|
"A2": (-185, 185),
|
||||||
|
"A3": (-185, 185),
|
||||||
|
"A4": (-350, 350),
|
||||||
|
"A5": (-130, 130),
|
||||||
|
"A6": (-350, 350),
|
||||||
|
}
|
||||||
|
for key, (lo, hi) in limits.items():
|
||||||
|
if key in pose and not (lo <= pose[key] <= hi):
|
||||||
|
return False
|
||||||
|
return True
|
||||||
@ -1,26 +1,21 @@
|
|||||||
from RSIPI.safety_manager import SafetyManager
|
from RSIPI.safety_manager import SafetyManager
|
||||||
import time
|
import time
|
||||||
|
|
||||||
def generate_trajectory(start, end, steps=100, space="cartesian", mode="relative", include_resets=True):
|
def generate_trajectory(start, end, steps=100, space="cartesian", mode="absolute", include_resets=False):
|
||||||
"""
|
"""
|
||||||
Generates a trajectory from start to end across N steps.
|
Generates a trajectory from start to end across N steps.
|
||||||
|
|
||||||
Args:
|
- Absolute mode (default): full poses, no resets
|
||||||
start (dict): Starting pose or joint values.
|
- Relative mode: incremental steps, optional resets after each step
|
||||||
end (dict): Target pose or joint values.
|
|
||||||
steps (int): Number of interpolation steps.
|
|
||||||
space (str): "cartesian" or "joint" (for API dispatch and validation).
|
|
||||||
mode (str): "relative" or "absolute" movement type.
|
|
||||||
include_resets (bool): Inserts zero-correction resets between steps in relative mode.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
list[dict]: List of movement command dictionaries.
|
|
||||||
"""
|
"""
|
||||||
if mode not in ["relative", "absolute"]:
|
if mode not in ["relative", "absolute"]:
|
||||||
raise ValueError("mode must be 'relative' or 'absolute'")
|
raise ValueError("mode must be 'relative' or 'absolute'")
|
||||||
if space not in ["cartesian", "joint"]:
|
if space not in ["cartesian", "joint"]:
|
||||||
raise ValueError("space must be 'cartesian' or 'joint'")
|
raise ValueError("space must be 'cartesian' or 'joint'")
|
||||||
|
|
||||||
|
if mode == "absolute":
|
||||||
|
include_resets = False # Smart safeguard
|
||||||
|
|
||||||
axes = start.keys()
|
axes = start.keys()
|
||||||
trajectory = []
|
trajectory = []
|
||||||
|
|
||||||
@ -29,6 +24,7 @@ def generate_trajectory(start, end, steps=100, space="cartesian", mode="relative
|
|||||||
global enforce_safety
|
global enforce_safety
|
||||||
enforce_safety = hasattr(SafetyManager, "check_cartesian_limits") # Enable only if those methods exist
|
enforce_safety = hasattr(SafetyManager, "check_cartesian_limits") # Enable only if those methods exist
|
||||||
|
|
||||||
|
|
||||||
for i in range(1, steps + 1):
|
for i in range(1, steps + 1):
|
||||||
point = {}
|
point = {}
|
||||||
for axis in axes:
|
for axis in axes:
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user