Files
planeMapper/_bmad-output/implementation-artifacts/4-1-gpio-button-hold-detection-and-led-feedback.md
Matt Edholm 8e57f2d927 Mark story 4-1 done, add deferred items from review
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>
2026-04-22 23:56:20 -04:00

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 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