ascom-alpaca-core 0.2.3

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

use crate::switch::Switch;
use crate::types::{AlpacaError, AlpacaResult, DeviceType};

/// Per-channel configuration for mock switches.
struct SwitchChannel {
    name: &'static str,
    description: &'static str,
    min: f64,
    max: f64,
    step: f64,
    can_write: bool,
}

/// Default channels demonstrating all ASCOM switch types:
/// - Boolean (on/off): min=0, max=1, step=1
/// - Multi-state (selector): min=0, max=3, step=1 (4 positions)
/// - Analog (dimmer): min=0, max=100, step=0.5
const CHANNELS: &[SwitchChannel] = &[
    SwitchChannel {
        name: "Power",
        description: "Boolean on/off switch",
        min: 0.0,
        max: 1.0,
        step: 1.0,
        can_write: true,
    },
    SwitchChannel {
        name: "Selector",
        description: "Multi-state rotary selector (4 positions)",
        min: 0.0,
        max: 3.0,
        step: 1.0,
        can_write: true,
    },
    SwitchChannel {
        name: "Dimmer",
        description: "Analog dimmer (0-100%, 0.5 step)",
        min: 0.0,
        max: 100.0,
        step: 0.5,
        can_write: true,
    },
];

pub struct MockSwitch {
    connected: Mutex<bool>,
    values: Mutex<Vec<f64>>,
    names: Mutex<Vec<String>>,
}

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

impl MockSwitch {
    pub fn new() -> Self {
        let values = CHANNELS.iter().map(|ch| ch.min).collect();
        let names = CHANNELS.iter().map(|ch| ch.name.to_string()).collect();
        Self {
            connected: Mutex::new(false),
            values: Mutex::new(values),
            names: Mutex::new(names),
        }
    }

    fn validate_id(id: u32) -> AlpacaResult<&'static SwitchChannel> {
        CHANNELS.get(id as usize).ok_or_else(|| {
            AlpacaError::InvalidValue(format!(
                "Switch ID {id} out of range 0-{}",
                CHANNELS.len() - 1
            ))
        })
    }

    /// Clamp value to nearest valid step within channel range.
    fn clamp_to_step(ch: &SwitchChannel, value: f64) -> f64 {
        let clamped = value.clamp(ch.min, ch.max);
        if ch.step > 0.0 {
            let steps = ((clamped - ch.min) / ch.step).round();
            (ch.min + steps * ch.step).min(ch.max)
        } else {
            clamped
        }
    }
}

impl_mock_device!(MockSwitch,
    name: "Mock Switch",
    unique_id: "mock-sw-001",
    device_type: DeviceType::Switch,
    interface_version: 3,
    device_state: |self_: &MockSwitch| {
        use crate::device::common::DeviceStateBuilder;
        let values = self_.values.lock().unwrap();
        let mut b = DeviceStateBuilder::new();
        for (i, ch) in CHANNELS.iter().enumerate() {
            let val = values[i];
            let bool_val = val >= (ch.min + ch.max) / 2.0;
            b = b
                .add(&format!("GetSwitch{i}"), bool_val)
                .add(&format!("GetSwitchValue{i}"), val)
                .add(&format!("StateChangeComplete{i}"), true);
        }
        Ok(b.build())
    }
);

impl Switch for MockSwitch {
    fn max_switch(&self) -> AlpacaResult<i32> {
        Ok(CHANNELS.len() as i32)
    }

    fn can_write(&self, id: u32) -> AlpacaResult<bool> {
        let ch = Self::validate_id(id)?;
        Ok(ch.can_write)
    }

    fn get_switch(&self, id: u32) -> AlpacaResult<bool> {
        Self::validate_id(id)?;
        let values = self.values.lock().unwrap();
        let val = values[id as usize];
        // Boolean interpretation: value >= midpoint of range
        let ch = &CHANNELS[id as usize];
        Ok(val >= (ch.min + ch.max) / 2.0)
    }

    fn set_switch(&self, id: u32, state: bool) -> AlpacaResult<()> {
        let ch = Self::validate_id(id)?;
        if !ch.can_write {
            return Err(AlpacaError::NotImplemented("Switch is read-only".into()));
        }
        let mut values = self.values.lock().unwrap();
        values[id as usize] = if state { ch.max } else { ch.min };
        Ok(())
    }

    fn get_switch_value(&self, id: u32) -> AlpacaResult<f64> {
        Self::validate_id(id)?;
        let values = self.values.lock().unwrap();
        Ok(values[id as usize])
    }

    fn set_switch_value(&self, id: u32, value: f64) -> AlpacaResult<()> {
        let ch = Self::validate_id(id)?;
        if !ch.can_write {
            return Err(AlpacaError::NotImplemented("Switch is read-only".into()));
        }
        if value < ch.min || value > ch.max {
            return Err(AlpacaError::InvalidValue(format!(
                "Value {value} out of range {}-{}",
                ch.min, ch.max
            )));
        }
        let mut values = self.values.lock().unwrap();
        values[id as usize] = Self::clamp_to_step(ch, value);
        Ok(())
    }

    fn get_switch_name(&self, id: u32) -> AlpacaResult<String> {
        Self::validate_id(id)?;
        let names = self.names.lock().unwrap();
        Ok(names[id as usize].clone())
    }

    fn set_switch_name(&self, id: u32, name: &str) -> AlpacaResult<()> {
        Self::validate_id(id)?;
        let mut names = self.names.lock().unwrap();
        names[id as usize] = name.to_string();
        Ok(())
    }

    fn get_switch_description(&self, id: u32) -> AlpacaResult<String> {
        let ch = Self::validate_id(id)?;
        Ok(ch.description.to_string())
    }

    fn min_switch_value(&self, id: u32) -> AlpacaResult<f64> {
        let ch = Self::validate_id(id)?;
        Ok(ch.min)
    }

    fn max_switch_value(&self, id: u32) -> AlpacaResult<f64> {
        let ch = Self::validate_id(id)?;
        Ok(ch.max)
    }

    fn switch_step(&self, id: u32) -> AlpacaResult<f64> {
        let ch = Self::validate_id(id)?;
        Ok(ch.step)
    }

    fn can_async(&self, id: u32) -> AlpacaResult<bool> {
        Self::validate_id(id)?;
        Ok(false)
    }

    fn set_async(&self, id: u32, _state: bool) -> AlpacaResult<()> {
        Self::validate_id(id)?;
        Err(AlpacaError::NotImplemented("Async not supported".into()))
    }

    fn set_async_value(&self, id: u32, _value: f64) -> AlpacaResult<()> {
        Self::validate_id(id)?;
        Err(AlpacaError::NotImplemented("Async not supported".into()))
    }

    fn cancel_async(&self, id: u32) -> AlpacaResult<()> {
        Self::validate_id(id)?;
        Err(AlpacaError::NotImplemented("Async not supported".into()))
    }

    fn state_change_complete(&self, id: u32) -> AlpacaResult<bool> {
        Self::validate_id(id)?;
        Err(AlpacaError::NotImplemented("Async not supported".into()))
    }
}