diff --git a/docs/session-logs/2026-10-30_barometer-ble-baldav.md b/docs/session-logs/2026-10-30_barometer-ble-baldav.md deleted file mode 100644 index ce991ec..0000000 --- a/docs/session-logs/2026-10-30_barometer-ble-baldav.md +++ /dev/null @@ -1,51 +0,0 @@ -# 2026-10-30 - BLE-барометр: сторона baldav - -## Контекст -Делаем самодельный датчик высоты: ESP32-C3 SuperMini + BMP280, вещает давление и -температуру по BLE (стандартный Environmental Sensing). Железо заказано, ещё едет. -Прошивка датчика - в `tools/baro-sensor/` (PlatformIO + Arduino-скетч), схема сборки - -`tools/baro-sensor/baro-guide.html`. Эта задача - прочитать датчик в baldav и считать высоту. - -## Что сделано -Читаем барометр тем же путём, что и пульсометр (Kotlin BLE-сервис -> файл -> Rust -> JS), -новым `device_type = "baro"`. Диспетчер команд (`MainActivity`) не менялся - тип проходит насквозь. - -- `BleHrService.kt`: тип `baro` - подписка на ESS (`0x181A`): давление `0x2A6D` (uint32, 0.1 Па) - и температура `0x2A6E` (int16, 0.01 C). Пишет `ble_baro_data.json` + `ble_baro_log.jsonl`. - Все ветки барометра под `deviceType == "baro"`; HR/GPS-пути не тронуты. -- `ble.rs`: структура `BaroReading`, определение ESS, команда `ble_get_baro` - (Android читает файл; десктоп возвращает None). Зарегистрирована в `lib.rs`. -- `frontend/js/baro.js`: ПАССИВНЫЙ модуль `window.BARO`. Гипсометрический пересчёт давления - в высоту с реальной температурой датчика, якорь к высоте GPS на старте. Ничего не исполняется, - пока не вызван `BARO.start()` - без барометра приложение работает как раньше. - -## Состояние -Проверено компиляцией (сборка 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). -- UI: показать барометрическую высоту; выбор baro-устройства в scan-UI уже работает (тип проходит). -- Ограничение: сервис держит одно BLE-соединение, поэтому барометр и пульсометр одновременно - пока нельзя - отдельный следующий шаг (две связи / второй сервис). diff --git a/frontend/css/styles.css b/frontend/css/styles.css index a4e21d9..276915a 100644 --- a/frontend/css/styles.css +++ b/frontend/css/styles.css @@ -751,12 +751,7 @@ select { .full-width { width: 100%; } /* HR-only workout (зарядка) recording screen: big heart rate, timer, kcal, no map. */ -.workout-screen { display: flex; flex-direction: column; height: 100%; } -.wk-main { flex: 1; display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 4px; padding: 16px; } -.wk-panel { flex-shrink: 0; display: flex; flex-direction: column; padding: 10px 16px; padding-bottom: calc(10px + var(--safe-area-bottom, 0px)); } -/* Workout detail (in a modal): fill the scrollable body so the delete button sits at the bottom. */ -.wkd-wrap { display: flex; flex-direction: column; min-height: 100%; } -.wkd-del { margin-top: auto; flex: none; align-self: stretch; } +.workout-screen { padding: 24px 16px; display: flex; flex-direction: column; align-items: center; gap: 8px; } .wk-hr-big { font-size: 92px; font-weight: 700; line-height: 1; color: var(--primary); } .wk-hr-lbl { color: var(--muted); margin-top: -4px; } .wk-stats { display: flex; gap: 18px; margin: 14px 0; flex-wrap: wrap; justify-content: center; } @@ -1465,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(--on-surface); } +.rec-topbar .rec-top-mid { color: var(--muted); } .rec-lbl { font-size: 10px; diff --git a/frontend/index.html b/frontend/index.html index b13b9e1..f4550de 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -20,7 +20,6 @@ - diff --git a/frontend/js/baro.js b/frontend/js/baro.js deleted file mode 100644 index e99ffb0..0000000 --- a/frontend/js/baro.js +++ /dev/null @@ -1,116 +0,0 @@ -// Barometric altitude from the DIY BLE sensor "BaroBalDav" (ESP32-C3 + BMP280). -// PASSIVE module: it only defines window.BARO. Nothing runs until BARO.start() is called, so with -// no barometer connected the app behaves exactly as before. Activation (start on a "baro" connect, -// plus the getGpsAlt hook) is wired in main.js only when a baro device is in use. -// -// How it works: the Kotlin BLE service writes the latest {pressure_pa, temp_c, ts} to a file; the -// Rust command `ble_get_baro` returns it. We turn pressure into altitude with the hypsometric -// formula using the sensor's own temperature, and anchor the absolute value to the GPS altitude at -// ride start. The sensor is excellent for relative change (~1 m); weather drift of the absolute -// baseline is handled by the GPS anchor (optionally re-anchored slowly). -let BARO = {}; -(function () { - let R = 287.05; // specific gas constant for dry air, J/(kg*K) - let G = 9.80665; // standard gravity, m/s^2 - - let state = { - anchorPa: null, // reference pressure at the anchor point - anchorAltM: null, // absolute altitude (m) at the anchor point, from GPS - lastPa: null, // most recent pressure, Pa - lastTempC: null, // most recent chip temperature, C (for the hypsometric term) - lastTs: 0, // unix seconds of the last reading - lastAltM: null, // computed absolute altitude at the last reading - polling: false, - timer: null, - }; - BARO.state = state; - - // Height of a level at pressure `pa` above a level at pressure `refPa`, at air temperature tC. - // Hypsometric formula: dh = (R*T/g) * ln(refPa/pa). Measured temperature is used instead of a - // 15 C constant, so a hot or cold day does not bias the climb. Returns metres (positive = higher). - function deltaH(refPa, pa, tC) { - let T = (tC == null ? 15 : tC) + 273.15; - return (R * T / G) * Math.log(refPa / pa); - } - BARO.deltaH = deltaH; - - // Pin the barometric altitude to a known absolute altitude (from GPS) at a given pressure. - // Call once at ride start; may be re-called later to slowly correct weather drift. - BARO.anchor = function (gpsAltM, pa) { - if (pa == null) pa = state.lastPa; - if (gpsAltM == null || pa == null) return false; - state.anchorAltM = gpsAltM; - state.anchorPa = pa; - state.lastAltM = BARO.altitude(); - return true; - }; - - // Absolute altitude now = anchor altitude + barometric rise since the anchor. null until anchored. - BARO.altitude = function () { - if (state.anchorPa == null || state.lastPa == null) return null; - return state.anchorAltM + deltaH(state.anchorPa, state.lastPa, state.lastTempC); - }; - - // Fresh reading within 15 s? - BARO.isLive = function () { - return state.lastTs > 0 && (Date.now() / 1000 - state.lastTs) < 15; - }; - - // Override this to provide the current GPS altitude (m) for auto-anchoring; return null if no fix. - // Default: the altitude of the last recorded GPS point, if a recording is active. - BARO.getGpsAlt = function () { - try { - let pts = window.GPS && GPS.recording && GPS.recording.points; - if (pts && pts.length) { - let p = pts[pts.length - 1]; - let a = p.altitude_srtm_m != null ? p.altitude_srtm_m : p.altitude; - return a != null ? a : null; - } - } catch (e) {} - return null; - }; - - async function tick() { - let r = null; - try { r = await window.__TAURI__.core.invoke("ble_get_baro"); } catch (e) {} - if (r && r.pressure_pa > 0) { - state.lastPa = r.pressure_pa; - if (r.temp_c != null) state.lastTempC = r.temp_c; - state.lastTs = r.timestamp || Math.floor(Date.now() / 1000); - // auto-anchor on the first good reading, once GPS has an altitude to anchor to - if (state.anchorPa == null) { - let g = null; - try { g = BARO.getGpsAlt(); } catch (e) {} - if (g != null) BARO.anchor(g, state.lastPa); - } - state.lastAltM = BARO.altitude(); - } - } - BARO.tick = tick; - - // Begin polling the sensor (~1 Hz). No-op if already running. Call this only when a baro device - // is connected, so the app does nothing extra when no barometer is in use. - BARO.start = function () { - if (state.polling) return; - state.polling = true; - let loop = async function () { - if (!state.polling) return; - await tick(); - if (state.polling) state.timer = setTimeout(loop, 1000); // 1 s poll interval (a real delay) - }; - loop(); - }; - - BARO.stop = function () { - state.polling = false; - if (state.timer) { clearTimeout(state.timer); state.timer = null; } - }; - - // Clear the anchor and readings (e.g. between rides) so the next ride re-anchors fresh. - BARO.reset = function () { - state.anchorPa = null; state.anchorAltM = null; - state.lastPa = null; state.lastTempC = null; state.lastTs = 0; state.lastAltM = null; - }; - - window.BARO = BARO; -})(); diff --git a/frontend/js/db.js b/frontend/js/db.js index 2925738..eda3781 100644 --- a/frontend/js/db.js +++ b/frontend/js/db.js @@ -745,7 +745,6 @@ 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); } @@ -1677,7 +1676,6 @@ 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 ba79d19..9d9a288 100644 --- a/frontend/js/gps.js +++ b/frontend/js/gps.js @@ -311,8 +311,7 @@ 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. - // 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 rawAlt = sorted.map(p => 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; @@ -1171,15 +1170,6 @@ 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 f998d98..7e59ec0 100644 --- a/frontend/js/main.js +++ b/frontend/js/main.js @@ -1364,7 +1364,6 @@ function showFoodModal() { amountG = Math.round(rawAmount * selectedUnit.grams_per_unit * 10) / 10; } await DB.addFoodEntry(state.date, time, selectedProduct.id, amountG, null, unitId, unitAmount); - hydrationOnDrink(); // if it was liquid, quiet the reminder once you've drunk } else { let desc = overlay.querySelector("#m-desc").value.trim(); if (!desc) return false; @@ -1851,7 +1850,6 @@ async function renderProducts(container) { let note = overlay.querySelector("#m-note").value.trim() || null; if (isNaN(amount) || amount <= 0) return false; await DB.addFoodEntry(state.date, time, productId, amount, note); - hydrationOnDrink(); // quiet the drink reminder if this was liquid }); } @@ -2826,46 +2824,23 @@ let gpsMarker = null; // At session start, seed the native hydration deficit from the FOOD LOG: how far behind on water // you are for the time of day (need-so-far minus logged intake). During the session the native // service adds sweat on top. Intake comes from what you log, not a button. -// Sweat (extra water loss) per activity — cycling sweats far more than a walk, so a walk must -// NOT accrue the ride rate (that made it falsely hit the threshold after ~1 h of walking). -let HYDR_SWEAT = { bike: 600, walk: 200, run: 450, workout: 250, vr: 250 }; -function hydrSweatFor(type) { return HYDR_SWEAT[type] || 600; } - -// Compute the current sedentary shortfall (need-so-far for the time of day minus what you've -// logged drinking today). Shared by the session seed and the on-drink re-seed. -async function hydrBaseDeficit() { - let target = parseInt(state.profile?.daily_water_ml) || 2000; - let now = new Date(); - let nowMin = now.getHours() * 60 + now.getMinutes(); - let wakeStart = 7 * 60, wakeEnd = 23 * 60; // assume waking 07:00–23:00 - let frac = Math.max(0, Math.min(1, (nowMin - wakeStart) / (wakeEnd - wakeStart))); - let intake = await DB.getDailyWaterMl(now.toISOString().slice(0, 10)); - return Math.max(0, Math.min(5000, Math.round(target * frac - intake))); -} - -async function seedHydrationDeficit(activityType) { +async function seedHydrationDeficit() { if (localStorage.getItem("baldav_hydr_on") === "0") return; // ON by default unless turned off try { - let startDeficit = await hydrBaseDeficit(); + let target = parseInt(state.profile?.daily_water_ml) || 2000; + let now = new Date(); + let nowMin = now.getHours() * 60 + now.getMinutes(); + let wakeStart = 7 * 60, wakeEnd = 23 * 60; // assume waking 07:00–23:00 + let frac = Math.max(0, Math.min(1, (nowMin - wakeStart) / (wakeEnd - wakeStart))); + let needSoFar = target * frac; + let intake = await DB.getDailyWaterMl(now.toISOString().slice(0, 10)); + let startDeficit = Math.max(0, Math.min(5000, Math.round(needSoFar - intake))); let alertMl = parseInt(localStorage.getItem("baldav_hydr_alert")) || 600; await window.__TAURI__.core.invoke("set_hydration_config", - { enabled: true, alertMl, sweatMlPerHour: hydrSweatFor(activityType), reAlertMin: 15, startDeficitMl: startDeficit, seedSeq: Date.now() }); + { enabled: true, alertMl, sweatMlPerHour: 600, reAlertMin: 15, startDeficitMl: startDeficit }); } catch (e) { console.error("seedHydration", e); } } -// After you log a drink DURING a session: recompute the base deficit (now lower) and push it with -// a fresh seedSeq so the native side drops baseDeficit — the reminder goes quiet once you've drunk. -async function hydrationOnDrink() { - try { - if (!(GPS.recording && GPS.recording.active)) return; // only matters during a session - if (localStorage.getItem("baldav_hydr_on") === "0") return; - let base = await hydrBaseDeficit(); - let alertMl = parseInt(localStorage.getItem("baldav_hydr_alert")) || 600; - await window.__TAURI__.core.invoke("set_hydration_config", - { enabled: true, alertMl, sweatMlPerHour: hydrSweatFor(GPS.recording.activityType), reAlertMin: 15, startDeficitMl: base, seedSeq: Date.now() }); - } catch (e) {} -} - async function renderGpsScreen(container) { // Check Bluetooth state if BLE device is paired let bleAddr = await DB.getProfileValue("ble_device_addr"); @@ -2934,7 +2909,6 @@ async function renderGpsScreen(container) { `; @@ -2981,7 +2955,7 @@ async function renderGpsScreen(container) { heightCm: parseFloat(state.profile?.height_cm) || 178, } : null; await GPS.startRecording(type, wkg, hrProfile); - seedHydrationDeficit(type); + seedHydrationDeficit(); renderScreen("recording"); } }; @@ -2992,13 +2966,11 @@ async function renderGpsScreen(container) { let selBar = container.querySelector("#gps-sel-bar"); let selCount = container.querySelector("#gps-sel-count"); let selMergeBtn = container.querySelector("#gps-sel-merge"); - let selDeleteBtn = container.querySelector("#gps-sel-delete"); let refreshSel = () => { container.querySelectorAll(".gps-session-item").forEach(it => it.classList.toggle("selected", selected.has(it.dataset.sid))); if (selBar) selBar.hidden = !selMode; if (selCount) selCount.textContent = "Выбрано " + selected.size; - if (selMergeBtn) selMergeBtn.disabled = selected.size < 2; // merge needs 2+ - if (selDeleteBtn) selDeleteBtn.disabled = selected.size < 1; // delete works from 1 + if (selMergeBtn) selMergeBtn.disabled = selected.size < 2; }; let exitSel = () => { selMode = false; selected.clear(); refreshSel(); }; let toggleSel = (sid) => { @@ -3063,27 +3035,6 @@ async function renderGpsScreen(container) { selMergeBtn.disabled = false; selMergeBtn.textContent = "Объединить"; } }; - if (selDeleteBtn) selDeleteBtn.onclick = async () => { - if (selected.size < 1) return; - let ids = [...selected]; - let n = ids.length; - let word = (n % 10 === 1 && n % 100 !== 11) ? "трек" - : (n % 10 >= 2 && n % 10 <= 4 && (n % 100 < 10 || n % 100 >= 20)) ? "трека" : "треков"; - if (!(await appConfirm(`Удалить ${n} ${word}? Это необратимо.`, "Удалить"))) return; - try { - selDeleteBtn.disabled = true; selDeleteBtn.textContent = "Удаляю..."; - let done = 0; - for (let id of ids) { - try { await DB.deleteGpsSession(id); done++; } catch (e) { console.error("[bulk delete]", id, e); } - } - showToast(`Удалено: ${done}`); - exitSel(); - renderScreen("gps"); - } catch (e) { - showToast("Ошибка удаления: " + (e.message || e)); - selDeleteBtn.disabled = false; selDeleteBtn.textContent = "Удалить"; - } - }; // Pagination: "Показать ещё" loads the next page from the DB and appends it. let sessOffset = sessions.length; @@ -4801,7 +4752,6 @@ function renderWorkoutDetail(session) { let max = hrs.length ? Math.max(...hrs) : null; let kcal = Math.round(session.total_kcal || 0); let html = ` -
${session.activity_type === "vr" ? "Руки/VR" : "Зарядка"} · ${session.date} ${session.start_time ?? ""}
${GPS.formatDuration(durSec)}Время
@@ -4815,8 +4765,7 @@ function renderWorkoutDetail(session) {
Пульс
- -
`; + `; showModal(html, () => {}, (overlay) => { let ctx = overlay.querySelector("#wk-hr-chart"); if (ctx && samples.length) { @@ -4843,21 +4792,17 @@ function renderWorkoutDetail(session) { function renderWorkoutInline(container) { container.innerHTML = `
-
-
--
-
уд/мин
-
Подключите пульсометр
-
-
-
-
0:00Время
-
0ккал
-
--средний
-
--макс
-
- - +
--
+
уд/мин
+
+
0:00Время
+
0ккал
+
--средний
+
--макс
+
Подключите пульсометр
+ +
`; let hrEl = container.querySelector("#wk-hr"), timeEl = container.querySelector("#wk-time"); @@ -4901,8 +4846,8 @@ function renderWorkoutInline(container) { let res = await GPS.stopRecording(); if (res && res.sessionId) { showToast(`Зарядка: ${res.kcal} ккал, ${GPS.formatDuration(res.durationSec)}, пульс ${res.avgHr ?? "--"}/${res.maxHr ?? "--"}`); - } - renderScreen("gps"); // back to the tracks list after stop, like a GPS ride (not into details) + showSessionDetail(res.sessionId); + } else { renderScreen("gps"); } }; } @@ -5500,10 +5445,9 @@ async function renderRecordingInline(container) { let median = sorted[Math.floor(sorted.length / 2)] || 0; if (spdEl) spdEl.textContent = median.toFixed(1); - // 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 ? " м·" : " м"); + // Elevation + let alt = p.altitude; + if (alt != null && elevEl) elevEl.textContent = Math.round(alt) + " м"; // Pause indicator if (GPS.recording.pauseStart) { @@ -5537,14 +5481,6 @@ 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) @@ -7845,34 +7781,18 @@ 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-подключение за раз.
-
- -