16 KiB
OTA Improvements — Implementatieplan
For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (
- [ ]) syntax for tracking.
Goal: Zes code review aanbevelingen implementeren: SHA256 verificatie op ESP, OTA notification handling op Android, dynamische chunk size, writeCharacteristic return check, otaStatusText verwijderen.
Architecture: ESP stuurt al ACK/NAK/COMPLETE/ERROR notificaties — Android moet ze alleen nog verwerken. Chunk size wordt dynamisch afgeleid van onderhandelde MTU (512 via setMTU). SHA256 verificatie voorkomt dat corrupte firmware de boot partitie overschrijft.
Tech Stack: C++ (ESP32, NimBLE, esp_ota_ops, mbedtls), Kotlin (Android, BLE GATT, Coroutines)
Verify: Na elke task: compilatie (pio run voor ESP, ./gradlew assembleDebug voor Android). Geen automated tests — handmatig flashen naar ESP en OTA testen na alle tasks.
Task 1: ESP SHA256 verificatie
Files:
- Modify:
src/main.cpp:192-213
De ESP ontvangt al de SHA256 hash als laatste chunk (32 bytes, opgeslagen in otaExpectedSha) en roept esp_ota_end() aan. Wat ontbreekt: esp_ota_get_app_elf_sha256() + memcmp() vóór esp_ota_set_boot_partition().
- Step 1: Vervang CMD_END handler met SHA256 verificatie
In src/main.cpp, vervang de bestaande 32-byte handler (regels 192-213):
if (len == 32 && otaInProgress) {
memcpy(otaExpectedSha, data, 32);
esp_err_t err = esp_ota_end(otaHandle);
if (err != ESP_OK) {
Serial.printf("OTA: end failed (%d)\n", err);
otaReset();
sendOtaNotify(pChar, OTA_STATUS_ERROR, 0);
return;
}
uint8_t computedSha[32];
err = esp_ota_get_app_elf_sha256(otaHandle, (char *)computedSha, sizeof(computedSha));
if (err != ESP_OK) {
Serial.printf("OTA: SHA256 read failed (%d)\n", err);
otaReset();
sendOtaNotify(pChar, OTA_STATUS_ERROR, 0);
return;
}
if (memcmp(computedSha, otaExpectedSha, 32) != 0) {
Serial.println("OTA: SHA256 mismatch — aborting");
otaReset();
sendOtaNotify(pChar, OTA_STATUS_ERROR, 0);
return;
}
err = esp_ota_set_boot_partition(otaPartition);
if (err != ESP_OK) {
Serial.printf("OTA: set boot partition failed (%d)\n", err);
otaReset();
sendOtaNotify(pChar, OTA_STATUS_ERROR, 0);
return;
}
digitalWrite(LED_PIN, HIGH);
sendOtaNotify(pChar, OTA_STATUS_COMPLETE, 0);
Serial.println("OTA: SHA256 OK, rebooting...");
delay(500);
esp_restart();
return;
}
De wijziging: na esp_ota_end() wordt esp_ota_get_app_elf_sha256() aangeroepen en wordt memcmp gedaan. Bij mismatch: ERROR notificatie + otaReset() zonder boot partitie te wijzigen.
- Step 2: Compileer ESP firmware
pio run
- Step 3: Commit
git add src/main.cpp
git commit -m "feat: add SHA256 verification before esp_ota_set_boot_partition"
Task 2: Android OTA notification handling
Files:
- Modify:
android/app/src/main/java/nl/eddydevink/sylda/BleRelayManager.kt - Modify:
android/app/src/main/java/nl/eddydevink/sylda/OtaManager.kt
ESP stuurt 5-byte notificaties [status:1][seq:4_BE] met status 0x00=ACK, 0x01=NAK, 0x10=COMPLETE, 0x11=ERROR. Android moet deze verwerken met retry-logica.
- Step 1: Callback interface toevoegen voor OTA notificaties
In BleRelayManager.kt, voeg een callback interface toe voor OTA notificaties rond de BleCallbacks interface (regel 64):
In BleRelayManager.kt, voeg toe na de bestaande BleCallbacks interface:
interface OtaNotifyCallback {
fun onOtaNotify(status: Int, seq: Int)
}
private var otaNotifyCallback: OtaNotifyCallback? = null
fun setOtaNotifyCallback(callback: OtaNotifyCallback?) {
otaNotifyCallback = callback
}
- Step 2:
onCharacteristicChanged()toevoegen aanGattCallback
In BleRelayManager.kt, voeg onCharacteristicChanged() toe in de GattCallback class, na onCharacteristicWrite() (regel 327):
@SuppressLint("MissingPermission")
override fun onCharacteristicChanged(
gatt: BluetoothGatt,
characteristic: BluetoothGattCharacteristic,
value: ByteArray
) {
if (characteristic.uuid == OTA_UUID && value.size >= 1) {
val status = value[0].toInt() and 0xFF
val seq = if (value.size >= 5) {
((value[1].toInt() and 0xFF) shl 24) or
((value[2].toInt() and 0xFF) shl 16) or
((value[3].toInt() and 0xFF) shl 8) or
(value[4].toInt() and 0xFF)
} else 0
Log.d(TAG, "OTA notify: status=$status seq=$seq")
handler.post { otaNotifyCallback?.onOtaNotify(status, seq) }
}
}
- Step 3: Retry-logica in
OtaManager.kt
Herschrijf performOta() in OtaManager.kt (regels 54-109) om te wachten op ACK/NAK per chunk:
Replace de hele performOta() method:
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)
var chunkAckReceived = false
var chunkNakReceived = false
var finalStatusReceived = false
var finalStatus = -1
val notifyCallback = object : BleRelayManager.OtaNotifyCallback {
override fun onOtaNotify(status: Int, seq: Int) {
when (status) {
0x00 -> chunkAckReceived = true
0x01 -> chunkNakReceived = true
0x10 -> { finalStatusReceived = true; finalStatus = 0x10 }
0x11 -> { finalStatusReceived = true; finalStatus = 0x11 }
}
}
}
relayManager.setOtaNotifyCallback(notifyCallback)
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())
try {
withTimeout(OTA_START_TIMEOUT_MS) {
while (true) { delay(100); ensureActive() }
}
} catch (_: kotlinx.coroutines.TimeoutCancellationException) {}
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)
chunkAckReceived = false
chunkNakReceived = false
var retries = 0
while (retries < 3) {
relayManager.sendOtaData(packet.array())
Log.d(TAG, "Sent chunk $seq/$totalChunks ($chunkSize bytes) attempt ${retries + 1}")
try {
withTimeout(2000L) {
while (!chunkAckReceived && !chunkNakReceived) {
delay(50)
ensureActive()
}
}
} catch (_: kotlinx.coroutines.TimeoutCancellationException) {
Log.w(TAG, "Timeout waiting for ACK/NAK on chunk $seq")
}
if (chunkAckReceived) break
retries++
Log.w(TAG, "Chunk $seq NAK/timeout, retry $retries/3")
}
if (!chunkAckReceived) {
relayManager.setOtaNotifyCallback(null)
withContext(Dispatchers.Main) { callback.onError("OTA mislukt: chunk $seq geen ACK na 3 pogingen") }
return@withContext false
}
val pct = ((offset.toFloat() / fw.size.toFloat()) * 99).toInt().coerceAtMost(99)
withContext(Dispatchers.Main) {
callback.onProgress(pct, "OTA bezig: ${seq + 1}/$totalChunks")
}
offset += chunkSize
seq++
}
withContext(Dispatchers.Main) { callback.onProgress(99, "Hash verifiëren...") }
finalStatusReceived = false
relayManager.sendOtaData(sha256)
try {
withTimeout(10000L) {
while (!finalStatusReceived) {
delay(100)
ensureActive()
}
}
} catch (_: kotlinx.coroutines.TimeoutCancellationException) {
Log.w(TAG, "Timeout waiting for final OTA status")
}
relayManager.setOtaNotifyCallback(null)
if (finalStatus == 0x10) {
withContext(Dispatchers.Main) {
callback.onProgress(100, "Herstart...")
callback.onComplete()
}
true
} else {
withContext(Dispatchers.Main) {
callback.onError("OTA mislukt: ESP SHA256 verificatie fout")
}
false
}
} catch (e: Exception) {
relayManager.setOtaNotifyCallback(null)
withContext(Dispatchers.Main) { callback.onError("OTA fout: ${e.message}") }
false
}
}
- Step 4: Compileer Android app
./gradlew assembleDebug
- Step 5: Commit
git add android/app/src/main/java/nl/eddydevink/sylda/BleRelayManager.kt android/app/src/main/java/nl/eddydevink/sylda/OtaManager.kt
git commit -m "feat: OTA notification handling with ACK/NAK retry logic"
Task 3: ESP setMTU(512) + Android dynamische chunk size
Files:
-
Modify:
src/main.cpp(BLE setup, rond regel 270) -
Modify:
android/app/src/main/java/nl/eddydevink/sylda/BleRelayManager.kt -
Modify:
android/app/src/main/java/nl/eddydevink/sylda/OtaManager.kt -
Step 1: ESP — zet MTU naar 512
In src/main.cpp, in de setup() functie, voeg NimBLEDevice::setMTU(512) toe vóór de BLE initialisatie. Zoek NimBLEDevice::init (rond regel 275) en voeg de regel ervoor toe:
NimBLEDevice::setMTU(512);
NimBLEDevice::init(deviceName);
- Step 2: ESP — compileer
pio run
- Step 3: Android — MTU opslaan en doorgeven
In BleRelayManager.kt, onMtuChanged() (regel 265), sla de MTU op:
Voeg een field toe bij de andere fields (rond regel 59):
private var negotiatedMtu: Int = 23
In onMtuChanged():
override fun onMtuChanged(gatt: android.bluetooth.BluetoothGatt, mtu: Int, status: Int) {
Log.d(TAG, "MTU changed to $mtu")
negotiatedMtu = mtu
val service = gatt.getService(SERVICE_UUID)
...
Voeg getter toe (na getEspVersion(), regel 220):
fun getNegotiatedMtu(): Int = negotiatedMtu
- Step 4: Android — dynamische chunk size in
OtaManager.kt
Vervang OTA_CHUNK_DATA_SIZE constant (regel 26) door een dynamische property:
private val OTA_CHUNK_DATA_SIZE: Int
get() {
val mtu = instance?.relayManager?.getNegotiatedMtu() ?: 23
return minOf(maxOf(mtu - 3 - 4, 20), OTA_ESP_MAX_CHUNK)
}
private const val OTA_ESP_MAX_CHUNK = 500
Maar instance?.relayManager werkt niet direct in companion. Maak OtaManager non-companion eigenschap of sla de chunk size op bij constructie:
In OtaManager class body (na companion object, voor loadFirmware):
private var chunkDataSize: Int = 248
init {
chunkDataSize = calculateChunkSize(relayManager.getNegotiatedMtu())
}
private fun calculateChunkSize(mtu: Int): Int {
return minOf(maxOf(mtu - 3 - 4, 20), 500)
}
Vervang alle referenties naar OTA_CHUNK_DATA_SIZE in performOta() naar chunkDataSize (komt 3x voor: regel 77, 83, en een impliciete in de chunkSize berekening).
- Step 5: Android — compileer
./gradlew assembleDebug
- Step 6: Commit
git add src/main.cpp android/app/src/main/java/nl/eddydevink/sylda/BleRelayManager.kt android/app/src/main/java/nl/eddydevink/sylda/OtaManager.kt
git commit -m "feat: ESP MTU 512 + dynamic Android chunk size"
Task 4: writeCharacteristic return check
Files:
-
Modify:
android/app/src/main/java/nl/eddydevink/sylda/BleRelayManager.kt:186-192 -
Modify:
android/app/src/main/java/nl/eddydevink/sylda/OtaManager.kt -
Step 1:
sendOtaData()return waarde toevoegen
In BleRelayManager.kt, wijzig sendOtaData() (regel 186) om Boolean terug te geven:
@SuppressLint("MissingPermission")
fun sendOtaData(data: ByteArray): Boolean {
val char = otaCharacteristic ?: return false
val gatt = bluetoothGatt ?: return false
char.value = data
char.writeType = BluetoothGattCharacteristic.WRITE_TYPE_NO_RESPONSE
return gatt.writeCharacteristic(char)
}
- Step 2: Return check in
performOta()
In OtaManager.kt, in de retry-loop van performOta() (de while-loop die chunks stuurt), wijzig de relayManager.sendOtaData(packet.array()) aanroep:
while (retries < 3) {
val writeOk = relayManager.sendOtaData(packet.array())
if (!writeOk) {
Log.w(TAG, "writeCharacteristic returned false for chunk $seq, retrying")
retries++
delay(100)
continue
}
Log.d(TAG, "Sent chunk $seq/$totalChunks ($chunkSize bytes) attempt ${retries + 1}")
...
- Step 3: Compileer
./gradlew assembleDebug
- Step 4: Commit
git add android/app/src/main/java/nl/eddydevink/sylda/BleRelayManager.kt android/app/src/main/java/nl/eddydevink/sylda/OtaManager.kt
git commit -m "fix: check writeCharacteristic return value in sendOtaData"
Task 5: otaStatusText widget verwijderen
Files:
-
Modify:
android/app/src/main/res/layout/activity_settings.xml:39-47 -
Step 1: Verwijder
otaStatusTextuit layout
In activity_settings.xml, verwijder regels 39-47 (de TextView met id otaStatusText).
- Step 2: Compileer en verifieer dat er geen referenties meer zijn
./gradlew assembleDebug
Bevestig met grep dat er nergens meer otaStatusText voorkomt:
grep -r "otaStatusText" android/
Output moet leeg zijn.
- Step 3: Commit
git add android/app/src/main/res/layout/activity_settings.xml
git commit -m "chore: remove unused otaStatusText widget"
Task 6: Eindverificatie — alles compileren en OTA testen
- Step 1: ESP firmware compileren
pio run
Expected: SUCCESS
- Step 2: Android app compileren
./gradlew assembleDebug
Expected: BUILD SUCCESSFUL
- Step 3: ESP flashen via USB
pio run -t upload
- Step 4: OTA testen via BLE
- Open app op telefoon, verbind met SylDa_Server
- Ga naar Settings, selecteer Bluetooth
- Klik "Module bijwerken"
- Verify: progress bar vult, geen foutmeldingen
- Verify: ESP reboot en toont v5 in versie
- Step 5: Commit firmware.bin (indien build hash veranderd)
cp .pio/build/esp32-s3-devkitc-1/firmware.bin android/app/src/main/assets/firmware/firmware.bin
git add android/app/src/main/assets/firmware/firmware.bin
git commit -m "chore: update bundled firmware.bin"