Commit graph

279 commits

Author SHA1 Message Date
9251c1dd90 ble: barometer on its own connection - simultaneous HR+baro, auto-connect, UI
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 22:46:26 +03:00
82f3dc9d4a ble: read DIY barometer (BaroBalDav) over BLE + firmware
Self-made altitude sensor: ESP32-C3 SuperMini + BMP280 broadcasting pressure and
temperature over standard BLE Environmental Sensing. Read into baldav the same way as
the heart-rate strap (Kotlin BLE service -> file -> Rust -> JS), new device_type 'baro'.

- BleHrService: 'baro' type subscribes to ESS 0x181A (pressure 0x2A6D uint32 0.1 Pa,
  temperature 0x2A6E int16 0.01 C), writes ble_baro_data.json + ble_baro_log.jsonl.
  Every baro branch is gated by deviceType=='baro', so HR/GPS paths are untouched.
- ble.rs: BaroReading, ESS detection, ble_get_baro command (Android reads the file,
  desktop returns None). Registered in lib.rs.
- frontend/js/baro.js: passive window.BARO - hypsometric pressure->altitude using the
  sensor temperature, anchored to GPS altitude at ride start. Nothing runs until
  BARO.start(), so with no barometer the app is unchanged.
- tools/baro-sensor: firmware (PlatformIO main.cpp + Arduino sketch) that reads BMP280
  at max oversampling and advertises the ESS service.

Prep only: no hardware yet, verified by compile (build 0.1.538). Live wiring and
ride-track integration pending the board.
2026-10-30 00:38:12 +03:00
1101016ef4 hydration: spoken 'hydrate now' alert, per-activity sweat, drink reconciliation
- signal is now a loud TTS 'hydrate now' on the ALARM stream (TTS warmed at
  session start), so it is unmistakably BalDav and audible when the phone is not
  in view (walking/VR); the old ringtone was indistinguishable from a system alarm.
- sweat need scales by activity instead of a flat 600 ml/h: bike 600, walk 200,
  run 450, workout/vr 250 ml/h. Walking no longer nags every 15 min.
- deficit split into baseDeficit (seeded from the food log) + accrued sweat;
  logging a drink mid-session recomputes the base and re-seeds via seedSeq, so it
  silences the reminder. seedSeq=0 (profile save / init) never zeroes it mid-ride.
- persistent 'БалДав: попей воды (HH:MM)' notification left as a record.
2026-10-27 21:47:10 +03:00
1df97d3e73 brouter-web: dual-sided route coloring (slope offset-left, surface offset-right)
Per-segment offset polylines in pixels, rebuilt on zoom; surface from per-segment WayTags
(messages[i][9]), slope from point altitudes. Centre keeps the neutral black/white casing.
v1 — offset width/colours to tune visually.
2026-10-27 14:28:02 +03:00
06ffab66a1 trackme share: include POI near the ride (bbox of route+track), shown on the viewer 2026-10-27 14:18:00 +03:00
ab1259eb20 brouter-web: directional slope coloring (warm uphill / cool downhill to navy), signed grade 2026-10-27 14:09:04 +03:00
2cc16e0ed6 BRouter trekking: cost by surface (dirt < gravel < sand, approved ranking)
Surface was only paved/unpaved binary - gravel, dirt and sand cost the same. Add a
surfacepenalty into costfactor so a dirt track beats a gravel one of the same tracktype
(asphalt 0 < compacted .2 < ground/dirt/earth .4 < cobblestone/sett .6 < gravel .9 <
clay 1.3 < grass/wood 1.5 < sand/mud 5). Canonical lookup values only. Verified on the
home RouteServer: engages on unpaved, doesn't distort paved routes. Untagged-ford
avoidance by estimated_river_class was tried and dropped - it forced 5x detours (flags
riverside ways, not only crossings); untagged fords stay shown (wetcross) + future marks.
2026-10-27 14:06:22 +03:00
aae9c55e1a trackme share: round coords to 5 decimals (~1m) to shrink the payload 2026-10-27 01:10:15 +03:00
65a21543d2 Session log: trackme share (route/roads/track via RAM relay) + SMS permission fix 2026-10-27 00:55:45 +03:00
8a236bae3d Record screen: 'Отд' button — hand route/roads/track to the live viewer
shareRideContext() gathers the active route, ridden roads and track-so-far and POSTs
them to the server's RAM relay (/api/trackme/share); the viewer pulls them into their
own localStorage. One-shot — press again to resend after the track grows.
2026-10-27 00:10:59 +03:00
f91281a218 SMS: composer-only (ACTION_SENDTO), drop SEND_SMS permission
Reverting the SmsManager direct-send path and its runtime SEND_SMS request - the SMS
feature was meant to use the system SMS composer, which needs no permission. Keep only
the prefilled ACTION_SENDTO composer.
2026-10-26 22:12:31 +03:00
61cfe7c0ee BRouter danger: detect untagged fords via BRouter river-class heuristic
OSM doesn't tag many real fords (ford=yes). BRouter still flags them with a high
estimated_river_class on the crossing way. Flag a track/path/footway with
estimated_river_class>=5 and no bridge/tunnel as 'wetcross' (возм. брод, light cyan,
shown as a guess vs the solid-blue confirmed ford). Verified against a real untagged
ford on the Rodionovo route (47.4926,39.8610): river_class=5 on a track, now detected.
2026-10-26 22:07:41 +03:00
e12e19132d Docs: brouter Part B (show fords + adventure opt-in) done, verified on home portal 2026-10-26 21:48:36 +03:00
d25cb0de49 BRouter Part B: show fords/rough tracks on the route + adventure opt-in
- brouter_route gets a format param; routeBrouter returns formatAsGeoJson (WayTags per
  segment) when asked; BRouter.routeGeoJson + GPS.parseRouteGeoJson extract geometry +
  dangers (ford / grade5|bad-trail / ferry) from the tags
