🛰️ 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

📊 位置数据格式

// 位置数据特征值数据格式 (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

逐步构建过程

// ========== 步骤 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

数据包大小计算

头部大小 数据大小 本层总大小 累计大小
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 信道