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