diff --git a/README.md b/README.md index d62244b..cf1aece 100644 --- a/README.md +++ b/README.md @@ -20,7 +20,7 @@ Cargo.toml: onvif = { git = "https://github.com/lumeohq/onvif-rs" } ``` -## Troubleshooting +## Troubleshooting If you have an issue with OpenSSL build under Ubuntu, perform the following actions: @@ -41,6 +41,7 @@ cargo run --example discovery ``` To [inspect and control a camera](onvif/examples/camera.rs): + ```shell script cargo run --example camera -- help @@ -53,6 +54,12 @@ cargo run --example camera -- set-hostname \ cargo run --example camera -- get-stream-uris --uri=http://192.168.0.2:8000 ``` +To [pull events](onvif/examples/event.rs) from a camera, adjust credentials in event.rs and run: + +```shell script +cargo run --example event +``` + ## Dependencies - XSD -> Rust code generation: [xsd-parser-rs](https://github.com/lumeohq/xsd-parser-rs) diff --git a/onvif/Cargo.toml b/onvif/Cargo.toml index 31e9454..49a39a6 100644 --- a/onvif/Cargo.toml +++ b/onvif/Cargo.toml @@ -40,3 +40,4 @@ futures-util = "0.3.8" structopt = "0.3.21" tokio = { version = "1.0.1", features = ["full"] } tracing-subscriber = "0.2.20" +b_2 = {path = "../wsdl_rs/b_2"} diff --git a/onvif/examples/event.rs b/onvif/examples/event.rs new file mode 100644 index 0000000..2929df0 --- /dev/null +++ b/onvif/examples/event.rs @@ -0,0 +1,105 @@ +// This example pulls messages related to the RuleEngine topic. +// RuleEngine topic consists of events related to motion detection. +// Tested on Dahua, uniview, reolink and axis ip cameras. +// Don't forget to set the camera's IP address, username and password. + +use onvif::soap::client::{ClientBuilder, Credentials}; +use schema::event::{self, CreatePullPointSubscription, PullMessages}; +use url::Url; + +#[derive(Debug, Clone)] +pub struct Camera { + pub device_service_url: String, + pub username: String, + pub password: String, + pub event_service_url: String, +} + +impl Default for Camera { + fn default() -> Self { + Camera { + device_service_url: "http://192.168.1.100/onvif/device_service".to_string(), + username: "admin".to_string(), + password: "admin".to_string(), + event_service_url: "http://192.168.1.100/onvif/event_service".to_string(), + } + } +} + +#[tokio::main] +async fn main() { + let camera_ip = "192.168.1.50"; + let username = "admin"; + let password = "admin"; + + let camera: Camera = Camera { + device_service_url: format!("http://{}/onvif/device_service", camera_ip), + username: username.to_string(), + password: password.to_string(), + event_service_url: format!("http://{}/onvif/event_service", camera_ip), + }; + + let creds: Credentials = Credentials { + username: camera.username.to_string(), + password: camera.password.to_string(), + }; + let event_client = ClientBuilder::new(&Url::parse(&camera.event_service_url).unwrap()) + .credentials(Some(creds)) + .build(); + let create_pull_sub_request = CreatePullPointSubscription { + initial_termination_time: None, + filter: Some(b_2::FilterType { + topic_expression: Some(b_2::TopicExpressionType { + dialect: "http://www.onvif.org/ver10/tev/topicExpression/ConcreteSet".to_string(), + inner_text: "tns1:RuleEngine//.".to_string(), + }), + }), + subscription_policy: None, + }; + let create_pull_puint_sub_response = + event::create_pull_point_subscription(&event_client, &create_pull_sub_request).await; + let camera_sub = match create_pull_puint_sub_response { + Ok(sub) => sub, + Err(e) => { + println!("Error: {:?}", e); + return; + } + }; + + let uri: Url = Url::parse(&camera_sub.subscription_reference.address).unwrap(); + let creds: Credentials = Credentials { + username: camera.username.to_string(), + password: camera.password.to_string(), + }; + let pull_msg_client = ClientBuilder::new(&uri) + .credentials(Some(creds)) + .auth_type(onvif::soap::client::AuthType::Digest) + .build(); + let pull_messages_request = PullMessages { + message_limit: 256, + timeout: xsd_types::types::Duration { + seconds: 1.0, + ..Default::default() + }, + }; + + // Main Loop + loop { + let pull_messages_response = + event::pull_messages(&pull_msg_client, &pull_messages_request).await; + let msg = match pull_messages_response { + Ok(msg) => msg, + Err(e) => { + println!("Error: {:?}", e); + continue; + } + }; + if !msg.notification_message.is_empty() { + println!("Notification Message: {:?}", msg.notification_message[0]); + } else { + println!("No new notification message"); + } + + tokio::time::sleep(tokio::time::Duration::from_secs(3)).await; + } +} diff --git a/schema/src/tests.rs b/schema/src/tests.rs index 1552da2..a353ac4 100644 --- a/schema/src/tests.rs +++ b/schema/src/tests.rs @@ -20,6 +20,7 @@ impl transport::Transport for FakeTransport { } #[test] +#[cfg(feature = "devicemgmt")] fn basic_deserialization() { let response = r#" @@ -66,6 +67,7 @@ fn basic_deserialization() { assert_eq!(de.utc_date_time.as_ref().unwrap().time.second, 9); } +#[cfg(feature = "devicemgmt")] #[test] fn basic_serialization() { let expected = r#" @@ -327,6 +329,7 @@ fn duration_deserialization() { } #[tokio::test] +#[cfg(feature = "devicemgmt")] async fn operation_get_system_date_and_time() { let req: devicemgmt::GetSystemDateAndTime = Default::default(); @@ -369,6 +372,7 @@ async fn operation_get_system_date_and_time() { } #[tokio::test] +#[cfg(feature = "devicemgmt")] async fn operation_get_device_information() { let req: devicemgmt::GetDeviceInformation = Default::default(); @@ -693,3 +697,119 @@ fn media2_configs_name_serialization() { type_of(&media2::GetAudioDecoderConfigurationOptions::default()) ); } + +#[tokio::test] +#[cfg(feature = "event")] +async fn operation_pull_messages() { + let req: event::PullMessages = Default::default(); + + let transport = FakeTransport { + response: r#" + + + 2023-09-28T16:01:15Z + + + 2023-09-28T16:11:15Z + + + + tns1:RuleEngine/CellMotionDetector/Motion + + + + + + + + + + + + + + + + "# + .into(), + }; + + let response = event::pull_messages(&transport, &req).await; + + let resp = match response { + Ok(resp) => resp, + Err(err) => panic!("Error: {:?}", err), + }; + + assert_eq!( + resp.notification_message[0].message.msg.source.simple_item[0].name, + "VideoSourceConfigurationToken" + ); + assert_eq!( + resp.notification_message[0].message.msg.source.simple_item[0].value, + "00000" + ); + assert_eq!( + resp.notification_message[0].message.msg.data.simple_item[0].name, + "IsMotion" + ); + assert_eq!( + resp.notification_message[0].message.msg.data.simple_item[0].value, + "false" + ); +} + +#[tokio::test] +#[cfg(feature = "event")] +async fn operation_create_pullpoint_subscription() { + let req: event::CreatePullPointSubscription = Default::default(); + + let transport = FakeTransport { + response: r#" + + + + http://192.168.88.108/onvif/Subscription?Idx=5 + + + + 2023-09-28T16:01:15Z + + + 2023-09-28T16:11:15Z + + + "# + .into(), + }; + + let resp = event::create_pull_point_subscription(&transport, &req) + .await + .unwrap(); + + assert_eq!( + resp.subscription_reference.address, + "http://192.168.88.108/onvif/Subscription?Idx=5" + ); +} diff --git a/wsdl_rs/b_2/src/lib.rs b/wsdl_rs/b_2/src/lib.rs index b312162..ba72cd8 100644 --- a/wsdl_rs/b_2/src/lib.rs +++ b/wsdl_rs/b_2/src/lib.rs @@ -27,6 +27,9 @@ impl Validate for QueryExpressionType {} pub struct TopicExpressionType { #[yaserde(attribute, rename = "Dialect")] pub dialect: String, + + #[yaserde(text)] + pub inner_text: String, } impl Validate for TopicExpressionType {} @@ -36,7 +39,10 @@ impl Validate for TopicExpressionType {} prefix = "wsnt", namespace = "wsnt: http://docs.oasis-open.org/wsn/b-2" )] -pub struct FilterType {} +pub struct FilterType { + #[yaserde(prefix = "wsnt", rename = "TopicExpression")] + pub topic_expression: Option, +} impl Validate for FilterType {} @@ -131,15 +137,54 @@ pub struct NotificationMessageHolderType { impl Validate for NotificationMessageHolderType {} +#[derive(Default, PartialEq, Debug, YaSerialize, YaDeserialize)] +#[yaserde(prefix = "tt", namespace = "tt: http://www.onvif.org/ver10/schema")] +pub struct SimpleItemType { + // Item name. + #[yaserde(attribute, rename = "Name")] + pub name: String, + + // Item value. The type is defined in the corresponding description. + #[yaserde(attribute, rename = "Value")] + pub value: String, +} + pub mod notification_message_holder_type { use super::*; + #[derive(Default, PartialEq, Debug, YaSerialize, YaDeserialize)] + #[yaserde(prefix = "tt", namespace = "tt: http://www.onvif.org/ver10/schema")] + pub struct DataType { + #[yaserde(prefix = "tt", rename = "SimpleItem")] + pub simple_item: Vec, + } + + #[derive(Default, PartialEq, Debug, YaSerialize, YaDeserialize)] + #[yaserde(prefix = "tt", namespace = "tt: http://www.onvif.org/ver10/schema")] + pub struct SourceType { + #[yaserde(prefix = "tt", rename = "SimpleItem")] + pub simple_item: Vec, + } + + #[derive(Default, PartialEq, Debug, YaSerialize, YaDeserialize)] + #[yaserde(prefix = "tt", namespace = "tt: http://www.onvif.org/ver10/schema")] + pub struct MessageTypeInner { + #[yaserde(prefix = "tt", rename = "Source")] + pub source: SourceType, + + #[yaserde(prefix = "tt", rename = "Data")] + pub data: DataType, + } + #[derive(Default, PartialEq, Debug, YaSerialize, YaDeserialize)] #[yaserde( prefix = "wsnt", namespace = "wsnt: http://docs.oasis-open.org/wsn/b-2" )] - pub struct MessageType {} + pub struct MessageType { + #[yaserde(prefix = "tt", rename = "Message")] + pub msg: MessageTypeInner, + } impl Validate for MessageType {} } diff --git a/wsdl_rs/ws_addr/src/lib.rs b/wsdl_rs/ws_addr/src/lib.rs index 24546b8..074a901 100644 --- a/wsdl_rs/ws_addr/src/lib.rs +++ b/wsdl_rs/ws_addr/src/lib.rs @@ -14,7 +14,7 @@ pub type EndpointReference = EndpointReferenceType; )] pub struct EndpointReferenceType { #[yaserde(prefix = "tns", rename = "Address")] - pub address: AttributedURIType, + pub address: String, #[yaserde(prefix = "tns", rename = "ReferenceParameters")] pub reference_parameters: Option,