1use std::time::Duration;
4
5use serde_json::Value;
6
7use crate::auth::{self, GatewayAuth};
8use crate::error::{Error, Result};
9use crate::nets::adc::Adc;
10use crate::nets::arm::Arm;
11use crate::nets::battery::Battery;
12use crate::nets::ble::Ble;
13use crate::nets::blufi::Blufi;
14use crate::nets::dac::Dac;
15use crate::nets::debug::DebugNet;
16use crate::nets::dfu::Dfu;
17use crate::nets::eload::Eload;
18use crate::nets::energy::EnergyAnalyzer;
19use crate::nets::gpio::Gpio;
20use crate::nets::i2c::I2c;
21use crate::nets::router::Router;
22use crate::nets::scope::Scope;
23use crate::nets::solar::Solar;
24use crate::nets::spi::Spi;
25use crate::nets::supply::Supply;
26use crate::nets::thermocouple::Thermocouple;
27use crate::nets::usb::UsbPort;
28use crate::nets::watt::WattMeter;
29use crate::nets::webcam::Webcam;
30use crate::nets::wifi::Wifi;
31use crate::wire::{
32 self, BoxLock, BoxStatus, Health, HttpRequest, Method, NetRecord, Op, Timeout,
33 UsbDeviceFilter, UsbDeviceInfo,
34};
35use crate::BOX_HOST_ENV;
36
37pub struct LagerBox {
57 base: String,
58 debug_base: String,
59 agent: ureq::Agent,
60 default_timeout: Duration,
61 auth: GatewayAuth,
62}
63
64pub struct LagerBoxBuilder {
67 host: String,
68 debug_url: Option<String>,
69 default_timeout: Duration,
70 bearer_token: Option<String>,
71}
72
73impl LagerBoxBuilder {
74 pub fn timeout(mut self, timeout: Duration) -> Self {
78 self.default_timeout = timeout;
79 self
80 }
81
82 pub fn debug_service_url(mut self, url: impl Into<String>) -> Self {
87 self.debug_url = Some(url.into());
88 self
89 }
90
91 pub fn bearer_token(mut self, token: impl Into<String>) -> Self {
101 self.bearer_token = Some(token.into());
102 self
103 }
104
105 pub fn build(self) -> Result<LagerBox> {
107 let base = wire::base_url(&self.host)?;
108 let debug_base = match self.debug_url {
109 Some(url) => wire::base_url_with_port(&url, wire::DEBUG_SERVICE_PORT)?,
110 None => wire::service_base(&base, wire::DEBUG_SERVICE_PORT),
111 };
112 let auth = GatewayAuth::new(&base, self.bearer_token);
113 Ok(LagerBox {
114 base,
115 debug_base,
116 agent: ureq::AgentBuilder::new().build(),
117 default_timeout: self.default_timeout,
118 auth,
119 })
120 }
121}
122
123impl LagerBox {
124 pub fn connect(host: impl Into<String>) -> Result<Self> {
127 Self::builder(host).build()
128 }
129
130 pub fn from_env() -> Result<Self> {
133 let host = std::env::var(BOX_HOST_ENV)
134 .map_err(|_| Error::Config(format!("{BOX_HOST_ENV} is not set")))?;
135 Self::connect(host)
136 }
137
138 pub fn builder(host: impl Into<String>) -> LagerBoxBuilder {
140 LagerBoxBuilder {
141 host: host.into(),
142 debug_url: std::env::var(crate::DEBUG_SERVICE_URL_ENV).ok(),
143 default_timeout: wire::DEFAULT_TIMEOUT,
144 bearer_token: None,
145 }
146 }
147
148 pub fn base_url(&self) -> &str {
150 &self.base
151 }
152
153 pub(crate) fn execute(&self, req: &HttpRequest) -> Result<(u16, Value)> {
157 self.execute_at(&self.base, req)
158 }
159
160 pub(crate) fn execute_debug(&self, req: &HttpRequest) -> Result<(u16, Value)> {
162 self.execute_at(&self.debug_base, req)
163 }
164
165 fn execute_at(&self, base: &str, req: &HttpRequest) -> Result<(u16, Value)> {
170 let token = self.current_token();
171 let (status, gateway, resp_body) = self.send_once(base, req, token.as_deref())?;
172
173 let Some(auth_url) = gateway else {
174 return Ok((status, resp_body));
175 };
176 self.auth.learn_auth_server(&auth_url);
179 if status == 401 && !self.auth.has_static_token() {
180 if let Some(fresh) = self
181 .auth
182 .resolve_token_blocking(&auth_url, token.as_deref())
183 {
184 let (status, gateway, resp_body) =
185 self.send_once(base, req, Some(&fresh))?;
186 let Some(auth_url) = gateway else {
187 return Ok((status, resp_body));
188 };
189 return Err(auth::denial_error(
190 status,
191 self.auth.box_host(),
192 &auth_url,
193 true,
194 ));
195 }
196 }
197 Err(auth::denial_error(
198 status,
199 self.auth.box_host(),
200 &auth_url,
201 token.is_some(),
202 ))
203 }
204
205 fn current_token(&self) -> Option<String> {
208 if let Some(token) = self.auth.cached_token() {
209 return Some(token);
210 }
211 if self.auth.wants_store_token() {
212 let auth_url = self.auth.auth_url()?;
213 return self.auth.resolve_token_blocking(&auth_url, None);
214 }
215 None
216 }
217
218 fn send_once(
222 &self,
223 base: &str,
224 req: &HttpRequest,
225 token: Option<&str>,
226 ) -> Result<(u16, Option<String>, Value)> {
227 let url = format!("{}{}", base, req.path);
228 let mut r = match req.method {
229 Method::Get => self.agent.request("GET", &url),
230 Method::Post => self.agent.request("POST", &url),
231 };
232 match req.timeout {
233 Timeout::Default => r = r.timeout(self.default_timeout),
234 Timeout::After(d) => r = r.timeout(d),
235 Timeout::Unbounded => {}
236 }
237 if let Some(token) = token {
238 r = r.set("Authorization", &format!("Bearer {token}"));
239 }
240 let outcome = match (&req.method, &req.body) {
241 (Method::Post, Some(body)) => r.send_json(body.clone()),
242 _ => r.call(),
243 };
244 let resp = match outcome {
245 Ok(resp) => resp,
246 Err(ureq::Error::Status(_, resp)) => resp,
249 Err(ureq::Error::Transport(t)) => {
250 let msg = t.to_string();
251 return if msg.to_ascii_lowercase().contains("timed out")
252 || msg.to_ascii_lowercase().contains("timeout")
253 {
254 Err(Error::Timeout(msg))
255 } else {
256 Err(Error::Connection(msg))
257 };
258 }
259 };
260 let status = resp.status();
261 let gateway = if auth::is_denial(status) {
262 resp.header(auth::DISCOVERY_HEADER).map(str::to_string)
263 } else {
264 None
265 };
266 let body: Value = match resp.into_json() {
267 Ok(body) => body,
268 Err(e) if status < 400 => {
269 return Err(Error::Decode(format!("non-JSON response: {e}")))
270 }
271 Err(_) => Value::Null,
273 };
274 if gateway.is_none() && status >= 400 && body.is_null() {
275 return Err(Error::Box {
276 status,
277 message: format!("HTTP {status} (non-JSON body)"),
278 });
279 }
280 Ok((status, gateway, body))
281 }
282
283 pub(crate) fn run<T>(&self, op: Op<T>) -> Result<T> {
285 let (status, body) = self.execute(&op.req)?;
286 let resp = wire::parse_command(status, body)?;
287 (op.parse)(resp)
288 }
289
290 pub(crate) fn stream_debug(
293 &self,
294 req: &HttpRequest,
295 ) -> Result<Box<dyn std::io::Read + Send + Sync>> {
296 let token = self.current_token();
297 match self.stream_debug_once(req, token.as_deref()) {
298 Err(Error::AuthRequired {
302 box_host,
303 auth_url,
304 message,
305 }) if !self.auth.has_static_token() => {
306 match self
307 .auth
308 .resolve_token_blocking(&auth_url, token.as_deref())
309 {
310 Some(fresh) => self.stream_debug_once(req, Some(&fresh)),
311 None => Err(Error::AuthRequired {
312 box_host,
313 auth_url,
314 message,
315 }),
316 }
317 }
318 other => other,
319 }
320 }
321
322 fn stream_debug_once(
323 &self,
324 req: &HttpRequest,
325 token: Option<&str>,
326 ) -> Result<Box<dyn std::io::Read + Send + Sync>> {
327 let url = format!("{}{}", self.debug_base, req.path);
328 let mut r = self.agent.request("POST", &url);
329 match req.timeout {
330 Timeout::Default => r = r.timeout(self.default_timeout),
331 Timeout::After(d) => r = r.timeout(d),
332 Timeout::Unbounded => {}
333 }
334 if let Some(token) = token {
335 r = r.set("Authorization", &format!("Bearer {token}"));
336 }
337 let outcome = match &req.body {
338 Some(body) => r.send_json(body.clone()),
339 None => r.call(),
340 };
341 match outcome {
342 Ok(resp) => Ok(resp.into_reader()),
343 Err(ureq::Error::Status(status, resp)) => {
344 if auth::is_denial(status) {
345 if let Some(auth_url) = resp.header(auth::DISCOVERY_HEADER) {
346 let auth_url = auth_url.to_string();
347 self.auth.learn_auth_server(&auth_url);
348 return Err(auth::denial_error(
349 status,
350 self.auth.box_host(),
351 &auth_url,
352 token.is_some(),
353 ));
354 }
355 }
356 let body: Value = resp.into_json().unwrap_or(Value::Null);
357 Err(wire::parse_debug(status, body).unwrap_err())
358 }
359 Err(ureq::Error::Transport(t)) => Err(Error::Connection(t.to_string())),
360 }
361 }
362
363 pub(crate) fn debug_net_record(&self, name: &str) -> Result<Value> {
366 let records = self.nets_raw()?;
367 crate::nets::debug::find_debug_record(records, name)
368 }
369
370 fn nets_raw(&self) -> Result<Vec<Value>> {
373 let body = match self.get_json("/nets/list") {
374 Ok(body) => body,
375 Err(primary) => self.get_json("/uart/nets/list").map_err(|_| primary)?,
376 };
377 Ok(wire::nets_list_values(body))
378 }
379
380 fn get_json(&self, path: &str) -> Result<Value> {
381 let (status, body) = self.execute(&wire::get(path))?;
382 if status != 200 {
383 return Err(Error::Box {
384 status,
385 message: body
386 .get("error")
387 .and_then(Value::as_str)
388 .unwrap_or("request failed")
389 .to_string(),
390 });
391 }
392 Ok(body)
393 }
394
395 pub fn nets(&self) -> Result<Vec<NetRecord>> {
402 match self.get_json("/nets/list") {
403 Ok(body) => wire::nets_from_body(body),
404 Err(primary) => match self.get_json("/uart/nets/list") {
405 Ok(body) => wire::nets_from_body(body),
406 Err(_) => Err(primary),
407 },
408 }
409 }
410
411 pub fn health(&self) -> Result<Health> {
413 let body = self.get_json("/health")?;
414 serde_json::from_value(body).map_err(Into::into)
415 }
416
417 pub fn status(&self) -> Result<BoxStatus> {
419 let body = self.get_json("/status")?;
420 serde_json::from_value(body).map_err(Into::into)
421 }
422
423 pub fn usb_devices(&self) -> Result<Vec<UsbDeviceInfo>> {
432 self.usb_devices_matching(&UsbDeviceFilter::default())
433 }
434
435 pub fn usb_devices_matching(&self, filter: &UsbDeviceFilter) -> Result<Vec<UsbDeviceInfo>> {
438 match self.execute(&wire::usb_devices(filter)) {
439 Ok((status, body)) => wire::parse_usb_devices(status, body),
440 Err(e) => Err(wire::map_route_missing(e, wire::usb_devices_unsupported)),
441 }
442 }
443
444 fn lock_call(&self, req: &HttpRequest) -> Result<BoxLock> {
447 match self.execute(req) {
448 Ok((status, body)) => wire::parse_lock(status, body),
449 Err(e) => Err(wire::map_route_missing(e, wire::lock_unsupported)),
450 }
451 }
452
453 pub fn lock_status(&self) -> Result<BoxLock> {
455 self.lock_call(&wire::lock_status())
456 }
457
458 pub fn lock(&self, user: &str) -> Result<BoxLock> {
463 self.lock_call(&wire::lock_acquire(user, "user", None))
464 }
465
466 pub fn lock_with(
470 &self,
471 user: &str,
472 holder_type: &str,
473 ttl_seconds: Option<u64>,
474 ) -> Result<BoxLock> {
475 self.lock_call(&wire::lock_acquire(user, holder_type, ttl_seconds))
476 }
477
478 pub fn lock_heartbeat(&self, user: &str) -> Result<BoxLock> {
481 self.lock_call(&wire::lock_heartbeat(user))
482 }
483
484 pub fn unlock(&self, user: &str) -> Result<()> {
488 self.lock_call(&wire::unlock(user, false)).map(|_| ())
489 }
490
491 pub fn unlock_force(&self, user: &str) -> Result<()> {
494 self.lock_call(&wire::unlock(user, true)).map(|_| ())
495 }
496
497 pub fn lock_guard(&self, user: impl Into<String>) -> Result<BoxLockGuard<'_>> {
501 let user = user.into();
502 self.lock(&user)?;
503 Ok(BoxLockGuard { client: self, user, released: false })
504 }
505
506 pub fn supply(&self, name: impl Into<String>) -> Supply<'_> {
510 Supply { client: self, name: name.into() }
511 }
512
513 pub fn battery(&self, name: impl Into<String>) -> Battery<'_> {
515 Battery { client: self, name: name.into() }
516 }
517
518 pub fn eload(&self, name: impl Into<String>) -> Eload<'_> {
520 Eload { client: self, name: name.into() }
521 }
522
523 pub fn solar(&self, name: impl Into<String>) -> Solar<'_> {
525 Solar { client: self, name: name.into() }
526 }
527
528 pub fn gpio(&self, name: impl Into<String>) -> Gpio<'_> {
530 Gpio { client: self, name: name.into() }
531 }
532
533 pub fn adc(&self, name: impl Into<String>) -> Adc<'_> {
535 Adc { client: self, name: name.into() }
536 }
537
538 pub fn dac(&self, name: impl Into<String>) -> Dac<'_> {
540 Dac { client: self, name: name.into() }
541 }
542
543 pub fn thermocouple(&self, name: impl Into<String>) -> Thermocouple<'_> {
545 Thermocouple { client: self, name: name.into() }
546 }
547
548 pub fn watt_meter(&self, name: impl Into<String>) -> WattMeter<'_> {
550 WattMeter { client: self, name: name.into() }
551 }
552
553 pub fn energy_analyzer(&self, name: impl Into<String>) -> EnergyAnalyzer<'_> {
555 EnergyAnalyzer { client: self, name: name.into() }
556 }
557
558 pub fn spi(&self, name: impl Into<String>) -> Spi<'_> {
560 Spi { client: self, name: name.into() }
561 }
562
563 pub fn i2c(&self, name: impl Into<String>) -> I2c<'_> {
565 I2c { client: self, name: name.into() }
566 }
567
568 pub fn usb(&self, name: impl Into<String>) -> UsbPort<'_> {
570 UsbPort { client: self, name: name.into() }
571 }
572
573 pub fn arm(&self, name: impl Into<String>) -> Arm<'_> {
575 Arm { client: self, name: name.into() }
576 }
577
578 pub fn webcam(&self, name: impl Into<String>) -> Webcam<'_> {
580 Webcam { client: self, name: name.into() }
581 }
582
583 pub fn router(&self, name: impl Into<String>) -> Router<'_> {
585 Router { client: self, name: name.into() }
586 }
587
588 pub fn ble(&self) -> Ble<'_> {
590 Ble { client: self }
591 }
592
593 pub fn wifi(&self) -> Wifi<'_> {
595 Wifi { client: self }
596 }
597
598 pub fn blufi(&self) -> Blufi<'_> {
600 Blufi { client: self }
601 }
602
603 pub fn dfu(&self) -> Dfu<'_> {
605 Dfu { client: self }
606 }
607
608 pub fn debug(&self, name: impl Into<String>) -> DebugNet<'_> {
611 DebugNet { client: self, name: name.into(), record: Default::default() }
612 }
613
614 pub fn scope(&self, name: impl Into<String>) -> Scope {
616 Scope::new(name)
617 }
618
619 #[cfg(feature = "uart")]
624 pub fn uart(&self, name: impl Into<String>) -> Result<crate::nets::uart::Uart> {
625 crate::nets::uart::Uart::open(&self.base, name.into(), self.current_token())
626 }
627}
628
629pub struct BoxLockGuard<'a> {
633 client: &'a LagerBox,
634 user: String,
635 released: bool,
636}
637
638impl BoxLockGuard<'_> {
639 pub fn user(&self) -> &str {
641 &self.user
642 }
643
644 pub fn unlock(mut self) -> Result<()> {
646 self.released = true;
647 self.client.unlock(&self.user)
648 }
649}
650
651impl Drop for BoxLockGuard<'_> {
652 fn drop(&mut self) {
653 if !self.released {
654 let _ = self.client.unlock(&self.user);
655 }
656 }
657}