接上一篇
本文聚焦于LuatOS框架的实际使用,结合典型开发场景,详细讲解其模块化结构、API调用方式与调试技巧,助力开发者快速上手。
2.1.4 状态
task 被创建之后,就会按照代码中设计的业务逻辑来来运行,task 的状态有三种:
运行状态,阻塞状态,死亡状态;
我们先来看下一个 task 的状态机

1、调用 sys.taskInit 或者 sys.taskInitEx 两个 api 后,创建的 task 就会进入运行状态;
2、当发生以下两种事件中的任意一种,运行状态切换为死亡状态,一旦进入死亡状态,这个 task 也就结束了
(1) task 的任务处理函数,按照正常的业务逻辑已经运行结束,正常退出函数;也就是状态机中的 normal exit
(2) task 的任务处理函数,在运行过程中,出现了异常,例如对非 number 类型的变量进行数据运算,把一个 number 类型当做 table 来用等等等等,一旦出现类似的异常, 任务处理函数就会异常退出;也就是状态机中的 abnormal exit
3、当发生以下三种事件中的任意一种,运行状态切换为阻塞状态,一旦进入阻塞状态,就是 task 任务处理函数中的代码把自己给挂起了
(1) 任务处理函数中执行到 sys.wait(timeout),表示在这里阻塞等待 timeout 毫秒的时间;
(2) 任务处理函数中执行到 sys.waitUntil(msg, timeout),表示在这里阻塞等待全局的 msg 消息,或者阻塞等待 timeout 毫秒的时间;
(3) 任务处理函数中执行到 sys.waitMsg(task_name, msg, timeout),表示在这里阻塞等待定向发给 task_name 的 msg 消息,或者阻塞等待 timeout 毫秒的时间;
4、当发生以下三种事件中的任意一种,阻塞状态切换为运行状态
(1) 任务处理函数中执行到 sys.wait(timeout),sys.waitUntil(msg, timeout),sys.waitMsg(task_name, msg, timeout)创建的定时器超时时间到达,此时会产生一个定时器到达事件
(2) 任意代码处执行 sys.publish(msg, ...),发布了全局 msg 消息,在等待这个全局 msg 消息的所有处于阻塞状态的 task 都能依次接收到此消息,并且从阻塞状态切换为运行状态继续运行
(3) 任意代码处执行 sys.sendMsg(task_name, msg, arg2, arg3, arg4),向名称为 task_name 的高级 task 发送了定向 msg 消息,在等待这个定向 msg 消息的并且处于阻塞状态的 task 可以接收到此消息,并且从阻塞状态切换为运行状态继续运行
通过以上的 task 状态机介绍,我们可以看出,运行状态和阻塞状态之间的切换最为复杂;
这两种状态之间的切换,用到定时器和消息两种事件,接下来我们来重点看下消息和定时器两个概念。
2.2 LuatOS的消息(message)
2.2.1 基本概念
LuatOS 的消息机制是LuatOS task协作和事件驱动编程的核心部分,消息机制包括消息的订阅、发布/发送、接收处理;
消息需要存储到消息队列中,消息队列中的消息遵循先进先出(FIFO)原则;
根据消息队列的创建位置,把LuatOS 的消息队列分为两种: 内核消息队列和应用消息队列;
内核消息队列是内核固件中创建的,应用消息队列是扩展库脚本和项目应用脚本中创建的;

根据消息存储使用的消息队列类型以及消息发送方的不同,对消息做以下划分:

2.2.2 内核消息
看下面这张图,黄色背景的就是内核消息队列;
FreeRTOS创建的每一个task都有一个消息队列,这种消息队列就叫内核消息队列;
内核消息队列是FreeRTOS直接管理的,存储每个FreeRTOS task的内核消息的队列;
内核消息的发送,由内核固件自行处理,不开放给LuatOS用户使用;


从内核消息的接收处理方式来划分,内核消息可以分为定时器消息和非定时器消息两大类
2.2.2.1 非定时器消息
在LuatOS的应用脚本程序中,针对某一种消息或者某一类消息,注册回调函数,当内核固件中的驱动程序或者FreeRTOS的task给FreeRTOS的Lua虚拟机task发送消息后,在sys.run()调度器中会读取内核消息队列中的消息,读到消息后,直接执行注册的回调函数;在这里我举两个例子,同时结合上面的两张图说明一下这个过程:
第一个例子是串口接收数据处理,核心代码如下:

第二个例子是mqtt客户端异步消息处理,核心代码如下:

除了上面列举的这两种非定时器消息的uart,mqtt内核消息处理之外,LuatOS应用脚本开发中,还会用到很多种类似的内核消息处理逻辑,例如socket异步消息处理,http异步消息处理等等,这些内核消息的处理逻辑都是完全一样,只要注册一个回调处理数据即可,后续我们讲解具体的功能模块时,会详细讲解;
2.2.2.2 定时器消息
第二类内核消息是定时器消息,和第一类的内核消息相比,定时器消息不仅仅可以通过注册回调函数来处理,还能控制task的阻塞和运行,所以定时器消息功能更加强大,使用起来更加灵活,定时器内容在后续章节会重点讲解,在这里就暂时跳过了;
2.2.3 应用消息
应用消息队列由Lua虚拟机内部管理,应用消息队列中的消息订阅,发布/发送,和接收处理,LuatOS用户可以直接控制;
应用消息队列中的消息,和LuatOS应用脚本程序的关系最为密切,本章节,我们将重点学习应用消息队列中的消息如何使用;
应用消息队列又可以进一步划分为全局消息队列和定向消息队列:
全局消息队列中存储的全局消息有系统全局消息 和用户全局消息;
定向消息队列中存储的定向消息有系统定向消息 和用户定向消息;
2.2.3.1 系统全局消息
系统全局消息是内核固件的C代码中调用sys.publish接口发布的消息,例如"IP_READY";
LuatOS用户在脚本程序中可以使用sys.subscribe和sys.waitUntil订阅和接收处理;
这部分消息牵涉到的功能模块比较多,,有个简单的认识就行,这些系统全局消息,在后续的LuatOS课程中,每讲到一个主题,都会讲解对应的系统全局消息;
在本章节就不细讲了。
2.2.3.2 系统定向消息
系统定向消息是内核固件的C代码中调用sys.sendMsg接口发布的消息,例如socket.EVENT;
LuatOS用户在脚本程序中可以使用sys.waitMsg接收处理;
但是由于这部分内容和内核固件的耦合比较大,一般来说都是LuatOS的扩展库去使用,目前只有在socket功能有关的扩展库中使用过系统定向消息,用户编写的Lua应用脚本程序中很少用到,所以在这里也不细讲了;
在后续的LuatOS课程中,每讲到一个主题,如果有对应的系统定向消息,我们再逐一讲解;
接下来我们重点看下用户全局消息和用户定向消息:
从消息接收处理方的角度来划分,消息可以分为全局消息和定向消息;
全局消息是指消息可以被任意订阅方接收处理;
定向消息是指消息只能被指定的task接收处理;
sys核心库提供的全局消息管理功能有以下几种:
(1) 全局消息发布:sys.publish(msg, ...)
(2) 全局消息订阅:sys.subscribe(msg, msg_cbfunc)
(3) 全局消息取消订阅:sys.unsubscribe(msg, msg_cbfunc)
(4) 全局消息阻塞等待(只能在task中使用):sys.waitUntil(msg, timeout)
sys核心库提供的定向消息管理功能有以下几种:
(1) 定向消息发布:sys.sendMsg(task_name, msg, arg2, arg3, arg4)
(2) 定向消息阻塞等待(只能在task中使用):sys.waitMsg(task_name, msg, timeout)
(3) 定向消息清除:sys.cleanMsg(task_name)
2.2.3.3 用户全局消息
用户全局消息处理的完整周期
用户全局消息处理的完整周期包括以下几部分:


