Files
BR_YKC/4G/tools/resource/soc_script/v2025.12.31.22/lib/extalk.lua
2026-03-31 15:46:04 +08:00

589 lines
18 KiB
Lua
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
--[[
@module extalk
@summary extalk扩展库
@version 1.1.1
@date 2025.09.18
@author 梁健
@usage
local extalk = require "extalk"
-- 配置并初始化
extalk.setup({
key = "your_product_key",
heart_break_time = 30,
contact_list_cbfnc = function(dev_list) end,
state_cbfnc = function(state) end
})
-- 发起对讲
extalk.start("remote_device_id")
-- 结束对讲
extalk.stop()
]]
local extalk = {}
-- 模块常量(保留原始数据结构)
extalk.START = 1 -- 通话开始
extalk.STOP = 2 -- 通话结束
extalk.UNRESPONSIVE = 3 -- 未响应
extalk.ONE_ON_ONE = 5 -- 一对一来电
extalk.BROADCAST = 6 -- 广播
local AIRTALK_TASK_NAME = "airtalk_task"
-- 消息类型常量(保留原始数据结构)
local MSG_CONNECT_ON_IND = 0
local MSG_CONNECT_OFF_IND = 1
local MSG_AUTH_IND = 2
local MSG_SPEECH_ON_IND = 3
local MSG_SPEECH_OFF_IND = 4
local MSG_SPEECH_CONNECT_TO = 5
local MSG_SPEECH_STOP_TEST_END = 22
-- 设备状态常量(保留原始数据结构)
local SP_T_NO_READY = 0 -- 离线状态无法对讲
local SP_T_IDLE = 1 -- 对讲空闲状态
local SP_T_CONNECTING = 2 -- 主动发起对讲
local SP_T_CONNECTED = 3 -- 对讲中
local SUCC = "success"
-- 全局状态变量(保留原始数据结构)
local g_state = SP_T_NO_READY -- 设备状态
local g_mqttc = nil -- mqtt客户端
local g_local_id -- 本机ID
local g_stask_start = false -- 本机ID
local g_remote_id -- 对端ID
local g_s_type -- 对讲的模式,字符串形式
local g_s_topic -- 对讲用的topic
local g_s_mode -- 对讲的模式
local g_dev_list -- 对讲列表
local g_dl_topic -- 下行消息topic模板
-- 配置参数
local extalk_configs_local = {
key = 0, -- 项目key一般需要和main的PRODUCT_KEY保持一致
heart_break_time = 0, -- 心跳间隔(单位秒)
contact_list_cbfnc = nil, -- 联系人回调函数,含设备号和昵称
state_cbfnc = nil, -- 状态回调,分为对讲开始,对讲结束,未响应
}
-- 工具函数:参数检查
local function check_param(param, expected_type, name)
if type(param) ~= expected_type then
log.error(string.format("参数错误: %s 应为 %s 类型,实际为 %s",
name, expected_type, type(param)))
return false
end
return true
end
-- MQTT消息发布函数集中处理所有发布操作并打印日志
local function publish_message(topic, payload)
if g_mqttc then
log.info("MQTT发布 - 主题:", topic, "内容:", payload)
g_mqttc:publish(topic, payload)
else
log.error("MQTT客户端未初始化无法发布消息")
end
end
-- 对讲超时处理
function extalk.wait_speech_to()
log.info("主动请求对讲超时无应答")
extalk.speech_off(true, false)
end
-- 发送鉴权消息
local function auth()
if g_state == SP_T_NO_READY and g_mqttc then
local topic = string.format("ctrl/uplink/%s/0001", g_local_id)
local payload = json.encode({
["key"] = extalk_configs_local.key,
["device_type"] = 1
})
publish_message(topic, payload)
end
end
-- 发送心跳消息
local function heart()
if g_mqttc then
adc.open(adc.CH_VBAT)
local vbat = adc.get(adc.CH_VBAT)
adc.close(adc.CH_VBAT)
local topic = string.format("ctrl/uplink/%s/0005", g_local_id)
local payload = json.encode({
["csq"] = mobile.csq(),
["battery"] = vbat
})
publish_message(topic, payload)
end
end
-- 开始对讲
local function speech_on(ssrc, sample)
g_state = SP_T_CONNECTED
g_mqttc:subscribe(g_s_topic)
airtalk.set_topic(g_s_topic)
airtalk.set_ssrc(ssrc)
log.info("对讲模式", g_s_mode)
airtalk.speech(true, g_s_mode, sample)
sys.sendMsg(AIRTALK_TASK_NAME, MSG_SPEECH_ON_IND, true)
-- sys.timerLoopStart(heart, extalk_configs_local.heart_break_time * 1000)
sys.timerStopAll(extalk.wait_speech_to)
end
-- 结束对讲
function extalk.speech_off(need_upload, need_ind)
if g_state == SP_T_CONNECTED then
g_mqttc:unsubscribe(g_s_topic)
airtalk.speech(false)
g_s_topic = nil
end
g_state = SP_T_IDLE
sys.timerStopAll(auth)
sys.timerStopAll(extalk.wait_speech_to)
if need_upload and g_mqttc then
local topic = string.format("ctrl/uplink/%s/0004", g_local_id)
publish_message(topic, json.encode({["to"] = g_remote_id}))
end
if need_ind then
sys.sendMsg(AIRTALK_TASK_NAME, MSG_SPEECH_OFF_IND, true)
end
end
-- 命令处理:请求对讲应答
local function handle_speech_response(obj)
if g_state ~= SP_T_CONNECTING then
log.error("state", g_state, "need", SP_T_CONNECTING)
return
end
if obj and obj["result"] == SUCC and g_s_topic == obj["topic"] then
-- 开始对讲
local sample_rate = obj["audio_code"] == "amr-nb" and 8000 or 16000
speech_on(obj["ssrc"], sample_rate)
return
else
log.info(obj["result"], obj["topic"], g_s_topic)
sys.sendMsg(AIRTALK_TASK_NAME, MSG_SPEECH_ON_IND, false)
end
g_s_topic = nil
g_state = SP_T_IDLE
end
-- 命令处理:对端来电
local function handle_incoming_call(obj)
if not obj or not obj["topic"] or not obj["ssrc"] or not obj["audio_code"] or not obj["type"] then
local response = {
["result"] = "failed",
["topic"] = obj and obj["topic"] or "",
["info"] = "无效的请求参数"
}
publish_message(string.format("ctrl/uplink/%s/8102", g_local_id), json.encode(response))
return
end
-- 非空闲状态无法接收来电
if g_state ~= SP_T_IDLE then
log.error("state", g_state, "need", SP_T_IDLE)
local response = {
["result"] = "failed",
["topic"] = obj["topic"],
["info"] = "device is busy"
}
publish_message(string.format("ctrl/uplink/%s/8102", g_local_id), json.encode(response))
return
end
local response, from = {}, nil
-- 提取对端ID
from = string.match(obj["topic"], "audio/(.*)/.*/.*")
if not from then
response = {
["result"] = "failed",
["topic"] = obj["topic"],
["info"] = "topic error"
}
publish_message(string.format("ctrl/uplink/%s/8102", g_local_id), json.encode(response))
return
end
-- 处理一对一通话
if obj["type"] == "one-on-one" then
g_s_topic = obj["topic"]
g_remote_id = from
g_s_type = "one-on-one"
g_s_mode = airtalk.MODE_PERSON
-- 触发回调
if extalk_configs_local.state_cbfnc then
extalk_configs_local.state_cbfnc({
state = extalk.ONE_ON_ONE,
id = from
})
end
response = {["result"] = SUCC, ["topic"] = obj["topic"], ["info"] = ""}
local sample_rate = obj["audio_code"] == "amr-nb" and 8000 or 16000
speech_on(obj["ssrc"], sample_rate)
end
-- 处理广播
if obj["type"] == "broadcast" then
g_s_topic = obj["topic"]
g_remote_id = from
g_s_mode = airtalk.MODE_GROUP_LISTENER
g_s_type = "broadcast"
-- 触发回调
if extalk_configs_local.state_cbfnc then
extalk_configs_local.state_cbfnc({
state = extalk.BROADCAST,
id = from
})
end
response = {["result"] = SUCC, ["topic"] = obj["topic"], ["info"] = ""}
local sample_rate = obj["audio_code"] == "amr-nb" and 8000 or 16000
speech_on(obj["ssrc"], sample_rate)
end
-- 发送响应
publish_message(string.format("ctrl/uplink/%s/8102", g_local_id), json.encode(response))
end
-- 命令处理:对端挂断
local function handle_remote_hangup(obj)
local response = {}
if g_state == SP_T_IDLE then
response = {["result"] = "failed", ["info"] = "no speech"}
else
log.info("0103", obj, obj["type"], g_s_type)
if obj and obj["type"] == g_s_type then
response = {["result"] = SUCC, ["info"] = ""}
extalk.speech_off(false, true)
else
response = {["result"] = "failed", ["info"] = "type mismatch"}
end
end
publish_message(string.format("ctrl/uplink/%s/8103", g_local_id), json.encode(response))
end
-- 命令处理:更新设备列表
local function handle_device_list_update(obj)
local response = {}
if obj then
g_dev_list = obj["dev_list"]
response = {["result"] = SUCC, ["info"] = ""}
else
response = {["result"] = "failed", ["info"] = "json info error"}
end
publish_message(string.format("ctrl/uplink/%s/8101", g_local_id), json.encode(response))
end
-- 命令处理:鉴权结果
local function handle_auth_result(obj)
if obj and obj["result"] == SUCC then
publish_message(string.format("ctrl/uplink/%s/0002", g_local_id), "") -- 更新列表
sys.timerLoopStart(heart, extalk_configs_local.heart_break_time * 1000) -- 发起心跳
else
sys.sendMsg(AIRTALK_TASK_NAME, MSG_AUTH_IND, false,
"鉴权失败" .. (obj and obj["info"] or ""))
log.error("鉴权失败,可能是没有修改PRODUCT_KEY")
end
end
-- 命令处理:设备列表更新应答
local function handle_device_list_response(obj)
if obj and obj["result"] == SUCC then
g_dev_list = obj["dev_list"]
if extalk_configs_local.contact_list_cbfnc then
extalk_configs_local.contact_list_cbfnc(g_dev_list)
end
g_state = SP_T_IDLE
sys.sendMsg(AIRTALK_TASK_NAME, MSG_AUTH_IND, true) -- 完整登录流程结束
else
sys.sendMsg(AIRTALK_TASK_NAME, MSG_AUTH_IND, false, "更新设备列表失败")
end
end
-- 命令解析路由表
local cmd_handlers = {
["8003"] = handle_speech_response, -- 请求对讲应答
["0102"] = handle_incoming_call, -- 平台通知对端对讲开始
["0103"] = handle_remote_hangup, -- 平台通知终端对讲结束
["0101"] = handle_device_list_update,-- 平台通知终端更新对讲设备列表
["8001"] = handle_auth_result, -- 平台对鉴权应答
["8002"] = handle_device_list_response -- 平台对终端获取终端列表应答
}
-- 解析接收到的消息
local function analyze_v1(cmd, topic, obj)
-- 忽略心跳和结束对讲的应答
if cmd == "8005" or cmd == "8004" then
return
end
-- 查找并执行对应的命令处理器
local handler = cmd_handlers[cmd]
if handler then
handler(obj)
else
log.warn("未处理的命令", cmd)
end
end
-- MQTT回调处理
local function mqtt_cb(mqttc, event, topic, payload)
log.info(event, topic or "")
if event == "conack" then
-- MQTT连接成功开始自定义鉴权流程
sys.sendMsg(AIRTALK_TASK_NAME, MSG_CONNECT_ON_IND)
g_mqttc:subscribe("ctrl/downlink/" .. g_local_id .. "/#")
elseif event == "suback" then
if g_state == SP_T_NO_READY then
if topic then
auth()
else
sys.sendMsg(AIRTALK_TASK_NAME, MSG_AUTH_IND, false,
"订阅失败" .. "ctrl/downlink/" .. g_local_id .. "/#")
end
elseif g_state == SP_T_CONNECTED and not topic then
extalk.speech_off(false, true)
end
elseif event == "recv" then
local result = string.match(topic, g_dl_topic)
if result then
local obj = json.decode(payload)
analyze_v1(result, topic, obj)
end
elseif event == "disconnect" then
extalk.speech_off(false, true)
g_state = SP_T_NO_READY
elseif event == "error" then
log.error("MQTT错误发生",topic,payload)
end
end
-- 任务消息处理
local function task_cb(msg)
if msg[1] == MSG_SPEECH_CONNECT_TO then
extalk.speech_off(true, false)
else
log.info("未处理消息", msg[1], msg[2], msg[3], msg[4])
end
end
-- 对讲事件回调
local function airtalk_event_cb(event, param)
log.info("airtalk event", event, param)
if event == airtalk.EVENT_ERROR then
if param == airtalk.ERROR_NO_DATA and g_s_mode == airtalk.MODE_PERSON then
log.error("长时间没有收到音频数据")
extalk.speech_off(true, true)
end
end
end
-- MQTT任务主循环
local function airtalk_mqtt_task()
if g_stask_start then
log.info("airtalk task 已经初始化了")
return true
end
g_stask_start = true
local msg, online = nil, false
-- 初始化本地ID
g_local_id = mobile.imei()
g_dl_topic = "ctrl/downlink/" .. g_local_id .. "/(%w%w%w%w)"
-- 创建MQTT客户端
g_mqttc = mqtt.create(nil, "mqtt.airtalk.luatos.com", 1883, false, {rxSize = 32768})
-- 配置对讲参数
airtalk.config(airtalk.PROTOCOL_MQTT, g_mqttc, 200) -- 缓冲至少200ms播放
airtalk.on(airtalk_event_cb)
airtalk.start()
-- 配置MQTT客户端
g_mqttc:auth(g_local_id, g_local_id, mobile.muid())
g_mqttc:keepalive(240) -- 默认值240s
g_mqttc:autoreconn(true, 15000) -- 自动重连机制
g_mqttc:debug(false)
g_mqttc:on(mqtt_cb)
log.info("设备信息", g_local_id, mobile.muid())
-- 开始连接
g_mqttc:connect()
online = false
while true do
-- 等待MQTT连接成功
msg = sys.waitMsg(AIRTALK_TASK_NAME, MSG_CONNECT_ON_IND)
log.info("connected")
-- 处理登录流程
while not online do
msg = sys.waitMsg(AIRTALK_TASK_NAME, MSG_AUTH_IND, 30000) -- 30秒超时
if type(msg) == 'table' then
online = msg[2]
if online then
-- 鉴权通过60分钟后重新鉴权
sys.timerLoopStart(auth, 3600000)
else
log.info(msg[3])
-- 鉴权失败5分钟后重试
sys.timerLoopStart(auth, 300000)
end
else
-- 超时未收到鉴权结果,重新发送
auth()
end
end
log.info("对讲管理平台已连接")
-- 处理在线状态下的消息
while online do
msg = sys.waitMsg(AIRTALK_TASK_NAME)
if type(msg) == 'table' and type(msg[1]) == "number" then
if msg[1] == MSG_SPEECH_STOP_TEST_END then
if g_state ~= SP_T_CONNECTING and g_state ~= SP_T_CONNECTED then
log.info("没有对讲", g_state)
else
extalk.speech_off(true, false)
end
elseif msg[1] == MSG_SPEECH_ON_IND then
if extalk_configs_local.state_cbfnc then
local state = msg[2] and extalk.START or extalk.UNRESPONSIVE
extalk_configs_local.state_cbfnc({state = state})
end
elseif msg[1] == MSG_SPEECH_OFF_IND then
if extalk_configs_local.state_cbfnc then
extalk_configs_local.state_cbfnc({state = extalk.STOP})
end
elseif msg[1] == MSG_CONNECT_OFF_IND then
log.info("connect", msg[2])
online = msg[2]
end
else
log.info(type(msg), type(msg and msg[1]))
end
msg = nil -- 清理引用
end
online = false -- 重置在线状态
end
end
-- 模块初始化
function extalk.setup(extalk_configs)
if not extalk_configs or type(extalk_configs) ~= "table" then
log.error("AirTalk配置必须为table类型")
return false
end
-- 检查配置参数
if not check_param(extalk_configs.key, "string", "key") then
return false
end
extalk_configs_local.key = extalk_configs.key
if not check_param(extalk_configs.heart_break_time, "number", "heart_break_time") then
return false
end
extalk_configs_local.heart_break_time = extalk_configs.heart_break_time
if not check_param(extalk_configs.contact_list_cbfnc, "function", "contact_list_cbfnc") then
return false
end
extalk_configs_local.contact_list_cbfnc = extalk_configs.contact_list_cbfnc
if not check_param(extalk_configs.state_cbfnc, "function", "state_cbfnc") then
return false
end
extalk_configs_local.state_cbfnc = extalk_configs.state_cbfnc
-- 启动任务
sys.taskInitEx(airtalk_mqtt_task, AIRTALK_TASK_NAME, task_cb)
return true
end
-- 开始对讲
function extalk.start(id)
if g_state ~= SP_T_IDLE then
log.warn("正在对讲无法开始,当前状态:", g_state)
return false
end
if id == nil then
-- 广播模式
g_remote_id = "all"
g_state = SP_T_CONNECTING
g_s_mode = airtalk.MODE_GROUP_SPEAKER
g_s_type = "broadcast"
g_s_topic = string.format("audio/%s/all/%s",
g_local_id, string.sub(tostring(mcu.ticks()), -4, -1))
publish_message(string.format("ctrl/uplink/%s/0003", g_local_id),
json.encode({["topic"] = g_s_topic, ["type"] = g_s_type}))
sys.timerStart(extalk.wait_speech_to, 15000)
else
-- 一对一模式
log.info("", id, "主动发起对讲")
if id == g_local_id then
log.error("不允许本机给本机拨打电话")
return false
end
g_state = SP_T_CONNECTING
g_remote_id = id
g_s_mode = airtalk.MODE_PERSON
g_s_type = "one-on-one"
g_s_topic = string.format("audio/%s/%s/%s",
g_local_id, id, string.sub(tostring(mcu.ticks()), -4, -1))
publish_message(string.format("ctrl/uplink/%s/0003", g_local_id),
json.encode({["topic"] = g_s_topic, ["type"] = g_s_type}))
sys.timerStart(extalk.wait_speech_to, 15000)
end
return true
end
-- 结束对讲
function extalk.stop()
if g_state ~= SP_T_CONNECTING and g_state ~= SP_T_CONNECTED then
log.info("没有对讲,当前状态:", g_state)
return false
end
log.info("主动断开对讲")
extalk.speech_off(true, false)
return true
end
return extalk