自制APP连接OneNET---实现数据监控和下发控制(HTTP)

Posted ⁽⁽ଘ晴空万里ଓ⁾⁾

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了自制APP连接OneNET---实现数据监控和下发控制(HTTP)相关的知识,希望对你有一定的参考价值。

文章目录


前言

本案例主要是实现使用自制的手机APP对OneNET云平台上的数据查看和下发命令控制单片机上的小灯(PC13)亮灭,使用的HTTP协议接入OneNET,APP开发使用的是E4A中文安卓编程。


一、前期准备

1、STM32F103C8T6最小系统板
2、ESP8266-01S模块
3、在OneNET平台上创建HTTP协议的产品和设备
4、安装E4A软件,软件不大几百兆下载链接

二、功能介绍

1、自制APP页面展示

(1)设备ID和Master-APIkey要更改为自己的
(2)查询数据流:可以查询两个数据流,点击查询后,每3S向平台获取一次数据。数据流的名字可以更改以获取不同的数据流的数据
(3)新增数据流:上传一个名为Temp数据流到OneNET平台,分别是数据流名字和数值
(4)删除数据流:删除名为TEST的数据流
(5)历史记录:查看数据流Temp从某日期某时间开始的20条数据
(6)更新触发器:要现在OneNET平台上设置触发器,第一个参数是触发器ID,当数据流Temp的值大于100时就会进通知用户,我这里使用的是邮件通知。
(7)开灯:打开和关闭板子上的小灯亮灭

视频演示:
上传后的视频有点模糊,将就着看吧。

自制APP连接OneNET


视频截图:
左边是OneNET云平台上面的数据流面板,中间是自制的APP,右边是串口助手接32单片机。

三、使用SSCOM串口助手连接OneNET

这部分是让大家快速了解本案例的核心内容,主要就是OneNET平台上的HTTP请求方式GET/POST的使用。

1、串口助手配置

设置端口号为TCPClient
OneNET的HTTP服务器的IP地址为:183.230.40.33 端口号为:80

依次次发送如下指令

2、新增数据流

在OneNET平台指定设备ID下新增三个数据流,分别是Temp,Humi,KEY,后面跟着的是数据流的值,
24是数据包(,;Temp,26;Humi,60;KEY,1;)的长度大小
api-key:=08b33=G=KP8qxRoCYYrurN41yk=s 是Master-APIkey,可以在OneNET平台上获取

//新增数据流
POST http://api.heclouds.com/devices/699063374/datapoints?type=5 HTTP/1.1
api-key:=08b33=G=KP8qxRoCYYrurN41yk=
Host:api.heclouds.com
Content-Length:24

,;Temp,26;Humi,60;KEY,1;

服务器返回信息

HTTP/1.1 200 OK
Date: Sat, 14 May 2022 14:15:48 GMT
Content-Type: application/json
Content-Length: 26
Connection: keep-alive
Server: Apache-Coyote/1.1
Pragma: no-cache

"errno":0,"error":"succ"

3、查询数据流

查询OneNET上面特定设备ID下的名为Temp和Humi的数据流,查询多个数据流用逗号隔开

//查询多个数据流
GET http://api.heclouds.com/devices/699063374/datapoints?datastream_id=Temp,Humi HTTP/1.1
api-key:=08b33=G=KP8qxRoCYYrurN41yk=
Host:api.heclouds.com

(后面两个回车)

服务器返回信息

HTTP/1.1 200 OK
Date: Sat, 14 May 2022 14:21:11 GMT
Content-Type: application/json
Content-Length: 211
Connection: keep-alive
Server: Apache-Coyote/1.1
Pragma: no-cache

"errno":0,"data":"count":2,"datastreams":["datapoints":["at":"2022-05-14 22:15:48.830","value":"26"],"id":"Temp","datapoints":["at":"2022-05-14 22:15:48.833","value":"60"],"id":"Humi"],"error":"succ"

4、删除数据流

删除OneNET平台上指定设备ID中名为Humi的数据流

DELETE http://api.heclouds.com/devices/699063374/datastreams/Humi HTTP/1.1
api-key:=08b33=G=KP8qxRoCYYrurN41yk=
Host:api.heclouds.com

(后面两个回车)

服务器返回信息

