#[path = "daemon/arbitrator.rs"]
pub mod arbitrator;
#[path = "daemon/attitude.rs"]
pub mod attitude;
#[path = "daemon/calibration.rs"]
pub mod calibration;
#[path = "daemon/config.rs"]
pub mod config;
#[path = "daemon/gimbal_handle.rs"]
pub mod gimbal_handle;
#[path = "daemon/ipc_server.rs"]
pub mod ipc_server;
#[path = "daemon/mavlink_manager.rs"]
pub mod mavlink_manager;
#[path = "daemon/models.rs"]
pub mod models;
#[path = "daemon/state.rs"]
pub mod state;
#[path = "daemon/yaw_corrector.rs"]
pub mod yaw_corrector;
use crate::device_scanner::DeviceScanner;
use crate::gimbal;
use calibration::Calibration;
use config::DaemonConfig;
use gimbal_handle::GimbalHandle;
use ipc_server::IpcServer;
use mavlink_manager::MavlinkManager;
use state::StateManager;
use std::sync::Arc;
use std::time::Duration;
use tokio::sync::Notify;
use tokio::task::JoinHandle;
use tracing::{debug, error, info, warn};
const SHUTDOWN_GRACE: Duration = Duration::from_secs(2);
pub async fn run_daemon(config_path: Option<String>) -> crate::error::Result<()> {
let config = if let Some(path) = config_path {
DaemonConfig::load_or_default(path)
} else {
DaemonConfig::load_or_default("turret.yaml")
};
info!("Daemon configuration loaded");
info!(" Device: {}", config.device.path);
info!(" IPC socket: {}", config.ipc.socket_path);
info!(" MAVLink enabled: {}", config.mavlink.enabled);
let device_path = if config.device.path == "auto" {
let scanner = DeviceScanner::new();
match scanner.find_storm32_device() {
Some(path) => {
info!("Auto-detected device: {}", path);
path
}
None => {
error!("No STorM32 device found. Please specify device path in config.");
return Err(crate::error::Error::NotDetected {
device: "auto".to_string(),
tried: "STorM32 USB CDC".to_string(),
});
}
}
} else {
config.device.path.clone()
};
info!("Initializing gimbal device: {}", device_path);
let mut gimbal = gimbal::detect_gimbal(&device_path)?;
info!("Detected protocol: {}", gimbal.protocol_name());
let state_manager = StateManager::with_min_command_interval(Duration::from_millis(u64::from(
config.arbitrator.min_command_interval_ms,
)));
if let Ok(version) = gimbal.get_version() {
info!("Firmware version: {}", version);
state_manager.update_firmware_version(version);
}
let cal = Calibration::load();
let gimbal_shared = GimbalHandle::with_yaw_offset_deg(gimbal, cal.yaw_offset_deg);
let notify_lost = Arc::new(Notify::new());
let task_died = Arc::new(Notify::new());
let mut tasks: Vec<(&'static str, JoinHandle<()>)> = Vec::new();
{
let notify = notify_lost.clone();
let gimbal = gimbal_shared.clone();
let state = state_manager.clone();
let configured_path = config.device.path.clone();
spawn_critical(&mut tasks, "reconnect", &task_died, async move {
reconnect_task(notify, gimbal, state, configured_path).await;
});
}
let (attitude_tx, _attitude_rx_seed) = attitude::attitude_channel();
{
let gimbal = gimbal_shared.clone();
let state = state_manager.clone();
let notify = notify_lost.clone();
let corrector = config
.gimbal
.yaw_corrector_enabled
.then(|| crate::daemon::yaw_corrector::YawCorrector::new(0.0));
let tx = attitude_tx.clone();
spawn_critical(&mut tasks, "attitude-poll", &task_died, async move {
attitude::attitude_poll_loop(gimbal, state, notify, corrector, tx).await;
});
}
let ipc_gimbal = gimbal_shared.clone();
let ipc_state = state_manager.clone();
let ipc_config = config.ipc.clone();
let ipc_socket_path = ipc_config.socket_path.clone();
spawn_critical(&mut tasks, "ipc", &task_died, async move {
let server = IpcServer::new(ipc_config, ipc_state, ipc_gimbal);
if let Err(e) = server.start().await {
error!("IPC server error: {}", e);
}
});
if config.mavlink.enabled {
let mavlink_gimbal = gimbal_shared.clone();
let mavlink_state = state_manager.clone();
let mavlink_cfg = config.mavlink.clone();
let attitude_rx = attitude_tx.subscribe();
spawn_critical(&mut tasks, "mavlink", &task_died, async move {
let manager = MavlinkManager::new(mavlink_state, mavlink_gimbal);
if let Err(e) = manager.start(mavlink_cfg, attitude_rx).await {
error!("MAVLink manager error: {}", e);
}
});
}
info!("Daemon started successfully");
tokio::select! {
res = wait_for_shutdown_signal() => {
let name = res?;
info!("Received {}, beginning bounded shutdown...", name);
}
_ = task_died.notified() => {
warn!("A critical task exited unexpectedly; initiating shutdown...");
}
}
drain_tasks(tasks).await;
cleanup_ipc_socket(&ipc_socket_path);
info!("Daemon stopped");
Ok(())
}
fn spawn_critical<F>(
tasks: &mut Vec<(&'static str, JoinHandle<()>)>,
name: &'static str,
task_died: &Arc<Notify>,
fut: F,
) where
F: std::future::Future<Output = ()> + Send + 'static,
{
let died = task_died.clone();
tasks.push((
name,
tokio::spawn(async move {
fut.await;
died.notify_one();
}),
));
}
async fn drain_tasks(tasks: Vec<(&'static str, JoinHandle<()>)>) {
for (name, handle) in &tasks {
debug!("Aborting task '{}'", name);
handle.abort();
}
for (name, handle) in tasks {
match tokio::time::timeout(SHUTDOWN_GRACE, handle).await {
Ok(Ok(())) => debug!("Task '{}' exited cleanly", name),
Ok(Err(e)) if e.is_cancelled() => debug!("Task '{}' aborted", name),
Ok(Err(e)) => warn!("Task '{}' exited with error: {}", name, e),
Err(_) => warn!(
"Task '{}' did not exit within {:?}; runtime will drop it",
name, SHUTDOWN_GRACE
),
}
}
}
fn cleanup_ipc_socket(path: &str) {
match std::fs::remove_file(path) {
Ok(()) => debug!("Removed IPC socket file {}", path),
Err(e) if e.kind() == std::io::ErrorKind::NotFound => {}
Err(e) => warn!("Failed to remove IPC socket {}: {}", path, e),
}
}
async fn reconnect_task(
notify_lost: Arc<Notify>,
gimbal: GimbalHandle,
state_manager: StateManager,
configured_path: String,
) {
loop {
notify_lost.notified().await;
warn!(
"Reconnect: attitude loop signaled device lost; beginning recovery (configured path: {})",
configured_path
);
let mut backoff = Duration::from_millis(500);
const MAX_BACKOFF: Duration = Duration::from_secs(10);
let mut attempt: u32 = 0;
let new_gimbal = loop {
attempt = attempt.wrapping_add(1);
let path = if configured_path == "auto" {
DeviceScanner::new().find_storm32_device()
} else if std::path::Path::new(&configured_path).exists() {
Some(configured_path.clone())
} else {
None
};
if let Some(path) = path {
match gimbal::detect_gimbal(&path) {
Ok(g) => {
info!(
"Reconnect: re-opened gimbal at {} after {} attempts",
path, attempt
);
break g;
}
Err(e) => debug!("Reconnect attempt {} on {} failed: {}", attempt, path, e),
}
} else {
debug!("Reconnect attempt {}: no candidate device found", attempt);
}
tokio::time::sleep(backoff).await;
backoff = (backoff * 2).min(MAX_BACKOFF);
};
let mut new_gimbal = new_gimbal;
if let Ok(version) = new_gimbal.get_version() {
info!("Reconnect: firmware version is {}", version);
state_manager.update_firmware_version(version);
}
gimbal.replace(new_gimbal).await;
info!("Reconnect: gimbal swap complete; resuming normal operation");
}
}
async fn wait_for_shutdown_signal() -> crate::error::Result<&'static str> {
#[cfg(unix)]
{
use tokio::signal::unix::{signal, SignalKind};
let mut sigterm = signal(SignalKind::terminate())?;
tokio::select! {
_ = tokio::signal::ctrl_c() => Ok("SIGINT"),
_ = sigterm.recv() => Ok("SIGTERM"),
}
}
#[cfg(not(unix))]
{
tokio::signal::ctrl_c().await?;
Ok("Ctrl-C")
}
}