绿色再生·安卓4G智能远程操作巡视机器人小车

一、前言

1.1 项目介绍

【1】项目功能介绍

随着物联网技术与移动通信技术的快速发展,远程遥控设备在日常生活及工业应用中的普及度日益提高。无论是家用扫地机器人实现自主导航清扫,还是目前抖音平台上展示的实景互动小车等创新应用,都体现了远程控制和实时视频监控技术对现代智能设备的重要性。

本项目设计并实现一款基于STM32微控制器的远程遥控安卓小车系统。该系统充分利用了淘汰下来的安卓旧手机作为车载信息处理单元,不仅实现了资源的有效再利用,还结合4G网络技术以及先进的流媒体服务和物联网技术,搭建起一套集远程操控、实时视频音频传输于一体的高效解决方案。

项目的小车搭载了STM32主控板以精确控制四个电机的动作,通过L298N驱动芯片确保了底座移动的稳定性和灵活性。同时,小车的动力源采用两节18650锂电池提供充足的电力支持。

车载的旧安卓手机通过USB线连接到STM32主控板上,接收并执行来自远端手机APP的指令。这款由Qt开发的Android APP能够利用4G网络实现实时在线,并通过摄像头采集音视频数据,通过RTMP协议将这些数据推送到华为云ECS服务器上的NGINX流媒体服务器,从而实现高清流畅的远程视频监控。

为了实现双向交互和低延迟控制,整个系统还借助MQTT协议连接至华为云IOT服务器。另一台安装了同样由Qt开发的Android手机APP的终端设备,可以通过该APP拉取小车端的实时音视频流进行播放,并通过方向键菜单实现对小车的精准远程操控。这种设计不仅极大地拓展了传统遥控小车的功能性与实用性,还为其他类似应用场景提供了可借鉴的技术框架。

当前设计的这种基于4G网络设计的远程遥控巡检小车的技术应用场景主要是: 安全防护、环境监测、设备巡检、物料搬运、应急救援这些地方。

(1)远程监控

  • 可应用于安全防护、环境监测、农业监控等领域,例如森林防火、农田灌溉管理、危险区域侦查等,通过实时视频和音频传输,工作人员可以在远程位置对现场情况进行实时了解和操控。

(2)工业巡检

  • 在工厂、仓库或大型设施内部署此类小车,用于设备巡检、物料搬运或生产流程监控,尤其适合那些人员不易到达或者存在安全隐患的地方。

(3)应急救援

  • 在地震、火灾等灾害现场,远程遥控小车可用于进入倒塌建筑内部搜寻生命迹象,或是携带传感器测量有害气体浓度等,为救援决策提供及时信息

下面是当前小车整体技术框架:

小车的模型:

本次设计里放在小车终端上的Android手机采用的是小米4C,一款普通的Android手机:

2015年上市的小米4C。 从本身价值来讲,现在在某鱼上差不多是200块,本身就是一个完整的系统。性价比非常高。比去单独买Linux开发板进行模型开发实验来说,成本低低很多。 主流的Linux、Android开发板价格都比较贵的。

开发过程中,测试遥控效果:

【2】设计实现的功能

(1)STM32主控板功能:

  • 控制4个电机:STM32通过L298N驱动芯片驱动4个电机,实现小车的前进、后退、左转、右转等动作。
  • 数据通信:STM32通过USB接口与安卓手机通信,接收手机APP发送的控制指令,并将小车的状态信息(如电量、速度、位置等)发送回手机。
  • 电源管理:管理2节18650锂电池的供电,确保电压稳定并监控电池电量。

(2)安卓手机APP功能

  • 控制指令下发:手机APP通过USB接口向STM32发送控制指令,控制小车的动作。
  • 视频和音频流获取:APP从手机摄像头和麦克风获取视频和音频流,并进行编码处理。
  • 流媒体推流:通过RTMP协议将编码后的视频和音频流推送到华为云ECS服务器+NGINX搭建的RTMP流媒体服务器。
  • MQTT连接:APP通过MQTT协议与华为云IOT服务器建立连接,实现双向通信。

(3)华为云服务器功能:

  • RTMP流媒体服务:接收并转发安卓手机APP推送的视频和音频流。
  • MQTT服务:作为MQTT消息代理,实现远程手机与STM32主控板之间的通信。

(4)远程Android手机APP功能:

  • 实时视频和音频播放:从华为云ECS服务器拉取视频和音频流,并实时显示在APP界面上。
  • MQTT连接:与华为云IOT服务器建立连接,接收STM32主控板发送的小车状态信息。
  • 远程控制:提供方向键控制菜单,允许用户远程控制小车前进、后退、转弯等动作。

【3】项目硬件模块组成

(1)电源模块

  • 电池组:采用两节18650锂电池作为供电源,它们具有高能量密度、体积小的特点,能够为整个系统提供稳定的直流电能。

(2)主控模块

  • STM32微控制器:这是整个小车的核心控制单元,负责处理所有的逻辑运算和数据通信任务。通过编程实现对电机驱动、USB通信、网络连接等功能的控制。

(3)电机驱动模块

  • L298N驱动芯片:用于驱动底座上的四个电机,L298N是一个高性能的H桥电机驱动器,可以接收来自STM32的信号,转换为足够驱动电机工作的电流和电压,并且支持电机正反转及速度调节。

(4)移动平台模块

  • 四个直流电机:直接安装在小车底座上,通过L298N驱动进行精确的速度和方向控制,以实现小车前进、后退、左右转弯等运动功能。

(5)通信模块

  • USB接口:STM32主控板通过USB线与安卓手机物理连接,实现数据传输,接收来自手机APP的控制指令。
  • 4G模组:集成在安卓手机内部,插入SIM卡后可实现高速无线网络连接,使小车能够在远程环境下通过互联网与其他设备通信。

(6)多媒体采集模块

  • 安卓手机摄像头:用于捕捉实时视频和音频信息,是小车端环境感知的关键组件。

(7)云服务交互模块

  • 华为云ECS服务器+NGINX RTMP流媒体服务器:小车端将采集到的音视频流推送到华为云服务器上,通过RTMP协议实现实时音视频的低延迟传输和分发。
  • 华为云IOT服务器:小车和远端控制手机均通过MQTT协议与之建立连接,实现远程数据交换和控制命令的下发。

【3】功能总结

(1)电机驱动与控制:通过STM32微控制器和L298N驱动芯片,实现对小车上四个电机的精确控制,包括前进、后退、左转、右转等动作,从而控制小车的移动方向和速度。

(2)无线通信与数据传输:STM32与安卓手机之间通过USB接口建立通信,实现控制指令的下发和小车状态信息的上传。同时,安卓手机通过4G网络连接到华为云服务器,实现了远程控制命令的远程传输和视频音频流的推送。

(3)流媒体推流与播放:安卓手机APP能够捕获手机摄像头和麦克风的视频和音频流,通过RTMP协议推送到华为云服务器。另一台安卓手机APP则从服务器拉取这些流,实现实时播放,从而允许用户远程观看小车的实时画面和音频。

(4)华为云服务器支持:华为云ECS服务器和NGINX搭建的RTMP流媒体服务器负责接收、转发视频和音频流,确保流媒体的稳定性和实时性。同时,华为云IOT服务器通过MQTT协议提供消息代理服务,实现远程手机与STM32之间的双向通信。

(5)用户界面与交互设计:安卓手机APP提供了直观的用户界面,包括控制按钮、状态显示、视频播放器等,使用户能够方便地对小车进行控制、观看视频、监听音频,以及监控小车的状态信息。

(6)远程控制:通过结合STM32的电机控制、华为云服务器的数据处理和传输,以及安卓手机的用户界面和交互设计,实现了从远程手机到小车的远程控制功能。用户可以在远离小车的地点,通过手机APP发出控制指令,实时观察小车的动作和周围环境。

1.2 设计思路

1.3 系统功能总结

自主供电与移动控制 采用2节18650锂电池为小车提供电力供应;STM32微控制器结合L298N驱动芯片,精准控制4个电机动作,实现前进、后退、转弯等移动功能
手机APP通信与指令传输 STM32通过USB线与安卓手机连接,接收并解析来自手机APP的控制指令,实现人机交互和远程指令执行
实时视频音频流传输 安卓手机利用4G网络上网,搭载Qt开发的Android APP采集摄像头视频和音频数据,并通过RTMP协议将音视频流推送到华为云ECS服务器+NGINX搭建的流媒体服务器
物联网(IoT)连接与远程监控 小车端及远端控制手机均通过MQTT协议连接华为云IOT服务器,实现车辆状态信息实时上传及远程音视频流拉取显示;远端手机APP提供方向键菜单以远程操控小车
数据交互与低延迟控制 利用MQTT协议确保在4G网络环境下高效、低延迟的数据交互,实现对小车的实时远程控制,提升整体系统的响应速度和操作体验

二、搭建视频监控流媒体服务器

2.1 购买云服务器

如果之前没有使用过华为云的ECS服务器,可以先申请试用1个月,了解服务器的基本使用然后再购买,不得不说提供这个试用服务还是非常不错。

产品试用领取地址: https://activity.huaweicloud.com/free_test/index.html

每天9:30开抢,每天限量100份,这个页面不仅有云服务器可以领取试用,还有云数据库、沙盒等其他产品。

我这里领取了 2核4G S6云服务器,这个服务器是配套华为自研25GE智能高速网卡,适用于网站和Web应用等中轻载企业应用。

选择配置的时候发现S6规格的已经没有了,来晚了。

S6规格没有了,就选择了S3,2核,4GB的配置结算。

页面向下翻,下面选择系统预装的系统,我这里选择ubuntu 20.04,安装NGINX,来搭建流媒体服务器。

页面继续下翻,设置云服务器名称,设置系统的root密码。

接着就可以继续去支付了。

购买后等待一段时间,系统资源就即可分配完成。

2.2 登录云服务器

云服务器的管理控制台从这里进入: https://www.huaweicloud.com/product/ecs.html

在官网主页,点击产品,找到计算选项,就可以看到弹性云服务器ECS,点击进去就可以看到管理控制台的选项。

弹性云服务器的选项页面可以看到刚才购买的云服务器,如果点击进去提示该区域没有可用的服务器,说明区域选择的不对,在下面截图红色框框的位置可以看到可用的区域切换按钮,切换之后就行了。

点击服务器右边的更多,可以对服务器重装系统、切换操作系统、关机、开机、重启、重置密码等操作。

接下来先点击登录,了解一下登录的流程,看看系统进去的效果。

云服务器支持SSH协议远程登录,可以在浏览器上直接使用CloudShell方式或者VNC方式登录,如果本身你自己也是使用Linux系统,可以在Linux系统里通过ssh命令直接登录,如果是在windows下可以使用SecureCRT登录。

其他登录方式。

最方便的登录方式,直接在控制台使用VNC在浏览器里登录,点击立即登录

根据提示输入用户名,密码后,按下回车键即可登录。

用户名直接输入root,密码就是刚才配置云服务器时,填入的root密码。

注意: Linux下输入密码默认都是隐藏的,也就是在键盘上输入密码界面上是不会有反应的,自己按照正确的密码输入即可。

用户名、密码输入正确后,登录成功。

可以点击全屏模式,更好的操作。

2.3 使用CloudShell登录云服务器

在页面上直接点击CloudShell登录按钮。CloudShell方式比VNC方式方便、流畅多了,也支持命令的复制粘贴。

输入密码点击连接。

登录成功进入终端。

2.4 使用SecureCRT登录云服务器

上面演示了两种登录方式,都是直接在浏览器里登录,这种两种方式比较来看,VNC方式效率最低,CloudShell相对来说要方便很多。一般我自己正常开发时,都是使用第三方工具来登录的,如果本身自己开发环境的电脑就是Linux,MAC等,可以直接使用ssh命令登录,这种方式操作流畅方便。如果在windows下,可以使用第三方软件登录。

我现在使用的系统是win10,在windows系统下有很多远程终端软件支持SSH登录到Linux云服务器,我当前采用的使用SecureCRT 6.5来作为登录终端,登录云服务器。

注意: SecureCRT 6.5 登录高版本Linux系统会出现Key exchange failed问题,导致登录失败,比如: 登录ubuntu 20.04 系统。 出现这种问题需要对系统ssh配置文件进行添加配置。

添加配置的流程:

cpp 复制代码
命令行输入:
vim /etc/ssh/sshd_config

在文件最后添加:
KexAlgorithms curve25519-sha256@libssh.org,ecdh-sha2-nistp256,ecdh-sha2-nistp384,ecdh-sha2-nistp521,diffie-hellman-group-exchange-sha256,diffie-hellman-group14-sha1,diffie-hellman-group-exchange-sha1,diffie-hellman-group1-sha1

保存退出。
    
重启ssh服务
service ssh restart

如果不想这么麻烦的去修改配置文件,那么最简单的办法就是: 切换操作系统,换一个低版本的,比如:ubuntu18.04 即可。

在云服务器的控制台,找到自己的服务器,然后选择切换操作系统。

根据界面上的引导流程,切换即可。更换新的系统需要1~4分钟时间,稍微等待一下即可。

如果要使用远程SSH协议方式登录云服务器,需要具备以下几个前提条件。

