diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index 5059ef3..284e429 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -30,6 +30,15 @@ + android:parentActivityName=".MainActivity" + android:launchMode="singleTop" + android:exported="true"> + + + + + \ No newline at end of file diff --git a/android/app/src/main/assets/firmware/firmware.bin b/android/app/src/main/assets/firmware/firmware.bin index e5bf28e..4f086b7 100644 Binary files a/android/app/src/main/assets/firmware/firmware.bin and b/android/app/src/main/assets/firmware/firmware.bin differ diff --git a/android/app/src/main/java/com/eddy/atomrelay/SettingsActivity.kt b/android/app/src/main/java/com/eddy/atomrelay/SettingsActivity.kt index e1946e1..d4da9e0 100644 --- a/android/app/src/main/java/com/eddy/atomrelay/SettingsActivity.kt +++ b/android/app/src/main/java/com/eddy/atomrelay/SettingsActivity.kt @@ -60,11 +60,17 @@ class SettingsActivity : AppCompatActivity() { private const val UUID_HEX_LEN = 32 private const val UUID_BYTE_COUNT = 16 private const val HMAC_BYTE_COUNT = 32 + private const val VERSION_TIMEOUT_MS = 3000L + private const val REBOOT_WAIT_MS = 5000L + private const val REBOOT_POLL_MS = 500L + private const val REBOOT_MAX_WAIT_MS = 15000L + private const val EXPECTED_FIRMWARE_VERSION = "1" } private enum class UsbAction { NONE, EXCHANGE_KEY, FLASH } private var currentAction = UsbAction.NONE + private var expectingReboot = false private var progressDialog: AlertDialog? = null private var dialog: AlertDialog? = null private var flasherJob: Job? = null @@ -109,6 +115,7 @@ class SettingsActivity : AppCompatActivity() { if (intent.action == UsbManager.ACTION_USB_DEVICE_DETACHED) { val device = intent.getParcelableExtra(UsbManager.EXTRA_DEVICE) if (device?.vendorId == ESP_VENDOR_ID) { + if (expectingReboot) return if (dialog != null && currentAction == UsbAction.NONE) { dialog?.dismiss() dialog = null @@ -147,6 +154,13 @@ class SettingsActivity : AppCompatActivity() { setupLabelEditTexts() } + override fun onNewIntent(intent: Intent) { + super.onNewIntent(intent) + if (intent.action == UsbManager.ACTION_USB_DEVICE_ATTACHED && currentAction == UsbAction.NONE) { + finish() + } + } + override fun onResume() { super.onResume() val current = PrefsHelper.getPrefs(this).getString(PrefsHelper.PREF_THEME, "system") ?: "system" @@ -319,6 +333,26 @@ class SettingsActivity : AppCompatActivity() { } private fun doFlashing(usbManager: UsbManager, device: UsbDevice) { + showProgressDialog(getString(R.string.title_firmware_flash), getString(R.string.msg_checking_version), STYLE_SPINNER) + + lifecycleScope.launch { + val version = readEspVersion(usbManager, device) + if (version == EXPECTED_FIRMWARE_VERSION && !expectingReboot) { + Log.d(TAG, "Firmware version $version matches, skipping flash") + progressDialog?.dismiss() + progressDialog = null + currentAction = UsbAction.EXCHANGE_KEY + doKeyExchange(usbManager, device) + return@launch + } + Log.d(TAG, "Firmware version ${version ?: "none"}, flashing") + progressDialog?.dismiss() + progressDialog = null + doFlashInner(usbManager, device) + } + } + + private fun doFlashInner(usbManager: UsbManager, device: UsbDevice) { dialog?.dismiss() dialog = null showProgressDialog(getString(R.string.title_firmware_flash), getString(R.string.msg_initializing), STYLE_HORIZONTAL) @@ -337,19 +371,14 @@ class SettingsActivity : AppCompatActivity() { try { flasher.flashDevice(usbManager, device, progressCallback) if (isFinishing || isDestroyed) return@launch - currentAction = UsbAction.NONE + expectingReboot = true + currentAction = UsbAction.EXCHANGE_KEY progressDialog?.dismiss() progressDialog = null - dialog = AlertDialog.Builder(this@SettingsActivity) - .setTitle(getString(R.string.title_firmware_flash)) - .setMessage(getString(R.string.msg_flash_complete)) - .setPositiveButton(getString(R.string.button_close)) { d, _ -> - d.dismiss() - dialog = null - } - .setCancelable(true) - .setOnCancelListener { dialog = null } - .show() + showWaitingDialog( + getString(R.string.title_firmware_flash), + getString(R.string.msg_env_waiting_reboot) + ) } catch (e: kotlinx.coroutines.CancellationException) { cleanupAction() throw e @@ -412,6 +441,7 @@ class SettingsActivity : AppCompatActivity() { private fun cleanupAction() { currentAction = UsbAction.NONE + expectingReboot = false dialog?.dismiss() dialog = null progressDialog?.dismiss() @@ -440,6 +470,30 @@ class SettingsActivity : AppCompatActivity() { data class Error(val resId: Int, val arg: String? = null) : KeyResult() } + private fun readEspVersion(usbManager: UsbManager, device: UsbDevice): String? { + val intf = UsbUtils.findCdcDataInterface(device) ?: return null + val connection = usbManager.openDevice(device) ?: return null + if (!connection.claimInterface(intf, true)) { connection.close(); return null } + + val epOut = UsbUtils.findBulkEndpoint(intf, UsbConstants.USB_DIR_OUT) + ?: run { connection.close(); return null } + val epIn = UsbUtils.findBulkEndpoint(intf, UsbConstants.USB_DIR_IN) + ?: run { connection.close(); return null } + + return try { + setupCdcControl(connection, device) + Thread.sleep(KEY_SETTLE_MS) + val cmd = "VERSION?\n".toByteArray(StandardCharsets.US_ASCII) + connection.bulkTransfer(epOut, cmd, cmd.size, USB_BULK_TIMEOUT_MS) + Thread.sleep(RESPONSE_WAIT_MS) + val response = readResponse(connection, epIn, VERSION_TIMEOUT_MS) + val versionLine = response.lines().firstOrNull { it.startsWith("BUILD_NR:") } + versionLine?.removePrefix("BUILD_NR:")?.trim() + } finally { + connection.close() + } + } + private fun readKeyFromEsp(usbManager: UsbManager, device: UsbDevice, appUuid: String?, appKey: String?): KeyResult? { val intf = UsbUtils.findCdcDataInterface(device) ?: return KeyResult.Error(R.string.err_no_cdc) diff --git a/android/app/src/main/res/values/strings.xml b/android/app/src/main/res/values/strings.xml index 734035f..d3983d3 100644 --- a/android/app/src/main/res/values/strings.xml +++ b/android/app/src/main/res/values/strings.xml @@ -57,7 +57,9 @@ Sleutel uitlezen… Sluit de ESP aan via USB om te flashen. Initialiseren… - Firmware geflashed!\n\nDe ESP herstart automatisch. Sleutels blijven behouden in NVS. + Firmware versie controleren… + Wachten op ESP herstart… + Firmware geflashed!\n\nSleutels worden nu uitgelezen… Sluiten Annuleren diff --git a/android/app/src/main/res/xml/device_filter.xml b/android/app/src/main/res/xml/device_filter.xml new file mode 100644 index 0000000..092927f --- /dev/null +++ b/android/app/src/main/res/xml/device_filter.xml @@ -0,0 +1,4 @@ + + + + diff --git a/platformio.ini b/platformio.ini index f9a9638..4f488bb 100644 --- a/platformio.ini +++ b/platformio.ini @@ -13,5 +13,5 @@ build_flags = -DCONFIG_BT_NIMBLE_ENABLED lib_deps = h2zero/NimBLE-Arduino@^2 -upload_port = /dev/ttyACM0 +upload_port = /dev/ttyACM1 upload_speed = 115200 \ No newline at end of file diff --git a/src/main.cpp b/src/main.cpp index ffb428c..e674ecb 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -22,6 +22,7 @@ #define NVS_NAMESPACE "hmac" #define NVS_KEY_NAME "key" #define NVS_UUID_NAME "uuid" +#define FIRMWARE_VERSION "1" NimBLECharacteristic *pCharacteristic; bool deviceConnected = false; @@ -250,6 +251,9 @@ void loop() { } else { Serial.println("NO_KEY"); } + } else if (serialBuf == "VERSION?") { + Serial.print("BUILD_NR:"); + Serial.println(FIRMWARE_VERSION); } else if (serialBuf.startsWith("SET_KEY:")) { String hex = serialBuf.substring(8); hex.toUpperCase();