HR: auto-connect at recording start if a strap is paired; HR on/off toggle replaces the wind button

Earlier I over-corrected: 'don't scan when the strap is absent' became
'never auto-connect'. Restore conditional auto: at recording start (ride or
workout) connect the paired strap and poll; absent strap is safe now
(BleHrService scans briefly and stops - no drain). No paired strap -> HR off.

- gps.js: GPS.setHrOn(on) - connect+poll / disconnect+stop; start auto-ons it.
- Record screen: the inconvenient 'Ветер' button is gone; in its place an
  HR on/off toggle (square, heart+pulse icon; grey off, red on) -> setHrOn.

(Also corrected a stale assumption: the phone DOES have Google services.)
This commit is contained in:
4aiman 2026-10-17 01:45:02 +03:00
commit f7ba059628
4 changed files with 64 additions and 9 deletions

View file

@ -44,6 +44,22 @@ project-hr-sensor.
- Заметка: аудио в WebView может требовать user-gesture; старт записи - это жест, должно
играть. Проверить на устройстве.
## Правка после теста: авто-ЧСС на старте + тоггл ВКЛ/ВЫКЛ вместо "Ветер"
Я перестарался: по просьбе "не искать, если браслета нет" убрал автоконнект ВООБЩЕ. Нужно было
оставить авто с проверкой наличия. Исправлено:
- `GPS.setHrOn(on)` (gps.js): ON - проверяет BT (off -> запрос включения), коннектит привязанный
датчик, запускает поллинг; OFF - стоп поллинг + disconnect. Безопасно: BleHrService при
отсутствии браслета коротко сканит и `stopSelf` (драйна нет).
- Старт записи (и GPS, и зарядки): если датчик привязан - авто `setHrOn(true)`. Так "надел
датчик -> начал -> подключился сам", без ручного коннекта каждый раз. Нет привязанного - ЧСС off.
- Экран записи: кнопка **"Ветер" убрана** (неудобна), на её месте **тоггл ЧСС** (квадрат с
сердечком+линией пульса; серый = выкл, красный = вкл), дёргает `GPS.setHrOn`.
- Ручная кнопка "Подключить" в настройках остаётся.
Факт-чек (я ошибся ранее): на телефоне Google-сервисы ЕСТЬ (`com.google.android.gms`, Play
Store) - значит системный диалог включения гео через Play Services доступен. Авто-вкл BT/гео по
запросу (попап + 2 настройки "не спрашивать") - отдельная задача, теперь подтверждено выполнимо.
## Проверка
Синтаксис JS ок. На телефоне: выбрать "Зарядка", Старт (без браслета покажет "Подключите
пульсометр"), подключить датчик -> пульс крупно + ккал растут; Стоп -> деталь с графиком.

View file

@ -725,6 +725,11 @@ select {
.wk-status { color: var(--muted); font-size: 13px; text-align: center; min-height: 18px; margin-bottom: 6px; }
.workout-screen .btn-primary, .workout-screen .btn-secondary { margin-top: 8px; width: 100%; }
/* HR on/off toggle on the record screen (replaces the wind button): grey = off, coloured = on. */
.rec-hr-toggle { color: #888; display: inline-flex; align-items: center; justify-content: center; min-width: 46px; padding: 6px 10px; }
.rec-hr-toggle svg { display: block; }
.rec-hr-toggle.active { color: #ef5350; background: rgba(239, 83, 80, 0.18); }
/* Progress fill for long-running action buttons: the background fills left-to-right by
--pct (0-100%, set from JS). The label stays on top and readable; the unfilled part keeps
the button's own background. Used by recompute / elevation-refine / roads-rebuild. */

View file

@ -868,6 +868,28 @@ GPS._stopHrMonitor = function() {
try { window.__TAURI__.core.invoke("ble_clear_hr"); } catch {}
};
// 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.
GPS.setHrOn = async function(on) {
GPS.recording.hrOn = !!on;
if (!on) {
GPS._stopHrMonitor();
try { await window.__TAURI__.core.invoke("ble_disconnect"); } catch {}
return false;
}
const addr = await DB.getProfileValue("ble_device_addr");
const type = await DB.getProfileValue("ble_device_type");
if (!addr) { GPS.recording.hrOn = false; return false; }
try {
const bt = await window.__TAURI__.core.invoke("ble_check_bluetooth");
if (bt === "off") { try { await window.__TAURI__.core.invoke("ble_request_enable"); } catch {} }
else if (bt !== "no_adapter") { try { await window.__TAURI__.core.invoke("ble_connect", { address: addr, deviceType: type || "hr_standard" }); } catch {} }
} catch {}
try { await GPS._startHrMonitor(); } catch {}
return true;
};
// Low-battery warning for the HR strap: announce (voice) and beep ONCE at 10% and 5%.
// Resets when the strap reads healthy again (recharged / battery replaced).
GPS._batAlerted = {};
@ -1386,7 +1408,11 @@ GPS.startRecording = async function(activityType, weightKg, profile) {
GPS.recording.sessionId = await DB.createGpsSession(
new Date().toISOString().slice(0, 10), new Date().toTimeString().slice(0, 5), "workout");
} catch (e) { console.error("[workout] createGpsSession failed:", e); }
try { await GPS._startHrMonitor(); } catch (e) { console.error("[HR] start error:", e); }
// Auto-connect the paired strap on start (workout = you want HR). Absent strap -> stops clean.
try {
const hrAddr = await DB.getProfileValue("ble_device_addr");
if (hrAddr) await GPS.setHrOn(true); else GPS.recording.hrOn = false;
} catch (e) { console.error("[HR] start error:", e); }
return;
}
@ -1409,8 +1435,13 @@ GPS.startRecording = async function(activityType, weightKg, profile) {
// Start native foreground service (Android)
await GPS._startNativeService();
// Start BLE HR monitor (auto-connect to paired device)
try { await GPS._startHrMonitor(); } catch (e) { console.error("[HR] start error:", e); }
// HR: default ON if a strap is paired — auto-connect + poll at start (the record screen has an
// on/off toggle). Safe: BleHrService scans briefly and stops if the strap isn't present, so no
// constant scanning/drain when it's absent. No paired strap -> HR stays off.
try {
const hrAddr = await DB.getProfileValue("ble_device_addr");
if (hrAddr) await GPS.setHrOn(true); else GPS.recording.hrOn = false;
} catch (e) { console.error("[HR] start error:", e); }
// Poll native service output file
if (GPS.recording.nativeService) {

View file

@ -4578,7 +4578,7 @@ async function renderRecordingInline(container) {
</div>
<div class="rec-bottom">
<div class="rec-status" id="rec-status">${GPS.recording.state === "waiting_gps" ? "Поиск GPS..." : GPS.recording.state === "ready" ? "Начните движение..." : ""}</div>
<button id="rec-wind" class="btn-secondary"${GPS.recording._currentWind ? ' style="background:#ffb74d;color:#1a1a1a"' : ""}>Ветер</button>
<button id="rec-hr-toggle" class="btn-secondary rec-hr-toggle${GPS.recording.hrOn ? " active" : ""}" title="Пульсометр вкл/выкл" aria-label="ЧСС"><svg viewBox="0 0 24 22" width="22" height="20" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 20S3 14.5 3 8.3C3 5.6 5 4 7.2 4 9 4 10.6 5.1 12 7 13.4 5.1 15 4 16.8 4 19 4 21 5.6 21 8.3c0 1.3-.4 2.5-1 3.7"/><path d="M2 13h5l2-3 2.4 6 2-3H22"/></svg></button>
<button id="rec-pause" class="btn-secondary">${GPS.recording.manualPaused ? "Продолжить" : "Пауза"}</button>
<button id="rec-stop" class="btn-primary gps-rec recording">Стоп</button>
</div>
@ -5118,11 +5118,14 @@ async function renderRecordingInline(container) {
// Headwind label toggle: tap to open a "wind in the face" interval, tap to close.
// Marks are ground truth for validating the from-track wind estimate — not kcal.
let windBtn = container.querySelector("#rec-wind");
if (windBtn) windBtn.onclick = () => {
let active = GPS.toggleWindMark();
windBtn.style.background = active ? "#ffb74d" : "";
windBtn.style.color = active ? "#1a1a1a" : "";
// HR on/off toggle (replaces the old "Ветер" button). ON connects the paired strap + polls;
// OFF disconnects. Grey = off, coloured = on.
let hrToggle = container.querySelector("#rec-hr-toggle");
if (hrToggle) hrToggle.onclick = async () => {
hrToggle.disabled = true;
await GPS.setHrOn(!GPS.recording.hrOn);
hrToggle.classList.toggle("active", !!GPS.recording.hrOn);
hrToggle.disabled = false;
};
// Pause button