From 9251c1dd90a0f59df9e18f1adfaaaff06c350c8f Mon Sep 17 00:00:00 2001 From: 4aiman Date: Fri, 30 Oct 2026 22:46:26 +0300 Subject: [PATCH] ble: barometer on its own connection - simultaneous HR+baro, auto-connect, UI MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Separate BleBaroService so the HR strap and the barometer each hold their own GATT link and work at the same time (one adapter, several LE connections). Verified on device against a PC BlueZ emulator of the sensor: BleHR and BleBaro deliver in parallel, no cross-talk. - BleBaroService: dedicated baro connection (scan-before-connect + direct-connect fallback, ESS subscribe, writes ble_baro_data.json). MainActivity routes baro connects to it and auto-connects both HR (ble_device.json) and baro (baro_device.json) at start. - Rust: ble_baro_disconnect, ble_save_baro_device. - Settings: a 'Датчики (BLE)' section - heart-rate and barometer as separate rows with their own slots, bind/connect/unbind, and a live pressure/temperature/altitude readout. - Tracking: altitude_baro_m stored per point, preferred in elevation stats (null without baro). - Record screen: 'Высота' from the barometer (1 Hz, dot marker), temperature from the sensor. - Fixes: direct-connect on scan timeout; Locale.US JSON (RU decimal comma broke the parse); parsing routed by characteristic (baro no longer written into the pulse); VR/HR stop returns to the tracks list instead of into details. - tools/baro-sensor/emulate_baro.py: PC BlueZ peripheral that emulates the sensor for testing. Emulator-verified only; real board still pending. --- .../2026-10-30_barometer-ble-baldav.md | 21 ++ frontend/css/styles.css | 2 +- frontend/js/db.js | 2 + frontend/js/gps.js | 12 +- frontend/js/main.js | 138 ++++++++-- .../android/app/src/main/AndroidManifest.xml | 5 + .../java/dev/baldav/app/BleBaroService.kt | 257 ++++++++++++++++++ .../main/java/dev/baldav/app/BleHrService.kt | 28 +- .../main/java/dev/baldav/app/MainActivity.kt | 29 +- src-tauri/src/ble.rs | 23 ++ src-tauri/src/lib.rs | 2 +- tools/baro-sensor/emulate_baro.py | 168 ++++++++++++ 12 files changed, 653 insertions(+), 34 deletions(-) create mode 100644 src-tauri/gen/android/app/src/main/java/dev/baldav/app/BleBaroService.kt create mode 100644 tools/baro-sensor/emulate_baro.py diff --git a/docs/session-logs/2026-10-30_barometer-ble-baldav.md b/docs/session-logs/2026-10-30_barometer-ble-baldav.md index ab14fd0..ce991ec 100644 --- a/docs/session-logs/2026-10-30_barometer-ble-baldav.md +++ b/docs/session-logs/2026-10-30_barometer-ble-baldav.md @@ -22,6 +22,27 @@ ## Состояние Проверено компиляцией (сборка 0.1.538, без ошибок). На устройстве НЕ проверено - нет железа. +## Продолжение того же дня: обвязка, UI, проверка эмулятором + +Железа ещё нет, поэтому эмулировали датчик на ПК (`tools/baro-sensor/emulate_baro.py` - +BlueZ D-Bus GATT-сервер, вещает ESS `0x181A` с давлением/температурой), телефон читал его +реальным путём. Проверено на устройстве: `BleHR: HR:70` и `BleBaro: Baro:... Pa` одновременно. + +Сделано: +- `BleBaroService` - отдельный сервис/соединение для барометра, работает параллельно с + пульсометром (один адаптер держит несколько LE-связей). Баро-подключения маршрутизируются + в него из `MainActivity`. Авто-подключение при старте: баро из `baro_device.json`, пульс из + `ble_device.json` (оба сами цепляются). +- Rust: `ble_baro_disconnect`, `ble_save_baro_device`. JS сохраняет baro-устройство при привязке. +- Настройки: секция "Датчики (BLE)" - пульсометр и барометр раздельно, свои слоты и кнопки, + живой показ давления/температуры/высоты. +- Трекинг: `altitude_baro_m` в точках, предпочитается в статистике высот (null без датчика). +- Экран записи: "Высота" от барометра (обновление 1 Гц, пометка точкой), температура с датчика + (белая, с десятой). +- Починки по пути: directConnect при таймауте скана; Locale.US в JSON (была запятая от RU-локали); + маршрутизация парсинга по характеристике (баро не писал в пульс); навигация после Стоп в VR/ЧСС + (на список, а не в детали). + ## Осталось (по приходу платы) - Активировать модуль: `BARO.start()` при подключении baro-устройства, задать `BARO.getGpsAlt`. - Привязать высоту барометра к треку поездки (в точки / поправка высот П.5). diff --git a/frontend/css/styles.css b/frontend/css/styles.css index 276915a..e573c76 100644 --- a/frontend/css/styles.css +++ b/frontend/css/styles.css @@ -1460,7 +1460,7 @@ body.dark-map .poi-badge { background: #111; border-color: #eee; } background: var(--surface); } .rec-topbar .rec-top { font-size: 27px; font-weight: 700; color: var(--on-surface); } -.rec-topbar .rec-top-mid { color: var(--muted); } +.rec-topbar .rec-top-mid { color: var(--on-surface); } .rec-lbl { font-size: 10px; diff --git a/frontend/js/db.js b/frontend/js/db.js index eda3781..2925738 100644 --- a/frontend/js/db.js +++ b/frontend/js/db.js @@ -745,6 +745,7 @@ async function addGpsPoint(sessionId, ts, lat, lon, altGps, speedMs, seq, bleDat ["accel_rms", sensorData?.accel_rms], ["accel_max", sensorData?.accel_max], ["gyro_pitch", sensorData?.gyro_pitch], ["gyro_yaw", sensorData?.gyro_yaw], ["bearing_deg", sensorData?.bearing], ["accuracy_m", sensorData?.accuracy], + ["altitude_baro_m", sensorData?.altitude_baro_m], ]; for (const [col, val] of extra) { if (_gpsPointCols.has(col)) { cols.push(col); vals.push(val ?? null); } @@ -1676,6 +1677,7 @@ async function runMigrations() { if (!gpsCols.includes("gyro_yaw")) await execute("ALTER TABLE gps_points ADD COLUMN gyro_yaw REAL"); if (!gpsCols.includes("bearing_deg")) await execute("ALTER TABLE gps_points ADD COLUMN bearing_deg REAL"); if (!gpsCols.includes("accuracy_m")) await execute("ALTER TABLE gps_points ADD COLUMN accuracy_m REAL"); + if (!gpsCols.includes("altitude_baro_m")) await execute("ALTER TABLE gps_points ADD COLUMN altitude_baro_m REAL"); // barometric altitude from the BLE sensor (null without it) if (!gpsCols.includes("lat_smoothed")) await execute("ALTER TABLE gps_points ADD COLUMN lat_smoothed REAL"); if (!gpsCols.includes("lon_smoothed")) await execute("ALTER TABLE gps_points ADD COLUMN lon_smoothed REAL"); if (!gpsCols.includes("estimated")) await execute("ALTER TABLE gps_points ADD COLUMN estimated INTEGER DEFAULT 0"); // gap-fill (BRouter) points, reversible diff --git a/frontend/js/gps.js b/frontend/js/gps.js index 9d9a288..ba79d19 100644 --- a/frontend/js/gps.js +++ b/frontend/js/gps.js @@ -311,7 +311,8 @@ GPS.computeStats = function(points, weightKg, opts) { // e.g. [45, 45, 52, 45, 45] — the 52 is a GPS altitude glitch. // Prefer terrain (SRTM/open-elevation) altitude when available — GPS altitude is noisy // and the stuck-filter destroys its real signal. Terrain is clean and accurate for slope. - const rawAlt = sorted.map(p => p.altitude_srtm_m ?? p.altitude ?? p.altitude_gps_m ?? null); + // Prefer the barometer (best for real climbs/descents) when present, then terrain (SRTM), then GPS. + const rawAlt = sorted.map(p => p.altitude_baro_m ?? p.altitude_srtm_m ?? p.altitude ?? p.altitude_gps_m ?? null); const filteredAlt = rawAlt.slice(); for (let i = 1; i < filteredAlt.length - 1; i++) { if (filteredAlt[i] == null) continue; @@ -1170,6 +1171,15 @@ GPS.recoverFromFiles = async function(onProgress) { // live marker — the same fix is never written twice with different timestamps. GPS._processIncomingPoint = async function(p, fromNative) { if (!GPS.recording.active) return; + + // Attach barometric altitude to this point when the BLE baro sensor is live and anchored. + // Null without a barometer, so elevation stats fall back to GPS/SRTM exactly as before. + try { + if (window.BARO && BARO.isLive()) { + let ba = BARO.altitude(); + if (ba != null && isFinite(ba)) p.altitude_baro_m = ba; + } + } catch (e) {} // DB writes come only from the source of truth: native points on Android, or // watchPosition on desktop (no native service). const writeDb = !!fromNative || !GPS.recording.nativeService; diff --git a/frontend/js/main.js b/frontend/js/main.js index f40942e..4cee2f4 100644 --- a/frontend/js/main.js +++ b/frontend/js/main.js @@ -4871,8 +4871,8 @@ function renderWorkoutInline(container) { let res = await GPS.stopRecording(); if (res && res.sessionId) { showToast(`Зарядка: ${res.kcal} ккал, ${GPS.formatDuration(res.durationSec)}, пульс ${res.avgHr ?? "--"}/${res.maxHr ?? "--"}`); - showSessionDetail(res.sessionId); - } else { renderScreen("gps"); } + } + renderScreen("gps"); // back to the tracks list after stop, like a GPS ride (not into details) }; } @@ -5470,9 +5470,10 @@ async function renderRecordingInline(container) { let median = sorted[Math.floor(sorted.length / 2)] || 0; if (spdEl) spdEl.textContent = median.toFixed(1); - // Elevation - let alt = p.altitude; - if (alt != null && elevEl) elevEl.textContent = Math.round(alt) + " м"; + // Elevation - prefer the live barometer (smoother, tracks real climbs) over noisy GPS altitude + let baroAlt = (window.BARO && BARO.isLive()) ? BARO.altitude() : null; + let alt = (baroAlt != null) ? baroAlt : p.altitude; + if (alt != null && elevEl) elevEl.textContent = Math.round(alt) + (baroAlt != null ? " м·" : " м"); // Pause indicator if (GPS.recording.pauseStart) { @@ -5506,6 +5507,14 @@ async function renderRecordingInline(container) { // Update clock let clockEl = document.querySelector("#rec-clock"); if (clockEl) clockEl.textContent = new Date().toTimeString().slice(0, 5); + // Barometer readout at 1 Hz, independent of GPS fixes (so altitude/temp move smoothly). + if (window.BARO && BARO.isLive()) { + let ba = BARO.altitude(); + if (ba != null && elevEl) elevEl.textContent = Math.round(ba) + " м·"; // trailing dot = from the barometer + let tc = BARO.state ? BARO.state.lastTempC : null; + let tEl = document.querySelector("#rec-temp"); + if (tc != null && tEl) tEl.textContent = tc.toFixed(1) + "°"; // one decimal: visibly live + has the dot + } }, 1000); // Fetch temperature — try multiple APIs (mobile internet blocks foreign services) @@ -7806,18 +7815,34 @@ async function renderSettings(container) {
-
Пульсометр (BLE)
+
Датчики (BLE)
-
${state.profile.ble_device_name || "Не привязан"}
-
${state.profile.ble_device_addr ? state.profile.ble_device_addr : "Нажмите для поиска"}
+
Пульсометр: ${state.profile.ble_device_name || "не привязан"}
+
${state.profile.ble_device_addr ? state.profile.ble_device_addr : "найдите в поиске ниже"}
${state.profile.ble_device_addr ? `` : ""} ${state.profile.ble_device_addr ? `` : ""} -
+
+
+
Барометр высоты: ${state.profile.baro_device_name || "не привязан"}
+
${state.profile.baro_device_addr ? state.profile.baro_device_addr : "найдите в поиске ниже"}
+
+
+ ${state.profile.baro_device_addr ? `` : ""} + ${state.profile.baro_device_addr ? `` : ""} +
+
+
+
+
Поиск устройств
+
Пульсометр и барометр привяжутся каждый в свою строку. Пока работает одно BLE-подключение за раз.
+
+ +