diff --git a/asic-rs-core/src/config/mod.rs b/asic-rs-core/src/config/mod.rs index 3a84beaa..5d8c6d08 100644 --- a/asic-rs-core/src/config/mod.rs +++ b/asic-rs-core/src/config/mod.rs @@ -1,6 +1,7 @@ pub mod collector; pub mod fan; pub mod pools; +pub mod preset; pub mod scaling; pub mod temperature; pub mod tuning; diff --git a/asic-rs-core/src/config/preset.rs b/asic-rs-core/src/config/preset.rs new file mode 100644 index 00000000..5083ec3c --- /dev/null +++ b/asic-rs-core/src/config/preset.rs @@ -0,0 +1,38 @@ +#[cfg(feature = "python")] +use pyo3::prelude::*; +use serde::{Deserialize, Serialize}; + +/// An available autotune/overclock preset reported by the firmware. +/// +/// Read-only: produced by the library (e.g. `Miner.get_presets`), never taken +/// as input from Python, so it is a plain `pyclass` rather than a pydantic model. +#[cfg_attr( + feature = "python", + pyclass( + name = "PresetInfo", + frozen, + get_all, + skip_from_py_object, + module = "asic_rs" + ) +)] +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct PresetInfo { + /// Canonical preset name the firmware expects (e.g. `"5560"`). + pub name: String, + /// Human-readable description (e.g. `"5560 watt ~ 175 TH"`), if provided. + pub pretty: Option, + /// Tuning status (e.g. `"tuned"` / `"untuned"`), if provided. + pub status: Option, +} + +#[cfg(feature = "python")] +#[pymethods] +impl PresetInfo { + fn __repr__(&self) -> String { + format!( + "PresetInfo(name={:?}, pretty={:?}, status={:?})", + self.name, self.pretty, self.status + ) + } +} diff --git a/asic-rs-core/src/config/tuning.rs b/asic-rs-core/src/config/tuning.rs index a36799e1..fb4d37c1 100644 --- a/asic-rs-core/src/config/tuning.rs +++ b/asic-rs-core/src/config/tuning.rs @@ -41,6 +41,7 @@ impl TuningConfig { TuningTarget::Power(_) => "power", TuningTarget::HashRate(_) => "hashrate", TuningTarget::MiningMode(_) => "mode", + TuningTarget::Preset(_) => "preset", } } @@ -68,6 +69,14 @@ impl TuningConfig { } } + /// Target preset name, or `None` if targeting power, hashrate, or mining mode. + pub fn target_preset(&self) -> Option<&str> { + match &self.target { + TuningTarget::Preset(name) => Some(name), + _ => None, + } + } + pub fn algorithm(&self) -> Option<&str> { self.algorithm.as_deref() } @@ -109,6 +118,11 @@ impl TuningConfig { Self::new(TuningTarget::MiningMode(mode)) } + #[classmethod] + fn preset(_cls: &Bound<'_, pyo3::types::PyType>, name: String) -> Self { + Self::new(TuningTarget::Preset(name)) + } + #[getter] #[pyo3(name = "variant")] fn py_variant(&self) -> &'static str { @@ -136,6 +150,13 @@ impl TuningConfig { self.target_mode() } + /// Target preset name, or `None` if targeting power, hashrate, or mining mode. + #[getter] + #[pyo3(name = "target_preset")] + fn py_target_preset(&self) -> Option { + self.target_preset().map(str::to_owned) + } + #[getter] #[pyo3(name = "algorithm")] fn py_algorithm(&self) -> Option<&str> { @@ -200,9 +221,13 @@ mod python_impls { })?; TuningTarget::MiningMode(mode) } + "preset" => { + let name: String = get_required_field(&obj, "target_preset")?.extract()?; + TuningTarget::Preset(name) + } _ => { return Err(PyErr::new::(format!( - "Unknown TuningConfig variant '{variant}', expected 'power', 'hashrate', or 'mode'", + "Unknown TuningConfig variant '{variant}', expected 'power', 'hashrate', 'mode', or 'preset'", ))); } }; diff --git a/asic-rs-core/src/data/miner.rs b/asic-rs-core/src/data/miner.rs index 1abea3a9..d41a92bc 100644 --- a/asic-rs-core/src/data/miner.rs +++ b/asic-rs-core/src/data/miner.rs @@ -29,6 +29,8 @@ pub enum TuningTarget { HashRate(HashRate), /// Target a named mining mode. MiningMode(MiningMode), + /// Target a named firmware preset (e.g. VNish autotune presets). + Preset(String), } impl TuningTarget { @@ -158,6 +160,7 @@ mod python_tuning_target { Power { watts: f64 }, HashRate { target_hashrate: HashRate }, Mode { target_mode: MiningMode }, + Preset { name: String }, } #[pymethods] @@ -179,12 +182,18 @@ mod python_tuning_target { Self::Mode { target_mode: mode } } + #[staticmethod] + fn preset(name: String) -> Self { + Self::Preset { name } + } + #[getter] fn variant(&self) -> &'static str { match self { Self::Power { .. } => "power", Self::HashRate { .. } => "hashrate", Self::Mode { .. } => "mode", + Self::Preset { .. } => "preset", } } @@ -214,6 +223,14 @@ mod python_tuning_target { } } + #[getter] + fn preset_name(&self) -> Option { + match self { + Self::Preset { name } => Some(name.clone()), + _ => None, + } + } + fn __repr__(&self) -> String { match self { Self::Power { watts } => format!("TuningTarget.power(watts={watts:?})"), @@ -221,6 +238,7 @@ mod python_tuning_target { format!("TuningTarget.hashrate(hashrate={target_hashrate})") } Self::Mode { target_mode } => format!("TuningTarget.mode(mode={target_mode})"), + Self::Preset { name } => format!("TuningTarget.preset(name={name:?})"), } } @@ -239,6 +257,7 @@ mod python_tuning_target { target_hashrate: hashrate, }, TuningTarget::MiningMode(mode) => Self::Mode { target_mode: mode }, + TuningTarget::Preset(name) => Self::Preset { name }, } } } @@ -251,6 +270,7 @@ mod python_tuning_target { TuningTarget::HashRate(target_hashrate) } PyTuningTarget::Mode { target_mode } => TuningTarget::MiningMode(target_mode), + PyTuningTarget::Preset { name } => TuningTarget::Preset(name), } } } @@ -287,12 +307,17 @@ mod python_tuning_target { "type" => required(literal_schema(core_schema, &["mode"])?), "value" => required(::pydantic_schema(core_schema, mode)?), })?; + let preset_schema = pydantic_typed_dict_schema!(core_schema, "asic_rs.TuningTargetPreset", { + "type" => required(literal_schema(core_schema, &["preset"])?), + "value" => required(::pydantic_schema(core_schema, mode)?), + })?; let tagged_union = tagged_union_schema( core_schema, [ ("power", power_schema), ("hashrate", hashrate_schema), ("mode", mode_schema), + ("preset", preset_schema), ], "type", Some("asic_rs.TuningTarget"), @@ -323,8 +348,11 @@ mod python_tuning_target { "mode" => Ok(TuningTarget::MiningMode( ::from_pydantic(&v)?, )), + "preset" => Ok(TuningTarget::Preset( + ::from_pydantic(&v)?, + )), _ => Err(PyValueError::new_err(format!( - "Unknown TuningTarget type '{type_str}', expected 'power', 'hashrate', or 'mode'" + "Unknown TuningTarget type '{type_str}', expected 'power', 'hashrate', 'mode', or 'preset'" ))), } } @@ -351,6 +379,13 @@ mod python_tuning_target { ::to_pydantic_data(m, py)?, )?; } + TuningTarget::Preset(name) => { + dict.set_item("type", "preset")?; + dict.set_item( + "value", + ::to_pydantic_data(name, py)?, + )?; + } } Ok(dict.into_any().unbind()) } diff --git a/asic-rs-core/src/traits/miner.rs b/asic-rs-core/src/traits/miner.rs index 5cf340a0..beb0a7ac 100644 --- a/asic-rs-core/src/traits/miner.rs +++ b/asic-rs-core/src/traits/miner.rs @@ -14,6 +14,7 @@ use crate::{ collector::{ConfigCollector, ConfigField, ConfigLocation}, fan::FanConfig, pools::PoolGroupConfig, + preset::PresetInfo, scaling::ScalingConfig, temperature::TemperatureConfig, tuning::TuningConfig, @@ -43,12 +44,24 @@ pub trait MinerConstructor { } pub trait Miner: - GetMinerData + HasMinerControl + SupportsConfigs + UpgradeFirmware + HasAuth + HasDefaultAuth + GetMinerData + + HasMinerControl + + SupportsConfigs + + SupportsPresets + + UpgradeFirmware + + HasAuth + + HasDefaultAuth { } impl< - T: GetMinerData + HasMinerControl + SupportsConfigs + UpgradeFirmware + HasAuth + HasDefaultAuth, + T: GetMinerData + + HasMinerControl + + SupportsConfigs + + SupportsPresets + + UpgradeFirmware + + HasAuth + + HasDefaultAuth, > Miner for T { } @@ -813,6 +826,22 @@ pub trait SetTuningPercent { } } +#[async_trait] +pub trait SupportsPresets { + /// List the firmware's available autotune/overclock presets. + /// + /// Selecting a preset is done through [`SupportsTuningConfig::set_tuning_config`] + /// with a [`TuningTarget::Preset`], and the active preset is surfaced via + /// [`GetTuningTarget::get_tuning_target`]. + async fn get_presets(&self) -> Vec { + Vec::new() + } + /// Defaults to `false`; backends with named presets override this. + fn supports_presets(&self) -> bool { + false + } +} + #[async_trait] pub trait Restart { async fn restart(&self) -> anyhow::Result { diff --git a/asic-rs-firmwares/antminer/src/backends/v2020/mod.rs b/asic-rs-firmwares/antminer/src/backends/v2020/mod.rs index 1f6006da..f6e7eca0 100644 --- a/asic-rs-firmwares/antminer/src/backends/v2020/mod.rs +++ b/asic-rs-firmwares/antminer/src/backends/v2020/mod.rs @@ -1175,6 +1175,9 @@ impl SupportsTuningConfig for AntMinerV2020 { TuningTarget::HashRate(_) => { anyhow::bail!("Hashrate tuning target is not supported on Antminer stock firmware") } + TuningTarget::Preset(_) => { + anyhow::bail!("Preset tuning target is not supported on Antminer stock firmware") + } }; let pre = self.web.get_miner_conf().await?; @@ -1241,6 +1244,8 @@ impl SupportsTemperatureConfig for AntMinerV2020 {} impl GetTuningPercent for AntMinerV2020 {} impl SetTuningPercent for AntMinerV2020 {} +impl SupportsPresets for AntMinerV2020 {} + #[cfg(test)] mod tests { use std::sync::Arc; 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 89a3e46e..b6b9a1fa 100644 --- a/asic-rs-firmwares/antminer/src/backends/v2023_07/mod.rs +++ b/asic-rs-firmwares/antminer/src/backends/v2023_07/mod.rs @@ -1102,6 +1102,9 @@ impl SupportsTuningConfig for AntMinerV202307 { TuningTarget::HashRate(_) => { anyhow::bail!("Hashrate tuning target is not supported on Antminer stock firmware") } + TuningTarget::Preset(_) => { + anyhow::bail!("Preset tuning target is not supported on Antminer stock firmware") + } }; let pre = self.web.get_miner_conf().await?; @@ -1175,6 +1178,8 @@ impl SupportsTemperatureConfig for AntMinerV202307 {} impl GetTuningPercent for AntMinerV202307 {} impl SetTuningPercent for AntMinerV202307 {} +impl SupportsPresets for AntMinerV202307 {} + #[cfg(test)] mod tests { use std::sync::Arc; diff --git a/asic-rs-firmwares/auradine/src/backends/v1/mod.rs b/asic-rs-firmwares/auradine/src/backends/v1/mod.rs index cc048026..536a64b0 100644 --- a/asic-rs-firmwares/auradine/src/backends/v1/mod.rs +++ b/asic-rs-firmwares/auradine/src/backends/v1/mod.rs @@ -1436,6 +1436,8 @@ impl SupportsTemperatureConfig for AuradineV1 {} impl GetTuningPercent for AuradineV1 {} impl SetTuningPercent for AuradineV1 {} +impl SupportsPresets for AuradineV1 {} + #[cfg(test)] mod tests { use std::sync::Arc; 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 412d48c3..22282d26 100644 --- a/asic-rs-firmwares/avalonminer/src/backends/avalon_a/mod.rs +++ b/asic-rs-firmwares/avalonminer/src/backends/avalon_a/mod.rs @@ -770,6 +770,8 @@ impl SupportsTemperatureConfig for AvalonAMiner {} impl GetTuningPercent for AvalonAMiner {} impl SetTuningPercent for AvalonAMiner {} +impl SupportsPresets for AvalonAMiner {} + #[cfg(test)] mod tests { use asic_rs_core::data::board::MinerControlBoard; 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 4d3f853e..5f0c287e 100644 --- a/asic-rs-firmwares/avalonminer/src/backends/avalon_q/mod.rs +++ b/asic-rs-firmwares/avalonminer/src/backends/avalon_q/mod.rs @@ -720,6 +720,8 @@ impl SupportsTemperatureConfig for AvalonQMiner {} impl GetTuningPercent for AvalonQMiner {} impl SetTuningPercent for AvalonQMiner {} +impl SupportsPresets for AvalonQMiner {} + #[cfg(test)] mod tests { use asic_rs_core::test::api::MockAPIClient; 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 24214f95..2b2b1db9 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 @@ -585,6 +585,8 @@ impl SupportsTemperatureConfig for Bitaxe200 {} impl GetTuningPercent for Bitaxe200 {} impl SetTuningPercent for Bitaxe200 {} +impl SupportsPresets for Bitaxe200 {} + #[cfg(test)] mod tests { use asic_rs_core::test::api::MockAPIClient; 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 75c130aa..11592a13 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 @@ -568,3 +568,4 @@ impl SupportsFanConfig for Bitaxe290 { impl SupportsTemperatureConfig for Bitaxe290 {} impl GetTuningPercent for Bitaxe290 {} impl SetTuningPercent for Bitaxe290 {} +impl SupportsPresets for Bitaxe290 {} 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 c0326bde..57577637 100644 --- a/asic-rs-firmwares/braiins/src/backends/v21_09/mod.rs +++ b/asic-rs-firmwares/braiins/src/backends/v21_09/mod.rs @@ -1000,6 +1000,8 @@ impl SupportsTemperatureConfig for BraiinsV2109 {} impl GetTuningPercent for BraiinsV2109 {} impl SetTuningPercent for BraiinsV2109 {} +impl SupportsPresets for BraiinsV2109 {} + #[cfg(test)] mod tests { use asic_rs_core::test::api::MockAPIClient; 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 19abc5c8..e616233a 100644 --- a/asic-rs-firmwares/braiins/src/backends/v25_03/mod.rs +++ b/asic-rs-firmwares/braiins/src/backends/v25_03/mod.rs @@ -999,6 +999,8 @@ impl SupportsTemperatureConfig for BraiinsV2503 {} impl GetTuningPercent for BraiinsV2503 {} impl SetTuningPercent for BraiinsV2503 {} +impl SupportsPresets for BraiinsV2503 {} + #[cfg(test)] mod tests { use std::str::FromStr; 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 76a4d94d..92d3463b 100644 --- a/asic-rs-firmwares/braiins/src/backends/v25_05/mod.rs +++ b/asic-rs-firmwares/braiins/src/backends/v25_05/mod.rs @@ -1004,6 +1004,8 @@ impl SupportsTemperatureConfig for BraiinsV2505 {} impl GetTuningPercent for BraiinsV2505 {} impl SetTuningPercent for BraiinsV2505 {} +impl SupportsPresets for BraiinsV2505 {} + #[cfg(test)] mod tests { use std::str::FromStr; 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 72fd4cac..4e854f80 100644 --- a/asic-rs-firmwares/braiins/src/backends/v25_07/mod.rs +++ b/asic-rs-firmwares/braiins/src/backends/v25_07/mod.rs @@ -874,6 +874,8 @@ impl SupportsTemperatureConfig for BraiinsV2507 {} impl GetTuningPercent for BraiinsV2507 {} impl SetTuningPercent for BraiinsV2507 {} +impl SupportsPresets for BraiinsV2507 {} + #[cfg(test)] mod tests { use std::str::FromStr; 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 90d06df2..9e8ea0ac 100644 --- a/asic-rs-firmwares/braiins/src/backends/v26_04/mod.rs +++ b/asic-rs-firmwares/braiins/src/backends/v26_04/mod.rs @@ -891,6 +891,8 @@ impl SupportsTemperatureConfig for BraiinsV2604 {} impl GetTuningPercent for BraiinsV2604 {} impl SetTuningPercent for BraiinsV2604 {} +impl SupportsPresets for BraiinsV2604 {} + #[cfg(test)] mod tests { use std::str::FromStr; diff --git a/asic-rs-firmwares/epic/src/backends/v1/mod.rs b/asic-rs-firmwares/epic/src/backends/v1/mod.rs index 8abe9bb1..177a8469 100644 --- a/asic-rs-firmwares/epic/src/backends/v1/mod.rs +++ b/asic-rs-firmwares/epic/src/backends/v1/mod.rs @@ -1372,6 +1372,9 @@ impl SupportsTuningConfig for PowerPlayV1 { TuningTarget::MiningMode(_) => { anyhow::bail!("MiningMode tuning target is not supported on ePIC PowerPlay") } + TuningTarget::Preset(_) => { + anyhow::bail!("Preset tuning target is not supported on ePIC PowerPlay") + } }; let Some(scaling) = scaling_config else { @@ -1577,6 +1580,8 @@ impl SupportsTemperatureConfig for PowerPlayV1 {} impl GetTuningPercent for PowerPlayV1 {} impl SetTuningPercent for PowerPlayV1 {} +impl SupportsPresets for PowerPlayV1 {} + #[cfg(test)] mod tests { use std::sync::Arc; diff --git a/asic-rs-firmwares/futurebit/src/backends/v2/mod.rs b/asic-rs-firmwares/futurebit/src/backends/v2/mod.rs index 9c65122b..c94ca364 100644 --- a/asic-rs-firmwares/futurebit/src/backends/v2/mod.rs +++ b/asic-rs-firmwares/futurebit/src/backends/v2/mod.rs @@ -808,6 +808,8 @@ impl SupportsTemperatureConfig for ApolloV2 {} impl GetTuningPercent for ApolloV2 {} impl SetTuningPercent for ApolloV2 {} +impl SupportsPresets for ApolloV2 {} + #[cfg(test)] mod tests { use super::*; diff --git a/asic-rs-firmwares/luxminer/src/backends/v1/mod.rs b/asic-rs-firmwares/luxminer/src/backends/v1/mod.rs index 17998501..b60ce3ca 100644 --- a/asic-rs-firmwares/luxminer/src/backends/v1/mod.rs +++ b/asic-rs-firmwares/luxminer/src/backends/v1/mod.rs @@ -1206,6 +1206,8 @@ impl SupportsFanConfig for LuxMinerV1 { impl SupportsTemperatureConfig for LuxMinerV1 {} impl GetTuningPercent for LuxMinerV1 {} impl SetTuningPercent for LuxMinerV1 {} + +impl SupportsPresets for LuxMinerV1 {} #[cfg(test)] mod tests { use std::sync::{Arc, Mutex}; diff --git a/asic-rs-firmwares/marathon/src/backends/v1/mod.rs b/asic-rs-firmwares/marathon/src/backends/v1/mod.rs index 3ecdcfb1..daae224c 100644 --- a/asic-rs-firmwares/marathon/src/backends/v1/mod.rs +++ b/asic-rs-firmwares/marathon/src/backends/v1/mod.rs @@ -1163,6 +1163,8 @@ impl SupportsTemperatureConfig for MaraV1 {} impl GetTuningPercent for MaraV1 {} impl SetTuningPercent for MaraV1 {} +impl SupportsPresets for MaraV1 {} + #[cfg(test)] mod tests { use super::*; diff --git a/asic-rs-firmwares/nerdaxe/src/backends/v1/mod.rs b/asic-rs-firmwares/nerdaxe/src/backends/v1/mod.rs index 36fdae10..fd1ae5bd 100644 --- a/asic-rs-firmwares/nerdaxe/src/backends/v1/mod.rs +++ b/asic-rs-firmwares/nerdaxe/src/backends/v1/mod.rs @@ -570,3 +570,4 @@ impl SupportsFanConfig for NerdAxeV1 { impl SupportsTemperatureConfig for NerdAxeV1 {} impl GetTuningPercent for NerdAxeV1 {} impl SetTuningPercent for NerdAxeV1 {} +impl SupportsPresets for NerdAxeV1 {} diff --git a/asic-rs-firmwares/proto/src/backends/v1/mod.rs b/asic-rs-firmwares/proto/src/backends/v1/mod.rs index e00a579e..8a8e400f 100644 --- a/asic-rs-firmwares/proto/src/backends/v1/mod.rs +++ b/asic-rs-firmwares/proto/src/backends/v1/mod.rs @@ -1010,6 +1010,8 @@ impl SupportsTemperatureConfig for ProtoV1 {} impl GetTuningPercent for ProtoV1 {} impl SetTuningPercent for ProtoV1 {} +impl SupportsPresets for ProtoV1 {} + #[cfg(test)] mod tests { use std::{collections::HashMap, str::FromStr}; diff --git a/asic-rs-firmwares/sealminer/src/backends/v2025/mod.rs b/asic-rs-firmwares/sealminer/src/backends/v2025/mod.rs index b6d654dc..f442a681 100644 --- a/asic-rs-firmwares/sealminer/src/backends/v2025/mod.rs +++ b/asic-rs-firmwares/sealminer/src/backends/v2025/mod.rs @@ -807,6 +807,9 @@ impl SupportsTuningConfig for SealMinerV2025 { TuningTarget::MiningMode(_) => { anyhow::bail!("Mining mode not supported on SealMiner") } + TuningTarget::Preset(_) => { + anyhow::bail!("Preset tuning not supported on SealMiner") + } } } @@ -845,6 +848,8 @@ impl SupportsTemperatureConfig for SealMinerV2025 {} impl GetTuningPercent for SealMinerV2025 {} impl SetTuningPercent for SealMinerV2025 {} +impl SupportsPresets for SealMinerV2025 {} + #[cfg(test)] mod tests { use std::str::FromStr; 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 1d77039a..7dde403a 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 @@ -5,7 +5,10 @@ use asic_rs_core::{ config::{ collector::{ConfigCollector, ConfigExtractor, ConfigField, ConfigLocation}, pools::PoolGroupConfig, + preset::PresetInfo, + scaling::ScalingConfig, temperature::TemperatureConfig, + tuning::TuningConfig, }, data::{ board::{BoardData, ChipData, MinerControlBoard}, @@ -17,6 +20,7 @@ use asic_rs_core::{ fan::FanData, hashrate::{HashRate, HashRateUnit}, message::{MessageSeverity, MinerMessage}, + miner::TuningTarget, pool::{PoolData, PoolGroupData, PoolURL}, }, traits::{miner::*, model::MinerModel}, @@ -108,6 +112,10 @@ impl GetDataLocations for VnishV120 { command: "chains/factory-info", parameters: None, }; + const WEB_SETTINGS: MinerCommand = MinerCommand::WebAPI { + command: "settings", + parameters: None, + }; match data_field { DataField::Mac => vec![( @@ -292,6 +300,14 @@ impl GetDataLocations for VnishV120 { tag: None, }, )], + DataField::TuningTarget => vec![( + WEB_SETTINGS, + DataExtractor { + func: get_by_pointer, + key: Some("/miner/overclock/preset"), + tag: None, + }, + )], _ => vec![], } } @@ -606,7 +622,18 @@ impl GetWattage for VnishV120 { // VNish 1.2.x has no manual throttle endpoint; tuning_percent lands in v1_3_0. impl GetTuningPercent for VnishV120 {} -impl GetTuningTarget for VnishV120 {} +impl GetTuningTarget for VnishV120 { + /// On VNish the active tuning target is the selected autotune preset + /// (`miner.overclock.preset` in `/settings`). + fn parse_tuning_target(&self, data: &HashMap) -> Option { + let preset = data.get(&DataField::TuningTarget)?; + preset + .as_str() + .map(String::from) + .or_else(|| preset.as_i64().map(|n| n.to_string())) + .map(TuningTarget::Preset) + } +} impl GetScaledTuningTarget for VnishV120 {} impl GetTuningCapabilities for VnishV120 {} @@ -850,6 +877,36 @@ impl SetPowerLimit for VnishV120 { #[async_trait] impl SetTuningPercent for VnishV120 {} +#[async_trait] +impl SupportsPresets for VnishV120 { + fn supports_presets(&self) -> bool { + true + } + + /// VNish exposes its autotune presets at `/autotune/presets`, each + /// `{ "name": "5560", "pretty": "5560 watt ~ 175 TH", "status": "tuned" }`. + async fn get_presets(&self) -> Vec { + let Ok(presets) = self.web.autotune_presets().await else { + return Vec::new(); + }; + let list = presets + .as_array() + .cloned() + .or_else(|| presets.get("presets").and_then(|p| p.as_array()).cloned()) + .unwrap_or_default(); + list.iter() + .filter_map(|p| { + let name = p.get("name")?.as_str()?.to_string(); + Some(PresetInfo { + name, + pretty: p.get("pretty").and_then(|v| v.as_str()).map(String::from), + status: p.get("status").and_then(|v| v.as_str()).map(String::from), + }) + }) + .collect() + } +} + #[async_trait] impl SupportsPoolsConfig for VnishV120 { async fn get_pools_config(&self) -> anyhow::Result> { @@ -1018,7 +1075,49 @@ impl HasAuth for VnishV120 { #[async_trait] impl SupportsTuningConfig for VnishV120 { fn supports_tuning_config(&self) -> bool { - false + true + } + + /// VNish tunes by selecting a named autotune preset. A `Preset` target sets + /// `miner.overclock.preset` directly; a `Power` target reuses the + /// preset-picking logic in `set_power_limit`. + async fn set_tuning_config( + &self, + config: TuningConfig, + _scaling_config: Option, + ) -> anyhow::Result { + match config.target { + TuningTarget::Preset(name) => { + let mut settings = self.web.settings().await?; + { + let overclock = settings + .pointer_mut("/miner/overclock") + .ok_or_else(|| anyhow::anyhow!("settings missing miner.overclock"))?; + overclock["preset"] = json!(name); + } + let overclock = settings + .pointer("/miner/overclock") + .cloned() + .ok_or_else(|| anyhow::anyhow!("settings missing miner.overclock"))?; + self.web + .set_settings(json!({ "miner": { "overclock": overclock } })) + .await?; + let updated = self.web.settings().await?; + let applied = updated.pointer("/miner/overclock/preset").and_then(|p| { + p.as_str() + .map(String::from) + .or_else(|| p.as_i64().map(|n| n.to_string())) + }); + Ok(applied.as_deref() == Some(name.as_str())) + } + TuningTarget::Power(limit) => self.set_power_limit(limit).await, + TuningTarget::HashRate(_) => { + anyhow::bail!("HashRate tuning target is not supported on VNish") + } + TuningTarget::MiningMode(_) => { + anyhow::bail!("MiningMode tuning target is not supported on VNish") + } + } } } diff --git a/asic-rs-firmwares/vnish/src/backends/v1_3_0/mod.rs b/asic-rs-firmwares/vnish/src/backends/v1_3_0/mod.rs index c6973040..6c7b1983 100644 --- a/asic-rs-firmwares/vnish/src/backends/v1_3_0/mod.rs +++ b/asic-rs-firmwares/vnish/src/backends/v1_3_0/mod.rs @@ -5,6 +5,7 @@ use asic_rs_core::{ config::{ collector::{ConfigCollector, ConfigExtractor, ConfigField, ConfigLocation}, pools::PoolGroupConfig, + preset::PresetInfo, temperature::TemperatureConfig, }, data::{ @@ -17,6 +18,7 @@ use asic_rs_core::{ fan::FanData, hashrate::{HashRate, HashRateUnit}, message::{MessageSeverity, MinerMessage}, + miner::TuningTarget, pool::{PoolData, PoolGroupData, PoolURL}, }, traits::{miner::*, model::MinerModel}, @@ -108,6 +110,10 @@ impl GetDataLocations for VnishV130 { command: "chains/factory-info", parameters: None, }; + const WEB_SETTINGS: MinerCommand = MinerCommand::WebAPI { + command: "settings", + parameters: None, + }; match data_field { DataField::Mac => vec![( @@ -300,6 +306,14 @@ impl GetDataLocations for VnishV130 { tag: None, }, )], + DataField::TuningTarget => vec![( + WEB_SETTINGS, + DataExtractor { + func: get_by_pointer, + key: Some("/miner/overclock/preset"), + tag: None, + }, + )], _ => vec![], } } @@ -618,7 +632,18 @@ impl GetTuningPercent for VnishV130 { } } -impl GetTuningTarget for VnishV130 {} +impl GetTuningTarget for VnishV130 { + /// On VNish the active tuning target is the selected autotune preset + /// (`miner.overclock.preset` in `/settings`). + fn parse_tuning_target(&self, data: &HashMap) -> Option { + let preset = data.get(&DataField::TuningTarget)?; + preset + .as_str() + .map(String::from) + .or_else(|| preset.as_i64().map(|n| n.to_string())) + .map(TuningTarget::Preset) + } +} impl GetScaledTuningTarget for VnishV130 {} impl GetTuningCapabilities for VnishV130 {} @@ -873,6 +898,36 @@ impl SetTuningPercent for VnishV130 { } } +#[async_trait] +impl SupportsPresets for VnishV130 { + fn supports_presets(&self) -> bool { + true + } + + /// VNish exposes its autotune presets at `/autotune/presets`, each + /// `{ "name": "5560", "pretty": "5560 watt ~ 175 TH", "status": "tuned" }`. + async fn get_presets(&self) -> Vec { + let Ok(presets) = self.web.autotune_presets().await else { + return Vec::new(); + }; + let list = presets + .as_array() + .cloned() + .or_else(|| presets.get("presets").and_then(|p| p.as_array()).cloned()) + .unwrap_or_default(); + list.iter() + .filter_map(|p| { + let name = p.get("name")?.as_str()?.to_string(); + Some(PresetInfo { + name, + pretty: p.get("pretty").and_then(|v| v.as_str()).map(String::from), + status: p.get("status").and_then(|v| v.as_str()).map(String::from), + }) + }) + .collect() + } +} + #[async_trait] impl SupportsPoolsConfig for VnishV130 { async fn get_pools_config(&self) -> anyhow::Result> { diff --git a/asic-rs-firmwares/volcminer/src/backends/v1/mod.rs b/asic-rs-firmwares/volcminer/src/backends/v1/mod.rs index 260e09d5..d63db753 100644 --- a/asic-rs-firmwares/volcminer/src/backends/v1/mod.rs +++ b/asic-rs-firmwares/volcminer/src/backends/v1/mod.rs @@ -813,6 +813,7 @@ impl SupportsTuningConfig for VolcMinerV1 {} impl SupportsFanConfig for VolcMinerV1 {} impl SupportsTemperatureConfig for VolcMinerV1 {} +impl SupportsPresets for VolcMinerV1 {} impl GetTuningPercent for VolcMinerV1 {} impl SetTuningPercent for VolcMinerV1 {} diff --git a/asic-rs-firmwares/whatsminer/src/backends/v1/mod.rs b/asic-rs-firmwares/whatsminer/src/backends/v1/mod.rs index 4cb1903b..756ac975 100644 --- a/asic-rs-firmwares/whatsminer/src/backends/v1/mod.rs +++ b/asic-rs-firmwares/whatsminer/src/backends/v1/mod.rs @@ -831,3 +831,4 @@ mod integration_tests { impl SupportsTemperatureConfig for WhatsMinerV1 {} impl GetTuningPercent for WhatsMinerV1 {} impl SetTuningPercent for WhatsMinerV1 {} +impl SupportsPresets for WhatsMinerV1 {} diff --git a/asic-rs-firmwares/whatsminer/src/backends/v2/mod.rs b/asic-rs-firmwares/whatsminer/src/backends/v2/mod.rs index b8cb4e12..4089f924 100644 --- a/asic-rs-firmwares/whatsminer/src/backends/v2/mod.rs +++ b/asic-rs-firmwares/whatsminer/src/backends/v2/mod.rs @@ -791,6 +791,9 @@ fn tuning_config_to_rpc(config: &TuningConfig) -> anyhow::Result<(&'static str, TuningTarget::HashRate(_) => { anyhow::bail!("HashRate tuning target is not supported on WhatsMiner") } + TuningTarget::Preset(_) => { + anyhow::bail!("Preset tuning target is not supported on WhatsMiner") + } } } @@ -1208,3 +1211,4 @@ mod integration_tests { impl SupportsTemperatureConfig for WhatsMinerV2 {} impl GetTuningPercent for WhatsMinerV2 {} impl SetTuningPercent for WhatsMinerV2 {} +impl SupportsPresets for WhatsMinerV2 {} diff --git a/asic-rs-firmwares/whatsminer/src/backends/v3/mod.rs b/asic-rs-firmwares/whatsminer/src/backends/v3/mod.rs index a0326a57..5e6cf46c 100644 --- a/asic-rs-firmwares/whatsminer/src/backends/v3/mod.rs +++ b/asic-rs-firmwares/whatsminer/src/backends/v3/mod.rs @@ -821,6 +821,9 @@ fn tuning_config_to_v3_rpc(config: &TuningConfig) -> anyhow::Result<(&'static st TuningTarget::HashRate(_) => { anyhow::bail!("HashRate tuning target is not supported on WhatsMiner") } + TuningTarget::Preset(_) => { + anyhow::bail!("Preset tuning target is not supported on WhatsMiner") + } } } @@ -1204,3 +1207,4 @@ mod integration_tests { impl SupportsTemperatureConfig for WhatsMinerV3 {} impl GetTuningPercent for WhatsMinerV3 {} impl SetTuningPercent for WhatsMinerV3 {} +impl SupportsPresets for WhatsMinerV3 {} diff --git a/python/pyasic_rs/asic_rs.pyi b/python/pyasic_rs/asic_rs.pyi index b4a6229b..7b04c9a0 100644 --- a/python/pyasic_rs/asic_rs.pyi +++ b/python/pyasic_rs/asic_rs.pyi @@ -387,6 +387,7 @@ class Miner: def get_tuning_config(self, /) -> Awaitable[TuningConfig |None]: ... def get_tuning_target(self, /) -> Awaitable[TuningTarget |None]: ... def get_tuning_percent(self, /) -> Awaitable[int |None]: ... + def get_presets(self, /) -> Awaitable[list[PresetInfo]]: ... def get_uptime(self, /) -> Awaitable[timedelta |None]: ... def get_wattage(self, /) -> Awaitable[float |None]: ... @property @@ -421,6 +422,7 @@ class Miner: @property def supports_pools_config(self, /) -> bool: ... @property + def supports_presets(self, /) -> bool: ... def supports_read_logs(self, /) -> bool: ... @property def supports_restart(self, /) -> bool: ... @@ -838,6 +840,15 @@ class PowerTuningCapabilities: @classmethod def model_validate(cls, /, obj: "object", **_kwargs: "object") -> "PowerTuningCapabilities": ... +class PresetInfo: + def __repr__(self, /) -> str: ... + @property + def name(self, /) -> str: ... + @property + def pretty(self, /) -> str |None: ... + @property + def status(self, /) -> str |None: ... + @final class PresetTuningCapabilities: @classmethod @@ -946,11 +957,15 @@ class TuningConfig: def model_validate(cls, /, obj: "object", **_kwargs: "object") -> "TuningConfig": ... @classmethod def power(cls, /, watts: float, algorithm: Any |None = None) -> TuningConfig: ... + @classmethod + def preset(cls, /, name: str) -> TuningConfig: ... @property def target_hashrate(self, /) -> HashRate |None: ... @property def target_mode(self, /) -> MiningMode |None: ... @property + def target_preset(self, /) -> str |None: ... + @property def target_watts(self, /) -> float |None: ... @property def variant(self, /) -> str: ... @@ -964,6 +979,10 @@ class TuningTarget: def mode(mode: MiningMode) -> TuningTarget: ... @staticmethod def power(watts: float) -> TuningTarget: ... + @staticmethod + def preset(name: str) -> TuningTarget: ... + @property + def preset_name(self, /) -> str |None: ... @property def target_hashrate(self, /) -> HashRate |None: ... @property @@ -990,5 +1009,11 @@ class TuningTarget: def __new__(cls, /, watts: float) -> TuningTarget.Power: ... @property def watts(self, /) -> float: ... + @final + class Preset(TuningTarget): + __match_args__: Final = ("name",) + def __new__(cls, /, name: str) -> TuningTarget.Preset: ... + @property + def name(self, /) -> str: ... def __getattr__(name: str) -> Incomplete: ... diff --git a/src/python/miner.rs b/src/python/miner.rs index c08d484a..2958291f 100644 --- a/src/python/miner.rs +++ b/src/python/miner.rs @@ -4,8 +4,8 @@ use super::typing::{PyAwaitable, future_into_py}; use asic_rs_core::data::collector::DataField; use asic_rs_core::{ config::{ - fan::FanConfig, pools::PoolGroupConfig as PoolGroup, scaling::ScalingConfig, - temperature::TemperatureConfig, tuning::TuningConfig, + fan::FanConfig, pools::PoolGroupConfig as PoolGroup, preset::PresetInfo, + scaling::ScalingConfig, temperature::TemperatureConfig, tuning::TuningConfig, }, data::{ board::BoardData, @@ -183,6 +183,11 @@ impl Miner { fn supports_set_tuning_percent(&self, py: Python<'_>) -> bool { self.with_miner(py, |miner| miner.supports_set_tuning_percent()) } + /// Whether this miner exposes named autotune/overclock presets. + #[getter] + fn supports_presets(&self, py: Python<'_>) -> bool { + self.with_miner(py, |miner| miner.supports_presets()) + } /// Whether this miner supports restart commands. #[getter] fn supports_restart(&self, py: Python<'_>) -> bool { @@ -667,6 +672,17 @@ impl Miner { Ok(inner.set_tuning_percent(percent).await.ok()) }) } + /// Await the firmware's available autotune/overclock presets. + /// + /// Select one with `set_tuning_config(TuningConfig.preset(name))`; the active + /// preset is reported by `get_tuning_target()` as a `TuningTarget.preset`. + pub fn get_presets<'a>(&self, py: Python<'a>) -> PyResult>> { + let inner = Arc::clone(&self.inner); + future_into_py(py, async move { + let inner = inner.read().await; + Ok(inner.get_presets().await) + }) + } /// Replace the configured mining pool groups. #[pyo3(signature = (groups: "list[PoolGroup]"))] pub fn set_pools_config<'a>( diff --git a/src/python/mod.rs b/src/python/mod.rs index 6fc876c5..e372c507 100644 --- a/src/python/mod.rs +++ b/src/python/mod.rs @@ -31,6 +31,7 @@ mod asic_rs { use asic_rs_core::config::{ fan::{FanConfig, FanMode}, pools::{PoolConfig as Pool, PoolGroupConfig as PoolGroup}, + preset::PresetInfo, scaling::ScalingConfig, temperature::TemperatureConfig, tuning::TuningConfig,