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};
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) -> Result<Value> {
53 match command {
54 MinerCommand::WebAPI { .. } => self.web.get_api_result(command).await,
55 _ => Err(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 let system_info_cmd: MinerCommand = MinerCommand::WebAPI {
64 command: "system/info",
65 parameters: None,
66 };
67 let asic_info_cmd: MinerCommand = MinerCommand::WebAPI {
68 command: "system/asic",
69 parameters: None,
70 };
71
72 match data_field {
73 DataField::Mac => vec![(
74 system_info_cmd,
75 DataExtractor {
76 func: get_by_key,
77 key: Some("macAddr"),
78 tag: None,
79 },
80 )],
81 DataField::Hostname => vec![(
82 system_info_cmd,
83 DataExtractor {
84 func: get_by_key,
85 key: Some("hostname"),
86 tag: None,
87 },
88 )],
89 DataField::FirmwareVersion => vec![(
90 system_info_cmd,
91 DataExtractor {
92 func: get_by_key,
93 key: Some("version"),
94 tag: None,
95 },
96 )],
97 DataField::ApiVersion => vec![(
98 system_info_cmd,
99 DataExtractor {
100 func: get_by_key,
101 key: Some("version"),
102 tag: None,
103 },
104 )],
105 DataField::ControlBoardVersion => vec![(
106 system_info_cmd,
107 DataExtractor {
108 func: get_by_key,
109 key: Some("boardVersion"),
110 tag: None,
111 },
112 )],
113 DataField::ExpectedHashrate => vec![(
114 system_info_cmd,
115 DataExtractor {
116 func: get_by_key,
117 key: Some("expectedHashrate"),
118 tag: None,
119 },
120 )],
121 DataField::Hashboards => vec![
122 (
123 system_info_cmd,
124 DataExtractor {
125 func: get_by_pointer,
126 key: Some(""),
127 tag: None,
128 },
129 ),
130 (
131 asic_info_cmd,
132 DataExtractor {
133 func: get_by_pointer,
134 key: Some(""),
135 tag: None,
136 },
137 ),
138 ],
139 DataField::Hashrate => vec![(
140 system_info_cmd,
141 DataExtractor {
142 func: get_by_key,
143 key: Some("hashRate"),
144 tag: None,
145 },
146 )],
147 DataField::Fans => vec![(
148 system_info_cmd,
149 DataExtractor {
150 func: get_by_key,
151 key: Some("fanrpm"),
152 tag: None,
153 },
154 )],
155 DataField::AverageTemperature => vec![(
156 system_info_cmd,
157 DataExtractor {
158 func: get_by_key,
159 key: Some("temp"),
160 tag: None,
161 },
162 )],
163 DataField::Wattage => vec![(
164 system_info_cmd,
165 DataExtractor {
166 func: get_by_key,
167 key: Some("power"),
168 tag: None,
169 },
170 )],
171 DataField::Uptime => vec![(
172 system_info_cmd,
173 DataExtractor {
174 func: get_by_key,
175 key: Some("uptimeSeconds"),
176 tag: None,
177 },
178 )],
179 DataField::Pools => vec![(
180 system_info_cmd,
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(&self, data: &HashMap<DataField, Value>) -> Option<String> {
236 data.extract::<String>(DataField::ControlBoardVersion)
237 }
238}
239impl GetHashboards for BitAxe290 {
240 fn parse_hashboards(&self, data: &HashMap<DataField, Value>) -> Vec<BoardData> {
241 let board_voltage = data.extract_nested_map::<f64, _>(
243 DataField::Hashboards,
244 "voltage",
245 Voltage::from_millivolts,
246 );
247
248 let board_temperature = data.extract_nested_map::<f64, _>(
249 DataField::Hashboards,
250 "vrTemp",
251 Temperature::from_celsius,
252 );
253
254 let board_frequency = data.extract_nested_map::<f64, _>(
255 DataField::Hashboards,
256 "frequency",
257 Frequency::from_megahertz,
258 );
259
260 let chip_temperature = data.extract_nested_map::<f64, _>(
261 DataField::Hashboards,
262 "temp",
263 Temperature::from_celsius,
264 );
265
266 let expected_hashrate = Some(HashRate {
267 value: data.extract_nested_or::<f64>(DataField::Hashboards, "expectedHashrate", 0.0),
268 unit: HashRateUnit::GigaHash,
269 algo: "SHA256".to_string(),
270 });
271
272 let board_hashrate = Some(HashRate {
273 value: data.extract_nested_or::<f64>(DataField::Hashboards, "hashRate", 0.0),
274 unit: HashRateUnit::GigaHash,
275 algo: "SHA256".to_string(),
276 });
277
278 let total_chips =
279 data.extract_nested_map::<u64, _>(DataField::Hashboards, "asicCount", |u| u as u16);
280
281 let chip_info = ChipData {
282 position: 0,
283 temperature: chip_temperature,
284 voltage: board_voltage,
285 frequency: board_frequency,
286 tuned: Some(true),
287 working: Some(true),
288 hashrate: board_hashrate.clone(),
289 };
290
291 let board_data = BoardData {
292 position: 0,
293 hashrate: board_hashrate,
294 expected_hashrate,
295 board_temperature,
296 intake_temperature: board_temperature,
297 outlet_temperature: board_temperature,
298 expected_chips: self.device_info.hardware.chips,
299 working_chips: total_chips,
300 serial_number: None,
301 chips: vec![chip_info],
302 voltage: board_voltage,
303 frequency: board_frequency,
304 tuned: Some(true),
305 active: Some(true),
306 };
307
308 vec![board_data]
309 }
310}
311impl GetHashrate for BitAxe290 {
312 fn parse_hashrate(&self, data: &HashMap<DataField, Value>) -> Option<HashRate> {
313 data.extract_map::<f64, _>(DataField::Hashrate, |f| HashRate {
314 value: f,
315 unit: HashRateUnit::TeraHash,
316 algo: String::from("SHA256"),
317 })
318 }
319}
320
321impl GetExpectedHashrate for BitAxe290 {
322 fn parse_expected_hashrate(&self, data: &HashMap<DataField, Value>) -> Option<HashRate> {
323 data.extract_map::<f64, _>(DataField::Hashrate, |f| HashRate {
324 value: f,
325 unit: HashRateUnit::TeraHash,
326 algo: String::from("SHA256"),
327 })
328 }
329}
330impl GetFans for BitAxe290 {
331 fn parse_fans(&self, data: &HashMap<DataField, Value>) -> Vec<FanData> {
332 data.extract_map_or::<f64, _>(DataField::Fans, Vec::new(), |f| {
333 vec![FanData {
334 position: 0,
335 rpm: Some(AngularVelocity::from_rpm(f)),
336 }]
337 })
338 }
339}
340impl GetPsuFans for BitAxe290 {
341 }
343impl GetFluidTemperature for BitAxe290 {
344 }
346impl GetWattage for BitAxe290 {
347 fn parse_wattage(&self, data: &HashMap<DataField, Value>) -> Option<Power> {
348 data.extract_map::<f64, _>(DataField::Wattage, Power::from_watts)
349 }
350}
351impl GetWattageLimit for BitAxe290 {
352 }
354impl GetLightFlashing for BitAxe290 {
355 }
357impl GetMessages for BitAxe290 {
358 fn parse_messages(&self, data: &HashMap<DataField, Value>) -> Vec<MinerMessage> {
359 let mut messages = Vec::new();
360 let timestamp = SystemTime::now()
361 .duration_since(UNIX_EPOCH)
362 .expect("Failed to get system time")
363 .as_secs();
364
365 let is_overheating = data.extract_nested::<bool>(DataField::Hashboards, "overheat_mode");
366
367 if let Some(true) = is_overheating {
368 messages.push(MinerMessage {
369 timestamp: timestamp as u32,
370 code: 0u64,
371 message: "Overheat Mode is Enabled!".to_string(),
372 severity: MessageSeverity::Warning,
373 });
374 };
375 messages
376 }
377}
378impl GetUptime for BitAxe290 {
379 fn parse_uptime(&self, data: &HashMap<DataField, Value>) -> Option<Duration> {
380 data.extract_map::<u64, _>(DataField::Uptime, Duration::from_secs)
381 }
382}
383impl GetIsMining for BitAxe290 {
384 fn parse_is_mining(&self, data: &HashMap<DataField, Value>) -> bool {
385 let hashrate = self.parse_hashrate(data);
386 hashrate.as_ref().is_some_and(|hr| hr.value > 0.0)
387 }
388}
389impl GetPools for BitAxe290 {
390 fn parse_pools(&self, data: &HashMap<DataField, Value>) -> Vec<PoolData> {
391 let main_url =
392 data.extract_nested_or::<String>(DataField::Pools, "stratumURL", String::new());
393 let main_port = data.extract_nested_or::<u64>(DataField::Pools, "stratumPort", 0);
394 let accepted_share = data.extract_nested::<u64>(DataField::Pools, "sharesAccepted");
395 let rejected_share = data.extract_nested::<u64>(DataField::Pools, "sharesRejected");
396 let main_user = data.extract_nested::<String>(DataField::Pools, "stratumUser");
397
398 let is_using_fallback =
399 data.extract_nested_or::<bool>(DataField::Pools, "isUsingFallbackStratum", false);
400
401 let main_pool_url = PoolURL {
402 scheme: PoolScheme::StratumV1,
403 host: main_url,
404 port: main_port as u16,
405 pubkey: None,
406 };
407
408 let main_pool_data = PoolData {
409 position: Some(0),
410 url: Some(main_pool_url),
411 accepted_shares: accepted_share,
412 rejected_shares: rejected_share,
413 active: Some(!is_using_fallback),
414 alive: None,
415 user: main_user,
416 };
417
418 let fallback_url =
420 data.extract_nested_or::<String>(DataField::Pools, "fallbackStratumURL", String::new());
421 let fallback_port =
422 data.extract_nested_or::<u64>(DataField::Pools, "fallbackStratumPort", 0);
423 let fallback_user = data.extract_nested(DataField::Pools, "fallbackStratumUser");
424 let fallback_pool_url = PoolURL {
425 scheme: PoolScheme::StratumV1,
426 host: fallback_url,
427 port: fallback_port as u16,
428 pubkey: None,
429 };
430
431 let fallback_pool_data = PoolData {
432 position: Some(1),
433 url: Some(fallback_pool_url),
434 accepted_shares: accepted_share,
435 rejected_shares: rejected_share,
436 active: Some(is_using_fallback),
437 alive: None,
438 user: fallback_user,
439 };
440
441 vec![main_pool_data, fallback_pool_data]
442 }
443}