HTTP/1.1 200 OK
Date: Sat, 14 May 2022 14:26:09 GMT
Content-Type: application/json
Content-Length: 26
Connection: keep-alive
Server: Apache-Coyote/1.1
Pragma: no-cache

"errno":0,"error":"succ"

5、查询历史数据

请求指定设备从2022年5月14日零点以来,数据流Temp的第1至第5条数据

GET http://api.heclouds.com/devices/699063374/datapoints?datastream_id=Temp&start=2022-05-14T00:00:00&limit=5 HTTP/1.1
api-key:=08b33=G=KP8qxRoCYYrurN41yk=
Host:api.heclouds.com

(后面两个回车)

服务器返回信息

HTTP/1.1 200 OK
Date: Sat, 14 May 2022 14:31:35 GMT
Content-Type: application/json
Content-Length: 362
Connection: keep-alive
Server: Apache-Coyote/1.1
Pragma: no-cache

"errno":0,"data":"cursor":"411019_699063374_1652462674000","count":5,"datastreams":["datapoints":["at":"2022-05-14 00:44:33.379","value":"11","at":"2022-05-14 00:56:30.122","value":"91","at":"2022-05-14 01:08:23.547","value":"91","at":"2022-05-14 01:11:02.599","value":"91","at":"2022-05-14 01:14:34.183","value":"81"],"id":"Temp"],"error":"succ"

6、更新触发器

设置数据流Temp的数值大于100时通知用户

PUT http://api.heclouds.com/triggers/1596927 HTTP/1.1
api-key:=08b33=G=KP8qxRoCYYrurN41yk=
Host: api.heclouds.com
Content-Length:43

"ds_id":"Temp","type":">","threshold":100

服务器返回信息

HTTP/1.1 200 OK
Date: Sat, 14 May 2022 14:33:49 GMT
Content-Type: application/json
Content-Length: 26
Connection: keep-alive
Server: Apache-Coyote/1.1
Pragma: no-cache

"errno":0,"error":"succ"

以上就是本案例用到是指令,更多详细内容可以看OneNET的开发文档点击链接进入

四、STM32功能实现

这部分主要做的是完成上传数据到OneNET平台和获取数据从OneNET平台

1、修改OneNET官方提供的例程

将例程修改成适合自己的板子、完成数据上传的功能即可,我用的是C8T6板子。


数据包的打包处理
我的数据打包形式如下,由于没有sht20温湿度传感器,这里我的温湿度使用了全局变量进行自增处理。这个函数的功能是将需要上传的数据打包成一定格式,然后将打包好的数据发送到云平台。

u8 temp = 0;
u8 humi = 0;
void OneNet_FillBuf(char *buf)

	char  text[32];
	char buf1[128];
	memset(text, 0, sizeof(text));
	memset(buf1, 0, sizeof(buf1));
	
	strcpy(buf1, ",;");
		
//	memset(text, 0, sizeof(text));
//	sprintf(text, "Temp,%.1f;", sht20_info.tempreture); 
//	strcat(buf1, text);
//		  
//	memset(text, 0, sizeof(text));
//	sprintf(text, "Humi,%.1f;",sht20_info.humidity);
//	strcat(buf1, text);
	
	memset(text, 0, sizeof(text));
	sprintf(text, "Temp,%d;", temp++); 
	strcat(buf1, text);
		  
	memset(text, 0, sizeof(text));
	sprintf(text, "Humi,%d;",humi++);
	strcat(buf1, text);
	
	sprintf(buf, "POST /devices/%s/datapoints?type=5 HTTP/1.1\\r\\napi-key:%s\\r\\nHost:api.heclouds.com\\r\\n"
					"Content-Length:%d\\r\\n\\r\\n",
	
					DEVID, APIKEY, strlen(buf1));
	strcat(buf, buf1);

2、从OneNET平台获取数据

这部分主要是为了获取开关的值,以此实现控制板子的LED灯
我的代码编写如下:
这个函数主要是从OneNET平台上获取数据流名为KEY的当前值,其中DEVID,Master_APIkey可以在OneNET平台上获取。
函数实现:

void OneNet_GetData(void)

	char buf[256];
	u8 len = 0;
	
	sprintf(buf,"GET http://api.heclouds.com/devices/%s/datapoints?datastream_id=KEY HTTP/1.1\\r\\n%s\\r\\nHost:api.heclouds.com\\r\\n\\r\\n",DEVID,Master_APIkey);
	len = strlen(buf);
	ESP8266_SendData((u8*)buf,len);

