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, 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 {} impl 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}