I have a Philips Hue bulb that I only use over Bluetooth. No bridge. It works, but turning a light on means unlocking the phone, opening the app and waiting for it to connect. That is a lot of steps for a light switch.

I also had an M5Stack ATOM Lite sitting in a drawer. It is a tiny ESP32 board, about 24 mm square, with one button and one RGB LED on top. One button, one light. So I turned it into a remote: press the button, the bulb toggles. No bridge, no Wi-Fi, just BLE between the ATOM and the bulb.

The code is at github.com/ferbass/atom-hue-ble.

What the bulb speaks

Hue Bluetooth bulbs expose a light control service over BLE. Signify does not document it, but the UUIDs have been known for a while and they matched my bulb on the first try:

static const char* HUE_SERVICE_UUID = "932c32bd-0000-47a2-835a-a8d455b859dd";
static const char* HUE_ONOFF_UUID   = "932c32bd-0002-47a2-835a-a8d455b859dd";

The on/off characteristic is one byte. Write 0x01 and the light turns on, write 0x00 and it turns off. You can also read it, and it sends notifications, which turned out to be useful later.

That is the whole protocol for a toggle. The hard part is being allowed to talk to it.

The pairing mode you have to do

This is the part that cost me the most time, so it gets its own section.

The bulb will happily let anything connect to it. What it will not do is let an unknown device read or write the light characteristics. For that the link has to be encrypted and bonded, and the bulb only accepts a new bond while it is in pairing mode. Outside of that window it just ignores the pairing request. No error, nothing. On the ATOM side it looks like this:

[ble] connected, starting encryption/bonding...
[sec] ERROR: bulb did not answer the pairing request - is it in pairing mode?

I watched that loop for a while before it clicked that the bulb was simply not listening.

The switch for it is hidden in a place I would never have looked. In the Hue app:

  1. Go to Settings → Voice assistants.
  2. Pick Amazon Alexa or Google Home. You do not need to own either.
  3. Tap Make visible.

That opens the window, and it only lasts about a minute.

There is a second catch. The bulb holds roughly one BLE connection at a time, and the Hue app is that connection. So right after tapping Make visible you have to close the app, or turn off Bluetooth on the phone, otherwise the ATOM cannot get in.

So the order is: ATOM flashed and scanning, make the bulb visible, close the app, wait. The ATOM keeps retrying on its own, so there is nothing to time by hand. When it works:

[scan] picked fa:dd:..:c1 (rssi -76)
[ble] connected, starting encryption/bonding...
[sec] encrypted=1 bonded=1
[gatt] light is currently ON
[gatt] notifications enabled
[nvs] saved bulb fa:dd:..:c1
[ble] ready - press the button to toggle

You only do this once. The ATOM stores the bulb address in flash and the bond keys stay on both sides, so on every boot after that it skips the scan and connects straight to the saved bulb.

On the ESP32 side the security setup is three lines with NimBLE-Arduino: bonding on, no MITM, secure connections, and no input or output, because the ATOM has no screen or keypad to confirm a code with.

NimBLEDevice::setSecurityAuth(true, false, true);
NimBLEDevice::setSecurityIOCap(BLE_HS_IO_NO_INPUT_OUTPUT);

Using it

A press toggles the light. The LED on the ATOM tells you what is going on:

LED Meaning
Blinking blue Scanning / pairing
Warm white Connected, light is on
Red Connected, light is off
Blinking yellow Lost the bulb, reconnecting
Fast purple Pairing reset

Because the bulb sends notifications, the LED stays honest. If I turn the light off from the phone, the ATOM goes red by itself.

If the connection drops, it retries with a backoff that tops out at 30 seconds, and a button press while disconnected forces a retry right away.

Holding the button for 5 seconds forgets the bulb, deletes the bond and restarts into pairing mode. Then it is back to Make visible in the app.

Things I ran into

Range is short. With the ATOM on my desk the bulb showed up at around -85 dBm and some connection attempts failed outright. Closer is better. I would not count on more than 5 to 10 meters, and less through walls.

The bulb does not advertise the light service UUID. In its advertisements it shows a 16-bit Signify UUID, 0xFE0F, so the scan matches on that too. Matching by name alone is not reliable either, because the name is whatever you called the light in the app. Mine is not called “Hue” anything.

And the phone is always the first suspect. If the ATOM cannot connect, the Hue app is probably open somewhere.

If your bulb uses different UUIDs, there is a second build in the repo that connects and prints every service and characteristic it finds:

pio run -e debug-dump -t upload && pio device monitor

Next: a battery

Right now the ATOM hangs off a USB cable, which is not much of a remote. The plan is to snap it onto the Atomic Battery Base, a 200 mAh battery that the ATOM sits on top of.

200 mAh is not a lot, and the firmware keeps the BLE connection open all the time and never sleeps. So that is the next thing to fix: sleep, wake on the button, reconnect, toggle. I will write that up once I know how long it actually lasts.

For now it is a button that turns on a light, and it took a weekend and one hidden menu to get there.


This post was written with the help of AI (Claude by Anthropic).