From 11cfd3e27fb7fac574975a63ca1ffa34059c7309 Mon Sep 17 00:00:00 2001 From: Eric Coissac Date: Sun, 5 Oct 2025 21:58:36 +0200 Subject: [PATCH] lastest clean version --- pmoupnp/src/devices/device.rs | 309 +++++++++++++++++++++ pmoupnp/src/devices/device_instance.rs | 330 +++++++++++++++++++++++ pmoupnp/src/devices/device_methods.rs | 118 ++++++++ pmoupnp/src/devices/errors.rs | 23 ++ pmoupnp/src/devices/mod.rs | 40 +++ pmoupnp/src/lib.rs | 1 + pmoupnp/src/mediarenderer/device.rs | 69 +++++ pmoupnp/src/mediarenderer/mod.rs | 32 +++ pmoupnp/src/services/mod.rs | 6 +- pmoupnp/src/services/service_instance.rs | 102 ++++--- pmoupnp/src/services/service_methods.rs | 8 +- 11 files changed, 976 insertions(+), 62 deletions(-) create mode 100644 pmoupnp/src/devices/device.rs create mode 100644 pmoupnp/src/devices/device_instance.rs create mode 100644 pmoupnp/src/devices/device_methods.rs create mode 100644 pmoupnp/src/devices/errors.rs create mode 100644 pmoupnp/src/mediarenderer/device.rs diff --git a/pmoupnp/src/devices/device.rs b/pmoupnp/src/devices/device.rs new file mode 100644 index 00000000..eca7e7bc --- /dev/null +++ b/pmoupnp/src/devices/device.rs @@ -0,0 +1,309 @@ +//! Définition du modèle Device UPnP. + +use std::sync::{Arc, RwLock}; +use std::collections::HashMap; + +use crate::{ + UpnpTyped, UpnpObjectType, + services::Service, +}; + +use super::errors::DeviceError; + +/// Modèle d'un device UPnP. +/// +/// Représente la définition d'un device selon la spécification UPnP Device Architecture. +/// Un device peut contenir plusieurs services et éventuellement des sous-devices. +#[derive(Debug)] +pub struct Device { + /// Métadonnées de l'objet + object: UpnpObjectType, + + /// Type de device UPnP (ex: "MediaRenderer", "MediaServer") + device_type: String, + + /// Version du device + version: u8, + + /// Nom convivial du device + friendly_name: String, + + /// Fabricant + manufacturer: String, + + /// URL du fabricant + manufacturer_url: Option, + + /// Description du modèle + model_description: Option, + + /// Nom du modèle + model_name: String, + + /// Numéro du modèle + model_number: Option, + + /// URL du modèle + model_url: Option, + + /// Numéro de série + serial_number: Option, + + /// UDN (Unique Device Name) - sera généré à l'instance + udn_prefix: String, + + /// UPC (Universal Product Code) + upc: Option, + + /// URL de l'icône + icon_url: Option, + + /// URL de présentation + presentation_url: Option, + + /// Services du device + services: RwLock>>, + + /// Sous-devices (embedded devices) + devices: RwLock>>, +} + +impl Clone for Device { + fn clone(&self) -> Self { + Self { + object: self.object.clone(), + device_type: self.device_type.clone(), + version: self.version, + friendly_name: self.friendly_name.clone(), + manufacturer: self.manufacturer.clone(), + manufacturer_url: self.manufacturer_url.clone(), + model_description: self.model_description.clone(), + model_name: self.model_name.clone(), + model_number: self.model_number.clone(), + model_url: self.model_url.clone(), + serial_number: self.serial_number.clone(), + udn_prefix: self.udn_prefix.clone(), + upc: self.upc.clone(), + icon_url: self.icon_url.clone(), + presentation_url: self.presentation_url.clone(), + services: RwLock::new(self.services.read().unwrap().clone()), + devices: RwLock::new(self.devices.read().unwrap().clone()), + } + } +} + +impl Device { + /// Crée un nouveau modèle de device. + /// + /// # Arguments + /// + /// * `name` - Nom unique du device + /// * `device_type` - Type UPnP du device + /// * `friendly_name` - Nom convivial pour l'utilisateur + pub fn new(name: String, device_type: String, friendly_name: String) -> Self { + Self { + object: UpnpObjectType { + name: name.clone(), + object_type: "Device".to_string(), + }, + device_type, + version: 1, + friendly_name, + manufacturer: "PMOMusic".to_string(), + manufacturer_url: None, + model_description: None, + model_name: name.clone(), + model_number: None, + model_url: None, + serial_number: None, + udn_prefix: "pmomusic".to_string(), + upc: None, + icon_url: None, + presentation_url: None, + services: RwLock::new(HashMap::new()), + devices: RwLock::new(HashMap::new()), + } + } + + /// Retourne le type de device UPnP. + /// + /// Format: `urn:schemas-upnp-org:device:{type}:{version}` + pub fn device_type(&self) -> String { + format!("urn:schemas-upnp-org:device:{}:{}", self.device_type, self.version) + } + + /// Définit la version du device. + pub fn set_version(&mut self, version: u8) -> Result<(), DeviceError> { + if version == 0 { + return Err(DeviceError::InvalidVersion); + } + self.version = version; + Ok(()) + } + + /// Retourne la version du device. + pub fn version(&self) -> u8 { + self.version + } + + /// Définit le fabricant. + pub fn set_manufacturer(&mut self, manufacturer: String) { + self.manufacturer = manufacturer; + } + + /// Définit l'URL du fabricant. + pub fn set_manufacturer_url(&mut self, url: String) { + self.manufacturer_url = Some(url); + } + + /// Définit la description du modèle. + pub fn set_model_description(&mut self, description: String) { + self.model_description = Some(description); + } + + /// Définit le nom du modèle. + pub fn set_model_name(&mut self, name: String) { + self.model_name = name; + } + + /// Définit le numéro du modèle. + pub fn set_model_number(&mut self, number: String) { + self.model_number = Some(number); + } + + /// Définit le numéro de série. + pub fn set_serial_number(&mut self, serial: String) { + self.serial_number = Some(serial); + } + + /// Définit le préfixe UDN. + pub fn set_udn_prefix(&mut self, prefix: String) { + self.udn_prefix = prefix; + } + + /// Retourne le préfixe UDN. + pub fn udn_prefix(&self) -> &str { + &self.udn_prefix + } + + /// Définit l'URL de présentation. + pub fn set_presentation_url(&mut self, url: String) { + self.presentation_url = Some(url); + } + + /// Ajoute un service au device. + /// + /// # Errors + /// + /// Retourne une erreur si un service avec le même nom existe déjà. + pub fn add_service(&self, service: Arc) -> Result<(), DeviceError> { + let mut services = self.services.write().unwrap(); + let name = service.get_name().to_string(); + + if services.contains_key(&name) { + return Err(DeviceError::ServiceAlreadyExists(name)); + } + + services.insert(name, service); + Ok(()) + } + + /// Retourne tous les services. + pub fn services(&self) -> Vec> { + self.services.read().unwrap().values().cloned().collect() + } + + /// Retourne un service par nom. + pub fn get_service(&self, name: &str) -> Option> { + self.services.read().unwrap().get(name).cloned() + } + + /// Ajoute un sous-device. + /// + /// # Errors + /// + /// Retourne une erreur si un device avec le même nom existe déjà. + pub fn add_device(&self, device: Arc) -> Result<(), DeviceError> { + let mut devices = self.devices.write().unwrap(); + let name = device.get_name().to_string(); + + if devices.contains_key(&name) { + return Err(DeviceError::DeviceAlreadyExists(name)); + } + + devices.insert(name, device); + Ok(()) + } + + /// Retourne tous les sous-devices. + pub fn devices(&self) -> Vec> { + self.devices.read().unwrap().values().cloned().collect() + } + + /// Retourne le nom convivial. + pub fn friendly_name(&self) -> &str { + &self.friendly_name + } + + /// Retourne le fabricant. + pub fn manufacturer(&self) -> &str { + &self.manufacturer + } + + /// Retourne le nom du modèle. + pub fn model_name(&self) -> &str { + &self.model_name + } + + /// Retourne la description du modèle. + pub fn model_description(&self) -> Option<&str> { + self.model_description.as_deref() + } + + /// Retourne l'URL de présentation. + pub fn presentation_url(&self) -> Option<&str> { + self.presentation_url.as_deref() + } + + /// Retourne l'URL du fabricant. + pub fn manufacturer_url(&self) -> Option<&str> { + self.manufacturer_url.as_deref() + } + + /// Retourne le numéro du modèle. + pub fn model_number(&self) -> Option<&str> { + self.model_number.as_deref() + } + + /// Retourne l'URL du modèle. + pub fn model_url(&self) -> Option<&str> { + self.model_url.as_deref() + } + + /// Retourne le numéro de série. + pub fn serial_number(&self) -> Option<&str> { + self.serial_number.as_deref() + } + + /// Retourne l'UPC. + pub fn upc(&self) -> Option<&str> { + self.upc.as_deref() + } + + /// Retourne l'URL de l'icône. + pub fn icon_url(&self) -> Option<&str> { + self.icon_url.as_deref() + } +} + +impl std::fmt::Display for Device { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + write!(f, "Device({}:{})", self.get_name(), self.version) + } +} + +impl UpnpTyped for Device { + fn as_upnp_object_type(&self) -> &UpnpObjectType { + &self.object + } +} diff --git a/pmoupnp/src/devices/device_instance.rs b/pmoupnp/src/devices/device_instance.rs new file mode 100644 index 00000000..229646ce --- /dev/null +++ b/pmoupnp/src/devices/device_instance.rs @@ -0,0 +1,330 @@ +//! Implémentation de DeviceInstance. + +use std::{ + collections::HashMap, + sync::{Arc, RwLock}, +}; +use axum::{ + http::StatusCode, + response::{IntoResponse, Response}, +}; +use tracing::info; +use xmltree::{Element, XMLNode, EmitterConfig}; + +use crate::{ + devices::{Device, errors::DeviceError}, + services::ServiceInstance, + UpnpObject, UpnpInstance, UpnpTyped, UpnpTypedInstance, UpnpObjectType, +}; + +/// Instance d'un device UPnP. +/// +/// Représente une instance concrète d'un device UPnP, avec ses services instanciés +/// et son UDN unique. +pub struct DeviceInstance { + /// Métadonnées de l'objet + object: UpnpObjectType, + + /// Référence vers le modèle + model: Arc, + + /// UDN unique pour cette instance + udn: String, + + /// URL de base du serveur + server_base_url: String, + + /// Instances de services + services: RwLock>>, + + /// Instances de sous-devices + devices: RwLock>>, +} + +impl Clone for DeviceInstance { + fn clone(&self) -> Self { + Self { + object: self.object.clone(), + model: Arc::clone(&self.model), + udn: self.udn.clone(), + server_base_url: self.server_base_url.clone(), + services: RwLock::new(self.services.read().unwrap().clone()), + devices: RwLock::new(self.devices.read().unwrap().clone()), + } + } +} + +impl std::fmt::Debug for DeviceInstance { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("DeviceInstance") + .field("object", &self.object) + .field("udn", &self.udn) + .field("server_base_url", &self.server_base_url) + .field("services", &self.services) + .field("devices", &self.devices) + .finish() + } +} + +impl UpnpTyped for DeviceInstance { + fn as_upnp_object_type(&self) -> &UpnpObjectType { + &self.object + } +} + +impl UpnpInstance for DeviceInstance { + type Model = Device; + + fn new(model: &Device) -> Self { + // Obtenir ou créer un UDN persistant via la configuration + let device_name = model.get_name(); + + let udn = if let Ok(config_udn) = pmoconfig::get_config().get_device_udn("mediarenderer", device_name) { + config_udn + } else { + // Fallback : générer un UDN + tracing::warn!("Failed to get/save UDN from config, using generated UUID"); + format!("uuid:{}_{}", model.udn_prefix(), uuid::Uuid::new_v4()) + }; + + Self { + object: UpnpObjectType { + name: model.get_name().to_string(), + object_type: "DeviceInstance".to_string(), + }, + model: Arc::new(model.clone()), + udn, + server_base_url: "http://localhost:8080".to_string(), + services: RwLock::new(HashMap::new()), + devices: RwLock::new(HashMap::new()), + } + } +} + +impl UpnpTypedInstance for DeviceInstance { + fn get_model(&self) -> &Self::Model { + &self.model + } +} + +impl UpnpObject for DeviceInstance { + fn to_xml_element(&self) -> Element { + let mut elem = Element::new("device"); + + // deviceType + let mut device_type = Element::new("deviceType"); + device_type.children.push(XMLNode::Text(self.model.device_type())); + elem.children.push(XMLNode::Element(device_type)); + + // friendlyName + let mut friendly_name = Element::new("friendlyName"); + friendly_name.children.push(XMLNode::Text(self.model.friendly_name().to_string())); + elem.children.push(XMLNode::Element(friendly_name)); + + // manufacturer + let mut manufacturer = Element::new("manufacturer"); + manufacturer.children.push(XMLNode::Text(self.model.manufacturer().to_string())); + elem.children.push(XMLNode::Element(manufacturer)); + + // modelName + let mut model_name = Element::new("modelName"); + model_name.children.push(XMLNode::Text(self.model.model_name().to_string())); + elem.children.push(XMLNode::Element(model_name)); + + // UDN + let mut udn = Element::new("UDN"); + udn.children.push(XMLNode::Text(self.udn.clone())); + elem.children.push(XMLNode::Element(udn)); + + // serviceList + let services = self.services.read().unwrap(); + if !services.is_empty() { + let mut service_list = Element::new("serviceList"); + for service in services.values() { + service_list.children.push(XMLNode::Element(service.to_xml_element())); + } + elem.children.push(XMLNode::Element(service_list)); + } + + // deviceList (sous-devices) + let devices = self.devices.read().unwrap(); + if !devices.is_empty() { + let mut device_list = Element::new("deviceList"); + for device in devices.values() { + device_list.children.push(XMLNode::Element(device.to_xml_element())); + } + elem.children.push(XMLNode::Element(device_list)); + } + + // presentationURL + if let Some(url) = self.model.presentation_url() { + let mut presentation_url = Element::new("presentationURL"); + presentation_url.children.push(XMLNode::Text(url.to_string())); + elem.children.push(XMLNode::Element(presentation_url)); + } + + elem + } +} + +impl DeviceInstance { + /// Définit l'URL de base du serveur. + pub fn set_server_base_url(&mut self, url: String) { + self.server_base_url = url; + } + + /// Retourne l'UDN du device. + pub fn udn(&self) -> &str { + &self.udn + } + + /// Retourne l'URL de base du serveur (protocole + host + port). + pub fn base_url(&self) -> &str { + &self.server_base_url + } + + /// Retourne la route du device (chemin relatif). + pub fn route(&self) -> String { + format!("/device/{}", self.get_name()) + } + + /// Retourne la route de description du device. + pub fn description_route(&self) -> String { + format!("{}/desc.xml", self.route()) + } + + /// Ajoute une instance de service au device. + /// + /// Cette méthode configure automatiquement le service pour qu'il connaisse + /// son device parent. + /// + /// # Errors + /// + /// Retourne une erreur si un service avec le même nom existe déjà. + pub fn add_service(self: &Arc, service: Arc) -> Result<(), DeviceError> { + let mut services = self.services.write().unwrap(); + let name = service.get_name().to_string(); + + if services.contains_key(&name) { + return Err(DeviceError::ServiceAlreadyExists(name)); + } + + // Configurer le service pour qu'il connaisse son device parent + service.set_device(Arc::clone(self)); + + services.insert(name, service); + Ok(()) + } + + /// Retourne tous les services. + pub fn services(&self) -> Vec> { + self.services.read().unwrap().values().cloned().collect() + } + + /// Retourne un service par nom. + pub fn get_service(&self, name: &str) -> Option> { + self.services.read().unwrap().get(name).cloned() + } + + /// Ajoute une instance de sous-device. + pub fn add_device(&self, device: Arc) -> Result<(), DeviceError> { + let mut devices = self.devices.write().unwrap(); + let name = device.get_name().to_string(); + + if devices.contains_key(&name) { + return Err(DeviceError::DeviceAlreadyExists(name)); + } + + devices.insert(name, device); + Ok(()) + } + + /// Retourne tous les sous-devices. + pub fn devices(&self) -> Vec> { + self.devices.read().unwrap().values().cloned().collect() + } + + /// Enregistre toutes les URLs du device et de ses services dans le serveur. + pub fn register_urls<'a>(&'a self, server: &'a mut crate::server::Server) -> std::pin::Pin> + 'a>> { + Box::pin(async move { + info!( + "✅ Device description for {} available at: {}{}", + self.get_name(), + self.base_url(), + self.description_route(), + ); + + // Handler pour la description du device + let instance_desc = self.clone(); + server.add_handler(&self.description_route(), move || { + let instance = instance_desc.clone(); + async move { instance.description_handler().await } + }).await; + + // Enregistrer les services + for service in self.services() { + service.register_urls(server).await + .map_err(|e| DeviceError::UrlRegistrationError(e.to_string()))?; + } + + // Enregistrer les sous-devices + for device in self.devices() { + device.register_urls(server).await?; + } + + Ok(()) + }) + } + + /// Génère l'élément XML de description du device. + pub fn description_element(&self) -> Element { + let mut root = Element::new("root"); + root.attributes.insert( + "xmlns".to_string(), + "urn:schemas-upnp-org:device-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)); + + root.children.push(XMLNode::Element(spec)); + + // device + root.children.push(XMLNode::Element(self.to_xml_element())); + + root + } + + /// Handler HTTP pour la description du device. + async fn description_handler(&self) -> Response { + let elem = self.description_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) { + tracing::error!("Failed to serialize device description 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() + } +} diff --git a/pmoupnp/src/devices/device_methods.rs b/pmoupnp/src/devices/device_methods.rs new file mode 100644 index 00000000..96fd9b3b --- /dev/null +++ b/pmoupnp/src/devices/device_methods.rs @@ -0,0 +1,118 @@ +//! Implémentation des traits UPnP pour Device. + +use xmltree::{Element, XMLNode}; + +use crate::{ + devices::{Device, DeviceInstance}, + UpnpObject, UpnpModel, +}; + +impl UpnpObject for Device { + fn to_xml_element(&self) -> Element { + let mut elem = Element::new("device"); + + // deviceType + let mut device_type = Element::new("deviceType"); + device_type.children.push(XMLNode::Text(self.device_type())); + elem.children.push(XMLNode::Element(device_type)); + + // friendlyName + let mut friendly_name = Element::new("friendlyName"); + friendly_name.children.push(XMLNode::Text(self.friendly_name().to_string())); + elem.children.push(XMLNode::Element(friendly_name)); + + // manufacturer + let mut manufacturer = Element::new("manufacturer"); + manufacturer.children.push(XMLNode::Text(self.manufacturer().to_string())); + elem.children.push(XMLNode::Element(manufacturer)); + + // manufacturerURL (optionnel) + if let Some(url) = self.manufacturer_url() { + let mut manufacturer_url = Element::new("manufacturerURL"); + manufacturer_url.children.push(XMLNode::Text(url.to_string())); + elem.children.push(XMLNode::Element(manufacturer_url)); + } + + // modelDescription (optionnel) + if let Some(desc) = self.model_description() { + let mut model_description = Element::new("modelDescription"); + model_description.children.push(XMLNode::Text(desc.to_string())); + elem.children.push(XMLNode::Element(model_description)); + } + + // modelName + let mut model_name = Element::new("modelName"); + model_name.children.push(XMLNode::Text(self.model_name().to_string())); + elem.children.push(XMLNode::Element(model_name)); + + // modelNumber (optionnel) + if let Some(number) = self.model_number() { + let mut model_number = Element::new("modelNumber"); + model_number.children.push(XMLNode::Text(number.to_string())); + elem.children.push(XMLNode::Element(model_number)); + } + + // modelURL (optionnel) + if let Some(url) = self.model_url() { + let mut model_url = Element::new("modelURL"); + model_url.children.push(XMLNode::Text(url.to_string())); + elem.children.push(XMLNode::Element(model_url)); + } + + // serialNumber (optionnel) + if let Some(serial) = self.serial_number() { + let mut serial_number = Element::new("serialNumber"); + serial_number.children.push(XMLNode::Text(serial.to_string())); + elem.children.push(XMLNode::Element(serial_number)); + } + + // UPC (optionnel) + if let Some(upc) = self.upc() { + let mut upc_elem = Element::new("UPC"); + upc_elem.children.push(XMLNode::Text(upc.to_string())); + elem.children.push(XMLNode::Element(upc_elem)); + } + + // iconList (optionnel) + if let Some(icon_url) = self.icon_url() { + let mut icon_list = Element::new("iconList"); + let mut icon = Element::new("icon"); + + let mut mimetype = Element::new("mimetype"); + mimetype.children.push(XMLNode::Text("image/png".to_string())); + icon.children.push(XMLNode::Element(mimetype)); + + let mut width = Element::new("width"); + width.children.push(XMLNode::Text("48".to_string())); + icon.children.push(XMLNode::Element(width)); + + let mut height = Element::new("height"); + height.children.push(XMLNode::Text("48".to_string())); + icon.children.push(XMLNode::Element(height)); + + let mut depth = Element::new("depth"); + depth.children.push(XMLNode::Text("24".to_string())); + icon.children.push(XMLNode::Element(depth)); + + let mut url = Element::new("url"); + url.children.push(XMLNode::Text(icon_url.to_string())); + icon.children.push(XMLNode::Element(url)); + + icon_list.children.push(XMLNode::Element(icon)); + elem.children.push(XMLNode::Element(icon_list)); + } + + // presentationURL (optionnel) + if let Some(url) = self.presentation_url() { + let mut presentation_url = Element::new("presentationURL"); + presentation_url.children.push(XMLNode::Text(url.to_string())); + elem.children.push(XMLNode::Element(presentation_url)); + } + + elem + } +} + +impl UpnpModel for Device { + type Instance = DeviceInstance; +} diff --git a/pmoupnp/src/devices/errors.rs b/pmoupnp/src/devices/errors.rs new file mode 100644 index 00000000..8d185d7c --- /dev/null +++ b/pmoupnp/src/devices/errors.rs @@ -0,0 +1,23 @@ +//! Erreurs relatives aux devices UPnP. + +use thiserror::Error; + +/// Erreurs liées aux devices UPnP. +#[derive(Error, Debug)] +pub enum DeviceError { + /// Service déjà existant + #[error("Service '{0}' already exists in device")] + ServiceAlreadyExists(String), + + /// Device déjà existant + #[error("Device '{0}' already exists")] + DeviceAlreadyExists(String), + + /// Version invalide + #[error("Device version must be > 0")] + InvalidVersion, + + /// Erreur d'enregistrement d'URL + #[error("Failed to register URL: {0}")] + UrlRegistrationError(String), +} diff --git a/pmoupnp/src/devices/mod.rs b/pmoupnp/src/devices/mod.rs index e69de29b..fbcaa1ad 100644 --- a/pmoupnp/src/devices/mod.rs +++ b/pmoupnp/src/devices/mod.rs @@ -0,0 +1,40 @@ +//! Module pour les devices UPnP. +//! +//! Ce module fournit les structures et fonctionnalites pour creer et gerer +//! des devices UPnP selon la specification UPnP Device Architecture. +//! +//! # Architecture +//! +//! - [`Device`] : Modele d'un device UPnP +//! - [`DeviceInstance`] : Instance concrete d'un device +//! - [`DeviceError`](errors::DeviceError) : Erreurs liees aux devices +//! +//! # Exemple +//! +//! ```ignore +//! use pmoupnp::devices::Device; +//! use pmoupnp::services::Service; +//! use std::sync::Arc; +//! +//! // Creer un device MediaRenderer +//! let mut device = Device::new( +//! "MediaRenderer".to_string(), +//! "MediaRenderer".to_string(), +//! "PMOMusic Renderer".to_string() +//! ); +//! +//! // Ajouter des services +//! let avtransport = Arc::new(Service::new("AVTransport".to_string())); +//! device.add_service(avtransport).unwrap(); +//! +//! // Creer une instance +//! let instance = device.create_instance(); +//! ``` + +mod device; +mod device_instance; +mod device_methods; +pub mod errors; + +pub use device::Device; +pub use device_instance::DeviceInstance; diff --git a/pmoupnp/src/lib.rs b/pmoupnp/src/lib.rs index c3b74694..a193cf52 100644 --- a/pmoupnp/src/lib.rs +++ b/pmoupnp/src/lib.rs @@ -2,6 +2,7 @@ mod object_trait; mod object_set; pub mod actions; +pub mod devices; pub mod mediarenderer; pub mod server; pub mod services; diff --git a/pmoupnp/src/mediarenderer/device.rs b/pmoupnp/src/mediarenderer/device.rs new file mode 100644 index 00000000..016fe6e7 --- /dev/null +++ b/pmoupnp/src/mediarenderer/device.rs @@ -0,0 +1,69 @@ +//! Définition du device MediaRenderer. + +use once_cell::sync::Lazy; +use std::sync::Arc; + +use crate::{ + devices::Device, + mediarenderer::{ + avtransport::AVTTRANSPORT, + renderingcontrol::RENDERINGCONTROL, + connectionmanager::CONNECTIONMANAGER, + }, +}; + +/// Device MediaRenderer UPnP. +/// +/// MediaRenderer audio-only conforme UPnP AV Architecture 1.0. +/// +/// # Services inclus +/// +/// - **AVTransport:1** : Contrôle de la lecture +/// - **RenderingControl:1** : Contrôle du volume et du mute +/// - **ConnectionManager:1** : Gestion des connexions +/// +/// # Spécifications +/// +/// - Device Type : `urn:schemas-upnp-org:device:MediaRenderer:1` +/// - Version : 1 +/// - Manufacturer : PMOMusic +/// - Model : PMOMusic Audio Renderer +/// +/// # Exemple +/// +/// ```ignore +/// use pmoupnp::mediarenderer::MEDIA_RENDERER; +/// use pmoupnp::UpnpModel; +/// +/// // Créer une instance du renderer +/// let renderer_instance = MEDIA_RENDERER.create_instance(); +/// +/// // Accéder aux services +/// if let Some(avtransport) = renderer_instance.get_service("AVTransport") { +/// // Contrôler la lecture... +/// } +/// ``` +pub static MEDIA_RENDERER: Lazy> = Lazy::new(|| { + let mut device = Device::new( + "MediaRenderer".to_string(), + "MediaRenderer".to_string(), + "PMOMusic Audio Renderer".to_string(), + ); + + device.set_manufacturer("PMOMusic".to_string()); + device.set_model_name("PMOMusic Audio Renderer".to_string()); + device.set_model_description("UPnP AV MediaRenderer for audio streaming".to_string()); + device.set_udn_prefix("pmomusic".to_string()); + + // Ajouter les trois services obligatoires + device.add_service(Arc::clone(&AVTTRANSPORT)) + .expect("Failed to add AVTransport service"); + + device.add_service(Arc::clone(&RENDERINGCONTROL)) + .expect("Failed to add RenderingControl service"); + + device.add_service(Arc::clone(&CONNECTIONMANAGER)) + .expect("Failed to add ConnectionManager service"); + + Arc::new(device) +}); diff --git a/pmoupnp/src/mediarenderer/mod.rs b/pmoupnp/src/mediarenderer/mod.rs index 7f8e06b6..f3730436 100644 --- a/pmoupnp/src/mediarenderer/mod.rs +++ b/pmoupnp/src/mediarenderer/mod.rs @@ -1,3 +1,35 @@ +//! Module MediaRenderer UPnP. +//! +//! Ce module implémente un MediaRenderer UPnP audio-only conforme à la spécification +//! UPnP AV Architecture. Un MediaRenderer permet de recevoir et lire du contenu audio +//! depuis un serveur UPnP (MediaServer). +//! +//! # Architecture +//! +//! Le MediaRenderer est composé de trois services obligatoires : +//! +//! - **AVTransport** : Contrôle de la lecture (play, pause, stop, seek, etc.) +//! - **RenderingControl** : Contrôle du volume et du mute +//! - **ConnectionManager** : Gestion des connexions et des protocoles supportés +//! +//! # Device UPnP +//! +//! - Type : `urn:schemas-upnp-org:device:MediaRenderer:1` +//! - Services : AVTransport:1, RenderingControl:1, ConnectionManager:1 +//! +//! # Utilisation +//! +//! ```ignore +//! use pmoupnp::mediarenderer::MEDIA_RENDERER; +//! +//! // Le device est déjà configuré avec tous ses services +//! let renderer = MEDIA_RENDERER.clone(); +//! let instance = renderer.create_instance(); +//! ``` + pub mod avtransport; pub mod connectionmanager; pub mod renderingcontrol; +pub mod device; + +pub use device::MEDIA_RENDERER; diff --git a/pmoupnp/src/services/mod.rs b/pmoupnp/src/services/mod.rs index dda54f64..cd22ad46 100644 --- a/pmoupnp/src/services/mod.rs +++ b/pmoupnp/src/services/mod.rs @@ -400,15 +400,15 @@ impl Service { format!("/service/{}", self.name()) } - pub fn scpd_url(&self) -> String { + pub fn scpd_route(&self) -> String { format!("{}/desc.xml", self.service_base_url()) } - pub fn control_url(&self) -> String { + pub fn control_route(&self) -> String { format!("{}/control", self.service_base_url()) } - pub fn event_url(&self) -> String { + pub fn event_route(&self) -> String { format!("{}/event", self.service_base_url()) } diff --git a/pmoupnp/src/services/service_instance.rs b/pmoupnp/src/services/service_instance.rs index c1345ddf..326d2de7 100644 --- a/pmoupnp/src/services/service_instance.rs +++ b/pmoupnp/src/services/service_instance.rs @@ -19,6 +19,7 @@ use crate::{ services::{Service, ServiceError}, actions::{ActionInstance, ActionInstanceSet}, state_variables::{StateVarInstance, StateVarInstanceSet, UpnpVariable}, + devices::DeviceInstance, UpnpObject, UpnpInstance, UpnpTyped, UpnpTypedInstance, UpnpObjectType, }; @@ -74,8 +75,8 @@ pub struct ServiceInstance { /// Identifiant du service identifier: String, - /// Device parent (optionnel) - device: Option>, + /// Device parent (optionnel) - utilisé via interior mutability + device: Arc>>>, /// Variables d'état instanciées statevariables: StateVarInstanceSet, @@ -93,31 +94,6 @@ pub struct ServiceInstance { 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 { @@ -195,7 +171,7 @@ impl UpnpInstance for ServiceInstance { }, model: Arc::new(model.clone()), identifier: model.identifier().to_string(), - device: None, + device: Arc::new(RwLock::new(None)), statevariables, actions, subscribers: Arc::new(RwLock::new(HashMap::new())), @@ -224,15 +200,15 @@ impl UpnpObject for ServiceInstance { elem.children.push(XMLNode::Element(service_id)); let mut scpd_url = Element::new("SCPDURL"); - scpd_url.children.push(XMLNode::Text(self.scpd_url())); + scpd_url.children.push(XMLNode::Text(self.scpd_route())); elem.children.push(XMLNode::Element(scpd_url)); let mut control_url = Element::new("controlURL"); - control_url.children.push(XMLNode::Text(self.control_url())); + control_url.children.push(XMLNode::Text(self.control_route())); 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())); + event_sub_url.children.push(XMLNode::Text(self.event_route())); elem.children.push(XMLNode::Element(event_sub_url)); elem @@ -269,32 +245,43 @@ impl ServiceInstance { self.actions.get_by_name(name) } - /// 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()), + /// Définit le device parent pour ce service. + /// + /// Cette méthode doit être appelée après la création du service instance + /// pour établir la relation avec le device parent. + pub fn set_device(&self, device: Arc) { + let mut dev = self.device.write().unwrap(); + *dev = Some(device); + } + + /// Retourne la route du service (chemin relatif). + pub fn route(&self) -> String { + let device = self.device.read().unwrap(); + match device.as_ref() { + Some(device) => format!("{}/service/{}", device.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 la route de contrôle SOAP. + pub fn control_route(&self) -> String { + format!("{}/control", self.route()) } - /// Retourne l'URL de souscription aux événements. - pub fn event_sub_url(&self) -> String { - format!("{}/event", self.base_route()) + /// Retourne la route de souscription aux événements. + pub fn event_route(&self) -> String { + format!("{}/event", self.route()) } - /// Retourne l'URL de la description SCPD. - pub fn scpd_url(&self) -> String { - format!("{}/desc.xml", self.base_route()) + /// Retourne la route de la description SCPD. + pub fn scpd_route(&self) -> String { + format!("{}/desc.xml", self.route()) } /// Retourne l'USN (Unique Service Name). pub fn usn(&self) -> String { - match &self.device { + let device = self.device.read().unwrap(); + match device.as_ref() { Some(device) => format!("uuid:{}::urn:{}", device.udn(), self.service_type()), None => format!("uuid::urn:{}", self.service_type()), } @@ -316,17 +303,22 @@ impl ServiceInstance { /// /// Retourne une erreur si l'enregistrement des routes échoue. pub async fn register_urls(&self, server: &mut crate::server::Server) -> Result<(), ServiceError> { + let device = self.device.read().unwrap(); + let device_name = device.as_ref().map(|d| d.get_name().clone()).unwrap_or_else(|| "unknown".to_string()); + let server_url = device.as_ref().map(|d| d.base_url().to_string()).unwrap_or_default(); + drop(device); + info!( "✅ Service description for {}:{} available at : {}{}", - self.device.as_ref().map(|d| d.name()).unwrap_or("unknown"), + device_name, self.get_name(), - self.device.as_ref().map(|d| d.server_base_url()).unwrap_or_default(), - self.scpd_url(), + server_url, + self.scpd_route(), ); // Handler SCPD let instance_scpd = self.clone(); - server.add_handler(&self.scpd_url(), move || { + server.add_handler(&self.scpd_route(), move || { let instance = instance_scpd.clone(); async move { instance.scpd_handler().await } }).await; @@ -334,7 +326,7 @@ impl ServiceInstance { // Handler control let instance_control = self.clone(); server.add_post_handler_with_state( - &self.control_url(), + &self.control_route(), control_handler, instance_control, ).await; @@ -342,7 +334,7 @@ impl ServiceInstance { // Handler événements let instance_event = self.clone(); server.add_handler_with_state( - &self.event_sub_url(), + &self.event_route(), event_sub_handler, instance_event, ).await; @@ -690,10 +682,10 @@ mod tests { 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"); + assert_eq!(instance.route(), "/service/AVTransport"); + assert_eq!(instance.control_route(), "/service/AVTransport/control"); + assert_eq!(instance.event_route(), "/service/AVTransport/event"); + assert_eq!(instance.scpd_route(), "/service/AVTransport/desc.xml"); } #[test] diff --git a/pmoupnp/src/services/service_methods.rs b/pmoupnp/src/services/service_methods.rs index ec2c88a7..ec75cfb7 100644 --- a/pmoupnp/src/services/service_methods.rs +++ b/pmoupnp/src/services/service_methods.rs @@ -35,17 +35,17 @@ impl UpnpObject for Service { // SCPDURL let mut SCPDURL = Element::new("SCPDURL"); - SCPDURL.children.push(XMLNode::Text(self.scpd_url())); + SCPDURL.children.push(XMLNode::Text(self.scpd_route())); elem.children.push(XMLNode::Element(SCPDURL)); // controlURL let mut controlURL = Element::new("controlURL"); - controlURL.children.push(XMLNode::Text(self.control_url())); + controlURL.children.push(XMLNode::Text(self.control_route())); elem.children.push(XMLNode::Element(controlURL)); - // SCPDURL + // eventSubURL let mut eventSubURL = Element::new("eventSubURL"); - eventSubURL.children.push(XMLNode::Text(self.event_url())); + eventSubURL.children.push(XMLNode::Text(self.event_route())); elem.children.push(XMLNode::Element(eventSubURL)); elem