Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 49 additions & 0 deletions CLUE_BLE_Beacon_Remote/ble_transmitter.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
# SPDX-FileCopyrightText: 2026 Pedro Ruiz for Adafruit Industries
#
# SPDX-License-Identifier: MIT
'''MagicBand+ BLE transmitter for the Adafruit CLUE.

Wraps _bleio.adapter to broadcast raw advertisement packets with Disney's
0x0183 manufacturer company identifier. Works directly with the BLE stack
on the nRF52840 without going through adafruit_ble's Advertisement classes.
'''
# Target: Adafruit CLUE (nRF52840) - the BLE remote
import time
import _bleio

from magicband_protocol import DISNEY_CID

# BLE advertising interval in seconds. CircuitPython requires this to be
# in the range 0.02-10.24. We use 0.025 instead of 0.02 because float
# precision can cause 0.02 to internally evaluate as slightly less than
# the minimum, raising "interval must be in range" ValueError.
_AD_INTERVAL = 0.025

# Default broadcast duration. MagicBands latch a command within the first
# ~second, but the timing byte in the payload controls the actual fade so
# we can stop advertising well before the animation finishes.
_BROADCAST_SECONDS = 3.0


def _build_advertisement(payload):
'''Assemble a 31-byte BLE advertisement packet with Disney manufacturer data.'''
cid_lo = DISNEY_CID & 0xFF
cid_hi = (DISNEY_CID >> 8) & 0xFF
mfr_field_len = 3 + len(payload)
return bytes((
0x02, 0x01, 0x06, # Flags AD: LE General Discoverable
mfr_field_len, 0xFF, cid_lo, cid_hi, # Manufacturer data header
)) + payload


def broadcast(payload, duration=_BROADCAST_SECONDS):
'''Advertise a MagicBand+ manufacturer-data payload for duration seconds.'''
packet = _build_advertisement(payload)
adapter = _bleio.adapter
if not adapter.enabled:
adapter.enabled = True
if adapter.advertising:
adapter.stop_advertising()
adapter.start_advertising(packet, connectable=False, interval=_AD_INTERVAL)
time.sleep(duration)
adapter.stop_advertising()
78 changes: 78 additions & 0 deletions CLUE_BLE_Beacon_Remote/boot.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
# SPDX-FileCopyrightText: 2026 Pedro Ruiz for Adafruit Industries
#
# SPDX-License-Identifier: MIT
'''CLUE boot configuration: optional Python-writable filesystem.

CircuitPython by default mounts CIRCUITPY as read-only when USB is
connected, so Python can't write files. To capture BLE packets to a
file (Listen Mode in code.py) while USB is connected, we need to flip
that.

Two ways to enter capture mode:

1. **Marker file**: drop a file named `capture_mode.txt` onto the
CIRCUITPY drive while USB is connected, then reset. boot.py sees
the marker and remounts the filesystem as Python-writable.

2. **NVM flag**: code.py can request "next boot in capture mode" via
a byte in microcontroller.nvm. This survives reboots and works
regardless of who currently owns the filesystem. boot.py also
creates the marker file in this case so the user can see the
mode is engaged.

To exit capture mode: open the REPL and run
import os; os.remove("/capture_mode.txt")
then reset. The filesystem returns to host-writable.
'''
# Target: Adafruit CLUE (nRF52840) - the BLE remote
import os
import storage
import microcontroller

_MARKER = "/capture_mode.txt"
_NVM_FLAG_BYTE = 0 # NVM byte 0: 1 = request capture mode on this boot

marker_present = False
try:
os.stat(_MARKER)
marker_present = True
except OSError:
pass

# NVM-requested capture mode: code.py wrote 1 to byte 0 to ask for
# capture mode on this boot. Honor it by remounting writable and
# creating the marker file (which clears the NVM flag for next time).
nvm_request = microcontroller.nvm[_NVM_FLAG_BYTE] == 1

if marker_present:
storage.remount("/", readonly=False)
print("[boot] Capture mode (marker file present)")
elif nvm_request:
storage.remount("/", readonly=False)
# Create the marker file so user can SEE that capture mode is active.
# Content includes the literal REPL commands to undo it - paste-ready
# without leading whitespace, so users who open the file in any text
# editor can copy/paste directly into the serial REPL.
try:
with open(_MARKER, "w", encoding="utf-8") as f:
f.write(
"Capture mode active.\n"
"This file makes CIRCUITPY Python-writable so Listen Mode\n"
"can save captures. While this file exists, you CANNOT\n"
"drag-drop new code onto the drive.\n"
"\n"
"To return to dev mode (drag-drop), open the serial REPL,\n"
"press Ctrl+C to interrupt, then paste these lines:\n"
"\n"
"import os\n"
"os.remove(\"/capture_mode.txt\")\n"
"\n"
"Then reset the CLUE.\n"
)
# Clear the NVM flag - we honored it
microcontroller.nvm[_NVM_FLAG_BYTE] = 0
print("[boot] Capture mode (NVM-requested, marker created)")
except OSError as err:
print(f"[boot] Capture mode requested but write failed: {err}")
else:
print("[boot] Dev mode: USB host has filesystem write access")
Loading