Skip to main content

bambu_rs/server/
mod.rs

1//! The embedded HTTP server — a monitoring + control API (axum) and, when the
2//! `dashboard` feature is on, the React SPA (embedded via `rust-embed`). Behind
3//! the `server` feature; another consumer of the library, like the CLI.
4//!
5//! Auth: reads are always open; writes (control) are gated by an optional
6//! password (`None` = open). The LAN access code stays server-side and never
7//! reaches a client.
8
9pub mod api;
10#[cfg(feature = "dashboard")]
11pub mod assets;
12pub mod camera;
13pub mod control;
14pub mod files;
15pub mod live;
16pub mod start;
17pub mod stream_record;
18pub mod timelapse;
19
20use std::sync::Arc;
21use std::time::Duration;
22
23use crate::config::ResolvedTarget;
24pub use api::{AppState, FakeSource, PrinterSource};
25pub use camera::{CameraSource, ExternalCamera, LiveCamera, NoCamera};
26pub use control::{Controller, FakeController, LiveController};
27pub use files::{FakeFiles, FileStore, LiveFiles};
28pub use live::LiveSource;
29pub use start::{FakeStarter, LiveStarter, Starter};
30
31/// Options for [`serve`].
32pub struct ServeOpts {
33    /// Bind host (default `127.0.0.1`). A non-loopback host serves over the
34    /// network; without a password, control is open — a warning is printed.
35    pub host: String,
36    pub port: u16,
37    /// Optional password gating **write** (control) requests. `None` = control is
38    /// open. Reads are always unauthenticated.
39    pub password: Option<String>,
40    /// Serve deterministic fake data instead of talking to a printer.
41    pub fake: bool,
42    pub interval: Option<Duration>,
43    /// External IP cameras to seed at launch (each a single-JPEG-per-GET URL with
44    /// a label). The server proxies them via `/api/camera/{id}/snapshot` so a
45    /// browser that can't reach the LAN cam (e.g. over Tailscale) still gets a live
46    /// view; the dashboard can add/remove more at runtime.
47    pub external_cameras: Vec<ExternalCamera>,
48}
49
50/// Run the server (blocking; owns its own multi-thread runtime).
51pub fn serve(target: Option<ResolvedTarget>, opts: ServeOpts) -> anyhow::Result<()> {
52    let rt = tokio::runtime::Builder::new_multi_thread()
53        .enable_all()
54        .build()?;
55    let ServeOpts {
56        host,
57        port,
58        password,
59        fake,
60        interval,
61        external_cameras,
62    } = opts;
63    let external_cameras = Arc::new(std::sync::RwLock::new(external_cameras));
64    rt.block_on(async move {
65        // Live mode bridges the real MQTT monitor (and controls the real device);
66        // otherwise serve a ramping fake so the UI still has moving data.
67        let state = match target {
68            Some(t) if !fake => {
69                eprintln!("connecting to the printer over LAN…");
70                AppState {
71                    source: Arc::new(LiveSource::connect(t.clone(), interval)),
72                    controller: Arc::new(LiveController::new(t.clone())),
73                    files: Arc::new(LiveFiles::new(t.clone())),
74                    starter: Arc::new(LiveStarter::new(t.clone())),
75                    password,
76                    start_lock: Arc::new(tokio::sync::Mutex::new(())),
77                    external_cameras: external_cameras.clone(),
78                    internal_camera: Arc::new(LiveCamera::new(t)),
79                    timelapse: Default::default(),
80                }
81            }
82            _ => {
83                if !fake {
84                    eprintln!(
85                        "note: no printer configured; serving fake data (pass --fake to silence)"
86                    );
87                }
88                let tick = interval.unwrap_or(Duration::from_secs(1));
89                AppState {
90                    source: Arc::new(FakeSource::ramping(tick)),
91                    controller: Arc::new(FakeController::verified()),
92                    files: Arc::new(FakeFiles),
93                    starter: Arc::new(FakeStarter),
94                    password,
95                    start_lock: Arc::new(tokio::sync::Mutex::new(())),
96                    external_cameras,
97                    internal_camera: Arc::new(NoCamera),
98                    timelapse: Default::default(),
99                }
100            }
101        };
102        let addr = format!("{host}:{port}");
103        let listener = tokio::net::TcpListener::bind(&addr)
104            .await
105            .map_err(|e| anyhow::anyhow!("binding {addr}: {e}"))?;
106        let loopback = host.starts_with("127.") || host == "localhost" || host == "::1";
107        if !loopback {
108            match &state.password {
109                Some(_) => eprintln!(
110                    "warning: serving on non-loopback {addr}; control requires the password, \
111                     reads are open."
112                ),
113                None => eprintln!(
114                    "warning: serving on non-loopback {addr} with no --password — control \
115                     (pause/stop/light/speed) is OPEN to anyone who can reach this address."
116                ),
117            }
118        }
119        eprintln!("bambu serve: http://{addr}/");
120        axum::serve(listener, api::router(state))
121            .await
122            .map_err(|e| anyhow::anyhow!("serving: {e}"))
123    })
124}