fix: thread safety, GATT cleanup, onResume guard, relay2 docs

This commit is contained in:
eddy 2026-07-29 20:05:26 +02:00
commit a6e5df0052
3 changed files with 69 additions and 16 deletions

42
AGENTS.md Normal file
View file

@ -0,0 +1,42 @@
# Project_SylDa
ESP32-S3-PICO-1-N8R8 (Atom S3R Voice), 2 relais, 2 touch sensors (JR223B), BLE, Android app USB OTG.
## Hardware
- **Board:** Atom S3R Voice (ESP32-S3-PICO-1-N8R8), 16MB OPI flash
- **Pinnen:** G1=RELAY1, G2=RELAY2, G8+pullup=TOUCH1, G6+pullup=TOUCH2, G41=BUTTON (active low), G46=LED
- **Touch:** DC3-24V Capacitieve Touch Schakelaar JR223B (GEEN TTP223). Minder C op punt A = gevoeliger = meer false triggers.
- **BLE:** "AtomS3R-Relay", service `4fafc201-1fb5-459e-8fcc-c5c9c331914b`
- **BLE cmds:** 0x00=relay1 OFF, 0x01=ON, 0x02=relay2 ON, 0x03=OFF, HMAC_PKT_SIZE=37 (1 cmd + 4 seq + 32 hmac)
- **NVS:** namespace `hmac`, key `key` — HMAC key 32 bytes, NVS op 0x9000-0xDFFF (partitie tabel)
## Workspace
`/home/eddy/src/Project_SylDa/` — S3R firmware + Android app
## Firmware — `src/main.cpp`
- PlatformIO, `platform = espressif32@6.7.0`, board `esp32-s3-devkitc-1`
- CDC USB serial (`ARDUINO_USB_CDC_ON_BOOT=1`), QIO OPI flash
- Upload laptop: `pio run -t upload`, port `/dev/ttyACM0`, 115200 baud
- NVS: namespace "hmac", key "key" (32 bytes HMAC)
- BLE advertising "AtomS3R-Relay", HMAC-verified relay commands
## Android app — `android/`
- CDC key exchange (geen FTDI), 2 relay knoppen (touch-to-hold), byte commands
- Key exchange: SettingsActivity via USB CDC (`UsbSerialTransport`)
- Flash: EspFlasher via USB CDC, `eraseFlash()` VERWIJDERD (NVS blijft behouden)
- Partitie layout flash: bootloader 0x0, partitions 0x8000, boot_app0 0xE000, firmware 0x10000
- BLE scan: 30s timeout, auto-retry 3x, reconnect na disconnect
- `deviceId` = UUID van ESP, `hmacKey` = HMAC key uit key exchange
- Bestanden: `MainActivity.kt`, `SettingsActivity.kt`, `EspFlasher.kt`, `UsbSerialTransport.kt`, `PrefsHelper.kt`, `UsbUtils.kt`
- Assets: `firmware/bootloader.bin`, `partitions.bin`, `boot_app0.bin`, `firmware.bin`
## ADB — telefoon
```
adb connect 192.168.1.43:5555
adb -s 192.168.1.43:5555 install -r app/build/outputs/apk/debug/app-debug.apk
```

View file

