Hello,
I am trying to adapt this tutorial originally made for a Adafruit Feather: Overview | ESP32-S3 BLE iOS Media Controller | Adafruit Learning System
The only thing I cannot get working is to see the Xiao board in iOS Bluetooth settings as per indicated in the tutorial. THe whole system stay “invisible” but if I use my friend’s Android phone or if I use nRF on my phone, I can see the Xiao.
What is bugging me is that, according to the tutorial, the board should appear in the Bluetooth settings without the need of an extra app.
So I wonder if I am missing (or I have overlooked) something. And if yes what could it be?
Thanks in advance for your help
Kind Regards
Alain
Hi there,
SO, Which Iphone you using there?
some older models don’t support the BLE advertising.
HTH
GL
PJ ![]()
Hello,
It is an Iphone 15, running the latest version of IOS
best
Alain
Hi there,
Ok Good,
So iOS Bluetooth Settings is not a general BLE scanner. That tutorial works because it uses an Apple-recognized BLE profile (HID) and advertises in a way iOS chooses to surface in Settings. Android and nRF Connect will show “normal” BLE devices all day long; iOS Settings often won’t.
The sketch is BLE-only, but iOS Settings mostly shows “classic-ish” / Apple-recognized stuff
iOS Settings will reliably show:
- Classic Bluetooth devices
- BLE HID devices (keyboard/media controller), when advertised correctly
- A few Apple-friendly categories (audio accessories, etc.)
If your code is just a custom BLE service, it will show in nRF Connect but not in Settings. That’s expected.
- ESP32 BLE Arduino (Bluedroid) + an HID wrapper library (often what Adafruit tutorials assume)
For a Media Controller visible to iOS Settings, you typically want an ESP32 HID library that emits proper BLE HID descriptors + appearance + flags.
Even if the device is a real HID service, iOS can ignore it in Settings if the advertising payload is “wrong-ish”.
Key bits that help:
- Include HID service UUID (0x1812) in advertising (not only in scan response)
- Set Appearance to HID (keyboard/remote control)
- Set flags properly (
LE General Discoverable,BR/EDR not supported) - Don’t hide the name in scan response only (some iOS behavior differs)
To appear in Settings, you must advertise as an Apple-recognized profile, typically BLE HID (0x1812), and your advertising must include the HID UUID and correct appearance/flags. the XIAO port probably changed BLE library/stack and lost HID-correct advertising.
Action plan (fastest path)
- Confirm the sketch is truly BLE HID (not just “media control over BLE UART”).
- Switch to a known-good ESP32 HID library/example:
- Look for an ESP32-S3 BLE HID Consumer Control example (play/pause/next/prev).
- Many people use
ESP32-BLE-Keyboard/BleKeyboardstyle libs, but for media keys you need Consumer Control support, not just keyboard.
- Ensure the advertising includes 0x1812 and device name in the primary ADV packet.
Post up the code YOU are trying, Use the code tags above" </> " We can look and be certain.
HTH
GL
PJ ![]()
FYI,
NimBleLib works well I see and I have used it with great success for multiple BLE concurrent connections. works very well with Xiao ESP32S3 There is a demo with code on here also.
Hello,
Thank you so much for your thorough answer, So here is the code I am using so far.And it seems I am missing the HID service you’re mentionning in your reply…
import time
import board
import pwmio
import busio
import displayio
import fourwire
import terminalio
import analogio
import digitalio
import adafruit_st7789
import adafruit_ble
from adafruit_display_text import label
from adafruit_ble.advertising.standard import SolicitServicesAdvertisement
from adafruit_ble_apple_media import AppleMediaService
from adafruit_ble_apple_media import UnsupportedCommand
#light Sensor
light_sensor = analogio.AnalogIn(board.A0)
#display
displayio.release_displays()
backlight = pwmio.PWMOut(board.D5, frequency=5000, duty_cycle=65535)
spi = busio.SPI(clock=board.D8, MOSI=board.D10)
display_bus = fourwire.FourWire(spi, command=board.D6, chip_select=board.D3, reset=board.D7)
display = adafruit_st7789.ST7789(display_bus, width=320, height=240, rotation=90)
BLUE = 0x0057B7
MIDDLE_BLUE = 0x1E3A5F
WHITE = 0xFFFFFF
YELLOW = 0xFFDD00
CYAN = 0x00FFFF
splash = displayio.Group()
background = displayio.Bitmap(320, 240, 1)
palette = displayio.Palette(1)
palette[0] = BLUE
bg_sprite = displayio.TileGrid(background, pixel_shader=palette, x=0, y=0)
splash.append(bg_sprite)
sep_palette = displayio.Palette(1)
sep_palette[0] = MIDDLE_BLUE
def format_text(text, max_chars=24):
if not text: return "---"
if len(text) > max_chars:
return text[:max_chars-3] + "..."
return text
label_artiste = label.Label(terminalio.FONT, text="ARTIST:", color=CYAN, x=10, y=20, scale=2)
splash.append(label_artiste)
valeur_artiste = label.Label(terminalio.FONT, text="---", color=WHITE, x=10, y= 45, scale=2)
splash.append(valeur_artiste)
sep1 = displayio.Bitmap(300, 2, 1)
splash.append(displayio.TileGrid(sep1, pixel_shader=sep_palette, x=10, y=65))
label_album = label.Label(terminalio.FONT, text="ALBUM:", color=CYAN, x=10, y=85, scale=2)
splash.append(label_album)
valeur_album = label.Label(terminalio.FONT, text="---", color=WHITE, x=10, y=110, scale=2)
splash.append(valeur_album)
sep2 = displayio.Bitmap(300, 2, 1)
splash.append(displayio.TileGrid(sep2, pixel_shader=sep_palette, x=10, y=130))
label_titre = label.Label(terminalio.FONT, text="TITLE:", color=CYAN, x=10, y=150, scale=2)
splash.append(label_titre)
valeur_titre = label.Label(terminalio.FONT, text="---", color=WHITE, x=10, y=175, scale=2)
splash.append(valeur_titre)
sep3 = displayio.Bitmap(300, 2, 1)
splash.append(displayio.TileGrid(sep3, pixel_shader=sep_palette, x=10, y=200))
label_statut = label.Label(terminalio.FONT, text="...", color=YELLOW, x=80, y=225, scale=2)
splash.append(label_statut)
display.root_group = splash
#buttons
btn_play = digitalio.DigitalInOut(board.D2)
btn_play.direction = digitalio.Direction.INPUT
btn_play.pull = digitalio.Pull.UP
btn_next = digitalio.DigitalInOut(board.D4)
btn_next.direction = digitalio.Direction.INPUT
btn_next.pull = digitalio.Pull.UP
btn_prev = digitalio.DigitalInOut(board.D1)
btn_prev.direction = digitalio.Direction.INPUT
btn_prev.pull = digitalio.Pull.UP
#Bluetooth
radio = adafruit_ble.BLERadio()
radio.name = "BLE Remote"
a = SolicitServicesAdvertisement()
a.solicited_services.append(AppleMediaService)
def update_display(ams):
try:
valeur_artiste.text = format_text(ams.artist, 25) or "---"
valeur_album.text = format_text(ams.album, 25) or "---"
valeur_titre.text = format_text(ams.title, 25) or "---"
if ams.playing:
label_statut.text = "Now Playing"
elif ams.paused:
label_statut.text = "Pause"
except:
pass
ams = None
old_play = True
old_next = True
old_prev = True
last_ui_update = 0
print("Starting up...")
while True:
val = light_sensor.value
backlight.duty_cycle = max(6500, val)
if not radio.connected:
ams = None
label_statut.text = "Awaiting Bluetooth"
if not radio.advertising:
print("Advertising started...")
radio.start_advertising(a)
while not radio.connected:
pass
print("Connected")
label_statut.text = "Connected"
if ams is None:
for connection in radio.connections:
if not connection.paired:
try:
connection.pair()
except:
pass
try:
ams = connection[AppleMediaService]
update_display(ams)
except:
continue
if ams:
cur_play = btn_play.value
cur_next = btn_next.value
cur_prev = btn_prev.value
if old_play and not cur_play:
try: ams.toggle_play_pause()
except: pass
time.sleep(0.25)
elif old_next and not cur_next:
try: ams.next_track()
except: pass
time.sleep(0.25)
elif old_prev and not cur_prev:
try: ams.previous_track()
except: pass
time.sleep(0.25)
old_play = cur_play
old_next = cur_next
old_prev = cur_prev
if time.monotonic() - last_ui_update > 3.0:
update_display(ams)
last_ui_update = time.monotonic()
time.sleep(0.01)
The question is using this HID approach can I “mix” it with AppleMediaService thus keeping the information of what is being played?
Best
Alain
Hi there,
So, I know Apple often is different so YMMV, the IOS Bluetooth is very tight.
things need to be in a specific order and version often to get it to work…
You’re actually very close — nothing is “broken.” What you’re’re seeing is mostly iOS behavior, not a XIAO issue.
Why your board is invisible in iOS Settings
iOS Bluetooth Settings does NOT list generic BLE devices.
It only shows devices that present themselves as specific profiles such as:
- HID (keyboard, mouse, media remote)
- Audio devices
- Some approved accessory categories
Your current code advertises AppleMediaService (AMS) using SolicitServicesAdvertisement.
That’s correct for metadata and control — but it does not make the device appear in iOS Settings.
That’s why:
- Android sees it
- nRF Connect sees it
- iOS Settings does not
This is expected behavior.
Can you combine HID + AppleMediaService?
Yes. Absolutely.
And this is actually the cleanest solution.
You can:
- Use AppleMediaService (AMS) to:
- Read artist
- Read album
- Read title
- Read play/pause state
- Use BLE HID (Consumer Control) to:
- Send Play/Pause
- Send Next Track
- Send Previous Track
This way:
- iOS will recognize the device as a media remote (HID)
- It will show up in Bluetooth Settings
- You still keep full “Now Playing” metadata via AMS
They do not conflict — they are separate BLE services.
You need to:
- Add HID service (Consumer Control)
- Advertise HID (not only AMS solicit)
- Keep AMS for metadata
In CircuitPython that typically means adding something like:
from adafruit_ble.services.standard.hid import HIDService
from adafruit_hid.consumer_control import ConsumerControl
from adafruit_hid.consumer_control_code import ConsumerControlCode
Then:
- Create HIDService
- Add it to advertising
- Use
ConsumerControlto send media key events
You can still do:
ams = connection[AppleMediaService]
for metadata.
Important note about advertising
Right now you use:
a = SolicitServicesAdvertisement()
a.solicited_services.append(AppleMediaService)
That’s good for AMS.
But for HID visibility, you’ll likely need a normal ProvideServicesAdvertisement including HIDService, or combine both in your advertising strategy.
iOS is picky about how it discovers services.
So you can see the answer is ;
- Yes — you can combine HID + AppleMediaService.
- HID makes it appear in iOS Settings.
- AMS keeps your Now Playing metadata.
This is actually a very nice hybrid design when done correctly.
HTH
GL
PJ ![]()
Hello,
Thank you so much for all those explanations and most importantly for your time.
It is so helpful.
Now it’s time to do some reading for me and after trying to implement all of this correctly.
thanks again
Have a beautiful day
Best
Alain
Hi there,
Certainly, Please keep us posted on your progress. ![]()
GL
PJ ![]()
thanks a lot, this really help