RoboticArmTest/src/gui/main_window.py
sladro 0119d9365f 集成KDL运动学引擎到主系统
功能更新:
- 实现KDL自动链检测,无需手动配置链接名称
- 创建ArmController统一控制接口,集成KDL与PyBullet
- 更新MainWindow使用ArmController替换独立的RobotLoader
- 修复KDL固定关节类型语法错误

已知问题:
- KDL逆运动学在Windows上崩溃,需要进一步调试

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-09-11 09:04:42 +08:00

448 lines
18 KiB
Python

import tkinter as tk
from tkinter import ttk, messagebox, scrolledtext
import pybullet as p
import pybullet_data
import threading
import time
from typing import Dict, Any
import sys
import os
# Add parent directory to path for imports
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))))
from src.config_loader import ConfigLoader
from src.robot.arm_controller import create_arm_controller
from src.simulation.environment import Environment
from src.gui.config_window import ConfigWindow
class MainWindow:
def __init__(self):
self.root = tk.Tk()
self.root.title("Robotic Arm Simulation - Feasibility Test")
# Get window dimensions from config or use defaults
self._setup_window_geometry()
# Simulation components
self.physics_client = None
self.config_loader = None
self.arm_controller = None
self.environment = None
self.config_window = None
# Simulation state
self.simulation_running = False
self.simulation_thread = None
# Create GUI
self._create_control_panel()
self._create_log_panel()
self._create_bottom_panel()
# Initialize simulation
self.initialize_simulation()
def _setup_window_geometry(self):
"""Setup window geometry from config or defaults"""
try:
# Try to load from config if exists
config = ConfigLoader().get_full_config()
window_size = config.get('gui', {}).get('window_size', "1200x800")
self.root.geometry(window_size)
except Exception as e:
# Use config default if loading fails
default_config = {"gui": {"window_size": "1200x800"}}
self.root.geometry(default_config['gui']['window_size'])
def _create_control_panel(self):
"""Create control panel on the left side"""
# Create main frame
self.main_frame = tk.Frame(self.root)
self.main_frame.pack(fill=tk.BOTH, expand=True, padx=10, pady=10)
# Left panel - Control panel
left_panel = tk.Frame(self.main_frame, width=300)
left_panel.pack(side=tk.LEFT, fill=tk.Y, padx=(0, 10))
# Title
title_label = tk.Label(left_panel, text="Control Panel",
font=("Arial", 14, "bold"))
title_label.pack(pady=(0, 10))
# Configuration section
config_frame = tk.LabelFrame(left_panel, text="Configuration", padx=10, pady=10)
config_frame.pack(fill=tk.X, pady=(0, 10))
self.config_btn = tk.Button(config_frame, text="Open Config Manager",
command=self.open_config_window,
bg="#2196F3", fg="white", padx=10, pady=5)
self.config_btn.pack(fill=tk.X)
self.reload_btn = tk.Button(config_frame, text="Reload Config",
command=self.reload_configuration,
padx=10, pady=5)
self.reload_btn.pack(fill=tk.X, pady=(5, 0))
# Simulation control section
control_frame = tk.LabelFrame(left_panel, text="Simulation Control", padx=10, pady=10)
control_frame.pack(fill=tk.X, pady=(0, 10))
self.start_btn = tk.Button(control_frame, text="Start Simulation",
command=self.start_simulation,
bg="#4CAF50", fg="white", padx=10, pady=5)
self.start_btn.pack(fill=tk.X)
self.pause_btn = tk.Button(control_frame, text="Pause Simulation",
command=self.pause_simulation,
state=tk.DISABLED, padx=10, pady=5)
self.pause_btn.pack(fill=tk.X, pady=(5, 0))
self.reset_btn = tk.Button(control_frame, text="Reset Environment",
command=self.reset_environment,
bg="#FF9800", fg="white", padx=10, pady=5)
self.reset_btn.pack(fill=tk.X, pady=(5, 0))
# Reachability test section
test_frame = tk.LabelFrame(left_panel, text="Reachability Test", padx=10, pady=10)
test_frame.pack(fill=tk.X, pady=(0, 10))
self.test_btn = tk.Button(test_frame, text="Test Reachability",
command=self.test_reachability,
bg="#9C27B0", fg="white", padx=10, pady=5)
self.test_btn.pack(fill=tk.X)
tk.Label(test_frame, text="Check if A and B points are reachable",
font=("Arial", 9)).pack(pady=(5, 0))
# Status section
status_frame = tk.LabelFrame(left_panel, text="Status", padx=10, pady=10)
status_frame.pack(fill=tk.X, pady=(0, 10))
# Robot status
tk.Label(status_frame, text="Robot:", font=("Arial", 9, "bold")).grid(row=0, column=0, sticky="w")
self.robot_status = tk.Label(status_frame, text="Not Loaded", fg="red")
self.robot_status.grid(row=0, column=1, sticky="w", padx=(5, 0))
# Environment status
tk.Label(status_frame, text="Environment:", font=("Arial", 9, "bold")).grid(row=1, column=0, sticky="w")
self.env_status = tk.Label(status_frame, text="Not Loaded", fg="red")
self.env_status.grid(row=1, column=1, sticky="w", padx=(5, 0))
# Simulation status
tk.Label(status_frame, text="Simulation:", font=("Arial", 9, "bold")).grid(row=2, column=0, sticky="w")
self.sim_status = tk.Label(status_frame, text="Stopped", fg="orange")
self.sim_status.grid(row=2, column=1, sticky="w", padx=(5, 0))
def _create_log_panel(self):
"""Create log panel with copyable text"""
from tkinter import scrolledtext
# Right panel - Log display
right_panel = tk.Frame(self.main_frame)
right_panel.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
# Log display frame
log_label = tk.Label(right_panel, text="System Log", font=("Arial", 12, "bold"))
log_label.pack(pady=(0, 5))
# Scrollable text widget for logs (can select and copy)
self.log_text = scrolledtext.ScrolledText(right_panel, height=25, wrap=tk.WORD)
self.log_text.pack(fill=tk.BOTH, expand=True, padx=5, pady=5)
# Make it read-only initially but still selectable
self.log_text.config(state=tk.DISABLED)
def _create_bottom_panel(self):
"""Create bottom panel with viewer info and exit button"""
bottom_frame = tk.Frame(self.root)
bottom_frame.pack(fill=tk.X, padx=10, pady=(0, 10))
tk.Label(bottom_frame, text="PyBullet Viewer:", font=("Arial", 9, "bold")).pack(side=tk.LEFT, padx=(0, 5))
tk.Label(bottom_frame, text="Use external PyBullet GUI window for 3D visualization",
fg="gray").pack(side=tk.LEFT)
# Exit button
exit_btn = tk.Button(bottom_frame, text="Exit", command=self.on_closing,
bg="#f44336", fg="white", padx=20)
exit_btn.pack(side=tk.RIGHT)
def initialize_simulation(self):
"""Initialize PyBullet simulation"""
try:
# Connect to PyBullet in GUI mode
self.physics_client = p.connect(p.GUI)
p.setAdditionalSearchPath(pybullet_data.getDataPath())
# Configure visualization
p.configureDebugVisualizer(p.COV_ENABLE_GUI, 1)
p.configureDebugVisualizer(p.COV_ENABLE_SHADOWS, 1)
p.configureDebugVisualizer(p.COV_ENABLE_RENDERING, 1)
# Set camera from config or defaults
self._reset_camera_view()
# Load configuration
self.config_loader = ConfigLoader()
# Setup environment
self.environment = Environment(self.config_loader, self.physics_client)
self.environment.setup_environment()
self.env_status.config(text="Loaded", fg="green")
# Initialize arm controller
self.arm_controller = create_arm_controller(self.config_loader, self.physics_client)
self.robot_status.config(text="Loaded", fg="green")
self.update_status("System ready")
except Exception as e:
self.update_status(f"Initialization failed: {e}")
messagebox.showerror("Initialization Error", f"Failed to initialize simulation:\n{e}")
def open_config_window(self):
"""Open configuration management window"""
if not self.config_window:
self.config_window = ConfigWindow(
self.root,
on_apply_callback=self.on_config_applied
)
self.config_window.show()
def on_config_applied(self, new_config: Dict[str, Any]):
"""Handle configuration changes from config window"""
self.update_status("Applying configuration...")
try:
# Save current robot state if exists
robot_state = None
if self.arm_controller and self.arm_controller.is_initialized:
robot_state = self.arm_controller.get_current_joint_states()
# Clear current environment
if self.environment:
self.environment.cleanup()
# Note: Robot cleanup is handled by ArmController internally
# Create new ConfigLoader with updated configuration
self.config_loader = ConfigLoader()
# Update the config loader's internal config directly
self.config_loader._config = new_config
# Recreate environment
self.environment = Environment(self.config_loader, self.physics_client)
self.environment.setup_environment()
# Recreate arm controller
self.arm_controller = create_arm_controller(self.config_loader, self.physics_client)
# Restore robot state if available
if robot_state:
try:
positions = [state['position'] for state in robot_state.values()]
self.arm_controller.move_to_joint_positions(positions)
except Exception:
# State restoration failed, continue with defaults
pass
self.update_status("Configuration applied")
except Exception as e:
self.update_status(f"Configuration error: {e}")
messagebox.showerror("Configuration Error", f"Failed to apply configuration:\n{e}")
def reload_configuration(self):
"""Reload configuration from file"""
try:
self.config_loader = ConfigLoader()
self.on_config_applied(self.config_loader.get_full_config())
self.update_status("Configuration reloaded")
except Exception as e:
self.update_status(f"Reload failed: {e}")
def start_simulation(self):
"""Start simulation loop"""
if not self.simulation_running:
self.simulation_running = True
self.sim_status.config(text="Running", fg="green")
self.start_btn.config(state=tk.DISABLED)
self.pause_btn.config(state=tk.NORMAL)
# Start simulation thread
self.simulation_thread = threading.Thread(target=self.simulation_loop, daemon=True)
self.simulation_thread.start()
self.update_status("Simulation running")
def pause_simulation(self):
"""Pause simulation loop"""
if self.simulation_running:
self.simulation_running = False
self.sim_status.config(text="Paused", fg="orange")
self.start_btn.config(state=tk.NORMAL)
self.pause_btn.config(state=tk.DISABLED)
self.update_status("Simulation paused")
def simulation_loop(self):
"""Main simulation loop (runs in separate thread)"""
while self.simulation_running:
try:
p.stepSimulation(physicsClientId=self.physics_client)
# Get timestep from config
timestep = self.config_loader.get_full_config().get('simulation', {}).get('timestep', 0.01)
time.sleep(timestep)
except Exception as e:
self.update_status(f"Simulation error: {e}")
self.simulation_running = False
break
def reset_environment(self):
"""Reset environment to initial state"""
try:
# Reset robot to home position
if self.arm_controller and self.arm_controller.is_initialized:
self.arm_controller.reset_to_home_position()
# Reset environment objects
if self.environment:
self.environment.reset_environment()
# Reset camera
self._reset_camera_view()
self.update_status("Environment reset")
except Exception as e:
self.update_status(f"Reset failed: {e}")
def test_reachability(self):
"""Test reachability of task points"""
self.update_status("Testing reachability...")
try:
# Get task points
task_points = self.config_loader.get_task_points()
point_a = task_points['point_A']['position']
point_b = task_points['point_B']['position']
# Perform reachability check - this will expose the KDL problem
self._check_reachability(point_a, point_b)
except Exception as e:
import traceback
error_msg = f"Reachability test failed: {str(e)}"
self.update_status(error_msg)
print(f"ERROR: {error_msg}")
print("Full traceback:")
traceback.print_exc()
def _check_reachability(self, point_a, point_b):
"""Check if points are reachable by the robot using KDL kinematics"""
try:
# Use ArmController's precise reachability checking with KDL
# This will call KDL inverse kinematics and expose any problems
a_reachable = self.arm_controller.check_workspace_reachability(point_a)
b_reachable = self.arm_controller.check_workspace_reachability(point_b)
# Get detailed results
results = []
if a_reachable:
results.append("Point A: Reachable")
else:
results.append("Point A: Not reachable")
if b_reachable:
results.append("Point B: Reachable")
else:
results.append("Point B: Not reachable")
# Update status based on results
if a_reachable and b_reachable:
self.update_status("Both points are reachable")
else:
self.update_status(f"Reachability: {', '.join(results)}")
except Exception as e:
import traceback
error_msg = f"Reachability check failed: {str(e)}"
self.update_status(error_msg)
print(f"ERROR in _check_reachability: {error_msg}")
print("Full traceback:")
traceback.print_exc()
def _calculate_distance(self, point):
"""Calculate Euclidean distance from origin"""
return (point[0]**2 + point[1]**2 + point[2]**2) ** 0.5
def _reset_camera_view(self):
"""Reset camera to default view using config values"""
try:
# Get camera settings from config
config = self.config_loader.get_full_config() if self.config_loader else {}
camera_config = config.get('camera', {})
# Use config values with explicit defaults from config structure
default_camera = {'distance': 4.0, 'yaw': 45, 'pitch': -30, 'target': [0, 0, 0]}
distance = camera_config.get('distance', default_camera['distance'])
yaw = camera_config.get('yaw', default_camera['yaw'])
pitch = camera_config.get('pitch', default_camera['pitch'])
target = camera_config.get('target', default_camera['target'])
p.resetDebugVisualizerCamera(
cameraDistance=distance,
cameraYaw=yaw,
cameraPitch=pitch,
cameraTargetPosition=target,
physicsClientId=self.physics_client
)
except Exception as e:
# Re-raise exception for proper error handling
raise Exception(f"Failed to reset camera view: {e}")
def update_status(self, message: str):
"""Update status display with copyable text"""
import time
if hasattr(self, 'log_text'):
# Enable text widget to add content
self.log_text.config(state=tk.NORMAL)
# Add timestamp and message
timestamp = time.strftime("%H:%M:%S")
self.log_text.insert(tk.END, f"[{timestamp}] {message}\n")
# Auto-scroll to bottom
self.log_text.see(tk.END)
# Make read-only again but still selectable
self.log_text.config(state=tk.DISABLED)
def on_closing(self):
"""Handle window closing"""
if messagebox.askokcancel("Quit", "Do you want to quit the simulation?"):
try:
# Stop simulation
self.simulation_running = False
# Cleanup
if self.environment:
self.environment.cleanup()
# Disconnect PyBullet
if self.physics_client is not None:
p.disconnect(self.physics_client)
except Exception as e:
# Log cleanup failure but still close
print(f"Cleanup error: {e}")
self.root.destroy()
def run(self):
"""Start the main window"""
self.root.protocol("WM_DELETE_WINDOW", self.on_closing)
self.root.mainloop()
if __name__ == "__main__":
# Test the main window directly
app = MainWindow()
app.run()