1use std::{fs::File, io::Write, sync::Arc};
6
7use actix_web::{
8 App, HttpResponse, HttpServer, Responder,
9 body::BodyStream,
10 get,
11 http::{
12 StatusCode,
13 header::{self, CacheDirective},
14 },
15 post,
16 web::{self, Data},
17};
18use mime_guess::from_path;
19use rust_embed::Embed;
20use rustix::system::RebootCommand;
21use tokio::{io::BufStream, sync::watch};
22use utopia::{OpenApi, ToSchema};
23
24use crate::{
25 Cfg, Nt,
26 cameras::{self, CameraManager},
27 config::Config,
28};
29
30#[derive(OpenApi)]
31#[openapi(
32 info(title = "Chalkydri Manager API"),
33 paths(
34 info,
35 configuration,
36 configure,
37 calibration_intrinsics,
38 calibration_status,
39 calibration_step,
40 sys_info,
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: CameraManager) {
84 HttpServer::new(move || {
85 App::new()
86 .app_data(Data::new(cam_man.clone()))
87 .service(index)
88 .service(info)
89 .service(configuration)
90 .service(configure)
91 .service(calibration_intrinsics)
92 .service(calibration_status)
93 .service(calibration_step)
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}
112
113#[utopia::path(
115 responses(
116 (status = 200, body = Info),
117 ),
118)]
119#[get("/api/info")]
120pub(super) async fn info() -> impl Responder {
121 #[cfg(feature = "python")]
122 let sys = "rpi";
123
124 web::Json(Info {
125 version: env!("CARGO_PKG_VERSION"),
126 })
127}
128
129#[utopia::path(
131 responses(
132 (status = 200, body = Config),
133 ),
134)]
135#[get("/api/configuration")]
136pub(super) async fn configuration(data: web::Data<CameraManager>) -> impl Responder {
137 let cam_man = data.get_ref();
138
139 let mut cfgg = Cfg.read().await.clone();
140 for cam in cam_man.devices() {
141 if let Some(cameras) = &mut cfgg.cameras {
142 if cameras.iter().filter(|c| c.id == cam.id).next().is_none() {
143 cameras.push(cam);
144 }
145 } else {
146 cfgg.cameras = Some(Vec::new());
147 if let Some(cameras) = &mut cfgg.cameras {
148 cameras.push(cam);
149 }
150 }
151 }
152 web::Json(cfgg)
153}
154
155#[utopia::path(
157 responses(
158 (status = 200, body = Config),
159 ),
160)]
161#[post("/api/configuration")]
162pub(super) async fn configure(
163 data: web::Data<CameraManager>,
164 web::Json(cfgg): web::Json<Config>,
165) -> impl Responder {
166 let cam_man = data.get_ref();
167
168 let old_config = Cfg.read().await.clone();
169
170 if cfgg.device_name != old_config.device_name {
171 rustix::system::sethostname(cfgg.device_name.clone().unwrap().as_bytes()).unwrap();
172 }
173
174 for cam in cam_man.devices() {
179 cam_man.update_pipeline(cam.id.clone()).await;
186 }
187
188 {
189 *Cfg.write().await = cfgg.clone();
190
191 let mut f = File::create("chalkydri.toml").unwrap();
192 let toml_cfgg = toml::to_string_pretty(&cfgg).unwrap();
193 f.write_all(toml_cfgg.as_bytes()).unwrap();
194 f.flush().unwrap();
195 }
196
197 web::Json(Cfg.read().await.clone())
198}
199
200#[utopia::path(
201 responses(
202 (status = 200),
203 ),
204)]
205#[get("/api/calibrate/{cam_name}/intrinsics")]
206pub(super) async fn calibration_intrinsics(
207 path: web::Path<String>,
208 data: web::Data<CameraManager>,
209) -> impl Responder {
210 let cam_name = path.to_string();
211
212 let cam_man = data.get_ref();
213 let calibrated_model = cam_man
214 .calibrators()
215 .await
216 .get_mut(&cam_name)
217 .unwrap()
218 .calibrate()
219 .unwrap();
220 {
221 let json = serde_json::to_value(calibrated_model).unwrap();
222 let cfgg = &mut (*Cfg.write().await);
223 if let Some(cams) = &mut cfgg.cameras {
224 (*cams)
225 .iter_mut()
226 .filter(|cam| cam.id == cam_name)
227 .next()
228 .unwrap()
229 .calib = Some(json);
230 }
231 }
232
233 HttpResponse::new(StatusCode::OK)
234}
235
236#[derive(Serialize, ToSchema)]
237struct CalibrationStatus {
238 width: u32,
239 height: u32,
240 current_step: usize,
241 total_steps: usize,
242}
243
244#[utopia::path(
245 responses(
246 (status = 200, body = CalibrationStatus),
247 ),
248)]
249#[get("/api/calibrate/status")]
250pub(super) async fn calibration_status(data: web::Data<CameraManager>) -> impl Responder {
251 let cam_man = data.get_ref();
252
253 web::Json(CalibrationStatus {
254 width: 1280,
255 height: 720,
256 current_step: 0,
257 total_steps: 200,
258 })
259}
260
261#[utopia::path(
262 responses(
263 (status = 200),
264 ),
265)]
266#[get("/api/calibrate/{cam_name}/step")]
267pub(super) async fn calibration_step(
268 path: web::Path<String>,
269 data: web::Data<CameraManager>,
270) -> impl Responder {
271 let cam_name = path.to_string();
272
273 let cam_man = data.get_ref();
274 let current_step = cam_man.calib_step(cam_name).await;
275
276 web::Json(CalibrationStatus {
277 width: 1280,
278 height: 720,
279 current_step,
280 total_steps: 200,
281 })
282}
283
284#[utopia::path(
285 responses(
286 (status = 200),
287 )
288)]
289#[post("/api/sys/reboot")]
290pub(super) async fn sys_reboot() -> impl Responder {
291 rustix::system::reboot(RebootCommand::Restart).unwrap();
292
293 web::Json(())
294}
295
296#[utopia::path(
297 responses(
298 (status = 200),
299 )
300)]
301#[post("/api/sys/shutdown")]
302pub(super) async fn sys_shutdown() -> impl Responder {
303 rustix::system::reboot(RebootCommand::PowerOff).unwrap();
304
305 web::Json(())
306}
307
308#[derive(Serialize)]
309struct SysInfo {
310 uptime: u64,
311 mem_usage: u8,
312}
313
314#[utopia::path(
315 responses(
316 (status = 200),
317 )
318)]
319#[get("/api/sys/info")]
320pub(super) async fn sys_info() -> impl Responder {
321 let sysinfo = rustix::system::sysinfo();
322
323 let uptime = sysinfo.uptime as u64;
324 let mem_usage =
325 (((sysinfo.totalram - sysinfo.freeram) as f32 / sysinfo.totalram as f32) * 100.0) as u8;
326
327 web::Json(SysInfo { uptime, mem_usage })
328}
329
330#[get("/stream/{cam_name}")]
331pub(super) async fn stream(
332 path: web::Path<String>,
333 data: web::Data<CameraManager>,
334) -> impl Responder {
335 let cam_name = path.clone();
336
337 println!("{cam_name}");
338
339 if let Some(mjpeg_stream) = data.mjpeg_streams().await.get(&cam_name) {
340 HttpResponse::Ok()
341 .append_header(header::CacheControl(vec![CacheDirective::NoCache]))
342 .append_header((header::PRAGMA, "no-cache"))
343 .append_header((header::EXPIRES, 0))
344 .append_header((header::CONNECTION, "close"))
345 .append_header((
346 header::CONTENT_TYPE,
347 "multipart/x-mixed-replace; boundary=frame",
348 ))
349 .streaming(mjpeg_stream.clone())
350 } else {
351 HttpResponse::NotFound().await.unwrap()
352 }
353}