Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 34 additions & 8 deletions asic-rs-firmwares/antminer/src/backends/v2020/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -509,14 +509,37 @@ impl GetDataLocations for AntMinerV2020 {
tag: None,
},
)],
DataField::Wattage => vec![(
RPC_STATS,
DataExtractor {
func: get_by_pointer,
key: Some("/STATS/1"),
tag: None,
},
)],
DataField::Wattage => {
// Newer stock firmware reports power draw only in the `new_api`
// variant of `stats`. That is a different payload to the legacy
// one: its `STATS` array holds a single element, so the reading
// sits at `/STATS/0` rather than the `/STATS/1` used above.
//
// Not a `const` because `json!` is not const-evaluable.
let rpc_new_stats = MinerCommand::RPC {
command: "stats",
parameters: Some(json!({ "new_api": true })),
};

vec![
(
RPC_STATS,
DataExtractor {
func: get_by_pointer,
key: Some("/STATS/1"),
tag: None,
},
),
(
rpc_new_stats,
DataExtractor {
func: get_by_pointer,
key: Some("/STATS/0"),
tag: None,
},
),
]
}
DataField::SerialNumber => vec![
(
WEB_SYSTEM_INFO,
Expand Down Expand Up @@ -835,6 +858,9 @@ impl GetWattage for AntMinerV2020 {
if let Some(power) = stats_data
.get("power")
.or_else(|| stats_data.get("Power"))
// Same firmware version spells this differently per model: the
// L9 reports `power`, the L11 `watt`.
.or_else(|| stats_data.get("watt"))
.and_then(|v| v.as_f64())
{
return Some(Power::from_watts(power));
Expand Down
80 changes: 70 additions & 10 deletions asic-rs-firmwares/antminer/src/backends/v2020/rpc.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,35 @@ use asic_rs_core::{
use async_trait::async_trait;
use serde_json::{Value, json};

/// Build the JSON request for a cgminer-style RPC call.
///
/// Bitmain's API extensions — `new_api` being the one this backend uses — are
/// top-level flags alongside `command`, not values of cgminer's `parameter`
/// argument. Wrapping them makes the firmware ignore the flag and silently
/// answer with the legacy payload, which is indistinguishable from a
/// successful call.
///
/// An object is always one of those extensions; cgminer's own convention
/// passes a scalar (e.g. `switchpool`'s pool index), so scalars keep the
/// `parameter` wrapper.
fn build_rpc_request(command: &str, parameters: Option<Value>) -> Value {
match parameters {
Some(Value::Object(params)) => {
let mut request = params;
// Inserted last so a stray `command` key cannot displace it.
request.insert("command".to_string(), Value::from(command));
Value::Object(request)
}
Some(params) => json!({
"command": command,
"parameter": params
}),
None => json!({
"command": command
}),
}
}

#[derive(Debug)]
pub struct AntMinerRPCAPI {
ip: IpAddr,
Expand All @@ -28,16 +57,7 @@ impl AntMinerRPCAPI {
_privileged: bool,
parameters: Option<Value>,
) -> anyhow::Result<Value> {
let request = if let Some(params) = parameters {
json!({
"command": command,
"parameter": params
})
} else {
json!({
"command": command
})
};
let request = build_rpc_request(command, parameters);

let json_str = request.to_string();
let message = format!("{}\n", json_str);
Expand Down Expand Up @@ -159,3 +179,43 @@ impl StatusFromAntMiner for RPCCommandStatus {
Ok(Self::Success)
}
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn object_parameters_become_top_level_flags() {
// The firmware only honours `new_api` at the top level; nested under
// `parameter` it is ignored and the legacy payload comes back.
assert_eq!(
build_rpc_request("stats", Some(json!({"new_api": true}))),
json!({"command": "stats", "new_api": true})
);
}

#[test]
fn scalar_parameters_keep_the_cgminer_wrapper() {
// cgminer's own convention, e.g. `switchpool` with a pool index.
assert_eq!(
build_rpc_request("switchpool", Some(json!("1"))),
json!({"command": "switchpool", "parameter": "1"})
);
}

#[test]
fn absent_parameters_send_the_bare_command() {
assert_eq!(
build_rpc_request("version", None),
json!({"command": "version"})
);
}

#[test]
fn command_wins_over_a_conflicting_parameter_key() {
assert_eq!(
build_rpc_request("stats", Some(json!({"command": "evil"}))),
json!({"command": "stats"})
);
}
}
42 changes: 34 additions & 8 deletions asic-rs-firmwares/antminer/src/backends/v2023_07/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -485,14 +485,37 @@ impl GetDataLocations for AntMinerV202307 {
tag: None,
},
)],
DataField::Wattage => vec![(
RPC_STATS,
DataExtractor {
func: get_by_pointer,
key: Some("/STATS/1"),
tag: None,
},
)],
DataField::Wattage => {
// Newer stock firmware reports power draw only in the `new_api`
// variant of `stats`. That is a different payload to the legacy
// one: its `STATS` array holds a single element, so the reading
// sits at `/STATS/0` rather than the `/STATS/1` used above.
//
// Not a `const` because `json!` is not const-evaluable.
let rpc_new_stats = MinerCommand::RPC {
command: "stats",
parameters: Some(json!({ "new_api": true })),
};

vec![
(
RPC_STATS,
DataExtractor {
func: get_by_pointer,
key: Some("/STATS/1"),
tag: None,
},
),
(
rpc_new_stats,
DataExtractor {
func: get_by_pointer,
key: Some("/STATS/0"),
tag: None,
},
),
]
}
DataField::SerialNumber => vec![
(
WEB_SYSTEM_INFO,
Expand Down Expand Up @@ -812,6 +835,9 @@ impl GetWattage for AntMinerV202307 {
if let Some(power) = stats_data
.get("power")
.or_else(|| stats_data.get("Power"))
// Same firmware version spells this differently per model: the
// L9 reports `power`, the L11 `watt`.
.or_else(|| stats_data.get("watt"))
.and_then(|v| v.as_f64())
{
return Some(Power::from_watts(power));
Expand Down
80 changes: 70 additions & 10 deletions asic-rs-firmwares/antminer/src/backends/v2023_07/rpc.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,35 @@ use asic_rs_core::{
use async_trait::async_trait;
use serde_json::{Value, json};

/// Build the JSON request for a cgminer-style RPC call.
///
/// Bitmain's API extensions — `new_api` being the one this backend uses — are
/// top-level flags alongside `command`, not values of cgminer's `parameter`
/// argument. Wrapping them makes the firmware ignore the flag and silently
/// answer with the legacy payload, which is indistinguishable from a
/// successful call.
///
/// An object is always one of those extensions; cgminer's own convention
/// passes a scalar (e.g. `switchpool`'s pool index), so scalars keep the
/// `parameter` wrapper.
fn build_rpc_request(command: &str, parameters: Option<Value>) -> Value {
match parameters {
Some(Value::Object(params)) => {
let mut request = params;
// Inserted last so a stray `command` key cannot displace it.
request.insert("command".to_string(), Value::from(command));
Value::Object(request)
}
Some(params) => json!({
"command": command,
"parameter": params
}),
None => json!({
"command": command
}),
}
}

#[derive(Debug)]
pub struct AntMinerRPCAPI {
ip: IpAddr,
Expand All @@ -28,16 +57,7 @@ impl AntMinerRPCAPI {
_privileged: bool,
parameters: Option<Value>,
) -> anyhow::Result<Value> {
let request = if let Some(params) = parameters {
json!({
"command": command,
"parameter": params
})
} else {
json!({
"command": command
})
};
let request = build_rpc_request(command, parameters);

let json_str = request.to_string();
let message = format!("{}\n", json_str);
Expand Down Expand Up @@ -159,3 +179,43 @@ impl StatusFromAntMiner for RPCCommandStatus {
Ok(Self::Success)
}
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn object_parameters_become_top_level_flags() {
// The firmware only honours `new_api` at the top level; nested under
// `parameter` it is ignored and the legacy payload comes back.
assert_eq!(
build_rpc_request("stats", Some(json!({"new_api": true}))),
json!({"command": "stats", "new_api": true})
);
}

#[test]
fn scalar_parameters_keep_the_cgminer_wrapper() {
// cgminer's own convention, e.g. `switchpool` with a pool index.
assert_eq!(
build_rpc_request("switchpool", Some(json!("1"))),
json!({"command": "switchpool", "parameter": "1"})
);
}

#[test]
fn absent_parameters_send_the_bare_command() {
assert_eq!(
build_rpc_request("version", None),
json!({"command": "version"})
);
}

#[test]
fn command_wins_over_a_conflicting_parameter_key() {
assert_eq!(
build_rpc_request("stats", Some(json!({"command": "evil"}))),
json!({"command": "stats"})
);
}
}