1use actix_web::{
6 App, HttpResponse, HttpServer, Responder, get,
7 http::{
8 StatusCode,
9 header::{self, CacheDirective},
10 },
11 post, put,
12 web::{self, Data},
13};
14use mime_guess::from_path;
15use rust_embed::Embed;
16use rustix::system::RebootCommand;
17use sysinfo::System;
18use utopia::{OpenApi, ToSchema};
19
20use crate::{
21 Cfg,
22 cameras::CamManager,
23 config::{self, Config},
24};
25
26#[derive(OpenApi)]
27#[openapi(
28 info(
29 title = "Chalkydri Manager API",
30 version = env!("CARGO_PKG_VERSION"),
31 ),
32 paths(
33 info,
34 configuration,
35 configure,
36 save_configuration,
37 calibration_intrinsics,
38 calibration_status,
39 calibration_step,
40 restart,
41 sys_reboot,
42 sys_shutdown,
43 )
44)]
45#[allow(dead_code)]
46struct ApiDoc;
47
48#[derive(rust_embed::Embed)]
49#[folder = "../../ui/build/"]
50struct Assets;
51
52fn handle_embedded_file(path: &str) -> HttpResponse {
53 match <Assets as Embed>::get(path) {
54 Some(content) => HttpResponse::Ok()
55 .content_type(from_path(path).first_or_octet_stream().as_ref())
56 .body(content.data.into_owned()),
57 None => HttpResponse::NotFound().body("404 Not Found"),
58 }
59}
60
61#[get("/")]
62async fn index() -> impl Responder {
63 handle_embedded_file("index.html")
64}
65
66#[get("/api/openapi.json")]
67async fn openapi_json() -> impl Responder {
68 web::Json(ApiDoc::openapi())
69}
70
71#[get("/{_:.*}")]
72async fn dist(path: web::Path<String>) -> impl Responder {
73 if Assets::get(path.as_str()).is_some() {
74 handle_embedded_file(path.as_str()).map_into_boxed_body()
75 } else {
76 HttpResponse::TemporaryRedirect()
77 .insert_header(("Location", "/"))
78 .body(())
79 .map_into_boxed_body()
80 }
81}
82
83pub async fn run_api(cam_man: CamManager) {
85 HttpServer::new(move || {
86 App::new()
87 .app_data(Data::new(cam_man.clone()))
88 .service(index)
89 .service(info)
90 .service(configuration)
91 .service(configure)
92 .service(save_configuration)
93 .service(calibration_intrinsics)
94 .service(calibration_status)
95 .service(calibration_step)
96 .service(restart)
97 .service(sys_reboot)
98 .service(sys_shutdown)
99 .service(openapi_json)
100 .service(stream)
101 .service(dist)
102 })
103 .bind(("0.0.0.0", 6942))
104 .unwrap()
105 .run()
106 .await
107 .unwrap();
108}
109
110#[derive(Serialize, ToSchema)]
111pub struct Info {
112 pub version: &'static str,
113 pub cpu_usage: u8,
114 pub mem_usage: u8,
115 pub new_cams: Vec<config::Camera>,
116}
117
118#[utopia::path(
120 responses(
121 (status = 200, body = Info),
122 ),
123)]
124#[get("/api/info")]
125pub(super) async fn info(data: web::Data<CamManager>) -> impl Responder {
126 let mut system = System::new();
127 system.refresh_cpu_usage();
128 system.refresh_memory();
129
130 let cpu_usage = (system.global_cpu_usage() * 100.0) as u8;
131 let mem_usage = ((system.used_memory() as f64 / system.total_memory() as f64) * 100.0) as u8;
132
133 let mut new_cams = Vec::new();
134 while let Ok(cam) = data.new_dev_rx.lock().await.try_recv() {
135 new_cams.push(cam);
136 }
137
138 web::Json(Info {
139 version: env!("CARGO_PKG_VERSION"),
140 cpu_usage,
141 mem_usage,
142 new_cams,
143 })
144}
145
146#[utopia::path(
148 responses(
149 (status = 200, body = Config),
150 ),
151)]
152#[get("/api/configuration")]
153pub(super) async fn configuration(data: web::Data<CamManager>) -> impl Responder {
154 let cam_man = data.get_ref();
155
156 cam_man.refresh_devices().await;
157
158 let cfgg = Cfg.read().await.clone();
159
160 web::Json(cfgg)
174}
175
176#[utopia::path(
178 responses(
179 (status = 200, body = Config),
180 ),
181)]
182#[post("/api/configuration")]
183pub(super) async fn configure(
184 data: web::Data<CamManager>,
185 web::Json(cfgg): web::Json<Config>,
186) -> impl Responder {
187 let cam_man = data.get_ref();
188
189 *Cfg.write().await = cfgg;
190 cam_man.refresh_devices().await;
191
192 let cfgg = Cfg.read().await.clone();
193 if let Some(cams) = cfgg.cameras {
194 for cam in cams {
195 cam_man.update_pipeline(cam.id.clone()).await;
196 }
197 }
198
199 web::Json(Cfg.read().await.clone())
200}
201
202#[utopia::path(
204 responses(
205 (status = 200, body = Config),
206 ),
207)]
208#[put("/api/configuration")]
209pub(super) async fn save_configuration(web::Json(cfgg): web::Json<Config>) -> impl Responder {
210 let old_config = Cfg.read().await.clone();
211
212 if cfgg.device_name != old_config.device_name {
213 rustix::system::sethostname(cfgg.device_name.clone().unwrap().as_bytes()).unwrap();
214 }
215
216 cfgg.save("chalkydri.toml").await.unwrap();
217
218 web::Json(cfgg)
219}
220
221#[utopia::path(
223 responses(
224 (status = 200),
225 ),
226)]
227#[get("/api/calibrate/{cam_name}/intrinsics")]
228pub(super) async fn calibration_intrinsics(
229 path: web::Path<String>,
230 data: web::Data<CamManager>,
231) -> impl Responder {
232 let cam_name = path.to_string();
233
234 HttpResponse::new(StatusCode::OK)
256}
257
258#[derive(Serialize, ToSchema)]
259struct CalibrationStatus {
260 width: u32,
261 height: u32,
262 current_step: usize,
263 total_steps: usize,
264}
265
266#[utopia::path(
267 responses(
268 (status = 200, body = CalibrationStatus),
269 ),
270)]
271#[get("/api/calibrate/status")]
272pub(super) async fn calibration_status(data: web::Data<CamManager>) -> impl Responder {
273 let cam_man = data.get_ref();
274
275 web::Json(CalibrationStatus {
276 width: 1280,
277 height: 720,
278 current_step: 0,
279 total_steps: 200,
280 })
281}
282
283#[utopia::path(
285 responses(
286 (status = 200),
287 ),
288)]
289#[get("/api/calibrate/{cam_name}/step")]
290pub(super) async fn calibration_step(
291 path: web::Path<String>,
292 data: web::Data<CamManager>,
293) -> impl Responder {
294 let cam_name = path.to_string();
295
296 let cam_man = data.get_ref();
297 let current_step = 0; web::Json(CalibrationStatus {
300 width: 1280,
301 height: 720,
302 current_step,
303 total_steps: 200,
304 })
305}
306
307#[utopia::path(
309 responses(
310 (status = 200),
311 )
312)]
313#[post("/api/restart")]
314pub(super) async fn restart(data: web::Data<CamManager>) -> impl Responder {
315 HttpResponse::Ok().await.unwrap()
318}
319
320#[utopia::path(
322 responses(
323 (status = 200),
324 )
325)]
326#[post("/api/sys/reboot")]
327pub(super) async fn sys_reboot() -> impl Responder {
328 rustix::system::reboot(RebootCommand::Restart).unwrap();
329
330 web::Json(())
331}
332
333#[utopia::path(
335 responses(
336 (status = 200),
337 )
338)]
339#[post("/api/sys/shutdown")]
340pub(super) async fn sys_shutdown() -> impl Responder {
341 rustix::system::reboot(RebootCommand::PowerOff).unwrap();
342
343 web::Json(())
344}
345
346#[derive(Serialize)]
347struct SysInfo {
348 uptime: u64,
349 mem_usage: u8,
350}
351
352#[utopia::path(
354 responses(
355 (status = 200),
356 )
357)]
358#[get("/api/sys/info")]
359pub(super) async fn sys_info() -> impl Responder {
360 let sysinfo = rustix::system::sysinfo();
361 let mut system = sysinfo::System::new();
362 system.refresh_cpu_usage();
363
364 let uptime = sysinfo.uptime as u64;
365 let mem_usage =
366 (((sysinfo.totalram - sysinfo.freeram) as f32 / sysinfo.totalram as f32) * 100.0) as u8;
367
368 web::Json(SysInfo { uptime, mem_usage })
369}
370
371#[get("/stream/{cam_name}")]
373pub(super) async fn stream(path: web::Path<String>, data: web::Data<CamManager>) -> impl Responder {
374 let cam_name = path.clone();
375
376 println!("{cam_name}");
377
378 let mjpeg_stream = data.pipelines.read().await.get(&cam_name).unwrap().mjpeg_preproc.inner().clone();
379 drop(data);
380
381 HttpResponse::Ok()
382 .append_header(header::CacheControl(vec![CacheDirective::NoCache]))
383 .append_header((header::PRAGMA, "no-cache"))
384 .append_header((header::EXPIRES, 0))
385 .append_header((header::CONNECTION, "close"))
386 .append_header((
387 header::CONTENT_TYPE,
388 "multipart/x-mixed-replace; boundary=frame",
389 ))
390 .streaming(mjpeg_stream.clone())
391}