1use std::cmp::max;
2use std::collections::HashSet;
3use std::fmt::{Display, Formatter};
4use std::net::Ipv4Addr;
5use std::time::Duration;
6
7use anyhow::Context;
8use log::{error, info};
9use serde::{Deserialize, Serialize};
10use tokio::net::UdpSocket;
11use tokio::time::{timeout, Instant};
12
13use derivative::Derivative;
14
15use crate::control_interface::ControlInterface;
16
17const PING_MESSAGE: &[u8] = b"\x01discover";
18const BROADCAST_ADDRESS: &str = "255.255.255.255:5555";
19
20#[derive(Deserialize, Debug)]
21pub struct GestaltResponse {
22 mac: String,
23 device_name: String,
24 }
26
27impl Display for GestaltResponse {
28 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
29 write!(f, "MAC: {}, Device Name: {}", self.mac, self.device_name)
30 }
31}
32
33#[derive(Debug, Hash, Eq, PartialEq)]
34pub struct DiscoveryResponse {
35 ip_address: Ipv4Addr,
36 device_id: String,
37}
38
39impl DiscoveryResponse {
40 pub fn new(ip_address: Ipv4Addr, device_id: String) -> Self {
41 DiscoveryResponse {
42 ip_address,
43 device_id,
44 }
45 }
46}
47
48#[derive(Derivative)]
49#[derivative(Hash, PartialEq, Eq, PartialOrd)]
50#[derive(Debug, Clone, Serialize)]
51pub struct DeviceIdentifier {
52 pub ip_address: Ipv4Addr,
53 pub device_id: String,
54 pub mac_address: String,
55 pub device_name: String,
56 pub led_count: u16,
57
58 #[derivative(Hash = "ignore", PartialEq = "ignore", PartialOrd = "ignore")]
67 pub auth_token: Option<String>,
68}
69
70impl DeviceIdentifier {
71 pub fn new(
72 ip_address: Ipv4Addr,
73 device_id: String,
74 mac_address: String,
75 device_name: String,
76 led_count: u16,
77 auth_token: Option<String>,
78 ) -> Self {
79 DeviceIdentifier {
80 ip_address,
81 device_id,
82 mac_address,
83 device_name,
84 led_count,
85 auth_token,
86 }
87 }
88}
89
90pub struct Discovery;
91
92pub struct ResponseNewExisting {
100 pub new_devices: HashSet<DeviceIdentifier>,
102
103 pub existing_devices: HashSet<DeviceIdentifier>,
108}
109
110impl Discovery {
111 pub fn decode_discovery_response(data: &[u8]) -> Option<DiscoveryResponse> {
112 if data.len() < 8 || *data.last().unwrap() != 0 {
114 return None;
115 }
116
117 if data[4..6] != [b'O', b'K'] {
119 return None;
120 }
121
122 let ip_address = Ipv4Addr::new(data[3], data[2], data[1], data[0]);
124
125 let device_id_bytes = &data[6..data.len() - 1];
127 let device_id = match std::str::from_utf8(device_id_bytes) {
128 Ok(v) => v.to_string(),
129 Err(_) => return None,
130 };
131
132 Some(DiscoveryResponse {
134 ip_address,
135 device_id,
136 })
137 }
138
139 pub async fn find_devices(
140 given_timeout: Duration,
141 ) -> anyhow::Result<HashSet<DeviceIdentifier>> {
142 Self::find_new_devices(given_timeout, None)
143 .await
144 .map(|devices: ResponseNewExisting| devices.new_devices)
145 }
146
147 pub async fn find_new_devices(
154 given_timeout: Duration,
155 existing_devices: Option<HashSet<DeviceIdentifier>>,
156 ) -> anyhow::Result<ResponseNewExisting> {
157 let socket = UdpSocket::bind("0.0.0.0:0").await?;
158 socket.set_broadcast(true)?;
159 socket.send_to(PING_MESSAGE, BROADCAST_ADDRESS).await?;
160
161 let mut discovered_devices = HashSet::<DeviceIdentifier>::new();
162 let mut buffer = [0; 1024];
163
164 let timeout_end = Instant::now() + given_timeout;
165
166 let mut found_existing_devices = HashSet::<DeviceIdentifier>::new();
167
168 loop {
169 if Instant::now() >= timeout_end {
170 break;
171 }
172
173 let remaining_time = timeout_end - Instant::now();
174 let result = timeout(remaining_time, socket.recv_from(&mut buffer)).await;
175
176 match result {
177 Ok(Ok((number_of_bytes, _src_addr))) => {
178 let received_data = &buffer[..number_of_bytes];
179 if let Some(discovery_response) = Self::decode_discovery_response(received_data)
180 {
181 if Self::find_discovered_device(&discovered_devices, &discovery_response)
192 .is_some()
193 {
194 info!("Found device {:?} again, skipping", discovery_response);
195 continue;
196 }
197 if let Some(existing_devices) = &existing_devices {
199 if let Some(exist) =
200 Self::find_discovered_device(existing_devices, &discovery_response)
201 {
202 found_existing_devices.insert(exist);
203 info!("Device {:?} isn't new, skipping", discovery_response);
204 continue;
205 }
206 }
207 info!("Found device: {:?}", discovery_response);
208 match Self::fetch_gestalt_info(discovery_response.ip_address).await {
209 Ok(gestalt_info) => {
210 info!("MAC address: {}", gestalt_info);
211 let high_control_interface = ControlInterface::new(
213 &discovery_response.ip_address.to_string(),
214 &gestalt_info.mac,
215 None,
216 )
217 .await?;
218 let led_count =
219 high_control_interface.get_device_info().number_of_led as u16;
220 let device = DeviceIdentifier::new(
221 discovery_response.ip_address,
222 discovery_response.device_id,
223 gestalt_info.mac,
224 gestalt_info.device_name,
225 led_count,
226 Some(high_control_interface.auth_token),
228 );
229 discovered_devices.insert(device);
230 }
231 Err(e) => eprintln!("Error fetching MAC address: {:?}", e),
232 }
233 }
234 }
235 Ok(Err(e)) => {
236 eprintln!("Failed to receive response: {}", e);
237 break;
238 }
239 Err(_) => {
240 eprintln!("Discovery time complete. If devices are missing, try increasing the search timeout.");
241 break;
242 }
243 }
244 }
245
246 Ok(ResponseNewExisting {
247 new_devices: discovered_devices,
248 existing_devices: found_existing_devices,
249 })
250 }
251
252 fn find_discovered_device(
254 devices: &HashSet<DeviceIdentifier>,
255 discovery_response: &DiscoveryResponse,
256 ) -> Option<DeviceIdentifier> {
257 let filtered: Vec<DeviceIdentifier> = devices
258 .iter()
259 .filter(|device_identifier: &&DeviceIdentifier| {
260 device_identifier.device_id == discovery_response.device_id
261 && device_identifier.ip_address == discovery_response.ip_address
262 })
263 .cloned()
264 .collect();
265 match filtered.len() {
266 0 => None,
267 1 => filtered.first().cloned(),
268 _ => {
269 error!(
270 "Found multiple devices with the same IP address {} and device ID {}",
271 discovery_response.ip_address, discovery_response.device_id
272 );
273 None
274 }
275 }
276 }
277
278 async fn fetch_gestalt_info(ip_address: Ipv4Addr) -> anyhow::Result<GestaltResponse> {
279 let url = format!("http://{}/xled/v1/gestalt", ip_address);
280 let client = reqwest::Client::new();
281 let response = client
282 .get(&url)
283 .send()
284 .await
285 .context("Failed to send request to device")?;
286
287 if response.status().is_success() {
288 let gestalt: GestaltResponse = response
289 .json()
290 .await
291 .context("Failed to parse JSON response")?;
292 Ok(gestalt)
293 } else {
294 Err(anyhow::anyhow!(
295 "Received non-success status code: {}",
296 response.status()
297 ))
298 }
299 }
300 pub fn pretty_print_devices(devices: &HashSet<DeviceIdentifier>) {
301 let max_ip_width = devices
303 .iter()
304 .map(|d| d.ip_address.to_string().len())
305 .max()
306 .unwrap_or(0);
307 let max_device_id_width = devices.iter().map(|d| d.device_id.len()).max().unwrap_or(0);
308 let max_mac_width = devices
309 .iter()
310 .map(|d| d.mac_address.len())
311 .max()
312 .unwrap_or(0);
313 let max_device_name_width = devices
314 .iter()
315 .map(|d| max(d.device_name.len(), 20))
316 .max()
317 .unwrap_or(0);
318
319 let max_led_count_width = devices
320 .iter()
321 .map(|d| d.led_count.to_string().len())
322 .max()
323 .unwrap_or(0);
324
325 println!(
327 "{:<ip_width$} {:<device_id_width$} {:<mac_width$} {:<device_name_width$} {:<led_count_width$}",
328 "IP Address",
329 "Device ID",
330 "MAC Address",
331 "Device Name",
332 "LED Count",
333 ip_width = max_ip_width + 2, device_id_width = max_device_id_width + 2,
335 mac_width = max_mac_width + 2,
336 device_name_width = max_device_name_width + 2,
337 led_count_width = max_led_count_width + 2,
338 );
339
340 println!(
342 "{:<ip_width$} {:<device_id_width$} {:<mac_width$} {:<device_name_width$} {:<led_count_width$}",
343 "-".repeat(max_ip_width),
344 "-".repeat(max_device_id_width),
345 "-".repeat(max_mac_width),
346 "-".repeat(max_device_name_width),
347 "-".repeat(max_led_count_width),
348 ip_width = max_ip_width + 2,
349 device_id_width = max_device_id_width + 2,
350 mac_width = max_mac_width + 2,
351 device_name_width = max_device_name_width + 2,
352 led_count_width = max_led_count_width + 2,
353 );
354
355 for device in devices {
357 println!(
358 "{:<ip_width$} {:<device_id_width$} {:<mac_width$} {:<device_name_width$} {:<led_count_width$}",
359 device.ip_address,
360 device.device_id,
361 device.mac_address,
362 device.device_name,
363 device.led_count,
364 ip_width = max_ip_width + 2,
365 device_id_width = max_device_id_width + 2,
366 mac_width = max_mac_width + 2,
367 device_name_width = max_device_name_width + 2,
368 led_count_width = max_led_count_width + 2,
369 );
370 }
371 }
372}