BLE HR: connect manually only, no auto-scan on every ride (battery)

Recording auto-called ble_connect each ride, and the Kotlin service held a
GATT connect to an absent band (radio busy) plus START_STICKY churn -
draining the battery when the band was away or Bluetooth off.

- gps.js _startHrMonitor: drop the auto ble_connect; keep only the cheap HR
  poll so a manual connect (now or mid-ride) lands HR in the track.
- Settings: a 'Подключить' button for the paired band, usable any time incl.
  during a ride. BT off -> ble_request_enable (system enable dialog); on ->
  ble_connect, start the poll if recording, report connected / not found.
- ble.rs: ble_request_enable command (+ handler registration).
- MainActivity: ble_request_enable -> ACTION_REQUEST_ENABLE (BLUETOOTH_CONNECT
  already in manifest).
- BleHrService: START_NOT_STICKY; if the 5s scan doesn't find the address,
  stopSelf instead of directConnect to an absent device.
This commit is contained in:
4aiman 2026-10-02 14:03:16 +03:00
commit dd1642b6e9
9 changed files with 121 additions and 19 deletions

View file

@ -72,7 +72,7 @@ class BleHrService : Service() {
ACTION_SCAN -> {
startForeground(NOTIFICATION_ID, buildNotification("Поиск BLE..."))
scanDevices()
return START_STICKY
return START_NOT_STICKY // don't let Android restart us into an empty null-intent churn
}
else -> {
val address = intent?.getStringExtra(EXTRA_ADDRESS)
@ -84,7 +84,7 @@ class BleHrService : Service() {
dataFile = File(appDataDir, "ble_hr_data.json")
connectDevice(address)
}
return START_STICKY
return START_NOT_STICKY // connect is user-initiated; no sticky restart
}
}
}
@ -184,8 +184,12 @@ class BleHrService : Service() {
android.os.Handler(mainLooper).postDelayed({
scanner.stopScan(scanCallback)
if (gatt == null) {
Log.d(TAG, "Scan timeout, trying direct connect to $address")
directConnect(adapter, address)
// Not seen in the scan → the band isn't nearby/on. Do NOT hold a GATT connect to
// an absent device (that keeps the radio busy and drains the battery). Give up;
// the user re-taps "Подключить" once the band is on.
Log.d(TAG, "Scan timeout, device $address not found - stopping")
writeData("""{"status":"not_found","ts":${System.currentTimeMillis()/1000}}""")
stopSelf()
}
}, 5000)
}

View file

@ -507,6 +507,13 @@ class MainActivity : TauriActivity() {
intent.action = BleHrService.ACTION_STOP
startService(intent)
}
cmd == "ble_request_enable" -> {
Log.d("BalDav", "Requesting Bluetooth enable")
if (ensureBlePermissions()) {
try { startActivity(Intent(android.bluetooth.BluetoothAdapter.ACTION_REQUEST_ENABLE)) }
catch (e: Exception) { Log.e("BalDav", "BT enable request failed: ${e.message}") }
}
}
cmd == "check_update" -> {
checkForUpdate(appDataDir)
}

View file

@ -438,6 +438,21 @@ pub async fn ble_disconnect(
}
}
// Ask the OS to turn Bluetooth on (system ACTION_REQUEST_ENABLE dialog). BLUETOOTH_CONNECT is
// already granted, so no extra permission. Called when the user taps "Подключить" with BT off.
#[tauri::command]
pub async fn ble_request_enable(
app: tauri::AppHandle,
) -> Result<String, String> {
#[cfg(not(target_os = "android"))]
{ let _ = &app; Ok("desktop: no-op".to_string()) }
#[cfg(target_os = "android")]
{
android::send_command(&app, "ble_request_enable")?;
Ok("Requesting enable".to_string())
}
}
#[tauri::command]
pub fn ble_get_hr(
app: tauri::AppHandle,

View file

@ -1154,7 +1154,7 @@ pub fn run() {
.add_migrations("sqlite:baldav.db", migrations)
.build(),
)
.invoke_handler(tauri::generate_handler![get_safe_insets, import_seed_products, import_tiles, download_file, get_points_filename, delete_points_file, gps_set_session_id, list_points_files, read_points_file, pick_file_open, get_picked_file, debug_native_paths, rebuild_roads, get_road_points, get_ride_graph, get_app_info, gps_service_command, gps_service_read_points, brouter_route, brouter_download_tile, brouter_download_status, brouter_list_tiles, save_file, export_image, read_file_base64, export_db, import_db, restart_app, get_update_info, http_request, chzn_http, ble::ble_scan, ble::ble_scan_results, ble::ble_connect, ble::ble_disconnect, ble::ble_get_hr, ble::ble_is_connected, ble::ble_check_bluetooth, ble::ble_save_device, ble::ble_clear_hr])
.invoke_handler(tauri::generate_handler![get_safe_insets, import_seed_products, import_tiles, download_file, get_points_filename, delete_points_file, gps_set_session_id, list_points_files, read_points_file, pick_file_open, get_picked_file, debug_native_paths, rebuild_roads, get_road_points, get_ride_graph, get_app_info, gps_service_command, gps_service_read_points, brouter_route, brouter_download_tile, brouter_download_status, brouter_list_tiles, save_file, export_image, read_file_base64, export_db, import_db, restart_app, get_update_info, http_request, chzn_http, ble::ble_scan, ble::ble_scan_results, ble::ble_connect, ble::ble_disconnect, ble::ble_get_hr, ble::ble_is_connected, ble::ble_check_bluetooth, ble::ble_save_device, ble::ble_clear_hr, ble::ble_request_enable])
.run(tauri::generate_context!())
.expect("error while running tauri application");
}