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