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 let trimmed = uptime_str.trim();
412
413 if trimmed.contains("days") {
415 let mut total_seconds = 0u64;
416
417 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 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
493impl 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 chain
577 .pointer("/serial")
578 .and_then(|v| v.as_str())
579 .map(String::from)
580 .or_else(|| {
581 data.extract::<String>(DataField::SerialNumber)
583 })
584 }
585
586 fn extract_tuned_status(_chain: &Value, data: &HashMap<DataField, Value>) -> Option<bool> {
587 if let Some(miner_state) = data.extract::<String>(DataField::IsMining) {
589 match miner_state.as_str() {
590 "auto-tuning" => Some(false), "mining" => Some(true), _ => 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}