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}