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, }
}
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 })
}