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};
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) -> anyhow::Result<Value> {
54 match command {
55 MinerCommand::WebAPI { .. } => self.web.get_api_result(command).await,
56 _ => Err(anyhow::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 const WEB_SYSTEM_INFO: MinerCommand = MinerCommand::WebAPI {
65 command: "system/info",
66 parameters: None,
67 };
68
69 match data_field {
70 DataField::Mac => vec![(
71 WEB_SYSTEM_INFO,
72 DataExtractor {
73 func: get_by_key,
74 key: Some("macAddr"),
75 tag: None,
76 },
77 )],
78 DataField::Hostname => vec![(
79 WEB_SYSTEM_INFO,
80 DataExtractor {
81 func: get_by_key,
82 key: Some("hostname"),
83 tag: None,
84 },
85 )],
86 DataField::FirmwareVersion => vec![(
87 WEB_SYSTEM_INFO,
88 DataExtractor {
89 func: get_by_key,
90 key: Some("version"),
91 tag: None,
92 },
93 )],
94 DataField::ApiVersion => vec![(
95 WEB_SYSTEM_INFO,
96 DataExtractor {
97 func: get_by_key,
98 key: Some("version"),
99 tag: None,
100 },
101 )],
102 DataField::ControlBoardVersion => vec![(
103 WEB_SYSTEM_INFO,
104 DataExtractor {
105 func: get_by_key,
106 key: Some("boardVersion"),
107 tag: None,
108 },
109 )],
110 DataField::Hashboards => vec![(
111 WEB_SYSTEM_INFO,
112 DataExtractor {
113 func: get_by_pointer,
114 key: Some(""),
115 tag: None,
116 },
117 )],
118 DataField::Hashrate => vec![(
119 WEB_SYSTEM_INFO,
120 DataExtractor {
121 func: get_by_key,
122 key: Some("hashRate"),
123 tag: None,
124 },
125 )],
126 DataField::ExpectedHashrate => vec![(
127 WEB_SYSTEM_INFO,
128 DataExtractor {
129 func: get_by_pointer,
130 key: Some(""),
131 tag: None,
132 },
133 )],
134 DataField::Fans => vec![(
135 WEB_SYSTEM_INFO,
136 DataExtractor {
137 func: get_by_key,
138 key: Some("fanrpm"),
139 tag: None,
140 },
141 )],
142 DataField::AverageTemperature => vec![(
143 WEB_SYSTEM_INFO,
144 DataExtractor {
145 func: get_by_key,
146 key: Some("temp"),
147 tag: None,
148 },
149 )],
150 DataField::Wattage => vec![(
151 WEB_SYSTEM_INFO,
152 DataExtractor {
153 func: get_by_key,
154 key: Some("power"),
155 tag: None,
156 },
157 )],
158 DataField::Uptime => vec![(
159 WEB_SYSTEM_INFO,
160 DataExtractor {
161 func: get_by_key,
162 key: Some("uptimeSeconds"),
163 tag: None,
164 },
165 )],
166 DataField::Pools => vec![(
167 WEB_SYSTEM_INFO,
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(
223 &self,
224 data: &HashMap<DataField, Value>,
225 ) -> Option<MinerControlBoard> {
226 data.extract::<String>(DataField::ControlBoardVersion)
227 .and_then(|s| MinerControlBoard::from_str(&s).ok())
228 }
229}
230impl GetHashboards for Bitaxe200 {
231 fn parse_hashboards(&self, data: &HashMap<DataField, Value>) -> Vec<BoardData> {
232 let board_voltage = data.extract_nested_map::<f64, _>(
234 DataField::Hashboards,
235 "voltage",
236 Voltage::from_millivolts,
237 );
238
239 let board_temperature = data.extract_nested_map::<f64, _>(
240 DataField::Hashboards,
241 "vrTemp",
242 Temperature::from_celsius,
243 );
244
245 let board_frequency = data.extract_nested_map::<f64, _>(
246 DataField::Hashboards,
247 "frequency",
248 Frequency::from_megahertz,
249 );
250
251 let chip_temperature = data.extract_nested_map::<f64, _>(
252 DataField::Hashboards,
253 "temp",
254 Temperature::from_celsius,
255 );
256
257 let board_hashrate = Some(HashRate {
258 value: data.extract_nested_or::<f64>(DataField::Hashboards, "hashRate", 0.0),
259 unit: HashRateUnit::GigaHash,
260 algo: "SHA256".to_string(),
261 });
262
263 let total_chips =
264 data.extract_nested_map::<u64, _>(DataField::Hashboards, "asicCount", |u| u as u16);
265
266 let core_count =
267 data.extract_nested_or::<u64>(DataField::Hashboards, "smallCoreCount", 0u64);
268
269 let expected_hashrate = Some(HashRate {
270 value: core_count as f64
271 * total_chips.unwrap_or(0) as f64
272 * board_frequency
273 .unwrap_or(Frequency::from_megahertz(0f64))
274 .as_gigahertz(),
275 unit: HashRateUnit::GigaHash,
276 algo: "SHA256".to_string(),
277 });
278
279 let chip_info = ChipData {
280 position: 0,
281 temperature: chip_temperature,
282 voltage: board_voltage,
283 frequency: board_frequency,
284 tuned: Some(true),
285 working: Some(true),
286 hashrate: board_hashrate.clone(),
287 };
288
289 let board_data = BoardData {
290 position: 0,
291 hashrate: board_hashrate,
292 expected_hashrate,
293 board_temperature,
294 intake_temperature: board_temperature,
295 outlet_temperature: board_temperature,
296 expected_chips: self.device_info.hardware.chips,
297 working_chips: total_chips,
298 serial_number: None,
299 chips: vec![chip_info],
300 voltage: board_voltage,
301 frequency: board_frequency,
302 tuned: Some(true),
303 active: Some(true),
304 };
305
306 vec![board_data]
307 }
308}
309impl GetHashrate for Bitaxe200 {
310 fn parse_hashrate(&self, data: &HashMap<DataField, Value>) -> Option<HashRate> {
311 data.extract_map::<f64, _>(DataField::Hashrate, |f| HashRate {
312 value: f,
313 unit: HashRateUnit::GigaHash,
314 algo: String::from("SHA256"),
315 })
316 }
317}
318impl GetExpectedHashrate for Bitaxe200 {
319 fn parse_expected_hashrate(&self, data: &HashMap<DataField, Value>) -> Option<HashRate> {
320 let total_chips =
321 data.extract_nested_map::<u64, _>(DataField::ExpectedHashrate, "asicCount", |u| {
322 u as u16
323 });
324
325 let core_count =
326 data.extract_nested_or::<u64>(DataField::ExpectedHashrate, "smallCoreCount", 0u64);
327
328 let board_frequency = data.extract_nested_map::<f64, _>(
329 DataField::Hashboards,
330 "frequency",
331 Frequency::from_megahertz,
332 );
333
334 Some(HashRate {
335 value: core_count as f64
336 * total_chips.unwrap_or(0) as f64
337 * board_frequency
338 .unwrap_or(Frequency::from_megahertz(0f64))
339 .as_gigahertz(),
340 unit: HashRateUnit::GigaHash,
341 algo: "SHA256".to_string(),
342 })
343 }
344}
345impl GetFans for Bitaxe200 {
346 fn parse_fans(&self, data: &HashMap<DataField, Value>) -> Vec<FanData> {
347 data.extract_map_or::<f64, _>(DataField::Fans, Vec::new(), |f| {
348 vec![FanData {
349 position: 0,
350 rpm: Some(AngularVelocity::from_rpm(f)),
351 }]
352 })
353 }
354}
355impl GetPsuFans for Bitaxe200 {
356 }
358impl GetFluidTemperature for Bitaxe200 {
359 }
361impl GetWattage for Bitaxe200 {
362 fn parse_wattage(&self, data: &HashMap<DataField, Value>) -> Option<Power> {
363 data.extract_map::<f64, _>(DataField::Wattage, Power::from_watts)
364 }
365}
366impl GetWattageLimit for Bitaxe200 {
367 }
369impl GetLightFlashing for Bitaxe200 {
370 }
372impl GetMessages for Bitaxe200 {
373 fn parse_messages(&self, data: &HashMap<DataField, Value>) -> Vec<MinerMessage> {
374 let mut messages = Vec::new();
375 let timestamp = SystemTime::now()
376 .duration_since(UNIX_EPOCH)
377 .expect("Failed to get system time")
378 .as_secs();
379
380 let is_overheating = data.extract_nested::<bool>(DataField::Hashboards, "overheat_mode");
381
382 if let Some(true) = is_overheating {
383 messages.push(MinerMessage {
384 timestamp: timestamp as u32,
385 code: 0u64,
386 message: "Overheat Mode is Enabled!".to_string(),
387 severity: MessageSeverity::Warning,
388 });
389 };
390 messages
391 }
392}
393
394impl GetUptime for Bitaxe200 {
395 fn parse_uptime(&self, data: &HashMap<DataField, Value>) -> Option<Duration> {
396 data.extract_map::<u64, _>(DataField::Uptime, Duration::from_secs)
397 }
398}
399impl GetIsMining for Bitaxe200 {
400 fn parse_is_mining(&self, data: &HashMap<DataField, Value>) -> bool {
401 let hashrate = self.parse_hashrate(data);
402 hashrate.as_ref().is_some_and(|hr| hr.value > 0.0)
403 }
404}
405impl GetPools for Bitaxe200 {
406 fn parse_pools(&self, data: &HashMap<DataField, Value>) -> Vec<PoolData> {
407 let main_url =
408 data.extract_nested_or::<String>(DataField::Pools, "stratumURL", String::new());
409 let main_port = data.extract_nested_or::<u64>(DataField::Pools, "stratumPort", 0);
410 let accepted_share = data.extract_nested::<u64>(DataField::Pools, "sharesAccepted");
411 let rejected_share = data.extract_nested::<u64>(DataField::Pools, "sharesRejected");
412 let main_user = data.extract_nested::<String>(DataField::Pools, "stratumUser");
413
414 let is_using_fallback =
415 data.extract_nested_or::<bool>(DataField::Pools, "isUsingFallbackStratum", false);
416
417 let main_pool_url = PoolURL {
418 scheme: PoolScheme::StratumV1,
419 host: main_url,
420 port: main_port as u16,
421 pubkey: None,
422 };
423
424 let main_pool_data = PoolData {
425 position: Some(0),
426 url: Some(main_pool_url),
427 accepted_shares: accepted_share,
428 rejected_shares: rejected_share,
429 active: Some(!is_using_fallback),
430 alive: None,
431 user: main_user,
432 };
433
434 let fallback_url =
436 data.extract_nested_or::<String>(DataField::Pools, "fallbackStratumURL", String::new());
437 let fallback_port =
438 data.extract_nested_or::<u64>(DataField::Pools, "fallbackStratumPort", 0);
439 let fallback_user = data.extract_nested(DataField::Pools, "fallbackStratumUser");
440 let fallback_pool_url = PoolURL {
441 scheme: PoolScheme::StratumV1,
442 host: fallback_url,
443 port: fallback_port as u16,
444 pubkey: None,
445 };
446
447 let fallback_pool_data = PoolData {
448 position: Some(1),
449 url: Some(fallback_pool_url),
450 accepted_shares: accepted_share,
451 rejected_shares: rejected_share,
452 active: Some(is_using_fallback),
453 alive: None,
454 user: fallback_user,
455 };
456
457 vec![main_pool_data, fallback_pool_data]
458 }
459}
460
461#[async_trait]
462impl SetFaultLight for Bitaxe200 {
463 #[allow(unused_variables)]
464 async fn set_fault_light(&self, fault: bool) -> anyhow::Result<bool> {
465 anyhow::bail!("Unsupported command");
466 }
467}
468
469#[async_trait]
470impl SetPowerLimit for Bitaxe200 {
471 #[allow(unused_variables)]
472 async fn set_power_limit(&self, limit: Power) -> anyhow::Result<bool> {
473 anyhow::bail!("Unsupported command");
474 }
475}
476
477#[async_trait]
478impl Restart for Bitaxe200 {
479 async fn restart(&self) -> anyhow::Result<bool> {
480 anyhow::bail!("Unsupported command");
481 }
482}
483
484#[async_trait]
485impl Pause for Bitaxe200 {
486 #[allow(unused_variables)]
487 async fn pause(&self, at_time: Option<Duration>) -> anyhow::Result<bool> {
488 anyhow::bail!("Unsupported command");
489 }
490}
491
492#[async_trait]
493impl Resume for Bitaxe200 {
494 #[allow(unused_variables)]
495 async fn resume(&self, at_time: Option<Duration>) -> anyhow::Result<bool> {
496 anyhow::bail!("Unsupported command");
497 }
498}
499
500#[cfg(test)]
501mod tests {
502 use super::*;
503 use crate::data::device::models::bitaxe::BitaxeModel;
504 use crate::test::api::MockAPIClient;
505 use crate::test::json::bitaxe::v2_0_0::SYSTEM_INFO_COMMAND;
506
507 #[tokio::test]
508 async fn test_espminer_200_data_parsers() {
509 let miner = Bitaxe200::new(
510 IpAddr::from([127, 0, 0, 1]),
511 MinerModel::Bitaxe(BitaxeModel::Supra),
512 );
513 let mut results = HashMap::new();
514 let system_info_command: MinerCommand = MinerCommand::WebAPI {
515 command: "system/info",
516 parameters: None,
517 };
518 results.insert(
519 system_info_command,
520 Value::from_str(SYSTEM_INFO_COMMAND).unwrap(),
521 );
522 let mock_api = MockAPIClient::new(results);
523
524 let mut collector = DataCollector::new_with_client(&miner, &mock_api);
525 let data = collector.collect_all().await;
526
527 let miner_data = miner.parse_data(data);
528
529 assert_eq!(&miner_data.ip, &miner.ip);
530 assert_eq!(
531 &miner_data.mac.unwrap(),
532 &MacAddr::from_str("AA:BB:CC:DD:EE:FF").unwrap()
533 );
534 assert_eq!(&miner_data.device_info, &miner.device_info);
535 assert_eq!(&miner_data.hostname, &Some("bitaxe".to_string()));
536 assert_eq!(
537 &miner_data.api_version,
538 &Some("v2.4.5-3-gb5d1e36-dirty".to_string())
539 );
540 assert_eq!(
541 &miner_data.firmware_version,
542 &Some("v2.4.5-3-gb5d1e36-dirty".to_string())
543 );
544 assert_eq!(
545 &miner_data.control_board_version,
546 &Some(MinerControlBoard::from_str("401").unwrap())
547 );
548 assert_eq!(
549 &miner_data.hashrate,
550 &Some(HashRate {
551 value: 1f64,
552 unit: HashRateUnit::TeraHash,
553 algo: "SHA256".to_string(),
554 })
555 );
556 assert_eq!(&miner_data.total_chips, &Some(1u16));
557 assert_eq!(
558 &miner_data.fans,
559 &vec![FanData {
560 position: 0,
561 rpm: Some(AngularVelocity::from_rpm(3517f64)),
562 }]
563 );
564 assert_eq!(
565 &miner_data.wattage,
566 &Some(Power::from_watts(2.65000009536743))
567 )
568 }
569}