Skip to content

Commit 4206d9d

Browse files
authored
Merge branch 'master' into version-bump
2 parents 08a655c + ec2f219 commit 4206d9d

54 files changed

Lines changed: 3145 additions & 1302 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/dependabot.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ updates:
1010
cooldown:
1111
default-days: 14
1212
commit-message:
13-
prefix: deps
13+
prefix: chore
1414
include: scope
1515
open-pull-requests-limit: 5
1616
labels:
Lines changed: 214 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,214 @@
1+
---
2+
name: add-new-plugin
3+
description: 'Add a new plugin to ArduPilot Methodic Configurator (AMC). Use when implementing a new calibration, monitoring, or configuration plugin — e.g., "add a radio calibration plugin". Covers all five mandatory touch-points: plugin_constants.py, __main__.py, data_model_parameter_editor.py, frontend_tkinter_*.py, and configuration_steps_schema.json, plus the optional configuration_steps_*.json wiring and an optional renderer module.'
4+
argument-hint: 'plugin name (e.g. radio_calibration)'
5+
---
6+
7+
# Add a New Plugin to AMC
8+
9+
## When to Use
10+
11+
- Implementing a new GUI panel that appears alongside parameter editing (calibration,
12+
monitoring, testing, …).
13+
- Extending an existing configuration step with a plugin widget.
14+
15+
## Background
16+
17+
AMC uses a **plugin factory** pattern. Every plugin consists of:
18+
19+
| Layer | File(s) |
20+
| ------- | --------- |
21+
| Constant | `plugin_constants.py` |
22+
| Data model | `data_model_<plugin>.py` |
23+
| Frontend | `frontend_tkinter_<plugin>.py` |
24+
| Registration | `__main__.py → register_plugins()` |
25+
| Data-model wiring | `data_model_parameter_editor.py → create_plugin_data_model()` |
26+
| Schema | `configuration_steps_schema.json` |
27+
| Step wiring | `configuration_steps_<VehicleType>.json` |
28+
| Architecture doc | `ARCHITECTURE_<plugin_name>.md` in the project root |
29+
| Renderer (optional) | `renderer_<name>.py` — for plugins needing a dedicated visualisation helper |
30+
31+
The inline docs in `plugin_constants.py`, `__main__.py:register_plugins()`, and
32+
`data_model_parameter_editor.py:create_plugin_data_model()` should be kept in sync
33+
with this skill (schema path: `plugin > properties > name > enum`).
34+
35+
---
36+
37+
## Step-by-Step Procedure
38+
39+
### 1. Add the constant — `plugin_constants.py`
40+
41+
Add a `PLUGIN_<NAME>` constant at the bottom of the file:
42+
43+
```python
44+
PLUGIN_<NAME> = "<snake_case_name>"
45+
```
46+
47+
Example (RC calibration):
48+
49+
```python
50+
PLUGIN_RC_CALIBRATION = "rc_calibration"
51+
```
52+
53+
### 2. Create the data model — `data_model_<plugin>.py`
54+
55+
Create `ardupilot_methodic_configurator/data_model_<plugin>.py`.
56+
57+
- Accept `flight_controller: FlightController` (and optionally
58+
`local_filesystem: LocalFilesystem`) in `__init__`.
59+
- Expose only business logic; **no tkinter imports**.
60+
- Follow the same structure as `data_model_accelerometer_calibration.py` or
61+
`data_model_battery_monitor.py` for inspiration.
62+
63+
### 3. Create the frontend — `frontend_tkinter_<plugin>.py`
64+
65+
Create `ardupilot_methodic_configurator/frontend_tkinter_<plugin>.py`.
66+
67+
Mandatory elements:
68+
69+
```python
70+
from ardupilot_methodic_configurator.plugin_constants import PLUGIN_<NAME>
71+
from ardupilot_methodic_configurator.plugin_factory import plugin_factory
72+
73+
def _create_<plugin>_view(parent: object, model: object, base_window: object) -> <PluginView>:
74+
# Type checker verifies correct types are provided by the caller
75+
return <PluginView>(parent, model, base_window) # type: ignore[arg-type]
76+
77+
def register_<plugin>_plugin() -> None:
78+
"""Register the <plugin> plugin with the factory."""
79+
plugin_factory.register(PLUGIN_<NAME>, _create_<plugin>_view)
80+
```
81+
82+
Optionally add a standalone `<PluginName>Window(BaseWindow)` class for
83+
development/testing (mark it `# pragma: no cover`).
84+
85+
### 4. Register the plugin — `__main__.py → register_plugins()`
86+
87+
Inside `register_plugins()` add a deferred import and a registration call:
88+
89+
```python
90+
from ardupilot_methodic_configurator.frontend_tkinter_<plugin> import ( # noqa: PLC0415
91+
register_<plugin>_plugin,
92+
)
93+
# ...
94+
register_<plugin>_plugin()
95+
```
96+
97+
Keep imports inside the function body to avoid circular-import issues (the
98+
`frontend_tkinter_*` modules import `plugin_factory` at module level).
99+
100+
### 5. Wire the data model — `data_model_parameter_editor.py → create_plugin_data_model()`
101+
102+
Add an `if` branch **before** the `raise ValueError` at the end:
103+
104+
```python
105+
from ardupilot_methodic_configurator.data_model_<plugin> import <PluginDataModel>
106+
from ardupilot_methodic_configurator.plugin_constants import PLUGIN_<NAME>
107+
108+
# inside create_plugin_data_model():
109+
if plugin_name == PLUGIN_<NAME>:
110+
return <PluginDataModel>(self._flight_controller) if self.is_fc_connected else None
111+
```
112+
113+
Add the import with the other data-model imports near the top of the file, maintaining
114+
**alphabetical order** within the `data_model_*` import block (ruff enforces sorted
115+
imports and will flag violations during `ruff check`).
116+
117+
### 6. Update the schema — `configuration_steps_schema.json`
118+
119+
Find the `plugin > properties > name > enum` array and append the new plugin
120+
name string:
121+
122+
```json
123+
"enum": [
124+
"motor_test",
125+
"battery_monitor",
126+
"compass_calibration",
127+
"accelerometer_calibration",
128+
"<snake_case_name>"
129+
]
130+
```
131+
132+
### 7. (Optional) Wire to a configuration step — `configuration_steps_<VehicleType>.json`
133+
134+
To show the plugin in a specific parameter-editing step, add a `"plugin"` key
135+
to the relevant step object:
136+
137+
```json
138+
"<param_file>.param": {
139+
"why": "...",
140+
"plugin": {
141+
"name": "<snake_case_name>",
142+
"placement": "left"
143+
}
144+
}
145+
```
146+
147+
`placement` is either `"left"` (beside the scrollable frame) or `"top"` (above
148+
the parameter list). Repeat for each vehicle-type JSON that should display the
149+
plugin (`configuration_steps_ArduCopter.json`, `configuration_steps_ArduPlane.json`,
150+
`configuration_steps_Heli.json`, `configuration_steps_Rover.json`).
151+
152+
### 8. Create the architecture document — `ARCHITECTURE_<plugin_name>.md`
153+
154+
Every plugin must have a corresponding architecture document in the project root.
155+
Follow the same structure as `ARCHITECTURE_accelerometer_calibration.md` or
156+
`ARCHITECTURE_rc_calibration.md`:
157+
158+
- Overview paragraph + key features list
159+
- Component layers ASCII diagram
160+
- File map table
161+
- Requirements Analysis (functional + non-functional) with ✅ / 🟡 / ❌ status
162+
- Data flow section (sequence diagrams in code blocks)
163+
- Any plugin-specific design notes (e.g., popup window, renderer module)
164+
- External reference links (MAVLink docs, ArduPilot wiki)
165+
166+
### 9. (Optional) Create a renderer module — `renderer_<name>.py`
167+
168+
If the plugin needs a dedicated visualisation helper (e.g., a 3D attitude renderer),
169+
create `ardupilot_methodic_configurator/renderer_<name>.py`.
170+
171+
- **No tkinter dependency** — return a `PIL.Image.Image` that the frontend displays
172+
via a `ttk.Label`.
173+
- Avoid wildcard imports (`from SomeLib import *`) — ruff enforces explicit imports.
174+
Use `# noqa: ARG002` on the method signature if arguments are unused in a stub.
175+
- See `renderer_3d_quadcopter.py` for the established pattern.
176+
177+
---
178+
179+
## Verification Checklist
180+
181+
After completing all steps, verify:
182+
183+
- [ ] `PLUGIN_<NAME>` constant exists in `plugin_constants.py`
184+
- [ ] `data_model_<plugin>.py` is importable and has no tkinter dependency
185+
- [ ] `frontend_tkinter_<plugin>.py` exports `register_<plugin>_plugin()`
186+
- [ ] `register_plugins()` in `__main__.py` imports and calls the register function
187+
- [ ] `create_plugin_data_model()` handles the new plugin name
188+
- [ ] New `data_model_*` import in `data_model_parameter_editor.py` is in alphabetical order
189+
- [ ] `configuration_steps_schema.json` enum includes the new name
190+
- [ ] Relevant `configuration_steps_*.json` files reference the plugin
191+
- [ ] (If renderer) `renderer_<name>.py` has no wildcard imports; unused stub args use `# noqa: ARG002`
192+
- [ ] `ARCHITECTURE_<plugin_name>.md` exists in the project root
193+
- [ ] `pytest tests/ -v` passes
194+
- [ ] `ruff check .` and `ruff format` pass
195+
- [ ] `mypy` / `pyright` / `pylint` pass
196+
197+
---
198+
199+
## Reference: Touch-Point Summary
200+
201+
The same five mandatory touch-points are documented inline in the source:
202+
203+
- `plugin_constants.py` — top-of-file comment block
204+
- `__main__.py → register_plugins()` — docstring
205+
- `data_model_parameter_editor.py → create_plugin_data_model()` — docstring
206+
207+
## Lessons Learned
208+
209+
### Deferred imports inside functions avoid circular imports
210+
211+
The `frontend_tkinter_*` modules import `plugin_factory` at module level. Importing
212+
them at the top of `__main__.py` would create a circular import. Always place the
213+
`from frontend_tkinter_<plugin> import register_<plugin>_plugin` call **inside** the
214+
`register_plugins()` function body, annotated with `# noqa: PLC0415`.

