asic_rs/miners/backends/whatsminer/v2/
mod.rs

1use anyhow::{Result, anyhow};
2use async_trait::async_trait;
3use macaddr::MacAddr;
4use measurements::{AngularVelocity, Frequency, Power, Temperature};
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;
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 rpc::WhatsMinerRPCAPI;
24
25mod rpc;
26
27#[derive(Debug)]
28pub struct WhatsMinerV2 {
29    pub ip: IpAddr,
30    pub rpc: WhatsMinerRPCAPI,
31    pub device_info: DeviceInfo,
32}
33
34impl WhatsMinerV2 {
35    pub fn new(ip: IpAddr, model: MinerModel) -> Self {
36        WhatsMinerV2 {
37            ip,
38            rpc: WhatsMinerRPCAPI::new(ip, None),
39            device_info: DeviceInfo::new(
40                MinerMake::WhatsMiner,
41                model,
42                MinerFirmware::Stock,
43                HashAlgorithm::SHA256,
44            ),
45        }
46    }
47}
48
49#[async_trait]
50impl APIClient for WhatsMinerV2 {
51    async fn get_api_result(&self, command: &MinerCommand) -> Result<Value> {
52        match command {
53            MinerCommand::RPC { .. } => self.rpc.get_api_result(command).await,
54            _ => Err(anyhow!("Unsupported command type for WhatsMiner API")),
55        }
56    }
57}
58
59impl GetDataLocations for WhatsMinerV2 {
60    fn get_locations(&self, data_field: DataField) -> Vec<DataLocation> {
61        let get_miner_info_cmd: MinerCommand = MinerCommand::RPC {
62            command: "get_miner_info",
63            parameters: None,
64        };
65        let summary_cmd: MinerCommand = MinerCommand::RPC {
66            command: "summary",
67            parameters: None,
68        };
69        let devs_cmd: MinerCommand = MinerCommand::RPC {
70            command: "devs",
71            parameters: None,
72        };
73        let pools_cmd: MinerCommand = MinerCommand::RPC {
74            command: "pools",
75            parameters: None,
76        };
77        let status_cmd: MinerCommand = MinerCommand::RPC {
78            command: "status",
79            parameters: None,
80        };
81        let get_version_cmd: MinerCommand = MinerCommand::RPC {
82            command: "get_version",
83            parameters: None,
84        };
85        let get_psu_cmd: MinerCommand = MinerCommand::RPC {
86            command: "get_psu",
87            parameters: None,
88        };
89
90        match data_field {
91            DataField::Mac => vec![(
92                get_miner_info_cmd,
93                DataExtractor {
94                    func: get_by_pointer,
95                    key: Some("/Msg/mac"),
96                    tag: None,
97                },
98            )],
99            DataField::ApiVersion => vec![(
100                get_version_cmd,
101                DataExtractor {
102                    func: get_by_pointer,
103                    key: Some("/Msg/api_ver"),
104                    tag: None,
105                },
106            )],
107            DataField::FirmwareVersion => vec![(
108                get_version_cmd,
109                DataExtractor {
110                    func: get_by_pointer,
111                    key: Some("/Msg/fw_ver"),
112                    tag: None,
113                },
114            )],
115            DataField::ControlBoardVersion => vec![(
116                get_version_cmd,
117                DataExtractor {
118                    func: get_by_pointer,
119                    key: Some("/Msg/platform"),
120                    tag: None,
121                },
122            )],
123            DataField::Hostname => vec![(
124                get_miner_info_cmd,
125                DataExtractor {
126                    func: get_by_pointer,
127                    key: Some("/Msg/hostname"),
128                    tag: None,
129                },
130            )],
131            DataField::LightFlashing => vec![(
132                get_miner_info_cmd,
133                DataExtractor {
134                    func: get_by_pointer,
135                    key: Some("/Msg/ledstat"),
136                    tag: None,
137                },
138            )],
139            DataField::WattageLimit => vec![(
140                summary_cmd,
141                DataExtractor {
142                    func: get_by_pointer,
143                    key: Some("/SUMMARY/0/Power Limit"),
144                    tag: None,
145                },
146            )],
147            DataField::Fans => vec![(
148                summary_cmd,
149                DataExtractor {
150                    func: get_by_pointer,
151                    key: Some("/SUMMARY/0"),
152                    tag: None,
153                },
154            )],
155            DataField::PsuFans => vec![(
156                get_psu_cmd,
157                DataExtractor {
158                    func: get_by_pointer,
159                    key: Some("/Msg/fan_speed"),
160                    tag: None,
161                },
162            )],
163            DataField::Hashboards => vec![(
164                devs_cmd,
165                DataExtractor {
166                    func: get_by_pointer,
167                    key: Some(""),
168                    tag: None,
169                },
170            )],
171            DataField::Pools => vec![(
172                pools_cmd,
173                DataExtractor {
174                    func: get_by_pointer,
175                    key: Some("/POOLS"),
176                    tag: None,
177                },
178            )],
179            DataField::Uptime => vec![(
180                summary_cmd,
181                DataExtractor {
182                    func: get_by_pointer,
183                    key: Some("/SUMMARY/0/Elapsed"),
184                    tag: None,
185                },
186            )],
187            DataField::Wattage => vec![(
188                summary_cmd,
189                DataExtractor {
190                    func: get_by_pointer,
191                    key: Some("/SUMMARY/0/Power"),
192                    tag: None,
193                },
194            )],
195            DataField::Hashrate => vec![(
196                summary_cmd,
197                DataExtractor {
198                    func: get_by_pointer,
199                    key: Some("/SUMMARY/0/HS RT"),
200                    tag: None,
201                },
202            )],
203            DataField::ExpectedHashrate => vec![(
204                summary_cmd,
205                DataExtractor {
206                    func: get_by_pointer,
207                    key: Some("/SUMMARY/0/Factory GHS"),
208                    tag: None,
209                },
210            )],
211            DataField::FluidTemperature => vec![(
212                summary_cmd,
213                DataExtractor {
214                    func: get_by_pointer,
215                    key: Some("/SUMMARY/0/Env Temp"),
216                    tag: None,
217                },
218            )],
219            DataField::IsMining => vec![(
220                status_cmd,
221                DataExtractor {
222                    func: get_by_pointer,
223                    key: Some("/SUMMARY/0/btmineroff"),
224                    tag: None,
225                },
226            )],
227            _ => vec![],
228        }
229    }
230}
231
232impl GetIP for WhatsMinerV2 {
233    fn get_ip(&self) -> IpAddr {
234        self.ip
235    }
236}
237impl GetDeviceInfo for WhatsMinerV2 {
238    fn get_device_info(&self) -> DeviceInfo {
239        self.device_info
240    }
241}
242
243impl CollectData for WhatsMinerV2 {
244    fn get_collector(&self) -> DataCollector<'_> {
245        DataCollector::new(self)
246    }
247}
248
249impl GetMAC for WhatsMinerV2 {
250    fn parse_mac(&self, data: &HashMap<DataField, Value>) -> Option<MacAddr> {
251        data.extract::<String>(DataField::Mac)
252            .and_then(|s| MacAddr::from_str(&s).ok())
253    }
254}
255
256impl GetSerialNumber for WhatsMinerV2 {}
257impl GetHostname for WhatsMinerV2 {
258    fn parse_hostname(&self, data: &HashMap<DataField, Value>) -> Option<String> {
259        data.extract::<String>(DataField::Hostname)
260    }
261}
262impl GetApiVersion for WhatsMinerV2 {
263    fn parse_api_version(&self, data: &HashMap<DataField, Value>) -> Option<String> {
264        data.extract::<String>(DataField::ApiVersion)
265    }
266}
267impl GetFirmwareVersion for WhatsMinerV2 {
268    fn parse_firmware_version(&self, data: &HashMap<DataField, Value>) -> Option<String> {
269        data.extract::<String>(DataField::FirmwareVersion)
270    }
271}
272impl GetControlBoardVersion for WhatsMinerV2 {
273    fn parse_control_board_version(&self, data: &HashMap<DataField, Value>) -> Option<String> {
274        data.extract::<String>(DataField::ControlBoardVersion)
275    }
276}
277impl GetHashboards for WhatsMinerV2 {
278    fn parse_hashboards(&self, data: &HashMap<DataField, Value>) -> Vec<BoardData> {
279        let mut hashboards: Vec<BoardData> = Vec::new();
280        let board_count = self.device_info.hardware.boards.unwrap_or(3);
281        let hashboard_data = data.get(&DataField::Hashboards);
282
283        for idx in 0..board_count {
284            let hashrate = hashboard_data
285                .and_then(|val| val.pointer(&format!("/DEVS/{idx}/MHS av")))
286                .and_then(|val| val.as_f64())
287                .map(|f| {
288                    HashRate {
289                        value: f,
290                        unit: HashRateUnit::MegaHash,
291                        algo: String::from("SHA256"),
292                    }
293                    .as_unit(HashRateUnit::TeraHash)
294                });
295            let expected_hashrate = hashboard_data
296                .and_then(|val| val.pointer(&format!("/DEVS/{idx}/Factory GHS")))
297                .and_then(|val| val.as_f64())
298                .map(|f| {
299                    HashRate {
300                        value: f,
301                        unit: HashRateUnit::GigaHash,
302                        algo: String::from("SHA256"),
303                    }
304                    .as_unit(HashRateUnit::TeraHash)
305                });
306            let board_temperature = hashboard_data
307                .and_then(|val| val.pointer(&format!("/DEVS/{idx}/Temperature")))
308                .and_then(|val| val.as_f64())
309                .map(Temperature::from_celsius);
310            let intake_temperature = hashboard_data
311                .and_then(|val| val.pointer(&format!("/DEVS/{idx}/Chip Temp Min")))
312                .and_then(|val| val.as_f64())
313                .map(Temperature::from_celsius);
314            let outlet_temperature = hashboard_data
315                .and_then(|val| val.pointer(&format!("/DEVS/{idx}/Chip Temp Max")))
316                .and_then(|val| val.as_f64())
317                .map(Temperature::from_celsius);
318            let serial_number = hashboard_data
319                .and_then(|val| val.pointer(&format!("/DEVS/{idx}/PCB SN")))
320                .and_then(|val| val.as_str())
321                .map(String::from);
322            let working_chips = hashboard_data
323                .and_then(|val| val.pointer(&format!("/DEVS/{idx}/Effective Chips")))
324                .and_then(|val| val.as_u64())
325                .map(|u| u as u16);
326            let frequency = hashboard_data
327                .and_then(|val| val.pointer(&format!("/DEVS/{idx}/Frequency")))
328                .and_then(|val| val.as_f64())
329                .map(Frequency::from_megahertz);
330
331            let active = Some(hashrate.clone().map(|h| h.value).unwrap_or(0f64) > 0f64);
332            hashboards.push(BoardData {
333                hashrate,
334                position: idx,
335                expected_hashrate,
336                board_temperature,
337                intake_temperature,
338                outlet_temperature,
339                expected_chips: self.device_info.hardware.chips,
340                working_chips,
341                serial_number,
342                chips: vec![],
343                voltage: None, // TODO
344                frequency,
345                tuned: Some(true),
346                active,
347            });
348        }
349        hashboards
350    }
351}
352impl GetHashrate for WhatsMinerV2 {
353    fn parse_hashrate(&self, data: &HashMap<DataField, Value>) -> Option<HashRate> {
354        data.extract_map::<f64, _>(DataField::Hashrate, |f| {
355            HashRate {
356                value: f,
357                unit: HashRateUnit::MegaHash,
358                algo: String::from("SHA256"),
359            }
360            .as_unit(HashRateUnit::TeraHash)
361        })
362    }
363}
364impl GetExpectedHashrate for WhatsMinerV2 {
365    fn parse_expected_hashrate(&self, data: &HashMap<DataField, Value>) -> Option<HashRate> {
366        data.extract_map::<f64, _>(DataField::ExpectedHashrate, |f| {
367            HashRate {
368                value: f,
369                unit: HashRateUnit::GigaHash,
370                algo: String::from("SHA256"),
371            }
372            .as_unit(HashRateUnit::TeraHash)
373        })
374    }
375}
376impl GetFans for WhatsMinerV2 {
377    fn parse_fans(&self, data: &HashMap<DataField, Value>) -> Vec<FanData> {
378        let mut fans: Vec<FanData> = Vec::new();
379        for (idx, direction) in ["In", "Out"].iter().enumerate() {
380            let fan = data.extract_nested_map::<f64, _>(
381                DataField::Fans,
382                &format!("Fan Speed {direction}"),
383                |rpm| FanData {
384                    position: idx as i16,
385                    rpm: Some(AngularVelocity::from_rpm(rpm)),
386                },
387            );
388            if let Some(f) = fan {
389                fans.push(f)
390            }
391        }
392        fans
393    }
394}
395impl GetPsuFans for WhatsMinerV2 {
396    fn parse_psu_fans(&self, data: &HashMap<DataField, Value>) -> Vec<FanData> {
397        let mut psu_fans: Vec<FanData> = Vec::new();
398
399        let psu_fan = data.extract_map::<String, _>(DataField::PsuFans, |rpm| FanData {
400            position: 0i16,
401            rpm: Some(AngularVelocity::from_rpm(rpm.parse().unwrap())),
402        });
403        if let Some(f) = psu_fan {
404            psu_fans.push(f)
405        }
406        psu_fans
407    }
408}
409impl GetFluidTemperature for WhatsMinerV2 {
410    fn parse_fluid_temperature(&self, data: &HashMap<DataField, Value>) -> Option<Temperature> {
411        data.extract_map::<f64, _>(DataField::FluidTemperature, Temperature::from_celsius)
412    }
413}
414impl GetWattage for WhatsMinerV2 {
415    fn parse_wattage(&self, data: &HashMap<DataField, Value>) -> Option<Power> {
416        data.extract_map::<f64, _>(DataField::Wattage, Power::from_watts)
417    }
418}
419impl GetWattageLimit for WhatsMinerV2 {
420    fn parse_wattage_limit(&self, data: &HashMap<DataField, Value>) -> Option<Power> {
421        data.extract_map::<f64, _>(DataField::WattageLimit, Power::from_watts)
422    }
423}
424impl GetLightFlashing for WhatsMinerV2 {
425    fn parse_light_flashing(&self, data: &HashMap<DataField, Value>) -> Option<bool> {
426        data.extract_map::<String, _>(DataField::LightFlashing, |l| l != "auto")
427    }
428}
429impl GetMessages for WhatsMinerV2 {} // TODO
430impl GetUptime for WhatsMinerV2 {
431    fn parse_uptime(&self, data: &HashMap<DataField, Value>) -> Option<Duration> {
432        data.extract_map::<u64, _>(DataField::Uptime, Duration::from_secs)
433    }
434}
435impl GetIsMining for WhatsMinerV2 {
436    fn parse_is_mining(&self, data: &HashMap<DataField, Value>) -> bool {
437        data.extract_map::<String, _>(DataField::IsMining, |l| l != "false")
438            .unwrap_or(true)
439    }
440}
441impl GetPools for WhatsMinerV2 {
442    fn parse_pools(&self, data: &HashMap<DataField, Value>) -> Vec<PoolData> {
443        let mut pools: Vec<PoolData> = Vec::new();
444        let pools_raw = data.get(&DataField::Pools);
445        if let Some(pools_response) = pools_raw {
446            for (idx, _) in pools_response
447                .as_array()
448                .unwrap_or(&Vec::new())
449                .iter()
450                .enumerate()
451            {
452                let user = pools_raw
453                    .and_then(|val| val.pointer(&format!("/{idx}/User")))
454                    .map(|val| String::from(val.as_str().unwrap_or("")));
455
456                let alive = pools_raw
457                    .and_then(|val| val.pointer(&format!("/{idx}/Status")))
458                    .map(|val| val.as_str())
459                    .map(|val| val == Some("Alive"));
460
461                let active = pools_raw
462                    .and_then(|val| val.pointer(&format!("/{idx}/Stratum Active")))
463                    .and_then(|val| val.as_bool());
464
465                let url = pools_raw
466                    .and_then(|val| val.pointer(&format!("/{idx}/URL")))
467                    .map(|val| PoolURL::from(String::from(val.as_str().unwrap_or(""))));
468
469                let accepted_shares = pools_raw
470                    .and_then(|val| val.pointer(&format!("/{idx}/Accepted")))
471                    .and_then(|val| val.as_u64());
472
473                let rejected_shares = pools_raw
474                    .and_then(|val| val.pointer(&format!("/{idx}/Rejected")))
475                    .and_then(|val| val.as_u64());
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        pools
489    }
490}