把 F1 官方公开的赛事控制数据做成 Android 手机上的实时仪表。
比赛日你会盯着它看:红旗、安全车、双黄旗立刻强提醒;赛道当前状态一眼可见; 噪音默认过滤掉。
直连 Formula 1 官方的实时计时服务,不带任何令牌、不需要账号、不经过任何中间服务器。
给 Android 6.0 老设备做的:不经过 WebView、不依赖 Google 服务、零第三方依赖, APK 85 KB。
v3.0.0起是「A 方案」——数据链从「F1 官方 → 上游集成 → Home Assistant → App」 缩成「App → F1 官方」。旧的 HA 版界面与代码仍保留在仓库里作为兜底, 但已不是启动入口。上一代是
Android_HA_Viewer(通用的「任意实体变化流水」)。两者包名不同,可以同时装在一台手机上。
原始的 HA App 或者一个通用的实体列表,在比赛时会变成这样:
- 一个比赛周末 697 条消息,其中 54.8% 是噪音(蓝旗、解除、删圈速通报); 正赛里这个比例高达 68.5% —— 蓝旗一项就占 42.9%
- 双黄旗一个周末 142 条,但它只对应 24 个真实事件 (F1 的赛道系统会把一次事故扩散到几十个区段,最极端的一次:52 条消息、20 个区段、持续 284 秒)
- 如果你让每条双黄都报警,一个周末会被自己的手机吓 146 次
这个 App 把这些都处理掉了。
全部来自 livetiming.formula1.com 的公开流,不需要登录:
| 流 | 内容 |
|---|---|
RaceControlMessages |
赛事控制消息(旗语、判罚、赛道状况) |
TrackStatus |
全赛道状态码(1/8 全清、2 黄、4 安全车、5 红旗、6 VSC、7 VSC 结束) |
SessionInfo / SessionStatus |
会议、赛道、环节、进行状态 |
LapCount |
当前圈 / 总圈数 |
DriverList |
车手、车队、车队配色 |
TimingData |
名次、与领先者差距、与前车间隔、是否在维修区 |
TimingAppData |
轮胎配方、每套胎的圈数(进站次数) |
TimingStats |
个人最快圈 |
TopThree / WeatherData / ExtrapolatedClock |
前三名 / 气温赛道温湿度风雨 / 剩余时间 |
需要 F1TV 订阅的只有 CarData.z(遥测)、Position.z(赛道位置)、
TeamRadio(车队无线电)那几路 —— 本 App 一个都不用。
连接过程只做一次 POST 拿一个一次性令牌,再拿它开 WebSocket。令牌用第二次就失效, 所以每次重连都必须重新申请;App 里不保存任何凭据,你也不需要填任何东西。
竖屏就是消息列表 + 顶部旗语栏。横屏变成左右分栏,最右侧一条窄框图标条切换 6 屏:
| 图标 | 面板 | 内容 |
|---|---|---|
| ◎ | 赛道图 | 圆环 + 各区段旗语颜色 |
| ◍ | 轮胎进站 | 22 个位置、三列每列 8 行、顺序按赛道位置 |
| ≡ | 成绩 | 名次 + 差距 / 间隔 + 轮胎 |
| ☂ | 天气 | 气温 / 赛道温 / 湿度 / 风 / 雨 + 全赛道状态 |
| ⏱ | 最快圈 | 个人最快圈排行 |
| ⓘ | 环节 | 会议 / 赛道 / 环节 / 剩余 / 圈数 + 前三名 |
赛道图为什么是圆环:真形状要靠 Position.z(赛道位置流),那需要 F1TV,
实时拿不到。所以退而求其次用区段均布的圆环。环上的段数取「本场出现过的最大
区段号」,不依赖任何外部数据表 —— 官方没有任何文件写出过区段总数(FIA 每站
55 份文档都查过,编号只画在赛道图上)。
以前只显示优先级最高的那一个,会丢掉同时存在的其他旗语。现在按种类分段:
┌──────────┬──────────┬─────────┐
│ 黄旗 │ 双黄旗 │ 安全车 │
│ 3,7 │ 12 │ 全场 │
└──────────┴──────────┴─────────┘
轨道级的(红旗 / 安全车 / VSC / 全赛道黄旗)下面写「全场」,区段级的写区段号。
这个区分不是猜的:轨道级来自官方 TrackStatus 流,区段级来自消息的 Scope=Sector
—— 实测消息里根本不存在 Scope=Track 的黄旗。
到达的新消息先闪两下再定格到等级配色。
全屏横幅 + 循环警报音 + 震动 + 点亮屏幕。
- 走 闹钟通道,突破系统静音/振动模式(可在设置里关)
- 不同类型给不同震动节奏:红旗三长震、安全车两震、VSC 一长震、双黄三短震
- 锁屏上也能看到(全屏横幅 + 通知栏各一条)
- 「知道了」按钮停止;不点也会在 15 秒后自动停(可配)
订阅时官方会推一份历史全量快照。这些旧消息照常进列表,但不会触发提醒 —— 否则开机瞬间会弹几十次「几小时前的红旗」。
双黄旗到达时先给轻提醒(一声短音 + 一短震),同时挂一个定时器; 存活超过 15 秒仍未清除才升级为全屏强提醒。
为什么要这样:实测双黄旗的存活时长是双峰的 ——
| 存活时长 | 次数 | 判读 |
|---|---|---|
| 0–5 秒 | 8 | 系统抖动 |
| 6–15 秒 | 21 | 测试或瞬时 |
| 16–60 秒 | 19 | 局部事故 |
| 1–5 分钟 | 21 | 真事故 |
| > 5 分钟 | 14 | 重大事故 |
最短只有 1 秒。3 秒的双黄不可能是真事故。 所以「存活 ≤15 秒」是干净的判据。
警报的单位是「一次事故」而不是「一个区段」:事故之间用静默间隔划界 (45 秒 —— 实测双黄消息的间隔分布在 30 秒处有个天然断档)。
一个周末的警报次数:146 次 → 23 次(用真实客户端代码在真实数据上跑出来的)。
仲裁消息又长又全是术语,比赛时根本没空读。所以列表里每条补一行中文:
FIA STEWARDS: TURN 1 INCIDENT INVOLVING CAR 5 (BOR) NOTED -
FAILING TO FOLLOW RACE DIRECTORS INSTRUCTIONS – ESCAPE ROAD INSTRUCTIONS (14:27:29)
1 号弯事故(博托莱托(5)):已记录 —— 未遵守赛会指令(逃生通道)
判罚尤其讲究,谁被罚、罚多少、为什么,一眼要能看出:
★ 判罚:塞恩斯(55) 罚时 5 秒 ★ 判罚:加斯利(10) 罚时 5 秒 —— 维修区超速 仲裁:塞恩斯(55) 已执行 5 秒罚时
覆盖情况:把 2024–2026 三个赛季全部 RaceControl 消息从官方归档拉下来
(16,517 条 → 6,825 个模板)逐个过一遍,该翻译却没有中文的缺口为 0;
剩下 87 个是纯旗语模板(CLEAR IN TRACK SECTOR 12 这种),徽标已经说全了,
有意不翻。有中文的模板占 98.7%。
翻不出来就不显示(宁可给原文,也不瞎猜)。规则全部是从真实数据的措辞反推的, 三位车手缩写会转成中文姓。
顶部一键切换「精简 / 全部」。默认隐藏:蓝旗、解除、删圈速通报。
这条比强提醒本身还重要。 比赛中掉线时,你会以为「赛道没消息」, 实际是漏了红旗 —— 静默失败比崩溃危险。所以顶部状态行常驻显示当前阶段 (未启动 / 连接中 / 已连上等数据 / 连接出错),连接出错时并附带最后一条错误原文。
- 按车号筛选(输入
44只看 Hamilton 相关) - 自由关键词排除
- 看消息时不熄屏(窗口标志实现,不需要额外权限)
不用等比赛、不用网络 —— 从 Releases 下载一个 .rclog 示例文件,
在设置页 ->「回放测试(假数据)」->「选择 .rclog 文件…」 选上它,
保存返回,界面就会把一整场真实比赛完整跑一遍。
文件里是官方归档的一整场真实比赛(2026 巴林站正赛,10 月 4 日),
格式与线上 WebSocket 逐字节同构:先一条 type:3 完整快照,
之后逐条 type:1 增量,记录之间同样是 0x1e 分隔。
所以解析和渲染走的是和实时完全同一份代码。
两个示例文件,区别在「接上时那一刻」:
| 文件 | 接上时 | 模拟的场景 |
|---|---|---|
..._race.rclog |
只有 1 条消息 | 比赛刚开始就打开 App,看着旗语栏逐步长出来 |
..._race_mid.rclog |
194 条消息 + 22 辆车全字段 | 比赛过半才打开 App —— 也就是真机上最容易崩的那种负载 |
倍速 10 / 30 / 60 / 120 / 300,60 倍下一整场 4 小时只要 4 分钟。 放出来的消息时间会被改成「现在」,所以提醒、闪动、全屏横幅都会真的触发。
为什么示例数据不放在 APK 里:v3.1.0 放过,代价是 APK 从 85 KB 涨到 845 KB,其中 751 KB 全是示例数据 —— 而多数人一辈子用不上一次。 v3.2.0 起改成外置:APK 92.7 KB,想要示例的人自己下一个就行。 用系统的文件选择器读它,所以不需要任何存储权限,文件放哪儿都行。
- 从 Releases 下载 APK,拷到手机,开启「未知来源」后安装
- 打开就能用 —— 不需要填任何东西
就这两步。没有服务器地址、没有令牌、没有实体 ID。
构建、测试、发版都写在 docs/CONTRIBUTING.md; 使用说明见 docs/USAGE.md。
值得一提的两个坑(代码里都有回归测试钉住):
- 连接令牌是一次性的。 同一个
connectionToken用第二次直接 404, 所以每次重连都必须重新 negotiate。 - 大消息会分片。 初始快照几十 KB,服务端按 FIN/续帧拆开,必须重组
(
WsFrame.readMessage),否则只会拿到半截 JSON。
还有一个数据形状上的坑,同一种错犯了三次:Stints(轮胎套数)、Messages、
TopThree.Lines 在真实数据里list 和 dict 两种形状都会出现,
方向还不固定。手写的测试夹具发现不了这个 —— 仓库里的
tools/mock_data/f1_snapshot_real.json 是官方归档的真实快照,
tools/mock_data/bahrain2026_race_mid.rclog 是整场时间线,
就是它们把这类问题抓出来的(TopThree 那次连"面板空着是因为没订流、
还是因为形状读错"都分得清)。
顺带一个教训:README 这张数据来源表早就写了
TimingStats和TopThree, 但F1Feed.STREAMS里其实没订这两条流 —— 文档比代码诚实。 现在两边的对账是单测里钉住的。
- 本应用不收集任何数据,所有通信只发生在你的手机与 F1 官方服务器之间
- 不保存任何凭据:连接用的是一次性令牌,用完即弃
- 内容版权归 Formula One World Championship Limited,请仅供个人非商业使用
同族的 Windows 版:F1-RaceControl-Desktop —— 同一份数据层源码编译两遍(不是照着重写),右侧面板可以各自拉成独立原生窗口同时显示, 并自带 Web 服务器,手机 / iPhone 打开浏览器看到的界面和 Windows 上完全一样。
许可证:MIT