函数调用:
在main函数中每隔1S调用一次OneNet_GetData()函数,保证能够及时响应命令。当服务器接收到数据包后就会回复数据流KEY的当前值给ESP8266。

3、下发命令的处理

在void OneNet_RevPro()函数里面添加控制代码,我添加的代码如下:

	//当GET KEY的数据流时,服务器会下发这样的数据, ..,"value":"0"],"id":"KEY"  
	if(strstr((char *)dataPtr, "\\"value\\":\\"1\\""))
	
		UsartPrintf(USART_DEBUG, "LED ON\\r\\n");
		LED = 0;
	
	else if(strstr((char *)dataPtr, "\\"value\\":\\"0\\""))
	
		UsartPrintf(USART_DEBUG, "LED OFF\\r\\n");
		LED = 1;
	
	ESP8266_Clear();

五、手机APP制作

这部分主要讲如何制作手机APP来连OneNET平台,并且获取数据和下发命令控制LED灯。APP是使用E4A中文安卓编程开发的,编程思维和C语言很相似,并且是中文编程,只需一天就能基本掌握。

1、页面布局

由于时间原因,布局随意了点,大家有时间可以慢慢优化页面

2、连接OneNET云平台

主要用到了网络类主组件中的客户组件

我们选中这个组件,右击可以查看组件的命令,方法在单片机编程中相当于函数,事件相当于中断。

在提示信息中,我们可以清楚的看到连接服务器方法的用法。IP地址和端口号被我定义为全局变量,这在E4A中叫程序集变量。

3、查询数据流

查询按钮单击事件编写如下:
在E4A编程中单引号’ 是注释。在单击事件中,主要做的就是将需要发送到OneNET服务器的数据组装好,赋值给程序集变量客户请求数据,然后打开定时器事件。从上部分的串口助手连接OneNET服务器中,我们可以知道打包好的数据是干什么用的。

事件 查询按钮.被单击()

	如果 查询按钮.标题 = "查询数据流" 则		
	    查询按钮.标题 = "结束查询"  '更改按钮标题
	
	    变量 子串1 为 文本型
	    变量 子串2 为 文本型
	    变量 子串3 为 文本型
	    变量 子串4 为 文本型
		变量 子串5 为 文本型
	    子串1 = "GET /devices/"
	    子串2 = "/datapoints?datastream_id="
	    子串3 = " HTTP/1.1\\napi-key:"
	    子串4 = "Host:api.heclouds.com\\n"
		子串5 = "Connection:close\\n\\n"
		'GET /devices/699063374/datapoints?datastream_id=Temp,Humi HTTP/1.1
		'api-key:=08b33=G=KP8qxRoCYYrurN41yk=
		'Host:api.heclouds.com
		'Connection:close(后两回车)
	
		客户端请求数据 = 子串1 & 设备ID框.内容 & 子串2 & 数据流名框1.内容 & "," & 数据流名框2.内容 & 子串3 & 密钥框.内容 & "\\n" & 子串4 & 子串5
		'弹出提示(客户端请求数据)
		时钟1.时钟周期 = 3*1000  '开始定时中断3S
	
	否则
	   时钟1.时钟周期 = 0  '关闭定时
	   查询按钮.标题 = "查询数据流"  
	结束 如果
	
结束 事件

时钟周期事件

事件 时钟1.周期事件()
	
	客户1.连接服务器(IP地址,端口号,5000)
	客户1.发送数据(文本到字节(客户端请求数据,"GBK")) '向服务器发送数据
	'由于发送内容有Connection:close,故发送完断开连接

结束 事件

客户收到数据事件
当客户端发送刚刚的组装好的数据包后,服务器就会返回信息,这时就会触发客户收到数据事件
为了从服务器发过来的数据拿出想要的“温湿度值”,并且放在页面中对应的编辑框内,做如下处理:

