Major refactoring to improve code quality, maintainability, and API organization for publication-quality research software. Phase 1 - Code Quality Foundation: - Add comprehensive type hints across all core modules (500+ annotations) - Create custom exception hierarchy with 20+ specialized exceptions - Replace all print() statements with proper logging (debug, info, warning, error, critical) - Enhance all docstrings with Args/Returns/Raises sections - Improve error handling with exception chaining Modified core modules: - rsi_client.py: State machine with typed exceptions, full type hints - network_handler.py: CSV logging and UDP communication with typed interfaces - config_parser.py: XML parsing with proper exception handling - safety_manager.py: Safety validation with typed limits - __init__.py: Clean exports for all public APIs Phase 5 - Namespaced API Architecture: - Restructure RSIAPI as orchestrator providing 9 specialized namespaces - Create clean separation of concerns with dedicated API classes New namespace APIs: - motion_api.py: Motion control (Cartesian, joints, trajectories) - io_api.py: Digital I/O control - krl_api.py: KRL program manipulation utilities - safety_api.py: Safety management and limits - monitoring_api.py: Live data access and monitoring - logging_api.py: CSV data logging - diagnostics_api.py: Network diagnostics (Phase 2 placeholder) - viz_api.py: Static and live visualization - tools_api.py: Utilities, debugging, inspection Breaking Changes: - No backward compatibility - clean slate API design - Old: api.start_rsi() → New: api.start() - Old: api.update_cartesian(...) → New: api.motion.update_cartesian(...) - See migration guide in PHASE_5_SUMMARY.md Benefits: - Organized and discoverable API structure - Scalable architecture for future enhancements - Type-safe with full IDE autocomplete support - Easier testing and maintenance - Professional industry-standard design pattern Files changed: 6 modified, 9 new (net -37 lines, improved organization)
272 lines
9.7 KiB
Python
272 lines
9.7 KiB
Python
"""Visualization API namespace for RSIPI."""
|
|
|
|
import logging
|
|
import os
|
|
from typing import Optional, TYPE_CHECKING
|
|
from threading import Thread
|
|
|
|
if TYPE_CHECKING:
|
|
from .rsi_client import RSIClient
|
|
|
|
|
|
class VizAPI:
|
|
"""
|
|
Data visualization interface for KUKA RSI robot control.
|
|
|
|
Provides static plot generation from CSV logs and live plotting
|
|
capabilities for real-time monitoring of robot motion.
|
|
"""
|
|
|
|
def __init__(self, client: 'RSIClient') -> None:
|
|
"""
|
|
Initialize VizAPI namespace.
|
|
|
|
Args:
|
|
client: RSIClient instance for live data access
|
|
"""
|
|
self.client = client
|
|
self.live_plotter = None
|
|
self.live_plot_thread = None
|
|
|
|
@staticmethod
|
|
def plot_static(csv_path: str, plot_type: str = "3d", overlay_path: Optional[str] = None) -> str:
|
|
"""
|
|
Generate static plots from CSV log data.
|
|
|
|
Creates matplotlib visualizations of recorded robot trajectories,
|
|
velocities, forces, and other metrics.
|
|
|
|
Args:
|
|
csv_path: Path to CSV log file
|
|
plot_type: Type of plot to generate:
|
|
- "3d": 3D trajectory visualization
|
|
- "2d_xy", "2d_xz", "2d_yz": 2D projections
|
|
- "position": Position vs time
|
|
- "velocity": Velocity vs time
|
|
- "acceleration": Acceleration vs time
|
|
- "joints": Joint angles vs time
|
|
- "force": Motor currents vs time
|
|
- "deviation": Deviation from planned path (requires overlay_path)
|
|
overlay_path: Optional CSV with planned trajectory for deviation plots
|
|
|
|
Returns:
|
|
Status message
|
|
|
|
Raises:
|
|
FileNotFoundError: If CSV file doesn't exist
|
|
ValueError: If plot_type is invalid
|
|
|
|
Example:
|
|
>>> api.viz.plot_static('logs/test.csv', '3d')
|
|
'Plot 3d generated successfully'
|
|
|
|
>>> api.viz.plot_static('logs/actual.csv', 'deviation', 'logs/planned.csv')
|
|
'Plot deviation generated successfully'
|
|
|
|
Note:
|
|
Plots are displayed interactively. Close the plot window to continue.
|
|
"""
|
|
if not os.path.exists(csv_path):
|
|
return f"CSV file not found: {csv_path}"
|
|
|
|
try:
|
|
from .static_plotter import StaticPlotter
|
|
|
|
plot_type = plot_type.lower()
|
|
|
|
match plot_type:
|
|
case "3d":
|
|
StaticPlotter.plot_3d_trajectory(csv_path)
|
|
case "2d_xy":
|
|
StaticPlotter.plot_2d_projection(csv_path, plane="xy")
|
|
case "2d_xz":
|
|
StaticPlotter.plot_2d_projection(csv_path, plane="xz")
|
|
case "2d_yz":
|
|
StaticPlotter.plot_2d_projection(csv_path, plane="yz")
|
|
case "position":
|
|
StaticPlotter.plot_position_vs_time(csv_path)
|
|
case "velocity":
|
|
StaticPlotter.plot_velocity_vs_time(csv_path)
|
|
case "acceleration":
|
|
StaticPlotter.plot_acceleration_vs_time(csv_path)
|
|
case "joints":
|
|
StaticPlotter.plot_joint_angles(csv_path)
|
|
case "force":
|
|
StaticPlotter.plot_motor_currents(csv_path)
|
|
case "deviation":
|
|
if overlay_path is None or not os.path.exists(overlay_path):
|
|
return "Deviation plot requires a valid overlay CSV file"
|
|
StaticPlotter.plot_deviation(csv_path, overlay_path)
|
|
case _:
|
|
valid_types = "3d, 2d_xy, 2d_xz, 2d_yz, position, velocity, acceleration, joints, force, deviation"
|
|
return f"Invalid plot type '{plot_type}'. Use one of: {valid_types}"
|
|
|
|
logging.info(f"Generated {plot_type} plot from {csv_path}")
|
|
return f"Plot '{plot_type}' generated successfully"
|
|
except Exception as e:
|
|
logging.error(f"Plot generation failed: {e}")
|
|
return f"Failed to generate plot '{plot_type}': {str(e)}"
|
|
|
|
def start_live_plot(self, mode: str = "3d", interval: int = 100) -> str:
|
|
"""
|
|
Start live plotting of robot data.
|
|
|
|
Opens an interactive matplotlib window that updates in real-time
|
|
as robot data is received. Useful for monitoring during operation.
|
|
|
|
Args:
|
|
mode: Plot mode:
|
|
- "3d": Live 3D trajectory
|
|
- "position": Position vs time
|
|
- "velocity": Velocity vs time
|
|
- "force": Motor currents vs time
|
|
interval: Update interval in milliseconds (default: 100ms = 10Hz)
|
|
|
|
Returns:
|
|
Status message
|
|
|
|
Example:
|
|
>>> api.viz.start_live_plot('3d', interval=100)
|
|
'Live plot started in 3d mode at 100ms interval'
|
|
|
|
>>> # Watch robot move in real-time
|
|
>>> # Close plot window or call stop_live_plot() to end
|
|
|
|
Note:
|
|
Live plotting runs in a background thread. The plot window must
|
|
remain open for updates to continue.
|
|
"""
|
|
if self.live_plotter and hasattr(self.live_plotter, 'running') and self.live_plotter.running:
|
|
return "Live plotting already active"
|
|
|
|
def runner():
|
|
from .live_plotter import LivePlotter
|
|
self.live_plotter = LivePlotter(self.client, mode=mode, interval=interval)
|
|
self.live_plotter.start()
|
|
|
|
self.live_plot_thread = Thread(target=runner, daemon=True)
|
|
self.live_plot_thread.start()
|
|
logging.info(f"Live plot started: {mode} mode at {interval}ms")
|
|
return f"Live plot started in '{mode}' mode at {interval}ms interval"
|
|
|
|
def stop_live_plot(self) -> str:
|
|
"""
|
|
Stop live plotting.
|
|
|
|
Closes the live plot window and stops the update thread.
|
|
|
|
Returns:
|
|
Status message
|
|
|
|
Example:
|
|
>>> api.viz.stop_live_plot()
|
|
'Live plotting stopped'
|
|
"""
|
|
if self.live_plotter and hasattr(self.live_plotter, 'running') and self.live_plotter.running:
|
|
self.live_plotter.stop()
|
|
logging.info("Live plotting stopped")
|
|
return "Live plotting stopped"
|
|
return "No live plot is currently running"
|
|
|
|
def change_live_plot_mode(self, mode: str) -> str:
|
|
"""
|
|
Change the mode of an active live plot.
|
|
|
|
Switches between different visualization modes without restarting
|
|
the plot window.
|
|
|
|
Args:
|
|
mode: New plot mode (e.g., '3d', 'position', 'velocity', 'force')
|
|
|
|
Returns:
|
|
Status message
|
|
|
|
Example:
|
|
>>> api.viz.start_live_plot('3d')
|
|
>>> # ... watch for a while ...
|
|
>>> api.viz.change_live_plot_mode('position')
|
|
'Live plot mode changed to position'
|
|
"""
|
|
if self.live_plotter and hasattr(self.live_plotter, 'running') and self.live_plotter.running:
|
|
if hasattr(self.live_plotter, 'change_mode'):
|
|
self.live_plotter.change_mode(mode)
|
|
logging.info(f"Live plot mode changed to: {mode}")
|
|
return f"Live plot mode changed to '{mode}'"
|
|
else:
|
|
return "Live plotter does not support mode changing"
|
|
return "No live plot is active to change mode"
|
|
|
|
@staticmethod
|
|
def visualize_csv_log(csv_file: str, export: bool = False) -> None:
|
|
"""
|
|
Comprehensive visualization of CSV log file.
|
|
|
|
Generates multiple plots (trajectory, joints, force) using the
|
|
KukaRSIVisualiser. Optionally exports plots to files.
|
|
|
|
Args:
|
|
csv_file: Path to CSV log file
|
|
export: If True, save plots to disk instead of displaying
|
|
|
|
Example:
|
|
>>> # Display interactive plots
|
|
>>> api.viz.visualize_csv_log('logs/test.csv')
|
|
|
|
>>> # Export plots to files
|
|
>>> api.viz.visualize_csv_log('logs/test.csv', export=True)
|
|
|
|
Note:
|
|
This generates three separate plots:
|
|
- 3D trajectory with start/end markers
|
|
- Joint angle evolution
|
|
- Motor current trends
|
|
"""
|
|
from .kuka_visualiser import KukaRSIVisualiser
|
|
|
|
visualizer = KukaRSIVisualiser(csv_file)
|
|
visualizer.plot_trajectory()
|
|
visualizer.plot_joint_positions()
|
|
visualizer.plot_force_trends()
|
|
|
|
if export:
|
|
visualizer.export_graphs()
|
|
logging.info(f"Exported visualizations for {csv_file}")
|
|
else:
|
|
logging.info(f"Displayed visualizations for {csv_file}")
|
|
|
|
@staticmethod
|
|
def compare_runs(file1: str, file2: str) -> str:
|
|
"""
|
|
Generate comparison plots for two test runs.
|
|
|
|
Visualizes deviations between two recorded trajectories for
|
|
repeatability analysis.
|
|
|
|
Args:
|
|
file1: Path to first CSV log file
|
|
file2: Path to second CSV log file
|
|
|
|
Returns:
|
|
Status message
|
|
|
|
Example:
|
|
>>> api.viz.compare_runs('run1.csv', 'run2.csv')
|
|
'Comparison plots generated successfully'
|
|
|
|
Note:
|
|
Uses plot_static() with deviation mode internally.
|
|
"""
|
|
if not os.path.exists(file1):
|
|
return f"First file not found: {file1}"
|
|
if not os.path.exists(file2):
|
|
return f"Second file not found: {file2}"
|
|
|
|
try:
|
|
from .static_plotter import StaticPlotter
|
|
StaticPlotter.plot_deviation(file1, file2)
|
|
logging.info(f"Generated comparison plot: {file1} vs {file2}")
|
|
return "Comparison plots generated successfully"
|
|
except Exception as e:
|
|
logging.error(f"Comparison plot failed: {e}")
|
|
return f"Failed to generate comparison: {str(e)}"
|