Revenons sur les devices

This commit is contained in:
2025-10-05 14:35:44 +02:00
parent c9d357d647
commit e5f5686ba1
40 changed files with 11779 additions and 599 deletions

View File

@@ -5,6 +5,7 @@ use xmltree::{Element, XMLNode};
use crate::actions::Action;
use crate::actions::Argument;
use crate::actions::ArgumentSet;
use crate::actions::ArgInstanceSet;
use crate::actions::ActionInstance;
use crate::UpnpInstance;
use crate::UpnpObject;
@@ -13,7 +14,7 @@ use crate::UpnpTypedInstance;
use crate::UpnpObjectType;
impl UpnpObject for ActionInstance {
fn to_xml_element(&self) -> Element {
fn to_xml_element(&self) -> Element {
let mut elem = Element::new("action");
// <name>
@@ -21,8 +22,8 @@ fn to_xml_element(&self) -> Element {
name_elem.children.push(XMLNode::Text(self.get_name().clone()));
elem.children.push(XMLNode::Element(name_elem));
// déplacer tous les enfants de args_elem dans un nouvel Element
let args_container = self.arguments_set().to_xml_element();
// Utiliser le set d'instances d'arguments
let args_container = self.arguments.to_xml_element();
elem.children.push(XMLNode::Element(args_container));
elem
@@ -40,12 +41,23 @@ impl UpnpInstance for ActionInstance {
type Model = Action;
fn new(action: &Action) -> Self {
// Créer les instances d'arguments
let mut arguments = ArgInstanceSet::new();
for arg_model in action.arguments().all() {
let arg_instance = Arc::new(crate::actions::ArgumentInstance::new(&*arg_model));
if let Err(e) = arguments.insert(arg_instance) {
tracing::error!("Failed to insert argument instance: {:?}", e);
}
}
Self {
object: UpnpObjectType {
name: action.get_name().clone(),
object_type: "ActionInstance".to_string(),
},
model: action.clone(),
arguments, // ⬅️ Set d'instances, pas le modèle !
}
}
@@ -60,13 +72,65 @@ impl UpnpTypedInstance for ActionInstance {
}
impl ActionInstance {
pub fn arguments(&self, name: &str) -> Option<Arc<Argument>> {
self.model.arguments.get_by_name(name)
/// Retourne une instance d'argument par son nom.
///
/// # Arguments
///
/// * `name` - Nom de l'argument à rechercher
///
/// # Returns
///
/// `Some(Arc<ArgumentInstance>)` si trouvé, `None` sinon.
pub fn argument(&self, name: &str) -> Option<Arc<crate::actions::ArgumentInstance>> {
self.arguments.get_by_name(name)
}
pub fn arguments_set(&self) -> &ArgumentSet {
&self.model.arguments
/// Retourne le set d'instances d'arguments.
///
/// # Returns
///
/// Référence vers le `ArgInstanceSet` contenant toutes les instances.
///
/// # Examples
///
/// ```ignore
/// for arg_instance in action_instance.arguments_set().all() {
/// println!("Argument: {}", arg_instance.get_name());
/// if let Some(var) = arg_instance.get_variable_instance() {
/// println!(" Variable: {} = {}", var.get_name(), var.value());
/// }
/// }
/// ```
pub fn arguments_set(&self) -> &ArgInstanceSet {
&self.arguments // ⬅️ Retourne les INSTANCES, pas les modèles !
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::actions::Action;
use crate::UpnpInstance;
#[test]
fn test_action_instance_creation() {
let action = Action::new("Play".to_string());
let instance = ActionInstance::new(&action);
assert_eq!(instance.get_name(), "Play");
}
#[test]
fn test_action_instance_has_argument_instances() {
let action = Action::new("Play".to_string());
let instance = ActionInstance::new(&action);
// Vérifier que arguments_set() retourne bien des instances
assert!(instance.arguments_set().all().iter().all(|arg| {
// Chaque argument doit être une ArgumentInstance
arg.get_model(); // Cette méthode existe seulement sur les instances
true
}));
}
}

View File

@@ -1,7 +1,7 @@
use xmltree::{Element, XMLNode};
use crate::actions::ActionSet;
use crate::UpnpObject;
use crate::actions::{ActionInstanceSet, ActionSet};
use crate::{UpnpModel, UpnpObject};
impl UpnpObject for ActionSet {
fn to_xml_element(&self) -> Element {
@@ -17,3 +17,7 @@ impl UpnpObject for ActionSet {
}
impl UpnpModel for ActionSet {
type Instance = ActionInstanceSet;
}

View File

@@ -1,6 +1,8 @@
use std::{collections::HashMap, sync::{Arc, RwLock}};
use xmltree::Element;
use crate::{actions::{Argument, ArgumentInstance}, UpnpInstance, UpnpObject, UpnpObjectType, UpnpTyped, UpnpTypedInstance};
use crate::{actions::{ActionInstanceSet, ActionSet, Argument, ArgumentInstance}, state_variables::StateVarInstance, UpnpInstance, UpnpObject, UpnpObjectType, UpnpTyped, UpnpTypedInstance};
impl UpnpObject for ArgumentInstance {
@@ -15,28 +17,240 @@ impl UpnpTyped for ArgumentInstance {
}
}
/// Implémentation de [`UpnpTypedInstance`] pour [`ArgumentInstance`].
///
/// Cette implémentation permet d'accéder au modèle [`Argument`] depuis l'instance
/// via la méthode [`get_model()`](UpnpTypedInstance::get_model).
///
/// # Examples
///
/// ```ignore
/// use pmoupnp::UpnpTypedInstance;
///
/// let arg_instance = ArgumentInstance::new(&arg_model);
///
/// // Accéder au modèle
/// let model = arg_instance.get_model();
/// println!("Direction: in={}, out={}", model.is_in(), model.is_out());
/// println!("Related variable: {}", model.state_variable().get_name());
/// ```
impl UpnpTypedInstance for ArgumentInstance {
/// Retourne une référence vers le modèle [`Argument`].
///
/// Permet d'accéder aux métadonnées statiques définies dans le modèle :
/// - Direction de l'argument (in/out)
/// - Variable d'état associée
/// - Nom et type
fn get_model(&self) -> &Self::Model {
&self.model
}
}
/// Implémentation de [`UpnpInstance`] pour [`ArgumentInstance`].
///
/// Cette implémentation fournit le constructeur standard qui crée une instance
/// **non liée** d'un argument. La liaison à une [`StateVarInstance`] doit être
/// effectuée séparément via [`bind_variable`](ArgumentInstance::bind_variable).
///
/// # Processus de construction en deux phases
///
/// ```text
/// Phase 1 (new) Phase 2 (bind_variable)
/// ┌─────────────────┐ ┌──────────────────────┐
/// │ ArgumentInstance│ │ StateVarInstance │
/// │ │ │ │
/// │ model: Arc<...> │────>│ Liaison établie │
/// │ variable: None │ │ variable: Some(...) │
/// └─────────────────┘ └──────────────────────┘
/// ↓ ↓
/// Création bind_variable(&var)
/// ```
///
/// # Pourquoi deux phases ?
///
/// 1. **Ordre de création** : Les modèles (`Argument`) existent avant les instances
/// 2. **Validation différée** : Les dépendances sont vérifiées après instanciation
/// 3. **Découplage** : Permet de créer des arguments même si les variables n'existent pas encore
///
/// # Examples
///
/// ```ignore
/// use pmoupnp::actions::{Argument, ArgumentInstance};
/// use pmoupnp::UpnpInstance;
///
/// let arg_model = Argument::new_in("InstanceID".to_string(), instance_id_var);
///
/// // Création de l'instance - Phase 1
/// let arg_instance = ArgumentInstance::new(&arg_model);
///
/// // À ce stade, l'instance existe mais n'est pas encore liée
/// assert_eq!(arg_instance.get_name(), "InstanceID");
/// assert!(arg_instance.get_variable_instance().is_none());
///
/// // La liaison se fera plus tard via bind_variable()
/// ```
impl UpnpInstance for ArgumentInstance {
type Model = Argument;
/// Crée une nouvelle instance d'argument depuis son modèle.
///
/// # Arguments
///
/// * `from` - Référence vers le modèle [`Argument`] définissant cet argument
///
/// # Returns
///
/// Une nouvelle `ArgumentInstance` avec :
/// - Nom copié depuis le modèle
/// - Référence vers le modèle (clone)
/// - `variable_instance` initialisé à `None` (liaison non établie)
///
/// # État initial
///
/// L'instance créée n'est **pas encore liée** à une variable d'état.
/// Pour établir la liaison, appelez [`bind_variable`](ArgumentInstance::bind_variable).
///
/// # Thread-safety
///
/// L'instance retournée est thread-safe et peut être partagée via `Arc`.
///
/// # Examples
///
/// ```ignore
/// use pmoupnp::UpnpInstance;
///
/// // Création depuis un modèle
/// let instance = ArgumentInstance::new(&arg_model);
///
/// // L'instance hérite des propriétés du modèle
/// assert_eq!(instance.get_name(), arg_model.get_name());
/// assert_eq!(instance.is_in(), arg_model.is_in());
///
/// // Mais n'a pas encore de valeur runtime
/// assert!(instance.get_variable_instance().is_none());
/// ```
fn new(from: &Argument) -> Self {
Self {
// Copie des métadonnées depuis le modèle
object: UpnpObjectType {
name: from.get_name().clone(),
object_type: "UpnpInstance".to_string(),
object_type: "ArgumentInstance".to_string(),
},
// Clone du modèle pour référence future
model: from.clone(),
variable_instance: None,
}
// Initialisation à None - sera lié plus tard via bind_variable()
// Arc<RwLock<...>> permet la modification thread-safe post-construction
variable_instance: Arc::new(RwLock::new(None)),
}
}
}
// ============================================================================
// Méthodes de liaison et d'accès
// ============================================================================
impl ArgumentInstance {
/// Lie cet argument à une instance de variable d'état.
///
/// Cette méthode établit la connexion entre l'argument et sa variable d'état,
/// permettant l'accès aux valeurs runtime lors de l'exécution d'actions.
///
/// # Arguments
///
/// * `var_instance` - Instance de la variable d'état à lier
///
/// # Thread-safety
///
/// Cette méthode acquiert un **write lock** sur `variable_instance` et peut
/// bloquer si d'autres threads lisent actuellement la valeur.
///
/// # Panics
///
/// Panique si le lock est empoisonné (poisoned), ce qui ne devrait jamais
/// arriver dans un usage normal.
///
/// # Examples
///
/// ```ignore
/// use std::sync::Arc;
///
/// let arg_instance = ArgumentInstance::new(&arg_model);
/// let var_instance = Arc::new(StateVarInstance::new(&state_var));
///
/// // Établir la liaison
/// arg_instance.bind_variable(var_instance.clone());
///
/// // Vérifier que la liaison est établie
/// assert!(arg_instance.get_variable_instance().is_some());
/// ```
///
/// # Note
///
/// Cette méthode peut être appelée plusieurs fois pour changer la variable liée,
/// bien que ce ne soit généralement pas recommandé dans un usage normal.
pub fn bind_variable(&self, var_instance: Arc<StateVarInstance>) {
let mut var = self.variable_instance.write().unwrap();
*var = Some(var_instance);
}
/// Retourne l'instance de variable d'état liée, si elle existe.
///
/// # Returns
///
/// - `Some(Arc<StateVarInstance>)` si une variable est liée
/// - `None` si aucune liaison n'a été établie via [`bind_variable`](Self::bind_variable)
///
/// # Thread-safety
///
/// Cette méthode acquiert un **read lock** sur `variable_instance`.
/// Plusieurs threads peuvent lire simultanément sans blocage.
///
/// # Panics
///
/// Panique si le lock est empoisonné (poisoned).
///
/// # Examples
///
/// ```ignore
/// // Vérifier si la liaison existe
/// if let Some(var) = arg_instance.get_variable_instance() {
/// println!("Variable liée : {}", var.get_name());
/// println!("Valeur actuelle : {}", var.value());
/// } else {
/// println!("Aucune variable liée");
/// }
/// ```
///
/// # Usage dans l'exécution d'actions
///
/// ```ignore
/// async fn execute_action(action: &ActionInstance) -> Result<(), ActionError> {
/// for arg in action.arguments_set().all() {
/// if let Some(var) = arg.get_variable_instance() {
/// // Utiliser var.value() pour lire/écrire
/// println!("Paramètre {} = {}", arg.get_name(), var.value());
/// } else {
/// return Err(ActionError::UnboundArgument(arg.get_name().to_string()));
/// }
/// }
/// Ok(())
/// }
/// ```
pub fn get_variable_instance(&self) -> Option<Arc<StateVarInstance>> {
self.variable_instance.read().unwrap().clone()
}
}
impl UpnpInstance for ActionInstanceSet {
type Model = ActionSet;
fn new(_: &ActionSet) -> Self {
Self {
objects: RwLock::new(HashMap::new())
}
}
}

View File

@@ -1,19 +1,21 @@
mod errors;
mod action_methods;
mod action_instance;
mod action_set_methods;
mod action_instance_set;
mod argument_methods;
mod arg_set_methods;
mod action_methods;
mod action_set_methods;
mod arg_inst_set_methods;
mod arg_instance_methods;
mod arg_set_methods;
mod argument_methods;
mod macros;
use std::sync::Arc;
use crate::{state_variables::{StateVarInstance, StateVariable}, UpnpObjectSet, UpnpObjectType};
use crate::{
UpnpObjectSet, UpnpObjectType,
state_variables::{StateVarInstance, StateVariable},
};
use std::sync::{Arc, RwLock};
pub use errors::ActionError;
@@ -29,6 +31,7 @@ pub type ActionSet = UpnpObjectSet<Action>;
pub struct ActionInstance {
object: UpnpObjectType,
model: Action,
arguments: ArgInstanceSet,
}
pub type ActionInstanceSet = UpnpObjectSet<ActionInstance>;
@@ -43,12 +46,71 @@ pub struct Argument {
pub type ArgumentSet = UpnpObjectSet<Argument>;
/// Instance d'un argument d'action UPnP.
///
/// Un `ArgumentInstance` représente un argument concret utilisé lors de l'exécution
/// d'une action. Contrairement au modèle [`Argument`] qui définit la structure,
/// l'instance maintient une liaison dynamique vers une [`StateVarInstance`] qui
/// contient la valeur runtime.
///
/// # Cycle de vie
///
/// 1. **Création** : Instanciation via [`UpnpInstance::new`] avec `variable_instance = None`
/// 2. **Liaison** : Association à une [`StateVarInstance`] via [`bind_variable`](Self::bind_variable)
/// 3. **Utilisation** : Accès à la valeur runtime via [`get_variable_instance`](Self::get_variable_instance)
///
/// # Pourquoi `variable_instance` est optionnel ?
///
/// La liaison ne peut pas être faite dans le constructeur car :
/// - Les `StateVarInstance` sont créées **après** les modèles
/// - Les `ActionInstance` sont créées **avant** que toutes les variables soient disponibles
/// - La validation des dépendances se fait en deux phases
///
/// # Thread-safety
///
/// Le champ `variable_instance` est protégé par un `RwLock` pour permettre :
/// - La liaison après création (write lock)
/// - L'accès concurrent en lecture (read lock)
/// - L'utilisation dans un contexte multi-thread
///
/// # Examples
///
/// ```ignore
/// use pmoupnp::actions::{Argument, ArgumentInstance};
/// use pmoupnp::state_variables::StateVarInstance;
/// use std::sync::Arc;
///
/// // Phase 1 : Créer l'instance (sans liaison)
/// let arg_model = Argument::new_in("Volume".to_string(), volume_var);
/// let arg_instance = ArgumentInstance::new(&arg_model);
/// assert!(arg_instance.get_variable_instance().is_none());
///
/// // Phase 2 : Lier à une variable d'état
/// let var_instance = Arc::new(StateVarInstance::new(&volume_var));
/// arg_instance.bind_variable(var_instance.clone());
/// assert!(arg_instance.get_variable_instance().is_some());
///
/// // Phase 3 : Utiliser la valeur runtime
/// if let Some(var) = arg_instance.get_variable_instance() {
/// println!("Current value: {}", var.value());
/// }
/// ```
#[derive(Debug, Clone)]
pub struct ArgumentInstance {
/// Métadonnées de l'objet UPnP
object: UpnpObjectType,
/// Référence vers le modèle définissant la structure
model: Argument,
variable_instance: Option<Arc<StateVarInstance>>,
/// Liaison optionnelle vers l'instance de variable d'état.
///
/// - `None` : Pas encore liée (état initial après construction)
/// - `Some(Arc<StateVarInstance>)` : Liée et prête à l'emploi
///
/// Protégée par `RwLock` pour permettre la liaison post-construction
/// et l'accès concurrent en lecture.
variable_instance: Arc<RwLock<Option<Arc<StateVarInstance>>>>,
}
pub type ArgInstanceSet = UpnpObjectSet<ArgumentInstance>;

View File

View File

@@ -4,7 +4,7 @@ mod object_set;
pub mod actions;
pub mod mediarenderer;
pub mod server;
// pub mod services;
pub mod services;
pub mod state_variables;
pub mod value_ranges;
pub mod variable_types;
@@ -27,6 +27,7 @@ pub struct UpnpObjectSet<T: UpnpTypedObject> {
objects: RwLock<HashMap<String, Arc<T>>>,
}
#[derive(Debug)]
pub enum UpnpObjectSetError {
AlreadyExists(String),
}

View File

@@ -0,0 +1,8 @@
use crate::mediarenderer::avtransport::variables::A_ARG_TYPE_INSTANCE_ID;
use crate::define_action;
define_action! {
pub static GETDEVICECAPABILITIES = "GetDeviceCapabilities" {
in "InstanceID" => A_ARG_TYPE_INSTANCE_ID,
}
}

View File

@@ -0,0 +1,14 @@
use crate::mediarenderer::avtransport::variables::{A_ARG_TYPE_INSTANCE_ID, NUMBEROFTRACKS, CURRENTTRACK, AVTRANSPORTURI, AVTRANSPORTURIMETADATA, AVTRANSPORTNEXTURI, AVTRANSPORTNEXTURIMETADATA};
use crate::define_action;
define_action! {
pub static GETMEDIAINFO = "GetMediaInfo" {
in "InstanceID" => A_ARG_TYPE_INSTANCE_ID,
out "NrTracks" => NUMBEROFTRACKS,
out "CurrentTrack" => CURRENTTRACK,
out "CurrentURI" => AVTRANSPORTURI,
out "CurrentURIMetaData" => AVTRANSPORTURIMETADATA,
out "NextURI" => AVTRANSPORTNEXTURI,
out "NextURIMetaData" => AVTRANSPORTNEXTURIMETADATA,
}
}

View File

@@ -0,0 +1,14 @@
use crate::mediarenderer::avtransport::variables::{A_ARG_TYPE_INSTANCE_ID, CURRENTTRACK, CURRENTTRACKDURATION, AVTRANSPORTURI, AVTRANSPORTURIMETADATA, RELATIVETIMEPOSITION, ABSOLUTETIMEPOSITION};
use crate::define_action;
define_action! {
pub static GETPOSITIONINFO = "GetPositionInfo" {
in "InstanceID" => A_ARG_TYPE_INSTANCE_ID,
out "Track" => CURRENTTRACK,
out "TrackDuration" => CURRENTTRACKDURATION,
out "TrackURI" => AVTRANSPORTURI,
out "TrackMetaData" => AVTRANSPORTURIMETADATA,
out "RelTime" => RELATIVETIMEPOSITION,
out "AbsTime" => ABSOLUTETIMEPOSITION,
}
}

View File

@@ -0,0 +1,10 @@
use crate::mediarenderer::avtransport::variables::{A_ARG_TYPE_INSTANCE_ID, TRANSPORTSTATE, TRANSPORTSTATUS};
use crate::define_action;
define_action! {
pub static GETTRANSPORTINFO = "GetTransportInfo" {
in "InstanceID" => A_ARG_TYPE_INSTANCE_ID,
out "CurrentTransportState" => TRANSPORTSTATE,
out "CurrentTransportStatus" => TRANSPORTSTATUS,
}
}

View File

@@ -0,0 +1,8 @@
use crate::mediarenderer::avtransport::variables::A_ARG_TYPE_INSTANCE_ID;
use crate::define_action;
define_action! {
pub static GETTRANSPORTSETTINGS = "GetTransportSettings" {
in "InstanceID" => A_ARG_TYPE_INSTANCE_ID,
}
}

View File

@@ -1,8 +1,28 @@
mod getdevicecapabilities;
mod getmediainfo;
mod getpositioninfo;
mod gettransportinfo;
mod gettransportsettings;
mod next;
mod pause;
mod play;
mod stop;
mod previous;
mod seek;
mod setavtransportnexturi;
mod setavtransporturi;
mod stop;
pub use getdevicecapabilities::GETDEVICECAPABILITIES;
pub use getmediainfo::GETMEDIAINFO;
pub use getpositioninfo::GETPOSITIONINFO;
pub use gettransportinfo::GETTRANSPORTINFO;
pub use gettransportsettings::GETTRANSPORTSETTINGS;
pub use next::NEXT;
pub use pause::PAUSE;
pub use play::PLAY;
pub use stop::STOP;
pub use previous::PREVIOUS;
pub use seek::SEEK;
pub use setavtransportnexturi::SETNEXTAVTRANSPORTURI;
pub use setavtransporturi::SETAVTRANSPORTURI;
pub use stop::STOP;

View File

@@ -0,0 +1,8 @@
use crate::mediarenderer::avtransport::variables::A_ARG_TYPE_INSTANCE_ID;
use crate::define_action;
define_action! {
pub static NEXT = "Next" {
in "InstanceID" => A_ARG_TYPE_INSTANCE_ID,
}
}

View File

@@ -0,0 +1,8 @@
use crate::mediarenderer::avtransport::variables::A_ARG_TYPE_INSTANCE_ID;
use crate::define_action;
define_action! {
pub static PAUSE = "Pause" {
in "InstanceID" => A_ARG_TYPE_INSTANCE_ID,
}
}

View File

@@ -0,0 +1,8 @@
use crate::mediarenderer::avtransport::variables::A_ARG_TYPE_INSTANCE_ID;
use crate::define_action;
define_action! {
pub static PREVIOUS = "Previous" {
in "InstanceID" => A_ARG_TYPE_INSTANCE_ID,
}
}

View File

@@ -0,0 +1,10 @@
use crate::mediarenderer::avtransport::variables::{A_ARG_TYPE_INSTANCE_ID, A_ARG_TYPE_SEEKMODE, CURRENTTRACKDURATION};
use crate::define_action;
define_action! {
pub static SEEK = "Seek" {
in "InstanceID" => A_ARG_TYPE_INSTANCE_ID,
in "Unit" => A_ARG_TYPE_SEEKMODE,
in "Target" => CURRENTTRACKDURATION,
}
}

View File

@@ -0,0 +1,10 @@
use crate::mediarenderer::avtransport::variables::{A_ARG_TYPE_INSTANCE_ID, AVTRANSPORTNEXTURI, AVTRANSPORTNEXTURIMETADATA};
use crate::define_action;
define_action! {
pub static SETNEXTAVTRANSPORTURI = "SetNextAVTransportURI" {
in "InstanceID" => A_ARG_TYPE_INSTANCE_ID,
in "NextURI" => AVTRANSPORTNEXTURI,
in "NextURIMetaData" => AVTRANSPORTNEXTURIMETADATA,
}
}

View File

@@ -1,3 +1,55 @@
use crate::define_service;
pub mod variables;
pub mod actions;
use actions::{
GETDEVICECAPABILITIES, GETMEDIAINFO, GETPOSITIONINFO, GETTRANSPORTINFO,
GETTRANSPORTSETTINGS, NEXT, PAUSE, PLAY, PREVIOUS, SEEK,
SETNEXTAVTRANSPORTURI, SETAVTRANSPORTURI, STOP
};
use variables::{
ABSOLUTETIMEPOSITION, AVTRANSPORTNEXTURI, AVTRANSPORTNEXTURIMETADATA,
AVTRANSPORTURI, AVTRANSPORTURIMETADATA, A_ARG_TYPE_INSTANCE_ID,
A_ARG_TYPE_PLAY_SPEED, A_ARG_TYPE_SEEKMODE, CURRENTTRACK,
CURRENTTRACKDURATION, NUMBEROFTRACKS, RELATIVETIMEPOSITION, SEEKMODE,
TRANSPORTPLAYSPEED, TRANSPORTSTATE, TRANSPORTSTATUS
};
define_service! {
pub static AVTTRANSPORT = "AVTransport" {
variables: [
ABSOLUTETIMEPOSITION,
A_ARG_TYPE_INSTANCE_ID,
A_ARG_TYPE_PLAY_SPEED,
A_ARG_TYPE_SEEKMODE,
AVTRANSPORTNEXTURI,
AVTRANSPORTNEXTURIMETADATA,
AVTRANSPORTURI,
AVTRANSPORTURIMETADATA,
CURRENTTRACK,
CURRENTTRACKDURATION,
NUMBEROFTRACKS,
RELATIVETIMEPOSITION,
SEEKMODE,
TRANSPORTPLAYSPEED,
TRANSPORTSTATE,
TRANSPORTSTATUS,
],
actions: [
GETDEVICECAPABILITIES,
GETMEDIAINFO,
GETPOSITIONINFO,
GETTRANSPORTINFO,
GETTRANSPORTSETTINGS,
NEXT,
PAUSE,
PLAY,
PREVIOUS,
SEEK,
SETNEXTAVTRANSPORTURI,
SETAVTRANSPORTURI,
STOP,
]
}
}

View File

@@ -0,0 +1,17 @@
use std::sync::Arc;
use crate::state_variables::StateVariable;
use crate::variable_types::{StateValue, StateVarType};
use once_cell::sync::Lazy;
pub static A_ARG_TYPE_SEEKMODE: Lazy<Arc<StateVariable>> = Lazy::new(|| -> Arc<StateVariable> {
let mut sv = StateVariable::new(StateVarType::UI4, "A_ARG_TYPE_SeekMode".to_string());
sv.extend_allowed_values(&[
StateValue::String("TRACK_NR".to_string()),
StateValue::String("REL_TIME".to_string()),
StateValue::String("ABS_TIME".to_string()),
]).expect("Cannot set default value");
Arc::new(sv)
});

View File

@@ -7,3 +7,7 @@ use once_cell::sync::Lazy;
pub static AVTRANSPORTURI: Lazy<Arc<StateVariable>> = Lazy::new(|| -> Arc<StateVariable> {
Arc::new(StateVariable::new(StateVarType::String, "AVTransportURI".to_string()))
});
pub static AVTRANSPORTNEXTURI: Lazy<Arc<StateVariable>> = Lazy::new(|| -> Arc<StateVariable> {
Arc::new(StateVariable::new(StateVarType::String, "AVTransportNextURI".to_string()))
});

View File

@@ -6,15 +6,6 @@ use bevy_reflect::Reflect;
use once_cell::sync::Lazy;
use pmodidl::{DIDLLite, MediaMetadataParser};
// func _AVTransportURIMetaDataParser(value string) (interface{}, error) {
// log.Debug("[avtransport] Parsing AVTransport)")
// didl, err := pmodidl.Parse(value)
// if err != nil {
// return value, err
// }
// return didl, nil
// }
fn avtransporturimetadataparser(value: &str) -> Result<Box<dyn Reflect>, StateVariableError> {
// Parse DIDL-Lite
@@ -31,3 +22,10 @@ pub static AVTRANSPORTURIMETADATA: Lazy<Arc<StateVariable>> = Lazy::new(|| -> Ar
sv.set_value_parser(Arc::new(avtransporturimetadataparser)).expect("Failed to set parser");
Arc::new(sv)
});
pub static AVTRANSPORTNEXTURIMETADATA: Lazy<Arc<StateVariable>> = Lazy::new(|| -> Arc<StateVariable> {
let mut sv = StateVariable::new(StateVarType::String, "AVTransportNextURIMetaData".to_string());
sv.set_value_parser(Arc::new(avtransporturimetadataparser)).expect("Failed to set parser");
Arc::new(sv)
});

View File

@@ -1,10 +0,0 @@
use std::sync::Arc;
use crate::state_variables::StateVariable;
use crate::variable_types::StateVarType;
use once_cell::sync::Lazy;
pub static CURRENTTRACKDURATION: Lazy<Arc<StateVariable>> = Lazy::new(|| -> Arc<StateVariable> {
Arc::new(StateVariable::new(StateVarType::String, "CurrentTrackDuration".to_string()))
});

View File

@@ -1,8 +1,10 @@
mod a_arg_type_instanceid;
mod a_arg_type_playspeed;
mod a_arg_type_seekmode;
mod avtransporturi;
mod avtransporturimetadata;
mod currenttrackduration;
mod track;
mod trackduration;
mod seekmode;
mod transportplayspeed;
mod transportstate;
@@ -10,9 +12,16 @@ mod transportstatus;
pub use a_arg_type_instanceid::A_ARG_TYPE_INSTANCE_ID;
pub use a_arg_type_playspeed::A_ARG_TYPE_PLAY_SPEED;
pub use a_arg_type_seekmode::A_ARG_TYPE_SEEKMODE;
pub use avtransporturi::AVTRANSPORTURI;
pub use avtransporturi::AVTRANSPORTNEXTURI;
pub use avtransporturimetadata::AVTRANSPORTURIMETADATA;
pub use currenttrackduration::CURRENTTRACKDURATION;
pub use avtransporturimetadata::AVTRANSPORTNEXTURIMETADATA;
pub use track::CURRENTTRACK;
pub use track::NUMBEROFTRACKS;
pub use trackduration::CURRENTTRACKDURATION;
pub use trackduration::ABSOLUTETIMEPOSITION;
pub use trackduration::RELATIVETIMEPOSITION;
pub use seekmode::SEEKMODE;
pub use transportplayspeed::TRANSPORTPLAYSPEED;
pub use transportstate::TRANSPORTSTATE;

View File

@@ -0,0 +1,22 @@
use std::sync::Arc;
use crate::state_variables::StateVariable;
use crate::variable_types::StateVarType;
use once_cell::sync::Lazy;
pub static CURRENTTRACK: Lazy<Arc<StateVariable>> = Lazy::new(|| -> Arc<StateVariable> {
let mut sv = StateVariable::new(StateVarType::String, "CurrentTrack".to_string());
sv.set_send_notification();
Arc::new(sv)
});
pub static NUMBEROFTRACKS: Lazy<Arc<StateVariable>> = Lazy::new(|| -> Arc<StateVariable> {
let mut sv = StateVariable::new(StateVarType::String, "NumberOfTracks".to_string());
sv.set_send_notification();
Arc::new(sv)
});

View File

@@ -0,0 +1,30 @@
use std::sync::Arc;
use crate::state_variables::StateVariable;
use crate::variable_types::StateVarType;
use once_cell::sync::Lazy;
pub static CURRENTTRACKDURATION: Lazy<Arc<StateVariable>> = Lazy::new(|| -> Arc<StateVariable> {
let mut sv = StateVariable::new(StateVarType::String, "CurrentTrackDuration".to_string());
sv.set_send_notification();
Arc::new(sv)
});
pub static ABSOLUTETIMEPOSITION: Lazy<Arc<StateVariable>> = Lazy::new(|| -> Arc<StateVariable> {
let mut sv = StateVariable::new(StateVarType::String, "AbsoluteTimePosition".to_string());
sv.set_send_notification();
Arc::new(sv)
});
pub static RELATIVETIMEPOSITION: Lazy<Arc<StateVariable>> = Lazy::new(|| -> Arc<StateVariable> {
let mut sv = StateVariable::new(StateVarType::String, "RelativeTimePosition".to_string());
sv.set_send_notification();
Arc::new(sv)
});

View File

@@ -7,7 +7,6 @@ use once_cell::sync::Lazy;
pub static TRANSPORTSTATE: Lazy<Arc<StateVariable>> = Lazy::new(|| -> Arc<StateVariable> {
let mut sv = StateVariable::new(StateVarType::String, "TransportState".to_string());
sv.push_allowed_value(&StateValue::String("NO_MEDIA_PRESENT".to_string())).expect("Cannot add allowed value");
sv.extend_allowed_values(&[
StateValue::String("STOPPED".to_string()),
StateValue::String("PLAYING".to_string()),
@@ -18,6 +17,8 @@ pub static TRANSPORTSTATE: Lazy<Arc<StateVariable>> = Lazy::new(|| -> Arc<StateV
StateValue::String("NO_MEDIA_PRESENT".to_string()),
]).expect("Cannt set default value");
sv.set_send_notification();
Arc::new(sv)
});

View File

@@ -7,13 +7,13 @@ use once_cell::sync::Lazy;
pub static TRANSPORTSTATUS: Lazy<Arc<StateVariable>> = Lazy::new(|| -> Arc<StateVariable> {
let mut sv = StateVariable::new(StateVarType::String, "TransportStatus".to_string());
sv.push_allowed_value(&StateValue::String("OK".to_string()))
.expect("Cannot add allowed value");
sv.extend_allowed_values(&[
StateValue::String("OK".to_string()),
StateValue::String("ERROR_OCCURRED".to_string()),
])
.expect("Cannt set default value");
sv.set_send_notification();
Arc::new(sv)
});

View File

@@ -2,7 +2,7 @@ use std::{collections::HashMap, sync::Arc};
use std::sync::RwLock;
use crate::{UpnpDeepClone, UpnpObjectSet, UpnpObjectSetError, UpnpTypedObject};
use crate::{UpnpDeepClone, UpnpObjectSet, UpnpObjectSetError, UpnpSet, UpnpTypedObject};
/// Implémentation du clonage profond pour `UpnpObjectSet`.
///

View File

@@ -1,19 +1,62 @@
//! Erreurs du module services.
use thiserror::Error;
/// Erreurs liées aux services UPnP.
///
/// Cette énumération couvre toutes les erreurs possibles lors de la manipulation
/// de services UPnP, incluant les erreurs de validation, de configuration et d'exécution.
#[derive(Error, Debug)]
pub enum ServiceError {
#[error("Action error: {0}")]
/// Erreur générale du service.
#[error("Service error: {0}")]
GeneralError(String),
#[error("Argument error: {0}")]
ArgumentError(String),
/// Erreur de validation (paramètres invalides).
#[error("Validation error: {0}")]
ValidationError(String),
/// Erreur lors d'une opération sur un ensemble (Set).
#[error("Set operation error: {0}")]
SetError(String),
/// Erreur liée à une action.
#[error("Action error: {0}")]
ActionError(String),
/// Erreur liée à une variable d'état.
#[error("State variable error: {0}")]
StateVariableError(String),
/// Erreur de configuration.
#[error("Configuration error: {0}")]
ConfigError(String),
/// Erreur réseau ou HTTP.
#[error("Network error: {0}")]
NetworkError(String),
/// Erreur de sérialisation XML.
#[error("XML serialization error: {0}")]
XmlError(String),
/// Erreur lors du traitement SOAP.
#[error("SOAP error: {0}")]
SoapError(String),
}
impl From<std::io::Error> for ServiceError {
fn from(err: std::io::Error) -> Self {
ServiceError::GeneralError(format!("IO error: {}", err))
}
}
impl From<crate::UpnpObjectSetError> for ServiceError {
fn from(err: crate::UpnpObjectSetError) -> Self {
match err {
crate::UpnpObjectSetError::AlreadyExists(name) => {
ServiceError::SetError(format!("Object already exists: {}", name))
}
}
}
}

View File

@@ -0,0 +1,109 @@
/// Macro pour définir facilement un service UPnP avec ses variables et actions.
///
/// Cette macro simplifie la création de services UPnP statiques en générant
/// automatiquement le code nécessaire pour initialiser un service avec ses
/// variables d'état et ses actions.
///
/// # Syntaxe
///
/// ```ignore
/// define_service! {
/// pub static SERVICE_NAME = "ServiceName" {
/// variables: [
/// VARIABLE1,
/// VARIABLE2,
/// ],
/// actions: [
/// ACTION1,
/// ACTION2,
/// ]
/// }
/// }
/// ```
///
/// # Arguments
///
/// - `SERVICE_NAME` : Nom de la constante statique Rust
/// - `"ServiceName"` : Nom du service UPnP (chaîne littérale)
/// - `variables:` : Section listant les références aux variables d'état
/// - `actions:` : Section listant les références aux actions
///
/// # Type de retour
///
/// La macro génère une `Lazy<Arc<Service>>` qui sera initialisée lors du premier accès.
///
/// # Prérequis
///
/// Les variables et actions référencées doivent être définies comme `Lazy<Arc<T>>`.
///
/// # Examples
///
/// ```ignore
/// use once_cell::sync::Lazy;
/// use std::sync::Arc;
///
/// // Définir les variables et actions ailleurs
/// pub static TRANSPORT_STATE: Lazy<Arc<StateVariable>> = ...;
/// pub static PLAY: Lazy<Arc<Action>> = ...;
/// pub static STOP: Lazy<Arc<Action>> = ...;
///
/// // Définir le service
/// define_service! {
/// pub static AVTRANSPORT = "AVTransport" {
/// variables: [
/// TRANSPORT_STATE,
/// TRANSPORT_URI,
/// ],
/// actions: [
/// PLAY,
/// STOP,
/// PAUSE,
/// ]
/// }
/// }
///
/// // Utilisation
/// fn main() {
/// let service = &*AVTRANSPORT;
/// println!("Service: {}", service.name());
/// }
/// ```
///
/// # Notes d'implémentation
///
/// - Les `Arc<StateVariable>` et `Arc<Action>` sont clonés
/// - Le service est wrappé dans un `Arc`
/// - Initialisation paresseuse via `Lazy` (thread-safe)
/// - Utilise `.expect()` pour les erreurs d'ajout
#[macro_export]
macro_rules! define_service {
(pub static $name:ident = $service_name:literal {
variables: [
$($var:expr),* $(,)?
],
actions: [
$($action:expr),* $(,)?
]
}) => {
pub static $name: once_cell::sync::Lazy<std::sync::Arc<$crate::services::Service>> =
once_cell::sync::Lazy::new(|| {
use $crate::UpnpTyped;
let mut svc = $crate::services::Service::new($service_name.to_string());
$(
svc.add_variable(std::sync::Arc::clone(&*$var))
.expect(&format!("Cannot add variable {} to service {}",
(*$var).get_name(), svc.name()));
)*
$(
svc.add_action(std::sync::Arc::clone(&*$action))
.expect(&format!("Cannot add action {} to service {}",
(*$action).get_name(), svc.name()));
)*
std::sync::Arc::new(svc)
});
};
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,710 @@
//! Implémentation de ServiceInstance.
use std::{
collections::HashMap,
sync::{Arc, Mutex, RwLock},
time::Duration,
};
use axum::{
extract::{Request, State},
http::{HeaderMap, StatusCode},
response::{IntoResponse, Response},
body::Body,
};
use tokio::time;
use tracing::{info, warn, error};
use xmltree::{Element, XMLNode, EmitterConfig};
use crate::{
services::{Service, ServiceError},
actions::{ActionInstance, ActionInstanceSet},
state_variables::{StateVarInstance, StateVarInstanceSet, UpnpVariable},
UpnpObject, UpnpInstance, UpnpTyped, UpnpTypedInstance, UpnpObjectType,
};
/// Méthodes HTTP pour les événements UPnP.
pub const METHOD_SUBSCRIBE: &str = "SUBSCRIBE";
pub const METHOD_UNSUBSCRIBE: &str = "UNSUBSCRIBE";
/// Instance de service UPnP.
///
/// Représente une instance concrète d'un service UPnP, attachée à un device.
/// Gère l'exécution des actions, les notifications d'événements et les abonnements.
///
/// # Fonctionnalités
///
/// - Exécution d'actions via SOAP
/// - Gestion des abonnements aux événements (SUBSCRIBE/UNSUBSCRIBE)
/// - Notifications automatiques des changements d'état
/// - Génération de la description SCPD
///
/// # Cycle de vie
///
/// 1. Création via [`Service::create_instance`](crate::UpnpModel::create_instance)
/// 2. Enregistrement des URLs avec [`register_urls`](Self::register_urls)
/// 3. Démarrage du notifier avec [`start_notifier`](Self::start_notifier)
///
/// # Examples
///
/// ```rust,no_run
/// # use pmoupnp::services::Service;
/// # use pmoupnp::server::Server;
/// # use std::time::Duration;
/// # #[tokio::main]
/// # async fn main() {
/// let service = Service::new("AVTransport".to_string());
/// let instance = service.create_instance();
///
/// // Enregistrer les endpoints
/// let mut server = Server::new("test", "http://localhost:8080", 8080);
/// instance.register_urls(&mut server).await.unwrap();
///
/// // Démarrer les notifications
/// let _handle = instance.start_notifier(Duration::from_secs(5));
/// # }
/// ```
#[derive(Clone)]
pub struct ServiceInstance {
/// Métadonnées de l'objet
object: UpnpObjectType,
/// Référence vers le modèle
model: Arc<Service>,
/// Identifiant du service
identifier: String,
/// Device parent (optionnel)
device: Option<Arc<DeviceStub>>,
/// Variables d'état instanciées
statevariables: StateVarInstanceSet,
/// Actions instanciées
actions: ActionInstanceSet,
/// Abonnés aux événements (SID -> Callback URL)
subscribers: Arc<RwLock<HashMap<String, String>>>,
/// Buffer des changements en attente de notification
changed_buffer: Arc<Mutex<HashMap<String, String>>>,
/// Compteurs de séquence par abonné
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 {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("ServiceInstance")
.field("object", &self.object)
.field("identifier", &self.identifier)
.field("device", &self.device)
.field("statevariables", &self.statevariables)
.field("actions", &self.actions)
.finish()
}
}
impl UpnpTyped for ServiceInstance {
fn as_upnp_object_type(&self) -> &UpnpObjectType {
&self.object
}
}
impl UpnpInstance for ServiceInstance {
type Model = Service;
fn new(model: &Service) -> Self {
// Phase 1 : Créer les instances de variables d'état
let mut statevariables = StateVarInstanceSet::new();
for v in model.variables() {
if let Err(e) = statevariables.insert(Arc::new(StateVarInstance::new(&*v))) {
error!("Failed to insert state variable: {:?}", e);
}
}
// Phase 2 : Créer les instances d'actions avec validation
let mut actions = ActionInstanceSet::new();
for a in model.actions() {
// Vérifier que toutes les variables référencées existent
let mut missing_vars = Vec::new();
for arg in a.arguments().all() {
let related_var_name = arg.state_variable().get_name();
if statevariables.get_by_name(related_var_name).is_none() {
missing_vars.push(related_var_name.to_string());
}
}
if !missing_vars.is_empty() {
error!(
"Action '{}' references missing state variables: {:?}",
a.get_name(),
missing_vars
);
continue;
}
// Créer l'instance d'action
let action_instance = Arc::new(ActionInstance::new(&*a));
// ✅ Phase 3 : ACTIVER le binding des arguments aux variables d'instance
for arg_instance in action_instance.arguments_set().all() {
let var_name = arg_instance.get_model().state_variable().get_name();
if let Some(var_instance) = statevariables.get_by_name(var_name) {
// ✅ Activer cette ligne (déjà présente dans ArgumentInstance)
arg_instance.bind_variable(var_instance);
}
}
if let Err(e) = actions.insert(action_instance) {
error!("Failed to insert action '{}': {:?}", a.get_name(), e);
}
}
Self {
object: UpnpObjectType {
name: model.name().to_string(),
object_type: "ServiceInstance".to_string(),
},
model: Arc::new(model.clone()),
identifier: model.identifier().to_string(),
device: None,
statevariables,
actions,
subscribers: Arc::new(RwLock::new(HashMap::new())),
changed_buffer: Arc::new(Mutex::new(HashMap::new())),
seqid: Arc::new(Mutex::new(HashMap::new())),
}
}
}
impl UpnpTypedInstance for ServiceInstance {
fn get_model(&self) -> &Self::Model {
&self.model
}
}
impl UpnpObject for ServiceInstance {
fn to_xml_element(&self) -> Element {
let mut elem = Element::new("service");
let mut service_type = Element::new("serviceType");
service_type.children.push(XMLNode::Text(self.service_type()));
elem.children.push(XMLNode::Element(service_type));
let mut service_id = Element::new("serviceId");
service_id.children.push(XMLNode::Text(self.service_id()));
elem.children.push(XMLNode::Element(service_id));
let mut scpd_url = Element::new("SCPDURL");
scpd_url.children.push(XMLNode::Text(self.scpd_url()));
elem.children.push(XMLNode::Element(scpd_url));
let mut control_url = Element::new("controlURL");
control_url.children.push(XMLNode::Text(self.control_url()));
elem.children.push(XMLNode::Element(control_url));
let mut event_sub_url = Element::new("eventSubURL");
event_sub_url.children.push(XMLNode::Text(self.event_sub_url()));
elem.children.push(XMLNode::Element(event_sub_url));
elem
}
}
impl ServiceInstance {
/// Retourne l'identifiant du service.
pub fn identifier(&self) -> &str {
&self.identifier
}
/// Retourne le type de service UPnP.
///
/// Format: `urn:schemas-upnp-org:service:{name}:{version}`
pub fn service_type(&self) -> String {
self.model.service_type()
}
/// Retourne l'ID de service UPnP.
///
/// Format: `urn:upnp-org:serviceId:{identifier}`
pub fn service_id(&self) -> String {
format!("urn:upnp-org:serviceId:{}", self.identifier)
}
/// Raccourci pour obtenir une variable d'état par nom
pub fn get_variable(&self, name: &str) -> Option<Arc<StateVarInstance>> {
self.statevariables.get_by_name(name)
}
/// Raccourci pour obtenir une action par nom
pub fn get_action(&self, name: &str) -> Option<Arc<ActionInstance>> {
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()),
None => format!("/service/{}", self.get_name()),
}
}
/// Retourne l'URL de contrôle SOAP.
pub fn control_url(&self) -> String {
format!("{}/control", self.base_route())
}
/// Retourne l'URL de souscription aux événements.
pub fn event_sub_url(&self) -> String {
format!("{}/event", self.base_route())
}
/// Retourne l'URL de la description SCPD.
pub fn scpd_url(&self) -> String {
format!("{}/desc.xml", self.base_route())
}
/// Retourne l'USN (Unique Service Name).
pub fn usn(&self) -> String {
match &self.device {
Some(device) => format!("uuid:{}::urn:{}", device.udn(), self.service_type()),
None => format!("uuid::urn:{}", self.service_type()),
}
}
/// Retourne les variables d'état.
pub fn statevariables(&self) -> &StateVarInstanceSet {
&self.statevariables
}
/// Retourne les actions.
pub fn actions(&self) -> &ActionInstanceSet {
&self.actions
}
/// Enregistre les routes UPnP dans le serveur Axum.
///
/// # Errors
///
/// Retourne une erreur si l'enregistrement des routes échoue.
pub async fn register_urls(&self, server: &mut crate::server::Server) -> Result<(), ServiceError> {
info!(
"✅ Service description for {}:{} available at : {}{}",
self.device.as_ref().map(|d| d.name()).unwrap_or("unknown"),
self.get_name(),
self.device.as_ref().map(|d| d.server_base_url()).unwrap_or_default(),
self.scpd_url(),
);
// Handler SCPD
let instance_scpd = self.clone();
server.add_handler(&self.scpd_url(), move || {
let instance = instance_scpd.clone();
async move { instance.scpd_handler().await }
}).await;
// Handler control
let instance_control = self.clone();
server.add_post_handler_with_state(
&self.control_url(),
control_handler,
instance_control,
).await;
// Handler événements
let instance_event = self.clone();
server.add_handler_with_state(
&self.event_sub_url(),
event_sub_handler,
instance_event,
).await;
Ok(())
}
/// Génère l'élément XML SCPD.
pub fn scpd_element(&self) -> Element {
let mut elem = Element::new("scpd");
elem.attributes.insert(
"xmlns".to_string(),
"urn:schemas-upnp-org:service-1-0".to_string(),
);
// specVersion
let mut spec = Element::new("specVersion");
let mut major = Element::new("major");
major.children.push(XMLNode::Text("1".to_string()));
spec.children.push(XMLNode::Element(major));
let mut minor = Element::new("minor");
minor.children.push(XMLNode::Text("0".to_string()));
spec.children.push(XMLNode::Element(minor));
elem.children.push(XMLNode::Element(spec));
// actionList
if !self.actions.all().is_empty() {
elem.children.push(XMLNode::Element(
self.actions.to_xml_element()
));
}
// serviceStateTable
if !self.statevariables.all().is_empty() {
elem.children.push(XMLNode::Element(
self.statevariables.to_xml_element()
));
}
elem
}
/// Handler pour la description SCPD.
async fn scpd_handler(&self) -> Response {
let elem = self.scpd_element();
let config = EmitterConfig::new()
.perform_indent(true)
.indent_string(" ");
let mut xml_output = Vec::new();
if let Err(e) = elem.write_with_config(&mut xml_output, config) {
error!("Failed to serialize SCPD XML: {}", e);
return StatusCode::INTERNAL_SERVER_ERROR.into_response();
}
let mut xml = String::from_utf8_lossy(&xml_output).to_string();
// Ajouter l'en-tête XML
xml.insert_str(0, "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n");
(
StatusCode::OK,
[(axum::http::header::CONTENT_TYPE, "text/xml; charset=\"utf-8\"")],
xml,
).into_response()
}
/// Ajoute un abonné aux événements.
pub async fn add_subscriber(&self, sid: String, callback: String) {
let mut subscribers = self.subscribers.write().unwrap();
subscribers.insert(sid, callback);
}
/// Renouvelle un abonnement.
pub async fn renew_subscriber(&self, sid: &str, timeout: &str) {
info!("♻️ Renewed SID {} for timeout {}", sid, timeout);
}
/// Supprime un abonné.
pub async fn remove_subscriber(&self, sid: &str) {
let mut subscribers = self.subscribers.write().unwrap();
subscribers.remove(sid);
}
/// Envoie l'événement initial à un nouvel abonné.
pub async fn send_initial_event(&self, sid: String) {
let callback = {
let subscribers = self.subscribers.read().unwrap();
subscribers.get(&sid).cloned()
};
if let Some(callback) = callback {
let mut changed = HashMap::new();
for sv in self.statevariables.all() {
if sv.is_sending_notification() {
changed.insert(sv.get_name().to_string(), sv.value().to_string());
}
}
if changed.is_empty() {
return;
}
tokio::spawn(async move {
let callback = callback.trim().trim_matches(|c| c == '<' || c == '>');
let mut body = r#"<e:propertyset xmlns:e="urn:schemas-upnp-org:event-1-0">"#.to_string();
for (name, val) in changed {
body.push_str(&format!("<e:property><{0}>{1}</{0}></e:property>", name, val));
}
body.push_str("</e:propertyset>");
let client = reqwest::Client::new();
match client
.request(reqwest::Method::from_bytes(b"NOTIFY").unwrap(), callback)
.header("Content-Type", r#"text/xml; charset="utf-8"#)
.header("NT", "upnp:event")
.header("NTS", "upnp:propchange")
.header("SID", &sid)
.header("SEQ", "0")
.body(body)
.send()
.await
{
Ok(resp) => {
info!("✅ Initial event sent to {}, status={}", callback, resp.status());
}
Err(e) => {
error!("Failed to send initial event to {}: {}", callback, e);
}
}
});
}
}
/// Marque un changement à notifier.
pub fn event_to_be_sent(&self, name: String, value: String) {
let mut buffer = self.changed_buffer.lock().unwrap();
buffer.insert(name, value);
}
/// Récupère le prochain numéro de séquence pour un abonné.
fn next_seq(&self, sid: &str) -> String {
let mut seqid = self.seqid.lock().unwrap();
let counter = seqid.entry(sid.to_string()).or_insert(0);
*counter += 1;
counter.to_string()
}
/// Notifie tous les abonnés des changements.
pub async fn notify_subscribers(&self) {
let subscribers_copy = {
let subscribers = self.subscribers.read().unwrap();
if subscribers.is_empty() {
return;
}
subscribers.clone()
};
let changed = {
let mut buffer = self.changed_buffer.lock().unwrap();
if buffer.is_empty() {
return;
}
std::mem::take(&mut *buffer)
};
for (sid, callback) in subscribers_copy {
let changed_clone = changed.clone();
let seq = self.next_seq(&sid);
tokio::spawn(async move {
let callback = callback.trim().trim_matches(|c| c == '<' || c == '>');
let mut body = r#"<e:propertyset xmlns:e="urn:schemas-upnp-org:event-1-0">"#.to_string();
for (name, val) in changed_clone {
body.push_str(&format!("<e:property><{0}>{1}</{0}></e:property>", name, val));
}
body.push_str("</e:propertyset>");
let client = reqwest::Client::new();
match client
.request(reqwest::Method::from_bytes(b"NOTIFY").unwrap(), callback)
.header("Content-Type", r#"text/xml; charset="utf-8"#)
.header("NT", "upnp:event")
.header("NTS", "upnp:propchange")
.header("SID", &sid)
.header("SEQ", seq)
.body(body)
.send()
.await
{
Ok(_) => {
info!("✅ Notified subscriber {} of changes", callback);
}
Err(e) => {
error!("Failed to notify subscriber {}: {}", callback, e);
}
}
});
}
}
/// Démarre le notifier périodique.
///
/// # Arguments
///
/// * `interval` - Intervalle entre les notifications
///
/// # Returns
///
/// Un handle vers la tâche tokio du notifier.
pub fn start_notifier(&self, interval: Duration) -> tokio::task::JoinHandle<()> {
let instance = self.clone();
tokio::spawn(async move {
let mut ticker = time::interval(interval);
info!("✅ Starting notifier every {:?}", interval);
loop {
ticker.tick().await;
instance.notify_subscribers().await;
}
})
}
}
/// Handler Axum pour les événements (SUBSCRIBE/UNSUBSCRIBE).
async fn event_sub_handler(
State(instance): State<ServiceInstance>,
headers: HeaderMap,
req: Request<Body>,
) -> Response {
info!("📡 Event Subscription request for {}", instance.get_name());
let method = req.method().as_str();
let sid = headers.get("SID").and_then(|v| v.to_str().ok()).unwrap_or("");
let timeout = headers.get("Timeout").and_then(|v| v.to_str().ok()).unwrap_or("");
let callback = headers.get("Callback").and_then(|v| v.to_str().ok()).unwrap_or("");
match method {
METHOD_SUBSCRIBE => {
let (response_sid, response_timeout) = if sid.is_empty() {
// Nouvelle souscription
let new_sid = format!("uuid:{}", uuid::Uuid::new_v4());
if !callback.is_empty() {
instance.add_subscriber(new_sid.clone(), callback.to_string()).await;
}
let timeout_val = if timeout.is_empty() {
"Second-1800"
} else {
timeout
};
info!("🔒 New subscription: SID={}, Callback={}, Timeout={}", new_sid, callback, timeout_val);
let sid_clone = new_sid.clone();
let instance_clone = instance.clone();
tokio::spawn(async move {
instance_clone.send_initial_event(sid_clone).await;
});
(new_sid, timeout_val.to_string())
} else {
// Renouvellement
instance.renew_subscriber(sid, timeout).await;
info!("♻️ Renew subscription: SID={}, Timeout={}", sid, timeout);
(sid.to_string(), timeout.to_string())
};
(
StatusCode::OK,
[
(
axum::http::header::HeaderName::from_static("sid"),
axum::http::HeaderValue::from_str(&response_sid).unwrap()
),
(
axum::http::header::HeaderName::from_static("timeout"),
axum::http::HeaderValue::from_str(&response_timeout).unwrap()
),
],
).into_response()
}
METHOD_UNSUBSCRIBE => {
if !sid.is_empty() {
instance.remove_subscriber(sid).await;
info!("❌ Unsubscribe SID={}", sid);
}
StatusCode::OK.into_response()
}
_ => {
warn!("Unsupported EventSub method: {}", method);
StatusCode::METHOD_NOT_ALLOWED.into_response()
}
}
}
/// Handler Axum pour le contrôle SOAP.
async fn control_handler(
State(instance): State<ServiceInstance>,
body: String,
) -> Response {
info!("📡 Control request for {}", instance.get_name());
// TODO: Parser le SOAP et appeler l'action correspondante
let response_xml = format!(
r#"<?xml version="1.0"?>
<s:Envelope xmlns:s="http://schemas.xmlsoap.org/soap/envelope/"
s:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/">
<s:Body>
<u:Response xmlns:u="{}">
</u:Response>
</s:Body>
</s:Envelope>"#,
instance.service_type()
);
(
StatusCode::OK,
[(axum::http::header::CONTENT_TYPE, "text/xml; charset=\"utf-8\"")],
response_xml,
).into_response()
}
#[cfg(test)]
mod tests {
use super::*;
use crate::services::Service;
#[test]
fn test_service_instance_creation() {
let service = Service::new("AVTransport".to_string());
let instance = ServiceInstance::new(&service);
assert_eq!(instance.get_name(), "AVTransport");
assert_eq!(instance.identifier(), "AVTransport");
}
#[test]
fn test_service_urls() {
let service = Service::new("AVTransport".to_string());
let instance = ServiceInstance::new(&service);
assert_eq!(instance.base_route(), "/service/AVTransport");
assert_eq!(instance.control_url(), "/service/AVTransport/control");
assert_eq!(instance.event_sub_url(), "/service/AVTransport/event");
assert_eq!(instance.scpd_url(), "/service/AVTransport/desc.xml");
}
#[test]
fn test_service_type() {
let mut service = Service::new("AVTransport".to_string());
service.set_version(2).unwrap();
let instance = ServiceInstance::new(&service);
assert_eq!(
instance.service_type(),
"urn:schemas-upnp-org:service:AVTransport:2"
);
}
}

View File

@@ -0,0 +1,57 @@
//! Implémentation des traits UPnP pour Service.
use xmltree::{Element, XMLNode};
use crate::{
services::{Service, ServiceInstance},
UpnpObject, UpnpModel, UpnpTyped, UpnpObjectType,
};
impl std::fmt::Display for Service {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
write!(f, "Service({}:{})", self.name(), self.version())
}
}
impl UpnpTyped for Service {
fn as_upnp_object_type(&self) -> &UpnpObjectType {
&self.object
}
}
impl UpnpObject for Service {
fn to_xml_element(&self) -> Element {
let mut elem = Element::new("service");
// serviceType
let mut service_type = Element::new("serviceType");
service_type.children.push(XMLNode::Text(self.service_type()));
elem.children.push(XMLNode::Element(service_type));
// serviceId
let mut service_id = Element::new("serviceId");
service_id.children.push(XMLNode::Text(self.service_id()));
elem.children.push(XMLNode::Element(service_id));
// SCPDURL
let mut SCPDURL = Element::new("SCPDURL");
SCPDURL.children.push(XMLNode::Text(self.scpd_url()));
elem.children.push(XMLNode::Element(SCPDURL));
// controlURL
let mut controlURL = Element::new("controlURL");
controlURL.children.push(XMLNode::Text(self.control_url()));
elem.children.push(XMLNode::Element(controlURL));
// SCPDURL
let mut eventSubURL = Element::new("eventSubURL");
eventSubURL.children.push(XMLNode::Text(self.event_url()));
elem.children.push(XMLNode::Element(eventSubURL));
elem
}
}
impl UpnpModel for Service {
type Instance = ServiceInstance;
}