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

Support for other transports (SPI, BLE, TCP/IP, RTT, ...) #26

Open
pperanich opened this issue Jun 10, 2024 · 13 comments
Open

Support for other transports (SPI, BLE, TCP/IP, RTT, ...) #26

pperanich opened this issue Jun 10, 2024 · 13 comments
Labels
open to sponsorship This issue is something James wants, and would be willing to prioritize as sponsored/contract work

Comments

@pperanich
Copy link
Contributor

Thank you for the fantastic work on this library! It significantly reduces the effort to get RPC comms established between a host and embedded device.

Currently, the framework as a whole only supports transport over serial or USB, with the target_server side specifically relying on the embassy-usb driver. I am interested in what it would take to support other transports such as SPI, BLE (using the trouble lib, which abstracts over Bluetooth HCI and is thus more versatile than implementing support for a specific stack like the NRF SoftDevice), or TCP/IP via smoltcp. While I see a clear path to implementing SPI or BLE support on the host_client side, it is unclear how to extend this support to the target_server embedded side. I've loosely been following your blog where it looks like you've been thinking about this on some level already. I hope this issue can serve as a log or discussion place for these implementations.

@jamesmunns
Copy link
Owner

Yep, on the PC side, #25 is starting to work towards this, and in the near future I plan to restore "UART + COBS" functionality.

Likely, we will need some kind of similar "Wire" abstraction for the MCU side as well, and I'll need to rework the define_dispatch! macro to be less hardcoded to USB, and instead require you to name some type where T: Wire or something like that.

I'm open to PRs that make this happen, ideally starting with adding back UART+COBS, but also open to a PR that refactors this in a way where supporting two different transports (ideally without a lot of duplicated dispatcher code) is possible.

@okhsunrog
Copy link

It would be awesome if we could use any transport that implements embedded-io

@jamesmunns
Copy link
Owner

If anyone is following this, the changes between 0.7 and 0.10 have made it much easier to write a transport, including blanket ones like @okhsunrog suggested.

I probably will not write this in the near future (as I don't need it), but if someone wants to take it on, I'm happy to chat or say how I would do it.

@jamesmunns
Copy link
Owner

In particular, if you can implement the WireTx, WireRx, and WireSpawn traits (this third one is probably the same for all of embassy and can be reused):

//////////////////////////////////////////////////////////////////////////////
// TX
//////////////////////////////////////////////////////////////////////////////
/// This trait defines how the server sends frames to the client
pub trait WireTx: Clone {
/// The error type of this connection.
///
/// For simple cases, you can use [`WireTxErrorKind`] directly. You can also
/// use your own custom type that implements [`AsWireTxErrorKind`].
type Error: AsWireTxErrorKind;
/// Send a single frame to the client, returning when send is complete.
async fn send<T: Serialize + ?Sized>(&self, hdr: VarHeader, msg: &T)
-> Result<(), Self::Error>;
/// Send a single frame to the client, without handling serialization
async fn send_raw(&self, buf: &[u8]) -> Result<(), Self::Error>;
/// Send a logging message on the [`LoggingTopic`][crate::standard_icd::LoggingTopic]
///
/// This message is simpler as it does not do any formatting
async fn send_log_str(&self, kkind: VarKeyKind, s: &str) -> Result<(), Self::Error>;
/// Send a logging message on the [`LoggingTopic`][crate::standard_icd::LoggingTopic]
///
/// This version formats to the outgoing buffer
async fn send_log_fmt<'a>(
&self,
kkind: VarKeyKind,
a: Arguments<'a>,
) -> Result<(), Self::Error>;
}
/// The base [`WireTx`] Error Kind
#[derive(Debug, Clone, Copy)]
#[non_exhaustive]
pub enum WireTxErrorKind {
/// The connection has been closed, and is unlikely to succeed until
/// the connection is re-established. This will cause the Server run
/// loop to terminate.
ConnectionClosed,
/// Other unspecified errors
Other,
/// Timeout (WireTx impl specific) reached
Timeout,
}
/// A conversion trait to convert a user error into a base Kind type
pub trait AsWireTxErrorKind {
/// Convert the error type into a base type
fn as_kind(&self) -> WireTxErrorKind;
}
impl AsWireTxErrorKind for WireTxErrorKind {
#[inline]
fn as_kind(&self) -> WireTxErrorKind {
*self
}
}
//////////////////////////////////////////////////////////////////////////////
// RX
//////////////////////////////////////////////////////////////////////////////
/// This trait defines how to receive a single frame from a client
pub trait WireRx {
/// The error type of this connection.
///
/// For simple cases, you can use [`WireRxErrorKind`] directly. You can also
/// use your own custom type that implements [`AsWireRxErrorKind`].
type Error: AsWireRxErrorKind;
/// Receive a single frame
///
/// On success, the portion of `buf` that contains a single frame is returned.
async fn receive<'a>(&mut self, buf: &'a mut [u8]) -> Result<&'a mut [u8], Self::Error>;
}
/// The base [`WireRx`] Error Kind
#[derive(Debug, Clone, Copy)]
#[non_exhaustive]
pub enum WireRxErrorKind {
/// The connection has been closed, and is unlikely to succeed until
/// the connection is re-established. This will cause the Server run
/// loop to terminate.
ConnectionClosed,
/// The received message was too large for the server to handle
ReceivedMessageTooLarge,
/// Other message kinds
Other,
}
/// A conversion trait to convert a user error into a base Kind type
pub trait AsWireRxErrorKind {
/// Convert the error type into a base type
fn as_kind(&self) -> WireRxErrorKind;
}
impl AsWireRxErrorKind for WireRxErrorKind {
#[inline]
fn as_kind(&self) -> WireRxErrorKind {
*self
}
}
//////////////////////////////////////////////////////////////////////////////
// SPAWN
//////////////////////////////////////////////////////////////////////////////
/// A trait to assist in spawning a handler task
///
/// This trait is weird, and mostly exists to abstract over how "normal" async
/// executors like tokio spawn tasks, taking a future, and how unusual async
/// executors like embassy spawn tasks, taking a task token that maps to static
/// storage
pub trait WireSpawn: Clone {
/// An error type returned when spawning fails. If this cannot happen,
/// [`Infallible`][core::convert::Infallible] can be used.
type Error;
/// The context used for spawning a task.
///
/// For example, in tokio this is `()`, and in embassy this is `Spawner`.
type Info;
/// Retrieve [`Self::Info`]
fn info(&self) -> &Self::Info;
}

