feat: 星图解析
This commit is contained in:
parent
07d99d009f
commit
b19122e693
236
config-example-updated.toml
Normal file
236
config-example-updated.toml
Normal file
@ -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
|
||||
98
docs/star_chart.md
Normal file
98
docs/star_chart.md
Normal file
@ -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.
|
||||
39
src/app.rs
39
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<Arc<Mutex<detection::DetectionPipeline>>>,
|
||||
hook_manager: Option<Arc<Mutex<HookManager>>>,
|
||||
watermark: Option<Arc<Mutex<watermark::Watermark>>>,
|
||||
star_chart: Option<Arc<Mutex<star_chart::StarChart>>>,
|
||||
storage_manager: Option<storage::StorageManager>,
|
||||
communication_manager: Option<Arc<Mutex<communication::CommunicationManager>>>,
|
||||
rtsp_server: Option<Arc<Mutex<RtspServer>>>,
|
||||
@ -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();
|
||||
|
||||
@ -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(),
|
||||
}
|
||||
|
||||
@ -20,3 +20,4 @@ pub mod communication;
|
||||
pub mod monitoring;
|
||||
pub mod overlay;
|
||||
pub mod hooks;
|
||||
pub mod events;
|
||||
@ -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);
|
||||
|
||||
@ -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};
|
||||
|
||||
686
src/overlay/star_chart.rs
Normal file
686
src/overlay/star_chart.rs
Normal file
@ -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<Utc>,
|
||||
}
|
||||
|
||||
/// Star information for rendering
|
||||
#[derive(Debug, Clone)]
|
||||
struct Star {
|
||||
/// Name of the star (if available)
|
||||
name: Option<String>,
|
||||
/// 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<core::Point>,
|
||||
}
|
||||
|
||||
/// 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<Mutex<GpsStatus>>,
|
||||
/// Latest solution hint
|
||||
latest_hint: Option<SolutionHint>,
|
||||
/// Stars from the latest solution
|
||||
stars: Vec<Star>,
|
||||
/// Constellation lines
|
||||
constellation_lines: Vec<ConstellationLine>,
|
||||
/// WCS solution file path
|
||||
current_wcs_path: Option<PathBuf>,
|
||||
/// Whether a solve is currently in progress
|
||||
solving_in_progress: bool,
|
||||
/// Solver thread handle
|
||||
_solver_thread: Option<tokio::task::JoinHandle<()>>,
|
||||
/// Channel for sending messages to the solver thread
|
||||
solver_tx: Option<mpsc::Sender<SolverMessage>>,
|
||||
}
|
||||
|
||||
impl StarChart {
|
||||
/// Create a new star chart overlay with the given options
|
||||
pub async fn new(
|
||||
options: StarChartOptions,
|
||||
gps_status: Arc<Mutex<GpsStatus>>,
|
||||
) -> Result<Self> {
|
||||
// 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::<SolverMessage>(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<SolverMessage>,
|
||||
options: StarChartOptions,
|
||||
) -> Result<tokio::task::JoinHandle<()>> {
|
||||
// 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<SolutionHint> = 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<PathBuf> {
|
||||
// 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::<i32>::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<SolutionHint>,
|
||||
gps_status: &Arc<Mutex<GpsStatus>>
|
||||
) -> Result<SolutionHint> {
|
||||
// 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<PathBuf> {
|
||||
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::<f64>().unwrap_or(0.0);
|
||||
}
|
||||
} else if line.starts_with("CRVAL2") {
|
||||
if let Some(value) = line.split_whitespace().nth(2) {
|
||||
dec = value.parse::<f64>().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::<f64>().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<Utc>) -> (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<Utc>) -> 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<Utc>) -> 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");
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user