diff --git a/android/app/src/main/java/com/noop/ble/WhoopBleClient.kt b/android/app/src/main/java/com/noop/ble/WhoopBleClient.kt
index e02f131249..0f4583d383 100644
--- a/android/app/src/main/java/com/noop/ble/WhoopBleClient.kt
+++ b/android/app/src/main/java/com/noop/ble/WhoopBleClient.kt
@@ -281,6 +281,11 @@ interface GattOps {
/** Request a GATT connection priority (battery, #477). Mirrors `BluetoothGatt`'s boolean contract;
* the stack no-ops a request equal to the current interval. */
fun requestConnectionPriorityCompat(priority: Int): Boolean
+
+ /** Ask the controller to prefer a PHY for this link (#533). Mirrors `BluetoothGatt.setPreferredPhy`,
+ * which is VOID and fire-and-forget: the real outcome arrives on `onPhyUpdate`, and the peer can
+ * decline. Masks (not single values) so the controller may fall back. API 26 = our minSdk. */
+ fun setPreferredPhyCompat(txPhy: Int, rxPhy: Int, phyOptions: Int)
}
/**
@@ -332,6 +337,8 @@ class RealGattOps(private val gatt: BluetoothGatt) : GattOps {
override fun readRemoteRssiCompat(): Boolean = gatt.readRemoteRssi()
override fun discoverServicesCompat(): Boolean = gatt.discoverServices()
override fun requestConnectionPriorityCompat(priority: Int): Boolean = gatt.requestConnectionPriority(priority)
+ override fun setPreferredPhyCompat(txPhy: Int, rxPhy: Int, phyOptions: Int) =
+ gatt.setPreferredPhy(txPhy, rxPhy, phyOptions)
}
class WhoopBleClient(
@@ -505,6 +512,34 @@ class WhoopBleClient(
fun releasesConnectionPriority(wasEnabled: Boolean, nowEnabled: Boolean): Boolean =
wasEnabled && !nowEnabled
+ /** #533: the PHY mask to ask the controller for. NOOP has never called `setPreferredPhy`, so every
+ * offload has run on the 1M PHY. LE 2M doubles the symbol rate, which for a bulk transfer means the
+ * SAME bytes spend HALF the air-time — unlike the connection-interval lever above it should cost
+ * LESS radio energy per byte, not more. The two are orthogonal and stack.
+ *
+ * Always a MASK INCLUDING 1M, never 2M alone: this is a preference, and leaving 1M in it lets the
+ * controller fall back rather than cling to a 2M link that has gone marginal (2M trades range for
+ * speed). Off → plain 1M, byte-for-byte today's link. The peer still has the final say, and
+ * `onPhyUpdate` reports what was actually negotiated.
+ *
+ * Android-only by necessity, exactly like [connectionPriorityFor]: CoreBluetooth exposes no
+ * app-side PHY API — Apple's stack negotiates the PHY itself and gives apps no say — so there is
+ * no Swift twin. A deliberate platform divergence, not a parity gap. (It also makes iOS/macOS a
+ * useful control: their link parameters are chosen for them, so a Mac draining a backlog faster
+ * than Android would show the strap is not the bottleneck.) */
+ fun preferredPhyMask(fastLinkEnabled: Boolean): Int =
+ if (fastLinkEnabled) BluetoothDevice.PHY_LE_1M_MASK or BluetoothDevice.PHY_LE_2M_MASK
+ else BluetoothDevice.PHY_LE_1M_MASK
+
+ /** Human-readable PHY for the strap log (#533). `onPhyUpdate` reports a PHY_LE_* VALUE (1/2/3),
+ * not the *_MASK constants used to request one — don't compare the two. */
+ fun phyLabel(phy: Int): String = when (phy) {
+ BluetoothDevice.PHY_LE_1M -> "1M"
+ BluetoothDevice.PHY_LE_2M -> "2M"
+ BluetoothDevice.PHY_LE_CODED -> "coded"
+ else -> "unknown($phy)"
+ }
+
/** Stretched periodic-offload interval while the STRAP is low on battery (#477). The offload tick
* is a PURE sync timer (the live-stream keep-alive is separate), so stretching it can't affect
* link health — worst case is data arriving in slightly larger batches; the strap banks
@@ -1250,6 +1285,57 @@ class WhoopBleClient(
}
}
+ /** #533 (EXPERIMENTAL, default off): ask for the LE 2M PHY around the historical offload. See
+ * [preferredPhyMask]. Requested at offload START rather than on connect ON PURPOSE: the connect
+ * handshake is fragile — an extra GATT op before `requestMtu` can make it return false, which skips
+ * the MTU bump and caps the very offload this is trying to speed up (#85/#50). The offload burst is
+ * where the throughput matters anyway, and PHY is a link-level setting that persists once negotiated. */
+ @Volatile private var fastLinkPhyEnabled: Boolean = false
+
+ /** Opt into the experimental LE 2M PHY preference (#533). No-op by default; applied at the next offload.
+ *
+ * Turning it OFF hands the link back to 1M rather than merely stopping future offloads from asking for
+ * 2M. A PHY PERSISTS once negotiated, so without this an already-2M link would stay 2M until the next
+ * reconnect — and this toggle's own copy tells the user to switch it off if syncing goes flaky at
+ * range, which is exactly the case where 2M is the suspect. Reuses [releasesConnectionPriority]'s
+ * edge rule, so the default path (re-applying `false` while already off, every launch) issues ZERO
+ * BLE ops. */
+ fun setFastLinkPhy(enabled: Boolean) {
+ val wasEnabled = fastLinkPhyEnabled
+ fastLinkPhyEnabled = enabled
+ if (releasesConnectionPriority(wasEnabled, enabled)) handler.post { releasePreferredPhy() }
+ }
+
+ /** #533: ask the controller to prefer 2M for this link (mask always includes 1M so it can fall back).
+ * Fire-and-forget — `setPreferredPhy` is void and the peer may decline; `onPhyUpdate` logs the PHY
+ * actually negotiated, which is also how we learn whether WHOOP supports 2M at all. Swallows throws
+ * like the connection-priority hint: a PHY preference must never tear the link down. No-op when off,
+ * so the default path issues ZERO extra BLE ops. */
+ private fun applyPreferredPhy() {
+ if (!fastLinkPhyEnabled) return
+ val ops = gattOps ?: return
+ val mask = preferredPhyMask(true)
+ try {
+ ops.setPreferredPhyCompat(mask, mask, BluetoothDevice.PHY_OPTION_NO_PREFERRED)
+ log("Offload: requested LE 2M PHY preference (#533)")
+ } catch (t: Throwable) {
+ log("preferred-PHY request failed (${t.javaClass.simpleName}); skipped")
+ }
+ }
+
+ /** #533: ask the controller back down to 1M when the experiment is switched off, undoing a 2M link
+ * still in force. Swallows throws like [applyPreferredPhy]. */
+ private fun releasePreferredPhy() {
+ val ops = gattOps ?: return
+ val mask = preferredPhyMask(false)
+ try {
+ ops.setPreferredPhyCompat(mask, mask, BluetoothDevice.PHY_OPTION_NO_PREFERRED)
+ log("Offload: released the LE 2M PHY preference — back to 1M (#533)")
+ } catch (t: Throwable) {
+ log("preferred-PHY release failed (${t.javaClass.simpleName}); skipped")
+ }
+ }
+
/** Battery-% at/below which the periodic offload cadence stretches to
* [LOW_BATTERY_BACKFILL_INTERVAL_MS] while discharging; 0 = never (normal 15-min cadence). DEFAULT
* OFF, so this ships dormant. The Settings picker offers 10/15/20/25/30. */
@@ -3466,6 +3552,15 @@ class WhoopBleClient(
kickServiceDiscovery(g, "mtu=$mtu")
}
+ /** #533: what the controller and the strap ACTUALLY settled on — the request is only a preference
+ * and the peer can decline, so this is the only way to know whether 2M took (and whether WHOOP
+ * supports it at all). Fires on any PHY change, including a fall back to 1M on a marginal link, so
+ * a before/after strap log shows the negotiated PHY next to the offload's records/sec.
+ * Log-only: nothing branches on the PHY. */
+ override fun onPhyUpdate(g: BluetoothGatt, txPhy: Int, rxPhy: Int, status: Int) {
+ log("PHY negotiated: tx=${phyLabel(txPhy)} rx=${phyLabel(rxPhy)} (status=$status)")
+ }
+
override fun onReadRemoteRssi(g: BluetoothGatt, rssi: Int, status: Int) {
// Signal strength at connect — diagnoses weak-link syncs (drops/busy storms/timeouts) that
// otherwise look mysterious in the log. Only on a clean read; a failure just stays silent.
@@ -4955,6 +5050,7 @@ class WhoopBleClient(
historicalKickSent = false
_state.update { it.copy(backfilling = true, syncChunksThisSession = 0) }
refreshConnectionPriority() // #477: escalate to HIGH for the offload burst (faster sync). No-op unless enabled.
+ applyPreferredPhy() // #533: prefer LE 2M for the burst (halves air-time). No-op unless enabled.
// Opt-in raw capture (research aid): pref read fresh per session, like the probes gate.
if (connectedFamily == DeviceFamily.WHOOP5 && PuffinExperiment.from(context).isCaptureEnabled) {
startWhoop5BackfillCapture()
diff --git a/android/app/src/main/java/com/noop/ui/AppViewModel.kt b/android/app/src/main/java/com/noop/ui/AppViewModel.kt
index 1548d955df..9ed0a49740 100644
--- a/android/app/src/main/java/com/noop/ui/AppViewModel.kt
+++ b/android/app/src/main/java/com/noop/ui/AppViewModel.kt
@@ -910,6 +910,10 @@ class AppViewModel(app: Application) : AndroidViewModel(app) {
enabled = NoopPrefs.fastHistorySync(appContext),
idleThrottleBatteryPct = 0,
)
+ // #533: the second, orthogonal sync-speed lever — prefer LE 2M around the offload burst. Also
+ // independent of the Power-saving master, and its own toggle so a field report can tell the two
+ // apart (they have opposite battery profiles). No-op unless on.
+ ble.setFastLinkPhy(NoopPrefs.fastLinkPhy(appContext))
ble.setLowBatteryOffloadThrottle(if (on) NoopPrefs.powerSavingBatteryPct(appContext) else 0)
// HRV pause is a sub-option: only effective while the master is on (defaults on when it is), and
// now battery-%-aware like the offload lever — pass the same threshold.
@@ -944,6 +948,13 @@ class AppViewModel(app: Application) : AndroidViewModel(app) {
applyPowerSaving()
}
+ /** Flip the experimental LE 2M PHY preference (#533). Persists + pushes it to the client; it applies
+ * at the next offload burst, and switching it off releases an already-2M link back to 1M. */
+ fun setFastLinkPhy(enabled: Boolean) {
+ NoopPrefs.setFastLinkPhy(appContext, enabled)
+ applyPowerSaving()
+ }
+
/** The effective continuous-HRV want: the user's "Continuous HRV capture" preference AND
* "Keep connected in the background" — the latter is what holds the link up for the stream to ride,
* so continuous capture is meaningless without it. */
diff --git a/android/app/src/main/java/com/noop/ui/MainActivity.kt b/android/app/src/main/java/com/noop/ui/MainActivity.kt
index 5606f78b97..1eb93003e0 100644
--- a/android/app/src/main/java/com/noop/ui/MainActivity.kt
+++ b/android/app/src/main/java/com/noop/ui/MainActivity.kt
@@ -261,6 +261,24 @@ object NoopPrefs {
of(context).edit().putBoolean(KEY_FAST_HISTORY_SYNC, enabled).apply()
}
+ /** EXPERIMENTAL (#533): prefer the LE 2M PHY around the historical offload. LE 2M doubles the symbol
+ * rate, so the same bytes spend half the air-time — it should cost LESS radio energy per byte, not
+ * more (unlike [KEY_FAST_HISTORY_SYNC]'s connection-interval lever). NOOP has never called
+ * setPreferredPhy, so every offload to date has run on 1M. Orthogonal to that lever; they stack, and
+ * they are separate toggles so a field report can attribute which one did what.
+ *
+ * DEFAULT OFF: it is a preference the strap may decline, 2M trades range for speed, and BLE behaviour
+ * can't be CI-tested — the negotiated PHY and the speedup both need real-strap field reports. The
+ * request always allows 1M too, so the controller can fall back. */
+ const val KEY_FAST_LINK_PHY = "noop.fastLinkPhy"
+
+ fun fastLinkPhy(context: Context): Boolean =
+ of(context).getBoolean(KEY_FAST_LINK_PHY, false)
+
+ fun setFastLinkPhy(context: Context, enabled: Boolean) {
+ of(context).edit().putBoolean(KEY_FAST_LINK_PHY, enabled).apply()
+ }
+
/** #836, the raw-HR fingerprint ("count:maxTs") the last COMPLETED idle rescore scored against. The
* 15-min backstop tick skips when the current fingerprint equals this; cleared implicitly by any HR
* insert/delete (the fingerprint moves). Mirrors the Swift `analyzeWatermark` UserDefaults key. */
diff --git a/android/app/src/main/java/com/noop/ui/SettingsScreen.kt b/android/app/src/main/java/com/noop/ui/SettingsScreen.kt
index ccae555bba..a4b4647caa 100644
--- a/android/app/src/main/java/com/noop/ui/SettingsScreen.kt
+++ b/android/app/src/main/java/com/noop/ui/SettingsScreen.kt
@@ -471,6 +471,7 @@ fun SettingsScreen(
// on. SharedPreferences isn't reactive, so the Switch mirrors into a local state.
var backgroundConnection by remember { mutableStateOf(NoopPrefs.backgroundConnection(context)) }
var fastHistorySync by remember { mutableStateOf(NoopPrefs.fastHistorySync(context)) }
+ var fastLinkPhy by remember { mutableStateOf(NoopPrefs.fastLinkPhy(context)) }
// "Continuous HRV capture" — hold the dense realtime stream armed 24/7 (better overnight HRV) at the
// cost of more battery. Default OFF; only does anything with background connection on. Local mirror.
@@ -1318,6 +1319,45 @@ fun SettingsScreen(
)
}
+ // "Faster Bluetooth link" (#533, EXPERIMENTAL): the other, orthogonal sync-speed lever —
+ // prefer the LE 2M PHY around the offload. Same bytes in half the airtime, so unlike the
+ // interval lever above it should cost LESS strap radio energy, not more. Separate toggle so
+ // a field report can attribute which lever did what. Off by default: the strap may decline
+ // it, 2M trades range for speed, and BLE behaviour can't be CI-tested. The negotiated PHY
+ // lands in the strap log (onPhyUpdate).
+ Row(
+ modifier = Modifier.fillMaxWidth(),
+ verticalAlignment = Alignment.CenterVertically,
+ horizontalArrangement = Arrangement.spacedBy(16.dp),
+ ) {
+ Column(modifier = Modifier.weight(1f)) {
+ Text(
+ stringResource(R.string.fast_link_phy),
+ style = NoopType.subhead,
+ color = Palette.textPrimary,
+ )
+ Text(
+ stringResource(R.string.fast_link_phy_desc),
+ style = NoopType.footnote,
+ color = Palette.textTertiary,
+ )
+ }
+ Switch(
+ checked = fastLinkPhy,
+ onCheckedChange = {
+ fastLinkPhy = it
+ vm.setFastLinkPhy(it)
+ },
+ colors = SwitchDefaults.colors(
+ checkedThumbColor = Palette.surfaceBase,
+ checkedTrackColor = Palette.accent,
+ uncheckedThumbColor = Palette.textSecondary,
+ uncheckedTrackColor = Palette.surfaceInset,
+ uncheckedBorderColor = Palette.hairline,
+ ),
+ )
+ }
+
// "Keep NOOP alive overnight" (#386): the battery-optimisation whitelist. Shown ONLY while
// background connection is on (meaningless otherwise), so it never adds noise on a
// foreground-only setup. `checked` reflects the LIVE system exempt state, so an already-exempt
diff --git a/android/app/src/main/res/values-de/strings.xml b/android/app/src/main/res/values-de/strings.xml
index 741265a55e..9fd2fedbb6 100644
--- a/android/app/src/main/res/values-de/strings.xml
+++ b/android/app/src/main/res/values-de/strings.xml
@@ -147,6 +147,8 @@
Andere Quellen übertragen keine rohen Sekundendaten auf das Gerät.
+ Schnellere Bluetooth-Verbindung (experimentell)
+ Fordert einen schnelleren Bluetooth-Funkmodus an, während Ihr Riemen seinen gespeicherten Verlauf überträgt. Dieselben Daten in kürzerer Sendezeit — möglicherweise schnellere Synchronisierung und etwas weniger Akkuverbrauch am Riemen. Ihr Riemen muss dies unterstützen, und es geht etwas Reichweite verloren. Neu und unerprobt: Schalten Sie es aus, wenn die Synchronisierung auf Distanz unzuverlässig wird.
Schnellere Verlaufssynchronisierung (experimentell)
Fordert eine schnellere Bluetooth-Verbindung an, während Ihr Riemen seinen gespeicherten Verlauf überträgt, damit ein großer Rückstand schneller aufholt. Nur während der Synchronisierung — Ihre Live-Herzfrequenz und der nächtliche Datenstrom bleiben unberührt. Neu und unerprobt: Wenn es nicht hilft oder zu viel Akku kostet, schalten Sie es wieder aus.
Energie sparen
diff --git a/android/app/src/main/res/values-es/strings.xml b/android/app/src/main/res/values-es/strings.xml
index 9e7b6730b1..eb20cf6e04 100644
--- a/android/app/src/main/res/values-es/strings.xml
+++ b/android/app/src/main/res/values-es/strings.xml
@@ -1591,6 +1591,8 @@
No %1$s aquí
Aún no se ha descargado nada para esta ventana.
Otras fuentes no descargan datos brutos por segundo en el dispositivo.
+ Conexión Bluetooth más rápida (experimental)
+ Solicita un modo de radio Bluetooth más rápido mientras su correa transfiere su historial almacenado. Los mismos datos en menos tiempo de emisión: puede sincronizar antes y consumir algo menos de batería de la correa. Su correa debe admitirlo, y se sacrifica algo de alcance. Nuevo y no probado: si la sincronización falla cuando el teléfono está lejos, vuelva a desactivarlo.
Sincronización de historial más rápida (experimental)
Solicita una conexión Bluetooth más rápida mientras su correa transfiere su historial almacenado, para que un gran retraso se ponga al día antes. Solo durante la sincronización: su frecuencia cardíaca en vivo y el flujo nocturno no se ven afectados. Nuevo y no probado: si no ayuda o consume demasiada batería, vuelva a desactivarlo.
Salvamento de energía
diff --git a/android/app/src/main/res/values-fr/strings.xml b/android/app/src/main/res/values-fr/strings.xml
index bc44a428c4..34bd758456 100644
--- a/android/app/src/main/res/values-fr/strings.xml
+++ b/android/app/src/main/res/values-fr/strings.xml
@@ -1591,6 +1591,8 @@
Pas de %1$s ici
Rien de déchargé pour cette fenêtre pour l\'instant.
D\'autres sources ne déchargent pas les données brutes par seconde sur les appareils.
+ Liaison Bluetooth plus rapide (expérimental)
+ Demande un mode radio Bluetooth plus rapide pendant que votre sangle transfère son historique stocké. Les mêmes données en moins de temps d\'antenne : la synchronisation peut être plus rapide et consommer un peu moins de batterie. Votre sangle doit le prendre en charge, et cela réduit un peu la portée. Nouveau et non éprouvé : si la synchronisation devient instable à distance, désactivez-le.
Synchronisation de l\'historique plus rapide (expérimental)
Demande une connexion Bluetooth plus rapide pendant que votre sangle transfère son historique stocké, afin qu\'un gros retard se rattrape plus vite. Uniquement pendant la synchronisation : votre fréquence cardiaque en direct et le flux nocturne ne sont pas affectés. Nouveau et non éprouvé : si cela n\'aide pas ou consomme trop de batterie, désactivez-le.
Économie d\'énergie
diff --git a/android/app/src/main/res/values/strings.xml b/android/app/src/main/res/values/strings.xml
index 71b4160b77..9be184c3d7 100644
--- a/android/app/src/main/res/values/strings.xml
+++ b/android/app/src/main/res/values/strings.xml
@@ -156,6 +156,8 @@
Other sources don\'t offload raw per-second data on-device.
+ Faster Bluetooth link (experimental)
+ Ask for a faster Bluetooth radio mode while your strap hands over its stored history. Sends the same data in less airtime, so it may sync sooner and use a little less strap battery — but your strap has to support it, and it trades some range for speed. New and unproven: if syncing gets flaky when your phone is further away, turn it back off.
Faster history sync (experimental)
Ask Android for a faster Bluetooth link while your strap hands over its stored history, so a big backlog catches up sooner. Only during the sync itself — your live heart rate and the overnight stream are untouched. New and unproven: if it doesn\'t help, or costs more battery than it\'s worth, turn it back off and say so on the issue tracker.
Power saving
diff --git a/android/app/src/test/java/com/noop/ble/PreferredPhyTest.kt b/android/app/src/test/java/com/noop/ble/PreferredPhyTest.kt
new file mode 100644
index 0000000000..0eb0b5935f
--- /dev/null
+++ b/android/app/src/test/java/com/noop/ble/PreferredPhyTest.kt
@@ -0,0 +1,68 @@
+package com.noop.ble
+
+import android.bluetooth.BluetoothDevice
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertTrue
+import org.junit.Test
+
+/**
+ * Pins [WhoopBleClient.preferredPhyMask] / [WhoopBleClient.phyLabel] — the pure LE 2M PHY decision (#533),
+ * unit-testable without a BLE stack (the [ConnectionPriorityTest] idiom).
+ *
+ * NOOP has never called `setPreferredPhy`, so every historical offload has run on the 1M PHY. LE 2M doubles
+ * the symbol rate: the same bytes spend HALF the air-time, which should cost LESS radio energy per byte —
+ * the opposite of the connection-interval lever, which buys speed with extra wakeups.
+ */
+class PreferredPhyTest {
+
+ @Test fun offIsPlain1M_todaysLink() {
+ // Default path must be byte-for-byte today: 1M only, no 2M preference expressed.
+ assertEquals(BluetoothDevice.PHY_LE_1M_MASK, WhoopBleClient.preferredPhyMask(fastLinkEnabled = false))
+ }
+
+ /**
+ * The request must ALWAYS keep 1M in the mask, never ask for 2M alone. It is only a preference, and
+ * leaving 1M in lets the controller fall back rather than cling to a 2M link gone marginal — 2M trades
+ * range for speed, and the strap may decline it outright.
+ */
+ @Test fun onAllowsBoth2MAnd1MFallback() {
+ val mask = WhoopBleClient.preferredPhyMask(fastLinkEnabled = true)
+ assertTrue("2M must be offered", mask and BluetoothDevice.PHY_LE_2M_MASK != 0)
+ assertTrue("1M must stay in the mask so the controller can fall back", mask and BluetoothDevice.PHY_LE_1M_MASK != 0)
+ }
+
+ /**
+ * Turning the experiment OFF must hand the link BACK to 1M, not merely stop future offloads asking for
+ * 2M: a PHY persists once negotiated, so an already-2M link would otherwise stay 2M until the next
+ * reconnect — and the toggle's copy tells the user to switch it off when syncing goes flaky at range,
+ * which is exactly when 2M is the suspect. The release requests the same mask the OFF state describes.
+ */
+ @Test fun theReleaseMaskIsTheOffMaskAndDropsThe2MOffer() {
+ val off = WhoopBleClient.preferredPhyMask(fastLinkEnabled = false)
+ assertEquals(BluetoothDevice.PHY_LE_1M_MASK, off)
+ assertEquals("off must not still offer 2M, or it wouldn't undo the escalation", 0, off and BluetoothDevice.PHY_LE_2M_MASK)
+ }
+
+ /** The on→off edge rule is shared with the connection-priority lever (#536): only a real on→off
+ * transition releases, so the default launch path (re-applying false while already off) is a no-op. */
+ @Test fun onlyTheOnToOffEdgeReleasesThePhy() {
+ assertTrue(WhoopBleClient.releasesConnectionPriority(wasEnabled = true, nowEnabled = false))
+ assertEquals(false, WhoopBleClient.releasesConnectionPriority(wasEnabled = false, nowEnabled = false))
+ assertEquals(false, WhoopBleClient.releasesConnectionPriority(wasEnabled = false, nowEnabled = true))
+ assertEquals(false, WhoopBleClient.releasesConnectionPriority(wasEnabled = true, nowEnabled = true))
+ }
+
+ /**
+ * `onPhyUpdate` reports a PHY_LE_* VALUE (1/2/3), NOT the *_MASK constants used to request one — the
+ * two numbering schemes overlap, so a label that compared against masks would misreport the link.
+ */
+ @Test fun labelsTheNegotiatedPhyValueNotTheMask() {
+ assertEquals("1M", WhoopBleClient.phyLabel(BluetoothDevice.PHY_LE_1M))
+ assertEquals("2M", WhoopBleClient.phyLabel(BluetoothDevice.PHY_LE_2M))
+ assertEquals("coded", WhoopBleClient.phyLabel(BluetoothDevice.PHY_LE_CODED))
+ }
+
+ @Test fun labelsAnUnexpectedPhyRatherThanHidingIt() {
+ assertEquals("unknown(9)", WhoopBleClient.phyLabel(9))
+ }
+}