Skip to content

BLE Codec PHY for better range and obstacle penetration#873

Open
Ashoka74 wants to merge 2 commits into
torlando-tech:mainfrom
Ashoka74:claude/add-ble-codec-button-15xid
Open

BLE Codec PHY for better range and obstacle penetration#873
Ashoka74 wants to merge 2 commits into
torlando-tech:mainfrom
Ashoka74:claude/add-ble-codec-button-15xid

Conversation

@Ashoka74
Copy link
Copy Markdown

Add BLE PHY Codec Selector (PHY_1M / PHY_2M / CODED_S2 / CODED_S8)

Extends the AndroidBLE interface with a PHY codec preference that persists across app restarts and is applied at GATT connection time.


Why

Bluetooth 5 introduced LE Coded PHY, which trades throughput for range using forward error correction. Different deployments need different trade-offs: a dense urban mesh benefits from standard 1M PHY throughput, while disaster-response or rural scenarios need the ~4× range that S=8 provides. Previously there was no way to select PHY in the UI — the codec was always 1M regardless of the environment.


What Changed

Model & Persistence

  • BleCodec enum (PHY_1M, PHY_2M, CODED_S2, CODED_S8) in BlePowerSettings.kt with displayName and description strings used directly by the UI
  • bleCodec: String field on InterfaceConfig.AndroidBLE, defaulting to "PHY_1M"
  • Serialized as ble_codec in the interface JSON config; absent key deserializes to PHY_1M for backward compatibility

BLE Stack Wiring

  • BleGattClient.applyPreferredPhy() calls setPreferredPhy(txPhy, rxPhy, phyOptions) after GATT connection, guarded by Build.VERSION_CODES.O (API 26+) — BT4 devices skip it cleanly
  • KotlinBLEBridge.configureCodec(bleCodec) propagates the persisted selection to the GATT client before the interface starts
  • NativeInterfaceFactory.startBleInterface() calls configureCodec() so the preference is in place when the first connection fires applyPreferredPhy()

UI

  • Four-segment button row (1M / 2M / S=2 / S=8) in AndroidBLEFields
  • Selecting a segment updates bleCodec in the config state immediately
  • Codec description shown below the selector; S=8 adds a neutral-toned note explaining the counter-intuitive power behaviour

Why the S=8 Note Is Neutral, Not a Warning

LE Coded PHY S=8 runs at 125 Kbps with FEC. The FEC overhead costs roughly 70% more TX power per packet, but at link budgets where 1M PHY would need more than 2 retransmissions per packet, the avoided retransmissions more than compensate. At poor signal (rubble, thick walls, disaster response) net battery draw can be lower than 1M PHY. The note reflects this accurately rather than discouraging a mode that is net-beneficial in the primary use case.


Test Coverage

Suite Tests What's covered
BleCodecTest 17 fromString() for all values, case-insensitivity, unknown-name fallback, PHY constant mapping (mask + phyOptions), FEC presence in descriptions
InterfaceConfigExtTest 4 Serialization round-trips for ble_codec
InterfaceRepositoryTest 3 Deserialization including missing-key backward compatibility
InterfaceConfigDialogTest 8 Compose UI — header, all four button labels, per-codec descriptions, S=8 note present/absent

claude and others added 2 commits April 30, 2026 19:42
Expose BLE 5 PHY selection as a per-interface config option so relay nodes
can use Coded PHY for ~4× range through walls and rubble at reduced
throughput. Defaults to 1M PHY for full backward compatibility.

Components:
- BleCodec enum (BlePowerSettings.kt): PHY_1M, PHY_2M, CODED_S2, CODED_S8
  each with displayName and description; fromString() defaults to PHY_1M
- bleCodec field on InterfaceConfig.AndroidBLE (default "PHY_1M"),
  serialised as "ble_codec" JSON key; absent key deserialises to PHY_1M
- BleGattClient.preferredCodec var + applyPreferredPhy(): calls
  BluetoothGatt.setPreferredPhy() after GATT services are discovered;
  guarded on Build.VERSION_CODES.O — BT4 devices silently stay on 1M
- KotlinBLEBridge.configureCodec(): propagates codec from InterfaceConfig
  down to the active BleGattClient before connection
- AndroidBLEFields UI: four-segment button row (1M | 2M | S=2 | S=8)
  with per-selection description; S=8 shows an FEC note explaining the
  counter-intuitive power trade-off at poor link quality

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Call KotlinBLEBridge.configureCodec(config.bleCodec) in
  NativeInterfaceFactory.startBleInterface() so the PHY preference is
  applied before the first GATT connection attempt.

- Correct S=8 note: FEC eliminates retransmissions at poor signal
  quality, so net battery draw at bad link budgets (rubble, walls) can
  be lower than 1M PHY — not higher. Changed copy and color accordingly.
  Also add "FEC" to CODED_S2 / CODED_S8 descriptions in BleCodec enum
  and UI.

- Add BleCodecTest: fromString() resolution, PHY constant mapping,
  default / case-insensitive / unknown-value fallbacks, FEC description
  assertions.

- Add InterfaceConfigExtTest: AndroidBLE ble_codec serialisation
  round-trips for all four codec values.

- Add InterfaceRepositoryTest: AndroidBLE deserialises missing ble_codec
  key to PHY_1M (backward compat) and round-trips all four values.

- Add InterfaceConfigDialogTest: PHY Codec section renders, all four
  buttons present, 1M default description, S=8 FEC note present/absent.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps Bot commented Apr 30, 2026

Greptile Summary

This PR extends InterfaceConfig.AndroidBLE with a bleCodec field (PHY_1M / PHY_2M / CODED_S2 / CODED_S8), wires it from persistence through KotlinBLEBridge.configureCodec() to BleGattClient.applyPreferredPhy() on connection, and exposes a four-segment selector in the BLE settings UI. The BLE stack changes are well-structured with proper API-26 guards and exception handling; the main findings are a description-string divergence between the BleCodec enum and the hardcoded UI when block, and a misleading log in configureCodec when Bluetooth is unavailable.

Confidence Score: 4/5

Safe to merge; all findings are P2 quality/consistency issues with no runtime impact on the happy path.

The BLE stack wiring is correct, serialization is backward-compatible, and API-level guards are in place. The only issues are duplicate description strings that have drifted from the enum, a misleading log when Bluetooth hardware is absent, and a tautological test assertion — none of these affect runtime behavior on supported devices.

InterfaceConfigDialog.kt (hardcoded description strings diverge from BleCodec.description) and BleCodecTest.kt (tautological assertion).

Important Files Changed

Filename Overview
app/src/main/java/network/columba/app/ui/components/InterfaceConfigDialog.kt Adds four-segment PHY selector; hardcodes description strings that diverge from BleCodec.description for CODED_S2/S8.
reticulum/src/main/java/network/columba/app/reticulum/ble/bridge/KotlinBLEBridge.kt Adds configureCodec() — logs success even when gattClient is null (no Bluetooth hardware), producing a misleading log line.
reticulum/src/main/java/network/columba/app/reticulum/ble/client/BleGattClient.kt Adds preferredCodec field and applyPreferredPhy() with API-26 guard; PHY is applied after connection, which is the correct timing.
reticulum/src/test/java/network/columba/app/reticulum/ble/model/BleCodecTest.kt Good enum coverage, but one test contains a tautological assertion (PHY_1M == PHY_1M) that can never fail.
reticulum/src/main/java/network/columba/app/reticulum/ble/model/BlePowerSettings.kt Adds BleCodec enum with displayName, description, and fromString() fallback — clean, well-documented model.
reticulum/src/main/java/network/columba/app/reticulum/model/ReticulumConfig.kt Adds bleCodec: String = "PHY_1M" to InterfaceConfig.AndroidBLE with backward-compatible default.
reticulum/src/main/java/network/columba/app/reticulum/protocol/NativeInterfaceFactory.kt Calls configureCodec() before interface start so PHY preference is applied on the first GATT connection.
app/src/main/java/network/columba/app/repository/InterfaceRepository.kt Deserializes ble_codec with optString default "PHY_1M" — correct backward-compatible deserialization.

