asic_rs/miners/backends/avalonminer/avalon_a/
mod.rs

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            // compact 12-digit → colon-separated
322            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}