Document pmoupnp::services

This commit is contained in:
2025-10-09 20:26:51 +02:00
parent 22dc0c7374
commit 913da8a5a3
3 changed files with 473 additions and 9 deletions

View File

@@ -396,22 +396,69 @@ impl Service {
format!("urn:schemas-upnp-org:serviceId:{}", self.name())
}
/// Retourne l'URL de base du service.
///
/// Cette méthode est utilisée en interne pour construire les routes du service.
fn service_base_url(&self) -> String {
format!("/service/{}", self.name())
}
/// Retourne la route de la description SCPD.
///
/// # Examples
///
/// ```rust
/// # use pmoupnp::services::Service;
/// let service = Service::new("AVTransport".to_string());
/// assert_eq!(service.scpd_route(), "/service/AVTransport/desc.xml");
/// ```
pub fn scpd_route(&self) -> String {
format!("{}/desc.xml", self.service_base_url())
}
/// Retourne la route de contrôle SOAP.
///
/// # Examples
///
/// ```rust
/// # use pmoupnp::services::Service;
/// let service = Service::new("AVTransport".to_string());
/// assert_eq!(service.control_route(), "/service/AVTransport/control");
/// ```
pub fn control_route(&self) -> String {
format!("{}/control", self.service_base_url())
}
/// Retourne la route de souscription aux événements.
///
/// # Examples
///
/// ```rust
/// # use pmoupnp::services::Service;
/// let service = Service::new("AVTransport".to_string());
/// assert_eq!(service.event_route(), "/service/AVTransport/event");
/// ```
pub fn event_route(&self) -> String {
format!("{}/event", self.service_base_url())
}
/// Génère l'élément XML de la description SCPD (Service Control Protocol Description).
///
/// Cette méthode crée un élément XML conforme à la spécification UPnP qui décrit
/// le service, ses actions et ses variables d'état.
///
/// # Returns
///
/// Un élément `xmltree::Element` représentant le document SCPD.
///
/// # Examples
///
/// ```rust
/// # use pmoupnp::services::Service;
/// let service = Service::new("AVTransport".to_string());
/// let scpd = service.scpd_element();
/// assert_eq!(scpd.name, "scpd");
/// ```
pub fn scpd_element(&self) -> Element {
let mut scpd = Element::new("scpd");
scpd.attributes.insert(
@@ -436,6 +483,27 @@ impl Service {
scpd
}
/// Génère la chaîne XML de la description SCPD.
///
/// Cette méthode produit un document XML complet et formaté décrivant le service
/// selon la spécification UPnP.
///
/// # Returns
///
/// Une chaîne de caractères contenant le XML formaté de la description SCPD.
///
/// # Panics
///
/// Panique si la sérialisation XML échoue ou produit un UTF-8 invalide.
///
/// # Examples
///
/// ```rust
/// # use pmoupnp::services::Service;
/// let service = Service::new("AVTransport".to_string());
/// let xml = service.scpd_xml();
/// assert!(xml.contains("<scpd"));
/// ```
pub fn scpd_xml(&self) -> String {
let elem = self.scpd_element();

View File

@@ -1,4 +1,32 @@
//! Implémentation de ServiceInstance.
//!
//! Ce module contient l'implémentation complète de [`ServiceInstance`],
//! qui représente une instance active d'un service UPnP.
//!
//! # Composants principaux
//!
//! - [`ServiceInstance`] : Structure principale contenant l'état du service
//! - [`event_sub_handler`] : Handler Axum pour les abonnements aux événements
//! - [`control_handler`] : Handler Axum pour les appels SOAP
//!
//! # Gestion des événements
//!
//! L'instance gère automatiquement :
//! - Les souscriptions aux événements (SUBSCRIBE/UNSUBSCRIBE)
//! - L'envoi d'événements initiaux aux nouveaux abonnés
//! - Les notifications périodiques des changements d'état
//! - Le séquençage des messages par abonné
//!
//! # Architecture
//!
//! ```text
//! ServiceInstance
//! ├── Variables d'état (StateVarInstanceSet)
//! ├── Actions (ActionInstanceSet)
//! ├── Abonnés (HashMap<SID, Callback>)
//! ├── Buffer de changements (Mutex<HashMap>)
//! └── Séquences (Mutex<HashMap<SID, u32>>)
//! ```
use axum::{
body::Body,
@@ -224,6 +252,16 @@ impl UpnpObject for ServiceInstance {
impl ServiceInstance {
/// Retourne l'identifiant du service.
///
/// # Examples
///
/// ```rust
/// # use pmoupnp::services::Service;
/// # use pmoupnp::UpnpModel;
/// let service = Service::new("AVTransport".to_string());
/// let instance = service.create_instance();
/// assert_eq!(instance.identifier(), "AVTransport");
/// ```
pub fn identifier(&self) -> &str {
&self.identifier
}
@@ -231,6 +269,16 @@ impl ServiceInstance {
/// Retourne le type de service UPnP.
///
/// Format: `urn:schemas-upnp-org:service:{name}:{version}`
///
/// # Examples
///
/// ```rust
/// # use pmoupnp::services::Service;
/// # use pmoupnp::UpnpModel;
/// let service = Service::new("AVTransport".to_string());
/// let instance = service.create_instance();
/// assert_eq!(instance.service_type(), "urn:schemas-upnp-org:service:AVTransport:1");
/// ```
pub fn service_type(&self) -> String {
self.model.service_type()
}
@@ -238,16 +286,66 @@ impl ServiceInstance {
/// Retourne l'ID de service UPnP.
///
/// Format: `urn:upnp-org:serviceId:{identifier}`
///
/// # Examples
///
/// ```rust
/// # use pmoupnp::services::Service;
/// # use pmoupnp::UpnpModel;
/// let service = Service::new("AVTransport".to_string());
/// let instance = service.create_instance();
/// assert_eq!(instance.service_id(), "urn:upnp-org:serviceId:AVTransport");
/// ```
pub fn service_id(&self) -> String {
format!("urn:upnp-org:serviceId:{}", self.identifier)
}
/// Raccourci pour obtenir une variable d'état par nom
/// Récupère une variable d'état par son nom.
///
/// # Arguments
///
/// * `name` - Nom de la variable d'état
///
/// # Returns
///
/// `Some(Arc<StateVarInstance>)` si la variable existe, `None` sinon.
///
/// # Examples
///
/// ```rust,no_run
/// # use pmoupnp::services::Service;
/// # use pmoupnp::UpnpModel;
/// # let service = Service::new("AVTransport".to_string());
/// # let instance = service.create_instance();
/// if let Some(var) = instance.get_variable("TransportState") {
/// println!("Value: {}", var.value());
/// }
/// ```
pub fn get_variable(&self, name: &str) -> Option<Arc<StateVarInstance>> {
self.statevariables.get_by_name(name)
}
/// Raccourci pour obtenir une action par nom
/// Récupère une action par son nom.
///
/// # Arguments
///
/// * `name` - Nom de l'action
///
/// # Returns
///
/// `Some(Arc<ActionInstance>)` si l'action existe, `None` sinon.
///
/// # Examples
///
/// ```rust,no_run
/// # use pmoupnp::services::Service;
/// # use pmoupnp::UpnpModel;
/// # let service = Service::new("AVTransport".to_string());
/// # let instance = service.create_instance();
/// if let Some(action) = instance.get_action("Play") {
/// println!("Action found: {}", action.get_name());
/// }
/// ```
pub fn get_action(&self, name: &str) -> Option<Arc<ActionInstance>> {
self.actions.get_by_name(name)
}
@@ -256,12 +354,44 @@ impl ServiceInstance {
///
/// Cette méthode doit être appelée après la création du service instance
/// pour établir la relation avec le device parent.
///
/// # Arguments
///
/// * `device` - Le device parent
///
/// # Examples
///
/// ```rust,no_run
/// # use pmoupnp::services::Service;
/// # use pmoupnp::devices::Device;
/// # use pmoupnp::UpnpModel;
/// # use std::sync::Arc;
/// # let service = Service::new("AVTransport".to_string());
/// # let device = Device::new("MediaRenderer".to_string());
/// let service_instance = service.create_instance();
/// let device_instance = Arc::new(device.create_instance());
/// service_instance.set_device(device_instance);
/// ```
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).
///
/// # Returns
///
/// Chemin relatif incluant le device parent si présent.
///
/// # Examples
///
/// ```rust
/// # use pmoupnp::services::Service;
/// # use pmoupnp::UpnpModel;
/// let service = Service::new("AVTransport".to_string());
/// let instance = service.create_instance();
/// assert_eq!(instance.route(), "/service/AVTransport");
/// ```
pub fn route(&self) -> String {
let device = self.device.read().unwrap();
match device.as_ref() {
@@ -271,21 +401,64 @@ impl ServiceInstance {
}
/// Retourne la route de contrôle SOAP.
///
/// # Examples
///
/// ```rust
/// # use pmoupnp::services::Service;
/// # use pmoupnp::UpnpModel;
/// let service = Service::new("AVTransport".to_string());
/// let instance = service.create_instance();
/// assert_eq!(instance.control_route(), "/service/AVTransport/control");
/// ```
pub fn control_route(&self) -> String {
format!("{}/control", self.route())
}
/// Retourne la route de souscription aux événements.
///
/// # Examples
///
/// ```rust
/// # use pmoupnp::services::Service;
/// # use pmoupnp::UpnpModel;
/// let service = Service::new("AVTransport".to_string());
/// let instance = service.create_instance();
/// assert_eq!(instance.event_route(), "/service/AVTransport/event");
/// ```
pub fn event_route(&self) -> String {
format!("{}/event", self.route())
}
/// Retourne la route de la description SCPD.
///
/// # Examples
///
/// ```rust
/// # use pmoupnp::services::Service;
/// # use pmoupnp::UpnpModel;
/// let service = Service::new("AVTransport".to_string());
/// let instance = service.create_instance();
/// assert_eq!(instance.scpd_route(), "/service/AVTransport/desc.xml");
/// ```
pub fn scpd_route(&self) -> String {
format!("{}/desc.xml", self.route())
}
/// Retourne l'USN (Unique Service Name).
///
/// L'USN combine l'UUID du device parent et le type de service UPnP.
///
/// # Examples
///
/// ```rust,no_run
/// # use pmoupnp::services::Service;
/// # use pmoupnp::UpnpModel;
/// let service = Service::new("AVTransport".to_string());
/// let instance = service.create_instance();
/// let usn = instance.usn();
/// // Format: uuid:{device-uuid}::urn:schemas-upnp-org:service:AVTransport:1
/// ```
pub fn usn(&self) -> String {
let device = self.device.read().unwrap();
match device.as_ref() {
@@ -294,12 +467,34 @@ impl ServiceInstance {
}
}
/// Retourne les variables d'état.
/// Retourne une référence vers l'ensemble des variables d'état.
///
/// # Examples
///
/// ```rust
/// # use pmoupnp::services::Service;
/// # use pmoupnp::UpnpModel;
/// let service = Service::new("AVTransport".to_string());
/// let instance = service.create_instance();
/// let vars = instance.statevariables();
/// println!("Variables count: {}", vars.all().len());
/// ```
pub fn statevariables(&self) -> &StateVarInstanceSet {
&self.statevariables
}
/// Retourne les actions.
/// Retourne une référence vers l'ensemble des actions.
///
/// # Examples
///
/// ```rust
/// # use pmoupnp::services::Service;
/// # use pmoupnp::UpnpModel;
/// let service = Service::new("AVTransport".to_string());
/// let instance = service.create_instance();
/// let actions = instance.actions();
/// println!("Actions count: {}", actions.all().len());
/// ```
pub fn actions(&self) -> &ActionInstanceSet {
&self.actions
}
@@ -353,7 +548,25 @@ impl ServiceInstance {
Ok(())
}
/// Génère l'élément XML SCPD.
/// Génère l'élément XML SCPD (Service Control Protocol Description).
///
/// Cette méthode crée un élément XML conforme à la spécification UPnP décrivant
/// le service, ses actions et ses variables d'état.
///
/// # Returns
///
/// Un élément `xmltree::Element` représentant le document SCPD.
///
/// # Examples
///
/// ```rust
/// # use pmoupnp::services::Service;
/// # use pmoupnp::UpnpModel;
/// let service = Service::new("AVTransport".to_string());
/// let instance = service.create_instance();
/// let scpd = instance.scpd_element();
/// assert_eq!(scpd.name, "scpd");
/// ```
pub fn scpd_element(&self) -> Element {
let mut elem = Element::new("scpd");
elem.attributes.insert(
@@ -388,7 +601,19 @@ impl ServiceInstance {
elem
}
/// Handler pour la description SCPD.
/// Handler HTTP pour la description SCPD.
///
/// Génère et retourne le document XML SCPD décrivant le service.
/// Cette méthode est appelée lorsqu'un client accède à l'URL SCPD du service.
///
/// # Returns
///
/// Une réponse HTTP 200 avec le XML SCPD, ou 500 en cas d'erreur de sérialisation.
///
/// # Format de réponse
///
/// - Content-Type: `text/xml; charset="utf-8"`
/// - Body: Document SCPD formaté avec indentation
async fn scpd_handler(&self) -> Response {
let elem = self.scpd_element();
@@ -416,23 +641,100 @@ impl ServiceInstance {
}
/// Ajoute un abonné aux événements.
///
/// # Arguments
///
/// * `sid` - Identifiant de la souscription (SID)
/// * `callback` - URL de callback pour les notifications
///
/// # Examples
///
/// ```rust,no_run
/// # use pmoupnp::services::Service;
/// # use pmoupnp::UpnpModel;
/// # #[tokio::main]
/// # async fn main() {
/// # let service = Service::new("AVTransport".to_string());
/// # let instance = service.create_instance();
/// instance.add_subscriber(
/// "uuid:12345".to_string(),
/// "<http://192.168.1.100:8080/callback>".to_string()
/// ).await;
/// # }
/// ```
pub async fn add_subscriber(&self, sid: String, callback: String) {
let mut subscribers = self.subscribers.write().unwrap();
subscribers.insert(sid, callback);
}
/// Renouvelle un abonnement.
/// Renouvelle un abonnement existant.
///
/// # Arguments
///
/// * `sid` - Identifiant de la souscription (SID)
/// * `timeout` - Nouvelle durée de validité (format "Second-{n}")
///
/// # Examples
///
/// ```rust,no_run
/// # use pmoupnp::services::Service;
/// # use pmoupnp::UpnpModel;
/// # #[tokio::main]
/// # async fn main() {
/// # let service = Service::new("AVTransport".to_string());
/// # let instance = service.create_instance();
/// instance.renew_subscriber("uuid:12345", "Second-1800").await;
/// # }
/// ```
pub async fn renew_subscriber(&self, sid: &str, timeout: &str) {
info!("♻️ Renewed SID {} for timeout {}", sid, timeout);
}
/// Supprime un abonné.
///
/// # Arguments
///
/// * `sid` - Identifiant de la souscription (SID) à supprimer
///
/// # Examples
///
/// ```rust,no_run
/// # use pmoupnp::services::Service;
/// # use pmoupnp::UpnpModel;
/// # #[tokio::main]
/// # async fn main() {
/// # let service = Service::new("AVTransport".to_string());
/// # let instance = service.create_instance();
/// instance.remove_subscriber("uuid:12345").await;
/// # }
/// ```
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é.
///
/// Lorsqu'un client s'abonne aux événements, cette méthode lui envoie
/// immédiatement les valeurs actuelles de toutes les variables d'état
/// qui envoient des notifications.
///
/// # Arguments
///
/// * `sid` - Identifiant de la souscription (SID)
///
/// # Examples
///
/// ```rust,no_run
/// # use pmoupnp::services::Service;
/// # use pmoupnp::UpnpModel;
/// # #[tokio::main]
/// # async fn main() {
/// # let service = Service::new("AVTransport".to_string());
/// # let instance = service.create_instance();
/// instance.send_initial_event("uuid:12345".to_string()).await;
/// # }
/// ```
pub async fn send_initial_event(&self, sid: String) {
let callback = {
let subscribers = self.subscribers.read().unwrap();
@@ -491,13 +793,42 @@ impl ServiceInstance {
}
}
/// Marque un changement à notifier.
/// Marque un changement de variable à notifier ultérieurement.
///
/// Les changements sont mis en buffer et seront envoyés lors du prochain
/// appel à [`notify_subscribers`](Self::notify_subscribers).
///
/// # Arguments
///
/// * `name` - Nom de la variable d'état modifiée
/// * `value` - Nouvelle valeur de la variable
///
/// # Examples
///
/// ```rust
/// # use pmoupnp::services::Service;
/// # use pmoupnp::UpnpModel;
/// let service = Service::new("AVTransport".to_string());
/// let instance = service.create_instance();
/// instance.event_to_be_sent("TransportState".to_string(), "PLAYING".to_string());
/// ```
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é.
///
/// Chaque notification envoyée à un abonné doit avoir un numéro de séquence
/// unique et croissant.
///
/// # Arguments
///
/// * `sid` - Identifiant de la souscription (SID)
///
/// # Returns
///
/// Le prochain numéro de séquence sous forme de chaîne.
fn next_seq(&self, sid: &str) -> String {
let mut seqid = self.seqid.lock().unwrap();
let counter = seqid.entry(sid.to_string()).or_insert(0);
@@ -505,7 +836,25 @@ impl ServiceInstance {
counter.to_string()
}
/// Notifie tous les abonnés des changements.
/// Notifie tous les abonnés des changements en attente.
///
/// Cette méthode envoie les changements bufferisés à tous les abonnés actuels
/// via des requêtes HTTP NOTIFY. Les changements sont envoyés de manière
/// asynchrone dans des tâches séparées.
///
/// # Examples
///
/// ```rust,no_run
/// # use pmoupnp::services::Service;
/// # use pmoupnp::UpnpModel;
/// # #[tokio::main]
/// # async fn main() {
/// # let service = Service::new("AVTransport".to_string());
/// # let instance = service.create_instance();
/// instance.event_to_be_sent("TransportState".to_string(), "PLAYING".to_string());
/// instance.notify_subscribers().await;
/// # }
/// ```
pub async fn notify_subscribers(&self) {
let subscribers_copy = {
let subscribers = self.subscribers.read().unwrap();
@@ -588,6 +937,26 @@ impl ServiceInstance {
}
/// Handler Axum pour les événements (SUBSCRIBE/UNSUBSCRIBE).
///
/// Gère les requêtes HTTP SUBSCRIBE et UNSUBSCRIBE selon la spécification
/// UPnP Device Architecture.
///
/// # Opérations supportées
///
/// - `SUBSCRIBE` sans SID : Nouvelle souscription
/// - `SUBSCRIBE` avec SID : Renouvellement d'abonnement
/// - `UNSUBSCRIBE` : Annulation d'abonnement
///
/// # Arguments
///
/// * `instance` - L'instance du service
/// * `headers` - En-têtes HTTP de la requête
/// * `req` - La requête HTTP complète
///
/// # Returns
///
/// Une réponse HTTP avec le SID et le timeout pour SUBSCRIBE,
/// ou une simple confirmation pour UNSUBSCRIBE.
async fn event_sub_handler(
State(instance): State<ServiceInstance>,
headers: HeaderMap,
@@ -673,6 +1042,22 @@ async fn event_sub_handler(
}
/// Handler Axum pour le contrôle SOAP.
///
/// Gère les requêtes de contrôle SOAP pour invoquer des actions sur le service.
///
/// # Arguments
///
/// * `instance` - L'instance du service
/// * `_body` - Corps de la requête SOAP (actuellement non utilisé)
///
/// # Returns
///
/// Une réponse SOAP avec le résultat de l'action.
///
/// # Note
///
/// Cette implémentation est actuellement un stub et retourne une réponse vide.
/// Le parsing SOAP et l'exécution des actions doivent être implémentés.
async fn control_handler(State(instance): State<ServiceInstance>, _body: String) -> Response {
info!("📡 Control request for {}", instance.get_name());

View File

@@ -1,4 +1,15 @@
//! Implémentation des traits UPnP pour Service.
//!
//! Ce module fournit les implémentations des traits principaux du framework
//! UPnP pour le type [`Service`]:
//!
//! - [`Display`] : Affichage formaté d'un service
//! - [`UpnpTyped`] : Accès aux métadonnées de type UPnP
//! - [`UpnpObject`] : Sérialisation XML pour la description de device
//! - [`UpnpModel`] : Association du modèle avec son type d'instance
//!
//! Ces implémentations permettent aux services de s'intégrer dans
//! l'architecture UPnP générique du framework.
use xmltree::{Element, XMLNode};