lastest clean version
This commit is contained in:
309
pmoupnp/src/devices/device.rs
Normal file
309
pmoupnp/src/devices/device.rs
Normal file
@@ -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<String>,
|
||||||
|
|
||||||
|
/// Description du modèle
|
||||||
|
model_description: Option<String>,
|
||||||
|
|
||||||
|
/// Nom du modèle
|
||||||
|
model_name: String,
|
||||||
|
|
||||||
|
/// Numéro du modèle
|
||||||
|
model_number: Option<String>,
|
||||||
|
|
||||||
|
/// URL du modèle
|
||||||
|
model_url: Option<String>,
|
||||||
|
|
||||||
|
/// Numéro de série
|
||||||
|
serial_number: Option<String>,
|
||||||
|
|
||||||
|
/// UDN (Unique Device Name) - sera généré à l'instance
|
||||||
|
udn_prefix: String,
|
||||||
|
|
||||||
|
/// UPC (Universal Product Code)
|
||||||
|
upc: Option<String>,
|
||||||
|
|
||||||
|
/// URL de l'icône
|
||||||
|
icon_url: Option<String>,
|
||||||
|
|
||||||
|
/// URL de présentation
|
||||||
|
presentation_url: Option<String>,
|
||||||
|
|
||||||
|
/// Services du device
|
||||||
|
services: RwLock<HashMap<String, Arc<Service>>>,
|
||||||
|
|
||||||
|
/// Sous-devices (embedded devices)
|
||||||
|
devices: RwLock<HashMap<String, Arc<Device>>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
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<Service>) -> 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<Arc<Service>> {
|
||||||
|
self.services.read().unwrap().values().cloned().collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Retourne un service par nom.
|
||||||
|
pub fn get_service(&self, name: &str) -> Option<Arc<Service>> {
|
||||||
|
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<Device>) -> 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<Arc<Device>> {
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
330
pmoupnp/src/devices/device_instance.rs
Normal file
330
pmoupnp/src/devices/device_instance.rs
Normal file
@@ -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<Device>,
|
||||||
|
|
||||||
|
/// UDN unique pour cette instance
|
||||||
|
udn: String,
|
||||||
|
|
||||||
|
/// URL de base du serveur
|
||||||
|
server_base_url: String,
|
||||||
|
|
||||||
|
/// Instances de services
|
||||||
|
services: RwLock<HashMap<String, Arc<ServiceInstance>>>,
|
||||||
|
|
||||||
|
/// Instances de sous-devices
|
||||||
|
devices: RwLock<HashMap<String, Arc<DeviceInstance>>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
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<Self>, service: Arc<ServiceInstance>) -> 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<Arc<ServiceInstance>> {
|
||||||
|
self.services.read().unwrap().values().cloned().collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Retourne un service par nom.
|
||||||
|
pub fn get_service(&self, name: &str) -> Option<Arc<ServiceInstance>> {
|
||||||
|
self.services.read().unwrap().get(name).cloned()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Ajoute une instance de sous-device.
|
||||||
|
pub fn add_device(&self, device: Arc<DeviceInstance>) -> 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<Arc<DeviceInstance>> {
|
||||||
|
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<Box<dyn std::future::Future<Output = Result<(), DeviceError>> + '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, "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n");
|
||||||
|
|
||||||
|
(
|
||||||
|
StatusCode::OK,
|
||||||
|
[(axum::http::header::CONTENT_TYPE, "text/xml; charset=\"utf-8\"")],
|
||||||
|
xml,
|
||||||
|
).into_response()
|
||||||
|
}
|
||||||
|
}
|
||||||
118
pmoupnp/src/devices/device_methods.rs
Normal file
118
pmoupnp/src/devices/device_methods.rs
Normal file
@@ -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;
|
||||||
|
}
|
||||||
23
pmoupnp/src/devices/errors.rs
Normal file
23
pmoupnp/src/devices/errors.rs
Normal file
@@ -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),
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ mod object_trait;
|
|||||||
mod object_set;
|
mod object_set;
|
||||||
|
|
||||||
pub mod actions;
|
pub mod actions;
|
||||||
|
pub mod devices;
|
||||||
pub mod mediarenderer;
|
pub mod mediarenderer;
|
||||||
pub mod server;
|
pub mod server;
|
||||||
pub mod services;
|
pub mod services;
|
||||||
|
|||||||
69
pmoupnp/src/mediarenderer/device.rs
Normal file
69
pmoupnp/src/mediarenderer/device.rs
Normal file
@@ -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<Arc<Device>> = 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)
|
||||||
|
});
|
||||||
@@ -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 avtransport;
|
||||||
pub mod connectionmanager;
|
pub mod connectionmanager;
|
||||||
pub mod renderingcontrol;
|
pub mod renderingcontrol;
|
||||||
|
pub mod device;
|
||||||
|
|
||||||
|
pub use device::MEDIA_RENDERER;
|
||||||
|
|||||||
@@ -400,15 +400,15 @@ impl Service {
|
|||||||
format!("/service/{}", self.name())
|
format!("/service/{}", self.name())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn scpd_url(&self) -> String {
|
pub fn scpd_route(&self) -> String {
|
||||||
format!("{}/desc.xml", self.service_base_url())
|
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())
|
format!("{}/control", self.service_base_url())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn event_url(&self) -> String {
|
pub fn event_route(&self) -> String {
|
||||||
format!("{}/event", self.service_base_url())
|
format!("{}/event", self.service_base_url())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ use crate::{
|
|||||||
services::{Service, ServiceError},
|
services::{Service, ServiceError},
|
||||||
actions::{ActionInstance, ActionInstanceSet},
|
actions::{ActionInstance, ActionInstanceSet},
|
||||||
state_variables::{StateVarInstance, StateVarInstanceSet, UpnpVariable},
|
state_variables::{StateVarInstance, StateVarInstanceSet, UpnpVariable},
|
||||||
|
devices::DeviceInstance,
|
||||||
UpnpObject, UpnpInstance, UpnpTyped, UpnpTypedInstance, UpnpObjectType,
|
UpnpObject, UpnpInstance, UpnpTyped, UpnpTypedInstance, UpnpObjectType,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -74,8 +75,8 @@ pub struct ServiceInstance {
|
|||||||
/// Identifiant du service
|
/// Identifiant du service
|
||||||
identifier: String,
|
identifier: String,
|
||||||
|
|
||||||
/// Device parent (optionnel)
|
/// Device parent (optionnel) - utilisé via interior mutability
|
||||||
device: Option<Arc<DeviceStub>>,
|
device: Arc<RwLock<Option<Arc<DeviceInstance>>>>,
|
||||||
|
|
||||||
/// Variables d'état instanciées
|
/// Variables d'état instanciées
|
||||||
statevariables: StateVarInstanceSet,
|
statevariables: StateVarInstanceSet,
|
||||||
@@ -93,31 +94,6 @@ pub struct ServiceInstance {
|
|||||||
seqid: Arc<Mutex<HashMap<String, u32>>>,
|
seqid: Arc<Mutex<HashMap<String, u32>>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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 {
|
impl std::fmt::Debug for ServiceInstance {
|
||||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
@@ -195,7 +171,7 @@ impl UpnpInstance for ServiceInstance {
|
|||||||
},
|
},
|
||||||
model: Arc::new(model.clone()),
|
model: Arc::new(model.clone()),
|
||||||
identifier: model.identifier().to_string(),
|
identifier: model.identifier().to_string(),
|
||||||
device: None,
|
device: Arc::new(RwLock::new(None)),
|
||||||
statevariables,
|
statevariables,
|
||||||
actions,
|
actions,
|
||||||
subscribers: Arc::new(RwLock::new(HashMap::new())),
|
subscribers: Arc::new(RwLock::new(HashMap::new())),
|
||||||
@@ -224,15 +200,15 @@ impl UpnpObject for ServiceInstance {
|
|||||||
elem.children.push(XMLNode::Element(service_id));
|
elem.children.push(XMLNode::Element(service_id));
|
||||||
|
|
||||||
let mut scpd_url = Element::new("SCPDURL");
|
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));
|
elem.children.push(XMLNode::Element(scpd_url));
|
||||||
|
|
||||||
let mut control_url = Element::new("controlURL");
|
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));
|
elem.children.push(XMLNode::Element(control_url));
|
||||||
|
|
||||||
let mut event_sub_url = Element::new("eventSubURL");
|
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.children.push(XMLNode::Element(event_sub_url));
|
||||||
|
|
||||||
elem
|
elem
|
||||||
@@ -269,32 +245,43 @@ impl ServiceInstance {
|
|||||||
self.actions.get_by_name(name)
|
self.actions.get_by_name(name)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Retourne la route de base du service.
|
/// Définit le device parent pour ce service.
|
||||||
pub fn base_route(&self) -> String {
|
///
|
||||||
match &self.device {
|
/// Cette méthode doit être appelée après la création du service instance
|
||||||
Some(device) => format!("{}/service/{}", device.base_route(), self.get_name()),
|
/// pour établir la relation avec le device parent.
|
||||||
|
pub fn set_device(&self, device: Arc<DeviceInstance>) {
|
||||||
|
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()),
|
None => format!("/service/{}", self.get_name()),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Retourne l'URL de contrôle SOAP.
|
/// Retourne la route de contrôle SOAP.
|
||||||
pub fn control_url(&self) -> String {
|
pub fn control_route(&self) -> String {
|
||||||
format!("{}/control", self.base_route())
|
format!("{}/control", self.route())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Retourne l'URL de souscription aux événements.
|
/// Retourne la route de souscription aux événements.
|
||||||
pub fn event_sub_url(&self) -> String {
|
pub fn event_route(&self) -> String {
|
||||||
format!("{}/event", self.base_route())
|
format!("{}/event", self.route())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Retourne l'URL de la description SCPD.
|
/// Retourne la route de la description SCPD.
|
||||||
pub fn scpd_url(&self) -> String {
|
pub fn scpd_route(&self) -> String {
|
||||||
format!("{}/desc.xml", self.base_route())
|
format!("{}/desc.xml", self.route())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Retourne l'USN (Unique Service Name).
|
/// Retourne l'USN (Unique Service Name).
|
||||||
pub fn usn(&self) -> String {
|
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()),
|
Some(device) => format!("uuid:{}::urn:{}", device.udn(), self.service_type()),
|
||||||
None => format!("uuid::urn:{}", 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.
|
/// Retourne une erreur si l'enregistrement des routes échoue.
|
||||||
pub async fn register_urls(&self, server: &mut crate::server::Server) -> Result<(), ServiceError> {
|
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!(
|
info!(
|
||||||
"✅ Service description for {}:{} available at : {}{}",
|
"✅ Service description for {}:{} available at : {}{}",
|
||||||
self.device.as_ref().map(|d| d.name()).unwrap_or("unknown"),
|
device_name,
|
||||||
self.get_name(),
|
self.get_name(),
|
||||||
self.device.as_ref().map(|d| d.server_base_url()).unwrap_or_default(),
|
server_url,
|
||||||
self.scpd_url(),
|
self.scpd_route(),
|
||||||
);
|
);
|
||||||
|
|
||||||
// Handler SCPD
|
// Handler SCPD
|
||||||
let instance_scpd = self.clone();
|
let instance_scpd = self.clone();
|
||||||
server.add_handler(&self.scpd_url(), move || {
|
server.add_handler(&self.scpd_route(), move || {
|
||||||
let instance = instance_scpd.clone();
|
let instance = instance_scpd.clone();
|
||||||
async move { instance.scpd_handler().await }
|
async move { instance.scpd_handler().await }
|
||||||
}).await;
|
}).await;
|
||||||
@@ -334,7 +326,7 @@ impl ServiceInstance {
|
|||||||
// Handler control
|
// Handler control
|
||||||
let instance_control = self.clone();
|
let instance_control = self.clone();
|
||||||
server.add_post_handler_with_state(
|
server.add_post_handler_with_state(
|
||||||
&self.control_url(),
|
&self.control_route(),
|
||||||
control_handler,
|
control_handler,
|
||||||
instance_control,
|
instance_control,
|
||||||
).await;
|
).await;
|
||||||
@@ -342,7 +334,7 @@ impl ServiceInstance {
|
|||||||
// Handler événements
|
// Handler événements
|
||||||
let instance_event = self.clone();
|
let instance_event = self.clone();
|
||||||
server.add_handler_with_state(
|
server.add_handler_with_state(
|
||||||
&self.event_sub_url(),
|
&self.event_route(),
|
||||||
event_sub_handler,
|
event_sub_handler,
|
||||||
instance_event,
|
instance_event,
|
||||||
).await;
|
).await;
|
||||||
@@ -690,10 +682,10 @@ mod tests {
|
|||||||
let service = Service::new("AVTransport".to_string());
|
let service = Service::new("AVTransport".to_string());
|
||||||
let instance = ServiceInstance::new(&service);
|
let instance = ServiceInstance::new(&service);
|
||||||
|
|
||||||
assert_eq!(instance.base_route(), "/service/AVTransport");
|
assert_eq!(instance.route(), "/service/AVTransport");
|
||||||
assert_eq!(instance.control_url(), "/service/AVTransport/control");
|
assert_eq!(instance.control_route(), "/service/AVTransport/control");
|
||||||
assert_eq!(instance.event_sub_url(), "/service/AVTransport/event");
|
assert_eq!(instance.event_route(), "/service/AVTransport/event");
|
||||||
assert_eq!(instance.scpd_url(), "/service/AVTransport/desc.xml");
|
assert_eq!(instance.scpd_route(), "/service/AVTransport/desc.xml");
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|||||||
@@ -35,17 +35,17 @@ impl UpnpObject for Service {
|
|||||||
|
|
||||||
// SCPDURL
|
// SCPDURL
|
||||||
let mut SCPDURL = Element::new("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));
|
elem.children.push(XMLNode::Element(SCPDURL));
|
||||||
|
|
||||||
// controlURL
|
// controlURL
|
||||||
let mut controlURL = Element::new("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));
|
elem.children.push(XMLNode::Element(controlURL));
|
||||||
|
|
||||||
// SCPDURL
|
// eventSubURL
|
||||||
let mut eventSubURL = Element::new("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.children.push(XMLNode::Element(eventSubURL));
|
||||||
|
|
||||||
elem
|
elem
|
||||||
|
|||||||
Reference in New Issue
Block a user