embedded_touch/lib.rs
1//! Common traits and types for touch screen drivers (and mice)
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 interface for touch devices.
11pub trait TouchInputDevice {
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 interface for event-driven operation of touch devices
23pub trait AsyncTouchInputDevice {
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 /// Touch is hovering above the screen without contact
73 Hovering,
74}
75
76/// Tool/instrument used for touch interaction
77#[derive(Debug, Clone, Copy, PartialEq)]
78pub enum Tool {
79 /// Finger or unknown tool
80 Finger,
81 /// Virtual pointing device (e.g., mouse cursor)
82 Pointer {
83 /// The button pressed on the virtual pointer
84 button: PointerButton,
85 },
86 /// Passive or active stylus
87 Stylus {
88 /// Pressure, in grams
89 pressure: Option<u16>,
90 /// Tilt angle
91 ///
92 /// 0 degrees is a vector normal to the screen, and 90 degrees is parallel to the screen.
93 tilt: Option<UnitAngle>,
94 /// Azimuth angle
95 ///
96 /// 0 degrees points up to the top of the screen in its default orientation.
97 azimuth: Option<UnitAngle>,
98 },
99}
100
101/// The button state of a virtual pointer device
102#[derive(Debug, Clone, Copy, PartialEq, Eq)]
103pub enum PointerButton {
104 /// No button pressed, e.g., mouse hover state
105 None,
106 /// Primary mouse button, typically left
107 Primary,
108 /// Secondary mouse button, typically right
109 Secondary,
110 /// Tertiary mouse button, typically middle or wheel
111 Tertiary,
112}
113
114/// Error types for touch operations
115#[derive(Debug, Clone, Copy, PartialEq)]
116pub enum Error<E> {
117 /// I2C/SPI communication error
118 Interface(E),
119 /// Data corruption detected (e.g., checksum failure)
120 DataCorruption,
121 /// Device not responding or not initialized
122 DeviceError,
123}
124
125impl<E> From<E> for Error<E> {
126 fn from(error: E) -> Self {
127 Error::Interface(error)
128 }
129}
130
131/// An angle in the range [0, 2π) radians
132///
133/// The angle is stored as a [`fixed::types::U1F15`]
134#[derive(Debug, Clone, Copy, PartialEq, Eq)]
135pub struct UnitAngle(fixed::types::U1F15);
136
137impl UnitAngle {
138 /// Create a new angle from an angle in π * radians.
139 ///
140 /// Angles outside the range [0, 2) are wrapped.
141 ///
142 /// This method does not result in loss of precision if the argument is
143 /// [`fixed::types::U1F15`]
144 #[must_use]
145 pub fn from_pi_radians(value: impl ToFixed) -> Self {
146 UnitAngle(value.wrapping_to_fixed())
147 }
148
149 /// Create a new angle from an angle in radians
150 ///
151 /// Angles outside the range [0, 2π) are wrapped.
152 #[must_use]
153 pub fn from_radians(value: impl ToFixed) -> Self {
154 let fixed_radians = value.to_fixed::<fixed::types::U17F15>();
155 let pi_radians = fixed_radians / U17F15!(3.14159265359);
156 UnitAngle(pi_radians.wrapping_to_fixed())
157 }
158
159 /// Create a new angle from an angle in degrees
160 ///
161 /// Angles outside the range [0, 360) are wrapped.
162 #[must_use]
163 pub fn from_degrees(value: impl ToFixed) -> Self {
164 let fixed_degrees = value.to_fixed::<fixed::types::I17F15>();
165 let radians = fixed_degrees / I17F15!(180);
166 UnitAngle(radians.wrapping_to_fixed())
167 }
168
169 /// Returns the angle in π radians, in the range [0, 2)
170 ///
171 /// This method does not result in loss of precision from the original value.
172 #[must_use]
173 pub fn as_pi_radians(&self) -> fixed::types::U1F15 {
174 self.0
175 }
176
177 #[must_use]
178 pub fn as_radians_f32(&self) -> f32 {
179 (self.0.to_fixed::<U17F15>() * U17F15!(3.14159265359)).to_num::<f32>()
180 }
181
182 #[must_use]
183 pub fn as_degrees_f32(&self) -> f32 {
184 (self.0.to_fixed::<U17F15>() * U17F15!(180.0)).to_num::<f32>()
185 }
186}
187
188#[cfg(test)]
189mod tests {
190 use core::f32;
191
192 use super::*;
193
194 #[test]
195 #[expect(clippy::cast_precision_loss)]
196 fn angle_from_pi_radians() {
197 let angle = UnitAngle::from_pi_radians(0.0);
198 assert_eq!(angle.as_pi_radians(), fixed::types::U1F15::from_num(0.0));
199 assert!(angle.as_radians_f32().abs() < 0.00001);
200 assert!(angle.as_degrees_f32().abs() < 0.00001);
201
202 for i in -8..8 {
203 let offset = (i * 2) as f32;
204 let angle = UnitAngle::from_pi_radians(1.0 + offset);
205 assert_eq!(angle.as_pi_radians(), fixed::types::U1F15::from_num(1.0));
206 assert!((angle.as_radians_f32() - 1.0 * f32::consts::PI).abs() < 0.00001);
207 assert!((angle.as_degrees_f32() - 180.0).abs() < 0.00001);
208 }
209 }
210
211 #[test]
212 #[expect(clippy::cast_precision_loss)]
213 fn sweep_360_degrees() {
214 for i in -1080..1080 {
215 let angle = UnitAngle::from_degrees(i);
216
217 let unit_degrees = (i + 360 * 20) % 360;
218 let radians = unit_degrees as f32 * f32::consts::PI / 180.0;
219
220 assert!(
221 (angle.as_degrees_f32() - unit_degrees as f32).abs() < 0.01,
222 "Expected {} to be nearly {unit_degrees}",
223 angle.as_degrees_f32()
224 );
225 assert!(
226 (angle.as_radians_f32() - radians).abs() < 0.001,
227 "Expected {} to be nearly {radians}",
228 angle.as_radians_f32()
229 );
230 }
231 }
232}