Add USB auto-read key transfer + UI redesign
USB auto-read: - ESP responds to 'KEY?\n' over USB serial with 'HMAC_KEY:<hex>' - Android UsbKeyReceiver (manifest BroadcastReceiver) catches USB_DEVICE_ATTACHED - Filters on Espressif vendor 0x303a, requests USB permission - Opens CDC-ACM serial (115200), sends KEY?, parses response, saves to SharedPreferences - Works even when app not in foreground - usb-serial-for-android v3.7.3 via JitPack UI redesign: - activity_main.xml: FrameLayout with settings icon top-right, relay button center, status bottom-left (14sp) - Scan & Connect button moved to SettingsActivity - SettingsActivity: hint text 'Voer de HMAC sleutel in of sluit de relaismodule aan op de telefoon' - SettingsActivity: scan button saves key + returns RESULT_OK to trigger scanning - MainActivity: removed scanButton, auto-scan on onResume when key is set - Back button removed from settings (ActionBar up navigation only) README updated with USB protocol docs, new UI description, and updated setup guide
This commit is contained in:
parent
7f68e9ad70
commit
4518aec34e
10 changed files with 270 additions and 74 deletions
|
|
@ -8,6 +8,7 @@
|
|||
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/>
|
||||
|
||||
<uses-feature android:name="android.hardware.bluetooth_le" android:required="true"/>
|
||||
<uses-feature android:name="android.hardware.usb.host" android:required="false"/>
|
||||
|
||||
<application
|
||||
android:allowBackup="true"
|
||||
|
|
@ -29,5 +30,13 @@
|
|||
android:name=".SettingsActivity"
|
||||
android:label="Instellingen"
|
||||
android:parentActivityName=".MainActivity"/>
|
||||
|
||||
<receiver
|
||||
android:name=".UsbKeyReceiver"
|
||||
android:exported="true">
|
||||
<intent-filter>
|
||||
<action android:name="android.hardware.usb.action.USB_DEVICE_ATTACHED"/>
|
||||
</intent-filter>
|
||||
</receiver>
|
||||
</application>
|
||||
</manifest>
|
||||
|
|
@ -2,6 +2,7 @@ package com.eddy.atomrelay
|
|||
|
||||
import android.Manifest
|
||||
import android.annotation.SuppressLint
|
||||
import android.app.Activity
|
||||
import android.bluetooth.BluetoothAdapter
|
||||
import android.bluetooth.BluetoothDevice
|
||||
import android.bluetooth.BluetoothGatt
|
||||
|
|
@ -40,8 +41,9 @@ class MainActivity : AppCompatActivity() {
|
|||
private val CHARACTERISTIC_UUID: UUID = UUID.fromString("beb5483e-36e1-4688-b7f5-ea07361f26d8")
|
||||
private val CCCD_UUID: UUID = UUID.fromString("00002902-0000-1000-8000-00805f9b34fb")
|
||||
private const val DEVICE_NAME = "AtomS3R-Relay"
|
||||
private const val HMAC_PKT_SIZE = 37 // 1 cmd + 4 seq + 32 hmac
|
||||
private const val HMAC_PKT_SIZE = 37
|
||||
private const val HMAC_LEN = 32
|
||||
private const val REQUEST_SETTINGS = 1
|
||||
}
|
||||
|
||||
private var bluetoothGatt: BluetoothGatt? = null
|
||||
|
|
@ -55,7 +57,6 @@ class MainActivity : AppCompatActivity() {
|
|||
|
||||
private lateinit var statusText: TextView
|
||||
private lateinit var relayButton: Button
|
||||
private lateinit var scanButton: Button
|
||||
|
||||
private val scanCallback = object : ScanCallback() {
|
||||
override fun onScanResult(callbackType: Int, result: ScanResult) {
|
||||
|
|
@ -111,7 +112,7 @@ class MainActivity : AppCompatActivity() {
|
|||
gatt.close()
|
||||
bluetoothGatt = null
|
||||
relayCharacteristic = null
|
||||
seq = 0
|
||||
seq = 1
|
||||
handler.postDelayed({ startScan() }, 2000)
|
||||
}
|
||||
}
|
||||
|
|
@ -160,18 +161,22 @@ class MainActivity : AppCompatActivity() {
|
|||
|
||||
statusText = findViewById(R.id.statusText)
|
||||
relayButton = findViewById(R.id.relayButton)
|
||||
scanButton = findViewById(R.id.scanButton)
|
||||
val settingsButton = findViewById<ImageButton>(R.id.settingsButton)
|
||||
|
||||
scanButton.setOnClickListener { startScan() }
|
||||
settingsButton.setOnClickListener {
|
||||
startActivity(Intent(this, SettingsActivity::class.java))
|
||||
startActivityForResult(Intent(this, SettingsActivity::class.java), REQUEST_SETTINGS)
|
||||
}
|
||||
setupRelayButton()
|
||||
|
||||
registerReceiver(bondReceiver, IntentFilter(BluetoothDevice.ACTION_BOND_STATE_CHANGED))
|
||||
}
|
||||
|
||||
handler.postDelayed({ startScan() }, 500)
|
||||
@Deprecated("Deprecated in Java")
|
||||
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
||||
super.onActivityResult(requestCode, resultCode, data)
|
||||
if (requestCode == REQUEST_SETTINGS && resultCode == Activity.RESULT_OK) {
|
||||
handler.postDelayed({ startScan() }, 300)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
|
|
@ -179,6 +184,8 @@ class MainActivity : AppCompatActivity() {
|
|||
loadHmacKey()
|
||||
if (hmacKey == null) {
|
||||
statusText.text = "Geen HMAC sleutel - open Instellingen"
|
||||
} else if (!isScanning && bluetoothGatt == null) {
|
||||
handler.postDelayed({ startScan() }, 300)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -261,7 +268,6 @@ class MainActivity : AppCompatActivity() {
|
|||
|
||||
isScanning = true
|
||||
statusText.text = "Scanning..."
|
||||
scanButton.isEnabled = false
|
||||
Log.d(TAG, "Starting BLE scan for $DEVICE_NAME")
|
||||
|
||||
adapter.bluetoothLeScanner.startScan(scanCallback)
|
||||
|
|
@ -275,7 +281,6 @@ class MainActivity : AppCompatActivity() {
|
|||
private fun stopScan() {
|
||||
if (!isScanning) return
|
||||
isScanning = false
|
||||
scanButton.isEnabled = true
|
||||
val adapter = BluetoothAdapter.getDefaultAdapter()
|
||||
adapter?.bluetoothLeScanner?.stopScan(scanCallback)
|
||||
if (statusText.text == "Scanning...") {
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ class SettingsActivity : AppCompatActivity() {
|
|||
companion object {
|
||||
const val PREFS_NAME = "relay_prefs"
|
||||
const val PREF_KEY = "hmac_key"
|
||||
const val RESULT_SCAN = 100
|
||||
}
|
||||
|
||||
private var isVisible = false
|
||||
|
|
@ -31,7 +32,7 @@ class SettingsActivity : AppCompatActivity() {
|
|||
val keyInput = findViewById<EditText>(R.id.keyInput)
|
||||
val toggleBtn = findViewById<ImageButton>(R.id.toggleVisibility)
|
||||
val saveButton = findViewById<Button>(R.id.saveButton)
|
||||
val backButton = findViewById<Button>(R.id.backButton)
|
||||
val scanConnectButton = findViewById<Button>(R.id.scanConnectButton)
|
||||
val keyError = findViewById<TextView>(R.id.keyError)
|
||||
|
||||
keyInput.setText(prefs.getString(PREF_KEY, ""))
|
||||
|
|
@ -80,7 +81,14 @@ class SettingsActivity : AppCompatActivity() {
|
|||
finish()
|
||||
}
|
||||
|
||||
backButton.setOnClickListener { finish() }
|
||||
scanConnectButton.setOnClickListener {
|
||||
val key = keyInput.text.toString().uppercase().filter { it in "0123456789ABCDEF" }
|
||||
if (key.length == 64) {
|
||||
prefs.edit().putString(PREF_KEY, key).apply()
|
||||
}
|
||||
setResult(RESULT_SCAN)
|
||||
finish()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onSupportNavigateUp(): Boolean {
|
||||
|
|
|
|||
139
android/app/src/main/java/com/eddy/atomrelay/UsbKeyReceiver.kt
Normal file
139
android/app/src/main/java/com/eddy/atomrelay/UsbKeyReceiver.kt
Normal file
|
|
@ -0,0 +1,139 @@
|
|||
package com.eddy.atomrelay
|
||||
|
||||
import android.app.PendingIntent
|
||||
import android.content.BroadcastReceiver
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.hardware.usb.UsbDevice
|
||||
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() {
|
||||
|
||||
companion object {
|
||||
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
|
||||
}
|
||||
|
||||
override fun onReceive(context: Context, intent: Intent) {
|
||||
when (intent.action) {
|
||||
UsbManager.ACTION_USB_DEVICE_ATTACHED -> {
|
||||
val device = intent.getParcelableExtra<UsbDevice>(UsbManager.EXTRA_DEVICE) ?: return
|
||||
Log.d(TAG, "USB device attached: vid=0x${device.vendorId.toString(16)} pid=0x${device.productId.toString(16)}")
|
||||
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()
|
||||
} 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}")
|
||||
}
|
||||
}
|
||||
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 {
|
||||
Log.w(TAG, "USB permission denied")
|
||||
Handler(Looper.getMainLooper()).post {
|
||||
Toast.makeText(context, "USB-toegang geweigerd", Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun readKey(context: Context, usbManager: UsbManager, device: UsbDevice) {
|
||||
val handler = Handler(Looper.getMainLooper())
|
||||
var port: UsbSerialPort? = null
|
||||
|
||||
try {
|
||||
val driver = CdcAcmSerialDriver(device)
|
||||
val connection = usbManager.openDevice(device)
|
||||
if (connection == null) {
|
||||
Log.e(TAG, "Failed to open USB device")
|
||||
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() }
|
||||
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?")
|
||||
|
||||
port.purgeHwBuffers(true, true)
|
||||
|
||||
val cmd = "KEY?\n".toByteArray(StandardCharsets.US_ASCII)
|
||||
port.write(cmd, 500)
|
||||
|
||||
Thread.sleep(200)
|
||||
|
||||
val buf = ByteArray(256)
|
||||
val startTime = System.currentTimeMillis()
|
||||
val sb = StringBuilder()
|
||||
|
||||
while (System.currentTimeMillis() - startTime < KEY_TIMEOUT_MS) {
|
||||
val len = port.read(buf, 200)
|
||||
if (len > 0) {
|
||||
sb.append(String(buf, 0, len, StandardCharsets.US_ASCII))
|
||||
if (sb.contains("HMAC_KEY:") && sb.contains("\n")) {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val response = sb.toString()
|
||||
Log.d(TAG, "Serial response: $response")
|
||||
|
||||
val keyLine = response.lines().firstOrNull { it.startsWith("HMAC_KEY:") }
|
||||
if (keyLine != null) {
|
||||
val hexKey = keyLine.removePrefix("HMAC_KEY:").trim()
|
||||
if (hexKey.length == 64 && hexKey.all { it in "0123456789ABCDEFabcdef" }) {
|
||||
val upperKey = hexKey.uppercase()
|
||||
val prefs = context.getSharedPreferences(SettingsActivity.PREFS_NAME, Context.MODE_PRIVATE)
|
||||
prefs.edit().putString(SettingsActivity.PREF_KEY, upperKey).apply()
|
||||
Log.d(TAG, "HMAC key saved: $upperKey")
|
||||
handler.post {
|
||||
Toast.makeText(context, "HMAC sleutel gelezen van ESP!", Toast.LENGTH_LONG).show()
|
||||
}
|
||||
} else {
|
||||
Log.e(TAG, "Invalid key format: length=${hexKey.length}")
|
||||
handler.post { Toast.makeText(context, "Ongeldig sleutel formaat van ESP", Toast.LENGTH_SHORT).show() }
|
||||
}
|
||||
} 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() }
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Error reading key: ${e.message}", e)
|
||||
handler.post { Toast.makeText(context, "Fout bij uitlezen: ${e.message}", Toast.LENGTH_SHORT).show() }
|
||||
} finally {
|
||||
try { port?.close() } catch (e: Exception) {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,42 +1,33 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:orientation="vertical"
|
||||
android:gravity="center"
|
||||
android:padding="32dp">
|
||||
android:padding="16dp">
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/settingsButton"
|
||||
android:layout_width="48dp"
|
||||
android:layout_height="48dp"
|
||||
android:layout_gravity="end"
|
||||
android:layout_gravity="end|top"
|
||||
android:src="@drawable/ic_settings"
|
||||
android:background="?android:attr/selectableItemBackgroundBorderless"
|
||||
android:contentDescription="Instellingen"
|
||||
android:layout_marginBottom="16dp" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/statusText"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="Disconnected"
|
||||
android:textSize="20sp"
|
||||
android:layout_marginBottom="48dp"/>
|
||||
android:contentDescription="Instellingen" />
|
||||
|
||||
<Button
|
||||
android:id="@+id/relayButton"
|
||||
android:layout_width="200dp"
|
||||
android:layout_height="200dp"
|
||||
android:layout_gravity="center"
|
||||
android:text="HOLD"
|
||||
android:textSize="24sp"
|
||||
android:enabled="false"/>
|
||||
android:enabled="false" />
|
||||
|
||||
<Button
|
||||
android:id="@+id/scanButton"
|
||||
<TextView
|
||||
android:id="@+id/statusText"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="Scan & Connect"
|
||||
android:layout_marginTop="48dp"/>
|
||||
android:layout_gravity="bottom|start"
|
||||
android:text="Disconnected"
|
||||
android:textSize="14sp" />
|
||||
|
||||
</LinearLayout>
|
||||
</FrameLayout>
|
||||
|
|
@ -5,6 +5,15 @@
|
|||
android:orientation="vertical"
|
||||
android:padding="24dp">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/keyHint"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="Voer de HMAC sleutel in of sluit de relaismodule aan op de telefoon."
|
||||
android:textSize="14sp"
|
||||
android:textStyle="italic"
|
||||
android:layout_marginBottom="16dp" />
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
|
|
@ -66,11 +75,10 @@
|
|||
android:layout_marginTop="24dp" />
|
||||
|
||||
<Button
|
||||
android:id="@+id/backButton"
|
||||
android:id="@+id/scanConnectButton"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="Terug"
|
||||
android:background="@android:color/transparent"
|
||||
android:text="Scan & Verbind"
|
||||
android:layout_marginTop="8dp" />
|
||||
|
||||
</LinearLayout>
|
||||
Loading…
Reference in a new issue