Strap usage tracking: accrue real HR-delivering time + battery steps natively
The strap's battery % is coarse/laggy (reads 100% for >24h), so to judge real battery life we accumulate the time it actually delivered HR plus every battery step into strap_uptime.json in the foreground service (persistent across sessions). ble_read_strap_uptime / ble_reset_strap_uptime expose and reset it; Settings shows hours-since-charge + steps with a Charged reset button.
This commit is contained in:
parent
9bc71b19c9
commit
abc4e3cef0
6 changed files with 174 additions and 4 deletions
|
|
@ -20,6 +20,8 @@ import android.os.ParcelUuid
|
|||
import android.util.Log
|
||||
import java.io.File
|
||||
import java.util.UUID
|
||||
import org.json.JSONArray
|
||||
import org.json.JSONObject
|
||||
|
||||
class BleHrService : Service() {
|
||||
|
||||
|
|
@ -65,6 +67,13 @@ class BleHrService : Service() {
|
|||
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)
|
||||
// Strap usage/battery accounting — persistent across sessions (strap_uptime.json), so we can
|
||||
// compute real battery life ourselves and see the battery-level steps over time.
|
||||
private var uptimeFile: File? = null
|
||||
private var lastReadingMs = 0L // for accruing only time the strap was actually delivering HR
|
||||
private var pendingActiveMs = 0L
|
||||
private var lastUptimeFlushMs = 0L
|
||||
private var lastBatLogged = -1
|
||||
private var lastBattery: Int = -1 // strap charge %, -1 = unknown
|
||||
private var scanResultsFile: File? = null
|
||||
private var fitproWriteChar: BluetoothGattCharacteristic? = null
|
||||
|
|
@ -124,6 +133,7 @@ class BleHrService : Service() {
|
|||
val appDataDir = filesDir.parentFile ?: filesDir
|
||||
dataFile = File(appDataDir, "ble_hr_data.json")
|
||||
logFile = File(appDataDir, "ble_hr_log.jsonl")
|
||||
uptimeFile = File(appDataDir, "strap_uptime.json")
|
||||
// fresh user-initiated connect: arm reconnect only after it actually connects
|
||||
lastAddress = address; stopping = false; connectedOnce = false; reconnectTries = 0
|
||||
connectDevice(address)
|
||||
|
|
@ -528,11 +538,48 @@ class BleHrService : Service() {
|
|||
// recorder rebuilds its samples from here (ble_read_hr_log) instead of the JS poll.
|
||||
if (json.contains("\"hr\":")) {
|
||||
try { logFile?.appendText(json + "\n") } catch (e: Exception) { Log.e(TAG, "Append hr log failed", e) }
|
||||
accrueUptime()
|
||||
}
|
||||
}
|
||||
|
||||
// Accrue strap "actually delivering HR" time + battery-level steps into strap_uptime.json
|
||||
// (persistent — NOT cleared per session). Flushed every ~20s (read-modify-write, small file).
|
||||
private fun accrueUptime() {
|
||||
val now = System.currentTimeMillis()
|
||||
val dt = now - lastReadingMs
|
||||
if (lastReadingMs > 0L && dt in 1..10000) pendingActiveMs += dt // only count gap-free stretches
|
||||
lastReadingMs = now
|
||||
if (now - lastUptimeFlushMs < 20000) return
|
||||
writeUptimeFile()
|
||||
}
|
||||
|
||||
// Persist accrued active time + battery step to strap_uptime.json (read-modify-write).
|
||||
private fun writeUptimeFile() {
|
||||
val now = System.currentTimeMillis()
|
||||
lastUptimeFlushMs = now
|
||||
val f = uptimeFile ?: return
|
||||
try {
|
||||
val obj = if (f.exists() && f.length() > 0) JSONObject(f.readText()) else JSONObject()
|
||||
obj.put("active_sec", obj.optDouble("active_sec", 0.0) + pendingActiveMs / 1000.0)
|
||||
if (!obj.has("since")) obj.put("since", now / 1000)
|
||||
val bat = lastBattery
|
||||
if (bat in 0..100) {
|
||||
obj.put("bat", bat)
|
||||
if (bat != lastBatLogged) {
|
||||
val steps = obj.optJSONArray("steps") ?: JSONArray()
|
||||
steps.put(JSONObject().put("ts", now / 1000).put("bat", bat))
|
||||
obj.put("steps", steps)
|
||||
lastBatLogged = bat
|
||||
}
|
||||
}
|
||||
f.writeText(obj.toString())
|
||||
pendingActiveMs = 0L
|
||||
} catch (e: Exception) { Log.e(TAG, "uptime flush: ${e.message}") }
|
||||
}
|
||||
|
||||
private fun disconnect() {
|
||||
isRunning = false
|
||||
try { writeUptimeFile() } catch (e: Exception) {} // persist the last accrued strap time
|
||||
handler.removeCallbacksAndMessages(null) // no stray callback may touch GATT after this
|
||||
try { gatt?.disconnect() } catch (e: Exception) { Log.e(TAG, "gatt.disconnect: ${e.message}") }
|
||||
try { gatt?.close() } catch (e: Exception) { Log.e(TAG, "gatt.close: ${e.message}") }
|
||||
|
|
|
|||
|
|
@ -477,6 +477,32 @@ pub fn ble_read_hr_log(app: tauri::AppHandle) -> Result<String, String> {
|
|||
{ Ok(android::read_file(&app, "ble_hr_log.jsonl").unwrap_or_default()) }
|
||||
}
|
||||
|
||||
// Strap usage accounting (strap_uptime.json): total time the strap actually delivered HR plus
|
||||
// battery-level steps, accumulated by the foreground service across sessions (persistent — so we
|
||||
// can compute real battery life ourselves). Returns "{}" if none yet.
|
||||
#[tauri::command]
|
||||
pub fn ble_read_strap_uptime(app: tauri::AppHandle) -> Result<String, String> {
|
||||
#[cfg(not(target_os = "android"))]
|
||||
{ let _ = &app; Ok("{}".to_string()) }
|
||||
#[cfg(target_os = "android")]
|
||||
{ Ok(android::read_file(&app, "strap_uptime.json").unwrap_or_else(|| "{}".to_string())) }
|
||||
}
|
||||
|
||||
// Reset the usage counter — call when the strap is put on charge / newly charged.
|
||||
#[tauri::command]
|
||||
pub fn ble_reset_strap_uptime(app: tauri::AppHandle) -> Result<(), String> {
|
||||
use tauri::Manager;
|
||||
let data_dir = app.path().app_data_dir().map_err(|e: tauri::Error| e.to_string())?;
|
||||
let now = std::time::SystemTime::now()
|
||||
.duration_since(std::time::UNIX_EPOCH).unwrap_or_default().as_secs();
|
||||
let fresh = format!("{{\"active_sec\":0,\"since\":{},\"steps\":[]}}", now);
|
||||
let _ = std::fs::write(data_dir.join("strap_uptime.json"), &fresh);
|
||||
if let Some(root) = data_dir.parent().and_then(|p| p.parent()) {
|
||||
let _ = std::fs::write(root.join("strap_uptime.json"), &fresh);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn ble_is_connected(
|
||||
app: tauri::AppHandle,
|
||||
|
|
|
|||
|
|
@ -1162,7 +1162,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, ble::ble_read_hr_log, ble::ble_request_enable, read_state_file])
|
||||
.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_read_hr_log, ble::ble_read_strap_uptime, ble::ble_reset_strap_uptime, ble::ble_request_enable, read_state_file])
|
||||
.run(tauri::generate_context!())
|
||||
.expect("error while running tauri application");
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue