From 2c8a1325b407f23e203968338224f4a1d3944629 Mon Sep 17 00:00:00 2001 From: Eric Coissac Date: Fri, 10 Oct 2025 12:33:24 +0200 Subject: [PATCH] =?UTF-8?q?Ajoute=20une=20API=20d'exposition=20de=20l'?= =?UTF-8?q?=C3=A9tat=20interne=20du=20serveur=20UPNP=20et=20un=20composant?= =?UTF-8?q?=20=C3=A0=20l'application=20web=20qui=20permet=20de=20l'explore?= =?UTF-8?q?r?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pmoapp/webapp/src/App.vue | 3 +- pmoapp/webapp/src/components/UpnpExplorer.vue | 421 ++++++++++++++ .../src/components/upnp/ActionsList.vue | 491 ++++++++++++++++ .../webapp/src/components/upnp/DeviceCard.vue | 307 ++++++++++ .../src/components/upnp/ServicePanel.vue | 281 +++++++++ .../src/components/upnp/VariablesList.vue | 546 ++++++++++++++++++ pmoapp/webapp/src/router/index.ts | 2 + .../src/state_variables/variable_methods.rs | 25 + pmoupnp/src/upnp_api.rs | 72 ++- pmoupnp/src/upnp_server.rs | 43 +- 10 files changed, 2166 insertions(+), 25 deletions(-) create mode 100644 pmoapp/webapp/src/components/UpnpExplorer.vue create mode 100644 pmoapp/webapp/src/components/upnp/ActionsList.vue create mode 100644 pmoapp/webapp/src/components/upnp/DeviceCard.vue create mode 100644 pmoapp/webapp/src/components/upnp/ServicePanel.vue create mode 100644 pmoapp/webapp/src/components/upnp/VariablesList.vue diff --git a/pmoapp/webapp/src/App.vue b/pmoapp/webapp/src/App.vue index db750d1a..4559f92e 100644 --- a/pmoapp/webapp/src/App.vue +++ b/pmoapp/webapp/src/App.vue @@ -3,7 +3,8 @@ diff --git a/pmoapp/webapp/src/components/UpnpExplorer.vue b/pmoapp/webapp/src/components/UpnpExplorer.vue new file mode 100644 index 00000000..37c3752a --- /dev/null +++ b/pmoapp/webapp/src/components/UpnpExplorer.vue @@ -0,0 +1,421 @@ + + + + + diff --git a/pmoapp/webapp/src/components/upnp/ActionsList.vue b/pmoapp/webapp/src/components/upnp/ActionsList.vue new file mode 100644 index 00000000..4447242d --- /dev/null +++ b/pmoapp/webapp/src/components/upnp/ActionsList.vue @@ -0,0 +1,491 @@ + + + + + diff --git a/pmoapp/webapp/src/components/upnp/DeviceCard.vue b/pmoapp/webapp/src/components/upnp/DeviceCard.vue new file mode 100644 index 00000000..f8b52461 --- /dev/null +++ b/pmoapp/webapp/src/components/upnp/DeviceCard.vue @@ -0,0 +1,307 @@ + + + + + diff --git a/pmoapp/webapp/src/components/upnp/ServicePanel.vue b/pmoapp/webapp/src/components/upnp/ServicePanel.vue new file mode 100644 index 00000000..8ab745a3 --- /dev/null +++ b/pmoapp/webapp/src/components/upnp/ServicePanel.vue @@ -0,0 +1,281 @@ + + + + + diff --git a/pmoapp/webapp/src/components/upnp/VariablesList.vue b/pmoapp/webapp/src/components/upnp/VariablesList.vue new file mode 100644 index 00000000..ff11a443 --- /dev/null +++ b/pmoapp/webapp/src/components/upnp/VariablesList.vue @@ -0,0 +1,546 @@ + + + + + diff --git a/pmoapp/webapp/src/router/index.ts b/pmoapp/webapp/src/router/index.ts index 7aaaebc6..44965899 100644 --- a/pmoapp/webapp/src/router/index.ts +++ b/pmoapp/webapp/src/router/index.ts @@ -2,11 +2,13 @@ import { createRouter, createWebHistory } from "vue-router"; import HelloWorld from "../components/HelloWorld.vue"; import LogView from "../components/LogView.vue"; import CoverCacheManager from "../components/CoverCacheManager.vue"; +import UpnpExplorer from "../components/UpnpExplorer.vue"; const routes = [ { path: "/", name: "home", component: HelloWorld }, { path: "/logs", name: "logs", component: LogView }, { path: "/covers-cache", name: "covers-cache", component: CoverCacheManager }, + { path: "/upnp", name: "upnp", component: UpnpExplorer }, ]; const router = createRouter({ diff --git a/pmoupnp/src/state_variables/variable_methods.rs b/pmoupnp/src/state_variables/variable_methods.rs index 3bda6759..d9299b0d 100644 --- a/pmoupnp/src/state_variables/variable_methods.rs +++ b/pmoupnp/src/state_variables/variable_methods.rs @@ -403,4 +403,29 @@ impl StateVariable { pub fn unset_value_marshaler(&mut self) { self.marshal = None; } + + /// Retourne le type de données de cette variable. + pub fn get_data_type(&self) -> &StateVarType { + &self.value_type + } + + /// Retourne la valeur par défaut si définie. + pub fn get_default_value(&self) -> Option<&StateValue> { + self.default_value.as_ref() + } + + /// Retourne le step si défini. + pub fn get_step(&self) -> Option<&StateValue> { + self.step.as_ref() + } + + /// Retourne les valeurs autorisées. + pub fn get_allowed_values(&self) -> Vec { + self.allowed_values.read().unwrap().clone() + } + + /// Indique si cette variable envoie des notifications d'événements. + pub fn sends_events(&self) -> bool { + self.send_events + } } diff --git a/pmoupnp/src/upnp_api.rs b/pmoupnp/src/upnp_api.rs index 5d0c4e13..d32c9f44 100644 --- a/pmoupnp/src/upnp_api.rs +++ b/pmoupnp/src/upnp_api.rs @@ -64,6 +64,45 @@ async fn get_device(Path(udn): Path) -> impl IntoResponse { .services() .iter() .map(|s| { + // Collecter les actions + let actions: Vec<_> = s.actions() + .all() + .iter() + .map(|a| { + let all_args = a.arguments_set().all(); + + let in_args: Vec<_> = all_args + .iter() + .filter(|arg| arg.get_model().is_in()) + .map(|arg| { + let model = arg.get_model(); + json!({ + "name": arg.get_name(), + "related_state_variable": model.state_variable().get_name() + }) + }) + .collect(); + + let out_args: Vec<_> = all_args + .iter() + .filter(|arg| arg.get_model().is_out()) + .map(|arg| { + let model = arg.get_model(); + json!({ + "name": arg.get_name(), + "related_state_variable": model.state_variable().get_name() + }) + }) + .collect(); + + json!({ + "name": a.get_name(), + "in_arguments": in_args, + "out_arguments": out_args + }) + }) + .collect(); + json!({ "name": s.get_name(), "service_type": s.service_type(), @@ -71,6 +110,7 @@ async fn get_device(Path(udn): Path) -> impl IntoResponse { "control_url": format!("{}{}", device.base_url(), s.control_route()), "event_url": format!("{}{}", device.base_url(), s.event_route()), "scpd_url": format!("{}{}", device.base_url(), s.scpd_route()), + "actions": actions }) }) .collect(); @@ -112,10 +152,38 @@ async fn get_service_variables(Path((udn, service_name)): Path<(String, String)> .all() .iter() .map(|v| { + let model = v.get_model(); + + // Obtenir les allowed values + let allowed_values = { + let av = model.get_allowed_values(); + if av.is_empty() { + None + } else { + Some(av.iter().map(|val| val.to_string()).collect::>()) + } + }; + + // Accéder au range si défini + let (min, max) = if let Some(range) = model.get_range() { + ( + Some(range.get_minimum().to_string()), + Some(range.get_maximum().to_string()) + ) + } else { + (None, None) + }; + json!({ "name": v.get_name(), "value": v.value().to_string(), + "data_type": model.get_data_type().to_string(), "sends_events": v.is_sending_notification(), + "default_value": model.get_default_value().map(|dv| dv.to_string()), + "allowed_values": allowed_values, + "min": min, + "max": max, + "step": model.get_step().map(|s| s.to_string()), }) }) .collect(); @@ -168,9 +236,9 @@ impl UpnpApiExt for Server { // Créer le routeur Axum let app = Router::new() .route("/devices", get(list_devices)) - .route("/devices/:udn", get(get_device)) + .route("/devices/{udn}", get(get_device)) .route( - "/devices/:udn/services/:service/variables", + "/devices/{udn}/services/{service}/variables", get(get_service_variables), ); diff --git a/pmoupnp/src/upnp_server.rs b/pmoupnp/src/upnp_server.rs index 040ec41d..994c9b98 100644 --- a/pmoupnp/src/upnp_server.rs +++ b/pmoupnp/src/upnp_server.rs @@ -19,7 +19,8 @@ //! ``` use std::sync::Arc; -use std::cell::RefCell; +use std::sync::RwLock; +use once_cell::sync::Lazy; use pmoserver::Server; @@ -27,13 +28,14 @@ use crate::devices::errors::DeviceError; use crate::devices::{Device, DeviceInstance, DeviceRegistry}; use crate::UpnpModel; -thread_local! { - /// Registre de devices thread-local. - /// - /// Permet de maintenir un registre de devices par thread/serveur - /// sans modifier la structure `pmoserver::Server`. - static DEVICE_REGISTRY: RefCell = RefCell::new(DeviceRegistry::new()); -} +/// Registre de devices global et thread-safe. +/// +/// Utilise Lazy pour une initialisation paresseuse et RwLock pour le partage entre threads. +/// Ceci permet aux API handlers (qui s'exécutent dans des threads différents) d'accéder +/// au même registre de devices. +static DEVICE_REGISTRY: Lazy> = Lazy::new(|| { + RwLock::new(DeviceRegistry::new()) +}); /// Trait pour étendre un serveur avec des fonctionnalités UPnP. /// @@ -99,31 +101,30 @@ impl UpnpServer for Server { di.register_urls(self).await?; // Ajouter au registre pour l'introspection - DEVICE_REGISTRY.with(|registry| { - registry.borrow_mut() - .register(di.clone()) - .map_err(|e| DeviceError::UrlRegistrationError(e)) - })?; + DEVICE_REGISTRY.write() + .unwrap() + .register(di.clone()) + .map_err(|e| DeviceError::UrlRegistrationError(e))?; Ok(di) } fn device_count(&self) -> usize { - DEVICE_REGISTRY.with(|registry| registry.borrow().count()) + DEVICE_REGISTRY.read().unwrap().count() } fn list_devices(&self) -> Vec> { - DEVICE_REGISTRY.with(|registry| registry.borrow().list_devices()) + DEVICE_REGISTRY.read().unwrap().list_devices() } fn get_device(&self, udn: &str) -> Option> { - DEVICE_REGISTRY.with(|registry| registry.borrow().get_device(udn)) + DEVICE_REGISTRY.read().unwrap().get_device(udn) } } /// Fonctions helper pour accéder au registre depuis les handlers. /// -/// Ces fonctions permettent d'accéder au registre thread-local depuis +/// Ces fonctions permettent d'accéder au registre global depuis /// n'importe où dans le code, notamment depuis les handlers Axum. /// Exécute une closure avec un accès en lecture seule aux devices. @@ -139,10 +140,8 @@ pub fn with_devices(f: F) -> R where F: FnOnce(&Vec>) -> R, { - DEVICE_REGISTRY.with(|registry| { - let devices = registry.borrow().list_devices(); - f(&devices) - }) + let devices = DEVICE_REGISTRY.read().unwrap().list_devices(); + f(&devices) } /// Récupère un device par son UDN. @@ -157,7 +156,7 @@ where /// } /// ``` pub fn get_device_by_udn(udn: &str) -> Option> { - DEVICE_REGISTRY.with(|registry| registry.borrow().get_device(udn)) + DEVICE_REGISTRY.read().unwrap().get_device(udn) } #[cfg(test)]