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
31pub struct CameraController {
33 base_path: String,
35 fullsize_map_image: RwLock<FullsizeMapImage>,
37 thumbnail_map_image: RwLock<ThumbnailMapImage>,
39 request_client: Arc<HTTPClient>,
41}
42
43const MAP_BUFFER_PATH: &str = "map.bin";
45const SNAPSHOT_FULL_PATH: &str = "snapshot_full.png";
47const SNAPSHOT_THUMBNAIL_PATH: &str = "snapshot_thumb.png";
49
50impl CameraController {
51 const LAST_IMG_END_DELAY: TimeDelta = TimeDelta::milliseconds(500);
53 const ZO_IMG_FOLDER: &'static str = "zo_img/";
55 const ZO_IMG_ACQ_DELAY: TimeDelta = TimeDelta::seconds(2);
57
58 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 #[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 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 #[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 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 #[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 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 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 #[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 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 #[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 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 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 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 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 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 #[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 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 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 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}