gpiocdev_embedded_hal/
lib.rs

1// SPDX-FileCopyrightText: 2024 Kent Gibson <warthog618@gmail.com>
2//
3// SPDX-License-Identifier: Apache-2.0 OR MIT
4
5//! A library providing [`embedded_hal::digital`] traits for
6//! [`gpiocdev::Request`] and therefore for any Linux platform supporting the
7//! GPIO character device.
8//!
9//! The requests contain only a single pin which must be configured as an
10//! input or output.
11//!
12//! Asynchronous wrappers providing [`embedded_hal_async::digital::Wait`]
13//! traits are available for various async reactors.
14//!
15//! The library can also be used to provide a simplified interface to
16//! [`gpiocdev`] for simple use cases.
17//!
18//! # Example Usage
19//!
20//! Reading an input pin:
21//!
22//! ```no_run
23//! # fn example() -> Result<(), gpiocdev_embedded_hal::Error> {
24//! use embedded_hal::digital::InputPin;
25//!
26//! let mut pin = gpiocdev_embedded_hal::InputPin::new("/dev/gpiochip0", 4)?;
27//! if pin.is_high()? {
28//!     println!("Input is high.");
29//! }
30//! # Ok(())
31//! # }
32//! ```
33//! Setting an output pin:
34//!
35//! ```no_run
36//! # fn example() -> Result<(), gpiocdev_embedded_hal::Error> {
37//! use embedded_hal::digital::{OutputPin, PinState};
38//!
39//! // level is set as part of the request
40//! let mut led0 = gpiocdev_embedded_hal::OutputPin::from_name("LED0", PinState::High)?;
41//!
42//! // change the level later
43//! led0.set_low()?;
44//! # Ok(())
45//! # }
46//! ```
47//!
48//! Waiting for edges on an input pin:
49//!
50//!  ```no_run
51//! # #[cfg(feature = "async_tokio")]
52//! # async fn example() -> Result<(), gpiocdev_embedded_hal::Error> {
53//! use embedded_hal::digital::InputPin;
54//! use embedded_hal_async::digital::Wait;
55//!
56//! let mut pin = gpiocdev_embedded_hal::tokio::InputPin::new("/dev/gpiochip0", 4)?;
57//!
58//! pin.wait_for_any_edge().await?;
59//! if pin.is_high()? {
60//!     println!("Input is high.");
61//! }
62//! # Ok(())
63//! # }
64//! ```
65use std::path::Path;
66
67use embedded_hal::digital::PinState;
68use gpiocdev::{
69    line::{Config, Direction, Offset, Value},
70    Request,
71};
72
73/// Wrappers for various async reactors.
74#[cfg(any(feature = "async_tokio", feature = "async_io"))]
75mod r#async;
76
77#[cfg(feature = "async_io")]
78pub use r#async::async_io;
79#[cfg(feature = "async_tokio")]
80pub use r#async::tokio;
81
82/// Core common class for InputPin and OutputPin.
83#[derive(Debug)]
84struct Pin {
85    req: Request,
86    offset: Offset,
87    config: Config,
88}
89
90impl Pin {
91    #[inline]
92    fn is_high(&mut self) -> Result<bool, Error> {
93        Ok(self.req.as_ref().value(self.offset)?
94            == state_to_value(PinState::High, self.config.active_low))
95    }
96
97    #[inline]
98    fn is_low(&mut self) -> Result<bool, Error> {
99        Ok(!self.is_high()?)
100    }
101}
102
103impl From<Pin> for Request {
104    fn from(pin: Pin) -> Self {
105        pin.req
106    }
107}
108
109/// Provides [`embedded_hal::digital`] traits for a [`gpiocdev::Request`]
110/// containing a single input pin.
111///
112/// Holding the [`InputPin`] grants exclusive access to the pin.
113///
114/// Do NOT drop the [`InputPin`] until you are completely done with it.
115/// Dropping and re-requesting the line is far more expensive than getting the
116/// value.
117#[derive(Debug)]
118pub struct InputPin(Pin);
119
120impl InputPin {
121    /// Creates a new input pin for the given `offset` on the given `chip`.
122    ///
123    /// ```no_run
124    /// # fn example() -> Result<(), gpiocdev_embedded_hal::Error> {
125    /// use embedded_hal::digital::InputPin;
126    ///
127    /// let mut pin = gpiocdev_embedded_hal::InputPin::new("/dev/gpiochip0", 4)?;
128    /// if pin.is_high()? {
129    ///     println!("Input is high.");
130    /// }
131    /// # Ok(())
132    /// # }
133    /// ```
134    pub fn new<P>(chip: P, offset: u32) -> Result<Self, Error>
135    where
136        P: AsRef<Path>,
137    {
138        let req = Request::builder()
139            .on_chip(chip.as_ref())
140            .with_line(offset)
141            .as_input()
142            .request()?;
143
144        InputPin::try_from(req)
145    }
146
147    /// Set this pin to output mode.
148    pub fn into_output_pin(mut self, state: PinState) -> Result<OutputPin, Error> {
149        let pin = &mut self.0;
150        let req = pin.req.as_ref();
151        let value = state_to_value(state, pin.config.active_low);
152        let mut config = req.config();
153        config.from_line_config(&pin.config).as_output(value);
154        req.reconfigure(&config)?;
155        // don't update the whole config - retain the input specific fields
156        // (edge_detection and debounce) in case the pin is switched to input.
157        pin.config.direction = Some(Direction::Output);
158        pin.config.value = Some(value);
159
160        Ok(OutputPin(self.0))
161    }
162
163    /// Create an [`InputPin`] from a [`gpiocdev::FoundLine`].
164    ///
165    /// # Examples
166    /// ```no_run
167    /// # fn example() -> Result<(), gpiocdev_embedded_hal::Error> {
168    /// use embedded_hal::digital::InputPin;
169    ///
170    /// let sensor0 = gpiocdev::find_named_line("SENSOR0").unwrap();
171    /// let mut pin = gpiocdev_embedded_hal::InputPin::from_found_line(sensor0)?;
172    /// if pin.is_low()? {
173    ///     println!("Input is low.");
174    /// }
175    /// # Ok(())
176    /// # }
177    /// ```
178    pub fn from_found_line(fl: gpiocdev::FoundLine) -> Result<Self, Error> {
179        let req = Request::builder()
180            .with_found_line(&fl)
181            .as_input()
182            .request()?;
183        let config = req.config();
184        let line_config = config.line_config(fl.info.offset).unwrap().clone();
185        Ok(InputPin(Pin {
186            req,
187            offset: fl.info.offset,
188            config: line_config,
189        }))
190    }
191
192    /// Create an [`InputPin`] given a line name.
193    ///
194    /// # Examples
195    /// ```no_run
196    /// # fn example() -> Result<(), gpiocdev_embedded_hal::Error> {
197    /// use embedded_hal::digital::InputPin;
198    ///
199    /// let mut gpio22 = gpiocdev_embedded_hal::InputPin::from_name("GPIO22")?;
200    /// if gpio22.is_low()? {
201    ///     println!("Input is low.");
202    /// }
203    /// # Ok(())
204    /// # }
205    /// ```
206    pub fn from_name(name: &str) -> Result<Self, Error> {
207        let line = gpiocdev::find_named_line(name)
208            .ok_or_else(|| Error::UnfoundLine(name.into()))?;
209        Self::from_found_line(line)
210    }
211}
212
213impl TryFrom<Request> for InputPin {
214    type Error = Error;
215
216    /// Convert any single input line [`gpiocdev::Request`] into a [`InputPin`].
217    ///
218    /// This allows for advanced configurations such as setting bias
219    /// or using the active_low flag to flip the line polarity.
220    /// [`InputPin::new<P>()`] should be used for less complex configurations.
221    fn try_from(req: Request) -> Result<Self, Self::Error> {
222        let config = req.as_ref().config();
223        let offsets = config.lines();
224        if offsets.len() != 1 {
225            return Err(Error::MultipleLinesRequested);
226        }
227        let offset = offsets[0];
228        // unwrap is safe as line config must exist.
229        let line_config = config.line_config(offset).unwrap().clone();
230        if line_config.direction != Some(Direction::Input) {
231            return Err(Error::RequiresInputMode);
232        }
233
234        Ok(InputPin(Pin {
235            req,
236            offset,
237            config: line_config,
238        }))
239    }
240}
241
242impl From<InputPin> for Request {
243    /// Convert the [`InputPin`] into the contained [`Request`].
244    fn from(pin: InputPin) -> Self {
245        pin.0.req
246    }
247}
248
249impl embedded_hal::digital::InputPin for InputPin {
250    #[inline]
251    fn is_high(&mut self) -> Result<bool, Self::Error> {
252        self.0.is_high()
253    }
254
255    #[inline]
256    fn is_low(&mut self) -> Result<bool, Self::Error> {
257        self.0.is_low()
258    }
259}
260
261impl embedded_hal::digital::ErrorType for InputPin {
262    /// Errors returned by [`InputPin`].
263    type Error = Error;
264}
265
266/// Provides [`embedded_hal::digital`] traits for a [`gpiocdev::Request`]
267/// containing a single output pin.
268///
269/// Holding the [`OutputPin`] grants exclusive access to the pin.
270///
271/// Do NOT drop the [`OutputPin`] until you are completely done with it.
272/// While you hold the [`OutputPin`] the line is guaranteed to remain as set,
273/// but when dropped it may be altered, either by other users or by the kernel
274/// itself.
275/// Dropping and re-requesting the line is also far more expensive than setting
276/// the value.
277#[derive(Debug)]
278pub struct OutputPin(Pin);
279
280impl OutputPin {
281    /// Creates a new output pin for the given `offset` on the given `chip`.
282    ///
283    /// ```no_run
284    /// # fn example() -> Result<(), gpiocdev_embedded_hal::Error> {
285    /// use embedded_hal::digital::{OutputPin, PinState};
286    ///
287    /// let mut pin = gpiocdev_embedded_hal::OutputPin::new("/dev/gpiochip0", 17, PinState::Low)?;
288    /// // later...
289    /// pin.set_high()?;
290    /// # Ok(())
291    /// # }
292    /// ```
293    pub fn new<P>(chip: P, offset: u32, state: PinState) -> Result<Self, Error>
294    where
295        P: AsRef<Path>,
296    {
297        let req = Request::builder()
298            .on_chip(chip.as_ref())
299            .with_line(offset)
300            .as_output(state_to_value(state, false))
301            .request()?;
302
303        OutputPin::try_from(req)
304    }
305
306    /// Set this pin to input mode.
307    pub fn into_input_pin(mut self) -> Result<InputPin, Error> {
308        let pin = &mut self.0;
309        let req = pin.req.as_ref();
310        let mut config = req.config();
311        config.from_line_config(&pin.config).as_input();
312        req.reconfigure(&config)?;
313        // don't update the whole config - retain the output specific fields
314        // (drive) in case the pin is switched back to output.
315        pin.config.direction = Some(Direction::Input);
316        pin.config.value = None;
317
318        Ok(InputPin(self.0))
319    }
320
321    /// Create an [`OutputPin`] from a [`gpiocdev::FoundLine`].
322    ///
323    /// # Examples
324    /// ```no_run
325    /// # fn example() -> Result<(), gpiocdev_embedded_hal::Error> {
326    /// use embedded_hal::digital::{OutputPin, PinState};
327    ///
328    /// let led0 = gpiocdev::find_named_line("LED0").unwrap();
329    /// let mut pin = gpiocdev_embedded_hal::OutputPin::from_found_line(led0, PinState::High)?;
330    /// // ...
331    /// pin.set_low()?;
332    /// # Ok(())
333    /// # }
334    /// ```
335    pub fn from_found_line(fl: gpiocdev::FoundLine, state: PinState) -> Result<Self, Error> {
336        let req = Request::builder()
337            .with_found_line(&fl)
338            .as_output(state_to_value(state, false))
339            .request()?;
340        let config = req.config();
341        let line_config = config.line_config(fl.info.offset).unwrap().clone();
342        Ok(OutputPin(Pin {
343            req,
344            offset: fl.info.offset,
345            config: line_config,
346        }))
347    }
348
349    /// Create an [`OutputPin`] given a line name.
350    ///
351    /// # Examples
352    /// ```no_run
353    /// use embedded_hal::digital::{OutputPin, PinState};
354    ///
355    /// # fn example() -> Result<(), gpiocdev_embedded_hal::Error> {
356    /// let mut led0 = gpiocdev_embedded_hal::OutputPin::from_name("LED0", PinState::High)?;
357    /// // ...
358    /// led0.set_low()?;
359    /// # Ok(())
360    /// # }
361    /// ```
362    pub fn from_name(name: &str, state: PinState) -> Result<Self, Error> {
363        let line = gpiocdev::find_named_line(name)
364            .ok_or_else(|| Error::UnfoundLine(name.into()))?;
365        Self::from_found_line(line, state)
366    }
367}
368
369impl TryFrom<Request> for OutputPin {
370    type Error = Error;
371
372    /// Convert any single output line [`gpiocdev::Request`] into an [`OutputPin`].
373    ///
374    /// This allows for advanced configurations such as setting bias or drive
375    /// or using the active_low flag to flip the line polarity.
376    /// [`OutputPin::new<P>()`] should be used for less complex configurations.
377    fn try_from(req: Request) -> Result<Self, Self::Error> {
378        let config = req.as_ref().config();
379        let offsets = config.lines();
380        if offsets.len() != 1 {
381            return Err(Error::MultipleLinesRequested);
382        }
383        let offset = offsets[0];
384        // unwrap is safe as line config must exist.
385        let line_config = config.line_config(offset).unwrap().clone();
386        if line_config.direction != Some(Direction::Output) {
387            return Err(Error::RequiresOutputMode);
388        }
389
390        Ok(OutputPin(Pin {
391            req,
392            offset,
393            config: line_config,
394        }))
395    }
396}
397
398impl From<OutputPin> for Request {
399    /// Convert the [`OutputPin`] into the contained [`Request`].
400    fn from(pin: OutputPin) -> Self {
401        pin.0.req
402    }
403}
404
405impl embedded_hal::digital::InputPin for OutputPin {
406    // Supporting InputPin is intentional to allow support for reading the
407    // physical line value for output lines where supported by hardware,
408    // e.g. to read an open drain line while not actively driving it.
409
410    #[inline]
411    /// Is the line high?
412    fn is_high(&mut self) -> Result<bool, Self::Error> {
413        self.0.is_high()
414    }
415
416    #[inline]
417    /// Is the line low?
418    fn is_low(&mut self) -> Result<bool, Self::Error> {
419        self.0.is_low()
420    }
421}
422
423impl embedded_hal::digital::OutputPin for OutputPin {
424    #[inline]
425    fn set_low(&mut self) -> Result<(), Self::Error> {
426        self.set_state(PinState::Low)
427    }
428
429    #[inline]
430    fn set_high(&mut self) -> Result<(), Self::Error> {
431        self.set_state(PinState::High)
432    }
433
434    fn set_state(&mut self, state: PinState) -> Result<(), Error> {
435        let pin = &mut self.0;
436        let value = state_to_value(state, pin.config.active_low);
437        if pin.config.value != Some(value) {
438            pin.req.as_ref().set_value(pin.offset, value)?;
439            pin.config.value = Some(value);
440        }
441        Ok(())
442    }
443}
444
445impl embedded_hal::digital::StatefulOutputPin for OutputPin {
446    fn is_set_high(&mut self) -> Result<bool, Self::Error> {
447        Ok(self.0.config.value == Some(Value::Active))
448    }
449
450    fn is_set_low(&mut self) -> Result<bool, Self::Error> {
451        Ok(self.0.config.value == Some(Value::Inactive))
452    }
453
454    fn toggle(&mut self) -> Result<(), Self::Error> {
455        let pin = &mut self.0;
456        let value = pin.config.value.unwrap_or_default().not();
457        pin.req.as_ref().set_value(pin.offset, value)?;
458        pin.config.value = Some(value);
459        Ok(())
460    }
461}
462
463impl embedded_hal::digital::ErrorType for OutputPin {
464    /// Errors returned by [`OutputPin`].
465    type Error = Error;
466}
467
468/// Converts a [`PinState`] to the gpiocdev logical line [`Value`].
469fn state_to_value(state: PinState, is_active_low: bool) -> Value {
470    let value = match state {
471        PinState::High => Value::Active,
472        PinState::Low => Value::Inactive,
473    };
474    if is_active_low {
475        return value.not();
476    }
477    value
478}
479
480/// Errors returned by [`gpiocdev_embedded_hal`](crate) types.
481#[derive(Clone, Debug, thiserror::Error, Eq, PartialEq)]
482pub enum Error {
483    /// Requests can only contain a single requested line.
484    #[error("Request must not contain a multiple lines")]
485    MultipleLinesRequested,
486
487    /// InputPins must be in input mode.
488    #[error("Requested pin must be in input mode")]
489    RequiresInputMode,
490
491    /// OutputPins must be in output mode.
492    #[error("Requested pin must be in output mode")]
493    RequiresOutputMode,
494
495    /// Cannot find named line.
496    #[error("Cannot find a line named '{0}'")]
497    UnfoundLine(String),
498
499    /// An error returned from an underlying gpiocdev call.
500    #[error("gpiocdev returned: {0}")]
501    Cdev(#[source] gpiocdev::Error),
502}
503
504impl From<gpiocdev::Error> for Error {
505    fn from(err: gpiocdev::Error) -> Self {
506        Self::Cdev(err)
507    }
508}
509
510impl embedded_hal::digital::Error for Error {
511    fn kind(&self) -> embedded_hal::digital::ErrorKind {
512        embedded_hal::digital::ErrorKind::Other
513    }
514}