Project_SylDa/docs/superpowers/plans/2026-07-30-ble-ota.md

26 KiB

BLE OTA Firmware Updates — Implementation Plan

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: Enable firmware updates over BLE from the Android app, with automatic version check on connect.

Architecture: ESP32 exposes a firmware version READ characteristic and an OTA WRITE+NOTIFY characteristic. Android reads version on every BLE connect, shows toast if an update is available. OTA flow: app sends firmware chunks via BLE → ESP writes to ota_1 via esp_ota_* API → reboot. Relays drop OFF during OTA.

Tech Stack: ESP32-S3 Arduino (NimBLE, Preferences, mbedtls, esp_ota), Android Kotlin (standard BLE APIs, coroutines)

Spec: docs/superpowers/specs/2026-07-30-ble-ota-design.md


Task 1: Firmware — Add VERSION_UUID READ characteristic

Files:

  • Modify: src/main.cpp

  • Step 1: Define constant and add version characteristic in setup()

Add after #define UUID_CHAR_UUID:

#define VERSION_UUID "beb5483e-36e1-4688-b7f5-ea07361f26db"

Replace the NimBLECharacteristic *pUuidChar section in setup() with:

    NimBLECharacteristic *pUuidChar = pService->createCharacteristic(
        UUID_CHAR_UUID,
        NIMBLE_PROPERTY::READ
    );
    pUuidChar->setValue(deviceUuid, ESP_UUID_LEN);

    NimBLECharacteristic *pVersionChar = pService->createCharacteristic(
        VERSION_UUID,
        NIMBLE_PROPERTY::READ
    );
    pVersionChar->setValue(FIRMWARE_VERSION);
  • Step 2: Build firmware
cd /home/eddy/src/Project_SylDa && pio run

Expected: SUCCESS with no errors.

  • Step 3: Flash and verify via USB serial
pio run -t upload

Open serial monitor (115200 baud) and send:

VERSION?

Expected: BUILD_NR:1

  • Step 4: Verify via BLE from Android side

Open the Android app, connect to ESP. In BleRelayManager's onMtuChanged, add a read for the new VERSION_UUID characteristic (will be done in Task 5). For now, verify the characteristic is discoverable with nRF Connect or similar BLE scanner — should show a READ characteristic with value "1".

  • Step 5: Commit
git add src/main.cpp
git commit -m "feat: add firmware version BLE characteristic (VERSION_UUID)"

Task 2: Firmware — Add OTA_UUID characteristic and OTA logic

Files:

  • Modify: src/main.cpp

  • Step 1: Add includes and constants

Add at top of file after existing includes:

#include <esp_ota_ops.h>
#include <esp_partition.h>
#include <nvs_flash.h>

Add after #define VERSION_UUID:

#define OTA_UUID    "beb5483e-36e1-4688-b7f5-ea07361f26da"

#define OTA_CHUNK_SIZE 500
#define OTA_CMD_START  0x10
#define OTA_CMD_END    0x11
#define OTA_STATUS_ACK 0x00
#define OTA_STATUS_NAK 0x01
#define OTA_STATUS_COMPLETE 0x10
#define OTA_STATUS_ERROR    0x11

Add global state variables after bool relay2State = false;:

bool otaInProgress = false;
esp_ota_handle_t otaHandle = 0;
const esp_partition_t *otaPartition = nullptr;
uint32_t otaExpectedSize = 0;
uint32_t otaWritten = 0;
uint8_t otaExpectedSha[32];

Add helper functions before setup():

static void ledOtaBusy() {
    digitalWrite(LED_PIN, (millis() / 100) % 2 ? HIGH : LOW);
}

static void otaReset() {
    otaInProgress = false;
    otaHandle = 0;
    otaPartition = nullptr;
    otaExpectedSize = 0;
    otaWritten = 0;
}

static void sendOtaNotify(NimBLECharacteristic *otaChar, uint8_t status, uint32_t seq) {
    uint8_t buf[5];
    buf[0] = status;
    buf[1] = (seq >> 24) & 0xFF;
    buf[2] = (seq >> 16) & 0xFF;
    buf[3] = (seq >> 8) & 0xFF;
    buf[4] = seq & 0xFF;
    otaChar->setValue(buf, 5);
    otaChar->notify();
}

static void abortOta(NimBLECharacteristic *otaChar, const char *reason) {
    Serial.printf("OTA: abort — %s\n", reason);
    if (otaHandle != 0) {
        esp_ota_end(otaHandle);
    }
    otaReset();
    sendOtaNotify(otaChar, OTA_STATUS_ERROR, 0);
}
  • Step 2: Add OTA characteristic callback class

