Rewrite USB key reader: raw USB API + retry logic
- Replaced usb-serial-for-android library with raw Android USB API (UsbDeviceConnection.bulkTransfer) — the library's testConnection() sent a get_status CDC request that the ESP32-S3 USB-JTAG/serial controller does not support, causing read failures - Added CDC control interface setup (line coding + DTR/RTS) to enable data flow before sending KEY? command - Added retry logic: 5 attempts, 1s delay between retries, to handle other apps grabbing the USB device first (JTAG/serial debug unit triggers multiple system dialogs before our app gets permission) - Re-lookup ESP device on each retry to handle device path changes - Removed usb-serial-for-android dependency and JitPack repository
This commit is contained in:
parent
4518aec34e
commit
c707c21d7f
3 changed files with 207 additions and 80 deletions
|
|
@ -34,5 +34,4 @@ android {
|
|||
dependencies {
|
||||
implementation 'androidx.core:core-ktx:1.12.0'
|
||||
implementation 'androidx.appcompat:appcompat:1.6.1'
|
||||
implementation 'com.github.mik3y:usb-serial-for-android:3.7.3'
|
||||
}
|
||||
|
|
@ -4,14 +4,16 @@ import android.app.PendingIntent
|
|||
import android.content.BroadcastReceiver
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.hardware.usb.UsbConstants
|
||||
import android.hardware.usb.UsbDevice
|
||||
import android.hardware.usb.UsbDeviceConnection
|
||||
import android.hardware.usb.UsbEndpoint
|
||||
import android.hardware.usb.UsbInterface
|
||||
import android.hardware.usb.UsbManager
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
import android.util.Log
|
||||
import android.widget.Toast
|
||||
import com.hoho.android.usbserial.driver.CdcAcmSerialDriver
|
||||
import com.hoho.android.usbserial.driver.UsbSerialPort
|
||||
import java.nio.charset.StandardCharsets
|
||||
|
||||
class UsbKeyReceiver : BroadcastReceiver() {
|
||||
|
|
@ -20,7 +22,10 @@ class UsbKeyReceiver : BroadcastReceiver() {
|
|||
private const val TAG = "UsbKeyReceiver"
|
||||
private const val ACTION_USB_PERMISSION = "com.eddy.atomrelay.USB_PERMISSION"
|
||||
private const val ESP_VENDOR_ID = 0x303a
|
||||
private const val KEY_TIMEOUT_MS = 3000
|
||||
private const val KEY_TIMEOUT_MS = 5000
|
||||
private const val MAX_RETRIES = 5
|
||||
private const val RETRY_DELAY_MS = 1000L
|
||||
private const val CDC_DATA_INTERFACE_CLASS = 0x0A
|
||||
}
|
||||
|
||||
override fun onReceive(context: Context, intent: Intent) {
|
||||
|
|
@ -31,74 +36,154 @@ class UsbKeyReceiver : BroadcastReceiver() {
|
|||
if (device.vendorId != ESP_VENDOR_ID) return
|
||||
|
||||
val usbManager = context.getSystemService(Context.USB_SERVICE) as UsbManager
|
||||
if (usbManager.hasPermission(device)) {
|
||||
Thread { readKey(context, usbManager, device) }.start()
|
||||
val currentDevice = findEspDevice(usbManager) ?: device
|
||||
if (usbManager.hasPermission(currentDevice)) {
|
||||
Thread { readKeyRaw(context, usbManager, currentDevice) }.start()
|
||||
} else {
|
||||
val permIntent = PendingIntent.getBroadcast(
|
||||
context, 0,
|
||||
Intent(ACTION_USB_PERMISSION)
|
||||
.setClass(context, UsbKeyReceiver::class.java)
|
||||
.putExtra(UsbManager.EXTRA_DEVICE, device),
|
||||
PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT
|
||||
)
|
||||
usbManager.requestPermission(device, permIntent)
|
||||
Log.d(TAG, "Requesting USB permission for ${device.deviceName}")
|
||||
requestPermission(context, usbManager, currentDevice)
|
||||
}
|
||||
}
|
||||
ACTION_USB_PERMISSION -> {
|
||||
val device = intent.getParcelableExtra<UsbDevice>(UsbManager.EXTRA_DEVICE) ?: return
|
||||
if (intent.getBooleanExtra(UsbManager.EXTRA_PERMISSION_GRANTED, false)) {
|
||||
Log.d(TAG, "USB permission granted for ${device.deviceName}")
|
||||
val usbManager = context.getSystemService(Context.USB_SERVICE) as UsbManager
|
||||
Thread { readKey(context, usbManager, device) }.start()
|
||||
} else {
|
||||
if (!intent.getBooleanExtra(UsbManager.EXTRA_PERMISSION_GRANTED, false)) {
|
||||
Log.w(TAG, "USB permission denied")
|
||||
Handler(Looper.getMainLooper()).post {
|
||||
Toast.makeText(context, "USB-toegang geweigerd", Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
return
|
||||
}
|
||||
Log.d(TAG, "USB permission granted")
|
||||
val usbManager = context.getSystemService(Context.USB_SERVICE) as UsbManager
|
||||
val device = findEspDevice(usbManager)
|
||||
if (device != null) {
|
||||
Thread { readKeyRaw(context, usbManager, device) }.start()
|
||||
} else {
|
||||
Log.e(TAG, "ESP device not found after permission grant")
|
||||
Handler(Looper.getMainLooper()).post {
|
||||
Toast.makeText(context, "ESP niet (meer) gevonden", Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun readKey(context: Context, usbManager: UsbManager, device: UsbDevice) {
|
||||
private fun findEspDevice(usbManager: UsbManager): UsbDevice? {
|
||||
return usbManager.deviceList.values.firstOrNull { it.vendorId == ESP_VENDOR_ID }
|
||||
}
|
||||
|
||||
private fun requestPermission(context: Context, usbManager: UsbManager, device: UsbDevice) {
|
||||
val flags = if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.S) {
|
||||
PendingIntent.FLAG_MUTABLE or PendingIntent.FLAG_UPDATE_CURRENT
|
||||
} else {
|
||||
PendingIntent.FLAG_UPDATE_CURRENT
|
||||
}
|
||||
val permIntent = PendingIntent.getBroadcast(
|
||||
context, 0,
|
||||
Intent(context, UsbKeyReceiver::class.java)
|
||||
.setAction(ACTION_USB_PERMISSION),
|
||||
flags
|
||||
)
|
||||
usbManager.requestPermission(device, permIntent)
|
||||
Log.d(TAG, "Requesting USB permission for ${device.deviceName}")
|
||||
}
|
||||
|
||||
private fun readKeyRaw(context: Context, usbManager: UsbManager, device: UsbDevice) {
|
||||
val handler = Handler(Looper.getMainLooper())
|
||||
var port: UsbSerialPort? = null
|
||||
|
||||
for (attempt in 1..MAX_RETRIES) {
|
||||
Log.d(TAG, "Read attempt $attempt/$MAX_RETRIES")
|
||||
var connection: UsbDeviceConnection? = null
|
||||
|
||||
try {
|
||||
val driver = CdcAcmSerialDriver(device)
|
||||
val connection = usbManager.openDevice(device)
|
||||
val currentDevice = findEspDevice(usbManager) ?: device
|
||||
connection = usbManager.openDevice(currentDevice)
|
||||
if (connection == null) {
|
||||
Log.e(TAG, "Failed to open USB device")
|
||||
Log.w(TAG, "Attempt $attempt: openDevice returned null")
|
||||
if (attempt < MAX_RETRIES) {
|
||||
Thread.sleep(RETRY_DELAY_MS)
|
||||
continue
|
||||
}
|
||||
handler.post { Toast.makeText(context, "Kan USB-apparaat niet openen", Toast.LENGTH_SHORT).show() }
|
||||
return
|
||||
}
|
||||
|
||||
val ports = driver.ports
|
||||
if (ports.isEmpty()) {
|
||||
Log.e(TAG, "No serial ports found")
|
||||
handler.post { Toast.makeText(context, "Geen serial-poort gevonden", Toast.LENGTH_SHORT).show() }
|
||||
val dataInterface = findCdcDataInterface(currentDevice)
|
||||
if (dataInterface == null) {
|
||||
Log.w(TAG, "Attempt $attempt: no CDC data interface")
|
||||
connection.close()
|
||||
if (attempt < MAX_RETRIES) {
|
||||
Thread.sleep(RETRY_DELAY_MS)
|
||||
continue
|
||||
}
|
||||
handler.post { Toast.makeText(context, "Geen CDC interface gevonden op ESP", Toast.LENGTH_SHORT).show() }
|
||||
return
|
||||
}
|
||||
|
||||
port = ports[0]
|
||||
port.open(connection)
|
||||
port.setParameters(115200, 8, UsbSerialPort.STOPBITS_1, UsbSerialPort.PARITY_NONE)
|
||||
Log.d(TAG, "Serial port opened, sending KEY?")
|
||||
if (!connection.claimInterface(dataInterface, true)) {
|
||||
Log.w(TAG, "Attempt $attempt: failed to claim interface")
|
||||
connection.close()
|
||||
if (attempt < MAX_RETRIES) {
|
||||
Thread.sleep(RETRY_DELAY_MS)
|
||||
continue
|
||||
}
|
||||
handler.post { Toast.makeText(context, "Kan USB interface niet claimen", Toast.LENGTH_SHORT).show() }
|
||||
return
|
||||
}
|
||||
|
||||
port.purgeHwBuffers(true, true)
|
||||
val epOut = findBulkEndpoint(dataInterface, UsbConstants.USB_DIR_OUT)
|
||||
val epIn = findBulkEndpoint(dataInterface, UsbConstants.USB_DIR_IN)
|
||||
if (epOut == null || epIn == null) {
|
||||
Log.w(TAG, "Attempt $attempt: missing endpoints")
|
||||
connection.close()
|
||||
if (attempt < MAX_RETRIES) {
|
||||
Thread.sleep(RETRY_DELAY_MS)
|
||||
continue
|
||||
}
|
||||
handler.post { Toast.makeText(context, "USB endpoints niet gevonden", Toast.LENGTH_SHORT).show() }
|
||||
return
|
||||
}
|
||||
|
||||
Log.d(TAG, "CDC interface claimed, endpoints ready (out max=${epOut.maxPacketSize}, in max=${epIn.maxPacketSize})")
|
||||
|
||||
val controlIntf = findCdcControlInterface(currentDevice)
|
||||
if (controlIntf != null) {
|
||||
connection.claimInterface(controlIntf, true)
|
||||
connection.controlTransfer(
|
||||
UsbConstants.USB_DIR_OUT or UsbConstants.USB_TYPE_CLASS or 0x01,
|
||||
0x20,
|
||||
0,
|
||||
controlIntf.id,
|
||||
byteArrayOf(0x00, 0xC2.toByte(), 0x01, 0x00, 0x00, 0x00, 0x08),
|
||||
7,
|
||||
500
|
||||
)
|
||||
connection.controlTransfer(
|
||||
UsbConstants.USB_DIR_OUT or UsbConstants.USB_TYPE_CLASS or 0x01,
|
||||
0x22,
|
||||
3,
|
||||
controlIntf.id,
|
||||
null,
|
||||
0,
|
||||
500
|
||||
)
|
||||
Log.d(TAG, "CDC control interface configured (line coding + DTR/RTS)")
|
||||
} else {
|
||||
Log.w(TAG, "No CDC control interface found, trying without")
|
||||
}
|
||||
|
||||
Thread.sleep(300)
|
||||
|
||||
val cmd = "KEY?\n".toByteArray(StandardCharsets.US_ASCII)
|
||||
port.write(cmd, 500)
|
||||
val written = connection.bulkTransfer(epOut, cmd, cmd.size, 1000)
|
||||
Log.d(TAG, "Sent KEY? (${written} bytes)")
|
||||
|
||||
Thread.sleep(200)
|
||||
Thread.sleep(300)
|
||||
|
||||
val buf = ByteArray(256)
|
||||
val startTime = System.currentTimeMillis()
|
||||
val buf = ByteArray(512)
|
||||
val sb = StringBuilder()
|
||||
val startTime = System.currentTimeMillis()
|
||||
|
||||
while (System.currentTimeMillis() - startTime < KEY_TIMEOUT_MS) {
|
||||
val len = port.read(buf, 200)
|
||||
val len = connection.bulkTransfer(epIn, buf, buf.size, 500)
|
||||
if (len > 0) {
|
||||
sb.append(String(buf, 0, len, StandardCharsets.US_ASCII))
|
||||
if (sb.contains("HMAC_KEY:") && sb.contains("\n")) {
|
||||
|
|
@ -108,7 +193,7 @@ class UsbKeyReceiver : BroadcastReceiver() {
|
|||
}
|
||||
|
||||
val response = sb.toString()
|
||||
Log.d(TAG, "Serial response: $response")
|
||||
Log.d(TAG, "Serial response: ${response.take(200)}")
|
||||
|
||||
val keyLine = response.lines().firstOrNull { it.startsWith("HMAC_KEY:") }
|
||||
if (keyLine != null) {
|
||||
|
|
@ -121,19 +206,63 @@ class UsbKeyReceiver : BroadcastReceiver() {
|
|||
handler.post {
|
||||
Toast.makeText(context, "HMAC sleutel gelezen van ESP!", Toast.LENGTH_LONG).show()
|
||||
}
|
||||
return
|
||||
} else {
|
||||
Log.e(TAG, "Invalid key format: length=${hexKey.length}")
|
||||
handler.post { Toast.makeText(context, "Ongeldig sleutel formaat van ESP", Toast.LENGTH_SHORT).show() }
|
||||
handler.post { Toast.makeText(context, "Ongeldig sleutel formaat", Toast.LENGTH_SHORT).show() }
|
||||
return
|
||||
}
|
||||
} else {
|
||||
Log.e(TAG, "No HMAC_KEY line found in response")
|
||||
handler.post { Toast.makeText(context, "Geen sleutel gevonden op de ESP", Toast.LENGTH_SHORT).show() }
|
||||
Log.w(TAG, "Attempt $attempt: no HMAC_KEY in response, will retry")
|
||||
connection.close()
|
||||
connection = null
|
||||
if (attempt < MAX_RETRIES) {
|
||||
Thread.sleep(RETRY_DELAY_MS)
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Error reading key: ${e.message}", e)
|
||||
Log.e(TAG, "Attempt $attempt error: ${e.message}", e)
|
||||
if (attempt < MAX_RETRIES) {
|
||||
Thread.sleep(RETRY_DELAY_MS)
|
||||
} else {
|
||||
handler.post { Toast.makeText(context, "Fout bij uitlezen: ${e.message}", Toast.LENGTH_SHORT).show() }
|
||||
} finally {
|
||||
try { port?.close() } catch (e: Exception) {}
|
||||
}
|
||||
} finally {
|
||||
try { connection?.close() } catch (e: Exception) {}
|
||||
}
|
||||
}
|
||||
|
||||
Log.e(TAG, "All $MAX_RETRIES attempts failed")
|
||||
handler.post { Toast.makeText(context, "Sleutel niet gevonden na $MAX_RETRIES pogingen", Toast.LENGTH_SHORT).show() }
|
||||
}
|
||||
|
||||
private fun findCdcDataInterface(device: UsbDevice): UsbInterface? {
|
||||
for (i in 0 until device.interfaceCount) {
|
||||
val intf = device.getInterface(i)
|
||||
if (intf.interfaceClass == CDC_DATA_INTERFACE_CLASS) {
|
||||
return intf
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
private fun findCdcControlInterface(device: UsbDevice): UsbInterface? {
|
||||
for (i in 0 until device.interfaceCount) {
|
||||
val intf = device.getInterface(i)
|
||||
if (intf.interfaceClass == 0x02) {
|
||||
return intf
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
private fun findBulkEndpoint(intf: UsbInterface, direction: Int): UsbEndpoint? {
|
||||
for (i in 0 until intf.endpointCount) {
|
||||
val ep = intf.getEndpoint(i)
|
||||
if (ep.type == UsbConstants.USB_ENDPOINT_XFER_BULK && ep.direction == direction) {
|
||||
return ep
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
|
@ -10,7 +10,6 @@ dependencyResolutionManagement {
|
|||
repositories {
|
||||
google()
|
||||
mavenCentral()
|
||||
maven { url 'https://jitpack.io' }
|
||||
}
|
||||
}
|
||||
rootProject.name = "AtomRelay"
|
||||
|
|
|
|||
Loading…
Reference in a new issue