init: ShowenV2 项目骨架 — 数字生命窗口平台

- core/ 跨平台内核骨架 (Plugin trait, Message, ServiceManager, Config)
- plugins/ 空桩 (video, http, ble, screen, wifi)
- PROGRESS.md 进度跟踪, TEAM.md 团队档案
- cargo check 零 warning 通过

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
showen
2026-03-12 05:03:58 +08:00
commit 23f4d46287
21 changed files with 2223 additions and 0 deletions

365
src/core/config.rs Normal file
View File

@@ -0,0 +1,365 @@
use anyhow::{bail, Context, Result};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::fs;
use std::path::{Path, PathBuf};
pub type Config = AppConfig;
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct AppConfig {
pub display: DisplayConfig,
pub playlist: Vec<VideoItem>,
pub transition: TransitionConfig,
pub playback: PlaybackConfig,
pub scenes: ScenesConfig,
pub remote_control: RemoteControlConfig,
#[serde(default)]
pub ble: BleConfig,
#[serde(skip)]
pub source_path: PathBuf,
#[serde(skip)]
pub source_dir: PathBuf,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct DisplayConfig {
pub fullscreen: bool,
pub window_title: String,
pub rotation: i32,
pub flip_horizontal: bool,
pub flip_vertical: bool,
#[serde(default)]
pub offset_x: i32,
#[serde(default)]
pub offset_y: i32,
#[serde(default)]
pub prevent_screen_lock: bool,
#[serde(default = "default_render_width")]
pub render_width: i32,
#[serde(default = "default_render_height")]
pub render_height: i32,
#[serde(default)]
pub output_width: Option<i32>,
#[serde(default)]
pub output_height: Option<i32>,
#[serde(default)]
pub scale_mode: ScaleMode,
#[serde(default = "default_allow_upscale")]
pub allow_upscale: bool,
pub perspective_correction: PerspectiveCorrectionConfig,
#[serde(default)]
pub chroma_key: ChromaKeyConfig,
#[serde(default)]
pub brightness_adjust: BrightnessAdjustConfig,
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Default)]
#[serde(rename_all = "lowercase")]
pub enum ScaleMode {
#[default]
Fit,
Stretch,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct PerspectiveCorrectionConfig {
pub enabled: bool,
pub points: Vec<[i32; 2]>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ChromaKeyConfig {
#[serde(default)]
pub enabled: bool,
#[serde(default = "default_hsv_min")]
pub hsv_min: [i32; 3],
#[serde(default = "default_hsv_max")]
pub hsv_max: [i32; 3],
#[serde(default)]
pub invert: bool,
#[serde(default)]
pub feather: i32,
}
impl Default for ChromaKeyConfig {
fn default() -> Self {
Self {
enabled: false,
hsv_min: default_hsv_min(),
hsv_max: default_hsv_max(),
invert: false,
feather: 0,
}
}
}
fn default_hsv_min() -> [i32; 3] {
[0, 0, 200]
}
fn default_hsv_max() -> [i32; 3] {
[180, 30, 255]
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct BrightnessAdjustConfig {
#[serde(default)]
pub enabled: bool,
#[serde(default = "default_subject_boost")]
pub subject_boost: f64,
#[serde(default = "default_background_suppress")]
pub background_suppress: f64,
#[serde(default = "default_brightness_threshold")]
pub threshold: i32,
}
impl Default for BrightnessAdjustConfig {
fn default() -> Self {
Self {
enabled: false,
subject_boost: default_subject_boost(),
background_suppress: default_background_suppress(),
threshold: default_brightness_threshold(),
}
}
}
fn default_subject_boost() -> f64 { 1.5 }
fn default_background_suppress() -> f64 { 0.3 }
fn default_brightness_threshold() -> i32 { 30 }
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct VideoItem {
pub id: String,
pub path: String,
pub duration: Option<f64>,
#[serde(default = "default_loop_count")]
pub loop_count: i32,
#[serde(default)]
pub random_loop_range: Option<[i32; 2]>,
}
fn default_loop_count() -> i32 { 1 }
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "lowercase")]
pub enum TransitionType {
Fade,
Cut,
None,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct TransitionConfig {
pub enabled: bool,
#[serde(rename = "type")]
pub transition_type: TransitionType,
pub duration: f64,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct PlaybackConfig {
pub loop_playlist: bool,
pub auto_start: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ScenesConfig {
#[serde(default)]
pub rest: Vec<VideoItem>,
#[serde(default)]
pub active: Vec<VideoItem>,
#[serde(default)]
pub sleep: Vec<VideoItem>,
#[serde(default)]
pub interact: Vec<VideoItem>,
#[serde(default)]
pub state_machine: Option<StateMachineConfig>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct StateMachineConfig {
pub initial_state: String,
pub states: HashMap<String, StateConfig>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct StateConfig {
pub name: String,
#[serde(default)]
pub mode: StateMode,
pub sequence: Vec<AnimationStep>,
#[serde(default)]
pub next_state: Option<String>,
#[serde(default)]
pub next_states: Option<Vec<NextStateEntry>>,
#[serde(default)]
pub transitions: Vec<StateTransition>,
#[serde(default = "default_weight")]
pub weight: f32,
#[serde(default)]
pub defer_triggers: bool,
#[serde(default)]
pub ignore_triggers: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct NextStateEntry {
pub state: String,
#[serde(default = "default_weight")]
pub weight: f32,
}
fn default_weight() -> f32 { 1.0 }
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Default)]
#[serde(rename_all = "snake_case")]
pub enum StateMode {
#[default]
FreeMode,
InteractiveMode,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AnimationStep {
pub video_id: String,
#[serde(default)]
pub loop_count: Option<i32>,
#[serde(default)]
pub random_loop_range: Option<[i32; 2]>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct StateTransition {
pub trigger: TriggerType,
pub target_state: String,
#[serde(default)]
pub priority: i32,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum TriggerType {
Button { name: String },
Voice { keyword: String },
Sensor { name: String },
Timer { seconds: f64 },
Random { probability: f64 },
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct RemoteControlConfig {
pub enabled: bool,
pub host: String,
pub port: u16,
}
/// BLE 配网配置(新增)
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct BleConfig {
#[serde(default = "default_ble_enabled")]
pub enabled: bool,
#[serde(default = "default_ble_device_name")]
pub device_name: String,
}
impl Default for BleConfig {
fn default() -> Self {
Self {
enabled: default_ble_enabled(),
device_name: default_ble_device_name(),
}
}
}
fn default_ble_enabled() -> bool { true }
fn default_ble_device_name() -> String { "showen".to_string() }
// ── 加载与验证 ──
impl AppConfig {
pub fn from_file<P: AsRef<Path>>(path: P) -> Result<Self> {
let path = path.as_ref();
let raw = fs::read_to_string(path)
.with_context(|| format!("读取配置文件失败: {}", path.display()))?;
let mut config: AppConfig = serde_json::from_str(&raw)
.with_context(|| format!("解析配置 JSON 失败: {}", path.display()))?;
config.set_source_path(path)?;
config.validate()?;
Ok(config)
}
fn set_source_path(&mut self, source_path: &Path) -> Result<()> {
if source_path.as_os_str().is_empty() {
bail!("配置文件路径不能为空");
}
self.source_path = source_path
.canonicalize()
.unwrap_or_else(|_| absolute_from_current_dir(source_path));
self.source_dir = self
.source_path
.parent()
.map(Path::to_path_buf)
.unwrap_or_else(|| PathBuf::from("."));
Ok(())
}
pub fn validate(&self) -> Result<()> {
// 基础验证 — 完整验证在 Commit 2 补全
if self.playlist.is_empty() {
bail!("playlist 不能为空");
}
Ok(())
}
pub fn validate_paths(&self) -> Result<()> {
for item in &self.playlist {
let full_path = self.resolve_media_path(item);
if !full_path.exists() {
bail!(
"视频文件 '{}' 不存在: {} (相对于 {})",
item.id, full_path.display(), self.source_path.display()
);
}
}
Ok(())
}
pub fn resolve_media_path(&self, item: &VideoItem) -> PathBuf {
let media_path = Path::new(&item.path);
if media_path.is_absolute() {
media_path.to_path_buf()
} else {
self.source_dir.join(media_path)
}
}
}
pub fn parse_str(raw: &str, source_path: impl Into<PathBuf>) -> Result<AppConfig> {
let source_path = source_path.into();
let mut config: AppConfig = serde_json::from_str(raw).context("解析配置 JSON 失败")?;
config.set_source_path(&source_path)?;
config.validate()?;
Ok(config)
}
fn absolute_from_current_dir(path: &Path) -> PathBuf {
if path.is_absolute() {
return path.to_path_buf();
}
std::env::current_dir()
.map(|cd| cd.join(path))
.unwrap_or_else(|_| path.to_path_buf())
}
fn default_render_width() -> i32 { 1024 }
fn default_render_height() -> i32 { 1024 }
fn default_allow_upscale() -> bool { true }

75
src/core/message.rs Normal file
View File

@@ -0,0 +1,75 @@
use crate::core::config::AppConfig;
use std::sync::Arc;
/// 消息信封:包含来源、目的地、消息体
pub struct Envelope {
pub from: &'static str,
pub to: Destination,
pub message: Message,
}
/// 消息目的地
pub enum Destination {
/// 点对点发送给指定插件
Plugin(&'static str),
/// 广播给所有插件
Broadcast,
/// 发给管理层自身
Manager,
}
/// 所有插件间通信的类型安全消息
pub enum Message {
// ── 播放控制 ──
PlayerCommand(PlayerCommand),
PlayerStatus(PlayerStatusData),
Trigger { name: String, value: String },
StateChanged { old_state: String, new_state: String },
// ── 屏幕管理 ──
ScreenLockRequest(bool),
CursorVisibility(bool),
// ── 网络 ──
WifiCommand(WifiCommand),
WifiResult(String),
WifiProvisioned { ssid: String, ip: String },
// ── 配置 ──
ConfigReloaded(Arc<AppConfig>),
ConfigReloadRequest,
// ── 系统 ──
Shutdown,
PluginReady(&'static str),
// ── 扩展(未来插件用) ──
Custom { kind: String, payload: String },
}
pub enum PlayerCommand {
Play,
Pause,
Next,
Previous,
Goto(usize),
ChangeScene(String),
}
#[derive(Debug, Clone, serde::Serialize)]
pub struct PlayerStatusData {
pub running: bool,
pub paused: bool,
pub in_transition: bool,
pub current_index: usize,
pub playlist_length: usize,
pub current_video: Option<String>,
}
pub enum WifiCommand {
Scan,
Connect { ssid: String, password: String },
Status,
ApStart { ssid: String, password: String },
ApStop,
}

4
src/core/mod.rs Normal file
View File

@@ -0,0 +1,4 @@
pub mod config;
pub mod message;
pub mod plugin;
pub mod service_manager;

47
src/core/plugin.rs Normal file
View File

@@ -0,0 +1,47 @@
use crate::core::message::{Envelope, Message};
use anyhow::Result;
use std::sync::{mpsc, Arc};
use crate::core::config::AppConfig;
/// 所有功能都通过实现此 trait 接入系统
pub trait Plugin: Send {
/// 唯一标识 (如 "video", "http", "ble")
fn id(&self) -> &'static str;
/// 插件信息
fn info(&self) -> PluginInfo;
/// 初始化:获取发送通道,声明订阅的消息类型
fn init(&mut self, ctx: PluginContext) -> Result<()>;
/// 启动(可在此 spawn 线程/任务)
fn start(&mut self) -> Result<()>;
/// 接收来自其他插件或管理层的消息
fn handle_message(&mut self, msg: Message) -> Result<()>;
/// 优雅停止
fn stop(&mut self) -> Result<()>;
}
pub struct PluginInfo {
pub name: &'static str,
pub version: &'static str,
pub description: &'static str,
pub platform: Platform,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Platform {
Any,
Linux,
LinuxArm64,
}
/// 传给插件的上下文
pub struct PluginContext {
/// 向管理层发消息
pub tx: mpsc::Sender<Envelope>,
/// 共享配置(只读)
pub config: Arc<AppConfig>,
}

135
src/core/service_manager.rs Normal file
View File

@@ -0,0 +1,135 @@
use crate::core::config::AppConfig;
use crate::core::message::{Destination, Envelope, Message};
use crate::core::plugin::{Plugin, PluginContext};
use anyhow::Result;
use std::sync::{mpsc, Arc};
/// 中央调度器:插件注册、生命周期管理、消息路由
pub struct ServiceManager {
plugins: Vec<Box<dyn Plugin>>,
config: Arc<AppConfig>,
tx: mpsc::Sender<Envelope>,
rx: mpsc::Receiver<Envelope>,
}
impl ServiceManager {
pub fn new(config: AppConfig) -> Self {
let (tx, rx) = mpsc::channel();
Self {
plugins: Vec::new(),
config: Arc::new(config),
tx,
rx,
}
}
/// 注册插件
pub fn register(&mut self, plugin: Box<dyn Plugin>) {
println!("[ServiceManager] 注册插件: {}", plugin.id());
self.plugins.push(plugin);
}
/// 按注册顺序 init() + start() 所有插件
pub fn start_all(&mut self) -> Result<()> {
// init
for plugin in &mut self.plugins {
let ctx = PluginContext {
tx: self.tx.clone(),
config: Arc::clone(&self.config),
};
println!("[ServiceManager] 初始化插件: {}", plugin.id());
plugin.init(ctx)?;
}
// start
for plugin in &mut self.plugins {
println!("[ServiceManager] 启动插件: {}", plugin.id());
plugin.start()?;
}
Ok(())
}
/// 主消息循环(阻塞)
pub fn run(&mut self) -> Result<()> {
println!("[ServiceManager] 进入主消息循环");
loop {
let envelope = match self.rx.recv() {
Ok(env) => env,
Err(_) => {
println!("[ServiceManager] 所有发送端已关闭,退出");
break;
}
};
match envelope.to {
Destination::Plugin(id) => {
if let Some(plugin) = self.plugins.iter_mut().find(|p| p.id() == id) {
if let Err(e) = plugin.handle_message(envelope.message) {
eprintln!("[ServiceManager] 插件 '{}' 处理消息失败: {}", id, e);
}
} else {
eprintln!("[ServiceManager] 目标插件 '{}' 不存在", id);
}
}
Destination::Broadcast => {
let from = envelope.from;
for plugin in &mut self.plugins {
// 不回送给发送者
if plugin.id() == from {
continue;
}
// Broadcast 需要重建 MessageMessage 不是 Clone
// 对于 Broadcast 我们跳过非 Shutdown 消息的深拷贝问题
// 实际实现中 Shutdown 是最关键的广播消息
}
// 处理 Shutdown
if matches!(envelope.message, Message::Shutdown) {
println!("[ServiceManager] 收到 Shutdown 广播");
break;
}
}
Destination::Manager => {
self.handle_manager_message(envelope.message)?;
}
}
}
self.stop_all()
}
/// 逆序 stop() 所有插件
pub fn stop_all(&mut self) -> Result<()> {
println!("[ServiceManager] 停止所有插件");
for plugin in self.plugins.iter_mut().rev() {
println!("[ServiceManager] 停止插件: {}", plugin.id());
if let Err(e) = plugin.stop() {
eprintln!("[ServiceManager] 停止插件 '{}' 失败: {}", plugin.id(), e);
}
}
Ok(())
}
/// 处理发给管理层自身的消息
fn handle_manager_message(&mut self, msg: Message) -> Result<()> {
match msg {
Message::Shutdown => {
println!("[ServiceManager] 收到 Shutdown 指令");
// 通过返回 Err 来退出 run 循环不合适,用标志位
// 实际上 run() 中已经 break 了
}
Message::ConfigReloadRequest => {
println!("[ServiceManager] 收到配置重载请求");
// TODO: 重载配置并广播 ConfigReloaded
}
Message::PluginReady(id) => {
println!("[ServiceManager] 插件 '{}' 就绪", id);
}
_ => {}
}
Ok(())
}
/// 获取发送通道的克隆(供外部使用)
pub fn sender(&self) -> mpsc::Sender<Envelope> {
self.tx.clone()
}
}

9
src/lib.rs Normal file
View File

@@ -0,0 +1,9 @@
//! ShowenV2 — 数字生命窗口平台
//!
//! 跨平台插件架构,支持全息/VR/AR/屏幕显示,
//! 承载虚拟宠物、数字人、AI歌姬、3D模型等数字生命内容。
//!
//! 核心理念:平台不关心内容是什么,插件决定一切。
pub mod core;
pub mod plugins;

29
src/main.rs Normal file
View File

@@ -0,0 +1,29 @@
use anyhow::Result;
use showen_v2::core::config::AppConfig;
use showen_v2::core::service_manager::ServiceManager;
fn main() -> Result<()> {
let config_path = std::env::args()
.nth(1)
.unwrap_or_else(|| "configs/dog_state_machine.json".to_string());
println!("ShowenV2 — 数字生命窗口平台");
println!("加载配置: {}", config_path);
let config = AppConfig::from_file(&config_path)?;
config.validate_paths()?;
let mut manager = ServiceManager::new(config);
// TODO: 按平台注册插件 (Commit 8)
// manager.register(Box::new(VideoPlugin::new()));
// manager.register(Box::new(HttpPlugin::new()));
// manager.register(Box::new(BlePlugin::new()));
// manager.register(Box::new(ScreenPlugin::new()));
// manager.register(Box::new(WifiPlugin::new()));
manager.start_all()?;
manager.run()?;
Ok(())
}

40
src/plugins/ble/mod.rs Normal file
View File

@@ -0,0 +1,40 @@
//! BlePlugin — BLE 配网服务
//!
//! 通过 D-Bus 与 BlueZ 交互,注册 GATT 服务和 LE Advertisement。
//! 含 LocalName 双连接修复。
use crate::core::message::Message;
use crate::core::plugin::{Plugin, PluginContext, PluginInfo, Platform};
use anyhow::Result;
pub struct BlePlugin {
ctx: Option<PluginContext>,
}
impl BlePlugin {
pub fn new() -> Self {
Self { ctx: None }
}
}
impl Plugin for BlePlugin {
fn id(&self) -> &'static str { "ble" }
fn info(&self) -> PluginInfo {
PluginInfo {
name: "BLE Provisioning",
version: "0.2.0",
description: "BLE GATT WiFi 配网 (D-Bus BlueZ)",
platform: Platform::Linux,
}
}
fn init(&mut self, ctx: PluginContext) -> Result<()> {
self.ctx = Some(ctx);
Ok(())
}
fn start(&mut self) -> Result<()> { Ok(()) }
fn handle_message(&mut self, _msg: Message) -> Result<()> { Ok(()) }
fn stop(&mut self) -> Result<()> { Ok(()) }
}

39
src/plugins/http/mod.rs Normal file
View File

@@ -0,0 +1,39 @@
//! HttpPlugin — Web UI + REST API
//!
//! 基于 warp 的 HTTP 服务,提供播放控制、配置管理、视频管理等 API。
use crate::core::message::Message;
use crate::core::plugin::{Plugin, PluginContext, PluginInfo, Platform};
use anyhow::Result;
pub struct HttpPlugin {
ctx: Option<PluginContext>,
}
impl HttpPlugin {
pub fn new() -> Self {
Self { ctx: None }
}
}
impl Plugin for HttpPlugin {
fn id(&self) -> &'static str { "http" }
fn info(&self) -> PluginInfo {
PluginInfo {
name: "HTTP API",
version: "0.2.0",
description: "Web UI + REST API (warp)",
platform: Platform::Any,
}
}
fn init(&mut self, ctx: PluginContext) -> Result<()> {
self.ctx = Some(ctx);
Ok(())
}
fn start(&mut self) -> Result<()> { Ok(()) }
fn handle_message(&mut self, _msg: Message) -> Result<()> { Ok(()) }
fn stop(&mut self) -> Result<()> { Ok(()) }
}

5
src/plugins/mod.rs Normal file
View File

@@ -0,0 +1,5 @@
pub mod video;
pub mod http;
pub mod ble;
pub mod screen;
pub mod wifi;

39
src/plugins/screen/mod.rs Normal file
View File

@@ -0,0 +1,39 @@
//! ScreenPlugin — 屏幕管理
//!
//! 唤醒锁systemd-inhibit、光标隐藏unclutter
use crate::core::message::Message;
use crate::core::plugin::{Plugin, PluginContext, PluginInfo, Platform};
use anyhow::Result;
pub struct ScreenPlugin {
ctx: Option<PluginContext>,
}
impl ScreenPlugin {
pub fn new() -> Self {
Self { ctx: None }
}
}
impl Plugin for ScreenPlugin {
fn id(&self) -> &'static str { "screen" }
fn info(&self) -> PluginInfo {
PluginInfo {
name: "Screen Manager",
version: "0.2.0",
description: "屏幕唤醒锁 + 光标管理",
platform: Platform::Linux,
}
}
fn init(&mut self, ctx: PluginContext) -> Result<()> {
self.ctx = Some(ctx);
Ok(())
}
fn start(&mut self) -> Result<()> { Ok(()) }
fn handle_message(&mut self, _msg: Message) -> Result<()> { Ok(()) }
fn stop(&mut self) -> Result<()> { Ok(()) }
}

54
src/plugins/video/mod.rs Normal file
View File

@@ -0,0 +1,54 @@
//! VideoPlugin — 视频播放引擎
//!
//! 基于 OpenCV 的视频播放,支持状态机驱动、帧变换、过渡效果。
//! Phase 1 核心:迁移旧 video_processor.rs + state_machine.rs
pub mod processor;
pub mod state_machine;
use crate::core::message::Message;
use crate::core::plugin::{Plugin, PluginContext, PluginInfo, Platform};
use anyhow::Result;
pub struct VideoPlugin {
ctx: Option<PluginContext>,
}
impl VideoPlugin {
pub fn new() -> Self {
Self { ctx: None }
}
}
impl Plugin for VideoPlugin {
fn id(&self) -> &'static str { "video" }
fn info(&self) -> PluginInfo {
PluginInfo {
name: "Video Player",
version: "0.2.0",
description: "视频播放引擎 (OpenCV)",
platform: Platform::Any,
}
}
fn init(&mut self, ctx: PluginContext) -> Result<()> {
self.ctx = Some(ctx);
Ok(())
}
fn start(&mut self) -> Result<()> {
// TODO: Commit 4 实现
Ok(())
}
fn handle_message(&mut self, _msg: Message) -> Result<()> {
// TODO: Commit 4 实现
Ok(())
}
fn stop(&mut self) -> Result<()> {
// TODO: Commit 4 实现
Ok(())
}
}

View File

@@ -0,0 +1 @@
// VideoProcessor + VideoTransformer — 待 Commit 4 迁移

View File

@@ -0,0 +1 @@
// StateMachine — 待 Commit 4 迁移

39
src/plugins/wifi/mod.rs Normal file
View File

@@ -0,0 +1,39 @@
//! WifiPlugin — WiFi 管理
//!
//! 通过 nmcli 实现 WiFi 扫描、连接、AP 热点。
use crate::core::message::Message;
use crate::core::plugin::{Plugin, PluginContext, PluginInfo, Platform};
use anyhow::Result;
pub struct WifiPlugin {
ctx: Option<PluginContext>,
}
impl WifiPlugin {
pub fn new() -> Self {
Self { ctx: None }
}
}
impl Plugin for WifiPlugin {
fn id(&self) -> &'static str { "wifi" }
fn info(&self) -> PluginInfo {
PluginInfo {
name: "WiFi Manager",
version: "0.2.0",
description: "WiFi 管理 (nmcli)",
platform: Platform::Linux,
}
}
fn init(&mut self, ctx: PluginContext) -> Result<()> {
self.ctx = Some(ctx);
Ok(())
}
fn start(&mut self) -> Result<()> { Ok(()) }
fn handle_message(&mut self, _msg: Message) -> Result<()> { Ok(()) }
fn stop(&mut self) -> Result<()> { Ok(()) }
}