//! Emote Tool - LED expressions and sound effects //! //! Control LED matrix/strips for robot "expressions" and play sounds. //! Makes the robot more engaging for kids! use crate::config::RobotConfig; use crate::traits::{Tool, ToolResult}; use anyhow::Result; use async_trait::async_trait; use serde_json::{json, Value}; use std::path::PathBuf; /// Predefined LED expressions #[derive(Debug, Clone, Copy)] pub enum Expression { Happy, // :) Sad, // :( Surprised, // :O Thinking, // :? Sleepy, // -_- Excited, // ^_^ Love, // <3 <3 Angry, // >:( Confused, // @_@ Wink, // ;) } impl Expression { fn from_str(s: &str) -> Option { match s.to_lowercase().as_str() { "happy" | "smile" => Some(Self::Happy), "sad" | "frown" => Some(Self::Sad), "surprised" | "wow" => Some(Self::Surprised), "thinking" | "hmm" => Some(Self::Thinking), "sleepy" | "tired" => Some(Self::Sleepy), "excited" | "yay" => Some(Self::Excited), "love" | "heart" => Some(Self::Love), "angry" | "mad" => Some(Self::Angry), "confused" | "huh" => Some(Self::Confused), "wink" => Some(Self::Wink), _ => None, } } /// Get LED matrix pattern (8x8 example) /// Returns array of 64 RGB values fn pattern(&self) -> Vec<(u8, u8, u8)> { let black = (0, 0, 0); let white = (255, 255, 255); let yellow = (255, 255, 0); let red = (255, 0, 0); let blue = (0, 100, 255); let pink = (255, 100, 150); // 8x8 patterns (simplified representations) match self { Self::Happy => { // Simple smiley vec![ black, black, yellow, yellow, yellow, yellow, black, black, black, yellow, black, black, black, black, yellow, black, yellow, black, white, black, black, white, black, yellow, yellow, black, black, black, black, black, black, yellow, yellow, black, white, black, black, white, black, yellow, yellow, black, black, white, white, black, black, yellow, black, yellow, black, black, black, black, yellow, black, black, black, yellow, yellow, yellow, yellow, black, black, ] } Self::Sad => { vec![ black, black, blue, blue, blue, blue, black, black, black, blue, black, black, black, black, blue, black, blue, black, white, black, black, white, black, blue, blue, black, black, black, black, black, black, blue, blue, black, black, white, white, black, black, blue, blue, black, white, black, black, white, black, blue, black, blue, black, black, black, black, blue, black, black, black, blue, blue, blue, blue, black, black, ] } Self::Excited => { vec![ yellow, yellow, yellow, yellow, yellow, yellow, yellow, yellow, yellow, black, black, yellow, yellow, black, black, yellow, yellow, black, white, yellow, yellow, white, black, yellow, yellow, yellow, yellow, yellow, yellow, yellow, yellow, yellow, yellow, black, black, black, black, black, black, yellow, yellow, black, white, white, white, white, black, yellow, yellow, black, black, black, black, black, black, yellow, yellow, yellow, yellow, yellow, yellow, yellow, yellow, yellow, ] } Self::Love => { vec![ black, pink, pink, black, black, pink, pink, black, pink, pink, pink, pink, pink, pink, pink, pink, pink, pink, pink, pink, pink, pink, pink, pink, pink, pink, pink, pink, pink, pink, pink, pink, black, pink, pink, pink, pink, pink, pink, black, black, black, pink, pink, pink, pink, black, black, black, black, black, pink, pink, black, black, black, black, black, black, black, black, black, black, black, ] } Self::Angry => { vec![ red, red, black, black, black, black, red, red, black, red, red, black, black, red, red, black, black, black, red, black, black, red, black, black, black, black, white, black, black, white, black, black, black, black, black, black, black, black, black, black, black, black, white, white, white, white, black, black, black, white, black, black, black, black, white, black, black, black, black, black, black, black, black, black, ] } _ => { // Default neutral vec![white; 64] } } } } pub struct EmoteTool { #[allow(dead_code)] config: RobotConfig, sounds_dir: PathBuf, } impl EmoteTool { pub fn new(config: RobotConfig) -> Self { let sounds_dir = directories::UserDirs::new() .map(|d| d.home_dir().join(".zeroclaw/sounds")) .unwrap_or_else(|| PathBuf::from("/usr/local/share/zeroclaw/sounds")); Self { config, sounds_dir } } /// Set LED matrix expression async fn set_expression(&self, expr: Expression) -> Result<()> { let pattern = expr.pattern(); // Convert to format for LED driver // In production, use rs_ws281x or similar let pattern_json = serde_json::to_string(&pattern)?; // Try to write to LED controller // Option 1: Write to FIFO/socket if LED daemon is running let led_fifo = PathBuf::from("/tmp/zeroclaw_led.fifo"); if led_fifo.exists() { tokio::fs::write(&led_fifo, pattern_json).await?; return Ok(()); } // Option 2: Shell out to LED control script let output = tokio::process::Command::new("zeroclaw-led") .args(["--pattern", &format!("{:?}", expr)]) .output() .await; match output { Ok(out) if out.status.success() => Ok(()), _ => { tracing::info!("LED display: {:?} (hardware not connected)", expr); Ok(()) // Don't fail if LED hardware isn't available } } } /// Play emotion sound effect async fn play_emotion_sound(&self, emotion: &str) -> Result<()> { let sound_file = self.sounds_dir.join(format!("{}.wav", emotion)); if !sound_file.exists() { tracing::debug!("No sound file for emotion: {}", emotion); return Ok(()); } tokio::process::Command::new("aplay") .arg(sound_file) .output() .await?; Ok(()) } /// Animate expression (e.g., blinking) async fn animate(&self, animation: &str) -> Result<()> { match animation { "blink" => { self.set_expression(Expression::Happy).await?; tokio::time::sleep(std::time::Duration::from_millis(100)).await; // "Closed eyes" - simplified tokio::time::sleep(std::time::Duration::from_millis(100)).await; self.set_expression(Expression::Happy).await?; } "nod" => { // Would control servo if available tracing::info!("Animation: nod"); } "shake" => { tracing::info!("Animation: shake"); } "dance" => { // Cycle through expressions for expr in [ Expression::Happy, Expression::Excited, Expression::Love, Expression::Happy, ] { self.set_expression(expr).await?; tokio::time::sleep(std::time::Duration::from_millis(500)).await; } } _ => {} } Ok(()) } } #[async_trait] impl Tool for EmoteTool { fn name(&self) -> &str { "emote" } fn description(&self) -> &str { "Express emotions through LED display and sounds. Use this to show the robot's \ emotional state - happy when playing, sad when saying goodbye, excited for games, etc. \ This makes interactions with kids more engaging!" } fn parameters_schema(&self) -> Value { json!({ "type": "object", "properties": { "expression": { "type": "string", "enum": ["happy", "sad", "surprised", "thinking", "sleepy", "excited", "love", "angry", "confused", "wink"], "description": "Facial expression to display on LED matrix" }, "animation": { "type": "string", "enum": ["blink", "nod", "shake", "dance"], "description": "Optional animation to perform" }, "sound": { "type": "boolean", "description": "Play matching sound effect (default true)" }, "duration": { "type": "integer", "description": "How long to hold expression in seconds (default 3)" } }, "required": ["expression"] }) } async fn execute(&self, args: Value) -> Result { let expression_str = args["expression"] .as_str() .ok_or_else(|| anyhow::anyhow!("Missing 'expression' parameter"))?; let expression = Expression::from_str(expression_str) .ok_or_else(|| anyhow::anyhow!("Unknown expression: {}", expression_str))?; let play_sound = args["sound"].as_bool().unwrap_or(true); let duration = args["duration"].as_u64().unwrap_or(3); // Set expression self.set_expression(expression).await?; // Play sound if enabled if play_sound { let _ = self.play_emotion_sound(expression_str).await; } // Run animation if specified if let Some(animation) = args["animation"].as_str() { self.animate(animation).await?; } // Hold expression if duration > 0 { tokio::time::sleep(std::time::Duration::from_secs(duration.min(10))).await; } Ok(ToolResult { success: true, output: format!("Expressing: {} for {}s", expression_str, duration), error: None, }) } } #[cfg(test)] mod tests { use super::*; #[test] fn emote_tool_name() { let tool = EmoteTool::new(RobotConfig::default()); assert_eq!(tool.name(), "emote"); } #[test] fn expression_parsing() { assert!(Expression::from_str("happy").is_some()); assert!(Expression::from_str("EXCITED").is_some()); assert!(Expression::from_str("unknown").is_none()); } #[test] fn expression_pattern_size() { let expr = Expression::Happy; assert_eq!(expr.pattern().len(), 64); // 8x8 } #[tokio::test] async fn emote_happy() { let tool = EmoteTool::new(RobotConfig::default()); let result = tool .execute(json!({ "expression": "happy", "duration": 0 })) .await .unwrap(); assert!(result.success); } }