3. 物模型LUA开发手册
3. 物模型LUA开发手册
工具函数:为了避免不同开发人员使用工具不同造成的结果差异,云端将事业部lua里常用的函数抽取为公共函数库,事业部编写lua直接调用云端定义好的函数据即可,同时也减少了事业部lua文件的大小。
云能力函数:在实现物模型的过程中,事业部lua里需要向设备发起指令透传,或者查询设备的影子状态等,云端把这些操作封装在固定的lua文件中,事业部访问云端的能力就和调用本地函数一样便捷。
事业部lua函数:事业部开发同学需要实现lua模板里的五个函数做具体的物模型实现逻辑,上游调用者根据不同的操作调用对应的函数。
快速开始
在开始之前,需要先下载一份云端提供的lua模板(第五章节有提供下载路径,压缩包里的luaTemplate即为lua模板),事业部lua开发者只需实现里面的对应的五个函数做业务即可。在做业务的过程中用到的一些工具函数/云端能力函数使用方法参照下面对应章节的函数介绍。同时云端提供了一份针对某个型号编写的物模型代码样例,供有需要的同事进行参考。
一、工具函数
云端将事业部lua中的通用的工具方法抽取为公共函数,包括json和table的互转,table和字符串的互相操作等,这些函数在lua模板开头已经声明为了local变量,需要时直接调用即可。
1.1 jsonEncode
将table类型的数据转为json字符串返回
入参:table类型的数据
输出:输出json字符串
示例:
local tab = { message = "this is a test message", title = "test" }``local jsonStr = jsonEncode(tab)``print(jsonStr)``--输出:{"message":"this is a test message","title":"test"}
1.2 jsonDecode
将json字符串转为table类型的数据返回
入参:字符串类型的json数据
输出:输出table结构的数据
示例:
local jsonStr = '{"message":"this is a test message","title":"test"}'``local tab = jsonDecode(jsonStr)``print(tab)``--输出table类型的数据,key,value可以⾃⾏遍历
1.3 makeSum
进行sum校验
入参:输入需要校验的table,table的起始位置和结束位置
注意:入参的table存放数据时指定下标应该从1开始,start_pos最小指定1
输出:输出检验和
示例:
local msgBytes = {}``msgBytes[1] = 0xAA``msgBytes[2] = 1``msgBytes[3] = 200``print(makeSum(msgBytes, 1, 3))``--输出sum校验后的值:141
1.4 crc8_854
crc校验,逻辑和事业部之前的实现保持一致
入参:输入需要校验的table,table的起始位置和结束位置
输出:输出校验值
注意:入参的table存放数据时指定下标应该从1开始,start_pos最小指定1
示例:
local msgBytes = {}``msgBytes[1] = 0xAA``msgBytes[2] = 0xAC``msgBytes[3] = 1``msgBytes[4] = 13``msgBytes[5] = 56``msgBytes[6] = 88``msgBytes[7] = 90``msgBytes[8] = 0xAD` `print(crc8_854(msgBytes, 1, 8))``--输出crc8_854后的值:248
1.5 hexStr2DecimalArray
将十六进制字符串转为10进制数组
字符串每两位拆分一次做为16进制进行10进制转换,如果长度为奇数,则转为table后最后一位丢失
入参:16进制字符串
输出:10进制数组
示例:
---hexStr2DecimalArray``---输⼊:0A0B03040F``---输出:10、11、3、4、15``tab = hexStr2DecimalArray("0A0B03040F")``for _, value in pairs(tab) do``print(value)``end``---输⼊:0A0B03040FF``---输出:10、11、3、4、15``--注意,⻓度为奇数,所以最后⼀位会被忽略``tab = hexStr2DecimalArray("0A0B03040FF")``for _, value in ipairs(tab) do``print(value)``end
1.6 decimalArray2HexStr
将10进制的数组转为16进制字符串
注意:根据使用场景,输出的每个16进制数字使用2个字符表示,所以输入的整数最大值不能超过255
入参:10进制数组
输出:16进制字符串
示例:
---输⼊ { 97, 98, 99 }``---输出616263``local hexStr = decimalArray2HexStr({ 97, 98, 99 })``print(hexStr)``---输⼊ { 0, 98, 256 }``---输出0062100``--注意:这⾥最后⼀个元素为256,所以输出的16进制为三位字符100,``--在事业部的使⽤场景下应该传0-255之间的数字``local hexStr = decimalArray2HexStr({ 0, 98, 256 })``print(hexStr)
1.7 numstring2table
字符串每两位拆分一次做为table的元素,如果长度为奇数,则转为table后最后一位丢失
入参:字符串
输出:输出table结构的数据
示例:
--numstring2table``--输⼊0102030405``--输出01、02、03、04、05``tab = numstring2table("0102030405")``for _, value in pairs(tab) do``print(value)``end``--输⼊010203040``--输出01、02、03、04``--注意,⻓度为奇数,所以最后⼀位会被忽略``tab = numstring2table("010203040")``for _, value in pairs(tab) do``print(value)``end
1.8 checkBoundary
检查数据边界
入参:被检查数据,范围最小值,范围最大值
输出:满足条件的数字
示例:
--checkBoundary``--参数依次表⽰data, min, max``--如果data为nil,则返回0``--如果data介于min和max之间,则返回data本⾝``--如果data⽐min⼩,则返回min``--如果data⽐max⼤,则返回max``local data = checkBoundary(10, 90, 99)``print(data)``--输出:90``data = checkBoundary(91, 90, 99)``print(data)``--输出:91``data = checkBoundary(100, 90, 99)``print(data)``--输出:99
二 、云端能力函数
2.1 queryShadow(查询设备影子)
描述:从设备影子查询设备状态
调用方式:
local result = queryShadow(applianceCode)
入参:
参数 | 格式 | 示例 | 说明 |
---|---|---|---|
applianceCode | string | 198821311121312 | 设备code |
出参:
返回值 | 格式 | 示例 | 说明 |
---|---|---|---|
result | table | { ["moduleA.property1"] = "value1"} | 满足物模型格式的json数据 |
2.2 transparent(透传指令到设备端,会返回设备的最新响应信息)
描述:下发二进制指令到设备端
调用方式:
local result= transparent(applianceCode,order)
入参:
参数 | 格式 | 示例 | 说明 |
---|---|---|---|
applianceCode | string | 198821311121312 | 设备code |
data | string | 0cbd40a77f364484bb9c548b15629288 | 透传的指令待透传的指令,十六进制字符串 |
出参:
返回值 | 格式 | 示例 | 说明 |
---|---|---|---|
result | string | AA4340000000000000020101FFFFFF63 | result表示设备回复的指令,十六进制字符串 |
2.3 transparentWithoutResp(透传指令到设备端,无需等待设备响应)
描述:下发二进制指令到设备端,无需等待设备响应
调用方式:
transparentWithoutResp(applianceCode,order)
入参:
参数 | 格式 | 示例 | 说明 |
---|---|---|---|
applianceCode | string | 198821311121312 | 设备code |
data | string | 0cbd40a77f364484bb9c548b15629288 | 透传的指令待透传的指令,十六进制字符串 |
2.3.1 transparentFullJson(透传json数据到南向网关)
描述:透传json数据给到南向网关
调用方式:
local result= transparentFullJson(applianceCode,order)
入参:
参数 | 格式 | 示例 | 说明 |
---|---|---|---|
applianceCode | string | 198821311121312 | 设备code |
data | json对象格式 | json对象格式 |
出参:
返回值 | 格式 | 示例 | 说明 |
---|---|---|---|
result | json对象格式 |
2.4 triggerEvent(触发事件)
描述:触发事件
调用方式:
triggerEvent(applianceCode, moduleCode, eventCode, eventData)
入参:
参数 | 格式 | 示例 | 说明 |
---|---|---|---|
applianceCode | string | 198821311121312 | 设备code |
moduleCode | string | common | |
eventCode | string | error | |
eventData | table | { ["key1"]= "value1", ["key2"]="value2" } | 物模型格式数据 |
2.5 reportProperties(上报属性)
描述:上报属性
调用方式:
reportProperties(applianceCode, props)
入参:
参数 | 格式 | 示例 | 说明 |
---|---|---|---|
applianceCode | string | 198821311121312 | 设备code |
props | table | table | { ["moduleA.prop1"]= "value1", ["moduleB.prop1"]= "value2" } |
version | string | 1.0或者2.0 | **由于历史原因,在2025年3月18日以后,如果要在下行函数,如setProperty中调用reportProperties函数,需要加上version取值为2.0。否则调用不生效。**备注:如非必要请不要在下行函数中调用reportProperties,除非特殊情况 |
用于将物模型json上报到云端,云端做设备影子更新、状态推送到终端等操作,通常在reportData里调用,将设备上报的指令(0040,0044,8020)转换为物模型属性后再掉reportProperties这个函数将结果发送云端。
但是存在这样的设备:透传控制设备,设备状态变化后不能及时的主动上报,这时候云端的设备影子数据得不到及时更新,调用方查询影子还是设备的旧状态。此时我们可以将设备控制的返回结果(8020指令)转换为物模型属性,然后通过reportProperties函数将结果发送给云端以更新设备影子,我们称之为:设备影子补偿。
实现补偿有两种方式:
一种是在代码里主动补偿,即在invokeAction、getProperty、getAllProperties、setProperty函数里主动调用reportProperties函数。随着云端不断迭代和优化,此种方式已废弃,请使用 配置式补偿。
另一种是配置式补偿,在开发者平台配置开启补偿,则8020的上报指令,最终会转发到物模型lua里的reportData函数里,在reportData里一般都是会做reportProperties属性上报的。
主动补偿:(代码里主动编写补偿,自2023-11-08后废弃此种方式)
废弃理由以及使用其他方案的好处:比如在非物模型链路控制设备(如插件页,场景,事业部等使用原生json控制/二进制透传),物模型链路里的设备影子数据依然得不到及时更新。而使用 配置式补偿 方式,任何渠道控制设备,只要设备回复了8020都会更新设备影子。
function _M:setProperty(sn, applianceCode, spid, params)
local currentStatus = self:encodeMcuAndSend(sn, applianceCode, spid, CONST_MCU_TYPE_QUERY, nil)
local setData = convert:thingJsonToMcuJson(params)
--合并json(用params的值 覆盖 currentStatus的值)
local luaJson = jsonMerge(setData,currentStatus)
if(logger.isDebugEnabled()) then
logger.debug("==final参数begin")
logger.debug(jsonEncode(luaJson))
logger.debug("==final参数end")
end
local rtnJson = self:encodeMcuAndSend(sn, applianceCode, spid, CONST_MCU_TYPE_CONTROL, luaJson)
if(logger.isDebugEnabled()) then
logger.debug("==set return begin")
logger.debug(jsonEncode(rtnJson))
logger.debug("==set return end")
end
--如需补偿影子,可以在这里操作
local thingJson = convert:mcuJsonToThingJson(rtnJson)
formatResult(thingJson, sn)
reportProperties(applianceCode, thingJson) //主动补偿,2023-11-08后,废弃此种方式,开发者自行切换到配置式补偿方案
return thingJson
end
配置式补偿(补偿唯一方式)
在适配lua的时候,开启 影子补偿 操作,则8020指令上报后,最终会触发Thing lua里的reportData函数的调用,在reportData里掉reportProperties进行属性上报即可。
function _M:reportData(sn, applianceCode, spid, msgType, msgBody)– 0040,0044,8020指令都会到这里
local luaJson = self:mcu():decodeMcu(sn, applianceCode, spid, hexStr2DecimalArray(msgBody))
local thingJson = self:convert():mcuJsonToThingJson(luaJson)
if (msgType == '0x0040' ) then
--todo
--上报属性
reportProperties(applianceCode, thingJson)
elseif (msgType == '0x0044' ) then
--todo
--上报属性
reportProperties(applianceCode, thingJson)
elseif (msgType == '0x8020' ) then
local msg = hexStr2DecimalArray(msgBody)
if (msg[10] == 0x02) then
--todo
reportProperties(applianceCode, thingJson) --只在reportData里调用
elseif (msg[10] == 0x03) then
--todo
reportProperties(applianceCode, thingJson) --只在reportData里调用
end
end
end
2.6 状态存储能力
在lua头部引入:
local stateStore = interactive.stateStore
状态存储
调用方式:
local key = "APPLIANCE_STATUS"
local value = jsonEncode({power = "on",temperature = 60})
local expireSeconds = 60 * 60
stateStore.set(key ,value , expireSeconds ``)
参数 | 格式 | 示例 | 说明 |
---|---|---|---|
key | string | APPLIANCE_STATUS | **用户设置的key,一般是业务码,如保存设备状态,可以设置为APPLIANCE_STATUS,保存设备信息,可以设置为**APPLIANCE_INFO****为了避免缓存key的冲突,服务端实际存储的key是: applianceCode:{applianceCode}:{key}以做数据间的隔离,也就是数据是以设备code维度隔离的 |
value | string | {"power":"on"," temperature" : 60} | 缓存值,字符串类型 |
expireSeconds | int | 60*60 | **过期时间,单位秒。**如果不设置则默认3600秒,即1小时 |
状态查询
调用方式:
local key = "APPLIANCE_STATUS"
local value = stateStore.get(key``)
参数 | 格式 | 示例 | 说明 |
---|---|---|---|
key | string | APPLIANCE_STATUS | 缓存key |
出参:
返回值 | 格式 | 示例 | 说明 |
---|---|---|---|
value | string | 返回的value值,即存储时保存的值,如果未设置值或者设置的值已过期/被删除,则返回nil |
状态删除
调用方式:
local key = "APPLIANCE_STATUS"
stateStore.delete(key``)
参数 | 格式 | 示例 | 说明 |
---|---|---|---|
key | string | APPLIANCE_STATUS | 缓存key |
2.7 休眠延迟能力
在lua头部引入:
local sleep= interactive.sleep
调用方式:
doA
**sleep(1000) --休眠1000毫秒,即1秒**
**doB**
参数 | 格式 | 示例 | 说明 |
---|---|---|---|
milliseconds | int | **50** | sleep休眠时间,单位毫秒,最大 8*1000,即8秒。一般业务应控制在几十到几百毫秒以内最好,不宜过长 |
注意:在一次完整的业务逻辑交互中,可以调用多次sleep,但是多次休眠的总时长尽可能的控制在8秒(时间越短越好)以内。因为整个物模型控制链路的总时长最大只能是9.5秒,超过9.5秒接口会超时,9.5秒包含透传等待回复时间 + sleep休眠时间+其他业务处理耗时。
2.8 设备联动
在lua头部引入:
local getLinkages,linkInvokeAction, linkSetProperty,linkGetProperty,linkGetAllProperties = interactive.getLinkages,interactive.linkInvokeAction, interactive.linkSetProperty, interactive.linkGetProperty, interactive.linkGetAllProperties
控制从设备时(对于linkInvokeAction, linkSetProperty,linkGetProperty,linkGetAllProperties四个函数的调用),从设备如果不在线,则这四个函数会抛出设备离线的异常
getLinkages(获取联动关系)
调用方式:
local value = getLinkages()
if(value and next(value ) ~= nil) then
--获取到联动关系,开始做逻辑处理
end
linkInvokeAction(调用从设备的invokeAction能力)
调用方式:
local value = linkInvokeAction(interNum, slaveApplianceCode, moduleCode, actionCode, params)
参数 | 格式 | 示例 | 说明 |
---|---|---|---|
interNum | number | 内部编号,设备端可能只存编号 | |
slaveApplianceCode | string | 从设备code | |
moduleCode | string | 从设备物模型中定义的模块 | |
actionCode | string | 从设备物模型中定义的action code | |
params | table | action的入参 |
linkSetProperty (控制从设备)
调用方式:
localvalue = linkSetProperty(interNum, slaveApplianceCode, params);
参数 | 格式 | 示例 | 说明 |
---|---|---|---|
interNum | number | 内部编号,设备端可能只存编号 | |
slaveApplianceCode | string | 从设备code | |
params | table | {["power"] = true} | 待控制的物模型属性 |
linkGetProperty (获取从设备指定属性)
调用方式:
local value = linkGetProperty(interNum, slaveApplianceCode, params);
参数 | 格式 | 示例 | 说明 |
---|---|---|---|
interNum | number | 内部编号,设备端可能只存编号 | |
slaveApplianceCode | string | 从设备code | |
params | table | 待查询的物模型属性 |
linkGetAllProperties
(获取从设备全属性)
调用方式:
local value = linkGetAllProperties(interNum, slaveApplianceCode);
参数 | 格式 | 示例 | 说明 |
---|---|---|---|
interNum | number | 内部编号,设备端可能只存编号 | |
slaveApplianceCode | string | 从设备code |
三、物模型lua模板
事业部开发同学需要实现模板里的5个函数,具体函数定义见模板里的注释或者3.1~3.5的介绍
一个物模型lua拆分为3个模块,主要是为了在功能上解耦:
1.物模型模块:物模型请求入口,负责接收请求、调用 属性转换模块和电控模块完成具体逻辑、和设备交互
2.属性转换模块:负责将物模型的json和mcuJson相互转换
3.电控协议模块:负责把mcuJson转换为电控指令,或把电控指令转换为mcuJson
下面用一张图来描述一个真实交互流程,用于说明各个模块之间测职责划分
3.1 invokeAction(调用物模型方法)
提供一个调用物模型定义的Action入口,包含需要调用的方法名,参数等必要信息,此函数需要实现电控逻辑的交互、协议转换及物模型定义的JSON参数、响应结果处理。
参数信息
参数名 类型 说明 sn string 当前待操作设备的SN,可用于Lua脚本的一些基本信息判断 applianceCode string 设备编码,当前待操作的设备编码 spid number 产品唯一标识 method string 要调用的方法名称,格式为{module}.{actionCode},由模块代码+模块下的方法组成 params table 要操作方法的输入参数信息,格式须符合物模型API中定义的输入参数格式 响应信息
返回符合物模型定义的JSON对象(lua中返回Table)。
3.2 setProperty(设置属性)
修改设备属性为指定的状态。
参数信息
参数名 类型 说明 sn string 当前待操作设备的SN,可用于Lua脚本的一些基本信息判断 applianceCode string 设备编码,当前待操作的设备编码 spid number 产品唯一标识 params
table 要设置的设备参数,table类型,key表示要获取的模块,value(也是table类型)表示要设置的属性和属性值 响应信息
返回空table,即 {}即可。返回的任何数据都会被丢弃
针对某些型号的设备,如果控制下发的参数和设备当前对应的属性值一致,设备是不会回复的,体现在前端报错:控制超时,如冰箱的OEM设备。这里可以做以下处理:
先查询设备当前状态(支持影子的查询影子,不支持影子则透传查询),如果查询的结果跟要控制的属性值一致,那就直接返回成功,无需再透传控制。
全状态控制的写法:
3.3 getProperty(查询属性)
查询设备指定的属性状态。
参数信息
参数名 类型 说明 sn string 当前待操作设备的SN,可用于Lua脚本的一些基本信息判断 applianceCode string 设备编码,当前待操作的设备编码 spid
number 产品唯一标识 params
table 要查询的设备属性,table类型。如 {"fan.power","fan.workMode"}
响应信息
返回符合物模型定义的JSON对象(lua中返回Table)。
3.4 getAllProperties(查询所有属性)
查询设备的所有状态。
参数信息
| 参数名 | 类型 | 说明 |
---------------| -------------| --------| --------------------------------------------------- |
| sn | string | 当前待操作设备的SN,可用于Lua脚本的一些基本信息判断 |
| applianceCode | string | 设备编码,当前待操作的设备编码 |
| spid | number | 产品唯一标识 |响应信息
返回符合物模型定义的JSON对象(lua中返回Table)。
3.5 reportData(上报一个二进制数据)
宿主服务将设备上报的消息,通过此函数通知到Lua脚本,Lua脚本需要解析此消息并生成属性变更事件或通知事件。
参数信息
参数名 类型 说明 sn string 当前待操作设备的SN,可用于Lua脚本的一些基本信息判断 applianceCode string 设备编码,当前待操作的设备编码 spid
number 产品唯一标识 msgType string 指令类型,如 0x0040 msgBody string 上报的二进制数据,十六进制编码格式,包含了完整的电控上报数据包 响应信息
无。
3.6 listenReport(从设备上报信息转发到主设备的lua函数)
从设备上报的指令信息转发到主设备的lua函数。
主设备的Thing lua里增加函数:
listenReport(applianceCode, spid, slaveApplianceCode, slaveSpid, reportData)
参数信息
参数名 类型 说明 applianceCode string 当前待操作设备的code(主设备) spid string 主设备的产品唯一标识 slaveApplianceCode
number 从设备的code slaveSpid string 从设备的产品唯一标识 reportData string 从设备上报的二进制数据,十六进制编码格式,包含了完整的电控上报数据包 响应信息
无。
四、lua最佳实践/云端规范
云端同学在编写lua的过程中,踩了一些坑,同时也总结了一些经验,为了避免大家再走同样的弯路,所以把这些整理为规范供大家参考
参考:http://lua-users.org/wiki/OptimisationTips
https://springrts.com/wiki/Lua_Performance
http://www.lua.org/gems/sample.pdf
lua的索引是从1开始
在lua中,不论是数组操作,还是sting等操作,下标都是从1开始的。这是与绝大多数语言不同的地方。如果非要指定下标为0,只能说lua允许你这么用,但是语言层面并不提供足够的支持。
如下:输出ABCD字符串中A出现的为位置,我们发现输出的结果为1
local s, e = string.find("ABCD", "A")``print(s, e) --> 输出:1 1` `s, e = string.find("ABCD", "B")``print(s, e) --> 输出:2 2
如下,输出数组A、B、C的第0个和第1个元素,根据返回结果说明数组的下标其实是从1开始的
local arr = { "A", "B", "C" }``print(arr[0]) --nil``print(arr[1]) --
如下:使用 ipairs 遍历数组的时候,下标为0的元素不会被遍历进去
local msgBytes = {}``msgBytes[0] = "AAA" --应当在设置值的时候,指定下标从1开始设置值``msgBytes[1] = "BBB"``msgBytes[2] = "CCC"``for i, j in ipairs(msgBytes) do``print(i, '->', j)``end``1 -> BBB``2 -> CCC
同样用#计算它长度计算也不会计算下标为0(包括负数)的元素
local msgBytes = {}``msgBytes[0] = "AAA"``msgBytes[1] = "BBB"``msgBytes[2] = "CCC"``print(#msgBytes) -- 2
同样使用pairs遍历,顺序被打乱,因为下标为0的元素没有保存在array part里,会导致遍历顺序不一样(因为优先遍历array part)
local msgBytes = {}``msgBytes[0] = "AAA"``msgBytes[1] = "BBB"``msgBytes[2] = "CCC"``for i, j in pairs(msgBytes) do``print(i, '->', j)``end``--输出:``--1 -> BBB``--2 -> CCC``--0 -> AAA
以下为lua table的数据结构部分c代码
// lua table的基本数据结构``typedef struct Table {``CommonHeader;``lu_byte flags; /* 1<<p means tagmethod(p) is not present */``lu_byte lsizenode; /* log2 of size of `node' array */``struct Table *metatable;``TValue *array; /* array part */``Node *node; /* hash part */``Node *lastfree; /* any free position is before this position */``GCObject *gclist;``int sizearray; /* size of `array' array */``} Table;
函数外禁止声明可以被修改的local变量
这里是由于云端为了提升性能缓存了dofile的结果,非lua本身问题
如果变量值可能会被修,那么尽量把它的生命周期控制在函数内部,避免函数外部的变量被修改。
如下代码,函数外的局部变量tab被createTab函数修改,如果期望的结果是每次访问createTab都给里面设置(新增)一个元素,那这样并没问题。但是如果是期望每次访问createTab都只是把传入的参数放到一个新的table返回,那这样就有问题了,这样每次请求会相互干扰(因为云端缓存了dofile的结果,这里每次访问tab变量都是指向同一个tab),导致结果不正确。
如下:每次访问createTab都返回的是同一个table,多次访问都会修改同一个table
local _M = {}``local tab = {} -- 如果这个值会被修改,那尽量在函数内部声明` `function _M.createTab(num)``table.insert(tab, num)``return` `tab``end``return` `_M
local _M = dofile("BBB.lua")``print(#_M.createTab(1))--1``print(#_M.createTab(2)) --2``print(#_M.createTab(3)) --3
如下:每次访问createTab都返回不同的table,多次访问结果无干扰
local _M = {}``function _M.createTab(num)``local tab = {} --每次都返回⼀个新的table``table.insert(tab, num)``return` `tab``end``return` `_M
local _M = dofile("BBB.lua")``print(#_M.createTab(1))--1``print(#_M.createTab(2)) --1``print(#_M.createTab(3)) --1
变量局部化
本地变量访问快于全局变量
本地变量非常快,因为它们驻留在虚拟机寄存器中,并且直接由索引访问。另一方面,全局变量位于一个lua表中,因此可以通过哈希查找来访问。但是索引访问总是比哈希查找快。
如下:反复运行发现,大多情况下第二种情况比第一种情况快10-15%左右(以下结果是在luajit运行的情况下)
local socket = require "socket"``local beginTime = socket.gettime()``for i = 1, 100000000, 1 do``math.min(100,2)``end``print("cost:", tostring(socket.gettime() - beginTime))--较慢 0.038616895675659 s``beginTime = socket.gettime()``local min = math.min --将math.min函数声明为本地变量``for i = 1, 100000000, 1 do``min(100,2)``end``print("cost:", tostring(socket.gettime() - beginTime)) --较快 0.032744169235229
数组预分配
为了减少rehash操作,当构造一个数组时,如果预先知道其大小,可以预分配数组大小,尽量减少rehash动态扩容带来的性能影响。在脚本层可以使用local t = {nil,nil,nil}来预分配数组大小(以下结果是在luajit运行的情况下)
local socket = require "socket"``local beginTime = socket.gettime()``--NO``for i = 1, 10000000, 1 do``local t = {}``t[1] = 1``t[2] = 2``t[3] = 3``end``print("cost:", tostring(socket.gettime() - beginTime)) --cost: 1.6545231342316``beginTime = socket.gettime()``--YES``for i = 1, 10000000, 1 do``local t = { nil, nil, nil }``t[1] = 1``t[2] = 2``t[3] = 3``--或者直接声明式赋值 local t = { 1, 2, 3}``end`` ``print("cost:", tostring(socket.gettime() - beginTime)) --cost: 0.0040500164031
以下代码在luajit运行情况下对比耗时效果不明显,但是在原生lua运行下,耗时相差一倍多
local socket = require "socket"``local beginTime = socket.gettime()``--NO``for i = 1, 10000000, 1 do``local t = {}``t['e'] = 1;``t['s'] = 2;``t['m'] = 3``end``print("cost:", tostring(socket.gettime() - beginTime)) --cost: 4.9161558151245``beginTime = socket.gettime()``--YES``for i = 1, 10000000, 1 do``local t = { e = 1, s = 2, m = 3 }``end``print("cost:", tostring(socket.gettime() - beginTime)) --cost: 2.2480080127716
禁止在热代码路径上拼接字符串
多个字符串连接时使用table.concat代替..的字符串连接。table.concat只会创建一块buffer,然后在此拼接所有的字符串,实际上是在用table模拟buffer。而..则每次拼接都会产生一串新的字符串,开辟一块新的buffer。
以下案例结果差异非常大,无论是内存还是时间,主要原因是:Lua中字符串的拼接都是新创建一个新的字符串,有新创建一块内存、copy字符串的动作,时间、空间上消耗都比较大
local socket = require "socket"``local beginTime = socket.gettime()``--NO``local s = ""``for i = 1, 100000, 1 do``s = s .. "a"``end` `print("cost:", tostring(socket.gettime() - beginTime)) --0.73219180107117 s``beginTime = socket.gettime()``--YES``local t = {}``for i = 1, 100000, 1 do``t[i] = "a"``end``local s = table.concat(t, "")``print("cost:", tostring(socket.gettime() - beginTime)) --0.0025370121002197 s
不要在for循环中加载模块(require等),加载一次即可
以下两种案例相差上百倍,主要是for循环里频繁访问全局函数(require),且多次触发require自身的模块加载实现
local socket = require "socket"``local beginTime = socket.gettime()``--NO``for i = 1, 10000000, 1 do``require("moduleB").test()``end``print("cost:", tostring(socket.gettime() - beginTime)) --cost: 0.7703239917755``beginTime = socket.gettime()``--YES``local moduleB = require "moduleB"``for i = 1, 10000000, 1 do``moduleB.test()``end``print("cost:", tostring(socket.gettime() - beginTime)) --cost: 0.0034441947937
moduleB.lua
local _M = {}``function _M.test()``end` `return _M
五:如何在本地调试
通过下载本地调试工具以便在开发阶段在本地使用真实设备进行物模型调试:
物模型工具:https://yunpan.midea.com/#/sharelist?sharekey=QmDhoDuFIp
六:如何复用插件lua
开发者只需要写 Thing和Convert代码,Mcu层代码在应用包发布阶段系统会自动拉取插件lua,达到物模型Mcu层完全复用插件lua的目的。
七:获取物模型实例 & 互斥关系
获取物模型实例 / 互斥关系只限于在包管理模式中使用
如非必要,禁止引入!!会增加lua运行时内存
八:设备联动中台接口
接口名称:查询设备联动配置
接口路径:
GET/v1/thing/linkage/{masterApplianceCode}
Mock地址:
https://yapi.smartmidea.net/mock/225/v1/thing/linkage/{masterApplianceCode}
请求参数
路径参数:
参数名称 | 示例 | 备注 |
---|---|---|
masterApplianceCode |
Query:
参数名称 | 是否必须 | 示例 | 备注 |
---|---|---|---|
requestId | 是 |
返回数据
名称 | 类型 | 是否必须 | 默认值 | 备注 | 其他信息 |
---|---|---|---|---|---|
requestId | string | 必须 | |||
code | string | 必须 | |||
data | object [] | 必须 | item 类型: object | ||
id | number | 必须 | 数据id | ||
masterSpid | number | 必须 | 主设备spid | ||
masterApplianceCode | number | 必须 | 主设备code | ||
interNum | null | 非必须 | 内部编号,设备端可能只存编号 | ||
slaveSpid | number | 必须 | 从设备spid | ||
slaveApplianceCode | number | 必须 | 从设备code |
接口名称:新增设备联动配置
接口路径:
POST/v1/thing/linkage
Mock地址:
https://yapi.smartmidea.net/mock/225/v1/thing/linkage
请求参数
Headers:
参数名称 | 参数值 | 是否必须 | 示例 | 备注 |
---|---|---|---|---|
Content-Type | application/json | 是 | ||
c4auid | string | 是 | 用户id |
Query:
参数名称 | 是否必须 | 示例 | 备注 |
---|---|---|---|
requestId | 是 |
Body:
名称 | 类型 | 是否必须 | 默认值 | 备注 | 其他信息 |
---|---|---|---|---|---|
masterApplianceCode | number | 必须 | 主设备code | ||
slaveApplianceCode | number | 必须 | 从设备code | ||
interNum | integer | 非必须 | 内部编号,设备端可能只存编号 |
返回数据
名称 | 类型 | 是否必须 | 默认值 | 备注 | 其他信息 |
---|---|---|---|---|---|
requestId | string | 必须 | |||
code | string | 必须 | |||
data | boolean | 必须 |
接口名称:修改设备联动配置
接口路径:
PUT/v1/thing/linkage/{id}
Mock地址:
https://yapi.smartmidea.net/mock/225/v1/thing/linkage/{id}
请求参数
路径参数:
参数名称 | 示例 | 备注 |
---|---|---|
id |
Headers:
参数名称 | 参数值 | 是否必须 | 示例 | 备注 |
---|---|---|---|---|
Content-Type | application/json | 是 | ||
c4auid | string | 是 | 用户id |
Query:
参数名称 | 是否必须 | 示例 | 备注 |
---|---|---|---|
requestId | 是 |
Body:
名称 | 类型 | 是否必须 | 默认值 | 备注 | 其他信息 |
---|---|---|---|---|---|
masterApplianceCode | number | 必须 | 主设备code | ||
interNum | integer | 非必须 | 内部编号,设备端可能只存编号 | ||
slaveApplianceCode | number | 必须 | 从设备code |
返回数据
名称 | 类型 | 是否必须 | 默认值 | 备注 | 其他信息 |
---|---|---|---|---|---|
requestId | string | 非必须 | |||
code | string | 非必须 | |||
data | boolean | 非必须 |
接口名称:删除设备联动配置
接口路径:
DELETE/v1/thing/linkage/{id}
Mock地址:
https://yapi.smartmidea.net/mock/225/v1/thing/linkage/{id}
请求参数
路径参数:
参数名称 | 示例 | 备注 |
---|---|---|
id |
Headers:
参数名称 | 参数值 | 是否必须 | 示例 | 备注 |
---|---|---|---|---|
Content-Type | application/x-www-form-urlencoded | 是 |
Query:
参数名称 | 是否必须 | 示例 | 备注 |
---|---|---|---|
requestId | 是 |
返回数据
名称 | 类型 | 是否必须 | 默认值 | 备注 | 其他信息 |
---|---|---|---|---|---|
requestId | string | 非必须 | |||
code | string | 非必须 | |||
data | boolean | 非必须 |
九:自定义业务异常码和错误信息
引入依赖公共库:
local businessException = require "BusinessException"
在lua中使用:
function _M:getAllProperties(sn, applianceCode, spid)
if (not sn) then
return businessException.new(44300, "SN不能为空") --注意需要主动return
end
local allProperties = self:getProperty(sn, applianceCode, spid, nil)
local params = self:allPropertiesOffset()
if (params == nil) then
return allProperties
end
local offsetProperties = self:getProperty(sn, applianceCode, spid, params)
return jsonMerge(offsetProperties, allProperties)
end
注意:Lua中开发者指定的错误码范围在 44300~44999 之间,不在范围内则按照最小/最大值返回
如果函数调用层级太深,不想一层层返回,可以使用 :
if (not sn) then
businessException.throw(44300,"xxxxxxxxxxxxxxxxx")
end
最终的错误码和错误信息,会在物模型北向接口返回,格式如
{
"code": "44300",
"msg": "xxxxxxxxxxxxxxxxx",
"requestId": "t1112223334447"
}