asic_rs/miners/backends/bitaxe/v2_9_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};
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) -> anyhow::Result<Value> {
53        match command {
54            MinerCommand::WebAPI { .. } => self.web.get_api_result(command).await,
55            _ => Err(anyhow::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        const WEB_SYSTEM_INFO: MinerCommand = MinerCommand::WebAPI {
64            command: "system/info",
65            parameters: None,
66        };
67        const WEB_ASIC_INFO: MinerCommand = MinerCommand::WebAPI {
68            command: "system/asic",
69            parameters: None,
70        };
71
72        match data_field {
73            DataField::Mac => vec![(
74                WEB_SYSTEM_INFO,
75                DataExtractor {
76                    func: get_by_key,
77                    key: Some("macAddr"),
78                    tag: None,
79                },
80            )],
81            DataField::Hostname => vec![(
82                WEB_SYSTEM_INFO,
83                DataExtractor {
84                    func: get_by_key,
85                    key: Some("hostname"),
86                    tag: None,
87                },
88            )],
89            DataField::FirmwareVersion => vec![(
90                WEB_SYSTEM_INFO,
91                DataExtractor {
92                    func: get_by_key,
93                    key: Some("version"),
94                    tag: None,
95                },
96            )],
97            DataField::ApiVersion => vec![(
98                WEB_SYSTEM_INFO,
99                DataExtractor {
100                    func: get_by_key,
101                    key: Some("version"),
102                    tag: None,
103                },
104            )],
105            DataField::ControlBoardVersion => vec![(
106                WEB_SYSTEM_INFO,
107                DataExtractor {
108                    func: get_by_key,
109                    key: Some("boardVersion"),
110                    tag: None,
111                },
112            )],
113            DataField::ExpectedHashrate => vec![(
114                WEB_SYSTEM_INFO,
115                DataExtractor {
116                    func: get_by_key,
117                    key: Some("expectedHashrate"),
118                    tag: None,
119                },
120            )],
121            DataField::Hashboards => vec![
122                (
123                    WEB_SYSTEM_INFO,
124                    DataExtractor {
125                        func: get_by_pointer,
126                        key: Some(""),
127                        tag: None,
128                    },
129                ),
130                (
131                    WEB_ASIC_INFO,
132                    DataExtractor {
133                        func: get_by_pointer,
134                        key: Some(""),
135                        tag: None,
136                    },
137                ),
138            ],
139            DataField::Hashrate => vec![(
140                WEB_SYSTEM_INFO,
141                DataExtractor {
142                    func: get_by_key,
143                    key: Some("hashRate"),
144                    tag: None,
145                },
146            )],
147            DataField::Fans => vec![(
148                WEB_SYSTEM_INFO,
149                DataExtractor {
150                    func: get_by_key,
151                    key: Some("fanrpm"),
152                    tag: None,
153                },
154            )],
155            DataField::AverageTemperature => vec![(
156                WEB_SYSTEM_INFO,
157                DataExtractor {
158                    func: get_by_key,
159                    key: Some("temp"),
160                    tag: None,
161                },
162            )],
163            DataField::Wattage => vec![(
164                WEB_SYSTEM_INFO,
165                DataExtractor {
166                    func: get_by_key,
167                    key: Some("power"),
168                    tag: None,
169                },
170            )],
171            DataField::Uptime => vec![(
172                WEB_SYSTEM_INFO,
173                DataExtractor {
174                    func: get_by_key,
175                    key: Some("uptimeSeconds"),
176                    tag: None,
177                },
178            )],
179            DataField::Pools => vec![(
180                WEB_SYSTEM_INFO,
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(
236        &self,
237        data: &HashMap<DataField, Value>,
238    ) -> Option<MinerControlBoard> {
239        data.extract::<String>(DataField::ControlBoardVersion)
240            .and_then(|s| MinerControlBoard::from_str(&s).ok())
241    }
242}
243impl GetHashboards for Bitaxe290 {
244    fn parse_hashboards(&self, data: &HashMap<DataField, Value>) -> Vec<BoardData> {
245        // Extract nested values with type conversion
246        let board_voltage = data.extract_nested_map::<f64, _>(
247            DataField::Hashboards,
248            "voltage",
249            Voltage::from_millivolts,
250        );
251
252        let board_temperature = data.extract_nested_map::<f64, _>(
253            DataField::Hashboards,
254            "vrTemp",
255            Temperature::from_celsius,
256        );
257
258        let board_frequency = data.extract_nested_map::<f64, _>(
259            DataField::Hashboards,
260            "frequency",
261            Frequency::from_megahertz,
262        );
263
264        let chip_temperature = data.extract_nested_map::<f64, _>(
265            DataField::Hashboards,
266            "temp",
267            Temperature::from_celsius,
268        );
269
270        let expected_hashrate = Some(HashRate {
271            value: data.extract_nested_or::<f64>(DataField::Hashboards, "expectedHashrate", 0.0),
272            unit: HashRateUnit::GigaHash,
273            algo: "SHA256".to_string(),
274        });
275
276        let board_hashrate = Some(HashRate {
277            value: data.extract_nested_or::<f64>(DataField::Hashboards, "hashRate", 0.0),
278            unit: HashRateUnit::GigaHash,
279            algo: "SHA256".to_string(),
280        });
281
282        let total_chips =
283            data.extract_nested_map::<u64, _>(DataField::Hashboards, "asicCount", |u| u as u16);
284
285        let chip_info = ChipData {
286            position: 0,
287            temperature: chip_temperature,
288            voltage: board_voltage,
289            frequency: board_frequency,
290            tuned: Some(true),
291            working: Some(true),
292            hashrate: board_hashrate.clone(),
293        };
294
295        let board_data = BoardData {
296            position: 0,
297            hashrate: board_hashrate,
298            expected_hashrate,
299            board_temperature,
300            intake_temperature: board_temperature,
301            outlet_temperature: board_temperature,
302            expected_chips: self.device_info.hardware.chips,
303            working_chips: total_chips,
304            serial_number: None,
305            chips: vec![chip_info],
306            voltage: board_voltage,
307            frequency: board_frequency,
308            tuned: Some(true),
309            active: Some(true),
310        };
311
312        vec![board_data]
313    }
314}
315impl GetHashrate for Bitaxe290 {
316    fn parse_hashrate(&self, data: &HashMap<DataField, Value>) -> Option<HashRate> {
317        data.extract_map::<f64, _>(DataField::Hashrate, |f| HashRate {
318            value: f,
319            unit: HashRateUnit::GigaHash,
320            algo: String::from("SHA256"),
321        })
322    }
323}
324
325impl GetExpectedHashrate for Bitaxe290 {
326    fn parse_expected_hashrate(&self, data: &HashMap<DataField, Value>) -> Option<HashRate> {
327        data.extract_map::<f64, _>(DataField::ExpectedHashrate, |f| HashRate {
328            value: f,
329            unit: HashRateUnit::GigaHash,
330            algo: String::from("SHA256"),
331        })
332    }
333}
334impl GetFans for Bitaxe290 {
335    fn parse_fans(&self, data: &HashMap<DataField, Value>) -> Vec<FanData> {
336        data.extract_map_or::<f64, _>(DataField::Fans, Vec::new(), |f| {
337            vec![FanData {
338                position: 0,
339                rpm: Some(AngularVelocity::from_rpm(f)),
340            }]
341        })
342    }
343}
344impl GetPsuFans for Bitaxe290 {
345    // N/A
346}
347impl GetFluidTemperature for Bitaxe290 {
348    // N/A
349}
350impl GetWattage for Bitaxe290 {
351    fn parse_wattage(&self, data: &HashMap<DataField, Value>) -> Option<Power> {
352        data.extract_map::<f64, _>(DataField::Wattage, Power::from_watts)
353    }
354}
355impl GetWattageLimit for Bitaxe290 {
356    // N/A
357}
358impl GetLightFlashing for Bitaxe290 {
359    // N/A
360}
361impl GetMessages for Bitaxe290 {
362    fn parse_messages(&self, data: &HashMap<DataField, Value>) -> Vec<MinerMessage> {
363        let mut messages = Vec::new();
364        let timestamp = SystemTime::now()
365            .duration_since(UNIX_EPOCH)
366            .expect("Failed to get system time")
367            .as_secs();
368
369        let is_overheating = data.extract_nested::<bool>(DataField::Hashboards, "overheat_mode");
370
371        if let Some(true) = is_overheating {
372            messages.push(MinerMessage {
373                timestamp: timestamp as u32,
374                code: 0u64,
375                message: "Overheat Mode is Enabled!".to_string(),
376                severity: MessageSeverity::Warning,
377            });
378        };
379        messages
380    }
381}
382impl GetUptime for Bitaxe290 {
383    fn parse_uptime(&self, data: &HashMap<DataField, Value>) -> Option<Duration> {
384        data.extract_map::<u64, _>(DataField::Uptime, Duration::from_secs)
385    }
386}
387impl GetIsMining for Bitaxe290 {
388    fn parse_is_mining(&self, data: &HashMap<DataField, Value>) -> bool {
389        let hashrate = self.parse_hashrate(data);
390        hashrate.as_ref().is_some_and(|hr| hr.value > 0.0)
391    }
392}
393impl GetPools for Bitaxe290 {
394    fn parse_pools(&self, data: &HashMap<DataField, Value>) -> Vec<PoolData> {
395        let main_url =
396            data.extract_nested_or::<String>(DataField::Pools, "stratumURL", String::new());
397        let main_port = data.extract_nested_or::<u64>(DataField::Pools, "stratumPort", 0);
398        let accepted_share = data.extract_nested::<u64>(DataField::Pools, "sharesAccepted");
399        let rejected_share = data.extract_nested::<u64>(DataField::Pools, "sharesRejected");
400        let main_user = data.extract_nested::<String>(DataField::Pools, "stratumUser");
401
402        let is_using_fallback =
403            data.extract_nested_or::<bool>(DataField::Pools, "isUsingFallbackStratum", false);
404
405        let main_pool_url = PoolURL {
406            scheme: PoolScheme::StratumV1,
407            host: main_url,
408            port: main_port as u16,
409            pubkey: None,
410        };
411
412        let main_pool_data = PoolData {
413            position: Some(0),
414            url: Some(main_pool_url),
415            accepted_shares: accepted_share,
416            rejected_shares: rejected_share,
417            active: Some(!is_using_fallback),
418            alive: None,
419            user: main_user,
420        };
421
422        // Extract fallback pool data
423        let fallback_url =
424            data.extract_nested_or::<String>(DataField::Pools, "fallbackStratumURL", String::new());
425        let fallback_port =
426            data.extract_nested_or::<u64>(DataField::Pools, "fallbackStratumPort", 0);
427        let fallback_user = data.extract_nested(DataField::Pools, "fallbackStratumUser");
428        let fallback_pool_url = PoolURL {
429            scheme: PoolScheme::StratumV1,
430            host: fallback_url,
431            port: fallback_port as u16,
432            pubkey: None,
433        };
434
435        let fallback_pool_data = PoolData {
436            position: Some(1),
437            url: Some(fallback_pool_url),
438            accepted_shares: accepted_share,
439            rejected_shares: rejected_share,
440            active: Some(is_using_fallback),
441            alive: None,
442            user: fallback_user,
443        };
444
445        vec![main_pool_data, fallback_pool_data]
446    }
447}
448
449#[async_trait]
450impl SetFaultLight for Bitaxe290 {
451    #[allow(unused_variables)]
452    async fn set_fault_light(&self, fault: bool) -> anyhow::Result<bool> {
453        anyhow::bail!("Unsupported command");
454    }
455}
456
457#[async_trait]
458impl SetPowerLimit for Bitaxe290 {
459    #[allow(unused_variables)]
460    async fn set_power_limit(&self, limit: Power) -> anyhow::Result<bool> {
461        anyhow::bail!("Unsupported command");
462    }
463}
464
465#[async_trait]
466impl Restart for Bitaxe290 {
467    async fn restart(&self) -> anyhow::Result<bool> {
468        anyhow::bail!("Unsupported command");
469    }
470}
471
472#[async_trait]
473impl Pause for Bitaxe290 {
474    #[allow(unused_variables)]
475    async fn pause(&self, at_time: Option<Duration>) -> anyhow::Result<bool> {
476        anyhow::bail!("Unsupported command");
477    }
478}
479
480#[async_trait]
481impl Resume for Bitaxe290 {
482    #[allow(unused_variables)]
483    async fn resume(&self, at_time: Option<Duration>) -> anyhow::Result<bool> {
484        anyhow::bail!("Unsupported command");
485    }
486}