asic_rs/miners/backends/vnish/v1_2_0/
mod.rs

1use 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;
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 web::VnishWebAPI;
24
25mod web;
26
27#[derive(Debug)]
28pub struct VnishV120 {
29    ip: IpAddr,
30    web: VnishWebAPI,
31    device_info: DeviceInfo,
32}
33
34impl VnishV120 {
35    pub fn new(ip: IpAddr, model: MinerModel) -> Self {
36        VnishV120 {
37            ip,
38            web: VnishWebAPI::new(ip, 80),
39            device_info: DeviceInfo::new(
40                MinerMake::from(model),
41                model,
42                MinerFirmware::VNish,
43                HashAlgorithm::SHA256,
44            ),
45        }
46    }
47}
48
49#[async_trait]
50impl APIClient for VnishV120 {
51    async fn get_api_result(&self, command: &MinerCommand) -> anyhow::Result<Value> {
52        match command {
53            MinerCommand::WebAPI { .. } => self.web.get_api_result(command).await,
54            _ => Err(anyhow::anyhow!("Unsupported command type for Vnish API")),
55        }
56    }
57}
58
59impl GetDataLocations for VnishV120 {
60    fn get_locations(&self, data_field: DataField) -> Vec<DataLocation> {
61        const WEB_INFO: MinerCommand = MinerCommand::WebAPI {
62            command: "info",
63            parameters: None,
64        };
65        const WEB_STATUS: MinerCommand = MinerCommand::WebAPI {
66            command: "status",
67            parameters: None,
68        };
69        const WEB_SUMMARY: MinerCommand = MinerCommand::WebAPI {
70            command: "summary",
71            parameters: None,
72        };
73        const WEB_CHAINS: MinerCommand = MinerCommand::WebAPI {
74            command: "chains",
75            parameters: None,
76        };
77        const WEB_FACTORY_INFO: MinerCommand = MinerCommand::WebAPI {
78            command: "chains/factory-info",
79            parameters: None,
80        };
81
82        match data_field {
83            DataField::Mac => vec![(
84                WEB_INFO,
85                DataExtractor {
86                    func: get_by_pointer,
87                    key: Some("/system/network_status/mac"),
88                    tag: None,
89                },
90            )],
91            DataField::SerialNumber => vec![
92                (
93                    WEB_FACTORY_INFO,
94                    DataExtractor {
95                        func: get_by_pointer,
96                        key: Some("/psu_serial"),
97                        tag: None,
98                    },
99                ),
100                (
101                    WEB_INFO,
102                    DataExtractor {
103                        func: get_by_pointer,
104                        key: Some("/serial"),
105                        tag: None,
106                    },
107                ),
108            ],
109            DataField::Hostname => vec![(
110                WEB_INFO,
111                DataExtractor {
112                    func: get_by_pointer,
113                    key: Some("/system/network_status/hostname"),
114                    tag: None,
115                },
116            )],
117            DataField::ApiVersion => vec![(
118                WEB_INFO,
119                DataExtractor {
120                    func: get_by_pointer,
121                    key: Some("/fw_version"),
122                    tag: None,
123                },
124            )],
125            DataField::FirmwareVersion => vec![(
126                WEB_INFO,
127                DataExtractor {
128                    func: get_by_pointer,
129                    key: Some("/fw_version"),
130                    tag: None,
131                },
132            )],
133            DataField::ControlBoardVersion => vec![(
134                WEB_INFO,
135                DataExtractor {
136                    func: get_by_pointer,
137                    key: Some("/platform"),
138                    tag: None,
139                },
140            )],
141            DataField::Uptime => vec![(
142                WEB_INFO,
143                DataExtractor {
144                    func: get_by_pointer,
145                    key: Some("/system/uptime"),
146                    tag: None,
147                },
148            )],
149            DataField::Hashrate => vec![(
150                WEB_SUMMARY,
151                DataExtractor {
152                    func: get_by_pointer,
153                    key: Some("/miner/hr_realtime"),
154                    tag: None,
155                },
156            )],
157            DataField::ExpectedHashrate => vec![
158                (
159                    WEB_FACTORY_INFO,
160                    DataExtractor {
161                        func: get_by_pointer,
162                        key: Some("/hr_stock"),
163                        tag: None,
164                    },
165                ),
166                (
167                    WEB_SUMMARY,
168                    DataExtractor {
169                        func: get_by_pointer,
170                        key: Some("/miner/hr_stock"),
171                        tag: None,
172                    },
173                ),
174            ],
175            DataField::Wattage => vec![(
176                WEB_SUMMARY,
177                DataExtractor {
178                    func: get_by_pointer,
179                    key: Some("/miner/power_consumption"),
180                    tag: None,
181                },
182            )],
183            DataField::Fans => vec![(
184                WEB_SUMMARY,
185                DataExtractor {
186                    func: get_by_pointer,
187                    key: Some("/miner/cooling/fans"),
188                    tag: None,
189                },
190            )],
191            DataField::Hashboards => vec![
192                (
193                    WEB_SUMMARY,
194                    DataExtractor {
195                        func: get_by_pointer,
196                        key: Some("/miner/chains"),
197                        tag: None,
198                    },
199                ),
200                (
201                    WEB_CHAINS,
202                    DataExtractor {
203                        func: get_by_pointer,
204                        key: Some(""),
205                        tag: None,
206                    },
207                ),
208            ],
209            DataField::Pools => vec![(
210                WEB_SUMMARY,
211                DataExtractor {
212                    func: get_by_pointer,
213                    key: Some("/miner/pools"),
214                    tag: None,
215                },
216            )],
217            DataField::IsMining => vec![(
218                WEB_STATUS,
219                DataExtractor {
220                    func: get_by_pointer,
221                    key: Some("/miner_state"),
222                    tag: None,
223                },
224            )],
225            DataField::LightFlashing => vec![(
226                WEB_STATUS,
227                DataExtractor {
228                    func: get_by_pointer,
229                    key: Some("/find_miner"),
230                    tag: None,
231                },
232            )],
233            _ => vec![],
234        }
235    }
236}
237
238impl GetIP for VnishV120 {
239    fn get_ip(&self) -> IpAddr {
240        self.ip
241    }
242}
243
244impl GetDeviceInfo for VnishV120 {
245    fn get_device_info(&self) -> DeviceInfo {
246        self.device_info
247    }
248}
249
250impl CollectData for VnishV120 {
251    fn get_collector(&self) -> DataCollector<'_> {
252        DataCollector::new(self)
253    }
254}
255
256impl GetMAC for VnishV120 {
257    fn parse_mac(&self, data: &HashMap<DataField, Value>) -> Option<MacAddr> {
258        data.extract::<String>(DataField::Mac)
259            .and_then(|s| MacAddr::from_str(&s).ok())
260    }
261}
262
263impl GetSerialNumber for VnishV120 {
264    fn parse_serial_number(&self, data: &HashMap<DataField, Value>) -> Option<String> {
265        data.extract::<String>(DataField::SerialNumber)
266    }
267}
268
269impl GetHostname for VnishV120 {
270    fn parse_hostname(&self, data: &HashMap<DataField, Value>) -> Option<String> {
271        data.extract::<String>(DataField::Hostname)
272    }
273}
274
275impl GetApiVersion for VnishV120 {
276    fn parse_api_version(&self, data: &HashMap<DataField, Value>) -> Option<String> {
277        data.extract::<String>(DataField::ApiVersion)
278    }
279}
280
281impl GetFirmwareVersion for VnishV120 {
282    fn parse_firmware_version(&self, data: &HashMap<DataField, Value>) -> Option<String> {
283        data.extract::<String>(DataField::FirmwareVersion)
284    }
285}
286
287impl GetControlBoardVersion for VnishV120 {
288    fn parse_control_board_version(
289        &self,
290        data: &HashMap<DataField, Value>,
291    ) -> Option<MinerControlBoard> {
292        data.extract::<String>(DataField::ControlBoardVersion)
293            .and_then(|s| MinerControlBoard::from_str(&s).ok())
294    }
295}
296
297impl GetHashboards for VnishV120 {
298    fn parse_hashboards(&self, data: &HashMap<DataField, Value>) -> Vec<BoardData> {
299        let mut hashboards: Vec<BoardData> = Vec::new();
300
301        let chains_data = data.get(&DataField::Hashboards).and_then(|v| v.as_array());
302
303        if let Some(chains_array) = chains_data {
304            for (idx, chain) in chains_array.iter().enumerate() {
305                let hashrate = Self::extract_hashrate(chain, &["/hashrate_rt", "/hr_realtime"]);
306                let expected_hashrate =
307                    Self::extract_hashrate(chain, &["/hashrate_ideal", "/hr_nominal"]);
308
309                let frequency = Self::extract_frequency(chain);
310                let voltage = Self::extract_voltage(chain);
311                let (board_temperature, chip_temperature) = Self::extract_temperatures(chain);
312
313                let working_chips = Self::extract_working_chips(chain);
314                let active = Self::extract_chain_active_status(chain, &hashrate);
315                let serial_number = Self::extract_chain_serial(chain, data);
316                let tuned = Self::extract_tuned_status(chain, data);
317                let chips = Self::extract_chips(chain);
318
319                hashboards.push(BoardData {
320                    position: chain
321                        .pointer("/id")
322                        .and_then(|v| v.as_u64())
323                        .unwrap_or(idx as u64) as u8,
324                    hashrate,
325                    expected_hashrate,
326                    board_temperature,
327                    intake_temperature: chip_temperature,
328                    outlet_temperature: chip_temperature,
329                    expected_chips: self.device_info.hardware.chips,
330                    working_chips,
331                    serial_number,
332                    chips,
333                    voltage,
334                    frequency,
335                    tuned,
336                    active,
337                });
338            }
339        }
340
341        hashboards
342    }
343}
344
345impl GetHashrate for VnishV120 {
346    fn parse_hashrate(&self, data: &HashMap<DataField, Value>) -> Option<HashRate> {
347        data.extract_map::<f64, _>(DataField::Hashrate, |f| HashRate {
348            value: f,
349            unit: HashRateUnit::GigaHash,
350            algo: String::from("SHA256"),
351        })
352    }
353}
354
355impl GetExpectedHashrate for VnishV120 {
356    fn parse_expected_hashrate(&self, data: &HashMap<DataField, Value>) -> Option<HashRate> {
357        data.extract_map::<f64, _>(DataField::ExpectedHashrate, |f| HashRate {
358            value: f,
359            unit: HashRateUnit::GigaHash,
360            algo: String::from("SHA256"),
361        })
362    }
363}
364
365impl GetFans for VnishV120 {
366    fn parse_fans(&self, data: &HashMap<DataField, Value>) -> Vec<FanData> {
367        let mut fans: Vec<FanData> = Vec::new();
368
369        if let Some(fans_data) = data.get(&DataField::Fans)
370            && let Some(fans_array) = fans_data.as_array()
371        {
372            for (idx, fan) in fans_array.iter().enumerate() {
373                if let Some(rpm) = fan.pointer("/rpm").and_then(|v| v.as_i64()) {
374                    fans.push(FanData {
375                        position: idx as i16,
376                        rpm: Some(AngularVelocity::from_rpm(rpm as f64)),
377                    });
378                }
379            }
380        }
381
382        fans
383    }
384}
385
386impl GetPsuFans for VnishV120 {}
387
388impl GetFluidTemperature for VnishV120 {}
389
390impl GetWattage for VnishV120 {
391    fn parse_wattage(&self, data: &HashMap<DataField, Value>) -> Option<Power> {
392        data.extract_map::<i64, _>(DataField::Wattage, |w| Power::from_watts(w as f64))
393    }
394}
395
396impl GetWattageLimit for VnishV120 {}
397
398impl GetLightFlashing for VnishV120 {
399    fn parse_light_flashing(&self, data: &HashMap<DataField, Value>) -> Option<bool> {
400        data.extract::<bool>(DataField::LightFlashing)
401    }
402}
403
404impl GetMessages for VnishV120 {}
405
406impl GetUptime for VnishV120 {
407    fn parse_uptime(&self, data: &HashMap<DataField, Value>) -> Option<Duration> {
408        data.extract::<String>(DataField::Uptime)
409            .and_then(|uptime_str| {
410                // Parse uptime strings like "10 days, 18:00"
411                let trimmed = uptime_str.trim();
412
413                // Try to parse format like "X days, HH:MM" or "X days"
414                if trimmed.contains("days") {
415                    let mut total_seconds = 0u64;
416
417                    // Extract days
418                    if let Some(days_part) = trimmed.split("days").next()
419                        && let Ok(days) = days_part.trim().parse::<u64>()
420                    {
421                        total_seconds += days * 24 * 60 * 60;
422                    }
423
424                    // Extract hours and minutes if present (after comma)
425                    if let Some(time_part) = trimmed.split(',').nth(1) {
426                        let time_part = time_part.trim();
427                        if let Some((hours_str, minutes_str)) = time_part.split_once(':')
428                            && let (Ok(hours), Ok(minutes)) = (
429                                hours_str.trim().parse::<u64>(),
430                                minutes_str.trim().parse::<u64>(),
431                            )
432                        {
433                            total_seconds += hours * 60 * 60 + minutes * 60;
434                        }
435                    }
436
437                    return Some(Duration::from_secs(total_seconds));
438                }
439
440                None
441            })
442    }
443}
444
445impl GetIsMining for VnishV120 {
446    fn parse_is_mining(&self, data: &HashMap<DataField, Value>) -> bool {
447        data.extract::<String>(DataField::IsMining)
448            .map(|state| state == "mining")
449            .unwrap_or(false)
450    }
451}
452
453impl GetPools for VnishV120 {
454    fn parse_pools(&self, data: &HashMap<DataField, Value>) -> Vec<PoolData> {
455        let mut pools: Vec<PoolData> = Vec::new();
456
457        if let Some(pools_data) = data.get(&DataField::Pools)
458            && let Some(pools_array) = pools_data.as_array()
459        {
460            for (idx, pool) in pools_array.iter().enumerate() {
461                let url = pool
462                    .pointer("/url")
463                    .and_then(|v| v.as_str())
464                    .map(String::from)
465                    .map(PoolURL::from);
466
467                let user = pool
468                    .pointer("/user")
469                    .and_then(|v| v.as_str())
470                    .map(String::from);
471
472                let accepted_shares = pool.pointer("/accepted").and_then(|v| v.as_u64());
473                let rejected_shares = pool.pointer("/rejected").and_then(|v| v.as_u64());
474                let pool_status = pool.pointer("/status").and_then(|v| v.as_str());
475                let (active, alive) = Self::parse_pool_status(pool_status);
476
477                pools.push(PoolData {
478                    position: Some(idx as u16),
479                    url,
480                    accepted_shares,
481                    rejected_shares,
482                    active,
483                    alive,
484                    user,
485                });
486            }
487        }
488
489        pools
490    }
491}
492
493// Helper methods for data extraction
494impl VnishV120 {
495    fn extract_hashrate(chain: &Value, paths: &[&str]) -> Option<HashRate> {
496        paths
497            .iter()
498            .find_map(|&path| chain.pointer(path).and_then(|v| v.as_f64()))
499            .map(|f| HashRate {
500                value: f,
501                unit: HashRateUnit::GigaHash,
502                algo: String::from("SHA256"),
503            })
504    }
505
506    fn extract_frequency(chain: &Value) -> Option<Frequency> {
507        chain
508            .pointer("/frequency")
509            .or_else(|| chain.pointer("/freq"))
510            .and_then(|v| v.as_f64())
511            .map(Frequency::from_megahertz)
512    }
513
514    fn extract_voltage(chain: &Value) -> Option<Voltage> {
515        chain
516            .pointer("/voltage")
517            .and_then(|v| v.as_i64())
518            .map(|v| Voltage::from_millivolts(v as f64))
519    }
520
521    fn extract_temperatures(chain: &Value) -> (Option<Temperature>, Option<Temperature>) {
522        let board_temp = chain
523            .pointer("/pcb_temp/max")
524            .and_then(|v| v.as_i64())
525            .map(|t| Temperature::from_celsius(t as f64));
526
527        let chip_temp = chain
528            .pointer("/chip_temp/max")
529            .and_then(|v| v.as_i64())
530            .map(|t| Temperature::from_celsius(t as f64));
531
532        (board_temp, chip_temp)
533    }
534
535    fn extract_working_chips(chain: &Value) -> Option<u16> {
536        chain
537            .pointer("/chip_statuses")
538            .map(|statuses| {
539                let red = statuses
540                    .pointer("/red")
541                    .and_then(|v| v.as_u64())
542                    .unwrap_or(0);
543                let orange = statuses
544                    .pointer("/orange")
545                    .and_then(|v| v.as_u64())
546                    .unwrap_or(0);
547                (red + orange) as u16
548            })
549            .or_else(|| {
550                chain
551                    .pointer("/chips")
552                    .and_then(|v| v.as_array())
553                    .map(|chips| chips.len() as u16)
554            })
555    }
556
557    fn extract_chain_active_status(chain: &Value, hashrate: &Option<HashRate>) -> Option<bool> {
558        chain
559            .pointer("/status/state")
560            .and_then(|v| v.as_str())
561            .map(|s| s == "mining")
562            .or_else(|| hashrate.as_ref().map(|h| h.value > 0.0))
563    }
564
565    fn parse_pool_status(status: Option<&str>) -> (Option<bool>, Option<bool>) {
566        match status {
567            Some("active" | "working") => (Some(true), Some(true)),
568            Some("offline" | "disabled") => (Some(false), Some(false)),
569            Some("rejecting") => (Some(false), Some(true)),
570            _ => (None, None),
571        }
572    }
573
574    fn extract_chain_serial(chain: &Value, data: &HashMap<DataField, Value>) -> Option<String> {
575        // Try to get serial from chain-specific data first (factory-info)
576        chain
577            .pointer("/serial")
578            .and_then(|v| v.as_str())
579            .map(String::from)
580            .or_else(|| {
581                // Fallback to miner-wide serial number
582                data.extract::<String>(DataField::SerialNumber)
583            })
584    }
585
586    fn extract_tuned_status(_chain: &Value, data: &HashMap<DataField, Value>) -> Option<bool> {
587        // Check miner state to determine tuning status
588        if let Some(miner_state) = data.extract::<String>(DataField::IsMining) {
589            match miner_state.as_str() {
590                "auto-tuning" => Some(false), // Currently tuning, not yet tuned
591                "mining" => Some(true),       // Tuned and mining
592                _ => None,
593            }
594        } else {
595            None
596        }
597    }
598
599    fn extract_chips(chain: &Value) -> Vec<ChipData> {
600        let mut chips: Vec<ChipData> = Vec::new();
601
602        if let Some(chips_array) = chain.pointer("/chips").and_then(|v| v.as_array()) {
603            for (idx, chip) in chips_array.iter().enumerate() {
604                let hashrate = chip
605                    .pointer("/hr")
606                    .and_then(|v| v.as_f64())
607                    .map(|f| HashRate {
608                        value: f,
609                        unit: HashRateUnit::GigaHash,
610                        algo: String::from("SHA256"),
611                    });
612
613                let temperature = chip
614                    .pointer("/temp")
615                    .and_then(|v| v.as_f64())
616                    .map(Temperature::from_celsius);
617
618                let voltage = chip
619                    .pointer("/volt")
620                    .and_then(|v| v.as_i64())
621                    .map(|v| Voltage::from_millivolts(v as f64));
622
623                let frequency = chip
624                    .pointer("/freq")
625                    .and_then(|v| v.as_i64())
626                    .map(|f| Frequency::from_megahertz(f as f64));
627
628                let working = hashrate.as_ref().map(|hr| hr.value > 0.0);
629
630                chips.push(ChipData {
631                    position: chip
632                        .pointer("/id")
633                        .and_then(|v| v.as_u64())
634                        .unwrap_or(idx as u64) as u16,
635                    hashrate,
636                    temperature,
637                    voltage,
638                    frequency,
639                    tuned: None,
640                    working,
641                });
642            }
643        }
644
645        chips
646    }
647}
648
649#[async_trait]
650impl SetFaultLight for VnishV120 {
651    #[allow(unused_variables)]
652    async fn set_fault_light(&self, fault: bool) -> anyhow::Result<bool> {
653        anyhow::bail!("Unsupported command");
654    }
655}
656
657#[async_trait]
658impl SetPowerLimit for VnishV120 {
659    #[allow(unused_variables)]
660    async fn set_power_limit(&self, limit: Power) -> anyhow::Result<bool> {
661        anyhow::bail!("Unsupported command");
662    }
663}
664
665#[async_trait]
666impl Restart for VnishV120 {
667    async fn restart(&self) -> anyhow::Result<bool> {
668        anyhow::bail!("Unsupported command");
669    }
670}
671
672#[async_trait]
673impl Pause for VnishV120 {
674    #[allow(unused_variables)]
675    async fn pause(&self, at_time: Option<Duration>) -> anyhow::Result<bool> {
676        anyhow::bail!("Unsupported command");
677    }
678}
679
680#[async_trait]
681impl Resume for VnishV120 {
682    #[allow(unused_variables)]
683    async fn resume(&self, at_time: Option<Duration>) -> anyhow::Result<bool> {
684        anyhow::bail!("Unsupported command");
685    }
686}