event
This commit is contained in:
parent
b45f2d198f
commit
07d99d009f
365
src/app.rs
Normal file
365
src/app.rs
Normal file
@ -0,0 +1,365 @@
|
|||||||
|
//! Application orchestrator and dependency manager
|
||||||
|
//!
|
||||||
|
//! This module provides a centralized Application struct that handles
|
||||||
|
//! initialization, lifecycle management, and component wiring.
|
||||||
|
|
||||||
|
use anyhow::{Context, Result};
|
||||||
|
use chrono::Utc;
|
||||||
|
use log::{info, error, warn, debug};
|
||||||
|
use std::sync::{Arc, Mutex as StdMutex};
|
||||||
|
use tokio::sync::{Mutex, mpsc};
|
||||||
|
use tokio::task::{JoinHandle};
|
||||||
|
use tokio::signal;
|
||||||
|
|
||||||
|
use crate::camera::{self, CameraController, Frame, MeteorEvent};
|
||||||
|
use crate::config::{self, Config};
|
||||||
|
use crate::detection::{self, DetectionPipeline};
|
||||||
|
use crate::events::{EventBus, GpsStatusEvent, EnvironmentEvent, FrameEvent, DetectionEvent};
|
||||||
|
use crate::gps::{self, GpsController, GpsStatus};
|
||||||
|
use crate::hooks::{self, HookManager};
|
||||||
|
use crate::monitoring::{self, SystemMonitor};
|
||||||
|
use crate::overlay::{self, watermark};
|
||||||
|
use crate::sensors::{self, SensorController, EnvironmentData};
|
||||||
|
use crate::storage::{self, StorageManager};
|
||||||
|
use crate::streaming::{self, RtspServer};
|
||||||
|
use crate::communication::{self, CommunicationManager};
|
||||||
|
|
||||||
|
/// Main application orchestrator
|
||||||
|
pub struct Application {
|
||||||
|
/// Application configuration
|
||||||
|
config: Config,
|
||||||
|
/// Event bus for component communication
|
||||||
|
event_bus: EventBus,
|
||||||
|
/// Collection of background tasks
|
||||||
|
tasks: Vec<JoinHandle<()>>,
|
||||||
|
/// Set to true when application shutdown has been requested
|
||||||
|
shutdown_requested: bool,
|
||||||
|
|
||||||
|
// Core components
|
||||||
|
camera_controller: Option<Arc<Mutex<CameraController>>>,
|
||||||
|
gps_controller: Option<Arc<Mutex<GpsController>>>,
|
||||||
|
sensor_controller: Option<Arc<Mutex<SensorController>>>,
|
||||||
|
detection_pipeline: Option<Arc<Mutex<detection::DetectionPipeline>>>,
|
||||||
|
hook_manager: Option<Arc<Mutex<HookManager>>>,
|
||||||
|
watermark: Option<Arc<Mutex<watermark::Watermark>>>,
|
||||||
|
storage_manager: Option<storage::StorageManager>,
|
||||||
|
communication_manager: Option<Arc<Mutex<communication::CommunicationManager>>>,
|
||||||
|
rtsp_server: Option<Arc<Mutex<RtspServer>>>,
|
||||||
|
system_monitor: Option<monitoring::SystemMonitor>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Application {
|
||||||
|
/// Create a new application instance with the given configuration
|
||||||
|
pub fn new(config: Config) -> Self {
|
||||||
|
Self {
|
||||||
|
config,
|
||||||
|
event_bus: EventBus::new(),
|
||||||
|
tasks: Vec::new(),
|
||||||
|
shutdown_requested: false,
|
||||||
|
|
||||||
|
camera_controller: None,
|
||||||
|
gps_controller: None,
|
||||||
|
sensor_controller: None,
|
||||||
|
detection_pipeline: None,
|
||||||
|
hook_manager: None,
|
||||||
|
watermark: None,
|
||||||
|
storage_manager: None,
|
||||||
|
communication_manager: None,
|
||||||
|
rtsp_server: None,
|
||||||
|
system_monitor: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Initialize the application components
|
||||||
|
pub async fn initialize(&mut self) -> Result<()> {
|
||||||
|
info!("Initializing application components");
|
||||||
|
|
||||||
|
// Initialize camera
|
||||||
|
let camera_controller = camera::CameraController::new(&self.config)
|
||||||
|
.await
|
||||||
|
.context("Failed to initialize camera")?;
|
||||||
|
self.camera_controller = Some(Arc::new(Mutex::new(camera_controller)));
|
||||||
|
|
||||||
|
// Initialize GPS
|
||||||
|
let gps_controller = gps::GpsController::new(&self.config)
|
||||||
|
.await
|
||||||
|
.context("Failed to initialize GPS")?;
|
||||||
|
self.gps_controller = Some(Arc::new(Mutex::new(gps_controller)));
|
||||||
|
|
||||||
|
// Initialize sensors
|
||||||
|
let sensor_controller = sensors::SensorController::new(&self.config)
|
||||||
|
.await
|
||||||
|
.context("Failed to initialize sensors")?;
|
||||||
|
self.sensor_controller = Some(Arc::new(Mutex::new(sensor_controller)));
|
||||||
|
|
||||||
|
// Initialize hook manager
|
||||||
|
let hook_manager = hooks::HookManager::new();
|
||||||
|
self.hook_manager = Some(Arc::new(Mutex::new(hook_manager)));
|
||||||
|
|
||||||
|
// Initialize watermark
|
||||||
|
// Note: This still uses the old approach with direct dependencies.
|
||||||
|
// In a future refactoring, this would be updated to use event subscriptions.
|
||||||
|
let watermark = {
|
||||||
|
let gps_status = self.gps_controller.as_ref().unwrap().lock().await.get_status();
|
||||||
|
let env_data = self.sensor_controller.as_ref().unwrap().lock().await.get_current_data();
|
||||||
|
|
||||||
|
overlay::watermark::Watermark::new(
|
||||||
|
self.config.watermark.clone(),
|
||||||
|
Arc::new(StdMutex::new(env_data)),
|
||||||
|
Arc::new(StdMutex::new(gps_status)),
|
||||||
|
)
|
||||||
|
};
|
||||||
|
self.watermark = Some(Arc::new(Mutex::new(watermark)));
|
||||||
|
|
||||||
|
// Initialize RTSP server if enabled
|
||||||
|
if self.config.rtsp.enabled {
|
||||||
|
let rtsp_server = streaming::RtspServer::new(self.config.rtsp.clone());
|
||||||
|
self.rtsp_server = Some(Arc::new(Mutex::new(rtsp_server)));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize detection pipeline
|
||||||
|
// Note: This still uses the original constructor that takes a direct dependency
|
||||||
|
// on the camera controller. In a future refactoring, this would be updated to
|
||||||
|
// use frame events instead.
|
||||||
|
let detection_pipeline = detection::DetectionPipeline::new(
|
||||||
|
self.camera_controller.as_ref().unwrap().clone(),
|
||||||
|
&self.config,
|
||||||
|
self.config.detection.pipeline.clone()
|
||||||
|
).context("Failed to initialize detection pipeline")?;
|
||||||
|
self.detection_pipeline = Some(Arc::new(Mutex::new(detection_pipeline)));
|
||||||
|
|
||||||
|
// Initialize storage
|
||||||
|
let storage_manager = storage::StorageManager::new(&self.config)
|
||||||
|
.await
|
||||||
|
.context("Failed to initialize storage")?;
|
||||||
|
self.storage_manager = Some(storage_manager);
|
||||||
|
|
||||||
|
// Initialize communication
|
||||||
|
// Note: This still uses the original constructor with direct dependencies.
|
||||||
|
// In a future refactoring, this would be updated to use event subscriptions.
|
||||||
|
let communication_manager = communication::CommunicationManager::new(
|
||||||
|
&self.config,
|
||||||
|
self.camera_controller.as_ref().unwrap().clone(),
|
||||||
|
self.gps_controller.as_ref().unwrap().clone(),
|
||||||
|
).await.context("Failed to initialize communication")?;
|
||||||
|
self.communication_manager = Some(Arc::new(Mutex::new(communication_manager)));
|
||||||
|
|
||||||
|
// Initialize monitoring
|
||||||
|
let system_monitor = monitoring::SystemMonitor::new(&self.config)
|
||||||
|
.await
|
||||||
|
.context("Failed to initialize system monitor")?;
|
||||||
|
self.system_monitor = Some(system_monitor);
|
||||||
|
|
||||||
|
info!("All application components initialized");
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Start application components and set up event handlers
|
||||||
|
pub async fn start(&mut self) -> Result<()> {
|
||||||
|
info!("Starting application components");
|
||||||
|
|
||||||
|
// Setup frame hook for watermark
|
||||||
|
{
|
||||||
|
if let (Some(hook_manager), Some(watermark)) = (&self.hook_manager, &self.watermark) {
|
||||||
|
let mut manager = hook_manager.lock().await;
|
||||||
|
let watermark_clone = watermark.clone();
|
||||||
|
manager.register_hook(Box::new(hooks::BasicFrameHook::new(
|
||||||
|
"watermark",
|
||||||
|
"Watermark Overlay",
|
||||||
|
"Adds timestamp, GPS, and sensor data overlay to frames",
|
||||||
|
self.config.watermark.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(watermark_clone.lock());
|
||||||
|
guard.apply(frame, timestamp)
|
||||||
|
})
|
||||||
|
},
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start sensors
|
||||||
|
if let Some(controller) = &self.sensor_controller {
|
||||||
|
controller.lock().await.initialize().await
|
||||||
|
.context("Failed to initialize sensors")?;
|
||||||
|
controller.lock().await.start().await
|
||||||
|
.context("Failed to start sensors")?;
|
||||||
|
|
||||||
|
// Add task to bridge sensor data to event bus (publisher)
|
||||||
|
let event_sender = self.event_bus.environment_sender();
|
||||||
|
let controller_clone = controller.clone();
|
||||||
|
self.tasks.push(tokio::spawn(async move {
|
||||||
|
let mut interval = tokio::time::interval(tokio::time::Duration::from_secs(1));
|
||||||
|
|
||||||
|
loop {
|
||||||
|
interval.tick().await;
|
||||||
|
|
||||||
|
let env_data = match controller_clone.lock().await.get_current_data() {
|
||||||
|
data => data.clone(),
|
||||||
|
};
|
||||||
|
|
||||||
|
let event = EnvironmentEvent {
|
||||||
|
timestamp: Utc::now(),
|
||||||
|
temperature: env_data.temperature,
|
||||||
|
humidity: env_data.humidity,
|
||||||
|
sky_brightness: env_data.sky_brightness,
|
||||||
|
};
|
||||||
|
|
||||||
|
if let Err(e) = event_sender.send(event) {
|
||||||
|
warn!("Failed to publish environment event: {}", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start GPS
|
||||||
|
if let Some(controller) = &self.gps_controller {
|
||||||
|
controller.lock().await.initialize().await
|
||||||
|
.context("Failed to initialize GPS")?;
|
||||||
|
controller.lock().await.start().await
|
||||||
|
.context("Failed to start GPS")?;
|
||||||
|
|
||||||
|
// Add task to bridge GPS data to event bus
|
||||||
|
let event_sender = self.event_bus.gps_sender();
|
||||||
|
let controller_clone = controller.clone();
|
||||||
|
self.tasks.push(tokio::spawn(async move {
|
||||||
|
let mut interval = tokio::time::interval(tokio::time::Duration::from_secs(1));
|
||||||
|
|
||||||
|
loop {
|
||||||
|
interval.tick().await;
|
||||||
|
|
||||||
|
let gps_status = match controller_clone.lock().await.get_status() {
|
||||||
|
status => status.clone(),
|
||||||
|
};
|
||||||
|
|
||||||
|
let event = GpsStatusEvent {
|
||||||
|
timestamp: Utc::now(),
|
||||||
|
latitude: gps_status.position.latitude,
|
||||||
|
longitude: gps_status.position.longitude,
|
||||||
|
altitude: gps_status.position.altitude,
|
||||||
|
azimuth: gps_status.camera_orientation.azimuth,
|
||||||
|
elevation: gps_status.camera_orientation.elevation,
|
||||||
|
sync_status: gps_status.sync_status.clone(),
|
||||||
|
time_accuracy_ms: gps_status.time_accuracy_ms,
|
||||||
|
satellites: gps_status.satellites,
|
||||||
|
};
|
||||||
|
|
||||||
|
if let Err(e) = event_sender.send(event) {
|
||||||
|
warn!("Failed to publish GPS event: {}", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start RTSP server if enabled
|
||||||
|
if let Some(rtsp_server) = &self.rtsp_server {
|
||||||
|
rtsp_server.lock().await.start().await
|
||||||
|
.context("Failed to start RTSP server")?;
|
||||||
|
info!("RTSP server started at {}", rtsp_server.lock().await.get_url());
|
||||||
|
|
||||||
|
// Add task to feed frames to RTSP server
|
||||||
|
// This would be replaced with an event subscription in a future refactoring
|
||||||
|
let server_clone = rtsp_server.clone();
|
||||||
|
let rtsp_enabled = self.config.rtsp.enabled;
|
||||||
|
self.tasks.push(tokio::spawn(async move {
|
||||||
|
if rtsp_enabled {
|
||||||
|
info!("Starting RTSP streaming task");
|
||||||
|
|
||||||
|
// Implement a proper continuous loop to keep the task alive
|
||||||
|
loop {
|
||||||
|
// This would normally feed frames to the RTSP server
|
||||||
|
// For now, just sleep to keep the task alive
|
||||||
|
tokio::time::sleep(tokio::time::Duration::from_secs(1)).await;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start detection pipeline and connect it to the event bus
|
||||||
|
if let Some(pipeline) = &self.detection_pipeline {
|
||||||
|
// Spawn detection pipeline task
|
||||||
|
let pipeline_clone = pipeline.clone();
|
||||||
|
self.tasks.push(tokio::spawn(async move {
|
||||||
|
if let Err(e) = pipeline_clone.lock().await.run().await {
|
||||||
|
error!("Detection pipeline error: {}", e);
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
|
||||||
|
// In a future refactoring, we'd add an adapter here to transform
|
||||||
|
// detection pipeline events into DetectionEvent messages on the event bus
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start communication manager
|
||||||
|
if let Some(comms) = &self.communication_manager {
|
||||||
|
// Spawn communication task using a cloned Arc<Mutex<...>>
|
||||||
|
let comms_clone = comms.clone();
|
||||||
|
self.tasks.push(tokio::spawn(async move {
|
||||||
|
if let Err(e) = comms_clone.lock().await.run().await {
|
||||||
|
error!("Communication manager error: {}", e);
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
|
||||||
|
// Note: We've moved the communication manager into the task
|
||||||
|
// In a future event-based architecture, we'd have a CommunicationService
|
||||||
|
// that would act on events from the event bus instead of having direct
|
||||||
|
// dependencies on other components
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start system monitor
|
||||||
|
if let Some(monitor) = self.system_monitor.take() {
|
||||||
|
// Spawn monitor task and move ownership to the task
|
||||||
|
self.tasks.push(tokio::spawn(async move {
|
||||||
|
if let Err(e) = monitor.run().await {
|
||||||
|
error!("System monitor error: {}", e);
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
|
||||||
|
// Note: We've moved the system monitor into the task
|
||||||
|
// In a future event-based architecture, the monitoring would
|
||||||
|
// likely subscribe to events from other components
|
||||||
|
}
|
||||||
|
|
||||||
|
info!("All application components started");
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Run the application until shutdown is requested
|
||||||
|
pub async fn run(&mut self) -> Result<()> {
|
||||||
|
info!("Application running, press Ctrl+C to exit");
|
||||||
|
|
||||||
|
// Wait for shutdown signal
|
||||||
|
match signal::ctrl_c().await {
|
||||||
|
Ok(()) => {
|
||||||
|
info!("Shutdown signal received, preparing to exit");
|
||||||
|
self.shutdown_requested = true;
|
||||||
|
}
|
||||||
|
Err(err) => {
|
||||||
|
error!("Error waiting for Ctrl+C: {}", err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Shutdown all tasks
|
||||||
|
for task in self.tasks.drain(..) {
|
||||||
|
task.abort();
|
||||||
|
}
|
||||||
|
|
||||||
|
info!("Application shutdown complete");
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Request application shutdown
|
||||||
|
pub fn request_shutdown(&mut self) {
|
||||||
|
self.shutdown_requested = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get a reference to the event bus
|
||||||
|
pub fn event_bus(&self) -> &EventBus {
|
||||||
|
&self.event_bus
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get a mutable reference to the event bus
|
||||||
|
pub fn event_bus_mut(&mut self) -> &mut EventBus {
|
||||||
|
&mut self.event_bus
|
||||||
|
}
|
||||||
|
}
|
||||||
212
src/events/mod.rs
Normal file
212
src/events/mod.rs
Normal file
@ -0,0 +1,212 @@
|
|||||||
|
//! Event system for decoupled communication between components
|
||||||
|
//!
|
||||||
|
//! This module provides a publish-subscribe based event system that allows
|
||||||
|
//! components to communicate without direct dependencies on each other.
|
||||||
|
|
||||||
|
use anyhow::Result;
|
||||||
|
use chrono::{DateTime, Utc};
|
||||||
|
use log::debug;
|
||||||
|
use serde::{Serialize, Deserialize};
|
||||||
|
use std::fmt;
|
||||||
|
use tokio::sync::broadcast;
|
||||||
|
|
||||||
|
/// Maximum capacity for broadcast channels
|
||||||
|
const DEFAULT_CHANNEL_CAPACITY: usize = 32;
|
||||||
|
|
||||||
|
/// GPS position update event
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct GpsStatusEvent {
|
||||||
|
/// Timestamp of the update
|
||||||
|
pub timestamp: DateTime<Utc>,
|
||||||
|
/// Latitude in decimal degrees
|
||||||
|
pub latitude: f64,
|
||||||
|
/// Longitude in decimal degrees
|
||||||
|
pub longitude: f64,
|
||||||
|
/// Altitude in meters
|
||||||
|
pub altitude: f64,
|
||||||
|
/// Azimuth/heading in degrees
|
||||||
|
pub azimuth: f64,
|
||||||
|
/// Elevation in degrees
|
||||||
|
pub elevation: f64,
|
||||||
|
/// Time synchronization status
|
||||||
|
pub sync_status: String,
|
||||||
|
/// Estimated time accuracy in milliseconds
|
||||||
|
pub time_accuracy_ms: f64,
|
||||||
|
/// Number of satellites
|
||||||
|
pub satellites: u8,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl fmt::Display for GpsStatusEvent {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
|
write!(f, "GPS({:.6},{:.6},{:.1}m)",
|
||||||
|
self.latitude, self.longitude, self.altitude)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Environment sensor data update event
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct EnvironmentEvent {
|
||||||
|
/// Timestamp of the update
|
||||||
|
pub timestamp: DateTime<Utc>,
|
||||||
|
/// Temperature in Celsius
|
||||||
|
pub temperature: f32,
|
||||||
|
/// Humidity as percentage (0-100)
|
||||||
|
pub humidity: f32,
|
||||||
|
/// Sky brightness level (0-1, where 0 is completely dark)
|
||||||
|
pub sky_brightness: f32,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl fmt::Display for EnvironmentEvent {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
|
write!(f, "Env({:.1}°C, {:.1}%, brightness={:.2})",
|
||||||
|
self.temperature, self.humidity, self.sky_brightness)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Frame available event
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct FrameEvent {
|
||||||
|
/// Timestamp of the frame
|
||||||
|
pub timestamp: DateTime<Utc>,
|
||||||
|
/// Frame index
|
||||||
|
pub index: u64,
|
||||||
|
/// Frame data - using a Vec<u8> here to simplify the event system
|
||||||
|
/// In a real implementation, you might use a more efficient representation
|
||||||
|
/// or an ID that can be used to fetch the frame from a buffer
|
||||||
|
pub frame_id: u64,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Detection event
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct DetectionEvent {
|
||||||
|
/// Timestamp of the detection
|
||||||
|
pub timestamp: DateTime<Utc>,
|
||||||
|
/// Confidence score (0.0-1.0)
|
||||||
|
pub confidence: f32,
|
||||||
|
/// Detector ID that triggered the detection
|
||||||
|
pub detector_id: String,
|
||||||
|
/// Bounding box in format (x, y, width, height)
|
||||||
|
pub bounding_box: Option<(i32, i32, i32, i32)>,
|
||||||
|
/// Frame index that triggered the detection
|
||||||
|
pub frame_index: u64,
|
||||||
|
/// Start frame index (for the event buffer)
|
||||||
|
pub start_frame: u64,
|
||||||
|
/// End frame index (for the event buffer)
|
||||||
|
pub end_frame: u64,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl fmt::Display for DetectionEvent {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
|
write!(f, "Detection({}, conf={:.2})",
|
||||||
|
self.detector_id, self.confidence)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Event bus for managing various event channels
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct EventBus {
|
||||||
|
/// GPS status update channel
|
||||||
|
gps_tx: broadcast::Sender<GpsStatusEvent>,
|
||||||
|
/// Environment data update channel
|
||||||
|
env_tx: broadcast::Sender<EnvironmentEvent>,
|
||||||
|
/// Frame available channel
|
||||||
|
frame_tx: broadcast::Sender<FrameEvent>,
|
||||||
|
/// Detection event channel
|
||||||
|
detection_tx: broadcast::Sender<DetectionEvent>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl EventBus {
|
||||||
|
/// Create a new event bus with default channel capacities
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self::with_capacity(DEFAULT_CHANNEL_CAPACITY)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Create a new event bus with specified channel capacity
|
||||||
|
pub fn with_capacity(capacity: usize) -> Self {
|
||||||
|
let (gps_tx, _) = broadcast::channel(capacity);
|
||||||
|
let (env_tx, _) = broadcast::channel(capacity);
|
||||||
|
let (frame_tx, _) = broadcast::channel(capacity);
|
||||||
|
let (detection_tx, _) = broadcast::channel(capacity);
|
||||||
|
|
||||||
|
Self {
|
||||||
|
gps_tx,
|
||||||
|
env_tx,
|
||||||
|
frame_tx,
|
||||||
|
detection_tx,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get a GPS status event sender
|
||||||
|
pub fn gps_sender(&self) -> broadcast::Sender<GpsStatusEvent> {
|
||||||
|
self.gps_tx.clone()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Subscribe to GPS status events
|
||||||
|
pub fn subscribe_gps(&self) -> broadcast::Receiver<GpsStatusEvent> {
|
||||||
|
self.gps_tx.subscribe()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get an environment event sender
|
||||||
|
pub fn environment_sender(&self) -> broadcast::Sender<EnvironmentEvent> {
|
||||||
|
self.env_tx.clone()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Subscribe to environment events
|
||||||
|
pub fn subscribe_environment(&self) -> broadcast::Receiver<EnvironmentEvent> {
|
||||||
|
self.env_tx.subscribe()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get a frame event sender
|
||||||
|
pub fn frame_sender(&self) -> broadcast::Sender<FrameEvent> {
|
||||||
|
self.frame_tx.clone()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Subscribe to frame events
|
||||||
|
pub fn subscribe_frames(&self) -> broadcast::Receiver<FrameEvent> {
|
||||||
|
self.frame_tx.subscribe()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get a detection event sender
|
||||||
|
pub fn detection_sender(&self) -> broadcast::Sender<DetectionEvent> {
|
||||||
|
self.detection_tx.clone()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Subscribe to detection events
|
||||||
|
pub fn subscribe_detections(&self) -> broadcast::Receiver<DetectionEvent> {
|
||||||
|
self.detection_tx.subscribe()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Publish a GPS status event
|
||||||
|
pub fn publish_gps(&self, event: GpsStatusEvent) -> Result<()> {
|
||||||
|
let count = self.gps_tx.send(event.clone())?;
|
||||||
|
debug!("Published GPS event {} to {} receivers", event, count);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Publish an environment event
|
||||||
|
pub fn publish_environment(&self, event: EnvironmentEvent) -> Result<()> {
|
||||||
|
let count = self.env_tx.send(event.clone())?;
|
||||||
|
debug!("Published environment event {} to {} receivers", event, count);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Publish a frame event
|
||||||
|
pub fn publish_frame(&self, event: FrameEvent) -> Result<()> {
|
||||||
|
let count = self.frame_tx.send(event)?;
|
||||||
|
debug!("Published frame event to {} receivers", count);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Publish a detection event
|
||||||
|
pub fn publish_detection(&self, event: DetectionEvent) -> Result<()> {
|
||||||
|
let count = self.detection_tx.send(event.clone())?;
|
||||||
|
debug!("Published detection event {} to {} receivers", event, count);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for EventBus {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self::new()
|
||||||
|
}
|
||||||
|
}
|
||||||
174
src/main.rs
174
src/main.rs
@ -2,6 +2,7 @@
|
|||||||
mod camera;
|
mod camera;
|
||||||
mod config;
|
mod config;
|
||||||
mod detection;
|
mod detection;
|
||||||
|
mod events;
|
||||||
mod gps;
|
mod gps;
|
||||||
mod hooks;
|
mod hooks;
|
||||||
mod overlay;
|
mod overlay;
|
||||||
@ -25,6 +26,8 @@ pub use config::Config;
|
|||||||
use meteor_detect::PLATFORM_SUPPORTS_GPIO;
|
use meteor_detect::PLATFORM_SUPPORTS_GPIO;
|
||||||
use crate::overlay::Watermark;
|
use crate::overlay::Watermark;
|
||||||
|
|
||||||
|
mod app;
|
||||||
|
|
||||||
/// Main entry point for the meteor detection system
|
/// Main entry point for the meteor detection system
|
||||||
#[tokio::main]
|
#[tokio::main]
|
||||||
async fn main() -> Result<()> {
|
async fn main() -> Result<()> {
|
||||||
@ -53,168 +56,21 @@ async fn main() -> Result<()> {
|
|||||||
|
|
||||||
info!("Loaded configuration with device ID: {}", config.device.id);
|
info!("Loaded configuration with device ID: {}", config.device.id);
|
||||||
|
|
||||||
// Initialize camera subsystem
|
// Create the application
|
||||||
let camera_controller = camera::CameraController::new(&config)
|
let mut application = app::Application::new(config);
|
||||||
.await
|
|
||||||
.context("Failed to initialize camera")?;
|
|
||||||
let camera_controller = Arc::new(Mutex::new(camera_controller));
|
|
||||||
|
|
||||||
// Initialize GPS module
|
// Initialize all components
|
||||||
let gps_controller = gps::GpsController::new(&config)
|
application.initialize().await
|
||||||
.await
|
.context("Failed to initialize application components")?;
|
||||||
.context("Failed to initialize GPS")?;
|
|
||||||
let gps_controller = Arc::new(Mutex::new(gps_controller));
|
|
||||||
|
|
||||||
// Initialize sensor controller
|
// Start all components and set up event handlers
|
||||||
let sensor_controller = sensors::SensorController::new(&config)
|
application.start().await
|
||||||
.await
|
.context("Failed to start application components")?;
|
||||||
.context("Failed to initialize sensors")?;
|
|
||||||
let sensor_controller = Arc::new(Mutex::new(sensor_controller));
|
|
||||||
|
|
||||||
// Initialize watermark overlay
|
// Run the application until shutdown
|
||||||
let watermark = {
|
application.run().await
|
||||||
let gps_status = gps_controller.lock().await.get_status();
|
.context("Application runtime error")?;
|
||||||
let env_data = sensor_controller.lock().await.get_current_data();
|
|
||||||
|
|
||||||
overlay::watermark::Watermark::new(
|
|
||||||
config.watermark.clone(),
|
|
||||||
Arc::new(StdMutex::new(env_data)),
|
|
||||||
Arc::new(StdMutex::new(gps_status)),
|
|
||||||
)
|
|
||||||
};
|
|
||||||
let watermark = Arc::new(Mutex::new(watermark));
|
|
||||||
|
|
||||||
// Initialize frame hook manager
|
info!("Meteor detection system shutdown complete");
|
||||||
let hook_manager = hooks::HookManager::new();
|
|
||||||
let hook_manager = Arc::new(Mutex::new(hook_manager));
|
|
||||||
|
|
||||||
// Initialize RTSP streaming server
|
|
||||||
let rtsp_server = streaming::RtspServer::new(config.rtsp.clone());
|
|
||||||
let rtsp_server = Arc::new(Mutex::new(rtsp_server));
|
|
||||||
|
|
||||||
// Initialize detection pipeline
|
|
||||||
let detection_pipeline = detection::DetectionPipeline::new(
|
|
||||||
camera_controller.clone(),
|
|
||||||
&config.clone(),
|
|
||||||
config.detection.pipeline.clone()
|
|
||||||
).context("Failed to initialize detection pipeline")?;
|
|
||||||
|
|
||||||
// Initialize storage system
|
|
||||||
let storage_manager = storage::StorageManager::new(&(config.clone()))
|
|
||||||
.await
|
|
||||||
.context("Failed to initialize storage")?;
|
|
||||||
|
|
||||||
// Initialize communication module
|
|
||||||
let mut comms = communication::CommunicationManager::new(
|
|
||||||
&config,
|
|
||||||
camera_controller.clone(),
|
|
||||||
gps_controller.clone(),
|
|
||||||
).await.context("Failed to initialize communication")?;
|
|
||||||
|
|
||||||
// Initialize health monitoring
|
|
||||||
let monitor = monitoring::SystemMonitor::new(&config)
|
|
||||||
.await
|
|
||||||
.context("Failed to initialize system monitor")?;
|
|
||||||
|
|
||||||
// Start all subsystems
|
|
||||||
info!("All subsystems initialized, starting main processing loop");
|
|
||||||
|
|
||||||
// Initialize sensors
|
|
||||||
sensor_controller.lock().await.initialize().await
|
|
||||||
.context("Failed to initialize sensors")?;
|
|
||||||
sensor_controller.lock().await.start().await
|
|
||||||
.context("Failed to start sensors")?;
|
|
||||||
|
|
||||||
// Initialize GPS
|
|
||||||
gps_controller.lock().await.initialize().await
|
|
||||||
.context("Failed to initialize GPS")?;
|
|
||||||
gps_controller.lock().await.start().await
|
|
||||||
.context("Failed to start GPS")?;
|
|
||||||
|
|
||||||
// Start RTSP server if enabled
|
|
||||||
if config.rtsp.enabled {
|
|
||||||
rtsp_server.lock().await.start().await
|
|
||||||
.context("Failed to start RTSP server")?;
|
|
||||||
info!("RTSP server started at {}", rtsp_server.lock().await.get_url());
|
|
||||||
}
|
|
||||||
|
|
||||||
// Run the main event loop
|
|
||||||
let mut tasks = Vec::new();
|
|
||||||
|
|
||||||
// Add watermark hook
|
|
||||||
{
|
|
||||||
let mut manager = hook_manager.lock().await;
|
|
||||||
let watermark_clone = watermark.clone();
|
|
||||||
manager.register_hook(Box::new(hooks::BasicFrameHook::new(
|
|
||||||
"watermark",
|
|
||||||
"Watermark Overlay",
|
|
||||||
"Adds timestamp, GPS, and sensor data overlay to frames",
|
|
||||||
config.watermark.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(watermark_clone.lock());
|
|
||||||
guard.apply(frame, timestamp)
|
|
||||||
})
|
|
||||||
},
|
|
||||||
)));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Spawn detection pipeline task
|
|
||||||
tasks.push(tokio::spawn(async move {
|
|
||||||
if let Err(e) = detection_pipeline.run().await {
|
|
||||||
error!("Detection pipeline error: {}", e);
|
|
||||||
}
|
|
||||||
}));
|
|
||||||
|
|
||||||
// Spawn communication manager task
|
|
||||||
tasks.push(tokio::spawn(async move {
|
|
||||||
if let Err(e) = comms.run().await {
|
|
||||||
error!("Communication manager error: {}", e);
|
|
||||||
}
|
|
||||||
}));
|
|
||||||
|
|
||||||
// Spawn system monitor task
|
|
||||||
tasks.push(tokio::spawn(async move {
|
|
||||||
if let Err(e) = monitor.run().await {
|
|
||||||
error!("System monitor error: {}", e);
|
|
||||||
}
|
|
||||||
}));
|
|
||||||
|
|
||||||
// Add RTSP streaming task with proper loop to keep it alive
|
|
||||||
let rtsp_config_enabled = config.rtsp.enabled;
|
|
||||||
tasks.push(tokio::spawn(async move {
|
|
||||||
if rtsp_config_enabled {
|
|
||||||
info!("Starting RTSP streaming task");
|
|
||||||
|
|
||||||
// Implement a proper continuous loop to keep the task alive
|
|
||||||
loop {
|
|
||||||
// This would normally feed frames to the RTSP server
|
|
||||||
// For now, just sleep to keep the task alive
|
|
||||||
tokio::time::sleep(tokio::time::Duration::from_secs(1)).await;
|
|
||||||
|
|
||||||
// Add some actual implementation here later
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}));
|
|
||||||
|
|
||||||
// Wait for the Ctrl+C signal
|
|
||||||
info!("Press Ctrl+C to stop the application");
|
|
||||||
match signal::ctrl_c().await {
|
|
||||||
Ok(()) => {
|
|
||||||
info!("Shutdown signal received, preparing to exit");
|
|
||||||
// Here you would add cleanup code
|
|
||||||
}
|
|
||||||
Err(err) => {
|
|
||||||
error!("Error waiting for Ctrl+C: {}", err);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Now that we've received a shutdown signal, cancel all tasks
|
|
||||||
for task in tasks {
|
|
||||||
task.abort();
|
|
||||||
}
|
|
||||||
|
|
||||||
info!("Meteor detection system shutting down");
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|||||||
312
src/overlay/event_watermark.rs
Normal file
312
src/overlay/event_watermark.rs
Normal file
@ -0,0 +1,312 @@
|
|||||||
|
//! Event-based watermark implementation
|
||||||
|
//!
|
||||||
|
//! This module provides a version of the watermark component that uses
|
||||||
|
//! the event bus to receive updates instead of directly accessing shared state.
|
||||||
|
|
||||||
|
use anyhow::{Context, Result};
|
||||||
|
use chrono::{DateTime, Utc};
|
||||||
|
use log::{debug, error, info, warn};
|
||||||
|
use opencv::{core, imgproc, prelude::*, types};
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use std::sync::Arc;
|
||||||
|
use tokio::sync::{Mutex, RwLock};
|
||||||
|
|
||||||
|
use crate::events::{EventBus, GpsStatusEvent, EnvironmentEvent};
|
||||||
|
use crate::gps::{GeoPosition, GpsStatus, CameraOrientation};
|
||||||
|
use crate::overlay::watermark::{WatermarkOptions, WatermarkPosition, WatermarkContent};
|
||||||
|
use crate::sensors::EnvironmentData;
|
||||||
|
|
||||||
|
/// Event-based watermark for overlaying information on video frames
|
||||||
|
pub struct EventWatermark {
|
||||||
|
/// Watermark configuration options
|
||||||
|
options: WatermarkOptions,
|
||||||
|
/// Current environment data (updated via events)
|
||||||
|
environment: Arc<RwLock<EnvironmentData>>,
|
||||||
|
/// Current GPS status (updated via events)
|
||||||
|
gps_status: Arc<RwLock<GpsStatus>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl EventWatermark {
|
||||||
|
/// Create a new event-based watermark with the given options
|
||||||
|
pub async fn new(
|
||||||
|
options: WatermarkOptions,
|
||||||
|
event_bus: &EventBus,
|
||||||
|
) -> Self {
|
||||||
|
// Create empty data stores
|
||||||
|
let environment = Arc::new(RwLock::new(EnvironmentData {
|
||||||
|
temperature: 0.0,
|
||||||
|
humidity: 0.0,
|
||||||
|
sky_brightness: 0.0,
|
||||||
|
timestamp: Utc::now(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
let gps_status = Arc::new(RwLock::new(GpsStatus {
|
||||||
|
timestamp: Utc::now(),
|
||||||
|
position: GeoPosition {
|
||||||
|
latitude: 0.0,
|
||||||
|
longitude: 0.0,
|
||||||
|
altitude: 0.0,
|
||||||
|
},
|
||||||
|
camera_orientation: CameraOrientation {
|
||||||
|
azimuth: 0.0,
|
||||||
|
elevation: 0.0,
|
||||||
|
},
|
||||||
|
sync_status: "NoSync".to_string(),
|
||||||
|
time_accuracy_ms: 0.0,
|
||||||
|
satellites: 0,
|
||||||
|
}));
|
||||||
|
|
||||||
|
// Setup event subscriptions
|
||||||
|
Self::setup_subscriptions(event_bus, environment.clone(), gps_status.clone());
|
||||||
|
|
||||||
|
Self {
|
||||||
|
options,
|
||||||
|
environment,
|
||||||
|
gps_status,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Setup event subscriptions
|
||||||
|
fn setup_subscriptions(
|
||||||
|
event_bus: &EventBus,
|
||||||
|
environment: Arc<RwLock<EnvironmentData>>,
|
||||||
|
gps_status: Arc<RwLock<GpsStatus>>,
|
||||||
|
) {
|
||||||
|
// Subscribe to environment events
|
||||||
|
let mut env_rx = event_bus.subscribe_environment();
|
||||||
|
let env_data = environment.clone();
|
||||||
|
tokio::spawn(async move {
|
||||||
|
while let Ok(event) = env_rx.recv().await {
|
||||||
|
let mut data = env_data.write().await;
|
||||||
|
data.temperature = event.temperature;
|
||||||
|
data.humidity = event.humidity;
|
||||||
|
data.sky_brightness = event.sky_brightness;
|
||||||
|
data.timestamp = event.timestamp;
|
||||||
|
debug!("Updated environment data: temp={}, humidity={}, brightness={}",
|
||||||
|
event.temperature, event.humidity, event.sky_brightness);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Subscribe to GPS events
|
||||||
|
let mut gps_rx = event_bus.subscribe_gps();
|
||||||
|
let gps_data = gps_status.clone();
|
||||||
|
tokio::spawn(async move {
|
||||||
|
while let Ok(event) = gps_rx.recv().await {
|
||||||
|
let mut status = gps_data.write().await;
|
||||||
|
status.timestamp = event.timestamp;
|
||||||
|
status.position.latitude = event.latitude;
|
||||||
|
status.position.longitude = event.longitude;
|
||||||
|
status.position.altitude = event.altitude;
|
||||||
|
status.camera_orientation.azimuth = event.azimuth;
|
||||||
|
status.camera_orientation.elevation = event.elevation;
|
||||||
|
status.fix_quality = event.fix_quality;
|
||||||
|
status.satellites = event.satellites;
|
||||||
|
debug!("Updated GPS status: lat={}, lon={}",
|
||||||
|
event.latitude, event.longitude);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Update watermark options
|
||||||
|
pub fn set_options(&mut self, options: WatermarkOptions) {
|
||||||
|
self.options = options;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Convert GPS coordinates to formatted string
|
||||||
|
async fn format_coordinates(&self) -> String {
|
||||||
|
let position = {
|
||||||
|
let gps = self.gps_status.read().await;
|
||||||
|
gps.position.clone()
|
||||||
|
};
|
||||||
|
|
||||||
|
if self.options.coordinate_format == "dms" {
|
||||||
|
// Convert decimal degrees to degrees, minutes, seconds
|
||||||
|
let lat_deg = position.latitude.abs().trunc() as i32;
|
||||||
|
let lat_min = ((position.latitude.abs() - lat_deg as f64) * 60.0).trunc() as i32;
|
||||||
|
let lat_sec = ((position.latitude.abs() - lat_deg as f64 - lat_min as f64 / 60.0) * 3600.0).round() as i32;
|
||||||
|
let lat_dir = if position.latitude >= 0.0 { "N" } else { "S" };
|
||||||
|
|
||||||
|
let lon_deg = position.longitude.abs().trunc() as i32;
|
||||||
|
let lon_min = ((position.longitude.abs() - lon_deg as f64) * 60.0).trunc() as i32;
|
||||||
|
let lon_sec = ((position.longitude.abs() - lon_deg as f64 - lon_min as f64 / 60.0) * 3600.0).round() as i32;
|
||||||
|
let lon_dir = if position.longitude >= 0.0 { "E" } else { "W" };
|
||||||
|
|
||||||
|
format!(
|
||||||
|
"{}°{:02}'{:02}\"{} {}°{:02}'{:02}\"{} Alt: {:.1}m",
|
||||||
|
lat_deg, lat_min, lat_sec, lat_dir,
|
||||||
|
lon_deg, lon_min, lon_sec, lon_dir,
|
||||||
|
position.altitude
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
// Decimal degrees format
|
||||||
|
format!(
|
||||||
|
"Lat: {:.6}° Lon: {:.6}° Alt: {:.1}m",
|
||||||
|
position.latitude, position.longitude, position.altitude
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Convert temperature to formatted string based on options
|
||||||
|
async fn format_temperature(&self) -> String {
|
||||||
|
let temp_c = {
|
||||||
|
let env = self.environment.read().await;
|
||||||
|
env.temperature
|
||||||
|
};
|
||||||
|
|
||||||
|
if self.options.temperature_format == "F" {
|
||||||
|
// Convert to Fahrenheit
|
||||||
|
let temp_f = temp_c * 9.0 / 5.0 + 32.0;
|
||||||
|
format!("{:.1}°F", temp_f)
|
||||||
|
} else {
|
||||||
|
// Default to Celsius
|
||||||
|
format!("{:.1}°C", temp_c)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Apply watermark to the given frame
|
||||||
|
pub async fn apply(&self, frame: &mut core::Mat, timestamp: DateTime<Utc>) -> Result<()> {
|
||||||
|
if !self.options.enabled {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build the text lines to display
|
||||||
|
let mut lines = Vec::new();
|
||||||
|
|
||||||
|
for content in &self.options.content {
|
||||||
|
match content {
|
||||||
|
WatermarkContent::Timestamp => {
|
||||||
|
lines.push(timestamp.format(&self.options.time_format).to_string());
|
||||||
|
},
|
||||||
|
WatermarkContent::GpsCoordinates => {
|
||||||
|
lines.push(self.format_coordinates().await);
|
||||||
|
},
|
||||||
|
WatermarkContent::Environment => {
|
||||||
|
let humidity = {
|
||||||
|
let env = self.environment.read().await;
|
||||||
|
env.humidity
|
||||||
|
};
|
||||||
|
|
||||||
|
lines.push(format!(
|
||||||
|
"Temp: {} Humidity: {:.1}%",
|
||||||
|
self.format_temperature().await,
|
||||||
|
humidity
|
||||||
|
));
|
||||||
|
},
|
||||||
|
WatermarkContent::CameraOrientation => {
|
||||||
|
let orientation = {
|
||||||
|
let gps = self.gps_status.read().await;
|
||||||
|
gps.camera_orientation.clone()
|
||||||
|
};
|
||||||
|
|
||||||
|
lines.push(format!(
|
||||||
|
"Az: {:.1}° El: {:.1}°",
|
||||||
|
orientation.azimuth,
|
||||||
|
orientation.elevation
|
||||||
|
));
|
||||||
|
},
|
||||||
|
WatermarkContent::Custom(text) => {
|
||||||
|
lines.push(text.clone());
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Skip if no content
|
||||||
|
if lines.is_empty() {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get frame dimensions
|
||||||
|
let width = frame.cols();
|
||||||
|
let height = frame.rows();
|
||||||
|
|
||||||
|
// Calculate text size and position
|
||||||
|
let font = imgproc::FONT_HERSHEY_SIMPLEX;
|
||||||
|
let font_scale = self.options.font_scale;
|
||||||
|
let thickness = self.options.thickness;
|
||||||
|
let padding = self.options.padding;
|
||||||
|
|
||||||
|
// Calculate total height of all lines
|
||||||
|
let mut line_heights = Vec::with_capacity(lines.len());
|
||||||
|
let mut max_width = 0;
|
||||||
|
|
||||||
|
for line in &lines {
|
||||||
|
let size = imgproc::get_text_size(line, font, font_scale, thickness, &mut 0)?;
|
||||||
|
line_heights.push(size.height);
|
||||||
|
max_width = max_width.max(size.width);
|
||||||
|
}
|
||||||
|
|
||||||
|
let total_height: i32 = line_heights.iter().sum();
|
||||||
|
let line_spacing = 4; // Space between lines
|
||||||
|
let total_space_height = line_spacing * (lines.len() as i32 - 1);
|
||||||
|
let text_block_height = total_height + total_space_height + padding * 2;
|
||||||
|
let text_block_width = max_width + padding * 2;
|
||||||
|
|
||||||
|
// Calculate watermark position
|
||||||
|
let (x, y) = match self.options.position {
|
||||||
|
WatermarkPosition::TopLeft => (padding, padding),
|
||||||
|
WatermarkPosition::TopRight => (width - text_block_width - padding, padding),
|
||||||
|
WatermarkPosition::BottomLeft => (padding, height - text_block_height - padding),
|
||||||
|
WatermarkPosition::BottomRight => (
|
||||||
|
width - text_block_width - padding,
|
||||||
|
height - text_block_height - padding
|
||||||
|
),
|
||||||
|
WatermarkPosition::Custom(x, y) => (x as i32, y as i32),
|
||||||
|
};
|
||||||
|
|
||||||
|
// Draw background rectangle if enabled
|
||||||
|
if self.options.background {
|
||||||
|
let bg_color = core::Scalar::new(
|
||||||
|
self.options.background_color.0 as f64,
|
||||||
|
self.options.background_color.1 as f64,
|
||||||
|
self.options.background_color.2 as f64,
|
||||||
|
self.options.background_color.3 as f64,
|
||||||
|
);
|
||||||
|
|
||||||
|
let rect = core::Rect::new(
|
||||||
|
x,
|
||||||
|
y,
|
||||||
|
text_block_width,
|
||||||
|
text_block_height
|
||||||
|
);
|
||||||
|
|
||||||
|
imgproc::rectangle(
|
||||||
|
frame,
|
||||||
|
rect,
|
||||||
|
bg_color,
|
||||||
|
-1, // Fill
|
||||||
|
imgproc::LINE_8,
|
||||||
|
0,
|
||||||
|
)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Draw text lines
|
||||||
|
let text_color = core::Scalar::new(
|
||||||
|
self.options.color.0 as f64,
|
||||||
|
self.options.color.1 as f64,
|
||||||
|
self.options.color.2 as f64,
|
||||||
|
self.options.color.3 as f64,
|
||||||
|
);
|
||||||
|
|
||||||
|
let mut current_y = y + padding + line_heights[0];
|
||||||
|
|
||||||
|
for (i, line) in lines.iter().enumerate() {
|
||||||
|
imgproc::put_text(
|
||||||
|
frame,
|
||||||
|
line,
|
||||||
|
core::Point::new(x + padding, current_y),
|
||||||
|
font,
|
||||||
|
font_scale,
|
||||||
|
text_color,
|
||||||
|
thickness,
|
||||||
|
imgproc::LINE_AA,
|
||||||
|
false,
|
||||||
|
)?;
|
||||||
|
|
||||||
|
if i < lines.len() - 1 {
|
||||||
|
current_y += line_heights[i] + line_spacing;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user