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