本教程目的:介绍儿童手表功能的代码实现,主要是UI显示,触屏消息处理,通过MQTT来实现手表和APP数据交互。
手表包含功能
1.待机模拟时钟。
2.待机界面,包含时间日期,网络信号和电池电量等显示。
3.拨号功能,可以按拨号键拨出电话。也可以通过成长线索APP设置关闭此功能。
4.电话本,通过绑定手表来添加管理员。管理员可以通过APP添加号码到电话本。可以通过电话本,直接拨出电话。
5.微聊功能,按住说话可以发送微聊到APP,也可以接受微聊发送过来的语音。
6.相机功能,可以拍照片发送到APP。
7.工具箱,包含相册,秒表,主题,音量,屏幕亮度。
8.小游戏,包含数学,英语,语文等小游戏。
9.设备信息,包含APP下载,注册码,微信看位置。
10.手电筒,可以打开用来照明。
APP包含功能
1.定位,监听,课程表,留言。
2.寻找手表,远程关机,远程拍照,智能提醒和各种设置等。
手表图片
APP截图
代码结构截图
代码架构介绍
1.audio文件夹,存放需要的音频文件。如:开关机,来电铃音。
2.font文件夹,存放用来显示待机界面时间和秒表的大字体。
3.image文件夹,存放需要用来显示的图片资源。如:开关机动画。
4.lib文件夹,存放脚本的库文件。如:sys.lua,mqtt.lua。
5.src文件夹,存放一些通用,协议相关的脚本。如:ui.lua,display.lua,linkair.lua,protoair.lua。
6.tool文件夹,存放生成模拟时钟的python脚本。
7.windows文件夹,存放各个功能模块的界面处理脚本。如:idle.lua
8.main.lua脚本,入口脚本。会require需要的各个功能模块。
代码说明
1.模拟时钟实现 clock.lua
通过入口函数enter(clock())进入模拟时钟界面。
function clock() 显示和按键和触摸消息的处理都是通过此函数实现的。
ui.window 创建窗口对象。
return ui.window {
lockscreen = true,
enter = function()
ui.onkey(ui.KEY_POWER, ui.KEY_DOWN, function() end)
ui.onkey(ui.KEY_POWER, ui.KEY_UP, lcd.off) -- 锁屏界面下按关机键灭屏
end,
draw = function()
disp.setactivelayer(2)
disp.clear()
disp.putimage('clock.png', 5, 5)
disp.setlayerstartpos(2, VSCREEN_X, VSCREEN_Y)
disp.setactivelayer(0)
disp.setbkcolor(BLACK)
disp.clear()
disp.setlayerstartpos(0, VSCREEN_X, VSCREEN_Y)
disp.setlayershow(true, false, true)
redraw()
end,
penup = function()
ui.goback()
end,
}
enter = function() 进入窗口,通过ui.onkey(ui.KEY_POWER, ui.KEY_UP, lcd.off)注册按键处理函数。
draw = function() 绘制窗口界面,主要用到disp相关接口进行图层设置和画图片,写文字。如:disp.putimage('clock.png', 5, 5)。会从屏的x=5,y=5的开始显示clock.png对应的时钟表盘图片。
penup = function() 触屏抬起消息处理。
timer = ui.timer { interval = 1000, callback = redraw }
定时器处理1S钟会刷新一次界面。
local function redraw() 根据当前时间算出对应的坐标,画出对应的时,分,秒对应的指针图片。
2.待机实现 idle.lua
通过ui.enter(idle())进入待机窗口
ui.window 通过这个创建窗口对象
enter = function() 进入窗口后的按键注册消息注册处理。
如:ui.onkey(ui.KEY_POWER, ui.KEY_UP, lcd.off) -- 待机界面下按关机键灭屏
sys.subscribe('BATT_VOLT_IND', drawbatt) --电池上报的消息注册函数,有电量变化是会调用drawbatt函数去画电池电量显示图标。
losefocus = function() 窗口失去焦点去处理的函数
如:sys.unsubscribe('BATT_VOLT_IND', drawbatt) --注销电池电量上报消息。
draw = redraw 窗口绘制函数
pendown = function() 窗口触屏按下消息处理
penup = function(x, y) 窗口触屏抬起消息处理
penmove = function() 窗口触屏移动消息处理
penup = function(x, y)
if moved then
ui.enter(mainmenu())
return
end
end,
待机界面移动抬起的情况下会进入主菜单界面。
local function drawDateTime() 时间绘制函数
local function drawSignal() 信号强度绘制函数
local function drawbatt(lvl) 电池电量绘制函数
local function drawIPStatus() IP状态绘制函数
local function drawChatStatus() 微聊状态绘制函数
local function drawAlarmStatus() 智能提醒状态绘制函数
3.主实现 mainmenu.lua
local items = {
'dial', 'contact', 'wechat', 'camera', 'toolbox', 'minigame','deviceinfo', 'flashlight'
}
items主菜单对应的菜单项:拨号,电话本,微聊,相机,工具箱,小游戏,设备信息,手电筒。
ui.window 窗体的创建也是通过这个创建的。
ui.enter(_Gitems[highlight + 1]) 通过这进入不同的主菜单界面
local highlight = 0 -- 高亮菜单项
local delta = offset > 0 and 1 or -1 -- 图层偏移方向 往右滑动为正与触屏坐标增加方向一致
highlight = (highlight - delta) % #items 根据移动方向算出当前高亮菜单项
local function reload(right)
disp.layerbuffermove(layer, right and 0 or 1)
-- 预加载数据到后一屏缓冲中
putimage(right and 0 or 2)
end
reload(delta == 1) -- 右为正 左为负 与图层偏移方向一致
4.拨号实现 call.lua
注册 CALL_INCOMING 来的消息
sys.subscribe('CALL_INCOMING', function(number)
if not manage.isContact(number) then cc.hangup(number) return end -- 非电话本联系人来电自动拒接
ui.enter(call(number, cc.INCOMING))
end)
manage.isContact(number) 判断是否为电话本联系人来电
cc.hangup(number) 非电话本来的自动拒接
ui.enter(call(number, cc.INCOMING)) 是电话本来进入来电界面
sys.subscribe('CALL_CONNECTED', onConnect) 注册电话接通处理函数
sys.subscribe('CALL_DISCONNECTED', onDisconnect) 注册电话断开处理函数
窗口函数和之前一样就不做说明了。
下面的主菜单的窗体的创建和窗口函数处理都一样,就不一一做介绍了。
5.mqtt实现 linkair.lua
sys.taskInit(
function()
while true do
while not socket.isReady() do sys.waitUntil('IP_READY_IND') end
local imei = misc.getimei()
local mqttc = mqtt.client(imei,600,imei,enPwd(imei))
--阻塞执行MQTT CONNECT动作,直至成功
while not mqttc:connect(nvm.get("addr"),nvm.get("port"),nvm.get("prot")) do
sys.wait(2000)
end
ready = true
--订阅主题
if mqttc:subscribe(
{
[linkout.enTopic("devparareq/+")]=1,
[linkout.enTopic("deveventreq/+")]=1,
[linkout.enTopic("set")]=1,
[linkout.enTopic("updpbreq/+")]=1,
[linkout.enTopic2("+",misc.getimei(),"single/stod/+")]=1,
[linkout.enTopic2("+",misc.getimei(),"group/+/stod/+")]=1
}
) then
linkout.init()
while true do
if not linkin.procMsg(mqttc) then log.error("linkin.procMsg error") break end
if not linkout.procMsg(mqttc) then log.error("linkout.procMsg error") break end
coroutine.resume(co_monitor, 'feed monitor')
end
linkout.unInit()
end
ready = false
--断开MQTT连接
mqttc:disconnect()
end
end
)
通过创建一个TASK来实践MQTT的连接,订阅,接收和发送消息
local mqttc = mqtt.client(imei,600,imei,enPwd(imei)) MQTT的创建
mqttc:connect(nvm.get("addr"),nvm.get("port"),nvm.get("prot")) 连接MQTT
if mqttc:subscribe(
{
[linkout.enTopic("devparareq/+")]=1,
[linkout.enTopic("deveventreq/+")]=1,
[linkout.enTopic("set")]=1,
[linkout.enTopic("updpbreq/+")]=1,
[linkout.enTopic2("+",misc.getimei(),"single/stod/+")]=1,
[linkout.enTopic2("+",misc.getimei(),"group/+/stod/+")]=1
}
MQTT主题订阅
linkout.procMsg(mqttc) 通过mqttc:publish 发送MQTT消息
linkin.procMsg(mqttc) 通过mqttc:receive(2000) 来接收MQTT发送的消息
6.微聊实现 wechat.lua
微聊主要是通过录音来实现的,介绍下用到的接口。
record.start 开始录音
record.stop() 停止录音
record.isBusy() 录音中
sys.publish("SND_NEW_CHAT_REQ", contacts[1].phone[1]) 录音完成通过publish "SND_NEW_CHAT_REQ" 来发送录音到服务器
sys.subscribe("SND_NEW_CHAT_CNF", function(result)
recordSending = false
ui.popup(result and '微聊发送成功' or '微聊发送失败')
end)
注册微聊发送结果消息"SND_NEW_CHAT_CNF" 做对应的提示
微聊消息接收处理函数
function rcvrcd(ctyp, cid, mid, tm, seq, total, cur, typ, tmlen, dat)
log.info("manage.rcvrcd", collectgarbage("count"), cid, mid, seq, total, cur, typ)
collectgarbage()
if mid == misc.getimei() then return end
local pth, name = CHAT_DIR .. "/" .. mid:sub(-10,-1) .. "_" .. tm .. "." .. (typ == 0 and "amr" or (typ == 1 and "mp3" or "wav"))
name = split.merge(cid .. mid .. seq, pth, total, cur, dat)
if name then
savercd(cid, mid, tmlen, name, true)
--cid表示联系人的id(单聊时为联系人的号码,群聊时为群组的id)
sys.publish("RCV_NEW_CHAT_IND", cid)
end
collectgarbage()
end
收到微聊数据后会保存为.amr格式文件。
用audio.play()播放收到微聊。
7.相机实现 camera.lua
用到的相关接口
打开相机窗口的时候做下列动作
disp.cameraopen() 打开相机
disp.camerapreview(VSCREEN_X, VSCREEN_Y, 0, 0, 127, 127) 预览设置
退出相机窗口或失去焦点的时候做下列动作
disp.camerapreviewclose() 关闭预览
disp.cameraclose() 关闭相机
点相机图片拍照的时候做下列动作
disp.cameracapture(128, 128) 抓拍照片
disp.camerasavephoto('/ldata/PHOTO.jpg') 保存照片
disp.camerapreview(VSCREEN_X, VSCREEN_Y, 0, 0, 127, 127) 重新设置预览
8.ui实现 ui.lua
local windowmt = { __index = function() return empty end }
--- 创建窗口
-- @param 无
-- @return 窗口对象
-- @usage ui.window{draw=function() end}
function window(o)
setmetatable(o, windowmt)
return o
end
通过setmetatable设置元表来创建窗口
--- 进入新的窗口
-- @param w 新窗口对象ui.window
-- @return 无
-- @usage ui.enter(ui.window{draw=function() end})
function enter(w)
if w.popup == true or w.notice == true then lcd.turnon() end -- UI通知可能会在熄屏状态下弹出,因此需要打开背光
if #wstack ~= 0 then
if wstack[#wstack].popup == true then -- 进入新窗口时如果有popup自动清除掉
table.remove(wstack, #wstack)
elseif wstack[#wstack].notice == true and w.notice == true then -- 产生不同的通知窗口时自动清除旧的通知窗口
table.remove(wstack, #wstack).exit()
else
wstack[#wstack].losefocus()
end
end
table.insert(wstack, w)
__enter(w, true) -- 为新创建的窗口在调用enter回调时增加一个标记
draw()
end
local wstack = {} -- 窗口栈
先看 #wstack 是否为0,如不为0,根据窗口类型进行不同的处理
然后把当前窗口放入堆栈。
local function __enter(w, isNew)
在这个函数里会调用窗口对象的enter函数w.enter(isNew)
local function draw()
if reentries == 0 and #wstack ~= 0 then
-- 绘制新窗口总是设置为第一个图层显示且为黑底白字
disp.setlayershow(true, false, false)
disp.setactivelayer(0)
disp.setlayerstartpos(0, VSCREEN_X, VSCREEN_Y)
disp.setbkcolor(BLACK)
disp.setcolor(WHITE)
update()
end
end
draw()函数会设置图层参数,背景色和字体颜色。
function update()
if locked then return end
wstack[#wstack].draw()
end
update函数会调当前窗口对象的draw函数wstack[#wstack].draw()。
local pressedKey
rtos.init_module(rtos.MOD_KEYPAD, 0, 0x07, 0x07)
rtos.on(rtos.MSG_KEYPAD, function(msg)
if locked then return end
local key = msg.key_matrix_row * 256 + msg.key_matrix_col
log.debug('ui.KEYPAD', msg.key_matrix_row, msg.key_matrix_col, msg.pressed)
if msg.pressed then
if pressedKey == KEY_CALL and key == KEY_POWER then
if nvm.get('ft_result') ~= 'pass' then
ui.enter(functiontest())
end
end
pressedKey = key
keyWindow = wuid
sys.timer_start(onLongpress, 2000, key)
if not pm.isleep('lcd') then lcd.turnon('key') end
else
pressedKey = nil
sys.timer_stop(onLongpress, key)
lcd.turnoff('key')
if keyWindow ~= wuid then -- 不在一个界面下的完整按键流程不处理
return
end
end
handleKey(key, msg.pressed and KEY_DOWN or KEY_UP)
end)
按键处理rtos.init_module(rtos.MOD_KEYPAD, 0, 0x07, 0x07)
根据按键所在行列去初始化按键。
rtos.on(rtos.MSG_KEYPAD, function(msg))
按键消息回调函数注册。
-- 注册触屏消息
local touchEvent = { 'pendown', 'penmove', 'penup' }
local touchWindow
rtos.init_module(rtos.MOD_TP)
rtos.on(rtos.MSG_TP, function(msg)
if pm.isleep('lcd') or locked then return end -- 灭屏状态下不处理任何触屏消息
local event = touchEvent[msg.pen_state + 1]
log.debug('ui.KEYPAD', event, msg.x, msg.y)
local x, y = checkCoord(msg.x), checkCoord(msg.y)
if event == 'pendown' then
touchWindow = wuid
lcd.turnon('touch')
else
lcd.turnoff('touch')
if touchWindow ~= wuid then -- 如果是之前窗口发生的触摸消息,则忽略掉
return
end
end
if x == 100 and y == 140 then
handleKey(KEY_HOME, event == 'pendown' and KEY_DOWN or KEY_UP)
return
end
wstack[#wstack][event](x, y)
end)
触屏处理
local touchEvent = { 'pendown', 'penmove', 'penup' }
触屏消息类型。
rtos.init_module(rtos.MOD_TP) 初始化触屏模块。
rtos.on(rtos.MSG_TP, function(msg))注册触屏消息回调函数。
wstack[#wstack][event](x, y) 处理当前窗口对应触屏消息函数。
关于手表的源码介绍就先介绍这么多,其它的部分大家可以分析源码来学习。