Skip to main content

pamoja_gpio/
linux.rs

1//! Opening a GPIO line on a Linux board.
2//!
3//! On a Raspberry Pi or any Linux board a pin is a line on a GPIO chip, a device file such as
4//! `/dev/gpiochip0`, and a line's number on that chip is the one a board's pinout calls its
5//! GPIO or BCM number. [`output`] and [`input`] open one through the GPIO character device
6//! backend of `pamoja-hal` and hand it back as a [`Line`], which a
7//! [`Switch`](crate::switch::Switch) or a [`Contact`](crate::switch::Contact) sits over as
8//! it would over any other pin. The language bindings open lines through the same two
9//! functions, so a relay is driven the same way from Rust, TypeScript, Python, and C#.
10//!
11//! A line is held by one process at a time, and the kernel names the holder, which
12//! `gpioinfo` prints; a line held by another program or claimed by a kernel driver cannot be
13//! opened until it is let go. On Raspberry Pi OS a user in the `gpio` group can open lines
14//! without root.
15//!
16//! Only Linux has the GPIO character device. Everywhere else this module still builds and
17//! [`Line`] still names a type, but opening a line returns [`OpenError::Unsupported`], so a
18//! program and the bindings compile on any platform and say plainly where no line opens.
19//!
20//! # Examples
21//!
22//! ```no_run
23//! use pamoja_gpio::linux;
24//! use pamoja_gpio::pin::Level;
25//! use pamoja_gpio::switch::{Contact, Switch};
26//!
27//! # fn main() -> Result<(), Box<dyn std::error::Error>> {
28//! // A relay board on GPIO17 that energizes on a low input, opened high so the pump stays
29//! // off until it is asked to run, and a float switch to ground on GPIO27 with a pull-up.
30//! let mut pump = Switch::active_low(linux::output("/dev/gpiochip0", 17, Level::High)?);
31//! let mut float = Contact::active_low(linux::input("/dev/gpiochip0", 27)?);
32//! pump.set(!float.is_asserted()?)?;
33//! # Ok(())
34//! # }
35//! ```
36
37use std::fmt;
38use std::path::{Path, PathBuf};
39
40use embedded_hal::digital::{self, ErrorType, InputPin, OutputPin};
41
42use crate::pin::Level;
43
44/// The name the kernel shows as holding a line opened here, which `gpioinfo` prints.
45pub const CONSUMER: &str = "pamoja";
46
47#[cfg(target_os = "linux")]
48type Handle = pamoja_hal::linux::CdevPin;
49
50#[cfg(not(target_os = "linux"))]
51type Handle = core::convert::Infallible;
52
53/// A GPIO line opened on a Linux board, as an output or an input.
54///
55/// It implements the `embedded-hal` pin traits, so it goes under a
56/// [`Switch`](crate::switch::Switch), a [`Contact`](crate::switch::Contact), or any driver
57/// written against them, and [`Line::drive`] and [`Line::read`] work it directly.
58pub struct Line {
59    handle: Handle,
60    chip: PathBuf,
61    offset: u32,
62}
63
64impl fmt::Debug for Line {
65    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
66        f.debug_struct("Line")
67            .field("chip", &self.chip)
68            .field("offset", &self.offset)
69            .finish_non_exhaustive()
70    }
71}
72
73/// Why a line could not be opened.
74#[derive(Debug, Clone, PartialEq, Eq)]
75pub enum OpenError {
76    /// The platform has no GPIO character device, which is any platform but Linux.
77    Unsupported,
78    /// The chip or the line could not be opened.
79    Gpio {
80        /// The chip's device file.
81        chip: PathBuf,
82        /// The line's number on it.
83        line: u32,
84        /// Why, which is most often a missing chip, a line number past the chip's last, a
85        /// line another program holds, or a user outside the `gpio` group.
86        reason: String,
87    },
88}
89
90impl fmt::Display for OpenError {
91    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
92        match self {
93            OpenError::Unsupported => f.write_str(
94                "a GPIO line is opened through the GPIO character device, which only Linux has",
95            ),
96            OpenError::Gpio { chip, line, reason } => {
97                write!(f, "{} line {line}: {reason}", chip.display())
98            }
99        }
100    }
101}
102
103impl std::error::Error for OpenError {}
104
105/// Why an open line could not be driven or read.
106#[derive(Debug, Clone, PartialEq, Eq)]
107pub struct LineError {
108    /// The chip's device file.
109    pub chip: PathBuf,
110    /// The line's number on it.
111    pub line: u32,
112    /// What the kernel reported.
113    pub reason: String,
114}
115
116impl fmt::Display for LineError {
117    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
118        write!(
119            f,
120            "{} line {}: {}",
121            self.chip.display(),
122            self.line,
123            self.reason
124        )
125    }
126}
127
128impl std::error::Error for LineError {}
129
130impl digital::Error for LineError {
131    fn kind(&self) -> digital::ErrorKind {
132        digital::ErrorKind::Other
133    }
134}
135
136/// Opens a line as an output, driving `initial` from the moment it is taken.
137///
138/// Opening at the level that leaves the part off matters for anything that should not
139/// twitch at start-up: an active-low relay is opened [`Level::High`].
140///
141/// # Arguments
142///
143/// * `chip` - the GPIO chip's device file, `/dev/gpiochip0` on most boards.
144/// * `line` - the line's number on that chip, the GPIO or BCM number on a Raspberry Pi.
145/// * `initial` - the level to drive as soon as the line is taken.
146///
147/// # Returns
148///
149/// The line.
150///
151/// # Errors
152///
153/// [`OpenError::Unsupported`] on any platform but Linux, or [`OpenError::Gpio`] when the
154/// chip or the line cannot be opened.
155pub fn output(chip: impl AsRef<Path>, line: u32, initial: Level) -> Result<Line, OpenError> {
156    open(chip.as_ref(), line, Some(initial))
157}
158
159/// Opens a line as an input.
160///
161/// # Arguments
162///
163/// * `chip` - the GPIO chip's device file.
164/// * `line` - the line's number on that chip.
165///
166/// # Returns
167///
168/// The line.
169///
170/// # Errors
171///
172/// [`OpenError::Unsupported`] on any platform but Linux, or [`OpenError::Gpio`] when the
173/// chip or the line cannot be opened.
174pub fn input(chip: impl AsRef<Path>, line: u32) -> Result<Line, OpenError> {
175    open(chip.as_ref(), line, None)
176}
177
178#[cfg(target_os = "linux")]
179fn open(chip: &Path, line: u32, initial: Option<Level>) -> Result<Line, OpenError> {
180    use embedded_hal::digital::PinState;
181    use pamoja_hal::linux;
182
183    let opened = match initial {
184        Some(level) => linux::output(chip, line, CONSUMER, PinState::from(level == Level::High)),
185        None => linux::input(chip, line, CONSUMER),
186    };
187    let handle = opened.map_err(|error| OpenError::Gpio {
188        chip: chip.to_path_buf(),
189        line,
190        reason: error.to_string(),
191    })?;
192    Ok(Line {
193        handle,
194        chip: chip.to_path_buf(),
195        offset: line,
196    })
197}
198
199#[cfg(not(target_os = "linux"))]
200fn open(_chip: &Path, _line: u32, _initial: Option<Level>) -> Result<Line, OpenError> {
201    Err(OpenError::Unsupported)
202}
203
204impl Line {
205    /// The chip the line is on.
206    ///
207    /// # Returns
208    ///
209    /// The chip's device file.
210    pub fn chip(&self) -> &Path {
211        &self.chip
212    }
213
214    /// The line's number on its chip.
215    ///
216    /// # Returns
217    ///
218    /// The offset it was opened at.
219    pub fn offset(&self) -> u32 {
220        self.offset
221    }
222
223    /// Drives the line to a level. The line must have been opened with [`output`].
224    ///
225    /// # Arguments
226    ///
227    /// * `level` - the level to drive.
228    ///
229    /// # Errors
230    ///
231    /// A [`LineError`] naming the line when the kernel refuses the write, which is what an
232    /// input line answers.
233    pub fn drive(&mut self, level: Level) -> Result<(), LineError> {
234        self.write(level)
235    }
236
237    /// Reads the level on the line now.
238    ///
239    /// # Returns
240    ///
241    /// The level.
242    ///
243    /// # Errors
244    ///
245    /// A [`LineError`] naming the line when the kernel refuses the read.
246    pub fn read(&mut self) -> Result<Level, LineError> {
247        self.sample()
248    }
249
250    #[cfg(target_os = "linux")]
251    fn write(&mut self, level: Level) -> Result<(), LineError> {
252        let written = match level {
253            Level::High => self.handle.set_high(),
254            Level::Low => self.handle.set_low(),
255        };
256        written.map_err(|error| self.failed(error))
257    }
258
259    #[cfg(target_os = "linux")]
260    fn sample(&mut self) -> Result<Level, LineError> {
261        match self.handle.is_high() {
262            Ok(high) => Ok(Level::from_bool(high)),
263            Err(error) => Err(self.failed(error)),
264        }
265    }
266
267    #[cfg(target_os = "linux")]
268    fn failed(&self, error: impl fmt::Display) -> LineError {
269        LineError {
270            chip: self.chip.clone(),
271            line: self.offset,
272            reason: error.to_string(),
273        }
274    }
275
276    #[cfg(not(target_os = "linux"))]
277    fn write(&mut self, _level: Level) -> Result<(), LineError> {
278        match self.handle {}
279    }
280
281    #[cfg(not(target_os = "linux"))]
282    fn sample(&mut self) -> Result<Level, LineError> {
283        match self.handle {}
284    }
285}
286
287impl ErrorType for Line {
288    type Error = LineError;
289}
290
291impl OutputPin for Line {
292    fn set_low(&mut self) -> Result<(), LineError> {
293        self.drive(Level::Low)
294    }
295
296    fn set_high(&mut self) -> Result<(), LineError> {
297        self.drive(Level::High)
298    }
299}
300
301impl InputPin for Line {
302    fn is_high(&mut self) -> Result<bool, LineError> {
303        self.read().map(|level| level == Level::High)
304    }
305
306    fn is_low(&mut self) -> Result<bool, LineError> {
307        self.read().map(|level| level == Level::Low)
308    }
309}
310
311#[cfg(test)]
312mod tests {
313    use super::*;
314
315    #[cfg(not(target_os = "linux"))]
316    #[test]
317    fn opening_off_linux_is_unsupported() {
318        let refused = output("/dev/gpiochip0", 17, Level::High).unwrap_err();
319        assert_eq!(refused, OpenError::Unsupported);
320        assert!(refused.to_string().contains("only Linux"), "{refused}");
321        assert_eq!(
322            input("/dev/gpiochip0", 27).unwrap_err(),
323            OpenError::Unsupported
324        );
325    }
326
327    #[cfg(target_os = "linux")]
328    #[test]
329    fn a_missing_chip_is_named_in_the_error() {
330        let refused = output("/dev/gpiochip-pamoja-absent", 17, Level::High).unwrap_err();
331        assert!(
332            matches!(&refused, OpenError::Gpio { line: 17, .. }),
333            "{refused:?}"
334        );
335        assert!(
336            refused
337                .to_string()
338                .starts_with("/dev/gpiochip-pamoja-absent line 17: "),
339            "{refused}"
340        );
341        assert!(input("/dev/gpiochip-pamoja-absent", 27).is_err());
342    }
343
344    #[test]
345    fn a_line_error_names_the_line() {
346        let error = LineError {
347            chip: PathBuf::from("/dev/gpiochip0"),
348            line: 17,
349            reason: "Operation not permitted".to_owned(),
350        };
351        assert_eq!(
352            error.to_string(),
353            "/dev/gpiochip0 line 17: Operation not permitted"
354        );
355        assert_eq!(digital::Error::kind(&error), digital::ErrorKind::Other);
356    }
357}