From cce6cad2185a68d4bad0888be00ab4293c0080ab Mon Sep 17 00:00:00 2001 From: Eric Coissac Date: Sun, 5 Oct 2025 20:56:48 +0200 Subject: [PATCH] implementation de ConnectionManager --- pmo_src.txt | 9290 ----------------- pmoupnp/src/mediarenderer/avtransport/mod.rs | 26 +- .../actions/getcurrentconnectionids.rs | 8 + .../actions/getcurrentconnectioninfo.rs | 18 + .../actions/getprotocolinfo.rs | 9 + .../connectionmanager/actions/mod.rs | 7 + .../mediarenderer/connectionmanager/mod.rs | 89 + .../variables/a_arg_type_avtransportid.rs | 5 + .../variables/a_arg_type_connectionid.rs | 5 + .../variables/a_arg_type_connectionstatus.rs | 7 + .../variables/a_arg_type_direction.rs | 7 + .../variables/a_arg_type_protocolinfo.rs | 5 + .../variables/a_arg_type_rcsid.rs | 5 + .../variables/currentconnectionids.rs | 7 + .../connectionmanager/variables/mod.rs | 19 + .../variables/sinkprotocolinfo.rs | 8 + .../variables/sourceprotocolinfo.rs | 7 + pmoupnp/src/mediarenderer/mod.rs | 1 + .../src/mediarenderer/renderingcontrol/mod.rs | 26 +- pmoupnp/webapp/.gitignore | 1 - 20 files changed, 211 insertions(+), 9339 deletions(-) delete mode 100644 pmo_src.txt create mode 100644 pmoupnp/src/mediarenderer/connectionmanager/actions/getcurrentconnectionids.rs create mode 100644 pmoupnp/src/mediarenderer/connectionmanager/actions/getcurrentconnectioninfo.rs create mode 100644 pmoupnp/src/mediarenderer/connectionmanager/actions/getprotocolinfo.rs create mode 100644 pmoupnp/src/mediarenderer/connectionmanager/actions/mod.rs create mode 100644 pmoupnp/src/mediarenderer/connectionmanager/mod.rs create mode 100644 pmoupnp/src/mediarenderer/connectionmanager/variables/a_arg_type_avtransportid.rs create mode 100644 pmoupnp/src/mediarenderer/connectionmanager/variables/a_arg_type_connectionid.rs create mode 100644 pmoupnp/src/mediarenderer/connectionmanager/variables/a_arg_type_connectionstatus.rs create mode 100644 pmoupnp/src/mediarenderer/connectionmanager/variables/a_arg_type_direction.rs create mode 100644 pmoupnp/src/mediarenderer/connectionmanager/variables/a_arg_type_protocolinfo.rs create mode 100644 pmoupnp/src/mediarenderer/connectionmanager/variables/a_arg_type_rcsid.rs create mode 100644 pmoupnp/src/mediarenderer/connectionmanager/variables/currentconnectionids.rs create mode 100644 pmoupnp/src/mediarenderer/connectionmanager/variables/mod.rs create mode 100644 pmoupnp/src/mediarenderer/connectionmanager/variables/sinkprotocolinfo.rs create mode 100644 pmoupnp/src/mediarenderer/connectionmanager/variables/sourceprotocolinfo.rs diff --git a/pmo_src.txt b/pmo_src.txt deleted file mode 100644 index 393fb003..00000000 --- a/pmo_src.txt +++ /dev/null @@ -1,9290 +0,0 @@ -# Debut des sources des crates - -## fichier: `Cargo.toml` - -```toml -[workspace] -resolver = "3" -members = ["PMOMusic", "pmoupnp","pmoconfig", "pmoutils", "pmodidl"] -``` - -## fichier: `pmoconfig/Cargo.toml` - -```toml - -[package] -name = "pmoconfig" -version = "0.1.0" -edition = "2021" - -[dependencies] -pmoutils ={ path = "../pmoutils" } - -serde = { version = "1.0", features = ["derive"] } -serde_yaml = "0.9.33" -lazy_static = "1.4.0" -dirs = "6.0.0" -log = "0.4.20" -anyhow = "1.0.75" -uuid = { version = "1.18.1", features = ["v4"] } -tracing = "0.1.41"``` - -## fichier: `pmoconfig/src/lib.rs` - -```rust -use anyhow::{anyhow, Result}; -use dirs::home_dir; -use lazy_static::lazy_static; -use pmoutils::guess_local_ip; -use serde_yaml::{Mapping, Value}; -use std::{ - env, fs, - path::{Path, PathBuf}, - sync::{Arc, Mutex}, -}; -use tracing::{info, warn}; -use uuid::Uuid; - -// Configuration par défaut intégrée -const DEFAULT_CONFIG: &str = include_str!("pmomusic.yaml"); - -lazy_static! { - static ref CONFIG: Arc = - Arc::new(Config::load_config("").expect("Failed to load PMOMusic configuration")); -} - -const ENV_CONFIG_FILE: &str = "PMOMUSIC_CONFIG"; -const ENV_PREFIX: &str = "PMOMUSIC_CONFIG__"; - -#[derive(Debug)] -pub struct Config { - path: String, - data: Mutex, -} - -// Implémentation manuelle de Clone -impl Clone for Config { - fn clone(&self) -> Self { - let data = self.data.lock().unwrap().clone(); - Self { - path: self.path.clone(), - data: Mutex::new(data), - } - } -} - -impl Config { - pub fn load_config(filename: &str) -> Result { - let mut path = filename.to_string(); - let mut data: Option> = None; - - // Essayer de charger depuis différents emplacements - if !filename.is_empty() { - info!(config_file=%path, "Trying to load config"); - data = fs::read(&path).ok(); - if data.is_none() { - warn!(config_file=%path, "Cannot read config file"); - path.clear(); - } - } - - if path.is_empty() { - if let Ok(env_path) = env::var(ENV_CONFIG_FILE) { - info!(env_var=ENV_CONFIG_FILE, path=%env_path, "Trying to load config from env"); - path = env_path.clone(); - data = fs::read(&path).ok(); - if data.is_none() { - warn!(config_file=%path, "Cannot read config file from env var"); - path.clear(); - } - } - } - - if path.is_empty() { - let current_dir = env::current_dir().unwrap_or_else(|_| PathBuf::from(".")); - path = current_dir - .join(".pmomusic.yml") - .to_string_lossy() - .to_string(); - info!(config_file=%path, "Trying to load config file from current directory"); - data = fs::read(&path).ok(); - if data.is_none() { - warn!(config_file=%path, "Cannot read config file in current dir"); - path.clear(); - } - } - - if path.is_empty() { - path = Self::get_home_yml_path(); - info!(config_file=%path, "Trying to load config file from home directory"); - data = fs::read(&path).ok(); - if data.is_none() { - warn!(config_file=%path, "Cannot read config file in home directory"); - path.clear(); - } - } - - let yaml_data = if let Some(d) = data { - d - } else { - info!("Using default embedded config"); - DEFAULT_CONFIG.as_bytes().to_vec() - }; - - let mut config_value: Value = serde_yaml::from_slice(&yaml_data)?; - config_value = Self::lower_keys_value(config_value); - Self::apply_env_overrides(&mut config_value); - - if path.is_empty() || !Self::is_writable(&path) { - let candidates = [ - filename.to_string(), - env::var(ENV_CONFIG_FILE).unwrap_or_default(), - ".pmomusic.yml".to_string(), - Self::get_home_yml_path(), - ]; - for candidate in candidates.iter().filter(|c| !c.is_empty()) { - if Self::is_writable(candidate) { - path = candidate.clone(); - break; - } - } - } - - if path.is_empty() { - return Err(anyhow!("Cannot find a place to store config file")); - } - - info!(config_file=%path, "Config file will be stored here"); - - let config = Config { - path, - data: Mutex::new(config_value), - }; - config.save()?; - Ok(config) - } - - pub fn save(&self) -> Result<()> { - let data = self.data.lock().unwrap(); - let yaml = serde_yaml::to_string(&*data)?; - fs::write(&self.path, yaml)?; - Ok(()) - } - - pub fn set_value(&self, path: &[&str], value: Value) -> Result<()> { - let mut data = self.data.lock().unwrap(); - Self::set_value_internal(&mut data, path, value.clone())?; - drop(data); - self.save()?; - Ok(()) - } - - fn set_value_internal(data: &mut Value, path: &[&str], value: Value) -> Result<()> { - if path.is_empty() { - *data = value; - return Ok(()); - } - if let Value::Mapping(map) = data { - let key = path[0].to_lowercase(); - let key_value = Value::String(key.clone()); - if path.len() == 1 { - map.insert(key_value, value); - } else { - let entry = map - .entry(key_value) - .or_insert(Value::Mapping(Mapping::new())); - Self::set_value_internal(entry, &path[1..], value)?; - } - Ok(()) - } else { - Err(anyhow!("Current node is not a map")) - } - } - - pub fn get_value(&self, path: &[&str]) -> Result { - let data = self.data.lock().unwrap(); - Self::get_value_internal(&data, path) - } - - fn get_value_internal(data: &Value, path: &[&str]) -> Result { - let mut current = data; - for (i, key) in path.iter().enumerate() { - if let Value::Mapping(map) = current { - let key = key.to_lowercase(); - if let Some(next) = map.get(&Value::String(key)) { - current = next; - } else { - return Err(anyhow!("Path {} does not exist", path[..=i].join("."))); - } - } else { - return Err(anyhow!("Path {} is not a Config", path[..i].join("."))); - } - } - Ok(current.clone()) - } - - fn get_home_yml_path() -> String { - home_dir() - .map(|p| p.join(".pmomusic.yml")) - .unwrap_or_else(|| PathBuf::from(".")) - .to_string_lossy() - .to_string() - } - - fn apply_env_overrides(config: &mut Value) { - for (key, value) in env::vars() { - if key.starts_with(ENV_PREFIX) { - let key_path = key - .trim_start_matches(ENV_PREFIX) - .split("__") - .collect::>(); - let yaml_value = Self::convert_env_value(&value); - let _ = Self::set_value_internal(config, &key_path, yaml_value); - } - } - } - - fn convert_env_value(value: &str) -> Value { - if let Ok(parsed) = serde_yaml::from_str::(value) { - return parsed; - } - Value::String(value.to_string()) - } - - fn lower_keys_value(value: Value) -> Value { - match value { - Value::Mapping(map) => { - let mut new_map = Mapping::new(); - for (k, v) in map { - if let Value::String(s) = k { - let new_key = Value::String(s.to_lowercase()); - let new_val = Self::lower_keys_value(v); - new_map.insert(new_key, new_val); - } else { - new_map.insert(k, Self::lower_keys_value(v)); - } - } - Value::Mapping(new_map) - } - Value::Sequence(seq) => { - Value::Sequence(seq.into_iter().map(Self::lower_keys_value).collect()) - } - _ => value, - } - } - - fn is_writable(path: &str) -> bool { - let path = Path::new(path); - if let Some(parent) = path.parent() { - fs::metadata(parent) - .map(|m| !m.permissions().readonly()) - .unwrap_or(false) - } else { - false - } - } - - pub fn get_base_url(&self) -> String { - match self.get_value(&["host", "base_url"]) { - Ok(Value::String(s)) if !s.is_empty() => s, - Ok(_) => { - tracing::warn!("Base URL is not a string or empty, using default localhost"); - guess_local_ip() - } - Err(err) => { - tracing::warn!("Failed to get base URL: {}, using default localhost", err); - guess_local_ip() - } - } - } - - pub fn get_http_port(&self) -> u16 { - match self.get_value(&["host", "http_port"]) { - Ok(Value::Number(n)) if n.is_i64() => n.as_i64().unwrap() as u16, - Ok(Value::String(s)) => match s.parse::() { - Ok(port) => port, - Err(_) => { - tracing::warn!("Invalid HTTP port '{}', using default 8080", s); - 8080 - } - }, - Ok(_) => { - tracing::warn!("HTTP port not a number or string, using default 8080"); - 8080 - } - Err(err) => { - tracing::warn!("Failed to get HTTP port: {}, using default 8080", err); - 8080 - } - } - } - - pub fn get_device_udn(&self, devtype: &str, name: &str) -> Result { - let path = &["devices", devtype, name, "udn"]; - match self.get_value(path) { - Ok(Value::String(udn)) => Ok(udn), - _ => { - let new_udn = Uuid::new_v4().to_string(); - self.set_value(path, Value::String(new_udn.clone()))?; - Ok(new_udn) - } - } - } - - pub fn get_cover_cache_dir(&self) -> Result { - match self.get_value(&["host", "cover_cache", "directory"])? { - Value::String(s) => Ok(s), - _ => Ok("./.pmomusic_covers".to_string()), - } - } - - pub fn get_cover_cache_size(&self) -> Result { - match self.get_value(&["host", "cover_cache", "size"])? { - Value::Number(n) if n.is_i64() => Ok(n.as_i64().unwrap() as usize), - Value::Number(n) if n.is_u64() => Ok(n.as_u64().unwrap() as usize), - _ => Ok(2000), - } - } -} - -/// Retourne l'instance globale -pub fn get_config() -> Arc { - CONFIG.clone() -} -``` - -## fichier: `pmodidl/Cargo.toml` - -```toml -[package] -name = "pmodidl" -version = "0.1.0" -edition = "2024" - -[dependencies] -serde = "1.0.228" -utoipa = { version = "5.4.0", features = ["axum_extras"] } -utoipa-swagger-ui = { version = "9.0.2", features = ["axum"] } -quick-xml = { version = "0.38.3", features = ["serialize"] } -bevy_reflect = "0.17.1" -bevy_reflect_derive = "0.17.1" -``` - -## fichier: `pmodidl/src/lib.rs` - -```rust -//! # pmodidl - DIDL-Lite Parser -//! -//! Parser et utilitaires pour le format DIDL-Lite utilisé dans UPnP/DLNA. - -use serde::{Deserialize, Serialize}; -use std::fmt::Write; -use bevy_reflect::Reflect; - -// ============= Couche d'abstraction générique ============= - -/// Trait pour tout parser de métadonnées média -pub trait MediaMetadataParser: Sized { - type Error: std::error::Error + Send + Sync + 'static; - - /// Parse une chaîne de métadonnées - fn parse(input: &str) -> Result; - - /// Retourne le format du parser - fn format_name() -> &'static str; -} - -/// Enveloppe générique pour tout type de métadonnées parsées -#[derive(Debug, Clone, Serialize, Deserialize, Reflect)] -pub struct ParsedMetadata { - /// Format du document (ex: "DIDL-Lite", "RSS", etc.) - pub format: String, - - /// Données parsées - pub data: T, - - /// Timestamp du parsing (exclu de la réflexion car SystemTime n'implémente pas Reflect) - #[reflect(ignore)] - #[serde(skip_serializing_if = "Option::is_none")] - pub parsed_at: Option, -} - -impl ParsedMetadata { - pub fn new(format: impl Into, data: T) -> Self { - Self { - format: format.into(), - data, - parsed_at: Some(std::time::SystemTime::now()), - } - } - - /// Transforme les données avec une fonction - pub fn map(self, f: F) -> ParsedMetadata - where - F: FnOnce(T) -> U, - { - ParsedMetadata { - format: self.format, - data: f(self.data), - parsed_at: self.parsed_at, - } - } -} - -/// Fonction helper pour parser et envelopper automatiquement -pub fn parse_metadata(input: &str) -> Result, P::Error> { - let data = P::parse(input)?; - Ok(ParsedMetadata::new(P::format_name(), data)) -} - -// ============= Implémentation pour DIDLLite ============= - -impl MediaMetadataParser for DIDLLite { - type Error = quick_xml::de::DeError; - - fn parse(input: &str) -> Result { - quick_xml::de::from_str(input) - } - - fn format_name() -> &'static str { - "DIDL-Lite" - } -} - -/// Type alias pour faciliter l'utilisation -pub type DidlMetadata = ParsedMetadata; - -// ============= Structures DIDL-Lite ============= - - -/// Racine d'un document DIDL-Lite -#[derive(Debug, Clone, Serialize, Deserialize, utoipa::ToSchema, Reflect)] -#[serde(rename = "DIDL-Lite")] -pub struct DIDLLite { - #[serde(rename = "@xmlns")] - pub xmlns: String, - - #[serde(rename = "@xmlns:upnp", skip_serializing_if = "Option::is_none")] - pub xmlns_upnp: Option, - - #[serde(rename = "@xmlns:dc", skip_serializing_if = "Option::is_none")] - pub xmlns_dc: Option, - - #[serde(rename = "@xmlns:dlna", skip_serializing_if = "Option::is_none")] - pub xmlns_dlna: Option, - - #[serde(rename = "@xmlns:sec", skip_serializing_if = "Option::is_none")] - pub xmlns_sec: Option, - - #[serde(rename = "@xmlns:pv", skip_serializing_if = "Option::is_none")] - pub xmlns_pv: Option, - - #[serde(rename = "container", default)] - pub containers: Vec, - - #[serde(rename = "item", default)] - pub items: Vec, -} - -/// Container pouvant contenir d'autres containers ou items -#[derive(Debug, Clone, Serialize, Deserialize, utoipa::ToSchema, Reflect)] -pub struct Container { - #[serde(rename = "@id")] - pub id: String, - - #[serde(rename = "@parentID")] - pub parent_id: String, - - #[serde(rename = "@restricted", skip_serializing_if = "Option::is_none")] - pub restricted: Option, - - #[serde(rename = "@childCount", skip_serializing_if = "Option::is_none")] - pub child_count: Option, - - #[serde(rename = "dc:title", alias = "title")] - pub title: String, - - #[serde(rename = "upnp:class", alias = "class")] - pub class: String, - - #[serde(rename = "container", default)] - pub containers: Vec, - - #[serde(rename = "item", default)] - pub items: Vec, -} - -/// Item représentant un objet audio -#[derive(Debug, Clone, Serialize, Deserialize, utoipa::ToSchema, Reflect)] -pub struct Item { - #[serde(rename = "@id")] - pub id: String, - - #[serde(rename = "@parentID")] - pub parent_id: String, - - #[serde(rename = "@restricted", skip_serializing_if = "Option::is_none")] - pub restricted: Option, - - #[serde(rename = "dc:title", alias = "title")] - pub title: String, - - #[serde(rename = "dc:creator", alias = "creator", skip_serializing_if = "Option::is_none")] - pub creator: Option, - - #[serde(rename = "upnp:class", alias = "class")] - pub class: String, - - #[serde(rename = "upnp:artist", alias = "artist", skip_serializing_if = "Option::is_none")] - pub artist: Option, - - #[serde(rename = "upnp:album", alias = "album", skip_serializing_if = "Option::is_none")] - pub album: Option, - - #[serde(rename = "upnp:genre", alias = "genre", skip_serializing_if = "Option::is_none")] - pub genre: Option, - - #[serde(rename = "upnp:albumArtURI", alias = "albumArtURI", skip_serializing_if = "Option::is_none")] - pub album_art: Option, - - #[serde(skip)] - pub album_art_pk: Option, - - #[serde(rename = "dc:date", alias = "date", skip_serializing_if = "Option::is_none")] - pub date: Option, - - #[serde(rename = "upnp:originalTrackNumber", alias = "originalTrackNumber", skip_serializing_if = "Option::is_none")] - pub original_track_number: Option, - - #[serde(rename = "res", default)] - pub resources: Vec, - - #[serde(rename = "desc", default)] - pub descriptions: Vec, -} - -/// Ressource média (fichier audio) -#[derive(Debug, Clone, Serialize, Deserialize, utoipa::ToSchema, Reflect)] -pub struct Resource { - #[serde(rename = "@protocolInfo")] - pub protocol_info: String, - - #[serde(rename = "@bitsPerSample", skip_serializing_if = "Option::is_none")] - pub bits_per_sample: Option, - - #[serde(rename = "@sampleFrequency", skip_serializing_if = "Option::is_none")] - pub sample_frequency: Option, - - #[serde(rename = "@nrAudioChannels", skip_serializing_if = "Option::is_none")] - pub nr_audio_channels: Option, - - #[serde(rename = "@duration", skip_serializing_if = "Option::is_none")] - pub duration: Option, - - #[serde(rename = "$text")] - pub url: String, -} - -/// Description avec métadonnées additionnelles (replaygain, etc.) -#[derive(Debug, Clone, Serialize, Deserialize, utoipa::ToSchema, Reflect)] -pub struct Description { - #[serde(rename = "@id", skip_serializing_if = "Option::is_none")] - pub id: Option, - - #[serde(rename = "@nameSpace", skip_serializing_if = "Option::is_none")] - pub namespace: Option, - - #[serde(rename = "track_gain", skip_serializing_if = "Option::is_none")] - pub track_gain: Option, - - #[serde(rename = "track_peak", skip_serializing_if = "Option::is_none")] - pub track_peak: Option, -} - -// ============= Implémentation des méthodes ============= - -impl DIDLLite { - /// Itère sur tous les containers de manière récursive - pub fn all_containers(&self) -> impl Iterator { - AllContainersIter::new(&self.containers) - } - - /// Itère sur tous les items de manière récursive - pub fn all_items(&self) -> impl Iterator { - AllItemsIter::new(&self.containers, &self.items) - } - - /// Trouve un container par ID - pub fn get_container_by_id(&self, id: &str) -> Option<&Container> { - self.all_containers().find(|c| c.id == id) - } - - /// Trouve un item par ID - pub fn get_item_by_id(&self, id: &str) -> Option<&Item> { - self.all_items().find(|i| i.id == id) - } - - /// Filtre les containers - pub fn filter_containers(&self, predicate: F) -> impl Iterator - where - F: Fn(&Container) -> bool, - { - self.all_containers().filter(move |c| predicate(c)) - } - - /// Filtre les items - pub fn filter_items(&self, predicate: F) -> impl Iterator - where - F: Fn(&Item) -> bool, - { - self.all_items().filter(move |i| predicate(i)) - } - - /// Génère une représentation Markdown - pub fn to_markdown(&self) -> String { - let mut buf = String::new(); - buf.push_str("### DIDL-Lite Document\n\n"); - - if !self.containers.is_empty() { - buf.push_str("#### Containers\n\n"); - for container in &self.containers { - container.write_markdown(&mut buf, 0); - } - } - - if !self.items.is_empty() { - buf.push_str("#### Items\n\n"); - for item in &self.items { - item.write_markdown(&mut buf, 0); - } - } - - buf - } -} - -impl Container { - /// Itère sur tous les containers enfants récursivement - pub fn all_containers(&self) -> impl Iterator { - AllContainersIter::new(&self.containers) - } - - /// Itère sur tous les items de ce container et ses enfants - pub fn all_items(&self) -> impl Iterator { - AllItemsIter::new(&self.containers, &self.items) - } - - fn write_markdown(&self, buf: &mut String, depth: usize) { - let indent = " ".repeat(depth); - - writeln!(buf, "{}- **Container**: {}", indent, self.title).unwrap(); - writeln!(buf, "{} - ID: `{}`", indent, self.id).unwrap(); - writeln!(buf, "{} - ParentID: `{}`", indent, self.parent_id).unwrap(); - writeln!(buf, "{} - Class: `{}`", indent, self.class).unwrap(); - - if let Some(ref restricted) = self.restricted { - writeln!(buf, "{} - Restricted: `{}`", indent, restricted).unwrap(); - } - if let Some(ref count) = self.child_count { - writeln!(buf, "{} - ChildCount: `{}`", indent, count).unwrap(); - } - - if !self.containers.is_empty() { - writeln!(buf, "{} - Subcontainers:", indent).unwrap(); - for sub in &self.containers { - sub.write_markdown(buf, depth + 2); - } - } - - if !self.items.is_empty() { - writeln!(buf, "{} - Items:", indent).unwrap(); - for item in &self.items { - item.write_markdown(buf, depth + 2); - } - } - - buf.push('\n'); - } -} - -impl Item { - /// Itère sur les ressources audio uniquement - pub fn audio_resources(&self) -> impl Iterator { - self.resources.iter() - .filter(|r| r.protocol_info.contains("audio/")) - } - - /// Retourne la ressource principale (première disponible) - pub fn primary_resource(&self) -> Option<&Resource> { - self.resources.first() - } - - /// Itère sur les métadonnées sous forme de paires clé-valeur - pub fn metadata(&self) -> impl Iterator { - let mut pairs = Vec::new(); - - pairs.push(("title", self.title.as_str())); - - if let Some(ref artist) = self.artist { - pairs.push(("artist", artist.as_str())); - } - if let Some(ref album) = self.album { - pairs.push(("album", album.as_str())); - } - if let Some(ref genre) = self.genre { - pairs.push(("genre", genre.as_str())); - } - if let Some(ref date) = self.date { - pairs.push(("date", date.as_str())); - } - if let Some(ref track) = self.original_track_number { - pairs.push(("trackNumber", track.as_str())); - } - - for desc in &self.descriptions { - if let Some(ref gain) = desc.track_gain { - pairs.push(("replayGain", gain.as_str())); - } - if let Some(ref peak) = desc.track_peak { - pairs.push(("replayPeak", peak.as_str())); - } - } - - pairs.into_iter() - } - - fn write_markdown(&self, buf: &mut String, depth: usize) { - let indent = " ".repeat(depth); - - writeln!(buf, "{}- **Item**: {}", indent, self.title).unwrap(); - writeln!(buf, "{} - ID: `{}`", indent, self.id).unwrap(); - writeln!(buf, "{} - ParentID: `{}`", indent, self.parent_id).unwrap(); - writeln!(buf, "{} - Class: `{}`", indent, self.class).unwrap(); - - if let Some(ref creator) = self.creator { - writeln!(buf, "{} - Creator: {}", indent, creator).unwrap(); - } - if let Some(ref artist) = self.artist { - writeln!(buf, "{} - Artist: {}", indent, artist).unwrap(); - } - if let Some(ref album) = self.album { - writeln!(buf, "{} - Album: {}", indent, album).unwrap(); - } - if let Some(ref genre) = self.genre { - writeln!(buf, "{} - Genre: {}", indent, genre).unwrap(); - } - if let Some(ref art) = self.album_art { - writeln!(buf, "{} - Album Art: ![Cover]({})", indent, art).unwrap(); - } - if let Some(ref date) = self.date { - writeln!(buf, "{} - Date: {}", indent, date).unwrap(); - } - if let Some(ref track) = self.original_track_number { - writeln!(buf, "{} - Track: {}", indent, track).unwrap(); - } - - if !self.resources.is_empty() { - writeln!(buf, "{} - Resources:", indent).unwrap(); - for res in &self.resources { - writeln!(buf, "{} - URL: {}", indent, res.url).unwrap(); - writeln!(buf, "{} - Protocol: `{}`", indent, res.protocol_info).unwrap(); - if let Some(ref dur) = res.duration { - writeln!(buf, "{} - Duration: `{}`", indent, dur).unwrap(); - } - if let Some(ref bits) = res.bits_per_sample { - writeln!(buf, "{} - BitsPerSample: `{}`", indent, bits).unwrap(); - } - if let Some(ref freq) = res.sample_frequency { - writeln!(buf, "{} - SampleFrequency: `{}`", indent, freq).unwrap(); - } - if let Some(ref channels) = res.nr_audio_channels { - writeln!(buf, "{} - Channels: `{}`", indent, channels).unwrap(); - } - } - } - - if !self.descriptions.is_empty() { - writeln!(buf, "{} - Descriptions:", indent).unwrap(); - for desc in &self.descriptions { - if let Some(ref ns) = desc.namespace { - writeln!(buf, "{} - Namespace: `{}`", indent, ns).unwrap(); - } - if let Some(ref gain) = desc.track_gain { - writeln!(buf, "{} - Track Gain: `{}`", indent, gain).unwrap(); - } - if let Some(ref peak) = desc.track_peak { - writeln!(buf, "{} - Track Peak: `{}`", indent, peak).unwrap(); - } - } - } - - buf.push('\n'); - } -} - -// ============= Itérateurs personnalisés ============= - -struct AllContainersIter<'a> { - stack: Vec<&'a Container>, -} - -impl<'a> AllContainersIter<'a> { - fn new(containers: &'a [Container]) -> Self { - Self { - stack: containers.iter().collect(), - } - } -} - -impl<'a> Iterator for AllContainersIter<'a> { - type Item = &'a Container; - - fn next(&mut self) -> Option { - self.stack.pop().map(|container| { - // Ajouter les enfants à la pile - self.stack.extend(container.containers.iter()); - container - }) - } -} - -struct AllItemsIter<'a> { - containers: Vec<&'a Container>, - current_items: std::slice::Iter<'a, Item>, -} - -impl<'a> AllItemsIter<'a> { - fn new(containers: &'a [Container], items: &'a [Item]) -> Self { - Self { - containers: containers.iter().collect(), - current_items: items.iter(), - } - } -} - -impl<'a> Iterator for AllItemsIter<'a> { - type Item = &'a Item; - - fn next(&mut self) -> Option { - loop { - if let Some(item) = self.current_items.next() { - return Some(item); - } - - let container = self.containers.pop()?; - self.containers.extend(container.containers.iter()); - self.current_items = container.items.iter(); - } - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_parse_simple_didl() { - let xml = r#" - - - Test Song - object.item.audioItem.musicTrack - http://example.com/song.mp3 - - - "#; - - let didl = DIDLLite::parse(xml).unwrap(); - assert_eq!(didl.items.len(), 1); - assert_eq!(didl.items[0].title, "Test Song"); - } - - #[test] - fn test_parse_without_namespaces() { - // Teste un XML sans namespaces explicites (devices UPnP laxistes) - let xml = r#" - - - Test Song - object.item.audioItem.musicTrack - http://example.com/song.mp3 - - - "#; - - let didl = DIDLLite::parse(xml).unwrap(); - assert_eq!(didl.items.len(), 1); - assert_eq!(didl.items[0].title, "Test Song"); - } - - #[test] - fn test_generic_parser() { - let xml = r#" - - - "#; - - // Utiliser le parser générique - let metadata: DidlMetadata = parse_metadata(xml).unwrap(); - - assert_eq!(metadata.format, "DIDL-Lite"); - assert!(metadata.parsed_at.is_some()); - } - - #[test] - fn test_metadata_map() { - let xml = r#" - - - "#; - - let metadata: DidlMetadata = parse_metadata(xml).unwrap(); - - // Transformer les données - let item_count = metadata.map(|didl| didl.items.len()); - - assert_eq!(item_count.format, "DIDL-Lite"); - assert_eq!(item_count.data, 0); - } -}``` - -## fichier: `PMOMusic/Cargo.toml` - -```toml -[package] -name = "PMOMusic" -version = "0.1.0" -edition = "2024" - -[dependencies] -pmoconfig = { path = "../pmoconfig" } -pmoupnp = { path = "../pmoupnp"} - - -tokio = { version = "1.35", features = ["rt-multi-thread", "macros", "sync", "time","signal"] } -tracing = "0.1.41" -tracing-subscriber = "0.3.20" -axum = "0.8.4" -serde_json = "1.0.145" -``` - -## fichier: `PMOMusic/src/main.rs` - -```rust -use pmoupnp::{mediarenderer::avtransport::actions::{SETAVTRANSPORTURI}, server::{ - logs::{log_dump, log_sse, LogState, SseLayer}, ServerBuilder, Webapp -}, UpnpObject}; // ton module pmoupnp::server -use tracing_subscriber::Registry; -use tracing_subscriber::prelude::*; -use tracing::info; - -#[tokio::main] -async fn main() { - // Charger la config - - let mut server = ServerBuilder::new_configured().build(); - - // Ajouter des routes - server - .add_route("/hello", || async { - serde_json::json!({"message": "Hello World"}) - }) - .await; - - server - .add_route("/info", || async { - serde_json::json!({"version": "1.0.0"}) - }) - .await; - - server.add_spa::("/app").await; - - // Gère la sortie des logs et sur le serveur SSE pour l'interface web et sur la console - let log_state = LogState::new(1000); - let subscriber = Registry::default() - .with( - tracing_subscriber::fmt::layer() - .with_target(true) - .with_level(true) - .with_ansi(true), // Couleurs dans le terminal - ) - .with(SseLayer::new(log_state.clone())); - tracing::subscriber::set_global_default(subscriber).unwrap(); - - server - .add_handler_with_state("/log-sse", log_sse, log_state.clone()) - .await; - server - .add_handler_with_state("/log-dump", log_dump, log_state.clone()) - .await; - - server.add_redirect("/", "/app").await; - - info!("{}",SETAVTRANSPORTURI.to_markdown()); - - server.start().await; - server.wait().await; -} -``` - -## fichier: `pmoupnp/Cargo.toml` - -```toml -[package] -name = "pmoupnp" -version = "0.1.0" -edition = "2024" - -[dependencies] -pmoconfig = { path = "../pmoconfig" } -pmodidl = { path = "../pmodidl"} - -url = "2.5.7" -uuid = "1.18.1" -hex = "0.4.3" -base64 = "0.22.1" -thiserror = "2.0.16" -xmltree = "0.11.0" -get_if_addrs = "0.5.3" -axum = "0.8.4" -tokio = { version = "1.35", features = ["rt-multi-thread", "macros", "sync", "time"] } -tokio-stream = "0.1" -futures-util = "0.3" -serde = { version = "1.0", features = ["derive"] } -serde_json = "1.0" -chrono = { version = "0.4.42", features = ["serde"] } -log = "0.4.28" -once_cell = "1.20" -parking_lot = "0.12" -tracing = "0.1" -tracing-subscriber = { version = "0.3", features = ["fmt", "env-filter"] } -futures = "0.3" -async-stream = "0.3.6" -axum-server = "0.7.2" -axum-embed = "0.1.0" -rust-embed = "8.7.2" -anyhow = "1.0" -utoipa = { version = "5.4.0", features = ["axum_extras"] } -utoipa-swagger-ui = { version = "9.0.2", features = ["axum"] } -validator = { version = "0.20.0", features = ["derive"] } -bevy_reflect = "0.17.1" -bevy_reflect_derive = "0.17.1" -reqwest = "0.12.23" -``` - -## fichier: `pmoupnp/webapp/src/App.vue` - -```vue - - - - - -``` - -## fichier: `pmoupnp/webapp/src/main.ts` - -```typescript -import { createApp } from "vue"; -import App from "./App.vue"; -import router from "./router"; - -import "./style.css"; - -createApp(App).use(router).mount("#app"); -``` - -## fichier: `pmoupnp/webapp/src/components/LogView.vue` - -```vue - - - - -``` - -## fichier: `pmoupnp/webapp/src/components/HelloWorld.vue` - -```vue - - - - - -``` - -## fichier: `pmoupnp/webapp/src/style.css` - -```css -:root { - font-family: system-ui, Avenir, Helvetica, Arial, sans-serif; - line-height: 1.5; - font-weight: 400; - - color-scheme: light dark; - color: rgba(255, 255, 255, 0.87); - background-color: #242424; - - font-synthesis: none; - text-rendering: optimizeLegibility; - -webkit-font-smoothing: antialiased; - -moz-osx-font-smoothing: grayscale; -} - -a { - font-weight: 500; - color: #646cff; - text-decoration: inherit; -} -a:hover { - color: #535bf2; -} - -body { - margin: 0; - display: flex; - place-items: center; - min-width: 320px; - min-height: 100vh; - width: 100vw; -} - -h1 { - font-size: 3.2em; - line-height: 1.1; -} - -button { - border-radius: 8px; - border: 1px solid transparent; - padding: 0.6em 1.2em; - font-size: 1em; - font-weight: 500; - font-family: inherit; - background-color: #1a1a1a; - cursor: pointer; - transition: border-color 0.25s; -} -button:hover { - border-color: #646cff; -} -button:focus, -button:focus-visible { - outline: 4px auto -webkit-focus-ring-color; -} - -.card { - padding: 2em; -} - -#app { - max-width: 1280px; - margin: 0 auto; - padding: 2rem; - text-align: center; -} - -@media (prefers-color-scheme: light) { - :root { - color: #213547; - background-color: #ffffff; - } - a:hover { - color: #747bff; - } - button { - background-color: #f9f9f9; - } -} -``` - -## fichier: `pmoupnp/webapp/src/shims-vue.d.ts` - -```typescript -declare module "*.vue" { - import { DefineComponent } from "vue"; - const component: DefineComponent<{}, {}, any>; - export default component; -} -``` - -## fichier: `pmoupnp/webapp/src/router/index.ts` - -```typescript -import { createRouter, createWebHistory } from "vue-router"; -import HelloWorld from "../components/HelloWorld.vue"; -import LogView from "../components/LogView.vue"; - -const routes = [ - { path: "/", name: "home", component: HelloWorld }, - { path: "/logs", name: "logs", component: LogView }, -]; - -const router = createRouter({ - // history avec base /app - history: createWebHistory("/app"), - routes, -}); - -export default router; -``` - -## fichier: `pmoupnp/errors.rs` - -```rust -use thiserror::Error; - - - -#[derive(Error, Debug)] -pub enum StateVariableError { - #[error("Conversion error: {0}")] - ConversionError(String), - - #[error("Validation error: {0}")] - ValidationError(String), - - #[error("Range error: {0}")] - RangeError(String), - - #[error("Type error: {0}")] - TypeError(String), - - #[error("Parse error: {0}")] - ParseError(String), - - #[error("Event condition error: {0}")] - EventConditionError(String), - - #[error("Arithmetic error: {0}")] - ArithmeticError(String), - - #[error("Unknown error: {0}")] - Unknown(String), -} - -impl From for StateVariableError { - fn from(err: std::num::TryFromIntError) -> Self { - StateVariableError::ConversionError(format!("Integer conversion error: {}", err)) - } -} - -impl From for StateVariableError { - fn from(err: std::str::ParseBoolError) -> Self { - StateVariableError::ConversionError(format!("Boolean conversion error: {}", err)) - } -} - -impl From for StateVariableError { - fn from(err: uuid::Error) -> Self { - StateVariableError::ConversionError(format!("UUID conversion error: {}", err)) - } -} - -impl From for StateVariableError { - fn from(err: chrono::ParseError) -> Self { - StateVariableError::ConversionError(format!("Time conversion error: {}", err)) - } -} - -impl From for StateVariableError { - fn from(err: url::ParseError) -> Self { - StateVariableError::ConversionError(format!("URI conversion error: {}", err)) - } -} - -impl From for StateVariableError { - fn from(err: base64::DecodeError) -> Self { - StateVariableError::ConversionError(format!("Base64 conversion error: {}", err)) - } -} - -impl From for StateVariableError { - fn from(err: hex::FromHexError) -> Self { - StateVariableError::ConversionError(format!("Hex conversion error: {}", err)) - } -} -``` - -## fichier: `pmoupnp/src/object_set.rs` - -```rust -use std::{collections::HashMap, sync::Arc}; - -use std::sync::RwLock; - -use crate::{UpnpDeepClone, UpnpObjectSet, UpnpObjectSetError, UpnpTypedObject}; - -/// Implémentation du clonage profond pour `UpnpObjectSet`. -/// -/// Cette implémentation crée une copie complète et indépendante du set, -/// en clonant chaque objet `T` et en créant de nouveaux `Arc` autour de ces clones. -/// Les modifications sur l'un des sets n'affectent pas l'autre. -impl UpnpDeepClone for UpnpObjectSet { - fn deep_clone(&self) -> Self { - let guard = self.objects.read().unwrap(); - - let cloned_map: HashMap> = guard - .iter() - .map(|(key, arc)| (key.clone(), Arc::new((**arc).clone()))) - .collect(); - - Self { - objects: RwLock::new(cloned_map), - } - } -} - -/// Implémentation du clonage superficiel pour `UpnpObjectSet`. -/// -/// Cette implémentation crée une copie du set qui **partage** les objets `T` -/// via les `Arc`. C'est beaucoup plus rapide et économe en mémoire qu'un clonage -/// profond, car seuls les pointeurs `Arc` sont clonés (incrémentation du compteur -/// de références). -/// -/// # Note -/// -/// Les deux sets partagent les mêmes instances d'objets `T`. Si `T` contient -/// de la mutabilité interne (via `Mutex`, `RwLock`, etc.), les modifications -/// seront visibles depuis les deux sets. -impl Clone for UpnpObjectSet { - fn clone(&self) -> Self { - let guard = self.objects.read().unwrap(); - - Self { - objects: RwLock::new(guard.clone()), - } - } -} - -impl UpnpObjectSet { - /// Crée un nouveau `UpnpObjectSet` vide. - /// - /// # Examples - /// - /// ``` - /// let set: UpnpObjectSet = UpnpObjectSet::new(); - /// ``` - pub fn new() -> Self { - Self { - objects: RwLock::new(HashMap::new()), - } - } - - /// Insère un objet dans le set. - /// - /// # Arguments - /// - /// * `object` - L'objet à insérer, encapsulé dans un `Arc` - /// - /// # Returns - /// - /// * `Ok(())` - Si l'insertion a réussi - /// * `Err(UpnpObjectSetError::AlreadyExists)` - Si un objet avec le même nom existe déjà - /// - /// # Examples - /// - /// ``` - /// let mut set = UpnpObjectSet::new(); - /// let obj = Arc::new(MyObject::new("test")); - /// set.insert(obj)?; - /// ``` - pub fn insert(&mut self, object: Arc) -> Result<(), UpnpObjectSetError> { - let mut guard = self.objects.write().unwrap(); - let key = object.get_name().to_string(); - - if guard.contains_key(&key) { - return Err(UpnpObjectSetError::AlreadyExists(key)); - } - - guard.insert(key, object); - Ok(()) - } - - /// Insère un objet dans le set, ou remplace l'objet existant s'il y en a un avec le même nom. - /// - /// Cette méthode ne retourne jamais d'erreur et écrase silencieusement tout objet existant. - /// - /// # Arguments - /// - /// * `object` - L'objet à insérer ou remplacer, encapsulé dans un `Arc` - /// - /// # Examples - /// - /// ``` - /// let mut set = UpnpObjectSet::new(); - /// let obj1 = Arc::new(MyObject::new("test")); - /// let obj2 = Arc::new(MyObject::new("test")); // Même nom - /// - /// set.insert_or_replace(obj1); - /// set.insert_or_replace(obj2); // Remplace obj1 - /// ``` - pub fn insert_or_replace(&mut self, object: Arc) { - let mut guard = self.objects.write().unwrap(); - let key: String = object.get_name().to_string(); - - guard.insert(key, object); - } - - /// Vérifie si le set contient un objet donné. - /// - /// La vérification se base sur le nom de l'objet retourné par `get_name()`. - /// - /// # Arguments - /// - /// * `object` - L'objet à rechercher - /// - /// # Returns - /// - /// `true` si un objet avec le même nom existe dans le set, `false` sinon. - /// - /// # Examples - /// - /// ``` - /// let set = UpnpObjectSet::new(); - /// let obj = Arc::new(MyObject::new("test")); - /// - /// if set.contains(obj.clone()) { - /// println!("L'objet existe déjà"); - /// } - /// ``` - pub fn contains(&self, object: Arc) -> bool { - let guard = self.objects.read().unwrap(); - let key: String = object.get_name().to_string(); - - guard.contains_key(&key) - } - - /// Récupère un objet par son nom. - /// - /// # Arguments - /// - /// * `name` - Le nom de l'objet à rechercher - /// - /// # Returns - /// - /// * `Some(Arc)` - Si un objet avec ce nom existe - /// * `None` - Si aucun objet n'est trouvé - /// - /// # Examples - /// - /// ``` - /// let set = UpnpObjectSet::new(); - /// - /// if let Some(obj) = set.get_by_name("test") { - /// println!("Objet trouvé: {}", obj.get_name()); - /// } - /// ``` - pub fn get_by_name(&self, name: &str) -> Option> { - let guard = self.objects.read().unwrap(); - guard.get(name).cloned() - } - - /// Retourne tous les objets du set. - /// - /// # Returns - /// - /// Un vecteur contenant des clones des `Arc` pointant vers tous les objets du set. - /// L'ordre des éléments n'est pas garanti. - /// - /// # Examples - /// - /// ``` - /// let set = UpnpObjectSet::new(); - /// - /// for obj in set.all() { - /// println!("Objet: {}", obj.get_name()); - /// } - /// ``` - /// - /// # Thread-safety - /// - /// Cette méthode acquiert un verrou de lecture. Plusieurs threads peuvent - /// appeler cette méthode simultanément sans blocage. - pub fn all(&self) -> Vec> { - let guard = self.objects.read().unwrap(); - guard.values().cloned().collect() - } -}``` - -## fichier: `pmoupnp/src/state_variables/instance_methods.rs` - -```rust -use std::fmt; - -use chrono::{DateTime, Utc}; -use std::sync::RwLock; -use xmltree::Element; - -use crate::{ - object_trait::{UpnpInstance, UpnpObject}, - state_variables::{StateVarInstance, StateVariable, UpnpVariable}, - variable_types::{StateValue, StateValueError, UpnpVarType}, - UpnpObjectType, UpnpTyped, UpnpTypedInstance -}; - -impl UpnpVariable for StateVarInstance { - fn get_definition(&self) -> &StateVariable { - return &self.model; - } -} - -impl UpnpObject for StateVarInstance { - fn to_xml_element(&self) -> Element { - self.get_definition().to_xml_element() - } -} - -impl UpnpVarType for StateVarInstance { - fn as_state_var_type(&self) -> crate::variable_types::StateVarType { - self.get_definition().as_state_var_type() - } -} - -impl UpnpInstance for StateVarInstance { - type Model = StateVariable; - - fn new(from: &StateVariable) -> Self { - Self { - object: UpnpObjectType { - name: from.object.name.clone(), - object_type: "StateVarInstance".to_string(), - }, - model: from.clone(), - value: RwLock::new(from.get_default()), - old_value: RwLock::new(from.get_default()), - last_modified: RwLock::new(Utc::now()), - last_notification: RwLock::new(Utc::now()), - } - } - -} - -impl UpnpTyped for StateVarInstance { - fn as_upnp_object_type(&self) -> &UpnpObjectType { - return &self.object; - } -} - -impl UpnpTypedInstance for StateVarInstance { - - fn get_model(&self) -> &Self::Model { - &self.model - } -} - -impl fmt::Debug for StateVarInstance { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - f.debug_struct("StateVarInstance") - .field("object", &self.object) - .field("model", &self.model) - .field("value", &self.value) - .field("old_value", &self.old_value) - .field("last_modified", &self.last_modified) - .field("last_notification", &self.last_notification) - .finish() - } -} - -impl Clone for StateVarInstance { - fn clone(&self) -> Self { - Self { - object: self.object.clone(), - model: self.model.clone(), - value: RwLock::new(self.value.read().unwrap().clone()), - old_value: RwLock::new(self.old_value.read().unwrap().clone()), - last_modified: RwLock::new(self.last_modified.read().unwrap().clone()), - last_notification: RwLock::new(self.last_notification.read().unwrap().clone()), - } - } -} - -impl StateVarInstance { - pub async fn set_value(&self, new_value: StateValue) -> Result<(), StateValueError> { - // Validation du type - if self.as_state_var_type() != new_value.as_state_var_type() { - return Err(StateValueError::TypeError( - "Value type mismatch".to_string() - )); - } - - // Mise à jour avec les locks - let mut old_val = self.old_value.write().unwrap(); - let mut val = self.value.write().unwrap(); - let mut modified = self.last_modified.write().unwrap(); - - *old_val = val.clone(); - *val = new_value; - *modified = Utc::now(); - - Ok(()) - } - /// Accès à la valeur - pub fn value(&self) -> StateValue { - self.value.read().unwrap().clone() - } - - /// Accès au timestamp - pub fn last_modified(&self) -> DateTime { - self.last_modified.read().unwrap().clone() - } -} -``` - -## fichier: `pmoupnp/src/state_variables/var_set_methods.rs` - -```rust -use xmltree::{Element, XMLNode}; - -use crate::{object_trait::UpnpModel, state_variables::{StateVarInstanceSet, StateVariableSet}, UpnpObject}; - - -impl UpnpObject for StateVariableSet { - - fn to_xml_element(&self) -> Element { - let mut elem = Element::new("serviceStateTable"); - - for state_var in self.all() { - let state_var_elem = state_var.to_xml_element(); // retourne un complet - elem.children.push(XMLNode::Element(state_var_elem)); - } - - elem - } - -} - -impl UpnpModel for StateVariableSet { - type Instance = StateVarInstanceSet; -} - - -``` - -## fichier: `pmoupnp/src/state_variables/variable_trait.rs` - -```rust -use crate::{ - state_variables::StateVariable, - variable_types::{StateValue, UpnpVarType}, -}; - -/// Trait pour accéder aux propriétés et contraintes d'une variable UPnP. -/// -/// Ce trait fournit une interface uniforme pour interroger les métadonnées, -/// contraintes et comportements d'une variable UPnP, qu'il s'agisse d'une -/// définition ([`StateVariable`]) ou d'une instance ([`StateVarInstance`]). -/// -/// # Architecture -/// -/// Le trait utilise le pattern "trait avec implémentation par défaut" : -/// - Une seule méthode requise : [`get_definition`](Self::get_definition) -/// - Toutes les autres méthodes sont implémentées par défaut en déléguant à la définition -/// -/// Cela permet une interface cohérente entre modèles et instances sans duplication de code. -/// -/// # Hiérarchie -/// -/// ```text -/// UpnpVariable -/// ├─> StateVariable (get_definition() retourne self) -/// └─> StateVarInstance (get_definition() retourne self.definition) -/// ``` -/// -/// # Examples -/// -/// ```ignore -/// fn display_variable_info(var: &V) { -/// println!("Variable: {}", var.get_definition().get_name()); -/// -/// if var.has_default() { -/// println!("Default: {:?}", var.get_default()); -/// } -/// -/// if var.has_range() { -/// println!("Has range constraints"); -/// } -/// -/// if var.has_allowed_values() { -/// println!("Has allowed values list"); -/// } -/// } -/// ``` -pub trait UpnpVariable { - /// Retourne une référence vers la définition de la variable. - /// - /// Cette méthode est la base de toutes les autres méthodes du trait. - /// - /// # Implementation - /// - /// - Pour [`StateVariable`] : retourne `self` - /// - Pour [`StateVarInstance`] : retourne `self.definition` - fn get_definition(&self) -> &StateVariable; - - /// Indique si la variable a un pas (step) défini. - /// - /// Le pas définit l'incrément minimal entre deux valeurs valides pour - /// les types numériques. - /// - /// # Returns - /// - /// `true` si un pas est défini, `false` sinon. - /// - /// # Examples - /// - /// ```ignore - /// if var.has_step() { - /// println!("Step: {:?}", var.get_step()); - /// } - /// ``` - fn has_step(&self) -> bool { - self.get_definition().step.is_some() - } - - /// Retourne le pas (step) de la variable s'il est défini. - /// - /// # Returns - /// - /// - `Some(StateValue)` si un pas est défini - /// - `None` sinon - /// - /// # See also - /// - /// - [`has_step`](Self::has_step) pour tester l'existence - fn get_step(&self) -> Option { - self.get_definition().step.clone() - } - - /// Indique si la variable a une plage de valeurs (range) définie. - /// - /// La plage définit les valeurs minimale et maximale acceptables. - /// - /// # Returns - /// - /// `true` si une plage est définie, `false` sinon. - fn has_range(&self) -> bool { - self.get_definition().value_range.is_some() - } - - /// Indique si la variable est modifiable. - /// - /// Une variable non modifiable est en lecture seule. - /// - /// # Returns - /// - /// `true` si la variable peut être modifiée, `false` sinon. - fn is_modifiable(&self) -> bool { - self.get_definition().modifiable - } - - /// Indique si la variable a des conditions d'événement définies. - /// - /// Les conditions d'événement déterminent quand des notifications - /// doivent être envoyées lors de changements de valeur. - /// - /// # Returns - /// - /// `true` si au moins une condition d'événement existe, `false` sinon. - /// - /// # Note - /// - /// Retourne `false` si le lock est empoisonné (poisoned). - fn has_event_conditions(&self) -> bool { - let guard = self.get_definition().event_conditions.read().unwrap(); - !guard.is_empty() - } - - /// Vérifie si une condition d'événement spécifique existe. - /// - /// # Arguments - /// - /// * `name` - Le nom de la condition à rechercher - /// - /// # Returns - /// - /// `true` si la condition existe, `false` sinon. - /// - /// # Note - /// - /// Retourne `false` si le lock est empoisonné (poisoned). - fn has_event_condition(&self, name: &String) -> bool { - let guard = self.get_definition().event_conditions.read().unwrap(); - guard.contains_key(name) - } - - /// Indique si la variable a une description non vide. - /// - /// # Returns - /// - /// `true` si une description existe et n'est pas vide, `false` sinon. - fn has_description(&self) -> bool { - !self.get_definition().description.is_empty() - } - - /// Retourne la description de la variable. - /// - /// # Returns - /// - /// La description sous forme de `String`. Peut être vide. - /// - /// # See also - /// - /// - [`has_description`](Self::has_description) pour tester si non vide - fn get_description(&self) -> String { - self.get_definition().description.clone() - } - - /// Indique si la variable a une valeur par défaut définie explicitement. - /// - /// # Returns - /// - /// `true` si une valeur par défaut est explicitement définie, `false` sinon. - /// - /// # Note - /// - /// Même si cette méthode retourne `false`, [`get_default`](Self::get_default) - /// retournera toujours une valeur (la valeur par défaut du type). - fn has_default(&self) -> bool { - self.get_definition().default_value.is_some() - } - - /// Retourne la valeur par défaut de la variable. - /// - /// # Returns - /// - /// La valeur par défaut. Si aucune valeur par défaut n'est explicitement - /// définie, retourne la valeur par défaut du type de la variable - /// (ex: 0 pour les entiers, chaîne vide pour String, etc.). - /// - /// # Examples - /// - /// ```ignore - /// let default = var.get_default(); - /// println!("Default value: {:?}", default); - /// ``` - fn get_default(&self) -> StateValue { - self.get_definition() - .default_value - .clone() - .unwrap_or_else(|| self.get_definition().as_state_var_type().default_value()) - } - - /// Indique si la variable a une liste de valeurs autorisées. - /// - /// Lorsqu'une liste de valeurs autorisées est définie, seules ces valeurs - /// sont acceptables pour la variable. - /// - /// # Returns - /// - /// `true` si une liste non vide de valeurs autorisées existe, `false` sinon. - /// - /// # Note - /// - /// Retourne `false` si le lock est empoisonné (poisoned). - fn has_allowed_values(&self) -> bool { - let guard = self.get_definition() - .allowed_values - .read().unwrap(); - - !guard.is_empty() - } - - /// Vérifie si une valeur fait partie des valeurs autorisées. - /// - /// # Arguments - /// - /// * `value` - La valeur à vérifier - /// - /// # Returns - /// - /// `true` si la valeur est dans la liste des valeurs autorisées, `false` sinon. - /// Retourne également `false` si aucune liste de valeurs autorisées n'est définie - /// ou si le lock est empoisonné. - /// - /// # Examples - /// - /// ```ignore - /// let value = StateValue::String("ON".to_string()); - /// if var.is_an_allowed_value(&value) { - /// println!("Value is allowed"); - /// } - /// ``` - /// - /// # Note - /// - /// Si aucune liste de valeurs autorisées n'est définie, cette méthode - /// retourne `false`. Utilisez [`has_allowed_values`](Self::has_allowed_values) - /// pour distinguer "pas de liste" de "valeur non autorisée". - fn is_an_allowed_value(&self, value: &StateValue) -> bool { - let guard = self.get_definition() - .allowed_values - .read().unwrap(); - - guard.contains(value) - } - - /// Indique si la variable envoie des notifications d'événement. - /// - /// Les notifications d'événement sont envoyées aux abonnés lorsque - /// la valeur de la variable change. - /// - /// # Returns - /// - /// `true` si les notifications sont activées, `false` sinon. - /// - /// # See also - /// - /// - [`has_event_conditions`](Self::has_event_conditions) pour vérifier - /// les conditions d'envoi d'événements - fn is_sending_notification(&self) -> bool { - self.get_definition().send_events - } - - /// Indique si la variable a un parser de valeur personnalisé. - /// - /// Un parser personnalisé est utilisé pour convertir des chaînes de - /// caractères en valeurs typées. Disponible uniquement pour les variables - /// de type String. - /// - /// # Returns - /// - /// `true` si un parser est défini, `false` sinon. - fn has_value_parser(&self) -> bool { - self.get_definition().parse.is_some() - } - - /// Indique si la variable a un marshaler de valeur personnalisé. - /// - /// Un marshaler personnalisé est utilisé pour sérialiser des valeurs - /// en chaînes de caractères. Disponible uniquement pour les variables - /// de type String. - /// - /// # Returns - /// - /// `true` si un marshaler est défini, `false` sinon. - fn has_value_marshaler(&self) -> bool { - self.get_definition().marshal.is_some() - } -} -``` - -## fichier: `pmoupnp/src/state_variables/mod.rs` - -```rust -mod errors; -mod instance_methods; -mod variable_methods; -mod var_set_methods; -mod var_inst_set_methods; -mod variable_trait; - -use std::{ - collections::HashMap, - sync::Arc, -}; - -pub use crate::state_variables::variable_trait::UpnpVariable; -use bevy_reflect::Reflect; -use chrono::{DateTime, Utc}; -pub use errors::StateVariableError; -use std::sync::RwLock; - -use crate::{ - value_ranges::ValueRange, - variable_types::{StateValue, StateVarType}, - UpnpObjectSet, UpnpObjectType, -}; - -/// Type pour les fonctions de condition d'événement -pub type StateConditionFunc = Arc bool + Send + Sync>; - -/// Type pour les fonctions de parsing de valeurs depuis des chaînes -pub type StringValueParser = - Arc Result, StateVariableError> + Send + Sync>; - -/// Type pour les fonctions de sérialisation de valeurs vers des chaînes -pub type ValueSerializer = - Arc Result + Send + Sync>; - -pub struct StateVariable { - object: UpnpObjectType, - value_type: StateVarType, - step: Option, - modifiable: bool, - event_conditions: Arc>>, - description: String, - default_value: Option, - value_range: Option, - allowed_values: Arc>>, - send_events: bool, - parse: Option, - marshal: Option, -} - -pub type StateVariableSet = UpnpObjectSet; - -pub struct StateVarInstance { - object: UpnpObjectType, - model: StateVariable, - value: RwLock, - old_value: RwLock, - last_modified: RwLock>, - last_notification: RwLock>, -} - -pub type StateVarInstanceSet = UpnpObjectSet; - -``` - -## fichier: `pmoupnp/src/state_variables/var_inst_set_methods.rs` - -```rust -use std::collections::HashMap; - -use std::sync::RwLock; -use xmltree::{Element, XMLNode}; - -use crate::{state_variables::{StateVarInstanceSet, StateVariableSet}, UpnpObject}; - -use crate::UpnpInstance; - -impl UpnpObject for StateVarInstanceSet { - fn to_xml_element(&self) -> Element { - let mut elem = Element::new("serviceStateTable"); - - for state_var in self.all() { - let state_var_elem = state_var.to_xml_element(); // retourne un complet - elem.children.push(XMLNode::Element(state_var_elem)); - } - - elem - } -} - -impl UpnpInstance for StateVarInstanceSet { - type Model = StateVariableSet; - - fn new(_: &StateVariableSet) -> Self { - Self { objects: RwLock::new(HashMap::new()) } - } - - -} - - -``` - -## fichier: `pmoupnp/src/state_variables/errors.rs` - -```rust -use thiserror::Error; - -#[derive(Error, Debug)] -pub enum StateVariableError { - #[error("Conversion error: {0}")] - ConversionError(String), - - #[error("Validation error: {0}")] - ValidationError(String), - - #[error("Range error: {0}")] - RangeError(String), - - #[error("Type error: {0}")] - TypeError(String), - - #[error("Parse error: {0}")] - ParseError(String), - - #[error("Event condition error: {0}")] - EventConditionError(String), - - #[error("Arithmetic error: {0}")] - ArithmeticError(String), - - #[error("Unknown error: {0}")] - Unknown(String), -} -``` - -## fichier: `pmoupnp/src/state_variables/variable_methods.rs` - -```rust -use std::{ - collections::HashMap, - fmt, - sync::Arc, -}; - -use std::sync::RwLock; -use xmltree::{Element, XMLNode}; - -use crate::{ - UpnpObjectType, UpnpTyped, - object_trait::{UpnpModel, UpnpObject}, - state_variables::{ - StateConditionFunc, StateVarInstance, StateVariable, StringValueParser, ValueSerializer, - variable_trait::UpnpVariable, - }, - value_ranges::ValueRange, - variable_types::{StateValue, StateValueError, StateVarType, UpnpVarType}, -}; - -impl UpnpTyped for StateVariable { - fn as_upnp_object_type(&self) -> &UpnpObjectType { - &self.object - } -} - -impl UpnpVarType for StateVariable { - fn as_state_var_type(&self) -> StateVarType { - self.value_type.as_state_var_type() // utilise ton From<&StateValue> existant - } -} - -impl UpnpObject for StateVariable { - fn to_xml_element(&self) -> Element { - // Création de l'élément racine - let mut root = Element::new("stateVariable"); - root.attributes.insert( - "sendEvents".to_string(), - if self.send_events { "yes" } else { "no" }.to_string(), - ); - - // - let mut name_elem = Element::new("name"); - name_elem - .children - .push(XMLNode::Text(self.get_name().clone())); - - // - let mut datatype_elem = Element::new("dataType"); - datatype_elem - .children - .push(XMLNode::Text(self.value_type.to_string())); // StateVarType doit impl Display - - // si défini - if let Some(default) = &self.default_value { - let mut def_elem = Element::new("defaultValue"); - def_elem.children.push(XMLNode::Text(default.to_string())); - root.children.push(XMLNode::Element(def_elem)); - } - - // si défini - let av = self.allowed_values.read().unwrap(); - if !av.is_empty() { - let mut list_elem = Element::new("allowedValueList"); - for val in av.iter() { - let mut val_elem = Element::new("allowedValue"); - val_elem.children.push(XMLNode::Text(val.to_string())); - list_elem.children.push(XMLNode::Element(val_elem)); - } - root.children.push(XMLNode::Element(list_elem)); - } - - // si défini - if let Some(range) = &self.value_range { - let mut range_elem = Element::new("allowedValueRange"); - - let mut min_elem = Element::new("minimum"); - min_elem - .children - .push(XMLNode::Text(range.get_minimum().to_string())); - range_elem.children.push(XMLNode::Element(min_elem)); - - let mut max_elem = Element::new("maximum"); - max_elem - .children - .push(XMLNode::Text(range.get_maximum().to_string())); - range_elem.children.push(XMLNode::Element(max_elem)); - - if let Some(step) = &self.step { - let mut step_elem = Element::new("step"); - step_elem.children.push(XMLNode::Text(step.to_string())); - range_elem.children.push(XMLNode::Element(step_elem)); - } - - root.children.push(XMLNode::Element(range_elem)); - } - - // Ajouter les enfants communs - root.children.push(XMLNode::Element(name_elem)); - root.children.push(XMLNode::Element(datatype_elem)); - - root - } -} - -impl UpnpModel for StateVariable { - type Instance = StateVarInstance; -} - -impl Clone for StateVariable { - fn clone(&self) -> Self { - // clone safe des structures protégées par RwLock en prenant un read lock - let event_conditions_clone = { - // si le lock est "poisoned" on panic - tu peux adapter la gestion si tu veux - let guard = self - .event_conditions - .read().unwrap(); - // nécessite que Key: Clone, Value: Clone - Arc::new(RwLock::new(guard.clone())) - }; - - let allowed_values_clone = { - let guard = self - .allowed_values - .read().unwrap(); - Arc::new(RwLock::new(guard.clone())) - }; - - Self { - object: self.object.clone(), - value_type: self.value_type.clone(), - step: self.step.clone(), - modifiable: self.modifiable, - event_conditions: event_conditions_clone, - description: self.description.clone(), - default_value: self.default_value.clone(), - value_range: self.value_range.clone(), - allowed_values: allowed_values_clone, - send_events: self.send_events, - // parse et marshal sont typiquement des Arc — on clone l'Arc (shallow). - // Deep-cloner une closure ou un trait-objet n'est pas possible en général. - parse: self.parse.clone(), - marshal: self.marshal.clone(), - } - } -} - -impl fmt::Debug for StateVariable { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - f.debug_struct("StateVariable") - .field("object", &self.object) - .field("value_type", &self.value_type) - .field("step", &self.step) - .field("modifiable", &self.modifiable) - .field( - "event_conditions", - &format_args!( - "len={}", - self.event_conditions.read().unwrap().len() - ), - ) - .field("description", &self.description) - .field("default_value", &self.default_value) - .field("value_range", &self.value_range) - .field( - "allowed_values", - &format_args!( - "len={}", - self.allowed_values.read().unwrap().len() - ), - ) - .field("send_events", &self.send_events) - .field( - "parse", - &self - .parse - .as_ref() - .map(|_| "Some(StringValueParser)") - .unwrap_or("None"), - ) - .field( - "marshal", - &self - .marshal - .as_ref() - .map(|_| "Some(ValueSerializer)") - .unwrap_or("None"), - ) - .finish() - } -} - -impl UpnpVariable for StateVariable { - fn get_definition(&self) -> &StateVariable { - return self; - } -} - -impl StateVariable { - pub fn new(vartype: StateVarType, name: String) -> StateVariable { - Self { - object: UpnpObjectType { - name, - object_type: "StateVariable".to_string(), - }, - value_type: vartype.clone(), - step: None, - modifiable: true, - event_conditions: Arc::new(RwLock::new(HashMap::new())), - description: "".to_string(), - default_value: None, - value_range: None, - allowed_values: Arc::new(RwLock::new(Vec::new())), - send_events: false, - parse: None, - marshal: None, - } - } - - pub fn set_step(&mut self, step: StateValue) -> Result<(), StateValueError> { - if self.as_state_var_type() != step.as_state_var_type() { - return Err(StateValueError::TypeError("Bad step type".to_string())); - } - - self.step = Some(step); - Ok(()) - } - - pub fn set_range(&mut self, min: &StateValue, max: &StateValue) -> Result<(), StateValueError> { - if self.as_state_var_type() != min.as_state_var_type() { - return Err(StateValueError::TypeError("Bad range type".to_string())); - } - - let range = ValueRange::new(min, max)?; // ? propage l'erreur si elle existe - self.value_range = Some(range); - Ok(()) - } - - pub fn update_minimum(&mut self, min: &StateValue) -> Result<(), StateValueError> { - if !self.has_range() { - return Err(StateValueError::RangeError( - "No range specified for this variable".to_string(), - )); - } - if self - .value_range - .as_ref() - .expect("Range is not defined") - .as_state_var_type() - != min.as_state_var_type() - { - return Err(StateValueError::TypeError( - "new minimum is not the same than state variable".to_string(), - )); - } - self.value_range - .as_mut() - .expect("Range is not defined") - .set_minimum(min); - return Ok(()); - } - - pub fn update_maximum(&mut self, min: &StateValue) -> Result<(), StateValueError> { - if !self.has_range() { - return Err(StateValueError::RangeError( - "No range specified for this variable".to_string(), - )); - } - if self - .value_range - .as_ref() - .expect("Range is not defined") - .as_state_var_type() - != min.as_state_var_type() - { - return Err(StateValueError::TypeError( - "new minimum is not the same than state variable".to_string(), - )); - } - self.value_range - .as_mut() - .expect("Range is not defined") - .set_maximum(min); - return Ok(()); - } - - pub fn get_range(&self) -> Option<&ValueRange> { - return self.value_range.as_ref(); - } - - pub fn set_modifiable(&mut self) { - self.modifiable = true; - } - - pub fn set_not_modifiable(&mut self) { - self.modifiable = false; - } - - pub fn add_event_condition(&self, name: String, func: StateConditionFunc) { - // on lock en écriture - let mut guard = self.event_conditions.write().unwrap(); - guard.insert(name, func); - // le lock est automatiquement relâché ici (RAII) - } - - pub fn remove_event_condition(&self, name: &str) { - let mut guard = self.event_conditions.write().unwrap(); - guard.remove(name); - } - - pub fn clear_event_conditions(&mut self) { - let mut guard = self.event_conditions.write().unwrap(); - guard.clear() - } - - pub fn set_description(&mut self, description: String) { - self.description = description; - } - - pub fn set_default(&mut self, value: &StateValue) -> Result<(), StateValueError> { - if self.as_state_var_type() != value.as_state_var_type() { - return Err(StateValueError::TypeError( - "value does not have the right type".to_string(), - )); - } - self.default_value = Some(value.clone()); - return Ok(()); - } - - pub fn unset_default(&mut self) { - self.default_value = None; - } - - pub fn extend_allowed_values(&mut self, values: &[StateValue]) -> Result<(), StateValueError> { - let mut av = self - .allowed_values - .write().unwrap(); - - for v in values { - if self.as_state_var_type() == v.as_state_var_type() { - av.push(v.clone()); - } else { - return Err(StateValueError::TypeError( - "new allowed value does not have the right type".to_string(), - )); - } - } - - Ok(()) - } - - pub fn push_allowed_value(&mut self, value: &StateValue) -> Result<(), StateValueError> { - let mut av = self - .allowed_values - .write().unwrap(); - - if self.as_state_var_type() == value.as_state_var_type() { - av.push(value.clone()); - } else { - return Err(StateValueError::TypeError( - "new allowed value does not have the right type".to_string(), - )); - } - - return Ok(()); - } - - pub fn set_send_notification(&mut self) { - self.send_events = true; - } - - pub fn unset_send_notification(&mut self) { - self.send_events = false; - } - - pub fn set_value_parser(&mut self, parser: StringValueParser) -> Result<(), StateValueError> { - if self.as_state_var_type() == StateVarType::String { - self.parse = Some(parser); - return Ok(()); - } - return Err(StateValueError::TypeError( - "Only String variables can have a parser".to_string(), - )); - } - - pub fn unset_value_parser(&mut self) { - self.parse = None; - } - - pub fn set_value_marshaler( - &mut self, - marshaler: ValueSerializer, - ) -> Result<(), StateValueError> { - if self.as_state_var_type() == StateVarType::String { - self.marshal = Some(marshaler); - return Ok(()); - } - return Err(StateValueError::TypeError( - "Only String variables can have a marshaler".to_string(), - )); - } - - pub fn unset_value_marshaler(&mut self) { - self.marshal = None; - } -} -``` - -## fichier: `pmoupnp/src/lib.rs` - -```rust -mod object_trait; -mod object_set; - -pub mod actions; -pub mod mediarenderer; -pub mod server; -pub mod services; -pub mod state_variables; -pub mod value_ranges; -pub mod variable_types; - - -use std::{collections::HashMap, sync::Arc}; - -use std::sync::RwLock; - -pub use crate::object_trait::*; - -#[derive(Debug, Clone)] -pub struct UpnpObjectType { - name: String, - object_type: String, -} - -#[derive(Debug)] -pub struct UpnpObjectSet { - objects: RwLock>>, -} - -#[derive(Debug)] -pub enum UpnpObjectSetError { - AlreadyExists(String), -} - -``` - -## fichier: `pmoupnp/src/value_ranges/methods.rs` - -```rust -use std::cmp::Ordering; - -use crate::{ - value_ranges::ValueRange, - variable_types::{StateValue, StateValueError, StateVarType, UpnpVarType}, -}; - -impl UpnpVarType for ValueRange { - fn as_state_var_type(&self) -> StateVarType { - self.min.as_state_var_type() // utilise ton From<&StateValue> existant - } -} - -impl ValueRange { - pub fn new(min: &StateValue, max: &StateValue) -> Result { - if min.as_state_var_type() != max.as_state_var_type() { - return Err(StateValueError::TypeError( - "min and max do not belong the same time".to_string(), - )); - } - - // Vérifier que min <= max - if let Some(cmp) = min.partial_cmp(max) { - if cmp == Ordering::Greater { - return Err(StateValueError::RangeError( - "Minimum cannot be greater than maximum".to_string(), - )); - } - } - - Ok(Self { - min: min.clone(), - max: max.clone(), - }) - } - - pub fn get_minimum(self: &ValueRange) -> StateValue { - return self.min.clone(); - } - - pub fn set_minimum(&mut self, value: &StateValue) { - self.min = value.clone() - } - - pub fn get_maximum(self: &ValueRange) -> StateValue { - return self.max.clone(); - } - - pub fn set_maximum(&mut self, value: &StateValue) { - self.max = value.clone() - } - - pub fn is_in_range(&self, value: &StateValue) -> bool { - if self.as_state_var_type() == value.as_state_var_type() - && let Some(cmp) = self.min.partial_cmp(value) - { - if cmp == Ordering::Greater { - return false; - } - if let Some(cmp2) = self.max.partial_cmp(value) { - if cmp2 == Ordering::Less { - return false; - } - return true; - } - } - return false; - } -} -``` - -## fichier: `pmoupnp/src/value_ranges/mod.rs` - -```rust -mod methods; - -use crate::variable_types::StateValue; - -#[derive(Debug, Clone)] -pub struct ValueRange { - min: StateValue, - max: StateValue, -} -``` - -## fichier: `pmoupnp/src/mediarenderer/mod.rs` - -```rust -pub mod avtransport; -``` - -## fichier: `pmoupnp/src/mediarenderer/avtransport/mod.rs` - -```rust -pub mod variables; -pub mod actions; - -``` - -## fichier: `pmoupnp/src/mediarenderer/avtransport/variables/a_arg_type_instanceid.rs` - -```rust -use std::sync::Arc; - -use crate::state_variables::StateVariable; -use crate::variable_types::StateVarType; -use once_cell::sync::Lazy; - -pub static A_ARG_TYPE_INSTANCE_ID: Lazy> = Lazy::new(|| -> Arc { - Arc::new(StateVariable::new(StateVarType::UI4, "A_ARG_TYPE_InstanceID".to_string())) -}); -``` - -## fichier: `pmoupnp/src/mediarenderer/avtransport/variables/a_arg_type_playspeed.rs` - -```rust -use std::sync::Arc; - -use crate::state_variables::StateVariable; -use crate::variable_types::StateVarType; -use once_cell::sync::Lazy; - -pub static A_ARG_TYPE_PLAY_SPEED: Lazy> = Lazy::new(|| -> Arc { - Arc::new(StateVariable::new(StateVarType::String, "A_ARG_TYPE_PlaySpeed".to_string())) -}); -``` - -## fichier: `pmoupnp/src/mediarenderer/avtransport/variables/transportstatus.rs` - -```rust -use std::sync::Arc; - -use crate::state_variables::StateVariable; -use crate::variable_types::{StateValue, StateVarType}; -use once_cell::sync::Lazy; - -pub static TRANSPORTSTATUS: Lazy> = Lazy::new(|| -> Arc { - let mut sv = StateVariable::new(StateVarType::String, "TransportStatus".to_string()); - - sv.push_allowed_value(&StateValue::String("OK".to_string())) - .expect("Cannot add allowed value"); - sv.extend_allowed_values(&[ - StateValue::String("OK".to_string()), - StateValue::String("ERROR_OCCURRED".to_string()), - ]) - .expect("Cannt set default value"); - - Arc::new(sv) -}); -``` - -## fichier: `pmoupnp/src/mediarenderer/avtransport/variables/transportplayspeed.rs` - -```rust -use std::sync::Arc; - -use crate::state_variables::StateVariable; -use crate::variable_types::{StateValue, StateVarType}; -use once_cell::sync::Lazy; - -pub static TRANSPORTPLAYSPEED: Lazy> = Lazy::new(|| -> Arc { - let mut sv = StateVariable::new(StateVarType::String, "TransportPlaySpeed".to_string()); - - sv.push_allowed_value(&StateValue::String("1".to_string())).expect("Cannot add allowed value"); - sv.set_default(&StateValue::String("1".to_string())).expect("Cannt set default value"); - - Arc::new(sv) -}); - -``` - -## fichier: `pmoupnp/src/mediarenderer/avtransport/variables/avtransporturi.rs` - -```rust -use std::sync::Arc; - -use crate::state_variables::StateVariable; -use crate::variable_types::StateVarType; -use once_cell::sync::Lazy; - -pub static AVTRANSPORTURI: Lazy> = Lazy::new(|| -> Arc { - Arc::new(StateVariable::new(StateVarType::String, "AVTransportURI".to_string())) -}); -``` - -## fichier: `pmoupnp/src/mediarenderer/avtransport/variables/mod.rs` - -```rust -mod a_arg_type_instanceid; -mod a_arg_type_playspeed; -mod avtransporturi; -mod avtransporturimetadata; -mod currenttrackduration; -mod seekmode; -mod transportplayspeed; -mod transportstate; -mod transportstatus; - -pub use a_arg_type_instanceid::A_ARG_TYPE_INSTANCE_ID; -pub use a_arg_type_playspeed::A_ARG_TYPE_PLAY_SPEED; -pub use avtransporturi::AVTRANSPORTURI; -pub use avtransporturimetadata::AVTRANSPORTURIMETADATA; -pub use currenttrackduration::CURRENTTRACKDURATION; -pub use seekmode::SEEKMODE; -pub use transportplayspeed::TRANSPORTPLAYSPEED; -pub use transportstate::TRANSPORTSTATE; -pub use transportstatus::TRANSPORTSTATUS; - - - -``` - -## fichier: `pmoupnp/src/mediarenderer/avtransport/variables/seekmode.rs` - -```rust -use std::sync::Arc; - -use crate::state_variables::StateVariable; -use crate::variable_types::StateVarType; -use once_cell::sync::Lazy; - -pub static SEEKMODE: Lazy> = Lazy::new(|| -> Arc { - Arc::new(StateVariable::new(StateVarType::String, "SeekMode".to_string())) -}); - -``` - -## fichier: `pmoupnp/src/mediarenderer/avtransport/variables/currenttrackduration.rs` - -```rust -use std::sync::Arc; - -use crate::state_variables::StateVariable; -use crate::variable_types::StateVarType; -use once_cell::sync::Lazy; - -pub static CURRENTTRACKDURATION: Lazy> = Lazy::new(|| -> Arc { - Arc::new(StateVariable::new(StateVarType::String, "CurrentTrackDuration".to_string())) -}); - -``` - -## fichier: `pmoupnp/src/mediarenderer/avtransport/variables/avtransporturimetadata.rs` - -```rust -use std::sync::Arc; - -use crate::state_variables::{StateVariable, StateVariableError}; -use crate::variable_types::StateVarType; -use bevy_reflect::Reflect; -use once_cell::sync::Lazy; -use pmodidl::{DIDLLite, MediaMetadataParser}; - -// func _AVTransportURIMetaDataParser(value string) (interface{}, error) { -// log.Debug("[avtransport] Parsing AVTransport)") -// didl, err := pmodidl.Parse(value) -// if err != nil { -// return value, err -// } - -// return didl, nil -// } - -fn avtransporturimetadataparser(value: &str) -> Result, StateVariableError> { - // Parse DIDL-Lite - let didl = DIDLLite::parse(value) - .map_err(|e| StateVariableError::ParseError(format!("Failed to parse DIDL-Lite: {}", e)))?; - - // Retourne le résultat sous forme de Box - Ok(Box::new(didl) as Box) -} - -pub static AVTRANSPORTURIMETADATA: Lazy> = Lazy::new(|| -> Arc { - let mut sv = StateVariable::new(StateVarType::String, "AVTransportURIMetaData".to_string()); - - sv.set_value_parser(Arc::new(avtransporturimetadataparser)).expect("Failed to set parser"); - Arc::new(sv) -}); -``` - -## fichier: `pmoupnp/src/mediarenderer/avtransport/variables/transportstate.rs` - -```rust -use std::sync::Arc; - -use crate::state_variables::StateVariable; -use crate::variable_types::{StateValue, StateVarType}; -use once_cell::sync::Lazy; - -pub static TRANSPORTSTATE: Lazy> = Lazy::new(|| -> Arc { - let mut sv = StateVariable::new(StateVarType::String, "TransportState".to_string()); - - sv.push_allowed_value(&StateValue::String("NO_MEDIA_PRESENT".to_string())).expect("Cannot add allowed value"); - sv.extend_allowed_values(&[ - StateValue::String("STOPPED".to_string()), - StateValue::String("PLAYING".to_string()), - StateValue::String("RECORDING".to_string()), - StateValue::String("TRANSITIONING".to_string()), - StateValue::String("PAUSED_PLAYBACK".to_string()), - StateValue::String("PAUSED_RECORDING".to_string()), - StateValue::String("NO_MEDIA_PRESENT".to_string()), - ]).expect("Cannt set default value"); - - Arc::new(sv) -}); - -``` - -## fichier: `pmoupnp/src/mediarenderer/avtransport/actions/play.rs` - -```rust -use crate::mediarenderer::avtransport::variables::{A_ARG_TYPE_INSTANCE_ID, TRANSPORTPLAYSPEED}; -use crate::define_action; - -define_action! { - pub static PLAY = "Play" { - in "InstanceID" => A_ARG_TYPE_INSTANCE_ID, - in "Speed" => TRANSPORTPLAYSPEED, - } -} -``` - -## fichier: `pmoupnp/src/mediarenderer/avtransport/actions/stop.rs` - -```rust -use crate::mediarenderer::avtransport::variables::A_ARG_TYPE_INSTANCE_ID; -use crate::define_action; - -define_action! { - pub static STOP = "Stop" { - in "InstanceID" => A_ARG_TYPE_INSTANCE_ID, - } -} -``` - -## fichier: `pmoupnp/src/mediarenderer/avtransport/actions/setavtransporturi.rs` - -```rust -use crate::mediarenderer::avtransport::variables::{A_ARG_TYPE_INSTANCE_ID, AVTRANSPORTURI, AVTRANSPORTURIMETADATA}; -use crate::define_action; - -define_action! { - pub static SETAVTRANSPORTURI = "SetAVTransportURI" { - in "InstanceID" => A_ARG_TYPE_INSTANCE_ID, - in "CurrentURI" => AVTRANSPORTURI, - in "CurrentURIMetaData" => AVTRANSPORTURIMETADATA, - } -} -``` - -## fichier: `pmoupnp/src/mediarenderer/avtransport/actions/mod.rs` - -```rust -mod play; -mod stop; -mod setavtransporturi; - -pub use play::PLAY; -pub use stop::STOP; -pub use setavtransporturi::SETAVTRANSPORTURI; - -``` - -## fichier: `pmoupnp/src/server/mod.rs` - -```rust -//! # Module Server - API de haut niveau pour Axum -//! -//! Ce module fournit une abstraction simple et ergonomique pour créer des serveurs HTTP -//! avec Axum, en cachant la complexité de la configuration et du routage. -//! -//! ## Fonctionnalités -//! -//! - 🚀 **Routes JSON simples** : Ajoutez des endpoints API avec `add_route()` -//! - 📁 **Fichiers statiques** : Servez des assets avec `add_dir()` -//! - ⚛️ **Applications SPA** : Support pour Vue.js/React avec `add_spa()` -//! - 🔀 **Redirections** : Redirigez des routes avec `add_redirect()` -//! - 🎯 **Handlers personnalisés** : Support SSE, WebSocket, etc. avec `add_handler_with_state()` -//! - 📚 **Documentation API** : OpenAPI/Swagger automatique avec `add_openapi()` -//! - ⚡ **Gestion gracieuse** : Arrêt propre sur Ctrl+C - -pub mod logs; - -use axum::handler::Handler; -use axum::response::Redirect; -use axum::routing::get; -use axum::{Json, Router}; -use axum_embed::ServeEmbed; -use pmoconfig::get_config; -use rust_embed::RustEmbed; -use serde::Serialize; -use std::{net::SocketAddr, sync::Arc}; -use tokio::{signal, sync::RwLock, task::JoinHandle}; -use tracing::{info, warn, debug, error}; -use utoipa::OpenApi; -use utoipa_swagger_ui::SwaggerUi; - -/// Info serveur sérialisable -#[derive(Clone, Serialize, utoipa::ToSchema)] -pub struct ServerInfo { - /// Nom du serveur - pub name: String, - /// URL de base - pub base_url: String, - /// Port HTTP - pub http_port: u16, -} - -/// Serveur principal -pub struct Server { - name: String, - base_url: String, - http_port: u16, - router: Arc>, - api_router: Arc>>, - join_handle: Option>, -} - -#[derive(RustEmbed, Clone)] -#[folder = "webapp/dist"] -pub struct Webapp; - -impl Server { - /// Crée une nouvelle instance de serveur - /// - /// # Arguments - /// - /// * `name` - Nom du serveur (pour les logs) - /// * `base_url` - URL de base (ex: "http://localhost:3000") - /// * `http_port` - Port HTTP à écouter - /// - /// # Exemple - /// - /// ```rust - /// # use pmoupnp::server::Server; - /// let server = Server::new("MyAPI", "http://localhost:3000", 3000); - /// ``` - pub fn new(name: impl Into, base_url: impl Into, http_port: u16) -> Self { - Self { - name: name.into(), - base_url: base_url.into(), - http_port, - router: Arc::new(RwLock::new(Router::new())), - api_router: Arc::new(RwLock::new(None)), - join_handle: None, - } - } - - pub fn new_configured() -> Self { - let config = get_config(); - let url = config.get_base_url(); - let port = config.get_http_port(); - - return Self::new("PMO-Music-Server", url, port); - } - - /// Ajoute une route JSON dynamique - /// - /// Crée un endpoint qui retourne du JSON. La closure fournie sera appelée - /// à chaque requête GET sur le chemin spécifié. - /// - /// # Arguments - /// - /// * `path` - Chemin de la route (ex: "/api/hello") - /// * `f` - Closure async retournant une valeur sérialisable - /// - /// # Exemple - /// - /// ```rust,no_run - /// # use pmoupnp::server::Server; - /// # #[tokio::main] - /// # async fn main() { - /// # let mut server = Server::new("Test", "http://localhost:3000", 3000); - /// server.add_route("/api/status", || async { - /// serde_json::json!({ - /// "status": "online", - /// "version": "1.0.0" - /// }) - /// }).await; - /// # } - /// ``` - pub async fn add_route(&mut self, path: &str, f: F) - where - F: Fn() -> Fut + Send + Sync + 'static, - Fut: std::future::Future + Send + 'static, - T: Serialize + Send + 'static, - { - let f = Arc::new(f); - - let handler = { - let f = f.clone(); - move || { - let f = f.clone(); - async move { Json(f().await) } - } - }; - - let route = Router::new().route("/", get(handler)); - - let mut r = self.router.write().await; - *r = std::mem::take(&mut *r).nest(path, route); - } - - /// Ajoute un répertoire de fichiers statiques - /// - /// Sert des fichiers embarqués via `RustEmbed`. Les fichiers sont compilés - /// dans le binaire à la compilation. - /// - /// # Arguments - /// - /// * `path` - Chemin où monter les fichiers statiques - /// - /// # Type Parameter - /// - /// * `E` - Type RustEmbed définissant le répertoire à servir - /// - /// # Exemple - /// - /// ```ignore - /// use pmoupnp::server::Server; - /// use rust_embed::RustEmbed; - /// - /// #[derive(RustEmbed, Clone)] - /// #[folder = "static/"] - /// struct Assets; - /// - /// # #[tokio::main] - /// # async fn main() { - /// let mut server = Server::new("Test", "http://localhost:3000", 3000); - /// server.add_dir::("/assets").await; - /// // Les fichiers de static/ sont accessibles via /assets/* - /// # } - /// ``` - pub async fn add_dir(&mut self, path: &str) - where - E: RustEmbed + Clone + Send + Sync + 'static, - { - let serve = ServeEmbed::::new(); - - let mut r = self.router.write().await; - - if path == "/" { - *r = std::mem::take(&mut *r).fallback_service(serve); - } else { - let route = Router::new().fallback_service(serve); - *r = std::mem::take(&mut *r).nest(path, route); - } - } - - /// Ajoute une Single Page Application (SPA) - /// - /// Sert une application JavaScript moderne (Vue.js, React, etc.) avec support - /// du routage côté client. Tous les chemins non trouvés renvoient `index.html` - /// pour permettre au routeur JavaScript de gérer la navigation. - /// - /// # Arguments - /// - /// * `path` - Chemin où monter l'application (souvent "/" ou "/app") - /// - /// # Type Parameter - /// - /// * `E` - Type RustEmbed contenant les fichiers de la SPA - /// - /// # Exemple avec Vue.js - /// - /// ```rust,no_run - /// # use pmoupnp::server::Server; - /// # use rust_embed::RustEmbed; - /// #[derive(RustEmbed, Clone)] - /// #[folder = "webapp/dist"] // Build output de Vue.js - /// struct WebApp; - /// - /// # #[tokio::main] - /// # async fn main() { - /// # let mut server = Server::new("Test", "http://localhost:3000", 3000); - /// server.add_spa::("/").await; - /// // L'app Vue.js gère toutes les routes comme /about, /users, etc. - /// # } - /// ``` - /// - /// # Note - /// - /// Pour Vue.js/Vite, configure le `base` dans `vite.config.js` si tu montes - /// sur un sous-chemin : - /// ```javascript - /// export default { - /// base: '/app/' - /// } - /// ``` - pub async fn add_spa(&mut self, path: &str) - where - E: RustEmbed + Clone + Send + Sync + 'static, - { - let serve = ServeEmbed::::with_parameters( - Some("index.html".to_string()), - axum_embed::FallbackBehavior::Ok, - Some("index.html".to_string()), - ); - - let mut r = self.router.write().await; - - if path == "/" { - *r = std::mem::take(&mut *r).fallback_service(serve); - } else { - let route = Router::new().fallback_service(serve); - *r = std::mem::take(&mut *r).nest(path, route); - } - } - - /// Ajoute un handler Axum personnalisé - /// - /// Pour des cas d'usage avancés nécessitant un contrôle complet sur le handler. - /// - /// # Arguments - /// - /// * `path` - Chemin de la route - /// * `handler` - Handler Axum - /// - /// # Exemple - /// - /// ```rust,no_run - /// # use pmoupnp::server::Server; - /// # use axum::response::Html; - /// # #[tokio::main] - /// # async fn main() { - /// # let mut server = Server::new("Test", "http://localhost:3000", 3000); - /// async fn custom_handler() -> Html<&'static str> { - /// Html("

