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

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