vexide_devices/smart/
optical.rs

1//! Optical Sensor
2//!
3//! This module provides an interface to interact with the V5 Optical Sensor, which combines
4//! ambient light sensing, color detection, proximity measurement, and gesture recognition
5//! capabilities.
6//!
7//! # Hardware Overview
8//!
9//! The optical sensor provides multi-modal optical sensing with an integrated white LED
10//! for low-light operation.
11//!
12//! ## Color Detection
13//!
14//! Color data reported as RGB, HSV, and grayscale data, with optimal performance at
15//! distances under 100mm. The proximity sensing uses reflected light intensity, making
16//! readings dependent on both ambient lighting and target reflectivity.
17//!
18//! ## Gesture Detection
19//!
20//! The optical sensor can detect four distinct motions (up, down, left, right) of objects
21//! passing over the sensor.
22
23use core::time::Duration;
24
25use vex_sdk::{
26    vexDeviceOpticalBrightnessGet, vexDeviceOpticalGestureEnable, vexDeviceOpticalGestureGet,
27    vexDeviceOpticalHueGet, vexDeviceOpticalIntegrationTimeGet, vexDeviceOpticalIntegrationTimeSet,
28    vexDeviceOpticalLedPwmGet, vexDeviceOpticalLedPwmSet, vexDeviceOpticalProximityGet,
29    vexDeviceOpticalRawGet, vexDeviceOpticalRgbGet, vexDeviceOpticalSatGet,
30    vexDeviceOpticalStatusGet, V5_DeviceOpticalGesture, V5_DeviceOpticalRaw, V5_DeviceOpticalRgb,
31    V5_DeviceT,
32};
33
34use super::{SmartDevice, SmartDeviceTimestamp, SmartDeviceType, SmartPort};
35use crate::PortError;
36
37/// An optical sensor plugged into a Smart Port.
38#[derive(Debug, Eq, PartialEq)]
39pub struct OpticalSensor {
40    port: SmartPort,
41    device: V5_DeviceT,
42}
43
44// SAFETY: Required because we store a raw pointer to the device handle to avoid it getting from the
45// SDK each device function. Simply sharing a raw pointer across threads is not inherently unsafe.
46unsafe impl Send for OpticalSensor {}
47unsafe impl Sync for OpticalSensor {}
48
49impl OpticalSensor {
50    /// The smallest integration time you can set on an optical sensor.
51    ///
52    /// Source: <https://www.vexforum.com/t/v5-optical-sensor-refresh-rate/109632/9>
53    pub const MIN_INTEGRATION_TIME: Duration = Duration::from_millis(3);
54
55    /// The largest integration time you can set on an optical sensor.
56    ///
57    /// Source: <https://www.vexforum.com/t/v5-optical-sensor-refresh-rate/109632/9>
58    pub const MAX_INTEGRATION_TIME: Duration = Duration::from_millis(712);
59
60    /// The interval that gesture detection through [`OpticalSensor::last_gesture`] provides new data at.
61    pub const GESTURE_UPDATE_INTERVAL: Duration = Duration::from_millis(50);
62
63    /// Creates a new optical sensor from a [`SmartPort`].
64    ///
65    /// # Examples
66    ///
67    /// ```
68    /// use vexide::prelude::*;
69    ///
70    /// #[vexide::main]
71    /// async fn main(peripherals: Peripherals) {
72    ///     let sensor = OpticalSensor::new(peripherals.port_1);
73    /// }
74    /// ```
75    #[must_use]
76    pub fn new(port: SmartPort) -> Self {
77        Self {
78            device: unsafe { port.device_handle() },
79            port,
80        }
81    }
82
83    /// Returns the intensity/brightness of the sensor's LED indicator as a number from [0.0-1.0].
84    ///
85    /// # Errors
86    ///
87    /// An error is returned if an optical sensor is not currently connected to the Smart Port.
88    ///
89    ///
90    /// # Examples
91    ///
92    /// ```
93    /// use vexide::prelude::*;
94    ///
95    /// #[vexide::main]
96    /// async fn main(peripherals: Peripherals) {
97    ///     let sensor = OpticalSensor::new(peripherals.port_1);
98    ///
99    ///     if let Ok(brightness) = sensor.led_brightness() {
100    ///         println!("LED brightness: {:.1}%", brightness * 100.0);
101    ///     }
102    /// }
103    /// ```
104    pub fn led_brightness(&self) -> Result<f64, PortError> {
105        self.validate_port()?;
106
107        Ok(f64::from(unsafe { vexDeviceOpticalLedPwmGet(self.device) }) / 100.0)
108    }
109
110    /// Set the intensity of (intensity/brightness) of the sensor's LED indicator.
111    ///
112    /// Intensity is expressed as a number from [0.0, 1.0].
113    ///
114    /// # Errors
115    ///
116    /// An error is returned if an optical sensor is not currently connected to the Smart Port.
117    ///
118    /// # Examples
119    ///
120    /// ```
121    /// use vexide::prelude::*;
122    /// use core::time::Duration;
123    ///
124    /// #[vexide::main]
125    /// async fn main(peripherals: Peripherals) {
126    ///     let mut sensor = OpticalSensor::new(peripherals.port_1);
127    ///
128    ///     // Blink LED 3 times
129    ///     for _ in 0..3 {
130    ///         // Turn LED on
131    ///         if let Err(e) = sensor.set_led_brightness(1.0) {
132    ///             println!("Failed to turn LED on: {:?}", e);
133    ///         }
134    ///
135    ///         sleep(Duration::from_millis(250)).await;
136    ///
137    ///         // Turn LED off
138    ///         if let Err(e) = sensor.set_led_brightness(0.0) {
139    ///             println!("Failed to turn LED off: {:?}", e);
140    ///         }
141    ///
142    ///         sleep(Duration::from_millis(250)).await;
143    ///     }
144    /// }
145    /// ```
146    pub fn set_led_brightness(&mut self, brightness: f64) -> Result<(), PortError> {
147        self.validate_port()?;
148
149        unsafe { vexDeviceOpticalLedPwmSet(self.device, (brightness * 100.0) as i32) }
150
151        Ok(())
152    }
153
154    /// Returns integration time of the optical sensor in milliseconds, with
155    /// minimum time being 3ms and the maximum time being 712ms.
156    ///
157    /// The default integration time for the sensor is 103mS, unless otherwise set with
158    /// [`OpticalSensor::set_integration_time`].
159    ///
160    /// # Errors
161    ///
162    /// An error is returned if an optical sensor is not currently connected to the Smart Port.
163    ///
164    /// # Examples
165    ///
166    /// ```
167    /// use vexide::prelude::*;
168    /// use core::time::Duration;
169    ///
170    /// #[vexide::main]
171    /// async fn main(peripherals: Peripherals) {
172    ///     let mut sensor = OpticalSensor::new(peripherals.port_1);
173    ///
174    ///     // Set integration time to 50 milliseconds.
175    ///     _ = sensor.set_integration_time(Duration::from_millis(50));
176    ///
177    ///     // Log out the new integration time.
178    ///     if let Ok(time) = sensor.integration_time() {
179    ///         println!("Integration time: {:?}", time);
180    ///     }
181    /// }
182    /// ```
183    pub fn integration_time(&self) -> Result<Duration, PortError> {
184        self.validate_port()?;
185
186        Ok(Duration::from_millis(
187            unsafe { vexDeviceOpticalIntegrationTimeGet(self.device) } as u64,
188        ))
189    }
190
191    /// Set the integration time of the optical sensor.
192    ///
193    /// Lower integration time results in faster update rates with lower accuracy
194    /// due to less available light being read by the sensor.
195    ///
196    /// The `time` value must be a [`Duration`] between 3 and 712 milliseconds. If
197    /// the integration time is out of this range, it will be clamped to fit inside it. See
198    /// <https://www.vexforum.com/t/v5-optical-sensor-refresh-rate/109632/9> for
199    /// more information.
200    ///
201    /// The default integration time for the sensor is 103mS, unless otherwise set.
202    ///
203    /// # Errors
204    ///
205    /// An error is returned if an optical sensor is not currently connected to the Smart Port.
206    ///
207    /// # Examples
208    ///
209    /// ```
210    /// use vexide::prelude::*;
211    /// use core::time::Duration;
212    ///
213    /// #[vexide::main]
214    /// async fn main(peripherals: Peripherals) {
215    ///     let mut sensor = OpticalSensor::new(peripherals.port_1);
216    ///
217    ///     // Set integration time to 50 milliseconds.
218    ///     _ = sensor.set_integration_time(Duration::from_millis(50));
219    /// }
220    /// ```
221    pub fn set_integration_time(&mut self, time: Duration) -> Result<(), PortError> {
222        self.validate_port()?;
223
224        // `time_ms` is clamped to a range that will not cause precision loss.
225        #[allow(clippy::cast_precision_loss)]
226        let time_ms = time.as_millis().clamp(
227            Self::MIN_INTEGRATION_TIME.as_millis(),
228            Self::MAX_INTEGRATION_TIME.as_millis(),
229        ) as f64;
230
231        unsafe { vexDeviceOpticalIntegrationTimeSet(self.device, time_ms) }
232
233        Ok(())
234    }
235
236    /// Returns the detected color's hue.
237    ///
238    /// Hue has a range of `0` to `359.999`.
239    ///
240    /// # Errors
241    ///
242    /// An error is returned if an optical sensor is not currently connected to the Smart Port.
243    ///
244    /// # Examples
245    ///
246    /// ```
247    /// use vexide::prelude::*;
248    ///
249    /// #[vexide::main]
250    /// async fn main(peripherals: Peripherals) {
251    ///     let sensor = OpticalSensor::new(peripherals.port_1);
252    ///
253    ///     if let Ok(hue) = sensor.hue() {
254    ///         println!("Detected color hue: {:.1}°", hue);
255    ///
256    ///         // Classify the color based on hue angle
257    ///         let color = match hue as u32 {
258    ///             0..=30 => "Red",
259    ///             31..=90 => "Yellow",
260    ///             91..=150 => "Green",
261    ///             151..=210 => "Cyan",
262    ///             211..=270 => "Blue",
263    ///             271..=330 => "Magenta",
264    ///             _ => "Red", // 331-359 wraps back to red
265    ///         };
266    ///
267    ///         println!("Color: {}", color);
268    ///     }
269    /// }
270    /// ```
271    pub fn hue(&self) -> Result<f64, PortError> {
272        self.validate_port()?;
273
274        Ok(unsafe { vexDeviceOpticalHueGet(self.device) })
275    }
276
277    /// Returns the detected color's saturation.
278    ///
279    /// Saturation has a range `0` to `1.0`.
280    ///
281    /// # Errors
282    ///
283    /// An error is returned if an optical sensor is not currently connected to the Smart Port.
284    ///
285    /// # Examples
286    ///
287    /// ```
288    /// use vexide::prelude::*;
289    ///
290    /// #[vexide::main]
291    /// async fn main(peripherals: Peripherals) {
292    ///     let sensor = OpticalSensor::new(peripherals.port_1);
293    ///
294    ///     if let Ok(saturation) = sensor.saturation() {
295    ///         println!("Color saturation: {}%", saturation * 100.0);
296    ///
297    ///         // Check if color is muted or vibrant
298    ///         if saturation < 0.5 {
299    ///             println!("Muted color detected");
300    ///         } else {
301    ///             println!("Vibrant color detected");
302    ///         }
303    ///     }
304    /// }
305    /// ```
306    pub fn saturation(&self) -> Result<f64, PortError> {
307        self.validate_port()?;
308
309        Ok(unsafe { vexDeviceOpticalSatGet(self.device) })
310    }
311
312    /// Returns the detected color's brightness.
313    ///
314    /// Brightness values range from `0` to `1.0`.
315    ///
316    /// # Errors
317    ///
318    /// An error is returned if an optical sensor is not currently connected to the Smart Port.
319    ///
320    /// # Examples
321    ///
322    /// ```
323    /// use vexide::prelude::*;
324    ///
325    /// #[vexide::main]
326    /// async fn main(peripherals: Peripherals) {
327    ///     let sensor = OpticalSensor::new(peripherals.port_1);
328    ///
329    ///     if let Ok(brightness) = sensor.brightness() {
330    ///         println!("Color brightness: {}%", brightness * 100.0);
331    ///
332    ///         // Check if color is dark or bright
333    ///         if brightness < 0.3 {
334    ///             println!("Dark color detected");
335    ///         } else if brightness > 0.7 {
336    ///             println!("Bright color detected");
337    ///         } else {
338    ///             println!("Medium brightness color detected");
339    ///         }
340    ///     }
341    /// }
342    /// ```
343    pub fn brightness(&self) -> Result<f64, PortError> {
344        self.validate_port()?;
345
346        Ok(unsafe { vexDeviceOpticalBrightnessGet(self.device) })
347    }
348
349    /// Returns an analog proximity value from `0` to `1.0`.
350    ///
351    /// A reading of 1.0 indicates that the object is close to the sensor, while 0.0
352    /// indicates that no object is detected in range of the sensor.
353    ///
354    /// # Errors
355    ///
356    /// An error is returned if an optical sensor is not currently connected to the Smart Port.
357    ///
358    /// # Examples
359    ///
360    /// ```
361    /// use vexide::prelude::*;
362    ///
363    /// #[vexide::main]
364    /// async fn main(peripherals: Peripherals) {
365    ///     let sensor = OpticalSensor::new(peripherals.port_1);
366    ///
367    ///     // Monitor proximity with thresholds
368    ///     if let Ok(prox) = sensor.proximity() {
369    ///         match prox {
370    ///             x if x > 0.8 => println!("Object very close!"),
371    ///             x if x > 0.5 => println!("Object nearby"),
372    ///             x if x > 0.2 => println!("Object detected"),
373    ///             _ => println!("No object in range"),
374    ///         }
375    ///     }
376    /// }
377    /// ```
378    pub fn proximity(&self) -> Result<f64, PortError> {
379        self.validate_port()?;
380
381        Ok(f64::from(unsafe { vexDeviceOpticalProximityGet(self.device) }) / 255.0)
382    }
383
384    /// Returns the processed RGB color data from the sensor.
385    ///
386    /// # Errors
387    ///
388    /// An error is returned if an optical sensor is not currently connected to the Smart Port.
389    ///
390    /// # Examples
391    ///
392    /// ```
393    /// use vexide::prelude::*;
394    ///
395    /// #[vexide::main]
396    /// async fn main(peripherals: Peripherals) {
397    ///     let sensor = OpticalSensor::new(peripherals.port_1);
398    ///
399    ///     // Color detection with RGB values
400    ///     if let Ok(rgb) = sensor.color() {
401    ///         println!("Color reading: R={}, G={}, B={}", rgb.red, rgb.green, rgb.blue);
402    ///
403    ///         // Example: Check if object is primarily red
404    ///         // Note that you should probably use `OpticalSensor::hue` instead for this.
405    ///         if rgb.red > rgb.green && rgb.red > rgb.blue {
406    ///             println!("Object is primarily red!");
407    ///         }
408    ///     }
409    /// }
410    /// ```
411    pub fn color(&self) -> Result<OpticalRgb, PortError> {
412        self.validate_port()?;
413
414        let mut data = V5_DeviceOpticalRgb::default();
415        unsafe { vexDeviceOpticalRgbGet(self.device, &mut data) };
416
417        Ok(data.into())
418    }
419
420    /// Returns the raw, unprocessed RGBC color data from the sensor.
421    ///
422    /// # Errors
423    ///
424    /// An error is returned if an optical sensor is not currently connected to the Smart Port.
425    pub fn raw_color(&self) -> Result<OpticalRaw, PortError> {
426        self.validate_port()?;
427
428        let mut data = V5_DeviceOpticalRaw::default();
429        unsafe { vexDeviceOpticalRawGet(self.device, &mut data) };
430
431        Ok(data.into())
432    }
433
434    /// Returns the most recent gesture data from the sensor, or `None` if no gesture was detected.
435    ///
436    /// Gesture data updates every 500 milliseconds.
437    ///
438    /// # Errors
439    ///
440    /// An error is returned if an optical sensor is not currently connected to the Smart Port.
441    ///
442    /// # Examples
443    ///
444    /// ```
445    /// use vexide::prelude::*;
446    /// use core::time::Duration;
447    ///
448    /// #[vexide::main]
449    /// async fn main(peripherals: Peripherals) {
450    ///     let sensor = OpticalSensor::new(peripherals.port_1);
451    ///
452    ///     // Print the details of the last detected gesture.
453    ///     loop {
454    ///         if let Ok(Some(gesture)) = sensor.last_gesture() {
455    ///             println!("Direction: {:?}", gesture.direction);
456    ///         }
457    ///
458    ///         sleep(Duration::from_millis(25)).await;
459    ///     }
460    /// }
461    /// ```
462    pub fn last_gesture(&self) -> Result<Option<Gesture>, PortError> {
463        self.validate_port()?;
464
465        // Enable gesture detection if not already enabled.
466        //
467        // For some reason, PROS docs claim that this function makes color reading
468        // unavailable, but from hardware testing this is false.
469        unsafe { vexDeviceOpticalGestureEnable(self.device) };
470
471        let mut gesture = V5_DeviceOpticalGesture::default();
472        let direction = match unsafe { vexDeviceOpticalGestureGet(self.device, &mut gesture) } {
473            // see: https://github.com/purduesigbots/pros/blob/master/include/pros/optical.h#L37
474            1 => GestureDirection::Up,
475            2 => GestureDirection::Down,
476            3 => GestureDirection::Left,
477            4 => GestureDirection::Right,
478
479            // This is just a zero return usually if no gesture was detected.
480            _ => return Ok(None),
481        };
482
483        Ok(Some(Gesture {
484            direction,
485            up: gesture.udata,
486            down: gesture.ddata,
487            left: gesture.ldata,
488            right: gesture.rdata,
489            gesture_type: gesture.gesture_type,
490            count: gesture.count,
491            time: SmartDeviceTimestamp(gesture.time),
492        }))
493    }
494
495    /// Returns the internal status code of the optical sensor.
496    ///
497    /// # Errors
498    ///
499    /// An error is returned if an optical sensor is not currently connected to the Smart Port.
500    ///
501    /// # Examples
502    ///
503    /// ```
504    /// use vexide::prelude::*;
505    ///
506    /// #[vexide::main]
507    /// async fn main(peripherals: Peripherals) {
508    ///     let sensor = OpticalSensor::new(peripherals.port_1, Direction::Forward);
509    ///
510    ///     if let Ok(status) = sensor.status() {
511    ///         println!("Status: {:b}", status);
512    ///     }
513    /// }
514    /// ```
515    pub fn status(&self) -> Result<u32, PortError> {
516        self.validate_port()?;
517
518        Ok(unsafe { vexDeviceOpticalStatusGet(self.device) })
519    }
520}
521
522impl SmartDevice for OpticalSensor {
523    const UPDATE_INTERVAL: Duration = Duration::from_millis(20);
524
525    fn port_number(&self) -> u8 {
526        self.port.number()
527    }
528
529    fn device_type(&self) -> SmartDeviceType {
530        SmartDeviceType::Optical
531    }
532}
533impl From<OpticalSensor> for SmartPort {
534    fn from(device: OpticalSensor) -> Self {
535        device.port
536    }
537}
538
539/// Represents a gesture and its direction.
540#[derive(Debug, Clone, Copy, Eq, PartialEq)]
541pub enum GestureDirection {
542    /// Up gesture.
543    Up = 1,
544    /// Down gesture.
545    Down = 2,
546    /// Left gesture.
547    Left = 3,
548    /// Right gesture.
549    Right = 4,
550}
551
552/// Gesture data from an [`OpticalSensor`].
553#[derive(Debug, Clone, Copy, Eq, PartialEq)]
554pub struct Gesture {
555    /// Gesture Direction
556    pub direction: GestureDirection,
557    /// Up value.
558    pub up: u8,
559    /// Down value.
560    pub down: u8,
561    /// Left value.
562    pub left: u8,
563    /// Right value.
564    pub right: u8,
565    /// Gesture type.
566    pub gesture_type: u8,
567    /// The count of the gesture.
568    pub count: u16,
569    /// The time of the gesture.
570    pub time: SmartDeviceTimestamp,
571}
572
573/// RGB data from a [`OpticalSensor`].
574#[derive(Default, Debug, Clone, Copy, PartialEq)]
575pub struct OpticalRgb {
576    /// The red value from the sensor.
577    pub red: f64,
578    /// The green value from the sensor.
579    pub green: f64,
580    /// The blue value from the sensor.
581    pub blue: f64,
582    /// The brightness value from the sensor.
583    pub brightness: f64,
584}
585
586impl From<V5_DeviceOpticalRgb> for OpticalRgb {
587    fn from(value: V5_DeviceOpticalRgb) -> Self {
588        Self {
589            red: value.red,
590            green: value.green,
591            blue: value.blue,
592            brightness: value.brightness,
593        }
594    }
595}
596
597/// Represents the raw RGBC data from the sensor.
598#[derive(Default, Debug, Clone, Copy, PartialEq, Eq)]
599pub struct OpticalRaw {
600    /// The red value from the sensor.
601    pub red: u16,
602    /// The green value from the sensor.
603    pub green: u16,
604    /// The blue value from the sensor.
605    pub blue: u16,
606    /// The clear value from the sensor.
607    pub clear: u16,
608}
609
610impl From<V5_DeviceOpticalRaw> for OpticalRaw {
611    fn from(value: V5_DeviceOpticalRaw) -> Self {
612        Self {
613            red: value.red,
614            green: value.green,
615            blue: value.blue,
616            clear: value.clear,
617        }
618    }
619}