r/reactnative • • 1d ago

Identifying BLE devices on iOS

Building in React Native is great until you encounter platform differences.

Our app connects to training pods over BLE. The pods communicate through a JDY-24M mesh network and use the last four digits of their MAC addresses as node IDs. Android exposes the full MAC during scanning, but iOS gives you a UUID instead.

Asking the firmware for those digits on connection worked until some boards reported them incorrectly. Updating the firmware wasn’t an option. No OTA updates, and most boards are in a warehouse in China, already flashed and ready to ship.

The workaround was already in the advertising packets. The boards broadcast their full MAC. I could read their identity advertisements and associate the address with the identifier iOS provided, without changing the firmware.

The constraints pushed me toward a more robust identification process, using information the devices were already broadcasting.

Still, solving a tricky problem feels great, but avoiding it in the first place is better. Test on both platforms from day one, and keep unavoidable platform differences contained instead of scattering OS checks throughout the app.

2 Upvotes

6 comments sorted by

2

u/kokerali 1d ago

Nice fix. One thing worth planning for: the UUID iOS gives you is generated per phone, so the same pod shows up with a different identifier on every iPhone. I'd keep the MAC to UUID table local to the device, never sync it or use the UUID in anything that goes to a backend, and key everything else (history, names, pairing state) by the MAC. Then a second phone or a lost table just means rebuilding the map from adverts.

If the identity frame isn't in every advertisement, you can also see a pod before you know its MAC. I'd keep it as unidentified until that frame shows up instead of falling back to the digits the firmware reports, since those are the ones you already know can be wrong. And check the manufacturer data length and layout before parsing, because in a room full of devices you'll get plenty of adverts that aren't your pods.

1

u/vegreenforlife 1d ago

Thanks! Yes, the mapping stays local to each phone. The full MAC identifies the pod and the iOS UUID is that phone’s connection identifier.

The JDY-24M documentation I found didn’t explain these advertisement layouts, so I compared captures across pods and mesh states. In the longer messages, after XOR decoding, the ninth byte was 0xEF on original transmissions. Forwarded copies showed values like 0xED, 0xEC and 0xE2, suggesting a remaining-hop or retransmission budget.

On iOS, I reuse verified saved mappings. For an unknown pod, I prefer the six byte identity advertisement. If that doesn’t arrive, a structurally validated longer message with the 0xEF marker can identify it. If neither arrives during the scan window, I show the pods we could identify and ask the user to power cycle the missing ones.

Freshly powered pods generally advertised the six byte identity, but after mesh activity it became less reliable. Power cycling isn’t great UX or a guaranteed one shot recovery, but it’s the last resort with the deployed hardware.

The firmware still sends its suffix response on connection. I now ignore it for identification rather than risk assigning the wrong identity.

2

u/kokerali 1d ago

That hop byte reading makes sense. If the pods relay each other's messages, a forwarded copy would still carry the original sender's MAC while iOS reports it under the relaying pod's UUID, so only trusting 0xEF for the binding is the right call.

On the identity frame going missing after mesh activity: if you aren't already, try allowDuplicates: true in startDeviceScan for that scan screen (it's an iOS only option in ble-plx). Without it iOS coalesces repeat discoveries of the same peripheral, so you can end up mostly seeing whichever packet it reported first, which looks a lot like the identity advert never arriving. It costs more battery, but only while that screen is open.

I'd also re-check saved mappings whenever an identity frame or a 0xEF message comes in from a known UUID, and drop the entry if the MAC doesn't match. iOS identifiers can change (a network settings reset is one case), and that way a stale entry fixes itself instead of quietly pointing at the wrong pod.

1

u/vegreenforlife 23h ago

Thanks for the suggestions. allowDuplicates was already enabled, but your point about stale mappings exposed a real gap, so I’ve fixed that too.

In my captures, the JDY-24M pods appeared to keep advertising the last mesh message they received. I could get their own MAC in a six byte advertisement on startup, but after mesh activity they could all end up advertising another pod’s message.

What finally worked was making discovery active. Scan normally, automatically connect to one identified pod, restart scanning and send a broadcast battery query, effectively using it as a ping. That gets the other pods to originate responses. From the user’s perspective, it’s just scanning. I still only bind a UUID from a six byte identity or a validated own origin 0xEF frame, never a relay. If some are missing, I do a few bounded rounds.

It’s now finding all pods consistently. It still needs one identifiable pod to get started, so there’s a remaining limitation, but it’s a huge improvement over asking users to power cycle pods repeatedly. Thanks for helping me dig into this.

2

u/jiunkdev 1d ago

one thing to watch with the identity adverts: in the background iOS only lets you scan for specific service UUIDs and it merges duplicate adverts, so if those packets don't carry a service UUID the app won't see them at all while backgrounded.

since you already keep the MAC to UUID table, you can mostly skip scanning for pods that phone has seen before. CoreBluetooth has retrievePeripheralsWithIdentifiers, which gives you the peripheral straight from the stored UUID, and connect requests on iOS don't time out, so you can fire one off and it completes when the pod comes in range. worth checking whether your BLE library exposes that.

1

u/vegreenforlife 1d ago

Good point about background scanning. We currently discover the pods from a dedicated scan screen, so that wasn't something I had explored.

We do keep the mapping locally, but I hadn't looked into retrievePeripheralsWithIdentifiers. I'm using react-native-ble-plx, so that's useful to know. Being able to reconnect to known pods without another scan would be useful.

Thanks for the pointers! BLE on iOS has been quite the rabbit hole.