Source code for openbricks.bluetooth_button

# SPDX-License-Identifier: MIT
"""
Short press on the BLE-toggle button toggles BLE on/off.

Wires a ``machine.Timer``-driven poll loop (default 50 ms) against a
``Button``-conformant object and calls ``openbricks.bluetooth.toggle()``
once per press-release cycle. State is persisted via NVS by the
``bluetooth`` module, so the new value survives reboots.

This is a different physical button from the one ``openbricks.launcher``
watches for program start/stop — the BLE toggle lives on its own
GPIO (default 5, see :class:`openbricks.hub.Hub`) while the program
button is on GPIO 4. Two pins → no duration-based dispatch, every
press on this pin means "flip BLE".

Usage from ``main.py``:

    from openbricks import bluetooth
    from openbricks.bluetooth_button import BluetoothToggleButton
    from openbricks.hub import ESP32DevkitHub

    bluetooth.apply_persisted_state()
    hub = ESP32DevkitHub()
    BluetoothToggleButton(hub.bluetooth_button).start()

The helper is deliberately standalone (not baked into ``Hub``) so tests
can exercise it in isolation, and so boards without a button — or
users who want to drive the toggle from something other than a
physical press — can skip it.
"""

from machine import Timer


DEFAULT_POLL_MS = 50

# BLE on → blue, off → yellow. Picked for high contrast on the WS2812;
# override via ``BluetoothToggleButton(..., color_on=..., color_off=...)``
# if you want different hues.
DEFAULT_COLOR_ON  = (0, 0, 255)       # blue
DEFAULT_COLOR_OFF = (255, 200, 0)     # yellow


[docs] class BluetoothToggleButton: def __init__(self, button, led=None, poll_ms=DEFAULT_POLL_MS, timer_id=1, color_on=DEFAULT_COLOR_ON, color_off=DEFAULT_COLOR_OFF): """ Args: button: any object with a ``.pressed() -> bool`` method (the ``Button`` / ``PushButton`` from ``openbricks.hub`` both qualify). led: optional RGB-capable ``StatusLED`` (i.e. one whose ``.rgb(r, g, b)`` is implemented — the ``NeoPixelLED`` on the S3 DevKitC-1 qualifies). When provided, ``start()`` immediately colours the LED based on the current persisted BLE state (blue = on, yellow = off) and each toggle recolours it. Pass ``None`` to skip LED feedback. poll_ms: polling period. Default 50 ms (20 Hz) — well under human reaction time, negligible CPU. timer_id: ``machine.Timer`` hardware ID (0..3 on ESP32-S3). Default 1 stays out of the way of the ``launcher`` (which takes timer 0). The previous default ``-1`` (virtual timer) was supported by older MicroPython but raises ``ValueError: invalid Timer number`` on the v1.27+ MP we vendor. color_on, color_off: ``(r, g, b)`` tuples the LED is set to when BLE is enabled / disabled. Defaults: blue / yellow. """ self._button = button self._led = led self._poll_ms = int(poll_ms) self._timer_id = timer_id self._color_on = tuple(color_on) self._color_off = tuple(color_off) self._timer = None # Edge-detection state: True from the moment we first saw the # button pressed until the subsequent release (when we fire). self._was_pressed = False # ---- lifecycle ----
[docs] def start(self): """Begin polling. Safe to call repeatedly — the second call is a no-op. On first call, paints the LED (if one was provided) to reflect the current persisted BLE state, so the boot indicator matches reality. """ if self._timer is not None: return self._apply_led_for_current_state() self._timer = Timer(self._timer_id) self._timer.init( period=self._poll_ms, mode=Timer.PERIODIC, callback=self._on_tick, )
[docs] def stop(self): """Stop polling and release the timer.""" if self._timer is None: return self._timer.deinit() self._timer = None self._was_pressed = False
# ---- tick body ---- def _on_tick(self, _timer): if self._button.pressed(): self._was_pressed = True return if self._was_pressed: # Release after a press — fire once. self._was_pressed = False self._fire() def _fire(self): # Imported inside the method so tests that don't install the BLE # fake don't explode at module-load time. In production both # imports succeed because the firmware freezes the module in. from openbricks import bluetooth bluetooth.toggle() self._apply_led_for_current_state() def _apply_led_for_current_state(self): """Paint the LED to match the current persisted BLE state, if an RGB-capable LED was provided. Silently no-ops on plain on/off LEDs (whose ``.rgb()`` raises ``NotImplementedError``) so the hub can pass ``self.led`` unconditionally without caring which variant it is.""" if self._led is None: return from openbricks import bluetooth color = self._color_on if bluetooth.is_enabled() else self._color_off try: self._led.rgb(*color) except NotImplementedError: pass