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

1use 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::{DeviceInfo, HashAlgorithm, MinerFirmware, MinerModel};
13use crate::data::device::{MinerControlBoard, MinerMake};
14use crate::data::fan::FanData;
15use crate::data::hashrate::{HashRate, HashRateUnit};
16use crate::data::message::{MessageSeverity, MinerMessage};
17use crate::data::pool::{PoolData, PoolURL};
18use crate::miners::backends::traits::*;
19use crate::miners::commands::MinerCommand;
20use crate::miners::data::{
21    DataCollector, DataExtensions, DataExtractor, DataField, DataLocation, get_by_pointer,
22};
23
24use rpc::WhatsMinerRPCAPI;
25
26mod rpc;
27
28#[derive(Debug)]
29pub struct WhatsMinerV1 {
30    pub ip: IpAddr,
31    pub rpc: WhatsMinerRPCAPI,
32    pub device_info: DeviceInfo,
33}
34
35impl WhatsMinerV1 {
36    pub fn new(ip: IpAddr, model: MinerModel) -> Self {
37        WhatsMinerV1 {
38            ip,
39            rpc: WhatsMinerRPCAPI::new(ip, None),
40            device_info: DeviceInfo::new(
41                MinerMake::WhatsMiner,
42                model,
43                MinerFirmware::Stock,
44                HashAlgorithm::SHA256,
45            ),
46        }
47    }
48}
49
50#[async_trait]
51impl APIClient for WhatsMinerV1 {
52    async fn get_api_result(&self, command: &MinerCommand) -> anyhow::Result<Value> {
53        match command {
54            MinerCommand::RPC { .. } => self.rpc.get_api_result(command).await,
55            _ => Err(anyhow::anyhow!(
56                "Unsupported command type for WhatsMiner API"
57            )),
58        }
59    }
60}
61
62impl GetDataLocations for WhatsMinerV1 {
63    fn get_locations(&self, data_field: DataField) -> Vec<DataLocation> {
64        const RPC_SUMMARY: MinerCommand = MinerCommand::RPC {
65            command: "summary",
66            parameters: None,
67        };
68        const RPC_DEVS: MinerCommand = MinerCommand::RPC {
69            command: "devs",
70            parameters: None,
71        };
72        const RPC_POOLS: MinerCommand = MinerCommand::RPC {
73            command: "pools",
74            parameters: None,
75        };
76        const RPC_STATUS: MinerCommand = MinerCommand::RPC {
77            command: "status",
78            parameters: None,
79        };
80        const RPC_GET_VERSION: MinerCommand = MinerCommand::RPC {
81            command: "get_version",
82            parameters: None,
83        };
84        const RPC_GET_PSU: MinerCommand = MinerCommand::RPC {
85            command: "get_psu",
86            parameters: None,
87        };
88
89        match data_field {
90            DataField::Mac => vec![(
91                RPC_SUMMARY,
92                DataExtractor {
93                    func: get_by_pointer,
94                    key: Some("/SUMMARY/0/MAC"),
95                    tag: None,
96                },
97            )],
98            DataField::ApiVersion => vec![(
99                RPC_GET_VERSION,
100                DataExtractor {
101                    func: get_by_pointer,
102                    key: Some("/Msg/api_ver"),
103                    tag: None,
104                },
105            )],
106            DataField::FirmwareVersion => vec![(
107                RPC_GET_VERSION,
108                DataExtractor {
109                    func: get_by_pointer,
110                    key: Some("/Msg/fw_ver"),
111                    tag: None,
112                },
113            )],
114            DataField::ControlBoardVersion => vec![(
115                RPC_SUMMARY,
116                DataExtractor {
117                    func: get_by_pointer,
118                    key: Some("/SUMMARY/0/CB Platform"),
119                    tag: None,
120                },
121            )],
122            DataField::WattageLimit => vec![(
123                RPC_SUMMARY,
124                DataExtractor {
125                    func: get_by_pointer,
126                    key: Some("/SUMMARY/0/Power Limit"),
127                    tag: None,
128                },
129            )],
130            DataField::Fans => vec![(
131                RPC_SUMMARY,
132                DataExtractor {
133                    func: get_by_pointer,
134                    key: Some("/SUMMARY/0"),
135                    tag: None,
136                },
137            )],
138            DataField::PsuFans => vec![(
139                RPC_GET_PSU,
140                DataExtractor {
141                    func: get_by_pointer,
142                    key: Some("/Msg/fan_speed"),
143                    tag: None,
144                },
145            )],
146            DataField::Hashboards => vec![(
147                RPC_DEVS,
148                DataExtractor {
149                    func: get_by_pointer,
150                    key: Some(""),
151                    tag: None,
152                },
153            )],
154            DataField::Pools => vec![(
155                RPC_POOLS,
156                DataExtractor {
157                    func: get_by_pointer,
158                    key: Some("/POOLS"),
159                    tag: None,
160                },
161            )],
162            DataField::Uptime => vec![(
163                RPC_SUMMARY,
164                DataExtractor {
165                    func: get_by_pointer,
166                    key: Some("/SUMMARY/0/Elapsed"),
167                    tag: None,
168                },
169            )],
170            DataField::Wattage => vec![(
171                RPC_SUMMARY,
172                DataExtractor {
173                    func: get_by_pointer,
174                    key: Some("/SUMMARY/0/Power"),
175                    tag: None,
176                },
177            )],
178            DataField::Hashrate => vec![(
179                RPC_SUMMARY,
180                DataExtractor {
181                    func: get_by_pointer,
182                    key: Some("/SUMMARY/0/HS RT"),
183                    tag: None,
184                },
185            )],
186            DataField::ExpectedHashrate => vec![(
187                RPC_SUMMARY,
188                DataExtractor {
189                    func: get_by_pointer,
190                    key: Some("/SUMMARY/0/Factory GHS"),
191                    tag: None,
192                },
193            )],
194            DataField::FluidTemperature => vec![(
195                RPC_SUMMARY,
196                DataExtractor {
197                    func: get_by_pointer,
198                    key: Some("/SUMMARY/0/Env Temp"),
199                    tag: None,
200                },
201            )],
202            DataField::IsMining => vec![(
203                RPC_STATUS,
204                DataExtractor {
205                    func: get_by_pointer,
206                    key: Some("/SUMMARY/0/btmineroff"),
207                    tag: None,
208                },
209            )],
210            DataField::Messages => vec![(
211                RPC_SUMMARY,
212                DataExtractor {
213                    func: get_by_pointer,
214                    key: Some("/SUMMARY/0"),
215                    tag: None,
216                },
217            )],
218            _ => vec![],
219        }
220    }
221}
222
223impl GetIP for WhatsMinerV1 {
224    fn get_ip(&self) -> IpAddr {
225        self.ip
226    }
227}
228impl GetDeviceInfo for WhatsMinerV1 {
229    fn get_device_info(&self) -> DeviceInfo {
230        self.device_info
231    }
232}
233
234impl CollectData for WhatsMinerV1 {
235    fn get_collector(&self) -> DataCollector<'_> {
236        DataCollector::new(self)
237    }
238}
239
240impl GetMAC for WhatsMinerV1 {
241    fn parse_mac(&self, data: &HashMap<DataField, Value>) -> Option<MacAddr> {
242        data.extract::<String>(DataField::Mac)
243            .and_then(|s| MacAddr::from_str(&s).ok())
244    }
245}
246
247impl GetSerialNumber for WhatsMinerV1 {}
248impl GetHostname for WhatsMinerV1 {}
249impl GetApiVersion for WhatsMinerV1 {
250    fn parse_api_version(&self, data: &HashMap<DataField, Value>) -> Option<String> {
251        data.extract::<String>(DataField::ApiVersion)
252            .and_then(|s| Some(s.strip_prefix("whatsminer v")?.to_string()))
253    }
254}
255impl GetFirmwareVersion for WhatsMinerV1 {
256    fn parse_firmware_version(&self, data: &HashMap<DataField, Value>) -> Option<String> {
257        data.extract::<String>(DataField::FirmwareVersion)
258    }
259}
260impl GetControlBoardVersion for WhatsMinerV1 {
261    fn parse_control_board_version(
262        &self,
263        data: &HashMap<DataField, Value>,
264    ) -> Option<MinerControlBoard> {
265        data.extract::<String>(DataField::ControlBoardVersion)
266            .and_then(|s| {
267                MinerControlBoard::from_str(s.to_uppercase().strip_prefix("ALLWINNER_")?).ok()
268            })
269    }
270}
271impl GetHashboards for WhatsMinerV1 {
272    fn parse_hashboards(&self, data: &HashMap<DataField, Value>) -> Vec<BoardData> {
273        let mut hashboards: Vec<BoardData> = Vec::new();
274        let board_count = self.device_info.hardware.boards.unwrap_or(3);
275        let hashboard_data = data.get(&DataField::Hashboards);
276
277        for idx in 0..board_count {
278            let hashrate = hashboard_data
279                .and_then(|val| val.pointer(&format!("/DEVS/{}/MHS av", idx)))
280                .and_then(|val| val.as_f64())
281                .map(|f| {
282                    HashRate {
283                        value: f,
284                        unit: HashRateUnit::MegaHash,
285                        algo: String::from("SHA256"),
286                    }
287                    .as_unit(HashRateUnit::TeraHash)
288                });
289            let expected_hashrate = hashboard_data
290                .and_then(|val| val.pointer(&format!("/DEVS/{}/Factory GHS", idx)))
291                .and_then(|val| val.as_f64())
292                .map(|f| {
293                    HashRate {
294                        value: f,
295                        unit: HashRateUnit::GigaHash,
296                        algo: String::from("SHA256"),
297                    }
298                    .as_unit(HashRateUnit::TeraHash)
299                });
300            let board_temperature = hashboard_data
301                .and_then(|val| val.pointer(&format!("/DEVS/{}/Temperature", idx)))
302                .and_then(|val| val.as_f64())
303                .map(Temperature::from_celsius);
304            let intake_temperature = hashboard_data
305                .and_then(|val| val.pointer(&format!("/DEVS/{}/Chip Temp Min", idx)))
306                .and_then(|val| val.as_f64())
307                .map(Temperature::from_celsius);
308            let outlet_temperature = hashboard_data
309                .and_then(|val| val.pointer(&format!("/DEVS/{}/Chip Temp Max", idx)))
310                .and_then(|val| val.as_f64())
311                .map(Temperature::from_celsius);
312            let serial_number = hashboard_data
313                .and_then(|val| val.pointer(&format!("/DEVS/{}/PCB SN", idx)))
314                .and_then(|val| val.as_str())
315                .map(String::from);
316            let working_chips = hashboard_data
317                .and_then(|val| val.pointer(&format!("/DEVS/{}/Effective Chips", idx)))
318                .and_then(|val| val.as_u64())
319                .map(|u| u as u16);
320            let frequency = hashboard_data
321                .and_then(|val| val.pointer(&format!("/DEVS/{}/Frequency", idx)))
322                .and_then(|val| val.as_f64())
323                .map(Frequency::from_megahertz);
324
325            let active = Some(hashrate.clone().map(|h| h.value).unwrap_or(0f64) > 0f64);
326            hashboards.push(BoardData {
327                hashrate,
328                position: idx,
329                expected_hashrate,
330                board_temperature,
331                intake_temperature,
332                outlet_temperature,
333                expected_chips: self.device_info.hardware.chips,
334                working_chips,
335                serial_number,
336                chips: vec![],
337                voltage: None, // TODO
338                frequency,
339                tuned: Some(true),
340                active,
341            });
342        }
343        hashboards
344    }
345}
346impl GetHashrate for WhatsMinerV1 {
347    fn parse_hashrate(&self, data: &HashMap<DataField, Value>) -> Option<HashRate> {
348        data.extract_map::<f64, _>(DataField::Hashrate, |f| {
349            HashRate {
350                value: f,
351                unit: HashRateUnit::MegaHash,
352                algo: String::from("SHA256"),
353            }
354            .as_unit(HashRateUnit::TeraHash)
355        })
356    }
357}
358impl GetExpectedHashrate for WhatsMinerV1 {
359    fn parse_expected_hashrate(&self, data: &HashMap<DataField, Value>) -> Option<HashRate> {
360        data.extract_map::<f64, _>(DataField::ExpectedHashrate, |f| {
361            HashRate {
362                value: f,
363                unit: HashRateUnit::GigaHash,
364                algo: String::from("SHA256"),
365            }
366            .as_unit(HashRateUnit::TeraHash)
367        })
368    }
369}
370impl GetFans for WhatsMinerV1 {
371    fn parse_fans(&self, data: &HashMap<DataField, Value>) -> Vec<FanData> {
372        let mut fans: Vec<FanData> = Vec::new();
373        for (idx, direction) in ["In", "Out"].iter().enumerate() {
374            let fan = data.extract_nested_map::<f64, _>(
375                DataField::Fans,
376                &format!("Fan Speed {}", direction),
377                |rpm| FanData {
378                    position: idx as i16,
379                    rpm: Some(AngularVelocity::from_rpm(rpm)),
380                },
381            );
382            if let Some(f) = fan {
383                fans.push(f)
384            }
385        }
386        fans
387    }
388}
389impl GetPsuFans for WhatsMinerV1 {
390    fn parse_psu_fans(&self, data: &HashMap<DataField, Value>) -> Vec<FanData> {
391        let mut psu_fans: Vec<FanData> = Vec::new();
392
393        let psu_fan = data.extract_map::<String, _>(DataField::PsuFans, |rpm| FanData {
394            position: 0i16,
395            rpm: Some(AngularVelocity::from_rpm(rpm.parse().unwrap())),
396        });
397        if let Some(f) = psu_fan {
398            psu_fans.push(f)
399        }
400        psu_fans
401    }
402}
403impl GetFluidTemperature for WhatsMinerV1 {
404    fn parse_fluid_temperature(&self, data: &HashMap<DataField, Value>) -> Option<Temperature> {
405        data.extract_map::<f64, _>(DataField::FluidTemperature, Temperature::from_celsius)
406    }
407}
408impl GetWattage for WhatsMinerV1 {
409    fn parse_wattage(&self, data: &HashMap<DataField, Value>) -> Option<Power> {
410        data.extract_map::<f64, _>(DataField::Wattage, Power::from_watts)
411    }
412}
413impl GetWattageLimit for WhatsMinerV1 {
414    fn parse_wattage_limit(&self, data: &HashMap<DataField, Value>) -> Option<Power> {
415        data.extract_map::<f64, _>(DataField::WattageLimit, Power::from_watts)
416    }
417}
418impl GetLightFlashing for WhatsMinerV1 {}
419impl GetMessages for WhatsMinerV1 {
420    fn parse_messages(&self, data: &HashMap<DataField, Value>) -> Vec<MinerMessage> {
421        let mut messages = Vec::new();
422
423        let error_count = data
424            .get(&DataField::Messages)
425            .and_then(|val| {
426                val.pointer("/Error Code Count")
427                    .and_then(|val| val.as_u64())
428            })
429            .unwrap_or(0u64) as usize;
430        for idx in 0..error_count {
431            let e_code = data
432                .get(&DataField::Messages)
433                .and_then(|val| val.pointer(&format!("/Error Code {}", idx)))
434                .and_then(|val| val.as_u64());
435            if let Some(code) = e_code {
436                messages.push(MinerMessage::new(
437                    0,
438                    code,
439                    "".to_string(), // TODO: parse message from mapping
440                    MessageSeverity::Error,
441                ));
442            }
443        }
444
445        messages
446    }
447}
448impl GetUptime for WhatsMinerV1 {
449    fn parse_uptime(&self, data: &HashMap<DataField, Value>) -> Option<Duration> {
450        data.extract_map::<u64, _>(DataField::Uptime, Duration::from_secs)
451    }
452}
453impl GetIsMining for WhatsMinerV1 {
454    fn parse_is_mining(&self, data: &HashMap<DataField, Value>) -> bool {
455        data.extract_map::<String, _>(DataField::IsMining, |l| l != "false")
456            .unwrap_or(true)
457    }
458}
459impl GetPools for WhatsMinerV1 {
460    fn parse_pools(&self, data: &HashMap<DataField, Value>) -> Vec<PoolData> {
461        let mut pools: Vec<PoolData> = Vec::new();
462        let pools_raw = data.get(&DataField::Pools);
463        if let Some(pools_response) = pools_raw {
464            for (idx, _) in pools_response
465                .as_array()
466                .unwrap_or(&Vec::new())
467                .iter()
468                .enumerate()
469            {
470                let user = pools_raw
471                    .and_then(|val| val.pointer(&format!("/{}/User", idx)))
472                    .map(|val| String::from(val.as_str().unwrap_or("")));
473
474                let alive = pools_raw
475                    .and_then(|val| val.pointer(&format!("/{}/Status", idx)))
476                    .map(|val| val.as_str())
477                    .map(|val| val == Some("Alive"));
478
479                let active = pools_raw
480                    .and_then(|val| val.pointer(&format!("/{}/Stratum Active", idx)))
481                    .and_then(|val| val.as_bool());
482
483                let url = pools_raw
484                    .and_then(|val| val.pointer(&format!("/{}/URL", idx)))
485                    .map(|val| PoolURL::from(String::from(val.as_str().unwrap_or(""))));
486
487                let accepted_shares = pools_raw
488                    .and_then(|val| val.pointer(&format!("/{}/Accepted", idx)))
489                    .and_then(|val| val.as_u64());
490
491                let rejected_shares = pools_raw
492                    .and_then(|val| val.pointer(&format!("/{}/Rejected", idx)))
493                    .and_then(|val| val.as_u64());
494
495                pools.push(PoolData {
496                    position: Some(idx as u16),
497                    url,
498                    accepted_shares,
499                    rejected_shares,
500                    active,
501                    alive,
502                    user,
503                });
504            }
505        }
506        pools
507    }
508}
509
510#[async_trait]
511impl SetFaultLight for WhatsMinerV1 {
512    #[allow(unused_variables)]
513    async fn set_fault_light(&self, fault: bool) -> anyhow::Result<bool> {
514        anyhow::bail!("Unsupported command");
515    }
516}
517
518#[async_trait]
519impl SetPowerLimit for WhatsMinerV1 {
520    #[allow(unused_variables)]
521    async fn set_power_limit(&self, limit: Power) -> anyhow::Result<bool> {
522        anyhow::bail!("Unsupported command");
523    }
524}
525
526#[async_trait]
527impl Restart for WhatsMinerV1 {
528    async fn restart(&self) -> anyhow::Result<bool> {
529        anyhow::bail!("Unsupported command");
530    }
531}
532
533#[async_trait]
534impl Pause for WhatsMinerV1 {
535    #[allow(unused_variables)]
536    async fn pause(&self, at_time: Option<Duration>) -> anyhow::Result<bool> {
537        anyhow::bail!("Unsupported command");
538    }
539}
540
541#[async_trait]
542impl Resume for WhatsMinerV1 {
543    #[allow(unused_variables)]
544    async fn resume(&self, at_time: Option<Duration>) -> anyhow::Result<bool> {
545        anyhow::bail!("Unsupported command");
546    }
547}
548
549#[cfg(test)]
550mod tests {
551    use super::*;
552    use crate::data::device::models::whatsminer::WhatsMinerModel;
553    use crate::test::api::MockAPIClient;
554    use crate::test::json::btminer::v1::{
555        DEVS_COMMAND, GET_PSU_COMMAND, GET_VERSION_COMMAND, POOLS_COMMAND, STATUS_COMMAND,
556        SUMMARY_COMMAND,
557    };
558
559    #[tokio::test]
560    async fn test_whatsminer_v1_data_parsers() -> anyhow::Result<()> {
561        let miner = WhatsMinerV1::new(
562            IpAddr::from([127, 0, 0, 1]),
563            MinerModel::WhatsMiner(WhatsMinerModel::M20SV10),
564        );
565        let mut results = HashMap::new();
566        let summary_command: MinerCommand = MinerCommand::RPC {
567            command: "summary",
568            parameters: None,
569        };
570        let status_command: MinerCommand = MinerCommand::RPC {
571            command: "status",
572            parameters: None,
573        };
574        let pools_command: MinerCommand = MinerCommand::RPC {
575            command: "pools",
576            parameters: None,
577        };
578        let devs_command: MinerCommand = MinerCommand::RPC {
579            command: "devs",
580            parameters: None,
581        };
582        let get_version_command: MinerCommand = MinerCommand::RPC {
583            command: "get_version",
584            parameters: None,
585        };
586        let get_psu_command: MinerCommand = MinerCommand::RPC {
587            command: "get_psu",
588            parameters: None,
589        };
590
591        results.insert(summary_command, Value::from_str(SUMMARY_COMMAND)?);
592        results.insert(status_command, Value::from_str(STATUS_COMMAND)?);
593        results.insert(pools_command, Value::from_str(POOLS_COMMAND)?);
594        results.insert(devs_command, Value::from_str(DEVS_COMMAND)?);
595        results.insert(get_version_command, Value::from_str(GET_VERSION_COMMAND)?);
596        results.insert(get_psu_command, Value::from_str(GET_PSU_COMMAND)?);
597
598        let mock_api = MockAPIClient::new(results);
599
600        let mut collector = DataCollector::new_with_client(&miner, &mock_api);
601        let data = collector.collect_all().await;
602
603        let miner_data = miner.parse_data(data);
604
605        assert_eq!(&miner_data.ip, &miner.ip);
606        assert_eq!(
607            miner_data.mac,
608            Some(MacAddr::from_str("C4:08:28:00:A4:19")?)
609        );
610        assert_eq!(miner_data.api_version, Some("1.4.0".to_string()));
611        assert_eq!(
612            miner_data.firmware_version,
613            Some("20210322.22.REL".to_string())
614        );
615        assert_eq!(
616            miner_data.control_board_version,
617            Some(MinerControlBoard::H3)
618        );
619        assert_eq!(
620            miner_data.hashrate,
621            Some(HashRate {
622                value: 67.39480097,
623                unit: HashRateUnit::TeraHash,
624                algo: String::from("SHA256"),
625            })
626        );
627        assert_eq!(
628            miner_data.expected_hashrate,
629            Some(HashRate {
630                value: 68.796,
631                unit: HashRateUnit::TeraHash,
632                algo: String::from("SHA256"),
633            })
634        );
635        assert_eq!(miner_data.wattage, Some(Power::from_watts(3417f64)));
636        assert_eq!(miner_data.wattage_limit, Some(Power::from_watts(3500f64)));
637        assert_eq!(miner_data.uptime, Some(Duration::from_secs(10154)));
638        assert_eq!(miner_data.fans.len(), 2);
639        assert_eq!(miner_data.pools.len(), 3);
640
641        Ok(())
642    }
643}