ONENET
一、MQTT 协议介绍
此部分内容来源于网络并稍作修订。
2.1 MQTT 概念
MQTT(Message Queuing Telemetry Transport)是一种轻量级、基于发布-订阅模式的消息传输协议,适用于资源受限的设备和低带宽、高延迟或不稳定的网络环境。
2.2 MQTT 的工作原理
要了解 MQTT 的工作原理,首先需要掌握以下几个概念:MQTT 客户端、MQTT Broker、发布-订阅模式、主题、QoS。
a. MQTT 客户端
任何运行 MQTT 客户端库的应用或设备都是 MQTT 客户端。例如,使用 MQTT 的即时通讯应用是客户端,使用 MQTT 上报数据的各种传感器是客户端,各种 MQTT 测试工具也是客户端。
b. MQTT Broker
MQTT Broker 是负责处理客户端请求的关键组件,包括建立连接、断开连接、订阅和取消订阅等操作,同时还负责消息的转发。一个高效强大的 MQTT Broker 能够轻松应对海量连接和百万级消息吞吐量,从而帮助物联网服务提供商专注于业务发展,快速构建可靠的 MQTT 应用。
关于 MQTT Broker 的更多详情,请参阅文章 2024 年最全面的 MQTT Broker 比较指南。
c. 发布-订阅模式
发布-订阅模式与客户端-服务器模式的不同之处在于,它将发送消息的客户端(发布者)和接收消息的客户端(订阅者)进行了解耦。发布者和订阅者之间无需建立直接连接,而是通过 MQTT Broker 来负责消息的路由和分发。
下图展示了 MQTT 发布/订阅过程。各客户端连接到 OneNET 物联网开放平台,订阅 OneNET 物联网开放平台的指令或设置,而客户端如 Air724UG 开发板将取得的参量值如 AD 采样值或指示灯状态等以特定的主题发布。而不管是客户端还是 OneNET 物联网开放平台,需要什么样的信息,就订阅该主题,相关的信息就会由 Broker 推送到订阅了该信息的客户端。
d. 主题
MQTT 协议根据主题来转发消息。主题通过 / 来区分层级,类似于 URL 路径,例如:
$sys/5f8FW5P3Vd/warn_01/thing/property/set
$sys/5f8FW5P3Vd/warn_01/thing/property/post/reply
e. QoS
MQTT 提供了三种服务质量(QoS),在不同网络环境下保证消息的可靠性。
a. QoS 0:消息最多传送一次。如果当前客户端不可用,它将丢失这条消息。
b. QoS 1:消息至少传送一次。
c. QoS 2:消息只传送一次。
二、功能演示概述
本文档使用 Air724UG 开发板通过 Lua 脚本语言连接到 OneNET 物联网平台,实现设备属性值的读写操作。以演示代码 onenet.lua 为基础,边修订边演示,一步一步地指导完成 OneNET 物联网平台的基本操作,并最终演示指示灯的亮灭与属性值的读写。
三、硬件环境
3.1 开发板准备及购买链接
本文所涉及到的所有演示,都使用开发板 EVB_Air724UG_A14 完成,大家可以通过以下的淘宝连接购买:
淘宝购买链接:Air724UG-NFM 开发板淘宝购买链接 ;
此开发板的详细使用说明参考:Air724UG 产品手册 中的《EVB_Air724UG_AXX 开发板使用说明》,写这篇文章时最新版本的使用说明为:《EVB_Air724UG_A14 开发板使用说明》;开发板使用过程中遇到任何问题,可以直接参考这份使用说明文档。
中国大陆环境下,可以上网的 SIM 卡,一般来说,使用移动,电信,联通的物联网卡或者手机卡都行;
3.2 开发板的接线方式
首先将开发板放置好,接上 USB 并连接到电脑,同时,记得将天线也连接好,保证信号环境比较良好,比如可以看看手机信号来判断一下所在环境的信号状况。USB 的连接如上图所示。
在上图中,连接 USB 的插口旁边有一个 USB 字样,在进行脚本下载时,须连接此端口。旁边另一个 USB 插口是 USB 转 UART 的接口,可以通过串行口工具查看调试 TRACE 信息。在本文的测试环境中,使用 USB 打印 TRACE,即在 Luatools 工具中,将软件上的 USB 打印 TRACE 选中即可,不必再另外连接串行口监视打印 TRACE 的相关信息。
3.3 固件操作相关内容
a. Luatools,是下载固件与脚本必不可少的工具,并且使用其查看 TRACE 调试信息也非常方便。
https://docs.openluat.com/Luatools/
b. Luatools 工具的使用请参阅:
https://docs.openluat.com/blog/Luatools/
c. 远程固件升级请参阅:
https://docs.openluat.com/blog/fota_lesson/
d. USB 驱动安装请参阅:
https://docs.openluat.com/blog/usb_drv/
四、软件环境准备
4.1 Lua 脚本语言
本文以 Lua 脚本语言为基础,因而需要有 Lua 脚本语言基础,可以通过下列文档了解:
Lua 脚本语言的语法,请参阅:
https://wiki.luatos.com/luaGuide/luaReference.html#lua-5-3
https://docs.openluat.com/blog/lua_lesson/
4.2 辅助工具
为了有效的分析开发过程中可能遇到的问题,并能查看数据,准备一些辅助工具很有必要,将大大缩短大家解决问题的时间。本文编写过程中,使用了 MQTTX 这款工具介绍给大家,本文在后面将使用用或提到此工具,请大家先了解。
MQTTX 下载地址:https://packages.emqx.io/MQTTX/v1.11.0/MQTTX-Setup-1.11.0-x64.exe
时间戳工具:时间戳(Unix timestamp)转换工具
Token 生成工具:OneNET - 中国移动物联网开放平台
五、OneNET 物联网开放平台
5.1 OneNET 物联网概述
本文档所说 OneNET 仅指物联网开放平台,OneNET 物联网开放平台是中国移动打造的面向产业互联和智慧生活应用的物联网 PaaS 平台,OneNET 物联网开放平台支持适配各种网络环境和协议类型,可实现各种传感器和智能硬件的快速接入,提供丰富的 API 和应用模板以支撑各类行业应用和智能硬件的开发,有效降低物联网应用开发和部署成本,满足物联网领域设备连接、智能化改造、协议适配、数据存储、数据安全以及大数据分析等等平台级服务需求。
平台的网址:OneNET - 中国移动物联网开放平台。
5.2 账号建立与登录
要使用 OneNET 物联网开放平台,首先得注册账号并建立设备,如上图所示,点击上图右上角的登录,弹出登录窗口,如果你还没有一个 OneNET 物联网开放平台的账号,则需要首先注册一个账号。
有关更详细的操作说明,请参考:OneNET - 中国移动物联网开放平台入门概述的有关文档说明。
顺便说一下,对于有尝试要求的用户,登录后,有必要准备一个小本本,记录一些必要的内容。如下图:
在上图中,点击访问权限,记录下用户 ID 及 Accesskey,此处的 Accesskey 是用户 key,与产品 key 及设备 key 有所不同,本文后面会有说及。当使用到可视化操作时,可能会用到用户 ID 及 key(本文不讨论可视化相关操作)。当读取用户 key 时,会要求验证,如图所示:
5.3 建立产品
所谓物联网,就是物物互联的网络,因而产品是必不可少的内容,接下来我们来建立产品。在网页的左上部,“全部产品服务”字样,当将光标移上去时,会下拉出来一个选择菜单,里面内容比较多,如下图。
在上图中,选择物联网开放平台,点击进入,然后点击产品开发,如图所示。
在上图中,点”创建产品“按键,弹出创建产品对话框,首先得选择产品品类,本文选择其它行业,当然大家可根据自己的实际需求来选择,比如选智慧城市等。
点击设备接入,弹出设备接入对话框,如下图。
在上面和对话框中,前面有*号是内容是必需项,不能空。我们可以依自己的习惯或产品类型取一个名称,本文使用 warn_test,所属城市依实际情况填写即可。节点类型选择直连设备,接入协议选择 MQTT,数据协议可以依自己的要求而选择,可以选择自定义(即透传),本文选择 OneJson。其它内容依上图选择即可。完成数据输入后,点确定,产品即建立完成,如下图列表中所示。
在上图中,点产品开发,把产品 ID 及 access_key 记录下来备用。此处的 ID 及 key 是产品 ID 与产品 key,与前面说到的用户 ID 及用户 key 是有区别的,请大家注意。强调一点,查看产品 ID 同样需要进行验证操作才能看到,为了避免频繁地进行手机验证,与前面的用户 key 一样,记录一下为好。
5.4 设备创建
产品创建后,就需要在产品下建立产品,在网页左边栏的导航栏中,选择”设备接入管理“,选择”设备管理“项,然后于页面的右侧找到”添加设备“,点击进入,如图。
所属产品选择上面创建的产品 warn_test,大家可以依自己的喜好自由命名,本文设备名称为 warn_01 进行阐述。完成后点确定返回,如下图。
在设备 warn_01 栏后面,点击详细,可看到设备密钥,复制记录备用。
需要注意的是,我们要注意设备名称为 warn_01,产品名称为 warn_test。同时,产品 accesskey 与设备密钥以及用户 accesskey 也是不同的信息,切记勿忘。
5.5 设置物模型
上面的操作完成后,点击左侧导航栏的产品开发,并接着点击右侧产品数据栏未尾(位于右侧)的产品开发,进入物模型的设置页面。
上图中,点击右下的设置物模型按键,打开物模型的设置界面。
上面设置了设备的三个属性,分别的指示灯,电压 1 组,电压 2 组。这就是所谓的功能点,对于各类产品,有不同的功能点,有些是系统的,有些是标准的,内容请大家自己操作时了解。但本文我们都不使用这些功能点,选择自定义功能点,因而上面列表中可以看到,都是自定义的功能点。本文演示添加一个布尔型的可读写功能点”指示灯“,同时也添加了两个只读的整型功能点”电压 1 组“与”电压 2 组“。
我们接着说明一下功能点的具体添加方式,本文只讲解自定义类型的属性添加,其它系统属性与标准属性,大家依需要自行选择即可。
在上图中,功能类型选择为”属性类型“,输入功能名称为电压 1 组,标识设置为 voltage1,数据类型为整形,大家可依据实际的项目要求进行设置,比如可以设置为浮点型数据,步长依数据类型而定,整型可设置为 1,浮点可以设置为 0.1 等。读写类型图上为只读,当然也可以设置为读写,只是本文演示时设置为只读罢了。
依此类推,依次添加三个功能点,即”指示灯“,”电压 1 组“与”电压 2 组“。
5.6 MQTT 协议接入与安全认证
本节的内容繁杂而且信息量较大,请直接参考 OneNET 物联网开放平台的官方文档 https://open.iot.10086.cn/doc/v5/fuse/detail/913 了解具体内容,比如接入地址与端口号。
六、使用 MQTTX 进行 OneNET 操作
6.1 新建连接
首先启动 MQTTX 客户端,点新建连接如下图,图中对于官方文档给出的连接内容进行了对应关系标示。
输入名称 onenet,当然大家也可以自己命名,Client ID 使用设备名称,即 warn_01,注意这里是设备名称,不是产品名称。MQTT 版本,大家选用 3.1.1 版本,高版本可能不支持。
上文说到密码需要 key 计算生成 Token,对于怎么样生成 Token 官方文档内有详细说明,更详细的内容请大家阅读官方文档。为了计算 Token,官方给出的计算工具,大家可以下载此工具,在前面的 4.2 节有给出下载地址。与前图类似,我们也建立一个工具内容与 Token 组成的对照图,方便大家理解,如下图。
图中各参数对应关系比较清晰,其中版本 2018-10-31 不能变动,key 为 OneNET 物联网开放平台中创建的设备密钥,至于 et,这个比较麻烦,因为如果这个值比当前时间早的话,会导致连接失败,因而这个值一定要大于当前时间值,这很麻烦。但大家也不必担心,前面辅助工具内已经列出时间戳工具,我们打开此工具,如下图所示。
在上图中复制当前时间,为了在使用时仍有效,修订这个值,把其稍加大一点即可,比如我测试时,是在此基础上追加 20000 秒的样子,这样就有足够的时间来保证时间戳有效。
所有输入完成后,点击 Generate 生成 Token,并将生成的 Token 复制到 MQTTX 的密码栏内。
当 MQTTX 连接的各项数据输入完成后,点击连接按钮,即可连接到 OneNET 物联网开放平台,如下图。
此时我们看到 OneNET 物联网开放平台上的设备的状态变成了在线,如图所示。
6.2 发布与订阅主题
连接成功了,接下来就是订阅哪些主题,因为我们主要的操作都是自定义的属性功能,因而我们只关注这部分的主题即可,如下图所示。
6.2.1 发布主题
前文第五节中阐述物联网开放平台时,已经选择了通信协议为 OneJson,我们依据此协议构建一个数据包,用于模仿设备的发布内容,这个数据包给出于下面代码块中。其中 123 是数据包标识号 ID,在响应时,此 ID 将原样返回。此值没有特别要求。有关 OneJson 的详细内容,请查阅官方的相关文档。
{"id":"123","params":{"led_flag":{"value":true},"voltage1":{"value":485},"voltage2":{"value":456}}}
我们可以理解为发布主题,即是属性内容上报,如设备的电压,指示灯状态上报给 OneNET 物联网开放平台,因而我们发布时的主题是:
$sys/5f8FW5P3Vd/warn_01/thing/property/post
即上面主题列举图片中的第 1 项内容,我们依其要求,将 pid 替换为产品 ID 即 5f8FW5P3Vd,将 device-name 替换成 warn_01。有关主题都依此操作修订,不再赘述。
6.2.2 订阅主题
在前面的图中,我们查看主题内容,有设备属性上报响应,这个是希望有的,因而需要订阅,即订阅:
$sys/5f8FW5P3Vd/warn_01/thing/property/post/reply
在前面的物模型设置时,我们定义一个布尔量的指示灯,可以通过 OneNET 物联网开放平台来控制开发板上的 LED 灯的亮灭,因而需要订阅设备属性设置请求,即:
$sys/5f8FW5P3Vd/warn_01/thing/property/set
到此我们订阅了两个主题,一个是上报响应主题,一个是设置属性主题。
我们将相关主题在 MQTTX 中添加。
6.3 验证结果
将各主题填写完成,将发布内容也填入发送区,点发送图标按钮,将数据发送到 OneNET 物联网开放平台,马上就看到了数据返回,如下图所示:
上图中,我看到上报了指示灯的状态是 true,voltage1 的值是 485,voltage2 的值是 456,同时我们也看到平台的响应信息如下面代码所示,其中第 1 行为订阅主题,第 2 行为 JSON 格式的报文,这个我们要留意,因为我们在后面对 Air724UG 开发板的演示将会同样看到这样的信息,以表征通信成功。
$sys/5f8FW5P3Vd/warn_01/thing/property/post/reply
{"id":"123","code":200,"msg":"success"}
我们同样去看看 OneNET 物联网开放平台上设备 warn_01 的状态是否有改变。点击设备管理,点击设备 warn_01 的设备详情,如下图。
可以看到,指示灯状态及电压 1 组与电压 2 组的各显示值与上报的各值相符。
6.4 指示灯的设置
至此我们验证了数据上报,但我们还缺少 OneNET 物联网开放平台对设备的数据设置操作。在前面阐述有关 OneNET 物联网开放平台的章节中,定义了一个指示灯,我们可以通过设置指示灯的亮与灭来进行此功能的验证,当然,我们此处只能看到数据流的变化,如下面代码块所示,"led_flag":true 表示亮,而 led_flag":false 表示灭。
{"id":"22","version":"1.0","params":{"led_flag":true}}
{"id":"23","version":"1.0","params":{"led_flag":false}}
我们按在 6.3 节时所说的,切换到设备详情页面,如上图所示。在上面图示中,点击设备调试,然后点击应用模拟器,出现如下图所示内容。
在上图中,勾选指示灯,此时中下部的属性设置按钮使能。将 true 开 选上,点属性设置按钮,即可发送属性设置数据包,点亮指示灯。同样地,选中 false 关,点属性设置按钮,可熄灭指示灯。演示结果如下图。
操作结果可查看下图中的调试日志,这里之所以超时,是由于我们仅仅是查看了数据,并没有实际对数据进行响应,因而在等待响应的过程中,导致超时。
6.5 指示灯的自动翻转
当然,如果我们能让指示灯依某些功能点相关联,同步进行改变,从而体现出设备的某种状态来,这样具有实际的意义,因而我们尝试来完成这样的工作。
如上图,我们切换到 OneNET 物联网开放平台的场景联动栏目,新建两个场景,分别这命名为“指示灯开”与“指示灯关”。点击新建场景,在输入场景名称等信息后,出现如下所示页面。
在上图中,我们定义了一个场景,即当电压 1 组电压小于 480V 时,指示灯关闭。同样,我们也可以设置指示灯打开的场景,本文设置的打开场景是电压 1 组大于 480V 时打开,具体内容与上图类似,就不再帖出来了。
接下来我们来测试场景是否有效,在 MQTTX 中,将前述的发布内容点击发送后,接着就有返回信息,此时多了一条记录,如图。
我们发送的数据电压 1 组是 485V,大于设置 480V 的值,因而会打开指示灯,从返回的数据看,确实有相关的设置操作。
七、Lua 代码的编写与修订
7.1 演示愿景
前文对 OneNET 物联网开放平台的产品创建,设备创建以及功能点的添加等内容进行了讲解,经过与 MQTTX 的配合使用,我们对 OneNET 物联网开放平台有一个初步的概念,同样,对于使用 MQTTX 与 OneNET 物联网开放平台建立连接并操作设备等功能也有了一定的认识,在此基础上,正式进入本文的主题,即使用 Air724UG 完成前文的演示,让大家能使用 Air724UG 比较快速的进入状态,能解决大家的实际问题,并进而在实际的产品中,使用 Air724UG,用好 Air724UG,更主要的是,能使大家的产品研发,更上一个新的台阶。
7.2 资源规划
前文我们设置了三个功能点,一个指示灯、两个电压组。这些功能点的设置也并不是漫无目的。我们使用的开发板 EVB_Air724UG_A14 有两个 ADC 采样点,同时有三个指示灯,分别是 PWR、NET、LTE 指示灯,我们将 LTE 指示灯拿来做本文的指示灯操作用途,NET 指示灯与 PWR 指示灯不改变用途,事实上,核心板一个指示灯也能工作得很好。类似地我们将 ADC 对应两个电压组,因为是数据输入性质的功能,因而这两个功能点是只读功能点。
这里需要强调一下,EVB_Air724UG_A14 开发板中,对这两个 ADC 采集点配置热敏电阻与可调电阻,这使得我们可以不外接电路即可完成演示,如下图所示,具体的硬件配置请查阅开发板说明书。当然,我们前文中所使用的测试数据如 485V 这样的值是比较大的,但在实际应用中,电压都是经过电阻分压到可采集范围,然后再通过电阻的分压比例核算的,因而我们也可以使用虚拟的比例,使采集的电压与前文所配置的电压相匹配,从而能顺利地完成演示。
7.3 项目建立
首先,下载 Air724UG 的演示代码 onenet_studio,下载地址:SDK& Demo - luatos@air724ug - 合宙文档中心,当然,本文后面会提供全面修订过的文件,如果不想去折腾,可以直接复制本文后面的代码即可。
然后启动 Luatools 并点击创建项目,输入 onenet 建立项目,然后选择底层核心,本文使用 core_V4028\LuatOS-Air_V4028_RDA8910.pac,底层核心文件一般位于 Luatools 工具的安装目录下。
最后将前面下载的 main.lua 与 onenet.lua 两个文件加入到项目,如下图。
7.4 代码修订
打开 main.lua 文件,因为选用了 LTE 作为指示灯,因而网络指示灯设置这里要改动一下,将指示灯 P0_4 改为通用 IO 控制,如下代码的示。
--加载网络指示灯和LTE指示灯功能模块
--根据自己的项目需求和硬件配置决定:1、是否加载此功能模块;2、配置指示灯引脚
--合宙官方出售的Air720U开发板上的网络指示灯引脚为pio.P0_1,LTE指示灯引脚为pio.P0_4
require "netLed"
pmd.ldoset(2,pmd.LDO_VLCD)
--netLed.setup(true,pio.P0_1,pio.P0_4)
--将P0_4不作为网络LTE指示灯
netLed.setup(true,pio.P0_1,nil)
打开 onenet.lua 文件,进行一些必要的修订,文件的开头部分,有产品 ID 等信息,原始代码为如下面代码所示。
-- 产品ID和产品动态注册密钥
local ProductId = "vh8xhj9sxz"
local ProductSecret = "t7Ojq/VBDQO3r8l5nQXXPZdzZQ3JCY8riZMj87vX96c="
将 ProductId 替换为本文档前面创建产品时的 ID,将 ProductSecret 替换为其对应的 key,如下所示。各位使用时,注意要替换为自己在 OneNET 物联网开放平台上的产品 ID 及其对应的 key。本文使用的产品 ID 及 key 修订如下。
-- 产品ID和产品动态注册密钥
local ProductId = "5f8FW5P3Vd"
local ProductSecret = "M09KR0djN2VBcjRzcEhyMVh0VGlvRm5VamFLcmJRUno="
由于 Token 的产品附加了时间属性,因而有必要自动产生这个 Token,为此演示文件 onenet.lua 文件内也有对应的函数,如下所示。
local function get_token()
local version = '2018-10-31'
-- 通过MQ实例名称访问MQ
local res = "products/"..ProductId.."/devices/"..getDeviceName()
-- 用户自定义token过期时间
local et = tostring(os.time() + 3600)
-- 签名方法,支持md5、sha1、sha256
local method = 'sha256'
-- 对access_key进行decode
local key = crypto.base64_decode(ProductSecret,#ProductSecret)
-- 计算sign
local StringForSignature = et .. '\n' .. method .. '\n' .. res ..'\n' .. version
local sign1 = crypto.hmac_sha256(StringForSignature,key)
local sign2 = sign1:fromHex()
local sign = crypto.base64_encode(sign2,#sign2)
-- value 部分进行url编码
sign = string.urlEncode(sign)
res = string.urlEncode(res)
-- token参数拼接
local token = string.format('version=%s&res=%s&et=%s&method=%s&sign=%s',version, res, et, method, sign)
return token
end
可以看到,上面代码使用系统时间加上 3600 秒来保证时间有效。对于上面的代码,大家也可以对照代码及官方关于 Token 生成的有关说明,比照阅读,加深理解。
前文说到,为了接受设置,我们额外订阅了 $sys/5f8FW5P3Vd/warn_01/thing/property/set,因而这个我们得在订阅部分加上,修订代码如下所示。
local function onenet_subscribe()
--mqtt订阅主题,根据自己需要修改
local onenet_topic = {
["$sys/"..ProductId.."/"..getDeviceName().."/thing/property/post/reply"]=0,
--加上设置订阅
["$sys/"..ProductId.."/"..getDeviceName().."/thing/property/set"]=1
}
if onenet_mqttClient:subscribe(onenet_topic) then
return true
else
return false
end
end
修订设备名称,将设备名称修改为实际的设备名称,此处为 warn_01,同时将一些与本文档不符的注释说明去掉。
local function getDeviceName()
--默认使用设备的IMEI作为设备名称,用户可以根据项目需求自行修改
--return misc.getImei()
--修订设备名称为实际的设备名称
return "warn_01"
end
onenet.lua 中原来的连接代码使用的是地址而不是域名,也不知对不对,因而按前文所说的内容修订一下,这样更直观易理解。
-- while not onenet_mqttClient:connect("218.201.45.7",1883) do sys.wait(2000) end
while not onenet_mqttClient:connect("mqtts.heclouds.com",1883) do sys.wait(2000) end
接收数据处理函数 proc 的 TRACE 输出转化为了 HEX 即十六进制数据格式,因为我们是使用 JSON 格式的协议,因而没有必要转为 HEX,反而看不清,直接输出即可,修订如下。我们看到代码中有 mqttInMsg 字样,明显是从别处抄来而没有修订,因而此处改为 onenet,以示严谨。
local function proc(onenet_mqttClient)
local result,data
while true do
result,data = onenet_mqttClient:receive(60000,"APP_SOCKET_SEND_DATA")
--接收到数据
if result then
--log.info("mqttInMsg.proc",data.topic,string.toHex(data.payload))
log.info("onenet.proc",data.topic,data.payload)
--TODO:根据需求自行处理data.payload
else
break
end
end
return result or data=="timeout" or data=="APP_SOCKET_SEND_DATA"
end
到这里基本上修订也就完成了,我们将脚本下载到开发板,看看结果如何。特别提醒,如果是第一次下载,请一定使用下载底层与脚本,如果已经有过下载,则可只下载脚本即可。
可以看到平台上设备状态变为了在线,表示连接成功。
我们来看看 TRACE 信息,因过程较长,不好贴图,把文字复制下来,列出如下。
[2024-11-09 15:51:58.834] [I]-[ril.proatc] OK
[2024-11-09 15:51:58.834] [I]-[link.pdpCmdCnf] CONNECT_DELAY true nil nil
[2024-11-09 15:51:58.834] [I]-[publish IP_READY_IND]
[2024-11-09 15:51:58.834] [I]-[ril.sendat] AT+CSQ
[2024-11-09 15:51:58.834] [I]-[---------------------- 网络注册已成功 ----------------------]
[2024-11-09 15:51:58.834] [I]-[socket:connect-coreid,prot,addr,port,cert,timeout] 0 UDP s2c.time.edu.cn 123 nil 120
[2024-11-09 15:51:58.834] [I]-[socket:connect-coreid,prot,addr,port,cert,timeout] 1 UDP dev_msg1.openluat.com 12425 nil 120
[2024-11-09 15:51:58.889] [I]-[ril.proatc] +CSQ: 27,99
[2024-11-09 15:51:58.889] [I]-[ril.proatc] OK
此处删除了一些较次要信息。
"连接成功"显示成了乱码,我已经在前后添加特征字符验证,此处就是输出“连接成功”四个字。
[2024-11-09 15:52:00.109] [I]-[mqtt杩炴帴鎴愬姛]
[2024-11-09 15:52:00.109] [D]-[mqtt.client:write] 826300020031247379732F356638465735503356642F7761726E5F30312F7468696E672F70726F70657274792F706F73742F 50
[2024-11-09 15:52:00.109] [D]-[socket.send] total 101 bytes first 30 bytes 俢
[2024-11-09 15:52:00.215] [I]-[socket:on_response:] 0 SOCKET_SEND 0
[2024-11-09 15:52:00.215] [D]-[socket.recv] 6 nil
[2024-11-09 15:52:00.259] [D]-[mqtt.unpack] 6 900400020000 6
显示了订阅成功
[2024-11-09 15:52:00.259] [I]-[mqtt订阅成功]
[2024-11-09 15:52:26.076] [I]-[ril.proatc] +CREG: 1,"f12d","08759f36",7
由上面的信息可知,成功连接了 OneNET 物联网开放平台,并订阅主题成功。
接下来我们在 OneNET 物联网开放平台设置一下指示灯的值,看是不是有数据下来,是不是与使用 MQTTX 看到的数据一致。设置指示灯的值请阅读本文档第六章节(使用 MQTTX 进行 OneNET 操作)的有关内容。
很幸运,我们看到了期待的 JSON 字符串:
$sys/5f8FW5P3Vd/warn_01/thing/property/set {"id":"2","version":"1.0","params":{"led_flag":true}}
7.5 Pin 操作
目前为止,我们还只是看到了数据的变化,并没有实际地点亮或熄灭开发板上的指示灯,接下来我们来处理这件事。为了解析 JSON 字符串并操作指示灯,我们编写一个函数 led_on_off() 来使流程更清晰有条理。
-- $sys/5f8FW5P3Vd/warn_01/thing/property/set {"id":"2","version":"1.0","params":{"led_flag":true}}
--非常感谢网友 [清脆的小兔子z4yjk6M3nV] 提供的简洁代码
--在其基础上,加上了info输出提示。
local function led_on_off( set_payload )
--log.info(" json str = ".. js.." " )
if set_payload ~= nil then
local js_table,result,err = json.decode(set_payload)
if result and js_table and type(js_table) == "table" then
log.info( "onenet.len_on_off "," id =" ..js_table.id )
log.info( "onenet.len_on_off "," ver =" ..js_table.version )
if js_table.params then
log.info( "onenet.len_on_off "," led_flag =" ..tostring(js_table.params.led_flag ) )
if( js_table.params.led_flag == true ) then
log.info("onenet.led_on_off","指示灯亮" )
pins.setup( pio.P0_4,1 )
else
log.info("onenet.led_on_off","指示灯灭" )
pins.setup( pio.P0_4,0 )
end
end
else
log.info("onenet.led_on_off",js_table,result,err )
end
end
end
--[[
local function led_on_off( set_payload )
--log.info(" json str = ".. js.." " )
if set_payload ~= nil then
local js_table = json.decode(set_payload)
for k,v in pairs( js_table ) do
if type(v) == "table" then
log.info( "onenet.len_on_off "," body_vv =" ..k )
for kk,vv in pairs(v) do
log.info("onenet.led_on_off","body_vv=".. kk.." : ".. tostring(vv) )
local led_flag = string.find(kk,"led_flag")
if( led_flag ~= nil ) then
if( vv == true ) then
log.info("onenet.led_on_off","指示灯亮" )
pins.setup(pio.P0_4,1)
else
log.info("onenet.led_on_off","指示灯灭" )
pins.setup(pio.P0_4,0)
end
end
end
else
log.info("onenet.led_on_off","body_v=".. k.." : ".. v )
end
end
end
end
--]]
然后我们在接收数据的函数中调用此函数即可,因而我们需要修订 onenet.lua 文件,将函数调用添加进去,如下面代码段所示。
--- MQTT客户端数据接收处理
-- @param onenet_mqttClient,MQTT客户端对象
-- @return 处理成功返回true,处理出错返回false
-- @usage mqttInMsg.proc(onenet_mqttClient)
local function proc(onenet_mqttClient)
local result,data
while true do
result,data = onenet_mqttClient:receive(60000,"APP_SOCKET_SEND_DATA")
--接收到数据
if result then
--log.info("mqttInMsg.proc",data.topic,string.toHex(data.payload))
log.info("onenet.proc",data.topic.." : ",data.payload)
--此处调用指示灯处理函数
led_on_off( data.payload )
--TODO:根据需求自行处理data.payload
else
break
end
end
return result or data=="timeout" or data=="APP_SOCKET_SEND_DATA"
end
修订完成,我们将代码下载到 Air724UG 开发板中运行,查看结果如下图所示。
上图中输出格式是首先将整个字符串输出,然后解析各属性值,并在最后给出指示灯的亮灭情况。至于开发板的实际指示灯亮灭就不贴图了,大家实际下载代码实际操作即可看到。
7.6 ADC 操作
为了使 ADC 有效,得对 ADC 进行一些配置。查看原理图,我们知道开发板上的两个 ADC 转换端口是 2 号端口与 3 号端口,因而我们需要对这两个端口进行一些设置,同时,为了能不停的采集 ADC 的值,我们设置一个循环定时器,使其每 10 秒读取一次 ADC 的值,代码如下,注意要添加在 sys.taskInit(iot) 之前。
-- 开启对应的adc通道
adc.open(2)
adc.open(3)
-- 定时每十秒读取adc值
sys.timerLoopStart(timer_proc,10000)
接下来编写定时器的回调函数,取名为 timer_proc,如下代码所示。注意,此函数仅对本文所阐述的工作点有效,如果用于别处,请进行修订补全。
function timer_proc( )
--sys.publish("APP_SOCKET_SEND_DATA")
--mqtt发布主题根据自己需要修改
local publish_data =
{
id = "123",
params = {
led_flag = { value = true },
voltage1 = { value = 482 },
voltage2 = { value = 406 }
}
}
local adcval1,volt1 = adc.read(2)
local adcval2,voltva2 = adc.read(3)
publish_data.id = "123"
--取得ADC的采样值
publish_data.params.voltage1.value = adcval1
publish_data.params.voltage2.value = adcval2
local jsondata = json.encode(publish_data)
publish_str = jsondata
--ADC采样完成
adc_get = 1
log.info( publish_str )
end
为了能传递参数,在 onenet.lua 中定义两个变量,分别命名为 adc_get 与 publish_str,用于表征 adc 采样完成以及使用新 adc 值重新组合的发布的数据内容也准备好。onenet.lua 原本就有一个用于发布信息时的函数没有实际调用,这个函数名为 onenet_publish,对其修订如下:
function onenet_publish()
--local publish_data =
--{
-- id = "123",
-- version = "1.0",
-- params = {},
--}
if(adc_get ~=0 ) then
log.info( "$sys/"..ProductId.."/"..getDeviceName().."/thing/property/post", publish_str )
onenet_mqttClient:publish("$sys/"..ProductId.."/"..getDeviceName().."/thing/property/post", publish_str, 0)
adc_get = 0
end;
end
修订基本完成,为了能在 adc 采样完成后即将数据向 OneNET 物联网开放平台上报,我们在 proc 任务体中加入调用,如下所示。
--循环处理接收和发送的数据
while true do
mqtt_ready = true
if not proc(onenet_mqttClient) then log.error("mqttTask.mqttInMsg.proc error") break end
--加入发布调用
onenet_publish()
end
如此我们对 ADC 的采样内容添加完成。我们可以测试一下结果是否正确,将文件保存,然后在 Lautools 中完成脚本下载,结果列出如下图。
图中有几部分的内容,第 1 部分是信息发布,具体内容形式与组成,前文使用 MQTTX 与 OneNET 物联网开发平台进行操作时有过说明,请大家参阅前文。第 2 部分的内容是 OneNET 物联网开放平台的响应,这部分内容也与前面的测试相符。第 3 部分则是场景反馈,在前面的论述中,我们定义了两个场景,当电压 1 组电压大于 480V 时,点亮指示灯,当电压 1 组的电压小于 480V 时,熄灭指示灯。大家可以查看第 1 部分内容的 voltage1 的值,从上图可知其值为 115V,所以指示灯熄灭,OneNET 物联网开放平台下发一个设置数据包,用以熄灭指示灯,可见这部分内容也与前文的演示相符,我们也可以将 ADC 值与本文前面在 OneNET 物联网开放平台上设置的值做一下匹配,比如乘某个比例参数,使电压是 0-500V 左右变动,这样我们就可以在调节可调电阻时,就能看到指示灯的亮与灭,即当匹配后的值超过 480V 时,指示亮,否则指示灯灭。
另外说明一点,本文 ADC 采集值是直接取的 ADC 的读数值,而不是计算转换后的电压值,原因无它,盖因为我要的就是一个整数值罢了。实际应用中,大家应当要清楚这一点,使用正确的电压值,尽管本质上是一样的东西。
到此 OneNET 物联网开放平台的操作演示顺利完成。
八、测试完整代码
8.1 main.lua 文件
--必须在这个位置定义PROJECT和VERSION变量
--PROJECT:ascii string类型,可以随便定义,只要不使用,就行
--VERSION:ascii string类型,如果使用Luat物联云平台固件升级的功能,必须按照"X.X.X"定义,X表示1位数字;否则可随便定义
PROJECT = "HTTP"
VERSION = "2.0.0"
--加载日志功能模块,并且设置日志输出等级
--如果关闭调用log模块接口输出的日志,等级设置为log.LOG_SILENT即可
require "log"
LOG_LEVEL = log.LOGLEVEL_TRACE
--[[
如果使用UART输出日志,打开这行注释的代码"--log.openTrace(true,1,115200)"即可,根据自己的需求修改此接口的参数
如果要彻底关闭脚本中的输出日志(包括调用log模块接口和Lua标准print接口输出的日志),执行log.openTrace(false,第二个参数跟调用openTrace接口打开日志的第二个参数相同),例如:
1、没有调用过sys.opntrace配置日志输出端口或者最后一次是调用log.openTrace(true,nil,921600)配置日志输出端口,此时要关闭输出日志,直接调用log.openTrace(false)即可
2、最后一次是调用log.openTrace(true,1,115200)配置日志输出端口,此时要关闭输出日志,直接调用log.openTrace(false,1)即可
]]
--log.openTrace(true,1,115200)
require "sys"
require "net"
--每1分钟查询一次GSM信号强度
--每1分钟查询一次基站信息
net.startQueryAll(60000, 60000)
--此处关闭RNDIS网卡功能
--否则,模块通过USB连接电脑后,会在电脑的网络适配器中枚举一个RNDIS网卡,电脑默认使用此网卡上网,导致模块使用的sim卡流量流失
--如果项目中需要打开此功能,把ril.request("AT+RNDISCALL=0,1")修改为ril.request("AT+RNDISCALL=1,1")即可
--注意:core固件:V0030以及之后的版本、V3028以及之后的版本,才以稳定地支持此功能
ril.request("AT+RNDISCALL=0,1")
--加载控制台调试功能模块(此处代码配置的是uart2,波特率115200)
--此功能模块不是必须的,根据项目需求决定是否加载
--使用时注意:控制台使用的uart不要和其他功能使用的uart冲突
--使用说明参考demo/console下的《console功能使用说明.docx》
--require "console"
--console.setup(2, 115200)
--加载网络指示灯和LTE指示灯功能模块
--根据自己的项目需求和硬件配置决定:1、是否加载此功能模块;2、配置指示灯引脚
--合宙官方出售的Air720U开发板上的网络指示灯引脚为pio.P0_1,LTE指示灯引脚为pio.P0_4
require "netLed"
pmd.ldoset(2,pmd.LDO_VLCD)
--netLed.setup(true,pio.P0_1,pio.P0_4)
--将P0_4不作为网络LTE指示灯
netLed.setup(true,pio.P0_1,nil)
--网络指示灯功能模块中,默认配置了各种工作状态下指示灯的闪烁规律,参考netLed.lua中ledBlinkTime配置的默认值
--如果默认值满足不了需求,此处调用netLed.updateBlinkTime去配置闪烁时长
--LTE指示灯功能模块中,配置的是注册上4G网络,灯就常亮,其余任何状态灯都会熄灭
--加载错误日志管理功能模块【强烈建议打开此功能】
--如下2行代码,只是简单的演示如何使用errDump功能,详情参考errDump的api
require "errDump"
errDump.request("udp://dev_msg1.openluat.com:12425", nil, true)
--加载远程升级功能模块【强烈建议打开此功能,如果使用了阿里云的OTA功能,可以不打开此功能】
--如下3行代码,只是简单的演示如何使用update功能,详情参考update的api以及demo/update
--PRODUCT_KEY = "v32xEAKsGTIEQxtqgwCldp5aPlcnPs3K"
--require "update"
--update.request()
require"ntp"
ntp.timeSync()
require "onenet"
--启动系统框架
sys.init(0, 0)
sys.run()
8.2 onenet.lua 文件
--- 模块功能:onenet studio功能测试.
-- @module onenet
-- @author Dozingfiretruck
-- @license MIT
-- @copyright OpenLuat.com
-- @release 2021.4.7
module(...,package.seeall)
require "ntp"
require "pm"
require "misc"
require "mqtt"
require "utils"
require "patch"
require "socket"
require "http"
require "common"
-- 产品ID和产品动态注册密钥
local ProductId = "5f8FW5P3Vd"
local ProductSecret = "M09KR0djN2VBcjRzcEhyMVh0VGlvRm5VamFLcmJRUno="
local onenet_mqttClient
local adc_get = 0
local publish_str
--[[
函数名:getDeviceName
功能 :获取设备名称
参数 :无
返回值:设备名称
]]
local function getDeviceName()
--默认使用设备的IMEI作为设备名称,用户可以根据项目需求自行修改
--return misc.getImei()
--修订设备名称为实际的设备名称
return "warn_01"
end
function timer_proc( )
--sys.publish("APP_SOCKET_SEND_DATA")
--mqtt发布主题根据自己需要修改
local publish_data =
{
id = "123",
params = {
led_flag = { value = true },
voltage1 = { value = 482 },
voltage2 = { value = 406 }
}
}
local adcval1,volt1 = adc.read(2)
local adcval2,voltva2 = adc.read(3)
publish_data.id = "123"
--取得ADC的采样值
publish_data.params.voltage1.value = adcval1
publish_data.params.voltage2.value = adcval2
local jsondata = json.encode(publish_data)
publish_str = jsondata
--ADC采样完成
adc_get = 1
log.info( publish_str )
end
function onenet_publish()
--原演示代码的发布数据删除
--local publish_data =
--{
-- id = "123",
-- version = "1.0",
-- params = {},
--}
if(adc_get ~=0 ) then
log.info( "$sys/"..ProductId.."/"..getDeviceName().."/thing/property/post", publish_str )
onenet_mqttClient:publish("$sys/"..ProductId.."/"..getDeviceName().."/thing/property/post", publish_str, 0)
adc_get = 0
end
end
local function onenet_subscribe()
--mqtt订阅主题,根据自己需要修改
local onenet_topic = {
["$sys/"..ProductId.."/"..getDeviceName().."/thing/property/post/reply"]=0,
--加上设置订阅
["$sys/"..ProductId.."/"..getDeviceName().."/thing/property/set"]=1
}
if onenet_mqttClient:subscribe(onenet_topic) then
return true
else
return false
end
end
-- 无网络重启时间,飞行模式启动时间
local rstTim, flyTim = 600000, 300000
local mqtt_ready = false
--- MQTT连接是否处于激活状态
-- @return 激活状态返回true,非激活状态返回false
-- @usage mqttTask.isReady()
function isReady()
return mqtt_ready
end
local function get_token()
local version = '2018-10-31'
-- 通过MQ实例名称访问MQ
local res = "products/"..ProductId.."/devices/"..getDeviceName()
-- 用户自定义token过期时间
local et = tostring(os.time() + 3600)
-- 签名方法,支持md5、sha1、sha256
local method = 'sha256'
-- 对access_key进行decode
local key = crypto.base64_decode(ProductSecret,#ProductSecret)
-- 计算sign
local StringForSignature = et .. '\n' .. method .. '\n' .. res ..'\n' .. version
local sign1 = crypto.hmac_sha256(StringForSignature,key)
local sign2 = sign1:fromHex()
local sign = crypto.base64_encode(sign2,#sign2)
-- value 部分进行url编码
sign = string.urlEncode(sign)
res = string.urlEncode(res)
-- token参数拼接
local token = string.format('version=%s&res=%s&et=%s&method=%s&sign=%s',version, res, et, method, sign)
return token
end
-- $sys/5f8FW5P3Vd/warn_01/thing/property/set {"id":"2","version":"1.0","params":{"led_flag":true}}
--非常感谢网友 [清脆的小兔子z4yjk6M3nV] 提供的简洁代码
--在其基础上,加上了info输出提示。
local function led_on_off( set_payload )
--log.info(" json str = ".. js.." " )
if set_payload ~= nil then
local js_table,result,err = json.decode(set_payload)
if result and js_table and type(js_table) == "table" then
log.info( "onenet.len_on_off "," id =" ..js_table.id )
log.info( "onenet.len_on_off "," ver =" ..js_table.version )
if js_table.params then
log.info( "onenet.len_on_off "," led_flag =" ..tostring(js_table.params.led_flag ) )
if( js_table.params.led_flag == true ) then
log.info("onenet.led_on_off","指示灯亮" )
pins.setup( pio.P0_4,1 )
else
log.info("onenet.led_on_off","指示灯灭" )
pins.setup( pio.P0_4,0 )
end
end
else
log.info("onenet.led_on_off",js_table,result,err )
end
end
end
--[[
local function led_on_off( set_payload )
--log.info(" json str = ".. js.." " )
if set_payload ~= nil then
local js_table = json.decode(set_payload)
for k,v in pairs( js_table ) do
if type(v) == "table" then
log.info( "onenet.len_on_off "," body_vv =" ..k )
for kk,vv in pairs(v) do
log.info("onenet.led_on_off","body_vv=".. kk.." : ".. tostring(vv) )
local led_flag = string.find(kk,"led_flag")
if( led_flag ~= nil ) then
if( vv == true ) then
log.info("onenet.led_on_off","指示灯亮" )
pins.setup(pio.P0_4,1)
else
log.info("onenet.led_on_off","指示灯灭" )
pins.setup(pio.P0_4,0)
end
end
end
else
log.info("onenet.led_on_off","body_v=".. k.." : ".. v )
end
end
end
end
--]]
--- MQTT客户端数据接收处理
-- @param onenet_mqttClient,MQTT客户端对象
-- @return 处理成功返回true,处理出错返回false
-- @usage mqttInMsg.proc(onenet_mqttClient)
local function proc(onenet_mqttClient)
local result,data
while true do
result,data = onenet_mqttClient:receive(60000,"APP_SOCKET_SEND_DATA")
--接收到数据
if result then
--log.info("mqttInMsg.proc",data.topic,string.toHex(data.payload))
log.info("onenet.proc",data.topic.." : ",data.payload)
--此处调用指示灯处理函数
led_on_off( data.payload )
--TODO:根据需求自行处理data.payload
else
break
end
end
return result or data=="timeout" or data=="APP_SOCKET_SEND_DATA"
end
local function onenet_iot()
while true do
if not socket.isReady() and not sys.waitUntil("IP_READY_IND", rstTim) then sys.restart("网络初始化失败!") end
local clientid = getDeviceName()
local username = ProductId
local password = get_token()
--创建一个MQTT客户端
onenet_mqttClient = mqtt.client(clientid,300,username,password)
--阻塞执行MQTT CONNECT动作,直至成功
while not onenet_mqttClient:connect("mqtts.heclouds.com",1883) do sys.wait(2000) end
log.info("mqtt connect success ")
--订阅主题
if onenet_subscribe() then
log.info("mqtt订阅成功")
--循环处理接收和发送的数据
while true do
mqtt_ready = true
if not proc(onenet_mqttClient) then log.error("mqttTask.mqttInMsg.proc error") break end
--加入发布调用
onenet_publish()
end
else
log.info("mqtt订阅失败")
end
mqtt_ready = false
--断开MQTT连接
onenet_mqttClient:disconnect()
end
end
local function iot()
if not socket.isReady() and not sys.waitUntil("IP_READY_IND", rstTim) then sys.restart("网络初始化失败!") end
while not ntp.isEnd() do sys.wait(1000) end
onenet_iot()
end
net.switchFly(false)
-- NTP同步失败强制重启
local tid = sys.timerStart(function()
net.switchFly(true)
sys.timerStart(net.switchFly, 5000, false)
end, flyTim)
sys.subscribe("IP_READY_IND", function()
sys.timerStop(tid)
log.info("---------------------- 网络注册已成功 ----------------------")
end)
-- 开启对应的adc通道
adc.open(2)
adc.open(3)
-- 定时每十秒读取adc值
sys.timerLoopStart(timer_proc,10000)
sys.taskInit(iot)
九、总结
本文力求从实际应用出发,在 OneNET 物联网开放平台上从无到有,将产品创建、设备创建以及数据点的创建等进行了较系统的讲解与操作实践,并在此过程中,配合 MQTTX 辅助工具,一步步测试验证,完成了 OneNET 物联网开放平台的比较全面的演示说明,尔后在此基础上,使用 Air724 开发板,以 onenet_studio 演示项目作为蓝本,详细地说明了修订过程,最后顺利完成 OneNET 物联网开放平台的功能演示。本文综合了对 JSON 数据、字符串,table 等数据类型的使用,同时在硬件方面操作了 PIN、定时器、ADC 采样,内容详实,是一个综合性比较强的一次实践。最后,希望本文对各位读者有用,并能解决大家一些实际问题。
十、常见问题
10.1 脚本下载后没有反应。
检查有没有下载底层核心,因为开发板出厂时,一般情况下是 AT 版本,此时如果不下载底层核心,Lua 脚本就运行不起来。
10.2 接上 USB 线后,不能下载,也不闪灯
检查 USB 线是否连接正确,Air724UG 开发板有两个 USB 口,旁边有 USB 字样的,才是下载用的 USB 通讯口,另一个 USB 口是 USB 转 TTL 的 UART 通讯端口,此端口不提供电源支持,因而当然不会有反应。
10.3 不小心变砖了怎么办?
在 Luatools 软件中,点击下载固件,选择底层核心文件,使能 USB BOOT 下载,按住开发板上的重启按钮,直到听到嘀的一声后开始下载,看到可松开按键提示后,松开按键。如下图。
10.4 一直连接不上网
检查天线是否连接完好,检查所在位置信号是否良好,检查 SIM 卡 是否锁死,检查 SIM 卡是否欠费等。可使用自己的手机卡换上去进行比较检查。如果手机卡能上网,则与硬件无关,此时可检查 SIM 卡的相关内容。
10.5 注意跨任务函数调用
在编写本文档的过程中,曾在定时器内调用发布函数,但一直返回错误。后请教合宙技术大佬问题才得到解决,这里特别提出来,也是为了避免大家在这里遇阻。具体的错误表现如下,供大家参考。
[2024-11-09 21:04:56.028] +EEMLTEINTRA:0,0,0,0,0,0,0,0,0
程序运行错误,请根据上方提示,找到对应lua文件修改程序!
[2024-11-09 21:05:05.221] lua /lua/socket4G.lua:336: socket:send: coroutine mismatch
[2024-11-09 21:05:05.221] stack traceback:
[2024-11-09 21:05:05.221] [C]: in function 'assert'
[2024-11-09 21:05:05.221] /lua/socket4G.lua:336: in function 'send'
[2024-11-09 21:05:05.221] /lua/mqtt.lua:210: in function 'write'
[2024-11-09 21:05:05.221] /lua/mqtt.lua:448: in function 'publish'
[2024-11-09 21:05:05.221] /lua/onenet.lua:69: in f
给读者的话
本篇文章由 PenGH 开发; 本篇文章描述的内容,如果有错误、细节缺失、细节不清晰或者其他任何问题,总之就是无法解决您遇到的问题; 请登录合宙技术交流论坛;合宙技术交流 用截图标注 + 文字描述的方式跟帖回复,记录清楚您发现的问题; 我们会迅速核实并且修改文档; 同时也会为您累计找错积分,您还可能赢取月度找错奖金!