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

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