[Beginner][Embedded] Understanding the usage of Traits and Errors in a crate and what it means

Hi I'm a beginner working on a rp235x-hal project that also uses this tmc2209-uart crate on crates.io .

I have managed to simplify the library to 3 files with the basic barebones which is slightly modified to make it simpler.

I would be grateful if y'all could explain exactly what all the trait implementations and generic parts do and how they do this. I am especially confused about the Errors and how they get mapped etc, because I have been trying to replace the generic U with a rp235x-hal uart peripheral type.

Thank You

Here is the code in 3 separate files:-

driver.rs:-

use crate::error::Error;

pub struct Tmc2209<U> {
    uart: U,
}

impl<U> Tmc2209<U> {
    pub fn new(uart: U) -> Self {
        Self { uart }
    }
}

impl<U, E> Tmc2209<U>
where
    U: embedded_io::Read<Error = E>
        + embedded_io::Write<Error = E>
        + embedded_io::ReadReady<Error = E>,
{
    pub fn write_register(&mut self, buf: &[u8]) -> Result<(), Error<E>> {
        self.uart.write_all(buf).map_err(Error::Uart)?;
        Ok(())
    }
}

error.rs:-

use core::fmt;

#[derive(Debug, Clone, Copy, PartialEq, Eq, defmt::Format)]
pub enum Error<E> {
    Uart(E),
}

impl<E> Error<E> {
    pub fn map_uart<F, E2>(self, f: F) -> Error<E2>
    where
        F: FnOnce(E) -> E2,
    {
        match self {
            Error::Uart(e) => Error::Uart(f(e)),
        }
    }
}

impl<E: fmt::Debug> fmt::Display for Error<E> {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            Error::Uart(e) => write!(f, "Uart error: {:?}", e),
        }
    }
}

test.rs:-

use crate::driver;

//The exact specific type that is actually passed to the function
type TmcUart = rp235x_hal::uart::UartPeripheral<
    rp235x_hal::uart::Enabled,
    rp235x_hal::pac::UART1,
    (
        rp235x_hal::gpio::Pin<
            rp235x_hal::gpio::bank0::Gpio8,
            rp235x_hal::gpio::FunctionUart,
            rp235x_hal::gpio::PullDown,
        >,
        rp235x_hal::gpio::Pin<
            rp235x_hal::gpio::bank0::Gpio9,
            rp235x_hal::gpio::FunctionUart,
            rp235x_hal::gpio::PullDown,
        >,
    ),
>;

pub fn test(
    uart: TmcUart
) {
    let mut driver1 = driver::Tmc2209::new(uart);
    driver1.write_register(&[0x00]).unwrap();
}

I would also like to know what exact type generic E has, what is E really

Maybe thinking of generic parameters as "compile-time arguments passed to the generic thing" provides decent intuition for how they work?

Unless you're forking their crate and ripping out the generic U, I wouldn't say you're "replacing" the generic U; you're "substituting" a concrete type for U, much as in fn foo(x: u32) -> u32 { x + 5 }, calling foo(2) substitutes x = 2 (just at runtime instead of compile time).

E is sort of like a compile-time variable that represents "any type". (And then the trait bounds constrain what sort of type it is.) Just like how x is "any value", and x: u32 narrows that down quite a lot.

Thanks.

I was thinking something more like the following but it doesn't work and I can't understand why

type TmcUart = rp235x_hal::uart::UartPeripheral<
    rp235x_hal::uart::Enabled,
    rp235x_hal::pac::UART1,
    (
        rp235x_hal::gpio::Pin<
            rp235x_hal::gpio::bank0::Gpio8,
            rp235x_hal::gpio::FunctionUart,
            rp235x_hal::gpio::PullDown,
        >,
        rp235x_hal::gpio::Pin<
            rp235x_hal::gpio::bank0::Gpio9,
            rp235x_hal::gpio::FunctionUart,
            rp235x_hal::gpio::PullDown,
        >,
    ),
