Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Handle embedded_io_async::Read/Write Guarantee #235

Open
wants to merge 11 commits into
base: main
Choose a base branch
from

Conversation

grabpot
Copy link

@grabpot grabpot commented Jan 5, 2025

embedded_io_async::Read/Write do not guarantee "that all available buffer space is filled", and could read/write less than buf.len(), and to handle this:

  • Messages use postcard::from_bytes_cobs() to encode messages with COBS, using 0x00 byte as message separator
  • SplitReader loops over serial.read() until 0x00 separator byte read
    • Loop handles receiving < 1 message on serial.read(), but also > 1 && < 2 messages (partial messages stored) and > 2 && < 3 (max. messages limited by buf.len()) messages (messages returned in vector)
    • Is "cancel-safe", with state stored in self, so no issue to be dropped by select() if interrupted by Timer/KeyEvent
  • SplitWriter loops over serial.write() until all bytes written
  • SPLIT_MESSAGE_MAX_SIZE, which sets buffer size, increased by 2 to account for COBS overhead

NOTE: I've only tested this with an rp2040 and my custom half-duplex UART driver. I don't expect any issues, but it should be tested with the standard BufferedUart or equivalent serial driver.

Stuart Andrews added 11 commits January 2, 2025 08:32
* Return Vec<SplitMessages> as read() from buffer may contain
  more than 1 messages (currently up to 2 based on MAX_SIZE)
* Store partial messages in struct value until next read()
* Change to postcard::to_slice_cobs() to allow individual
  messages to be identified
* Loop read() until at least one complete message received
* Loop write() until all bytes confirmed to be sent
* Increase SPLIT_MESSAGE_MAX_SIZE to allow for COBS overhead
* Handles read() returning < 1 message, > 1 && < 2 messages and
  up to < 3 messages correctly
* If message is corrupt, unlikely to respect COBS encoding
  so not possible to skip bytes in loop
* Avoid possible situation where end_byte is greater than
  buffer.len()
@HaoboGu
Copy link
Owner

HaoboGu commented Jan 5, 2025

embedded_io_async::Read/Write do not guarantee "that all available buffer space is filled", and could read/write less than buf.len()

So what's the problem? I think postcard could solve it automatically, deserialize the message from a partial filled buffer.

@grabpot
Copy link
Author

grabpot commented Jan 5, 2025

The main problem is that the serial Read trait isn't guaranteed to read all the bytes in a message. This is very likely if the UART bus is slower than the monitor cycle, but still quite likely if the UART buffer isn't a multiple of the message size (the internal ring buffer in BufferedUart will only return a contiguous slice, so when it gets to the end of the buffer, it won't return all the data in the buffer, even if its available).

The code could call postcard::take_from_bytes() after each serial read() and loop until it doesn't error, then save any used slice for the next read if necessary. This would skip (or replace) the second while loop in SerialSplitDriver::read(), but otherwise handling a remaining partial message and the possibility of two messages is still required. I thought explicitly identifying complete messages using the COBS separator was more robust than just trying to deserialise any returned bytes.

@HaoboGu
Copy link
Owner

HaoboGu commented Jan 5, 2025

The main problem is that the serial Read trait isn't guaranteed to read all the bytes in a message

I think it is guaranteed. The buffer of BufferredUart is set to SPLIT_MESSAGE_MAX_SIZE. Could you give me a repo or a working example so that I can repro the issue?

@grabpot
Copy link
Author

grabpot commented Jan 5, 2025

I'm not sure, maybe I misunderstood, but:

  1. The trait says "it is not guaranteed that all available bytes are returned". Even if embassy_rp::BufferedUart did guarantee it, I'm not sure it's a given other drivers, e.g. for STM32, that implement the trait do too.
  2. When I asked in the Embassy Matrix a related question about the embassy_hal_internal::RingBuffer behaviour, dirbaio referred to the trait and replied "in general when reading stuff you have to write a loop, you can't rely you'll receive the entire "message" in a single read call."
  3. I'm not sure the size of a postcard serialized message is even fixed. Anything larger than a u8 (admittedly not used today) is encoded as a Varint. Otherwise, RMK could use read_exact() from the trait to read the required number of bytes.
  4. The SPLIT_MESSAGE_MAX_SIZE is set to SplitMessage::POSTCARD_MAX_SIZE + 4, so it's big enough to fit two messages (or was, at least until 0.4.4, I didn't check the new conection state messages)

For an example, I wrote my own half-duplex UART driver as the Sofle keyboard isn't wired for full-duplex UART, and this doesn't (and can't see 3. above) return all the bytes of a message on read(): https://github.com/grabpot/rmk-sofle-dvorak

@@ -19,7 +20,7 @@ pub(crate) enum SplitDriverError {

/// Split message reader from other split devices
pub(crate) trait SplitReader {
async fn read(&mut self) -> Result<SplitMessage, SplitDriverError>;
async fn read(&mut self) -> Result<Vec<SplitMessage, 2>, SplitDriverError>;
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For the trait, each read call should read only one SplitMessage, not two. It defines "expected behavior", should not be affected by the serial implementation


let mut start_byte = 0;
let mut end_byte = start_byte;
while end_byte < self.n_bytes_part {
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The buffer size is SPLIT_MESSAGE_MAX_SIZE, so this second loop is not needed -- we can assume that after the first while loop, there's a complete SplitMessage received.

return Err(SplitDriverError::EmptyMessage);
}

self.n_bytes_part += n_bytes;
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Check if n_bytes_part > self.buffer.len()

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants