Skip to content

Commit 9be901b

Browse files
committed
feat(tempcal IMU): Move the tempcal IMU feature to a plugin
The big advantage is that it allows for easier testing and development of the tempcal IMU (and other similar) feature in isolation from the rest of the codebase.
1 parent 252d4ad commit 9be901b

21 files changed

+1941
-355
lines changed

ARCHITECTURE_plugins.md

Lines changed: 180 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,180 @@
1+
# Plugin Architecture
2+
3+
This document describes the plugin architecture of the ArduPilot Methodic Configurator,
4+
which allows extending the application with modular components for UI enhancements and workflow automation.
5+
6+
## Overview
7+
8+
The plugin system enables developers to add new functionality to the application without modifying the core codebase.
9+
There are two types of plugins:
10+
11+
1. **UI Plugins**: Provide persistent user interface components (e.g., motor test panels)
12+
2. **Workflow Plugins**: Execute triggered actions (e.g., IMU temperature calibration workflows)
13+
14+
## Architecture Principles
15+
16+
### Separation of Concerns
17+
18+
- **Business Logic**: Encapsulated in data models with no UI dependencies
19+
- **UI Coordination**: Handled by coordinators that orchestrate user interactions
20+
- **Dependency Injection**: UI callbacks injected into business logic for testability
21+
22+
### Plugin Lifecycle
23+
24+
- **Registration**: Plugins register themselves with factories during application startup
25+
- **Discovery**: The system discovers available plugins through factory queries
26+
- **Instantiation**: Plugins are created on-demand with appropriate context
27+
- **Execution**: UI plugins remain active; workflow plugins execute once and complete
28+
29+
## UI Plugin System
30+
31+
### UI Components
32+
33+
#### Plugin Factory UI (`plugin_factory_ui.py`)
34+
35+
- Manages registration and creation of UI plugins
36+
- Provides a registry of plugin creators
37+
- Ensures unique plugin names
38+
39+
#### Plugin Constants (`plugin_constants.py`)
40+
41+
- Defines string constants for plugin identifiers
42+
- Centralizes plugin naming for consistency
43+
44+
#### Registration (`__main__.py`)
45+
46+
- Plugins register their creators during application initialization
47+
- Ensures plugins are available when needed
48+
49+
### UI Implementation Pattern
50+
51+
```python
52+
# 1. Define plugin constant
53+
PLUGIN_MOTOR_TEST = "motor_test"
54+
55+
# 2. Create plugin view class
56+
class MotorTestView:
57+
def __init__(self, parent, model, base_window):
58+
# Implementation
59+
60+
# 3. Create factory function
61+
def create_motor_test_view(parent, model, base_window):
62+
return MotorTestView(parent, model, base_window)
63+
64+
# 4. Register plugin
65+
plugin_factory_ui.register(PLUGIN_MOTOR_TEST, create_motor_test_view)
66+
```
67+
68+
### UI usage in Parameter Editor
69+
70+
- UI plugins are instantiated when a parameter file with plugin configuration is selected
71+
- Plugins receive parent frame, data model, and base window references
72+
- Plugins manage their own lifecycle and cleanup
73+
74+
## Workflow Plugin System
75+
76+
### Workflow Components
77+
78+
#### Plugin Factory Workflow (`plugin_factory_workflow.py`)
79+
80+
- Manages registration and creation of workflow plugins
81+
- Symmetric API to UI plugin factory
82+
- Handles one-time execution workflows
83+
84+
#### Data Models
85+
86+
- Contain business logic with no UI dependencies
87+
- Use dependency injection for user interactions
88+
- Implement callback-based interfaces for testability
89+
90+
#### Workflow Coordinators
91+
92+
- Handle UI orchestration for workflows
93+
- Manage progress indicators and user feedback
94+
- Ensure proper cleanup after execution
95+
96+
### Workflow Implementation Pattern
97+
98+
```python
99+
# 1. Define plugin constant
100+
PLUGIN_TEMPCAL_IMU = "tempcal_imu"
101+
102+
# 2. Create data model class
103+
class TempCalIMUDataModel:
104+
def __init__(self, config_manager, step_filename, callbacks...):
105+
# Business logic only
106+
107+
def run_calibration(self) -> bool:
108+
# Orchestrate workflow using injected callbacks
109+
110+
# 3. Create workflow coordinator class
111+
class TempCalIMUWorkflow:
112+
def __init__(self, root_window, data_model):
113+
# UI coordination
114+
115+
def run_workflow(self) -> bool:
116+
# Execute workflow with UI feedback
117+
118+
# 4. Create factory function
119+
def create_tempcal_imu_workflow(root_window, data_model):
120+
return TempCalIMUWorkflow(root_window, data_model)
121+
122+
# 5. Register plugin
123+
plugin_factory_workflow.register(PLUGIN_TEMPCAL_IMU, create_tempcal_imu_workflow)
124+
```
125+
126+
### Workflow usage in Parameter Editor
127+
128+
- Workflow plugins are triggered when specific parameter files are selected
129+
- Configuration manager creates data models with injected UI callbacks
130+
- Workflow coordinators handle execution and provide user feedback
131+
- Cleanup ensures resources are released after completion
132+
133+
## Configuration Integration
134+
135+
### JSON Schema Updates
136+
137+
The configuration schema supports plugin definitions:
138+
139+
```json
140+
{
141+
"plugin": {
142+
"name": "tempcal_imu",
143+
"placement": "workflow"
144+
}
145+
}
146+
```
147+
148+
### Supported Placements
149+
150+
- `"left"`: Plugin appears left of scrollable content
151+
- `"top"`: Plugin appears above content
152+
- `"workflow"`: Plugin executes as a triggered workflow
153+
154+
## Testing Strategy
155+
156+
### Unit Testing
157+
158+
- Business logic tested in isolation with mocked callbacks
159+
- Plugin factories tested for registration and creation
160+
- Data models tested for all execution paths
161+
162+
### Integration Testing
163+
164+
- End-to-end plugin execution workflows
165+
- UI interaction verification
166+
- Configuration loading and plugin discovery
167+
168+
## Implementation Notes
169+
170+
### Error Handling
171+
172+
- Plugin loading failures don't prevent application startup
173+
- Individual plugin errors are logged and isolated
174+
- Graceful degradation when plugins are unavailable
175+
176+
### Performance Considerations
177+
178+
- Lazy loading prevents startup delays
179+
- Plugin instantiation on-demand reduces memory usage
180+
- Callback-based design minimizes coupling overhead

