晨旭Lua系列入门教程

第一期 下载调试工具LuaTools的使用指南

1 下载LuaTools工具

1.1 2G模块烧写教程(Air2xx系列、Air8xx系列):

1.2 刷入AT版本的lod

1.3 刷入luat版本的lod

1.4 下载lua脚本文件

1.5 4G模块烧写教程(Air7xx系列):

1.6 安装驱动

1.7 下载AT版本的固件

1.8 下载LUAT底层固件

1.9 烧录脚本文件

2 控制LED小灯

2.1 准备一个代码编辑器

2.2 模块化编程

2.3 连接硬件

2.4 点亮LED小灯

2.5 让LED灯闪烁起来

3 LUAT程序的基本时序

3.1 简单定时器函数的使用

3.2 协程

3.3 程序注册

4 学会使用并看懂luatools的trace信息

4.1 trace的几个基本部分

4.2 自定义输出trace(LuaTask格式)

4.3 语法错误输出

5 socket代码详解

5.1 Socket(TCP/UDP)

5.2 代码详解(LuaTask格式)

5.3 完整代码

5.4 验证功能

6 mqtt代码详解

6.1 先定义一个假装能用来测试的mqtt需求

6.2 建立Lua文件

6.3 完整代码

6.4 实现协议需求

7 串口收发

7.1 官方demo代码

7.2 先定义一个假装能用来测试的串口收发规则

7.3 建立文件结构

7.4 实现功能

7.5 处理串口数据截断问题

7.6 完整代码

7.7 最终测试

本来是想直接开始写脚本的hello world的教程,但是发现和多人从最基本的烧程序都不知道从何入手,官网给的资料也并不多(我在刚用的时候也卡在这里很久),所以就单独出一期LuaTools的使用教程。

下载LuaTools工具

下载工具一般要经过以下步骤:

打开合宙官网:http://www.openluat.com/
选择上面的“产品中心”,随便选一个产品,如:Air202 GPRS 通信模块
点击模块介绍那边的“资料下载”选项卡
点击“[LuaTools] Luat下载调试工具x.x.x”下载
当然,如果嫌麻烦,直接打开下面链接下载吧。。

下载后可能会进行自动升级,这个工具基本上是持续使用,都不会出现版本过旧的问题 (截止发稿Luatools最新版本为1.7.24)
alt
↑软件界面

2G模块烧写教程(Air2xx系列、Air8xx系列):

(4G模块烧录方法请看往下翻,看下一部分)

这时候,我们只需要用usb转串口线,与给模块/开发板的HOST串口相连,GND相连,并给模块上点即可

注意:模块的HOST口使用的是921600波特率,如果你使用的是劣质串口芯片(如质量差的pl2303以及山寨的ch340之类的),会出现无法通讯的现象。并且需要保证模块供电充足,不然会出现无限重启的状况。

下载lod固件文件/更改AT和luat底层版本

lod文件相当于一个模块的底层部分,lod文件可以分为AT版与luat版

AT版只能使用AT命令对模块进行控制,AT串口为uart1口
luat版为运行lua脚本所使用的底层,可对模块内部进行片内编程,完成一部分或全部的本应由外接单片机完成的工作

刷入AT版本的lod

alt
刷入AT版本的固件十分简单,在上面连接模块完成之后,确保串口选项卡的串口为你设备的串口值,点击菜单中的AT–切换为标准的AT版本,如下图:

点击下载为模块烧入AT固件:
alt![alt](http://openluat-luatcommunity.oss-cn-hangzhou.aliyuncs.com/images/20200219001535502_attachments-2018-08-YPmuTKwZ5b7eaf06b4f37.png
这样,AT版本固件的lod就烧录完成了。

刷入luat版本的lod

luat的固件因为需求不同,所以为了节省空间,提供了多种库的组合
luat的固件可以在下载工具目录的/LuaTools x.x.x/8955Lod/找到,每个版本的区别可以参考readme.txt文件中的描述,部分内容也贴在下面:

Luat_VXXXX_8955.lod:不支持SSL、TTS、SPI接口的LCD功能
Luat_VXXXX_8955_SSL.lod:支持SSL功能
Luat_VXXXX_8955_SSL_TTS.lod:支持SSL、TTS功能,不支持MP3、MIDI、录音功能
Luat_VXXXX_8955_SSL_UI.lod:支持SSL、SPI接口的LCD功能
Luat_VXXXX_8955_TTS_UI.lod:支持TTS、SPI接口的LCD功能
Luat_VXXXX_8955_TTS1.lod:支持TTS功能
Luat_VXXXX_8955_TTS2.lod:支持TTS功能、不支持MP3、MIDI、录音、json
Luat_VXXXX_8955_UI.lod:支持SPI接口的LCD功能
Luat_VXXXX_8955_SSL_FLOAT.lod:支持SSL功能、浮点数
Luat_VXXXX_8955_SSL_UI_FLOAT.lod:支持SSL功能、PI接口的LCD功能、浮点数
Luat_VXXXX_8955F.lod:64M flash版本(Air202F),支持SSL、TTS、SPI接口的LCD功能

我以烧录最基本的Luat_VXXXX_8955.lod为例,首先在调试工具中确保串口选项卡的串口为你设备的串口值,点击下载LOD(core)按钮,选中之前在工具目录的lod文件:
)![alt](http://openluat-luatcommunity.oss-cn-hangzhou.aliyuncs.com/images/20200219001640169_attachments-2018-08-UI7Ip9Yl5b7eaf14a2596.
选中确认无误后,点击下载进行写入lod的操作
png)alt
alt
上面下载失败了,要重新下载一遍
###下载lua脚本文件

由于luat模块的特色就是lua二次开发,所以下载烧录脚本的方法也是必须掌握的,下面以烧录S9开发板的示例程序为例(S9就是那个板载usb的开发板)

lib库已在luatools中自带,无需再自己下载

烧录S9需要的底层lod文件

S9需要的底层lod是Luat_VXXXX_8955_UI.lod,这里的UI库包含了字库和各种SPI、I2C屏幕的驱动
烧录步骤不再重复,请确保烧录最后成功了
烧录lua脚本

lua脚本分为两部分:一个部分是官方写的lib库文件(library),另一部分是用户自己的脚本文件。一般lib的文件不用去修改,除非有特殊需求。

lua库分为两个版本:
一个是老的script,
另一个是由稀饭放姜进行编写的script_LuaTask库

新项目都用script_LuaTask进行编写;老用户使用了script编写的,如码夫支付模块,可以继续用老版本,没有任何影响。

这里烧录的S9开发板就是使用的script_LuaTask进行编写的,具体步骤如下:

点击下载lua脚本按钮,将文件列表全部清空:
![alt](http://openluat-luatcommunity.oss-cn-hangzhou.aliyuncs.com/images/20200219002050281_attachments-2018-09-DIs4Ap8Z5b9b64f9e550e_QQ截图20180914153616.
点击添加脚本,将刚刚从git仓库下载的Luat_2G_RDA_8955/script_LuaTask/product/LuatBoard_Air202/demo下的所有文件全部选中,点击打开确认选中

这里如果没有选择lib文件,工具会自动进行提示

点击下载,会弹出对话框让你补全lib库文件,这里我们选择“优先推荐”的LuaTask库,点击确定:
png)alt
最后,等待下载成功即可
![alt](http://openluat-luatcommunity.oss-cn-hangzhou.aliyuncs.com/images/20200219002540439_attachments-2018-09-F81NV54U5b9b65a8806a1_QQ截图20180914153841.p
关闭对话框,即可看到设备运行的trace信息,这个信息可能会在下一章进行解释:
ng)alt
##4G模块烧写教程(Air7xx系列):

