# ButterflyPilot 可视化大屏 AI 开发指南

> 给后续接手大屏开发的 AI/开发者：先读完本文件，再动代码。
> 当前阶段：**行车数据上传链路已经完成**；登录/注册/邮箱验证码/车机 Web 设置登录**尚未实现**，
> 大屏先使用只读令牌开发。不要修改上传接口和车机端协议。

## 1. 你现在要做什么

在云服务器上开发一个 **ButterflyPilot 行车数据可视化大屏**，数据来自已经落库的真实行车摘要：

- 车机：Jetson Orin NX，BYD 宋 PLUS DMi 2023，openpilot fork
- 上传：`tools/cloud_uploader.py` 解析 `runtime/data/byd/**/qlog.zst`，持续上传
- 云端：`https://bp.fd-tw.com/bp-api/`（FastAPI + MySQL）
- 你的大屏：只读消费数据，不要写 `trip_segments`/`devices`，不要接触车机写令牌

原简易查看器 `/www/wwwroot/butterflyPilot-view` 已合并进主项目 `/www/wwwroot/butterflyPilot`。
现在根路径 `https://bp.fd-tw.com/`、大屏 BFF `/viewer-api/*` 与车机 API `/bp-api/*`
都由同一个 FastAPI 服务（`butterflyPilot.service`，`127.0.0.1:18181`）提供。

后续迭代大屏时只改：

- 前端：`/www/wwwroot/butterflyPilot/dashboard/`
- BFF：`/www/wwwroot/butterflyPilot/app/routers/dashboard.py`
- 不要改 `app/routers/ingest.py` 和车机上传协议。

## 2. 服务器现状（必须遵守）

| 项 | 值 |
|---|---|
| 域名 | `https://bp.fd-tw.com` |
| 上传/只读 API | `/www/wwwroot/butterflyPilot`，systemd `butterflyPilot.service`，`127.0.0.1:18181` |
| nginx | `/bp-api/` 与根路径 `/` → `18181`（同一 FastAPI） |
| 8888 | **已被 frps 占用**，不是 Web 项目；新服务内部端口是 18181 |
| 可视化大屏 | `/www/wwwroot/butterflyPilot/dashboard`，由 `butterflyPilot.service` 提供 |
| 旧查看器 | `/www/wwwroot/butterflyPilot-view`，已停用；systemd `butterflyPilot-view.service` 不再运行 |
| 数据库 | MySQL `butterflyPilot`，表 `devices`、`trip_segments`，视图 `v_trip_summary` |
| 只读令牌 | `/www/wwwroot/butterflyPilot/.read_token`（600，禁止提交/打印到前端） |
| 上传服务 | 车机用户级 systemd `butterflyPilot-uploader`，有网时持续运行 |
| WAF | BT `free_waf` 会拦 gzip JSON 上传；车机已关闭 gzip。大屏只读 GET 不受影响 |

部署大屏时：

1. **不要**删除或改写 nginx 的 `/bp-api/` location。
2. 根路径 `/` 与大屏 BFF `/viewer-api/*` 都转发到 `127.0.0.1:18181`；`/bp-api/` 用更长前缀优先匹配。
3. 大屏 BFF 在 `/viewer-api/*`，由主服务直接读库，浏览器不接触 `BP_READ_TOKEN`。
4. 不要再启动 `butterflyPilot-view.service`（18182 已退役）。
5. 不要把 `.read_token`、设备写令牌、数据库密码写进前端 JS 或 Git。

## 3. 数据链路与口径

```text
qlog (1 分钟 segment)
  -> tools/cloud_uploader.py
  -> POST /bp-api/v1/ingest/segments (X-Device-Token)
  -> trip_segments
  -> v_trip_summary (按 route 聚合)
  -> 你的大屏 (只读)
```

当前真实数据（2026-10-24 18:05）：

- 6,386 个 segment，289 条 route（trip），383.2 km
- 有 GPS 的 segment 363 个（GPS 是 2026-10-24 才接入，更早的行程无轨迹）
- 行驶超过 100 m 的 trip 120 条
- 最大速度约 124 km/h，自动驾驶开启约 2.92 h，接管 269 次
- 停车但 logger 仍在录制的 segment **也会入库**，里程为 0；这是为了保留时间线

### 3.1 关键字段语义

`trip_segments`（每个 segment 一行）：