Add before class MyCharacteristicCallbacks:

class OtaCharacteristicCallbacks : public NimBLECharacteristicCallbacks {
    void onWrite(NimBLECharacteristic *pChar, NimBLEConnInfo &connInfo) override {
        std::string value = pChar->getValue();
        if (value.empty()) return;
        const uint8_t *data = (const uint8_t *)value.data();
        size_t len = value.length();

        if (len == 5 && data[0] == OTA_CMD_START) {
            if (otaInProgress) {
                esp_ota_end(otaHandle);
                otaReset();
            }
            otaExpectedSize = ((uint32_t)data[1] << 24) | ((uint32_t)data[2] << 16) |
                              ((uint32_t)data[3] << 8)  | (uint32_t)data[4];
            otaPartition = esp_ota_get_next_update_partition(nullptr);
            if (otaPartition == nullptr) {
                Serial.println("OTA: no update partition found");
                sendOtaNotify(pChar, OTA_STATUS_ERROR, 0);
                return;
            }
            esp_err_t err = esp_ota_begin(otaPartition, otaExpectedSize, &otaHandle);
            if (err != ESP_OK) {
                Serial.printf("OTA: begin failed (%d)\n", err);
                sendOtaNotify(pChar, OTA_STATUS_ERROR, 0);
                return;
            }
            otaInProgress = true;
            otaWritten = 0;
            digitalWrite(RELAY1_PIN, LOW);
            digitalWrite(RELAY2_PIN, LOW);
            relay1State = false;
            relay2State = false;
            bleRelay1 = false;
            bleRelay2 = false;
            Serial.printf("OTA: started, expecting %lu bytes\n", otaExpectedSize);
            return;
        }

        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;
            }
            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: complete, rebooting...");
            delay(500);
            esp_restart();
            return;
        }

        if (!otaInProgress) return;

        if (data[0] < 0x10) {
            uint32_t seq = ((uint32_t)data[0] << 24) | ((uint32_t)data[1] << 16) |
                           ((uint32_t)data[2] << 8)  | (uint32_t)data[3];
            size_t dataLen = len - 4;
            esp_err_t err = esp_ota_write(otaHandle, data + 4, dataLen);
            if (err != ESP_OK) {
                Serial.printf("OTA: write err at seq %lu (%d)\n", seq, err);
                sendOtaNotify(pChar, OTA_STATUS_NAK, seq);
                abortOta(pChar, "write error");
                return;
            }
            otaWritten += dataLen;
            sendOtaNotify(pChar, OTA_STATUS_ACK, seq);
        }
    }
};
  • Step 3: Add OTA characteristic to BLE service in setup()

Add after the pVersionChar block in setup():

    NimBLECharacteristic *pOtaChar = pService->createCharacteristic(
        OTA_UUID,
        NIMBLE_PROPERTY::WRITE | NIMBLE_PROPERTY::NOTIFY
    );
    pOtaChar->setCallbacks(new OtaCharacteristicCallbacks());
  • Step 4: Update loop() to handle OTA mode

Modify the relay state logic in loop(). Replace:

    relay1State = t1Active || btnActive || (deviceConnected && bleRelay1);
    relay2State = t2Active || (deviceConnected && bleRelay2);

With:

    if (!otaInProgress) {
        relay1State = t1Active || btnActive || (deviceConnected && bleRelay1);
        relay2State = t2Active || (deviceConnected && bleRelay2);
    } else {
        relay1State = false;
        relay2State = false;
        ledOtaBusy();
    }

Also gate the serial command processing, BLE relay commands, and heartbeat logging with !otaInProgress. In the onWrite of MyCharacteristicCallbacks, add at the top:

if (otaInProgress) return;

In the serial handler, wrap the command processing in:

if (!otaInProgress) {
    // existing serial command handling
}

And wrap the heartbeat log in:

if (!otaInProgress && millis() - heartbeat > 5000) {
  • Step 5: Build firmware
cd /home/eddy/src/Project_SylDa && pio run

Expected: SUCCESS. Firmware should still be ~530-600KB.

  • Step 6: Commit
git add src/main.cpp
git commit -m "feat: add BLE OTA characteristic with esp_ota integration"

Task 3: Firmware — Bump version to "2" and rebuild asset firmware

Files:

  • Modify: src/main.cpp

  • Overwrite: android/app/src/main/assets/firmware/firmware.bin

  • Step 1: Bump version

Change:

#define FIRMWARE_VERSION "1"

To:

#define FIRMWARE_VERSION "2"
  • Step 2: Build and copy firmware to assets
cd /home/eddy/src/Project_SylDa && pio run && cp .pio/build/m5stack-atoms3r/firmware.bin android/app/src/main/assets/firmware/firmware.bin
  • Step 3: Flash to ESP and verify version
pio run -t upload

Serial: send VERSION? → Expected: BUILD_NR:2

  • Step 4: Commit
git add src/main.cpp android/app/src/main/assets/firmware/firmware.bin
git commit -m "feat: bump firmware to v2, update bundled firmware asset"

Task 4: Android — Update BleStatus and BleCallbacks

Files:

  • Modify: android/app/src/main/java/nl/eddydevink/sylda/BleStatus.kt
  • Modify: android/app/src/main/java/nl/eddydevink/sylda/BleCallbacks.kt (extract from BleStatus.kt — it's currently in the same file)

Wait, BleCallbacks is already in BleStatus.kt. Let me just modify BleStatus.kt.

  • Step 1: Add firmware version check callback to BleCallbacks

Modify BleCallbacks interface in BleStatus.kt. Add the method:

interface BleCallbacks {
    fun onStatusChanged(status: BleStatus)
    fun onFirmwareVersion(espVersion: String, appVersion: String)
}
  • Step 2: Commit
git add android/app/src/main/java/nl/eddydevink/sylda/BleStatus.kt
git commit -m "feat: add onFirmwareVersion callback to BleCallbacks"

Task 5: Android — Update BleRelayManager for version + OTA characteristics

Files:

  • Modify: android/app/src/main/java/nl/eddydevink/sylda/BleRelayManager.kt

  • Step 1: Add companion instance for cross-activity access

Add before companion object:

    companion object {
        var instance: BleRelayManager? = null

And at the end of the constructor body (before closing brace of companion object), add:

Wait — the instance assignment must happen in the class body, not in companion object. Add after the init block doesn't exist, so use a field initializer approach. Actually, just add at the end of the primary constructor body. Since Kotlin doesn't have an init block here, add it inline with the property declarations:

After private var triedAddresses = mutableSetOf<String>(), add:

    init {
        instance = this
    }

Full updated companion object + init:

    companion object {
        var instance: BleRelayManager? = null
        private const val TAG = "BleRelayManager"
        // ... keep existing companion members ...
    }

    // ... existing fields ...

    init {
        instance = this
    }
  • Step 2: Add UUID constants and MTU

Add after existing UUID constants:

private val VERSION_UUID: UUID = UUID.fromString("beb5483e-36e1-4688-b7f5-ea07361f26db")
private val OTA_UUID: UUID = UUID.fromString("beb5483e-36e1-4688-b7f5-ea07361f26da")

Change BLE_MTU from 256 to 512:

private const val BLE_MTU = 512
  • Step 3: Add OTA characteristic field and version fields

Add after private var relayCharacteristic:

private var otaCharacteristic: BluetoothGattCharacteristic? = null
private var espVersion: String? = null
private var appVersion: String = ""

Add a method to set the bundled app version:

fun setAppVersion(version: String) {
    appVersion = version
}
  • Step 4: Read version characteristic on service discovery

In onMtuChanged, after reading the UUID characteristic, also read the version characteristic. The current code reads uuidChar only. Modify onMtuChanged:

override fun onMtuChanged(gatt: BluetoothGatt, mtu: Int, status: Int) {
    Log.d(TAG, "MTU changed to $mtu")
    val service = gatt.getService(SERVICE_UUID)
    if (service != null) {
        relayCharacteristic = service.getCharacteristic(CHARACTERISTIC_UUID)
        otaCharacteristic = service.getCharacteristic(OTA_UUID)
        val uuidChar = service.getCharacteristic(UUID_CHAR_UUID)
        val versionChar = service.getCharacteristic(VERSION_UUID)
        if (relayCharacteristic != null && uuidChar != null) {
            Log.d(TAG, "Reading device UUID characteristic...")
            gatt.readCharacteristic(uuidChar)
        } else {
            Log.e(TAG, "Service or characteristics not found")
            handler.post { callbacks.onStatusChanged(BleStatus.ServiceNotFound) }
        }
    } else {
        Log.e(TAG, "Service not found")
        handler.post { callbacks.onStatusChanged(BleStatus.ServiceNotFound) }
    }
}
  • Step 5: Handle version characteristic read in onCharacteristicRead

Add version handling to onCharacteristicRead. After the UUID check block, add:

if (characteristic.uuid == VERSION_UUID && status == BluetoothGatt.GATT_SUCCESS) {
    val version = String(value, Charsets.UTF_8).trim()
    espVersion = version
    Log.d(TAG, "ESP firmware version: $version, app bundled: $appVersion")
    handler.post { callbacks.onFirmwareVersion(version, appVersion) }
}
  • Step 6: Add OTA write method

Add a method to write OTA data:

fun sendOtaData(data: ByteArray) {
    val char = otaCharacteristic ?: return
    val gatt = bluetoothGatt ?: return
    char.value = data
    char.writeType = BluetoothGattCharacteristic.WRITE_TYPE_NO_RESPONSE
    gatt.writeCharacteristic(char)
}
  • Step 7: Add method to enable OTA notifications
@SuppressLint("MissingPermission")
fun enableOtaNotifications(): Boolean {
    val char = otaCharacteristic ?: return false
    val gatt = bluetoothGatt ?: return false
    return gatt.setCharacteristicNotification(char, true)
}
  • Step 8: Commit
git add android/app/src/main/java/nl/eddydevink/sylda/BleRelayManager.kt
git commit -m "feat: add version read, OTA characteristic, BLE MTU 512 in BleRelayManager"

Task 6: Android — Update MainActivity for version check toast

Files:

  • Modify: android/app/src/main/java/nl/eddydevink/sylda/MainActivity.kt

  • Step 1: Add version constant and implement callback

Add companion object constant:

private const val BUNDLED_FIRMWARE_VERSION = "2"

In onCreate, after initializing manager:

manager.setAppVersion(BUNDLED_FIRMWARE_VERSION)

Add onFirmwareVersion implementation to the MainActivity class:

override fun onFirmwareVersion(espVersion: String, appVersion: String) {
    if (espVersion != appVersion) {
        runOnUiThread {
            Toast.makeText(this, getString(R.string.ota_update_available), Toast.LENGTH_LONG).show()
        }
    }
}
  • Step 2: Commit
git add android/app/src/main/java/nl/eddydevink/sylda/MainActivity.kt
git commit -m "feat: show toast when firmware update available via BLE"

Task 7: Android — Create OtaManager.kt

Files:

  • Create: android/app/src/main/java/nl/eddydevink/sylda/OtaManager.kt

  • Step 1: Create OtaManager class

// 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.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); kotlinx.coroutines.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++
            kotlinx.coroutines.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
    }
}
  • Step 2: Add import to OtaManager for delay

The delay and ensureActive calls need kotlinx.coroutines.delay and kotlinx.coroutines.ensureActive. These are already available through the lifecycle-runtime-ktx dependency.

  • Step 3: Commit
git add android/app/src/main/java/nl/eddydevink/sylda/OtaManager.kt
git commit -m "feat: add OtaManager for BLE firmware update"

Task 8: Android — Add OTA button and flow to SettingsActivity

Files:

  • Modify: android/app/src/main/res/layout/activity_settings.xml

  • Modify: android/app/src/main/java/nl/eddydevink/sylda/SettingsActivity.kt

  • Step 1 (layout): Add OTA button to activity_settings.xml

Add before the first <View divider (i.e., after the flashFirmwareButton Button):

    <Button
        android:id="@+id/otaUpdateButton"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="@string/button_ota_update"
        android:layout_marginTop="8dp" />

    <TextView
        android:id="@+id/otaStatusText"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:textSize="12sp"
        android:gravity="center"
        android:layout_marginTop="4dp"
        android:layout_marginBottom="4dp"
        android:visibility="gone" />
  • Step 2 (code): Add OTA button handler and logic to SettingsActivity

Add fields at top of class:

private var otaManager: OtaManager? = null
private var otaJob: Job? = null

Add OTA button setup in onCreate (after the flash button setup):

val otaButton = findViewById<Button>(R.id.otaUpdateButton)
otaButton.setOnClickListener {
    startOtaUpdate()
}

Add the OTA methods:

private fun startOtaUpdate() {
    val manager = BleRelayManager.instance
    if (manager == null) {
        Toast.makeText(this, "Niet verbonden met SylDa_Server", Toast.LENGTH_SHORT).show()
        return
    }
    otaManager = OtaManager(this, manager)

    if (otaManager?.loadFirmware() != true) {
        Toast.makeText(this, getString(R.string.ota_load_failed), Toast.LENGTH_SHORT).show()
        return
    }

    showOtaProgressDialog()

    otaJob = lifecycleScope.launch {
        val result = otaManager!!.performOta(object : OtaManager.OtaCallback {
            override fun onProgress(percent: Int, message: String) {
                lifecycleScope.launch(Dispatchers.Main) {
                    val d = progressDialog ?: return@launch
                    d.findViewById<ProgressBar>(R.id.progressBar)?.progress = percent
                    d.findViewById<TextView>(R.id.message)?.text = message
                }
            }
            override fun onComplete() {
                lifecycleScope.launch(Dispatchers.Main) {
                    progressDialog?.dismiss()
                    progressDialog = null
                    Toast.makeText(this@SettingsActivity, getString(R.string.ota_complete), Toast.LENGTH_LONG).show()
                }
            }
            override fun onError(message: String) {
                lifecycleScope.launch(Dispatchers.Main) {
                    progressDialog?.dismiss()
                    progressDialog = null
                    Toast.makeText(this@SettingsActivity, getString(R.string.ota_failed, message), Toast.LENGTH_LONG).show()
                }
            }
        })
    }
}

private fun showOtaProgressDialog() {
    showProgressDialog(
        getString(R.string.title_ota_update),
        getString(R.string.msg_ota_starting),
        STYLE_HORIZONTAL
    )
}

Update onDestroy to cancel the OTA job:

override fun onDestroy() {
    super.onDestroy()
    currentAction = UsbAction.NONE
    flasherJob?.cancel()
    flasherJob = null
    otaJob?.cancel()
    otaJob = null
    try { unregisterReceiver(usbAttachedReceiver) } catch (_: Exception) {}
    try { unregisterReceiver(usbPermissionReceiver) } catch (_: Exception) {}
    try { unregisterReceiver(usbDetachedReceiver) } catch (_: Exception) {}
}
  • Step 3: Commit
git add android/app/src/main/res/layout/activity_settings.xml android/app/src/main/java/nl/eddydevink/sylda/SettingsActivity.kt
git commit -m "feat: add BLE OTA update button to SettingsActivity"

Task 9: Android — Add OTA strings

Files:

  • Modify: android/app/src/main/res/values/strings.xml

  • Step 1: Add OTA-related strings

Add before </resources>:

    <!-- OTA Updates -->
    <string name="button_ota_update">Firmware bijwerken via Bluetooth</string>
    <string name="title_ota_update">OTA Firmware Update</string>
    <string name="msg_ota_starting">OTA update starten...</string>
    <string name="ota_update_available">Nieuwe firmware beschikbaar — open Instellingen om bij te werken</string>
    <string name="ota_complete">Update voltooid! ESP herstart...</string>
    <string name="ota_failed">OTA update mislukt: %s</string>
    <string name="ota_load_failed">Kan firmware bestand niet laden</string>
  • Step 2: Commit
git add android/app/src/main/res/values/strings.xml
git commit -m "feat: add OTA UI strings"

Task 10: Build APK and test full flow

Files:

  • All modified files from Tasks 1-9

  • Step 1: Build APK

cd /home/eddy/src/Project_SylDa/android && ./gradlew assembleDebug

Expected: BUILD SUCCESSFUL.

  • Step 2: Install APK on test phone

Use the adb-wireless skill to connect to the phone, then:

adb -s 192.168.1.43:5555 install -r app/build/outputs/apk/debug/app-debug.apk
  • Step 3: Test flow
  1. Open the app — should connect to SylDa_Server via BLE
  2. If ESP runs firmware v2 and app has v2: no toast (versions match)
  3. Flash ESP with v1 firmware (git checkout HEAD~2 src/main.cpp && pio run -t upload), then test: should see toast "Nieuwe firmware beschikbaar"
  4. Go to Settings → tap "Firmware bijwerken via Bluetooth" → should see progress bar
  5. After completion: ESP should reboot and run v2 firmware
  6. App reconnects, no toast (versions now match)
  • Step 4: Verify rollback (optional test)

Build a bad firmware (e.g., intentionally corrupt) and push it. ESP should fail esp_ota_end() and stay on current firmware.

  • Step 5: Commit (re-flash v2)
git stash  # restore v2 main.cpp
pio run -t upload

Final commit if any changes were made during testing:

git add -A
git commit -m "fix: OTA test adjustments"