vexide_devices/smart/
rotation.rs

1//! Rotation Sensor
2//!
3//! This module provides an interface to interact with the VEX V5 Rotation Sensor,
4//! which measures the absolute position, rotation count, and angular velocity of a
5//! rotating shaft.
6//!
7//! # Hardware Overview
8//!
9//! The sensor provides absolute rotational position tracking from 0° to 360° with 0.088° accuracy. The
10//! sensor is compromised of two magnets which utilize the [Hall Effect] to indicate angular position. A
11//! chip inside the rotation sensor (a Cortex M0+) then keeps track of the total rotations of the sensor
12//! to determine total position travelled.
13//!
14//! Position is reported by VEXos in centidegrees before being converted to an instance of [`Position`].
15//!
16//! The absolute angle reading is preserved across power cycles (similar to a potentiometer), while the
17//! position count stores the cumulative forward and reverse revolutions relative to program start, however
18//! the *position* reading will be reset if the sensor loses power. Angular velocity is measured in degrees
19//! per second.
20//!
21//! Like all other Smart devices, VEXos will process sensor updates every 10mS.
22//!
23//! [Hall Effect]: https://en.wikipedia.org/wiki/Hall_effect_sensor
24
25use core::time::Duration;
26
27use vex_sdk::{
28    vexDeviceAbsEncAngleGet, vexDeviceAbsEncDataRateSet, vexDeviceAbsEncPositionGet,
29    vexDeviceAbsEncPositionSet, vexDeviceAbsEncStatusGet, vexDeviceAbsEncVelocityGet, V5_DeviceT,
30};
31
32use super::{motor::Direction, SmartDevice, SmartDeviceType, SmartPort};
33use crate::{position::Position, PortError};
34
35/// A rotation sensor plugged into a Smart Port.
36#[derive(Debug, PartialEq)]
37pub struct RotationSensor {
38    /// Smart Port
39    port: SmartPort,
40
41    /// Handle to the internal SDK device instance.
42    device: V5_DeviceT,
43
44    /// Current direction state of the sensor.
45    direction: Direction,
46
47    /// The position data recorded by [`Self::position`] at the time the sensor is reversed.
48    direction_offset: Position,
49
50    /// The raw position data recorded by the SDK at the time the sensor is reversed.
51    raw_direction_offset: Position,
52}
53
54// SAFETY: Required because we store a raw pointer to the device handle to avoid it getting from the
55// SDK each device function. Simply sharing a raw pointer across threads is not inherently unsafe.
56unsafe impl Send for RotationSensor {}
57unsafe impl Sync for RotationSensor {}
58
59impl RotationSensor {
60    /// The minimum data rate that you can set a rotation sensor to.
61    pub const MIN_DATA_INTERVAL: Duration = Duration::from_millis(5);
62
63    /// The amount of unique sensor readings per one revolution of the sensor.
64    pub const TICKS_PER_REVOLUTION: u32 = 36000;
65
66    /// Creates a new rotation sensor on the given port.
67    ///
68    /// Whether or not the sensor should be reversed on creation can be specified.
69    ///
70    /// # Examples
71    ///
72    /// ```
73    /// use vexide::prelude::*;
74    ///
75    /// #[vexide::main]
76    /// async fn main(peripherals: Peripherals) {
77    ///     let sensor = RotationSensor::new(peripherals.port_1, Direction::Forward);
78    /// }
79    /// ```
80    #[must_use]
81    pub fn new(port: SmartPort, direction: Direction) -> Self {
82        let device = unsafe { port.device_handle() };
83
84        Self {
85            device,
86            port,
87            direction,
88            direction_offset: Position::default(),
89            raw_direction_offset: Position::default(),
90        }
91    }
92
93    /// Reset's the sensor's position reading to zero.
94    ///
95    /// # Errors
96    ///
97    /// An error is returned if a rotation sensor is not currently connected to the Smart Port.
98    ///
99    /// # Examples
100    ///
101    /// ```
102    /// use vexide::prelude::*;
103    ///
104    /// #[vexide::main]
105    /// async fn main(peripherals: Peripherals) {
106    ///     let mut sensor = RotationSensor::new(peripherals.port_1, Direction::Forward);
107    ///
108    ///     println!("Before reset: {:?}", sensor.position());
109    ///
110    ///     _ = sensor.reset_position();
111    ///
112    ///     println!("After reset: {:?}", sensor.position());
113    /// }
114    /// ```
115    pub fn reset_position(&mut self) -> Result<(), PortError> {
116        // NOTE: We don't use vexDeviceAbsEncReset, since that doesn't actually
117        // zero position. It sets position to whatever the angle value is.
118        self.set_position(Position::default())
119    }
120
121    /// Sets the sensor's position reading.
122    ///
123    /// # Errors
124    ///
125    /// An error is returned if a rotation sensor is not currently connected to the Smart Port.
126    ///
127    /// # Examples
128    ///
129    /// ```
130    /// use vexide::prelude::*;
131    ///
132    /// #[vexide::main]
133    /// async fn main(peripherals: Peripherals) {
134    ///     let mut sensor = RotationSensor::new(peripherals.port_1, Direction::Forward);
135    ///
136    ///     // Set position to 15 degrees.
137    ///     _ = sensor.set_position(Position::from_degrees(15.0));
138    /// }
139    /// ```
140    pub fn set_position(&mut self, mut position: Position) -> Result<(), PortError> {
141        self.validate_port()?;
142
143        if self.direction == Direction::Reverse {
144            position = -position;
145        }
146
147        unsafe {
148            self.direction_offset = Position::default();
149            self.raw_direction_offset = Position::default();
150
151            vexDeviceAbsEncPositionSet(self.device, position.as_ticks(36000) as i32);
152        }
153
154        Ok(())
155    }
156
157    /// Sets the sensor to operate in a given [`Direction`].
158    ///
159    /// This determines which way the sensor considers to be “forwards”. You can use the marking on the top of the
160    /// motor as a reference:
161    ///
162    /// - When [`Direction::Forward`] is specified, positive velocity/voltage values will cause the motor to rotate
163    ///   **with the arrow on the top**. Position will increase as the motor rotates **with the arrow**.
164    /// - When [`Direction::Reverse`] is specified, positive velocity/voltage values will cause the motor to rotate
165    ///   **against the arrow on the top**. Position will increase as the motor rotates **against the arrow**.
166    ///
167    /// # Errors
168    ///
169    /// - An error is returned if an rotation sensor is not currently connected to the Smart Port.
170    ///
171    /// # Examples
172    ///
173    /// Set the sensor's direction to [`Direction::Reverse`].
174    ///
175    /// ```
176    /// use vexide::prelude::*;
177    ///
178    /// #[vexide::main]
179    /// async fn main(peripherals: Peripherals) {
180    ///     let mut sensor = RotationSensor::new(peripherals.port_1, Direction::Forward);
181    ///
182    ///     // Reverse the sensor
183    ///     _ = sensor.set_direction(Direction::Reverse);
184    /// }
185    /// ```
186    ///
187    /// Reverse the sensor's direction (set to opposite of the previous direction):
188    ///
189    /// ```
190    /// use vexide::prelude::*;
191    ///
192    /// #[vexide::main]
193    /// async fn main(peripherals: Peripherals) {
194    ///     let mut sensor = RotationSensor::new(peripherals.port_1, Direction::Forward);
195    ///
196    ///     // Reverse the sensor
197    ///     _ = sensor.set_direction(!sensor.direction());
198    /// }
199    /// ```
200    pub fn set_direction(&mut self, new_direction: Direction) -> Result<(), PortError> {
201        // You're probably wondering why I don't use [`vexDeviceAbsEncReverseFlagSet`] here. So about that...
202        //
203        // This sensor is a little unique in that it stores two separate values - "position" and "angle". Angle is the literal
204        // angle of rotation of the sensor from 0-36000 centidegrees. Position is how *many* centidegrees the sensor was rotated
205        // by. Position is completely unbounded. Both of these values are treated separately in the SDK, and have different
206        // behaviors when dealing with the reverse flag. When the sensor is reversed, angle is transformed to become 36000 - angle
207        // (converted clockwise -> counterclockwise essentially), while position actually doesn't change at all.
208        //
209        // Rather than simply negating position when reversed, the SDK keeps the current position before reversing and just
210        // reverses the direction of future measurements. So if I were to rotate the sensor by 90 degrees, reverse the
211        // direction, then rotate it another 90 degrees it would now be at a net 0 degree rotation value.
212        //
213        // Now, here's where this all falls apart. There's a known race condition in [`vexDeviceAbsEncReverseFlagSet`], where
214        // if the reverse flag is set before the device reports its first *position* value, the starting position will be at
215        // 36000 rather than 0. This is because the SDK has code for ensuring that "angle" and "position" are set the same. If
216        // we set the reverse flag before position has been initially set, then rather than starting with a position of 0, we
217        // start with a position of 36000 (the default angle after being reversed). So rather than dealing with polling and
218        // status codes and potentially blocking the current thread until this device is initialized, I just recreated this
219        // behavior on our end without ever touching the status code.
220        //
221        // For more information: <https://www.vexforum.com/t/rotation-sensor-bug-workaround-on-vexos-1-1-0/96577/2>
222        if new_direction != self.direction() {
223            self.direction_offset = self.position()?;
224            self.raw_direction_offset = Position::from_ticks(
225                i64::from(unsafe { vexDeviceAbsEncPositionGet(self.device) }),
226                Self::TICKS_PER_REVOLUTION,
227            );
228            self.direction = new_direction;
229        }
230
231        Ok(())
232    }
233
234    /// Sets the internal computation speed of the rotation sensor.
235    ///
236    /// This method does NOT change the rate at which user code can read data off the sensor, as the brain will only talk to
237    /// the device every 10mS regardless of how fast data is being sent or computed. See [`RotationSensor::UPDATE_INTERVAL`].
238    ///
239    /// This duration should be above [`Self::MIN_DATA_INTERVAL`] (5 milliseconds).
240    ///
241    /// # Errors
242    ///
243    /// An error is returned if an rotation sensor is not currently connected to the Smart Port.
244    ///
245    /// # Examples
246    ///
247    /// ```
248    /// use vexide::prelude::*;
249    ///
250    /// #[vexide::main]
251    /// async fn main(peripherals: Peripherals) {
252    ///     let mut sensor = RotationSensor::new(peripherals.port_1, Direction::Forward);
253    ///
254    ///     // Set to minimum interval.
255    ///     _ = sensor.set_data_interval(RotationSensor::MIN_DATA_INTERVAL);
256    /// }
257    /// ```
258    pub fn set_computation_interval(&mut self, interval: Duration) -> Result<(), PortError> {
259        self.validate_port()?;
260
261        let mut time_ms = interval
262            .as_millis()
263            .max(Self::MIN_DATA_INTERVAL.as_millis()) as u32;
264        time_ms -= time_ms % 5; // Rate is in increments of 5ms - not sure if this is necessary, but PROS does it.
265
266        unsafe { vexDeviceAbsEncDataRateSet(self.device, time_ms) }
267
268        Ok(())
269    }
270
271    /// Returns the [`Direction`] of this sensor.
272    ///
273    /// # Examples
274    ///
275    /// ```
276    /// use vexide::prelude::*;
277    ///
278    /// #[vexide::main]
279    /// async fn main(peripherals: Peripherals) {
280    ///     let sensor = RotationSensor::new(peripherals.port_1, Direction::Forward);
281    ///
282    ///     println!(
283    ///         "Sensor's direction is {}",
284    ///         match sensor.direction() {
285    ///             Direction::Forward => "forward",
286    ///             Direction::Reverse => "reverse",
287    ///         }
288    ///     );
289    /// }
290    /// ```
291    #[must_use]
292    pub const fn direction(&self) -> Direction {
293        self.direction
294    }
295
296    /// Returns the total number of degrees rotated by the sensor based on direction.
297    ///
298    /// # Errors
299    ///
300    /// An error is returned if an rotation sensor is not currently connected to the Smart Port.
301    ///
302    /// # Examples
303    ///
304    /// ```
305    /// use vexide::prelude::*;
306    ///
307    /// #[vexide::main]
308    /// async fn main(peripherals: Peripherals) {
309    ///     let sensor = RotationSensor::new(peripherals.port_1, Direction::Forward);
310    ///
311    ///     if let Ok(position) = sensor.position() {
312    ///         println!("Position in degrees: {}°", position.as_degrees());
313    ///         println!("Position in radians: {}°", position.as_radians());
314    ///         println!("Position in raw ticks (centidegrees): {}°", position.as_ticks(RotationSensor::TICKS_PER_REVOLUTION));
315    ///         println!("Number of revolutions spun: {}°", position.as_revolutions());
316    ///     }
317    /// }
318    /// ```
319    pub fn position(&self) -> Result<Position, PortError> {
320        self.validate_port()?;
321
322        let mut delta_position = Position::from_ticks(
323            i64::from(unsafe { vexDeviceAbsEncPositionGet(self.device) }),
324            Self::TICKS_PER_REVOLUTION,
325        ) - self.raw_direction_offset;
326
327        if self.direction == Direction::Reverse {
328            delta_position = -delta_position;
329        }
330
331        Ok(self.direction_offset + delta_position)
332    }
333
334    /// Returns the angle of rotation measured by the sensor.
335    ///
336    /// This value is reported from 0-360 degrees.
337    ///
338    /// # Errors
339    ///
340    /// An error is returned if an rotation sensor is not currently connected to the Smart Port.
341    ///
342    /// # Examples
343    ///
344    /// ```
345    /// use vexide::prelude::*;
346    ///
347    /// #[vexide::main]
348    /// async fn main(peripherals: Peripherals) {
349    ///     let sensor = RotationSensor::new(peripherals.port_1, Direction::Forward);
350    ///
351    ///     if let Ok(angle) = sensor.angle() {
352    ///         println!("Angle in degrees: {}°", angle.as_degrees());
353    ///         println!("Angle in radians: {}°", angle.as_radians());
354    ///         println!("Angle in raw ticks (centidegrees): {}°", angle.as_ticks(RotationSensor::TICKS_PER_REVOLUTION));
355    ///     }
356    /// }
357    /// ```
358    pub fn angle(&self) -> Result<Position, PortError> {
359        self.validate_port()?;
360
361        let mut raw_angle = unsafe { vexDeviceAbsEncAngleGet(self.device) };
362
363        if self.direction == Direction::Reverse {
364            raw_angle = (Self::TICKS_PER_REVOLUTION as i32) - raw_angle;
365        }
366
367        Ok(Position::from_ticks(
368            i64::from(raw_angle),
369            Self::TICKS_PER_REVOLUTION,
370        ))
371    }
372
373    /// Returns the sensor's current velocity in degrees per second.
374    ///
375    /// # Errors
376    ///
377    /// An error is returned if an rotation sensor is not currently connected to the Smart Port.
378    ///
379    /// ```
380    /// use vexide::prelude::*;
381    ///
382    /// #[vexide::main]
383    /// async fn main(peripherals: Peripherals) {
384    ///     let sensor = RotationSensor::new(peripherals.port_1, Direction::Forward);
385    ///
386    ///     if let Some(velocity) = sensor.velocity() {
387    ///         println!(
388    ///             "Velocity in RPM {}",
389    ///             velocity / 6.0, // 1rpm = 6dps
390    ///         );
391    ///     }
392    /// }
393    /// ```
394    pub fn velocity(&self) -> Result<f64, PortError> {
395        self.validate_port()?;
396
397        let mut raw_velocity = unsafe { vexDeviceAbsEncVelocityGet(self.device) };
398
399        if self.direction == Direction::Reverse {
400            raw_velocity *= -1;
401        }
402
403        Ok(f64::from(raw_velocity) / 100.0)
404    }
405
406    /// Returns the sensor's internal status code.
407    ///
408    /// # Errors
409    ///
410    /// An error is returned if an rotation sensor is not currently connected to the Smart Port.
411    ///
412    /// # Examples
413    ///
414    /// ```
415    /// use vexide::prelude::*;
416    ///
417    /// #[vexide::main]
418    /// async fn main(peripherals: Peripherals) {
419    ///     let sensor = RotationSensor::new(peripherals.port_1, Direction::Forward);
420    ///
421    ///     if let Ok(status) = sensor.status() {
422    ///         println!("Status: {:b}", status);
423    ///     }
424    /// }
425    /// ```
426    pub fn status(&self) -> Result<u32, PortError> {
427        self.validate_port()?;
428
429        Ok(unsafe { vexDeviceAbsEncStatusGet(self.device) })
430    }
431}
432
433impl SmartDevice for RotationSensor {
434    fn port_number(&self) -> u8 {
435        self.port.number()
436    }
437
438    fn device_type(&self) -> SmartDeviceType {
439        SmartDeviceType::Rotation
440    }
441}
442impl From<RotationSensor> for SmartPort {
443    fn from(device: RotationSensor) -> Self {
444        device.port
445    }
446}