diff --git a/asic-rs-firmwares/antminer/src/backends/v2020/mod.rs b/asic-rs-firmwares/antminer/src/backends/v2020/mod.rs index ef9dc531..68fe3e2f 100644 --- a/asic-rs-firmwares/antminer/src/backends/v2020/mod.rs +++ b/asic-rs-firmwares/antminer/src/backends/v2020/mod.rs @@ -78,20 +78,6 @@ impl MinerMode { } } -fn miner_conf_mode_matches(conf: &Value, mode: MinerMode) -> bool { - let expected = mode.to_string(); - ["miner-mode", "bitmain-work-mode"] - .iter() - .filter_map(|key| conf.get(key)) - .any(|value| { - value - .as_str() - .map(|mode| mode == expected) - .or_else(|| value.as_i64().map(|mode| mode.to_string() == expected)) - .unwrap_or(false) - }) -} - fn miner_mode_config_key(miner_conf: &Value) -> Option<&'static str> { if miner_conf.get("miner-mode").is_some() { Some("miner-mode") @@ -150,9 +136,11 @@ fn miner_conf_with_pools(miner_conf: &Value, pools: Vec) -> Value { } fn miner_conf_with_miner_mode(miner_conf: &Value, mode: MinerMode) -> Option { - let mode_key = miner_mode_config_key(miner_conf)?; + miner_mode_config_key(miner_conf)?; let mut payload = browser_miner_conf_payload(miner_conf); - payload.insert(mode_key.to_string(), Value::from(mode.as_web_value())); + let mode = Value::from(mode.as_web_value()); + payload.insert("miner-mode".to_string(), mode.clone()); + payload.insert("bitmain-work-mode".to_string(), mode); Some(Value::Object(payload)) } @@ -1043,9 +1031,15 @@ impl Pause for AntMinerV2020 { return Ok(false); }; - self.web.set_miner_conf(miner_conf).await?; - let post = self.web.get_miner_conf().await?; - Ok(miner_conf_mode_matches(&post, MinerMode::Sleep)) + let response = self.web.set_miner_conf(miner_conf).await?; + if response.get("stats").and_then(Value::as_str) != Some("success") { + return Ok(false); + } + + // set_miner_conf.cgi writes the config before starting a miner + // reload/restart in the background; an immediate follow-up read can + // race the device becoming temporarily unavailable. + Ok(true) } } @@ -1061,9 +1055,12 @@ impl Resume for AntMinerV2020 { return Ok(false); }; - self.web.set_miner_conf(miner_conf).await?; - let post = self.web.get_miner_conf().await?; - Ok(miner_conf_mode_matches(&post, MinerMode::Normal)) + let response = self.web.set_miner_conf(miner_conf).await?; + if response.get("stats").and_then(Value::as_str) != Some("success") { + return Ok(false); + } + + Ok(true) } } @@ -1253,6 +1250,43 @@ mod tests { AM_DEVS, AM_POOLS, AM_STATS, AM_SUMMARY, AM_SYSTEM_INFO, AM_VERSION, }; + #[test] + fn set_miner_conf_payload_matches_cgi_contract_for_pause_resume() { + let pre = json!({ + "pools": [ + {"url": "stratum+tcp://pool1.example:3333", "user": "worker.1", "pass": "x"}, + {"url": "stratum+tcp://pool2.example:3333", "user": "worker.2", "pass": "x"}, + {"url": "stratum+tcp://pool3.example:3333", "user": "worker.3", "pass": "x"} + ], + "bitmain-fan-ctrl": true, + "bitmain-fan-pwm": "70", + "bitmain-work-mode": "0", + "bitmain-freq-level": "100", + "api-listen": true + }); + + assert_eq!( + miner_conf_with_miner_mode(&pre, MinerMode::Sleep).unwrap(), + json!({ + "pools": [ + {"url": "stratum+tcp://pool1.example:3333", "user": "worker.1", "pass": "x"}, + {"url": "stratum+tcp://pool2.example:3333", "user": "worker.2", "pass": "x"}, + {"url": "stratum+tcp://pool3.example:3333", "user": "worker.3", "pass": "x"} + ], + "bitmain-fan-ctrl": true, + "bitmain-fan-pwm": "70", + "freq-level": "100", + "miner-mode": 1, + "bitmain-work-mode": 1 + }) + ); + + assert_eq!( + miner_conf_with_miner_mode(&pre, MinerMode::Normal).unwrap()["miner-mode"], + json!(0) + ); + } + #[tokio::test] async fn test_antminer() { let miner = AntMinerV2020::new(IpAddr::from([127, 0, 0, 1]), AntMinerModel::S19Pro); 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 0791342e..b97150f9 100644 --- a/asic-rs-firmwares/antminer/src/backends/v2023_07/mod.rs +++ b/asic-rs-firmwares/antminer/src/backends/v2023_07/mod.rs @@ -66,6 +66,17 @@ impl Display for MinerMode { } } +impl MinerMode { + fn as_web_value(&self) -> u8 { + match self { + MinerMode::Sleep => 1, + MinerMode::Low => 3, + MinerMode::Normal => 0, + MinerMode::High => 2, + } + } +} + fn miner_mode_config_key(miner_conf: &Value) -> Option<&'static str> { if miner_conf.get("miner-mode").is_some() { Some("miner-mode") @@ -76,6 +87,56 @@ fn miner_mode_config_key(miner_conf: &Value) -> Option<&'static str> { } } +fn browser_miner_conf_payload(miner_conf: &Value) -> serde_json::Map { + let mut payload = serde_json::Map::new(); + payload.insert( + "bitmain-fan-ctrl".to_string(), + miner_conf + .get("bitmain-fan-ctrl") + .cloned() + .unwrap_or(Value::Bool(false)), + ); + payload.insert( + "bitmain-fan-pwm".to_string(), + miner_conf + .get("bitmain-fan-pwm") + .cloned() + .unwrap_or(Value::String("100".to_string())), + ); + + if let Some(mode_key) = miner_mode_config_key(miner_conf) + && let Some(mode) = miner_conf.get(mode_key) + { + payload.insert(mode_key.to_string(), mode.clone()); + } + payload.insert( + "freq-level".to_string(), + miner_conf + .get("freq-level") + .or_else(|| miner_conf.get("bitmain-freq-level")) + .cloned() + .unwrap_or(Value::String("100".to_string())), + ); + payload.insert( + "pools".to_string(), + miner_conf + .get("pools") + .cloned() + .unwrap_or_else(|| Value::Array(Vec::new())), + ); + + payload +} + +fn miner_conf_with_miner_mode(miner_conf: &Value, mode: MinerMode) -> Option { + miner_mode_config_key(miner_conf)?; + let mut payload = browser_miner_conf_payload(miner_conf); + let mode = Value::from(mode.as_web_value()); + payload.insert("miner-mode".to_string(), mode.clone()); + payload.insert("bitmain-work-mode".to_string(), mode); + Some(Value::Object(payload)) +} + fn miner_mode_from_value(mode: &Value) -> Option { let mode = mode .as_str() @@ -635,6 +696,7 @@ impl GetIsMining for AntMinerV202307 { && status_lower != "idle" && status_lower != "sleep" && status_lower != "1" + && status_lower != "5" }) .unwrap_or(true) } @@ -944,24 +1006,16 @@ impl Pause for AntMinerV202307 { #[allow(unused_variables)] async fn pause(&self, at_time: Option) -> anyhow::Result { let pre = self.web.get_miner_conf().await?; + let Some(miner_conf) = miner_conf_with_miner_mode(&pre, MinerMode::Sleep) else { + return Ok(false); + }; - if pre.get("miner-mode").is_some() { - return Ok(self - .web - .set_miner_conf(json!({"miner-mode": MinerMode::Sleep.to_string()})) - .await - .is_ok()); - } - - if pre.get("bitmain-work-mode").is_some() { - return Ok(self - .web - .set_miner_conf(json!({"bitmain-work-mode": MinerMode::Sleep.to_string()})) - .await - .is_ok()); + let response = self.web.set_miner_conf(miner_conf).await?; + if response.get("stats").and_then(Value::as_str) != Some("success") { + return Ok(false); } - Ok(false) + Ok(true) } } @@ -973,24 +1027,19 @@ impl Resume for AntMinerV202307 { #[allow(unused_variables)] async fn resume(&self, at_time: Option) -> anyhow::Result { let pre = self.web.get_miner_conf().await?; + let Some(miner_conf) = miner_conf_with_miner_mode(&pre, MinerMode::Normal) else { + return Ok(false); + }; - if pre.get("miner-mode").is_some() { - return Ok(self - .web - .set_miner_conf(json!({"miner-mode": MinerMode::Normal.to_string()})) - .await - .is_ok()); - } - - if pre.get("bitmain-work-mode").is_some() { - return Ok(self - .web - .set_miner_conf(json!({"bitmain-work-mode": MinerMode::Normal.to_string()})) - .await - .is_ok()); + let response = self.web.set_miner_conf(miner_conf).await?; + if response.get("stats").and_then(Value::as_str) != Some("success") { + return Ok(false); } - Ok(false) + // set_miner_conf.cgi writes the config before starting a miner + // reload/restart in the background; an immediate follow-up read can + // race the device becoming temporarily unavailable. + Ok(true) } } @@ -1104,12 +1153,15 @@ impl SupportsTuningConfig for AntMinerV202307 { }; let pre = self.web.get_miner_conf().await?; - let Some(mode_key) = miner_mode_config_key(&pre) else { + if miner_mode_config_key(&pre).is_none() { anyhow::bail!("No Antminer mining mode field found in miner config") }; self.web - .set_miner_conf(json!({ mode_key: mode.to_string() })) + .set_miner_conf(json!({ + "miner-mode": mode.to_string(), + "bitmain-work-mode": mode.to_string(), + })) .await?; Ok(true) } @@ -1187,6 +1239,43 @@ mod tests { AM_DEVS, AM_POOLS, AM_STATS, AM_SUMMARY, AM_SYSTEM_INFO, AM_VERSION, }; + #[test] + fn set_miner_conf_payload_matches_cgi_contract_for_pause_resume() { + let pre = json!({ + "pools": [ + {"url": "stratum+tcp://pool1.example:3333", "user": "worker.1", "pass": "x"}, + {"url": "stratum+tcp://pool2.example:3333", "user": "worker.2", "pass": "x"}, + {"url": "stratum+tcp://pool3.example:3333", "user": "worker.3", "pass": "x"} + ], + "bitmain-fan-ctrl": true, + "bitmain-fan-pwm": "70", + "bitmain-work-mode": "0", + "bitmain-freq-level": "100", + "api-listen": true + }); + + assert_eq!( + miner_conf_with_miner_mode(&pre, MinerMode::Sleep).unwrap(), + json!({ + "pools": [ + {"url": "stratum+tcp://pool1.example:3333", "user": "worker.1", "pass": "x"}, + {"url": "stratum+tcp://pool2.example:3333", "user": "worker.2", "pass": "x"}, + {"url": "stratum+tcp://pool3.example:3333", "user": "worker.3", "pass": "x"} + ], + "bitmain-fan-ctrl": true, + "bitmain-fan-pwm": "70", + "freq-level": "100", + "miner-mode": 1, + "bitmain-work-mode": 1 + }) + ); + + assert_eq!( + miner_conf_with_miner_mode(&pre, MinerMode::Normal).unwrap()["miner-mode"], + json!(0) + ); + } + #[tokio::test] async fn test_antminer() { let miner = AntMinerV202307::new(IpAddr::from([127, 0, 0, 1]), AntMinerModel::S19Pro); diff --git a/asic-rs-firmwares/antminer/src/backends/v2023_07/web.rs b/asic-rs-firmwares/antminer/src/backends/v2023_07/web.rs index cb4d006b..550aef26 100644 --- a/asic-rs-firmwares/antminer/src/backends/v2023_07/web.rs +++ b/asic-rs-firmwares/antminer/src/backends/v2023_07/web.rs @@ -89,11 +89,23 @@ impl AntMinerWebAPI { _privileged: bool, parameters: Option, method: Method, + ) -> Result { + self.send_web_command_with_timeout(command, _privileged, parameters, method, self.timeout) + .await + } + + async fn send_web_command_with_timeout( + &self, + command: &str, + _privileged: bool, + parameters: Option, + method: Method, + timeout: Duration, ) -> Result { let url = format!("http://{}:{}/cgi-bin/{}.cgi", self.ip, self.port, command); let response = self - .execute_web_request(&url, &method, parameters.clone()) + .execute_web_request(&url, &method, parameters.clone(), timeout) .await?; let status = response.status(); @@ -112,7 +124,9 @@ impl AntMinerWebAPI { method: Method, ) -> Result { let url = format!("http://{}:{}/cgi-bin/{}.cgi", self.ip, self.port, command); - let response = self.execute_web_request(&url, &method, parameters).await?; + let response = self + .execute_web_request(&url, &method, parameters, self.timeout) + .await?; let status = response.status(); if !status.is_success() { @@ -129,7 +143,9 @@ impl AntMinerWebAPI { method: Method, ) -> Result { let url = format!("http://{}:{}/cgi-bin/{}.cgi", self.ip, self.port, command); - let response = self.execute_web_request(&url, &method, parameters).await?; + let response = self + .execute_web_request(&url, &method, parameters, self.timeout) + .await?; let status = response.status(); if status.is_success() { @@ -144,13 +160,14 @@ impl AntMinerWebAPI { url: &str, method: &Method, parameters: Option, + timeout: Duration, ) -> Result { let client = self.client()?; let response = match *method { Method::GET => client .get(url) - .timeout(self.timeout) + .timeout(timeout) .send_digest_auth((self.auth.username(), self.auth.password())) .await .map_err(|e| anyhow!(e.to_string()))?, @@ -159,7 +176,7 @@ impl AntMinerWebAPI { client .post(url) .json(&data) - .timeout(self.timeout) + .timeout(timeout) .send_digest_auth((self.auth.username(), self.auth.password())) .await .map_err(|e| anyhow!(e.to_string()))? @@ -176,8 +193,14 @@ impl AntMinerWebAPI { } pub async fn set_miner_conf(&self, conf: Value) -> Result { - self.send_web_command("set_miner_conf", false, Some(conf), Method::POST) - .await + self.send_web_command_with_timeout( + "set_miner_conf", + false, + Some(conf), + Method::POST, + self.timeout.max(Duration::from_secs(30)), + ) + .await } pub async fn blink(&self, blink: bool) -> Result { diff --git a/asic-rs-firmwares/bitaxe/src/backends/v2_0_0/web.rs b/asic-rs-firmwares/bitaxe/src/backends/v2_0_0/web.rs index 750d6691..de531512 100644 --- a/asic-rs-firmwares/bitaxe/src/backends/v2_0_0/web.rs +++ b/asic-rs-firmwares/bitaxe/src/backends/v2_0_0/web.rs @@ -90,17 +90,17 @@ impl WebAPIClient for BitaxeWebAPI { Ok(json_data) => return Ok(json_data), Err(e) => { if attempt == self.retries { - return Err(BitaxeError::ParseError(e.to_string()))?; + Err(BitaxeError::ParseError(e.to_string()))?; } } } } else if attempt == self.retries { - return Err(BitaxeError::HttpError(response.status().as_u16()))?; + Err(BitaxeError::HttpError(response.status().as_u16()))?; } } Err(e) => { if attempt == self.retries { - return Err(e)?; + Err(e)?; } } } diff --git a/asic-rs-firmwares/epic/src/backends/v1/web.rs b/asic-rs-firmwares/epic/src/backends/v1/web.rs index 98705312..5fcf6299 100644 --- a/asic-rs-firmwares/epic/src/backends/v1/web.rs +++ b/asic-rs-firmwares/epic/src/backends/v1/web.rs @@ -188,7 +188,7 @@ impl PowerPlayWebAPI { let response = self.execute_request(&url, &Method::GET, None).await?; let status = response.status(); if !status.is_success() { - return Err(PowerPlayError::HttpError(status.as_u16()))?; + Err(PowerPlayError::HttpError(status.as_u16()))?; } response diff --git a/asic-rs-firmwares/nerdaxe/src/backends/v1/web.rs b/asic-rs-firmwares/nerdaxe/src/backends/v1/web.rs index 09f761df..e0b2d1c9 100644 --- a/asic-rs-firmwares/nerdaxe/src/backends/v1/web.rs +++ b/asic-rs-firmwares/nerdaxe/src/backends/v1/web.rs @@ -56,17 +56,17 @@ impl WebAPIClient for NerdAxeWebAPI { Ok(json_data) => return Ok(json_data), Err(e) => { if attempt == self.retries { - return Err(NerdAxeError::ParseError(e.to_string()))?; + Err(NerdAxeError::ParseError(e.to_string()))?; } } } } else if attempt == self.retries { - return Err(NerdAxeError::HttpError(response.status().as_u16()))?; + Err(NerdAxeError::HttpError(response.status().as_u16()))?; } } Err(e) => { if attempt == self.retries { - return Err(e)?; + Err(e)?; } } } diff --git a/asic-rs-firmwares/vnish/src/backends/v1_2_0/web.rs b/asic-rs-firmwares/vnish/src/backends/v1_2_0/web.rs index cedfafce..1b197692 100644 --- a/asic-rs-firmwares/vnish/src/backends/v1_2_0/web.rs +++ b/asic-rs-firmwares/vnish/src/backends/v1_2_0/web.rs @@ -283,7 +283,7 @@ impl VnishWebAPI { let response = self.execute_request(&url, &Method::GET, None).await?; let status = response.status(); if !status.is_success() { - return Err(VnishError::HttpError(status.as_u16()))?; + Err(VnishError::HttpError(status.as_u16()))?; } response diff --git a/asic-rs-firmwares/vnish/src/backends/v1_3_0/web.rs b/asic-rs-firmwares/vnish/src/backends/v1_3_0/web.rs index 06ee3290..b37bafb5 100644 --- a/asic-rs-firmwares/vnish/src/backends/v1_3_0/web.rs +++ b/asic-rs-firmwares/vnish/src/backends/v1_3_0/web.rs @@ -294,7 +294,7 @@ impl VnishWebAPI { let response = self.execute_request(&url, &Method::GET, None).await?; let status = response.status(); if !status.is_success() { - return Err(VnishError::HttpError(status.as_u16()))?; + Err(VnishError::HttpError(status.as_u16()))?; } response