Skip to content

Repository files navigation

VeloVerse 🚴

室内功率车 / 智能骑行台的模拟骑行 App。连接 BLE FTMS 骑行台与心率带,导入 Strava 路线,按真实坡度模拟阻力骑行;OSM 地图导航 + 可定制码表界面, 支持自动暂停 / 自动分段,结束后生成 FIT 文件并可自动上传 Strava(虚拟骑行)。

功能

  • BLE FTMS 连接 — 智能骑行台 / 功率车(服务 0x1826
    • 解析 Indoor Bike Data(0x2AD2):功率、踏频、速度、阻力、距离…
    • 写控制点(0x2AD9)OpCode 0x11 设置坡度模拟阻力
  • 心率带 — Heart Rate Service(0x180D
  • Strava 路线导入 — GPX / TCX,自动计算逐段距离与坡度
  • 坡度模拟 — 按骑行台速度沿路线推进,实时把坡度写给骑行台
  • OSM 地图 — 路线线条、当前位置、镜头跟随导航
  • 码表界面 — 功率/心率/踏频/速度/距离/时间/坡度/平均值/卡路里,坡度条 + 海拔剖面
  • 会话 — 自动暂停(低速/无功率)、自动分段(每 N 公里)、总结统计
  • FIT 导出 & 自动上传sport=cycling, subSport=virtualActivity → 虚拟骑行, 同一份 FIT 可同时推送到 StravaIntervals.icu

运行

flutter pub get
flutter run                 # 连接真机(BLE 需真机,模拟器无蓝牙)

无硬件时可在首页打开 演示模式,用示例路线体验完整流程。

上传服务

凭据一律在 App 内「首页 → 设置」运行时填写,只存本机,分发的安装包里不含任何密钥。 自用构建也可以用 dart-define 预置默认值,运行时填写的值优先。

Strava(OAuth2)

  1. https://www.strava.com/settings/api 创建应用,拿到 Client ID / Secret。
  2. Authorization Callback Domain 填 localhost(Strava 只校验 host,不看 scheme)。
  3. 在 App 里「Strava → 编辑凭据」填入,再点 Connect 授权。

Strava OAuth 不支持 PKCE(换 token 强制要 secret),所以移动端去不掉 secret,只能靠 运行时填写或自建后端代理。回调 scheme veloverse://localhost 已在 iOS Info.plist 与 Android AndroidManifest.xml 中注册。

可选的构建期默认值:

flutter run \
  --dart-define=STRAVA_CLIENT_ID=你的ID \
  --dart-define=STRAVA_CLIENT_SECRET=你的密钥

Intervals.icu(API Key)

没有 OAuth 那一套:在 https://intervals.icu/settings 底部「Developer Settings」生成 API Key,在 App 里「Intervals.icu → 编辑 API Key」粘贴,点「测试连接」即可。 运动员 ID 会自动识别(接口用 athlete/0 代表「当前 key 的主人」),不用手填。

可选的构建期默认值:--dart-define=INTERVALS_API_KEY=你的Key

Intervals.icu 按文件内容 hash 去重:HTTP 201 表示新建活动,200 表示这份 FIT 已经传过了。两者都算成功,App 会分别提示。

持续集成

.github/workflows/build.yml 在 push / PR / 打 tag 时跑 flutter analyze + flutter test, 然后并行产出:

  • Android app-release.apk
  • iOS VeloVerse-unsigned.ipa(未签名,需自行侧载签名)

v* tag 会把两份产物挂到 GitHub Release 上。CI 不需要任何 API 密钥。

Android 默认用 debug key 签名——每台机器/每次 CI 的 debug key 都不同,装了新包无法覆盖旧包。 想让产物能覆盖升级,就配好这几个仓库 secret,CI 会自动改用正式签名: ANDROID_KEYSTORE_BASE64base64 -i upload-keystore.jks)、ANDROID_STORE_PASSWORDANDROID_KEY_ALIASANDROID_KEY_PASSWORD。本地则放一个 android/key.properties(已 gitignore)。

架构

ARCHITECTURE.md。核心分层:

lib/
  core/        BLE UUID / OpCode 常量、格式化、haversine
  models/      BikeData(FTMS 解析)、HrData、GeoRoute(坡度)、RideSample、Lap
  services/
    ble/       BleManager、FtmsService、HeartRateService
    route/     RouteImporter(GPX/TCX)、GradientSimulator
    session/   SessionManager(自动暂停/分段/统计)
    export/    FitWriter(Garmin FIT)
    strava/    StravaService(OAuth2 + 上传为 VirtualRide)
    intervals/ IntervalsService(API Key + 上传 FIT)
  state/       RideController(编排 1Hz 循环) + Riverpod providers
  ui/          home / devices / ride(地图+码表+总结) / widgets

关于 FIT

Garmin 官方 FIT SDK 只提供 Java/C/C++/C#/Obj-C/Rust,无官方 Dart 绑定。本项目用 社区包 fit_tool(忠实实现 FIT 二进制协议)编码, 所有 FIT 细节封装在 services/export/fit_writer.dart,便于日后替换为平台通道调用官方 SDK。

Strava 地图为什么有时不显示

结论:要走 App 内的「上传 Strava」,别把导出的 FIT 手动传到 strava.com。

生成的 FIT 里 GPS 是完整的(每条 record 都带 position_lat/long,测试有 round-trip 断言)。 差别在上传通道:

  • App 内上传POST /uploads 并带 sport_type=VirtualRide,这个参数会 覆盖从文件推断出的类型,活动被判为 Virtual Ride,地图正常。
  • 手动在网页上传没有这个覆盖。Strava 不会把 virtual 类型授予一个未注册设备的文件 (我们是 manufacturer 255/development,拿不到 ANT+ 分配的厂商 ID),于是回落成室内 Ride——而室内活动 Strava 不渲染地图。 已经这样传上去的,可以在网页版活动里点铅笔图标、取消勾选 Indoor 标签,地图就会回来。

另外上传时绝不要带 trainer 字段,哪怕值是 0——它把活动标成室内, 实测会让轨迹消失strava_service.dart 里有注释守着这几条。

说明

  • BLE 与传感器功能需在 真机 上测试。
  • OSM 瓦片来自 tile.openstreetmap.org,生产环境请遵守其使用政策或自建瓦片源。

About

室内功率车 / 智能骑行台的模拟骑行 App。连接 BLE FTMS 骑行台与心率带,导入 Strava 路线,按真实坡度模拟阻力骑行;OSM 地图导航 + 可定制码表界面, 支持自动暂停 / 自动分段,结束后生成 FIT 文件并可自动上传 Strava(虚拟骑行)。

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages