use std::net::{TcpListener, TcpStream, UdpSocket};
use std::time::Duration;
use crate::error::{OpenBciError, Result};
pub fn local_ip_for(remote_ip: &str) -> Result<String> {
let sock = UdpSocket::bind("0.0.0.0:0")?;
sock.connect(format!("{}:80", remote_ip))?;
let addr = sock.local_addr()?;
Ok(addr.ip().to_string())
}
pub fn discover_wifi_shield(timeout: Duration) -> String {
let default_ip = "192.168.4.1".to_string();
let Ok(sock) = UdpSocket::bind("0.0.0.0:0") else { return default_ip; };
if sock.set_read_timeout(Some(Duration::from_secs(3))).is_err() { return default_ip; }
if sock.set_broadcast(true).is_err() { return default_ip; }
let msearch = concat!(
"M-SEARCH * HTTP/1.1\r\n",
"Host: 239.255.255.250:1900\r\n",
"MAN: ssdp:discover\r\n",
"ST: urn:schemas-upnp-org:device:Basic:1\r\n",
"MX: 3\r\n\r\n\r\n",
);
if sock.send_to(msearch.as_bytes(), "239.255.255.250:1900").is_err() {
return default_ip;
}
let deadline = std::time::Instant::now() + timeout;
let mut buf = [0u8; 512];
while std::time::Instant::now() < deadline {
let Ok((n, _)) = sock.recv_from(&mut buf) else { break };
let resp = String::from_utf8_lossy(&buf[..n]);
if let Some(ip) = parse_ssdp_location_ip(&resp) {
return ip;
}
}
default_ip
}
fn parse_ssdp_location_ip(resp: &str) -> Option<String> {
for line in resp.lines() {
let low = line.to_lowercase();
if low.starts_with("location:") {
if let Some(start) = line.find("http://") {
let after_scheme = &line[start + 7..];
let end = after_scheme
.find(|c: char| c == '/' || c.is_whitespace())
.unwrap_or(after_scheme.len());
let candidate = &after_scheme[..end];
if candidate.contains('.') {
return Some(candidate.to_string());
}
}
}
}
None
}
#[derive(Debug, Clone)]
pub struct WifiShieldConfig {
pub shield_ip: String,
pub local_port: u16,
pub http_timeout: u64,
}
impl Default for WifiShieldConfig {
fn default() -> Self {
Self {
shield_ip: String::new(),
local_port: 3000,
http_timeout: 10,
}
}
}
pub fn connect_wifi_shield(config: &mut WifiShieldConfig) -> Result<TcpStream> {
if config.shield_ip.is_empty() {
config.shield_ip = discover_wifi_shield(Duration::from_secs(config.http_timeout));
log::info!("WiFi shield discovered at {}", config.shield_ip);
}
let ip = &config.shield_ip;
let local_ip = local_ip_for(ip)?;
log::info!("Local IP for WiFi communication: {}", local_ip);
let listener = TcpListener::bind(format!("{}:{}", local_ip, config.local_port))?;
listener.set_nonblocking(false)?;
let accept_timeout = Duration::from_secs(config.http_timeout);
let board_url = format!("http://{}/board", ip);
ureq::get(&board_url)
.timeout(Duration::from_secs(config.http_timeout))
.call()
.map_err(|e| OpenBciError::Wifi(e.to_string()))?;
let tcp_url = format!("http://{}/tcp", ip);
let body = serde_json::json!({
"ip": local_ip,
"port": config.local_port,
"output": "raw",
"delimiter": true,
"latency": 10000
});
ureq::post(&tcp_url)
.timeout(Duration::from_secs(config.http_timeout))
.send_json(body)
.map_err(|e| OpenBciError::Wifi(e.to_string()))?;
listener.set_nonblocking(true)?;
let deadline = std::time::Instant::now() + accept_timeout;
loop {
match listener.accept() {
Ok((stream, _addr)) => {
log::info!("WiFi shield connected to our TCP server");
stream.set_nodelay(true)?;
stream.set_read_timeout(Some(Duration::from_secs(5)))?;
return Ok(stream);
}
Err(ref e) if e.kind() == std::io::ErrorKind::WouldBlock => {
if std::time::Instant::now() >= deadline {
return Err(OpenBciError::Timeout);
}
std::thread::sleep(Duration::from_millis(100));
}
Err(e) => return Err(e.into()),
}
}
}
pub fn send_wifi_command(shield_ip: &str, cmd: &str, http_timeout: u64) -> Result<()> {
let url = format!("http://{}/command", shield_ip);
let body = serde_json::json!({ "command": cmd });
ureq::post(&url)
.timeout(Duration::from_secs(http_timeout))
.send_json(body)
.map_err(|e| OpenBciError::Wifi(e.to_string()))?;
Ok(())
}
pub fn wifi_start_stream(shield_ip: &str, http_timeout: u64) -> Result<()> {
let url = format!("http://{}/stream/start", shield_ip);
ureq::get(&url)
.timeout(Duration::from_secs(http_timeout))
.call()
.map_err(|e| OpenBciError::Wifi(e.to_string()))?;
Ok(())
}
pub fn wifi_stop_stream(shield_ip: &str, http_timeout: u64) -> Result<()> {
let url = format!("http://{}/stream/stop", shield_ip);
ureq::get(&url)
.timeout(Duration::from_secs(http_timeout))
.call()
.map_err(|e| OpenBciError::Wifi(e.to_string()))?;
Ok(())
}