refactor: 删除部分多余的代码和注释

This commit is contained in:
mofeng-git
2026-05-01 17:31:04 +08:00
parent 74035f8e12
commit d8e7de74a6
165 changed files with 2960 additions and 9917 deletions

View File

@@ -1,41 +1,28 @@
//! Event system for real-time state notifications
//!
//! This module provides a global event bus for broadcasting system events
//! to WebSocket clients and other subscribers.
//! Event bus: [`SystemEvent`] fan-out to WebSocket subscribers and internal tasks.
pub mod types;
use self::types::EXACT_EVENT_TOPICS;
pub use types::{
AtxDeviceInfo, AudioDeviceInfo, ClientStats, HidDeviceInfo, MsdDeviceInfo, SystemEvent,
TtydDeviceInfo, VideoDeviceInfo,
AtxDeviceInfo, AudioDeviceInfo, ClientStats, HidDeviceInfo, LedState, MsdDeviceInfo,
SystemEvent, TtydDeviceInfo, VideoDeviceInfo,
};
use tokio::sync::broadcast;
/// Event channel capacity (ring buffer size)
const EVENT_CHANNEL_CAPACITY: usize = 256;
const EXACT_TOPICS: &[&str] = &[
"stream.mode_switching",
"stream.state_changed",
"stream.config_changing",
"stream.config_applied",
"stream.device_lost",
"stream.reconnecting",
"stream.recovered",
"stream.webrtc_ready",
"stream.stats_update",
"stream.mode_changed",
"stream.mode_ready",
"webrtc.ice_candidate",
"webrtc.ice_complete",
"msd.upload_progress",
"msd.download_progress",
"system.device_info",
"error",
];
const PREFIX_TOPICS: &[&str] = &["stream.*", "webrtc.*", "msd.*", "system.*"];
fn collect_prefix_wildcards(exact: &[&'static str]) -> Vec<String> {
use std::collections::BTreeSet;
let mut segments = BTreeSet::new();
for name in exact {
if let Some((seg, _)) = name.split_once('.') {
segments.insert(seg);
}
}
segments.into_iter().map(|s| format!("{}.*", s)).collect()
}
fn make_sender() -> broadcast::Sender<SystemEvent> {
let (tx, _rx) = broadcast::channel(EVENT_CHANNEL_CAPACITY);
@@ -48,52 +35,23 @@ fn topic_prefix(event_name: &str) -> Option<String> {
.map(|(prefix, _)| format!("{}.*", prefix))
}
/// Global event bus for broadcasting system events
///
/// The event bus uses tokio's broadcast channel to distribute events
/// to multiple subscribers. Events are delivered to all active subscribers.
///
/// # Example
///
/// ```no_run
/// use one_kvm::events::{EventBus, SystemEvent};
///
/// let bus = EventBus::new();
///
/// // Publish an event
/// bus.publish(SystemEvent::StreamStateChanged {
/// state: "streaming".to_string(),
/// device: Some("/dev/video0".to_string()),
/// reason: None,
/// next_retry_ms: None,
/// });
///
/// // Subscribe to events
/// let mut rx = bus.subscribe();
/// tokio::spawn(async move {
/// while let Ok(event) = rx.recv().await {
/// println!("Received event: {:?}", event);
/// }
/// });
/// ```
pub struct EventBus {
tx: broadcast::Sender<SystemEvent>,
exact_topics: std::collections::HashMap<&'static str, broadcast::Sender<SystemEvent>>,
prefix_topics: std::collections::HashMap<&'static str, broadcast::Sender<SystemEvent>>,
prefix_topics: std::collections::HashMap<String, broadcast::Sender<SystemEvent>>,
device_info_dirty_tx: broadcast::Sender<()>,
}
impl EventBus {
/// Create a new event bus
pub fn new() -> Self {
let tx = make_sender();
let exact_topics = EXACT_TOPICS
let exact_topics = EXACT_EVENT_TOPICS
.iter()
.map(|topic| (*topic, make_sender()))
.collect();
let prefix_topics = PREFIX_TOPICS
.iter()
.map(|topic| (*topic, make_sender()))
let prefix_topics = collect_prefix_wildcards(EXACT_EVENT_TOPICS)
.into_iter()
.map(|topic| (topic, make_sender()))
.collect();
let (device_info_dirty_tx, _dirty_rx) = broadcast::channel(EVENT_CHANNEL_CAPACITY);
@@ -105,10 +63,6 @@ impl EventBus {
}
}
/// Publish an event to all subscribers
///
/// If there are no active subscribers, the event is silently dropped.
/// This is by design - events are fire-and-forget notifications.
pub fn publish(&self, event: SystemEvent) {
let event_name = event.event_name();
@@ -117,28 +71,18 @@ impl EventBus {
}
if let Some(prefix) = topic_prefix(event_name) {
if let Some(tx) = self.prefix_topics.get(prefix.as_str()) {
if let Some(tx) = self.prefix_topics.get(&prefix) {
let _ = tx.send(event.clone());
}
}
// If no subscribers, send returns Err which is normal
let _ = self.tx.send(event);
}
/// Subscribe to events
///
/// Returns a receiver that will receive all future events.
/// The receiver uses a ring buffer, so if a subscriber falls too far
/// behind, it will receive a `Lagged` error and miss some events.
pub fn subscribe(&self) -> broadcast::Receiver<SystemEvent> {
self.tx.subscribe()
}
/// Subscribe to a specific topic.
///
/// Supports exact event names, namespace wildcards like `stream.*`, and
/// `*` for the full event stream.
pub fn subscribe_topic(&self, topic: &str) -> Option<broadcast::Receiver<SystemEvent>> {
if topic == "*" {
return Some(self.tx.subscribe());
@@ -151,22 +95,14 @@ impl EventBus {
self.exact_topics.get(topic).map(|tx| tx.subscribe())
}
/// Mark the device-info snapshot as stale.
///
/// This is an internal trigger used to refresh the latest `system.device_info`
/// snapshot without exposing another public WebSocket event.
pub fn mark_device_info_dirty(&self) {
let _ = self.device_info_dirty_tx.send(());
}
/// Subscribe to internal device-info refresh triggers.
pub fn subscribe_device_info_dirty(&self) -> broadcast::Receiver<()> {
self.device_info_dirty_tx.subscribe()
}
/// Get the current number of active subscribers
///
/// Useful for monitoring and debugging.
pub fn subscriber_count(&self) -> usize {
self.tx.receiver_count()
}
@@ -263,7 +199,6 @@ mod tests {
let bus = EventBus::new();
assert_eq!(bus.subscriber_count(), 0);
// Should not panic when publishing with no subscribers
bus.publish(SystemEvent::StreamStateChanged {
state: "ready".to_string(),
device: None,

View File

@@ -1,165 +1,96 @@
//! System event types
//!
//! Defines all event types that can be broadcast through the event bus.
//! [`SystemEvent`] and device snapshot types (WebSocket / JSON).
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use crate::hid::LedState;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
pub struct LedState {
pub num_lock: bool,
pub caps_lock: bool,
pub scroll_lock: bool,
pub compose: bool,
pub kana: bool,
}
// ============================================================================
// Device Info Structures (for system.device_info event)
// ============================================================================
/// Video device information
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct VideoDeviceInfo {
/// Whether video device is available
pub available: bool,
/// Device path (e.g., /dev/video0)
pub device: Option<String>,
/// Pixel format (e.g., "MJPEG", "YUYV")
pub format: Option<String>,
/// Resolution (width, height)
pub resolution: Option<(u32, u32)>,
/// Frames per second
pub fps: u32,
/// Whether stream is currently active
pub online: bool,
/// Current streaming mode: "mjpeg", "h264", "h265", "vp8", or "vp9"
pub stream_mode: String,
/// Whether video config is currently being changed (frontend should skip mode sync)
pub config_changing: bool,
/// Error message if any, None if OK
pub error: Option<String>,
}
/// HID device information
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct HidDeviceInfo {
/// Whether HID backend is available
pub available: bool,
/// Backend type: "otg", "ch9329", "none"
pub backend: String,
/// Whether backend is initialized and ready
pub initialized: bool,
/// Whether backend is currently online
pub online: bool,
/// Whether absolute mouse positioning is supported
pub supports_absolute_mouse: bool,
/// Whether keyboard LED/status feedback is enabled.
pub keyboard_leds_enabled: bool,
/// Last known keyboard LED state.
pub led_state: LedState,
/// Device path (e.g., serial port for CH9329)
pub device: Option<String>,
/// Error message if any, None if OK
pub error: Option<String>,
/// Error code if any, None if OK
pub error_code: Option<String>,
}
/// MSD device information
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct MsdDeviceInfo {
/// Whether MSD is available
pub available: bool,
/// Operating mode: "none", "image", "drive"
pub mode: String,
/// Whether storage is connected to target
pub connected: bool,
/// Currently mounted image ID
pub image_id: Option<String>,
/// Error message if any, None if OK
pub error: Option<String>,
}
/// ATX device information
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AtxDeviceInfo {
/// Whether ATX controller is available
pub available: bool,
/// Backend type: "gpio", "usb_relay", "none"
pub backend: String,
/// Whether backend is initialized
pub initialized: bool,
/// Whether power is currently on
pub power_on: bool,
/// Error message if any, None if OK
pub error: Option<String>,
}
/// Audio device information
///
/// Note: Sample rate is fixed at 48000Hz and channels at 2 (stereo).
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AudioDeviceInfo {
/// Whether audio is enabled/available
pub available: bool,
/// Whether audio is currently streaming
pub streaming: bool,
/// Current audio device name
pub device: Option<String>,
/// Quality preset: "voice", "balanced", "high"
pub quality: String,
/// Error message if any, None if OK
pub error: Option<String>,
}
/// ttyd status information
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TtydDeviceInfo {
/// Whether ttyd binary is available
pub available: bool,
/// Whether ttyd is currently running
pub running: bool,
}
/// Per-client statistics
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ClientStats {
/// Client ID
pub id: String,
/// Current FPS for this client (frames sent in last second)
pub fps: u32,
/// Connected duration (seconds)
pub connected_secs: u64,
}
/// System event enumeration
///
/// All events are tagged with their event name for serialization.
/// The `serde(tag = "event", content = "data")` attribute creates a
/// JSON structure like:
/// ```json
/// {
/// "event": "stream.state_changed",
/// "data": { "state": "streaming", "device": "/dev/video0" }
/// }
/// ```
/// JSON: `{"event": "<name>", "data": { ... }}`.
#[derive(Clone, Debug, Serialize, Deserialize)]
#[serde(tag = "event", content = "data")]
#[allow(clippy::large_enum_variant)]
pub enum SystemEvent {
// ============================================================================
// Video Stream Events
// ============================================================================
/// Stream mode switching started (transactional, correlates all following events)
///
/// Sent immediately after a mode switch request is accepted.
/// Clients can use `transition_id` to correlate subsequent `stream.*` events.
#[serde(rename = "stream.mode_switching")]
StreamModeSwitching {
/// Unique transition ID for this mode switch transaction
transition_id: String,
/// Target mode: "mjpeg", "h264", "h265", "vp8", "vp9"
to_mode: String,
/// Previous mode: "mjpeg", "h264", "h265", "vp8", "vp9"
from_mode: String,
},
/// Stream state for the UI (`streaming`, `no_signal`, `device_lost`, `device_busy`, etc.).
/// Optional `reason` / `next_retry_ms` are hints only; branch on `state`.
#[serde(rename = "stream.state_changed")]
StreamStateChanged {
state: String,
@@ -170,193 +101,122 @@ pub enum SystemEvent {
next_retry_ms: Option<u64>,
},
/// Stream configuration is being changed
///
/// Sent before applying new configuration to notify clients that
/// the stream will be interrupted temporarily.
#[serde(rename = "stream.config_changing")]
StreamConfigChanging {
/// Optional transition ID if this config change is part of a mode switch transaction
#[serde(skip_serializing_if = "Option::is_none")]
transition_id: Option<String>,
/// Reason for change: "device_switch", "resolution_change", "format_change"
reason: String,
},
/// Stream configuration has been applied successfully
///
/// Sent after new configuration is active. Clients can reconnect now.
#[serde(rename = "stream.config_applied")]
StreamConfigApplied {
/// Optional transition ID if this config change is part of a mode switch transaction
#[serde(skip_serializing_if = "Option::is_none")]
transition_id: Option<String>,
/// Device path
device: String,
/// Resolution (width, height)
resolution: (u32, u32),
/// Pixel format: "mjpeg", "yuyv", etc.
format: String,
/// Frames per second
fps: u32,
},
/// Stream device was lost (disconnected or error)
#[serde(rename = "stream.device_lost")]
StreamDeviceLost {
/// Device path that was lost
device: String,
/// Reason for loss
reason: String,
},
StreamDeviceLost { device: String, reason: String },
/// Stream device is reconnecting
#[serde(rename = "stream.reconnecting")]
StreamReconnecting {
/// Device path being reconnected
device: String,
/// Retry attempt number
attempt: u32,
},
StreamReconnecting { device: String, attempt: u32 },
/// Stream device has recovered
#[serde(rename = "stream.recovered")]
StreamRecovered {
/// Device path that was recovered
device: String,
},
StreamRecovered { device: String },
/// WebRTC is ready to accept connections
///
/// Sent after video frame source is connected to WebRTC pipeline.
/// Clients should wait for this event before attempting to create WebRTC sessions.
#[serde(rename = "stream.webrtc_ready")]
WebRTCReady {
/// Optional transition ID if this readiness is part of a mode switch transaction
#[serde(skip_serializing_if = "Option::is_none")]
transition_id: Option<String>,
/// Current video codec
codec: String,
/// Whether hardware encoding is being used
hardware: bool,
},
/// WebRTC ICE candidate (server -> client trickle)
#[serde(rename = "webrtc.ice_candidate")]
WebRTCIceCandidate {
/// WebRTC session ID
session_id: String,
/// ICE candidate data
candidate: crate::webrtc::signaling::IceCandidate,
candidate: serde_json::Value,
},
/// WebRTC ICE gathering complete (server -> client)
#[serde(rename = "webrtc.ice_complete")]
WebRTCIceComplete {
/// WebRTC session ID
session_id: String,
},
WebRTCIceComplete { session_id: String },
/// Stream statistics update (sent periodically for client stats)
#[serde(rename = "stream.stats_update")]
StreamStatsUpdate {
/// Number of connected clients
clients: u64,
/// Per-client statistics (client_id -> client stats)
/// Each client's FPS reflects the actual frames sent in the last second
clients_stat: HashMap<String, ClientStats>,
},
/// Stream mode changed (MJPEG <-> WebRTC)
///
/// Sent when the streaming mode is switched. Clients should disconnect
/// from the current stream and reconnect using the new mode.
#[serde(rename = "stream.mode_changed")]
StreamModeChanged {
/// Optional transition ID if this change is part of a mode switch transaction
#[serde(skip_serializing_if = "Option::is_none")]
transition_id: Option<String>,
/// New mode: "mjpeg", "h264", "h265", "vp8", or "vp9"
mode: String,
/// Previous mode: "mjpeg", "h264", "h265", "vp8", or "vp9"
previous_mode: String,
},
/// Stream mode switching completed (transactional end marker)
///
/// Sent when the backend considers the new mode ready for clients to connect.
#[serde(rename = "stream.mode_ready")]
StreamModeReady {
/// Unique transition ID for this mode switch transaction
transition_id: String,
/// Active mode after switch: "mjpeg", "h264", "h265", "vp8", "vp9"
mode: String,
},
StreamModeReady { transition_id: String, mode: String },
// ============================================================================
// MSD (Mass Storage Device) Events
// ============================================================================
/// File upload progress (for large file uploads)
#[serde(rename = "msd.upload_progress")]
MsdUploadProgress {
/// Upload operation ID
upload_id: String,
/// Filename being uploaded
filename: String,
/// Bytes uploaded so far
bytes_uploaded: u64,
/// Total file size
total_bytes: u64,
/// Progress percentage (0.0 - 100.0)
progress_pct: f32,
},
/// Image download progress (for URL downloads)
#[serde(rename = "msd.download_progress")]
MsdDownloadProgress {
/// Download operation ID
download_id: String,
/// Source URL
url: String,
/// Target filename
filename: String,
/// Bytes downloaded so far
bytes_downloaded: u64,
/// Total file size (None if unknown)
total_bytes: Option<u64>,
/// Progress percentage (0.0 - 100.0, None if total unknown)
progress_pct: Option<f32>,
/// Download status: "started", "in_progress", "completed", "failed"
status: String,
},
/// Complete device information (sent on WebSocket connect and state changes)
#[serde(rename = "system.device_info")]
DeviceInfo {
/// Video device information
video: VideoDeviceInfo,
/// HID device information
hid: HidDeviceInfo,
/// MSD device information (None if MSD not enabled)
msd: Option<MsdDeviceInfo>,
/// ATX device information (None if ATX not enabled)
atx: Option<AtxDeviceInfo>,
/// Audio device information (None if audio not enabled)
audio: Option<AudioDeviceInfo>,
/// ttyd status information
ttyd: TtydDeviceInfo,
},
/// WebSocket error notification (for connection-level errors like lag)
#[serde(rename = "error")]
Error {
/// Error message
message: String,
},
Error { message: String },
}
/// One entry per [`SystemEvent::event_name`]. `EventBus` builds `*.`-wildcard channels from the first segment; names without `.` (e.g. `error`) have no wildcard channel.
pub(crate) const EXACT_EVENT_TOPICS: &[&str] = &[
"stream.mode_switching",
"stream.state_changed",
"stream.config_changing",
"stream.config_applied",
"stream.device_lost",
"stream.reconnecting",
"stream.recovered",
"stream.webrtc_ready",
"stream.stats_update",
"stream.mode_changed",
"stream.mode_ready",
"webrtc.ice_candidate",
"webrtc.ice_complete",
"msd.upload_progress",
"msd.download_progress",
"system.device_info",
"error",
];
impl SystemEvent {
/// Get the event name (for filtering/routing)
pub fn event_name(&self) -> &'static str {
match self {
Self::StreamModeSwitching { .. } => "stream.mode_switching",
@@ -378,27 +238,6 @@ impl SystemEvent {
Self::Error { .. } => "error",
}
}
/// Check if event name matches a topic pattern
///
/// Supports wildcards:
/// - `*` matches all events
/// - `stream.*` matches all stream events
/// - `stream.state_changed` matches exact event
pub fn matches_topic(&self, topic: &str) -> bool {
if topic == "*" {
return true;
}
let event_name = self.event_name();
if topic.ends_with(".*") {
let prefix = topic.trim_end_matches(".*");
event_name.starts_with(prefix)
} else {
event_name == topic
}
}
}
#[cfg(test)]
@@ -417,19 +256,124 @@ mod tests {
}
#[test]
fn test_matches_topic() {
let event = SystemEvent::StreamStateChanged {
state: "streaming".to_string(),
device: None,
reason: None,
next_retry_ms: None,
};
fn exact_topics_covers_all_variants() {
use std::collections::HashSet;
assert!(event.matches_topic("*"));
assert!(event.matches_topic("stream.*"));
assert!(event.matches_topic("stream.state_changed"));
assert!(!event.matches_topic("msd.*"));
assert!(!event.matches_topic("stream.config_changed"));
let samples = vec![
SystemEvent::StreamModeSwitching {
transition_id: String::new(),
to_mode: String::new(),
from_mode: String::new(),
},
SystemEvent::StreamStateChanged {
state: String::new(),
device: None,
reason: None,
next_retry_ms: None,
},
SystemEvent::StreamConfigChanging {
transition_id: None,
reason: String::new(),
},
SystemEvent::StreamConfigApplied {
transition_id: None,
device: String::new(),
resolution: (0, 0),
format: String::new(),
fps: 0,
},
SystemEvent::StreamDeviceLost {
device: String::new(),
reason: String::new(),
},
SystemEvent::StreamReconnecting {
device: String::new(),
attempt: 0,
},
SystemEvent::StreamRecovered {
device: String::new(),
},
SystemEvent::WebRTCReady {
transition_id: None,
codec: String::new(),
hardware: false,
},
SystemEvent::StreamStatsUpdate {
clients: 0,
clients_stat: HashMap::new(),
},
SystemEvent::StreamModeChanged {
transition_id: None,
mode: String::new(),
previous_mode: String::new(),
},
SystemEvent::StreamModeReady {
transition_id: String::new(),
mode: String::new(),
},
SystemEvent::WebRTCIceCandidate {
session_id: String::new(),
candidate: serde_json::Value::Null,
},
SystemEvent::WebRTCIceComplete {
session_id: String::new(),
},
SystemEvent::MsdUploadProgress {
upload_id: String::new(),
filename: String::new(),
bytes_uploaded: 0,
total_bytes: 0,
progress_pct: 0.0,
},
SystemEvent::MsdDownloadProgress {
download_id: String::new(),
url: String::new(),
filename: String::new(),
bytes_downloaded: 0,
total_bytes: None,
progress_pct: None,
status: String::new(),
},
SystemEvent::DeviceInfo {
video: VideoDeviceInfo {
available: false,
device: None,
format: None,
resolution: None,
fps: 0,
online: false,
stream_mode: String::new(),
config_changing: false,
error: None,
},
hid: HidDeviceInfo {
available: false,
backend: String::new(),
initialized: false,
online: false,
supports_absolute_mouse: false,
keyboard_leds_enabled: false,
led_state: LedState::default(),
device: None,
error: None,
error_code: None,
},
msd: None,
atx: None,
audio: None,
ttyd: TtydDeviceInfo {
available: false,
running: false,
},
},
SystemEvent::Error {
message: String::new(),
},
];
let from_enum: HashSet<_> = samples.iter().map(|e| e.event_name()).collect();
let from_const: HashSet<_> = super::EXACT_EVENT_TOPICS.iter().copied().collect();
assert_eq!(from_enum, from_const);
}
#[test]