ardupilot_methodic_configurator/__main__.py

Lines changed: 22 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -48,8 +48,8 @@
4848
from ardupilot_methodic_configurator.frontend_tkinter_parameter_editor import ParameterEditorWindow
4949
from ardupilot_methodic_configurator.frontend_tkinter_project_opener import VehicleProjectOpenerWindow
5050
from ardupilot_methodic_configurator.frontend_tkinter_show import show_error_message
51-
from ardupilot_methodic_configurator.plugin_constants import PLUGIN_MOTOR_TEST
52-
from ardupilot_methodic_configurator.plugin_factory import plugin_factory
51+
from ardupilot_methodic_configurator.plugin_factory_ui import plugin_factory_ui
52+
from ardupilot_methodic_configurator.plugin_factory_workflow import plugin_factory_workflow
5353

5454

5555
def register_plugins() -> None:
@@ -62,9 +62,12 @@ def register_plugins() -> None:
6262
# Import and register motor test plugin
6363
# pylint: disable=import-outside-toplevel, cyclic-import
6464
from ardupilot_methodic_configurator.frontend_tkinter_motor_test import register_motor_test_plugin # noqa: PLC0415
65+
from ardupilot_methodic_configurator.frontend_tkinter_tempcal_imu import register_tempcal_imu_plugin # noqa: PLC0415
66+
6567
# pylint: enable=import-outside-toplevel, cyclic-import
6668

6769
register_motor_test_plugin()
70+
register_tempcal_imu_plugin()
6871

6972
# Add more plugin registrations here in the future
7073

@@ -85,12 +88,25 @@ def validate_plugin_registry(local_filesystem: LocalFilesystem) -> None:
8588
if plugin and plugin.get("name"):
8689
configured_plugins.add(plugin["name"])
8790

88-
# Verify each configured plugin is registered
91+
# Verify each configured plugin is registered (check both UI and workflow factories)
8992
for plugin_name in configured_plugins:
90-
if not plugin_factory.is_registered(plugin_name):
93+
is_ui_plugin = plugin_factory_ui.is_registered(plugin_name)
94+
is_workflow_plugin = plugin_factory_workflow.is_registered(plugin_name)
95+
96+
if not is_ui_plugin and not is_workflow_plugin:
97+
available_ui = ", ".join(plugin_factory_ui.get_registered_plugins())
98+
available_workflow = ", ".join(plugin_factory_workflow.get_registered_plugins())
9199
logging_error(
92-
_("Plugin '%(plugin_name)s' is configured but not registered. Available plugins: %(available)s"),
93-
{"plugin_name": plugin_name, "available": PLUGIN_MOTOR_TEST},
100+
_(
101+
"Plugin '%(plugin_name)s' is configured but not registered. "
102+
"Available UI plugins: %(ui_plugins)s. "
103+
"Available workflow plugins: %(workflow_plugins)s"
104+
),
105+
{
106+
"plugin_name": plugin_name,
107+
"ui_plugins": available_ui or _("none"),
108+
"workflow_plugins": available_workflow or _("none"),
109+
},
94110
)
95111

96112

ardupilot_methodic_configurator/backend_filesystem.py

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -639,9 +639,8 @@ def remove_created_files_and_vehicle_dir(self) -> str:
639639
def getcwd() -> str:
640640
return os_getcwd()
641641

642-
def tempcal_imu_result_param_tuple(self) -> tuple[str, str]:
643-
tempcal_imu_result_param_filename = "03_imu_temperature_calibration_results.param"
644-
return tempcal_imu_result_param_filename, os_path.join(self.vehicle_dir, tempcal_imu_result_param_filename)
642+
def get_configuration_file_fullpath(self, filename: str) -> str:
643+
return os_path.join(self.vehicle_dir, filename)
645644

646645
def copy_fc_values_to_file(self, selected_file: str, params: dict[str, float]) -> int:
647646
ret = 0

0 commit comments

Comments
 (0)