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, SafetyLimits, 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 pub(crate) fn current_token(&self) -> Option<String> {
210 if let Some(token) = self.auth.cached_token() {
211 return Some(token);
212 }
213 if self.auth.wants_store_token() {
214 let auth_url = self.auth.auth_url()?;
215 return self.auth.resolve_token_blocking(&auth_url, None);
216 }
217 None
218 }
219
220 fn send_once(
224 &self,
225 base: &str,
226 req: &HttpRequest,
227 token: Option<&str>,
228 ) -> Result<(u16, Option<String>, Value)> {
229 let url = format!("{}{}", base, req.path);
230 let mut r = match req.method {
231 Method::Get => self.agent.request("GET", &url),
232 Method::Post => self.agent.request("POST", &url),
233 Method::Put => self.agent.request("PUT", &url),
234 };
235 match req.timeout {
236 Timeout::Default => r = r.timeout(self.default_timeout),
237 Timeout::After(d) => r = r.timeout(d),
238 Timeout::Unbounded => {}
239 }
240 if let Some(token) = token {
241 r = r.set("Authorization", &format!("Bearer {token}"));
242 }
243 let outcome = match (&req.method, &req.body) {
244 (Method::Post | Method::Put, Some(body)) => r.send_json(body.clone()),
245 _ => r.call(),
246 };
247 let resp = match outcome {
248 Ok(resp) => resp,
249 Err(ureq::Error::Status(_, resp)) => resp,
252 Err(ureq::Error::Transport(t)) => {
253 let msg = t.to_string();
254 return if msg.to_ascii_lowercase().contains("timed out")
255 || msg.to_ascii_lowercase().contains("timeout")
256 {
257 Err(Error::Timeout(msg))
258 } else {
259 Err(Error::Connection(msg))
260 };
261 }
262 };
263 let status = resp.status();
264 let gateway = if auth::is_denial(status) {
265 resp.header(auth::DISCOVERY_HEADER).map(str::to_string)
266 } else {
267 None
268 };
269 let body: Value = match resp.into_json() {
270 Ok(body) => body,
271 Err(e) if status < 400 => {
272 return Err(Error::Decode(format!("non-JSON response: {e}")))
273 }
274 Err(_) => Value::Null,
276 };
277 if gateway.is_none() && status >= 400 && body.is_null() {
278 return Err(Error::Box {
279 status,
280 message: format!("HTTP {status} (non-JSON body)"),
281 });
282 }
283 Ok((status, gateway, body))
284 }
285
286 pub(crate) fn run<T>(&self, op: Op<T>) -> Result<T> {
288 let (status, body) = self.execute(&op.req)?;
289 let resp = wire::parse_command(status, body)?;
290 (op.parse)(resp)
291 }
292
293 pub(crate) fn stream_debug(
296 &self,
297 req: &HttpRequest,
298 ) -> Result<Box<dyn std::io::Read + Send + Sync>> {
299 let token = self.current_token();
300 match self.stream_debug_once(req, token.as_deref()) {
301 Err(Error::AuthRequired {
305 box_host,
306 auth_url,
307 message,
308 }) if !self.auth.has_static_token() => {
309 match self
310 .auth
311 .resolve_token_blocking(&auth_url, token.as_deref())
312 {
313 Some(fresh) => self.stream_debug_once(req, Some(&fresh)),
314 None => Err(Error::AuthRequired {
315 box_host,
316 auth_url,
317 message,
318 }),
319 }
320 }
321 other => other,
322 }
323 }
324
325 fn stream_debug_once(
326 &self,
327 req: &HttpRequest,
328 token: Option<&str>,
329 ) -> Result<Box<dyn std::io::Read + Send + Sync>> {
330 let url = format!("{}{}", self.debug_base, req.path);
331 let mut r = self.agent.request("POST", &url);
332 match req.timeout {
333 Timeout::Default => r = r.timeout(self.default_timeout),
334 Timeout::After(d) => r = r.timeout(d),
335 Timeout::Unbounded => {}
336 }
337 if let Some(token) = token {
338 r = r.set("Authorization", &format!("Bearer {token}"));
339 }
340 let outcome = match &req.body {
341 Some(body) => r.send_json(body.clone()),
342 None => r.call(),
343 };
344 match outcome {
345 Ok(resp) => Ok(resp.into_reader()),
346 Err(ureq::Error::Status(status, resp)) => {
347 if auth::is_denial(status) {
348 if let Some(auth_url) = resp.header(auth::DISCOVERY_HEADER) {
349 let auth_url = auth_url.to_string();
350 self.auth.learn_auth_server(&auth_url);
351 return Err(auth::denial_error(
352 status,
353 self.auth.box_host(),
354 &auth_url,
355 token.is_some(),
356 ));
357 }
358 }
359 let body: Value = resp.into_json().unwrap_or(Value::Null);
360 Err(wire::parse_debug(status, body).unwrap_err())
361 }
362 Err(ureq::Error::Transport(t)) => Err(Error::Connection(t.to_string())),
363 }
364 }
365
366 pub(crate) fn debug_net_record(&self, name: &str) -> Result<Value> {
369 let records = self.nets_raw()?;
370 crate::nets::debug::find_debug_record(records, name)
371 }
372
373 fn nets_raw(&self) -> Result<Vec<Value>> {
376 let body = match self.get_json("/nets/list") {
377 Ok(body) => body,
378 Err(primary) => self.get_json("/uart/nets/list").map_err(|_| primary)?,
379 };
380 Ok(wire::nets_list_values(body))
381 }
382
383 fn get_json(&self, path: &str) -> Result<Value> {
384 let (status, body) = self.execute(&wire::get(path))?;
385 if status != 200 {
386 return Err(Error::Box {
387 status,
388 message: body
389 .get("error")
390 .and_then(Value::as_str)
391 .unwrap_or("request failed")
392 .to_string(),
393 });
394 }
395 Ok(body)
396 }
397
398 pub fn nets(&self) -> Result<Vec<NetRecord>> {
405 match self.get_json("/nets/list") {
406 Ok(body) => wire::nets_from_body(body),
407 Err(primary) => match self.get_json("/uart/nets/list") {
408 Ok(body) => wire::nets_from_body(body),
409 Err(_) => Err(primary),
410 },
411 }
412 }
413
414 pub fn health(&self) -> Result<Health> {
416 let body = self.get_json("/health")?;
417 serde_json::from_value(body).map_err(Into::into)
418 }
419
420 pub fn status(&self) -> Result<BoxStatus> {
422 let body = self.get_json("/status")?;
423 serde_json::from_value(body).map_err(Into::into)
424 }
425
426 pub fn usb_devices(&self) -> Result<Vec<UsbDeviceInfo>> {
435 self.usb_devices_matching(&UsbDeviceFilter::default())
436 }
437
438 pub fn usb_devices_matching(&self, filter: &UsbDeviceFilter) -> Result<Vec<UsbDeviceInfo>> {
441 match self.execute(&wire::usb_devices(filter)) {
442 Ok((status, body)) => wire::parse_usb_devices(status, body),
443 Err(e) => Err(wire::map_route_missing(e, wire::usb_devices_unsupported)),
444 }
445 }
446
447 fn lock_call(&self, req: &HttpRequest) -> Result<BoxLock> {
450 match self.execute(req) {
451 Ok((status, body)) => wire::parse_lock(status, body),
452 Err(e) => Err(wire::map_route_missing(e, wire::lock_unsupported)),
453 }
454 }
455
456 pub fn lock_status(&self) -> Result<BoxLock> {
458 self.lock_call(&wire::lock_status())
459 }
460
461 pub fn lock(&self, user: &str) -> Result<BoxLock> {
466 self.lock_call(&wire::lock_acquire(user, "user", None))
467 }
468
469 pub fn lock_with(
473 &self,
474 user: &str,
475 holder_type: &str,
476 ttl_seconds: Option<u64>,
477 ) -> Result<BoxLock> {
478 self.lock_call(&wire::lock_acquire(user, holder_type, ttl_seconds))
479 }
480
481 pub fn lock_heartbeat(&self, user: &str) -> Result<BoxLock> {
484 self.lock_call(&wire::lock_heartbeat(user))
485 }
486
487 pub fn unlock(&self, user: &str) -> Result<()> {
491 self.lock_call(&wire::unlock(user, false)).map(|_| ())
492 }
493
494 pub fn unlock_force(&self, user: &str) -> Result<()> {
497 self.lock_call(&wire::unlock(user, true)).map(|_| ())
498 }
499
500 pub fn lock_guard(&self, user: impl Into<String>) -> Result<BoxLockGuard<'_>> {
504 let user = user.into();
505 self.lock(&user)?;
506 Ok(BoxLockGuard { client: self, user, released: false })
507 }
508
509 pub fn set_safety_limits(
526 &self,
527 name: &str,
528 limits: &SafetyLimits,
529 ) -> Result<Option<SafetyLimits>> {
530 match self.execute(&wire::safety_limits_set(name, limits)) {
531 Ok((status, body)) => wire::parse_safety_limits(status, body),
532 Err(e) => Err(wire::map_route_missing(e, wire::safety_limits_unsupported)),
533 }
534 }
535
536 pub fn clear_safety_limits(&self, name: &str) -> Result<()> {
538 self.set_safety_limits(name, &SafetyLimits::default())
539 .map(|_| ())
540 }
541
542 pub fn safety_limits(&self, name: &str) -> Result<Option<SafetyLimits>> {
546 let nets = self.nets()?;
547 nets.iter()
548 .find(|rec| rec.name == name)
549 .map(|rec| rec.safety_limits)
550 .ok_or_else(|| Error::Box {
551 status: 404,
552 message: format!("no saved net named '{name}' on this box"),
553 })
554 }
555
556 pub fn supply(&self, name: impl Into<String>) -> Supply<'_> {
560 Supply { client: self, name: name.into() }
561 }
562
563 pub fn battery(&self, name: impl Into<String>) -> Battery<'_> {
565 Battery { client: self, name: name.into() }
566 }
567
568 pub fn eload(&self, name: impl Into<String>) -> Eload<'_> {
570 Eload { client: self, name: name.into() }
571 }
572
573 pub fn solar(&self, name: impl Into<String>) -> Solar<'_> {
575 Solar { client: self, name: name.into() }
576 }
577
578 pub fn gpio(&self, name: impl Into<String>) -> Gpio<'_> {
580 Gpio { client: self, name: name.into() }
581 }
582
583 pub fn adc(&self, name: impl Into<String>) -> Adc<'_> {
585 Adc { client: self, name: name.into() }
586 }
587
588 pub fn dac(&self, name: impl Into<String>) -> Dac<'_> {
590 Dac { client: self, name: name.into() }
591 }
592
593 pub fn thermocouple(&self, name: impl Into<String>) -> Thermocouple<'_> {
595 Thermocouple { client: self, name: name.into() }
596 }
597
598 pub fn watt_meter(&self, name: impl Into<String>) -> WattMeter<'_> {
600 WattMeter { client: self, name: name.into() }
601 }
602
603 pub fn energy_analyzer(&self, name: impl Into<String>) -> EnergyAnalyzer<'_> {
605 EnergyAnalyzer { client: self, name: name.into() }
606 }
607
608 pub fn spi(&self, name: impl Into<String>) -> Spi<'_> {
610 Spi { client: self, name: name.into() }
611 }
612
613 pub fn i2c(&self, name: impl Into<String>) -> I2c<'_> {
615 I2c { client: self, name: name.into() }
616 }
617
618 pub fn usb(&self, name: impl Into<String>) -> UsbPort<'_> {
620 UsbPort { client: self, name: name.into() }
621 }
622
623 pub fn arm(&self, name: impl Into<String>) -> Arm<'_> {
625 Arm { client: self, name: name.into() }
626 }
627
628 pub fn webcam(&self, name: impl Into<String>) -> Webcam<'_> {
630 Webcam { client: self, name: name.into() }
631 }
632
633 pub fn router(&self, name: impl Into<String>) -> Router<'_> {
635 Router { client: self, name: name.into() }
636 }
637
638 pub fn ble(&self) -> Ble<'_> {
640 Ble { client: self }
641 }
642
643 pub fn wifi(&self) -> Wifi<'_> {
645 Wifi { client: self }
646 }
647
648 pub fn blufi(&self) -> Blufi<'_> {
650 Blufi { client: self }
651 }
652
653 pub fn dfu(&self) -> Dfu<'_> {
655 Dfu { client: self }
656 }
657
658 pub fn debug(&self, name: impl Into<String>) -> DebugNet<'_> {
661 DebugNet { client: self, name: name.into(), record: Default::default() }
662 }
663
664 pub fn scope(&self, name: impl Into<String>) -> Scope {
666 Scope::new(name)
667 }
668
669 #[cfg(feature = "uart")]
674 pub fn uart(&self, name: impl Into<String>) -> Result<crate::nets::uart::Uart> {
675 crate::nets::uart::Uart::open(&self.base, name.into(), self.current_token())
676 }
677}
678
679pub struct BoxLockGuard<'a> {
683 client: &'a LagerBox,
684 user: String,
685 released: bool,
686}
687
688impl BoxLockGuard<'_> {
689 pub fn user(&self) -> &str {
691 &self.user
692 }
693
694 pub fn unlock(mut self) -> Result<()> {
696 self.released = true;
697 self.client.unlock(&self.user)
698 }
699}
700
701impl Drop for BoxLockGuard<'_> {
702 fn drop(&mut self) {
703 if !self.released {
704 let _ = self.client.unlock(&self.user);
705 }
706 }
707}