这里我们使用Air720开发板(S720 Border)作为演示,首先需要将开发板按如下方式连接电脑:

连接后,我们会在设备管理器看到有几个未安装的驱动,我们需要安装上这些驱动

安装驱动

<img id="portrait" src=http://openluat-luatcommunity.oss-cn-hangzhou.aliyuncs.com/images/20200221012058993_attachments-2018-09-OcuKrp1S5b9b596995edb_QQ截图20180914144646.png></img>
对于win8/win8.1/win10电脑,在安装驱动前需要进行如下操作(win7可以跳过该部分直接安装驱动):
win8:在桌面右侧点击弹出菜单中的“设置”,点击“更改电脑配置”,在左侧点击“通用”,接着点击右侧“高级启动”中的“立刻重启”按钮
win10:按下win+i快捷键,点击“更新和安全”—“恢复”—“高级启动”下的“立即重启”按钮

接着就进入了高级启动界面,选择“疑难解答”—“高级选项”—“启动设置”—“重启”
在启动设置页按F7选择禁用驱动强制数字签名,就可以在该模式下开机了。

我们可以在http://www.openluat.com/Product/4g/Air720D.html页面的资料下载,下载[驱动程序] AirM2M_USB_Download&Com_Driver_for_4G_V1.0

安装压缩包中的驱动。注意,32位与64位驱动不同,请根据自己电脑选好安装包exe
安装过程中肯定会弹出下面的窗口,点击始终安装即可

安装成功后,将板子重新连接即可。

下载AT版本的固件

注意:AT固件也可以直接通过AT指令来进行远程升级,无需使用LuaTools工具,详情请参阅http://oldask.openluat.com/article/186

如果是模块飞线烧录,请先确保可以保证模块的稳定供电,并且烧录时模块不会被重启,按下图进行飞线usb:

img id=”portrait” src=http://openluat-luatcommunity.oss-cn-hangzhou.aliyuncs.com/images/20200221012509038_5_51295.jpg>
打开luatools,将在菜单栏切换为4G模式:

直接点击AT–升级到标准AT版本即可,点击下载:

src=http://openluat-luatcommunity.oss-cn-hangzhou.aliyuncs.com/images/20200221012712634_attachments-2018-08-5cy7CJI85b7eafcd118f8.png>
这时工具会进入下载模块,我们可以点击开发板上的“重启”按键,或者开关旁边的拨码开关,下载工具会自动开始下载:

烧录后,我们可以用串口工具,通过虚拟的AT串口来进行AT操作:

同时,也可以从开发板的另一个usb引脚,将拨码改至UART2,进行AT控制:

下载LUAT底层固件

打开luatools,将在菜单栏切换为4G模式:
img id=”portrait” src=http://openluat-luatcommunity.oss-cn-hangzhou.aliyuncs.com/images/20200221020312010_attachments-2018-09-LfNmMIl85b9b5f5070499_QQ截图20180914151206.png>
首先要先烧录blf底层文件,点击右上角的下载CORE,
选择需要烧入的底层文件(可在LuaTools x.x.x\asr1802\CORE\Luat_V0005_ASR1802\文件夹找到),点击下载:

这时工具会进入下载模块,我们可以点击开发板上的“重启”按键,或者开关旁边的拨码开关,下载工具会自动开始下载:
img id=”portrait” src=http://openluat-luatcommunity.oss-cn-hangzhou.aliyuncs.com/images/20200221020928102_attachments-2018-09-PfYPshpL5b9b6076c12ef_QQ截图20180914151701.png>

底层烧录完成。

烧录脚本文件

我们用demo文件夹中的adc举例,烧录一个完整的脚本工程
(如果需要新建项目,千万不要在luatool下建立项目文件夹,不然会在更新时被删掉)
点击下载lua脚本,先点击打开4G(.blf)文件按钮,选择之前烧录的底层文件
然后点击清空所有:

接着点击添加脚本我们把LuaTools x.x.x\asr1802\lib\demo\adc文件夹下的所有文件添加进去,点击下载脚本
alt
这是会由于没有添加lib文件夹内的lua库文件而弹出一个对话框,我们直接点击确定,自动补全即可:
(如果没补全,那就手动把lib文件夹的所有文件都加进去)
alt
接着就和上面烧录固件一样了,点击开发板上的重启按键,或者开关旁边的拨码开关,下载工具会自动开始下载
alt
alt
烧录完成后,关闭烧录对话框,检查一下左上角是否勾上了自动usb口,如果已勾选,等待十几秒后即可看到luatools会输出adc demo的trace:
alt

第二期 控制LED小灯

本教程使用的开发板为S9开发板,使用其他开发板的请使用杜邦线连接相关的引脚,代码也请根据需要自行修改

接触过单片机的人应该都知道,使用一款单片机,第一件事就是学会如何点亮LED灯,学会了控制LED灯,就相当于学会了最基础的GPIO高低电平控制操作

准备一个代码编辑器

代码编辑器有很多可供选择,比如notepad++、Sublime Text

如果你已经可以熟练掌握了某一个代码编辑器,那么你可以直接跳过这一章,直接看代码编写。如果你想试试我推荐的编辑器,那么也可以看下去

详细的安装和配置,请参考这篇文章vscode lua开发推荐配置

新建一个最基本的工程

在你觉得适当的位置,新建一个文件夹,名为LUAT-LED,使用你的代码编辑器打开该文件夹,vscode如下图所示操作:
alt
打开后,在编辑器左侧的文件夹中,右击空白处,新建文件,输入文件名main.lua,回车键保存:
alt
接着,在新建的文件夹中添加如下代码,完成最主要的main.lua文件的编写:

--必须在这个位置定义PROJECT和VERSION变量
--PROJECT:ascii string类型,可以随便定义,只要不使用,就行
--VERSION:ascii string类型,如果使用Luat物联云平台固件升级的功能,必须按照"X.X.X"定义,X表示1位数字;否则可随便定义
PROJECT = "LED-TEST"
VERSION = "0.0.1"
--根据固件判断模块类型
moduleType = string.find(rtos.get_version(),"8955") and 2 or 4
--加载日志功能模块,并且设置日志输出等级
--如果关闭调用log模块接口输出的日志,等级设置为log.LOG_SILENT即可
require "log"
LOG_LEVEL = log.LOGLEVEL_TRACE
require "sys"
require "net"
--每1分钟查询一次GSM信号强度
--每1分钟查询一次基站信息
net.startQueryAll(60000, 60000)
--加载硬件看门狗功能模块
--根据自己的硬件配置决定:1、是否加载此功能模块;2、配置Luat模块复位单片机引脚和互相喂狗引脚
--合宙官方出售的Air201开发板上有硬件看门狗,所以使用官方Air201开发板时,必须加载此功能模块
--如果用的是720 4g模块,请注释掉这两行
require "wdt"
wdt.setup(pio.P0_30, pio.P0_31)
--加载网络指示灯功能模块
--根据自己的项目需求和硬件配置决定:1、是否加载此功能模块;2、配置指示灯引脚
--合宙官方出售的Air800和Air801开发板上的指示灯引脚为pio.P0_28,其他开发板上的指示灯引脚为pio.P1_1
require "netLed"
netLed.setup(true,moduleType == 2 and pio.P1_1 or pio.P2_0,moduleType == 2 and nil or pio.P2_1)--自动判断2/4g默认网络灯引脚配置
--网络指示灯功能模块中,默认配置了各种工作状态下指示灯的闪烁规律,参考netLed.lua中ledBlinkTime配置的默认值
--如果默认值满足不了需求,此处调用netLed.updateBlinkTime去配置闪烁时长
--加载错误日志管理功能模块【强烈建议打开此功能】
--如下2行代码,只是简单的演示如何使用errDump功能,详情参考errDump的api
require "errDump"
errDump.request("udp://ota.airm2m.com:9072")
--加载远程升级功能模块【强烈建议打开此功能】
--如下3行代码,只是简单的演示如何使用update功能,详情参考update的api以及demo/update
-- PRODUCT_KEY = "xxxxxx"
-- require "update"
-- update.request()
--启动系统框架
sys.init(0, 0)
sys.run()

