9291 lines
264 KiB
Plaintext
9291 lines
264 KiB
Plaintext
|
|
# Debut des sources des crates
|
||
|
|
|
||
|
|
## fichier: `Cargo.toml`
|
||
|
|
|
||
|
|
```toml
|
||
|
|
[workspace]
|
||
|
|
resolver = "3"
|
||
|
|
members = ["PMOMusic", "pmoupnp","pmoconfig", "pmoutils", "pmodidl"]
|
||
|
|
```
|
||
|
|
|
||
|
|
## fichier: `pmoconfig/Cargo.toml`
|
||
|
|
|
||
|
|
```toml
|
||
|
|
|
||
|
|
[package]
|
||
|
|
name = "pmoconfig"
|
||
|
|
version = "0.1.0"
|
||
|
|
edition = "2021"
|
||
|
|
|
||
|
|
[dependencies]
|
||
|
|
pmoutils ={ path = "../pmoutils" }
|
||
|
|
|
||
|
|
serde = { version = "1.0", features = ["derive"] }
|
||
|
|
serde_yaml = "0.9.33"
|
||
|
|
lazy_static = "1.4.0"
|
||
|
|
dirs = "6.0.0"
|
||
|
|
log = "0.4.20"
|
||
|
|
anyhow = "1.0.75"
|
||
|
|
uuid = { version = "1.18.1", features = ["v4"] }
|
||
|
|
tracing = "0.1.41"```
|
||
|
|
|
||
|
|
## fichier: `pmoconfig/src/lib.rs`
|
||
|
|
|
||
|
|
```rust
|
||
|
|
use anyhow::{anyhow, Result};
|
||
|
|
use dirs::home_dir;
|
||
|
|
use lazy_static::lazy_static;
|
||
|
|
use pmoutils::guess_local_ip;
|
||
|
|
use serde_yaml::{Mapping, Value};
|
||
|
|
use std::{
|
||
|
|
env, fs,
|
||
|
|
path::{Path, PathBuf},
|
||
|
|
sync::{Arc, Mutex},
|
||
|
|
};
|
||
|
|
use tracing::{info, warn};
|
||
|
|
use uuid::Uuid;
|
||
|
|
|
||
|
|
// Configuration par défaut intégrée
|
||
|
|
const DEFAULT_CONFIG: &str = include_str!("pmomusic.yaml");
|
||
|
|
|
||
|
|
lazy_static! {
|
||
|
|
static ref CONFIG: Arc<Config> =
|
||
|
|
Arc::new(Config::load_config("").expect("Failed to load PMOMusic configuration"));
|
||
|
|
}
|
||
|
|
|
||
|
|
const ENV_CONFIG_FILE: &str = "PMOMUSIC_CONFIG";
|
||
|
|
const ENV_PREFIX: &str = "PMOMUSIC_CONFIG__";
|
||
|
|
|
||
|
|
#[derive(Debug)]
|
||
|
|
pub struct Config {
|
||
|
|
path: String,
|
||
|
|
data: Mutex<Value>,
|
||
|
|
}
|
||
|
|
|
||
|
|
// Implémentation manuelle de Clone
|
||
|
|
impl Clone for Config {
|
||
|
|
fn clone(&self) -> Self {
|
||
|
|
let data = self.data.lock().unwrap().clone();
|
||
|
|
Self {
|
||
|
|
path: self.path.clone(),
|
||
|
|
data: Mutex::new(data),
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
impl Config {
|
||
|
|
pub fn load_config(filename: &str) -> Result<Self> {
|
||
|
|
let mut path = filename.to_string();
|
||
|
|
let mut data: Option<Vec<u8>> = None;
|
||
|
|
|
||
|
|
// Essayer de charger depuis différents emplacements
|
||
|
|
if !filename.is_empty() {
|
||
|
|
info!(config_file=%path, "Trying to load config");
|
||
|
|
data = fs::read(&path).ok();
|
||
|
|
if data.is_none() {
|
||
|
|
warn!(config_file=%path, "Cannot read config file");
|
||
|
|
path.clear();
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
if path.is_empty() {
|
||
|
|
if let Ok(env_path) = env::var(ENV_CONFIG_FILE) {
|
||
|
|
info!(env_var=ENV_CONFIG_FILE, path=%env_path, "Trying to load config from env");
|
||
|
|
path = env_path.clone();
|
||
|
|
data = fs::read(&path).ok();
|
||
|
|
if data.is_none() {
|
||
|
|
warn!(config_file=%path, "Cannot read config file from env var");
|
||
|
|
path.clear();
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
if path.is_empty() {
|
||
|
|
let current_dir = env::current_dir().unwrap_or_else(|_| PathBuf::from("."));
|
||
|
|
path = current_dir
|
||
|
|
.join(".pmomusic.yml")
|
||
|
|
.to_string_lossy()
|
||
|
|
.to_string();
|
||
|
|
info!(config_file=%path, "Trying to load config file from current directory");
|
||
|
|
data = fs::read(&path).ok();
|
||
|
|
if data.is_none() {
|
||
|
|
warn!(config_file=%path, "Cannot read config file in current dir");
|
||
|
|
path.clear();
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
if path.is_empty() {
|
||
|
|
path = Self::get_home_yml_path();
|
||
|
|
info!(config_file=%path, "Trying to load config file from home directory");
|
||
|
|
data = fs::read(&path).ok();
|
||
|
|
if data.is_none() {
|
||
|
|
warn!(config_file=%path, "Cannot read config file in home directory");
|
||
|
|
path.clear();
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
let yaml_data = if let Some(d) = data {
|
||
|
|
d
|
||
|
|
} else {
|
||
|
|
info!("Using default embedded config");
|
||
|
|
DEFAULT_CONFIG.as_bytes().to_vec()
|
||
|
|
};
|
||
|
|
|
||
|
|
let mut config_value: Value = serde_yaml::from_slice(&yaml_data)?;
|
||
|
|
config_value = Self::lower_keys_value(config_value);
|
||
|
|
Self::apply_env_overrides(&mut config_value);
|
||
|
|
|
||
|
|
if path.is_empty() || !Self::is_writable(&path) {
|
||
|
|
let candidates = [
|
||
|
|
filename.to_string(),
|
||
|
|
env::var(ENV_CONFIG_FILE).unwrap_or_default(),
|
||
|
|
".pmomusic.yml".to_string(),
|
||
|
|
Self::get_home_yml_path(),
|
||
|
|
];
|
||
|
|
for candidate in candidates.iter().filter(|c| !c.is_empty()) {
|
||
|
|
if Self::is_writable(candidate) {
|
||
|
|
path = candidate.clone();
|
||
|
|
break;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
if path.is_empty() {
|
||
|
|
return Err(anyhow!("Cannot find a place to store config file"));
|
||
|
|
}
|
||
|
|
|
||
|
|
info!(config_file=%path, "Config file will be stored here");
|
||
|
|
|
||
|
|
let config = Config {
|
||
|
|
path,
|
||
|
|
data: Mutex::new(config_value),
|
||
|
|
};
|
||
|
|
config.save()?;
|
||
|
|
Ok(config)
|
||
|
|
}
|
||
|
|
|
||
|
|
pub fn save(&self) -> Result<()> {
|
||
|
|
let data = self.data.lock().unwrap();
|
||
|
|
let yaml = serde_yaml::to_string(&*data)?;
|
||
|
|
fs::write(&self.path, yaml)?;
|
||
|
|
Ok(())
|
||
|
|
}
|
||
|
|
|
||
|
|
pub fn set_value(&self, path: &[&str], value: Value) -> Result<()> {
|
||
|
|
let mut data = self.data.lock().unwrap();
|
||
|
|
Self::set_value_internal(&mut data, path, value.clone())?;
|
||
|
|
drop(data);
|
||
|
|
self.save()?;
|
||
|
|
Ok(())
|
||
|
|
}
|
||
|
|
|
||
|
|
fn set_value_internal(data: &mut Value, path: &[&str], value: Value) -> Result<()> {
|
||
|
|
if path.is_empty() {
|
||
|
|
*data = value;
|
||
|
|
return Ok(());
|
||
|
|
}
|
||
|
|
if let Value::Mapping(map) = data {
|
||
|
|
let key = path[0].to_lowercase();
|
||
|
|
let key_value = Value::String(key.clone());
|
||
|
|
if path.len() == 1 {
|
||
|
|
map.insert(key_value, value);
|
||
|
|
} else {
|
||
|
|
let entry = map
|
||
|
|
.entry(key_value)
|
||
|
|
.or_insert(Value::Mapping(Mapping::new()));
|
||
|
|
Self::set_value_internal(entry, &path[1..], value)?;
|
||
|
|
}
|
||
|
|
Ok(())
|
||
|
|
} else {
|
||
|
|
Err(anyhow!("Current node is not a map"))
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
pub fn get_value(&self, path: &[&str]) -> Result<Value> {
|
||
|
|
let data = self.data.lock().unwrap();
|
||
|
|
Self::get_value_internal(&data, path)
|
||
|
|
}
|
||
|
|
|
||
|
|
fn get_value_internal(data: &Value, path: &[&str]) -> Result<Value> {
|
||
|
|
let mut current = data;
|
||
|
|
for (i, key) in path.iter().enumerate() {
|
||
|
|
if let Value::Mapping(map) = current {
|
||
|
|
let key = key.to_lowercase();
|
||
|
|
if let Some(next) = map.get(&Value::String(key)) {
|
||
|
|
current = next;
|
||
|
|
} else {
|
||
|
|
return Err(anyhow!("Path {} does not exist", path[..=i].join(".")));
|
||
|
|
}
|
||
|
|
} else {
|
||
|
|
return Err(anyhow!("Path {} is not a Config", path[..i].join(".")));
|
||
|
|
}
|
||
|
|
}
|
||
|
|
Ok(current.clone())
|
||
|
|
}
|
||
|
|
|
||
|
|
fn get_home_yml_path() -> String {
|
||
|
|
home_dir()
|
||
|
|
.map(|p| p.join(".pmomusic.yml"))
|
||
|
|
.unwrap_or_else(|| PathBuf::from("."))
|
||
|
|
.to_string_lossy()
|
||
|
|
.to_string()
|
||
|
|
}
|
||
|
|
|
||
|
|
fn apply_env_overrides(config: &mut Value) {
|
||
|
|
for (key, value) in env::vars() {
|
||
|
|
if key.starts_with(ENV_PREFIX) {
|
||
|
|
let key_path = key
|
||
|
|
.trim_start_matches(ENV_PREFIX)
|
||
|
|
.split("__")
|
||
|
|
.collect::<Vec<_>>();
|
||
|
|
let yaml_value = Self::convert_env_value(&value);
|
||
|
|
let _ = Self::set_value_internal(config, &key_path, yaml_value);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
fn convert_env_value(value: &str) -> Value {
|
||
|
|
if let Ok(parsed) = serde_yaml::from_str::<Value>(value) {
|
||
|
|
return parsed;
|
||
|
|
}
|
||
|
|
Value::String(value.to_string())
|
||
|
|
}
|
||
|
|
|
||
|
|
fn lower_keys_value(value: Value) -> Value {
|
||
|
|
match value {
|
||
|
|
Value::Mapping(map) => {
|
||
|
|
let mut new_map = Mapping::new();
|
||
|
|
for (k, v) in map {
|
||
|
|
if let Value::String(s) = k {
|
||
|
|
let new_key = Value::String(s.to_lowercase());
|
||
|
|
let new_val = Self::lower_keys_value(v);
|
||
|
|
new_map.insert(new_key, new_val);
|
||
|
|
} else {
|
||
|
|
new_map.insert(k, Self::lower_keys_value(v));
|
||
|
|
}
|
||
|
|
}
|
||
|
|
Value::Mapping(new_map)
|
||
|
|
}
|
||
|
|
Value::Sequence(seq) => {
|
||
|
|
Value::Sequence(seq.into_iter().map(Self::lower_keys_value).collect())
|
||
|
|
}
|
||
|
|
_ => value,
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
fn is_writable(path: &str) -> bool {
|
||
|
|
let path = Path::new(path);
|
||
|
|
if let Some(parent) = path.parent() {
|
||
|
|
fs::metadata(parent)
|
||
|
|
.map(|m| !m.permissions().readonly())
|
||
|
|
.unwrap_or(false)
|
||
|
|
} else {
|
||
|
|
false
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
pub fn get_base_url(&self) -> String {
|
||
|
|
match self.get_value(&["host", "base_url"]) {
|
||
|
|
Ok(Value::String(s)) if !s.is_empty() => s,
|
||
|
|
Ok(_) => {
|
||
|
|
tracing::warn!("Base URL is not a string or empty, using default localhost");
|
||
|
|
guess_local_ip()
|
||
|
|
}
|
||
|
|
Err(err) => {
|
||
|
|
tracing::warn!("Failed to get base URL: {}, using default localhost", err);
|
||
|
|
guess_local_ip()
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
pub fn get_http_port(&self) -> u16 {
|
||
|
|
match self.get_value(&["host", "http_port"]) {
|
||
|
|
Ok(Value::Number(n)) if n.is_i64() => n.as_i64().unwrap() as u16,
|
||
|
|
Ok(Value::String(s)) => match s.parse::<u16>() {
|
||
|
|
Ok(port) => port,
|
||
|
|
Err(_) => {
|
||
|
|
tracing::warn!("Invalid HTTP port '{}', using default 8080", s);
|
||
|
|
8080
|
||
|
|
}
|
||
|
|
},
|
||
|
|
Ok(_) => {
|
||
|
|
tracing::warn!("HTTP port not a number or string, using default 8080");
|
||
|
|
8080
|
||
|
|
}
|
||
|
|
Err(err) => {
|
||
|
|
tracing::warn!("Failed to get HTTP port: {}, using default 8080", err);
|
||
|
|
8080
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
pub fn get_device_udn(&self, devtype: &str, name: &str) -> Result<String> {
|
||
|
|
let path = &["devices", devtype, name, "udn"];
|
||
|
|
match self.get_value(path) {
|
||
|
|
Ok(Value::String(udn)) => Ok(udn),
|
||
|
|
_ => {
|
||
|
|
let new_udn = Uuid::new_v4().to_string();
|
||
|
|
self.set_value(path, Value::String(new_udn.clone()))?;
|
||
|
|
Ok(new_udn)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
pub fn get_cover_cache_dir(&self) -> Result<String> {
|
||
|
|
match self.get_value(&["host", "cover_cache", "directory"])? {
|
||
|
|
Value::String(s) => Ok(s),
|
||
|
|
_ => Ok("./.pmomusic_covers".to_string()),
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
pub fn get_cover_cache_size(&self) -> Result<usize> {
|
||
|
|
match self.get_value(&["host", "cover_cache", "size"])? {
|
||
|
|
Value::Number(n) if n.is_i64() => Ok(n.as_i64().unwrap() as usize),
|
||
|
|
Value::Number(n) if n.is_u64() => Ok(n.as_u64().unwrap() as usize),
|
||
|
|
_ => Ok(2000),
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
/// Retourne l'instance globale
|
||
|
|
pub fn get_config() -> Arc<Config> {
|
||
|
|
CONFIG.clone()
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
## fichier: `pmodidl/Cargo.toml`
|
||
|
|
|
||
|
|
```toml
|
||
|
|
[package]
|
||
|
|
name = "pmodidl"
|
||
|
|
version = "0.1.0"
|
||
|
|
edition = "2024"
|
||
|
|
|
||
|
|
[dependencies]
|
||
|
|
serde = "1.0.228"
|
||
|
|
utoipa = { version = "5.4.0", features = ["axum_extras"] }
|
||
|
|
utoipa-swagger-ui = { version = "9.0.2", features = ["axum"] }
|
||
|
|
quick-xml = { version = "0.38.3", features = ["serialize"] }
|
||
|
|
bevy_reflect = "0.17.1"
|
||
|
|
bevy_reflect_derive = "0.17.1"
|
||
|
|
```
|
||
|
|
|
||
|
|
## fichier: `pmodidl/src/lib.rs`
|
||
|
|
|
||
|
|
```rust
|
||
|
|
//! # pmodidl - DIDL-Lite Parser
|
||
|
|
//!
|
||
|
|
//! Parser et utilitaires pour le format DIDL-Lite utilisé dans UPnP/DLNA.
|
||
|
|
|
||
|
|
use serde::{Deserialize, Serialize};
|
||
|
|
use std::fmt::Write;
|
||
|
|
use bevy_reflect::Reflect;
|
||
|
|
|
||
|
|
// ============= Couche d'abstraction générique =============
|
||
|
|
|
||
|
|
/// Trait pour tout parser de métadonnées média
|
||
|
|
pub trait MediaMetadataParser: Sized {
|
||
|
|
type Error: std::error::Error + Send + Sync + 'static;
|
||
|
|
|
||
|
|
/// Parse une chaîne de métadonnées
|
||
|
|
fn parse(input: &str) -> Result<Self, Self::Error>;
|
||
|
|
|
||
|
|
/// Retourne le format du parser
|
||
|
|
fn format_name() -> &'static str;
|
||
|
|
}
|
||
|
|
|
||
|
|
/// Enveloppe générique pour tout type de métadonnées parsées
|
||
|
|
#[derive(Debug, Clone, Serialize, Deserialize, Reflect)]
|
||
|
|
pub struct ParsedMetadata<T> {
|
||
|
|
/// Format du document (ex: "DIDL-Lite", "RSS", etc.)
|
||
|
|
pub format: String,
|
||
|
|
|
||
|
|
/// Données parsées
|
||
|
|
pub data: T,
|
||
|
|
|
||
|
|
/// Timestamp du parsing (exclu de la réflexion car SystemTime n'implémente pas Reflect)
|
||
|
|
#[reflect(ignore)]
|
||
|
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||
|
|
pub parsed_at: Option<std::time::SystemTime>,
|
||
|
|
}
|
||
|
|
|
||
|
|
impl<T> ParsedMetadata<T> {
|
||
|
|
pub fn new(format: impl Into<String>, data: T) -> Self {
|
||
|
|
Self {
|
||
|
|
format: format.into(),
|
||
|
|
data,
|
||
|
|
parsed_at: Some(std::time::SystemTime::now()),
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
/// Transforme les données avec une fonction
|
||
|
|
pub fn map<U, F>(self, f: F) -> ParsedMetadata<U>
|
||
|
|
where
|
||
|
|
F: FnOnce(T) -> U,
|
||
|
|
{
|
||
|
|
ParsedMetadata {
|
||
|
|
format: self.format,
|
||
|
|
data: f(self.data),
|
||
|
|
parsed_at: self.parsed_at,
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
/// Fonction helper pour parser et envelopper automatiquement
|
||
|
|
pub fn parse_metadata<P: MediaMetadataParser>(input: &str) -> Result<ParsedMetadata<P>, P::Error> {
|
||
|
|
let data = P::parse(input)?;
|
||
|
|
Ok(ParsedMetadata::new(P::format_name(), data))
|
||
|
|
}
|
||
|
|
|
||
|
|
// ============= Implémentation pour DIDLLite =============
|
||
|
|
|
||
|
|
impl MediaMetadataParser for DIDLLite {
|
||
|
|
type Error = quick_xml::de::DeError;
|
||
|
|
|
||
|
|
fn parse(input: &str) -> Result<Self, Self::Error> {
|
||
|
|
quick_xml::de::from_str(input)
|
||
|
|
}
|
||
|
|
|
||
|
|
fn format_name() -> &'static str {
|
||
|
|
"DIDL-Lite"
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
/// Type alias pour faciliter l'utilisation
|
||
|
|
pub type DidlMetadata = ParsedMetadata<DIDLLite>;
|
||
|
|
|
||
|
|
// ============= Structures DIDL-Lite =============
|
||
|
|
|
||
|
|
|
||
|
|
/// Racine d'un document DIDL-Lite
|
||
|
|
#[derive(Debug, Clone, Serialize, Deserialize, utoipa::ToSchema, Reflect)]
|
||
|
|
#[serde(rename = "DIDL-Lite")]
|
||
|
|
pub struct DIDLLite {
|
||
|
|
#[serde(rename = "@xmlns")]
|
||
|
|
pub xmlns: String,
|
||
|
|
|
||
|
|
#[serde(rename = "@xmlns:upnp", skip_serializing_if = "Option::is_none")]
|
||
|
|
pub xmlns_upnp: Option<String>,
|
||
|
|
|
||
|
|
#[serde(rename = "@xmlns:dc", skip_serializing_if = "Option::is_none")]
|
||
|
|
pub xmlns_dc: Option<String>,
|
||
|
|
|
||
|
|
#[serde(rename = "@xmlns:dlna", skip_serializing_if = "Option::is_none")]
|
||
|
|
pub xmlns_dlna: Option<String>,
|
||
|
|
|
||
|
|
#[serde(rename = "@xmlns:sec", skip_serializing_if = "Option::is_none")]
|
||
|
|
pub xmlns_sec: Option<String>,
|
||
|
|
|
||
|
|
#[serde(rename = "@xmlns:pv", skip_serializing_if = "Option::is_none")]
|
||
|
|
pub xmlns_pv: Option<String>,
|
||
|
|
|
||
|
|
#[serde(rename = "container", default)]
|
||
|
|
pub containers: Vec<Container>,
|
||
|
|
|
||
|
|
#[serde(rename = "item", default)]
|
||
|
|
pub items: Vec<Item>,
|
||
|
|
}
|
||
|
|
|
||
|
|
/// Container pouvant contenir d'autres containers ou items
|
||
|
|
#[derive(Debug, Clone, Serialize, Deserialize, utoipa::ToSchema, Reflect)]
|
||
|
|
pub struct Container {
|
||
|
|
#[serde(rename = "@id")]
|
||
|
|
pub id: String,
|
||
|
|
|
||
|
|
#[serde(rename = "@parentID")]
|
||
|
|
pub parent_id: String,
|
||
|
|
|
||
|
|
#[serde(rename = "@restricted", skip_serializing_if = "Option::is_none")]
|
||
|
|
pub restricted: Option<String>,
|
||
|
|
|
||
|
|
#[serde(rename = "@childCount", skip_serializing_if = "Option::is_none")]
|
||
|
|
pub child_count: Option<String>,
|
||
|
|
|
||
|
|
#[serde(rename = "dc:title", alias = "title")]
|
||
|
|
pub title: String,
|
||
|
|
|
||
|
|
#[serde(rename = "upnp:class", alias = "class")]
|
||
|
|
pub class: String,
|
||
|
|
|
||
|
|
#[serde(rename = "container", default)]
|
||
|
|
pub containers: Vec<Container>,
|
||
|
|
|
||
|
|
#[serde(rename = "item", default)]
|
||
|
|
pub items: Vec<Item>,
|
||
|
|
}
|
||
|
|
|
||
|
|
/// Item représentant un objet audio
|
||
|
|
#[derive(Debug, Clone, Serialize, Deserialize, utoipa::ToSchema, Reflect)]
|
||
|
|
pub struct Item {
|
||
|
|
#[serde(rename = "@id")]
|
||
|
|
pub id: String,
|
||
|
|
|
||
|
|
#[serde(rename = "@parentID")]
|
||
|
|
pub parent_id: String,
|
||
|
|
|
||
|
|
#[serde(rename = "@restricted", skip_serializing_if = "Option::is_none")]
|
||
|
|
pub restricted: Option<String>,
|
||
|
|
|
||
|
|
#[serde(rename = "dc:title", alias = "title")]
|
||
|
|
pub title: String,
|
||
|
|
|
||
|
|
#[serde(rename = "dc:creator", alias = "creator", skip_serializing_if = "Option::is_none")]
|
||
|
|
pub creator: Option<String>,
|
||
|
|
|
||
|
|
#[serde(rename = "upnp:class", alias = "class")]
|
||
|
|
pub class: String,
|
||
|
|
|
||
|
|
#[serde(rename = "upnp:artist", alias = "artist", skip_serializing_if = "Option::is_none")]
|
||
|
|
pub artist: Option<String>,
|
||
|
|
|
||
|
|
#[serde(rename = "upnp:album", alias = "album", skip_serializing_if = "Option::is_none")]
|
||
|
|
pub album: Option<String>,
|
||
|
|
|
||
|
|
#[serde(rename = "upnp:genre", alias = "genre", skip_serializing_if = "Option::is_none")]
|
||
|
|
pub genre: Option<String>,
|
||
|
|
|
||
|
|
#[serde(rename = "upnp:albumArtURI", alias = "albumArtURI", skip_serializing_if = "Option::is_none")]
|
||
|
|
pub album_art: Option<String>,
|
||
|
|
|
||
|
|
#[serde(skip)]
|
||
|
|
pub album_art_pk: Option<String>,
|
||
|
|
|
||
|
|
#[serde(rename = "dc:date", alias = "date", skip_serializing_if = "Option::is_none")]
|
||
|
|
pub date: Option<String>,
|
||
|
|
|
||
|
|
#[serde(rename = "upnp:originalTrackNumber", alias = "originalTrackNumber", skip_serializing_if = "Option::is_none")]
|
||
|
|
pub original_track_number: Option<String>,
|
||
|
|
|
||
|
|
#[serde(rename = "res", default)]
|
||
|
|
pub resources: Vec<Resource>,
|
||
|
|
|
||
|
|
#[serde(rename = "desc", default)]
|
||
|
|
pub descriptions: Vec<Description>,
|
||
|
|
}
|
||
|
|
|
||
|
|
/// Ressource média (fichier audio)
|
||
|
|
#[derive(Debug, Clone, Serialize, Deserialize, utoipa::ToSchema, Reflect)]
|
||
|
|
pub struct Resource {
|
||
|
|
#[serde(rename = "@protocolInfo")]
|
||
|
|
pub protocol_info: String,
|
||
|
|
|
||
|
|
#[serde(rename = "@bitsPerSample", skip_serializing_if = "Option::is_none")]
|
||
|
|
pub bits_per_sample: Option<String>,
|
||
|
|
|
||
|
|
#[serde(rename = "@sampleFrequency", skip_serializing_if = "Option::is_none")]
|
||
|
|
pub sample_frequency: Option<String>,
|
||
|
|
|
||
|
|
#[serde(rename = "@nrAudioChannels", skip_serializing_if = "Option::is_none")]
|
||
|
|
pub nr_audio_channels: Option<String>,
|
||
|
|
|
||
|
|
#[serde(rename = "@duration", skip_serializing_if = "Option::is_none")]
|
||
|
|
pub duration: Option<String>,
|
||
|
|
|
||
|
|
#[serde(rename = "$text")]
|
||
|
|
pub url: String,
|
||
|
|
}
|
||
|
|
|
||
|
|
/// Description avec métadonnées additionnelles (replaygain, etc.)
|
||
|
|
#[derive(Debug, Clone, Serialize, Deserialize, utoipa::ToSchema, Reflect)]
|
||
|
|
pub struct Description {
|
||
|
|
#[serde(rename = "@id", skip_serializing_if = "Option::is_none")]
|
||
|
|
pub id: Option<String>,
|
||
|
|
|
||
|
|
#[serde(rename = "@nameSpace", skip_serializing_if = "Option::is_none")]
|
||
|
|
pub namespace: Option<String>,
|
||
|
|
|
||
|
|
#[serde(rename = "track_gain", skip_serializing_if = "Option::is_none")]
|
||
|
|
pub track_gain: Option<String>,
|
||
|
|
|
||
|
|
#[serde(rename = "track_peak", skip_serializing_if = "Option::is_none")]
|
||
|
|
pub track_peak: Option<String>,
|
||
|
|
}
|
||
|
|
|
||
|
|
// ============= Implémentation des méthodes =============
|
||
|
|
|
||
|
|
impl DIDLLite {
|
||
|
|
/// Itère sur tous les containers de manière récursive
|
||
|
|
pub fn all_containers(&self) -> impl Iterator<Item = &Container> {
|
||
|
|
AllContainersIter::new(&self.containers)
|
||
|
|
}
|
||
|
|
|
||
|
|
/// Itère sur tous les items de manière récursive
|
||
|
|
pub fn all_items(&self) -> impl Iterator<Item = &Item> {
|
||
|
|
AllItemsIter::new(&self.containers, &self.items)
|
||
|
|
}
|
||
|
|
|
||
|
|
/// Trouve un container par ID
|
||
|
|
pub fn get_container_by_id(&self, id: &str) -> Option<&Container> {
|
||
|
|
self.all_containers().find(|c| c.id == id)
|
||
|
|
}
|
||
|
|
|
||
|
|
/// Trouve un item par ID
|
||
|
|
pub fn get_item_by_id(&self, id: &str) -> Option<&Item> {
|
||
|
|
self.all_items().find(|i| i.id == id)
|
||
|
|
}
|
||
|
|
|
||
|
|
/// Filtre les containers
|
||
|
|
pub fn filter_containers<F>(&self, predicate: F) -> impl Iterator<Item = &Container>
|
||
|
|
where
|
||
|
|
F: Fn(&Container) -> bool,
|
||
|
|
{
|
||
|
|
self.all_containers().filter(move |c| predicate(c))
|
||
|
|
}
|
||
|
|
|
||
|
|
/// Filtre les items
|
||
|
|
pub fn filter_items<F>(&self, predicate: F) -> impl Iterator<Item = &Item>
|
||
|
|
where
|
||
|
|
F: Fn(&Item) -> bool,
|
||
|
|
{
|
||
|
|
self.all_items().filter(move |i| predicate(i))
|
||
|
|
}
|
||
|
|
|
||
|
|
/// Génère une représentation Markdown
|
||
|
|
pub fn to_markdown(&self) -> String {
|
||
|
|
let mut buf = String::new();
|
||
|
|
buf.push_str("### DIDL-Lite Document\n\n");
|
||
|
|
|
||
|
|
if !self.containers.is_empty() {
|
||
|
|
buf.push_str("#### Containers\n\n");
|
||
|
|
for container in &self.containers {
|
||
|
|
container.write_markdown(&mut buf, 0);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
if !self.items.is_empty() {
|
||
|
|
buf.push_str("#### Items\n\n");
|
||
|
|
for item in &self.items {
|
||
|
|
item.write_markdown(&mut buf, 0);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
buf
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
impl Container {
|
||
|
|
/// Itère sur tous les containers enfants récursivement
|
||
|
|
pub fn all_containers(&self) -> impl Iterator<Item = &Container> {
|
||
|
|
AllContainersIter::new(&self.containers)
|
||
|
|
}
|
||
|
|
|
||
|
|
/// Itère sur tous les items de ce container et ses enfants
|
||
|
|
pub fn all_items(&self) -> impl Iterator<Item = &Item> {
|
||
|
|
AllItemsIter::new(&self.containers, &self.items)
|
||
|
|
}
|
||
|
|
|
||
|
|
fn write_markdown(&self, buf: &mut String, depth: usize) {
|
||
|
|
let indent = " ".repeat(depth);
|
||
|
|
|
||
|
|
writeln!(buf, "{}- **Container**: {}", indent, self.title).unwrap();
|
||
|
|
writeln!(buf, "{} - ID: `{}`", indent, self.id).unwrap();
|
||
|
|
writeln!(buf, "{} - ParentID: `{}`", indent, self.parent_id).unwrap();
|
||
|
|
writeln!(buf, "{} - Class: `{}`", indent, self.class).unwrap();
|
||
|
|
|
||
|
|
if let Some(ref restricted) = self.restricted {
|
||
|
|
writeln!(buf, "{} - Restricted: `{}`", indent, restricted).unwrap();
|
||
|
|
}
|
||
|
|
if let Some(ref count) = self.child_count {
|
||
|
|
writeln!(buf, "{} - ChildCount: `{}`", indent, count).unwrap();
|
||
|
|
}
|
||
|
|
|
||
|
|
if !self.containers.is_empty() {
|
||
|
|
writeln!(buf, "{} - Subcontainers:", indent).unwrap();
|
||
|
|
for sub in &self.containers {
|
||
|
|
sub.write_markdown(buf, depth + 2);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
if !self.items.is_empty() {
|
||
|
|
writeln!(buf, "{} - Items:", indent).unwrap();
|
||
|
|
for item in &self.items {
|
||
|
|
item.write_markdown(buf, depth + 2);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
buf.push('\n');
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
impl Item {
|
||
|
|
/// Itère sur les ressources audio uniquement
|
||
|
|
pub fn audio_resources(&self) -> impl Iterator<Item = &Resource> {
|
||
|
|
self.resources.iter()
|
||
|
|
.filter(|r| r.protocol_info.contains("audio/"))
|
||
|
|
}
|
||
|
|
|
||
|
|
/// Retourne la ressource principale (première disponible)
|
||
|
|
pub fn primary_resource(&self) -> Option<&Resource> {
|
||
|
|
self.resources.first()
|
||
|
|
}
|
||
|
|
|
||
|
|
/// Itère sur les métadonnées sous forme de paires clé-valeur
|
||
|
|
pub fn metadata(&self) -> impl Iterator<Item = (&str, &str)> {
|
||
|
|
let mut pairs = Vec::new();
|
||
|
|
|
||
|
|
pairs.push(("title", self.title.as_str()));
|
||
|
|
|
||
|
|
if let Some(ref artist) = self.artist {
|
||
|
|
pairs.push(("artist", artist.as_str()));
|
||
|
|
}
|
||
|
|
if let Some(ref album) = self.album {
|
||
|
|
pairs.push(("album", album.as_str()));
|
||
|
|
}
|
||
|
|
if let Some(ref genre) = self.genre {
|
||
|
|
pairs.push(("genre", genre.as_str()));
|
||
|
|
}
|
||
|
|
if let Some(ref date) = self.date {
|
||
|
|
pairs.push(("date", date.as_str()));
|
||
|
|
}
|
||
|
|
if let Some(ref track) = self.original_track_number {
|
||
|
|
pairs.push(("trackNumber", track.as_str()));
|
||
|
|
}
|
||
|
|
|
||
|
|
for desc in &self.descriptions {
|
||
|
|
if let Some(ref gain) = desc.track_gain {
|
||
|
|
pairs.push(("replayGain", gain.as_str()));
|
||
|
|
}
|
||
|
|
if let Some(ref peak) = desc.track_peak {
|
||
|
|
pairs.push(("replayPeak", peak.as_str()));
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
pairs.into_iter()
|
||
|
|
}
|
||
|
|
|
||
|
|
fn write_markdown(&self, buf: &mut String, depth: usize) {
|
||
|
|
let indent = " ".repeat(depth);
|
||
|
|
|
||
|
|
writeln!(buf, "{}- **Item**: {}", indent, self.title).unwrap();
|
||
|
|
writeln!(buf, "{} - ID: `{}`", indent, self.id).unwrap();
|
||
|
|
writeln!(buf, "{} - ParentID: `{}`", indent, self.parent_id).unwrap();
|
||
|
|
writeln!(buf, "{} - Class: `{}`", indent, self.class).unwrap();
|
||
|
|
|
||
|
|
if let Some(ref creator) = self.creator {
|
||
|
|
writeln!(buf, "{} - Creator: {}", indent, creator).unwrap();
|
||
|
|
}
|
||
|
|
if let Some(ref artist) = self.artist {
|
||
|
|
writeln!(buf, "{} - Artist: {}", indent, artist).unwrap();
|
||
|
|
}
|
||
|
|
if let Some(ref album) = self.album {
|
||
|
|
writeln!(buf, "{} - Album: {}", indent, album).unwrap();
|
||
|
|
}
|
||
|
|
if let Some(ref genre) = self.genre {
|
||
|
|
writeln!(buf, "{} - Genre: {}", indent, genre).unwrap();
|
||
|
|
}
|
||
|
|
if let Some(ref art) = self.album_art {
|
||
|
|
writeln!(buf, "{} - Album Art: ", indent, art).unwrap();
|
||
|
|
}
|
||
|
|
if let Some(ref date) = self.date {
|
||
|
|
writeln!(buf, "{} - Date: {}", indent, date).unwrap();
|
||
|
|
}
|
||
|
|
if let Some(ref track) = self.original_track_number {
|
||
|
|
writeln!(buf, "{} - Track: {}", indent, track).unwrap();
|
||
|
|
}
|
||
|
|
|
||
|
|
if !self.resources.is_empty() {
|
||
|
|
writeln!(buf, "{} - Resources:", indent).unwrap();
|
||
|
|
for res in &self.resources {
|
||
|
|
writeln!(buf, "{} - URL: {}", indent, res.url).unwrap();
|
||
|
|
writeln!(buf, "{} - Protocol: `{}`", indent, res.protocol_info).unwrap();
|
||
|
|
if let Some(ref dur) = res.duration {
|
||
|
|
writeln!(buf, "{} - Duration: `{}`", indent, dur).unwrap();
|
||
|
|
}
|
||
|
|
if let Some(ref bits) = res.bits_per_sample {
|
||
|
|
writeln!(buf, "{} - BitsPerSample: `{}`", indent, bits).unwrap();
|
||
|
|
}
|
||
|
|
if let Some(ref freq) = res.sample_frequency {
|
||
|
|
writeln!(buf, "{} - SampleFrequency: `{}`", indent, freq).unwrap();
|
||
|
|
}
|
||
|
|
if let Some(ref channels) = res.nr_audio_channels {
|
||
|
|
writeln!(buf, "{} - Channels: `{}`", indent, channels).unwrap();
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
if !self.descriptions.is_empty() {
|
||
|
|
writeln!(buf, "{} - Descriptions:", indent).unwrap();
|
||
|
|
for desc in &self.descriptions {
|
||
|
|
if let Some(ref ns) = desc.namespace {
|
||
|
|
writeln!(buf, "{} - Namespace: `{}`", indent, ns).unwrap();
|
||
|
|
}
|
||
|
|
if let Some(ref gain) = desc.track_gain {
|
||
|
|
writeln!(buf, "{} - Track Gain: `{}`", indent, gain).unwrap();
|
||
|
|
}
|
||
|
|
if let Some(ref peak) = desc.track_peak {
|
||
|
|
writeln!(buf, "{} - Track Peak: `{}`", indent, peak).unwrap();
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
buf.push('\n');
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// ============= Itérateurs personnalisés =============
|
||
|
|
|
||
|
|
struct AllContainersIter<'a> {
|
||
|
|
stack: Vec<&'a Container>,
|
||
|
|
}
|
||
|
|
|
||
|
|
impl<'a> AllContainersIter<'a> {
|
||
|
|
fn new(containers: &'a [Container]) -> Self {
|
||
|
|
Self {
|
||
|
|
stack: containers.iter().collect(),
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
impl<'a> Iterator for AllContainersIter<'a> {
|
||
|
|
type Item = &'a Container;
|
||
|
|
|
||
|
|
fn next(&mut self) -> Option<Self::Item> {
|
||
|
|
self.stack.pop().map(|container| {
|
||
|
|
// Ajouter les enfants à la pile
|
||
|
|
self.stack.extend(container.containers.iter());
|
||
|
|
container
|
||
|
|
})
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
struct AllItemsIter<'a> {
|
||
|
|
containers: Vec<&'a Container>,
|
||
|
|
current_items: std::slice::Iter<'a, Item>,
|
||
|
|
}
|
||
|
|
|
||
|
|
impl<'a> AllItemsIter<'a> {
|
||
|
|
fn new(containers: &'a [Container], items: &'a [Item]) -> Self {
|
||
|
|
Self {
|
||
|
|
containers: containers.iter().collect(),
|
||
|
|
current_items: items.iter(),
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
impl<'a> Iterator for AllItemsIter<'a> {
|
||
|
|
type Item = &'a Item;
|
||
|
|
|
||
|
|
fn next(&mut self) -> Option<Self::Item> {
|
||
|
|
loop {
|
||
|
|
if let Some(item) = self.current_items.next() {
|
||
|
|
return Some(item);
|
||
|
|
}
|
||
|
|
|
||
|
|
let container = self.containers.pop()?;
|
||
|
|
self.containers.extend(container.containers.iter());
|
||
|
|
self.current_items = container.items.iter();
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
#[cfg(test)]
|
||
|
|
mod tests {
|
||
|
|
use super::*;
|
||
|
|
|
||
|
|
#[test]
|
||
|
|
fn test_parse_simple_didl() {
|
||
|
|
let xml = r#"
|
||
|
|
<DIDL-Lite xmlns="urn:schemas-upnp-org:metadata-1-0/DIDL-Lite/"
|
||
|
|
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||
|
|
xmlns:upnp="urn:schemas-upnp-org:metadata-1-0/upnp/">
|
||
|
|
<item id="1" parentID="0">
|
||
|
|
<dc:title>Test Song</dc:title>
|
||
|
|
<upnp:class>object.item.audioItem.musicTrack</upnp:class>
|
||
|
|
<res protocolInfo="http-get:*:audio/mpeg:*">http://example.com/song.mp3</res>
|
||
|
|
</item>
|
||
|
|
</DIDL-Lite>
|
||
|
|
"#;
|
||
|
|
|
||
|
|
let didl = DIDLLite::parse(xml).unwrap();
|
||
|
|
assert_eq!(didl.items.len(), 1);
|
||
|
|
assert_eq!(didl.items[0].title, "Test Song");
|
||
|
|
}
|
||
|
|
|
||
|
|
#[test]
|
||
|
|
fn test_parse_without_namespaces() {
|
||
|
|
// Teste un XML sans namespaces explicites (devices UPnP laxistes)
|
||
|
|
let xml = r#"
|
||
|
|
<DIDL-Lite xmlns="urn:schemas-upnp-org:metadata-1-0/DIDL-Lite/">
|
||
|
|
<item id="1" parentID="0">
|
||
|
|
<title>Test Song</title>
|
||
|
|
<class>object.item.audioItem.musicTrack</class>
|
||
|
|
<res protocolInfo="http-get:*:audio/mpeg:*">http://example.com/song.mp3</res>
|
||
|
|
</item>
|
||
|
|
</DIDL-Lite>
|
||
|
|
"#;
|
||
|
|
|
||
|
|
let didl = DIDLLite::parse(xml).unwrap();
|
||
|
|
assert_eq!(didl.items.len(), 1);
|
||
|
|
assert_eq!(didl.items[0].title, "Test Song");
|
||
|
|
}
|
||
|
|
|
||
|
|
#[test]
|
||
|
|
fn test_generic_parser() {
|
||
|
|
let xml = r#"
|
||
|
|
<DIDL-Lite xmlns="urn:schemas-upnp-org:metadata-1-0/DIDL-Lite/"
|
||
|
|
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||
|
|
xmlns:upnp="urn:schemas-upnp-org:metadata-1-0/upnp/">
|
||
|
|
</DIDL-Lite>
|
||
|
|
"#;
|
||
|
|
|
||
|
|
// Utiliser le parser générique
|
||
|
|
let metadata: DidlMetadata = parse_metadata(xml).unwrap();
|
||
|
|
|
||
|
|
assert_eq!(metadata.format, "DIDL-Lite");
|
||
|
|
assert!(metadata.parsed_at.is_some());
|
||
|
|
}
|
||
|
|
|
||
|
|
#[test]
|
||
|
|
fn test_metadata_map() {
|
||
|
|
let xml = r#"
|
||
|
|
<DIDL-Lite xmlns="urn:schemas-upnp-org:metadata-1-0/DIDL-Lite/"
|
||
|
|
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||
|
|
xmlns:upnp="urn:schemas-upnp-org:metadata-1-0/upnp/">
|
||
|
|
</DIDL-Lite>
|
||
|
|
"#;
|
||
|
|
|
||
|
|
let metadata: DidlMetadata = parse_metadata(xml).unwrap();
|
||
|
|
|
||
|
|
// Transformer les données
|
||
|
|
let item_count = metadata.map(|didl| didl.items.len());
|
||
|
|
|
||
|
|
assert_eq!(item_count.format, "DIDL-Lite");
|
||
|
|
assert_eq!(item_count.data, 0);
|
||
|
|
}
|
||
|
|
}```
|
||
|
|
|
||
|
|
## fichier: `PMOMusic/Cargo.toml`
|
||
|
|
|
||
|
|
```toml
|
||
|
|
[package]
|
||
|
|
name = "PMOMusic"
|
||
|
|
version = "0.1.0"
|
||
|
|
edition = "2024"
|
||
|
|
|
||
|
|
[dependencies]
|
||
|
|
pmoconfig = { path = "../pmoconfig" }
|
||
|
|
pmoupnp = { path = "../pmoupnp"}
|
||
|
|
|
||
|
|
|
||
|
|
tokio = { version = "1.35", features = ["rt-multi-thread", "macros", "sync", "time","signal"] }
|
||
|
|
tracing = "0.1.41"
|
||
|
|
tracing-subscriber = "0.3.20"
|
||
|
|
axum = "0.8.4"
|
||
|
|
serde_json = "1.0.145"
|
||
|
|
```
|
||
|
|
|
||
|
|
## fichier: `PMOMusic/src/main.rs`
|
||
|
|
|
||
|
|
```rust
|
||
|
|
use pmoupnp::{mediarenderer::avtransport::actions::{SETAVTRANSPORTURI}, server::{
|
||
|
|
logs::{log_dump, log_sse, LogState, SseLayer}, ServerBuilder, Webapp
|
||
|
|
}, UpnpObject}; // ton module pmoupnp::server
|
||
|
|
use tracing_subscriber::Registry;
|
||
|
|
use tracing_subscriber::prelude::*;
|
||
|
|
use tracing::info;
|
||
|
|
|
||
|
|
#[tokio::main]
|
||
|
|
async fn main() {
|
||
|
|
// Charger la config
|
||
|
|
|
||
|
|
let mut server = ServerBuilder::new_configured().build();
|
||
|
|
|
||
|
|
// Ajouter des routes
|
||
|
|
server
|
||
|
|
.add_route("/hello", || async {
|
||
|
|
serde_json::json!({"message": "Hello World"})
|
||
|
|
})
|
||
|
|
.await;
|
||
|
|
|
||
|
|
server
|
||
|
|
.add_route("/info", || async {
|
||
|
|
serde_json::json!({"version": "1.0.0"})
|
||
|
|
})
|
||
|
|
.await;
|
||
|
|
|
||
|
|
server.add_spa::<Webapp>("/app").await;
|
||
|
|
|
||
|
|
// Gère la sortie des logs et sur le serveur SSE pour l'interface web et sur la console
|
||
|
|
let log_state = LogState::new(1000);
|
||
|
|
let subscriber = Registry::default()
|
||
|
|
.with(
|
||
|
|
tracing_subscriber::fmt::layer()
|
||
|
|
.with_target(true)
|
||
|
|
.with_level(true)
|
||
|
|
.with_ansi(true), // Couleurs dans le terminal
|
||
|
|
)
|
||
|
|
.with(SseLayer::new(log_state.clone()));
|
||
|
|
tracing::subscriber::set_global_default(subscriber).unwrap();
|
||
|
|
|
||
|
|
server
|
||
|
|
.add_handler_with_state("/log-sse", log_sse, log_state.clone())
|
||
|
|
.await;
|
||
|
|
server
|
||
|
|
.add_handler_with_state("/log-dump", log_dump, log_state.clone())
|
||
|
|
.await;
|
||
|
|
|
||
|
|
server.add_redirect("/", "/app").await;
|
||
|
|
|
||
|
|
info!("{}",SETAVTRANSPORTURI.to_markdown());
|
||
|
|
|
||
|
|
server.start().await;
|
||
|
|
server.wait().await;
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
## fichier: `pmoupnp/Cargo.toml`
|
||
|
|
|
||
|
|
```toml
|
||
|
|
[package]
|
||
|
|
name = "pmoupnp"
|
||
|
|
version = "0.1.0"
|
||
|
|
edition = "2024"
|
||
|
|
|
||
|
|
[dependencies]
|
||
|
|
pmoconfig = { path = "../pmoconfig" }
|
||
|
|
pmodidl = { path = "../pmodidl"}
|
||
|
|
|
||
|
|
url = "2.5.7"
|
||
|
|
uuid = "1.18.1"
|
||
|
|
hex = "0.4.3"
|
||
|
|
base64 = "0.22.1"
|
||
|
|
thiserror = "2.0.16"
|
||
|
|
xmltree = "0.11.0"
|
||
|
|
get_if_addrs = "0.5.3"
|
||
|
|
axum = "0.8.4"
|
||
|
|
tokio = { version = "1.35", features = ["rt-multi-thread", "macros", "sync", "time"] }
|
||
|
|
tokio-stream = "0.1"
|
||
|
|
futures-util = "0.3"
|
||
|
|
serde = { version = "1.0", features = ["derive"] }
|
||
|
|
serde_json = "1.0"
|
||
|
|
chrono = { version = "0.4.42", features = ["serde"] }
|
||
|
|
log = "0.4.28"
|
||
|
|
once_cell = "1.20"
|
||
|
|
parking_lot = "0.12"
|
||
|
|
tracing = "0.1"
|
||
|
|
tracing-subscriber = { version = "0.3", features = ["fmt", "env-filter"] }
|
||
|
|
futures = "0.3"
|
||
|
|
async-stream = "0.3.6"
|
||
|
|
axum-server = "0.7.2"
|
||
|
|
axum-embed = "0.1.0"
|
||
|
|
rust-embed = "8.7.2"
|
||
|
|
anyhow = "1.0"
|
||
|
|
utoipa = { version = "5.4.0", features = ["axum_extras"] }
|
||
|
|
utoipa-swagger-ui = { version = "9.0.2", features = ["axum"] }
|
||
|
|
validator = { version = "0.20.0", features = ["derive"] }
|
||
|
|
bevy_reflect = "0.17.1"
|
||
|
|
bevy_reflect_derive = "0.17.1"
|
||
|
|
reqwest = "0.12.23"
|
||
|
|
```
|
||
|
|
|
||
|
|
## fichier: `pmoupnp/webapp/src/App.vue`
|
||
|
|
|
||
|
|
```vue
|
||
|
|
<template>
|
||
|
|
<div>
|
||
|
|
<nav>
|
||
|
|
<router-link to="/">Accueil</router-link> |
|
||
|
|
<router-link to="/logs">Logs</router-link>
|
||
|
|
</nav>
|
||
|
|
<router-view />
|
||
|
|
</div>
|
||
|
|
</template>
|
||
|
|
|
||
|
|
<script setup lang="ts">
|
||
|
|
// rien à importer
|
||
|
|
</script>
|
||
|
|
|
||
|
|
<style scoped>
|
||
|
|
nav {
|
||
|
|
background: #333;
|
||
|
|
width: 100vw;
|
||
|
|
padding: 0.5rem;
|
||
|
|
}
|
||
|
|
a {
|
||
|
|
color: #eee;
|
||
|
|
margin: 0 0.5rem;
|
||
|
|
}
|
||
|
|
a.router-link-active {
|
||
|
|
font-weight: bold;
|
||
|
|
text-decoration: underline;
|
||
|
|
}
|
||
|
|
</style>
|
||
|
|
```
|
||
|
|
|
||
|
|
## fichier: `pmoupnp/webapp/src/main.ts`
|
||
|
|
|
||
|
|
```typescript
|
||
|
|
import { createApp } from "vue";
|
||
|
|
import App from "./App.vue";
|
||
|
|
import router from "./router";
|
||
|
|
|
||
|
|
import "./style.css";
|
||
|
|
|
||
|
|
createApp(App).use(router).mount("#app");
|
||
|
|
```
|
||
|
|
|
||
|
|
## fichier: `pmoupnp/webapp/src/components/LogView.vue`
|
||
|
|
|
||
|
|
```vue
|
||
|
|
<template>
|
||
|
|
<div class="log-viewer">
|
||
|
|
<div class="header">
|
||
|
|
<h2>📋 System Logs</h2>
|
||
|
|
<div class="controls">
|
||
|
|
<button @click="toggleAutoScroll" :class="{ active: autoScroll }">
|
||
|
|
{{ autoScroll ? '📌 Auto-scroll ON' : '📌 Auto-scroll OFF' }}
|
||
|
|
</button>
|
||
|
|
<button @click="clearLogs">🗑️ Clear</button>
|
||
|
|
<select v-model="levelFilter" class="filter">
|
||
|
|
<option value="ALL">All Levels</option>
|
||
|
|
<option value="TRACE">TRACE</option>
|
||
|
|
<option value="DEBUG">DEBUG</option>
|
||
|
|
<option value="INFO">INFO</option>
|
||
|
|
<option value="WARN">WARN</option>
|
||
|
|
<option value="ERROR">ERROR</option>
|
||
|
|
</select>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<div class="log-container" ref="logContainer">
|
||
|
|
<div
|
||
|
|
v-for="(log, index) in filteredLogs"
|
||
|
|
:key="index"
|
||
|
|
:class="['log-entry', `level-${log.level.toLowerCase()}`, { 'is-history': log.isHistory }]"
|
||
|
|
>
|
||
|
|
<span class="timestamp">{{ formatTimestamp(log.timestamp) }}</span>
|
||
|
|
<span class="level">{{ log.level }}</span>
|
||
|
|
<span class="target">{{ log.target }}</span>
|
||
|
|
<span class="message markdown-content" v-html="renderMarkdown(log.message)"></span>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<div v-if="isLoadingHistory" class="loading-state">
|
||
|
|
⏳ Loading history...
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<div v-else-if="filteredLogs.length === 0" class="empty-state">
|
||
|
|
{{ isConnected ? 'Waiting for logs...' : 'Connecting to log stream...' }}
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<div class="footer">
|
||
|
|
<span :class="['status', { connected: isConnected }]">
|
||
|
|
{{ isConnected ? '🟢 Connected' : '🔴 Disconnected' }}
|
||
|
|
</span>
|
||
|
|
<span class="count">{{ filteredLogs.length }} logs</span>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</template>
|
||
|
|
|
||
|
|
<script setup>
|
||
|
|
import { ref, computed, onMounted, onUnmounted, watch, nextTick } from 'vue'
|
||
|
|
import { marked } from 'marked'
|
||
|
|
import DOMPurify from 'dompurify'
|
||
|
|
|
||
|
|
// Configurer marked pour un rendu inline simple
|
||
|
|
marked.setOptions({
|
||
|
|
breaks: true,
|
||
|
|
gfm: true,
|
||
|
|
})
|
||
|
|
|
||
|
|
const logs = ref([])
|
||
|
|
const autoScroll = ref(true)
|
||
|
|
const isConnected = ref(false)
|
||
|
|
const isLoadingHistory = ref(true)
|
||
|
|
const levelFilter = ref('ALL')
|
||
|
|
const logContainer = ref(null)
|
||
|
|
let eventSource = null
|
||
|
|
let historyLoaded = false
|
||
|
|
const seenLogIds = new Set() // Pour détecter les duplicatas
|
||
|
|
|
||
|
|
const filteredLogs = computed(() => {
|
||
|
|
if (levelFilter.value === 'ALL') {
|
||
|
|
return logs.value
|
||
|
|
}
|
||
|
|
return logs.value.filter(log => log.level === levelFilter.value)
|
||
|
|
})
|
||
|
|
|
||
|
|
function formatTimestamp(timestamp) {
|
||
|
|
const date = new Date(timestamp.secs_since_epoch * 1000)
|
||
|
|
return date.toLocaleTimeString('fr-FR', {
|
||
|
|
hour: '2-digit',
|
||
|
|
minute: '2-digit',
|
||
|
|
second: '2-digit',
|
||
|
|
fractionalSecondDigits: 3
|
||
|
|
})
|
||
|
|
}
|
||
|
|
|
||
|
|
function renderMarkdown(text) {
|
||
|
|
// Convertir markdown en HTML et nettoyer pour la sécurité
|
||
|
|
const rawHtml = marked.parse(text, { async: false })
|
||
|
|
return DOMPurify.sanitize(rawHtml, {
|
||
|
|
ALLOWED_TAGS: ['strong', 'em', 'code', 'pre', 'a', 'ul', 'ol', 'li', 'p', 'br'],
|
||
|
|
ALLOWED_ATTR: ['href', 'target']
|
||
|
|
})
|
||
|
|
}
|
||
|
|
|
||
|
|
function toggleAutoScroll() {
|
||
|
|
autoScroll.value = !autoScroll.value
|
||
|
|
if (autoScroll.value) {
|
||
|
|
scrollToBottom()
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
function clearLogs() {
|
||
|
|
logs.value = []
|
||
|
|
seenLogIds.clear()
|
||
|
|
}
|
||
|
|
|
||
|
|
function scrollToBottom() {
|
||
|
|
if (!logContainer.value || !autoScroll.value) return
|
||
|
|
nextTick(() => {
|
||
|
|
logContainer.value.scrollTop = logContainer.value.scrollHeight
|
||
|
|
})
|
||
|
|
}
|
||
|
|
|
||
|
|
function connectSSE() {
|
||
|
|
// Ajuste l'URL selon ton setup
|
||
|
|
const baseUrl = window.location.origin
|
||
|
|
eventSource = new EventSource(`${baseUrl}/log-sse`)
|
||
|
|
|
||
|
|
eventSource.onopen = () => {
|
||
|
|
isConnected.value = true
|
||
|
|
console.log('SSE connection opened')
|
||
|
|
}
|
||
|
|
|
||
|
|
eventSource.onmessage = (event) => {
|
||
|
|
try {
|
||
|
|
const logEntry = JSON.parse(event.data)
|
||
|
|
|
||
|
|
// Créer un ID unique basé sur timestamp + message + target
|
||
|
|
const logId = `${logEntry.timestamp.secs_since_epoch}-${logEntry.timestamp.nanos_since_epoch}-${logEntry.message}-${logEntry.target}`
|
||
|
|
|
||
|
|
// Ignorer les duplicatas
|
||
|
|
if (seenLogIds.has(logId)) {
|
||
|
|
return
|
||
|
|
}
|
||
|
|
seenLogIds.add(logId)
|
||
|
|
|
||
|
|
// Marquer les logs historiques
|
||
|
|
if (!historyLoaded) {
|
||
|
|
logEntry.isHistory = true
|
||
|
|
}
|
||
|
|
|
||
|
|
logs.value.push(logEntry)
|
||
|
|
|
||
|
|
// Limiter à 1000 logs en mémoire
|
||
|
|
if (logs.value.length > 1000) {
|
||
|
|
const removed = logs.value.shift()
|
||
|
|
// Nettoyer aussi le Set pour éviter qu'il grandisse indéfiniment
|
||
|
|
const removedId = `${removed.timestamp.secs_since_epoch}-${removed.timestamp.nanos_since_epoch}-${removed.message}-${removed.target}`
|
||
|
|
seenLogIds.delete(removedId)
|
||
|
|
}
|
||
|
|
|
||
|
|
scrollToBottom()
|
||
|
|
} catch (error) {
|
||
|
|
console.error('Failed to parse log entry:', error)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
eventSource.onerror = () => {
|
||
|
|
isConnected.value = false
|
||
|
|
isLoadingHistory.value = false
|
||
|
|
console.error('SSE connection error')
|
||
|
|
|
||
|
|
// Reconnexion automatique après 3 secondes
|
||
|
|
setTimeout(() => {
|
||
|
|
if (eventSource.readyState === EventSource.CLOSED) {
|
||
|
|
historyLoaded = false
|
||
|
|
connectSSE()
|
||
|
|
}
|
||
|
|
}, 3000)
|
||
|
|
}
|
||
|
|
|
||
|
|
// Détecter la fin du chargement de l'historique
|
||
|
|
// (on considère qu'après 500ms sans log, l'historique est chargé)
|
||
|
|
let historyTimeout
|
||
|
|
const originalOnMessage = eventSource.onmessage
|
||
|
|
eventSource.onmessage = (event) => {
|
||
|
|
clearTimeout(historyTimeout)
|
||
|
|
originalOnMessage(event)
|
||
|
|
|
||
|
|
if (!historyLoaded) {
|
||
|
|
historyTimeout = setTimeout(() => {
|
||
|
|
historyLoaded = true
|
||
|
|
isLoadingHistory.value = false
|
||
|
|
console.log('History loaded, now streaming live logs')
|
||
|
|
}, 500)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
onMounted(() => {
|
||
|
|
connectSSE()
|
||
|
|
})
|
||
|
|
|
||
|
|
onUnmounted(() => {
|
||
|
|
if (eventSource) {
|
||
|
|
eventSource.close()
|
||
|
|
}
|
||
|
|
})
|
||
|
|
|
||
|
|
// Désactiver auto-scroll si l'utilisateur scroll manuellement
|
||
|
|
watch(logContainer, (container) => {
|
||
|
|
if (!container) return
|
||
|
|
|
||
|
|
container.addEventListener('scroll', () => {
|
||
|
|
const isAtBottom =
|
||
|
|
container.scrollHeight - container.scrollTop <= container.clientHeight + 50
|
||
|
|
|
||
|
|
if (!isAtBottom && autoScroll.value) {
|
||
|
|
autoScroll.value = false
|
||
|
|
}
|
||
|
|
})
|
||
|
|
})
|
||
|
|
</script>
|
||
|
|
|
||
|
|
<style scoped>
|
||
|
|
.log-viewer {
|
||
|
|
display: flex;
|
||
|
|
flex-direction: column;
|
||
|
|
height: 80vh;
|
||
|
|
width: 100vw;
|
||
|
|
margin: 0;
|
||
|
|
padding: 0;
|
||
|
|
background: #1e1e1e;
|
||
|
|
color: #d4d4d4;
|
||
|
|
font-family: 'Consolas', 'Monaco', monospace;
|
||
|
|
box-sizing: border-box;
|
||
|
|
}
|
||
|
|
|
||
|
|
.header {
|
||
|
|
display: flex;
|
||
|
|
justify-content: space-between;
|
||
|
|
align-items: center;
|
||
|
|
padding: 1rem 1.5rem;
|
||
|
|
background: #252526;
|
||
|
|
border-bottom: 1px solid #3e3e42;
|
||
|
|
flex-wrap: wrap;
|
||
|
|
gap: 0.5rem;
|
||
|
|
}
|
||
|
|
|
||
|
|
.header h2 {
|
||
|
|
margin: 0;
|
||
|
|
color: #ffffff;
|
||
|
|
font-size: 1.2rem;
|
||
|
|
flex-shrink: 0;
|
||
|
|
}
|
||
|
|
|
||
|
|
@media (max-width: 768px) {
|
||
|
|
.header {
|
||
|
|
padding: 0.75rem 1rem;
|
||
|
|
}
|
||
|
|
|
||
|
|
.header h2 {
|
||
|
|
font-size: 1rem;
|
||
|
|
width: 100%;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
.controls {
|
||
|
|
display: flex;
|
||
|
|
gap: 0.5rem;
|
||
|
|
flex-wrap: wrap;
|
||
|
|
}
|
||
|
|
|
||
|
|
@media (max-width: 768px) {
|
||
|
|
.controls {
|
||
|
|
width: 100%;
|
||
|
|
justify-content: space-between;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
button {
|
||
|
|
padding: 0.5rem 1rem;
|
||
|
|
background: #3c3c3c;
|
||
|
|
color: #d4d4d4;
|
||
|
|
border: 1px solid #555;
|
||
|
|
border-radius: 4px;
|
||
|
|
cursor: pointer;
|
||
|
|
font-size: 0.9rem;
|
||
|
|
transition: all 0.2s;
|
||
|
|
white-space: nowrap;
|
||
|
|
}
|
||
|
|
|
||
|
|
@media (max-width: 768px) {
|
||
|
|
button {
|
||
|
|
padding: 0.4rem 0.7rem;
|
||
|
|
font-size: 0.8rem;
|
||
|
|
flex: 1;
|
||
|
|
min-width: 0;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
button:hover {
|
||
|
|
background: #505050;
|
||
|
|
}
|
||
|
|
|
||
|
|
button.active {
|
||
|
|
background: #0e639c;
|
||
|
|
border-color: #1177bb;
|
||
|
|
}
|
||
|
|
|
||
|
|
.filter {
|
||
|
|
padding: 0.5rem;
|
||
|
|
background: #3c3c3c;
|
||
|
|
color: #d4d4d4;
|
||
|
|
border: 1px solid #555;
|
||
|
|
border-radius: 4px;
|
||
|
|
cursor: pointer;
|
||
|
|
}
|
||
|
|
|
||
|
|
@media (max-width: 768px) {
|
||
|
|
.filter {
|
||
|
|
padding: 0.4rem;
|
||
|
|
font-size: 0.8rem;
|
||
|
|
flex: 1;
|
||
|
|
min-width: 0;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
.log-container {
|
||
|
|
flex: 1;
|
||
|
|
overflow-y: auto;
|
||
|
|
padding: 1rem;
|
||
|
|
background: #1e1e1e;
|
||
|
|
}
|
||
|
|
|
||
|
|
.log-entry {
|
||
|
|
display: grid;
|
||
|
|
grid-template-columns: 130px 80px 200px 1fr;
|
||
|
|
gap: 1rem;
|
||
|
|
padding: 0.5rem;
|
||
|
|
margin-bottom: 0.25rem;
|
||
|
|
border-left: 3px solid transparent;
|
||
|
|
font-size: 0.9rem;
|
||
|
|
line-height: 1.4;
|
||
|
|
}
|
||
|
|
|
||
|
|
@media (max-width: 768px) {
|
||
|
|
.log-entry {
|
||
|
|
grid-template-columns: 1fr;
|
||
|
|
gap: 0.3rem;
|
||
|
|
padding: 0.75rem 0.5rem;
|
||
|
|
font-size: 0.85rem;
|
||
|
|
border-left-width: 4px;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
.log-entry:hover {
|
||
|
|
background: #2d2d30;
|
||
|
|
}
|
||
|
|
|
||
|
|
.log-entry.is-history {
|
||
|
|
opacity: 0.7;
|
||
|
|
}
|
||
|
|
|
||
|
|
.timestamp {
|
||
|
|
color: #858585;
|
||
|
|
font-weight: 500;
|
||
|
|
}
|
||
|
|
|
||
|
|
@media (max-width: 768px) {
|
||
|
|
.timestamp {
|
||
|
|
font-size: 0.75rem;
|
||
|
|
order: 1;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
.level {
|
||
|
|
font-weight: bold;
|
||
|
|
text-transform: uppercase;
|
||
|
|
padding: 0.1rem 0.5rem;
|
||
|
|
border-radius: 3px;
|
||
|
|
text-align: center;
|
||
|
|
}
|
||
|
|
|
||
|
|
@media (max-width: 768px) {
|
||
|
|
.level {
|
||
|
|
order: 2;
|
||
|
|
width: fit-content;
|
||
|
|
font-size: 0.75rem;
|
||
|
|
padding: 0.2rem 0.6rem;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
.target {
|
||
|
|
color: #4ec9b0;
|
||
|
|
font-style: italic;
|
||
|
|
}
|
||
|
|
|
||
|
|
@media (max-width: 768px) {
|
||
|
|
.target {
|
||
|
|
order: 3;
|
||
|
|
font-size: 0.8rem;
|
||
|
|
color: #6eb8a5;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
.message {
|
||
|
|
color: #d4d4d4;
|
||
|
|
word-break: break-word;
|
||
|
|
text-align: left;
|
||
|
|
}
|
||
|
|
|
||
|
|
@media (max-width: 768px) {
|
||
|
|
.message {
|
||
|
|
order: 4;
|
||
|
|
margin-top: 0.25rem;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
.markdown-content {
|
||
|
|
line-height: 1.5;
|
||
|
|
}
|
||
|
|
|
||
|
|
.markdown-content :deep(code) {
|
||
|
|
background: #3c3c3c;
|
||
|
|
padding: 0.1rem 0.3rem;
|
||
|
|
border-radius: 3px;
|
||
|
|
font-family: 'Consolas', 'Monaco', monospace;
|
||
|
|
font-size: 0.85em;
|
||
|
|
color: #ce9178;
|
||
|
|
}
|
||
|
|
|
||
|
|
.markdown-content :deep(pre) {
|
||
|
|
background: #2d2d30;
|
||
|
|
padding: 0.5rem;
|
||
|
|
border-radius: 4px;
|
||
|
|
overflow-x: auto;
|
||
|
|
margin: 0.25rem 0;
|
||
|
|
}
|
||
|
|
|
||
|
|
.markdown-content :deep(pre code) {
|
||
|
|
background: transparent;
|
||
|
|
padding: 0;
|
||
|
|
color: #d4d4d4;
|
||
|
|
}
|
||
|
|
|
||
|
|
.markdown-content :deep(strong) {
|
||
|
|
color: #ffffff;
|
||
|
|
font-weight: bold;
|
||
|
|
}
|
||
|
|
|
||
|
|
.markdown-content :deep(em) {
|
||
|
|
color: #dcdcaa;
|
||
|
|
font-style: italic;
|
||
|
|
}
|
||
|
|
|
||
|
|
.markdown-content :deep(a) {
|
||
|
|
color: #569cd6;
|
||
|
|
text-decoration: none;
|
||
|
|
}
|
||
|
|
|
||
|
|
.markdown-content :deep(a:hover) {
|
||
|
|
text-decoration: underline;
|
||
|
|
}
|
||
|
|
|
||
|
|
.markdown-content :deep(p) {
|
||
|
|
margin: 0;
|
||
|
|
display: inline;
|
||
|
|
}
|
||
|
|
|
||
|
|
.markdown-content :deep(ul),
|
||
|
|
.markdown-content :deep(ol) {
|
||
|
|
margin: 0.25rem 0;
|
||
|
|
padding-left: 1.5rem;
|
||
|
|
}
|
||
|
|
|
||
|
|
/* Level colors */
|
||
|
|
.level-trace {
|
||
|
|
border-left-color: #808080;
|
||
|
|
}
|
||
|
|
|
||
|
|
.level-trace .level {
|
||
|
|
background: #3a3a3a;
|
||
|
|
color: #a0a0a0;
|
||
|
|
}
|
||
|
|
|
||
|
|
.level-debug {
|
||
|
|
border-left-color: #569cd6;
|
||
|
|
}
|
||
|
|
|
||
|
|
.level-debug .level {
|
||
|
|
background: #1e3a5f;
|
||
|
|
color: #569cd6;
|
||
|
|
}
|
||
|
|
|
||
|
|
.level-info {
|
||
|
|
border-left-color: #4ec9b0;
|
||
|
|
}
|
||
|
|
|
||
|
|
.level-info .level {
|
||
|
|
background: #1e4d42;
|
||
|
|
color: #4ec9b0;
|
||
|
|
}
|
||
|
|
|
||
|
|
.level-warn {
|
||
|
|
border-left-color: #dcdcaa;
|
||
|
|
}
|
||
|
|
|
||
|
|
.level-warn .level {
|
||
|
|
background: #4d4d2a;
|
||
|
|
color: #dcdcaa;
|
||
|
|
}
|
||
|
|
|
||
|
|
.level-error {
|
||
|
|
border-left-color: #f48771;
|
||
|
|
}
|
||
|
|
|
||
|
|
.level-error .level {
|
||
|
|
background: #5a1e1e;
|
||
|
|
color: #f48771;
|
||
|
|
}
|
||
|
|
|
||
|
|
.empty-state {
|
||
|
|
text-align: center;
|
||
|
|
padding: 3rem;
|
||
|
|
color: #858585;
|
||
|
|
font-size: 1.1rem;
|
||
|
|
}
|
||
|
|
|
||
|
|
.loading-state {
|
||
|
|
text-align: center;
|
||
|
|
padding: 3rem;
|
||
|
|
color: #569cd6;
|
||
|
|
font-size: 1.1rem;
|
||
|
|
animation: pulse 1.5s ease-in-out infinite;
|
||
|
|
}
|
||
|
|
|
||
|
|
@keyframes pulse {
|
||
|
|
0%, 100% { opacity: 1; }
|
||
|
|
50% { opacity: 0.5; }
|
||
|
|
}
|
||
|
|
|
||
|
|
.footer {
|
||
|
|
display: flex;
|
||
|
|
justify-content: space-between;
|
||
|
|
padding: 0.75rem 1.5rem;
|
||
|
|
background: #252526;
|
||
|
|
border-top: 1px solid #3e3e42;
|
||
|
|
font-size: 0.9rem;
|
||
|
|
}
|
||
|
|
|
||
|
|
@media (max-width: 768px) {
|
||
|
|
.footer {
|
||
|
|
padding: 0.6rem 1rem;
|
||
|
|
font-size: 0.8rem;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
.status {
|
||
|
|
color: #f48771;
|
||
|
|
}
|
||
|
|
|
||
|
|
.status.connected {
|
||
|
|
color: #4ec9b0;
|
||
|
|
}
|
||
|
|
|
||
|
|
.count {
|
||
|
|
color: #858585;
|
||
|
|
}
|
||
|
|
|
||
|
|
/* Scrollbar styling */
|
||
|
|
.log-container::-webkit-scrollbar {
|
||
|
|
width: 12px;
|
||
|
|
}
|
||
|
|
|
||
|
|
.log-container::-webkit-scrollbar-track {
|
||
|
|
background: #1e1e1e;
|
||
|
|
}
|
||
|
|
|
||
|
|
.log-container::-webkit-scrollbar-thumb {
|
||
|
|
background: #424242;
|
||
|
|
border-radius: 6px;
|
||
|
|
}
|
||
|
|
|
||
|
|
.log-container::-webkit-scrollbar-thumb:hover {
|
||
|
|
background: #4e4e4e;
|
||
|
|
}
|
||
|
|
</style>```
|
||
|
|
|
||
|
|
## fichier: `pmoupnp/webapp/src/components/HelloWorld.vue`
|
||
|
|
|
||
|
|
```vue
|
||
|
|
<script setup lang="ts">
|
||
|
|
import { ref } from 'vue'
|
||
|
|
|
||
|
|
defineProps<{ msg: string }>()
|
||
|
|
|
||
|
|
const count = ref(0)
|
||
|
|
</script>
|
||
|
|
|
||
|
|
<template>
|
||
|
|
<h1>{{ msg }}</h1>
|
||
|
|
|
||
|
|
<div class="card">
|
||
|
|
<button type="button" @click="count++">count is {{ count }}</button>
|
||
|
|
<p>
|
||
|
|
Edit
|
||
|
|
<code>components/HelloWorld.vue</code> to test HMR
|
||
|
|
</p>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<p>
|
||
|
|
Check out
|
||
|
|
<a href="https://vuejs.org/guide/quick-start.html#local" target="_blank"
|
||
|
|
>create-vue</a
|
||
|
|
>, the official Vue + Vite starter
|
||
|
|
</p>
|
||
|
|
<p>
|
||
|
|
Learn more about IDE Support for Vue in the
|
||
|
|
<a
|
||
|
|
href="https://vuejs.org/guide/scaling-up/tooling.html#ide-support"
|
||
|
|
target="_blank"
|
||
|
|
>Vue Docs Scaling up Guide</a
|
||
|
|
>.
|
||
|
|
</p>
|
||
|
|
<p class="read-the-docs">Click on the Vite and Vue logos to learn more</p>
|
||
|
|
</template>
|
||
|
|
|
||
|
|
<style scoped>
|
||
|
|
.read-the-docs {
|
||
|
|
color: #888;
|
||
|
|
}
|
||
|
|
</style>
|
||
|
|
```
|
||
|
|
|
||
|
|
## fichier: `pmoupnp/webapp/src/style.css`
|
||
|
|
|
||
|
|
```css
|
||
|
|
:root {
|
||
|
|
font-family: system-ui, Avenir, Helvetica, Arial, sans-serif;
|
||
|
|
line-height: 1.5;
|
||
|
|
font-weight: 400;
|
||
|
|
|
||
|
|
color-scheme: light dark;
|
||
|
|
color: rgba(255, 255, 255, 0.87);
|
||
|
|
background-color: #242424;
|
||
|
|
|
||
|
|
font-synthesis: none;
|
||
|
|
text-rendering: optimizeLegibility;
|
||
|
|
-webkit-font-smoothing: antialiased;
|
||
|
|
-moz-osx-font-smoothing: grayscale;
|
||
|
|
}
|
||
|
|
|
||
|
|
a {
|
||
|
|
font-weight: 500;
|
||
|
|
color: #646cff;
|
||
|
|
text-decoration: inherit;
|
||
|
|
}
|
||
|
|
a:hover {
|
||
|
|
color: #535bf2;
|
||
|
|
}
|
||
|
|
|
||
|
|
body {
|
||
|
|
margin: 0;
|
||
|
|
display: flex;
|
||
|
|
place-items: center;
|
||
|
|
min-width: 320px;
|
||
|
|
min-height: 100vh;
|
||
|
|
width: 100vw;
|
||
|
|
}
|
||
|
|
|
||
|
|
h1 {
|
||
|
|
font-size: 3.2em;
|
||
|
|
line-height: 1.1;
|
||
|
|
}
|
||
|
|
|
||
|
|
button {
|
||
|
|
border-radius: 8px;
|
||
|
|
border: 1px solid transparent;
|
||
|
|
padding: 0.6em 1.2em;
|
||
|
|
font-size: 1em;
|
||
|
|
font-weight: 500;
|
||
|
|
font-family: inherit;
|
||
|
|
background-color: #1a1a1a;
|
||
|
|
cursor: pointer;
|
||
|
|
transition: border-color 0.25s;
|
||
|
|
}
|
||
|
|
button:hover {
|
||
|
|
border-color: #646cff;
|
||
|
|
}
|
||
|
|
button:focus,
|
||
|
|
button:focus-visible {
|
||
|
|
outline: 4px auto -webkit-focus-ring-color;
|
||
|
|
}
|
||
|
|
|
||
|
|
.card {
|
||
|
|
padding: 2em;
|
||
|
|
}
|
||
|
|
|
||
|
|
#app {
|
||
|
|
max-width: 1280px;
|
||
|
|
margin: 0 auto;
|
||
|
|
padding: 2rem;
|
||
|
|
text-align: center;
|
||
|
|
}
|
||
|
|
|
||
|
|
@media (prefers-color-scheme: light) {
|
||
|
|
:root {
|
||
|
|
color: #213547;
|
||
|
|
background-color: #ffffff;
|
||
|
|
}
|
||
|
|
a:hover {
|
||
|
|
color: #747bff;
|
||
|
|
}
|
||
|
|
button {
|
||
|
|
background-color: #f9f9f9;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
## fichier: `pmoupnp/webapp/src/shims-vue.d.ts`
|
||
|
|
|
||
|
|
```typescript
|
||
|
|
declare module "*.vue" {
|
||
|
|
import { DefineComponent } from "vue";
|
||
|
|
const component: DefineComponent<{}, {}, any>;
|
||
|
|
export default component;
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
## fichier: `pmoupnp/webapp/src/router/index.ts`
|
||
|
|
|
||
|
|
```typescript
|
||
|
|
import { createRouter, createWebHistory } from "vue-router";
|
||
|
|
import HelloWorld from "../components/HelloWorld.vue";
|
||
|
|
import LogView from "../components/LogView.vue";
|
||
|
|
|
||
|
|
const routes = [
|
||
|
|
{ path: "/", name: "home", component: HelloWorld },
|
||
|
|
{ path: "/logs", name: "logs", component: LogView },
|
||
|
|
];
|
||
|
|
|
||
|
|
const router = createRouter({
|
||
|
|
// history avec base /app
|
||
|
|
history: createWebHistory("/app"),
|
||
|
|
routes,
|
||
|
|
});
|
||
|
|
|
||
|
|
export default router;
|
||
|
|
```
|
||
|
|
|
||
|
|
## fichier: `pmoupnp/errors.rs`
|
||
|
|
|
||
|
|
```rust
|
||
|
|
use thiserror::Error;
|
||
|
|
|
||
|
|
|
||
|
|
|
||
|
|
#[derive(Error, Debug)]
|
||
|
|
pub enum StateVariableError {
|
||
|
|
#[error("Conversion error: {0}")]
|
||
|
|
ConversionError(String),
|
||
|
|
|
||
|
|
#[error("Validation error: {0}")]
|
||
|
|
ValidationError(String),
|
||
|
|
|
||
|
|
#[error("Range error: {0}")]
|
||
|
|
RangeError(String),
|
||
|
|
|
||
|
|
#[error("Type error: {0}")]
|
||
|
|
TypeError(String),
|
||
|
|
|
||
|
|
#[error("Parse error: {0}")]
|
||
|
|
ParseError(String),
|
||
|
|
|
||
|
|
#[error("Event condition error: {0}")]
|
||
|
|
EventConditionError(String),
|
||
|
|
|
||
|
|
#[error("Arithmetic error: {0}")]
|
||
|
|
ArithmeticError(String),
|
||
|
|
|
||
|
|
#[error("Unknown error: {0}")]
|
||
|
|
Unknown(String),
|
||
|
|
}
|
||
|
|
|
||
|
|
impl From<std::num::TryFromIntError> for StateVariableError {
|
||
|
|
fn from(err: std::num::TryFromIntError) -> Self {
|
||
|
|
StateVariableError::ConversionError(format!("Integer conversion error: {}", err))
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
impl From<std::str::ParseBoolError> for StateVariableError {
|
||
|
|
fn from(err: std::str::ParseBoolError) -> Self {
|
||
|
|
StateVariableError::ConversionError(format!("Boolean conversion error: {}", err))
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
impl From<uuid::Error> for StateVariableError {
|
||
|
|
fn from(err: uuid::Error) -> Self {
|
||
|
|
StateVariableError::ConversionError(format!("UUID conversion error: {}", err))
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
impl From<chrono::ParseError> for StateVariableError {
|
||
|
|
fn from(err: chrono::ParseError) -> Self {
|
||
|
|
StateVariableError::ConversionError(format!("Time conversion error: {}", err))
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
impl From<url::ParseError> for StateVariableError {
|
||
|
|
fn from(err: url::ParseError) -> Self {
|
||
|
|
StateVariableError::ConversionError(format!("URI conversion error: {}", err))
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
impl From<base64::DecodeError> for StateVariableError {
|
||
|
|
fn from(err: base64::DecodeError) -> Self {
|
||
|
|
StateVariableError::ConversionError(format!("Base64 conversion error: {}", err))
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
impl From<hex::FromHexError> for StateVariableError {
|
||
|
|
fn from(err: hex::FromHexError) -> Self {
|
||
|
|
StateVariableError::ConversionError(format!("Hex conversion error: {}", err))
|
||
|
|
}
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
## fichier: `pmoupnp/src/object_set.rs`
|
||
|
|
|
||
|
|
```rust
|
||
|
|
use std::{collections::HashMap, sync::Arc};
|
||
|
|
|
||
|
|
use std::sync::RwLock;
|
||
|
|
|
||
|
|
use crate::{UpnpDeepClone, UpnpObjectSet, UpnpObjectSetError, UpnpTypedObject};
|
||
|
|
|
||
|
|
/// Implémentation du clonage profond pour `UpnpObjectSet`.
|
||
|
|
///
|
||
|
|
/// Cette implémentation crée une copie complète et indépendante du set,
|
||
|
|
/// en clonant chaque objet `T` et en créant de nouveaux `Arc` autour de ces clones.
|
||
|
|
/// Les modifications sur l'un des sets n'affectent pas l'autre.
|
||
|
|
impl<T: UpnpTypedObject> UpnpDeepClone for UpnpObjectSet<T> {
|
||
|
|
fn deep_clone(&self) -> Self {
|
||
|
|
let guard = self.objects.read().unwrap();
|
||
|
|
|
||
|
|
let cloned_map: HashMap<String, Arc<T>> = guard
|
||
|
|
.iter()
|
||
|
|
.map(|(key, arc)| (key.clone(), Arc::new((**arc).clone())))
|
||
|
|
.collect();
|
||
|
|
|
||
|
|
Self {
|
||
|
|
objects: RwLock::new(cloned_map),
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
/// Implémentation du clonage superficiel pour `UpnpObjectSet`.
|
||
|
|
///
|
||
|
|
/// Cette implémentation crée une copie du set qui **partage** les objets `T`
|
||
|
|
/// via les `Arc`. C'est beaucoup plus rapide et économe en mémoire qu'un clonage
|
||
|
|
/// profond, car seuls les pointeurs `Arc` sont clonés (incrémentation du compteur
|
||
|
|
/// de références).
|
||
|
|
///
|
||
|
|
/// # Note
|
||
|
|
///
|
||
|
|
/// Les deux sets partagent les mêmes instances d'objets `T`. Si `T` contient
|
||
|
|
/// de la mutabilité interne (via `Mutex`, `RwLock`, etc.), les modifications
|
||
|
|
/// seront visibles depuis les deux sets.
|
||
|
|
impl<T: UpnpTypedObject> Clone for UpnpObjectSet<T> {
|
||
|
|
fn clone(&self) -> Self {
|
||
|
|
let guard = self.objects.read().unwrap();
|
||
|
|
|
||
|
|
Self {
|
||
|
|
objects: RwLock::new(guard.clone()),
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
impl<T: UpnpTypedObject> UpnpObjectSet<T> {
|
||
|
|
/// Crée un nouveau `UpnpObjectSet` vide.
|
||
|
|
///
|
||
|
|
/// # Examples
|
||
|
|
///
|
||
|
|
/// ```
|
||
|
|
/// let set: UpnpObjectSet<MyObject> = UpnpObjectSet::new();
|
||
|
|
/// ```
|
||
|
|
pub fn new() -> Self {
|
||
|
|
Self {
|
||
|
|
objects: RwLock::new(HashMap::new()),
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
/// Insère un objet dans le set.
|
||
|
|
///
|
||
|
|
/// # Arguments
|
||
|
|
///
|
||
|
|
/// * `object` - L'objet à insérer, encapsulé dans un `Arc`
|
||
|
|
///
|
||
|
|
/// # Returns
|
||
|
|
///
|
||
|
|
/// * `Ok(())` - Si l'insertion a réussi
|
||
|
|
/// * `Err(UpnpObjectSetError::AlreadyExists)` - Si un objet avec le même nom existe déjà
|
||
|
|
///
|
||
|
|
/// # Examples
|
||
|
|
///
|
||
|
|
/// ```
|
||
|
|
/// let mut set = UpnpObjectSet::new();
|
||
|
|
/// let obj = Arc::new(MyObject::new("test"));
|
||
|
|
/// set.insert(obj)?;
|
||
|
|
/// ```
|
||
|
|
pub fn insert(&mut self, object: Arc<T>) -> Result<(), UpnpObjectSetError> {
|
||
|
|
let mut guard = self.objects.write().unwrap();
|
||
|
|
let key = object.get_name().to_string();
|
||
|
|
|
||
|
|
if guard.contains_key(&key) {
|
||
|
|
return Err(UpnpObjectSetError::AlreadyExists(key));
|
||
|
|
}
|
||
|
|
|
||
|
|
guard.insert(key, object);
|
||
|
|
Ok(())
|
||
|
|
}
|
||
|
|
|
||
|
|
/// Insère un objet dans le set, ou remplace l'objet existant s'il y en a un avec le même nom.
|
||
|
|
///
|
||
|
|
/// Cette méthode ne retourne jamais d'erreur et écrase silencieusement tout objet existant.
|
||
|
|
///
|
||
|
|
/// # Arguments
|
||
|
|
///
|
||
|
|
/// * `object` - L'objet à insérer ou remplacer, encapsulé dans un `Arc`
|
||
|
|
///
|
||
|
|
/// # Examples
|
||
|
|
///
|
||
|
|
/// ```
|
||
|
|
/// let mut set = UpnpObjectSet::new();
|
||
|
|
/// let obj1 = Arc::new(MyObject::new("test"));
|
||
|
|
/// let obj2 = Arc::new(MyObject::new("test")); // Même nom
|
||
|
|
///
|
||
|
|
/// set.insert_or_replace(obj1);
|
||
|
|
/// set.insert_or_replace(obj2); // Remplace obj1
|
||
|
|
/// ```
|
||
|
|
pub fn insert_or_replace(&mut self, object: Arc<T>) {
|
||
|
|
let mut guard = self.objects.write().unwrap();
|
||
|
|
let key: String = object.get_name().to_string();
|
||
|
|
|
||
|
|
guard.insert(key, object);
|
||
|
|
}
|
||
|
|
|
||
|
|
/// Vérifie si le set contient un objet donné.
|
||
|
|
///
|
||
|
|
/// La vérification se base sur le nom de l'objet retourné par `get_name()`.
|
||
|
|
///
|
||
|
|
/// # Arguments
|
||
|
|
///
|
||
|
|
/// * `object` - L'objet à rechercher
|
||
|
|
///
|
||
|
|
/// # Returns
|
||
|
|
///
|
||
|
|
/// `true` si un objet avec le même nom existe dans le set, `false` sinon.
|
||
|
|
///
|
||
|
|
/// # Examples
|
||
|
|
///
|
||
|
|
/// ```
|
||
|
|
/// let set = UpnpObjectSet::new();
|
||
|
|
/// let obj = Arc::new(MyObject::new("test"));
|
||
|
|
///
|
||
|
|
/// if set.contains(obj.clone()) {
|
||
|
|
/// println!("L'objet existe déjà");
|
||
|
|
/// }
|
||
|
|
/// ```
|
||
|
|
pub fn contains(&self, object: Arc<T>) -> bool {
|
||
|
|
let guard = self.objects.read().unwrap();
|
||
|
|
let key: String = object.get_name().to_string();
|
||
|
|
|
||
|
|
guard.contains_key(&key)
|
||
|
|
}
|
||
|
|
|
||
|
|
/// Récupère un objet par son nom.
|
||
|
|
///
|
||
|
|
/// # Arguments
|
||
|
|
///
|
||
|
|
/// * `name` - Le nom de l'objet à rechercher
|
||
|
|
///
|
||
|
|
/// # Returns
|
||
|
|
///
|
||
|
|
/// * `Some(Arc<T>)` - Si un objet avec ce nom existe
|
||
|
|
/// * `None` - Si aucun objet n'est trouvé
|
||
|
|
///
|
||
|
|
/// # Examples
|
||
|
|
///
|
||
|
|
/// ```
|
||
|
|
/// let set = UpnpObjectSet::new();
|
||
|
|
///
|
||
|
|
/// if let Some(obj) = set.get_by_name("test") {
|
||
|
|
/// println!("Objet trouvé: {}", obj.get_name());
|
||
|
|
/// }
|
||
|
|
/// ```
|
||
|
|
pub fn get_by_name(&self, name: &str) -> Option<Arc<T>> {
|
||
|
|
let guard = self.objects.read().unwrap();
|
||
|
|
guard.get(name).cloned()
|
||
|
|
}
|
||
|
|
|
||
|
|
/// Retourne tous les objets du set.
|
||
|
|
///
|
||
|
|
/// # Returns
|
||
|
|
///
|
||
|
|
/// Un vecteur contenant des clones des `Arc` pointant vers tous les objets du set.
|
||
|
|
/// L'ordre des éléments n'est pas garanti.
|
||
|
|
///
|
||
|
|
/// # Examples
|
||
|
|
///
|
||
|
|
/// ```
|
||
|
|
/// let set = UpnpObjectSet::new();
|
||
|
|
///
|
||
|
|
/// for obj in set.all() {
|
||
|
|
/// println!("Objet: {}", obj.get_name());
|
||
|
|
/// }
|
||
|
|
/// ```
|
||
|
|
///
|
||
|
|
/// # Thread-safety
|
||
|
|
///
|
||
|
|
/// Cette méthode acquiert un verrou de lecture. Plusieurs threads peuvent
|
||
|
|
/// appeler cette méthode simultanément sans blocage.
|
||
|
|
pub fn all(&self) -> Vec<Arc<T>> {
|
||
|
|
let guard = self.objects.read().unwrap();
|
||
|
|
guard.values().cloned().collect()
|
||
|
|
}
|
||
|
|
}```
|
||
|
|
|
||
|
|
## fichier: `pmoupnp/src/state_variables/instance_methods.rs`
|
||
|
|
|
||
|
|
```rust
|
||
|
|
use std::fmt;
|
||
|
|
|
||
|
|
use chrono::{DateTime, Utc};
|
||
|
|
use std::sync::RwLock;
|
||
|
|
use xmltree::Element;
|
||
|
|
|
||
|
|
use crate::{
|
||
|
|
object_trait::{UpnpInstance, UpnpObject},
|
||
|
|
state_variables::{StateVarInstance, StateVariable, UpnpVariable},
|
||
|
|
variable_types::{StateValue, StateValueError, UpnpVarType},
|
||
|
|
UpnpObjectType, UpnpTyped, UpnpTypedInstance
|
||
|
|
};
|
||
|
|
|
||
|
|
impl UpnpVariable for StateVarInstance {
|
||
|
|
fn get_definition(&self) -> &StateVariable {
|
||
|
|
return &self.model;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
impl UpnpObject for StateVarInstance {
|
||
|
|
fn to_xml_element(&self) -> Element {
|
||
|
|
self.get_definition().to_xml_element()
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
impl UpnpVarType for StateVarInstance {
|
||
|
|
fn as_state_var_type(&self) -> crate::variable_types::StateVarType {
|
||
|
|
self.get_definition().as_state_var_type()
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
impl UpnpInstance for StateVarInstance {
|
||
|
|
type Model = StateVariable;
|
||
|
|
|
||
|
|
fn new(from: &StateVariable) -> Self {
|
||
|
|
Self {
|
||
|
|
object: UpnpObjectType {
|
||
|
|
name: from.object.name.clone(),
|
||
|
|
object_type: "StateVarInstance".to_string(),
|
||
|
|
},
|
||
|
|
model: from.clone(),
|
||
|
|
value: RwLock::new(from.get_default()),
|
||
|
|
old_value: RwLock::new(from.get_default()),
|
||
|
|
last_modified: RwLock::new(Utc::now()),
|
||
|
|
last_notification: RwLock::new(Utc::now()),
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
}
|
||
|
|
|
||
|
|
impl UpnpTyped for StateVarInstance {
|
||
|
|
fn as_upnp_object_type(&self) -> &UpnpObjectType {
|
||
|
|
return &self.object;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
impl UpnpTypedInstance for StateVarInstance {
|
||
|
|
|
||
|
|
fn get_model(&self) -> &Self::Model {
|
||
|
|
&self.model
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
impl fmt::Debug for StateVarInstance {
|
||
|
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||
|
|
f.debug_struct("StateVarInstance")
|
||
|
|
.field("object", &self.object)
|
||
|
|
.field("model", &self.model)
|
||
|
|
.field("value", &self.value)
|
||
|
|
.field("old_value", &self.old_value)
|
||
|
|
.field("last_modified", &self.last_modified)
|
||
|
|
.field("last_notification", &self.last_notification)
|
||
|
|
.finish()
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
impl Clone for StateVarInstance {
|
||
|
|
fn clone(&self) -> Self {
|
||
|
|
Self {
|
||
|
|
object: self.object.clone(),
|
||
|
|
model: self.model.clone(),
|
||
|
|
value: RwLock::new(self.value.read().unwrap().clone()),
|
||
|
|
old_value: RwLock::new(self.old_value.read().unwrap().clone()),
|
||
|
|
last_modified: RwLock::new(self.last_modified.read().unwrap().clone()),
|
||
|
|
last_notification: RwLock::new(self.last_notification.read().unwrap().clone()),
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
impl StateVarInstance {
|
||
|
|
pub async fn set_value(&self, new_value: StateValue) -> Result<(), StateValueError> {
|
||
|
|
// Validation du type
|
||
|
|
if self.as_state_var_type() != new_value.as_state_var_type() {
|
||
|
|
return Err(StateValueError::TypeError(
|
||
|
|
"Value type mismatch".to_string()
|
||
|
|
));
|
||
|
|
}
|
||
|
|
|
||
|
|
// Mise à jour avec les locks
|
||
|
|
let mut old_val = self.old_value.write().unwrap();
|
||
|
|
let mut val = self.value.write().unwrap();
|
||
|
|
let mut modified = self.last_modified.write().unwrap();
|
||
|
|
|
||
|
|
*old_val = val.clone();
|
||
|
|
*val = new_value;
|
||
|
|
*modified = Utc::now();
|
||
|
|
|
||
|
|
Ok(())
|
||
|
|
}
|
||
|
|
/// Accès à la valeur
|
||
|
|
pub fn value(&self) -> StateValue {
|
||
|
|
self.value.read().unwrap().clone()
|
||
|
|
}
|
||
|
|
|
||
|
|
/// Accès au timestamp
|
||
|
|
pub fn last_modified(&self) -> DateTime<Utc> {
|
||
|
|
self.last_modified.read().unwrap().clone()
|
||
|
|
}
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
## fichier: `pmoupnp/src/state_variables/var_set_methods.rs`
|
||
|
|
|
||
|
|
```rust
|
||
|
|
use xmltree::{Element, XMLNode};
|
||
|
|
|
||
|
|
use crate::{object_trait::UpnpModel, state_variables::{StateVarInstanceSet, StateVariableSet}, UpnpObject};
|
||
|
|
|
||
|
|
|
||
|
|
impl UpnpObject for StateVariableSet {
|
||
|
|
|
||
|
|
fn to_xml_element(&self) -> Element {
|
||
|
|
let mut elem = Element::new("serviceStateTable");
|
||
|
|
|
||
|
|
for state_var in self.all() {
|
||
|
|
let state_var_elem = state_var.to_xml_element(); // retourne un <stateVariable> complet
|
||
|
|
elem.children.push(XMLNode::Element(state_var_elem));
|
||
|
|
}
|
||
|
|
|
||
|
|
elem
|
||
|
|
}
|
||
|
|
|
||
|
|
}
|
||
|
|
|
||
|
|
impl UpnpModel for StateVariableSet {
|
||
|
|
type Instance = StateVarInstanceSet;
|
||
|
|
}
|
||
|
|
|
||
|
|
|
||
|
|
```
|
||
|
|
|
||
|
|
## fichier: `pmoupnp/src/state_variables/variable_trait.rs`
|
||
|
|
|
||
|
|
```rust
|
||
|
|
use crate::{
|
||
|
|
state_variables::StateVariable,
|
||
|
|
variable_types::{StateValue, UpnpVarType},
|
||
|
|
};
|
||
|
|
|
||
|
|
/// Trait pour accéder aux propriétés et contraintes d'une variable UPnP.
|
||
|
|
///
|
||
|
|
/// Ce trait fournit une interface uniforme pour interroger les métadonnées,
|
||
|
|
/// contraintes et comportements d'une variable UPnP, qu'il s'agisse d'une
|
||
|
|
/// définition ([`StateVariable`]) ou d'une instance ([`StateVarInstance`]).
|
||
|
|
///
|
||
|
|
/// # Architecture
|
||
|
|
///
|
||
|
|
/// Le trait utilise le pattern "trait avec implémentation par défaut" :
|
||
|
|
/// - Une seule méthode requise : [`get_definition`](Self::get_definition)
|
||
|
|
/// - Toutes les autres méthodes sont implémentées par défaut en déléguant à la définition
|
||
|
|
///
|
||
|
|
/// Cela permet une interface cohérente entre modèles et instances sans duplication de code.
|
||
|
|
///
|
||
|
|
/// # Hiérarchie
|
||
|
|
///
|
||
|
|
/// ```text
|
||
|
|
/// UpnpVariable
|
||
|
|
/// ├─> StateVariable (get_definition() retourne self)
|
||
|
|
/// └─> StateVarInstance (get_definition() retourne self.definition)
|
||
|
|
/// ```
|
||
|
|
///
|
||
|
|
/// # Examples
|
||
|
|
///
|
||
|
|
/// ```ignore
|
||
|
|
/// fn display_variable_info<V: UpnpVariable>(var: &V) {
|
||
|
|
/// println!("Variable: {}", var.get_definition().get_name());
|
||
|
|
///
|
||
|
|
/// if var.has_default() {
|
||
|
|
/// println!("Default: {:?}", var.get_default());
|
||
|
|
/// }
|
||
|
|
///
|
||
|
|
/// if var.has_range() {
|
||
|
|
/// println!("Has range constraints");
|
||
|
|
/// }
|
||
|
|
///
|
||
|
|
/// if var.has_allowed_values() {
|
||
|
|
/// println!("Has allowed values list");
|
||
|
|
/// }
|
||
|
|
/// }
|
||
|
|
/// ```
|
||
|
|
pub trait UpnpVariable {
|
||
|
|
/// Retourne une référence vers la définition de la variable.
|
||
|
|
///
|
||
|
|
/// Cette méthode est la base de toutes les autres méthodes du trait.
|
||
|
|
///
|
||
|
|
/// # Implementation
|
||
|
|
///
|
||
|
|
/// - Pour [`StateVariable`] : retourne `self`
|
||
|
|
/// - Pour [`StateVarInstance`] : retourne `self.definition`
|
||
|
|
fn get_definition(&self) -> &StateVariable;
|
||
|
|
|
||
|
|
/// Indique si la variable a un pas (step) défini.
|
||
|
|
///
|
||
|
|
/// Le pas définit l'incrément minimal entre deux valeurs valides pour
|
||
|
|
/// les types numériques.
|
||
|
|
///
|
||
|
|
/// # Returns
|
||
|
|
///
|
||
|
|
/// `true` si un pas est défini, `false` sinon.
|
||
|
|
///
|
||
|
|
/// # Examples
|
||
|
|
///
|
||
|
|
/// ```ignore
|
||
|
|
/// if var.has_step() {
|
||
|
|
/// println!("Step: {:?}", var.get_step());
|
||
|
|
/// }
|
||
|
|
/// ```
|
||
|
|
fn has_step(&self) -> bool {
|
||
|
|
self.get_definition().step.is_some()
|
||
|
|
}
|
||
|
|
|
||
|
|
/// Retourne le pas (step) de la variable s'il est défini.
|
||
|
|
///
|
||
|
|
/// # Returns
|
||
|
|
///
|
||
|
|
/// - `Some(StateValue)` si un pas est défini
|
||
|
|
/// - `None` sinon
|
||
|
|
///
|
||
|
|
/// # See also
|
||
|
|
///
|
||
|
|
/// - [`has_step`](Self::has_step) pour tester l'existence
|
||
|
|
fn get_step(&self) -> Option<StateValue> {
|
||
|
|
self.get_definition().step.clone()
|
||
|
|
}
|
||
|
|
|
||
|
|
/// Indique si la variable a une plage de valeurs (range) définie.
|
||
|
|
///
|
||
|
|
/// La plage définit les valeurs minimale et maximale acceptables.
|
||
|
|
///
|
||
|
|
/// # Returns
|
||
|
|
///
|
||
|
|
/// `true` si une plage est définie, `false` sinon.
|
||
|
|
fn has_range(&self) -> bool {
|
||
|
|
self.get_definition().value_range.is_some()
|
||
|
|
}
|
||
|
|
|
||
|
|
/// Indique si la variable est modifiable.
|
||
|
|
///
|
||
|
|
/// Une variable non modifiable est en lecture seule.
|
||
|
|
///
|
||
|
|
/// # Returns
|
||
|
|
///
|
||
|
|
/// `true` si la variable peut être modifiée, `false` sinon.
|
||
|
|
fn is_modifiable(&self) -> bool {
|
||
|
|
self.get_definition().modifiable
|
||
|
|
}
|
||
|
|
|
||
|
|
/// Indique si la variable a des conditions d'événement définies.
|
||
|
|
///
|
||
|
|
/// Les conditions d'événement déterminent quand des notifications
|
||
|
|
/// doivent être envoyées lors de changements de valeur.
|
||
|
|
///
|
||
|
|
/// # Returns
|
||
|
|
///
|
||
|
|
/// `true` si au moins une condition d'événement existe, `false` sinon.
|
||
|
|
///
|
||
|
|
/// # Note
|
||
|
|
///
|
||
|
|
/// Retourne `false` si le lock est empoisonné (poisoned).
|
||
|
|
fn has_event_conditions(&self) -> bool {
|
||
|
|
let guard = self.get_definition().event_conditions.read().unwrap();
|
||
|
|
!guard.is_empty()
|
||
|
|
}
|
||
|
|
|
||
|
|
/// Vérifie si une condition d'événement spécifique existe.
|
||
|
|
///
|
||
|
|
/// # Arguments
|
||
|
|
///
|
||
|
|
/// * `name` - Le nom de la condition à rechercher
|
||
|
|
///
|
||
|
|
/// # Returns
|
||
|
|
///
|
||
|
|
/// `true` si la condition existe, `false` sinon.
|
||
|
|
///
|
||
|
|
/// # Note
|
||
|
|
///
|
||
|
|
/// Retourne `false` si le lock est empoisonné (poisoned).
|
||
|
|
fn has_event_condition(&self, name: &String) -> bool {
|
||
|
|
let guard = self.get_definition().event_conditions.read().unwrap();
|
||
|
|
guard.contains_key(name)
|
||
|
|
}
|
||
|
|
|
||
|
|
/// Indique si la variable a une description non vide.
|
||
|
|
///
|
||
|
|
/// # Returns
|
||
|
|
///
|
||
|
|
/// `true` si une description existe et n'est pas vide, `false` sinon.
|
||
|
|
fn has_description(&self) -> bool {
|
||
|
|
!self.get_definition().description.is_empty()
|
||
|
|
}
|
||
|
|
|
||
|
|
/// Retourne la description de la variable.
|
||
|
|
///
|
||
|
|
/// # Returns
|
||
|
|
///
|
||
|
|
/// La description sous forme de `String`. Peut être vide.
|
||
|
|
///
|
||
|
|
/// # See also
|
||
|
|
///
|
||
|
|
/// - [`has_description`](Self::has_description) pour tester si non vide
|
||
|
|
fn get_description(&self) -> String {
|
||
|
|
self.get_definition().description.clone()
|
||
|
|
}
|
||
|
|
|
||
|
|
/// Indique si la variable a une valeur par défaut définie explicitement.
|
||
|
|
///
|
||
|
|
/// # Returns
|
||
|
|
///
|
||
|
|
/// `true` si une valeur par défaut est explicitement définie, `false` sinon.
|
||
|
|
///
|
||
|
|
/// # Note
|
||
|
|
///
|
||
|
|
/// Même si cette méthode retourne `false`, [`get_default`](Self::get_default)
|
||
|
|
/// retournera toujours une valeur (la valeur par défaut du type).
|
||
|
|
fn has_default(&self) -> bool {
|
||
|
|
self.get_definition().default_value.is_some()
|
||
|
|
}
|
||
|
|
|
||
|
|
/// Retourne la valeur par défaut de la variable.
|
||
|
|
///
|
||
|
|
/// # Returns
|
||
|
|
///
|
||
|
|
/// La valeur par défaut. Si aucune valeur par défaut n'est explicitement
|
||
|
|
/// définie, retourne la valeur par défaut du type de la variable
|
||
|
|
/// (ex: 0 pour les entiers, chaîne vide pour String, etc.).
|
||
|
|
///
|
||
|
|
/// # Examples
|
||
|
|
///
|
||
|
|
/// ```ignore
|
||
|
|
/// let default = var.get_default();
|
||
|
|
/// println!("Default value: {:?}", default);
|
||
|
|
/// ```
|
||
|
|
fn get_default(&self) -> StateValue {
|
||
|
|
self.get_definition()
|
||
|
|
.default_value
|
||
|
|
.clone()
|
||
|
|
.unwrap_or_else(|| self.get_definition().as_state_var_type().default_value())
|
||
|
|
}
|
||
|
|
|
||
|
|
/// Indique si la variable a une liste de valeurs autorisées.
|
||
|
|
///
|
||
|
|
/// Lorsqu'une liste de valeurs autorisées est définie, seules ces valeurs
|
||
|
|
/// sont acceptables pour la variable.
|
||
|
|
///
|
||
|
|
/// # Returns
|
||
|
|
///
|
||
|
|
/// `true` si une liste non vide de valeurs autorisées existe, `false` sinon.
|
||
|
|
///
|
||
|
|
/// # Note
|
||
|
|
///
|
||
|
|
/// Retourne `false` si le lock est empoisonné (poisoned).
|
||
|
|
fn has_allowed_values(&self) -> bool {
|
||
|
|
let guard = self.get_definition()
|
||
|
|
.allowed_values
|
||
|
|
.read().unwrap();
|
||
|
|
|
||
|
|
!guard.is_empty()
|
||
|
|
}
|
||
|
|
|
||
|
|
/// Vérifie si une valeur fait partie des valeurs autorisées.
|
||
|
|
///
|
||
|
|
/// # Arguments
|
||
|
|
///
|
||
|
|
/// * `value` - La valeur à vérifier
|
||
|
|
///
|
||
|
|
/// # Returns
|
||
|
|
///
|
||
|
|
/// `true` si la valeur est dans la liste des valeurs autorisées, `false` sinon.
|
||
|
|
/// Retourne également `false` si aucune liste de valeurs autorisées n'est définie
|
||
|
|
/// ou si le lock est empoisonné.
|
||
|
|
///
|
||
|
|
/// # Examples
|
||
|
|
///
|
||
|
|
/// ```ignore
|
||
|
|
/// let value = StateValue::String("ON".to_string());
|
||
|
|
/// if var.is_an_allowed_value(&value) {
|
||
|
|
/// println!("Value is allowed");
|
||
|
|
/// }
|
||
|
|
/// ```
|
||
|
|
///
|
||
|
|
/// # Note
|
||
|
|
///
|
||
|
|
/// Si aucune liste de valeurs autorisées n'est définie, cette méthode
|
||
|
|
/// retourne `false`. Utilisez [`has_allowed_values`](Self::has_allowed_values)
|
||
|
|
/// pour distinguer "pas de liste" de "valeur non autorisée".
|
||
|
|
fn is_an_allowed_value(&self, value: &StateValue) -> bool {
|
||
|
|
let guard = self.get_definition()
|
||
|
|
.allowed_values
|
||
|
|
.read().unwrap();
|
||
|
|
|
||
|
|
guard.contains(value)
|
||
|
|
}
|
||
|
|
|
||
|
|
/// Indique si la variable envoie des notifications d'événement.
|
||
|
|
///
|
||
|
|
/// Les notifications d'événement sont envoyées aux abonnés lorsque
|
||
|
|
/// la valeur de la variable change.
|
||
|
|
///
|
||
|
|
/// # Returns
|
||
|
|
///
|
||
|
|
/// `true` si les notifications sont activées, `false` sinon.
|
||
|
|
///
|
||
|
|
/// # See also
|
||
|
|
///
|
||
|
|
/// - [`has_event_conditions`](Self::has_event_conditions) pour vérifier
|
||
|
|
/// les conditions d'envoi d'événements
|
||
|
|
fn is_sending_notification(&self) -> bool {
|
||
|
|
self.get_definition().send_events
|
||
|
|
}
|
||
|
|
|
||
|
|
/// Indique si la variable a un parser de valeur personnalisé.
|
||
|
|
///
|
||
|
|
/// Un parser personnalisé est utilisé pour convertir des chaînes de
|
||
|
|
/// caractères en valeurs typées. Disponible uniquement pour les variables
|
||
|
|
/// de type String.
|
||
|
|
///
|
||
|
|
/// # Returns
|
||
|
|
///
|
||
|
|
/// `true` si un parser est défini, `false` sinon.
|
||
|
|
fn has_value_parser(&self) -> bool {
|
||
|
|
self.get_definition().parse.is_some()
|
||
|
|
}
|
||
|
|
|
||
|
|
/// Indique si la variable a un marshaler de valeur personnalisé.
|
||
|
|
///
|
||
|
|
/// Un marshaler personnalisé est utilisé pour sérialiser des valeurs
|
||
|
|
/// en chaînes de caractères. Disponible uniquement pour les variables
|
||
|
|
/// de type String.
|
||
|
|
///
|
||
|
|
/// # Returns
|
||
|
|
///
|
||
|
|
/// `true` si un marshaler est défini, `false` sinon.
|
||
|
|
fn has_value_marshaler(&self) -> bool {
|
||
|
|
self.get_definition().marshal.is_some()
|
||
|
|
}
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
## fichier: `pmoupnp/src/state_variables/mod.rs`
|
||
|
|
|
||
|
|
```rust
|
||
|
|
mod errors;
|
||
|
|
mod instance_methods;
|
||
|
|
mod variable_methods;
|
||
|
|
mod var_set_methods;
|
||
|
|
mod var_inst_set_methods;
|
||
|
|
mod variable_trait;
|
||
|
|
|
||
|
|
use std::{
|
||
|
|
collections::HashMap,
|
||
|
|
sync::Arc,
|
||
|
|
};
|
||
|
|
|
||
|
|
pub use crate::state_variables::variable_trait::UpnpVariable;
|
||
|
|
use bevy_reflect::Reflect;
|
||
|
|
use chrono::{DateTime, Utc};
|
||
|
|
pub use errors::StateVariableError;
|
||
|
|
use std::sync::RwLock;
|
||
|
|
|
||
|
|
use crate::{
|
||
|
|
value_ranges::ValueRange,
|
||
|
|
variable_types::{StateValue, StateVarType},
|
||
|
|
UpnpObjectSet, UpnpObjectType,
|
||
|
|
};
|
||
|
|
|
||
|
|
/// Type pour les fonctions de condition d'événement
|
||
|
|
pub type StateConditionFunc = Arc<dyn Fn(&StateVarInstance) -> bool + Send + Sync>;
|
||
|
|
|
||
|
|
/// Type pour les fonctions de parsing de valeurs depuis des chaînes
|
||
|
|
pub type StringValueParser =
|
||
|
|
Arc<dyn Fn(&str) -> Result<Box<dyn Reflect>, StateVariableError> + Send + Sync>;
|
||
|
|
|
||
|
|
/// Type pour les fonctions de sérialisation de valeurs vers des chaînes
|
||
|
|
pub type ValueSerializer =
|
||
|
|
Arc<dyn Fn(&StateValue) -> Result<String, StateVariableError> + Send + Sync>;
|
||
|
|
|
||
|
|
pub struct StateVariable {
|
||
|
|
object: UpnpObjectType,
|
||
|
|
value_type: StateVarType,
|
||
|
|
step: Option<StateValue>,
|
||
|
|
modifiable: bool,
|
||
|
|
event_conditions: Arc<RwLock<HashMap<String, StateConditionFunc>>>,
|
||
|
|
description: String,
|
||
|
|
default_value: Option<StateValue>,
|
||
|
|
value_range: Option<ValueRange>,
|
||
|
|
allowed_values: Arc<RwLock<Vec<StateValue>>>,
|
||
|
|
send_events: bool,
|
||
|
|
parse: Option<StringValueParser>,
|
||
|
|
marshal: Option<ValueSerializer>,
|
||
|
|
}
|
||
|
|
|
||
|
|
pub type StateVariableSet = UpnpObjectSet<StateVariable>;
|
||
|
|
|
||
|
|
pub struct StateVarInstance {
|
||
|
|
object: UpnpObjectType,
|
||
|
|
model: StateVariable,
|
||
|
|
value: RwLock<StateValue>,
|
||
|
|
old_value: RwLock<StateValue>,
|
||
|
|
last_modified: RwLock<DateTime<Utc>>,
|
||
|
|
last_notification: RwLock<DateTime<Utc>>,
|
||
|
|
}
|
||
|
|
|
||
|
|
pub type StateVarInstanceSet = UpnpObjectSet<StateVarInstance>;
|
||
|
|
|
||
|
|
```
|
||
|
|
|
||
|
|
## fichier: `pmoupnp/src/state_variables/var_inst_set_methods.rs`
|
||
|
|
|
||
|
|
```rust
|
||
|
|
use std::collections::HashMap;
|
||
|
|
|
||
|
|
use std::sync::RwLock;
|
||
|
|
use xmltree::{Element, XMLNode};
|
||
|
|
|
||
|
|
use crate::{state_variables::{StateVarInstanceSet, StateVariableSet}, UpnpObject};
|
||
|
|
|
||
|
|
use crate::UpnpInstance;
|
||
|
|
|
||
|
|
impl UpnpObject for StateVarInstanceSet {
|
||
|
|
fn to_xml_element(&self) -> Element {
|
||
|
|
let mut elem = Element::new("serviceStateTable");
|
||
|
|
|
||
|
|
for state_var in self.all() {
|
||
|
|
let state_var_elem = state_var.to_xml_element(); // retourne un <stateVariable> complet
|
||
|
|
elem.children.push(XMLNode::Element(state_var_elem));
|
||
|
|
}
|
||
|
|
|
||
|
|
elem
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
impl UpnpInstance for StateVarInstanceSet {
|
||
|
|
type Model = StateVariableSet;
|
||
|
|
|
||
|
|
fn new(_: &StateVariableSet) -> Self {
|
||
|
|
Self { objects: RwLock::new(HashMap::new()) }
|
||
|
|
}
|
||
|
|
|
||
|
|
|
||
|
|
}
|
||
|
|
|
||
|
|
|
||
|
|
```
|
||
|
|
|
||
|
|
## fichier: `pmoupnp/src/state_variables/errors.rs`
|
||
|
|
|
||
|
|
```rust
|
||
|
|
use thiserror::Error;
|
||
|
|
|
||
|
|
#[derive(Error, Debug)]
|
||
|
|
pub enum StateVariableError {
|
||
|
|
#[error("Conversion error: {0}")]
|
||
|
|
ConversionError(String),
|
||
|
|
|
||
|
|
#[error("Validation error: {0}")]
|
||
|
|
ValidationError(String),
|
||
|
|
|
||
|
|
#[error("Range error: {0}")]
|
||
|
|
RangeError(String),
|
||
|
|
|
||
|
|
#[error("Type error: {0}")]
|
||
|
|
TypeError(String),
|
||
|
|
|
||
|
|
#[error("Parse error: {0}")]
|
||
|
|
ParseError(String),
|
||
|
|
|
||
|
|
#[error("Event condition error: {0}")]
|
||
|
|
EventConditionError(String),
|
||
|
|
|
||
|
|
#[error("Arithmetic error: {0}")]
|
||
|
|
ArithmeticError(String),
|
||
|
|
|
||
|
|
#[error("Unknown error: {0}")]
|
||
|
|
Unknown(String),
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
## fichier: `pmoupnp/src/state_variables/variable_methods.rs`
|
||
|
|
|
||
|
|
```rust
|
||
|
|
use std::{
|
||
|
|
collections::HashMap,
|
||
|
|
fmt,
|
||
|
|
sync::Arc,
|
||
|
|
};
|
||
|
|
|
||
|
|
use std::sync::RwLock;
|
||
|
|
use xmltree::{Element, XMLNode};
|
||
|
|
|
||
|
|
use crate::{
|
||
|
|
UpnpObjectType, UpnpTyped,
|
||
|
|
object_trait::{UpnpModel, UpnpObject},
|
||
|
|
state_variables::{
|
||
|
|
StateConditionFunc, StateVarInstance, StateVariable, StringValueParser, ValueSerializer,
|
||
|
|
variable_trait::UpnpVariable,
|
||
|
|
},
|
||
|
|
value_ranges::ValueRange,
|
||
|
|
variable_types::{StateValue, StateValueError, StateVarType, UpnpVarType},
|
||
|
|
};
|
||
|
|
|
||
|
|
impl UpnpTyped for StateVariable {
|
||
|
|
fn as_upnp_object_type(&self) -> &UpnpObjectType {
|
||
|
|
&self.object
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
impl UpnpVarType for StateVariable {
|
||
|
|
fn as_state_var_type(&self) -> StateVarType {
|
||
|
|
self.value_type.as_state_var_type() // utilise ton From<&StateValue> existant
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
impl UpnpObject for StateVariable {
|
||
|
|
fn to_xml_element(&self) -> Element {
|
||
|
|
// Création de l'élément racine <stateVariable>
|
||
|
|
let mut root = Element::new("stateVariable");
|
||
|
|
root.attributes.insert(
|
||
|
|
"sendEvents".to_string(),
|
||
|
|
if self.send_events { "yes" } else { "no" }.to_string(),
|
||
|
|
);
|
||
|
|
|
||
|
|
// <name>
|
||
|
|
let mut name_elem = Element::new("name");
|
||
|
|
name_elem
|
||
|
|
.children
|
||
|
|
.push(XMLNode::Text(self.get_name().clone()));
|
||
|
|
|
||
|
|
// <dataType>
|
||
|
|
let mut datatype_elem = Element::new("dataType");
|
||
|
|
datatype_elem
|
||
|
|
.children
|
||
|
|
.push(XMLNode::Text(self.value_type.to_string())); // StateVarType doit impl Display
|
||
|
|
|
||
|
|
// <defaultValue> si défini
|
||
|
|
if let Some(default) = &self.default_value {
|
||
|
|
let mut def_elem = Element::new("defaultValue");
|
||
|
|
def_elem.children.push(XMLNode::Text(default.to_string()));
|
||
|
|
root.children.push(XMLNode::Element(def_elem));
|
||
|
|
}
|
||
|
|
|
||
|
|
// <allowedValueList> si défini
|
||
|
|
let av = self.allowed_values.read().unwrap();
|
||
|
|
if !av.is_empty() {
|
||
|
|
let mut list_elem = Element::new("allowedValueList");
|
||
|
|
for val in av.iter() {
|
||
|
|
let mut val_elem = Element::new("allowedValue");
|
||
|
|
val_elem.children.push(XMLNode::Text(val.to_string()));
|
||
|
|
list_elem.children.push(XMLNode::Element(val_elem));
|
||
|
|
}
|
||
|
|
root.children.push(XMLNode::Element(list_elem));
|
||
|
|
}
|
||
|
|
|
||
|
|
// <allowedValueRange> si défini
|
||
|
|
if let Some(range) = &self.value_range {
|
||
|
|
let mut range_elem = Element::new("allowedValueRange");
|
||
|
|
|
||
|
|
let mut min_elem = Element::new("minimum");
|
||
|
|
min_elem
|
||
|
|
.children
|
||
|
|
.push(XMLNode::Text(range.get_minimum().to_string()));
|
||
|
|
range_elem.children.push(XMLNode::Element(min_elem));
|
||
|
|
|
||
|
|
let mut max_elem = Element::new("maximum");
|
||
|
|
max_elem
|
||
|
|
.children
|
||
|
|
.push(XMLNode::Text(range.get_maximum().to_string()));
|
||
|
|
range_elem.children.push(XMLNode::Element(max_elem));
|
||
|
|
|
||
|
|
if let Some(step) = &self.step {
|
||
|
|
let mut step_elem = Element::new("step");
|
||
|
|
step_elem.children.push(XMLNode::Text(step.to_string()));
|
||
|
|
range_elem.children.push(XMLNode::Element(step_elem));
|
||
|
|
}
|
||
|
|
|
||
|
|
root.children.push(XMLNode::Element(range_elem));
|
||
|
|
}
|
||
|
|
|
||
|
|
// Ajouter les enfants communs
|
||
|
|
root.children.push(XMLNode::Element(name_elem));
|
||
|
|
root.children.push(XMLNode::Element(datatype_elem));
|
||
|
|
|
||
|
|
root
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
impl UpnpModel for StateVariable {
|
||
|
|
type Instance = StateVarInstance;
|
||
|
|
}
|
||
|
|
|
||
|
|
impl Clone for StateVariable {
|
||
|
|
fn clone(&self) -> Self {
|
||
|
|
// clone safe des structures protégées par RwLock en prenant un read lock
|
||
|
|
let event_conditions_clone = {
|
||
|
|
// si le lock est "poisoned" on panic - tu peux adapter la gestion si tu veux
|
||
|
|
let guard = self
|
||
|
|
.event_conditions
|
||
|
|
.read().unwrap();
|
||
|
|
// nécessite que Key: Clone, Value: Clone
|
||
|
|
Arc::new(RwLock::new(guard.clone()))
|
||
|
|
};
|
||
|
|
|
||
|
|
let allowed_values_clone = {
|
||
|
|
let guard = self
|
||
|
|
.allowed_values
|
||
|
|
.read().unwrap();
|
||
|
|
Arc::new(RwLock::new(guard.clone()))
|
||
|
|
};
|
||
|
|
|
||
|
|
Self {
|
||
|
|
object: self.object.clone(),
|
||
|
|
value_type: self.value_type.clone(),
|
||
|
|
step: self.step.clone(),
|
||
|
|
modifiable: self.modifiable,
|
||
|
|
event_conditions: event_conditions_clone,
|
||
|
|
description: self.description.clone(),
|
||
|
|
default_value: self.default_value.clone(),
|
||
|
|
value_range: self.value_range.clone(),
|
||
|
|
allowed_values: allowed_values_clone,
|
||
|
|
send_events: self.send_events,
|
||
|
|
// parse et marshal sont typiquement des Arc<dyn ...> — on clone l'Arc (shallow).
|
||
|
|
// Deep-cloner une closure ou un trait-objet n'est pas possible en général.
|
||
|
|
parse: self.parse.clone(),
|
||
|
|
marshal: self.marshal.clone(),
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
impl fmt::Debug for StateVariable {
|
||
|
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||
|
|
f.debug_struct("StateVariable")
|
||
|
|
.field("object", &self.object)
|
||
|
|
.field("value_type", &self.value_type)
|
||
|
|
.field("step", &self.step)
|
||
|
|
.field("modifiable", &self.modifiable)
|
||
|
|
.field(
|
||
|
|
"event_conditions",
|
||
|
|
&format_args!(
|
||
|
|
"len={}",
|
||
|
|
self.event_conditions.read().unwrap().len()
|
||
|
|
),
|
||
|
|
)
|
||
|
|
.field("description", &self.description)
|
||
|
|
.field("default_value", &self.default_value)
|
||
|
|
.field("value_range", &self.value_range)
|
||
|
|
.field(
|
||
|
|
"allowed_values",
|
||
|
|
&format_args!(
|
||
|
|
"len={}",
|
||
|
|
self.allowed_values.read().unwrap().len()
|
||
|
|
),
|
||
|
|
)
|
||
|
|
.field("send_events", &self.send_events)
|
||
|
|
.field(
|
||
|
|
"parse",
|
||
|
|
&self
|
||
|
|
.parse
|
||
|
|
.as_ref()
|
||
|
|
.map(|_| "Some(StringValueParser)")
|
||
|
|
.unwrap_or("None"),
|
||
|
|
)
|
||
|
|
.field(
|
||
|
|
"marshal",
|
||
|
|
&self
|
||
|
|
.marshal
|
||
|
|
.as_ref()
|
||
|
|
.map(|_| "Some(ValueSerializer)")
|
||
|
|
.unwrap_or("None"),
|
||
|
|
)
|
||
|
|
.finish()
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
impl UpnpVariable for StateVariable {
|
||
|
|
fn get_definition(&self) -> &StateVariable {
|
||
|
|
return self;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
impl StateVariable {
|
||
|
|
pub fn new(vartype: StateVarType, name: String) -> StateVariable {
|
||
|
|
Self {
|
||
|
|
object: UpnpObjectType {
|
||
|
|
name,
|
||
|
|
object_type: "StateVariable".to_string(),
|
||
|
|
},
|
||
|
|
value_type: vartype.clone(),
|
||
|
|
step: None,
|
||
|
|
modifiable: true,
|
||
|
|
event_conditions: Arc::new(RwLock::new(HashMap::new())),
|
||
|
|
description: "".to_string(),
|
||
|
|
default_value: None,
|
||
|
|
value_range: None,
|
||
|
|
allowed_values: Arc::new(RwLock::new(Vec::new())),
|
||
|
|
send_events: false,
|
||
|
|
parse: None,
|
||
|
|
marshal: None,
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
pub fn set_step(&mut self, step: StateValue) -> Result<(), StateValueError> {
|
||
|
|
if self.as_state_var_type() != step.as_state_var_type() {
|
||
|
|
return Err(StateValueError::TypeError("Bad step type".to_string()));
|
||
|
|
}
|
||
|
|
|
||
|
|
self.step = Some(step);
|
||
|
|
Ok(())
|
||
|
|
}
|
||
|
|
|
||
|
|
pub fn set_range(&mut self, min: &StateValue, max: &StateValue) -> Result<(), StateValueError> {
|
||
|
|
if self.as_state_var_type() != min.as_state_var_type() {
|
||
|
|
return Err(StateValueError::TypeError("Bad range type".to_string()));
|
||
|
|
}
|
||
|
|
|
||
|
|
let range = ValueRange::new(min, max)?; // ? propage l'erreur si elle existe
|
||
|
|
self.value_range = Some(range);
|
||
|
|
Ok(())
|
||
|
|
}
|
||
|
|
|
||
|
|
pub fn update_minimum(&mut self, min: &StateValue) -> Result<(), StateValueError> {
|
||
|
|
if !self.has_range() {
|
||
|
|
return Err(StateValueError::RangeError(
|
||
|
|
"No range specified for this variable".to_string(),
|
||
|
|
));
|
||
|
|
}
|
||
|
|
if self
|
||
|
|
.value_range
|
||
|
|
.as_ref()
|
||
|
|
.expect("Range is not defined")
|
||
|
|
.as_state_var_type()
|
||
|
|
!= min.as_state_var_type()
|
||
|
|
{
|
||
|
|
return Err(StateValueError::TypeError(
|
||
|
|
"new minimum is not the same than state variable".to_string(),
|
||
|
|
));
|
||
|
|
}
|
||
|
|
self.value_range
|
||
|
|
.as_mut()
|
||
|
|
.expect("Range is not defined")
|
||
|
|
.set_minimum(min);
|
||
|
|
return Ok(());
|
||
|
|
}
|
||
|
|
|
||
|
|
pub fn update_maximum(&mut self, min: &StateValue) -> Result<(), StateValueError> {
|
||
|
|
if !self.has_range() {
|
||
|
|
return Err(StateValueError::RangeError(
|
||
|
|
"No range specified for this variable".to_string(),
|
||
|
|
));
|
||
|
|
}
|
||
|
|
if self
|
||
|
|
.value_range
|
||
|
|
.as_ref()
|
||
|
|
.expect("Range is not defined")
|
||
|
|
.as_state_var_type()
|
||
|
|
!= min.as_state_var_type()
|
||
|
|
{
|
||
|
|
return Err(StateValueError::TypeError(
|
||
|
|
"new minimum is not the same than state variable".to_string(),
|
||
|
|
));
|
||
|
|
}
|
||
|
|
self.value_range
|
||
|
|
.as_mut()
|
||
|
|
.expect("Range is not defined")
|
||
|
|
.set_maximum(min);
|
||
|
|
return Ok(());
|
||
|
|
}
|
||
|
|
|
||
|
|
pub fn get_range(&self) -> Option<&ValueRange> {
|
||
|
|
return self.value_range.as_ref();
|
||
|
|
}
|
||
|
|
|
||
|
|
pub fn set_modifiable(&mut self) {
|
||
|
|
self.modifiable = true;
|
||
|
|
}
|
||
|
|
|
||
|
|
pub fn set_not_modifiable(&mut self) {
|
||
|
|
self.modifiable = false;
|
||
|
|
}
|
||
|
|
|
||
|
|
pub fn add_event_condition(&self, name: String, func: StateConditionFunc) {
|
||
|
|
// on lock en écriture
|
||
|
|
let mut guard = self.event_conditions.write().unwrap();
|
||
|
|
guard.insert(name, func);
|
||
|
|
// le lock est automatiquement relâché ici (RAII)
|
||
|
|
}
|
||
|
|
|
||
|
|
pub fn remove_event_condition(&self, name: &str) {
|
||
|
|
let mut guard = self.event_conditions.write().unwrap();
|
||
|
|
guard.remove(name);
|
||
|
|
}
|
||
|
|
|
||
|
|
pub fn clear_event_conditions(&mut self) {
|
||
|
|
let mut guard = self.event_conditions.write().unwrap();
|
||
|
|
guard.clear()
|
||
|
|
}
|
||
|
|
|
||
|
|
pub fn set_description(&mut self, description: String) {
|
||
|
|
self.description = description;
|
||
|
|
}
|
||
|
|
|
||
|
|
pub fn set_default(&mut self, value: &StateValue) -> Result<(), StateValueError> {
|
||
|
|
if self.as_state_var_type() != value.as_state_var_type() {
|
||
|
|
return Err(StateValueError::TypeError(
|
||
|
|
"value does not have the right type".to_string(),
|
||
|
|
));
|
||
|
|
}
|
||
|
|
self.default_value = Some(value.clone());
|
||
|
|
return Ok(());
|
||
|
|
}
|
||
|
|
|
||
|
|
pub fn unset_default(&mut self) {
|
||
|
|
self.default_value = None;
|
||
|
|
}
|
||
|
|
|
||
|
|
pub fn extend_allowed_values(&mut self, values: &[StateValue]) -> Result<(), StateValueError> {
|
||
|
|
let mut av = self
|
||
|
|
.allowed_values
|
||
|
|
.write().unwrap();
|
||
|
|
|
||
|
|
for v in values {
|
||
|
|
if self.as_state_var_type() == v.as_state_var_type() {
|
||
|
|
av.push(v.clone());
|
||
|
|
} else {
|
||
|
|
return Err(StateValueError::TypeError(
|
||
|
|
"new allowed value does not have the right type".to_string(),
|
||
|
|
));
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
Ok(())
|
||
|
|
}
|
||
|
|
|
||
|
|
pub fn push_allowed_value(&mut self, value: &StateValue) -> Result<(), StateValueError> {
|
||
|
|
let mut av = self
|
||
|
|
.allowed_values
|
||
|
|
.write().unwrap();
|
||
|
|
|
||
|
|
if self.as_state_var_type() == value.as_state_var_type() {
|
||
|
|
av.push(value.clone());
|
||
|
|
} else {
|
||
|
|
return Err(StateValueError::TypeError(
|
||
|
|
"new allowed value does not have the right type".to_string(),
|
||
|
|
));
|
||
|
|
}
|
||
|
|
|
||
|
|
return Ok(());
|
||
|
|
}
|
||
|
|
|
||
|
|
pub fn set_send_notification(&mut self) {
|
||
|
|
self.send_events = true;
|
||
|
|
}
|
||
|
|
|
||
|
|
pub fn unset_send_notification(&mut self) {
|
||
|
|
self.send_events = false;
|
||
|
|
}
|
||
|
|
|
||
|
|
pub fn set_value_parser(&mut self, parser: StringValueParser) -> Result<(), StateValueError> {
|
||
|
|
if self.as_state_var_type() == StateVarType::String {
|
||
|
|
self.parse = Some(parser);
|
||
|
|
return Ok(());
|
||
|
|
}
|
||
|
|
return Err(StateValueError::TypeError(
|
||
|
|
"Only String variables can have a parser".to_string(),
|
||
|
|
));
|
||
|
|
}
|
||
|
|
|
||
|
|
pub fn unset_value_parser(&mut self) {
|
||
|
|
self.parse = None;
|
||
|
|
}
|
||
|
|
|
||
|
|
pub fn set_value_marshaler(
|
||
|
|
&mut self,
|
||
|
|
marshaler: ValueSerializer,
|
||
|
|
) -> Result<(), StateValueError> {
|
||
|
|
if self.as_state_var_type() == StateVarType::String {
|
||
|
|
self.marshal = Some(marshaler);
|
||
|
|
return Ok(());
|
||
|
|
}
|
||
|
|
return Err(StateValueError::TypeError(
|
||
|
|
"Only String variables can have a marshaler".to_string(),
|
||
|
|
));
|
||
|
|
}
|
||
|
|
|
||
|
|
pub fn unset_value_marshaler(&mut self) {
|
||
|
|
self.marshal = None;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
## fichier: `pmoupnp/src/lib.rs`
|
||
|
|
|
||
|
|
```rust
|
||
|
|
mod object_trait;
|
||
|
|
mod object_set;
|
||
|
|
|
||
|
|
pub mod actions;
|
||
|
|
pub mod mediarenderer;
|
||
|
|
pub mod server;
|
||
|
|
pub mod services;
|
||
|
|
pub mod state_variables;
|
||
|
|
pub mod value_ranges;
|
||
|
|
pub mod variable_types;
|
||
|
|
|
||
|
|
|
||
|
|
use std::{collections::HashMap, sync::Arc};
|
||
|
|
|
||
|
|
use std::sync::RwLock;
|
||
|
|
|
||
|
|
pub use crate::object_trait::*;
|
||
|
|
|
||
|
|
#[derive(Debug, Clone)]
|
||
|
|
pub struct UpnpObjectType {
|
||
|
|
name: String,
|
||
|
|
object_type: String,
|
||
|
|
}
|
||
|
|
|
||
|
|
#[derive(Debug)]
|
||
|
|
pub struct UpnpObjectSet<T: UpnpTypedObject> {
|
||
|
|
objects: RwLock<HashMap<String, Arc<T>>>,
|
||
|
|
}
|
||
|
|
|
||
|
|
#[derive(Debug)]
|
||
|
|
pub enum UpnpObjectSetError {
|
||
|
|
AlreadyExists(String),
|
||
|
|
}
|
||
|
|
|
||
|
|
```
|
||
|
|
|
||
|
|
## fichier: `pmoupnp/src/value_ranges/methods.rs`
|
||
|
|
|
||
|
|
```rust
|
||
|
|
use std::cmp::Ordering;
|
||
|
|
|
||
|
|
use crate::{
|
||
|
|
value_ranges::ValueRange,
|
||
|
|
variable_types::{StateValue, StateValueError, StateVarType, UpnpVarType},
|
||
|
|
};
|
||
|
|
|
||
|
|
impl UpnpVarType for ValueRange {
|
||
|
|
fn as_state_var_type(&self) -> StateVarType {
|
||
|
|
self.min.as_state_var_type() // utilise ton From<&StateValue> existant
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
impl ValueRange {
|
||
|
|
pub fn new(min: &StateValue, max: &StateValue) -> Result<Self, StateValueError> {
|
||
|
|
if min.as_state_var_type() != max.as_state_var_type() {
|
||
|
|
return Err(StateValueError::TypeError(
|
||
|
|
"min and max do not belong the same time".to_string(),
|
||
|
|
));
|
||
|
|
}
|
||
|
|
|
||
|
|
// Vérifier que min <= max
|
||
|
|
if let Some(cmp) = min.partial_cmp(max) {
|
||
|
|
if cmp == Ordering::Greater {
|
||
|
|
return Err(StateValueError::RangeError(
|
||
|
|
"Minimum cannot be greater than maximum".to_string(),
|
||
|
|
));
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
Ok(Self {
|
||
|
|
min: min.clone(),
|
||
|
|
max: max.clone(),
|
||
|
|
})
|
||
|
|
}
|
||
|
|
|
||
|
|
pub fn get_minimum(self: &ValueRange) -> StateValue {
|
||
|
|
return self.min.clone();
|
||
|
|
}
|
||
|
|
|
||
|
|
pub fn set_minimum(&mut self, value: &StateValue) {
|
||
|
|
self.min = value.clone()
|
||
|
|
}
|
||
|
|
|
||
|
|
pub fn get_maximum(self: &ValueRange) -> StateValue {
|
||
|
|
return self.max.clone();
|
||
|
|
}
|
||
|
|
|
||
|
|
pub fn set_maximum(&mut self, value: &StateValue) {
|
||
|
|
self.max = value.clone()
|
||
|
|
}
|
||
|
|
|
||
|
|
pub fn is_in_range(&self, value: &StateValue) -> bool {
|
||
|
|
if self.as_state_var_type() == value.as_state_var_type()
|
||
|
|
&& let Some(cmp) = self.min.partial_cmp(value)
|
||
|
|
{
|
||
|
|
if cmp == Ordering::Greater {
|
||
|
|
return false;
|
||
|
|
}
|
||
|
|
if let Some(cmp2) = self.max.partial_cmp(value) {
|
||
|
|
if cmp2 == Ordering::Less {
|
||
|
|
return false;
|
||
|
|
}
|
||
|
|
return true;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
return false;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
## fichier: `pmoupnp/src/value_ranges/mod.rs`
|
||
|
|
|
||
|
|
```rust
|
||
|
|
mod methods;
|
||
|
|
|
||
|
|
use crate::variable_types::StateValue;
|
||
|
|
|
||
|
|
#[derive(Debug, Clone)]
|
||
|
|
pub struct ValueRange {
|
||
|
|
min: StateValue,
|
||
|
|
max: StateValue,
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
## fichier: `pmoupnp/src/mediarenderer/mod.rs`
|
||
|
|
|
||
|
|
```rust
|
||
|
|
pub mod avtransport;
|
||
|
|
```
|
||
|
|
|
||
|
|
## fichier: `pmoupnp/src/mediarenderer/avtransport/mod.rs`
|
||
|
|
|
||
|
|
```rust
|
||
|
|
pub mod variables;
|
||
|
|
pub mod actions;
|
||
|
|
|
||
|
|
```
|
||
|
|
|
||
|
|
## fichier: `pmoupnp/src/mediarenderer/avtransport/variables/a_arg_type_instanceid.rs`
|
||
|
|
|
||
|
|
```rust
|
||
|
|
use std::sync::Arc;
|
||
|
|
|
||
|
|
use crate::state_variables::StateVariable;
|
||
|
|
use crate::variable_types::StateVarType;
|
||
|
|
use once_cell::sync::Lazy;
|
||
|
|
|
||
|
|
pub static A_ARG_TYPE_INSTANCE_ID: Lazy<Arc<StateVariable>> = Lazy::new(|| -> Arc<StateVariable> {
|
||
|
|
Arc::new(StateVariable::new(StateVarType::UI4, "A_ARG_TYPE_InstanceID".to_string()))
|
||
|
|
});
|
||
|
|
```
|
||
|
|
|
||
|
|
## fichier: `pmoupnp/src/mediarenderer/avtransport/variables/a_arg_type_playspeed.rs`
|
||
|
|
|
||
|
|
```rust
|
||
|
|
use std::sync::Arc;
|
||
|
|
|
||
|
|
use crate::state_variables::StateVariable;
|
||
|
|
use crate::variable_types::StateVarType;
|
||
|
|
use once_cell::sync::Lazy;
|
||
|
|
|
||
|
|
pub static A_ARG_TYPE_PLAY_SPEED: Lazy<Arc<StateVariable>> = Lazy::new(|| -> Arc<StateVariable> {
|
||
|
|
Arc::new(StateVariable::new(StateVarType::String, "A_ARG_TYPE_PlaySpeed".to_string()))
|
||
|
|
});
|
||
|
|
```
|
||
|
|
|
||
|
|
## fichier: `pmoupnp/src/mediarenderer/avtransport/variables/transportstatus.rs`
|
||
|
|
|
||
|
|
```rust
|
||
|
|
use std::sync::Arc;
|
||
|
|
|
||
|
|
use crate::state_variables::StateVariable;
|
||
|
|
use crate::variable_types::{StateValue, StateVarType};
|
||
|
|
use once_cell::sync::Lazy;
|
||
|
|
|
||
|
|
pub static TRANSPORTSTATUS: Lazy<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");
|
||
|
|
|
||
|
|
Arc::new(sv)
|
||
|
|
});
|
||
|
|
```
|
||
|
|
|
||
|
|
## fichier: `pmoupnp/src/mediarenderer/avtransport/variables/transportplayspeed.rs`
|
||
|
|
|
||
|
|
```rust
|
||
|
|
use std::sync::Arc;
|
||
|
|
|
||
|
|
use crate::state_variables::StateVariable;
|
||
|
|
use crate::variable_types::{StateValue, StateVarType};
|
||
|
|
use once_cell::sync::Lazy;
|
||
|
|
|
||
|
|
pub static TRANSPORTPLAYSPEED: Lazy<Arc<StateVariable>> = Lazy::new(|| -> Arc<StateVariable> {
|
||
|
|
let mut sv = StateVariable::new(StateVarType::String, "TransportPlaySpeed".to_string());
|
||
|
|
|
||
|
|
sv.push_allowed_value(&StateValue::String("1".to_string())).expect("Cannot add allowed value");
|
||
|
|
sv.set_default(&StateValue::String("1".to_string())).expect("Cannt set default value");
|
||
|
|
|
||
|
|
Arc::new(sv)
|
||
|
|
});
|
||
|
|
|
||
|
|
```
|
||
|
|
|
||
|
|
## fichier: `pmoupnp/src/mediarenderer/avtransport/variables/avtransporturi.rs`
|
||
|
|
|
||
|
|
```rust
|
||
|
|
use std::sync::Arc;
|
||
|
|
|
||
|
|
use crate::state_variables::StateVariable;
|
||
|
|
use crate::variable_types::StateVarType;
|
||
|
|
use once_cell::sync::Lazy;
|
||
|
|
|
||
|
|
pub static AVTRANSPORTURI: Lazy<Arc<StateVariable>> = Lazy::new(|| -> Arc<StateVariable> {
|
||
|
|
Arc::new(StateVariable::new(StateVarType::String, "AVTransportURI".to_string()))
|
||
|
|
});
|
||
|
|
```
|
||
|
|
|
||
|
|
## fichier: `pmoupnp/src/mediarenderer/avtransport/variables/mod.rs`
|
||
|
|
|
||
|
|
```rust
|
||
|
|
mod a_arg_type_instanceid;
|
||
|
|
mod a_arg_type_playspeed;
|
||
|
|
mod avtransporturi;
|
||
|
|
mod avtransporturimetadata;
|
||
|
|
mod currenttrackduration;
|
||
|
|
mod seekmode;
|
||
|
|
mod transportplayspeed;
|
||
|
|
mod transportstate;
|
||
|
|
mod transportstatus;
|
||
|
|
|
||
|
|
pub use a_arg_type_instanceid::A_ARG_TYPE_INSTANCE_ID;
|
||
|
|
pub use a_arg_type_playspeed::A_ARG_TYPE_PLAY_SPEED;
|
||
|
|
pub use avtransporturi::AVTRANSPORTURI;
|
||
|
|
pub use avtransporturimetadata::AVTRANSPORTURIMETADATA;
|
||
|
|
pub use currenttrackduration::CURRENTTRACKDURATION;
|
||
|
|
pub use seekmode::SEEKMODE;
|
||
|
|
pub use transportplayspeed::TRANSPORTPLAYSPEED;
|
||
|
|
pub use transportstate::TRANSPORTSTATE;
|
||
|
|
pub use transportstatus::TRANSPORTSTATUS;
|
||
|
|
|
||
|
|
|
||
|
|
|
||
|
|
```
|
||
|
|
|
||
|
|
## fichier: `pmoupnp/src/mediarenderer/avtransport/variables/seekmode.rs`
|
||
|
|
|
||
|
|
```rust
|
||
|
|
use std::sync::Arc;
|
||
|
|
|
||
|
|
use crate::state_variables::StateVariable;
|
||
|
|
use crate::variable_types::StateVarType;
|
||
|
|
use once_cell::sync::Lazy;
|
||
|
|
|
||
|
|
pub static SEEKMODE: Lazy<Arc<StateVariable>> = Lazy::new(|| -> Arc<StateVariable> {
|
||
|
|
Arc::new(StateVariable::new(StateVarType::String, "SeekMode".to_string()))
|
||
|
|
});
|
||
|
|
|
||
|
|
```
|
||
|
|
|
||
|
|
## fichier: `pmoupnp/src/mediarenderer/avtransport/variables/currenttrackduration.rs`
|
||
|
|
|
||
|
|
```rust
|
||
|
|
use std::sync::Arc;
|
||
|
|
|
||
|
|
use crate::state_variables::StateVariable;
|
||
|
|
use crate::variable_types::StateVarType;
|
||
|
|
use once_cell::sync::Lazy;
|
||
|
|
|
||
|
|
pub static CURRENTTRACKDURATION: Lazy<Arc<StateVariable>> = Lazy::new(|| -> Arc<StateVariable> {
|
||
|
|
Arc::new(StateVariable::new(StateVarType::String, "CurrentTrackDuration".to_string()))
|
||
|
|
});
|
||
|
|
|
||
|
|
```
|
||
|
|
|
||
|
|
## fichier: `pmoupnp/src/mediarenderer/avtransport/variables/avtransporturimetadata.rs`
|
||
|
|
|
||
|
|
```rust
|
||
|
|
use std::sync::Arc;
|
||
|
|
|
||
|
|
use crate::state_variables::{StateVariable, StateVariableError};
|
||
|
|
use crate::variable_types::StateVarType;
|
||
|
|
use bevy_reflect::Reflect;
|
||
|
|
use once_cell::sync::Lazy;
|
||
|
|
use pmodidl::{DIDLLite, MediaMetadataParser};
|
||
|
|
|
||
|
|
// func _AVTransportURIMetaDataParser(value string) (interface{}, error) {
|
||
|
|
// log.Debug("[avtransport] Parsing AVTransport)")
|
||
|
|
// didl, err := pmodidl.Parse(value)
|
||
|
|
// if err != nil {
|
||
|
|
// return value, err
|
||
|
|
// }
|
||
|
|
|
||
|
|
// return didl, nil
|
||
|
|
// }
|
||
|
|
|
||
|
|
fn avtransporturimetadataparser(value: &str) -> Result<Box<dyn Reflect>, StateVariableError> {
|
||
|
|
// Parse DIDL-Lite
|
||
|
|
let didl = DIDLLite::parse(value)
|
||
|
|
.map_err(|e| StateVariableError::ParseError(format!("Failed to parse DIDL-Lite: {}", e)))?;
|
||
|
|
|
||
|
|
// Retourne le résultat sous forme de Box<dyn Reflect>
|
||
|
|
Ok(Box::new(didl) as Box<dyn Reflect>)
|
||
|
|
}
|
||
|
|
|
||
|
|
pub static AVTRANSPORTURIMETADATA: Lazy<Arc<StateVariable>> = Lazy::new(|| -> Arc<StateVariable> {
|
||
|
|
let mut sv = StateVariable::new(StateVarType::String, "AVTransportURIMetaData".to_string());
|
||
|
|
|
||
|
|
sv.set_value_parser(Arc::new(avtransporturimetadataparser)).expect("Failed to set parser");
|
||
|
|
Arc::new(sv)
|
||
|
|
});
|
||
|
|
```
|
||
|
|
|
||
|
|
## fichier: `pmoupnp/src/mediarenderer/avtransport/variables/transportstate.rs`
|
||
|
|
|
||
|
|
```rust
|
||
|
|
use std::sync::Arc;
|
||
|
|
|
||
|
|
use crate::state_variables::StateVariable;
|
||
|
|
use crate::variable_types::{StateValue, StateVarType};
|
||
|
|
use once_cell::sync::Lazy;
|
||
|
|
|
||
|
|
pub static TRANSPORTSTATE: Lazy<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()),
|
||
|
|
StateValue::String("RECORDING".to_string()),
|
||
|
|
StateValue::String("TRANSITIONING".to_string()),
|
||
|
|
StateValue::String("PAUSED_PLAYBACK".to_string()),
|
||
|
|
StateValue::String("PAUSED_RECORDING".to_string()),
|
||
|
|
StateValue::String("NO_MEDIA_PRESENT".to_string()),
|
||
|
|
]).expect("Cannt set default value");
|
||
|
|
|
||
|
|
Arc::new(sv)
|
||
|
|
});
|
||
|
|
|
||
|
|
```
|
||
|
|
|
||
|
|
## fichier: `pmoupnp/src/mediarenderer/avtransport/actions/play.rs`
|
||
|
|
|
||
|
|
```rust
|
||
|
|
use crate::mediarenderer::avtransport::variables::{A_ARG_TYPE_INSTANCE_ID, TRANSPORTPLAYSPEED};
|
||
|
|
use crate::define_action;
|
||
|
|
|
||
|
|
define_action! {
|
||
|
|
pub static PLAY = "Play" {
|
||
|
|
in "InstanceID" => A_ARG_TYPE_INSTANCE_ID,
|
||
|
|
in "Speed" => TRANSPORTPLAYSPEED,
|
||
|
|
}
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
## fichier: `pmoupnp/src/mediarenderer/avtransport/actions/stop.rs`
|
||
|
|
|
||
|
|
```rust
|
||
|
|
use crate::mediarenderer::avtransport::variables::A_ARG_TYPE_INSTANCE_ID;
|
||
|
|
use crate::define_action;
|
||
|
|
|
||
|
|
define_action! {
|
||
|
|
pub static STOP = "Stop" {
|
||
|
|
in "InstanceID" => A_ARG_TYPE_INSTANCE_ID,
|
||
|
|
}
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
## fichier: `pmoupnp/src/mediarenderer/avtransport/actions/setavtransporturi.rs`
|
||
|
|
|
||
|
|
```rust
|
||
|
|
use crate::mediarenderer::avtransport::variables::{A_ARG_TYPE_INSTANCE_ID, AVTRANSPORTURI, AVTRANSPORTURIMETADATA};
|
||
|
|
use crate::define_action;
|
||
|
|
|
||
|
|
define_action! {
|
||
|
|
pub static SETAVTRANSPORTURI = "SetAVTransportURI" {
|
||
|
|
in "InstanceID" => A_ARG_TYPE_INSTANCE_ID,
|
||
|
|
in "CurrentURI" => AVTRANSPORTURI,
|
||
|
|
in "CurrentURIMetaData" => AVTRANSPORTURIMETADATA,
|
||
|
|
}
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
## fichier: `pmoupnp/src/mediarenderer/avtransport/actions/mod.rs`
|
||
|
|
|
||
|
|
```rust
|
||
|
|
mod play;
|
||
|
|
mod stop;
|
||
|
|
mod setavtransporturi;
|
||
|
|
|
||
|
|
pub use play::PLAY;
|
||
|
|
pub use stop::STOP;
|
||
|
|
pub use setavtransporturi::SETAVTRANSPORTURI;
|
||
|
|
|
||
|
|
```
|
||
|
|
|
||
|
|
## fichier: `pmoupnp/src/server/mod.rs`
|
||
|
|
|
||
|
|
```rust
|
||
|
|
//! # Module Server - API de haut niveau pour Axum
|
||
|
|
//!
|
||
|
|
//! Ce module fournit une abstraction simple et ergonomique pour créer des serveurs HTTP
|
||
|
|
//! avec Axum, en cachant la complexité de la configuration et du routage.
|
||
|
|
//!
|
||
|
|
//! ## Fonctionnalités
|
||
|
|
//!
|
||
|
|
//! - 🚀 **Routes JSON simples** : Ajoutez des endpoints API avec `add_route()`
|
||
|
|
//! - 📁 **Fichiers statiques** : Servez des assets avec `add_dir()`
|
||
|
|
//! - ⚛️ **Applications SPA** : Support pour Vue.js/React avec `add_spa()`
|
||
|
|
//! - 🔀 **Redirections** : Redirigez des routes avec `add_redirect()`
|
||
|
|
//! - 🎯 **Handlers personnalisés** : Support SSE, WebSocket, etc. avec `add_handler_with_state()`
|
||
|
|
//! - 📚 **Documentation API** : OpenAPI/Swagger automatique avec `add_openapi()`
|
||
|
|
//! - ⚡ **Gestion gracieuse** : Arrêt propre sur Ctrl+C
|
||
|
|
|
||
|
|
pub mod logs;
|
||
|
|
|
||
|
|
use axum::handler::Handler;
|
||
|
|
use axum::response::Redirect;
|
||
|
|
use axum::routing::get;
|
||
|
|
use axum::{Json, Router};
|
||
|
|
use axum_embed::ServeEmbed;
|
||
|
|
use pmoconfig::get_config;
|
||
|
|
use rust_embed::RustEmbed;
|
||
|
|
use serde::Serialize;
|
||
|
|
use std::{net::SocketAddr, sync::Arc};
|
||
|
|
use tokio::{signal, sync::RwLock, task::JoinHandle};
|
||
|
|
use tracing::{info, warn, debug, error};
|
||
|
|
use utoipa::OpenApi;
|
||
|
|
use utoipa_swagger_ui::SwaggerUi;
|
||
|
|
|
||
|
|
/// Info serveur sérialisable
|
||
|
|
#[derive(Clone, Serialize, utoipa::ToSchema)]
|
||
|
|
pub struct ServerInfo {
|
||
|
|
/// Nom du serveur
|
||
|
|
pub name: String,
|
||
|
|
/// URL de base
|
||
|
|
pub base_url: String,
|
||
|
|
/// Port HTTP
|
||
|
|
pub http_port: u16,
|
||
|
|
}
|
||
|
|
|
||
|
|
/// Serveur principal
|
||
|
|
pub struct Server {
|
||
|
|
name: String,
|
||
|
|
base_url: String,
|
||
|
|
http_port: u16,
|
||
|
|
router: Arc<RwLock<Router>>,
|
||
|
|
api_router: Arc<RwLock<Option<Router>>>,
|
||
|
|
join_handle: Option<JoinHandle<()>>,
|
||
|
|
}
|
||
|
|
|
||
|
|
#[derive(RustEmbed, Clone)]
|
||
|
|
#[folder = "webapp/dist"]
|
||
|
|
pub struct Webapp;
|
||
|
|
|
||
|
|
impl Server {
|
||
|
|
/// Crée une nouvelle instance de serveur
|
||
|
|
///
|
||
|
|
/// # Arguments
|
||
|
|
///
|
||
|
|
/// * `name` - Nom du serveur (pour les logs)
|
||
|
|
/// * `base_url` - URL de base (ex: "http://localhost:3000")
|
||
|
|
/// * `http_port` - Port HTTP à écouter
|
||
|
|
///
|
||
|
|
/// # Exemple
|
||
|
|
///
|
||
|
|
/// ```rust
|
||
|
|
/// # use pmoupnp::server::Server;
|
||
|
|
/// let server = Server::new("MyAPI", "http://localhost:3000", 3000);
|
||
|
|
/// ```
|
||
|
|
pub fn new(name: impl Into<String>, base_url: impl Into<String>, http_port: u16) -> Self {
|
||
|
|
Self {
|
||
|
|
name: name.into(),
|
||
|
|
base_url: base_url.into(),
|
||
|
|
http_port,
|
||
|
|
router: Arc::new(RwLock::new(Router::new())),
|
||
|
|
api_router: Arc::new(RwLock::new(None)),
|
||
|
|
join_handle: None,
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
pub fn new_configured() -> Self {
|
||
|
|
let config = get_config();
|
||
|
|
let url = config.get_base_url();
|
||
|
|
let port = config.get_http_port();
|
||
|
|
|
||
|
|
return Self::new("PMO-Music-Server", url, port);
|
||
|
|
}
|
||
|
|
|
||
|
|
/// Ajoute une route JSON dynamique
|
||
|
|
///
|
||
|
|
/// Crée un endpoint qui retourne du JSON. La closure fournie sera appelée
|
||
|
|
/// à chaque requête GET sur le chemin spécifié.
|
||
|
|
///
|
||
|
|
/// # Arguments
|
||
|
|
///
|
||
|
|
/// * `path` - Chemin de la route (ex: "/api/hello")
|
||
|
|
/// * `f` - Closure async retournant une valeur sérialisable
|
||
|
|
///
|
||
|
|
/// # Exemple
|
||
|
|
///
|
||
|
|
/// ```rust,no_run
|
||
|
|
/// # use pmoupnp::server::Server;
|
||
|
|
/// # #[tokio::main]
|
||
|
|
/// # async fn main() {
|
||
|
|
/// # let mut server = Server::new("Test", "http://localhost:3000", 3000);
|
||
|
|
/// server.add_route("/api/status", || async {
|
||
|
|
/// serde_json::json!({
|
||
|
|
/// "status": "online",
|
||
|
|
/// "version": "1.0.0"
|
||
|
|
/// })
|
||
|
|
/// }).await;
|
||
|
|
/// # }
|
||
|
|
/// ```
|
||
|
|
pub async fn add_route<F, Fut, T>(&mut self, path: &str, f: F)
|
||
|
|
where
|
||
|
|
F: Fn() -> Fut + Send + Sync + 'static,
|
||
|
|
Fut: std::future::Future<Output = T> + Send + 'static,
|
||
|
|
T: Serialize + Send + 'static,
|
||
|
|
{
|
||
|
|
let f = Arc::new(f);
|
||
|
|
|
||
|
|
let handler = {
|
||
|
|
let f = f.clone();
|
||
|
|
move || {
|
||
|
|
let f = f.clone();
|
||
|
|
async move { Json(f().await) }
|
||
|
|
}
|
||
|
|
};
|
||
|
|
|
||
|
|
let route = Router::new().route("/", get(handler));
|
||
|
|
|
||
|
|
let mut r = self.router.write().await;
|
||
|
|
*r = std::mem::take(&mut *r).nest(path, route);
|
||
|
|
}
|
||
|
|
|
||
|
|
/// Ajoute un répertoire de fichiers statiques
|
||
|
|
///
|
||
|
|
/// Sert des fichiers embarqués via `RustEmbed`. Les fichiers sont compilés
|
||
|
|
/// dans le binaire à la compilation.
|
||
|
|
///
|
||
|
|
/// # Arguments
|
||
|
|
///
|
||
|
|
/// * `path` - Chemin où monter les fichiers statiques
|
||
|
|
///
|
||
|
|
/// # Type Parameter
|
||
|
|
///
|
||
|
|
/// * `E` - Type RustEmbed définissant le répertoire à servir
|
||
|
|
///
|
||
|
|
/// # Exemple
|
||
|
|
///
|
||
|
|
/// ```ignore
|
||
|
|
/// use pmoupnp::server::Server;
|
||
|
|
/// use rust_embed::RustEmbed;
|
||
|
|
///
|
||
|
|
/// #[derive(RustEmbed, Clone)]
|
||
|
|
/// #[folder = "static/"]
|
||
|
|
/// struct Assets;
|
||
|
|
///
|
||
|
|
/// # #[tokio::main]
|
||
|
|
/// # async fn main() {
|
||
|
|
/// let mut server = Server::new("Test", "http://localhost:3000", 3000);
|
||
|
|
/// server.add_dir::<Assets>("/assets").await;
|
||
|
|
/// // Les fichiers de static/ sont accessibles via /assets/*
|
||
|
|
/// # }
|
||
|
|
/// ```
|
||
|
|
pub async fn add_dir<E>(&mut self, path: &str)
|
||
|
|
where
|
||
|
|
E: RustEmbed + Clone + Send + Sync + 'static,
|
||
|
|
{
|
||
|
|
let serve = ServeEmbed::<E>::new();
|
||
|
|
|
||
|
|
let mut r = self.router.write().await;
|
||
|
|
|
||
|
|
if path == "/" {
|
||
|
|
*r = std::mem::take(&mut *r).fallback_service(serve);
|
||
|
|
} else {
|
||
|
|
let route = Router::new().fallback_service(serve);
|
||
|
|
*r = std::mem::take(&mut *r).nest(path, route);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
/// Ajoute une Single Page Application (SPA)
|
||
|
|
///
|
||
|
|
/// Sert une application JavaScript moderne (Vue.js, React, etc.) avec support
|
||
|
|
/// du routage côté client. Tous les chemins non trouvés renvoient `index.html`
|
||
|
|
/// pour permettre au routeur JavaScript de gérer la navigation.
|
||
|
|
///
|
||
|
|
/// # Arguments
|
||
|
|
///
|
||
|
|
/// * `path` - Chemin où monter l'application (souvent "/" ou "/app")
|
||
|
|
///
|
||
|
|
/// # Type Parameter
|
||
|
|
///
|
||
|
|
/// * `E` - Type RustEmbed contenant les fichiers de la SPA
|
||
|
|
///
|
||
|
|
/// # Exemple avec Vue.js
|
||
|
|
///
|
||
|
|
/// ```rust,no_run
|
||
|
|
/// # use pmoupnp::server::Server;
|
||
|
|
/// # use rust_embed::RustEmbed;
|
||
|
|
/// #[derive(RustEmbed, Clone)]
|
||
|
|
/// #[folder = "webapp/dist"] // Build output de Vue.js
|
||
|
|
/// struct WebApp;
|
||
|
|
///
|
||
|
|
/// # #[tokio::main]
|
||
|
|
/// # async fn main() {
|
||
|
|
/// # let mut server = Server::new("Test", "http://localhost:3000", 3000);
|
||
|
|
/// server.add_spa::<WebApp>("/").await;
|
||
|
|
/// // L'app Vue.js gère toutes les routes comme /about, /users, etc.
|
||
|
|
/// # }
|
||
|
|
/// ```
|
||
|
|
///
|
||
|
|
/// # Note
|
||
|
|
///
|
||
|
|
/// Pour Vue.js/Vite, configure le `base` dans `vite.config.js` si tu montes
|
||
|
|
/// sur un sous-chemin :
|
||
|
|
/// ```javascript
|
||
|
|
/// export default {
|
||
|
|
/// base: '/app/'
|
||
|
|
/// }
|
||
|
|
/// ```
|
||
|
|
pub async fn add_spa<E>(&mut self, path: &str)
|
||
|
|
where
|
||
|
|
E: RustEmbed + Clone + Send + Sync + 'static,
|
||
|
|
{
|
||
|
|
let serve = ServeEmbed::<E>::with_parameters(
|
||
|
|
Some("index.html".to_string()),
|
||
|
|
axum_embed::FallbackBehavior::Ok,
|
||
|
|
Some("index.html".to_string()),
|
||
|
|
);
|
||
|
|
|
||
|
|
let mut r = self.router.write().await;
|
||
|
|
|
||
|
|
if path == "/" {
|
||
|
|
*r = std::mem::take(&mut *r).fallback_service(serve);
|
||
|
|
} else {
|
||
|
|
let route = Router::new().fallback_service(serve);
|
||
|
|
*r = std::mem::take(&mut *r).nest(path, route);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
/// Ajoute un handler Axum personnalisé
|
||
|
|
///
|
||
|
|
/// Pour des cas d'usage avancés nécessitant un contrôle complet sur le handler.
|
||
|
|
///
|
||
|
|
/// # Arguments
|
||
|
|
///
|
||
|
|
/// * `path` - Chemin de la route
|
||
|
|
/// * `handler` - Handler Axum
|
||
|
|
///
|
||
|
|
/// # Exemple
|
||
|
|
///
|
||
|
|
/// ```rust,no_run
|
||
|
|
/// # use pmoupnp::server::Server;
|
||
|
|
/// # use axum::response::Html;
|
||
|
|
/// # #[tokio::main]
|
||
|
|
/// # async fn main() {
|
||
|
|
/// # let mut server = Server::new("Test", "http://localhost:3000", 3000);
|
||
|
|
/// async fn custom_handler() -> Html<&'static str> {
|
||
|
|
/// Html("<h1>Custom Response</h1>")
|
||
|
|
/// }
|
||
|
|
///
|
||
|
|
/// server.add_handler("/custom", custom_handler).await;
|
||
|
|
/// # }
|
||
|
|
/// ```
|
||
|
|
pub async fn add_handler<H, T>(&mut self, path: &str, handler: H)
|
||
|
|
where
|
||
|
|
H: Handler<T, ()>,
|
||
|
|
T: 'static,
|
||
|
|
{
|
||
|
|
let route = Router::new().route("/", get(handler));
|
||
|
|
|
||
|
|
let mut r = self.router.write().await;
|
||
|
|
*r = std::mem::take(&mut *r).nest(path, route);
|
||
|
|
}
|
||
|
|
|
||
|
|
/// Ajoute un handler avec state (pour SSE, extracteurs, etc.)
|
||
|
|
///
|
||
|
|
/// Permet d'utiliser des extracteurs Axum comme `State`, `Query`, etc.
|
||
|
|
/// Idéal pour Server-Sent Events (SSE), WebSockets ou tout handler nécessitant un état partagé.
|
||
|
|
///
|
||
|
|
/// # Arguments
|
||
|
|
///
|
||
|
|
/// * `path` - Chemin de la route
|
||
|
|
/// * `handler` - Handler Axum avec extracteurs
|
||
|
|
/// * `state` - État partagé (doit être Clone + Send + Sync)
|
||
|
|
///
|
||
|
|
/// # Exemple avec SSE
|
||
|
|
///
|
||
|
|
/// ```ignore
|
||
|
|
/// use pmoupnp::server::Server;
|
||
|
|
/// use axum::extract::State;
|
||
|
|
/// use axum::response::sse::{Event, Sse, KeepAlive};
|
||
|
|
/// use tokio::sync::broadcast;
|
||
|
|
///
|
||
|
|
/// #[derive(Clone)]
|
||
|
|
/// struct LogState {
|
||
|
|
/// tx: broadcast::Sender<String>
|
||
|
|
/// }
|
||
|
|
///
|
||
|
|
/// impl LogState {
|
||
|
|
/// fn subscribe(&self) -> broadcast::Receiver<String> {
|
||
|
|
/// self.tx.subscribe()
|
||
|
|
/// }
|
||
|
|
/// }
|
||
|
|
///
|
||
|
|
/// async fn log_sse(State(state): State<LogState>) -> Sse<impl futures::Stream<Item = Result<Event, std::convert::Infallible>>> {
|
||
|
|
/// let mut rx = state.subscribe();
|
||
|
|
/// let stream = async_stream::stream! {
|
||
|
|
/// while let Ok(msg) = rx.recv().await {
|
||
|
|
/// yield Ok(Event::default().data(msg));
|
||
|
|
/// }
|
||
|
|
/// };
|
||
|
|
/// Sse::new(stream).keep_alive(KeepAlive::default())
|
||
|
|
/// }
|
||
|
|
///
|
||
|
|
/// let log_state = LogState { tx: broadcast::channel(100).0 };
|
||
|
|
/// server.add_handler_with_state("/logs", log_sse, log_state).await;
|
||
|
|
/// ```
|
||
|
|
pub async fn add_handler_with_state<H, T, S>(&mut self, path: &str, handler: H, state: S)
|
||
|
|
where
|
||
|
|
H: Handler<T, S>,
|
||
|
|
T: 'static,
|
||
|
|
S: Clone + Send + Sync + 'static,
|
||
|
|
{
|
||
|
|
let route = Router::new()
|
||
|
|
.route("/", get(handler))
|
||
|
|
.with_state(state);
|
||
|
|
|
||
|
|
let mut r = self.router.write().await;
|
||
|
|
*r = std::mem::take(&mut *r).nest(path, route);
|
||
|
|
}
|
||
|
|
|
||
|
|
/// Ajoute un handler POST avec state
|
||
|
|
///
|
||
|
|
/// Similaire à `add_handler_with_state` mais pour les requêtes POST.
|
||
|
|
///
|
||
|
|
/// # Arguments
|
||
|
|
///
|
||
|
|
/// * `path` - Chemin de la route
|
||
|
|
/// * `handler` - Handler Axum pour POST
|
||
|
|
/// * `state` - État partagé
|
||
|
|
pub async fn add_post_handler_with_state<H, T, S>(&mut self, path: &str, handler: H, state: S)
|
||
|
|
where
|
||
|
|
H: Handler<T, S>,
|
||
|
|
T: 'static,
|
||
|
|
S: Clone + Send + Sync + 'static,
|
||
|
|
{
|
||
|
|
let route = Router::new()
|
||
|
|
.route("/", axum::routing::post(handler))
|
||
|
|
.with_state(state);
|
||
|
|
|
||
|
|
let mut r = self.router.write().await;
|
||
|
|
*r = std::mem::take(&mut *r).nest(path, route);
|
||
|
|
}
|
||
|
|
|
||
|
|
/// Ajoute une redirection HTTP
|
||
|
|
///
|
||
|
|
/// Redirige automatiquement les requêtes d'un chemin vers un autre avec un code 308 (permanent).
|
||
|
|
///
|
||
|
|
/// # Arguments
|
||
|
|
///
|
||
|
|
/// * `from` - Chemin source (peut être "/" pour la racine)
|
||
|
|
/// * `to` - Chemin de destination
|
||
|
|
///
|
||
|
|
/// # Exemple
|
||
|
|
///
|
||
|
|
/// ```rust,no_run
|
||
|
|
/// # use pmoupnp::server::Server;
|
||
|
|
/// # #[tokio::main]
|
||
|
|
/// # async fn main() {
|
||
|
|
/// # let mut server = Server::new("Test", "http://localhost:3000", 3000);
|
||
|
|
/// // Rediriger la racine vers /app
|
||
|
|
/// server.add_redirect("/", "/app").await;
|
||
|
|
/// # }
|
||
|
|
/// ```
|
||
|
|
pub async fn add_redirect(&mut self, from: &str, to: &str) {
|
||
|
|
let to = to.to_string();
|
||
|
|
let handler = move || {
|
||
|
|
let to = to.clone();
|
||
|
|
async move { Redirect::permanent(&to) }
|
||
|
|
};
|
||
|
|
|
||
|
|
let mut r = self.router.write().await;
|
||
|
|
|
||
|
|
if from == "/" {
|
||
|
|
// Pour la racine, utiliser merge au lieu de nest
|
||
|
|
let route = Router::new().route("/", get(handler));
|
||
|
|
*r = std::mem::take(&mut *r).merge(route);
|
||
|
|
} else {
|
||
|
|
let route = Router::new().route("/", get(handler));
|
||
|
|
*r = std::mem::take(&mut *r).nest(from, route);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
/// Ajoute une API documentée avec OpenAPI
|
||
|
|
///
|
||
|
|
/// Monte un routeur d'API sous `/api` et active Swagger UI sur `/swagger-ui`
|
||
|
|
///
|
||
|
|
/// # Arguments
|
||
|
|
///
|
||
|
|
/// * `api_router` - Router Axum contenant les routes API
|
||
|
|
/// * `openapi` - Spécification OpenAPI générée par utoipa
|
||
|
|
///
|
||
|
|
/// # Exemple
|
||
|
|
///
|
||
|
|
/// ```ignore
|
||
|
|
/// use utoipa::OpenApi;
|
||
|
|
/// use axum::{Router, Json, routing::get};
|
||
|
|
/// use serde::{Serialize, Deserialize};
|
||
|
|
///
|
||
|
|
/// #[derive(Serialize, Deserialize, utoipa::ToSchema)]
|
||
|
|
/// struct User {
|
||
|
|
/// id: u64,
|
||
|
|
/// name: String,
|
||
|
|
/// }
|
||
|
|
///
|
||
|
|
/// #[derive(utoipa::OpenApi)]
|
||
|
|
/// #[openapi(
|
||
|
|
/// paths(get_users),
|
||
|
|
/// components(schemas(User))
|
||
|
|
/// )]
|
||
|
|
/// struct ApiDoc;
|
||
|
|
///
|
||
|
|
/// #[utoipa::path(
|
||
|
|
/// get,
|
||
|
|
/// path = "/users",
|
||
|
|
/// responses((status = 200, description = "List users"))
|
||
|
|
/// )]
|
||
|
|
/// async fn get_users() -> Json<Vec<User>> {
|
||
|
|
/// Json(vec![])
|
||
|
|
/// }
|
||
|
|
///
|
||
|
|
/// let api_router = Router::new()
|
||
|
|
/// .route("/users", get(get_users));
|
||
|
|
///
|
||
|
|
/// server.add_openapi(api_router, ApiDoc::openapi()).await;
|
||
|
|
/// ```
|
||
|
|
pub async fn add_openapi(&mut self, api_router: Router, openapi: utoipa::openapi::OpenApi) {
|
||
|
|
// Stocker le routeur API
|
||
|
|
let mut api_r = self.api_router.write().await;
|
||
|
|
*api_r = Some(api_router);
|
||
|
|
|
||
|
|
// Ajouter Swagger UI
|
||
|
|
let swagger = SwaggerUi::new("/swagger-ui")
|
||
|
|
.url("/api-docs/openapi.json", openapi);
|
||
|
|
|
||
|
|
let mut r = self.router.write().await;
|
||
|
|
*r = std::mem::take(&mut *r).merge(swagger);
|
||
|
|
}
|
||
|
|
|
||
|
|
/// Démarre le serveur HTTP
|
||
|
|
///
|
||
|
|
/// Lance le serveur sur le port configuré et met en place la gestion
|
||
|
|
/// de Ctrl+C pour un arrêt gracieux.
|
||
|
|
///
|
||
|
|
/// # Exemple
|
||
|
|
///
|
||
|
|
/// ```rust,no_run
|
||
|
|
/// # use pmoupnp::server::Server;
|
||
|
|
/// # #[tokio::main]
|
||
|
|
/// # async fn main() {
|
||
|
|
/// # let mut server = Server::new("Test", "http://localhost:3000", 3000);
|
||
|
|
/// server.start().await;
|
||
|
|
/// server.wait().await; // Attend Ctrl+C
|
||
|
|
/// # }
|
||
|
|
/// ```
|
||
|
|
pub async fn start(&mut self) {
|
||
|
|
let addr = SocketAddr::from(([0, 0, 0, 0], self.http_port));
|
||
|
|
info!("Server {} running at [http://{}:{}](http://{}:{})", self.name, self.base_url, self.http_port, self.base_url, self.http_port);
|
||
|
|
|
||
|
|
// Merger le routeur API si présent
|
||
|
|
let api_router = self.api_router.read().await;
|
||
|
|
if let Some(api_r) = api_router.as_ref() {
|
||
|
|
let mut r = self.router.write().await;
|
||
|
|
*r = std::mem::take(&mut *r).nest("/api", api_r.clone());
|
||
|
|
}
|
||
|
|
drop(api_router);
|
||
|
|
|
||
|
|
let router = self.router.clone();
|
||
|
|
|
||
|
|
let server_task = tokio::spawn(async move {
|
||
|
|
let r = router.read().await.clone();
|
||
|
|
let listener = tokio::net::TcpListener::bind(addr).await.unwrap();
|
||
|
|
axum::serve(listener, r.into_make_service()).await.unwrap();
|
||
|
|
});
|
||
|
|
|
||
|
|
let shutdown_task = tokio::spawn(async move {
|
||
|
|
signal::ctrl_c().await.expect("failed to listen for ctrl_c");
|
||
|
|
info!("Ctrl+C reçu, arrêt gracieux");
|
||
|
|
});
|
||
|
|
|
||
|
|
self.join_handle = Some(tokio::spawn(async move {
|
||
|
|
tokio::select! {
|
||
|
|
_ = server_task => {},
|
||
|
|
_ = shutdown_task => {},
|
||
|
|
}
|
||
|
|
}));
|
||
|
|
}
|
||
|
|
|
||
|
|
/// Attend la fin du serveur
|
||
|
|
pub async fn wait(&mut self) {
|
||
|
|
if let Some(h) = self.join_handle.take() {
|
||
|
|
let _ = h.await;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
/// Récupère les infos du serveur
|
||
|
|
pub fn info(&self) -> ServerInfo {
|
||
|
|
ServerInfo {
|
||
|
|
name: self.name.clone(),
|
||
|
|
base_url: self.base_url.clone(),
|
||
|
|
http_port: self.http_port,
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
/// Builder pattern
|
||
|
|
pub struct ServerBuilder {
|
||
|
|
name: String,
|
||
|
|
base_url: String,
|
||
|
|
http_port: u16,
|
||
|
|
}
|
||
|
|
|
||
|
|
impl ServerBuilder {
|
||
|
|
/// Crée un nouveau builder
|
||
|
|
///
|
||
|
|
/// # Arguments
|
||
|
|
///
|
||
|
|
/// * `name` - Nom du serveur
|
||
|
|
/// * `base_url` - URL de base (ex: "http://localhost:3000")
|
||
|
|
/// * `http_port` - Port HTTP
|
||
|
|
pub fn new(name: impl Into<String>, base_url: impl Into<String>, http_port: u16) -> Self {
|
||
|
|
Self {
|
||
|
|
name: name.into(),
|
||
|
|
base_url: base_url.into(),
|
||
|
|
http_port,
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
pub fn new_configured() -> Self {
|
||
|
|
let config = get_config();
|
||
|
|
Self {
|
||
|
|
name: "PMO-Music-Server".to_string(),
|
||
|
|
base_url: config.get_base_url(),
|
||
|
|
http_port: config.get_http_port()
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
/// Construit le serveur
|
||
|
|
///
|
||
|
|
/// Consomme le builder et retourne une instance de `Server` prête à l'emploi.
|
||
|
|
///
|
||
|
|
/// # Exemple
|
||
|
|
///
|
||
|
|
/// ```rust
|
||
|
|
/// # use pmoupnp::server::ServerBuilder;
|
||
|
|
/// let mut server = ServerBuilder::new("MyAPI", "http://localhost:3000", 3000)
|
||
|
|
/// .build();
|
||
|
|
/// ```
|
||
|
|
pub fn build(self) -> Server {
|
||
|
|
Server::new(self.name, self.base_url, self.http_port)
|
||
|
|
}
|
||
|
|
}```
|
||
|
|
|
||
|
|
## fichier: `pmoupnp/src/server/logs/mod.rs`
|
||
|
|
|
||
|
|
```rust
|
||
|
|
// logs.rs
|
||
|
|
mod sselayer;
|
||
|
|
|
||
|
|
pub use sselayer::SseLayer;
|
||
|
|
|
||
|
|
use std::{
|
||
|
|
collections::VecDeque,
|
||
|
|
sync::{Arc, RwLock},
|
||
|
|
time::SystemTime,
|
||
|
|
};
|
||
|
|
|
||
|
|
use axum::{
|
||
|
|
Json,
|
||
|
|
extract::{Query, State},
|
||
|
|
response::{
|
||
|
|
IntoResponse,
|
||
|
|
sse::{Event, KeepAlive, Sse},
|
||
|
|
},
|
||
|
|
};
|
||
|
|
use serde::{Deserialize, Serialize};
|
||
|
|
use tokio::sync::broadcast;
|
||
|
|
|
||
|
|
/// Représente une entrée de log
|
||
|
|
#[derive(Debug, Clone, Serialize)]
|
||
|
|
pub struct LogEntry {
|
||
|
|
pub timestamp: SystemTime,
|
||
|
|
pub level: String,
|
||
|
|
pub target: String,
|
||
|
|
pub message: String,
|
||
|
|
}
|
||
|
|
|
||
|
|
/// Buffer circulaire partagé
|
||
|
|
#[derive(Clone)]
|
||
|
|
pub struct LogState {
|
||
|
|
buffer: Arc<RwLock<VecDeque<LogEntry>>>,
|
||
|
|
tx: broadcast::Sender<LogEntry>,
|
||
|
|
}
|
||
|
|
|
||
|
|
impl LogState {
|
||
|
|
pub fn new(capacity: usize) -> Self {
|
||
|
|
Self {
|
||
|
|
buffer: Arc::new(RwLock::new(VecDeque::with_capacity(capacity))),
|
||
|
|
tx: broadcast::channel(1000).0,
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
fn push(&self, entry: LogEntry) {
|
||
|
|
let mut buf = self.buffer.write().unwrap();
|
||
|
|
if buf.len() == buf.capacity() {
|
||
|
|
buf.pop_front();
|
||
|
|
}
|
||
|
|
buf.push_back(entry.clone());
|
||
|
|
let _ = self.tx.send(entry);
|
||
|
|
}
|
||
|
|
|
||
|
|
pub fn subscribe(&self) -> broadcast::Receiver<LogEntry> {
|
||
|
|
self.tx.subscribe()
|
||
|
|
}
|
||
|
|
|
||
|
|
pub fn dump(&self) -> Vec<LogEntry> {
|
||
|
|
self.buffer.read().unwrap().iter().cloned().collect()
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
/// Query params pour /log-sse
|
||
|
|
#[derive(Debug, Deserialize)]
|
||
|
|
pub struct LogQuery {
|
||
|
|
#[serde(default)]
|
||
|
|
pub error: Option<bool>,
|
||
|
|
#[serde(default)]
|
||
|
|
pub warn: Option<bool>,
|
||
|
|
#[serde(default)]
|
||
|
|
pub info: Option<bool>,
|
||
|
|
#[serde(default)]
|
||
|
|
pub debug: Option<bool>,
|
||
|
|
#[serde(default)]
|
||
|
|
pub trace: Option<bool>,
|
||
|
|
#[serde(default)]
|
||
|
|
pub search: Option<String>,
|
||
|
|
}
|
||
|
|
|
||
|
|
/// Handler SSE
|
||
|
|
// Dans logs.rs
|
||
|
|
pub async fn log_sse(
|
||
|
|
State(state): State<LogState>,
|
||
|
|
Query(params): Query<LogQuery>,
|
||
|
|
) -> impl IntoResponse {
|
||
|
|
let mut rx = state.subscribe();
|
||
|
|
|
||
|
|
// Récupérer l'historique du buffer
|
||
|
|
let history = state.dump();
|
||
|
|
|
||
|
|
let stream = async_stream::stream! {
|
||
|
|
// 1. Envoyer d'abord tous les logs historiques
|
||
|
|
for entry in history {
|
||
|
|
if !filter_entry(&entry, ¶ms) {
|
||
|
|
continue;
|
||
|
|
}
|
||
|
|
let json = serde_json::to_string(&entry).unwrap();
|
||
|
|
yield Ok::<_, axum::Error>(Event::default().data(json));
|
||
|
|
}
|
||
|
|
|
||
|
|
// 2. Puis streamer les nouveaux logs en temps réel
|
||
|
|
while let Ok(entry) = rx.recv().await {
|
||
|
|
if !filter_entry(&entry, ¶ms) {
|
||
|
|
continue;
|
||
|
|
}
|
||
|
|
let json = serde_json::to_string(&entry).unwrap();
|
||
|
|
yield Ok::<_, axum::Error>(Event::default().data(json));
|
||
|
|
}
|
||
|
|
};
|
||
|
|
|
||
|
|
Sse::new(stream).keep_alive(KeepAlive::default())
|
||
|
|
}
|
||
|
|
|
||
|
|
/// Handler REST (dump JSON du buffer)
|
||
|
|
pub async fn log_dump(State(state): State<LogState>) -> impl IntoResponse {
|
||
|
|
Json(state.dump())
|
||
|
|
}
|
||
|
|
|
||
|
|
/// Fonction de filtrage
|
||
|
|
fn filter_entry(entry: &LogEntry, q: &LogQuery) -> bool {
|
||
|
|
// Filtrage par niveau
|
||
|
|
let lvl = entry.level.to_lowercase();
|
||
|
|
let mut allowed = false;
|
||
|
|
|
||
|
|
if let Some(true) = q.error {
|
||
|
|
allowed |= lvl == "error";
|
||
|
|
}
|
||
|
|
if let Some(true) = q.warn {
|
||
|
|
allowed |= lvl == "warn";
|
||
|
|
}
|
||
|
|
if let Some(true) = q.info {
|
||
|
|
allowed |= lvl == "info";
|
||
|
|
}
|
||
|
|
if let Some(true) = q.debug {
|
||
|
|
allowed |= lvl == "debug";
|
||
|
|
}
|
||
|
|
if let Some(true) = q.trace {
|
||
|
|
allowed |= lvl == "trace";
|
||
|
|
}
|
||
|
|
|
||
|
|
// si aucun flag → tout est autorisé
|
||
|
|
if !(q.error.unwrap_or(false)
|
||
|
|
|| q.warn.unwrap_or(false)
|
||
|
|
|| q.info.unwrap_or(false)
|
||
|
|
|| q.debug.unwrap_or(false)
|
||
|
|
|| q.trace.unwrap_or(false))
|
||
|
|
{
|
||
|
|
allowed = true;
|
||
|
|
}
|
||
|
|
|
||
|
|
// Filtrage par mot-clé
|
||
|
|
if let Some(search) = &q.search {
|
||
|
|
allowed &= entry.message.contains(search) || entry.target.contains(search);
|
||
|
|
}
|
||
|
|
|
||
|
|
allowed
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
## fichier: `pmoupnp/src/server/logs/sselayer.rs`
|
||
|
|
|
||
|
|
```rust
|
||
|
|
use tracing::field::{Field, Visit};
|
||
|
|
use tracing::{Event, Subscriber};
|
||
|
|
use tracing_subscriber::{Layer, layer::Context};
|
||
|
|
|
||
|
|
use super::{LogEntry, LogState};
|
||
|
|
use std::time::SystemTime;
|
||
|
|
|
||
|
|
struct LogVisitor {
|
||
|
|
message: String,
|
||
|
|
}
|
||
|
|
|
||
|
|
impl LogVisitor {
|
||
|
|
fn new() -> Self {
|
||
|
|
Self {
|
||
|
|
message: String::new(),
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
impl Visit for LogVisitor {
|
||
|
|
fn record_debug(&mut self, field: &Field, value: &dyn std::fmt::Debug) {
|
||
|
|
// capture le champ "message" ou concatène les autres
|
||
|
|
if field.name() == "message" {
|
||
|
|
self.message = format!("{:?}", value);
|
||
|
|
} else {
|
||
|
|
if !self.message.is_empty() {
|
||
|
|
self.message.push(' ');
|
||
|
|
}
|
||
|
|
self.message
|
||
|
|
.push_str(&format!("{}={:?}", field.name(), value));
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
/// Layer de tracing qui pousse les events dans le buffer
|
||
|
|
pub struct SseLayer {
|
||
|
|
state: LogState,
|
||
|
|
}
|
||
|
|
|
||
|
|
impl SseLayer {
|
||
|
|
pub fn new(state: LogState) -> Self {
|
||
|
|
Self { state }
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
impl<S> Layer<S> for SseLayer
|
||
|
|
where
|
||
|
|
S: Subscriber,
|
||
|
|
{
|
||
|
|
fn on_event(&self, event: &Event<'_>, _ctx: Context<'_, S>) {
|
||
|
|
let mut visitor = LogVisitor::new();
|
||
|
|
event.record(&mut visitor);
|
||
|
|
|
||
|
|
let entry = LogEntry {
|
||
|
|
timestamp: SystemTime::now(),
|
||
|
|
level: event.metadata().level().to_string(),
|
||
|
|
target: event.metadata().target().to_string(),
|
||
|
|
message: visitor.message,
|
||
|
|
};
|
||
|
|
|
||
|
|
self.state.push(entry);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
## fichier: `pmoupnp/src/actions/action_instance_set.rs`
|
||
|
|
|
||
|
|
```rust
|
||
|
|
use crate::{
|
||
|
|
UpnpObject,
|
||
|
|
actions::{ActionInstanceSet},
|
||
|
|
};
|
||
|
|
|
||
|
|
use xmltree::{Element,XMLNode};
|
||
|
|
|
||
|
|
impl UpnpObject for ActionInstanceSet {
|
||
|
|
// Méthode pour convertir en XML (à implémenter avec une librairie XML)
|
||
|
|
fn to_xml_element(&self) -> Element {
|
||
|
|
let mut elem = Element::new("actionList");
|
||
|
|
|
||
|
|
for action in self.all() {
|
||
|
|
let action_elem = action.to_xml_element(); // retourne un <action> complet
|
||
|
|
elem.children.push(XMLNode::Element(action_elem));
|
||
|
|
}
|
||
|
|
|
||
|
|
elem
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
```
|
||
|
|
|
||
|
|
## fichier: `pmoupnp/src/actions/action_instance.rs`
|
||
|
|
|
||
|
|
```rust
|
||
|
|
use std::sync::Arc;
|
||
|
|
|
||
|
|
use xmltree::{Element, XMLNode};
|
||
|
|
|
||
|
|
use crate::actions::Action;
|
||
|
|
use crate::actions::Argument;
|
||
|
|
use crate::actions::ArgumentSet;
|
||
|
|
use crate::actions::ArgInstanceSet;
|
||
|
|
use crate::actions::ActionInstance;
|
||
|
|
use crate::UpnpInstance;
|
||
|
|
use crate::UpnpObject;
|
||
|
|
use crate::UpnpTyped;
|
||
|
|
use crate::UpnpTypedInstance;
|
||
|
|
use crate::UpnpObjectType;
|
||
|
|
|
||
|
|
impl UpnpObject for ActionInstance {
|
||
|
|
fn to_xml_element(&self) -> Element {
|
||
|
|
let mut elem = Element::new("action");
|
||
|
|
|
||
|
|
// <name>
|
||
|
|
let mut name_elem = Element::new("name");
|
||
|
|
name_elem.children.push(XMLNode::Text(self.get_name().clone()));
|
||
|
|
elem.children.push(XMLNode::Element(name_elem));
|
||
|
|
|
||
|
|
// Utiliser le set d'instances d'arguments
|
||
|
|
let args_container = self.arguments.to_xml_element();
|
||
|
|
elem.children.push(XMLNode::Element(args_container));
|
||
|
|
|
||
|
|
elem
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
impl UpnpTyped for ActionInstance {
|
||
|
|
fn as_upnp_object_type(&self) -> &UpnpObjectType {
|
||
|
|
return &self.object;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
impl UpnpInstance for ActionInstance {
|
||
|
|
|
||
|
|
type Model = Action;
|
||
|
|
|
||
|
|
fn new(action: &Action) -> Self {
|
||
|
|
// Créer les instances d'arguments
|
||
|
|
let mut arguments = ArgInstanceSet::new();
|
||
|
|
|
||
|
|
for arg_model in action.arguments().all() {
|
||
|
|
let arg_instance = Arc::new(crate::actions::ArgumentInstance::new(&*arg_model));
|
||
|
|
if let Err(e) = arguments.insert(arg_instance) {
|
||
|
|
tracing::error!("Failed to insert argument instance: {:?}", e);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
Self {
|
||
|
|
object: UpnpObjectType {
|
||
|
|
name: action.get_name().clone(),
|
||
|
|
object_type: "ActionInstance".to_string(),
|
||
|
|
},
|
||
|
|
model: action.clone(),
|
||
|
|
arguments, // ⬅️ Set d'instances, pas le modèle !
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
}
|
||
|
|
|
||
|
|
|
||
|
|
impl UpnpTypedInstance for ActionInstance {
|
||
|
|
|
||
|
|
fn get_model(&self) -> &Self::Model {
|
||
|
|
&self.model
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
impl ActionInstance {
|
||
|
|
/// Retourne une instance d'argument par son nom.
|
||
|
|
///
|
||
|
|
/// # Arguments
|
||
|
|
///
|
||
|
|
/// * `name` - Nom de l'argument à rechercher
|
||
|
|
///
|
||
|
|
/// # Returns
|
||
|
|
///
|
||
|
|
/// `Some(Arc<ArgumentInstance>)` si trouvé, `None` sinon.
|
||
|
|
pub fn argument(&self, name: &str) -> Option<Arc<crate::actions::ArgumentInstance>> {
|
||
|
|
self.arguments.get_by_name(name)
|
||
|
|
}
|
||
|
|
|
||
|
|
/// Retourne le set d'instances d'arguments.
|
||
|
|
///
|
||
|
|
/// # Returns
|
||
|
|
///
|
||
|
|
/// Référence vers le `ArgInstanceSet` contenant toutes les instances.
|
||
|
|
///
|
||
|
|
/// # Examples
|
||
|
|
///
|
||
|
|
/// ```ignore
|
||
|
|
/// for arg_instance in action_instance.arguments_set().all() {
|
||
|
|
/// println!("Argument: {}", arg_instance.get_name());
|
||
|
|
/// if let Some(var) = arg_instance.get_variable_instance() {
|
||
|
|
/// println!(" Variable: {} = {}", var.get_name(), var.value());
|
||
|
|
/// }
|
||
|
|
/// }
|
||
|
|
/// ```
|
||
|
|
pub fn arguments_set(&self) -> &ArgInstanceSet {
|
||
|
|
&self.arguments // ⬅️ Retourne les INSTANCES, pas les modèles !
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
#[cfg(test)]
|
||
|
|
mod tests {
|
||
|
|
use super::*;
|
||
|
|
use crate::actions::Action;
|
||
|
|
use crate::UpnpInstance;
|
||
|
|
|
||
|
|
#[test]
|
||
|
|
fn test_action_instance_creation() {
|
||
|
|
let action = Action::new("Play".to_string());
|
||
|
|
let instance = ActionInstance::new(&action);
|
||
|
|
|
||
|
|
assert_eq!(instance.get_name(), "Play");
|
||
|
|
}
|
||
|
|
|
||
|
|
#[test]
|
||
|
|
fn test_action_instance_has_argument_instances() {
|
||
|
|
let action = Action::new("Play".to_string());
|
||
|
|
let instance = ActionInstance::new(&action);
|
||
|
|
|
||
|
|
// Vérifier que arguments_set() retourne bien des instances
|
||
|
|
assert!(instance.arguments_set().all().iter().all(|arg| {
|
||
|
|
// Chaque argument doit être une ArgumentInstance
|
||
|
|
arg.get_model(); // Cette méthode existe seulement sur les instances
|
||
|
|
true
|
||
|
|
}));
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
```
|
||
|
|
|
||
|
|
## fichier: `pmoupnp/src/actions/arg_set_methods.rs`
|
||
|
|
|
||
|
|
```rust
|
||
|
|
use crate::actions::ArgInstanceSet;
|
||
|
|
use crate::UpnpModel;
|
||
|
|
use crate::{
|
||
|
|
UpnpObject,
|
||
|
|
actions::{ArgumentSet},
|
||
|
|
};
|
||
|
|
use xmltree::Element;
|
||
|
|
|
||
|
|
impl UpnpObject for ArgumentSet {
|
||
|
|
// Méthode pour convertir en XML (à implémenter avec une librairie XML)
|
||
|
|
fn to_xml_element(&self) -> Element {
|
||
|
|
let mut elem = Element::new("argumentList");
|
||
|
|
|
||
|
|
for arg in self.all() {
|
||
|
|
let arg_elem = arg.to_xml_element(); // toujours un <argumentList> contenant 1 ou 2 <argument>
|
||
|
|
|
||
|
|
// Pour InOut, on ajoute tous les enfants du <argumentList> généré
|
||
|
|
for child in arg_elem.children {
|
||
|
|
elem.children.push(child);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
elem
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
impl UpnpModel for ArgumentSet {
|
||
|
|
type Instance = ArgInstanceSet;
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
## fichier: `pmoupnp/src/actions/argument_methods.rs`
|
||
|
|
|
||
|
|
```rust
|
||
|
|
use std::sync::Arc;
|
||
|
|
|
||
|
|
use xmltree::{Element, XMLNode};
|
||
|
|
|
||
|
|
use crate::{
|
||
|
|
actions::{Argument, ArgumentInstance}, state_variables::StateVariable, UpnpModel, UpnpObject, UpnpObjectType, UpnpTyped
|
||
|
|
};
|
||
|
|
|
||
|
|
impl UpnpTyped for Argument {
|
||
|
|
fn as_upnp_object_type(&self) -> &UpnpObjectType {
|
||
|
|
&self.object
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
impl UpnpObject for Argument {
|
||
|
|
fn to_xml_element(&self) -> Element {
|
||
|
|
let mut parent = Element::new("argumentList");
|
||
|
|
|
||
|
|
if self.is_in() && self.is_out() {
|
||
|
|
// InOut → deux arguments
|
||
|
|
parent.children.push(XMLNode::Element(make_argument_elem(
|
||
|
|
self.get_name(),
|
||
|
|
"in",
|
||
|
|
self.state_variable().get_name(),
|
||
|
|
)));
|
||
|
|
parent.children.push(XMLNode::Element(make_argument_elem(
|
||
|
|
self.get_name(),
|
||
|
|
"out",
|
||
|
|
self.state_variable().get_name(),
|
||
|
|
)));
|
||
|
|
} else {
|
||
|
|
// Cas simple
|
||
|
|
let direction = if self.is_in() { "in" } else { "out" };
|
||
|
|
parent.children.push(XMLNode::Element(make_argument_elem(
|
||
|
|
self.get_name(),
|
||
|
|
direction,
|
||
|
|
self.state_variable().get_name(),
|
||
|
|
)));
|
||
|
|
}
|
||
|
|
|
||
|
|
parent
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
impl UpnpModel for Argument {
|
||
|
|
type Instance = ArgumentInstance;
|
||
|
|
}
|
||
|
|
|
||
|
|
|
||
|
|
|
||
|
|
impl Argument {
|
||
|
|
fn new(name: String, state_variable: Arc<StateVariable>) -> Self {
|
||
|
|
Self {
|
||
|
|
object: UpnpObjectType {
|
||
|
|
name,
|
||
|
|
object_type: "Argument".to_string(),
|
||
|
|
},
|
||
|
|
state_variable,
|
||
|
|
is_in: false,
|
||
|
|
is_out: false,
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
pub fn new_in(name: String, state_variable: Arc<StateVariable>) -> Self {
|
||
|
|
let mut arg = Self::new(name, state_variable);
|
||
|
|
arg.is_in = true;
|
||
|
|
arg
|
||
|
|
}
|
||
|
|
|
||
|
|
pub fn new_out(name: String, state_variable: Arc<StateVariable>) -> Self {
|
||
|
|
let mut arg = Self::new(name, state_variable);
|
||
|
|
arg.is_out = true;
|
||
|
|
arg
|
||
|
|
}
|
||
|
|
|
||
|
|
pub fn new_in_out(name: String, state_variable: Arc<StateVariable>) -> Self {
|
||
|
|
let mut arg = Self::new(name, state_variable);
|
||
|
|
arg.is_in = true;
|
||
|
|
arg.is_out = true;
|
||
|
|
arg
|
||
|
|
}
|
||
|
|
|
||
|
|
pub fn state_variable(&self) -> &StateVariable {
|
||
|
|
&self.state_variable
|
||
|
|
}
|
||
|
|
|
||
|
|
pub fn is_in(&self) -> bool {
|
||
|
|
self.is_in
|
||
|
|
}
|
||
|
|
|
||
|
|
pub fn is_out(&self) -> bool {
|
||
|
|
self.is_out
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
/// Fabrique un <argument> complet avec ses sous-éléments
|
||
|
|
fn make_argument_elem(name: &str, direction: &str, state_var_name: &str) -> Element {
|
||
|
|
let mut arg = Element::new("argument");
|
||
|
|
|
||
|
|
let mut name_elem = Element::new("name");
|
||
|
|
name_elem.children.push(XMLNode::Text(name.to_string()));
|
||
|
|
|
||
|
|
let mut dir_elem = Element::new("direction");
|
||
|
|
dir_elem.children.push(XMLNode::Text(direction.to_string()));
|
||
|
|
|
||
|
|
let mut rel_elem = Element::new("relatedStateVariable");
|
||
|
|
rel_elem
|
||
|
|
.children
|
||
|
|
.push(XMLNode::Text(state_var_name.to_string()));
|
||
|
|
|
||
|
|
arg.children.push(XMLNode::Element(name_elem));
|
||
|
|
arg.children.push(XMLNode::Element(dir_elem));
|
||
|
|
arg.children.push(XMLNode::Element(rel_elem));
|
||
|
|
|
||
|
|
arg
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
## fichier: `pmoupnp/src/actions/arg_inst_set_methods.rs`
|
||
|
|
|
||
|
|
```rust
|
||
|
|
use std::collections::HashMap;
|
||
|
|
|
||
|
|
use std::sync::RwLock;
|
||
|
|
use xmltree::{Element, XMLNode};
|
||
|
|
|
||
|
|
use crate::{actions::{ArgInstanceSet, ArgumentSet}, UpnpObject};
|
||
|
|
|
||
|
|
use crate::UpnpInstance;
|
||
|
|
|
||
|
|
impl UpnpObject for ArgInstanceSet {
|
||
|
|
fn to_xml_element(&self) -> Element {
|
||
|
|
let mut elem = Element::new("serviceStateTable");
|
||
|
|
|
||
|
|
for state_var in self.all() {
|
||
|
|
let state_var_elem = state_var.to_xml_element(); // retourne un <stateVariable> complet
|
||
|
|
elem.children.push(XMLNode::Element(state_var_elem));
|
||
|
|
}
|
||
|
|
|
||
|
|
elem
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
impl UpnpInstance for ArgInstanceSet {
|
||
|
|
type Model = ArgumentSet;
|
||
|
|
|
||
|
|
fn new(_: &ArgumentSet) -> Self {
|
||
|
|
Self { objects: RwLock::new(HashMap::new()) }
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
|
||
|
|
```
|
||
|
|
|
||
|
|
## fichier: `pmoupnp/src/actions/mod.rs`
|
||
|
|
|
||
|
|
```rust
|
||
|
|
mod errors;
|
||
|
|
|
||
|
|
mod action_instance;
|
||
|
|
mod action_instance_set;
|
||
|
|
mod action_methods;
|
||
|
|
mod action_set_methods;
|
||
|
|
mod arg_inst_set_methods;
|
||
|
|
mod arg_instance_methods;
|
||
|
|
mod arg_set_methods;
|
||
|
|
mod argument_methods;
|
||
|
|
|
||
|
|
mod macros;
|
||
|
|
|
||
|
|
use crate::{
|
||
|
|
UpnpObjectSet, UpnpObjectType,
|
||
|
|
state_variables::{StateVarInstance, StateVariable},
|
||
|
|
};
|
||
|
|
use std::sync::{Arc, RwLock};
|
||
|
|
|
||
|
|
pub use errors::ActionError;
|
||
|
|
|
||
|
|
#[derive(Debug, Clone)]
|
||
|
|
pub struct Action {
|
||
|
|
object: UpnpObjectType,
|
||
|
|
arguments: ArgumentSet,
|
||
|
|
}
|
||
|
|
|
||
|
|
pub type ActionSet = UpnpObjectSet<Action>;
|
||
|
|
|
||
|
|
#[derive(Debug, Clone)]
|
||
|
|
pub struct ActionInstance {
|
||
|
|
object: UpnpObjectType,
|
||
|
|
model: Action,
|
||
|
|
arguments: ArgInstanceSet,
|
||
|
|
}
|
||
|
|
|
||
|
|
pub type ActionInstanceSet = UpnpObjectSet<ActionInstance>;
|
||
|
|
|
||
|
|
#[derive(Debug, Clone)]
|
||
|
|
pub struct Argument {
|
||
|
|
object: UpnpObjectType,
|
||
|
|
state_variable: Arc<StateVariable>,
|
||
|
|
is_in: bool,
|
||
|
|
is_out: bool,
|
||
|
|
}
|
||
|
|
|
||
|
|
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,
|
||
|
|
|
||
|
|
/// 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>;
|
||
|
|
```
|
||
|
|
|
||
|
|
## fichier: `pmoupnp/src/actions/errors.rs`
|
||
|
|
|
||
|
|
```rust
|
||
|
|
use thiserror::Error;
|
||
|
|
|
||
|
|
#[derive(Error, Debug)]
|
||
|
|
pub enum ActionError {
|
||
|
|
#[error("Action error: {0}")]
|
||
|
|
GeneralError(String),
|
||
|
|
|
||
|
|
#[error("Argument error: {0}")]
|
||
|
|
ArgumentError(String),
|
||
|
|
|
||
|
|
#[error("Set operation error: {0}")]
|
||
|
|
SetError(String),
|
||
|
|
}
|
||
|
|
|
||
|
|
impl From<std::io::Error> for ActionError {
|
||
|
|
fn from(err: std::io::Error) -> Self {
|
||
|
|
ActionError::GeneralError(format!("IO error: {}", err))
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
#[derive(Error, Debug)]
|
||
|
|
pub enum ArgumentError {
|
||
|
|
#[error("Argument error: {0}")]
|
||
|
|
GeneralError(String),
|
||
|
|
|
||
|
|
#[error("Argument error: {0}")]
|
||
|
|
ArgumentError(String),
|
||
|
|
|
||
|
|
#[error("Set operation error: {0}")]
|
||
|
|
SetError(String),
|
||
|
|
}
|
||
|
|
|
||
|
|
impl From<std::io::Error> for ArgumentError {
|
||
|
|
fn from(err: std::io::Error) -> Self {
|
||
|
|
ArgumentError::GeneralError(format!("IO error: {}", err))
|
||
|
|
}
|
||
|
|
}```
|
||
|
|
|
||
|
|
## fichier: `pmoupnp/src/actions/macros.rs`
|
||
|
|
|
||
|
|
```rust
|
||
|
|
/// Macro pour définir facilement une action UPnP.
|
||
|
|
///
|
||
|
|
/// Cette macro simplifie la création d'actions UPnP statiques en générant
|
||
|
|
/// automatiquement le code nécessaire pour initialiser une action avec ses arguments.
|
||
|
|
///
|
||
|
|
/// # Syntaxe
|
||
|
|
///
|
||
|
|
/// ## Action avec arguments
|
||
|
|
///
|
||
|
|
/// ```ignore
|
||
|
|
/// define_action! {
|
||
|
|
/// pub static ACTION_NAME = "ActionName" {
|
||
|
|
/// in "ParamName" => VARIABLE_REF,
|
||
|
|
/// out "ResultParam" => RESULT_VAR,
|
||
|
|
/// }
|
||
|
|
/// }
|
||
|
|
/// ```
|
||
|
|
///
|
||
|
|
/// ## Action sans arguments
|
||
|
|
///
|
||
|
|
/// ```ignore
|
||
|
|
/// define_action! {
|
||
|
|
/// pub static ACTION_NAME = "ActionName"
|
||
|
|
/// }
|
||
|
|
/// ```
|
||
|
|
///
|
||
|
|
/// # Arguments
|
||
|
|
///
|
||
|
|
/// - `ACTION_NAME` : Nom de la constante statique Rust
|
||
|
|
/// - `"ActionName"` : Nom de l'action UPnP (chaîne littérale)
|
||
|
|
/// - `in` ou `out` : Direction de l'argument (entrée ou sortie)
|
||
|
|
/// - `"ParamName"` : Nom du paramètre UPnP (chaîne littérale)
|
||
|
|
/// - `VARIABLE_REF` : Référence vers une `Lazy<Arc<StateVariable>>`
|
||
|
|
///
|
||
|
|
/// # Type de retour
|
||
|
|
///
|
||
|
|
/// La macro génère une `Lazy<Arc<Action>>` qui sera initialisée lors du premier accès.
|
||
|
|
///
|
||
|
|
/// # Prérequis
|
||
|
|
///
|
||
|
|
/// Les variables d'état référencées doivent être définies comme :
|
||
|
|
///
|
||
|
|
/// ```ignore
|
||
|
|
/// pub static MY_VAR: Lazy<Arc<StateVariable>> = Lazy::new(|| {
|
||
|
|
/// Arc::new(StateVariable::new(StateVarType::UI4, "MyVar".to_string()))
|
||
|
|
/// });
|
||
|
|
/// ```
|
||
|
|
///
|
||
|
|
/// # Examples
|
||
|
|
///
|
||
|
|
/// ```ignore
|
||
|
|
/// use once_cell::sync::Lazy;
|
||
|
|
/// use std::sync::Arc;
|
||
|
|
///
|
||
|
|
/// // Définir les variables d'état
|
||
|
|
/// pub static INSTANCE_ID: Lazy<Arc<StateVariable>> = Lazy::new(|| {
|
||
|
|
/// Arc::new(StateVariable::new(StateVarType::UI4, "InstanceID".to_string()))
|
||
|
|
/// });
|
||
|
|
///
|
||
|
|
/// pub static TRANSPORT_URI: Lazy<Arc<StateVariable>> = Lazy::new(|| {
|
||
|
|
/// Arc::new(StateVariable::new(StateVarType::String, "TransportURI".to_string()))
|
||
|
|
/// });
|
||
|
|
///
|
||
|
|
/// // Définir une action avec arguments
|
||
|
|
/// define_action! {
|
||
|
|
/// pub static PLAY = "Play" {
|
||
|
|
/// in "InstanceID" => INSTANCE_ID,
|
||
|
|
/// in "Speed" => TRANSPORT_SPEED,
|
||
|
|
/// }
|
||
|
|
/// }
|
||
|
|
///
|
||
|
|
/// // Action sans arguments
|
||
|
|
/// define_action! {
|
||
|
|
/// pub static PAUSE = "Pause"
|
||
|
|
/// }
|
||
|
|
///
|
||
|
|
/// // Utilisation
|
||
|
|
/// fn main() {
|
||
|
|
/// let play_action = &*PLAY; // Déréférence la Lazy<Arc<Action>>
|
||
|
|
/// println!("Action: {}", play_action.get_name());
|
||
|
|
/// }
|
||
|
|
/// ```
|
||
|
|
///
|
||
|
|
/// # Notes d'implémentation
|
||
|
|
///
|
||
|
|
/// - Les `Arc<StateVariable>` sont clonés (shallow copy du pointeur)
|
||
|
|
/// - Chaque `Argument` est wrappé dans un `Arc`
|
||
|
|
/// - L'`Action` finale est wrappée dans un `Arc`
|
||
|
|
/// - Initialisation paresseuse via `Lazy` (thread-safe)
|
||
|
|
#[macro_export]
|
||
|
|
macro_rules! define_action {
|
||
|
|
// Variante sans arguments
|
||
|
|
(pub static $name:ident = $action_name:literal) => {
|
||
|
|
pub static $name: once_cell::sync::Lazy<std::sync::Arc<$crate::actions::Action>> =
|
||
|
|
once_cell::sync::Lazy::new(|| {
|
||
|
|
std::sync::Arc::new($crate::actions::Action::new($action_name.to_string()))
|
||
|
|
});
|
||
|
|
};
|
||
|
|
|
||
|
|
// Variante avec arguments
|
||
|
|
(pub static $name:ident = $action_name:literal {
|
||
|
|
$(
|
||
|
|
$direction:ident $arg_name:literal => $var_ref:expr
|
||
|
|
),* $(,)?
|
||
|
|
}) => {
|
||
|
|
pub static $name: once_cell::sync::Lazy<std::sync::Arc<$crate::actions::Action>> =
|
||
|
|
once_cell::sync::Lazy::new(|| {
|
||
|
|
let mut ac = $crate::actions::Action::new($action_name.to_string());
|
||
|
|
|
||
|
|
$(
|
||
|
|
ac.add_argument(
|
||
|
|
define_action!(@arg $direction $arg_name, $var_ref)
|
||
|
|
);
|
||
|
|
)*
|
||
|
|
|
||
|
|
std::sync::Arc::new(ac)
|
||
|
|
});
|
||
|
|
};
|
||
|
|
|
||
|
|
// Helper interne pour créer un argument d'entrée
|
||
|
|
(@arg in $name:literal, $var:expr) => {
|
||
|
|
std::sync::Arc::new(
|
||
|
|
$crate::actions::Argument::new_in(
|
||
|
|
$name.to_string(),
|
||
|
|
std::sync::Arc::clone(&$var)
|
||
|
|
)
|
||
|
|
)
|
||
|
|
};
|
||
|
|
|
||
|
|
// Helper interne pour créer un argument de sortie
|
||
|
|
(@arg out $name:literal, $var:expr) => {
|
||
|
|
std::sync::Arc::new(
|
||
|
|
$crate::actions::Argument::new_out(
|
||
|
|
$name.to_string(),
|
||
|
|
std::sync::Arc::clone(&$var)
|
||
|
|
)
|
||
|
|
)
|
||
|
|
};
|
||
|
|
}
|
||
|
|
|
||
|
|
/// Macro pour définir plusieurs actions UPnP en une seule déclaration.
|
||
|
|
///
|
||
|
|
/// Cette macro permet de regrouper la définition de plusieurs actions pour
|
||
|
|
/// améliorer la lisibilité et réduire la répétition de code.
|
||
|
|
///
|
||
|
|
/// # Syntaxe
|
||
|
|
///
|
||
|
|
/// ```ignore
|
||
|
|
/// define_actions! {
|
||
|
|
/// ACTION1 = "Action1" {
|
||
|
|
/// in "Param1" => VAR1,
|
||
|
|
/// out "Result1" => VAR2,
|
||
|
|
/// }
|
||
|
|
///
|
||
|
|
/// ACTION2 = "Action2" {
|
||
|
|
/// in "Param1" => VAR1,
|
||
|
|
/// }
|
||
|
|
///
|
||
|
|
/// ACTION3 = "Action3"
|
||
|
|
/// }
|
||
|
|
/// ```
|
||
|
|
///
|
||
|
|
/// # Arguments
|
||
|
|
///
|
||
|
|
/// Chaque action suit la même syntaxe que [`define_action!`], mais sans
|
||
|
|
/// le mot-clé `pub static`.
|
||
|
|
///
|
||
|
|
/// # Type de retour
|
||
|
|
///
|
||
|
|
/// Génère une `Lazy<Arc<Action>>` pour chaque action définie.
|
||
|
|
///
|
||
|
|
/// # Examples
|
||
|
|
///
|
||
|
|
/// ```ignore
|
||
|
|
/// use once_cell::sync::Lazy;
|
||
|
|
/// use std::sync::Arc;
|
||
|
|
///
|
||
|
|
/// // Variables d'état
|
||
|
|
/// pub static INSTANCE_ID: Lazy<Arc<StateVariable>> = Lazy::new(|| {
|
||
|
|
/// Arc::new(StateVariable::new(StateVarType::UI4, "InstanceID".to_string()))
|
||
|
|
/// });
|
||
|
|
///
|
||
|
|
/// pub static TRANSPORT_URI: Lazy<Arc<StateVariable>> = Lazy::new(|| {
|
||
|
|
/// Arc::new(StateVariable::new(StateVarType::String, "TransportURI".to_string()))
|
||
|
|
/// });
|
||
|
|
///
|
||
|
|
/// pub static URI_METADATA: Lazy<Arc<StateVariable>> = Lazy::new(|| {
|
||
|
|
/// Arc::new(StateVariable::new(StateVarType::String, "URIMetaData".to_string()))
|
||
|
|
/// });
|
||
|
|
///
|
||
|
|
/// // Définir plusieurs actions ensemble
|
||
|
|
/// define_actions! {
|
||
|
|
/// PLAY = "Play" {
|
||
|
|
/// in "InstanceID" => INSTANCE_ID,
|
||
|
|
/// }
|
||
|
|
///
|
||
|
|
/// STOP = "Stop" {
|
||
|
|
/// in "InstanceID" => INSTANCE_ID,
|
||
|
|
/// }
|
||
|
|
///
|
||
|
|
/// PAUSE = "Pause" {
|
||
|
|
/// in "InstanceID" => INSTANCE_ID,
|
||
|
|
/// }
|
||
|
|
///
|
||
|
|
/// SET_AV_TRANSPORT_URI = "SetAVTransportURI" {
|
||
|
|
/// in "InstanceID" => INSTANCE_ID,
|
||
|
|
/// in "CurrentURI" => TRANSPORT_URI,
|
||
|
|
/// in "CurrentURIMetaData" => URI_METADATA,
|
||
|
|
/// }
|
||
|
|
/// }
|
||
|
|
///
|
||
|
|
/// // Utilisation
|
||
|
|
/// fn setup_transport_service() {
|
||
|
|
/// let actions = vec![&*PLAY, &*STOP, &*PAUSE, &*SET_AV_TRANSPORT_URI];
|
||
|
|
/// for action in actions {
|
||
|
|
/// println!("Action: {}", action.get_name());
|
||
|
|
/// }
|
||
|
|
/// }
|
||
|
|
/// ```
|
||
|
|
///
|
||
|
|
/// # Avantages
|
||
|
|
///
|
||
|
|
/// - Regroupement logique des actions d'un service
|
||
|
|
/// - Réduction de la répétition de `pub static` et `define_action!`
|
||
|
|
/// - Meilleure lisibilité pour les services avec nombreuses actions
|
||
|
|
///
|
||
|
|
/// # Notes
|
||
|
|
///
|
||
|
|
/// - Toutes les actions définies sont publiques (`pub`)
|
||
|
|
/// - Chaque action est indépendante et peut être utilisée séparément
|
||
|
|
/// - La macro se développe en plusieurs appels à [`define_action!`]
|
||
|
|
#[macro_export]
|
||
|
|
macro_rules! define_actions {
|
||
|
|
// Variante avec arguments pour chaque action
|
||
|
|
(
|
||
|
|
$(
|
||
|
|
$name:ident = $action_name:literal {
|
||
|
|
$(
|
||
|
|
$direction:ident $arg_name:literal => $var_ref:expr
|
||
|
|
),* $(,)?
|
||
|
|
}
|
||
|
|
)*
|
||
|
|
) => {
|
||
|
|
$(
|
||
|
|
define_action! {
|
||
|
|
pub static $name = $action_name {
|
||
|
|
$($direction $arg_name => $var_ref),*
|
||
|
|
}
|
||
|
|
}
|
||
|
|
)*
|
||
|
|
};
|
||
|
|
|
||
|
|
// Variante mixte : actions avec et sans arguments
|
||
|
|
(
|
||
|
|
$(
|
||
|
|
$name:ident = $action_name:literal $({
|
||
|
|
$(
|
||
|
|
$direction:ident $arg_name:literal => $var_ref:expr
|
||
|
|
),* $(,)?
|
||
|
|
})?
|
||
|
|
)*
|
||
|
|
) => {
|
||
|
|
$(
|
||
|
|
$(
|
||
|
|
define_action! {
|
||
|
|
pub static $name = $action_name {
|
||
|
|
$($direction $arg_name => $var_ref),*
|
||
|
|
}
|
||
|
|
}
|
||
|
|
)?
|
||
|
|
$(
|
||
|
|
// Cas sans accolades (action sans arguments)
|
||
|
|
#[allow(unused)]
|
||
|
|
define_action! {
|
||
|
|
pub static $name = $action_name
|
||
|
|
}
|
||
|
|
)?
|
||
|
|
)*
|
||
|
|
};
|
||
|
|
}```
|
||
|
|
|
||
|
|
## fichier: `pmoupnp/src/actions/action_set_methods.rs`
|
||
|
|
|
||
|
|
```rust
|
||
|
|
use xmltree::{Element, XMLNode};
|
||
|
|
|
||
|
|
use crate::actions::ActionSet;
|
||
|
|
use crate::UpnpObject;
|
||
|
|
|
||
|
|
impl UpnpObject for ActionSet {
|
||
|
|
fn to_xml_element(&self) -> Element {
|
||
|
|
let mut elem = Element::new("actionList");
|
||
|
|
|
||
|
|
for action in self.all() {
|
||
|
|
let action_elem = action.to_xml_element(); // retourne un <action> complet
|
||
|
|
elem.children.push(XMLNode::Element(action_elem));
|
||
|
|
}
|
||
|
|
|
||
|
|
elem
|
||
|
|
}
|
||
|
|
|
||
|
|
}
|
||
|
|
|
||
|
|
```
|
||
|
|
|
||
|
|
## fichier: `pmoupnp/src/actions/action_methods.rs`
|
||
|
|
|
||
|
|
```rust
|
||
|
|
use std::sync::Arc;
|
||
|
|
|
||
|
|
use xmltree::{Element, XMLNode};
|
||
|
|
|
||
|
|
use crate::UpnpModel;
|
||
|
|
use crate::UpnpObject;
|
||
|
|
use crate::UpnpObjectSetError;
|
||
|
|
use crate::UpnpObjectType;
|
||
|
|
use crate::UpnpTyped;
|
||
|
|
use crate::actions::Action;
|
||
|
|
use crate::actions::ActionInstance;
|
||
|
|
use crate::actions::Argument;
|
||
|
|
use crate::actions::ArgumentSet;
|
||
|
|
|
||
|
|
impl UpnpObject for Action {
|
||
|
|
fn to_xml_element(&self) -> Element {
|
||
|
|
let mut action_elem = Element::new("action");
|
||
|
|
|
||
|
|
// <name>
|
||
|
|
let mut name_elem = Element::new("name");
|
||
|
|
name_elem
|
||
|
|
.children
|
||
|
|
.push(XMLNode::Text(self.get_name().clone()));
|
||
|
|
action_elem.children.push(XMLNode::Element(name_elem));
|
||
|
|
|
||
|
|
// <argumentList>
|
||
|
|
let args_elem = self.arguments.to_xml_element();
|
||
|
|
action_elem.children.push(XMLNode::Element(args_elem));
|
||
|
|
|
||
|
|
action_elem
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
impl UpnpModel for Action {
|
||
|
|
type Instance = ActionInstance;
|
||
|
|
}
|
||
|
|
|
||
|
|
impl UpnpTyped for Action {
|
||
|
|
fn as_upnp_object_type(&self) -> &UpnpObjectType {
|
||
|
|
return &self.object;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
impl Action {
|
||
|
|
pub fn new(name: String) -> Action {
|
||
|
|
Self {
|
||
|
|
object: UpnpObjectType {
|
||
|
|
name,
|
||
|
|
object_type: "Action".to_string(),
|
||
|
|
},
|
||
|
|
arguments: ArgumentSet::new(),
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
pub fn add_argument(&mut self, arg: Arc<Argument>) -> Result<(), UpnpObjectSetError> {
|
||
|
|
self.arguments.insert(arg)
|
||
|
|
}
|
||
|
|
|
||
|
|
pub fn arguments(&self) -> &ArgumentSet {
|
||
|
|
&self.arguments
|
||
|
|
}
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
## fichier: `pmoupnp/src/actions/arg_instance_methods.rs`
|
||
|
|
|
||
|
|
```rust
|
||
|
|
use std::sync::{Arc, RwLock};
|
||
|
|
|
||
|
|
use xmltree::Element;
|
||
|
|
|
||
|
|
use crate::{actions::{Argument, ArgumentInstance}, state_variables::StateVarInstance, UpnpInstance, UpnpObject, UpnpObjectType, UpnpTyped, UpnpTypedInstance};
|
||
|
|
|
||
|
|
|
||
|
|
impl UpnpObject for ArgumentInstance {
|
||
|
|
fn to_xml_element(&self) -> Element {
|
||
|
|
self.get_model().to_xml_element()
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
impl UpnpTyped for ArgumentInstance {
|
||
|
|
fn as_upnp_object_type(&self) -> &UpnpObjectType {
|
||
|
|
return &self.object;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
/// Implémentation de [`UpnpTypedInstance`] pour [`ArgumentInstance`].
|
||
|
|
///
|
||
|
|
/// Cette implémentation permet d'accéder au modèle [`Argument`] depuis l'instance
|
||
|
|
/// via la méthode [`get_model()`](UpnpTypedInstance::get_model).
|
||
|
|
///
|
||
|
|
/// # Examples
|
||
|
|
///
|
||
|
|
/// ```ignore
|
||
|
|
/// use pmoupnp::UpnpTypedInstance;
|
||
|
|
///
|
||
|
|
/// let arg_instance = ArgumentInstance::new(&arg_model);
|
||
|
|
///
|
||
|
|
/// // Accéder au modèle
|
||
|
|
/// let model = arg_instance.get_model();
|
||
|
|
/// println!("Direction: in={}, out={}", model.is_in(), model.is_out());
|
||
|
|
/// println!("Related variable: {}", model.state_variable().get_name());
|
||
|
|
/// ```
|
||
|
|
impl UpnpTypedInstance for ArgumentInstance {
|
||
|
|
/// Retourne une référence vers le modèle [`Argument`].
|
||
|
|
///
|
||
|
|
/// Permet d'accéder aux métadonnées statiques définies dans le modèle :
|
||
|
|
/// - Direction de l'argument (in/out)
|
||
|
|
/// - Variable d'état associée
|
||
|
|
/// - Nom et type
|
||
|
|
fn get_model(&self) -> &Self::Model {
|
||
|
|
&self.model
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
|
||
|
|
/// Implémentation de [`UpnpInstance`] pour [`ArgumentInstance`].
|
||
|
|
///
|
||
|
|
/// Cette implémentation fournit le constructeur standard qui crée une instance
|
||
|
|
/// **non liée** d'un argument. La liaison à une [`StateVarInstance`] doit être
|
||
|
|
/// effectuée séparément via [`bind_variable`](ArgumentInstance::bind_variable).
|
||
|
|
///
|
||
|
|
/// # Processus de construction en deux phases
|
||
|
|
///
|
||
|
|
/// ```text
|
||
|
|
/// Phase 1 (new) Phase 2 (bind_variable)
|
||
|
|
/// ┌─────────────────┐ ┌──────────────────────┐
|
||
|
|
/// │ ArgumentInstance│ │ StateVarInstance │
|
||
|
|
/// │ │ │ │
|
||
|
|
/// │ model: Arc<...> │────>│ Liaison établie │
|
||
|
|
/// │ variable: None │ │ variable: Some(...) │
|
||
|
|
/// └─────────────────┘ └──────────────────────┘
|
||
|
|
/// ↓ ↓
|
||
|
|
/// Création bind_variable(&var)
|
||
|
|
/// ```
|
||
|
|
///
|
||
|
|
/// # Pourquoi deux phases ?
|
||
|
|
///
|
||
|
|
/// 1. **Ordre de création** : Les modèles (`Argument`) existent avant les instances
|
||
|
|
/// 2. **Validation différée** : Les dépendances sont vérifiées après instanciation
|
||
|
|
/// 3. **Découplage** : Permet de créer des arguments même si les variables n'existent pas encore
|
||
|
|
///
|
||
|
|
/// # Examples
|
||
|
|
///
|
||
|
|
/// ```ignore
|
||
|
|
/// use pmoupnp::actions::{Argument, ArgumentInstance};
|
||
|
|
/// use pmoupnp::UpnpInstance;
|
||
|
|
///
|
||
|
|
/// let arg_model = Argument::new_in("InstanceID".to_string(), instance_id_var);
|
||
|
|
///
|
||
|
|
/// // Création de l'instance - Phase 1
|
||
|
|
/// let arg_instance = ArgumentInstance::new(&arg_model);
|
||
|
|
///
|
||
|
|
/// // À ce stade, l'instance existe mais n'est pas encore liée
|
||
|
|
/// assert_eq!(arg_instance.get_name(), "InstanceID");
|
||
|
|
/// assert!(arg_instance.get_variable_instance().is_none());
|
||
|
|
///
|
||
|
|
/// // La liaison se fera plus tard via bind_variable()
|
||
|
|
/// ```
|
||
|
|
impl UpnpInstance for ArgumentInstance {
|
||
|
|
type Model = Argument;
|
||
|
|
|
||
|
|
/// Crée une nouvelle instance d'argument depuis son modèle.
|
||
|
|
///
|
||
|
|
/// # Arguments
|
||
|
|
///
|
||
|
|
/// * `from` - Référence vers le modèle [`Argument`] définissant cet argument
|
||
|
|
///
|
||
|
|
/// # Returns
|
||
|
|
///
|
||
|
|
/// Une nouvelle `ArgumentInstance` avec :
|
||
|
|
/// - Nom copié depuis le modèle
|
||
|
|
/// - Référence vers le modèle (clone)
|
||
|
|
/// - `variable_instance` initialisé à `None` (liaison non établie)
|
||
|
|
///
|
||
|
|
/// # État initial
|
||
|
|
///
|
||
|
|
/// L'instance créée n'est **pas encore liée** à une variable d'état.
|
||
|
|
/// Pour établir la liaison, appelez [`bind_variable`](ArgumentInstance::bind_variable).
|
||
|
|
///
|
||
|
|
/// # Thread-safety
|
||
|
|
///
|
||
|
|
/// L'instance retournée est thread-safe et peut être partagée via `Arc`.
|
||
|
|
///
|
||
|
|
/// # Examples
|
||
|
|
///
|
||
|
|
/// ```ignore
|
||
|
|
/// use pmoupnp::UpnpInstance;
|
||
|
|
///
|
||
|
|
/// // Création depuis un modèle
|
||
|
|
/// let instance = ArgumentInstance::new(&arg_model);
|
||
|
|
///
|
||
|
|
/// // L'instance hérite des propriétés du modèle
|
||
|
|
/// assert_eq!(instance.get_name(), arg_model.get_name());
|
||
|
|
/// assert_eq!(instance.is_in(), arg_model.is_in());
|
||
|
|
///
|
||
|
|
/// // Mais n'a pas encore de valeur runtime
|
||
|
|
/// assert!(instance.get_variable_instance().is_none());
|
||
|
|
/// ```
|
||
|
|
fn new(from: &Argument) -> Self {
|
||
|
|
Self {
|
||
|
|
// Copie des métadonnées depuis le modèle
|
||
|
|
object: UpnpObjectType {
|
||
|
|
name: from.get_name().clone(),
|
||
|
|
object_type: "ArgumentInstance".to_string(),
|
||
|
|
},
|
||
|
|
|
||
|
|
// Clone du modèle pour référence future
|
||
|
|
model: from.clone(),
|
||
|
|
|
||
|
|
// Initialisation à None - sera lié plus tard via bind_variable()
|
||
|
|
// Arc<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()
|
||
|
|
}
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
## fichier: `pmoupnp/src/variable_types/values_from_i64.rs`
|
||
|
|
|
||
|
|
```rust
|
||
|
|
use crate::variable_types::{StateValue, StateValueError};
|
||
|
|
use std::convert::TryFrom;
|
||
|
|
|
||
|
|
impl TryFrom<&StateValue> for i64 {
|
||
|
|
type Error = StateValueError;
|
||
|
|
|
||
|
|
fn try_from(value: &StateValue) -> Result<Self, Self::Error> {
|
||
|
|
match value {
|
||
|
|
// signés
|
||
|
|
StateValue::I1(v) => Ok(*v as i64),
|
||
|
|
StateValue::I2(v) => Ok(*v as i64),
|
||
|
|
StateValue::I4(v) => Ok(*v as i64),
|
||
|
|
StateValue::Int(v) => Ok(*v as i64),
|
||
|
|
|
||
|
|
// non signés
|
||
|
|
StateValue::UI1(v) => Ok(*v as i64),
|
||
|
|
StateValue::UI2(v) => Ok(*v as i64),
|
||
|
|
StateValue::UI4(v) => Ok(*v as i64), // toujours dans l'intervalle d'un i64
|
||
|
|
|
||
|
|
// booléen
|
||
|
|
StateValue::Boolean(v) => Ok(*v as i64),
|
||
|
|
|
||
|
|
// chaîne → i64
|
||
|
|
StateValue::String(s) => s
|
||
|
|
.parse::<i64>()
|
||
|
|
.map_err(|_| StateValueError::TypeError(format!("Cannot parse '{}' as i64", s))),
|
||
|
|
|
||
|
|
_ => Err(StateValueError::TypeError("Cannot cast to i64".into())),
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
impl TryFrom<StateValue> for i64 {
|
||
|
|
type Error = StateValueError;
|
||
|
|
|
||
|
|
fn try_from(value: StateValue) -> Result<Self, Self::Error> {
|
||
|
|
i64::try_from(&value)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
impl From<i64> for StateValue {
|
||
|
|
fn from(value: i64) -> Self {
|
||
|
|
StateValue::Int(value as i32) // ⚠️ choix à discuter : Int est i32, pas i64
|
||
|
|
}
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
## fichier: `pmoupnp/src/variable_types/values_from_f32.rs`
|
||
|
|
|
||
|
|
```rust
|
||
|
|
use std::convert::TryFrom;
|
||
|
|
|
||
|
|
use crate::variable_types::{StateValue, StateValueError};
|
||
|
|
|
||
|
|
impl TryFrom<&StateValue> for f32 {
|
||
|
|
type Error = StateValueError;
|
||
|
|
|
||
|
|
fn try_from(value: &StateValue) -> Result<Self, Self::Error> {
|
||
|
|
const MAX_EXACT: i32 = 1 << f32::MANTISSA_DIGITS;
|
||
|
|
match value {
|
||
|
|
// --- Signed integers ---
|
||
|
|
StateValue::I1(v) => Ok(*v as f32),
|
||
|
|
StateValue::I2(v) => Ok(*v as f32),
|
||
|
|
StateValue::I4(v) if *v > -MAX_EXACT && *v < MAX_EXACT => Ok(*v as f32),
|
||
|
|
StateValue::Int(v) if *v >= -MAX_EXACT && *v <= MAX_EXACT as i32 => Ok(*v as f32),
|
||
|
|
|
||
|
|
// --- Unsigned integers ---
|
||
|
|
StateValue::UI1(v) => Ok(*v as f32),
|
||
|
|
StateValue::UI2(v) => Ok(*v as f32),
|
||
|
|
StateValue::UI4(v) if *v <= MAX_EXACT as u32 => Ok(*v as f32),
|
||
|
|
StateValue::UI4(_) => Err(StateValueError::TypeError(
|
||
|
|
"Cannot cast UI4 to f32: out of range".into(),
|
||
|
|
)),
|
||
|
|
|
||
|
|
// --- Floats ---
|
||
|
|
StateValue::R4(v) => Ok(*v), // déjà un f32
|
||
|
|
StateValue::R8(v)
|
||
|
|
if !v.is_finite() || (*v <= f32::MAX as f64 && *v >= f32::MIN as f64) =>
|
||
|
|
{
|
||
|
|
Ok(*v as f32)
|
||
|
|
}
|
||
|
|
StateValue::R8(_) => Err(StateValueError::TypeError(
|
||
|
|
"Cannot cast R8 to f32: out of range".into(),
|
||
|
|
)),
|
||
|
|
StateValue::Number(v)
|
||
|
|
if !v.is_finite() || (*v <= f32::MAX as f64 && *v >= f32::MIN as f64) =>
|
||
|
|
{
|
||
|
|
Ok(*v as f32)
|
||
|
|
}
|
||
|
|
StateValue::Number(_) => Err(StateValueError::TypeError(
|
||
|
|
"Cannot cast Number to f32: out of range".into(),
|
||
|
|
)),
|
||
|
|
StateValue::Fixed14_4(v)
|
||
|
|
if !v.is_finite() || (*v <= f32::MAX as f64 && *v >= f32::MIN as f64) =>
|
||
|
|
{
|
||
|
|
Ok(*v as f32)
|
||
|
|
}
|
||
|
|
StateValue::Fixed14_4(_) => Err(StateValueError::TypeError(
|
||
|
|
"Cannot cast Fixed14_4 to f32: out of range".into(),
|
||
|
|
)),
|
||
|
|
|
||
|
|
StateValue::Boolean(v) => Ok((*v as i32) as Self),
|
||
|
|
|
||
|
|
StateValue::String(s) => s
|
||
|
|
.parse::<f32>()
|
||
|
|
.map_err(|_| StateValueError::TypeError(format!("Cannot parse '{}' as f32", s))),
|
||
|
|
|
||
|
|
// --- Par défaut : erreur ---
|
||
|
|
_ => Err(StateValueError::TypeError("Cannot cast to f32".into())),
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
impl TryFrom<StateValue> for f32 {
|
||
|
|
type Error = StateValueError;
|
||
|
|
|
||
|
|
fn try_from(value: StateValue) -> Result<Self, Self::Error> {
|
||
|
|
f32::try_from(&value)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
impl From<f32> for StateValue {
|
||
|
|
fn from(value: f32) -> Self {
|
||
|
|
StateValue::R4(value)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
## fichier: `pmoupnp/src/variable_types/values_from_naivedate.rs`
|
||
|
|
|
||
|
|
```rust
|
||
|
|
use crate::variable_types::{StateValue, StateValueError};
|
||
|
|
use chrono::NaiveDate;
|
||
|
|
use std::convert::TryFrom;
|
||
|
|
|
||
|
|
impl TryFrom<&StateValue> for NaiveDate {
|
||
|
|
type Error = StateValueError;
|
||
|
|
|
||
|
|
fn try_from(value: &StateValue) -> Result<Self, Self::Error> {
|
||
|
|
match value {
|
||
|
|
StateValue::Date(v) => Ok(v.clone()),
|
||
|
|
StateValue::String(v) => NaiveDate::parse_from_str(v, "%Y-%m-%d").map_err(|e| {
|
||
|
|
StateValueError::ParseError(format!("Cannot parse Date from string '{}': {}", v, e))
|
||
|
|
}),
|
||
|
|
_ => Err(StateValueError::TypeError(
|
||
|
|
"Cannot cast to NaiveDate".into(),
|
||
|
|
)),
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
impl TryFrom<StateValue> for NaiveDate {
|
||
|
|
type Error = StateValueError;
|
||
|
|
|
||
|
|
fn try_from(value: StateValue) -> Result<Self, Self::Error> {
|
||
|
|
NaiveDate::try_from(&value)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
impl From<NaiveDate> for StateValue {
|
||
|
|
fn from(value: NaiveDate) -> Self {
|
||
|
|
StateValue::Date(value)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
## fichier: `pmoupnp/src/variable_types/value_methods.rs`
|
||
|
|
|
||
|
|
```rust
|
||
|
|
use std::cmp::Ordering;
|
||
|
|
|
||
|
|
use crate::variable_types::{StateValue, StateVarType, type_trait::UpnpVarType};
|
||
|
|
|
||
|
|
impl UpnpVarType for StateValue {
|
||
|
|
fn as_state_var_type(&self) -> StateVarType {
|
||
|
|
StateVarType::from(self) // utilise ton From<&StateValue> existant
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
impl PartialEq for StateValue {
|
||
|
|
fn eq(&self, other: &Self) -> bool {
|
||
|
|
match (self, other) {
|
||
|
|
(a, b) if a.is_integer() && b.is_integer() => {
|
||
|
|
if let (Ok(ia), Ok(ib)) = (i64::try_from(a), i64::try_from(b)) {
|
||
|
|
return ia == ib;
|
||
|
|
};
|
||
|
|
return false;
|
||
|
|
}
|
||
|
|
(a, b) if a.is_float() && b.is_float() => {
|
||
|
|
if let (Ok(a), Ok(b)) = (f64::try_from(self), f64::try_from(other)) {
|
||
|
|
return a == b; // NaN respecte la sémantique IEEE (NaN != NaN)
|
||
|
|
}
|
||
|
|
return false;
|
||
|
|
}
|
||
|
|
(a, b) if a.is_string() && b.is_string() => {
|
||
|
|
let (a, b) = (self.to_string(), other.to_string());
|
||
|
|
return a == b; // NaN respecte la sémantique IEEE (NaN != NaN)
|
||
|
|
}
|
||
|
|
(StateValue::Date(a), StateValue::Date(b)) => {
|
||
|
|
return a == b;
|
||
|
|
}
|
||
|
|
(StateValue::Time(a), StateValue::Time(b)) => {
|
||
|
|
return a == b;
|
||
|
|
}
|
||
|
|
(StateValue::DateTime(a), StateValue::DateTime(b)) => {
|
||
|
|
return a == b;
|
||
|
|
}
|
||
|
|
(StateValue::DateTimeTZ(a), StateValue::DateTimeTZ(b)) => {
|
||
|
|
return a == b;
|
||
|
|
}
|
||
|
|
(StateValue::TimeTZ(a), StateValue::TimeTZ(b)) => {
|
||
|
|
return a == b;
|
||
|
|
}
|
||
|
|
|
||
|
|
(_, _) => return false,
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
impl PartialOrd for StateValue {
|
||
|
|
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
|
||
|
|
match (self, other) {
|
||
|
|
(a, b) if a.is_integer() && b.is_integer() => {
|
||
|
|
if let (Ok(ia), Ok(ib)) = (i64::try_from(a), i64::try_from(b)) {
|
||
|
|
return Some(ia.cmp(&ib));
|
||
|
|
};
|
||
|
|
return None;
|
||
|
|
}
|
||
|
|
(a, b) if a.is_float() && b.is_float() => {
|
||
|
|
if let (Ok(a), Ok(b)) = (f64::try_from(self), f64::try_from(other)) {
|
||
|
|
return a.partial_cmp(&b); // NaN respecte la sémantique IEEE (NaN != NaN)
|
||
|
|
}
|
||
|
|
return None;
|
||
|
|
}
|
||
|
|
(a, b) if a.is_string() && b.is_string() => {
|
||
|
|
let (a, b) = (self.to_string(), other.to_string());
|
||
|
|
return Some(a.cmp(&b)); // NaN respecte la sémantique IEEE (NaN != NaN)
|
||
|
|
}
|
||
|
|
(StateValue::Date(a), StateValue::Date(b)) => {
|
||
|
|
return Some(a.cmp(&b));
|
||
|
|
}
|
||
|
|
(StateValue::Time(a), StateValue::Time(b)) => {
|
||
|
|
return Some(a.cmp(&b));
|
||
|
|
}
|
||
|
|
(StateValue::DateTime(a), StateValue::DateTime(b)) => {
|
||
|
|
return Some(a.cmp(&b));
|
||
|
|
}
|
||
|
|
(StateValue::DateTimeTZ(a), StateValue::DateTimeTZ(b)) => {
|
||
|
|
return Some(a.cmp(&b));
|
||
|
|
}
|
||
|
|
(StateValue::TimeTZ(a), StateValue::TimeTZ(b)) => {
|
||
|
|
return Some(a.cmp(&b));
|
||
|
|
}
|
||
|
|
(_, _) => return None,
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
## fichier: `pmoupnp/src/variable_types/values_from_type.rs`
|
||
|
|
|
||
|
|
```rust
|
||
|
|
use crate::variable_types::{StateValue, StateVarType};
|
||
|
|
|
||
|
|
impl From<&StateValue> for StateVarType {
|
||
|
|
fn from(value: &StateValue) -> Self {
|
||
|
|
match value {
|
||
|
|
StateValue::UI1(_) => StateVarType::UI1,
|
||
|
|
StateValue::UI2(_) => StateVarType::UI2,
|
||
|
|
StateValue::UI4(_) => StateVarType::UI4,
|
||
|
|
StateValue::I1(_) => StateVarType::I1,
|
||
|
|
StateValue::I2(_) => StateVarType::I2,
|
||
|
|
StateValue::I4(_) => StateVarType::I4,
|
||
|
|
StateValue::Int(_) => StateVarType::Int,
|
||
|
|
StateValue::R4(_) => StateVarType::R4,
|
||
|
|
StateValue::R8(_) => StateVarType::R8,
|
||
|
|
StateValue::Number(_) => StateVarType::Number,
|
||
|
|
StateValue::Fixed14_4(_) => StateVarType::Fixed14_4,
|
||
|
|
StateValue::Char(_) => StateVarType::Char,
|
||
|
|
StateValue::String(_) => StateVarType::String,
|
||
|
|
StateValue::BinBase64(_) => StateVarType::BinBase64,
|
||
|
|
StateValue::BinHex(_) => StateVarType::BinHex,
|
||
|
|
StateValue::URI(_) => StateVarType::URI,
|
||
|
|
StateValue::UUID(_) => StateVarType::UUID,
|
||
|
|
StateValue::Date(_) => StateVarType::Date,
|
||
|
|
StateValue::DateTime(_) => StateVarType::DateTime,
|
||
|
|
StateValue::DateTimeTZ(_) => StateVarType::DateTimeTZ,
|
||
|
|
StateValue::Time(_) => StateVarType::Time,
|
||
|
|
StateValue::TimeTZ(_) => StateVarType::TimeTZ,
|
||
|
|
StateValue::Boolean(_) => StateVarType::Boolean,
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
impl From<StateValue> for StateVarType {
|
||
|
|
fn from(value: StateValue) -> Self {
|
||
|
|
StateVarType::from(&value)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
## fichier: `pmoupnp/src/variable_types/values_from_datetime.rs`
|
||
|
|
|
||
|
|
```rust
|
||
|
|
use crate::variable_types::{StateValue, StateValueError};
|
||
|
|
use chrono::{DateTime, FixedOffset};
|
||
|
|
use std::convert::TryFrom;
|
||
|
|
|
||
|
|
impl TryFrom<&StateValue> for DateTime<FixedOffset> {
|
||
|
|
type Error = StateValueError;
|
||
|
|
|
||
|
|
fn try_from(value: &StateValue) -> Result<Self, Self::Error> {
|
||
|
|
match value {
|
||
|
|
StateValue::DateTimeTZ(v) => Ok(v.clone()),
|
||
|
|
StateValue::TimeTZ(v) => Ok(v.clone()),
|
||
|
|
StateValue::String(v) => DateTime::parse_from_rfc3339(v).map_err(|e| {
|
||
|
|
StateValueError::ParseError(format!(
|
||
|
|
"Cannot parse DateTimeTZ from string '{}': {}",
|
||
|
|
v, e
|
||
|
|
))
|
||
|
|
}),
|
||
|
|
_ => Err(StateValueError::TypeError(
|
||
|
|
"Cannot cast to DateTime<FixedOffset>".into(),
|
||
|
|
)),
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
impl TryFrom<StateValue> for DateTime<FixedOffset> {
|
||
|
|
type Error = StateValueError;
|
||
|
|
|
||
|
|
fn try_from(value: StateValue) -> Result<Self, Self::Error> {
|
||
|
|
DateTime::<FixedOffset>::try_from(&value)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
impl From<DateTime<FixedOffset>> for StateValue {
|
||
|
|
fn from(value: DateTime<FixedOffset>) -> Self {
|
||
|
|
StateValue::DateTimeTZ(value)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
## fichier: `pmoupnp/src/variable_types/values_from_u16.rs`
|
||
|
|
|
||
|
|
```rust
|
||
|
|
use std::convert::TryFrom;
|
||
|
|
|
||
|
|
use crate::variable_types::{StateValue, StateValueError};
|
||
|
|
|
||
|
|
// Implémentations TryFrom<StateValue> pour types numériques
|
||
|
|
|
||
|
|
impl TryFrom<&StateValue> for u16 {
|
||
|
|
type Error = StateValueError;
|
||
|
|
|
||
|
|
fn try_from(value: &StateValue) -> Result<Self, Self::Error> {
|
||
|
|
match value {
|
||
|
|
StateValue::UI1(v) => Ok(*v as Self),
|
||
|
|
StateValue::UI2(v) => Ok(*v),
|
||
|
|
StateValue::UI4(v) if *v <= i16::MAX as u32 => Ok(*v as Self),
|
||
|
|
StateValue::I1(v) if *v >= 0 => Ok(*v as Self),
|
||
|
|
StateValue::I2(v) if *v >= 0 => Ok(*v as Self),
|
||
|
|
StateValue::I4(v) if *v >= 0 && *v <= u16::MAX as i32 => Ok(*v as Self),
|
||
|
|
StateValue::Int(v) if *v >= 0 && *v <= u16::MAX as i32 => Ok(*v as Self),
|
||
|
|
StateValue::Boolean(v) => Ok(*v as Self),
|
||
|
|
|
||
|
|
StateValue::String(s) => s
|
||
|
|
.parse::<u16>()
|
||
|
|
.map_err(|_| StateValueError::TypeError(format!("Cannot parse '{}' as u16", s))),
|
||
|
|
|
||
|
|
_ => Err(StateValueError::TypeError("Cannot cast to u16".into())),
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
impl TryFrom<StateValue> for u16 {
|
||
|
|
type Error = StateValueError;
|
||
|
|
|
||
|
|
fn try_from(value: StateValue) -> Result<Self, Self::Error> {
|
||
|
|
u16::try_from(&value)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
impl From<u16> for StateValue {
|
||
|
|
fn from(value: u16) -> Self {
|
||
|
|
StateValue::UI2(value)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
## fichier: `pmoupnp/src/variable_types/fromstr.rs`
|
||
|
|
|
||
|
|
```rust
|
||
|
|
use crate::variable_types::StateVarType;
|
||
|
|
use std::str::FromStr;
|
||
|
|
|
||
|
|
impl FromStr for StateVarType {
|
||
|
|
type Err = String; // Type d'erreur personnalisé
|
||
|
|
|
||
|
|
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||
|
|
match s.to_lowercase().as_str() {
|
||
|
|
"ui1" => Ok(StateVarType::UI1),
|
||
|
|
"ui2" => Ok(StateVarType::UI2),
|
||
|
|
"ui4" => Ok(StateVarType::UI4),
|
||
|
|
"i1" => Ok(StateVarType::I1),
|
||
|
|
"i2" => Ok(StateVarType::I2),
|
||
|
|
"i4" => Ok(StateVarType::I4),
|
||
|
|
"int" => Ok(StateVarType::Int),
|
||
|
|
"r4" => Ok(StateVarType::R4),
|
||
|
|
"r8" => Ok(StateVarType::R8),
|
||
|
|
"number" => Ok(StateVarType::Number),
|
||
|
|
"fixed.14.4" => Ok(StateVarType::Fixed14_4),
|
||
|
|
"char" => Ok(StateVarType::Char),
|
||
|
|
"string" => Ok(StateVarType::String),
|
||
|
|
"boolean" => Ok(StateVarType::Boolean),
|
||
|
|
"bin.base64" => Ok(StateVarType::BinBase64),
|
||
|
|
"bin.hex" => Ok(StateVarType::BinHex),
|
||
|
|
"date" => Ok(StateVarType::Date),
|
||
|
|
"datetime" => Ok(StateVarType::DateTime),
|
||
|
|
"datetime.tz" => Ok(StateVarType::DateTimeTZ),
|
||
|
|
"time" => Ok(StateVarType::Time),
|
||
|
|
"time.tz" => Ok(StateVarType::TimeTZ),
|
||
|
|
"uuid" => Ok(StateVarType::UUID),
|
||
|
|
"uri" => Ok(StateVarType::URI),
|
||
|
|
_ => Err(format!("Type inconnu: {}", s)),
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
## fichier: `pmoupnp/src/variable_types/values_from_u32.rs`
|
||
|
|
|
||
|
|
```rust
|
||
|
|
use std::convert::TryFrom;
|
||
|
|
|
||
|
|
use crate::variable_types::{StateValue, StateValueError};
|
||
|
|
|
||
|
|
impl TryFrom<&StateValue> for u32 {
|
||
|
|
type Error = StateValueError;
|
||
|
|
|
||
|
|
fn try_from(value: &StateValue) -> Result<Self, Self::Error> {
|
||
|
|
match value {
|
||
|
|
StateValue::UI1(v) => Ok(*v as Self),
|
||
|
|
StateValue::UI2(v) => Ok(*v as Self),
|
||
|
|
StateValue::UI4(v) => Ok(*v),
|
||
|
|
StateValue::I1(v) if *v >= 0 => Ok(*v as Self),
|
||
|
|
StateValue::I2(v) if *v >= 0 => Ok(*v as Self),
|
||
|
|
StateValue::I4(v) if *v >= 0 => Ok(*v as Self),
|
||
|
|
StateValue::Int(v) if *v >= 0 => Ok(*v as Self),
|
||
|
|
StateValue::Boolean(v) => Ok(*v as Self),
|
||
|
|
|
||
|
|
StateValue::String(s) => s
|
||
|
|
.parse::<u32>()
|
||
|
|
.map_err(|_| StateValueError::TypeError(format!("Cannot parse '{}' as u32", s))),
|
||
|
|
|
||
|
|
_ => Err(StateValueError::TypeError("Cannot cast to u32".into())),
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
impl TryFrom<StateValue> for u32 {
|
||
|
|
type Error = StateValueError;
|
||
|
|
|
||
|
|
fn try_from(value: StateValue) -> Result<Self, Self::Error> {
|
||
|
|
u32::try_from(&value)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
impl From<u32> for StateValue {
|
||
|
|
fn from(value: u32) -> Self {
|
||
|
|
StateValue::UI4(value)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
## fichier: `pmoupnp/src/variable_types/values_from_vec_u8.rs`
|
||
|
|
|
||
|
|
```rust
|
||
|
|
use crate::variable_types::{StateValue, StateValueError};
|
||
|
|
use base64::{Engine as _, engine::general_purpose::STANDARD};
|
||
|
|
use std::convert::TryFrom;
|
||
|
|
|
||
|
|
impl TryFrom<&StateValue> for Vec<u8> {
|
||
|
|
type Error = StateValueError;
|
||
|
|
|
||
|
|
fn try_from(value: &StateValue) -> Result<Self, Self::Error> {
|
||
|
|
match value {
|
||
|
|
// Déjà un vecteur binaire
|
||
|
|
StateValue::BinBase64(v) => STANDARD
|
||
|
|
.decode(v)
|
||
|
|
.map_err(|e| StateValueError::ParseError(format!("Base64 decode error: {}", e))),
|
||
|
|
StateValue::BinHex(v) => hex::decode(v)
|
||
|
|
.map_err(|e| StateValueError::ParseError(format!("BinHex decode error: {}", e))),
|
||
|
|
|
||
|
|
// Conversion depuis une chaîne encodée
|
||
|
|
StateValue::String(s) => {
|
||
|
|
// Essayer Base64
|
||
|
|
if let Ok(bytes) = STANDARD.decode(s) {
|
||
|
|
return Ok(bytes);
|
||
|
|
}
|
||
|
|
// Essayer Hex
|
||
|
|
if let Ok(bytes) = hex::decode(s) {
|
||
|
|
return Ok(bytes);
|
||
|
|
}
|
||
|
|
Err(StateValueError::ParseError(format!(
|
||
|
|
"Cannot parse string '{}' as binary",
|
||
|
|
s
|
||
|
|
)))
|
||
|
|
}
|
||
|
|
|
||
|
|
_ => Err(StateValueError::TypeError(
|
||
|
|
"Cannot cast to binary Vec<u8>".into(),
|
||
|
|
)),
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
impl TryFrom<StateValue> for Vec<u8> {
|
||
|
|
type Error = StateValueError;
|
||
|
|
|
||
|
|
fn try_from(value: StateValue) -> Result<Self, Self::Error> {
|
||
|
|
Vec::<u8>::try_from(&value)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
## fichier: `pmoupnp/src/variable_types/default_value.rs`
|
||
|
|
|
||
|
|
```rust
|
||
|
|
use chrono::{DateTime, FixedOffset, NaiveDate, NaiveTime};
|
||
|
|
use url::Url;
|
||
|
|
use uuid::Uuid;
|
||
|
|
|
||
|
|
use crate::variable_types::{StateValue, StateVarType};
|
||
|
|
|
||
|
|
impl StateVarType {
|
||
|
|
pub fn default_value(&self) -> StateValue {
|
||
|
|
match self {
|
||
|
|
StateVarType::UI1 => StateValue::UI1(0),
|
||
|
|
StateVarType::UI2 => StateValue::UI2(0),
|
||
|
|
StateVarType::UI4 => StateValue::UI4(0),
|
||
|
|
StateVarType::I1 => StateValue::I1(0),
|
||
|
|
StateVarType::I2 => StateValue::I2(0),
|
||
|
|
StateVarType::I4 => StateValue::I4(0),
|
||
|
|
StateVarType::Int => StateValue::Int(0),
|
||
|
|
StateVarType::R4 => StateValue::R4(0.0),
|
||
|
|
StateVarType::R8 => StateValue::R8(0.0),
|
||
|
|
StateVarType::Number => StateValue::Number(0.0),
|
||
|
|
StateVarType::Fixed14_4 => StateValue::Fixed14_4(0.0),
|
||
|
|
StateVarType::Char => StateValue::Char('\0'),
|
||
|
|
StateVarType::String => StateValue::String(String::new()),
|
||
|
|
StateVarType::Boolean => StateValue::Boolean(false),
|
||
|
|
StateVarType::BinBase64 => StateValue::BinBase64(String::new()),
|
||
|
|
StateVarType::BinHex => StateValue::BinHex(String::new()),
|
||
|
|
StateVarType::Date => StateValue::Date(NaiveDate::from_ymd_opt(1970, 1, 1).unwrap()),
|
||
|
|
StateVarType::DateTime => {
|
||
|
|
StateValue::DateTime(DateTime::from_timestamp(0, 0).unwrap().naive_utc().into())
|
||
|
|
}
|
||
|
|
StateVarType::DateTimeTZ => {
|
||
|
|
StateValue::DateTimeTZ(DateTime::from_naive_utc_and_offset(
|
||
|
|
DateTime::from_timestamp(0, 0).unwrap().naive_utc(),
|
||
|
|
FixedOffset::east_opt(0).unwrap(),
|
||
|
|
))
|
||
|
|
}
|
||
|
|
StateVarType::Time => StateValue::Time(NaiveTime::from_hms_opt(0, 0, 0).unwrap()),
|
||
|
|
StateVarType::TimeTZ => StateValue::TimeTZ(DateTime::from_naive_utc_and_offset(
|
||
|
|
DateTime::from_timestamp(0, 0).unwrap().naive_utc(),
|
||
|
|
FixedOffset::east_opt(0).unwrap(),
|
||
|
|
)),
|
||
|
|
StateVarType::UUID => StateValue::UUID(Uuid::nil()),
|
||
|
|
StateVarType::URI => StateValue::URI(Url::parse("http://localhost").unwrap()),
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
## fichier: `pmoupnp/src/variable_types/values_from_str.rs`
|
||
|
|
|
||
|
|
```rust
|
||
|
|
use std::convert::TryFrom;
|
||
|
|
|
||
|
|
use crate::variable_types::{StateValue, StateValueError};
|
||
|
|
|
||
|
|
impl TryFrom<&str> for StateValue {
|
||
|
|
type Error = StateValueError;
|
||
|
|
|
||
|
|
fn try_from(s: &str) -> Result<Self, StateValueError> {
|
||
|
|
Ok(StateValue::String(s.to_string()))
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// Conversion depuis String
|
||
|
|
impl TryFrom<String> for StateValue {
|
||
|
|
type Error = StateValueError;
|
||
|
|
|
||
|
|
fn try_from(s: String) -> Result<Self, StateValueError> {
|
||
|
|
Ok(StateValue::String(s))
|
||
|
|
}
|
||
|
|
}```
|
||
|
|
|
||
|
|
## fichier: `pmoupnp/src/variable_types/display_value.rs`
|
||
|
|
|
||
|
|
```rust
|
||
|
|
use base64::Engine;
|
||
|
|
use base64::engine::general_purpose;
|
||
|
|
use std::fmt;
|
||
|
|
|
||
|
|
use crate::variable_types::StateValue;
|
||
|
|
|
||
|
|
impl fmt::Display for StateValue {
|
||
|
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||
|
|
match self {
|
||
|
|
// Numériques
|
||
|
|
StateValue::UI1(v) => write!(f, "{}", v),
|
||
|
|
StateValue::UI2(v) => write!(f, "{}", v),
|
||
|
|
StateValue::UI4(v) => write!(f, "{}", v),
|
||
|
|
StateValue::I1(v) => write!(f, "{}", v),
|
||
|
|
StateValue::I2(v) => write!(f, "{}", v),
|
||
|
|
StateValue::I4(v) => write!(f, "{}", v),
|
||
|
|
StateValue::Int(v) => write!(f, "{}", v),
|
||
|
|
StateValue::R4(v) => write!(f, "{}", v),
|
||
|
|
StateValue::R8(v) => write!(f, "{}", v),
|
||
|
|
StateValue::Number(v) => write!(f, "{}", v),
|
||
|
|
StateValue::Fixed14_4(v) => write!(f, "{}", v),
|
||
|
|
|
||
|
|
// Types déjà Display
|
||
|
|
StateValue::Char(v) => write!(f, "{}", v),
|
||
|
|
StateValue::String(v) => write!(f, "{}", v),
|
||
|
|
StateValue::UUID(v) => write!(f, "{}", v),
|
||
|
|
StateValue::URI(v) => write!(f, "{}", v),
|
||
|
|
|
||
|
|
// Booléen : 1 ou 0
|
||
|
|
StateValue::Boolean(v) => write!(f, "{}", if *v { "1" } else { "0" }),
|
||
|
|
|
||
|
|
// Encodages binaires
|
||
|
|
StateValue::BinBase64(v) => write!(f, "{}", general_purpose::URL_SAFE.encode(v)),
|
||
|
|
StateValue::BinHex(v) => write!(f, "{}", hex::encode(v)),
|
||
|
|
|
||
|
|
// Dates et temps
|
||
|
|
StateValue::Date(v) => write!(f, "{}", v.format("%Y-%m-%d")),
|
||
|
|
StateValue::DateTime(v) => write!(f, "{}", v.format("%Y-%m-%dT%H:%M:%S")),
|
||
|
|
StateValue::DateTimeTZ(v) => write!(f, "{}", v.to_rfc3339()),
|
||
|
|
StateValue::Time(v) => write!(f, "{}", v.format("%H:%M:%S")),
|
||
|
|
StateValue::TimeTZ(v) => write!(f, "{}", v.format("%H:%M:%S%z")),
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
## fichier: `pmoupnp/src/variable_types/values_from_i8.rs`
|
||
|
|
|
||
|
|
```rust
|
||
|
|
use std::convert::TryFrom;
|
||
|
|
|
||
|
|
use crate::variable_types::{StateValue, StateValueError};
|
||
|
|
|
||
|
|
// Implémentations TryFrom<StateValue> pour types numériques
|
||
|
|
|
||
|
|
impl TryFrom<&StateValue> for i8 {
|
||
|
|
type Error = StateValueError;
|
||
|
|
|
||
|
|
fn try_from(value: &StateValue) -> Result<Self, Self::Error> {
|
||
|
|
match value {
|
||
|
|
StateValue::I1(v) if *v >= 0 => Ok(*v as i8),
|
||
|
|
StateValue::I2(v) if *v <= i8::MAX as i16 && *v >= i8::MIN as i16 => Ok(*v as i8),
|
||
|
|
StateValue::I4(v) if *v <= i8::MAX as i32 && *v >= i8::MIN as i32 => Ok(*v as i8),
|
||
|
|
StateValue::Int(v) if *v <= i8::MAX as i32 && *v >= i8::MIN as i32 => Ok(*v as i8),
|
||
|
|
|
||
|
|
StateValue::UI1(v) if *v <= i8::MAX as u8 => Ok(*v as i8),
|
||
|
|
StateValue::UI2(v) if *v <= i8::MAX as u16 => Ok(*v as i8),
|
||
|
|
StateValue::UI4(v) if *v <= i8::MAX as u32 => Ok(*v as i8),
|
||
|
|
StateValue::Boolean(v) => Ok(*v as Self),
|
||
|
|
|
||
|
|
StateValue::String(s) => s
|
||
|
|
.parse::<i8>()
|
||
|
|
.map_err(|_| StateValueError::TypeError(format!("Cannot parse '{}' as i8", s))),
|
||
|
|
|
||
|
|
_ => Err(StateValueError::TypeError("Cannot cast to i8".into())),
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
impl TryFrom<StateValue> for i8 {
|
||
|
|
type Error = StateValueError;
|
||
|
|
|
||
|
|
fn try_from(value: StateValue) -> Result<Self, Self::Error> {
|
||
|
|
i8::try_from(&value)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
impl From<i8> for StateValue {
|
||
|
|
fn from(value: i8) -> Self {
|
||
|
|
StateValue::I1(value)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
## fichier: `pmoupnp/src/variable_types/cast.rs`
|
||
|
|
|
||
|
|
```rust
|
||
|
|
use crate::variable_types::{StateValue, StateValueError, StateVarType};
|
||
|
|
use std::convert::TryFrom;
|
||
|
|
|
||
|
|
impl StateValue {
|
||
|
|
pub fn try_cast(&self, target: StateVarType) -> Result<StateValue, StateValueError> {
|
||
|
|
let source = StateVarType::from(self);
|
||
|
|
|
||
|
|
// Identité (même type)
|
||
|
|
if source == target {
|
||
|
|
return Ok(self.clone());
|
||
|
|
}
|
||
|
|
|
||
|
|
match (self, target) {
|
||
|
|
(val, StateVarType::String) => Ok(StateValue::String(val.to_string())),
|
||
|
|
|
||
|
|
(_, StateVarType::UI1) => Ok(StateValue::UI1(u8::try_from(self)?)),
|
||
|
|
(_, StateVarType::UI2) => Ok(StateValue::UI2(u16::try_from(self)?)),
|
||
|
|
(_, StateVarType::UI4) => Ok(StateValue::UI4(u32::try_from(self)?)),
|
||
|
|
(_, StateVarType::I1) => Ok(StateValue::I1(i8::try_from(self)?)),
|
||
|
|
(_, StateVarType::I2) => Ok(StateValue::I2(i16::try_from(self)?)),
|
||
|
|
(_, StateVarType::I4) => Ok(StateValue::I4(i32::try_from(self)?)),
|
||
|
|
(_, StateVarType::Int) => Ok(StateValue::Int(i32::try_from(self)?)),
|
||
|
|
|
||
|
|
(_, StateVarType::R8) => Ok(StateValue::R8(f64::try_from(self)?)),
|
||
|
|
(_, StateVarType::Number) => Ok(StateValue::Number(f64::try_from(self)?)),
|
||
|
|
(_, StateVarType::Fixed14_4) => Ok(StateValue::Fixed14_4(f64::try_from(self)?)),
|
||
|
|
(_, StateVarType::R4) => Ok(StateValue::R4(f32::try_from(self)?)),
|
||
|
|
|
||
|
|
// --- Pas encore implémenté pour les autres types ---
|
||
|
|
(val, target) => Err(StateValueError::TypeError(format!(
|
||
|
|
"Cannot cast {:?} to {:?}",
|
||
|
|
val, target
|
||
|
|
))),
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
## fichier: `pmoupnp/src/variable_types/type_methods.rs`
|
||
|
|
|
||
|
|
```rust
|
||
|
|
use crate::variable_types::{StateVarType, type_trait::UpnpVarType};
|
||
|
|
|
||
|
|
impl UpnpVarType for StateVarType {
|
||
|
|
fn as_state_var_type(&self) -> StateVarType {
|
||
|
|
*self
|
||
|
|
}
|
||
|
|
|
||
|
|
fn bit_size(&self) -> Option<usize> {
|
||
|
|
match self {
|
||
|
|
StateVarType::UI1 | StateVarType::I1 => Some(8),
|
||
|
|
StateVarType::UI2 | StateVarType::I2 => Some(16),
|
||
|
|
StateVarType::UI4 | StateVarType::I4 | StateVarType::Int | StateVarType::R4 => Some(32),
|
||
|
|
StateVarType::R8 | StateVarType::Number | StateVarType::Fixed14_4 => Some(64),
|
||
|
|
_ => None,
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
fn is_numeric(&self) -> bool {
|
||
|
|
matches!(
|
||
|
|
self,
|
||
|
|
StateVarType::UI1
|
||
|
|
| StateVarType::UI2
|
||
|
|
| StateVarType::UI4
|
||
|
|
| StateVarType::I1
|
||
|
|
| StateVarType::I2
|
||
|
|
| StateVarType::I4
|
||
|
|
| StateVarType::Int
|
||
|
|
| StateVarType::R4
|
||
|
|
| StateVarType::R8
|
||
|
|
| StateVarType::Number
|
||
|
|
| StateVarType::Fixed14_4
|
||
|
|
)
|
||
|
|
}
|
||
|
|
|
||
|
|
fn is_integer(&self) -> bool {
|
||
|
|
matches!(
|
||
|
|
self,
|
||
|
|
StateVarType::UI1
|
||
|
|
| StateVarType::UI2
|
||
|
|
| StateVarType::UI4
|
||
|
|
| StateVarType::I1
|
||
|
|
| StateVarType::I2
|
||
|
|
| StateVarType::I4
|
||
|
|
| StateVarType::Int
|
||
|
|
)
|
||
|
|
}
|
||
|
|
|
||
|
|
fn is_signed_int(&self) -> bool {
|
||
|
|
matches!(
|
||
|
|
self,
|
||
|
|
StateVarType::I1 | StateVarType::I2 | StateVarType::I4 | StateVarType::Int
|
||
|
|
)
|
||
|
|
}
|
||
|
|
|
||
|
|
fn is_unsigned_int(&self) -> bool {
|
||
|
|
matches!(
|
||
|
|
self,
|
||
|
|
StateVarType::UI1 | StateVarType::UI2 | StateVarType::UI4
|
||
|
|
)
|
||
|
|
}
|
||
|
|
|
||
|
|
fn is_float(&self) -> bool {
|
||
|
|
matches!(
|
||
|
|
self,
|
||
|
|
StateVarType::R4 | StateVarType::R8 | StateVarType::Number | StateVarType::Fixed14_4
|
||
|
|
)
|
||
|
|
}
|
||
|
|
|
||
|
|
fn is_bool(&self) -> bool {
|
||
|
|
matches!(self, StateVarType::Boolean)
|
||
|
|
}
|
||
|
|
|
||
|
|
fn is_string(&self) -> bool {
|
||
|
|
matches!(
|
||
|
|
self,
|
||
|
|
StateVarType::String
|
||
|
|
| StateVarType::Char
|
||
|
|
| StateVarType::BinHex
|
||
|
|
| StateVarType::BinBase64
|
||
|
|
)
|
||
|
|
}
|
||
|
|
|
||
|
|
fn is_time(&self) -> bool {
|
||
|
|
matches!(
|
||
|
|
self,
|
||
|
|
StateVarType::Date
|
||
|
|
| StateVarType::DateTime
|
||
|
|
| StateVarType::DateTimeTZ
|
||
|
|
| StateVarType::Time
|
||
|
|
| StateVarType::TimeTZ
|
||
|
|
)
|
||
|
|
}
|
||
|
|
|
||
|
|
fn is_uuid(&self) -> bool {
|
||
|
|
matches!(self, StateVarType::UUID)
|
||
|
|
}
|
||
|
|
|
||
|
|
fn is_uri(&self) -> bool {
|
||
|
|
matches!(self, StateVarType::URI)
|
||
|
|
}
|
||
|
|
|
||
|
|
fn is_binary(&self) -> bool {
|
||
|
|
matches!(self, StateVarType::BinBase64 | StateVarType::BinHex)
|
||
|
|
}
|
||
|
|
|
||
|
|
fn is_comparable(&self) -> bool {
|
||
|
|
!self.is_binary()
|
||
|
|
}
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
## fichier: `pmoupnp/src/variable_types/mod.rs`
|
||
|
|
|
||
|
|
```rust
|
||
|
|
mod cast;
|
||
|
|
mod default_value;
|
||
|
|
mod display_type;
|
||
|
|
mod display_value;
|
||
|
|
mod errors;
|
||
|
|
mod fromstr;
|
||
|
|
mod type_methods;
|
||
|
|
mod type_trait;
|
||
|
|
mod value_methods;
|
||
|
|
mod value_trait;
|
||
|
|
|
||
|
|
mod values_from_type;
|
||
|
|
|
||
|
|
mod values_from_i16;
|
||
|
|
mod values_from_i32;
|
||
|
|
mod values_from_i64;
|
||
|
|
mod values_from_i8;
|
||
|
|
mod values_from_u16;
|
||
|
|
mod values_from_u32;
|
||
|
|
mod values_from_u8;
|
||
|
|
|
||
|
|
mod values_from_f32;
|
||
|
|
mod values_from_f64;
|
||
|
|
|
||
|
|
mod values_from_datetime;
|
||
|
|
mod values_from_naivedate;
|
||
|
|
mod values_from_naivedatetime;
|
||
|
|
mod values_from_naivetime;
|
||
|
|
mod values_from_uri;
|
||
|
|
mod values_from_uuid;
|
||
|
|
mod values_from_vec_u8;
|
||
|
|
|
||
|
|
mod values_from_str;
|
||
|
|
|
||
|
|
use std::fmt::Debug;
|
||
|
|
|
||
|
|
use chrono::{DateTime, FixedOffset, NaiveDate, NaiveDateTime, NaiveTime};
|
||
|
|
use url::Url;
|
||
|
|
use uuid::Uuid;
|
||
|
|
|
||
|
|
pub use errors::StateValueError;
|
||
|
|
pub use type_trait::UpnpVarType;
|
||
|
|
|
||
|
|
pub use crate::variable_types::value_trait::UpnpValue;
|
||
|
|
|
||
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||
|
|
pub enum StateVarType {
|
||
|
|
UI1, // Unsigned 8-bit integer
|
||
|
|
UI2, // Unsigned 16-bit integer
|
||
|
|
UI4, // Unsigned 32-bit integer
|
||
|
|
I1, // Signed 8-bit integer
|
||
|
|
I2, // Signed 16-bit integer
|
||
|
|
I4, // Signed 32-bit integer
|
||
|
|
Int, // Synonymous with i4
|
||
|
|
R4, // 32-bit floating point
|
||
|
|
R8, // 64-bit floating point
|
||
|
|
Number, // Synonymous with r8
|
||
|
|
Fixed14_4, // Fixed-point decimal
|
||
|
|
Char, // Single Unicode character
|
||
|
|
String, // Character string
|
||
|
|
Boolean, // Boolean value
|
||
|
|
BinBase64, // Base64-encoded binary
|
||
|
|
BinHex, // Hex-encoded binary
|
||
|
|
Date, // Date (YYYY-MM-DD)
|
||
|
|
DateTime, // DateTime without timezone
|
||
|
|
DateTimeTZ, // DateTime with timezone
|
||
|
|
Time, // Time without timezone
|
||
|
|
TimeTZ, // Time with timezone
|
||
|
|
UUID, // Universally unique identifier
|
||
|
|
URI, // Uniform Resource Identifier
|
||
|
|
}
|
||
|
|
|
||
|
|
#[derive(Clone, Debug)]
|
||
|
|
pub enum StateValue {
|
||
|
|
UI1(u8),
|
||
|
|
UI2(u16),
|
||
|
|
UI4(u32),
|
||
|
|
I1(i8),
|
||
|
|
I2(i16),
|
||
|
|
I4(i32),
|
||
|
|
Int(i32),
|
||
|
|
R4(f32),
|
||
|
|
R8(f64),
|
||
|
|
Number(f64),
|
||
|
|
Fixed14_4(f64),
|
||
|
|
Char(char),
|
||
|
|
String(String),
|
||
|
|
Boolean(bool),
|
||
|
|
BinBase64(String),
|
||
|
|
BinHex(String),
|
||
|
|
Date(NaiveDate),
|
||
|
|
DateTime(NaiveDateTime),
|
||
|
|
DateTimeTZ(DateTime<FixedOffset>),
|
||
|
|
Time(NaiveTime),
|
||
|
|
TimeTZ(DateTime<FixedOffset>),
|
||
|
|
UUID(Uuid),
|
||
|
|
URI(Url),
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
## fichier: `pmoupnp/src/variable_types/values_from_u8.rs`
|
||
|
|
|
||
|
|
```rust
|
||
|
|
use std::convert::TryFrom;
|
||
|
|
|
||
|
|
use crate::variable_types::{StateValue, StateValueError};
|
||
|
|
|
||
|
|
// Implémentations TryFrom<StateValue> pour types numériques
|
||
|
|
|
||
|
|
impl TryFrom<&StateValue> for u8 {
|
||
|
|
type Error = StateValueError;
|
||
|
|
|
||
|
|
fn try_from(value: &StateValue) -> Result<Self, Self::Error> {
|
||
|
|
match value {
|
||
|
|
StateValue::UI1(v) => Ok(*v),
|
||
|
|
StateValue::UI2(v) if *v <= u8::MAX as u16 => Ok(*v as u8),
|
||
|
|
StateValue::UI4(v) if *v <= i8::MAX as u32 => Ok(*v as u8),
|
||
|
|
StateValue::I1(v) if *v >= 0 => Ok(*v as u8),
|
||
|
|
StateValue::I2(v) if *v >= 0 && *v <= u8::MAX as i16 => Ok(*v as u8),
|
||
|
|
StateValue::I4(v) if *v >= 0 && *v <= u8::MAX as i32 => Ok(*v as u8),
|
||
|
|
StateValue::Int(v) if *v >= 0 && *v <= u8::MAX as i32 => Ok(*v as u8),
|
||
|
|
StateValue::Boolean(v) => Ok(*v as Self),
|
||
|
|
|
||
|
|
StateValue::String(s) => s
|
||
|
|
.parse::<u8>()
|
||
|
|
.map_err(|_| StateValueError::TypeError(format!("Cannot parse '{}' as u8", s))),
|
||
|
|
|
||
|
|
_ => Err(StateValueError::TypeError("Cannot cast to u8".into())),
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
impl TryFrom<StateValue> for u8 {
|
||
|
|
type Error = StateValueError;
|
||
|
|
|
||
|
|
fn try_from(value: StateValue) -> Result<Self, Self::Error> {
|
||
|
|
u8::try_from(&value)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
impl From<u8> for StateValue {
|
||
|
|
fn from(value: u8) -> Self {
|
||
|
|
StateValue::UI1(value)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
## fichier: `pmoupnp/src/variable_types/errors.rs`
|
||
|
|
|
||
|
|
```rust
|
||
|
|
use thiserror::Error;
|
||
|
|
|
||
|
|
#[derive(Error, Debug)]
|
||
|
|
pub enum StateValueError {
|
||
|
|
#[error("Conversion error: {0}")]
|
||
|
|
ConversionError(String),
|
||
|
|
|
||
|
|
#[error("Validation error: {0}")]
|
||
|
|
ValidationError(String),
|
||
|
|
|
||
|
|
#[error("Range error: {0}")]
|
||
|
|
RangeError(String),
|
||
|
|
|
||
|
|
#[error("Type error: {0}")]
|
||
|
|
TypeError(String),
|
||
|
|
|
||
|
|
#[error("Parse error: {0}")]
|
||
|
|
ParseError(String),
|
||
|
|
|
||
|
|
#[error("Event condition error: {0}")]
|
||
|
|
EventConditionError(String),
|
||
|
|
|
||
|
|
#[error("Arithmetic error: {0}")]
|
||
|
|
ArithmeticError(String),
|
||
|
|
|
||
|
|
#[error("Unknown error: {0}")]
|
||
|
|
Unknown(String),
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
## fichier: `pmoupnp/src/variable_types/display_type.rs`
|
||
|
|
|
||
|
|
```rust
|
||
|
|
use std::fmt;
|
||
|
|
|
||
|
|
use crate::variable_types::StateVarType;
|
||
|
|
|
||
|
|
impl fmt::Display for StateVarType {
|
||
|
|
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||
|
|
let s = match self {
|
||
|
|
StateVarType::UI1 => "ui1",
|
||
|
|
StateVarType::UI2 => "ui2",
|
||
|
|
StateVarType::UI4 => "ui4",
|
||
|
|
StateVarType::I1 => "i1",
|
||
|
|
StateVarType::I2 => "i2",
|
||
|
|
StateVarType::I4 => "i4",
|
||
|
|
StateVarType::Int => "int",
|
||
|
|
StateVarType::R4 => "r4",
|
||
|
|
StateVarType::R8 => "r8",
|
||
|
|
StateVarType::Number => "number",
|
||
|
|
StateVarType::Fixed14_4 => "fixed.14.4",
|
||
|
|
StateVarType::Char => "char",
|
||
|
|
StateVarType::String => "string",
|
||
|
|
StateVarType::Boolean => "boolean",
|
||
|
|
StateVarType::BinBase64 => "bin.base64",
|
||
|
|
StateVarType::BinHex => "bin.hex",
|
||
|
|
StateVarType::Date => "date",
|
||
|
|
StateVarType::DateTime => "dateTime",
|
||
|
|
StateVarType::DateTimeTZ => "dateTime.tz",
|
||
|
|
StateVarType::Time => "time",
|
||
|
|
StateVarType::TimeTZ => "time.tz",
|
||
|
|
StateVarType::UUID => "uuid",
|
||
|
|
StateVarType::URI => "uri",
|
||
|
|
};
|
||
|
|
write!(f, "{}", s)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
## fichier: `pmoupnp/src/variable_types/values_from_uuid.rs`
|
||
|
|
|
||
|
|
```rust
|
||
|
|
use std::convert::TryFrom;
|
||
|
|
use uuid::Uuid;
|
||
|
|
|
||
|
|
use crate::variable_types::{StateValue, StateValueError};
|
||
|
|
|
||
|
|
impl TryFrom<StateValue> for Uuid {
|
||
|
|
type Error = StateValueError;
|
||
|
|
|
||
|
|
fn try_from(value: StateValue) -> Result<Self, Self::Error> {
|
||
|
|
match value {
|
||
|
|
// Si déjà un URI encodé comme StateValue::URI
|
||
|
|
StateValue::UUID(v) => Ok(v),
|
||
|
|
|
||
|
|
// Si c'est une String, on tente un parse
|
||
|
|
StateValue::String(v) => Uuid::parse_str(&v)
|
||
|
|
.map_err(|_| StateValueError::TypeError("Invalid UUID string".into())),
|
||
|
|
|
||
|
|
// Autres types : erreur
|
||
|
|
_ => Err(StateValueError::TypeError("Cannot cast to Uuid".into())),
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
impl From<Uuid> for StateValue {
|
||
|
|
fn from(value: Uuid) -> Self {
|
||
|
|
StateValue::UUID(value)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
## fichier: `pmoupnp/src/variable_types/values_from_uri.rs`
|
||
|
|
|
||
|
|
```rust
|
||
|
|
use std::convert::TryFrom;
|
||
|
|
use url::Url;
|
||
|
|
|
||
|
|
use crate::variable_types::{StateValue, StateValueError};
|
||
|
|
|
||
|
|
impl TryFrom<StateValue> for Url {
|
||
|
|
type Error = StateValueError;
|
||
|
|
|
||
|
|
fn try_from(value: StateValue) -> Result<Self, Self::Error> {
|
||
|
|
match value {
|
||
|
|
// Si déjà un URI encodé comme StateValue::URI
|
||
|
|
StateValue::URI(v) => Ok(v),
|
||
|
|
|
||
|
|
// Si c'est une String, on tente un parse
|
||
|
|
StateValue::String(v) => {
|
||
|
|
Url::parse(&v).map_err(|_| StateValueError::TypeError("Invalid URI string".into()))
|
||
|
|
}
|
||
|
|
|
||
|
|
// Autres types : erreur
|
||
|
|
_ => Err(StateValueError::TypeError("Cannot cast to Url".into())),
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
impl From<Url> for StateValue {
|
||
|
|
fn from(value: Url) -> Self {
|
||
|
|
StateValue::URI(value)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
## fichier: `pmoupnp/src/variable_types/value_trait.rs`
|
||
|
|
|
||
|
|
```rust
|
||
|
|
pub trait UpnpValue: Clone {}
|
||
|
|
```
|
||
|
|
|
||
|
|
## fichier: `pmoupnp/src/variable_types/values_from_f64.rs`
|
||
|
|
|
||
|
|
```rust
|
||
|
|
use std::convert::TryFrom;
|
||
|
|
|
||
|
|
use crate::variable_types::{StateValue, StateValueError};
|
||
|
|
|
||
|
|
impl TryFrom<&StateValue> for f64 {
|
||
|
|
type Error = StateValueError;
|
||
|
|
|
||
|
|
fn try_from(value: &StateValue) -> Result<Self, Self::Error> {
|
||
|
|
match value {
|
||
|
|
// --- Signed integers ---
|
||
|
|
StateValue::I1(v) => Ok(*v as f64),
|
||
|
|
StateValue::I2(v) => Ok(*v as f64),
|
||
|
|
StateValue::I4(v) => Ok(*v as f64),
|
||
|
|
StateValue::Int(v) => Ok(*v as f64),
|
||
|
|
|
||
|
|
// --- Unsigned integers ---
|
||
|
|
StateValue::UI1(v) => Ok(*v as f64),
|
||
|
|
StateValue::UI2(v) => Ok(*v as f64),
|
||
|
|
StateValue::UI4(v) => Ok(*v as f64),
|
||
|
|
|
||
|
|
// --- Floats ---
|
||
|
|
StateValue::R4(v) => Ok(*v as f64),
|
||
|
|
StateValue::R8(v) => Ok(*v),
|
||
|
|
StateValue::Number(v) => Ok(*v),
|
||
|
|
StateValue::Fixed14_4(v) => Ok(*v),
|
||
|
|
|
||
|
|
StateValue::Boolean(v) => Ok((*v as i32) as Self),
|
||
|
|
|
||
|
|
StateValue::String(s) => s
|
||
|
|
.parse::<f64>()
|
||
|
|
.map_err(|_| StateValueError::TypeError(format!("Cannot parse '{}' as f64", s))),
|
||
|
|
|
||
|
|
// --- Par défaut : erreur ---
|
||
|
|
_ => Err(StateValueError::TypeError("Cannot cast to f64".into())),
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
impl TryFrom<StateValue> for f64 {
|
||
|
|
type Error = StateValueError;
|
||
|
|
|
||
|
|
fn try_from(value: StateValue) -> Result<Self, Self::Error> {
|
||
|
|
f64::try_from(&value)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
impl From<f64> for StateValue {
|
||
|
|
fn from(value: f64) -> Self {
|
||
|
|
StateValue::R8(value)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
## fichier: `pmoupnp/src/variable_types/values_from_naivedatetime.rs`
|
||
|
|
|
||
|
|
```rust
|
||
|
|
use crate::variable_types::{StateValue, StateValueError};
|
||
|
|
use chrono::NaiveDateTime;
|
||
|
|
use std::convert::TryFrom;
|
||
|
|
|
||
|
|
impl TryFrom<&StateValue> for NaiveDateTime {
|
||
|
|
type Error = StateValueError;
|
||
|
|
|
||
|
|
fn try_from(value: &StateValue) -> Result<Self, Self::Error> {
|
||
|
|
match value {
|
||
|
|
StateValue::DateTime(v) => Ok(v.clone()),
|
||
|
|
StateValue::String(v) => NaiveDateTime::parse_from_str(&v, "%Y-%m-%dT%H:%M:%S")
|
||
|
|
.map_err(|e| {
|
||
|
|
StateValueError::ParseError(format!(
|
||
|
|
"Cannot parse DateTime from string '{}': {}",
|
||
|
|
v, e
|
||
|
|
))
|
||
|
|
}),
|
||
|
|
_ => Err(StateValueError::TypeError(
|
||
|
|
"Cannot cast to NaiveDateTime".into(),
|
||
|
|
)),
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
impl TryFrom<StateValue> for NaiveDateTime {
|
||
|
|
type Error = StateValueError;
|
||
|
|
|
||
|
|
fn try_from(value: StateValue) -> Result<Self, Self::Error> {
|
||
|
|
NaiveDateTime::try_from(&value)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
impl From<NaiveDateTime> for StateValue {
|
||
|
|
fn from(value: NaiveDateTime) -> Self {
|
||
|
|
StateValue::DateTime(value)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
## fichier: `pmoupnp/src/variable_types/values_from_i32.rs`
|
||
|
|
|
||
|
|
```rust
|
||
|
|
use std::convert::TryFrom;
|
||
|
|
|
||
|
|
use crate::variable_types::{StateValue, StateValueError};
|
||
|
|
|
||
|
|
impl TryFrom<&StateValue> for i32 {
|
||
|
|
type Error = StateValueError;
|
||
|
|
|
||
|
|
fn try_from(value: &StateValue) -> Result<Self, Self::Error> {
|
||
|
|
match value {
|
||
|
|
// signés
|
||
|
|
StateValue::I1(v) => Ok(*v as i32),
|
||
|
|
StateValue::I2(v) => Ok(*v as i32),
|
||
|
|
StateValue::I4(v) => Ok(*v),
|
||
|
|
StateValue::Int(v) => Ok(*v),
|
||
|
|
|
||
|
|
// non signés
|
||
|
|
StateValue::UI1(v) => Ok(*v as i32),
|
||
|
|
StateValue::UI2(v) => Ok(*v as i32),
|
||
|
|
StateValue::UI4(v) if *v <= i32::MAX as u32 => Ok(*v as i32),
|
||
|
|
StateValue::Boolean(v) => Ok(*v as Self),
|
||
|
|
|
||
|
|
StateValue::String(s) => s
|
||
|
|
.parse::<i32>()
|
||
|
|
.map_err(|_| StateValueError::TypeError(format!("Cannot parse '{}' as i32", s))),
|
||
|
|
|
||
|
|
_ => Err(StateValueError::TypeError("Cannot cast to i32".into())),
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
impl TryFrom<StateValue> for i32 {
|
||
|
|
type Error = StateValueError;
|
||
|
|
|
||
|
|
fn try_from(value: StateValue) -> Result<Self, Self::Error> {
|
||
|
|
i32::try_from(&value)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
impl From<i32> for StateValue {
|
||
|
|
fn from(value: i32) -> Self {
|
||
|
|
StateValue::I4(value)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
## fichier: `pmoupnp/src/variable_types/values_from_naivetime.rs`
|
||
|
|
|
||
|
|
```rust
|
||
|
|
use crate::variable_types::{StateValue, StateValueError};
|
||
|
|
use chrono::NaiveTime;
|
||
|
|
use std::convert::TryFrom;
|
||
|
|
|
||
|
|
impl TryFrom<&StateValue> for NaiveTime {
|
||
|
|
type Error = StateValueError;
|
||
|
|
|
||
|
|
fn try_from(value: &StateValue) -> Result<Self, Self::Error> {
|
||
|
|
match value {
|
||
|
|
StateValue::Time(v) => Ok(v.clone()),
|
||
|
|
StateValue::String(v) => NaiveTime::parse_from_str(&v, "%H:%M:%S").map_err(|e| {
|
||
|
|
StateValueError::ParseError(format!("Cannot parse Time from string '{}': {}", v, e))
|
||
|
|
}),
|
||
|
|
_ => Err(StateValueError::TypeError(
|
||
|
|
"Cannot cast to NaiveTime".into(),
|
||
|
|
)),
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
impl TryFrom<StateValue> for NaiveTime {
|
||
|
|
type Error = StateValueError;
|
||
|
|
|
||
|
|
fn try_from(value: StateValue) -> Result<Self, Self::Error> {
|
||
|
|
NaiveTime::try_from(&value)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
impl From<NaiveTime> for StateValue {
|
||
|
|
fn from(value: NaiveTime) -> Self {
|
||
|
|
StateValue::Time(value)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
## fichier: `pmoupnp/src/variable_types/values_from_i16.rs`
|
||
|
|
|
||
|
|
```rust
|
||
|
|
use std::convert::TryFrom;
|
||
|
|
|
||
|
|
use crate::variable_types::{StateValue, StateValueError};
|
||
|
|
|
||
|
|
// Implémentations TryFrom<StateValue> pour types numériques
|
||
|
|
|
||
|
|
impl TryFrom<&StateValue> for i16 {
|
||
|
|
type Error = StateValueError;
|
||
|
|
|
||
|
|
fn try_from(value: &StateValue) -> Result<Self, Self::Error> {
|
||
|
|
match value {
|
||
|
|
StateValue::I1(v) => Ok(*v as i16),
|
||
|
|
StateValue::I2(v) => Ok(*v),
|
||
|
|
StateValue::I4(v) if *v <= i16::MAX as i32 && *v >= i16::MIN as i32 => Ok(*v as i16),
|
||
|
|
StateValue::Int(v) if *v <= i16::MAX as i32 && *v >= i16::MIN as i32 => Ok(*v as i16),
|
||
|
|
|
||
|
|
StateValue::UI1(v) => Ok(*v as i16),
|
||
|
|
StateValue::UI2(v) if *v <= i16::MAX as u16 => Ok(*v as i16),
|
||
|
|
StateValue::UI4(v) if *v <= i16::MAX as u32 => Ok(*v as i16),
|
||
|
|
StateValue::Boolean(v) => Ok(*v as Self),
|
||
|
|
|
||
|
|
StateValue::String(s) => s
|
||
|
|
.parse::<i16>()
|
||
|
|
.map_err(|_| StateValueError::TypeError(format!("Cannot parse '{}' as i16", s))),
|
||
|
|
|
||
|
|
_ => Err(StateValueError::TypeError("Cannot cast to i32".into())),
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
impl TryFrom<StateValue> for i16 {
|
||
|
|
type Error = StateValueError;
|
||
|
|
|
||
|
|
fn try_from(value: StateValue) -> Result<Self, Self::Error> {
|
||
|
|
i16::try_from(&value)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
impl From<i16> for StateValue {
|
||
|
|
fn from(value: i16) -> Self {
|
||
|
|
StateValue::I2(value)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
## fichier: `pmoupnp/src/variable_types/type_trait.rs`
|
||
|
|
|
||
|
|
```rust
|
||
|
|
use crate::variable_types::StateVarType;
|
||
|
|
|
||
|
|
pub trait UpnpVarType {
|
||
|
|
fn as_state_var_type(&self) -> StateVarType;
|
||
|
|
|
||
|
|
fn bit_size(&self) -> Option<usize> {
|
||
|
|
self.as_state_var_type().bit_size()
|
||
|
|
}
|
||
|
|
|
||
|
|
fn is_numeric(&self) -> bool {
|
||
|
|
self.as_state_var_type().is_numeric()
|
||
|
|
}
|
||
|
|
|
||
|
|
fn is_integer(&self) -> bool {
|
||
|
|
self.as_state_var_type().is_integer()
|
||
|
|
}
|
||
|
|
|
||
|
|
fn is_signed_int(&self) -> bool {
|
||
|
|
self.as_state_var_type().is_signed_int()
|
||
|
|
}
|
||
|
|
|
||
|
|
fn is_unsigned_int(&self) -> bool {
|
||
|
|
self.as_state_var_type().is_unsigned_int()
|
||
|
|
}
|
||
|
|
|
||
|
|
fn is_float(&self) -> bool {
|
||
|
|
self.as_state_var_type().is_float()
|
||
|
|
}
|
||
|
|
|
||
|
|
fn is_bool(&self) -> bool {
|
||
|
|
self.as_state_var_type().is_bool()
|
||
|
|
}
|
||
|
|
|
||
|
|
fn is_string(&self) -> bool {
|
||
|
|
self.as_state_var_type().is_string()
|
||
|
|
}
|
||
|
|
|
||
|
|
fn is_time(&self) -> bool {
|
||
|
|
self.as_state_var_type().is_time()
|
||
|
|
}
|
||
|
|
|
||
|
|
fn is_uuid(&self) -> bool {
|
||
|
|
self.as_state_var_type().is_uuid()
|
||
|
|
}
|
||
|
|
|
||
|
|
fn is_uri(&self) -> bool {
|
||
|
|
self.as_state_var_type().is_uri()
|
||
|
|
}
|
||
|
|
|
||
|
|
fn is_binary(&self) -> bool {
|
||
|
|
self.as_state_var_type().is_binary()
|
||
|
|
}
|
||
|
|
|
||
|
|
fn is_comparable(&self) -> bool {
|
||
|
|
self.as_state_var_type().is_comparable()
|
||
|
|
}
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
## fichier: `pmoupnp/src/object_trait.rs`
|
||
|
|
|
||
|
|
```rust
|
||
|
|
//! ## Hiérarchie des traits
|
||
|
|
//!
|
||
|
|
//! ```text
|
||
|
|
//! Clone + Debug
|
||
|
|
//! └─> UpnpObject (trait de base)
|
||
|
|
//! ├─> UpnpModel (modèles créant des instances)
|
||
|
|
//! ├─> UpnpInstance (instances concrètes)
|
||
|
|
//! ├─> UpnpTyped (objets avec nom et type)
|
||
|
|
//! │ └─> UpnpTypedObject = UpnpObject + UpnpTyped
|
||
|
|
//! │ └─> UpnpTypedInstance = UpnpTypedObject + UpnpInstance
|
||
|
|
//! └─> UpnpSet (collections) + UpnpDeepClone
|
||
|
|
//! ├─> UpnpModelSet = UpnpSet + UpnpModel
|
||
|
|
//! └─> UpnInstanceSet = UpnpSet + UpnpInstance
|
||
|
|
//!
|
||
|
|
//! UpnpDeepClone (indépendant)
|
||
|
|
//! ```
|
||
|
|
//!
|
||
|
|
//! ## Description des traits
|
||
|
|
//!
|
||
|
|
//! - **Traits de base** :
|
||
|
|
//! - [`UpnpObject`] : Trait principal avec sérialisation XML/Markdown
|
||
|
|
//! - [`UpnpDeepClone`] : Clonage profond (indépendant de la hiérarchie)
|
||
|
|
//!
|
||
|
|
//! - **Traits de spécialisation niveau 1** :
|
||
|
|
//! - [`UpnpModel`] : Modèle pouvant créer des instances
|
||
|
|
//! - [`UpnpInstance`] : Instance concrète créée depuis un modèle
|
||
|
|
//! - [`UpnpTyped`] : Ajoute les informations de type et nom
|
||
|
|
//! - [`UpnpSet`] : Marque un objet comme collection
|
||
|
|
//!
|
||
|
|
//! - **Traits combinés niveau 2** :
|
||
|
|
//! - [`UpnpTypedObject`] : Objet typé (marker trait)
|
||
|
|
//!
|
||
|
|
//! - **Traits combinés niveau 3** :
|
||
|
|
//! - [`UpnpTypedInstance`] : Instance typée (marker trait)
|
||
|
|
//! - [`UpnpModelSet`] : Collection de modèles (marker trait)
|
||
|
|
//! - [`UpnInstanceSet`] : Collection d'instances (marker trait)
|
||
|
|
|
||
|
|
use std::{fmt::Debug, sync::Arc};
|
||
|
|
|
||
|
|
use xmltree::{Element, EmitterConfig};
|
||
|
|
|
||
|
|
use crate::UpnpObjectType;
|
||
|
|
|
||
|
|
/// Trait pour le clonage profond d'objets UPnP.
|
||
|
|
///
|
||
|
|
/// Contrairement au trait standard [`Clone`] qui peut effectuer un clonage superficiel
|
||
|
|
/// (partage via `Arc`), ce trait garantit un clonage complet et indépendant de l'objet.
|
||
|
|
///
|
||
|
|
/// # Note
|
||
|
|
///
|
||
|
|
/// Ce trait est indépendant de la hiérarchie [`UpnpObject`] et peut être implémenté
|
||
|
|
/// séparément.
|
||
|
|
pub trait UpnpDeepClone {
|
||
|
|
/// Crée un clone profond de l'objet.
|
||
|
|
///
|
||
|
|
/// Tous les éléments internes sont clonés, créant un objet complètement indépendant.
|
||
|
|
fn deep_clone(&self) -> Self;
|
||
|
|
}
|
||
|
|
|
||
|
|
/// Trait de base pour tous les objets UPnP.
|
||
|
|
///
|
||
|
|
/// Ce trait fournit les fonctionnalités communes à tous les objets UPnP :
|
||
|
|
/// - Sérialisation XML
|
||
|
|
/// - Conversion en Markdown
|
||
|
|
/// - Identification du type d'objet (instance ou set)
|
||
|
|
///
|
||
|
|
/// # Traits requis
|
||
|
|
///
|
||
|
|
/// - [`Clone`] : Pour pouvoir dupliquer les objets
|
||
|
|
/// - [`Debug`] : Pour le débogage
|
||
|
|
///
|
||
|
|
/// # Hiérarchie
|
||
|
|
///
|
||
|
|
/// Ce trait est à la base de toute la hiérarchie UPnP. Voir la documentation du module
|
||
|
|
/// pour le graphe complet.
|
||
|
|
pub trait UpnpObject: Clone + Debug {
|
||
|
|
/// Convertit l'objet en élément XML.
|
||
|
|
///
|
||
|
|
/// # Returns
|
||
|
|
///
|
||
|
|
/// Un [`Element`] xmltree représentant l'objet.
|
||
|
|
fn to_xml_element(&self) -> Element;
|
||
|
|
|
||
|
|
/// Convertit l'objet en chaîne XML formatée.
|
||
|
|
///
|
||
|
|
/// Génère une représentation XML complète avec en-tête et indentation.
|
||
|
|
///
|
||
|
|
/// # Returns
|
||
|
|
///
|
||
|
|
/// Une chaîne XML formatée avec :
|
||
|
|
/// - En-tête `<?xml version="1.0" encoding="UTF-8"?>`
|
||
|
|
/// - Indentation de 2 espaces
|
||
|
|
///
|
||
|
|
/// # Examples
|
||
|
|
///
|
||
|
|
/// ```ignore
|
||
|
|
/// let xml = my_object.to_xml();
|
||
|
|
/// println!("{}", xml);
|
||
|
|
/// // <?xml version="1.0" encoding="UTF-8"?>
|
||
|
|
/// // <element>
|
||
|
|
/// // <child>value</child>
|
||
|
|
/// // </element>
|
||
|
|
/// ```
|
||
|
|
fn to_xml(&self) -> String {
|
||
|
|
let elem = self.to_xml_element();
|
||
|
|
|
||
|
|
let config = EmitterConfig::new()
|
||
|
|
.perform_indent(true)
|
||
|
|
.indent_string(" ");
|
||
|
|
|
||
|
|
let mut buf = Vec::new();
|
||
|
|
elem.write_with_config(&mut buf, config)
|
||
|
|
.expect("Failed to write XML");
|
||
|
|
|
||
|
|
let mut xml_string = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n".to_string();
|
||
|
|
xml_string.push_str(&String::from_utf8(buf).expect("Invalid UTF-8"));
|
||
|
|
|
||
|
|
xml_string
|
||
|
|
}
|
||
|
|
|
||
|
|
/// Convertit l'objet en représentation Markdown.
|
||
|
|
///
|
||
|
|
/// Génère une vue hiérarchique de la structure XML en format Markdown,
|
||
|
|
/// avec détection automatique des URLs et images.
|
||
|
|
///
|
||
|
|
/// # Fonctionnalités
|
||
|
|
///
|
||
|
|
/// - Les URLs sont converties en liens cliquables
|
||
|
|
/// - Les URLs d'images sont affichées comme images
|
||
|
|
/// - Les attributs sont formatés comme `key=value`
|
||
|
|
/// - Structure hiérarchique avec indentation
|
||
|
|
///
|
||
|
|
/// # Returns
|
||
|
|
///
|
||
|
|
/// Une chaîne Markdown formatée.
|
||
|
|
///
|
||
|
|
/// # Examples
|
||
|
|
///
|
||
|
|
/// ```ignore
|
||
|
|
/// let md = my_object.to_markdown();
|
||
|
|
/// println!("{}", md);
|
||
|
|
/// // # UPnP XML (Markdown view)
|
||
|
|
/// //
|
||
|
|
/// // - **element**
|
||
|
|
/// // - **child**: `value`
|
||
|
|
/// ```
|
||
|
|
fn to_markdown(&self) -> String {
|
||
|
|
let elem = self.to_xml_element();
|
||
|
|
let mut md = String::new();
|
||
|
|
|
||
|
|
fn is_url(s: &str) -> bool {
|
||
|
|
s.starts_with("http://") || s.starts_with("https://") || s.starts_with("urn:")
|
||
|
|
}
|
||
|
|
|
||
|
|
fn is_image_url(s: &str) -> bool {
|
||
|
|
let s = s.to_lowercase();
|
||
|
|
s.ends_with(".png")
|
||
|
|
|| s.ends_with(".jpg")
|
||
|
|
|| s.ends_with(".jpeg")
|
||
|
|
|| s.ends_with(".gif")
|
||
|
|
|| s.ends_with(".svg")
|
||
|
|
|| s.ends_with(".webp")
|
||
|
|
}
|
||
|
|
|
||
|
|
fn format_value(v: &str) -> String {
|
||
|
|
let v = v.trim().to_string();
|
||
|
|
if is_url(&v) {
|
||
|
|
if is_image_url(&v) {
|
||
|
|
format!("[{}]({})<br>", v, v, v)
|
||
|
|
} else {
|
||
|
|
format!("[{}]({})", v, v)
|
||
|
|
}
|
||
|
|
} else {
|
||
|
|
format!("`{}`", v)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
fn recurse(elem: &xmltree::Element, md: &mut String, depth: usize) {
|
||
|
|
let indent = " ".repeat(depth);
|
||
|
|
md.push_str(&format!("{}- **{}**", indent, elem.name));
|
||
|
|
|
||
|
|
if !elem.attributes.is_empty() {
|
||
|
|
let attrs: Vec<String> = elem
|
||
|
|
.attributes
|
||
|
|
.iter()
|
||
|
|
.map(|(k, v)| format!("{}={}", k, format_value(v)))
|
||
|
|
.collect();
|
||
|
|
md.push_str(&format!(" ({})", attrs.join(", ")));
|
||
|
|
}
|
||
|
|
|
||
|
|
if let Some(text) = elem
|
||
|
|
.get_text()
|
||
|
|
.map(|s| s.trim().to_string())
|
||
|
|
.filter(|s| !s.is_empty())
|
||
|
|
{
|
||
|
|
md.push_str(&format!(": {}", format_value(&text)));
|
||
|
|
}
|
||
|
|
|
||
|
|
md.push('\n');
|
||
|
|
|
||
|
|
for child in &elem.children {
|
||
|
|
if let xmltree::XMLNode::Element(child_elem) = child {
|
||
|
|
recurse(child_elem, md, depth + 1);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
md.push_str("# UPnP XML (Markdown view)\n\n");
|
||
|
|
recurse(&elem, &mut md, 0);
|
||
|
|
md
|
||
|
|
}
|
||
|
|
|
||
|
|
/// Indique si l'objet est une instance.
|
||
|
|
///
|
||
|
|
/// # Returns
|
||
|
|
///
|
||
|
|
/// `false` par défaut. Surchargé par [`UpnpInstance`] pour retourner `true`.
|
||
|
|
fn is_instance(&self) -> bool {
|
||
|
|
false
|
||
|
|
}
|
||
|
|
|
||
|
|
/// Indique si l'objet est une collection (set).
|
||
|
|
///
|
||
|
|
/// # Returns
|
||
|
|
///
|
||
|
|
/// `false` par défaut. Surchargé par [`UpnpSet`] pour retourner `true`.
|
||
|
|
fn is_set(&self) -> bool {
|
||
|
|
false
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
/// Trait pour les modèles UPnP qui peuvent créer des instances.
|
||
|
|
///
|
||
|
|
/// Un modèle représente la définition ou template d'un objet UPnP, tandis qu'une
|
||
|
|
/// instance est une occurrence concrète de cet objet.
|
||
|
|
///
|
||
|
|
/// # Type associé
|
||
|
|
///
|
||
|
|
/// - [`Instance`](Self::Instance) : Le type d'instance créée par ce modèle
|
||
|
|
///
|
||
|
|
/// # Méthodes
|
||
|
|
///
|
||
|
|
/// - [`create_instance`](Self::create_instance) : Crée une nouvelle instance
|
||
|
|
///
|
||
|
|
/// # Hiérarchie
|
||
|
|
///
|
||
|
|
/// ```text
|
||
|
|
/// UpnpObject
|
||
|
|
/// └─> UpnpModel
|
||
|
|
/// ```
|
||
|
|
///
|
||
|
|
/// # Relation avec UpnpInstance
|
||
|
|
///
|
||
|
|
/// `UpnpModel` et [`UpnpInstance`] sont liés via leurs types associés :
|
||
|
|
/// - Le modèle spécifie quel type d'instance il crée
|
||
|
|
/// - L'instance spécifie de quel type de modèle elle provient
|
||
|
|
///
|
||
|
|
/// # Examples
|
||
|
|
///
|
||
|
|
/// ```ignore
|
||
|
|
/// struct DeviceModel { /* ... */ }
|
||
|
|
/// struct DeviceInstance { /* ... */ }
|
||
|
|
///
|
||
|
|
/// impl UpnpModel for DeviceModel {
|
||
|
|
/// type Instance = DeviceInstance;
|
||
|
|
/// }
|
||
|
|
///
|
||
|
|
/// impl UpnpInstance for DeviceInstance {
|
||
|
|
/// type Model = DeviceModel;
|
||
|
|
///
|
||
|
|
/// fn new(model: &DeviceModel) -> Self {
|
||
|
|
/// // Création de l'instance depuis le modèle
|
||
|
|
/// }
|
||
|
|
/// }
|
||
|
|
///
|
||
|
|
/// // Utilisation
|
||
|
|
/// let model = DeviceModel::new();
|
||
|
|
/// let instance = model.create_instance(); // Arc<DeviceInstance>
|
||
|
|
/// ```
|
||
|
|
pub trait UpnpModel: UpnpObject {
|
||
|
|
/// Le type d'instance créée par ce modèle.
|
||
|
|
type Instance: UpnpInstance<Model = Self>;
|
||
|
|
|
||
|
|
/// Crée une nouvelle instance à partir de ce modèle.
|
||
|
|
///
|
||
|
|
/// # Returns
|
||
|
|
///
|
||
|
|
/// Un `Arc` contenant la nouvelle instance créée.
|
||
|
|
///
|
||
|
|
/// # Implémentation par défaut
|
||
|
|
///
|
||
|
|
/// Par défaut, appelle [`UpnpInstance::new`] avec une référence vers ce modèle
|
||
|
|
/// et encapsule le résultat dans un `Arc`.
|
||
|
|
fn create_instance(&self) -> Arc<Self::Instance> {
|
||
|
|
Arc::new(Self::Instance::new(self))
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
/// Trait pour les instances UPnP concrètes.
|
||
|
|
///
|
||
|
|
/// Une instance représente une occurrence concrète d'un objet UPnP, créée à partir
|
||
|
|
/// d'un modèle ([`UpnpModel`]).
|
||
|
|
///
|
||
|
|
/// # Type associé
|
||
|
|
///
|
||
|
|
/// - [`Model`](Self::Model) : Le type du modèle dont cette instance dérive
|
||
|
|
///
|
||
|
|
/// # Méthodes requises
|
||
|
|
///
|
||
|
|
/// - [`new`](Self::new) : Constructeur créant l'instance depuis un modèle
|
||
|
|
///
|
||
|
|
/// # Hiérarchie
|
||
|
|
///
|
||
|
|
/// ```text
|
||
|
|
/// UpnpObject
|
||
|
|
/// └─> UpnpInstance
|
||
|
|
/// ```
|
||
|
|
///
|
||
|
|
/// # Relation avec UpnpModel
|
||
|
|
///
|
||
|
|
/// Voir la documentation de [`UpnpModel`] pour comprendre la relation entre
|
||
|
|
/// modèles et instances.
|
||
|
|
pub trait UpnpInstance: UpnpObject {
|
||
|
|
/// Le type du modèle dont cette instance est dérivée.
|
||
|
|
type Model: UpnpModel<Instance = Self>;
|
||
|
|
|
||
|
|
/// Crée une nouvelle instance à partir d'un modèle.
|
||
|
|
///
|
||
|
|
/// # Arguments
|
||
|
|
///
|
||
|
|
/// * `model` - Référence vers le modèle à partir duquel créer l'instance
|
||
|
|
///
|
||
|
|
/// # Returns
|
||
|
|
///
|
||
|
|
/// Une nouvelle instance initialisée depuis le modèle.
|
||
|
|
fn new(model: &Self::Model) -> Self;
|
||
|
|
|
||
|
|
/// Indique que cet objet est une instance.
|
||
|
|
///
|
||
|
|
/// # Returns
|
||
|
|
///
|
||
|
|
/// Toujours `true` pour les instances.
|
||
|
|
fn is_instance(&self) -> bool {
|
||
|
|
true
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
/// Trait pour les objets UPnP typés.
|
||
|
|
///
|
||
|
|
/// Ajoute les informations de type et de nom aux objets UPnP.
|
||
|
|
///
|
||
|
|
/// # Méthodes requises
|
||
|
|
///
|
||
|
|
/// - [`as_upnp_object_type`](Self::as_upnp_object_type) : Accès au type de l'objet
|
||
|
|
///
|
||
|
|
/// # Méthodes fournies
|
||
|
|
///
|
||
|
|
/// - [`get_name`](Self::get_name) : Récupère le nom de l'objet
|
||
|
|
/// - [`get_object_type`](Self::get_object_type) : Récupère le type de l'objet
|
||
|
|
///
|
||
|
|
/// # Hiérarchie
|
||
|
|
///
|
||
|
|
/// ```text
|
||
|
|
/// UpnpObject
|
||
|
|
/// └─> UpnpTyped
|
||
|
|
/// ```
|
||
|
|
pub trait UpnpTyped: UpnpObject {
|
||
|
|
/// Retourne une référence vers le type de l'objet.
|
||
|
|
fn as_upnp_object_type(&self) -> &UpnpObjectType;
|
||
|
|
|
||
|
|
/// Retourne le nom de l'objet.
|
||
|
|
///
|
||
|
|
/// # Returns
|
||
|
|
///
|
||
|
|
/// Une référence vers le nom de l'objet.
|
||
|
|
fn get_name(&self) -> &String {
|
||
|
|
&self.as_upnp_object_type().name
|
||
|
|
}
|
||
|
|
|
||
|
|
/// Retourne le type de l'objet sous forme de chaîne.
|
||
|
|
///
|
||
|
|
/// # Returns
|
||
|
|
///
|
||
|
|
/// Une référence vers le type de l'objet (ex: "Device", "Service", etc.).
|
||
|
|
fn get_object_type(&self) -> &String {
|
||
|
|
&self.as_upnp_object_type().object_type
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
/// Trait marqueur pour les objets UPnP typés.
|
||
|
|
///
|
||
|
|
/// Combine [`UpnpObject`] et [`UpnpTyped`] pour créer un objet avec toutes
|
||
|
|
/// les fonctionnalités de base plus les informations de type.
|
||
|
|
///
|
||
|
|
/// # Hiérarchie
|
||
|
|
///
|
||
|
|
/// ```text
|
||
|
|
/// UpnpObject + UpnpTyped
|
||
|
|
/// └─> UpnpTypedObject
|
||
|
|
/// ```
|
||
|
|
///
|
||
|
|
/// # Note
|
||
|
|
///
|
||
|
|
/// C'est un *marker trait* (trait marqueur) sans méthodes supplémentaires.
|
||
|
|
pub trait UpnpTypedObject: UpnpObject + UpnpTyped {}
|
||
|
|
|
||
|
|
/// Trait marqueur pour les instances typées UPnP.
|
||
|
|
///
|
||
|
|
/// Combine [`UpnpTypedObject`] et [`UpnpInstance`] pour représenter une instance
|
||
|
|
/// concrète d'un objet typé avec toutes les fonctionnalités :
|
||
|
|
/// - Sérialisation XML/Markdown (de [`UpnpObject`])
|
||
|
|
/// - Informations de type et nom (de [`UpnpTyped`])
|
||
|
|
/// - Relation avec un modèle (de [`UpnpInstance`])
|
||
|
|
///
|
||
|
|
/// # Hiérarchie
|
||
|
|
///
|
||
|
|
/// ```text
|
||
|
|
/// UpnpTypedObject + UpnpInstance
|
||
|
|
/// └─> UpnpTypedInstance
|
||
|
|
/// ```
|
||
|
|
///
|
||
|
|
/// # Note
|
||
|
|
///
|
||
|
|
/// Ce trait ajoute la méthode [`get_model`](Self::get_model) pour accéder
|
||
|
|
/// au modèle de l'instance. Les collections d'instances ([`UpnInstanceSet`])
|
||
|
|
/// n'ont pas cette méthode car elles contiennent plusieurs instances.
|
||
|
|
pub trait UpnpTypedInstance: UpnpTypedObject + UpnpInstance
|
||
|
|
where
|
||
|
|
Self::Model: UpnpModel<Instance = Self>
|
||
|
|
{
|
||
|
|
/// Retourne une référence vers le modèle dont cette instance est dérivée.
|
||
|
|
///
|
||
|
|
/// Permet d'accéder aux métadonnées et contraintes définies dans le modèle,
|
||
|
|
/// telles que les plages de valeurs autorisées, les types, les descriptions, etc.
|
||
|
|
///
|
||
|
|
/// # Returns
|
||
|
|
///
|
||
|
|
/// Une référence immuable vers le modèle.
|
||
|
|
///
|
||
|
|
/// # Examples
|
||
|
|
///
|
||
|
|
/// ```ignore
|
||
|
|
/// let instance = model.create_instance();
|
||
|
|
///
|
||
|
|
/// // Accéder aux propriétés du modèle depuis l'instance
|
||
|
|
/// let model_ref = instance.get_model();
|
||
|
|
/// println!("Instance du modèle: {}", model_ref.get_name());
|
||
|
|
///
|
||
|
|
/// // Vérifier les contraintes définies dans le modèle
|
||
|
|
/// if let Some(range) = model_ref.get_range() {
|
||
|
|
/// println!("Plage autorisée: {:?}", range);
|
||
|
|
/// }
|
||
|
|
/// ```
|
||
|
|
///
|
||
|
|
/// # Use cases
|
||
|
|
///
|
||
|
|
/// Cette méthode est particulièrement utile pour :
|
||
|
|
/// - Valider des valeurs contre les contraintes du modèle
|
||
|
|
/// - Accéder aux métadonnées sans dupliquer les informations
|
||
|
|
/// - Afficher des informations de type ou de description
|
||
|
|
/// - Implémenter des logiques conditionnelles basées sur le modèle
|
||
|
|
///
|
||
|
|
/// # Différence avec les traits spécifiques
|
||
|
|
///
|
||
|
|
/// Pour les variables d'état, le trait [`UpnpVariable`](crate::state_variables::UpnpVariable)
|
||
|
|
/// fournit également `get_definition()` qui est sémantiquement équivalent
|
||
|
|
/// mais spécifique au domaine des variables.
|
||
|
|
fn get_model(&self) -> &Self::Model;
|
||
|
|
}
|
||
|
|
|
||
|
|
|
||
|
|
/// Trait marqueur pour les collections UPnP.
|
||
|
|
///
|
||
|
|
/// Représente un ensemble (set) d'objets UPnP.
|
||
|
|
///
|
||
|
|
/// # Super-traits requis
|
||
|
|
///
|
||
|
|
/// - [`UpnpObject`] : Fonctionnalités de base (XML, etc.)
|
||
|
|
/// - [`UpnpDeepClone`] : Permet le clonage profond des collections
|
||
|
|
///
|
||
|
|
/// # Implémentation
|
||
|
|
///
|
||
|
|
/// Ce trait surcharge [`UpnpObject::is_set`] pour retourner `true`.
|
||
|
|
///
|
||
|
|
/// # Hiérarchie
|
||
|
|
///
|
||
|
|
/// ```text
|
||
|
|
/// UpnpObject + UpnpDeepClone
|
||
|
|
/// └─> UpnpSet
|
||
|
|
/// ```
|
||
|
|
///
|
||
|
|
/// # Note sur le clonage
|
||
|
|
///
|
||
|
|
/// Les collections UPnP contiennent généralement des `Arc<T>` vers leurs éléments.
|
||
|
|
/// Le trait [`Clone`] (via `UpnpObject`) effectue un clonage shallow des `Arc`,
|
||
|
|
/// tandis que [`UpnpDeepClone`] clone profondément les éléments contenus.
|
||
|
|
///
|
||
|
|
/// # Examples
|
||
|
|
///
|
||
|
|
/// ```ignore
|
||
|
|
/// struct ServiceSet {
|
||
|
|
/// services: HashMap<String, Arc<Service>>,
|
||
|
|
/// }
|
||
|
|
///
|
||
|
|
/// impl Clone for ServiceSet {
|
||
|
|
/// fn clone(&self) -> Self {
|
||
|
|
/// // Clone shallow : partage les Services via Arc
|
||
|
|
/// Self {
|
||
|
|
/// services: self.services.clone()
|
||
|
|
/// }
|
||
|
|
/// }
|
||
|
|
/// }
|
||
|
|
///
|
||
|
|
/// impl UpnpDeepClone for ServiceSet {
|
||
|
|
/// fn deep_clone(&self) -> Self {
|
||
|
|
/// // Clone profond : crée de nouveaux Services
|
||
|
|
/// let deep_services = self.services
|
||
|
|
/// .iter()
|
||
|
|
/// .map(|(k, v)| (k.clone(), Arc::new((**v).clone())))
|
||
|
|
/// .collect();
|
||
|
|
///
|
||
|
|
/// Self {
|
||
|
|
/// services: deep_services
|
||
|
|
/// }
|
||
|
|
/// }
|
||
|
|
/// }
|
||
|
|
/// ```
|
||
|
|
pub trait UpnpSet: UpnpObject + UpnpDeepClone {
|
||
|
|
/// Indique que cet objet est une collection.
|
||
|
|
///
|
||
|
|
/// # Returns
|
||
|
|
///
|
||
|
|
/// Toujours `true` pour les collections.
|
||
|
|
fn is_set(&self) -> bool {
|
||
|
|
true
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
/// Trait marqueur pour les collections de modèles UPnP.
|
||
|
|
///
|
||
|
|
/// Combine [`UpnpSet`] et [`UpnpModel`] pour représenter une collection
|
||
|
|
/// de modèles qui peut elle-même créer une collection d'instances.
|
||
|
|
///
|
||
|
|
/// # Cas d'usage
|
||
|
|
///
|
||
|
|
/// Ce trait est utilisé quand une collection de modèles doit pouvoir instancier
|
||
|
|
/// une collection d'instances correspondante. Par exemple :
|
||
|
|
/// - Un ensemble de modèles de services d'un device qui crée un ensemble d'instances de services
|
||
|
|
/// - Une liste de modèles d'actions qui instancie une liste d'actions actives
|
||
|
|
/// - Une collection de modèles de variables d'état qui génère une collection d'instances
|
||
|
|
///
|
||
|
|
/// # Hiérarchie
|
||
|
|
///
|
||
|
|
/// ```text
|
||
|
|
/// UpnpSet + UpnpModel
|
||
|
|
/// └─> UpnpModelSet
|
||
|
|
/// ```
|
||
|
|
///
|
||
|
|
/// # Relation avec d'autres traits
|
||
|
|
///
|
||
|
|
/// - [`UpnpSet`] : Fournit les fonctionnalités de collection
|
||
|
|
/// - [`UpnpModel`] : Fournit la capacité de créer des instances
|
||
|
|
/// - [`UpnInstanceSet`] : Représente les collections d'instances (contrepartie)
|
||
|
|
///
|
||
|
|
/// # Note
|
||
|
|
///
|
||
|
|
/// C'est un *marker trait* (trait marqueur) sans méthodes supplémentaires.
|
||
|
|
/// Il est automatiquement implémenté pour tous les types éligibles via une
|
||
|
|
/// blanket implementation.
|
||
|
|
///
|
||
|
|
/// # Examples
|
||
|
|
///
|
||
|
|
/// ```ignore
|
||
|
|
/// /// Collection de modèles de services
|
||
|
|
/// struct ServiceSetModel {
|
||
|
|
/// services: Vec<Arc<ServiceModel>>,
|
||
|
|
/// }
|
||
|
|
///
|
||
|
|
/// /// Collection d'instances de services
|
||
|
|
/// struct ServiceSetInstance {
|
||
|
|
/// model: Arc<ServiceSetModel>,
|
||
|
|
/// service_instances: Vec<Arc<ServiceInstance>>,
|
||
|
|
/// }
|
||
|
|
///
|
||
|
|
/// impl UpnpObject for ServiceSetModel { /* ... */ }
|
||
|
|
/// impl UpnpSet for ServiceSetModel {}
|
||
|
|
///
|
||
|
|
/// impl UpnpModel for ServiceSetModel {
|
||
|
|
/// type Instance = ServiceSetInstance;
|
||
|
|
///
|
||
|
|
/// fn create_instance(&self) -> Arc<ServiceSetInstance> {
|
||
|
|
/// // Créer des instances pour chaque service
|
||
|
|
/// let instances = self.services
|
||
|
|
/// .iter()
|
||
|
|
/// .map(|model| model.create_instance())
|
||
|
|
/// .collect();
|
||
|
|
///
|
||
|
|
/// Arc::new(ServiceSetInstance {
|
||
|
|
/// model: Arc::new(self.clone()),
|
||
|
|
/// service_instances: instances,
|
||
|
|
/// })
|
||
|
|
/// }
|
||
|
|
/// }
|
||
|
|
///
|
||
|
|
/// // UpnpModelSet est automatiquement implémenté !
|
||
|
|
///
|
||
|
|
/// // Utilisation
|
||
|
|
/// let model_set = ServiceSetModel::new();
|
||
|
|
/// let instance_set = model_set.create_instance(); // Crée toutes les instances
|
||
|
|
/// ```
|
||
|
|
pub trait UpnpModelSet: UpnpSet + UpnpModel {}
|
||
|
|
|
||
|
|
|
||
|
|
/// Trait marqueur pour les collections d'instances UPnP.
|
||
|
|
///
|
||
|
|
/// Combine [`UpnpSet`] et [`UpnpInstance`] pour représenter une collection
|
||
|
|
/// d'instances UPnP. Cela permet d'avoir des collections qui sont elles-mêmes
|
||
|
|
/// des instances créées depuis un modèle.
|
||
|
|
///
|
||
|
|
/// # Hiérarchie
|
||
|
|
///
|
||
|
|
/// ```text
|
||
|
|
/// UpnpSet + UpnpInstance
|
||
|
|
/// └─> UpnInstanceSet
|
||
|
|
/// ```
|
||
|
|
///
|
||
|
|
/// # Note
|
||
|
|
///
|
||
|
|
/// C'est un *marker trait* (trait marqueur) sans méthodes supplémentaires.
|
||
|
|
pub trait UpnInstanceSet: UpnpSet + UpnpInstance {}
|
||
|
|
|
||
|
|
|
||
|
|
/// Implémentation automatique de [`UpnInstanceSet`] pour tous les types éligibles.
|
||
|
|
///
|
||
|
|
/// Cette *blanket implementation* fournit automatiquement le trait [`UpnInstanceSet`]
|
||
|
|
/// à tout type `T` qui implémente à la fois [`UpnpSet`] et [`UpnpInstance`].
|
||
|
|
///
|
||
|
|
/// # Contraintes
|
||
|
|
///
|
||
|
|
/// - `T` doit implémenter [`UpnpSet`] (collection d'objets UPnP)
|
||
|
|
/// - `T` doit implémenter [`UpnpInstance`] (instance créée depuis un modèle)
|
||
|
|
///
|
||
|
|
/// # Pourquoi cette implémentation existe
|
||
|
|
///
|
||
|
|
/// Certaines collections UPnP sont elles-mêmes des instances (par exemple, une
|
||
|
|
/// collection de services pour un device spécifique). Ce trait marker permet
|
||
|
|
/// d'identifier ces collections qui combinent les deux aspects. La blanket
|
||
|
|
/// implementation évite d'avoir à l'implémenter manuellement pour chaque type.
|
||
|
|
///
|
||
|
|
/// # Utilisation
|
||
|
|
///
|
||
|
|
/// ```ignore
|
||
|
|
/// struct ServiceSetInstance {
|
||
|
|
/// model: Arc<ServiceSetModel>,
|
||
|
|
/// services: Vec<Arc<ServiceInstance>>,
|
||
|
|
/// }
|
||
|
|
///
|
||
|
|
/// impl UpnpObject for ServiceSetInstance { /* ... */ }
|
||
|
|
/// impl UpnpSet for ServiceSetInstance {}
|
||
|
|
/// impl UpnpInstance for ServiceSetInstance {
|
||
|
|
/// type Model = ServiceSetModel;
|
||
|
|
/// fn new(model: &ServiceSetModel) -> Self { /* ... */ }
|
||
|
|
/// }
|
||
|
|
///
|
||
|
|
/// // UpnInstanceSet est automatiquement implémenté !
|
||
|
|
///
|
||
|
|
/// fn process_instance_set<T: UpnInstanceSet>(set: &T) {
|
||
|
|
/// if set.is_set() && set.is_instance() {
|
||
|
|
/// println!("C'est une collection ET une instance");
|
||
|
|
/// }
|
||
|
|
/// }
|
||
|
|
/// ```
|
||
|
|
impl<T> UpnInstanceSet for T
|
||
|
|
where
|
||
|
|
T: UpnpSet + UpnpInstance
|
||
|
|
{}
|
||
|
|
|
||
|
|
/// Implémentation automatique de [`UpnpTypedObject`] pour tous les types éligibles.
|
||
|
|
///
|
||
|
|
/// Cette *blanket implementation* fournit automatiquement le trait [`UpnpTypedObject`]
|
||
|
|
/// à tout type `T` qui implémente à la fois [`UpnpObject`] et [`UpnpTyped`].
|
||
|
|
///
|
||
|
|
/// # Contraintes
|
||
|
|
///
|
||
|
|
/// - `T` doit implémenter [`UpnpObject`] (fonctionnalités de base UPnP)
|
||
|
|
/// - `T` doit implémenter [`UpnpTyped`] (informations de type et nom)
|
||
|
|
///
|
||
|
|
/// # Utilisation
|
||
|
|
///
|
||
|
|
/// ```ignore
|
||
|
|
/// struct Device {
|
||
|
|
/// object_type: UpnpObjectType,
|
||
|
|
/// }
|
||
|
|
///
|
||
|
|
/// impl UpnpObject for Device { /* ... */ }
|
||
|
|
/// impl UpnpTyped for Device { /* ... */ }
|
||
|
|
///
|
||
|
|
/// // UpnpTypedObject est automatiquement implémenté !
|
||
|
|
/// fn process<T: UpnpTypedObject>(obj: &T) {
|
||
|
|
/// println!("{}", obj.get_name());
|
||
|
|
/// }
|
||
|
|
/// ```
|
||
|
|
impl<T> UpnpTypedObject for T
|
||
|
|
where
|
||
|
|
T: UpnpObject + UpnpTyped
|
||
|
|
{}
|
||
|
|
|
||
|
|
|
||
|
|
/// Implémentation automatique de [`UpnpModelSet`] pour tous les types éligibles.
|
||
|
|
///
|
||
|
|
/// Cette *blanket implementation* fournit automatiquement le trait [`UpnpModelSet`]
|
||
|
|
/// à tout type `T` qui implémente à la fois [`UpnpSet`] et [`UpnpModel`].
|
||
|
|
///
|
||
|
|
/// # Contraintes
|
||
|
|
///
|
||
|
|
/// - `T` doit implémenter [`UpnpSet`] (collection d'objets UPnP)
|
||
|
|
/// - `T` doit implémenter [`UpnpModel`] (peut créer des instances)
|
||
|
|
///
|
||
|
|
/// # Pourquoi cette implémentation existe
|
||
|
|
///
|
||
|
|
/// [`UpnpModelSet`] est un *marker trait* qui identifie les collections pouvant
|
||
|
|
/// créer des collections d'instances. Plutôt que de demander aux développeurs
|
||
|
|
/// d'écrire manuellement `impl UpnpModelSet for MyType {}`, cette blanket
|
||
|
|
/// implementation le fait automatiquement dès que les traits requis sont implémentés.
|
||
|
|
///
|
||
|
|
/// # Fonctionnement
|
||
|
|
///
|
||
|
|
/// Lorsque vous définissez une collection de modèles :
|
||
|
|
///
|
||
|
|
/// ```ignore
|
||
|
|
/// struct ActionSetModel {
|
||
|
|
/// actions: Vec<Arc<ActionModel>>,
|
||
|
|
/// }
|
||
|
|
///
|
||
|
|
/// impl UpnpObject for ActionSetModel { /* ... */ }
|
||
|
|
/// impl UpnpSet for ActionSetModel {}
|
||
|
|
///
|
||
|
|
/// impl UpnpModel for ActionSetModel {
|
||
|
|
/// type Instance = ActionSetInstance;
|
||
|
|
/// fn create_instance(&self) -> Arc<ActionSetInstance> { /* ... */ }
|
||
|
|
/// }
|
||
|
|
/// ```
|
||
|
|
///
|
||
|
|
/// Le compilateur Rust vérifie automatiquement que `ActionSetModel` satisfait
|
||
|
|
/// toutes les contraintes (implémente `UpnpSet` ET `UpnpModel`) et applique
|
||
|
|
/// donc `UpnpModelSet` sans code supplémentaire.
|
||
|
|
///
|
||
|
|
/// # Utilisation dans des signatures génériques
|
||
|
|
///
|
||
|
|
/// ```ignore
|
||
|
|
/// fn process_model_set<T: UpnpModelSet>(set: &T) {
|
||
|
|
/// println!("Processing model set that can create instances");
|
||
|
|
/// let instance = set.create_instance();
|
||
|
|
/// // ...
|
||
|
|
/// }
|
||
|
|
/// ```
|
||
|
|
///
|
||
|
|
/// # Différence avec UpnInstanceSet
|
||
|
|
///
|
||
|
|
/// - [`UpnpModelSet`] : Collection de **modèles** (peut créer des instances)
|
||
|
|
/// - [`UpnInstanceSet`] : Collection d'**instances** (créée depuis un modèle)
|
||
|
|
impl<T> UpnpModelSet for T
|
||
|
|
where
|
||
|
|
T: UpnpSet + UpnpModel
|
||
|
|
{}
|
||
|
|
|
||
|
|
```
|
||
|
|
|
||
|
|
## fichier: `pmoupnp/src/services/service_instance.rs`
|
||
|
|
|
||
|
|
```rust
|
||
|
|
//! Implémentation de ServiceInstance.
|
||
|
|
|
||
|
|
use std::{
|
||
|
|
collections::HashMap,
|
||
|
|
sync::{Arc, Mutex, RwLock},
|
||
|
|
time::Duration,
|
||
|
|
};
|
||
|
|
use axum::{
|
||
|
|
extract::{Request, State},
|
||
|
|
http::{HeaderMap, StatusCode},
|
||
|
|
response::{IntoResponse, Response},
|
||
|
|
body::Body,
|
||
|
|
};
|
||
|
|
use tokio::time;
|
||
|
|
use tracing::{info, warn, error};
|
||
|
|
use xmltree::{Element, XMLNode, EmitterConfig};
|
||
|
|
|
||
|
|
use crate::{
|
||
|
|
services::{Service, ServiceError},
|
||
|
|
actions::{ActionInstance, ActionInstanceSet},
|
||
|
|
state_variables::{StateVarInstance, StateVarInstanceSet, UpnpVariable},
|
||
|
|
UpnpObject, UpnpInstance, UpnpTyped, UpnpTypedInstance, UpnpObjectType,
|
||
|
|
};
|
||
|
|
|
||
|
|
/// Méthodes HTTP pour les événements UPnP.
|
||
|
|
pub const METHOD_SUBSCRIBE: &str = "SUBSCRIBE";
|
||
|
|
pub const METHOD_UNSUBSCRIBE: &str = "UNSUBSCRIBE";
|
||
|
|
|
||
|
|
/// Instance de service UPnP.
|
||
|
|
///
|
||
|
|
/// Représente une instance concrète d'un service UPnP, attachée à un device.
|
||
|
|
/// Gère l'exécution des actions, les notifications d'événements et les abonnements.
|
||
|
|
///
|
||
|
|
/// # Fonctionnalités
|
||
|
|
///
|
||
|
|
/// - Exécution d'actions via SOAP
|
||
|
|
/// - Gestion des abonnements aux événements (SUBSCRIBE/UNSUBSCRIBE)
|
||
|
|
/// - Notifications automatiques des changements d'état
|
||
|
|
/// - Génération de la description SCPD
|
||
|
|
///
|
||
|
|
/// # Cycle de vie
|
||
|
|
///
|
||
|
|
/// 1. Création via [`Service::create_instance`](crate::UpnpModel::create_instance)
|
||
|
|
/// 2. Enregistrement des URLs avec [`register_urls`](Self::register_urls)
|
||
|
|
/// 3. Démarrage du notifier avec [`start_notifier`](Self::start_notifier)
|
||
|
|
///
|
||
|
|
/// # Examples
|
||
|
|
///
|
||
|
|
/// ```rust,no_run
|
||
|
|
/// # use pmoupnp::services::Service;
|
||
|
|
/// # use pmoupnp::server::Server;
|
||
|
|
/// # use std::time::Duration;
|
||
|
|
/// # #[tokio::main]
|
||
|
|
/// # async fn main() {
|
||
|
|
/// let service = Service::new("AVTransport".to_string());
|
||
|
|
/// let instance = service.create_instance();
|
||
|
|
///
|
||
|
|
/// // Enregistrer les endpoints
|
||
|
|
/// let mut server = Server::new("test", "http://localhost:8080", 8080);
|
||
|
|
/// instance.register_urls(&mut server).await.unwrap();
|
||
|
|
///
|
||
|
|
/// // Démarrer les notifications
|
||
|
|
/// let _handle = instance.start_notifier(Duration::from_secs(5));
|
||
|
|
/// # }
|
||
|
|
/// ```
|
||
|
|
#[derive(Clone)]
|
||
|
|
pub struct ServiceInstance {
|
||
|
|
/// Métadonnées de l'objet
|
||
|
|
object: UpnpObjectType,
|
||
|
|
|
||
|
|
/// Référence vers le modèle
|
||
|
|
model: Arc<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 : Lier les arguments aux instances de variables
|
||
|
|
// Note : Nécessite que ArgumentInstance ait une méthode bind_variable()
|
||
|
|
// et que variable_instance soit dans un RwLock pour modification après création
|
||
|
|
for arg_instance in action_instance.arguments_set().all() {
|
||
|
|
let var_name = arg_instance.get_model().state_variable().get_name();
|
||
|
|
if let Some(var_instance) = statevariables.get_by_name(var_name) {
|
||
|
|
// Appeler bind_variable() si elle existe
|
||
|
|
// arg_instance.bind_variable(var_instance);
|
||
|
|
// ⚠️ TODO: Cette ligne nécessite les modifications dans ArgumentInstance
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
if let Err(e) = actions.insert(action_instance) {
|
||
|
|
error!("Failed to insert action '{}': {:?}", a.get_name(), e);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
Self {
|
||
|
|
object: UpnpObjectType {
|
||
|
|
name: model.name().to_string(),
|
||
|
|
object_type: "ServiceInstance".to_string(),
|
||
|
|
},
|
||
|
|
model: Arc::new(model.clone()),
|
||
|
|
identifier: model.identifier().to_string(),
|
||
|
|
device: None,
|
||
|
|
statevariables,
|
||
|
|
actions,
|
||
|
|
subscribers: Arc::new(RwLock::new(HashMap::new())),
|
||
|
|
changed_buffer: Arc::new(Mutex::new(HashMap::new())),
|
||
|
|
seqid: Arc::new(Mutex::new(HashMap::new())),
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
impl UpnpTypedInstance for ServiceInstance {
|
||
|
|
fn get_model(&self) -> &Self::Model {
|
||
|
|
&self.model
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
impl UpnpObject for ServiceInstance {
|
||
|
|
fn to_xml_element(&self) -> Element {
|
||
|
|
let mut elem = Element::new("service");
|
||
|
|
|
||
|
|
let mut service_type = Element::new("serviceType");
|
||
|
|
service_type.children.push(XMLNode::Text(self.service_type()));
|
||
|
|
elem.children.push(XMLNode::Element(service_type));
|
||
|
|
|
||
|
|
let mut service_id = Element::new("serviceId");
|
||
|
|
service_id.children.push(XMLNode::Text(self.service_id()));
|
||
|
|
elem.children.push(XMLNode::Element(service_id));
|
||
|
|
|
||
|
|
let mut scpd_url = Element::new("SCPDURL");
|
||
|
|
scpd_url.children.push(XMLNode::Text(self.scpd_url()));
|
||
|
|
elem.children.push(XMLNode::Element(scpd_url));
|
||
|
|
|
||
|
|
let mut control_url = Element::new("controlURL");
|
||
|
|
control_url.children.push(XMLNode::Text(self.control_url()));
|
||
|
|
elem.children.push(XMLNode::Element(control_url));
|
||
|
|
|
||
|
|
let mut event_sub_url = Element::new("eventSubURL");
|
||
|
|
event_sub_url.children.push(XMLNode::Text(self.event_sub_url()));
|
||
|
|
elem.children.push(XMLNode::Element(event_sub_url));
|
||
|
|
|
||
|
|
elem
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
impl ServiceInstance {
|
||
|
|
/// Retourne l'identifiant du service.
|
||
|
|
pub fn identifier(&self) -> &str {
|
||
|
|
&self.identifier
|
||
|
|
}
|
||
|
|
|
||
|
|
/// Retourne le type de service UPnP.
|
||
|
|
///
|
||
|
|
/// Format: `urn:schemas-upnp-org:service:{name}:{version}`
|
||
|
|
pub fn service_type(&self) -> String {
|
||
|
|
self.model.service_type()
|
||
|
|
}
|
||
|
|
|
||
|
|
/// Retourne l'ID de service UPnP.
|
||
|
|
///
|
||
|
|
/// Format: `urn:upnp-org:serviceId:{identifier}`
|
||
|
|
pub fn service_id(&self) -> String {
|
||
|
|
format!("urn:upnp-org:serviceId:{}", self.identifier)
|
||
|
|
}
|
||
|
|
|
||
|
|
/// Retourne la route de base du service.
|
||
|
|
pub fn base_route(&self) -> String {
|
||
|
|
match &self.device {
|
||
|
|
Some(device) => format!("{}/service/{}", device.base_route(), self.get_name()),
|
||
|
|
None => format!("/service/{}", self.get_name()),
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
/// Retourne l'URL de contrôle SOAP.
|
||
|
|
pub fn control_url(&self) -> String {
|
||
|
|
format!("{}/control", self.base_route())
|
||
|
|
}
|
||
|
|
|
||
|
|
/// Retourne l'URL de souscription aux événements.
|
||
|
|
pub fn event_sub_url(&self) -> String {
|
||
|
|
format!("{}/event", self.base_route())
|
||
|
|
}
|
||
|
|
|
||
|
|
/// Retourne l'URL de la description SCPD.
|
||
|
|
pub fn scpd_url(&self) -> String {
|
||
|
|
format!("{}/desc.xml", self.base_route())
|
||
|
|
}
|
||
|
|
|
||
|
|
/// Retourne l'USN (Unique Service Name).
|
||
|
|
pub fn usn(&self) -> String {
|
||
|
|
match &self.device {
|
||
|
|
Some(device) => format!("uuid:{}::urn:{}", device.udn(), self.service_type()),
|
||
|
|
None => format!("uuid::urn:{}", self.service_type()),
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
/// Retourne les variables d'état.
|
||
|
|
pub fn statevariables(&self) -> &StateVarInstanceSet {
|
||
|
|
&self.statevariables
|
||
|
|
}
|
||
|
|
|
||
|
|
/// Retourne les actions.
|
||
|
|
pub fn actions(&self) -> &ActionInstanceSet {
|
||
|
|
&self.actions
|
||
|
|
}
|
||
|
|
|
||
|
|
/// Enregistre les routes UPnP dans le serveur Axum.
|
||
|
|
///
|
||
|
|
/// # Errors
|
||
|
|
///
|
||
|
|
/// Retourne une erreur si l'enregistrement des routes échoue.
|
||
|
|
pub async fn register_urls(&self, server: &mut crate::server::Server) -> Result<(), ServiceError> {
|
||
|
|
info!(
|
||
|
|
"✅ Service description for {}:{} available at : {}{}",
|
||
|
|
self.device.as_ref().map(|d| d.name()).unwrap_or("unknown"),
|
||
|
|
self.get_name(),
|
||
|
|
self.device.as_ref().map(|d| d.server_base_url()).unwrap_or_default(),
|
||
|
|
self.scpd_url(),
|
||
|
|
);
|
||
|
|
|
||
|
|
// Handler SCPD
|
||
|
|
let instance_scpd = self.clone();
|
||
|
|
server.add_handler(&self.scpd_url(), move || {
|
||
|
|
let instance = instance_scpd.clone();
|
||
|
|
async move { instance.scpd_handler().await }
|
||
|
|
}).await;
|
||
|
|
|
||
|
|
// Handler control
|
||
|
|
let instance_control = self.clone();
|
||
|
|
server.add_post_handler_with_state(
|
||
|
|
&self.control_url(),
|
||
|
|
control_handler,
|
||
|
|
instance_control,
|
||
|
|
).await;
|
||
|
|
|
||
|
|
// Handler événements
|
||
|
|
let instance_event = self.clone();
|
||
|
|
server.add_handler_with_state(
|
||
|
|
&self.event_sub_url(),
|
||
|
|
event_sub_handler,
|
||
|
|
instance_event,
|
||
|
|
).await;
|
||
|
|
|
||
|
|
Ok(())
|
||
|
|
}
|
||
|
|
|
||
|
|
/// Génère l'élément XML SCPD.
|
||
|
|
pub fn scpd_element(&self) -> Element {
|
||
|
|
let mut elem = Element::new("scpd");
|
||
|
|
elem.attributes.insert(
|
||
|
|
"xmlns".to_string(),
|
||
|
|
"urn:schemas-upnp-org:service-1-0".to_string(),
|
||
|
|
);
|
||
|
|
|
||
|
|
// specVersion
|
||
|
|
let mut spec = Element::new("specVersion");
|
||
|
|
let mut major = Element::new("major");
|
||
|
|
major.children.push(XMLNode::Text("1".to_string()));
|
||
|
|
spec.children.push(XMLNode::Element(major));
|
||
|
|
|
||
|
|
let mut minor = Element::new("minor");
|
||
|
|
minor.children.push(XMLNode::Text("0".to_string()));
|
||
|
|
spec.children.push(XMLNode::Element(minor));
|
||
|
|
|
||
|
|
elem.children.push(XMLNode::Element(spec));
|
||
|
|
|
||
|
|
// actionList
|
||
|
|
if !self.actions.all().is_empty() {
|
||
|
|
elem.children.push(XMLNode::Element(
|
||
|
|
self.actions.to_xml_element()
|
||
|
|
));
|
||
|
|
}
|
||
|
|
|
||
|
|
// serviceStateTable
|
||
|
|
if !self.statevariables.all().is_empty() {
|
||
|
|
elem.children.push(XMLNode::Element(
|
||
|
|
self.statevariables.to_xml_element()
|
||
|
|
));
|
||
|
|
}
|
||
|
|
|
||
|
|
elem
|
||
|
|
}
|
||
|
|
|
||
|
|
/// Handler pour la description SCPD.
|
||
|
|
async fn scpd_handler(&self) -> Response {
|
||
|
|
let elem = self.scpd_element();
|
||
|
|
|
||
|
|
let config = EmitterConfig::new()
|
||
|
|
.perform_indent(true)
|
||
|
|
.indent_string(" ");
|
||
|
|
|
||
|
|
let mut xml_output = Vec::new();
|
||
|
|
if let Err(e) = elem.write_with_config(&mut xml_output, config) {
|
||
|
|
error!("Failed to serialize SCPD XML: {}", e);
|
||
|
|
return StatusCode::INTERNAL_SERVER_ERROR.into_response();
|
||
|
|
}
|
||
|
|
|
||
|
|
let mut xml = String::from_utf8_lossy(&xml_output).to_string();
|
||
|
|
|
||
|
|
// Ajouter l'en-tête XML
|
||
|
|
xml.insert_str(0, "<?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"
|
||
|
|
);
|
||
|
|
}
|
||
|
|
}```
|
||
|
|
|
||
|
|
## fichier: `pmoupnp/src/services/mod.rs`
|
||
|
|
|
||
|
|
```rust
|
||
|
|
//! # Module Services - Gestion des services UPnP
|
||
|
|
//!
|
||
|
|
//! Ce module implémente les services UPnP selon la spécification UPnP Device Architecture.
|
||
|
|
//! Un service UPnP contient des actions (méthodes appelables) et des variables d'état
|
||
|
|
//! (propriétés observables).
|
||
|
|
//!
|
||
|
|
//! ## Architecture
|
||
|
|
//!
|
||
|
|
//! - [`Service`] : Modèle définissant la structure d'un service
|
||
|
|
//! - [`ServiceInstance`] : Instance concrète d'un service attachée à un device
|
||
|
|
//!
|
||
|
|
//! ## Fonctionnalités
|
||
|
|
//!
|
||
|
|
//! - ✅ Actions UPnP avec arguments typés
|
||
|
|
//! - ✅ Variables d'état avec notifications d'événements
|
||
|
|
//! - ✅ Génération SCPD (Service Control Protocol Description)
|
||
|
|
//! - ✅ Endpoints SOAP pour le contrôle
|
||
|
|
//! - ✅ Gestion des abonnements aux événements (SUBSCRIBE/UNSUBSCRIBE)
|
||
|
|
//! - ✅ Notifications automatiques des changements d'état
|
||
|
|
//!
|
||
|
|
//! ## Examples
|
||
|
|
//!
|
||
|
|
//! ```rust
|
||
|
|
//! use pmoupnp::services::Service;
|
||
|
|
//! use pmoupnp::state_variables::StateVariable;
|
||
|
|
//! use pmoupnp::variable_types::StateVarType;
|
||
|
|
//! use std::sync::Arc;
|
||
|
|
//!
|
||
|
|
//! // Créer un service
|
||
|
|
//! let mut service = Service::new("AVTransport".to_string());
|
||
|
|
//! service.set_version(1).unwrap();
|
||
|
|
//!
|
||
|
|
//! // Ajouter une variable d'état
|
||
|
|
//! let transport_state = Arc::new(
|
||
|
|
//! StateVariable::new(StateVarType::String, "TransportState".to_string())
|
||
|
|
//! );
|
||
|
|
//! service.add_variable(transport_state);
|
||
|
|
//!
|
||
|
|
//! // Créer une instance
|
||
|
|
//! let instance = service.create_instance();
|
||
|
|
//! ```
|
||
|
|
|
||
|
|
mod errors;
|
||
|
|
mod service_methods;
|
||
|
|
mod service_instance;
|
||
|
|
|
||
|
|
use std::sync::Arc;
|
||
|
|
|
||
|
|
pub use errors::ServiceError;
|
||
|
|
pub use service_instance::ServiceInstance;
|
||
|
|
|
||
|
|
use crate::{
|
||
|
|
actions::ActionSet,
|
||
|
|
state_variables::StateVariableSet,
|
||
|
|
UpnpObjectType,
|
||
|
|
};
|
||
|
|
|
||
|
|
/// Service UPnP (modèle).
|
||
|
|
///
|
||
|
|
/// Représente la définition d'un service UPnP avec ses actions et variables d'état.
|
||
|
|
/// Un service est attaché à un device et expose des fonctionnalités via SOAP.
|
||
|
|
///
|
||
|
|
/// # Structure
|
||
|
|
///
|
||
|
|
/// Un service UPnP contient :
|
||
|
|
/// - Un identifiant unique (`identifier`)
|
||
|
|
/// - Une version (ex: 1, 2, 3...)
|
||
|
|
/// - Un ensemble d'actions ([`ActionSet`])
|
||
|
|
/// - Une table de variables d'état ([`StateVariableSet`])
|
||
|
|
///
|
||
|
|
/// # Cycle de vie
|
||
|
|
///
|
||
|
|
/// 1. Création avec [`Service::new`]
|
||
|
|
/// 2. Configuration (ajout d'actions et variables)
|
||
|
|
/// 3. Instanciation avec [`create_instance`](crate::UpnpModel::create_instance)
|
||
|
|
///
|
||
|
|
/// # Examples
|
||
|
|
///
|
||
|
|
/// ```rust
|
||
|
|
/// # use pmoupnp::services::Service;
|
||
|
|
/// # use pmoupnp::state_variables::StateVariable;
|
||
|
|
/// # use pmoupnp::variable_types::StateVarType;
|
||
|
|
/// # use std::sync::Arc;
|
||
|
|
/// let mut service = Service::new("ContentDirectory".to_string());
|
||
|
|
/// service.set_identifier("urn:upnp-org:serviceId:ContentDirectory".to_string());
|
||
|
|
/// service.set_version(1).unwrap();
|
||
|
|
///
|
||
|
|
/// // Ajouter une variable d'état
|
||
|
|
/// let search_caps = Arc::new(
|
||
|
|
/// StateVariable::new(StateVarType::String, "SearchCapabilities".to_string())
|
||
|
|
/// );
|
||
|
|
/// service.add_variable(search_caps);
|
||
|
|
/// ```
|
||
|
|
#[derive(Debug, Clone)]
|
||
|
|
pub struct Service {
|
||
|
|
/// Métadonnées de l'objet UPnP
|
||
|
|
object: UpnpObjectType,
|
||
|
|
|
||
|
|
/// Identifiant du service (ex: "urn:upnp-org:serviceId:AVTransport")
|
||
|
|
identifier: String,
|
||
|
|
|
||
|
|
/// Version du service (>= 1)
|
||
|
|
version: u32,
|
||
|
|
|
||
|
|
/// Actions disponibles dans ce service
|
||
|
|
actions: ActionSet,
|
||
|
|
|
||
|
|
/// Variables d'état du service
|
||
|
|
state_table: StateVariableSet,
|
||
|
|
}
|
||
|
|
|
||
|
|
impl Service {
|
||
|
|
/// Crée un nouveau service UPnP.
|
||
|
|
///
|
||
|
|
/// # Arguments
|
||
|
|
///
|
||
|
|
/// * `name` - Nom du service (ex: "AVTransport", "RenderingControl")
|
||
|
|
///
|
||
|
|
/// # Returns
|
||
|
|
///
|
||
|
|
/// Un nouveau service avec :
|
||
|
|
/// - Identifiant initialisé au nom
|
||
|
|
/// - Version 1 par défaut
|
||
|
|
/// - Collections vides d'actions et de variables
|
||
|
|
///
|
||
|
|
/// # Examples
|
||
|
|
///
|
||
|
|
/// ```rust
|
||
|
|
/// # use pmoupnp::services::Service;
|
||
|
|
/// let service = Service::new("AVTransport".to_string());
|
||
|
|
/// assert_eq!(service.name(), "AVTransport");
|
||
|
|
/// assert_eq!(service.version(), 1);
|
||
|
|
/// ```
|
||
|
|
pub fn new(name: String) -> Self {
|
||
|
|
Self {
|
||
|
|
object: UpnpObjectType {
|
||
|
|
name: name.clone(),
|
||
|
|
object_type: "Service".to_string(),
|
||
|
|
},
|
||
|
|
identifier: name,
|
||
|
|
version: 1,
|
||
|
|
state_table: StateVariableSet::new(),
|
||
|
|
actions: ActionSet::new(),
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
/// Retourne le nom du service.
|
||
|
|
///
|
||
|
|
/// # Examples
|
||
|
|
///
|
||
|
|
/// ```rust
|
||
|
|
/// # use pmoupnp::services::Service;
|
||
|
|
/// let service = Service::new("AVTransport".to_string());
|
||
|
|
/// assert_eq!(service.name(), "AVTransport");
|
||
|
|
/// ```
|
||
|
|
pub fn name(&self) -> &str {
|
||
|
|
&self.object.name
|
||
|
|
}
|
||
|
|
|
||
|
|
/// Retourne le type d'objet.
|
||
|
|
///
|
||
|
|
/// # Examples
|
||
|
|
///
|
||
|
|
/// ```rust
|
||
|
|
/// # use pmoupnp::services::Service;
|
||
|
|
/// let service = Service::new("AVTransport".to_string());
|
||
|
|
/// assert_eq!(service.type_id(), "Service");
|
||
|
|
/// ```
|
||
|
|
pub fn type_id(&self) -> &str {
|
||
|
|
&self.object.object_type
|
||
|
|
}
|
||
|
|
|
||
|
|
/// Retourne l'identifiant du service.
|
||
|
|
///
|
||
|
|
/// # Examples
|
||
|
|
///
|
||
|
|
/// ```rust
|
||
|
|
/// # use pmoupnp::services::Service;
|
||
|
|
/// let mut service = Service::new("AVTransport".to_string());
|
||
|
|
/// service.set_identifier("urn:upnp-org:serviceId:AVTransport".to_string());
|
||
|
|
/// assert_eq!(service.identifier(), "urn:upnp-org:serviceId:AVTransport");
|
||
|
|
/// ```
|
||
|
|
pub fn identifier(&self) -> &str {
|
||
|
|
&self.identifier
|
||
|
|
}
|
||
|
|
|
||
|
|
/// Définit l'identifiant du service.
|
||
|
|
///
|
||
|
|
/// # Arguments
|
||
|
|
///
|
||
|
|
/// * `id` - Nouvel identifiant (typiquement un URN UPnP)
|
||
|
|
///
|
||
|
|
/// # Examples
|
||
|
|
///
|
||
|
|
/// ```rust
|
||
|
|
/// # use pmoupnp::services::Service;
|
||
|
|
/// let mut service = Service::new("AVTransport".to_string());
|
||
|
|
/// service.set_identifier("urn:upnp-org:serviceId:AVTransport".to_string());
|
||
|
|
/// ```
|
||
|
|
pub fn set_identifier(&mut self, id: String) {
|
||
|
|
self.identifier = id;
|
||
|
|
}
|
||
|
|
|
||
|
|
/// Retourne la version du service.
|
||
|
|
///
|
||
|
|
/// # Examples
|
||
|
|
///
|
||
|
|
/// ```rust
|
||
|
|
/// # use pmoupnp::services::Service;
|
||
|
|
/// let service = Service::new("AVTransport".to_string());
|
||
|
|
/// assert_eq!(service.version(), 1);
|
||
|
|
/// ```
|
||
|
|
pub fn version(&self) -> u32 {
|
||
|
|
self.version
|
||
|
|
}
|
||
|
|
|
||
|
|
/// Définit la version du service.
|
||
|
|
///
|
||
|
|
/// # Arguments
|
||
|
|
///
|
||
|
|
/// * `version` - Numéro de version (doit être >= 1)
|
||
|
|
///
|
||
|
|
/// # Errors
|
||
|
|
///
|
||
|
|
/// Retourne une erreur si la version est < 1.
|
||
|
|
///
|
||
|
|
/// # Examples
|
||
|
|
///
|
||
|
|
/// ```rust
|
||
|
|
/// # use pmoupnp::services::Service;
|
||
|
|
/// let mut service = Service::new("AVTransport".to_string());
|
||
|
|
/// assert!(service.set_version(2).is_ok());
|
||
|
|
/// assert!(service.set_version(0).is_err());
|
||
|
|
/// ```
|
||
|
|
pub fn set_version(&mut self, version: u32) -> Result<(), ServiceError> {
|
||
|
|
if version < 1 {
|
||
|
|
return Err(ServiceError::ValidationError(
|
||
|
|
"Version must be >= 1".to_string()
|
||
|
|
));
|
||
|
|
}
|
||
|
|
self.version = version;
|
||
|
|
Ok(())
|
||
|
|
}
|
||
|
|
|
||
|
|
/// Ajoute une variable d'état au service.
|
||
|
|
///
|
||
|
|
/// # Arguments
|
||
|
|
///
|
||
|
|
/// * `sv` - Variable d'état à ajouter
|
||
|
|
///
|
||
|
|
/// # Errors
|
||
|
|
///
|
||
|
|
/// Retourne une erreur si une variable avec le même nom existe déjà.
|
||
|
|
///
|
||
|
|
/// # Examples
|
||
|
|
///
|
||
|
|
/// ```rust
|
||
|
|
/// # use pmoupnp::services::Service;
|
||
|
|
/// # use pmoupnp::state_variables::StateVariable;
|
||
|
|
/// # use pmoupnp::variable_types::StateVarType;
|
||
|
|
/// # use std::sync::Arc;
|
||
|
|
/// let mut service = Service::new("AVTransport".to_string());
|
||
|
|
/// let var = Arc::new(
|
||
|
|
/// StateVariable::new(StateVarType::String, "TransportState".to_string())
|
||
|
|
/// );
|
||
|
|
/// service.add_variable(var).unwrap();
|
||
|
|
/// ```
|
||
|
|
pub fn add_variable(&mut self, sv: Arc<crate::state_variables::StateVariable>)
|
||
|
|
-> Result<(), ServiceError>
|
||
|
|
{
|
||
|
|
self.state_table
|
||
|
|
.insert(sv)
|
||
|
|
.map_err(|e| ServiceError::SetError(format!("Failed to add variable: {:?}", e)))
|
||
|
|
}
|
||
|
|
|
||
|
|
/// Vérifie si une variable d'état existe dans le service.
|
||
|
|
///
|
||
|
|
/// # Arguments
|
||
|
|
///
|
||
|
|
/// * `sv` - Variable à rechercher
|
||
|
|
///
|
||
|
|
/// # Returns
|
||
|
|
///
|
||
|
|
/// `true` si la variable existe, `false` sinon.
|
||
|
|
///
|
||
|
|
/// # Examples
|
||
|
|
///
|
||
|
|
/// ```rust
|
||
|
|
/// # use pmoupnp::services::Service;
|
||
|
|
/// # use pmoupnp::state_variables::StateVariable;
|
||
|
|
/// # use pmoupnp::variable_types::StateVarType;
|
||
|
|
/// # use std::sync::Arc;
|
||
|
|
/// let mut service = Service::new("AVTransport".to_string());
|
||
|
|
/// let var = Arc::new(
|
||
|
|
/// StateVariable::new(StateVarType::String, "TransportState".to_string())
|
||
|
|
/// );
|
||
|
|
/// service.add_variable(var.clone()).unwrap();
|
||
|
|
/// assert!(service.contains_variable(var));
|
||
|
|
/// ```
|
||
|
|
pub fn contains_variable(&self, sv: Arc<crate::state_variables::StateVariable>) -> bool {
|
||
|
|
self.state_table.contains(sv)
|
||
|
|
}
|
||
|
|
|
||
|
|
/// Retourne toutes les variables d'état du service.
|
||
|
|
///
|
||
|
|
/// # Examples
|
||
|
|
///
|
||
|
|
/// ```rust
|
||
|
|
/// # use pmoupnp::services::Service;
|
||
|
|
/// let service = Service::new("AVTransport".to_string());
|
||
|
|
/// for var in service.variables() {
|
||
|
|
/// println!("Variable: {}", var.get_name());
|
||
|
|
/// }
|
||
|
|
/// ```
|
||
|
|
pub fn variables(&self) -> Vec<Arc<crate::state_variables::StateVariable>> {
|
||
|
|
self.state_table.all()
|
||
|
|
}
|
||
|
|
|
||
|
|
/// Ajoute une action au service.
|
||
|
|
///
|
||
|
|
/// # Arguments
|
||
|
|
///
|
||
|
|
/// * `action` - Action à ajouter
|
||
|
|
///
|
||
|
|
/// # Errors
|
||
|
|
///
|
||
|
|
/// Retourne une erreur si une action avec le même nom existe déjà.
|
||
|
|
///
|
||
|
|
/// # Examples
|
||
|
|
///
|
||
|
|
/// ```rust
|
||
|
|
/// # use pmoupnp::services::Service;
|
||
|
|
/// # use pmoupnp::actions::Action;
|
||
|
|
/// # use std::sync::Arc;
|
||
|
|
/// let mut service = Service::new("AVTransport".to_string());
|
||
|
|
/// let action = Arc::new(Action::new("Play".to_string()));
|
||
|
|
/// service.add_action(action).unwrap();
|
||
|
|
/// ```
|
||
|
|
pub fn add_action(&mut self, action: Arc<crate::actions::Action>)
|
||
|
|
-> Result<(), ServiceError>
|
||
|
|
{
|
||
|
|
self.actions
|
||
|
|
.insert(action)
|
||
|
|
.map_err(|e| ServiceError::SetError(format!("Failed to add action: {:?}", e)))
|
||
|
|
}
|
||
|
|
|
||
|
|
/// Retourne toutes les actions du service.
|
||
|
|
///
|
||
|
|
/// # Examples
|
||
|
|
///
|
||
|
|
/// ```rust
|
||
|
|
/// # use pmoupnp::services::Service;
|
||
|
|
/// let service = Service::new("AVTransport".to_string());
|
||
|
|
/// for action in service.actions() {
|
||
|
|
/// println!("Action: {}", action.get_name());
|
||
|
|
/// }
|
||
|
|
/// ```
|
||
|
|
pub fn actions(&self) -> Vec<Arc<crate::actions::Action>> {
|
||
|
|
self.actions.all()
|
||
|
|
}
|
||
|
|
|
||
|
|
/// Retourne le type de service UPnP.
|
||
|
|
///
|
||
|
|
/// Format: `urn:schemas-upnp-org:service:{name}:{version}`
|
||
|
|
///
|
||
|
|
/// # Examples
|
||
|
|
///
|
||
|
|
/// ```rust
|
||
|
|
/// # use pmoupnp::services::Service;
|
||
|
|
/// let service = Service::new("AVTransport".to_string());
|
||
|
|
/// assert_eq!(
|
||
|
|
/// service.service_type(),
|
||
|
|
/// "urn:schemas-upnp-org:service:AVTransport:1"
|
||
|
|
/// );
|
||
|
|
/// ```
|
||
|
|
pub fn service_type(&self) -> String {
|
||
|
|
format!("urn:schemas-upnp-org:service:{}:{}", self.name(), self.version)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
#[cfg(test)]
|
||
|
|
mod tests {
|
||
|
|
use super::*;
|
||
|
|
use crate::state_variables::StateVariable;
|
||
|
|
use crate::variable_types::StateVarType;
|
||
|
|
use crate::actions::Action;
|
||
|
|
|
||
|
|
#[test]
|
||
|
|
fn test_service_new() {
|
||
|
|
let service = Service::new("AVTransport".to_string());
|
||
|
|
assert_eq!(service.name(), "AVTransport");
|
||
|
|
assert_eq!(service.type_id(), "Service");
|
||
|
|
assert_eq!(service.version(), 1);
|
||
|
|
assert_eq!(service.identifier(), "AVTransport");
|
||
|
|
}
|
||
|
|
|
||
|
|
#[test]
|
||
|
|
fn test_service_set_version() {
|
||
|
|
let mut service = Service::new("AVTransport".to_string());
|
||
|
|
assert!(service.set_version(2).is_ok());
|
||
|
|
assert_eq!(service.version(), 2);
|
||
|
|
|
||
|
|
// Version 0 devrait échouer
|
||
|
|
assert!(service.set_version(0).is_err());
|
||
|
|
}
|
||
|
|
|
||
|
|
#[test]
|
||
|
|
fn test_service_add_variable() {
|
||
|
|
let mut service = Service::new("AVTransport".to_string());
|
||
|
|
let var = Arc::new(
|
||
|
|
StateVariable::new(StateVarType::String, "TransportState".to_string())
|
||
|
|
);
|
||
|
|
|
||
|
|
assert!(service.add_variable(var.clone()).is_ok());
|
||
|
|
assert!(service.contains_variable(var));
|
||
|
|
}
|
||
|
|
|
||
|
|
#[test]
|
||
|
|
fn test_service_add_action() {
|
||
|
|
let mut service = Service::new("AVTransport".to_string());
|
||
|
|
let action = Arc::new(Action::new("Play".to_string()));
|
||
|
|
|
||
|
|
assert!(service.add_action(action).is_ok());
|
||
|
|
assert_eq!(service.actions().len(), 1);
|
||
|
|
}
|
||
|
|
|
||
|
|
#[test]
|
||
|
|
fn test_service_type() {
|
||
|
|
let mut service = Service::new("AVTransport".to_string());
|
||
|
|
service.set_version(2).unwrap();
|
||
|
|
|
||
|
|
assert_eq!(
|
||
|
|
service.service_type(),
|
||
|
|
"urn:schemas-upnp-org:service:AVTransport:2"
|
||
|
|
);
|
||
|
|
}
|
||
|
|
}```
|
||
|
|
|
||
|
|
## fichier: `pmoupnp/src/services/errors.rs`
|
||
|
|
|
||
|
|
```rust
|
||
|
|
//! Erreurs du module services.
|
||
|
|
|
||
|
|
use thiserror::Error;
|
||
|
|
|
||
|
|
/// Erreurs liées aux services UPnP.
|
||
|
|
///
|
||
|
|
/// Cette énumération couvre toutes les erreurs possibles lors de la manipulation
|
||
|
|
/// de services UPnP, incluant les erreurs de validation, de configuration et d'exécution.
|
||
|
|
#[derive(Error, Debug)]
|
||
|
|
pub enum ServiceError {
|
||
|
|
/// Erreur générale du service.
|
||
|
|
#[error("Service error: {0}")]
|
||
|
|
GeneralError(String),
|
||
|
|
|
||
|
|
/// Erreur de validation (paramètres invalides).
|
||
|
|
#[error("Validation error: {0}")]
|
||
|
|
ValidationError(String),
|
||
|
|
|
||
|
|
/// Erreur lors d'une opération sur un ensemble (Set).
|
||
|
|
#[error("Set operation error: {0}")]
|
||
|
|
SetError(String),
|
||
|
|
|
||
|
|
/// Erreur liée à une action.
|
||
|
|
#[error("Action error: {0}")]
|
||
|
|
ActionError(String),
|
||
|
|
|
||
|
|
/// Erreur liée à une variable d'état.
|
||
|
|
#[error("State variable error: {0}")]
|
||
|
|
StateVariableError(String),
|
||
|
|
|
||
|
|
/// Erreur de configuration.
|
||
|
|
#[error("Configuration error: {0}")]
|
||
|
|
ConfigError(String),
|
||
|
|
|
||
|
|
/// Erreur réseau ou HTTP.
|
||
|
|
#[error("Network error: {0}")]
|
||
|
|
NetworkError(String),
|
||
|
|
|
||
|
|
/// Erreur de sérialisation XML.
|
||
|
|
#[error("XML serialization error: {0}")]
|
||
|
|
XmlError(String),
|
||
|
|
|
||
|
|
/// Erreur lors du traitement SOAP.
|
||
|
|
#[error("SOAP error: {0}")]
|
||
|
|
SoapError(String),
|
||
|
|
}
|
||
|
|
|
||
|
|
impl From<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))
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}```
|
||
|
|
|
||
|
|
## fichier: `pmoupnp/src/services/service_methods.rs`
|
||
|
|
|
||
|
|
```rust
|
||
|
|
//! Implémentation des traits UPnP pour Service.
|
||
|
|
|
||
|
|
use xmltree::{Element, XMLNode};
|
||
|
|
|
||
|
|
use crate::{
|
||
|
|
services::{Service, ServiceInstance},
|
||
|
|
UpnpObject, UpnpModel, UpnpTyped, UpnpObjectType,
|
||
|
|
};
|
||
|
|
|
||
|
|
impl UpnpTyped for Service {
|
||
|
|
fn as_upnp_object_type(&self) -> &UpnpObjectType {
|
||
|
|
&self.object
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
impl UpnpObject for Service {
|
||
|
|
fn to_xml_element(&self) -> Element {
|
||
|
|
let mut elem = Element::new("service");
|
||
|
|
|
||
|
|
// serviceType
|
||
|
|
let mut service_type = Element::new("serviceType");
|
||
|
|
service_type.children.push(XMLNode::Text(self.service_type()));
|
||
|
|
elem.children.push(XMLNode::Element(service_type));
|
||
|
|
|
||
|
|
// serviceId
|
||
|
|
let mut service_id = Element::new("serviceId");
|
||
|
|
service_id.children.push(XMLNode::Text(self.identifier().to_string()));
|
||
|
|
elem.children.push(XMLNode::Element(service_id));
|
||
|
|
|
||
|
|
elem
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
impl UpnpModel for Service {
|
||
|
|
type Instance = ServiceInstance;
|
||
|
|
}```
|
||
|
|
|
||
|
|
## fichier: `pmoupnp/src/devices/mod.rs`
|
||
|
|
|
||
|
|
```rust
|
||
|
|
```
|
||
|
|
|
||
|
|
## fichier: `pmoutils/Cargo.toml`
|
||
|
|
|
||
|
|
```toml
|
||
|
|
[package]
|
||
|
|
name = "pmoutils"
|
||
|
|
version = "0.1.0"
|
||
|
|
edition = "2024"
|
||
|
|
|
||
|
|
[dependencies]
|
||
|
|
get_if_addrs = "0.5.3"```
|
||
|
|
|
||
|
|
## fichier: `pmoutils/src/lib.rs`
|
||
|
|
|
||
|
|
```rust
|
||
|
|
/// Utilitaires pour la gestion des adresses IP réseau.
|
||
|
|
///
|
||
|
|
/// Ce module fournit des fonctions pour détecter et lister les adresses IP
|
||
|
|
/// des interfaces réseau locales de la machine.
|
||
|
|
///
|
||
|
|
/// # Fonctions principales
|
||
|
|
///
|
||
|
|
/// - [`guess_local_ip`] : Devine l'adresse IP locale utilisée pour les connexions sortantes
|
||
|
|
///
|
||
|
|
/// # Examples
|
||
|
|
///
|
||
|
|
/// ```
|
||
|
|
/// use votre_crate::guess_local_ip;
|
||
|
|
///
|
||
|
|
/// let ip = guess_local_ip();
|
||
|
|
/// println!("Adresse IP locale: {}", ip);
|
||
|
|
/// ```
|
||
|
|
mod ip_utils;
|
||
|
|
|
||
|
|
pub use ip_utils::guess_local_ip;```
|
||
|
|
|
||
|
|
## fichier: `pmoutils/src/ip_utils.rs`
|
||
|
|
|
||
|
|
```rust
|
||
|
|
use get_if_addrs::get_if_addrs;
|
||
|
|
use std::net::UdpSocket;
|
||
|
|
|
||
|
|
/// Devine l'adresse IP locale de la machine.
|
||
|
|
///
|
||
|
|
/// Cette fonction tente de déterminer l'adresse IP locale en créant une connexion UDP
|
||
|
|
/// vers un serveur DNS public (8.8.8.8). Cette technique permet d'identifier l'interface
|
||
|
|
/// réseau qui serait utilisée pour communiquer avec Internet.
|
||
|
|
///
|
||
|
|
/// # Fonctionnement
|
||
|
|
///
|
||
|
|
/// 1. Crée un socket UDP lié à `0.0.0.0:0` (n'importe quelle interface, port aléatoire)
|
||
|
|
/// 2. Tente une connexion (non effective pour UDP) vers `8.8.8.8:80`
|
||
|
|
/// 3. Récupère l'adresse IP locale du socket
|
||
|
|
/// 4. En cas d'échec à n'importe quelle étape, retourne `127.0.0.1`
|
||
|
|
///
|
||
|
|
/// # Returns
|
||
|
|
///
|
||
|
|
/// Retourne l'adresse IP locale sous forme de `String`, ou `"127.0.0.1"` en cas d'erreur.
|
||
|
|
///
|
||
|
|
/// # Examples
|
||
|
|
///
|
||
|
|
/// ```
|
||
|
|
/// let ip = guess_local_ip();
|
||
|
|
/// println!("IP locale détectée: {}", ip);
|
||
|
|
/// // Affiche par exemple: "IP locale détectée: 192.168.1.42"
|
||
|
|
/// ```
|
||
|
|
///
|
||
|
|
/// # Note
|
||
|
|
///
|
||
|
|
/// Cette méthode ne crée pas de véritable connexion réseau (UDP est sans connexion),
|
||
|
|
/// elle demande simplement au système d'exploitation quelle interface serait utilisée
|
||
|
|
/// pour joindre l'adresse cible.
|
||
|
|
pub fn guess_local_ip() -> String {
|
||
|
|
match UdpSocket::bind("0.0.0.0:0") {
|
||
|
|
Ok(socket) => {
|
||
|
|
if socket.connect("8.8.8.8:80").is_ok() {
|
||
|
|
if let Ok(local_addr) = socket.local_addr() {
|
||
|
|
return local_addr.ip().to_string();
|
||
|
|
}
|
||
|
|
}
|
||
|
|
"127.0.0.1".to_string()
|
||
|
|
}
|
||
|
|
Err(_) => "127.0.0.1".to_string(),
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
/// Liste toutes les adresses IP non-loopback des interfaces réseau.
|
||
|
|
///
|
||
|
|
/// Parcourt toutes les interfaces réseau de la machine et collecte leurs adresses IPv4,
|
||
|
|
/// en excluant les adresses de loopback (127.0.0.1).
|
||
|
|
///
|
||
|
|
/// # Returns
|
||
|
|
///
|
||
|
|
/// Retourne une `HashMap` où :
|
||
|
|
/// - **Clé** : nom de l'interface réseau (ex: `"eth0"`, `"wlan0"`, `"en0"`)
|
||
|
|
/// - **Valeur** : vecteur des adresses IP (format String) associées à cette interface
|
||
|
|
///
|
||
|
|
/// En cas d'erreur lors de la récupération des interfaces, retourne une HashMap
|
||
|
|
/// contenant une entrée `"error"` avec un message d'erreur.
|
||
|
|
///
|
||
|
|
/// # Examples
|
||
|
|
///
|
||
|
|
/// ```
|
||
|
|
/// let ips = list_all_ips();
|
||
|
|
/// for (interface, addresses) in ips {
|
||
|
|
/// println!("Interface {}: {:?}", interface, addresses);
|
||
|
|
/// }
|
||
|
|
/// // Affiche par exemple:
|
||
|
|
/// // Interface eth0: ["192.168.1.42"]
|
||
|
|
/// // Interface wlan0: ["10.0.0.15"]
|
||
|
|
/// ```
|
||
|
|
///
|
||
|
|
/// # Note
|
||
|
|
///
|
||
|
|
/// - Seules les adresses IPv4 sont retournées
|
||
|
|
/// - Les adresses de loopback (127.x.x.x) sont filtrées
|
||
|
|
/// - Les adresses IPv6 sont ignorées
|
||
|
|
pub fn list_all_ips() -> std::collections::HashMap<String, Vec<String>> {
|
||
|
|
let mut result = std::collections::HashMap::new();
|
||
|
|
|
||
|
|
if let Ok(interfaces) = get_if_addrs() {
|
||
|
|
for iface in interfaces {
|
||
|
|
let ip = iface.ip();
|
||
|
|
if ip.is_loopback() {
|
||
|
|
continue;
|
||
|
|
}
|
||
|
|
if ip.is_ipv4() {
|
||
|
|
result
|
||
|
|
.entry(iface.name)
|
||
|
|
.or_insert_with(Vec::new)
|
||
|
|
.push(ip.to_string());
|
||
|
|
}
|
||
|
|
}
|
||
|
|
} else {
|
||
|
|
result.insert(
|
||
|
|
"error".to_string(),
|
||
|
|
vec!["Failed to get interfaces".to_string()],
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
result
|
||
|
|
}
|
||
|
|
|
||
|
|
#[cfg(test)]
|
||
|
|
mod tests {
|
||
|
|
use super::*;
|
||
|
|
use std::net::IpAddr;
|
||
|
|
|
||
|
|
#[test]
|
||
|
|
fn test_guess_local_ip_returns_valid_ip() {
|
||
|
|
let ip = guess_local_ip();
|
||
|
|
|
||
|
|
// Vérifie que le résultat est parsable comme une IP
|
||
|
|
assert!(ip.parse::<IpAddr>().is_ok(), "Should return a valid IP address");
|
||
|
|
}
|
||
|
|
|
||
|
|
#[test]
|
||
|
|
fn test_guess_local_ip_not_empty() {
|
||
|
|
let ip = guess_local_ip();
|
||
|
|
|
||
|
|
assert!(!ip.is_empty(), "IP should not be empty");
|
||
|
|
}
|
||
|
|
|
||
|
|
#[test]
|
||
|
|
fn test_guess_local_ip_is_ipv4() {
|
||
|
|
let ip = guess_local_ip();
|
||
|
|
|
||
|
|
if let Ok(parsed_ip) = ip.parse::<IpAddr>() {
|
||
|
|
assert!(parsed_ip.is_ipv4(), "Should return an IPv4 address");
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
#[test]
|
||
|
|
fn test_guess_local_ip_fallback_is_localhost() {
|
||
|
|
// Ce test vérifie que si aucune IP n'est trouvée, on retourne 127.0.0.1
|
||
|
|
// (difficile à tester sans mocker, mais on vérifie la cohérence)
|
||
|
|
let ip = guess_local_ip();
|
||
|
|
let parsed = ip.parse::<IpAddr>().unwrap();
|
||
|
|
|
||
|
|
// L'IP doit être soit locale (127.0.0.1) soit une IP privée valide
|
||
|
|
assert!(
|
||
|
|
parsed.is_loopback() || is_private_ip(&ip),
|
||
|
|
"IP should be either loopback or private"
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
#[test]
|
||
|
|
fn test_list_all_ips_no_loopback() {
|
||
|
|
let ips = list_all_ips();
|
||
|
|
|
||
|
|
// Vérifie qu'aucune adresse de loopback n'est présente
|
||
|
|
for (_, addresses) in ips.iter() {
|
||
|
|
for addr in addresses {
|
||
|
|
if let Ok(parsed_ip) = addr.parse::<IpAddr>() {
|
||
|
|
assert!(
|
||
|
|
!parsed_ip.is_loopback(),
|
||
|
|
"Loopback addresses should be filtered out"
|
||
|
|
);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
#[test]
|
||
|
|
fn test_list_all_ips_only_ipv4() {
|
||
|
|
let ips = list_all_ips();
|
||
|
|
|
||
|
|
// Vérifie que seules des adresses IPv4 sont retournées
|
||
|
|
for (iface_name, addresses) in ips.iter() {
|
||
|
|
if iface_name == "error" {
|
||
|
|
continue; // Skip error entries
|
||
|
|
}
|
||
|
|
|
||
|
|
for addr in addresses {
|
||
|
|
if let Ok(parsed_ip) = addr.parse::<IpAddr>() {
|
||
|
|
assert!(
|
||
|
|
parsed_ip.is_ipv4(),
|
||
|
|
"Only IPv4 addresses should be returned"
|
||
|
|
);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
#[test]
|
||
|
|
fn test_list_all_ips_valid_format() {
|
||
|
|
let ips = list_all_ips();
|
||
|
|
|
||
|
|
// Vérifie que toutes les IPs sont dans un format valide
|
||
|
|
for (iface_name, addresses) in ips.iter() {
|
||
|
|
if iface_name == "error" {
|
||
|
|
continue;
|
||
|
|
}
|
||
|
|
|
||
|
|
for addr in addresses {
|
||
|
|
assert!(
|
||
|
|
addr.parse::<IpAddr>().is_ok(),
|
||
|
|
"Each IP should be in valid format: {}",
|
||
|
|
addr
|
||
|
|
);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
#[test]
|
||
|
|
fn test_list_all_ips_interface_names_not_empty() {
|
||
|
|
let ips = list_all_ips();
|
||
|
|
|
||
|
|
// Vérifie que les noms d'interface ne sont pas vides
|
||
|
|
for (iface_name, _) in ips.iter() {
|
||
|
|
assert!(!iface_name.is_empty(), "Interface names should not be empty");
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
#[test]
|
||
|
|
fn test_list_all_ips_no_duplicate_ips_per_interface() {
|
||
|
|
let ips = list_all_ips();
|
||
|
|
|
||
|
|
// Vérifie qu'il n'y a pas de doublons par interface
|
||
|
|
for (iface_name, addresses) in ips.iter() {
|
||
|
|
if iface_name == "error" {
|
||
|
|
continue;
|
||
|
|
}
|
||
|
|
|
||
|
|
let unique_addresses: std::collections::HashSet<_> = addresses.iter().collect();
|
||
|
|
assert_eq!(
|
||
|
|
addresses.len(),
|
||
|
|
unique_addresses.len(),
|
||
|
|
"No duplicate IPs should exist for interface {}",
|
||
|
|
iface_name
|
||
|
|
);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// Fonction helper pour les tests
|
||
|
|
fn is_private_ip(ip_str: &str) -> bool {
|
||
|
|
if let Ok(ip) = ip_str.parse::<IpAddr>() {
|
||
|
|
match ip {
|
||
|
|
IpAddr::V4(ipv4) => {
|
||
|
|
let octets = ipv4.octets();
|
||
|
|
// Plages privées: 10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16
|
||
|
|
octets[0] == 10
|
||
|
|
|| (octets[0] == 172 && octets[1] >= 16 && octets[1] <= 31)
|
||
|
|
|| (octets[0] == 192 && octets[1] == 168)
|
||
|
|
}
|
||
|
|
IpAddr::V6(_) => false,
|
||
|
|
}
|
||
|
|
} else {
|
||
|
|
false
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
#[test]
|
||
|
|
fn test_helper_is_private_ip() {
|
||
|
|
// Tests pour la fonction helper
|
||
|
|
assert!(is_private_ip("10.0.0.1"));
|
||
|
|
assert!(is_private_ip("172.16.0.1"));
|
||
|
|
assert!(is_private_ip("192.168.1.1"));
|
||
|
|
assert!(!is_private_ip("8.8.8.8"));
|
||
|
|
assert!(!is_private_ip("127.0.0.1")); // loopback n'est pas "privé" au sens réseau local
|
||
|
|
}
|
||
|
|
}```
|
||
|
|
|