guillaut5/bryton460-webapp
GitHub: guillaut5/bryton460-webapp
一款通过逆向工程将 GPX 文件转换为 Bryton 460 GPS 码表原生二进制格式的非官方 Web 工具。
Stars: 0 | Forks: 0
# bryton460-webapp
一款 HTML 工具,用于从 GPX 文件生成 **Bryton 460** GPS 的原生文件。
专为无法使用 Bryton 官方应用的 PC 用户设计(如 Nokia 用户、仅有 PC 的用户等)。
**非官方工具 — 与 Bryton 无关。**
文件格式均通过逆向工程获取,仅用于互操作目的。
基于 [MIT License](LICENSE) 授权。
**→ Demo : [dev.agriscope.fr/bryton.html](https://dev.agriscope.fr/bryton.html)**

## 开发说明
### 前置条件
- Node.js ≥ 18
### 安装环境
```
npm install
```
### 运行测试
```
npm test
```
40 个单元测试,覆盖二进制编码器和地理相关函数。
### 构建发行版
```
npm run build
```
生成 `dist/index.html` — 这是一个完全独立的单文件(CSS + JS 由 `vite-plugin-singlefile` 内联处理)。
部署到静态 Web 服务器上的**只需这一个文件**。
### 带自动刷新的本地开发
```
npm run dev
```
打开 `http://localhost:5173` 即可享受 hot reload。
## 使用说明
1. 在 Chrome 或 Edge 中打开 `dist/index.html`(或在线 demo)
2. 拖拽一个 `.gpx` 文件
3. 下载 `.zip` 文件 → 解压到 Bryton(USB 大容量存储)的 `Tracks\` 目录中
## 生成格式
| 文件 | 内容 |
|---|---|
| `.smy` | 摘要:bbox、点数、距离、D+ |
| `.tinfo` | 每次爬坡的起点/终点标记 |
| `.track` | 轨迹:每个点的纬度、经度、海拔、坡度 |
| `/dupli.track` | 轨迹的副本(firmware 要求) |
| `/list.junc` + `list2.junc` | 交叉路口 / 转弯 |
| `/.climb` | 爬坡数据(距离、长度、D+、%) |
| `/sort1.path` | 地理段索引 |
## 发布说明
### v0.4 — 2026-07-02
- **通过 OSRM 实现 turn-by-turn 导航** — UI 中新增选项:将 GPX 轨迹提交至
[OSRM](https://project-osrm.org/) 服务(map matching),以便从 OSM 道路网络获取真实的转弯指令。
生成包含 UTF-8 街道名称、方向代码和到下一转弯处距离的 B 格式 `.tinfo` 文件
— firmware 会将其读取为手动规划的路线。
支持的指令:出发、到达、直行、轻微转弯、右转、左转、
急转弯、掉头、岔路口、环岛(1/2/3+ 出口)。
道路网络之外的部分(自行车道、海岸小径)将被静默忽略
— 其他转弯指令依然有效。
UI 会显示统计细分:`✓ 17 个转弯 — ↻ 右转 6 ↺ 左转 2 → 直行 1`。
- **`.tinfo` B 格式逆向工程** — 通过分析两个参考文件
(`voiceTrip`, `test_route`)得出:在相同的 44 字节格式中采用了 nav + 爬坡混合结构。
生成的文件严格遵循以下顺序:DÉPART (0x01), INFO rue départ (0xFA),
转弯, INFO distance totale (0xFA), ARRIVÉE (0x21), 爬坡标记 (0xBE/0xBF)。
- **v0.3 重构** — 采用 ES 模块架构 (src/),Vite 构建,Vitest (40 个测试),
`vite-plugin-singlefile` → 完全独立的 `dist/index.html` (~134 KB)。
- **`sort1.path` 已验证** — 基于 OSM zoom 13 瓦片进行分段,保留 1 个点的重叠。
在 100K 数据上验证:41 个段,tx ∈ [4182,4185],ty ∈ [2982,2990]。
- **通过 Overpass API 生成 `list.junc`** — 真实的 OSM 交叉路口(半径 25m),
如果 Overpass 不可用,则回退至通过角度变化 ≥ 25° 进行检测。
### v0.3 — 2026-06-29
开发架构重构为结合 Vite + Vitest 的 ES 模块 (src/)。
(与此版本一同发布的关联功能请参见 v0.4。)
### v0.2 — 2026-06-25
- **`.track` byte 10 : 局部坡度** — 每个点现在都会编码以 % 为单位的坡度(带符号的 int8),
计算窗口为 200m 的滑动平均值。已通过 Bryton 官方文件验证:偏差 ≤ 1%。
- **`.smy` : D- = 0** — Bryton 官方应用不填充 D− 字段。已修复以保持一致性。
- **爬坡检测** — 将中位数平滑(按点数)替换为基于距离的
滑动平均值 (200m)。中位数会产生人为的阶梯效应,导致漏判爬坡起点。
新阈值:长度 ≥ 500m,D+ ≥ 25m,坡度 ≥ 2.2%,评分 = grade × D+。
### v0.1 — 2026-06-25
- 初始原型版本:独立 HTML 页面,实现 GPX → Bryton 460 zip 转换
- 生成 8 种原生格式文件:`.smy`, `.tinfo`, `.track`, `dupli.track`,
`list.junc`, `list2.junc`, `.climb`, `sort1.path`
- 可选的 RDP / 均匀简化
- 如果 GPX 没有海拔数据,通过 Open-Elevation API 获取 SRTM 海拔
- 通过 File System Access API (Chrome/Edge) 进行直接 USB 传输
## 二进制格式详情
所有整数均为 **Little Endian**(低位字节在前)。
### `.smy` — 固定 68 字节
路线摘要,Bryton 会优先读取此文件以显示路线列表。
```
Offset Taille Type Contenu
00 2 uint16 version = 1
02 2 uint16 nb_points
04 4 int32 lat_max × 1 000 000
08 4 int32 lat_min × 1 000 000
12 4 int32 lon_max × 1 000 000
16 4 int32 lon_min × 1 000 000
20 4 int32 distance totale (m)
24 4 int32 inconnu (= 1 638 732 dans tous les fichiers officiels)
28 32 — zéros
60 4 int32 D+ (m)
64 4 int32 D− = toujours 0 (l'appli officielle ne remplit pas ce champ)
```
真实示例(官方 100K):
```
01 00 54 3C 30 D2 9D 02 54 AD 99 02 11 C9 3B 00 A1 0E 3A 00
↑ ↑ ↑ ↑ ↑
15444 43.775°N 43.626°N 3.885°E 3.806°E
5E 82 01 00 4C 01 19 00 00…(32 zéros)…00 87 04 00 00 00 00 00 00
↑ ↑ ↑ ↑
98910m inconnu D+=1159m D-=0
```
### `.track` — N × 16 字节
每个 GPS 点对应一条记录。作为主文件 — 整个轨迹都存储于此。
```
Offset Taille Type Contenu
00 4 int32 lat × 1 000 000
04 4 int32 lon × 1 000 000
08 2 uint16 altitude (m)
10 1 int8 pente locale (%) — moyenne glissante sur 200m
11 5 — zéros
```
真实示例(100K 的点 0 和点 4):
```
pt0 54 AD 99 02 9D 95 3B 00 1F 00 FF 00 00 00 00 00
↑ ↑ ↑ ↑
43.626°N 3.906°E 31m -1%
pt4 96 AD 99 02 98 95 3B 00 1F 00 FF 00 00 00 00 00
↑
43.626°N (+4m Nord vs pt0)
```
`FF` 作为带符号 int8 = -1。坡度:`00`=0%,`0A`=+10%,`F6`=-10%,`7F`=最大 +127%。
### `.tinfo` — N × 88 字节(每次爬坡 N 对)
爬坡索引。按对存储:每次爬坡包含一条“起点”记录和一条“终点”记录。
每条记录长 44 字节 — 只有前 4 个字节有效,其余均为零。
```
Offset Taille Type Contenu
00 4 uint32 (0x00BE << 16) | ptIdx → début de montée
(0x00BF << 16) | ptIdx → fin de montée
04 40 — zéros
```
真实示例(100K 的 5 次爬坡):
```
rec0 DE 04 BE 00 → 0x00BE04DE → flag=0xBE (début), ptIdx=0x04DE=1246
rec1 93 05 BF 00 → 0x00BF0593 → flag=0xBF (fin), ptIdx=0x0593=1427
rec2 26 0A BE 00 → ptIdx=2598
rec3 BE 10 BF 00 → ptIdx=4286
... (5 paires = 10 records = 440 octets)
```
### `list.junc` / `list2.junc` — N × 12 字节 + sentinel
路线经过的 OSM 交叉路口。这两个文件完全相同。
最后一条记录是 sentinel `FF×12`。
```
Offset Taille Type Contenu
00 4 int32 lat × 1 000 000
04 4 int32 lon × 1 000 000
08 2 uint16 ptIdx (point de trace le plus proche)
10 1 uint8 flag : 0x01 = virage, 0x00 = tout droit
11 1 uint8 bearing × (256/360) — 0=Nord, 64=Est, 128=Sud, 192=Ouest
```
真实示例(100K 开头部分):
```
junc0 D0 AD 99 02 45 95 3B 00 00 00 01 34
↑ ↑ ↑ ↑ ↑
43.626°N 3.906°E pt=0 virage 0x34=52 → 73° (NE)
junc4 F8 AE 99 02 79 8A 3B 00 26 00 01 30
junc5 F8 AE 99 02 79 8A 3B 00 26 00 01 5A
↑ même carrefour, deux routes qui partent → deux records
```
Sentinel:`FF FF FF FF FF FF FF FF FF FF FF FF`
### `sort1.path` — N × 16 字节
空间索引:按 OSM zoom 13 瓦片切割轨迹。
使 Bryton 仅加载当前瓦片约 500 个点,而不是整个轨迹的 15,000 个点。
```
Offset Taille Type Contenu
00 4 uint32 start_ptIdx
04 4 uint32 end_ptIdx
08 4 uint32 tile_id = (tile_y_z13 << 16) | tile_x_z13
12 4 uint32 0
```
各段之间存在** 1 个点的重叠**:段 N 的 `end` = 段 N+1 的 `start` − 1。
OSM zoom 13 瓦片公式:
```
tx = floor((lon + 180) / 360 × 8192)
ty = floor((1 − ln(tan(lat_rad) + 1/cos(lat_rad)) / π) / 2 × 8192)
```
真实示例(100K — 41 个段,tx ∈ [4182,4185],ty ∈ [2982,2990]):
```
seg0 00 00 00 00 F9 01 00 00 58 10 AE 0B 00 00 00 00
↑ ↑ ↑
start=0 end=505 tile=(tx=4184, ty=2990)
seg1 F8 01 00 00 0F 04 00 00 58 10 AD 0B 00 00 00 00
↑
start=504 ← overlap d'1 point avec seg0
```
### `dupli.track`
根目录 `.track` 的逐字节副本。确切作用未知 — 但没有它 firmware 就会崩溃。
## 格式设计理念 — 一切均预计算
Bryton 460 搭载了一颗低端 ARM 处理器(几十 MHz,~1 MB RAM)。
没有三角函数,没有解析,运行时也没有动态内存分配。
所有繁重的工作均**在转换时的 PC 端完成** — 设备只负责读取和显示。
| 数值 | 计算位置 | 存储方式 |
|---|---|---|
| 局部坡度 | PC — 200m 滑动窗口 | 带符号 `int8` 存于 `.track` 的 byte 10 |
| 交叉路口方位角 | PC — haversine 公式 | `uint8` 0–255 存于 `list.junc` (非度数或弧度) |
| 边界框 / 距离 / D+ | PC | `.smy` — 固定 68 字节 |
| 爬坡索引 | PC — 根据剖面图检测 | `.tinfo` — 起始/结束 ptIdx 对 |
| 每个瓦片的空间索引 | PC — Mercator z=13 计算 | `sort1.path` |
### 两步式“轨迹上”检测
Bryton 不会每秒将 GPS 位置与轨迹的 15,000 个点进行对比。
它将 `sort1.path` 用作粗略过滤器:
```
Étape 1 — filtre rapide (sort1.path)
GPS → tuile OSM z=13 courante
→ charge uniquement les ~500 points du segment correspondant
→ élimine les 14 500 autres points
Étape 2 — comparaison fine (.track)
Pour chaque point du segment :
distance(GPS, point) < seuil → sur le trajet
→ off route
```
如果 `sort1.path` 不正确(tuile_id = 0 而非实际瓦片),Bryton 会立即提示
“off route”,根本不去查看 `.track` 中的坐标。
### 设备端不使用余弦
方位角以 **0–255** 存储(而非度数):
```
0 → Nord 64 → Est 128 → Sud 192 → Ouest
```
设备只需对一个字节进行简单的箭头查找。在生成文件时,
由我们负责计算 `atan2` 及 `× 256/360` 转换。
## 参考数据
```
data_references/
100k/
100K.gpx ← sortie Strava, ~100km, région Montpellier
output_brytonofficial/ ← fichiers générés par l'appli Bryton officielle (ground truth)
output_mytool/ ← fichiers générés par cet outil (pour comparaison)
bales/
bales.gpx ← sortie Strava, Pyrénées, départ ~1200m
```
## 已确认与估算的内容
通过与 Bryton 官方应用在 100 km / 15,444 个点的真实轨迹上生成的文件进行逐字节对比来验证。
| 文件 | 状态 | 详情 |
|---|---|---|
| `.track` lat/lon/ele | ✅ 正确 | int32/uint16 LE 编码已验证 |
| `.track` byte 10 坡度 | ✅ 正确 | 对比官方文件偏差 ≤ 1% (200m 窗口) |
| `.smy` | ✅ 正确 | bbox、距离、D+ 正确 — D- = 0 与官方一致 |
| `.tinfo` | ✅ 正确 | Flags 0xBE/0xBF + 16 位 ptIdx |
| `.climb` 结构 | ✅ 正确 | 4 × float32 : start_m, longueur_m, D+_m, grade |
| `sort1.path` | ✅ 正确 | 按 OSM z=13 瓦片分段 — 格式已验证 |
| `.climb` 检测 | 🔶 估算 | 第 1 次爬坡准确,其余对比官方 ≈ ±2 km |
| `list.junc` | 🔶 估算 | 根据转弯角度检测 — 并非真实的 OSM 交叉路口 |
标签:GPS轨迹, HTML, MITM代理, 前端工具, 后端开发, 多模态安全, 数据可视化, 数据转换, 文件解析