🛰️ GPS 追踪器
基于 BLE 的 GPS 位置追踪设备,支持实时位置上报和历史轨迹回放。
📐 系统架构
flowchart TB
subgraph GPS["GPS 追踪器"]
GNSS[GNSS 模块] --> MCU[BLE MCU]
MCU --> FLASH[Flash 存储]
MCU --> ANT[天线]
end
subgraph Phone["手机 App"]
ANT2[天线] --> MCU2[手机 BLE]
MCU2 --> MAP[地图显示]
MCU2 --> DB[(本地数据库)]
end
ANT -.->|BLE 连接 | ANT2
style GPS fill:#e3f2fd,stroke:#007bff
style Phone fill:#d4edda,stroke:#28a745
📋 自定义 GATT 服务设计
flowchart TB
subgraph Service["位置服务 (自定义 UUID)"]
LOC[位置特征值
Notify/Indicate] CFG[配置特征值
Write/Read] HIST[历史数据特征值
Read] BATT[电池特征值
Read] end LOC --> CCCD1[CCCD] CFG --> CCCD2[CCCD] style Service fill:#e3f2fd,stroke:#007bff style LOC fill:#fff3cd,stroke:#ffc107
Notify/Indicate] CFG[配置特征值
Write/Read] HIST[历史数据特征值
Read] BATT[电池特征值
Read] end LOC --> CCCD1[CCCD] CFG --> CCCD2[CCCD] style Service fill:#e3f2fd,stroke:#007bff style LOC fill:#fff3cd,stroke:#ffc107
📊 位置数据格式
// 位置数据特征值数据格式 (20 bytes)
typedef struct {
uint8_t flags; // 标志位
int32_t latitude; // 纬度 (1e-7 度)
int32_t longitude; // 经度 (1e-7 度)
int16_t altitude; // 海拔 (米)
uint8_t speed; // 速度 (km/h * 10)
uint8_t satellites; // 卫星数量
uint32_t timestamp; // UTC 时间戳
} gps_location_t;
// 标志位定义
#define GPS_FLAG_3D_FIX 0x01 // 3D 定位
#define GPS_FLAG_MOTION 0x02 // 移动中
#define GPS_FLAG_CHARGING 0x04 // 充电中
#define GPS_FLAG_ALARM 0x08 // 报警状态
// 示例:北京位置,海拔 50m,速度 36km/h,8 颗卫星
uint8_t gps_data[] = {
0x03, // Flags: 3D fix + motion
0x00, 0xD9, 0x04, 0x19, // Latitude: 39.9042° N (0x1904D900 * 1e-7)
0x00, 0x7D, 0x39, 0x07, // Longitude: 116.4074° E
0x32, 0x00, // Altitude: 50m
0x5A, // Speed: 36 km/h (90 = 9.0 m/s)
0x08, // Satellites: 8
0x00, 0x00, 0x00, 0x00 // Timestamp: UTC
};
🔧 GPS 数据包构建详解
从 GPS 原始数据到蓝牙空中传输,每一层如何添加头部,组合成完整的数据包。
数据包组合流程
flowchart TB
subgraph Step1["步骤 1: GPS 原始数据"]
A1["经纬度 + 海拔
速度 + 卫星数"] end subgraph Step2["步骤 2: GATT 层"] A2["特征值句柄 + 位置数据"] end subgraph Step3["步骤 3: ATT 层"] A3["Opcode + Handle + Data"] end subgraph Step4["步骤 4: L2CAP 层"] A4["L2CAP Header
Length + CID"] end subgraph Step5["步骤 5: HCI 层"] A5["HCI ACL Header
Handle + Length"] end subgraph Step6["步骤 6: LL 层"] A6["LL Header + MIC + CRC"] end subgraph Step7["步骤 7: PHY 层"] A7["前导码 + 访问地址
+ Payload + CRC"] end A1 --> A2 A2 --> A3 A3 --> A4 A4 --> A5 A5 --> A6 A6 --> A7 style Step1 fill:#d4edda,stroke:#28a745 style Step2 fill:#e3f2fd,stroke:#007bff style Step3 fill:#e3f2fd,stroke:#007bff style Step4 fill:#e3f2fd,stroke:#007bff style Step5 fill:#fff3cd,stroke:#ffc107 style Step6 fill:#d4edda,stroke:#28a745 style Step7 fill:#d4edda,stroke:#28a745
速度 + 卫星数"] end subgraph Step2["步骤 2: GATT 层"] A2["特征值句柄 + 位置数据"] end subgraph Step3["步骤 3: ATT 层"] A3["Opcode + Handle + Data"] end subgraph Step4["步骤 4: L2CAP 层"] A4["L2CAP Header
Length + CID"] end subgraph Step5["步骤 5: HCI 层"] A5["HCI ACL Header
Handle + Length"] end subgraph Step6["步骤 6: LL 层"] A6["LL Header + MIC + CRC"] end subgraph Step7["步骤 7: PHY 层"] A7["前导码 + 访问地址
+ Payload + CRC"] end A1 --> A2 A2 --> A3 A3 --> A4 A4 --> A5 A5 --> A6 A6 --> A7 style Step1 fill:#d4edda,stroke:#28a745 style Step2 fill:#e3f2fd,stroke:#007bff style Step3 fill:#e3f2fd,stroke:#007bff style Step4 fill:#e3f2fd,stroke:#007bff style Step5 fill:#fff3cd,stroke:#ffc107 style Step6 fill:#d4edda,stroke:#28a745 style Step7 fill:#d4edda,stroke:#28a745
逐步构建过程
// ========== 步骤 1: GPS 原始数据 ==========
// 从 GNSS 模块解析到的位置信息
int32_t latitude = 39904200; // 39.9042° N (乘以 1e7)
int32_t longitude = 116407400; // 116.4074° E
int16_t altitude = 50; // 海拔 50 米
uint8_t speed = 90; // 9.0 m/s (乘以 10)
uint8_t satellites = 8; // 8 颗卫星
uint32_t timestamp = 1234567890;// UTC 时间戳
// ========== 步骤 2: GATT 层封装 ==========
// 构建位置数据结构 (20 bytes)
uint8_t gps_payload[20] = {
0x03, // [0] Flags: 3D fix + motion
// 纬度 (小端序)
0x00, 0xD9, 0x04, 0x19, // [1-4] 39.9042° N
// 经度 (小端序)
0x00, 0x7D, 0x39, 0x07, // [5-8] 116.4074° E
// 海拔 (小端序)
0x32, 0x00, // [9-10] 50m
// 速度
0x5A, // [11] 9.0 m/s
// 卫星数
0x08, // [12] 8 颗
// 时间戳 (小端序)
0xD2, 0x02, 0x96, 0x49 // [13-16] 1234567890
};
// GATT 数据总长度:17 bytes (实际使用)
// 特征值句柄
uint16_t location_handle = 0x0023; // 位置特征值句柄
// ========== 步骤 3: ATT 层封装 ==========
// 添加 ATT Header (Opcode + Handle)
uint8_t att_packet[] = {
0x1B, // [0] Opcode: Handle Value Notification
0x23, 0x00, // [1-2] Handle: 0x0023 (小端序)
// GATT 数据 (从步骤 2)
0x03, 0x00, 0xD9, 0x04, 0x19, 0x00, 0x7D, 0x39,
0x07, 0x32, 0x00, 0x5A, 0x08, 0xD2, 0x02, 0x96, 0x49
};
// ATT 层总长度:20 bytes
// ========== 步骤 4: L2CAP 层封装 ==========
// 添加 L2CAP Header (Length + CID)
uint8_t l2cap_packet[] = {
0x14, 0x00, // [0-1] Length: 20 (ATT 包长度)
0x04, 0x00, // [2-3] CID: 0x0004 (ATT 信道)
// ATT Packet (从步骤 3)
0x1B, 0x23, 0x00, 0x03, 0x00, 0xD9, 0x04, 0x19,
0x00, 0x7D, 0x39, 0x07, 0x32, 0x00, 0x5A, 0x08,
0xD2, 0x02, 0x96, 0x49
};
// L2CAP 层总长度:24 bytes
// ========== 步骤 5: HCI ACL 层封装 ==========
// 添加 HCI ACL Header (Handle + Data Length)
uint8_t hci_packet[] = {
0x01, 0x00, // [0-1] Handle: 0x0001 (连接句柄)
0x18, 0x00, // [2-3] Data Length: 24 (L2CAP 包长度)
// L2CAP Packet (从步骤 4)
0x14, 0x00, 0x04, 0x00, 0x1B, 0x23, 0x00, 0x03,
0x00, 0xD9, 0x04, 0x19, 0x00, 0x7D, 0x39, 0x07,
0x32, 0x00, 0x5A, 0x08, 0xD2, 0x02, 0x96, 0x49
};
// HCI 层总长度:28 bytes
// ========== 步骤 6: Link Layer 封装 ==========
// LL 层添加:LL Header + MIC(加密) + CRC
// LL Data PDU 结构:
// +----------+------------+---------+-----+
// | LL Header| LL Payload | MIC(4B) | CRC |
// | (2B) | (0-27B) | |(3B) |
// +----------+------------+---------+-----+
// LL Header (2 bytes)
uint8_t ll_header[] = {
0x05, // [0] LLID=01 (数据), NESN=0, SN=0, MD=0
0x18 // [1] Length: 24 bytes (HCI 数据长度)
};
// LL Payload = HCI Packet (28 bytes)
// 注意:28 > 27-2-4-3 = 18,需要分段发送!
// 分段 1 (LL PDU #1)
uint8_t ll_pdu1[] = {
0x05, 0x1B, // LL Header
// HCI 数据前 23 bytes
0x01, 0x00, 0x18, 0x00, 0x14, 0x00, 0x04, 0x00,
0x1B, 0x23, 0x00, 0x03, 0x00, 0xD9, 0x04, 0x19,
0x00, 0x7D, 0x39, 0x07, 0x32, 0x00, 0x5A,
// MIC (4 bytes, 如果启用加密)
0xA1, 0xB2, 0xC3, 0xD4,
// CRC (3 bytes)
0x12, 0x34, 0x56
};
// 分段 2 (LL PDU #2) - 剩余数据
uint8_t ll_pdu2[] = {
0x01, 0x05, // LL Header (LLID=00 继续)
// HCI 数据剩余 4 bytes
0x08, 0xD2, 0x02, 0x96, 0x49,
// MIC (4 bytes)
0xE5, 0xF6, 0x07, 0x18,
// CRC (3 bytes)
0x78, 0x9A, 0xBC
};
// ========== 步骤 7: PHY 层封装 ==========
// PHY 添加:前导码 + 访问地址 + CRC
// PHY Packet 结构:
// +--------+----------+------------+-----+
// | Preamble| Access | Payload | CRC |
// | (1B) | Address | (LL PDU) | |
// | | (4B) | | |
// +--------+----------+------------+-----+
uint8_t preamble = 0x55; // 前导码 (01010101)
uint32_t access_address = 0x8E89BED6; // 访问地址 (BLE 固定)
// Payload = LL PDU (从步骤 6)
// CRC 已在 LL 层计算
完整数据包结构图
flowchart LR
subgraph PHY_Packet["PHY Packet (空中传输)"]
P1["Preamble
1B"] P2["Access Addr
4B"] P3["LL PDU
可变"] P4["CRC
3B"] end subgraph LL_PDU["LL PDU"] L1["LL Header
2B"] L2["LL Payload
0-27B"] L3["MIC
4B"] end subgraph LL_Payload["LL Payload = HCI Packet"] H1["HCI ACL Header
4B"] H2["L2CAP Packet
可变"] end subgraph L2CAP["L2CAP Packet"] C1["L2CAP Header
4B"] C2["ATT PDU
可变"] end subgraph ATT["ATT PDU"] A1["ATT Header
3B"] A2["GATT Data
可变"] end subgraph GATT["GATT Data"] G1["Flags
1B"] G2["Latitude
4B"] G3["Longitude
4B"] G4["Altitude
2B"] G5["Speed
1B"] G6["Satellites
1B"] G7["Timestamp
4B"] end P3 --> LL_PDU L2 --> LL_Payload H2 --> L2CAP C2 --> ATT A2 --> GATT style PHY_Packet fill:#d4edda,stroke:#28a745 style LL_PDU fill:#e3f2fd,stroke:#007bff style LL_Payload fill:#fff3cd,stroke:#ffc107 style L2CAP fill:#e3f2fd,stroke:#007bff style ATT fill:#e3f2fd,stroke:#007bff style GATT fill:#d4edda,stroke:#28a745
1B"] P2["Access Addr
4B"] P3["LL PDU
可变"] P4["CRC
3B"] end subgraph LL_PDU["LL PDU"] L1["LL Header
2B"] L2["LL Payload
0-27B"] L3["MIC
4B"] end subgraph LL_Payload["LL Payload = HCI Packet"] H1["HCI ACL Header
4B"] H2["L2CAP Packet
可变"] end subgraph L2CAP["L2CAP Packet"] C1["L2CAP Header
4B"] C2["ATT PDU
可变"] end subgraph ATT["ATT PDU"] A1["ATT Header
3B"] A2["GATT Data
可变"] end subgraph GATT["GATT Data"] G1["Flags
1B"] G2["Latitude
4B"] G3["Longitude
4B"] G4["Altitude
2B"] G5["Speed
1B"] G6["Satellites
1B"] G7["Timestamp
4B"] end P3 --> LL_PDU L2 --> LL_Payload H2 --> L2CAP C2 --> ATT A2 --> GATT style PHY_Packet fill:#d4edda,stroke:#28a745 style LL_PDU fill:#e3f2fd,stroke:#007bff style LL_Payload fill:#fff3cd,stroke:#ffc107 style L2CAP fill:#e3f2fd,stroke:#007bff style ATT fill:#e3f2fd,stroke:#007bff style GATT fill:#d4edda,stroke:#28a745
数据包大小计算
| 层 | 头部大小 | 数据大小 | 本层总大小 | 累计大小 |
|---|---|---|---|---|
| GATT | 0 B | 17 B (GPS 数据) | 17 B | 17 B |
| ATT | 3 B (Opcode+Handle) | 17 B (GATT) | 20 B | 20 B |
| L2CAP | 4 B (Length+CID) | 20 B (ATT) | 24 B | 24 B |
| HCI ACL | 4 B (Handle+Length) | 24 B (L2CAP) | 28 B | 28 B |
| LL | 2 B (Header) | 28 B (HCI) | 34 B (+MIC 4B +CRC 3B) | 41 B |
| PHY | 5 B (Preamble+Access) | 41 B (LL) | 46 B (+CRC 3B) | 49 B |
注意: 由于 LL Payload 最大 27 bytes,28 bytes 的 HCI 数据需要分段发送(2 个 LL PDU)。
实际字节流 (十六进制)
# 最终在空中传输的完整字节流 (从内到外):
# 应用数据 (GPS 位置 17 bytes):
03 00 D9 04 19 00 7D 39 07 32 00 5A 08 D2 02 96 49
# + ATT Header (3 bytes):
1B 23 00 03 00 D9 04 19 00 7D 39 07 32 00 5A 08 D2 02 96 49
# + L2CAP Header (4 bytes):
14 00 04 00 1B 23 00 03 00 D9 04 19 00 7D 39 07 32 00 5A 08 D2 02 96 49
# + HCI ACL Header (4 bytes):
18 00 14 00 04 00 1B 23 00 03 00 D9 04 19 00 7D 39 07 32 00 5A 08 D2 02 96 49
# + LL Header + MIC + CRC (分段 1):
05 1B 01 00 18 00 14 00 04 00 1B 23 00 03 00 D9 04 19 00 7D 39 07 32 00 5A A1 B2 C3 D4 12 34 56
# + LL Header + MIC + CRC (分段 2):
01 05 08 D2 02 96 49 E5 F6 07 18 78 9A BC
# + PHY Preamble + Access Address:
55 8E 89 BE D6 05 1B ... (分段 1)
55 8E 89 BE D6 01 05 ... (分段 2)
# 最终空中传输:约 49 bytes (分 2 个包)
📈 完整工作流程
sequenceDiagram
participant GPS as GPS 追踪器
participant Phone as 手机 App
Note over GPS,Phone: 1. 广播阶段
GPS->>Phone: ADV_IND (位置服务 UUID)
Note over GPS,Phone: 2. 连接阶段
Phone->>GPS: CONNECT_IND
GPS-->>Phone: 连接完成
Note over GPS,Phone: 3. 服务发现
Phone->>GPS: 发现自定义服务
Phone->>GPS: 发现所有特征值
Note over GPS,Phone: 4. 配置上报
Phone->>GPS: 写入配置 (间隔 1s)
Phone->>GPS: 启用位置通知
Note over GPS,Phone: 5. 实时位置
loop 每秒
GPS->>Phone: 位置通知 (经纬度 + 时间戳)
end
Note over GPS,Phone: 6. 历史轨迹 (可选)
Phone->>GPS: 读取历史数据
GPS-->>Phone: 批量位置数据
Note over GPS,Phone: 7. 进入低功耗
Phone->>GPS: 断开连接
GPS->>GPS: 休眠 (GPS 模块关闭)
💻 固件代码示例
#include "nrf_sdm.h"
#include "nrf_sdh.h"
#include "ble_gatts.h"
// 自定义服务 UUID: 12345678-1234-5678-1234-56789ABCDEF0
// 位置特征值 UUID: 12345678-1234-5678-1234-56789ABCDEF1
static uint16_t m_service_handle;
static ble_gatts_char_handles_t m_location_char;
static uint16_t m_conn_handle = BLE_CONN_HANDLE_INVALID;
// 初始化位置服务
void gps_service_init(void) {
ble_uuid_t service_uuid = {
.uuid = 0x0001,
.type = m_uuid_type
};
// 添加服务
sd_ble_gatts_service_add(BLE_GATTS_SRVC_TYPE_PRIMARY,
&service_uuid,
&m_service_handle);
// 配置位置特征值
ble_gatts_char_md_t char_md = {
.char_props.notify = 1,
.char_props.read = 1,
.char_props.write = 1,
.p_char_user_desc = "GPS Location",
};
ble_uuid_t char_uuid = {
.uuid = 0x0001,
.type = m_uuid_type
};
ble_attr_char_value_t char_val = {
.def_val = NULL,
.max_len = 20,
};
sd_ble_gatts_characteristic_add(m_service_handle,
&char_md,
&char_attr,
&m_location_char);
}
// 发送位置数据
void send_location_data(gps_location_t *loc) {
ble_gatts_hvx_params_t hvx_params = {
.handle = m_location_char.value_handle,
.p_data = (uint8_t *)loc,
.p_len = &(uint16_t){sizeof(gps_location_t)},
.type = BLE_GATT_HVX_NOTIFICATION,
};
sd_ble_gatts_hvx(m_conn_handle, &hvx_params);
}
// GPS 数据更新任务
void gps_update_task(void) {
static gps_location_t location;
// 从 GNSS 模块读取 NMEA 数据
if (gnss_has_fix()) {
nmea_data_t nmea = gnss_parse();
// 转换为二进制格式
location.flags = GPS_FLAG_3D_FIX;
location.latitude = (int32_t)(nmea.latitude * 1e7);
location.longitude = (int32_t)(nmea.longitude * 1e7);
location.altitude = (int16_t)nmea.altitude;
location.speed = (uint8_t)(nmea.speed * 10);
location.satellites = nmea.satellites;
location.timestamp = nmea.timestamp;
// 通过 BLE 发送
send_location_data(&location);
// 存储到 Flash (用于历史轨迹)
flash_store_location(&location);
}
}
📱 手机 App 代码示例
// Android - GPS 追踪器客户端
class GPSTrackerClient(private val context: Context) {
private var bluetoothGatt: BluetoothGatt? = null
private var locationCharacteristic: BluetoothGattCharacteristic? = null
private var configCharacteristic: BluetoothGattCharacteristic? = null
// 自定义服务 UUID
companion object {
val GPS_SERVICE_UUID = UUID.fromString("12345678-1234-5678-1234-56789abcdef0")
val LOCATION_UUID = UUID.fromString("12345678-1234-5678-1234-56789abcdef1")
val CONFIG_UUID = UUID.fromString("12345678-1234-5678-1234-56789abcdef2")
val CCCD_UUID = UUID.fromString("00002902-0000-1000-8000-00805f9b34fb")
}
// 解析位置数据
private fun parseLocation(data: ByteArray): Location {
val flags = data[0].toInt()
val latitude = ((data[1].toInt() and 0xFF) or
((data[2].toInt() and 0xFF) shl 8) or
((data[3].toInt() and 0xFF) shl 16) or
((data[4].toInt() and 0xFF) shl 24)) * 1e-7
val longitude = ((data[5].toInt() and 0xFF) or
((data[6].toInt() and 0xFF) shl 8) or
((data[7].toInt() and 0xFF) shl 16) or
((data[8].toInt() and 0xFF) shl 24)) * 1e-7
val altitude = ((data[9].toInt() and 0xFF) or
((data[10].toInt() and 0xFF) shl 8)).toShort()
val speed = (data[11].toInt() and 0xFF) / 10.0f
val satellites = data[12].toInt() and 0xFF
return Location(latitude, longitude, altitude, speed, satellites)
}
// 配置上报间隔
fun setUpdateInterval(seconds: Int) {
val config = byteArrayOf(
0x01, // Command: Set interval
(seconds and 0xFF).toByte(),
((seconds shr 8) and 0xFF).toByte()
)
configCharacteristic?.value = config
bluetoothGatt?.writeCharacteristic(configCharacteristic)
}
// 读取历史轨迹
fun downloadHistory() {
val command = byteArrayOf(0x02) // Command: Download history
configCharacteristic?.value = command
bluetoothGatt?.writeCharacteristic(configCharacteristic)
}
}
⚡ 低功耗优化策略
flowchart LR
A[上电] --> B[GPS 定位]
B --> C{定位成功?}
C -->|是 | D[BLE 广播]
C -->|否 | E[等待 30s]
E --> B
D --> F{手机连接?}
F -->|是 | G[实时上报]
F -->|否 | H[存储数据]
G --> I{断开连接?}
I -->|是 | J[关闭 GPS]
I -->|否 | G
J --> K[深度休眠]
K --> L[定时唤醒]
L --> B
style A fill:#28a745,color:#fff
style J fill:#ffc107,color:#000
style K fill:#dc3545,color:#fff
| 模式 | 状态 | 功耗 | 说明 |
|---|---|---|---|
| 定位中 | GPS 工作 | 50-100mA | 搜索卫星信号 |
| 广播中 | BLE 广播 | 10-20mA | 等待手机连接 |
| 连接中 | 数据上报 | 15-25mA | 实时位置推送 |
| 休眠 | 深度睡眠 | 10-50μA | 定时唤醒定位 |
🔍 调试要点
GPS 定位问题
- 室内无信号:需要室外测试
- 冷启动慢:需要 30-60 秒
- 热启动快:3-5 秒
- 检查天线方向
BLE 连接问题
- 连接超时:调整 supervision timeout
- 数据丢失:使用 Indicate 代替 Notify
- MTU 限制:协商更大 MTU
- 干扰:避开 WiFi 信道