BLE beacon ears#3240
Conversation
QTPy code to make into a BLE receiver
|
The receiver code.py has lint disables on main() for Implication for contributors: if you extend code.py and |
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
|
@TheKitty @BlitzCityDIY QTPy ble receiver code is ready to checkout - lint disables on main() for |
BlitzCityDIY
left a comment
There was a problem hiding this comment.
okay! here's a summary of the comments i'd like you to look at:
- most important is the
import battery. it's throwing an import error, not sure if there is another file that got missed? - consider not using a
main(). the code setup should be able to happen before the loop and then go into the loop - consider not using internal declarations for so many of the variables, methods and functions that are then called in
code.py - the checks for the incoming commands could be condensed
- 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
| renderer.render_idle(zones) | ||
|
|
||
|
|
||
| def main(): |
There was a problem hiding this comment.
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
| @@ -0,0 +1,791 @@ | |||
| # SPDX-FileCopyrightText: 2026 Pedro Ruiz for Adafruit Industries | |||
There was a problem hiding this comment.
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
There was a problem hiding this comment.
just saw the other open PR, got it
| import digitalio | ||
| from adafruit_debouncer import Button | ||
|
|
||
| import battery as battery_mod |
There was a problem hiding this comment.
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'
| short_duration_ms=_BUTTON_SHORT_MS, | ||
| long_duration_ms=_BUTTON_LONG_MS) | ||
|
|
||
| state = _RuntimeState() |
There was a problem hiding this comment.
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
| _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 | ||
|
|
There was a problem hiding this comment.
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]))
| 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 |
There was a problem hiding this comment.
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
| import neopixel | ||
|
|
||
|
|
||
| class OnboardSingle: |
There was a problem hiding this comment.
i would take this out since it was used for prototyping only and just uses the onboard neopixel
| self._last_shown_black = False | ||
|
|
||
|
|
||
| class _SingleZone: |
There was a problem hiding this comment.
seems like it is part of the prototyping, could remove
applied fixes to code, removed main and added battery.py
|
@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 |
QTPy code to make into a BLE receiver