事件 客户1.收到数据(数据 为 字节型())
	
	服务器数据 = 字节到文本(数据,"GBK") 
    接收框.内容 = 接收框.内容 & "\\n" & "服务器:" & 服务器数据 '收到服务器发来的字节集数据,转换成文本
	接收框.置光标位置(取文本长度(接收框.内容))
	
	如果 查询按钮.标题 = "结束查询" 则 '如果正在查询数据流,就执行下面
		变量 分割数组 为 文本型()
		变量 数据流数 为 文本型
		变量 计次 为 整数型
		变量 结果 为 整数型
		变量 上传类型 为 整数型
		
		'从服务器请求的数据如下
		'type3 格式的数据
		'"errno":0,"data":"count":2,"datastreams":["datapoints":["at":"2022-05-14 01:24:36.000","value":29],"id":"Temp","datapoints":["at":"2022-05-14 01:25:01.000","value":100],"id":"Humi"],"error":"succ"
		'type5 格式的数据
		'"errno":0,"data":"count":2,"datastreams":["datapoints":["at":"2022-05-13 22:43:48.030","value":"41"],"id":"Temp","datapoints":["at":"2022-05-13 22:43:48.032","value":"60"],"id":"Humi"],"error":"succ"
	
		分割数组 = 分割文本(服务器数据,"value")
		'分割数组(1) = ":"89"],"id":"Temp","datapoints":["at":"2022-05-12 01:37:24.447","
		'分割数组(2) = ":"45"],"id":"Humi"],"error":"succ"
	
		'读取数据流个数 
		数据流数 = 取指定文本2(分割数组(0),"count\\":",",\\"datastreams")
		上传类型 = 寻找文本(服务器数据,"value\\":\\"",0)  '如果找到了说明是type5上传的数据类型,否则是type3
		如果 上传类型 = -1 则 '找不到
			'type3格式数据
			计次 = 1  '从下分割数组(1)开始循环查找
			判断循环首 计次 < 到整数(数据流数)+1    ' 越界访问数组,会闪退
			结果 = 寻找文本(分割数组(计次),数据流名框1.内容,0)
			如果 结果 <> -1 则
				数据框1.内容 = 取指定文本2(分割数组(计次) ,"\\":"   ,   "],\\"id\\":\\"" & 数据流名框1.内容)
			结束 如果
			
			结果 = 寻找文本(分割数组(计次),数据流名框2.内容,0)
			如果 结果 <> -1 则
				数据框2.内容 = 取指定文本2(分割数组(计次) ,"\\":"   ,   "],\\"id\\":\\"" & 数据流名框2.内容)
			结束 如果
		    计次 = 计次 + 1
			判断循环尾 
			
			否则
			'type5格式数据
			计次 = 1  '从下分割数组(1)开始循环查找
		判断循环首 计次 < 到整数(数据流数)+1    ' 越界访问数组,会闪退
			结果 = 寻找文本(分割数组(计次),数据流名框1.内容,0)
			如果 结果 <> -1 则
				数据框1.内容 = 取指定文本2(分割数组(计次) ,"\\":\\""   ,   "\\"],\\"id\\":\\"" & 数据流名框1.内容)
			结束 如果
			
			结果 = 寻找文本(分割数组(计次),数据流名框2.内容,0)
			如果 结果 <> -1 则
				数据框2.内容 = 取指定文本2(分割数组(计次) ,"\\":\\""   ,   "\\"],\\"id\\":\\"" & 数据流名框2.内容)
			结束 如果
		    计次 = 计次 + 1
		判断循环尾 
		否则
		'客户1.断开连接()
		
	结束 如果
	
结束 事件

4、开关灯控制

开灯按钮单击事件编写如下:
在事件中,主要是组装新增数据流的数据包,然后发送给服务器,服务器接收到后会将更新对应数据流的值。此时32单片机就能通过查询数据流的方法来获取KEY数据流的当前的值,从而实现了手机APP远程控制单片机的功能。

