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}