feat(2-7): operational radar loop, startup screen, and systemd wiring
Implements story 2-7: full main.py with _make_startup_screen(), _run_one_cycle() with per-phase timing and slow-render warning, and main() connecting config → bounds → fetcher → renderer → display. Adds provision.py early-exit when already provisioned. Adds User=root to both systemd service files. Adds tests/test_main.py (3 new tests, 99 total). Updates scaffold test to allow provisioning.config import from main.py. All quality gates pass. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
+22
-22
@@ -1,6 +1,6 @@
|
||||
# Story 2.7: Operational Radar Loop, Startup Screen & Systemd Wiring
|
||||
|
||||
Status: ready-for-dev
|
||||
Status: review
|
||||
|
||||
## Story
|
||||
|
||||
@@ -22,10 +22,10 @@ AC5: **Given** device reboots with `provisioned: true` in config **When** `plane
|
||||
|
||||
## Tasks / Subtasks
|
||||
|
||||
- [ ] Task 1: Update `src/planemapper/main.py` with full implementation (AC: #1, #2, #3)
|
||||
- [ ] 1.1 Add `"src/planemapper/main.py"` to TID251 per-file-ignores in `pyproject.toml` (alongside the existing `provision.py` entry) so that `main.py` may import from `planemapper.provisioning.config`
|
||||
- [ ] 1.2 Implement `_make_startup_screen() -> Image.Image` — returns a white 800×480 RGB `PIL.Image` with `"planeMapper starting..."` drawn using `ImageFont.load_default()`
|
||||
- [ ] 1.3 Implement `_run_one_cycle(renderer, fetcher, display) -> None` with per-phase timing and slow-render warning:
|
||||
- [x] Task 1: Update `src/planemapper/main.py` with full implementation (AC: #1, #2, #3)
|
||||
- [x] 1.1 Add `"src/planemapper/main.py"` to TID251 per-file-ignores in `pyproject.toml` (alongside the existing `provision.py` entry) so that `main.py` may import from `planemapper.provisioning.config`
|
||||
- [x] 1.2 Implement `_make_startup_screen() -> Image.Image` — returns a white 800×480 RGB `PIL.Image` with `"planeMapper starting..."` drawn using `ImageFont.load_default()`
|
||||
- [x] 1.3 Implement `_run_one_cycle(renderer, fetcher, display) -> None` with per-phase timing and slow-render warning:
|
||||
```python
|
||||
def _run_one_cycle(renderer, fetcher, display):
|
||||
t0 = time.monotonic()
|
||||
@@ -41,7 +41,7 @@ AC5: **Given** device reboots with `provisioned: true` in config **When** `plane
|
||||
if total > RENDER_WARN_S:
|
||||
log.warning("render slow: %.1fs > %ds threshold", total, RENDER_WARN_S)
|
||||
```
|
||||
- [ ] 1.4 Implement `main()`: read config → build `MapBounds` + load base map → init `HttpFetcher`, `Renderer`, `WaveshareDisplay` → show startup screen → enter `while True` loop calling `_run_one_cycle` then `time.sleep(REFRESH_INTERVAL_S)`:
|
||||
- [x] 1.4 Implement `main()`: read config → build `MapBounds` + load base map → init `HttpFetcher`, `Renderer`, `WaveshareDisplay` → show startup screen → enter `while True` loop calling `_run_one_cycle` then `time.sleep(REFRESH_INTERVAL_S)`:
|
||||
```python
|
||||
def main() -> None:
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
@@ -68,12 +68,12 @@ AC5: **Given** device reboots with `provisioned: true` in config **When** `plane
|
||||
_run_one_cycle(renderer, fetcher, display)
|
||||
time.sleep(REFRESH_INTERVAL_S)
|
||||
```
|
||||
- [ ] 1.5 Add module-level constants: `REFRESH_INTERVAL_S = 60` and `RENDER_WARN_S = 40`
|
||||
- [ ] 1.6 Add required imports: `import logging`, `import time`, `from PIL import Image, ImageDraw, ImageFont`, `from planemapper.fetcher import HttpFetcher`, `from planemapper.renderer.projection import MapBounds`, `from planemapper.renderer.renderer import Renderer`, `from planemapper.renderer import basemap`, `from planemapper.display import WaveshareDisplay`
|
||||
- [x] 1.5 Add module-level constants: `REFRESH_INTERVAL_S = 60` and `RENDER_WARN_S = 40`
|
||||
- [x] 1.6 Add required imports: `import logging`, `import time`, `from PIL import Image, ImageDraw, ImageFont`, `from planemapper.fetcher import HttpFetcher`, `from planemapper.renderer.projection import MapBounds`, `from planemapper.renderer.renderer import Renderer`, `from planemapper.renderer import basemap`, `from planemapper.display import WaveshareDisplay`
|
||||
|
||||
- [ ] Task 2: Update `src/planemapper/provision.py` to exit early if already provisioned (AC: #5)
|
||||
- [ ] 2.1 Add import for `planemapper.provisioning.config` at the top of `provision.py` (already in TID251 per-file-ignores)
|
||||
- [ ] 2.2 At the top of `main()` in `provision.py`, before the provisioning loop, add:
|
||||
- [x] Task 2: Update `src/planemapper/provision.py` to exit early if already provisioned (AC: #5)
|
||||
- [x] 2.1 Add import for `planemapper.provisioning.config` at the top of `provision.py` (already in TID251 per-file-ignores)
|
||||
- [x] 2.2 At the top of `main()` in `provision.py`, before the provisioning loop, add:
|
||||
```python
|
||||
try:
|
||||
cfg = config.read()
|
||||
@@ -84,19 +84,19 @@ AC5: **Given** device reboots with `provisioned: true` in config **When** `plane
|
||||
pass # no config — proceed to portal
|
||||
```
|
||||
|
||||
- [ ] Task 3: Verify/update systemd service files (AC: #4)
|
||||
- [ ] 3.1 Verify `systemd/planemapper-radar.service` has `Restart=always` and `After=planemapper-provision.service` — these already exist in the file; confirm `User=root` is present (currently absent — add it)
|
||||
- [ ] 3.2 Verify `systemd/planemapper-provision.service` has `Type=oneshot` and `RemainAfterExit=yes` — these already exist in the file; confirm `User=root` is present (currently absent — add it)
|
||||
- [x] Task 3: Verify/update systemd service files (AC: #4)
|
||||
- [x] 3.1 Verify `systemd/planemapper-radar.service` has `Restart=always` and `After=planemapper-provision.service` — these already exist in the file; confirm `User=root` is present (currently absent — add it)
|
||||
- [x] 3.2 Verify `systemd/planemapper-provision.service` has `Type=oneshot` and `RemainAfterExit=yes` — these already exist in the file; confirm `User=root` is present (currently absent — add it)
|
||||
|
||||
- [ ] Task 4: Write tests in `tests/test_main.py` (AC: #2, #3, #5)
|
||||
- [ ] 4.1 Test AC2: call `_run_one_cycle` with `NullDisplay`, a `FileFixtureFetcher` (or `unittest.mock.MagicMock` returning a list), and a `Renderer` backed by a fake 800×480 white base map; assert `display.show()` was called exactly once
|
||||
- [ ] 4.2 Test AC3: mock `time.monotonic` to return values simulating a total elapsed time > 40s (e.g. return sequence `[0, 1, 2, 43]`); assert `log.warning` is called (use `pytest` `caplog` fixture at WARNING level); assert the log message contains `"render slow"`
|
||||
- [ ] 4.3 Test AC5: patch `planemapper.provisioning.config.read` to return `{"provisioned": True}` and patch `planemapper.provisioning.wifi.start_ap`; call `planemapper.provision.main()`; assert `wifi.start_ap` was NOT called
|
||||
- [x] Task 4: Write tests in `tests/test_main.py` (AC: #2, #3, #5)
|
||||
- [x] 4.1 Test AC2: call `_run_one_cycle` with `NullDisplay`, a `FileFixtureFetcher` (or `unittest.mock.MagicMock` returning a list), and a `Renderer` backed by a fake 800×480 white base map; assert `display.show()` was called exactly once
|
||||
- [x] 4.2 Test AC3: mock `time.monotonic` to return values simulating a total elapsed time > 40s (e.g. return sequence `[0, 1, 2, 43]`); assert `log.warning` is called (use `pytest` `caplog` fixture at WARNING level); assert the log message contains `"render slow"`
|
||||
- [x] 4.3 Test AC5: patch `planemapper.provisioning.config.read` to return `{"provisioned": True}` and patch `planemapper.provisioning.wifi.start_ap`; call `planemapper.provision.main()`; assert `wifi.start_ap` was NOT called
|
||||
|
||||
- [ ] Task 5: Run quality gates
|
||||
- [ ] 5.1 `python -m pytest tests/` — all tests pass
|
||||
- [ ] 5.2 `python -m ruff check .` — zero violations
|
||||
- [ ] 5.3 `python -m ruff format --check .` — no formatting issues
|
||||
- [x] Task 5: Run quality gates
|
||||
- [x] 5.1 `python -m pytest tests/` — all tests pass
|
||||
- [x] 5.2 `python -m ruff check .` — zero violations
|
||||
- [x] 5.3 `python -m ruff format --check .` — no formatting issues
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
|
||||
@@ -35,7 +35,7 @@
|
||||
# - Dev moves story to 'review', then runs code-review (fresh context, different LLM recommended)
|
||||
|
||||
generated: 2026-04-22
|
||||
last_updated: 2026-04-22 # 2-1 done, 2-2 done, 2-3 done, 2-4 done, 2-5 done, 2-6 done, 2-7 ready-for-dev, epic-2 in-progress
|
||||
last_updated: 2026-04-22 # 2-1 done, 2-2 done, 2-3 done, 2-4 done, 2-5 done, 2-6 done, 2-7 review, epic-2 in-progress
|
||||
project: planeMapper
|
||||
project_key: NOKEY
|
||||
tracking_system: file-system
|
||||
@@ -59,7 +59,7 @@ development_status:
|
||||
2-4-altitude-colour-bands-and-aircraft-type-icons: done
|
||||
2-5-per-aircraft-drawing: done
|
||||
2-6-stateful-renderer-and-display-interface: done
|
||||
2-7-operational-radar-loop-startup-screen-and-systemd-wiring: ready-for-dev
|
||||
2-7-operational-radar-loop-startup-screen-and-systemd-wiring: review
|
||||
epic-2-retrospective: optional
|
||||
|
||||
# Epic 3: Stale Data Resilience
|
||||
|
||||
Reference in New Issue
Block a user