- Tracks list: the long-press multi-select (used for merge) now also offers Удалить -
bulk delete of the selected tracks (1+; merge still needs 2+), same selection flow.
- HR/VR record screen: heart rate fills the main area (like the map on a GPS ride),
Подключить/Стоп moved into a bottom bar, consistent with the GPS screen.
- HR/VR workout detail: 'Удалить зарядку' pinned to the bottom (was floating in the
scroll flow and stretched).
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.
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.
- 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.
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.
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.
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.
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.
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.
- 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).
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.
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.
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).
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.
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.
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.
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.
- 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
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
- 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.
- 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.
- Стоп 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.