使用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
核心功能及实现流程
1. 硬件配置与模块通信
本项目使用Arduino作为主控设备,通过串口与CT511N模块通信。
模块使用以下关键AT指令完成初始化、网络连接、GPS开启以及数据发布:
- AT+MGPSC=1:开启GPS功能。
- AT+MCONFIG 和 AT+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协议,支持发布主题与消息。核心流程如下:
- 使用 AT+MCONNECT 建立MQTT连接。
将解析后的经纬度数据封装为JSON格式,例如:
{"latitude":39.974545,"longitude":121.583888}
- 通过 AT+MPUBEX 指令发布数据到指定主题。
注意:
- 数据发布时需指定消息长度,否则模块可能无法正确发送。
- 串口通信中需适当延时,确保指令执行顺序不被打乱。
4. 高德地图与前端展示
Web页面使用高德地图API进行经纬度数据的可视化展示:
- 高德地图API初始化:通过
AMapLoader
加载地图,确保使用合法的key
和安全密钥。 - 标记点绘制:接收到MQTT数据后,将其解析为经纬度点,通过高德地图的
Marker
接口动态更新地图上的设备位置。 - 实时刷新:通过
Paho MQTT
客户端监听MQTT消息,实时更新设备位置。
关键实现技巧:
- 使用
AMap.Marker
动态标记设备位置。 - 在前端处理数据时,需确保数据格式与高德地图API接口匹配。
项目优化与心得
- 定位精度问题
由于GPS信号可能受到遮挡或多路径效应影响,单次采样结果可能偏离实际位置。通过多组数据的加权平均或滤波算法,可以有效提升定位精度。
- MQTT连接稳定性
在使用MQTT协议时,网络波动可能导致连接中断。为此可以加入以下优化措施:
- 在模块端实现自动重连机制。
- 设置较长的心跳时间(例如60秒)以减少频繁的连接开销。
- 前端性能优化
当设备移动频率较高时,地图的频繁更新可能导致卡顿。可以通过以下方式优化:
- 设定最小移动距离阈值,仅当设备移动超出一定距离时更新标记点。
- 使用地图平滑移动效果(如
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>
作者注
感谢硬件设备与地图服务的支持,使本项目得以顺利实现。如果您有类似需求或项目问题,欢迎留言交流!