Workout HR: log samples natively so recording survives screen-off

The JS 2s poll that stored workout HR samples is frozen by Android when the
screen is off, so a sleep-long recording kept only the on-screen stretches
(the strap and live display were fine — the persist path was the gap). The
foreground service now appends every reading to ble_hr_log.jsonl; ble_read_hr_log
exposes it; on stop the workout rebuilds its samples from that log (falling back
to the poll array). ble_clear_hr truncates the log at start.
This commit is contained in:
4aiman 2026-10-20 23:09:50 +03:00
commit ee47af4479
6 changed files with 98 additions and 5 deletions

View file

@ -881,6 +881,33 @@ GPS._stopHrMonitor = function() {
try { window.__TAURI__.core.invoke("ble_clear_hr"); } catch {}
};
// Rebuild the workout's HR series from the NATIVE log (ble_hr_log.jsonl), which the
// foreground service appends to even with the screen off — unlike the JS poll, which
// Android freezes in the background (so a sleep-long workout kept only the on-screen bits).
// Returns [[relSec, bpm], …] relative to the recording start, sorted and de-duplicated,
// or [] if the log is empty/unavailable.
GPS._readHrLogSamples = async function() {
if (!GPS.recording.startTime) return [];
let raw = "";
try { raw = await window.__TAURI__.core.invoke("ble_read_hr_log"); } catch { return []; }
if (!raw) return [];
const startSec = GPS.recording.startTime.getTime() / 1000;
const out = [];
let lastRel = -1;
for (const line of raw.split("\n")) {
const s = line.trim();
if (!s) continue;
let o; try { o = JSON.parse(s); } catch { continue; } // skip a torn last line
if (!o || !(o.hr > 0) || o.ts == null) continue;
const rel = Math.round(o.ts - startSec);
if (rel < 0 || rel === lastRel) continue; // before start, or same-second dup
out.push([rel, o.hr]);
lastRel = rel;
}
out.sort((a, b) => a[0] - b[0]);
return out;
};
// Turn HR on/off during a recording (the toggle on the record screen). ON connects to the
// paired strap (Kotlin scans ~5s and stops cleanly if it's absent — so no constant scanning /
// drain when the strap isn't there) and polls; OFF stops the poll and disconnects.
@ -1550,7 +1577,13 @@ GPS.stopRecording = async function() {
// HR-only workout: no GPS to stop. Compute net kcal from HR, save samples, finalise.
if (GPS.recording.activityType === "workout") {
const samples = GPS.recording.hrSamples || [];
// Prefer the native HR log (complete through screen-off) over the JS poll's in-memory
// array (frozen in the background). Fall back to the poll array if the log is empty.
let samples = GPS.recording.hrSamples || [];
try {
const native = await GPS._readHrLogSamples();
if (native.length > samples.length) samples = native;
} catch (e) { console.error("[workout] native hr log read", e); }
const kcal = GPS.hrKcalNet(samples, GPS.recording.hrProfile);
const durationSec = GPS.recording.startTime ? Math.round((Date.now() - GPS.recording.startTime.getTime()) / 1000) : 0;
const hrs = samples.map(s => s[1]);