事件 控制按钮.被单击()
	
	变量 子串1 为 文本型
	变量 子串2 为 文本型
	变量 子串3 为 文本型
	变量 子串4 为 文本型
    变量 子串5 为 文本型
	子串1 = "POST /devices/"
	子串2 = "/datapoints?type=5 HTTP/1.1\\napi-key:"
	子串3 = "Host:api.heclouds.com\\n"
    子串4 = "Content-Length:8\\n\\n"
	'POST /devices/699063374/datapoints?type=5 HTTP/1.1
	'api-key:=08b33=G=KP8qxRoCYYrurN41yk=
	'Host:api.heclouds.com
	'Content-Length:8
	
	',;KEY,1;
	客户1.连接服务器(IP地址,端口号,5000)
	
	如果 控制按钮.标题 = "开灯"  则
		控制按钮.标题 = "关灯"
		子串5 =  ",;KEY,1;"
		客户端推送数据 = 子串1 & 设备ID框.内容 & 子串2 & 密钥框.内容 & "\\n" & 子串3 & 子串4 & 子串5 

		否则
		控制按钮.标题 = "开灯"
		子串5 =  ",;KEY,0;"
		客户端推送数据 = 子串1 & 设备ID框.内容 & 子串2 & 密钥框.内容 & "\\n" & 子串3 & 子串4 & 子串5 
	结束 如果
	
	客户1.发送数据(文本到字节(客户端推送数据,"GBK")) '向服务器发送数据
	接收框.内容 =  接收框.内容 & "\\n" & "客户端:"& 客户端推送数据
	接收框.置光

APP通过http获取OneNet数据与命令下发应用层

文章目录


前言

在之前我们已经完成了感知层和网络层的设计,现在我们来完成应用层的设计,这样大概的物联网框架就有了。这边采用APP实现,由于没有花很多时间学习app的设计,所以ui和程序都设计的比较简陋。需要完成设计的小伙伴可以参考前几篇博客。

ESP32驱动震动传感器、MAX4466(感知层)

ESP32通过MQTT协议把数据上传到OntNet平台(网络层)


一、项目思路及结构

1.结构(如下图):

其中http保存的是get请求和post请求的文件夹,json保存的是请求返回json数据解析的文件。

2.思路:
首先我们需要设计一个能够接收信息和发送信息到云平台的应用,使用get请求获取云平台的额数据显示到文本框上,通过按钮发送post请求到云平台,其中get请求获取的是一个json格式的数据如下:


	"errno": 0,
	"data": 
		"count": 3,
		"datastreams": [
			
				"datapoints": [
					
						"at": "2022-09-29 01:22:34.128",
						"value": 326
					,
					
						"at": "2022-09-29 01:22:32.997",
						"value": 325
					,
					
						"at": "2022-09-29 01:22:31.967",
						"value": 324
					
				],
				"id": "data"
			
		]
	,
	"error": "succ"

所以还要对数据进行解析,由于涉及的细节比较多这里只介绍主要的函数,当然完整工程会在文末分享出来。

二、主要函数

1.Get请求方法

代码如下:

 public  String GetDateMethod(String url)
        String time=new String();
        OkHttpClient client = new OkHttpClient();
        Request request = new Request.Builder()
                .url(url)
                .addHeader("Authorization","version=2018-10-31&res=products%2F417488&et=1700000000&method=md5&sign=2c5X8shBYQAVp%2B7BtJ0iTg%3D%3D")//这里需要生成新的key  我搞了一晚上才发现
                .build();
        String responseData="";
        Response response=null;
        try 
            response=client.newCall(request).execute();
            responseData=response.body().string();
         catch (IOException e) 
            e.printStackTrace();
        
        Log.w("ww","responsedata:"+responseData);


        return responseData;
    

新的密钥生成格式如下:

具体可以查看网络层的文章 其中res的参数是不一样的,key需要在云平台的产品概况找到,如下图:

2.Post请求

代码如下:

 public String PostDataMethod(String commd)
        OkHttpClient client=new OkHttpClient();
        String url=String.format("http://api.heclouds.com/v1/synccmds?device_id=997936235&timeout=30"); //api接口只需要把device_id=后面的改成你自己的设备id就可以
        
        String body=String.format(commd);
        Log.e("url",url);
        Log.e("body",body);
        RequestBody bodyJson=RequestBody.create(MediaType.parse("application/json;charset=utf-8"),body);
        Request request = new Request.Builder()
                .url(url).headers(new Headers.Builder().add("Authorization",
                                "version=2018-10-31&res=products%2F417488&et=1700000000&method=md5&sign=2c5X8shBYQAVp%2B7BtJ0iTg%3D%3D")  //这里和get请求的一样
                        .build()).post(bodyJson).build();
        Call call=client.newCall(request);
        call.enqueue(new Callback() 
            @Override
            public void onFailure(Call call, IOException e) 
                Log.e("e","post请求失败");
            

            @Override
            public void onResponse(Call call, Response response) throws IOException 

            
        );
        Response response=null;
        String responseData="";
        try
            response=client.newCall(request).execute();
            String jsonResStr=response.body().string();
            Log.e("onenet回应数据",jsonResStr);
            JSONObject jsonObject=new JSONObject(jsonResStr);
            responseData=jsonObject.getString("error");
            Log.e("val",responseData);
        catch (Exception e)
            e.printStackTrace();
        
        return responseData;
    

