Skip to content

Commit 1f97b68

Browse files
authored
Merge pull request #3239 from videopixil/CLUE_BLE_Beacon_Remote
CLUE ble beacon remote
2 parents 883ad49 + 77a62b0 commit 1f97b68

6 files changed

Lines changed: 1633 additions & 0 deletions

File tree

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
# SPDX-FileCopyrightText: 2026 Pedro Ruiz for Adafruit Industries
2+
#
3+
# SPDX-License-Identifier: MIT
4+
'''MagicBand+ BLE transmitter for the Adafruit CLUE.
5+
6+
Wraps _bleio.adapter to broadcast raw advertisement packets with Disney's
7+
0x0183 manufacturer company identifier. Works directly with the BLE stack
8+
on the nRF52840 without going through adafruit_ble's Advertisement classes.
9+
'''
10+
# Target: Adafruit CLUE (nRF52840) - the BLE remote
11+
import time
12+
import _bleio
13+
14+
from magicband_protocol import DISNEY_CID
15+
16+
# BLE advertising interval in seconds. CircuitPython requires this to be
17+
# in the range 0.02-10.24. We use 0.025 instead of 0.02 because float
18+
# precision can cause 0.02 to internally evaluate as slightly less than
19+
# the minimum, raising "interval must be in range" ValueError.
20+
_AD_INTERVAL = 0.025
21+
22+
# Default broadcast duration. MagicBands latch a command within the first
23+
# ~second, but the timing byte in the payload controls the actual fade so
24+
# we can stop advertising well before the animation finishes.
25+
_BROADCAST_SECONDS = 3.0
26+
27+
28+
def _build_advertisement(payload):
29+
'''Assemble a 31-byte BLE advertisement packet with Disney manufacturer data.'''
30+
cid_lo = DISNEY_CID & 0xFF
31+
cid_hi = (DISNEY_CID >> 8) & 0xFF
32+
mfr_field_len = 3 + len(payload)
33+
return bytes((
34+
0x02, 0x01, 0x06, # Flags AD: LE General Discoverable
35+
mfr_field_len, 0xFF, cid_lo, cid_hi, # Manufacturer data header
36+
)) + payload
37+
38+
39+
def broadcast(payload, duration=_BROADCAST_SECONDS):
40+
'''Advertise a MagicBand+ manufacturer-data payload for duration seconds.'''
41+
packet = _build_advertisement(payload)
42+
adapter = _bleio.adapter
43+
if not adapter.enabled:
44+
adapter.enabled = True
45+
if adapter.advertising:
46+
adapter.stop_advertising()
47+
adapter.start_advertising(packet, connectable=False, interval=_AD_INTERVAL)
48+
time.sleep(duration)
49+
adapter.stop_advertising()

CLUE_BLE_Beacon_Remote/boot.py

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
# SPDX-FileCopyrightText: 2026 Pedro Ruiz for Adafruit Industries
2+
#
3+
# SPDX-License-Identifier: MIT
4+
'''CLUE boot configuration: optional Python-writable filesystem.
5+
6+
CircuitPython by default mounts CIRCUITPY as read-only when USB is
7+
connected, so Python can't write files. To capture BLE packets to a
8+
file (Listen Mode in code.py) while USB is connected, we need to flip
9+
that.
10+
11+
Two ways to enter capture mode:
12+
13+
1. **Marker file**: drop a file named `capture_mode.txt` onto the
14+
CIRCUITPY drive while USB is connected, then reset. boot.py sees
15+
the marker and remounts the filesystem as Python-writable.
16+
17+
2. **NVM flag**: code.py can request "next boot in capture mode" via
18+
a byte in microcontroller.nvm. This survives reboots and works
19+
regardless of who currently owns the filesystem. boot.py also
20+
creates the marker file in this case so the user can see the
21+
mode is engaged.
22+
23+
To exit capture mode: open the REPL and run
24+
import os; os.remove("/capture_mode.txt")
25+
then reset. The filesystem returns to host-writable.
26+
'''
27+
# Target: Adafruit CLUE (nRF52840) - the BLE remote
28+
import os
29+
import storage
30+
import microcontroller
31+
32+
_MARKER = "/capture_mode.txt"
33+
_NVM_FLAG_BYTE = 0 # NVM byte 0: 1 = request capture mode on this boot
34+
35+
marker_present = False
36+
try:
37+
os.stat(_MARKER)
38+
marker_present = True
39+
except OSError:
40+
pass
41+
42+
# NVM-requested capture mode: code.py wrote 1 to byte 0 to ask for
43+
# capture mode on this boot. Honor it by remounting writable and
44+
# creating the marker file (which clears the NVM flag for next time).
45+
nvm_request = microcontroller.nvm[_NVM_FLAG_BYTE] == 1
46+
47+
if marker_present:
48+
storage.remount("/", readonly=False)
49+
print("[boot] Capture mode (marker file present)")
50+
elif nvm_request:
51+
storage.remount("/", readonly=False)
52+
# Create the marker file so user can SEE that capture mode is active.
53+
# Content includes the literal REPL commands to undo it - paste-ready
54+
# without leading whitespace, so users who open the file in any text
55+
# editor can copy/paste directly into the serial REPL.
56+
try:
57+
with open(_MARKER, "w", encoding="utf-8") as f:
58+
f.write(
59+
"Capture mode active.\n"
60+
"This file makes CIRCUITPY Python-writable so Listen Mode\n"
61+
"can save captures. While this file exists, you CANNOT\n"
62+
"drag-drop new code onto the drive.\n"
63+
"\n"
64+
"To return to dev mode (drag-drop), open the serial REPL,\n"
65+
"press Ctrl+C to interrupt, then paste these lines:\n"
66+
"\n"
67+
"import os\n"
68+
"os.remove(\"/capture_mode.txt\")\n"
69+
"\n"
70+
"Then reset the CLUE.\n"
71+
)
72+
# Clear the NVM flag - we honored it
73+
microcontroller.nvm[_NVM_FLAG_BYTE] = 0
74+
print("[boot] Capture mode (NVM-requested, marker created)")
75+
except OSError as err:
76+
print(f"[boot] Capture mode requested but write failed: {err}")
77+
else:
78+
print("[boot] Dev mode: USB host has filesystem write access")

0 commit comments

Comments
 (0)