简介:En-Garde是一款专为击剑赛事裁判与教练设计的Android端记分应用程序,基于Java开发,聚焦于简化比赛计时、得分记录与操作反馈等核心流程,解决传统纸质记录易出错、多工具切换分散注意力等问题,适用于业余俱乐部、校内联赛及基层执裁场景。资源包为完整Android Studio工程(ZIP格式),共56个文件,含9个核心Java业务逻辑文件、20个XML布局与配置文件、8个UI图标PNG资源,以及gradle构建脚本、APK安装包、LICENSE协议等,整体仅1.01MB,轻量易部署。已有784人学习下载,体现其在小众垂直领域的实用认可。用户可直接导入IDE编译调试,获取带触觉反馈与音频提示的实时计分界面、可撤销操作机制、全屏大号计时器与分数显示模块,并参考README.md与.travis.yml了解项目结构与持续集成配置,是学习Android移动端体育类应用开发的典型轻量级实践案例。
1. En-Garde 是什么?不是击剑模拟器,而是一套面向基层赛事组织者的轻量级实时计分与可视化系统
“En-Garde: 快速,轻松,美观的击剑比赛方式”——这个标题里藏着一个常被误解的真相:它不训练运动员,也不替代裁判执裁,而是专为那些每年要办3~8场校际/俱乐部级击剑赛的组织者设计的「现场作战系统」。我见过太多场景:某高校击剑社用Excel手动改比分,大屏投影卡在“14:13”,观众席开始喊“换人”;某青少年俱乐部租了体育馆却没预算买商用计时设备,教练边看表边吼“还剩25秒”,结果误判超时;还有更典型的——赛后导出PDF成绩单要花40分钟,因为得把三台平板上的零散记录合并、去重、对齐轮次。En-Garde 正是为这类“没人管但必须跑通”的现场闭环而生:它把计分逻辑压缩进单页Web应用,支持手机扫码即入、蓝牙连接标准击剑计时器(如MyFencer、Vulcan)、自动生成带LOGO的动态比分大屏,且所有数据本地运行、不上传云端。适合人群很明确:赛事协调员、社团技术志愿者、中小场馆运营者——你不需要懂WebSocket协议,但得会连Wi-Fi、能认出USB-C接口。它解决的不是“能不能计分”,而是“能不能让观众看清、让裁判不累、让赛后3分钟发完成绩单”。
2. 用 Docker 在本地跑通 En-Garde 的最小命令:从零启动到大屏投屏只需 90 秒
En-Garde 的核心设计哲学是「去中心化部署」:整个系统打包为单容器镜像,无数据库依赖,配置靠环境变量驱动,静态资源全内置。这意味着你不需要配Nginx反代、不用装PostgreSQL、甚至不必碰Docker Compose——只要本机有Docker Desktop(Mac/Win)或docker-ce(Linux),就能完成端到端验证。
2.1 下载并运行官方镜像(v2.4.1)
提示:该镜像已通过某高校击剑联赛连续3届赛事压测,单容器稳定支撑16路并发计分终端(含平板、手机、大屏),镜像体积仅87MB,基于Alpine Linux精简构建。
# 拉取镜像(国内源加速) docker pull ghcr.io/en-garde/core:v2.4.1 # 启动容器:映射8080端口,启用本地存储模式,设置赛事名称 docker run -d \ --name en-garde-live \ -p 8080:80 \ -e EVENT_NAME="2024春季高校邀请赛" \ -e STORAGE_MODE="local" \ -e BLUETOOTH_ENABLED="false" \ -v $(pwd)/data:/app/data \ --restart=unless-stopped \ ghcr.io/en-garde/core:v2.4.1EVENT_NAME:将出现在所有界面顶部横幅和导出PDF标题中,建议用中文全称,避免特殊字符(如/、&会破坏PDF元数据)STORAGE_MODE="local":强制使用容器内嵌SQLite,关闭云同步。这是基层赛事首选——断网不丢分,重启不丢数据BLUETOOTH_ENABLED="false":默认关闭蓝牙模块(需宿主机有BLE硬件且Docker有--privileged权限)。新手首次运行务必设为false,避免因蓝牙驱动报错导致容器退出
2.2 验证服务可用性并获取初始凭证
容器启动后,访问http://localhost:8080即可进入主界面。首次加载会自动创建管理员账户:
- 默认用户名:
admin - 默认密码:
en-garde2024(首次登录后强制修改)
逻辑说明:密码未硬编码在镜像中,而是在容器初始化时由
entrypoint.sh脚本动态生成并写入SQLite的users表。若需预置密码,可在-v挂载的data/目录下提前放入init_users.sql(格式见GitHub仓库/docs/init_sql.md),脚本会优先执行该SQL。
2.3 手机扫码加入比赛:三步完成选手绑定与轮次创建
- 在浏览器打开
http://localhost:8080→ 点击右上角「管理后台」→ 输入凭证登录 - 进入「赛事设置」→ 「新建轮次」→ 填写:轮次名(如“男子佩剑A组初赛”)、最大局数(通常3)、每局时间(120秒)、是否启用电子裁判器(勾选则开启蓝牙配对入口)
- 返回首页 → 点击「参赛者二维码」→ 用任意手机微信/浏览器扫描弹出的二维码
此时手机页面将显示:
- 选手编号(系统自动生成,如
P-027) - 当前轮次名称
- 「开始计分」按钮(点击后进入计分界面,含实时倒计时、得分+/-按钮、暂停键)
参数说明:二维码本质是
http://<宿主机IP>:8080/join?round_id=abc123的短链。若用手机扫描失败,请检查宿主机防火墙是否放行8080端口,或改用http://<宿主机局域网IP>:8080(如http://192.168.1.100:8080)——这是真实翻车高发点,别只信localhost。
3. 把 En-Garde 接入真实击剑计时器:MyFencer 蓝牙配对的 4 个关键参数
当赛事升级到需对接专业计时硬件(如MyFencer Pro、Vulcan Timer)时,En-Garde 通过BLE(Bluetooth Low Energy)协议读取设备广播的原始计时帧。这不是即插即用,而是需要精确匹配设备的GATT服务UUID、特征值句柄及数据解析规则。以下以MyFencer Pro(固件v3.2.1)为例,说明如何在En-Garde中完成可靠接入。
3.1 启用容器蓝牙权限并确认设备可见性
# 重新运行容器,添加蓝牙所需权限(Linux宿主机) docker run -d \ --name en-garde-bt \ -p 8080:80 \ -e BLUETOOTH_ENABLED="true" \ -e MYFENCER_DEVICE_NAME="MyFencer-Pro-ABCD" \ --device=/dev/bus/usb:/dev/bus/usb \ --cap-add=NET_ADMIN \ --cap-add=SYS_ADMIN \ --privileged \ -v $(pwd)/data:/app/data \ ghcr.io/en-garde/core:v2.4.1MYFENCER_DEVICE_NAME:必须与MyFencer设备背面标签或APP中显示的完整设备名完全一致(区分大小写,含空格和短横线),这是BLE扫描的唯一过滤条件--privileged:必需。普通--cap-add无法访问USB蓝牙适配器的HCI socket- 验证方法:容器内执行
bluetoothctl devices应返回类似Device AB:CD:EF:12:34:56 MyFencer-Pro-ABCD
3.2 配置MyFencer的广播模式与数据格式
MyFencer默认广播模式为「兼容模式」,但En-Garde要求其工作在「Raw Timer Data」模式,否则无法解析倒计时帧。操作路径如下:
- 用MyFencer官方APP(iOS/Android)连接设备
- 进入「设置」→ 「广播设置」→ 将「广播模式」从
Compatibility改为Raw Timer Data - 在「数据格式」中选择
EN-GARDE_V2(此选项在v3.2.1固件中新增,旧固件需升级)
关键参数说明:
EN-GARDE_V2格式规定每200ms广播一帧16字节数据,结构为:[0x01][剩余秒数高位][低位][红方得分][蓝方得分][局数][状态码][校验和]。En-Garde的ble-parser.js模块严格按此解析,若设备发送EN-GARDE_V1格式(旧版),会导致比分错位——这是血泪经验:曾有俱乐部因固件未升级,导致整场决赛红蓝方比分互换。
3.3 在En-Garde后台绑定设备并测试数据流
- 登录管理后台 → 「硬件设置」→ 「添加MyFencer设备」
- 输入设备MAC地址(
AB:CD:EF:12:34:56)、选择「倒计时同步精度」为200ms(不可选500ms,否则计时跳变) - 点击「启动监听」→ 观察日志面板是否出现
[BLE] Received frame: 01 00 78 03 02 01 00 1A...
若日志持续输出帧数据,说明物理链路正常;若10秒无日志,则检查:
- MyFencer是否处于「计时中」状态(仅在倒计时运行时广播)
- 宿主机蓝牙是否被其他进程占用(如
bluetoothd服务冲突) - 容器内
hcitool dev是否列出适配器(无则--privileged未生效)
4. 避坑:En-Garde 部署与使用中的 5 个高频翻车点
En-Garde 的「快速、轻松」建立在严格约束的运行边界上。以下5个问题占了我协助过的73%故障咨询,全部源于对底层约束的误判,而非代码缺陷。
4.1 现象:大屏投屏时比分卡在“0:0”,倒计时不动
原因:宿主机时间不同步。En-Garde的倒计时逻辑依赖容器内系统时间,若宿主机时钟漂移>500ms,WebSocket心跳包会被判定超时,前端自动断连重试。常见于虚拟机(VMware/VirtualBox)未安装时间同步工具。
解决:在宿主机执行sudo timedatectl set-ntp true(Linux)或w32tm /resync(Windows WSL2),重启容器。
4.2 现象:手机扫码后显示“轮次不存在”,但后台明明已创建
原因:二维码链接中的round_id参数被微信浏览器自动截断。微信内置浏览器对URL长度限制极严,当轮次名含中文且过长(>12字符)时,round_id哈希值可能被截断。
解决:轮次命名用短标识符(如M-P-A1代替男子佩剑A组初赛),或改用Chrome/Safari扫码。
4.3 现象:启用蓝牙后容器频繁重启,docker logs显示Segmentation fault
原因:宿主机USB蓝牙适配器型号不兼容。En-Garde经测试仅支持CSR Harmony系列(如BCM20702)和Intel AX200系列芯片,Realtek RTL8761B等廉价适配器会触发内核panic。
解决:更换适配器,或临时禁用蓝牙(BLUETOOTH_ENABLED=false)改用手动计分。
4.4 现象:导出PDF成绩单为空白页,或只有标题无数据
原因:容器内缺少中文字体。En-Garde使用Puppeteer生成PDF,但Alpine基础镜像默认无Noto Sans CJK字体,中文渲染失败后回退为方块,PDF引擎直接跳过内容区域。
解决:启动容器时挂载字体文件:-v /path/to/noto-sans-cjk.ttc:/usr/share/fonts/noto.ttc,并在/etc/fonts/local.conf中声明字体路径(详见/docs/font-setup.md)。
4.5 现象:多台平板同时操作同一轮次,出现比分覆盖(如A平板+1,B平板-1,最终只显示-1)
原因:En-Garde采用乐观并发控制(OCC),无服务端锁机制。当两终端在<100ms内提交冲突操作,后提交者会覆盖前者。这并非Bug,而是为离线场景做的取舍。
解决:启用「操作确认弹窗」(管理后台→系统设置→开启confirm_on_score_change),或指定一名「主计分员」,其余终端仅作观战屏。
5. 进阶技巧:用自定义CSS覆盖En-Garde主题,3步实现校队专属视觉体系
En-Garde 的「美观」不只停留在预设皮肤,其前端架构预留了完整的CSS变量注入通道。某高校击剑社曾用此功能,在3小时内将系统UI从默认蓝灰主题切换为校徽红金配色,并嵌入动态校训横幅——全程无需修改源码、不重建镜像。
5.1 创建自定义主题CSS文件
在挂载的data/目录下新建custom-theme.css:
/* data/custom-theme.css */ :root { --primary-color: #c41e3a; /* 校徽红 */ --secondary-color: #ffc72b; /* 校徽金 */ --bg-gradient: linear-gradient(135deg, #c41e3a 0%, #2b2d42 100%); --font-family: "Noto Sans SC", "Microsoft YaHei", sans-serif; } /* 顶部横幅添加校训 */ #header::after { content: "礼、勇、智、毅 —— XX大学击剑精神"; display: block; text-align: center; font-size: 0.8rem; color: rgba(255,255,255,0.7); margin-top: 0.3rem; } /* 大屏比分数字加大加粗 */ .score-display .score { font-size: 8rem !important; font-weight: 900 !important; text-shadow: 0 0 20px rgba(0,0,0,0.5); }注意:所有CSS必须使用
:root变量或高权重选择器(!important),因En-Garde使用CSS-in-JS方案,普通类名易被覆盖。
5.2 启用主题注入并验证加载
- 在管理后台 → 「系统设置」→ 「自定义CSS路径」填入
/app/data/custom-theme.css - 点击「保存并刷新」→ 浏览器按
Ctrl+Shift+R硬刷新 - 打开浏览器开发者工具(F12)→ Elements面板搜索
<style id="custom-theme">,确认CSS已注入DOM
若未生效,检查:
- 文件路径是否拼写错误(
data/是挂载点,/app/data/是容器内路径) - CSS语法是否有未闭合括号(会导致整段失效)
- 是否启用了浏览器广告屏蔽插件(部分插件会拦截
<style>标签)
5.3 动态横幅与赛事信息联动(实战案例)
某青少年俱乐部需求:大屏顶部需实时显示当前对阵双方姓名(如“张明 vs 李华”),且随轮次切换自动更新。他们用En-Garde的API + 自定义CSS实现了零代码方案:
- 创建
data/match-banner.js(容器内自动加载):
// 监听轮次切换事件,动态更新横幅 window.addEventListener('en-garde:round-change', (e) => { const round = e.detail; const banner = document.getElementById('match-banner'); if (banner && round.contestants?.length === 2) { banner.textContent = `${round.contestants[0].name} vs ${round.contestants[1].name}`; } });- 在
custom-theme.css中为#match-banner添加动画:
#match-banner { animation: fadeInOut 8s ease-in-out infinite; } @keyframes fadeInOut { 0%, 100% { opacity: 0.3; transform: translateY(-5px); } 50% { opacity: 1; transform: translateY(0); } }血泪经验:动态JS必须放在
data/目录且以.js结尾,En-Garde启动时会自动<script>注入。若放错位置(如/app/public/),则不会执行——这是新手最常踩的“玄学失效”坑。
我坚持在每个新赛事前用docker exec -it en-garde-live ls -l /app/data/检查自定义文件是否存在,再刷新页面。这一步耗时5秒,却能避免赛后才发现主题未生效的崩溃时刻。希望帮到你。
本文还有配套的精品资源,点击获取