通话功能(volte)
一、VoLTE 通话功能概述
Air201 模组的 4G 通信功能,通过 VoLTE 技术实现高清语音通话,支持音频编解码、硅麦输入和喇叭输出。在通话过程中,声音信号通过 MIC 捕捉并转换为数字音频数据,经 4G 网络实时传输至对方设备。这一应用广泛适用于物联网设备中的远程通信、语音交互等场景,为用户提供便捷、高效的通话服务。
二、演示功能概述
本文通过 Air201+ 喇叭 + 扩展板来演示 VoLTE 通话功能。
三、准备硬件环境
3.1 Air201 模组
使用 Air201 开发套件,如下图所示:
淘宝购买链接:Air201 开发套件淘宝购买链接 ;
此开发套件的详细使用说明参考:Air201 产品手册 中的 Air201 硬件手册 和 Air201 的 LuatOS 快速入门。
3.2 SIM 卡
请准备一张可正常上网且可以通话的 SIM 卡,该卡可以是物联网卡或您的个人手机卡。
特别提醒:请确保 SIM 卡未欠费且网络功能正常,以便顺利进行后续操作。
3.3 PC 电脑
WIN10 以及以上版本的 WINDOWS 系统。
3.4 数据通信线
USB 数据线(其一端为 Type-C 接口,用于连接 Air201)。
3.5 BTB 扩展板
3.6 喇叭
最大支持 8Ω 1.2W 功率喇叭(默认)或者 4Ω 2.5W 功率喇叭
四、准备软件环境
4.1 下载调试工具
使用说明参考:Luatools 下载和详细使用
4.2 源码及固件
1. 需要使用支持语音通话的固件,已打包放置下面的 volte.zip 压缩包中。
2. 本教程使用的 demo:https://gitee.com/openLuat/LuatOS-Air201/tree/master/demo/cc
3. 源码和固件已打包,如下所示:
注:压缩包中 core 文件夹存放固件,code 文件夹存放 demo
五、软硬件资料
5.1 API 接口介绍
本教程使用 api 接口为:https://docs.openluat.com/air201/luatos/api/core/cc/
5.2 Air201 烧录说明
将 Air201 通过 usb-boot 小板连接电脑,如下图所示:
注意:boot 小板和 Air201 连接时,要确保 RESET 按键,BOOT 按键,电源开关机键 三个按键在同一侧,否则无法进入 boot 下载模式。
如何判断有没有进入下载模式:可以通过 PC 端的设备管理器中虚拟出来的 USB 端口数量来判断。
正常开机模式:
下载模式:
5.3 硬件安装与说明
5.3.1 实物连接图
Air201 通过 FPC 线连接 BTB 扩展板,使用扩展板上的按键,接线如下所示:
六、代码示例介绍
6.1 代码介绍
6.1.1 初始化驱动 es8311
Air201 板子自带了 es8311 音频编解码芯片(audio codec),所以硬件配置参数是固定的。
1. es8311 使用了 I2C0,电源脚为 GPIO2,pa 控制脚为 GPIO23
local es8311i2cId = 0 -- I2CID
local es8311PowerPin = 2 -- ES8311电源控制引脚
local paPin = 23 -- PA放大器控制引脚
mcu.altfun(mcu.I2C, es8311i2cId, 13, 2, 0)
mcu.altfun(mcu.I2C, es8311i2cId, 14, 2, 0)
i2c.setup(es8311i2cId, i2c.FAST)
gpio.setup(2, 1)
sys.wait(20)
if i2c.send(0, 0x18, 0xfd) == true then
log.info("音频小板或内置ES8311", "codec on i2c0")
i2c_id = 0
find_es8311 = true
end
if not find_es8311 then
while true do
log.info("not find es8311")
sys.wait(1000)
end
end
i2s.setup(0, 0, 16000, 16, i2s.MONO_R, i2s.MODE_LSB, 16)
audio.config(0, paPin, 1, 3, 100, es8311PowerPin, 1, 100)
audio.setBus(0, audio.BUS_I2S, {
chip = "es8311",
i2cid = es8311i2cId,
i2sid = 0,
voltage = audio.VOLTAGE_1800
}) -- 通道0的硬件输出通道设置为I2S
audio.vol(0, 80) -- 喇叭输出音量
audio.micVol(0, 80) -- mic输入音量
6.1.2 订阅通话状态
通过 sys.subscribe
函数订阅了一个名为 CC_IND
的事件。当这个事件被触发时,会调用后面的匿名函数,传入一个参数 state
,表示当前的通话状态。
sys.subscribe("CC_IND", function(state)
log.info("cc state", state)
if state == "READY" then -- 通话准备就绪
sys.publish("CC_READY") -- 发布"CC_READY"消息
sysplus.sendMsg(taskName, "CC_STATE", state)
elseif state == "INCOMINGCALL" then
log.info("cc.lastNum", cc.lastNum())
sysplus.sendMsg(taskName, "CC_STATE", state)
-- 当状态为 "HANGUP_CALL_DONE"(通话结束)、"MAKE_CALL_FAILED"(拨打失败)或 "DISCONNECTED"(通话断开)时,会将音频模块设置为待机状态,调用 audio.pm(0, audio.STANDBY)。
elseif state == "HANGUP_CALL_DONE" or state == "MAKE_CALL_FAILED" or state == "DISCONNECTED" then
sysplus.sendMsg(taskName, "CC_STATE", "CC_DONE")
audio.pm(0,audio.STANDBY)
-- audio.pm(0,audio.SHUTDOWN) --低功耗可以选择SHUTDOWN或者POWEROFF,如果codec无法断电用SHUTDOWN
elseif state == "CONNECTED" then
sysplus.sendMsg(taskName, "CC_STATE", "CONNECTED")
end
end)
6.1.3 注册音频事件回调函数
--[[
CC_STATE 通话状态
PLAY_DONE 音频播放结束
CC_READY 通话功能准备OK
CC_DONE 无论何种原因的通话结束
INCOMINGCALL 来电
CONNECTED 通话建立
CALL_KEY 按键1 拨打
HANGUP_KEY 按键2 挂断
]]
audio.on(0, function(id, event)
-- 使用play来播放文件时只有播放完成回调
local succ, stop, file_cnt = audio.getError(0)
if not succ then
if stop then
log.info("用户停止播放")
else
log.info("第", file_cnt, "个文件解码失败")
end
end
sysplus.sendMsg(taskName, "CC_STATE", "PLAY_DONE")
end)
6.1.4 通讯录列表
local list = {{
name = "测试电话1",
num = "xxx"
}, {
name = "测试电话2",
num = "xxx"
}}
6.1.5 通话管理状态机
状态定义:
1. IDLE(空闲状态)
(1) 描述:系统处于等待状态,准备接收来电或拨号。
(2) 转移条件:
收到INCOMINGCALL事件,进入准备通话状态PREPARE。
收到CALL_KEY事件,进入等待呼叫状态WAIT_CALLING。
2. WAIT_CALLING(等待呼叫状态)
(1) 描述:系统在等待后续的拨号操作。
(2) 转移条件:
超时(4000 毫秒),默认拨打选择的联系人,进入拨号状态CALLING。
收到CALL_KEY事件,切换到下一个联系人。
收到HANGUP_KEY事件,进入挂断流程DISCONNECTING。
收到PLAY_DONE事件,进行拨打。
3. CALLING(拨号中状态)
(1) 描述:正在拨打电话。
(2) 转移条件:
收到CONNECTED事件,进入通话中状态CONNECTING。
收到CC_DONE或HANGUP_KEY事件,进入挂断流程DISCONNECTING。
4. PREPARE(准备通话状态)
(1) 描述:准备接听来电或拨打电话。
(2) 转移条件:
收到CALL_KEY事件,接听来电,进入通话中状态CONNECTING。
收到HANGUP_KEY事件,进入挂断流程DISCONNECTING。
收到PLAY_DONE事件,循环播放提示。
收到CC_DONE,返回空闲状态IDLE。
5. CONNECTING(通话中状态)
(1) 描述:准备接听来电或拨打电话。
(2) 转移条件:
收到CALL_KEY事件,接听来电,进入通话中状态CONNECTING。
收到HANGUP_KEY事件,进入挂断流程DISCONNECTING。
收到PLAY_DONE事件,循环播放提示。
收到CC_DONE,返回空闲状态IDLE。
6. DISCONNECTING(挂断流程状态)
(1) 描述:处理挂断电话的流程。
(2) 转移条件:
执行挂断操作后,等待确认消息。
超时或收到CC_DONE,返回空闲状态IDLE。
local mode, index, ret = "IDLE", 0, nil --默认空闲状态
local ttsDone = true
local isSelect = false
while true do
if mode == "IDLE" then
index = 1
ret = sysplus.waitMsg(taskName, nil, nil)
log.info("IDLE", ret[1], ret[2])
if ret[2] == "INCOMINGCALL" then -- 来电
local num = cc.lastNum() -- 获取来电号码
log.info("来电", num)
index = findVaildNum(list, num) -- 如果号码有效,则进入准备通话状态,无效则挂断
if index then
mode = "PREPARE"
local result = audio.tts(0, list[index].name .. "电话")
log.info("tts result", result)
else
mode = "DISCONNECTING"
end
elseif ret[2] == "CALL_KEY" then -- 主动拨号
mode = "WAIT_CALLING"
elseif ret[2] == "PLAY_DONE" then
ttsDone = true
end
elseif mode == "WAIT_CALLING" then -- 等待呼叫
ret = sysplus.waitMsg(taskName, nil, 4000)
if not ret then -- 等待超时,拨打当前选中的联系人,默认为第一个
if not isSelect then
isSelect = true
audio.tts(0, "正在拨打" .. string.fromHex(list[index].name) .. "的电话")
end
log.info("WAIT_CALLING timeout, dial", list[index].name, list[index].num)
cc.dial(0, list[index].num)
mode = "CALLING"
elseif ret[2] == "CALL_KEY" then -- 按下拨号键,则选择下一个联系人
if ttsDone then
index = index + 1
if index > #list then
index = 1
end
ttsDone = false
audio.playStop(0)
local result = audio.tts(0, "即将呼叫" .. string.fromHex(list[index].name))
log.info("即将呼叫", list[index].name, list[index].num)
end
elseif ret[2] == "HANGUP_KEY" then -- 按下挂断键,挂断电话
audio.playStop(0)
mode = "DISCONNECTING"
elseif ret[2] == "PLAY_DONE" then
ttsDone = true
if isSelect then
isSelect = false
mode = "CALLING"
cc.dial(0, list[index].num)
end
elseif ret[2] == "CC_DONE" then -- 通话结束,回到IDLE
audio.playStop(0)
mode = "IDLE"
end
elseif mode == "CALLING" then -- 拨号中
ret = sysplus.waitMsg(taskName, nil, nil)
if ret[2] == "CC_DONE" or ret[2] == "HANGUP_KEY" then -- 通话结束或挂断键按下,走到挂断流程
mode = "DISCONNECTING"
elseif ret[2] == "CONNECTED" then -- 收到通话建立消息,则进入通话中状态
mode = "CONNECTING"
end
elseif mode == "PREPARE" then -- 准备通话
ret = sysplus.waitMsg(taskName, nil, nil)
log.info("PREPARE", ret[1], ret[2])
if ret[2] == "CALL_KEY" then -- 按下拨号键,接听,则进入通话中状态
audio.playStop(0)
cc.accept(0)
mode = "CONNECTING"
elseif ret[2] == "HANGUP_KEY" then -- 按下挂断键,走到挂断流程
audio.playStop(0)
mode = "DISCONNECTING"
elseif ret[2] == "PLAY_DONE" then -- 播放完毕,循环播放
ttsDone = true
local result = audio.tts(0, string.fromHex(list[index].name .. "电话"))
log.info("tts result", result)
elseif ret[2] == "CC_DONE" then -- 通话结束,回到IDLE
audio.playStop(0)
mode = "IDLE"
end
elseif mode == "CONNECTING" then -- 通话中
ret = sysplus.waitMsg(taskName, nil, nil)
log.info("CONNECTING", ret[1], ret[2])
if ret[2] == "HANGUP_KEY" then -- 按下挂断键,走到挂断流程
mode = "DISCONNECTING"
elseif ret[2] == "CC_DONE" then -- 通话结束,回到IDLE
mode = "IDLE"
end
elseif mode == "DISCONNECTING" then -- 挂断流程
cc.hangUp(0) -- 挂断电话
ret = sysplus.waitMsg(taskName, nil, 10000)
log.info("DISCONNECTING", ret[1], ret[2])
if not ret or ret[2] == "CC_DONE" then -- 通话结束或者超时没有等到挂断结束的消息,回到IDLE
mode = "IDLE"
elseif ret[2] == "PLAY_DONE" then
ttsDone = true
end
end
end
6.1.6 呼叫,挂断按键配置
-- 按键1 呼叫
gpio.debounce(28, 1000)
gpio.setup(28, function()
log.info("callme", "拨号键触发, 拨打电话")
sysplus.sendMsg(taskName, "CC_STATE", "CALL_KEY")
end, gpio.PULLUP)
-- 按键2 挂断
gpio.debounce(18, 1000)
gpio.setup(18, function()
log.info("callme", "挂断键触发, 挂断电话")
sysplus.sendMsg(taskName, "CC_STATE", "HANGUP_KEY")
end, gpio.PULLUP)
6.2 运行结果展示
1. 模组主动拨打电话
空闲状态下按下按键 1,默认拨打通讯录第一位联系人,日志打印显示如下:
2. 模组主动挂断电话
通话状态下按下按键 2,挂断电话,日志打印显示如下:
七、总结
CC 库的通话管理 API 接口共同构成了通话控制的核心功能,使开发者能够高效地管理通话的启动、挂断、接听、参数配置以及附加的通话处理功能。通过合理利用这些接口,开发者可以构建出具备出色通话体验的应用程序,满足用户在多种通话场景下的需求。同时,也需关注接口之间的协同配合,以确保通话功能的流畅性和可靠性。
常见问题
1. 打不了电话,确认能不能正常注册上网络,有没有欠费。确认卡是否开通 VOLTE 功能, 只有开通 VOLTE 功能才能进行语音通话。固件是否支持 VOLTE 功能,固件需要支持 VOLTE 功能。注:建议使用手机卡测试,普通物联网卡可能不支持 VOLTE 功能。