feat: add OtaManager for BLE firmware update
This commit is contained in:
parent
2ab1c83655
commit
675504cbf5
1 changed files with 110 additions and 0 deletions
110
android/app/src/main/java/nl/eddydevink/sylda/OtaManager.kt
Normal file
110
android/app/src/main/java/nl/eddydevink/sylda/OtaManager.kt
Normal file
|
|
@ -0,0 +1,110 @@
|
||||||
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
package nl.eddydevink.sylda
|
||||||
|
|
||||||
|
import android.annotation.SuppressLint
|
||||||
|
import android.bluetooth.BluetoothGatt
|
||||||
|
import android.bluetooth.BluetoothGattCallback
|
||||||
|
import android.bluetooth.BluetoothGattCharacteristic
|
||||||
|
import android.content.Context
|
||||||
|
import android.util.Log
|
||||||
|
import kotlinx.coroutines.CompletableDeferred
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.delay
|
||||||
|
import kotlinx.coroutines.ensureActive
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
|
import kotlinx.coroutines.withTimeout
|
||||||
|
import java.nio.ByteBuffer
|
||||||
|
import java.nio.ByteOrder
|
||||||
|
import java.security.MessageDigest
|
||||||
|
|
||||||
|
class OtaManager(
|
||||||
|
private val context: Context,
|
||||||
|
private val relayManager: BleRelayManager
|
||||||
|
) {
|
||||||
|
companion object {
|
||||||
|
private const val TAG = "OtaManager"
|
||||||
|
private const val OTA_CHUNK_DATA_SIZE = 500
|
||||||
|
private const val OTA_CMD_START: Byte = 0x10
|
||||||
|
private const val OTA_CMD_END: Byte = 0x11
|
||||||
|
private const val OTA_STATUS_ACK: Byte = 0x00
|
||||||
|
private const val OTA_STATUS_COMPLETE: Byte = 0x10
|
||||||
|
private const val OTA_STATUS_ERROR: Byte = 0x11
|
||||||
|
private const val OTA_CHUNK_TIMEOUT_MS = 5000L
|
||||||
|
private const val OTA_START_TIMEOUT_MS = 10000L
|
||||||
|
}
|
||||||
|
|
||||||
|
interface OtaCallback {
|
||||||
|
fun onProgress(percent: Int, message: String)
|
||||||
|
fun onComplete()
|
||||||
|
fun onError(message: String)
|
||||||
|
}
|
||||||
|
|
||||||
|
private var firmwareData: ByteArray? = null
|
||||||
|
|
||||||
|
fun loadFirmware(): Boolean {
|
||||||
|
return try {
|
||||||
|
firmwareData = context.assets.open("firmware/firmware.bin").readBytes()
|
||||||
|
true
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e(TAG, "Failed to load firmware", e)
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun performOta(callback: OtaCallback): Boolean = withContext(Dispatchers.IO) {
|
||||||
|
val fw = firmwareData ?: run {
|
||||||
|
withContext(Dispatchers.Main) { callback.onError("Firmware niet geladen") }
|
||||||
|
return@withContext false
|
||||||
|
}
|
||||||
|
|
||||||
|
val sha256 = MessageDigest.getInstance("SHA-256").digest(fw)
|
||||||
|
|
||||||
|
try {
|
||||||
|
withContext(Dispatchers.Main) { callback.onProgress(0, "OTA starten...") }
|
||||||
|
|
||||||
|
val startPacket = ByteBuffer.allocate(5).order(ByteOrder.BIG_ENDIAN)
|
||||||
|
startPacket.put(OTA_CMD_START)
|
||||||
|
startPacket.putInt(fw.size)
|
||||||
|
relayManager.sendOtaData(startPacket.array())
|
||||||
|
|
||||||
|
withTimeout(OTA_START_TIMEOUT_MS) {
|
||||||
|
while (true) { delay(100); ensureActive() }
|
||||||
|
}
|
||||||
|
} catch (e: kotlinx.coroutines.TimeoutCancellationException) {
|
||||||
|
// timeout is expected — ESP doesn't ACK OTA_START, just begins
|
||||||
|
}
|
||||||
|
|
||||||
|
val totalChunks = (fw.size + OTA_CHUNK_DATA_SIZE - 1) / OTA_CHUNK_DATA_SIZE
|
||||||
|
var seq = 0
|
||||||
|
var offset = 0
|
||||||
|
|
||||||
|
while (offset < fw.size) {
|
||||||
|
val remaining = fw.size - offset
|
||||||
|
val chunkSize = minOf(remaining, OTA_CHUNK_DATA_SIZE)
|
||||||
|
|
||||||
|
val packet = ByteBuffer.allocate(4 + chunkSize).order(ByteOrder.BIG_ENDIAN)
|
||||||
|
packet.putInt(seq)
|
||||||
|
packet.put(fw, offset, chunkSize)
|
||||||
|
|
||||||
|
relayManager.sendOtaData(packet.array())
|
||||||
|
Log.d(TAG, "Sent chunk $seq ($chunkSize bytes)")
|
||||||
|
|
||||||
|
val pct = ((offset.toFloat() / fw.size.toFloat()) * 100).toInt().coerceAtMost(99)
|
||||||
|
withContext(Dispatchers.Main) {
|
||||||
|
callback.onProgress(pct, "OTA bezig: ${seq + 1}/$totalChunks")
|
||||||
|
}
|
||||||
|
|
||||||
|
offset += chunkSize
|
||||||
|
seq++
|
||||||
|
delay(50)
|
||||||
|
}
|
||||||
|
|
||||||
|
withContext(Dispatchers.Main) { callback.onProgress(99, "Hash verifiëren...") }
|
||||||
|
|
||||||
|
relayManager.sendOtaData(sha256)
|
||||||
|
|
||||||
|
withContext(Dispatchers.Main) { callback.onProgress(100, "Herstart...") }
|
||||||
|
withContext(Dispatchers.Main) { callback.onComplete() }
|
||||||
|
true
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in a new issue