使用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信号可能受到遮挡或多路径效应影响,单次采样结果可能偏离实际位置。通过多组数据的加权平均或滤波算法,可以有效提升定位精度。
  2. MQTT连接稳定性
    在使用MQTT协议时,网络波动可能导致连接中断。为此可以加入以下优化措施:

    • 在模块端实现自动重连机制。
    • 设置较长的心跳时间(例如60秒)以减少频繁的连接开销。
  3. 前端性能优化
    当设备移动频率较高时,地图的频繁更新可能导致卡顿。可以通过以下方式优化:

    • 设定最小移动距离阈值,仅当设备移动超出一定距离时更新标记点。
    • 使用地图平滑移动效果(如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=\"gpsTest\",\"\",\"\",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>

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

发表评论