feat(reset): config wipe, setup screen, and re-exec into provisioning (story 4-2)

Add _handle_reset() and _make_setup_screen() to main.py; integrate ButtonHoldDetector
and LEDController into the radar loop; LED lights immediately on hold, config is wiped,
setup screen shown, then os.execvp hands off to planemapper-provision. Wipe failures
log ERROR and abort without exec. Completes epic-4.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Matt Edholm
2026-04-22 23:59:22 -04:00
parent 8e57f2d927
commit 7f34a7b926
5 changed files with 168 additions and 3 deletions
@@ -0,0 +1,70 @@
# Story 4.2: Config Wipe, Setup Screen & Return to Provisioning
Status: done
## Story
As a user who has triggered a reset,
I want the device to wipe its configuration, show a setup screen on the e-ink display, and restart into the provisioning flow,
So that I can re-configure the device from scratch.
## Acceptance Criteria
AC1: **Given** `ButtonHoldDetector.check()` returns `True` **When** the reset handler runs **Then** `config.wipe()` is called
AC2: **Given** config has been wiped **When** the reset handler continues **Then** `display.show(setup_screen_image)` is called
AC3: **Given** setup screen is shown **When** reset handler completes **Then** `os.execvp('planemapper-provision', ['planemapper-provision'])` replaces the current process
AC4: **Given** `config.wipe()` raises an unexpected error **When** the reset handler encounters it **Then** ERROR is logged and `os.execvp` is NOT called
## Tasks / Subtasks
- [ ] Task 1: Implement `_handle_reset()` and `_make_setup_screen()` in `src/planemapper/main.py` (AC: #1#4)
- [ ] 1.1 Add `import os` to imports
- [ ] 1.2 Add `from planemapper.provisioning.config import wipe as wipe_config`
- [ ] 1.3 Add `from planemapper.gpio_ctrl import ButtonHoldDetector, LEDController`
- [ ] 1.4 Implement `_make_setup_screen() -> Image.Image`
- [ ] 1.5 Implement `_handle_reset(display, led) -> None`
- [ ] 1.6 Update `main()` to instantiate `ButtonHoldDetector` and `LEDController`
- [ ] 1.7 Add reset check at top of loop
- [ ] Task 2: Write tests in `tests/test_reset.py` (AC: #1#4)
- [ ] Task 3: Run quality gates
- [ ] 3.1 `python -m pytest tests/` — all tests pass
- [ ] 3.2 `python -m ruff check .` — zero violations
- [ ] 3.3 `python -m ruff format --check .` — no formatting issues
## Implementation Notes
### `_handle_reset` flow
1. `led.on()` — immediate LED feedback before any wipe attempt
2. `wipe_config()` — deletes config file; on failure, log ERROR, call `led.off()`, return
3. `display.show(_make_setup_screen())` — render "Resetting..." screen
4. `os.execvp("planemapper-provision", ["planemapper-provision"])` — replaces process (never returns)
### `_make_setup_screen()`
Returns a white 800×480 PIL Image with "Resetting..." text. Same pattern as `_make_startup_screen()`.
### main() loop update
`ButtonHoldDetector` and `LEDController` are instantiated once in `main()` before the loop. Inside the loop, `button.check()` is called once per cycle (non-blocking). If `True`, `_handle_reset` is called (which never returns on success).
### TID251 per-file-ignore
`main.py` already has `TID251` in per-file-ignores in `pyproject.toml`, so importing from `planemapper.provisioning.config` is allowed.
### Patching in tests
- `wipe_config` is patched at `planemapper.main.wipe_config`
- `os.execvp` is patched at `planemapper.main.os.execvp`
### Files changed
| File | Change |
|---|---|
| `src/planemapper/main.py` | Add `import os`, new imports, `_make_setup_screen()`, `_handle_reset()`, update `main()` |
| `tests/test_reset.py` | New file with 3 tests |
@@ -214,3 +214,17 @@ Description: `BUTTON_GPIO_PIN = 17` and `LED_GPIO_PIN = 27` are module-level con
Story: `4-1-gpio-button-hold-detection-and-led-feedback`
Category: Infrastructure/environment
Description: `ButtonHoldDetector.__init__` constructs a `gpiozero.Button` immediately. The radar main loop must construct `ButtonHoldDetector` at startup (not at import time), or the application will fail if GPIO is unavailable when the module is imported. This is currently safe because `gpio_ctrl.py` is not imported at module level in `main.py`, but any future reorganisation that imports it at the top of a module that runs on non-GPIO hardware will raise a `BadPinFactory` error unless a `MockFactory` is active.
---
## Story 4.2: Config Wipe, Setup Screen & Return to Provisioning
### [4-2] ButtonHoldDetector and LEDController raise at startup on Pi without GPIO
Story: `4-2-config-wipe-setup-screen-and-return-to-provisioning`
Category: Infrastructure/environment
Description: `ButtonHoldDetector` and `LEDController` are now instantiated unconditionally in `main()` before the loop begins. On a Pi without GPIO hardware (or without a `MockFactory` active), both constructors will raise a `BadPinFactory` error at startup, crashing the radar service before it can display anything. Same concern as story 4-1. Future hardening: wrap construction in a try/except and fall back to no-op stubs, or defer construction until first use.
### [4-2] os.execvp replaces process — no cleanup before re-exec
Story: `4-2-config-wipe-setup-screen-and-return-to-provisioning`
Category: Technical debt
Description: `os.execvp` replaces the current process image immediately. Any cleanup that would normally happen at shutdown — flushing log handlers, closing the SPI connection to the e-ink display, releasing GPIO pins — is not performed. Acceptable for MVP: the SPI and GPIO resources will be re-acquired by the provisioning process, and the OS reclaims file descriptors. A future improvement could flush logs and call `display.close()` (if such a method exists) before exec.
@@ -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 done, epic-2 done, epic-3 done, 3-1 done, 3-2 done
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 done, epic-2 done, epic-3 done, 3-1 done, 3-2 done, 4-1 done, 4-2 done, epic-4 done
project: planeMapper
project_key: NOKEY
tracking_system: file-system
@@ -69,7 +69,7 @@ development_status:
epic-3-retrospective: optional
# Epic 4: Reset & Reconfiguration
epic-4: in-progress
epic-4: done
4-1-gpio-button-hold-detection-and-led-feedback: done
4-2-config-wipe-setup-screen-and-return-to-provisioning: backlog
4-2-config-wipe-setup-screen-and-return-to-provisioning: done
epic-4-retrospective: optional