Revenons sur les devices

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

1
.gitignore vendored
View File

@@ -21,3 +21,4 @@ OLD-GO-CODE/
xxx
xx
all.txt
pmo_src.txt

View File

@@ -1,4 +1,7 @@
{
"makefile.configureOnOpen": false,
"git.enabled": false
"git.enabled": false,
"claude-code.environmentVariables": [
]
}

480
Cargo.lock generated
View File

@@ -233,7 +233,7 @@ dependencies = [
"miniz_oxide",
"object",
"rustc-demangle",
"windows-link",
"windows-link 0.2.0",
]
[[package]]
@@ -414,7 +414,17 @@ dependencies = [
"num-traits",
"serde",
"wasm-bindgen",
"windows-link",
"windows-link 0.2.0",
]
[[package]]
name = "core-foundation"
version = "0.9.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f"
dependencies = [
"core-foundation-sys",
"libc",
]
[[package]]
@@ -577,6 +587,15 @@ version = "2.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "117240f60069e65410b3ae1bb213295bd828f707b5bec6596a1afc8793ce0cbc"
[[package]]
name = "encoding_rs"
version = "0.8.35"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3"
dependencies = [
"cfg-if",
]
[[package]]
name = "equivalent"
version = "1.0.2"
@@ -594,6 +613,22 @@ dependencies = [
"typeid",
]
[[package]]
name = "errno"
version = "0.3.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb"
dependencies = [
"libc",
"windows-sys 0.61.1",
]
[[package]]
name = "fastrand"
version = "2.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be"
[[package]]
name = "find-msvc-tools"
version = "0.1.2"
@@ -623,6 +658,21 @@ version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb"
[[package]]
name = "foreign-types"
version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1"
dependencies = [
"foreign-types-shared",
]
[[package]]
name = "foreign-types-shared"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b"
[[package]]
name = "form_urlencoded"
version = "1.2.2"
@@ -908,6 +958,39 @@ dependencies = [
"pin-utils",
"smallvec",
"tokio",
"want",
]
[[package]]
name = "hyper-rustls"
version = "0.27.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e3c93eb611681b207e1fe55d5a71ecf91572ec8a6705cdb6857f7d8d5242cf58"
dependencies = [
"http",
"hyper",
"hyper-util",
"rustls",
"rustls-pki-types",
"tokio",
"tokio-rustls",
"tower-service",
]
[[package]]
name = "hyper-tls"
version = "0.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "70206fc6890eaca9fde8a0bf71caa2ddfc9fe045ac9e5c70df101a7dbde866e0"
dependencies = [
"bytes",
"http-body-util",
"hyper",
"hyper-util",
"native-tls",
"tokio",
"tokio-native-tls",
"tower-service",
]
[[package]]
@@ -916,14 +999,24 @@ version = "0.1.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3c6995591a8f1380fcb4ba966a252a4b29188d51d2b89e3a252f5305be65aea8"
dependencies = [
"base64",
"bytes",
"futures-channel",
"futures-core",
"futures-util",
"http",
"http-body",
"hyper",
"ipnet",
"libc",
"percent-encoding",
"pin-project-lite",
"socket2",
"system-configuration",
"tokio",
"tower-service",
"tracing",
"windows-registry",
]
[[package]]
@@ -1095,6 +1188,22 @@ dependencies = [
"libc",
]
[[package]]
name = "ipnet"
version = "2.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130"
[[package]]
name = "iri-string"
version = "0.7.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dbc5ebe9c3a1a7a5127f920a418f7585e9e758e911d0466ed004f393b0e380b2"
dependencies = [
"memchr",
"serde",
]
[[package]]
name = "itoa"
version = "1.0.15"
@@ -1142,6 +1251,12 @@ dependencies = [
"zlib-rs",
]
[[package]]
name = "linux-raw-sys"
version = "0.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "df1d3c3b53da64cf5760482273a98e575c651a67eec7f77df96b5b642de8f039"
[[package]]
name = "litemap"
version = "0.8.0"
@@ -1221,6 +1336,23 @@ dependencies = [
"windows-sys 0.59.0",
]
[[package]]
name = "native-tls"
version = "0.2.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "87de3442987e9dbec73158d5c715e7ad9072fda936bb03d19d7fa10e00520f0e"
dependencies = [
"libc",
"log",
"openssl",
"openssl-probe",
"openssl-sys",
"schannel",
"security-framework",
"security-framework-sys",
"tempfile",
]
[[package]]
name = "nu-ansi-term"
version = "0.50.1"
@@ -1254,6 +1386,50 @@ version = "1.21.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d"
[[package]]
name = "openssl"
version = "0.10.73"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8505734d46c8ab1e19a1dce3aef597ad87dcb4c37e7188231769bd6bd51cebf8"
dependencies = [
"bitflags",
"cfg-if",
"foreign-types",
"libc",
"once_cell",
"openssl-macros",
"openssl-sys",
]
[[package]]
name = "openssl-macros"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "openssl-probe"
version = "0.1.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e"
[[package]]
name = "openssl-sys"
version = "0.9.109"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "90096e2e47630d78b7d1c20952dc621f957103f8bc2c8359ec81290d75238571"
dependencies = [
"cc",
"libc",
"pkg-config",
"vcpkg",
]
[[package]]
name = "option-ext"
version = "0.2.0"
@@ -1301,6 +1477,12 @@ version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184"
[[package]]
name = "pkg-config"
version = "0.3.32"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c"
[[package]]
name = "pmoconfig"
version = "0.1.0"
@@ -1350,6 +1532,7 @@ dependencies = [
"parking_lot",
"pmoconfig",
"pmodidl",
"reqwest",
"rust-embed",
"serde",
"serde_json",
@@ -1502,6 +1685,60 @@ version = "0.8.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "caf4aa5b0f434c91fe5c7f1ecb6a5ece2130b02ad2a590589dda5146df959001"
[[package]]
name = "reqwest"
version = "0.12.23"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d429f34c8092b2d42c7c93cec323bb4adeb7c67698f70839adec842ec10c7ceb"
dependencies = [
"base64",
"bytes",
"encoding_rs",
"futures-core",
"h2",
"http",
"http-body",
"http-body-util",
"hyper",
"hyper-rustls",
"hyper-tls",
"hyper-util",
"js-sys",
"log",
"mime",
"native-tls",
"percent-encoding",
"pin-project-lite",
"rustls-pki-types",
"serde",
"serde_json",
"serde_urlencoded",
"sync_wrapper",
"tokio",
"tokio-native-tls",
"tower",
"tower-http",
"tower-service",
"url",
"wasm-bindgen",
"wasm-bindgen-futures",
"web-sys",
]
[[package]]
name = "ring"
version = "0.17.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7"
dependencies = [
"cc",
"cfg-if",
"getrandom 0.2.16",
"libc",
"untrusted",
"windows-sys 0.52.0",
]
[[package]]
name = "rust-embed"
version = "8.7.2"
@@ -1542,6 +1779,52 @@ version = "0.1.26"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "56f7d92ca342cea22a06f2121d944b4fd82af56988c270852495420f961d4ace"
[[package]]
name = "rustix"
version = "1.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cd15f8a2c5551a84d56efdc1cd049089e409ac19a3072d5037a17fd70719ff3e"
dependencies = [
"bitflags",
"errno",
"libc",
"linux-raw-sys",
"windows-sys 0.61.1",
]
[[package]]
name = "rustls"
version = "0.23.32"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cd3c25631629d034ce7cd9940adc9d45762d46de2b0f57193c4443b92c6d4d40"
dependencies = [
"once_cell",
"rustls-pki-types",
"rustls-webpki",
"subtle",
"zeroize",
]
[[package]]
name = "rustls-pki-types"
version = "1.12.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "229a4a4c221013e7e1f1a043678c5cc39fe5171437c88fb47151a21e6f5b5c79"
dependencies = [
"zeroize",
]
[[package]]
name = "rustls-webpki"
version = "0.103.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e10b3f4191e8a80e6b43eebabfac91e5dcecebb27a71f04e820c47ec41d314bf"
dependencies = [
"ring",
"rustls-pki-types",
"untrusted",
]
[[package]]
name = "rustversion"
version = "1.0.22"
@@ -1563,12 +1846,44 @@ dependencies = [
"winapi-util",
]
[[package]]
name = "schannel"
version = "0.1.28"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "891d81b926048e76efe18581bf793546b4c0eaf8448d72be8de2bbee5fd166e1"
dependencies = [
"windows-sys 0.61.1",
]
[[package]]
name = "scopeguard"
version = "1.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
[[package]]
name = "security-framework"
version = "2.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02"
dependencies = [
"bitflags",
"core-foundation",
"core-foundation-sys",
"libc",
"security-framework-sys",
]
[[package]]
name = "security-framework-sys"
version = "2.15.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cc1f0cbffaac4852523ce30d8bd3c5cdc873501d96ff467ca09b6767bb8cd5c0"
dependencies = [
"core-foundation-sys",
"libc",
]
[[package]]
name = "serde"
version = "1.0.228"
@@ -1741,6 +2056,12 @@ version = "0.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
[[package]]
name = "subtle"
version = "2.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292"
[[package]]
name = "syn"
version = "2.0.106"
@@ -1757,6 +2078,9 @@ name = "sync_wrapper"
version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263"
dependencies = [
"futures-core",
]
[[package]]
name = "synstructure"
@@ -1769,6 +2093,40 @@ dependencies = [
"syn",
]
[[package]]
name = "system-configuration"
version = "0.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3c879d448e9d986b661742763247d3693ed13609438cf3d006f51f5368a5ba6b"
dependencies = [
"bitflags",
"core-foundation",
"system-configuration-sys",
]
[[package]]
name = "system-configuration-sys"
version = "0.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8e1d1b10ced5ca923a1fcb8d03e96b8d3268065d724548c0211415ff6ac6bac4"
dependencies = [
"core-foundation-sys",
"libc",
]
[[package]]
name = "tempfile"
version = "3.23.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2d31c77bdf42a745371d260a26ca7163f1e0924b64afa0b688e61b5a9fa02f16"
dependencies = [
"fastrand",
"getrandom 0.3.3",
"once_cell",
"rustix",
"windows-sys 0.61.1",
]
[[package]]
name = "thiserror"
version = "2.0.17"
@@ -1838,6 +2196,26 @@ dependencies = [
"syn",
]
[[package]]
name = "tokio-native-tls"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bbae76ab933c85776efabc971569dd6119c580d8f5d448769dec1764bf796ef2"
dependencies = [
"native-tls",
"tokio",
]
[[package]]
name = "tokio-rustls"
version = "0.26.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61"
dependencies = [
"rustls",
"tokio",
]
[[package]]
name = "tokio-stream"
version = "0.1.17"
@@ -1908,6 +2286,24 @@ dependencies = [
"tracing",
]
[[package]]
name = "tower-http"
version = "0.6.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "adc82fd73de2a9722ac5da747f12383d2bfdb93591ee6c58486e0097890f05f2"
dependencies = [
"bitflags",
"bytes",
"futures-util",
"http",
"http-body",
"iri-string",
"pin-project-lite",
"tower",
"tower-layer",
"tower-service",
]
[[package]]
name = "tower-layer"
version = "0.3.3"
@@ -1982,6 +2378,12 @@ dependencies = [
"tracing-log",
]
[[package]]
name = "try-lock"
version = "0.2.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b"
[[package]]
name = "typeid"
version = "1.0.3"
@@ -2012,6 +2414,12 @@ version = "0.2.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "673aac59facbab8a9007c7f6108d11f63b603f7cabff99fabf650fea5c32b861"
[[package]]
name = "untrusted"
version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1"
[[package]]
name = "url"
version = "2.5.7"
@@ -2131,6 +2539,12 @@ dependencies = [
"syn",
]
[[package]]
name = "vcpkg"
version = "0.2.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426"
[[package]]
name = "version_check"
version = "0.9.5"
@@ -2147,6 +2561,15 @@ dependencies = [
"winapi-util",
]
[[package]]
name = "want"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e"
dependencies = [
"try-lock",
]
[[package]]
name = "wasi"
version = "0.11.1+wasi-snapshot-preview1"
@@ -2291,9 +2714,9 @@ checksum = "6844ee5416b285084d3d3fffd743b925a6c9385455f64f6d4fa3031c4c2749a9"
dependencies = [
"windows-implement",
"windows-interface",
"windows-link",
"windows-result",
"windows-strings",
"windows-link 0.2.0",
"windows-result 0.4.0",
"windows-strings 0.5.0",
]
[[package]]
@@ -2318,19 +2741,54 @@ dependencies = [
"syn",
]
[[package]]
name = "windows-link"
version = "0.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a"
[[package]]
name = "windows-link"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "45e46c0661abb7180e7b9c281db115305d49ca1709ab8242adf09666d2173c65"
[[package]]
name = "windows-registry"
version = "0.5.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5b8a9ed28765efc97bbc954883f4e6796c33a06546ebafacbabee9696967499e"
dependencies = [
"windows-link 0.1.3",
"windows-result 0.3.4",
"windows-strings 0.4.2",
]
[[package]]
name = "windows-result"
version = "0.3.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "56f42bd332cc6c8eac5af113fc0c1fd6a8fd2aa08a0119358686e5160d0586c6"
dependencies = [
"windows-link 0.1.3",
]
[[package]]
name = "windows-result"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7084dcc306f89883455a206237404d3eaf961e5bd7e0f312f7c91f57eb44167f"
dependencies = [
"windows-link",
"windows-link 0.2.0",
]
[[package]]
name = "windows-strings"
version = "0.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "56e6c93f3a0c3b36176cb1327a4958a0353d5d166c2a35cb268ace15e91d3b57"
dependencies = [
"windows-link 0.1.3",
]
[[package]]
@@ -2339,7 +2797,7 @@ version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7218c655a553b0bed4426cf54b20d7ba363ef543b52d515b3e48d7fd55318dda"
dependencies = [
"windows-link",
"windows-link 0.2.0",
]
[[package]]
@@ -2366,7 +2824,7 @@ version = "0.61.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6f109e41dd4a3c848907eb83d5a42ea98b3769495597450cf6d153507b166f0f"
dependencies = [
"windows-link",
"windows-link 0.2.0",
]
[[package]]
@@ -2514,6 +2972,12 @@ dependencies = [
"synstructure",
]
[[package]]
name = "zeroize"
version = "1.8.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0"
[[package]]
name = "zerotrie"
version = "0.2.2"

View File

@@ -1,4 +1,4 @@
use pmoupnp::{mediarenderer::avtransport::actions::{SETAVTRANSPORTURI}, server::{
use pmoupnp::{mediarenderer::avtransport::{actions::SETAVTRANSPORTURI, AVTTRANSPORT}, server::{
logs::{log_dump, log_sse, LogState, SseLayer}, ServerBuilder, Webapp
}, UpnpObject}; // ton module pmoupnp::server
use tracing_subscriber::Registry;
@@ -47,7 +47,8 @@ async fn main() {
server.add_redirect("/", "/app").await;
info!("{}",SETAVTRANSPORTURI.to_markdown());
info!("{}",AVTTRANSPORT.to_markdown());
info!("{}",AVTTRANSPORT.scpd_xml());
server.start().await;
server.wait().await;

9290
pmo_src.txt Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -15,7 +15,7 @@ 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 = { version = "1.35", features = ["rt-multi-thread", "macros", "sync", "time", "signal"] }
tokio-stream = "0.1"
futures-util = "0.3"
serde = { version = "1.0", features = ["derive"] }
@@ -37,3 +37,4 @@ 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"

View File

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

View File

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

View File

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

View File

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

View File

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

@@ -1,25 +1,32 @@
#!/bin/bash
cat << EOF
Est-ce que ces packages te semblent fonctionnel.
Il ne s'agit pas de l'enrichir de nouvelle fonctionnalité, ni de l'optimiser d'avantage. Juste de le finaliser en detectant vrais bug. Ce package doit être thread safe, uniquement stéréo.
Ne signale que les bugs conduisant à un disfonctionnement
Regénère des version complètes et corrigées des fichiers nécessaires
EOF
echo ============== Debut des sources des packages ===============
echo "# Debut des sources des crates "
echo
for package in $*; do
find $package -type f \( -name '*.rs' -o -name '*.toml' \) -print0 |
while IFS= read -r -d '' file; do
echo "------- $file ------"
find $package -type f \( -name '*.rs' -o -name '*.go' \
-o -name '*.js' -o -name '*.ts' \
-o -name '*.vue' -o -name '*.css' \
-o -name '*.toml' \) -print \
| grep -v 'webapp/[^s]' \
| while read -r file; do
case "$file" in
*.rs) language="rust" ;;
*.go) language="go" ;;
*.js) lanquage="javascript" ;;
*.ts) language="typescript" ;;
*.vue) language="vue" ;;
*.css) language="css" ;;
*.toml) language="toml" ;;
*) language="" ;;
esac
echo "## fichier: \`$file\`"
echo
echo "\`\`\`$language"
cat $file
echo "-----------------"
echo "\`\`\`"
echo
done
done
echo ============== Fin des sources des packages ===============
echo ============== Analyse des bugs par chatgpt ===============