asic_rs/miners/backends/bitaxe/v2_0_0/
mod.rs

1use anyhow::{Result, anyhow};
2use async_trait::async_trait;
3use macaddr::MacAddr;
4use measurements::{AngularVelocity, Frequency, Power, Temperature, Voltage};
5use serde_json::Value;
6use std::collections::HashMap;
7use std::net::IpAddr;
8use std::str::FromStr;
9use std::time::{Duration, SystemTime, UNIX_EPOCH};
10
11use crate::data::board::{BoardData, ChipData};
12use crate::data::device::MinerMake;
13use crate::data::device::{DeviceInfo, HashAlgorithm, MinerFirmware, MinerModel};
14use crate::data::fan::FanData;
15use crate::data::hashrate::{HashRate, HashRateUnit};
16use crate::data::message::{MessageSeverity, MinerMessage};
17use crate::data::pool::{PoolData, PoolScheme, PoolURL};
18use crate::miners::backends::traits::*;
19use crate::miners::commands::MinerCommand;
20use crate::miners::data::{
21    DataCollector, DataExtensions, DataExtractor, DataField, DataLocation, get_by_key,
22    get_by_pointer,
23};
24
25use web::BitAxeWebAPI;
26
27pub(crate) mod web;
28
29#[derive(Debug)]
30pub struct BitAxe200 {
31    ip: IpAddr,
32    web: BitAxeWebAPI,
33    device_info: DeviceInfo,
34}
35
36impl BitAxe200 {
37    pub fn new(ip: IpAddr, model: MinerModel) -> Self {
38        BitAxe200 {
39            ip,
40            web: BitAxeWebAPI::new(ip, 80),
41            device_info: DeviceInfo::new(
42                MinerMake::BitAxe,
43                model,
44                MinerFirmware::Stock,
45                HashAlgorithm::SHA256,
46            ),
47        }
48    }
49}
50
51#[async_trait]
52impl APIClient for BitAxe200 {
53    async fn get_api_result(&self, command: &MinerCommand) -> Result<Value> {
54        match command {
55            MinerCommand::WebAPI { .. } => self.web.get_api_result(command).await,
56            _ => Err(anyhow!("Unsupported command type for BitAxe API")),
57        }
58    }
59}
60
61#[async_trait]
62impl GetDataLocations for BitAxe200 {
63    fn get_locations(&self, data_field: DataField) -> Vec<DataLocation> {
64        let system_info_command: MinerCommand = MinerCommand::WebAPI {
65            command: "system/info",
66            parameters: None,
67        };
68
69        match data_field {
70            DataField::Mac => vec![(
71                system_info_command,
72                DataExtractor {
73                    func: get_by_key,
74                    key: Some("macAddr"),
75                    tag: None,
76                },
77            )],
78            DataField::Hostname => vec![(
79                system_info_command,
80                DataExtractor {
81                    func: get_by_key,
82                    key: Some("hostname"),
83                    tag: None,
84                },
85            )],
86            DataField::FirmwareVersion => vec![(
87                system_info_command,
88                DataExtractor {
89                    func: get_by_key,
90                    key: Some("version"),
91                    tag: None,
92                },
93            )],
94            DataField::ApiVersion => vec![(
95                system_info_command,
96                DataExtractor {
97                    func: get_by_key,
98                    key: Some("version"),
99                    tag: None,
100                },
101            )],
102            DataField::ControlBoardVersion => vec![(
103                system_info_command,
104                DataExtractor {
105                    func: get_by_key,
106                    key: Some("boardVersion"),
107                    tag: None,
108                },
109            )],
110            DataField::Hashboards => vec![(
111                system_info_command,
112                DataExtractor {
113                    func: get_by_pointer,
114                    key: Some(""),
115                    tag: None,
116                },
117            )],
118            DataField::Hashrate => vec![(
119                system_info_command,
120                DataExtractor {
121                    func: get_by_key,
122                    key: Some("hashRate"),
123                    tag: None,
124                },
125            )],
126            DataField::ExpectedHashrate => vec![(
127                system_info_command,
128                DataExtractor {
129                    func: get_by_pointer,
130                    key: Some(""),
131                    tag: None,
132                },
133            )],
134            DataField::Fans => vec![(
135                system_info_command,
136                DataExtractor {
137                    func: get_by_key,
138                    key: Some("fanrpm"),
139                    tag: None,
140                },
141            )],
142            DataField::AverageTemperature => vec![(
143                system_info_command,
144                DataExtractor {
145                    func: get_by_key,
146                    key: Some("temp"),
147                    tag: None,
148                },
149            )],
150            DataField::Wattage => vec![(
151                system_info_command,
152                DataExtractor {
153                    func: get_by_key,
154                    key: Some("power"),
155                    tag: None,
156                },
157            )],
158            DataField::Uptime => vec![(
159                system_info_command,
160                DataExtractor {
161                    func: get_by_key,
162                    key: Some("uptimeSeconds"),
163                    tag: None,
164                },
165            )],
166            DataField::Pools => vec![(
167                system_info_command,
168                DataExtractor {
169                    func: get_by_pointer,
170                    key: Some(""),
171                    tag: None,
172                },
173            )],
174            _ => vec![],
175        }
176    }
177}
178
179impl GetIP for BitAxe200 {
180    fn get_ip(&self) -> IpAddr {
181        self.ip
182    }
183}
184impl GetDeviceInfo for BitAxe200 {
185    fn get_device_info(&self) -> DeviceInfo {
186        self.device_info
187    }
188}
189
190impl CollectData for BitAxe200 {
191    fn get_collector(&self) -> DataCollector<'_> {
192        DataCollector::new(self)
193    }
194}
195
196impl GetMAC for BitAxe200 {
197    fn parse_mac(&self, data: &HashMap<DataField, Value>) -> Option<MacAddr> {
198        data.extract::<String>(DataField::Mac)
199            .and_then(|s| MacAddr::from_str(&s).ok())
200    }
201}
202
203impl GetSerialNumber for BitAxe200 {
204    // N/A
205}
206impl GetHostname for BitAxe200 {
207    fn parse_hostname(&self, data: &HashMap<DataField, Value>) -> Option<String> {
208        data.extract::<String>(DataField::Hostname)
209    }
210}
211impl GetApiVersion for BitAxe200 {
212    fn parse_api_version(&self, data: &HashMap<DataField, Value>) -> Option<String> {
213        data.extract::<String>(DataField::ApiVersion)
214    }
215}
216impl GetFirmwareVersion for BitAxe200 {
217    fn parse_firmware_version(&self, data: &HashMap<DataField, Value>) -> Option<String> {
218        data.extract::<String>(DataField::FirmwareVersion)
219    }
220}
221impl GetControlBoardVersion for BitAxe200 {
222    fn parse_control_board_version(&self, data: &HashMap<DataField, Value>) -> Option<String> {
223        data.extract::<String>(DataField::ControlBoardVersion)
224    }
225}
226impl GetHashboards for BitAxe200 {
227    fn parse_hashboards(&self, data: &HashMap<DataField, Value>) -> Vec<BoardData> {
228        // Extract nested values with type conversion
229        let board_voltage = data.extract_nested_map::<f64, _>(
230            DataField::Hashboards,
231            "voltage",
232            Voltage::from_millivolts,
233        );
234
235        let board_temperature = data.extract_nested_map::<f64, _>(
236            DataField::Hashboards,
237            "vrTemp",
238            Temperature::from_celsius,
239        );
240
241        let board_frequency = data.extract_nested_map::<f64, _>(
242            DataField::Hashboards,
243            "frequency",
244            Frequency::from_megahertz,
245        );
246
247        let chip_temperature = data.extract_nested_map::<f64, _>(
248            DataField::Hashboards,
249            "temp",
250            Temperature::from_celsius,
251        );
252
253        let board_hashrate = Some(HashRate {
254            value: data.extract_nested_or::<f64>(DataField::Hashboards, "hashRate", 0.0),
255            unit: HashRateUnit::GigaHash,
256            algo: "SHA256".to_string(),
257        });
258
259        let total_chips =
260            data.extract_nested_map::<u64, _>(DataField::Hashboards, "asicCount", |u| u as u16);
261
262        let core_count =
263            data.extract_nested_or::<u64>(DataField::Hashboards, "smallCoreCount", 0u64);
264
265        let expected_hashrate = Some(HashRate {
266            value: core_count as f64
267                * total_chips.unwrap_or(0) as f64
268                * board_frequency
269                    .unwrap_or(Frequency::from_megahertz(0f64))
270                    .as_gigahertz(),
271            unit: HashRateUnit::GigaHash,
272            algo: "SHA256".to_string(),
273        });
274
275        let chip_info = ChipData {
276            position: 0,
277            temperature: chip_temperature,
278            voltage: board_voltage,
279            frequency: board_frequency,
280            tuned: Some(true),
281            working: Some(true),
282            hashrate: board_hashrate.clone(),
283        };
284
285        let board_data = BoardData {
286            position: 0,
287            hashrate: board_hashrate,
288            expected_hashrate,
289            board_temperature,
290            intake_temperature: board_temperature,
291            outlet_temperature: board_temperature,
292            expected_chips: self.device_info.hardware.chips,
293            working_chips: total_chips,
294            serial_number: None,
295            chips: vec![chip_info],
296            voltage: board_voltage,
297            frequency: board_frequency,
298            tuned: Some(true),
299            active: Some(true),
300        };
301
302        vec![board_data]
303    }
304}
305impl GetHashrate for BitAxe200 {
306    fn parse_hashrate(&self, data: &HashMap<DataField, Value>) -> Option<HashRate> {
307        data.extract_map::<f64, _>(DataField::Hashrate, |f| HashRate {
308            value: f,
309            unit: HashRateUnit::TeraHash,
310            algo: String::from("SHA256"),
311        })
312    }
313}
314impl GetExpectedHashrate for BitAxe200 {
315    fn parse_expected_hashrate(&self, data: &HashMap<DataField, Value>) -> Option<HashRate> {
316        let total_chips =
317            data.extract_nested_map::<u64, _>(DataField::ExpectedHashrate, "asicCount", |u| {
318                u as u16
319            });
320
321        let core_count =
322            data.extract_nested_or::<u64>(DataField::ExpectedHashrate, "smallCoreCount", 0u64);
323
324        let board_frequency = data.extract_nested_map::<f64, _>(
325            DataField::Hashboards,
326            "frequency",
327            Frequency::from_megahertz,
328        );
329
330        Some(HashRate {
331            value: core_count as f64
332                * total_chips.unwrap_or(0) as f64
333                * board_frequency
334                    .unwrap_or(Frequency::from_megahertz(0f64))
335                    .as_gigahertz(),
336            unit: HashRateUnit::GigaHash,
337            algo: "SHA256".to_string(),
338        })
339    }
340}
341impl GetFans for BitAxe200 {
342    fn parse_fans(&self, data: &HashMap<DataField, Value>) -> Vec<FanData> {
343        data.extract_map_or::<f64, _>(DataField::Fans, Vec::new(), |f| {
344            vec![FanData {
345                position: 0,
346                rpm: Some(AngularVelocity::from_rpm(f)),
347            }]
348        })
349    }
350}
351impl GetPsuFans for BitAxe200 {
352    // N/A
353}
354impl GetFluidTemperature for BitAxe200 {
355    // N/A
356}
357impl GetWattage for BitAxe200 {
358    fn parse_wattage(&self, data: &HashMap<DataField, Value>) -> Option<Power> {
359        data.extract_map::<f64, _>(DataField::Wattage, Power::from_watts)
360    }
361}
362impl GetWattageLimit for BitAxe200 {
363    // N/A
364}
365impl GetLightFlashing for BitAxe200 {
366    // N/A
367}
368impl GetMessages for BitAxe200 {
369    fn parse_messages(&self, data: &HashMap<DataField, Value>) -> Vec<MinerMessage> {
370        let mut messages = Vec::new();
371        let timestamp = SystemTime::now()
372            .duration_since(UNIX_EPOCH)
373            .expect("Failed to get system time")
374            .as_secs();
375
376        let is_overheating = data.extract_nested::<bool>(DataField::Hashboards, "overheat_mode");
377
378        if let Some(true) = is_overheating {
379            messages.push(MinerMessage {
380                timestamp: timestamp as u32,
381                code: 0u64,
382                message: "Overheat Mode is Enabled!".to_string(),
383                severity: MessageSeverity::Warning,
384            });
385        };
386        messages
387    }
388}
389
390impl GetUptime for BitAxe200 {
391    fn parse_uptime(&self, data: &HashMap<DataField, Value>) -> Option<Duration> {
392        data.extract_map::<u64, _>(DataField::Uptime, Duration::from_secs)
393    }
394}
395impl GetIsMining for BitAxe200 {
396    fn parse_is_mining(&self, data: &HashMap<DataField, Value>) -> bool {
397        let hashrate = self.parse_hashrate(data);
398        hashrate.as_ref().is_some_and(|hr| hr.value > 0.0)
399    }
400}
401impl GetPools for BitAxe200 {
402    fn parse_pools(&self, data: &HashMap<DataField, Value>) -> Vec<PoolData> {
403        let main_url =
404            data.extract_nested_or::<String>(DataField::Pools, "stratumURL", String::new());
405        let main_port = data.extract_nested_or::<u64>(DataField::Pools, "stratumPort", 0);
406        let accepted_share = data.extract_nested::<u64>(DataField::Pools, "sharesAccepted");
407        let rejected_share = data.extract_nested::<u64>(DataField::Pools, "sharesRejected");
408        let main_user = data.extract_nested::<String>(DataField::Pools, "stratumUser");
409
410        let is_using_fallback =
411            data.extract_nested_or::<bool>(DataField::Pools, "isUsingFallbackStratum", false);
412
413        let main_pool_url = PoolURL {
414            scheme: PoolScheme::StratumV1,
415            host: main_url,
416            port: main_port as u16,
417            pubkey: None,
418        };
419
420        let main_pool_data = PoolData {
421            position: Some(0),
422            url: Some(main_pool_url),
423            accepted_shares: accepted_share,
424            rejected_shares: rejected_share,
425            active: Some(!is_using_fallback),
426            alive: None,
427            user: main_user,
428        };
429
430        // Extract fallback pool data
431        let fallback_url =
432            data.extract_nested_or::<String>(DataField::Pools, "fallbackStratumURL", String::new());
433        let fallback_port =
434            data.extract_nested_or::<u64>(DataField::Pools, "fallbackStratumPort", 0);
435        let fallback_user = data.extract_nested(DataField::Pools, "fallbackStratumUser");
436        let fallback_pool_url = PoolURL {
437            scheme: PoolScheme::StratumV1,
438            host: fallback_url,
439            port: fallback_port as u16,
440            pubkey: None,
441        };
442
443        let fallback_pool_data = PoolData {
444            position: Some(1),
445            url: Some(fallback_pool_url),
446            accepted_shares: accepted_share,
447            rejected_shares: rejected_share,
448            active: Some(is_using_fallback),
449            alive: None,
450            user: fallback_user,
451        };
452
453        vec![main_pool_data, fallback_pool_data]
454    }
455}
456
457#[cfg(test)]
458mod tests {
459    use super::*;
460    use crate::data::device::models::bitaxe::BitAxeModel;
461    use crate::test::api::MockAPIClient;
462    use crate::test::json::bitaxe::v2_0_0::SYSTEM_INFO_COMMAND;
463
464    #[tokio::test]
465    async fn test_espminer_200_data_parsers() {
466        let miner = BitAxe200::new(
467            IpAddr::from([127, 0, 0, 1]),
468            MinerModel::BitAxe(BitAxeModel::Supra),
469        );
470        let mut results = HashMap::new();
471        let system_info_command: MinerCommand = MinerCommand::WebAPI {
472            command: "system/info",
473            parameters: None,
474        };
475        results.insert(
476            system_info_command,
477            Value::from_str(SYSTEM_INFO_COMMAND).unwrap(),
478        );
479        let mock_api = MockAPIClient::new(results);
480
481        let mut collector = DataCollector::new_with_client(&miner, &mock_api);
482        let data = collector.collect_all().await;
483
484        let miner_data = miner.parse_data(data);
485
486        assert_eq!(&miner_data.ip, &miner.ip);
487        assert_eq!(
488            &miner_data.mac.unwrap(),
489            &MacAddr::from_str("AA:BB:CC:DD:EE:FF").unwrap()
490        );
491        assert_eq!(&miner_data.device_info, &miner.device_info);
492        assert_eq!(&miner_data.hostname, &Some("bitaxe".to_string()));
493        assert_eq!(
494            &miner_data.api_version,
495            &Some("v2.4.5-3-gb5d1e36-dirty".to_string())
496        );
497        assert_eq!(
498            &miner_data.firmware_version,
499            &Some("v2.4.5-3-gb5d1e36-dirty".to_string())
500        );
501        assert_eq!(&miner_data.control_board_version, &Some("401".to_string()));
502        assert_eq!(
503            &miner_data.hashrate,
504            &Some(HashRate {
505                value: 0f64,
506                unit: HashRateUnit::TeraHash,
507                algo: "SHA256".to_string(),
508            })
509        );
510        assert_eq!(&miner_data.total_chips, &Some(1u16));
511        assert_eq!(
512            &miner_data.fans,
513            &vec![FanData {
514                position: 0,
515                rpm: Some(AngularVelocity::from_rpm(3517f64)),
516            }]
517        );
518        assert_eq!(
519            &miner_data.wattage,
520            &Some(Power::from_watts(2.65000009536743))
521        )
522    }
523}