Skip to main content

cal_hardware/
lib.rs

1use anyhow::{Result, anyhow};
2use serialport::SerialPort;
3use std::sync::Arc;
4use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
5use std::time::Duration;
6
7pub mod projection_window;
8
9/// Message IDs for Thorlabs APT Protocol
10#[allow(dead_code)]
11mod msg {
12    pub const MOT_MOVE_HOME: u16 = 0x0443;
13    pub const MOT_MOVE_VELOCITY: u16 = 0x0457;
14    pub const MOT_STOP_IMMEDIATE: u16 = 0x0465;
15    pub const MOT_REQ_POSN: u16 = 0x0411;
16    pub const MOT_GET_POSN: u16 = 0x0412;
17    pub const MOT_MOVE_COMPLETED: u16 = 0x0464;
18}
19
20/// Trait for a rotation stage motor.
21pub trait Motor {
22    /// Returns the current position in degrees (0.0 to 360.0).
23    fn get_position(&mut self) -> Result<f32>;
24    /// Starts rotation at a given velocity (deg/sec).
25    fn start_rotation(&mut self, velocity: f32) -> Result<()>;
26    /// Stops rotation immediately.
27    fn stop(&mut self) -> Result<()>;
28}
29
30/// Implementation of a Thorlabs motor stage via Serial (APT Protocol).
31pub struct ThorlabsMotor {
32    port: Box<dyn SerialPort>,
33    counts_per_deg: f32, // Conversion factor (device dependent)
34    channel: u8,
35}
36
37impl ThorlabsMotor {
38    pub fn new(port_name: &str) -> Result<Self> {
39        let port = serialport::new(port_name, 115_200)
40            .timeout(Duration::from_millis(50))
41            .open()?;
42
43        Ok(Self {
44            port,
45            counts_per_deg: 1919.64, // Default for many Thorlabs PRMT1 stages
46            channel: 1,
47        })
48    }
49
50    /// Sends a short 6-byte message
51    fn send_command(&mut self, msg_id: u16, param1: u8, param2: u8) -> Result<()> {
52        let mut buf = [0u8; 6];
53        buf[0..2].copy_from_slice(&msg_id.to_le_bytes());
54        buf[2] = param1;
55        buf[3] = param2;
56        buf[4] = 0x50; // Destination: Generic USB unit
57        buf[5] = 0x01; // Source: Host
58        self.port.write_all(&buf)?;
59        Ok(())
60    }
61
62    /// Receives and parses a GET_POSN message
63    fn read_position_response(&mut self) -> Result<f32> {
64        let mut header = [0u8; 6];
65        self.port.read_exact(&mut header)?;
66
67        let msg_id = u16::from_le_bytes([header[0], header[1]]);
68        if msg_id != msg::MOT_GET_POSN {
69            return Err(anyhow!("Unexpected message ID: 0x{:04X}", msg_id));
70        }
71
72        // MOT_GET_POSN has 6 bytes of data following the header
73        let mut data = [0u8; 6];
74        self.port.read_exact(&mut data)?;
75
76        // Position is a 32-bit signed integer in the first 4 bytes of data
77        let counts = i32::from_le_bytes([data[2], data[3], data[4], data[5]]);
78
79        Ok(counts as f32 / self.counts_per_deg)
80    }
81}
82
83impl Motor for ThorlabsMotor {
84    fn get_position(&mut self) -> Result<f32> {
85        self.send_command(msg::MOT_REQ_POSN, self.channel, 0)?;
86        self.read_position_response()
87    }
88
89    fn start_rotation(&mut self, velocity: f32) -> Result<()> {
90        // Simple version: Param1 is channel, Param2 is direction (1 for forward)
91        // Note: Real velocity control requires a longer data packet (MOT_SET_VELPARAMS)
92        // and then MOT_MOVE_VELOCITY.
93        self.send_command(
94            msg::MOT_MOVE_VELOCITY,
95            self.channel,
96            if velocity >= 0.0 { 1 } else { 2 },
97        )
98    }
99
100    fn stop(&mut self) -> Result<()> {
101        self.send_command(msg::MOT_STOP_IMMEDIATE, self.channel, 0)
102    }
103}
104
105/// A mock motor for testing without hardware.
106pub struct MockMotor {
107    start_time: Option<std::time::Instant>,
108    velocity: f32,
109}
110
111impl MockMotor {
112    pub fn new() -> Self {
113        Self {
114            start_time: None,
115            velocity: 0.0,
116        }
117    }
118}
119
120impl Default for MockMotor {
121    fn default() -> Self {
122        Self::new()
123    }
124}
125
126impl Motor for MockMotor {
127    fn get_position(&mut self) -> Result<f32> {
128        if let Some(start) = self.start_time {
129            let elapsed = start.elapsed().as_secs_f32();
130            Ok((elapsed * self.velocity) % 360.0)
131        } else {
132            Ok(0.0)
133        }
134    }
135
136    fn start_rotation(&mut self, velocity: f32) -> Result<()> {
137        self.start_time = Some(std::time::Instant::now());
138        self.velocity = velocity;
139        Ok(())
140    }
141
142    fn stop(&mut self) -> Result<()> {
143        self.start_time = None;
144        Ok(())
145    }
146}
147
148/// Orchestrates the synchronization between motor and display.
149pub struct ProjectionController<M: Motor + Send + 'static> {
150    pub motor: Arc<std::sync::Mutex<M>>,
151    _projections: ndarray::Array3<f32>, // (nr, n_angles, nz)
152    angles: Vec<f32>,
153    shared_frame_index: Arc<AtomicUsize>,
154}
155
156impl<M: Motor + Send + 'static> ProjectionController<M> {
157    pub fn new(motor: M, projections: ndarray::Array3<f32>, angles: Vec<f32>) -> Self {
158        Self {
159            motor: Arc::new(std::sync::Mutex::new(motor)),
160            _projections: projections,
161            angles,
162            shared_frame_index: Arc::new(AtomicUsize::new(0)),
163        }
164    }
165
166    pub fn shared_frame_index(&self) -> Arc<AtomicUsize> {
167        Arc::clone(&self.shared_frame_index)
168    }
169
170    /// Finds the closest projection frame index for a given motor angle.
171    pub fn get_frame_index(&self, current_angle: f32) -> usize {
172        let mut closest_idx = 0;
173        let mut min_diff = f32::MAX;
174
175        for (i, &angle) in self.angles.iter().enumerate() {
176            let diff = (angle - current_angle).abs();
177            let wrapped_diff = diff.min(360.0 - diff);
178            if wrapped_diff < min_diff {
179                min_diff = wrapped_diff;
180                closest_idx = i;
181            }
182        }
183        closest_idx
184    }
185
186    /// Spawns a background thread to poll the motor and update the shared frame index.
187    pub fn spawn_sync_thread(&self, stop_signal: Arc<AtomicBool>) {
188        let motor = Arc::clone(&self.motor);
189        let shared_idx = Arc::clone(&self.shared_frame_index);
190        let angles = self.angles.clone();
191
192        std::thread::spawn(move || {
193            while !stop_signal.load(Ordering::Relaxed) {
194                if let Ok(mut m) = motor.lock() {
195                    if let Ok(pos) = m.get_position() {
196                        // Find closest index
197                        let mut closest_idx = 0;
198                        let mut min_diff = f32::MAX;
199                        for (i, &angle) in angles.iter().enumerate() {
200                            let diff = (angle - pos).abs();
201                            let wrapped_diff = diff.min(360.0 - diff);
202                            if wrapped_diff < min_diff {
203                                min_diff = wrapped_diff;
204                                closest_idx = i;
205                            }
206                        }
207                        shared_idx.store(closest_idx, Ordering::Relaxed);
208                    }
209                }
210                std::thread::sleep(Duration::from_millis(5)); // ~200Hz polling
211            }
212        });
213    }
214
215    pub fn poll_and_render(&mut self, window: &projection_window::ProjectionWindow) -> Result<()> {
216        let frame_idx = self.shared_frame_index.load(Ordering::Relaxed);
217        window.render(frame_idx)?;
218        Ok(())
219    }
220
221    pub fn run_sync_loop(&mut self) -> Result<()> {
222        {
223            let mut m = self
224                .motor
225                .lock()
226                .map_err(|_| anyhow!("Mutex lock failed"))?;
227            m.start_rotation(10.0)?; // Example 10 deg/sec
228        }
229
230        loop {
231            let frame_idx = self.shared_frame_index.load(Ordering::Relaxed);
232
233            // In a real implementation, we would send the frame_idx to the display thread/GPU.
234            println!("Frame Index: {}", frame_idx);
235
236            std::thread::sleep(Duration::from_millis(16)); // ~60Hz polling
237        }
238    }
239}