rmk icon indicating copy to clipboard operation
rmk copied to clipboard

Create RMK's own protocol

Open pcasotti opened this issue 3 months ago • 6 comments

Currently, due to feature and license differences with qmk, we have some limitations when working with Via/Vial, therefore it would be nice to have our own protocol for dynamic keyboard configuration. Thankfully #539 and #554 have some preliminary work done in this regard, making the next steps much easier.

Next steps (I think):

  • Figure out a name
  • Decide the commands used in the protocol
  • Check if postcard-rpc is suitable for our use case

pcasotti avatar Sep 22 '25 13:09 pcasotti

Check if postcard_rpc is suitable for our use case

I've finished some investigation. The main issue of postcard-rpc is it's define_dispatch! macro. It is designed for calling from user(app) code, no generics in this macro, so it's hard to use it in the rmk crate. I also tried ergot, but I feel that ergot is too complex for our case.

If we still want postcard_rpc, there are two options I think:

  1. Change define_dispatch! macro upstream, make it compatible with generics, so it can be used in libraries.
  2. Study the expanded code of define_dispatch! and write our own version of define_dispatch.

HaoboGu avatar Sep 22 '25 14:09 HaoboGu

I'm experimenting on this branch, where I replicated a smaller version of postcard-rpc to experiment on.

I'm using a proc macro instead of define_dispatch! to deal with the generics, the macro also only maps the requests to the handlers, unlike define_dispatch! which handles sending and receiving data as well.

#[dispatcher(
    GetKeyAction = get_key_action_handler;
    SetKeyAction = set_key_action_handler;
    GetActiveLayer = get_active_layer_handler;
)]
pub(crate) struct RmkRpcService< // ..

In rmk-types I'm implementing a simpler version of postcard_rpc::Endpoint manually, but it's not hard to create a macro similar to endpoints! for that. For now I'm using a single u8 value for the key, similar to vial but I believe using a hash like postcard does is better to avoid accidentally hitting the wrong endpoint. There's also no message id to manage ordering yet.

The integration with the rest of the code is a mess because of the feature flags and probably only compiles on my setup.

pcasotti avatar Sep 25 '25 01:09 pcasotti

I'm experimenting on this branch, where I replicated a smaller version of postcard-rpc to experiment on.

I'm using a proc macro instead of define_dispatch! to deal with the generics, the macro also only maps the requests to the handlers, unlike define_dispatch! which handles sending and receiving data as well.

#[dispatcher( GetKeyAction = get_key_action_handler; SetKeyAction = set_key_action_handler; GetActiveLayer = get_active_layer_handler; )] pub(crate) struct RmkRpcService< // .. In rmk-types I'm implementing a simpler version of postcard_rpc::Endpoint manually, but it's not hard to create a macro similar to endpoints! for that. For now I'm using a single u8 value for the key, similar to vial but I believe using a hash like postcard does is better to avoid accidentally hitting the wrong endpoint. There's also no message id to manage ordering yet.

The integration with the rest of the code is a mess because of the feature flags and probably only compiles on my setup.

This is quite impressive! I think we can use reuse most parts of postcard_rpc, including endpoint!, topic! and others.

I played with the expanded code of define_dispatch yesterday, it's not very complicated, only two main parts, const(such as key size) calculation and dispatch impl. I simplified the expanded code a bit:

// Expanded code for 
define_dispatch! {
    app: MyApp;
    spawn_fn: spawn_fn;
    tx_impl: AppTx;
    spawn_impl: WireSpawnImpl;
    context: Context;
    endpoints: {
        list: ENDPOINT_LIST;

        | EndpointTy                | kind      | handler                       |
        | ----------                | ----      | -------                       |
        | PingEndpoint              | blocking  | ping_handler                  |
    };
    topics_in: {
        list: TOPICS_IN_LIST;

        | TopicTy                   | kind      | handler                       |
        | ----------                | ----      | -------                       |
    };
    topics_out: {
        list: TOPICS_OUT_LIST;
    };
}

// Recursive expansion of define_dispatch! macro
// ==============================================

