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