diff --git a/asic-rs-core/src/data/capabilities.rs b/asic-rs-core/src/data/capabilities.rs new file mode 100644 index 00000000..5c7eaaa6 --- /dev/null +++ b/asic-rs-core/src/data/capabilities.rs @@ -0,0 +1,67 @@ +//! Tuning capability metadata reported by a miner's firmware. +//! +//! Where [`MinerData::tuning_target`](super::miner::MinerData::tuning_target) +//! describes the *current* tuning setpoint, these types describe the *envelope* +//! the firmware allows: the factory default plus the accepted range (for power +//! and hashrate tuning) or the set of selectable presets. Every field is +//! optional — a backend fills in only what its firmware actually exposes. + +#[cfg(feature = "python")] +use pyo3::pyclass; +use serde::{Deserialize, Serialize}; + +use super::miner::TuningTarget; + +/// Factory tuning envelope for power-target tuning. +#[cfg_attr(feature = "python", pyclass(from_py_object, module = "asic_rs"))] +#[cfg_attr(feature = "python", asic_rs_pydantic::py_pydantic_model(getters))] +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)] +pub struct PowerTuningCapabilities { + /// The factory default power target, i.e. the value tuning starts from. + pub default: Option, + /// The lowest power target the miner accepts. + pub minimum: Option, + /// The highest power target the miner accepts. + pub maximum: Option, +} + +/// Factory tuning envelope for hashrate-target tuning. +#[cfg_attr(feature = "python", pyclass(from_py_object, module = "asic_rs"))] +#[cfg_attr(feature = "python", asic_rs_pydantic::py_pydantic_model(getters))] +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)] +pub struct HashRateTuningCapabilities { + /// The factory default hashrate target, i.e. the value tuning starts from. + pub default: Option, + /// The lowest hashrate target the miner accepts. + pub minimum: Option, + /// The highest hashrate target the miner accepts. + pub maximum: Option, +} + +/// Factory tuning envelope for preset / mining-mode tuning. +#[cfg_attr(feature = "python", pyclass(from_py_object, module = "asic_rs"))] +#[cfg_attr(feature = "python", asic_rs_pydantic::py_pydantic_model(getters))] +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)] +pub struct PresetTuningCapabilities { + /// The factory default preset, when one is marked as default. + pub default: Option, + /// The presets the firmware offers to select between. + pub presets: Vec, +} + +/// The tuning envelopes a miner exposes, grouped by tuning domain. +/// +/// A backend populates only the domains its firmware supports; the rest stay +/// `None`. This keeps a single, extensible home for "what can this miner be +/// tuned to" instead of a flat list of per-aspect fields. +#[cfg_attr(feature = "python", pyclass(from_py_object, module = "asic_rs"))] +#[cfg_attr(feature = "python", asic_rs_pydantic::py_pydantic_model(getters))] +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)] +pub struct TuningCapabilities { + /// Power-target tuning envelope, when the firmware tunes by power. + pub power: Option, + /// Hashrate-target tuning envelope, when the firmware tunes by hashrate. + pub hashrate: Option, + /// Preset / mining-mode tuning envelope, when the firmware tunes by preset. + pub presets: Option, +} diff --git a/asic-rs-core/src/data/collector.rs b/asic-rs-core/src/data/collector.rs index 04655310..971b68ca 100644 --- a/asic-rs-core/src/data/collector.rs +++ b/asic-rs-core/src/data/collector.rs @@ -77,6 +77,10 @@ pub enum DataField { /// Configured tuning target (power or hashrate). #[cfg_attr(feature = "python", pydantic(value = "TuningTarget"))] TuningTarget, + /// Factory tuning envelope the firmware exposes (default/min/max power or + /// hashrate, or selectable presets). + #[cfg_attr(feature = "python", pydantic(value = "TuningCapabilities"))] + TuningCapabilities, /// Efficiency of the miner (e.g., J/TH). #[cfg_attr(feature = "python", pydantic(value = "Efficiency"))] Efficiency, diff --git a/asic-rs-core/src/data/miner.rs b/asic-rs-core/src/data/miner.rs index d86c6da1..9ee31953 100644 --- a/asic-rs-core/src/data/miner.rs +++ b/asic-rs-core/src/data/miner.rs @@ -8,6 +8,7 @@ use serde::{Deserialize, Serialize}; use super::{ board::{BoardData, MinerControlBoard}, + capabilities::TuningCapabilities, device::DeviceInfo, fan::FanData, hashrate::HashRate, @@ -118,6 +119,9 @@ pub struct MinerData { pub tuning_target: Option, /// The current tuning target adjusted by scaling settings, when available. pub scaled_tuning_target: Option, + /// The factory tuning envelope the firmware exposes (default / min / max + /// power and hashrate, or selectable presets), when reported. + pub tuning_capabilities: Option, /// The current efficiency in W/TH/s (J/TH) of the miner pub efficiency: Option, /// The state of the fault/alert light on the miner diff --git a/asic-rs-core/src/data/mod.rs b/asic-rs-core/src/data/mod.rs index 0d147914..889afc5e 100644 --- a/asic-rs-core/src/data/mod.rs +++ b/asic-rs-core/src/data/mod.rs @@ -3,6 +3,7 @@ //! The most important data type is [`MinerData`][`miner::MinerData`], it contains all the data asic-rs gathers with `get_data`. pub mod board; +pub mod capabilities; pub mod collector; pub mod command; pub(crate) mod deserialize; diff --git a/asic-rs-core/src/traits/miner.rs b/asic-rs-core/src/traits/miner.rs index 200aa177..7bdf4804 100644 --- a/asic-rs-core/src/traits/miner.rs +++ b/asic-rs-core/src/traits/miner.rs @@ -19,6 +19,7 @@ use crate::{ }, data::{ board::{BoardData, MinerControlBoard}, + capabilities::TuningCapabilities, collector::{DataCollector, DataField, DataLocation}, command::MinerCommand, device::DeviceInfo, @@ -130,6 +131,7 @@ pub trait GetMinerData: + GetWattage + GetTuningTarget + GetScaledTuningTarget + + GetTuningCapabilities + GetLightFlashing + GetMessages + GetUptime @@ -188,6 +190,7 @@ impl< + GetWattage + GetTuningTarget + GetScaledTuningTarget + + GetTuningCapabilities + GetLightFlashing + GetMessages + GetUptime @@ -224,6 +227,7 @@ impl< let wattage = self.parse_wattage(&data); let tuning_target = self.parse_tuning_target(&data); let scaled_tuning_target = self.parse_scaled_tuning_target(&data); + let tuning_capabilities = self.parse_tuning_capabilities(&data); let fluid_temperature = self.parse_fluid_temperature(&data); let outlet_fluid_temperature = self.parse_outlet_fluid_temperature(&data); let fans = self.parse_fans(&data); @@ -314,6 +318,7 @@ impl< wattage, tuning_target, scaled_tuning_target, + tuning_capabilities, efficiency, // Status information @@ -651,6 +656,25 @@ pub trait GetScaledTuningTarget: CollectData { } } +// Tuning Capabilities +#[async_trait] +pub trait GetTuningCapabilities: CollectData { + #[tracing::instrument(level = "debug")] + async fn get_tuning_capabilities(&self) -> Option { + let mut collector = self.get_collector(); + let data = collector.collect(&[DataField::TuningCapabilities]).await; + self.parse_tuning_capabilities(&data) + } + + #[allow(unused_variables)] + fn parse_tuning_capabilities( + &self, + data: &HashMap, + ) -> Option { + None + } +} + // Light Flashing #[async_trait] pub trait GetLightFlashing: CollectData { diff --git a/asic-rs-firmwares/antminer/src/backends/v2020/mod.rs b/asic-rs-firmwares/antminer/src/backends/v2020/mod.rs index 93c2d7b0..bbd0ec3e 100644 --- a/asic-rs-firmwares/antminer/src/backends/v2020/mod.rs +++ b/asic-rs-firmwares/antminer/src/backends/v2020/mod.rs @@ -889,6 +889,7 @@ impl GetFluidTemperature for AntMinerV2020 { } impl GetPsuFans for AntMinerV2020 {} +impl GetTuningCapabilities for AntMinerV2020 {} impl GetMessages for AntMinerV2020 { fn parse_messages(&self, data: &HashMap) -> Vec { diff --git a/asic-rs-firmwares/antminer/src/backends/v2023_07/mod.rs b/asic-rs-firmwares/antminer/src/backends/v2023_07/mod.rs index 3f663cab..b0e1e926 100644 --- a/asic-rs-firmwares/antminer/src/backends/v2023_07/mod.rs +++ b/asic-rs-firmwares/antminer/src/backends/v2023_07/mod.rs @@ -792,6 +792,7 @@ impl GetFluidTemperature for AntMinerV202307 { } impl GetPsuFans for AntMinerV202307 {} +impl GetTuningCapabilities for AntMinerV202307 {} impl GetMessages for AntMinerV202307 { fn parse_messages(&self, data: &HashMap) -> Vec { diff --git a/asic-rs-firmwares/auradine/src/backends/v1/mod.rs b/asic-rs-firmwares/auradine/src/backends/v1/mod.rs index a97a21a9..8dc1d2f7 100644 --- a/asic-rs-firmwares/auradine/src/backends/v1/mod.rs +++ b/asic-rs-firmwares/auradine/src/backends/v1/mod.rs @@ -1118,7 +1118,7 @@ impl GetTuningTarget for AuradineV1 { } impl GetScaledTuningTarget for AuradineV1 {} - +impl GetTuningCapabilities for AuradineV1 {} impl GetLightFlashing for AuradineV1 { fn parse_light_flashing(&self, data: &HashMap) -> Option { data.extract_map::(DataField::LightFlashing, |code| code == 3) diff --git a/asic-rs-firmwares/avalonminer/src/backends/avalon_a/mod.rs b/asic-rs-firmwares/avalonminer/src/backends/avalon_a/mod.rs index 2db3eb8f..d78c0f75 100644 --- a/asic-rs-firmwares/avalonminer/src/backends/avalon_a/mod.rs +++ b/asic-rs-firmwares/avalonminer/src/backends/avalon_a/mod.rs @@ -683,7 +683,7 @@ impl GetTuningTarget for AvalonAMiner { } impl GetScaledTuningTarget for AvalonAMiner {} - +impl GetTuningCapabilities for AvalonAMiner {} impl GetLightFlashing for AvalonAMiner { fn parse_light_flashing(&self, data: &HashMap) -> Option { data.extract::(DataField::LightFlashing) diff --git a/asic-rs-firmwares/avalonminer/src/backends/avalon_q/mod.rs b/asic-rs-firmwares/avalonminer/src/backends/avalon_q/mod.rs index d41c2d20..3548f3cd 100644 --- a/asic-rs-firmwares/avalonminer/src/backends/avalon_q/mod.rs +++ b/asic-rs-firmwares/avalonminer/src/backends/avalon_q/mod.rs @@ -627,7 +627,7 @@ impl GetTuningTarget for AvalonQMiner { } impl GetScaledTuningTarget for AvalonQMiner {} - +impl GetTuningCapabilities for AvalonQMiner {} impl GetLightFlashing for AvalonQMiner { fn parse_light_flashing(&self, data: &HashMap) -> Option { data.extract::(DataField::LightFlashing) diff --git a/asic-rs-firmwares/bitaxe/src/backends/v2_0_0/mod.rs b/asic-rs-firmwares/bitaxe/src/backends/v2_0_0/mod.rs index 032a2eb9..07cc0597 100644 --- a/asic-rs-firmwares/bitaxe/src/backends/v2_0_0/mod.rs +++ b/asic-rs-firmwares/bitaxe/src/backends/v2_0_0/mod.rs @@ -386,6 +386,7 @@ impl GetTuningTarget for Bitaxe200 { impl GetScaledTuningTarget for Bitaxe200 { // N/A } +impl GetTuningCapabilities for Bitaxe200 {} impl GetLightFlashing for Bitaxe200 { // N/A } diff --git a/asic-rs-firmwares/bitaxe/src/backends/v2_9_0/mod.rs b/asic-rs-firmwares/bitaxe/src/backends/v2_9_0/mod.rs index e229eca5..02502930 100644 --- a/asic-rs-firmwares/bitaxe/src/backends/v2_9_0/mod.rs +++ b/asic-rs-firmwares/bitaxe/src/backends/v2_9_0/mod.rs @@ -371,6 +371,7 @@ impl GetTuningTarget for Bitaxe290 { impl GetScaledTuningTarget for Bitaxe290 { // N/A } +impl GetTuningCapabilities for Bitaxe290 {} impl GetLightFlashing for Bitaxe290 { // N/A } diff --git a/asic-rs-firmwares/braiins/src/backends/util.rs b/asic-rs-firmwares/braiins/src/backends/util.rs index 2b6ee616..2a6619fa 100644 --- a/asic-rs-firmwares/braiins/src/backends/util.rs +++ b/asic-rs-firmwares/braiins/src/backends/util.rs @@ -1,8 +1,77 @@ +use asic_rs_core::data::capabilities::{ + HashRateTuningCapabilities, PowerTuningCapabilities, TuningCapabilities, +}; use asic_rs_core::data::hashrate::{HashRate, HashRateUnit}; use asic_rs_core::data::miner::TuningTarget; use measurements::Power; use serde_json::Value; +/// Build [`TuningCapabilities`] from a BOS GraphQL `powerTarget` metadata object +/// (`{ default, min, max }`, all in watts). Older GraphQL BraiinsOS backends only +/// expose the power-target envelope this way. +pub(crate) fn power_target_capabilities(power_target: &Value) -> TuningCapabilities { + let watts = |key: &str| { + power_target + .get(key) + .and_then(|v| v.as_i64()) + .map(|w| TuningTarget::from_watts(w as f64)) + }; + TuningCapabilities { + power: Some(PowerTuningCapabilities { + default: watts("default"), + minimum: watts("min"), + maximum: watts("max"), + }), + ..Default::default() + } +} + +/// Build [`TuningCapabilities`] from a BOS+ REST `tuner_constraints` object +/// (`power_target` as `PowerConstraints` in watts, `hashrate_target` as +/// `HashrateConstraints` in TH/s). Used by the REST BraiinsOS backends +/// (25.07 and newer). +pub(crate) fn tuner_constraints_capabilities(tuner: &Value) -> TuningCapabilities { + let power = tuner.get("power_target").map(|target| { + let watts = |key: &str| { + target + .pointer(&format!("/{key}/watt")) + .and_then(|v| v.as_i64()) + .map(|w| TuningTarget::from_watts(w as f64)) + }; + PowerTuningCapabilities { + default: watts("default"), + minimum: watts("min"), + maximum: watts("max"), + } + }); + + let hashrate = tuner.get("hashrate_target").map(|target| { + let ths = |key: &str| { + target + .pointer(&format!("/{key}/terahash_per_second")) + .and_then(|v| v.as_f64()) + .map(|value| { + TuningTarget::HashRate(HashRate { + value, + unit: HashRateUnit::TeraHash, + algo: "SHA256".to_string(), + }) + }) + }; + HashRateTuningCapabilities { + default: ths("default"), + minimum: ths("min"), + maximum: ths("max"), + } + }); + + TuningCapabilities { + power, + hashrate, + presets: None, + } +} + pub(crate) fn parse_configured_tuning_target(value: &Value) -> Option { parse_tagged_tuning_target(value, "configured") } diff --git a/asic-rs-firmwares/braiins/src/backends/v21_09/mod.rs b/asic-rs-firmwares/braiins/src/backends/v21_09/mod.rs index 3906596f..6625f696 100644 --- a/asic-rs-firmwares/braiins/src/backends/v21_09/mod.rs +++ b/asic-rs-firmwares/braiins/src/backends/v21_09/mod.rs @@ -8,6 +8,7 @@ use asic_rs_core::{ }, data::{ board::BoardData, + capabilities::TuningCapabilities, collector::{ DataCollector, DataExtensions, DataExtractor, DataField, DataLocation, get_by_pointer, }, @@ -27,7 +28,9 @@ use measurements::{AngularVelocity, Frequency, Power, Temperature, Voltage}; use serde_json::{Value, json}; use web::BraiinsWebAPI; -use crate::backends::util::{parse_configured_tuning_target, parse_scaled_tuning_target}; +use crate::backends::util::{ + parse_configured_tuning_target, parse_scaled_tuning_target, power_target_capabilities, +}; use crate::{ backends::v21_09::{graphql::BraiinsGraphQLAPI, rpc::BraiinsRPCAPI}, @@ -196,6 +199,21 @@ impl GetDataLocations for BraiinsV2109 { command: "admin/network/iface_status/lan", parameters: None, }; + const GQL_POWER_TARGET_META_QUERY: MinerCommand = MinerCommand::GraphQL { + command: r#"{ + bosminer { + metadata { + autotuning { + powerTarget { + default + min + max + } + } + } + } + }"#, + }; match data_field { DataField::ApiVersion => vec![( @@ -344,6 +362,14 @@ impl GetDataLocations for BraiinsV2109 { tag: None, }, )], + DataField::TuningCapabilities => vec![( + GQL_POWER_TARGET_META_QUERY, + DataExtractor { + func: get_by_pointer, + key: Some("/bosminer/metadata/autotuning/powerTarget"), + tag: None, + }, + )], _ => vec![], } } @@ -394,6 +420,15 @@ impl GetFirmwareVersion for BraiinsV2109 { } impl GetControlBoardVersion for BraiinsV2109 {} +impl GetTuningCapabilities for BraiinsV2109 { + fn parse_tuning_capabilities( + &self, + data: &HashMap, + ) -> Option { + let power_target = data.get(&DataField::TuningCapabilities)?; + Some(power_target_capabilities(power_target)) + } +} impl GetHashboards for BraiinsV2109 { fn parse_hashboards(&self, data: &HashMap) -> Vec { diff --git a/asic-rs-firmwares/braiins/src/backends/v25_03/mod.rs b/asic-rs-firmwares/braiins/src/backends/v25_03/mod.rs index b4bfb047..73007ba0 100644 --- a/asic-rs-firmwares/braiins/src/backends/v25_03/mod.rs +++ b/asic-rs-firmwares/braiins/src/backends/v25_03/mod.rs @@ -2,7 +2,9 @@ use std::{collections::HashMap, net::IpAddr, str::FromStr, time::Duration}; use crate::{ backends::{ - util::{parse_configured_tuning_target, parse_scaled_tuning_target}, + util::{ + parse_configured_tuning_target, parse_scaled_tuning_target, power_target_capabilities, + }, v21_09::{graphql::BraiinsGraphQLAPI, rpc::BraiinsRPCAPI, web::BraiinsWebAPI}, }, firmware::BraiinsFirmware, @@ -15,6 +17,7 @@ use asic_rs_core::{ }, data::{ board::BoardData, + capabilities::TuningCapabilities, collector::{ DataCollector, DataExtensions, DataExtractor, DataField, DataLocation, get_by_pointer, }, @@ -191,6 +194,21 @@ impl GetDataLocations for BraiinsV2503 { command: "admin/network/iface_status/lan", parameters: None, }; + const GQL_POWER_TARGET_META_QUERY: MinerCommand = MinerCommand::GraphQL { + command: r#"{ + bosminer { + metadata { + autotuning { + powerTarget { + default + min + max + } + } + } + } + }"#, + }; match data_field { DataField::ApiVersion => vec![( @@ -339,6 +357,14 @@ impl GetDataLocations for BraiinsV2503 { tag: None, }, )], + DataField::TuningCapabilities => vec![( + GQL_POWER_TARGET_META_QUERY, + DataExtractor { + func: get_by_pointer, + key: Some("/bosminer/metadata/autotuning/powerTarget"), + tag: None, + }, + )], _ => vec![], } } @@ -389,6 +415,15 @@ impl GetFirmwareVersion for BraiinsV2503 { } impl GetControlBoardVersion for BraiinsV2503 {} +impl GetTuningCapabilities for BraiinsV2503 { + fn parse_tuning_capabilities( + &self, + data: &HashMap, + ) -> Option { + let power_target = data.get(&DataField::TuningCapabilities)?; + Some(power_target_capabilities(power_target)) + } +} impl GetHashboards for BraiinsV2503 { fn parse_hashboards(&self, data: &HashMap) -> Vec { diff --git a/asic-rs-firmwares/braiins/src/backends/v25_05/mod.rs b/asic-rs-firmwares/braiins/src/backends/v25_05/mod.rs index 5fc487c1..5a42550d 100644 --- a/asic-rs-firmwares/braiins/src/backends/v25_05/mod.rs +++ b/asic-rs-firmwares/braiins/src/backends/v25_05/mod.rs @@ -8,6 +8,7 @@ use asic_rs_core::{ }, data::{ board::BoardData, + capabilities::TuningCapabilities, collector::{ DataCollector, DataExtensions, DataExtractor, DataField, DataLocation, get_by_pointer, }, @@ -28,7 +29,9 @@ use serde_json::{Value, json}; use crate::{ backends::{ - util::{parse_configured_tuning_target, parse_scaled_tuning_target}, + util::{ + parse_configured_tuning_target, parse_scaled_tuning_target, power_target_capabilities, + }, v21_09::{graphql::BraiinsGraphQLAPI, rpc::BraiinsRPCAPI, web::BraiinsWebAPI}, }, firmware::BraiinsFirmware, @@ -192,6 +195,21 @@ impl GetDataLocations for BraiinsV2505 { command: "admin/network/iface_status/lan", parameters: None, }; + const GQL_POWER_TARGET_META_QUERY: MinerCommand = MinerCommand::GraphQL { + command: r#"{ + bosminer { + metadata { + autotuning { + powerTarget { + default + min + max + } + } + } + } + }"#, + }; match data_field { DataField::ApiVersion => vec![( @@ -340,6 +358,14 @@ impl GetDataLocations for BraiinsV2505 { tag: None, }, )], + DataField::TuningCapabilities => vec![( + GQL_POWER_TARGET_META_QUERY, + DataExtractor { + func: get_by_pointer, + key: Some("/bosminer/metadata/autotuning/powerTarget"), + tag: None, + }, + )], _ => vec![], } } @@ -390,6 +416,15 @@ impl GetFirmwareVersion for BraiinsV2505 { } impl GetControlBoardVersion for BraiinsV2505 {} +impl GetTuningCapabilities for BraiinsV2505 { + fn parse_tuning_capabilities( + &self, + data: &HashMap, + ) -> Option { + let power_target = data.get(&DataField::TuningCapabilities)?; + Some(power_target_capabilities(power_target)) + } +} impl GetHashboards for BraiinsV2505 { fn parse_hashboards(&self, data: &HashMap) -> Vec { diff --git a/asic-rs-firmwares/braiins/src/backends/v25_07/mod.rs b/asic-rs-firmwares/braiins/src/backends/v25_07/mod.rs index d4e04c9b..7a374ef5 100644 --- a/asic-rs-firmwares/braiins/src/backends/v25_07/mod.rs +++ b/asic-rs-firmwares/braiins/src/backends/v25_07/mod.rs @@ -8,6 +8,7 @@ use asic_rs_core::{ }, data::{ board::{BoardData, MinerControlBoard}, + capabilities::TuningCapabilities, collector::{ DataCollector, DataExtensions, DataExtractor, DataField, DataLocation, get_by_pointer, }, @@ -32,7 +33,10 @@ use web::BraiinsWebAPI; use crate::{ backends::{ - util::{parse_configured_tuning_target, parse_scaled_tuning_target}, + util::{ + parse_configured_tuning_target, parse_scaled_tuning_target, + tuner_constraints_capabilities, + }, v21_09::graphql::BraiinsGraphQLAPI, }, firmware::BraiinsFirmware, @@ -138,6 +142,10 @@ impl GetDataLocations for BraiinsV2507 { } }"#, }; + const WEB_CONSTRAINTS: MinerCommand = MinerCommand::WebAPI { + command: "configuration/constraints", + parameters: None, + }; match data_field { DataField::Mac => vec![( @@ -312,6 +320,14 @@ impl GetDataLocations for BraiinsV2507 { tag: None, }, )], + DataField::TuningCapabilities => vec![( + WEB_CONSTRAINTS, + DataExtractor { + func: get_by_pointer, + key: Some("/tuner_constraints"), + tag: None, + }, + )], _ => vec![], } } @@ -610,6 +626,15 @@ impl GetScaledTuningTarget for BraiinsV2507 { } impl GetFluidTemperature for BraiinsV2507 {} +impl GetTuningCapabilities for BraiinsV2507 { + fn parse_tuning_capabilities( + &self, + data: &HashMap, + ) -> Option { + let tuner = data.get(&DataField::TuningCapabilities)?; + Some(tuner_constraints_capabilities(tuner)) + } +} impl GetPsuFans for BraiinsV2507 {} diff --git a/asic-rs-firmwares/braiins/src/backends/v26_04/mod.rs b/asic-rs-firmwares/braiins/src/backends/v26_04/mod.rs index b3eff350..b4b5ce60 100644 --- a/asic-rs-firmwares/braiins/src/backends/v26_04/mod.rs +++ b/asic-rs-firmwares/braiins/src/backends/v26_04/mod.rs @@ -8,6 +8,7 @@ use asic_rs_core::{ }, data::{ board::{BoardData, MinerControlBoard}, + capabilities::TuningCapabilities, collector::{ DataCollector, DataExtensions, DataExtractor, DataField, DataLocation, get_by_pointer, }, @@ -33,7 +34,10 @@ use web::BraiinsWebAPI; use crate::{ backends::{ - util::{parse_configured_tuning_target, parse_scaled_tuning_target}, + util::{ + parse_configured_tuning_target, parse_scaled_tuning_target, + tuner_constraints_capabilities, + }, v21_09::graphql::BraiinsGraphQLAPI, }, firmware::BraiinsFirmware, @@ -139,6 +143,10 @@ impl GetDataLocations for BraiinsV2604 { } }"#, }; + const WEB_CONSTRAINTS: MinerCommand = MinerCommand::WebAPI { + command: "configuration/constraints", + parameters: None, + }; match data_field { DataField::Mac => vec![( @@ -313,6 +321,14 @@ impl GetDataLocations for BraiinsV2604 { tag: None, }, )], + DataField::TuningCapabilities => vec![( + WEB_CONSTRAINTS, + DataExtractor { + func: get_by_pointer, + key: Some("/tuner_constraints"), + tag: None, + }, + )], _ => vec![], } } @@ -606,6 +622,17 @@ impl GetScaledTuningTarget for BraiinsV2604 { } impl GetFluidTemperature for BraiinsV2604 {} +impl GetTuningCapabilities for BraiinsV2604 { + fn parse_tuning_capabilities( + &self, + data: &HashMap, + ) -> Option { + // BOS+ REST `/configuration/constraints` → `tuner_constraints`, with + // power_target (watts) and hashrate_target (TH/s) envelopes. + let tuner = data.get(&DataField::TuningCapabilities)?; + Some(tuner_constraints_capabilities(tuner)) + } +} impl GetPsuFans for BraiinsV2604 {} diff --git a/asic-rs-firmwares/epic/src/backends/v1/mod.rs b/asic-rs-firmwares/epic/src/backends/v1/mod.rs index bfff0283..d7a11409 100644 --- a/asic-rs-firmwares/epic/src/backends/v1/mod.rs +++ b/asic-rs-firmwares/epic/src/backends/v1/mod.rs @@ -835,6 +835,7 @@ impl GetFans for PowerPlayV1 { } impl GetPsuFans for PowerPlayV1 {} +impl GetTuningCapabilities for PowerPlayV1 {} impl GetFluidTemperature for PowerPlayV1 {} diff --git a/asic-rs-firmwares/futurebit/src/backends/v2/mod.rs b/asic-rs-firmwares/futurebit/src/backends/v2/mod.rs index 66f0f59f..fb02b898 100644 --- a/asic-rs-firmwares/futurebit/src/backends/v2/mod.rs +++ b/asic-rs-firmwares/futurebit/src/backends/v2/mod.rs @@ -600,7 +600,7 @@ impl GetWattage for ApolloV2 { impl GetTuningTarget for ApolloV2 {} impl GetScaledTuningTarget for ApolloV2 {} - +impl GetTuningCapabilities for ApolloV2 {} impl GetLightFlashing for ApolloV2 {} impl GetMessages for ApolloV2 {} diff --git a/asic-rs-firmwares/luxminer/src/backends/v1/mod.rs b/asic-rs-firmwares/luxminer/src/backends/v1/mod.rs index d7637298..e439234e 100644 --- a/asic-rs-firmwares/luxminer/src/backends/v1/mod.rs +++ b/asic-rs-firmwares/luxminer/src/backends/v1/mod.rs @@ -984,7 +984,7 @@ impl GetTuningTarget for LuxMinerV1 { } impl GetScaledTuningTarget for LuxMinerV1 {} - +impl GetTuningCapabilities for LuxMinerV1 {} impl GetPsuFans for LuxMinerV1 {} impl GetMessages for LuxMinerV1 { diff --git a/asic-rs-firmwares/marathon/src/backends/v1/mod.rs b/asic-rs-firmwares/marathon/src/backends/v1/mod.rs index ef2ec29f..10c236e4 100644 --- a/asic-rs-firmwares/marathon/src/backends/v1/mod.rs +++ b/asic-rs-firmwares/marathon/src/backends/v1/mod.rs @@ -884,7 +884,7 @@ impl GetTuningTarget for MaraV1 { } impl GetScaledTuningTarget for MaraV1 {} - +impl GetTuningCapabilities for MaraV1 {} impl GetLightFlashing for MaraV1 { fn parse_light_flashing(&self, data: &HashMap) -> Option { data.extract::(DataField::LightFlashing) diff --git a/asic-rs-firmwares/nerdaxe/src/backends/v1/mod.rs b/asic-rs-firmwares/nerdaxe/src/backends/v1/mod.rs index ee02b28e..df60e9d2 100644 --- a/asic-rs-firmwares/nerdaxe/src/backends/v1/mod.rs +++ b/asic-rs-firmwares/nerdaxe/src/backends/v1/mod.rs @@ -375,6 +375,7 @@ impl GetWattage for NerdAxeV1 { } impl GetTuningTarget for NerdAxeV1 {} impl GetScaledTuningTarget for NerdAxeV1 {} +impl GetTuningCapabilities for NerdAxeV1 {} impl GetLightFlashing for NerdAxeV1 {} impl GetMessages for NerdAxeV1 { fn parse_messages(&self, data: &HashMap) -> Vec { diff --git a/asic-rs-firmwares/proto/src/backends/v1/mod.rs b/asic-rs-firmwares/proto/src/backends/v1/mod.rs index 814ba6ee..5fc1af1e 100644 --- a/asic-rs-firmwares/proto/src/backends/v1/mod.rs +++ b/asic-rs-firmwares/proto/src/backends/v1/mod.rs @@ -623,7 +623,7 @@ impl GetTuningTarget for ProtoV1 { } impl GetScaledTuningTarget for ProtoV1 {} - +impl GetTuningCapabilities for ProtoV1 {} // No endpoint reports locate-LED state, so it's set-only. impl GetLightFlashing for ProtoV1 {} diff --git a/asic-rs-firmwares/sealminer/src/backends/v2025/mod.rs b/asic-rs-firmwares/sealminer/src/backends/v2025/mod.rs index 13ea0323..7a38961b 100644 --- a/asic-rs-firmwares/sealminer/src/backends/v2025/mod.rs +++ b/asic-rs-firmwares/sealminer/src/backends/v2025/mod.rs @@ -553,7 +553,7 @@ impl GetTuningTarget for SealMinerV2025 { } impl GetScaledTuningTarget for SealMinerV2025 {} - +impl GetTuningCapabilities for SealMinerV2025 {} impl GetLightFlashing for SealMinerV2025 { fn parse_light_flashing(&self, data: &HashMap) -> Option { data.extract::(DataField::LightFlashing) diff --git a/asic-rs-firmwares/vnish/src/backends/v1_2_0/mod.rs b/asic-rs-firmwares/vnish/src/backends/v1_2_0/mod.rs index 1b914e1e..10397e8e 100644 --- a/asic-rs-firmwares/vnish/src/backends/v1_2_0/mod.rs +++ b/asic-rs-firmwares/vnish/src/backends/v1_2_0/mod.rs @@ -592,7 +592,7 @@ impl GetWattage for VnishV120 { impl GetTuningTarget for VnishV120 {} impl GetScaledTuningTarget for VnishV120 {} - +impl GetTuningCapabilities for VnishV120 {} impl GetLightFlashing for VnishV120 { fn parse_light_flashing(&self, data: &HashMap) -> Option { data.extract::(DataField::LightFlashing) diff --git a/asic-rs-firmwares/whatsminer/src/backends/v1/mod.rs b/asic-rs-firmwares/whatsminer/src/backends/v1/mod.rs index 3be418c2..c84f29c2 100644 --- a/asic-rs-firmwares/whatsminer/src/backends/v1/mod.rs +++ b/asic-rs-firmwares/whatsminer/src/backends/v1/mod.rs @@ -435,6 +435,7 @@ impl GetTuningTarget for WhatsMinerV1 { } } impl GetScaledTuningTarget for WhatsMinerV1 {} +impl GetTuningCapabilities for WhatsMinerV1 {} impl GetLightFlashing for WhatsMinerV1 {} impl GetMessages for WhatsMinerV1 { fn parse_messages(&self, data: &HashMap) -> Vec { diff --git a/asic-rs-firmwares/whatsminer/src/backends/v2/mod.rs b/asic-rs-firmwares/whatsminer/src/backends/v2/mod.rs index f63911c1..2aced011 100644 --- a/asic-rs-firmwares/whatsminer/src/backends/v2/mod.rs +++ b/asic-rs-firmwares/whatsminer/src/backends/v2/mod.rs @@ -499,6 +499,7 @@ impl GetTuningTarget for WhatsMinerV2 { } } impl GetScaledTuningTarget for WhatsMinerV2 {} +impl GetTuningCapabilities for WhatsMinerV2 {} impl GetLightFlashing for WhatsMinerV2 { fn parse_light_flashing(&self, data: &HashMap) -> Option { data.extract_map::(DataField::LightFlashing, |l| l != "auto") diff --git a/asic-rs-firmwares/whatsminer/src/backends/v3/mod.rs b/asic-rs-firmwares/whatsminer/src/backends/v3/mod.rs index 02f4ad05..db496e9d 100644 --- a/asic-rs-firmwares/whatsminer/src/backends/v3/mod.rs +++ b/asic-rs-firmwares/whatsminer/src/backends/v3/mod.rs @@ -534,6 +534,7 @@ impl GetTuningTarget for WhatsMinerV3 { } } impl GetScaledTuningTarget for WhatsMinerV3 {} +impl GetTuningCapabilities for WhatsMinerV3 {} impl GetLightFlashing for WhatsMinerV3 { fn parse_light_flashing(&self, data: &HashMap) -> Option { data.extract_map::(DataField::LightFlashing, |l| l != "auto") diff --git a/python/pyasic_rs/asic_rs.pyi b/python/pyasic_rs/asic_rs.pyi index e9bfb588..c9308a7b 100644 --- a/python/pyasic_rs/asic_rs.pyi +++ b/python/pyasic_rs/asic_rs.pyi @@ -102,6 +102,7 @@ class DataField: SchemaVersion: Final[DataField] SerialNumber: Final[DataField] Timestamp: Final[DataField] + TuningCapabilities: Final[DataField] TuningTarget: Final[DataField] Uptime: Final[DataField] Wattage: Final[DataField] @@ -550,6 +551,8 @@ class MinerData: @property def total_chips(self, /) -> int |None: ... @property + def tuning_capabilities(self, /) -> TuningCapabilities |None: ... + @property def tuning_target(self, /) -> TuningTarget |None: ... @property def uptime(self, /) -> timedelta |None: ... @@ -839,6 +842,88 @@ class TuningConfig: @property def variant(self, /) -> str: ... +@final +class PowerTuningCapabilities: + @classmethod + def __get_pydantic_core_schema__(cls, /, _source_type: "object", _handler: "object") -> "object": ... + def __repr__(self, /) -> str: ... + @staticmethod + def _pydantic_serialize(value: "PowerTuningCapabilities") -> "dict[str, object]": ... + @classmethod + def _pydantic_validate(cls, /, value: "object") -> "PowerTuningCapabilities": ... + @property + def default(self, /) -> TuningTarget |None: ... + @property + def maximum(self, /) -> TuningTarget |None: ... + @property + def minimum(self, /) -> TuningTarget |None: ... + def model_dump(self, /, **_kwargs: "object") -> "dict[str, object]": ... + @classmethod + def model_json_schema(cls, /, **kwargs: "object") -> "dict[str, object]": ... + @classmethod + def model_validate(cls, /, obj: "object", **_kwargs: "object") -> "PowerTuningCapabilities": ... + +@final +class HashRateTuningCapabilities: + @classmethod + def __get_pydantic_core_schema__(cls, /, _source_type: "object", _handler: "object") -> "object": ... + def __repr__(self, /) -> str: ... + @staticmethod + def _pydantic_serialize(value: "HashRateTuningCapabilities") -> "dict[str, object]": ... + @classmethod + def _pydantic_validate(cls, /, value: "object") -> "HashRateTuningCapabilities": ... + @property + def default(self, /) -> TuningTarget |None: ... + @property + def maximum(self, /) -> TuningTarget |None: ... + @property + def minimum(self, /) -> TuningTarget |None: ... + def model_dump(self, /, **_kwargs: "object") -> "dict[str, object]": ... + @classmethod + def model_json_schema(cls, /, **kwargs: "object") -> "dict[str, object]": ... + @classmethod + def model_validate(cls, /, obj: "object", **_kwargs: "object") -> "HashRateTuningCapabilities": ... + +@final +class PresetTuningCapabilities: + @classmethod + def __get_pydantic_core_schema__(cls, /, _source_type: "object", _handler: "object") -> "object": ... + def __repr__(self, /) -> str: ... + @staticmethod + def _pydantic_serialize(value: "PresetTuningCapabilities") -> "dict[str, object]": ... + @classmethod + def _pydantic_validate(cls, /, value: "object") -> "PresetTuningCapabilities": ... + @property + def default(self, /) -> TuningTarget |None: ... + @property + def presets(self, /) -> list[TuningTarget]: ... + def model_dump(self, /, **_kwargs: "object") -> "dict[str, object]": ... + @classmethod + def model_json_schema(cls, /, **kwargs: "object") -> "dict[str, object]": ... + @classmethod + def model_validate(cls, /, obj: "object", **_kwargs: "object") -> "PresetTuningCapabilities": ... + +@final +class TuningCapabilities: + @classmethod + def __get_pydantic_core_schema__(cls, /, _source_type: "object", _handler: "object") -> "object": ... + def __repr__(self, /) -> str: ... + @staticmethod + def _pydantic_serialize(value: "TuningCapabilities") -> "dict[str, object]": ... + @classmethod + def _pydantic_validate(cls, /, value: "object") -> "TuningCapabilities": ... + @property + def hashrate(self, /) -> HashRateTuningCapabilities |None: ... + def model_dump(self, /, **_kwargs: "object") -> "dict[str, object]": ... + @classmethod + def model_json_schema(cls, /, **kwargs: "object") -> "dict[str, object]": ... + @classmethod + def model_validate(cls, /, obj: "object", **_kwargs: "object") -> "TuningCapabilities": ... + @property + def power(self, /) -> PowerTuningCapabilities |None: ... + @property + def presets(self, /) -> PresetTuningCapabilities |None: ... + class TuningTarget: def __repr__(self, /) -> str: ... def __str__(self, /) -> str: ... diff --git a/python/pyasic_rs/data.py b/python/pyasic_rs/data.py index 8dd008ef..76b3687c 100644 --- a/python/pyasic_rs/data.py +++ b/python/pyasic_rs/data.py @@ -6,6 +6,12 @@ """ from pyasic_rs.asic_rs import BoardData, ChipData +from pyasic_rs.asic_rs import ( + HashRateTuningCapabilities, + PowerTuningCapabilities, + PresetTuningCapabilities, + TuningCapabilities, +) from pyasic_rs.asic_rs import DeviceInfo, MinerHardware from pyasic_rs.asic_rs import FanData from pyasic_rs.asic_rs import HashRate, HashRateUnit @@ -20,6 +26,10 @@ "BoardData", "ChipData", "DeviceInfo", + "HashRateTuningCapabilities", + "PowerTuningCapabilities", + "PresetTuningCapabilities", + "TuningCapabilities", "FanData", "HashRate", "HashRateUnit", diff --git a/python/tests/test_pydantic_interop.py b/python/tests/test_pydantic_interop.py index 9265be90..3bc678eb 100644 --- a/python/tests/test_pydantic_interop.py +++ b/python/tests/test_pydantic_interop.py @@ -119,6 +119,7 @@ def minimal_miner_data(**overrides: object) -> dict[str, object]: "wattage": None, "tuning_target": None, "scaled_tuning_target": None, + "tuning_capabilities": None, "efficiency": None, "light_flashing": None, "messages": [], diff --git a/src/python/mod.rs b/src/python/mod.rs index 20d69031..6a6e40e6 100644 --- a/src/python/mod.rs +++ b/src/python/mod.rs @@ -37,6 +37,10 @@ mod asic_rs { #[pymodule_export] use asic_rs_core::data::{ board::{BoardData, ChipData, MinerControlBoard}, + capabilities::{ + HashRateTuningCapabilities, PowerTuningCapabilities, PresetTuningCapabilities, + TuningCapabilities, + }, device::{DeviceInfo, MinerHardware}, fan::FanData, message::{MessageSeverity, MinerComponent, MinerMessage},