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
JavaScript
/**
* 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;