feat: add OtaManager for BLE firmware update

This commit is contained in:
eddy 2026-07-30 17:14:27 +02:00
commit 675504cbf5

View 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
}
}