Description
Draft: adds a Bluetooth 5 LE Coded PHY long-range mode to the BLE mesh, to extend direct phone-to-phone range without root or extra hardware. It works on two real phones, but the actual range gain hasn't been measured yet. That's why this is a draft, and why we're asking for help testing it in the field (see below).
What it does
- Long-range discovery: a second, connectable extended advertising set on LE Coded, alongside the unchanged legacy 1M advertisement. Extended scanning listens on 1M and Coded at the same time.
- Long-range links: connections are opened on the PHY the advertisement was heard on, then Coded is requested on the link (
setPreferredPhy). The coding is S=8 by default; S=2 or no preference can be chosen. - Modes (debug menu, persisted):
Adaptive(default): each connection stays on fast 1M while the signal is good, and moves to Coded quickly when it weakens. That's after 3 s below −78 dBm, at once below −88 dBm, after 3 failed sends, or if it's already weak at connect. It returns to 1M only after 30 s above −65 dBm.- Incoming connections, which have no RSSI read on Android, follow our outgoing connection to the same peer and only ever request Coded, so the two ends can't fight over a link. Once that measurement is strong again, the incoming side releases its Coded preference ("any PHY") so the other side can return to 1M.
- PHY requests use an exponential back-off, since a remote can refuse a change silently. After 5 refusals a link pauses 5 minutes, then tries again.
- The thresholds can be adjusted live in the debug menu, for testing.
Max range: Coded on every capable link.Off: stock behavior.
- Runtime detection and fallback:
isLeCodedPhySupported, extended advertising support and max advertising data length are read on the phone; support is never assumed.- If the phone lacks support, or rejects the extended scan or the Coded advertising set at runtime, it falls back automatically to standard BLE.
- PHY-aware RSSI gating: the stock thresholds (down to −65 dBm) would discard exactly the weak advertisements long range is for. Coded advertisements are now accepted down to about receiver sensitivity.
- One in-flight client attempt per peer. A long-range peer advertises under two addresses (legacy set and Coded set), so without this a scanner could open two links to the same phone.
- Fix: the previous legacy advertisement is now stopped before advertising restarts. Before, a restart racing the initial start (e.g. a power profile change during startup) could leave an advertisement running that could never be stopped, occupying an advertising slot.
- Diagnostics: a reorganized debug section shows:
- controller capabilities
- advertiser and scanner state, including the TX power the controller chose
- per device, grouped into its real radio connections (outgoing and incoming; Android's server-side mirror of our outgoing connection is merged in): PHY, requested coding, RSSI, delivered/failed/received counts, and readable status codes
- a radio event log
- User-facing indicator: the Mesh row of the channel sheet shows a "long range" badge, an estimated range, and whether connections are on Coded.
Unchanged: wire protocol, GATT service, packet formats, Noise encryption, and the legacy advertisement and scan response. Phones without Coded support, older clients and iOS keep using standard 1M BLE. Details are in docs/ble-long-range.md.
What has been verified
On two physical phones from different manufacturers (Android 13 and Android 16), both reporting Coded PHY and extended advertising support:
- Both phones report Coded PHY, extended advertising and 2M support; Coded advertising and extended scanning start on each.
- Each phone discovers the other through its Coded advertisement and connects directly on Coded, in both directions.
- A link established on 1M moves to Coded (TX and RX) within about 0.3 s once Coded is requested.
- Signed announcements verify over Coded links; gossip sync runs over them.
- Chat messages are delivered in both directions over Coded links.
- Off mode on one phone: legacy-only scan, no Coded advertisement, connects on 1M, and still interoperates with a phone in Max range.
- Adaptive on real devices, using the debug thresholds to simulate distance at a desk:
- with the "to Coded" threshold above the current signal, outgoing connections moved 1M → Coded within ~1 s of the first reading;
- after the thresholds were lowered live, they returned Coded → 1M after exactly the 30 s stable-signal window;
- a remote that ignored PHY requests got retries spaced 6 s → 10 s → 20 s → 40 s, then paused.
- Adaptive return to 1M after a real walk indoors (one phone carried a floor down, then back): the links moved to Coded downstairs. Back upstairs, the first build kept retrying 1M every ~30 s without effect (see the finding below). The fix was reproduced at a desk with both phones going from
Max rangetoAdaptive: each incoming side logged "releases Coded" and every link between them returned to 1M within a second. The walk itself hasn't been repeated with the fix yet. - Mixed setup with an unmodified client: a third device running the unmodified Play Store app connected to a phone with this change. Its connection was then moved to Coded (TX and RX): the long-range phone requested it and that device's Bluetooth chip accepted, even though its app knows nothing about long range. Announcements were verified over that link. Discovery with such a device still uses standard range, since it doesn't send Coded advertisements.
- Unit tests for the fallback rules, PHY hysteresis, the per-peer guard, the range indicator and the coding options all pass, along with the existing mesh tests.
- Lint: no new errors, apart from missing translations for the new strings (only English and French are provided).
The branch is rebased on current main (2.0.3); the phone and Wear builds both pass. On Windows, testDebugUnitTest has 23 failures (SQLite / conversation database and related tests); they fail identically on main without this change.
Mesh Lab
Run on the same two phones with the rebased debug build, both in Max range / S=8. The device logs confirm the links between them were on Coded PHY (TX and RX) for the whole run.
| Scenario | Phone A → B | Phone B → A |
|---|---|---|
dm | ✅ pass | ✅ pass |
broadcast | ✅ pass | ✅ pass |
raw | ✅ pass | ✅ pass |
file | ✅ pass | — |
file_private | ✅ pass | ✅ pass |
media_private | ✅ pass | — |
file_oversize | ✅ pass | — |
ptt_dm | ✅ pass | — |
ptt_broadcast | ✅ pass | — |
Not run:
identity_reset,session_recoveryandfavorite_verification: they wipe or change app data on personal phones.mesh_lab.py setup: it wipes app data and needsadb install, which one of the phones blocks. The apps were installed and onboarded by hand instead.
Maintainers with a dedicated Mesh Lab pair should run scenario all.
Not verified yet: help wanted 🙏
The most important question is still open: does it actually extend range enough to be worth it? Coded S=8 gives roughly 12 dB more link budget, about 4× the free-space distance in theory. Phone antennas, bodies, walls and how each chip implements Coded will decide the real gain. The "~40–200 m" shown in the app is an estimate, not a measurement.
If you have two phones that support LE Coded PHY (check Debug settings → "Long range (LE Coded PHY)" → This phone), please try:
- Walk test, Off vs Adaptive vs Max range. Outdoors, with line of sight, phones held normally. Walk apart while sending a message every ~10 m. Note the distance at which messages stop arriving, for each mode on both phones.
- Indoors, the same test through walls or floors, if you can.
- Adaptive: walk away and come back. Check in the debug menu (Radio events) that the link switches to Coded before messages stop, and back to 1M without flapping. Does Adaptive reach the same distance as Max range?
- S=8 vs S=2 in Max range, if you have time.
- Mixed setup: a long-range phone with a phone that doesn't support Coded, or with iOS. Discovery and messaging should still work over standard BLE. With an unmodified app on Coded-capable hardware, check in the debug menu whether the connection moves to Coded, and how far it holds once connected.
Please report the phone models, Android versions, environment, and the distances for each mode. The debug menu shows which PHY each link was actually on. Please don't post Bluetooth addresses, peer IDs, nicknames, raw logcat or exact locations.
Known trade-off: Coded S=8 is 1/8 of the 1M data rate. Text isn't noticeably affected; image and voice transfers on a Coded link are proportionally slower. That's why Adaptive, which keeps short links on 1M, is the default.
Findings from the device tests:
- Advertisement RSSI isn't a usable proxy for link quality: between the same two phones it read 15–25 dB below link RSSI, because it depends on the remote's advertising power and power mode.
- PHY requests from the peripheral side (incoming connections) were sometimes ignored by the remote. Switching on those connections in practice comes from the side that opened the connection.
- A PHY preference sticks.
setPreferredPhyleaves the preference in the controller, and a PHY update needs a PHY both ends accept. An incoming side that had asked for Coded-only blocked the other side's later 1M request indefinitely. A stock client sets no preference, so its links were unaffected. Hence the explicit release described above.
Screenshots
No screenshots yet: the ones I took from real devices showed personal data. Before/after screenshots of the Mesh row and the debug section can be added from test devices.
Checklist
- I have read the contribution guidelines: https://github.com/permissionlesstech/bitchat-android?tab=readme-ov-file#contributing
- I have performed a self-review of my code
- I have mentioned the corresponding issue and the relevant keyword (e.g., "Closes: #xy") in the description (see https://docs.github.com/en/issues/tracking-your-work-with-issues/linking-a-pull-request-to-an-issue)
- If it is a core feature, I have added automated tests
🤖 Generated with Claude Code
Activity
4This pull request is a draft
Merging is unavailable until it is marked ready for review.