Replace BLE encryption with HMAC-only auth, add buildnr, fix post-flash flow
BLE bonding/encryption removed — HMAC-SHA256 on application level provides authentication + replay protection. Eliminates bonding state bugs, write queue issues, and GATT_INSUFFICIENT_AUTHENTICATION complexity. Firmware: - Remove BLESecurity, setRespEncryptionKey, ESP_GATT_PERM_WRITE_ENCRYPTED - Characteristic permission: ESP_GATT_PERM_WRITE (no encryption required) - Characteristic property: PROPERTY_WRITE_NR (Write Without Response) Enables direct ON/OFF sequences without ACK wait time Android: - Remove bondReceiver, createBond, bonding state handling - Remove write queue / pendingRelayState / writeInProgress - Use WRITE_TYPE_NO_RESPONSE for minimal latency - Post-flash dialog auto-dismisses on USB detach (currentAction = NONE) - Separate progressDialog (flash progress) from dialog (post-flash info) - Detach during active action now calls cleanupAction() Build number: - BuildConfig.BUILD_NUMBER in build.gradle as yyyyMMddHHmmss timestamp - buildFeatures.buildConfig enabled - Displayed in SettingsActivity bottom (12sp, secondary color) Gradle: - Wrapper upgraded 8.5 → 8.14 - minSdk 24 → 26 (Android 8+) README updated to reflect HMAC-only security model.
This commit is contained in:
parent
0ff2c59b6d
commit
81abf2d824
13 changed files with 212 additions and 145 deletions
|
|
@ -9,10 +9,15 @@ android {
|
|||
|
||||
defaultConfig {
|
||||
applicationId "com.eddy.atomrelay"
|
||||
minSdk 24
|
||||
minSdk 26
|
||||
targetSdk 34
|
||||
versionCode 1
|
||||
versionName "1.0"
|
||||
buildConfigField "String", "BUILD_NUMBER", "\"${new java.text.SimpleDateFormat("yyyyMMddHHmmss").format(new Date())}\""
|
||||
}
|
||||
|
||||
buildFeatures {
|
||||
buildConfig true
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
|
|
|
|||
BIN
android/app/src/main/assets/firmware/boot_app0.bin
Normal file
BIN
android/app/src/main/assets/firmware/boot_app0.bin
Normal file
Binary file not shown.
BIN
android/app/src/main/assets/firmware/bootloader.bin
Normal file
BIN
android/app/src/main/assets/firmware/bootloader.bin
Normal file
Binary file not shown.
BIN
android/app/src/main/assets/firmware/firmware.bin
Normal file
BIN
android/app/src/main/assets/firmware/firmware.bin
Normal file
Binary file not shown.
BIN
android/app/src/main/assets/firmware/partitions.bin
Normal file
BIN
android/app/src/main/assets/firmware/partitions.bin
Normal file
Binary file not shown.
|
|
@ -4,7 +4,6 @@ import android.Manifest
|
|||
import android.annotation.SuppressLint
|
||||
import android.app.Activity
|
||||
import android.bluetooth.BluetoothAdapter
|
||||
import android.bluetooth.BluetoothDevice
|
||||
import android.bluetooth.BluetoothGatt
|
||||
import android.bluetooth.BluetoothGattCallback
|
||||
import android.bluetooth.BluetoothGattCharacteristic
|
||||
|
|
@ -12,10 +11,8 @@ import android.bluetooth.BluetoothGattDescriptor
|
|||
import android.bluetooth.BluetoothProfile
|
||||
import android.bluetooth.le.ScanCallback
|
||||
import android.bluetooth.le.ScanResult
|
||||
import android.content.BroadcastReceiver
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.IntentFilter
|
||||
import android.content.pm.PackageManager
|
||||
import android.os.Bundle
|
||||
import android.os.Handler
|
||||
|
|
@ -55,7 +52,6 @@ class MainActivity : AppCompatActivity() {
|
|||
|
||||
private var seq = 1
|
||||
private var hmacKey: ByteArray? = null
|
||||
private var pendingDevice: BluetoothDevice? = null
|
||||
|
||||
private lateinit var statusText: TextView
|
||||
private lateinit var relayButton: Button
|
||||
|
|
@ -72,43 +68,18 @@ class MainActivity : AppCompatActivity() {
|
|||
}
|
||||
}
|
||||
|
||||
private val bondReceiver = object : BroadcastReceiver() {
|
||||
@SuppressLint("MissingPermission")
|
||||
override fun onReceive(context: Context, intent: Intent) {
|
||||
val action = intent.action
|
||||
if (action == BluetoothDevice.ACTION_BOND_STATE_CHANGED) {
|
||||
val device = intent.getParcelableExtra<BluetoothDevice>(BluetoothDevice.EXTRA_DEVICE)
|
||||
val bondState = intent.getIntExtra(BluetoothDevice.EXTRA_BOND_STATE, BluetoothDevice.BOND_NONE)
|
||||
Log.d(TAG, "Bond state changed: ${device?.address} -> $bondState")
|
||||
if (bondState == BluetoothDevice.BOND_BONDED && device == pendingDevice) {
|
||||
Log.d(TAG, "Bonded, discovering services...")
|
||||
runOnUiThread { statusText.text = "Bonded, connecting..." }
|
||||
bluetoothGatt?.discoverServices()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private val gattCallback = object : BluetoothGattCallback() {
|
||||
@SuppressLint("MissingPermission")
|
||||
override fun onConnectionStateChange(gatt: BluetoothGatt, status: Int, newState: Int) {
|
||||
if (newState == BluetoothProfile.STATE_CONNECTED) {
|
||||
Log.d(TAG, "GATT connected")
|
||||
bluetoothGatt = gatt
|
||||
val device = gatt.device
|
||||
pendingDevice = device
|
||||
if (device.bondState == BluetoothDevice.BOND_BONDED) {
|
||||
Log.d(TAG, "Already bonded, discovering services...")
|
||||
runOnUiThread { statusText.text = "Connected, discovering..." }
|
||||
gatt.discoverServices()
|
||||
} else {
|
||||
Log.d(TAG, "Not bonded, creating bond...")
|
||||
runOnUiThread { statusText.text = "Pairing..." }
|
||||
device.createBond()
|
||||
}
|
||||
runOnUiThread { statusText.text = "Verbonden, services zoeken..." }
|
||||
gatt.discoverServices()
|
||||
} else if (newState == BluetoothProfile.STATE_DISCONNECTED) {
|
||||
Log.d(TAG, "GATT disconnected")
|
||||
runOnUiThread {
|
||||
statusText.text = "Reconnecting..."
|
||||
statusText.text = "Verbinden..."
|
||||
relayButton.isEnabled = false
|
||||
}
|
||||
gatt.close()
|
||||
|
|
@ -147,14 +118,6 @@ class MainActivity : AppCompatActivity() {
|
|||
runOnUiThread { statusText.text = "Service not found" }
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCharacteristicWrite(
|
||||
gatt: BluetoothGatt,
|
||||
characteristic: BluetoothGattCharacteristic,
|
||||
status: Int
|
||||
) {
|
||||
Log.d(TAG, "Write status: $status")
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
|
|
@ -169,15 +132,19 @@ class MainActivity : AppCompatActivity() {
|
|||
startActivityForResult(Intent(this, SettingsActivity::class.java), REQUEST_SETTINGS)
|
||||
}
|
||||
setupRelayButton()
|
||||
|
||||
registerReceiver(bondReceiver, IntentFilter(BluetoothDevice.ACTION_BOND_STATE_CHANGED), Context.RECEIVER_NOT_EXPORTED)
|
||||
}
|
||||
|
||||
@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)
|
||||
stopScan()
|
||||
bluetoothGatt?.close()
|
||||
bluetoothGatt = null
|
||||
relayCharacteristic = null
|
||||
seq = 1
|
||||
loadHmacKey()
|
||||
handler.postDelayed({ startScan() }, 500)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -300,7 +267,7 @@ class MainActivity : AppCompatActivity() {
|
|||
}
|
||||
|
||||
@SuppressLint("MissingPermission")
|
||||
private fun connectToDevice(device: BluetoothDevice) {
|
||||
private fun connectToDevice(device: android.bluetooth.BluetoothDevice) {
|
||||
runOnUiThread { statusText.text = "Connecting..." }
|
||||
Log.d(TAG, "Connecting to ${device.address}")
|
||||
bluetoothGatt = device.connectGatt(this, false, gattCallback)
|
||||
|
|
@ -348,6 +315,5 @@ class MainActivity : AppCompatActivity() {
|
|||
handler.removeCallbacksAndMessages(null)
|
||||
stopScan()
|
||||
bluetoothGatt?.close()
|
||||
try { unregisterReceiver(bondReceiver) } catch (e: Exception) {}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,5 +1,6 @@
|
|||
package com.eddy.atomrelay
|
||||
|
||||
import android.app.AlertDialog
|
||||
import android.app.PendingIntent
|
||||
import android.app.ProgressDialog
|
||||
import android.content.BroadcastReceiver
|
||||
|
|
@ -17,6 +18,7 @@ import android.os.Handler
|
|||
import android.os.Looper
|
||||
import android.util.Log
|
||||
import android.widget.Button
|
||||
import android.widget.TextView
|
||||
import android.widget.Toast
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import java.nio.charset.StandardCharsets
|
||||
|
|
@ -39,7 +41,8 @@ class SettingsActivity : AppCompatActivity() {
|
|||
private enum class UsbAction { NONE, EXCHANGE_KEY, FLASH }
|
||||
|
||||
private var currentAction = UsbAction.NONE
|
||||
private var dialog: ProgressDialog? = null
|
||||
private var progressDialog: ProgressDialog? = null
|
||||
private var dialog: AlertDialog? = null
|
||||
private val handler = Handler(Looper.getMainLooper())
|
||||
|
||||
private val usbAttachedReceiver = object : BroadcastReceiver() {
|
||||
|
|
@ -80,9 +83,13 @@ class SettingsActivity : AppCompatActivity() {
|
|||
override fun onReceive(context: Context, intent: Intent) {
|
||||
if (intent.action == UsbManager.ACTION_USB_DEVICE_DETACHED) {
|
||||
val device = intent.getParcelableExtra<UsbDevice>(UsbManager.EXTRA_DEVICE)
|
||||
if (device?.vendorId == ESP_VENDOR_ID && dialog != null && currentAction == UsbAction.NONE) {
|
||||
dialog?.dismiss()
|
||||
dialog = null
|
||||
if (device?.vendorId == ESP_VENDOR_ID) {
|
||||
if (dialog != null && currentAction == UsbAction.NONE) {
|
||||
dialog?.dismiss()
|
||||
dialog = null
|
||||
} else if (currentAction != UsbAction.NONE) {
|
||||
cleanupAction()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -103,6 +110,8 @@ class SettingsActivity : AppCompatActivity() {
|
|||
startFlashing()
|
||||
}
|
||||
|
||||
findViewById<TextView>(R.id.buildNumber).text = "Buildnummer: ${BuildConfig.BUILD_NUMBER}"
|
||||
|
||||
registerReceiver(usbAttachedReceiver, IntentFilter(UsbManager.ACTION_USB_DEVICE_ATTACHED), Context.RECEIVER_NOT_EXPORTED)
|
||||
registerReceiver(usbPermissionReceiver, IntentFilter(ACTION_USB_PERMISSION), Context.RECEIVER_NOT_EXPORTED)
|
||||
registerReceiver(usbDetachedReceiver, IntentFilter(UsbManager.ACTION_USB_DEVICE_DETACHED), Context.RECEIVER_NOT_EXPORTED)
|
||||
|
|
@ -198,27 +207,33 @@ class SettingsActivity : AppCompatActivity() {
|
|||
flasher.flashDevice(usbManager, device, object : EspFlasher.FlashCallback {
|
||||
override fun onProgress(percent: Int, message: String) {
|
||||
runOnUiThread {
|
||||
dialog?.progress = percent
|
||||
dialog?.setMessage(message)
|
||||
progressDialog?.progress = percent
|
||||
progressDialog?.setMessage(message)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onComplete() {
|
||||
runOnUiThread {
|
||||
dialog?.setMessage("Firmware geflashed!\n\nOntkoppel de ESP en sluit opnieuw aan om de sleutel in te stellen.")
|
||||
dialog?.setCancelable(true)
|
||||
dialog?.setButton(ProgressDialog.BUTTON_NEUTRAL, "Sluiten") { _, _ ->
|
||||
dialog?.dismiss()
|
||||
dialog = null
|
||||
cleanupAction()
|
||||
}
|
||||
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()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onError(message: String) {
|
||||
runOnUiThread {
|
||||
dialog?.dismiss()
|
||||
dialog = null
|
||||
progressDialog?.dismiss()
|
||||
progressDialog = null
|
||||
Toast.makeText(this@SettingsActivity, "Flashen mislukt: $message", Toast.LENGTH_LONG).show()
|
||||
cleanupAction()
|
||||
}
|
||||
|
|
@ -253,8 +268,8 @@ class SettingsActivity : AppCompatActivity() {
|
|||
}
|
||||
|
||||
private fun showProgressDialog(title: String, message: String, style: Int) {
|
||||
dialog?.dismiss()
|
||||
dialog = ProgressDialog(this).apply {
|
||||
progressDialog?.dismiss()
|
||||
progressDialog = ProgressDialog(this).apply {
|
||||
setTitle(title)
|
||||
setMessage(message)
|
||||
setProgressStyle(style)
|
||||
|
|
@ -271,6 +286,8 @@ class SettingsActivity : AppCompatActivity() {
|
|||
currentAction = UsbAction.NONE
|
||||
dialog?.dismiss()
|
||||
dialog = null
|
||||
progressDialog?.dismiss()
|
||||
progressDialog = null
|
||||
}
|
||||
|
||||
private fun requestUsbPermission(usbManager: UsbManager, device: UsbDevice) {
|
||||
|
|
|
|||
|
|
@ -38,4 +38,17 @@
|
|||
android:layout_height="wrap_content"
|
||||
android:text="Firmware Flashen" />
|
||||
|
||||
<View
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="0dp"
|
||||
android:layout_weight="1" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/buildNumber"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:textSize="12sp"
|
||||
android:textColor="?android:attr/textColorSecondary"
|
||||
android:gravity="center" />
|
||||
|
||||
</LinearLayout>
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.5-bin.zip
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.14-bin.zip
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
zipStorePath=wrapper/dists
|
||||
Loading…
Reference in a new issue