跳转至

通话功能(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 功能。