asic_rs/miners/backends/braiins/v25_07/
mod.rs

1use crate::data::board::BoardData;
2use crate::data::device::{
3    DeviceInfo, HashAlgorithm, MinerControlBoard, MinerFirmware, MinerMake, MinerModel,
4};
5use crate::data::fan::FanData;
6use crate::data::hashrate::{HashRate, HashRateUnit};
7use crate::data::message::{MessageSeverity, MinerMessage};
8use crate::data::pool::{PoolData, PoolURL};
9use crate::miners::backends::traits::*;
10use crate::miners::commands::MinerCommand;
11use crate::miners::data::{
12    DataCollector, DataExtensions, DataExtractor, DataField, DataLocation, get_by_pointer,
13};
14use anyhow;
15use async_trait::async_trait;
16use chrono::{DateTime, Utc};
17use macaddr::MacAddr;
18use measurements::{AngularVelocity, Frequency, Power, Temperature, Voltage};
19use reqwest::Method;
20use serde_json::{Value, json};
21use std::collections::HashMap;
22use std::net::IpAddr;
23use std::str::FromStr;
24use std::time::Duration;
25use web::BraiinsWebAPI;
26
27mod web;
28
29#[derive(Debug)]
30pub struct BraiinsV2507 {
31    pub ip: IpAddr,
32    pub web: BraiinsWebAPI,
33    pub device_info: DeviceInfo,
34}
35
36impl BraiinsV2507 {
37    pub fn new(ip: IpAddr, model: MinerModel) -> Self {
38        BraiinsV2507 {
39            ip,
40            web: BraiinsWebAPI::new(ip),
41            device_info: DeviceInfo::new(
42                MinerMake::from(model),
43                model,
44                MinerFirmware::BraiinsOS,
45                HashAlgorithm::SHA256,
46            ),
47        }
48    }
49}
50
51#[async_trait]
52impl APIClient for BraiinsV2507 {
53    async fn get_api_result(&self, command: &MinerCommand) -> anyhow::Result<Value> {
54        match command {
55            MinerCommand::WebAPI { .. } => self.web.get_api_result(command).await,
56            _ => Err(anyhow::anyhow!("Unsupported command type for Braiins API")),
57        }
58    }
59}
60
61impl GetDataLocations for BraiinsV2507 {
62    fn get_locations(&self, data_field: DataField) -> Vec<DataLocation> {
63        const WEB_NETWORK: MinerCommand = MinerCommand::WebAPI {
64            command: "network",
65            parameters: None,
66        };
67        const WEB_VERSION: MinerCommand = MinerCommand::WebAPI {
68            command: "version",
69            parameters: None,
70        };
71        const WEB_MINER_DETAILS: MinerCommand = MinerCommand::WebAPI {
72            command: "miner/details",
73            parameters: None,
74        };
75        const WEB_LOCATE: MinerCommand = MinerCommand::WebAPI {
76            command: "actions/locate",
77            parameters: None,
78        };
79        const WEB_MINER_STATS: MinerCommand = MinerCommand::WebAPI {
80            command: "miner/stats",
81            parameters: None,
82        };
83        const WEB_PERFORMANCE_TUNER_STATE: MinerCommand = MinerCommand::WebAPI {
84            command: "performance/tuner-state",
85            parameters: None,
86        };
87        const WEB_MINER_ERRORS: MinerCommand = MinerCommand::WebAPI {
88            command: "miner/errors",
89            parameters: None,
90        };
91        const WEB_POOLS: MinerCommand = MinerCommand::WebAPI {
92            command: "pools",
93            parameters: None,
94        };
95        const WEB_COOLING_STATE: MinerCommand = MinerCommand::WebAPI {
96            command: "cooling/state",
97            parameters: None,
98        };
99        const WEB_HASHBOARDS: MinerCommand = MinerCommand::WebAPI {
100            command: "miner/hw/hashboards",
101            parameters: None,
102        };
103
104        match data_field {
105            DataField::Mac => vec![(
106                WEB_NETWORK,
107                DataExtractor {
108                    func: get_by_pointer,
109                    key: Some("/mac_address"),
110                    tag: None,
111                },
112            )],
113            DataField::Hostname => vec![(
114                WEB_NETWORK,
115                DataExtractor {
116                    func: get_by_pointer,
117                    key: Some("/hostname"),
118                    tag: None,
119                },
120            )],
121            DataField::ApiVersion => vec![(
122                WEB_VERSION,
123                DataExtractor {
124                    func: get_by_pointer,
125                    key: Some(""),
126                    tag: None,
127                },
128            )],
129            DataField::FirmwareVersion => vec![(
130                WEB_MINER_DETAILS,
131                DataExtractor {
132                    func: get_by_pointer,
133                    key: Some("/bos_version/current"),
134                    tag: None,
135                },
136            )],
137            DataField::Hashrate => vec![(
138                WEB_MINER_STATS,
139                DataExtractor {
140                    func: get_by_pointer,
141                    key: Some("/miner_stats/real_hashrate/last_5s/gigahash_per_second"),
142                    tag: None,
143                },
144            )],
145            DataField::ExpectedHashrate => vec![(
146                WEB_MINER_DETAILS,
147                DataExtractor {
148                    func: get_by_pointer,
149                    key: Some("/sticker_hashrate/gigahash_per_second"),
150                    tag: None,
151                },
152            )],
153            DataField::Fans => vec![(
154                WEB_COOLING_STATE,
155                DataExtractor {
156                    func: get_by_pointer,
157                    key: Some("/fans"),
158                    tag: None,
159                },
160            )],
161            DataField::Hashboards => vec![(
162                WEB_HASHBOARDS,
163                DataExtractor {
164                    func: get_by_pointer,
165                    key: Some("/hashboards"),
166                    tag: None,
167                },
168            )],
169            DataField::LightFlashing => vec![(
170                WEB_LOCATE,
171                DataExtractor {
172                    func: get_by_pointer,
173                    key: Some(""),
174                    tag: None,
175                },
176            )],
177            DataField::IsMining => vec![(
178                WEB_MINER_DETAILS,
179                DataExtractor {
180                    func: get_by_pointer,
181                    key: Some("/status"),
182                    tag: None,
183                },
184            )],
185            DataField::Uptime => vec![(
186                WEB_MINER_DETAILS,
187                DataExtractor {
188                    func: get_by_pointer,
189                    key: Some("/system_uptime_s"),
190                    tag: None,
191                },
192            )],
193            DataField::ControlBoardVersion => vec![(
194                WEB_MINER_DETAILS,
195                DataExtractor {
196                    func: get_by_pointer,
197                    key: Some("/control_board_soc_family"),
198                    tag: None,
199                },
200            )],
201            DataField::Pools => vec![(
202                WEB_POOLS,
203                DataExtractor {
204                    func: get_by_pointer,
205                    key: Some("/0/pools"), // assuming there is 1 pool group
206                    tag: None,
207                },
208            )],
209            DataField::Wattage => vec![(
210                WEB_MINER_STATS,
211                DataExtractor {
212                    func: get_by_pointer,
213                    key: Some("/power_stats/approximated_consumption/watt"),
214                    tag: None,
215                },
216            )],
217            DataField::WattageLimit => vec![(
218                WEB_PERFORMANCE_TUNER_STATE,
219                DataExtractor {
220                    func: get_by_pointer,
221                    key: Some("/mode_state/powertargetmodestate/current_target/watt"),
222                    tag: None,
223                },
224            )],
225            DataField::SerialNumber => vec![(
226                WEB_MINER_DETAILS,
227                DataExtractor {
228                    func: get_by_pointer,
229                    key: Some("/serial_number"),
230                    tag: None,
231                },
232            )],
233            DataField::Messages => vec![(
234                WEB_MINER_ERRORS,
235                DataExtractor {
236                    func: get_by_pointer,
237                    key: Some("/errors"),
238                    tag: None,
239                },
240            )],
241            _ => vec![],
242        }
243    }
244}
245
246impl GetIP for BraiinsV2507 {
247    fn get_ip(&self) -> IpAddr {
248        self.ip
249    }
250}
251
252impl GetDeviceInfo for BraiinsV2507 {
253    fn get_device_info(&self) -> DeviceInfo {
254        self.device_info
255    }
256}
257
258impl CollectData for BraiinsV2507 {
259    fn get_collector(&self) -> DataCollector<'_> {
260        DataCollector::new(self)
261    }
262}
263
264impl GetMAC for BraiinsV2507 {
265    fn parse_mac(&self, data: &HashMap<DataField, Value>) -> Option<MacAddr> {
266        data.extract::<String>(DataField::Mac)
267            .and_then(|s| MacAddr::from_str(&s).ok())
268    }
269}
270
271impl GetHostname for BraiinsV2507 {
272    fn parse_hostname(&self, data: &HashMap<DataField, Value>) -> Option<String> {
273        data.extract::<String>(DataField::Hostname)
274    }
275}
276
277impl GetApiVersion for BraiinsV2507 {
278    fn parse_api_version(&self, data: &HashMap<DataField, Value>) -> Option<String> {
279        let major = data.extract_nested::<f64>(DataField::ApiVersion, "major");
280        let minor = data.extract_nested::<f64>(DataField::ApiVersion, "minor");
281        let patch = data.extract_nested::<f64>(DataField::ApiVersion, "patch");
282
283        Some(format!("{}.{}.{}", major?, minor?, patch?))
284    }
285}
286
287impl GetFirmwareVersion for BraiinsV2507 {
288    fn parse_firmware_version(&self, data: &HashMap<DataField, Value>) -> Option<String> {
289        data.extract::<String>(DataField::FirmwareVersion)
290    }
291}
292
293impl GetHashboards for BraiinsV2507 {
294    fn parse_hashboards(&self, data: &HashMap<DataField, Value>) -> Vec<BoardData> {
295        let mut hashboards: Vec<BoardData> = Vec::new();
296
297        let chains_data = data.get(&DataField::Hashboards).and_then(|v| v.as_array());
298
299        if let Some(chains_array) = chains_data {
300            for (idx, chain) in chains_array.iter().enumerate() {
301                let hashrate = chain
302                    .pointer("/stats/real_hashrate/last_5s/gigahash_per_second")
303                    .and_then(|v| v.as_f64())
304                    .map(|f| HashRate {
305                        value: f,
306                        unit: HashRateUnit::GigaHash,
307                        algo: String::from("SHA256"),
308                    });
309                let expected_hashrate = chain
310                    .pointer("/stats/nominal_hashrate/gigahash_per_second")
311                    .and_then(|v| v.as_f64())
312                    .map(|f| HashRate {
313                        value: f,
314                        unit: HashRateUnit::GigaHash,
315                        algo: String::from("SHA256"),
316                    });
317
318                let frequency = chain
319                    .pointer("/current_frequency/hertz")
320                    .and_then(|v| v.as_f64())
321                    .map(Frequency::from_hertz);
322                let voltage = chain
323                    .pointer("/current_voltage/volt")
324                    .and_then(|v| v.as_f64())
325                    .map(Voltage::from_volts);
326                let board_temperature = chain
327                    .pointer("/board_temp/degree_c")
328                    .and_then(|v| v.as_f64())
329                    .map(Temperature::from_celsius);
330                let chip_temperature = chain
331                    .pointer("/highest_chip_temp/temperature/degree_c")
332                    .and_then(|v| v.as_f64())
333                    .map(Temperature::from_celsius);
334
335                let working_chips = chain
336                    .pointer("/chips_count")
337                    .and_then(|v| v.as_u64())
338                    .map(|u| u as u16);
339                let active = chain.pointer("/enabled").and_then(|v| v.as_bool());
340                let serial_number = chain
341                    .pointer("/serial_number")
342                    .and_then(|v| v.as_str())
343                    .map(|u| u.to_string());
344
345                hashboards.push(BoardData {
346                    position: chain
347                        .pointer("/id")
348                        .and_then(|v| v.as_u64())
349                        .unwrap_or(idx as u64) as u8,
350                    hashrate,
351                    expected_hashrate,
352                    board_temperature,
353                    intake_temperature: chip_temperature,
354                    outlet_temperature: chip_temperature,
355                    expected_chips: self.device_info.hardware.chips,
356                    working_chips,
357                    serial_number,
358                    chips: Vec::new(),
359                    voltage,
360                    frequency,
361                    tuned: None, // Can maybe be parsed later from tuner status endpoint
362                    active,
363                });
364            }
365        }
366
367        hashboards
368    }
369}
370
371impl GetHashrate for BraiinsV2507 {
372    fn parse_hashrate(&self, data: &HashMap<DataField, Value>) -> Option<HashRate> {
373        data.extract_map::<f64, _>(DataField::Hashrate, |f| HashRate {
374            value: f,
375            unit: HashRateUnit::GigaHash,
376            algo: String::from("SHA256"),
377        })
378    }
379}
380
381impl GetExpectedHashrate for BraiinsV2507 {
382    fn parse_expected_hashrate(&self, data: &HashMap<DataField, Value>) -> Option<HashRate> {
383        data.extract_map::<f64, _>(DataField::ExpectedHashrate, |f| HashRate {
384            value: f,
385            unit: HashRateUnit::GigaHash,
386            algo: String::from("SHA256"),
387        })
388    }
389}
390
391impl GetFans for BraiinsV2507 {
392    fn parse_fans(&self, data: &HashMap<DataField, Value>) -> Vec<FanData> {
393        let mut fans: Vec<FanData> = Vec::new();
394
395        if let Some(fans_data) = data.get(&DataField::Fans)
396            && let Some(fans_array) = fans_data.as_array()
397        {
398            for (idx, fan) in fans_array.iter().enumerate() {
399                if let Some(rpm) = fan.pointer("/rpm").and_then(|v| v.as_i64()) {
400                    let pos = fan
401                        .pointer("/position")
402                        .and_then(|v| v.as_i64())
403                        .unwrap_or(idx as i64);
404                    fans.push(FanData {
405                        position: pos as i16,
406                        rpm: Some(AngularVelocity::from_rpm(rpm as f64)),
407                    });
408                }
409            }
410        }
411
412        fans
413    }
414}
415
416impl GetLightFlashing for BraiinsV2507 {
417    fn parse_light_flashing(&self, data: &HashMap<DataField, Value>) -> Option<bool> {
418        data.extract::<bool>(DataField::LightFlashing)
419    }
420}
421
422impl GetUptime for BraiinsV2507 {
423    fn parse_uptime(&self, data: &HashMap<DataField, Value>) -> Option<Duration> {
424        data.extract_map::<u64, _>(DataField::Uptime, Duration::from_secs)
425    }
426}
427
428impl GetIsMining for BraiinsV2507 {
429    fn parse_is_mining(&self, data: &HashMap<DataField, Value>) -> bool {
430        // 1 -> Not Started
431        // 2 -> Normal
432        // 3 -> Paused
433        // 4 -> Suspended
434        // See: https://github.com/braiins/bos-plus-api/blob/ef28e752f80711c54d5587ec8f2cd838fdb34042/proto/bos/v1/miner.proto#L117-L124
435        data.extract::<u64>(DataField::IsMining) == Some(2)
436    }
437}
438
439impl GetPools for BraiinsV2507 {
440    fn parse_pools(&self, data: &HashMap<DataField, Value>) -> Vec<PoolData> {
441        let mut pools: Vec<PoolData> = Vec::new();
442
443        if let Some(pools_data) = data.get(&DataField::Pools)
444            && let Some(pools_array) = pools_data.as_array()
445        {
446            for (idx, pool) in pools_array.iter().enumerate() {
447                let url = pool
448                    .pointer("/url")
449                    .and_then(|v| v.as_str())
450                    .map(String::from)
451                    .map(PoolURL::from);
452
453                let user = pool
454                    .pointer("/user")
455                    .and_then(|v| v.as_str())
456                    .map(String::from);
457
458                let accepted_shares = pool
459                    .pointer("/stats/accepted_shares")
460                    .and_then(|v| v.as_u64());
461                let rejected_shares = pool
462                    .pointer("/stats/rejected_shares")
463                    .and_then(|v| v.as_u64());
464                let active = pool.pointer("/active").and_then(|v| v.as_bool());
465                let alive = pool.pointer("/alive").and_then(|v| v.as_bool());
466
467                pools.push(PoolData {
468                    position: Some(idx as u16),
469                    url,
470                    accepted_shares,
471                    rejected_shares,
472                    active,
473                    alive,
474                    user,
475                });
476            }
477        }
478
479        pools
480    }
481}
482
483impl GetSerialNumber for BraiinsV2507 {
484    fn parse_serial_number(&self, data: &HashMap<DataField, Value>) -> Option<String> {
485        data.extract::<String>(DataField::SerialNumber)
486    }
487}
488
489impl GetControlBoardVersion for BraiinsV2507 {
490    fn parse_control_board_version(
491        &self,
492        data: &HashMap<DataField, Value>,
493    ) -> Option<MinerControlBoard> {
494        let cb_type = data.extract::<u64>(DataField::ControlBoardVersion)?;
495        match cb_type {
496            0 => Some(MinerControlBoard::Unknown("".to_string())),
497            1 => Some(MinerControlBoard::CVITek),
498            2 => Some(MinerControlBoard::BeagleBoneBlack),
499            3 => Some(MinerControlBoard::AMLogic),
500            4 => Some(MinerControlBoard::Xilinx),
501            5 => Some(MinerControlBoard::BraiinsCB),
502            _ => Some(MinerControlBoard::Unknown("".to_string())),
503        }
504    }
505}
506
507impl GetWattage for BraiinsV2507 {
508    fn parse_wattage(&self, data: &HashMap<DataField, Value>) -> Option<Power> {
509        data.extract_map::<i64, _>(DataField::Wattage, |w| Power::from_watts(w as f64))
510    }
511}
512
513impl GetWattageLimit for BraiinsV2507 {
514    fn parse_wattage_limit(&self, data: &HashMap<DataField, Value>) -> Option<Power> {
515        data.extract_map::<i64, _>(DataField::WattageLimit, |w| Power::from_watts(w as f64))
516    }
517}
518
519impl GetFluidTemperature for BraiinsV2507 {}
520
521impl GetPsuFans for BraiinsV2507 {}
522
523impl GetMessages for BraiinsV2507 {
524    fn parse_messages(&self, data: &HashMap<DataField, Value>) -> Vec<MinerMessage> {
525        let mut messages: Vec<MinerMessage> = Vec::new();
526
527        if let Some(errors_data) = data.get(&DataField::Messages)
528            && let Some(errors_array) = errors_data.as_array()
529        {
530            for error in errors_array.iter() {
531                let timestamp = error
532                    .get("timestamp")
533                    .and_then(|v| v.as_str())
534                    .and_then(|dt| dt.parse::<DateTime<Utc>>().ok())
535                    .map(|dt| dt.timestamp_millis() as u32);
536                let message = error.get("message").and_then(|v| v.as_str());
537                if let Some(ts) = timestamp {
538                    messages.push(MinerMessage::new(
539                        ts,
540                        0, // They have codes, but they include a string
541                        message.unwrap_or("Unknown error").to_string(),
542                        MessageSeverity::Error,
543                    ))
544                }
545            }
546        };
547
548        messages
549    }
550}
551
552#[async_trait]
553impl SetFaultLight for BraiinsV2507 {
554    async fn set_fault_light(&self, fault: bool) -> anyhow::Result<bool> {
555        Ok(self
556            .web
557            .send_command("actions/locate", true, Some(json!(fault)), Method::PUT)
558            .await
559            .is_ok())
560    }
561}
562
563#[async_trait]
564impl SetPowerLimit for BraiinsV2507 {
565    async fn set_power_limit(&self, limit: Power) -> anyhow::Result<bool> {
566        Ok(self
567            .web
568            .send_command(
569                "performance/power-target",
570                true,
571                Some(json!({"watt": limit.as_watts() as u64})),
572                Method::PUT,
573            )
574            .await
575            .is_ok())
576    }
577}
578
579#[async_trait]
580impl Restart for BraiinsV2507 {
581    async fn restart(&self) -> anyhow::Result<bool> {
582        Ok(self
583            .web
584            .send_command("actions/reboot", true, None, Method::PUT)
585            .await
586            .is_ok())
587    }
588}
589
590#[async_trait]
591impl Pause for BraiinsV2507 {
592    #[allow(unused_variables)]
593    async fn pause(&self, at_time: Option<Duration>) -> anyhow::Result<bool> {
594        Ok(self
595            .web
596            .send_command("actions/pause", true, None, Method::PUT)
597            .await
598            .is_ok())
599    }
600}
601
602#[async_trait]
603impl Resume for BraiinsV2507 {
604    #[allow(unused_variables)]
605    async fn resume(&self, at_time: Option<Duration>) -> anyhow::Result<bool> {
606        Ok(self
607            .web
608            .send_command("actions/resume", true, None, Method::PUT)
609            .await
610            .is_ok())
611    }
612}