extalk.lua 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574
  1. --[[
  2. @module extalk
  3. @summary extalk扩展库
  4. @version 1.1.1
  5. @date 2025.09.18
  6. @author 梁健
  7. @usage
  8. local extalk = require "extalk"
  9. -- 配置并初始化
  10. extalk.setup({
  11. key = "your_product_key",
  12. heart_break_time = 30,
  13. contact_list_cbfnc = function(dev_list) end,
  14. state_cbfnc = function(state) end
  15. })
  16. -- 发起对讲
  17. extalk.start("remote_device_id")
  18. -- 结束对讲
  19. extalk.stop()
  20. ]]
  21. local extalk = {}
  22. -- 模块常量(保留原始数据结构)
  23. extalk.START = 1 -- 通话开始
  24. extalk.STOP = 2 -- 通话结束
  25. extalk.UNRESPONSIVE = 3 -- 未响应
  26. extalk.ONE_ON_ONE = 5 -- 一对一来电
  27. extalk.BROADCAST = 6 -- 广播
  28. local AIRTALK_TASK_NAME = "airtalk_task"
  29. -- 消息类型常量(保留原始数据结构)
  30. local MSG_CONNECT_ON_IND = 0
  31. local MSG_CONNECT_OFF_IND = 1
  32. local MSG_AUTH_IND = 2
  33. local MSG_SPEECH_ON_IND = 3
  34. local MSG_SPEECH_OFF_IND = 4
  35. local MSG_SPEECH_CONNECT_TO = 5
  36. local MSG_SPEECH_STOP_TEST_END = 22
  37. -- 设备状态常量(保留原始数据结构)
  38. local SP_T_NO_READY = 0 -- 离线状态无法对讲
  39. local SP_T_IDLE = 1 -- 对讲空闲状态
  40. local SP_T_CONNECTING = 2 -- 主动发起对讲
  41. local SP_T_CONNECTED = 3 -- 对讲中
  42. local SUCC = "success"
  43. -- 全局状态变量(保留原始数据结构)
  44. local g_state = SP_T_NO_READY -- 设备状态
  45. local g_mqttc = nil -- mqtt客户端
  46. local g_local_id -- 本机ID
  47. local g_remote_id -- 对端ID
  48. local g_s_type -- 对讲的模式,字符串形式
  49. local g_s_topic -- 对讲用的topic
  50. local g_s_mode -- 对讲的模式
  51. local g_dev_list -- 对讲列表
  52. local g_dl_topic -- 下行消息topic模板
  53. -- 配置参数
  54. local extalk_configs_local = {
  55. key = 0, -- 项目key,一般需要和main的PRODUCT_KEY保持一致
  56. heart_break_time = 0, -- 心跳间隔(单位秒)
  57. contact_list_cbfnc = nil, -- 联系人回调函数,含设备号和昵称
  58. state_cbfnc = nil, -- 状态回调,分为对讲开始,对讲结束,未响应
  59. }
  60. -- 工具函数:参数检查
  61. local function check_param(param, expected_type, name)
  62. if type(param) ~= expected_type then
  63. log.error(string.format("参数错误: %s 应为 %s 类型,实际为 %s",
  64. name, expected_type, type(param)))
  65. return false
  66. end
  67. return true
  68. end
  69. -- MQTT消息发布函数,集中处理所有发布操作并打印日志
  70. local function publish_message(topic, payload)
  71. if g_mqttc then
  72. log.info("MQTT发布 - 主题:", topic, "内容:", payload)
  73. g_mqttc:publish(topic, payload)
  74. else
  75. log.error("MQTT客户端未初始化,无法发布消息")
  76. end
  77. end
  78. -- 对讲超时处理
  79. local function wait_speech_to()
  80. log.info("主动请求对讲超时无应答")
  81. speech_off(true, false)
  82. end
  83. -- 发送鉴权消息
  84. local function auth()
  85. if g_state == SP_T_NO_READY and g_mqttc then
  86. local topic = string.format("ctrl/uplink/%s/0001", g_local_id)
  87. local payload = json.encode({
  88. ["key"] = extalk_configs_local.key,
  89. ["device_type"] = 1
  90. })
  91. publish_message(topic, payload)
  92. end
  93. end
  94. -- 发送心跳消息
  95. local function heart()
  96. if g_state == SP_T_CONNECTED and g_mqttc then
  97. local topic = string.format("ctrl/uplink/%s/0005", g_local_id)
  98. local payload = json.encode({
  99. ["from"] = g_local_id,
  100. ["to"] = g_remote_id
  101. })
  102. publish_message(topic, payload)
  103. end
  104. end
  105. -- 开始对讲
  106. local function speech_on(ssrc, sample)
  107. g_state = SP_T_CONNECTED
  108. g_mqttc:subscribe(g_s_topic)
  109. airtalk.set_topic(g_s_topic)
  110. airtalk.set_ssrc(ssrc)
  111. log.info("对讲模式", g_s_mode)
  112. airtalk.speech(true, g_s_mode, sample)
  113. sys.sendMsg(AIRTALK_TASK_NAME, MSG_SPEECH_ON_IND, true)
  114. sys.timerLoopStart(heart, extalk_configs_local.heart_break_time * 1000)
  115. sys.timerStopAll(wait_speech_to)
  116. end
  117. -- 结束对讲
  118. local function speech_off(need_upload, need_ind)
  119. if g_state == SP_T_CONNECTED then
  120. g_mqttc:unsubscribe(g_s_topic)
  121. airtalk.speech(false)
  122. g_s_topic = nil
  123. end
  124. g_state = SP_T_IDLE
  125. sys.timerStopAll(auth)
  126. sys.timerStopAll(heart)
  127. log.info("wait_speech_to",wait_speech_to)
  128. sys.timerStopAll(wait_speech_to)
  129. if need_upload and g_mqttc then
  130. local topic = string.format("ctrl/uplink/%s/0004", g_local_id)
  131. publish_message(topic, json.encode({["to"] = g_remote_id}))
  132. end
  133. if need_ind then
  134. sys.sendMsg(AIRTALK_TASK_NAME, MSG_SPEECH_OFF_IND, true)
  135. end
  136. end
  137. -- 命令处理:请求对讲应答
  138. local function handle_speech_response(obj)
  139. if g_state ~= SP_T_CONNECTING then
  140. log.error("state", g_state, "need", SP_T_CONNECTING)
  141. return
  142. end
  143. if obj and obj["result"] == SUCC and g_s_topic == obj["topic"] then
  144. -- 开始对讲
  145. local sample_rate = obj["audio_code"] == "amr-nb" and 8000 or 16000
  146. speech_on(obj["ssrc"], sample_rate)
  147. return
  148. else
  149. log.info(obj["result"], obj["topic"], g_s_topic)
  150. sys.sendMsg(AIRTALK_TASK_NAME, MSG_SPEECH_ON_IND, false)
  151. end
  152. g_s_topic = nil
  153. g_state = SP_T_IDLE
  154. end
  155. -- 命令处理:对端来电
  156. local function handle_incoming_call(obj)
  157. if not obj or not obj["topic"] or not obj["ssrc"] or not obj["audio_code"] or not obj["type"] then
  158. local response = {
  159. ["result"] = "failed",
  160. ["topic"] = obj and obj["topic"] or "",
  161. ["info"] = "无效的请求参数"
  162. }
  163. publish_message(string.format("ctrl/uplink/%s/8102", g_local_id), json.encode(response))
  164. return
  165. end
  166. -- 非空闲状态无法接收来电
  167. if g_state ~= SP_T_IDLE then
  168. log.error("state", g_state, "need", SP_T_IDLE)
  169. local response = {
  170. ["result"] = "failed",
  171. ["topic"] = obj["topic"],
  172. ["info"] = "device is busy"
  173. }
  174. publish_message(string.format("ctrl/uplink/%s/8102", g_local_id), json.encode(response))
  175. return
  176. end
  177. local response, from = {}, nil
  178. -- 提取对端ID
  179. from = string.match(obj["topic"], "audio/(.*)/.*/.*")
  180. if not from then
  181. response = {
  182. ["result"] = "failed",
  183. ["topic"] = obj["topic"],
  184. ["info"] = "topic error"
  185. }
  186. publish_message(string.format("ctrl/uplink/%s/8102", g_local_id), json.encode(response))
  187. return
  188. end
  189. -- 处理一对一通话
  190. if obj["type"] == "one-on-one" then
  191. g_s_topic = obj["topic"]
  192. g_remote_id = from
  193. g_s_type = "one-on-one"
  194. g_s_mode = airtalk.MODE_PERSON
  195. -- 触发回调
  196. if extalk_configs_local.state_cbfnc then
  197. extalk_configs_local.state_cbfnc({
  198. state = extalk.ONE_ON_ONE,
  199. id = from
  200. })
  201. end
  202. response = {["result"] = SUCC, ["topic"] = obj["topic"], ["info"] = ""}
  203. local sample_rate = obj["audio_code"] == "amr-nb" and 8000 or 16000
  204. speech_on(obj["ssrc"], sample_rate)
  205. end
  206. -- 处理广播
  207. if obj["type"] == "broadcast" then
  208. g_s_topic = obj["topic"]
  209. g_remote_id = from
  210. g_s_mode = airtalk.MODE_GROUP_LISTENER
  211. g_s_type = "broadcast"
  212. -- 触发回调
  213. if extalk_configs_local.state_cbfnc then
  214. extalk_configs_local.state_cbfnc({
  215. state = extalk.BROADCAST,
  216. id = from
  217. })
  218. end
  219. response = {["result"] = SUCC, ["topic"] = obj["topic"], ["info"] = ""}
  220. local sample_rate = obj["audio_code"] == "amr-nb" and 8000 or 16000
  221. speech_on(obj["ssrc"], sample_rate)
  222. end
  223. -- 发送响应
  224. publish_message(string.format("ctrl/uplink/%s/8102", g_local_id), json.encode(response))
  225. end
  226. -- 命令处理:对端挂断
  227. local function handle_remote_hangup(obj)
  228. local response = {}
  229. if g_state == SP_T_IDLE then
  230. response = {["result"] = "failed", ["info"] = "no speech"}
  231. else
  232. log.info("0103", obj, obj["type"], g_s_type)
  233. if obj and obj["type"] == g_s_type then
  234. response = {["result"] = SUCC, ["info"] = ""}
  235. speech_off(false, true)
  236. else
  237. response = {["result"] = "failed", ["info"] = "type mismatch"}
  238. end
  239. end
  240. publish_message(string.format("ctrl/uplink/%s/8103", g_local_id), json.encode(response))
  241. end
  242. -- 命令处理:更新设备列表
  243. local function handle_device_list_update(obj)
  244. local response = {}
  245. if obj then
  246. g_dev_list = obj["dev_list"]
  247. response = {["result"] = SUCC, ["info"] = ""}
  248. else
  249. response = {["result"] = "failed", ["info"] = "json info error"}
  250. end
  251. publish_message(string.format("ctrl/uplink/%s/8101", g_local_id), json.encode(response))
  252. end
  253. -- 命令处理:鉴权结果
  254. local function handle_auth_result(obj)
  255. if obj and obj["result"] == SUCC then
  256. publish_message(string.format("ctrl/uplink/%s/0002", g_local_id), "") -- 更新列表
  257. else
  258. sys.sendMsg(AIRTALK_TASK_NAME, MSG_AUTH_IND, false,
  259. "鉴权失败" .. (obj and obj["info"] or ""))
  260. end
  261. end
  262. -- 命令处理:设备列表更新应答
  263. local function handle_device_list_response(obj)
  264. if obj and obj["result"] == SUCC then
  265. g_dev_list = obj["dev_list"]
  266. if extalk_configs_local.contact_list_cbfnc then
  267. extalk_configs_local.contact_list_cbfnc(g_dev_list)
  268. end
  269. g_state = SP_T_IDLE
  270. sys.sendMsg(AIRTALK_TASK_NAME, MSG_AUTH_IND, true) -- 完整登录流程结束
  271. else
  272. sys.sendMsg(AIRTALK_TASK_NAME, MSG_AUTH_IND, false, "更新设备列表失败")
  273. end
  274. end
  275. -- 命令解析路由表
  276. local cmd_handlers = {
  277. ["8003"] = handle_speech_response, -- 请求对讲应答
  278. ["0102"] = handle_incoming_call, -- 平台通知对端对讲开始
  279. ["0103"] = handle_remote_hangup, -- 平台通知终端对讲结束
  280. ["0101"] = handle_device_list_update,-- 平台通知终端更新对讲设备列表
  281. ["8001"] = handle_auth_result, -- 平台对鉴权应答
  282. ["8002"] = handle_device_list_response -- 平台对终端获取终端列表应答
  283. }
  284. -- 解析接收到的消息
  285. local function analyze_v1(cmd, topic, obj)
  286. -- 忽略心跳和结束对讲的应答
  287. if cmd == "8005" or cmd == "8004" then
  288. return
  289. end
  290. -- 查找并执行对应的命令处理器
  291. local handler = cmd_handlers[cmd]
  292. if handler then
  293. handler(obj)
  294. else
  295. log.warn("未处理的命令", cmd)
  296. end
  297. end
  298. -- MQTT回调处理
  299. local function mqtt_cb(mqttc, event, topic, payload)
  300. log.info(event, topic or "")
  301. if event == "conack" then
  302. -- MQTT连接成功,开始自定义鉴权流程
  303. sys.sendMsg(AIRTALK_TASK_NAME, MSG_CONNECT_ON_IND)
  304. g_mqttc:subscribe("ctrl/downlink/" .. g_local_id .. "/#")
  305. elseif event == "suback" then
  306. if g_state == SP_T_NO_READY then
  307. if topic then
  308. auth()
  309. else
  310. sys.sendMsg(AIRTALK_TASK_NAME, MSG_AUTH_IND, false,
  311. "订阅失败" .. "ctrl/downlink/" .. g_local_id .. "/#")
  312. end
  313. elseif g_state == SP_T_CONNECTED and not topic then
  314. speech_off(false, true)
  315. end
  316. elseif event == "recv" then
  317. local result = string.match(topic, g_dl_topic)
  318. if result then
  319. local obj = json.decode(payload)
  320. analyze_v1(result, topic, obj)
  321. end
  322. elseif event == "disconnect" then
  323. speech_off(false, true)
  324. g_state = SP_T_NO_READY
  325. elseif event == "error" then
  326. log.error("MQTT错误发生")
  327. end
  328. end
  329. -- 任务消息处理
  330. local function task_cb(msg)
  331. if msg[1] == MSG_SPEECH_CONNECT_TO then
  332. speech_off(true, false)
  333. else
  334. log.info("未处理消息", msg[1], msg[2], msg[3], msg[4])
  335. end
  336. end
  337. -- 对讲事件回调
  338. local function airtalk_event_cb(event, param)
  339. log.info("airtalk event", event, param)
  340. if event == airtalk.EVENT_ERROR then
  341. if param == airtalk.ERROR_NO_DATA then
  342. log.error("长时间没有收到音频数据")
  343. speech_off(true, true)
  344. end
  345. end
  346. end
  347. -- MQTT任务主循环
  348. local function airtalk_mqtt_task()
  349. local msg, online = nil, false
  350. -- 初始化本地ID
  351. g_local_id = mobile.imei()
  352. g_dl_topic = "ctrl/downlink/" .. g_local_id .. "/(%w%w%w%w)"
  353. -- 创建MQTT客户端
  354. g_mqttc = mqtt.create(nil, "mqtt.airtalk.luatos.com", 1883, false, {rxSize = 32768})
  355. -- 配置对讲参数
  356. airtalk.config(airtalk.PROTOCOL_MQTT, g_mqttc, 200) -- 缓冲至少200ms播放
  357. airtalk.on(airtalk_event_cb)
  358. airtalk.start()
  359. -- 配置MQTT客户端
  360. g_mqttc:auth(g_local_id, g_local_id, mobile.muid())
  361. g_mqttc:keepalive(240) -- 默认值240s
  362. g_mqttc:autoreconn(true, 15000) -- 自动重连机制
  363. g_mqttc:debug(false)
  364. g_mqttc:on(mqtt_cb)
  365. log.info("设备信息", g_local_id, mobile.muid())
  366. -- 开始连接
  367. g_mqttc:connect()
  368. online = false
  369. while true do
  370. -- 等待MQTT连接成功
  371. msg = sys.waitMsg(AIRTALK_TASK_NAME, MSG_CONNECT_ON_IND)
  372. log.info("connected")
  373. -- 处理登录流程
  374. while not online do
  375. msg = sys.waitMsg(AIRTALK_TASK_NAME, MSG_AUTH_IND, 30000) -- 30秒超时
  376. if type(msg) == 'table' then
  377. online = msg[2]
  378. if online then
  379. -- 鉴权通过,60分钟后重新鉴权
  380. sys.timerLoopStart(auth, 3600000)
  381. else
  382. log.info(msg[3])
  383. -- 鉴权失败,5分钟后重试
  384. sys.timerLoopStart(auth, 300000)
  385. end
  386. else
  387. -- 超时未收到鉴权结果,重新发送
  388. auth()
  389. end
  390. end
  391. log.info("对讲管理平台已连接")
  392. -- 处理在线状态下的消息
  393. while online do
  394. msg = sys.waitMsg(AIRTALK_TASK_NAME)
  395. if type(msg) == 'table' and type(msg[1]) == "number" then
  396. if msg[1] == MSG_SPEECH_STOP_TEST_END then
  397. if g_state ~= SP_T_CONNECTING and g_state ~= SP_T_CONNECTED then
  398. log.info("没有对讲", g_state)
  399. else
  400. speech_off(true, false)
  401. end
  402. elseif msg[1] == MSG_SPEECH_ON_IND then
  403. if extalk_configs_local.state_cbfnc then
  404. local state = msg[2] and extalk.START or extalk.UNRESPONSIVE
  405. extalk_configs_local.state_cbfnc({state = state})
  406. end
  407. elseif msg[1] == MSG_SPEECH_OFF_IND then
  408. if extalk_configs_local.state_cbfnc then
  409. extalk_configs_local.state_cbfnc({state = extalk.STOP})
  410. end
  411. elseif msg[1] == MSG_CONNECT_OFF_IND then
  412. log.info("connect", msg[2])
  413. online = msg[2]
  414. end
  415. else
  416. log.info(type(msg), type(msg and msg[1]))
  417. end
  418. msg = nil -- 清理引用
  419. end
  420. online = false -- 重置在线状态
  421. end
  422. end
  423. -- 模块初始化
  424. function extalk.setup(extalk_configs)
  425. if not extalk_configs or type(extalk_configs) ~= "table" then
  426. log.error("AirTalk配置必须为table类型")
  427. return false
  428. end
  429. -- 检查配置参数
  430. if not check_param(extalk_configs.key, "string", "key") then
  431. return false
  432. end
  433. extalk_configs_local.key = extalk_configs.key
  434. if not check_param(extalk_configs.heart_break_time, "number", "heart_break_time") then
  435. return false
  436. end
  437. extalk_configs_local.heart_break_time = extalk_configs.heart_break_time
  438. if not check_param(extalk_configs.contact_list_cbfnc, "function", "contact_list_cbfnc") then
  439. return false
  440. end
  441. extalk_configs_local.contact_list_cbfnc = extalk_configs.contact_list_cbfnc
  442. if not check_param(extalk_configs.state_cbfnc, "function", "state_cbfnc") then
  443. return false
  444. end
  445. extalk_configs_local.state_cbfnc = extalk_configs.state_cbfnc
  446. -- 启动任务
  447. sys.taskInitEx(airtalk_mqtt_task, AIRTALK_TASK_NAME, task_cb)
  448. return true
  449. end
  450. -- 开始对讲
  451. function extalk.start(id)
  452. if g_state ~= SP_T_IDLE then
  453. log.warn("正在对讲无法开始,当前状态:", g_state)
  454. return false
  455. end
  456. if id == nil then
  457. -- 广播模式
  458. g_remote_id = "all"
  459. g_state = SP_T_CONNECTING
  460. g_s_mode = airtalk.MODE_GROUP_SPEAKER
  461. g_s_type = "broadcast"
  462. g_s_topic = string.format("audio/%s/all/%s",
  463. g_local_id, string.sub(tostring(mcu.ticks()), -4, -1))
  464. publish_message(string.format("ctrl/uplink/%s/0003", g_local_id),
  465. json.encode({["topic"] = g_s_topic, ["type"] = g_s_type}))
  466. sys.timerStart(wait_speech_to, 15000)
  467. else
  468. -- 一对一模式
  469. log.info("向", id, "主动发起对讲")
  470. if id == g_local_id then
  471. log.error("不允许本机给本机拨打电话")
  472. return false
  473. end
  474. g_state = SP_T_CONNECTING
  475. g_remote_id = id
  476. g_s_mode = airtalk.MODE_PERSON
  477. g_s_type = "one-on-one"
  478. g_s_topic = string.format("audio/%s/%s/%s",
  479. g_local_id, id, string.sub(tostring(mcu.ticks()), -4, -1))
  480. publish_message(string.format("ctrl/uplink/%s/0003", g_local_id),
  481. json.encode({["topic"] = g_s_topic, ["type"] = g_s_type}))
  482. sys.timerStart(wait_speech_to, 15000)
  483. end
  484. return true
  485. end
  486. -- 结束对讲
  487. function extalk.stop()
  488. if g_state ~= SP_T_CONNECTING and g_state ~= SP_T_CONNECTED then
  489. log.info("没有对讲,当前状态:", g_state)
  490. return false
  491. end
  492. log.info("主动断开对讲")
  493. speech_off(true, false)
  494. return true
  495. end
  496. return extalk