cpp 复制代码
[1]弹性云服务器状态为"运行中"。
[2]弹性云服务器已经绑定弹性公网IP,绑定方式请参见绑定弹性公网IP。
[3]所在安全组入方向已开放22端口,配置方式请参见配置安全组规则。
[4]使用的登录工具(如PuTTY,SecureCRT`)与待登录的弹性云服务器之间网络连通。例如,默认的22端口没有被防火墙屏蔽。

但是这些配置不用担心,在购买服务器后,根据引导一套走完,上面的这些配置都已经默认配置好了,不用自己再去单独配置。

系统切换成功后,打开SecureCRT 6.5软件,进行登录。SecureCRT 6.5整体而言还是挺好用的。

如果自己没有``SecureCRT,自己下载一个即可。当然,不一定非要使用SecureCRT`,其他还有很多SSH远程登录工具,喜欢哪个使用哪个。

下面介绍``SecureCRT `登录的流程。

选择新建会话。

选择SSH2协议。

主机名就填服务器的公网IP地址,端口号默认是22,用户名填root。

自己云服务器的公网IP地址,在控制台可以看到。

软件点击下一步之后,可以填充描述信息,备注这个链接是干什么的。

选择刚才新建的协议端口点击连接。

云服务器连接上之后,软件界面会弹出对话框填充用户名、密码。

登录成功的效果如下。

软件可以配置一些选项,让界面符合Linux终端配色,可以修改字体大小、字体编码等等。

配置后的效果。

2.5 安装NFS服务器

为了方便向服务器上拷贝文件,可以采用NFS服务器、或者FTP服务器 这些方式。 我本地有一台ubuntu 18.04 系统笔记本,我这里采用NFS方式拷贝文件上去。

(1)安装NFS服务器

cpp 复制代码
root@ecs-348470:~# sudo apt-get install nfs-kernel-server

(2)创建一个work目录方便当做共享目录使用

cpp 复制代码
root@ecs-348470:~# mkdir work

(3)编写NFS配置文件

NFS 服务的配置文件为/etc/exports,如果系统没有默认值,这个文件就不一定会存在,可以使用 vim 手动建立,然后在文件里面写入配置内容。

cpp 复制代码
/home/work *(rw,no_root_squash,sync,no_subtree_check,insecure)    

配置文件里参数的含义:

(4)NFS服务器相关指令

cpp 复制代码
/etc/init.d/nfs-kernel-server start #启动 NFS 服务
ufw disable     #关闭防火墙
/etc/init.d/nfs-kernel-server restart  #重启NFS服务
exportfs -arv   #共享路径生效

(5)本地客户机挂载服务器的目录

cpp 复制代码
wbyq@wbyq:~$ sudo mount -t nfs -o nolock 122.112.212.171:/home/work /home/wbyq/mnt/

(6)设置华为云服务器的安全策略

需要把华为云服务器的端口号开放出来,不然其他客户端是无法访问服务器的。

我这里比较粗暴直接,直接将所有端口号,所有IP地址都开放出来了。

**(7)本地客户机挂载服务器测试 **

挂载指令:

cpp 复制代码
sudo mount -t nfs -o nolock 122.112.212.171:/home/work /home/wbyq/mnt/

2.6 安装NGINX(配置RTMP服务)

(1)下载编译时需要依赖的一些工具

cpp 复制代码
root@ecs-348470:~# sudo apt-get install build-essential libpcre3 libpcre3-dev libssl-dev

(2)下载NGINX编译需要的软件包

cpp 复制代码
root@ecs-348470:~# mkdir nginx      
root@ecs-348470:~# cd nginx/
root@ecs-348470:~# wget http://nginx.org/download/nginx-1.10.3.tar.gz
root@ecs-348470:~# wget http://zlib.net/zlib-1.2.11.tar.gz
root@ecs-348470:~# wget https://ftp.pcre.org/pub/pcre/pcre-8.40.tar.gz
root@ecs-348470:~# wget https://www.openssl.org/source/openssl-1.0.2k.tar.gz
root@ecs-348470:~# wget https://github.com/arut/nginx-rtmp-module/archive/master.zip

(3)下载后的文件全部解压

cpp 复制代码
root@ecs-348470:~# tar xvf openssl-1.0.2k.tar.gz
root@ecs-348470:~# tar xvf nginx-rtmp-module-master.tar.gz
root@ecs-348470:~# tar xvf nginx-1.8.1.tar.gz
root@ecs-348470:~# tar xvf pcre-8.40.tar.gz
root@ecs-348470:~# tar xvf zlib-1.2.11.tar.gz

(4)配置NGINX源码,生成Makefile文件

cpp 复制代码
root@ecs-348470:~# cd nginx-1.8.1/
root@ecs-348470:~# ./configure --prefix=/usr/local/nginx --with-debug --with-pcre=../pcre-8.40 --with-zlib=../zlib-1.2.11 --with-openssl=../openssl-1.0.2k --add-module=../nginx-rtmp-module-master

执行完上一步之后,打开objs/Makefile文件,找到-Werror选项并删除。

(5)编译并安装NGINX

cpp 复制代码
 root@ecs-348470:~/nginx/nginx-1.8.1# make && make install

安装之后NGINX的配置文件存放路径:

cpp 复制代码
/usr/local/nginx/nginx:主程序

(6)查看NGINX的版本号

cpp 复制代码
root@ecs-348470:/usr/local/nginx/sbin# /usr/local/nginx/sbin/nginx -v
nginx version: nginx/1.8.1

(5)在配置文件里加入RTMP服务器的配置

cpp 复制代码
root@ecs-348470:~# vim /usr/local/nginx/conf/nginx.conf 
打开文件后,在文件最后加入以下配置:

rtmp {  
    server {  
        listen 8888;   
        application live {  
            live on;  
			record all;
    		record_unique on;
    		record_path "./video";  #视频缓存的路径
    		record_suffix -%Y-%m-%d-%H_%M_%S.flv;
        	}
         } 		 
}

这样配置之后,服务器收到RTMP流会在NGINX的当前目录下,创建一个video目录用来缓存视频。

客户端向服务器推流之后,服务器就会缓存视频到设置的目录。

(5)检查配置文件是否正确

cpp 复制代码
root@ecs-348470:/usr/local/nginx/sbin# /usr/local/nginx/sbin/nginx -t
nginx: the configuration file /usr/local/nginx/conf/nginx.conf syntax is ok
nginx: configuration file /usr/local/nginx/conf/nginx.conf test is successful

(6)NGINX常用的3个命令

cpp 复制代码
sudo service nginx start
sudo service nginx stop
sudo service nginx restart

(7)启动NGINX服务器

cpp 复制代码
sudo service nginx start

2.7 摄像头推流音视频到服务器

为了模拟摄像头实时监控推流,我这使用QT+FFMPEG编写了一个小软件,在windows下运行,推流本地笔记本电脑的数据到服务器。软件运行之后,将本地音频、视频编码之后通过RTMP协议推流到NGINX服务器。

软件运行效果:

推流工具运行过程中效果。

2.8 编写本地RTMP流播放器

在上面通过推流客户端模拟摄像头,已经将本地的摄像头数据实时推流到服务器了,那么还差一个播放器,为了方便能够在任何有网的地方实时查看摄像头的音频和图像,还需要编写一个RTMP流媒体播放器。

我这里的播放器内核是采用libvlc开发的,使用QT作为GUI框架,开发了一个流媒体播放器,可以实时拉取服务器上的流数据,并且还支持回放。因为服务器上的NGINX配置了自动保存的参数,可以将推上去的流按时间段保存下来。

输入服务器地址之后就可以拉取流进行播放。

点击获取回放列表,可以查看服务器上保存的历史视频回放播放。

三、华为云IOT服务器部署过程

在华为云IOT平台上,需要进行设备接入、数据模型定义、规则引擎配置和应用开发等四个核心模块的开发。其中,设备接入模块包括设备注册、获取设备证书、建立连接等步骤,以保障设备与云平台之间的安全通信;数据模型定义模块需要根据实际需求定义相应的数据模型,包括上传数据格式、设备属性和服务等。规则引擎配置模块需要完成实时消息推送、远程控制和告警等功能。应用开发模块则是将完整的智能井盖系统进行打包,为用户提供统一的操作接口。

华为云官网: https://www.huaweicloud.com/

打开官网,搜索物联网,就能快速找到 设备接入IoTDA

3.1 物联网平台介绍

华为云物联网平台(IoT 设备接入云服务)提供海量设备的接入和管理能力,将物理设备联接到云,支撑设备数据采集上云和云端下发命令给设备进行远程控制,配合华为云其他产品,帮助我们快速构筑物联网解决方案。

使用物联网平台构建一个完整的物联网解决方案主要包括3部分:物联网平台、业务应用和设备。

物联网平台作为连接业务应用和设备的中间层,屏蔽了各种复杂的设备接口,实现设备的快速接入;同时提供强大的开放能力,支撑行业用户构建各种物联网解决方案。

设备可以通过固网、2G/3G/4G/5G、NB-IoT、Wifi等多种网络接入物联网平台,并使用LWM2M/CoAP、MQTT、HTTPS协议将业务数据上报到平台,平台也可以将控制命令下发给设备。

业务应用通过调用物联网平台提供的API,实现设备数据采集、命令下发、设备管理等业务场景。

3.2 开通物联网服务

地址: https://www.huaweicloud.com/product/iothub.html

开通标准版免费单元。

开通之后,点击总览,查看接入信息。 我们当前设备准备采用MQTT协议接入华为云平台,这里可以看到MQTT协议的地址和端口号等信息。

总结:

cpp 复制代码
端口号:   MQTT (1883)| MQTTS (8883)	
接入地址: a3433ab133.iot-mqtts.cn-north-4.myhuaweicloud.com

根据域名地址得到IP地址信息:

cpp 复制代码
Microsoft Windows [版本 10.0.19044.2846]
(c) Microsoft Corporation。保留所有权利。

C:\Users\11266>ping a3433ab133.iot-mqtts.cn-north-4.myhuaweicloud.com

正在 Ping a3433ab133.iot-mqtts.cn-north-4.myhuaweicloud.com [121.36.42.100] 具有 32 字节的数据:
来自 121.36.42.100 的回复: 字节=32 时间=37ms TTL=31
来自 121.36.42.100 的回复: 字节=32 时间=37ms TTL=31
来自 121.36.42.100 的回复: 字节=32 时间=36ms TTL=31
来自 121.36.42.100 的回复: 字节=32 时间=37ms TTL=31

121.36.42.100 的 Ping 统计信息:
    数据包: 已发送 = 4,已接收 = 4,丢失 = 0 (0% 丢失),
往返行程的估计时间(以毫秒为单位):
    最短 = 36ms,最长 = 37ms,平均 = 36ms

C:\Users\11266>

MQTT协议接入端口号有两个,1883是非加密端口,8883是证书加密端口,单片机无法加载证书,所以使用1883端口比较合适。 接下来的ESP8266就采用1883端口连接华为云物联网平台。

3.3 创建产品

(1)创建产品

点击右上角创建产品。

(2)填写产品信息

根据自己产品名字填写,设备类型选择自定义类型。

(3)添加自定义模型

产品创建完成之后,点击进入产品详情页面,翻到最下面可以看到模型定义。

模型简单来说: 就是存放设备上传到云平台的数据。比如:环境温度、环境湿度、环境烟雾浓度、火焰检测状态图等等,这些我们都可以单独创建一个模型保存。

3.4 添加设备

产品是属于上层的抽象模型,接下来在产品模型下添加实际的设备。添加的设备最终需要与真实的设备关联在一起,完成数据交互。

(1)注册设备

点击右上角注册设备。

(2)根据自己的设备填写

在弹出的对话框里填写自己设备的信息。根据自己设备详细情况填写。

(3)保存设备信息

创建完毕之后,点击保存并关闭,得到创建的设备密匙信息。该信息在后续生成MQTT三元组的时候需要使用。

比如我当前设备的信息如下:

cpp 复制代码
{
    "device_id": "64000697352830580e48df07_dev1",
    "secret": "12345678"
}

3.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

对于设备而言,一般会订阅平台下发消息给设备 这个主题。

设备想接收平台下发的消息,就需要订阅平台下发消息给设备 的主题,订阅后,平台下发消息给设备,设备就会收到消息。

比如:我创建的设备信息如下

cpp 复制代码
以当前设备为例,最终订阅主题的格式如下:
$oc/devices/{device_id}/sys/messages/down
    
最终的格式:
$oc/devices/64000697352830580e48df07_dev1/sys/messages/down

(4)主题发布格式

对于设备来说,主题发布表示向云平台上传数据,将最新的传感器数据,设备状态上传到云平台。

这个操作称为:属性上报。

帮助文档地址:https://support.huaweicloud.com/usermanual-iothub/iot_06_v5_3010.html

根据帮助文档的介绍, 当前设备发布主题,上报属性的格式总结如下:

cpp 复制代码
发布的主题格式:
$oc/devices/{device_id}/sys/properties/report
 
最终的格式:
$oc/devices/64000697352830580e48df07_dev1/sys/properties/report
发布主题时,需要上传数据,这个数据格式是JSON格式。

上传的JSON数据格式如下:

{
  "services": [
    {
      "service_id": <填服务ID>,
      "properties": {
        "<填属性名称1>": <填属性值>,
        "<填属性名称2>": <填属性值>,
        ..........
      }
    }
  ]
}
根据JSON格式,一次可以上传多个属性字段。 这个JSON格式里的,服务ID,属性字段名称,属性值类型,在前面创建产品的时候就已经介绍了,不记得可以翻到前面去查看。

//Up, Down, Left, Right, Stop
    
根据这个格式,组合一次上传的属性数据:
{"services": [{"service_id": "stm32","properties":{"Up":1,"Down":1,"Left":1,"Right":1,"Stop":1}}]}

3.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地址就是域名解析得到的)

cpp 复制代码
华为云的MQTT服务器地址:117.78.5.125
华为云的MQTT端口号:1883

(2)生成MQTT三元组

华为云提供了一个在线工具,用来生成MQTT鉴权三元组: https://iot-tool.obs-website.cn-north-4.myhuaweicloud.com/

打开这个工具,填入设备的信息(也就是刚才创建完设备之后保存的信息),点击生成,就可以得到MQTT的登录信息了。

下面是打开的页面:

填入设备的信息: (上面两行就是设备创建完成之后保存得到的)

直接得到三元组信息。

得到三元组之后,设备端通过MQTT协议登录鉴权的时候,填入参数即可。

cpp 复制代码
DeviceId      64000697352830580e48df07_dev1
DeviceSecret  12345678
ClientId      64000697352830580e48df07_dev1_0_0_2023030206
Username      64000697352830580e48df07_dev1
Password      a695af9883c5d0e3817bc6971beeecadf8c7c595677c461b1fe75882ed2bf449

3.7 模拟设备登录测试

经过上面的步骤介绍,已经创建了产品,设备,数据模型,得到MQTT登录信息。 接下来就用MQTT客户端软件模拟真实的设备来登录平台。测试与服务器通信是否正常。

(1)填入登录信息

打开MQTT客户端软件,对号填入相关信息(就是上面的文本介绍)。然后,点击登录,订阅主题,发布主题。

(2)打开网页查看

完成上面的操作之后,打开华为云网页后台,可以看到设备已经在线了。

到此,云平台的部署已经完成,设备已经可以正常上传数据了。

(3)MQTT登录测试参数总结

cpp 复制代码
IP地址:117.78.5.125
端口号:1883
DeviceId      64000697352830580e48df07_dev1
DeviceSecret  12345678
ClientId      64000697352830580e48df07_dev1_0_0_2023030206
Username      64000697352830580e48df07_dev1
Password      a695af9883c5d0e3817bc6971beeecadf8c7c595677c461b1fe75882ed2bf449
订阅主题:$oc/devices/64000697352830580e48df07_dev1/sys/messages/down
发布主题:$oc/devices/64000697352830580e48df07_dev1/sys/properties/report
发布的消息:{"services": [{"service_id": "stm32","properties":{"Up":1,"Down":1,"Left":1,"Right":1,"Stop":1}}]}

四、Android手机APP开发

4.1 开发环境介绍

在当前项目中,用于远程遥控安卓小车的Android手机APP是基于Qt框架开发的。Qt是一个功能强大且高度灵活的跨平台应用程序开发框架,特别适合构建具有丰富图形用户界面(GUI)的应用程序,同时也支持开发非GUI程序。在开发这款远程遥控APP时,Qt的优势在于其跨平台性,使得同一份代码可以轻松部署在不同操作系统平台上,包括Android。

cpp 复制代码
Android开发必备的工具链包括:Java JDK  、Android SDK 、Android NDK。

NDK下载地址:https://dl.google.com/android/repository/android-ndk-r19c-linux-x86_64.zip
SDK下载: https://www.androiddevtools.cn/
JDK下载地址:https://www.oracle.com/java/technologies/javase-jdk8-downloads.html

4.2 ffmpeg介绍

说起ffmpeg,只要是搞音视频相关的开发应该都是听过的。FFmpeg提供了非常先进的音频/视频编解码库,并且支持跨平台。

现在互联网上ffmpeg相关的文章、教程也非常的多,ffmpeg本身主要是用来对视频、音频进行解码、编码,对音视频进行处理。

其中主要是解码和编码。 解码的应用主要是视频播放器制作、音乐播放器制作,解码视频文件得到视频画面再渲染显示出来就是播放器的基本模型了。 编码主要是用于视频录制保存,就是将摄像头的画面或者屏幕的画面编码后写入文件保存为视频,比如:行车记录仪录制视频,监控摄像头录制视频等等。 当然也可以编码推流到服务器,现在的直播平台、智能家居里的视频监控、智能安防摄像头都是这样的应用。

在本项目里,通过ffmpeg技术将手机采集的视频图像编码后,推流到搭建好的流媒体服务器,实现远程监控。

4.3 Linux下编译安装ffmpeg

(1)安装依赖项:

复制代码
sudo apt-get update
sudo apt-get install build-essential nasm yasm cmake libx264-dev libx265-dev libvpx-dev libfdk-aac-dev libmp3lame-dev libopus-dev libssl-dev

(2)下载FFmpeg源码:

复制代码
wget https://ffmpeg.org/releases/ffmpeg-4.2.2.tar.gz
tar -zxvf ffmpeg-x.y.z.tar.gz
cd ffmpeg-x.y.z

注意替换 4.2.2 为实际的版本号。

(3)配置编译选项:

复制代码
./configure --enable-gpl --enable-libx264 --enable-libx265 --enable-libvpx --enable-libfdk-aac --enable-libmp3lame --enable-libopus --enable-nonfree

如果需要其他编码器或功能,可以根据需要添加或修改配置选项。

(4)编译和安装:

复制代码
make -j$(nproc)
sudo make install

-j$(nproc) 表示使用多个CPU核心进行并行编译,可以根据实际情况调整。

(5)完成后,可以通过运行以下命令来验证FFmpeg是否正确安装:

复制代码
ffmpeg -version

这样就完成了在Linux下编译FFmpeg源码的过程。

4.4 Qt摄像头采集

下面代码实现,通过子线程采集摄像头画面,并通过信号槽机制将图像传递给主线程显示。

cpp 复制代码
// mainwindow.h

#ifndef MAINWINDOW_H
#define MAINWINDOW_H

#include <QMainWindow>
#include <QCamera>
#include <QCameraViewfinder>
#include <QCameraImageCapture>
#include <QThread>

class CameraThread;

namespace Ui {
class MainWindow;
}

class MainWindow : public QMainWindow
{
    Q_OBJECT

public:
    explicit MainWindow(QWidget *parent = nullptr);
    ~MainWindow();

private slots:
    void on_pushButton_start_clicked();
    void on_pushButton_stop_clicked();
    void onNewImageAvailable(const QImage &image);

private:
    Ui::MainWindow *ui;
    QCamera *camera;
    QCameraViewfinder *viewfinder;
    QCameraImageCapture *imageCapture;
    CameraThread *cameraThread;
};

#endif // MAINWINDOW_H


// mainwindow.cpp

#include "mainwindow.h"
#include "ui_mainwindow.h"
#include <QThread>

class CameraThread : public QThread
{
    Q_OBJECT

public:
    explicit CameraThread(QObject *parent = nullptr);

signals:
    void newImageAvailable(const QImage &image);

protected:
    void run() override;

private:
    QCamera *camera;
    QCameraImageCapture *imageCapture;
};

CameraThread::CameraThread(QObject *parent) : QThread(parent)
{
    camera = new QCamera(this);
    imageCapture = new QCameraImageCapture(camera, this);
}

void CameraThread::run()
{
    camera->setCaptureMode(QCamera::CaptureStillImage);
    camera->start();

    connect(imageCapture, &QCameraImageCapture::imageCaptured, this, [&](int id, const QImage &preview) {
        emit newImageAvailable(preview);
    });

    exec();
}

MainWindow::MainWindow(QWidget *parent) :
    QMainWindow(parent),
    ui(new Ui::MainWindow)
{
    ui->setupUi(this);

    camera = new QCamera(this);
    viewfinder = new QCameraViewfinder(this);
    imageCapture = new QCameraImageCapture(camera, this);
    cameraThread = new CameraThread(this);

    cameraThread->start();

    connect(cameraThread, &CameraThread::newImageAvailable, this, &MainWindow::onNewImageAvailable);
}

MainWindow::~MainWindow()
{
    delete ui;
}

void MainWindow::on_pushButton_start_clicked()
{
    camera->setViewfinder(viewfinder);
    camera->start();
    ui->verticalLayout->addWidget(viewfinder);
}

void MainWindow::on_pushButton_stop_clicked()
{
    camera->stop();
    ui->verticalLayout->removeWidget(viewfinder);
    viewfinder->deleteLater();
}

void MainWindow::onNewImageAvailable(const QImage &image)
{
    // 在这里处理接收到的图像,例如将其显示在 QLabel 上
    ui->label_image->setPixmap(QPixmap::fromImage(image));
}

// main.cpp

#include "mainwindow.h"
#include <QApplication>

int main(int argc, char *argv[])
{
    QApplication a(argc, argv);
    MainWindow w;
    w.show();
    return a.exec();
}

(1)CameraThread 类继承自 QThread,在子线程中负责采集摄像头画面。在 run() 方法中,首先创建了一个 QCamera 对象和一个 QCameraImageCapture 对象,然后设置摄像头的捕获模式为静态图像,启动摄像头。通过连接 imageCaptureimageCaptured 信号到 lambda 函数,当捕获到新图像时,将图像通过自定义信号 newImageAvailable 发送出去。

(2)MainWindow 类是程序的主窗口,其中包含了摄像头的视图控件、开始按钮、停止按钮以及用于显示图像的标签。在构造函数中,创建了摄像头对象、视图控件对象、图像捕获对象,并创建了一个 CameraThread 对象作为子线程来处理摄像头画面的采集。

(3)当用户点击开始按钮时,调用 on_pushButton_start_clicked() 槽函数,将摄像头视图控件添加到界面上并启动摄像头,开始显示摄像头画面。

(4)当用户点击停止按钮时,调用 on_pushButton_stop_clicked() 槽函数,停止摄像头捕获并移除视图控件。

(5)当子线程采集到新的图像时,通过 onNewImageAvailable() 槽函数接收到图像,并在标签上显示该图像。

(6)main.cpp 文件是程序的入口,创建了 QApplication 对象和 MainWindow 对象,并执行主事件循环。

4.6 ffmpeg视频编码推流代码

使用Qt(C++)结合FFmpeg库来采集摄像头画面,进行编码,并通过子线程将视频推送到RTMP流媒体服务器。

cpp 复制代码
// mainwindow.h

#ifndef MAINWINDOW_H
#define MAINWINDOW_H

#include <QMainWindow>
#include <QThread>

extern "C" {
#include <libavformat/avformat.h>
#include <libavdevice/avdevice.h>
#include <libavcodec/avcodec.h>
#include <libswscale/swscale.h>
}

class VideoCaptureThread;

namespace Ui {
class MainWindow;
}

class MainWindow : public QMainWindow
{
    Q_OBJECT

public:
    explicit MainWindow(QWidget *parent = nullptr);
    ~MainWindow();

private slots:
    void on_pushButton_start_clicked();
    void on_pushButton_stop_clicked();
    void onNewFrameAvailable(const QImage &frame);

private:
    Ui::MainWindow *ui;
    VideoCaptureThread *videoCaptureThread;
};

#endif // MAINWINDOW_H


// mainwindow.cpp

#include "mainwindow.h"
#include "ui_mainwindow.h"

class VideoCaptureThread : public QThread
{
    Q_OBJECT

public:
    explicit VideoCaptureThread(QObject *parent = nullptr);
    ~VideoCaptureThread();

protected:
    void run() override;

signals:
    void newFrameAvailable(const QImage &frame);

private:
    AVFormatContext *formatContext;
    AVCodecContext *codecContext;
    AVStream *videoStream;
    SwsContext *swsContext;
    bool stop;
};

VideoCaptureThread::VideoCaptureThread(QObject *parent) : QThread(parent), stop(false)
{
    avformat_network_init();

    formatContext = avformat_alloc_context();
    AVInputFormat *inputFormat = av_find_input_format("dshow");
    avformat_open_input(&formatContext, "video=YourCameraDevice", inputFormat, NULL);

    // 省略了初始化视频编码器的部分,需要根据实际情况添加

    swsContext = sws_getContext(codecContext->width, codecContext->height, codecContext->pix_fmt,
                                codecContext->width, codecContext->height, AV_PIX_FMT_RGB32,
                                SWS_BICUBIC, NULL, NULL, NULL);
}

VideoCaptureThread::~VideoCaptureThread()
{
    stop = true;
    wait();

    sws_freeContext(swsContext);
    avcodec_free_context(&codecContext);
    avformat_close_input(&formatContext);
    avformat_free_context(formatContext);
}

void VideoCaptureThread::run()
{
    while (!stop) {
        AVPacket packet;
        av_init_packet(&packet);
        if (av_read_frame(formatContext, &packet) >= 0) {
            // 省略了视频编码的部分,需要根据实际情况添加

            QImage frameImage(codecContext->width, codecContext->height, QImage::Format_RGB32);
            sws_scale(swsContext, codecContext->coded_frame->data, codecContext->coded_frame->linesize,
                      0, codecContext->height, reinterpret_cast<uint8_t **>(frameImage.bits()), frameImage.bytesPerLine());

            emit newFrameAvailable(frameImage);
        }
        av_packet_unref(&packet);
    }
}

MainWindow::MainWindow(QWidget *parent) :
    QMainWindow(parent),
    ui(new Ui::MainWindow)
{
    ui->setupUi(this);
    videoCaptureThread = new VideoCaptureThread(this);
    connect(videoCaptureThread, &VideoCaptureThread::newFrameAvailable, this, &MainWindow::onNewFrameAvailable);
}

MainWindow::~MainWindow()
{
    delete ui;
}

void MainWindow::on_pushButton_start_clicked()
{
    videoCaptureThread->start();
}

void MainWindow::on_pushButton_stop_clicked()
{
    videoCaptureThread->quit();
}

void MainWindow::onNewFrameAvailable(const QImage &frame)
{
    // 将QImage转换为AVFrame
    AVFrame *avFrame = av_frame_alloc();
    avFrame->width = frame.width();
    avFrame->height = frame.height();
    avFrame->format = AV_PIX_FMT_RGB32;
    av_frame_get_buffer(avFrame, 0);
    for (int y = 0; y < frame.height(); ++y) {
        memcpy(avFrame->data[0] + y * avFrame->linesize[0], frame.scanLine(y), frame.width() * 4);
    }

    // 编码视频帧
    AVPacket packet;
    av_init_packet(&packet);
    int ret = avcodec_send_frame(codecContext, avFrame);
    if (ret < 0) {
        qDebug() << "Failed to send frame to encoder";
        return;
    }
    while (ret >= 0) {
        ret = avcodec_receive_packet(codecContext, &packet);
        if (ret == AVERROR(EAGAIN) || ret == AVERROR_EOF) {
            break;
        } else if (ret < 0) {
            qDebug() << "Error during encoding";
            return;
        }

        // 推送到RTMP服务器
        RTMP *rtmp = RTMP_Alloc();
        RTMP_Init(rtmp);
        if (!RTMP_SetupURL(rtmp, "rtmp://your_rtmp_server_url")) {
            qDebug() << "Failed to set RTMP URL";
            RTMP_Close(rtmp);
            RTMP_Free(rtmp);
            return;
        }
        RTMP_EnableWrite(rtmp);
        if (!RTMP_Connect(rtmp, NULL)) {
            qDebug() << "Failed to connect to RTMP server";
            RTMP_Close(rtmp);
            RTMP_Free(rtmp);
            return;
        }
        if (!RTMP_ConnectStream(rtmp, 0)) {
            qDebug() << "Failed to connect to RTMP stream";
            RTMP_Close(rtmp);
            RTMP_Free(rtmp);
            return;
        }

        packet.stream_index = videoStream->index;
        ret = RTMP_SendPacket(rtmp, reinterpret_cast<char*>(packet.data), packet.size, TRUE);
        if (ret < 0) {
            qDebug() << "Failed to send packet to RTMP server";
            RTMP_Close(rtmp);
            RTMP_Free(rtmp);
            return;
        }

        av_packet_unref(&packet);
        RTMP_Close(rtmp);
        RTMP_Free(rtmp);
    }

    av_frame_free(&avFrame);
}

代码中创建了一个 VideoCaptureThread 类作为子线程,负责采集摄像头画面并进行视频编码。在 run() 方法中,利用FFmpeg库读取摄像头画面,进行视频编码,并通过自定义信号 newFrameAvailable 发送每一帧图像。主界面中的 MainWindow 类负责开始和停止视频采集线程,并处理接收到的视频帧,可以在 onNewFrameAvailable() 槽函数中将视频帧编码为RTMP流并推送到服务器。将QImage转换为AVFrame,使用avcodec_send_frameavcodec_receive_packet函数对视频帧进行编码。创建一个RTMP连接,并将编码后的视频包发送到RTMP服务器。

五、STM32端代码设计

STM32端的代码主要是控制小车的移动,代码比较少。

5.1 STM32小车底座驱动代码

cpp 复制代码
#include "stm32f10x.h"

#define MOTOR1_PIN1 GPIO_Pin_0
#define MOTOR1_PIN2 GPIO_Pin_1
#define MOTOR2_PIN1 GPIO_Pin_2
#define MOTOR2_PIN2 GPIO_Pin_3
#define MOTOR3_PIN1 GPIO_Pin_4
#define MOTOR3_PIN2 GPIO_Pin_5
#define MOTOR4_PIN1 GPIO_Pin_6
#define MOTOR4_PIN2 GPIO_Pin_7

void delay_ms(uint32_t ms) {
    uint32_t i, j;
    for (i = 0; i < ms; i++)
        for (j = 0; j < 7200; j++);
}

void motor_init() {
    GPIO_InitTypeDef GPIO_InitStructure;
    
    RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);
    
    GPIO_InitStructure.GPIO_Pin = MOTOR1_PIN1 | MOTOR1_PIN2 | MOTOR2_PIN1 | MOTOR2_PIN2 | MOTOR3_PIN1 | MOTOR3_PIN2 | MOTOR4_PIN1 | MOTOR4_PIN2;
    GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP;
    GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
    
    GPIO_Init(GPIOA, &GPIO_InitStructure);
}

void forward() {
    GPIO_ResetBits(GPIOA, MOTOR1_PIN2 | MOTOR2_PIN2 | MOTOR3_PIN2 | MOTOR4_PIN2);
    GPIO_SetBits(GPIOA, MOTOR1_PIN1 | MOTOR2_PIN1 | MOTOR3_PIN1 | MOTOR4_PIN1);
}

void backward() {
    GPIO_ResetBits(GPIOA, MOTOR1_PIN1 | MOTOR2_PIN1 | MOTOR3_PIN1 | MOTOR4_PIN1);
    GPIO_SetBits(GPIOA, MOTOR1_PIN2 | MOTOR2_PIN2 | MOTOR3_PIN2 | MOTOR4_PIN2);
}

void left() {
    GPIO_ResetBits(GPIOA, MOTOR1_PIN2 | MOTOR2_PIN1 | MOTOR3_PIN1 | MOTOR4_PIN2);
    GPIO_SetBits(GPIOA, MOTOR1_PIN1 | MOTOR2_PIN2 | MOTOR3_PIN2 | MOTOR4_PIN1);
}

void right() {
    GPIO_ResetBits(GPIOA, MOTOR1_PIN1 | MOTOR2_PIN2 | MOTOR3_PIN2 | MOTOR4_PIN1);
    GPIO_SetBits(GPIOA, MOTOR1_PIN2 | MOTOR2_PIN1 | MOTOR3_PIN1 | MOTOR4_PIN2);
}

int main(void) {
    motor_init();
    
    while (1) {
        forward();
        delay_ms(1000);
        
        backward();
        delay_ms(1000);
        
        left();
        delay_ms(1000);
        
        right();
        delay_ms(1000);
    }
}

5.2 小车控制代码

cpp 复制代码
#include "stm32f10x.h"
#include <stdio.h>
#include <string.h>

#define MOTOR1_PIN1 GPIO_Pin_0
#define MOTOR1_PIN2 GPIO_Pin_1
#define MOTOR2_PIN1 GPIO_Pin_2
#define MOTOR2_PIN2 GPIO_Pin_3
#define MOTOR3_PIN1 GPIO_Pin_4
#define MOTOR3_PIN2 GPIO_Pin_5
#define MOTOR4_PIN1 GPIO_Pin_6
#define MOTOR4_PIN2 GPIO_Pin_7

void delay_ms(uint32_t ms) {
    uint32_t i, j;
    for (i = 0; i < ms; i++)
        for (j = 0; j < 7200; j++);
}

void motor_init() {
    GPIO_InitTypeDef GPIO_InitStructure;
    
    RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);
    
    GPIO_InitStructure.GPIO_Pin = MOTOR1_PIN1 | MOTOR1_PIN2 | MOTOR2_PIN1 | MOTOR2_PIN2 | MOTOR3_PIN1 | MOTOR3_PIN2 | MOTOR4_PIN1 | MOTOR4_PIN2;
    GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP;
    GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
    
    GPIO_Init(GPIOA, &GPIO_InitStructure);
}

void forward() {
    GPIO_ResetBits(GPIOA, MOTOR1_PIN2 | MOTOR2_PIN2 | MOTOR3_PIN2 | MOTOR4_PIN2);
    GPIO_SetBits(GPIOA, MOTOR1_PIN1 | MOTOR2_PIN1 | MOTOR3_PIN1 | MOTOR4_PIN1);
}

void backward() {
    GPIO_ResetBits(GPIOA, MOTOR1_PIN1 | MOTOR2_PIN1 | MOTOR3_PIN1 | MOTOR4_PIN1);
    GPIO_SetBits(GPIOA, MOTOR1_PIN2 | MOTOR2_PIN2 | MOTOR3_PIN2 | MOTOR4_PIN2);
}

void left() {
    GPIO_ResetBits(GPIOA, MOTOR1_PIN2 | MOTOR2_PIN1 | MOTOR3_PIN1 | MOTOR4_PIN2);
    GPIO_SetBits(GPIOA, MOTOR1_PIN1 | MOTOR2_PIN2 | MOTOR3_PIN2 | MOTOR4_PIN1);
}

void right() {
    GPIO_ResetBits(GPIOA, MOTOR1_PIN1 | MOTOR2_PIN2 | MOTOR3_PIN2 | MOTOR4_PIN1);
    GPIO_SetBits(GPIOA, MOTOR1_PIN2 | MOTOR2_PIN1 | MOTOR3_PIN1 | MOTOR4_PIN2);
}

void usart_init() {
    USART_InitTypeDef USART_InitStructure;
    GPIO_InitTypeDef GPIO_InitStructure;

    RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1 | RCC_APB2Periph_GPIOA, ENABLE);

    USART_InitStructure.USART_BaudRate = 115200;
    USART_InitStructure.USART_WordLength = USART_WordLength_8b;
    USART_InitStructure.USART_StopBits = USART_StopBits_1;
    USART_InitStructure.USART_Parity = USART_Parity_No;
    USART_InitStructure.USART_HardwareFlowControl = USART_HardwareFlowControl_None;
    USART_InitStructure.USART_Mode = USART_Mode_Rx | USART_Mode_Tx;

    USART_Init(USART1, &USART_InitStructure);
    USART_Cmd(USART1, ENABLE);

    GPIO_InitStructure.GPIO_Pin = GPIO_Pin_9;
    GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;
    GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
    GPIO_Init(GPIOA, &GPIO_InitStructure);

    GPIO_InitStructure.GPIO_Pin = GPIO_Pin_10;
    GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING;
    GPIO_Init(GPIOA, &GPIO_InitStructure);
}

void usart_send(USART_TypeDef* USARTx, uint8_t data) {
    while (USART_GetFlagStatus(USARTx, USART_FLAG_TXE) == RESET);
    USART_SendData(USARTx, data);
}

uint8_t usart_receive(USART_TypeDef* USARTx) {
    while (USART_GetFlagStatus(USARTx, USART_FLAG_RXNE) == RESET);
    return USART_ReceiveData(USARTx);
}

void usart_puts(USART_TypeDef* USARTx, char* str) {
    while (*str) {
        usart_send(USARTx, *str++);
    }
}

void control(char* cmd) {
    if (strcmp(cmd, "forward") == 0) {
        forward();
        usart_puts(USART1, "OK\n");
    } else if (strcmp(cmd, "backward") == 0) {
        backward();
        usart_puts(USART1, "OK\n");
    } else if (strcmp(cmd, "left") == 0) {
        left();
        usart_puts(USART1, "OK\n");
    } else if (strcmp(cmd, "right") == 0) {
        right();
        usart_puts(USART1, "OK\n");
    } else {
        usart_puts(USART1, "Invalid command\n");
    }
}

int main(void) {
    motor_init();
    usart_init();
    
    while (1) {
        char cmd[10];
        memset(cmd, 0, sizeof(cmd));
        int i = 0;
        while (1) {
            char c = usart_receive(USART1);
            if (c == '\r' || c == '\n') {
                break;
            }
            cmd[i++] = c;
        }
        control(cmd);
    }
}

六、关于Android手机USB通信的问题

在Qt中开发Android手机APP并利用USB线进行串口通信,需要启用权限。

(1)添加权限:在AndroidManifest.xml文件中添加USB权限,并在Qt项目中的Android配置文件中声明需要的权限。例如,在AndroidManifest.xml中添加以下代码:

xml 复制代码
<uses-permission android:name="android.permission.USB_PERMISSION" />

(2)检测USB连接:通过Qt的Android JNI接口(Java Native Interface)来检测USB设备的插拔状态,并获取USB设备的信息。

(3)打开和关闭USB串口:使用Qt的QSerialPort类来打开和关闭USB串口,并进行数据的读写操作。可以通过检测到的USB设备路径来打开对应的串口。

(4)处理串口数据:接收到的串口数据可以通过信号槽机制或者其他方式传递给界面进行显示或进一步处理。

下面是测试的代码:

cpp 复制代码
#include <QSerialPort>
#include <QSerialPortInfo>

void detectUsbDevices() {
    QList<QSerialPortInfo> usbDevices = QSerialPortInfo::availablePorts();
    
    foreach (const QSerialPortInfo &info, usbDevices) {
        qDebug() << "USB Device Name: " << info.portName();
        qDebug() << "Description: " << info.description();
    }
}

void openUsbSerialPort(const QString &portName) {
    QSerialPort serialPort;
    serialPort.setPortName(portName);
    serialPort.setBaudRate(QSerialPort::Baud9600);
    
    if (serialPort.open(QIODevice::ReadWrite)) {
        qDebug() << "USB Serial Port opened successfully!";
        
        // Read or write data here
        
        serialPort.close();
    } else {
        qDebug() << "Failed to open USB Serial Port!";
    }
}

int main(int argc, char *argv[]) {
    QApplication app(argc, argv);
    
    // Detect USB devices
    detectUsbDevices();
    
    // Open USB serial port
    openUsbSerialPort("/dev/ttyUSB0"); // Replace with the actual port name
    
    return app.exec();
}

七、总结

本文详细介绍了一款创新且环保的基于4G网络设计的远程遥控安卓小车系统的开发与实现过程。该项目巧妙地将被淘汰的安卓旧手机升级转化为车载信息处理单元,赋予其新的生命力,同时融入先进的4G网络技术、流媒体服务以及物联网技术,成功打造出一个集远程操控与实时音视频传输功能于一身的高效率解决方案。

该智能小车以其独特的设计思路和强大的功能特性,展现出广泛的应用潜力。无论是作为教育科研领域的实践平台,还是在远程监控、工业巡检、应急救援、无人驾驶技术验证,甚至智能家居与物流等方面,均能发挥重要作用,显著提升了工作效率,降低了人力成本,并有效保障了作业的安全性。

该项目积极响应可持续发展号召,通过资源循环利用,成功展示了科技如何助力环保,彰显了技术创新的社会价值。展望未来,随着5G网络技术的广泛应用,这款基于4G网络的远程遥控安卓小车将进一步优化性能,拓展应用场景,为社会各领域带来更加智能化、便捷化的技术服务。

相关推荐
aaajj28 分钟前
android contentprovider及其查看
android
fundroid6 小时前
Android Studio + Gemini:重塑安卓 AI 开发新范式
android·android studio·ai编程
vortex57 小时前
谷歌黑客语法挖掘 SQL 注入漏洞
android·数据库·sql
-指短琴长-10 小时前
MySQL快速入门——基本查询(下)
android·mysql·adb
stevenzqzq11 小时前
android lambda回调
android
林北北的霸霸13 小时前
django初识与安装
android·mysql·adb
Java 码农15 小时前
MySQL EXPLAIN 详解与优化指南
android·mysql·adb
nenchoumi311918 小时前
ROS2 Humble 笔记(七)标准与自定义 Interface 接口
笔记·机器人·ros2
stevenzqzq19 小时前
Android Hilt 入门教程_传统写法和Hilt写法的比较
android
wuwu_q19 小时前
用通俗易懂方式,详细讲讲 Kotlin Flow 中的 map 操作符
android·开发语言·kotlin