baldav/docs/gps-model-findings.md
4aiman a52e998fb6 BRouter: analysis + embed the offline routing engine (Stage 1)
Analysis in docs/brouter-findings.md: how BRouter knows roads (.rd5
segments), routes and reaches low elevation gain (profile uphill/downhill
cost + kinematics), the three call surfaces, and an evaluation of two ideas
— fill GPS gaps along roads, and offline waypoint route planning. License is
MIT; the engine has no desktop-only deps, so it embeds on Android ART.

Stage 1 (embed + bridge, compiles into the apk; on-device route not yet run):
- vendored brouter-1.6.3-all.jar into the Android module (dexes cleanly) and
  bundled the trekking/shortest profiles + lookups.dat as assets, copied to
  files/brouter/profiles2 on launch
- MainActivity: a brouter_command.txt handler on the existing FileObserver
  runs btools.router.RoutingEngine off the UI thread (recipe taken via javap
  from ServerHandler) and returns the track as GPX
- Rust brouter_route command: same file bridge as GPS (request + command,
  poll for the result), registered in the invoke handler
- frontend/js/router.js: BRouter.route(points, profile) -> GPX (parseRouteGPX)
- LICENSE-brouter (MIT notice) kept alongside the vendored engine

Segments (.rd5, ~13-23 MB per 5x5 tile) are delivered later (Stage 2, wired
download inside BalDav). IMPLEMENTATION_PLAN.md tracks the 5 stages.
2026-09-09 00:56:13 +03:00

