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

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