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:
4aiman 2026-10-20 23:49:17 +03:00
commit abc4e3cef0
6 changed files with 174 additions and 4 deletions

View file

@ -22,7 +22,7 @@
| Файл | Строк | Назначение |
|------|-------|------------|
| `js/main.js` | ~5900 | Экраны, навигация, модалки, графики, нутриенты, HR UI, компас (EMA + gimbal lock), ландшафт (3 режима), дороги. `appConfirm`/`appPrompt` (in-app диалоги вместо нативных, в стеке окон). Страница «Сохранённые маршруты», оверлей маршрута на записи и просмотре (`whenMapReady` — await готовности карты, не таймаут). Панель де-джиттера на детали трека (ползунки окно/итерации, живое превью, сохранить/откат, `reopenDetail` — переоткрытие детали без наложения оверлеев). `showRouteDetail` — предпросмотр маршрута (карта+профиль высоты+тап-высоты+фуллскрин), `showRouteEditor` — создание/правка маршрута (тап карты=точка в конец, тап линии=вставка в сегмент, тащить=двигать, тап точки=удалить; `makeMarker` возвращает маркер для push/splice). `appConfirm`/`appPrompt`/`appAlert` — in-app диалоги (нативных `alert/prompt/confirm` не осталось; `<select>` оставлены). `orient-dbg` (сенсор-оверлей на карте) — только при `window.__BALDAV_DEBUG`. `drawRoutePolyline`/`routeSlopeColor` (полосы уклона по 1%)/`routeElevColor` — топо/уклон-обводка в панели `routePane` (маршрут ПОД треком; на rotate-карте панель вешается внутрь `_rotatePane`, иначе маршрут не виден при записи). `fetchElevations` — цепочка GPXZ→open-elevation→open-meteo→OpenTopoData. Диагностические `console.log("[тег]...")` глушатся в release по `get_app_info().debug` (cfg!(debug_assertions)) |
| `js/db.js` | ~1550 | SQLite через Tauri IPC. Миграции, CRUD, 41 поле добавок, water_pct, reserves, sync. Хелперы маршрутов (routes CRUD, `updateRoutePoints`/`addRoute(...,waypoints)` — плотная геометрия + `waypoints_json` {waypoints,legMode} для правки планировщиком, `getRoutes.hasEle/.wpts`). `saveSmoothedTrack` — чанковый `CASE`-апдейт lat/lon_smoothed (18 UPDATE на 4366 точек вместо 4366), `setSessionSmoothed` — откат де-джиттера. `saveGapFill`/`revertGapFill` — обратимый гэп-филл: вставка достроенных точек с `estimated=1` (seq = seq предыдущей реальной точки) / удаление их; колонки `gps_points.estimated`, `gps_sessions.gap_filled`. Nogo: таблица `nogo_areas` (lat/lon/radius_m) + `getNogoAreas`/`addNogoArea`/`deleteNogoArea`. `runMigrations` -> стартовый seq-репейр для NULL-seq сессий (merge/split создают их намеренно): батчевый `CASE` по 300 точек (как `saveGapFill`), а не UPDATE на точку - иначе ~15000 IPC на старте = ~15с и не досохранялось, гонялось каждый запуск |
| `js/db.js` | ~1550 | SQLite через Tauri IPC. Миграции, CRUD, 41 поле добавок, water_pct, reserves, sync. Хелперы маршрутов (routes CRUD, `updateRoutePoints`/`addRoute(...,waypoints)` — плотная геометрия + `waypoints_json` {waypoints,legMode} для правки планировщиком, `getRoutes.hasEle/.wpts`). `saveSmoothedTrack` — чанковый `CASE`-апдейт lat/lon_smoothed (18 UPDATE на 4366 точек вместо 4366), `setSessionSmoothed` — откат де-джиттера. `saveGapFill`/`revertGapFill` — обратимый гэп-филл: вставка достроенных точек с `estimated=1` (seq = seq предыдущей реальной точки) / удаление их; колонки `gps_points.estimated`, `gps_sessions.gap_filled`. Nogo: таблица `nogo_areas` (lat/lon/radius_m) + `getNogoAreas`/`addNogoArea`/`deleteNogoArea`. `unmergeGpsSession`/`countMergedOrigins` — разделение склейки обратно по `orig_session_id` (парно к `mergeGpsSessions`): пересоздаёт сессии по исходным id, ретег точек, выставляет дату/время каждой из её точек; кнопка "Разделить склейку" в деталях трека (видна только если склейка). `runMigrations` -> стартовый seq-репейр для NULL-seq сессий (merge/split создают их намеренно): батчевый `CASE` по 300 точек (как `saveGapFill`), а не UPDATE на точку - иначе ~15000 IPC на старте = ~15с и не досохранялось, гонялось каждый запуск |
| `js/gps.js` | ~1650 | GPS запись, ккал (нетто-модель + MET-нетто floor, накат=базовый), altitude filter + гистерезис высоты, BLE HR polling, dead reckoning. Источник истины БД при живой записи — нативный файл (`_processIncomingPoint(p, fromNative)`: watchPosition только рисует, поллинг пишет). `recoverFromFiles` — восстановление точек с устройства (матч по id/дате + `_s<id>` в имени, дедуп ±2.5с, `recalculateSession(noDelete)`, чистка импортированных файлов). `recomputeAllStats` — пересчёт статы всех треков без изменения точек. `filterByAccuracy` — отбор по точности (minAccuracyM) внутри `computeStats`+`filterNoise`: неточные фиксы исключаются из расчёта/линии (обратимо, non-destructive; null/≥999 keep), bearing_deg. `smoothTrackSG(pts,W,it)` — де-джиттер Савицкого-Голея (квадратичный МНК по окну, итеративно, концы закреплены; lat/lon→метры→обратно, все поля точки сохраняются). `parseRouteGPX(xml)` — парсер маршрутов (`rtept`/`trkpt`/`wpt`, читает `<ele>`, DOMParser). `routeElevStats(pts)` — профиль маршрута (накопл. дистанция, грейд к след. точке, набор/сброс с гистерезисом, макс-уклон; высоты сглаживаются). `fillGaps(points)` → `{points, gaps}` — дорожная достройка GPS-дропаутов (BRouter shortest) с интерполяцией ts и флагом `estimated`; `gaps[].status` = filled/skipped-long/skipped-noroute для UI-предпросмотра. `config.gpxzKey` — ключ GPXZ (localStorage) |
| `js/sync.js` | ~300 | Синхронизация между устройствами (LAN, PHP сервер на роутере) |
| `js/chzn.js` | ~330 | Честный знак (CRPT). Скан DataMatrix → карточка Нацкаталога → префилл КБЖУ. `ChZ`: сессия в localStorage (`chzn_session`), `importFromText`, `refresh`/`ensureSid` (sid 48ч, refreshToken ~год), `authedPost/Get` (userId+sid в query, авто-refresh на 401), `check`, `nutritionInfo` (витамины, часто 204), `normalizeCode` (реконструкция GS 0x1D перед AI 93), `parseCard` (группа «Энергетическая ценность…», имена атрибутов разнятся у производителей), `resolve`. `/authorize/anonymous` закрыт Play Integrity — свою сессию не завести, только импорт из приложения (`tools/chzn_export.sh`) |
@ -42,7 +42,7 @@
| Файл | Строк | Назначение |
|------|-------|------------|
| `src/lib.rs` | ~790 | Миграции v1, IPC команды, seed import, update check, tile import, road map rebuild. `native_root()` — выбор корня приложения по наличию `gps_live_points.jsonl`/`gps_points_*` (на этом Tauri `app_data_dir()` = корень напрямую; старый `.parent().parent()` читал мимо, отсюда потеря фоновых точек). `http_request` (GET/POST, тело) и `chzn_http` (браузерный User-Agent + возврат `{status,body}` для 204/401/427, `http_status_as_error(false)`) — прокси HTTP мимо CORS WebView |
| `src/ble.rs` | ~480 | BLE HR: btleplug (desktop), file IPC (Android), FitPro + Standard HR. `ble_read_hr_log` — читает нативный лог ЧСС `ble_hr_log.jsonl` (пишет foreground-сервис, переживает гаснущий экран); `ble_clear_hr` чистит и его |
| `src/ble.rs` | ~480 | BLE HR: btleplug (desktop), file IPC (Android), FitPro + Standard HR. `ble_read_hr_log` — читает нативный лог ЧСС `ble_hr_log.jsonl` (пишет foreground-сервис, переживает гаснущий экран); `ble_clear_hr` чистит и его. `ble_read_strap_uptime`/`ble_reset_strap_uptime` — наработка ремня (время реальной отдачи ЧСС + шаги заряда, персистентно в `strap_uptime.json`, для расчёта ресурса батареи самими) |
| `src/main.rs` | 5 | Точка входа |
| `Cargo.toml` | — | tauri, tauri-plugin-sql, rusqlite, serde, btleplug, tokio, uuid |
| `tauri.conf.json` | — | Конфиг приложения, плагины, ресурсы |
@ -53,7 +53,7 @@
|------|-------|------------|
| `MainActivity.kt` | ~300 | FileObserver IPC, seed DB, safe insets, SAF, BLE commands, OTA check |
| `GpsRecorderService.kt` | ~530 | Foreground service GPS, WakeLock, JSONL (один файл на сессию до Стоп, идемпотентный onStartCommand при START_STICKY-перезапуске ОС; `bindSessionIfPending()` дописывает id сессии в имя файла `_s<id>` по `gps_session.txt`), 50Hz акселерометр/гироскоп, dead reckoning, raw sensor files |
| `BleHrService.kt` | ~390 | Foreground service BLE HR, FitPro protocol, GATT queue, scan/connect. START_NOT_STICKY; скан не нашёл адрес -> stopSelf (не держит радио). Подключение инициируется ТОЛЬКО вручную (кнопка в настройках), не при старте записи. Выключение BT в любой момент не роняет app: единый `handler` (все postDelayed, снимаются разом), `BroadcastReceiver` на `ACTION_STATE_CHANGED` -> на STATE_OFF чистая остановка (disconnect+stopSelf), все нативные BLE-вызовы (scan/stopScan/connectGatt/writeCharacteristic/discoverServices) в try/catch. Каждый замер ЧСС дописывается в `ble_hr_log.jsonl` (append) прямо в сервисе — запись истории переживает гаснущий экран (JS-поллинг в фоне заморожен Android); воркаут при остановке пересобирает сэмплы из этого лога (`GPS._readHrLogSamples`), а не из JS-массива. Авто-реконнект: обрыв УЖЕ установленного соединения (connectedOnce) переподключается через скан (до 3 попыток, 8с), сброс бюджета при успехе; если скан не нашёл ремень или это stopping (ACTION_STOP/BT-off/onDestroy) - stopSelf, не долбится в отсутствующий/не запущенный вручную ремень |
| `BleHrService.kt` | ~390 | Foreground service BLE HR, FitPro protocol, GATT queue, scan/connect. START_NOT_STICKY; скан не нашёл адрес -> stopSelf (не держит радио). Подключение инициируется ТОЛЬКО вручную (кнопка в настройках), не при старте записи. Выключение BT в любой момент не роняет app: единый `handler` (все postDelayed, снимаются разом), `BroadcastReceiver` на `ACTION_STATE_CHANGED` -> на STATE_OFF чистая остановка (disconnect+stopSelf), все нативные BLE-вызовы (scan/stopScan/connectGatt/writeCharacteristic/discoverServices) в try/catch. Каждый замер ЧСС дописывается в `ble_hr_log.jsonl` (append) прямо в сервисе — запись истории переживает гаснущий экран (JS-поллинг в фоне заморожен Android); воркаут при остановке пересобирает сэмплы из этого лога (`GPS._readHrLogSamples`), а не из JS-массива. Авто-реконнект: обрыв УЖЕ установленного соединения (connectedOnce) переподключается через скан (до 3 попыток, 8с), сброс бюджета при успехе; если скан не нашёл ремень или это stopping (ACTION_STOP/BT-off/onDestroy) - stopSelf, не долбится в отсутствующий/не запущенный вручную ремень. Наработка ремня: `accrueUptime`/`writeUptimeFile` копят время реальной отдачи ЧСС (только промежутки без разрывов) + шаги заряда в `strap_uptime.json` (персистентно, не чистится per-session, flush ~20с и при обрыве) |
| `generated/*.kt` | ~1100 | Tauri-сгенерированные: WebView, IPC, permissions |
### Инструменты (`tools/`)