Skip to content

BLE beacon ears#3240

Merged
BlitzCityDIY merged 3 commits into
adafruit:mainfrom
videopixil:BLE_Beacon_Ears
May 11, 2026
Merged

BLE beacon ears#3240
BlitzCityDIY merged 3 commits into
adafruit:mainfrom
videopixil:BLE_Beacon_Ears

Conversation

@videopixil
Copy link
Copy Markdown
Contributor

QTPy code to make into a BLE receiver

QTPy code to make into a BLE receiver
@videopixil
Copy link
Copy Markdown
Contributor Author

The receiver code.py has lint disables on main() for
too-many-locals, too-many-branches, and too-many-statements.
These are intentional and supported by direct on-hardware bisection.
Symptom: Refactoring main() into additional helper functions
causes import _bleio to fail at boot with
espidf.IDFError: Generic Failure, even after USB power-cycle.
Standalone _bleio import in the REPL works fine, the platform is
healthy, and gc.mem_free() reports 120KB+ free with 20KB
contiguous allocation succeeding - so it is not memory pressure
in the way that's typically reported.

Implication for contributors: if you extend code.py and
adding code pushes the file over ~32.5KB, the firmware will fail
to boot. The remote-trigger if/elif cascade in the BLE scan loop
is intentionally inline for this reason (extracting it to helpers
puts us over the cliff). When in doubt, prefer adding code to the
helper modules (renderer.py, magicband_protocol.py) which
don't have this constraint.

Removed unused import neopixel line
32339 bytes (16 bytes smaller, slightly more headroom under the cliff)
Lint clean even with unused-import enforced (Adafruit CI doesn't disable that one — my local config did, mistake on my part)
Behavior unchanged — neopixel was never actually used here, only inside pixel_zones.py
@videopixil
Copy link
Copy Markdown
Contributor Author

@TheKitty @BlitzCityDIY QTPy ble receiver code is ready to checkout - lint disables on main() for
too-many-locals, too-many-branches, and too-many-statements explained above

@BlitzCityDIY BlitzCityDIY self-requested a review May 7, 2026 23:19
Copy link
Copy Markdown
Collaborator

@BlitzCityDIY BlitzCityDIY left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

okay! here's a summary of the comments i'd like you to look at:

  1. most important is the import battery. it's throwing an import error, not sure if there is another file that got missed?
  2. consider not using a main(). the code setup should be able to happen before the loop and then go into the loop
  3. consider not using internal declarations for so many of the variables, methods and functions that are then called in code.py
  4. the checks for the incoming commands could be condensed
  5. anything used for prototyping could be removed. that stuff could confuse folks if they're following along with the guide or trying to edit the code

good to see the debouncer library being used, i think it helps to simplify the button press tracking

Comment thread BLE_Beacon_Ears/code.py Outdated
renderer.render_idle(zones)


def main():
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

code.py usually does not utilize a main(). the code segment before while True: could go at the top of the code and then the while True loop could be at the bottom of the code

Comment thread BLE_Beacon_Ears/code.py
@@ -0,0 +1,791 @@
# SPDX-FileCopyrightText: 2026 Pedro Ruiz for Adafruit Industries
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

just a general question- is all of this code being used in the guide? i know there were multiple apps being prototyped so just want to check that the beacon ears need all of this code

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

just saw the other open PR, got it

Comment thread BLE_Beacon_Ears/code.py
import digitalio
from adafruit_debouncer import Button

import battery as battery_mod
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what is this battery import? it is not in any of the helper files and when i try to run the code on a qt py esp32-s3 i get an ImportError:

Traceback (most recent call last):
  File "code.py", line 24, in <module>
ImportError: no module named 'battery'

Comment thread BLE_Beacon_Ears/code.py Outdated
short_duration_ms=_BUTTON_SHORT_MS,
long_duration_ms=_BUTTON_LONG_MS)

state = _RuntimeState()
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the purpose of a leading underscore in python is to mark code as internal use only, meaning that the end user wouldn't access it directly. this file uses it a lot for variables and methods that are being called directly in user facing code

Comment thread BLE_Beacon_Ears/code.py Outdated
Comment on lines +377 to +397
_REMOTE_BATTERY_PACKET = bytes.fromhex("aa4201")
_REMOTE_BRIGHTNESS_PACKET = bytes.fromhex("aa4203")
_REMOTE_FIND_PACKET = bytes.fromhex("aa4204")
_REMOTE_STATUE_PACKET = bytes.fromhex("aa4205")


