跳转至

04 airtalk合宙对讲方案

作者:孟伟 | 最后修改:2026-04-15

一、概述

文档详细介绍了基于 Air780EHM/Air780EGH 核心板的对讲功能开发。通过本指南,开发者可以快速掌握 Air780EHM/Air780EGH 的对讲处理功能,实现一对一通话和一对多广播等应用场景。

合宙对讲方案,是基于 MQTT 的传输方案,通过语音流,数据流来进行通信和控制。

对讲采用 AMR 对数据进行上下行编码,得益于 AMR 的优良特性,传输消耗流量仅为 1.6KB/s, 这样一小时仅消耗 5.7MB 的流量

AirTalk 属于 LuatOS 核心库

AirTalk 分为三部分,云,管,端。其中:

  1. 云,即指服务器端,主要处理逻辑为 mqtt 消息转发
  2. 管,即管理平台,对终端进行增,删,改,查,对对讲流程进行控制
  3. 端,包含设备端(目前 Air8000 系列,Air780EHV,Air780EHM,Air780EGH 支持),网页端

二、演示功能概述

1、main.lua:主程序入口;

2、talk.lua:对讲核心业务模块

  • 音频硬件初始化和配置
  • 对讲功能的核心逻辑实现
  • 设备发现和联系人管理
  • 按键事件处理和状态控制
  • 网络连接管理和优化

三、准备硬件环境

1、Air780EHM 核心板/Air780EGH 核心板 +AirAUDIO_1010 音频配件板 + 喇叭

注意: 如果搭配AirAUDIO_1010 扩展板测试,需将AirAUDIO_1010 扩展板中PA开关拨到OFF,让软件控制PA,避免pop音

Air780EHM 核心板/Air780EGH 核心板和 AirAUDIO_1010 扩展板的硬件接线方式为:

Air780EHM 核心板/Air780EGH 核心板 AirAUDIO_1010 扩展板
26/I2S_MCLK I2S_MCLK
30/I2S_BCK I2S_BCK
31/I2S_LRCK I2S_LRCK
32/I2S_DIN I2S_DIN
33/I2S_DOUT I2S_DOUT
67/I2C1_SCL I2C_SCL
66/I2C1_SDA I2C_SDA
25/GPIO26 PA_EN
23/GPIO2 8311_EN
3V3 VCC
GND GND

2、TYPE-C USB 数据线一根

  • Air780EHM 核心板/Air780EGH 核心板通过 TYPE-C USB 口供电;
  • TYPE-C USB 数据线直接插到核心板的 TYPE-C USB 座子,另外一端连接电脑 USB 口;

四、准备软件环境

1、Luatools 下载调试工具

2、本demo开发测试时使用的固件为Air780EGH V2020 版本固件(请选择支持 airtalk 功能固件),所以你如果要测试本demo时,可以直接使用最新版本支持 airtalk 功能的内核固件;如果发现最新版本的内核固件测试有问题,可以使用我们开发本demo时使用的内核固件版本来对比测试;

3、 luatos 需要的脚本和资源文件

4、合宙 LuatIO 工具(GPIO 复用初始化配置)使用说明

5、 lib 脚本文件:使用 Luatools 烧录时,勾选 添加默认 lib 选项,使用默认 lib 脚本文件;

五、API 接口

extalk 扩展库

extalk 是 airtalk 的扩展库,简化了使用方法,扩展了部分应用,建议使用此库。

六、代码解析

1、main.lua:主程序入口;

--[[
@module  main
@summary LuatOS语音对讲应用主入口,负责加载功能模块
@version 1.0
@date    2025.11.26
@author 陈媛媛
@usage
demo演示的核心功能为
1audio_drv:音频设备初始化与控制
2talkairtalk 对讲业务逻辑处理。

更多说明参考本目录下的readme.md文件
]]

--[[
必须定义PROJECT和VERSION变量Luatools工具会用到这两个变量,远程升级功能也会用到这两个变量
PROJECT:项目名,ascii string类型
        可以随便定义,只要不使用,就行
VERSION:项目版本号,ascii string类型
        如果使用合宙iot.openluat.com进行远程升级,必须按照"XXX.YYY.ZZZ"三段格式定义:
            XYZ各表示1位数字,三个X表示的数字可以相同,也可以不同,同理三个Y和三个Z表示的数字也是可以相同,可以不同
            因为历史原因,YYY这三位数字必须存在,但是没有任何用处,可以一直写为000
        如果不使用合宙iot.openluat.com进行远程升级,根据自己项目的需求,自定义格式即可
]]

PROJECT = "extalk"
VERSION = "001.000.000"
--到 iot.openluat.com 创建项目,获取正确的项目key
PRODUCT_KEY =  "5544VIDOIHH9Nv8huYVyEIGT4tCvldxI"

--在日志中打印项目名和项目版本号
log.info("main", PROJECT, VERSION)

-- 如果内核固件支持wdt看门狗功能,此处对看门狗进行初始化和定时喂狗处理
-- 如果脚本程序死循环卡死,就会无法及时喂狗,最终会自动重启
if wdt then
    --配置喂狗超时时间为9秒钟
    wdt.init(9000)
    --启动一个循环定时器,每隔3秒钟喂一次狗
    sys.timerLoopStart(wdt.feed, 3000)
end

-- 如果内核固件支持errDump功能,此处进行配置,【强烈建议打开此处的注释】
-- 因为此功能模块可以记录并且上传脚本在运行过程中出现的语法错误或者其他自定义的错误信息,可以初步分析一些设备运行异常的问题
-- 以下代码是最基本的用法,更复杂的用法可以详细阅读API说明文档
-- 启动errDump日志存储并且上传功能,600秒上传一次
-- if errDump then
--     errDump.config(true, 600)
-- end

-- 使用LuatOS开发的任何一个项目,都强烈建议使用远程升级FOTA功能
-- 可以使用合宙的iot.openluat.com平台进行远程升级
-- 也可以使用客户自己搭建的平台进行远程升级
-- 远程升级的详细用法,可以参考fota的demo进行使用

require "audio_drv"        -- 音频驱动模块
require "talk"             -- 对讲主模块

-- 音频对内存影响较大,不断的打印内存,用于判断是否异常
sys.timerLoopStart(function()
    log.info("mem.lua", rtos.meminfo())
    log.info("mem.sys", rtos.meminfo("sys"))
 end, 3000)

-- 用户代码已结束---------------------------------------------
-- 结尾总是这一句
sys.run()
-- sys.run()之后后面不要加任何语句!!!!!

2、audio_drv.lua 音频设备管理模块,负责音频设备的初始化和控制

  • 定义所有硬件引脚常量
  • 使用 exaudio 扩展库初始化音频设备
--[[
@module  audio_drv
@summary 音频设备管理模块,负责音频设备的初始化和控制
@version 2.0
@date    2025.11.26
@author  陈媛媛
@usage
本模块提供以下功能:
1、定义所有硬件引脚常量
2、使用exaudio扩展库初始化音频设备
]]


local audio_drv = {}
local exaudio = require "exaudio"
local _initialized = false

-- 音频初始化参数
local audio_setup_param = {
    model = "es8311",       -- 音频编解码类型,可填入"es8311","es8211"
    i2c_id = 0,             -- i2c_id,可填入0,1 并使用pins工具配置对应的管脚
    pa_ctrl = gpio.AUDIOPA_EN,          -- 音频放大器电源控制管脚
    dac_ctrl = 20,         -- 音频编解码芯片电源控制管脚
}

-- 初始化音频设备
function audio_drv.init()
    if _initialized then
        log.info("audio_drv", "音频设备已经初始化")
        return true
    end

    log.info("audio_drv", "开始初始化音频设备")

    local audio_init_ok = exaudio.setup(audio_setup_param)

    if audio_init_ok then
        _initialized = true
        log.info("audio_drv", "音频设备初始化成功")
        return true
    else
        log.error("audio_drv", "音频设备初始化失败")
        return false
    end
end

return audio_drv

3、talk.lua: Airtalk 对讲业务核心模块

  • 支持广播对讲(一对多)和一对一对讲;
  • 自动设备发现和管理;
  • 按一次 Boot 键选择指定设备,开始 1 对 1 对讲,再按一次 Boot 键或 powerkey 键结束对讲;
  • 按一次 powerkey 键开始一对多广播,再按一次 Boot 键或 powerkey 键结束广播。
