glow_control_lib/util/
discovery.rs

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    // Include other fields from the response as needed
25}
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    /**
59    The auth-token if the device was authenticated.
60
61    If the device was found by a search, the auth_token must
62    be generated to pull info from the device, and then that new token must be used.
63
64    **Too frequent token generation _can_ lead to erroneous behavior.**
65    */
66    #[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
92/**
93The response from the discovery request.
94
95It includes the newly found devices, and the existing devices
96(if the `existing_devices` argument has been supplied to
97[`Discovery::find_new_devices`]).
98*/
99pub struct ResponseNewExisting {
100    /// Newly found devices.
101    pub new_devices: HashSet<DeviceIdentifier>,
102
103    /**
104    Existing devices, which have been re-discovered.
105    Used by [`Self::find_new_devices`] if the `existing_devices` argument  has been supplied.
106     */
107    pub existing_devices: HashSet<DeviceIdentifier>,
108}
109
110impl Discovery {
111    pub fn decode_discovery_response(data: &[u8]) -> Option<DiscoveryResponse> {
112        // Check if the response is at least 8 bytes long and ends with a zero byte
113        if data.len() < 8 || *data.last().unwrap() != 0 {
114            return None;
115        }
116
117        // Check if the response contains "OK" status
118        if data[4..6] != [b'O', b'K'] {
119            return None;
120        }
121
122        // Extract the IP address from the response
123        let ip_address = Ipv4Addr::new(data[3], data[2], data[1], data[0]);
124
125        // Extract the device ID from the response, which starts at byte 6 and ends before the last byte
126        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        // Return the struct with the IP address object and device ID
133        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    /**
148    Finds new devices on the network.
149
150    Skips devices which are already in `existing_devices` and reports them in [`ResponseNewExisting::existing_devices`].
151    Newly found devices are reported in [`ResponseNewExisting::new_devices`].
152     */
153    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                        /*
182                        Look first if the device (i.e. address) is already in `discovered_devices`,
183                        and skip it, if it is, that makes the discovery process faster.
184
185                        It also saves the needless re-authentication of the device, which saves additional time.
186
187                        Cause: Some devices may respond multiple times for one request, to make sure the listener
188                               gets it.
189                        */
190                        // Search if `discovered_devices` matches a `discovery_response`:
191                        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                        // Search if `existing_devices` matches a `discovery_response`:
198                        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                                // Fetch the LED count from a high control interface
212                                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                                    // Reuse the auth token from the high control interface to speed up authentication.
227                                    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    /// Returns if `discovery_response` is in the Set of `devices`.
253    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        // Determine the maximum width for each column
302        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        // Print the header with appropriate spacing
326        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, // Add some padding
334            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        // Print the separator line
341        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        // Print each device entry with appropriate spacing
356        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}