embedded_touch/
lib.rs

1//! Common traits and types for touch screen drivers
2
3#![no_std]
4
5use core::fmt::Debug;
6
7use fixed::{traits::ToFixed, types::U17F15};
8use fixed_macro::types::{I17F15, U17F15};
9
10/// Blocking touch interface for touch screens
11pub trait TouchScreen {
12    /// Error type from the underlying interface
13    type Error;
14
15    /// Read current touch points, blocking until data is available
16    ///
17    /// Returns an iterator of touch points currently detected.
18    /// Drivers must track touch IDs across calls to maintain correct phase information.
19    fn touches(&mut self) -> Result<impl IntoIterator<Item = Touch>, Error<Self::Error>>;
20}
21
22/// Async touch screen interface for event-driven operation
23pub trait AsyncTouchScreen {
24    /// Error type from the underlying interface
25    type Error;
26
27    /// Asynchronously wait until touch points are available
28    ///
29    /// Returns an iterator of touch points currently detected.
30    /// Drivers must track touch IDs across calls to maintain correct phase information.
31    fn touches(
32        &mut self,
33    ) -> impl Future<Output = Result<impl IntoIterator<Item = Touch>, Error<Self::Error>>>;
34}
35
36/// Represents a single touch point on the screen
37#[derive(Debug, Clone, PartialEq)]
38pub struct Touch {
39    /// Unique ID for tracking this touch point across frames
40    ///
41    /// The ID should remain stable for a given finger/stylus while it remains in contact
42    /// but can be reused for new touches after sending [`Phase::Ended`] or [`Phase::Cancelled`]
43    pub id: u8,
44
45    /// X coordinate in screen pixels
46    pub x: u16,
47
48    /// Y coordinate in screen pixels
49    pub y: u16,
50
51    /// Current phase of this touch interaction
52    pub phase: Phase,
53
54    /// The tool used for this touch point
55    pub tool: Tool,
56
57    /// Optional proximity distance (implementation-specific units)
58    pub proximity: Option<u16>,
59}
60
61/// Phase of a touch interaction
62#[derive(Debug, Clone, Copy, PartialEq, Eq)]
63pub enum Phase {
64    /// Touch just started
65    Started,
66    /// Touch moved from previous position
67    Moved,
68    /// Touch ended normally
69    Ended,
70    /// Touch was cancelled (e.g., palm rejection triggered)
71    Cancelled,
72}
73
74/// Tool/instrument used for touch interaction
75#[derive(Debug, Clone, Copy, PartialEq)]
76pub enum Tool {
77    /// Finger or unknown tool
78    Finger,
79    /// Passive or active stylus
80    Stylus {
81        /// Pressure, in grams
82        pressure: Option<u16>,
83        /// Tilt angle
84        ///
85        /// 0 degrees is a vector normal to the screen, and 90 degrees is parallel to the screen.
86        tilt: Option<UnitAngle>,
87        /// Azimuth angle
88        ///
89        /// 0 degrees points up to the top of the screen in its default orientation.
90        azimuth: Option<UnitAngle>,
91    },
92}
93
94/// Error types for touch operations
95#[derive(Debug, Clone, Copy, PartialEq)]
96pub enum Error<E> {
97    /// I2C/SPI communication error
98    Interface(E),
99    /// Data corruption detected (e.g., checksum failure)
100    DataCorruption,
101    /// Device not responding or not initialized
102    DeviceError,
103}
104
105impl<E> From<E> for Error<E> {
106    fn from(error: E) -> Self {
107        Error::Interface(error)
108    }
109}
110
111/// An angle in the range [0, 2π) radians
112///
113/// The angle is stored as a [`fixed::types::U1F15`]
114#[derive(Debug, Clone, Copy, PartialEq, Eq)]
115pub struct UnitAngle(fixed::types::U1F15);
116
117impl UnitAngle {
118    /// Create a new angle from an angle in π * radians.
119    ///
120    /// Angles outside the range [0, 2) are wrapped.
121    ///
122    /// This method does not result in loss of precision if the argument is
123    /// [`fixed::types::U1F15`]
124    #[must_use]
125    pub fn from_pi_radians(value: impl ToFixed) -> Self {
126        UnitAngle(value.wrapping_to_fixed())
127    }
128
129    /// Create a new angle from an angle in radians
130    ///
131    /// Angles outside the range [0, 2π) are wrapped.
132    #[must_use]
133    pub fn from_radians(value: impl ToFixed) -> Self {
134        let fixed_radians = value.to_fixed::<fixed::types::U17F15>();
135        let pi_radians = fixed_radians / U17F15!(3.14159265359);
136        UnitAngle(pi_radians.wrapping_to_fixed())
137    }
138
139    /// Create a new angle from an angle in degrees
140    ///
141    /// Angles outside the range [0, 360) are wrapped.
142    #[must_use]
143    pub fn from_degrees(value: impl ToFixed) -> Self {
144        let fixed_degrees = value.to_fixed::<fixed::types::I17F15>();
145        let radians = fixed_degrees / I17F15!(180);
146        UnitAngle(radians.wrapping_to_fixed())
147    }
148
149    /// Returns the angle in π radians, in the range [0, 2)
150    ///
151    /// This method does not result in loss of precision from the original value.
152    #[must_use]
153    pub fn as_pi_radians(&self) -> fixed::types::U1F15 {
154        self.0
155    }
156
157    #[must_use]
158    pub fn as_radians_f32(&self) -> f32 {
159        (self.0.to_fixed::<U17F15>() * U17F15!(3.14159265359)).to_num::<f32>()
160    }
161
162    #[must_use]
163    pub fn as_degrees_f32(&self) -> f32 {
164        (self.0.to_fixed::<U17F15>() * U17F15!(180.0)).to_num::<f32>()
165    }
166}
167
168#[cfg(test)]
169mod tests {
170    use core::f32;
171
172    use super::*;
173
174    #[test]
175    #[expect(clippy::cast_precision_loss)]
176    fn angle_from_pi_radians() {
177        let angle = UnitAngle::from_pi_radians(0.0);
178        assert_eq!(angle.as_pi_radians(), fixed::types::U1F15::from_num(0.0));
179        assert!(angle.as_radians_f32().abs() < 0.00001);
180        assert!(angle.as_degrees_f32().abs() < 0.00001);
181
182        for i in -8..8 {
183            let offset = (i * 2) as f32;
184            let angle = UnitAngle::from_pi_radians(1.0 + offset);
185            assert_eq!(angle.as_pi_radians(), fixed::types::U1F15::from_num(1.0));
186            assert!((angle.as_radians_f32() - 1.0 * f32::consts::PI).abs() < 0.00001);
187            assert!((angle.as_degrees_f32() - 180.0).abs() < 0.00001);
188        }
189    }
190
191    #[test]
192    #[expect(clippy::cast_precision_loss)]
193    fn sweep_360_degrees() {
194        for i in -1080..1080 {
195            let angle = UnitAngle::from_degrees(i);
196
197            let unit_degrees = (i + 360 * 20) % 360;
198            let radians = unit_degrees as f32 * f32::consts::PI / 180.0;
199
200            assert!(
201                (angle.as_degrees_f32() - unit_degrees as f32).abs() < 0.01,
202                "Expected {} to be nearly {unit_degrees}",
203                angle.as_degrees_f32()
204            );
205            assert!(
206                (angle.as_radians_f32() - radians).abs() < 0.001,
207                "Expected {}  to be nearly {radians}",
208                angle.as_radians_f32()
209            );
210        }
211    }
212}