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

1use 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::{DeviceInfo, HashAlgorithm, MinerFirmware, MinerModel};
13use crate::data::device::{MinerControlBoard, MinerMake};
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) -> anyhow::Result<Value> {
54        match command {
55            MinerCommand::WebAPI { .. } => self.web.get_api_result(command).await,
56            _ => Err(anyhow::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        const WEB_SYSTEM_INFO: MinerCommand = MinerCommand::WebAPI {
65            command: "system/info",
66            parameters: None,
67        };
68
69        match data_field {
70            DataField::Mac => vec![(
71                WEB_SYSTEM_INFO,
72                DataExtractor {
73                    func: get_by_key,
74                    key: Some("macAddr"),
75                    tag: None,
76                },
77            )],
78            DataField::Hostname => vec![(
79                WEB_SYSTEM_INFO,
80                DataExtractor {
81                    func: get_by_key,
82                    key: Some("hostname"),
83                    tag: None,
84                },
85            )],
86            DataField::FirmwareVersion => vec![(
87                WEB_SYSTEM_INFO,
88                DataExtractor {
89                    func: get_by_key,
90                    key: Some("version"),
91                    tag: None,
92                },
93            )],
94            DataField::ApiVersion => vec![(
95                WEB_SYSTEM_INFO,
96                DataExtractor {
97                    func: get_by_key,
98                    key: Some("version"),
99                    tag: None,
100                },
101            )],
102            DataField::ControlBoardVersion => vec![(
103                WEB_SYSTEM_INFO,
104                DataExtractor {
105                    func: get_by_key,
106                    key: Some("boardVersion"),
107                    tag: None,
108                },
109            )],
110            DataField::Hashboards => vec![(
111                WEB_SYSTEM_INFO,
112                DataExtractor {
113                    func: get_by_pointer,
114                    key: Some(""),
115                    tag: None,
116                },
117            )],
118            DataField::Hashrate => vec![(
119                WEB_SYSTEM_INFO,
120                DataExtractor {
121                    func: get_by_key,
122                    key: Some("hashRate"),
123                    tag: None,
124                },
125            )],
126            DataField::ExpectedHashrate => vec![(
127                WEB_SYSTEM_INFO,
128                DataExtractor {
129                    func: get_by_pointer,
130                    key: Some(""),
131                    tag: None,
132                },
133            )],
134            DataField::Fans => vec![(
135                WEB_SYSTEM_INFO,
136                DataExtractor {
137                    func: get_by_key,
138                    key: Some("fanrpm"),
139                    tag: None,
140                },
141            )],
142            DataField::AverageTemperature => vec![(
143                WEB_SYSTEM_INFO,
144                DataExtractor {
145                    func: get_by_key,
146                    key: Some("temp"),
147                    tag: None,
148                },
149            )],
150            DataField::Wattage => vec![(
151                WEB_SYSTEM_INFO,
152                DataExtractor {
153                    func: get_by_key,
154                    key: Some("power"),
155                    tag: None,
156                },
157            )],
158            DataField::Uptime => vec![(
159                WEB_SYSTEM_INFO,
160                DataExtractor {
161                    func: get_by_key,
162                    key: Some("uptimeSeconds"),
163                    tag: None,
164                },
165            )],
166            DataField::Pools => vec![(
167                WEB_SYSTEM_INFO,
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(
223        &self,
224        data: &HashMap<DataField, Value>,
225    ) -> Option<MinerControlBoard> {
226        data.extract::<String>(DataField::ControlBoardVersion)
227            .and_then(|s| MinerControlBoard::from_str(&s).ok())
228    }
229}
230impl GetHashboards for Bitaxe200 {
231    fn parse_hashboards(&self, data: &HashMap<DataField, Value>) -> Vec<BoardData> {
232        // Extract nested values with type conversion
233        let board_voltage = data.extract_nested_map::<f64, _>(
234            DataField::Hashboards,
235            "voltage",
236            Voltage::from_millivolts,
237        );
238
239        let board_temperature = data.extract_nested_map::<f64, _>(
240            DataField::Hashboards,
241            "vrTemp",
242            Temperature::from_celsius,
243        );
244
245        let board_frequency = data.extract_nested_map::<f64, _>(
246            DataField::Hashboards,
247            "frequency",
248            Frequency::from_megahertz,
249        );
250
251        let chip_temperature = data.extract_nested_map::<f64, _>(
252            DataField::Hashboards,
253            "temp",
254            Temperature::from_celsius,
255        );
256
257        let board_hashrate = Some(HashRate {
258            value: data.extract_nested_or::<f64>(DataField::Hashboards, "hashRate", 0.0),
259            unit: HashRateUnit::GigaHash,
260            algo: "SHA256".to_string(),
261        });
262
263        let total_chips =
264            data.extract_nested_map::<u64, _>(DataField::Hashboards, "asicCount", |u| u as u16);
265
266        let core_count =
267            data.extract_nested_or::<u64>(DataField::Hashboards, "smallCoreCount", 0u64);
268
269        let expected_hashrate = Some(HashRate {
270            value: core_count as f64
271                * total_chips.unwrap_or(0) as f64
272                * board_frequency
273                    .unwrap_or(Frequency::from_megahertz(0f64))
274                    .as_gigahertz(),
275            unit: HashRateUnit::GigaHash,
276            algo: "SHA256".to_string(),
277        });
278
279        let chip_info = ChipData {
280            position: 0,
281            temperature: chip_temperature,
282            voltage: board_voltage,
283            frequency: board_frequency,
284            tuned: Some(true),
285            working: Some(true),
286            hashrate: board_hashrate.clone(),
287        };
288
289        let board_data = BoardData {
290            position: 0,
291            hashrate: board_hashrate,
292            expected_hashrate,
293            board_temperature,
294            intake_temperature: board_temperature,
295            outlet_temperature: board_temperature,
296            expected_chips: self.device_info.hardware.chips,
297            working_chips: total_chips,
298            serial_number: None,
299            chips: vec![chip_info],
300            voltage: board_voltage,
301            frequency: board_frequency,
302            tuned: Some(true),
303            active: Some(true),
304        };
305
306        vec![board_data]
307    }
308}
309impl GetHashrate for Bitaxe200 {
310    fn parse_hashrate(&self, data: &HashMap<DataField, Value>) -> Option<HashRate> {
311        data.extract_map::<f64, _>(DataField::Hashrate, |f| HashRate {
312            value: f,
313            unit: HashRateUnit::GigaHash,
314            algo: String::from("SHA256"),
315        })
316    }
317}
318impl GetExpectedHashrate for Bitaxe200 {
319    fn parse_expected_hashrate(&self, data: &HashMap<DataField, Value>) -> Option<HashRate> {
320        let total_chips =
321            data.extract_nested_map::<u64, _>(DataField::ExpectedHashrate, "asicCount", |u| {
322                u as u16
323            });
324
325        let core_count =
326            data.extract_nested_or::<u64>(DataField::ExpectedHashrate, "smallCoreCount", 0u64);
327
328        let board_frequency = data.extract_nested_map::<f64, _>(
329            DataField::Hashboards,
330            "frequency",
331            Frequency::from_megahertz,
332        );
333
334        Some(HashRate {
335            value: core_count as f64
336                * total_chips.unwrap_or(0) as f64
337                * board_frequency
338                    .unwrap_or(Frequency::from_megahertz(0f64))
339                    .as_gigahertz(),
340            unit: HashRateUnit::GigaHash,
341            algo: "SHA256".to_string(),
342        })
343    }
344}
345impl GetFans for Bitaxe200 {
346    fn parse_fans(&self, data: &HashMap<DataField, Value>) -> Vec<FanData> {
347        data.extract_map_or::<f64, _>(DataField::Fans, Vec::new(), |f| {
348            vec![FanData {
349                position: 0,
350                rpm: Some(AngularVelocity::from_rpm(f)),
351            }]
352        })
353    }
354}
355impl GetPsuFans for Bitaxe200 {
356    // N/A
357}
358impl GetFluidTemperature for Bitaxe200 {
359    // N/A
360}
361impl GetWattage for Bitaxe200 {
362    fn parse_wattage(&self, data: &HashMap<DataField, Value>) -> Option<Power> {
363        data.extract_map::<f64, _>(DataField::Wattage, Power::from_watts)
364    }
365}
366impl GetWattageLimit for Bitaxe200 {
367    // N/A
368}
369impl GetLightFlashing for Bitaxe200 {
370    // N/A
371}
372impl GetMessages for Bitaxe200 {
373    fn parse_messages(&self, data: &HashMap<DataField, Value>) -> Vec<MinerMessage> {
374        let mut messages = Vec::new();
375        let timestamp = SystemTime::now()
376            .duration_since(UNIX_EPOCH)
377            .expect("Failed to get system time")
378            .as_secs();
379
380        let is_overheating = data.extract_nested::<bool>(DataField::Hashboards, "overheat_mode");
381
382        if let Some(true) = is_overheating {
383            messages.push(MinerMessage {
384                timestamp: timestamp as u32,
385                code: 0u64,
386                message: "Overheat Mode is Enabled!".to_string(),
387                severity: MessageSeverity::Warning,
388            });
389        };
390        messages
391    }
392}
393
394impl GetUptime for Bitaxe200 {
395    fn parse_uptime(&self, data: &HashMap<DataField, Value>) -> Option<Duration> {
396        data.extract_map::<u64, _>(DataField::Uptime, Duration::from_secs)
397    }
398}
399impl GetIsMining for Bitaxe200 {
400    fn parse_is_mining(&self, data: &HashMap<DataField, Value>) -> bool {
401        let hashrate = self.parse_hashrate(data);
402        hashrate.as_ref().is_some_and(|hr| hr.value > 0.0)
403    }
404}
405impl GetPools for Bitaxe200 {
406    fn parse_pools(&self, data: &HashMap<DataField, Value>) -> Vec<PoolData> {
407        let main_url =
408            data.extract_nested_or::<String>(DataField::Pools, "stratumURL", String::new());
409        let main_port = data.extract_nested_or::<u64>(DataField::Pools, "stratumPort", 0);
410        let accepted_share = data.extract_nested::<u64>(DataField::Pools, "sharesAccepted");
411        let rejected_share = data.extract_nested::<u64>(DataField::Pools, "sharesRejected");
412        let main_user = data.extract_nested::<String>(DataField::Pools, "stratumUser");
413
414        let is_using_fallback =
415            data.extract_nested_or::<bool>(DataField::Pools, "isUsingFallbackStratum", false);
416
417        let main_pool_url = PoolURL {
418            scheme: PoolScheme::StratumV1,
419            host: main_url,
420            port: main_port as u16,
421            pubkey: None,
422        };
423
424        let main_pool_data = PoolData {
425            position: Some(0),
426            url: Some(main_pool_url),
427            accepted_shares: accepted_share,
428            rejected_shares: rejected_share,
429            active: Some(!is_using_fallback),
430            alive: None,
431            user: main_user,
432        };
433
434        // Extract fallback pool data
435        let fallback_url =
436            data.extract_nested_or::<String>(DataField::Pools, "fallbackStratumURL", String::new());
437        let fallback_port =
438            data.extract_nested_or::<u64>(DataField::Pools, "fallbackStratumPort", 0);
439        let fallback_user = data.extract_nested(DataField::Pools, "fallbackStratumUser");
440        let fallback_pool_url = PoolURL {
441            scheme: PoolScheme::StratumV1,
442            host: fallback_url,
443            port: fallback_port as u16,
444            pubkey: None,
445        };
446
447        let fallback_pool_data = PoolData {
448            position: Some(1),
449            url: Some(fallback_pool_url),
450            accepted_shares: accepted_share,
451            rejected_shares: rejected_share,
452            active: Some(is_using_fallback),
453            alive: None,
454            user: fallback_user,
455        };
456
457        vec![main_pool_data, fallback_pool_data]
458    }
459}
460
461#[async_trait]
462impl SetFaultLight for Bitaxe200 {
463    #[allow(unused_variables)]
464    async fn set_fault_light(&self, fault: bool) -> anyhow::Result<bool> {
465        anyhow::bail!("Unsupported command");
466    }
467}
468
469#[async_trait]
470impl SetPowerLimit for Bitaxe200 {
471    #[allow(unused_variables)]
472    async fn set_power_limit(&self, limit: Power) -> anyhow::Result<bool> {
473        anyhow::bail!("Unsupported command");
474    }
475}
476
477#[async_trait]
478impl Restart for Bitaxe200 {
479    async fn restart(&self) -> anyhow::Result<bool> {
480        anyhow::bail!("Unsupported command");
481    }
482}
483
484#[async_trait]
485impl Pause for Bitaxe200 {
486    #[allow(unused_variables)]
487    async fn pause(&self, at_time: Option<Duration>) -> anyhow::Result<bool> {
488        anyhow::bail!("Unsupported command");
489    }
490}
491
492#[async_trait]
493impl Resume for Bitaxe200 {
494    #[allow(unused_variables)]
495    async fn resume(&self, at_time: Option<Duration>) -> anyhow::Result<bool> {
496        anyhow::bail!("Unsupported command");
497    }
498}
499
500#[cfg(test)]
501mod tests {
502    use super::*;
503    use crate::data::device::models::bitaxe::BitaxeModel;
504    use crate::test::api::MockAPIClient;
505    use crate::test::json::bitaxe::v2_0_0::SYSTEM_INFO_COMMAND;
506
507    #[tokio::test]
508    async fn test_espminer_200_data_parsers() {
509        let miner = Bitaxe200::new(
510            IpAddr::from([127, 0, 0, 1]),
511            MinerModel::Bitaxe(BitaxeModel::Supra),
512        );
513        let mut results = HashMap::new();
514        let system_info_command: MinerCommand = MinerCommand::WebAPI {
515            command: "system/info",
516            parameters: None,
517        };
518        results.insert(
519            system_info_command,
520            Value::from_str(SYSTEM_INFO_COMMAND).unwrap(),
521        );
522        let mock_api = MockAPIClient::new(results);
523
524        let mut collector = DataCollector::new_with_client(&miner, &mock_api);
525        let data = collector.collect_all().await;
526
527        let miner_data = miner.parse_data(data);
528
529        assert_eq!(&miner_data.ip, &miner.ip);
530        assert_eq!(
531            &miner_data.mac.unwrap(),
532            &MacAddr::from_str("AA:BB:CC:DD:EE:FF").unwrap()
533        );
534        assert_eq!(&miner_data.device_info, &miner.device_info);
535        assert_eq!(&miner_data.hostname, &Some("bitaxe".to_string()));
536        assert_eq!(
537            &miner_data.api_version,
538            &Some("v2.4.5-3-gb5d1e36-dirty".to_string())
539        );
540        assert_eq!(
541            &miner_data.firmware_version,
542            &Some("v2.4.5-3-gb5d1e36-dirty".to_string())
543        );
544        assert_eq!(
545            &miner_data.control_board_version,
546            &Some(MinerControlBoard::from_str("401").unwrap())
547        );
548        assert_eq!(
549            &miner_data.hashrate,
550            &Some(HashRate {
551                value: 1f64,
552                unit: HashRateUnit::TeraHash,
553                algo: "SHA256".to_string(),
554            })
555        );
556        assert_eq!(&miner_data.total_chips, &Some(1u16));
557        assert_eq!(
558            &miner_data.fans,
559            &vec![FanData {
560                position: 0,
561                rpm: Some(AngularVelocity::from_rpm(3517f64)),
562            }]
563        );
564        assert_eq!(
565            &miner_data.wattage,
566            &Some(Power::from_watts(2.65000009536743))
567        )
568    }
569}