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::event::{ClientEvent, NotificationLevel, ServerEvent};
use crate::net::network::{AudioSocket, Connection};
use crate::net::protocol::{ClientInfo, ControlPacket};
use crate::net::protocol::ControlPacket;
use anyhow::{Result, anyhow};
use session::handshake;
use std::net::SocketAddr;
@@ -49,24 +49,37 @@ use tokio::sync::{RwLock, broadcast};
use tokio_util::sync::CancellationToken;
/// 客户端配置
#[derive(Debug, Clone)]
pub struct ClientConfig {
/// 客户端型号
pub model: String,
/// 序列号
pub serial_number: String,
/// 版本
pub version: String,
/// 客户端认证
pub client_auth: String,
/// 服务端认证
pub server_auth: String,
/// 心跳间隔(秒)
pub heartbeat_interval: u64,
/// 连接超时(秒)
pub timeout: u64,
/// 客户端型号
pub model: String,
/// 序列号
pub serial_number: String,
}
impl Default for ClientConfig {
fn default() -> Self {
Self {
model: "Open-XiaoAi-V2".to_string(),
serial_number: "00:00:00:00:00:00".to_string(),
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()),
heartbeat_interval: 10,
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,
/// 服务端事件广播
server_events: broadcast::Sender<ServerEvent>,
/// 客户端启动时间
started_at: std::time::Instant,
}
impl Client {
@@ -94,7 +105,6 @@ impl Client {
session: RwLock::new(None),
cancel: CancellationToken::new(),
server_events,
started_at: std::time::Instant::now(),
}
}
@@ -153,13 +163,8 @@ impl Client {
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 =
handshake(&conn, audio_socket.port(), server_addr, client_info).await?;
handshake(&conn, audio_socket.port(), server_addr, &self.config).await?;
println!(
"[Client] Handshake OK, server audio at {}",
+11 -14
View File
@@ -15,7 +15,7 @@ use std::net::SocketAddr;
use std::sync::Arc;
use tokio_util::sync::CancellationToken;
use super::pipeline::PipelineHandle;
use super::{ClientConfig, pipeline::PipelineHandle};
/// 活动管道追踪
pub struct ActivePipelines {
@@ -224,20 +224,17 @@ pub async fn handshake(
conn: &Connection,
audio_port: u16,
server_addr: SocketAddr,
client_info: ClientInfo,
config: &ClientConfig,
) -> 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
conn.send(&ControlPacket::ClientHello {
auth: server_auth,
version: version.clone(),
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?;
@@ -248,10 +245,10 @@ pub async fn handshake(
udp_port,
auth,
} => {
if v != version {
return Err(anyhow!("Server version mismatch: {} != {}", v, version));
if v != config.version {
return Err(anyhow!("Server version mismatch"));
}
if auth != client_auth {
if auth != config.client_auth {
return Err(anyhow!("Invalid server auth"));
}
udp_port
+36 -13
View File
@@ -56,8 +56,36 @@ use std::net::SocketAddr;
use std::sync::Arc;
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 {
/// 配置
config: ServerConfig,
/// 会话管理器
sessions: Arc<SessionManager>,
/// 音频总线
@@ -72,10 +100,11 @@ pub struct 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?);
Ok(Self {
config,
sessions: Arc::new(SessionManager::new()),
audio_bus,
event_bus: Arc::new(ServerEventBus::default()),
@@ -216,12 +245,6 @@ impl Server {
conn: &Arc<Connection>,
addr: 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
let (info, client_audio_port) = match conn.recv().await? {
ControlPacket::ClientHello {
@@ -230,10 +253,10 @@ impl Server {
udp_port,
info,
} => {
if v != version {
return Err(anyhow!("Version mismatch: {} != {}", v, version));
if v != self.config.version {
return Err(anyhow!("Client version mismatch"));
}
if auth != server_auth {
if auth != self.config.server_auth {
return Err(anyhow!("Invalid client auth"));
}
(info, udp_port)
@@ -243,8 +266,8 @@ impl Server {
// 发送服务器 Hello
conn.send(&ControlPacket::ServerHello {
auth: client_auth,
version,
auth: self.config.client_auth.clone(),
version: self.config.version.clone(),
udp_port: self.audio_bus.port(),
})
.await?;
@@ -255,7 +278,7 @@ impl Server {
/// Session 消息循环
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 {
tokio::select! {
+3 -3
View File
@@ -30,7 +30,7 @@ impl AudioConfig {
audio_scene: AudioScene::Voice,
bitrate: 32_000,
vbr: true,
fec: true,
fec: false,
}
}
@@ -42,9 +42,9 @@ impl AudioConfig {
channels: 2,
frame_size: 960, // 20ms
audio_scene: AudioScene::Music,
bitrate: 128_000,
bitrate: 320_000,
vbr: true,
fec: true,
fec: false,
}
}
}
+5 -9
View File
@@ -11,21 +11,17 @@ pub struct AudioPlayer {
impl AudioPlayer {
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_format(Format::s16())?;
hwp.set_rate_near(config.sample_rate, alsa::ValueOr::Nearest)?;
hwp.set_channels_near(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)?;
hwp.set_channels(config.channels as u32)?;
pcm.hw_params(&hwp)?;
}
pcm.prepare()?;
pcm.prepare().context("Failed to prepare PCM")?;
Ok(Self { pcm })
}
+6 -23
View File
@@ -24,18 +24,15 @@ fn main() {
#[tokio::main]
async fn main() -> anyhow::Result<()> {
println!("╔═══════════════════════════════════════════════════════╗");
println!("║ XiaoAi Audio Client v{}", env!("CARGO_PKG_VERSION"));
println!(
"║ XiaoAi Audio Client v{}",
env!("CARGO_PKG_VERSION")
);
println!("╚═══════════════════════════════════════════════════════╝");
println!();
// 创建客户端(可以自定义配置)
let config = ClientConfig {
model: "Open-XiaoAi-Demo".to_string(),
serial_number: get_mac_address(),
heartbeat_interval: 10,
timeout: 60,
};
// 创建客户端
let config = ClientConfig::default();
let client = Arc::new(Client::new(config));
// 启动事件监听器
@@ -104,17 +101,3 @@ async fn main() -> anyhow::Result<()> {
client.shutdown();
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 xiao::app::server::Server;
use xiao::app::server::{Server, ServerConfig};
use xiao::audio::config::AudioConfig;
use xiao::net::command::Command;
use xiao::net::event::{NotificationLevel, ServerEvent};
@@ -15,11 +15,16 @@ use xiao::net::event::{NotificationLevel, ServerEvent};
#[tokio::main]
async fn main() -> anyhow::Result<()> {
println!("╔═══════════════════════════════════════════════════════╗");
println!("║ XiaoAi Audio Server v{}", env!("CARGO_PKG_VERSION"));
println!(
"║ XiaoAi Audio Server v{}",
env!("CARGO_PKG_VERSION")
);
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();
// 启动服务器
@@ -98,18 +103,6 @@ async fn main() -> anyhow::Result<()> {
.await;
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. 测试音频播放(如果有测试文件)
println!("\n6️⃣ Testing Audio Playback...");
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...");
}
// 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!("✅ All tests completed!");
println!("\nServer status:");
+35 -40
View File
@@ -16,6 +16,13 @@ pub struct ClientInfo {
pub serial_number: String,
}
/// 音频数据包 - UDP 通道传输
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct AudioPacket {
/// Opus 编码的音频数据
pub data: Vec<u8>,
}
// ==================== 控制包 ====================
/// 控制包 - TCP 通道传输的所有消息类型
@@ -23,7 +30,10 @@ pub struct ClientInfo {
pub enum ControlPacket {
// ========== 服务发现 ==========
/// 服务发现广播
Discovery { protocol: String, port: u16 },
Discovery {
protocol: String,
port: u16,
},
// ========== 握手 ==========
/// 服务端握手
@@ -46,9 +56,15 @@ pub enum ControlPacket {
// ========== RPC ==========
/// RPC 请求(新版,使用 Command 类型)
RpcRequest { id: u32, command: Command },
RpcRequest {
id: u32,
command: Command,
},
/// RPC 响应(新版,使用 CommandResult 类型)
RpcResponse { id: u32, result: CommandResult },
RpcResponse {
id: u32,
result: CommandResult,
},
// ========== 事件 ==========
/// 服务端事件推送
@@ -56,48 +72,27 @@ pub enum ControlPacket {
/// 客户端事件推送
ClientEvent(ClientEvent),
// ========== 订阅管理 ==========
/// 订阅事件类型
Subscribe {
event_types: Vec<String>,
},
/// 取消订阅
Unsubscribe {
event_types: Vec<String>,
},
// ========== 音频控制 ==========
/// 开始录音
StartRecording { config: AudioConfig },
StartRecording {
config: AudioConfig,
},
/// 停止录音
StopRecording,
/// 开始播放
StartPlayback { config: AudioConfig },
StartPlayback {
config: AudioConfig,
},
/// 停止播放
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),
}
}
#[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(_)));
}
}