1#![allow(dead_code)]
2
3use super::shared::rpc::AvalonMinerRPCAPI;
4use crate::data::board::{BoardData, ChipData};
5use crate::data::device::MinerMake;
6use crate::data::device::{DeviceInfo, HashAlgorithm, MinerFirmware, MinerModel};
7use crate::data::fan::FanData;
8use crate::data::hashrate::{HashRate, HashRateUnit};
9use crate::data::pool::{PoolData, PoolURL};
10use crate::miners::backends::traits::*;
11use crate::miners::commands::MinerCommand;
12use crate::miners::data::{
13 DataCollector, DataExtensions, DataExtractor, DataField, DataLocation, get_by_pointer,
14};
15
16use crate::miners::api::RPCAPIClient;
17use anyhow::{Result, anyhow};
18use async_trait::async_trait;
19use macaddr::MacAddr;
20use measurements::{AngularVelocity, Power, Temperature, Voltage};
21use serde_json::{Value, json};
22use std::collections::HashMap;
23use std::net::IpAddr;
24use std::str::FromStr;
25use std::time::{Duration, SystemTime, UNIX_EPOCH};
26
27#[derive(Debug)]
28pub struct AvalonAMiner {
29 ip: IpAddr,
30 rpc: AvalonMinerRPCAPI,
31 device_info: DeviceInfo,
32}
33
34impl AvalonAMiner {
35 pub fn new(ip: IpAddr, model: MinerModel, miner_firmware: MinerFirmware) -> Self {
36 Self {
37 ip,
38 rpc: AvalonMinerRPCAPI::new(ip),
39 device_info: DeviceInfo::new(
40 MinerMake::AvalonMiner,
41 model,
42 miner_firmware,
43 HashAlgorithm::SHA256,
44 ),
45 }
46 }
47}
48
49#[async_trait]
50impl Restart for AvalonAMiner {
51 async fn restart(&self) -> Result<bool> {
52 let data = self.rpc.send_command("restart", false, None).await?;
53
54 if let Some(status) = data.get("STATUS").and_then(|s| s.as_str()) {
55 return Ok(status == "RESTART");
56 }
57
58 Ok(false)
59 }
60}
61#[async_trait]
62impl Pause for AvalonAMiner {
63 async fn pause(&self, after: Option<Duration>) -> Result<bool> {
64 let offset = after.unwrap_or(Duration::from_secs(5));
65 let shutdown_time = SystemTime::now() + offset;
66
67 let timestamp = shutdown_time
68 .duration_since(UNIX_EPOCH)
69 .expect("Shutdown time is before UNIX epoch")
70 .as_secs();
71
72 let data = self
73 .rpc
74 .send_command(
75 "ascset",
76 false,
77 Some(json!(["0", format!("softoff,1:{}", timestamp)])),
78 )
79 .await?;
80
81 if let Some(status) = data.get("STATUS").and_then(|s| s.as_array())
82 && !status.is_empty()
83 && let Some(status_code) = status[0].get("STATUS").and_then(|s| s.as_str())
84 && status_code == "I"
85 && let Some(msg) = status[0].get("Msg").and_then(|m| m.as_str())
86 {
87 return Ok(msg.contains("success softoff"));
88 }
89
90 Ok(false)
91 }
92}
93#[async_trait]
94impl Resume for AvalonAMiner {
95 async fn resume(&self, after: Option<Duration>) -> Result<bool> {
96 let offset = after.unwrap_or(Duration::from_secs(5));
97 let shutdown_time = SystemTime::now() + offset;
98
99 let timestamp = shutdown_time
100 .duration_since(UNIX_EPOCH)
101 .expect("Shutdown time is before UNIX epoch")
102 .as_secs();
103
104 let data = self
105 .rpc
106 .send_command(
107 "ascset",
108 false,
109 Some(json!(["0", format!("softon,1:{}", timestamp)])),
110 )
111 .await?;
112
113 if let Some(status) = data.get("STATUS").and_then(|s| s.as_array())
114 && !status.is_empty()
115 && let Some(status_code) = status[0].get("STATUS").and_then(|s| s.as_str())
116 && status_code == "I"
117 && let Some(msg) = status[0].get("Msg").and_then(|m| m.as_str())
118 {
119 return Ok(msg.contains("success softon"));
120 }
121 Ok(false)
122 }
123}
124#[async_trait]
125impl SetFaultLight for AvalonAMiner {
126 async fn set_fault_light(&self, fault: bool) -> Result<bool> {
127 let command = if fault { "1-1" } else { "1-0" };
128
129 let data = self
130 .rpc
131 .send_command("ascset", false, Some(json!(["0", "led", command])))
132 .await?;
133
134 if let Some(status) = data.get("STATUS").and_then(|s| s.as_array())
135 && let Some(msg) = status
136 .first()
137 .and_then(|s| s.get("Msg"))
138 .and_then(|m| m.as_str())
139 {
140 return Ok(msg == "ASC 0 set OK");
141 }
142
143 Err(anyhow!("Failed to set fault light to {}", command))
144 }
145}
146
147#[async_trait]
148impl SetPowerLimit for AvalonAMiner {
149 async fn set_power_limit(&self, limit: Power) -> Result<bool> {
150 let data = self
151 .rpc
152 .send_command(
153 "ascset",
154 false,
155 Some(json!(["0", "worklevel,set", limit.to_string()])),
156 )
157 .await?;
158
159 if let Some(status) = data.get("STATUS").and_then(|s| s.as_array())
160 && !status.is_empty()
161 && let Some(msg) = status[0].get("Msg").and_then(|m| m.as_str())
162 {
163 return Ok(msg == "ASC 0 set OK");
164 }
165
166 Err(anyhow!("Failed to set power limit"))
167 }
168}
169
170impl GetDataLocations for AvalonAMiner {
171 fn get_locations(&self, data_field: DataField) -> Vec<DataLocation> {
172 let version_cmd: MinerCommand = MinerCommand::RPC {
173 command: "version",
174 parameters: None,
175 };
176 let stats_cmd: MinerCommand = MinerCommand::RPC {
177 command: "stats",
178 parameters: None,
179 };
180 let devs_cmd: MinerCommand = MinerCommand::RPC {
181 command: "devs",
182 parameters: None,
183 };
184 let pools_cmd: MinerCommand = MinerCommand::RPC {
185 command: "pools",
186 parameters: None,
187 };
188
189 match data_field {
190 DataField::Mac => vec![(
191 version_cmd,
192 DataExtractor {
193 func: get_by_pointer,
194 key: Some("/VERSION/0/MAC"),
195 tag: None,
196 },
197 )],
198 DataField::ControlBoardVersion => vec![(
199 version_cmd,
200 DataExtractor {
201 func: get_by_pointer,
202 key: Some("/VERSION/0/HWTYPE"),
203 tag: None,
204 },
205 )],
206 DataField::ApiVersion => vec![(
207 version_cmd,
208 DataExtractor {
209 func: get_by_pointer,
210 key: Some("/VERSION/0/API"),
211 tag: None,
212 },
213 )],
214 DataField::FirmwareVersion => vec![(
215 version_cmd,
216 DataExtractor {
217 func: get_by_pointer,
218 key: Some("/VERSION/0/VERSION"),
219 tag: None,
220 },
221 )],
222 DataField::Hashrate => vec![(
223 devs_cmd,
224 DataExtractor {
225 func: get_by_pointer,
226 key: Some("/DEVS/0/MHS 1m"),
227 tag: None,
228 },
229 )],
230 DataField::ExpectedHashrate => vec![(
231 stats_cmd,
232 DataExtractor {
233 func: get_by_pointer,
234 key: Some("/STATS/0/MM ID0/STATS/GHSmm"),
235 tag: None,
236 },
237 )],
238 DataField::Hashboards => vec![(
239 stats_cmd,
240 DataExtractor {
241 func: get_by_pointer,
242 key: Some("/STATS/0/MM ID0"),
243 tag: None,
244 },
245 )],
246 DataField::Wattage => vec![(
247 stats_cmd,
248 DataExtractor {
249 func: get_by_pointer,
250 key: Some("/STATS/0/MM ID0/PS"),
251 tag: None,
252 },
253 )],
254 DataField::WattageLimit => vec![(
255 stats_cmd,
256 DataExtractor {
257 func: get_by_pointer,
258 key: Some("/STATS/0/MM ID0/PS"),
259 tag: None,
260 },
261 )],
262 DataField::Fans => vec![(
263 stats_cmd,
264 DataExtractor {
265 func: get_by_pointer,
266 key: Some("/STATS/0/MM ID0"),
267 tag: None,
268 },
269 )],
270 DataField::LightFlashing => vec![(
271 stats_cmd,
272 DataExtractor {
273 func: get_by_pointer,
274 key: Some("/STATS/0/MM ID0/Led"),
275 tag: None,
276 },
277 )],
278 DataField::Uptime => vec![(
279 stats_cmd,
280 DataExtractor {
281 func: get_by_pointer,
282 key: Some("/STATS/0/Elapsed"),
283 tag: None,
284 },
285 )],
286 DataField::Pools => vec![(
287 pools_cmd,
288 DataExtractor {
289 func: get_by_pointer,
290 key: Some("/POOLS"),
291 tag: None,
292 },
293 )],
294 _ => vec![],
295 }
296 }
297}
298
299impl GetIP for AvalonAMiner {
300 fn get_ip(&self) -> IpAddr {
301 self.ip
302 }
303}
304
305impl GetDeviceInfo for AvalonAMiner {
306 fn get_device_info(&self) -> DeviceInfo {
307 self.device_info
308 }
309}
310
311impl CollectData for AvalonAMiner {
312 fn get_collector(&self) -> DataCollector<'_> {
313 DataCollector::new(self, &self.rpc)
314 }
315}
316
317impl GetMAC for AvalonAMiner {
318 fn parse_mac(&self, data: &HashMap<DataField, Value>) -> Option<MacAddr> {
319 data.extract::<String>(DataField::Mac).and_then(|raw| {
320 let mut mac = raw.trim().to_lowercase();
321 if mac.len() == 12 && !mac.contains(':') {
323 let mut colon = String::with_capacity(17);
324 for (i, byte) in mac.chars().enumerate() {
325 if i > 0 && i % 2 == 0 {
326 colon.push(':');
327 }
328 colon.push(byte);
329 }
330 mac = colon;
331 }
332 MacAddr::from_str(&mac).ok()
333 })
334 }
335}
336
337impl GetSerialNumber for AvalonAMiner {}
338
339impl GetControlBoardVersion for AvalonAMiner {
340 fn parse_control_board_version(&self, data: &HashMap<DataField, Value>) -> Option<String> {
341 data.extract::<String>(DataField::ControlBoardVersion)
342 }
343}
344
345impl GetHostname for AvalonAMiner {}
346
347impl GetApiVersion for AvalonAMiner {
348 fn parse_api_version(&self, data: &HashMap<DataField, Value>) -> Option<String> {
349 data.extract::<String>(DataField::ApiVersion)
350 }
351}
352
353impl GetFirmwareVersion for AvalonAMiner {
354 fn parse_firmware_version(&self, data: &HashMap<DataField, Value>) -> Option<String> {
355 data.extract::<String>(DataField::FirmwareVersion)
356 }
357}
358
359impl GetHashboards for AvalonAMiner {
360 fn parse_hashboards(&self, data: &HashMap<DataField, Value>) -> Vec<BoardData> {
361 let hw = &self.device_info.hardware;
362 let board_cnt = hw.boards.unwrap_or(1) as usize;
363 let chips_per = hw.chips.unwrap_or(0);
364
365 let hb_info = match data.get(&DataField::Hashboards).and_then(|v| v.as_object()) {
366 Some(v) => v,
367 _ => return Vec::new(),
368 };
369
370 (0..board_cnt)
371 .map(|idx| {
372 let _chip_temp = hb_info
373 .get("MTmax")
374 .and_then(|v| v.as_array())
375 .and_then(|arr| arr.get(idx))
376 .and_then(|v| v.as_f64())
377 .map(Temperature::from_celsius);
378
379 let board_temp = hb_info
380 .get("MTavg")
381 .and_then(|v| v.as_array())
382 .and_then(|arr| arr.get(idx))
383 .and_then(|v| v.as_f64())
384 .map(Temperature::from_celsius);
385
386 let intake_temp = hb_info
387 .get("ITemp")
388 .and_then(|v| v.as_array())
389 .and_then(|arr| arr.get(idx))
390 .and_then(|v| v.as_f64())
391 .map(Temperature::from_celsius);
392
393 let hashrate = hb_info
394 .get("MGHS")
395 .and_then(|v| v.as_array())
396 .and_then(|arr| arr.get(idx))
397 .and_then(|v| v.as_f64())
398 .map(|r| HashRate {
399 value: r,
400 unit: HashRateUnit::GigaHash,
401 algo: "SHA256".into(),
402 });
403
404 let chip_temps: Vec<f64> = hb_info
405 .get(&format!("PVT_T{idx}"))
406 .and_then(|v| v.as_array())
407 .map(|arr| arr.iter().filter_map(|v| v.as_f64()).collect())
408 .unwrap_or_default();
409
410 let chip_volts: Vec<f64> = hb_info
411 .get(&format!("PVT_V{idx}"))
412 .and_then(|v| v.as_array())
413 .map(|arr| arr.iter().filter_map(|v| v.as_f64()).collect())
414 .unwrap_or_default();
415
416 let chip_works: Vec<f64> = hb_info
417 .get(&format!("MW{idx}"))
418 .and_then(|v| v.as_array())
419 .map(|arr| arr.iter().filter_map(|v| v.as_f64()).collect())
420 .unwrap_or_default();
421
422 let mut chips = Vec::new();
423 let max_len = chip_temps.len().max(chip_volts.len()).max(chip_works.len());
424
425 for pos in 0..max_len {
426 let temp = chip_temps.get(pos).copied().unwrap_or(0.0);
427 let volt = chip_volts.get(pos).copied().unwrap_or(0.0);
428 let work = chip_works.get(pos).copied().unwrap_or(0.0);
429
430 if temp == 0.0 {
431 continue;
432 }
433
434 chips.push(ChipData {
435 position: pos as u16,
436 temperature: Some(Temperature::from_celsius(temp)),
437 voltage: Some(Voltage::from_millivolts(volt)),
438 working: Some(work > 0.0),
439 ..Default::default()
440 });
441 }
442
443 let working_chips = chips.len() as u16;
444 let missing = working_chips == 0;
445
446 BoardData {
447 position: idx as u8,
448 expected_chips: Some(chips_per),
449 working_chips: Some(working_chips),
450 chips,
451 intake_temperature: intake_temp,
452 board_temperature: board_temp,
453 hashrate,
454 active: Some(!missing),
455 ..Default::default()
456 }
457 })
458 .collect()
459 }
460}
461
462impl GetHashrate for AvalonAMiner {
463 fn parse_hashrate(&self, data: &HashMap<DataField, Value>) -> Option<HashRate> {
464 data.extract_map::<f64, _>(DataField::Hashrate, |f| HashRate {
465 value: f,
466 unit: HashRateUnit::MegaHash,
467 algo: "SHA256".into(),
468 })
469 }
470}
471
472impl GetExpectedHashrate for AvalonAMiner {
473 fn parse_expected_hashrate(&self, data: &HashMap<DataField, Value>) -> Option<HashRate> {
474 data.extract_map::<f64, _>(DataField::ExpectedHashrate, |f| HashRate {
475 value: f,
476 unit: HashRateUnit::GigaHash,
477 algo: "SHA256".into(),
478 })
479 }
480}
481
482impl GetFans for AvalonAMiner {
483 fn parse_fans(&self, data: &HashMap<DataField, Value>) -> Vec<FanData> {
484 let stats = match data.get(&DataField::Fans) {
485 Some(v) => v,
486 _ => return Vec::new(),
487 };
488
489 let expected_fans = self.device_info.hardware.fans.unwrap_or(0) as usize;
490 if expected_fans == 0 {
491 return Vec::new();
492 }
493
494 (1..=expected_fans)
495 .filter_map(|idx| {
496 let key = format!("Fan{idx}");
497 stats
498 .get(&key)
499 .and_then(|val| val.as_f64())
500 .map(|rpm| FanData {
501 position: idx as i16,
502 rpm: Some(AngularVelocity::from_rpm(rpm)),
503 })
504 })
505 .collect()
506 }
507}
508
509impl GetPsuFans for AvalonAMiner {}
510
511impl GetWattage for AvalonAMiner {
512 fn parse_wattage(&self, data: &HashMap<DataField, Value>) -> Option<Power> {
513 let wattage = data.get(&DataField::Wattage).and_then(|v| v.as_array())?;
514 let wattage = wattage.get(4).and_then(|watts: &Value| watts.as_f64())?;
515 Some(Power::from_watts(wattage))
516 }
517}
518
519impl GetWattageLimit for AvalonAMiner {
520 fn parse_wattage_limit(&self, data: &HashMap<DataField, Value>) -> Option<Power> {
521 let limit = data
522 .get(&DataField::WattageLimit)
523 .and_then(|v| v.as_array())?;
524 let limit = limit.get(6).and_then(|watts: &Value| watts.as_f64())?;
525 Some(Power::from_watts(limit))
526 }
527}
528
529impl GetLightFlashing for AvalonAMiner {
530 fn parse_light_flashing(&self, data: &HashMap<DataField, Value>) -> Option<bool> {
531 data.extract::<bool>(DataField::LightFlashing)
532 }
533}
534
535impl GetMessages for AvalonAMiner {}
536
537impl GetUptime for AvalonAMiner {
538 fn parse_uptime(&self, data: &HashMap<DataField, Value>) -> Option<Duration> {
539 data.extract_map::<u64, _>(DataField::Uptime, Duration::from_secs)
540 }
541}
542
543impl GetFluidTemperature for AvalonAMiner {}
544impl GetIsMining for AvalonAMiner {}
545
546impl GetPools for AvalonAMiner {
547 fn parse_pools(&self, data: &HashMap<DataField, Value>) -> Vec<PoolData> {
548 data.get(&DataField::Pools)
549 .and_then(|v| v.as_array())
550 .map(|slice| slice.to_vec())
551 .unwrap_or_default()
552 .into_iter()
553 .enumerate()
554 .map(|(idx, pool)| PoolData {
555 url: pool
556 .get("URL")
557 .and_then(|v| v.as_str())
558 .map(|x| PoolURL::from(x.to_owned())),
559 user: pool.get("User").and_then(|v| v.as_str()).map(|s| s.into()),
560 position: Some(idx as u16),
561 alive: pool
562 .get("Status")
563 .and_then(|v| v.as_str())
564 .map(|s| s == "Alive"),
565 active: pool.get("Stratum Active").and_then(|v| v.as_bool()),
566 accepted_shares: pool.get("Accepted").and_then(|v| v.as_u64()),
567 rejected_shares: pool.get("Rejected").and_then(|v| v.as_u64()),
568 })
569 .collect()
570 }
571}
572
573#[cfg(test)]
574mod tests {
575 use super::*;
576 use crate::data::device::models::avalon::AvalonMinerModel::Avalon1246;
577 use crate::test::api::MockAPIClient;
578 use crate::test::json::cgminer::avalon::AVALON_A_STATS_PARSED;
579
580 #[tokio::test]
581 async fn test_avalon_a() -> Result<()> {
582 let miner = AvalonAMiner::new(
583 IpAddr::from([127, 0, 0, 1]),
584 MinerModel::Avalon(Avalon1246),
585 MinerFirmware::Stock,
586 );
587 let mut results = HashMap::new();
588 let stats_cmd: MinerCommand = MinerCommand::RPC {
589 command: "stats",
590 parameters: None,
591 };
592
593 results.insert(stats_cmd, Value::from_str(AVALON_A_STATS_PARSED)?);
594
595 let mock_api = MockAPIClient::new(results);
596 let mut collector = DataCollector::new(&miner, &mock_api);
597 let data = collector.collect_all().await;
598
599 let miner_data = miner.parse_data(data);
600
601 assert_eq!(miner_data.uptime, Some(Duration::from_secs(24684)));
602 assert_eq!(miner_data.wattage, Some(Power::from_watts(3189.0)));
603 assert_eq!(miner_data.fans.len(), 4);
604 assert_eq!(miner_data.hashboards[0].chips.len(), 120);
605 assert_eq!(
606 miner_data.average_temperature,
607 Some(Temperature::from_celsius(65.0))
608 );
609
610 Ok(())
611 }
612}