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 +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,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user