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