1use std::collections::HashMap;
2use std::net::IpAddr;
3use std::str::FromStr;
4use std::time::{Duration, SystemTime, UNIX_EPOCH};
5
6use async_trait::async_trait;
7use macaddr::MacAddr;
8use measurements::{AngularVelocity, Frequency, Power, Temperature, Voltage};
9use serde_json::Value;
10
11use crate::data::board::{BoardData, ChipData};
12use crate::data::device::MinerMake;
13use crate::data::device::{DeviceInfo, HashAlgorithm, MinerFirmware, MinerModel};
14use crate::data::fan::FanData;
15use crate::data::hashrate::{HashRate, HashRateUnit};
16use crate::data::message::{MessageSeverity, MinerMessage};
17use crate::data::pool::{PoolData, PoolScheme, PoolURL};
18use crate::miners::backends::traits::*;
19use crate::miners::commands::MinerCommand;
20use crate::miners::data::{
21 DataCollector, DataExtensions, DataExtractor, DataField, DataLocation, get_by_key,
22 get_by_pointer,
23};
24use web::ESPMinerWebAPI;
25pub mod web;
26
27#[derive(Debug)]
28pub struct ESPMiner290 {
29 ip: IpAddr,
30 web: ESPMinerWebAPI,
31 device_info: DeviceInfo,
32}
33
34impl ESPMiner290 {
35 pub fn new(ip: IpAddr, model: MinerModel, firmware: MinerFirmware) -> Self {
36 ESPMiner290 {
37 ip,
38 web: ESPMinerWebAPI::new(ip, 80),
39 device_info: DeviceInfo::new(MinerMake::BitAxe, model, firmware, HashAlgorithm::SHA256),
40 }
41 }
42}
43
44#[async_trait]
45impl GetDataLocations for ESPMiner290 {
46 fn get_locations(&self, data_field: DataField) -> Vec<DataLocation> {
47 let system_info_cmd: MinerCommand = MinerCommand::WebAPI {
48 command: "system/info",
49 parameters: None,
50 };
51 let asic_info_cmd: MinerCommand = MinerCommand::WebAPI {
52 command: "system/asic",
53 parameters: None,
54 };
55
56 match data_field {
57 DataField::Mac => vec![(
58 system_info_cmd,
59 DataExtractor {
60 func: get_by_key,
61 key: Some("macAddr"),
62 tag: None,
63 },
64 )],
65 DataField::Hostname => vec![(
66 system_info_cmd,
67 DataExtractor {
68 func: get_by_key,
69 key: Some("hostname"),
70 tag: None,
71 },
72 )],
73 DataField::FirmwareVersion => vec![(
74 system_info_cmd,
75 DataExtractor {
76 func: get_by_key,
77 key: Some("version"),
78 tag: None,
79 },
80 )],
81 DataField::ApiVersion => vec![(
82 system_info_cmd,
83 DataExtractor {
84 func: get_by_key,
85 key: Some("version"),
86 tag: None,
87 },
88 )],
89 DataField::ControlBoardVersion => vec![(
90 system_info_cmd,
91 DataExtractor {
92 func: get_by_key,
93 key: Some("boardVersion"),
94 tag: None,
95 },
96 )],
97 DataField::ExpectedHashrate => vec![(
98 system_info_cmd,
99 DataExtractor {
100 func: get_by_key,
101 key: Some("expectedHashrate"),
102 tag: None,
103 },
104 )],
105 DataField::Hashboards => vec![
106 (
107 system_info_cmd,
108 DataExtractor {
109 func: get_by_pointer,
110 key: Some(""),
111 tag: None,
112 },
113 ),
114 (
115 asic_info_cmd,
116 DataExtractor {
117 func: get_by_pointer,
118 key: Some(""),
119 tag: None,
120 },
121 ),
122 ],
123 DataField::Hashrate => vec![(
124 system_info_cmd,
125 DataExtractor {
126 func: get_by_key,
127 key: Some("hashRate"),
128 tag: None,
129 },
130 )],
131 DataField::Fans => vec![(
132 system_info_cmd,
133 DataExtractor {
134 func: get_by_key,
135 key: Some("fanrpm"),
136 tag: None,
137 },
138 )],
139 DataField::AverageTemperature => vec![(
140 system_info_cmd,
141 DataExtractor {
142 func: get_by_key,
143 key: Some("temp"),
144 tag: None,
145 },
146 )],
147 DataField::Wattage => vec![(
148 system_info_cmd,
149 DataExtractor {
150 func: get_by_key,
151 key: Some("power"),
152 tag: None,
153 },
154 )],
155 DataField::Uptime => vec![(
156 system_info_cmd,
157 DataExtractor {
158 func: get_by_key,
159 key: Some("uptimeSeconds"),
160 tag: None,
161 },
162 )],
163 DataField::Pools => vec![(
164 system_info_cmd,
165 DataExtractor {
166 func: get_by_pointer,
167 key: Some(""),
168 tag: None,
169 },
170 )],
171 _ => vec![],
172 }
173 }
174}
175
176impl GetIP for ESPMiner290 {
177 fn get_ip(&self) -> IpAddr {
178 self.ip
179 }
180}
181impl GetDeviceInfo for ESPMiner290 {
182 fn get_device_info(&self) -> DeviceInfo {
183 self.device_info
184 }
185}
186
187impl CollectData for ESPMiner290 {
188 fn get_collector(&self) -> DataCollector<'_> {
189 DataCollector::new(self, &self.web)
190 }
191}
192
193impl GetMAC for ESPMiner290 {
194 fn parse_mac(&self, data: &HashMap<DataField, Value>) -> Option<MacAddr> {
195 data.extract::<String>(DataField::Mac)
196 .and_then(|s| MacAddr::from_str(&s).ok())
197 }
198}
199
200impl GetSerialNumber for ESPMiner290 {
201 }
203impl GetHostname for ESPMiner290 {
204 fn parse_hostname(&self, data: &HashMap<DataField, Value>) -> Option<String> {
205 data.extract::<String>(DataField::Hostname)
206 }
207}
208impl GetApiVersion for ESPMiner290 {
209 fn parse_api_version(&self, data: &HashMap<DataField, Value>) -> Option<String> {
210 data.extract::<String>(DataField::ApiVersion)
211 }
212}
213impl GetFirmwareVersion for ESPMiner290 {
214 fn parse_firmware_version(&self, data: &HashMap<DataField, Value>) -> Option<String> {
215 data.extract::<String>(DataField::FirmwareVersion)
216 }
217}
218impl GetControlBoardVersion for ESPMiner290 {
219 fn parse_control_board_version(&self, data: &HashMap<DataField, Value>) -> Option<String> {
220 data.extract::<String>(DataField::ControlBoardVersion)
221 }
222}
223impl GetHashboards for ESPMiner290 {
224 fn parse_hashboards(&self, data: &HashMap<DataField, Value>) -> Vec<BoardData> {
225 let board_voltage = data.extract_nested_map::<f64, _>(
227 DataField::Hashboards,
228 "voltage",
229 Voltage::from_millivolts,
230 );
231
232 let board_temperature = data.extract_nested_map::<f64, _>(
233 DataField::Hashboards,
234 "vrTemp",
235 Temperature::from_celsius,
236 );
237
238 let board_frequency = data.extract_nested_map::<f64, _>(
239 DataField::Hashboards,
240 "frequency",
241 Frequency::from_megahertz,
242 );
243
244 let chip_temperature = data.extract_nested_map::<f64, _>(
245 DataField::Hashboards,
246 "temp",
247 Temperature::from_celsius,
248 );
249
250 let expected_hashrate = Some(HashRate {
251 value: data.extract_nested_or::<f64>(DataField::Hashboards, "expectedHashrate", 0.0),
252 unit: HashRateUnit::GigaHash,
253 algo: "SHA256".to_string(),
254 });
255
256 let board_hashrate = Some(HashRate {
257 value: data.extract_nested_or::<f64>(DataField::Hashboards, "hashRate", 0.0),
258 unit: HashRateUnit::GigaHash,
259 algo: "SHA256".to_string(),
260 });
261
262 let total_chips =
263 data.extract_nested_map::<u64, _>(DataField::Hashboards, "asicCount", |u| u as u16);
264
265 let chip_info = ChipData {
266 position: 0,
267 temperature: chip_temperature,
268 voltage: board_voltage,
269 frequency: board_frequency,
270 tuned: Some(true),
271 working: Some(true),
272 hashrate: board_hashrate.clone(),
273 };
274
275 let board_data = BoardData {
276 position: 0,
277 hashrate: board_hashrate,
278 expected_hashrate,
279 board_temperature,
280 intake_temperature: board_temperature,
281 outlet_temperature: board_temperature,
282 expected_chips: self.device_info.hardware.chips,
283 working_chips: total_chips,
284 serial_number: None,
285 chips: vec![chip_info],
286 voltage: board_voltage,
287 frequency: board_frequency,
288 tuned: Some(true),
289 active: Some(true),
290 };
291
292 vec![board_data]
293 }
294}
295impl GetHashrate for ESPMiner290 {
296 fn parse_hashrate(&self, data: &HashMap<DataField, Value>) -> Option<HashRate> {
297 data.extract_map::<f64, _>(DataField::Hashrate, |f| HashRate {
298 value: f,
299 unit: HashRateUnit::TeraHash,
300 algo: String::from("SHA256"),
301 })
302 }
303}
304
305impl GetExpectedHashrate for ESPMiner290 {
306 fn parse_expected_hashrate(&self, data: &HashMap<DataField, Value>) -> Option<HashRate> {
307 data.extract_map::<f64, _>(DataField::Hashrate, |f| HashRate {
308 value: f,
309 unit: HashRateUnit::TeraHash,
310 algo: String::from("SHA256"),
311 })
312 }
313}
314impl GetFans for ESPMiner290 {
315 fn parse_fans(&self, data: &HashMap<DataField, Value>) -> Vec<FanData> {
316 data.extract_map_or::<f64, _>(DataField::Fans, Vec::new(), |f| {
317 vec![FanData {
318 position: 0,
319 rpm: Some(AngularVelocity::from_rpm(f)),
320 }]
321 })
322 }
323}
324impl GetPsuFans for ESPMiner290 {
325 }
327impl GetFluidTemperature for ESPMiner290 {
328 }
330impl GetWattage for ESPMiner290 {
331 fn parse_wattage(&self, data: &HashMap<DataField, Value>) -> Option<Power> {
332 data.extract_map::<f64, _>(DataField::Wattage, Power::from_watts)
333 }
334}
335impl GetWattageLimit for ESPMiner290 {
336 }
338impl GetLightFlashing for ESPMiner290 {
339 }
341impl GetMessages for ESPMiner290 {
342 fn parse_messages(&self, data: &HashMap<DataField, Value>) -> Vec<MinerMessage> {
343 let mut messages = Vec::new();
344 let timestamp = SystemTime::now()
345 .duration_since(UNIX_EPOCH)
346 .expect("Failed to get system time")
347 .as_secs();
348
349 let is_overheating = data.extract_nested::<bool>(DataField::Hashboards, "overheat_mode");
350
351 if let Some(true) = is_overheating {
352 messages.push(MinerMessage {
353 timestamp: timestamp as u32,
354 code: 0u64,
355 message: "Overheat Mode is Enabled!".to_string(),
356 severity: MessageSeverity::Warning,
357 });
358 };
359 messages
360 }
361}
362impl GetUptime for ESPMiner290 {
363 fn parse_uptime(&self, data: &HashMap<DataField, Value>) -> Option<Duration> {
364 data.extract_map::<u64, _>(DataField::Uptime, Duration::from_secs)
365 }
366}
367impl GetIsMining for ESPMiner290 {
368 fn parse_is_mining(&self, data: &HashMap<DataField, Value>) -> bool {
369 let hashrate = self.parse_hashrate(data);
370 hashrate.as_ref().is_some_and(|hr| hr.value > 0.0)
371 }
372}
373impl GetPools for ESPMiner290 {
374 fn parse_pools(&self, data: &HashMap<DataField, Value>) -> Vec<PoolData> {
375 let main_url =
376 data.extract_nested_or::<String>(DataField::Pools, "stratumURL", String::new());
377 let main_port = data.extract_nested_or::<u64>(DataField::Pools, "stratumPort", 0);
378 let accepted_share = data.extract_nested::<u64>(DataField::Pools, "sharesAccepted");
379 let rejected_share = data.extract_nested::<u64>(DataField::Pools, "sharesRejected");
380 let main_user = data.extract_nested::<String>(DataField::Pools, "stratumUser");
381
382 let is_using_fallback =
383 data.extract_nested_or::<bool>(DataField::Pools, "isUsingFallbackStratum", false);
384
385 let main_pool_url = PoolURL {
386 scheme: PoolScheme::StratumV1,
387 host: main_url,
388 port: main_port as u16,
389 pubkey: None,
390 };
391
392 let main_pool_data = PoolData {
393 position: Some(0),
394 url: Some(main_pool_url),
395 accepted_shares: accepted_share,
396 rejected_shares: rejected_share,
397 active: Some(!is_using_fallback),
398 alive: None,
399 user: main_user,
400 };
401
402 let fallback_url =
404 data.extract_nested_or::<String>(DataField::Pools, "fallbackStratumURL", String::new());
405 let fallback_port =
406 data.extract_nested_or::<u64>(DataField::Pools, "fallbackStratumPort", 0);
407 let fallback_user = data.extract_nested(DataField::Pools, "fallbackStratumUser");
408 let fallback_pool_url = PoolURL {
409 scheme: PoolScheme::StratumV1,
410 host: fallback_url,
411 port: fallback_port as u16,
412 pubkey: None,
413 };
414
415 let fallback_pool_data = PoolData {
416 position: Some(1),
417 url: Some(fallback_pool_url),
418 accepted_shares: accepted_share,
419 rejected_shares: rejected_share,
420 active: Some(is_using_fallback),
421 alive: None,
422 user: fallback_user,
423 };
424
425 vec![main_pool_data, fallback_pool_data]
426 }
427}