>;
pub struct myTmc2209 {
    uart: TmcUart,
}

impl myTmc2209 {
    pub fn new(uart: TmcUart) -> Self {
        Self { uart }
    }
}

impl myTmc2209 {
    pub fn read_register(
        &mut self,
        buf: &mut [u8],
    ) -> Result<(), Error<rp235x_hal::uart::ReadError>> {
        self.uart.read_full_blocking(buf).map_err(Error::Uart)?;
        Ok(())
    }
}

the above code is driver.rs

What's the (full) error you get?

error[E0277]: `?` couldn't convert the error to `error::Error<ReadError<'_>>`
  --> src/driver.rs:56:63
   |
56 |         self.uart.read_full_blocking(buf).map_err(Error::Uart)?;
   |                   ----------------------- --------------------^ the trait `From<error::Error<ReadErrorType>>` is not implemented for `error::Error<ReadError<'_>>`
   |                   |                       |
   |                   |                       this can't be annotated with `?` because it has type `Result<_, error::Error<ReadErrorType>>`
   |                   this has type `Result<_, ReadErrorType>`
   |
note: `error::Error<ReadError<'_>>` needs to implement `From<error::Error<ReadErrorType>>`
  --> src/error.rs:4:1
   |
 4 | pub enum Error<E> {
   | ^^^^^^^^^^^^^^^^^
   = note: the question mark operation (`?`) implicitly performs a conversion on the error value using the `From` trait

I think you're using the wrong error type; I looked here before reading your response with the error, and saw both ReadErrorType and ReadError.

Note that last line: "the question mark operation (?) implicitly performs a conversion on the error value using the From trait". That's why you get an error about a From bound not being satisfied, it doesn't just immediately say "well these error types aren't the same, ERROR".

Thanks I didn't notice the ReadErrorType , it seems to resolve the error.

Thanks.

so about the From trait is it something I would need to implement if I decide to use read error instead of readerrortype

You can't impl From<ForeignType> for OtherForeignType, but you could do any necessary conversion in .map_error(_). (Note that, for all T, T implements From<T>, so ? is always capable of doing no conversion at all.)

so what does the error actually mean then

so does this mean that It should work but then it can also not work?

sorry for asking these stupid questions but rust is kind of my first real programming language and I only learnt it recently

and also what does the pub fn map_uart do in this context

I don't see it used anywhere else in the crate so is it like used for the implicit ? conversion

If it's a public function not used elsewhere in the crate, then they're just providing it for the utility or convenience of users of the library. Just an ordinary function (and, in this case, a method in particular).

It's not automatically hooked into most language-level features like ?, though since it has a self parameter, you can use .map_uart(_). Whether something takes a self parameter is how Rust distinguishes "methods", which can be called as foo.method(bar) or as something like Foo::method(foo, bar), from ordinary functions which can be called in the second format but not the first.

The compiler is trying to figure out what would be necessary for your code to work, and reports an error if it thinks your code can't work.

The compiler isn't sufficiently all-knowing to always determine a good way to fix the problem, but it can offer potential solutions (sometimes) and explain what the compiler was trying to do that failed.

In this case, it's saying, "I ended up trying to apply ?, and to do that I needed to figure out how to convert from Foo to Bar, and I couldn't, but if Bar: From<Foo>, then I could (and then that part of the code would work)". Again, the compiler isn't saying that that's the only possible solution to the error, it's just one.

But the failure noticed by the compiler might be a side effect of the actual cause of the error; in this case, the compiler found a failure in the From bound not holding, when I think it's fair to say that the actual cause of the error is the mismatch in error types.

The compiler's error messages, generally, can help you trace back from the error encountered by the compiler (stuff about ? and From in this case) to whatever caused that error. (Sometimes, of course, the compiler actually manages to figure out the exact mistake.)

Thanks that makes a lot of sense to me. I think I have a better understanding of rust from your explanations.