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.
翻訳待ち
steve link tree
翻訳待ち
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 ↗