asic_rs/miners/factory/
mod.rs

1mod commands;
2mod hardware;
3mod model;
4mod traits;
5
6use anyhow::Result;
7use futures::future::FutureExt;
8use futures::{Stream, StreamExt, pin_mut, stream};
9use ipnet::IpNet;
10use rand::seq::SliceRandom;
11use reqwest::StatusCode;
12use reqwest::header::HeaderMap;
13use std::collections::HashSet;
14use std::net::IpAddr;
15use std::net::Ipv4Addr;
16use std::str::FromStr;
17use std::time::Duration;
18use tokio::net::TcpStream;
19use tokio::task::JoinSet;
20use tokio::time::timeout;
21
22use super::commands::MinerCommand;
23use super::util::{send_rpc_command, send_web_command};
24use crate::data::device::{MinerFirmware, MinerMake, MinerModel};
25use crate::miners::backends::antminer::AntMiner;
26use crate::miners::backends::avalonminer::AvalonMiner;
27use crate::miners::backends::bitaxe::Bitaxe;
28use crate::miners::backends::braiins::Braiins;
29use crate::miners::backends::epic::PowerPlay;
30use crate::miners::backends::luxminer::LuxMiner;
31use crate::miners::backends::marathon::Marathon;
32use crate::miners::backends::traits::*;
33use crate::miners::backends::vnish::Vnish;
34use crate::miners::backends::whatsminer::WhatsMiner;
35use crate::miners::factory::traits::VersionSelection;
36use std::net::SocketAddr;
37use traits::{DiscoveryCommands, ModelSelection};
38
39const IDENTIFICATION_TIMEOUT: Duration = Duration::from_secs(10);
40const CONNECTIVITY_TIMEOUT: Duration = Duration::from_secs(1);
41const CONNECTIVITY_RETRIES: u32 = 3;
42
43fn calculate_optimal_concurrency(ip_count: usize) -> usize {
44    // Adaptive concurrency based on scale
45    match ip_count {
46        0..=100 => 100,      // Small networks - conservative
47        101..=1000 => 250,   // Medium networks - moderate
48        1001..=5000 => 500,  // Large networks - aggressive
49        5001..=10000 => 750, // Very large networks - high throughput
50        _ => 1000,           // Massive mining operations - maximum throughput
51    }
52}
53
54/// Fast port connectivity check with TCP optimizations
55async fn check_port_open(ip: IpAddr, port: u16, connectivity_timeout: Duration) -> bool {
56    let addr: SocketAddr = (ip, port).into();
57
58    let stream = match timeout(connectivity_timeout, TcpStream::connect(addr)).await {
59        Ok(Ok(stream)) => stream,
60        _ => return false,
61    };
62
63    // disable Nagle's algorithm for immediate transmission
64    let _ = stream.set_nodelay(true);
65
66    // immediate close without waiting for lingering data
67    let _ = stream.set_linger(Some(Duration::from_secs(0)));
68
69    true
70}
71
72async fn get_miner_type_from_command(
73    ip: IpAddr,
74    command: MinerCommand,
75) -> Option<(Option<MinerMake>, Option<MinerFirmware>)> {
76    match command {
77        MinerCommand::RPC {
78            command,
79            parameters: _,
80        } => {
81            let response = send_rpc_command(&ip, command).await?;
82            parse_type_from_socket(response)
83        }
84        MinerCommand::WebAPI {
85            command,
86            parameters: _,
87        } => {
88            let response = send_web_command(&ip, command).await?;
89            parse_type_from_web(response)
90        }
91        _ => None,
92    }
93}
94
95fn parse_type_from_socket(
96    response: serde_json::Value,
97) -> Option<(Option<MinerMake>, Option<MinerFirmware>)> {
98    let json_string = response.to_string().to_uppercase();
99    match () {
100        _ if json_string.contains("BOSMINER") || json_string.contains("BOSER") => {
101            Some((None, Some(MinerFirmware::BraiinsOS)))
102        }
103        _ if json_string.contains("LUXMINER") => Some((None, Some(MinerFirmware::LuxOS))),
104        _ if json_string.contains("MARAFW") || json_string.contains("KAONSU") => {
105            Some((None, Some(MinerFirmware::Marathon)))
106        }
107        _ if json_string.contains("VNISH") => Some((None, Some(MinerFirmware::VNish))),
108        _ if json_string.contains("BITMICRO") || json_string.contains("BTMINER") => {
109            Some((Some(MinerMake::WhatsMiner), Some(MinerFirmware::Stock)))
110        }
111        _ if json_string.contains("ANTMINER") => {
112            Some((Some(MinerMake::AntMiner), Some(MinerFirmware::Stock)))
113        }
114        _ if json_string.contains("AVALON") => {
115            Some((Some(MinerMake::AvalonMiner), Some(MinerFirmware::Stock)))
116        }
117        _ => None,
118    }
119}
120
121fn parse_type_from_web(
122    response: (String, HeaderMap, StatusCode),
123) -> Option<(Option<MinerMake>, Option<MinerFirmware>)> {
124    let (resp_text, resp_headers, resp_status) = response;
125    let auth_header = match resp_headers.get("www-authenticate") {
126        Some(header) => header.to_str().unwrap(),
127        None => "",
128    };
129    let algo_header = match resp_headers.get("algorithm") {
130        Some(header) => header.to_str().unwrap(),
131        None => "",
132    };
133    let redirect_header = match resp_headers.get("location") {
134        Some(header) => header.to_str().unwrap(),
135        None => "",
136    };
137    match () {
138        _ if resp_status == 401 && algo_header.contains("MD5") => {
139            Some((None, Some(MinerFirmware::Marathon)))
140        }
141        _ if resp_status == 401 && auth_header.contains("realm=\"antMiner") => {
142            Some((Some(MinerMake::AntMiner), Some(MinerFirmware::Stock)))
143        }
144        _ if resp_text.contains("Braiins OS") => Some((None, Some(MinerFirmware::BraiinsOS))),
145        _ if resp_text.contains("Luxor Firmware") => Some((None, Some(MinerFirmware::LuxOS))),
146        _ if resp_text.contains("AxeOS") => {
147            Some((Some(MinerMake::Bitaxe), Some(MinerFirmware::Stock)))
148        }
149        _ if resp_text.contains("Miner Web Dashboard") => Some((None, Some(MinerFirmware::EPic))),
150        _ if resp_text.contains("Avalon") => {
151            Some((Some(MinerMake::AvalonMiner), Some(MinerFirmware::Stock)))
152        }
153        _ if resp_text.contains("AnthillOS") => Some((None, Some(MinerFirmware::VNish))),
154        _ if redirect_header.contains("https://") && resp_status == 307
155            || resp_text.contains("/cgi-bin/luci") =>
156        {
157            Some((Some(MinerMake::WhatsMiner), Some(MinerFirmware::Stock)))
158        }
159        _ => None,
160    }
161}
162
163fn select_backend(
164    ip: IpAddr,
165    model: Option<MinerModel>,
166    firmware: Option<MinerFirmware>,
167    version: Option<semver::Version>,
168) -> Option<Box<dyn Miner>> {
169    match (model, firmware) {
170        (Some(MinerModel::WhatsMiner(_)), Some(MinerFirmware::Stock)) => {
171            Some(WhatsMiner::new(ip, model?, version))
172        }
173        (Some(MinerModel::Bitaxe(_)), Some(MinerFirmware::Stock)) => {
174            Some(Bitaxe::new(ip, model?, version))
175        }
176        (Some(MinerModel::AvalonMiner(_)), Some(MinerFirmware::Stock)) => {
177            Some(AvalonMiner::new(ip, model?, version))
178        }
179        (Some(MinerModel::AntMiner(_)), Some(MinerFirmware::Stock)) => {
180            Some(AntMiner::new(ip, model?, version))
181        }
182        (Some(_), Some(MinerFirmware::VNish)) => Some(Vnish::new(ip, model?, version)),
183        (Some(_), Some(MinerFirmware::EPic)) => Some(PowerPlay::new(ip, model?, version)),
184        (Some(_), Some(MinerFirmware::Marathon)) => Some(Marathon::new(ip, model?, version)),
185        (Some(_), Some(MinerFirmware::LuxOS)) => Some(LuxMiner::new(ip, model?, version)),
186        (Some(_), Some(MinerFirmware::BraiinsOS)) => Some(Braiins::new(ip, model?, version)),
187        _ => None,
188    }
189}
190
191pub struct MinerFactory {
192    search_makes: Option<Vec<MinerMake>>,
193    search_firmwares: Option<Vec<MinerFirmware>>,
194    ips: Vec<IpAddr>,
195    identification_timeout: Duration,
196    connectivity_timeout: Duration,
197    connectivity_retries: u32,
198    concurrent: Option<usize>,
199    check_port: bool,
200}
201
202impl Default for MinerFactory {
203    fn default() -> Self {
204        Self::new()
205    }
206}
207
208impl MinerFactory {
209    pub async fn scan_miner(&self, ip: IpAddr) -> Result<Option<Box<dyn Miner>>> {
210        // Quick port check first to avoid wasting time on dead IPs
211        if (1..self.connectivity_retries).next().is_some() {
212            if self.check_port && !check_port_open(ip, 80, self.connectivity_timeout).await {
213                return Ok(None);
214            } else {
215                return self.get_miner(ip).await;
216            }
217        }
218        Ok(None)
219    }
220
221    pub async fn get_miner(&self, ip: IpAddr) -> Result<Option<Box<dyn Miner>>> {
222        let search_makes = self.search_makes.clone().unwrap_or(vec![
223            MinerMake::AntMiner,
224            MinerMake::WhatsMiner,
225            MinerMake::AvalonMiner,
226            MinerMake::EPic,
227            MinerMake::Braiins,
228            MinerMake::Bitaxe,
229        ]);
230        let search_firmwares = self.search_firmwares.clone().unwrap_or(vec![
231            MinerFirmware::Stock,
232            MinerFirmware::BraiinsOS,
233            MinerFirmware::VNish,
234            MinerFirmware::EPic,
235            MinerFirmware::HiveOS,
236            MinerFirmware::LuxOS,
237            MinerFirmware::Marathon,
238            MinerFirmware::MSKMiner,
239        ]);
240        let mut commands: HashSet<MinerCommand> = HashSet::new();
241
242        for make in search_makes {
243            for command in make.get_discovery_commands() {
244                commands.insert(command);
245            }
246        }
247        for firmware in search_firmwares {
248            for command in firmware.get_discovery_commands() {
249                commands.insert(command);
250            }
251        }
252
253        let mut discovery_tasks = JoinSet::new();
254        for command in commands {
255            let _ = discovery_tasks.spawn(get_miner_type_from_command(ip, command));
256        }
257
258        let timeout = tokio::time::sleep(self.identification_timeout).fuse();
259        let tasks = tokio::spawn(async move {
260            loop {
261                if discovery_tasks.is_empty() {
262                    return None;
263                };
264                match discovery_tasks.join_next().await.unwrap_or(Ok(None)) {
265                    Ok(Some(result)) => {
266                        return Some(result);
267                    }
268                    _ => continue,
269                };
270            }
271        });
272
273        pin_mut!(timeout, tasks);
274
275        let miner_info = tokio::select!(
276            Ok(miner_info) = &mut tasks => {
277                miner_info
278            },
279            _ = &mut timeout => {
280                None
281            }
282        );
283
284        match miner_info {
285            Some((Some(make), Some(MinerFirmware::Stock))) => {
286                let model = make.get_model(ip).await;
287                let version = make.get_version(ip).await;
288
289                Ok(select_backend(
290                    ip,
291                    model,
292                    Some(MinerFirmware::Stock),
293                    version,
294                ))
295            }
296            Some((_, Some(firmware))) => {
297                let model = firmware.get_model(ip).await;
298                let version = firmware.get_version(ip).await;
299
300                if let Some(model) = model {
301                    return Ok(select_backend(ip, Some(model), Some(firmware), version));
302                }
303
304                Ok(select_backend(ip, model, Some(firmware), version))
305            }
306            Some((Some(make), firmware)) => {
307                let model = make.get_model(ip).await;
308                let version = make.get_version(ip).await;
309
310                Ok(select_backend(ip, model, firmware, version))
311            }
312            _ => Ok(None),
313        }
314    }
315
316    pub fn new() -> MinerFactory {
317        MinerFactory {
318            search_makes: None,
319            search_firmwares: None,
320            ips: Vec::new(),
321            identification_timeout: IDENTIFICATION_TIMEOUT,
322            connectivity_timeout: CONNECTIVITY_TIMEOUT,
323            connectivity_retries: CONNECTIVITY_RETRIES,
324            concurrent: None,
325            check_port: true, // Enable port checking by default
326        }
327    }
328
329    // Port checking
330    pub fn with_port_check(mut self, enabled: bool) -> Self {
331        self.check_port = enabled;
332        self
333    }
334
335    // Concurrency limiting
336    pub fn with_concurrent_limit(mut self, limit: usize) -> Self {
337        self.concurrent = Some(limit);
338        self
339    }
340
341    pub fn with_adaptive_concurrency(mut self) -> Self {
342        self.concurrent = Some(calculate_optimal_concurrency(self.ips.len()));
343        self
344    }
345
346    fn update_adaptive_concurrency(&mut self) {
347        if self.concurrent.is_none() {
348            self.concurrent = Some(calculate_optimal_concurrency(self.ips.len()));
349        }
350    }
351
352    // Timeout
353    pub fn with_identification_timeout(mut self, timeout: Duration) -> Self {
354        self.identification_timeout = timeout;
355        self
356    }
357
358    pub fn with_identification_timeout_secs(mut self, timeout_secs: u64) -> Self {
359        self.identification_timeout = Duration::from_secs(timeout_secs);
360        self
361    }
362
363    pub fn with_connectivity_timeout(mut self, timeout: Duration) -> Self {
364        self.connectivity_timeout = timeout;
365        self
366    }
367
368    pub fn with_connectivity_timeout_secs(mut self, timeout_secs: u64) -> Self {
369        self.connectivity_timeout = Duration::from_secs(timeout_secs);
370        self
371    }
372
373    pub fn with_connectivity_retries(mut self, retries: u32) -> Self {
374        self.connectivity_retries = retries;
375        self
376    }
377
378    // Makes
379    pub fn with_search_makes(mut self, search_makes: Vec<MinerMake>) -> Self {
380        self.search_makes = Some(search_makes);
381        self
382    }
383
384    pub fn with_makes(mut self, makes: Vec<MinerMake>) -> Self {
385        self.search_makes = Some(makes);
386        self
387    }
388
389    pub fn add_search_make(mut self, search_make: MinerMake) -> Self {
390        if self.search_makes.is_none() {
391            self.search_makes = Some(vec![search_make]);
392        } else {
393            self.search_makes.as_mut().unwrap().push(search_make);
394        }
395        self
396    }
397
398    pub fn remove_search_make(mut self, search_make: MinerMake) -> Self {
399        if let Some(makes) = self.search_makes.as_mut() {
400            makes.retain(|val| *val != search_make);
401        }
402        self
403    }
404
405    // Firmwares
406    pub fn with_search_firmwares(mut self, search_firmwares: Vec<MinerFirmware>) -> Self {
407        self.search_firmwares = Some(search_firmwares);
408        self
409    }
410
411    pub fn with_firmwares(mut self, firmwares: Vec<MinerFirmware>) -> Self {
412        self.search_firmwares = Some(firmwares);
413        self
414    }
415
416    pub fn add_search_firmware(mut self, search_firmware: MinerFirmware) -> Self {
417        if self.search_firmwares.is_none() {
418            self.search_firmwares = Some(vec![search_firmware]);
419        } else {
420            self.search_firmwares
421                .as_mut()
422                .unwrap()
423                .push(search_firmware);
424        }
425        self
426    }
427
428    pub fn remove_search_firmware(mut self, search_firmware: MinerFirmware) -> Self {
429        if let Some(firmwares) = self.search_firmwares.as_mut() {
430            firmwares.retain(|val| *val != search_firmware);
431        }
432        self
433    }
434
435    // Subnet handlers
436    /// Set IPs from a subnet
437    pub fn with_subnet(mut self, subnet: &str) -> Result<Self> {
438        let ips = self.hosts_from_subnet(subnet)?;
439        self.ips = ips;
440        self.shuffle_ips();
441        Ok(self)
442    }
443    pub fn set_subnet(&mut self, subnet: &str) -> Result<&Self> {
444        let ips = self.hosts_from_subnet(subnet)?;
445        self.ips = ips;
446        self.shuffle_ips();
447        Ok(self)
448    }
449    fn hosts_from_subnet(&self, subnet: &str) -> Result<Vec<IpAddr>> {
450        let network = IpNet::from_str(subnet)?;
451        Ok(network.hosts().collect())
452    }
453
454    /// Randomize IP order to avoid bursts to a single switch/segment
455    fn shuffle_ips(&mut self) {
456        let mut rng = rand::rng();
457        self.ips.shuffle(&mut rng);
458    }
459
460    // Octet handlers
461    /// Set IPs from octet ranges
462    pub fn with_octets(
463        mut self,
464        octet1: &str,
465        octet2: &str,
466        octet3: &str,
467        octet4: &str,
468    ) -> Result<Self> {
469        let ips = self.hosts_from_octets(octet1, octet2, octet3, octet4)?;
470        self.ips = ips;
471        self.shuffle_ips();
472        self.update_adaptive_concurrency();
473        Ok(self)
474    }
475    pub fn set_octets(
476        &mut self,
477        octet1: &str,
478        octet2: &str,
479        octet3: &str,
480        octet4: &str,
481    ) -> Result<&Self> {
482        let ips = self.hosts_from_octets(octet1, octet2, octet3, octet4)?;
483        self.ips = ips;
484        self.shuffle_ips();
485        self.update_adaptive_concurrency();
486        Ok(self)
487    }
488    fn hosts_from_octets(
489        &self,
490        octet1: &str,
491        octet2: &str,
492        octet3: &str,
493        octet4: &str,
494    ) -> Result<Vec<IpAddr>> {
495        let octet1_range = parse_octet_range(octet1)?;
496        let octet2_range = parse_octet_range(octet2)?;
497        let octet3_range = parse_octet_range(octet3)?;
498        let octet4_range = parse_octet_range(octet4)?;
499
500        Ok(generate_ips_from_ranges(
501            &octet1_range,
502            &octet2_range,
503            &octet3_range,
504            &octet4_range,
505        ))
506    }
507
508    // Range handler
509    /// Set IPs from a range string in the format "10.1-199.0.1-199"
510    pub fn with_range(self, range_str: &str) -> Result<Self> {
511        let parts: Vec<&str> = range_str.split('.').collect();
512        if parts.len() != 4 {
513            return Err(anyhow::anyhow!(
514                "Invalid IP range format. Expected format: 10.1-199.0.1-199"
515            ));
516        }
517
518        self.with_octets(parts[0], parts[1], parts[2], parts[3])
519    }
520
521    /// Return current scan IPs
522    pub fn hosts(&self) -> Vec<IpAddr> {
523        self.ips.clone()
524    }
525
526    /// Get current count of scan IPs
527    pub fn len(&self) -> usize {
528        self.ips.len()
529    }
530
531    /// Check if the list of IPs is empty
532    pub fn is_empty(&self) -> bool {
533        self.ips.is_empty()
534    }
535
536    /// Scan the IPs specified in the factory
537    pub async fn scan(&self) -> Result<Vec<Box<dyn Miner>>> {
538        if self.ips.is_empty() {
539            return Err(anyhow::anyhow!(
540                "No IPs to scan. Use with_subnet, with_octets, or with_range to set IPs."
541            ));
542        }
543
544        let concurrency = self
545            .concurrent
546            .unwrap_or(calculate_optimal_concurrency(self.ips.len()));
547
548        let miners: Vec<Box<dyn Miner>> = stream::iter(self.ips.iter().copied())
549            .map(|ip| async move { self.scan_miner(ip).await.ok().flatten() })
550            .buffer_unordered(concurrency)
551            .filter_map(|miner_opt| async move { miner_opt })
552            .collect()
553            .await;
554
555        Ok(miners)
556    }
557
558    pub fn scan_stream(&self) -> Result<impl Stream<Item = Box<dyn Miner>>> {
559        if self.ips.is_empty() {
560            return Err(anyhow::anyhow!(
561                "No IPs to scan. Use with_subnet, with_octets, or with_range to set IPs."
562            ));
563        }
564
565        let concurrency = self
566            .concurrent
567            .unwrap_or(calculate_optimal_concurrency(self.ips.len()));
568
569        let stream = stream::iter(
570            self.ips
571                .iter()
572                .copied()
573                .map(move |ip| async move { self.scan_miner(ip).await.ok().flatten() }),
574        )
575        .buffer_unordered(concurrency)
576        .filter_map(|miner_opt| async move { miner_opt });
577
578        Ok(Box::pin(stream))
579    }
580
581    pub fn scan_stream_with_ip(
582        &self,
583    ) -> Result<impl Stream<Item = (IpAddr, Option<Box<dyn Miner>>)>> {
584        if self.ips.is_empty() {
585            return Err(anyhow::anyhow!(
586                "No IPs to scan. Use with_subnet, with_octets, or with_range to set IPs."
587            ));
588        }
589
590        let concurrency = self
591            .concurrent
592            .unwrap_or(calculate_optimal_concurrency(self.ips.len()));
593
594        let stream = stream::iter(
595            self.ips
596                .iter()
597                .copied()
598                .map(move |ip| async move { (ip, self.scan_miner(ip).await.ok().flatten()) }),
599        )
600        .buffer_unordered(concurrency);
601
602        Ok(Box::pin(stream))
603    }
604
605    /// Scan for miners by specific octets
606    pub async fn scan_by_octets(
607        self,
608        octet1: &str,
609        octet2: &str,
610        octet3: &str,
611        octet4: &str,
612    ) -> Result<Vec<Box<dyn Miner>>> {
613        self.with_octets(octet1, octet2, octet3, octet4)?
614            .scan()
615            .await
616    }
617
618    /// Scan for miners by IP range in the format "10.1-199.0.1-199"
619    pub async fn scan_by_range(self, range_str: &str) -> Result<Vec<Box<dyn Miner>>> {
620        self.with_range(range_str)?.scan().await
621    }
622}
623
624/// Helper function to parse an octet range string like "1-199" into a vector of u8 values
625fn parse_octet_range(range_str: &str) -> Result<Vec<u8>> {
626    if range_str.contains('-') {
627        let parts: Vec<&str> = range_str.split('-').collect();
628        if parts.len() != 2 {
629            return Err(anyhow::anyhow!("Invalid range format: {}", range_str));
630        }
631
632        let start: u8 = parts[0].parse()?;
633        let end: u8 = parts[1].parse()?;
634
635        if start > end {
636            return Err(anyhow::anyhow!(
637                "Invalid range: start > end in {}",
638                range_str
639            ));
640        }
641
642        Ok((start..=end).collect())
643    } else {
644        // Single value
645        let value: u8 = range_str.parse()?;
646        Ok(vec![value])
647    }
648}
649
650/// Generate all IPv4 addresses from octet ranges
651fn generate_ips_from_ranges(
652    octet1_range: &[u8],
653    octet2_range: &[u8],
654    octet3_range: &[u8],
655    octet4_range: &[u8],
656) -> Vec<IpAddr> {
657    let mut ips = Vec::new();
658
659    for &o1 in octet1_range {
660        for &o2 in octet2_range {
661            for &o3 in octet3_range {
662                for &o4 in octet4_range {
663                    ips.push(IpAddr::V4(Ipv4Addr::new(o1, o2, o3, o4)));
664                }
665            }
666        }
667    }
668
669    ips
670}
671
672#[cfg(test)]
673mod tests {
674    use super::*;
675
676    #[test]
677    fn test_parse_type_from_socket_whatsminer_2024_09_30() {
678        const RAW_DATA: &str = r#"{"STATUS": [{"STATUS": "S", "Msg": "Device Details"}], "DEVDETAILS": [{"DEVDETAILS": 0, "Name": "SM", "ID": 0, "Driver": "bitmicro", "Kernel": "", "Model": "M30S+_VE40"}, {"DEVDETAILS": 1, "Name": "SM", "ID": 1, "Driver": "bitmicro", "Kernel": "", "Model": "M30S+_VE40"}, {"DEVDETAILS": 2, "Name": "SM", "ID": 2, "Driver": "bitmicro", "Kernel": "", "Model": "M30S+_VE40"}], "id": 1}"#;
679        let parsed_data = serde_json::from_str(RAW_DATA).unwrap();
680        let result = parse_type_from_socket(parsed_data);
681        assert_eq!(
682            result,
683            Some((Some(MinerMake::WhatsMiner), Some(MinerFirmware::Stock)))
684        )
685    }
686
687    #[test]
688    fn test_parse_type_from_web_whatsminer_2024_09_30() {
689        let mut headers = HeaderMap::new();
690        headers.insert("location", "https://example.com/".parse().unwrap());
691
692        let response_data = (String::from(""), headers, StatusCode::TEMPORARY_REDIRECT);
693
694        let result = parse_type_from_web(response_data);
695        assert_eq!(
696            result,
697            Some((Some(MinerMake::WhatsMiner), Some(MinerFirmware::Stock)))
698        )
699    }
700
701    #[test]
702    fn test_parse_octet_range() {
703        // Test single value
704        let result = parse_octet_range("10").unwrap();
705        assert_eq!(result, vec![10]);
706
707        // Test range
708        let result = parse_octet_range("1-5").unwrap();
709        assert_eq!(result, vec![1, 2, 3, 4, 5]);
710
711        // Test larger range
712        let result = parse_octet_range("200-255").unwrap();
713        assert_eq!(result, (200..=255).collect::<Vec<u8>>());
714
715        // Test invalid range (start > end)
716        let result = parse_octet_range("200-100");
717        assert!(result.is_err());
718
719        // Test invalid format
720        let result = parse_octet_range("1-5-10");
721        assert!(result.is_err());
722
723        // Test invalid value
724        let result = parse_octet_range("300");
725        assert!(result.is_err());
726    }
727
728    #[test]
729    fn test_generate_ips_from_ranges() {
730        let octet1 = vec![192];
731        let octet2 = vec![168];
732        let octet3 = vec![1];
733        let octet4 = vec![1, 2];
734
735        let ips = generate_ips_from_ranges(&octet1, &octet2, &octet3, &octet4);
736
737        assert_eq!(ips.len(), 2);
738        assert!(ips.contains(&IpAddr::V4(Ipv4Addr::new(192, 168, 1, 1))));
739        assert!(ips.contains(&IpAddr::V4(Ipv4Addr::new(192, 168, 1, 2))));
740    }
741}