def _is_remote_battery_trigger(payload):
return bytes(payload[:3]) == _REMOTE_BATTERY_PACKET


def _is_remote_brightness_trigger(payload):
return bytes(payload[:3]) == _REMOTE_BRIGHTNESS_PACKET


def _is_remote_find_trigger(payload):
return bytes(payload[:3]) == _REMOTE_FIND_PACKET


def _is_remote_statue_trigger(payload):
return bytes(payload[:3]) == _REMOTE_STATUE_PACKET

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i think this could be condensed into one lookup function that checks the possible commands:

REMOTE_COMMANDS = {
    bytes.fromhex("aa4201"): "battery",
    bytes.fromhex("aa4203"): "brightness",
    bytes.fromhex("aa4204"): "find",
    bytes.fromhex("aa4205"): "statue",
}

def remote_command(payload):
    return REMOTE_COMMANDS.get(bytes(payload[:3]))

Comment thread BLE_Beacon_Ears/code.py Outdated
Comment on lines +684 to +743
if payload is None:
continue
if not payload:
continue
if _is_remote_brightness_trigger(payload):
now = time.monotonic()
if now - state.last_trigger_time < _TRIGGER_COOLDOWN_S:
continue
state.last_trigger_time = now
state.brightness_idx[0] = (
(state.brightness_idx[0] + 1) % len(_BRIGHTNESS_PRESETS))
new_b = _BRIGHTNESS_PRESETS[state.brightness_idx[0]]
pixels.set_brightness(new_b)
state.brightness_flash_until = now + _BRIGHTNESS_FLASH_DURATION_S
state.brightness_flash_level = state.brightness_idx[0]
continue
if _is_remote_battery_trigger(payload):
now = time.monotonic()
if now - state.last_trigger_time < _TRIGGER_COOLDOWN_S:
continue
state.last_trigger_time = now
batt.update(force=True)
state.battery_display_voltage = batt.voltage
v_str = (f"{state.battery_display_voltage:.3f}V"
if state.battery_display_voltage is not None else "None")
if (state.battery_display_voltage is not None
and state.battery_display_voltage > _USB_PRESENT_V_RAW):
print(f"[battery trigger remote] {v_str} (USB - showing display)")
state.battery_display_until = now + _BATTERY_DISPLAY_DURATION_S
state.battery_display_started_at = now
else:
print(f"[battery trigger remote] {v_str} (battery - yellow flash)")
state.unavailable_flash_until = now + _UNAVAILABLE_FLASH_DURATION_S
state.unavailable_flash_started_at = now
continue
if _is_remote_find_trigger(payload):
now = time.monotonic()
if now - state.last_trigger_time < _TRIGGER_COOLDOWN_S:
continue
state.last_trigger_time = now
print("[find me] starting high-visibility animation")
pixels.set_brightness(1.0)
state.find_mode_until = now + renderer.FIND_MODE_DURATION_S
state.find_mode_started_at = now
continue
if _is_remote_statue_trigger(payload):
now = time.monotonic()
if now - state.last_trigger_time < _TRIGGER_COOLDOWN_S:
continue
state.last_trigger_time = now
print("[statue preview] firing golden swirl")
fake_statue_cmd = {
"kind": "statue_beacon",
"statue_id": "PV",
"raw": bytes(payload),
}
candidate = renderer.for_command(fake_statue_cmd)
if candidate is not None:
new_command = (candidate, entry.rssi, bytes(payload))
continue
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

and then you can call the command and perform logic depending on the result:

command = remote_command(payload)
if command is None:
    continue

now = time.monotonic()
if now - state.last_trigger_time < TRIGGER_COOLDOWN_S:
    continue
state.last_trigger_time = now

if command == "brightness":
    # etc

Comment thread BLE_Beacon_Ears/pixel_zones.py Outdated
import neopixel


class OnboardSingle:
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i would take this out since it was used for prototyping only and just uses the onboard neopixel

Comment thread BLE_Beacon_Ears/pixel_zones.py Outdated
self._last_shown_black = False


class _SingleZone:
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

seems like it is part of the prototyping, could remove

applied fixes to code, removed main and added battery.py
@videopixil
Copy link
Copy Markdown
Contributor Author

@BlitzCityDIY awesome thanks! applied fixes with all of the suggested sections and added the battery.py file. verified new files all still work with hardware - stating on the CLUE edits next

@BlitzCityDIY BlitzCityDIY merged commit 92a79a1 into adafruit:main May 11, 2026
4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants