fix: USB/OTA update flow - version check, chunk size, UX improvements
- MainActivity: suppress toast when activity not resumed, auto-navigate to Settings on new firmware - SettingsActivity: unified BLE/USB radio buttons + single update button, version check before OTA, show dialog before loadFirmware, spinner timeout - BleRelayManager: expose getEspVersion() - OtaManager: reduce chunk size 500→248 to fit BLE MTU 255 - ESP: WRITE→WRITE_NR on OTA characteristic (was silently dropping data) - Layout/strings: remove intro text, add method radio buttons - Bump all firmware versions to v5
This commit is contained in:
parent
e05d57a32c
commit
0513e7777d
8 changed files with 67 additions and 32 deletions
Binary file not shown.
|
|
@ -217,6 +217,8 @@ class BleRelayManager(
|
||||||
appVersion = version
|
appVersion = version
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun getEspVersion(): String? = espVersion
|
||||||
|
|
||||||
@SuppressLint("MissingPermission")
|
@SuppressLint("MissingPermission")
|
||||||
private fun connectToDevice(device: android.bluetooth.BluetoothDevice) {
|
private fun connectToDevice(device: android.bluetooth.BluetoothDevice) {
|
||||||
handler.post { callbacks.onStatusChanged(BleStatus.Connecting) }
|
handler.post { callbacks.onStatusChanged(BleStatus.Connecting) }
|
||||||
|
|
|
||||||
|
|
@ -22,7 +22,7 @@ class MainActivity : AppCompatActivity(), BleCallbacks {
|
||||||
private const val REQUEST_SETTINGS = 1
|
private const val REQUEST_SETTINGS = 1
|
||||||
private const val POST_SETTINGS_RESCAN_MS = 500L
|
private const val POST_SETTINGS_RESCAN_MS = 500L
|
||||||
private const val ON_RESUME_SCAN_DELAY_MS = 300L
|
private const val ON_RESUME_SCAN_DELAY_MS = 300L
|
||||||
private const val BUNDLED_FIRMWARE_VERSION = "2"
|
private const val BUNDLED_FIRMWARE_VERSION = "5"
|
||||||
}
|
}
|
||||||
|
|
||||||
private lateinit var manager: BleRelayManager
|
private lateinit var manager: BleRelayManager
|
||||||
|
|
@ -222,8 +222,10 @@ class MainActivity : AppCompatActivity(), BleCallbacks {
|
||||||
|
|
||||||
override fun onFirmwareVersion(espVersion: String, appVersion: String) {
|
override fun onFirmwareVersion(espVersion: String, appVersion: String) {
|
||||||
runOnUiThread {
|
runOnUiThread {
|
||||||
|
if (isFinishing || isDestroyed || !lifecycle.currentState.isAtLeast(androidx.lifecycle.Lifecycle.State.RESUMED)) return@runOnUiThread
|
||||||
if (espVersion.isEmpty() || espVersion != appVersion) {
|
if (espVersion.isEmpty() || espVersion != appVersion) {
|
||||||
Toast.makeText(this, getString(R.string.ota_update_available), Toast.LENGTH_LONG).show()
|
Toast.makeText(this, getString(R.string.ota_update_available), Toast.LENGTH_SHORT).show()
|
||||||
|
startActivityForResult(Intent(this, SettingsActivity::class.java), REQUEST_SETTINGS)
|
||||||
} else {
|
} else {
|
||||||
Toast.makeText(this, getString(R.string.ota_up_to_date), Toast.LENGTH_SHORT).show()
|
Toast.makeText(this, getString(R.string.ota_up_to_date), Toast.LENGTH_SHORT).show()
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -23,7 +23,7 @@ class OtaManager(
|
||||||
) {
|
) {
|
||||||
companion object {
|
companion object {
|
||||||
private const val TAG = "OtaManager"
|
private const val TAG = "OtaManager"
|
||||||
private const val OTA_CHUNK_DATA_SIZE = 500
|
private const val OTA_CHUNK_DATA_SIZE = 248
|
||||||
private const val OTA_CMD_START: Byte = 0x10
|
private const val OTA_CMD_START: Byte = 0x10
|
||||||
private const val OTA_CMD_END: Byte = 0x11
|
private const val OTA_CMD_END: Byte = 0x11
|
||||||
private const val OTA_STATUS_ACK: Byte = 0x00
|
private const val OTA_STATUS_ACK: Byte = 0x00
|
||||||
|
|
|
||||||
|
|
@ -64,7 +64,7 @@ class SettingsActivity : AppCompatActivity() {
|
||||||
private const val REBOOT_WAIT_MS = 5000L
|
private const val REBOOT_WAIT_MS = 5000L
|
||||||
private const val REBOOT_POLL_MS = 500L
|
private const val REBOOT_POLL_MS = 500L
|
||||||
private const val REBOOT_MAX_WAIT_MS = 15000L
|
private const val REBOOT_MAX_WAIT_MS = 15000L
|
||||||
private const val EXPECTED_FIRMWARE_VERSION = "1"
|
private const val EXPECTED_FIRMWARE_VERSION = "5"
|
||||||
}
|
}
|
||||||
|
|
||||||
private enum class UsbAction { NONE, EXCHANGE_KEY, FLASH }
|
private enum class UsbAction { NONE, EXCHANGE_KEY, FLASH }
|
||||||
|
|
@ -77,6 +77,8 @@ class SettingsActivity : AppCompatActivity() {
|
||||||
private var otaManager: OtaManager? = null
|
private var otaManager: OtaManager? = null
|
||||||
private var otaJob: Job? = null
|
private var otaJob: Job? = null
|
||||||
private var lastTheme: String? = null
|
private var lastTheme: String? = null
|
||||||
|
private val rebootTimeoutHandler = Handler(Looper.getMainLooper())
|
||||||
|
private var rebootTimeoutRunnable: Runnable? = null
|
||||||
|
|
||||||
private val usbAttachedReceiver = object : BroadcastReceiver() {
|
private val usbAttachedReceiver = object : BroadcastReceiver() {
|
||||||
override fun onReceive(context: Context, intent: Intent) {
|
override fun onReceive(context: Context, intent: Intent) {
|
||||||
|
|
@ -138,13 +140,12 @@ class SettingsActivity : AppCompatActivity() {
|
||||||
supportActionBar?.setDisplayHomeAsUpEnabled(true)
|
supportActionBar?.setDisplayHomeAsUpEnabled(true)
|
||||||
supportActionBar?.title = getString(R.string.title_settings)
|
supportActionBar?.title = getString(R.string.title_settings)
|
||||||
|
|
||||||
findViewById<Button>(R.id.flashFirmwareButton).setOnClickListener {
|
findViewById<Button>(R.id.updateModuleButton).setOnClickListener {
|
||||||
startFlashing()
|
if (findViewById<RadioGroup>(R.id.flashMethodGroup).checkedRadioButtonId == R.id.methodUsb) {
|
||||||
}
|
startFlashing()
|
||||||
|
} else {
|
||||||
val otaButton = findViewById<Button>(R.id.otaUpdateButton)
|
startOtaUpdate()
|
||||||
otaButton.setOnClickListener {
|
}
|
||||||
startOtaUpdate()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
registerReceiver(usbAttachedReceiver, IntentFilter(UsbManager.ACTION_USB_DEVICE_ATTACHED), Context.RECEIVER_EXPORTED)
|
registerReceiver(usbAttachedReceiver, IntentFilter(UsbManager.ACTION_USB_DEVICE_ATTACHED), Context.RECEIVER_EXPORTED)
|
||||||
|
|
@ -219,15 +220,26 @@ class SettingsActivity : AppCompatActivity() {
|
||||||
Toast.makeText(this, getString(R.string.ota_needs_usb_first), Toast.LENGTH_LONG).show()
|
Toast.makeText(this, getString(R.string.ota_needs_usb_first), Toast.LENGTH_LONG).show()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
otaManager = OtaManager(this, manager)
|
|
||||||
|
|
||||||
if (otaManager?.loadFirmware() != true) {
|
val espVersion = manager.getEspVersion()
|
||||||
Toast.makeText(this, getString(R.string.ota_load_failed), Toast.LENGTH_SHORT).show()
|
if (espVersion != null && espVersion == EXPECTED_FIRMWARE_VERSION) {
|
||||||
|
Toast.makeText(this, getString(R.string.ota_up_to_date), Toast.LENGTH_SHORT).show()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
showOtaProgressDialog()
|
showOtaProgressDialog()
|
||||||
|
|
||||||
|
otaManager = OtaManager(this, manager)
|
||||||
|
|
||||||
|
if (otaManager?.loadFirmware() != true) {
|
||||||
|
progressDialog?.dismiss()
|
||||||
|
progressDialog = null
|
||||||
|
Toast.makeText(this, getString(R.string.ota_load_failed), Toast.LENGTH_SHORT).show()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
manager.enableOtaNotifications()
|
||||||
|
|
||||||
otaJob = lifecycleScope.launch {
|
otaJob = lifecycleScope.launch {
|
||||||
val result = otaManager!!.performOta(object : OtaManager.OtaCallback {
|
val result = otaManager!!.performOta(object : OtaManager.OtaCallback {
|
||||||
override fun onProgress(percent: Int, message: String) {
|
override fun onProgress(percent: Int, message: String) {
|
||||||
|
|
@ -267,6 +279,8 @@ class SettingsActivity : AppCompatActivity() {
|
||||||
|
|
||||||
override fun onDestroy() {
|
override fun onDestroy() {
|
||||||
super.onDestroy()
|
super.onDestroy()
|
||||||
|
rebootTimeoutRunnable?.let { rebootTimeoutHandler.removeCallbacks(it) }
|
||||||
|
rebootTimeoutRunnable = null
|
||||||
currentAction = UsbAction.NONE
|
currentAction = UsbAction.NONE
|
||||||
flasherJob?.cancel()
|
flasherJob?.cancel()
|
||||||
flasherJob = null
|
flasherJob = null
|
||||||
|
|
@ -396,9 +410,10 @@ class SettingsActivity : AppCompatActivity() {
|
||||||
lifecycleScope.launch {
|
lifecycleScope.launch {
|
||||||
val version = readEspVersion(usbManager, device)
|
val version = readEspVersion(usbManager, device)
|
||||||
if (version == EXPECTED_FIRMWARE_VERSION && !expectingReboot) {
|
if (version == EXPECTED_FIRMWARE_VERSION && !expectingReboot) {
|
||||||
Log.d(TAG, "Firmware version $version matches, skipping flash")
|
Log.d(TAG, "Firmware version $version matches, up to date")
|
||||||
progressDialog?.dismiss()
|
progressDialog?.dismiss()
|
||||||
progressDialog = null
|
progressDialog = null
|
||||||
|
Toast.makeText(this@SettingsActivity, getString(R.string.ota_up_to_date), Toast.LENGTH_SHORT).show()
|
||||||
currentAction = UsbAction.EXCHANGE_KEY
|
currentAction = UsbAction.EXCHANGE_KEY
|
||||||
doKeyExchange(usbManager, device)
|
doKeyExchange(usbManager, device)
|
||||||
return@launch
|
return@launch
|
||||||
|
|
@ -437,6 +452,8 @@ class SettingsActivity : AppCompatActivity() {
|
||||||
getString(R.string.title_firmware_flash),
|
getString(R.string.title_firmware_flash),
|
||||||
getString(R.string.msg_env_waiting_reboot)
|
getString(R.string.msg_env_waiting_reboot)
|
||||||
)
|
)
|
||||||
|
rebootTimeoutRunnable = Runnable { cleanupAction() }
|
||||||
|
rebootTimeoutHandler.postDelayed(rebootTimeoutRunnable!!, REBOOT_MAX_WAIT_MS)
|
||||||
} catch (e: kotlinx.coroutines.CancellationException) {
|
} catch (e: kotlinx.coroutines.CancellationException) {
|
||||||
cleanupAction()
|
cleanupAction()
|
||||||
throw e
|
throw e
|
||||||
|
|
@ -453,6 +470,8 @@ class SettingsActivity : AppCompatActivity() {
|
||||||
// === USB dispatch ===
|
// === USB dispatch ===
|
||||||
|
|
||||||
private fun onDeviceReady(usbManager: UsbManager, device: UsbDevice) {
|
private fun onDeviceReady(usbManager: UsbManager, device: UsbDevice) {
|
||||||
|
rebootTimeoutRunnable?.let { rebootTimeoutHandler.removeCallbacks(it) }
|
||||||
|
rebootTimeoutRunnable = null
|
||||||
when (currentAction) {
|
when (currentAction) {
|
||||||
UsbAction.EXCHANGE_KEY -> doKeyExchange(usbManager, device)
|
UsbAction.EXCHANGE_KEY -> doKeyExchange(usbManager, device)
|
||||||
UsbAction.FLASH -> doFlashing(usbManager, device)
|
UsbAction.FLASH -> doFlashing(usbManager, device)
|
||||||
|
|
@ -501,6 +520,8 @@ class SettingsActivity : AppCompatActivity() {
|
||||||
private fun cleanupAction() {
|
private fun cleanupAction() {
|
||||||
currentAction = UsbAction.NONE
|
currentAction = UsbAction.NONE
|
||||||
expectingReboot = false
|
expectingReboot = false
|
||||||
|
rebootTimeoutRunnable?.let { rebootTimeoutHandler.removeCallbacks(it) }
|
||||||
|
rebootTimeoutRunnable = null
|
||||||
dialog?.dismiss()
|
dialog?.dismiss()
|
||||||
dialog = null
|
dialog = null
|
||||||
progressDialog?.dismiss()
|
progressDialog?.dismiss()
|
||||||
|
|
|
||||||
|
|
@ -5,25 +5,36 @@
|
||||||
android:orientation="vertical"
|
android:orientation="vertical"
|
||||||
android:padding="24dp">
|
android:padding="24dp">
|
||||||
|
|
||||||
<TextView
|
<RadioGroup
|
||||||
|
android:id="@+id/flashMethodGroup"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:text="@string/msg_flash_mode_help"
|
android:orientation="horizontal"
|
||||||
android:textSize="14sp"
|
android:layout_marginBottom="8dp"
|
||||||
android:layout_marginBottom="16dp" />
|
android:checkedButton="@id/methodBluetooth">
|
||||||
|
|
||||||
|
<RadioButton
|
||||||
|
android:id="@+id/methodBluetooth"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:gravity="center"
|
||||||
|
android:text="@string/method_bluetooth" />
|
||||||
|
|
||||||
|
<RadioButton
|
||||||
|
android:id="@+id/methodUsb"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:gravity="center"
|
||||||
|
android:text="@string/method_usb" />
|
||||||
|
</RadioGroup>
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
android:id="@+id/flashFirmwareButton"
|
android:id="@+id/updateModuleButton"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:text="@string/button_firmware_flash" />
|
android:text="@string/button_update_module" />
|
||||||
|
|
||||||
<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
|
<TextView
|
||||||
android:id="@+id/otaStatusText"
|
android:id="@+id/otaStatusText"
|
||||||
|
|
|
||||||
|
|
@ -27,9 +27,9 @@
|
||||||
|
|
||||||
<!-- SettingsActivity -->
|
<!-- SettingsActivity -->
|
||||||
<string name="title_settings">Instellingen</string>
|
<string name="title_settings">Instellingen</string>
|
||||||
<string name="msg_settings_intro">Sluit de ESP aan op USB en kies een actie.</string>
|
<string name="method_bluetooth">Bluetooth</string>
|
||||||
<string name="msg_flash_mode_help">Sluit de module aan op de USB-poort.</string>
|
<string name="method_usb">USB</string>
|
||||||
<string name="button_firmware_flash">Firmware bijwerken</string>
|
<string name="button_update_module">Module bijwerken</string>
|
||||||
<string name="usb_access_denied">USB-toegang geweigerd</string>
|
<string name="usb_access_denied">USB-toegang geweigerd</string>
|
||||||
<string name="key_read_success">Beveiligingssleutel gelezen</string>
|
<string name="key_read_success">Beveiligingssleutel gelezen</string>
|
||||||
<string name="key_exchange_failed">Sleutel uitwisselen mislukt - probeer opnieuw</string>
|
<string name="key_exchange_failed">Sleutel uitwisselen mislukt - probeer opnieuw</string>
|
||||||
|
|
@ -104,8 +104,7 @@
|
||||||
<string name="theme_applied">Thema: %s</string>
|
<string name="theme_applied">Thema: %s</string>
|
||||||
|
|
||||||
<!-- OTA Updates -->
|
<!-- OTA Updates -->
|
||||||
<string name="button_ota_update">Firmware bijwerken via Bluetooth</string>
|
<string name="title_ota_update">Module bijwerken</string>
|
||||||
<string name="title_ota_update">OTA Firmware Update</string>
|
|
||||||
<string name="msg_ota_starting">OTA update starten...</string>
|
<string name="msg_ota_starting">OTA update starten...</string>
|
||||||
<string name="ota_update_available">Nieuwe firmware beschikbaar</string>
|
<string name="ota_update_available">Nieuwe firmware beschikbaar</string>
|
||||||
<string name="ota_up_to_date">Firmware is up‑to‑date</string>
|
<string name="ota_up_to_date">Firmware is up‑to‑date</string>
|
||||||
|
|
|
||||||
|
|
@ -34,7 +34,7 @@
|
||||||
#define NVS_NAMESPACE "hmac"
|
#define NVS_NAMESPACE "hmac"
|
||||||
#define NVS_KEY_NAME "key"
|
#define NVS_KEY_NAME "key"
|
||||||
#define NVS_UUID_NAME "uuid"
|
#define NVS_UUID_NAME "uuid"
|
||||||
#define FIRMWARE_VERSION "2"
|
#define FIRMWARE_VERSION "5"
|
||||||
|
|
||||||
NimBLECharacteristic *pCharacteristic;
|
NimBLECharacteristic *pCharacteristic;
|
||||||
bool deviceConnected = false;
|
bool deviceConnected = false;
|
||||||
|
|
@ -343,7 +343,7 @@ void setup() {
|
||||||
|
|
||||||
NimBLECharacteristic *pOtaChar = pService->createCharacteristic(
|
NimBLECharacteristic *pOtaChar = pService->createCharacteristic(
|
||||||
OTA_UUID,
|
OTA_UUID,
|
||||||
NIMBLE_PROPERTY::WRITE | NIMBLE_PROPERTY::NOTIFY
|
NIMBLE_PROPERTY::WRITE_NR | NIMBLE_PROPERTY::NOTIFY
|
||||||
);
|
);
|
||||||
pOtaChar->setCallbacks(new OtaCharacteristicCallbacks());
|
pOtaChar->setCallbacks(new OtaCharacteristicCallbacks());
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue