chore: polish code
This commit is contained in:
@@ -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 {}",
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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! {
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 })
|
||||
}
|
||||
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
@@ -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:");
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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(_)));
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user