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