353 lines
27 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Находки по расчётной модели GPS (2026-09-07)
Получено при независимом разборе базы `baldav.db` (156 поездок, 366 тыс. точек)
сторонним инструментом `tools/track/`. Константы там взяты из `GPS.computeStats`,
поэтому расхождения означают разницу в модели, а не в настройках.
Проверка сходимости: дистанция расходится с базой на **0.2%**, калории по формуле
приложения -- на **4%** (трек 4 сентября). То есть база и формула воспроизводятся,
и нижеперечисленное -- не ошибки чтения.
Пункты про **нулевую отметку высоты** и **паузу на подъёме** сюда не включены:
они уже обсуждены отдельно.
Файл состоит из двух частей. **Первая -- находки:** измеренные расхождения,
каждое с замером и по возможности с контрольной проверкой. **Вторая --
предположения:** варианты отслеживания ветра, составленные по просьбе
пользователя, с вопросами к вам в конце. Смешивать их не стоит: первая часть
проверена на данных, вторая -- это перечень возможностей, а не рекомендация.
---
## 1. Потолок качения 0.008 занижен втрое
`crr_from_accel`: при `accel_rms > 7` возвращается 0.008 и выше не поднимается.
0.008 -- это разбитый асфальт. Реальные грунты дают втрое больше.
**Замер.** Трек 6 сентября (`id 1788683393199`), километры 36.5-38.5: 1996 м,
ровно (уклон −0.1%), `accel_rms` медиана **15.1**, скорость 13.8 км/ч.
Пользователь описал место как траву и тракторную колею с ямами.
Обратное решение уравнения мощности относительно Crr:
| если мощность была | требуемый Crr |
|---|---|
| 90 Вт | 0.0198 |
| 110 Вт | 0.0252 |
| 130 Вт | 0.0307 |
Справочные значения: плотный грунт 0.012, щебень 0.020, трава и песок 0.035.
Замер попадает ровно в диапазон, соответствующий описанию.
**Контрольная проверка обратным знаком.** Те же вычисления на километрах 9-11
(25 км/ч, тряска 5.2) дают Crr отрицательным при 90-110 Вт -- значит там
мощность была около 150 Вт. Сплиты на тех километрах показывают 148-172 Вт.
Метод не завышает и не занижает систематически.
**Предлагаемая шкала** (взята нижняя граница замера -- лучше недосчитать):
< 3 -> 0.004 гладкий асфальт (как сейчас)
3-7 -> 0.006 смешанное (как сейчас)
7-11 -> 0.008 разбитый асфальт (как сейчас)
11-15 -> 0.012 плотный грунт (новое)
> 15 -> 0.020 трава, колея, щебень (новое)
**Влияние на трек 6 сентября:** средняя мощность 89 -> 108 Вт,
механическая работа 1233 -> 1585 кДж. То есть на грунтах модель
недосчитывала примерно четверть работы.
## 2. `max(physics, MET)` никогда не складывает, и подъёмы от этого страдают
На одних и тех же отрезках трека 6 сентября:
| участки | физика | MET | MET/физика |
|---|---|---|---|
| вся поездка | 1318 | 1764 | 1.34 |
| равнина, \|уклон\| < 1% | 727 | 1344 | **1.85** |
| в горку круче 2% | 400 | 165 | **0.41** |
Комментарий в коде говорит про «~2x занижение физики» -- на равнине это верно
(1.85), но по поездке в целом всего 1.34, потому что на подъёмах физика
выигрывает с большим запасом.
Изъян в том, что оценки не складываются. Биологические накладные расходы
(размахивание ногами, равновесие, гашение тряски корпусом) никуда не деваются
на подъёме, но там берётся физика и табличная оценка отбрасывается целиком.
**Направление ошибки известно: подъёмы недосчитываются.**
Осмысленная альтернатива, если решите менять: не максимум, а
`физика/КПД + накладные`, где накладные оцениваются табличным минимумом
для медленной езды. Требует калибровки, поэтому просто как направление.
## 3. Ходьба с велосипедом засчитывается как езда
На односкоростном передача даёт точный признак: 40/18 на колесе 24" --
это 4.26 м за оборот шатунов. Скорость ниже **7.7 км/ч** означает каденс
ниже 30 об/мин, а на таком каденсе велосипед не едет, он падает. Значит
человек идёт и ведёт или несёт его.
На треке 6 сентября таких кусков **0.66 км за 8:44 в движении** -- пользователь
подтвердил, что нёс велосипед руками в горку. Участок: км 56.31, длина 289 м,
набор 47.8 м, **уклон 16.6%**, 4.3 км/ч.
Побочный эффект для статистики: если этот кусок не отделять, он склеивается
с последующим ездовым подъёмом (км 56.60, 624 м, 3.8%, 12.3 км/ч) в один
«подъём 915 м, 7.8%, 7.7 км/ч», которого в природе не было.
Признак применим только когда `bikes.gear_count` пусто или 1. На многоскоростном
низкая скорость ничего не доказывает.
**По акселерометру это не различается, проверено:** нести велосипед в гору 3.24,
ехать по гладкому асфальту 2.88, идти пешком (сессия walk) 3.98. `accel_rms`
показывает дорогу, а не походку.
## 4. Набор высоты расходится с независимым пересчётом в обе стороны
| трек | в базе | независимый пересчёт |
|---|---|---|
| 6 сентября | 328 м | 528 м |
| 4 сентября | 314 м | 264 м |
Знак разный, значит это не простое смещение. Без модели рельефа проверить
нечем: попытка построить карту высот по своим же 156 поездкам провалилась --
перекрытие маршрутов всего 26% ячеек, а разброс высоты между поездками
в одной и той же точке имеет медиану **10 метров**.
**Поправка после уточнения у пользователя.** Изначально я счёл пустую
`altitude_srtm_m` (заполнена в одной сессии из 156) недоработкой. Это не так:
поле заполняется вручную кнопкой «уточнить высоты», которая ходит в GPXZ
и подобные сервисы, и пользователь жмёт её редко. Полагаться на сервисы
в поле нельзя -- РЭБ и РКН глушат, устойчиво работает преимущественно
проводной интернет.
Отсюда следует не «починить SRTM», а вопрос по существу: **расхождение
в наборе высоты остаётся неразрешённым, и разрешить его без внешнего рельефа
нечем.** Если это важно -- имеет смысл думать в сторону предварительной
загрузки рельефа по району дома, по проводу, один раз. Если не важно --
достаточно знать, что цифра набора имеет точность порядка десятков процентов,
и не строить на ней выводов. На расчёт мощности это влияет слабо: там работает
уклон, а он считается по окну и ограничен сверху.
## 5. Пульса в точках нет, и это ожидаемо
`gps_points.heart_rate` равен нулю во всех 156 сессиях. **Это не дефект
записи:** пользователь потерял браслет и пульсометр не носит, писать в поле
попросту нечего. `ble_hr_data.json` и запись устройства в `profile`
остались от прежнего использования.
Упомянуто только затем, чтобы никто не искал ошибку там, где её нет,
и чтобы разбор треков не обещал анализ по пульсу.
## 6. Ветер вычисляется из `bearing_deg`
Побочная находка, в приложении такого нет. Ветер виден по тому, что на ровных
однотипных дорогах скорость зависит от курса. Берутся только точки с ровным
уклоном, невысокой тряской и в седле, считается медианная скорость по восьми
секторам курса, и подбирается ветер, при котором подразумеваемая мощность
во всех секторах одинакова.
Трек 6 сентября: **6.3 км/ч из направления 345°**. Разброс мощности
по направлениям падает с 33% до 11%. Пользователь независимо подтвердил,
что ветер часто дул в лицо.
Медианная скорость по курсу на этом треке -- 23.6 км/ч на юго-восток
против 15.7 км/ч на северо-запад, при одинаковых дорогах.
Второй, независимый метод -- по встречным проездам одного места (758 пар)
-- даёт 3.6 км/ч из 325°. Направление сходится в пределах 20°, сила --
в пределах множителя 1.8. Это и есть достижимая точность оценки ветра
без датчика.
Разбор вариантов отслеживания ветра, включая идею пользователя про голосовые
отметки, -- во второй части этого файла. Там же перечислены вопросы,
по которым пользователь просит ваше мнение.
## 7. Ловушка при отделении ходьбы: порог по длительности не нужен
Найдено после замечания пользователя, что встречные проезды одного места
случались и пешком.
Первая реализация признака ходьбы требовала, чтобы медленный кусок длился
дольше 30 секунд подряд. Это оказалось неверно. На треке 6 сентября всего
**14.7 минут** движения медленнее 7.7 км/ч, из них сплошными кусками --
только 8.7 минуты. Оставшиеся **6 минут** -- короткие эпизоды, и они попадали
в статистику как педалирование.
Видно это по корзине каденса: «0-40 об/мин» занимала 21 минуту, и **6.4 минуты
из неё приходилось на каденс ниже 30**, которого при езде не бывает вовсе.
После исправления корзина стала 9 минут, а средний каденс поднялся с 64 до 67.
**Правильное разделение двух вопросов:**
- *Исключать из статистики* нужно **любую** точку ниже порога, без оглядки
на длительность: на этой передаче ниже 30 об/мин не едут, и точка. Сюда же
попадут обычные торможения до нуля, но они короткие и маломощные, вреда нет.
- *Называть кусок ходьбой в отчёте* стоит только при длительности дольше
30 секунд подряд -- иначе отчёт будет объявлять ходьбой каждое торможение
перед ямой.
На оценку ветра эта примесь, к счастью, не повлияла: там отдельный порог
9 км/ч, и пешие точки не проходили. Проверено прямым сравнением -- с ними
3.5 км/ч из 326°, без них 3.6 км/ч из 325°.
---
---
# ЧАСТЬ ВТОРАЯ: предложения по отслеживанию ветра
Ниже -- **не находки, а предположения**, составленные по просьбе пользователя.
Это перечень способов с замерами там, где замеры удалось сделать.
Решение принимает сессия разработки.
## Зачем
Ветер -- крупнейший неучтённый источник ошибки в расчёте мощности на равнине.
На треке 6 сентября медианная скорость на одинаковых дорогах составила
**23.6 км/ч на юго-восток против 15.7 км/ч на северо-запад**. Разброс
подразумеваемой мощности по направлениям -- **33%**. Пока ветра в модели нет,
эти 33% списываются на «просел» и «разогнался».
## Ограничения, названные пользователем
- **Сетевые сервисы в поле недоступны.** РЭБ и РКН глушат, устойчиво работает
преимущественно проводной интернет. Любая схема, которой нужна сеть во время
поездки, нерабочая.
- **Метеостанция меряет в другой точке.** Стандартное измерение -- на высоте
10 м в открытом месте, усреднение за 10 минут. Велосипедист едет на высоте
1.5 м среди лесополос, домов и насыпей, где ветер и слабее, и порывистее.
Среднее по области пользователю не поможет.
Оба возражения справедливы и отсекают наивное «дёргать погодный API».
## Вариант 1. Считать ветер из самого трека (без железа и без сети)
Реализовано и проверено в `tools/track/track.py`. Два независимых метода.
**1а. Подбор по секторам курса.** Берутся только точки на ровном участке,
с невысокой тряской, в седле. Медианная скорость считается по восьми секторам
направления. Подбирается такой ветер, при котором подразумеваемая мощность
во всех секторах одинакова.
**1б. Встречные проезды одного места.** Ищутся пары точек в одной ячейке 30 м,
разнесённые по времени, с противоположными курсами, одинаковым покрытием
и зеркальным уклоном. На одной и той же дороге качение и уклон сокращаются,
и разница скоростей при равной мощности задаёт составляющую ветра вдоль дороги.
Составляющие по разным дорогам сводятся в вектор методом наименьших квадратов.
**Результат на треке 6 сентября:**
| метод | скорость | направление |
|---|---|---|
| по секторам курса | 6.3 км/ч | 345° |
| по встречным проездам (758 пар) | 3.6 км/ч | 325° |
Направление сходится в пределах 20°, сила -- в пределах множителя 1.8.
**Это и есть достижимая точность способа без железа: направление уверенно,
сила -- с точностью до двух раз.** Любой вариант с датчиком должен
эту планку превосходить, иначе он не нужен.
Ограничения: работает только задним числом, требует разнообразия курсов
(кольцевой маршрут или «туда-обратно» -- идеально, прямая в одну сторону --
бесполезно), и предполагает, что ветер за поездку не менялся. Последнее
лечится подгонкой в скользящем окне 20-30 минут, но точность падает.
## Вариант 2. Прогноз, скачанный ЗАРАНЕЕ по проводу
Обходит запрет на сеть в поле: перед выездом, на домашнем проводном интернете,
скачивается почасовой прогноз ветра по району и кладётся в базу. В поездке сеть
не нужна вообще.
Прогноз при этом используется **не как истина, а как начальное приближение**
для варианта 1: он задаёт правдоподобный диапазон, а трек его уточняет.
Возражение про «меряют в другой точке» остаётся в силе, но начальное
приближение с известным смещением лучше, чем поиск вслепую.
Спорный момент: прогноз может оказаться хуже, чем ничего, если приземный ветер
в лесополосах отличается от станционного в разы. Стоит проверить на нескольких
треках, прежде чем закладывать.
## Вариант 3. Трубка Пито на руле (единственный способ мерить по-настоящему)
Так устроены настоящие аэродинамические измерители для велосипеда (Notio,
Aeropod). Трубка, направленная вперёд, плюс датчик разности давлений дают
**скорость набегающего воздуха напрямую**, с частотой в десятки герц.
Разность между воздушной скоростью и путевой скоростью с GPS, вместе с курсом,
даёт вектор ветра непрерывно, включая порывы. Сети не требуется вообще.
- датчик разности давлений класса MPXV7002DP плюс микроконтроллер с BLE
- порядок цены комплектующих -- полторы-три тысячи рублей
- BLE в приложении уже есть, канал знакомый
- нужна калибровка (сравнением с вариантом 1 на кольцевом маршруте в штиль)
и защита от дождя и грязи
- трубка на руле уязвима при падении и при переноске велосипеда
**Это единственный из перечисленных способов, который измеряет, а не оценивает.**
И единственный, который видит порывы, а не среднее.
## Вариант 4. Флюгер на руле
Дешевле Пито: измеряется только **угол** набегающего потока относительно
велосипеда, обычным поворотным датчиком. Скорости ветра не даёт, но сильно
сужает решение в варианте 1 -- направление перестаёт быть неизвестным,
остаётся подобрать одну величину вместо двух.
На велосипеде хрупок и чувствителен к завихрениям от самого седока.
## Вариант 5. Голосовой ввод по словам-триггерам (идея пользователя)
Седок произносит «в лицо», «в спину», «боковой», «порыв», приложение ставит
отметку времени. Распознавание на Android умеет работать без сети.
**Замечание, которое считаю важным.** Как измерение это слабо: оценка
субъективна, отметки редкие, а говорить на тяжёлом дыхании и в шуме ветра
неудобно -- причём шум ветра в микрофоне мешает именно тогда, когда ветер
и надо отметить.
Но у этого есть куда более сильное применение: **не как измерение,
а как разметка для проверки автоматической оценки.** Десяток отметок
«в лицо» и «в спину» за поездку -- это готовый ответ, по которому можно
проверить и откалибровать вариант 1, не покупая железа. Если оценка по треку
совпадает с отметками -- ей можно верить и без отметок.
При таком применении редкость и субъективность отметок почти не мешают:
проверяется знак и порядок величины, а не точное значение.
## Вариант 6. Кнопка вместо голоса
То же самое, но нажатием: выносная кнопка на руле по BLE либо кнопка
на гарнитуре. Надёжнее голоса на тяжёлом дыхании и в шуме, но выражает меньше
-- реально различимы два-три состояния, не больше.
## Вариант 7. Шум ветра в микрофоне
Обдув микрофона даёт характерный низкочастотный шум, растущий со скоростью
потока. Теоретически из него извлекается воздушная скорость.
Практически: в кармане бесполезно, нужен вынесенный микрофон; шум дороги
и шин мешает; калибровка своя для каждого телефона и чехла. Дёшево проверить,
но рассчитывать не стоит. Упомянуто для полноты.
---
## О чём хотелось бы услышать мнение сессии разработки
1. **Стоит ли вообще ветер в модели**, или проще честно писать «мощность
на равнине оценена с точностью ±30%» и не усложнять. Разброс 33% измерен,
вопрос в том, окупается ли его устранение.
2. **Вариант 5 в роли разметки** (а не измерения) -- насколько это ложится
на текущий UI и на запись событий с отметкой времени. По цене внедрения
это самый дешёвый из осмысленных шагов, и он не требует решать заранее,
покупать ли датчик.
3. **Оправдан ли вариант 3.** Планка задана: без железа выходит направление
с точностью около 20° и сила с точностью до двух раз. Датчик за
несколько тысяч рублей должен дать заметно больше, иначе он не нужен.
4. **Не ломает ли вариант 2 принцип автономности.** Скачивание заранее
по проводу формально сеть в поле не требует, но привязывает точность
расчёта к тому, вспомнил ли пользователь нажать кнопку перед выездом.
Это тот же изъян, что и у кнопки «уточнить высоты».
Готовый код обоих методов варианта 1 -- в `tools/track/track.py`
(функция `wind_by_passes` и секция «ветер» в `report`), выше описан
алгоритмически.