1、消息订阅:有两个api可以订阅全局消息
sys.subscribe(msg, cbfunc):为全局消息msg订阅一个回调函数cbfunc;相当于在全局消息订阅表中增加一项[msg] = {cbfunc = true}
sys.waitUntil(msg, timeout):为全局消息msg订阅一个task,这个api只能在task任务处理函数的业务逻辑中使用;相当于在全局消息订阅表中增加一项[msg] = {task = true}
2、消息发布:有一个api可以发布全局消息
sys.publish(msg, ...):发布一条全局消息;相当于在全局消息队列中的队尾位置增加一项{msg, ...}
3、消息调度处理:有一个api对全局消息进行调度处理
sys.run():读取全局消息并且分发给订阅者去处理;
从全局消息队列中取出第一条消息{msg1, ...}
根据消息名msg1,到全局消息订阅表中找msg1的订阅者
如果订阅者是回调函数bfunc1,则执行cbfunc1,执行完之后,并不会自动从全局消息订阅表中删除msg1对应的cbfunc1 = true标记,只有脚本程序中主动调用sys.unsubscribe(msg1, cbfunc1),才会删除这个标记;
如果订阅者是一个task1,task1处于阻塞状态,则让task1退出阻塞状态,继续运行;然后自动从全局消息订阅表中删除msg1对应的task1 = true标记
在全局消息表中处理完msg1的所有订阅者;
从全局消息队列中删除{msg1, ...}这条消息
4、删除订阅者:订阅者有两种,删除的方式各不相同
回调函数类型的订阅者,在消息调度处理过程中,不会自动删除;只有脚本程序中主动调用sys.unsubscribe(msg, cbfunc),才会删除
task类型的订阅者,在消息调度处理过程中,处理完之后,会自动删除;
5、删除消息:sys.run()消息调度处理过程中,自动删除;
有一个重要注意事项:
全局消息必须先订阅,然后再发布,这样才能保证发布的消息可以被订阅者处理;怎么理解呢?我们再看下上面这张图,在sys.sun()调度时,首先从全局消息队列中取出队首的一条消息,然后从全局消息订阅表中找到这条消息对应的所有订阅者进行处理,处理完之后,就删除了这条消息,所以此时如果订阅者还没有准备好,则无法处理消息;
了解了用户全局消息的完整生命周期后,接下来我们:
1、先看下在整个生命周期中用到的几个sys核心库的api
2、写一个完整的demo示例来实际运行演示一下如何使用;
sys.publish(msg, ...)
功能
发布一个全局消息;
注意事项
可以在能够执行到的任意代码位置使用此函数;
sys.publish(msg, ...)是全局消息的生产者,全局消息有生产就会有消费,不然消息就没有存在的意义了;
有两个接口可以注册全局消息的消费者:
1、一个是sys.subscribe(msg, msg_cbfunc)中注册的msg_cbfunc消息回调函数;
2、一个是sys.waitUntil(msg, timeout)所在的task;
所以全局消息的生产者和消费者的使用组合,有以下两种:
1、sys.publish(msg, ...) 和 sys.subscribe(msg, msg_cbfunc)
在sys.publish(msg, ...)之前,必须使用sys.subscribe(msg, msg_cbfunc)注册消息回调函数;
这样才能保证发布的msg消息可以被msg_cbfunc消息回调函数处理;
2、sys.publish(msg, ...) 和 sys.waitUntil(msg, timeout)
在sys.publish(msg, ...)之前,必须保证task正在sys.waitUntil(msg, timeout)代码处,处于阻塞等待状态;
这样才能保证发布的msg消息可以被task处理;
参数
msg
**参数含义:**消息的名称;
**数据类型:**推荐string类型(虽然number类型也行,但是不好理解,不要使用);
**取值范围:**任意string类型的字符串都行,无特别限制;
**是否必选:**必须传入此参数;
**注意事项:**暂无;
参数示例:"SEND_DATA_REQ"等任意自定义的字符串;
...
**参数含义:**消息携带的可变参数;
**数据类型:**任意数据类型;
**取值范围:**无特别限制;
**是否必选:**可选传入此参数;
注意事项:...是Lua语言的一种语法,表示可变参数,参数数量可以是0个,1个,2个,...,多个;
参数示例:
不传入,或者boolean类型的true,或者number类型的2,或者string类型的"led",或者table类型的{name="LuatOS", password="123456"}等等等等;
总之,任何数据类型的任何自定义内容都行;
返回值
nil
示例

sys.subscribe(msg, msg_cbfunc)
功能
订阅一个全局消息的回调函数;
注意事项
可以在能够执行到的任意代码位置使用此函数;
sys.publish(msg, ...) 和 sys.subscribe(msg, msg_cbfunc)配合使用时:
在sys.publish(msg, ...)之前,必须使用sys.subscribe(msg, msg_cbfunc)注册消息回调函数;
这样才能保证发布的msg消息可以被msg_cbfunc消息回调函数处理;
同一个全局消息msg,可以多次调用sys.subscribe(msg, msg_cbfunc)订阅多个不同的回调函数;
参数
msg
**参数含义:**全局消息的名称,和sys.publish(msg, ...)中的msg保持一致;
**数据类型:**推荐string类型(虽然number类型也行,但是不好理解,不要使用);
**取值范围:**任意string类型的字符串都行,无特别限制;
**是否必选:**必须传入此参数;
**注意事项:**暂无;
参数示例:"SEND_DATA_REQ"等任意自定义的字符串;
msg_cbfunc

返回值
nil
示例

sys.unsubscribe(msg, msg_cbfunc)
功能
取消订阅一个全局消息的回调函数;
注意事项
可以在能够执行到的任意代码位置使用此函数;
参数
msg
**参数含义:**全局消息的名称,和sys.subscribe(msg, msg_cbfunc)中的msg保持一致;
**数据类型:**推荐string类型(虽然number类型也行,但是不好理解,不要使用);
**取值范围:**任意string类型的字符串都行,无特别限制;
**是否必选:**必须传入此参数;
**注意事项:**暂无;
参数示例:"SEND_DATA_REQ"等任意自定义的字符串;
msg_cbfunc

返回值
nil
示例

sys.waitUntil(msg, timeout)
功能
在task中阻塞等待一个全局消息;
注意事项
只能在基础task和高级task处理函数的业务逻辑中使用此函数;
sys.publish(msg, ...) 和 sys.waitUntil(msg, timeout)配合使用时:
在sys.publish(msg, ...)之前,必须保证task正在sys.waitUntil(msg, timeout)代码处,处于阻塞等待状态;
这样才能保证发布的msg消息可以被task处理;
同一个全局消息msg,可以被多个正在sys.waitUntil(msg, timeout)代码处阻塞等待的task处理;
参数
msg
参数含义:全局消息的名称,和sys.publish(msg, ...)中的msg保持一致;
数据类型:推荐string类型(虽然number类型也行,但是不好理解,不要使用);
取值范围:任意string类型的字符串都行,无特别限制;
是否必选:必须传入此参数;
注意事项:暂无;
参数示例:"SEND_DATA_REQ"等任意自定义的字符串;
timeout
参数含义:阻塞等待全局消息msg的超时时长,单位毫秒;
数据类型:number或者nil;
取值范围:
大于等于1,小于等于0x7FFFFFFF,之间的所有正整数;最大时长0x7FFFFFFF毫秒 ≈ 596小时 ≈ 24.85天;
如果为nil,表示一直阻塞等待全局消息,不会超时;
是否必选:可选传入此参数;
注意事项:
此处的超时机制基于软件定时器实现,受系统负载、任务数量、消息堆积、网络中断优先级最高等多因素影响,无法实现高精度;
尤其是几个毫秒级别,几十毫秒级别,几百毫秒级别的超时时长,误差都比较大,秒级别以上的超时时长误差较小;
例如:设置的1毫秒,可能要等几十毫秒;设置的几十毫秒,可能要等上百毫秒;设置的几百毫秒,可能要等几百几十毫秒;
可以简单的认为会延迟几十毫秒左右,以此来评估超时时长精度是否可以满足业务需求;
调用本接口时,如果传入了timeout参数,则sys核心库内部会自动创建并且运行一个软件定时器,超时时长到达后,会自动删除这个定时器;
LuatOS应用程序中可用的软件定时器总数量为64个;
注意控制自已应用程序中的同时运行的软件定时器总数量不要超过64个,否则创建新的定时器会返回失败;
参数示例:50、1000、60000等;
返回值
local result, arg1, arg2, arg3, argN = sys.waitUntil(msg, timeout)
有数量不固定的返回值:
第一个返回值为result
剩余的返回值arg1, arg2, arg3, argN,表示可变数量的返回值,只有当第一个返回值result为true时,这些可变数量的返回值才有意义,和sys.publish(msg, ...)中...表示的可变参数一一对应
result
含义说明:阻塞等待的结果;true表示收到了msg消息,false表示超时没有收到msg消息;
数据类型:boolean;
取值范围:true或者false;
注意事项:暂无;
arg1, arg2, arg3, argN
含义说明:
当result为true时,arg1, arg2, arg3, argN表示sys.publish(msg, ...)中...可变参数,从前到后一一对应;
当result为false时,arg1, arg2, arg3, argN全部都为nil,没有任何意义;
数据类型:
当result为true时,arg1, arg2, arg3, argN的数据类型和sys.publish(msg, ...)中...可变参数的数据类型,从前到后一一对应;
当result为false时,arg1, arg2, arg3, argN全部都为nil类型;
取值范围:无特别限制;
注意事项:暂无;
正确示例

错误示例

用户全局消息处理的代码示例
在了解了全局消息的几个api之后,我们再看下图回顾一下全局消息处理的完整周期

下面这个例子用来说明用户全局消息的完整处理过程;
因为是三个文件,并且代码比较多,所以在这里我就不粘贴代码到这里了,直接打开vscode来分析代码的业务逻辑;
我们在模拟器上实际运行一下看看,输入命令
luatos --llt=H:\Luatools\project\luatos_framework_luatos_task_Air8000.ini
运行15秒钟的日志如下,为了方便分析,我把日志做了处理,每个订阅者订阅同一类型的消息日志汇总到了一起:
init_subscribe_cbfunc订阅者
init_subscribe_cbfunc订阅的task内部发布的消息,运行正常,符合预期,每秒接收到一次task内部发布的消息;
因为10秒钟之后,取消了订阅,所以只收到前10条消息

init_subscribe_cbfunc订阅的timer发布的消息,运行正常,符合预期,每秒接收到一次task内部发布的消息;
因为10秒钟之后,取消了订阅,所以只收到前10条消息

delay_subscribe_cbfunc订阅者
delay_subscribe_cbfunc订阅的task内部发布的消息,我们可以看到是从第6条消息开始接收处理,前面的5条丢失了;这是因为sys.timerStart(sys.subscribe, 5000, "SEND_DATA_REQ", delay_subscribe_cbfunc)这行代码,是开机之后延迟5秒,然后才订阅了"SEND_DATA_REQ"回调函数的delay_subscribe_cbfunc,所以前5秒的消息都无法接收处理;这段运行日志就可以验证本文前面描述的这段话:
在sys.publish(msg, ...)之前,必须使用sys.subscribe(msg, msg_cbfunc)注册消息回调函数;这样才能保证发布的msg消息可以被msg_cbfunc消息回调函数处理;再简单点来说,订阅要在发布前,就像邮局定报纸一样,你只有了订阅服务,在邮局发报纸的时候才不会把你漏掉;

同理,delay_subscribe_cbfunc订阅的timer内部发布的消息,表现也是一样

task(success_wait_until_base_task_func)订阅者
task任务处理函数success_wait_until_base_task_func中通过sys.waitUntil("SEND_DATA_REQ")订阅的task内部发布的消息,运行正常,符合预期,每秒接收到一次task内部发布的消息,一共收到15条消息;

task任务处理函数success_wait_until_base_task_func中通过sys.waitUntil("SEND_DATA_REQ")订阅的timer发布的消息,运行正常,符合预期,每秒接收到一次task内部发布的消息,一共收到15条消息;

task(lost_wait_until_base_task_func)订阅者
task任务处理函数lost_wait_until_base_task_func中通过sys.waitUntil("SEND_DATA_REQ")订阅的task内部发布的消息,运行异常,一共收到0条消息;15条消息全部丢失
为什么task内部发布的15条消息,**task(lost_wait_until_base_task_func)**全部丢失,我们添加一些日志来分析一下,如下两部分代码块的黄色背景为新增的日志打印代码

再来看一下运行日志

在这段日志中:
lost_wait_until_base_task_func before wait 3000,表示接下来lost_wait_until_base_task_func的task阻塞等待3秒钟,此状态下,其他地方发布的"SEND_DATA_REQ"无法处理,会丢失;global_sender_msg_task_func这个task所发布的消息"SEND_DATA_REQ",都是在lost_wait_until_base_task_func before wait 3000之后发布的,所以消息全部丢失;
lost_wait_until_base_task_func before wait SEND_DATA_REQ,表示接下来lost_wait_until_base_task_func的task阻塞等待消息"SEND_DATA_REQ",此状态下,其他地方发布的"SEND_DATA_REQ"可以被处理;在lost_wait_until_base_task_func before wait SEND_DATA_REQ之后,都是global_sender_msg_timer_cbfun这个定时器回调函数在发布"SEND_DATA_REQ",所以这个时间点的"SEND_DATA_REQ"都可以被处理;
task任务处理函数lost_wait_until_base_task_func中通过sys.waitUntil("SEND_DATA_REQ")订阅的定时器发布的消息,运行异常,一共收到4条消息;11条消息全部丢失

这段运行日志就可以验证本文前面描述的这段话:
在sys.publish(msg, ...)之前,必须保证task正在sys.waitUntil(msg, timeout)代码处,处于阻塞等待状态;
这样才能保证发布的msg消息可以被task处理;
我们再看下这个订阅者task的任务处理函数代码,在如下代码的第4行和第5行,都会使task阻塞,其中第4行会阻塞3秒,这3秒内,无论是task发布的消息还是timer发布的消息,都无法接收处理,直接丢失;3秒后,在第5行阻塞等待"SEND_DATA_REQ"消息,等到一条消息后,重新又阻塞3秒,在本demo中的表现就是每隔3秒接收到一次task或者timer发布的消息;

如何避免这种问题呢?
在接收消息的task任务处理函数业务逻辑中,最好只有sys.waitUntil,并且也只有一处sys.waitUntil阻塞当前task;上面这个代码块,只要把第4行去掉就没有问题了;
2.2.3.4 用户定向消息
用户定向消息处理的完整周期
简化后用户定向消息处理的完整周期包括以下几部分:


消息发送:有一个api可以发送定向消息
sys.sendMsg(task_name, msg, arg2, arg3, arg4):发送一条定向消息给task_name;相当于在定向消息队列中的队尾位置增加一项{msg, arg2, arg3, arg4}
消息接收:有一个api可以接收定向消息
sys.waitMsg(task_name, msg, timeout):读取task_name的一条定向消息,如果定向消息队列中有指定的msg消息,则读出处理;如果有其他非指定的消息,给非目标消息回调函数处理;如果没有消息,则阻塞等待
消息调度处理:有一个api对定向消息进行调度处理
如果发送定向消息后,接收者高级task处于阻塞状态,则sys.run()调度器会控制处于阻塞状态的task,退出阻塞状态,读取定向消息进行处理;
删除接收者:在消息调度处理过程中,sys.waitMsg(task_name, msg, timeout)读出指定的msg消息,处理完之后,会自动删除接收者,只有等到下次运行到sys.waitMsg(task_name, msg, timeout)时才会创建新的接收者;
删除消息:sys.run()消息调度处理过程中,sys.waitMsg(task_name, msg, timeout)读出指定的msg消息,处理完消息之后会自动删除;
上一小节我们讨论了用户全局消息,还记得用户全局消息处理有两个注意事项:
1、在sys.publish(msg, ...)之前,必须使用sys.subscribe(msg, msg_cbfunc)注册消息回调函数;这样才能保证发布的msg消息可以被msg_cbfunc消息回调函数处理;
2、在sys.publish(msg, ...)之前,必须保证task正在sys.waitUntil(msg, timeout)代码处,处于阻塞等待状态;这样才能保证发布的msg消息可以被task处理;
现在我们讨论的用户定向消息已经不存在这个问题,消息发布和消息处理没有严格的时序要求,可以先发送定向消息,消息会存储到定向消息队列中,只有当调用sys.waitMsg(task_name, msg, timeout)接口主动去读取定向消息时,才会从定向消息队列中读出消息进行处理,所以不存在消息丢失的问题;但是如果使用不当,可能会出现消息处理延迟的问题;
了解了用户定向消息的完整生命周期后,接下来我们:
1、先看下在整个生命周期中用到的几个sys核心库的api
2、写一个完整的demo示例来实际运行演示一下如何使用;
sys.sendMsg(task_name, msg, arg2, arg3, arg4)
功能
向名称为task_name的task发布一个定向消息;
注意事项
可以在能够执行到的任意代码位置使用此函数;
sys.sendMsg(task_name, msg, arg2, arg3, arg4)是定向消息的生产者,定向消息有生产就会有消费,不然消息就没有存在的意义了;
sys.waitMsg(task_name, msg, timeout)所在的task是定向消息的消费者;
sys.sendMsg(task_name, msg, arg2, arg3, arg4) 和 sys.waitMsg(task_name, msg, timeout)配合使用;
在sys.sendMsg(task_name, msg, arg2, arg3, arg4)之前,需要保证名称为task_name的task已经被创建,否则定向消息也会丢失;
参数
task_name
参数含义:task的名称,和sys.taskInitEx(task_func, task_name, non_targeted_msg_cbfunc, ...)中的task_name保持一致;
数据类型:推荐string类型(虽然number类型也行,但是不好理解,不要使用);
取值范围:任意string类型的字符串都行,无特别限制;
是否必选:必须传入此参数;
注意事项:暂无;
参数示例:"LED_TASK"、"GPIO_TASK"等任意自定义的字符串;
msg
参数含义:消息的名称;
数据类型:推荐string类型(虽然number类型也行,但是不好理解,不要使用);
取值范围:任意string类型的字符串都行,无特别限制;
是否必选:必须传入此参数;
注意事项:暂无;
参数示例:"SEND_DATA_REQ"等任意自定义的字符串;
arg2
参数含义:msg消息携带的第一个参数;
数据类型:任意数据类型;
取值范围:无特别限制;
是否必选:可选传入此参数;
注意事项:暂无;
参数示例:
不传入,或者boolean类型的true,或者number类型的2,或者string类型的"led",或者table类型的{name="LuatOS", password="123456"}等等等等;
总之,任何数据类型的任何自定义内容都行;
arg3
参数含义:msg消息携带的第二个参数;
数据类型:任意数据类型;
取值范围:无特别限制;
是否必选:可选传入此参数;
注意事项:暂无;
参数示例:
不传入,或者boolean类型的true,或者number类型的2,或者string类型的"led",或者table类型的{name="LuatOS", password="123456"}等等等等;
总之,任何数据类型的任何自定义内容都行;
arg4
参数含义:msg消息携带的第三个参数;
数据类型:任意数据类型;
取值范围:无特别限制;
是否必选:可选传入此参数;
注意事项:暂无;
参数示例:
不传入,或者boolean类型的true,或者number类型的2,或者string类型的"led",或者table类型的{name="LuatOS", password="123456"}等等等等;
总之,任何数据类型的任何自定义内容都行;
返回值
local result = sys.sendMsg(task_name, msg, arg2, arg3, arg4)
result
含义说明:定向消息发布结果,成功返回true,失败返回false;
数据类型:boolean;
取值范围:true或者false;
注意事项:暂无;
示例

sys.waitMsg(task_name, msg, timeout)
功能
在task中阻塞等待名称为task_name的task的定向消息;
注意事项
只能在高级task处理函数的业务逻辑中使用此函数;
sys.sendMsg(task_name, msg, arg2, arg3, arg4)是定向消息的生产者,定向消息有生产就会有消费,不然消息就没有存在的意义了;
sys.waitMsg(task_name, msg, timeout)所在的task是定向消息的消费者;
sys.sendMsg(task_name, msg, arg2, arg3, arg4) 和 sys.waitMsg(task_name, msg, timeout)配合使用;
在sys.sendMsg(task_name, msg, arg2, arg3, arg4)之前,需要保证名称为task_name的task已经被创建,否则定向消息也会丢失;
参数
task_name
参数含义:task的名称,和sys.taskInitEx(task_func, task_name, non_targeted_msg_cbfunc, ...)中的task_name保持一致;
数据类型:推荐string类型(虽然number类型也行,但是不好理解,不要使用);
取值范围:任意string类型的字符串都行,无特别限制;
是否必选:必须传入此参数;
注意事项:暂无;
参数示例:"LED_TASK"、"GPIO_TASK"等任意自定义的字符串;
msg
参数含义:消息的名称,和sys.sendMsg(task_name, msg, arg2, arg3, arg4)中的msg保持一致;
数据类型:推荐string类型(虽然number类型也行,但是不好理解,不要使用);
取值范围:任意string类型的字符串都行,无特别限制;
是否必选:必须传入此参数;
注意事项:暂无;
参数示例:"SEND_DATA_REQ"等任意自定义的字符串;
timeout
参数含义:阻塞等待定向消息msg的超时时长,单位毫秒,nil表示一直阻塞等待;
数据类型:number或者nil;
取值范围:
大于等于1,小于等于0x7FFFFFFF,之间的所有正整数;最大时长0x7FFFFFFF毫秒 ≈ 596小时 ≈ 24.85天;
如果为nil,表示一直阻塞等待全局消息,不会超时;
是否必选:可选传入此参数;
注意事项:
此处的超时机制基于软件定时器实现,受系统负载、任务数量、消息堆积、网络中断优先级最高等多因素影响,无法实现高精度;
尤其是几个毫秒级别,几十毫秒级别,几百毫秒级别的超时时长,误差都比较大,秒级别以上的超时时长误差较小;
例如:设置的1毫秒,可能要等几十毫秒;设置的几十毫秒,可能要等上百毫秒;设置的几百毫秒,可能要等几百几十毫秒;
可以简单的认为会延迟几十毫秒左右,以此来评估超时时长精度是否可以满足业务需求;
调用本接口时,如果传入了timeout参数,则sys核心库内部会自动创建并且运行一个软件定时器,超时时长到达后,会自动删除这个定时器;
LuatOS应用程序中可用的软件定时器总数量为64个;
注意控制自已应用程序中的同时运行的软件定时器总数量不要超过64个,否则创建新的定时器会返回失败;
参数示例:50、1000、60000等;
返回值
local message = sys.waitMsg(task_name, msg, timeout)
有一个返回值为message
message
含义说明:
阻塞等待的结果;table类型表示接收到msg消息,false表示超时没有收到msg消息;
当接收到msg消息时,message[1],message[2],message[3],message[4]和sys.sendMsg(task_name, msg, arg2, arg3, arg4)中的 msg, arg2, arg3, arg4 一一对应;
当超时没有收到msg消息时,message为false;
数据类型:table或者boolean;
取值范围:无特别限制;
注意事项:暂无;
示例

用户定向消息处理的代码示例
在了解了定向消息的两个api之后,我们再看下图回顾一下定向消息处理的完整周期

下面这个例子用来说明用户定向消息的完整处理过程;
核心代码片段如下,我们首先分析下这两段代码的业务逻辑

我们在模拟器上实际运行一下看看,输入命令
luatos --llt=H:\Luatools\project\luatos_framework_luatos_task_Air8000.ini
运行15秒钟的日志如下,为了方便分析,我把日志做了处理,每个消息接收者的日志汇总到了一起:
normal_wait_msg_task接收者
nromal_wait_msg_task的任务处理函数中,调用sysplus.waitMsg("nromal_wait_msg_task", "SEND_DATA_REQ")接收消息,在15秒的时间内,一共收到15条消息,运行正常,符合预期,每秒接收到一次消息;

delay_wait_msg_task接收者
delay_wait_msg_task的任务处理函数中,调用sysplus.waitMsg("nromal_wait_msg_task", "SEND_DATA_REQ")接收消息,在15秒的时间内,一共收到5条消息,和代码设计相符,每延时3秒接收到一次消息;
但是我们再仔细观察下收到消息,消息的计数参数依次为1,2,3,4,5,说明虽然延时收到消息,但是消息并没有出现丢失;
这一点儿和上一小节讲的全局消息的处理完全不同,全局消息在这种情况下会丢失消息;

2.2.3.5 用户定向消息 vs 用户全局消息,如何选择?
在了解了用户定向消息和用户全局消息之后,大家可能会有一个疑问:我平时开发程序时,怎么判断应该使用定向消息还是全局消息呢?一般来说,建议大家按照以下几步来做选择:
1、首先确认你的程序中有没有使用高级task,因为有些应用功能模块是必须要使用高级task的,例如创建一个socket时,要使用高级task;如果使用了高级task,那就只能使用用户定向消息了;如果没有使用高级task,继续向下判断;
如果你对用户全局消息的处理过程以及其中容易出现的消息丢失问题,非常清楚,知道如何规避这种消息丢失问题,则可以使用全局消息,因为全局消息使用起来简单;
如果你对用户全局消息的处理过程以及其中容易出现的问题都不是很清楚,这种情况下,建议使用定向消息和高级task,因为这种方式下,只要在发送定向消息前,高级task已经创建,则消息不会丢失;
2.3 LuatOS 的定时器(timer)
对于 LuatOS 应用程序来说,定时器本质上也算是一种特殊的消息,因为定时器太常用了,所以把他单独拎出来,单独的一个章节进行讲解;
2.3.1 基本概念
LuatOS 定时器的分类如下:

LuatOS 定时器管理的 API 列表如下:
(1) 单次定时器创建并且启动:sys.timerStart(cbfunc, timeout, ...)
(2) 循环定时器创建并且启动:sys.timerLoopStart(cbfunc, timeout, ...)
(3) 单个定时器停止并且删除:sys.timerStop(timer_id)
(4) 单个定时器停止并且删除:sys.timerStop(cbfunc, ...)
(5) 多个定时器停止并且删除:sys.timerStopAll(cbfunc)
(6) 阻塞等待一段时间(只能在 task 中使用):sys.wait(timeout)
(7) 阻塞等待全局消息或者阻塞等待一段时间(只能在 task 中使用):sys.waitUntil(msg, timeout)
(8) 阻塞等待定向消息或者阻塞等待一段时间(只能在 task 中使用):sys.waitMsg(task_name, msg, timeout)
2.3.2 定时器消息处理的完整周期

2.3.3 sys.timerStart(cbfunc, timeout, ...)
功能
创建并且运行一个单次定时器;
注意事项
可以在能够执行到的任意代码位置使用此函数;
有两种方式可以唯一标识一个定时器:
1、定时器 id;如果使用 sys.timerStart(cbfunc, timeout, ...)创建定时器成功,会返回定时器 id
2、定时器回调函数 cbfunc 和可变参数...,此种方式的说明如下:
如果 cbfunc 和...相同,重复调用 sys.timerStart(cbfunc, timeout, ...)接口创建并且运行定时器;
在 sys.timerStart 内部会自动停止并且删除已经存在的重复定时器;
例如执行如下三行代码后:
sys.timerStart(led_on_timer_cbfunc, 1000, "red")
sys.timerStart(led_on_timer_cbfunc, 2000, "red")
sys.timerStart(led_on_timer_cbfunc, 3000, "red")
最后只有 sys.timerStart(led_on_timer_cbfunc, 3000, "red") 这个定时器在运行,前面创建的两个定时器都被自动删除了,没有完整运行;
参数
cbfunc

timeout

关于定时器精度的问题,我们再来看下面这张图来理解:
1、FreeRTOS中的一些任务优先级比Lua虚拟机任务优先级高,尤其是4G网络中断的任务优先级最高,这些高优先级的任务的抢占执行,会直接影响Lua虚拟机任务执行的实时性,进而导致sys.run()调度器的运行实时性也不会很高;
2、在Lua虚拟机任务内部的sys.run()调度器中,首先是遍历并且分发处理用户全局消息队列中的所有消息,这些消息全部处理完,才会去执行内核消息队列中的第一条消息,定时器事件到达的消息是存储在内核消息队列中的,如果用户全局消息队列中的消息处理耗时较长,或者内核消息队列中在定时器消息之前还有其他消息(例如串口消息,mqtt消息等),定时器消息都要排队才能执行,所以整个项目的业务越复杂,系统负载就越重,消息数量就越多,定时器消息处理的实时性就越低;


返回值
local timer_id = sys.timerStart(cbfunc, timeout, ...)
有一个返回值为 timer_id
timer_id

示例

2.3.4 sys.timerLoopStart(cbfunc, timeout, ...)
功能
创建并且运行一个循环定时器;
注意事项
可以在能够执行到的任意代码位置使用此函数;
有两种方式可以唯一标识一个定时器:
1、定时器 id;如果使用 sys.timerLoopStart(cbfunc, timeout, ...)创建定时器成功,会返回定时器 id
2、定时器回调函数 cbfunc 和可变参数...,此种方式的说明如下:
如果 cbfunc 和...相同,重复调用 sys.timerLoopStart(cbfunc, timeout, ...)接口创建并且运行定时器;
在 sys.timerLoopStart 内部会自动停止并且删除已经存在的重复定时器;
例如执行如下三行代码后:
sys.timerLoopStart(led_on_timer_cbfunc, 1000, "red")
sys.timerLoopStart(led_on_timer_cbfunc, 2000, "red")
sys.timerLoopStart(led_on_timer_cbfunc, 3000, "red")
最后只有 sys.timerLoopStart(led_on_timer_cbfunc, 3000, "red") 这个定时器在运行,前面创建的两个定时器都被自动删除了,没有完整运行;
参数
cbfunc

timeout

...

返回值
local timer_id = sys.timerLoopStart(cbfunc, timeout, ...)
有一个返回值为 timer_id
timer_id

示例

2.3.5 sys.timerStop(timer_id)
功能
根据定时器 id 停止运行并且删除一个定时器;
注意事项
可以在能够执行到的任意代码位置使用此函数;
有两种方式可以唯一标识一个定时器:
1、定时器 id;如果使用 sys.timerStart(cbfunc, timeout, ...)或者 sys.timerLoopStart(cbfunc, timeout, ...)创建定时器成功,会返回定时器 id
2、定时器回调函数 cbfunc 和可变参数...;
参数
timer_id

返回值
nil
示例

