melvin_ob/imaging/
camera_controller.rs

1use super::{CameraAngle, cycle_state::CycleState, map_image::*};
2use crate::console_communication::ConsoleMessenger;
3use crate::flight_control::FlightComputer;
4use crate::http_handler::{
5    http_client::HTTPClient,
6    http_request::{
7        daily_map_post::DailyMapRequest,
8        objective_image_post::ObjectiveImageRequest,
9        request_common::{MultipartBodyHTTPRequestType, NoBodyHTTPRequestType},
10        shoot_image_get::ShootImageRequest,
11    },
12};
13use crate::mode_control::PeriodicImagingEndSignal::{self, KillLastImage, KillNow};
14use crate::util::Vec2D;
15use crate::{DT_0_STD, error, fatal, info, log, obj};
16use chrono::{DateTime, TimeDelta, Utc};
17use fixed::types::I32F32;
18use futures::StreamExt;
19use image::{GenericImageView, ImageReader, Pixel, RgbImage, imageops::Lanczos3};
20use std::{
21    fs,
22    path::{Path, PathBuf},
23    {io::Cursor, sync::Arc},
24};
25use tokio::{
26    fs::File,
27    io::AsyncWriteExt,
28    sync::{Mutex, RwLock, oneshot},
29};
30
31/// A struct for managing camera-related operations and map snapshots.
32pub struct CameraController {
33    /// The base path for saving map image data.
34    base_path: String,
35    /// The lock-protected full-size map image.
36    fullsize_map_image: RwLock<FullsizeMapImage>,
37    /// The lock-protected thumbnail map image.
38    thumbnail_map_image: RwLock<ThumbnailMapImage>,
39    /// The HTTP client for sending requests.
40    request_client: Arc<HTTPClient>,
41}
42
43/// Path to the binary map buffer file.
44const MAP_BUFFER_PATH: &str = "map.bin";
45/// Path to the full-size snapshot file.
46const SNAPSHOT_FULL_PATH: &str = "snapshot_full.png";
47/// Path to the thumbnail snapshot file.
48const SNAPSHOT_THUMBNAIL_PATH: &str = "snapshot_thumb.png";
49
50impl CameraController {
51    /// Constant minimum delay to perform another image.
52    const LAST_IMG_END_DELAY: TimeDelta = TimeDelta::milliseconds(500);
53    /// Directory where zoned objective images should be stored.
54    const ZO_IMG_FOLDER: &'static str = "zo_img/";
55    /// Constant `TimeDelta` between images when in zoned objective acquisition.
56    const ZO_IMG_ACQ_DELAY: TimeDelta = TimeDelta::seconds(2);
57
58    /// Initializes the [`CameraController`] with the given base path and HTTP client.
59    ///
60    /// # Arguments
61    ///
62    /// * `base_path` - The base path for storing files.
63    /// * `request_client` - The HTTP client for sending requests.
64    ///
65    /// # Returns
66    ///
67    /// A new instance of [`CameraController`].
68    pub fn start(base_path: String, request_client: Arc<HTTPClient>) -> Self {
69        let fullsize_map_image =
70            FullsizeMapImage::open(Path::new(&base_path).join(MAP_BUFFER_PATH));
71        let thumbnail_map_image =
72            ThumbnailMapImage::from_snapshot(Path::new(&base_path).join(SNAPSHOT_THUMBNAIL_PATH));
73        if let Err(e) = fs::create_dir_all(Self::ZO_IMG_FOLDER) {
74            fatal!("Failed to create objective image directory: {e}!");
75        }
76        Self {
77            fullsize_map_image: RwLock::new(fullsize_map_image),
78            thumbnail_map_image: RwLock::new(thumbnail_map_image),
79            request_client,
80            base_path,
81        }
82    }
83
84    /// Scores the offset by comparing the decoded image against the map base image.
85    ///
86    /// # Arguments
87    ///
88    /// * `decoded_image` - The decoded image to match.
89    /// * `base` - The reference full-size map image.
90    /// * `offset` - The initial offset to evaluate.
91    ///
92    /// # Returns
93    ///
94    /// The best scored offset as `Vec2D<i32>`.
95    #[allow(clippy::cast_sign_loss, clippy::cast_possible_wrap)]
96    fn score_offset(
97        decoded_image: &RgbImage,
98        base: &FullsizeMapImage,
99        offset: Vec2D<u32>,
100    ) -> Vec2D<i32> {
101        let mut best_score = i32::MIN;
102        let mut best_additional_offset = Vec2D::new(0, 0);
103        for additional_offset_x in -2..=2 {
104            for additional_offset_y in -2..=2 {
105                let current_offset: Vec2D<u32> = Vec2D::new(
106                    offset.x() as i32 + additional_offset_x,
107                    offset.y() as i32 + additional_offset_y,
108                )
109                .wrap_around_map()
110                .to_unsigned();
111                let map_image_view = base.vec_view(
112                    current_offset,
113                    Vec2D::new(decoded_image.width(), decoded_image.height()),
114                );
115                let mut score: i32 = map_image_view
116                    .pixels()
117                    .zip(decoded_image.pixels())
118                    .map(
119                        |((_, _, existing_pixel), new_pixel)| {
120                            if existing_pixel.to_rgb() == new_pixel.to_rgb() { 0 } else { -1 }
121                        },
122                    )
123                    .sum();
124
125                score -= additional_offset_x.abs() + additional_offset_y.abs();
126                if score > best_score {
127                    best_additional_offset = Vec2D::new(additional_offset_x, additional_offset_y);
128                    best_score = score;
129                }
130            }
131        }
132        best_additional_offset
133    }
134
135    /// Performs the HTTP request to retrieve an image from the DRS backend. Then calculates the position and image offset.
136    ///
137    /// # Arguments
138    /// `f_cont_locked`: A shared `RwLock` containing the [`FlightComputer`] instance
139    /// `angle`: The current [`CameraAngle`]
140    ///
141    /// # Returns
142    /// A Result containing a tuple with:
143    ///   - The `Vec2D<I32F32>` position where the image was taken
144    ///   - The `Vec2D<i32>` offset in the map image buffer
145    ///   - The decoded `RgbImage`
146    /// or an Error
147    pub async fn get_image(
148        &self,
149        f_cont_locked: Arc<RwLock<FlightComputer>>,
150        angle: CameraAngle,
151    ) -> Result<(Vec2D<I32F32>, Vec2D<i32>, RgbImage), Box<dyn std::error::Error + Send + Sync>>
152    {
153        let (position, collected_png) = {
154            let mut f_cont = f_cont_locked.write().await;
155            let ((), collected_png) =
156                tokio::join!(f_cont.update_observation(), self.fetch_image_data());
157            (f_cont.current_pos(), collected_png)
158        };
159        let decoded_image = Self::decode_png_data(&collected_png?, angle)?;
160        let angle_const = angle.get_square_side_length() / 2;
161        let offset: Vec2D<i32> = Vec2D::new(
162            position.x().round().to_num::<i32>() - i32::from(angle_const),
163            position.y().round().to_num::<i32>() - i32::from(angle_const),
164        )
165        .wrap_around_map();
166        Ok((position, offset, decoded_image))
167    }
168
169    /// Captures an image, processes it, and stores it in the map buffer.
170    ///
171    /// # Arguments
172    /// * `f_cont_locked` - The lock-protected flight computer.
173    /// * `angle` - The camera angle and field of view.
174    ///
175    /// # Returns
176    /// The position of the image as `Vec2D<I32F32>` or an error.
177    #[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss, clippy::cast_possible_wrap)]
178    pub async fn shoot_image_to_map_buffer(
179        &self,
180        f_cont_locked: Arc<RwLock<FlightComputer>>,
181        angle: CameraAngle,
182    ) -> Result<(Vec2D<I32F32>, Vec2D<u32>), Box<dyn std::error::Error + Send + Sync>> {
183        let (pos, offset, decoded_image) = self.get_image(f_cont_locked, angle).await?;
184
185        let tot_offset_u32 = {
186            let mut fullsize_map_image = self.fullsize_map_image.write().await;
187            let best_additional_offset =
188                Self::score_offset(&decoded_image, &fullsize_map_image, offset.to_unsigned());
189            let tot_offset: Vec2D<u32> =
190                (offset + best_additional_offset).wrap_around_map().to_unsigned();
191            fullsize_map_image.update_area(tot_offset, &decoded_image);
192            tot_offset
193        };
194        self.update_thumbnail_area_from_fullsize(
195            tot_offset_u32,
196            u32::from(angle.get_square_side_length() / 2),
197        )
198        .await;
199        Ok((pos, tot_offset_u32))
200    }
201
202    /// Captures an image, processes it, and stores it in a custom zoned objective buffer.
203    ///
204    /// # Arguments
205    /// * `f_cont_locked` - The lock-protected flight computer.
206    /// * `angle` - The camera angle and field of view.
207    /// * `zoned_objective_map_image`: An optional mutable reference to an `OffsetZonedObjectiveImage`
208    ///
209    /// # Returns
210    /// The imaging position as `Vec2D<I32F32>` or an error.
211    pub async fn shoot_image_to_zo_buffer(
212        &self,
213        f_cont_locked: Arc<RwLock<FlightComputer>>,
214        angle: CameraAngle,
215        zoned_objective_map_image: Option<&mut OffsetZonedObjectiveImage>,
216    ) -> Result<Vec2D<I32F32>, Box<dyn std::error::Error + Send + Sync>> {
217        let (pos, offset, decoded_image) = self.get_image(f_cont_locked, angle).await?;
218        let offset_u32 = offset.to_unsigned();
219        if let Some(image) = zoned_objective_map_image {
220            image.update_area(offset_u32, &decoded_image);
221        }
222
223        Ok(pos)
224    }
225
226    /// Updates the thumbnail area of the map based on the full-size map data.
227    ///
228    /// # Arguments
229    ///
230    /// * `offset` - Offset to update.
231    /// * `size` - Size of the region to update.
232    #[allow(clippy::cast_possible_wrap)]
233    async fn update_thumbnail_area_from_fullsize(&self, offset: Vec2D<u32>, size: u32) {
234        let thumbnail_offset = Vec2D::new(
235            offset.x() as i32 - ThumbnailMapImage::THUMBNAIL_SCALE_FACTOR as i32 * 2,
236            offset.y() as i32 - ThumbnailMapImage::THUMBNAIL_SCALE_FACTOR as i32 * 2,
237        )
238        .wrap_around_map()
239        .to_unsigned();
240        let size_scaled = size * 2 + ThumbnailMapImage::THUMBNAIL_SCALE_FACTOR * 4;
241        let fullsize_map_image = self.fullsize_map_image.read().await;
242        let map_image_view =
243            fullsize_map_image.vec_view(thumbnail_offset, Vec2D::new(size_scaled, size_scaled));
244
245        let resized_image = image::imageops::thumbnail(
246            &map_image_view,
247            size_scaled / ThumbnailMapImage::THUMBNAIL_SCALE_FACTOR,
248            size_scaled / ThumbnailMapImage::THUMBNAIL_SCALE_FACTOR,
249        );
250        self.thumbnail_map_image.write().await.update_area(
251            thumbnail_offset / ThumbnailMapImage::THUMBNAIL_SCALE_FACTOR,
252            &resized_image,
253        );
254    }
255
256    /// Fetches image data from the camera as a byte vector.
257    ///
258    /// # Returns
259    ///
260    /// The raw PNG data or an error.
261    async fn fetch_image_data(&self) -> Result<Vec<u8>, Box<dyn std::error::Error + Send + Sync>> {
262        let response_stream = ShootImageRequest {}.send_request(&self.request_client).await?;
263
264        let mut collected_png: Vec<u8> = Vec::new();
265        futures::pin_mut!(response_stream);
266
267        while let Some(Ok(chunk_result)) = response_stream.next().await {
268            collected_png.extend_from_slice(&chunk_result[..]);
269        }
270
271        Ok(collected_png)
272    }
273
274    /// Decodes PNG data into an RGB image and resizes it based on the camera angle.
275    ///
276    /// # Arguments
277    ///
278    /// * `collected_png` - Raw PNG data.
279    /// * `angle` - The camera angle defining the image resolution.
280    ///
281    /// # Returns
282    ///
283    /// The decoded and resized image as `RgbImage` or an error.
284    fn decode_png_data(
285        collected_png: &[u8],
286        angle: CameraAngle,
287    ) -> Result<RgbImage, Box<dyn std::error::Error + Send + Sync>> {
288        let decoded_image =
289            ImageReader::new(Cursor::new(collected_png)).with_guessed_format()?.decode()?.to_rgb8();
290        let resized_unit_length = angle.get_square_side_length();
291
292        let resized_image = image::imageops::resize(
293            &decoded_image,
294            u32::from(resized_unit_length),
295            u32::from(resized_unit_length),
296            Lanczos3,
297        );
298
299        Ok(resized_image)
300    }
301
302    /// Exports a specific region of the map as a PNG and uploads it to the server associated with the given objective ID.
303    ///
304    /// # Arguments
305    ///
306    /// * `objective_id` - The identifier of the objective to associate the exported PNG with.
307    /// * `offset` - The offset in the map to start the export.
308    /// * `size` - The dimensions of the region to export as a PNG.
309    ///
310    /// # Returns
311    ///
312    /// A result indicating the success or failure of the operation.
313    #[allow(clippy::cast_sign_loss)]
314    pub(crate) async fn export_and_upload_objective_png(
315        &self,
316        objective_id: usize,
317        offset: Vec2D<u32>,
318        size: Vec2D<u32>,
319        export_path: Option<PathBuf>,
320        zoned_objective_map_image: Option<&OffsetZonedObjectiveImage>,
321    ) -> Result<(), Box<dyn std::error::Error>> {
322        let encoded_image = if let Some(zo_image) = zoned_objective_map_image {
323            zo_image.export_as_png()?
324        } else {
325            let map_image = self.fullsize_map_image.read().await;
326            map_image.export_area_as_png(offset, size)?
327        };
328        if let Some(img_path) = export_path {
329            let mut img_file = File::create(&img_path).await?;
330            img_file.write_all(encoded_image.data.as_slice()).await?;
331            drop(img_file);
332            ObjectiveImageRequest::new(objective_id, img_path)
333                .send_request(&self.request_client)
334                .await?;
335        }
336        log!("Successfully exported and uploaded objective png.");
337        Ok(())
338    }
339
340    /// Helper method generating the export path for a given zoned objective id.
341    ///
342    /// # Arguments
343    /// `id`: The objective id
344    ///
345    /// # Returns
346    /// The path to the zoned objective image file as a `PathBuf`
347    pub(crate) fn generate_zo_img_path(id: usize) -> PathBuf {
348        let dir = Path::new(Self::ZO_IMG_FOLDER);
349        let mut path = dir.join(format!("zo_{id}.png"));
350        let mut counter = 0;
351        while path.exists() {
352            path = dir.join(format!("zo_{id}_{counter}.png",));
353            counter += 1;
354        }
355        path
356    }
357
358    /// Uploads the daily map snapshot as a PNG to the server.
359    ///
360    /// # Returns
361    ///
362    /// A result indicating the success or failure of the operation.
363    #[allow(clippy::cast_sign_loss)]
364    pub(crate) async fn upload_daily_map_png(&self) -> Result<(), Box<dyn std::error::Error>> {
365        DailyMapRequest::new(SNAPSHOT_FULL_PATH)?.send_request(&self.request_client).await?;
366        Ok(())
367    }
368
369    /// Creates and saves a thumbnail snapshot of the map.
370    ///
371    /// # Returns
372    ///
373    /// A result indicating the success or failure of the operation.
374    pub(crate) async fn create_thumb_snapshot(&self) -> Result<(), Box<dyn std::error::Error>> {
375        self.thumbnail_map_image
376            .read()
377            .await
378            .create_snapshot(Path::new(&self.base_path).join(SNAPSHOT_THUMBNAIL_PATH))
379    }
380
381    /// Creates and saves a full-size snapshot of the map.
382    ///
383    /// # Returns
384    ///
385    /// A result indicating the success or failure of the operation.
386    pub(crate) async fn export_full_snapshot(&self) -> Result<(), Box<dyn std::error::Error>> {
387        let start_time = Utc::now();
388        self.fullsize_map_image
389            .read()
390            .await
391            .create_snapshot(Path::new(&self.base_path).join(SNAPSHOT_FULL_PATH))?;
392        info!(
393            "Exported Full-View PNG in {}s!",
394            (Utc::now() - start_time).num_seconds()
395        );
396        Ok(())
397    }
398
399    /// Exports a part of the thumbnail map as a PNG.
400    ///
401    /// # Arguments
402    ///
403    /// * `offset` - The offset to start exporting from.
404    /// * `angle` - The camera angle, which influences the resolution and dimensions.
405    ///
406    /// # Returns
407    ///
408    /// A result containing the extracted PNG image data or an error.
409    pub(crate) async fn export_thumbnail_png(
410        &self,
411        offset: Vec2D<u32>,
412        angle: CameraAngle,
413    ) -> Result<EncodedImageExtract, Box<dyn std::error::Error>> {
414        let size =
415            u32::from(angle.get_square_side_length()) / ThumbnailMapImage::THUMBNAIL_SCALE_FACTOR;
416        self.thumbnail_map_image.read().await.export_area_as_png(
417            offset / ThumbnailMapImage::THUMBNAIL_SCALE_FACTOR,
418            Vec2D::new(size, size),
419        )
420    }
421
422    /// Exports the entire map thumbnail as a PNG.
423    ///
424    /// # Returns
425    ///
426    /// A result containing the extracted PNG image data or an error.
427    pub(crate) async fn export_full_thumbnail_png(
428        &self,
429    ) -> Result<EncodedImageExtract, Box<dyn std::error::Error>> {
430        self.thumbnail_map_image.read().await.export_as_png()
431    }
432
433    /// Compares the thumbnail map with its saved snapshot.
434    ///
435    /// # Returns
436    ///
437    /// A result containing the difference as an encoded PNG image or an error.
438    pub(crate) async fn diff_thumb_snapshot(
439        &self,
440    ) -> Result<EncodedImageExtract, Box<dyn std::error::Error>> {
441        self.thumbnail_map_image
442            .read()
443            .await
444            .diff_with_snapshot(Path::new(&self.base_path).join(SNAPSHOT_THUMBNAIL_PATH))
445            .await
446    }
447
448    /// Executes a series of image acquisitions, processes them, and updates the associated map buffers.
449    ///
450    /// # Arguments
451    ///
452    /// * `f_cont_lock` - Lock-protected flight computer controlling the acquisition cycle.
453    /// * `console_messenger` - Used for sending notifications during processing.
454    /// * `(end_time, last_img_kill)` - The end time for the cycle and a notify object to terminate the process prematurely.
455    /// * `image_max_dt` - Maximum allowed interval between consecutive images.
456    /// * `lens` - The camera angle and field of view.
457    /// * `start_index` - The starting index for tracking image acquisitions.
458    ///
459    /// # Returns
460    ///
461    /// A vector of completed (start, end) time ranges when images were successfully taken.
462    #[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss, clippy::cast_possible_wrap)]
463    pub async fn execute_acquisition_cycle(
464        self: &Arc<Self>,
465        f_cont_lock: Arc<RwLock<FlightComputer>>,
466        console_messenger: Arc<ConsoleMessenger>,
467        (end_time, kill): (DateTime<Utc>, oneshot::Receiver<PeriodicImagingEndSignal>),
468        image_max_dt: I32F32,
469        start_index: usize,
470    ) -> Vec<(isize, isize)> {
471        log!(
472            "Starting acquisition cycle. Deadline: {}",
473            end_time.format("%H:%M:%S")
474        );
475        let lens = f_cont_lock.read().await.current_angle();
476        let mut kill_box = Box::pin(kill);
477        let mut last_image_flag = false;
478
479        let pic_count_lock = Arc::new(Mutex::new(0));
480        let mut state = CycleState::init_cycle(image_max_dt, start_index as isize);
481
482        loop {
483            let (img_t, offset) =
484                Self::exec_map_capture(self, &f_cont_lock, &pic_count_lock, lens).await;
485
486            let mut next_img_due = Self::get_next_map_img(image_max_dt, end_time);
487            if let Some(off) = offset {
488                console_messenger.send_thumbnail(off, lens);
489                state.update_success(img_t);
490            } else {
491                state.update_failed(img_t);
492                error!("Rescheduling failed picture immediately!");
493                next_img_due = Utc::now() + TimeDelta::seconds(1);
494            }
495
496            if last_image_flag {
497                return state.finish();
498            } else if next_img_due + Self::LAST_IMG_END_DELAY >= end_time {
499                last_image_flag = true;
500            }
501
502            let sleep_time = next_img_due - Utc::now();
503            tokio::select! {
504                () = tokio::time::sleep(sleep_time.to_std().unwrap_or(DT_0_STD)) => {},
505                msg = &mut kill_box => {
506                     match msg.unwrap_or_else(|e| {
507                            error!("Couldn't receive kill signal: {e}");
508                            KillNow
509                        }) {
510                        KillLastImage => last_image_flag = true,
511                        KillNow => {
512                             return state.finish();
513                        }
514                    }
515                }
516            }
517        }
518    }
519
520    /// Executes a series of image acquisitions, processes them, and updates an associated zoned objective buffer.
521    ///
522    /// # Arguments
523    /// * `f_cont_lock` - Lock-protected flight computer controlling the acquisition cycle.
524    /// * `deadline` - The end time for the cycle.
525    /// * `zoned_objective_image_buffer` - An optional mutable reference to an `OffsetZonedObjectiveImage`
526    /// * `offset` - The offset of the buffer in the global map buffer.
527    /// * `dimensions` - The dimensions of the zoned objective.
528    pub async fn execute_zo_target_cycle(
529        self: Arc<Self>,
530        f_cont_lock: Arc<RwLock<FlightComputer>>,
531        deadline: DateTime<Utc>,
532        zoned_objective_image_buffer: &mut Option<OffsetZonedObjectiveImage>,
533        offset: Vec2D<u32>,
534        dimensions: Vec2D<u32>,
535    ) {
536        obj!(
537            "Starting acquisition cycle for objective. Deadline {}!",
538            deadline.format("%H:%M:%S")
539        );
540        zoned_objective_image_buffer.replace(OffsetZonedObjectiveImage::new(offset, dimensions));
541        let lens = f_cont_lock.read().await.current_angle();
542        let mut pics = 0;
543        let deadline_cont = deadline - Utc::now() > TimeDelta::seconds(20);
544        let step_print = if deadline_cont { 20 } else { 2 };
545        loop {
546            let next_img_due = Utc::now() + Self::ZO_IMG_ACQ_DELAY;
547            let img_init_timestamp = Utc::now();
548            match self
549                .shoot_image_to_zo_buffer(
550                    Arc::clone(&f_cont_lock),
551                    lens,
552                    zoned_objective_image_buffer.as_mut(),
553                )
554                .await
555            {
556                Ok(pos) => {
557                    pics += 1;
558                    let s = (Utc::now() - img_init_timestamp).num_seconds();
559                    if pics % step_print == 0 {
560                        obj!("Took {pics:02}. picture. Processed for {s}s. Position was {pos}");
561                    }
562                }
563                Err(e) => {
564                    error!("Couldn't take picture: {e}");
565                }
566            }
567            if Utc::now() > deadline {
568                return;
569            }
570            tokio::time::sleep((next_img_due - Utc::now()).to_std().unwrap_or(DT_0_STD)).await;
571        }
572    }
573
574    /// Helper method returning the timestamp of the next image
575    ///
576    /// # Arguments
577    /// * `img_max_dt`: An `I32F32` resembling the maximum number of seconds between consecutive images in mapping.
578    /// * `end_time`: The deadline as a `DateTime<Utc>`
579    ///
580    /// # Returns
581    /// The next image timestamp as an `DateTime<Utc>`
582    fn get_next_map_img(img_max_dt: I32F32, end_time: DateTime<Utc>) -> DateTime<Utc> {
583        let next_max_dt = Utc::now() + TimeDelta::seconds(img_max_dt.to_num::<i64>());
584        if next_max_dt > end_time { end_time - Self::LAST_IMG_END_DELAY } else { next_max_dt }
585    }
586
587    /// Captures a single image during mapping operation.
588    ///
589    /// # Arguments
590    /// * `f_cont_lock` - Lock-protected flight computer controlling the acquisition cycle.
591    /// * `p_c` - A shared, locked reference to the number of pictures in the current cycle.
592    /// * `lens` - The desired `CameraAngle` for this picture
593    ///
594    /// # Returns
595    /// A tuple containing:
596    ///   - The UTC timestamp when the image was taken
597    ///   - The `Vec2D<i32>` offset in the global map image buffer
598    async fn exec_map_capture(
599        self: &Arc<Self>,
600        f_cont: &Arc<RwLock<FlightComputer>>,
601        p_c: &Arc<Mutex<i32>>,
602        lens: CameraAngle,
603    ) -> (DateTime<Utc>, Option<Vec2D<u32>>) {
604        let f_cont_clone = Arc::clone(f_cont);
605        let p_c_clone = Arc::clone(p_c);
606        let self_clone = Arc::clone(self);
607        let img_init_timestamp = Utc::now();
608
609        let img_handle = tokio::spawn(async move {
610            match self_clone.shoot_image_to_map_buffer(Arc::clone(&f_cont_clone), lens).await {
611                Ok((pos, offset)) => {
612                    let pic_num = {
613                        let mut lock = p_c_clone.lock().await;
614                        *lock += 1;
615                        *lock
616                    };
617                    let s = (Utc::now() - img_init_timestamp).num_seconds();
618                    info!("Took {pic_num:02}. picture. Processed for {s}s. Position was {pos}");
619                    Some(offset)
620                }
621                Err(e) => {
622                    error!("Couldn't take picture: {e}");
623                    None
624                }
625            }
626        });
627
628        let res = img_handle.await.ok().flatten();
629        (img_init_timestamp, res)
630    }
631}