| 字段 | 说明 |
|---|---|
| `device_id` / `trip_key` / `segment_index` | 设备 + route + 段序号；唯一键 |
| `started_at` / `ended_at` | Unix 秒（UTC 基准） |
| `duration_s` | 段时长，包含停车 |
| `moving_s` | 有速度（>0.3 m/s）的时长 |
| `distance_m` | 段里程 |
| `max_speed_kmh` / `avg_speed_kmh` | 最高速 / 段平均（含停车） |
| `engaged_s` | openpilot 开启时长 |
| `disengagements` | 本段接管次数 |
| `alert_count` | 本段非 normal 告警事件数（已按 type+status 去重） |
| `gps_points` | `[[lat, lon, t_relative_s], ...]`，无有效定位为 `[]` |
| `speed_series` | `[[t_relative_s, kmh], ...]`，约 1 Hz |
| `steer_series` | `[[t_relative_s, deg], ...]` |
| `accel_series` | `[[t_relative_s, m/s^2], ...]` |
| `engaged_changes` | `[[t_relative_s, 0/1], ...]` |
| `alerts` | `[{t, type, text1, text2, status, count}]`，最多 100 条 |
| `extra` | 版本、git commit、车型指纹、qlog 大小等 |

`v_trip_summary`（每条 route 一行，**大屏列表优先用它**）：

- `started_at` / `ended_at`：trip 的首尾时间
- `duration_s` / `moving_s` / `distance_m`
- `max_speed_kmh` / `avg_speed_kmh` / `moving_avg_speed_kmh`
- `engaged_s` / `engaged_ratio` / `disengagements` / `alert_count`
- `segment_count`

**重要口径**：

- 停车 route 的 `distance_m` 可能为 0；列表/统计请默认加 `distance_m >= 100` 或
  `moving_s > 0`，否则总时长/平均速度会被停车时间稀释。
- 大屏上的“平均速度”建议用 `moving_avg_speed_kmh`；“行驶时长”用 `moving_s`。
- 时间显示统一按 `Asia/Shanghai`（+08:00）格式化；数据库存的是 epoch 秒。
- GPS 只覆盖 2026-10-24 14:13 之后的 segment；更早行程 `gps_points=[]`，地图要有空态。
- `gps_points` 的 t 是相对 segment 起点的秒数；直接用 `/track` 接口拿绝对时间更方便。

## 4. 只读 API（推荐先用它开发）

Base：`https://bp.fd-tw.com/bp-api/v1`

请求头：`X-Read-Token: <读取 /www/wwwroot/butterflyPilot/.read_token>`

```bash
TOKEN=$(cat /www/wwwroot/butterflyPilot/.read_token)
curl -sS -H "X-Read-Token: $TOKEN" \
  "https://bp.fd-tw.com/bp-api/v1/trips?limit=20&min_distance_m=100"
```

| 方法 | 路径 | 用途 |
|---|---|---|
| GET | `/health` | 服务健康（公开） |
| GET | `/devices` | 设备、在线状态、累计里程 |
| GET | `/trips?device_uid=&since=&until=&min_distance_m=&limit=&offset=&order=` | 行程列表（`order=newest|oldest`） |
| GET | `/trips/{trip_key}?device_uid=&include_series=true` | 行程详情 + 曲线 |
| GET | `/trips/{trip_key}/track` | GPS 轨迹 `[[lat,lon,unix_s],...]` |
| GET | `/latest` | 各设备最后一次有效定位 |
| GET | `/stats/summary?device_uid=&since=&until=&min_distance_m=` | 总量 + 日统计 + 分设备 |
| GET | `/bp-api/docs` | Swagger（字段和示例的最终真相） |

响应示例（`/stats/summary?min_distance_m=100`）：

```json
{
  "server_time": 1792836436,
  "tz_offset_hours": 8,
  "totals": {
    "trips": 120,
    "distance_km": 382.946,
    "duration_s": 241574.7,
    "moving_s": 40603.4,
    "engaged_s": 10523.5,
    "engaged_ratio": 0.0436,
    "disengagements": 269,
    "alert_count": 719,
    "max_speed_kmh": 124.19,
    "moving_avg_speed_kmh": 33.95
  },
  "daily": [{"day": "2026-10-19", "trips": 21, "distance_km": 39.909}],
  "devices": [{"device_uid": "byd-song-plus-dmi", "distance_km": 382.946}]
}
```

`/trips/{trip_key}` 返回：