Sequence Diagram

sequenceDiagram
    participant UI as AndroidBLEFields (UI)
    participant VM as InterfaceManagementViewModel
    participant Repo as InterfaceRepository
    participant DB as InterfaceEntity (JSON)
    participant Factory as NativeInterfaceFactory
    participant Bridge as KotlinBLEBridge
    participant GATT as BleGattClient

    UI->>VM: onConfigUpdate(bleCodec = "CODED_S8")
    VM->>Repo: saveInterface(InterfaceConfig.AndroidBLE(bleCodec))
    Repo->>DB: toJsonString() → {"ble_codec":"CODED_S8"}

    Note over Factory,GATT: On interface start
    Factory->>Bridge: configureCodec("CODED_S8")
    Bridge->>GATT: preferredCodec = BleCodec.CODED_S8

    Note over GATT: On GATT connection (API 26+)
    GATT->>GATT: applyPreferredPhy()
    GATT->>GATT: setPreferredPhy(PHY_LE_CODED_MASK, PHY_LE_CODED_MASK, PHY_OPTION_S8)
Loading

Comments Outside Diff (3)

  1. app/src/main/java/network/columba/app/ui/components/InterfaceConfigDialog.kt, line 44-54 (link)

    P2 Description strings duplicated and diverge from the enum

    The when block hardcodes descriptions that have already drifted from BleCodec.description: the enum says "Coded PHY S=2 — 500 Kbps, ~2× range, FEC" and "Coded PHY S=8 — 125 Kbps, ~4× range, FEC (long range)", while the UI emits "... FEC (Bluetooth 5+)" for both. The tests in InterfaceConfigDialogTest pin the UI strings, so the divergence is invisible to CI. Using BleCodec.fromString(configState.bleCodec).description directly removes the duplication and makes future description changes automatically consistent.

    Prompt To Fix With AI
    This is a comment left during a code review.
    Path: app/src/main/java/network/columba/app/ui/components/InterfaceConfigDialog.kt
    Line: 44-54
    
    Comment:
    **Description strings duplicated and diverge from the enum**
    
    The `when` block hardcodes descriptions that have already drifted from `BleCodec.description`: the enum says `"Coded PHY S=2 — 500 Kbps, ~2× range, FEC"` and `"Coded PHY S=8 — 125 Kbps, ~4× range, FEC (long range)"`, while the UI emits `"... FEC (Bluetooth 5+)"` for both. The tests in `InterfaceConfigDialogTest` pin the UI strings, so the divergence is invisible to CI. Using `BleCodec.fromString(configState.bleCodec).description` directly removes the duplication and makes future description changes automatically consistent.
    
    
    
    How can I resolve this? If you propose a fix, please make it concise.
  2. reticulum/src/main/java/network/columba/app/reticulum/ble/bridge/KotlinBLEBridge.kt, line 323-327 (link)

    P2 Log fires even when codec assignment is silently dropped

    gattClient is a val initialized to null when bluetoothAdapter is null (no Bluetooth hardware). In that case gattClient?.preferredCodec = codec is a no-op, but the Log.i still prints "BLE codec configured: …", which could mislead diagnostics into thinking PHY is configured when it wasn't. A guard or a conditional log would be more accurate.

    Prompt To Fix With AI
    This is a comment left during a code review.
    Path: reticulum/src/main/java/network/columba/app/reticulum/ble/bridge/KotlinBLEBridge.kt
    Line: 323-327
    
    Comment:
    **Log fires even when codec assignment is silently dropped**
    
    `gattClient` is a `val` initialized to `null` when `bluetoothAdapter` is `null` (no Bluetooth hardware). In that case `gattClient?.preferredCodec = codec` is a no-op, but the `Log.i` still prints "BLE codec configured: …", which could mislead diagnostics into thinking PHY is configured when it wasn't. A guard or a conditional log would be more accurate.
    
    How can I resolve this? If you propose a fix, please make it concise.
  3. reticulum/src/test/java/network/columba/app/reticulum/ble/model/BleCodecTest.kt, line 630-637 (link)

    P2 Tautological assertion doesn't test anything

    val isDefault = BleCodec.PHY_1M == BleCodec.PHY_1M is always true by identity — this assertion can never fail regardless of any code change. The meaningful invariant being guarded (that PHY_1M is the enum default and thus skips setPreferredPhy) is already covered by BleGattClient.applyPreferredPhy's early-return check; consider either removing this test or asserting BleCodec.PHY_1M == BleCodec.fromString("") to actually exercise the default-fallback path.

    Prompt To Fix With AI
    This is a comment left during a code review.
    Path: reticulum/src/test/java/network/columba/app/reticulum/ble/model/BleCodecTest.kt
    Line: 630-637
    
    Comment:
    **Tautological assertion doesn't test anything**
    
    `val isDefault = BleCodec.PHY_1M == BleCodec.PHY_1M` is always `true` by identity — this assertion can never fail regardless of any code change. The meaningful invariant being guarded (that `PHY_1M` is the enum default and thus skips `setPreferredPhy`) is already covered by `BleGattClient.applyPreferredPhy`'s early-return check; consider either removing this test or asserting `BleCodec.PHY_1M == BleCodec.fromString("")` to actually exercise the default-fallback path.
    
    How can I resolve this? If you propose a fix, please make it concise.
