Files
desungongpai/.claude/CLAUDE.md
default 9daa81621c docs: 更新CLAUDE.md反映最新API和前端变化
- routers注释更新: 补充/stats、/batch-acknowledge、/report、/broadcast等新端点
- 管理后台UI描述更新: 各页面统计面板、批量确认、轨迹摘要等新功能
- main.py描述更新: 补充system/overview和system/cleanup端点

via [HAPI](https://hapi.run)

Co-Authored-By: HAPI <noreply@hapi.run>
2026-03-31 10:14:54 +00:00

958 lines
62 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Badge Admin - KKS P240/P241 蓝牙工牌管理系统
## 项目概览
KKS P240/P241 蓝牙工牌管理后台,基于 FastAPI + SQLAlchemy + asyncio TCP 服务器。
支持设备管理、实时定位、告警、考勤打卡、蓝牙记录、指令下发、TTS语音播报等功能。
## 项目结构
```
/home/gpsystem/
├── run.py # 启动脚本 (uvicorn)
├── .env.example # 环境变量模板 (复制为 .env 使用)
├── requirements.txt # Python 依赖
├── frpc.toml # FRP 客户端配置 (TCP隧道)
├── badge_admin.db # SQLite 数据库
├── server.log # 服务器日志
├── app/
│ ├── main.py # FastAPI 应用入口, 挂载静态文件, 启动TCP服务器, /api/system/overview, /api/system/cleanup
│ ├── config.py # 配置 (pydantic-settings, .env支持, 端口/API密钥/缓存/限流)
│ ├── database.py # SQLAlchemy async 数据库连接
│ ├── dependencies.py # FastAPI 依赖 (多API Key认证 + 权限控制: read/write/admin)
│ ├── extensions.py # 共享实例 (rate limiter, 真实IP提取)
│ ├── websocket_manager.py # WebSocket 连接管理器 (topic订阅, 实时广播)
│ ├── models.py # ORM 模型 (Device, LocationRecord, AlarmRecord, HeartbeatRecord, AttendanceRecord, BluetoothRecord, BeaconConfig, FenceConfig, DeviceFenceBinding, DeviceFenceState, CommandLog, ApiKey)
│ ├── schemas.py # Pydantic 请求/响应模型
│ ├── tcp_server.py # TCP 服务器核心 (~2400行), 管理设备连接、协议处理、数据持久化
│ ├── geocoding.py # 高德地理编码服务 (IoT v5 定位 + 逆地理编码 + legacy回退)
│ │
│ ├── protocol/
│ │ ├── constants.py # 协议常量 (协议号、告警类型snake_case、信号等级等)
│ │ ├── parser.py # 二进制协议解析器
│ │ ├── builder.py # 响应包构建器
│ │ └── crc.py # CRC-ITU (CRC-16/X-25, 多项式 0x8408)
│ │
│ ├── services/
│ │ ├── device_service.py # 设备 CRUD
│ │ ├── command_service.py # 指令日志 CRUD
│ │ ├── location_service.py # 位置记录查询
│ │ ├── beacon_service.py # 蓝牙信标 CRUD
│ │ ├── fence_service.py # 电子围栏 CRUD + 设备绑定管理
│ │ ├── fence_checker.py # 围栏自动考勤引擎 (几何判定+状态机+自动打卡)
│ │ └── tcp_command_service.py # TCP指令抽象层 (解耦routers↔tcp_server)
│ │
│ ├── routers/
│ │ ├── devices.py # /api/devices (含 /stats增强, /batch, /batch-delete, /all-latest-locations)
│ │ ├── commands.py # /api/commands (含 /send, /message, /tts, /batch, /broadcast, /stats)
│ │ ├── locations.py # /api/locations (含 /stats, /track-summary/{id}, /latest, /batch-latest, /track, /batch-delete, /delete-no-coords)
│ │ ├── alarms.py # /api/alarms (含 /stats增强, /batch-acknowledge, /batch-delete, acknowledge, alarm_source过滤)
│ │ ├── attendance.py # /api/attendance (含 /stats增强, /report, /device/{id}, attendance_source过滤)
│ │ ├── bluetooth.py # /api/bluetooth (含 /stats, beacon_mac过滤, /batch-delete)
│ │ ├── heartbeats.py # /api/heartbeats (含 /stats, /batch-delete, 心跳记录查询)
│ │ ├── beacons.py # /api/beacons (信标管理 CRUD)
│ │ ├── fences.py # /api/fences (含 /stats, /{id}/events, /all-active, 设备绑定CRUD)
│ │ ├── geocoding.py # /api/geocode (POI搜索代理 /search, 逆地理编码 /reverse)
│ │ ├── api_keys.py # /api/keys (API密钥管理 CRUD, admin only)
│ │ └── ws.py # /ws (WebSocket实时推送, topic订阅)
│ │
│ └── static/
│ └── admin.html # 管理后台 SPA (暗色主题, 10个页面)
└── doc/
└── KKS_Protocol_P240_P241.md # 协议文档 (从PDF转换)
```
## 架构设计
### 网络拓扑
```
[P241 工牌] --TCP--> [152.69.205.186:5001 (frps)]
|
FRP Tunnel
|
[Container:5000 (frpc)]
|
[TCP Server (asyncio)]
|
[FastAPI :8088] --CF Tunnel--> [Web 访问]
```
### 端口说明
- **8088**: FastAPI HTTP API + 管理后台 UI
- **5000**: TCP 服务器 (KKS 二进制协议)
- **5001**: 远程服务器 FRP 映射端口 (152.69.205.186)
- **7000**: FRP 服务端管理端口
- **7500**: FRP Dashboard (admin/PassWord0325)
### 配置管理
- `app/config.py` 使用 **pydantic-settings** (`BaseSettings`),支持 `.env` 文件覆盖默认值
- `.env.example` 提供所有可配置项模板,复制为 `.env` 使用
- DATABASE_URL 使用绝对路径 (基于 `__file__` 计算项目根目录)
- 高德 API 密钥集中在 `config.py``geocoding.py``settings` 导入 (含 AMAP_HARDWARE_SECRET)
- 端口号有 pydantic 校验 (ge=1, le=65535)
## 启动服务
```bash
# 启动服务
cd /home/gpsystem
nohup python3 -m uvicorn app.main:app --host 0.0.0.0 --port 8088 > server.log 2>&1 &
# 启动 FRP 客户端 (TCP隧道)
nohup /tmp/frpc -c /home/gpsystem/frpc.toml > /tmp/frpc.log 2>&1 &
# 检查服务状态
curl http://localhost:8088/health
curl http://localhost:8088/api/devices
```
## 管理后台 UI
访问 `/admin` 路径,包含以下页面:
1. **仪表盘** - 设备统计、今日告警/考勤/定位、在线率%、告警类型饼图、最近告警 (调用 system/overview + devices/stats + alarms/stats)
2. **设备管理** - 添加/编辑/删除设备、定位模式列、点击排序、快捷指令按钮、广播指令
3. **位置追踪** - 高德 JS API 2.0 3D 地图、轨迹回放+摘要(距离/时长/速度)、地址显示、左侧设备面板、低精度过滤、全部设备总览Tab
4. **告警管理** - 告警列表、单条/批量确认、今日计数、告警类型饼图、位置/地址显示
5. **考勤记录** - 打卡记录+今日计数、按来源筛选(设备/蓝牙/围栏) (0xB0/0xB1 GPS+LBS+WiFi)
6. **蓝牙记录** - 统计面板(总记录/打卡/定位/信标数)、蓝牙打卡(0xB2)/定位(0xB3)记录含信标MAC/UUID/RSSI等
7. **信标管理** - 蓝牙信标注册、位置配置MAC/UUID/楼层/区域/经纬度)、左侧信标面板
8. **电子围栏** - 统计面板(总围栏/启用/绑定/今日事件)、围栏管理(圆形/多边形/矩形)、地图绘制、POI搜索、左侧围栏面板、顶部Tab切换(围栏管理/设备绑定)
9. **数据日志** - 多类型数据记录统一查询 (位置/心跳/告警/考勤/蓝牙)
10. **指令管理** - 统计面板(总数/成功率/已发送/失败)、发送指令/留言/TTS语音、广播指令
## 协议详情
KKS 二进制协议,详见 `doc/KKS_Protocol_P240_P241.md`
### 已支持协议号
| 协议号 | 名称 | 方向 |
|--------|------|------|
| 0x01 | Login 登录 | 终端→服务器 |
| 0x13 | Heartbeat 心跳 | 双向 |
| 0x1F | Time Sync 时间同步 | 双向 |
| 0x22 | GPS 定位 | 终端→服务器 |
| 0x26 | Alarm ACK 告警确认 | 服务器→终端 |
| 0x28 | LBS 多基站 | 终端→服务器 |
| 0x2C | WiFi 定位 | 终端→服务器 |
| 0x36 | Heartbeat Extended 扩展心跳 | 双向 |
| 0x80 | Online Command 在线指令 | 服务器→终端 |
| 0x81 | Online Command Reply | 终端→服务器 |
| 0x82 | Message 留言 | 服务器→终端 |
| 0x94 | General Info (ICCID/IMSI等) | 双向 |
| 0xA0 | 4G GPS 定位 | 终端→服务器 |
| 0xA1 | 4G LBS 多基站 | 终端→服务器 |
| 0xA2 | 4G WiFi 定位 | 终端→服务器 |
| 0xA3-0xA5 | 4G 告警 (围栏/LBS) | 终端→服务器 |
| 0xA9 | WiFi 告警 | 终端→服务器 |
| 0xB0-0xB1 | 考勤打卡 | 双向 |
| 0xB2 | 蓝牙打卡 | 双向 |
| 0xB3 | 蓝牙定位 | 终端→服务器 |
### 数据包格式
```
[Start 0x7878/0x7979] [Length 1-2B] [Protocol 1B] [Content NB] [Serial 2B] [CRC 2B] [Stop 0x0D0A]
```
- `0x7878`: 包长度占 1 字节
- `0x7979`: 包长度占 2 字节 (用于 0xB3 蓝牙定位等大包)
### GPS 坐标编码 (协议文档)
- **纬度/经度**: 4字节大端序无符号整数 ÷ 1,800,000 = 度数
- **坐标系**: WGS-84 (GPS芯片原生输出)
- **航向/状态** (2字节): Bit15-14保留, Bit13=实时差分, Bit12=GPS定位, Bit11=0东经/1西经, Bit10=0南纬/1北纬, Bit9-0=航向(0-360度)
### 2G vs 4G 协议差异
| 字段 | 2G (0x22/0x28/0x2C/0xB0) | 4G (0xA0/0xA1/0xA2/0xB1) |
|------|--------------------------|--------------------------|
| MNC | 1 或 2 字节 (MCC bit15) | 固定 2 字节 |
| LAC | 2 字节 | 4 字节 |
| Cell ID | 3 字节 | 8 字节 |
| 邻区 LAC | 2 字节 | 4 字节 |
| 邻区 Cell ID | 3 字节 | 8 字节 |
### 告警代码 (alarm_code, 协议文档)
| 代码 | 名称 | 代码 | 名称 |
|------|------|------|------|
| 0x00 | 正常 | 0x0C | 开机报警 |
| 0x01 | SOS求救 | 0x0D | GPS首次定位 |
| 0x02 | 断电报警 | 0x0E | 外电低电报警 |
| 0x03 | 震动报警 | 0x0F | 外电低电保护 |
| 0x04 | 进围栏报警 | 0x10 | 换卡报警 |
| 0x05 | 出围栏报警 | 0x11 | 关机报警 |
| 0x06 | 超速报警 | 0x12 | 低电飞行模式 |
| 0x09 | 位移报警 | 0x13 | 拆卸报警 |
| 0x0A | 进GPS盲区 | 0x14 | 门报警 |
| 0x0B | 出GPS盲区 | 0x15 | 低电关机报警 |
| — | — | 0x16 | 声控报警 |
| — | — | 0x17 | 伪基站报警 |
| — | — | 0x18 | 开盖报警 |
| — | — | 0x19 | 内部电池低电 |
### terminal_info 字节 (考勤包 0xB0/0xB1/0xB2)
- Bit[5:2]: 打卡/报警类型
- `0001` = 上班打卡 (clock_in)
- `0010` = 下班打卡 (clock_out)
- `0100` = 开机报警
- `0110` = 低电报警
- `1000` = SOS 报警
- `1010` = 进电子围栏
- `1100` = 出电子围栏
- `1110` = 关机报警
### 蓝牙包字段 (协议文档)
- **0xB2 蓝牙打卡**: datetime(6) + RSSI(1) + MAC(6) + UUID(16) + Major(2) + Minor(2) + 电量(2) + terminal_info(1) + 保留(2)
- **0xB3 蓝牙定位**: datetime(6) + beacon_count(1) + N×[RSSI(1) + MAC(6) + UUID(16) + Major(2) + Minor(2) + 电量(2) + 电量单位(1)] = 每信标30字节
- **电量单位**: 0=伏(值×0.01V), 1=百分比
### 考勤包字段差异 (协议文档)
- **0xB0**: datetime(6) + gps_positioned(1) + 保留(2) + GPS(12) + terminal_info(1) + voltage(1) + gsm(1) + 扩展(2) + **2G LBS+WiFi**
- **0xB1**: datetime(6) + gps_positioned(1) + 保留(2) + GPS(12) + terminal_info(1) + voltage(1) + gsm(1) + 扩展(2) + **4G LBS+WiFi**
- **响应格式**: datetime(6) + 状态(1, 1=成功) + 打卡类型(1, 1=上班/2=下班) + 保留(2)
## 当前设备
| 字段 | 值 |
|------|-----|
| IMEI | 868120334031363 |
| 型号 | P241 |
| 名称 | 测试 |
| DB ID | 1 |
| ICCID | 89860848102570005286 |
| IMSI | 460240388355286 |
| SIM卡 | 中国移动 IoT (MCC=460, MNC=0) |
| 位置 | 成都地区 (~30.605°N, 103.936°E) |
## 重要实现细节
### IMEI 解析
- BCD 编码: 8字节 = 16 hex digits, 首位为填充 0
- 使用 `raw_hex[1:]` 只移除首位填充0 (不用 lstrip 避免移除多个)
### 设备登录
- 登录时不覆盖用户设置的 device_type (避免被覆盖为 raw hex)
- 重连时先关闭旧连接再建立新连接
- 断开连接时检查 writer 是否匹配,避免误将新连接标记为 offline
### 告警处理
- 告警响应使用 **0x26 ACK** (非原始协议号回复)
- 告警类型常量使用 **snake_case** (如 `sos`, `vibration`, `enter_fence`)
- 4G 告警包解析:
- **0xA3/0xA4** (围栏告警): datetime(6) + gps(12) + lbs_length(1) + MCC/MNC + LAC(4) + CellID(8) + terminal_info(1) + voltage_level(**1**字节) + gsm_signal(1) + alarm_code(1) + language(1)
- **0xA5** (LBS告警): **无datetime, 无GPS** — 直接从 MCC 开始
- **0xA9** (WiFi告警): datetime(6) + MCC/MNC + cell_type(1) + cell_count(1) + 基站列表 + timing_advance(1) + WiFi列表 + alarm_code(1) + language(1)
- MCC 高位 `0x8000` 标志: 表示 MNC 为 2 字节 (非默认1字节)
- voltage_level 是 **1字节** (0x00-0x06 等级)不是2字节毫伏值
- LBS/WiFi 报警自动执行前向地理编码(基站→经纬度)+ 逆地理编码(经纬度→地址)
### 坐标系统与地理编码
#### 坐标系统概览
| 来源 | 坐标系 | 说明 |
|------|--------|------|
| GPS 芯片 (0x22/0xA0/0xB0/0xB1) | **WGS-84** | 原始值 ÷ 1,800,000GPS硬件原生输出 |
| 高德前向地理编码 (LBS/WiFi) | **GCJ-02** | 高德 API 返回的都是 GCJ-02 |
| 高德逆地理编码输入 | **GCJ-02** | 需将 WGS-84 转 GCJ-02 后查询 |
| 高德 JS API 前端地图 | **GCJ-02** | 高德地图原生使用 GCJ-02 |
| 数据库存储 | **统一 WGS-84** | 高德返回 GCJ-02 经 `gcj02_to_wgs84()` 反转后存储 |
#### 坐标统一方案 (已修复)
- **修复**: `geocoding.py` 的 v5 API (`_geocode_amap_v5`) 和旧版 API (`_geocode_amap_legacy`) 返回前均将高德 GCJ-02 坐标反转为 WGS-84
- **新增函数**: `gcj02_to_wgs84()` — 使用迭代近似法 (lat*2 - gcj_lat)
- **结果**: 数据库统一存储 WGS-84前端/逆地理编码/围栏 统一做 WGS-84→GCJ-02 转换,无二次偏移
- **注意**: 修复前已存储的 LBS/WiFi 历史数据仍为 GCJ-02新数据为 WGS-84
#### GPS 坐标解析 (协议文档)
- **格式**: 纬度(4字节) + 经度(4字节),大端序无符号整数
- **计算**: 十进制值 ÷ 1,800,000 = 度数
- **方向**: 由 course_status 字段的 bit 位决定:
- Bit 11: 0=东经, 1=西经
- Bit 10: 0=南纬, 1=北纬 (注意: 1=北, 与直觉相反)
- Bit 12: GPS 定位状态 (1=已定位)
- Bit 13: GPS 实时/差分定位
- Bit 9-0: 航向 (0-360度, 正北为0)
#### 2G vs 4G 基站字段差异 (协议文档)
| 字段 | 2G (0x22/0x28/0x2C/0xB0) | 4G (0xA0/0xA1/0xA2/0xB1) |
|------|--------------------------|--------------------------|
| MNC | 1 或 2 字节 (MCC bit15 决定) | 固定 2 字节 |
| LAC | 2 字节 | 4 字节 |
| Cell ID | 3 字节 | 8 字节 |
| 邻区 LAC | 2 字节 | 4 字节 |
| 邻区 Cell ID | 3 字节 | 8 字节 |
#### 地理编码服务
- **所有地理编码服务统一使用高德 (Amap)**
- **前向地理编码** (`geocode_location`): 基站/WiFi → 经纬度 (GCJ-02)
- **v5 IoT 定位 API** (主): `POST restapi.amap.com/v5/position/IoT`,使用 AMAP_KEY (企业订阅)
- accesstype: 1=移动网络, 2=WiFi (需2+个AP)
- **network**: `GSM`(2G) / `WCDMA`(3G/4G) / `NR`(5G) — **关键参数,`LTE` 不是有效值!** 4G 必须用 `WCDMA`
- **bts 格式**: `mcc,mnc,lac,cellid,signal,cage` (6字段cage=信号新鲜度秒数v5 必须包含)
- **4G LTE**: LAC 字段传 TACCellID 字段传 ECI (28位>65535)
- WiFi 需 mmac (最强信号AP) + macs (其余AP)MAC 用冒号分隔小写
- diu 替代 imei 参数show_fields=formatted_address 可直接返回地址
- POST body 方式提交key 在 URL params无需数字签名
- `location_type` 参数自动映射: lbs_4g/wifi_4g → network=WCDMA, lbs/wifi → network=GSM
- **旧版智能硬件定位 API** (回退): `GET apilocate.amap.com/position`,使用 AMAP_HARDWARE_KEY
- 仅当 v5 API 失败且 AMAP_HARDWARE_KEY 已配置时使用
- 使用 AMAP_HARDWARE_SECRET 签名 (与 AMAP_SECRET 独立)
- **重要**: bts 格式只能 5 字段 (`mcc,mnc,lac,cellid,signal`)**不能包含 cage 字段**,否则返回空位置
- **逆地理编码** (`reverse_geocode`): 经纬度 → 中文地址
- **高德**: `restapi.amap.com/v3/geocode/regeo`,使用 AMAP_KEY
- 输入需 WGS84→GCJ02 坐标转换 (服务端 Python 实现)
- 缓存策略: 坐标四舍五入到3位小数 (~100m) 作为缓存key
- **POI 搜索代理** (`/api/geocode/search`): 关键词搜索地点,返回 GCJ-02 坐标
- 代理 `restapi.amap.com/v3/place/text`AMAP_KEY 不暴露到前端
- 用于围栏弹窗搜索地点功能
- 内置 LRU 缓存 (maxsize=10000)WiFi 和基站分别缓存,基站缓存 key 包含邻区基站 hash (有/无邻区精度不同)
- **WGS84→GCJ02 服务端转换**: geocoding.py 内置 `wgs84_to_gcj02()` 函数 (与前端 JS 版一致)
- **高德数字签名**: `_amap_sign()` — 参数按key排序拼接 + AMAP_SECRET → MD5 → sig 参数 (逆地理编码/POI搜索用)
### API 认证与限流
- **认证**: 设置 `API_KEY` 环境变量后,所有 `/api/` 请求需携带 `X-API-Key` 请求头
- **多 API Key**: 支持 master key (环境变量) + 数据库管理的 API Key (SHA-256 hash)
- **权限级别**: `read` (只读) < `write` (读写) < `admin` (管理,含 key 管理)
- **权限控制**: 所有 POST/PUT/DELETE 端点需要 `write` 权限,`/api/keys` 需要 `admin` 权限
- **Key 管理**: `POST /api/keys` 创建 key (返回明文一次), `GET /api/keys` 列表, `PUT /api/keys/{id}` 更新, `DELETE /api/keys/{id}` 停用
- **限流**: 全局 60/min (default_limits),写操作 30/min (`@limiter.limit`)
- **真实 IP**: 从 `X-Forwarded-For``CF-Connecting-IP``request.client.host` 提取
- **CORS**: `CORS_ORIGINS=*` 时自动禁用 `allow_credentials`
### WebSocket 实时推送
- **端点**: `ws://host/ws?api_key=xxx&topics=location,alarm`
- **Topics**: location, alarm, device_status, attendance, bluetooth, fence_attendance
- **认证**: query param api_key (支持 master key + DB key)
- **最大连接**: 100 个 WebSocket 连接
- **消息格式**: JSON `{"topic": "...", "data": {...}, "timestamp": "..."}`
- **广播点**: 位置存储、报警存储、设备上下线、考勤存储、蓝牙存储
- **Ping/Pong**: 客户端发送 "ping" 保活,服务器回复 "pong"
### 数据清理
- **自动清理**: 后台定时任务,每 `DATA_CLEANUP_INTERVAL_HOURS`(默认24) 小时执行
- **保留天数**: `DATA_RETENTION_DAYS`(默认90) 天,删除过期的 location/heartbeat/alarm/attendance/bluetooth 记录
- **手动清理**: `POST /api/system/cleanup` (admin only)
### 批量操作 API
- `POST /api/devices/batch` — 批量创建 (最多500),输入去重 + DB去重结果按输入顺序
- `PUT /api/devices/batch` — 批量更新,单次 WHERE IN 查询 + 单次 flush
- `POST /api/devices/batch-delete` — 批量删除 (最多100),通过 body 传 device_ids
- `POST /api/locations/batch-latest` — 批量获取多设备最新位置 (最多100)
- `POST /api/locations/batch-delete` — 批量删除位置记录 (最多500),通过 body 传 location_ids
- `POST /api/locations/delete-no-coords` — 删除无坐标记录 (lat/lon为NULL),支持 device_id/时间范围过滤
- `GET /api/devices/all-latest-locations` — 获取所有在线设备最新位置
- `POST /api/commands/batch` — 批量发送指令 (最多100)`model_validator` 互斥校验
- 所有批量操作使用 WHERE IN 批量查询,避免 N+1
### API 分页
- page_size 最大限制为 100 (schema 层校验)
- 前端设备选择器使用 page_size=100 (不能超过限制)
### CRC 算法
- CRC-ITU / CRC-16/X-25
- Reflected polynomial: 0x8408
- Initial value: 0xFFFF
- Final XOR: 0xFFFF
### TCP 指令下发
- 0x80 (Online Command): ASCII 指令 + 2字节语言字段 (`0x0001`=中文)
- 0x81 (Command Reply): 设备回复,格式 length(1) + server_flag(4) + content(n) + language(2)
- 0x82 (Message): **UTF-16 BE** 编码的文本消息 (非UTF-8)
- TTS: 通过 0x80 发送 `TTS,<文本>` 格式
- 常用指令: `GPSON#` 开启GPS, `BTON#` 开启蓝牙, `BTSCAN,1#` 开启BLE扫描
- 已验证可用指令: `BTON#`, `BTSCAN,1#`, `GPSON#`, `MODE,1/3#`, `PARAM#`, `CHECK#`, `VERSION#`, `TIMER#`, `WHERE#`, `STATUS#`, `RESET#`
- **架构**: `tcp_command_service.py` 作为抽象层解耦 routers↔tcp_server 的循环导入,通过 lazy import 访问 tcp_manager
- **重要**: TCP 层 (`send_command`/`send_message`) 只负责发送,不创建 CommandLog。CommandLog 由 API 层 (commands.py) 管理
- **重要**: 服务器启动时自动将所有设备标记为 offline等待设备重新登录
### 蓝牙信标管理
- **BeaconConfig 表**: 注册蓝牙信标,配置 MAC/UUID/Major/Minor/楼层/区域/经纬度/地址
- **自动关联**: 0xB2 打卡和 0xB3 定位时,根据 beacon_mac 查询 beacon_configs 表
- **位置写入**: 将已注册信标的经纬度写入 BluetoothRecord 的 latitude/longitude
- **多信标定位**: 0xB3 多信标场景,取 RSSI 信号最强的已注册信标位置
- **当前状态**: ✅ 0xB2 蓝牙打卡已验证可用 (2026-03-18),设备成功检测信标并上报数据
- **已验证信标**: MAC=`C3:00:00:34:43:5E`, UUID=FDA50693-A4E2-4FB1-AFCF-C6EB07647825, Major=10001, Minor=19641
- **注意**: 信标需配置经纬度/地址,否则打卡记录中位置为空
- **制造商**: 几米物联 (Jimi IoT / jimiiot.com.cn)P240/P241 智能电子工牌系列
### P241 蓝牙模式配置指南 (设备端)
设备蓝牙模式通过在线指令 (0x80) 配置,需依次完成以下步骤:
#### 步骤1: 设置打卡方式
- **指令**: `CLOCKWAY,A#`
- **参数 A**: `1`=仅手动打卡, `2`=仅蓝牙打卡, `3`=手动+蓝牙打卡 (推荐)
- **示例**: `CLOCKWAY,3#` → 回复 `setting OK.打卡方式:手动+蓝牙打卡`
#### 步骤2: 关闭/开启打卡才工作限制 (可选)
- **指令**: `BTWORKSW,ON#``BTWORKSW,OFF#`
- 设备默认需打卡后才开始工作,如不需要此限制可关闭
#### 步骤3: 设置工作模式
- **指令**: `MODE,A#`
- **参数 A**: `0`=计步定位, `1`=定时定位, `2`=蓝牙定位, `3`=智能模式(ZNMS)
- **示例**: `MODE,2#` → 回复 `setting OK.Bt`
#### 步骤4: 写入信标数据 (三种方式任选其一)
**方式1 — MAC 地址绑定**:
- **指令**: `BTMACSET,MAC1,MAC2,...#``BTMACSET1,MAC1,...#` ~ `BTMACSET4,MAC1,...#`
- 共 5 个序号 (默认+1~4),每个序号最多 10 个 MAC总计最多 **50 个 MAC 地址**
- MAC 格式: 冒号分隔,如 `C3:00:00:34:43:5E`
- **示例**: `BTMACSET,C3:00:00:34:43:5E#` → 回复 `setting OK.bt mac address:1,C3:00:00:34:43:5E;`
**方式2 — UUID 绑定 (Major+Minor)**:
- **指令**: `BTUUIDSET,MAJOR,MINOR,MAJOR1,MINOR1,...#``BTUUIDSET1,...#` ~ `BTUUIDSET4,...#`
- 共 5 个序号,每个序号最多 10 个 UUID总计最多 **50 个 UUID**
- **注意**: Major/Minor 需从十进制转换为十六进制
- **示例**: Major=10001(0x2711), Minor=19641(0x4CA9) → `BTUUIDSET,2711,4CA9#`
**方式3 — 仅 MAJOR 匹配**:
- **指令**: `ONLYMAJOR,ON/OFF,MAJOR#`
- ON 时蓝牙搜索只匹配 MAJORMAJOR 需十进制转十六进制)
- **示例**: `ONLYMAJOR,ON,2711#`
#### 步骤5: 语音播报开关
- **指令**: `BTMP3SW,A#`
- **参数 A**: `0`=关闭, `1`=开启
- 开启后设备识别到信标会语音播报"定位成功"
- **示例**: `BTMP3SW,1#` → 回复 `setting OK.BTWorkMp3SW:1`
#### 完整配置示例 (已验证 2026-03-30)
```bash
CLOCKWAY,3# # → setting OK.打卡方式:手动+蓝牙打卡
MODE,2# # → setting OK.Bt
BTMACSET,C3:00:00:34:43:5E# # → setting OK.bt mac address:1,C3:00:00:34:43:5E;
BTMP3SW,1# # → setting OK.BTWorkMp3SW:1
```
配置完成后,设备在每次心跳 (约5分钟) 扫描到已绑定信标时会语音播报"定位成功",并上报 0xB2 蓝牙打卡数据
#### 其他蓝牙相关指令
- `BTON#` → 开启蓝牙 (回复 `setting OK.btON:1`)
- `BTSCAN,1#` → 开启蓝牙扫描 (回复 `setting OK.btSCAN:1`)
### 电子围栏管理
- **FenceConfig 表**: 注册电子围栏,支持 circle/polygon/rectangle 三种类型
- **圆形围栏**: center_lat/center_lng (WGS-84) + radius (米)
- **多边形/矩形围栏**: points 字段存储 JSON `[[lng,lat], ...]` (WGS-84)
- **显示属性**: color (边框色), fill_color (填充色), fill_opacity (透明度), is_active (启用状态)
- **API 端点**: `/api/fences` CRUD + `/api/fences/all-active` 获取所有启用围栏
- **前端绘制**: 高德 MouseTool 组件,多边形默认 (左键添加顶点, 右键/双击完成), 圆形 (点击拖拽)
- **POI 搜索**: 围栏弹窗支持搜索地点并定位到搜索结果 (通过 `/api/geocode/search` 代理)
- **坐标转换**: 绘制完成后 GCJ-02→WGS-84 存储,显示时 WGS-84→GCJ-02 渲染
- **围栏面板**: 左侧面板显示围栏列表,支持显示/隐藏、编辑、绑定设备、定位到围栏
### 设备-围栏绑定与自动考勤
- **DeviceFenceBinding 表**: 多对多关系,绑定设备到围栏 (唯一约束 device_id+fence_id)
- **DeviceFenceState 表**: 状态机记录,跟踪每个设备-围栏对的进出状态 (is_inside, last_transition_at)
- **绑定 API**:
- `GET /api/fences/{id}/devices` — 获取围栏绑定的设备列表 (含进出状态)
- `POST /api/fences/{id}/devices` — 绑定设备到围栏 (body: device_ids, 幂等)
- `DELETE /api/fences/{id}/devices` — 解绑设备 (body: device_ids)
- **自动考勤引擎** (`fence_checker.py`):
- `haversine_distance()` — WGS-84 坐标间距离 (米)
- `is_inside_circle()` — 半径判定
- `is_inside_polygon()` — Ray-casting 射线法判定
- `check_device_fences()` — 主入口: 查询设备绑定的活跃围栏 → 几何判定 → 状态转换 → 自动打卡
- **自动打卡**: 进入围栏创建 clock_in、离开围栏创建 clock_out (AttendanceRecord, protocol_number=0, attendance_source="fence", lbs_data.source="fence_auto")
- **容差机制**: GPS=0m, WiFi=100m (FENCE_WIFI_TOLERANCE_METERS), LBS=200m (FENCE_LBS_TOLERANCE_METERS)
- **防抖**: FENCE_MIN_INSIDE_SECONDS=60防止 GPS 抖动导致频繁进出
- **TCP 集成**: _store_location 存储位置后自动调用 check_device_fences事件通过 WebSocket 广播 (fence_attendance + attendance topics)
- **WebSocket topic**: `fence_attendance` — 围栏进出事件实时推送
- **前端**: 围栏管理页面顶部 Tab 切换布局 — "围栏管理" Tab (地图+围栏列表) 和 "设备绑定" Tab (全屏绑定矩阵, 无地图)
- 围栏管理 Tab: 工具栏+地图+围栏表格,与原有布局一致
- 设备绑定 Tab: 独立工具栏(刷新/提示/保存)、全高绑定矩阵表格、表头sticky固定
### 考勤来源区分
- **attendance_source 字段**: 区分考勤记录来源String(20),默认 "device"
- `device` — 设备硬件打卡 (0xB0/0xB1 协议)
- `bluetooth` — 蓝牙信标打卡 (0xB2 协议)
- `fence` — 围栏自动打卡 (fence_checker.py, protocol_number=0)
- **TCP 层**: 根据协议号自动设置 `_att_source = "bluetooth" if proto == 0xB2 else "device"`
- **围栏引擎**: fence_checker.py `_create_attendance()` 设置 `attendance_source="fence"`
- **前端**: 考勤记录表格增加"来源"列 (设备/蓝牙/围栏 图标),支持按来源筛选
- **API**: `GET /api/attendance` 新增 `attendance_source` 查询参数过滤
### 0x94 General Info 子协议
- 子协议 0x0A: IMEI(8字节) + IMSI(8字节) + ICCID(10字节)
- 子协议 0x09: GPS 卫星状态
- 子协议 0x00: ICCID(10字节)
- 子协议 0x04: 设备配置上报 `ALM2=40;ALM4=E0;MODE=03;IMSI=...`
### 前端字段映射 (admin.html)
- 设备信号: `d.gsm_signal` (非 `d.signal_strength`)
- 指令响应: `c.response_content` (非 `c.response`)
- 响应时间: `c.response_at || c.sent_at` (非 `c.updated_at`)
- 位置地址: `l.address` (高德逆地理编码结果)
- 卫星数: `l.gps_satellites` (非 `l.accuracy`)
- 记录时间: `l.recorded_at` (非 `l.timestamp`)
- 报警来源: `a.alarm_source` (非 `a.source`)
- 报警信号: `a.gsm_signal` (非 `a.signal_strength`)
- 报警类型: snake_case (如 `vibration`, `power_cut`, `voice_alarm`)
### 位置追踪低精度过滤
- **切换按钮**: "低精度" 按钮,点击后隐藏 LBS/WiFi 低精度定位点,仅显示 GPS
- **按钮状态**: 默认 `<i class="fas fa-eye"></i> 低精度` (灰色),激活后 `<i class="fas fa-eye-slash"></i> 低精度` (红色 #b91c1c)
- **影响范围**: 地图轨迹标记点 + 轨迹折线路径 + 记录表格行,三者联动
- **实时切换**: 已加载的轨迹和表格即时过滤,无需重新查询
- **判定逻辑**: `location_type``lbs``wifi` 开头视为低精度
- **轨迹标记颜色**: GPS=蓝色(#3b82f6), WiFi=青色(#06b6d4), LBS=橙色(#f59e0b), 蓝牙=紫色(#a855f7), 起点=绿色, 终点=红色
- **无精度圈**: 不绘制 LBS/WiFi 精度半径圆圈,仅通过标记颜色区分定位类型
## FRP 配置
```toml
# frpc.toml (容器端)
serverAddr = "152.69.205.186"
serverPort = 7000
auth.method = "token"
auth.token = "PassWord0325"
[[proxies]]
name = "badge-tcp"
type = "tcp"
localIP = "127.0.0.1"
localPort = 5000
remotePort = 5001
```
## 外部服务器
- IP: 152.69.205.186
- OS: Ubuntu 20.04, Python 3.11, 24GB RAM
- FRP Server: frps v0.62.1 (1panel Docker, network_mode: host)
- Config: /opt/1panel/apps/frps/frps/data/frps.toml
## 高德地图 API
### Key
| 名称 | Key | Secret | 用途 |
|------|-----|--------|------|
| 测试2 | `a9f4e04f5c8e739e5efb07175333f30b` | — | Web服务 (AMAP_KEY: IoT v5定位 + 逆地理编码 + POI搜索) |
| 测试 | `9c2fe56bb2bad44d238dd9b4be249e33` | `bfc4e002c49ab5f47df71e0aeaa086a5` | Web端 (JS API 2.0 3D地图, AMAP_SECRET用此key的secret) |
| 工牌 | `30eedee9d3c5cfcc43c3b27dd7654e3c` | — | 旧版智能硬件定位回退 (AMAP_HARDWARE_KEY, 可选) |
- **账号类型**: 企业认证开发者 (IoT 智能硬件订阅已开通)
### 已接入服务
- **✅ IoT 定位 v5** (`POST restapi.amap.com/v5/position/IoT`): WiFi+基站混合定位,使用 AMAP_KEY (企业订阅)
- **✅ 逆地理编码** (`restapi.amap.com/v3/geocode/regeo`): 经纬度 → 地址文本,服务端 WGS84→GCJ02 坐标转换,使用 AMAP_KEY
- **✅ POI 搜索** (`restapi.amap.com/v3/place/text`): 关键词搜索地点,服务端代理 (`/api/geocode/search`)
- **✅ 旧版智能硬件定位** (`apilocate.amap.com/position`): v5 失败时回退,使用 AMAP_HARDWARE_KEY
- **✅ 前端 3D 地图**: 高德 JS API 2.0 (GCJ-02)viewMode='3D'pitch=45暗色主题前端 WGS84→GCJ02 坐标转换
- **数字签名**: `_amap_sign()` — 参数按key排序拼接 + AMAP_SECRET → MD5 → sig 参数 (v5 API 不需签名)
### 配额 (企业认证开发者)
- 基础LBS服务: 5,000 次/日 (逆地理编码等)
- IoT 在线定位 v5: 1,000,000 次/日 (企业订阅)
## 已知限制
1. **IoT SIM 卡不支持 SMS** - 144 号段的物联网卡无法收发短信,需通过平台或 TCP 连接配置设备
2. **Cloudflare Tunnel 仅代理 HTTP** - TCP 流量必须通过 FRP 转发
3. **SQLite 单写** - 高并发场景需切换 PostgreSQL
4. **设备最多 100 台列表** - 受 page_size 限制,超过需翻页查询
5. **WiFi 定位精度依赖 AP 数量** - v5 API 要求至少 2 个 WiFi AP 才能使用 WiFi 定位模式,少于 2 个时回退为基站定位
## 已修复的问题 (Bug Fix 记录)
### 数据链路修复
1. **IMEI 解析** - 修复 BCD 解码逻辑,确保正确提取 15 位 IMEI
2. **设备类型覆盖** - 登录时不再覆盖用户设置的 device_type
3. **设备重连** - 新连接建立前先关闭旧连接,断开时检查 writer 身份
4. **告警类型提取** - 修正 alarm byte 位置,从 terminal_info+voltage+gsm_signal 之后读取
### 协议修复
5. **0x80 指令包** - 添加缺失的 2 字节语言字段 (`0x0001`)
6. **0x82 消息编码** - 从 UTF-8 改为 UTF-16 BE (协议要求)
7. **0x94 通用信息** - 完善子协议 0x0A (IMEI+IMSI+ICCID) 和 0x09 (GPS卫星状态) 解析
### 前端修复
8. **设备选择器为空** - `page_size=500` 超过 API 最大限制 100返回 422 错误
9. **位置页面崩溃** - API 返回 `data: null` 时未做空值检查
10. **轨迹查询失败** - 缺少必填的 start_time/end_time 参数
11. **字段名不匹配** - 修复 signal_strength→gsm_signal, response→response_content, source→alarm_source 等
### 定位功能修复
12. **WiFi/LBS 无坐标** - 添加 wifi/wifi_4g 解析分支 (原代码缺失)
13. **地理编码集成** - LBS/WiFi 定位数据自动转换为经纬度坐标
14. **邻近基站和WiFi数据** - 存储到 LocationRecord 的 neighbor_cells 和 wifi_data 字段
### 告警功能修复
15. **告警响应协议** - 改为使用 0x26 ACK 响应 (原来错误使用地址回复)
16. **voltage_level 解析** - 从2字节改为1字节 (0x00-0x06等级)
17. **0xA5 LBS告警格式** - 移除错误的 datetime/lbs_length 前缀,直接从 MCC 开始
18. **0xA9 WiFi告警** - 独立解析器 (非GPS格式),支持 cell_type/cell_count/WiFi AP 列表
19. **MNC 2字节标志** - `_parse_mcc_mnc_from_content` 正确处理 MCC 高位 0x8000 标志
20. **告警类型常量** - 改为 snake_case新增 voice_alarm(0x16), fake_base_station(0x17), cover_open(0x18), internal_low_battery(0x19)
21. **LBS/WiFi 报警定位** - 为无GPS的报警添加前向地理编码
### 逆地理编码
22. **逆地理编码集成** - 位置和报警记录自动获取中文地址 (高德逆地理编码)
23. **地址字段** - LocationRecord 新增 address 字段,前端位置表/报警表/地图弹窗显示地址
### 蓝牙与考勤功能
24. **0xB0/0xB1 考勤解析** - 完整解析 GPS+LBS+WiFi 考勤包含基站邻区、WiFi AP、前向+逆地理编码
25. **0xB2 蓝牙打卡解析** - 解析 iBeacon 数据 (RSSI/MAC/UUID/Major/Minor/电池/打卡类型)
26. **0xB3 蓝牙定位解析** - 解析多信标数据,每信标 30 字节,支持电压(V)/百分比(%)电池单位
27. **考勤类型检测** - 从 terminal_info 字节 bits[5:2] 判断 clock_in(0001)/clock_out(0010)
28. **信标管理系统** - BeaconConfig CRUD注册信标物理位置TCP 自动关联信标经纬度
### 指令发送修复
29. **server_flag_str 未定义** - send_command 中记录日志时变量未定义导致 NameError
30. **TCP 层重复创建日志** - 简化 send_command/send_message 只负责发送CommandLog 由 API 层管理
31. **IMEI lstrip 问题** - `lstrip("0")` 可能删除多个前导零,改为 `raw_hex[1:]`
32. **设备启动状态** - 服务器启动时重置所有设备为 offline避免显示在线但实际未连接
33. **0x81 回复解析** - 去除末尾 2 字节语言字段,正确提取回复文本
### 协议全面审计修复 (2026-03-16)
34. **0x1F 时间同步格式** - 从6字节(YY MM DD HH MM SS)改为4字节Unix时间戳+2字节语言字段
35. **地址回复完整格式** - 重写 `_send_address_reply`,实现完整格式: server_flag(4)+ADDRESS/ALARMSMS(7-8)+&&+UTF16BE地址+&&+电话(21)+##
36. **报警地址回复** - 报警包在发送0x26 ACK后额外发送0x17地址回复(含ALARMSMS标记)
37. **0x82 留言格式** - 添加 server_flag(4字节) + language(2字节) 字段
38. **0x22 GPS Cell ID** - 从2字节改为3字节解析 (2G基站CellID为3字节)
39. **登录包时区/语言** - 解析 content[10:12] 提取时区(bit15-04)和语言(bit00)存入Device
40. **0x94 不需回复** - 移除服务器响应,从 PROTOCOLS_REQUIRING_RESPONSE 中删除
41. **WiFi缓存+LRU淘汰** - 新增 LRUCache(OrderedDict, maxsize=10000)替换所有dict缓存修复WiFi缓存未写入
42. **前端4G筛选** - 位置类型筛选添加 gps_4g/wifi_4g/lbs_4g 选项
43. **DATA_REPORT_MODES** - 修正所有模式名称匹配协议文档
### 架构优化 (2026-03-17)
44. **配置集中化** - API密钥从 geocoding.py 硬编码移至 config.py (pydantic-settings),支持 .env 覆盖
45. **数据库绝对路径** - DATABASE_URL 从相对路径改为基于 `__file__` 的绝对路径,避免 CWD 依赖
46. **tcp_command_service 抽象层** - 新建 services/tcp_command_service.py通过 lazy import 解耦 routers↔tcp_server 循环依赖
47. **commands.py 去重** - send_command/send_message/send_tts 提取 `_send_to_device()` 通用函数
48. **协议常量扩展** - constants.py 新增 DEFAULT_DEVICE_TYPE, SERVER_FLAG_BYTES, ATTENDANCE_TYPES, COURSE_BIT_*, MCC_MNC2_FLAG, VOLTAGE_LEVELS
49. **前端侧边面板** - 位置追踪/信标管理页面添加左侧设备/信标列表面板,自动选中最近活跃设备
50. **前端面板解耦** - 提取 PANEL_IDS 配置 + _initPanelRender 通用函数toggleSidePanel 仅在 locations 页调用 invalidateSize
### 连接修复 (2026-03-19)
51. **PROTO_ADDRESS_REPLY_EN 未导入** - 0xA5 报警地址回复时 NameError补充 import
52. **心跳自动恢复 online** - 心跳处理时自动将设备 status 设为 online + 重新注册 connections
### 全面切换高德 API (2026-03-23)
53. **高德逆地理编码** - 接入 restapi.amap.com/v3/geocode/regeo服务端 WGS84→GCJ02 坐标转换
54. **高德智能硬件定位** - 接入 apilocate.amap.com/position (基站+WiFi定位企业认证通过前返回 10012)
55. **高德数字签名** - 实现 `_amap_sign()` 函数 (参数排序 + AMAP_SECRET + MD5)
56. **服务端坐标转换** - geocoding.py 内置 `wgs84_to_gcj02()` Python 实现
57. **高德地图瓦片** - 前端替换为高德瓦片 (GCJ-02, 标准Mercator),前端 WGS84→GCJ02 坐标转换
58. **移除第三方地理编码** - 清理天地图/百度/Google/Unwired/Mylnikov统一使用高德
### API 安全加固 & 批量管理 (2026-03-20)
59. **API Key 认证** - `dependencies.py` 实现 X-API-Key 头认证,`secrets.compare_digest` 防时序攻击
60. **CORS + 限流** - `SlowAPIMiddleware` 全局限流 (60/min),写操作独立限速 (30/min)
61. **限流器真实 IP** - `extensions.py``X-Forwarded-For` / `CF-Connecting-IP` 提取真实客户端 IP
62. **全局异常处理** - 拦截未处理异常返回 500不泄露堆栈放行 HTTPException/ValidationError
63. **Schema 校验加强** - IMEI pattern、经纬度范围、Literal 枚举、command max_length、BeaconConfig MAC/UUID pattern
64. **Health 端点增强** - `/health` 检测数据库连通性 + TCP 连接设备数
65. **批量设备创建** - `POST /api/devices/batch` (最多500台)WHERE IN 单次查询去重,输入列表内 IMEI 去重
66. **批量设备更新** - `PUT /api/devices/batch`,单次查询 + 批量更新 + 单次 flush
67. **批量设备删除** - `POST /api/devices/batch-delete`,通过 body 传递避免 URL 长度限制
68. **批量指令发送** - `POST /api/commands/batch` (最多100台)`model_validator` 互斥校验 device_ids/imeis
69. **Heartbeats 路由** - 新增 `GET /api/heartbeats` 心跳记录查询 + 按 ID 获取
70. **按 ID 查询端点** - locations/{id}, attendance/{id}, bluetooth/{id} 放在路由末尾避免冲突
71. **Beacons double-commit 修复** - 移除 router 层多余的 flush/refresh依赖 service 层
### 全面改进 (2026-03-22)
#### Protocol 修复
72. **parser.py 0x22 CellID** - 从2字节改为3字节解析
73. **parser.py 0xA5 LBS告警** - 移除错误的 datetime+lbs_length 前缀
74. **parser.py 0xA9 WiFi告警** - 完全重写为 datetime+MCC/MNC+cell_type+cell_count+基站+TA+WiFi+alarm_code
75. **parser.py voltage** - 0xA3/0xA5/0xA9 voltage 从2字节改为1字节
76. **parser.py 0xA4** - 新增多围栏告警解析器 (含 fence_id)
77. **parser.py 0xB2** - 完整 iBeacon 解析 (RSSI/MAC/UUID/Major/Minor/Battery)
78. **parser.py 0xB3** - beacon_count + 每信标30字节解析
79. **parser.py 0xB0/0xB1** - 补充 gps_positioned/terminal_info/voltage/gsm_signal 等中间字段
80. **parser.py 0x81** - 新增指令回复解析器
81. **builder.py 0x1F** - 时间同步添加2字节 language 参数
82. **builder.py 地址回复** - 重写 CN/EN 地址回复完整格式
83. **builder.py 0x80/0x82** - cmd_len 正确包含 language(2)
84. **constants.py** - 从 PROTOCOLS_REQUIRING_RESPONSE 移除 0x28
#### 批量 API + 索引
85. **batch-latest** - `POST /api/locations/batch-latest` 批量获取多设备最新位置
86. **all-latest-locations** - `GET /api/devices/all-latest-locations` 所有在线设备位置
87. **数据库索引** - 新增5个索引 (alarm_type, acknowledged, beacon_mac, location_type, attendance_type)
#### 多 API Key + 权限控制
88. **ApiKey 模型** - SHA-256 hash, permissions(read/write/admin), is_active
89. **多 Key 认证** - master key (env) + DB keys, last_used_at 追踪
90. **权限控制** - require_write/require_admin 依赖,写端点需 write 权限
91. **Key 管理 API** - `/api/keys` CRUD (admin only),创建时返回明文一次
92. **DeviceUpdate** - 移除 status 字段 (设备状态由系统管理)
#### WebSocket 实时推送
93. **WebSocketManager** - 连接管理器topic 订阅最大100连接
94. **WS 端点** - `/ws?api_key=xxx&topics=location,alarm` WebSocket 认证
95. **TCP 广播集成** - 7个广播点 (location, alarm, device_status x2, attendance, bluetooth x2)
#### 数据清理 + 补充
96. **自动数据清理** - 后台定时任务DATA_RETENTION_DAYS=90, DATA_CLEANUP_INTERVAL_HOURS=24
97. **手动清理 API** - `POST /api/system/cleanup` (admin only)
98. **alarm_source 过滤** - alarms 列表新增 alarm_source 参数
99. **beacon_mac 过滤** - bluetooth 列表新增 beacon_mac 参数
100. **command_type 过滤** - commands 列表新增 command_type 参数
101. **sort_order 参数** - alarms/bluetooth 列表支持 asc/desc 排序
#### 协议层统一
102. **PacketBuilder 统一** - tcp_server.py 内嵌 PacketBuilder 改为委托 protocol/builder.py
### 审计修复 (2026-03-23)
103. **require_admin 导入** - main.py 缺少 require_admin 导入,设置 API_KEY 后 cleanup 端点崩溃
104. **0x28/0x2C CellID 3字节** - tcp_server.py 2G 基站 CellID 从2字节修正为3字节 (0x28 LBS, 0x2C WiFi, 邻区解析)
105. **parser.py CellID 3字节** - parse_lbs_station 默认 cell_id_size=2→3新增3字节分支; _parse_lbs_address_req CellID 2→3字节
106. **alarm_source 字段长度** - models.py String(10)→String(20), schemas.py max_length=10→20 ("single_fence"=12字符)
107. **AlarmRecord wifi_data/fence_data** - 0xA9 WiFi报警存储 wifi_data; 0xA4 多围栏报警提取并存储 fence_id
108. **CommandLog.sent_at** - 指令发送成功后设置 sent_at 时间戳 (原来始终为 NULL)
109. **geocoding IMEI 参数化** - 移除硬编码 IMEI新增 GEOCODING_DEFAULT_IMEI 配置项geocode_location 接受 imei 参数TCP 层传递设备 IMEI
110. **parser.py 循环长度检查** - _parse_lbs_multi 和 _parse_wifi 站点循环检查从 pos+5 改为 pos+6 (LAC=2+CellID=3+RSSI=1=6)
111. **batch sent_at** - batch_send_command 批量指令路径也设置 cmd_log.sent_at (与单条路径一致)
112. **GPS 经度符号反转** - course_status bit 11 含义为 0=东经/1=西经 (非 1=东经)`is_east` 改为 `is_west`,修复成都定位到北美洲的问题 (tcp_server.py + parser.py)
### 高德 IoT v5 API 升级 + 围栏功能 (2026-03-27)
#### 前向地理编码升级
113. **IoT v5 API** - 从旧版 `apilocate.amap.com/position` (GET) 升级为 `restapi.amap.com/v5/position/IoT` (POST),企业订阅已开通
114. **v5 WiFi 格式** - mmac (最强信号AP) + macs (其余AP)MAC 冒号分隔小写accesstype=2 (WiFi)
115. **v5 回退机制** - v5 失败自动回退旧版 `apilocate.amap.com`,使用 AMAP_HARDWARE_KEY + AMAP_HARDWARE_SECRET 独立签名
116. **签名密钥分离** - 新增 AMAP_HARDWARE_SECRET 配置,旧版 API 不再错误使用 AMAP_SECRET 签名
117. **WiFi 缓存优化** - WiFi 和基站分别缓存WiFi 缓存 key 使用排序后的 MAC 地址元组
#### 电子围栏模块
118. **FenceConfig 模型** - 新增 fence_configs 表 (circle/polygon/rectangle + 显示属性 + WGS-84 坐标)
119. **围栏 API** - fences.py router + fence_service.py service完整 CRUD + all-active 端点
120. **POI 搜索代理** - 新增 `/api/geocode/search``/api/geocode/reverse`AMAP_KEY 不暴露前端
#### 前端改进
121. **围栏管理页面** - 高德 MouseTool 绘制,多边形默认,左键添加顶点/右键完成,绘制后保留展示
122. **围栏 POI 搜索** - 弹窗内搜索框,搜索结果列表点击定位,服务端代理高德 API
123. **围栏地图展示** - 选中围栏后在地图上渲染 circle/polygon overlay切换可见性
124. **位置追踪列表修复** - 修复位置记录列表不渲染数据的问题
125. **数据日志页面** - 新增统一数据查询页面,支持多类型记录查询
### 设备-围栏绑定与自动考勤 (2026-03-27)
126. **DeviceFenceBinding 模型** - 多对多绑定表 (device_id + fence_id 唯一约束, CASCADE 外键)
127. **DeviceFenceState 模型** - 状态机表 (is_inside, last_transition_at, last_check_at, 最后坐标)
128. **fence_checker.py** - 围栏自动考勤引擎: haversine 距离, ray-casting 多边形判定, 容差机制 (GPS=0m, WiFi=100m, LBS=200m), 防抖 (60s)
129. **绑定 CRUD** - fence_service.py 新增 get_fence_devices/get_device_fences/bind_devices_to_fence/unbind_devices_from_fence
130. **绑定 API** - fences.py 新增 GET/POST/DELETE /api/fences/{id}/devices 端点
131. **TCP 围栏集成** - _store_location 存储位置后自动检测围栏进出, 自动创建 AttendanceRecord (protocol_number=0, source=fence_auto)
132. **fence_attendance WebSocket** - 新增 topic, 围栏进出事件实时广播
133. **绑定设备 UI** - 围栏表格/面板添加绑定按钮, 弹窗管理已绑定设备 + 添加新绑定
### LBS 定位精度修复 (2026-03-30)
134. **v5 API network 参数修正** - `LTE` 不是有效 network 值 (返回 INVALID_PARAMS 20000)4G 改用 `WCDMA`;映射: lbs_4g/wifi_4g → WCDMA, lbs/wifi → GSM
135. **bts cage 字段条件化** - v5 API 需要 6 字段 bts (`...,cage`),旧版 API 只能 5 字段 (含 cage 返回空位置);新增 `_build_bts(include_cage)` 参数
136. **geocode_location 传递 location_type** - 新增 `location_type` 参数TCP 三处调用 (位置/报警/考勤) 均传入,自动区分 2G(GSM)/4G(WCDMA)
137. **缓存 key 含邻区 hash** - 基站缓存 key 从 `(mcc, mnc, lac, cell_id)` 扩展为 `(mcc, mnc, lac, cell_id, nb_hash)`,修复有/无邻区基站返回同一缓存结果的精度问题
### 批量删除 + 考勤来源 (2026-03-30)
138. **批量删除位置记录** - 新增 `POST /api/locations/batch-delete` (最多500条),前端位置表格增加勾选框和"删除选中"按钮
139. **删除无坐标记录** - 新增 `POST /api/locations/delete-no-coords`,删除 lat/lon 为 NULL 的记录,支持 device_id/时间范围过滤,前端增加"清除无坐标"按钮
140. **attendance_source 字段** - AttendanceRecord 新增 `attendance_source` 字段 (device/bluetooth/fence)TCP 层根据协议号设置,围栏引擎设置 "fence"
141. **lbs_data schema 类型修复** - `lbs_data` 类型从 `list[dict]` 改为 `list[dict] | dict | None`,修复围栏打卡写入 dict 导致 pydantic ValidationError
142. **最新位置按钮发命令** - "最新位置"按钮改为发送 WHERE# 指令获取实时位置,每 2s 轮询最多 15s显示倒计时
143. **围栏管理 Tab 布局** - 围栏页面从单表格改为 Tab 切换 ("围栏列表" + "设备绑定"),设备绑定 Tab 含围栏选择器、快速绑定/解绑、已绑定设备状态表
### 前端 UI 优化 (2026-03-30)
144. **围栏管理顶部 Tab** - Tab 从地图下方表格内移至页面顶部,"围栏管理" Tab 显示地图+围栏列表,"设备绑定" Tab 隐藏地图全屏展示绑定矩阵
145. **低精度定位过滤** - 位置追踪页面新增"低精度"切换按钮,隐藏 LBS/WiFi 点 (地图标记+轨迹折线+表格行联动过滤)
146. **移除精度圈** - 删除轨迹和最新位置视图中 LBS(1000m) 和 WiFi(80m) 的精度半径圆圈,仅通过标记颜色区分定位类型
### 设备管理 UI 增强 (2026-03-31)
147. **设备列表定位模式列** - 设备管理表格新增"定位模式"列,通过 batch-latest API 异步获取每设备最新 location_type颜色编码 (GPS=蓝/WiFi=青/LBS=橙/蓝牙=紫)
148. **设备列表点击排序** - 表头可点击排序 (IMEI/名称/类型/状态/电量/信号/登录/心跳),默认按名称升序,支持中文自然排序 (numeric: true),▲/▼ 蓝色箭头指示
149. **设备列表快捷操作列** - 每行末尾新增 5 个带文字的快捷按钮: 定位(WHERE#)、GPS(GPSON#)、定时(MODE,1#)、状态(STATUS#)、重启(RESET#,红色+确认弹窗),离线设备按钮自动禁用
150. **IMEI 列蓝色可点击** - IMEI 列文字改为蓝色 (#60a5fa),视觉提示可点击进入设备详情
151. **广播指令按钮** - 工具栏刷新按钮右侧新增: "全部定位"、"全部开GPS"、"全部定时"、"全部智能"、"自定义广播" 按钮,发送给所有在线设备
152. **自定义广播弹窗** - 弹窗含指令输入框 + 常用指令快捷选择 (每个按钮带中文说明和 title 悬浮提示)
### 广播指令 API (2026-03-31)
153. **POST /api/commands/broadcast** - 广播指令给所有在线设备,自动查找 status=online 的设备,逐一通过 TCP 发送,返回 sent/failed 统计,跳过 TCP 未连接的设备
### 低精度过滤修正 (2026-03-31)
154. **低精度仅隐藏 LBS** - `_isLowPrecision()` 从匹配 `lbs|wifi` 改为仅匹配 `lbs`WiFi 定位 (30~100m精度) 不再被隐藏
155. **低精度过滤后端联动** - 隐藏低精度时,后端请求加 `exclude_type=lbs` 参数,分页 total/页数准确反映过滤后数量
156. **exclude_type API 参数** - `GET /api/locations` 新增 `exclude_type` 查询参数,后端使用 `notin_` (非 LIKE) 实现索引友好的类型排除
157. **轨迹折线联动** - 隐藏 LBS 点时,折线跳过 LBS 点重新连接剩余 GPS/WiFi 点
### 性能优化与并发修复 (2026-03-31)
158. **SQLite WAL 模式** - `database.py` 启动时设置 `PRAGMA journal_mode=WAL; synchronous=NORMAL; cache_size=-64000; busy_timeout=5000`,读写并发不再互斥
159. **aiohttp Session 复用** - `geocoding.py` 新增 `_get_http_session()` 懒初始化共享 Session消除每次地理编码的 TCP/TLS 握手开销app shutdown 时 `close_http_session()`
160. **connections 字典加锁** - `tcp_server.py` TCPManager 新增 `_conn_lock = asyncio.Lock()`login/cleanup/send_command/send_message 操作均在锁内执行,消除重连竞态条件
161. **device_id 缓存** - ConnectionInfo 新增 `device_id` 槽位,`_get_device_id()` 首次查询后缓存到连接信息,后续数据包不再查询 DB
162. **recv_buffer 改 bytearray** - TCP 接收缓冲区从 `bytes` 改为 `bytearray`,拼接操作从 O(N²) 降为摊销 O(1)
163. **WebSocket 广播并发化** - `websocket_manager.py` broadcast 从串行循环改为 `asyncio.gather()` 并发发送 + 3秒超时慢客户端不再阻塞其他订阅者
164. **补充数据库索引** - 新增 `ix_alarm_source`, `ix_att_source`, `ix_att_device_type_time`, `ix_device_status`, `ix_fence_active` 共 5 个索引
165. **exclude_type 查询优化** - 从 `NOT LIKE 'lbs%'` 改为 `notin_(['lbs','lbs_4g'])`,可利用 `ix_loc_type` 索引
166. **地理编码日志降级** - Amap v5/legacy 请求/响应日志从 INFO 降为 DEBUG减少高频同步日志对事件循环的阻塞
### 性能优化第二批 (2026-03-31)
167. **围栏 N+1 查询批量化** - `fence_checker.py` DeviceFenceState 从 per-fence 循环查询改为一次 `WHERE IN` 批量加载 `states_map`N+1→1 查询
168. **考勤去重提前查询** - `_has_attendance_today` 从循环内调用改为循环前预查一次,多围栏场景避免重复查询
169. **地理编码并行化** - `_store_location` 中 reverse_geocode 改为 `asyncio.ensure_future` 与 DB 操作并行,减少串行等待
170. **围栏检查复用会话** - 围栏检查从独立开 `async_session` 改为与位置存储共用同一个 session/transaction减半 DB 连接开销
171. **API Key 认证缓存** - `dependencies.py` 新增 `_AUTH_CACHE` 内存缓存 (TTL=60s)DB 查询+flush 仅在 cache miss 时执行
172. **TCP 空闲超时** - `config.py` 新增 `TCP_IDLE_TIMEOUT=600` (默认10分钟)`reader.read` 包裹 `asyncio.wait_for`,超时自动断开清理连接
173. **admin.html 内存缓存** - 启动时一次性读入内存,`/admin` 请求不再每次读磁盘 (1.2ms vs ~5ms)
174. **SQLite 外键约束** - `database.py` 启动时 `PRAGMA foreign_keys=ON`,确保围栏删除时级联清理绑定/状态数据
### 前端 IMEI 显示 + 位置追踪增强 (2026-03-31)
175. **全局 device_id→IMEI 映射** - `_devIdToImei` 全局映射 + `_imei()` 辅助函数,设备列表加载时自动构建
176. **表格设备列改显示 IMEI** - 告警管理、考勤记录、蓝牙记录、指令管理、位置追踪、数据日志、仪表盘告警摘要全部从 device_id 改为 IMEI 显示
177. **地图标记悬浮弹窗** - 轨迹点/最新位置/总览标记改为 mouseover 自动弹出、mouseout 自动关闭,保留 click 打开
178. **位置追踪 Tab 切换** - 位置追踪页面顶部新增 Tab: "轨迹追踪" (原有功能) + "全部设备总览" (新功能)
179. **全部设备总览** - 左侧设备选择面板 (勾选/取消/全选,显示名称+IMEI+在线状态) + 右侧地图展示所有选中设备最新位置
180. **总览数据源** - 通过 `POST /api/locations/batch-latest` 获取所有设备最新位置 (含离线设备历史位置),非仅在线设备
181. **总览获取实时位置** - "获取实时位置" 按钮向所有在线设备广播 WHERE# 指令,回传后点"刷新位置"更新地图
182. **总览设备选择联动** - 勾选/取消设备立即刷新地图,全选/取消全选生效,取消勾选的设备从地图移除
183. **总览标记样式** - 标签显示 `名称 (IMEI后4位)`,边框颜色区分在线(绿)/离线(灰),填充色按定位类型(GPS蓝/WiFi青/LBS橙),弹窗显示名称+IMEI+在线状态+坐标+地址
### 广播指令改为全设备 (2026-03-31)
184. **broadcast API 改为全设备** - `POST /api/commands/broadcast` 从只查 online 改为查所有设备,逐一尝试 TCP 发送,未连接的跳过返回 "TCP not connected"
185. **前端提示更新** - 确认弹窗改为"向所有设备发送",结果提示"X 台已发送, Y 台未连接"
### 设备总览全面增强 (2026-03-31)
186. **设备面板交互分离** - 点击勾选框仅切换地图标记显示/隐藏,点击名称/IMEI 区域定位到设备并弹窗
187. **弹窗常驻模式** - 鼠标悬浮预览弹窗(移开消失),点击弹窗常驻(不消失),再次点击或关闭恢复悬浮模式;轨迹点/最新位置/总览标记统一行为
188. **设备搜索** - 总览左侧面板新增搜索框实时过滤设备列表匹配名称或IMEI
189. **标记点缓存** - 首次加载后标记缓存在 `_ovMarkerMap`,勾选变更只做 setMap 显示/隐藏,不重新请求 API 和重建标记
190. **设备专属颜色** - 每台设备分配固定颜色14色循环标记点和轨迹线用同一色板不同设备一眼区分
191. **多设备轨迹叠加** - "显示轨迹"加载所有勾选设备的当天轨迹,每台设备独立颜色折线+起终点标记
192. **轨迹线交互** - 悬浮加粗预览点击高亮选中加粗6px+不透明+zIndex置顶200其他设备轨迹点隐藏再次点击取消高亮
193. **三入口联动高亮** - 点击左侧设备名称/点击地图设备标记点/点击轨迹线均触发对应轨迹高亮,通过 `_ovDidToPL` 映射联动
194. **总览低精度过滤** - 独立的低精度按钮切换时自动重新加载轨迹跳过LBS点重连折线
195. **离线设备支持** - batch-latest 查数据库历史位置(不限在线),取消勾选的设备点击名称仍可定位+查轨迹
## 待完成功能
### 第一优先级 — API 增强 (用于系统集成)
#### 第二批: 数据导出 API
5. **设备数据导出** - `GET /api/devices/export?format=csv` — 导出设备列表 CSV/Excel支持状态/类型筛选
6. **位置数据导出** - `GET /api/locations/export?format=csv` — 导出位置记录 CSV支持设备/时间/类型筛选
7. **告警数据导出** - `GET /api/alarms/export?format=csv` — 导出告警记录 CSV支持类型/状态/时间筛选
8. **考勤数据导出** - `GET /api/attendance/export?format=csv` — 导出考勤记录 CSV支持设备/来源/时间筛选
9. **蓝牙数据导出** - `GET /api/bluetooth/export?format=csv` — 导出蓝牙记录 CSV
10. **考勤报表导出** - `GET /api/attendance/report/export?format=csv` — 导出考勤日报表 (每设备每天汇总)
#### 第三批: 批量操作 + 筛选增强
11. **告警批量删除增强** - 支持按时间范围/类型/设备批量删除 (不限于选中 ID)
12. **位置记录清理增强** - `POST /api/locations/cleanup` — 删除N天前旧记录支持按定位类型筛选
13. **设备分组管理** - 新增 DeviceGroup 模型,支持设备分组、按组查询、按组批量操作
14. **位置数据聚合** - `GET /api/locations/heatmap?device_id=&start_time=&end_time=` — 返回热力图数据 (经纬度+权重)
15. **告警规则配置** - 新增告警规则 API支持自定义告警阈值 (低电量阈值、围栏停留时间等)
#### 第四批: 系统管理增强
16. **操作审计日志** - 新增 AuditLog 模型,记录所有 POST/PUT/DELETE 操作 (操作人/IP/端点/参数/时间)
17. **系统配置 API** - `GET/PUT /api/system/config` — 运行时配置查看和修改 (数据保留天数、清理间隔等)
18. **数据库备份 API** - `POST /api/system/backup` — 触发 SQLite 数据库备份,返回下载链接
19. **设备固件管理** - 新增固件版本记录 API支持批量查询设备固件版本
### 第二优先级 — 前端与体验
20. **前端 WebSocket 集成** - admin.html Dashboard 改用 WebSocket 替代 30s 轮询,报警页实时通知弹窗
21. **考勤报表页面** - 前端新增考勤报表 Tab调用 `GET /api/attendance/report` 展示每设备每天签到汇总
22. **心跳统计页面** - 数据日志页面心跳 Tab 增加统计面板,调用 `GET /api/heartbeats/stats` 展示设备活跃/异常/电量分布
23. **位置统计页面** - 位置追踪页面增加统计面板,调用 `GET /api/locations/stats` 展示定位类型分布/小时趋势
24. **导出按钮集成** - 各数据页面工具栏添加"导出 CSV"按钮 (需先完成第二批导出 API)
### 第三优先级 — 性能与架构
25. **性能优化第三批** - 迁移 PostgreSQL、多worker部署 (几千台设备时)
26. **API 版本化** - 添加 `/api/v1/` 前缀,为将来 v2 预留兼容空间
### 可选优化 (不影响功能)
27. **心跳扩展模块解析** - 计步器、外部电压等模块未解析 (已存原始 hex按需解析)
28. **协议层深度统一** - tcp_server.py 辅助方法 (_parse_gps, _parse_datetime 等) 逐步迁移到 protocol/parser.py (代码重构)
### 已完成的 API 增强 (第一批 — 统计/聚合2026-03-31)
-`GET /api/system/overview` — 系统总览 (设备在线率/今日统计/表记录数/DB大小)
-`GET /api/devices/stats` 增强 — 新增 by_type, battery_distribution, signal_distribution, online_rate
-`GET /api/locations/stats` — 位置统计 (总数/有坐标率/类型分布/小时分布)
-`GET /api/locations/track-summary/{id}` — 轨迹摘要 (距离/时长/速度/类型分布)
-`GET /api/alarms/stats` 增强 — 新增 today, by_source, daily_trend, top_devices
-`POST /api/alarms/batch-acknowledge` — 批量确认告警
-`GET /api/attendance/stats` 增强 — 新增 today, by_source, daily_trend, by_device
-`GET /api/attendance/report` — 考勤日报表 (每设备每天打卡次数/首末打卡/来源)
-`GET /api/bluetooth/stats` — 蓝牙统计 (类型分布/TOP信标/RSSI分布)
-`GET /api/heartbeats/stats` — 心跳统计 (活跃设备/电量/信号/间隔分析)
-`GET /api/fences/stats` — 围栏统计 (活跃数/绑定数/设备进出状态/今日事件)
-`GET /api/fences/{id}/events` — 围栏进出事件历史
-`GET /api/commands/stats` — 指令统计 (状态分布/类型分布/成功率/趋势)
- ✅ 前端 admin.html 同步: 仪表盘今日统计+在线率、告警批量确认、轨迹摘要、蓝牙/围栏/指令统计面板
## 调试技巧
```bash
# 查看实时日志
tail -f /home/gpsystem/server.log | grep -aE "TCP|login|heartbeat|error|geocod|Amap" --line-buffered
# 检查数据库
python3 -c "
import sqlite3
conn = sqlite3.connect('/home/gpsystem/badge_admin.db')
cur = conn.cursor()
cur.execute('SELECT id, imei, device_type, status, battery_level FROM devices')
for row in cur.fetchall(): print(row)
cur.execute('SELECT id, location_type, latitude, longitude, address, recorded_at FROM location_records ORDER BY id DESC LIMIT 5')
for row in cur.fetchall(): print(row)
"
# 测试 API
curl -s http://localhost:8088/api/devices | python3 -m json.tool
curl -s http://localhost:8088/api/locations/latest/1 | python3 -m json.tool
curl -s http://localhost:8088/health
# 发送指令
curl -s -X POST http://localhost:8088/api/commands/send \
-H "Content-Type: application/json" \
-d '{"device_id":1,"command_type":"online_cmd","command_content":"GPSON#"}' | python3 -m json.tool
# 开启蓝牙扫描
curl -s -X POST http://localhost:8088/api/commands/send \
-H "Content-Type: application/json" \
-d '{"device_id":1,"command_type":"online_cmd","command_content":"BTON#"}' | python3 -m json.tool
# 管理围栏
curl -s http://localhost:8088/api/fences | python3 -m json.tool
curl -s http://localhost:8088/api/fences/all-active | python3 -m json.tool
curl -s -X POST http://localhost:8088/api/fences \
-H "Content-Type: application/json" \
-d '{"name":"办公区","fence_type":"circle","center_lat":30.605,"center_lng":103.936,"radius":500}' | python3 -m json.tool
# POI 搜索 (服务端代理)
curl -s "http://localhost:8088/api/geocode/search?keyword=天府广场" | python3 -m json.tool
# 管理信标
curl -s http://localhost:8088/api/beacons | python3 -m json.tool
curl -s -X POST http://localhost:8088/api/beacons \
-H "Content-Type: application/json" \
-d '{"beacon_mac":"AA:BB:CC:DD:EE:FF","name":"前台","floor":"1F","latitude":30.27,"longitude":120.15}' | python3 -m json.tool
# 检查 FRP 连接
ps aux | grep frpc
# FRP Dashboard: http://152.69.205.186:7500 (admin/PassWord0325)
# 检查端口
python3 -c "
with open('/proc/net/tcp') as f:
for line in f:
parts = line.split()
if len(parts)>=2 and ':' in parts[1]:
port = int(parts[1].split(':')[1], 16)
if port in (8088, 5000): print(f'Port {port}: listening')
"
# 测试地理编码
python3 -c "
import asyncio
from app.geocoding import geocode_location, reverse_geocode
async def test():
lat, lon = await geocode_location(mcc=460, mnc=0, lac=32775, cell_id=205098688)
print(f'lat={lat}, lon={lon}')
if lat: print(await reverse_geocode(lat, lon))
asyncio.run(test())
"
```