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

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