3.0 KiB · text History b85db9e
//! Normal-operation status server.
//!
//! Serves one HTML page over the station IP plus any number of JSON endpoints.
//! The crate supplies the style shell and the plumbing; *what* a device
//! reports is entirely the consumer's (`nfc-522` exposes recent card reads,
//! `lambent-flex` its UDP port map).
//!
//! Read-only by design — it accepts no config. Reconfiguration goes through
//! [`crate::portal`], which only runs in provisioning mode.

use anyhow::Result;
use esp_idf_svc::http::server::{Configuration, EspHttpServer};
use esp_idf_svc::http::Method;
use esp_idf_svc::io::Write as _;

use crate::assets::{Asset, H_JSON};
use crate::http::{serve_asset, serve_shell};

/// A status server under construction.
///
/// The built [`EspHttpServer`] **must be kept alive** — dropping it
/// unregisters every handler.
pub struct StatusServer {
    server: EspHttpServer<'static>,
}

impl StatusServer {
    /// Start a server serving `page` at `/`, plus the shared CSS and fonts.
    pub fn new(page: Asset) -> Result<Self> {
        let mut server = EspHttpServer::new(&Configuration::default())?;
        serve_asset(&mut server, "/", page)?;
        serve_shell(&mut server)?;
        Ok(Self { server })
    }

    /// Start a server with the shared CSS and fonts but **no page at `/`**.
    ///
    /// For firmware that wants to keep `/` for something else (an existing
    /// JSON API, say) and serve its status page elsewhere.
    pub fn bare() -> Result<Self> {
        let mut server = EspHttpServer::new(&Configuration::default())?;
        serve_shell(&mut server)?;
        Ok(Self { server })
    }

    /// Register a JSON endpoint.
    ///
    /// `body` is called per request and returns the encoded response. An error
    /// becomes a `500` rather than killing the handler, so a transient
    /// serialization failure cannot take the status page down.
    pub fn json<F>(mut self, uri: &str, body: F) -> Result<Self>
    where
        F: Fn() -> Result<Vec<u8>> + Send + 'static,
    {
        self.server.fn_handler(uri, Method::Get, move |req| {
            match body() {
                Ok(bytes) => {
                    req.into_response(200, Some("OK"), H_JSON)?
                        .write_all(&bytes)?;
                }
                Err(e) => {
                    log::warn!("status json handler failed: {e:#}");
                    req.into_response(500, Some("Internal Server Error"), &[])?;
                }
            }
            Ok::<(), anyhow::Error>(())
        })?;
        Ok(self)
    }

    /// Register an extra static asset (an icon, a second page, …).
    pub fn asset(mut self, uri: &str, asset: Asset) -> Result<Self> {
        serve_asset(&mut self.server, uri, asset)?;
        Ok(self)
    }

    /// Escape hatch for routes this builder does not cover.
    pub fn server_mut(&mut self) -> &mut EspHttpServer<'static> {
        &mut self.server
    }

    /// Finish, yielding the live server to be held for the program's lifetime.
    pub fn build(self) -> EspHttpServer<'static> {
        self.server
    }
}