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}