--[[
@module  talk
@summary Airtalk 对讲业务核心模块
@date    2025.11.26
@author 陈媛媛
@usage
本demo演示的核心功能为:
    1. 支持广播对讲(一对多)和一对一对讲;
    2. 自动设备发现和管理;
    3. 按一次Boot键选择指定设备,开始1对1对讲,再按一次Boot键或powerkey键结束对讲;
    4. 按一次powerkey键开始一对多广播,再按一次Boot键或powerkey键结束广播。

]]

local extalk = require "extalk"
local audio_drv = require "audio_drv"  -- 引入音频驱动模块

-- 配置日志格式
log.style(1)

-- 常量定义
local USER_TASK_NAME = "user_task"  -- 用户任务名称
local MSG_KEY_PRESS = 12            -- 按键消息类型

-- 目标设备终端ID,修改为你想要对讲的终端ID
TARGET_DEVICE_ID = "78122397"  -- 请替换为实际的目标设备终端ID


-- 全局状态变量
local g_dev_list = nil              -- 设备列表,存储所有可用对讲设备
local g_speech_active = false       -- 对讲状态标记,true表示正在对讲中

-- 联系人列表回调函数
-- 当设备列表更新时调用,维护当前可用的对讲设备
local function contact_list_callback(dev_list)
    g_dev_list = dev_list
    if dev_list and #dev_list > 0 then
        log.info("联系人列表更新:")
        for i = 1, #dev_list do
            log.info(string.format("  %d. ID: %s, 名称: %s",
                i, dev_list[i]["id"], dev_list[i]["name"] or "未知"))
        end
    else
        log.info("联系人列表为空")
    end
end

-- 对讲状态回调函数
-- 处理对讲状态变化事件
local function speech_state_callback(event_table)
    if not event_table then return end

    if event_table.state == extalk.START then
        -- extalk.START: 对讲开始(广播或一对一通话已开始)
        log.info("对讲开始")
        g_speech_active = true
    elseif event_table.state == extalk.STOP then
        -- extalk.STOP: 对讲结束(广播或一对一通话已结束)
        log.info("对讲结束")
        g_speech_active = false
    elseif event_table.state == extalk.UNRESPONSIVE then
        -- extalk.UNRESPONSIVE: 对端未响应(一对一呼叫时对方无应答)
        log.info("对端未响应")
        g_speech_active = false
    elseif event_table.state == extalk.ONE_ON_ONE then
        -- extalk.ONE_ON_ONE: 一对一呼叫建立(已连接到指定设备)
        g_speech_active = true
        local dev_name = "未知设备"
        if g_dev_list then
            for i = 1, #g_dev_list do
                if g_dev_list[i]["id"] == event_table.id then
                    dev_name = g_dev_list[i]["name"] or "未知设备"
                    break
                end
            end
        end
        log.info(string.format("%s 来电", dev_name))
    elseif event_table.state == extalk.BROADCAST then
        -- extalk.BROADCAST: 广播开始(已进入广播模式)
        g_speech_active = true
        local dev_name = "未知设备"
        if g_dev_list then
            for i = 1, #g_dev_list do
                if g_dev_list[i]["id"] == event_table.id then
                    dev_name = g_dev_list[i]["name"] or "未知设备"
                    break
                end
            end
        end
        log.info(string.format("%s 开始广播", dev_name))
    end

    log.info("当前对讲状态:", g_speech_active and "正在对讲" or "空闲")
end

-- extalk配置参数
local extalk_configs = {
    key = PRODUCT_KEY,           -- 产品密钥
    heart_break_time = 120,      -- 心跳间隔(单位秒)
    contact_list_cbfnc = contact_list_callback,  -- 联系人列表回调
    state_cbfnc = speech_state_callback,         -- 状态回调
}

-- Boot键回调函数
-- GPIO0按键,用于一对一對講控制
local function boot_key_callback()
    log.info("boot_key_callback")
    sys.sendMsg(USER_TASK_NAME, MSG_KEY_PRESS, false)  -- false表示Boot键
end

-- Power键回调函数
-- 电源按键,用于广播对讲控制
local function power_key_callback()
    log.info("power_key_callback")
    sys.sendMsg(USER_TASK_NAME, MSG_KEY_PRESS, true)   -- true表示Power键
end

-- 初始化按键
-- 配置Boot键和Power键的GPIO中断
local function init_buttons()
    -- 配置Boot键 (GPIO0),下拉电阻,上升沿触发
    gpio.setup(0, boot_key_callback, gpio.PULLDOWN, gpio.RISING)
    gpio.debounce(0, 200, 1)  -- 200ms去抖,防止按键抖动

    -- 配置Power键,上拉电阻,下降沿触发
    gpio.setup(gpio.PWR_KEY, power_key_callback, gpio.PULLUP, gpio.FALLING)
    gpio.debounce(gpio.PWR_KEY, 200, 1)  -- 200ms去抖,防止按键抖动
end

-- 查找目标设备
-- 根据配置的目标ID,不自动查找其他设备
local function find_target_device()
    -- 优先使用配置的目标ID
    if TARGET_DEVICE_ID and TARGET_DEVICE_ID ~= "" then
        return TARGET_DEVICE_ID
    end

    -- 没有配置目标ID,直接返回nil,不自动查找其他设备
    log.warn("未配置目标设备ID")
    return nil
end

-- 处理按键消息,在结束对讲时立即更新状态
local function handle_key_press(is_power_key)
    if g_speech_active then
        -- 当前正在对讲,按任何键都结束对讲
        log.info("结束当前对讲")
        extalk.stop()
        g_speech_active = false  -- 立即更新状态
    else
        -- 当前未在对讲,根据按键类型开始不同对讲
        if is_power_key then
            -- Power键:开始一对多广播
            log.info("开始一对多广播")
            extalk.start()  -- 不带参数表示广播
        else
            -- Boot键:开始一对一对讲
            log.info("开始一对一对讲")
            local remote_id = find_target_device()
            if remote_id then
                extalk.start(remote_id)
            else
                log.error("无法开始一对一对讲,没有找到可用设备")
            end
        end
    end
end

-- 用户主任务
-- 系统初始化主流程和对讲功能主循环
local function user_main_task()
    -- 初始化音频设备
    log.info("初始化音频...")
    if not audio_drv.init() then
        log.error("音频初始化失败")
        return
    end
    log.info("音频初始化成功")

    -- 初始化extalk对讲功能
    log.info("初始化extalk...")
    local extalk_init_ok = extalk.setup(extalk_configs)
    if not extalk_init_ok then
        log.error("extalk初始化失败")
        return
    end
    log.info("extalk初始化成功")

    log.info("对讲系统准备就绪")

    -- 主消息循环 - 等待和处理按键消息
    while true do
        local msg = sys.waitMsg(USER_TASK_NAME, MSG_KEY_PRESS)
        if msg and msg[1] == MSG_KEY_PRESS then
            handle_key_press(msg[2])  -- msg[2]区分Power键(true)和Boot键(false)
        end
    end
end

-- 系统初始化
-- 配置按键并启动用户主任务
local function init()
    init_buttons()
    -- 使用sys.taskInitEx创建支持waitMsg的任务
    sys.taskInitEx(user_main_task, USER_TASK_NAME)
end

-- 直接初始化,无需等待
init()

七、功能演示

1、搭建好硬件环境

2、创建群组:详情请见:Airtalk 第 5.2 章节--创建群组

3、main.lua 中,修改 PRODUCT_KEY 。

--到 iot.openluat.com 创建项目,获取正确的项目key
 PRODUCT_KEY =  "5544VIDOIHH9Nv8huYVyEIGT4tCvldxI"

4、talk.lua 中,修改目标设备终端 ID。

-- 目标设备终端ID,修改为你想要对讲的终端ID
  TARGET_DEVICE_ID = "78122397"  -- 请替换为实际的目标设备终端ID

5、Luatools 烧录内核固件和修改后的 demo 脚本代码

6、烧录成功后,自动开机运行

  • 初始化音频设备,配置 ES8311 编解码芯片和 PA 功放
  • 音频设备初始化成功,可正常录音和播放
  • 初始化 extalk 对讲功能
  • 设备信息显示,本机 IMEI:866965083769676,设备密钥:20250724030359A635078A5501877477
  • extalk 初始化成功
  • 对讲系统准备就绪等待用户操作
  • 连接到对讲管理平台,可进行对讲业务
  • 联系人列表更新,获取到可用设备列表,包括本机设备和目标设备