- route editor routes road legs via geojson, marks fords (cyan) and rough tracks
  (orange) on the map with a '⚠ брод/грунт' counter, so the rider sees the hell BEFORE
  riding it
- 'Броды: избегать/разрешить' toggle picks trekking vs trekking-adventure (allow_adventure)
  and re-routes; new trekking-adventure.brf profile (asset + staging)
Parser validated against the home RouteServer on a real ford (47.5387,40.3164).
2026-10-26 21:46:56 +03:00
640429a209 BRouter trekking: apply adventure penalty in costfactor (was a no-op in initialcost)
The ford/grade5/bad-trail penalty sat in the way initialcost and never changed the
route cost (verified on the home RouteServer: avoid and adventure gave identical cost).
Moved it into costfactor next to town/traffic penalties. Verified against a real ford
(47.5387,40.3164): avoid now detours 1.4km->6.2km with fords=0, adventure keeps the
1.4km ford crossing. Node-ford penalty (node initialcost) kept.
2026-10-26 21:38:17 +03:00
9d510931ad Docs: elevation/slope accuracy + brouter ford avoidance (plan, session log, README) 2026-10-26 21:11:25 +03:00
85aa87edb0 BRouter trekking: avoid fords / grade5 tracks / bad-visibility trails by default
Routed through icy fords and abandoned descents 3x. Penalise ford nodes & ways
(ford=yes|stepping_stones), tracktype=grade5 and trail_visibility=bad|horrible|no in
the trekking profile, gated by a new %allow_adventure% param (default false = avoid).
Both profile copies (asset + staging) kept in sync. NOTE: profile syntax must be
verified by routing on device; a bad profile makes routeBrouter error out (safe), not
produce bad routes.
2026-10-26 21:08:22 +03:00
0d5a5986ac Elevation correction from ridden tracks (SRTM base + median offset)
get_elev_correction (Rust): per ~20m cell, the median (altitude_gps_m − altitude_srtm_m)
offset over >= N distinct-session passes. SRTM stays the base; this is only the up/down
nudge where you've actually ridden. JS applies a smoothed per-cell delta to a route's
SRTM elevations in the preview (non-destructive; no coverage -> unchanged).
2026-10-26 21:08:22 +03:00
ac37f36a06 Reroute (↪): only detour when a nogo is actually ahead on the route
The button built a detour unconditionally and picked the target from the globally
nearest route point + 1500m, which on loops/off-route jumped to the start ('first
waypoint'). Now: find current progress on the route, use the compass bearing to pick
travel direction, and only reroute when a nogo sits on the route ahead within a
lookahead window - else toast 'впереди затыка нет'. Target is the first point clear
of nogos past the blockage.
2026-10-26 20:57:24 +03:00
80ddff9b72 Route grade: compute over a ~30m distance window, not adjacent points
DEM elevations quantise to 30m cells; dense points inside a cell share one value and
the whole rise lands on the single boundary segment -> a false steep spike in slope
colouring. Compute each segment's grade as a central difference over a >=30m window
(config gradeWindowM) so a cell step spreads across the cell and the colour reflects
the real grade. maxGrade uses the windowed value too.
2026-10-26 20:55:11 +03:00
27c60a460a Route slope coloring: colour descents by a cool fan (green->cyan->navy)
Slope was coloured by |grade| - up and down looked identical. Colour by signed
grade in travel-direction order: uphill keeps the warm fan (green->yellow->red->
brown->black), downhill drops through a cool fan (green->cyan->blue->navy). Legend
shows both strips.
2026-10-26 20:55:11 +03:00
4a77132317 Session log: trackme rename, menu security, auth forms, logout fix, record buttons 2026-10-26 19:55:01 +03:00
a68c298946 SMS: send only the Yandex Maps link so it fits in one message
Drop the 'Я тут: lat,lon' prefix - the Cyrillic forced UCS-2 (70-char SMS) and the
coords were redundant with the pin. The bare ASCII link is GSM-7 and fits in one SMS.
2026-10-26 19:28:08 +03:00
c70cc1e4a4 Record screen: LIVE + SMS as square buttons bottom-left; toggle live on the fly
- move the SMS button out of the bottom panel into a left-hand square-button group
  (bottom-left, opposite the right control stack), add a LIVE toggle next to it
- LIVE turns location sharing on/off right from the record screen, even if it was
  never enabled in the profile; warns if the token is still unset
- LiveShare re-reads its config every tick so the toggle takes effect within ~10s
2026-10-26 19:13:34 +03:00
738c4e81a1 Profile: render settings checkboxes as toggle switches
The .form-group input rule (width:100% + appearance:none) stretched every
checkbox into an empty full-width box. Add a .toggle class styling a checkbox
as a sliding on/off switch and apply it to the TEF, hydration and live-share
toggles in the profile modal.
2026-10-26 18:25:07 +03:00
1410062d85 Live location sharing (botovod) + SMS 'я тут' with Yandex Maps link
Feature 1 - opt-in live location:
- set_live_config Rust command writes live_share.json (url/token stay on device)
- LiveShare.kt singleton posts the current fix to /api/live/push every ~10s from the
  GPS flush loop, on a background thread; diagnostics in live_share_log.jsonl
- profile toggle + endpoint/token fields, off by default; server side is in botovod

Feature 2 - SMS 'я тут':
- send_sms / pick_contact / get_contact_result Rust commands
- MainActivity: system contact picker (no READ_CONTACTS), direct SmsManager send when
  SEND_SMS is granted else the prefilled system composer; SEND_SMS in the manifest
- contact config in profile (manual + picker), 'СМС' button on the record screen with
  an in-app confirm; text carries coords + a Yandex Maps pin link

README + session log updated.
2026-10-26 16:12:11 +03:00
762221d186 Hydration on by default + diagnostics + reliable alarm; calorie bank; balance chart scale
Hydration reminder: ON by default (native defaults on when hydration.json is
missing; JS writes the default config at startup; the profile toggle is
checked by default) so it works out of the box on a ride instead of needing a
hidden toggle. Adds hydration_log.jsonl diagnostics and swaps the ToneGenerator
for the real alarm ringtone via MediaPlayer on STREAM_ALARM, with the alarm
volume forced to max for the alert, so it is actually audible.

Calorie bank: cumulative sum of daily balance over the window (+kg) in the day
summary, answering "how much to eat up/under". Balance chart (weight vs balance)
y-axis now floats symmetrically about 0 (±500 floor, no fixed ceiling) so one
hard day no longer flattens the line.
2026-10-26 15:05:31 +03:00
9a8ef04230 Hydration: water intake from the food log, drop the "+вода" button
Intake is what you already log (liquids count via water_pct), so there is
nothing to tap mid-ride and no guessing an amount. At session start JS seeds
the native deficit from the food log — need-so-far for the time of day minus
logged intake — via set_hydration_config(start_deficit_ml); the service then
accrues sweat on top. Removes the +вода button, hydration_drink and water_drink.txt.
2026-10-24 01:29:30 +03:00
6183f0c529 Hydration reminder: level-based drink alerts during a ride or VR
A shared Hydration singleton (ticked by the GPS ride service and the HR/VR
service, deduped in one process) accrues the body's water need over an active
session and subtracts what you logged via the "+вода" button. It stays silent
while you keep up — can be quiet for hours — and only beeps LOUD on the alarm
stream (cuts through media volume) once the running deficit passes a threshold,
then re-beeps on a cooldown. Not a fixed timer.

Rust set_hydration_config / hydration_drink bridge JS settings and the "+вода"
button to hydration.json / water_drink.txt that the native service reads.
2026-10-24 01:13:43 +03:00
4637846839 Roads overlay: one polyline per segment + viewport culling (fix jank)
The per-pass thickness split fragmented each segment into a run per
weight change, turning a ~1.4k-segment network into ~84k polylines and
janking the map. Draw one polyline per segment at its median-pass width
instead (verified on device: 1430 vs 84083). Add buildRoadSegments
(segment + bbox) and cullRoadsInto (draw only segments meeting the padded
view, redraw on moveend/zoomend); the recording overlay now renders on a
canvas pane instead of SVG, and showRoadsMap culls too. Thickness is now
per-segment (median) rather than per-point — a deliberate simplification.
2026-10-23 01:11:15 +03:00
c7dd588f37 TEF in daily balance + separate HR-only "vr" (hands/VR) workout type
TEF (thermic effect of food): optional profile toggle, 10% of intake,
introduced so maintenance is preserved - base = BMR*actLevel*(1-tef),
then +tef*intake. On a deficit day expenditure is slightly lower (eat
less, digest less), which keeps the existing calibration rather than
inflating the deficit. Day summary shows "расход: база + переваривание
+ активность"; the 7-day trend uses it too.

New activity_type "vr" (Руки/VR): HR-only like "workout" (no GPS, kcal
from HR), tracked separately. GPS.isHrOnly() replaces the hardcoded
"workout" checks so both kinds share the no-GPS record/detail path.
2026-10-22 00:51:31 +03:00
76efc9238b Roads overlay: glue repeated passes, thicken line by ride count
rebuild_roads now counts how many times each ~2m cell was ridden
(by cell entry, not per point, so a slow crawl stays one pass) and
stores it in road_points.passes. get_road_points returns it; the
overlay draws each stretch at base width x min(3, 1 + 0.2*(passes-1)),
split into runs of equal thickness. Optional via the "Толщина дорог
по проездам" setting (default on). Both the recording-screen roads
toggle and the Дороги screen honour it.
2026-10-22 00:49:18 +03:00
1cb6efbee3 Track detail: reference calibrated-HR kcal figure (Keytel x personal factor)
HR methods over-read vs physics (ride) and weight (sedentary day); a rough
single factor ~0.65 reconciles both. Add config.hrCalibFactor (0.65) and show a
'ЧСС откалибр. (грубо)' figure next to the Keytel one in the calories block,
clearly labelled as a rough reference. Activity balance still uses physics.
2026-10-21 11:22:46 +03:00
435a0dd27c Tracks: un-merge a merged session back into its original rides (UI button)
A merged session keeps orig_session_id on every point, so split it back:
unmergeGpsSession recreates the original session rows by that id, retags points,
and sets each piece's date/times from its own points. A 'Разделить склейку (N)'
button shows in track detail only when the session is a merge; it un-merges and
recomputes each piece.
2026-10-20 23:49:17 +03:00
abc4e3cef0 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.
2026-10-20 23:49:17 +03:00
9bc71b19c9 BleHrService: auto-reconnect a dropped strap (bounded, never for an absent one)
On an unexpected disconnect of an established connection (connectedOnce, i.e.
services were discovered), reconnect via the scan path up to 3 times, 8s apart,
resetting the budget on each successful reconnect. The scan gives up (stopSelf)
if the strap isn't found, so we never hammer a band that wasn't present at start
or wasn't connected by hand. A user stop, Bluetooth off, or onDestroy sets a
stopping flag that suppresses reconnect.
2026-10-20 23:25:06 +03:00
ee47af4479 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.
2026-10-20 23:09:50 +03:00
85d5e47972 Bump version (build.sh output) 2026-10-19 23:16:39 +03:00
10ff635192 Session log and plan: builds for all OS uploaded to the router (build 506) 2026-10-19 23:16:33 +03:00
f2d39459e6 Builds: any mix of targets and flags, Windows icon, router upload per file
icon.ico is required by tauri-build for the Windows resource; made from the
existing icon.png. Docs describe build.sh (gitignored): targets combine freely
under one build number, -da makes only the APK debug (the phone runs
debug-signed builds), and BUILD_NUMBER on the router is written only with the
APK, since the phone's OTA reads it and would otherwise offer the old APK.
2026-10-19 23:05:54 +03:00
1d133d0067 Docs: desktop builds for Linux/Windows/macOS and router upload via build.sh
build.sh itself is gitignored; this records the new targets (linux, windows,
macos, release, upload), the router file names and the osxcross location so
the flow is reproducible.
2026-10-19 22:47:44 +03:00
3f092cced0 Tiles: heal blank tiles online (periodic OSM-reachability check + backfill)
The on-demand loader never retries a tile whose fetch failed, so holes from a
bad signal stayed blank even after the network recovered. Add a healer: every
30s, when OSM is reachable, re-fetch MISSING tiles (current zoom) for the
visible area and along the ridden route, cache them, and redraw. Gentle to OSM
(one-tile probe, 300ms throttle, <=40 fetches/cycle); respects offline mode.
Wired on record/detail/compare/roads maps, stopped on screen change.
2026-10-19 20:07:05 +03:00
d4dda3da0c Dark map: give the roads overlay the same invert/hue/brightness filter as tiles
Road colours were tuned for light tiles and blended into the inverted dark
tiles. Filter the roads pane identically: add the CSS rule for
.leaflet-roadsPane (record screen roads already use that pane), and on the
roads screen draw the polylines through a canvas renderer bound to a new
roadsPane so the same rule applies there too.
2026-10-19 19:26:20 +03:00
802b2cc6e9 Bump version (build.sh output) 2026-10-19 14:12:04 +03:00
eeca9a7d94 HR connect: one BT-enable prompt per start, only on user action
setHrOn gains an 'interactive' flag; it pops the OS enable-Bluetooth dialog
only on a user tap (record-screen toggle). Auto-calls at recording start no
longer re-request enable after the single ensureSystemReady prompt, so a
decline is not immediately followed by the system dialog and is not re-asked
until the next start.
2026-10-19 13:43:55 +03:00
13c61f506a BleHrService: survive Bluetooth being turned off (no native crash)
Single Handler for all postDelayed work, cancelled on teardown; a BT
ACTION_STATE_CHANGED receiver that cleanly stops the service on STATE_OFF;
try/catch around every native scan/connect/GATT call. Prevents the libc
destroyed-mutex abort when the adapter is disabled mid-connection.
2026-10-19 13:43:55 +03:00
1df24000a3 Track detail: HR chart as its own collapsible section + 'what if by HR' kcal comparison
- Speed/Elevation/Пульс are now collapsible <details> sections; the HR chart
  moved out of the Elevation block into its own 'Пульс' section (avg/max + chart),
  shown when the track has HR.
- Калории now also shows the Keytel HR-based kcal as a COMPARISON when HR is
  present ('По ЧСС (сравнение)'). The activity balance keeps the physics/MET
  number unchanged - this is only to see the difference.
2026-10-17 14:53:21 +03:00
87a7fccfcc Record screen: remove the status line + its black bar, tighten button padding, move ⛔/↪ off the zoom controls
- Drop the status line (and any text it showed) from the record screen; keep
  a no-op statusEl sink so existing writes don't crash. Removes the empty
  black bar above the buttons. Tighter .rec-bottom padding.
- The nogo (⛔) and reroute (↪) buttons had no position class, so they piled
  at the map top-left over the zoom controls. Give them stack slots.
2026-10-17 14:43:57 +03:00
69b36be591 Record buttons equal width (border-box + uniform padding); HR strap charge as a background fill on the HR toggle
- Стоп was narrower: buttons had different horizontal padding and weren't
  border-box, so flex-basis:0 didn't equalize them. Force box-sizing + one
  padding on all three .rec-bottom buttons.
- HR strap battery now paints the HR toggle's background bottom-up 0-100%
  (--bat), coloured green/amber/red by level (setHrBatteryFill), instead of
  a number; cleared when HR is toggled off.
2026-10-17 14:35:54 +03:00
e88c2756f7 Record screen: big digits in the top strip (match bottom), drop the black status-bar backdrop band and the extra control offset
Top strip time/temp/clock now use the same 27px bold as the bottom stats. The
old .rec-map-wrap::before status-bar backdrop (and the leaflet-top safe-area
margin) are gone - the top strip already backs the status bar, so that strip
showed as a black band between the bar and the map.
2026-10-17 14:30:42 +03:00