3.Main函数

代码如下:

public class MainActivity extends AppCompatActivity 
    HttpNetwork httpNetwork = new HttpNetwork();
    String openRes;
    @Override
    protected void onCreate(Bundle savedInstanceState) 
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        Button bt1=findViewById(R.id.button);
        Button bt2=findViewById(R.id.button2);
        Button bt3=findViewById(R.id.button3);
        TextView tw1=findViewById(R.id.textView);

		//get请求需要用的url
        String url="http://api.heclouds.com/devices/997936235/datapoints?datastream_id=data&limit=3";
        bt1.setOnClickListener(new View.OnClickListener() 
            @Override
            public void onClick(View view) 
                new Thread(() -> 
                    Looper.prepare();
                    openRes=httpNetwork.PostDataMethod("1");
                    Toast.makeText(getApplicationContext(),openRes.toString(),Toast.LENGTH_LONG).show();
                    Looper.loop();
                ).start();
            
        );

        bt2.setOnClickListener(new View.OnClickListener() 
            @Override
            public void onClick(View view) 
                new Thread(() -> 
                    Looper.prepare();
                    openRes=httpNetwork.PostDataMethod("0");
                    Toast.makeText(getApplicationContext(),openRes.toString(),Toast.LENGTH_LONG).show();
                    Looper.loop();
                ).start();
            
        );
        bt3.setOnClickListener(new View.OnClickListener() 
            @Override
            public void onClick(View view) 

                    new Thread(() -> 
                        Looper.prepare();
                        String data = httpNetwork.GetDateMethod(url);
                        int date= getvalue(data).intValue();
                        Log.i("data", String.valueOf(date));
                        tw1.post(new Runnable() 
                            @Override
                            public void run() 
                                tw1.setText(String.valueOf(date));
                            
                        );

                    ).start();


            
        );
    

	//json数据解析的函数
    public Integer getvalue(String responseData)
        JsonDate data = new Gson().fromJson(responseData, JsonDate.class);
        List<DatastreamItem> streams = data.getData().getDatastreams();
        List<DatapointItem> points = streams.get(0).getDatapoints();
        Integer resultData = new Integer(1);
        resultData = points.get(0).getValue();
        return resultData;
    

4.运行效果

如图:

这里返回了云平台的最新数据!

完整工程链接:https://pan.baidu.com/s/1LZZS_9tlLnPaN-YwgMAHmw?pwd=8888
提取码:8888


总结

上面的程序仅供参考,写的也比较简陋,文笔也不是很好,所以好多东西都表达的不到位,由于涉及的东西比较多,所以就直接把工程分享出来了。到此为止,一个简单的物联网项目设计就完成了,也是在复习之前学习的知识,同时,也欢迎小伙伴们和我一起进行学习交流呀!!!要学习的东西还很多,大家一起加油进步呀!如果觉得是不错分享动动发财的小手点一个赞呀!

最后多看官方给出的文档,如下:
https://open.iot.10086.cn/doc/v5/develop/
https://developer.android.com/

以上是关于自制APP连接OneNET---实现数据监控和下发控制(HTTP)的主要内容,如果未能解决你的问题,请参考以下文章

esp32 mqtt协议上报 dht11温湿度数据到onenet 指令下发控制开关灯

基于STM32的ESP8266 WIFI与ONENET通信连接,云平台以及手机APP数据显示

玩转 ESP32 + Arduino (十二) 通过MQTT协议上传数据至OneNet云平台

基于51单片机+DHT11温湿度模块+ESP8266模块+上传oneNET APP显示+LCD1602显示

uniapp怎么绑定onenet设备

基于OneNet平台设计的多节点温度采集系统-有人云4G模块+STM32