chalkydri/api/
mod.rs

1//!
2//! JSON API used by the web UI and possibly third-party applications
3//!
4
5use 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
83/// Run the API server
84pub 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/// Get Chalkydri's version and system information
119#[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/// List possible configurations
147#[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    //for cam in cam_man.devices() {
161    //    if let Some(cameras) = &mut cfgg.cameras {
162    //        if cameras.iter().filter(|c| c.id == cam.id).next().is_none() {
163    //            cameras.push(cam);
164    //        }
165    //    } else {
166    //        cfgg.cameras = Some(Vec::new());
167    //        if let Some(cameras) = &mut cfgg.cameras {
168    //            cameras.push(cam);
169    //        }
170    //    }
171    //}
172
173    web::Json(cfgg)
174}
175
176/// Set the configuration without saving it to the disk
177#[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/// Save the configuration to disk
203#[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/// Calibrate the given camera's intrinsic parameters
222#[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    //let cam_man = data.get_ref();
235    //let calibrated_model = cam_man
236    //    .calibrators()
237    //    .await
238    //    .get_mut(&cam_name)
239    //    .unwrap()
240    //    .calibrate()
241    //    .unwrap();
242    //{
243    //    let json = serde_json::to_value(calibrated_model).unwrap();
244    //    let cfgg = &mut (*Cfg.write().await);
245    //    if let Some(cams) = &mut cfgg.cameras {
246    //        (*cams)
247    //            .iter_mut()
248    //            .filter(|cam| cam.id == cam_name)
249    //            .next()
250    //            .unwrap()
251    //            .calib = Some(json);
252    //    }
253    //}
254
255    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/// Complete a calibration step for the given camera
284#[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; //cam_man.calib_step(cam_name).await;
298
299    web::Json(CalibrationStatus {
300        width: 1280,
301        height: 720,
302        current_step,
303        total_steps: 200,
304    })
305}
306
307/// Restart Chalkydri
308#[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    //data.restart().await;
316
317    HttpResponse::Ok().await.unwrap()
318}
319
320/// Restart the system
321#[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/// Power off the system
334#[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/// Get system information
353#[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 an MJPEG camera stream for the given camera
372#[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}