framesmith 0.1.0

A Rust library for controlling Samsung Frame TVs over the local network
Documentation
use std::net::{IpAddr, Ipv4Addr, SocketAddr};

use crate::Error;
use crate::types::DiscoveredTv;

const SSDP_MULTICAST_ADDR: Ipv4Addr = Ipv4Addr::new(239, 255, 255, 250);
const SSDP_PORT: u16 = 1900;
const SAMSUNG_DEVICE_TYPE: &str = "urn:samsung.com:device:RemoteControlReceiver:1";

pub async fn discover() -> Result<Vec<DiscoveredTv>, Error> {
    let socket =
        tokio::net::UdpSocket::bind(SocketAddr::new(IpAddr::V4(Ipv4Addr::UNSPECIFIED), 0)).await?;

    let search_request = format!(
        "M-SEARCH * HTTP/1.1\r\n\
         HOST: 239.255.255.250:1900\r\n\
         MAN: \"ssdp:discover\"\r\n\
         MX: 3\r\n\
         ST: {SAMSUNG_DEVICE_TYPE}\r\n\
         \r\n"
    );

    socket
        .send_to(
            search_request.as_bytes(),
            SocketAddr::new(IpAddr::V4(SSDP_MULTICAST_ADDR), SSDP_PORT),
        )
        .await?;

    let mut tvs = Vec::new();
    let mut buf = [0u8; 4096];

    loop {
        let result = tokio::time::timeout(
            std::time::Duration::from_secs(3),
            socket.recv_from(&mut buf),
        )
        .await;

        match result {
            Ok(Ok((len, addr))) => {
                let response = String::from_utf8_lossy(&buf[..len]);
                if let Some(tv) = parse_ssdp_response(&response, addr.ip())
                    && !tvs.iter().any(|t: &DiscoveredTv| t.ip() == tv.ip())
                {
                    tvs.push(tv);
                }
            }
            Ok(Err(e)) => return Err(e.into()),
            Err(_) => break, // timeout
        }
    }

    Ok(tvs)
}

fn parse_ssdp_response(response: &str, ip: IpAddr) -> Option<DiscoveredTv> {
    if !response.contains("Samsung") && !response.contains("samsung") {
        return None;
    }

    let mut model = String::new();
    let mut name = String::new();

    for line in response.lines() {
        let line = line.trim();
        if let Some(val) = line.strip_prefix("USN:") {
            name = val.trim().to_string();
        }
        if let Some(val) = line.strip_prefix("SERVER:") {
            model = val.trim().to_string();
        }
    }

    if name.is_empty() {
        name = format!("Samsung TV at {ip}");
    }

    Some(DiscoveredTv { name, ip, model })
}