BLE Eddystone Beacon
import time
from adafruit_ble import BLERadio
from adafruit_ble_eddystone import uid, url
# ------------------------ BLE RADIO ------------------------
# BLERadio is the CircuitPython BLE stack entry point.
# It handles advertising and (if needed) connections.
ble = BLERadio()
ble.name = "EE5127EddystoneBeacon" # optional: set a custom device name
print(":".join(f"{b:02X}" for b in ble.address_bytes)) # print the board's BLE address (for convenience)
# ------------------------ CONFIG ---------------------------
# INSTANCE_ID must be EXACTLY 6 bytes for Eddystone-UID.
# Using the board's BLE address is a convenient, unique default.
# (You can replace this with your own stable 6-byte value.)
INSTANCE_ID = ble.address_bytes # 6 bytes (48-bit BLE address)
# Example custom instance instead:
# INSTANCE_ID = bytes.fromhex("A1B2C3D4E5F6")
# NAMESPACE_ID must be EXACTLY 10 bytes for Eddystone-UID.
# Your library supports `namespace_id=...`, so we can set it explicitly.
# Choose a project/company/site-wide namespace so all your beacons "belong together".
NAMESPACE_ID = b"CircuitPy!" # 10 bytes here (yes, exactly 10 chars)
# Measured power = calibrated RSSI at ~1 meter.
# Apps use this to estimate distance from RSSI.
# Typical values are around -59 dBm (but CALIBRATE with your phone!).
TX_POWER_DBM = -30
# ------------------------ HELPERS --------------------------
def dbm_to_byte(dbm: int) -> int:
"""
Convert a signed dBm value (-128..127) to the 0..255 raw byte
the Eddystone helper expects. This avoids 'value must fit in 1 byte'
errors when you pass negative dBm values like -59.
(Two's complement mapping: -59 -> 197 (0xC5))
"""
x = int(round(dbm))
if x < -128:
x = -128
if x > 127:
x = 127
return (x + 256) % 256
# Sanity checks (fail early with a clear message if lengths are off)
assert len(INSTANCE_ID) == 6, "Eddystone Instance ID must be exactly 6 bytes"
assert len(NAMESPACE_ID) == 10, "Eddystone Namespace ID must be exactly 10 bytes"
# Convert signed dBm to the raw one-byte format expected by the library
TX_POWER_BYTE = dbm_to_byte(TX_POWER_DBM)
# ------------------------ EDDYSTONE FRAMES -----------------
# UID frame: carries a 10-byte Namespace + 6-byte Instance.
# Why: ideal for proximity/zone IDs; receiver compares RSSI vs. tx_power to estimate distance.
eddystone_uid = uid.EddystoneUID(
INSTANCE_ID,
namespace_id=NAMESPACE_ID, # your custom 10-byte Namespace
tx_power=TX_POWER_BYTE # raw byte form of measured power (avoid negative-int errors)
)
# (Optional) URL frame: broadcasts a short link.
# Why: handy for "tap to open" behavior in scanners; not needed for proximity, but nice to include.
eddystone_url = url.EddystoneURL("https://adafru.it/discord")
# ------------------------ ADVERTISING LOOP -----------------
# Strategy: alternate short bursts of UID and URL advertising.
# Why alternate? A single radio can only send one advertising payload at a time.
# Short bursts (≈0.5 s) let scanners see both frames without waiting too long.
# interval=0.1 -> ~100 ms advertising interval (good for phones to discover quickly).
while True:
# --- UID burst (proximity / zone identification) ---
ble.start_advertising(eddystone_uid, interval=0.1) # non-connectable by default
time.sleep(0.6) # send several packets (~0.6 s is plenty)
ble.stop_advertising()
# --- URL burst (optional) ---
ble.start_advertising(eddystone_url, interval=0.1)
time.sleep(0.6)
ble.stop_advertising()
# Pause between cycles.
# Why: reduces duty cycle & power, and prevents constant radio use.
# Adjust to taste (shorter = more responsive; longer = lower power).
time.sleep(4.0)
Last updated