.github/skills/pytest-testing/SKILL.md

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -203,6 +203,43 @@ def test_integration_behavior(self, mock_api) -> None:
203203
"""Test integration points."""
204204
```
205205

206+
### macOS CI Headless Testing (Tkinter Segfault Guidelines)
207+
208+
Tkinter is extremely buggy and unstable on macOS GitHub Action runners.
209+
Without a physical display, forcing UI updates will cause Segmentation
210+
Faults (`AppKit`/`HIToolbox` crashes) which instantly kill the MacOS Pytest runner.
211+
To prevent this, please follow these rules:
212+
213+
1. **Never use `.update()`**: It forces a full event loop evaluation and will crash macOS.
214+
Always use `.update_idletasks()` instead.
215+
2. Preventing CI Crashes During Setup: Whenever you create a Tkinter window in a test,
216+
you must block the main tkinter library from trying to render to a physical screen.
217+
Make sure your yield statement stays inside the with patch(...) block so that
218+
these overrides don't expire before the test finishes running.
219+
220+
```python
221+
@pytest.fixture
222+
def safe_window_fixture(tk_root) -> Generator[MyWindow, None, None]:
223+
with (
224+
# Prevent Tkinter DPI scaling calculations from crashing macOS
225+
patch.object(tk.Toplevel, "winfo_fpixels", side_effect=tk.TclError("no display")),
226+
# Block macOS C-level screen rendering universally
227+
patch("tkinter.Misc.update"),
228+
patch("tkinter.Misc.update_idletasks"),
229+
patch("tkinter.Misc.wait_visibility"),
230+
patch("tkinter.Misc.wait_window"),
231+
):
232+
window = MyWindow(tk_root)
233+
234+
# Follow Point 2
235+
yield window
236+
237+
- GUI Tests (gui_*.py): Any test simulating physical interaction (e.g., PyAutoGUI) must
238+
explicitly load the CI environment setup at the very top of the file:
239+
240+
from tests.conftest import gui_test_environment # noqa: F401 # pylint: disable=unused-import
241+
pytestmark = pytest.mark.gui
242+
206243
## 📋 Test Categories
207244

