Review passed all criteria; update sprint-status to done, close the story file, and record two deferred items (GPIO pins into constants.py, ButtonHoldDetector init-time GPIO dependency) in deferred-work.md. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
3.5 KiB
Story 4.1: GPIO Button Hold Detection & LED Feedback
Status: done
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
ButtonHoldDetectorandLEDControllerinsrc/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 = 27as 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) -> NoneandLEDController.off(self) -> None
- 1.1 Add imports:
-
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()returnsFalsewhen button not pressed (MockFactory default) - 2.4 Test
LEDController.on()andLEDController.off()work without exception
- 2.1 Add
-
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
- 3.1
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 |