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();