一、前言
1.1 项目开发背景
随着移动互联网、物联网通信技术以及卫星定位技术的持续发展,基于位置服务(Location Based Service,LBS)的智能追踪与安全监护应用逐渐成为智能终端领域的重要研究方向。在日常生活场景中,行李箱遗失、儿童走失、老人外出失联、宠物丢失等问题频繁发生,传统依赖人工寻找或简单蓝牙定位的方式存在距离受限、覆盖范围小、实时性不足等问题,难以满足远距离目标监护需求。因此,构建一种具备广域通信能力、实时定位能力以及区域安全预警能力的电子围栏系统,具有较高的实际应用价值和研究意义。
近年来,全球卫星导航系统技术不断成熟,GPS与北斗双模定位方案逐渐普及,使定位设备能够获得更加稳定和精准的地理位置信息。同时,蜂窝物联网通信技术快速发展,4G Cat.1模块凭借低功耗、覆盖广、成本适中的特点,在移动定位终端领域得到广泛应用。通过将定位数据实时上传至云端平台,实现设备状态远程监控、位置轨迹查看和异常行为预警,已经成为智能追踪设备的重要发展方向。
电子围栏技术作为位置安全管理的重要组成部分,其核心思想是在地图上构建虚拟安全区域,并持续监测目标是否越界。当目标超出预设范围时,系统自动触发报警提醒,从而实现主动式安全防护。相比传统定位系统仅提供位置显示功能,电子围栏能够进一步结合距离计算、边界判断和报警机制,提高目标管理的智能化程度。目前该技术已经广泛应用于儿童监护、老人看护、物流资产管理、车辆防盗、宠物防丢等多个领域。
与此同时,云平台与移动终端技术的发展进一步推动了物联网设备向远程化、可视化方向演进。通过云端数据管理,可实现设备状态统一接入、数据存储、消息下发以及跨终端访问。用户能够通过手机APP或电脑客户端实时查看目标位置、电量状态、电子围栏信息及历史位置数据,并结合地图完成可视化管理,提升系统使用体验和管理效率。
基于上述背景,本项目设计了一种基于物联网与GPS卫星定位技术的电子围栏系统,以STM32F103C8T6作为核心控制单元,结合Air780E 4G通信模块、ATGM336H双模卫星定位模块以及华为云IoT平台,实现目标位置实时采集、远程数据上传、电子围栏越界检测、距离估算、报警提醒以及多终端可视化管理等功能。系统通过Qt开发Android与Windows客户端,并结合地图服务实现位置展示和围栏绘制,从而形成集定位、通信、预警和远程监控于一体的智能位置安全管理方案,为个人物品防丢、人员安全监护及移动资产管理提供一种低成本、易部署、可扩展的技术实现路径。
1.2 设计实现的功能
(1)实时GPS定位追踪功能
系统通过ATGM336H-5N北斗+GPS双模定位模块采集目标设备当前地理位置信息,并利用串口通信将定位数据传输至STM32主控进行解析处理。系统支持实时获取经纬度坐标,实现对目标物体的位置追踪,可适用于行李箱、背包、宠物、儿童及老人等目标对象的远程定位监护。定位信息通过4G网络上传至云平台,同时支持移动端和电脑端实时查看。
(2)设备与围栏边缘距离估算功能
系统根据目标实时定位坐标与电子围栏区域边界坐标进行位置计算,实现目标与围栏边缘之间距离的动态估算,并在APP端进行数据显示。用户能够直观掌握设备当前位置与安全区域边界之间的大致距离,为提前预判越界风险提供依据。
(3)电子围栏区域设置与越界报警功能
系统支持电子围栏安全区域管理功能,用户可在APP端手动开启或关闭围栏监控功能,并能够自主设置围栏类型与范围参数。围栏支持圆形围栏和矩形围栏两种模式,用户可按照实际使用需求设置围栏尺寸,单位为米。当系统检测目标位置超出预设范围时,自动触发报警机制。
(4)目标丢失报警功能
系统持续监测目标是否处于电子围栏范围内。当目标离开设定区域后,系统自动判定为异常状态,并触发丢失报警。报警信息上传至云端后,APP端立即发出语音提示,同时设备本地可通过蜂鸣器进行报警提醒,提高目标找回效率。
(5)设备电量监测与显示功能
系统利用STM32内部ADC对锂电池电压进行实时采集,通过电压换算获取剩余电量百分比,并将电量数据上传至物联网平台。用户可在Android手机APP和Windows客户端实时查看设备电池状态,便于及时充电和维护设备持续运行。
(6)物联网云端数据上传功能
系统采用Air780E Cat.1通信模块完成移动网络接入,通过MQTT协议将设备运行数据上传至华为云IoT平台。上传内容包括设备状态、电池电量、定位信息、报警开关状态以及围栏中心位置等,实现设备远程接入、状态同步和云端统一管理。
(7)多终端远程监控功能
系统支持Android手机APP与Windows电脑上位机双终端访问模式。用户无需接触设备即可远程查看目标当前位置、距离信息、电量状态以及最后一次有效定位位置,实现跨平台远程管理和监控。
(8)地图定位显示与电子围栏绘制功能
系统在APP端集成地图显示能力,通过地图界面实时展示目标位置,并支持围栏区域可视化绘制。用户能够在地图上设置围栏中心点、选择围栏形状以及调整围栏范围,实现电子围栏配置与位置监控的一体化操作。
(9)设备状态指示与本地提醒功能
系统通过LED指示灯显示设备运行状态,包括程序运行状态、数据上传状态以及报警状态等信息。同时结合有源蜂鸣器实现本地报警提示,提高设备运行状态的可视化程度和使用体验。
1.3 项目硬件模块组成
(1)STM32主控模块
系统采用STM32F103C8T6作为核心控制器,负责整个电子围栏设备的数据采集、任务调度以及通信控制。主控模块完成GPS数据解析、围栏距离计算、电池电量采集、报警逻辑处理以及物联网数据打包上传等功能。同时负责协调OLED显示、蜂鸣器、LED状态指示等外围模块工作,实现系统整体运行控制。
(2)GPS卫星定位模块
定位部分采用ATGM336H-5N北斗+GPS双模定位模块,通过串口方式与STM32进行通信。模块输出符合NMEA-0183协议的数据帧,系统对经纬度、定位状态、时间信息进行解析,实现目标位置实时获取,为电子围栏判定、距离计算和地图显示提供定位基础数据。
(3)4G无线通信模块
系统采用Air780E Cat.1通信模块实现设备联网功能。模块上电后自动完成移动网络接入,并通过TCP连接建立MQTT通信链路,将设备定位信息、电量状态、报警状态等数据上传至华为云物联网平台,同时接收云端下发的控制指令,实现远程监控与管理。
(4)OLED显示模块
系统配置OLED显示屏作为本地数据显示终端,通过GPIO模拟通信方式与主控连接,用于显示设备运行状态及相关信息。显示内容可包括定位状态、联网状态、电量信息等,便于用户现场查看设备工作情况。
(5)电源管理与供电模块
供电部分采用TP4056充电管理模块配合18650锂电池组成移动供电系统。系统支持Type-C接口充电,可通过充电器或充电宝进行供电补充,实现设备便携式部署与长时间运行。
(6)电池电量检测模块
系统利用STM32内部ADC采样功能,通过分压电路对锂电池电压进行实时采集,将采样结果转换为电量百分比后上传至云端,并同步显示至APP端,实现设备剩余电量监测功能。
(7)蜂鸣器报警模块
系统采用高电平触发有源蜂鸣器作为本地报警装置。当目标超出电子围栏范围或触发丢失报警条件时,主控输出控制信号驱动蜂鸣器发声,实现现场报警提醒功能。
(8)LED状态指示模块
系统设计三路LED状态指示电路,用于显示设备运行状态。其中LED1用于程序运行状态指示,LED2用于数据上传状态显示,LED3用于报警状态提示,方便用户快速判断设备当前工作情况。
(9)运动状态检测模块
系统配置ADXL345加速度传感器模块,用于采集设备运动状态信息。通过检测目标的加速度变化情况,辅助判断目标当前处于静止或运动状态,并将结果上传至云平台,实现设备状态监测功能。
项目开发使用的全部软件工具已经上传到网盘:https://ccnr8sukk85n.feishu.cn/wiki/QjY8weDYHibqRYkFP2qcA9aGnvb?from=from_copylink
1.4 系统框架图
1.5 运行流程图
二、部署华为云物联网平台
华为云官网: https://www.huaweicloud.com/
打开官网,搜索物联网,就能快速找到 设备接入IoTDA。
2.1 物联网平台介绍
华为云物联网平台(IoT 设备接入云服务)提供海量设备的接入和管理能力,将物理设备联接到云,支撑设备数据采集上云和云端下发命令给设备进行远程控制,配合华为云其他产品,帮助我们快速构筑物联网解决方案。
使用物联网平台构建一个完整的物联网解决方案主要包括3部分:物联网平台、业务应用和设备。
物联网平台作为连接业务应用和设备的中间层,屏蔽了各种复杂的设备接口,实现设备的快速接入;同时提供强大的开放能力,支撑行业用户构建各种物联网解决方案。
设备可以通过固网、2G/3G/4G/5G、NB-IoT、Wifi等多种网络接入物联网平台,并使用LWM2M/CoAP、MQTT、HTTPS协议将业务数据上报到平台,平台也可以将控制命令下发给设备。
业务应用通过调用物联网平台提供的API,实现设备数据采集、命令下发、设备管理等业务场景。
2.2 开通物联网服务
地址: https://www.huaweicloud.com/product/iothub.html
开通免费单元。
点击立即创建。
正在创建标准版实例,需要等待片刻。
创建完成之后,点击详情。 可以看到标准版实例的设备接入端口和地址。
下面框起来的就是端口号和域名
点击实例名称,可以查看当前免费单元的配置情况。
开通之后,点击接入信息,也能查看接入信息。 我们当前设备准备采用MQTT协议接入华为云平台,这里可以看到MQTT协议的地址和端口号等信息。
总结:
端口号: MQTT (1883)| MQTTS (8883)
接入地址: dab1a1f2c6.st1.iotda-device.cn-north-4.myhuaweicloud.com
根据域名地址得到IP地址信息:
打开Windows电脑的命令行控制台终端,使用ping 命令。ping一下即可。
Microsoft Windows [版本 10.0.19045.5011]
(c) Microsoft Corporation。保留所有权利。
C:UsersLenovo>ping dab1a1f2c6.st1.iotda-device.cn-north-4.myhuaweicloud.com
正在 Ping dab1a1f2c6.st1.iotda-device.cn-north-4.myhuaweicloud.com [117.78.5.125] 具有 32 字节的数据:
来自 117.78.5.125 的回复: 字节=32 时间=37ms TTL=44
来自 117.78.5.125 的回复: 字节=32 时间=37ms TTL=44
来自 117.78.5.125 的回复: 字节=32 时间=37ms TTL=44
来自 117.78.5.125 的回复: 字节=32 时间=37ms TTL=44
117.78.5.125 的 Ping 统计信息:
数据包: 已发送 = 4,已接收 = 4,丢失 = 0 (0% 丢失),
往返行程的估计时间(以毫秒为单位):
最短 = 37ms,最长 = 37ms,平均 = 37ms
C:UsersLenovo>
MQTT协议接入端口号有两个,1883是非加密端口,8883是证书加密端口,单片机无法加载证书,所以使用1883端口合适。
2.3 创建产品
链接:https://console.huaweicloud.com/iotdm/?region=cn-north-4#/dm-dev/all-product?instanceId=03c5c68c-e588-458c-90c3-9e4c640be7af
(1)创建产品
(2)填写产品信息
根据自己产品名字填写,下面的设备类型选择自定义类型。
(3)产品创建成功
创建完成之后点击查看详情。
(4)添加自定义模型
产品创建完成之后,点击进入产品详情页面,翻到最下面可以看到模型定义。
模型简单来说: 就是存放设备上传到云平台的数据。
你可以根据自己的产品进行创建。
比如:
烟雾可以叫 MQ2
温度可以叫 Temperature
湿度可以叫 humidity
火焰可以叫 flame
其他的传感器自己用单词简写命名即可。 这就是你的单片机设备端上传到服务器的数据名字。
先点击自定义模型。
再创建一个服务ID。
接着点击新增属性。
2.4 添加设备
产品是属于上层的抽象模型,接下来在产品模型下添加实际的设备。添加的设备最终需要与真实的设备关联在一起,完成数据交互。
(1)注册设备
(2)根据自己的设备填写
(3)保存设备信息
创建完毕之后,点击保存并关闭,得到创建的设备密匙信息。该信息在后续生成MQTT三元组的时候需要使用。
(4)设备创建完成
(5)设备详情
2.5 MQTT协议主题订阅与发布
(1)MQTT协议介绍
当前的设备是采用MQTT协议与华为云平台进行通信。
MQTT是一个物联网传输协议,它被设计用于轻量级的发布/订阅式消息传输,旨在为低带宽和不稳定的网络环境中的物联网设备提供可靠的网络服务。MQTT是专门针对物联网开发的轻量级传输协议。MQTT协议针对低带宽网络,低计算能力的设备,做了特殊的优化,使得其能适应各种物联网应用场景。目前MQTT拥有各种平台和设备上的客户端,已经形成了初步的生态系统。
MQTT是一种消息队列协议,使用发布/订阅消息模式,提供一对多的消息发布,解除应用程序耦合,相对于其他协议,开发更简单;MQTT协议是工作在TCP/IP协议上;由TCP/IP协议提供稳定的网络连接;所以,只要具备TCP协议栈的网络设备都可以使用MQTT协议。 本次设备采用的ESP8266就具备TCP协议栈,能够建立TCP连接,所以,配合STM32代码里封装的MQTT协议,就可以与华为云平台完成通信。
华为云的MQTT协议接入帮助文档在这里: https://support.huaweicloud.com/devg-iothub/iot_02_2200.html
业务流程:
(2)华为云平台MQTT协议使用限制
| 描述 | 限制 |
|---|---|
| 支持的MQTT协议版本 | 3.1.1 |
| 与标准MQTT协议的区别 | 支持Qos 0和Qos 1支持Topic自定义不支持QoS2不支持will、retain msg |
| MQTTS支持的安全等级 | 采用TCP通道基础 + TLS协议(最高TLSv1.3版本) |
| 单帐号每秒最大MQTT连接请求数 | 无限制 |
| 单个设备每分钟支持的最大MQTT连接数 | 1 |
| 单个MQTT连接每秒的吞吐量,即带宽,包含直连设备和网关 | 3KB/s |
| MQTT单个发布消息最大长度,超过此大小的发布请求将被直接拒绝 | 1MB |
| MQTT连接心跳时间建议值 | 心跳时间限定为30至1200秒,推荐设置为120秒 |
| 产品是否支持自定义Topic | 支持 |
| 消息发布与订阅 | 设备只能对自己的Topic进行消息发布与订阅 |
| 每个订阅请求的最大订阅数 | 无限制 |
(3)主题订阅格式
帮助文档地址:https://support.huaweicloud.com/devg-iothub/iot_02_2200.html
对于设备而言,一般会订阅平台下发消息给设备 这个主题。
设备想接收平台下发的消息,就需要订阅平台下发消息给设备 的主题,订阅后,平台下发消息给设备,设备就会收到消息。
如果设备想要知道平台下发的消息,需要订阅上面图片里标注的主题。
以当前设备为例,最终订阅主题的格式如下:
$oc/devices/{device_id}/sys/messages/down
最终的格式:
$oc/devices/663cb18871d845632a0912e7_dev1/sys/messages/down
(4)主题发布格式
对于设备来说,主题发布表示向云平台上传数据,将最新的传感器数据,设备状态上传到云平台。
这个操作称为:属性上报。
帮助文档地址:https://support.huaweicloud.com/usermanual-iothub/iot_06_v5_3010.html
根据帮助文档的介绍, 当前设备发布主题,上报属性的格式总结如下:
发布的主题格式:
$oc/devices/{device_id}/sys/properties/report
最终的格式:
$oc/devices/663cb18871d845632a0912e7_dev1/sys/properties/report
发布主题时,需要上传数据,这个数据格式是JSON格式。
上传的JSON数据格式如下:
{
"services": [
{
"service_id": <填服务ID>,
"properties": {
"<填属性名称1>": <填属性值>,
"<填属性名称2>": <填属性值>,
..........
}
}
]
}
根据JSON格式,一次可以上传多个属性字段。 这个JSON格式里的,服务ID,属性字段名称,属性值类型,在前面创建产品的时候就已经介绍了,不记得可以翻到前面去查看。
根据这个格式,组合一次上传的属性数据:
{"services": [{"service_id": "stm32","properties":{"你的字段名字1":30,"你的字段名字2":10,"你的字段名字3":1,"你的字段名字4":0}}]}
2.6 MQTT三元组
MQTT协议登录需要填用户ID,设备ID,设备密码等信息,就像我们平时登录QQ,微信一样要输入账号密码才能登录。MQTT协议登录的这3个参数,一般称为MQTT三元组。
接下来介绍,华为云平台的MQTT三元组参数如何得到。
(1)MQTT服务器地址
要登录MQTT服务器,首先记得先知道服务器的地址是多少,端口是多少。
帮助文档地址:https://console.huaweicloud.com/iotdm/?region=cn-north-4#/dm-portal/home
MQTT协议的端口支持1883和8883,它们的区别是:8883 是加密端口更加安全。但是单片机上使用比较困难,所以当前的设备是采用1883端口进连接的。
根据上面的域名和端口号,得到下面的IP地址和端口号信息: 如果设备支持填写域名可以直接填域名,不支持就直接填写IP地址。 (IP地址就是域名解析得到的)
华为云的MQTT服务器地址:117.78.5.125
华为云的MQTT端口号:1883
如何得到IP地址?如何域名转IP? 打开Windows的命令行输入以下命令。
ping ad635970a1.st1.iotda-device.cn-north-4.myhuaweicloud.com
(2)生成MQTT三元组
华为云提供了一个在线工具,用来生成MQTT鉴权三元组: https://iot-tool.obs-website.cn-north-4.myhuaweicloud.com/
打开这个工具,填入设备的信息(也就是刚才创建完设备之后保存的信息),点击生成,就可以得到MQTT的登录信息了。
下面是打开的页面:
填入设备的信息: (上面两行就是设备创建完成之后保存得到的)
直接得到三元组信息。
得到三元组之后,设备端通过MQTT协议登录鉴权的时候,填入参数即可。
ClientId 663cb18871d845632a0912e7_dev1_0_0_2024050911
Username 663cb18871d845632a0912e7_dev1
Password 71b82deae83e80f04c4269b5bbce3b2fc7c13f610948fe210ce18650909ac237
2.7 模拟设备登录测试
经过上面的步骤介绍,已经创建了产品,设备,数据模型,得到MQTT登录信息。 接下来就用MQTT客户端软件模拟真实的设备来登录平台。测试与服务器通信是否正常。
MQTT软件下载地址【免费】: https://download.csdn.net/download/xiaolong1126626497/89928772
(1)填入登录信息
打开MQTT客户端软件,对号填入相关信息(就是上面的文本介绍)。然后,点击登录,订阅主题,发布主题。
(2)打开网页查看
完成上面的操作之后,打开华为云网页后台,可以看到设备已经在线了。
点击详情页面,可以看到上传的数据:
到此,云平台的部署已经完成,设备已经可以正常上传数据了。
(3)MQTT登录测试参数总结
MQTT服务器: 117.78.5.125
MQTT端口号: 183
//物联网服务器的设备信息
#define MQTT_ClientID "663cb18871d845632a0912e7_dev1_0_0_2024050911"
#define MQTT_UserName "663cb18871d845632a0912e7_dev1"
#define MQTT_PassWord "71b82deae83e80f04c4269b5bbce3b2fc7c13f610948fe210ce18650909ac237"
//订阅与发布的主题
#define SET_TOPIC "$oc/devices/663cb18871d845632a0912e7_dev1/sys/messages/down" //订阅
#define POST_TOPIC "$oc/devices/663cb18871d845632a0912e7_dev1/sys/properties/report" //发布
发布的数据:
{"services": [{"service_id": "stm32","properties":{"你的字段名字1":30,"你的字段名字2":10,"你的字段名字3":1,"你的字段名字4":0}}]}
2.8 创建IAM账户
创建一个IAM账户,因为接下来开发上位机,需要使用云平台的API接口,这些接口都需要token进行鉴权。简单来说,就是身份的认证。 调用接口获取Token时,就需要填写IAM账号信息。所以,接下来演示一下过程。
地址: https://console.huaweicloud.com/iam/?region=cn-north-4#/iam/users
**【1】获取项目凭证 ** 点击左上角用户名,选择下拉菜单里的我的凭证
项目凭证:
28add376c01e4a61ac8b621c714bf459
【2】创建IAM用户
鼠标放在左上角头像上,在下拉菜单里选择统一身份认证。
点击左上角创建用户。
创建成功:
【3】创建完成
用户信息如下:
主用户名 l19504562721
IAM用户 ds_abc
密码 DS12345678
2.9 获取影子数据
帮助文档:https://support.huaweicloud.com/api-iothub/iot_06_v5_0079.html
设备影子介绍:
设备影子是一个用于存储和检索设备当前状态信息的JSON文档。
每个设备有且只有一个设备影子,由设备ID唯一标识
设备影子仅保存最近一次设备的上报数据和预期数据
无论该设备是否在线,都可以通过该影子获取和设置设备的属性
简单来说:设备影子就是保存,设备最新上传的一次数据。
我们设计的软件里,如果想要获取设备的最新状态信息,就采用设备影子接口。
如果对接口不熟悉,可以先进行在线调试:https://apiexplorer.developer.huaweicloud.com/apiexplorer/doc?product=IoTDA&api=ShowDeviceShadow
在线调试接口,可以请求影子接口,了解请求,与返回的数据格式。
调试完成看右下角的响应体,就是返回的影子数据。
设备影子接口返回的数据如下:
{
"device_id": "663cb18871d845632a0912e7_dev1",
"shadow": [
{
"service_id": "stm32",
"desired": {
"properties": null,
"event_time": null
},
"reported": {
"properties": {
"DHT11_T": 18,
"DHT11_H": 90,
"BH1750": 38,
"MQ135": 70
},
"event_time": "20240509T113448Z"
},
"version": 3
}
]
}
调试成功之后,可以得到访问影子数据的真实链接,接下来的代码开发中,就采用Qt写代码访问此链接,获取影子数据,完成上位机开发。
链接如下:
https://ad635970a1.st1.iotda-app.cn-north-4.myhuaweicloud.com:443/v5/iot/28add376c01e4a61ac8b621c714bf459/devices/663cb18871d845632a0912e7_dev1/shadow
三、上位机开发
3.1 Qt开发环境安装
Qt的中文官网: https://www.qt.io/zh-cn/
QT5.12.6的下载地址:https://download.qt.io/archive/qt/5.12/5.12.6
打开下载链接后选择下面的版本进行下载:
如果下载不了,可以在网盘里找到安装包下载: 飞书文档记录的网盘地址:https://ccnr8sukk85n.feishu.cn/wiki/QjY8weDYHibqRYkFP2qcA9aGnvb?from=from_copylink
软件安装时断网安装,否则会提示输入账户。
安装的时候,第一个复选框里的编译器可以全选,直接点击下一步继续安装。
选择编译器: (一定要看清楚了)
3.2 新建上位机工程
前面2讲解了需要用的API接口,接下来就使用Qt设计上位机,设计界面,完成整体上位机的逻辑设计。
【1】新建工程
【2】设置项目的名称。
【3】选择编译系统
【4】选择默认继承的类
【5】选择编译器
【6】点击完成
【7】工程创建完成
3.3 切换编译器
在左下角是可以切换编译器的。 可以选择用什么样的编译器编译程序。
目前新建工程的时候选择了2种编译器。 一种是mingw32这个编译Windows下运行的程序。 一种是Android编译器,可以生成Android手机APP。
不过要注意:Android的编译器需要配置一些环境才可以正常使用,这个大家可以网上找找教程配置一下就行了。
windows的编译器就没有这么麻烦,安装好Qt就可以编译使用。
下面我这里就选择的 mingw32这个编译器,编译Windows下运行的程序。
3.4 编译测试功能
创建完毕之后,编译测试一下功能是否OK。
点击左下角的绿色三角形按钮。
正常运行就可以看到弹出一个白色的框框。这就表示工程环境没有问题了。 接下来就可以放心的设计界面了。
3.5 设计UI界面与工程配置
【1】打开UI文件
打开默认的界面如下:
【2】开始设计界面
根据自己需求设计界面。
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>Widget</class>
<widget class="QWidget" name="Widget">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>1200</width>
<height>800</height>
</rect>
</property>
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<layout class="QHBoxLayout" name="mainLayout">
<item>
<widget class="QFrame" name="mapFrame">
<property name="minimumSize">
<size>
<width>700</width>
<height>600</height>
</size>
</property>
</widget>
</item>
<item>
<widget class="QFrame" name="controlFrame">
<layout class="QVBoxLayout" name="controlLayout">
<item>
<widget class="QGroupBox" name="connectionGroup">
<property name="title">
<string>连接设置</string>
</property>
<layout class="QHBoxLayout" name="connectionLayout">
<item>
<widget class="QPushButton" name="connectBtn">
<property name="text">
<string>连接服务器</string>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="disconnectBtn">
<property name="text">
<string>断开连接</string>
</property>
</widget>
</item>
</layout>
</widget>
</item>
<item>
<widget class="QGroupBox" name="statusGroup">
<property name="title">
<string>设备状态</string>
</property>
<layout class="QFormLayout" name="statusLayout">
<item row="0" column="0">
<widget class="QLabel" name="label">
<property name="text">
<string>连接状态:</string>
</property>
</widget>
</item>
<item row="0" column="1">
<widget class="QLabel" name="statusLabel">
<property name="text">
<string>未连接</string>
</property>
</widget>
</item>
<item row="1" column="0">
<widget class="QLabel" name="label_2">
<property name="text">
<string>设备状态:</string>
</property>
</widget>
</item>
<item row="1" column="1">
<widget class="QLabel" name="deviceStatusLabel">
<property name="text">
<string>未知</string>
</property>
</widget>
</item>
<item row="2" column="0">
<widget class="QLabel" name="label_3">
<property name="text">
<string>电池电量:</string>
</property>
</widget>
</item>
<item row="2" column="1">
<layout class="QHBoxLayout" name="batteryLayout">
<item>
<widget class="QProgressBar" name="batteryBar">
<property name="maximum">
<number>100</number>
</property>
<property name="value">
<number>0</number>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="batteryLabel">
<property name="text">
<string>0%</string>
</property>
</widget>
</item>
</layout>
</item>
<item row="3" column="0">
<widget class="QLabel" name="label_4">
<property name="text">
<string>经度:</string>
</property>
</widget>
</item>
<item row="3" column="1">
<widget class="QLabel" name="lonLabel">
<property name="text">
<string>0.000000</string>
</property>
</widget>
</item>
<item row="4" column="0">
<widget class="QLabel" name="label_5">
<property name="text">
<string>纬度:</string>
</property>
</widget>
</item>
<item row="4" column="1">
<widget class="QLabel" name="latLabel">
<property name="text">
<string>0.000000</string>
</property>
</widget>
</item>
</layout>
</widget>
</item>
<item>
<widget class="QGroupBox" name="fenceGroup">
<property name="title">
<string>围栏设置</string>
</property>
<layout class="QFormLayout" name="fenceLayout">
<item row="0" column="0">
<widget class="QLabel" name="label_6">
<property name="text">
<string>围栏类型:</string>
</property>
</widget>
</item>
<item row="0" column="1">
<widget class="QComboBox" name="fenceTypeCombo"/>
</item>
<item row="1" column="0">
<widget class="QLabel" name="rangeLabel">
<property name="text">
<string>半径(米):</string>
</property>
</widget>
</item>
<item row="1" column="1">
<widget class="QSpinBox" name="fenceRangeSpin"/>
</item>
<item row="2" column="0" colspan="2">
<widget class="QPushButton" name="setFenceBtn">
<property name="text">
<string>设置围栏</string>
</property>
</widget>
</item>
</layout>
</widget>
</item>
<item>
<widget class="QGroupBox" name="alarmGroup">
<property name="title">
<string>报警设置</string>
</property>
<layout class="QVBoxLayout" name="alarmLayout">
<item>
<widget class="QCheckBox" name="alarmSWCheck">
<property name="text">
<string>启用报警</string>
</property>
</widget>
</item>
<item>
<layout class="QHBoxLayout" name="alarmStatusLayout">
<item>
<widget class="QLabel" name="label_7">
<property name="text">
<string>报警状态:</string>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="alarmStatusLabel">
<property name="text">
<string>已禁用</string>
</property>
</widget>
</item>
</layout>
</item>
<item>
<layout class="QHBoxLayout" name="fenceStatusLayout">
<item>
<widget class="QLabel" name="label_8">
<property name="text">
<string>围栏状态:</string>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="inFenceLabel">
<property name="text">
<string>未设置</string>
</property>
</widget>
</item>
</layout>
</item>
<item>
<layout class="QHBoxLayout" name="distanceLayout">
<item>
<widget class="QLabel" name="label_9">
<property name="text">
<string>距离围栏:</string>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="distanceLabel">
<property name="text">
<string>0.0 米</string>
</property>
</widget>
</item>
</layout>
</item>
</layout>
</widget>
</item>
<item>
<widget class="QGroupBox" name="lastPositionGroup">
<property name="title">
<string>最后已知位置</string>
</property>
<layout class="QFormLayout" name="lastPositionLayout">
<item row="0" column="0">
<widget class="QLabel" name="label_10">
<property name="text">
<string>经度:</string>
</property>
</widget>
</item>
<item row="0" column="1">
<widget class="QLabel" name="lastKnownLonLabel">
<property name="text">
<string>0.000000</string>
</property>
</widget>
</item>
<item row="1" column="0">
<widget class="QLabel" name="label_11">
<property name="text">
<string>纬度:</string>
</property>
</widget>
</item>
<item row="1" column="1">
<widget class="QLabel" name="lastKnownLatLabel">
<property name="text">
<string>0.000000</string>
</property>
</widget>
</item>
</layout>
</widget>
</item>
</layout>
</widget>
</item>
</layout>
</item>
</layout>
</widget>
<resources/>
<connections/>
</ui>
3.6 设计代码
widget.h 头文件
#ifndef WIDGET_H
#define WIDGET_H
#include <QWidget>
#include <QtMqtt/QMqttClient>
#include <QTimer>
#include <QJsonDocument>
#include <QJsonObject>
#include <QJsonArray>
#include <QGeoCoordinate>
#include <QGeoPath>
#include <QGeoCircle>
#include <QPainter>
#include <QPolygonF>
#include <QTextToSpeech>
#include <QGeoPositionInfoSource>
#include <QNetworkAccessManager>
#include <QNetworkReply>
#include <QUrl>
#include <QUrlQuery>
#include <QEventLoop>
#include <QtConcurrent>
QT_BEGIN_NAMESPACE
namespace Ui { class Widget; }
QT_END_NAMESPACE
// 围栏类型枚举
enum FenceType {
CIRCLE_FENCE = 0,
RECTANGLE_FENCE = 1
};
// 设备状态结构体
struct DeviceInfo {
int state; // 0:静止 1:运动
float electricity; // 电池电量
double lon; // 经度
double lat; // 纬度
int alarmSW; // 报警开关
double fenceLon; // 围栏中心经度
double fenceLat; // 围栏中心纬度
int fenceRadius; // 围栏半径/半边长(米)
int fenceType; // 围栏类型
bool isInFence; // 是否在围栏内
float lastKnownLon; // 最后已知经度
float lastKnownLat; // 最后已知纬度
bool hasAlarmed; // 是否已报警
};
// 百度地图坐标转换结果
struct CoordinateConvertResult {
double lng;
double lat;
bool success;
};
class Widget : public QWidget
{
Q_OBJECT
public:
Widget(QWidget *parent = nullptr);
~Widget();
private slots:
// MQTT连接相关
void on_connectBtn_clicked();
void on_disconnectBtn_clicked();
void on_mqttStateChanged(QMqttClient::ClientState state);
void on_mqttMessageReceived(const QByteArray &message, const QMqttTopicName &topic);
// 围栏设置相关
void on_setFenceBtn_clicked();
void on_fenceTypeCombo_currentIndexChanged(int index);
void on_alarmSWCheck_stateChanged(int state);
// 更新显示
void updateDeviceInfo(const QJsonObject &properties);
void updateMapAndFence();
// 定时器
void onUpdateTimerTimeout();
// 位置更新
void onPositionUpdated(const QGeoPositionInfo &info);
// 地图相关
void loadMap();
void onMapLoaded();
void convertCoordinates(double lon, double lat);
private:
Ui::Widget *ui;
// MQTT客户端
QMqttClient *mqttClient;
// 设备信息
DeviceInfo deviceInfo;
// 定时器
QTimer *updateTimer;
// 地理坐标
QGeoCoordinate currentPosition;
QGeoCoordinate fenceCenter;
// 围栏路径
QGeoPath fencePath;
QGeoCircle fenceCircle;
// 百度地图相关
QNetworkAccessManager *networkManager;
QString mapHtmlContent;
QWebEngineView *mapView;
// 语音播报
QTextToSpeech *textToSpeech;
// 标志位
bool isConnected;
bool isAlarmEnabled;
bool hasAlarmTriggered;
bool isFirstUpdate;
// 私有方法
void initMQTT();
void initUI();
void initMap();
void updateDisplay();
double calculateDistance(double lat1, double lon1, double lat2, double lon2);
bool checkIfInFence(double lat, double lon);
void triggerAlarm();
void stopAlarm();
void playVoiceAlert();
void updateBatteryDisplay(float battery);
void updateStatusDisplay(int state);
void updatePositionDisplay(double lon, double lat);
void updateFenceOnMap();
// 百度坐标转换
CoordinateConvertResult convertToBaidu(double lon, double lat);
void drawFenceOnMap(const QJsonObject &fenceInfo);
// 数据处理
void parseGPSData(const QJsonValue &gpsValue);
void parseFenceData(const QJsonValue &fenceValue);
QString getStateString(int state);
QString getFenceTypeString(int type);
};
#endif // WIDGET_H
widget.cpp 主要实现
#include "widget.h"
#include "ui_widget.h"
#include <QJsonParseError>
#include <QMessageBox>
#include <QDebug>
#include <QThread>
#include <QDesktopServices>
#include <QWebEngineView>
#include <QWebEnginePage>
#include <QWebEngineProfile>
#include <QWebEngineSettings>
#include <cmath>
#include <QThreadPool>
#include <QtConcurrent/QtConcurrent>
Widget::Widget(QWidget *parent)
: QWidget(parent)
, ui(new Ui::Widget)
, mqttClient(nullptr)
, updateTimer(nullptr)
, networkManager(nullptr)
, mapView(nullptr)
, textToSpeech(nullptr)
, isConnected(false)
, isAlarmEnabled(false)
, hasAlarmTriggered(false)
, isFirstUpdate(true)
{
ui->setupUi(this);
// 初始化设备信息
memset(&deviceInfo, 0, sizeof(DeviceInfo));
deviceInfo.fenceRadius = 100; // 默认100米
deviceInfo.fenceType = CIRCLE_FENCE;
deviceInfo.isInFence = true;
deviceInfo.hasAlarmed = false;
// 初始化UI
initUI();
// 初始化地图
initMap();
// 初始化MQTT
initMQTT();
// 初始化定时器(每2秒更新一次)
updateTimer = new QTimer(this);
connect(updateTimer, &QTimer::timeout, this, &Widget::onUpdateTimerTimeout);
updateTimer->start(2000);
// 初始化语音播报
textToSpeech = new QTextToSpeech(this);
}
Widget::~Widget()
{
if (mqttClient) {
mqttClient->disconnectFromHost();
delete mqttClient;
}
delete ui;
}
void Widget::initUI()
{
// 设置窗口标题和大小
this->setWindowTitle("电子围栏系统 - 实时监控");
this->resize(1200, 800);
// 初始化围栏类型下拉框
ui->fenceTypeCombo->addItem("圆形围栏", CIRCLE_FENCE);
ui->fenceTypeCombo->addItem("矩形围栏", RECTANGLE_FENCE);
// 设置默认值
ui->fenceRangeSpin->setValue(100);
ui->fenceRangeSpin->setRange(10, 10000);
ui->alarmSWCheck->setChecked(false);
// 连接信号槽
connect(ui->connectBtn, &QPushButton::clicked, this, &Widget::on_connectBtn_clicked);
connect(ui->disconnectBtn, &QPushButton::clicked, this, &Widget::on_disconnectBtn_clicked);
connect(ui->setFenceBtn, &QPushButton::clicked, this, &Widget::on_setFenceBtn_clicked);
connect(ui->fenceTypeCombo, QOverload<int>::of(&QComboBox::currentIndexChanged),
this, &Widget::on_fenceTypeCombo_currentIndexChanged);
connect(ui->alarmSWCheck, &QCheckBox::stateChanged,
this, &Widget::on_alarmSWCheck_stateChanged);
// 设置状态显示样式
ui->statusLabel->setStyleSheet("QLabel { color: gray; }");
ui->deviceStatusLabel->setStyleSheet("QLabel { color: gray; }");
}
void Widget::initMQTT()
{
mqttClient = new QMqttClient(this);
// 设置MQTT参数
mqttClient->setHostname("5a00dd0860.st1.iotda-device.cn-north-4.myhuaweicloud.com");
mqttClient->setPort(1883);
mqttClient->setClientId("69da739fcbb0cf6bb95011f8_dev1_0_0_2026041116");
mqttClient->setUsername("69da739fcbb0cf6bb95011f8_dev1");
mqttClient->setPassword("80133902f623c8a6479567170d93fd8c4df09f3865b3bc1c858157da4a0a1eaf");
// 连接状态变化
connect(mqttClient, &QMqttClient::stateChanged,
this, &Widget::on_mqttStateChanged);
// 消息接收
connect(mqttClient, &QMqttClient::messageReceived,
this, &Widget::on_mqttMessageReceived);
// 连接错误
connect(mqttClient, &QMqttClient::errorChanged, [this](QMqttClient::ClientError error) {
qDebug() << "MQTT Error:" << error;
ui->statusLabel->setText("MQTT连接错误");
ui->statusLabel->setStyleSheet("QLabel { color: red; }");
});
}
void Widget::initMap()
{
// 创建WebEngineView用于显示百度地图
mapView = new QWebEngineView(ui->mapFrame);
mapView->setGeometry(0, 0, ui->mapFrame->width(), ui->mapFrame->height());
mapView->settings()->setAttribute(QWebEngineSettings::JavascriptEnabled, true);
mapView->settings()->setAttribute(QWebEngineSettings::JavascriptCanOpenWindows, true);
mapView->settings()->setAttribute(QWebEngineSettings::LocalStorageEnabled, true);
mapView->settings()->setAttribute(QWebEngineSettings::PluginsEnabled, true);
// 加载百度地图
loadMap();
// 网络管理器用于坐标转换
networkManager = new QNetworkAccessManager(this);
}
void Widget::loadMap()
{
// 百度地图HTML内容
mapHtmlContent = R"(
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>电子围栏地图</title>
<style type="text/css">
html, body { margin: 0; padding: 0; height: 100%; }
#container { width: 100%; height: 100%; }
</style>
<script type="text/javascript" src="https://api.map.baidu.com/api?v=3.0&ak=YOUR_BAIDU_API_KEY"></script>
</head>
<body>
<div id="container"></div>
<script type="text/javascript">
var map = null;
var marker = null;
var circle = null;
var polygon = null;
var infoWindow = null;
function initMap(lon, lat) {
if (map == null) {
map = new BMap.Map("container");
map.centerAndZoom(new BMap.Point(lon, lat), 16);
map.enableScrollWheelZoom(true);
map.addControl(new BMap.NavigationControl());
map.addControl(new BMap.ScaleControl());
map.addControl(new BMap.OverviewMapControl());
map.addControl(new BMap.MapTypeControl());
}
// 添加标记
if (marker != null) {
map.removeOverlay(marker);
}
var point = new BMap.Point(lon, lat);
marker = new BMap.Marker(point);
map.addOverlay(marker);
map.centerAndZoom(point, 16);
}
function drawCircle(lon, lat, radius) {
if (circle != null) {
map.removeOverlay(circle);
}
var point = new BMap.Point(lon, lat);
circle = new BMap.Circle(point, radius, {
strokeColor: "#FF0000",
strokeWeight: 3,
strokeOpacity: 0.8,
fillColor: "#FF0000",
fillOpacity: 0.2
});
map.addOverlay(circle);
}
function drawPolygon(lon, lat, halfLength) {
if (polygon != null) {
map.removeOverlay(polygon);
}
var center = new BMap.Point(lon, lat);
var points = [
new BMap.Point(lon - halfLength/111000, lat + halfLength/111000),
new BMap.Point(lon + halfLength/111000, lat + halfLength/111000),
new BMap.Point(lon + halfLength/111000, lat - halfLength/111000),
new BMap.Point(lon - halfLength/111000, lat - halfLength/111000)
];
polygon = new BMap.Polygon(points, {
strokeColor: "#FF0000",
strokeWeight: 3,
strokeOpacity: 0.8,
fillColor: "#FF0000",
fillOpacity: 0.2
});
map.addOverlay(polygon);
}
function clearOverlays() {
if (circle != null) {
map.removeOverlay(circle);
circle = null;
}
if (polygon != null) {
map.removeOverlay(polygon);
polygon = null;
}
}
function updateInfo(title, content) {
if (infoWindow != null) {
map.removeOverlay(infoWindow);
}
infoWindow = new BMap.InfoWindow(content, {
title: title,
width: 200,
height: 80
});
if (marker != null) {
marker.openInfoWindow(infoWindow);
}
}
</script>
</body>
</html>
)";
mapView->setHtml(mapHtmlContent);
}
void Widget::on_connectBtn_clicked()
{
if (!mqttClient) {
QMessageBox::warning(this, "警告", "MQTT客户端未初始化");
return;
}
ui->connectBtn->setEnabled(false);
ui->connectBtn->setText("连接中...");
// 设置Will消息
mqttClient->setWillTopic("$oc/devices/69da739fcbb0cf6bb95011f8_dev1/sys/messages/down");
mqttClient->setWillMessage("{\"state\":\"offline\"}");
// 连接到MQTT服务器
mqttClient->connectToHost();
// 发送连接成功消息
qDebug() << "正在连接到MQTT服务器...";
}
void Widget::on_disconnectBtn_clicked()
{
if (mqttClient && mqttClient->state() == QMqttClient::Connected) {
mqttClient->disconnectFromHost();
isConnected = false;
ui->connectBtn->setEnabled(true);
ui->connectBtn->setText("连接服务器");
ui->statusLabel->setText("已断开连接");
ui->statusLabel->setStyleSheet("QLabel { color: gray; }");
}
}
void Widget::on_mqttStateChanged(QMqttClient::ClientState state)
{
switch (state) {
case QMqttClient::Disconnected:
isConnected = false;
ui->connectBtn->setEnabled(true);
ui->connectBtn->setText("连接服务器");
ui->statusLabel->setText("已断开连接");
ui->statusLabel->setStyleSheet("QLabel { color: gray; }");
qDebug() << "MQTT已断开";
break;
case QMqttClient::Connecting:
ui->connectBtn->setEnabled(false);
ui->connectBtn->setText("连接中...");
ui->statusLabel->setText("正在连接...");
ui->statusLabel->setStyleSheet("QLabel { color: orange; }");
qDebug() << "MQTT连接中...";
break;
case QMqttClient::Connected:
isConnected = true;
ui->connectBtn->setEnabled(true);
ui->connectBtn->setText("已连接");
ui->statusLabel->setText("已连接");
ui->statusLabel->setStyleSheet("QLabel { color: green; }");
qDebug() << "MQTT已连接";
// 订阅主题
auto subscription = mqttClient->subscribe(
"$oc/devices/69da739fcbb0cf6bb95011f8_dev1/sys/messages/down",
qMqttQos0);
if (subscription) {
qDebug() << "订阅成功";
} else {
qDebug() << "订阅失败";
}
break;
}
}
void Widget::on_mqttMessageReceived(const QByteArray &message, const QMqttTopicName &topic)
{
qDebug() << "收到消息:" << message;
qDebug() << "主题:" << topic.name();
// 解析JSON消息
QJsonParseError error;
QJsonDocument doc = QJsonDocument::fromJson(message, &error);
if (error.error != QJsonParseError::NoError) {
qDebug() << "JSON解析错误:" << error.errorString();
return;
}
// 处理设备上报的属性
QJsonObject root = doc.object();
if (root.contains("services")) {
QJsonArray services = root["services"].toArray();
for (const QJsonValue &serviceValue : services) {
QJsonObject service = serviceValue.toObject();
if (service["service_id"].toString() == "stm32") {
QJsonObject properties = service["properties"].toObject();
updateDeviceInfo(properties);
}
}
}
}
void Widget::updateDeviceInfo(const QJsonObject &properties)
{
// 更新设备状态
if (properties.contains("state")) {
deviceInfo.state = properties["state"].toInt();
updateStatusDisplay(deviceInfo.state);
}
// 更新电量
if (properties.contains("Electricity")) {
deviceInfo.electricity = properties["Electricity"].toDouble();
updateBatteryDisplay(deviceInfo.electricity);
}
// 更新GPS信息
if (properties.contains("GPS")) {
parseGPSData(properties["GPS"]);
}
// 更新围栏信息
if (properties.contains("GPS_fence")) {
parseFenceData(properties["GPS_fence"]);
}
// 更新报警开关
if (properties.contains("AlarmSW")) {
deviceInfo.alarmSW = properties["AlarmSW"].toInt();
isAlarmEnabled = (deviceInfo.alarmSW == 1);
ui->alarmSWCheck->setChecked(isAlarmEnabled);
}
// 更新显示
updateDisplay();
// 检查围栏报警
if (isAlarmEnabled && deviceInfo.isInFence == false) {
triggerAlarm();
} else if (isAlarmEnabled && deviceInfo.isInFence == true) {
if (hasAlarmTriggered) {
stopAlarm();
playVoiceAlert();
}
}
}
void Widget::parseGPSData(const QJsonValue &gpsValue)
{
if (gpsValue.isObject()) {
QJsonObject gps = gpsValue.toObject();
if (gps.contains("lon") && gps.contains("lat")) {
deviceInfo.lon = gps["lon"].toDouble();
deviceInfo.lat = gps["lat"].toDouble();
updatePositionDisplay(deviceInfo.lon, deviceInfo.lat);
// 更新地图位置
currentPosition.setLatitude(deviceInfo.lat);
currentPosition.setLongitude(deviceInfo.lon);
// 转换坐标并更新地图
convertCoordinates(deviceInfo.lon, deviceInfo.lat);
// 检查是否在围栏内
if (deviceInfo.fenceLon != 0 && deviceInfo.fenceLat != 0) {
deviceInfo.isInFence = checkIfInFence(deviceInfo.lat, deviceInfo.lon);
ui->inFenceLabel->setText(deviceInfo.isInFence ? "在围栏内" : "在围栏外");
ui->inFenceLabel->setStyleSheet(
deviceInfo.isInFence ? "QLabel { color: green; }" : "QLabel { color: red; font-weight: bold; }"
);
}
}
}
}
void Widget::parseFenceData(const QJsonValue &fenceValue)
{
if (fenceValue.isObject()) {
QJsonObject fence = fenceValue.toObject();
if (fence.contains("lon") && fence.contains("lat")) {
deviceInfo.fenceLon = fence["lon"].toDouble();
deviceInfo.fenceLat = fence["lat"].toDouble();
fenceCenter.setLatitude(deviceInfo.fenceLat);
fenceCenter.setLongitude(deviceInfo.fenceLon);
// 绘制围栏
updateFenceOnMap();
}
}
}
double Widget::calculateDistance(double lat1, double lon1, double lat2, double lon2)
{
// 使用Haversine公式计算两点间距离
const double R = 6371000.0; // 地球半径(米)
double dLat = (lat2 - lat1) * M_PI / 180.0;
double dLon = (lon2 - lon1) * M_PI / 180.0;
double a = sin(dLat/2) * sin(dLat/2) +
cos(lat1 * M_PI / 180.0) * cos(lat2 * M_PI / 180.0) *
sin(dLon/2) * sin(dLon/2);
double c = 2 * atan2(sqrt(a), sqrt(1-a));
return R * c;
}
bool Widget::checkIfInFence(double lat, double lon)
{
if (deviceInfo.fenceLon == 0 || deviceInfo.fenceLat == 0) {
return true; // 未设置围栏时认为在围栏内
}
double distance = calculateDistance(lat, lon, deviceInfo.fenceLat, deviceInfo.fenceLon);
if (deviceInfo.fenceType == CIRCLE_FENCE) {
return distance <= deviceInfo.fenceRadius;
} else { // RECTANGLE_FENCE
// 矩形围栏:计算经纬度偏移
double halfSide = deviceInfo.fenceRadius / 111000.0; // 转换为度
double minLat = deviceInfo.fenceLat - halfSide;
double maxLat = deviceInfo.fenceLat + halfSide;
double minLon = deviceInfo.fenceLon - halfSide / cos(deviceInfo.fenceLat * M_PI / 180.0);
double maxLon = deviceInfo.fenceLon + halfSide / cos(deviceInfo.fenceLat * M_PI / 180.0);
return (lat >= minLat && lat <= maxLat && lon >= minLon && lon <= maxLon);
}
}
void Widget::triggerAlarm()
{
if (!hasAlarmTriggered) {
hasAlarmTriggered = true;
ui->alarmStatusLabel->setText("⚠ 报警触发!");
ui->alarmStatusLabel->setStyleSheet("QLabel { color: red; font-weight: bold; }");
// 播放语音提示
playVoiceAlert();
// 显示报警信息
QMessageBox::warning(this, "报警", "目标已超出电子围栏范围!");
}
}
void Widget::stopAlarm()
{
hasAlarmTriggered = false;
ui->alarmStatusLabel->setText("正常");
ui->alarmStatusLabel->setStyleSheet("QLabel { color: green; }");
}
void Widget::playVoiceAlert()
{
if (textToSpeech) {
QString alertText = "警告!目标超出电子围栏范围,请立即检查!";
if (deviceInfo.state == 1) {
alertText = "警告!目标正在移动并已超出电子围栏范围!";
}
textToSpeech->say(alertText);
}
}
void Widget::updateDisplay()
{
// 计算与围栏中心的距离
if (deviceInfo.fenceLon != 0 && deviceInfo.fenceLat != 0) {
double distance = calculateDistance(deviceInfo.lat, deviceInfo.lon,
deviceInfo.fenceLat, deviceInfo.fenceLon);
ui->distanceLabel->setText(QString::number(distance, 'f', 1) + " 米");
ui->distanceLabel->setStyleSheet("QLabel { color: blue; font-weight: bold; }");
} else {
ui->distanceLabel->setText("未设置围栏");
ui->distanceLabel->setStyleSheet("QLabel { color: gray; }");
}
// 更新最后已知位置
if (deviceInfo.lon != 0 && deviceInfo.lat != 0) {
ui->lastPositionLabel->setText(
QString("经度: %1, 纬度: %2")
.arg(deviceInfo.lon, 0, 'f', 6)
.arg(deviceInfo.lat, 0, 'f', 6)
);
}
}
void Widget::updateStatusDisplay(int state)
{
QString status = getStateString(state);
ui->deviceStatusLabel->setText(status);
ui->deviceStatusLabel->setStyleSheet(
state == 1 ? "QLabel { color: green; }" : "QLabel { color: blue; }"
);
}
void Widget::updateBatteryDisplay(float battery)
{
ui->batteryBar->setValue((int)battery);
ui->batteryLabel->setText(QString::number(battery, 'f', 1) + "%");
// 根据电量改变颜色
QString color;
if (battery > 50) {
color = "green";
} else if (battery > 20) {
color = "orange";
} else {
color = "red";
}
ui->batteryLabel->setStyleSheet("QLabel { color: " + color + "; font-weight: bold; }");
}
void Widget::updatePositionDisplay(double lon, double lat)
{
ui->lonLabel->setText(QString::number(lon, 'f', 6));
ui->latLabel->setText(QString::number(lat, 'f', 6));
}
QString Widget::getStateString(int state)
{
switch (state) {
case 0: return "静止";
case 1: return "运动";
default: return "未知";
}
}
void Widget::getFenceTypeString(int type)
{
switch (type) {
case CIRCLE_FENCE: return "圆形";
case RECTANGLE_FENCE: return "矩形";
default: return "未知";
}
}
void Widget::convertCoordinates(double lon, double lat)
{
// 百度坐标系转换
QUrl url("https://api.map.baidu.com/geoconv/v1/");
QUrlQuery query;
query.addQueryItem("coords", QString::number(lon, 'f', 6) + "," + QString::number(lat, 'f', 6));
query.addQueryItem("from", "1"); // GPS坐标系
query.addQueryItem("to", "5"); // 百度坐标系
query.addQueryItem("ak", "YOUR_BAIDU_API_KEY");
url.setQuery(query);
QNetworkRequest request(url);
QNetworkReply *reply = networkManager->get(request);
connect(reply, &QNetworkReply::finished, [this, reply]() {
if (reply->error() == QNetworkReply::NoError) {
QByteArray data = reply->readAll();
QJsonDocument doc = QJsonDocument::fromJson(data);
QJsonObject obj = doc.object();
if (obj["status"].toInt() == 0) {
QJsonArray result = obj["result"].toArray();
if (!result.isEmpty()) {
QJsonObject coord = result[0].toObject();
double lon = coord["x"].toDouble();
double lat = coord["y"].toDouble();
// 更新地图显示
QString jsCode = QString("initMap(%1, %2);")
.arg(lon, 0, 'f', 6)
.arg(lat, 0, 'f', 6);
// 在主线程中执行JavaScript
QMetaObject::invokeMethod(mapView, [=]() {
mapView->page()->runJavaScript(jsCode);
});
}
}
}
reply->deleteLater();
});
}
void Widget::updateFenceOnMap()
{
if (deviceInfo.fenceLon == 0 || deviceInfo.fenceLat == 0) {
return;
}
// 转换围栏中心坐标
convertCoordinates(deviceInfo.fenceLon, deviceInfo.fenceLat);
// 绘制围栏
if (deviceInfo.fenceType == CIRCLE_FENCE) {
QString jsCode = QString("drawCircle(%1, %2, %3);")
.arg(deviceInfo.fenceLon, 0, 'f', 6)
.arg(deviceInfo.fenceLat, 0, 'f', 6)
.arg(deviceInfo.fenceRadius);
mapView->page()->runJavaScript(jsCode);
} else {
QString jsCode = QString("drawPolygon(%1, %2, %3);")
.arg(deviceInfo.fenceLon, 0, 'f', 6)
.arg(deviceInfo.fenceLat, 0, 'f', 6)
.arg(deviceInfo.fenceRadius);
mapView->page()->runJavaScript(jsCode);
}
}
void Widget::on_setFenceBtn_clicked()
{
// 使用当前设备位置作为围栏中心
if (deviceInfo.lon == 0 || deviceInfo.lat == 0) {
QMessageBox::warning(this, "警告", "当前没有有效的GPS位置数据");
return;
}
deviceInfo.fenceLon = deviceInfo.lon;
deviceInfo.fenceLat = deviceInfo.lat;
deviceInfo.fenceRadius = ui->fenceRangeSpin->value();
deviceInfo.fenceType = ui->fenceTypeCombo->currentData().toInt();
fenceCenter.setLatitude(deviceInfo.fenceLat);
fenceCenter.setLongitude(deviceInfo.fenceLon);
// 更新地图围栏
updateFenceOnMap();
// 检查当前位置是否在围栏内
deviceInfo.isInFence = checkIfInFence(deviceInfo.lat, deviceInfo.lon);
ui->inFenceLabel->setText(deviceInfo.isInFence ? "在围栏内" : "在围栏外");
ui->inFenceLabel->setStyleSheet(
deviceInfo.isInFence ? "QLabel { color: green; }" : "QLabel { color: red; font-weight: bold; }"
);
// 计算并显示距离
double distance = calculateDistance(deviceInfo.lat, deviceInfo.lon,
deviceInfo.fenceLat, deviceInfo.fenceLon);
ui->distanceLabel->setText(QString::number(distance, 'f', 1) + " 米");
ui->distanceLabel->setStyleSheet("QLabel { color: blue; font-weight: bold; }");
// 通过MQTT发布围栏设置
if (isConnected && mqttClient) {
QJsonObject properties;
properties["GPS_fence"] = QJsonObject{
{"lon", deviceInfo.fenceLon},
{"lat", deviceInfo.fenceLat}
};
QJsonObject payload;
QJsonArray services;
QJsonObject service;
service["service_id"] = "stm32";
service["properties"] = properties;
services.append(service);
payload["services"] = services;
QJsonDocument doc(payload);
QString message = doc.toJson(QJsonDocument::Compact);
mqttClient->publish(
"$oc/devices/69da739fcbb0cf6bb95011f8_dev1/sys/properties/report",
message.toUtf8(),
qMqttQos1
);
QMessageBox::information(this, "成功", "围栏设置已更新并上传到云端");
}
}
void Widget::on_fenceTypeCombo_currentIndexChanged(int index)
{
Q_UNUSED(index);
// 更新围栏范围标签
if (ui->fenceTypeCombo->currentData().toInt() == CIRCLE_FENCE) {
ui->rangeLabel->setText("半径(米)");
} else {
ui->rangeLabel->setText("半边长(米)");
}
}
void Widget::on_alarmSWCheck_stateChanged(int state)
{
isAlarmEnabled = (state == Qt::Checked);
deviceInfo.alarmSW = isAlarmEnabled ? 1 : 0;
// 通过MQTT发布报警开关状态
if (isConnected && mqttClient) {
QJsonObject properties;
properties["AlarmSW"] = deviceInfo.alarmSW;
QJsonObject payload;
QJsonArray services;
QJsonObject service;
service["service_id"] = "stm32";
service["properties"] = properties;
services.append(service);
payload["services"] = services;
QJsonDocument doc(payload);
QString message = doc.toJson(QJsonDocument::Compact);
mqttClient->publish(
"$oc/devices/69da739fcbb0cf6bb95011f8_dev1/sys/properties/report",
message.toUtf8(),
qMqttQos1
);
}
if (isAlarmEnabled) {
ui->alarmStatusLabel->setText("已启用");
ui->alarmStatusLabel->setStyleSheet("QLabel { color: green; }");
QMessageBox::information(this, "提示", "电子围栏报警已启用");
} else {
ui->alarmStatusLabel->setText("已禁用");
ui->alarmStatusLabel->setStyleSheet("QLabel { color: gray; }");
stopAlarm();
QMessageBox::information(this, "提示", "电子围栏报警已禁用");
}
}
void Widget::onUpdateTimerTimeout()
{
// 定时更新显示
if (isConnected) {
ui->statusLabel->setText("已连接 - 接收数据中");
ui->statusLabel->setStyleSheet("QLabel { color: green; }");
}
// 更新最后已知位置显示
if (deviceInfo.lon != 0 && deviceInfo.lat != 0) {
ui->lastKnownLonLabel->setText(QString::number(deviceInfo.lon, 'f', 6));
ui->lastKnownLatLabel->setText(QString::number(deviceInfo.lat, 'f', 6));
}
// 检查并更新围栏状态
if (deviceInfo.fenceLon != 0 && deviceInfo.fenceLat != 0 && deviceInfo.lon != 0 && deviceInfo.lat != 0) {
bool inFence = checkIfInFence(deviceInfo.lat, deviceInfo.lon);
if (inFence != deviceInfo.isInFence) {
deviceInfo.isInFence = inFence;
ui->inFenceLabel->setText(inFence ? "在围栏内" : "在围栏外");
ui->inFenceLabel->setStyleSheet(
inFence ? "QLabel { color: green; }" : "QLabel { color: red; font-weight: bold; }"
);
// 触发报警
if (!inFence && isAlarmEnabled) {
triggerAlarm();
} else if (inFence && hasAlarmTriggered) {
stopAlarm();
playVoiceAlert();
}
}
}
}
void Widget::onPositionUpdated(const QGeoPositionInfo &info)
{
// 位置更新处理(预留接口)
}
void Widget::updateMapAndFence()
{
// 更新地图显示(预留接口)
}
四、STM32代码设计
4.1 硬件连线说明
【1】OLED显示屏
VCC<--------->3.3V
GND<--------->GND
SCL<--------->PB0
SDA<--------->PB1
【2】GPS模块
GND---->GND
VCC---->5V
PB11--->GPS_TX
【3】Air780E-4G模块
PA2<--------->RXD 模块接收脚
PA3<--------->TXD 模块发送脚
GND<--------->GND 地
VCC<--------->3.3V
【4】LED灯接线
LED1---PC13 //程序运行指示灯
LED2---PC14 //数据上传指示灯
LED3---PC15 //报警指示灯
【5】蜂鸣器
VCC<--------->5V
GND<--------->GND
IO<--------->PB5
【6】电池电量采集
BAT+-------->PA1
4.2 项目完整代码设计
#include "stm32f10x.h"
#include <string.h>
#include <stdio.h>
#include <stdarg.h>
// ==================== 硬件配置宏定义 ====================
// OLED引脚定义
#define OLED_SCL_PIN GPIO_Pin_0
#define OLED_SCL_PORT GPIOB
#define OLED_SDA_PIN GPIO_Pin_1
#define OLED_SDA_PORT GPIOB
// GPS模块引脚定义 (串口3)
#define GPS_USART USART3
#define GPS_BAUDRATE 9600
// 4G模块引脚定义 (串口2)
#define LTE_USART USART2
#define LTE_BAUDRATE 115200
// ADXL345引脚定义 (I2C1)
#define ADXL345_SDA_PIN GPIO_Pin_6
#define ADXL345_SCL_PIN GPIO_Pin_7
#define ADXL345_PORT GPIOA
// LED引脚定义
#define LED1_PIN GPIO_Pin_13 // PC13 - 程序运行指示灯
#define LED2_PIN GPIO_Pin_14 // PC14 - 数据上传指示灯
#define LED3_PIN GPIO_Pin_15 // PC15 - 报警指示灯
#define LED_PORT GPIOC
// 蜂鸣器引脚定义
#define BEEP_PIN GPIO_Pin_5
#define BEEP_PORT GPIOB
// 电池电量采集引脚
#define ADC_CHANNEL ADC_Channel_1 // PA1
#define ADC_PORT GPIOA
#define ADC_PIN GPIO_Pin_1
// ==================== MQTT配置 ====================
#define MQTT_CLIENT_ID "69da739fcbb0cf6bb95011f8_dev1_0_0_2026041116"
#define MQTT_USERNAME "69da739fcbb0cf6bb95011f8_dev1"
#define MQTT_PASSWORD "80133902f623c8a6479567170d93fd8c4df09f3865b3bc1c858157da4a0a1eaf"
#define MQTT_SERVER "5a00dd0860.st1.iotda-device.cn-north-4.myhuaweicloud.com"
#define MQTT_PORT 1883
#define SUB_TOPIC "$oc/devices/69da739fcbb0cf6bb95011f8_dev1/sys/messages/down"
#define PUB_TOPIC "$oc/devices/69da739fcbb0cf6bb95011f8_dev1/sys/properties/report"
// ==================== 数据结构 ====================
typedef struct {
float electricity; // 电池电量百分比
int state; // 0:静止 1:运动
double lon; // 经度
double lat; // 纬度
int alarmSW; // 报警开关 0:关闭 1:开启
double fenceLon; // 围栏中心经度
double fenceLat; // 围栏中心纬度
int fenceRadius; // 围栏半径(米)
int fenceType; // 0:圆形 1:矩形
uint8_t gpsFixed; // GPS定位状态 0:未定位 1:已定位
} DeviceInfo_t;
// GPS数据结构
typedef struct {
char time[10]; // UTC时间
double latitude; // 纬度
double longitude; // 经度
float speed; // 速度 km/h
char status; // 定位状态 A:有效 V:无效
uint8_t fixed; // 定位标志
} GPS_Data_t;
// ==================== 全局变量 ====================
DeviceInfo_t g_deviceInfo = {
.electricity = 0,
.state = 0,
.lon = 0,
.lat = 0,
.alarmSW = 0,
.fenceLon = 0,
.fenceLat = 0,
.fenceRadius = 100,
.fenceType = 0,
.gpsFixed = 0
};
GPS_Data_t g_gpsData = {0};
uint8_t g_rxBuffer[512];
uint16_t g_rxIndex = 0;
uint8_t g_mqttConnected = 0;
uint32_t g_sysTick = 0;
uint32_t g_lastUploadTime = 0;
uint32_t g_lastGPSTime = 0;
uint32_t g_ledToggle = 0;
// 串口接收缓冲区
char g_usart2_rx_buf[256];
uint16_t g_usart2_rx_len = 0;
char g_usart3_rx_buf[512];
uint16_t g_usart3_rx_len = 0;
// ==================== 函数声明 ====================
void SystemClock_Config(void);
void GPIO_Init(void);
void USART_Init(void);
void ADC_Init(void);
void I2C_Init(void);
void OLED_Init(void);
void ADXL345_Init(void);
void TIM_Init(void);
void NVIC_Init(void);
void USART_SendChar(USART_TypeDef* USARTx, char ch);
void USART_SendString(USART_TypeDef* USARTx, char* str);
void USART_SendData(USART_TypeDef* USARTx, uint8_t* data, uint16_t len);
void OLED_WriteCmd(uint8_t cmd);
void OLED_WriteData(uint8_t data);
void OLED_Clear(void);
void OLED_ShowChar(uint8_t x, uint8_t y, char ch);
void OLED_ShowString(uint8_t x, uint8_t y, char* str);
void OLED_ShowNum(uint8_t x, uint8_t y, int num, uint8_t len);
void OLED_ShowFloat(uint8_t x, uint8_t y, float num, uint8_t decimal);
void ADXL345_Read(int16_t* x, int16_t* y, int16_t* z);
uint8_t ADXL345_ReadReg(uint8_t reg);
void ADXL345_WriteReg(uint8_t reg, uint8_t value);
float GetBatteryVoltage(void);
float CalculateBatteryPercentage(float voltage);
void ParseGPSData(char* data);
void ProcessGPSData(void);
void UpdateDeviceState(void);
void MQTT_Connect(void);
void MQTT_PublishData(void);
void MQTT_ProcessMessage(char* message);
void LTE_SendAT(const char* cmd);
uint8_t LTE_WaitResponse(const char* expected, uint32_t timeout);
void CheckFenceAlarm(void);
double CalculateDistance(double lat1, double lon1, double lat2, double lon2);
void Delay_us(uint32_t us);
void Delay_ms(uint32_t ms);
// ==================== 系统时钟配置 ====================
void SystemClock_Config(void)
{
// 启用HSE
RCC->CR |= RCC_CR_HSEON;
while(!(RCC->CR & RCC_CR_HSERDY));
// 配置PLL: HSE 8MHz * 9 = 72MHz
RCC->CFGR |= RCC_CFGR_PLLSRC_HSE | RCC_CFGR_PLLMULL9;
// 启用PLL
RCC->CR |= RCC_CR_PLLON;
while(!(RCC->CR & RCC_CR_PLLRDY));
// 设置PLL为系统时钟
RCC->CFGR |= RCC_CFGR_SW_PLL;
while((RCC->CFGR & RCC_CFGR_SWS) != RCC_CFGR_SWS_PLL);
// 配置AHB/APB时钟
RCC->CFGR |= RCC_CFGR_HPRE_DIV1; // AHB = SYSCLK
RCC->CFGR |= RCC_CFGR_PPRE1_DIV2; // APB1 = SYSCLK/2 = 36MHz
RCC->CFGR |= RCC_CFGR_PPRE2_DIV1; // APB2 = SYSCLK = 72MHz
// 配置Flash等待周期
FLASH->ACR = FLASH_ACR_LATENCY_2 | FLASH_ACR_PRFTBE;
}
// ==================== GPIO初始化 ====================
void GPIO_Init(void)
{
// 使能GPIO时钟
RCC->APB2ENR |= RCC_APB2ENR_IOPAEN | RCC_APB2ENR_IOPBEN | RCC_APB2ENR_IOPCEN;
RCC->APB2ENR |= RCC_APB2ENR_AFIOEN;
// LED引脚配置 (PC13, PC14, PC15) - 推挽输出
GPIOC->CRH &= ~(GPIO_CRH_CNF13 | GPIO_CRH_MODE13);
GPIOC->CRH |= GPIO_CRH_MODE13_0; // 输出模式,最大速度10MHz
GPIOC->CRH &= ~(GPIO_CRH_CNF14 | GPIO_CRH_MODE14);
GPIOC->CRH |= GPIO_CRH_MODE14_0;
GPIOC->CRH &= ~(GPIO_CRH_CNF15 | GPIO_CRH_MODE15);
GPIOC->CRH |= GPIO_CRH_MODE15_0;
// 蜂鸣器引脚配置 (PB5) - 推挽输出
GPIOB->CRL &= ~(GPIO_CRL_CNF5 | GPIO_CRL_MODE5);
GPIOB->CRL |= GPIO_CRL_MODE5_0;
// OLED SCL (PB0) 和 SDA (PB1) - 复用推挽输出 (I2C)
GPIOB->CRL &= ~(GPIO_CRL_CNF0 | GPIO_CRL_MODE0);
GPIOB->CRL |= GPIO_CRL_CNF0_1 | GPIO_CRL_MODE0_0; // 复用推挽,10MHz
GPIOB->CRL &= ~(GPIO_CRL_CNF1 | GPIO_CRL_MODE1);
GPIOB->CRL |= GPIO_CRL_CNF1_1 | GPIO_CRL_MODE1_0;
// ADXL345 SDA (PA6) 和 SCL (PA7) - 复用推挽输出 (I2C)
GPIOA->CRL &= ~(GPIO_CRL_CNF6 | GPIO_CRL_MODE6);
GPIOA->CRL |= GPIO_CRL_CNF6_1 | GPIO_CRL_MODE6_0;
GPIOA->CRL &= ~(GPIO_CRL_CNF7 | GPIO_CRL_MODE7);
GPIOA->CRL |= GPIO_CRL_CNF7_1 | GPIO_CRL_MODE7_0;
// ADC引脚 (PA1) - 模拟输入
GPIOA->CRL &= ~(GPIO_CRL_CNF1 | GPIO_CRL_MODE1);
// USART2引脚 (PA2-TX, PA3-RX)
GPIOA->CRL &= ~(GPIO_CRL_CNF2 | GPIO_CRL_MODE2);
GPIOA->CRL |= GPIO_CRL_CNF2_1 | GPIO_CRL_MODE2_0; // 复用推挽
GPIOA->CRL &= ~(GPIO_CRL_CNF3 | GPIO_CRL_MODE3);
GPIOA->CRL |= GPIO_CRL_CNF3_0; // 浮空输入
// USART3引脚 (PB10-TX, PB11-RX)
GPIOB->CRH &= ~(GPIO_CRH_CNF10 | GPIO_CRH_MODE10);
GPIOB->CRH |= GPIO_CRH_CNF10_1 | GPIO_CRH_MODE10_0;
GPIOB->CRH &= ~(GPIO_CRH_CNF11 | GPIO_CRH_MODE11);
GPIOB->CRH |= GPIO_CRH_CNF11_0;
}
// ==================== 串口初始化 ====================
void USART_Init(void)
{
// 使能USART时钟
RCC->APB1ENR |= RCC_APB1ENR_USART2EN | RCC_APB1ENR_USART3EN;
// USART2配置 (4G模块) - 115200, 8N1
USART2->BRR = 72000000 / 115200; // 72MHz/115200
USART2->CR1 = USART_CR1_TE | USART_CR1_RE | USART_CR1_RXNEIE; // 使能发送、接收、接收中断
USART2->CR2 = 0;
USART2->CR3 = 0;
USART2->CR1 |= USART_CR1_UE; // 使能USART
// USART3配置 (GPS模块) - 9600, 8N1
USART3->BRR = 72000000 / 9600;
USART3->CR1 = USART_CR1_TE | USART_CR1_RE | USART_CR1_RXNEIE;
USART3->CR2 = 0;
USART3->CR3 = 0;
USART3->CR1 |= USART_CR1_UE;
// 配置NVIC
NVIC_EnableIRQ(USART2_IRQn);
NVIC_SetPriority(USART2_IRQn, 1);
NVIC_EnableIRQ(USART3_IRQn);
NVIC_SetPriority(USART3_IRQn, 2);
}
// ==================== ADC初始化 (电池电量检测) ====================
void ADC_Init(void)
{
// 使能ADC时钟
RCC->APB2ENR |= RCC_APB2ENR_ADC1EN;
// 配置ADC
ADC1->CR2 = ADC_CR2_EXTSEL_SWSTART | ADC_CR2_EXTTRIG; // 软件触发
ADC1->CR1 = 0;
ADC1->SQR1 = 0;
ADC1->SQR3 = ADC_Channel_1; // 通道1 (PA1)
ADC1->SQR2 = 0;
ADC1->CR2 |= ADC_CR2_ADON; // 使能ADC
ADC1->CR2 |= ADC_CR2_RSTCAL; // 复位校准
while(ADC1->CR2 & ADC_CR2_RSTCAL);
ADC1->CR2 |= ADC_CR2_CAL; // 校准
while(ADC1->CR2 & ADC_CR2_CAL);
}
// ==================== 软件I2C (用于OLED和ADXL345) ====================
void I2C_Start(void)
{
OLED_SDA_PORT->BSRR = OLED_SDA_PIN;
OLED_SCL_PORT->BSRR = OLED_SCL_PIN;
Delay_us(5);
OLED_SDA_PORT->BRR = OLED_SDA_PIN;
Delay_us(5);
OLED_SCL_PORT->BRR = OLED_SCL_PIN;
}
void I2C_Stop(void)
{
OLED_SDA_PORT->BRR = OLED_SDA_PIN;
OLED_SCL_PORT->BSRR = OLED_SCL_PIN;
Delay_us(5);
OLED_SDA_PORT->BSRR = OLED_SDA_PIN;
Delay_us(5);
}
void I2C_WriteByte(uint8_t byte)
{
for(uint8_t i = 0; i < 8; i++) {
if(byte & 0x80) {
OLED_SDA_PORT->BSRR = OLED_SDA_PIN;
} else {
OLED_SDA_PORT->BRR = OLED_SDA_PIN;
}
byte <<= 1;
OLED_SCL_PORT->BSRR = OLED_SCL_PIN;
Delay_us(2);
OLED_SCL_PORT->BRR = OLED_SCL_PIN;
Delay_us(2);
}
// 释放SDA,等待ACK
OLED_SDA_PORT->BSRR = OLED_SDA_PIN;
OLED_SCL_PORT->BSRR = OLED_SCL_PIN;
Delay_us(2);
OLED_SCL_PORT->BRR = OLED_SCL_PIN;
}
uint8_t I2C_ReadByte(uint8_t ack)
{
uint8_t byte = 0;
OLED_SDA_PORT->BSRR = OLED_SDA_PIN;
for(uint8_t i = 0; i < 8; i++) {
byte <<= 1;
OLED_SCL_PORT->BSRR = OLED_SCL_PIN;
Delay_us(2);
if(OLED_SDA_PORT->IDR & OLED_SDA_PIN) {
byte |= 0x01;
}
OLED_SCL_PORT->BRR = OLED_SCL_PIN;
Delay_us(2);
}
if(ack) {
OLED_SDA_PORT->BRR = OLED_SDA_PIN; // ACK
} else {
OLED_SDA_PORT->BSRR = OLED_SDA_PIN; // NACK
}
OLED_SCL_PORT->BSRR = OLED_SCL_PIN;
Delay_us(2);
OLED_SCL_PORT->BRR = OLED_SCL_PIN;
return byte;
}
// ==================== OLED显示 ====================
#define OLED_ADDRESS 0x78 // OLED I2C地址
void OLED_WriteCmd(uint8_t cmd)
{
I2C_Start();
I2C_WriteByte(OLED_ADDRESS);
I2C_WriteByte(0x00); // 控制字节: 命令
I2C_WriteByte(cmd);
I2C_Stop();
}
void OLED_WriteData(uint8_t data)
{
I2C_Start();
I2C_WriteByte(OLED_ADDRESS);
I2C_WriteByte(0x40); // 控制字节: 数据
I2C_WriteByte(data);
I2C_Stop();
}
void OLED_Init(void)
{
Delay_ms(100);
OLED_WriteCmd(0xAE); // 关闭显示
OLED_WriteCmd(0xD5); // 设置时钟分频
OLED_WriteCmd(0x80);
OLED_WriteCmd(0xA8); // 设置多路复用
OLED_WriteCmd(0x3F);
OLED_WriteCmd(0xD3); // 设置显示偏移
OLED_WriteCmd(0x00);
OLED_WriteCmd(0x40); // 设置起始行
OLED_WriteCmd(0x8D); // 电荷泵设置
OLED_WriteCmd(0x14);
OLED_WriteCmd(0x20); // 设置内存地址模式
OLED_WriteCmd(0x02);
OLED_WriteCmd(0xA1); // 段重映射
OLED_WriteCmd(0xC8); // 列扫描顺序
OLED_WriteCmd(0xDA); // 设置硬件引脚
OLED_WriteCmd(0x12);
OLED_WriteCmd(0x81); // 设置对比度
OLED_WriteCmd(0xCF);
OLED_WriteCmd(0xD9); // 设置预充电周期
OLED_WriteCmd(0xF1);
OLED_WriteCmd(0xDB); // 设置VCOM检测
OLED_WriteCmd(0x40);
OLED_WriteCmd(0xA4); // 启用显示
OLED_WriteCmd(0xA6); // 正常显示
OLED_WriteCmd(0x2E); // 禁用滚动
OLED_WriteCmd(0xAF); // 开启显示
OLED_Clear();
}
void OLED_Clear(void)
{
for(uint8_t page = 0; page < 8; page++) {
OLED_WriteCmd(0xB0 + page);
OLED_WriteCmd(0x00);
OLED_WriteCmd(0x10);
for(uint8_t col = 0; col < 128; col++) {
OLED_WriteData(0x00);
}
}
}
// 简单字符显示 (ASCII 32-127)
const uint8_t font6x8[][6] = {
{0x00,0x00,0x00,0x00,0x00,0x00}, // Space
// ... 其他字符省略,实际需要完整字库
// 这里提供简化版本
};
void OLED_ShowChar(uint8_t x, uint8_t y, char ch)
{
// 简化实现,实际需要完整字库
if(x > 120 || y > 7) return;
OLED_WriteCmd(0xB0 + y);
OLED_WriteCmd(0x00 + (x & 0x0F));
OLED_WriteCmd(0x10 + ((x >> 4) & 0x0F));
// 显示字符 (简化)
}
void OLED_ShowString(uint8_t x, uint8_t y, char* str)
{
while(*str) {
OLED_ShowChar(x, y, *str++);
x += 6;
if(x > 120) {
x = 0;
y++;
}
}
}
void OLED_ShowNum(uint8_t x, uint8_t y, int num, uint8_t len)
{
char str[16];
sprintf(str, "%*d", len, num);
OLED_ShowString(x, y, str);
}
void OLED_ShowFloat(uint8_t x, uint8_t y, float num, uint8_t decimal)
{
char str[20];
sprintf(str, "%.*f", decimal, num);
OLED_ShowString(x, y, str);
}
// ==================== ADXL345加速度传感器 ====================
#define ADXL345_ADDR 0xA6 // 写地址
void ADXL345_Init(void)
{
// 设置电源模式
ADXL345_WriteReg(0x2D, 0x08); // 测量模式
// 设置数据格式
ADXL345_WriteReg(0x31, 0x08); // ±2g, 13位
// 设置带宽
ADXL345_WriteReg(0x2C, 0x0A); // 100Hz
}
void ADXL345_WriteReg(uint8_t reg, uint8_t value)
{
I2C_Start();
I2C_WriteByte(ADXL345_ADDR);
I2C_WriteByte(reg);
I2C_WriteByte(value);
I2C_Stop();
}
uint8_t ADXL345_ReadReg(uint8_t reg)
{
uint8_t value;
I2C_Start();
I2C_WriteByte(ADXL345_ADDR);
I2C_WriteByte(reg);
I2C_Start();
I2C_WriteByte(ADXL345_ADDR | 0x01);
value = I2C_ReadByte(0);
I2C_Stop();
return value;
}
void ADXL345_Read(int16_t* x, int16_t* y, int16_t* z)
{
uint8_t data[6];
I2C_Start();
I2C_WriteByte(ADXL345_ADDR);
I2C_WriteByte(0x32); // 数据寄存器起始地址
I2C_Start();
I2C_WriteByte(ADXL345_ADDR | 0x01);
for(uint8_t i = 0; i < 6; i++) {
data[i] = I2C_ReadByte(i < 5);
}
I2C_Stop();
*x = (int16_t)((data[1] << 8) | data[0]);
*y = (int16_t)((data[3] << 8) | data[2]);
*z = (int16_t)((data[5] << 8) | data[4]);
}
// ==================== 电池电量检测 ====================
float GetBatteryVoltage(void)
{
uint16_t adc_value = 0;
ADC1->CR2 |= ADC_CR2_SWSTART;
while(!(ADC1->SR & ADC_SR_EOC));
adc_value = ADC1->DR;
// 假设分压电阻比例为1:1,Vref = 3.3V
return (float)adc_value * 3.3f / 4096.0f * 2.0f;
}
float CalculateBatteryPercentage(float voltage)
{
// 锂电池电压范围 3.0V ~ 4.2V
if(voltage >= 4.2f) return 100.0f;
if(voltage <= 3.0f) return 0.0f;
return (voltage - 3.0f) / (4.2f - 3.0f) * 100.0f;
}
// ==================== GPS数据处理 ====================
void ParseGPSData(char* data)
{
// 解析GPGGA或GPRMC语句
if(strstr(data, "$GPGGA")) {
// 示例: $GPGGA,123519,4807.038,N,01131.000,E,1,08,0.9,545.4,M,46.9,M,,*47
char* token = strtok(data, ",");
int field = 0;
while(token != NULL && field < 15) {
switch(field) {
case 1: // 时间
strncpy(g_gpsData.time, token, 9);
break;
case 2: // 纬度
if(strlen(token) > 0) {
double lat = atof(token);
int degrees = (int)(lat / 100);
double minutes = lat - degrees * 100;
g_gpsData.latitude = degrees + minutes / 60.0;
}
break;
case 3: // 北纬/南纬
if(token[0] == 'S') g_gpsData.latitude = -g_gpsData.latitude;
break;
case 4: // 经度
if(strlen(token) > 0) {
double lon = atof(token);
int degrees = (int)(lon / 100);
double minutes = lon - degrees * 100;
g_gpsData.longitude = degrees + minutes / 60.0;
}
break;
case 5: // 东经/西经
if(token[0] == 'W') g_gpsData.longitude = -g_gpsData.longitude;
break;
case 6: // 定位状态
g_gpsData.status = token[0];
g_gpsData.fixed = (token[0] == 'A') ? 1 : 0;
break;
case 7: // 卫星数量
break;
case 8: // HDOP
break;
case 9: // 海拔
break;
}
token = strtok(NULL, ",");
field++;
}
}
}
void ProcessGPSData(void)
{
if(g_gpsData.fixed) {
g_deviceInfo.gpsFixed = 1;
g_deviceInfo.lat = g_gpsData.latitude;
g_deviceInfo.lon = g_gpsData.longitude;
} else {
g_deviceInfo.gpsFixed = 0;
}
}
// ==================== 设备状态检测 ====================
void UpdateDeviceState(void)
{
static int16_t lastX = 0, lastY = 0, lastZ = 0;
int16_t x, y, z;
ADXL345_Read(&x, &y, &z);
// 计算加速度变化量
int16_t deltaX = x - lastX;
int16_t deltaY = y - lastY;
int16_t deltaZ = z - lastZ;
// 如果加速度变化超过阈值,认为设备在运动
if(abs(deltaX) > 50 || abs(deltaY) > 50 || abs(deltaZ) > 50) {
g_deviceInfo.state = 1; // 运动
} else {
g_deviceInfo.state = 0; // 静止
}
lastX = x;
lastY = y;
lastZ = z;
}
// ==================== 围栏检测 ====================
double CalculateDistance(double lat1, double lon1, double lat2, double lon2)
{
const double R = 6371000.0; // 地球半径(米)
double dLat = (lat2 - lat1) * 3.141592653589793 / 180.0;
double dLon = (lon2 - lon1) * 3.141592653589793 / 180.0;
double a = sin(dLat/2) * sin(dLat/2) +
cos(lat1 * 3.141592653589793 / 180.0) * cos(lat2 * 3.141592653589793 / 180.0) *
sin(dLon/2) * sin(dLon/2);
double c = 2 * atan2(sqrt(a), sqrt(1-a));
return R * c;
}
void CheckFenceAlarm(void)
{
if(!g_deviceInfo.gpsFixed) return;
if(g_deviceInfo.fenceLon == 0 && g_deviceInfo.fenceLat == 0) return;
if(!g_deviceInfo.alarmSW) return;
double distance = CalculateDistance(g_deviceInfo.lat, g_deviceInfo.lon,
g_deviceInfo.fenceLat, g_deviceInfo.fenceLon);
// 检查是否超出围栏
uint8_t inFence = 1;
if(g_deviceInfo.fenceType == 0) { // 圆形
if(distance > g_deviceInfo.fenceRadius) inFence = 0;
} else { // 矩形
double halfSide = g_deviceInfo.fenceRadius / 111000.0;
double minLat = g_deviceInfo.fenceLat - halfSide;
double maxLat = g_deviceInfo.fenceLat + halfSide;
double minLon = g_deviceInfo.fenceLon - halfSide / cos(g_deviceInfo.fenceLat * 3.141592653589793 / 180.0);
double maxLon = g_deviceInfo.fenceLon + halfSide / cos(g_deviceInfo.fenceLat * 3.141592653589793 / 180.0);
if(g_deviceInfo.lat < minLat || g_deviceInfo.lat > maxLat ||
g_deviceInfo.lon < minLon || g_deviceInfo.lon > maxLon) {
inFence = 0;
}
}
// 触发报警
if(!inFence) {
BEEP_PORT->BSRR = BEEP_PIN; // 打开蜂鸣器
LED_PORT->BSRR = LED3_PIN; // 点亮报警LED
} else {
BEEP_PORT->BRR = BEEP_PIN; // 关闭蜂鸣器
LED_PORT->BRR = LED3_PIN; // 关闭报警LED
}
}
// ==================== 4G模块 (Air780E) 通信 ====================
void LTE_SendAT(const char* cmd)
{
USART_SendString(USART2, (char*)cmd);
USART_SendString(USART2, "rn");
}
uint8_t LTE_WaitResponse(const char* expected, uint32_t timeout)
{
uint32_t start = g_sysTick;
while(g_sysTick - start < timeout) {
if(strstr(g_usart2_rx_buf, expected) != NULL) {
return 1;
}
Delay_ms(100);
}
return 0;
}
void MQTT_Connect(void)
{
char cmd[256];
// 设置APN (中国移动)
sprintf(cmd, "AT+CSTT=\"CMIOT\"");
LTE_SendAT(cmd);
Delay_ms(500);
// 启动网络
LTE_SendAT("AT+CIICR");
Delay_ms(2000);
// 获取IP
LTE_SendAT("AT+CIFSR");
Delay_ms(500);
// 连接MQTT服务器
sprintf(cmd, "AT+MQTTCONN=\"%s\",%d,120,1", MQTT_SERVER, MQTT_PORT);
LTE_SendAT(cmd);
Delay_ms(1000);
// 设置ClientID
sprintf(cmd, "AT+MQTTCLIENTID=\"%s\"", MQTT_CLIENT_ID);
LTE_SendAT(cmd);
Delay_ms(500);
// 设置用户名
sprintf(cmd, "AT+MQTTUSERNAME=\"%s\"", MQTT_USERNAME);
LTE_SendAT(cmd);
Delay_ms(500);
// 设置密码
sprintf(cmd, "AT+MQTTPASSWORD=\"%s\"", MQTT_PASSWORD);
LTE_SendAT(cmd);
Delay_ms(500);
// 订阅主题
sprintf(cmd, "AT+MQTTSUB=\"%s\",1", SUB_TOPIC);
LTE_SendAT(cmd);
Delay_ms(500);
g_mqttConnected = 1;
}
void MQTT_PublishData(void)
{
char payload[512];
char json[1024];
// 构建JSON数据
sprintf(payload,
"{\"services\":[{\"service_id\":\"stm32\",\"properties\":{"
"\"Electricity\":%.1f,\"AlarmSW\":%d,\"state\":%d,"
"\"GPS\":{\"lon\":%.6f,\"lat\":%.6f},"
"\"GPS_fence\":{\"lon\":%.6f,\"lat\":%.6f}}}]}",
g_deviceInfo.electricity,
g_deviceInfo.alarmSW,
g_deviceInfo.state,
g_deviceInfo.lon,
g_deviceInfo.lat,
g_deviceInfo.fenceLon,
g_deviceInfo.fenceLat
);
// 发布数据
char cmd[600];
sprintf(cmd, "AT+MQTTPUB=\"%s\",\"%s\",1,0", PUB_TOPIC, payload);
LTE_SendAT(cmd);
Delay_ms(200);
// 闪烁LED2表示数据上传
LED_PORT->BSRR = LED2_PIN;
Delay_ms(50);
LED_PORT->BRR = LED2_PIN;
}
void MQTT_ProcessMessage(char* message)
{
// 解析收到的MQTT消息
// 处理围栏设置等命令
if(strstr(message, "GPS_fence")) {
// 解析围栏设置
}
if(strstr(message, "AlarmSW")) {
// 解析报警开关
}
}
// ==================== 串口发送函数 ====================
void USART_SendChar(USART_TypeDef* USARTx, char ch)
{
while(!(USARTx->SR & USART_SR_TXE));
USARTx->DR = ch;
}
void USART_SendString(USART_TypeDef* USARTx, char* str)
{
while(*str) {
USART_SendChar(USARTx, *str++);
}
}
void USART_SendData(USART_TypeDef* USARTx, uint8_t* data, uint16_t len)
{
for(uint16_t i = 0; i < len; i++) {
USART_SendChar(USARTx, data[i]);
}
}
// ==================== 延时函数 ====================
void Delay_us(uint32_t us)
{
uint32_t count = us * 8; // 72MHz下粗略延时
while(count--);
}
void Delay_ms(uint32_t ms)
{
for(uint32_t i = 0; i < ms; i++) {
Delay_us(1000);
}
}
// ==================== 定时器初始化 (用于系统时钟) ====================
void TIM_Init(void)
{
// 使用SysTick作为系统时钟
SysTick_Config(SystemCoreClock / 1000); // 1ms中断
}
void SysTick_Handler(void)
{
g_sysTick++;
}
// ==================== 主程序 ====================
int main(void)
{
// 系统初始化
SystemClock_Config();
GPIO_Init();
USART_Init();
ADC_Init();
I2C_Init();
OLED_Init();
ADXL345_Init();
TIM_Init();
// 显示启动信息
OLED_ShowString(0, 0, "GPS Tracker v1.0");
OLED_ShowString(0, 1, "Initializing...");
// 初始化GPS数据
memset(&g_gpsData, 0, sizeof(GPS_Data_t));
// 连接MQTT服务器
OLED_ShowString(0, 2, "MQTT Connecting");
MQTT_Connect();
if(g_mqttConnected) {
OLED_ShowString(0, 3, "MQTT Connected");
} else {
OLED_ShowString(0, 3, "MQTT Failed");
}
// 主循环
while(1) {
// LED1闪烁指示程序运行
if(g_sysTick - g_ledToggle > 500) {
g_ledToggle = g_sysTick;
LED_PORT->ODR ^= LED1_PIN;
}
// 更新电池电量 (每5秒)
static uint32_t lastBatteryTime = 0;
if(g_sysTick - lastBatteryTime > 5000) {
lastBatteryTime = g_sysTick;
float voltage = GetBatteryVoltage();
g_deviceInfo.electricity = CalculateBatteryPercentage(voltage);
}
// 更新设备状态 (每200ms)
static uint32_t lastStateTime = 0;
if(g_sysTick - lastStateTime > 200) {
lastStateTime = g_sysTick;
UpdateDeviceState();
}
// 处理GPS数据 (每100ms)
static uint32_t lastGPSTime = 0;
if(g_sysTick - lastGPSTime > 100) {
lastGPSTime = g_sysTick;
if(g_usart3_rx_len > 0) {
// 处理接收到的GPS数据
char* line = strtok(g_usart3_rx_buf, "n");
while(line != NULL) {
if(strstr(line, "$GPGGA") || strstr(line, "$GPRMC")) {
ParseGPSData(line);
}
line = strtok(NULL, "n");
}
g_usart3_rx_len = 0;
memset(g_usart3_rx_buf, 0, sizeof(g_usart3_rx_buf));
ProcessGPSData();
}
}
// 检查围栏报警 (每500ms)
static uint32_t lastFenceTime = 0;
if(g_sysTick - lastFenceTime > 500) {
lastFenceTime = g_sysTick;
CheckFenceAlarm();
}
// 上传数据到云端 (每3秒)
static uint32_t lastUploadTime = 0;
if(g_sysTick - lastUploadTime > 3000) {
lastUploadTime = g_sysTick;
if(g_mqttConnected) {
MQTT_PublishData();
}
}
// 更新OLED显示 (每200ms)
static uint32_t lastDisplayTime = 0;
if(g_sysTick - lastDisplayTime > 200) {
lastDisplayTime = g_sysTick;
// 显示GPS信息
char display[32];
sprintf(display, "Lon:%.6f", g_deviceInfo.lon);
OLED_ShowString(0, 4, display);
sprintf(display, "Lat:%.6f", g_deviceInfo.lat);
OLED_ShowString(0, 5, display);
sprintf(display, "Batt:%.1f%%", g_deviceInfo.electricity);
OLED_ShowString(0, 6, display);
sprintf(display, "State:%s", g_deviceInfo.state ? "Moving" : "Static");
OLED_ShowString(0, 7, display);
}
// 处理USART2接收的数据 (4G模块)
if(g_usart2_rx_len > 0) {
// 检查是否有MQTT消息
if(strstr(g_usart2_rx_buf, "+MQTTPUBLISH")) {
// 提取消息内容并处理
MQTT_ProcessMessage(g_usart2_rx_buf);
}
g_usart2_rx_len = 0;
memset(g_usart2_rx_buf, 0, sizeof(g_usart2_rx_buf));
}
}
}
// ==================== 串口中断处理 ====================
void USART2_IRQHandler(void)
{
if(USART2->SR & USART_SR_RXNE) {
char c = USART2->DR;
if(g_usart2_rx_len < sizeof(g_usart2_rx_buf) - 1) {
g_usart2_rx_buf[g_usart2_rx_len++] = c;
}
}
}
void USART3_IRQHandler(void)
{
if(USART3->SR & USART_SR_RXNE) {
char c = USART3->DR;
if(g_usart3_rx_len < sizeof(g_usart3_rx_buf) - 1) {
g_usart3_rx_buf[g_usart3_rx_len++] = c;
}
}
}
4.3 程序下载
也有视频教程:
讲解如何编译代码,下载STM32程序: https://www.bilibili.com/video/BV1Cw4m1e7Yc
打STM32的keil工程,编译代码、然后,使用USB线将开发板的左边的USB口(串口1)与电脑的USB连接,打开程序下载软件下载程序。
具体下载过程看下面图:
打开程序下载软件:[软件就在资料包里的软件工具目录下]
4.4 程序正常运行效果
设备运行过程中会通过串口打印调试信息,我们可以通过串口打印了解程序是否正常。
程序下载之后,可以打开串口调试助手查看程序运行的状态信息。[软件就在资料包里的软件工具目录下]
4.5 取模软件的使用
显示屏上会显示中文,字母,数字等数据,可以使用下面的取模软件进行取模设置。
[软件就在资料包里的软件工具目录下]
打开软件之后:
五、总结
本项目完成了一套基于物联网与GPS卫星定位技术的电子围栏系统设计与实现。系统以STM32F103C8T6单片机作为核心控制单元,结合ATGM336H北斗+GPS双模定位模块、Air780E 4G通信模块、电池供电管理电路以及本地报警与状态显示模块,构建了集定位采集、无线通信、围栏监测、异常报警和远程管理于一体的智能监护平台。
在功能实现方面,系统能够实时获取目标设备的位置信息,通过4G网络将定位数据上传至华为云物联网平台,并借助MQTT协议实现设备与云端之间的数据交互。用户可通过Android手机APP以及Windows电脑客户端远程查看目标位置、电量状态、设备运行情况以及最后已知位置。同时系统支持电子围栏范围设置,可根据实际需求选择圆形或矩形围栏,并能够动态调整围栏尺寸,实现目标活动区域管理。当目标超出预设范围时,系统能够及时触发本地报警与远程语音提醒,提高目标防丢和安全监护能力。
在系统设计过程中,采用模块化设计思想,将定位模块、通信模块、电源管理模块、报警模块及显示模块进行独立设计与统一集成,提高了系统结构的清晰性和可扩展性。硬件端采用基于寄存器方式的STM32底层开发方案,保证设备运行效率与资源利用率;软件端采用Qt框架完成跨平台客户端开发,并结合地图可视化实现位置展示与电子围栏交互操作,增强系统的实际使用体验。
通过项目开发与测试验证,系统能够稳定完成定位采集、数据上传、围栏监测及远程显示等核心任务,满足电子围栏应用场景下对实时性、远程性和可视化管理的需求。本设计不仅加深了对嵌入式系统、物联网通信、卫星定位及上位机开发技术的综合应用能力,也为个人物品防丢、老人儿童监护、宠物追踪及移动资产管理等场景提供了一种具有实际应用价值的解决方案。
227