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