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>
400 lines
19 KiB
Python
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() |