Compare commits
No commits in common. "ea30baae3ca75964d97a8dfc0319ca633f796fed" and "07d99d009ff459f83b5ba395fe4dc76a359b410a" have entirely different histories.
ea30baae3c
...
07d99d009f
2
.gitignore
vendored
2
.gitignore
vendored
@ -1,5 +1,5 @@
|
|||||||
# Rust build artifacts
|
# Rust build artifacts
|
||||||
**/target/
|
/target/
|
||||||
**/*.rs.bk
|
**/*.rs.bk
|
||||||
Cargo.lock
|
Cargo.lock
|
||||||
|
|
||||||
|
|||||||
@ -1,236 +0,0 @@
|
|||||||
# 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
|
|
||||||
@ -1,98 +0,0 @@
|
|||||||
# 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::gps::{self, GpsController, GpsStatus};
|
||||||
use crate::hooks::{self, HookManager};
|
use crate::hooks::{self, HookManager};
|
||||||
use crate::monitoring::{self, SystemMonitor};
|
use crate::monitoring::{self, SystemMonitor};
|
||||||
use crate::overlay::{self, watermark, star_chart};
|
use crate::overlay::{self, watermark};
|
||||||
use crate::sensors::{self, SensorController, EnvironmentData};
|
use crate::sensors::{self, SensorController, EnvironmentData};
|
||||||
use crate::storage::{self, StorageManager};
|
use crate::storage::{self, StorageManager};
|
||||||
use crate::streaming::{self, RtspServer};
|
use crate::streaming::{self, RtspServer};
|
||||||
@ -42,7 +42,6 @@ pub struct Application {
|
|||||||
detection_pipeline: Option<Arc<Mutex<detection::DetectionPipeline>>>,
|
detection_pipeline: Option<Arc<Mutex<detection::DetectionPipeline>>>,
|
||||||
hook_manager: Option<Arc<Mutex<HookManager>>>,
|
hook_manager: Option<Arc<Mutex<HookManager>>>,
|
||||||
watermark: Option<Arc<Mutex<watermark::Watermark>>>,
|
watermark: Option<Arc<Mutex<watermark::Watermark>>>,
|
||||||
star_chart: Option<Arc<Mutex<star_chart::StarChart>>>,
|
|
||||||
storage_manager: Option<storage::StorageManager>,
|
storage_manager: Option<storage::StorageManager>,
|
||||||
communication_manager: Option<Arc<Mutex<communication::CommunicationManager>>>,
|
communication_manager: Option<Arc<Mutex<communication::CommunicationManager>>>,
|
||||||
rtsp_server: Option<Arc<Mutex<RtspServer>>>,
|
rtsp_server: Option<Arc<Mutex<RtspServer>>>,
|
||||||
@ -64,7 +63,6 @@ impl Application {
|
|||||||
detection_pipeline: None,
|
detection_pipeline: None,
|
||||||
hook_manager: None,
|
hook_manager: None,
|
||||||
watermark: None,
|
watermark: None,
|
||||||
star_chart: None,
|
|
||||||
storage_manager: None,
|
storage_manager: None,
|
||||||
communication_manager: None,
|
communication_manager: None,
|
||||||
rtsp_server: None,
|
rtsp_server: None,
|
||||||
@ -113,13 +111,6 @@ impl Application {
|
|||||||
};
|
};
|
||||||
self.watermark = Some(Arc::new(Mutex::new(watermark)));
|
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
|
// Initialize RTSP server if enabled
|
||||||
if self.config.rtsp.enabled {
|
if self.config.rtsp.enabled {
|
||||||
let rtsp_server = streaming::RtspServer::new(self.config.rtsp.clone());
|
let rtsp_server = streaming::RtspServer::new(self.config.rtsp.clone());
|
||||||
@ -188,27 +179,6 @@ 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
|
// Start sensors
|
||||||
if let Some(controller) = &self.sensor_controller {
|
if let Some(controller) = &self.sensor_controller {
|
||||||
controller.lock().await.initialize().await
|
controller.lock().await.initialize().await
|
||||||
@ -369,13 +339,6 @@ 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
|
// Shutdown all tasks
|
||||||
for task in self.tasks.drain(..) {
|
for task in self.tasks.drain(..) {
|
||||||
task.abort();
|
task.abort();
|
||||||
|
|||||||
@ -9,7 +9,7 @@ use toml;
|
|||||||
use crate::camera::{CameraSettings, Resolution, ExposureMode};
|
use crate::camera::{CameraSettings, Resolution, ExposureMode};
|
||||||
use crate::detection::{DetectorConfig, PipelineConfig};
|
use crate::detection::{DetectorConfig, PipelineConfig};
|
||||||
use crate::gps::{GpsConfig, CameraOrientation};
|
use crate::gps::{GpsConfig, CameraOrientation};
|
||||||
use crate::overlay::{WatermarkOptions, StarChartOptions};
|
use crate::overlay::WatermarkOptions;
|
||||||
use crate::sensors::SensorConfig;
|
use crate::sensors::SensorConfig;
|
||||||
use crate::streaming::RtspConfig;
|
use crate::streaming::RtspConfig;
|
||||||
|
|
||||||
@ -272,10 +272,6 @@ pub struct Config {
|
|||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub watermark: WatermarkOptions,
|
pub watermark: WatermarkOptions,
|
||||||
|
|
||||||
/// Star chart configuration
|
|
||||||
#[serde(default)]
|
|
||||||
pub star_chart: StarChartOptions,
|
|
||||||
|
|
||||||
/// RTSP streaming configuration
|
/// RTSP streaming configuration
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub rtsp: RtspConfig,
|
pub rtsp: RtspConfig,
|
||||||
@ -315,7 +311,6 @@ impl Default for Config {
|
|||||||
http_api: HttpApiConfig::default(),
|
http_api: HttpApiConfig::default(),
|
||||||
communication: None,
|
communication: None,
|
||||||
watermark: WatermarkOptions::default(),
|
watermark: WatermarkOptions::default(),
|
||||||
star_chart: StarChartOptions::default(),
|
|
||||||
rtsp: RtspConfig::default(),
|
rtsp: RtspConfig::default(),
|
||||||
log_level: "info".to_string(),
|
log_level: "info".to_string(),
|
||||||
}
|
}
|
||||||
|
|||||||
@ -20,4 +20,3 @@ pub mod communication;
|
|||||||
pub mod monitoring;
|
pub mod monitoring;
|
||||||
pub mod overlay;
|
pub mod overlay;
|
||||||
pub mod hooks;
|
pub mod hooks;
|
||||||
pub mod events;
|
|
||||||
@ -11,7 +11,7 @@ use serde::{Deserialize, Serialize};
|
|||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use tokio::sync::{Mutex, RwLock};
|
use tokio::sync::{Mutex, RwLock};
|
||||||
|
|
||||||
use crate::events::EventBus;
|
use crate::events::{EventBus, GpsStatusEvent, EnvironmentEvent};
|
||||||
use crate::gps::{GeoPosition, GpsStatus, CameraOrientation};
|
use crate::gps::{GeoPosition, GpsStatus, CameraOrientation};
|
||||||
use crate::overlay::watermark::{WatermarkOptions, WatermarkPosition, WatermarkContent};
|
use crate::overlay::watermark::{WatermarkOptions, WatermarkPosition, WatermarkContent};
|
||||||
use crate::sensors::EnvironmentData;
|
use crate::sensors::EnvironmentData;
|
||||||
@ -99,7 +99,7 @@ impl EventWatermark {
|
|||||||
status.position.altitude = event.altitude;
|
status.position.altitude = event.altitude;
|
||||||
status.camera_orientation.azimuth = event.azimuth;
|
status.camera_orientation.azimuth = event.azimuth;
|
||||||
status.camera_orientation.elevation = event.elevation;
|
status.camera_orientation.elevation = event.elevation;
|
||||||
status.sync_status = event.sync_status.clone();
|
status.fix_quality = event.fix_quality;
|
||||||
status.satellites = event.satellites;
|
status.satellites = event.satellites;
|
||||||
debug!("Updated GPS status: lat={}, lon={}",
|
debug!("Updated GPS status: lat={}, lon={}",
|
||||||
event.latitude, event.longitude);
|
event.latitude, event.longitude);
|
||||||
|
|||||||
@ -1,7 +1,3 @@
|
|||||||
pub(crate) mod watermark;
|
pub(crate) mod watermark;
|
||||||
pub(crate) mod event_watermark;
|
|
||||||
pub(crate) mod star_chart;
|
|
||||||
|
|
||||||
pub use watermark::{WatermarkOptions, WatermarkPosition, Watermark, WatermarkContent};
|
pub use watermark::{WatermarkOptions, WatermarkPosition, Watermark, WatermarkContent};
|
||||||
pub use event_watermark::EventWatermark;
|
|
||||||
pub use star_chart::{StarChart, StarChartOptions};
|
|
||||||
|
|||||||
@ -1,686 +0,0 @@
|
|||||||
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