Create RMK's own protocol
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-rpcis suitable for our use case
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:
- Change
define_dispatch!macro upstream, make it compatible with generics, so it can be used in libraries. - Study the expanded code of
define_dispatch!and write our own version ofdefine_dispatch.
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.
I'm experimenting on this branch, where I replicated a smaller version of
postcard-rpcto 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, unlikedefine_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-typesI'm implementing a simpler version ofpostcard_rpc::Endpointmanually, but it's not hard to create a macro similar toendpoints!for that. For now I'm using a singleu8value 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>
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
On the host side, looks like we cannot use the postcard
HostClientif 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
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