You should be able to support whatever interface you'd like. Please feel free to add an impl to source/postcard-rpc/src/server/impls, or let me know if you have any questions!

@jamesmunns jamesmunns changed the title Support for other transports (SPI, BLE, TCP/IP, ...) Support for other transports (SPI, BLE, TCP/IP, RTT, ...) Oct 31, 2024
@jamesmunns
Copy link
Owner

Added RTT to the title because at some point I would like to have that.

@MathiasKoch
Copy link

+1 for RTT transport support 👍

@okhsunrog
Copy link

okhsunrog commented Nov 1, 2024

I'm using a cli terminal in a similar way, I implemented embedded-io for RTT and I can easily switch interfaces. I'm actually interested in trying postcard-rpc with RTT, but I'm not sure yet if it fits my task.
@jamesmunns can it be used with 2 embedded devices, both being no_std or the host side needs to be std?

@jamesmunns
Copy link
Owner

@okhsunrog:

one potential blocker to supporting RTT is we need some kind of async recv() to be notified when a frame is ready. We could have a low priority, blocking task that does this (using interrupt executors on embassy), but it might be nicer to have some way for the debugger to "notify" when it has written RTT data, potentially using an interrupt as a callback handler. I discussed this in chat today: https://libera.irclog.whitequark.org/rust-embedded/2024-11-01#1730467273-1730468273; - there were some ideas of how this could be possible

re: two embedded devices, see #37 - there isn't a no-std client yet, only a std/host client. It should definitely be possible to implement, especially if you are willing to accept some limitations (you can't clone the client, only one in-flight request at a time, handling topic subscriptions might be a little tricky), but this isn't a priority for me at the moment.

@okhsunrog
Copy link

@jamesmunns I guess we can do something like this for now, polling in a loop:

pub async fn async_recv(channels: &mut Channels, buf: &mut [u8]) -> usize {
    loop {
        let n = channels.down.0.read(buf);
        if n > 0 {
            return n;
        }
        Timer::after(Duration::from_millis(1)).await;
    }
}

@jamesmunns
Copy link
Owner

@okhsunrog yep, I agree that is a reasonable "not great, not bad" solution, and is exactly what I had in mind :)

@Dicklessgreat
Copy link
Contributor

I'm developing an embedded device that should be operable through multiple interfaces:

  • Via USB from a host PC with rich GUI
  • Via physical controls (like rotary encoders) directly on the device
  • (Potentially via other interfaces in the future)

The goal is to make all device functionality accessible regardless of the available interface. This leads to two concerns with the current implementation:

  1. For endpoints and topics, we need to duplicate their definitions in each "Wire", which increases maintenance costs.

  2. In my embedded application, I want to post messages to the server from internal tasks (like UI events from physical controls). Currently, this seems difficult because the server is tightly coupled with external transport implementations (like USB). Would it make sense to have an "internal" transport implementation that could work alongside external transports?

Let me know if you'd like more details about either of these use cases.

@jamesmunns
Copy link
Owner

Hey @Dicklessgreat, so this issue is mostly for tracking the implementation of new WireRx/WireTx impls. You asked two main things (let me know if I missed anything):

  1. "Can we offer multiple interfaces", and the answer is "not yet, but maybe in the future". I've opened [server] How to support multiple interfaces at once? #67 to track this. This feature is something I am open to, and if anyone is interested in sponsoring this work, I'd be happy to prioritize it!
  2. "Can we have local/internal clients", and the answer is "I don't think this is a good idea". I've opened [server] Should we support "internal" interfaces? #68 for this, and I'm open to discuss this there.

@jamesmunns jamesmunns added the open to sponsorship This issue is something James wants, and would be willing to prioritize as sponsored/contract work label Dec 17, 2024
@jamesmunns
Copy link
Owner

Noting here, I currently have two WIP transports, help welcome for getting them polished up and merged:

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
open to sponsorship This issue is something James wants, and would be willing to prioritize as sponsored/contract work
Projects
None yet
Development

No branches or pull requests

5 participants