At which levels of abstraction, 'nb' should be used?

Most methods from embedded-hal traits use nb crate to make it possible to use these traits in non-blocking mode.

As this approach has limits, when, and at what level in abstraction stack, should I stop bothering and making methods blocking?

  • Device level crate, timers — very good, calling method to wait for timer periodically is ok.
  • Device level crate, setting pins high or low — seems that there's nothing to wait and these method don't return nb::Result.
  • Device level crate, serial/i2c/spi — much less usable. embedded-hal's serial, spi and i2c API is blocking. Even sending a single byte by calling "send" again and again with single u8 argument requires unnecessary state management at callee level.
  • Driver level: should I even bother with nb? I.e. if my LCD driver exposes draw method that takes byte array of whole screen, it's definitely a very bad candidate for nb; but if my driver has get_temperature (with no arguments), should I make it nonblocking with returning nb::Result?

On the other hand, I can't imagine doing something with pins in nonblocking manner. When I'm doing something with pins, I don't want anything to preempt me while I wait to make a pulse of required width, especially some futures-based homemade 'green thread'. Why bother with nonblocking at all? Or it's just for long-running timers?

Non-blocking should be used when you want to be able to do other stuff while waiting, especially on embedded devices where you probably don't have multi-threading available.

Hmm, I thought serial, spi, etc are blocking because I confused core's Result with nb::Result in returned values. In fact, they are non-blocking. Except i2c, as it's message-oriented, maybe that's a clue for my question.

Regarding serial. Write of single word is nonblocking, but writing multiple words is blocking.

From documentation of nb:

The WouldBlock error variant signals that the operation can't be completed right now and would need to block to complete.

Does that mean that it's unwise to use nb for operations that can wait for something multiple times during execution? (As opposed of operations that wait only before doing something?)

I.e. WouldBlock means requested operation didn't start yet at all, not completed at half and returned for waiting?


Let's imagine we're going to make nb version of serial bwrite_all operation:

fn write_all(&mut self, buffer: &[Word]) -> nb::Result<(), Self::Error>

This function should keep track how many words it already written. There are many options where to put this state:

  • Writer struct. Most questionable. If there will be another call to write_all before first call isn't yet finished, there will be data races. It's just conceptually does not belong to Writer, but to write operation.

  • Operation struct.

    fn write_all(&mut self, buffer: &[Word]) -> WriteOp { ... }
    
    impl WriteOp {
      fn execute() -> nb::Result((), Error) { ... };
    }
    
    let w = rx.write_all(&packet);
    block!(w.execute());
    
  • Callee

    fn write_all(&mut self, buffer: &[Word], words_written: &mut u32) -> nb::Result<(), Self::Error>
    

    (Or something like that)

Is it okay to use nb for such things?