我们将main.lua和基本的task库文件烧录到开发板中(不烧录会的请看第一章),会发现:并没有什么事情发生
因为代码基本是空的啊2333
##模块化编程

在编写lua功能时,我们一般会把相似功能的代码放到同一个文件中,写完后只需要在main.lua中添加require语句即可,所以我们需要将main.lua结尾改成如下形式:

....上面一堆代码省略
--加载远程升级功能模块【强烈建议打开此功能】
--如下3行代码,只是简单的演示如何使用update功能,详情参考update的api以及demo/update
-- PRODUCT_KEY = "xxxxxx"
-- require "update"
-- update.request()
require "ledtest"    --新加上的代码
--启动系统框架
sys.init(0, 0)
sys.run()

添加完后,使用和新建main.lua文件相同的方式,新建一个新的文件ledtest.lua

我们在ledtest.lua的第一行可以先加上如下一句话:

module(..., package.seeall)  --使得文件中的函数在何处都可调用

连接硬件

注意:这里演示用的是2g模块,4g模块请详细阅读下面代码注释中的信息进行修改,代码不可以直接拿去用!!

由于我这里使用的是普通的S9开发板,和一个配套的LED灯小主板,所以我直接将其连接到了双排针上,插入方式如下图:
alt
为了照顾其他未使用S9开发板的读者,我将led的电气连接在下方进行标识,有条件的可以手动按下文进行连接(用的是Air202模块,其他模块请根据情况自行修改):
引脚名称 灯序号 另一端连接哪里
SPI1_CLK/GPIO_8 LED1 GND
SPI1_DO/GPIO_11 LED2 GND
SPI1_DI/GPIO_12 LED3 GND
UART1_CTS/GPIO_3 LED4 GND
UART1_RTS/GPIO_2 LED5 GND

可以看到,每个管脚都有各种复用功能,我们本文之将其作为普通GPIO口使用

点亮LED小灯

模块中几乎所有的函数都可以在wiki中找到,所以我们也去wiki中进行搜索
打开openluat的wiki页:http://wiki.openluat.com/
在网页左边选择Luat API接口,可以看到所有接口都被整理好放到了这里,点击LuaTask—pins可以找到我们需要的函数接口:
alt
这样我们就明白这个改如何点亮LED小灯了,我们将ledtest.lua改成如下代码:

module(..., package.seeall)
require"pins"  --用到了pin库,该库为luatask专用库,需要进行引用
-- GPIO 0到GPIO 31表示为pio.P0_0到pio.P0_31 。
-- GPIO 32到GPIO XX表示为pio.P1_0到pio.P1_(XX-32),例如GPIO33 表示为pio.P1_1
if moduleType == 2 then
    pmd.ldoset(5,pmd.LDO_VMMC)  --使用某些GPIO时,必须在脚本中写代码打开GPIO所属的电压域,配置电压输出输入等级,这些GPIO才能正常工作
end
--注意!!!4G模块无需设置电压域!
--设置led的GPIO口
local led1 = pins.setup(pio.P0_8,0)--如果你用的是4G模块,请更改这个gpio编号
local led2 = pins.setup(pio.P0_11,0)--如果你用的是4G模块,请更改这个gpio编号
local led3 = pins.setup(pio.P0_12,0)--如果你用的是4G模块,请更改这个gpio编号
local led4 = pins.setup(pio.P0_3,0)--如果你用的是4G模块,请更改这个gpio编号
local led5 = pins.setup(pio.P0_2,0)--如果你用的是4G模块,请更改这个gpio编号
--将gpio口都置为高电平
led1(1)
led2(1)
led3(1)
led4(1)
led5(1)

更改完保存后,将改好的文件全部烧入模块中,查看效果
如果一切正常的话,五个灯都会正常被点亮
将led1(1)改为led1(0)即可熄灭第一个灯,以此类推,可以多尝试更改着玩一下,再看下一部分
让LED灯闪烁起来

LED灯已经可以点亮了,那么我们就要让它动起来
我们可以在wiki页查到,开启一个定时器函数为sys.timerStart(fnc, ms, …),那么我们可以将代码改成如下样式:

module(..., package.seeall)
require"pins"  --用到了pin库,该库为luatask专用库,需要进行引用
-- GPIO 0到GPIO 31表示为pio.P0_0到pio.P0_31 。
-- GPIO 32到GPIO XX表示为pio.P1_0到pio.P1_(XX-32),例如GPIO33 表示为pio.P1_1
if moduleType == 2 then
    pmd.ldoset(5,pmd.LDO_VMMC)  --使用某些GPIO时,必须在脚本中写代码打开GPIO所属的电压域,配置电压输出输入等级,这些GPIO才能正常工作
end
--注意!!!4G模块无需设置电压域!
--设置led的GPIO口
local led1 = pins.setup(pio.P0_8,0)--如果你用的是4G模块,请更改这个gpio编号
local led2 = pins.setup(pio.P0_11,0)--如果你用的是4G模块,请更改这个gpio编号
local led3 = pins.setup(pio.P0_12,0)--如果你用的是4G模块,请更改这个gpio编号
local led4 = pins.setup(pio.P0_3,0)--如果你用的是4G模块,请更改这个gpio编号
local led5 = pins.setup(pio.P0_2,0)--如果你用的是4G模块,请更改这个gpio编号
local ledon = false --led是否开启
function changeLED()
    if ledon then
        led1(1)
        led2(1)
        led3(1)
        led4(1)
        led5(1)
    else
        led1(0)
        led2(0)
        led3(0)
        led4(0)
        led5(0)
    end
    ledon = not ledon
    sys.timerStart(changeLED,1000)--一秒后执行指定函数
end
changeLED() --开机后立刻运行该函数

保存后烧入程序即可,如果不出意外,五个LED灯就应该一秒亮一秒灭了

既然学会了延时、点亮LED、熄灭LED,那么就可以自己尝试编写一个流水灯了,这里不再赘述,请大家自己尝试

第三期 LUAT程序的基本时序

本文只会介绍LuaTask中多线程任务的基本用法,不会过多的讨论原理,如果需要深入研究,请查看wiki页的详细介绍:http://wiki.openluat.com

适合阅读本文的人
至少用过一款单片机的
接触、了解过或听说过rtos、ucos等多线程系统
前几篇文章所提内容都已经懂了的
有耐心看完本文的
对lua语法熟悉的,如不熟悉请移步
http://www.runoob.com/lua/lua-tutorial.html
或
https://www.lua.org/manual/5.1/manual.html
进行学习。

本文只会介绍LuaTask中多线程任务的基本用法,不会过多的讨论原理,如果需要深入研究,请查看wiki页的详细介绍:http://wiki.openluat.com/doc/run/

简单定时器函数的使用

上一章里的点亮led小灯,最后一步是让led灯闪烁起来。习惯了c语言写代码的人可能会发现,这个lua程序中好像没有delay之类的函数。

我们把上一章的代码简化一下,像如下这样:

function sayHello()
print("hello")
sys.timerStart(sayHello,1000)--一秒后执行指定函数
print("bye")
end

sayHello()

.....其他代码

这里使用了定时器的接口:sys.timerStart(fnc, ms, …),用过js之类语言的可能会感到特别熟悉。在luat中,一般不在主线程中使用类似c一样用死循环阻塞式地来延时,取而代之的是这种定时器的结构,流程图如下:

