use std::time::Duration;
use serde_json::Value;
use crate::auth::{self, GatewayAuth};
use crate::error::{Error, Result};
use crate::nets::adc::Adc;
use crate::nets::arm::Arm;
use crate::nets::battery::Battery;
use crate::nets::ble::Ble;
use crate::nets::blufi::Blufi;
use crate::nets::dac::Dac;
use crate::nets::debug::DebugNet;
use crate::nets::eload::Eload;
use crate::nets::energy::EnergyAnalyzer;
use crate::nets::gpio::Gpio;
use crate::nets::i2c::I2c;
use crate::nets::router::Router;
use crate::nets::scope::Scope;
use crate::nets::solar::Solar;
use crate::nets::spi::Spi;
use crate::nets::supply::Supply;
use crate::nets::thermocouple::Thermocouple;
use crate::nets::usb::UsbPort;
use crate::nets::watt::WattMeter;
use crate::nets::webcam::Webcam;
use crate::nets::wifi::Wifi;
use crate::wire::{self, BoxStatus, Health, HttpRequest, Method, NetRecord, Op, Timeout};
use crate::BOX_HOST_ENV;
pub struct LagerBox {
base: String,
debug_base: String,
agent: ureq::Agent,
default_timeout: Duration,
auth: GatewayAuth,
}
pub struct LagerBoxBuilder {
host: String,
debug_url: Option<String>,
default_timeout: Duration,
bearer_token: Option<String>,
}
impl LagerBoxBuilder {
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<LagerBox> {
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(LagerBox {
base,
debug_base,
agent: ureq::AgentBuilder::new().build(),
default_timeout: self.default_timeout,
auth,
})
}
}
impl LagerBox {
pub fn connect(host: impl Into<String>) -> Result<Self> {
Self::builder(host).build()
}
pub fn from_env() -> Result<Self> {
let host = std::env::var(BOX_HOST_ENV)
.map_err(|_| Error::Config(format!("{BOX_HOST_ENV} is not set")))?;
Self::connect(host)
}
pub fn builder(host: impl Into<String>) -> LagerBoxBuilder {
LagerBoxBuilder {
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) fn execute(&self, req: &HttpRequest) -> Result<(u16, Value)> {
self.execute_at(&self.base, req)
}
pub(crate) fn execute_debug(&self, req: &HttpRequest) -> Result<(u16, Value)> {
self.execute_at(&self.debug_base, req)
}
fn execute_at(&self, base: &str, req: &HttpRequest) -> Result<(u16, Value)> {
let token = self.current_token();
let (status, gateway, resp_body) = self.send_once(base, req, token.as_deref())?;
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_blocking(&auth_url, token.as_deref())
{
let (status, gateway, resp_body) =
self.send_once(base, req, Some(&fresh))?;
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(),
))
}
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_blocking(&auth_url, None);
}
None
}
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.agent.request("GET", &url),
Method::Post => self.agent.request("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.set("Authorization", &format!("Bearer {token}"));
}
let outcome = match (&req.method, &req.body) {
(Method::Post, Some(body)) => r.send_json(body.clone()),
_ => r.call(),
};
let resp = match outcome {
Ok(resp) => resp,
Err(ureq::Error::Status(_, resp)) => resp,
Err(ureq::Error::Transport(t)) => {
let msg = t.to_string();
return if msg.to_ascii_lowercase().contains("timed out")
|| msg.to_ascii_lowercase().contains("timeout")
{
Err(Error::Timeout(msg))
} else {
Err(Error::Connection(msg))
};
}
};
let status = resp.status();
let gateway = if auth::is_denial(status) {
resp.header(auth::DISCOVERY_HEADER).map(str::to_string)
} else {
None
};
let body: Value = match resp.into_json() {
Ok(body) => body,
Err(e) if status < 400 => {
return Err(Error::Decode(format!("non-JSON response: {e}")))
}
Err(_) => Value::Null,
};
if gateway.is_none() && status >= 400 && body.is_null() {
return Err(Error::Box {
status,
message: format!("HTTP {status} (non-JSON body)"),
});
}
Ok((status, gateway, body))
}
pub(crate) fn run<T>(&self, op: Op<T>) -> Result<T> {
let (status, body) = self.execute(&op.req)?;
let resp = wire::parse_command(status, body)?;
(op.parse)(resp)
}
pub(crate) fn stream_debug(
&self,
req: &HttpRequest,
) -> Result<Box<dyn std::io::Read + Send + Sync>> {
let token = self.current_token();
match self.stream_debug_once(req, token.as_deref()) {
Err(Error::AuthRequired {
box_host,
auth_url,
message,
}) if !self.auth.has_static_token() => {
match self
.auth
.resolve_token_blocking(&auth_url, token.as_deref())
{
Some(fresh) => self.stream_debug_once(req, Some(&fresh)),
None => Err(Error::AuthRequired {
box_host,
auth_url,
message,
}),
}
}
other => other,
}
}
fn stream_debug_once(
&self,
req: &HttpRequest,
token: Option<&str>,
) -> Result<Box<dyn std::io::Read + Send + Sync>> {
let url = format!("{}{}", self.debug_base, req.path);
let mut r = self.agent.request("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.set("Authorization", &format!("Bearer {token}"));
}
let outcome = match &req.body {
Some(body) => r.send_json(body.clone()),
None => r.call(),
};
match outcome {
Ok(resp) => Ok(resp.into_reader()),
Err(ureq::Error::Status(status, resp)) => {
if auth::is_denial(status) {
if let Some(auth_url) = resp.header(auth::DISCOVERY_HEADER) {
let auth_url = auth_url.to_string();
self.auth.learn_auth_server(&auth_url);
return Err(auth::denial_error(
status,
self.auth.box_host(),
&auth_url,
token.is_some(),
));
}
}
let body: Value = resp.into_json().unwrap_or(Value::Null);
Err(wire::parse_debug(status, body).unwrap_err())
}
Err(ureq::Error::Transport(t)) => Err(Error::Connection(t.to_string())),
}
}
pub(crate) fn debug_net_record(&self, name: &str) -> Result<Value> {
let records = self.nets_raw()?;
crate::nets::debug::find_debug_record(records, name)
}
fn nets_raw(&self) -> Result<Vec<Value>> {
let body = match self.get_json("/nets/list") {
Ok(body) => body,
Err(primary) => self.get_json("/uart/nets/list").map_err(|_| primary)?,
};
Ok(wire::nets_list_values(body))
}
fn get_json(&self, path: &str) -> Result<Value> {
let (status, body) = self.execute(&wire::get(path))?;
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 fn nets(&self) -> Result<Vec<NetRecord>> {
match self.get_json("/nets/list") {
Ok(body) => wire::nets_from_body(body),
Err(primary) => match self.get_json("/uart/nets/list") {
Ok(body) => wire::nets_from_body(body),
Err(_) => Err(primary),
},
}
}
pub fn health(&self) -> Result<Health> {
let body = self.get_json("/health")?;
serde_json::from_value(body).map_err(Into::into)
}
pub fn status(&self) -> Result<BoxStatus> {
let body = self.get_json("/status")?;
serde_json::from_value(body).map_err(Into::into)
}
pub fn supply(&self, name: impl Into<String>) -> Supply<'_> {
Supply { client: self, name: name.into() }
}
pub fn battery(&self, name: impl Into<String>) -> Battery<'_> {
Battery { client: self, name: name.into() }
}
pub fn eload(&self, name: impl Into<String>) -> Eload<'_> {
Eload { client: self, name: name.into() }
}
pub fn solar(&self, name: impl Into<String>) -> Solar<'_> {
Solar { client: self, name: name.into() }
}
pub fn gpio(&self, name: impl Into<String>) -> Gpio<'_> {
Gpio { client: self, name: name.into() }
}
pub fn adc(&self, name: impl Into<String>) -> Adc<'_> {
Adc { client: self, name: name.into() }
}
pub fn dac(&self, name: impl Into<String>) -> Dac<'_> {
Dac { client: self, name: name.into() }
}
pub fn thermocouple(&self, name: impl Into<String>) -> Thermocouple<'_> {
Thermocouple { client: self, name: name.into() }
}
pub fn watt_meter(&self, name: impl Into<String>) -> WattMeter<'_> {
WattMeter { client: self, name: name.into() }
}
pub fn energy_analyzer(&self, name: impl Into<String>) -> EnergyAnalyzer<'_> {
EnergyAnalyzer { client: self, name: name.into() }
}
pub fn spi(&self, name: impl Into<String>) -> Spi<'_> {
Spi { client: self, name: name.into() }
}
pub fn i2c(&self, name: impl Into<String>) -> I2c<'_> {
I2c { client: self, name: name.into() }
}
pub fn usb(&self, name: impl Into<String>) -> UsbPort<'_> {
UsbPort { client: self, name: name.into() }
}
pub fn arm(&self, name: impl Into<String>) -> Arm<'_> {
Arm { client: self, name: name.into() }
}
pub fn webcam(&self, name: impl Into<String>) -> Webcam<'_> {
Webcam { client: self, name: name.into() }
}
pub fn router(&self, name: impl Into<String>) -> Router<'_> {
Router { client: self, name: name.into() }
}
pub fn ble(&self) -> Ble<'_> {
Ble { client: self }
}
pub fn wifi(&self) -> Wifi<'_> {
Wifi { client: self }
}
pub fn blufi(&self) -> Blufi<'_> {
Blufi { client: self }
}
pub fn debug(&self, name: impl Into<String>) -> DebugNet<'_> {
DebugNet { client: self, name: name.into() }
}
pub fn scope(&self, name: impl Into<String>) -> Scope {
Scope::new(name)
}
#[cfg(feature = "uart")]
pub fn uart(&self, name: impl Into<String>) -> Result<crate::nets::uart::Uart> {
crate::nets::uart::Uart::open(&self.base, name.into(), self.current_token())
}
}