BikeControl (SwiftControl)
Use Zwift Click, Zwift Play, Shimano Di2, or any Bluetooth controller with MyWhoosh, Rouvy, Zwift, and more. Free to try on iOS, Android, Windows, and macOS.
手錶震一下,之後全部自動:Garmin → intervals.icu → GitHub Actions → 每日訓練報告與計時成績。
Translation pending
Sports & fitnessHTML · PythonGitHub @chenhan20OnlineOwner unconfirmed
Have you tried it? One tap is enough. No sign-in needed.
steve link tree
Translation pending
Loading the full README.
Read the full README ↗個人入口網站 + 運動儀表板。部署在 GitHub Pages,純靜態、零後端、沒有 build step。
核心是一條把 Garmin 手錶的原始訓練檔自動變成每日訓練報告與計時成績的管線 —— 全自動、免費、沒有任何 LLM 參與、零推論成本。
🔗 Live: https://chenhan20.github.io/linkTree/linkTreeIndex.html
🏛️ 系統架構: architecture.html (三章、三張圖:資料怎麼進來 → CI 算了什麼 → 前端怎麼呈現。自足靜態頁,無工具鏈)
⌚ Garmin 手錶
│ Garmin 官方 webhook(手錶同步完 → 主動 push,不是輪詢)
▼
intervals.icu ← Garmin 官方核准的 partner
│ 開放 API:GET /activity/{id}/file → 原封不動的原始 FIT
▼
GitHub Actions(每天 10:30 / 22:30 台灣時間)
│
├──► data/fit/*.fit 原始訓練檔(目前 259 個,約 50 MB / 一年份)
├──► data/fit/_wellness.json 每日 HRV / 靜息心率 / 睡眠 / eFTP
├──► rides/<date>.html 單日訓練報告(含課表對帳評分)
├──► data/itt-segments.json ITT 計時成績(自建偵測器算的)
└──► data/training-block.json 評分回填進訓練週期
整條路上沒有帳號密碼、沒有爬蟲、沒有 LLM。要設定的祕密只有一個 API key。
要拿到「手錶原封不動吐出來的那個檔」,帳面上有三條路,只有一條走得通:
| 路 | 結論 |
|---|---|
| Garmin 官方 Developer API | ❌ 法人限定,個人申請不到 |
| Strava API | ⚠️ 拿得到活動,但資料是處理過的;路段成績需付費訂閱。本 repo 的 App 已在 2026-10-12 被停用(Application Inactive) |
| intervals.icu 開放 API | ✅ 免費、拿得到原始 FIT、不挑 IP |
關鍵洞察:你申請不到 Garmin 官方 API,但可以免費用別人已經拿到的。 intervals.icu 是 Garmin 官方核准的 partner,Garmin 用官方 webhook 主動把活動 push 給它,你再用它的開放 API 把檔案原封不動拿回來。
這條路的好處是連鎖的:
| 說明 | |
|---|---|
| 不需要 Garmin 帳密 | 也不存任何 Garmin token |
| 沒有 Cloudflare、沒有 429、沒有鎖帳號風險 | 走的是正規 API,不是爬蟲 |
| 不挑 IP | 機房、雲端、GitHub Actions 都能跑,你的電腦不用開 |
| 免費 | 個人 API key 額度 5000/日、2500/15 分、10/秒 |
| 拿得到原始檔 | 左右平衡、錶上 FTP、真實功率區間邊界、逐秒功率與座標 |
/file 不是 /fit-file
/file是你手錶產出的原始 FIT,原封不動(gzip 壓縮過)。/fit-file是 intervals.icu 用處理過的資料重新生成的,會掉東西。抓錯的話後面所有東西都會歪:ITT 計時吃的是逐秒座標,功率評分吃的是逐秒功率。
完整脈絡與驗證紀錄見 docs/fit-pipeline.md, 還有哪些欄位沒挖見 docs/intervals-api-survey.md。
約 10 分鐘,不需要寫任何程式,也不需要付費。
前置:GitHub 帳號、Garmin 帳號(有運動紀錄,而且會同步到 Garmin Connect)。
✅ 這一步的驗收:手錶同步到 Garmin Connect 之後,約 5 分鐘內活動就會自己出現在 intervals.icu 的行事曆上。沒出現就先別往下走 —— 後面每一步都建立在這條 webhook 上。
💡 因為是主動 push 而不是輪詢,所以你在 Garmin Connect 或 intervals.icu 改活動名稱,改動也會傳過來;下一班同步會偵測到標題變了並自動重生報告。 (只有名稱與描述會同步,而且 Garmin 那邊是覆寫。)
🔑 這把金鑰的用法是 HTTP Basic auth,但很反直覺: 使用者名稱是字面上的
API_KEY這五個字,密碼才是你的金鑰。 填反了會拿到 401 ——scripts/sync-intervals.py已經幫你在錯誤訊息裡註明這件事。
main branch,Folder 選 /(root)https://{你的帳號}.github.io/{repo名稱}/Settings → Secrets and variables → Actions → New repository secret
| Secret 名稱 | 值 | 必填 |
|---|---|---|
INTERVALS_API_KEY | Step 2 複製的那串 | ✅ |
就這一個。 athlete ID 預設用 0(intervals.icu 的慣例,代表「我自己」),不必填。
🔒 金鑰只放 GitHub Secrets。本機要跑的話放
scripts/.env(已在.gitignore), 絕對不要 commit。
Actions → FIT Sync (intervals.icu) → Run workflow,backfill 填天數(例如 365 拉一年)。
| 欄位 | 意思 |
|---|---|
backfill | 回補過去 N 天。留空 = 日常增量(只掃最近 14 天) |
min_tss | 產報告的 TSS 門檻,預設 100 |
rebuild | 重生所有報告(會保留教練評語,肌力訓練不會蓋掉騎乘) |
跑完之後 data/fit/、rides/、data/itt-segments.json 會被 commit 回你的 repo,
之後每天台灣時間 10:30 / 22:30 自動增量更新。
📦 一年份大約是 259 個 FIT、50 MB。這是 repo 裡最大的一塊,但換來的是 「重跑任何分析都不用再打任何 API」—— 評分邏輯改了就重掃本機檔案,成本是零。
想讓報告拿當天的處方跟實際騎乘逐段對帳,編輯 data/plan.json。
那個檔開頭的 _howToAdd / _roles / _ruleKinds 就是完整規格,照著加即可。
沒有寫處方的日子一律不評分 —— 絕不拿課表標準去打一趟自由騎。
自訂計時賽路段,設定在 data/itt-config.json:
{
"segments": [
{ "id": 641218, "nameZh": "風櫃嘴", "nameApi": "風櫃嘴ITT",
"type": "CLIMB", "accent": "#e87c1a" }
]
}
type:CLIMB / SPRINT / ENDURANCE。
ENDURANCE(河濱平路)預設不畫 3D 路線圖 —— 平路的立體圖是一條沒有起伏的線,
佔掉整張卡的高度卻什麼也沒說明。要逐條覆寫就加 "route3d": true / false。accent 配色有語意:山路走暖色家族(橘/琥珀/金/珊瑚),河濱平路走藍色家族。⚠️ 路段折線要跟 Strava 拿一次(
scripts/fetch-segment-streams.js), 因為閘門座標來自官方路段資料。拿到之後計時就完全不靠 Strava 了, 之後每一次計時都是本機從 FIT 算出來的。 2026-10-12 起 Strava API 已停用(Application Inactive),新增路段暫時拿不到官方折線; 已封存的路段不受影響。詳見附錄。
| 檢查 | 該看到什麼 |
|---|---|
| Step 1 之後 | 騎完 5 分鐘內,活動自己出現在 intervals.icu 行事曆 |
| Step 5 之後 | repo 多出 data/fit/*.fit,Actions 綠燈 |
| Step 5 之後 | rides/ 出現達標日的 HTML 報告 |
| Step 7 之後 | 儀表板 ITT 區塊的挑戰次數不是 0 |
.github/workflows/fit-sync.yml,台灣時間 10:30 / 22:30:
scripts/sync-intervals.py,只抓本機還沒有的(增量預設掃 14 天)scripts/build-ride-reports.py,達標的日子寫出 rides/<date>.htmlscripts/backfill-itt-efforts.py,用自建偵測器掃出計時成績data/training-block.json 的 sessions[].actualThis is the beginning of the README. Read the rest at the original. Read the full README ↗