~liz/yooper

04488b7ad84d170d61efe3dd79f0c9c2569918af — Ellie Frost 2 years ago c4cb255
Add UniqueServiceName type
M yooper/src/discovery.rs => yooper/src/discovery.rs +12 -10
@@ 14,7 14,7 @@ use tokio_util::udp::UdpFramed;
use uuid::{self, Uuid};

use crate::{
    ssdp::message::{Codec, MSearch, Message, SearchTarget},
    ssdp::message::{Codec, MSearch, Message, SearchTarget, UniqueServiceName},
    Error,
};



@@ 38,12 38,14 @@ pub struct Device {
    pub address: SocketAddr,
    /// A list of discovered services
    pub services: Vec<Service>,
    /// the location to retrieve more service information
    pub location: String,
}

/// A Service represents a running service on a device
pub struct Service {
    /// Unique Service Name identifies a unique instance of a device or service.
    pub service_name: String,
    pub service_name: UniqueServiceName,
    /// the search target you would use to describe this service
    pub target: SearchTarget,
}


@@ 93,7 95,7 @@ impl Discovery {
    /// Will block for n secs then return a list of discovered devices
    /// secs should be between 1 and 5 to comply with
    pub async fn find(&mut self, secs: u8) -> Result<Vec<Device>, Error> {
        let mut map: HashMap<SocketAddr, Device> = HashMap::new();
        let mut map: HashMap<String, Device> = HashMap::new();
        self.start_search(secs).await?;

        let mut delay = time::delay_for(Duration::from_secs(secs.into()));


@@ 104,16 106,16 @@ impl Discovery {
                    match msg {
                        Some(Err(e)) => eprintln!("Error receiving: {:?}", e),
                        Some(Ok((Message::SearchResponse(sr), address))) => {
                            let device = map.entry(address).or_insert(Device {
                                address,
                            let uuid = sr.unique_service_name.uuid.clone();
                            map.entry(uuid).or_insert(Device{
                                server: sr.server,
                                address,
                                services: Vec::new(),

                            });
                            device.services.push(Service{
                                location: sr.secure_location.unwrap_or(sr.location),
                            }).services.push(Service{
                                target: sr.target,
                                service_name: sr.unique_service_name
                            })
                                service_name: sr.unique_service_name,
                            });
                        }
                        _ => (),
                    }

M yooper/src/main.rs => yooper/src/main.rs +1 -1
@@ 17,7 17,7 @@ async fn main() -> Result<(), Error> {
    let mut discovery = Discovery::new().await?;

    for result in discovery.find(5).await? {
        println!("{} at {}", result.server, result.address);
        println!("{} at {}", result.server, result.location);
        for service in result.services {
            println!("∟ {:?}", service.target)
        }

M yooper/src/ssdp/message.rs => yooper/src/ssdp/message.rs +6 -8
@@ 6,7 6,7 @@ pub(self) mod types;
use crate::ssdp::packet::{FromHeaders, FromPacket, ToHeaders, ToPacket};
pub use codec::Codec;

pub use types::SearchTarget;
pub use types::{SearchTarget, UniqueServiceName};

#[derive(ToHeaders, FromHeaders, Debug, PartialEq, Default)]
pub struct MSearch {


@@ 25,7 25,7 @@ pub struct MSearch {
    // TODO: enum
    /// Field value contains Search Target.
    #[header("st")]
    pub target: types::SearchTarget,
    pub target: SearchTarget,
    /// Field value shall begin with the following “product tokens” (defined
    /// by HTTP/1.1). The first product token identifes the operating system in the form OS name/OS version, the
    /// second token represents the UPnP version and shall be UPnP/2.0, and the third token identifes the product


@@ 62,9 62,9 @@ pub struct Available {

    #[header("securelocation.upnp.org")]
    pub secure_location: Option<String>,
    // TODO: Enum

    #[header("nt")]
    pub notification_type: String,
    pub notification_type: SearchTarget,
    /// Field  value  shall begin  with  the  following  “product  tokens”
    /// (defined by HTTP/1.1). The first product token identifes the operating
    /// system in the form OSname/OSversion, the  second  token  represents


@@ 73,9 73,8 @@ pub struct Available {
    pub server: String,

    /// Identifies a unique instance of a device or service.
    // TODO: Enum
    #[header("usn")]
    pub unique_service_name: String,
    pub unique_service_name: UniqueServiceName,
    /// presents the boot instance of the device expressed according to a monotonically increasing value.
    #[header("bootid.upnp.org")]
    pub boot_id: Option<i32>,


@@ 114,9 113,8 @@ pub struct SearchResponse {
    pub target: types::SearchTarget,

    /// A unique service name for this particular service
    // TODO: Enum
    #[header("usn")]
    pub unique_service_name: String,
    pub unique_service_name: UniqueServiceName,

    /// presents the boot instance of the device expressed according to a monotonically increasing value.
    #[header("bootid.upnp.org")]

M yooper/src/ssdp/message/types.rs => yooper/src/ssdp/message/types.rs +39 -1
@@ 40,7 40,7 @@ impl FromStr for ManDiscover {
}

/// What kind of control point to search for
#[derive(PartialEq, Debug)]
#[derive(PartialEq, Debug, Hash, Clone)]
pub enum SearchTarget {
    /// Search for all devices and services
    All,


@@ 146,3 146,41 @@ impl Default for SearchTarget {
        Self::All
    }
}

#[derive(Default, PartialEq, Debug, Hash, Clone)]
pub struct UniqueServiceName {
    pub uuid: String,
    pub search_target: Option<SearchTarget>,
}

impl FromStr for UniqueServiceName {
    type Err = Error;

    fn from_str(s: &str) -> Result<Self, Self::Err> {
        match s.split("::").collect::<Vec<&str>>().as_slice() {
            [urn, st] if urn.starts_with("uuid:") => {
                let uuid = urn[5..].to_string();
                st.parse().map(|st| UniqueServiceName {
                    uuid,
                    search_target: Some(st),
                })
            }
            [urn] if urn.starts_with("uuid:") => Ok(UniqueServiceName {
                uuid: urn[5..].to_string(),
                search_target: None,
            }),
            _ => Err(Error::MalformedHeader("usn", s.to_owned())),
        }
    }
}

impl ToString for UniqueServiceName {
    fn to_string(&self) -> String {
        let out = format!("uuid:{}", self.uuid);
        if let Some(st) = &self.search_target {
            format!("{}::{}", out, st.to_string())
        } else {
            out
        }
    }
}