Prompt To Fix All With AI
Fix the following 3 code review issues. Work through them one at a time, proposing concise fixes.

---

### Issue 1 of 3
app/src/main/java/network/columba/app/ui/components/InterfaceConfigDialog.kt:44-54
**Description strings duplicated and diverge from the enum**

The `when` block hardcodes descriptions that have already drifted from `BleCodec.description`: the enum says `"Coded PHY S=2 — 500 Kbps, ~2× range, FEC"` and `"Coded PHY S=8 — 125 Kbps, ~4× range, FEC (long range)"`, while the UI emits `"... FEC (Bluetooth 5+)"` for both. The tests in `InterfaceConfigDialogTest` pin the UI strings, so the divergence is invisible to CI. Using `BleCodec.fromString(configState.bleCodec).description` directly removes the duplication and makes future description changes automatically consistent.

```suggestion
    Text(
        BleCodec.fromString(configState.bleCodec).description,
        style = MaterialTheme.typography.bodySmall,
        color = MaterialTheme.colorScheme.onSurfaceVariant,
    )
```

### Issue 2 of 3
reticulum/src/main/java/network/columba/app/reticulum/ble/bridge/KotlinBLEBridge.kt:323-327
**Log fires even when codec assignment is silently dropped**

`gattClient` is a `val` initialized to `null` when `bluetoothAdapter` is `null` (no Bluetooth hardware). In that case `gattClient?.preferredCodec = codec` is a no-op, but the `Log.i` still prints "BLE codec configured: …", which could mislead diagnostics into thinking PHY is configured when it wasn't. A guard or a conditional log would be more accurate.

### Issue 3 of 3
reticulum/src/test/java/network/columba/app/reticulum/ble/model/BleCodecTest.kt:630-637
**Tautological assertion doesn't test anything**

`val isDefault = BleCodec.PHY_1M == BleCodec.PHY_1M` is always `true` by identity — this assertion can never fail regardless of any code change. The meaningful invariant being guarded (that `PHY_1M` is the enum default and thus skips `setPreferredPhy`) is already covered by `BleGattClient.applyPreferredPhy`'s early-return check; consider either removing this test or asserting `BleCodec.PHY_1M == BleCodec.fromString("")` to actually exercise the default-fallback path.

Reviews (1): Last reviewed commit: "test: add BLE codec tests and wire confi..." | Re-trigger Greptile

@Ashoka74 Ashoka74 changed the title Claude/add ble codec button 15xid Add BLE Codec PHY for better range and obstacle penetration Apr 30, 2026
@Ashoka74 Ashoka74 changed the title Add BLE Codec PHY for better range and obstacle penetration BLE Codec PHY for better range and obstacle penetration Apr 30, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants