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 }
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 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 }
337impl GetFluidTemperature for ESPMiner200 {
338 }
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 }
348impl GetLightFlashing for ESPMiner200 {
349 }
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 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}