1pub 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
31pub struct ServeOpts {
33 pub host: String,
36 pub port: u16,
37 pub password: Option<String>,
40 pub fake: bool,
42 pub interval: Option<Duration>,
43 pub external_cameras: Vec<ExternalCamera>,
48}
49
50pub 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 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}