asic_rs/miners/backends/marathon/v1/
mod.rs

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}