I agree, that using something like tokio_util::codec in combination with Framed sounds like a good idea.
However, all this stuff will be running on an NXP i.MX6ULL SoC, which is a single-core 32-bit ARM processor.
Hence, I am unsure whether it'd be reasonable to implement an async interface for this protocol, which would ultimately result in a load of different threads being spawned (right?!).
My question is, how I'd best go about implementing the interface to the to-be-decoded stream of frames handled by the transceiver, so that I'd get an API that may decode frames into a stream of valid packages for a higher-level protocol.
Should this interface be sync or async, given the current hardware limitations.
If sync, should I even use the traits from tokio_util and/or tokio-stream?
Should I implement the generic en-/decoding within this library or rather just implement concrete en-/decoding in the higher level library.
Firstly, things like Embassy and RTIC provide cheap single-threaded async runtimes for embedded (things like Arm Cortex-M0 cores or RISC-V, clocked at single digit megahertz). Async is designed to get you I/O concurrency from a single thread, and is ideal for this application. If you've got Linux atop the core (it's more powerful than the Arm Linux system I work on), then you could use Tokio's current thread runtime quite happily.
Second, even if you decide to use a sync interface, I'd reuse Decoder and Encoder from tokio-util; you'd want to reimplement Framed in a suitable fashion for your interface, however, since that's very firmly in async land.
Thank you. Yes, the system runs a Yocto/OE Linux. I may have been too preoccupied with the single core fact, but the system already runs a plethora of threads and processes from the Linux kernel and userland just fine.
If you're already paying the overhead of Linux to begin with, just use Tokio for the serial I/O. It can be faster to use spawn_blocking to handle a sequence of file I/O operations via std::fs instead of tokio::fs, but that's about the only gotcha I can see.
And it's immensely helpful when dealing with a device on the other end of a UART to have tracing spitting out Perfetto-compatible traces, so that you can look at the relationship between events nice and clearly.
If you use Tokio, it might be useful to select the current-thread scheduler — see the runtime module for docs on how its behavior differs in detail. The important part is that it avoids spawning a dedicated thread for async tasks, instead running them from the thread(s) which call block_on(), which may be beneficial on a single-core system.