ironbird/
lib.rs

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    // Get still frames directory from environment variable
58    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    // Scan directory for image files
70    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    // Output directory for copied images (where Sauron expects them)
86    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
157/// Copies an image file to the output directory with a timestamped filename
158async fn copy_image_to_output(source_path: &Path, output_dir: &Path) -> Result<String> {
159    let timestamp = Utc::now().timestamp_millis();
160
161    // Preserve original extension
162    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    // Copy the file
171    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
184/// Starts the fake GNC actor that simulates sending telemetry via ZMQ
185pub 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    // Simulate movement
211    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        // Simulate circular flight pattern (Purdue University coordinates)
226        let radius = 0.001_f64; // ~100 meters
227        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}