ascom-alpaca-core 0.2.2

Framework-agnostic ASCOM Alpaca protocol types and traits for Rust — all 10 device types, no HTTP framework required
Documentation
use std::sync::Mutex;
use std::time::Instant;

use crate::dome::{Dome, ShutterState};
use crate::types::{AlpacaError, AlpacaResult, DeviceType};

pub struct MockDome {
    connected: Mutex<bool>,
    shutter: Mutex<ShutterState>,
    at_home: Mutex<bool>,
    at_park: Mutex<bool>,
    azimuth: Mutex<f64>,
    altitude: Mutex<f64>,
    target_azimuth: Mutex<f64>,
    target_altitude: Mutex<f64>,
    slew_start: Mutex<Option<Instant>>,
    alt_slew_start: Mutex<Option<Instant>>,
    slaved: Mutex<bool>,
    park_azimuth: Mutex<f64>,
}

impl Default for MockDome {
    fn default() -> Self {
        Self::new()
    }
}

impl MockDome {
    pub fn new() -> Self {
        Self {
            connected: Mutex::new(false),
            shutter: Mutex::new(ShutterState::Closed),
            at_home: Mutex::new(true),
            at_park: Mutex::new(false),
            azimuth: Mutex::new(0.0),
            altitude: Mutex::new(90.0),
            target_azimuth: Mutex::new(0.0),
            target_altitude: Mutex::new(90.0),
            slew_start: Mutex::new(None),
            alt_slew_start: Mutex::new(None),
            slaved: Mutex::new(false),
            park_azimuth: Mutex::new(0.0),
        }
    }

    /// Check if azimuth/altitude slews have completed.
    /// ConformU requires slewing to be true for at least 3 seconds — use 4s for margin.
    fn check_slew_complete(&self) {
        let start = *self.slew_start.lock().unwrap();
        if let Some(started_at) = start {
            if started_at.elapsed().as_millis() >= 4000 {
                *self.azimuth.lock().unwrap() = *self.target_azimuth.lock().unwrap();
                *self.slew_start.lock().unwrap() = None;
            }
        }
        let alt_start = *self.alt_slew_start.lock().unwrap();
        if let Some(started_at) = alt_start {
            if started_at.elapsed().as_millis() >= 4000 {
                *self.altitude.lock().unwrap() = *self.target_altitude.lock().unwrap();
                *self.alt_slew_start.lock().unwrap() = None;
            }
        }
    }
}

impl_mock_device!(MockDome,
    name: "Mock Dome",
    unique_id: "mock-dome-001",
    device_type: DeviceType::Dome,
    interface_version: 3,
    device_state: |self_: &MockDome| {
        use crate::device::common::DeviceStateBuilder;
        self_.check_slew_complete();
        let slewing = self_.slew_start.lock().unwrap().is_some()
            || self_.alt_slew_start.lock().unwrap().is_some();
        Ok(DeviceStateBuilder::new()
            .add("Altitude", *self_.altitude.lock().unwrap())
            .add("AtHome", *self_.at_home.lock().unwrap())
            .add("AtPark", *self_.at_park.lock().unwrap())
            .add("Azimuth", *self_.azimuth.lock().unwrap())
            .add("ShutterStatus", *self_.shutter.lock().unwrap() as i32)
            .add("Slewing", slewing)
            .build())
    }
);

impl Dome for MockDome {
    fn altitude(&self) -> AlpacaResult<f64> {
        Ok(*self.altitude.lock().unwrap())
    }

    fn azimuth(&self) -> AlpacaResult<f64> {
        self.check_slew_complete();
        Ok(*self.azimuth.lock().unwrap())
    }

    fn at_home(&self) -> AlpacaResult<bool> {
        Ok(*self.at_home.lock().unwrap())
    }
    fn at_park(&self) -> AlpacaResult<bool> {
        Ok(*self.at_park.lock().unwrap())
    }
    fn shutter_status(&self) -> AlpacaResult<ShutterState> {
        Ok(*self.shutter.lock().unwrap())
    }

    fn slaved(&self) -> AlpacaResult<bool> {
        Ok(*self.slaved.lock().unwrap())
    }
    fn set_slaved(&self, slaved: bool) -> AlpacaResult<()> {
        *self.slaved.lock().unwrap() = slaved;
        Ok(())
    }

