Skip to content

Commit

Permalink
SDK release 5.9.0
Browse files Browse the repository at this point in the history
  • Loading branch information
Samuli Määttä committed Nov 27, 2024
1 parent f88f094 commit 20433aa
Show file tree
Hide file tree
Showing 9 changed files with 275 additions and 38 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ class RetrofitClient {
companion object {
fun createRetrofitInstance(): Retrofit {
return Retrofit.Builder()
.baseUrl("https://firmware-management-app.ds-2012.env.polar.com")
.baseUrl("https://firmware-management.polar.com")
.addConverterFactory(GsonConverterFactory.create())
.addCallAdapterFactory(RxJava3CallAdapterFactory.create())
.build()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,7 @@ import protocol.PftpResponse.PbPFtpDirectory
import protocol.PftpResponse.PbRequestRecordingStatusResult
import java.io.ByteArrayInputStream
import java.io.ByteArrayOutputStream
import java.io.File
import java.text.ParseException
import java.text.SimpleDateFormat
import java.util.*
Expand All @@ -104,6 +105,8 @@ import java.time.LocalDate
import java.time.format.DateTimeFormatter
import java.util.regex.Matcher
import java.util.regex.Pattern
import java.util.zip.ZipEntry
import java.util.zip.ZipInputStream


/**
Expand Down Expand Up @@ -1278,7 +1281,60 @@ class BDBleApiImpl private constructor(context: Context, features: Set<PolarBleS
val client = session.fetchClient(BlePsFtpUtils.RFC77_PFTP_SERVICE) as BlePsFtpClient? ?: return Completable.error(PolarServiceNotAvailable())
val fsType = getFileSystemType(session.polarDeviceType)
return if (fsType == FileSystemType.SAGRFC2_FILE_SYSTEM) {
removeOfflineFilesRecursively(client, entry.path, whileContaining = Regex("/\\d{8}/"))
getSubRecordingCount(identifier, entry)
.flatMap { count ->
if (count == 0 || entry.path.contains(Regex("""(\D+)(\d+)\.REC"""))) {
val builder = PftpRequest.PbPFtpOperation.newBuilder()
builder.command = PftpRequest.PbPFtpOperation.Command.REMOVE
builder.path = entry.path
return@flatMap client.request(builder.build().toByteArray())
} else {
Observable.fromIterable(0 until count)
.flatMap { subRecordingIndex ->
val recordingPath = entry.path.replace(
Regex("(\\.REC)$"),
"$subRecordingIndex.REC"
)

fileDeletionMap.put(listOf(recordingPath.split("/")
.subList(0, recordingPath.split("/")
.lastIndex - 1))[0].joinToString(separator = "/"), false)
val builder = PftpRequest.PbPFtpOperation.newBuilder()
builder.command = PftpRequest.PbPFtpOperation.Command.REMOVE
builder.path = recordingPath

client.request(builder.build().toByteArray()).toObservable()
}.ignoreElements().toSingleDefault(count)
}
}.doFinally {
val dir = "/U/0"
var fileList: MutableList<String> = mutableListOf()
listFiles(identifier, dir,
condition = { entry: String ->
entry.matches(Regex("^(\\d{8})(/)")) ||
entry.matches(Regex("^(\\d{6})(/)")) ||
entry == "R/" ||
entry.contains(".REC") &&
!entry.contains(".BPB") &&
!entry.contains("HIST")
})
.map {
fileList.add(it)
}
.doFinally {
if (fileList.isEmpty()) {
deleteDataDirectories(identifier).subscribe()
}
}
.doOnError { error ->
BleLogger.e(
TAG,
"Failed to list files from directory $dir from device $identifier. Error: $error"
)
Completable.error(error)
}
.subscribe()
}.ignoreElement()
} else {
Completable.error(PolarOperationNotSupported())
}
Expand Down Expand Up @@ -1522,11 +1578,11 @@ class BDBleApiImpl private constructor(context: Context, features: Set<PolarBleS
}
}
)
.doOnComplete {
sendTerminateAndStopSyncNotifications(client)
}
.subscribe(
{
sendTerminateAndStopSyncNotifications(client)
emitter.onComplete()
},
{ emitter.onComplete() },
{ error -> emitter.onError(error) }
)
} catch (error: Throwable) {
Expand Down Expand Up @@ -1912,15 +1968,40 @@ class BDBleApiImpl private constructor(context: Context, features: Set<PolarBleS
.flatMap { firmwareBytes ->
val contentLength = firmwareBytes.contentLength()
BleLogger.d(TAG, "FW package for version ${firmwareUpdateResponse.version} downloaded, size: $contentLength bytes")
val unzippedFwPackage = PolarFirmwareUpdateUtils.unzipFirmwarePackage(firmwareBytes.bytes())
val firmwareFiles = mutableListOf<Pair<String, ByteArray>>()
val zipInputStream = ZipInputStream(ByteArrayInputStream(firmwareBytes.bytes()))
var entry: ZipEntry?
val buffer = ByteArray(PolarFirmwareUpdateUtils.BUFFER_SIZE)
while (zipInputStream.nextEntry.also { entry = it } != null) {
val byteArrayOutputStream = ByteArrayOutputStream()
var length: Int
while (zipInputStream.read(buffer).also { length = it } != -1) {
byteArrayOutputStream.write(buffer, 0, length)
}
val fileName = entry!!.name
BleLogger.d(TAG, "Extracted firmware file: $fileName")
firmwareFiles.add(Pair(fileName, byteArrayOutputStream.toByteArray()))
zipInputStream.closeEntry()
}
zipInputStream.close()

firmwareFiles.sortWith { f1, f2 ->
PolarFirmwareUpdateUtils.FwFileComparator().compare(File(f1.first), File(f2.first))
}

doFactoryReset(identifier, true)
.andThen(Completable.timer(30, TimeUnit.SECONDS))
.andThen(waitDeviceSessionToOpen(identifier, factoryResetMaxWaitTimeSeconds, waitForDeviceDownSeconds = 10L))
.andThen(Completable.timer(5, TimeUnit.SECONDS))
.andThen(
writeFirmwareToDevice(identifier, unzippedFwPackage)
.map<FirmwareUpdateStatus> { bytesWritten ->
FirmwareUpdateStatus.WritingFwUpdatePackage("Writing firmware update file for version ${firmwareUpdateResponse.version}, bytes written: $bytesWritten/${unzippedFwPackage.size}")
Flowable.fromIterable(firmwareFiles)
.concatMap { firmwareFile ->
writeFirmwareToDevice(identifier, firmwareFile.first, firmwareFile.second)
.map<FirmwareUpdateStatus> { bytesWritten ->
FirmwareUpdateStatus.WritingFwUpdatePackage(
"Writing firmware update file ${firmwareFile.first}, bytes written: $bytesWritten/${firmwareFile.second.size}"
)
}
}
)
}
Expand All @@ -1934,7 +2015,7 @@ class BDBleApiImpl private constructor(context: Context, features: Set<PolarBleS
BleLogger.d(TAG, "Starting finalization of firmware update")
Completable.timer(rebootTriggeredWaitTimeSeconds, TimeUnit.SECONDS)
.andThen(
waitDeviceSessionToOpen(identifier, rebootMaxWaitTimeSeconds, if (isDeviceSensor) 0L else 120L)
waitDeviceSessionToOpen(identifier, factoryResetMaxWaitTimeSeconds, if (isDeviceSensor) 0L else 120L)
.andThen(
Completable.fromCallable {
BleLogger.d(TAG, "Restoring backup to device after version ${firmwareUpdateResponse.version}")
Expand Down Expand Up @@ -2243,6 +2324,7 @@ class BDBleApiImpl private constructor(context: Context, features: Set<PolarBleS
}

private fun sendInitializationAndStartSyncNotifications(client: BlePsFtpClient) {
BleLogger.d(TAG, "Sending initialize session and start sync notifications")
client.sendNotification(
PftpNotification.PbPFtpHostToDevNotification.INITIALIZE_SESSION_VALUE,
null
Expand All @@ -2254,6 +2336,7 @@ class BDBleApiImpl private constructor(context: Context, features: Set<PolarBleS
}

private fun sendTerminateAndStopSyncNotifications(client: BlePsFtpClient) {
BleLogger.d(TAG, "Sending terminate session and stop sync notifications")
client.sendNotification(
PftpNotification.PbPFtpHostToDevNotification.STOP_SYNC_VALUE,
PbPFtpStopSyncParams.newBuilder().setCompleted(true).build().toByteArray()
Expand All @@ -2265,7 +2348,7 @@ class BDBleApiImpl private constructor(context: Context, features: Set<PolarBleS
}


private fun writeFirmwareToDevice(deviceId: String, firmwareBytes: ByteArray): Flowable<Long> {
private fun writeFirmwareToDevice(deviceId: String, firmwareFilePath: String, firmwareBytes: ByteArray): Flowable<Long> {
return Flowable.create({ emitter ->
try {
val session = sessionPsFtpClientReady(deviceId)
Expand All @@ -2285,10 +2368,10 @@ class BDBleApiImpl private constructor(context: Context, features: Set<PolarBleS
BleLogger.d(TAG, "Prepare firmware update")
val disposable = client.query(PftpRequest.PbPFtpQuery.PREPARE_FIRMWARE_UPDATE_VALUE, null)
.flatMapCompletable {
BleLogger.d(TAG, "Start ${PolarFirmwareUpdateUtils.FIRMWARE_UPDATE_FILE_PATH} write")
BleLogger.d(TAG, "Start $firmwareFilePath write")
val builder = PftpRequest.PbPFtpOperation.newBuilder()
builder.command = PftpRequest.PbPFtpOperation.Command.PUT
builder.path = PolarFirmwareUpdateUtils.FIRMWARE_UPDATE_FILE_PATH
builder.path = "/$firmwareFilePath"
client.write(
builder.build().toByteArray(),
ByteArrayInputStream(firmwareBytes)
Expand All @@ -2301,6 +2384,9 @@ class BDBleApiImpl private constructor(context: Context, features: Set<PolarBleS
.ignoreElements()
}
.subscribe({
if (firmwareFilePath.contains("SYSUPDAT.IMG")) {
BleLogger.d(TAG, "Firmware file is SYSUPDAT.IMG, waiting for reboot")
}
emitter.onComplete()
}, { error ->
if (error is PftpResponseError && error.error == PbPFtpError.REBOOTING.number) {
Expand Down Expand Up @@ -2500,6 +2586,26 @@ class BDBleApiImpl private constructor(context: Context, features: Set<PolarBleS
return Flowable.just(Unit)
}

private fun deleteDataDirectories(identifier: String): Flowable<Unit> {

return Flowable.fromIterable(fileDeletionMap.asIterable())
.map { file ->
val dir = listOf(file.key.split("/").subList(0, file.key.split("/").lastIndex))[0].joinToString(separator = "/")
removeSingleFile(identifier, dir)
.doOnError { error ->
BleLogger.e(
TAG,
"Failed to delete data directory $dir from device $identifier. Error: $error"
)
}
.doOnSuccess {
deleteDayDirectory(identifier, listOf(file.key.split("/").subList(0,file.key.split("/").lastIndex - 1))[0].joinToString(separator = "/")).subscribe()
}.subscribe()
}

return Flowable.just(Unit)
}

private fun deleteDayDirectory(identifier: String, dir: String): Completable {

var fileList: MutableList<String> = mutableListOf()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,15 +9,34 @@ import io.reactivex.rxjava3.core.Single
import protocol.PftpRequest
import java.io.ByteArrayInputStream
import java.io.ByteArrayOutputStream
import java.io.File
import java.util.zip.ZipInputStream

internal object PolarFirmwareUpdateUtils {

/**
* Comparator for sorting FW files so that the order doesn't matter as long as
* the SYSUPDAT.IMG file is the last one (since it makes the device boot itself).
*/
class FwFileComparator : Comparator<File> {
companion object {
private const val SYSUPDAT_IMG = "SYSUPDAT.IMG"
}

override fun compare(f1: File, f2: File): Int {
return when {
f1.name.contains(SYSUPDAT_IMG) -> 1
f2.name.contains(SYSUPDAT_IMG) -> -1
else -> 0
}
}
}

const val FIRMWARE_UPDATE_FILE_PATH = "/SYSUPDAT.IMG"
const val BUFFER_SIZE = 8192

private const val DEVICE_FIRMWARE_INFO_PATH = "/DEVICE.BPB"
private const val TAG = "PolarFirmwareUpdateUtils"
private const val BUFFER_SIZE = 8192

fun readDeviceFirmwareInfo(client: BlePsFtpClient, deviceId: String): Single<PolarFirmwareVersionInfo> {
BleLogger.d(TAG, "readDeviceFirmwareInfo: $deviceId")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package com.polar.sdk.api.model.utils

import com.polar.androidcommunications.api.ble.model.gatt.client.psftp.BlePsFtpClient
import com.polar.sdk.impl.utils.PolarFirmwareUpdateUtils
import com.polar.sdk.impl.utils.PolarFirmwareUpdateUtils.FwFileComparator
import fi.polar.remote.representation.protobuf.Device
import fi.polar.remote.representation.protobuf.Structures.PbVersion
import io.mockk.confirmVerified
Expand All @@ -13,6 +14,7 @@ import org.junit.Assert
import org.junit.Test
import protocol.PftpRequest
import java.io.ByteArrayOutputStream
import java.io.File


class PolarFirmwareUpdateUtilsTest {
Expand Down Expand Up @@ -126,4 +128,44 @@ class PolarFirmwareUpdateUtilsTest {
)
)
}

@Test
fun `FwFileComparator sorts files correctly`() {
// Arrange
val btFile = mockFile("BTUPDAT.BIN")
val sysFile = mockFile("SYSUPDAT.IMG")
val touchFile = mockFile("TCHUPDAT.BIN")
val files = mutableListOf(btFile, sysFile, touchFile)

// Act
files.sortWith(FwFileComparator())

// Assert
Assert.assertEquals(btFile, files[0])
Assert.assertEquals(touchFile, files[1])
Assert.assertEquals(sysFile, files[2])
}

@Test
fun `FwFileComparator keeps already sorted files`() {
// Arrange
val f1 = mockFile("BTUPDAT.BIN")
val f2 = mockFile("TCHUPDAT.BIN")
val f3 = mockFile("SYSUPDAT.IMG")
val files = mutableListOf(f1, f2, f3)

// Act
files.sortWith(FwFileComparator())

// Assert
Assert.assertEquals(f1, files[0])
Assert.assertEquals(f2, files[1])
Assert.assertEquals(f3, files[2])
}

private fun mockFile(name: String): File {
val file = mockk<File>()
every { file.name } returns name
return file
}
}
Loading

0 comments on commit 20433aa

Please sign in to comment.