RoboticArmTest/src/gui/config_window.py
sladro 3b5306611a Implement KDL kinematics engine and complete core framework
Major Features Added:
- KDL-based kinematics engine with world/local coordinate transformation
- Complete GUI system with configuration management
- Robot loader with URDF parsing and joint control
- Enhanced environment system with transport object management
- Main application entry point with dependency validation

Technical Improvements:
- World coordinate to robot base coordinate transformation in KDL
- Real-time configuration updates through GUI
- Comprehensive error handling and validation
- Configuration-driven design throughout
- Robot model scaling adjusted to 0.2x for proper visualization

Files Added:
- src/robot/kinematics.py: KDL forward/inverse kinematics with coordinate transforms
- src/gui/: Complete GUI framework with main window and config management
- main.py: Application entry point with environment validation
- models/*.stl: Robot link mesh files for URDF visualization
- tests/: Unit test framework for core components

This commit establishes the complete foundation for robotic arm path planning
and task execution development.

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-09-10 14:37:27 +08:00

400 lines
19 KiB
Python

import tkinter as tk
from tkinter import ttk, messagebox
import json
from typing import Dict, Any, Callable, Tuple, List
class ConfigWindow:
# UI Constants extracted from hardcoded values
WINDOW_GEOMETRY = "800x600"
BUTTON_COLORS = {
"apply": "#4CAF50",
"save": "#2196F3",
"reset": "#f44336"
}
FONT_TITLE = ("Arial", 12, "bold")
FONT_SECTION = ("Arial", 10, "bold")
PADDING_STANDARD = 5
PADDING_LARGE = 10
ENTRY_WIDTH_STANDARD = 10
ENTRY_WIDTH_LONG = 40
BUTTON_PADDING = 20
def __init__(self, parent, config_path: str = "config.json", on_apply_callback: Callable = None):
self.parent = parent
self.config_path = config_path
self.on_apply_callback = on_apply_callback
self.window = None
self.config_data = {}
self.entry_widgets = {}
# Load current configuration
self.load_config()
def load_config(self):
"""Load configuration from file"""
try:
with open(self.config_path, 'r', encoding='utf-8') as f:
self.config_data = json.load(f)
except Exception as e:
# Follow fail-fast principle - raise exception instead of hiding it
raise RuntimeError(f"Failed to load configuration from {self.config_path}: {e}")
def save_config(self):
"""Save configuration to file"""
try:
with open(self.config_path, 'w', encoding='utf-8') as f:
json.dump(self.config_data, f, indent=2, ensure_ascii=False)
return True
except Exception as e:
# Log error but allow UI to handle it
raise RuntimeError(f"Failed to save configuration to {self.config_path}: {e}")
def show(self):
"""Show configuration window"""
if self.window and self.window.winfo_exists():
self.window.lift()
return
self.window = tk.Toplevel(self.parent)
self.window.title("Configuration Management")
self.window.geometry(self.WINDOW_GEOMETRY)
# Create notebook for tabs
notebook = ttk.Notebook(self.window)
notebook.pack(fill=tk.BOTH, expand=True, padx=self.PADDING_LARGE, pady=self.PADDING_LARGE)
# Create tabs
self.create_robot_tab(notebook)
self.create_wall_tab(notebook)
self.create_hole_tab(notebook)
self.create_task_points_tab(notebook)
self.create_transport_object_tab(notebook)
self.create_simulation_tab(notebook)
# Create buttons frame
button_frame = tk.Frame(self.window)
button_frame.pack(fill=tk.X, padx=self.PADDING_LARGE, pady=self.PADDING_STANDARD)
# Apply button
apply_btn = tk.Button(button_frame, text="Apply", command=self.apply_changes,
bg=self.BUTTON_COLORS["apply"], fg="white", padx=self.BUTTON_PADDING)
apply_btn.pack(side=tk.LEFT, padx=self.PADDING_STANDARD)
# Save button
save_btn = tk.Button(button_frame, text="Save to File", command=self.save_changes,
bg=self.BUTTON_COLORS["save"], fg="white", padx=self.BUTTON_PADDING)
save_btn.pack(side=tk.LEFT, padx=self.PADDING_STANDARD)
# Reset button
reset_btn = tk.Button(button_frame, text="Reset", command=self.reset_values,
bg=self.BUTTON_COLORS["reset"], fg="white", padx=self.BUTTON_PADDING)
reset_btn.pack(side=tk.LEFT, padx=self.PADDING_STANDARD)
# Close button
close_btn = tk.Button(button_frame, text="Close", command=self.window.destroy,
padx=self.BUTTON_PADDING)
close_btn.pack(side=tk.RIGHT, padx=self.PADDING_STANDARD)
def _create_vector_input(self, parent: tk.Frame, row: int, label: str, key_prefix: str,
values: List[float], labels: List[str] = None) -> None:
"""Create vector input fields (x, y, z or custom labels)"""
if labels is None:
labels = ["X:", "Y:", "Z:"]
tk.Label(parent, text=label).grid(row=row, column=0, sticky="e",
padx=self.PADDING_STANDARD, pady=self.PADDING_STANDARD)
for i, (lbl, val) in enumerate(zip(labels, values)):
tk.Label(parent, text=lbl).grid(row=row + i, column=1, sticky="e",
padx=self.PADDING_STANDARD)
entry_key = f"{key_prefix}.{lbl.lower().rstrip(':')}"
self.entry_widgets[entry_key] = tk.Entry(parent, width=self.ENTRY_WIDTH_STANDARD)
self.entry_widgets[entry_key].grid(row=row + i, column=2,
padx=self.PADDING_STANDARD, pady=self.PADDING_STANDARD)
self.entry_widgets[entry_key].insert(0, str(val))
def _create_title_label(self, parent: tk.Frame, text: str, row: int = 0,
font_type: str = "title") -> None:
"""Create a title label"""
font = self.FONT_TITLE if font_type == "title" else self.FONT_SECTION
tk.Label(parent, text=text, font=font).grid(
row=row, column=0, columnspan=6, pady=self.PADDING_LARGE)
def _get_config_value(self, *keys, default=None):
"""Safely get nested configuration value"""
value = self.config_data
for key in keys:
if isinstance(value, dict):
value = value.get(key, {})
else:
return default
return value if value != {} else default
def create_robot_tab(self, notebook):
"""Create robot configuration tab"""
frame = ttk.Frame(notebook)
notebook.add(frame, text="Robot")
# Title
self._create_title_label(frame, "Robot Configuration")
# Model path
tk.Label(frame, text="Model Path:").grid(row=1, column=0, sticky="e",
padx=self.PADDING_STANDARD, pady=self.PADDING_STANDARD)
self.entry_widgets['robot.model_path'] = tk.Entry(frame, width=self.ENTRY_WIDTH_LONG)
self.entry_widgets['robot.model_path'].grid(row=1, column=1, columnspan=3,
padx=self.PADDING_STANDARD, pady=self.PADDING_STANDARD)
self.entry_widgets['robot.model_path'].insert(0, self._get_config_value('robot', 'model_path', default=''))
# Base position
base_pos = self._get_config_value('robot', 'base_position', default=[0, 0, 0])
self._create_vector_input(frame, 2, "Base Position:", "robot.base_position", base_pos)
# Base orientation
base_orient = self._get_config_value('robot', 'base_orientation', default=[0, 0, 0])
self._create_vector_input(frame, 5, "Base Orientation:", "robot.base_orientation",
base_orient, labels=["Roll:", "Pitch:", "Yaw:"])
def _create_dimension_inputs(self, parent: tk.Frame, row: int, key_prefix: str,
dimensions: Dict[str, float], dim_names: List[str]) -> None:
"""Create dimension input fields"""
tk.Label(parent, text="Dimensions:").grid(row=row, column=0, sticky="e",
padx=self.PADDING_STANDARD, pady=self.PADDING_STANDARD)
for i, dim_name in enumerate(dim_names):
tk.Label(parent, text=f"{dim_name.capitalize()}:").grid(row=row + i, column=1, sticky="e",
padx=self.PADDING_STANDARD)
entry_key = f"{key_prefix}.{dim_name}"
self.entry_widgets[entry_key] = tk.Entry(parent, width=self.ENTRY_WIDTH_STANDARD)
self.entry_widgets[entry_key].grid(row=row + i, column=2,
padx=self.PADDING_STANDARD, pady=self.PADDING_STANDARD)
# Get default value from config or use 0
default_val = dimensions.get(dim_name, 0.0)
self.entry_widgets[entry_key].insert(0, str(default_val))
def create_wall_tab(self, notebook):
"""Create wall configuration tab"""
frame = ttk.Frame(notebook)
notebook.add(frame, text="Wall")
# Title
self._create_title_label(frame, "Wall Configuration")
# Wall position
wall_pos = self._get_config_value('wall', 'position', default=[2.0, 0.0, 1.0])
self._create_vector_input(frame, 1, "Position:", "wall.position", wall_pos)
# Wall dimensions
wall_dims = self._get_config_value('wall', 'dimensions', default={})
# Set defaults if not in config
wall_dims.setdefault('width', 3.0)
wall_dims.setdefault('height', 2.5)
wall_dims.setdefault('thickness', 0.2)
self._create_dimension_inputs(frame, 4, "wall.dimensions", wall_dims,
['width', 'height', 'thickness'])
def create_hole_tab(self, notebook):
"""Create hole configuration tab"""
frame = ttk.Frame(notebook)
notebook.add(frame, text="Hole")
# Title
self._create_title_label(frame, "Hole Configuration")
# Hole position relative to wall
hole_pos = self._get_config_value('hole', 'position_relative_to_wall', default=[0, 0, 0])
self._create_vector_input(frame, 1, "Position (relative):", "hole.position", hole_pos)
# Hole dimensions
hole_dims = self._get_config_value('hole', 'dimensions', default={})
hole_dims.setdefault('width', 0.6)
hole_dims.setdefault('height', 0.6)
self._create_dimension_inputs(frame, 4, "hole.dimensions", hole_dims, ['width', 'height'])
def _create_horizontal_vector_input(self, parent: tk.Frame, row: int, label: str,
key_prefix: str, values: List[float]) -> None:
"""Create horizontal vector input fields (for compact layout)"""
labels = ["X:", "Y:", "Z:"]
for i, (lbl, val) in enumerate(zip(labels, values)):
tk.Label(parent, text=lbl).grid(row=row, column=i*2, sticky="e",
padx=self.PADDING_STANDARD)
entry_key = f"{key_prefix}.{lbl.lower().rstrip(':')}"
self.entry_widgets[entry_key] = tk.Entry(parent, width=self.ENTRY_WIDTH_STANDARD)
self.entry_widgets[entry_key].grid(row=row, column=i*2+1,
padx=self.PADDING_STANDARD, pady=self.PADDING_STANDARD)
self.entry_widgets[entry_key].insert(0, str(val))
def create_task_points_tab(self, notebook):
"""Create task points configuration tab"""
frame = ttk.Frame(notebook)
notebook.add(frame, text="Task Points")
# Title
self._create_title_label(frame, "Task Points Configuration")
# Point A
self._create_title_label(frame, "Point A (Pick-up):", row=1, font_type="section")
point_a = self._get_config_value('task_points', 'point_A', 'position',
default=[1.0, 0.5, 0.8])
self._create_horizontal_vector_input(frame, 2, "Point A", "point_a", point_a)
# Point B
self._create_title_label(frame, "Point B (Drop-off):", row=3, font_type="section")
point_b = self._get_config_value('task_points', 'point_B', 'position',
default=[3.0, 0.5, 0.8])
self._create_horizontal_vector_input(frame, 4, "Point B", "point_b", point_b)
def create_transport_object_tab(self, notebook):
"""Create transport object configuration tab"""
frame = ttk.Frame(notebook)
notebook.add(frame, text="Transport Object")
# Title
self._create_title_label(frame, "Transport Object Configuration")
# Initial position
obj_pos = self._get_config_value('transport_object', 'initial_position',
default=[1.0, 0.5, 0.5])
self._create_vector_input(frame, 1, "Initial Position:", "object.position", obj_pos)
# Dimensions
obj_dims = self._get_config_value('transport_object', 'dimensions', default={})
obj_dims.setdefault('width', 0.1)
obj_dims.setdefault('height', 0.1)
obj_dims.setdefault('depth', 0.1)
self._create_dimension_inputs(frame, 4, "object.dimensions", obj_dims,
['width', 'height', 'depth'])
# Mass
mass = self._get_config_value('transport_object', 'mass', default=0.5)
tk.Label(frame, text="Mass (kg):").grid(row=7, column=0, sticky="e",
padx=self.PADDING_STANDARD, pady=self.PADDING_STANDARD)
self.entry_widgets['object.mass'] = tk.Entry(frame, width=self.ENTRY_WIDTH_STANDARD)
self.entry_widgets['object.mass'].grid(row=7, column=2,
padx=self.PADDING_STANDARD, pady=self.PADDING_STANDARD)
self.entry_widgets['object.mass'].insert(0, str(mass))
def create_simulation_tab(self, notebook):
"""Create simulation configuration tab"""
frame = ttk.Frame(notebook)
notebook.add(frame, text="Simulation")
# Title
self._create_title_label(frame, "Simulation Configuration")
# Timestep
timestep = self._get_config_value('simulation', 'timestep', default=0.01)
tk.Label(frame, text="Timestep:").grid(row=1, column=0, sticky="e",
padx=self.PADDING_STANDARD, pady=self.PADDING_STANDARD)
self.entry_widgets['simulation.timestep'] = tk.Entry(frame, width=self.ENTRY_WIDTH_STANDARD)
self.entry_widgets['simulation.timestep'].grid(row=1, column=1,
padx=self.PADDING_STANDARD, pady=self.PADDING_STANDARD)
self.entry_widgets['simulation.timestep'].insert(0, str(timestep))
# Gravity
gravity = self._get_config_value('simulation', 'gravity', default=[0, 0, -9.81])
self._create_vector_input(frame, 2, "Gravity:", "simulation.gravity", gravity)
# Real-time mode
real_time = self._get_config_value('simulation', 'real_time', default=False)
tk.Label(frame, text="Real-time Mode:").grid(row=5, column=0, sticky="e",
padx=self.PADDING_STANDARD, pady=self.PADDING_STANDARD)
self.real_time_var = tk.BooleanVar()
self.real_time_var.set(real_time)
tk.Checkbutton(frame, variable=self.real_time_var).grid(row=5, column=1,
padx=self.PADDING_STANDARD,
pady=self.PADDING_STANDARD, sticky="w")
def _get_vector_from_widgets(self, key_prefix: str) -> List[float]:
"""Get vector values from widgets"""
return [
float(self.entry_widgets[f'{key_prefix}.x'].get()),
float(self.entry_widgets[f'{key_prefix}.y'].get()),
float(self.entry_widgets[f'{key_prefix}.z'].get())
]
def _get_vector_custom_from_widgets(self, key_prefix: str, labels: List[str]) -> List[float]:
"""Get vector values from widgets with custom labels"""
return [float(self.entry_widgets[f'{key_prefix}.{label}'].get()) for label in labels]
def _ensure_nested_dict(self, *keys) -> None:
"""Ensure nested dictionary structure exists"""
current = self.config_data
for key in keys:
if key not in current:
current[key] = {}
current = current[key]
def get_values_from_widgets(self) -> Dict[str, Any]:
"""Get values from all entry widgets and update config_data"""
try:
# Ensure all necessary nested structures exist
self._ensure_nested_dict('robot')
self._ensure_nested_dict('wall', 'dimensions')
self._ensure_nested_dict('hole', 'dimensions')
self._ensure_nested_dict('task_points', 'point_A')
self._ensure_nested_dict('task_points', 'point_B')
self._ensure_nested_dict('transport_object', 'dimensions')
self._ensure_nested_dict('simulation')
# Robot configuration
self.config_data['robot']['model_path'] = self.entry_widgets['robot.model_path'].get()
self.config_data['robot']['base_position'] = self._get_vector_from_widgets('robot.base_position')
self.config_data['robot']['base_orientation'] = self._get_vector_custom_from_widgets(
'robot.base_orientation', ['roll', 'pitch', 'yaw'])
# Wall configuration
self.config_data['wall']['position'] = self._get_vector_from_widgets('wall.position')
for dim in ['width', 'height', 'thickness']:
self.config_data['wall']['dimensions'][dim] = float(
self.entry_widgets[f'wall.dimensions.{dim}'].get())
# Hole configuration
self.config_data['hole']['position_relative_to_wall'] = self._get_vector_from_widgets('hole.position')
for dim in ['width', 'height']:
self.config_data['hole']['dimensions'][dim] = float(
self.entry_widgets[f'hole.dimensions.{dim}'].get())
# Task points
self.config_data['task_points']['point_A']['position'] = self._get_vector_from_widgets('point_a')
self.config_data['task_points']['point_B']['position'] = self._get_vector_from_widgets('point_b')
# Transport object
self.config_data['transport_object']['initial_position'] = self._get_vector_from_widgets('object.position')
for dim in ['width', 'height', 'depth']:
self.config_data['transport_object']['dimensions'][dim] = float(
self.entry_widgets[f'object.dimensions.{dim}'].get())
self.config_data['transport_object']['mass'] = float(self.entry_widgets['object.mass'].get())
# Simulation
self.config_data['simulation']['timestep'] = float(self.entry_widgets['simulation.timestep'].get())
self.config_data['simulation']['gravity'] = self._get_vector_from_widgets('simulation.gravity')
self.config_data['simulation']['real_time'] = self.real_time_var.get()
return self.config_data
except (ValueError, KeyError) as e:
# Allow UI to handle the error display
raise ValueError(f"Invalid input or configuration structure: {e}")
def apply_changes(self):
"""Apply configuration changes"""
config = self.get_values_from_widgets()
if self.on_apply_callback:
self.on_apply_callback(config)
def save_changes(self):
"""Save configuration changes to file"""
self.config_data = self.get_values_from_widgets()
self.save_config()
def reset_values(self):
"""Reset values to original configuration"""
self.load_config()
# Refresh window
if self.window:
self.window.destroy()
self.show()