GPS model: accel-based Crr scale, auto walk-the-bike split; document climb/elevation limits
Acted on docs/gps-model-findings.md (independent analysis of 156 rides). Implemented: - crrFromAccel extended past the old 0.008 ceiling: 7-11 -> 0.008, 11-15 -> 0.012 (packed dirt), >15 -> 0.020 (grass/rut/gravel). Rough surfaces were undercounted ~3x. Applied to session median and per-point Crr. - Auto walk-the-bike detection (single-speed only, gear_count 1/unset): a normal-fix segment slower than the 30-rpm-cadence speed is the rider pushing, not riding. Excluded from ride distance/speed/elevation (elevRef reset so a pushed climb doesn't merge with the ridden one); effort counted as walking, not bike physics. Every sub-threshold point excluded regardless of duration; only runs >=30s reported as walked (walkBikeSec/walkBikeMeters). gearCount threaded through every computeStats/finish caller. Documented as known limitations (left as-is by decision, need calibration/DEM): - kcal max(physics, MET) never sums the biological overhead, so climbs are slightly undercounted (comment + README + PROJECT_MAP). - absolute elevation gain is bounded by the raw GPS altimeter (tens of percent; needs a terrain model / wired DEM preload).
This commit is contained in:
parent
ec77f67cab
commit
dd9ec1bda7
5 changed files with 125 additions and 8 deletions
|
|
@ -177,17 +177,29 @@
|
|||
```
|
||||
Power = v * (m*g*sin(gradient) + Crr*m*g*cos(gradient) + 0.5*CdA*rho*v^2)
|
||||
m = rider_kg + bike_kg; gradient из СГЛАЖЕННОЙ высоты
|
||||
Crr = 0.004 (асфальт) / 0.006 (смешанный) / 0.008 (грунт)
|
||||
Crr (ручной surface_type): 0.004 асфальт / 0.006 смешанный / 0.008 грунт
|
||||
Crr по акселерометру (crrFromAccel, если есть accel_rms >10 точек): <3=0.004, 3-7=0.006,
|
||||
7-11=0.008, 11-15=0.012 (плотный грунт), >15=0.020 (трава/колея/щебень). Прежний потолок 0.008
|
||||
занижал грунты ~3x (замер: трава требует 0.020-0.031). Мапится и медиана сессии, и каждая точка (ptCrr).
|
||||
CdA = 0.6, rho = 1.225, efficiency = 0.25
|
||||
|
||||
Педалирование (power>0): kcal = max(физика, MET-нетто), физика = power*dt/(0.25*4184)
|
||||
MET по скорости: <16км/ч=6.0, <19=6.8, <22=8.0, <25=10.0, <28=12.0, иначе 14.0
|
||||
MET-нетто = (dt/60)*((MET-1)*3.5*weight)/200 (−1 MET покоя, он уже в BMR)
|
||||
Floor только при v≥5км/ч (ниже — дрожание GPS, не езда)
|
||||
ИЗВЕСТНОЕ ОГРАНИЧЕНИЕ (по решению не правим, нужна калибровка): max() не СКЛАДЫВАЕТ —
|
||||
на подъёмах физика выигрывает, а накладные (баланс/стабилизация корпуса, без внешней работы,
|
||||
их ловит MET-этаж) теряются. Направление ошибки известно: подъёмы недосчитаны (по поездке ~1.34x
|
||||
MET/физика, но равнина 1.85x, подъём 0.41x). Честный фикс = физика/КПД + накладные, отложен.
|
||||
Накат / freewheel (power≤0, «велик катился бы сам»): kcal = базальный метаболизм
|
||||
(детектор кручу/накат по IMU-каденсу проверен на 50Hz — сигнал слишком слаб, не используется; хук cadence_rpm — под BLE-датчик шатуна)
|
||||
|
||||
Набор высоты: кумулятивный гистерезис (elevRef, порог 1м) — на плотных треках per-point разница <1м даёт 0
|
||||
ИЗВЕСТНОЕ ОГРАНИЧЕНИЕ (по решению не правим): абсолютный набор по сырому GPS-альтиметру расходится
|
||||
с независимым пересчётом в ОБЕ стороны (медиана разброса высоты между проездами одной точки ~10м).
|
||||
Без модели рельефа (DEM) не разрешается. Реальный фикс — предзагрузка DEM по району дома по проводу
|
||||
(кнопка «уточнить высоты» → GPXZ работает, но требует сети, а РЭБ/РКН глушат в поле). Пока: набор
|
||||
имеет точность порядка десятков процентов, на расчёт мощности влияет слабо (там уклон по окну, ограничен сверху).
|
||||
```
|
||||
|
||||
### Дистанция по сегментам (computeStats, gps.js ~189-235)
|
||||
|
|
@ -205,6 +217,12 @@ distance += segDist; высота/maxSpeed — только на «норме»;
|
|||
Глюк-гард высоты: соседний сглаженный перепад с грейдом > maxRealGradePct (1.0 = 100% = 45°) не банкуется и не двигает elevRef
|
||||
(скачок/ступенька, в т.ч. Android no-fix 0.0). Плавный приход к любому значению (реальный 0 у реки) проходит.
|
||||
Kotlin пишет "alt":null без фикса высоты (hasAltitude()), честный 0 с фиксом сохраняется. Паузы: колонка gps_sessions.pauses (JSON).
|
||||
Пеший пуш велосипеда (только односкоростной, gear_count 1 или пусто + есть звезда/трещотка/колесо):
|
||||
на фиксированной передаче каденс <30 об/мин = велик не едет, его ведут. Порог скорости walkBikeMs = 30*wheelCirc*gearRatio/60.
|
||||
Сегмент медленнее порога (нормальный фикс, не гэп) исключается из ездовой дистанции/скорости/высоты (elevRef=null — пеший
|
||||
подъём не склеивается с ездовым), усилие считается как ходьба (walk-MET нетто), не по физике. #7: исключается КАЖДАЯ такая точка
|
||||
без оглядки на длительность (краткие торможения тоже, безвредно ~0м); в отчёт как «пешком» идут только сплошные пробеги ≥30с.
|
||||
Возврат computeStats: walkBikeSec (время пуша ≥30с-пробегами), walkBikeMeters (пешая дистанция). opts.gearCount прокинут во все вызовы.
|
||||
```
|
||||
|
||||
### Ходьба/бег — MET-таблица
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@
|
|||
- **Дневник питания** — ~13000 продуктов с меткой источника (Магнит, 5ка, ИОН, KDV, Гриднев, свои). Поиск порядко-независимый (слова в любом порядке). Каталог чистится от неиспользованного импорта без метки (health-diet и пр.) кнопкой в настройках — съеденные и в рецептах сохраняются. 37 нутриентов (витамины, минералы, omega-3/6, вода, клетчатка). Рецепты с термообработкой. Формулы порций (4*30, 300+50).
|
||||
- **GPS трекинг** — запись маршрутов на велосипеде/пешком в фоне (foreground service, точки пишутся при выключенном экране; один файл на сессию с id сессии в имени `gps_points_<ms>_s<id>.jsonl` для однозначной привязки, переживает START_STICKY-перезапуск ОС; полный до-импорт нативных точек по timestamp при финализации — без потерь, даже если экран был выключен всю запись, а при пробуждении экрана точки видимо нарастают на карте по мере импорта). При живой записи нативный файл — источник истины для БД, watchPosition только рисует маркер (без дублей одного фикса с разными ts). Восстановление точек из файлов на устройстве — матч файла к сессии по id/дате, дозаполнение разрывов с дедупом по близости ±2.5с, безопасный пересчёт без удаления точек, чистка уже импортированных файлов. Оффлайн карты (Leaflet + IndexedDB кэш). Импорт GPX/FTB (FitoTrack). Экспорт GPX. Фильтр altitude outlier'ов и GPS-залипаний. Отбор точек по точности (жёсткий порог по настройке «Мин. точность» — рубит грубые сотовые фиксы ~800м, оставляет слабый но точный GPS; где сигнала нет — честный разрыв). Направление (bearing) каждой точки пишется в БД. Вращение карты по компасу (leaflet-rotate, EMA-сглаживание, gimbal lock компенсация) и course-up по GPS-курсу (в ландшафте карта доворачивается вместе с экраном — курс к физическому верху). Ландшафтный режим (CSS rotation + панель справа, 3 режима: off/auto/lock). Dead reckoning при потере GPS (гироскоп + акселерометр). Placeholder до GPS (пиксельарт). Карта проезженных дорог (дедупликация 2м, вело/пешие). Настраиваемый цвет линии трека (чтобы отличать от цветных дорог). Навигационный маркер позиции с треугольником-направленцем (по компасу или GPS-курсу); порядок слоёв тайлы<дороги<маршрут<трек<маркер. Импорт тайлов по регионам (SQLite, роутер LAN).
|
||||
- **BLE пульсометр** — подключение к FitPro браслетам и стандартным HR датчикам (0x180D). ЧСС, SpO2, давление в реальном времени при записи трека.
|
||||
- **Расчёт ккал** — велосипед: НЕТТО-модель (суточный баланс уже учитывает BMR, поэтому активность считается сверх покоя). Кручение = max(физика мощности из gradient+Crr+CdA / КПД, эмпирический MET-нетто по скорости); накат (freewheel, `power≤0` — «велик катился бы сам») = только базовый обмен. MET-таблица для ходьбы/бега и тренировок (приседания 6.0, скакалка 12.3, растяжки 2.3, бассейн 8.0). Набор высоты — кумулятивный гистерезис (устойчив на плотных треках, где per-point разница <1м). Пункт меню «Пересчитать высоту/калории» пересчитывает статистику всех треков без изменения точек. Профили велосипедов. BMR по Mifflin-St Jeor с коэффициентом активности.
|
||||
- **Расчёт ккал** — велосипед: НЕТТО-модель (суточный баланс уже учитывает BMR, поэтому активность считается сверх покоя). Кручение = max(физика мощности из gradient+Crr+CdA / КПД, эмпирический MET-нетто по скорости); накат (freewheel, `power≤0` — «велик катился бы сам») = только базовый обмен. Crr на грунтах уточняется по вибрации акселерометра (до 0.020 на траве/колее). Ходьба с велосипедом в горку (только односкоростной) отделяется автоматически по скорости ниже каденса 30 об/мин и не засчитывается как езда. MET-таблица для ходьбы/бега и тренировок (приседания 6.0, скакалка 12.3, растяжки 2.3, бассейн 8.0). Набор высоты — кумулятивный гистерезис (устойчив на плотных треках, где per-point разница <1м); абсолютная точность набора ограничена сырым GPS-альтиметром (порядка десятков процентов, без модели рельефа не разрешается). Известное ограничение: `max(физика, MET)` не складывает накладные расходы, поэтому подъёмы по ккал слегка недосчитываются. Пункт меню «Пересчитать высоту/калории» пересчитывает статистику всех треков без изменения точек. Профили велосипедов. BMR по Mifflin-St Jeor с коэффициентом активности.
|
||||
- **Дистанция на разрывах GPS** — на «слепых» участках (интервал между фиксами > `gapThresholdSec`) путь считается по скорости (speed×dt, с ограничением по макс. скорости), а не прямой хордой — иначе разрывы занижают/искажают длину. Телепорт-скачки без временного разрыва отбрасываются. Расчёт зависит только от времени и скорости, поэтому live-счётчик, «Стоп» и пересчёт дают одинаковую длину. Порог `gapThresholdSec` настраивается; при изменении и пересчёте применяется сразу.
|
||||
- **Тренировки** — приседания (подходы x повторения), скакалка, растяжки, бассейн (дистанция). Автоматический расчёт ккал по MET и весу.
|
||||
- **Нутриенты** — 37 микронутриентов (витамины, минералы, жирные кислоты, женьшень, ресвератрол, CoQ10). Нормы РСП/АУП (РФ/ЕС/США). Фармакокинетика — запасы в организме по полураспаду.
|
||||
|
|
|
|||
40
docs/session-logs/2026-09-07_gps-model-findings-impl.md
Normal file
40
docs/session-logs/2026-09-07_gps-model-findings-impl.md
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
# Сессия 2026-09-07: реализация находок по расчётной модели GPS
|
||||
|
||||
## Контекст
|
||||
Разобрали `docs/gps-model-findings.md` (вывод независимого инструмента `tools/track/track.py`
|
||||
по 156 поездкам). Пользователь принял решения по каждой находке. Плана в plan-mode не было —
|
||||
работали по прямым решениям пользователя, зафиксированным ниже.
|
||||
|
||||
## Решения пользователя и что сделано
|
||||
- **#1 Crr-потолок 0.008 занижен → внедряем.** `crrFromAccel` в gps.js расширен: 7-11=0.008,
|
||||
11-15=0.012 (плотный грунт), >15=0.020 (трава/колея). Мапится и медиана сессии, и per-point ptCrr.
|
||||
- **#2 max(физика,MET) не складывает, подъёмы недосчитаны → только пометить.** Не меняем формулу:
|
||||
добавлен комментарий у `Math.max(physicsKcal, metNetKcal)` (ИЗВЕСТНОЕ ОГРАНИЧЕНИЕ, нужна калибровка),
|
||||
записано в README и PROJECT_MAP.
|
||||
- **#3+#7 ходьба с велосипедом как езда → внедряем.** computeStats: для односкоростного
|
||||
(`gearCount` 1/пусто + звезда/трещотка/колесо) порог walkBikeMs = 30 об/мин; сегмент медленнее
|
||||
порога (нормальный фикс) исключается из ездовой дистанции/скорости/высоты (elevRef=null),
|
||||
усилие = walk-MET нетто. #7: исключается каждая точка без гейта по длительности, в отчёт «пешком»
|
||||
только пробеги ≥30с. Возврат: walkBikeSec/walkBikeMeters. `gearCount` прокинут во все вызовы
|
||||
(live-stop, recalculateSession, recovery, bulk, main.js старт записи + detail). Время пуша ≥30с
|
||||
вычитается из duration (средняя скорость остаётся чисто ездовой).
|
||||
- **#4 набор высоты расходится в обе стороны → только пометить.** Не код: записано в README и
|
||||
PROJECT_MAP (предел сырого GPS-альтиметра, реальный фикс — предзагрузка DEM по проводу).
|
||||
- **#5/#6** — к сведению (пульс потерян браслет; ветер в track.py, не в приложении).
|
||||
|
||||
## Ветер (часть 2 доки)
|
||||
Пользователь: калории от неточного ветра не считать. Хочет простые отслеживаемые вещи — голосовые
|
||||
метки («ветер в лицо») и/или кнопки в UI (нажал когда дует в лицо, отжал когда перестал), писать
|
||||
данные и делать выводы после набора километража. Это вариант 5 в роли РАЗМЕТКИ, не измерения.
|
||||
Трубка Пито (вариант 3) — точно нет. Вариант 2 (предзагрузка прогноза) — пользователь не понял,
|
||||
нужно объяснить. Реализация ветер-фичи — отдельным шагом (нужны решения по UX: триггер-слова,
|
||||
место кнопки, схема хранения событий).
|
||||
|
||||
## Результат
|
||||
Модельные правки #1/#3/#7 внедрены, #2/#4 задокументированы. `node --check` чист по всем файлам.
|
||||
Собрано и поставлено ранее build 436 (только правки пауз/высоты); эти правки требуют новой сборки.
|
||||
|
||||
## Следующие шаги
|
||||
- Собрать/поставить новую сборку, проверить: на грунтах ккал выше; пеший подъём на односкоростном
|
||||
не в ездовой статистике и не склеивается с ездовым подъёмом.
|
||||
- Ветер: спроектировать и внедрить разметку событий (голос/кнопки).
|
||||
|
|
@ -181,15 +181,21 @@ GPS.computeStats = function(points, weightKg, opts) {
|
|||
const rho = GPS.config.airDensity ?? 1.225;
|
||||
const efficiency = (GPS.config.pedalEfficiency ?? 25) / 100;
|
||||
// Rolling resistance: base from surface type, corrected by tire pressure.
|
||||
// If accelerometer data available, estimate Crr from vibration (accel RMS):
|
||||
// RMS < 3 → smooth asphalt (0.004), 3-7 → mixed (0.006), > 7 → gravel (0.008)
|
||||
// If accelerometer data available, estimate Crr from vibration (accel RMS).
|
||||
// Scale (lower bound of a back-solve on measured tracks — better to undercount):
|
||||
// RMS < 3 → 0.004 smooth asphalt | 11-15 → 0.012 packed dirt
|
||||
// 3-7 → 0.006 mixed | > 15 → 0.020 grass / rut / gravel
|
||||
// 7-11 → 0.008 broken asphalt
|
||||
// The old ceiling of 0.008 (gravel) undercounted rough surfaces ~3x: measured
|
||||
// grass+tractor-rut needs 0.020-0.031, packed dirt 0.012. crrFromAccel maps both
|
||||
// the session median (default Crr) and each point (ptCrr below) through this scale.
|
||||
const crrFromAccel = (rms) => rms < 3 ? 0.004 : rms < 7 ? 0.006 : rms < 11 ? 0.008 : rms < 15 ? 0.012 : 0.020;
|
||||
let baseCrr = opts?.surfaceCrr ?? 0.006;
|
||||
const accelVals = sorted.map(p => p.accel_rms).filter(v => v != null && v > 0);
|
||||
if (accelVals.length > 10) {
|
||||
accelVals.sort((a, b) => a - b);
|
||||
const medianAccel = accelVals[Math.floor(accelVals.length / 2)];
|
||||
const accelCrr = medianAccel < 3 ? 0.004 : medianAccel < 7 ? 0.006 : 0.008;
|
||||
baseCrr = accelCrr; // override with sensor-based estimate
|
||||
baseCrr = crrFromAccel(medianAccel); // override with sensor-based estimate
|
||||
}
|
||||
const tirePressure = opts?.tirePressureBar ?? 3.0;
|
||||
const Crr = baseCrr * Math.sqrt(3.0 / tirePressure);
|
||||
|
|
@ -207,6 +213,20 @@ GPS.computeStats = function(points, weightKg, opts) {
|
|||
const wheelCirc = hasGearData ? Math.PI * opts.wheelDiameterInch * 0.0254 : 0; // meters
|
||||
const gearRatio = hasGearData ? opts.chainringTeeth / opts.cogTeeth : 0;
|
||||
|
||||
// Walking the bike (single-speed only). On a fixed ratio, below ~30 rpm cadence
|
||||
// the bike can't stay upright — so a sustained sub-30-rpm speed means the rider
|
||||
// is pushing it, not riding. speed = cadence * distance-per-crank-rev.
|
||||
// distance-per-rev = wheelCirc * gearRatio; at 30 rpm → walkBikeMs m/s.
|
||||
// Only for single-speed (gear_count 1 or unset): on a geared bike low speed proves
|
||||
// nothing (accelerometer can't tell pushing from riding — measured). Sub-threshold
|
||||
// segments are excluded from ride distance/speed/elevation so a pushed climb doesn't
|
||||
// merge with the ridden one; the effort is counted as walking, not bike physics.
|
||||
const gearCount = opts?.gearCount;
|
||||
const singleSpeed = isBike && hasGearData && (gearCount == null || gearCount <= 1);
|
||||
const walkBikeMs = singleSpeed ? (30 * wheelCirc * gearRatio) / 60 : 0;
|
||||
let walkBikeSec = 0, walkBikeDist = 0; // reported total (only runs ≥30s, see #7)
|
||||
let wbRunSec = 0, wbRunDist = 0; // current contiguous push run
|
||||
|
||||
for (let i = 1; i < sorted.length; i++) {
|
||||
let p = sorted[i], prev = sorted[i - 1];
|
||||
let d = GPS.distance(prev.lat, prev.lon, p.lat, p.lon);
|
||||
|
|
@ -245,6 +265,24 @@ GPS.computeStats = function(points, weightKg, opts) {
|
|||
// altitude and max-speed stats; gap and spike segments do not.
|
||||
let normalMove = !isGap && !isSpike && dt > 0;
|
||||
|
||||
// Walking the bike (single-speed): a normal-fix segment slower than the 30-rpm
|
||||
// speed. Exclude it from ride distance/speed/elevation (elevRef reset breaks the
|
||||
// pushed-vs-ridden climb merge) and count the effort as walking, not bike physics.
|
||||
// Per #7: exclude EVERY such point regardless of duration (brief brake-to-stops
|
||||
// land here too — harmless, ~0 m and low cost); accumulate contiguous runs and
|
||||
// only report those ≥30s as "walked" (a stop before a pothole isn't walking).
|
||||
if (walkBikeMs > 0 && normalMove && v > 0 && v < walkBikeMs) {
|
||||
wbRunSec += dt; wbRunDist += segDist;
|
||||
elevRef = null;
|
||||
const wKmh = v * 3.6;
|
||||
const wmet = wKmh < 4 ? 2.5 : wKmh < 5.5 ? 3.5 : wKmh < 7 ? 5.0 : 8.0;
|
||||
totalKcal += (dt / 60) * ((wmet - 1) * 3.5 * wKg) / 200; // NET (BMR booked in daily balance)
|
||||
continue;
|
||||
}
|
||||
// Not (or no longer) pushing → flush a completed push run into the reported total.
|
||||
if (wbRunSec >= 30) { walkBikeSec += wbRunSec; walkBikeDist += wbRunDist; }
|
||||
wbRunSec = 0; wbRunDist = 0;
|
||||
|
||||
if (segDist > 0) distance += segDist;
|
||||
if (normalMove) {
|
||||
speeds.push(v);
|
||||
|
|
@ -297,7 +335,7 @@ GPS.computeStats = function(points, weightKg, opts) {
|
|||
// Per-point Crr from accelerometer (if available), else session-wide Crr
|
||||
const ptAccel = sorted[i].accel_rms;
|
||||
const ptCrr = (ptAccel != null && ptAccel > 0)
|
||||
? (ptAccel < 3 ? 0.004 : ptAccel < 7 ? 0.006 : 0.008) * Math.sqrt(3.0 / tirePressure)
|
||||
? crrFromAccel(ptAccel) * Math.sqrt(3.0 / tirePressure)
|
||||
: Crr;
|
||||
|
||||
// Power required (watts): gravity + rolling resistance + aerodynamic drag
|
||||
|
|
@ -332,6 +370,12 @@ GPS.computeStats = function(points, weightKg, opts) {
|
|||
if (vKmh >= 5) {
|
||||
const met = vKmh < 16 ? 6.0 : vKmh < 19 ? 6.8 : vKmh < 22 ? 8.0 : vKmh < 25 ? 10.0 : vKmh < 28 ? 12.0 : 14.0;
|
||||
const metNetKcal = (dt / 60) * ((met - 1) * 3.5 * wKg) / 200;
|
||||
// KNOWN LIMITATION (measured, left as-is by decision): max() never SUMS the
|
||||
// two, so on climbs — where physics wins big — the biological overhead the
|
||||
// MET floor captures (balancing, core stabilisation, no external work) is
|
||||
// dropped. Direction of the error is known: climbs are undercounted (whole-
|
||||
// ride ~1.34x MET/physics, but flat 1.85x and climb 0.41x). A principled fix
|
||||
// (physics/eff + overhead) needs calibration, so it is deferred, not applied.
|
||||
totalKcal += Math.max(physicsKcal, metNetKcal);
|
||||
} else {
|
||||
totalKcal += physicsKcal; // creeping < 5 km/h: negligible
|
||||
|
|
@ -362,6 +406,9 @@ GPS.computeStats = function(points, weightKg, opts) {
|
|||
}
|
||||
}
|
||||
|
||||
// Flush a push run still open at the track's end.
|
||||
if (wbRunSec >= 30) { walkBikeSec += wbRunSec; walkBikeDist += wbRunDist; }
|
||||
|
||||
// 9-point smoothed speeds for maxSpeed (kills spikes from GPS noise)
|
||||
let maxSpeed = 0;
|
||||
for (let i = 0; i < speeds.length; i++) {
|
||||
|
|
@ -375,7 +422,9 @@ GPS.computeStats = function(points, weightKg, opts) {
|
|||
|
||||
const first = new Date(sorted[0].ts);
|
||||
const last = new Date(sorted[sorted.length - 1].ts);
|
||||
const duration = Math.max(0, (last - first) / 1000 - pausedSec); // exclude manual-pause time
|
||||
// Exclude manual-pause time and sustained bike-push time — both keep avgSpeed
|
||||
// (distance/duration) a pure riding figure, consistent with the excluded distance.
|
||||
const duration = Math.max(0, (last - first) / 1000 - pausedSec - walkBikeSec);
|
||||
const avgSpeed = duration > 0 ? (distance / duration) : 0;
|
||||
const avgSpeedKmh = avgSpeed * 3.6;
|
||||
const maxSpeedKmh = maxSpeed * 3.6;
|
||||
|
|
@ -481,6 +530,8 @@ GPS.computeStats = function(points, weightKg, opts) {
|
|||
duration: Math.round(duration),
|
||||
avgCadence,
|
||||
segments,
|
||||
walkBikeSec: Math.round(walkBikeSec), // time spent pushing the bike (single-speed, ≥30s runs)
|
||||
walkBikeMeters: Math.round(walkBikeDist), // distance walked pushing it (excluded from ride distance)
|
||||
};
|
||||
};
|
||||
|
||||
|
|
@ -930,6 +981,7 @@ GPS._persistState = function() {
|
|||
chainringTeeth: GPS.recording.chainringTeeth ?? null,
|
||||
cogTeeth: GPS.recording.cogTeeth ?? null,
|
||||
wheelDiameterInch: GPS.recording.wheelDiameterInch ?? null,
|
||||
gearCount: GPS.recording.gearCount ?? null,
|
||||
}));
|
||||
} catch {}
|
||||
};
|
||||
|
|
@ -1013,6 +1065,7 @@ GPS.recoverSession = async function() {
|
|||
chainringTeeth: s.chainringTeeth,
|
||||
cogTeeth: s.cogTeeth,
|
||||
wheelDiameterInch: s.wheelDiameterInch,
|
||||
gearCount: s.gearCount,
|
||||
pauses: s.pauses || [],
|
||||
};
|
||||
const stats = GPS.computeStats(pts, s.weightKg || 80, recoverOpts);
|
||||
|
|
@ -1193,6 +1246,7 @@ GPS.stopRecording = async function() {
|
|||
chainringTeeth: GPS.recording.chainringTeeth,
|
||||
cogTeeth: GPS.recording.cogTeeth,
|
||||
wheelDiameterInch: GPS.recording.wheelDiameterInch,
|
||||
gearCount: GPS.recording.gearCount,
|
||||
};
|
||||
const recPauses = GPS.recording.pauses || [];
|
||||
recOpts.pauses = recPauses;
|
||||
|
|
@ -1232,6 +1286,7 @@ GPS.stopRecording = async function() {
|
|||
GPS.recording.chainringTeeth = null;
|
||||
GPS.recording.cogTeeth = null;
|
||||
GPS.recording.wheelDiameterInch = null;
|
||||
GPS.recording.gearCount = null;
|
||||
GPS._clearPersistedState();
|
||||
return { sessionId: sid, stats, pauses };
|
||||
};
|
||||
|
|
@ -1745,6 +1800,7 @@ GPS.recalculateSession = async function(sessionId, opts) {
|
|||
chainringTeeth: bike?.chainring_teeth,
|
||||
cogTeeth: bike?.cog_teeth,
|
||||
wheelDiameterInch: bike?.wheel_diameter_inch,
|
||||
gearCount: bike?.gear_count,
|
||||
pauses: sessPauses,
|
||||
});
|
||||
|
||||
|
|
@ -1798,6 +1854,7 @@ GPS.recomputeAllStats = async function(onProgress) {
|
|||
chainringTeeth: bike?.chainring_teeth,
|
||||
cogTeeth: bike?.cog_teeth,
|
||||
wheelDiameterInch: bike?.wheel_diameter_inch,
|
||||
gearCount: bike?.gear_count,
|
||||
pauses: bulkPauses,
|
||||
});
|
||||
await DB.finishGpsSession(sid, session.end_time ?? session.start_time,
|
||||
|
|
|
|||
|
|
@ -2774,6 +2774,7 @@ async function renderGpsScreen(container) {
|
|||
GPS.recording.chainringTeeth = bike.chainring_teeth;
|
||||
GPS.recording.cogTeeth = bike.cog_teeth;
|
||||
GPS.recording.wheelDiameterInch = bike.wheel_diameter_inch;
|
||||
GPS.recording.gearCount = bike.gear_count;
|
||||
}
|
||||
}
|
||||
await GPS.startRecording(type, wkg);
|
||||
|
|
@ -4776,6 +4777,7 @@ async function showSessionDetail(sessionId) {
|
|||
bikeWeightKg: bike?.weight_kg ?? 0, activityType: session.activity_type ?? "bike",
|
||||
surfaceCrr: CRR[bike?.surface_type] ?? 0.006, tirePressureBar: bike?.tire_pressure_bar ?? 3.0,
|
||||
chainringTeeth: bike?.chainring_teeth, cogTeeth: bike?.cog_teeth, wheelDiameterInch: bike?.wheel_diameter_inch,
|
||||
gearCount: bike?.gear_count,
|
||||
});
|
||||
await DB.finishGpsSession(sessionId, session.end_time ?? session.start_time,
|
||||
st.distance, st.kcal, st.avgSpeed, st.maxSpeed, st.elevGain, st.elevLoss);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue