LuaTask

函数运行

由于没有main函数,一些习惯常规单片机开发者一时不知怎么运行程序。程序运行有多种方法

直接调用

项目中处理lib文件夹还有main.lua,test.lua。

main.lua

--重要提醒:必须在这个位置定义MODULE_TYPE、PROJECT和VERSION变量
--MODULE_TYPE:模块型号,目前仅支持Air201、Air202、Air800
--PROJECT:ascii string类型,可以随便定义,只要不使用,就行
--VERSION:ascii string类型,如果使用Luat物联云平台固件升级的功能,必须按照"X.X.X"定义,X表示1位数字;否则可随便定义
MODULE_TYPE = "Air800"
PROJECT = "WRITE_SN"
VERSION = "1.0.0"
require"sys"
--[[
如果使用UART输出trace,打开这行注释的代码"--sys.opntrace(true,1)"即可,第2个参数1表示UART1输出trace,根据自己的需要修改这个参数
这里是最早可以设置trace口的地方,代码写在这里可以保证UART口尽可能的输出“开机就出现的错误信息”
如果写在后面的其他位置,很有可能无法输出错误信息,从而增加调试难度
]]
--sys.opntrace(true,1)
require"test"
if MODULE_TYPE=="Air201" then
require"wdt"
end
sys.init(0,0)
sys.run()

main.lua需要使用require引用编写的脚本,这儿为test.lua。

test.lua

module(...,package.seeall)  

local function ss(  )
    print("ss function test")
end
ss()        --直接调用,在main.lua文件中require"test"的时候则会调用该函数

协程

main.lua同上

test.lua

module(..., package.seeall)

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

定时器

main.lua同上

test.lua

module(...,package.seeall)  

local function ss(  )
    print("ss function test")
end
--sys.timerStart(ss,3000)   --3秒运行一次
sys.timerLoopStart (ss,5000)  --循环定时器,每隔5秒运行一次

程序注册

LuaTask通过订阅发布来实现消息机制。

当函数完成一个操作后,可以发布一个消息,其他函数可以订阅该消息并做对应的操作。举个例子,当socket发送完数据后发布“SEND_FINISH”。这时开发者想socket发布完成后通过串口发送数据或者改变某个IO口的状态。就可以订阅该消息subscribe("SEND_FINISH",callback)。callback为接收到SEND_FINISH消息后需要做的事。

先来看一个程序

--testMsgPub.lua
module(...,package.seeall)

require"sys"
local  a = 2
local function pub()
    print("pub")
    sys.publish("TEST",a)       --可以发布多个变量sys.publish("TEST",1,2,3)
end
pub()
--testMsgSub.lua
module(...,package.seeall)

require"sys"

local function subCallBack(...)
    print("rev",arg[1]) 
end

sys.subscribe("TEST",subCallBack)

如果要在任务函数中订阅消息并做相应的处理,怎么办?

odule(...,package.seeall)

require"sys"
local  a = 2
local function pub()
    print("pub")
    sys.publish("TEST",a)
end
pub()
sys.taskInit(function()
    while true do
        result, data = sys.waitUntil("TEST", 10000)
        if result == true then 
            print("rev")
            print(data)
        end
        sys.wait(2000)
    end
end)

调用sys.waitUntil()函数即可。

接下来分析实现的源码

为了更好的理解源码,需要以下的预备知识:

1、回调函数的实现

local function callBackTest(...)
    print("callBack",arg[1])
end

local function test( a,callBackTest )
    if a > 1 then
        callBackTest(1)
    end
end
test(2,callBackTest)
--输出
--callBack  1

2、不定参数

function g (a, b, ...) end
g(3)              -- a=3, b=nil, arg={n=0}   -- n为不定参数的个数
g(3, 4)           -- a=3, b=4, arg={n=0}
g(3, 4, 5, 8)     -- a=3, b=4, arg={5, 8; n=2}  

进入正题

------------------------------------------ LUA应用消息订阅/发布接口 ------------------------------------------
-- 订阅者列表
local subscribers = {}
--内部消息队列
local messageQueue = {}

--- 订阅消息
-- @param id 消息id
-- @param callback 消息回调处理
-- @usage subscribe("NET_STATUS_IND", callback)
function subscribe(id, callback)
    if type(id) ~= "string" or (type(callback) ~= "function" and type(callback) ~= "thread") then
        log.warn("warning: sys.subscribe invalid parameter", id, callback)
        return
    end
    if not subscribers[id] then subscribers[id] = {} end    -- 如果没有重复消息
    subscribers[id][callback] = true        --标记id和callback关系
end

--- 取消订阅消息
-- @param id 消息id
-- @param callback 消息回调处理
-- @usage unsubscribe("NET_STATUS_IND", callback)
function unsubscribe(id, callback)
    if type(id) ~= "string" or (type(callback) ~= "function" and type(callback) ~= "thread") then
        log.warn("warning: sys.unsubscribe invalid parameter", id, callback)
        return
    end
    if subscribers[id] then subscribers[id][callback] = nil end  --删除id和callback关系
end

--- 发布内部消息,存储在内部消息队列中
-- @param ... 可变参数,用户自定义
-- @return 无
-- @usage publish("NET_STATUS_IND")
function publish(...)
    table.insert(messageQueue, arg)     -- 将不定参数插入队列中
end

-- 分发消息
local function dispatch()
    while true do
        if #messageQueue == 0 then      --如果队列长度为  跳出循环
            break
        end
        local message = table.remove(messageQueue, 1)   --获取队列的第一个
        if subscribers[message[1]] then                     --如果订消息存在
            for callback, _ in pairs(subscribers[message[1]]) do
                if type(callback) == "function" then
                    print("unpack",unpack(message, 2, #message))
                    callback(unpack(message, 2, #message))   -- 返回第二个到最后一个
                elseif type(callback) == "thread" then
                    coroutine.resume(callback, unpack(message))
                end
            end
        end
    end
end

以sys.publish("TEST",a)和sys.subscribe("TEST",subCallBack),订阅者列表为local subscribers = {}。内部消息队列为local messageQueue = {}为例:

1、在publish函数中,将"TEST"消息和参数插入messageQueue列表中

此时messageQueue中为{{"TEST",2;n=1}}

2、在subscribe函数中判断消息和callback类型是否正确,如果正确则在subscribers中建立消息与回调函数之间的关系。

此时subscribers["TEST"][subCallBack] = true。表明TEST消息对应的回掉函数为subCallBack

3、在dispatch()函数中,获得表头列表。

local message = table.remove(messageQueue, 1)

此时message为{"TEST",2;n=1}

找到该消息对应的回调函数或消息。将message中的参数传给回调函数。

通过pairs遍历得到消息对应的回调函数或者任务。

如果callback是函数,那么将publish时候的参数传给回调函数。

如果callback是线程,那么唤醒该线程。

以上只是单个消息举例,多个消息同理,因为每次循环都会将messageQueue的头部出队列,满足FIFO原则。

在有上基础下容易的理解waitUntil()的实现

--- Task任务的条件等待函数(包括事件消息和定时器消息等条件),只能用于任务函数中。
-- @param id 消息ID
-- @number ms 等待超时时间,单位ms,最大等待126322567毫秒
-- @return result 接收到消息返回true,超时返回false
-- @return data 接收到消息返回消息参数
-- @usage result, data = sys.waitUntil("SIM_IND", 120000)
function waitUntil(id, ms)
    subscribe(id, coroutine.running())      
    local message = ms and {wait(ms)} or {coroutine.yield()}
    unsubscribe(id, coroutine.running())
    return message[1] ~= nil, unpack(message, 2, #message)
end

1、订阅id,并传入线程号

2、阻塞线程,如果接收到了消息,那么返回message

3、取消订阅该id

4、返回结果

运行原理

Lua 支持 coroutine ,这个东西也被称为协同式多线程 (collaborative multithreading)。 Lua 为每个 coroutine 提供一个独立的运行线路。举个通俗易懂的例子:去饭店吃饭,假设饭店只有一个厨师,这时候来了三个客人,分别点了一号菜,二号菜,三号菜。如果按照一二三这样的顺序做菜的话,效率很低。现在引入一种新模式,每个菜花2分钟时间去做。这样的顺序就变为了花两分钟做第一道菜,两分钟到了,做第二道菜,二分钟到了,然后第三道菜。这样的好处是每个客人的菜都有一段时间正在制作过程中,不会出现其他菜必须等到一道菜结束后才可以去做。客人就是上帝,二号客人比较饿,所以可以要求厨师花5分钟制作二号菜。这样的好处之一是可以对每道菜灵活分配时间。不太恰当的比喻,厨师就是CPU,客人就是任务。

先看一个简单的程序:

co = coroutine.create(                                      --1
    function(i)
        print(coroutine.status(co))
        print(i);
    end
)

print(coroutine.status(co))                                 --2
coroutine.resume(co, 1)                                     --3
print(coroutine.status(co))                                 --4

--输出结果
--suspended
--running
--1
--dead
  • 创建一个 coroutine 需要调用一次coroutine.create。它只接收单个参数,这个参数是 coroutine 的主函数。 create 函数仅仅创建一个新的 coroutine 然后返回它的控制器(一个类型为 thread 的对象);它并不会启动 coroutine 的运行。
  • 输出当前线程状态,为suspend(挂起,并未执行)
  • 唤醒线程,传入参数,此时执行线程,线程状态为running,输出1
  • 线程结束,正常退出,coroutine.resume(co, 1)返回true。输出线程状态,为dead。注意:dead之后不能再resume(死了的人怎么能唤醒呢?/滑稽)

这儿提到了三种状态,画了一个图来描述它们之间的关系

flow

方法 释义
coroutine.create() 创建coroutine,返回thread, 参数是一个函数建之后线程属于挂起状态,并没有执行!
coroutine.resume() 执行线程,和create配合使用,此时线程为running状态。
coroutine.yield() 挂起coroutine,将coroutine设置为挂起状态。下次执行resume,程序将回到挂起的位置继续执行而不是从头再执行。挂起成功返回true
coroutine.status() 查看coroutine的状态注:coroutine的状态有三种:dead,suspend,running。
coroutine.running() 返回正在跑的coroutine,一个coroutine就是一个线程,当使用running的时候,就是返回一个corouting的线程号

coroutine 可以通过两种方式来终止运行:一种是正常退出,指它的主函数返回(最后一条指令被运行后,无论有没有显式的返回指令); 另一种是非正常退出,它发生在未保护的错误发生的时候。第一种情况中, coroutine.resume返回 true,接下来会跟着 coroutine 主函数的一系列返回值。第二种发生错误的情况下, coroutine.resume返回 false ,紧接着是一条错误信息。

接下来我们分析一个更详细的实例(引用于Lua手册):

function foo (a)                                        --1
    print("foo 函数输出", a)
    return coroutine.yield(2 * a) -- 返回  2*a 的值
end

co = coroutine.create(function (a , b)                  --2
    print("第一次协同程序执行输出", a, b) -- co-body 1 10
    local r = foo(a + 1)

    print("第二次协同程序执行输出", r)
    local r, s = coroutine.yield(a + b, a - b)  -- a,b的值为第一次调用协同程序时传入

    print("第三次协同程序执行输出", r, s)
    return b, "结束协同程序"                   -- b的值为第二次调用协同程序时传入
end)

print("main", coroutine.resume(co, 1, 10)) -- true, 4       --3
print("--分割线----")
print("main", coroutine.resume(co, "r")) -- true 11 -9      --4
print("---分割线---")
print("main", coroutine.resume(co, "x", "y")) -- true 10 end    --5
print("---分割线---")
print("main", coroutine.resume(co, "x", "y")) -- cannot resume dead coroutine   --5
print("---分割线---")

--输出结果
--[[
第一次协同程序执行输出 1   10
foo 函数输出    2
main    true    4
--分割线----
第二次协同程序执行输出 r
main    true    11  -9
---分割线---
第三次协同程序执行输出 x   y
main    true    10  结束协同程序
---分割线---
main    false   cannot resume dead coroutine
---分割线---

]]

显然,这个例子比上面例子复杂许多,不过只要仔细分析,理解起来也不会困难

  • 调用resume唤醒线程,并传参1,10。输出“第一次协同程序执行输出 1 10”。接下来执行foo函数,输出“foo 函数输出 2”。在foo函数中遇到了yeild,挂起线程,此时程序停留在这儿,下次唤醒线程时从该处继续执行。返回yeild的参数。输出“main true 4”。 第二次调用resume唤醒线程,传入参数“r”,注意:此时传入的参数“r”,赋值给coroutine.yield,所以相当于local r = "r",输出“第二次协同程序执行输出r”。再次遇到yeild,挂起线程,此时程序停留在这儿,下次唤醒线程时从该处继续执行。返回yeild的参数。输出“main true 11 -9”。 第三次调用resume唤醒线程,传入参数“x”,“y”,赋值给coroutine.yield,相当于local r,s = "r","s",输出“第三次协同程序执行输出xy”。到这儿整个线程就结束了,输出“main true 10 结束协同程序”
  • 第四次调用resume唤醒线程,此时线程已经为dead了,无法唤醒。

resume和yield的配合强大之处在于,resume处于主程中,它将外部状态(数据)传入到协同程序内部;而yield则将内部的状态(数据)返回到主程中。

再举个小例子说明resume和yield关系

co = coroutine.create (function (a,b)
  local a,b = coroutine.yield(a+b)
  print("co", a,b)
end)
print(coroutine.resume(co,4,5))
coroutine.resume(co, 7, 8)
--输出
--[[
true    9
co  7   8
]]
  • 调用resume唤醒线程,并且传入4,5。遇到yeild,挂起程序,返回a+b。所以输出“true 9”。
  • 第二次调用resume唤醒线程,并且传入7,8。此时回到上次挂起的位置,并将赋值给a,b。相当于local a,b = 7,8

为了更好的理解LuaTask,花了大量时间讲解Lua的协同式多线程 ,接下来进入正题

先写一个测试程序

module(..., package.seeall)


sys.taskInit(function()
    cnt = 0
    while true do
        cnt = cnt + 1
        print("task_A_cnt: ", cnt)
        sys.wait(1000)
    end
end)

sys.taskInit(function()
    cnt = 0
    while true do
        cnt = cnt + 1
        print("task_B_cnt: ", cnt)
        sys.wait(2000)
    end
end)

输出结果,只摘抄了一小部分

task_B_cnt:     132
task_A_cnt:     133
task_A_cnt:     134
task_B_cnt:     135
task_A_cnt:     136
task_A_cnt:     137
task_B_cnt:     138
task_A_cnt:     139
task_A_cnt:     140
task_B_cnt:     141
task_A_cnt:     142

该测试程序总共创建了2个任务,第一个任务每次加1,挂起1000ms,第二个任务每次加1,挂起2000ms,所以最后的输出为:输出两次task_A_cnt, 输出一次task_B_cnt。如果在单片机上习惯写UCOS或者FreeRTOS的开发者看到这样的结构肯定不会陌生。

首先调用sys.taskInit创建任务,任务体的格式为

sys.taskInit(function()
    xxxx
    while true do
        xxxxx
        sys.wait(100)
    end
end)

还有一种为

local function xxxx(...) 
    xxxx
end
sys.taskInit(xxxx,...)

和UCOS,FreeRTOS的任务体大致相同,一个while死循环,然后通过延时切换任务。

接下来分析一下sys.taskInit和sys.wait两个重要的函数

先看sys.taskInit的源码

function taskInit(fun, ...)
    local co = coroutine.create(fun)
    coroutine.resume(co, unpack(arg))
    return co
end

sys.taskInit实际是封装了coroutine.createcoroutine.resume。创建一个任务线程,并执行该线程,返回线程号。

再看sys.wait

function wait(ms)
    -- 参数检测,参数不能为负值
    assert(ms > 0, "The wait time cannot be negative!")
    -- 选一个未使用的定时器ID给该任务线程
    if taskTimerId >= TASK_TIMER_ID_MAX then taskTimerId = 0 end
    taskTimerId = taskTimerId + 1
    local timerid = taskTimerId
    taskTimerPool[coroutine.running()] = timerid
    timerPool[timerid] = coroutine.running()
    -- 调用core的rtos定时器
    if 1 ~= rtos.timer_start(timerid, ms) then log.debug("rtos.timer_start error") return end
    -- 挂起调用的任务线程
    local message, data = coroutine.yield()
    if message ~= nil then
        rtos.timer_stop(timerid)
        taskTimerPool[coroutine.running()] = nil
        timerPool[timerid] = nil
        return message, data
    end
end

如何将定时器和任务组织起来的呢?其中最重要的就是taskTimerPool,timerPool这两个表。在此之前我们得每个线程的线程号都是唯一不变的。

程序流程:

  • 检测定时时间是否正确
  • 判断定时器是否用完,如果没有,则分配一个未使用的定时器ID给该任务线程
  • 定时器ID加1
  • 以线程号为下标存储定时器ID号到taskTimerPool表中
  • 以定时器ID号为下标存储线程号ID到timerPool表中
  • 开启定时器

这样描述比较抽象,举个例子会更好理解一点

sys.taskInit(function()
    cnt = 0
    while true do
        print("task: ", 1)
        sys.wait(100)
    end
end)

以这个简单的例子来解释

sys.taskInit创建并运行该线程,进入sys.wait函数,taskTimerId的初始值为0,所以+1,taskTimerId=1,coroutine.running()会返回正在运行的任务的线程号,也就是当前任务的线程号,比如该例中为0x8218dbc0。注意:线程号是唯一不会改变的。所以taskTimerPool[0x8218dbc0] = 1,timerPool[1] = 0x8218dbc0。这样就将定时器ID和线程号联系起来了。然后开启定时器,挂起该任务,执行下一任务。

问题来了,定时器达到定时时间的时候怎么处理呢?

看下面的代码

function run()
    while true do
        -- 分发内部消息
        dispatch()
        -- 阻塞读取外部消息
        local msg, param = rtos.receive(rtos.INF_TIMEOUT)
        -- 判断是否为定时器消息,并且消息是否注册
        if msg == rtos.MSG_TIMER and timerPool[param] then
            if param < TASK_TIMER_ID_MAX then
                local taskId = timerPool[param]
                timerPool[param] = nil
                if taskTimerPool[taskId] == param then
                    taskTimerPool[task  Id] = nil
                    coroutine.resume(taskId)
                end
            else
                local cb = timerPool[param]
                timerPool[param] = nil
                if para[param] ~= nil then
                    cb(unpack(para[param]))
                else
                    cb()
                end
            end
        --其他消息(音频消息、充电管理消息、按键消息等)
        elseif type(msg) == "number" then
            handlers[msg](param)
        else
            handlers[msg.id](msg)
        end
    end
end

读取外部消息,当定时器达到定时时间后,会发生一个消息。

rtos

所以,msg为rtos.MSG_TIMER,param为定时器ID号。

  • 判断是否为任务开启的定时器,若是,判断定时器ID是否超过最大值
  • 根据timerPool获取线程号并清除
  • 如果能在taskTimerPool中找到定时器ID和任务号对应,则唤醒该线程

这样,就能实现任务与任务之间的调度了。


LuaScript

函数运行

由于没有main函数,习惯常规单片机开发者一时不知怎么运行程序。程序运行有多种方法。

直接调用

项目中处理lib文件夹还有main.lua,test.lua。

main.lua

--重要提醒:必须在这个位置定义MODULE_TYPE、PROJECT和VERSION变量
--MODULE_TYPE:模块型号,目前仅支持Air201、Air202、Air800
--PROJECT:ascii string类型,可以随便定义,只要不使用,就行
--VERSION:ascii string类型,如果使用Luat物联云平台固件升级的功能,必须按照"X.X.X"定义,X表示1位数字;否则可随便定义
MODULE_TYPE = "Air800"
PROJECT = "WRITE_SN"
VERSION = "1.0.0"
require"sys"
--[[
如果使用UART输出trace,打开这行注释的代码"--sys.opntrace(true,1)"即可,第2个参数1表示UART1输出trace,根据自己的需要修改这个参数
这里是最早可以设置trace口的地方,代码写在这里可以保证UART口尽可能的输出“开机就出现的错误信息”
如果写在后面的其他位置,很有可能无法输出错误信息,从而增加调试难度
]]
--sys.opntrace(true,1)
require"test"
if MODULE_TYPE=="Air201" then
require"wdt"
end
sys.init(0,0)
sys.run()

main.lua需要使用require引用编写的脚本,这儿为test.lua。

test.lua

module(...,package.seeall)  

local function ss(  )
    print("ss function test")
end
ss()        --直接调用,在main.lua文件中require"test"的时候则会调用该函数

定时器

main.lua同上

test.lua

module(...,package.seeall)  

local function ss(  )
    print("ss function test")
end
--sys.timer_start(ss,3000)  --3秒运行一次
sys.timer_loop_start(ss,5000)  --循环定时器,每隔5秒运行一次

程序注册

在使用相关的程序做各种处理的时候,需要先将程序注册下,否则无法使用。这样做的好处是可以 将同类型程序集中放置一个表中,方便管理和修改。

常用的程序注册函数有

  • regmsg( )
  • regurc ( )
  • regapp( )
  • regnotify( )
  • regrsp( )
  • reguart( )

regmsg()用来注册相应的消息处理程序

regmsg()这个程序放在 sys.lua 这个库文件中。提供的lua 主架构sys.run() 目前能够处理的消息中,除定时器消息(定时器函数直接用 sys.timer_start 来启动)和物理串口消息(用 reguart 来注册) 外,其余外部消息,例如AT 命令的虚拟串口数据接收消息、音频消息、充电管理消息、按键消息等,均需要用 regmsg 注册相应的消息处理函数。

  • 串口消息 分为虚拟 AT 口(msg.uart_id = uart.ATC)和物理串口(msg.uart_id = 串口号)消息。 其中虚拟 AT 口的消息已经缺省注册了,不需要用户再注册; 物理串口消息处理函数使用前需要用 reguart 来注册。
  • 键盘消息(msg.id=rtos.MSG_KEYPAD=3) 需要用 regmsg()来注册处理函数
  • 中断消息(msg.id=rtos.MSG_INT=4) 需要用 regmsg()来注册处理函数
  • 电源管理消息(msg.id=rtos.MSG_PMD=5) 需要用 regmsg()来注册处理函数
  • 音频消息(msg.id=rtos.MSG_AUDIO) 需要用 regmsg()来注册处理函数
function 
    regmsg(id,handler) handlers[id] = handler
end

regmsg处理流程见下面的图:

regmsg

使用方法举例

例1:在应用模块中加入键盘处理程keymsg(msg)

第一步:编制键盘消息处理程序

local function keymsg(msg) 

  if msg.pressed then

--键盘按下时的处理程序 

    else 
  end

--键盘弹起时的处理程序

end

第二步:注册该处理程序

sys.regmsg(rtos.MSG_KEYPAD,keymsg) 

这样,当 rtos.receive()收到键盘消息msg(msg.id=rtos.MSG_KEYPAD=3)时,通过 sys.run()

的分发,自动进入 keymsg(msg)这个 function 中进行处理。


例2:在应用模块中加入GPIO 中断处理程序

第一步:编制 GPIO中断处理程序

假设:

gsensor连接的是GPIO5

机械式震动传感器连接的是GPIO3

GPS 连接的是 GPIO1

IO.gsensor= pio.P0_5 IO.shake = pio.P0_3 IO.gps = pio.P1_1

local function gpio_int(msg)

--产生下降沿脉冲中断

    if msg.int_id == cpu.INT_GPIO_NEGEDGE then

--如果产生中断的 GPIO 是gsensor 引脚

        ifmsg.int_resnum == IO.gsensor then

--如果产生中断的 GPIO 是机械式振动传感器引脚

        elseif  msg.int_resnum == IO.shake then

--如果产生中断的 GPIO 是 GPS 引脚

        elseif  msg.int_resnum == IO.gps then

        end

--产生上升沿脉冲中断

    elseif id ==cpu.INT_GPIO_POSEDGE then

    end

end

第二步:注册该处理程序

sys.regmsg(rtos.MSG_INT,gpio_int)

例3:在应用模块中加入 PMD(电源管理)消息处理程序

第一步:编制电源管理消息处理程序

local functionBatManage(msg)
    print("msg.voltage,msg.charger,msg.state =",msg.voltage,msg.charger,msg.state)                  DealCharger(msg)
    DealVolt(msg)
end

第二步:注册该处理程序

sys.regmsg(rtos.MSG_PMD,BatManage)

regapp( )用来注册应用程序

对 AT 命令的查询结果或某些事件的处理结果,会以 dispatch(消息 id, 参数 1,参数 2,….)的形式 来通知,如果想在 app 中对该消息 id 及对应的参数(参数1,参数 2,….)进行进一步处理,需要事先以 regapp 方式注册该 app 程序,注册后,每次 dispatch 相应的消息 id,就自动会进入相应的 app 程序 对 dispatch 出来的消息 id 和参数进行处理。

regapp 的详细流程详见下图:

regapp

regapp有 2 种注册方式:

1)以表的形式来注册

localtable = { 消息 id1 = app1, 消息 id2 = app2, …… , 消息 idn = appn }sys.regapp(table)

这种以表的方式注册 app 程序,一次可以注册多个 app。

使用方法举例:

例:注册多个dispatch 消息处理程序

第一步:填写注册表

在本例中: 长按键消息(MMI_KEYPAD_LPRESS)和短按键消息(MMI_KEYPAD_SPRESS)由客户编写用程序 dispatch 出来; 通话相关的消息(CALL_CONNECTED,CALL_DISCONNECTED,CALL_INCOMING)由库文件 cc.lua dispatch 出来

local idleapp = {
  MMI_KEYPAD_LPRESS = LongPresskey,     --长按键处理
  MMI_KEYPAD_SPRESS = ShortPresskey,    --短按键处理
  CALL_CONNECTED = connect,             --通话接通 
  CALL_DISCONNECTED = disconnect,       --通话挂断
  CALL_INCOMING = incall                --来电话
}

第二步:注册该表

sys.regapp(idleapp)

2) 以 app 的形式注册

sys.regapp(app,消息 id)

以 app 形式注册的程序,一次只可以注册一个 app,但是消息 id 可以不止一个。 使用方法举例:

例:当网络注册状态改变时,闪灯随之改变

第一步:编制闪灯变化程序

local functionlight(id,data)

-- 当 GSM 网络注册状态发生变化时闪灯发生变化

    if id == "NET_STATE_CHANGED" then if data =="REGISTERED" then
        if data == "REGISTERED" then
            GPIO_ontime(SLOW,ioChgNet)            --蓝灯慢闪
        else
            GPIO_ontime(SLOW,ioChgingNoNet)       --红灯慢闪
    end

GPIO_ontime(SLOW,ioChgingNoNet)   --红灯慢闪

-- 当充电状态发生变化时的闪灯也相应变化

    elseif id == “CHARGE_STATE_CHANGED” then 
        if data ==“CHARGING” then
            GPIO_ontime(FAST,ioChgingNoNet)     --红灯快闪
        else
            GPIO_on(ioChgNet) --蓝灯常亮
        end
    end 
end

第二步:注册 light 程序

sys.regapp(light,"NET_STATE_CHANGED",”  CHARGE_STATE_CHANGED”)

regurc()用来注册某些 URC相应的处理程序

URC=unsolicitedresult code ,即非请求式上报命令,也就是主动上报的命令。

如果用户需要在应用模块中自己处理感兴趣的 URC,需要用 regurc() 程序来注册 URC 处理程序来达到 目的。regurc() 程序放在 ril.lua 中。

fuction regurc(prefix,handler) 
    urctable[prefix] =handler
end

regurc 处理流程见下面的图:

regurc

使用方法举例:

例:处理 URC

第一步:根据需求,自己编制 URC 的处理程序

local function IdleUrc(data,prefix) 
  if prefix =="+CPIN" then
    local p =smatch(data,"NOT%s*INSERTED",string.len(prefix)+1) 
        if p then
        pbapp.SetSim(false)
        end
        local p = smatch(data,"READY",string.len(prefix)+1) 
        if p then
            pbapp.SetSim(true)          
        end
    end
end

第二步:注册该程序

ril.regurc("+CPIN",IdleUrc)

regrsp()用来注册 AT 命令处理程序

当用户发送 AT 命令时,一般是需要这些命令的返回结果的,AT 命令返回结果的处理程序 app 是通 过 regrsp()函数注册在 rsptable 表中的。这样当发送AT 命令时,会自动执行该处理程序处理 AT 命令 返回结果。

functionregrsp(head,fnc,typ) 放在 ril.lua这个库文件中。

head是 AT 命令的头,fnc 是处理函数,typ 是该命令的类型(cmd type: 0:no reuslt 1:number 2:sline3:mline 4:string10:spec)。

例如:AT+CHFA?这个获取音频通道命令的 head 是+CHFA? ,fnc 是 AT 命令返回结果处理程序, 由客户自己来编写,typ 是 2。

对 AT 命令的返回结果的处理,一般有两种情况:

1)一些常用的 AT 命令的处理程序,库文件已经缺省用 regrsp 注册了。此时处理程序会把 AT 命令结果 dispatch 出来,后续再用 regapp 注册的 app 再做进一步处理(适用于异步处理或前后有因果关系的情 况)

2)如果 AT 命令在 lib库文件中没有被 regrsp 注册过,则需要用 regrsp()注册相应的处理程序。此时 用户可以在这些处理程序中直接处理 AT 命令返回结果(适用于同步处理),也可以用 1)的异步处理方 式。

regrsp 处理流程见下面的图:

regrsp

使用方法举例:

例 1:使用 AT+CPAS?命令获取手机状态

第一步:编制 rsp 程序来处理 AT+CPAS?命令的返回结果

local sta
local function rsp(cmd,success,response,intermediate) 
  if cmd == "AT+CPAS?" then
    sta =string.match(intermediate,”+CPAS:%s*(%d)”)
    end
end

第二步:注册该处理程序

ril.regrsp("+CPAS?",rsp,2)

reguart()用来注册 uart 口的数据处理程序

串口(包括物理串口和 debug uart 口)使用之前需要先用reguart()程序来注册处理程序。

reguart()放在sys.lua 这个库文件中。

function reguart(id,fnc) 
    uartprocs[id] = fnc
end

现在的端口 id 分配如下:

物理端口 1:id = 1

物理端口 2: id = 2

debug 口: id = 3

reguart 处理流程见下面的图:

reguart

举例说明:如何使用 debuguart 口与上位机通讯

第一步:设置 debug uart 口的通讯特性

uart.setup(3,921600,8,uart.PAR_NONE,uart.STOP_1,2)--(debug uart 口波特率必须是921600 ,mode=2,使用 ID=0xA2数据透传)

第二步:编写 debug uart 的输入输出处理程序

local function huartreader() 
    local s
    while true do
        s =uart.read(3,"*l",0)
        if string.len(s) ~= 0 then 
            cmddealer(s)
        else
            break;
        end
     end
end

第三步:注册该处理程序

sys.reguart(3,huartreader)

运行原理

​ 该架构是用消息机制实现的,运行框架基于消息处理机制,消息根据来源分为两种:内部消息和外部消息。 内部消息:lua 脚本调用本文件 dispatch 接口产生的消息,消息存储在 qmsg 表中。 外部消息:底层 core 软件产生的消息,lua 脚本通过 rtos.receive 接口读取这些外部消息。 ​ 消息根据类型,也有两种:table 型和 Number 型。Number 型使用比较频繁。 Number 类型的消息目前分为:

  • 定时器 timeout 消息(msg.id=rtos.MSG_TIMER=1)
  • 串口消息(msg.id = rtos.MSG_UART_RXDATA=2)又分为虚拟 AT 口(msg.uart_id = uart.ATC)和串口(msg.uart_id = 串口号)
  • 键盘消息(msg.id=rtos.MSG_KEYPAD=3)
  • 中断消息(msg.id=rtos.MSG_INT=4)
  • 电源管理消息(msg.id=rtos.MSG_PMD=5)。

​ 当电池在位状态、电池电压百分比、电池电压、充电器在位状态、充电状态任何一个发生-- 变化,就会上报该消息。

  • msg.present (电池在位状态,boolean 型,true 或 false)
  • msg.level (百分比 0-100,number 型)
  • msg.voltage (电池电压,number 型)
  • msg.charger (充电器在位状态,boolean 型,true 或 false)
  • msg.state (充电状态,number 型,0-不在充电 1-充电中 2-充电停止)

