chore: polish code

This commit is contained in:
Del Wang
2026-01-04 15:06:55 +08:00
parent cce3dd630a
commit d88d63b15b
9 changed files with 137 additions and 159 deletions
+21 -16
View File
@@ -40,7 +40,7 @@ use crate::net::command::{
use crate::net::discovery::Discovery; use crate::net::discovery::Discovery;
use crate::net::event::{ClientEvent, NotificationLevel, ServerEvent}; use crate::net::event::{ClientEvent, NotificationLevel, ServerEvent};
use crate::net::network::{AudioSocket, Connection}; use crate::net::network::{AudioSocket, Connection};
use crate::net::protocol::{ClientInfo, ControlPacket}; use crate::net::protocol::ControlPacket;
use anyhow::{Result, anyhow}; use anyhow::{Result, anyhow};
use session::handshake; use session::handshake;
use std::net::SocketAddr; use std::net::SocketAddr;
@@ -49,24 +49,37 @@ use tokio::sync::{RwLock, broadcast};
use tokio_util::sync::CancellationToken; use tokio_util::sync::CancellationToken;
/// 客户端配置 /// 客户端配置
#[derive(Debug, Clone)]
pub struct ClientConfig { pub struct ClientConfig {
/// 客户端型号 /// 版本
pub model: String, pub version: String,
/// 序列号 /// 客户端认证
pub serial_number: String, pub client_auth: String,
/// 服务端认证
pub server_auth: String,
/// 心跳间隔(秒) /// 心跳间隔(秒)
pub heartbeat_interval: u64, pub heartbeat_interval: u64,
/// 连接超时(秒) /// 连接超时(秒)
pub timeout: u64, pub timeout: u64,
/// 客户端型号
pub model: String,
/// 序列号
pub serial_number: String,
} }
impl Default for ClientConfig { impl Default for ClientConfig {
fn default() -> Self { fn default() -> Self {
Self { Self {
model: "Open-XiaoAi-V2".to_string(), version: env!("CARGO_PKG_VERSION").to_string(),
serial_number: "00:00:00:00:00:00".to_string(), server_auth: std::env::var("XIAO_SERVER_AUTH")
.unwrap_or_else(|_| "xiao-server".to_string()),
client_auth: std::env::var("XIAO_CLIENT_AUTH")
.unwrap_or_else(|_| "xiao-client".to_string()),
heartbeat_interval: 10, heartbeat_interval: 10,
timeout: 60, timeout: 60,
// todo 获取设备信息
model: "Open-XiaoAi-V2".to_string(),
serial_number: "00:00:00:00:00:00".to_string(),
} }
} }
} }
@@ -81,8 +94,6 @@ pub struct Client {
cancel: CancellationToken, cancel: CancellationToken,
/// 服务端事件广播 /// 服务端事件广播
server_events: broadcast::Sender<ServerEvent>, server_events: broadcast::Sender<ServerEvent>,
/// 客户端启动时间
started_at: std::time::Instant,
} }
impl Client { impl Client {
@@ -94,7 +105,6 @@ impl Client {
session: RwLock::new(None), session: RwLock::new(None),
cancel: CancellationToken::new(), cancel: CancellationToken::new(),
server_events, server_events,
started_at: std::time::Instant::now(),
} }
} }
@@ -153,13 +163,8 @@ impl Client {
let audio_socket = Arc::new(AudioSocket::bind().await?); let audio_socket = Arc::new(AudioSocket::bind().await?);
// 执行握手 // 执行握手
let client_info = ClientInfo {
model: self.config.model.clone(),
serial_number: self.config.serial_number.clone(),
};
let handshake_result = let handshake_result =
handshake(&conn, audio_socket.port(), server_addr, client_info).await?; handshake(&conn, audio_socket.port(), server_addr, &self.config).await?;
println!( println!(
"[Client] Handshake OK, server audio at {}", "[Client] Handshake OK, server audio at {}",
+11 -14
View File
@@ -15,7 +15,7 @@ use std::net::SocketAddr;
use std::sync::Arc; use std::sync::Arc;
use tokio_util::sync::CancellationToken; use tokio_util::sync::CancellationToken;
use super::pipeline::PipelineHandle; use super::{ClientConfig, pipeline::PipelineHandle};
/// 活动管道追踪 /// 活动管道追踪
pub struct ActivePipelines { pub struct ActivePipelines {
@@ -224,20 +224,17 @@ pub async fn handshake(
conn: &Connection, conn: &Connection,
audio_port: u16, audio_port: u16,
server_addr: SocketAddr, server_addr: SocketAddr,
client_info: ClientInfo, config: &ClientConfig,
) -> Result<HandshakeResult> { ) -> Result<HandshakeResult> {
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());
// 发送 ClientHello // 发送 ClientHello
conn.send(&ControlPacket::ClientHello { conn.send(&ControlPacket::ClientHello {
auth: server_auth,
version: version.clone(),
udp_port: audio_port, udp_port: audio_port,
info: client_info, auth: config.server_auth.clone(),
version: config.version.clone(),
info: ClientInfo {
model: config.model.clone(),
serial_number: config.serial_number.clone(),
},
}) })
.await?; .await?;
@@ -248,10 +245,10 @@ pub async fn handshake(
udp_port, udp_port,
auth, auth,
} => { } => {
if v != version { if v != config.version {
return Err(anyhow!("Server version mismatch: {} != {}", v, version)); return Err(anyhow!("Server version mismatch"));
} }
if auth != client_auth { if auth != config.client_auth {
return Err(anyhow!("Invalid server auth")); return Err(anyhow!("Invalid server auth"));
} }
udp_port udp_port
+36 -13
View File
@@ -56,8 +56,36 @@ use std::net::SocketAddr;
use std::sync::Arc; use std::sync::Arc;
use tokio_util::sync::CancellationToken; use tokio_util::sync::CancellationToken;
/// 服务端配置
#[derive(Debug, Clone)]
pub struct ServerConfig {
/// 版本
pub version: String,
/// 客户端认证
pub client_auth: String,
/// 服务端认证
pub server_auth: String,
/// 连接超时(秒)
pub timeout: u64,
}
impl Default for ServerConfig {
fn default() -> Self {
Self {
version: env!("CARGO_PKG_VERSION").to_string(),
server_auth: std::env::var("XIAO_SERVER_AUTH")
.unwrap_or_else(|_| "xiao-server".to_string()),
client_auth: std::env::var("XIAO_CLIENT_AUTH")
.unwrap_or_else(|_| "xiao-client".to_string()),
timeout: 60,
}
}
}
/// 实时音频服务器 /// 实时音频服务器
pub struct Server { pub struct Server {
/// 配置
config: ServerConfig,
/// 会话管理器 /// 会话管理器
sessions: Arc<SessionManager>, sessions: Arc<SessionManager>,
/// 音频总线 /// 音频总线
@@ -72,10 +100,11 @@ pub struct Server {
impl Server { impl Server {
/// 创建新服务器 /// 创建新服务器
pub async fn new() -> Result<Self> { pub async fn new(config: ServerConfig) -> Result<Self> {
let audio_bus = Arc::new(AudioBus::new().await?); let audio_bus = Arc::new(AudioBus::new().await?);
Ok(Self { Ok(Self {
config,
sessions: Arc::new(SessionManager::new()), sessions: Arc::new(SessionManager::new()),
audio_bus, audio_bus,
event_bus: Arc::new(ServerEventBus::default()), event_bus: Arc::new(ServerEventBus::default()),
@@ -216,12 +245,6 @@ impl Server {
conn: &Arc<Connection>, conn: &Arc<Connection>,
addr: SocketAddr, addr: SocketAddr,
) -> Result<(crate::net::protocol::ClientInfo, SocketAddr)> { ) -> Result<(crate::net::protocol::ClientInfo, SocketAddr)> {
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());
// 等待客户端 Hello // 等待客户端 Hello
let (info, client_audio_port) = match conn.recv().await? { let (info, client_audio_port) = match conn.recv().await? {
ControlPacket::ClientHello { ControlPacket::ClientHello {
@@ -230,10 +253,10 @@ impl Server {
udp_port, udp_port,
info, info,
} => { } => {
if v != version { if v != self.config.version {
return Err(anyhow!("Version mismatch: {} != {}", v, version)); return Err(anyhow!("Client version mismatch"));
} }
if auth != server_auth { if auth != self.config.server_auth {
return Err(anyhow!("Invalid client auth")); return Err(anyhow!("Invalid client auth"));
} }
(info, udp_port) (info, udp_port)
@@ -243,8 +266,8 @@ impl Server {
// 发送服务器 Hello // 发送服务器 Hello
conn.send(&ControlPacket::ServerHello { conn.send(&ControlPacket::ServerHello {
auth: client_auth, auth: self.config.client_auth.clone(),
version, version: self.config.version.clone(),
udp_port: self.audio_bus.port(), udp_port: self.audio_bus.port(),
}) })
.await?; .await?;
@@ -255,7 +278,7 @@ impl Server {
/// Session 消息循环 /// Session 消息循环
async fn session_loop(&self, session: Arc<Session>) -> Result<()> { async fn session_loop(&self, session: Arc<Session>) -> Result<()> {
let timeout = std::time::Duration::from_secs(60); let timeout = std::time::Duration::from_secs(self.config.timeout);
loop { loop {
tokio::select! { tokio::select! {
+3 -3
View File
@@ -30,7 +30,7 @@ impl AudioConfig {
audio_scene: AudioScene::Voice, audio_scene: AudioScene::Voice,
bitrate: 32_000, bitrate: 32_000,
vbr: true, vbr: true,
fec: true, fec: false,
} }
} }
@@ -42,9 +42,9 @@ impl AudioConfig {
channels: 2, channels: 2,
frame_size: 960, // 20ms frame_size: 960, // 20ms
audio_scene: AudioScene::Music, audio_scene: AudioScene::Music,
bitrate: 128_000, bitrate: 320_000,
vbr: true, vbr: true,
fec: true, fec: false,
} }
} }
} }
+5 -9
View File
@@ -11,21 +11,17 @@ pub struct AudioPlayer {
impl AudioPlayer { impl AudioPlayer {
pub fn new(config: &AudioConfig) -> Result<Self> { pub fn new(config: &AudioConfig) -> Result<Self> {
let pcm = PCM::new(&config.playback_device, Direction::Playback, false)?; let pcm = PCM::new(&config.playback_device, Direction::Playback, false)
.context("Failed to open playback PCM device")?;
{ {
let hwp = HwParams::any(&pcm)?; let hwp = HwParams::any(&pcm).context("Failed to get HwParams")?;
hwp.set_access(Access::RWInterleaved)?; hwp.set_access(Access::RWInterleaved)?;
hwp.set_format(Format::s16())?; hwp.set_format(Format::s16())?;
hwp.set_rate_near(config.sample_rate, alsa::ValueOr::Nearest)?; hwp.set_rate_near(config.sample_rate, alsa::ValueOr::Nearest)?;
hwp.set_channels_near(config.channels as u32)?; hwp.set_channels(config.channels as u32)?;
// 100ms buffer to prevent underruns
let buffer_size = (config.sample_rate as f64 * 0.1) as u32;
hwp.set_buffer_size_near(buffer_size as alsa::pcm::Frames)?;
pcm.hw_params(&hwp)?; pcm.hw_params(&hwp)?;
} }
pcm.prepare()?; pcm.prepare().context("Failed to prepare PCM")?;
Ok(Self { pcm }) Ok(Self { pcm })
} }
+6 -23
View File
@@ -24,18 +24,15 @@ fn main() {
#[tokio::main] #[tokio::main]
async fn main() -> anyhow::Result<()> { async fn main() -> anyhow::Result<()> {
println!("╔═══════════════════════════════════════════════════════╗"); println!("╔═══════════════════════════════════════════════════════╗");
println!("║ XiaoAi Audio Client v{}", env!("CARGO_PKG_VERSION")); println!(
"║ XiaoAi Audio Client v{}",
env!("CARGO_PKG_VERSION")
);
println!("╚═══════════════════════════════════════════════════════╝"); println!("╚═══════════════════════════════════════════════════════╝");
println!(); println!();
// 创建客户端(可以自定义配置) // 创建客户端
let config = ClientConfig { let config = ClientConfig::default();
model: "Open-XiaoAi-Demo".to_string(),
serial_number: get_mac_address(),
heartbeat_interval: 10,
timeout: 60,
};
let client = Arc::new(Client::new(config)); let client = Arc::new(Client::new(config));
// 启动事件监听器 // 启动事件监听器
@@ -104,17 +101,3 @@ async fn main() -> anyhow::Result<()> {
client.shutdown(); client.shutdown();
Ok(()) Ok(())
} }
#[cfg(target_os = "linux")]
fn get_mac_address() -> String {
// 尝试获取 MAC 地址
if let Ok(output) = std::process::Command::new("cat")
.arg("/sys/class/net/eth0/address")
.output()
{
if output.status.success() {
return String::from_utf8_lossy(&output.stdout).trim().to_string();
}
}
"00:00:00:00:00:00".to_string()
}
+20 -15
View File
@@ -7,7 +7,7 @@
//! - 事件广播 //! - 事件广播
use std::sync::Arc; use std::sync::Arc;
use xiao::app::server::Server; use xiao::app::server::{Server, ServerConfig};
use xiao::audio::config::AudioConfig; use xiao::audio::config::AudioConfig;
use xiao::net::command::Command; use xiao::net::command::Command;
use xiao::net::event::{NotificationLevel, ServerEvent}; use xiao::net::event::{NotificationLevel, ServerEvent};
@@ -15,11 +15,16 @@ use xiao::net::event::{NotificationLevel, ServerEvent};
#[tokio::main] #[tokio::main]
async fn main() -> anyhow::Result<()> { async fn main() -> anyhow::Result<()> {
println!("╔═══════════════════════════════════════════════════════╗"); println!("╔═══════════════════════════════════════════════════════╗");
println!("║ XiaoAi Audio Server v{}", env!("CARGO_PKG_VERSION")); println!(
"║ XiaoAi Audio Server v{}",
env!("CARGO_PKG_VERSION")
);
println!("╚═══════════════════════════════════════════════════════╝"); println!("╚═══════════════════════════════════════════════════════╝");
println!(); println!();
let server = Arc::new(Server::new().await?); // 创建服务端
let config = ServerConfig::default();
let server = Arc::new(Server::new(config).await?);
let s = server.clone(); let s = server.clone();
// 启动服务器 // 启动服务器
@@ -98,18 +103,6 @@ async fn main() -> anyhow::Result<()> {
.await; .await;
println!(" ✅ Event broadcasted"); println!(" ✅ Event broadcasted");
// 5. 测试音频录制
println!("\n5️⃣ Testing Audio Recording (5 seconds)...");
match server.start_record(addr, AudioConfig::voice_16k()).await {
Ok(_) => {
println!(" ⏺️ Recording started...");
tokio::time::sleep(std::time::Duration::from_secs(5)).await;
server.stop_record(addr).await?;
println!(" ⏹️ Recording stopped");
}
Err(e) => println!(" ❌ Recording failed: {}", e),
}
// 6. 测试音频播放(如果有测试文件) // 6. 测试音频播放(如果有测试文件)
println!("\n6️⃣ Testing Audio Playback..."); println!("\n6️⃣ Testing Audio Playback...");
if std::path::Path::new("temp/test.wav").exists() { if std::path::Path::new("temp/test.wav").exists() {
@@ -126,6 +119,18 @@ async fn main() -> anyhow::Result<()> {
println!(" ⚠️ No test file found at temp/test.wav, skipping..."); println!(" ⚠️ No test file found at temp/test.wav, skipping...");
} }
// 5. 测试音频录制
println!("\n5️⃣ Testing Audio Recording (5 seconds)...");
match server.start_record(addr, AudioConfig::voice_16k()).await {
Ok(_) => {
println!(" ⏺️ Recording started...");
tokio::time::sleep(std::time::Duration::from_secs(5)).await;
server.stop_record(addr).await?;
println!(" ⏹️ Recording stopped");
}
Err(e) => println!(" ❌ Recording failed: {}", e),
}
println!("\n═══════════════════════════════════════════════════════"); println!("\n═══════════════════════════════════════════════════════");
println!("✅ All tests completed!"); println!("✅ All tests completed!");
println!("\nServer status:"); println!("\nServer status:");
+35 -40
View File
@@ -16,6 +16,13 @@ pub struct ClientInfo {
pub serial_number: String, pub serial_number: String,
} }
/// 音频数据包 - UDP 通道传输
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct AudioPacket {
/// Opus 编码的音频数据
pub data: Vec<u8>,
}
// ==================== 控制包 ==================== // ==================== 控制包 ====================
/// 控制包 - TCP 通道传输的所有消息类型 /// 控制包 - TCP 通道传输的所有消息类型
@@ -23,7 +30,10 @@ pub struct ClientInfo {
pub enum ControlPacket { pub enum ControlPacket {
// ========== 服务发现 ========== // ========== 服务发现 ==========
/// 服务发现广播 /// 服务发现广播
Discovery { protocol: String, port: u16 }, Discovery {
protocol: String,
port: u16,
},
// ========== 握手 ========== // ========== 握手 ==========
/// 服务端握手 /// 服务端握手
@@ -46,9 +56,15 @@ pub enum ControlPacket {
// ========== RPC ========== // ========== RPC ==========
/// RPC 请求(新版,使用 Command 类型) /// RPC 请求(新版,使用 Command 类型)
RpcRequest { id: u32, command: Command }, RpcRequest {
id: u32,
command: Command,
},
/// RPC 响应(新版,使用 CommandResult 类型) /// RPC 响应(新版,使用 CommandResult 类型)
RpcResponse { id: u32, result: CommandResult }, RpcResponse {
id: u32,
result: CommandResult,
},
// ========== 事件 ========== // ========== 事件 ==========
/// 服务端事件推送 /// 服务端事件推送
@@ -56,48 +72,27 @@ pub enum ControlPacket {
/// 客户端事件推送 /// 客户端事件推送
ClientEvent(ClientEvent), ClientEvent(ClientEvent),
// ========== 订阅管理 ==========
/// 订阅事件类型
Subscribe {
event_types: Vec<String>,
},
/// 取消订阅
Unsubscribe {
event_types: Vec<String>,
},
// ========== 音频控制 ========== // ========== 音频控制 ==========
/// 开始录音 /// 开始录音
StartRecording { config: AudioConfig }, StartRecording {
config: AudioConfig,
},
/// 停止录音 /// 停止录音
StopRecording, StopRecording,
/// 开始播放 /// 开始播放
StartPlayback { config: AudioConfig }, StartPlayback {
config: AudioConfig,
},
/// 停止播放 /// 停止播放
StopPlayback, StopPlayback,
// ========== 订阅管理 ==========
/// 订阅事件类型
Subscribe { event_types: Vec<String> },
/// 取消订阅
Unsubscribe { event_types: Vec<String> },
}
// ==================== 音频包 ====================
/// 音频数据包 - UDP 通道传输
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct AudioPacket {
/// Opus 编码的音频数据
pub data: Vec<u8>,
}
// ==================== 兼容性:旧版 RPC 结果 ====================
/// 旧版 RPC 结果(保持向后兼容)
#[derive(Serialize, Deserialize, Debug, Clone, Default)]
pub struct RpcResult {
pub stdout: String,
pub stderr: String,
pub code: i32,
}
impl From<crate::net::command::ShellResponse> for RpcResult {
fn from(resp: crate::net::command::ShellResponse) -> Self {
Self {
stdout: resp.stdout,
stderr: resp.stderr,
code: resp.exit_code,
}
}
} }
-26
View File
@@ -139,29 +139,3 @@ where
Err(_) => Err(RpcError::Timeout), Err(_) => Err(RpcError::Timeout),
} }
} }
#[cfg(test)]
mod tests {
use super::*;
use crate::net::command::{CommandResult, ShellResponse};
#[tokio::test]
async fn test_rpc_manager() {
let mgr = RpcManager::new();
let (id, rx) = mgr.register();
assert_eq!(mgr.pending_count(), 1);
let result = CommandResult::Shell(ShellResponse {
stdout: "hello".to_string(),
stderr: String::new(),
exit_code: 0,
});
mgr.resolve(id, result.clone());
assert_eq!(mgr.pending_count(), 0);
let received = rx.await.unwrap();
assert!(matches!(received, CommandResult::Shell(_)));
}
}