2.3.6 sys.timerStop(cbfunc, ...)
功能
根据定时器的回调函数 cbfunc 和可变参数...停止运行并且删除一个定时器;
注意事项
可以在能够执行到的任意代码位置使用此函数;
有两种方式可以唯一标识一个定时器:
1、定时器 id;如果使用 sys.timerStart(cbfunc, timeout, ...)或者 sys.timerLoopStart(cbfunc, timeout, ...)创建定时器成功,会返回定时器 id;
2、定时器回调函数 cbfunc 和可变参数...;
参数
cbfunc

返回值
nil
示例

2.3.7 sys.timerStopAll(cbfunc)
功能
停止运行并且删除回调函数为 cbfunc 的所有定时器;
注意事项
可以在能够执行到的任意代码位置使用此函数;
参数
cbfunc

返回值
nil
示例

2.3.8 sys.wait(timeout)
功能
在 task 中阻塞等待一段时间;
注意事项
只能在基础 task 和高级 task 处理函数的业务逻辑中使用此函数;
参数
timeout

返回值
nil
示例

sys.waitUntil(msg, timeout)
功能
在 task 中阻塞等待一个全局消息;
注意事项
只能在基础 task 和高级 task 处理函数的业务逻辑中使用此函数;
sys.publish(msg, ...) 和 sys.waitUntil(msg, timeout)配合使用时:
在 sys.publish(msg, ...)之前,必须保证 task 正在 sys.waitUntil(msg, timeout)代码处,处于阻塞等待状态;
这样才能保证发布的 msg 消息可以被 task 处理;
同一个全局消息 msg,可以被多个正在 sys.waitUntil(msg, timeout)代码处阻塞等待的 task 处理;
参数
msg

timeout

返回值
local result, arg1, arg2, arg3, argN = sys.waitUntil(msg, timeout)
有数量不固定的返回值:
第一个返回值为 result
剩余的返回值 arg1, arg2, arg3, argN,表示可变数量的返回值,只有当第一个返回值 result 为 true 时,这些可变数量的返回值才有意义,和 sys.publish(msg, ...)中...表示的可变参数一一对应
result

arg1, arg2, arg3, argN

正确示例

错误示例

2.3.9 sys.waitUntil(msg, timeout)
功能
在 task 中阻塞等待一个全局消息;
注意事项
只能在基础 task 和高级 task 处理函数的业务逻辑中使用此函数;
sys.publish(msg, ...) 和 sys.waitUntil(msg, timeout)配合使用时:
在 sys.publish(msg, ...)之前,必须保证 task 正在 sys.waitUntil(msg, timeout)代码处,处于阻塞等待状态;
这样才能保证发布的 msg 消息可以被 task 处理;
同一个全局消息 msg,可以被多个正在 sys.waitUntil(msg, timeout)代码处阻塞等待的 task 处理;
参数
msg

timeout

返回值
local result, arg1, arg2, arg3, argN = sys.waitUntil(msg, timeout)
有数量不固定的返回值:
第一个返回值为 result
剩余的返回值 arg1, arg2, arg3, argN,表示可变数量的返回值,只有当第一个返回值 result 为 true 时,这些可变数量的返回值才有意义,和 sys.publish(msg, ...)中...表示的可变参数一一对应
result

arg1, arg2, arg3, argN

正确示例

错误示例

2.3.10 sys.waitMsg(task_name, msg, timeout)
功能
在 task 中阻塞等待名称为 task_name 的 task 的定向消息;
注意事项
只能在高级 task 处理函数的业务逻辑中使用此函数;
sys.sendMsg(task_name, msg, arg2, arg3, arg4)是定向消息的生产者,定向消息有生产就会有消费,不然消息就没有存在的意义了;
sys.waitMsg(task_name, msg, timeout)所在的 task 是定向消息的消费者;
sys.sendMsg(task_name, msg, arg2, arg3, arg4) 和 sys.waitMsg(task_name, msg, timeout)配合使用;
在 sys.sendMsg(task_name, msg, arg2, arg3, arg4)之前,需要保证名称为 task_name 的 task 已经被创建,否则定向消息也会丢失;
参数
task_name

msg

timeout

返回值
local message = sys.waitMsg(task_name, msg, timeout)
有一个返回值为 message
message

示例

2.3.11 定时器代码示例
在了解了定时器的 api 之后,我们再看下图回顾一下定时器消息处理的完整周期

下面这个例子用来说明定时器的使用方法;
核心代码片段如下,我们首先分析下这段代码的业务逻辑

我们在模拟器上实际运行一下看看,输入命令
luatos --llt=H:\Luatools\project\luatos_framework_luatos_task_Air8000.ini
运行日志如下:

我们结合运行日志分析一下代码的业务逻辑是否执行正常;
2.4 task 内部运行环境 vs task 外部运行环境
在前文内容中,我们提到了应用脚本代码的两种运行环境;当时仅仅对这两种概念做了一个初步的介绍,并没有结合示例来讲解,现在我们已经学习了 task,msg,timer,可以结合 task,msg,timer 来举一些实际的例子,来进一步理解这两种运行环境;
2.4.1 基本概念
首先复现一下这两种运行环境的概念:
在 LuatOS 应用脚本开发过程中,我们所编写的应用脚本代码,存在两种业务逻辑的运行环境:
1、一种是在 task 的任务处理函数内部的业务环境中运行,我们简称为:在 task 内部运行;
2、一种是在 task 的任务处理函数外部的业务环境中运行,我们简称为:在 task 外部运行;
怎么理解这两种业务逻辑运行环境?我们看下面这张图
看右边生长出分支的这棵大树,这棵大树就是 FreeRTOS 创建的 Lua 虚拟机 task,是一个 FreeRTOS task;
在这个 Lua 虚拟机 FreeRTOS task 上,这棵大树再分为两部分:
1、树干部分:树干部分运行的业务逻辑环境就是 LuatOS task 外部运行环境;
2、树枝部分:每个树枝都是一个独立的 LuatOS task,树枝部分运行的业务逻辑环境就是 LuatOS task 内部运行环境;


2.4.2 sys api 需要的运行环境
接下来对 task、msg、timer 的 api 需要的运行环境做一个说明

从以上表格可以看出,sys 核心库中的 api,从需要的运行环境来看,分为以下三类:
1、大部分的 api,既可以在 task 内部运行,也可以在 task 外部运行;
2、sys.waitUntil,sys.waitMsg,sys.wait,这三个 spi,只能在 task 内部运行;
3、sys.run,只能在 task 外部运行;
2.4.3 sys api 的回调函数提供的运行环境

