Implement GPIO button hold detection and LED feedback (story 4-1)
Replace stub ButtonHoldDetector and LEDController with real gpiozero implementations; update tests to use MockFactory so they run without physical GPIO hardware. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
+65
@@ -0,0 +1,65 @@
|
||||
# Story 4.1: GPIO Button Hold Detection & LED Feedback
|
||||
|
||||
Status: review
|
||||
|
||||
## Story
|
||||
|
||||
As a user wanting to reconfigure the device,
|
||||
I want to hold the reset button for 3 seconds and receive immediate LED confirmation,
|
||||
So that I know the reset was registered before anything else changes.
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
AC1: **Given** the button is held for `RESET_HOLD_S` (3s) **When** `ButtonHoldDetector.check()` is called **Then** it returns `True`
|
||||
|
||||
AC2: **Given** the button is held for less than 3s **When** `ButtonHoldDetector.check()` is called **Then** it returns `False`
|
||||
|
||||
AC3: **Given** `ButtonHoldDetector.check()` returns `True` **When** the main loop processes the result **Then** `LEDController.on()` is called immediately
|
||||
|
||||
AC4: **Given** gpiozero `MockFactory` is active **When** button/LED tests run **Then** they pass without physical GPIO hardware
|
||||
|
||||
AC5: **Given** `check()` is called once per render cycle **When** the render loop runs **Then** the call is non-blocking
|
||||
|
||||
## Tasks / Subtasks
|
||||
|
||||
- [ ] Task 1: Implement `ButtonHoldDetector` and `LEDController` in `src/planemapper/gpio_ctrl.py` (AC: #1, #2, #3, #5)
|
||||
- [ ] 1.1 Add imports: `import logging`, `from gpiozero import Button, LED`, `from planemapper.constants import RESET_HOLD_S`
|
||||
- [ ] 1.2 Define `BUTTON_GPIO_PIN = 17`, `LED_GPIO_PIN = 27` as module-level constants
|
||||
- [ ] 1.3 Implement `ButtonHoldDetector.__init__(self, pin=BUTTON_GPIO_PIN)`: `self._button = Button(pin, hold_time=RESET_HOLD_S)`
|
||||
- [ ] 1.4 Implement `ButtonHoldDetector.check(self) -> bool`: `return self._button.is_held`
|
||||
- [ ] 1.5 Implement `LEDController.__init__(self, pin=LED_GPIO_PIN)`: `self._led = LED(pin)`
|
||||
- [ ] 1.6 Implement `LEDController.on(self) -> None` and `LEDController.off(self) -> None`
|
||||
|
||||
- [ ] Task 2: Update `tests/test_gpio_ctrl.py` (AC: #4)
|
||||
- [ ] 2.1 Add `from gpiozero import Device`, `from gpiozero.pins.mock import MockFactory`
|
||||
- [ ] 2.2 Set `Device.pin_factory = MockFactory()` before instantiating classes in each test or in a fixture
|
||||
- [ ] 2.3 Test `ButtonHoldDetector.check()` returns `False` when button not pressed (MockFactory default)
|
||||
- [ ] 2.4 Test `LEDController.on()` and `LEDController.off()` work without exception
|
||||
|
||||
- [ ] 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
|
||||
|
||||
### gpiozero integration
|
||||
|
||||
`ButtonHoldDetector` wraps `gpiozero.Button` with `hold_time=RESET_HOLD_S`. The `is_held` property returns `True` only after the button has been continuously held for `hold_time` seconds — this is non-blocking (no sleep, no polling loop), satisfying AC5.
|
||||
|
||||
`LEDController` wraps `gpiozero.LED`. Calls to `on()` and `off()` delegate directly to the underlying device.
|
||||
|
||||
### MockFactory for tests
|
||||
|
||||
`gpiozero.Device.pin_factory = MockFactory()` substitutes all GPIO pin access with in-process mock objects. Tests construct `ButtonHoldDetector` and `LEDController` without needing real hardware. The mock button's default state is not-held, so `check()` returns `False` unless the mock pin is explicitly driven.
|
||||
|
||||
### Module-level pin constants
|
||||
|
||||
`BUTTON_GPIO_PIN = 17` and `LED_GPIO_PIN = 27` are defined at module level in `gpio_ctrl.py`. This makes them visible and easy to change for a hardware revision without touching any logic.
|
||||
|
||||
### Files changed
|
||||
|
||||
| File | Change |
|
||||
|---|---|
|
||||
| `src/planemapper/gpio_ctrl.py` | Full implementation replacing stubs |
|
||||
| `tests/test_gpio_ctrl.py` | Update to use MockFactory |
|
||||
@@ -69,7 +69,7 @@ development_status:
|
||||
epic-3-retrospective: optional
|
||||
|
||||
# Epic 4: Reset & Reconfiguration
|
||||
epic-4: backlog
|
||||
4-1-gpio-button-hold-detection-and-led-feedback: backlog
|
||||
epic-4: in-progress
|
||||
4-1-gpio-button-hold-detection-and-led-feedback: review
|
||||
4-2-config-wipe-setup-screen-and-return-to-provisioning: backlog
|
||||
epic-4-retrospective: optional
|
||||
|
||||
Reference in New Issue
Block a user