1use crate::data::board::{BoardData, ChipData};
2use crate::data::device::MinerMake;
3use crate::data::device::{DeviceInfo, HashAlgorithm, MinerFirmware, MinerModel};
4use crate::data::fan::FanData;
5use crate::data::hashrate::{HashRate, HashRateUnit};
6use crate::data::pool::{PoolData, PoolURL};
7use crate::miners::backends::traits::*;
8use crate::miners::commands::MinerCommand;
9use crate::miners::data::{
10 DataCollector, DataExtensions, DataExtractor, DataField, DataLocation, get_by_pointer,
11};
12use anyhow::{Result, anyhow};
13use async_trait::async_trait;
14use macaddr::MacAddr;
15use measurements::{AngularVelocity, Frequency, Power, Temperature, Voltage};
16use serde_json::Value;
17use std::collections::HashMap;
18use std::net::IpAddr;
19use std::str::FromStr;
20use std::time::Duration;
21
22use crate::data::message::{MessageSeverity, MinerMessage};
23use web::MaraWebAPI;
24
25mod web;
26
27#[derive(Debug)]
28pub struct MaraV1 {
29 ip: IpAddr,
30 web: MaraWebAPI,
31 device_info: DeviceInfo,
32}
33
34impl MaraV1 {
35 pub fn new(ip: IpAddr, model: MinerModel) -> Self {
36 MaraV1 {
37 ip,
38 web: MaraWebAPI::new(ip, 80),
39 device_info: DeviceInfo::new(
40 MinerMake::from(model),
41 model,
42 MinerFirmware::Marathon,
43 HashAlgorithm::SHA256,
44 ),
45 }
46 }
47}
48
49#[async_trait]
50impl APIClient for MaraV1 {
51 async fn get_api_result(&self, command: &MinerCommand) -> Result<Value> {
52 match command {
53 MinerCommand::WebAPI { .. } => self.web.get_api_result(command).await,
54 _ => Err(anyhow!("Unsupported command type for Marathon API")),
55 }
56 }
57}
58
59impl GetDataLocations for MaraV1 {
60 fn get_locations(&self, data_field: DataField) -> Vec<DataLocation> {
61 fn cmd(endpoint: &'static str) -> MinerCommand {
62 MinerCommand::WebAPI {
63 command: endpoint,
64 parameters: None,
65 }
66 }
67
68 let brief_cmd = cmd("brief");
69 let overview_cmd = cmd("overview");
70 let hashboards_cmd = cmd("hashboards");
71 let fans_cmd = cmd("fans");
72 let pools_cmd = cmd("pools");
73 let network_config_cmd = cmd("network_config");
74 let miner_config_cmd = cmd("miner_config");
75 let locate_miner_cmd = cmd("locate_miner");
76 let details_cmd = cmd("details");
77 let messages_cmd = cmd("event_chart");
78
79 match data_field {
80 DataField::Mac => vec![(
81 overview_cmd,
82 DataExtractor {
83 func: get_by_pointer,
84 key: Some("/mac"),
85 tag: None,
86 },
87 )],
88 DataField::FirmwareVersion => vec![(
89 overview_cmd,
90 DataExtractor {
91 func: get_by_pointer,
92 key: Some("/version_firmware"),
93 tag: None,
94 },
95 )],
96 DataField::ControlBoardVersion => vec![(
97 overview_cmd,
98 DataExtractor {
99 func: get_by_pointer,
100 key: Some("/control_board"),
101 tag: None,
102 },
103 )],
104 DataField::Hostname => vec![(
105 network_config_cmd,
106 DataExtractor {
107 func: get_by_pointer,
108 key: Some("/hostname"),
109 tag: None,
110 },
111 )],
112 DataField::Hashrate => vec![(
113 brief_cmd,
114 DataExtractor {
115 func: get_by_pointer,
116 key: Some("/hashrate_realtime"),
117 tag: None,
118 },
119 )],
120 DataField::ExpectedHashrate => vec![(
121 brief_cmd,
122 DataExtractor {
123 func: get_by_pointer,
124 key: Some("/hashrate_ideal"),
125 tag: None,
126 },
127 )],
128 DataField::Hashboards => vec![
129 (
130 details_cmd,
131 DataExtractor {
132 func: get_by_pointer,
133 key: Some("/hashboard_infos"),
134 tag: Some("chip_data"),
135 },
136 ),
137 (
138 hashboards_cmd,
139 DataExtractor {
140 func: get_by_pointer,
141 key: Some("/hashboards"),
142 tag: Some("hb_temps"),
143 },
144 ),
145 ],
146 DataField::Wattage => vec![(
147 brief_cmd,
148 DataExtractor {
149 func: get_by_pointer,
150 key: Some("/power_consumption_estimated"),
151 tag: None,
152 },
153 )],
154 DataField::WattageLimit => vec![(
155 miner_config_cmd,
156 DataExtractor {
157 func: get_by_pointer,
158 key: Some("/mode/concorde/power-target"),
159 tag: None,
160 },
161 )],
162 DataField::Fans => vec![(
163 fans_cmd,
164 DataExtractor {
165 func: get_by_pointer,
166 key: Some("/fans"),
167 tag: None,
168 },
169 )],
170 DataField::LightFlashing => vec![(
171 locate_miner_cmd,
172 DataExtractor {
173 func: get_by_pointer,
174 key: Some("/blinking"),
175 tag: None,
176 },
177 )],
178 DataField::IsMining => vec![(
179 brief_cmd,
180 DataExtractor {
181 func: get_by_pointer,
182 key: Some("/status"),
183 tag: None,
184 },
185 )],
186 DataField::Uptime => vec![(
187 brief_cmd,
188 DataExtractor {
189 func: get_by_pointer,
190 key: Some("/elapsed"),
191 tag: None,
192 },
193 )],
194 DataField::Pools => vec![(
195 pools_cmd,
196 DataExtractor {
197 func: get_by_pointer,
198 key: Some(""),
199 tag: None,
200 },
201 )],
202 DataField::Messages => vec![(
203 messages_cmd,
204 DataExtractor {
205 func: get_by_pointer,
206 key: Some("/event_flags"),
207 tag: None,
208 },
209 )],
210 _ => vec![],
211 }
212 }
213}
214
215impl GetIP for MaraV1 {
216 fn get_ip(&self) -> IpAddr {
217 self.ip
218 }
219}
220
221impl GetDeviceInfo for MaraV1 {
222 fn get_device_info(&self) -> DeviceInfo {
223 self.device_info
224 }
225}
226
227impl CollectData for MaraV1 {
228 fn get_collector(&self) -> DataCollector<'_> {
229 DataCollector::new(self)
230 }
231}
232
233impl GetMAC for MaraV1 {
234 fn parse_mac(&self, data: &HashMap<DataField, Value>) -> Option<MacAddr> {
235 data.extract::<String>(DataField::Mac)
236 .and_then(|mac_str| MacAddr::from_str(&mac_str.to_uppercase()).ok())
237 }
238}
239
240impl GetSerialNumber for MaraV1 {}
241
242impl GetHostname for MaraV1 {
243 fn parse_hostname(&self, data: &HashMap<DataField, Value>) -> Option<String> {
244 data.extract::<String>(DataField::Hostname)
245 }
246}
247
248impl GetApiVersion for MaraV1 {}
249
250impl GetFirmwareVersion for MaraV1 {
251 fn parse_firmware_version(&self, data: &HashMap<DataField, Value>) -> Option<String> {
252 data.extract::<String>(DataField::FirmwareVersion)
253 }
254}
255
256impl GetControlBoardVersion for MaraV1 {}
257
258impl MaraV1 {
259 fn parse_chip_data(asic_infos: &Value) -> Vec<ChipData> {
260 asic_infos
261 .as_array()
262 .map(|chips| {
263 chips
264 .iter()
265 .filter_map(|chip| {
266 let position = chip.get("index")?.as_u64()? as u16;
267
268 let hashrate =
269 chip.get("hashrate_avg")
270 .and_then(|hr| hr.as_f64())
271 .map(|value| HashRate {
272 value,
273 unit: HashRateUnit::GigaHash,
274 algo: "SHA256".to_string(),
275 });
276
277 let voltage = chip
278 .get("voltage")
279 .and_then(|v| v.as_f64())
280 .map(Voltage::from_volts);
281
282 let frequency = chip
283 .get("frequency")
284 .and_then(|f| f.as_f64())
285 .map(Frequency::from_megahertz);
286
287 let working = chip
288 .get("hashrate_avg")
289 .and_then(|hr| hr.as_f64())
290 .map(|hr| hr > 0.0);
291
292 Some(ChipData {
293 position,
294 hashrate,
295 temperature: None,
296 voltage,
297 frequency,
298 tuned: None,
299 working,
300 })
301 })
302 .collect()
303 })
304 .unwrap_or_default()
305 }
306}
307
308impl GetHashboards for MaraV1 {
309 fn parse_hashboards(&self, data: &HashMap<DataField, Value>) -> Vec<BoardData> {
310 let mut hashboards: Vec<BoardData> = Vec::new();
311
312 if let Some(expected_boards) = self.device_info.hardware.boards {
313 for i in 0..expected_boards {
314 hashboards.push(BoardData {
315 position: i,
316 hashrate: None,
317 expected_hashrate: None,
318 board_temperature: None,
319 intake_temperature: None,
320 outlet_temperature: None,
321 expected_chips: self.device_info.hardware.chips,
322 working_chips: None,
323 serial_number: None,
324 chips: vec![],
325 voltage: None,
326 frequency: None,
327 tuned: None,
328 active: None,
329 });
330 }
331 }
332
333 if let Some(hashboards_data) = data
334 .get(&DataField::Hashboards)
335 .and_then(|v| v.pointer("/chip_data"))
336 && let Some(hb_array) = hashboards_data.as_array()
337 {
338 let hashboard_temps = data
339 .get(&DataField::Hashboards)
340 .and_then(|v| v.pointer("/hb_temps"))
341 .and_then(|v| v.as_array());
342
343 for hb in hb_array {
344 if let Some(idx) = hb.get("index").and_then(|v| v.as_u64())
345 && let Some(hashboard) = hashboards.get_mut(idx as usize)
346 {
347 hashboard.position = idx as u8;
348
349 let hb_temps = hashboard_temps
350 .and_then(|temps| temps.get(idx as usize))
351 .and_then(|v| v.as_object());
352
353 if let Some(hashrate) = hb.get("hashrate_avg").and_then(|v| v.as_f64()) {
354 hashboard.hashrate = Some(HashRate {
355 value: hashrate,
356 unit: HashRateUnit::GigaHash,
357 algo: String::from("SHA256"),
358 });
359 }
360
361 if let Some(temps_obj) = hb_temps {
362 if let Some(temp_pcb) =
363 temps_obj.get("temperature_pcb").and_then(|v| v.as_array())
364 {
365 let temps: Vec<f64> =
366 temp_pcb.iter().filter_map(|t| t.as_f64()).collect();
367 if !temps.is_empty() {
368 let avg_temp = temps.iter().sum::<f64>() / temps.len() as f64;
369 hashboard.board_temperature =
370 Some(Temperature::from_celsius(avg_temp));
371 }
372 }
373
374 if let Some(temp_raw) =
375 temps_obj.get("temperature_raw").and_then(|v| v.as_array())
376 {
377 let temps: Vec<f64> =
378 temp_raw.iter().filter_map(|t| t.as_f64()).collect();
379 if !temps.is_empty() {
380 let avg_temp = temps.iter().sum::<f64>() / temps.len() as f64;
381 hashboard.intake_temperature =
382 Some(Temperature::from_celsius(avg_temp));
383 }
384 }
385 }
386
387 if let Some(asic_num) = hb.get("asic_num").and_then(|v| v.as_u64()) {
388 hashboard.working_chips = Some(asic_num as u16);
389 }
390
391 if let Some(serial) = hb.get("serial_number").and_then(|v| v.as_str()) {
392 hashboard.serial_number = Some(serial.to_string());
393 }
394
395 if let Some(voltage) = hb.get("voltage").and_then(|v| v.as_f64()) {
396 hashboard.voltage = Some(Voltage::from_volts(voltage));
397 }
398
399 if let Some(frequency) = hb.get("frequency_avg").and_then(|v| v.as_f64()) {
400 hashboard.frequency = Some(Frequency::from_megahertz(frequency));
401 }
402
403 if let Some(expected_hashrate) =
404 hb.get("hashrate_ideal").and_then(|v| v.as_f64())
405 {
406 hashboard.expected_hashrate = Some(HashRate {
407 value: expected_hashrate,
408 unit: HashRateUnit::GigaHash,
409 algo: String::from("SHA256"),
410 });
411 }
412
413 hashboard.active = Some(true);
414
415 if let Some(asic_infos) = hb.get("asic_infos") {
416 hashboard.chips = Self::parse_chip_data(asic_infos);
417 }
418 }
419 }
420 }
421
422 hashboards
423 }
424}
425
426impl GetHashrate for MaraV1 {
427 fn parse_hashrate(&self, data: &HashMap<DataField, Value>) -> Option<HashRate> {
428 data.extract::<f64>(DataField::Hashrate)
429 .map(|rate| HashRate {
430 value: rate,
431 unit: HashRateUnit::TeraHash,
432 algo: String::from("SHA256"),
433 })
434 }
435}
436
437impl GetExpectedHashrate for MaraV1 {
438 fn parse_expected_hashrate(&self, data: &HashMap<DataField, Value>) -> Option<HashRate> {
439 data.extract::<f64>(DataField::ExpectedHashrate)
440 .map(|rate| HashRate {
441 value: rate,
442 unit: HashRateUnit::GigaHash,
443 algo: String::from("SHA256"),
444 })
445 }
446}
447
448impl GetFans for MaraV1 {
449 fn parse_fans(&self, data: &HashMap<DataField, Value>) -> Vec<FanData> {
450 let mut fans: Vec<FanData> = Vec::new();
451
452 if let Some(fans_data) = data.get(&DataField::Fans)
453 && let Some(fans_array) = fans_data.as_array()
454 {
455 for (i, fan) in fans_array.iter().enumerate() {
456 if let Some(speed) = fan.get("current_speed").and_then(|v| v.as_f64()) {
457 fans.push(FanData {
458 position: i as i16,
459 rpm: Some(AngularVelocity::from_rpm(speed)),
460 });
461 }
462 }
463 }
464
465 if fans.is_empty()
466 && let Some(expected_fans) = self.device_info.hardware.fans
467 {
468 for i in 0..expected_fans {
469 fans.push(FanData {
470 position: i as i16,
471 rpm: None,
472 });
473 }
474 }
475
476 fans
477 }
478}
479
480impl GetPsuFans for MaraV1 {}
481
482impl GetFluidTemperature for MaraV1 {}
483
484impl GetWattage for MaraV1 {
485 fn parse_wattage(&self, data: &HashMap<DataField, Value>) -> Option<Power> {
486 data.extract::<f64>(DataField::Wattage)
487 .map(Power::from_watts)
488 }
489}
490
491impl GetWattageLimit for MaraV1 {
492 fn parse_wattage_limit(&self, data: &HashMap<DataField, Value>) -> Option<Power> {
493 data.extract::<f64>(DataField::WattageLimit)
494 .map(Power::from_watts)
495 }
496}
497
498impl GetLightFlashing for MaraV1 {
499 fn parse_light_flashing(&self, data: &HashMap<DataField, Value>) -> Option<bool> {
500 data.extract::<bool>(DataField::LightFlashing)
501 }
502}
503
504impl GetMessages for MaraV1 {
505 fn parse_messages(&self, data: &HashMap<DataField, Value>) -> Vec<MinerMessage> {
506 let messages = data.get(&DataField::Messages).and_then(|v| v.as_array());
507 let mut result = vec![];
508 if let Some(m) = messages {
509 for message in m {
510 let level = if let Some(level) = message.get("level").and_then(|v| v.as_str()) {
511 match level {
512 "info" => MessageSeverity::Info,
513 "warning" => MessageSeverity::Warning,
514 "error" => MessageSeverity::Error,
515 _ => MessageSeverity::Info,
516 }
517 } else {
518 MessageSeverity::Info
519 };
520
521 let message_text = message
522 .get("message")
523 .and_then(|v| v.as_str())
524 .unwrap_or("")
525 .to_string();
526 let timestamp = message
527 .get("timestamp")
528 .and_then(|v| v.as_u64())
529 .unwrap_or(0);
530
531 let m_msg = MinerMessage {
532 timestamp: timestamp as u32,
533 code: 0,
534 message: message_text,
535 severity: level,
536 };
537
538 result.push(m_msg);
539 }
540 }
541
542 result
543 }
544}
545impl GetUptime for MaraV1 {
546 fn parse_uptime(&self, data: &HashMap<DataField, Value>) -> Option<Duration> {
547 data.extract::<u64>(DataField::Uptime)
548 .map(Duration::from_secs)
549 }
550}
551
552impl GetIsMining for MaraV1 {
553 fn parse_is_mining(&self, data: &HashMap<DataField, Value>) -> bool {
554 data.extract::<String>(DataField::IsMining)
555 .map(|status| status == "Mining")
556 .unwrap_or(false)
557 }
558}
559
560impl GetPools for MaraV1 {
561 fn parse_pools(&self, data: &HashMap<DataField, Value>) -> Vec<PoolData> {
562 let mut pools_vec: Vec<PoolData> = Vec::new();
563
564 if let Some(pools_data) = data.get(&DataField::Pools)
565 && let Some(pools_array) = pools_data.as_array()
566 {
567 let mut active_pool_index = None;
568 let mut highest_priority = std::i32::MAX;
569
570 for pool_info in pools_array {
571 if let (Some(status), Some(priority), Some(index)) = (
572 pool_info.get("status").and_then(|v| v.as_str()),
573 pool_info.get("priority").and_then(|v| v.as_i64()),
574 pool_info.get("index").and_then(|v| v.as_u64()),
575 ) && status == "Alive"
576 && (priority as i32) < highest_priority
577 {
578 highest_priority = priority as i32;
579 active_pool_index = Some(index as u16);
580 }
581 }
582
583 for pool_info in pools_array {
584 let url = pool_info
585 .get("url")
586 .and_then(|v| v.as_str())
587 .filter(|s| !s.is_empty())
588 .map(|s| PoolURL::from(s.to_string()));
589
590 let index = pool_info
591 .get("index")
592 .and_then(|v| v.as_u64())
593 .map(|i| i as u16);
594 let user = pool_info
595 .get("user")
596 .and_then(|v| v.as_str())
597 .map(String::from);
598 let accepted = pool_info.get("accepted").and_then(|v| v.as_u64());
599 let rejected = pool_info.get("rejected").and_then(|v| v.as_u64());
600 let active = index.map(|i| Some(i) == active_pool_index).unwrap_or(false);
601 let alive = pool_info
602 .get("status")
603 .and_then(|v| v.as_str())
604 .map(|s| s == "Alive");
605
606 pools_vec.push(PoolData {
607 position: index,
608 url,
609 accepted_shares: accepted,
610 rejected_shares: rejected,
611 active: Some(active),
612 alive,
613 user,
614 });
615 }
616 }
617
618 pools_vec
619 }
620}