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

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            // Ignore version (eg `MaraCB_v1.4`)
287            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}