从以上表格可以看出,sys 核心库中的 api,如果支持回调函数,这些回调函数内部提供的运行环境,分为以下两类:
sys.taskInitEx(task_func, task_name,non_targeted_msg_cbfunc , ...)中的回调函数non_targeted_msg_cbfunc,提供的是 task 内部运行环境;
sys.subscribe(msg,msg_cbfunc ),sys.timerStart(cbfunc , timeout, ...),sys.timerLoopStart(cbfunc , timeout, ...)中的回调函数,提供的是 task 外部运行环境;所以这些回调函数内部不能调用"只能在 task 内部运行"的 api,例如在 sys.subscribe(msg,msg_cbfunc )的msg_cbfunc内部不能调用 sys.waitUntil,sys.waitMsg,sys.wait;
2.4.4 常犯的错误
新接触 LuatOS 开发的用户,经常会犯上面黄色背景标注的这个错误;
下面这个例子用来说明常犯的这种错误;
核心代码片段如下,我们首先分析下这段代码的业务逻辑(实际运行演示时,每次打开三段黄色背景代码中的其中一段)

我们在模拟器上实际运行一下看看,输入命令
luatos --llt=H:\Luatools\project\luatos_framework_luatos_task_Air8000.ini
运行日志如下:



2.5 sys 核心库 api 的组合使用关系
我们已经学习过了 sys 核心库中的 task,msg,timer 的 api,在这些 api 中:
1、有些 api 必须在一起组合使用,才能实现完整的业务流程;
2、有些 api 禁止在一起组合使用,否则会导致业务出错;
在这些 api 中,主要是消息的发送和接收 api 容易混用,组合使用关系参考下表(每一行的两个单元格所表示的 api 必须组合使用):

2.6 LuatOS 应用软件调度机制(sys.run()函数)
1、sys 核心库是 LuatOS 运行框架库,是 LuatOS 应用程序运行的核心大脑,所有 LuatOS 应用项目都会使用到 sys 核心库;
2、截止到目前,我们已经学习了 sys 核心库提供的 task,msg,timer 功能;
3、sys 核心库还剩最后一个功能 api,sys.run();
4、sys 核心库是 LuatOS 应用程序运行的核心大脑,sys.run()是 sys 核心库的大脑,负责整个 LuatOS 应用脚本程序的调度和管理,是 LuatOS 应用程序的调度器;
sys.run()非常重要,但是 sys.run()使用起来非常简单,仅仅在 main.lua 的最后一行调用 sys.run()即可。
虽然 sys.run()使用起来非常简单,但是如果大家对 sys.run()的运行原理有一个总体性的理解和认识,对开发 LuatOS 应用项目来说,帮助很大。
所以在这里,我先对 sys.run()内部的工作原理做一个简化后的总体介绍,至于更详细的原理介绍,我们会在后续的 LuatOS 直播课程中讲解;

我们看上面这张图:
1、LuatOS 内核固件中的 FreeRTOS 会创建一个 Lua 虚拟机任务;
2、Lua 虚拟机任务的处理函数中,首先进行初始化:
(1) 在内核固件的 C 代码中,加载 Lua 标准库和 LuatOS 核心库;
(2) 从 LuatOS 的脚本分区找到 main.lua
(3) 开始逐行嵌套解析执行 main.lua 中的脚本代码(加载必要的扩展库脚本文件和自己开发的应用脚本文件,并且运行这些脚本文件的初始化代码)
3、运行 main.lua 的最后一行代码 sys.run()
4、sys.run()中的实现是一个 while true 循环,在这个循环内,不断地从内核消息队列和用户全局消息队列中读取消息,并且分发消息给接收者进行处理。

2.7 分析 mqtt demo 中的 task,msg,timer,run 的使用案例
现在,LuatOS 框架的使用,基本上讲完了,接下来,我们来实际看一个完整 mqtt demo 项目代码,重点分析下这份 demo 项目代码中,使用到的本章节讲解的知识点;
Mqtt demo 项目的总体设计框图如下:

这份mqtt demo中的readme文件,以及代码中的注释都比较详细,接下来我用vscode直接打开这份demo项目代码,从以下几方面讲解一下:
1、先总体看一下mqtt demo的readme文件,让大家对这个demo项目的业务逻辑有一个总体的认识;
2、从以下几方面来详细分析mqtt demo项目代码:
mqtt demo项目脚本的整体运行逻辑;
mqtt demo项目脚本中使用到的LuatOS task,message,timer,调度器代码解读;
通过分析和本篇文章有关的代码,让大家对本节理解更加深刻;
现在我们开始进入mqtt demo项目中去分析;
三、课后作业
至少二选一
3.1 开发代码,在 LuatOS 模拟器 上验证可以同时运行的定时器数量
作业提交内容:
1、 6 个 Lua 文件
(1) main.lua:初始化,加载下面的 5 个 lua 文件功能模块(每次只打开其中的 1 个进行验证),执行 sys.run;(可以参考本讲课程中的 demo)
(2) timer_start.lua:使用 sys.timerStart 接口来验证可以同时运行的定时器数量;
(3) timer_loop_start.lua:使用 sys.timerLoopStart 接口来验证可以同时运行的定时器数量;
(4) wait.lua:使用 sys.wait 接口来验证可以同时运行的定时器数量;
(5) wait_until.lua:使用 sys.waitUntil 接口来验证可以同时运行的定时器数量;
(6) wait_msg.lua:使用 sys.waitMsg 接口来验证可以同时运行的定时器数量;
2、1 个运行日志文件
3、1 个分析文件,给出可以同时运行多少个定时器的结论,然后结合代码和日志分析出来为什么可以同时运行这么多的定时器;
3.2 开发代码,在 Air 系列模组的开发板或者核心板 上验证可以同时运行的定时器数量
作业提交内容:
1、 6 个 Lua 文件
(1) main.lua:初始化,加载下面的 5 个 lua 文件功能模块(每次只打开其中的 1 个进行验证),执行 sys.run;(可以参考本讲课程中的 demo)
(2) timer_start.lua:使用 sys.timerStart 接口来验证可以同时运行的定时器数量;
(3) timer_loop_start.lua:使用 sys.timerLoopStart 接口来验证可以同时运行的定时器数量;
(4) wait.lua:使用 sys.wait 接口来验证可以同时运行的定时器数量;
(5) wait_until.lua:使用 sys.waitUntil 接口来验证可以同时运行的定时器数量;
(6) wait_msg.lua:使用 sys.waitMsg 接口来验证可以同时运行的定时器数量;
2、 1 个运行日志文件
3、 1 个分析文件,给出可以同时运行多少个定时器的结论,然后结合代码和日志分析出来为什么可以同时运行这么多的定时器;
今天的内容就分享到这里了~