Combined flash + key exchange flow with version check and USB permission persistence
- Firmware: add VERSION? command returning BUILD_NR for version check - Android: flash button checks firmware version first, skips flash if current - After flash: auto key exchange via reboot detection (expectingReboot flag) - device_filter.xml + intent-filter: USB permission 'always allow' checkbox - singleTop + onNewIntent prevents unwanted SettingsActivity launch on USB connect - platformio.ini: upload_port updated to /dev/ttyACM1
This commit is contained in:
parent
96162a16be
commit
d6d6f6674a
7 changed files with 87 additions and 14 deletions
|
|
@ -30,6 +30,15 @@
|
|||
<activity
|
||||
android:name=".SettingsActivity"
|
||||
android:label="Instellingen"
|
||||
android:parentActivityName=".MainActivity"/>
|
||||
android:parentActivityName=".MainActivity"
|
||||
android:launchMode="singleTop"
|
||||
android:exported="true">
|
||||
<intent-filter>
|
||||
<action android:name="android.hardware.usb.action.USB_DEVICE_ATTACHED" />
|
||||
</intent-filter>
|
||||
<meta-data
|
||||
android:name="android.hardware.usb.action.USB_DEVICE_ATTACHED"
|
||||
android:resource="@xml/device_filter" />
|
||||
</activity>
|
||||
</application>
|
||||
</manifest>
|
||||
Binary file not shown.
|
|
@ -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<UsbDevice>(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)
|
||||
|
|
|
|||
|
|
@ -57,7 +57,9 @@
|
|||
<string name="msg_reading_key">Sleutel uitlezen…</string>
|
||||
<string name="msg_flash_download_mode">Sluit de ESP aan via USB om te flashen.</string>
|
||||
<string name="msg_initializing">Initialiseren…</string>
|
||||
<string name="msg_flash_complete">Firmware geflashed!\n\nDe ESP herstart automatisch. Sleutels blijven behouden in NVS.</string>
|
||||
<string name="msg_checking_version">Firmware versie controleren…</string>
|
||||
<string name="msg_env_waiting_reboot">Wachten op ESP herstart…</string>
|
||||
<string name="msg_flash_complete">Firmware geflashed!\n\nSleutels worden nu uitgelezen…</string>
|
||||
<string name="button_close">Sluiten</string>
|
||||
<string name="button_cancel">Annuleren</string>
|
||||
|
||||
|
|
|
|||
4
android/app/src/main/res/xml/device_filter.xml
Normal file
4
android/app/src/main/res/xml/device_filter.xml
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<usb-device vendor-id="12346" product-id="4097" />
|
||||
</resources>
|
||||
Loading…
Reference in a new issue