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