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