Source code for openbricks.bluetooth

# SPDX-License-Identifier: MIT
"""
Persistent Bluetooth on/off state for the hub.

The firmware ships with BLE compiled in (so ``mpremote``-over-BLE
code transfer works). At runtime we let the user toggle the advertising
stack on and off; the choice is persisted in NVS so reboots don't lose
the state. Default (no key in NVS yet) is **on**.

Typical usage from ``main.py`` right after the firmware boots:

    from openbricks import bluetooth
    bluetooth.apply_persisted_state()

Then toggle programmatically (or from the hub's button — see
``openbricks.hub.ESP32DevkitHub`` once that integration lands):

    bluetooth.toggle()                # flip current state, persist, apply
    bluetooth.set_enabled(False)      # explicit off

Activating BLE requires a hub name (``openbricks.HUB_NAME``) — flash
the image with ``scripts/flash_firmware.py --name NAME`` first. We
refuse to advertise with no name rather than defaulting to a shared
value, since two hubs with the same advertising name can't be
individually addressed.

``esp32.NVS`` and ``bluetooth.BLE`` are imported lazily so desktop
tests running under unix MicroPython don't need to have the firmware-
only modules present — they install fakes in ``tests/_fakes_ble.py``.
"""

_NAMESPACE = "openbricks"
_KEY       = "ble_enabled"


[docs] class HubNameNotSetError(RuntimeError): """Raised when the firmware was flashed without ``--name NAME``."""
def _require_hub_name(): import openbricks name = openbricks.HUB_NAME if name is None: raise HubNameNotSetError( "hub name not set — reflash with " "scripts/flash_firmware.py --name NAME --port /dev/ttyUSB0" ) return name
[docs] def is_enabled(): """Return the persisted flag (``True`` if no value has ever been written).""" try: import esp32 nvs = esp32.NVS(_NAMESPACE) return bool(nvs.get_i32(_KEY)) except OSError: # Key not set yet — default to enabled. return True
[docs] def set_enabled(enabled): """Persist ``enabled`` and apply it to the BLE stack immediately. Raises :class:`HubNameNotSetError` when turning BLE on with no hub name flashed. Turning BLE off is always allowed — a nameless hub can still be silenced. When enabling, also starts the NUS REPL bridge (``openbricks.ble_repl``) so ``openbricks run`` / ``stop`` can push scripts over BLE. When disabling, the bridge is torn down first so dupterm isn't left pointing at an inactive stack. """ import esp32 import bluetooth from openbricks import ble_repl if enabled: name = _require_hub_name() nvs = esp32.NVS(_NAMESPACE) nvs.set_i32(_KEY, 1 if enabled else 0) nvs.commit() ble = bluetooth.BLE() if not enabled: # Tear down the REPL bridge before we kill the radio so dupterm # doesn't end up writing into a dead stack. ble_repl.stop() if enabled: # gap_name must be set before active(True); config() raises if # the stack is already active. ble.config(gap_name=name) ble.active(bool(enabled)) if enabled: # Start the REPL bridge *after* active(True) — gatts_register_services # errors otherwise. ble_repl.start()
[docs] def toggle(): """Flip the current state. Returns the new (post-toggle) value.""" new_state = not is_enabled() set_enabled(new_state) return new_state
[docs] def apply_persisted_state(): """Read the persisted flag and apply it to the BLE stack. Call this at boot (e.g. top of ``main.py``) so the BLE radio reflects whatever the user last chose before the reboot — or comes up enabled on the very first boot. Tolerant of a missing hub name: if the persisted state says BLE-on but no hub name is flashed (typical on a freshly-flashed chip before the first ``openbricks flash --name ...`` run), prints a one-line warning and skips activation. Better than raising ``HubNameNotSetError`` from ``main.py``, which (per the 1.0.4 silent-boot bug) could lose its traceback to USB-Serial-JTAG timing and brick the boot. """ import bluetooth from openbricks import ble_repl enabled = is_enabled() if enabled: try: name = _require_hub_name() except HubNameNotSetError: print("openbricks: BLE persisted-on but no hub name in NVS; " "skipping BLE activation. Reflash with " "``openbricks flash --name NAME ...`` to enable.") return ble = bluetooth.BLE() if not enabled: ble_repl.stop() if enabled: ble.config(gap_name=name) ble.active(enabled) if enabled: ble_repl.start()