feat: config验证 + StateMachine + WifiPlugin + ScreenPlugin
团队交付 Phase 1 第一轮: - 张明远: config.rs 完整验证逻辑 (Display/VideoItem/Transition/Scenes/StateMachine) - 李思琪: state_machine.rs 完整实现 (defer/ignore triggers, 加权随机, loop range) - 王浩然: wifi/mod.rs WiFi管理插件 (scan/connect/status/ap via nmcli) - 赵雨薇: screen/mod.rs 屏幕管理插件 (systemd-inhibit唤醒锁 + unclutter光标) cargo check 零 warning 通过。 Co-Authored-By: GPT-5.4 <noreply@openai.com> Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,6 +1,6 @@
|
|||||||
use anyhow::{bail, Context, Result};
|
use anyhow::{bail, Context, Result};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use std::collections::HashMap;
|
use std::collections::{HashMap, HashSet};
|
||||||
use std::fs;
|
use std::fs;
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
|
|
||||||
@@ -128,9 +128,15 @@ impl Default for BrightnessAdjustConfig {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn default_subject_boost() -> f64 { 1.5 }
|
fn default_subject_boost() -> f64 {
|
||||||
fn default_background_suppress() -> f64 { 0.3 }
|
1.5
|
||||||
fn default_brightness_threshold() -> i32 { 30 }
|
}
|
||||||
|
fn default_background_suppress() -> f64 {
|
||||||
|
0.3
|
||||||
|
}
|
||||||
|
fn default_brightness_threshold() -> i32 {
|
||||||
|
30
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
#[serde(deny_unknown_fields)]
|
#[serde(deny_unknown_fields)]
|
||||||
@@ -144,7 +150,9 @@ pub struct VideoItem {
|
|||||||
pub random_loop_range: Option<[i32; 2]>,
|
pub random_loop_range: Option<[i32; 2]>,
|
||||||
}
|
}
|
||||||
|
|
||||||
fn default_loop_count() -> i32 { 1 }
|
fn default_loop_count() -> i32 {
|
||||||
|
1
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||||
#[serde(rename_all = "lowercase")]
|
#[serde(rename_all = "lowercase")]
|
||||||
@@ -217,7 +225,9 @@ pub struct NextStateEntry {
|
|||||||
pub weight: f32,
|
pub weight: f32,
|
||||||
}
|
}
|
||||||
|
|
||||||
fn default_weight() -> f32 { 1.0 }
|
fn default_weight() -> f32 {
|
||||||
|
1.0
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Default)]
|
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Default)]
|
||||||
#[serde(rename_all = "snake_case")]
|
#[serde(rename_all = "snake_case")]
|
||||||
@@ -280,8 +290,12 @@ impl Default for BleConfig {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn default_ble_enabled() -> bool { true }
|
fn default_ble_enabled() -> bool {
|
||||||
fn default_ble_device_name() -> String { "showen".to_string() }
|
true
|
||||||
|
}
|
||||||
|
fn default_ble_device_name() -> String {
|
||||||
|
"showen".to_string()
|
||||||
|
}
|
||||||
|
|
||||||
// ── 加载与验证 ──
|
// ── 加载与验证 ──
|
||||||
|
|
||||||
@@ -313,10 +327,24 @@ impl AppConfig {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn validate(&self) -> Result<()> {
|
pub fn validate(&self) -> Result<()> {
|
||||||
// 基础验证 — 完整验证在 Commit 2 补全
|
self.display.validate()?;
|
||||||
|
|
||||||
if self.playlist.is_empty() {
|
if self.playlist.is_empty() {
|
||||||
bail!("playlist 不能为空");
|
bail!("playlist 不能为空");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let mut playlist_ids = HashSet::new();
|
||||||
|
for (index, item) in self.playlist.iter().enumerate() {
|
||||||
|
item.validate(&format!("playlist[{index}]"))?;
|
||||||
|
if !playlist_ids.insert(item.id.as_str()) {
|
||||||
|
bail!("playlist[{index}] id '{}' 重复", item.id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
self.transition.validate()?;
|
||||||
|
self.scenes.validate(&playlist_ids)?;
|
||||||
|
self.remote_control.validate()?;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -326,7 +354,9 @@ impl AppConfig {
|
|||||||
if !full_path.exists() {
|
if !full_path.exists() {
|
||||||
bail!(
|
bail!(
|
||||||
"视频文件 '{}' 不存在: {} (相对于 {})",
|
"视频文件 '{}' 不存在: {} (相对于 {})",
|
||||||
item.id, full_path.display(), self.source_path.display()
|
item.id,
|
||||||
|
full_path.display(),
|
||||||
|
self.source_path.display()
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -360,6 +390,268 @@ fn absolute_from_current_dir(path: &Path) -> PathBuf {
|
|||||||
.unwrap_or_else(|_| path.to_path_buf())
|
.unwrap_or_else(|_| path.to_path_buf())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn default_render_width() -> i32 { 1024 }
|
fn default_render_width() -> i32 {
|
||||||
fn default_render_height() -> i32 { 1024 }
|
1024
|
||||||
fn default_allow_upscale() -> bool { true }
|
}
|
||||||
|
fn default_render_height() -> i32 {
|
||||||
|
1024
|
||||||
|
}
|
||||||
|
fn default_allow_upscale() -> bool {
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
impl DisplayConfig {
|
||||||
|
pub fn validate(&self) -> Result<()> {
|
||||||
|
if self.window_title.trim().is_empty() {
|
||||||
|
bail!("display.window_title 不能为空");
|
||||||
|
}
|
||||||
|
|
||||||
|
if self.render_width <= 0 {
|
||||||
|
bail!("display.render_width 必须大于 0");
|
||||||
|
}
|
||||||
|
|
||||||
|
if self.render_height <= 0 {
|
||||||
|
bail!("display.render_height 必须大于 0");
|
||||||
|
}
|
||||||
|
|
||||||
|
if !matches!(self.rotation, 0 | 90 | 180 | 270) {
|
||||||
|
bail!("display.rotation 只能是 0/90/180/270");
|
||||||
|
}
|
||||||
|
|
||||||
|
self.perspective_correction.validate()?;
|
||||||
|
self.chroma_key.validate()?;
|
||||||
|
self.brightness_adjust.validate()?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PerspectiveCorrectionConfig {
|
||||||
|
pub fn validate(&self) -> Result<()> {
|
||||||
|
let point_count = self.points.len();
|
||||||
|
if point_count != 0 && point_count != 4 {
|
||||||
|
bail!("display.perspective_correction.points 必须为 0 或 4 个点");
|
||||||
|
}
|
||||||
|
|
||||||
|
if self.enabled && point_count != 4 {
|
||||||
|
bail!("display.perspective_correction 启用时必须提供 4 个点");
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ChromaKeyConfig {
|
||||||
|
pub fn validate(&self) -> Result<()> {
|
||||||
|
validate_hsv_component("display.chroma_key.hsv_min[0]", self.hsv_min[0], 0, 180)?;
|
||||||
|
validate_hsv_component("display.chroma_key.hsv_min[1]", self.hsv_min[1], 0, 255)?;
|
||||||
|
validate_hsv_component("display.chroma_key.hsv_min[2]", self.hsv_min[2], 0, 255)?;
|
||||||
|
validate_hsv_component("display.chroma_key.hsv_max[0]", self.hsv_max[0], 0, 180)?;
|
||||||
|
validate_hsv_component("display.chroma_key.hsv_max[1]", self.hsv_max[1], 0, 255)?;
|
||||||
|
validate_hsv_component("display.chroma_key.hsv_max[2]", self.hsv_max[2], 0, 255)?;
|
||||||
|
|
||||||
|
for index in 0..3 {
|
||||||
|
if self.hsv_min[index] > self.hsv_max[index] {
|
||||||
|
bail!("display.chroma_key hsv_min[{index}] 不能大于 hsv_max[{index}]");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if self.feather < 0 {
|
||||||
|
bail!("display.chroma_key.feather 不能小于 0");
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl BrightnessAdjustConfig {
|
||||||
|
pub fn validate(&self) -> Result<()> {
|
||||||
|
if !self.subject_boost.is_finite() || self.subject_boost < 0.0 {
|
||||||
|
bail!("display.brightness_adjust.subject_boost 必须为非负有限数");
|
||||||
|
}
|
||||||
|
|
||||||
|
if !self.background_suppress.is_finite() || !(0.0..=1.0).contains(&self.background_suppress)
|
||||||
|
{
|
||||||
|
bail!("display.brightness_adjust.background_suppress 必须在 0.0 到 1.0 之间");
|
||||||
|
}
|
||||||
|
|
||||||
|
if !(0..=255).contains(&self.threshold) {
|
||||||
|
bail!("display.brightness_adjust.threshold 必须在 0 到 255 之间");
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl VideoItem {
|
||||||
|
pub fn validate(&self, section: &str) -> Result<()> {
|
||||||
|
if self.id.trim().is_empty() {
|
||||||
|
bail!("{section}.id 不能为空");
|
||||||
|
}
|
||||||
|
|
||||||
|
if self.path.trim().is_empty() {
|
||||||
|
bail!("{section}.path 不能为空");
|
||||||
|
}
|
||||||
|
|
||||||
|
if self.loop_count <= 0 {
|
||||||
|
bail!("{section}.loop_count 必须大于 0");
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(duration) = self.duration {
|
||||||
|
if !duration.is_finite() || duration <= 0.0 {
|
||||||
|
bail!("{section}.duration 必须为正有限数");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TransitionConfig {
|
||||||
|
pub fn validate(&self) -> Result<()> {
|
||||||
|
if !self.duration.is_finite() || self.duration < 0.0 {
|
||||||
|
bail!("transition.duration 必须为非负有限数");
|
||||||
|
}
|
||||||
|
|
||||||
|
if self.enabled && self.transition_type != TransitionType::None && self.duration <= 0.0 {
|
||||||
|
bail!("transition 启用且 type 非 none 时,duration 必须大于 0");
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ScenesConfig {
|
||||||
|
pub fn validate(&self, playlist_index: &HashSet<&str>) -> Result<()> {
|
||||||
|
self.rest.validate_items("scenes.rest")?;
|
||||||
|
self.active.validate_items("scenes.active")?;
|
||||||
|
self.sleep.validate_items("scenes.sleep")?;
|
||||||
|
self.interact.validate_items("scenes.interact")?;
|
||||||
|
|
||||||
|
let Some(state_machine) = &self.state_machine else {
|
||||||
|
return Ok(());
|
||||||
|
};
|
||||||
|
|
||||||
|
if state_machine.initial_state.trim().is_empty() {
|
||||||
|
bail!("scenes.state_machine.initial_state 不能为空");
|
||||||
|
}
|
||||||
|
|
||||||
|
if !state_machine
|
||||||
|
.states
|
||||||
|
.contains_key(&state_machine.initial_state)
|
||||||
|
{
|
||||||
|
bail!(
|
||||||
|
"scenes.state_machine.initial_state '{}' 不存在于 states 中",
|
||||||
|
state_machine.initial_state
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut has_free_mode = false;
|
||||||
|
for (state_id, state) in &state_machine.states {
|
||||||
|
if state.mode == StateMode::FreeMode {
|
||||||
|
has_free_mode = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (step_index, step) in state.sequence.iter().enumerate() {
|
||||||
|
if step.video_id.trim().is_empty() {
|
||||||
|
bail!(
|
||||||
|
"scenes.state_machine.states['{state_id}'].sequence[{step_index}].video_id 不能为空"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if !playlist_index.contains(step.video_id.as_str()) {
|
||||||
|
bail!(
|
||||||
|
"scenes.state_machine.states['{state_id}'].sequence[{step_index}] 引用的 video_id '{}' 不存在于 playlist 中",
|
||||||
|
step.video_id
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(next_state) = &state.next_state {
|
||||||
|
if next_state.trim().is_empty() {
|
||||||
|
bail!("scenes.state_machine.states['{state_id}'].next_state 不能为空");
|
||||||
|
}
|
||||||
|
if !state_machine.states.contains_key(next_state) {
|
||||||
|
bail!(
|
||||||
|
"scenes.state_machine.states['{state_id}'].next_state '{}' 不存在",
|
||||||
|
next_state
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(next_states) = &state.next_states {
|
||||||
|
for (entry_index, entry) in next_states.iter().enumerate() {
|
||||||
|
if entry.state.trim().is_empty() {
|
||||||
|
bail!(
|
||||||
|
"scenes.state_machine.states['{state_id}'].next_states[{entry_index}].state 不能为空"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if !state_machine.states.contains_key(&entry.state) {
|
||||||
|
bail!(
|
||||||
|
"scenes.state_machine.states['{state_id}'].next_states[{entry_index}] 的目标状态 '{}' 不存在",
|
||||||
|
entry.state
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if entry.weight <= 0.0 {
|
||||||
|
bail!(
|
||||||
|
"scenes.state_machine.states['{state_id}'].next_states[{entry_index}].weight 必须大于 0"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (transition_index, transition) in state.transitions.iter().enumerate() {
|
||||||
|
if transition.target_state.trim().is_empty() {
|
||||||
|
bail!(
|
||||||
|
"scenes.state_machine.states['{state_id}'].transitions[{transition_index}].target_state 不能为空"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if !state_machine.states.contains_key(&transition.target_state) {
|
||||||
|
bail!(
|
||||||
|
"scenes.state_machine.states['{state_id}'].transitions[{transition_index}] 的 target_state '{}' 不存在",
|
||||||
|
transition.target_state
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !has_free_mode {
|
||||||
|
bail!("scenes.state_machine 至少需要一个 FreeMode 状态");
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl RemoteControlConfig {
|
||||||
|
pub fn validate(&self) -> Result<()> {
|
||||||
|
if self.enabled && self.host.trim().is_empty() {
|
||||||
|
bail!("remote_control.enabled 为 true 时,host 不能为空");
|
||||||
|
}
|
||||||
|
|
||||||
|
if self.enabled && self.port == 0 {
|
||||||
|
bail!("remote_control.enabled 为 true 时,port 不能为 0");
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
trait ValidateVideoItems {
|
||||||
|
fn validate_items(&self, section: &str) -> Result<()>;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ValidateVideoItems for [VideoItem] {
|
||||||
|
fn validate_items(&self, section: &str) -> Result<()> {
|
||||||
|
for (index, item) in self.iter().enumerate() {
|
||||||
|
item.validate(&format!("{section}[{index}]"))?;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn validate_hsv_component(field: &str, value: i32, min: i32, max: i32) -> Result<()> {
|
||||||
|
if !(min..=max).contains(&value) {
|
||||||
|
bail!("{field} 必须在 {min} 到 {max} 之间");
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|||||||
@@ -2,22 +2,106 @@
|
|||||||
//!
|
//!
|
||||||
//! 唤醒锁(systemd-inhibit)、光标隐藏(unclutter)。
|
//! 唤醒锁(systemd-inhibit)、光标隐藏(unclutter)。
|
||||||
|
|
||||||
use crate::core::message::Message;
|
use crate::core::{message::Message, plugin::*};
|
||||||
use crate::core::plugin::{Plugin, PluginContext, PluginInfo, Platform};
|
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
|
use std::process::{Child, Command, Stdio};
|
||||||
|
|
||||||
pub struct ScreenPlugin {
|
pub struct ScreenPlugin {
|
||||||
ctx: Option<PluginContext>,
|
ctx: Option<PluginContext>,
|
||||||
|
wake_lock_child: Option<Child>,
|
||||||
|
cursor_hidden: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ScreenPlugin {
|
impl ScreenPlugin {
|
||||||
pub fn new() -> Self {
|
pub fn new() -> Self {
|
||||||
Self { ctx: None }
|
Self {
|
||||||
|
ctx: None,
|
||||||
|
wake_lock_child: None,
|
||||||
|
cursor_hidden: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(target_os = "linux")]
|
||||||
|
fn start_wake_lock(&mut self) {
|
||||||
|
if self.wake_lock_child.is_some() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
match Command::new("systemd-inhibit")
|
||||||
|
.arg("--what=idle")
|
||||||
|
.arg("--who=ShowenV2")
|
||||||
|
.arg("--why=Prevent screen lock during playback")
|
||||||
|
.arg("sleep")
|
||||||
|
.arg("infinity")
|
||||||
|
.stdin(Stdio::null())
|
||||||
|
.stdout(Stdio::null())
|
||||||
|
.stderr(Stdio::null())
|
||||||
|
.spawn()
|
||||||
|
{
|
||||||
|
Ok(child) => self.wake_lock_child = Some(child),
|
||||||
|
Err(err) => eprintln!("[ScreenPlugin] 启动防息屏失败: {err}"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(not(target_os = "linux"))]
|
||||||
|
fn start_wake_lock(&mut self) {}
|
||||||
|
|
||||||
|
#[cfg(target_os = "linux")]
|
||||||
|
fn stop_wake_lock(&mut self) {
|
||||||
|
if let Some(mut child) = self.wake_lock_child.take() {
|
||||||
|
if let Err(err) = child.kill() {
|
||||||
|
eprintln!("[ScreenPlugin] 停止防息屏失败: {err}");
|
||||||
|
}
|
||||||
|
let _ = child.wait();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(not(target_os = "linux"))]
|
||||||
|
fn stop_wake_lock(&mut self) {}
|
||||||
|
|
||||||
|
#[cfg(target_os = "linux")]
|
||||||
|
fn set_cursor_hidden(&mut self, hidden: bool) {
|
||||||
|
if hidden == self.cursor_hidden {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if hidden {
|
||||||
|
match Command::new("unclutter")
|
||||||
|
.arg("-idle")
|
||||||
|
.arg("0")
|
||||||
|
.arg("-root")
|
||||||
|
.stdin(Stdio::null())
|
||||||
|
.stdout(Stdio::null())
|
||||||
|
.stderr(Stdio::null())
|
||||||
|
.spawn()
|
||||||
|
{
|
||||||
|
Ok(_) => self.cursor_hidden = true,
|
||||||
|
Err(err) => eprintln!("[ScreenPlugin] 隐藏光标失败: {err}"),
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
match Command::new("pkill")
|
||||||
|
.arg("unclutter")
|
||||||
|
.stdin(Stdio::null())
|
||||||
|
.stdout(Stdio::null())
|
||||||
|
.stderr(Stdio::null())
|
||||||
|
.status()
|
||||||
|
{
|
||||||
|
Ok(_) => self.cursor_hidden = false,
|
||||||
|
Err(err) => eprintln!("[ScreenPlugin] 恢复光标失败: {err}"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(not(target_os = "linux"))]
|
||||||
|
fn set_cursor_hidden(&mut self, hidden: bool) {
|
||||||
|
self.cursor_hidden = hidden;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Plugin for ScreenPlugin {
|
impl Plugin for ScreenPlugin {
|
||||||
fn id(&self) -> &'static str { "screen" }
|
fn id(&self) -> &'static str {
|
||||||
|
"screen"
|
||||||
|
}
|
||||||
|
|
||||||
fn info(&self) -> PluginInfo {
|
fn info(&self) -> PluginInfo {
|
||||||
PluginInfo {
|
PluginInfo {
|
||||||
@@ -33,7 +117,42 @@ impl Plugin for ScreenPlugin {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn start(&mut self) -> Result<()> { Ok(()) }
|
fn start(&mut self) -> Result<()> {
|
||||||
fn handle_message(&mut self, _msg: Message) -> Result<()> { Ok(()) }
|
if self
|
||||||
fn stop(&mut self) -> Result<()> { Ok(()) }
|
.ctx
|
||||||
|
.as_ref()
|
||||||
|
.map(|ctx| ctx.config.display.prevent_screen_lock)
|
||||||
|
.unwrap_or(false)
|
||||||
|
{
|
||||||
|
self.start_wake_lock();
|
||||||
|
}
|
||||||
|
|
||||||
|
self.set_cursor_hidden(true);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn handle_message(&mut self, msg: Message) -> Result<()> {
|
||||||
|
match msg {
|
||||||
|
Message::ScreenLockRequest(lock) => {
|
||||||
|
if lock {
|
||||||
|
self.start_wake_lock();
|
||||||
|
} else {
|
||||||
|
self.stop_wake_lock();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Message::CursorVisibility(visible) => self.set_cursor_hidden(!visible),
|
||||||
|
Message::Shutdown => {
|
||||||
|
self.stop()?;
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn stop(&mut self) -> Result<()> {
|
||||||
|
self.stop_wake_lock();
|
||||||
|
self.set_cursor_hidden(false);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1 +1,275 @@
|
|||||||
// StateMachine — 待 Commit 4 迁移
|
use crate::core::config::{
|
||||||
|
AnimationStep, NextStateEntry, StateConfig, StateMachineConfig, StateMode, StateTransition,
|
||||||
|
TriggerType,
|
||||||
|
};
|
||||||
|
use anyhow::Result;
|
||||||
|
use rand::Rng;
|
||||||
|
|
||||||
|
pub struct StateMachine {
|
||||||
|
pub config: StateMachineConfig,
|
||||||
|
pub current_state: String,
|
||||||
|
pub current_sequence_index: usize,
|
||||||
|
pub current_loop_remaining: i32,
|
||||||
|
pub pending_trigger_target: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl StateMachine {
|
||||||
|
pub fn new(config: StateMachineConfig) -> Self {
|
||||||
|
Self {
|
||||||
|
current_state: config.initial_state.clone(),
|
||||||
|
config,
|
||||||
|
current_sequence_index: 0,
|
||||||
|
current_loop_remaining: 0,
|
||||||
|
pending_trigger_target: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn start(&mut self) -> Result<()> {
|
||||||
|
self.current_state = self.config.initial_state.clone();
|
||||||
|
self.pending_trigger_target = None;
|
||||||
|
self.reset_state_progress()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn current_video_id(&self) -> Option<String> {
|
||||||
|
self.current_state_config()
|
||||||
|
.and_then(|state| state.sequence.get(self.current_sequence_index))
|
||||||
|
.map(|step| step.video_id.clone())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn current_state_config(&self) -> Option<&StateConfig> {
|
||||||
|
self.config.states.get(&self.current_state)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn on_video_completed(&mut self) -> Result<bool> {
|
||||||
|
self.ensure_current_state_valid()?;
|
||||||
|
|
||||||
|
if self.current_loop_remaining > 1 {
|
||||||
|
self.current_loop_remaining -= 1;
|
||||||
|
return Ok(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(target_state) = self.pending_trigger_target.take() {
|
||||||
|
self.transition_to_state(&target_state)?;
|
||||||
|
return Ok(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
let sequence_len = self
|
||||||
|
.current_state_config()
|
||||||
|
.ok_or_else(|| anyhow::anyhow!("当前状态不存在: {}", self.current_state))?
|
||||||
|
.sequence
|
||||||
|
.len();
|
||||||
|
if self.current_sequence_index + 1 < sequence_len {
|
||||||
|
self.current_sequence_index += 1;
|
||||||
|
self.current_loop_remaining = self.resolve_current_loop_count()?;
|
||||||
|
return Ok(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
let next_state = self.select_next_state()?;
|
||||||
|
self.transition_to_state(&next_state)?;
|
||||||
|
Ok(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn handle_trigger(&mut self, name: &str, value: &str) -> Result<bool> {
|
||||||
|
let Some(state) = self.current_state_config() else {
|
||||||
|
return Ok(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
if state.ignore_triggers {
|
||||||
|
return Ok(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
let target_state = self
|
||||||
|
.matching_transition(&state.transitions, name, value)
|
||||||
|
.map(|transition| transition.target_state.clone());
|
||||||
|
|
||||||
|
let Some(target_state) = target_state else {
|
||||||
|
return Ok(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
if state.defer_triggers {
|
||||||
|
self.pending_trigger_target = Some(target_state);
|
||||||
|
return Ok(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
self.transition_to_state(&target_state)?;
|
||||||
|
Ok(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn check_random_triggers(&mut self) -> Result<bool> {
|
||||||
|
let Some(state) = self.current_state_config() else {
|
||||||
|
return Ok(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
if state.ignore_triggers {
|
||||||
|
return Ok(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
if !matches!(state.mode, StateMode::FreeMode | StateMode::InteractiveMode) {
|
||||||
|
return Ok(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut rng = rand::thread_rng();
|
||||||
|
let target_state = state
|
||||||
|
.transitions
|
||||||
|
.iter()
|
||||||
|
.filter_map(|transition| match &transition.trigger {
|
||||||
|
TriggerType::Random { probability } => {
|
||||||
|
let probability = probability.clamp(0.0, 1.0);
|
||||||
|
if rng.gen_bool(probability) {
|
||||||
|
Some(transition)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => None,
|
||||||
|
})
|
||||||
|
.max_by_key(|transition| transition.priority)
|
||||||
|
.map(|transition| transition.target_state.clone());
|
||||||
|
|
||||||
|
let Some(target_state) = target_state else {
|
||||||
|
return Ok(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
if state.defer_triggers {
|
||||||
|
self.pending_trigger_target = Some(target_state);
|
||||||
|
return Ok(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
self.transition_to_state(&target_state)?;
|
||||||
|
Ok(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn has_pending_trigger(&self) -> bool {
|
||||||
|
self.pending_trigger_target.is_some()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn reset_state_progress(&mut self) -> Result<()> {
|
||||||
|
self.ensure_current_state_valid()?;
|
||||||
|
self.current_sequence_index = 0;
|
||||||
|
self.current_loop_remaining = self.resolve_current_loop_count()?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn transition_to_state(&mut self, target_state: &str) -> Result<()> {
|
||||||
|
if !self.config.states.contains_key(target_state) {
|
||||||
|
anyhow::bail!("目标状态不存在: {}", target_state);
|
||||||
|
}
|
||||||
|
|
||||||
|
self.current_state = target_state.to_string();
|
||||||
|
self.pending_trigger_target = None;
|
||||||
|
self.reset_state_progress()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn ensure_current_state_valid(&self) -> Result<()> {
|
||||||
|
let state = self
|
||||||
|
.current_state_config()
|
||||||
|
.ok_or_else(|| anyhow::anyhow!("当前状态不存在: {}", self.current_state))?;
|
||||||
|
|
||||||
|
if state.sequence.is_empty() {
|
||||||
|
anyhow::bail!("状态 '{}' 的 sequence 不能为空", state.name);
|
||||||
|
}
|
||||||
|
|
||||||
|
if self.current_sequence_index >= state.sequence.len() {
|
||||||
|
anyhow::bail!(
|
||||||
|
"状态 '{}' 的 sequence 索引越界: {} >= {}",
|
||||||
|
state.name,
|
||||||
|
self.current_sequence_index,
|
||||||
|
state.sequence.len()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn resolve_current_loop_count(&self) -> Result<i32> {
|
||||||
|
let state = self
|
||||||
|
.current_state_config()
|
||||||
|
.ok_or_else(|| anyhow::anyhow!("当前状态不存在: {}", self.current_state))?;
|
||||||
|
let step = state
|
||||||
|
.sequence
|
||||||
|
.get(self.current_sequence_index)
|
||||||
|
.ok_or_else(|| {
|
||||||
|
anyhow::anyhow!(
|
||||||
|
"状态 '{}' 的 sequence 索引越界: {}",
|
||||||
|
state.name,
|
||||||
|
self.current_sequence_index
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
|
||||||
|
Ok(self.resolve_step_loop_count(step))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn resolve_step_loop_count(&self, step: &AnimationStep) -> i32 {
|
||||||
|
if let Some([start, end]) = step.random_loop_range {
|
||||||
|
let min = start.min(end);
|
||||||
|
let max = start.max(end);
|
||||||
|
return rand::thread_rng().gen_range(min..=max).max(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
step.loop_count.unwrap_or(1).max(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn select_next_state(&self) -> Result<String> {
|
||||||
|
let state = self
|
||||||
|
.current_state_config()
|
||||||
|
.ok_or_else(|| anyhow::anyhow!("当前状态不存在: {}", self.current_state))?;
|
||||||
|
|
||||||
|
if let Some(entries) = &state.next_states {
|
||||||
|
if let Some(next_state) = self.select_weighted_next_state(entries) {
|
||||||
|
return Ok(next_state);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(next_state) = &state.next_state {
|
||||||
|
return Ok(next_state.clone());
|
||||||
|
}
|
||||||
|
|
||||||
|
match state.mode {
|
||||||
|
StateMode::FreeMode | StateMode::InteractiveMode => Ok(self.current_state.clone()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn select_weighted_next_state(&self, entries: &[NextStateEntry]) -> Option<String> {
|
||||||
|
let valid_entries: Vec<&NextStateEntry> =
|
||||||
|
entries.iter().filter(|entry| entry.weight > 0.0).collect();
|
||||||
|
if valid_entries.is_empty() {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
let total_weight: f32 = valid_entries.iter().map(|entry| entry.weight).sum();
|
||||||
|
if total_weight <= 0.0 {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut cursor = rand::thread_rng().gen_range(0.0..total_weight);
|
||||||
|
for entry in valid_entries {
|
||||||
|
cursor -= entry.weight;
|
||||||
|
if cursor <= 0.0 {
|
||||||
|
return Some(entry.state.clone());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
entries.last().map(|entry| entry.state.clone())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn matching_transition<'a>(
|
||||||
|
&self,
|
||||||
|
transitions: &'a [StateTransition],
|
||||||
|
name: &str,
|
||||||
|
value: &str,
|
||||||
|
) -> Option<&'a StateTransition> {
|
||||||
|
transitions
|
||||||
|
.iter()
|
||||||
|
.filter(|transition| Self::trigger_matches(&transition.trigger, name, value))
|
||||||
|
.max_by_key(|transition| transition.priority)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn trigger_matches(trigger: &TriggerType, name: &str, value: &str) -> bool {
|
||||||
|
match trigger {
|
||||||
|
TriggerType::Button { name: expected } => expected == name,
|
||||||
|
TriggerType::Voice { keyword } => keyword == value || keyword == name,
|
||||||
|
TriggerType::Sensor { name: expected } => expected == name,
|
||||||
|
TriggerType::Timer { .. } | TriggerType::Random { .. } => false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,23 +1,223 @@
|
|||||||
//! WifiPlugin — WiFi 管理
|
//! WifiPlugin — WiFi 管理
|
||||||
//!
|
//!
|
||||||
//! 通过 nmcli 实现 WiFi 扫描、连接、AP 热点。
|
//! 通过 nmcli 实现 WiFi 扫描、连接、状态查询、AP 热点启停。
|
||||||
|
|
||||||
use crate::core::message::Message;
|
use crate::core::{message::*, plugin::*};
|
||||||
use crate::core::plugin::{Plugin, PluginContext, PluginInfo, Platform};
|
use anyhow::{anyhow, Context, Result};
|
||||||
use anyhow::Result;
|
use serde::Serialize;
|
||||||
|
use serde_json::json;
|
||||||
|
use std::collections::HashMap;
|
||||||
|
use std::process::Command;
|
||||||
|
use std::thread;
|
||||||
|
use std::time::Duration;
|
||||||
|
|
||||||
pub struct WifiPlugin {
|
pub struct WifiPlugin {
|
||||||
ctx: Option<PluginContext>,
|
ctx: Option<PluginContext>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize)]
|
||||||
|
struct WifiNetwork {
|
||||||
|
ssid: String,
|
||||||
|
signal: i32,
|
||||||
|
security: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize)]
|
||||||
|
struct DeviceStatus {
|
||||||
|
device: String,
|
||||||
|
device_type: String,
|
||||||
|
state: String,
|
||||||
|
connection: String,
|
||||||
|
ip4_addresses: Vec<String>,
|
||||||
|
}
|
||||||
|
|
||||||
impl WifiPlugin {
|
impl WifiPlugin {
|
||||||
pub fn new() -> Self {
|
pub fn new() -> Self {
|
||||||
Self { ctx: None }
|
Self { ctx: None }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn run_nmcli(args: &[&str]) -> Result<String> {
|
||||||
|
let output = Command::new("nmcli")
|
||||||
|
.args(args)
|
||||||
|
.output()
|
||||||
|
.with_context(|| format!("failed to execute nmcli {:?}", args))?;
|
||||||
|
|
||||||
|
if output.status.success() {
|
||||||
|
Ok(String::from_utf8_lossy(&output.stdout).trim().to_string())
|
||||||
|
} else {
|
||||||
|
let stderr = String::from_utf8_lossy(&output.stderr).trim().to_string();
|
||||||
|
let stdout = String::from_utf8_lossy(&output.stdout).trim().to_string();
|
||||||
|
let message = if !stderr.is_empty() { stderr } else { stdout };
|
||||||
|
Err(anyhow!("nmcli {:?} failed: {}", args, message))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn send_result(&self, payload: String) -> Result<()> {
|
||||||
|
let ctx = self
|
||||||
|
.ctx
|
||||||
|
.as_ref()
|
||||||
|
.context("wifi plugin context is not initialized")?;
|
||||||
|
|
||||||
|
ctx.tx.send(Envelope {
|
||||||
|
from: "wifi",
|
||||||
|
to: Destination::Manager,
|
||||||
|
message: Message::WifiResult(payload),
|
||||||
|
})?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn handle_wifi_command(&self, cmd: WifiCommand) -> String {
|
||||||
|
let result = match cmd {
|
||||||
|
WifiCommand::Scan => self.scan_networks(),
|
||||||
|
WifiCommand::Connect { ssid, password } => self.connect_network(&ssid, &password),
|
||||||
|
WifiCommand::Status => self.status(),
|
||||||
|
WifiCommand::ApStart { ssid, password } => self.ap_start(&ssid, &password),
|
||||||
|
WifiCommand::ApStop => self.ap_stop(),
|
||||||
|
};
|
||||||
|
|
||||||
|
match result {
|
||||||
|
Ok(value) => value.to_string(),
|
||||||
|
Err(err) => json!({
|
||||||
|
"ok": false,
|
||||||
|
"error": err.to_string(),
|
||||||
|
})
|
||||||
|
.to_string(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn scan_networks(&self) -> Result<serde_json::Value> {
|
||||||
|
Self::run_nmcli(&["device", "wifi", "rescan"])?;
|
||||||
|
thread::sleep(Duration::from_secs(2));
|
||||||
|
let output =
|
||||||
|
Self::run_nmcli(&["-t", "-f", "SSID,SIGNAL,SECURITY", "device", "wifi", "list"])?;
|
||||||
|
|
||||||
|
let networks = output
|
||||||
|
.lines()
|
||||||
|
.filter(|line| !line.trim().is_empty())
|
||||||
|
.map(|line| {
|
||||||
|
let mut parts = line.splitn(3, ':');
|
||||||
|
let ssid = parts.next().unwrap_or_default().trim().to_string();
|
||||||
|
let signal = parts
|
||||||
|
.next()
|
||||||
|
.unwrap_or_default()
|
||||||
|
.trim()
|
||||||
|
.parse::<i32>()
|
||||||
|
.unwrap_or_default();
|
||||||
|
let security = parts.next().unwrap_or_default().trim().to_string();
|
||||||
|
|
||||||
|
WifiNetwork {
|
||||||
|
ssid,
|
||||||
|
signal,
|
||||||
|
security,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
|
Ok(json!({
|
||||||
|
"ok": true,
|
||||||
|
"action": "scan",
|
||||||
|
"networks": networks,
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn connect_network(&self, ssid: &str, password: &str) -> Result<serde_json::Value> {
|
||||||
|
let output = Self::run_nmcli(&["device", "wifi", "connect", ssid, "password", password])?;
|
||||||
|
|
||||||
|
Ok(json!({
|
||||||
|
"ok": true,
|
||||||
|
"action": "connect",
|
||||||
|
"ssid": ssid,
|
||||||
|
"output": output,
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn status(&self) -> Result<serde_json::Value> {
|
||||||
|
let device_output = Self::run_nmcli(&[
|
||||||
|
"-t",
|
||||||
|
"-f",
|
||||||
|
"DEVICE,TYPE,STATE,CONNECTION",
|
||||||
|
"device",
|
||||||
|
"status",
|
||||||
|
])?;
|
||||||
|
let ip_output = Self::run_nmcli(&["-t", "-f", "DEVICE,IP4.ADDRESS", "device", "show"])?;
|
||||||
|
|
||||||
|
let mut ip_map: HashMap<String, Vec<String>> = HashMap::new();
|
||||||
|
for line in ip_output.lines().filter(|line| !line.trim().is_empty()) {
|
||||||
|
let mut parts = line.splitn(2, ':');
|
||||||
|
let device = parts.next().unwrap_or_default().trim();
|
||||||
|
let address = parts.next().unwrap_or_default().trim();
|
||||||
|
|
||||||
|
if device.is_empty() || address.is_empty() {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
ip_map
|
||||||
|
.entry(device.to_string())
|
||||||
|
.or_default()
|
||||||
|
.push(address.to_string());
|
||||||
|
}
|
||||||
|
|
||||||
|
let devices = device_output
|
||||||
|
.lines()
|
||||||
|
.filter(|line| !line.trim().is_empty())
|
||||||
|
.map(|line| {
|
||||||
|
let mut parts = line.splitn(4, ':');
|
||||||
|
let device = parts.next().unwrap_or_default().trim().to_string();
|
||||||
|
|
||||||
|
DeviceStatus {
|
||||||
|
ip4_addresses: ip_map.remove(&device).unwrap_or_default(),
|
||||||
|
device,
|
||||||
|
device_type: parts.next().unwrap_or_default().trim().to_string(),
|
||||||
|
state: parts.next().unwrap_or_default().trim().to_string(),
|
||||||
|
connection: parts.next().unwrap_or_default().trim().to_string(),
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
|
Ok(json!({
|
||||||
|
"ok": true,
|
||||||
|
"action": "status",
|
||||||
|
"devices": devices,
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn ap_start(&self, ssid: &str, password: &str) -> Result<serde_json::Value> {
|
||||||
|
let output = Self::run_nmcli(&[
|
||||||
|
"device", "wifi", "hotspot", "ssid", ssid, "password", password,
|
||||||
|
])?;
|
||||||
|
|
||||||
|
Ok(json!({
|
||||||
|
"ok": true,
|
||||||
|
"action": "ap_start",
|
||||||
|
"ssid": ssid,
|
||||||
|
"output": output,
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn ap_stop(&self) -> Result<serde_json::Value> {
|
||||||
|
let active = Self::run_nmcli(&["-t", "-f", "NAME", "connection", "show", "--active"])?;
|
||||||
|
let hotspot_name = active
|
||||||
|
.lines()
|
||||||
|
.map(str::trim)
|
||||||
|
.find(|name| *name == "hotspot")
|
||||||
|
.ok_or_else(|| anyhow!("active hotspot connection 'hotspot' not found"))?;
|
||||||
|
|
||||||
|
let output = Self::run_nmcli(&["connection", "down", hotspot_name])?;
|
||||||
|
|
||||||
|
Ok(json!({
|
||||||
|
"ok": true,
|
||||||
|
"action": "ap_stop",
|
||||||
|
"connection": hotspot_name,
|
||||||
|
"output": output,
|
||||||
|
}))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Plugin for WifiPlugin {
|
impl Plugin for WifiPlugin {
|
||||||
fn id(&self) -> &'static str { "wifi" }
|
fn id(&self) -> &'static str {
|
||||||
|
"wifi"
|
||||||
|
}
|
||||||
|
|
||||||
fn info(&self) -> PluginInfo {
|
fn info(&self) -> PluginInfo {
|
||||||
PluginInfo {
|
PluginInfo {
|
||||||
@@ -33,7 +233,20 @@ impl Plugin for WifiPlugin {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn start(&mut self) -> Result<()> { Ok(()) }
|
fn start(&mut self) -> Result<()> {
|
||||||
fn handle_message(&mut self, _msg: Message) -> Result<()> { Ok(()) }
|
Ok(())
|
||||||
fn stop(&mut self) -> Result<()> { Ok(()) }
|
}
|
||||||
|
|
||||||
|
fn handle_message(&mut self, msg: Message) -> Result<()> {
|
||||||
|
if let Message::WifiCommand(cmd) = msg {
|
||||||
|
let payload = self.handle_wifi_command(cmd);
|
||||||
|
self.send_result(payload)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn stop(&mut self) -> Result<()> {
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user