asic_rs/miners/backends/espminer/v2_0_0/
web.rs

1use crate::miners::{
2    api::{APIClient, WebAPIClient},
3    commands::MinerCommand,
4};
5use anyhow::{Result, anyhow};
6use async_trait::async_trait;
7use reqwest::{Client, Method, Response};
8use serde_json::Value;
9use std::{net::IpAddr, time::Duration};
10use tokio::time::timeout;
11
12/// ESPMiner WebAPI client for communicating with BitAxe and similar miners
13#[derive(Debug)]
14pub struct ESPMinerWebAPI {
15    client: Client,
16    pub ip: IpAddr,
17    port: u16,
18    timeout: Duration,
19    retries: u32,
20}
21
22#[async_trait]
23#[allow(dead_code)]
24trait ESPMiner200WebAPI: WebAPIClient {
25    /// Get system information
26    async fn system_info(&self) -> Result<Value> {
27        self.send_command("system/info", false, None, Method::GET)
28            .await
29    }
30
31    /// Get swarm information
32    async fn swarm_info(&self) -> Result<Value> {
33        self.send_command("swarm/info", false, None, Method::GET)
34            .await
35    }
36
37    /// Restart the system
38    async fn restart(&self) -> Result<Value> {
39        self.send_command("system/restart", false, None, Method::POST)
40            .await
41    }
42
43    /// Update system settings
44    async fn update_settings(&self, config: Value) -> Result<Value> {
45        self.send_command("system", false, Some(config), Method::PATCH)
46            .await
47    }
48}
49
50#[async_trait]
51impl APIClient for ESPMinerWebAPI {
52    async fn get_api_result(&self, command: &MinerCommand) -> Result<Value> {
53        match command {
54            MinerCommand::WebAPI {
55                command,
56                parameters,
57            } => self
58                .send_command(command, false, parameters.clone(), Method::GET)
59                .await
60                .map_err(|e| anyhow!(e.to_string())),
61            _ => Err(anyhow!("Cannot send non web command to web API")),
62        }
63    }
64}
65
66#[async_trait]
67impl WebAPIClient for ESPMinerWebAPI {
68    /// Send a command to the miner
69    async fn send_command(
70        &self,
71        command: &str,
72        _privileged: bool,
73        parameters: Option<Value>,
74        method: Method,
75    ) -> Result<Value> {
76        let url = format!("http://{}:{}/api/{}", self.ip, self.port, command);
77
78        for attempt in 0..=self.retries {
79            let result = self
80                .execute_request(&url, &method, parameters.clone())
81                .await;
82
83            match result {
84                Ok(response) => {
85                    if response.status().is_success() {
86                        match response.json().await {
87                            Ok(json_data) => return Ok(json_data),
88                            Err(e) => {
89                                if attempt == self.retries {
90                                    return Err(ESPMinerError::ParseError(e.to_string()))?;
91                                }
92                            }
93                        }
94                    } else if attempt == self.retries {
95                        return Err(ESPMinerError::HttpError(response.status().as_u16()))?;
96                    }
97                }
98                Err(e) => {
99                    if attempt == self.retries {
100                        return Err(e)?;
101                    }
102                }
103            }
104        }
105
106        Err(ESPMinerError::MaxRetriesExceeded)?
107    }
108}
109
110impl ESPMiner200WebAPI for ESPMinerWebAPI {}
111
112impl ESPMinerWebAPI {
113    /// Create a new ESPMiner WebAPI client
114    pub fn new(ip: IpAddr, port: u16) -> Self {
115        let client = Client::builder()
116            .timeout(Duration::from_secs(10))
117            .build()
118            .expect("Failed to create HTTP client");
119
120        Self {
121            client,
122            ip,
123            port,
124            timeout: Duration::from_secs(5),
125            retries: 1,
126        }
127    }
128
129    /// Set the timeout for API requests
130    pub fn with_timeout(mut self, timeout: Duration) -> Self {
131        self.timeout = timeout;
132        self
133    }
134
135    /// Set the number of retries for failed requests
136    pub fn with_retries(mut self, retries: u32) -> Self {
137        self.retries = retries;
138        self
139    }
140
141    /// Execute the actual HTTP request
142    async fn execute_request(
143        &self,
144        url: &str,
145        method: &Method,
146        parameters: Option<Value>,
147    ) -> Result<Response, ESPMinerError> {
148        let request_builder = match *method {
149            Method::GET => self.client.get(url),
150            Method::POST => {
151                let mut builder = self.client.post(url);
152                if let Some(params) = parameters {
153                    builder = builder.json(&params);
154                }
155                builder
156            }
157            Method::PATCH => {
158                let mut builder = self.client.patch(url);
159                if let Some(params) = parameters {
160                    builder = builder.json(&params);
161                }
162                builder
163            }
164            _ => return Err(ESPMinerError::UnsupportedMethod(method.to_string())),
165        };
166
167        let request = request_builder
168            .timeout(self.timeout)
169            .build()
170            .map_err(|e| ESPMinerError::RequestError(e.to_string()))?;
171
172        let response = timeout(self.timeout, self.client.execute(request))
173            .await
174            .map_err(|_| ESPMinerError::Timeout)?
175            .map_err(|e| ESPMinerError::NetworkError(e.to_string()))?;
176        Ok(response)
177    }
178}
179
180/// Error types for ESPMiner WebAPI operations
181#[derive(Debug, Clone)]
182pub enum ESPMinerError {
183    /// Network error (connection issues, DNS resolution, etc.)
184    NetworkError(String),
185    /// HTTP error with status code
186    HttpError(u16),
187    /// JSON parsing error
188    ParseError(String),
189    /// Request building error
190    RequestError(String),
191    /// Timeout error
192    Timeout,
193    /// Unsupported HTTP method
194    UnsupportedMethod(String),
195    /// Maximum retries exceeded
196    MaxRetriesExceeded,
197    WebError,
198}
199
200impl std::fmt::Display for ESPMinerError {
201    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
202        match self {
203            ESPMinerError::NetworkError(msg) => write!(f, "Network error: {msg}"),
204            ESPMinerError::HttpError(code) => write!(f, "HTTP error: {code}"),
205            ESPMinerError::ParseError(msg) => write!(f, "Parse error: {msg}"),
206            ESPMinerError::RequestError(msg) => write!(f, "Request error: {msg}"),
207            ESPMinerError::Timeout => write!(f, "Request timeout"),
208            ESPMinerError::UnsupportedMethod(method) => write!(f, "Unsupported method: {method}"),
209            ESPMinerError::MaxRetriesExceeded => write!(f, "Maximum retries exceeded"),
210            ESPMinerError::WebError => write!(f, "Web error"),
211        }
212    }
213}
214
215impl std::error::Error for ESPMinerError {}
216
217// Usage example
218#[cfg(test)]
219mod tests {
220    /*
221    #[tokio::test]
222    async fn test_espminer_api() {
223        let api = EspWebApi::new("192.168.1.100".into(), 80)
224            .with_timeout(Duration::from_secs(5))
225            .with_retries(3);
226
227        // Test system info
228        match api.system_info().await {
229            Ok(info) => println!("System info: {:?}", info),
230            Err(e) => println!("Error getting system info: {}", e),
231        }
232    }
233     */
234}