Custom Response

") - /// } - /// - /// server.add_handler("/custom", custom_handler).await; - /// # } - /// ``` - pub async fn add_handler(&mut self, path: &str, handler: H) - where - H: Handler, - T: 'static, - { - let route = Router::new().route("/", get(handler)); - - let mut r = self.router.write().await; - *r = std::mem::take(&mut *r).nest(path, route); - } - - /// Ajoute un handler avec state (pour SSE, extracteurs, etc.) - /// - /// Permet d'utiliser des extracteurs Axum comme `State`, `Query`, etc. - /// Idéal pour Server-Sent Events (SSE), WebSockets ou tout handler nécessitant un état partagé. - /// - /// # Arguments - /// - /// * `path` - Chemin de la route - /// * `handler` - Handler Axum avec extracteurs - /// * `state` - État partagé (doit être Clone + Send + Sync) - /// - /// # Exemple avec SSE - /// - /// ```ignore - /// use pmoupnp::server::Server; - /// use axum::extract::State; - /// use axum::response::sse::{Event, Sse, KeepAlive}; - /// use tokio::sync::broadcast; - /// - /// #[derive(Clone)] - /// struct LogState { - /// tx: broadcast::Sender - /// } - /// - /// impl LogState { - /// fn subscribe(&self) -> broadcast::Receiver { - /// self.tx.subscribe() - /// } - /// } - /// - /// async fn log_sse(State(state): State) -> Sse>> { - /// let mut rx = state.subscribe(); - /// let stream = async_stream::stream! { - /// while let Ok(msg) = rx.recv().await { - /// yield Ok(Event::default().data(msg)); - /// } - /// }; - /// Sse::new(stream).keep_alive(KeepAlive::default()) - /// } - /// - /// let log_state = LogState { tx: broadcast::channel(100).0 }; - /// server.add_handler_with_state("/logs", log_sse, log_state).await; - /// ``` - pub async fn add_handler_with_state(&mut self, path: &str, handler: H, state: S) - where - H: Handler, - T: 'static, - S: Clone + Send + Sync + 'static, - { - let route = Router::new() - .route("/", get(handler)) - .with_state(state); - - let mut r = self.router.write().await; - *r = std::mem::take(&mut *r).nest(path, route); - } - - /// Ajoute un handler POST avec state - /// - /// Similaire à `add_handler_with_state` mais pour les requêtes POST. - /// - /// # Arguments - /// - /// * `path` - Chemin de la route - /// * `handler` - Handler Axum pour POST - /// * `state` - État partagé - pub async fn add_post_handler_with_state(&mut self, path: &str, handler: H, state: S) - where - H: Handler, - T: 'static, - S: Clone + Send + Sync + 'static, - { - let route = Router::new() - .route("/", axum::routing::post(handler)) - .with_state(state); - - let mut r = self.router.write().await; - *r = std::mem::take(&mut *r).nest(path, route); - } - - /// Ajoute une redirection HTTP - /// - /// Redirige automatiquement les requêtes d'un chemin vers un autre avec un code 308 (permanent). - /// - /// # Arguments - /// - /// * `from` - Chemin source (peut être "/" pour la racine) - /// * `to` - Chemin de destination - /// - /// # Exemple - /// - /// ```rust,no_run - /// # use pmoupnp::server::Server; - /// # #[tokio::main] - /// # async fn main() { - /// # let mut server = Server::new("Test", "http://localhost:3000", 3000); - /// // Rediriger la racine vers /app - /// server.add_redirect("/", "/app").await; - /// # } - /// ``` - pub async fn add_redirect(&mut self, from: &str, to: &str) { - let to = to.to_string(); - let handler = move || { - let to = to.clone(); - async move { Redirect::permanent(&to) } - }; - - let mut r = self.router.write().await; - - if from == "/" { - // Pour la racine, utiliser merge au lieu de nest - let route = Router::new().route("/", get(handler)); - *r = std::mem::take(&mut *r).merge(route); - } else { - let route = Router::new().route("/", get(handler)); - *r = std::mem::take(&mut *r).nest(from, route); - } - } - - /// Ajoute une API documentée avec OpenAPI - /// - /// Monte un routeur d'API sous `/api` et active Swagger UI sur `/swagger-ui` - /// - /// # Arguments - /// - /// * `api_router` - Router Axum contenant les routes API - /// * `openapi` - Spécification OpenAPI générée par utoipa - /// - /// # Exemple - /// - /// ```ignore - /// use utoipa::OpenApi; - /// use axum::{Router, Json, routing::get}; - /// use serde::{Serialize, Deserialize}; - /// - /// #[derive(Serialize, Deserialize, utoipa::ToSchema)] - /// struct User { - /// id: u64, - /// name: String, - /// } - /// - /// #[derive(utoipa::OpenApi)] - /// #[openapi( - /// paths(get_users), - /// components(schemas(User)) - /// )] - /// struct ApiDoc; - /// - /// #[utoipa::path( - /// get, - /// path = "/users", - /// responses((status = 200, description = "List users")) - /// )] - /// async fn get_users() -> Json> { - /// Json(vec![]) - /// } - /// - /// let api_router = Router::new() - /// .route("/users", get(get_users)); - /// - /// server.add_openapi(api_router, ApiDoc::openapi()).await; - /// ``` - pub async fn add_openapi(&mut self, api_router: Router, openapi: utoipa::openapi::OpenApi) { - // Stocker le routeur API - let mut api_r = self.api_router.write().await; - *api_r = Some(api_router); - - // Ajouter Swagger UI - let swagger = SwaggerUi::new("/swagger-ui") - .url("/api-docs/openapi.json", openapi); - - let mut r = self.router.write().await; - *r = std::mem::take(&mut *r).merge(swagger); - } - - /// Démarre le serveur HTTP - /// - /// Lance le serveur sur le port configuré et met en place la gestion - /// de Ctrl+C pour un arrêt gracieux. - /// - /// # Exemple - /// - /// ```rust,no_run - /// # use pmoupnp::server::Server; - /// # #[tokio::main] - /// # async fn main() { - /// # let mut server = Server::new("Test", "http://localhost:3000", 3000); - /// server.start().await; - /// server.wait().await; // Attend Ctrl+C - /// # } - /// ``` - pub async fn start(&mut self) { - let addr = SocketAddr::from(([0, 0, 0, 0], self.http_port)); - info!("Server {} running at [http://{}:{}](http://{}:{})", self.name, self.base_url, self.http_port, self.base_url, self.http_port); - - // Merger le routeur API si présent - let api_router = self.api_router.read().await; - if let Some(api_r) = api_router.as_ref() { - let mut r = self.router.write().await; - *r = std::mem::take(&mut *r).nest("/api", api_r.clone()); - } - drop(api_router); - - let router = self.router.clone(); - - let server_task = tokio::spawn(async move { - let r = router.read().await.clone(); - let listener = tokio::net::TcpListener::bind(addr).await.unwrap(); - axum::serve(listener, r.into_make_service()).await.unwrap(); - }); - - let shutdown_task = tokio::spawn(async move { - signal::ctrl_c().await.expect("failed to listen for ctrl_c"); - info!("Ctrl+C reçu, arrêt gracieux"); - }); - - self.join_handle = Some(tokio::spawn(async move { - tokio::select! { - _ = server_task => {}, - _ = shutdown_task => {}, - } - })); - } - - /// Attend la fin du serveur - pub async fn wait(&mut self) { - if let Some(h) = self.join_handle.take() { - let _ = h.await; - } - } - - /// Récupère les infos du serveur - pub fn info(&self) -> ServerInfo { - ServerInfo { - name: self.name.clone(), - base_url: self.base_url.clone(), - http_port: self.http_port, - } - } -} - -/// Builder pattern -pub struct ServerBuilder { - name: String, - base_url: String, - http_port: u16, -} - -impl ServerBuilder { - /// Crée un nouveau builder - /// - /// # Arguments - /// - /// * `name` - Nom du serveur - /// * `base_url` - URL de base (ex: "http://localhost:3000") - /// * `http_port` - Port HTTP - pub fn new(name: impl Into, base_url: impl Into, http_port: u16) -> Self { - Self { - name: name.into(), - base_url: base_url.into(), - http_port, - } - } - - pub fn new_configured() -> Self { - let config = get_config(); - Self { - name: "PMO-Music-Server".to_string(), - base_url: config.get_base_url(), - http_port: config.get_http_port() - } - } - - /// Construit le serveur - /// - /// Consomme le builder et retourne une instance de `Server` prête à l'emploi. - /// - /// # Exemple - /// - /// ```rust - /// # use pmoupnp::server::ServerBuilder; - /// let mut server = ServerBuilder::new("MyAPI", "http://localhost:3000", 3000) - /// .build(); - /// ``` - pub fn build(self) -> Server { - Server::new(self.name, self.base_url, self.http_port) - } -}``` - -## fichier: `pmoupnp/src/server/logs/mod.rs` - -```rust -// logs.rs -mod sselayer; - -pub use sselayer::SseLayer; - -use std::{ - collections::VecDeque, - sync::{Arc, RwLock}, - time::SystemTime, -}; - -use axum::{ - Json, - extract::{Query, State}, - response::{ - IntoResponse, - sse::{Event, KeepAlive, Sse}, - }, -}; -use serde::{Deserialize, Serialize}; -use tokio::sync::broadcast; - -/// Représente une entrée de log -#[derive(Debug, Clone, Serialize)] -pub struct LogEntry { - pub timestamp: SystemTime, - pub level: String, - pub target: String, - pub message: String, -} - -/// Buffer circulaire partagé -#[derive(Clone)] -pub struct LogState { - buffer: Arc>>, - tx: broadcast::Sender, -} - -impl LogState { - pub fn new(capacity: usize) -> Self { - Self { - buffer: Arc::new(RwLock::new(VecDeque::with_capacity(capacity))), - tx: broadcast::channel(1000).0, - } - } - - fn push(&self, entry: LogEntry) { - let mut buf = self.buffer.write().unwrap(); - if buf.len() == buf.capacity() { - buf.pop_front(); - } - buf.push_back(entry.clone()); - let _ = self.tx.send(entry); - } - - pub fn subscribe(&self) -> broadcast::Receiver { - self.tx.subscribe() - } - - pub fn dump(&self) -> Vec { - self.buffer.read().unwrap().iter().cloned().collect() - } -} - -/// Query params pour /log-sse -#[derive(Debug, Deserialize)] -pub struct LogQuery { - #[serde(default)] - pub error: Option, - #[serde(default)] - pub warn: Option, - #[serde(default)] - pub info: Option, - #[serde(default)] - pub debug: Option, - #[serde(default)] - pub trace: Option, - #[serde(default)] - pub search: Option, -} - -/// Handler SSE -// Dans logs.rs -pub async fn log_sse( - State(state): State, - Query(params): Query, -) -> impl IntoResponse { - let mut rx = state.subscribe(); - - // Récupérer l'historique du buffer - let history = state.dump(); - - let stream = async_stream::stream! { - // 1. Envoyer d'abord tous les logs historiques - for entry in history { - if !filter_entry(&entry, ¶ms) { - continue; - } - let json = serde_json::to_string(&entry).unwrap(); - yield Ok::<_, axum::Error>(Event::default().data(json)); - } - - // 2. Puis streamer les nouveaux logs en temps réel - while let Ok(entry) = rx.recv().await { - if !filter_entry(&entry, ¶ms) { - continue; - } - let json = serde_json::to_string(&entry).unwrap(); - yield Ok::<_, axum::Error>(Event::default().data(json)); - } - }; - - Sse::new(stream).keep_alive(KeepAlive::default()) -} - -/// Handler REST (dump JSON du buffer) -pub async fn log_dump(State(state): State) -> impl IntoResponse { - Json(state.dump()) -} - -/// Fonction de filtrage -fn filter_entry(entry: &LogEntry, q: &LogQuery) -> bool { - // Filtrage par niveau - let lvl = entry.level.to_lowercase(); - let mut allowed = false; - - if let Some(true) = q.error { - allowed |= lvl == "error"; - } - if let Some(true) = q.warn { - allowed |= lvl == "warn"; - } - if let Some(true) = q.info { - allowed |= lvl == "info"; - } - if let Some(true) = q.debug { - allowed |= lvl == "debug"; - } - if let Some(true) = q.trace { - allowed |= lvl == "trace"; - } - - // si aucun flag → tout est autorisé - if !(q.error.unwrap_or(false) - || q.warn.unwrap_or(false) - || q.info.unwrap_or(false) - || q.debug.unwrap_or(false) - || q.trace.unwrap_or(false)) - { - allowed = true; - } - - // Filtrage par mot-clé - if let Some(search) = &q.search { - allowed &= entry.message.contains(search) || entry.target.contains(search); - } - - allowed -} -``` - -## fichier: `pmoupnp/src/server/logs/sselayer.rs` - -```rust -use tracing::field::{Field, Visit}; -use tracing::{Event, Subscriber}; -use tracing_subscriber::{Layer, layer::Context}; - -use super::{LogEntry, LogState}; -use std::time::SystemTime; - -struct LogVisitor { - message: String, -} - -impl LogVisitor { - fn new() -> Self { - Self { - message: String::new(), - } - } -} - -impl Visit for LogVisitor { - fn record_debug(&mut self, field: &Field, value: &dyn std::fmt::Debug) { - // capture le champ "message" ou concatène les autres - if field.name() == "message" { - self.message = format!("{:?}", value); - } else { - if !self.message.is_empty() { - self.message.push(' '); - } - self.message - .push_str(&format!("{}={:?}", field.name(), value)); - } - } -} - -/// Layer de tracing qui pousse les events dans le buffer -pub struct SseLayer { - state: LogState, -} - -impl SseLayer { - pub fn new(state: LogState) -> Self { - Self { state } - } -} - -impl Layer for SseLayer -where - S: Subscriber, -{ - fn on_event(&self, event: &Event<'_>, _ctx: Context<'_, S>) { - let mut visitor = LogVisitor::new(); - event.record(&mut visitor); - - let entry = LogEntry { - timestamp: SystemTime::now(), - level: event.metadata().level().to_string(), - target: event.metadata().target().to_string(), - message: visitor.message, - }; - - self.state.push(entry); - } -} -``` - -## fichier: `pmoupnp/src/actions/action_instance_set.rs` - -```rust -use crate::{ - UpnpObject, - actions::{ActionInstanceSet}, -}; - -use xmltree::{Element,XMLNode}; - -impl UpnpObject for ActionInstanceSet { - // Méthode pour convertir en XML (à implémenter avec une librairie XML) - fn to_xml_element(&self) -> Element { - let mut elem = Element::new("actionList"); - - for action in self.all() { - let action_elem = action.to_xml_element(); // retourne un complet - elem.children.push(XMLNode::Element(action_elem)); - } - - elem - } -} - -``` - -## fichier: `pmoupnp/src/actions/action_instance.rs` - -```rust -use std::sync::Arc; - -use xmltree::{Element, XMLNode}; - -use crate::actions::Action; -use crate::actions::Argument; -use crate::actions::ArgumentSet; -use crate::actions::ArgInstanceSet; -use crate::actions::ActionInstance; -use crate::UpnpInstance; -use crate::UpnpObject; -use crate::UpnpTyped; -use crate::UpnpTypedInstance; -use crate::UpnpObjectType; - -impl UpnpObject for ActionInstance { - fn to_xml_element(&self) -> Element { - let mut elem = Element::new("action"); - - // - let mut name_elem = Element::new("name"); - name_elem.children.push(XMLNode::Text(self.get_name().clone())); - elem.children.push(XMLNode::Element(name_elem)); - - // Utiliser le set d'instances d'arguments - let args_container = self.arguments.to_xml_element(); - elem.children.push(XMLNode::Element(args_container)); - - elem - } -} - -impl UpnpTyped for ActionInstance { - fn as_upnp_object_type(&self) -> &UpnpObjectType { - return &self.object; - } -} - -impl UpnpInstance for ActionInstance { - - type Model = Action; - - fn new(action: &Action) -> Self { - // Créer les instances d'arguments - let mut arguments = ArgInstanceSet::new(); - - for arg_model in action.arguments().all() { - let arg_instance = Arc::new(crate::actions::ArgumentInstance::new(&*arg_model)); - if let Err(e) = arguments.insert(arg_instance) { - tracing::error!("Failed to insert argument instance: {:?}", e); - } - } - - Self { - object: UpnpObjectType { - name: action.get_name().clone(), - object_type: "ActionInstance".to_string(), - }, - model: action.clone(), - arguments, // ⬅️ Set d'instances, pas le modèle ! - } - } - -} - - -impl UpnpTypedInstance for ActionInstance { - - fn get_model(&self) -> &Self::Model { - &self.model - } -} - -impl ActionInstance { - /// Retourne une instance d'argument par son nom. - /// - /// # Arguments - /// - /// * `name` - Nom de l'argument à rechercher - /// - /// # Returns - /// - /// `Some(Arc)` si trouvé, `None` sinon. - pub fn argument(&self, name: &str) -> Option> { - self.arguments.get_by_name(name) - } - - /// Retourne le set d'instances d'arguments. - /// - /// # Returns - /// - /// Référence vers le `ArgInstanceSet` contenant toutes les instances. - /// - /// # Examples - /// - /// ```ignore - /// for arg_instance in action_instance.arguments_set().all() { - /// println!("Argument: {}", arg_instance.get_name()); - /// if let Some(var) = arg_instance.get_variable_instance() { - /// println!(" Variable: {} = {}", var.get_name(), var.value()); - /// } - /// } - /// ``` - pub fn arguments_set(&self) -> &ArgInstanceSet { - &self.arguments // ⬅️ Retourne les INSTANCES, pas les modèles ! - } -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::actions::Action; - use crate::UpnpInstance; - - #[test] - fn test_action_instance_creation() { - let action = Action::new("Play".to_string()); - let instance = ActionInstance::new(&action); - - assert_eq!(instance.get_name(), "Play"); - } - - #[test] - fn test_action_instance_has_argument_instances() { - let action = Action::new("Play".to_string()); - let instance = ActionInstance::new(&action); - - // Vérifier que arguments_set() retourne bien des instances - assert!(instance.arguments_set().all().iter().all(|arg| { - // Chaque argument doit être une ArgumentInstance - arg.get_model(); // Cette méthode existe seulement sur les instances - true - })); - } -} - -``` - -## fichier: `pmoupnp/src/actions/arg_set_methods.rs` - -```rust -use crate::actions::ArgInstanceSet; -use crate::UpnpModel; -use crate::{ - UpnpObject, - actions::{ArgumentSet}, -}; -use xmltree::Element; - -impl UpnpObject for ArgumentSet { - // Méthode pour convertir en XML (à implémenter avec une librairie XML) - fn to_xml_element(&self) -> Element { - let mut elem = Element::new("argumentList"); - - for arg in self.all() { - let arg_elem = arg.to_xml_element(); // toujours un contenant 1 ou 2 - - // Pour InOut, on ajoute tous les enfants du généré - for child in arg_elem.children { - elem.children.push(child); - } - } - - elem - } -} - -impl UpnpModel for ArgumentSet { - type Instance = ArgInstanceSet; -} -``` - -## fichier: `pmoupnp/src/actions/argument_methods.rs` - -```rust -use std::sync::Arc; - -use xmltree::{Element, XMLNode}; - -use crate::{ - actions::{Argument, ArgumentInstance}, state_variables::StateVariable, UpnpModel, UpnpObject, UpnpObjectType, UpnpTyped -}; - -impl UpnpTyped for Argument { - fn as_upnp_object_type(&self) -> &UpnpObjectType { - &self.object - } -} - -impl UpnpObject for Argument { - fn to_xml_element(&self) -> Element { - let mut parent = Element::new("argumentList"); - - if self.is_in() && self.is_out() { - // InOut → deux arguments - parent.children.push(XMLNode::Element(make_argument_elem( - self.get_name(), - "in", - self.state_variable().get_name(), - ))); - parent.children.push(XMLNode::Element(make_argument_elem( - self.get_name(), - "out", - self.state_variable().get_name(), - ))); - } else { - // Cas simple - let direction = if self.is_in() { "in" } else { "out" }; - parent.children.push(XMLNode::Element(make_argument_elem( - self.get_name(), - direction, - self.state_variable().get_name(), - ))); - } - - parent - } -} - -impl UpnpModel for Argument { - type Instance = ArgumentInstance; -} - - - -impl Argument { - fn new(name: String, state_variable: Arc) -> Self { - Self { - object: UpnpObjectType { - name, - object_type: "Argument".to_string(), - }, - state_variable, - is_in: false, - is_out: false, - } - } - - pub fn new_in(name: String, state_variable: Arc) -> Self { - let mut arg = Self::new(name, state_variable); - arg.is_in = true; - arg - } - - pub fn new_out(name: String, state_variable: Arc) -> Self { - let mut arg = Self::new(name, state_variable); - arg.is_out = true; - arg - } - - pub fn new_in_out(name: String, state_variable: Arc) -> Self { - let mut arg = Self::new(name, state_variable); - arg.is_in = true; - arg.is_out = true; - arg - } - - pub fn state_variable(&self) -> &StateVariable { - &self.state_variable - } - - pub fn is_in(&self) -> bool { - self.is_in - } - - pub fn is_out(&self) -> bool { - self.is_out - } -} - -/// Fabrique un complet avec ses sous-éléments -fn make_argument_elem(name: &str, direction: &str, state_var_name: &str) -> Element { - let mut arg = Element::new("argument"); - - let mut name_elem = Element::new("name"); - name_elem.children.push(XMLNode::Text(name.to_string())); - - let mut dir_elem = Element::new("direction"); - dir_elem.children.push(XMLNode::Text(direction.to_string())); - - let mut rel_elem = Element::new("relatedStateVariable"); - rel_elem - .children - .push(XMLNode::Text(state_var_name.to_string())); - - arg.children.push(XMLNode::Element(name_elem)); - arg.children.push(XMLNode::Element(dir_elem)); - arg.children.push(XMLNode::Element(rel_elem)); - - arg -} -``` - -## fichier: `pmoupnp/src/actions/arg_inst_set_methods.rs` - -```rust -use std::collections::HashMap; - -use std::sync::RwLock; -use xmltree::{Element, XMLNode}; - -use crate::{actions::{ArgInstanceSet, ArgumentSet}, UpnpObject}; - -use crate::UpnpInstance; - -impl UpnpObject for ArgInstanceSet { - fn to_xml_element(&self) -> Element { - let mut elem = Element::new("serviceStateTable"); - - for state_var in self.all() { - let state_var_elem = state_var.to_xml_element(); // retourne un complet - elem.children.push(XMLNode::Element(state_var_elem)); - } - - elem - } -} - -impl UpnpInstance for ArgInstanceSet { - type Model = ArgumentSet; - - fn new(_: &ArgumentSet) -> Self { - Self { objects: RwLock::new(HashMap::new()) } - } -} - - -``` - -## fichier: `pmoupnp/src/actions/mod.rs` - -```rust -mod errors; - -mod action_instance; -mod action_instance_set; -mod action_methods; -mod action_set_methods; -mod arg_inst_set_methods; -mod arg_instance_methods; -mod arg_set_methods; -mod argument_methods; - -mod macros; - -use crate::{ - UpnpObjectSet, UpnpObjectType, - state_variables::{StateVarInstance, StateVariable}, -}; -use std::sync::{Arc, RwLock}; - -pub use errors::ActionError; - -#[derive(Debug, Clone)] -pub struct Action { - object: UpnpObjectType, - arguments: ArgumentSet, -} - -pub type ActionSet = UpnpObjectSet; - -#[derive(Debug, Clone)] -pub struct ActionInstance { - object: UpnpObjectType, - model: Action, - arguments: ArgInstanceSet, -} - -pub type ActionInstanceSet = UpnpObjectSet; - -#[derive(Debug, Clone)] -pub struct Argument { - object: UpnpObjectType, - state_variable: Arc, - is_in: bool, - is_out: bool, -} - -pub type ArgumentSet = UpnpObjectSet; - -/// Instance d'un argument d'action UPnP. -/// -/// Un `ArgumentInstance` représente un argument concret utilisé lors de l'exécution -/// d'une action. Contrairement au modèle [`Argument`] qui définit la structure, -/// l'instance maintient une liaison dynamique vers une [`StateVarInstance`] qui -/// contient la valeur runtime. -/// -/// # Cycle de vie -/// -/// 1. **Création** : Instanciation via [`UpnpInstance::new`] avec `variable_instance = None` -/// 2. **Liaison** : Association à une [`StateVarInstance`] via [`bind_variable`](Self::bind_variable) -/// 3. **Utilisation** : Accès à la valeur runtime via [`get_variable_instance`](Self::get_variable_instance) -/// -/// # Pourquoi `variable_instance` est optionnel ? -/// -/// La liaison ne peut pas être faite dans le constructeur car : -/// - Les `StateVarInstance` sont créées **après** les modèles -/// - Les `ActionInstance` sont créées **avant** que toutes les variables soient disponibles -/// - La validation des dépendances se fait en deux phases -/// -/// # Thread-safety -/// -/// Le champ `variable_instance` est protégé par un `RwLock` pour permettre : -/// - La liaison après création (write lock) -/// - L'accès concurrent en lecture (read lock) -/// - L'utilisation dans un contexte multi-thread -/// -/// # Examples -/// -/// ```ignore -/// use pmoupnp::actions::{Argument, ArgumentInstance}; -/// use pmoupnp::state_variables::StateVarInstance; -/// use std::sync::Arc; -/// -/// // Phase 1 : Créer l'instance (sans liaison) -/// let arg_model = Argument::new_in("Volume".to_string(), volume_var); -/// let arg_instance = ArgumentInstance::new(&arg_model); -/// assert!(arg_instance.get_variable_instance().is_none()); -/// -/// // Phase 2 : Lier à une variable d'état -/// let var_instance = Arc::new(StateVarInstance::new(&volume_var)); -/// arg_instance.bind_variable(var_instance.clone()); -/// assert!(arg_instance.get_variable_instance().is_some()); -/// -/// // Phase 3 : Utiliser la valeur runtime -/// if let Some(var) = arg_instance.get_variable_instance() { -/// println!("Current value: {}", var.value()); -/// } -/// ``` -#[derive(Debug, Clone)] -pub struct ArgumentInstance { - /// Métadonnées de l'objet UPnP - object: UpnpObjectType, - - /// Référence vers le modèle définissant la structure - model: Argument, - - /// Liaison optionnelle vers l'instance de variable d'état. - /// - /// - `None` : Pas encore liée (état initial après construction) - /// - `Some(Arc)` : Liée et prête à l'emploi - /// - /// Protégée par `RwLock` pour permettre la liaison post-construction - /// et l'accès concurrent en lecture. - variable_instance: Arc>>>, -} - -pub type ArgInstanceSet = UpnpObjectSet; -``` - -## fichier: `pmoupnp/src/actions/errors.rs` - -```rust -use thiserror::Error; - -#[derive(Error, Debug)] -pub enum ActionError { - #[error("Action error: {0}")] - GeneralError(String), - - #[error("Argument error: {0}")] - ArgumentError(String), - - #[error("Set operation error: {0}")] - SetError(String), -} - -impl From for ActionError { - fn from(err: std::io::Error) -> Self { - ActionError::GeneralError(format!("IO error: {}", err)) - } -} - -#[derive(Error, Debug)] -pub enum ArgumentError { - #[error("Argument error: {0}")] - GeneralError(String), - - #[error("Argument error: {0}")] - ArgumentError(String), - - #[error("Set operation error: {0}")] - SetError(String), -} - -impl From for ArgumentError { - fn from(err: std::io::Error) -> Self { - ArgumentError::GeneralError(format!("IO error: {}", err)) - } -}``` - -## fichier: `pmoupnp/src/actions/macros.rs` - -```rust -/// Macro pour définir facilement une action UPnP. -/// -/// Cette macro simplifie la création d'actions UPnP statiques en générant -/// automatiquement le code nécessaire pour initialiser une action avec ses arguments. -/// -/// # Syntaxe -/// -/// ## Action avec arguments -/// -/// ```ignore -/// define_action! { -/// pub static ACTION_NAME = "ActionName" { -/// in "ParamName" => VARIABLE_REF, -/// out "ResultParam" => RESULT_VAR, -/// } -/// } -/// ``` -/// -/// ## Action sans arguments -/// -/// ```ignore -/// define_action! { -/// pub static ACTION_NAME = "ActionName" -/// } -/// ``` -/// -/// # Arguments -/// -/// - `ACTION_NAME` : Nom de la constante statique Rust -/// - `"ActionName"` : Nom de l'action UPnP (chaîne littérale) -/// - `in` ou `out` : Direction de l'argument (entrée ou sortie) -/// - `"ParamName"` : Nom du paramètre UPnP (chaîne littérale) -/// - `VARIABLE_REF` : Référence vers une `Lazy>` -/// -/// # Type de retour -/// -/// La macro génère une `Lazy>` qui sera initialisée lors du premier accès. -/// -/// # Prérequis -/// -/// Les variables d'état référencées doivent être définies comme : -/// -/// ```ignore -/// pub static MY_VAR: Lazy> = Lazy::new(|| { -/// Arc::new(StateVariable::new(StateVarType::UI4, "MyVar".to_string())) -/// }); -/// ``` -/// -/// # Examples -/// -/// ```ignore -/// use once_cell::sync::Lazy; -/// use std::sync::Arc; -/// -/// // Définir les variables d'état -/// pub static INSTANCE_ID: Lazy> = Lazy::new(|| { -/// Arc::new(StateVariable::new(StateVarType::UI4, "InstanceID".to_string())) -/// }); -/// -/// pub static TRANSPORT_URI: Lazy> = Lazy::new(|| { -/// Arc::new(StateVariable::new(StateVarType::String, "TransportURI".to_string())) -/// }); -/// -/// // Définir une action avec arguments -/// define_action! { -/// pub static PLAY = "Play" { -/// in "InstanceID" => INSTANCE_ID, -/// in "Speed" => TRANSPORT_SPEED, -/// } -/// } -/// -/// // Action sans arguments -/// define_action! { -/// pub static PAUSE = "Pause" -/// } -/// -/// // Utilisation -/// fn main() { -/// let play_action = &*PLAY; // Déréférence la Lazy> -/// println!("Action: {}", play_action.get_name()); -/// } -/// ``` -/// -/// # Notes d'implémentation -/// -/// - Les `Arc` sont clonés (shallow copy du pointeur) -/// - Chaque `Argument` est wrappé dans un `Arc` -/// - L'`Action` finale est wrappée dans un `Arc` -/// - Initialisation paresseuse via `Lazy` (thread-safe) -#[macro_export] -macro_rules! define_action { - // Variante sans arguments - (pub static $name:ident = $action_name:literal) => { - pub static $name: once_cell::sync::Lazy> = - once_cell::sync::Lazy::new(|| { - std::sync::Arc::new($crate::actions::Action::new($action_name.to_string())) - }); - }; - - // Variante avec arguments - (pub static $name:ident = $action_name:literal { - $( - $direction:ident $arg_name:literal => $var_ref:expr - ),* $(,)? - }) => { - pub static $name: once_cell::sync::Lazy> = - once_cell::sync::Lazy::new(|| { - let mut ac = $crate::actions::Action::new($action_name.to_string()); - - $( - ac.add_argument( - define_action!(@arg $direction $arg_name, $var_ref) - ); - )* - - std::sync::Arc::new(ac) - }); - }; - - // Helper interne pour créer un argument d'entrée - (@arg in $name:literal, $var:expr) => { - std::sync::Arc::new( - $crate::actions::Argument::new_in( - $name.to_string(), - std::sync::Arc::clone(&$var) - ) - ) - }; - - // Helper interne pour créer un argument de sortie - (@arg out $name:literal, $var:expr) => { - std::sync::Arc::new( - $crate::actions::Argument::new_out( - $name.to_string(), - std::sync::Arc::clone(&$var) - ) - ) - }; -} - -/// Macro pour définir plusieurs actions UPnP en une seule déclaration. -/// -/// Cette macro permet de regrouper la définition de plusieurs actions pour -/// améliorer la lisibilité et réduire la répétition de code. -/// -/// # Syntaxe -/// -/// ```ignore -/// define_actions! { -/// ACTION1 = "Action1" { -/// in "Param1" => VAR1, -/// out "Result1" => VAR2, -/// } -/// -/// ACTION2 = "Action2" { -/// in "Param1" => VAR1, -/// } -/// -/// ACTION3 = "Action3" -/// } -/// ``` -/// -/// # Arguments -/// -/// Chaque action suit la même syntaxe que [`define_action!`], mais sans -/// le mot-clé `pub static`. -/// -/// # Type de retour -/// -/// Génère une `Lazy>` pour chaque action définie. -/// -/// # Examples -/// -/// ```ignore -/// use once_cell::sync::Lazy; -/// use std::sync::Arc; -/// -/// // Variables d'état -/// pub static INSTANCE_ID: Lazy> = Lazy::new(|| { -/// Arc::new(StateVariable::new(StateVarType::UI4, "InstanceID".to_string())) -/// }); -/// -/// pub static TRANSPORT_URI: Lazy> = Lazy::new(|| { -/// Arc::new(StateVariable::new(StateVarType::String, "TransportURI".to_string())) -/// }); -/// -/// pub static URI_METADATA: Lazy> = Lazy::new(|| { -/// Arc::new(StateVariable::new(StateVarType::String, "URIMetaData".to_string())) -/// }); -/// -/// // Définir plusieurs actions ensemble -/// define_actions! { -/// PLAY = "Play" { -/// in "InstanceID" => INSTANCE_ID, -/// } -/// -/// STOP = "Stop" { -/// in "InstanceID" => INSTANCE_ID, -/// } -/// -/// PAUSE = "Pause" { -/// in "InstanceID" => INSTANCE_ID, -/// } -/// -/// SET_AV_TRANSPORT_URI = "SetAVTransportURI" { -/// in "InstanceID" => INSTANCE_ID, -/// in "CurrentURI" => TRANSPORT_URI, -/// in "CurrentURIMetaData" => URI_METADATA, -/// } -/// } -/// -/// // Utilisation -/// fn setup_transport_service() { -/// let actions = vec![&*PLAY, &*STOP, &*PAUSE, &*SET_AV_TRANSPORT_URI]; -/// for action in actions { -/// println!("Action: {}", action.get_name()); -/// } -/// } -/// ``` -/// -/// # Avantages -/// -/// - Regroupement logique des actions d'un service -/// - Réduction de la répétition de `pub static` et `define_action!` -/// - Meilleure lisibilité pour les services avec nombreuses actions -/// -/// # Notes -/// -/// - Toutes les actions définies sont publiques (`pub`) -/// - Chaque action est indépendante et peut être utilisée séparément -/// - La macro se développe en plusieurs appels à [`define_action!`] -#[macro_export] -macro_rules! define_actions { - // Variante avec arguments pour chaque action - ( - $( - $name:ident = $action_name:literal { - $( - $direction:ident $arg_name:literal => $var_ref:expr - ),* $(,)? - } - )* - ) => { - $( - define_action! { - pub static $name = $action_name { - $($direction $arg_name => $var_ref),* - } - } - )* - }; - - // Variante mixte : actions avec et sans arguments - ( - $( - $name:ident = $action_name:literal $({ - $( - $direction:ident $arg_name:literal => $var_ref:expr - ),* $(,)? - })? - )* - ) => { - $( - $( - define_action! { - pub static $name = $action_name { - $($direction $arg_name => $var_ref),* - } - } - )? - $( - // Cas sans accolades (action sans arguments) - #[allow(unused)] - define_action! { - pub static $name = $action_name - } - )? - )* - }; -}``` - -## fichier: `pmoupnp/src/actions/action_set_methods.rs` - -```rust -use xmltree::{Element, XMLNode}; - -use crate::actions::ActionSet; -use crate::UpnpObject; - -impl UpnpObject for ActionSet { - fn to_xml_element(&self) -> Element { - let mut elem = Element::new("actionList"); - - for action in self.all() { - let action_elem = action.to_xml_element(); // retourne un complet - elem.children.push(XMLNode::Element(action_elem)); - } - - elem - } - -} - -``` - -## fichier: `pmoupnp/src/actions/action_methods.rs` - -```rust -use std::sync::Arc; - -use xmltree::{Element, XMLNode}; - -use crate::UpnpModel; -use crate::UpnpObject; -use crate::UpnpObjectSetError; -use crate::UpnpObjectType; -use crate::UpnpTyped; -use crate::actions::Action; -use crate::actions::ActionInstance; -use crate::actions::Argument; -use crate::actions::ArgumentSet; - -impl UpnpObject for Action { - fn to_xml_element(&self) -> Element { - let mut action_elem = Element::new("action"); - - // - let mut name_elem = Element::new("name"); - name_elem - .children - .push(XMLNode::Text(self.get_name().clone())); - action_elem.children.push(XMLNode::Element(name_elem)); - - // - let args_elem = self.arguments.to_xml_element(); - action_elem.children.push(XMLNode::Element(args_elem)); - - action_elem - } -} - -impl UpnpModel for Action { - type Instance = ActionInstance; -} - -impl UpnpTyped for Action { - fn as_upnp_object_type(&self) -> &UpnpObjectType { - return &self.object; - } -} - -impl Action { - pub fn new(name: String) -> Action { - Self { - object: UpnpObjectType { - name, - object_type: "Action".to_string(), - }, - arguments: ArgumentSet::new(), - } - } - - pub fn add_argument(&mut self, arg: Arc) -> Result<(), UpnpObjectSetError> { - self.arguments.insert(arg) - } - - pub fn arguments(&self) -> &ArgumentSet { - &self.arguments - } -} -``` - -## fichier: `pmoupnp/src/actions/arg_instance_methods.rs` - -```rust -use std::sync::{Arc, RwLock}; - -use xmltree::Element; - -use crate::{actions::{Argument, ArgumentInstance}, state_variables::StateVarInstance, UpnpInstance, UpnpObject, UpnpObjectType, UpnpTyped, UpnpTypedInstance}; - - -impl UpnpObject for ArgumentInstance { - fn to_xml_element(&self) -> Element { - self.get_model().to_xml_element() - } -} - -impl UpnpTyped for ArgumentInstance { - fn as_upnp_object_type(&self) -> &UpnpObjectType { - return &self.object; - } -} - -/// Implémentation de [`UpnpTypedInstance`] pour [`ArgumentInstance`]. -/// -/// Cette implémentation permet d'accéder au modèle [`Argument`] depuis l'instance -/// via la méthode [`get_model()`](UpnpTypedInstance::get_model). -/// -/// # Examples -/// -/// ```ignore -/// use pmoupnp::UpnpTypedInstance; -/// -/// let arg_instance = ArgumentInstance::new(&arg_model); -/// -/// // Accéder au modèle -/// let model = arg_instance.get_model(); -/// println!("Direction: in={}, out={}", model.is_in(), model.is_out()); -/// println!("Related variable: {}", model.state_variable().get_name()); -/// ``` -impl UpnpTypedInstance for ArgumentInstance { - /// Retourne une référence vers le modèle [`Argument`]. - /// - /// Permet d'accéder aux métadonnées statiques définies dans le modèle : - /// - Direction de l'argument (in/out) - /// - Variable d'état associée - /// - Nom et type - fn get_model(&self) -> &Self::Model { - &self.model - } -} - - -/// Implémentation de [`UpnpInstance`] pour [`ArgumentInstance`]. -/// -/// Cette implémentation fournit le constructeur standard qui crée une instance -/// **non liée** d'un argument. La liaison à une [`StateVarInstance`] doit être -/// effectuée séparément via [`bind_variable`](ArgumentInstance::bind_variable). -/// -/// # Processus de construction en deux phases -/// -/// ```text -/// Phase 1 (new) Phase 2 (bind_variable) -/// ┌─────────────────┐ ┌──────────────────────┐ -/// │ ArgumentInstance│ │ StateVarInstance │ -/// │ │ │ │ -/// │ model: Arc<...> │────>│ Liaison établie │ -/// │ variable: None │ │ variable: Some(...) │ -/// └─────────────────┘ └──────────────────────┘ -/// ↓ ↓ -/// Création bind_variable(&var) -/// ``` -/// -/// # Pourquoi deux phases ? -/// -/// 1. **Ordre de création** : Les modèles (`Argument`) existent avant les instances -/// 2. **Validation différée** : Les dépendances sont vérifiées après instanciation -/// 3. **Découplage** : Permet de créer des arguments même si les variables n'existent pas encore -/// -/// # Examples -/// -/// ```ignore -/// use pmoupnp::actions::{Argument, ArgumentInstance}; -/// use pmoupnp::UpnpInstance; -/// -/// let arg_model = Argument::new_in("InstanceID".to_string(), instance_id_var); -/// -/// // Création de l'instance - Phase 1 -/// let arg_instance = ArgumentInstance::new(&arg_model); -/// -/// // À ce stade, l'instance existe mais n'est pas encore liée -/// assert_eq!(arg_instance.get_name(), "InstanceID"); -/// assert!(arg_instance.get_variable_instance().is_none()); -/// -/// // La liaison se fera plus tard via bind_variable() -/// ``` -impl UpnpInstance for ArgumentInstance { - type Model = Argument; - - /// Crée une nouvelle instance d'argument depuis son modèle. - /// - /// # Arguments - /// - /// * `from` - Référence vers le modèle [`Argument`] définissant cet argument - /// - /// # Returns - /// - /// Une nouvelle `ArgumentInstance` avec : - /// - Nom copié depuis le modèle - /// - Référence vers le modèle (clone) - /// - `variable_instance` initialisé à `None` (liaison non établie) - /// - /// # État initial - /// - /// L'instance créée n'est **pas encore liée** à une variable d'état. - /// Pour établir la liaison, appelez [`bind_variable`](ArgumentInstance::bind_variable). - /// - /// # Thread-safety - /// - /// L'instance retournée est thread-safe et peut être partagée via `Arc`. - /// - /// # Examples - /// - /// ```ignore - /// use pmoupnp::UpnpInstance; - /// - /// // Création depuis un modèle - /// let instance = ArgumentInstance::new(&arg_model); - /// - /// // L'instance hérite des propriétés du modèle - /// assert_eq!(instance.get_name(), arg_model.get_name()); - /// assert_eq!(instance.is_in(), arg_model.is_in()); - /// - /// // Mais n'a pas encore de valeur runtime - /// assert!(instance.get_variable_instance().is_none()); - /// ``` - fn new(from: &Argument) -> Self { - Self { - // Copie des métadonnées depuis le modèle - object: UpnpObjectType { - name: from.get_name().clone(), - object_type: "ArgumentInstance".to_string(), - }, - - // Clone du modèle pour référence future - model: from.clone(), - - // Initialisation à None - sera lié plus tard via bind_variable() - // Arc> permet la modification thread-safe post-construction - variable_instance: Arc::new(RwLock::new(None)), - } - } -} - -// ============================================================================ -// Méthodes de liaison et d'accès -// ============================================================================ - -impl ArgumentInstance { - /// Lie cet argument à une instance de variable d'état. - /// - /// Cette méthode établit la connexion entre l'argument et sa variable d'état, - /// permettant l'accès aux valeurs runtime lors de l'exécution d'actions. - /// - /// # Arguments - /// - /// * `var_instance` - Instance de la variable d'état à lier - /// - /// # Thread-safety - /// - /// Cette méthode acquiert un **write lock** sur `variable_instance` et peut - /// bloquer si d'autres threads lisent actuellement la valeur. - /// - /// # Panics - /// - /// Panique si le lock est empoisonné (poisoned), ce qui ne devrait jamais - /// arriver dans un usage normal. - /// - /// # Examples - /// - /// ```ignore - /// use std::sync::Arc; - /// - /// let arg_instance = ArgumentInstance::new(&arg_model); - /// let var_instance = Arc::new(StateVarInstance::new(&state_var)); - /// - /// // Établir la liaison - /// arg_instance.bind_variable(var_instance.clone()); - /// - /// // Vérifier que la liaison est établie - /// assert!(arg_instance.get_variable_instance().is_some()); - /// ``` - /// - /// # Note - /// - /// Cette méthode peut être appelée plusieurs fois pour changer la variable liée, - /// bien que ce ne soit généralement pas recommandé dans un usage normal. - pub fn bind_variable(&self, var_instance: Arc) { - let mut var = self.variable_instance.write().unwrap(); - *var = Some(var_instance); - } - - /// Retourne l'instance de variable d'état liée, si elle existe. - /// - /// # Returns - /// - /// - `Some(Arc)` si une variable est liée - /// - `None` si aucune liaison n'a été établie via [`bind_variable`](Self::bind_variable) - /// - /// # Thread-safety - /// - /// Cette méthode acquiert un **read lock** sur `variable_instance`. - /// Plusieurs threads peuvent lire simultanément sans blocage. - /// - /// # Panics - /// - /// Panique si le lock est empoisonné (poisoned). - /// - /// # Examples - /// - /// ```ignore - /// // Vérifier si la liaison existe - /// if let Some(var) = arg_instance.get_variable_instance() { - /// println!("Variable liée : {}", var.get_name()); - /// println!("Valeur actuelle : {}", var.value()); - /// } else { - /// println!("Aucune variable liée"); - /// } - /// ``` - /// - /// # Usage dans l'exécution d'actions - /// - /// ```ignore - /// async fn execute_action(action: &ActionInstance) -> Result<(), ActionError> { - /// for arg in action.arguments_set().all() { - /// if let Some(var) = arg.get_variable_instance() { - /// // Utiliser var.value() pour lire/écrire - /// println!("Paramètre {} = {}", arg.get_name(), var.value()); - /// } else { - /// return Err(ActionError::UnboundArgument(arg.get_name().to_string())); - /// } - /// } - /// Ok(()) - /// } - /// ``` - pub fn get_variable_instance(&self) -> Option> { - self.variable_instance.read().unwrap().clone() - } -} -``` - -## fichier: `pmoupnp/src/variable_types/values_from_i64.rs` - -```rust -use crate::variable_types::{StateValue, StateValueError}; -use std::convert::TryFrom; - -impl TryFrom<&StateValue> for i64 { - type Error = StateValueError; - - fn try_from(value: &StateValue) -> Result { - match value { - // signés - StateValue::I1(v) => Ok(*v as i64), - StateValue::I2(v) => Ok(*v as i64), - StateValue::I4(v) => Ok(*v as i64), - StateValue::Int(v) => Ok(*v as i64), - - // non signés - StateValue::UI1(v) => Ok(*v as i64), - StateValue::UI2(v) => Ok(*v as i64), - StateValue::UI4(v) => Ok(*v as i64), // toujours dans l'intervalle d'un i64 - - // booléen - StateValue::Boolean(v) => Ok(*v as i64), - - // chaîne → i64 - StateValue::String(s) => s - .parse::() - .map_err(|_| StateValueError::TypeError(format!("Cannot parse '{}' as i64", s))), - - _ => Err(StateValueError::TypeError("Cannot cast to i64".into())), - } - } -} - -impl TryFrom for i64 { - type Error = StateValueError; - - fn try_from(value: StateValue) -> Result { - i64::try_from(&value) - } -} - -impl From for StateValue { - fn from(value: i64) -> Self { - StateValue::Int(value as i32) // ⚠️ choix à discuter : Int est i32, pas i64 - } -} -``` - -## fichier: `pmoupnp/src/variable_types/values_from_f32.rs` - -```rust -use std::convert::TryFrom; - -use crate::variable_types::{StateValue, StateValueError}; - -impl TryFrom<&StateValue> for f32 { - type Error = StateValueError; - - fn try_from(value: &StateValue) -> Result { - const MAX_EXACT: i32 = 1 << f32::MANTISSA_DIGITS; - match value { - // --- Signed integers --- - StateValue::I1(v) => Ok(*v as f32), - StateValue::I2(v) => Ok(*v as f32), - StateValue::I4(v) if *v > -MAX_EXACT && *v < MAX_EXACT => Ok(*v as f32), - StateValue::Int(v) if *v >= -MAX_EXACT && *v <= MAX_EXACT as i32 => Ok(*v as f32), - - // --- Unsigned integers --- - StateValue::UI1(v) => Ok(*v as f32), - StateValue::UI2(v) => Ok(*v as f32), - StateValue::UI4(v) if *v <= MAX_EXACT as u32 => Ok(*v as f32), - StateValue::UI4(_) => Err(StateValueError::TypeError( - "Cannot cast UI4 to f32: out of range".into(), - )), - - // --- Floats --- - StateValue::R4(v) => Ok(*v), // déjà un f32 - StateValue::R8(v) - if !v.is_finite() || (*v <= f32::MAX as f64 && *v >= f32::MIN as f64) => - { - Ok(*v as f32) - } - StateValue::R8(_) => Err(StateValueError::TypeError( - "Cannot cast R8 to f32: out of range".into(), - )), - StateValue::Number(v) - if !v.is_finite() || (*v <= f32::MAX as f64 && *v >= f32::MIN as f64) => - { - Ok(*v as f32) - } - StateValue::Number(_) => Err(StateValueError::TypeError( - "Cannot cast Number to f32: out of range".into(), - )), - StateValue::Fixed14_4(v) - if !v.is_finite() || (*v <= f32::MAX as f64 && *v >= f32::MIN as f64) => - { - Ok(*v as f32) - } - StateValue::Fixed14_4(_) => Err(StateValueError::TypeError( - "Cannot cast Fixed14_4 to f32: out of range".into(), - )), - - StateValue::Boolean(v) => Ok((*v as i32) as Self), - - StateValue::String(s) => s - .parse::() - .map_err(|_| StateValueError::TypeError(format!("Cannot parse '{}' as f32", s))), - - // --- Par défaut : erreur --- - _ => Err(StateValueError::TypeError("Cannot cast to f32".into())), - } - } -} - -impl TryFrom for f32 { - type Error = StateValueError; - - fn try_from(value: StateValue) -> Result { - f32::try_from(&value) - } -} - -impl From for StateValue { - fn from(value: f32) -> Self { - StateValue::R4(value) - } -} -``` - -## fichier: `pmoupnp/src/variable_types/values_from_naivedate.rs` - -```rust -use crate::variable_types::{StateValue, StateValueError}; -use chrono::NaiveDate; -use std::convert::TryFrom; - -impl TryFrom<&StateValue> for NaiveDate { - type Error = StateValueError; - - fn try_from(value: &StateValue) -> Result { - match value { - StateValue::Date(v) => Ok(v.clone()), - StateValue::String(v) => NaiveDate::parse_from_str(v, "%Y-%m-%d").map_err(|e| { - StateValueError::ParseError(format!("Cannot parse Date from string '{}': {}", v, e)) - }), - _ => Err(StateValueError::TypeError( - "Cannot cast to NaiveDate".into(), - )), - } - } -} - -impl TryFrom for NaiveDate { - type Error = StateValueError; - - fn try_from(value: StateValue) -> Result { - NaiveDate::try_from(&value) - } -} - -impl From for StateValue { - fn from(value: NaiveDate) -> Self { - StateValue::Date(value) - } -} -``` - -## fichier: `pmoupnp/src/variable_types/value_methods.rs` - -```rust -use std::cmp::Ordering; - -use crate::variable_types::{StateValue, StateVarType, type_trait::UpnpVarType}; - -impl UpnpVarType for StateValue { - fn as_state_var_type(&self) -> StateVarType { - StateVarType::from(self) // utilise ton From<&StateValue> existant - } -} - -impl PartialEq for StateValue { - fn eq(&self, other: &Self) -> bool { - match (self, other) { - (a, b) if a.is_integer() && b.is_integer() => { - if let (Ok(ia), Ok(ib)) = (i64::try_from(a), i64::try_from(b)) { - return ia == ib; - }; - return false; - } - (a, b) if a.is_float() && b.is_float() => { - if let (Ok(a), Ok(b)) = (f64::try_from(self), f64::try_from(other)) { - return a == b; // NaN respecte la sémantique IEEE (NaN != NaN) - } - return false; - } - (a, b) if a.is_string() && b.is_string() => { - let (a, b) = (self.to_string(), other.to_string()); - return a == b; // NaN respecte la sémantique IEEE (NaN != NaN) - } - (StateValue::Date(a), StateValue::Date(b)) => { - return a == b; - } - (StateValue::Time(a), StateValue::Time(b)) => { - return a == b; - } - (StateValue::DateTime(a), StateValue::DateTime(b)) => { - return a == b; - } - (StateValue::DateTimeTZ(a), StateValue::DateTimeTZ(b)) => { - return a == b; - } - (StateValue::TimeTZ(a), StateValue::TimeTZ(b)) => { - return a == b; - } - - (_, _) => return false, - } - } -} - -impl PartialOrd for StateValue { - fn partial_cmp(&self, other: &Self) -> Option { - match (self, other) { - (a, b) if a.is_integer() && b.is_integer() => { - if let (Ok(ia), Ok(ib)) = (i64::try_from(a), i64::try_from(b)) { - return Some(ia.cmp(&ib)); - }; - return None; - } - (a, b) if a.is_float() && b.is_float() => { - if let (Ok(a), Ok(b)) = (f64::try_from(self), f64::try_from(other)) { - return a.partial_cmp(&b); // NaN respecte la sémantique IEEE (NaN != NaN) - } - return None; - } - (a, b) if a.is_string() && b.is_string() => { - let (a, b) = (self.to_string(), other.to_string()); - return Some(a.cmp(&b)); // NaN respecte la sémantique IEEE (NaN != NaN) - } - (StateValue::Date(a), StateValue::Date(b)) => { - return Some(a.cmp(&b)); - } - (StateValue::Time(a), StateValue::Time(b)) => { - return Some(a.cmp(&b)); - } - (StateValue::DateTime(a), StateValue::DateTime(b)) => { - return Some(a.cmp(&b)); - } - (StateValue::DateTimeTZ(a), StateValue::DateTimeTZ(b)) => { - return Some(a.cmp(&b)); - } - (StateValue::TimeTZ(a), StateValue::TimeTZ(b)) => { - return Some(a.cmp(&b)); - } - (_, _) => return None, - } - } -} -``` - -## fichier: `pmoupnp/src/variable_types/values_from_type.rs` - -```rust -use crate::variable_types::{StateValue, StateVarType}; - -impl From<&StateValue> for StateVarType { - fn from(value: &StateValue) -> Self { - match value { - StateValue::UI1(_) => StateVarType::UI1, - StateValue::UI2(_) => StateVarType::UI2, - StateValue::UI4(_) => StateVarType::UI4, - StateValue::I1(_) => StateVarType::I1, - StateValue::I2(_) => StateVarType::I2, - StateValue::I4(_) => StateVarType::I4, - StateValue::Int(_) => StateVarType::Int, - StateValue::R4(_) => StateVarType::R4, - StateValue::R8(_) => StateVarType::R8, - StateValue::Number(_) => StateVarType::Number, - StateValue::Fixed14_4(_) => StateVarType::Fixed14_4, - StateValue::Char(_) => StateVarType::Char, - StateValue::String(_) => StateVarType::String, - StateValue::BinBase64(_) => StateVarType::BinBase64, - StateValue::BinHex(_) => StateVarType::BinHex, - StateValue::URI(_) => StateVarType::URI, - StateValue::UUID(_) => StateVarType::UUID, - StateValue::Date(_) => StateVarType::Date, - StateValue::DateTime(_) => StateVarType::DateTime, - StateValue::DateTimeTZ(_) => StateVarType::DateTimeTZ, - StateValue::Time(_) => StateVarType::Time, - StateValue::TimeTZ(_) => StateVarType::TimeTZ, - StateValue::Boolean(_) => StateVarType::Boolean, - } - } -} - -impl From for StateVarType { - fn from(value: StateValue) -> Self { - StateVarType::from(&value) - } -} -``` - -## fichier: `pmoupnp/src/variable_types/values_from_datetime.rs` - -```rust -use crate::variable_types::{StateValue, StateValueError}; -use chrono::{DateTime, FixedOffset}; -use std::convert::TryFrom; - -impl TryFrom<&StateValue> for DateTime { - type Error = StateValueError; - - fn try_from(value: &StateValue) -> Result { - match value { - StateValue::DateTimeTZ(v) => Ok(v.clone()), - StateValue::TimeTZ(v) => Ok(v.clone()), - StateValue::String(v) => DateTime::parse_from_rfc3339(v).map_err(|e| { - StateValueError::ParseError(format!( - "Cannot parse DateTimeTZ from string '{}': {}", - v, e - )) - }), - _ => Err(StateValueError::TypeError( - "Cannot cast to DateTime".into(), - )), - } - } -} - -impl TryFrom for DateTime { - type Error = StateValueError; - - fn try_from(value: StateValue) -> Result { - DateTime::::try_from(&value) - } -} - -impl From> for StateValue { - fn from(value: DateTime) -> Self { - StateValue::DateTimeTZ(value) - } -} -``` - -## fichier: `pmoupnp/src/variable_types/values_from_u16.rs` - -```rust -use std::convert::TryFrom; - -use crate::variable_types::{StateValue, StateValueError}; - -// Implémentations TryFrom pour types numériques - -impl TryFrom<&StateValue> for u16 { - type Error = StateValueError; - - fn try_from(value: &StateValue) -> Result { - match value { - StateValue::UI1(v) => Ok(*v as Self), - StateValue::UI2(v) => Ok(*v), - StateValue::UI4(v) if *v <= i16::MAX as u32 => Ok(*v as Self), - StateValue::I1(v) if *v >= 0 => Ok(*v as Self), - StateValue::I2(v) if *v >= 0 => Ok(*v as Self), - StateValue::I4(v) if *v >= 0 && *v <= u16::MAX as i32 => Ok(*v as Self), - StateValue::Int(v) if *v >= 0 && *v <= u16::MAX as i32 => Ok(*v as Self), - StateValue::Boolean(v) => Ok(*v as Self), - - StateValue::String(s) => s - .parse::() - .map_err(|_| StateValueError::TypeError(format!("Cannot parse '{}' as u16", s))), - - _ => Err(StateValueError::TypeError("Cannot cast to u16".into())), - } - } -} - -impl TryFrom for u16 { - type Error = StateValueError; - - fn try_from(value: StateValue) -> Result { - u16::try_from(&value) - } -} - -impl From for StateValue { - fn from(value: u16) -> Self { - StateValue::UI2(value) - } -} -``` - -## fichier: `pmoupnp/src/variable_types/fromstr.rs` - -```rust -use crate::variable_types::StateVarType; -use std::str::FromStr; - -impl FromStr for StateVarType { - type Err = String; // Type d'erreur personnalisé - - fn from_str(s: &str) -> Result { - match s.to_lowercase().as_str() { - "ui1" => Ok(StateVarType::UI1), - "ui2" => Ok(StateVarType::UI2), - "ui4" => Ok(StateVarType::UI4), - "i1" => Ok(StateVarType::I1), - "i2" => Ok(StateVarType::I2), - "i4" => Ok(StateVarType::I4), - "int" => Ok(StateVarType::Int), - "r4" => Ok(StateVarType::R4), - "r8" => Ok(StateVarType::R8), - "number" => Ok(StateVarType::Number), - "fixed.14.4" => Ok(StateVarType::Fixed14_4), - "char" => Ok(StateVarType::Char), - "string" => Ok(StateVarType::String), - "boolean" => Ok(StateVarType::Boolean), - "bin.base64" => Ok(StateVarType::BinBase64), - "bin.hex" => Ok(StateVarType::BinHex), - "date" => Ok(StateVarType::Date), - "datetime" => Ok(StateVarType::DateTime), - "datetime.tz" => Ok(StateVarType::DateTimeTZ), - "time" => Ok(StateVarType::Time), - "time.tz" => Ok(StateVarType::TimeTZ), - "uuid" => Ok(StateVarType::UUID), - "uri" => Ok(StateVarType::URI), - _ => Err(format!("Type inconnu: {}", s)), - } - } -} -``` - -## fichier: `pmoupnp/src/variable_types/values_from_u32.rs` - -```rust -use std::convert::TryFrom; - -use crate::variable_types::{StateValue, StateValueError}; - -impl TryFrom<&StateValue> for u32 { - type Error = StateValueError; - - fn try_from(value: &StateValue) -> Result { - match value { - StateValue::UI1(v) => Ok(*v as Self), - StateValue::UI2(v) => Ok(*v as Self), - StateValue::UI4(v) => Ok(*v), - StateValue::I1(v) if *v >= 0 => Ok(*v as Self), - StateValue::I2(v) if *v >= 0 => Ok(*v as Self), - StateValue::I4(v) if *v >= 0 => Ok(*v as Self), - StateValue::Int(v) if *v >= 0 => Ok(*v as Self), - StateValue::Boolean(v) => Ok(*v as Self), - - StateValue::String(s) => s - .parse::() - .map_err(|_| StateValueError::TypeError(format!("Cannot parse '{}' as u32", s))), - - _ => Err(StateValueError::TypeError("Cannot cast to u32".into())), - } - } -} - -impl TryFrom for u32 { - type Error = StateValueError; - - fn try_from(value: StateValue) -> Result { - u32::try_from(&value) - } -} - -impl From for StateValue { - fn from(value: u32) -> Self { - StateValue::UI4(value) - } -} -``` - -## fichier: `pmoupnp/src/variable_types/values_from_vec_u8.rs` - -```rust -use crate::variable_types::{StateValue, StateValueError}; -use base64::{Engine as _, engine::general_purpose::STANDARD}; -use std::convert::TryFrom; - -impl TryFrom<&StateValue> for Vec { - type Error = StateValueError; - - fn try_from(value: &StateValue) -> Result { - match value { - // Déjà un vecteur binaire - StateValue::BinBase64(v) => STANDARD - .decode(v) - .map_err(|e| StateValueError::ParseError(format!("Base64 decode error: {}", e))), - StateValue::BinHex(v) => hex::decode(v) - .map_err(|e| StateValueError::ParseError(format!("BinHex decode error: {}", e))), - - // Conversion depuis une chaîne encodée - StateValue::String(s) => { - // Essayer Base64 - if let Ok(bytes) = STANDARD.decode(s) { - return Ok(bytes); - } - // Essayer Hex - if let Ok(bytes) = hex::decode(s) { - return Ok(bytes); - } - Err(StateValueError::ParseError(format!( - "Cannot parse string '{}' as binary", - s - ))) - } - - _ => Err(StateValueError::TypeError( - "Cannot cast to binary Vec".into(), - )), - } - } -} - -impl TryFrom for Vec { - type Error = StateValueError; - - fn try_from(value: StateValue) -> Result { - Vec::::try_from(&value) - } -} -``` - -## fichier: `pmoupnp/src/variable_types/default_value.rs` - -```rust -use chrono::{DateTime, FixedOffset, NaiveDate, NaiveTime}; -use url::Url; -use uuid::Uuid; - -use crate::variable_types::{StateValue, StateVarType}; - -impl StateVarType { - pub fn default_value(&self) -> StateValue { - match self { - StateVarType::UI1 => StateValue::UI1(0), - StateVarType::UI2 => StateValue::UI2(0), - StateVarType::UI4 => StateValue::UI4(0), - StateVarType::I1 => StateValue::I1(0), - StateVarType::I2 => StateValue::I2(0), - StateVarType::I4 => StateValue::I4(0), - StateVarType::Int => StateValue::Int(0), - StateVarType::R4 => StateValue::R4(0.0), - StateVarType::R8 => StateValue::R8(0.0), - StateVarType::Number => StateValue::Number(0.0), - StateVarType::Fixed14_4 => StateValue::Fixed14_4(0.0), - StateVarType::Char => StateValue::Char('\0'), - StateVarType::String => StateValue::String(String::new()), - StateVarType::Boolean => StateValue::Boolean(false), - StateVarType::BinBase64 => StateValue::BinBase64(String::new()), - StateVarType::BinHex => StateValue::BinHex(String::new()), - StateVarType::Date => StateValue::Date(NaiveDate::from_ymd_opt(1970, 1, 1).unwrap()), - StateVarType::DateTime => { - StateValue::DateTime(DateTime::from_timestamp(0, 0).unwrap().naive_utc().into()) - } - StateVarType::DateTimeTZ => { - StateValue::DateTimeTZ(DateTime::from_naive_utc_and_offset( - DateTime::from_timestamp(0, 0).unwrap().naive_utc(), - FixedOffset::east_opt(0).unwrap(), - )) - } - StateVarType::Time => StateValue::Time(NaiveTime::from_hms_opt(0, 0, 0).unwrap()), - StateVarType::TimeTZ => StateValue::TimeTZ(DateTime::from_naive_utc_and_offset( - DateTime::from_timestamp(0, 0).unwrap().naive_utc(), - FixedOffset::east_opt(0).unwrap(), - )), - StateVarType::UUID => StateValue::UUID(Uuid::nil()), - StateVarType::URI => StateValue::URI(Url::parse("http://localhost").unwrap()), - } - } -} -``` - -## fichier: `pmoupnp/src/variable_types/values_from_str.rs` - -```rust -use std::convert::TryFrom; - -use crate::variable_types::{StateValue, StateValueError}; - -impl TryFrom<&str> for StateValue { - type Error = StateValueError; - - fn try_from(s: &str) -> Result { - Ok(StateValue::String(s.to_string())) - } -} - -// Conversion depuis String -impl TryFrom for StateValue { - type Error = StateValueError; - - fn try_from(s: String) -> Result { - Ok(StateValue::String(s)) - } -}``` - -## fichier: `pmoupnp/src/variable_types/display_value.rs` - -```rust -use base64::Engine; -use base64::engine::general_purpose; -use std::fmt; - -use crate::variable_types::StateValue; - -impl fmt::Display for StateValue { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match self { - // Numériques - StateValue::UI1(v) => write!(f, "{}", v), - StateValue::UI2(v) => write!(f, "{}", v), - StateValue::UI4(v) => write!(f, "{}", v), - StateValue::I1(v) => write!(f, "{}", v), - StateValue::I2(v) => write!(f, "{}", v), - StateValue::I4(v) => write!(f, "{}", v), - StateValue::Int(v) => write!(f, "{}", v), - StateValue::R4(v) => write!(f, "{}", v), - StateValue::R8(v) => write!(f, "{}", v), - StateValue::Number(v) => write!(f, "{}", v), - StateValue::Fixed14_4(v) => write!(f, "{}", v), - - // Types déjà Display - StateValue::Char(v) => write!(f, "{}", v), - StateValue::String(v) => write!(f, "{}", v), - StateValue::UUID(v) => write!(f, "{}", v), - StateValue::URI(v) => write!(f, "{}", v), - - // Booléen : 1 ou 0 - StateValue::Boolean(v) => write!(f, "{}", if *v { "1" } else { "0" }), - - // Encodages binaires - StateValue::BinBase64(v) => write!(f, "{}", general_purpose::URL_SAFE.encode(v)), - StateValue::BinHex(v) => write!(f, "{}", hex::encode(v)), - - // Dates et temps - StateValue::Date(v) => write!(f, "{}", v.format("%Y-%m-%d")), - StateValue::DateTime(v) => write!(f, "{}", v.format("%Y-%m-%dT%H:%M:%S")), - StateValue::DateTimeTZ(v) => write!(f, "{}", v.to_rfc3339()), - StateValue::Time(v) => write!(f, "{}", v.format("%H:%M:%S")), - StateValue::TimeTZ(v) => write!(f, "{}", v.format("%H:%M:%S%z")), - } - } -} -``` - -## fichier: `pmoupnp/src/variable_types/values_from_i8.rs` - -```rust -use std::convert::TryFrom; - -use crate::variable_types::{StateValue, StateValueError}; - -// Implémentations TryFrom pour types numériques - -impl TryFrom<&StateValue> for i8 { - type Error = StateValueError; - - fn try_from(value: &StateValue) -> Result { - match value { - StateValue::I1(v) if *v >= 0 => Ok(*v as i8), - StateValue::I2(v) if *v <= i8::MAX as i16 && *v >= i8::MIN as i16 => Ok(*v as i8), - StateValue::I4(v) if *v <= i8::MAX as i32 && *v >= i8::MIN as i32 => Ok(*v as i8), - StateValue::Int(v) if *v <= i8::MAX as i32 && *v >= i8::MIN as i32 => Ok(*v as i8), - - StateValue::UI1(v) if *v <= i8::MAX as u8 => Ok(*v as i8), - StateValue::UI2(v) if *v <= i8::MAX as u16 => Ok(*v as i8), - StateValue::UI4(v) if *v <= i8::MAX as u32 => Ok(*v as i8), - StateValue::Boolean(v) => Ok(*v as Self), - - StateValue::String(s) => s - .parse::() - .map_err(|_| StateValueError::TypeError(format!("Cannot parse '{}' as i8", s))), - - _ => Err(StateValueError::TypeError("Cannot cast to i8".into())), - } - } -} - -impl TryFrom for i8 { - type Error = StateValueError; - - fn try_from(value: StateValue) -> Result { - i8::try_from(&value) - } -} - -impl From for StateValue { - fn from(value: i8) -> Self { - StateValue::I1(value) - } -} -``` - -## fichier: `pmoupnp/src/variable_types/cast.rs` - -```rust -use crate::variable_types::{StateValue, StateValueError, StateVarType}; -use std::convert::TryFrom; - -impl StateValue { - pub fn try_cast(&self, target: StateVarType) -> Result { - let source = StateVarType::from(self); - - // Identité (même type) - if source == target { - return Ok(self.clone()); - } - - match (self, target) { - (val, StateVarType::String) => Ok(StateValue::String(val.to_string())), - - (_, StateVarType::UI1) => Ok(StateValue::UI1(u8::try_from(self)?)), - (_, StateVarType::UI2) => Ok(StateValue::UI2(u16::try_from(self)?)), - (_, StateVarType::UI4) => Ok(StateValue::UI4(u32::try_from(self)?)), - (_, StateVarType::I1) => Ok(StateValue::I1(i8::try_from(self)?)), - (_, StateVarType::I2) => Ok(StateValue::I2(i16::try_from(self)?)), - (_, StateVarType::I4) => Ok(StateValue::I4(i32::try_from(self)?)), - (_, StateVarType::Int) => Ok(StateValue::Int(i32::try_from(self)?)), - - (_, StateVarType::R8) => Ok(StateValue::R8(f64::try_from(self)?)), - (_, StateVarType::Number) => Ok(StateValue::Number(f64::try_from(self)?)), - (_, StateVarType::Fixed14_4) => Ok(StateValue::Fixed14_4(f64::try_from(self)?)), - (_, StateVarType::R4) => Ok(StateValue::R4(f32::try_from(self)?)), - - // --- Pas encore implémenté pour les autres types --- - (val, target) => Err(StateValueError::TypeError(format!( - "Cannot cast {:?} to {:?}", - val, target - ))), - } - } -} -``` - -## fichier: `pmoupnp/src/variable_types/type_methods.rs` - -```rust -use crate::variable_types::{StateVarType, type_trait::UpnpVarType}; - -impl UpnpVarType for StateVarType { - fn as_state_var_type(&self) -> StateVarType { - *self - } - - fn bit_size(&self) -> Option { - match self { - StateVarType::UI1 | StateVarType::I1 => Some(8), - StateVarType::UI2 | StateVarType::I2 => Some(16), - StateVarType::UI4 | StateVarType::I4 | StateVarType::Int | StateVarType::R4 => Some(32), - StateVarType::R8 | StateVarType::Number | StateVarType::Fixed14_4 => Some(64), - _ => None, - } - } - - fn is_numeric(&self) -> bool { - matches!( - self, - StateVarType::UI1 - | StateVarType::UI2 - | StateVarType::UI4 - | StateVarType::I1 - | StateVarType::I2 - | StateVarType::I4 - | StateVarType::Int - | StateVarType::R4 - | StateVarType::R8 - | StateVarType::Number - | StateVarType::Fixed14_4 - ) - } - - fn is_integer(&self) -> bool { - matches!( - self, - StateVarType::UI1 - | StateVarType::UI2 - | StateVarType::UI4 - | StateVarType::I1 - | StateVarType::I2 - | StateVarType::I4 - | StateVarType::Int - ) - } - - fn is_signed_int(&self) -> bool { - matches!( - self, - StateVarType::I1 | StateVarType::I2 | StateVarType::I4 | StateVarType::Int - ) - } - - fn is_unsigned_int(&self) -> bool { - matches!( - self, - StateVarType::UI1 | StateVarType::UI2 | StateVarType::UI4 - ) - } - - fn is_float(&self) -> bool { - matches!( - self, - StateVarType::R4 | StateVarType::R8 | StateVarType::Number | StateVarType::Fixed14_4 - ) - } - - fn is_bool(&self) -> bool { - matches!(self, StateVarType::Boolean) - } - - fn is_string(&self) -> bool { - matches!( - self, - StateVarType::String - | StateVarType::Char - | StateVarType::BinHex - | StateVarType::BinBase64 - ) - } - - fn is_time(&self) -> bool { - matches!( - self, - StateVarType::Date - | StateVarType::DateTime - | StateVarType::DateTimeTZ - | StateVarType::Time - | StateVarType::TimeTZ - ) - } - - fn is_uuid(&self) -> bool { - matches!(self, StateVarType::UUID) - } - - fn is_uri(&self) -> bool { - matches!(self, StateVarType::URI) - } - - fn is_binary(&self) -> bool { - matches!(self, StateVarType::BinBase64 | StateVarType::BinHex) - } - - fn is_comparable(&self) -> bool { - !self.is_binary() - } -} -``` - -## fichier: `pmoupnp/src/variable_types/mod.rs` - -```rust -mod cast; -mod default_value; -mod display_type; -mod display_value; -mod errors; -mod fromstr; -mod type_methods; -mod type_trait; -mod value_methods; -mod value_trait; - -mod values_from_type; - -mod values_from_i16; -mod values_from_i32; -mod values_from_i64; -mod values_from_i8; -mod values_from_u16; -mod values_from_u32; -mod values_from_u8; - -mod values_from_f32; -mod values_from_f64; - -mod values_from_datetime; -mod values_from_naivedate; -mod values_from_naivedatetime; -mod values_from_naivetime; -mod values_from_uri; -mod values_from_uuid; -mod values_from_vec_u8; - -mod values_from_str; - -use std::fmt::Debug; - -use chrono::{DateTime, FixedOffset, NaiveDate, NaiveDateTime, NaiveTime}; -use url::Url; -use uuid::Uuid; - -pub use errors::StateValueError; -pub use type_trait::UpnpVarType; - -pub use crate::variable_types::value_trait::UpnpValue; - -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] -pub enum StateVarType { - UI1, // Unsigned 8-bit integer - UI2, // Unsigned 16-bit integer - UI4, // Unsigned 32-bit integer - I1, // Signed 8-bit integer - I2, // Signed 16-bit integer - I4, // Signed 32-bit integer - Int, // Synonymous with i4 - R4, // 32-bit floating point - R8, // 64-bit floating point - Number, // Synonymous with r8 - Fixed14_4, // Fixed-point decimal - Char, // Single Unicode character - String, // Character string - Boolean, // Boolean value - BinBase64, // Base64-encoded binary - BinHex, // Hex-encoded binary - Date, // Date (YYYY-MM-DD) - DateTime, // DateTime without timezone - DateTimeTZ, // DateTime with timezone - Time, // Time without timezone - TimeTZ, // Time with timezone - UUID, // Universally unique identifier - URI, // Uniform Resource Identifier -} - -#[derive(Clone, Debug)] -pub enum StateValue { - UI1(u8), - UI2(u16), - UI4(u32), - I1(i8), - I2(i16), - I4(i32), - Int(i32), - R4(f32), - R8(f64), - Number(f64), - Fixed14_4(f64), - Char(char), - String(String), - Boolean(bool), - BinBase64(String), - BinHex(String), - Date(NaiveDate), - DateTime(NaiveDateTime), - DateTimeTZ(DateTime), - Time(NaiveTime), - TimeTZ(DateTime), - UUID(Uuid), - URI(Url), -} -``` - -## fichier: `pmoupnp/src/variable_types/values_from_u8.rs` - -```rust -use std::convert::TryFrom; - -use crate::variable_types::{StateValue, StateValueError}; - -// Implémentations TryFrom pour types numériques - -impl TryFrom<&StateValue> for u8 { - type Error = StateValueError; - - fn try_from(value: &StateValue) -> Result { - match value { - StateValue::UI1(v) => Ok(*v), - StateValue::UI2(v) if *v <= u8::MAX as u16 => Ok(*v as u8), - StateValue::UI4(v) if *v <= i8::MAX as u32 => Ok(*v as u8), - StateValue::I1(v) if *v >= 0 => Ok(*v as u8), - StateValue::I2(v) if *v >= 0 && *v <= u8::MAX as i16 => Ok(*v as u8), - StateValue::I4(v) if *v >= 0 && *v <= u8::MAX as i32 => Ok(*v as u8), - StateValue::Int(v) if *v >= 0 && *v <= u8::MAX as i32 => Ok(*v as u8), - StateValue::Boolean(v) => Ok(*v as Self), - - StateValue::String(s) => s - .parse::() - .map_err(|_| StateValueError::TypeError(format!("Cannot parse '{}' as u8", s))), - - _ => Err(StateValueError::TypeError("Cannot cast to u8".into())), - } - } -} - -impl TryFrom for u8 { - type Error = StateValueError; - - fn try_from(value: StateValue) -> Result { - u8::try_from(&value) - } -} - -impl From for StateValue { - fn from(value: u8) -> Self { - StateValue::UI1(value) - } -} -``` - -## fichier: `pmoupnp/src/variable_types/errors.rs` - -```rust -use thiserror::Error; - -#[derive(Error, Debug)] -pub enum StateValueError { - #[error("Conversion error: {0}")] - ConversionError(String), - - #[error("Validation error: {0}")] - ValidationError(String), - - #[error("Range error: {0}")] - RangeError(String), - - #[error("Type error: {0}")] - TypeError(String), - - #[error("Parse error: {0}")] - ParseError(String), - - #[error("Event condition error: {0}")] - EventConditionError(String), - - #[error("Arithmetic error: {0}")] - ArithmeticError(String), - - #[error("Unknown error: {0}")] - Unknown(String), -} -``` - -## fichier: `pmoupnp/src/variable_types/display_type.rs` - -```rust -use std::fmt; - -use crate::variable_types::StateVarType; - -impl fmt::Display for StateVarType { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - let s = match self { - StateVarType::UI1 => "ui1", - StateVarType::UI2 => "ui2", - StateVarType::UI4 => "ui4", - StateVarType::I1 => "i1", - StateVarType::I2 => "i2", - StateVarType::I4 => "i4", - StateVarType::Int => "int", - StateVarType::R4 => "r4", - StateVarType::R8 => "r8", - StateVarType::Number => "number", - StateVarType::Fixed14_4 => "fixed.14.4", - StateVarType::Char => "char", - StateVarType::String => "string", - StateVarType::Boolean => "boolean", - StateVarType::BinBase64 => "bin.base64", - StateVarType::BinHex => "bin.hex", - StateVarType::Date => "date", - StateVarType::DateTime => "dateTime", - StateVarType::DateTimeTZ => "dateTime.tz", - StateVarType::Time => "time", - StateVarType::TimeTZ => "time.tz", - StateVarType::UUID => "uuid", - StateVarType::URI => "uri", - }; - write!(f, "{}", s) - } -} -``` - -## fichier: `pmoupnp/src/variable_types/values_from_uuid.rs` - -```rust -use std::convert::TryFrom; -use uuid::Uuid; - -use crate::variable_types::{StateValue, StateValueError}; - -impl TryFrom for Uuid { - type Error = StateValueError; - - fn try_from(value: StateValue) -> Result { - match value { - // Si déjà un URI encodé comme StateValue::URI - StateValue::UUID(v) => Ok(v), - - // Si c'est une String, on tente un parse - StateValue::String(v) => Uuid::parse_str(&v) - .map_err(|_| StateValueError::TypeError("Invalid UUID string".into())), - - // Autres types : erreur - _ => Err(StateValueError::TypeError("Cannot cast to Uuid".into())), - } - } -} - -impl From for StateValue { - fn from(value: Uuid) -> Self { - StateValue::UUID(value) - } -} -``` - -## fichier: `pmoupnp/src/variable_types/values_from_uri.rs` - -```rust -use std::convert::TryFrom; -use url::Url; - -use crate::variable_types::{StateValue, StateValueError}; - -impl TryFrom for Url { - type Error = StateValueError; - - fn try_from(value: StateValue) -> Result { - match value { - // Si déjà un URI encodé comme StateValue::URI - StateValue::URI(v) => Ok(v), - - // Si c'est une String, on tente un parse - StateValue::String(v) => { - Url::parse(&v).map_err(|_| StateValueError::TypeError("Invalid URI string".into())) - } - - // Autres types : erreur - _ => Err(StateValueError::TypeError("Cannot cast to Url".into())), - } - } -} - -impl From for StateValue { - fn from(value: Url) -> Self { - StateValue::URI(value) - } -} -``` - -## fichier: `pmoupnp/src/variable_types/value_trait.rs` - -```rust -pub trait UpnpValue: Clone {} -``` - -## fichier: `pmoupnp/src/variable_types/values_from_f64.rs` - -```rust -use std::convert::TryFrom; - -use crate::variable_types::{StateValue, StateValueError}; - -impl TryFrom<&StateValue> for f64 { - type Error = StateValueError; - - fn try_from(value: &StateValue) -> Result { - match value { - // --- Signed integers --- - StateValue::I1(v) => Ok(*v as f64), - StateValue::I2(v) => Ok(*v as f64), - StateValue::I4(v) => Ok(*v as f64), - StateValue::Int(v) => Ok(*v as f64), - - // --- Unsigned integers --- - StateValue::UI1(v) => Ok(*v as f64), - StateValue::UI2(v) => Ok(*v as f64), - StateValue::UI4(v) => Ok(*v as f64), - - // --- Floats --- - StateValue::R4(v) => Ok(*v as f64), - StateValue::R8(v) => Ok(*v), - StateValue::Number(v) => Ok(*v), - StateValue::Fixed14_4(v) => Ok(*v), - - StateValue::Boolean(v) => Ok((*v as i32) as Self), - - StateValue::String(s) => s - .parse::() - .map_err(|_| StateValueError::TypeError(format!("Cannot parse '{}' as f64", s))), - - // --- Par défaut : erreur --- - _ => Err(StateValueError::TypeError("Cannot cast to f64".into())), - } - } -} - -impl TryFrom for f64 { - type Error = StateValueError; - - fn try_from(value: StateValue) -> Result { - f64::try_from(&value) - } -} - -impl From for StateValue { - fn from(value: f64) -> Self { - StateValue::R8(value) - } -} -``` - -## fichier: `pmoupnp/src/variable_types/values_from_naivedatetime.rs` - -```rust -use crate::variable_types::{StateValue, StateValueError}; -use chrono::NaiveDateTime; -use std::convert::TryFrom; - -impl TryFrom<&StateValue> for NaiveDateTime { - type Error = StateValueError; - - fn try_from(value: &StateValue) -> Result { - match value { - StateValue::DateTime(v) => Ok(v.clone()), - StateValue::String(v) => NaiveDateTime::parse_from_str(&v, "%Y-%m-%dT%H:%M:%S") - .map_err(|e| { - StateValueError::ParseError(format!( - "Cannot parse DateTime from string '{}': {}", - v, e - )) - }), - _ => Err(StateValueError::TypeError( - "Cannot cast to NaiveDateTime".into(), - )), - } - } -} - -impl TryFrom for NaiveDateTime { - type Error = StateValueError; - - fn try_from(value: StateValue) -> Result { - NaiveDateTime::try_from(&value) - } -} - -impl From for StateValue { - fn from(value: NaiveDateTime) -> Self { - StateValue::DateTime(value) - } -} -``` - -## fichier: `pmoupnp/src/variable_types/values_from_i32.rs` - -```rust -use std::convert::TryFrom; - -use crate::variable_types::{StateValue, StateValueError}; - -impl TryFrom<&StateValue> for i32 { - type Error = StateValueError; - - fn try_from(value: &StateValue) -> Result { - match value { - // signés - StateValue::I1(v) => Ok(*v as i32), - StateValue::I2(v) => Ok(*v as i32), - StateValue::I4(v) => Ok(*v), - StateValue::Int(v) => Ok(*v), - - // non signés - StateValue::UI1(v) => Ok(*v as i32), - StateValue::UI2(v) => Ok(*v as i32), - StateValue::UI4(v) if *v <= i32::MAX as u32 => Ok(*v as i32), - StateValue::Boolean(v) => Ok(*v as Self), - - StateValue::String(s) => s - .parse::() - .map_err(|_| StateValueError::TypeError(format!("Cannot parse '{}' as i32", s))), - - _ => Err(StateValueError::TypeError("Cannot cast to i32".into())), - } - } -} - -impl TryFrom for i32 { - type Error = StateValueError; - - fn try_from(value: StateValue) -> Result { - i32::try_from(&value) - } -} - -impl From for StateValue { - fn from(value: i32) -> Self { - StateValue::I4(value) - } -} -``` - -## fichier: `pmoupnp/src/variable_types/values_from_naivetime.rs` - -```rust -use crate::variable_types::{StateValue, StateValueError}; -use chrono::NaiveTime; -use std::convert::TryFrom; - -impl TryFrom<&StateValue> for NaiveTime { - type Error = StateValueError; - - fn try_from(value: &StateValue) -> Result { - match value { - StateValue::Time(v) => Ok(v.clone()), - StateValue::String(v) => NaiveTime::parse_from_str(&v, "%H:%M:%S").map_err(|e| { - StateValueError::ParseError(format!("Cannot parse Time from string '{}': {}", v, e)) - }), - _ => Err(StateValueError::TypeError( - "Cannot cast to NaiveTime".into(), - )), - } - } -} - -impl TryFrom for NaiveTime { - type Error = StateValueError; - - fn try_from(value: StateValue) -> Result { - NaiveTime::try_from(&value) - } -} - -impl From for StateValue { - fn from(value: NaiveTime) -> Self { - StateValue::Time(value) - } -} -``` - -## fichier: `pmoupnp/src/variable_types/values_from_i16.rs` - -```rust -use std::convert::TryFrom; - -use crate::variable_types::{StateValue, StateValueError}; - -// Implémentations TryFrom pour types numériques - -impl TryFrom<&StateValue> for i16 { - type Error = StateValueError; - - fn try_from(value: &StateValue) -> Result { - match value { - StateValue::I1(v) => Ok(*v as i16), - StateValue::I2(v) => Ok(*v), - StateValue::I4(v) if *v <= i16::MAX as i32 && *v >= i16::MIN as i32 => Ok(*v as i16), - StateValue::Int(v) if *v <= i16::MAX as i32 && *v >= i16::MIN as i32 => Ok(*v as i16), - - StateValue::UI1(v) => Ok(*v as i16), - StateValue::UI2(v) if *v <= i16::MAX as u16 => Ok(*v as i16), - StateValue::UI4(v) if *v <= i16::MAX as u32 => Ok(*v as i16), - StateValue::Boolean(v) => Ok(*v as Self), - - StateValue::String(s) => s - .parse::() - .map_err(|_| StateValueError::TypeError(format!("Cannot parse '{}' as i16", s))), - - _ => Err(StateValueError::TypeError("Cannot cast to i32".into())), - } - } -} - -impl TryFrom for i16 { - type Error = StateValueError; - - fn try_from(value: StateValue) -> Result { - i16::try_from(&value) - } -} - -impl From for StateValue { - fn from(value: i16) -> Self { - StateValue::I2(value) - } -} -``` - -## fichier: `pmoupnp/src/variable_types/type_trait.rs` - -```rust -use crate::variable_types::StateVarType; - -pub trait UpnpVarType { - fn as_state_var_type(&self) -> StateVarType; - - fn bit_size(&self) -> Option { - self.as_state_var_type().bit_size() - } - - fn is_numeric(&self) -> bool { - self.as_state_var_type().is_numeric() - } - - fn is_integer(&self) -> bool { - self.as_state_var_type().is_integer() - } - - fn is_signed_int(&self) -> bool { - self.as_state_var_type().is_signed_int() - } - - fn is_unsigned_int(&self) -> bool { - self.as_state_var_type().is_unsigned_int() - } - - fn is_float(&self) -> bool { - self.as_state_var_type().is_float() - } - - fn is_bool(&self) -> bool { - self.as_state_var_type().is_bool() - } - - fn is_string(&self) -> bool { - self.as_state_var_type().is_string() - } - - fn is_time(&self) -> bool { - self.as_state_var_type().is_time() - } - - fn is_uuid(&self) -> bool { - self.as_state_var_type().is_uuid() - } - - fn is_uri(&self) -> bool { - self.as_state_var_type().is_uri() - } - - fn is_binary(&self) -> bool { - self.as_state_var_type().is_binary() - } - - fn is_comparable(&self) -> bool { - self.as_state_var_type().is_comparable() - } -} -``` - -## fichier: `pmoupnp/src/object_trait.rs` - -```rust -//! ## Hiérarchie des traits -//! -//! ```text -//! Clone + Debug -//! └─> UpnpObject (trait de base) -//! ├─> UpnpModel (modèles créant des instances) -//! ├─> UpnpInstance (instances concrètes) -//! ├─> UpnpTyped (objets avec nom et type) -//! │ └─> UpnpTypedObject = UpnpObject + UpnpTyped -//! │ └─> UpnpTypedInstance = UpnpTypedObject + UpnpInstance -//! └─> UpnpSet (collections) + UpnpDeepClone -//! ├─> UpnpModelSet = UpnpSet + UpnpModel -//! └─> UpnInstanceSet = UpnpSet + UpnpInstance -//! -//! UpnpDeepClone (indépendant) -//! ``` -//! -//! ## Description des traits -//! -//! - **Traits de base** : -//! - [`UpnpObject`] : Trait principal avec sérialisation XML/Markdown -//! - [`UpnpDeepClone`] : Clonage profond (indépendant de la hiérarchie) -//! -//! - **Traits de spécialisation niveau 1** : -//! - [`UpnpModel`] : Modèle pouvant créer des instances -//! - [`UpnpInstance`] : Instance concrète créée depuis un modèle -//! - [`UpnpTyped`] : Ajoute les informations de type et nom -//! - [`UpnpSet`] : Marque un objet comme collection -//! -//! - **Traits combinés niveau 2** : -//! - [`UpnpTypedObject`] : Objet typé (marker trait) -//! -//! - **Traits combinés niveau 3** : -//! - [`UpnpTypedInstance`] : Instance typée (marker trait) -//! - [`UpnpModelSet`] : Collection de modèles (marker trait) -//! - [`UpnInstanceSet`] : Collection d'instances (marker trait) - -use std::{fmt::Debug, sync::Arc}; - -use xmltree::{Element, EmitterConfig}; - -use crate::UpnpObjectType; - -/// Trait pour le clonage profond d'objets UPnP. -/// -/// Contrairement au trait standard [`Clone`] qui peut effectuer un clonage superficiel -/// (partage via `Arc`), ce trait garantit un clonage complet et indépendant de l'objet. -/// -/// # Note -/// -/// Ce trait est indépendant de la hiérarchie [`UpnpObject`] et peut être implémenté -/// séparément. -pub trait UpnpDeepClone { - /// Crée un clone profond de l'objet. - /// - /// Tous les éléments internes sont clonés, créant un objet complètement indépendant. - fn deep_clone(&self) -> Self; -} - -/// Trait de base pour tous les objets UPnP. -/// -/// Ce trait fournit les fonctionnalités communes à tous les objets UPnP : -/// - Sérialisation XML -/// - Conversion en Markdown -/// - Identification du type d'objet (instance ou set) -/// -/// # Traits requis -/// -/// - [`Clone`] : Pour pouvoir dupliquer les objets -/// - [`Debug`] : Pour le débogage -/// -/// # Hiérarchie -/// -/// Ce trait est à la base de toute la hiérarchie UPnP. Voir la documentation du module -/// pour le graphe complet. -pub trait UpnpObject: Clone + Debug { - /// Convertit l'objet en élément XML. - /// - /// # Returns - /// - /// Un [`Element`] xmltree représentant l'objet. - fn to_xml_element(&self) -> Element; - - /// Convertit l'objet en chaîne XML formatée. - /// - /// Génère une représentation XML complète avec en-tête et indentation. - /// - /// # Returns - /// - /// Une chaîne XML formatée avec : - /// - En-tête `` - /// - Indentation de 2 espaces - /// - /// # Examples - /// - /// ```ignore - /// let xml = my_object.to_xml(); - /// println!("{}", xml); - /// // - /// // - /// // value - /// // - /// ``` - fn to_xml(&self) -> String { - let elem = self.to_xml_element(); - - let config = EmitterConfig::new() - .perform_indent(true) - .indent_string(" "); - - let mut buf = Vec::new(); - elem.write_with_config(&mut buf, config) - .expect("Failed to write XML"); - - let mut xml_string = "\n".to_string(); - xml_string.push_str(&String::from_utf8(buf).expect("Invalid UTF-8")); - - xml_string - } - - /// Convertit l'objet en représentation Markdown. - /// - /// Génère une vue hiérarchique de la structure XML en format Markdown, - /// avec détection automatique des URLs et images. - /// - /// # Fonctionnalités - /// - /// - Les URLs sont converties en liens cliquables - /// - Les URLs d'images sont affichées comme images - /// - Les attributs sont formatés comme `key=value` - /// - Structure hiérarchique avec indentation - /// - /// # Returns - /// - /// Une chaîne Markdown formatée. - /// - /// # Examples - /// - /// ```ignore - /// let md = my_object.to_markdown(); - /// println!("{}", md); - /// // # UPnP XML (Markdown view) - /// // - /// // - **element** - /// // - **child**: `value` - /// ``` - fn to_markdown(&self) -> String { - let elem = self.to_xml_element(); - let mut md = String::new(); - - fn is_url(s: &str) -> bool { - s.starts_with("http://") || s.starts_with("https://") || s.starts_with("urn:") - } - - fn is_image_url(s: &str) -> bool { - let s = s.to_lowercase(); - s.ends_with(".png") - || s.ends_with(".jpg") - || s.ends_with(".jpeg") - || s.ends_with(".gif") - || s.ends_with(".svg") - || s.ends_with(".webp") - } - - fn format_value(v: &str) -> String { - let v = v.trim().to_string(); - if is_url(&v) { - if is_image_url(&v) { - format!("[{}]({})
![]({})", v, v, v) - } else { - format!("[{}]({})", v, v) - } - } else { - format!("`{}`", v) - } - } - - fn recurse(elem: &xmltree::Element, md: &mut String, depth: usize) { - let indent = " ".repeat(depth); - md.push_str(&format!("{}- **{}**", indent, elem.name)); - - if !elem.attributes.is_empty() { - let attrs: Vec = elem - .attributes - .iter() - .map(|(k, v)| format!("{}={}", k, format_value(v))) - .collect(); - md.push_str(&format!(" ({})", attrs.join(", "))); - } - - if let Some(text) = elem - .get_text() - .map(|s| s.trim().to_string()) - .filter(|s| !s.is_empty()) - { - md.push_str(&format!(": {}", format_value(&text))); - } - - md.push('\n'); - - for child in &elem.children { - if let xmltree::XMLNode::Element(child_elem) = child { - recurse(child_elem, md, depth + 1); - } - } - } - - md.push_str("# UPnP XML (Markdown view)\n\n"); - recurse(&elem, &mut md, 0); - md - } - - /// Indique si l'objet est une instance. - /// - /// # Returns - /// - /// `false` par défaut. Surchargé par [`UpnpInstance`] pour retourner `true`. - fn is_instance(&self) -> bool { - false - } - - /// Indique si l'objet est une collection (set). - /// - /// # Returns - /// - /// `false` par défaut. Surchargé par [`UpnpSet`] pour retourner `true`. - fn is_set(&self) -> bool { - false - } -} - -/// Trait pour les modèles UPnP qui peuvent créer des instances. -/// -/// Un modèle représente la définition ou template d'un objet UPnP, tandis qu'une -/// instance est une occurrence concrète de cet objet. -/// -/// # Type associé -/// -/// - [`Instance`](Self::Instance) : Le type d'instance créée par ce modèle -/// -/// # Méthodes -/// -/// - [`create_instance`](Self::create_instance) : Crée une nouvelle instance -/// -/// # Hiérarchie -/// -/// ```text -/// UpnpObject -/// └─> UpnpModel -/// ``` -/// -/// # Relation avec UpnpInstance -/// -/// `UpnpModel` et [`UpnpInstance`] sont liés via leurs types associés : -/// - Le modèle spécifie quel type d'instance il crée -/// - L'instance spécifie de quel type de modèle elle provient -/// -/// # Examples -/// -/// ```ignore -/// struct DeviceModel { /* ... */ } -/// struct DeviceInstance { /* ... */ } -/// -/// impl UpnpModel for DeviceModel { -/// type Instance = DeviceInstance; -/// } -/// -/// impl UpnpInstance for DeviceInstance { -/// type Model = DeviceModel; -/// -/// fn new(model: &DeviceModel) -> Self { -/// // Création de l'instance depuis le modèle -/// } -/// } -/// -/// // Utilisation -/// let model = DeviceModel::new(); -/// let instance = model.create_instance(); // Arc -/// ``` -pub trait UpnpModel: UpnpObject { - /// Le type d'instance créée par ce modèle. - type Instance: UpnpInstance; - - /// Crée une nouvelle instance à partir de ce modèle. - /// - /// # Returns - /// - /// Un `Arc` contenant la nouvelle instance créée. - /// - /// # Implémentation par défaut - /// - /// Par défaut, appelle [`UpnpInstance::new`] avec une référence vers ce modèle - /// et encapsule le résultat dans un `Arc`. - fn create_instance(&self) -> Arc { - Arc::new(Self::Instance::new(self)) - } -} - -/// Trait pour les instances UPnP concrètes. -/// -/// Une instance représente une occurrence concrète d'un objet UPnP, créée à partir -/// d'un modèle ([`UpnpModel`]). -/// -/// # Type associé -/// -/// - [`Model`](Self::Model) : Le type du modèle dont cette instance dérive -/// -/// # Méthodes requises -/// -/// - [`new`](Self::new) : Constructeur créant l'instance depuis un modèle -/// -/// # Hiérarchie -/// -/// ```text -/// UpnpObject -/// └─> UpnpInstance -/// ``` -/// -/// # Relation avec UpnpModel -/// -/// Voir la documentation de [`UpnpModel`] pour comprendre la relation entre -/// modèles et instances. -pub trait UpnpInstance: UpnpObject { - /// Le type du modèle dont cette instance est dérivée. - type Model: UpnpModel; - - /// Crée une nouvelle instance à partir d'un modèle. - /// - /// # Arguments - /// - /// * `model` - Référence vers le modèle à partir duquel créer l'instance - /// - /// # Returns - /// - /// Une nouvelle instance initialisée depuis le modèle. - fn new(model: &Self::Model) -> Self; - - /// Indique que cet objet est une instance. - /// - /// # Returns - /// - /// Toujours `true` pour les instances. - fn is_instance(&self) -> bool { - true - } -} - -/// Trait pour les objets UPnP typés. -/// -/// Ajoute les informations de type et de nom aux objets UPnP. -/// -/// # Méthodes requises -/// -/// - [`as_upnp_object_type`](Self::as_upnp_object_type) : Accès au type de l'objet -/// -/// # Méthodes fournies -/// -/// - [`get_name`](Self::get_name) : Récupère le nom de l'objet -/// - [`get_object_type`](Self::get_object_type) : Récupère le type de l'objet -/// -/// # Hiérarchie -/// -/// ```text -/// UpnpObject -/// └─> UpnpTyped -/// ``` -pub trait UpnpTyped: UpnpObject { - /// Retourne une référence vers le type de l'objet. - fn as_upnp_object_type(&self) -> &UpnpObjectType; - - /// Retourne le nom de l'objet. - /// - /// # Returns - /// - /// Une référence vers le nom de l'objet. - fn get_name(&self) -> &String { - &self.as_upnp_object_type().name - } - - /// Retourne le type de l'objet sous forme de chaîne. - /// - /// # Returns - /// - /// Une référence vers le type de l'objet (ex: "Device", "Service", etc.). - fn get_object_type(&self) -> &String { - &self.as_upnp_object_type().object_type - } -} - -/// Trait marqueur pour les objets UPnP typés. -/// -/// Combine [`UpnpObject`] et [`UpnpTyped`] pour créer un objet avec toutes -/// les fonctionnalités de base plus les informations de type. -/// -/// # Hiérarchie -/// -/// ```text -/// UpnpObject + UpnpTyped -/// └─> UpnpTypedObject -/// ``` -/// -/// # Note -/// -/// C'est un *marker trait* (trait marqueur) sans méthodes supplémentaires. -pub trait UpnpTypedObject: UpnpObject + UpnpTyped {} - -/// Trait marqueur pour les instances typées UPnP. -/// -/// Combine [`UpnpTypedObject`] et [`UpnpInstance`] pour représenter une instance -/// concrète d'un objet typé avec toutes les fonctionnalités : -/// - Sérialisation XML/Markdown (de [`UpnpObject`]) -/// - Informations de type et nom (de [`UpnpTyped`]) -/// - Relation avec un modèle (de [`UpnpInstance`]) -/// -/// # Hiérarchie -/// -/// ```text -/// UpnpTypedObject + UpnpInstance -/// └─> UpnpTypedInstance -/// ``` -/// -/// # Note -/// -/// Ce trait ajoute la méthode [`get_model`](Self::get_model) pour accéder -/// au modèle de l'instance. Les collections d'instances ([`UpnInstanceSet`]) -/// n'ont pas cette méthode car elles contiennent plusieurs instances. -pub trait UpnpTypedInstance: UpnpTypedObject + UpnpInstance -where - Self::Model: UpnpModel -{ - /// Retourne une référence vers le modèle dont cette instance est dérivée. - /// - /// Permet d'accéder aux métadonnées et contraintes définies dans le modèle, - /// telles que les plages de valeurs autorisées, les types, les descriptions, etc. - /// - /// # Returns - /// - /// Une référence immuable vers le modèle. - /// - /// # Examples - /// - /// ```ignore - /// let instance = model.create_instance(); - /// - /// // Accéder aux propriétés du modèle depuis l'instance - /// let model_ref = instance.get_model(); - /// println!("Instance du modèle: {}", model_ref.get_name()); - /// - /// // Vérifier les contraintes définies dans le modèle - /// if let Some(range) = model_ref.get_range() { - /// println!("Plage autorisée: {:?}", range); - /// } - /// ``` - /// - /// # Use cases - /// - /// Cette méthode est particulièrement utile pour : - /// - Valider des valeurs contre les contraintes du modèle - /// - Accéder aux métadonnées sans dupliquer les informations - /// - Afficher des informations de type ou de description - /// - Implémenter des logiques conditionnelles basées sur le modèle - /// - /// # Différence avec les traits spécifiques - /// - /// Pour les variables d'état, le trait [`UpnpVariable`](crate::state_variables::UpnpVariable) - /// fournit également `get_definition()` qui est sémantiquement équivalent - /// mais spécifique au domaine des variables. - fn get_model(&self) -> &Self::Model; -} - - -/// Trait marqueur pour les collections UPnP. -/// -/// Représente un ensemble (set) d'objets UPnP. -/// -/// # Super-traits requis -/// -/// - [`UpnpObject`] : Fonctionnalités de base (XML, etc.) -/// - [`UpnpDeepClone`] : Permet le clonage profond des collections -/// -/// # Implémentation -/// -/// Ce trait surcharge [`UpnpObject::is_set`] pour retourner `true`. -/// -/// # Hiérarchie -/// -/// ```text -/// UpnpObject + UpnpDeepClone -/// └─> UpnpSet -/// ``` -/// -/// # Note sur le clonage -/// -/// Les collections UPnP contiennent généralement des `Arc` vers leurs éléments. -/// Le trait [`Clone`] (via `UpnpObject`) effectue un clonage shallow des `Arc`, -/// tandis que [`UpnpDeepClone`] clone profondément les éléments contenus. -/// -/// # Examples -/// -/// ```ignore -/// struct ServiceSet { -/// services: HashMap>, -/// } -/// -/// impl Clone for ServiceSet { -/// fn clone(&self) -> Self { -/// // Clone shallow : partage les Services via Arc -/// Self { -/// services: self.services.clone() -/// } -/// } -/// } -/// -/// impl UpnpDeepClone for ServiceSet { -/// fn deep_clone(&self) -> Self { -/// // Clone profond : crée de nouveaux Services -/// let deep_services = self.services -/// .iter() -/// .map(|(k, v)| (k.clone(), Arc::new((**v).clone()))) -/// .collect(); -/// -/// Self { -/// services: deep_services -/// } -/// } -/// } -/// ``` -pub trait UpnpSet: UpnpObject + UpnpDeepClone { - /// Indique que cet objet est une collection. - /// - /// # Returns - /// - /// Toujours `true` pour les collections. - fn is_set(&self) -> bool { - true - } -} - -/// Trait marqueur pour les collections de modèles UPnP. -/// -/// Combine [`UpnpSet`] et [`UpnpModel`] pour représenter une collection -/// de modèles qui peut elle-même créer une collection d'instances. -/// -/// # Cas d'usage -/// -/// Ce trait est utilisé quand une collection de modèles doit pouvoir instancier -/// une collection d'instances correspondante. Par exemple : -/// - Un ensemble de modèles de services d'un device qui crée un ensemble d'instances de services -/// - Une liste de modèles d'actions qui instancie une liste d'actions actives -/// - Une collection de modèles de variables d'état qui génère une collection d'instances -/// -/// # Hiérarchie -/// -/// ```text -/// UpnpSet + UpnpModel -/// └─> UpnpModelSet -/// ``` -/// -/// # Relation avec d'autres traits -/// -/// - [`UpnpSet`] : Fournit les fonctionnalités de collection -/// - [`UpnpModel`] : Fournit la capacité de créer des instances -/// - [`UpnInstanceSet`] : Représente les collections d'instances (contrepartie) -/// -/// # Note -/// -/// C'est un *marker trait* (trait marqueur) sans méthodes supplémentaires. -/// Il est automatiquement implémenté pour tous les types éligibles via une -/// blanket implementation. -/// -/// # Examples -/// -/// ```ignore -/// /// Collection de modèles de services -/// struct ServiceSetModel { -/// services: Vec>, -/// } -/// -/// /// Collection d'instances de services -/// struct ServiceSetInstance { -/// model: Arc, -/// service_instances: Vec>, -/// } -/// -/// impl UpnpObject for ServiceSetModel { /* ... */ } -/// impl UpnpSet for ServiceSetModel {} -/// -/// impl UpnpModel for ServiceSetModel { -/// type Instance = ServiceSetInstance; -/// -/// fn create_instance(&self) -> Arc { -/// // Créer des instances pour chaque service -/// let instances = self.services -/// .iter() -/// .map(|model| model.create_instance()) -/// .collect(); -/// -/// Arc::new(ServiceSetInstance { -/// model: Arc::new(self.clone()), -/// service_instances: instances, -/// }) -/// } -/// } -/// -/// // UpnpModelSet est automatiquement implémenté ! -/// -/// // Utilisation -/// let model_set = ServiceSetModel::new(); -/// let instance_set = model_set.create_instance(); // Crée toutes les instances -/// ``` -pub trait UpnpModelSet: UpnpSet + UpnpModel {} - - -/// Trait marqueur pour les collections d'instances UPnP. -/// -/// Combine [`UpnpSet`] et [`UpnpInstance`] pour représenter une collection -/// d'instances UPnP. Cela permet d'avoir des collections qui sont elles-mêmes -/// des instances créées depuis un modèle. -/// -/// # Hiérarchie -/// -/// ```text -/// UpnpSet + UpnpInstance -/// └─> UpnInstanceSet -/// ``` -/// -/// # Note -/// -/// C'est un *marker trait* (trait marqueur) sans méthodes supplémentaires. -pub trait UpnInstanceSet: UpnpSet + UpnpInstance {} - - -/// Implémentation automatique de [`UpnInstanceSet`] pour tous les types éligibles. -/// -/// Cette *blanket implementation* fournit automatiquement le trait [`UpnInstanceSet`] -/// à tout type `T` qui implémente à la fois [`UpnpSet`] et [`UpnpInstance`]. -/// -/// # Contraintes -/// -/// - `T` doit implémenter [`UpnpSet`] (collection d'objets UPnP) -/// - `T` doit implémenter [`UpnpInstance`] (instance créée depuis un modèle) -/// -/// # Pourquoi cette implémentation existe -/// -/// Certaines collections UPnP sont elles-mêmes des instances (par exemple, une -/// collection de services pour un device spécifique). Ce trait marker permet -/// d'identifier ces collections qui combinent les deux aspects. La blanket -/// implementation évite d'avoir à l'implémenter manuellement pour chaque type. -/// -/// # Utilisation -/// -/// ```ignore -/// struct ServiceSetInstance { -/// model: Arc, -/// services: Vec>, -/// } -/// -/// impl UpnpObject for ServiceSetInstance { /* ... */ } -/// impl UpnpSet for ServiceSetInstance {} -/// impl UpnpInstance for ServiceSetInstance { -/// type Model = ServiceSetModel; -/// fn new(model: &ServiceSetModel) -> Self { /* ... */ } -/// } -/// -/// // UpnInstanceSet est automatiquement implémenté ! -/// -/// fn process_instance_set(set: &T) { -/// if set.is_set() && set.is_instance() { -/// println!("C'est une collection ET une instance"); -/// } -/// } -/// ``` -impl UpnInstanceSet for T -where - T: UpnpSet + UpnpInstance -{} - -/// Implémentation automatique de [`UpnpTypedObject`] pour tous les types éligibles. -/// -/// Cette *blanket implementation* fournit automatiquement le trait [`UpnpTypedObject`] -/// à tout type `T` qui implémente à la fois [`UpnpObject`] et [`UpnpTyped`]. -/// -/// # Contraintes -/// -/// - `T` doit implémenter [`UpnpObject`] (fonctionnalités de base UPnP) -/// - `T` doit implémenter [`UpnpTyped`] (informations de type et nom) -/// -/// # Utilisation -/// -/// ```ignore -/// struct Device { -/// object_type: UpnpObjectType, -/// } -/// -/// impl UpnpObject for Device { /* ... */ } -/// impl UpnpTyped for Device { /* ... */ } -/// -/// // UpnpTypedObject est automatiquement implémenté ! -/// fn process(obj: &T) { -/// println!("{}", obj.get_name()); -/// } -/// ``` -impl UpnpTypedObject for T -where - T: UpnpObject + UpnpTyped -{} - - -/// Implémentation automatique de [`UpnpModelSet`] pour tous les types éligibles. -/// -/// Cette *blanket implementation* fournit automatiquement le trait [`UpnpModelSet`] -/// à tout type `T` qui implémente à la fois [`UpnpSet`] et [`UpnpModel`]. -/// -/// # Contraintes -/// -/// - `T` doit implémenter [`UpnpSet`] (collection d'objets UPnP) -/// - `T` doit implémenter [`UpnpModel`] (peut créer des instances) -/// -/// # Pourquoi cette implémentation existe -/// -/// [`UpnpModelSet`] est un *marker trait* qui identifie les collections pouvant -/// créer des collections d'instances. Plutôt que de demander aux développeurs -/// d'écrire manuellement `impl UpnpModelSet for MyType {}`, cette blanket -/// implementation le fait automatiquement dès que les traits requis sont implémentés. -/// -/// # Fonctionnement -/// -/// Lorsque vous définissez une collection de modèles : -/// -/// ```ignore -/// struct ActionSetModel { -/// actions: Vec>, -/// } -/// -/// impl UpnpObject for ActionSetModel { /* ... */ } -/// impl UpnpSet for ActionSetModel {} -/// -/// impl UpnpModel for ActionSetModel { -/// type Instance = ActionSetInstance; -/// fn create_instance(&self) -> Arc { /* ... */ } -/// } -/// ``` -/// -/// Le compilateur Rust vérifie automatiquement que `ActionSetModel` satisfait -/// toutes les contraintes (implémente `UpnpSet` ET `UpnpModel`) et applique -/// donc `UpnpModelSet` sans code supplémentaire. -/// -/// # Utilisation dans des signatures génériques -/// -/// ```ignore -/// fn process_model_set(set: &T) { -/// println!("Processing model set that can create instances"); -/// let instance = set.create_instance(); -/// // ... -/// } -/// ``` -/// -/// # Différence avec UpnInstanceSet -/// -/// - [`UpnpModelSet`] : Collection de **modèles** (peut créer des instances) -/// - [`UpnInstanceSet`] : Collection d'**instances** (créée depuis un modèle) -impl UpnpModelSet for T -where - T: UpnpSet + UpnpModel -{} - -``` - -## fichier: `pmoupnp/src/services/service_instance.rs` - -```rust -//! Implémentation de ServiceInstance. - -use std::{ - collections::HashMap, - sync::{Arc, Mutex, RwLock}, - time::Duration, -}; -use axum::{ - extract::{Request, State}, - http::{HeaderMap, StatusCode}, - response::{IntoResponse, Response}, - body::Body, -}; -use tokio::time; -use tracing::{info, warn, error}; -use xmltree::{Element, XMLNode, EmitterConfig}; - -use crate::{ - services::{Service, ServiceError}, - actions::{ActionInstance, ActionInstanceSet}, - state_variables::{StateVarInstance, StateVarInstanceSet, UpnpVariable}, - UpnpObject, UpnpInstance, UpnpTyped, UpnpTypedInstance, UpnpObjectType, -}; - -/// Méthodes HTTP pour les événements UPnP. -pub const METHOD_SUBSCRIBE: &str = "SUBSCRIBE"; -pub const METHOD_UNSUBSCRIBE: &str = "UNSUBSCRIBE"; - -/// Instance de service UPnP. -/// -/// Représente une instance concrète d'un service UPnP, attachée à un device. -/// Gère l'exécution des actions, les notifications d'événements et les abonnements. -/// -/// # Fonctionnalités -/// -/// - Exécution d'actions via SOAP -/// - Gestion des abonnements aux événements (SUBSCRIBE/UNSUBSCRIBE) -/// - Notifications automatiques des changements d'état -/// - Génération de la description SCPD -/// -/// # Cycle de vie -/// -/// 1. Création via [`Service::create_instance`](crate::UpnpModel::create_instance) -/// 2. Enregistrement des URLs avec [`register_urls`](Self::register_urls) -/// 3. Démarrage du notifier avec [`start_notifier`](Self::start_notifier) -/// -/// # Examples -/// -/// ```rust,no_run -/// # use pmoupnp::services::Service; -/// # use pmoupnp::server::Server; -/// # use std::time::Duration; -/// # #[tokio::main] -/// # async fn main() { -/// let service = Service::new("AVTransport".to_string()); -/// let instance = service.create_instance(); -/// -/// // Enregistrer les endpoints -/// let mut server = Server::new("test", "http://localhost:8080", 8080); -/// instance.register_urls(&mut server).await.unwrap(); -/// -/// // Démarrer les notifications -/// let _handle = instance.start_notifier(Duration::from_secs(5)); -/// # } -/// ``` -#[derive(Clone)] -pub struct ServiceInstance { - /// Métadonnées de l'objet - object: UpnpObjectType, - - /// Référence vers le modèle - model: Arc, - - /// Identifiant du service - identifier: String, - - /// Device parent (optionnel) - device: Option>, - - /// Variables d'état instanciées - statevariables: StateVarInstanceSet, - - /// Actions instanciées - actions: ActionInstanceSet, - - /// Abonnés aux événements (SID -> Callback URL) - subscribers: Arc>>, - - /// Buffer des changements en attente de notification - changed_buffer: Arc>>, - - /// Compteurs de séquence par abonné - seqid: Arc>>, -} - -// Stub temporaire pour DeviceInstance -// TODO: Remplacer par la vraie implémentation quand le module devices sera créé -#[derive(Debug, Clone)] -pub struct DeviceStub { - name: String, - udn: String, -} - -impl DeviceStub { - pub fn name(&self) -> &str { - &self.name - } - - pub fn base_route(&self) -> String { - format!("/device/{}", self.name) - } - - pub fn udn(&self) -> &str { - &self.udn - } - - pub fn server_base_url(&self) -> String { - "http://localhost:8080".to_string() - } -} - -impl std::fmt::Debug for ServiceInstance { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.debug_struct("ServiceInstance") - .field("object", &self.object) - .field("identifier", &self.identifier) - .field("device", &self.device) - .field("statevariables", &self.statevariables) - .field("actions", &self.actions) - .finish() - } -} - -impl UpnpTyped for ServiceInstance { - fn as_upnp_object_type(&self) -> &UpnpObjectType { - &self.object - } -} - -impl UpnpInstance for ServiceInstance { - type Model = Service; - - fn new(model: &Service) -> Self { - // Phase 1 : Créer les instances de variables d'état - let mut statevariables = StateVarInstanceSet::new(); - for v in model.variables() { - if let Err(e) = statevariables.insert(Arc::new(StateVarInstance::new(&*v))) { - error!("Failed to insert state variable: {:?}", e); - } - } - - // Phase 2 : Créer les instances d'actions avec validation - let mut actions = ActionInstanceSet::new(); - for a in model.actions() { - // Vérifier que toutes les variables référencées existent - let mut missing_vars = Vec::new(); - - for arg in a.arguments().all() { - let related_var_name = arg.state_variable().get_name(); - if statevariables.get_by_name(related_var_name).is_none() { - missing_vars.push(related_var_name.to_string()); - } - } - - if !missing_vars.is_empty() { - error!( - "Action '{}' references missing state variables: {:?}", - a.get_name(), - missing_vars - ); - continue; - } - - // Créer l'instance d'action - let action_instance = Arc::new(ActionInstance::new(&*a)); - - // Phase 3 : Lier les arguments aux instances de variables - // Note : Nécessite que ArgumentInstance ait une méthode bind_variable() - // et que variable_instance soit dans un RwLock pour modification après création - for arg_instance in action_instance.arguments_set().all() { - let var_name = arg_instance.get_model().state_variable().get_name(); - if let Some(var_instance) = statevariables.get_by_name(var_name) { - // Appeler bind_variable() si elle existe - // arg_instance.bind_variable(var_instance); - // ⚠️ TODO: Cette ligne nécessite les modifications dans ArgumentInstance - } - } - - if let Err(e) = actions.insert(action_instance) { - error!("Failed to insert action '{}': {:?}", a.get_name(), e); - } - } - - Self { - object: UpnpObjectType { - name: model.name().to_string(), - object_type: "ServiceInstance".to_string(), - }, - model: Arc::new(model.clone()), - identifier: model.identifier().to_string(), - device: None, - statevariables, - actions, - subscribers: Arc::new(RwLock::new(HashMap::new())), - changed_buffer: Arc::new(Mutex::new(HashMap::new())), - seqid: Arc::new(Mutex::new(HashMap::new())), - } - } -} - -impl UpnpTypedInstance for ServiceInstance { - fn get_model(&self) -> &Self::Model { - &self.model - } -} - -impl UpnpObject for ServiceInstance { - fn to_xml_element(&self) -> Element { - let mut elem = Element::new("service"); - - let mut service_type = Element::new("serviceType"); - service_type.children.push(XMLNode::Text(self.service_type())); - elem.children.push(XMLNode::Element(service_type)); - - let mut service_id = Element::new("serviceId"); - service_id.children.push(XMLNode::Text(self.service_id())); - elem.children.push(XMLNode::Element(service_id)); - - let mut scpd_url = Element::new("SCPDURL"); - scpd_url.children.push(XMLNode::Text(self.scpd_url())); - elem.children.push(XMLNode::Element(scpd_url)); - - let mut control_url = Element::new("controlURL"); - control_url.children.push(XMLNode::Text(self.control_url())); - elem.children.push(XMLNode::Element(control_url)); - - let mut event_sub_url = Element::new("eventSubURL"); - event_sub_url.children.push(XMLNode::Text(self.event_sub_url())); - elem.children.push(XMLNode::Element(event_sub_url)); - - elem - } -} - -impl ServiceInstance { - /// Retourne l'identifiant du service. - pub fn identifier(&self) -> &str { - &self.identifier - } - - /// Retourne le type de service UPnP. - /// - /// Format: `urn:schemas-upnp-org:service:{name}:{version}` - pub fn service_type(&self) -> String { - self.model.service_type() - } - - /// Retourne l'ID de service UPnP. - /// - /// Format: `urn:upnp-org:serviceId:{identifier}` - pub fn service_id(&self) -> String { - format!("urn:upnp-org:serviceId:{}", self.identifier) - } - - /// Retourne la route de base du service. - pub fn base_route(&self) -> String { - match &self.device { - Some(device) => format!("{}/service/{}", device.base_route(), self.get_name()), - None => format!("/service/{}", self.get_name()), - } - } - - /// Retourne l'URL de contrôle SOAP. - pub fn control_url(&self) -> String { - format!("{}/control", self.base_route()) - } - - /// Retourne l'URL de souscription aux événements. - pub fn event_sub_url(&self) -> String { - format!("{}/event", self.base_route()) - } - - /// Retourne l'URL de la description SCPD. - pub fn scpd_url(&self) -> String { - format!("{}/desc.xml", self.base_route()) - } - - /// Retourne l'USN (Unique Service Name). - pub fn usn(&self) -> String { - match &self.device { - Some(device) => format!("uuid:{}::urn:{}", device.udn(), self.service_type()), - None => format!("uuid::urn:{}", self.service_type()), - } - } - - /// Retourne les variables d'état. - pub fn statevariables(&self) -> &StateVarInstanceSet { - &self.statevariables - } - - /// Retourne les actions. - pub fn actions(&self) -> &ActionInstanceSet { - &self.actions - } - - /// Enregistre les routes UPnP dans le serveur Axum. - /// - /// # Errors - /// - /// Retourne une erreur si l'enregistrement des routes échoue. - pub async fn register_urls(&self, server: &mut crate::server::Server) -> Result<(), ServiceError> { - info!( - "✅ Service description for {}:{} available at : {}{}", - self.device.as_ref().map(|d| d.name()).unwrap_or("unknown"), - self.get_name(), - self.device.as_ref().map(|d| d.server_base_url()).unwrap_or_default(), - self.scpd_url(), - ); - - // Handler SCPD - let instance_scpd = self.clone(); - server.add_handler(&self.scpd_url(), move || { - let instance = instance_scpd.clone(); - async move { instance.scpd_handler().await } - }).await; - - // Handler control - let instance_control = self.clone(); - server.add_post_handler_with_state( - &self.control_url(), - control_handler, - instance_control, - ).await; - - // Handler événements - let instance_event = self.clone(); - server.add_handler_with_state( - &self.event_sub_url(), - event_sub_handler, - instance_event, - ).await; - - Ok(()) - } - - /// Génère l'élément XML SCPD. - pub fn scpd_element(&self) -> Element { - let mut elem = Element::new("scpd"); - elem.attributes.insert( - "xmlns".to_string(), - "urn:schemas-upnp-org:service-1-0".to_string(), - ); - - // specVersion - let mut spec = Element::new("specVersion"); - let mut major = Element::new("major"); - major.children.push(XMLNode::Text("1".to_string())); - spec.children.push(XMLNode::Element(major)); - - let mut minor = Element::new("minor"); - minor.children.push(XMLNode::Text("0".to_string())); - spec.children.push(XMLNode::Element(minor)); - - elem.children.push(XMLNode::Element(spec)); - - // actionList - if !self.actions.all().is_empty() { - elem.children.push(XMLNode::Element( - self.actions.to_xml_element() - )); - } - - // serviceStateTable - if !self.statevariables.all().is_empty() { - elem.children.push(XMLNode::Element( - self.statevariables.to_xml_element() - )); - } - - elem - } - - /// Handler pour la description SCPD. - async fn scpd_handler(&self) -> Response { - let elem = self.scpd_element(); - - let config = EmitterConfig::new() - .perform_indent(true) - .indent_string(" "); - - let mut xml_output = Vec::new(); - if let Err(e) = elem.write_with_config(&mut xml_output, config) { - error!("Failed to serialize SCPD XML: {}", e); - return StatusCode::INTERNAL_SERVER_ERROR.into_response(); - } - - let mut xml = String::from_utf8_lossy(&xml_output).to_string(); - - // Ajouter l'en-tête XML - xml.insert_str(0, "\n"); - - ( - StatusCode::OK, - [(axum::http::header::CONTENT_TYPE, "text/xml; charset=\"utf-8\"")], - xml, - ).into_response() - } - - /// Ajoute un abonné aux événements. - pub async fn add_subscriber(&self, sid: String, callback: String) { - let mut subscribers = self.subscribers.write().unwrap(); - subscribers.insert(sid, callback); - } - - /// Renouvelle un abonnement. - pub async fn renew_subscriber(&self, sid: &str, timeout: &str) { - info!("♻️ Renewed SID {} for timeout {}", sid, timeout); - } - - /// Supprime un abonné. - pub async fn remove_subscriber(&self, sid: &str) { - let mut subscribers = self.subscribers.write().unwrap(); - subscribers.remove(sid); - } - - /// Envoie l'événement initial à un nouvel abonné. - pub async fn send_initial_event(&self, sid: String) { - let callback = { - let subscribers = self.subscribers.read().unwrap(); - subscribers.get(&sid).cloned() - }; - - if let Some(callback) = callback { - let mut changed = HashMap::new(); - for sv in self.statevariables.all() { - if sv.is_sending_notification() { - changed.insert(sv.get_name().to_string(), sv.value().to_string()); - } - } - - if changed.is_empty() { - return; - } - - tokio::spawn(async move { - let callback = callback.trim().trim_matches(|c| c == '<' || c == '>'); - - let mut body = r#""#.to_string(); - for (name, val) in changed { - body.push_str(&format!("<{0}>{1}", name, val)); - } - body.push_str(""); - - let client = reqwest::Client::new(); - match client - .request(reqwest::Method::from_bytes(b"NOTIFY").unwrap(), callback) - .header("Content-Type", r#"text/xml; charset="utf-8"#) - .header("NT", "upnp:event") - .header("NTS", "upnp:propchange") - .header("SID", &sid) - .header("SEQ", "0") - .body(body) - .send() - .await - { - Ok(resp) => { - info!("✅ Initial event sent to {}, status={}", callback, resp.status()); - } - Err(e) => { - error!("Failed to send initial event to {}: {}", callback, e); - } - } - }); - } - } - - /// Marque un changement à notifier. - pub fn event_to_be_sent(&self, name: String, value: String) { - let mut buffer = self.changed_buffer.lock().unwrap(); - buffer.insert(name, value); - } - - /// Récupère le prochain numéro de séquence pour un abonné. - fn next_seq(&self, sid: &str) -> String { - let mut seqid = self.seqid.lock().unwrap(); - let counter = seqid.entry(sid.to_string()).or_insert(0); - *counter += 1; - counter.to_string() - } - - /// Notifie tous les abonnés des changements. - pub async fn notify_subscribers(&self) { - let subscribers_copy = { - let subscribers = self.subscribers.read().unwrap(); - if subscribers.is_empty() { - return; - } - subscribers.clone() - }; - - let changed = { - let mut buffer = self.changed_buffer.lock().unwrap(); - if buffer.is_empty() { - return; - } - std::mem::take(&mut *buffer) - }; - - for (sid, callback) in subscribers_copy { - let changed_clone = changed.clone(); - let seq = self.next_seq(&sid); - - tokio::spawn(async move { - let callback = callback.trim().trim_matches(|c| c == '<' || c == '>'); - - let mut body = r#""#.to_string(); - for (name, val) in changed_clone { - body.push_str(&format!("<{0}>{1}", name, val)); - } - body.push_str(""); - - let client = reqwest::Client::new(); - match client - .request(reqwest::Method::from_bytes(b"NOTIFY").unwrap(), callback) - .header("Content-Type", r#"text/xml; charset="utf-8"#) - .header("NT", "upnp:event") - .header("NTS", "upnp:propchange") - .header("SID", &sid) - .header("SEQ", seq) - .body(body) - .send() - .await - { - Ok(_) => { - info!("✅ Notified subscriber {} of changes", callback); - } - Err(e) => { - error!("Failed to notify subscriber {}: {}", callback, e); - } - } - }); - } - } - - /// Démarre le notifier périodique. - /// - /// # Arguments - /// - /// * `interval` - Intervalle entre les notifications - /// - /// # Returns - /// - /// Un handle vers la tâche tokio du notifier. - pub fn start_notifier(&self, interval: Duration) -> tokio::task::JoinHandle<()> { - let instance = self.clone(); - - tokio::spawn(async move { - let mut ticker = time::interval(interval); - info!("✅ Starting notifier every {:?}", interval); - - loop { - ticker.tick().await; - instance.notify_subscribers().await; - } - }) - } -} - -/// Handler Axum pour les événements (SUBSCRIBE/UNSUBSCRIBE). -async fn event_sub_handler( - State(instance): State, - headers: HeaderMap, - req: Request, -) -> Response { - info!("📡 Event Subscription request for {}", instance.get_name()); - - let method = req.method().as_str(); - let sid = headers.get("SID").and_then(|v| v.to_str().ok()).unwrap_or(""); - let timeout = headers.get("Timeout").and_then(|v| v.to_str().ok()).unwrap_or(""); - let callback = headers.get("Callback").and_then(|v| v.to_str().ok()).unwrap_or(""); - - match method { - METHOD_SUBSCRIBE => { - let (response_sid, response_timeout) = if sid.is_empty() { - // Nouvelle souscription - let new_sid = format!("uuid:{}", uuid::Uuid::new_v4()); - if !callback.is_empty() { - instance.add_subscriber(new_sid.clone(), callback.to_string()).await; - } - let timeout_val = if timeout.is_empty() { - "Second-1800" - } else { - timeout - }; - info!("🔒 New subscription: SID={}, Callback={}, Timeout={}", new_sid, callback, timeout_val); - - let sid_clone = new_sid.clone(); - let instance_clone = instance.clone(); - tokio::spawn(async move { - instance_clone.send_initial_event(sid_clone).await; - }); - - (new_sid, timeout_val.to_string()) - } else { - // Renouvellement - instance.renew_subscriber(sid, timeout).await; - info!("♻️ Renew subscription: SID={}, Timeout={}", sid, timeout); - (sid.to_string(), timeout.to_string()) - }; - - ( - StatusCode::OK, - [ - ( - axum::http::header::HeaderName::from_static("sid"), - axum::http::HeaderValue::from_str(&response_sid).unwrap() - ), - ( - axum::http::header::HeaderName::from_static("timeout"), - axum::http::HeaderValue::from_str(&response_timeout).unwrap() - ), - ], - ).into_response() - } - METHOD_UNSUBSCRIBE => { - if !sid.is_empty() { - instance.remove_subscriber(sid).await; - info!("❌ Unsubscribe SID={}", sid); - } - StatusCode::OK.into_response() - } - _ => { - warn!("Unsupported EventSub method: {}", method); - StatusCode::METHOD_NOT_ALLOWED.into_response() - } - } -} - -/// Handler Axum pour le contrôle SOAP. -async fn control_handler( - State(instance): State, - body: String, -) -> Response { - info!("📡 Control request for {}", instance.get_name()); - - // TODO: Parser le SOAP et appeler l'action correspondante - - let response_xml = format!( - r#" - - - - - -"#, - instance.service_type() - ); - - ( - StatusCode::OK, - [(axum::http::header::CONTENT_TYPE, "text/xml; charset=\"utf-8\"")], - response_xml, - ).into_response() -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::services::Service; - - #[test] - fn test_service_instance_creation() { - let service = Service::new("AVTransport".to_string()); - let instance = ServiceInstance::new(&service); - - assert_eq!(instance.get_name(), "AVTransport"); - assert_eq!(instance.identifier(), "AVTransport"); - } - - #[test] - fn test_service_urls() { - let service = Service::new("AVTransport".to_string()); - let instance = ServiceInstance::new(&service); - - assert_eq!(instance.base_route(), "/service/AVTransport"); - assert_eq!(instance.control_url(), "/service/AVTransport/control"); - assert_eq!(instance.event_sub_url(), "/service/AVTransport/event"); - assert_eq!(instance.scpd_url(), "/service/AVTransport/desc.xml"); - } - - #[test] - fn test_service_type() { - let mut service = Service::new("AVTransport".to_string()); - service.set_version(2).unwrap(); - let instance = ServiceInstance::new(&service); - - assert_eq!( - instance.service_type(), - "urn:schemas-upnp-org:service:AVTransport:2" - ); - } -}``` - -## fichier: `pmoupnp/src/services/mod.rs` - -```rust -//! # Module Services - Gestion des services UPnP -//! -//! Ce module implémente les services UPnP selon la spécification UPnP Device Architecture. -//! Un service UPnP contient des actions (méthodes appelables) et des variables d'état -//! (propriétés observables). -//! -//! ## Architecture -//! -//! - [`Service`] : Modèle définissant la structure d'un service -//! - [`ServiceInstance`] : Instance concrète d'un service attachée à un device -//! -//! ## Fonctionnalités -//! -//! - ✅ Actions UPnP avec arguments typés -//! - ✅ Variables d'état avec notifications d'événements -//! - ✅ Génération SCPD (Service Control Protocol Description) -//! - ✅ Endpoints SOAP pour le contrôle -//! - ✅ Gestion des abonnements aux événements (SUBSCRIBE/UNSUBSCRIBE) -//! - ✅ Notifications automatiques des changements d'état -//! -//! ## Examples -//! -//! ```rust -//! use pmoupnp::services::Service; -//! use pmoupnp::state_variables::StateVariable; -//! use pmoupnp::variable_types::StateVarType; -//! use std::sync::Arc; -//! -//! // Créer un service -//! let mut service = Service::new("AVTransport".to_string()); -//! service.set_version(1).unwrap(); -//! -//! // Ajouter une variable d'état -//! let transport_state = Arc::new( -//! StateVariable::new(StateVarType::String, "TransportState".to_string()) -//! ); -//! service.add_variable(transport_state); -//! -//! // Créer une instance -//! let instance = service.create_instance(); -//! ``` - -mod errors; -mod service_methods; -mod service_instance; - -use std::sync::Arc; - -pub use errors::ServiceError; -pub use service_instance::ServiceInstance; - -use crate::{ - actions::ActionSet, - state_variables::StateVariableSet, - UpnpObjectType, -}; - -/// Service UPnP (modèle). -/// -/// Représente la définition d'un service UPnP avec ses actions et variables d'état. -/// Un service est attaché à un device et expose des fonctionnalités via SOAP. -/// -/// # Structure -/// -/// Un service UPnP contient : -/// - Un identifiant unique (`identifier`) -/// - Une version (ex: 1, 2, 3...) -/// - Un ensemble d'actions ([`ActionSet`]) -/// - Une table de variables d'état ([`StateVariableSet`]) -/// -/// # Cycle de vie -/// -/// 1. Création avec [`Service::new`] -/// 2. Configuration (ajout d'actions et variables) -/// 3. Instanciation avec [`create_instance`](crate::UpnpModel::create_instance) -/// -/// # Examples -/// -/// ```rust -/// # use pmoupnp::services::Service; -/// # use pmoupnp::state_variables::StateVariable; -/// # use pmoupnp::variable_types::StateVarType; -/// # use std::sync::Arc; -/// let mut service = Service::new("ContentDirectory".to_string()); -/// service.set_identifier("urn:upnp-org:serviceId:ContentDirectory".to_string()); -/// service.set_version(1).unwrap(); -/// -/// // Ajouter une variable d'état -/// let search_caps = Arc::new( -/// StateVariable::new(StateVarType::String, "SearchCapabilities".to_string()) -/// ); -/// service.add_variable(search_caps); -/// ``` -#[derive(Debug, Clone)] -pub struct Service { - /// Métadonnées de l'objet UPnP - object: UpnpObjectType, - - /// Identifiant du service (ex: "urn:upnp-org:serviceId:AVTransport") - identifier: String, - - /// Version du service (>= 1) - version: u32, - - /// Actions disponibles dans ce service - actions: ActionSet, - - /// Variables d'état du service - state_table: StateVariableSet, -} - -impl Service { - /// Crée un nouveau service UPnP. - /// - /// # Arguments - /// - /// * `name` - Nom du service (ex: "AVTransport", "RenderingControl") - /// - /// # Returns - /// - /// Un nouveau service avec : - /// - Identifiant initialisé au nom - /// - Version 1 par défaut - /// - Collections vides d'actions et de variables - /// - /// # Examples - /// - /// ```rust - /// # use pmoupnp::services::Service; - /// let service = Service::new("AVTransport".to_string()); - /// assert_eq!(service.name(), "AVTransport"); - /// assert_eq!(service.version(), 1); - /// ``` - pub fn new(name: String) -> Self { - Self { - object: UpnpObjectType { - name: name.clone(), - object_type: "Service".to_string(), - }, - identifier: name, - version: 1, - state_table: StateVariableSet::new(), - actions: ActionSet::new(), - } - } - - /// Retourne le nom du service. - /// - /// # Examples - /// - /// ```rust - /// # use pmoupnp::services::Service; - /// let service = Service::new("AVTransport".to_string()); - /// assert_eq!(service.name(), "AVTransport"); - /// ``` - pub fn name(&self) -> &str { - &self.object.name - } - - /// Retourne le type d'objet. - /// - /// # Examples - /// - /// ```rust - /// # use pmoupnp::services::Service; - /// let service = Service::new("AVTransport".to_string()); - /// assert_eq!(service.type_id(), "Service"); - /// ``` - pub fn type_id(&self) -> &str { - &self.object.object_type - } - - /// Retourne l'identifiant du service. - /// - /// # Examples - /// - /// ```rust - /// # use pmoupnp::services::Service; - /// let mut service = Service::new("AVTransport".to_string()); - /// service.set_identifier("urn:upnp-org:serviceId:AVTransport".to_string()); - /// assert_eq!(service.identifier(), "urn:upnp-org:serviceId:AVTransport"); - /// ``` - pub fn identifier(&self) -> &str { - &self.identifier - } - - /// Définit l'identifiant du service. - /// - /// # Arguments - /// - /// * `id` - Nouvel identifiant (typiquement un URN UPnP) - /// - /// # Examples - /// - /// ```rust - /// # use pmoupnp::services::Service; - /// let mut service = Service::new("AVTransport".to_string()); - /// service.set_identifier("urn:upnp-org:serviceId:AVTransport".to_string()); - /// ``` - pub fn set_identifier(&mut self, id: String) { - self.identifier = id; - } - - /// Retourne la version du service. - /// - /// # Examples - /// - /// ```rust - /// # use pmoupnp::services::Service; - /// let service = Service::new("AVTransport".to_string()); - /// assert_eq!(service.version(), 1); - /// ``` - pub fn version(&self) -> u32 { - self.version - } - - /// Définit la version du service. - /// - /// # Arguments - /// - /// * `version` - Numéro de version (doit être >= 1) - /// - /// # Errors - /// - /// Retourne une erreur si la version est < 1. - /// - /// # Examples - /// - /// ```rust - /// # use pmoupnp::services::Service; - /// let mut service = Service::new("AVTransport".to_string()); - /// assert!(service.set_version(2).is_ok()); - /// assert!(service.set_version(0).is_err()); - /// ``` - pub fn set_version(&mut self, version: u32) -> Result<(), ServiceError> { - if version < 1 { - return Err(ServiceError::ValidationError( - "Version must be >= 1".to_string() - )); - } - self.version = version; - Ok(()) - } - - /// Ajoute une variable d'état au service. - /// - /// # Arguments - /// - /// * `sv` - Variable d'état à ajouter - /// - /// # Errors - /// - /// Retourne une erreur si une variable avec le même nom existe déjà. - /// - /// # Examples - /// - /// ```rust - /// # use pmoupnp::services::Service; - /// # use pmoupnp::state_variables::StateVariable; - /// # use pmoupnp::variable_types::StateVarType; - /// # use std::sync::Arc; - /// let mut service = Service::new("AVTransport".to_string()); - /// let var = Arc::new( - /// StateVariable::new(StateVarType::String, "TransportState".to_string()) - /// ); - /// service.add_variable(var).unwrap(); - /// ``` - pub fn add_variable(&mut self, sv: Arc) - -> Result<(), ServiceError> - { - self.state_table - .insert(sv) - .map_err(|e| ServiceError::SetError(format!("Failed to add variable: {:?}", e))) - } - - /// Vérifie si une variable d'état existe dans le service. - /// - /// # Arguments - /// - /// * `sv` - Variable à rechercher - /// - /// # Returns - /// - /// `true` si la variable existe, `false` sinon. - /// - /// # Examples - /// - /// ```rust - /// # use pmoupnp::services::Service; - /// # use pmoupnp::state_variables::StateVariable; - /// # use pmoupnp::variable_types::StateVarType; - /// # use std::sync::Arc; - /// let mut service = Service::new("AVTransport".to_string()); - /// let var = Arc::new( - /// StateVariable::new(StateVarType::String, "TransportState".to_string()) - /// ); - /// service.add_variable(var.clone()).unwrap(); - /// assert!(service.contains_variable(var)); - /// ``` - pub fn contains_variable(&self, sv: Arc) -> bool { - self.state_table.contains(sv) - } - - /// Retourne toutes les variables d'état du service. - /// - /// # Examples - /// - /// ```rust - /// # use pmoupnp::services::Service; - /// let service = Service::new("AVTransport".to_string()); - /// for var in service.variables() { - /// println!("Variable: {}", var.get_name()); - /// } - /// ``` - pub fn variables(&self) -> Vec> { - self.state_table.all() - } - - /// Ajoute une action au service. - /// - /// # Arguments - /// - /// * `action` - Action à ajouter - /// - /// # Errors - /// - /// Retourne une erreur si une action avec le même nom existe déjà. - /// - /// # Examples - /// - /// ```rust - /// # use pmoupnp::services::Service; - /// # use pmoupnp::actions::Action; - /// # use std::sync::Arc; - /// let mut service = Service::new("AVTransport".to_string()); - /// let action = Arc::new(Action::new("Play".to_string())); - /// service.add_action(action).unwrap(); - /// ``` - pub fn add_action(&mut self, action: Arc) - -> Result<(), ServiceError> - { - self.actions - .insert(action) - .map_err(|e| ServiceError::SetError(format!("Failed to add action: {:?}", e))) - } - - /// Retourne toutes les actions du service. - /// - /// # Examples - /// - /// ```rust - /// # use pmoupnp::services::Service; - /// let service = Service::new("AVTransport".to_string()); - /// for action in service.actions() { - /// println!("Action: {}", action.get_name()); - /// } - /// ``` - pub fn actions(&self) -> Vec> { - self.actions.all() - } - - /// Retourne le type de service UPnP. - /// - /// Format: `urn:schemas-upnp-org:service:{name}:{version}` - /// - /// # Examples - /// - /// ```rust - /// # use pmoupnp::services::Service; - /// let service = Service::new("AVTransport".to_string()); - /// assert_eq!( - /// service.service_type(), - /// "urn:schemas-upnp-org:service:AVTransport:1" - /// ); - /// ``` - pub fn service_type(&self) -> String { - format!("urn:schemas-upnp-org:service:{}:{}", self.name(), self.version) - } -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::state_variables::StateVariable; - use crate::variable_types::StateVarType; - use crate::actions::Action; - - #[test] - fn test_service_new() { - let service = Service::new("AVTransport".to_string()); - assert_eq!(service.name(), "AVTransport"); - assert_eq!(service.type_id(), "Service"); - assert_eq!(service.version(), 1); - assert_eq!(service.identifier(), "AVTransport"); - } - - #[test] - fn test_service_set_version() { - let mut service = Service::new("AVTransport".to_string()); - assert!(service.set_version(2).is_ok()); - assert_eq!(service.version(), 2); - - // Version 0 devrait échouer - assert!(service.set_version(0).is_err()); - } - - #[test] - fn test_service_add_variable() { - let mut service = Service::new("AVTransport".to_string()); - let var = Arc::new( - StateVariable::new(StateVarType::String, "TransportState".to_string()) - ); - - assert!(service.add_variable(var.clone()).is_ok()); - assert!(service.contains_variable(var)); - } - - #[test] - fn test_service_add_action() { - let mut service = Service::new("AVTransport".to_string()); - let action = Arc::new(Action::new("Play".to_string())); - - assert!(service.add_action(action).is_ok()); - assert_eq!(service.actions().len(), 1); - } - - #[test] - fn test_service_type() { - let mut service = Service::new("AVTransport".to_string()); - service.set_version(2).unwrap(); - - assert_eq!( - service.service_type(), - "urn:schemas-upnp-org:service:AVTransport:2" - ); - } -}``` - -## fichier: `pmoupnp/src/services/errors.rs` - -```rust -//! Erreurs du module services. - -use thiserror::Error; - -/// Erreurs liées aux services UPnP. -/// -/// Cette énumération couvre toutes les erreurs possibles lors de la manipulation -/// de services UPnP, incluant les erreurs de validation, de configuration et d'exécution. -#[derive(Error, Debug)] -pub enum ServiceError { - /// Erreur générale du service. - #[error("Service error: {0}")] - GeneralError(String), - - /// Erreur de validation (paramètres invalides). - #[error("Validation error: {0}")] - ValidationError(String), - - /// Erreur lors d'une opération sur un ensemble (Set). - #[error("Set operation error: {0}")] - SetError(String), - - /// Erreur liée à une action. - #[error("Action error: {0}")] - ActionError(String), - - /// Erreur liée à une variable d'état. - #[error("State variable error: {0}")] - StateVariableError(String), - - /// Erreur de configuration. - #[error("Configuration error: {0}")] - ConfigError(String), - - /// Erreur réseau ou HTTP. - #[error("Network error: {0}")] - NetworkError(String), - - /// Erreur de sérialisation XML. - #[error("XML serialization error: {0}")] - XmlError(String), - - /// Erreur lors du traitement SOAP. - #[error("SOAP error: {0}")] - SoapError(String), -} - -impl From for ServiceError { - fn from(err: std::io::Error) -> Self { - ServiceError::GeneralError(format!("IO error: {}", err)) - } -} - -impl From for ServiceError { - fn from(err: crate::UpnpObjectSetError) -> Self { - match err { - crate::UpnpObjectSetError::AlreadyExists(name) => { - ServiceError::SetError(format!("Object already exists: {}", name)) - } - } - } -}``` - -## fichier: `pmoupnp/src/services/service_methods.rs` - -```rust -//! Implémentation des traits UPnP pour Service. - -use xmltree::{Element, XMLNode}; - -use crate::{ - services::{Service, ServiceInstance}, - UpnpObject, UpnpModel, UpnpTyped, UpnpObjectType, -}; - -impl UpnpTyped for Service { - fn as_upnp_object_type(&self) -> &UpnpObjectType { - &self.object - } -} - -impl UpnpObject for Service { - fn to_xml_element(&self) -> Element { - let mut elem = Element::new("service"); - - // serviceType - let mut service_type = Element::new("serviceType"); - service_type.children.push(XMLNode::Text(self.service_type())); - elem.children.push(XMLNode::Element(service_type)); - - // serviceId - let mut service_id = Element::new("serviceId"); - service_id.children.push(XMLNode::Text(self.identifier().to_string())); - elem.children.push(XMLNode::Element(service_id)); - - elem - } -} - -impl UpnpModel for Service { - type Instance = ServiceInstance; -}``` - -## fichier: `pmoupnp/src/devices/mod.rs` - -```rust -``` - -## fichier: `pmoutils/Cargo.toml` - -```toml -[package] -name = "pmoutils" -version = "0.1.0" -edition = "2024" - -[dependencies] -get_if_addrs = "0.5.3"``` - -## fichier: `pmoutils/src/lib.rs` - -```rust -/// Utilitaires pour la gestion des adresses IP réseau. -/// -/// Ce module fournit des fonctions pour détecter et lister les adresses IP -/// des interfaces réseau locales de la machine. -/// -/// # Fonctions principales -/// -/// - [`guess_local_ip`] : Devine l'adresse IP locale utilisée pour les connexions sortantes -/// -/// # Examples -/// -/// ``` -/// use votre_crate::guess_local_ip; -/// -/// let ip = guess_local_ip(); -/// println!("Adresse IP locale: {}", ip); -/// ``` -mod ip_utils; - -pub use ip_utils::guess_local_ip;``` - -## fichier: `pmoutils/src/ip_utils.rs` - -```rust -use get_if_addrs::get_if_addrs; -use std::net::UdpSocket; - -/// Devine l'adresse IP locale de la machine. -/// -/// Cette fonction tente de déterminer l'adresse IP locale en créant une connexion UDP -/// vers un serveur DNS public (8.8.8.8). Cette technique permet d'identifier l'interface -/// réseau qui serait utilisée pour communiquer avec Internet. -/// -/// # Fonctionnement -/// -/// 1. Crée un socket UDP lié à `0.0.0.0:0` (n'importe quelle interface, port aléatoire) -/// 2. Tente une connexion (non effective pour UDP) vers `8.8.8.8:80` -/// 3. Récupère l'adresse IP locale du socket -/// 4. En cas d'échec à n'importe quelle étape, retourne `127.0.0.1` -/// -/// # Returns -/// -/// Retourne l'adresse IP locale sous forme de `String`, ou `"127.0.0.1"` en cas d'erreur. -/// -/// # Examples -/// -/// ``` -/// let ip = guess_local_ip(); -/// println!("IP locale détectée: {}", ip); -/// // Affiche par exemple: "IP locale détectée: 192.168.1.42" -/// ``` -/// -/// # Note -/// -/// Cette méthode ne crée pas de véritable connexion réseau (UDP est sans connexion), -/// elle demande simplement au système d'exploitation quelle interface serait utilisée -/// pour joindre l'adresse cible. -pub fn guess_local_ip() -> String { - match UdpSocket::bind("0.0.0.0:0") { - Ok(socket) => { - if socket.connect("8.8.8.8:80").is_ok() { - if let Ok(local_addr) = socket.local_addr() { - return local_addr.ip().to_string(); - } - } - "127.0.0.1".to_string() - } - Err(_) => "127.0.0.1".to_string(), - } -} - -/// Liste toutes les adresses IP non-loopback des interfaces réseau. -/// -/// Parcourt toutes les interfaces réseau de la machine et collecte leurs adresses IPv4, -/// en excluant les adresses de loopback (127.0.0.1). -/// -/// # Returns -/// -/// Retourne une `HashMap` où : -/// - **Clé** : nom de l'interface réseau (ex: `"eth0"`, `"wlan0"`, `"en0"`) -/// - **Valeur** : vecteur des adresses IP (format String) associées à cette interface -/// -/// En cas d'erreur lors de la récupération des interfaces, retourne une HashMap -/// contenant une entrée `"error"` avec un message d'erreur. -/// -/// # Examples -/// -/// ``` -/// let ips = list_all_ips(); -/// for (interface, addresses) in ips { -/// println!("Interface {}: {:?}", interface, addresses); -/// } -/// // Affiche par exemple: -/// // Interface eth0: ["192.168.1.42"] -/// // Interface wlan0: ["10.0.0.15"] -/// ``` -/// -/// # Note -/// -/// - Seules les adresses IPv4 sont retournées -/// - Les adresses de loopback (127.x.x.x) sont filtrées -/// - Les adresses IPv6 sont ignorées -pub fn list_all_ips() -> std::collections::HashMap> { - let mut result = std::collections::HashMap::new(); - - if let Ok(interfaces) = get_if_addrs() { - for iface in interfaces { - let ip = iface.ip(); - if ip.is_loopback() { - continue; - } - if ip.is_ipv4() { - result - .entry(iface.name) - .or_insert_with(Vec::new) - .push(ip.to_string()); - } - } - } else { - result.insert( - "error".to_string(), - vec!["Failed to get interfaces".to_string()], - ); - } - - result -} - -#[cfg(test)] -mod tests { - use super::*; - use std::net::IpAddr; - - #[test] - fn test_guess_local_ip_returns_valid_ip() { - let ip = guess_local_ip(); - - // Vérifie que le résultat est parsable comme une IP - assert!(ip.parse::().is_ok(), "Should return a valid IP address"); - } - - #[test] - fn test_guess_local_ip_not_empty() { - let ip = guess_local_ip(); - - assert!(!ip.is_empty(), "IP should not be empty"); - } - - #[test] - fn test_guess_local_ip_is_ipv4() { - let ip = guess_local_ip(); - - if let Ok(parsed_ip) = ip.parse::() { - assert!(parsed_ip.is_ipv4(), "Should return an IPv4 address"); - } - } - - #[test] - fn test_guess_local_ip_fallback_is_localhost() { - // Ce test vérifie que si aucune IP n'est trouvée, on retourne 127.0.0.1 - // (difficile à tester sans mocker, mais on vérifie la cohérence) - let ip = guess_local_ip(); - let parsed = ip.parse::().unwrap(); - - // L'IP doit être soit locale (127.0.0.1) soit une IP privée valide - assert!( - parsed.is_loopback() || is_private_ip(&ip), - "IP should be either loopback or private" - ); - } - - #[test] - fn test_list_all_ips_no_loopback() { - let ips = list_all_ips(); - - // Vérifie qu'aucune adresse de loopback n'est présente - for (_, addresses) in ips.iter() { - for addr in addresses { - if let Ok(parsed_ip) = addr.parse::() { - assert!( - !parsed_ip.is_loopback(), - "Loopback addresses should be filtered out" - ); - } - } - } - } - - #[test] - fn test_list_all_ips_only_ipv4() { - let ips = list_all_ips(); - - // Vérifie que seules des adresses IPv4 sont retournées - for (iface_name, addresses) in ips.iter() { - if iface_name == "error" { - continue; // Skip error entries - } - - for addr in addresses { - if let Ok(parsed_ip) = addr.parse::() { - assert!( - parsed_ip.is_ipv4(), - "Only IPv4 addresses should be returned" - ); - } - } - } - } - - #[test] - fn test_list_all_ips_valid_format() { - let ips = list_all_ips(); - - // Vérifie que toutes les IPs sont dans un format valide - for (iface_name, addresses) in ips.iter() { - if iface_name == "error" { - continue; - } - - for addr in addresses { - assert!( - addr.parse::().is_ok(), - "Each IP should be in valid format: {}", - addr - ); - } - } - } - - #[test] - fn test_list_all_ips_interface_names_not_empty() { - let ips = list_all_ips(); - - // Vérifie que les noms d'interface ne sont pas vides - for (iface_name, _) in ips.iter() { - assert!(!iface_name.is_empty(), "Interface names should not be empty"); - } - } - - #[test] - fn test_list_all_ips_no_duplicate_ips_per_interface() { - let ips = list_all_ips(); - - // Vérifie qu'il n'y a pas de doublons par interface - for (iface_name, addresses) in ips.iter() { - if iface_name == "error" { - continue; - } - - let unique_addresses: std::collections::HashSet<_> = addresses.iter().collect(); - assert_eq!( - addresses.len(), - unique_addresses.len(), - "No duplicate IPs should exist for interface {}", - iface_name - ); - } - } - - // Fonction helper pour les tests - fn is_private_ip(ip_str: &str) -> bool { - if let Ok(ip) = ip_str.parse::() { - match ip { - IpAddr::V4(ipv4) => { - let octets = ipv4.octets(); - // Plages privées: 10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16 - octets[0] == 10 - || (octets[0] == 172 && octets[1] >= 16 && octets[1] <= 31) - || (octets[0] == 192 && octets[1] == 168) - } - IpAddr::V6(_) => false, - } - } else { - false - } - } - - #[test] - fn test_helper_is_private_ip() { - // Tests pour la fonction helper - assert!(is_private_ip("10.0.0.1")); - assert!(is_private_ip("172.16.0.1")); - assert!(is_private_ip("192.168.1.1")); - assert!(!is_private_ip("8.8.8.8")); - assert!(!is_private_ip("127.0.0.1")); // loopback n'est pas "privé" au sens réseau local - } -}``` - diff --git a/pmoupnp/src/mediarenderer/avtransport/mod.rs b/pmoupnp/src/mediarenderer/avtransport/mod.rs index eed3f79a..ca4aaeca 100644 --- a/pmoupnp/src/mediarenderer/avtransport/mod.rs +++ b/pmoupnp/src/mediarenderer/avtransport/mod.rs @@ -108,30 +108,8 @@ use variables::{ TRANSPORTPLAYSPEED, TRANSPORTSTATE, TRANSPORTSTATUS }; -/// Service AVTransport:1 conforme à la spécification UPnP AV. -/// -/// Ce service permet de contrôler le transport de médias **audio** sur un MediaRenderer. -/// -/// # Initialisation -/// -/// Le service est initialisé paresseusement (lazy) au premier accès via la macro -/// [`define_service!`]. Toutes les variables d'état et actions sont automatiquement -/// enregistrées. -/// -/// # Exemples -/// -/// ```rust -/// use pmoupnp::mediarenderer::avtransport::AVTTRANSPORT; -/// -/// let service = &*AVTTRANSPORT; -/// assert_eq!(service.name(), "AVTransport"); -/// assert_eq!(service.version(), 1); -/// ``` -/// -/// # Voir aussi -/// -/// - Module [`actions`] : Définition de toutes les actions UPnP -/// - Module [`variables`] : Définition de toutes les variables d'état +// Service AVTransport:1 conforme à la spécification UPnP AV pour MediaRenderer audio +// Voir la documentation du module pour plus de détails define_service! { pub static AVTTRANSPORT = "AVTransport" { variables: [ diff --git a/pmoupnp/src/mediarenderer/connectionmanager/actions/getcurrentconnectionids.rs b/pmoupnp/src/mediarenderer/connectionmanager/actions/getcurrentconnectionids.rs new file mode 100644 index 00000000..34480ad8 --- /dev/null +++ b/pmoupnp/src/mediarenderer/connectionmanager/actions/getcurrentconnectionids.rs @@ -0,0 +1,8 @@ +use crate::mediarenderer::connectionmanager::variables::CURRENTCONNECTIONIDS; +use crate::define_action; + +define_action! { + pub static GETCURRENTCONNECTIONIDS = "GetCurrentConnectionIDs" { + out "ConnectionIDs" => CURRENTCONNECTIONIDS, + } +} diff --git a/pmoupnp/src/mediarenderer/connectionmanager/actions/getcurrentconnectioninfo.rs b/pmoupnp/src/mediarenderer/connectionmanager/actions/getcurrentconnectioninfo.rs new file mode 100644 index 00000000..e9d4abb0 --- /dev/null +++ b/pmoupnp/src/mediarenderer/connectionmanager/actions/getcurrentconnectioninfo.rs @@ -0,0 +1,18 @@ +use crate::mediarenderer::connectionmanager::variables::{ + A_ARG_TYPE_CONNECTIONID, A_ARG_TYPE_RCSID, A_ARG_TYPE_AVTRANSPORTID, + A_ARG_TYPE_PROTOCOLINFO, A_ARG_TYPE_DIRECTION, A_ARG_TYPE_CONNECTIONSTATUS +}; +use crate::define_action; + +define_action! { + pub static GETCURRENTCONNECTIONINFO = "GetCurrentConnectionInfo" { + in "ConnectionID" => A_ARG_TYPE_CONNECTIONID, + out "RcsID" => A_ARG_TYPE_RCSID, + out "AVTransportID" => A_ARG_TYPE_AVTRANSPORTID, + out "ProtocolInfo" => A_ARG_TYPE_PROTOCOLINFO, + out "PeerConnectionManager" => A_ARG_TYPE_PROTOCOLINFO, + out "PeerConnectionID" => A_ARG_TYPE_CONNECTIONID, + out "Direction" => A_ARG_TYPE_DIRECTION, + out "Status" => A_ARG_TYPE_CONNECTIONSTATUS, + } +} diff --git a/pmoupnp/src/mediarenderer/connectionmanager/actions/getprotocolinfo.rs b/pmoupnp/src/mediarenderer/connectionmanager/actions/getprotocolinfo.rs new file mode 100644 index 00000000..f4207679 --- /dev/null +++ b/pmoupnp/src/mediarenderer/connectionmanager/actions/getprotocolinfo.rs @@ -0,0 +1,9 @@ +use crate::mediarenderer::connectionmanager::variables::{SOURCEPROTOCOLINFO, SINKPROTOCOLINFO}; +use crate::define_action; + +define_action! { + pub static GETPROTOCOLINFO = "GetProtocolInfo" { + out "Source" => SOURCEPROTOCOLINFO, + out "Sink" => SINKPROTOCOLINFO, + } +} diff --git a/pmoupnp/src/mediarenderer/connectionmanager/actions/mod.rs b/pmoupnp/src/mediarenderer/connectionmanager/actions/mod.rs new file mode 100644 index 00000000..b9ccd37b --- /dev/null +++ b/pmoupnp/src/mediarenderer/connectionmanager/actions/mod.rs @@ -0,0 +1,7 @@ +mod getprotocolinfo; +mod getcurrentconnectionids; +mod getcurrentconnectioninfo; + +pub use getprotocolinfo::GETPROTOCOLINFO; +pub use getcurrentconnectionids::GETCURRENTCONNECTIONIDS; +pub use getcurrentconnectioninfo::GETCURRENTCONNECTIONINFO; diff --git a/pmoupnp/src/mediarenderer/connectionmanager/mod.rs b/pmoupnp/src/mediarenderer/connectionmanager/mod.rs new file mode 100644 index 00000000..7ca0664d --- /dev/null +++ b/pmoupnp/src/mediarenderer/connectionmanager/mod.rs @@ -0,0 +1,89 @@ +//! # ConnectionManager Service - Service de gestion des connexions UPnP +//! +//! Ce module implémente le service ConnectionManager:1 selon la spécification UPnP AV. +//! Le service ConnectionManager gère les connexions entre MediaServer et MediaRenderer, +//! et expose les protocoles et formats supportés. +//! +//! ## Fonctionnalités +//! +//! Le service ConnectionManager permet : +//! - **Énumération des protocoles** : GetProtocolInfo +//! - **Gestion des connexions** : GetCurrentConnectionIDs, GetCurrentConnectionInfo +//! - Support des formats audio (MP3, FLAC, WAV, etc.) +//! +//! ## Conformité UPnP +//! +//! Cette implémentation suit la spécification **UPnP ConnectionManager:1 Service Template**. +//! Toutes les actions obligatoires (Required) sont implémentées : +//! +//! - ✅ GetProtocolInfo +//! - ✅ GetCurrentConnectionIDs +//! - ✅ GetCurrentConnectionInfo +//! +//! ## Variables d'état +//! +//! Le service expose les variables d'état conformes à la spécification : +//! +//! ### Informations de protocole +//! - [`SOURCEPROTOCOLINFO`] : Protocoles source (vide pour un renderer) +//! - [`SINKPROTOCOLINFO`] : Protocoles sink supportés (http-get:*:audio/mpeg:*, etc.) +//! - [`CURRENTCONNECTIONIDS`] : IDs des connexions actives +//! +//! ### Arguments +//! - [`A_ARG_TYPE_CONNECTIONID`] : ID de connexion +//! - [`A_ARG_TYPE_CONNECTIONSTATUS`] : Statut de connexion +//! - [`A_ARG_TYPE_DIRECTION`] : Direction (Input/Output) +//! - [`A_ARG_TYPE_PROTOCOLINFO`] : Information de protocole +//! - [`A_ARG_TYPE_RCSID`] : ID RenderingControl +//! - [`A_ARG_TYPE_AVTRANSPORTID`] : ID AVTransport +//! +//! ## Examples +//! +//! ```rust +//! use pmoupnp::mediarenderer::connectionmanager::CONNECTIONMANAGER; +//! +//! // Accéder au service +//! let service = &*CONNECTIONMANAGER; +//! println!("Service: {}", service.name()); +//! println!("Type: {}", service.service_type()); +//! ``` +//! +//! ## Références +//! +//! - [UPnP ConnectionManager:1 Service Template](https://upnp.org/specs/av/UPnP-av-ConnectionManager-v1-Service.pdf) +//! - [UPnP AV Architecture](https://upnp.org/specs/av/) + +use crate::define_service; + +pub mod variables; +pub mod actions; + +use actions::{GETCURRENTCONNECTIONIDS, GETCURRENTCONNECTIONINFO, GETPROTOCOLINFO}; +use variables::{ + A_ARG_TYPE_AVTRANSPORTID, A_ARG_TYPE_CONNECTIONID, A_ARG_TYPE_CONNECTIONSTATUS, + A_ARG_TYPE_DIRECTION, A_ARG_TYPE_PROTOCOLINFO, A_ARG_TYPE_RCSID, + CURRENTCONNECTIONIDS, SINKPROTOCOLINFO, SOURCEPROTOCOLINFO +}; + +// Service ConnectionManager:1 conforme à la spécification UPnP AV pour MediaRenderer audio +// Voir la documentation du module pour plus de détails +define_service! { + pub static CONNECTIONMANAGER = "ConnectionManager" { + variables: [ + A_ARG_TYPE_AVTRANSPORTID, + A_ARG_TYPE_CONNECTIONID, + A_ARG_TYPE_CONNECTIONSTATUS, + A_ARG_TYPE_DIRECTION, + A_ARG_TYPE_PROTOCOLINFO, + A_ARG_TYPE_RCSID, + CURRENTCONNECTIONIDS, + SINKPROTOCOLINFO, + SOURCEPROTOCOLINFO, + ], + actions: [ + GETCURRENTCONNECTIONIDS, + GETCURRENTCONNECTIONINFO, + GETPROTOCOLINFO, + ] + } +} diff --git a/pmoupnp/src/mediarenderer/connectionmanager/variables/a_arg_type_avtransportid.rs b/pmoupnp/src/mediarenderer/connectionmanager/variables/a_arg_type_avtransportid.rs new file mode 100644 index 00000000..5445904c --- /dev/null +++ b/pmoupnp/src/mediarenderer/connectionmanager/variables/a_arg_type_avtransportid.rs @@ -0,0 +1,5 @@ +use crate::define_variable; + +define_variable! { + pub static A_ARG_TYPE_AVTRANSPORTID: I4 = "A_ARG_TYPE_AVTransportID" +} diff --git a/pmoupnp/src/mediarenderer/connectionmanager/variables/a_arg_type_connectionid.rs b/pmoupnp/src/mediarenderer/connectionmanager/variables/a_arg_type_connectionid.rs new file mode 100644 index 00000000..6349d6b3 --- /dev/null +++ b/pmoupnp/src/mediarenderer/connectionmanager/variables/a_arg_type_connectionid.rs @@ -0,0 +1,5 @@ +use crate::define_variable; + +define_variable! { + pub static A_ARG_TYPE_CONNECTIONID: I4 = "A_ARG_TYPE_ConnectionID" +} diff --git a/pmoupnp/src/mediarenderer/connectionmanager/variables/a_arg_type_connectionstatus.rs b/pmoupnp/src/mediarenderer/connectionmanager/variables/a_arg_type_connectionstatus.rs new file mode 100644 index 00000000..402a7736 --- /dev/null +++ b/pmoupnp/src/mediarenderer/connectionmanager/variables/a_arg_type_connectionstatus.rs @@ -0,0 +1,7 @@ +use crate::define_variable; + +define_variable! { + pub static A_ARG_TYPE_CONNECTIONSTATUS: String = "A_ARG_TYPE_ConnectionStatus" { + allowed: ["OK", "ContentFormatMismatch", "InsufficientBandwidth", "UnreliableChannel", "Unknown"], + } +} diff --git a/pmoupnp/src/mediarenderer/connectionmanager/variables/a_arg_type_direction.rs b/pmoupnp/src/mediarenderer/connectionmanager/variables/a_arg_type_direction.rs new file mode 100644 index 00000000..8604577b --- /dev/null +++ b/pmoupnp/src/mediarenderer/connectionmanager/variables/a_arg_type_direction.rs @@ -0,0 +1,7 @@ +use crate::define_variable; + +define_variable! { + pub static A_ARG_TYPE_DIRECTION: String = "A_ARG_TYPE_Direction" { + allowed: ["Input", "Output"], + } +} diff --git a/pmoupnp/src/mediarenderer/connectionmanager/variables/a_arg_type_protocolinfo.rs b/pmoupnp/src/mediarenderer/connectionmanager/variables/a_arg_type_protocolinfo.rs new file mode 100644 index 00000000..1d19b80c --- /dev/null +++ b/pmoupnp/src/mediarenderer/connectionmanager/variables/a_arg_type_protocolinfo.rs @@ -0,0 +1,5 @@ +use crate::define_variable; + +define_variable! { + pub static A_ARG_TYPE_PROTOCOLINFO: String = "A_ARG_TYPE_ProtocolInfo" +} diff --git a/pmoupnp/src/mediarenderer/connectionmanager/variables/a_arg_type_rcsid.rs b/pmoupnp/src/mediarenderer/connectionmanager/variables/a_arg_type_rcsid.rs new file mode 100644 index 00000000..ba902a0a --- /dev/null +++ b/pmoupnp/src/mediarenderer/connectionmanager/variables/a_arg_type_rcsid.rs @@ -0,0 +1,5 @@ +use crate::define_variable; + +define_variable! { + pub static A_ARG_TYPE_RCSID: I4 = "A_ARG_TYPE_RcsID" +} diff --git a/pmoupnp/src/mediarenderer/connectionmanager/variables/currentconnectionids.rs b/pmoupnp/src/mediarenderer/connectionmanager/variables/currentconnectionids.rs new file mode 100644 index 00000000..afe28709 --- /dev/null +++ b/pmoupnp/src/mediarenderer/connectionmanager/variables/currentconnectionids.rs @@ -0,0 +1,7 @@ +use crate::define_variable; + +define_variable! { + pub static CURRENTCONNECTIONIDS: String = "CurrentConnectionIDs" { + evented: true, + } +} diff --git a/pmoupnp/src/mediarenderer/connectionmanager/variables/mod.rs b/pmoupnp/src/mediarenderer/connectionmanager/variables/mod.rs new file mode 100644 index 00000000..4bc47ea4 --- /dev/null +++ b/pmoupnp/src/mediarenderer/connectionmanager/variables/mod.rs @@ -0,0 +1,19 @@ +mod sourceprotocolinfo; +mod sinkprotocolinfo; +mod currentconnectionids; +mod a_arg_type_connectionid; +mod a_arg_type_connectionstatus; +mod a_arg_type_direction; +mod a_arg_type_protocolinfo; +mod a_arg_type_rcsid; +mod a_arg_type_avtransportid; + +pub use sourceprotocolinfo::SOURCEPROTOCOLINFO; +pub use sinkprotocolinfo::SINKPROTOCOLINFO; +pub use currentconnectionids::CURRENTCONNECTIONIDS; +pub use a_arg_type_connectionid::A_ARG_TYPE_CONNECTIONID; +pub use a_arg_type_connectionstatus::A_ARG_TYPE_CONNECTIONSTATUS; +pub use a_arg_type_direction::A_ARG_TYPE_DIRECTION; +pub use a_arg_type_protocolinfo::A_ARG_TYPE_PROTOCOLINFO; +pub use a_arg_type_rcsid::A_ARG_TYPE_RCSID; +pub use a_arg_type_avtransportid::A_ARG_TYPE_AVTRANSPORTID; diff --git a/pmoupnp/src/mediarenderer/connectionmanager/variables/sinkprotocolinfo.rs b/pmoupnp/src/mediarenderer/connectionmanager/variables/sinkprotocolinfo.rs new file mode 100644 index 00000000..5022c92a --- /dev/null +++ b/pmoupnp/src/mediarenderer/connectionmanager/variables/sinkprotocolinfo.rs @@ -0,0 +1,8 @@ +use crate::define_variable; + +// Pour un MediaRenderer audio, liste les protocoles/formats audio supportés +define_variable! { + pub static SINKPROTOCOLINFO: String = "SinkProtocolInfo" { + evented: true, + } +} diff --git a/pmoupnp/src/mediarenderer/connectionmanager/variables/sourceprotocolinfo.rs b/pmoupnp/src/mediarenderer/connectionmanager/variables/sourceprotocolinfo.rs new file mode 100644 index 00000000..f182d9ac --- /dev/null +++ b/pmoupnp/src/mediarenderer/connectionmanager/variables/sourceprotocolinfo.rs @@ -0,0 +1,7 @@ +use crate::define_variable; + +define_variable! { + pub static SOURCEPROTOCOLINFO: String = "SourceProtocolInfo" { + evented: true, + } +} diff --git a/pmoupnp/src/mediarenderer/mod.rs b/pmoupnp/src/mediarenderer/mod.rs index bdbc0780..7f8e06b6 100644 --- a/pmoupnp/src/mediarenderer/mod.rs +++ b/pmoupnp/src/mediarenderer/mod.rs @@ -1,2 +1,3 @@ pub mod avtransport; +pub mod connectionmanager; pub mod renderingcontrol; diff --git a/pmoupnp/src/mediarenderer/renderingcontrol/mod.rs b/pmoupnp/src/mediarenderer/renderingcontrol/mod.rs index 4c441233..5d711521 100644 --- a/pmoupnp/src/mediarenderer/renderingcontrol/mod.rs +++ b/pmoupnp/src/mediarenderer/renderingcontrol/mod.rs @@ -57,30 +57,8 @@ pub mod actions; use actions::{GETMUTE, GETVOLUME, SETMUTE, SETVOLUME}; use variables::{A_ARG_TYPE_CHANNEL, A_ARG_TYPE_INSTANCE_ID, MUTE, VOLUME}; -/// Service RenderingControl:1 conforme à la spécification UPnP AV. -/// -/// Ce service permet de contrôler les paramètres de rendu audio sur un MediaRenderer. -/// -/// # Initialisation -/// -/// Le service est initialisé paresseusement (lazy) au premier accès via la macro -/// [`define_service!`]. Toutes les variables d'état et actions sont automatiquement -/// enregistrées. -/// -/// # Exemples -/// -/// ```rust -/// use pmoupnp::mediarenderer::renderingcontrol::RENDERINGCONTROL; -/// -/// let service = &*RENDERINGCONTROL; -/// assert_eq!(service.name(), "RenderingControl"); -/// assert_eq!(service.version(), 1); -/// ``` -/// -/// # Voir aussi -/// -/// - Module [`actions`] : Définition de toutes les actions UPnP -/// - Module [`variables`] : Définition de toutes les variables d'état +// Service RenderingControl:1 conforme à la spécification UPnP AV pour MediaRenderer audio +// Voir la documentation du module pour plus de détails define_service! { pub static RENDERINGCONTROL = "RenderingControl" { variables: [ diff --git a/pmoupnp/webapp/.gitignore b/pmoupnp/webapp/.gitignore index a547bf36..fe96e07f 100644 --- a/pmoupnp/webapp/.gitignore +++ b/pmoupnp/webapp/.gitignore @@ -1,4 +1,3 @@ -# Logs logs *.log npm-debug.log*