```json
{
  "summary": {
    "trip_key": "00000121--aba3dff17e",
    "started_at": 1792824971,
    "ended_at": 1792828118,
    "distance_km": 4.011,
    "moving_s": 524.81,
    "engaged_ratio": 0.0662,
    "moving_avg_speed_kmh": 27.51,
    "segment_count": 53
  },
  "segments": [
    {"segment_index": 0, "speed_series": [[0.2, 0.0]], "gps_points": [[29.52, 106.70, 1.2]], "alerts": []}
  ]
}
```

`include_series=false` 时不返回曲线/轨迹，适合列表页。

### 4.1 开发期如何用令牌

- **推荐**：大屏 + 一个很小的 BFF（FastAPI/Flask/Node）同域部署；BFF 从环境变量/文件读
  `BP_READ_TOKEN`，浏览器只调 `/dash-api/*`。令牌不进入浏览器。
- **仅限私有开发**：浏览器直接带 `X-Read-Token` 调 `/bp-api/`，令牌会出现在 DevTools，
  上线前必须改成 BFF + 登录。
- CORS：只读 API 默认没有放开跨域；如果前端和 API 不同源，需要给 `butterflyPilot` 配置
  `BP_CORS_ORIGINS` 并重启，但不建议把令牌暴露给公网页面。

### 4.2 可视化大屏的 BFF（已合并）

大屏与 API 现在是同一个 FastAPI 服务，BFF 位于
`/www/wwwroot/butterflyPilot/app/routers/dashboard.py`，直接调用 `app/routers/data.py`
的查询函数，不再经过 HTTP 自代理：

- `GET /` → `dashboard/index.html`
- `GET /manifest.webmanifest`、`/guide.md`
- `GET /viewer-api/health`、`/viewer-api/devices`、`/viewer-api/latest`
- `GET /viewer-api/stats`、`/viewer-api/trips`、`/viewer-api/trips/{key}`、`/viewer-api/trips/{key}/track`
- 前端只调 `/viewer-api/*`，不暴露令牌
- 设备、最新位置、统计和行程列表使用默认 10 秒的有界内存缓存（最多 64 项），
  `BP_DASHBOARD_CACHE_TTL=0` 可关闭；详情和轨迹接口不缓存
- 前端资源在 `dashboard/`，通过 `/static/*` 提供

大屏已做响应式适配：手机、平板、桌面、超宽屏；手机端 KPI 两列、行程列表横向滑动、
图表按容器尺寸重绘。后续如需登录，可在 BFF 上加会话校验，不要把只读令牌发到浏览器。

### 4.3 直接读 MySQL

如果大屏后端在同一台服务器，可以直接读库，性能更好。**不要用 `butterflyPilot` 应用账号**，
用 BT 面板创建一个只读账号，例如：

```sql
CREATE USER 'butterflyPilot_dash'@'localhost' IDENTIFIED BY '<强密码>';
GRANT SELECT ON butterflyPilot.* TO 'butterflyPilot_dash'@'localhost';
```

常用 SQL：

```sql
-- 最近有行驶的行程
SELECT * FROM v_trip_summary
WHERE distance_m >= 100
ORDER BY started_at DESC
LIMIT 50;

-- 日里程
SELECT DATE(FROM_UNIXTIME(started_at + 28800)) AS day,
       COUNT(*) trips, SUM(distance_m)/1000 km, SUM(moving_s)/3600 hours
FROM v_trip_summary
WHERE distance_m >= 100
GROUP BY day ORDER BY day;

-- 某行程轨迹（相对时间 + 起点）
SELECT segment_index, started_at, gps_points
FROM trip_segments
WHERE trip_key = '00000121--aba3dff17e'
ORDER BY segment_index;
```

## 5. 大屏产品建议（“最炫酷”但别牺牲可读性）

### 5.1 视觉方向

推荐“夜航驾驶舱”：

- 背景 `#050a0e` / `#070f14`，面板半透明 `rgba(14,25,32,.72)`
- 主色青 `#38e8ff`，辅色琥珀 `#ffb02e`，危险 `#ff4d5e`，正常 `#4dffa6`
- 细网格、扫描线、四角切角、玻璃高光、数字用等宽/科技字体（Orbitron 或系统 mono）
- 动效克制：数字 count-up、路线流光、告警脉冲、面板入场；尊重 `prefers-reduced-motion`

### 5.2 页面结构（桌面 16:9）