@ -73,23 +73,23 @@ class BleRelayManager(
fun startScan() { fun startScan() {
if (isScanning) return if (isScanning) return
if (!hasPermissions()) { if (!hasPermissions()) {
callbacks.onStatusChanged(BleStatus.PermissionsDenied) handler.post { callbacks.onStatusChanged(BleStatus.PermissionsDenied) }
return return
} }
val adapter = BluetoothAdapter.getDefaultAdapter() val adapter = BluetoothAdapter.getDefaultAdapter()
if (adapter == null || !adapter.isEnabled) { if (adapter == null || !adapter.isEnabled) {
callbacks.onStatusChanged(BleStatus.BluetoothDisabled) handler.post { callbacks.onStatusChanged(BleStatus.BluetoothDisabled) }
return return
} }
loadState() loadState()
if (hmacKey == null || deviceUuid == null) { if (hmacKey == null || deviceUuid == null) {
callbacks.onStatusChanged(BleStatus.NoKey) handler.post { callbacks.onStatusChanged(BleStatus.NoKey) }
return return
} }
isScanning = true isScanning = true
scanTimedOut = false scanTimedOut = false
scanTimeoutRetries = 0 scanTimeoutRetries = 0
callbacks.onStatusChanged(BleStatus.Scanning) handler.post { callbacks.onStatusChanged(BleStatus.Scanning) }
val filter = ScanFilter.Builder().setServiceUuid(ParcelUuid(SERVICE_UUID)).build() val filter = ScanFilter.Builder().setServiceUuid(ParcelUuid(SERVICE_UUID)).build()
val settings = ScanSettings.Builder().setScanMode(ScanSettings.SCAN_MODE_LOW_LATENCY).build() val settings = ScanSettings.Builder().setScanMode(ScanSettings.SCAN_MODE_LOW_LATENCY).build()
adapter.bluetoothLeScanner.startScan(listOf(filter), settings, scanCallback) adapter.bluetoothLeScanner.startScan(listOf(filter), settings, scanCallback)
@ -105,6 +105,7 @@ class BleRelayManager(
@SuppressLint("MissingPermission") @SuppressLint("MissingPermission")
fun stopScan() { fun stopScan() {
handler.removeCallbacks(scanRunnable)
if (!isScanning) return if (!isScanning) return
isScanning = false isScanning = false
scanTimeoutRunnable?.let { handler.removeCallbacks(it) } scanTimeoutRunnable?.let { handler.removeCallbacks(it) }
@ -116,7 +117,7 @@ class BleRelayManager(
handler.postDelayed(scanRunnable, 2000L) handler.postDelayed(scanRunnable, 2000L)
return return
} }
callbacks.onStatusChanged(BleStatus.DeviceNotFound) handler.post { callbacks.onStatusChanged(BleStatus.DeviceNotFound) }
} }
} }
@ -140,6 +141,9 @@ class BleRelayManager(
fun destroy() { fun destroy() {
handler.removeCallbacksAndMessages(null) handler.removeCallbacksAndMessages(null)
stopScan()
bluetoothGatt?.disconnect()
bluetoothGatt?.close()
} }
@SuppressLint("MissingPermission") @SuppressLint("MissingPermission")
@ -173,9 +177,14 @@ class BleRelayManager(
triedAddresses.clear() triedAddresses.clear()
} }
fun isConnected(): Boolean {
return bluetoothGatt != null
}
@SuppressLint("MissingPermission") @SuppressLint("MissingPermission")
private fun connectToDevice(device: android.bluetooth.BluetoothDevice) { private fun connectToDevice(device: android.bluetooth.BluetoothDevice) {
callbacks.onStatusChanged(BleStatus.Connecting) handler.post { callbacks.onStatusChanged(BleStatus.Connecting) }
Log.d(TAG, "Connecting to ${device.address}")
bluetoothGatt = device.connectGatt(context, false, gattCallback) bluetoothGatt = device.connectGatt(context, false, gattCallback)
} }
@ -185,7 +194,7 @@ class BleRelayManager(
if (newState == BluetoothProfile.STATE_CONNECTED) { if (newState == BluetoothProfile.STATE_CONNECTED) {
if (hmacKey == null) { if (hmacKey == null) {
Log.w(TAG, "GATT connected but no key -- disconnecting") Log.w(TAG, "GATT connected but no key -- disconnecting")
callbacks.onStatusChanged(BleStatus.NoKey) handler.post { callbacks.onStatusChanged(BleStatus.NoKey) }
gatt.disconnect() gatt.disconnect()
return return
} }
@ -194,7 +203,7 @@ class BleRelayManager(
gatt.discoverServices() gatt.discoverServices()
} else if (newState == BluetoothProfile.STATE_DISCONNECTED) { } else if (newState == BluetoothProfile.STATE_DISCONNECTED) {
Log.d(TAG, "GATT disconnected") Log.d(TAG, "GATT disconnected")
callbacks.onStatusChanged(BleStatus.Disconnected) handler.post { callbacks.onStatusChanged(BleStatus.Disconnected) }
gatt.close() gatt.close()
bluetoothGatt = null bluetoothGatt = null
relayCharacteristic = null relayCharacteristic = null
@ -207,7 +216,7 @@ class BleRelayManager(
override fun onServicesDiscovered(gatt: android.bluetooth.BluetoothGatt, status: Int) { override fun onServicesDiscovered(gatt: android.bluetooth.BluetoothGatt, status: Int) {
if (status != BluetoothGatt.GATT_SUCCESS) { if (status != BluetoothGatt.GATT_SUCCESS) {
Log.w(TAG, "Service discovery failed (status=$status)") Log.w(TAG, "Service discovery failed (status=$status)")
callbacks.onStatusChanged(BleStatus.ServiceError) handler.post { callbacks.onStatusChanged(BleStatus.ServiceError) }
return return
} }
Log.d(TAG, "Services discovered, requesting MTU...") Log.d(TAG, "Services discovered, requesting MTU...")
@ -226,11 +235,11 @@ class BleRelayManager(
gatt.readCharacteristic(uuidChar) gatt.readCharacteristic(uuidChar)
} else { } else {
Log.e(TAG, "Service or characteristics not found") Log.e(TAG, "Service or characteristics not found")
callbacks.onStatusChanged(BleStatus.ServiceNotFound) handler.post { callbacks.onStatusChanged(BleStatus.ServiceNotFound) }
} }
} else { } else {
Log.e(TAG, "Service not found") Log.e(TAG, "Service not found")
callbacks.onStatusChanged(BleStatus.ServiceNotFound) handler.post { callbacks.onStatusChanged(BleStatus.ServiceNotFound) }
} }
} }
@ -246,11 +255,11 @@ class BleRelayManager(
descriptor.value = BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE descriptor.value = BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE
gatt.writeDescriptor(descriptor) gatt.writeDescriptor(descriptor)
} }
callbacks.onStatusChanged(BleStatus.Connected) handler.post { callbacks.onStatusChanged(BleStatus.Connected) }
} else { } else {
Log.w(TAG, "UUID mismatch -- disconnecting") Log.w(TAG, "UUID mismatch -- disconnecting")
triedAddresses.add(gatt.device.address) triedAddresses.add(gatt.device.address)
callbacks.onStatusChanged(BleStatus.WrongDevice) handler.post { callbacks.onStatusChanged(BleStatus.WrongDevice) }
gatt.disconnect() gatt.disconnect()
} }
} }
@ -260,7 +269,7 @@ class BleRelayManager(
override fun onCharacteristicWrite(gatt: android.bluetooth.BluetoothGatt, characteristic: android.bluetooth.BluetoothGattCharacteristic, status: Int) { override fun onCharacteristicWrite(gatt: android.bluetooth.BluetoothGatt, characteristic: android.bluetooth.BluetoothGattCharacteristic, status: Int) {
if (status != BluetoothGatt.GATT_SUCCESS) { if (status != BluetoothGatt.GATT_SUCCESS) {
Log.w(TAG, "Write failed (status=$status)") Log.w(TAG, "Write failed (status=$status)")
callbacks.onStatusChanged(BleStatus.CommError) handler.post { callbacks.onStatusChanged(BleStatus.CommError) }
} }
} }
} }

View file

@ -117,8 +117,10 @@ class MainActivity : AppCompatActivity(), BleCallbacks {
requestPermissions() requestPermissions()
return return
} }
manager.resetTriedAddresses() if (!manager.isConnected()) {
handler.postDelayed({ manager.startScan() }, ON_RESUME_SCAN_DELAY_MS) manager.resetTriedAddresses()
handler.postDelayed({ manager.startScan() }, ON_RESUME_SCAN_DELAY_MS)
}
} }
override fun onPause() { override fun onPause() {