1use anyhow::{Context, Result};
2use chrono::Utc;
3use core::database::insert_image;
4use core::sauron::SauronHandle;
5use serde_json::json;
6use sqlx::postgres::PgPoolOptions;
7use std::env;
8use std::path::{Path, PathBuf};
9use std::time::Duration;
10use tokio::time;
11use tracing::{debug, error, info};
12use types::mailbox::{GNCTelemetry, MailboxMessage, MailboxMessageType};
13use walkdir::WalkDir;
14
15pub async fn start_fake_camera(
16 sauron: SauronHandle,
17 shutdown_token: tokio_util::sync::CancellationToken,
18) -> Result<()> {
19 info!("Starting fake camera actor (loading from still frames)");
20
21 let mut fail_counter: u32 = 0;
22 tokio::spawn(async move {
23 loop {
24 if shutdown_token.is_cancelled() {
25 info!("Fake camera starter shutting down");
26 break;
27 }
28 match run_fake_camera(&sauron, shutdown_token.clone()).await {
29 Ok(_) => break,
30 Err(e) => {
31 fail_counter += 1;
32 error!("Fake camera thread failed ({} times): {}", fail_counter, e);
33 info!("Restarting fake camera thread in 2500 milliseconds...");
34 tokio::time::sleep(Duration::from_millis(2500)).await;
35 }
36 }
37 }
38 });
39
40 Ok(())
41}
42
43async fn run_fake_camera(
44 sauron: &SauronHandle,
45 shutdown_token: tokio_util::sync::CancellationToken,
46) -> Result<()> {
47 info!("Fake Camera Actor Started");
48
49 let database_url: String = env::var("DATABASE_URL")
50 .unwrap_or("postgresql://user:password@localhost:5432/local".to_string());
51
52 let pool = PgPoolOptions::new()
53 .max_connections(constants::CONNECTIONS_PER_DATABASE_POOL)
54 .connect(&database_url)
55 .await?;
56
57 let still_frames_dir =
59 env::var("IRONBIRD_STILL_FRAMES_DIR").unwrap_or("./still_frames".to_string());
60 let still_frames_path = PathBuf::from(&still_frames_dir);
61
62 if !still_frames_path.exists() {
63 return Err(anyhow::anyhow!(
64 "Still frames directory does not exist: {}. Set IRONBIRD_STILL_FRAMES_DIR environment variable.",
65 still_frames_dir
66 ));
67 }
68
69 let image_files = scan_image_directory(&still_frames_path)?;
71
72 if image_files.is_empty() {
73 return Err(anyhow::anyhow!(
74 "No image files found in still frames directory: {}",
75 still_frames_dir
76 ));
77 }
78
79 info!(
80 "Found {} image files in {}",
81 image_files.len(),
82 still_frames_dir
83 );
84
85 let output_dir = PathBuf::from("./untagged_image_folder");
87 if !output_dir.exists() {
88 std::fs::create_dir_all(&output_dir)?;
89 }
90
91 let camera_interval_ms: u64 = env::var("CAMERA_INTERVAL_MS")
92 .unwrap_or("1000".to_string())
93 .parse()
94 .unwrap_or(1000);
95
96 let mut camera_timer = time::interval(Duration::from_millis(camera_interval_ms));
97
98 let mut image_index = 0;
99
100 loop {
101 camera_timer.tick().await;
102
103 if shutdown_token.is_cancelled() {
104 info!("Fake Camera shutting down");
105 break;
106 }
107
108 let time_before = Utc::now();
109
110 let source_image_path = &image_files[image_index % image_files.len()];
111 image_index += 1;
112
113 let photo_path = copy_image_to_output(source_image_path, &output_dir)
114 .await
115 .with_context(|| format!("Failed to copy image from {:?}", source_image_path))?;
116
117 let time_after = Utc::now();
118 let average_time = (time_before + (time_after - time_before) / 2).timestamp_millis();
119
120 debug!(
121 "Fake camera loaded photo from still frame: {} -> {}",
122 source_image_path.display(),
123 photo_path
124 );
125
126 let insert_image_promise = insert_image(&pool, &photo_path, &average_time);
127
128 sauron.send_photo(photo_path.clone(), average_time).await;
129
130 insert_image_promise.await?;
131 }
132 Ok(())
133}
134
135fn scan_image_directory(dir: &Path) -> Result<Vec<PathBuf>> {
136 let mut image_files = Vec::new();
137 let supported_extensions = ["jpg", "jpeg", "png", "bmp", "gif", "tiff", "tif"];
138
139 for entry in WalkDir::new(dir).into_iter().filter_map(|e| e.ok()) {
140 let path = entry.path();
141
142 if path.is_file() {
143 if let Some(ext) = path.extension() {
144 let ext_lower = ext.to_string_lossy().to_lowercase();
145 if supported_extensions.contains(&ext_lower.as_str()) {
146 image_files.push(path.to_path_buf());
147 }
148 }
149 }
150 }
151
152 image_files.sort();
153
154 Ok(image_files)
155}
156
157async fn copy_image_to_output(source_path: &Path, output_dir: &Path) -> Result<String> {
159 let timestamp = Utc::now().timestamp_millis();
160
161 let extension = source_path
163 .extension()
164 .and_then(|ext| ext.to_str())
165 .unwrap_or("jpeg");
166
167 let filename = format!("IMG-{}.{}", timestamp, extension);
168 let dest_path = output_dir.join(&filename);
169
170 tokio::fs::copy(source_path, &dest_path)
172 .await
173 .with_context(|| {
174 format!(
175 "Failed to copy {} to {}",
176 source_path.display(),
177 dest_path.display()
178 )
179 })?;
180
181 Ok(dest_path.to_string_lossy().into_owned())
182}
183
184pub async fn start_fake_gnc(shutdown_token: tokio_util::sync::CancellationToken) -> Result<()> {
186 info!("Starting fake GNC actor");
187
188 tokio::task::spawn_blocking(move || {
189 if let Err(e) = run_fake_gnc_blocking(shutdown_token.clone()) {
190 error!("Fake GNC error: {}", e);
191 }
192 });
193
194 Ok(())
195}
196
197fn run_fake_gnc_blocking(
198 shutdown_token: tokio_util::sync::CancellationToken,
199) -> Result<(), Box<dyn std::error::Error>> {
200 let context = zmq::Context::new();
201 let publisher = context.socket(zmq::PUB)?;
202
203 let gnc_port = env::var("GNC_PORT").unwrap_or("5556".to_string());
204 let localhost = env::var("LOCALHOST").unwrap_or("127.0.0.1".to_string());
205 let gnc_addr = format!("tcp://{}:{}", localhost, gnc_port);
206
207 publisher.bind(&gnc_addr)?;
208 info!("Fake GNC publisher bound to {}", gnc_addr);
209
210 let mut time_counter = 0.0;
212 let telemetry_interval_ms: u64 = env::var("GNC_TELEMETRY_INTERVAL_MS")
213 .unwrap_or("100".to_string())
214 .parse()
215 .unwrap_or(100);
216
217 loop {
218 if shutdown_token.is_cancelled() {
219 info!("Fake GNC shutting down");
220 break;
221 }
222
223 std::thread::sleep(Duration::from_millis(telemetry_interval_ms));
224
225 let radius = 0.001_f64; let angle = time_counter * 0.1_f64;
228 let lat = 40.4237 + radius * angle.cos();
229 let long = -86.9212 + radius * angle.sin();
230 let heading = (angle * 180.0 / std::f64::consts::PI) % 360.0;
231 let alt = 100.0 + 20.0 * (time_counter * 0.05_f64).sin();
232
233 time_counter += telemetry_interval_ms as f64 / 1000.0;
234
235 let gnc_telem = GNCTelemetry {
236 header: "GNC_TELEMETRY".to_string(),
237 lat,
238 long,
239 alt,
240 heading,
241 time: Utc::now().timestamp_millis() as f64 / 1000.0,
242 };
243
244 let message = MailboxMessage {
245 msg_type: MailboxMessageType::GNCTelemetry,
246 data: json!({
247 "header": gnc_telem.header,
248 "lat": gnc_telem.lat,
249 "long": gnc_telem.long,
250 "alt": gnc_telem.alt,
251 "heading": gnc_telem.heading,
252 "time": gnc_telem.time,
253 }),
254 };
255
256 let msg_json = serde_json::to_string(&message)?;
257 if let Err(e) = publisher.send(&msg_json, 0) {
258 error!("Failed to send GNC telemetry: {}", e);
259 } else {
260 debug!(
261 "Fake GNC sent telemetry: lat={}, long={}, alt={}",
262 lat, long, alt
263 );
264 }
265 }
266 Ok(())
267}