Files
open-xiaoai/packages/client-v2/src/app/client/mod.rs
T

256 lines
8.4 KiB
Rust

#![cfg(target_os = "linux")]
mod audio_manager;
use crate::net::discovery::Discovery;
use crate::net::network::{AudioSocket, Connection};
use crate::net::protocol::{ClientInfo, ControlPacket, RpcResult};
use crate::net::rpc::RpcManager;
use anyhow::{Result, anyhow};
use audio_manager::ClientAudioManager;
use std::net::SocketAddr;
use std::sync::Arc;
use tokio::sync::{RwLock};
use tokio_util::sync::CancellationToken;
struct ActiveSession {
conn: Arc<Connection>,
audio_manager: ClientAudioManager,
session_cancel: CancellationToken,
}
pub struct Client {
session: RwLock<Option<Arc<ActiveSession>>>,
rpc: Arc<RpcManager>,
}
impl Client {
pub fn new() -> Self {
Self {
session: RwLock::new(None),
rpc: Arc::new(RpcManager::new()),
}
}
pub async fn run(self: Arc<Self>) -> Result<()> {
loop {
println!("Searching for server...");
let (ip, tcp_port) = match Discovery::listen().await {
Ok(res) => res,
Err(e) => {
eprintln!("Discovery listen error: {}", e);
tokio::time::sleep(std::time::Duration::from_secs(1)).await;
continue;
}
};
let addr = SocketAddr::new(ip, tcp_port);
println!("Found server at {}", addr);
match tokio::net::TcpStream::connect(addr).await {
Err(e) => eprintln!("Failed to connect to {}: {}", addr, e),
Ok(stream) => {
println!("Connected to TCP server at {}", addr);
if let Err(e) = self.clone().handle_session(stream, addr).await {
eprintln!("Session error: {:?}", e);
}
}
}
self.cleanup().await;
tokio::time::sleep(std::time::Duration::from_secs(1)).await;
}
}
async fn cleanup(&self) {
let mut session_guard = self.session.write().await;
if let Some(s) = session_guard.take() {
s.session_cancel.cancel();
}
}
async fn handle_session(
self: Arc<Self>,
stream: tokio::net::TcpStream,
addr: SocketAddr,
) -> Result<()> {
let conn = Arc::new(Connection::new(stream)?);
let audio_socket = Arc::new(AudioSocket::bind().await?);
// --- 握手 (Handshake) ---
let version = env!("CARGO_PKG_VERSION").to_string();
let server_auth =
std::env::var("XIAO_SERVER_AUTH").unwrap_or_else(|_| "xiao-server".to_string());
let client_auth =
std::env::var("XIAO_CLIENT_AUTH").unwrap_or_else(|_| "xiao-client".to_string());
conn.send(&ControlPacket::ClientHello {
auth: server_auth,
version: version.clone(),
udp_port: audio_socket.port(),
// todo client info
info: ClientInfo {
model: "Open-XiaoAi-V2".to_string(),
serial_number: "00:00:00:00:00:00".to_string(),
},
})
.await?;
let server_udp_port = match conn.recv().await? {
ControlPacket::ServerHello {
version: v,
udp_port,
auth,
} => {
if v != version {
return Err(anyhow!("Server version mismatch: {} != {}", v, version));
}
if auth != client_auth {
return Err(anyhow!("Invalid server auth"));
}
udp_port
}
_ => return Err(anyhow!("Handshake failed")),
};
let audio_addr = SocketAddr::new(addr.ip(), server_udp_port);
println!(
"Handshake successful with {}, audio at {}",
addr, audio_addr
);
// --- 初始化 Session ---
let session_cancel = CancellationToken::new();
let session = Arc::new(ActiveSession {
conn: conn.clone(),
audio_manager: ClientAudioManager::new(
audio_socket,
audio_addr,
session_cancel.clone(),
),
session_cancel,
});
*self.session.write().await = Some(session.clone());
// 心跳
let hb_session = session.clone();
tokio::spawn(async move {
let mut interval = tokio::time::interval(std::time::Duration::from_secs(10));
loop {
tokio::select! {
_ = hb_session.session_cancel.cancelled() => break,
_ = interval.tick() => {
if hb_session.conn.send(&ControlPacket::Ping).await.is_err() { break; }
}
}
}
});
// 消息主循环
loop {
tokio::select! {
_ = session.session_cancel.cancelled() => break,
res = tokio::time::timeout(std::time::Duration::from_secs(60), conn.recv()) => {
match res {
Ok(Ok(packet)) => {
if let Err(e) = self.process_packet(packet, &session).await {
eprintln!("Process packet error: {}", e);
}
}
Ok(Err(e)) => {
return Err(anyhow!("Connection receive error: {}", e));
}
Err(_) => return Err(anyhow!("Connection timeout")),
}
}
}
}
Ok(())
}
// todo 考虑有些操作比较耗时,需要非阻塞处理
async fn process_packet(
&self,
packet: ControlPacket,
session: &Arc<ActiveSession>,
) -> Result<()> {
match packet {
ControlPacket::Ping => {
session.conn.send(&ControlPacket::Pong).await?;
}
ControlPacket::Pong => {}
ControlPacket::RpcResponse { id, result } => {
self.rpc.resolve(id, result);
}
ControlPacket::RpcRequest { id, method, args } => {
let result = self.handle_rpc(&method, args).await;
session
.conn
.send(&ControlPacket::RpcResponse { id, result })
.await?;
}
ControlPacket::StartRecording { config } => {
session.audio_manager.start_recording(config).await;
}
ControlPacket::StartPlayback { config } => {
session.audio_manager.start_playback(config).await;
}
ControlPacket::StopRecording => {
session.audio_manager.stop_recorder().await;
}
ControlPacket::StopPlayback => {
session.audio_manager.stop_player().await;
}
_ => {}
}
Ok(())
}
async fn handle_rpc(&self, method: &str, args: Vec<String>) -> RpcResult {
match method {
"shell" if !args.is_empty() => {
let output = std::process::Command::new("sh")
.arg("-c")
.arg(&args[0])
.output();
match output {
Ok(out) => RpcResult {
stdout: String::from_utf8_lossy(&out.stdout).to_string(),
stderr: String::from_utf8_lossy(&out.stderr).to_string(),
code: out.status.code().unwrap_or(0),
},
Err(e) => RpcResult {
stderr: e.to_string(),
code: -1,
..Default::default()
},
}
}
_ => RpcResult {
stderr: "Unsupported method".to_string(),
code: -1,
..Default::default()
},
}
}
pub async fn call(&self, method: &str, args: Vec<String>) -> Result<RpcResult> {
let (id, rx) = self.rpc.register();
let session_guard = self.session.read().await;
if let Some(session) = session_guard.as_ref() {
session
.conn
.send(&ControlPacket::RpcRequest {
id,
method: method.to_string(),
args,
})
.await?;
Ok(rx.await?)
} else {
Err(anyhow!("Not connected"))
}
}
}