async_resol_vbus/
device_information.rs

1use std::{net::SocketAddr, time::Duration};
2
3use async_std::{
4    net::{Shutdown, TcpStream},
5    prelude::*,
6};
7
8use crate::error::Result;
9
10/// A struct containing information about a VBus-over-TCP device.
11#[derive(Debug, Clone)]
12pub struct DeviceInformation {
13    /// The `SocketAddr` of the web server.
14    pub address: SocketAddr,
15
16    /// The vendor of the device.
17    pub vendor: Option<String>,
18
19    /// The product name of the device.
20    pub product: Option<String>,
21
22    /// The serial number of the device.
23    pub serial: Option<String>,
24
25    /// The firmware version of the device.
26    pub version: Option<String>,
27
28    /// The firmware build of the device.
29    pub build: Option<String>,
30
31    /// The user-chosen name of the device.
32    pub name: Option<String>,
33
34    /// The comma separated list of features supported by the device.
35    pub features: Option<String>,
36}
37
38impl DeviceInformation {
39    /// Fetch and parse the information from a VBus-over-TCP device.
40    ///
41    /// This function performs a web request to the `/cgi-bin/get_resol_device_information`
42    /// endpoint and tries to parse the resulting information into a `DeviceInformation`
43    /// instance.
44    ///
45    /// # Examples
46    ///
47    /// ```no_run
48    /// # fn main() -> async_resol_vbus::Result<()> { async_std::task::block_on(async {
49    /// #
50    /// use async_resol_vbus::DeviceInformation;
51    ///
52    /// let address = "192.168.5.217:80".parse()?;
53    /// let duration = std::time::Duration::from_millis(2000);
54    /// let device = DeviceInformation::fetch(address, duration).await?;
55    /// assert_eq!(address, device.address);
56    /// #
57    /// # Ok(()) }) }
58    /// ```
59    pub async fn fetch(addr: SocketAddr, timeout: Duration) -> Result<DeviceInformation> {
60        let (buf, len) = async_std::io::timeout(timeout, async {
61            let mut stream = TcpStream::connect(addr).await?;
62
63            stream
64                .write_all(b"GET /cgi-bin/get_resol_device_information HTTP/1.0\r\n\r\n")
65                .await?;
66
67            stream.shutdown(Shutdown::Write)?;
68
69            let mut buf = Vec::with_capacity(1024);
70            let len = stream.read_to_end(&mut buf).await?;
71
72            Ok((buf, len))
73        })
74        .await?;
75
76        let buf = &buf[0..len];
77
78        let body_idx = {
79            let mut body_idx = None;
80
81            let mut idx = 0;
82            while idx < len - 4 {
83                if buf[idx] != 13 {
84                    // nop
85                } else if buf[idx + 1] != 10 {
86                    // nop
87                } else if buf[idx + 2] != 13 {
88                    // nop
89                } else if buf[idx + 3] != 10 {
90                    // nop
91                } else {
92                    body_idx = Some(idx + 4);
93                    break;
94                }
95
96                idx += 1;
97            }
98
99            match body_idx {
100                Some(idx) => idx,
101                None => return Err("No HTTP header separator found".into()),
102            }
103        };
104
105        let body_bytes = &buf[body_idx..];
106        let body = std::str::from_utf8(body_bytes)?;
107
108        DeviceInformation::parse(addr, body)
109    }
110
111    /// Parse the information of a VBus-over-TCP device.
112    ///
113    /// # Examples
114    ///
115    /// ```
116    /// # fn main() -> async_resol_vbus::Result<()> { async_std::task::block_on(async {
117    /// #
118    /// use async_resol_vbus::DeviceInformation;
119    ///
120    /// let address = "192.168.5.217:80".parse()?;
121    /// let string = "vendor = \"RESOL\"\nproduct = \"KM2\"\n...";
122    /// let device = DeviceInformation::parse(address, string)?;
123    /// assert_eq!(address, device.address);
124    /// assert_eq!("RESOL", device.vendor.as_ref().unwrap());
125    /// assert_eq!("KM2", device.product.as_ref().unwrap());
126    /// #
127    /// # Ok(()) }) }
128    /// ```
129    pub fn parse(address: SocketAddr, s: &str) -> Result<DeviceInformation> {
130        #[derive(PartialEq)]
131        enum Phase {
132            InKey,
133            WaitingForEquals,
134            WaitingForValueStartQuote,
135            InValue,
136            AfterValueEndQuote,
137            Malformed,
138        }
139
140        let mut vendor = None;
141        let mut product = None;
142        let mut serial = None;
143        let mut version = None;
144        let mut build = None;
145        let mut name = None;
146        let mut features = None;
147
148        for line in s.lines() {
149            let key_start_idx = 0;
150            let mut key_end_idx = 0;
151            let mut value_start_idx = 0;
152            let mut value_end_idx = 0;
153            let mut phase = Phase::InKey;
154
155            for (idx, c) in line.char_indices() {
156                let is_word_char = match c {
157                    '0'..='9' | 'A'..='Z' | 'a'..='z' | '_' => true,
158                    _ => false,
159                };
160
161                match phase {
162                    Phase::InKey => {
163                        if !is_word_char {
164                            key_end_idx = idx;
165                            phase = if c == '=' {
166                                Phase::WaitingForValueStartQuote
167                            } else {
168                                Phase::WaitingForEquals
169                            };
170                        }
171                    }
172                    Phase::WaitingForEquals => {
173                        if c == '=' {
174                            phase = Phase::WaitingForValueStartQuote;
175                        } else {
176                            phase = Phase::Malformed;
177                        }
178                    }
179                    Phase::WaitingForValueStartQuote => {
180                        if c == '"' {
181                            value_start_idx = idx + 1;
182                            phase = Phase::InValue;
183                        } else if is_word_char {
184                            phase = Phase::Malformed;
185                        }
186                    }
187                    Phase::InValue => {
188                        if c == '"' {
189                            value_end_idx = idx;
190                            phase = Phase::AfterValueEndQuote;
191                        }
192                    }
193                    Phase::AfterValueEndQuote => phase = Phase::Malformed,
194                    Phase::Malformed => {}
195                }
196            }
197
198            if phase == Phase::AfterValueEndQuote {
199                let key = &line[key_start_idx..key_end_idx];
200                let value = &line[value_start_idx..value_end_idx];
201
202                if key.eq_ignore_ascii_case("vendor") {
203                    vendor = Some(value.into());
204                } else if key.eq_ignore_ascii_case("product") {
205                    product = Some(value.into());
206                } else if key.eq_ignore_ascii_case("serial") {
207                    serial = Some(value.into());
208                } else if key.eq_ignore_ascii_case("version") {
209                    version = Some(value.into());
210                } else if key.eq_ignore_ascii_case("build") {
211                    build = Some(value.into());
212                } else if key.eq_ignore_ascii_case("name") {
213                    name = Some(value.into());
214                } else if key.eq_ignore_ascii_case("features") {
215                    features = Some(value.into());
216                } else {
217                    // unknown key...
218                }
219            }
220        }
221
222        Ok(DeviceInformation {
223            address,
224            vendor,
225            product,
226            serial,
227            version,
228            build,
229            name,
230            features,
231        })
232    }
233}
234
235#[cfg(test)]
236mod tests {
237    use async_std::net::{SocketAddr, TcpListener};
238
239    use super::*;
240
241    #[test]
242    fn test() -> Result<()> {
243        async_std::task::block_on(async {
244            let web_addr = "127.0.0.1:0".parse::<SocketAddr>()?;
245            let web_socket = TcpListener::bind(web_addr).await?;
246            let web_addr = web_socket.local_addr()?;
247
248            let web_future = async_std::task::spawn::<_, Result<()>>(async move {
249                loop {
250                    let (mut stream, _) = web_socket.accept().await?;
251
252                    let mut buf = Vec::new();
253                    stream.read_to_end(&mut buf).await?;
254
255                    let response = b"HTTP/1.0 200 OK\r\n\r\nvendor = \"RESOL\"\r\nproduct = \"DL2\"\r\nserial = \"001E66xxxxxx\"\r\nversion = \"2.2.0\"\r\nbuild = \"rc1\"\r\nname = \"DL2-001E66xxxxxx\"\r\nfeatures = \"vbus,dl2\"\r\n";
256                    stream.write_all(response).await?;
257                    drop(stream);
258                }
259            });
260
261            let fetch_future = async_std::task::spawn::<_, Result<()>>(async move {
262                let device = DeviceInformation::fetch(web_addr, Duration::from_millis(100)).await?;
263
264                assert_eq!(Some("RESOL"), device.vendor.as_ref().map(|s| s.as_str()));
265                assert_eq!(Some("DL2"), device.product.as_ref().map(|s| s.as_str()));
266                assert_eq!(
267                    Some("001E66xxxxxx"),
268                    device.serial.as_ref().map(|s| s.as_str())
269                );
270                assert_eq!(Some("2.2.0"), device.version.as_ref().map(|s| s.as_str()));
271                assert_eq!(Some("rc1"), device.build.as_ref().map(|s| s.as_str()));
272                assert_eq!(
273                    Some("DL2-001E66xxxxxx"),
274                    device.name.as_ref().map(|s| s.as_str())
275                );
276                assert_eq!(
277                    Some("vbus,dl2"),
278                    device.features.as_ref().map(|s| s.as_str())
279                );
280
281                Ok(())
282            });
283
284            fetch_future.await?;
285            drop(web_future);
286
287            Ok(())
288        })
289    }
290}