From a6e5df00524798a1c5342b47714e134992e06487 Mon Sep 17 00:00:00 2001 From: eddy Date: Wed, 29 Jul 2026 20:05:26 +0200 Subject: [PATCH] fix: thread safety, GATT cleanup, onResume guard, relay2 docs --- AGENTS.md | 42 +++++++++++++++++++ .../com/eddy/atomrelay/BleRelayManager.kt | 37 +++++++++------- .../java/com/eddy/atomrelay/MainActivity.kt | 6 ++- 3 files changed, 69 insertions(+), 16 deletions(-) create mode 100644 AGENTS.md diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..f07152f --- /dev/null +++ b/AGENTS.md @@ -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 +``` diff --git a/android/app/src/main/java/com/eddy/atomrelay/BleRelayManager.kt b/android/app/src/main/java/com/eddy/atomrelay/BleRelayManager.kt index 74672a0..15fdeb1 100644 --- a/android/app/src/main/java/com/eddy/atomrelay/BleRelayManager.kt +++ b/android/app/src/main/java/com/eddy/atomrelay/BleRelayManager.kt @@ -73,23 +73,23 @@ class BleRelayManager( fun startScan() { if (isScanning) return if (!hasPermissions()) { - callbacks.onStatusChanged(BleStatus.PermissionsDenied) + handler.post { callbacks.onStatusChanged(BleStatus.PermissionsDenied) } return } val adapter = BluetoothAdapter.getDefaultAdapter() if (adapter == null || !adapter.isEnabled) { - callbacks.onStatusChanged(BleStatus.BluetoothDisabled) + handler.post { callbacks.onStatusChanged(BleStatus.BluetoothDisabled) } return } loadState() if (hmacKey == null || deviceUuid == null) { - callbacks.onStatusChanged(BleStatus.NoKey) + handler.post { callbacks.onStatusChanged(BleStatus.NoKey) } return } isScanning = true scanTimedOut = false scanTimeoutRetries = 0 - callbacks.onStatusChanged(BleStatus.Scanning) + handler.post { callbacks.onStatusChanged(BleStatus.Scanning) } val filter = ScanFilter.Builder().setServiceUuid(ParcelUuid(SERVICE_UUID)).build() val settings = ScanSettings.Builder().setScanMode(ScanSettings.SCAN_MODE_LOW_LATENCY).build() adapter.bluetoothLeScanner.startScan(listOf(filter), settings, scanCallback) @@ -105,6 +105,7 @@ class BleRelayManager( @SuppressLint("MissingPermission") fun stopScan() { + handler.removeCallbacks(scanRunnable) if (!isScanning) return isScanning = false scanTimeoutRunnable?.let { handler.removeCallbacks(it) } @@ -116,7 +117,7 @@ class BleRelayManager( handler.postDelayed(scanRunnable, 2000L) return } - callbacks.onStatusChanged(BleStatus.DeviceNotFound) + handler.post { callbacks.onStatusChanged(BleStatus.DeviceNotFound) } } } @@ -140,6 +141,9 @@ class BleRelayManager( fun destroy() { handler.removeCallbacksAndMessages(null) + stopScan() + bluetoothGatt?.disconnect() + bluetoothGatt?.close() } @SuppressLint("MissingPermission") @@ -173,9 +177,14 @@ class BleRelayManager( triedAddresses.clear() } + fun isConnected(): Boolean { + return bluetoothGatt != null + } + @SuppressLint("MissingPermission") 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) } @@ -185,7 +194,7 @@ class BleRelayManager( if (newState == BluetoothProfile.STATE_CONNECTED) { if (hmacKey == null) { Log.w(TAG, "GATT connected but no key -- disconnecting") - callbacks.onStatusChanged(BleStatus.NoKey) + handler.post { callbacks.onStatusChanged(BleStatus.NoKey) } gatt.disconnect() return } @@ -194,7 +203,7 @@ class BleRelayManager( gatt.discoverServices() } else if (newState == BluetoothProfile.STATE_DISCONNECTED) { Log.d(TAG, "GATT disconnected") - callbacks.onStatusChanged(BleStatus.Disconnected) + handler.post { callbacks.onStatusChanged(BleStatus.Disconnected) } gatt.close() bluetoothGatt = null relayCharacteristic = null @@ -207,7 +216,7 @@ class BleRelayManager( override fun onServicesDiscovered(gatt: android.bluetooth.BluetoothGatt, status: Int) { if (status != BluetoothGatt.GATT_SUCCESS) { Log.w(TAG, "Service discovery failed (status=$status)") - callbacks.onStatusChanged(BleStatus.ServiceError) + handler.post { callbacks.onStatusChanged(BleStatus.ServiceError) } return } Log.d(TAG, "Services discovered, requesting MTU...") @@ -226,11 +235,11 @@ class BleRelayManager( gatt.readCharacteristic(uuidChar) } else { Log.e(TAG, "Service or characteristics not found") - callbacks.onStatusChanged(BleStatus.ServiceNotFound) + handler.post { callbacks.onStatusChanged(BleStatus.ServiceNotFound) } } } else { 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 gatt.writeDescriptor(descriptor) } - callbacks.onStatusChanged(BleStatus.Connected) + handler.post { callbacks.onStatusChanged(BleStatus.Connected) } } else { Log.w(TAG, "UUID mismatch -- disconnecting") triedAddresses.add(gatt.device.address) - callbacks.onStatusChanged(BleStatus.WrongDevice) + handler.post { callbacks.onStatusChanged(BleStatus.WrongDevice) } gatt.disconnect() } } @@ -260,7 +269,7 @@ class BleRelayManager( override fun onCharacteristicWrite(gatt: android.bluetooth.BluetoothGatt, characteristic: android.bluetooth.BluetoothGattCharacteristic, status: Int) { if (status != BluetoothGatt.GATT_SUCCESS) { Log.w(TAG, "Write failed (status=$status)") - callbacks.onStatusChanged(BleStatus.CommError) + handler.post { callbacks.onStatusChanged(BleStatus.CommError) } } } } diff --git a/android/app/src/main/java/com/eddy/atomrelay/MainActivity.kt b/android/app/src/main/java/com/eddy/atomrelay/MainActivity.kt index ff2abd2..ed7e259 100644 --- a/android/app/src/main/java/com/eddy/atomrelay/MainActivity.kt +++ b/android/app/src/main/java/com/eddy/atomrelay/MainActivity.kt @@ -117,8 +117,10 @@ class MainActivity : AppCompatActivity(), BleCallbacks { requestPermissions() return } - manager.resetTriedAddresses() - handler.postDelayed({ manager.startScan() }, ON_RESUME_SCAN_DELAY_MS) + if (!manager.isConnected()) { + manager.resetTriedAddresses() + handler.postDelayed({ manager.startScan() }, ON_RESUME_SCAN_DELAY_MS) + } } override fun onPause() {