```text
┌ Header: logo | ButterflyPilot 行车数据 | 设备选择 | 时间范围 | 在线状态 | 时钟 ┐
├ KPI: 总里程 | 行驶时长 | 行程数 | 自动驾驶占比 | 接管次数 | 告警数 ┤
├──────────────────────────────┬──────────────────────────────┤
│ 中央地图：中国/城市 + 轨迹      │ 右侧：自动驾驶环形图          │
│ 选中行程流光 + 起终点 + 回放    │ 设备状态 / 最后定位 / 告警流   │
├──────────────────────────────┴──────────────────────────────┤
│ 日里程趋势 | 速度分布/曲线 | 最近行程表 | 告警类型 TOP        │
└─────────────────────────────────────────────────────────────┘
```

### 5.3 数据到图表的映射

| 区域 | 数据 | 建议图表 |
|---|---|---|
| KPI | `/stats/summary?min_distance_m=100` | 数字卡 + sparkline |
| 地图总览 | `/latest` / 所有 trip track | ECharts `geo` + `effectScatter`/`lines` |
| 行程轨迹 | `/trips/{key}/track` | 按速度着色 `visualMap` + 流光 |
| 行程回放 | `speed_series` / `gps_points` / `engaged_changes` | 时间轴 + 车辆点 + 速度曲线 |
| 自动驾驶占比 | `engaged_ratio` / `engaged_changes` | 环形进度 + 时间条 |
| 日趋势 | `daily` | 面积折线 + 渐变 |
| 告警 | `alerts` / `alert_count` | 滚动列表 + 类型条形图 |
| 最近行程 | `/trips?min_distance_m=100` | 表格/卡片，点击联动地图 |

地图实现二选一：

1. **ECharts + 中国 GeoJSON**（推荐，纯前端、可控）：本地放 `china.json`，行程轨迹用
   `coordinateSystem: 'geo'`；选中行程时按 bbox 设置 center/zoom。
2. Leaflet/MapLibre + 暗色瓦片（需要瓦片源/网络）：更像真实地图，注意内网/离线回退。

### 5.4 交互

- 顶栏：设备、时间范围（今天/7 天/30 天/全部）、刷新间隔（10s/30s/60s）
- 点击 KPI/日柱 → 过滤行程列表
- 点击行程 → 地图绘制轨迹 + 速度/接管曲线 + 告警
- 回放：播放/暂停、1x/2x、拖动时间轴、地图跟随
- 空态：无 GPS/无行驶行程/无告警时给出明确提示
- 移动端：KPI 两列、地图全宽、表格卡片化

## 6. 当前工程结构（已合并）

```text
/www/wwwroot/butterflyPilot/
  app/
    main.py              # /bp-api + 大屏根路径 + /static
    routers/
      dashboard.py       # /viewer-api/* 大屏 BFF（直接读库）
      data.py            # /bp-api/v1/* 只读 API
      ingest.py          # 车机上传（不要改）
  dashboard/             # 原生 HTML/CSS/JS + 本地 ECharts
    index.html
    app.js
    style.css
    echarts.min.js
    manifest.webmanifest
    icon-180.png / icon-192.png / icon-512.png
  deploy/
    butterflyPilot.service
    nginx-bp-api.conf
  .env                   # 数据库/只读令牌，600，禁止提交
  README.md
```

- 大屏前端由主 FastAPI 通过 `/static/*` 托管，根路径 `/` 返回 `dashboard/index.html`。
- BFF 只做三件事：读数据、缓存、鉴权（后续登录）。
- 不再维护第二个查看器服务；旧 `/www/wwwroot/butterflyPilot-view` 仅作备份。
- 不要把车机上传令牌 `bpd_...` 放进大屏项目；它只在车机 `/home/jetson/butterflyPilot/config/cloud_upload.json`。
- 如果修改根路径代理，先备份 nginx 配置；保留 `/bp-api/`。

## 7. 性能与稳定性

- 列表只查 `v_trip_summary`，不要一次 `SELECT * FROM trip_segments`。
- 详情按需加载；`include_series=false` 先出摘要，再拉曲线。
- 曲线已按约 1 Hz 下采样，单段最多约 900 点；长行程前端可再按像素宽度抽稀。
- 日统计/总量交给 `/stats/summary` 或 SQL 聚合，不要在浏览器遍历 6000+ segment。
- 概览结果已做默认 10 秒的有界 TTL 缓存；`/latest` 可 10 秒轮询，详情/轨迹不缓存。
- JSON 响应可让 nginx 开 gzip；**不要**给 `POST /bp-api/v1/ingest/*` 开请求压缩。
- 大屏 API 和车机上传 API 分开限流/超时；不要共用写 token。

### 7.1 后续车机数据扩展优先级

