UNPKG

esp-ai

Version:

Provide a complete set of AI dialogue solutions for your development board, including but not limited to the IAT+LLM+TTS integration solution for the ESP32 series development board. | 为你的开发板提供全套的AI对话方案,包括但不限于 `ESP32` 系列开发板的 `IAT+LLM+TTS` 集成方案。

267 lines (244 loc) 11.7 kB
/** * Copyright (c) 2024 小明IO * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * * Commercial use of this software requires prior written authorization from the Licensor. * 请注意:将 ESP-AI 代码用于商业用途需要事先获得许可方的授权。 * 删除与修改版权属于侵权行为,请尊重作者版权,避免产生不必要的纠纷。 * * @author 小明IO * @email 1746809408@qq.com * @github https://github.com/wangzongming/esp-ai * @websit https://espai.fun */ const WebSocket = require('ws') const log = require("../utils/log"); const getIPV4 = require("../utils/getIPV4"); const parseUrlParams = require("../utils/parseUrlParams"); const isOutTimeErr = require("../utils/isOutTimeErr"); const TTS_buffer_chunk_queue = require("../utils/tts_buffer_chunk_queue"); const { audio, start, play_audio_ws_conntceed, client_out_audio_ing: client_out_audio_ing_fn, reCache, client_out_audio_over, cts_time, set_wifi_config_res, digitalRead, analogRead, iat_end, client_available_audio, session_stop_ack } = require("../functions/client_messages"); const error_catch_hoc = require("./device_fns/error_catch") // 音频测试 // const fs = require('fs'); // const path = require('path'); // var index = 0; // var writeStream; function init_server() { try { const { port, devLog, onDeviceConnect, onDeviceDisConnect, auth, gen_client_config, api_key } = G_config; if (!gen_client_config) { log.error("请配置 gen_client_config 函数"); return; } if (!api_key) { log.error("请配置 api_key,获取方式: 打开 https://espai.fun -> 创建超体 -> 左下角 api_key"); return; } const wss = new WebSocket.Server({ port }); wss.on('connection', async function connection(ws, req) { const client_params = parseUrlParams(req.url); log.t_info(`设备连接参数:`, req.url); const client_version = client_params.v; const device_id = client_params.device_id; if (!device_id || !client_version) { log.error("设备异常,未读取到 device_id || v || api_key 参数,请检查设备配置。"); setTimeout(() => { ws.send(JSON.stringify({ type: "error", message: `设备异常,未读取到 device_id`, code: "004" })); ws.close(); }, 5000) return; } log.t_info(`[${device_id}] 硬件连接`) // 断电重连 // 这里存在问题,如果删除掉设备会导致设备端无法进入 ready 状态, ing... if (G_devices.get(device_id)) { const { ws: _ws } = G_devices.get(device_id); await G_Instance.stop(device_id, "打断会话时"); ws.terminate(); G_devices.delete(device_id); } G_devices.set(device_id, { started: false, // 会话是否已经停止, 作为 started 的辅助 stoped: true, ws, user_config: {}, first_session: true, llm_historys: [], tts_list: new Map(), await_out_tts: [], client_params, client_version, client_version_arr: client_version?.split?.("."), error_catch: error_catch_hoc(ws), tts_buffer_chunk_queue: new TTS_buffer_chunk_queue(device_id), // 已输出流量 kb useed_flow: 0, read_pin_cbs: new Map(), // 异步停止下一次会话 stop_next_session: false }); ws.isAlive = true; ws.device_id = device_id; ws.client_params = client_params; onDeviceConnect && onDeviceConnect({ ws, device_id, client_version, client_params, instance: G_Instance }); ws.on('message', async function (data) { const comm_args = { device_id }; if(!G_devices.get(device_id)){ log.error(`客戶端异常断开,马上进行重启:${device_id}`); ws && ws.close(); return; } try { if (typeof data === "string") { // console.log('data', data) const { type, tts_task_id, stc_time, session_id, session_status, sid, text, success, value, pin } = JSON.parse(data); comm_args.session_id = session_id; comm_args.session_status = session_status; comm_args.tts_task_id = tts_task_id; comm_args.sid = sid; comm_args.stc_time = stc_time; comm_args.type = type; comm_args.text = text; comm_args.success = success; comm_args.value = value; comm_args.pin = pin; comm_args._ws = ws; switch (type) { case "start": start(comm_args); // test... // writeStream = fs.createWriteStream(path.join(__dirname, `./${index}_output.mp3`)); // index++; break; case "iat_end": iat_end(comm_args); break; case "client_out_audio_ing": client_out_audio_ing_fn(comm_args) break; case "client_out_audio_over": client_out_audio_over(comm_args); break; case "play_audio_ws_conntceed": play_audio_ws_conntceed(comm_args) break; case "char_txt": G_Instance.llm(device_id, text) break; case "tts": G_Instance.tts(device_id, text) break; case "cts_time": cts_time(comm_args); break; case "set_wifi_config_res": set_wifi_config_res(comm_args); break; case "digitalRead": digitalRead(comm_args); break; case "analogRead": analogRead(comm_args); break; case "re_cache": reCache(comm_args); break; case "client_available_audio": client_available_audio(comm_args); break; case "session_stop_ack": session_stop_ack(comm_args); break; } } else { ws.isAlive = true; audio({ ...comm_args, data }) // test... // writeStream.write(data); } } catch (err) { console.log(err); log.error(`消息处理错误:${err}`) log.error(`消息数据:${data}`) } }); ws.on("pong", function () { // console.log("收到 pong") this.isAlive = true; }); ws.on('close', (code, reason) => { devLog && log.info(``); devLog && log.t_red_info(`硬件设备断开连接: ${device_id}, code: ${code}, reason: ${reason}`); devLog && log.t_red_info(`CLOSED: ${ws.CLOSED}`); devLog && log.info(``); onDeviceDisConnect && onDeviceDisConnect({ device_id, client_params, instance: G_Instance }); G_Instance.stop(device_id, "设备断开服务时"); G_devices.delete(device_id); }); ws.on('error', function (error) { log.error(`WebSocket Client error: ${error.toString()}`); }); }); wss.on('error', function (error) { log.error(`WebSocket server error: ${error.toString()}`); }); /** * 设备拔电的情况无法正确发送 close 事件,所以需要手动实现 * 活动检测一定不能太快,性能是一方面 * 主要还是在发送长音频时无法发送 ping 控制帧,如果时间过短会导致断连 */ const interval = setInterval(function ping() { wss.clients.forEach(function each(ws) { const bufferedAmount = ws.bufferedAmount.valueOf() if (bufferedAmount === 0 && ws.isAlive === false) { onDeviceDisConnect && onDeviceDisConnect({ device_id: ws.device_id, client_params: ws.client_params, instance: G_Instance }); log.t_info(`[${ws.device_id}] 设备掉线了,关闭连接`); return ws.terminate() } ws.isAlive = false; ws.ping(); }); }, 60 * 1000); setInterval(function () { log.info("当前客户端数量:" + wss.clients.size) }, 5 * 60 * 1000); wss.on('close', function close() { clearInterval(interval); }); const ips = getIPV4(); log.info(`---------------------------------------------------`); log.info(`- Github https://github.com/wangzongming/esp-ai`, ["bold"]); log.info(`- Website https://espai.fun`, ["bold"]); log.info(`- Server Address: (Select the correct address to copy to example.ino)`, ["bold"]); ips.forEach((ip) => { log.info(` -> ${ip}:${port}`); }) log.info(``); log.info(`客户端未自动连接时,重新为客户端上电即可!`); log.info(`---------------------------------------------------`); return wss; } catch (err) { console.log(err); log.error(`初始化服务失败`); } } module.exports = init_server;