esp-rs/esp-idf-hal

UartDriver as `embedded_io::Read` blocks until buffer is full

DaneSlattery opened this issue · 3 comments

In the below code snippet, modified from the uart loopback.
We know that the uart TX has written 1 byte.
The receive buffer is [0;10] , which is typical when we don't know how large the receive buffer is.

In this case, the read blocks forever, instead of popping 1 byte from the UART buffer (into buf) and returning 1. If the buffer is [0;1], the read returns instantly.

This means that the embedded_io::Read contract is not being upheld:

/// Read some bytes from this source into the specified buffer, returning how many bytes were read.
    ///
    /// If no bytes are currently available to read, this function blocks until at least one byte is available.
    ///
    /// If bytes are available, a non-zero amount of bytes is read to the beginning of `buf`, and the amount
    /// is returned. It is not guaranteed that *all* available bytes are returned, it is possible for the
    /// implementation to read an amount of bytes less than `buf.len()` while there are more bytes immediately
    /// available.
    ///
    /// If the reader is at end-of-file (EOF), `Ok(0)` is returned. There is no guarantee that a reader at EOF
    /// will always be so in the future, for example a reader can stop being at EOF if another process appends
    /// more bytes to the underlying file.
    ///
    /// If `buf.len() == 0`, `read` returns without blocking, with either `Ok(0)` or an error.
    /// The `Ok(0)` doesn't indicate EOF, unlike when called with a non-empty buffer.
//! UART loopback test
//!
//! Folowing pins are used:
//! TX    GPIO12
//! RX    GPIO13
//!
//! Depending on your target and the board you are using you have to change the pins.
//!
//! This example transfers data via UART.
//! Connect TX and RX pins to see the outgoing data is read as incoming data.

use embedded_io::Read;
use esp_idf_hal::delay::BLOCK;
use esp_idf_hal::gpio;
use esp_idf_hal::peripherals::Peripherals;
use esp_idf_hal::prelude::*;
use esp_idf_hal::uart::*;
use esp_idf_sys::EspError;

fn main() -> anyhow::Result<()> {
    esp_idf_hal::sys::link_patches();

    let peripherals = Peripherals::take()?;
    let tx = peripherals.pins.gpio12;
    let rx = peripherals.pins.gpio13;

    println!("Starting UART loopback test");
    let config = config::Config::new().baudrate(Hertz(115_200));
    let mut uart = UartDriver::new(
        peripherals.uart1,
        tx,
        rx,
        Option::<gpio::Gpio0>::None,
        Option::<gpio::Gpio1>::None,
        &config,
    )?;

    loop {
        uart.write(&[0xaa])?;


        read(&mut uart)?;


    }
}

fn read<R: embedded_io::Read>(r: &mut R) -> Result<(), anyhow::Error> {
    let mut buf = [0u8; 10];
    match r.read(&mut buf) {
        Err(x) => println!("Error reading"),
        Ok(x) => println!("Read :{:?} ", &buf[..x]),
    };
    Ok(())
}

The approach in UartRxDriver is to call uart_read_bytes with length=buffer.len().

I propose the following:

 /// Read multiple bytes into a slice; block until specified timeout
    pub fn read(&self, buf: &mut [u8], delay: TickType_t) -> Result<usize, EspError> {
        // uart_read_bytes() returns error (-1) or how many bytes were read out
        // 0 means timeout and nothing is yet read out
        let available = self.count()?;
        if (available > buf.len()) {
            // should we throw? or only read as many as the buffer has space for
        }

        let len = unsafe {
            uart_read_bytes(
                self.port(),
                buf.as_mut_ptr().cast(),
                available as u32,
                delay,
            )
        };

        if len >= 0 {
            Ok(len as usize)
        } else {
            Err(EspError::from_infallible::<ESP_ERR_INVALID_STATE>())
        }
    }

UPDATED:

You might be right to request the semantics you request as I polled the "Embedded Rust" Matrix channel, and it seems the consensus is it should operate as you intuitively expect. Though - I must say and the other folks agreed - this is not explicitly documented on the embedded_io::Read trait, nor on the std::io::Read trait. The POSIX read and recv syscalls seem a bit more explicit but IMO still leave room for interpretation!

(
I'll probably PR an update of the docu of embedded_io::Read and embedded_io_async::Read to be more explicit in this regard. And will probably PR or at least open an issue for std::io::Read as it has similar vagueness that leaves room for interpretation.
)

With that said, I just pushed a potential fix to master, so if you could try it out?

The reason why I did not accept your proposal from above ^^^ verbatim is because it has several problems. For completeness, let me comment on these (look for the comments marked with "(Ivan)"):

    /// Read multiple bytes into a slice; block until specified timeout
    pub fn read(&self, buf: &mut [u8], delay: TickType_t) -> Result<usize, EspError> {
        // uart_read_bytes() returns error (-1) or how many bytes were read out
        // 0 means timeout and nothing is yet read out
        // (Ivan) -1 also might mean a timeout. -1 is returned when `uart_read_bytes` cannot lock the internal semaphore, which 
        // might happen if multiple threads try to call this `read` method
        let available = self.count()?;
        if (available > buf.len()) {
            // should we throw? or only read as many as the buffer has space for
            // (Ivan) We should only read as many bytes as the buffer has space for
        }

        // (Ivan) The problem with this code is when available = 0
        // In this case, the call below will return immediately (as that's how `uart_read_bytes` is implemented)
        // - yet - you want it to block until at least one byte becomes available, or until the `delay` expires
        let len = unsafe {
            uart_read_bytes(
                self.port(),
                buf.as_mut_ptr().cast(),
                available as u32,
                delay,
            )
        };

        if len >= 0 {
            Ok(len as usize)
        } else {
            Err(EspError::from_infallible::<ESP_ERR_INVALID_STATE>())
        }
    }

That fixes it, thank you! Happy with the solution you came to.