Skip to main content

frclib_core/hal/gpio/
mod.rs

1//! A module for the HAL GPIO driver.
2
3pub mod analog;
4pub mod digital;
5
6use std::fmt::Display;
7
8#[allow(clippy::upper_case_acronyms, missing_docs)]
9#[derive(Debug, Clone, Copy, PartialEq, Eq)]
10pub enum GPIOPortType {
11    Analog,
12    Digital,
13    PWM,
14}
15impl Display for GPIOPortType {
16    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
17        match self {
18            Self::Analog => write!(f, "Analog Port"),
19            Self::Digital => write!(f, "Digital Port"),
20            Self::PWM => write!(f, "PWM Port"),
21        }
22    }
23}
24
25#[allow(variant_size_differences, missing_docs)]
26#[non_exhaustive]
27#[derive(Debug, thiserror::Error, Clone, Copy, PartialEq)]
28pub enum GPIOError {
29    #[error("GPIO port {0} is not available")]
30    PortNotAvailable(u8),
31    #[error("Value {0} is out of range for {1} port {2}")]
32    ValueOutOfRange(Volt, GPIOPortType, u8),
33    #[error("Wrong mode for {0} port {1}, expected output to be {2}")]
34    WrongMode(GPIOPortType, u8, bool),
35    #[error("GPIO port {0} is already in use")]
36    PortInUse(u8),
37}
38
39/// A trait for a GPIO channel.
40pub trait Channel {
41    /// The id the channel is opened on
42    fn channel_id(&self) -> u8;
43}
44
45use analog::{AnalogInput, AnalogOutput};
46use digital::{DigitalInput, DigitalOutput};
47
48use crate::units::energy::Volt;
49
50use super::NotSimError;
51
52/// A platform specific GPIO driver.
53///
54/// # Ports
55/// ## Identifying Ports
56/// Ports are identified by a `u8` value. The meaning of this value is up to driver developer but
57/// it is recommended to use the following convention of the ``NI RoboRio`` of starting at 0 and
58/// incrementing by 1 for each port. This is not enforced by the HAL.
59///
60/// ## Port Types
61/// There are three types of ports: analog, digital, and PWM.
62/// If the platform allows it one port can be multiple types.
63/// This means that id 0 could mean a different physical pin/port depending on the type
64/// or that a port could be both analog and digital and setting the analog value would overwrite the digital value.
65///
66/// ### Analog
67/// Analog ports are ports that can read a voltage value from 0 to 5 volts.
68/// If the platform supports a different range of voltages it is recommended to scale the value.
69///
70/// ### Digital
71/// Digital ports are ports that can read a boolean value.
72///
73/// ### PWM
74/// PWM ports are ports that can write a value from 0 to 1.
75///
76/// # Safety
77/// ## Thread Safety
78/// It's up to the driver developer to ensure that the driver is thread safe.
79///
80/// # Development Resources
81/// - [WPILib Rio DIO](https://github.com/wpilibsuite/allwpilib/blob/main/hal/src/main/native/athena/DIO.cpp)
82/// - [WPILib Sim DIO](https://github.com/wpilibsuite/allwpilib/blob/main/hal/src/main/native/sim/DIO.cpp)
83/// - [WPILib Rio AnalogIn](https://github.com/wpilibsuite/allwpilib/blob/main/hal/src/main/native/athena/AnalogInput.cpp)
84/// - [WPILib Sim AnalogIn](https://github.com/wpilibsuite/allwpilib/blob/main/hal/src/main/native/sim/AnalogInput.cpp)
85/// - [WPILib Rio AnalogOut](https://github.com/wpilibsuite/allwpilib/blob/main/hal/src/main/native/athena/AnalogOutput.cpp)
86/// - [WPILib Sim AnalogOut](https://github.com/wpilibsuite/allwpilib/blob/main/hal/src/main/native/sim/AnalogOutput.cpp)
87/// - [Raspberry Pi GPIO](https://github.com/golemparts/rppal/tree/master)
88/// - [Jetson GPIO](https://github.com/Kajatin/jetson-gpio-rust/tree/main)
89pub trait GPIODriver {
90    /// A list of all analog ports available on the platform.
91    const ANALOG_PORTS: &'static [u8];
92    /// A list of all digital ports available on the platform.
93    const DIGITAL_PORTS: &'static [u8];
94    /// A list of all PWM ports available on the platform.
95    const PWM_PORTS: &'static [u8];
96    /// A list of all relay ports available on the platform.
97    const RELAY_PORTS: &'static [u8];
98
99    //
100    // # Channels
101    //
102
103    /// Returns a new analog input for the specified port.
104    ///
105    /// # Errors
106    /// - [`GPIOError::PortNotAvailable`] if the port is not available for analog in use
107    /// - [`GPIOError::PortInUse`] if the port is already in use
108    fn new_analog_input(port: u8) -> Result<AnalogInput, GPIOError>;
109
110    /// Returns a new analog output for the specified port.
111    ///
112    /// # Errors
113    /// - [`GPIOError::PortNotAvailable`] if the port is not available for analog out use
114    /// - [`GPIOError::PortInUse`] if the port is already in use
115    fn new_analog_output(port: u8) -> Result<AnalogOutput, GPIOError>;
116
117    /// Returns a new digital input for the specified port.
118    ///
119    /// # Errors
120    /// - [`GPIOError::PortNotAvailable`] if the port is not available for digital in use
121    /// - [`GPIOError::PortInUse`] if the port is already in use
122    fn new_digital_input(port: u8) -> Result<DigitalInput, GPIOError>;
123
124    /// Returns a new digital output for the specified port.
125    ///
126    /// # Errors
127    /// - [`GPIOError::PortNotAvailable`] if the port is not available for digital out use
128    /// - [`GPIOError::PortInUse`] if the port is already in use
129    fn new_digital_output(port: u8) -> Result<DigitalOutput, GPIOError>;
130
131    //
132    // # Checks
133    //
134
135    /// Checks if the specified port is available for analog use.
136    /// Has a sensible default implementation but can be overriden if needed.
137    #[allow(clippy::missing_errors_doc)]
138    fn analog_available(port: u8) -> Result<(), GPIOError> {
139        if Self::ANALOG_PORTS.contains(&port) {
140            Ok(())
141        } else {
142            Err(GPIOError::PortNotAvailable(port))
143        }
144    }
145
146    /// Checks if the specified port is available for digital use.
147    /// Has a sensible default implementation but can be overriden if needed.
148    #[allow(clippy::missing_errors_doc)]
149    fn digital_available(port: u8) -> Result<(), GPIOError> {
150        if Self::DIGITAL_PORTS.contains(&port) {
151            Ok(())
152        } else {
153            Err(GPIOError::PortNotAvailable(port))
154        }
155    }
156
157    /// Checks if the specified port is available for PWM use.
158    /// Has a sensible default implementation but can be overriden if needed.
159    #[allow(clippy::missing_errors_doc)]
160    fn pwm_available(port: u8) -> Result<(), GPIOError> {
161        if Self::PWM_PORTS.contains(&port) {
162            Ok(())
163        } else {
164            Err(GPIOError::PortNotAvailable(port))
165        }
166    }
167}
168
169/// A platform specific GPIO driver extension for simulation.
170pub trait SimGPIODriver: GPIODriver {
171    /// Returns the other end of the analog input port as an [`AnalogOutput`].
172    fn sim_analog_input(port: u8) -> AnalogOutput;
173
174    /// Returns the other end of the analog output port as an [`AnalogInput`].
175    fn sim_analog_output(port: u8) -> AnalogInput;
176
177    /// Returns the other end of the digital input port as an [`DigitalOutput`].
178    fn sim_digital_input(port: u8) -> DigitalOutput;
179
180    /// Returns the other end of the digital output port as an [`DigitalInput`].
181    fn sim_digital_output(port: u8) -> DigitalInput;
182}
183
184/// A struct that defines a platform specific GPIO driver for user code.
185#[derive(Debug, Clone, Copy)]
186pub struct GPIOVTable {
187    pub(crate) new_analog_input: fn(u8) -> Result<AnalogInput, GPIOError>,
188    pub(crate) new_analog_output: fn(u8) -> Result<AnalogOutput, GPIOError>,
189    pub(crate) new_digital_input: fn(u8) -> Result<DigitalInput, GPIOError>,
190    pub(crate) new_digital_output: fn(u8) -> Result<DigitalOutput, GPIOError>,
191    //sim
192    pub(crate) sim_analog_input: Option<fn(u8) -> AnalogOutput>,
193    pub(crate) sim_analog_output: Option<fn(u8) -> AnalogInput>,
194    pub(crate) sim_digital_input: Option<fn(u8) -> DigitalOutput>,
195    pub(crate) sim_digital_output: Option<fn(u8) -> DigitalInput>,
196}
197
198impl GPIOVTable {
199    pub(crate) fn from_driver<T: GPIODriver>() -> Self {
200        assert!(
201            std::mem::size_of::<T>() == 0,
202            "GPIO Driver must be zero sized"
203        );
204        Self {
205            new_analog_input: T::new_analog_input,
206            new_analog_output: T::new_analog_output,
207            new_digital_input: T::new_digital_input,
208            new_digital_output: T::new_digital_output,
209            sim_analog_input: None,
210            sim_analog_output: None,
211            sim_digital_input: None,
212            sim_digital_output: None,
213        }
214    }
215
216    pub(crate) fn from_sim_driver<T: SimGPIODriver>() -> Self {
217        assert!(
218            std::mem::size_of::<T>() == 0,
219            "GPIO Driver must be zero sized"
220        );
221        Self {
222            new_analog_input: T::new_analog_input,
223            new_analog_output: T::new_analog_output,
224            new_digital_input: T::new_digital_input,
225            new_digital_output: T::new_digital_output,
226            sim_analog_input: Some(T::sim_analog_input),
227            sim_analog_output: Some(T::sim_analog_output),
228            sim_digital_input: Some(T::sim_digital_input),
229            sim_digital_output: Some(T::sim_digital_output),
230        }
231    }
232
233    /// Returns a new analog input for the specified port.
234    ///
235    /// # Errors
236    /// - [`GPIOError::PortNotAvailable`] if the port is not available for analog use
237    /// - [`GPIOError::PortInUse`] if the port is already in use
238    pub fn new_analog_input(&self, port: u8) -> Result<AnalogInput, GPIOError> {
239        (self.new_analog_input)(port)
240    }
241
242    /// Returns a new analog output for the specified port.
243    ///
244    /// # Errors
245    /// - [`GPIOError::PortNotAvailable`] if the port is not available for analog use
246    /// - [`GPIOError::PortInUse`] if the port is already in use
247    pub fn new_analog_output(&self, port: u8) -> Result<AnalogOutput, GPIOError> {
248        (self.new_analog_output)(port)
249    }
250
251    /// Returns a new digital input for the specified port.
252    ///
253    /// # Errors
254    /// - [`GPIOError::PortNotAvailable`] if the port is not available for digital in use
255    /// - [`GPIOError::PortInUse`] if the port is already in use
256    pub fn new_digital_input(&self, port: u8) -> Result<DigitalInput, GPIOError> {
257        (self.new_digital_input)(port)
258    }
259
260    /// Returns a new digital output for the specified port.
261    ///
262    /// # Errors
263    /// - [`GPIOError::PortNotAvailable`] if the port is not available for digital out use
264    /// - [`GPIOError::PortInUse`] if the port is already in use
265    pub fn new_digital_output(&self, port: u8) -> Result<DigitalOutput, GPIOError> {
266        (self.new_digital_output)(port)
267    }
268
269    /// Returns the other end of the analog input port as an [`AnalogOutput`] if initialized as sim.
270    /// If not sim this function will return `Err(NotSimError)`
271    #[allow(clippy::missing_errors_doc)]
272    pub fn sim_analog_input(&self, port: u8) -> Result<AnalogOutput, NotSimError> {
273        self.sim_analog_input
274            .map_or(Err(NotSimError), |f| Ok((f)(port)))
275    }
276
277    /// Returns the other end of the analog output port as an [`AnalogInput`] if initialized as sim.
278    /// If not sim this function will return `Err(NotSimError)`
279    #[allow(clippy::missing_errors_doc)]
280    pub fn sim_analog_output(&self, port: u8) -> Result<AnalogInput, NotSimError> {
281        self.sim_analog_output
282            .map_or(Err(NotSimError), |f| Ok((f)(port)))
283    }
284
285    /// Returns the other end of the digital input port as an [`DigitalOutput`] if initialized as sim.
286    /// If not sim this function will return `Err(NotSimError)`
287    #[allow(clippy::missing_errors_doc)]
288    pub fn sim_digital_input(&self, port: u8) -> Result<DigitalOutput, NotSimError> {
289        self.sim_digital_input
290            .map_or(Err(NotSimError), |f| Ok((f)(port)))
291    }
292
293    /// Returns the other end of the digital output port as an [`DigitalInput`] if initialized as sim.
294    /// If not sim this function will return `Err(NotSimError)`
295    #[allow(clippy::missing_errors_doc)]
296    pub fn sim_digital_output(&self, port: u8) -> Result<DigitalInput, NotSimError> {
297        self.sim_digital_output
298            .map_or(Err(NotSimError), |f| Ok((f)(port)))
299    }
300}