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"), 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, 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 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, 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}