sys/sysmod/
camera.rs

1//! Raspberry Pi カメラ機能。
2//!
3//! 専用カメラを搭載した Raspberry Pi 以外の環境では撮影できない。
4//! [CameraConfig::fake_camera] 設定でフェイクできる。
5
6use 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
26/// サムネイルファイル名のポストフィクス。
27const THUMB_POSTFIX: &str = "thumb";
28
29/// カメラ設定データ。toml 設定に対応する。
30#[derive(Debug, Clone, Serialize, Deserialize)]
31pub struct CameraConfig {
32    /// カメラ自動撮影タスクを有効化する。
33    enabled: bool,
34    /// 起動時に1回だけカメラ自動撮影タスクを起動する。デバッグ用。
35    debug_exec_once: bool,
36    /// raspistill によるリアル撮影ではなく、ダミー黒画像が撮れたことにする。
37    /// Raspberry Pi 以外の環境でのデバッグ用。
38    fake_camera: bool,
39    /// 撮影した画像を保存するディレクトリ。
40    /// [Self::total_size_limit_mb] により自動で削除される。
41    pic_history_dir: String,
42    /// [Self::pic_history_dir] から移す、永久保存ディレクトリ。
43    pic_archive_dir: String,
44    /// [Self::pic_history_dir] のサイズ制限。これを超えた分が古いものから削除される。
45    total_size_limit_mb: u32,
46    /// 画像一覧ページの1ページ当たりの画像数。
47    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/// ストレージ上の画像を示すエントリ。
65#[derive(Clone)]
66pub struct PicEntry {
67    /// メイン画像のファイルパス。
68    pub path_main: PathBuf,
69    /// サムネイル画像のファイルパス。
70    pub path_th: PathBuf,
71    /// [Self::path_main] と [Self::path_th] の合計ファイルサイズ。
72    pub total_size: u64,
73}
74
75/// 画像リストは [BTreeMap] により名前でソートされた状態で管理する。
76///
77/// 名前は撮影日時とするため、古い順にソートされる。
78type PicDict = BTreeMap<String, PicEntry>;
79
80/// ストレージ上の全データを管理するデータ構造。
81struct Storage {
82    /// 撮影された画像リスト。自動削除対象。
83    pic_history_list: PicDict,
84    /// [Self::pic_archive_list] から移動された画像リスト。自動削除しない。
85    pic_archive_list: PicDict,
86}
87
88/// Camera システムモジュール。
89pub struct Camera {
90    /// 設定データ。
91    ///
92    /// web からも参照される。
93    pub config: CameraConfig,
94    /// 自動撮影の時刻リスト。
95    wakeup_list: Vec<NaiveTime>,
96    /// ストレージ上の画像リストデータ。
97    storage: Storage,
98}
99
100impl Camera {
101    /// コンストラクタ。
102    ///
103    /// 設定データの読み込みと、ストレージの状態取得を行い画像リストを初期化する。
104    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    /// ストレージ上の画像リスト (history, archive) を取得する。
124    pub fn pic_list(&self) -> (&PicDict, &PicDict) {
125        (
126            &self.storage.pic_history_list,
127            &self.storage.pic_archive_list,
128        )
129    }
130
131    /// キーからファイル名を生成する。
132    fn create_file_names(key: &str) -> (String, String) {
133        (format!("{key}.jpg"), format!("{key}_{THUMB_POSTFIX}.jpg"))
134    }
135
136    /// 撮影した画像をストレージに書き出し、管理構造に追加する。
137    ///
138    /// 名前は現在日時から自動的に付与される。
139    ///
140    /// * `img` - jpg ファイルのバイナリデータ。
141    /// * `thumb` - サムネイル jpg ファイルのバイナリデータ。
142    pub async fn push_pic_history(&mut self, img: &[u8], thumb: &[u8]) -> Result<()> {
143        // 現在時刻からキーを生成する
144        // 重複するなら少し待ってからリトライする
145        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        // ファイルパスを生成する
163        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        // ファイルに書き込む
170        {
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        // 成功したらマップに追加する
181        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    /// ヒストリ内の `key` で指定したエントリを永続領域にコピーする。
192    ///
193    /// * `key` - エントリ名。
194    pub async fn push_pic_archive(&mut self, key: &str) -> Result<()> {
195        // ヒストリから name を検索する
196        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        // key からファイル名を生成し、パスを生成する
203        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        // コピーを実行する
211        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        // 成功したらマップに追加する
215        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    /// [PicDict] の合計ファイルサイズを計算する。
252    ///
253    /// オーダ O(n)。
254    /// 64 bit でオーバーフローすると panic する。
255    fn calc_total_size(list: &PicDict) -> u64 {
256        list.iter().fold(0, |acc, (_, entry)| {
257            // panic if overflow
258            acc.checked_add(entry.total_size).unwrap()
259        })
260    }
261
262    /// 必要に応じて自動削除を行う。
263    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            // 一番古いものを削除する (1.66.0 or later)
274            let (id, entry) = history.pop_first().unwrap();
275            // 削除でのエラーはログを出して続行する
276            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    /// 自動撮影タスク。
293    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    /// async 使用可能になってからの初期化。
309    ///
310    /// 設定有効ならば [Self::auto_task] を spawn する。
311    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
328/// 検索ルートディレクトリ内から jpg ファイルを検索して [PicDict] を構築する。
329///
330/// ルートディレクトリが存在しない場合は作成する。
331fn 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
345/// [init_pics] 用の再帰関数。
346fn find_files_rec(mut dict: PicDict, path: &Path) -> Result<PicDict> {
347    if path.is_file() {
348        // 拡張子が jpg でないものは無視
349        if path.extension().unwrap_or_default() != "jpg" {
350            return Ok(dict);
351        }
352        // 拡張子を除いた部分が空文字列およびサムネイルの場合は無視
353        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        // サムネイル画像のパスを生成
359        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        // サイズ取得
364        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        // PicEntry を生成して結果に追加
369        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
391/// 横最大サイズ。(Raspberry Pi Camera V2)
392const PIC_MAX_W: u32 = 3280;
393/// 縦最大サイズ。(Raspberry Pi Camera V2)
394const PIC_MAX_H: u32 = 2464;
395/// 横最小サイズ。(Raspberry Pi Camera V2)
396const PIC_MIN_W: u32 = 32;
397/// 縦最小サイズ。(Raspberry Pi Camera V2)
398const PIC_MIN_H: u32 = 24;
399/// 横デフォルトサイズ。
400const PIC_DEF_W: u32 = PIC_MAX_W;
401/// 縦デフォルトサイズ。
402const PIC_DEF_H: u32 = PIC_MAX_H;
403/// jpeg 最大クオリティ。
404const PIC_MAX_Q: u8 = 100;
405/// jpeg 最小クオリティ。
406const PIC_MIN_Q: u8 = 0;
407/// jpeg デフォルトクオリティ。
408const PIC_DEF_Q: u8 = 85;
409/// デフォルト撮影時間(ms)。TO はタイムアウト。
410const PIC_DEF_TO_MS: u32 = 1000;
411/// サムネイルの横サイズ。
412const THUMB_W: u32 = 128;
413/// サムネイルの縦サイズ。
414const THUMB_H: u32 = 96;
415
416/// 写真撮影オプション。
417pub struct TakePicOption {
418    /// 横サイズ。
419    w: u32,
420    /// 縦サイズ。
421    h: u32,
422    /// jpeg クオリティ。
423    q: u8,
424    /// 撮影時間(ms)。
425    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
468/// 写真を撮影する。成功すると jpeg バイナリデータを返す。
469///
470/// 従来は raspistill コマンドを使っていたが、Bullseye より廃止された。
471/// カメラ関連の各種操作は libcamera に移動、集約された。
472/// raspistill コマンド互換の libcamera-still コマンドを使う。
473///
474/// 同時に2つ以上を実行できないかつ時間がかかるので、[tokio::sync::Mutex] で排他する。
475///
476/// * `opt` - 撮影オプション。
477pub async fn take_a_pic(opt: TakePicOption) -> Result<Vec<u8>> {
478    // 他の関数でも raspistill を使う場合外に出す
479    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        // unlock
504    } else {
505        // バイナリ同梱のデフォルト画像が撮れたことにする
506        let buf =
507            include_bytes!(concat!(env!("CARGO_MANIFEST_DIR"), "/res/camera_def.jpg")).to_vec();
508
509        // オプションの w, h にリサイズする
510        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    // raspistill は同時に複数プロセス起動できないので mutex で保護する
518
519    Ok(bin)
520}
521
522/// サムネイルを作成する。
523/// 成功すれば jpeg バイナリデータを返す。
524///
525/// * `src_buf` - 元画像とする jpeg バイナリデータ。
526pub 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
536/// 画像をリサイズする。
537/// 成功すれば jpeg バイナリデータを返す。
538///
539/// * `src_buf` - 元画像とする jpeg バイナリデータ。
540pub 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}