use std::time::Duration;
use serde_json::Value;
use crate::auth::{self, GatewayAuth};
use crate::error::{Error, Result};
use crate::nets::adc::AsyncAdc;
use crate::nets::arm::AsyncArm;
use crate::nets::battery::AsyncBattery;
use crate::nets::ble::AsyncBle;
use crate::nets::blufi::AsyncBlufi;
use crate::nets::dac::AsyncDac;
use crate::nets::debug::AsyncDebugNet;
use crate::nets::eload::AsyncEload;
use crate::nets::energy::AsyncEnergyAnalyzer;
use crate::nets::gpio::AsyncGpio;
use crate::nets::i2c::AsyncI2c;
use crate::nets::router::AsyncRouter;
use crate::nets::scope::Scope;
use crate::nets::solar::AsyncSolar;
use crate::nets::spi::AsyncSpi;
use crate::nets::supply::AsyncSupply;
use crate::nets::thermocouple::AsyncThermocouple;
use crate::nets::usb::AsyncUsbPort;
use crate::nets::watt::AsyncWattMeter;
use crate::nets::webcam::AsyncWebcam;
use crate::nets::wifi::AsyncWifi;
use crate::wire::{self, BoxStatus, Health, HttpRequest, Method, NetRecord, Op, Timeout};
pub struct AsyncLagerBox {
base: String,
debug_base: String,
http: reqwest::Client,
default_timeout: Duration,
auth: GatewayAuth,
}
pub struct AsyncLagerBoxBuilder {
host: String,
debug_url: Option<String>,
default_timeout: Duration,
bearer_token: Option<String>,
}
impl AsyncLagerBoxBuilder {
pub fn timeout(mut self, timeout: Duration) -> Self {
self.default_timeout = timeout;
self
}
pub fn debug_service_url(mut self, url: impl Into<String>) -> Self {
self.debug_url = Some(url.into());
self
}
pub fn bearer_token(mut self, token: impl Into<String>) -> Self {
self.bearer_token = Some(token.into());
self
}
pub fn build(self) -> Result<AsyncLagerBox> {
let base = wire::base_url(&self.host)?;
let debug_base = match self.debug_url {
Some(url) => wire::base_url_with_port(&url, wire::DEBUG_SERVICE_PORT)?,
None => wire::service_base(&base, wire::DEBUG_SERVICE_PORT),
};
let auth = GatewayAuth::new(&base, self.bearer_token);
Ok(AsyncLagerBox {
base,
debug_base,
http: reqwest::Client::new(),
default_timeout: self.default_timeout,
auth,
})
}
}
impl AsyncLagerBox {
pub fn connect(host: impl Into<String>) -> Result<Self> {
Self::builder(host).build()
}
pub fn from_env() -> Result<Self> {
let host = std::env::var(crate::BOX_HOST_ENV)
.map_err(|_| Error::Config(format!("{} is not set", crate::BOX_HOST_ENV)))?;
Self::connect(host)
}
pub fn builder(host: impl Into<String>) -> AsyncLagerBoxBuilder {
AsyncLagerBoxBuilder {
host: host.into(),
debug_url: std::env::var(crate::DEBUG_SERVICE_URL_ENV).ok(),
default_timeout: wire::DEFAULT_TIMEOUT,
bearer_token: None,
}
}
pub fn base_url(&self) -> &str {
&self.base
}
pub(crate) async fn execute(&self, req: &HttpRequest) -> Result<(u16, Value)> {
self.execute_at(&self.base, req).await
}
pub(crate) async fn execute_debug(&self, req: &HttpRequest) -> Result<(u16, Value)> {
self.execute_at(&self.debug_base, req).await
}
async fn execute_at(&self, base: &str, req: &HttpRequest) -> Result<(u16, Value)> {
let token = self.current_token().await;
let (status, gateway, resp_body) = self.send_once(base, req, token.as_deref()).await?;
let Some(auth_url) = gateway else {
return Ok((status, resp_body));
};
self.auth.learn_auth_server(&auth_url);
if status == 401 && !self.auth.has_static_token() {
if let Some(fresh) = self
.auth
.resolve_token_async(&auth_url, token.as_deref())
.await
{
let (status, gateway, resp_body) =
self.send_once(base, req, Some(&fresh)).await?;
let Some(auth_url) = gateway else {
return Ok((status, resp_body));
};
return Err(auth::denial_error(
status,
self.auth.box_host(),
&auth_url,
true,
));
}
}
Err(auth::denial_error(
status,
self.auth.box_host(),
&auth_url,
token.is_some(),
))
}
async fn current_token(&self) -> Option<String> {
if let Some(token) = self.auth.cached_token() {
return Some(token);
}
if self.auth.wants_store_token() {
let auth_url = self.auth.auth_url()?;
return self.auth.resolve_token_async(&auth_url, None).await;
}
None
}
async fn send_once(
&self,
base: &str,
req: &HttpRequest,
token: Option<&str>,
) -> Result<(u16, Option<String>, Value)> {
let url = format!("{}{}", base, req.path);
let mut r = match req.method {
Method::Get => self.http.get(&url),
Method::Post => self.http.post(&url),
};
match req.timeout {
Timeout::Default => r = r.timeout(self.default_timeout),
Timeout::After(d) => r = r.timeout(d),
Timeout::Unbounded => {}
}
if let Some(token) = token {
r = r.header("Authorization", format!("Bearer {token}"));
}
if let Some(body) = &req.body {
r = r.json(body);
}
let resp = r.send().await.map_err(|e| {
if e.is_timeout() {
Error::Timeout(e.to_string())
} else {
Error::Connection(e.to_string())
}
})?;
let status = resp.status().as_u16();
let gateway = if auth::is_denial(status) {
resp.headers()
.get(auth::DISCOVERY_HEADER)
.and_then(|v| v.to_str().ok())
.map(str::to_string)
} else {
None
};
let body: Value = match resp.json().await {
Ok(body) => body,
Err(e) if e.is_timeout() => return Err(Error::Timeout(e.to_string())),
Err(e) if status < 400 => {
return Err(Error::Decode(format!("non-JSON response: {e}")))
}
Err(_) => Value::Null,
};
Ok((status, gateway, body))
}
pub(crate) async fn run<T>(&self, op: Op<T>) -> Result<T> {
let (status, body) = self.execute(&op.req).await?;
let resp = wire::parse_command(status, body)?;
(op.parse)(resp)
}
pub(crate) async fn debug_net_record(&self, name: &str) -> Result<Value> {
let records = self.nets_raw().await?;
crate::nets::debug::find_debug_record(records, name)
}
async fn nets_raw(&self) -> Result<Vec<Value>> {
let body = match self.get_json("/nets/list").await {
Ok(body) => body,
Err(primary) => self.get_json("/uart/nets/list").await.map_err(|_| primary)?,
};
Ok(wire::nets_list_values(body))
}
async fn get_json(&self, path: &str) -> Result<Value> {
let (status, body) = self.execute(&wire::get(path)).await?;
if status != 200 {
return Err(Error::Box {
status,
message: body
.get("error")
.and_then(Value::as_str)
.unwrap_or("request failed")
.to_string(),
});
}
Ok(body)
}
pub async fn nets(&self) -> Result<Vec<NetRecord>> {
match self.get_json("/nets/list").await {
Ok(body) => wire::nets_from_body(body),
Err(primary) => match self.get_json("/uart/nets/list").await {
Ok(body) => wire::nets_from_body(body),
Err(_) => Err(primary),
},
}
}
pub async fn health(&self) -> Result<Health> {
let body = self.get_json("/health").await?;
serde_json::from_value(body).map_err(Into::into)
}
pub async fn status(&self) -> Result<BoxStatus> {
let body = self.get_json("/status").await?;
serde_json::from_value(body).map_err(Into::into)
}
pub fn supply(&self, name: impl Into<String>) -> AsyncSupply<'_> {
AsyncSupply { client: self, name: name.into() }
}
pub fn battery(&self, name: impl Into<String>) -> AsyncBattery<'_> {
AsyncBattery { client: self, name: name.into() }
}
pub fn eload(&self, name: impl Into<String>) -> AsyncEload<'_> {
AsyncEload { client: self, name: name.into() }
}
pub fn solar(&self, name: impl Into<String>) -> AsyncSolar<'_> {
AsyncSolar { client: self, name: name.into() }
}
pub fn gpio(&self, name: impl Into<String>) -> AsyncGpio<'_> {
AsyncGpio { client: self, name: name.into() }
}
pub fn adc(&self, name: impl Into<String>) -> AsyncAdc<'_> {
AsyncAdc { client: self, name: name.into() }
}
pub fn dac(&self, name: impl Into<String>) -> AsyncDac<'_> {
AsyncDac { client: self, name: name.into() }
}
pub fn thermocouple(&self, name: impl Into<String>) -> AsyncThermocouple<'_> {
AsyncThermocouple { client: self, name: name.into() }
}
pub fn watt_meter(&self, name: impl Into<String>) -> AsyncWattMeter<'_> {
AsyncWattMeter { client: self, name: name.into() }
}
pub fn energy_analyzer(&self, name: impl Into<String>) -> AsyncEnergyAnalyzer<'_> {
AsyncEnergyAnalyzer { client: self, name: name.into() }
}
pub fn spi(&self, name: impl Into<String>) -> AsyncSpi<'_> {
AsyncSpi { client: self, name: name.into() }
}
pub fn i2c(&self, name: impl Into<String>) -> AsyncI2c<'_> {
AsyncI2c { client: self, name: name.into() }
}
pub fn usb(&self, name: impl Into<String>) -> AsyncUsbPort<'_> {
AsyncUsbPort { client: self, name: name.into() }
}
pub fn arm(&self, name: impl Into<String>) -> AsyncArm<'_> {
AsyncArm { client: self, name: name.into() }
}
pub fn webcam(&self, name: impl Into<String>) -> AsyncWebcam<'_> {
AsyncWebcam { client: self, name: name.into() }
}
pub fn router(&self, name: impl Into<String>) -> AsyncRouter<'_> {
AsyncRouter { client: self, name: name.into() }
}
pub fn ble(&self) -> AsyncBle<'_> {
AsyncBle { client: self }
}
pub fn wifi(&self) -> AsyncWifi<'_> {
AsyncWifi { client: self }
}
pub fn blufi(&self) -> AsyncBlufi<'_> {
AsyncBlufi { client: self }
}
pub fn debug(&self, name: impl Into<String>) -> AsyncDebugNet<'_> {
AsyncDebugNet { client: self, name: name.into() }
}
pub fn scope(&self, name: impl Into<String>) -> Scope {
Scope::new(name)
}
}