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