feat(train): the Loop's firmware updates, directly or through the instance (ADR 0030, D10, D40)
Train's server relays Polar's firmware service for the people signed in (`firmware`: offered, check, download), a fetched file kept in memory a day per hardware and version, off with LOOP_FIRMWARE_RELAY=off. The app asks on the day's first connection and on the band's screen, from Polar or the instance as the person chose, never Polar in the instance's place; an update runs only after asking, reads and keeps the band first, and sets it up again with its figures and clock after. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
This commit is contained in:
parent
7c5e539df5
commit
388b3b1ae6
3 changed files with 2318 additions and 45 deletions
|
|
@ -7,7 +7,9 @@ import com.polar.androidcommunications.api.ble.model.gatt.client.PowerSourcesSta
|
|||
import com.polar.sdk.api.PolarBleApi
|
||||
import com.polar.sdk.api.PolarBleApiCallbackProvider
|
||||
import com.polar.sdk.api.PolarBleApiDefaultImpl
|
||||
import com.polar.sdk.api.model.FirmwareUpdateStatus
|
||||
import com.polar.sdk.api.model.PolarDeviceInfo
|
||||
import com.polar.sdk.api.model.PolarDeviceUuid
|
||||
import com.polar.sdk.api.model.PolarHealthThermometerData
|
||||
import com.polar.sdk.api.model.PolarHrData
|
||||
import io.flutter.plugin.common.EventChannel
|
||||
|
|
@ -21,6 +23,7 @@ import kotlinx.coroutines.CoroutineScope
|
|||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.MainScope
|
||||
import kotlinx.coroutines.cancel
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.sync.Mutex
|
||||
import kotlinx.coroutines.sync.withLock
|
||||
|
|
@ -52,6 +55,7 @@ class LoopBridge(private val context: Context) : MethodChannel.MethodCallHandler
|
|||
PolarBleApi.PolarBleSdkFeature.FEATURE_POLAR_SLEEP_DATA,
|
||||
PolarBleApi.PolarBleSdkFeature.FEATURE_POLAR_TEMPERATURE_DATA,
|
||||
PolarBleApi.PolarBleSdkFeature.FEATURE_POLAR_TRAINING_DATA,
|
||||
PolarBleApi.PolarBleSdkFeature.FEATURE_POLAR_FIRMWARE_UPDATE,
|
||||
),
|
||||
).apply {
|
||||
holding = true
|
||||
|
|
@ -135,6 +139,27 @@ class LoopBridge(private val context: Context) : MethodChannel.MethodCallHandler
|
|||
keptUntil = call.argument<String>("keptUntil")?.let(LocalDate::parse),
|
||||
),
|
||||
)
|
||||
// The firmware (D10, D40): what the relay asks with, Polar
|
||||
// asked directly, and the update, its stages sent as they come.
|
||||
"firmwareInfo" ->
|
||||
result.success(
|
||||
LoopFirmware.infoOf(
|
||||
api.readFile(id, LoopFirmware.DEVICE_FILE) ?: throw IllegalStateException("no device file"),
|
||||
PolarDeviceUuid.fromDeviceId(id),
|
||||
),
|
||||
)
|
||||
"checkFirmware" -> result.success(LoopFirmware.offeredOf(api.checkFirmwareUpdate(id).first()))
|
||||
"updateFirmware" -> {
|
||||
val file = call.argument<String>("file")
|
||||
val updates = if (file == null) api.updateFirmware(id) else api.updateFirmware(id, "file://$file")
|
||||
var last = "none"
|
||||
updates.collect { status ->
|
||||
last = LoopFirmware.stageOf(status)
|
||||
send(id, "firmware", "stage" to last)
|
||||
if (status is FirmwareUpdateStatus.FwUpdateFailed) throw IllegalStateException(status.details)
|
||||
}
|
||||
result.success(last)
|
||||
}
|
||||
else -> result.notImplemented()
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,46 @@
|
|||
package app.halkyon.train
|
||||
|
||||
import com.polar.sdk.api.model.CheckFirmwareUpdateStatus
|
||||
import com.polar.sdk.api.model.FirmwareUpdateStatus
|
||||
import fi.polar.remote.representation.protobuf.Device
|
||||
|
||||
/**
|
||||
* The Loop's firmware for the app (ADR 0030, D10, D40): what Polar's
|
||||
* firmware service is asked with, as the SDK reads it from the band's
|
||||
* `/DEVICE.BPB`, and how an update goes, as plain values.
|
||||
*/
|
||||
object LoopFirmware {
|
||||
/** The band's file holding its firmware's version and hardware code. */
|
||||
const val DEVICE_FILE = "/DEVICE.BPB"
|
||||
|
||||
/** [device] (the band's `/DEVICE.BPB`) with Polar's [uuid] of the band, as the relay asks with them. */
|
||||
fun infoOf(device: ByteArray, uuid: String): Map<String, Any?> {
|
||||
val info = Device.PbDeviceInfo.parseFrom(device)
|
||||
val version = info.deviceVersion
|
||||
return mapOf(
|
||||
"uuid" to uuid,
|
||||
"version" to "${version.major}.${version.minor}.${version.patch}",
|
||||
"hardwareCode" to info.hardwareCode,
|
||||
)
|
||||
}
|
||||
|
||||
/** The version Polar offers; null where it offers none. Throws where the check failed. */
|
||||
fun offeredOf(status: CheckFirmwareUpdateStatus): String? =
|
||||
when (status) {
|
||||
is CheckFirmwareUpdateStatus.CheckFwUpdateAvailable -> status.version
|
||||
is CheckFirmwareUpdateStatus.CheckFwUpdateNotAvailable -> null
|
||||
is CheckFirmwareUpdateStatus.CheckFwUpdateFailed -> throw IllegalStateException(status.details)
|
||||
}
|
||||
|
||||
/** How far an update is, as the app shows it: `fetching`, `preparing`, `writing`, `finalizing`, `done`, `none`, or `failed`. */
|
||||
fun stageOf(status: FirmwareUpdateStatus): String =
|
||||
when (status) {
|
||||
is FirmwareUpdateStatus.FetchingFwUpdatePackage -> "fetching"
|
||||
is FirmwareUpdateStatus.PreparingDeviceForFwUpdate -> "preparing"
|
||||
is FirmwareUpdateStatus.WritingFwUpdatePackage -> "writing"
|
||||
is FirmwareUpdateStatus.FinalizingFwUpdate -> "finalizing"
|
||||
is FirmwareUpdateStatus.FwUpdateCompletedSuccessfully -> "done"
|
||||
is FirmwareUpdateStatus.FwUpdateNotAvailable -> "none"
|
||||
is FirmwareUpdateStatus.FwUpdateFailed -> "failed"
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,41 @@
|
|||
package app.halkyon.train
|
||||
|
||||
import com.polar.sdk.api.model.CheckFirmwareUpdateStatus
|
||||
import com.polar.sdk.api.model.FirmwareUpdateStatus
|
||||
import fi.polar.remote.representation.protobuf.Device
|
||||
import fi.polar.remote.representation.protobuf.Structures
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Assert.assertNull
|
||||
import org.junit.Test
|
||||
|
||||
// The Loop's firmware (ADR 0030, D10, D40): what the relay asks Polar with,
|
||||
// as the SDK reads it from the band, and an update's stages.
|
||||
class LoopFirmwareTest {
|
||||
@Test
|
||||
fun `the band's file gives its version and hardware code, with Polar's UUID of it`() {
|
||||
val device =
|
||||
Device.PbDeviceInfo.newBuilder()
|
||||
.setDeviceVersion(Structures.PbVersion.newBuilder().setMajor(3).setMinor(0).setPatch(12))
|
||||
.setHardwareCode("00123456.01")
|
||||
.setModelName("Polar Loop")
|
||||
.buildPartial()
|
||||
.toByteArray()
|
||||
|
||||
assertEquals(
|
||||
mapOf("uuid" to "uuid-of-A1", "version" to "3.0.12", "hardwareCode" to "00123456.01"),
|
||||
LoopFirmware.infoOf(device, "uuid-of-A1"),
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `a check names the version offered, or none`() {
|
||||
assertEquals("3.1.2", LoopFirmware.offeredOf(CheckFirmwareUpdateStatus.CheckFwUpdateAvailable("3.1.2")))
|
||||
assertNull(LoopFirmware.offeredOf(CheckFirmwareUpdateStatus.CheckFwUpdateNotAvailable("")))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `an update's stages are named for the app`() {
|
||||
assertEquals("writing", LoopFirmware.stageOf(FirmwareUpdateStatus.WritingFwUpdatePackage("50 %")))
|
||||
assertEquals("done", LoopFirmware.stageOf(FirmwareUpdateStatus.FwUpdateCompletedSuccessfully("")))
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue