使用4G-GPS模块实现实时位置跟踪与可视化

项目背景

本项目旨在通过4G-GPS模块(型号:CT511N)获取实时位置数据,并通过MQTT协议将经纬度信息发送到服务器端。客户端通过Web页面展示设备的实时位置,并使用高德地图完成可视化。以下是项目中涉及的关键技术与实现思路。


快速测试:消息发送
http://cloud.nbzch.cn:5080/mqtt
topic:

  gps/location

msg:

  {"latitude":29.875093,"longitude":121.583480}

快速测试:地图展示
http://cloud.nbzch.cn:5080/gps
http://cloud.nbzch.cn:5080/gps2
2024-12-17T17:42:11.png
2024-12-17T17:31:42.png


核心功能及实现流程

1. 硬件配置与模块通信

本项目使用Arduino作为主控设备,通过串口与CT511N模块通信。
模块使用以下关键AT指令完成初始化、网络连接、GPS开启以及数据发布:

  • AT+MGPSC=1:开启GPS功能。
  • AT+MCONFIGAT+MIPSTART:配置MQTT连接参数及服务器地址。
  • AT+MGPSGET=ALL,1:实时接收GPS数据。

注意
模块输出数据中,$GNGLL字段包含我们需要的经纬度信息,通过解析该字段可获得定位数据。


2. GPS数据解析与格式转换

GPS模块的经纬度数据格式为“度分格式”(如:3958.472727),为了满足地图API使用,我们需将其转换为“十进制度”。转换公式如下:
[
text{十进制度} = text{度} + frac{text{分}}{60}
]
实现过程中,需注意以下几点:

  • 判断数据是否有效:$GNGLL字段中倒数第二项为A表示定位有效,V表示无效。
  • 平均数据处理:为了减少因GPS信号漂移造成的误差,使用多组数据的平均值进行发送。

3. MQTT协议与数据发送

CT511N模块内置MQTT协议,支持发布主题与消息。核心流程如下:

  1. 使用 AT+MCONNECT 建立MQTT连接。
  2. 将解析后的经纬度数据封装为JSON格式,例如:

    {"latitude":39.974545,"longitude":121.583888}
  3. 通过 AT+MPUBEX 指令发布数据到指定主题。

注意

  • 数据发布时需指定消息长度,否则模块可能无法正确发送。
  • 串口通信中需适当延时,确保指令执行顺序不被打乱。

4. 高德地图与前端展示

Web页面使用高德地图API进行经纬度数据的可视化展示:

  • 高德地图API初始化:通过AMapLoader加载地图,确保使用合法的key和安全密钥。
  • 标记点绘制:接收到MQTT数据后,将其解析为经纬度点,通过高德地图的Marker接口动态更新地图上的设备位置。
  • 实时刷新:通过Paho MQTT客户端监听MQTT消息,实时更新设备位置。

关键实现技巧

  • 使用AMap.Marker动态标记设备位置。
  • 在前端处理数据时,需确保数据格式与高德地图API接口匹配。

项目优化与心得

  1. 定位精度问题

由于GPS信号可能受到遮挡或多路径效应影响,单次采样结果可能偏离实际位置。通过多组数据的加权平均或滤波算法,可以有效提升定位精度。

  1. MQTT连接稳定性

在使用MQTT协议时,网络波动可能导致连接中断。为此可以加入以下优化措施:

    • 在模块端实现自动重连机制。
    • 设置较长的心跳时间(例如60秒)以减少频繁的连接开销。
    1. 前端性能优化

    当设备移动频率较高时,地图的频繁更新可能导致卡顿。可以通过以下方式优化:

    • 设定最小移动距离阈值,仅当设备移动超出一定距离时更新标记点。
    • 使用地图平滑移动效果(如AMap.Marker.moveTo)提升用户体验。

    总结

    本项目通过4G-GPS模块和高德地图API的结合,实现了设备的实时定位和位置可视化功能。整个过程涉及硬件通信、数据解析、协议通信和前端展示多个环节,充分体现了物联网技术的集成与应用。

    未来,可以进一步优化数据处理算法,提升定位精度,并结合数据库实现轨迹回放功能,使其在更多场景中发挥作用。


    代码:

    arduino:

    #include <SoftwareSerial.h>
    
    // 定义软件串口用于 4G 模块通信(RX=10, TX=11)
    SoftwareSerial moduleSerial(10, 11);
    
    // GPS 数据缓冲区
    char gpsBuffer[200]; // 存储 GPS 数据的缓冲区
    bool gpsDataReady = false;
    
    // 函数声明
    void sendCommand(const char *cmd);
    void parseGPSData(const char *data);
    void sendMQTTMessage(float latitude, float longitude);
    
    // 初始化
    void setup() {
      // 设置串口通信
      Serial.begin(9600);        // Arduino 串口调试
      moduleSerial.begin(9600);  // 模块串口通信
    
      // 等待模块启动
      delay(3000);
      Serial.println("初始化模块...");
    
      // 初始化 GPS 定位
      sendCommand("AT+MGPSC=1\r\n"); // 开启 GPS 功能
      delay(500);
      sendCommand("AT+MGPSGET=0\r\n"); // 暂时关闭 NMEA 数据输出
      delay(500);
    
      // 设置 4G MQTT 连接
      sendCommand("AT+MDISCONNECT\r\n"); // 断开已有 MQTT 连接
      delay(200);
      sendCommand("AT+MIPCLOSE\r\n");    // 关闭当前会话
      delay(200);
      sendCommand("AT+QICSGP=1,1,\"\",\"\",\"\"\r\n"); // 设置 APN
      delay(200);
      sendCommand("AT+NETOPEN\r\n");     // 打开网络
      delay(3000);
    
      // 配置 MQTT 连接
      sendCommand("AT+MCONFIG=\"设备名称\",\"\",\"\",0,0,0,\"1883\",\"2024\"\r\n");
      delay(200);
      sendCommand("AT+MIPSTART=\"nbzch.cn\",1883,3\r\n"); // 连接 MQTT Broker
      delay(2000);
      sendCommand("AT+MCONNECT=0,60\r\n"); // 连接到 MQTT 服务器
      delay(200);
      sendCommand("AT+MGPSGET=ALL,1\r\n");
      delay(200);
      sendMQTTMessage(29.875172,121.583600);// 回家
    }
    
    void loop() {
      static int index = 0;
      while (moduleSerial.available()) {
        char c = moduleSerial.read();
    
        if (c == '\n') {
          gpsBuffer[index] = '\0';
          index = 0;
          gpsDataReady = true;
        } else if (c != '\r') {
          gpsBuffer[index++] = c;
        }
    
        if (gpsDataReady) {
          gpsDataReady = false;
          if (strncmp(gpsBuffer, "$GNGLL", 6) == 0) {
            Serial.println(gpsBuffer); // 打印完整的 $GNGLL 句子
            parseGPSData(gpsBuffer);
          }
        }
      }
    }
    
    // 发送 AT 指令并调试输出
    void sendCommand(const char *cmd) {
      moduleSerial.print(cmd);
      Serial.print("发送指令: ");
      Serial.println(cmd);
      delay(500); // 等待模块响应
      while (moduleSerial.available()) {
        Serial.write(moduleSerial.read()); // 打印模块的响应
      }
    }
    // 为了转换GPS数据中的纬度和经度格式,从度和分的表示法(如3958.472727)转换为十进制度(如39.97454545),编写一个函数来实现这一计算。
    float convertToDecimalDegrees(float rawValue) {
      int degrees = (int)(rawValue / 100);           // 提取度部分
      float minutes = rawValue - (degrees * 100);    // 提取分部分
      return degrees + (minutes / 60.0);             // 转换为十进制度
    }
    
    // 解析 GPS 数据
    void parseGPSData(const char *data) {
      char *token;
      char gpsLat[15], gpsLon[15], status;
    
      token = strtok(data, ",");
      token = strtok(NULL, ","); // Latitude
      strcpy(gpsLat, token);
    
      token = strtok(NULL, ",");
      token = strtok(NULL, ","); // Longitude
      strcpy(gpsLon, token);
    
      token = strtok(NULL, ",");
      token = strtok(NULL, ",");
      token = strtok(NULL, ","); // Status
      status = *token;
    
      if (status == 'A') { // 如果定位有效
        float rawLatitude = atof(gpsLat);            // 原始纬度
        float rawLongitude = atof(gpsLon);          // 原始经度
        float latitude = convertToDecimalDegrees(rawLatitude);  // 转换纬度
        float longitude = convertToDecimalDegrees(rawLongitude); // 转换经度
    
        // 调试输出
        Serial.print("纬度: ");
        Serial.println(latitude, 6);
        Serial.print("经度: ");
        Serial.println(longitude, 6);
    
        // 通过 MQTT 发送 GPS 数据
        sendMQTTMessage(latitude, longitude);
      } else {
        Serial.println("无效定位");
      }
    }
    
    void sendMQTTMessage(float latitude, float longitude) {
      char latBuffer[15]; // 用于存储纬度的字符串
      char lonBuffer[15]; // 用于存储经度的字符串
    
      // 转换浮点数为字符串
      dtostrf(latitude, 0, 6, latBuffer); // 6 表示保留 6 位小数
      dtostrf(longitude, 0, 6, lonBuffer);
    
      char mqttMessage[100]; // 增大缓冲区
      snprintf(mqttMessage, sizeof(mqttMessage), "{\"latitude\":%s,\"longitude\":%s}", latBuffer, lonBuffer);
    
      // 调试打印
      Serial.print("调试: MQTT 消息 = ");
      Serial.println(mqttMessage);
    
      char mqttCommand[100];
      snprintf(mqttCommand, sizeof(mqttCommand), "AT+MPUBEX=\"gps/location\",0,0,%d\r\n", strlen(mqttMessage));
    
      // 调试打印
      Serial.print("发送指令: ");
      Serial.println(mqttCommand);
    
      sendCommand(mqttCommand);       // 发送 MQTT 发布指令
      moduleSerial.print(mqttMessage); // 发送消息内容
    
      Serial.print("发送 MQTT 消息: ");
      Serial.println(mqttMessage);
      Serial.println("等待5S");
      delay(5000);
    }

    地图html

    <!DOCTYPE html>
    <html lang="en">
    <head>
        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width, initial-scale=1.0">
        <title>MQTT GPS 位置展示</title>
        <!-- 引入 Leaflet 地图库 CSS -->
        <link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css" />
        <style>
            #map {
                height: 600px; /* 地图高度 */
                width: 100%;
            }
        </style>
    </head>
    <body>
        <h2>MQTT GPS 定位器</h2>
        <!-- 地图容器 -->
        <div id="map"></div>
    
        <!-- 引入 Leaflet 和 MQTT.js -->
        <script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script>
        <script src="https://unpkg.com/mqtt/dist/mqtt.min.js"></script>
    
        <script>
            // 1. 初始化地图
            const map = L.map('map').setView([0, 0], 2); // 初始显示在 (0,0) 点,缩放等级 2
    
            // 加载 OpenStreetMap 图层
            L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
                maxZoom: 19,
                attribution: '© OpenStreetMap'
            }).addTo(map);
    
            // 2. 定义一个 Marker,用于显示当前位置
            let marker = L.marker([0, 0]).addTo(map);
            let circle = null; // 可选:添加一个圆圈标识区域
    
            // 3. 连接到 MQTT 服务器
            const MQTT_BROKER = "ws://nbzch.cn:8083/mqtt"; // 替换为你的MQTT服务器地址
            const MQTT_TOPIC = "gps/location"; // 替换为你的主题
    
            const client = mqtt.connect(MQTT_BROKER); // 创建 MQTT 客户端
    
            client.on('connect', function () {
                console.log("MQTT 已连接");
                client.subscribe(MQTT_TOPIC, function (err) {
                    if (!err) {
                        console.log("订阅主题成功:", MQTT_TOPIC);
                    } else {
                        console.error("订阅主题失败:", err);
                    }
                });
            });
    
            client.on('message', function (topic, message) {
                console.log(`收到消息 [${topic}]:`, message.toString());
    
                // 解析消息并更新地图
                try {
                    const payload = JSON.parse(message.toString());
                    const lat = parseFloat(payload.latitude);  // 纬度
                    const lon = parseFloat(payload.longitude); // 经度
    
                    if (!isNaN(lat) && !isNaN(lon)) {
                        // 更新 Marker 位置
                        marker.setLatLng([lat, lon]);
                        map.setView([lat, lon], 16); // 地图定位到新位置,缩放级别13
    
                        // 可选:添加一个圆圈标识范围
                        if (circle) map.removeLayer(circle); // 移除旧的圆圈
                        circle = L.circle([lat, lon], { radius: 50 }).addTo(map); // 半径 50 米
                    } else {
                        console.error("无效的经纬度数据");
                    }
                } catch (e) {
                    console.error("消息解析错误:", e);
                }
            });
    
            client.on('error', function (error) {
                console.error("MQTT 连接错误:", error);
            });
        </script>
    </body>
    </html>

    高德地图html

    <!DOCTYPE html>
    <html>
      <head>
        <meta charset="utf-8" />
        <meta http-equiv="X-UA-Compatible" content="IE=edge" />
        <meta name="viewport" content="initial-scale=1.0, user-scalable=no, width=device-width" />
        <title>高德地图 MQTT GPS 定位</title>
        <style>
          html, body, #container {
            width: 100%;
            height: 100%;
            margin: 0;
            padding: 0;
          }
        </style>
      </head>
      <body>
        <div id="container"></div>
    
        <!-- 安全密钥配置 -->
        <script type="text/javascript">
          window._AMapSecurityConfig = {
            securityJsCode: "你的安全密钥", // 替换为申请的安全密钥
          };
        </script>
    
        <!-- 引入高德地图 JS API Loader -->
        <script src="https://webapi.amap.com/loader.js"></script>
        <!-- 引入 MQTT.js -->
        <script src="https://unpkg.com/mqtt/dist/mqtt.min.js"></script>
    
        <script type="text/javascript">
          // 加载高德地图 JS API
          AMapLoader.load({
            key: "94af36b1b75bcda68807bd2684941e79", // 替换为申请的 Web 端 Key
            version: "2.0",
          })
            .then((AMap) => {
              // 1. 初始化高德地图
              const map = new AMap.Map("container", {
                zoom: 10, // 地图缩放级别
                center: [121.594, 29.8706], // 初始化中心位置(宁波三中)
              });
    
              // 2. 添加一个默认的标记
              const marker = new AMap.Marker({
                position: [116.39, 39.9], // 默认位置
              });
              map.add(marker);
    
              // 3. MQTT 客户端连接
              const MQTT_BROKER = "ws://nbzch.cn:8083/mqtt"; // 替换为 MQTT Broker 地址
              const MQTT_TOPIC = "gps/location"; // 替换为主题
    
              const client = mqtt.connect(MQTT_BROKER);
    
              client.on("connect", function () {
                console.log("MQTT 已连接");
                client.subscribe(MQTT_TOPIC, function (err) {
                  if (!err) {
                    console.log("订阅主题成功:", MQTT_TOPIC);
                  } else {
                    console.error("订阅主题失败:", err);
                  }
                });
              });
    
              client.on("message", function (topic, message) {
                console.log(`收到消息 [${topic}]:`, message.toString());
    
                // 解析消息并更新地图位置
                try {
                  const payload = JSON.parse(message.toString());
                  const lat = parseFloat(payload.latitude);  // 纬度
                  const lon = parseFloat(payload.longitude); // 经度
    
                  if (!isNaN(lat) && !isNaN(lon)) {
                    // 更新 Marker 位置
                    marker.setPosition([lon, lat]);
                    map.setCenter([lon, lat]); // 将地图中心移到新位置
                  } else {
                    console.error("无效的经纬度数据");
                  }
                } catch (e) {
                  console.error("消息解析错误:", e);
                }
              });
    
              client.on("error", function (error) {
                console.error("MQTT 连接错误:", error);
              });
            })
            .catch((e) => {
              console.error("高德地图加载失败:", e); // 加载错误提示
            });
        </script>
      </body>
    </html>

    作者注
    感谢硬件设备与地图服务的支持,使本项目得以顺利实现。如果您有类似需求或项目问题,欢迎留言交流!

    发表评论