mod sizer {
    use super::*;
    use postcard_rpc::Key;
    // A lot const size calculation, might be removed/simplified in our case
    const EP_IN_KEYS_SZ:usize = ENDPOINT_LIST.endpoints.len();
    const EP_IN_KEYS:[Key;EP_IN_KEYS_SZ] = const  {
        let mut keys = [unsafe {
            Key::from_bytes([0;8])
        };EP_IN_KEYS_SZ];
        let mut i = 0;
        while i<EP_IN_KEYS_SZ {
            keys[i] = ENDPOINT_LIST.endpoints[i].1;
            i+=1;
        }
        keys
    };

    // ... Ignored 
    
   #[doc = concat!("This defines the postcard-rpc app implementation for ",stringify!(MyApp))]
pub type MyApp = impls::MyApp<{ sizer::NEEDED_SZ }>;
const HAS_DUPE:bool = MyApp::has_dupe();
const _DUPE_CHECK:() = const  {
    {
        if!(!HAS_DUPE){
            {
                core::panicking::panic_fmt(core::const_format_args!("Caught duplicate items. Is `omit_std` set? This is likely a bug in your code. See https://github.com/jamesmunns/postcard-rpc/issues/135."));
            };
        }
    };
};
mod impls {
    use super::*;
    pub struct MyApp<const N:usize>{
        pub context:Context,
        pub spawn:WireSpawnImpl,
        pub device_map: &'static postcard_rpc::DeviceMap,
    }
    impl <const N:usize>MyApp<N>{
        #[doc = r" Create a new instance of the dispatcher"]
        pub fn new(context:Context,spawn:WireSpawnImpl,) -> Self {
            const MAP: &postcard_rpc::DeviceMap =  &postcard_rpc::DeviceMap {
                types: const  {
                    const LISTS: &[&[&'static postcard_rpc::postcard_schema::schema::NamedType]] =  &[ENDPOINT_LIST.types,TOPICS_IN_LIST.types,TOPICS_OUT_LIST.types,];
                    const TTL_COUNT:usize = ENDPOINT_LIST.types.len()+TOPICS_IN_LIST.types.len()+TOPICS_OUT_LIST.types.len();
                    const BIG_RPT:([Option<&'static postcard_rpc::postcard_schema::schema::NamedType>; TTL_COUNT],usize) = postcard_rpc::uniques::merge_nty_lists(LISTS);
                    const SMALL_RPT:[&'static postcard_rpc::postcard_schema::schema::NamedType; BIG_RPT.1] = postcard_rpc::uniques::cruncher(BIG_RPT.0.as_slice()); SMALL_RPT.as_slice()
                },
               endpoints: &ENDPOINT_LIST.endpoints,
               topics_in: &TOPICS_IN_LIST.topics,
               topics_out: &TOPICS_OUT_LIST.topics,
               min_key_len:const  {
                    match sizer::NEEDED_SZ {
                        1 => postcard_rpc::header::VarKeyKind::Key1,
                        2 => postcard_rpc::header::VarKeyKind::Key2,
                        4 => postcard_rpc::header::VarKeyKind::Key4,
                        8 => postcard_rpc::header::VarKeyKind::Key8,
                        _ => core::panicking::panic("internal error: entered unreachable code"),
                    
                        }
                }
            };
            MyApp {
                context,spawn,device_map:MAP,
            }
        }
    
        }
    impl MyApp<1>{
        #[doc = r" Check if there are any unexpected duplicates, typically this occurs because"]
        #[doc = r" the user has set `omit_std`"]
        #[doc(hidden)]
        pub const fn has_dupe() -> bool {
            const DUPE:bool = const  {
                const ALL_KEYS: &[postcard_rpc::Key1] =  &[<postcard_rpc::standard_icd::PingEndpoint as postcard_rpc::Endpoint>::REQ_KEY1, <postcard_rpc::standard_icd::GetAllSchemasEndpoint as postcard_rpc::Endpoint>::REQ_KEY1, <PingEndpoint as postcard_rpc::Endpoint>::REQ_KEY1,];
                const LEN:usize = ALL_KEYS.len();
                let mut i = 0;
                let mut dupe = false;
                while i<LEN {
                    let mut j = i+1;
                    while j<LEN {
                        dupe|=ALL_KEYS[i].const_cmp(&ALL_KEYS[j]);
                        j+=1;
                    }i+=1;
                }dupe
            };
            DUPE
        }
    
        }
    impl postcard_rpc::server::Dispatch for MyApp<1>{
        type Tx = AppTx;
        fn min_key_len(&self) -> postcard_rpc::header::VarKeyKind {
            (postcard_rpc::header::VarKeyKind::Key1)
        }
        #[doc = r" Handle dispatching of a single frame"]
        async fn handle(&mut self,tx: &postcard_rpc::server::Sender<Self::Tx>,hdr: &postcard_rpc::header::VarHeader,body: &[u8],) -> Result<(), <Self::Tx as postcard_rpc::server::WireTx>::Error>{
            let key = hdr.key;
            let Ok(keyb) =  <postcard_rpc::Key1>::try_from(&key)else {
                let err = postcard_rpc::standard_icd::WireError::KeyTooSmall;
                return tx.error(hdr.seq_no,err).await;
            };
            match keyb {
                <postcard_rpc::standard_icd::PingEndpoint as postcard_rpc::Endpoint>::REQ_KEY1 => {
                    let Ok(req) = postcard_rpc::postcard::from_bytes::<<postcard_rpc::standard_icd::PingEndpoint as postcard_rpc::Endpoint>::Request>(body)else {
                        let err = postcard_rpc::standard_icd::WireError::DeserFailed;
                        return tx.error(hdr.seq_no,err).await;
                    };
                    tx.reply::<postcard_rpc::standard_icd::PingEndpoint>(hdr.seq_no, &req).await
                }, 
                <postcard_rpc::standard_icd::GetAllSchemasEndpoint as postcard_rpc::Endpoint>::REQ_KEY1 => {
                    tx.send_all_schemas(hdr,self.device_map).await
                }
                <PingEndpoint as postcard_rpc::Endpoint>::REQ_KEY1 => {
                    let Ok(req) = postcard_rpc::postcard::from_bytes::<<PingEndpoint as postcard_rpc::Endpoint>::Request>(body)else {
                        let err = postcard_rpc::standard_icd::WireError::DeserFailed;
                        return tx.error(hdr.seq_no,err).await;
                    };
                    let dispatch = self;
                    let context =  &mut dispatch.context;
                    #[allow(unused)]
                    let spawninfo =  &dispatch.spawn;
                    {
                        let reply = ping_handler(context,hdr.clone(),req);
                        if tx.reply::<PingEndpoint>(hdr.seq_no, &reply).await.is_err(){
                            let err = postcard_rpc::standard_icd::WireError::SerFailed;
                            tx.error(hdr.seq_no,err).await
                        }else {
                            Ok(())
                        }
                    }
                }
                _other => {
                    let err = postcard_rpc::standard_icd::WireError::UnknownKey;
                    tx.error(hdr.seq_no,err).await
                },
            
                }
        }
    
        }
}

// The rest is similar impl for MyApp<2/4/8>

HaoboGu avatar Sep 25 '25 02:09 HaoboGu

On the host side, looks like we cannot use the postcard HostClient if we're sending messages over HID. The only thing left to use from postcard_rpc is the unique key generation for the endpoint

pcasotti avatar Oct 15 '25 17:10 pcasotti

On the host side, looks like we cannot use the postcard HostClient if we're sending messages over HID. The only thing left to use from postcard_rpc is the unique key generation for the endpoint

yes, postcard uses USB bulk, and I think that's fine? USB HID is much slower. And WebUSB provides raw USB access which can be used to read/write data via USB bulk endpoints

HaoboGu avatar Oct 15 '25 23:10 HaoboGu

yes, postcard uses USB bulk, and I think that's fine? USB HID is much slower. And WebUSB provides raw USB access which can be used to read/write data via USB bulk endpoints

I'll experiment with USB bulk then, I'll also try communicating over BLE

pcasotti avatar Oct 16 '25 00:10 pcasotti