1use super::SystemModule;
7use crate::taskserver::Control;
8use crate::{config, taskserver};
9use anyhow::{Result, anyhow, bail, ensure};
10use chrono::{Local, NaiveTime};
11use image::{ImageFormat, imageops::FilterType};
12use log::{error, info, warn};
13use serde::{Deserialize, Serialize};
14use std::{
15 collections::BTreeMap,
16 io::Cursor,
17 os::linux::fs::MetadataExt,
18 path::{Path, PathBuf},
19};
20use tokio::{
21 fs::{self, File},
22 io::AsyncWriteExt,
23 process::Command,
24};
25
26const THUMB_POSTFIX: &str = "thumb";
28
29#[derive(Debug, Clone, Serialize, Deserialize)]
31pub struct CameraConfig {
32 enabled: bool,
34 debug_exec_once: bool,
36 fake_camera: bool,
39 pic_history_dir: String,
42 pic_archive_dir: String,
44 total_size_limit_mb: u32,
46 pub page_by: u32,
48}
49
50impl Default for CameraConfig {
51 fn default() -> Self {
52 Self {
53 enabled: false,
54 debug_exec_once: false,
55 fake_camera: true,
56 pic_history_dir: "./camera/history".to_string(),
57 pic_archive_dir: "./camera/archive".to_string(),
58 total_size_limit_mb: 1024,
59 page_by: 100,
60 }
61 }
62}
63
64#[derive(Clone)]
66pub struct PicEntry {
67 pub path_main: PathBuf,
69 pub path_th: PathBuf,
71 pub total_size: u64,
73}
74
75type PicDict = BTreeMap<String, PicEntry>;
79
80struct Storage {
82 pic_history_list: PicDict,
84 pic_archive_list: PicDict,
86}
87
88pub struct Camera {
90 pub config: CameraConfig,
94 wakeup_list: Vec<NaiveTime>,
96 storage: Storage,
98}
99
100impl Camera {
101 pub fn new(wakeup_list: Vec<NaiveTime>) -> Result<Self> {
105 info!("[camera] initialize");
106
107 let config = config::get(|cfg| cfg.camera.clone());
108 ensure!(config.page_by > 0);
109
110 let pic_history_list = init_pics(&config.pic_history_dir)?;
111 let pic_archive_list = init_pics(&config.pic_archive_dir)?;
112
113 Ok(Camera {
114 config,
115 wakeup_list,
116 storage: Storage {
117 pic_history_list,
118 pic_archive_list,
119 },
120 })
121 }
122
123 pub fn pic_list(&self) -> (&PicDict, &PicDict) {
125 (
126 &self.storage.pic_history_list,
127 &self.storage.pic_archive_list,
128 )
129 }
130
131 fn create_file_names(key: &str) -> (String, String) {
133 (format!("{key}.jpg"), format!("{key}_{THUMB_POSTFIX}.jpg"))
134 }
135
136 pub async fn push_pic_history(&mut self, img: &[u8], thumb: &[u8]) -> Result<()> {
143 let mut now;
146 let mut dtstr;
147 loop {
148 now = Local::now();
149 dtstr = now.format("%Y%m%d_%H%M%S").to_string();
150
151 if self.storage.pic_history_list.contains_key(&dtstr) {
152 tokio::time::sleep(tokio::time::Duration::from_secs(1)).await
153 } else {
154 break;
155 }
156 }
157 let (name_main, name_th) = Self::create_file_names(&dtstr);
158
159 let total_size = img.len() + thumb.len();
160 let total_size = total_size as u64;
161
162 let root = Path::new(&self.config.pic_history_dir);
164 let mut path_main = PathBuf::from(root);
165 path_main.push(name_main);
166 let mut path_th = PathBuf::from(root);
167 path_th.push(name_th);
168
169 {
171 info!("[camera-pic] write {}", path_main.display());
172 let mut file = File::create(&path_main).await?;
173 file.write_all(img).await?;
174 }
175 {
176 info!("[camera-pic] write {}", path_th.display());
177 let mut file = File::create(&path_th).await?;
178 file.write_all(thumb).await?;
179 }
180 let entry = PicEntry {
182 path_main,
183 path_th,
184 total_size,
185 };
186 assert!(self.storage.pic_history_list.insert(dtstr, entry).is_none());
187
188 Ok(())
189 }
190
191 pub async fn push_pic_archive(&mut self, key: &str) -> Result<()> {
195 let history = &self.storage.pic_history_list;
197 let archive = &mut self.storage.pic_archive_list;
198 let entry = history
199 .get(key)
200 .ok_or_else(|| anyhow!("picture not found: {}", key))?;
201
202 let (name_main, name_th) = Self::create_file_names(key);
204 let root = Path::new(&self.config.pic_archive_dir);
205 let mut path_main = PathBuf::from(root);
206 path_main.push(name_main);
207 let mut path_th = PathBuf::from(root);
208 path_th.push(name_th);
209
210 let main_size = fs::copy(&entry.path_main, &path_main).await?;
212 let th_size = fs::copy(&entry.path_th, &path_th).await?;
213
214 let entry = PicEntry {
216 path_main,
217 path_th,
218 total_size: main_size + th_size,
219 };
220 if archive.insert(key.to_string(), entry).is_some() {
221 warn!("[camera-pic] pic archive is overwritten: {key}");
222 }
223
224 Ok(())
225 }
226
227 pub async fn delete_pic_history(&mut self, id: &str) -> Result<()> {
228 Self::delete_pic(&mut self.storage.pic_history_list, id).await
229 }
230
231 pub async fn delete_pic_archive(&mut self, id: &str) -> Result<()> {
232 Self::delete_pic(&mut self.storage.pic_archive_list, id).await
233 }
234
235 async fn delete_pic(list: &mut PicDict, id: &str) -> Result<()> {
236 let entry = list
237 .remove(id)
238 .ok_or_else(|| anyhow!("picture not found: {}", id))?;
239
240 if let Err(why) = fs::remove_file(&entry.path_main).await {
241 error!("[camera] cannot remove {id} main: {why}");
242 }
243 if let Err(why) = fs::remove_file(&entry.path_th).await {
244 error!("[camera] cannot remove {id} thumb: {why}");
245 }
246 info!("[camera] deleted: {id}");
247
248 Ok(())
249 }
250
251 fn calc_total_size(list: &PicDict) -> u64 {
256 list.iter().fold(0, |acc, (_, entry)| {
257 acc.checked_add(entry.total_size).unwrap()
259 })
260 }
261
262 async fn clean_pic_history(&mut self) -> Result<()> {
264 info!("[camera] clean history");
265
266 let limit = self.config.total_size_limit_mb as u64 * 1024 * 1024;
267 let history = &mut self.storage.pic_history_list;
268
269 let mut total = Self::calc_total_size(history);
270 while total > limit {
271 info!("[camera] total: {total}, limit: {limit}");
272
273 let (id, entry) = history.pop_first().unwrap();
275 if let Err(why) = fs::remove_file(entry.path_main).await {
277 error!("[camera] cannot remove {id} main: {why}");
278 }
279 if let Err(why) = fs::remove_file(entry.path_th).await {
280 error!("[camera] cannot remove {id} thumb: {why}");
281 }
282 info!("[camera] deleted: {id}");
283
284 total -= entry.total_size;
285 }
286
287 assert!(total == Self::calc_total_size(history));
288 info!("[camera] clean history completed");
289 Ok(())
290 }
291
292 async fn auto_task(ctrl: Control) -> Result<()> {
294 let pic = take_a_pic(TakePicOption::new()).await?;
295
296 let thumb = create_thumbnail(&pic)?;
297
298 let mut camera = ctrl.sysmods().camera.lock().await;
299 camera.push_pic_history(&pic, &thumb).await?;
300 camera.clean_pic_history().await?;
301 drop(camera);
302
303 Ok(())
304 }
305}
306
307impl SystemModule for Camera {
308 fn on_start(&mut self, ctrl: &Control) {
312 info!("[camera] on_start");
313 if self.config.enabled {
314 if self.config.debug_exec_once {
315 taskserver::spawn_oneshot_task(ctrl, "camera-auto", Camera::auto_task);
316 } else {
317 taskserver::spawn_periodic_task(
318 ctrl,
319 "camera-auto",
320 &self.wakeup_list,
321 Camera::auto_task,
322 );
323 }
324 }
325 }
326}
327
328fn init_pics(dir: &str) -> Result<PicDict> {
332 let root = Path::new(dir);
333 if !root.try_exists()? {
334 warn!("create dir: {}", root.to_string_lossy());
335 std::fs::create_dir_all(root)?;
336 }
337
338 let mut result = PicDict::new();
339 result = find_files_rec(result, root)?;
340 info!("find {} files in {}", result.len(), dir);
341
342 Ok(result)
343}
344
345fn find_files_rec(mut dict: PicDict, path: &Path) -> Result<PicDict> {
347 if path.is_file() {
348 if path.extension().unwrap_or_default() != "jpg" {
350 return Ok(dict);
351 }
352 let name = path.file_stem().unwrap_or_default().to_string_lossy();
354 if name.is_empty() || name.ends_with(THUMB_POSTFIX) {
355 return Ok(dict);
356 }
357
358 let mut path_th = PathBuf::from(path);
360 path_th.set_file_name(format!("{name}_{THUMB_POSTFIX}"));
361 path_th.set_extension("jpg");
362
363 let size = std::fs::metadata(path)?.st_size();
365 let size_th = std::fs::metadata(&path_th).map_or(0, |m| m.st_size());
366 let total_size = size + size_th;
367
368 let entry = PicEntry {
370 path_main: PathBuf::from(path),
371 path_th,
372 total_size,
373 };
374 if let Some(old) = dict.insert(name.to_string(), entry) {
375 warn!(
376 "duplicate picture: {}, {}",
377 old.path_main.display(),
378 path.display()
379 );
380 }
381 } else if path.is_dir() {
382 for entry in std::fs::read_dir(path)? {
383 let entry = entry?;
384 dict = find_files_rec(dict, &entry.path())?;
385 }
386 }
387
388 Ok(dict)
389}
390
391const PIC_MAX_W: u32 = 3280;
393const PIC_MAX_H: u32 = 2464;
395const PIC_MIN_W: u32 = 32;
397const PIC_MIN_H: u32 = 24;
399const PIC_DEF_W: u32 = PIC_MAX_W;
401const PIC_DEF_H: u32 = PIC_MAX_H;
403const PIC_MAX_Q: u8 = 100;
405const PIC_MIN_Q: u8 = 0;
407const PIC_DEF_Q: u8 = 85;
409const PIC_DEF_TO_MS: u32 = 1000;
411const THUMB_W: u32 = 128;
413const THUMB_H: u32 = 96;
415
416pub struct TakePicOption {
418 w: u32,
420 h: u32,
422 q: u8,
424 timeout_ms: u32,
426}
427
428impl Default for TakePicOption {
429 fn default() -> Self {
430 Self::new()
431 }
432}
433
434impl TakePicOption {
435 pub fn new() -> Self {
436 Self {
437 w: PIC_DEF_W,
438 h: PIC_DEF_H,
439 q: PIC_DEF_Q,
440 timeout_ms: PIC_DEF_TO_MS,
441 }
442 }
443 #[allow(dead_code)]
444 pub fn width(mut self, w: u32) -> Self {
445 assert!((PIC_MIN_W..=PIC_MAX_W).contains(&w));
446 self.w = w;
447 self
448 }
449 #[allow(dead_code)]
450 pub fn height(mut self, h: u32) -> Self {
451 assert!((PIC_MIN_H..=PIC_MAX_H).contains(&h));
452 self.h = h;
453 self
454 }
455 #[allow(dead_code)]
456 pub fn quality(mut self, q: u8) -> Self {
457 assert!((PIC_MIN_Q..=PIC_MAX_Q).contains(&q));
458 self.q = q;
459 self
460 }
461 #[allow(dead_code)]
462 pub fn timeout_ms(mut self, timeout_ms: u32) -> Self {
463 self.timeout_ms = timeout_ms;
464 self
465 }
466}
467
468pub async fn take_a_pic(opt: TakePicOption) -> Result<Vec<u8>> {
478 static LOCK: tokio::sync::Mutex<()> = tokio::sync::Mutex::const_new(());
480
481 let fake = config::get(|cfg| cfg.camera.fake_camera);
482
483 let bin = if !fake {
484 let _lock = LOCK.lock().await;
485 let output = Command::new("libcamera-still")
486 .arg("-o")
487 .arg("-")
488 .arg("-t")
489 .arg(opt.timeout_ms.to_string())
490 .arg("-q")
491 .arg(opt.q.to_string())
492 .arg("--width")
493 .arg(opt.w.to_string())
494 .arg("--height")
495 .arg(opt.h.to_string())
496 .output()
497 .await?;
498 if !output.status.success() {
499 bail!("libcamera-still failed: {}", output.status);
500 }
501
502 output.stdout
503 } else {
505 let buf =
507 include_bytes!(concat!(env!("CARGO_MANIFEST_DIR"), "/res/camera_def.jpg")).to_vec();
508
509 let src = image::load_from_memory_with_format(&buf, image::ImageFormat::Jpeg)?;
511 let dst = src.resize_exact(opt.w, opt.h, FilterType::Nearest);
512 let mut output = Cursor::new(vec![]);
513 dst.write_to(&mut output, ImageFormat::Jpeg)?;
514
515 output.into_inner()
516 };
517 Ok(bin)
520}
521
522pub fn create_thumbnail(src_buf: &[u8]) -> Result<Vec<u8>> {
527 let src = image::load_from_memory_with_format(src_buf, image::ImageFormat::Jpeg)?;
528 let dst = src.thumbnail(THUMB_W, THUMB_H);
529
530 let mut buf = Cursor::new(Vec::<u8>::new());
531 dst.write_to(&mut buf, ImageFormat::Jpeg)?;
532
533 Ok(buf.into_inner())
534}
535
536pub fn resize(src_buf: &[u8], w: u32, h: u32) -> Result<Vec<u8>> {
541 let src = image::load_from_memory_with_format(src_buf, image::ImageFormat::Jpeg)?;
542 let dst = src.resize_exact(w, h, FilterType::Nearest);
543
544 let mut buf = Cursor::new(Vec::<u8>::new());
545 dst.write_to(&mut buf, ImageFormat::Jpeg)?;
546
547 Ok(buf.into_inner())
548}