BleHrService: auto-reconnect a dropped strap (bounded, never for an absent one)
On an unexpected disconnect of an established connection (connectedOnce, i.e. services were discovered), reconnect via the scan path up to 3 times, 8s apart, resetting the budget on each successful reconnect. The scan gives up (stopSelf) if the strap isn't found, so we never hammer a band that wasn't present at start or wasn't connected by hand. A user stop, Bluetooth off, or onDestroy sets a stopping flag that suppresses reconnect.
This commit is contained in:
parent
ee47af4479
commit
9bc71b19c9
3 changed files with 58 additions and 2 deletions
|
|
@ -53,7 +53,7 @@
|
|||
|------|-------|------------|
|
||||
| `MainActivity.kt` | ~300 | FileObserver IPC, seed DB, safe insets, SAF, BLE commands, OTA check |
|
||||
| `GpsRecorderService.kt` | ~530 | Foreground service GPS, WakeLock, JSONL (один файл на сессию до Стоп, идемпотентный onStartCommand при START_STICKY-перезапуске ОС; `bindSessionIfPending()` дописывает id сессии в имя файла `_s<id>` по `gps_session.txt`), 50Hz акселерометр/гироскоп, dead reckoning, raw sensor files |
|
||||
| `BleHrService.kt` | ~390 | Foreground service BLE HR, FitPro protocol, GATT queue, scan/connect. START_NOT_STICKY; скан не нашёл адрес -> stopSelf (не держит радио). Подключение инициируется ТОЛЬКО вручную (кнопка в настройках), не при старте записи. Выключение BT в любой момент не роняет app: единый `handler` (все postDelayed, снимаются разом), `BroadcastReceiver` на `ACTION_STATE_CHANGED` -> на STATE_OFF чистая остановка (disconnect+stopSelf), все нативные BLE-вызовы (scan/stopScan/connectGatt/writeCharacteristic/discoverServices) в try/catch. Каждый замер ЧСС дописывается в `ble_hr_log.jsonl` (append) прямо в сервисе — запись истории переживает гаснущий экран (JS-поллинг в фоне заморожен Android); воркаут при остановке пересобирает сэмплы из этого лога (`GPS._readHrLogSamples`), а не из JS-массива |
|
||||
| `BleHrService.kt` | ~390 | Foreground service BLE HR, FitPro protocol, GATT queue, scan/connect. START_NOT_STICKY; скан не нашёл адрес -> stopSelf (не держит радио). Подключение инициируется ТОЛЬКО вручную (кнопка в настройках), не при старте записи. Выключение BT в любой момент не роняет app: единый `handler` (все postDelayed, снимаются разом), `BroadcastReceiver` на `ACTION_STATE_CHANGED` -> на STATE_OFF чистая остановка (disconnect+stopSelf), все нативные BLE-вызовы (scan/stopScan/connectGatt/writeCharacteristic/discoverServices) в try/catch. Каждый замер ЧСС дописывается в `ble_hr_log.jsonl` (append) прямо в сервисе — запись истории переживает гаснущий экран (JS-поллинг в фоне заморожен Android); воркаут при остановке пересобирает сэмплы из этого лога (`GPS._readHrLogSamples`), а не из JS-массива. Авто-реконнект: обрыв УЖЕ установленного соединения (connectedOnce) переподключается через скан (до 3 попыток, 8с), сброс бюджета при успехе; если скан не нашёл ремень или это stopping (ACTION_STOP/BT-off/onDestroy) - stopSelf, не долбится в отсутствующий/не запущенный вручную ремень |
|
||||
| `generated/*.kt` | ~1100 | Tauri-сгенерированные: WebView, IPC, permissions |
|
||||
|
||||
### Инструменты (`tools/`)
|
||||
|
|
|
|||
32
docs/session-logs/2026-10-20_hr-reconnect.md
Normal file
32
docs/session-logs/2026-10-20_hr-reconnect.md
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
# Сессия 2026-10-20 (доб.) - авто-реконнект ремня ЧСС
|
||||
|
||||
## Задача
|
||||
Чтобы ночная запись не оборвалась при кратком обрыве BLE. Условие пользователя: НЕ долбиться
|
||||
в ремень, если его не было при старте или его не подключали руками - реконнект только для
|
||||
реально установленного и оборвавшегося соединения.
|
||||
|
||||
## Сделано (BleHrService.kt)
|
||||
- Поля `lastAddress`, `connectedOnce`, `stopping`, `reconnectTries`, `maxReconnect=3`.
|
||||
- Фреш-коннект (onStartCommand connect-ветка): `lastAddress=address; stopping=false;
|
||||
connectedOnce=false; reconnectTries=0`.
|
||||
- `onServicesDiscovered` (реально подключились): `connectedOnce=true; reconnectTries=0;
|
||||
isRunning=true`.
|
||||
- `onConnectionStateChange` DISCONNECTED: закрыть gatt; если `!stopping && connectedOnce &&
|
||||
reconnectTries<max && adapter.isEnabled && lastAddress!=null` -> reconnectTries++,
|
||||
`connectDevice(lastAddress)` через 8с; иначе stopSelf. Реконнект идёт через скан: ремень не
|
||||
найден -> timeout-ветка stopSelf (не зацикливается на отсутствующем).
|
||||
- `stopping=true` на ACTION_STOP, BT-off (ACTION_STATE_CHANGED), onDestroy - там реконнекта нет.
|
||||
|
||||
Начальный "скан не нашёл при старте -> stopSelf" не тронут: если ремня не было при старте,
|
||||
сервис гаснет, ничего не долбит.
|
||||
|
||||
## Установка (важно)
|
||||
Сборка БЕЗ -i, чтобы не убить идущую ночную запись. Порядок: пользователь жмёт Стоп (короткая
|
||||
сессия финализируется из нативного лога), затем установка APK, затем свежий старт ночи уже с
|
||||
реконнектом. Переустановка убивает процесс и чистит лог при следующем старте - поэтому не
|
||||
ставить поверх идущей записи.
|
||||
|
||||
## Проверка
|
||||
Компиляция Kotlin - в сборке. На устройстве (после установки): старт воркаута, снять/вернуть
|
||||
ремень на 10-20с -> запись переподключается и продолжается; снять надолго -> через ~3 попытки
|
||||
сервис гаснет. Живую проверку делает пользователь.
|
||||
|
|
@ -60,6 +60,11 @@ class BleHrService : Service() {
|
|||
private var deviceType = "hr_standard"
|
||||
private var dataFile: File? = null
|
||||
private var logFile: File? = null // append-only HR history (ble_hr_log.jsonl); survives screen-off
|
||||
private var lastAddress: String? = null // for auto-reconnect of a DROPPED connection
|
||||
private var connectedOnce = false // a real connection (services discovered) happened — only then do we reconnect
|
||||
private var stopping = false // user / BT-off asked to stop — never auto-reconnect
|
||||
private var reconnectTries = 0
|
||||
private val maxReconnect = 3 // ride out a brief drop, then give up (don't hammer an absent strap)
|
||||
private var lastBattery: Int = -1 // strap charge %, -1 = unknown
|
||||
private var scanResultsFile: File? = null
|
||||
private var fitproWriteChar: BluetoothGattCharacteristic? = null
|
||||
|
|
@ -76,6 +81,7 @@ class BleHrService : Service() {
|
|||
val state = intent.getIntExtra(BluetoothAdapter.EXTRA_STATE, BluetoothAdapter.ERROR)
|
||||
if (state == BluetoothAdapter.STATE_TURNING_OFF || state == BluetoothAdapter.STATE_OFF) {
|
||||
Log.d(TAG, "Bluetooth turning off - stopping BLE service")
|
||||
stopping = true // BT off: do not auto-reconnect
|
||||
handler.removeCallbacksAndMessages(null)
|
||||
disconnect()
|
||||
writeData("""{"status":"bt_off","ts":${System.currentTimeMillis()/1000}}""")
|
||||
|
|
@ -99,6 +105,7 @@ class BleHrService : Service() {
|
|||
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
||||
when (intent?.action) {
|
||||
ACTION_STOP -> {
|
||||
stopping = true // user-initiated stop: do not auto-reconnect
|
||||
disconnect()
|
||||
stopSelf()
|
||||
return START_NOT_STICKY
|
||||
|
|
@ -117,6 +124,8 @@ class BleHrService : Service() {
|
|||
val appDataDir = filesDir.parentFile ?: filesDir
|
||||
dataFile = File(appDataDir, "ble_hr_data.json")
|
||||
logFile = File(appDataDir, "ble_hr_log.jsonl")
|
||||
// fresh user-initiated connect: arm reconnect only after it actually connects
|
||||
lastAddress = address; stopping = false; connectedOnce = false; reconnectTries = 0
|
||||
connectDevice(address)
|
||||
}
|
||||
return START_NOT_STICKY // connect is user-initiated; no sticky restart
|
||||
|
|
@ -249,7 +258,19 @@ class BleHrService : Service() {
|
|||
Log.d(TAG, "Disconnected")
|
||||
isRunning = false
|
||||
writeData("""{"status":"disconnected","ts":${System.currentTimeMillis()/1000}}""")
|
||||
stopSelf()
|
||||
try { g.close() } catch (e: Exception) { Log.e(TAG, "gatt close on drop: ${e.message}") }
|
||||
gatt = null
|
||||
// Reconnect ONLY a connection that was really established (connectedOnce) and that the
|
||||
// user didn't stop. The reconnect goes through connectDevice's scan — if the strap is
|
||||
// gone it won't be found and we stopSelf (so we never hammer an absent/never-present band).
|
||||
if (!stopping && connectedOnce && reconnectTries < maxReconnect && adapter()?.isEnabled == true && lastAddress != null) {
|
||||
reconnectTries++
|
||||
Log.d(TAG, "Auto-reconnect $reconnectTries/$maxReconnect in 8s")
|
||||
updateNotification("Переподключение...")
|
||||
handler.postDelayed({ if (!stopping) connectDevice(lastAddress!!) }, 8000)
|
||||
} else {
|
||||
stopSelf()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -268,6 +289,8 @@ class BleHrService : Service() {
|
|||
Log.d(TAG, "Auto-detected device type: $deviceType")
|
||||
}
|
||||
Log.d(TAG, "Services discovered, setting up for $deviceType")
|
||||
connectedOnce = true; reconnectTries = 0 // real connection established — arm/refresh reconnect budget
|
||||
isRunning = true
|
||||
when (deviceType) {
|
||||
"hr_standard" -> setupStandardHr(g)
|
||||
"fitpro" -> setupFitPro(g)
|
||||
|
|
@ -544,6 +567,7 @@ class BleHrService : Service() {
|
|||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
stopping = true
|
||||
try { unregisterReceiver(btStateReceiver) } catch (e: Exception) { /* not registered / already gone */ }
|
||||
disconnect()
|
||||
super.onDestroy()
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue