melvin_ob/console_communication/
console_messenger.rs

1use crate::flight_control::{FlightState, Supervisor};
2use crate::scheduling::TaskController;
3use crate::scheduling::task::{BaseTask, ImageTaskStatus};
4use crate::imaging::{CameraAngle, CameraController};
5use crate::util::Vec2D;
6use crate::info;
7use super::{
8    console_endpoint::{ConsoleEndpoint, ConsoleEvent},
9    melvin_messages,
10};
11
12use std::sync::Arc;
13
14/// Handles communication with the console.
15///
16/// `ConsoleMessenger` coordinates various operations involving the camera
17/// controller, task controller, and console endpoint. It provides methods
18/// to process upstream events and send downstream responses or data while
19/// ensuring proper synchronization through asynchronous tasks.
20pub struct ConsoleMessenger {
21    /// A shared reference to the camera controller, used for image-related operations.
22    camera_controller: Arc<CameraController>,
23    /// A shared reference to the task controller, used for managing tasks.
24    task_controller: Arc<TaskController>,
25    supervisor: Arc<Supervisor>,
26    /// A shared reference to the console endpoint, used for sending and receiving messages.
27    endpoint: Arc<ConsoleEndpoint>,
28}
29
30impl ConsoleMessenger {
31    /// Starts the `ConsoleMessenger`, initializing the console endpoint.
32    /// Listens for incoming console events asynchronously.
33    ///
34    /// # Arguments
35    /// - `camera_controller`: Shared reference to `CameraController`.
36    /// - `task_controller`: Shared reference to `TaskController`.
37    ///
38    /// # Returns
39    /// An instance of `ConsoleMessenger`.
40    #[allow(clippy::cast_possible_wrap)]
41    pub(crate) fn start(
42        camera_controller: Arc<CameraController>,
43        task_controller: Arc<TaskController>,
44        supervisor: Arc<Supervisor>,
45    ) -> Self {
46        let endpoint = Arc::new(ConsoleEndpoint::start());
47        let mut receiver = endpoint.subscribe_upstream_events();
48        let endpoint_local = endpoint.clone();
49        let camera_controller_local = camera_controller.clone();
50        let supervisor_local = supervisor.clone();
51        let t_cont_local = task_controller.clone();
52        tokio::spawn(async move {
53            while let Ok(event) = receiver.recv().await {
54                match event {
55                    ConsoleEvent::Message(
56                        melvin_messages::UpstreamContent::CreateSnapshotImage(_),
57                    ) => {
58                        camera_controller_local.create_thumb_snapshot().await.unwrap();
59                    }
60                    ConsoleEvent::Message(
61                        melvin_messages::UpstreamContent::GetSnapshotDiffImage(_),
62                    ) => {
63                        if let Ok(encoded_image) =
64                            camera_controller_local.diff_thumb_snapshot().await
65                        {
66                            endpoint_local.send_downstream(
67                                melvin_messages::DownstreamContent::Image(
68                                    melvin_messages::Image::from_encoded_image_extract(
69                                        encoded_image,
70                                    ),
71                                ),
72                            );
73                        }
74                    }
75                    ConsoleEvent::Message(melvin_messages::UpstreamContent::GetFullImage(_)) => {
76                        if let Ok(encoded_image) =
77                            camera_controller_local.export_full_thumbnail_png().await
78                        {
79                            endpoint_local.send_downstream(
80                                melvin_messages::DownstreamContent::Image(
81                                    melvin_messages::Image::from_encoded_image_extract(
82                                        encoded_image,
83                                    ),
84                                ),
85                            );
86                        }
87                        Self::send_tasklist_from_endpoint(&endpoint_local, &t_cont_local).await;
88                    }
89                    ConsoleEvent::Message(melvin_messages::UpstreamContent::SubmitObjective(
90                                              submit_objective,
91                                          )) => {
92                        let c_cont_lock_local_clone = camera_controller_local.clone();
93                        let endpoint_local_clone = endpoint_local.clone();
94                        tokio::spawn(async move {
95                            let objective_id = submit_objective.objective_id;
96                            let result = c_cont_lock_local_clone
97                                .export_and_upload_objective_png(
98                                    objective_id as usize,
99                                    Vec2D::new(
100                                        submit_objective.offset_x,
101                                        submit_objective.offset_y,
102                                    ),
103                                    Vec2D::new(submit_objective.width, submit_objective.height),
104                                    None,
105                                    None,
106                                )
107                                .await;
108                            info!("Submitted objective '{objective_id}' with result: {result:?}");
109                            endpoint_local_clone.send_downstream(
110                                melvin_messages::DownstreamContent::SubmitResponse(
111                                    melvin_messages::SubmitResponse {
112                                        success: result.is_ok(),
113                                        objective_id: Some(submit_objective.objective_id),
114                                    },
115                                ),
116                            );
117                        });
118                    }
119                    ConsoleEvent::Message(
120                        melvin_messages::UpstreamContent::ScheduleSecretObjective(objective),
121                    ) => {
122                        supervisor_local
123                            .schedule_secret_objective(objective.objective_id as usize, [
124                                objective.offset_x as i32,
125                                objective.offset_y as i32,
126                                (objective.offset_x + objective.width) as i32,
127                                (objective.offset_y + objective.height) as i32,
128                            ])
129                            .await;
130                    }
131                    ConsoleEvent::Message(melvin_messages::UpstreamContent::SubmitDailyMap(_)) => {
132                        let c_cont_lock_local_clone = camera_controller_local.clone();
133                        let endpoint_local_clone = endpoint_local.clone();
134                        tokio::spawn(async move {
135                            let mut success =
136                                c_cont_lock_local_clone.export_full_snapshot().await.is_ok();
137                            if success {
138                                success =
139                                    c_cont_lock_local_clone.upload_daily_map_png().await.is_ok();
140                            }
141                            endpoint_local_clone.send_downstream(
142                                melvin_messages::DownstreamContent::SubmitResponse(
143                                    melvin_messages::SubmitResponse { success, objective_id: None },
144                                ),
145                            );
146                        });
147                    }
148                    _ => {}
149                }
150            }
151        });
152        Self { camera_controller, task_controller, supervisor, endpoint }
153    }
154
155    /// Sends a thumbnail image to the operator console.
156    ///
157    /// If the console is not connected, this method does nothing.
158    ///
159    /// # Arguments
160    /// - `offset`: The offset coordinates for the thumbnail image.
161    /// - `angle`: The camera angle for the thumbnail.
162    pub(crate) fn send_thumbnail(&self, offset: Vec2D<u32>, angle: CameraAngle) {
163        if !self.endpoint.is_console_connected() {
164            return;
165        }
166        let endpoint_local = self.endpoint.clone();
167        let camera_controller_local = self.camera_controller.clone();
168        tokio::spawn(async move {
169            if !endpoint_local.is_console_connected() {
170                return;
171            }
172            if let Ok(encoded_image) =
173                camera_controller_local.export_thumbnail_png(offset, angle).await
174            {
175                endpoint_local.send_downstream(melvin_messages::DownstreamContent::Image(
176                    melvin_messages::Image::from_encoded_image_extract(encoded_image),
177                ));
178            }
179        });
180    }
181
182    /// Sends the task list to the operator console.
183    ///
184    /// If the console is not connected, this method does nothing.
185    pub(crate) async fn send_tasklist(&self) {
186        ConsoleMessenger::send_tasklist_from_endpoint(&self.endpoint, &self.task_controller).await;
187    }
188
189    /// Sends the task list to the operator console.
190    ///
191    /// If the console is not connected, this method does nothing.
192    pub(crate) async fn send_tasklist_from_endpoint(
193        endpoint: &Arc<ConsoleEndpoint>,
194        t_cont: &Arc<TaskController>,
195    ) {
196        if !endpoint.is_console_connected() {
197            return;
198        }
199        let tasks = t_cont
200            .sched_arc()
201            .read()
202            .await
203            .iter()
204            .map(|task| melvin_messages::Task {
205                scheduled_on: task.t().timestamp_millis(),
206                task: Some(match task.task_type() {
207                    BaseTask::TakeImage(take_image) => {
208                        let actual_position = if let ImageTaskStatus::Done { actual_pos, .. } =
209                            take_image.image_status
210                        {
211                            Some(actual_pos)
212                        } else {
213                            None
214                        };
215                        melvin_messages::TaskType::TakeImage(melvin_messages::TakeImage {
216                            planned_position_x: take_image.planned_pos.x(),
217                            planned_position_y: take_image.planned_pos.y(),
218                            actual_position_x: actual_position.map(|p| p.x()),
219                            actual_position_y: actual_position.map(|p| p.y()),
220                        })
221                    }
222                    BaseTask::SwitchState(state) => {
223                        melvin_messages::TaskType::SwitchState(match state.target_state() {
224                            FlightState::Charge => melvin_messages::SatelliteState::Charge,
225                            FlightState::Acquisition => {
226                                melvin_messages::SatelliteState::Acquisition
227                            }
228                            FlightState::Deployment => melvin_messages::SatelliteState::Deployment,
229                            FlightState::Transition => melvin_messages::SatelliteState::Transition,
230                            FlightState::Comms => melvin_messages::SatelliteState::Communication,
231                            FlightState::Safe => melvin_messages::SatelliteState::Safe,
232                        } as i32)
233                    }
234                    BaseTask::ChangeVelocity(velocity_change_task) => {
235                        melvin_messages::TaskType::VelocityChange(melvin_messages::BurnSequence {
236                            rational: melvin_messages::VelocityChangeTaskRationale::OrbitEscape as i32,
237                            target_x: 0,
238                            target_y: 0,
239                            add_target_x: None,
240                            add_target_y: None,
241                            position_x: velocity_change_task
242                                .burn()
243                                .sequence_pos()
244                                .iter()
245                                .map(|pos| pos.x().to_num())
246                                .collect(),
247                            position_y: velocity_change_task
248                                .burn()
249                                .sequence_pos()
250                                .iter()
251                                .map(|pos| pos.y().to_num())
252                                .collect(),
253                            velocity_x: velocity_change_task
254                                .burn()
255                                .sequence_vel()
256                                .iter()
257                                .map(|vel| vel.x().to_num())
258                                .collect(),
259                            velocity_y: velocity_change_task
260                                .burn()
261                                .sequence_vel()
262                                .iter()
263                                .map(|vel| vel.y().to_num())
264                                .collect(),
265                            acc_dt: velocity_change_task.burn().acc_dt() as u32,
266                            detumble_dt: velocity_change_task.burn().detumble_dt() as u32,
267                            rem_angle_dev: velocity_change_task.burn().rem_angle_dev().to_num(),
268                            min_charge: velocity_change_task.burn().min_charge().to_num(),
269                            min_fuel: velocity_change_task.burn().min_fuel().to_num(),
270                        })
271                    }
272                }),
273            })
274            .collect();
275
276        endpoint.send_downstream(melvin_messages::DownstreamContent::TaskList(
277            melvin_messages::TaskList { tasks },
278        ));
279    }
280}