alt

其实实现上述定时器循环执行某函数的功能,可以直接使用sys.timerLoopStart(fnc, ms, …)接口,改成如下样式:

function sayHello()
print(“hello”)
end

sys.timerLoopStart(sayHello,1000)

…..其他代码

Lua
Copy

整个流程图便变成了如下的顺序:

alt

为了使函数更简洁,我们可以直接把函数名改成function() ….. end的形式,在函数变量中直接定义要使用的函数:

sys.timerLoopStart(function()
print(“hello”)
end,1000)

Lua
Copy

甚至可以写成一行:

sys.timerLoopStart(function() print(“hello”) end,1000)

Lua
Copy

上述两段代码的功能与一开始的timerLoopStart示例代码的运行结果完全相同

协程

在wiki页中,给出了这样一段简洁的代码,我们为了照顾不熟悉lua语言的人,稍微改一下代码:

test.lua

function test()
    while true do
        print("ss function test")
        sys.wait(1000)          -- 挂起1000ms,同理为每隔1000ms运行一次
    end
end

sys.taskInit(test)

.....其他代码

在这里,sys.taskInit的作用可以理解为创建了一个新的线程,这个线程运行的内容就是test()函数

在test()进行while true这样的死循环时,并不会使其他程序被阻塞运行,反而是多线程运行的

在LuaTask架构下,协程内的函数可以直接使用sys.wait(ms)函数进行延时操作,延时途中只是将cpu让给了其他需要运行的程序,在倒计时完成后继续该进程的运行,从而实现了延时操作

我们一般会把sys.taskInit内的函数直接写在其中,像下面这样:

sys.taskInit(function()
    while true do
        print("ss function test")
        sys.wait(1000)          -- 挂起1000ms,同理为每隔1000ms运行一次
    end
end)

.....其他代码

相信各位已经可以理解这种写法了(如果从文章开头看到这里了的话),协程的运行流程图如下

alt

我们可以同时开多个线程,使用方法都是一样的

程序注册

LuaTask可以使用订阅和发布,使某个程序等待另一个程序完成后才继续运行

我们来看最简单的一个示例代码:

alt

程序定义了一个5秒后的定时器,并运行了一个包含了时间订阅等待的函数,整体流程运行如下:

alt

这种用法经常用在http、socket等操作和其他需要等待的操作中,利用回调函数可实现等待功能。

sys.publish也可传递参数:

sys.timerStart(function()
sys.publish(“TEST”,123)
end,3000)–三秒后执行发布”TEST”消息的函数

function sub()
print(“start”)
result,data = sys.waitUntil(“TEST”)
print(result,data)
end

sys.taskInit(sub)

Lua
Copy

上述代码将会输出:

true 123

同时,luat在库中自带了许多系统消息,部分如下:

alt

三种线程控制的使用方法全部介绍完毕了,如果需要了解原理,请去研读http://wiki.openluat.com 的说明与解释

第四期 学会使用并看懂luatools的trace信息

由于luat这个架构并不能直接连接仿真器进行调试,所以也无法在程序中设置断点来检查自己代码是否有问题,所以在开发过程中,一般我们都是靠各种print来输出trace获取程序运行的各种状态的。 并且由于lua是脚本文件,烧录时并没有进行编译,所以就算是报错,报错信息也可以准确地把错误所在行的具体位置详细指出来,方便我们进行排查问题

适合阅读本文的人需要:
理解或已经学习了前几章的内容
熟悉lua语法
有实际模块,可以自己实践验证
能耐心阅读完本文
有问题会在文章下方进行留言

由于luat这个架构并不能直接连接仿真器进行调试,所以也无法在程序中设置断点来检查自己代码是否有问题,所以在开发过程中,一般我们都是靠各种print来输出trace获取程序运行的各种状态的。
并且由于lua是脚本文件,烧录时并没有进行编译,所以就算是报错,报错信息也可以准确地把错误所在行的具体位置详细指出来,方便我们进行排查问题

trace的几个基本部分

首先我们随便烧录一个程序,就只包含下面一个main.lua和自带的LuaTask库文件好了:
main.lua

PROJECT = "LOG-TEST"
VERSION = "1.0.0"
--加载日志功能模块,并且设置日志输出等级
--如果关闭调用log模块接口输出的日志,等级设置为log.LOG_SILENT即可
require "log"
LOG_LEVEL = log.LOGLEVEL_TRACE
require "sys"
require "net"
--每1分钟查询一次GSM信号强度
--每1分钟查询一次基站信息
net.startQueryAll(60000, 60000)
--加载硬件看门狗功能模块
--根据自己的硬件配置决定:1、是否加载此功能模块;2、配置Luat模块复位单片机引脚和互相喂狗引脚
--合宙官方出售的Air201开发板上有硬件看门狗,所以使用官方Air201开发板时,必须加载此功能模块
require "wdt"
wdt.setup(pio.P0_30, pio.P0_31)
--加载网络指示灯功能模块
--根据自己的项目需求和硬件配置决定:1、是否加载此功能模块;2、配置指示灯引脚
--合宙官方出售的Air800和Air801开发板上的指示灯引脚为pio.P0_28,其他开发板上的指示灯引脚为pio.P1_1
require "netLed"
netLed.setup(true,pio.P1_1)
--加载错误日志管理功能模块【强烈建议打开此功能】
--如下2行代码,只是简单的演示如何使用errDump功能,详情参考errDump的api
require "errDump"
errDump.request("udp://ota.airm2m.com:9072")
--启动系统框架
sys.init(0, 0)
sys.run()

程序中除了必要的声明软件版本和名称、初始化看门狗、初始化网络灯、加载错误日志管理功能模块、启动系统框架之外,没有进行其他任何操作(lib库文件和lod底层里进行的各种操作除外)
我们可以将这个文件烧录到开发板中,查看trace的各种信息(这里用的是S9开发板):
alt
alt

↑显示烧录成功后,最好立即关闭烧录界面,以免错过一些开头的trace信息。实际开发中一般错过了也没事,反正开头的一些东西也没什么用

我们可以看到trace中输出了一些信息:
alt
由于trace中大多数的信息是由lib库文件打印出来的,随时可能会有更改,所以下面也就讲个大概,具体要以实际为准

