Skip to main content

pamoja_hal/
linux.rs

1//! Linux backends: the kernel's I2C, SPI, and GPIO character devices as the bus traits.
2//!
3//! On a Raspberry Pi or any Linux single-board computer the buses are files:
4//! `/dev/i2c-1`, `/dev/spidev0.0`, `/dev/gpiochip0`. These functions open them as the
5//! traits every driver is written against, through `linux-embedded-hal`, so a driver
6//! moves from a scripted test to a gateway by being handed a different bus and
7//! nothing else. The kernel drivers have to be enabled first: on a Raspberry Pi that
8//! is `raspi-config`, or `dtparam=i2c_arm=on` and `dtparam=spi=on` in `config.txt`.
9//!
10//! A DS18B20 is the one part that should not be bit-banged from a Linux process,
11//! because user space cannot hold the microsecond slot timing the bus needs. Enable
12//! the kernel's own `w1-gpio` driver instead (`dtoverlay=w1-gpio`) and read the
13//! thermometer through the sysfs file it exposes, which the DS18B20 driver in
14//! `pamoja-sensors` does.
15//!
16//! # Examples
17//!
18//! ```no_run
19//! use pamoja_hal::i2c::I2c;
20//! use pamoja_hal::linux;
21//!
22//! # fn main() -> Result<(), Box<dyn std::error::Error>> {
23//! // The BME280 on the Raspberry Pi's user I2C bus answers its chip id at 0xD0.
24//! const BME280: u8 = 0x76;
25//! let mut bus = linux::i2c("/dev/i2c-1")?;
26//! let mut id = [0u8; 1];
27//! bus.write_read(BME280, &[0xD0], &mut id)?;
28//! println!("chip id 0x{:02X}", id[0]);
29//! # Ok(())
30//! # }
31//! ```
32
33use nix::sys::termios::{self, BaudRate, ControlFlags, FlushArg, SetArg, SpecialCharacterIndices};
34use std::fmt;
35use std::fs::{File, OpenOptions};
36use std::io;
37use std::path::Path;
38
39use embedded_hal::digital::PinState;
40use linux_embedded_hal::gpio_cdev::{Chip, LineRequestFlags};
41use linux_embedded_hal::spidev::{SpiModeFlags, SpidevOptions};
42
43pub use linux_embedded_hal::{gpio_cdev, i2cdev, spidev};
44pub use linux_embedded_hal::{
45    CdevPin, CdevPinError, Delay, I2CError, I2cdev, SPIError, SpidevBus, SpidevDevice,
46};
47
48/// Why a bus or a line could not be opened.
49#[derive(Debug)]
50pub enum OpenError {
51    /// The I2C adapter could not be opened.
52    I2c(i2cdev::linux::LinuxI2CError),
53    /// The SPI device could not be opened or configured.
54    Spi(SPIError),
55    /// The GPIO chip or line could not be opened or requested.
56    Gpio(gpio_cdev::errors::Error),
57    /// The SPI mode was not 0, 1, 2, or 3.
58    SpiMode(u8),
59    /// A serial port could not be opened, or could not be set up as a raw byte pipe.
60    Serial(std::io::Error),
61}
62
63impl fmt::Display for OpenError {
64    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
65        match self {
66            OpenError::I2c(error) => write!(f, "opening the i2c adapter: {error}"),
67            OpenError::Spi(error) => write!(f, "opening the spi device: {error}"),
68            OpenError::Gpio(error) => write!(f, "opening the gpio line: {error}"),
69            OpenError::SpiMode(mode) => write!(f, "spi mode {mode} is not 0, 1, 2, or 3"),
70            OpenError::Serial(error) => write!(f, "opening the serial port: {error}"),
71        }
72    }
73}
74
75impl std::error::Error for OpenError {
76    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
77        match self {
78            OpenError::I2c(error) => Some(error),
79            OpenError::Spi(error) => Some(error),
80            OpenError::Gpio(error) => Some(error),
81            OpenError::SpiMode(_) => None,
82            OpenError::Serial(error) => Some(error),
83        }
84    }
85}
86
87/// Opens an I2C adapter, such as `/dev/i2c-1`.
88///
89/// # Arguments
90///
91/// * `path` - the adapter's device file.
92///
93/// # Returns
94///
95/// The adapter, implementing the 7-bit and 10-bit [`I2c`](embedded_hal::i2c::I2c)
96/// traits.
97///
98/// # Errors
99///
100/// Returns [`OpenError::I2c`] if the file cannot be opened, which usually means the
101/// kernel driver is not enabled or the process lacks permission.
102pub fn i2c(path: impl AsRef<Path>) -> Result<I2cdev, OpenError> {
103    I2cdev::new(path).map_err(OpenError::I2c)
104}
105
106/// Opens an SPI device, such as `/dev/spidev0.0`, at a clock mode and speed.
107///
108/// # Arguments
109///
110/// * `path` - the device file, one per chip-select line.
111/// * `mode` - the clock mode, 0 to 3, as the part's datasheet quotes it.
112/// * `max_speed_hz` - the fastest clock the part accepts.
113///
114/// # Returns
115///
116/// The device, implementing [`SpiDevice`](embedded_hal::spi::SpiDevice) with 8-bit
117/// words.
118///
119/// # Errors
120///
121/// Returns [`OpenError::SpiMode`] for a mode above 3, or [`OpenError::Spi`] if the
122/// device cannot be opened or configured.
123pub fn spi(path: impl AsRef<Path>, mode: u8, max_speed_hz: u32) -> Result<SpidevDevice, OpenError> {
124    let flags = match mode {
125        0 => SpiModeFlags::SPI_MODE_0,
126        1 => SpiModeFlags::SPI_MODE_1,
127        2 => SpiModeFlags::SPI_MODE_2,
128        3 => SpiModeFlags::SPI_MODE_3,
129        other => return Err(OpenError::SpiMode(other)),
130    };
131    let mut device = SpidevDevice::open(path).map_err(OpenError::Spi)?;
132    let options = SpidevOptions::new()
133        .bits_per_word(8)
134        .max_speed_hz(max_speed_hz)
135        .mode(flags)
136        .build();
137    device
138        .0
139        .configure(&options)
140        .map_err(|error| OpenError::Spi(error.into()))?;
141    Ok(device)
142}
143
144/// Requests a GPIO line as an output.
145///
146/// # Arguments
147///
148/// * `chip` - the GPIO chip's device file, `/dev/gpiochip0` on a Raspberry Pi.
149/// * `line` - the line offset on that chip, the BCM number on a Raspberry Pi.
150/// * `consumer` - the name the kernel shows as holding the line.
151/// * `initial` - the level to drive as soon as the line is taken.
152///
153/// # Returns
154///
155/// The line, implementing [`OutputPin`](embedded_hal::digital::OutputPin).
156///
157/// # Errors
158///
159/// Returns [`OpenError::Gpio`] if the chip or line cannot be opened, or the line is
160/// already held by another process or a kernel driver.
161pub fn output(
162    chip: impl AsRef<Path>,
163    line: u32,
164    consumer: &str,
165    initial: PinState,
166) -> Result<CdevPin, OpenError> {
167    let handle = Chip::new(chip)
168        .and_then(|mut chip| chip.get_line(line))
169        .and_then(|line| {
170            line.request(
171                LineRequestFlags::OUTPUT,
172                u8::from(initial == PinState::High),
173                consumer,
174            )
175        })
176        .map_err(OpenError::Gpio)?;
177    CdevPin::new(handle).map_err(OpenError::Gpio)
178}
179
180/// Requests a GPIO line as an input.
181///
182/// # Arguments
183///
184/// * `chip` - the GPIO chip's device file.
185/// * `line` - the line offset on that chip.
186/// * `consumer` - the name the kernel shows as holding the line.
187///
188/// # Returns
189///
190/// The line, implementing [`InputPin`](embedded_hal::digital::InputPin).
191///
192/// # Errors
193///
194/// Returns [`OpenError::Gpio`] if the chip or line cannot be opened or requested.
195pub fn input(chip: impl AsRef<Path>, line: u32, consumer: &str) -> Result<CdevPin, OpenError> {
196    let handle = Chip::new(chip)
197        .and_then(|mut chip| chip.get_line(line))
198        .and_then(|line| line.request(LineRequestFlags::INPUT, 0, consumer))
199        .map_err(OpenError::Gpio)?;
200    CdevPin::new(handle).map_err(OpenError::Gpio)
201}
202
203/// Returns the delay that sleeps the process, for pacing a driver on a gateway.
204///
205/// # Returns
206///
207/// A [`DelayNs`](embedded_hal::delay::DelayNs) over `std::thread::sleep`.
208pub fn delay() -> Delay {
209    Delay
210}
211
212/// The serial port a USB card's bridge answers on, opened raw.
213///
214/// A CDC-ACM device such as `/dev/ttyACM0` is a terminal as far as the kernel is concerned,
215/// and a terminal echoes, translates line endings, and buffers by line, every one of which
216/// would mangle a binary protocol. This opens the port and turns all of that off: eight data
217/// bits, no parity, one stop bit, no flow control, and a read that returns once at least one
218/// byte has arrived. Whatever the card sent before anyone was listening is dropped.
219///
220/// The speed is nominal, since a USB serial port has none, but the port has to be given one.
221///
222/// # Arguments
223///
224/// * `path` - the device file.
225/// * `baud` - the nominal speed, one of the standard rates from 9600 to 921600.
226///
227/// # Returns
228///
229/// The open port, which reads and writes bytes.
230///
231/// # Errors
232///
233/// [`OpenError::Serial`] when the file cannot be opened, the rate is not a standard one, or
234/// the terminal settings cannot be read or applied.
235///
236/// # Examples
237///
238/// ```no_run
239/// use pamoja_hal::linux;
240///
241/// let port = linux::serial("/dev/ttyACM0", 115_200)?;
242/// # Ok::<(), linux::OpenError>(())
243/// ```
244pub fn serial(path: impl AsRef<Path>, baud: u32) -> Result<File, OpenError> {
245    let file = OpenOptions::new()
246        .read(true)
247        .write(true)
248        .open(path)
249        .map_err(OpenError::Serial)?;
250    raw(&file, baud).map_err(OpenError::Serial)?;
251    Ok(file)
252}
253
254// Turns a terminal into a byte pipe at one speed.
255fn raw(file: &File, baud: u32) -> io::Result<()> {
256    let speed = match baud {
257        9_600 => BaudRate::B9600,
258        19_200 => BaudRate::B19200,
259        38_400 => BaudRate::B38400,
260        57_600 => BaudRate::B57600,
261        115_200 => BaudRate::B115200,
262        230_400 => BaudRate::B230400,
263        460_800 => BaudRate::B460800,
264        921_600 => BaudRate::B921600,
265        _ => {
266            return Err(io::Error::new(
267                io::ErrorKind::InvalidInput,
268                "not a standard serial rate",
269            ))
270        }
271    };
272
273    let mut tty = termios::tcgetattr(file)?;
274    termios::cfmakeraw(&mut tty);
275    termios::cfsetispeed(&mut tty, speed)?;
276    termios::cfsetospeed(&mut tty, speed)?;
277    tty.control_flags |= ControlFlags::CLOCAL | ControlFlags::CREAD;
278    tty.control_flags &= !(ControlFlags::PARENB | ControlFlags::CSTOPB | ControlFlags::CRTSCTS);
279    tty.control_flags &= !ControlFlags::CSIZE;
280    tty.control_flags |= ControlFlags::CS8;
281    tty.control_chars[SpecialCharacterIndices::VMIN as usize] = 1;
282    tty.control_chars[SpecialCharacterIndices::VTIME as usize] = 1;
283    termios::tcsetattr(file, SetArg::TCSANOW, &tty)?;
284    termios::tcflush(file, FlushArg::TCIOFLUSH)?;
285    Ok(())
286}