Convert Thread+Handler to lifecycle-aware coroutines, add GPL-3.0 license
This commit is contained in:
parent
4a9a15bca5
commit
bbf01946ac
11 changed files with 1034 additions and 100 deletions
|
|
@ -43,4 +43,5 @@ dependencies {
|
|||
implementation 'com.eclipsesource.minimal-json:minimal-json:0.9.5'
|
||||
implementation 'org.slf4j:slf4j-nop:2.0.16'
|
||||
implementation 'androidx.security:security-crypto:1.1.0-alpha06'
|
||||
implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.7.0'
|
||||
}
|
||||
|
|
@ -1,15 +1,18 @@
|
|||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
package com.eddy.atomrelay
|
||||
|
||||
import android.content.Context
|
||||
import android.hardware.usb.UsbConstants
|
||||
import android.hardware.usb.UsbDevice
|
||||
import android.hardware.usb.UsbManager
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
import android.util.Log
|
||||
import kotlinx.coroutines.CancellationException
|
||||
import kotlinx.coroutines.ensureActive
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.dkaukov.esp32.core.EspFlasherApi
|
||||
import org.dkaukov.esp32.io.ProgressCallback
|
||||
import java.io.IOException
|
||||
import kotlin.coroutines.coroutineContext
|
||||
|
||||
class EspFlasher(private val context: Context) {
|
||||
|
||||
|
|
@ -19,12 +22,8 @@ class EspFlasher(private val context: Context) {
|
|||
|
||||
interface FlashCallback {
|
||||
fun onProgress(percent: Int, message: String)
|
||||
fun onComplete()
|
||||
fun onError(message: String)
|
||||
}
|
||||
|
||||
private val handler = Handler(Looper.getMainLooper())
|
||||
|
||||
@Volatile
|
||||
private var cancelled = false
|
||||
|
||||
|
|
@ -32,16 +31,18 @@ class EspFlasher(private val context: Context) {
|
|||
cancelled = true
|
||||
}
|
||||
|
||||
fun flashDevice(usbManager: UsbManager, device: UsbDevice, callback: FlashCallback) {
|
||||
suspend fun flashDevice(usbManager: UsbManager, device: UsbDevice, callback: FlashCallback) {
|
||||
cancelled = false
|
||||
Thread {
|
||||
try {
|
||||
try {
|
||||
withContext(kotlinx.coroutines.Dispatchers.IO) {
|
||||
doFlash(usbManager, device, callback)
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Flash failed", e)
|
||||
handler.post { callback.onError(e.message ?: "Onbekende fout") }
|
||||
}
|
||||
}.start()
|
||||
} catch (e: CancellationException) {
|
||||
throw e
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Flash failed", e)
|
||||
throw e
|
||||
}
|
||||
}
|
||||
|
||||
private class FlashStep(
|
||||
|
|
@ -53,9 +54,9 @@ class EspFlasher(private val context: Context) {
|
|||
val compress: Boolean
|
||||
)
|
||||
|
||||
private fun doFlash(usbManager: UsbManager, device: UsbDevice, callback: FlashCallback) {
|
||||
Thread.sleep(500)
|
||||
if (cancelled) return
|
||||
private suspend fun doFlash(usbManager: UsbManager, device: UsbDevice, callback: FlashCallback) {
|
||||
kotlinx.coroutines.delay(500)
|
||||
checkCancelled()
|
||||
|
||||
val connection = usbManager.openDevice(device)
|
||||
?: throw IOException("Kan USB-apparaat niet openen")
|
||||
|
|
@ -73,10 +74,10 @@ class EspFlasher(private val context: Context) {
|
|||
val epIn = UsbUtils.findBulkEndpoint(intf, UsbConstants.USB_DIR_IN)
|
||||
?: throw IOException("Geen USB IN endpoint gevonden")
|
||||
|
||||
postProgress(callback, 0, "Verbinden met ESP bootloader...")
|
||||
callback.onProgress(0, "Verbinden met ESP bootloader...")
|
||||
|
||||
val transport = UsbSerialTransport(connection, epOut, epIn)
|
||||
if (cancelled) return
|
||||
checkCancelled()
|
||||
|
||||
val assets = context.assets
|
||||
val bootloader = assets.open("firmware/bootloader.bin").readBytes()
|
||||
|
|
@ -84,8 +85,6 @@ class EspFlasher(private val context: Context) {
|
|||
val bootApp0 = assets.open("firmware/boot_app0.bin").readBytes()
|
||||
val firmware = assets.open("firmware/firmware.bin").readBytes()
|
||||
|
||||
val totalSize = bootloader.size + partitions.size + bootApp0.size + firmware.size
|
||||
|
||||
val steps = listOf(
|
||||
FlashStep("Bootloader", 0x0, bootloader, 30, 10, false),
|
||||
FlashStep("Partitie tabel", 0x8000, partitions, 40, 10, false),
|
||||
|
|
@ -93,7 +92,7 @@ class EspFlasher(private val context: Context) {
|
|||
FlashStep("Firmware", 0x10000, firmware, 60, 35, true)
|
||||
)
|
||||
|
||||
postProgress(callback, 5, "Sync met bootloader...")
|
||||
callback.onProgress(5, "Sync met bootloader...")
|
||||
val api = EspFlasherApi.connect(transport)
|
||||
.withCallBack(object : ProgressCallback {
|
||||
override fun onProgress(pct: Float) {
|
||||
|
|
@ -104,42 +103,42 @@ class EspFlasher(private val context: Context) {
|
|||
}
|
||||
})
|
||||
|
||||
postProgress(callback, 10, "Chip detecteren...")
|
||||
callback.onProgress(10, "Chip detecteren...")
|
||||
val detected = api.chipDetect()
|
||||
val chipId = detected.getChipId()
|
||||
Log.i(TAG, "Detected chip: $chipId")
|
||||
postProgress(callback, 15, "Chip: ${chipId.readableName}")
|
||||
callback.onProgress(15, "Chip: ${chipId.readableName}")
|
||||
|
||||
postProgress(callback, 20, "Stub laden...")
|
||||
callback.onProgress(20, "Stub laden...")
|
||||
val stub = detected.loadStub()
|
||||
|
||||
postProgress(callback, 25, "Flash wissen...")
|
||||
callback.onProgress(25, "Flash wissen...")
|
||||
stub.eraseFlash()
|
||||
|
||||
for (step in steps) {
|
||||
if (cancelled) return
|
||||
checkCancelled()
|
||||
val sizeStr = if (step.data.size > 1024) "${step.data.size / 1024}KB" else "${step.data.size} bytes"
|
||||
postProgress(callback, step.baseProgress, "${step.name} schrijven ($sizeStr)...")
|
||||
callback.onProgress(step.baseProgress, "${step.name} schrijven ($sizeStr)...")
|
||||
|
||||
val writeStub = if (step.compress) stub.withCompression(true) else stub.withCompression(false)
|
||||
writeStub.writeFlash(step.address, step.data)
|
||||
|
||||
val endProgress = step.baseProgress + step.range
|
||||
postProgress(callback, endProgress.coerceAtMost(95), "${step.name} geschreven")
|
||||
callback.onProgress(endProgress.coerceAtMost(95), "${step.name} geschreven")
|
||||
}
|
||||
|
||||
postProgress(callback, 95, "ESP resetten...")
|
||||
callback.onProgress(95, "ESP resetten...")
|
||||
stub.reset()
|
||||
|
||||
postProgress(callback, 100, "Firmware geflashed!")
|
||||
handler.post { callback.onComplete() }
|
||||
callback.onProgress(100, "Firmware geflashed!")
|
||||
} finally {
|
||||
connection.close()
|
||||
}
|
||||
}
|
||||
|
||||
private fun postProgress(callback: FlashCallback, percent: Int, message: String) {
|
||||
Log.d(TAG, "Progress $percent%: $message")
|
||||
handler.post { callback.onProgress(percent, message) }
|
||||
private suspend fun checkCancelled() {
|
||||
if (cancelled) {
|
||||
coroutineContext.ensureActive()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
package com.eddy.atomrelay
|
||||
|
||||
import android.Manifest
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
package com.eddy.atomrelay
|
||||
|
||||
import android.content.Context
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
package com.eddy.atomrelay
|
||||
|
||||
import android.app.AlertDialog
|
||||
|
|
@ -13,14 +14,17 @@ import android.hardware.usb.UsbEndpoint
|
|||
import android.hardware.usb.UsbInterface
|
||||
import android.hardware.usb.UsbManager
|
||||
import android.os.Bundle
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
import android.util.Log
|
||||
import android.widget.Button
|
||||
import android.widget.ProgressBar
|
||||
import android.widget.TextView
|
||||
import android.widget.Toast
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import java.nio.charset.StandardCharsets
|
||||
import java.security.SecureRandom
|
||||
|
||||
|
|
@ -44,7 +48,6 @@ class SettingsActivity : AppCompatActivity() {
|
|||
private var progressDialog: AlertDialog? = null
|
||||
private var dialog: AlertDialog? = null
|
||||
private var flasher: EspFlasher? = null
|
||||
private val handler = Handler(Looper.getMainLooper())
|
||||
|
||||
private val usbAttachedReceiver = object : BroadcastReceiver() {
|
||||
override fun onReceive(context: Context, intent: Intent) {
|
||||
|
|
@ -123,9 +126,9 @@ class SettingsActivity : AppCompatActivity() {
|
|||
currentAction = UsbAction.NONE
|
||||
flasher?.cancel()
|
||||
flasher = null
|
||||
try { unregisterReceiver(usbAttachedReceiver) } catch (e: Exception) {}
|
||||
try { unregisterReceiver(usbPermissionReceiver) } catch (e: Exception) {}
|
||||
try { unregisterReceiver(usbDetachedReceiver) } catch (e: Exception) {}
|
||||
try { unregisterReceiver(usbAttachedReceiver) } catch (_: Exception) {}
|
||||
try { unregisterReceiver(usbPermissionReceiver) } catch (_: Exception) {}
|
||||
try { unregisterReceiver(usbDetachedReceiver) } catch (_: Exception) {}
|
||||
}
|
||||
|
||||
override fun onSupportNavigateUp(): Boolean {
|
||||
|
|
@ -155,30 +158,30 @@ class SettingsActivity : AppCompatActivity() {
|
|||
private fun doKeyExchange(usbManager: UsbManager, device: UsbDevice) {
|
||||
showProgressDialog("Sleutel Uitwisselen", "Sleutel uitlezen...", STYLE_SPINNER)
|
||||
|
||||
Thread {
|
||||
lifecycleScope.launch(Dispatchers.IO) {
|
||||
val result = readKeyFromEsp(usbManager, device)
|
||||
if (isFinishing || isDestroyed) return@Thread
|
||||
handler.post {
|
||||
withContext(Dispatchers.Main) {
|
||||
if (isFinishing || isDestroyed) return@withContext
|
||||
when (result) {
|
||||
is KeyResult.Success -> {
|
||||
val prefs = PrefsHelper.getPrefs(this@SettingsActivity)
|
||||
prefs.edit().putString(PrefsHelper.PREF_KEY, result.hexKey).apply()
|
||||
Toast.makeText(this, "Beveiligingssleutel gelezen", Toast.LENGTH_LONG).show()
|
||||
Toast.makeText(this@SettingsActivity, "Beveiligingssleutel gelezen", Toast.LENGTH_LONG).show()
|
||||
cleanupAction()
|
||||
setResult(RESULT_SCAN)
|
||||
finish()
|
||||
}
|
||||
is KeyResult.Error -> {
|
||||
Toast.makeText(this, "Sleutel uitwisselen mislukt - probeer opnieuw", Toast.LENGTH_LONG).show()
|
||||
Toast.makeText(this@SettingsActivity, "Sleutel uitwisselen mislukt - probeer opnieuw", Toast.LENGTH_LONG).show()
|
||||
cleanupAction()
|
||||
}
|
||||
null -> {
|
||||
Toast.makeText(this, "Sleutel uitwisselen mislukt - probeer opnieuw", Toast.LENGTH_SHORT).show()
|
||||
Toast.makeText(this@SettingsActivity, "Sleutel uitwisselen mislukt - probeer opnieuw", Toast.LENGTH_SHORT).show()
|
||||
cleanupAction()
|
||||
}
|
||||
}
|
||||
}
|
||||
}.start()
|
||||
}
|
||||
}
|
||||
|
||||
// === Firmware Flashing ===
|
||||
|
|
@ -209,42 +212,41 @@ class SettingsActivity : AppCompatActivity() {
|
|||
showProgressDialog("Firmware Flashen", "Initialiseren...", STYLE_HORIZONTAL)
|
||||
|
||||
flasher = EspFlasher(this)
|
||||
flasher?.flashDevice(usbManager, device, object : EspFlasher.FlashCallback {
|
||||
val progressCallback = object : EspFlasher.FlashCallback {
|
||||
override fun onProgress(percent: Int, message: String) {
|
||||
runOnUiThread {
|
||||
val d = progressDialog ?: return@runOnUiThread
|
||||
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() {
|
||||
runOnUiThread {
|
||||
currentAction = UsbAction.NONE
|
||||
progressDialog?.dismiss()
|
||||
progressDialog = null
|
||||
dialog = AlertDialog.Builder(this@SettingsActivity)
|
||||
.setTitle("Firmware Flashen")
|
||||
.setMessage("Firmware geflashed!\n\nOntkoppel de ESP en sluit opnieuw aan om de sleutel in te stellen.")
|
||||
.setPositiveButton("Sluiten") { d, _ ->
|
||||
d.dismiss()
|
||||
dialog = null
|
||||
}
|
||||
.setCancelable(true)
|
||||
.setOnCancelListener { dialog = null }
|
||||
.show()
|
||||
}
|
||||
}
|
||||
lifecycleScope.launch {
|
||||
try {
|
||||
flasher?.flashDevice(usbManager, device, progressCallback)
|
||||
if (isFinishing || isDestroyed) return@launch
|
||||
currentAction = UsbAction.NONE
|
||||
progressDialog?.dismiss()
|
||||
progressDialog = null
|
||||
dialog = AlertDialog.Builder(this@SettingsActivity)
|
||||
.setTitle("Firmware Flashen")
|
||||
.setMessage("Firmware geflashed!\n\nOntkoppel de ESP en sluit opnieuw aan om de sleutel in te stellen.")
|
||||
.setPositiveButton("Sluiten") { d, _ ->
|
||||
d.dismiss()
|
||||
dialog = null
|
||||
}
|
||||
.setCancelable(true)
|
||||
.setOnCancelListener { dialog = null }
|
||||
.show()
|
||||
} catch (e: kotlinx.coroutines.CancellationException) {
|
||||
cleanupAction()
|
||||
} catch (e: Exception) {
|
||||
progressDialog?.dismiss()
|
||||
progressDialog = null
|
||||
Toast.makeText(this@SettingsActivity, "Flashen mislukt: ${e.message}", Toast.LENGTH_LONG).show()
|
||||
cleanupAction()
|
||||
}
|
||||
|
||||
override fun onError(message: String) {
|
||||
runOnUiThread {
|
||||
progressDialog?.dismiss()
|
||||
progressDialog = null
|
||||
Toast.makeText(this@SettingsActivity, "Flashen mislukt: $message", Toast.LENGTH_LONG).show()
|
||||
cleanupAction()
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// === USB dispatch ===
|
||||
|
|
@ -403,7 +405,7 @@ class SettingsActivity : AppCompatActivity() {
|
|||
Log.e(TAG, "Key exchange error", e)
|
||||
return KeyResult.Error("Fout: ${e.message}")
|
||||
} finally {
|
||||
try { connection.close() } catch (e: Exception) {}
|
||||
try { connection.close() } catch (_: Exception) {}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
package com.eddy.atomrelay
|
||||
|
||||
import android.hardware.usb.UsbConstants
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
package com.eddy.atomrelay
|
||||
|
||||
import android.hardware.usb.UsbConstants
|
||||
|
|
|
|||
Loading…
Reference in a new issue