为了更好地观察,我们可以点击停止打印按钮,以免刷新新的log信息导致滚动条回到最低端:
alt
[时间]: CDFU_LoadSection: %d-%d
[时间]: CDFU_LoadSection: %d-%d
[时间]: ====================================
[时间]: INTR VER :Luat_V0027_8955_SSL_UI
[时间]: BASE VER :B5431
[时间]: SCRIPT ADDR :0x002b0000
[时间]: SCRIPT SIZE :0x000b0000
[时间]: BASE ADDR :0x00220000
[时间]: BASE SIZE :0x00090000
[时间]: ====================================
[时间]: [cust_task_main]: Enter message loop
[时间]: INTEGRITY file not exist!
[时间]: FH:file=/lua/wdt.lua,len=589,offset=29006
[时间]: FH:file=/lua/utils.lua,len=2526,offset=26455
[时间]: FH:file=/lua/sys.lua,len=5472,offset=20956
[时间]: FH:file=/lua/sim.lua,len=1084,offset=19847
[时间]: FH:file=/lua/ril.lua,len=7790,offset=12032
[时间]: FH:file=/lua/pins.lua,len=946,offset=11061
[时间]: FH:file=/lua/patch.lua,len=1186,offset=9849
[时间]: FH:file=/lua/netLed.lua,len=2601,offset=7221
[时间]: FH:file=/lua/net.lua,len=5141,offset=2052
[时间]: FH:file=/lua/main.lua,len=280,offset=1747
[时间]: FH:file=/lua/log.lua,len=1174,offset=547
[时间]: FH:file=/lua/clib.lua,len=472,offset=50
[时间]: parse_luadb_data:delupdpack=0,err=0,section=1,wrFile=0
[时间]: INTEGRITY file write begin!
[时间]: lualibc_fopen /integrity.bin wb 601 1 2
[时间]: INTEGRITY file write success!
[时间]: [fopen_ext]: /lua/main.lua 2!
[时间]: RUN main.lua
[时间]: [fopen_ext]: /lua/main.lua 2!
[时间]: [fopen_ext]: /lua/log.lua 2!
[时间]: [fopen_ext]: /lua/log.lua 2!
[时间]: [fopen_ext]: /lua/sys.lua 2!
[时间]: [fopen_ext]: /lua/sys.lua 2!
[时间]: [fopen_ext]: /lua/utils.lua 2!
[时间]: [fopen_ext]: /lua/utils.lua 2!
[时间]: [fopen_ext]: /lua/patch.lua 2!
[时间]: [fopen_ext]: /lua/patch.lua 2!
[时间]: [fopen_ext]: /lua/clib.lua 2!
[时间]: [fopen_ext]: /lua/clib.lua 2!
[时间]: [fopen_ext]: /lua/net.lua 2!
[时间]: [fopen_ext]: /lua/net.lua 2!
[时间]: [fopen_ext]: /lua/ril.lua 2!
[时间]: [fopen_ext]: /lua/ril.lua 2!
[时间]: [fopen_ext]: /lua/sim.lua 2!
[时间]: [fopen_ext]: /lua/sim.lua 2!
[时间]: [fopen_ext]: /lua/wdt.lua 2!
[时间]: [fopen_ext]: /lua/wdt.lua 2!
[时间]: [fopen_ext]: /lua/pins.lua 2!
[时间]: [fopen_ext]: /lua/pins.lua 2!
[时间]: [I]-[wdt.taskWdt] AirM2M –> WATCHDOG : OK
[时间]: [fopen_ext]: /lua/netLed.lua 2!
[时间]: [fopen_ext]: /lua/netLed.lua 2!
/**温馨提示******/
软件开机,如果是意外重启,请去http://wiki.openluat.com/doc/luatApi/#rtospoweron,查看
/**提示结束******/

开头的这一些代码表示读取到了上述的这些文件,可以用于后续的运行

注意:
我们烧录进模块的lua文件一般都在/lua/路径下
而烧录的其他文件如png图片、mp3音乐,则会在/ldata/路径下

[时间]: [I]-[poweron reason:]    3    LOG-TEST    1.0.0    2.0.8    Luat_V0027_8955_SSL_UI

这一行代表了上次关机的原因、脚本工程里定义的名称(main.lua第1行写的)、脚本工程里定义的版本号(main.lua第2行写的)、LuaTask库文件的版本号(1.x.x是旧版的lua script,2.x.x是LuaTask)、lod固件的版本号

[时间]: LJD VSIM sim_SetNextVoltage:1048,sim_present=0,IsVsim=0
[时间]: LJD VSIM sim_ProcessInstruction:2784,sim_present=1,IsVsim=1

这两行的IsVsim=1代表了当前模块拥有虚拟sim卡,sim_present=1代表没检测到外置sim卡,所以就使用内置的虚拟sim卡(可能是吧,我猜的)

[时间]: [I]-[ril.defrsp]    AT+CMEE=0    true    OK    nil
[时间]: [I]-[ril.sendat]    AT+CREG=2
[时间]: [I]-[ril.proatc]
[时间]: [I]-[ril.proatc]    OK
[时间]: [I]-[ril.defrsp]    AT+CREG=2    true    OK    nil
[时间]: [I]-[ril.sendat]    AT+CREG?
[时间]: [I]-[ril.proatc]
[时间]: [I]-[ril.proatc]    +CREG: 2,0
[时间]: [I]-[ril.proatc]
[时间]: [I]-[ril.proatc]    OK
[时间]: [I]-[ril.defrsp]    AT+CREG?    true    OK    nil
[时间]: [I]-[ril.sendat]    AT+CENG=1,1
[时间]: [I]-[ril.proatc]
[时间]: [I]-[ril.proatc]    OK
[时间]: [I]-[ril.sendat]    AT+CSQ
[时间]: [I]-[ril.proatc]
[时间]: [I]-[ril.proatc]    +CSQ: 0,0
[时间]: [I]-[ril.proatc]
[时间]: [I]-[ril.proatc]    OK
[时间]: [I]-[ril.sendat]    AT+CENG?
[时间]: [I]-[ril.proatc]
[时间]: [I]-[ril.proatc]    +CENG:1,1
[时间]: [I]-[ril.proatc]    +CENG:0,"0000, 0, 0,000,00,00,0000,00,00,0000,00"
[时间]: [I]-[ril.proatc]
[时间]: [I]-[ril.proatc]    OK
[时间]: [I]-[ril.sendat]    AT+CSQ
[时间]: [I]-[ril.proatc]
[时间]: [I]-[ril.proatc]    +CSQ: 0,0
[时间]: [I]-[ril.proatc]
[时间]: [I]-[ril.proatc]    OK
[时间]: [I]-[ril.sendat]    AT+CENG?
[时间]: [I]-[ril.proatc]
[时间]: [I]-[ril.proatc]    +CENG:1,1
[时间]: [I]-[ril.proatc]    +CENG:0,"0000, 0, 0,000,00,00,0000,00,00,0000,00"
[时间]: [I]-[ril.proatc]
[时间]: [I]-[ril.proatc]    OK
[时间]: [I]-[ril.proatc]
[时间]: [I]-[ril.proatc]    +CSIMTYPE: 0,0
[时间]: [I]-[ril.defurc]    +CSIMTYPE: 0,0
[时间]: [I]-[ril.proatc]
[时间]: [I]-[ril.proatc]    +ENCRET: 0
[时间]: [I]-[ril.defurc]    +ENCRET: 0
[时间]: [I]-[ril.proatc]
[时间]: [I]-[ril.proatc]    +CPIN: READY

上面这些就是lua脚本底层里,调用AT接口来对模块进行的操作了。熟悉AT指令的人会一下子就明白这些,不熟悉的人可以无视,只要知道这些是调用的AT指令就可以,操作基本都由底层和lib库来完成,一般情况下不用接触

[时间]: [I]-[wdt.taskWdt]    AirM2M --> WATCHDOG : OK
[时间]: [I]-[wdt.taskWdt]    AirM2M <-- WatchDog : OK

这是看门狗的喂狗操作。。。我觉得我不用解释了吧。。。