    fn slewing(&self) -> AlpacaResult<bool> {
        self.check_slew_complete();
        Ok(self.slew_start.lock().unwrap().is_some()
            || self.alt_slew_start.lock().unwrap().is_some())
    }

    // All capabilities enabled for comprehensive ConformU testing
    fn can_find_home(&self) -> AlpacaResult<bool> {
        Ok(true)
    }
    fn can_park(&self) -> AlpacaResult<bool> {
        Ok(true)
    }
    fn can_set_altitude(&self) -> AlpacaResult<bool> {
        Ok(true)
    }
    fn can_set_azimuth(&self) -> AlpacaResult<bool> {
        Ok(true)
    }
    fn can_set_park(&self) -> AlpacaResult<bool> {
        Ok(true)
    }
    fn can_set_shutter(&self) -> AlpacaResult<bool> {
        Ok(true)
    }
    fn can_slave(&self) -> AlpacaResult<bool> {
        Ok(true)
    }
    fn can_sync_azimuth(&self) -> AlpacaResult<bool> {
        Ok(true)
    }

    fn slew_to_azimuth(&self, azimuth: f64) -> AlpacaResult<()> {
        if !(0.0..360.0).contains(&azimuth) {
            return Err(AlpacaError::InvalidValue(format!(
                "Azimuth {azimuth} out of range 0-360"
            )));
        }
        *self.at_home.lock().unwrap() = false;
        *self.at_park.lock().unwrap() = false;
        *self.target_azimuth.lock().unwrap() = azimuth;
        *self.slew_start.lock().unwrap() = Some(Instant::now());
        Ok(())
    }

    fn slew_to_altitude(&self, altitude: f64) -> AlpacaResult<()> {
        if !(0.0..=90.0).contains(&altitude) {
            return Err(AlpacaError::InvalidValue(format!(
                "Altitude {altitude} out of range 0-90"
            )));
        }
        *self.target_altitude.lock().unwrap() = altitude;
        *self.alt_slew_start.lock().unwrap() = Some(Instant::now());
        Ok(())
    }

    fn sync_to_azimuth(&self, azimuth: f64) -> AlpacaResult<()> {
        if !(0.0..360.0).contains(&azimuth) {
            return Err(AlpacaError::InvalidValue(format!(
                "Azimuth {azimuth} out of range 0-360"
            )));
        }
        *self.azimuth.lock().unwrap() = azimuth;
        *self.target_azimuth.lock().unwrap() = azimuth;
        *self.slew_start.lock().unwrap() = None;
        Ok(())
    }

    fn open_shutter(&self) -> AlpacaResult<()> {
        *self.shutter.lock().unwrap() = ShutterState::Open;
        Ok(())
    }

    fn close_shutter(&self) -> AlpacaResult<()> {
        *self.shutter.lock().unwrap() = ShutterState::Closed;
        Ok(())
    }

    fn park(&self) -> AlpacaResult<()> {
        let park_az = *self.park_azimuth.lock().unwrap();
        *self.azimuth.lock().unwrap() = park_az;
        *self.target_azimuth.lock().unwrap() = park_az;
        *self.at_park.lock().unwrap() = true;
        *self.at_home.lock().unwrap() = false;
        *self.slew_start.lock().unwrap() = None;
        Ok(())
    }

    fn set_park(&self) -> AlpacaResult<()> {
        *self.park_azimuth.lock().unwrap() = *self.azimuth.lock().unwrap();
        Ok(())
    }

    fn find_home(&self) -> AlpacaResult<()> {
        *self.azimuth.lock().unwrap() = 0.0;
        *self.target_azimuth.lock().unwrap() = 0.0;
        *self.at_home.lock().unwrap() = true;
        *self.at_park.lock().unwrap() = false;
        *self.slew_start.lock().unwrap() = None;
        Ok(())
    }

    fn abort_slew(&self) -> AlpacaResult<()> {
        *self.slew_start.lock().unwrap() = None;
        *self.alt_slew_start.lock().unwrap() = None;
        Ok(())
    }
}