asic_rs/miners/backends/vnish/
mod.rs

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