[时间]: [I]-[ril.defrsp]    AT+CIPSTATUS    true    OK    nil
[时间]: [I]-[ril.proatc]
[时间]: [I]-[ril.proatc]    STATE: IP INITIAL
[时间]: [I]-[link.STATE]    IP STATUS    IP INITIAL
[时间]: [I]-[ril.proatc]
[时间]: [I]-[ril.proatc]    C: 0,,"","","","INITIAL"
[时间]: [I]-[ril.defurc]    C: 0,,"","","","INITIAL"
[时间]: [I]-[ril.proatc]    C: 1,,"","","","INITIAL"
[时间]: [I]-[ril.defurc]    C: 1,,"","","","INITIAL"
[时间]: [I]-[ril.proatc]    C: 2,,"","","","INITIAL"
[时间]: [I]-[ril.defurc]    C: 2,,"","","","INITIAL"
[时间]: [I]-[ril.proatc]    C: 3,,"","","","INITIAL"
[时间]: [I]-[ril.defurc]    C: 3,,"","","","INITIAL"
[时间]: [I]-[ril.proatc]    C: 4,,"","","","INITIAL"
[时间]: [I]-[ril.defurc]    C: 4,,"","","","INITIAL"
[时间]: [I]-[ril.proatc]    C: 5,,"","","","INITIAL"
[时间]: [I]-[ril.defurc]    C: 5,,"","","","INITIAL"
[时间]: [I]-[ril.proatc]    C: 6,,"","","","INITIAL"
[时间]: [I]-[ril.defurc]    C: 6,,"","","","INITIAL"
[时间]: [I]-[ril.proatc]    C: 7,,"","","","INITIAL"
[时间]: [I]-[ril.defurc]    C: 7,,"","","","INITIAL"
[时间]: [I]-[ril.sendat]    AT+CSTT="CMIOT","",""
[时间]: [I]-[ril.proatc]
[时间]: [I]-[ril.proatc]    OK
[时间]: [I]-[ril.defrsp]    AT+CSTT="CMIOT","",""    true    OK    nil
[时间]: [I]-[ril.sendat]    AT+CIICR
[时间]: [I]-[ril.proatc]
[时间]: [I]-[ril.proatc]    OK
[时间]: [I]-[ril.defrsp]    AT+CIICR    true    OK    nil
[时间]: [I]-[ril.proatc]
[时间]: [I]-[ril.proatc]    STATE: IP GPRSACT
[时间]: [I]-[link.STATE]    IP STATUS    IP GPRSACT
[时间]: [I]-[ril.sendat]    AT+CIFSR
[时间]: [I]-[ril.proatc]
[时间]: [I]-[ril.proatc]    010.007.081.239
[时间]: [I]-[ril.defrsp]    AT+CIFSR    true    nil    010.007.081.239
[时间]: [I]-[ril.sendat]    AT+CIPSTATUS
[时间]: [I]-[ril.proatc]
[时间]: [I]-[ril.proatc]    OK
[时间]: [I]-[ril.defrsp]    AT+CIPSTATUS    true    OK    nil
[时间]: [I]-[ril.proatc]
[时间]: [I]-[ril.proatc]    STATE: IP STATUS
[时间]: [I]-[link.STATE]    IP STATUS    IP STATUS
[时间]: [I]-[ril.proatc]
[时间]: [I]-[ril.proatc]    C: 0,,"","","","INITIAL"
[时间]: [I]-[ril.defurc]    C: 0,,"","","","INITIAL"
[时间]: [I]-[ril.proatc]    C: 1,,"","","","INITIAL"
[时间]: [I]-[ril.defurc]    C: 1,,"","","","INITIAL"
[时间]: [I]-[ril.proatc]    C: 2,,"","","","INITIAL"
[时间]: [I]-[ril.defurc]    C: 2,,"","","","INITIAL"
[时间]: [I]-[ril.proatc]    C: 3,,"","","","INITIAL"
[时间]: [I]-[ril.defurc]    C: 3,,"","","","INITIAL"
[时间]: [I]-[ril.proatc]    C: 4,,"","","","INITIAL"
[时间]: [I]-[ril.defurc]    C: 4,,"","","","INITIAL"
[时间]: [I]-[ril.proatc]    C: 5,,"","","","INITIAL"
[时间]: [I]-[ril.defurc]    C: 5,,"","","","INITIAL"
[时间]: [I]-[ril.proatc]    C: 6,,"","","","INITIAL"
[时间]: [I]-[ril.defurc]    C: 6,,"","","","INITIAL"
[时间]: [I]-[ril.proatc]    C: 7,,"","","","INITIAL"
[时间]: [I]-[ril.defurc]    C: 7,,"","","","INITIAL"

熟悉AT的各位如果看到这两大串,一定又明白了,这模块连上GPRS网络了。不明白的大概了解一下就好,可以在测试时作为参考

基本的trace就讲到这里,具体的自己去研究吧

自定义输出trace(LuaTask格式)

在第一版lua script时代,输出的trace基本都是直接使用print(xxxxx)这种格式,这个输出习惯的效果不怎么样,也不利于排查问题
于是在稀饭放姜大佬写的LuaTask版本中,使用了log语句,输出整齐具有区分度较高格式的trace

老套路,我们在上面的main.lua中稍作修改,在–启动系统框架的上方添加如下代码:

require"logtest"

并且新建一个lua文件,名为logtest.lua,文件内容如下:

require"common"
local function test()
    log.info("logtest.test",common.utf8ToGb2312("输出info级别的日志"))
    log.debug("logtest.test",common.utf8ToGb2312("输出debug级别的日志"))
    log.trace("logtest.test",common.utf8ToGb2312("输出trace级别的日志"))
    log.warn("logtest.test",common.utf8ToGb2312("输出warn级别的日志"))
    log.error("logtest.test",common.utf8ToGb2312("输出error级别的日志"))
    log.fatal("logtest.test",common.utf8ToGb2312("输出fatal级别的日志"))
end
test()

注意:
由于vscode新建文件默认都为utf8编码格式,
所以我们在代码中输出的中文字符也都为utf8编码格式。但是luatools使用的却是GB2312格式,所以为了输出正确不乱码的中文,我们需要对中文进行转码,使用common.utf8ToGb2312()函数就可以解决这个编码问题
输出英文没有类似问题,无需转码

我们把logtest.lua文件添加到烧录工程中,将程序烧录进去,可以得到如下的输出结果(需要自己在luatools往上翻地找):

同时,log输出也可以改成如下语句:

testlog.lua

local function test()
    local a = 1
    local b = true
    local c = "test"
    log.info("logtest.test","mixed output",a,b,c)
end
test()

输出如下:

[时间]: [I]-[logtest.test]    mixed output    1    true    test

这些输出日志方便了我们调试的过程,我们可以在程序必要处添加这些log输出,以便通过trace掌握程序运行的状态
##语法错误输出

为了演示,我们可以再次更改testlog.lua文件的内容如下:

testlog.lua

local function test()
    local a
    local b = 10
    b = a + b
end
test()

显而易见,这是会产生语法错误的,没看懂的也不要紧,烧录到模块中,可以看一下trace输出了什么:

[时间]: [E]-[errDump.luaErr]    /lua/logtest.lua:4: attempt to perform arithmetic on local 'a' (a nil value)
程序运行错误,请根据上方提示,找到对应lua文件修改程序
[时间]: stack traceback:
[时间]:     /lua/logtest.lua:4: in function 'test'
[时间]:     /lua/logtest.lua:7: in main chunk
[时间]:     [C]: in function 'require'
[时间]:     /lua/main.lua:34: in main chunk
[时间]:     [C]: ?
[时间]: [fopen_ext]: /lua/logtest.lua 2!
[时间]: [fopen_ext]: /lua/logtest.lua 2!
[时间]: lualibc_fopen /luaerrinfo.txt w 601 1 2
[时间]: lua: /lua/logtest.lua:4: attempt to perform arithmetic on local 'a' (a nil value)
程序运行错误,请根据上方提示,找到对应lua文件修改程序
[时间]: stack traceback:
[时间]:     /lua/logtest.lua:4: in function 'test'
[时间]:     /lua/logtest.lua:7: in main chunk
[时间]:     [C]: in function 'require'
[时间]:     /lua/main.lua:34: in main chunk
[时间]:     [C]: ?
[时间]: [lua_shell_main]: lua exit status 1

第一行的/lua/logtest.lua:4: attempt to perform arithmetic on local ‘a’ (a nil value)代表了这个错误的具体原因,我们可以直接到错误的行数寻找,同时错误信息也写得十分明确:a是个nil值,无法进行算术计算

