-
Notifications
You must be signed in to change notification settings - Fork 1.4k
Description
Summary
On Seeed XIAO ESP32-C6 running CircuitPython 10.0.3 and 10.1.0-beta1, BLE central operations using _bleio show failures when connecting to a BLE peripheral that advertises 16-bit service UUIDs 0xAE30 / 0xAF30: service discovery either raises MemoryError: Nimble out of memory or returns zero services. Additionally, BLE connection state/teardown appears inconsistent across soft reloads (see “Disconnect behavior” below).
Environment
- Board: Seeed XIAO ESP32-C6 (4MB Flash, 512KB SRAM);
board_id: seeed_xiao_esp32c6 - CircuitPython: 10.0.3 (e.g.
Adafruit CircuitPython 10.0.3 on 2025-10-17; Seeed Xiao ESP32-C6 4MB Flash 512KB SRAM with ESP32-C6FH4) and 10.1.0-beta1 - Host: Thonny over serial (no USB mass storage)
- BLE peripheral: A device advertising 16-bit service UUID
0xAE30or0xAF30(and optionally a local name). Model/type noted in "Tested peripheral" below.
Steps to reproduce
-
Power-cycle the ESP32-C6 to start from a clean BLE state (
adapter.connectedshould typically beFalseat start). Some BLE state/teardown behavior appears inconsistent across soft reloads (see Actual behavior). -
Ensure the target BLE peripheral is powered on and nearby.
-
Copy
ble_mx06_probe.pyto the device and run from REPL (to avoid module cache):exec(open("ble_mx06_probe.py").read())
-
Observe: on 10.0.3, discovery typically fails with NimBLE OOM; on 10.1.0-beta1, discovery often returns 0 services.
Adapting the probe to another BLE peripheral (optional)
- Run once and look at the probe output
scan: merged devices ...to see candidate devices and theirnameanduuids16. - If your device advertises a local name, set
TARGET_NAME_SUBSTRto a unique substring of that name. - If the name is missing but it advertises a 16-bit service UUID, set
UUID16_CANDIDATESto include that UUID (common examples: Battery0x180F, Heart Rate0x180D, Device Information0x180A). - If your device only advertises 128-bit service UUIDs, you can still match by name; otherwise you’ll need to tweak the probe to target a specific
addr=from the scan output (or add 128-bit UUID parsing).
Expected behavior
- After a successful connection,
conn.discover_remote_services([uuid])should return the advertised service(s) or fail with a clear Python exception. - After calling
disconnect(),adapter.connected/adapter.connectionsshould reflect the disconnection in a reasonable amount of time.
Actual behavior
- CP 10.0.3: Scan and connect succeed.
discover_remote_services([0xAE30])raisesMemoryError: Nimble out of memory. - CP 10.1.0-beta1: Scan shows the peripheral (e.g.
uuids16=[0xAF30]), connect succeeds.discover_remote_services([0xAF30])returns 0 services (no exception). In some runs a second discovery attempt triggersMemoryError: Nimble out of memory. - Disconnect behavior (10.1.0): After a
supervisor.reload()with a live connection, the next run starts withadapter.connected=Trueandadapter.connections=1. Callingdisconnect()on that connection can return without an exception, butadapter.connectedandadapter.connectionsmay remain unchanged (connection appears to persist). - Unconfirmed / historical note (possibly 10.0.3): In some debugging sessions, after interrupting a hanging
_bleio.adapter.connect(timeout=...)with Ctrl+C and then doingdel sys.modules[...]+ re-import of the test module, a hard fault was observed. We are currently unable to reproduce this reliably on 10.1.0, so this is included only as an anecdotal symptom.
Minimal logs
10.0.3 — discover fails with OOM:
connect: ok
post-connect mem_free= 178064
discover: AE30 ...
pre-discover mem_free= 178064
discover: MemoryError: Nimble out of memory
10.1.0-beta1 — discover returns 0 services:
scan: selected addr= ... uuids16= [0xAF30] match_uuid16= 0xAF30
connect: ok
discover: uuid16 0xaf30 -> services=0
discover: WARN no services discovered for candidates
Disconnect returns but state stays connected (10.1.0, after soft reload):
adapter.connected = True
adapter.connections = 1
check: disconnect conn 0
check: disconnect returned for conn 0
== after disconnect attempts ==
adapter.connected = True
adapter.connections = 1
Repro script
Save as ble_mx06_probe.py on the device and run from REPL: exec(open("ble_mx06_probe.py").read())
Full script (click to expand)
# BLE probe: scan -> connect -> discover (0xAE30/0xAF30). Run from REPL: exec(open("ble_mx06_probe.py").read())
import gc
import time
import _bleio
import sys
PROBE_VERSION = "10"
TARGET_NAME_SUBSTR = "MX"
UUID16_CANDIDATES = (0xAE30, 0xAF30)
DISCOVER_TRY_ALL = False
def _ad_structures(ad_bytes):
i = 0
n = len(ad_bytes)
while i < n:
length = ad_bytes[i]
if length == 0:
break
if i + 1 >= n:
break
end = i + 1 + length
if end > n:
break
ad_type = ad_bytes[i + 1]
yield ad_type, ad_bytes[i + 2 : end]
i += 1 + length
return
def _parse_name(ad_bytes):
for ad_type, data in _ad_structures(ad_bytes):
if ad_type in (0x08, 0x09):
try:
return data.decode("utf-8", "ignore")
except Exception:
return None
return None
def _list_uuid16(ad_bytes):
out = []
for ad_type, data in _ad_structures(ad_bytes):
if ad_type in (0x02, 0x03):
for j in range(0, len(data) - 1, 2):
out.append(data[j] | (data[j + 1] << 8))
return out
def mem(tag):
try:
print(tag, "mem_free=", gc.mem_free())
except Exception:
print(tag)
print("--- ble_mx06_probe: start v" + PROBE_VERSION + " ---")
try:
print("sys.implementation =", sys.implementation)
except Exception:
pass
try:
import supervisor
print("safe_mode =", getattr(supervisor.runtime, "safe_mode", None))
except Exception:
pass
mem("start")
entry_match = None
match_uuid16 = None
match_addr = None
by_addr = {}
print("adapter.enabled =", getattr(_bleio.adapter, "enabled", None))
print("adapter.connected =", getattr(_bleio.adapter, "connected", None))
print("adapter.connections =", len(getattr(_bleio.adapter, "connections", ()) or ()))
try:
_bleio.adapter.enabled = True
except Exception:
pass
if getattr(_bleio.adapter, "connected", False):
print("WARNING: adapter.connected=True at start; skipping disconnect (can hard-fault).")
print("ACTION: hard reset / power-cycle the board, and ensure printer is OFF, then retry.")
print("--- ble_mx06_probe: done (early) ---")
raise SystemExit
try:
_bleio.adapter.enabled = False
time.sleep(0.2)
_bleio.adapter.enabled = True
time.sleep(0.2)
print("adapter toggled, connected =", getattr(_bleio.adapter, "connected", None))
except Exception as e:
print("adapter toggle skipped:", e)
print("scan: ensure stopped (defensive)")
try:
_bleio.adapter.stop_scan()
except Exception:
pass
SCAN_TIMEOUT_S = 10
print("scan: calling _bleio.adapter.start_scan(timeout=" + str(SCAN_TIMEOUT_S) + ", active=True)")
mem("pre-scan")
try:
scan_iter = _bleio.adapter.start_scan(timeout=SCAN_TIMEOUT_S, active=True, minimum_rssi=-127, buffer_size=1024)
print("scan: iterator ok")
t0 = time.monotonic()
seen = 0
named = 0
uuid_hits = {u: 0 for u in UUID16_CANDIDATES}
for entry in scan_iter:
seen += 1
ad = entry.advertisement_bytes
name = _parse_name(ad)
uuids16 = _list_uuid16(ad)
k = str(entry.address)
st = by_addr.get(k)
if st is None:
st = {"addr": entry.address, "best_rssi": entry.rssi, "connectable_any": bool(entry.connectable),
"scan_response_any": bool(entry.scan_response), "name": None, "uuids16": set(), "packets": 0}
by_addr[k] = st
st["packets"] += 1
if entry.rssi > st["best_rssi"]:
st["best_rssi"] = entry.rssi
st["addr"] = entry.address
st["connectable_any"] = st["connectable_any"] or bool(entry.connectable)
st["scan_response_any"] = st["scan_response_any"] or bool(entry.scan_response)
if name:
st["name"] = name
for u in uuids16:
st["uuids16"].add(u)
for u in UUID16_CANDIDATES:
if u in uuids16:
uuid_hits[u] += 1
if name:
named += 1
if seen <= 8:
try:
print(" entry", seen, "addr=", str(entry.address), "rssi=", entry.rssi, "connectable=", entry.connectable,
"scan_response=", entry.scan_response, "len=", len(ad), "name=", name, "uuids16=", uuids16)
if seen <= 2:
print(" raw_hex=", ad.hex())
except Exception:
print(" entry", seen, "name=", name)
print("scan: loop ended after", round(time.monotonic() - t0, 2), "s; seen=", seen, "named=", named, "uuid16_hits=", uuid_hits)
finally:
try:
_bleio.adapter.stop_scan()
except Exception:
pass
print("scan: merged devices (top 6 by RSSI):")
top = sorted(by_addr.items(), key=lambda kv: kv[1]["best_rssi"], reverse=True)[:6]
for k, st in top:
print(" addr=", k, "rssi=", st["best_rssi"], "packets=", st["packets"], "conn_any=", st["connectable_any"],
"sr_any=", st["scan_response_any"], "name=", st["name"], "uuids16=", sorted(st["uuids16"]))
best = None
for k, st in sorted(by_addr.items(), key=lambda kv: kv[1]["best_rssi"], reverse=True):
if st["name"] and TARGET_NAME_SUBSTR in st["name"]:
best = (k, st)
break
if best is None:
for k, st in sorted(by_addr.items(), key=lambda kv: kv[1]["best_rssi"], reverse=True):
if any(u in st["uuids16"] for u in UUID16_CANDIDATES):
best = (k, st)
break
if best:
match_addr, st = best
if 0xAF30 in st["uuids16"]:
match_uuid16 = 0xAF30
elif 0xAE30 in st["uuids16"]:
match_uuid16 = 0xAE30
else:
match_uuid16 = None
print("scan: selected addr=", match_addr, "name=", st["name"], "uuids16=", sorted(st["uuids16"]), "match_uuid16=", match_uuid16)
entry_match = st["addr"]
else:
entry_match = None
mem("post-scan")
if not entry_match:
print("FAIL: no MX*/AE30/AF30 candidate seen in", SCAN_TIMEOUT_S, "s")
print("--- ble_mx06_probe: done ---")
raise SystemExit
print("connect: ...")
mem("pre-connect")
try:
conn = _bleio.adapter.connect(entry_match, timeout=10)
except Exception as e:
print("connect: error:", e)
print("--- ble_mx06_probe: done ---")
raise SystemExit
print("connect: ok")
mem("post-connect")
try:
order = [match_uuid16] if match_uuid16 else [0xAE30]
if DISCOVER_TRY_ALL:
for u in UUID16_CANDIDATES:
if u not in order:
order.append(u)
print("discover: trying uuid16 in order:", [hex(u) for u in order], "; DISCOVER_TRY_ALL=", DISCOVER_TRY_ALL)
mem("pre-discover")
any_found = False
for u in order:
svc_uuid = _bleio.UUID(u)
try:
services = conn.discover_remote_services([svc_uuid])
except MemoryError as e:
print("discover: uuid16 0x%04X -> MemoryError: %s" % (u, e))
raise
print("discover: uuid16 0x%04X -> services=%d" % (u, len(services)))
if services:
any_found = True
break
if not any_found:
print("discover: WARN no services discovered for candidates")
mem("post-discover")
time.sleep(0.1)
except MemoryError as e:
print("discover: MemoryError:", e)
mem("post-discover(MemoryError)")
finally:
print("disconnect")
conn.disconnect()
mem("post-disconnect")
print("--- ble_mx06_probe: done ---")Tested peripheral (context)
- Name: Often advertised as
MX06(in scan response). - Advertising: 16-bit service UUID seen as
0xAF30(e.g. raw020106030330af) or0xAE30; address type varies (e.g. random). - Device: MX06-style thermal printer used as a concrete BLE peripheral for testing; the issue is reproduced at the
_bleio/NimBLE level, not printer-specific.