fix(train): the Loop set up, its clock in UTC as Polar's SDK parses it

doFirstTimeUse (SDK 8.3.0) parses the device time as
yyyy-MM-dd'T'HH:mm:ss'Z' and refused the offset and fractions of a second
the app sent (ISO_OFFSET_DATE_TIME), so no band was ever set up: "The band
didn't answer". The test's clock had neither; it has both now, and checks the
time against the SDK's pattern.

What fails in the Loop's bridge is logged now (tag Loop), as the app says
only that the band didn't answer.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
This commit is contained in:
Tizian 「ティツィアーン」 2026-11-01 00:37:53 +01:00
commit cb486af671
Signed by: raisondetredev
GPG key ID: 07F6FCF405488B64
3 changed files with 21 additions and 3 deletions

View file

@ -1,6 +1,7 @@
package app.halkyon.train package app.halkyon.train
import android.content.Context import android.content.Context
import android.util.Log
import com.polar.androidcommunications.api.ble.model.DisInfo import com.polar.androidcommunications.api.ble.model.DisInfo
import com.polar.androidcommunications.api.ble.model.gatt.client.ChargeState import com.polar.androidcommunications.api.ble.model.gatt.client.ChargeState
import com.polar.androidcommunications.api.ble.model.gatt.client.PowerSourcesState import com.polar.androidcommunications.api.ble.model.gatt.client.PowerSourcesState
@ -77,6 +78,7 @@ class LoopBridge(private val context: Context) : MethodChannel.MethodCallHandler
} catch (error: SecurityException) { } catch (error: SecurityException) {
sink.error("refused", error.message, null) sink.error("refused", error.message, null)
} catch (error: Exception) { } catch (error: Exception) {
Log.w("Loop", "The search failed", error)
sink.error("failed", error.message, null) sink.error("failed", error.message, null)
} }
} }
@ -161,6 +163,8 @@ class LoopBridge(private val context: Context) : MethodChannel.MethodCallHandler
} catch (error: SecurityException) { } catch (error: SecurityException) {
result.error("refused", error.message, null) result.error("refused", error.message, null)
} catch (error: Exception) { } catch (error: Exception) {
// The app says only that the band didn't answer; what failed is here.
Log.w("Loop", "${call.method} failed", error)
result.error("failed", error.message, null) result.error("failed", error.message, null)
} }
} }

View file

@ -2,8 +2,10 @@ package app.halkyon.train
import com.polar.sdk.api.model.PolarFirstTimeUseConfig import com.polar.sdk.api.model.PolarFirstTimeUseConfig
import java.time.LocalDate import java.time.LocalDate
import java.time.ZoneOffset
import java.time.ZonedDateTime import java.time.ZonedDateTime
import java.time.format.DateTimeFormatter import java.time.format.DateTimeFormatter
import java.util.Locale
/** /**
* The app's figures for a Loop's first-time use (ADR 0030, D34) as Polar's * The app's figures for a Loop's first-time use (ADR 0030, D34) as Polar's
@ -22,6 +24,9 @@ object LoopSetup {
"pro" to 60, "pro" to 60,
) )
/** The device time as Polar's SDK reads it. */
private val deviceTimeFormat = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss'Z'", Locale.ENGLISH)
/** Whether a device found under [name] is a Loop. */ /** Whether a device found under [name] is a Loop. */
fun isLoop(name: String?): Boolean = name != null && name.trim().startsWith("Polar Loop", ignoreCase = true) fun isLoop(name: String?): Boolean = name != null && name.trim().startsWith("Polar Loop", ignoreCase = true)
@ -43,7 +48,9 @@ object LoopSetup {
restingHeartRate = number("restingHeartRate").toInt(), restingHeartRate = number("restingHeartRate").toInt(),
trainingBackground = trainingBackground =
trainingBackgrounds[figures["trainingBackground"]] ?: throw IllegalArgumentException("trainingBackground missing"), trainingBackgrounds[figures["trainingBackground"]] ?: throw IllegalArgumentException("trainingBackground missing"),
deviceTime = now.format(DateTimeFormatter.ISO_OFFSET_DATE_TIME), // UTC to the second: doFirstTimeUse parses it as yyyy-MM-dd'T'HH:mm:ss'Z'
// (SDK 8.3.0), refusing an offset and fractions of a second.
deviceTime = now.withZoneSameInstant(ZoneOffset.UTC).format(deviceTimeFormat),
typicalDay = typicalDay =
when (figures["typicalDay"]) { when (figures["typicalDay"]) {
"mostlySitting" -> PolarFirstTimeUseConfig.TypicalDay.MOSTLY_SITTING "mostlySitting" -> PolarFirstTimeUseConfig.TypicalDay.MOSTLY_SITTING

View file

@ -2,8 +2,11 @@ package app.halkyon.train
import com.polar.sdk.api.model.PolarFirstTimeUseConfig import com.polar.sdk.api.model.PolarFirstTimeUseConfig
import java.time.LocalDate import java.time.LocalDate
import java.time.LocalDateTime
import java.time.ZoneOffset import java.time.ZoneOffset
import java.time.ZonedDateTime import java.time.ZonedDateTime
import java.time.format.DateTimeFormatter
import java.util.Locale
import org.junit.Assert.assertEquals import org.junit.Assert.assertEquals
import org.junit.Assert.assertFalse import org.junit.Assert.assertFalse
import org.junit.Assert.assertThrows import org.junit.Assert.assertThrows
@ -13,7 +16,8 @@ import org.junit.Test
// A Loop's first-time use from the app's figures (ADR 0030, D34): Polar's // A Loop's first-time use from the app's figures (ADR 0030, D34): Polar's
// names for the app's, and nothing sent that the SDK would refuse. // names for the app's, and nothing sent that the SDK would refuse.
class LoopSetupTest { class LoopSetupTest {
private val now = ZonedDateTime.of(2026, 10, 28, 18, 30, 0, 0, ZoneOffset.ofHours(1)) // Fractions of a second and an offset, as the phone's clock gives them.
private val now = ZonedDateTime.of(2026, 10, 28, 18, 30, 0, 792_484_000, ZoneOffset.ofHours(1))
private val figures = private val figures =
mapOf<String, Any?>( mapOf<String, Any?>(
@ -43,7 +47,10 @@ class LoopSetupTest {
assertEquals(50, config.trainingBackground) assertEquals(50, config.trainingBackground)
assertEquals(PolarFirstTimeUseConfig.TypicalDay.MOSTLY_STANDING, config.typicalDay) assertEquals(PolarFirstTimeUseConfig.TypicalDay.MOSTLY_STANDING, config.typicalDay)
assertEquals(480, config.sleepGoalMinutes) assertEquals(480, config.sleepGoalMinutes)
assertEquals("2026-10-28T18:30:00+01:00", config.deviceTime) // UTC to the second, as doFirstTimeUse parses it (SDK 8.3.0): it refused
// the offset and the fractions, and v0.1.0 never set a band up.
assertEquals("2026-10-28T17:30:00Z", config.deviceTime)
LocalDateTime.parse(config.deviceTime, DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss'Z'", Locale.ENGLISH))
} }
@Test @Test