如果你觉得这个报错十分准确的话,那你就错了,如果真是这样我就不会在下面再加一段解释了

再次修改testlog.lua文件的内容如下:

testlog.lua

require"utils"
local function test()
    local a = ""
    log.info("logtest.test",string.toHex(a))
end
test()

报错信息如下:

[时间]: lua: /lua/utils.lua:19: attempt to index local 'str' (a nil value)
程序运行错误,请根据上方提示,找到对应lua文件修改程序
[时间]: stack traceback:
[时间]:     /lua/utils.lua:19: in function 'toHex'
[时间]:     /lua/logtest.lua:5: in function 'test'
[时间]:     /lua/logtest.lua:8: in main chunk
[时间]:     [C]: in function 'require'
[时间]:     /lua/main.lua:34: in main chunk
[时间]:     [C]: ?
[时间]: [lua_shell_main]: lua exit status 1

可能有人看到这个报错就凌乱了:utils.lua?这文件不是我写的啊?

确实,utils.lua这个文件是lib文件夹中的库文件,库文件中的函数报错也会导致trace中显示这个文件的错误位置
我们可以在错误报告中找到/lua/logtest.lua:5 : in function ‘test’这一段,在检查后发现,确实是logtest.lua文件的第五行导致了错误:对nil进行转成16进制字符串的处理是非法的

以上就是luatools中trace解析的基本解释,很多trace信息还需要各位自己来摸索,用的多了也就熟悉了这些信息了

第五期 socket代码详解

TCP和UDP除了在lua代码声明时有一些不同,其他地方完全一样,所以下面的代码将以TCP长连接的数据收发作为示例,如果需要UDP连接,只需要改声明对象时的三个字母即可。

阅读本文需要具有的技能:
看过该系列前几篇文章或明白前几篇文章内容的
熟悉lua语法,尤其是数组部分
可以明白字符串、字节码之间的区别
可以自己实践操作
对tcp/udp通讯有基本的了解
用过这东西

各位想看mqtt解释的,请等待下一篇文章,不过也可以顺便看看这一篇嘛,二者都差不多的

Socket(TCP/UDP)

TCP和UDP除了在lua代码声明时有一些不同,其他地方完全一样,所以下面的代码将以TCP长连接的数据收发作为示例,如果需要UDP连接,只需要改声明对象时的三个字母即可。

先定义一个假装能用来测试的TCP协议(需求)

客户端每10秒发送一条字符串heart beat
客户端接收到back开头的数据要回复相同的数据
客户端收到bin要回复二进制数组0x11 0x22 0x33
客户端收到time要回复当前时间的时间戳字符串

示例时序如下:
alt

代码详解

官方demo提供的示例代码

官方代码可以在github的Luat_2G_RDA_8955/script_LuaTask/demo/socket/longConnection目录或luatools的LuaTools 1.x.x\script\script_LuaTask\demo\socket\longConnection目录找到

如果你能看懂官方例程,那么可以直接去使用,不需要再看本文了

socket连接代码的拆解分析

这一部分会将官方demo的代码拆开来,只保留基础部分,放到一个文件中来解释

建立文件

首先先新建两个文件,用于测试这个工程

main.lua

PROJECT = "SOCKET-TEST"
VERSION = "1.0.0"
--根据固件判断模块类型
moduleType = string.find(rtos.get_version(),"8955") and 2 or 4
require "log"
LOG_LEVEL = log.LOGLEVEL_TRACE
require "sys"
--每1分钟查询一次GSM信号强度,每1分钟查询一次基站信息
require "net"
net.startQueryAll(60000, 60000)
--加载硬件看门狗功能模块
--如果用的是720 4g模块,请注释掉这两行
require "wdt"
wdt.setup(pio.P0_30, pio.P0_31)
--加载网络指示灯功能模块
require "netLed"
netLed.setup(true,moduleType == 2 and pio.P1_1 or pio.P2_0,moduleType == 2 and nil or pio.P2_1)
require"longlink"
--启动系统框架
sys.init(0, 0)
sys.run()

longlink.lua

module(...,package.seeall)
require"socket"
--下面代码一会儿写

找一个测试用的服务器

2G模块socket测试和wifi有着本质的区别:没法使用内网来调试,必须要使用一个公网服务器来调试

为了解决这个问题,luat官方提供了一个tcp测试实验室网站服务:http://tcplab.openluat.com/
这个工具有一个坏处,就是三分钟没有客户端连接的话就会被强行关闭服务。我们可以在本地用一个tcp调试工具提前连上,就不会被强制关闭服务了。

为了针对这种情况,我临时写了一个工具:
https://github.com/chenxuuu/tcplab.openluat.com
编译好的文件可以点我下载

打开后可以直接获取从服务器分配的ip、端口,还能接收客户端数据、主动发送数据:
alt
记住自己获取到的ip和端口,在下面的代码中会被使用到
###建立socket线程

一般来说,socket连接都是异步运行的,何时应该发送数据,何时应该接收数据,这些逻辑应该让socket收发的进程自己进行控制

所以我们在longlink.lua中添加一个新的线程(看不懂的回去看前几篇文章),文件改成如下(注意要自己改东西!):

longlink.lua

module(...,package.seeall)
require"socket"
--测试用的服务器信息,上一部分获取到的那个
local testip,testport = "",""
--启动socket客户端任务
sys.taskInit(
function()
    while true do
    --该区域的代码会永久循环运行(除非出现语法错误)
    end
end)

进行socket连接

一般来说,我们会在模块成功获取基站分配的ip后,才会进行网络的连接操作,所以我们需要使用socket.isReady()函数来判断是否连接网络,然后再进行网络操作

在成功获取ip后,我们才能新建一个tcp对象,对其进行联网操作,socket客户端线程代码改为如下:

--启动socket客户端任务
sys.taskInit(
function()
    while true do
        --是否获取到了分配的ip(是否连上网)
        if socket.isReady() then
            --新建一个socket对象,如果是udp只需要把tcp改成udp即可
            local socketClient = socket.tcp()
            --尝试连接指定服务器
            if socketClient:connect(testip,testport) then
                --连接成功
                log.info("longlink.socketClient","connect success")
            else
                log.info("longlink.socketClient","connect fail")
                --连接失败
            end
        else
            --没连上网,原地等待一秒,一秒后会循环回去重试
            sys.wait(1000)
        end
    end
end)

对连接失败的处理

上述代码只是一个简单的连接服务器的代码,并且连上之后没有进行任何的其他操作

为了增加代码的稳健性,我们可以利用sys.waitUntil()函数,设置五分钟内没有获取到ip就开启飞行模式几秒,再关闭,让模块重新去获取GPRS连接:

--启动socket客户端任务
sys.taskInit(
function()
    while true do
        --是否获取到了分配的ip(是否连上网)
        if socket.isReady() then
            --新建一个socket对象,如果是udp只需要把tcp改成udp即可
            local socketClient = socket.tcp()
            --尝试连接指定服务器
            if socketClient:connect(testip,testport) then
                --连接成功
                log.info("longlink.socketClient","connect success")
            else
                log.info("longlink.socketClient","connect fail")
                --连接失败
            end
        else
            --没连上网
            --等待网络环境准备就绪,超时时间是5分钟
            sys.waitUntil("IP_READY_IND",300000)
            --等完了还没连上?
            if not socket.isReady() then
                --进入飞行模式,20秒之后,退出飞行模式
                net.switchFly(true)
                sys.wait(20000)
                net.switchFly(false)
            end
        end
    end
end)

同样,我们也可以给socketClient:connect(testip,testport)的连接加上错误次数的判断,连接错误超过五次,强制断开socket连接,等待五秒后重试:

