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:
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
target/
|
||||
1122
Cargo.lock
generated
Normal file
1122
Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load Diff
22
Cargo.toml
Normal file
22
Cargo.toml
Normal file
@@ -0,0 +1,22 @@
|
||||
[package]
|
||||
name = "showen_v2"
|
||||
version = "0.2.0"
|
||||
authors = ["showen"]
|
||||
edition = "2018"
|
||||
|
||||
[dependencies]
|
||||
anyhow = "1"
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
serde_json = "1"
|
||||
rand = "0.8"
|
||||
|
||||
# 跨平台插件依赖
|
||||
opencv = { version = "0.66", default-features = false, features = ["highgui", "imgproc", "videoio"] }
|
||||
tokio = { version = "1", features = ["macros", "rt-multi-thread", "process", "sync"] }
|
||||
warp = { version = "0.3.7", default-features = false, features = ["multipart"] }
|
||||
bytes = "1"
|
||||
futures-util = "0.3"
|
||||
|
||||
# Linux 特有插件依赖
|
||||
dbus = "0.9"
|
||||
dbus-crossroads = "0.5"
|
||||
147
PROGRESS.md
Normal file
147
PROGRESS.md
Normal file
@@ -0,0 +1,147 @@
|
||||
# ShowenV2 — 数字生命窗口平台
|
||||
|
||||
## 愿景
|
||||
ShowenV2 不仅是全息宠物播放器,而是一个**通用数字生命窗口平台**。
|
||||
|
||||
支持的显示模式:
|
||||
- **全息显示** — 半透镜 45° 伪全息(当前硬件)
|
||||
- **VR** — 头显输出(未来)
|
||||
- **AR** — 增强现实叠加(未来)
|
||||
- **直接屏幕** — 普通显示器/手机/平板
|
||||
|
||||
支持的内容类型:
|
||||
- **宠物动画** — 视频状态机驱动的虚拟宠物(当前核心)
|
||||
- **3D 模型** — 实时渲染 3D 角色/物体
|
||||
- **数字人** — AI 驱动的虚拟形象
|
||||
- **AI 歌姬** — 人工歌姬/虚拟歌手
|
||||
- **未来内容** — 通过插件无限扩展
|
||||
|
||||
核心理念:**平台不关心内容是什么,插件决定一切**。
|
||||
|
||||
---
|
||||
|
||||
## 项目信息
|
||||
- 旧项目: `/home/showen/Showen/hologram_player_rust/` (单体全息宠物播放器)
|
||||
- 新项目: `/home/showen/Showen/ShowenV2/`
|
||||
- 架构: 跨平台插件内核 + 功能插件
|
||||
|
||||
---
|
||||
|
||||
## 架构概览
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────┐
|
||||
│ main.rs │
|
||||
│ 加载配置 → 按平台注册插件 → ServiceManager.run() │
|
||||
├─────────────────────────────────────────────────────┤
|
||||
│ core/ (跨平台内核,零业务逻辑) │
|
||||
│ ServiceManager — 插件注册/生命周期/消息路由 │
|
||||
│ Plugin trait — 统一插件接口 │
|
||||
│ Message enum — 类型安全的消息协议 │
|
||||
│ Config — 配置解析/验证(纯 serde) │
|
||||
├─────────────────────────────────────────────────────┤
|
||||
│ plugins/ (一切皆插件) │
|
||||
│ │
|
||||
│ ┌─ 渲染引擎 ─────────────────────────────────┐ │
|
||||
│ │ video/ — 视频播放 (OpenCV, 当前核心) │ │
|
||||
│ │ render/ — 3D渲染引擎 (未来: wgpu/vulkan) │ │
|
||||
│ │ avatar/ — 数字人驱动 (未来: Live2D等) │ │
|
||||
│ └────────────────────────────────────────────┘ │
|
||||
│ │
|
||||
│ ┌─ 显示后端 ─────────────────────────────────┐ │
|
||||
│ │ screen/ — 屏幕管理 (X11/fbv/唤醒锁/光标) │ │
|
||||
│ │ vr/ — VR 输出 (未来: OpenXR) │ │
|
||||
│ │ ar/ — AR 叠加 (未来) │ │
|
||||
│ └────────────────────────────────────────────┘ │
|
||||
│ │
|
||||
│ ┌─ 连接/交互 ────────────────────────────────┐ │
|
||||
│ │ http/ — Web UI + REST API (warp) │ │
|
||||
│ │ ble/ — BLE 配网 (dbus BlueZ) │ │
|
||||
│ │ wifi/ — WiFi 管理 (nmcli) │ │
|
||||
│ │ voice/ — 语音交互 (未来: ASR/TTS) │ │
|
||||
│ │ sensor/ — 传感器输入 (未来: GPIO/触摸) │ │
|
||||
│ └────────────────────────────────────────────┘ │
|
||||
│ │
|
||||
│ ┌─ AI 引擎 ──────────────────────────────────┐ │
|
||||
│ │ ai/ — LLM 对话 (未来) │ │
|
||||
│ │ singer/ — AI 歌姬 (未来: 歌声合成) │ │
|
||||
│ └────────────────────────────────────────────┘ │
|
||||
└─────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
**关键**: 内核 core/ 完全不知道"宠物""歌姬""3D模型"的存在。
|
||||
它只知道"插件发消息、收消息、有生命周期"。
|
||||
内容类型、显示方式、交互模式全部由插件组合决定。
|
||||
|
||||
---
|
||||
|
||||
## 当前实现范围 (Phase 1)
|
||||
|
||||
Phase 1 的目标:**完整迁移旧功能到新架构**,确保在 ARM 设备上运行正常。
|
||||
|
||||
| 插件 | 来源 | 平台 | 状态 |
|
||||
|------|------|------|------|
|
||||
| core/ | 新建 | Any | 进行中 |
|
||||
| plugins/video/ | video_processor.rs + state_machine.rs | OpenCV | 待迁移 |
|
||||
| plugins/http/ | api_server.rs | warp | 待迁移 |
|
||||
| plugins/ble/ | ble_service.rs | Linux dbus | 待迁移 |
|
||||
| plugins/screen/ | screen_wake_lock.rs | Linux | 待迁移 |
|
||||
| plugins/wifi/ | api_server.rs WiFi部分 | Linux nmcli | 待迁移 |
|
||||
|
||||
未来 Phase 的插件 (render/, avatar/, vr/, ar/, voice/, ai/, singer/) 只需定义接口预留,不实现。
|
||||
|
||||
---
|
||||
|
||||
## 提交计划
|
||||
|
||||
### Commit 1: 项目骨架 + 愿景文档
|
||||
- [x] PROGRESS.md 平台愿景与架构
|
||||
- [x] Cargo.toml, rust-toolchain.toml
|
||||
- [ ] core/plugin.rs — Plugin trait + PluginInfo + PluginContext + Platform
|
||||
- [ ] core/message.rs — Message/Envelope/Destination (通用消息,不限于宠物)
|
||||
- [ ] core/service_manager.rs — ServiceManager 骨架
|
||||
- [ ] core/config.rs — AppConfig 类型定义
|
||||
- [ ] core/mod.rs, lib.rs
|
||||
- [ ] plugins/ 空桩 (video, http, ble, screen, wifi)
|
||||
- [ ] main.rs 入口
|
||||
- 验证: `cargo check` 通过
|
||||
|
||||
### Commit 2: 配置系统完整实现
|
||||
- [ ] core/config.rs 完整验证逻辑
|
||||
- [ ] configs/dog_state_machine.json, cat_state_machine.json
|
||||
|
||||
### Commit 3: ServiceManager 消息路由
|
||||
- [ ] register(), start_all(), run(), stop_all()
|
||||
- [ ] mpsc 通道消息循环 + Broadcast 支持
|
||||
|
||||
### Commit 4: VideoPlugin 视频播放
|
||||
- [ ] plugins/video/processor.rs (VideoTransformer + VideoProcessor)
|
||||
- [ ] plugins/video/state_machine.rs (StateMachine)
|
||||
- [ ] plugins/video/mod.rs (Plugin trait impl)
|
||||
|
||||
### Commit 5: HttpPlugin HTTP API
|
||||
- [ ] plugins/http/routes.rs (warp 路由)
|
||||
- [ ] plugins/http/mod.rs (Plugin trait impl)
|
||||
- [ ] 内嵌 Web UI HTML
|
||||
|
||||
### Commit 6: BlePlugin (含 LocalName 修复)
|
||||
- [ ] plugins/ble/gatt.rs — 双 D-Bus 连接修复
|
||||
- [ ] plugins/ble/mod.rs (Plugin trait impl)
|
||||
|
||||
### Commit 7: ScreenPlugin + WifiPlugin
|
||||
- [ ] plugins/screen/mod.rs (唤醒锁+光标)
|
||||
- [ ] plugins/wifi/mod.rs (nmcli)
|
||||
|
||||
### Commit 8: 集成 main.rs + 编译验证
|
||||
- [ ] 串联所有插件
|
||||
- [ ] cargo build --release
|
||||
|
||||
---
|
||||
|
||||
## 关键决策记录
|
||||
1. **Rust edition 2018** — 兼容设备 stable toolchain
|
||||
2. **std::sync::mpsc** 消息传递 — VideoPlugin 在阻塞线程运行,不能全异步
|
||||
3. **BLE 双连接修复** — conn_server 处理回调, conn_client 同步注册
|
||||
4. **Message 枚举通用化** — 不绑定特定内容类型,Custom 变体支持未来插件
|
||||
5. **Platform 枚举** — 插件声明自己适用的平台,main.rs 按运行时平台选择
|
||||
6. **项目名 ShowenV2** — 不叫 hologram_player,因为不止全息
|
||||
47
TEAM.md
Normal file
47
TEAM.md
Normal file
@@ -0,0 +1,47 @@
|
||||
# ShowenV2 开发团队
|
||||
|
||||
## CEO / 技术总监
|
||||
- **姓名**: 陈逸飞 (Claude)
|
||||
- **角色**: CEO 兼技术总监,架构设计,代码审核,协调所有团队成员
|
||||
- **模型**: Claude Opus 4.6
|
||||
- **职责**: 总体架构决策、代码审核、任务分配、进度管理、最终集成
|
||||
|
||||
## 核心开发者 (GPT-5.4 团队)
|
||||
|
||||
### 1. 张明远 — 内核工程师
|
||||
- **代号**: kernel-zhang
|
||||
- **专长**: Rust 系统编程、插件架构、消息路由
|
||||
- **负责模块**: core/ (ServiceManager, Plugin trait, Message)
|
||||
- **背景**: 前 Linux 内核开发者,精通 Rust 并发编程和系统设计
|
||||
|
||||
### 2. 李思琪 — 视频引擎工程师
|
||||
- **代号**: video-li
|
||||
- **专长**: OpenCV、视频处理、状态机
|
||||
- **负责模块**: plugins/video/ (VideoProcessor, VideoTransformer, StateMachine)
|
||||
- **背景**: 计算机视觉方向硕士,有嵌入式视频处理经验
|
||||
|
||||
### 3. 王浩然 — 网络服务工程师
|
||||
- **代号**: net-wang
|
||||
- **专长**: warp/tokio HTTP 服务、BLE D-Bus、WiFi nmcli
|
||||
- **负责模块**: plugins/http/, plugins/ble/, plugins/wifi/
|
||||
- **背景**: 物联网全栈开发者,精通蓝牙协议栈和网络编程
|
||||
|
||||
### 4. 赵雨薇 — 前端 & 屏幕工程师
|
||||
- **代号**: ui-zhao
|
||||
- **专长**: Web UI、Linux 显示管理、用户体验
|
||||
- **负责模块**: plugins/screen/, Web UI HTML/JS
|
||||
- **背景**: 嵌入式 UI 开发者,熟悉 X11/Wayland 和响应式 Web 设计
|
||||
|
||||
---
|
||||
|
||||
## 工作流程
|
||||
1. CEO (陈逸飞) 分配任务给团队成员
|
||||
2. 团队成员通过 kilo run 执行任务,产出代码文件
|
||||
3. CEO 审核产出,合格则 git commit,不合格则反馈修改
|
||||
4. 每个 commit 前更新 PROGRESS.md
|
||||
5. 团队成员之间通过 Message 文件传递信息
|
||||
|
||||
## 通信机制
|
||||
- 任务下发: CEO → kilo run -m openai/gpt-5.4 (带上下文)
|
||||
- 产出回收: kilo 输出 → CEO 审核 → git commit
|
||||
- 团队讨论: 通过 TEAM_CHAT.md 记录讨论要点
|
||||
2
rust-toolchain.toml
Normal file
2
rust-toolchain.toml
Normal file
@@ -0,0 +1,2 @@
|
||||
[toolchain]
|
||||
channel = "stable"
|
||||
365
src/core/config.rs
Normal file
365
src/core/config.rs
Normal 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
75
src/core/message.rs
Normal 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
4
src/core/mod.rs
Normal 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
47
src/core/plugin.rs
Normal 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
135
src/core/service_manager.rs
Normal 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 需要重建 Message(Message 不是 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
9
src/lib.rs
Normal file
@@ -0,0 +1,9 @@
|
||||
//! ShowenV2 — 数字生命窗口平台
|
||||
//!
|
||||
//! 跨平台插件架构,支持全息/VR/AR/屏幕显示,
|
||||
//! 承载虚拟宠物、数字人、AI歌姬、3D模型等数字生命内容。
|
||||
//!
|
||||
//! 核心理念:平台不关心内容是什么,插件决定一切。
|
||||
|
||||
pub mod core;
|
||||
pub mod plugins;
|
||||
29
src/main.rs
Normal file
29
src/main.rs
Normal 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
40
src/plugins/ble/mod.rs
Normal 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
39
src/plugins/http/mod.rs
Normal 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
5
src/plugins/mod.rs
Normal 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
39
src/plugins/screen/mod.rs
Normal 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
54
src/plugins/video/mod.rs
Normal 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(())
|
||||
}
|
||||
}
|
||||
1
src/plugins/video/processor.rs
Normal file
1
src/plugins/video/processor.rs
Normal file
@@ -0,0 +1 @@
|
||||
// VideoProcessor + VideoTransformer — 待 Commit 4 迁移
|
||||
1
src/plugins/video/state_machine.rs
Normal file
1
src/plugins/video/state_machine.rs
Normal file
@@ -0,0 +1 @@
|
||||
// StateMachine — 待 Commit 4 迁移
|
||||
39
src/plugins/wifi/mod.rs
Normal file
39
src/plugins/wifi/mod.rs
Normal 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(()) }
|
||||
}
|
||||
Reference in New Issue
Block a user