208245
### Required Test Types

.github/workflows/ai-translation.yml

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -42,14 +42,14 @@ jobs:
4242
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
4343

4444
- name: Set up Python
45-
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
45+
uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0
4646
with:
4747
python-version: '3.x'
4848
cache: 'pip'
4949
cache-dependency-path: 'pyproject.toml'
5050

5151
- name: Cache apt packages
52-
uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
52+
uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
5353
with:
5454
path: |
5555
/var/cache/apt/archives/*.deb
@@ -401,14 +401,14 @@ jobs:
401401
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
402402

403403
- name: Set up Python
404-
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
404+
uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0
405405
with:
406406
python-version: '3.x'
407407
cache: 'pip'
408408
cache-dependency-path: 'pyproject.toml'
409409

410410
- name: Cache apt packages
411-
uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
411+
uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
412412
with:
413413
path: |
414414
/var/cache/apt/archives/*.deb

.github/workflows/build_windows_macos.yml

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ jobs:
5656
5757
- name: Restore Inno Setup installer from cache
5858
id: cache-innosetup
59-
uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
59+
uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
6060
with:
6161
path: installer.exe
6262
key: innosetup-6.7.1-exe
@@ -122,7 +122,7 @@ jobs:
122122
123123
- name: Restore ChineseSimplified.isl from cache
124124
id: cache-chinese-isl
125-
uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
125+
uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
126126
with:
127127
path: ChineseSimplified.isl
128128
key: chinese-simplified-isl-6.5
@@ -179,7 +179,7 @@ jobs:
179179

180180
- name: Set up Python
181181
id: setup-python
182-
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
182+
uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0
183183
with:
184184
python-version: ${{ matrix.python-version }}
185185

@@ -281,7 +281,7 @@ jobs:
281281
282282
- name: Set up Python
283283
id: setup-python
284-
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
284+
uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0
285285
with:
286286
python-version: ${{ matrix.python-version }}
287287

.github/workflows/bump_version_and_tag.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ jobs:
3434
ssh-key: "${{ secrets.VERSION_BUMP_KEY }}" # This is a deploy key with write access, without it other workflows will not get triggered to run
3535

3636
- name: Set up Python
37-
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
37+
uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0
3838
with:
3939
python-version: 3.x
4040

.github/workflows/docker-ci.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ jobs:
4141
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
4242

4343
- name: Set up Docker Buildx
44-
uses: docker/setup-buildx-action@d7f5e7f509e45cec5c76c4d5afdd7de93d0b3df5 # v4.1.0
44+
uses: docker/setup-buildx-action@bb05f3f5519dd87d3ba754cc423b652a5edd6d2c # v4.2.0
4545

4646
- name: Build and Run Tests
4747
run: |

.github/workflows/generate_apm.pdef.xml.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ jobs:
3333
fetch-tags: true
3434

3535
- name: Set up Python
36-
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
36+
uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0
3737
with:
3838
python-version: '3.x'
3939

.github/workflows/pytest.yml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,7 @@ jobs:
6666

6767
- name: Cache apt packages (Linux)
6868
if: matrix.os == 'ubuntu-latest'
69-
uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
69+
uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
7070
with:
7171
path: /var/cache/apt/archives
7272
key: apt-${{ runner.os }}-py${{ matrix.python-version }}-${{ hashFiles('.github/workflows/pytest.yml') }}
@@ -85,7 +85,7 @@ jobs:
8585
8686
- name: Cache Homebrew packages (macOS)
8787
if: matrix.os == 'macos-latest'
88-
uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
88+
uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
8989
with:
9090
path: ~/Library/Caches/Homebrew
9191
key: brew-${{ runner.os }}-python-tk-${{ hashFiles('.github/workflows/pytest.yml') }}
@@ -134,7 +134,7 @@ jobs:
134134
135135
- name: Cache SITL files
136136
if: matrix.os == 'ubuntu-latest'
137-
uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
137+
uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
138138
with:
139139
path: sitl/
140140
key: ${{ env.SITL_CACHE_KEY }}

0 commit comments

Comments
 (0)