消息注册

​ 各个消息处理程序使用前需要用 regmsg 程序注册到 handlers 表中(除了定时器 timeout 消息和 物理串口消息以外)。这样代码改动影响面小,当业务流程有修改的时候,只需要修改具体的消息处 理程序的内容即可。

run() 流程详细介绍

function run()
    local msg,msgpara
    while true do
        --处理内部消息
        runqmsg()
        --阻塞读取外部消息
        msg,msgpara = rtos.receive(rtos.INF_TIMEOUT)

        --电池电量为0%,用户应用脚本中没有定义“低电关机处理程序”,并且没有启动自动关机定时器       
        if --[[not lprfun and ]]not lpring and type(msg) == "table" and msg.id == rtos.MSG_PMD and msg.level == 0 then
            --启动自动关机定时器,60秒后关机
            lpring = true
            timer_start(rtos.poweroff,60000,"r1")
        end

        --外部消息为table类型
        if type(msg) == "table" then
            --定时器类型消息
            if msg.id == rtos.MSG_TIMER then
                timerfnc(msg.timer_id)
            --AT命令的虚拟串口数据接收消息
            elseif msg.id == rtos.MSG_UART_RXDATA and msg.uart_id == uart.ATC then
                handlers.atc()
            else
                --物理串口数据接收消息
                if msg.id == rtos.MSG_UART_RXDATA then
                    if uartprocs[msg.uart_id] ~= nil then
                        uartprocs[msg.uart_id]()
                    else
                        handlers[msg.id](msg)
                    end
                --串口发送数据完成消息
                elseif msg.id == rtos.MSG_UART_TX_DONE then
                    if uartxprocs[msgpara] then
                        uartxprocs[msgpara]()               
                    end
                --其他消息(音频消息、充电管理消息、按键消息等)
                else
                    handlers[msg.id](msg)
                end
            end
        --外部消息非table类型
        else
            --定时器类型消息
            if msg == rtos.MSG_TIMER then
                timerfnc(msgpara)
            --串口数据接收消息
            elseif msg == rtos.MSG_UART_RXDATA then
                --AT命令的虚拟串口
                if msgpara == uart.ATC then
                    handlers.atc()
                --物理串口
                else
                    if uartprocs[msgpara] ~= nil then
                        uartprocs[msgpara]()
                    else
                        handlers[msg](msg,msgpara)
                    end
                end
            --串口发送数据完成消息
            elseif msg == rtos.MSG_UART_TX_DONE then
                if uartxprocs[msgpara] then
                    uartxprocs[msgpara]()               
                end
            end
        end
        --打印lua脚本程序占用的内存,单位是K字节
        --print("mem:",base.collectgarbage("count"))
    end
end

​ 在 ril.lua 中我们已经用 sys.regmsg("atc",atcreader)把 ATC 口的处理程序注册好了。

​ 如果需要电源管理,可以单独写个应用模块,比如取名为 power.lua,并在里面注册 rtos.MSG_PMD 消息处理程序,并定义相应的处理程序。

​ 如果需要 GPIO 中断,也可以单独写个应用模块,并在里面注册 rtos.MSG_INT 消息的处理程序, 并定义相应的处理程序。

​ 如果想单独运行一个函数,可以将通过定时器调用函数。具体例子可参阅定时器