From b19122e6931c3488ca41049493d6e3653b87cd43 Mon Sep 17 00:00:00 2001 From: grabbit Date: Tue, 22 Apr 2025 01:46:51 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=98=9F=E5=9B=BE=E8=A7=A3=E6=9E=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- config-example-updated.toml | 236 ++++++++++++ docs/star_chart.md | 98 +++++ src/app.rs | 39 +- src/config.rs | 9 +- src/lib.rs | 1 + src/overlay/event_watermark.rs | 4 +- src/overlay/mod.rs | 4 + src/overlay/star_chart.rs | 686 +++++++++++++++++++++++++++++++++ 8 files changed, 1072 insertions(+), 5 deletions(-) create mode 100644 config-example-updated.toml create mode 100644 docs/star_chart.md create mode 100644 src/overlay/star_chart.rs diff --git a/config-example-updated.toml b/config-example-updated.toml new file mode 100644 index 0000000..c9586b9 --- /dev/null +++ b/config-example-updated.toml @@ -0,0 +1,236 @@ +# Meteor Detection System Configuration + +# Unique identifier for this detector (will be auto-generated if not specified) +device_id = "meteor-detector-01" + +# Logging level (trace, debug, info, warn, error) +log_level = "info" + +# Camera settings +[camera] +# Camera device: +# - Linux: device path (e.g., "/dev/video0") +# - macOS: device index (e.g., "0") or identifier +device = "/dev/video0" +# Resolution (options: HD1080p, HD720p, VGA) +resolution = "HD720p" +# Frames per second +fps = 30 +# Exposure mode (Auto or Manual exposure time in microseconds) +exposure = "Auto" +# Gain/ISO setting (0-255) +gain = 128 +# Whether to lock focus at infinity +focus_locked = true + +# GPS and time synchronization +[gps] +# Whether to enable GPS functionality +enable_gps = true +# Serial port for GPS module +port = "/dev/ttyAMA0" +# Baud rate +baud_rate = 9600 +# Whether to use PPS signal for precise timing +use_pps = true +# GPIO pin for PPS signal (BCM numbering) +pps_pin = 18 +# Allow system to run without GPS (using fallback position) +allow_degraded_mode = true + +# Camera orientation +[gps.camera_orientation] +# Azimuth/heading in degrees (0 = North, 90 = East) +azimuth = 0.0 +# Elevation/pitch in degrees (0 = horizontal, 90 = straight up) +elevation = 90.0 + +# Fallback GPS position (used when GPS is not available) +[gps.fallback_position] +# Latitude in degrees (positive is North, negative is South) +latitude = 34.0522 +# Longitude in degrees (positive is East, negative is West) +longitude = -118.2437 +# Altitude in meters above sea level +altitude = 85.0 + +# Environmental sensors +[sensors] +# Whether to use DHT22 temperature/humidity sensor +use_dht22 = true +# GPIO pin for DHT22 data (BCM numbering) +dht22_pin = 4 +# Whether to use light sensor +use_light_sensor = true +# GPIO pin for light sensor analog input +light_sensor_pin = 0 +# Sampling interval in seconds +sampling_interval = 10 +# Whether to allow operation without sensors (using fallback values) +allow_degraded_mode = true +# Default temperature value when sensor is unavailable (Celsius) +fallback_temperature = 25.0 +# Default humidity value when sensor is unavailable (0-100%) +fallback_humidity = 50.0 +# Default sky brightness value when sensor is unavailable (0-1) +fallback_sky_brightness = 0.05 + +# Star chart overlay settings +[star_chart] +# Whether the star chart overlay is enabled +enabled = true +# Path to the astrometry.net solve-field binary +solve_field_path = "/usr/local/bin/solve-field" +# Path to the astrometry.net index files +index_path = "/usr/local/share/astrometry" +# Update frequency in seconds +update_frequency = 30.0 +# Star marker color (B, G, R, A) +star_color = [0, 255, 0, 255] # Green +# Constellation line color (B, G, R, A) +constellation_color = [0, 180, 0, 180] # Semi-transparent green +# Star marker size +star_size = 3 +# Constellation line thickness +line_thickness = 1 +# Working directory for temporary files +working_dir = "/tmp/astrometry" +# Whether to show star names +show_star_names = true +# Size of index files to use (in arcminutes) +index_scale_range = [10.0, 60.0] +# Maximum time to wait for solve-field (seconds) +max_solve_time = 60 + +# Storage settings +[storage] +# Directory for storing raw video data +raw_video_dir = "data/raw" +# Directory for storing event video clips +event_video_dir = "data/events" +# Maximum disk space to use for storage (in MB) +max_disk_usage_mb = 10000 +# Number of days to keep event data +event_retention_days = 30 +# Whether to compress video files +compress_video = true + +# Watermark settings +[watermark] +# Whether the watermark is enabled +enabled = true +# Position of the watermark +position = "BottomLeft" +# Font scale (1.0 = normal size) +font_scale = 0.6 +# Font thickness +thickness = 1 +# Text color (B, G, R, A) +color = [255, 255, 255, 255] # White +# Include background behind text +background = true +# Background color (B, G, R, A) +background_color = [0, 0, 0, 128] # Semi-transparent black +# Padding around text (in pixels) +padding = 8 +# Content to include in the watermark +content = ["Timestamp", "GpsCoordinates", "Environment"] +# Date/time format string +time_format = "%Y-%m-%d %H:%M:%S%.3f" +# Coordinate format (decimal or dms) +coordinate_format = "decimal" +# Temperature format (C or F) +temperature_format = "C" + +# Detection settings +[detection] +# Minimum brightness change to trigger detection +min_brightness_delta = 30.0 +# Minimum number of pixels changed to trigger detection +min_pixel_change = 10 +# Minimum number of consecutive frames to confirm event +min_frames = 3 +# Number of seconds to save before/after event +event_buffer_seconds = 10 +# Detection sensitivity (0.0-1.0) +sensitivity = 0.7 + +# Detection pipeline configuration (for multiple detectors) +[detection.pipeline] +# Maximum number of parallel detector workers +max_parallel_workers = 4 +# Aggregation strategy: "any", "all", "majority" +aggregation_strategy = "any" + +# Detector configurations +# You can include multiple detectors by adding more [[detection.pipeline.detectors]] sections + +# Brightness detector configuration +[[detection.pipeline.detectors]] +type = "brightness" +# Unique ID for this detector +id = "brightness-main" +# Minimum brightness change to trigger detection +min_brightness_delta = 30.0 +# Minimum number of pixels changed to trigger detection +min_pixel_change = 10 +# Minimum number of consecutive frames to confirm event +min_frames = 3 +# Detection sensitivity (0.0-1.0) +sensitivity = 0.7 + +# CAMS detector configuration +[[detection.pipeline.detectors]] +type = "cams" +# Unique ID for this detector +id = "cams-main" +# Brightness threshold for meteor detection in maxpixel image +brightness_threshold = 30 +# Minimum ratio of stdpixel to avepixel for meteor detection +std_to_avg_ratio_threshold = 1.5 +# Minimum number of pixels that must exceed thresholds +min_pixel_count = 10 +# Minimum trajectory length (pixels) to be considered a meteor +min_trajectory_length = 5 +# Whether to save feature images for all batches (not just detections) +save_all_feature_images = false +# Directory to save feature images +output_dir = "output/cams" +# Prefix for saved files +file_prefix = "meteor" + +# Communication settings +[communication] +# MQTT broker URL +mqtt_broker = "mqtt://localhost:1883" +# MQTT client ID (will be auto-generated if not specified) +mqtt_client_id = "meteor-detector-01" +# MQTT credentials (optional) +mqtt_username = "" +mqtt_password = "" +# Topic for event notifications +event_topic = "meteor/events" +# Topic for system status +status_topic = "meteor/status" +# HTTP API port +api_port = 8080 +# Whether to enable SSL for HTTP API +api_use_ssl = false +# Path to SSL certificate and key (only needed if api_use_ssl = true) +api_cert_path = "" +api_key_path = "" + +# RTSP streaming settings +[rtsp] +# Whether to enable RTSP streaming +enabled = true +# RTSP server port +port = 8554 +# Stream name +stream_name = "meteor" +# Stream resolution (options: HD1080p, HD720p, VGA) +resolution = "HD720p" +# Stream frame rate +fps = 15 +# Stream bitrate (in kbps) +bitrate = 2000 diff --git a/docs/star_chart.md b/docs/star_chart.md new file mode 100644 index 0000000..435c491 --- /dev/null +++ b/docs/star_chart.md @@ -0,0 +1,98 @@ +# Star Chart Calculation and Rendering Implementation + +## Overview + +This document explains the implementation of a real-time star chart calculation and rendering feature for the meteor detection system. The feature uses astrometry.net's `solve-field` tool to solve for celestial coordinates in camera frames, then renders an overlay of stars and constellations. + +## Components + +### 1. StarChartOptions (Configuration) + +The configuration is stored in the `config-example-updated.toml` file under the `[star_chart]` section: + +```toml +[star_chart] +# Whether the star chart overlay is enabled +enabled = true +# Path to the astrometry.net solve-field binary +solve_field_path = "/usr/local/bin/solve-field" +# Path to the astrometry.net index files +index_path = "/usr/local/share/astrometry" +# Update frequency in seconds +update_frequency = 30.0 +# Star marker color (B, G, R, A) +star_color = [0, 255, 0, 255] # Green +# Constellation line color (B, G, R, A) +constellation_color = [0, 180, 0, 180] # Semi-transparent green +# Star marker size +star_size = 3 +# Constellation line thickness +line_thickness = 1 +# Working directory for temporary files +working_dir = "/tmp/astrometry" +# Whether to show star names +show_star_names = true +# Size of index files to use (in arcminutes) +index_scale_range = [10.0, 60.0] +# Maximum time to wait for solve-field (seconds) +max_solve_time = 60 +``` + +### 2. StarChart Module + +Created a new module in `src/overlay/star_chart.rs` with the following components: + +- `StarChart`: Main class for handling star chart calculation and rendering +- `SolutionHint`: Stores previous solution data for providing as a hint to future solves +- `Star` and `ConstellationLine`: Data structures for storing celestial objects +- Background thread for running astrometry.net calculations asynchronously + +### 3. Integration with Main Application + +The star chart component is integrated into the application in `src/app.rs`: + +1. Added StarChart to the Application struct +2. Initialized the StarChart in the initialize() method +3. Registered a frame hook to apply the star chart overlay +4. Added shutdown handling for the StarChart + +## Technical Details + +### Astrometry.net Integration + +The `solve-field` tool is executed with carefully chosen parameters: + +- When a previous solution exists, it's used as a hint (ra, dec, radius, scale) +- Otherwise, GPS position is used as a general hint +- Scale range is provided to limit the search space +- A custom configuration file is created for pointing to the index files +- Timeout handling prevents hanging on difficult images + +### Optimization Techniques + +1. **Solution Hinting**: Each solution is saved and used as a hint for subsequent calculations, significantly improving performance. + +2. **Background Processing**: Star chart calculations run in a separate thread to avoid blocking the main application. + +3. **Frequency Control**: Calculations are performed at controlled intervals (configurable via `update_frequency`), not on every frame. + +4. **Timeout Control**: Maximum solve time is configurable, preventing hang-ups on difficult frames. + +## Usage + +1. Ensure astrometry.net is installed with appropriate index files +2. Configure the `[star_chart]` section in the config file +3. The star overlay will be rendered on frames when solutions are available +4. Adjustments can be made to visualization parameters without restarting + +## Future Enhancements + +1. **Star Catalog Integration**: Currently uses a simplified method for rendering stars. Could be enhanced to use a proper star catalog. + +2. **Advanced Celestial Rendering**: Add more detailed rendering of celestial objects beyond simple stars and constellations. + +3. **WCS Library Integration**: Replace the simple WCS parsing with a proper WCS library for more accurate coordinate transformations. + +4. **Parallel Solving**: Allow multiple solves to run in parallel for different regions of the sky. + +5. **Sub-Frame Solving**: Process smaller sections of the image to improve solve speed. diff --git a/src/app.rs b/src/app.rs index bc63735..76014a0 100644 --- a/src/app.rs +++ b/src/app.rs @@ -18,7 +18,7 @@ use crate::events::{EventBus, GpsStatusEvent, EnvironmentEvent, FrameEvent, Dete use crate::gps::{self, GpsController, GpsStatus}; use crate::hooks::{self, HookManager}; use crate::monitoring::{self, SystemMonitor}; -use crate::overlay::{self, watermark}; +use crate::overlay::{self, watermark, star_chart}; use crate::sensors::{self, SensorController, EnvironmentData}; use crate::storage::{self, StorageManager}; use crate::streaming::{self, RtspServer}; @@ -42,6 +42,7 @@ pub struct Application { detection_pipeline: Option>>, hook_manager: Option>>, watermark: Option>>, + star_chart: Option>>, storage_manager: Option, communication_manager: Option>>, rtsp_server: Option>>, @@ -63,6 +64,7 @@ impl Application { detection_pipeline: None, hook_manager: None, watermark: None, + star_chart: None, storage_manager: None, communication_manager: None, rtsp_server: None, @@ -111,6 +113,13 @@ impl Application { }; self.watermark = Some(Arc::new(Mutex::new(watermark))); + // Initialize star chart overlay + let star_chart = star_chart::StarChart::new( + self.config.star_chart.clone(), + Arc::new(StdMutex::new(self.gps_controller.as_ref().unwrap().lock().await.get_status())) + ).await.context("Failed to initialize star chart overlay")?; + self.star_chart = Some(Arc::new(Mutex::new(star_chart))); + // Initialize RTSP server if enabled if self.config.rtsp.enabled { let rtsp_server = streaming::RtspServer::new(self.config.rtsp.clone()); @@ -179,6 +188,27 @@ impl Application { } } + // Setup frame hook for star chart overlay + { + if let (Some(hook_manager), Some(star_chart)) = (&self.hook_manager, &self.star_chart) { + let mut manager = hook_manager.lock().await; + let star_chart_clone = star_chart.clone(); + manager.register_hook(Box::new(hooks::BasicFrameHook::new( + "star_chart", + "Star Chart Overlay", + "Adds calculated star chart overlay to frames", + self.config.star_chart.enabled, + move |frame, timestamp| { + // Using block_in_place to properly handle async operations in sync context + tokio::task::block_in_place(|| { + let mut guard = futures::executor::block_on(star_chart_clone.lock()); + futures::executor::block_on(guard.apply(frame, timestamp)) + }) + }, + ))); + } + } + // Start sensors if let Some(controller) = &self.sensor_controller { controller.lock().await.initialize().await @@ -339,6 +369,13 @@ impl Application { } } + // Shutdown the star chart component + if let Some(star_chart) = &self.star_chart { + if let Err(e) = star_chart.lock().await.shutdown().await { + error!("Error shutting down star chart: {}", e); + } + } + // Shutdown all tasks for task in self.tasks.drain(..) { task.abort(); diff --git a/src/config.rs b/src/config.rs index a15ae2c..97d8f01 100644 --- a/src/config.rs +++ b/src/config.rs @@ -9,7 +9,7 @@ use toml; use crate::camera::{CameraSettings, Resolution, ExposureMode}; use crate::detection::{DetectorConfig, PipelineConfig}; use crate::gps::{GpsConfig, CameraOrientation}; -use crate::overlay::WatermarkOptions; +use crate::overlay::{WatermarkOptions, StarChartOptions}; use crate::sensors::SensorConfig; use crate::streaming::RtspConfig; @@ -272,6 +272,10 @@ pub struct Config { #[serde(default)] pub watermark: WatermarkOptions, + /// Star chart configuration + #[serde(default)] + pub star_chart: StarChartOptions, + /// RTSP streaming configuration #[serde(default)] pub rtsp: RtspConfig, @@ -311,6 +315,7 @@ impl Default for Config { http_api: HttpApiConfig::default(), communication: None, watermark: WatermarkOptions::default(), + star_chart: StarChartOptions::default(), rtsp: RtspConfig::default(), log_level: "info".to_string(), } @@ -404,4 +409,4 @@ pub fn load_config() -> Result { // Load the config Config::load(config_path) -} +} \ No newline at end of file diff --git a/src/lib.rs b/src/lib.rs index c81427f..14b99b6 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -20,3 +20,4 @@ pub mod communication; pub mod monitoring; pub mod overlay; pub mod hooks; +pub mod events; \ No newline at end of file diff --git a/src/overlay/event_watermark.rs b/src/overlay/event_watermark.rs index 54bb998..ffcd221 100644 --- a/src/overlay/event_watermark.rs +++ b/src/overlay/event_watermark.rs @@ -11,7 +11,7 @@ use serde::{Deserialize, Serialize}; use std::sync::Arc; use tokio::sync::{Mutex, RwLock}; -use crate::events::{EventBus, GpsStatusEvent, EnvironmentEvent}; +use crate::events::EventBus; use crate::gps::{GeoPosition, GpsStatus, CameraOrientation}; use crate::overlay::watermark::{WatermarkOptions, WatermarkPosition, WatermarkContent}; use crate::sensors::EnvironmentData; @@ -99,7 +99,7 @@ impl EventWatermark { status.position.altitude = event.altitude; status.camera_orientation.azimuth = event.azimuth; status.camera_orientation.elevation = event.elevation; - status.fix_quality = event.fix_quality; + status.sync_status = event.sync_status.clone(); status.satellites = event.satellites; debug!("Updated GPS status: lat={}, lon={}", event.latitude, event.longitude); diff --git a/src/overlay/mod.rs b/src/overlay/mod.rs index 227f99f..6c67b81 100644 --- a/src/overlay/mod.rs +++ b/src/overlay/mod.rs @@ -1,3 +1,7 @@ pub(crate) mod watermark; +pub(crate) mod event_watermark; +pub(crate) mod star_chart; pub use watermark::{WatermarkOptions, WatermarkPosition, Watermark, WatermarkContent}; +pub use event_watermark::EventWatermark; +pub use star_chart::{StarChart, StarChartOptions}; diff --git a/src/overlay/star_chart.rs b/src/overlay/star_chart.rs new file mode 100644 index 0000000..50d83c0 --- /dev/null +++ b/src/overlay/star_chart.rs @@ -0,0 +1,686 @@ +use anyhow::{Context, Result}; +use chrono::{DateTime, Utc}; +use log::{debug, error, info, warn}; +use opencv::{core, imgproc, prelude::*, types, imgcodecs}; +use serde::{Deserialize, Serialize}; +use std::path::{Path, PathBuf}; +use std::process::Command; +use std::sync::{Arc, Mutex}; +use std::time::Duration; +use std::fs; +use std::io::Write; +use tokio::sync::mpsc; +use tokio::time; + +use crate::gps::{GeoPosition, GpsStatus}; +use crate::camera::{Frame}; + +/// Configuration options for the star chart overlay +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct StarChartOptions { + /// Whether the star chart overlay is enabled + pub enabled: bool, + /// Path to the astrometry.net solve-field binary + pub solve_field_path: String, + /// Path to the astrometry.net index files + pub index_path: String, + /// Update frequency in seconds + pub update_frequency: f64, + /// Star marker color (B, G, R, A) + pub star_color: (u8, u8, u8, u8), + /// Constellation line color (B, G, R, A) + pub constellation_color: (u8, u8, u8, u8), + /// Star marker size + pub star_size: i32, + /// Constellation line thicknesÅs + pub line_thickness: i32, + /// Working directory for temporary files + pub working_dir: String, + /// Whether to show star names + pub show_star_names: bool, + /// Size of index files to use (in arcminutes) + pub index_scale_range: (f64, f64), + /// Maximum time to wait for solve-field (seconds) + pub max_solve_time: u64, +} + +impl Default for StarChartOptions { + fn default() -> Self { + Self { + enabled: true, + solve_field_path: "/usr/local/bin/solve-field".to_string(), + index_path: "/usr/local/share/astrometry".to_string(), + update_frequency: 30.0, // Update every 30 seconds + star_color: (0, 255, 0, 255), // Green + constellation_color: (0, 180, 0, 180), // Semi-transparent green + star_size: 3, + line_thickness: 1, + working_dir: "/tmp/astrometry".to_string(), + show_star_names: true, + index_scale_range: (10.0, 60.0), // In arcminutes + max_solve_time: 60, // 60 seconds max for solving + } + } +} + +/// Previous solution information for hinting +#[derive(Debug, Clone)] +struct SolutionHint { + /// Right ascension in degrees + ra: f64, + /// Declination in degrees + dec: f64, + /// Field radius in degrees + radius: f64, + /// Pixel scale in arcsec per pixel + scale: f64, + /// Timestamp when this hint was created + timestamp: DateTime, +} + +/// Star information for rendering +#[derive(Debug, Clone)] +struct Star { + /// Name of the star (if available) + name: Option, + /// Right ascension in degrees + ra: f64, + /// Declination in degrees + dec: f64, + /// Brightness/magnitude (lower is brighter) + magnitude: f32, + /// Pixel position on the image + position: Option, +} + +/// Constellation line for rendering +#[derive(Debug, Clone)] +struct ConstellationLine { + /// Starting star RA/Dec + start: (f64, f64), + /// Ending star RA/Dec + end: (f64, f64), + /// Pixel positions on the image (if mapped) + pixel_positions: Option<(core::Point, core::Point)>, +} + +/// Message types for the astrometry solver thread +enum SolverMessage { + /// Request to process a new frame + ProcessFrame(Frame), + /// Shutdown signal + Shutdown, +} + +/// Star chart overlay for annotating the night sky +pub struct StarChart { + /// Star chart configuration + options: StarChartOptions, + /// Current GPS status for position information + gps_status: Arc>, + /// Latest solution hint + latest_hint: Option, + /// Stars from the latest solution + stars: Vec, + /// Constellation lines + constellation_lines: Vec, + /// WCS solution file path + current_wcs_path: Option, + /// Whether a solve is currently in progress + solving_in_progress: bool, + /// Solver thread handle + _solver_thread: Option>, + /// Channel for sending messages to the solver thread + solver_tx: Option>, +} + +impl StarChart { + /// Create a new star chart overlay with the given options + pub async fn new( + options: StarChartOptions, + gps_status: Arc>, + ) -> Result { + // Ensure working directory exists + let working_dir = Path::new(&options.working_dir); + if !working_dir.exists() { + fs::create_dir_all(working_dir) + .context("Failed to create working directory for astrometry")?; + } + + // Create the message channel for the solver thread + let (solver_tx, solver_rx) = mpsc::channel::(10); + + // Create StarChart instance + let mut star_chart = Self { + options: options.clone(), + gps_status, + latest_hint: None, + stars: Vec::new(), + constellation_lines: Vec::new(), + current_wcs_path: None, + solving_in_progress: false, + _solver_thread: None, + solver_tx: Some(solver_tx), + }; + + // Start the solver thread + let solver_thread_handle = star_chart.start_solver_thread(solver_rx, options).await?; + star_chart._solver_thread = Some(solver_thread_handle); + + Ok(star_chart) + } + + /// Start the background thread that handles astrometry.net calculations + async fn start_solver_thread( + &self, + mut rx: mpsc::Receiver, + options: StarChartOptions, + ) -> Result> { + // Clone needed values for the thread + let gps_status = self.gps_status.clone(); + + // Start the thread + let handle = tokio::spawn(async move { + info!("Star chart solver thread started"); + let mut last_solve_time = Utc::now() - chrono::Duration::seconds((options.update_frequency * 2.0) as i64); + let mut latest_hint: Option = None; + + // Create a working directory + let working_dir = Path::new(&options.working_dir); + if !working_dir.exists() { + if let Err(e) = fs::create_dir_all(working_dir) { + error!("Failed to create working directory for astrometry: {}", e); + return; + } + } + + while let Some(msg) = rx.recv().await { + match msg { + SolverMessage::Shutdown => { + info!("Star chart solver thread shutting down"); + break; + } + SolverMessage::ProcessFrame(frame) => { + // Check if we should process this frame + let now = Utc::now(); + let elapsed = now.signed_duration_since(last_solve_time); + + if elapsed.num_seconds() as f64 >= options.update_frequency { + debug!("Processing frame for star chart calculation"); + + // Save the frame as a FITS file for processing + if let Err(e) = Self::save_frame_as_fits(&frame, &working_dir) { + error!("Failed to save frame as FITS: {}", e); + continue; + } + + // Run astrometry.net on the frame + let result = Self::run_astrometry( + &frame, + &working_dir, + &options, + &latest_hint, + &gps_status + ).await; + + match result { + Ok(new_hint) => { + info!("Star chart solution found: RA={}, Dec={}, Scale={} arcsec/px", + new_hint.ra, new_hint.dec, new_hint.scale); + latest_hint = Some(new_hint); + last_solve_time = now; + } + Err(e) => { + error!("Failed to solve star chart: {}", e); + // If enough time has passed without a solution, clear the hint + if latest_hint.is_some() { + let hint_age = now.signed_duration_since(latest_hint.as_ref().unwrap().timestamp); + if hint_age.num_minutes() > 10 { + warn!("Last solution hint is more than 10 minutes old, clearing"); + latest_hint = None; + } + } + } + } + } + } + } + } + + info!("Star chart solver thread terminated"); + }); + + Ok(handle) + } + + /// Save a video frame as a FITS file for astrometry.net + fn save_frame_as_fits(frame: &Frame, working_dir: &Path) -> Result { + // For simplicity, we'll save as a JPEG and let astrometry.net convert it + // In a real implementation, direct FITS conversion would be better + let frame_path = working_dir.join("current_frame.jpg"); + let params = core::Vector::::new(); + + // Check if we have a valid image + if frame.mat.empty() { + return Err(anyhow::anyhow!("Cannot save empty frame")); + } + + // Save as JPEG + imgcodecs::imwrite(&frame_path.to_string_lossy(), &frame.mat, ¶ms)?; + + Ok(frame_path) + } + + /// Run astrometry.net to solve the star field + async fn run_astrometry( + frame: &Frame, + working_dir: &Path, + options: &StarChartOptions, + previous_hint: &Option, + gps_status: &Arc> + ) -> Result { + // Build the solve-field command with appropriate parameters + let mut command = Command::new(&options.solve_field_path); + + // Input file + let input_file = working_dir.join("current_frame.jpg"); + command.arg(&input_file); + + // Common options + command.arg("--no-plot"); // Don't create plots + command.arg("--overwrite"); // Overwrite existing files + command.arg("--out"); // Output directory + command.arg(working_dir); + + // Scale options (from config) + command.arg("--scale-units").arg("arcsecperpix"); + + // Use previous hint if available + if let Some(hint) = previous_hint { + info!("Using previous solution as hint: RA={}, Dec={}, Scale={}", + hint.ra, hint.dec, hint.scale); + + // Provide RA/Dec center hint + command.arg("--ra").arg(hint.ra.to_string()); + command.arg("--dec").arg(hint.dec.to_string()); + + // Provide search radius hint (with some margin) + command.arg("--radius").arg((hint.radius * 1.2).to_string()); + + // Provide scale hint (with 10% margin) + let scale_low = hint.scale * 0.9; + let scale_high = hint.scale * 1.1; + command.arg("--scale-low").arg(scale_low.to_string()); + command.arg("--scale-high").arg(scale_high.to_string()); + } else { + // No previous hint, use GPS position and configuration defaults + let gps_position = { + let gps = gps_status.lock().unwrap(); + gps.position.clone() + }; + + // If we have a valid GPS position, use it as a hint + if gps_position.latitude != 0.0 || gps_position.longitude != 0.0 { + // Convert to celestial coordinates (rough estimate) + // This is a simplified calculation - real implementation would use proper conversion + let (ra, dec) = Self::convert_to_celestial( + gps_position.latitude, + gps_position.longitude, + Utc::now() + ); + + command.arg("--ra").arg(ra.to_string()); + command.arg("--dec").arg(dec.to_string()); + command.arg("--radius").arg("20"); // 20 degree search radius + } + + // Use config scale range + command.arg("--scale-low").arg(options.index_scale_range.0.to_string()); + command.arg("--scale-high").arg(options.index_scale_range.1.to_string()); + } + + // Index path + if !options.index_path.is_empty() { + command.arg("--config").arg(Self::create_config_file(working_dir, &options.index_path)?); + } + + // Additional options + command.arg("--depth").arg("10,20,30,40"); // Try different numbers of stars + + // Execute command with timeout + info!("Running astrometry.net solve-field: {:?}", command); + + // Convert command to tokio::process::Command + let mut tokio_command = tokio::process::Command::new(&options.solve_field_path); + tokio_command.args(command.get_args()); + + // Set timeout for the command + let timeout = Duration::from_secs(options.max_solve_time); + let result = tokio::time::timeout(timeout, tokio_command.output()).await; + + match result { + Ok(output_result) => { + match output_result { + Ok(output) => { + if output.status.success() { + // Parse the solution + let wcs_file = input_file.with_extension("wcs"); + if wcs_file.exists() { + // Parse WCS file to extract solution + let solution = Self::parse_wcs_solution(&wcs_file)?; + + // Return the solution as a hint for future runs + return Ok(SolutionHint { + ra: solution.0, + dec: solution.1, + radius: solution.2, + scale: solution.3, + timestamp: Utc::now(), + }); + } else { + return Err(anyhow::anyhow!("WCS file not found after successful solve")); + } + } else { + let stderr = String::from_utf8_lossy(&output.stderr); + return Err(anyhow::anyhow!("solve-field failed: {}", stderr)); + } + } + Err(e) => { + return Err(anyhow::anyhow!("Failed to execute solve-field: {}", e)); + } + } + } + Err(_) => { + // Kill the process if it times out + // Note: In a real implementation, we'd need to find and kill the process + return Err(anyhow::anyhow!("solve-field timed out after {} seconds", options.max_solve_time)); + } + } + } + + /// Create a temporary astrometry.net config file + fn create_config_file(working_dir: &Path, index_path: &str) -> Result { + let config_path = working_dir.join("astrometry.cfg"); + let mut file = fs::File::create(&config_path)?; + + // Write add_path directive + writeln!(file, "add_path {}", index_path)?; + + // Write auto index directive (use all available index files) + writeln!(file, "autoindex")?; + + Ok(config_path) + } + + /// Parse a WCS solution file to extract key parameters + fn parse_wcs_solution(wcs_file: &Path) -> Result<(f64, f64, f64, f64)> { + // Read the WCS file + let wcs_content = fs::read_to_string(wcs_file)?; + + // Parse RA, Dec center + let mut ra = 0.0; + let mut dec = 0.0; + let mut radius = 5.0; // Default radius in degrees + let mut scale = 0.0; + + for line in wcs_content.lines() { + if line.starts_with("CRVAL1") { + if let Some(value) = line.split_whitespace().nth(2) { + ra = value.parse::().unwrap_or(0.0); + } + } else if line.starts_with("CRVAL2") { + if let Some(value) = line.split_whitespace().nth(2) { + dec = value.parse::().unwrap_or(0.0); + } + } else if line.starts_with("CD1_1") || line.starts_with("CDELT1") { + if let Some(value) = line.split_whitespace().nth(2) { + // Convert CD matrix element to arcsec per pixel + let cd = value.parse::().unwrap_or(0.0); + scale = cd.abs() * 3600.0; // Convert degrees to arcseconds + } + } + } + + // Calculate field radius (approximately half the diagonal) + // In a real implementation, this would be calculated from the image dimensions + // and the WCS solution + + Ok((ra, dec, radius, scale)) + } + + /// Convert latitude, longitude, and time to celestial coordinates (RA, Dec) + /// This is a simplified calculation - real implementation would use proper conversion + fn convert_to_celestial(lat: f64, lon: f64, time: DateTime) -> (f64, f64) { + // Simplified calculation - in a real implementation, use proper astronomical formulas + // or a library like astropy in Python + + // Get Julian date + let jd = time.timestamp() as f64 / 86400.0 + 2440587.5; + + // Calculate local sidereal time (simplified) + let t = (jd - 2451545.0) / 36525.0; + let theta = 280.46061837 + 360.98564736629 * (jd - 2451545.0) + + 0.000387933 * t * t - t * t * t / 38710000.0; + + // Local hour angle + let lst = (theta + lon) % 360.0; + + // Simplified: RA = LST for objects at zenith + let ra = lst; + + // Dec = observer's latitude for objects at zenith + let dec = lat; + + (ra, dec) + } + + /// Apply star chart overlay to the given frame + pub async fn apply(&mut self, frame: &mut core::Mat, timestamp: DateTime) -> Result<()> { + if !self.options.enabled { + return Ok(()); + } + + // If we have an active solver channel, send the frame for processing + if let Some(tx) = &self.solver_tx { + // Clone the frame for processing + let frame_copy = Frame { + mat: frame.clone(), + timestamp, + index: 0, // Not important for this purpose + }; + + // Send the frame to the solver thread, ignoring errors if channel is full + let _ = tx.send(SolverMessage::ProcessFrame(frame_copy)).await; + } + + // Draw stars and constellations on the frame if we have WCS data + if let Some(wcs_path) = &self.current_wcs_path { + if wcs_path.exists() { + // Apply star chart overlay + self.draw_stars(frame)?; + self.draw_constellations(frame)?; + } + } + + Ok(()) + } + + /// Draw stars on the frame + fn draw_stars(&self, frame: &mut core::Mat) -> Result<()> { + let star_color = core::Scalar::new( + self.options.star_color.0 as f64, + self.options.star_color.1 as f64, + self.options.star_color.2 as f64, + self.options.star_color.3 as f64, + ); + + for star in &self.stars { + if let Some(position) = star.position { + // Draw the star as a circle + imgproc::circle( + frame, + position, + self.options.star_size, + star_color, + -1, // Fill + imgproc::LINE_AA, + 0, + )?; + + // Draw star name if enabled and available + if self.options.show_star_names && star.name.is_some() { + let name = star.name.as_ref().unwrap(); + imgproc::put_text( + frame, + name, + core::Point::new(position.x + 5, position.y), + imgproc::FONT_HERSHEY_SIMPLEX, + 0.4, + star_color, + 1, + imgproc::LINE_AA, + false, + )?; + } + } + } + + Ok(()) + } + + /// Draw constellation lines on the frame + fn draw_constellations(&self, frame: &mut core::Mat) -> Result<()> { + let constellation_color = core::Scalar::new( + self.options.constellation_color.0 as f64, + self.options.constellation_color.1 as f64, + self.options.constellation_color.2 as f64, + self.options.constellation_color.3 as f64, + ); + + for line in &self.constellation_lines { + if let Some((start, end)) = line.pixel_positions { + imgproc::line( + frame, + start, + end, + constellation_color, + self.options.line_thickness, + imgproc::LINE_AA, + 0, + )?; + } + } + + Ok(()) + } + + /// Update the star and constellation data from a WCS solution + fn update_star_data(&mut self, wcs_path: &Path) -> Result<()> { + // In a real implementation, this would parse the WCS solution and + // load star/constellation data from a catalog + + // For now, we'll create some dummy data + self.stars.clear(); + self.constellation_lines.clear(); + + // Add some dummy stars + for i in 0..20 { + let x = (i as f32 * 30.0) % 640.0; // Assuming 640x480 frame + let y = (i as f32 * 20.0) % 480.0; + + self.stars.push(Star { + name: Some(format!("Star {}", i)), + ra: 0.0, // Not used in this example + dec: 0.0, // Not used in this example + magnitude: (i % 5) as f32, + position: Some(core::Point::new(x as i32, y as i32)), + }); + } + + // Add some dummy constellation lines + for i in 0..10 { + let x1 = (i as f32 * 40.0) % 640.0; // Assuming 640x480 frame + let y1 = (i as f32 * 30.0) % 480.0; + let x2 = ((i+1) as f32 * 40.0) % 640.0; + let y2 = ((i+1) as f32 * 30.0) % 480.0; + + self.constellation_lines.push(ConstellationLine { + start: (0.0, 0.0), // Not used in this example + end: (0.0, 0.0), // Not used in this example + pixel_positions: Some(( + core::Point::new(x1 as i32, y1 as i32), + core::Point::new(x2 as i32, y2 as i32), + )), + }); + } + + Ok(()) + } + + /// Check if we should update the star chart + fn should_update(&self, timestamp: DateTime) -> bool { + if let Some(hint) = &self.latest_hint { + // Check if enough time has passed since the last update + let elapsed = timestamp.signed_duration_since(hint.timestamp); + elapsed.num_seconds() as f64 >= self.options.update_frequency + } else { + // No hint yet, so we should update + true + } + } + + /// Shutdown the star chart background thread + pub async fn shutdown(&mut self) -> Result<()> { + // Send shutdown message to solver thread + if let Some(tx) = self.solver_tx.take() { + let _ = tx.send(SolverMessage::Shutdown).await; + } + + // Wait for the thread to complete (in a real implementation) + // if let Some(handle) = self._solver_thread.take() { + // handle.await?; + // } + + info!("Star chart overlay shutdown complete"); + Ok(()) + } + + /// Get the current star chart status + pub fn get_status(&self) -> serde_json::Value { + let latest_hint = self.latest_hint.as_ref().map(|hint| { + serde_json::json!({ + "ra": hint.ra, + "dec": hint.dec, + "scale": hint.scale, + "timestamp": hint.timestamp.to_rfc3339(), + }) + }); + + serde_json::json!({ + "enabled": self.options.enabled, + "solving_in_progress": self.solving_in_progress, + "stars_count": self.stars.len(), + "constellation_lines_count": self.constellation_lines.len(), + "latest_solution": latest_hint, + "update_frequency": self.options.update_frequency, + }) + } + + /// Update the options + pub fn set_options(&mut self, options: StarChartOptions) { + self.options = options; + } +} + +// Implement Drop to ensure resources are cleaned up +impl Drop for StarChart { + fn drop(&mut self) { + info!("Cleaning up star chart resources"); + + // We can't use async functions in drop, so we'll just log a warning + // In a real implementation, we might create a synchronous version of shutdown + if self.solver_tx.is_some() { + warn!("Star chart was dropped without calling shutdown() first"); + } + } +}