luatools 会打印以下日志

6、 点击 BOOT 按键,会选择指定终端 ID 的目标设备,进行一对一对讲,再按一次 Boot 键或 powerkey 键结束对讲。

  • 按下 Boot 键,启动一对一对讲流程
  • 向指定设备(终端 ID:78122397)发起对讲请求
  • 通过 MQTT 向服务器发送一对一对讲请求,包含音频通道信息
  • 进入一对一对讲模式
  • 对讲连接建立成功,开始语音传输
  • 系统状态更新为对讲中
  • 再次按 Boot 或 powerkey 键,主动结束对讲

luatools 会打印以下日志

7、 点击 POWERKEY 按键,会进行广播,所有群组内的人,都会收到对讲消息,再按一次 Boot 键或 powerkey 键结束广播。

  • 按下 Power 键,启动广播对讲
  • 通过 MQTT 向服务器发送广播请求,音频通道主题包含"all"标识
  • 对讲模式 1,进入广播对讲模式
  • 广播连接建立成功,开始向所有设备广播
  • 系统状态更新为对讲中
  • 再次按 Boot 或 powerkey 键,结束广播对讲

luatools 会打印以下日志

8、当其他设备或手机/PC 的 web 网页端对设备发起一对一对讲。

  • 收到其他设备的对讲呼叫请求,系统自动接听对讲(无需用户按键操作)
  • 进入一对一对讲模式
  • 通过 MQTT 通知服务器接听成功
  • 确认对讲开始,语音通道建立
  • 系统状态更新为对讲中
  • 对方结束对讲,本机对讲也随之结束

luatools 会打印以下日志

9、当其他设备或手机/PC 的 web 网页端对设备发起广播。

  • 收到其他设备的广播邀请
  • 系统自动加入广播(无需按键操作)
  • 对讲模式 2,进入被动接听广播模式
  • 通过 MQTT 通知服务器加入广播成功
  • 确认广播对讲开始
  • 系统状态更新为对讲中
  • 广播结束,本机对讲也随之结束

luatools 会打印以下日志

八、总结

本文演示了如何使用 Air8000 开发板或 Air8000 核心板实现完整的实时对讲功能,通过模块化的设计实现了设备发现、一对一通话、一对多广播等核心场景。

八、常见问题

1. 对讲功能无法连接怎么办?

  • 确认设备网络连接正常
  • 验证音频硬件初始化是否成功

2. 无法发现其他设备怎么办?

  • 确认所有设备使用相同的 PRODUCT_KEY
  • 检查设备网络状态和云服务连接
  • 检查设备是否在同一设备群组

3. 对讲过程中音质差或有杂音怎么办?

  • 检查麦克风和喇叭硬件连接
  • 确认音频参数配置正确
  • 优化网络环境,确保网络稳定

4. AirTalk 通话时出现啸叫

原因:两个设备(web和设备;设备和设备)的麦克风过近,声音被重复拾取,产生高频反馈信号。

解决方案:

保持设备之间有适当距离;

在设备 Mic 上安装消音海绵,减少声音反射。

5. 希望将 AirTalk 集成至自有平台

解决方案:AirTalk 提供标准 API 接口,支持平台对接。

接口文档:https://iot.luatos.com/#/page3/airtalk_api

6. CODEC初始化时,I2C通讯失败

解决方案:

  • 使用合宙开发板时,如出现I2C/SPI通讯异常的情况,请使用exmux扩展库的setup函数初始化外设分组开关状态,使用open函数打开外设分组,并跳转至exmux扩展库介绍文档中了解I2C/SPI总线上拉问题;https://docs.openluat.com/osapi/ext/exmux/

  • 使用自己制作的板子时,如出现I2C通讯异常的情况,请根据各型号文档中”硬件设计资料“的I2C和SPI板块”常见的坑“栏目中的经验,检查板子上的I2C/SPI总线是正常上拉;也可使用exmux库来管理i2c和spi总线的上拉状态,详情请参考exmux扩展库介绍文档。