1use std::fmt;
38use std::path::{Path, PathBuf};
39
40use embedded_hal::digital::{self, ErrorType, InputPin, OutputPin};
41
42use crate::pin::Level;
43
44pub 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
53pub 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#[derive(Debug, Clone, PartialEq, Eq)]
75pub enum OpenError {
76 Unsupported,
78 Gpio {
80 chip: PathBuf,
82 line: u32,
84 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#[derive(Debug, Clone, PartialEq, Eq)]
107pub struct LineError {
108 pub chip: PathBuf,
110 pub line: u32,
112 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
136pub fn output(chip: impl AsRef<Path>, line: u32, initial: Level) -> Result<Line, OpenError> {
156 open(chip.as_ref(), line, Some(initial))
157}
158
159pub 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 pub fn chip(&self) -> &Path {
211 &self.chip
212 }
213
214 pub fn offset(&self) -> u32 {
220 self.offset
221 }
222
223 pub fn drive(&mut self, level: Level) -> Result<(), LineError> {
234 self.write(level)
235 }
236
237 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}