突破离线限制:Zwift-Offline Meetup团队竞赛结果展示功能全解析
【免费下载链接】zwift-offline Use Zwift offline 项目地址: https://gitcode.com/gh_mirrors/zw/zwift-offline
引言:离线环境下的团队竞赛痛点
你是否曾在没有网络连接的情况下组织Zwift团队训练?当骑行结束,队员们汗水淋漓地期待查看竞赛结果时,却因离线限制无法获取实时排名和数据统计——这正是Zwift-Offline项目要解决的核心痛点。本文将深入剖析Meetup团队竞赛结果展示功能的技术实现,从数据采集到前端渲染的全流程解析,帮助开发者掌握离线环境下的运动数据处理范式。
读完本文,你将获得:
- 理解离线运动数据捕获与存储的关键技术
- 掌握Protobuf协议在运动数据序列化中的应用
- 学习如何设计高效的竞赛结果计算引擎
- 实现响应式竞赛结果展示界面的前端技术
功能架构概览
Meetup团队竞赛结果展示功能采用三层架构设计,确保在完全离线环境下实现数据采集、处理和展示的闭环:
核心技术栈
- 后端框架:Python Flask
- 数据存储:SQLite
- 序列化协议:Protobuf
- 前端技术:HTML/CSS/JavaScript
- 实时通信:UDP/TCP协议处理
数据采集:从运动中捕获关键指标
FIT文件解析
当用户完成骑行后,Zwift客户端会生成FIT(Flexible and Interoperable Data Transfer)文件,这是一种专为运动设备设计的二进制格式。Zwift-Offline通过create_activity_file函数解析FIT文件,提取关键竞赛数据:
def create_activity_file(fit_file, small_file, full_file=None):
"""解析FIT文件并创建活动记录"""
try:
with fitdecode.FitReader(BytesIO(fit_file)) as fit:
activity = Activity()
# 解析基本信息
for frame in fit:
if isinstance(frame, fitdecode.FitDataMessage):
if frame.name == 'session':
activity.start_date = frame.get_value('start_time')
activity.end_date = frame.get_value('timestamp')
activity.distanceInMeters = frame.get_value('total_distance')
activity.movingTimeInMs = frame.get_value('total_elapsed_time') * 1000
# 解析心率、功率等生理指标
elif frame.name == 'record':
activity.avg_heart_rate = frame.get_value('heart_rate')
activity.avg_watts = frame.get_value('power')
# 保存到数据库
db.session.add(activity)
db.session.commit()
return activity.id
except Exception as exc:
logger.warning('create_activity_file: %s' % repr(exc))
return None
实时状态捕获
对于在线同步的Meetup活动,系统通过UDP协议实时捕获骑手状态,包括位置、速度和功率等关键指标:
def handle_udp_packet(data):
"""处理UDP协议中的骑手状态数据包"""
packet = udp_node_msgs_pb2.RiderState()
packet.ParseFromString(data)
# 更新骑手在线状态
online[packet.id] = packet
# 检测是否到达终点
if is_finish_line(packet):
record_race_result(packet.id, packet.timestamp)
数据处理:构建竞赛结果计算引擎
数据模型设计
系统设计了三个核心数据表存储竞赛相关数据,通过SQLAlchemy ORM实现:
class Activity(db.Model):
"""活动记录表"""
id = db.Column(db.Integer, primary_key=True)
player_id = db.Column(db.Integer)
course_id = db.Column(db.Integer)
start_date = db.Column(db.Text)
end_date = db.Column(db.Text)
distanceInMeters = db.Column(db.Float)
avg_watts = db.Column(db.Float)
movingTimeInMs = db.Column(db.Integer)
# 其他指标...
class SegmentResult(db.Model):
"""分段结果表"""
id = db.Column(db.Integer, primary_key=True)
player_id = db.Column(db.Integer)
segment_id = db.Column(db.Integer)
elapsed_ms = db.Column(db.Integer)
avg_power = db.Column(db.Integer)
# 其他指标...
class RaceResult(db.Model):
"""竞赛结果表"""
id = db.Column(db.Integer, primary_key=True)
event_id = db.Column(db.Integer)
player_id = db.Column(db.Integer)
finish_time = db.Column(db.Integer)
rank = db.Column(db.Integer)
# 其他指标...
Protobuf协议定义
为高效传输竞赛数据,系统使用Protobuf定义了竞赛结果协议:
// race_result.proto
message RaceResult {
int32 event_id = 1;
repeated RiderResult rider_results = 2;
int64 timestamp = 3;
}
message RiderResult {
int32 player_id = 1;
string name = 2;
int64 finish_time_ms = 3;
int32 rank = 4;
float avg_power = 5;
float avg_speed = 6;
// 其他指标...
}
竞赛结果计算算法
api_race_results函数实现了核心的竞赛结果计算逻辑,采用高效排序算法处理参赛选手数据:
def api_race_results():
"""计算并返回竞赛结果"""
event_id = request.args.get('event_id')
# 查询该赛事所有完成者
results = db.session.query(SegmentResult).filter_by(
event_subgroup_id=event_id
).all()
# 按完成时间排序
results.sort(key=lambda x: x.elapsed_ms)
# 计算排名和差距
ranked_results = []
if results:
winner_time = results[0].elapsed_ms
for i, result in enumerate(results):
rider = get_partial_profile(result.player_id)
ranked_results.append({
'rank': i + 1,
'player_id': result.player_id,
'name': f"{rider.first_name} {rider.last_name}",
'time': format_time(result.elapsed_ms),
'gap': format_time(result.elapsed_ms - winner_time) if i > 0 else "0s",
'avg_power': result.avg_power,
'avg_speed': calculate_speed(result.distance, result.elapsed_ms)
})
return jsonify(ranked_results)
结果展示:构建用户友好的界面
后端API设计
系统提供RESTful API接口,为前端提供竞赛结果数据:
@app.route("/api/race_results", methods=["GET"])
@jwt_to_session_cookie
@login_required
def api_race_results_endpoint():
"""竞赛结果API接口"""
event_id = request.args.get('event_id')
if not event_id:
return jsonify({"error": "event_id is required"}), 400
results = api_race_results(event_id)
return jsonify({
"event_id": event_id,
"results": results,
"timestamp": time.time()
})
前端实现
竞赛结果页面采用响应式设计,适配不同设备显示需求:
<!-- user_home.html -->
<div class="race-results-container">
<h2>Meetup团队竞赛结果</h2>
<div class="filters">
<select id="event-selector">
<option value="">选择赛事...</option>
<!-- 动态加载赛事列表 -->
</select>
<button onclick="exportResults()">导出结果</button>
</div>
<table class="results-table">
<thead>
<tr>
<th>排名</th>
<th>选手</th>
<th>完成时间</th>
<th>与冠军差距</th>
<th>平均功率 (W)</th>
<th>平均速度 (km/h)</th>
</tr>
</thead>
<tbody id="results-body">
<!-- 动态加载结果 -->
</tbody>
</table>
</div>
<script>
function loadRaceResults(eventId) {
fetch(`/api/race_results?event_id=${eventId}`)
.then(response => response.json())
.then(data => {
const tbody = document.getElementById('results-body');
tbody.innerHTML = '';
data.results.forEach(result => {
const row = document.createElement('tr');
row.innerHTML = `
<td class="rank">${result.rank}</td>
<td class="name">${result.name}</td>
<td class="time">${result.time}</td>
<td class="gap">${result.gap}</td>
<td class="power">${result.avg_power}</td>
<td class="speed">${result.avg_speed.toFixed(1)}</td>
`;
tbody.appendChild(row);
});
});
}
</script>
数据可视化
为增强结果可读性,系统实现了多种数据可视化图表,包括选手功率曲线对比和赛道分段成绩分析:
// 使用Chart.js绘制功率曲线对比
function renderPowerCurves(results) {
const ctx = document.getElementById('power-curve-chart').getContext('2d');
const datasets = results.slice(0, 3).map((result, index) => {
return {
label: result.name,
data: fetchPowerData(result.player_id),
borderColor: ['#FF6384', '#36A2EB', '#FFCE56'][index],
tension: 0.1
};
});
new Chart(ctx, {
type: 'line',
data: {
labels: Array.from({length: 60}, (_, i) => i + 1), // 1-60秒
datasets: datasets
},
options: {
title: {
display: true,
text: '前三名选手功率曲线对比'
},
scales: {
yAxes: [{
title: {
display: true,
text: '功率 (W)'
}
}],
xAxes: [{
title: {
display: true,
text: '时间 (秒)'
}
}]
}
}
});
}
性能优化:处理大规模竞赛数据
数据库优化
针对可能的大规模竞赛数据,系统采用以下数据库优化策略:
-
索引设计:在常用查询字段上创建索引
# 在模型定义中添加索引 __table_args__ = ( db.Index('idx_segment_event', 'event_subgroup_id'), db.Index('idx_activity_player', 'player_id'), ) -
查询优化:使用懒加载和分页查询
def get_paginated_results(event_id, page=1, per_page=20): return SegmentResult.query.filter_by( event_subgroup_id=event_id ).order_by(SegmentResult.elapsed_ms).paginate( page=page, per_page=per_page, error_out=False )
缓存策略
为提高结果展示速度,系统实现了多级缓存机制:
def get_cached_race_results(event_id):
"""获取缓存的竞赛结果"""
cache_key = f"race_results:{event_id}"
# 尝试从内存缓存获取
if cache_key in global_race_results:
# 检查缓存是否过期(5分钟)
if time.time() - global_race_results[cache_key]['time'] < 300:
return global_race_results[cache_key]['results']
# 缓存未命中,计算结果
results = api_race_results(event_id)
# 更新缓存
global_race_results[cache_key] = {
'results': results,
'time': time.time()
}
return results
部署与使用指南
环境准备
-
克隆项目仓库:
git clone https://gitcode.com/gh_mirrors/zw/zwift-offline cd zwift-offline -
安装依赖:
pip install -r requirements.txt -
初始化数据库:
python -c "from zwift_offline import db; db.create_all()"
启动服务
# 启动主服务
python zwift_offline.py
# (可选)启动Discord通知机器人
python discord_bot.py
使用流程
扩展与定制
添加新的竞赛类型
要支持新的竞赛类型(如计时赛),只需扩展结果计算逻辑:
def calculate_time_trial_results(event_id):
"""计算计时赛结果"""
results = db.session.query(SegmentResult).filter_by(
event_subgroup_id=event_id
).all()
# 计时赛特殊规则:按平均功率/体重比排序
results.sort(key=lambda x: x.avg_power/(x.weight_in_grams/1000), reverse=True)
# 生成排名结果...
return ranked_results
自定义结果展示
通过修改前端模板文件user_home.html,可以定制结果展示方式,添加团队排名、累计积分等功能。
总结与展望
Zwift-Offline的Meetup团队竞赛结果展示功能通过创新的数据处理和展示技术,解决了离线环境下团队训练数据无法共享和分析的痛点。核心优势包括:
- 全离线运行:无需互联网连接即可完成数据采集、处理和展示
- 高效数据处理:Protobuf协议和优化算法确保高性能
- 丰富的可视化:多种图表帮助分析竞赛数据
- 易于扩展:模块化设计支持添加新功能和竞赛类型
未来,该功能可以进一步扩展,添加AI辅助的成绩分析、训练建议生成以及更丰富的第三方平台集成(如训练计划系统)。
附录:核心API参考
| API端点 | 方法 | 描述 | 参数 |
|---|---|---|---|
/api/race_results | GET | 获取竞赛结果 | event_id: 赛事IDpage: 页码 |
/api/activities | GET | 获取活动列表 | player_id: 用户IDlimit: 数量限制 |
/api/activities/<id> | GET | 获取活动详情 | id: 活动ID |
/api/export/race_results | GET | 导出竞赛结果 | event_id: 赛事IDformat: 格式(json/csv) |
通过这些API,开发者可以构建更丰富的第三方应用,如训练分析工具、团队管理系统等。
收藏本文,随时查阅离线团队竞赛系统的实现细节。关注项目仓库获取最新更新,下一版本将支持实时竞赛直播功能!
【免费下载链接】zwift-offline Use Zwift offline 项目地址: https://gitcode.com/gh_mirrors/zw/zwift-offline
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考