--启动socket客户端任务
sys.taskInit(
function()
    local retryConnectCnt = 0   --失败次数统计
    while true do
        --是否获取到了分配的ip(是否连上网)
        if socket.isReady() then
            --新建一个socket对象,如果是udp只需要把tcp改成udp即可
            local socketClient = socket.tcp()
            --尝试连接指定服务器
            if socketClient:connect(testip,testport) then
                --连接成功
                log.info("longlink.socketClient","connect success")
                retryConnectCnt = 0 --失败次数清零
            else
                log.info("longlink.socketClient","connect fail")
                --连接失败
                retryConnectCnt = retryConnectCnt+1 --失败次数加一
            end
            socketClient:close()    --断开socket连接
            if retryConnectCnt>=5 then  --失败次数大于五次了
                link.shut()         --强制断开TCP/UDP连接
                retryConnectCnt=0   --失败次数清零
            end
            sys.wait(5000)
        else
            retryConnectCnt = 0     --没连上网,失败次数清零
            --没连上网
            --等待网络环境准备就绪,超时时间是5分钟
            sys.waitUntil("IP_READY_IND",300000)
            --等完了还没连上?
            if not socket.isReady() then
                --进入飞行模式,20秒之后,退出飞行模式
                net.switchFly(true)
                sys.wait(20000)
                net.switchFly(false)
            end
        end
    end
end)

添加发送/接收处理函数

到了这一步,整个的socket线程只剩下循环处理接收和发送的数据这一部分与demo不同了,我们直接把这两句话加到socket线程的代码中吧:

--启动socket客户端任务
sys.taskInit(
function()
    local retryConnectCnt = 0   --失败次数统计
    while true do
        --是否获取到了分配的ip(是否连上网)
        if socket.isReady() then
            --新建一个socket对象,如果是udp只需要把tcp改成udp即可
            local socketClient = socket.tcp()
            --尝试连接指定服务器
            if socketClient:connect(testip,testport) then
                --连接成功
                log.info("longlink.socketClient","connect success")
                retryConnectCnt = 0 --失败次数清零
                --循环处理接收和发送的数据
                while true do
                    if not inMsgProcess(socketClient) then  --接收消息处理函数
                        log.error("longlink.inMsgProcess error")
                        break
                    end
                    if not outMsgprocess(socketClient) then --发送消息处理函数
                        log.error("longlink.outMsgprocess error")
                        break
                    end
                end
            else
                log.info("longlink.socketClient","connect fail")
                --连接失败
                retryConnectCnt = retryConnectCnt+1 --失败次数加一
            end
            socketClient:close()    --断开socket连接
            if retryConnectCnt>=5 then  --失败次数大于五次了
                link.shut()         --强制断开TCP/UDP连接
                retryConnectCnt=0   --失败次数清零
            end
            sys.wait(5000)
        else
            retryConnectCnt = 0     --没连上网,失败次数清零
            --没连上网
            --等待网络环境准备就绪,超时时间是5分钟
            sys.waitUntil("IP_READY_IND",300000)
            --等完了还没连上?
            if not socket.isReady() then
                --进入飞行模式,20秒之后,退出飞行模式
                net.switchFly(true)
                sys.wait(20000)
                net.switchFly(false)
            end
        end
    end
end)

可以看到,在接收和发送函数不返回false的情况下,接收和发送循环会一直进行下去;只有当两个函数之一返回false时,才会触发break导致退出该接收和发送循环

inMsgProcess(socketClient)函数

这段的代码相对来说比较简单,我们可以直接使用socketClient:recv(毫秒数)来接收我们的tcp消息。
我们在合适的地方,新建一个inMsgProcess(socketClient)函数:

function inMsgProcess(socketClient)
    local result,data
    while true do
        result,data = socketClient:recv(2000)
        --接收数据
        if result then  --接收成功
            log.info("longlink.inMsgProcess",data)
            --处理data数据,现在还没代码,空着
        else    --接收失败
            break
        end
    end
    --返回结果,处理成功返回true,处理出错返回false
    return result or data=="timeout"
end

这段代码就是循环获取socket消息,如果没获取到,socketClient:recv(2000)就会返回false,”timeout”;如果获取到了,就会返回true,获取到的数据字符串;如果返回了false,不为”timeout”,则表示数据处理出错,说明socket连接有了什么问题

细心的读者可能看出来了,如果接收函数一直在2秒内有接收到数据,那么这段函数会永远无限循环下去,没办法到达outMsgprocess(socketClient)函数进行发送数据的操作,所以我们先去讲outMsgprocess(socketClient)函数的实现过程,再回来改进inMsgProcess(socketClient)函数

outMsgprocess(socketClient)函数

由于发送函数在socket线程中是一个循环的小部分,所以我们要建立一个消息发送的队列:有要发送的发数据时,将数据放到这个队列中;等运行到outMsgprocess(socketClient)函数时,将队列中的数据一个一个发出去

首先我们要建一个放这种队列的数组,在合适位置声明一下这个数组:

--数据发送的消息队列
local msgQuene = {}

接着我们构造一个可以往数组里插入数据的函数,table.insert()可以向数组添加数据,所以我们新建一个insertMsg函数:

local function insertMsg(data)
    table.insert(msgQuene,data)
end

还记得上面说过的消息接收函数函数会永远无限循环下去的问题吗?我们在合适的地方新建一个判断发送消息队列是否为空的函数:

function waitForSend()
    return #msgQuene > 0
end

在数组有数据时,这个函数会返回true,我们可以将inMsgProcess(socketClient)接收到数据后的代码添加一行判断发送队列是否有数据的代码,当检测到发送队列有数据时,就立即退出接收函数,转而去进行发送动作,接收函数最终改为了这样:

function inMsgProcess(socketClient)
    local result,data
    while true do
        result,data = socketClient:recv(2000)
        --接收到数据
        if result then  --接收成功
            log.info("longlink.inMsgProcess",data)
            --处理data数据,现在还没代码,空着
            --如果msgQuene中有等待发送的数据,则立即退出本循环
            if waitForSend() then return true end
        else    --接收失败
            break
        end
    end
    --返回结果,处理成功返回true,处理出错返回false
    return result or data=="timeout"
end

最后我们终于可以开始写消息发送函数了,整体的函数就是检查队列是否为空,不为空的话就发一条消息并将其从队列中删除,然后重复这一操作,函数代码如下:

function outMsgprocess(socketClient)
    --队列中有消息
    while #msgQuene>0 do
        --获取消息,并从队列中删除
        local outMsg = table.remove(msgQuene,1)
        --发送这条消息,并获取发送结果
        local result = socketClient:send(outMsg)
        --发送失败的话立刻返回nil(等同于false)
        if not result then return end
    end
    return true
end

完成基本的socket线程

经过上述的更改,最终,longlink.lua已经实现了连接服务器并自动处理错误的功能,并且预留了消息接收以及向发送队列添加数据的接口,文件的所有代码如下:

longlink.lua

module(...,package.seeall)
require"socket"
--测试用的服务器信息
local testip,testport = "180.97.81.180","50798"
--数据发送的消息队列
local msgQuene = {}
local function insertMsg(data)
    table.insert(msgQuene,data)
end
function waitForSend()
    return #msgQuene > 0
end
function outMsgprocess(socketClient)
    --队列中有消息
    while #msgQuene>0 do
        --获取消息,并从队列中删除
        local outMsg = table.remove(msgQuene,1)
        --发送这条消息,并获取发送结果
        local result = socketClient:send(outMsg)
        --发送失败的话立刻返回nil(等同于false)
        if not result then return end
    end
    return true
end
function inMsgProcess(socketClient)
    local result,data
    while true do

a

上次更新 2021-01-28