保持现有协议不变时，低频健康信息可先放入 heartbeat 的 `meta`。新增逐点字段或曲线应做版本化、
向后兼容的协议扩展，继续按约 1 Hz 采样，不要直接上传原始大日志。

1. **P0 数据可信度与排障**：逐点 GPS 精度/fix 状态/航向/海拔；接管原因与驾驶员干预事件；
   上传 backlog、最旧待传 segment 时长、上传/解析失败计数；GPU 温度、内存、网络状态、panda/CAN 健康。
2. **P1 驾驶行为分析**：巡航设定速度、油门/刹车、横纵向控制状态、目标速度/加速度、
   前车距离/相对速度/置信度、车道线置信度。可解释接管、急刹和跟车表现。
3. **P2 BYD 专项**：挡位、转向灯、总里程，以及车端确实能取得的 SOC、燃油和能耗数据。
4. **默认不上传**：视频/驾驶员影像、完整 rlog、全量原始 CAN。仅在未来有明确脱敏、授权和调试需求时另行设计。

注意：heartbeat 协议虽已有 `uptime_s`、`cpu_temp_c`、`disk_free_mb`、`load1` 字段，但当前服务端
只把 `meta` 持久化到设备表；要展示这些指标，需先放入 `meta`，或后续同时规划服务端存储迁移。

## 8. 登录/账号的后续规划（当前未实现）

大屏先只读；上线前再补：

- 用户表：`users`（email、password_hash、nickname、role）
- 会话：`user_sessions`（token_hash、expires_at）
- 设备归属：`devices.owner_user_id`、`device_secret_hash`
- 车机 Web 设置登录：车机生成 `device_uid + device_secret`，用户在 Web 设置输入网站账号，
  服务端校验后签发/轮换设备上传令牌；车机只保存设备令牌，不保存密码
- 邮箱验证码：参考服务器
  `/www/wwwroot/API/controllers/ToolsSwitch.py` 的 `sendQQmail`（SMTP 凭据不要复制进仓库）
- 注册/登录 API 建议放在 `/bp-api/v1/auth/*`，车机写接口继续用 `X-Device-Token` 不变
- 大屏登录后，BFF 用用户会话查自己的设备/行程；不要再把只读令牌发到浏览器

在登录完成前，**不要**把只读令牌或数据库密码写进公开前端。

## 9. 验收清单

- [x] 大屏在 `bp.fd-tw.com` 可访问，`/bp-api/v1/health` 不受影响
- [x] 使用真实数据：默认过滤 `min_distance_m>=100`，停车 trip 不污染统计
- [x] KPI、日趋势、地图、行程详情、告警、设备状态至少各有一个真实数据视图
- [x] 行程轨迹可回放/缩放，无 GPS 行程有空态
- [x] 移动端可用，无横向滚动溢出
- [x] 前端源码/构建产物无 `.read_token`、设备令牌、数据库密码
- [x] 刷新/断网/接口 5xx 有错误态和重试
- [x] 不修改 `/bp-api/` 的接收/上传逻辑
- [x] 提供 README：启动、构建、nginx、缓存、数据口径

2026-10-24 回归：真实 Chrome 浏览器验证公网首页、桌面 1440px 与移动端 390px；速度曲线、
Geo `lines` 轨迹、缩放/适配、2× 回放、无 GPS 空态、80 条分页加载和详情失败重试均通过。

## 10. 给 AI 的启动提示词

```text
你是 ButterflyPilot 可视化大屏的开发 AI。先读：
- /www/wwwroot/butterflyPilot/AI_VISUALIZATION_GUIDE.md
- /www/wwwroot/butterflyPilot/README.md
- https://bp.fd-tw.com/bp-api/docs

约束：
- 只读数据；不要修改 /www/wwwroot/butterflyPilot 的上传/接收服务
- 不要碰车机写令牌 bpd_*；只读令牌从 /www/wwwroot/butterflyPilot/.read_token 读取
- 大屏已合并进 /www/wwwroot/butterflyPilot：前端在 dashboard/，BFF 在 app/routers/dashboard.py
- 根路径 / 由 butterflyPilot.service (18181) 提供；旧 butterflyPilot-view.service 已停用
- 不要占用 /bp-api/，不要改 ingest 上传逻辑
- 默认过滤停车行程；GPS 缺失要有空态；时间按 +08:00
- 先做可运行的 v1（KPI + 地图 + 行程 + 趋势 + 告警），再迭代酷炫动效
- 每完成一步给出启动命令、验证 URL、关键截图/接口返回
```
