use core::sync::atomic::{AtomicBool, Ordering};
use embassy_futures::select::{Either, select};
use embassy_sync::channel::Channel;
use embassy_sync::signal::Signal;
use embedded_io_async::{Read, Write};
use rmk_types::protocol::vial::{VIAL_EP_SIZE, ViaCommand};
use crate::RawMutex;
pub(super) type VialReport = [u8; VIAL_EP_SIZE];
pub struct DongleRouter {
pub(super) to_keyboard: Channel<RawMutex, VialReport, 1>,
pub(super) to_host: Channel<RawMutex, VialReport, 4>,
link_connected: AtomicBool,
link_dropped: Signal<RawMutex, ()>,
}
impl DongleRouter {
pub const fn new() -> Self {
Self {
to_keyboard: Channel::new(),
to_host: Channel::new(),
link_connected: AtomicBool::new(false),
link_dropped: Signal::new(),
}
}
pub(super) fn link_up(&self) {
self.link_dropped.reset();
self.to_keyboard.clear();
self.link_connected.store(true, Ordering::Relaxed);
}
pub(super) fn link_down(&self) {
self.link_connected.store(false, Ordering::Relaxed);
self.link_dropped.signal(());
self.to_keyboard.clear();
}
pub async fn run_session<R: Read, T: Write>(&self, rx: &mut R, tx: &mut T) {
self.to_host.clear();
self.to_keyboard.clear();
select(self.host_to_keyboard(rx), self.keyboard_to_host(tx)).await;
}
async fn host_to_keyboard<R: Read>(&self, rx: &mut R) {
loop {
let mut report = [0u8; VIAL_EP_SIZE];
if rx.read_exact(&mut report).await.is_err() {
return;
}
self.forward_report(report).await;
}
}
async fn keyboard_to_host<T: Write>(&self, tx: &mut T) {
loop {
let report = self.to_host.receive().await;
if tx.write_all(&report).await.is_err() {
return;
}
}
}
async fn forward_report(&self, mut report: VialReport) {
if self.link_connected.load(Ordering::Relaxed)
&& let Either::Second(()) = select(self.link_dropped.wait(), self.to_keyboard.send(report)).await
{
return;
}
report[0] = ViaCommand::Unhandled as u8;
self.to_host.send(report).await;
}
}
impl Default for DongleRouter {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
extern crate alloc;
use alloc::collections::VecDeque;
use alloc::vec::Vec;
use embassy_futures::join::join;
use embassy_futures::yield_now;
use embedded_io_async::{ErrorKind, ErrorType};
use super::*;
use crate::test_support::test_block_on as block_on;
struct ChunkRead {
chunks: VecDeque<Vec<u8>>,
idle_reads: usize,
}
impl ErrorType for ChunkRead {
type Error = ErrorKind;
}
impl Read for ChunkRead {
async fn read(&mut self, buf: &mut [u8]) -> Result<usize, Self::Error> {
loop {
let Some(chunk) = self.chunks.front_mut() else {
if self.idle_reads == 0 {
return Ok(0);
}
self.idle_reads -= 1;
yield_now().await;
continue;
};
let n = chunk.len().min(buf.len());
buf[..n].copy_from_slice(&chunk[..n]);
chunk.drain(..n);
if chunk.is_empty() {
self.chunks.pop_front();
}
return Ok(n);
}
}
}
struct VecWrite {
captured: Vec<u8>,
}
impl ErrorType for VecWrite {
type Error = ErrorKind;
}
impl Write for VecWrite {
async fn write(&mut self, buf: &[u8]) -> Result<usize, Self::Error> {
self.captured.extend_from_slice(buf);
Ok(buf.len())
}
async fn flush(&mut self) -> Result<(), Self::Error> {
Ok(())
}
}
fn request(cmd: u8) -> VialReport {
let mut report = [0u8; VIAL_EP_SIZE];
report[0] = cmd;
report[1] = 0xAB; report
}
fn run(router: &DongleRouter, chunks: VecDeque<Vec<u8>>, idle_reads: usize) -> Vec<u8> {
let mut rx = ChunkRead { chunks, idle_reads };
let mut tx = VecWrite { captured: Vec::new() };
block_on(router.run_session(&mut rx, &mut tx));
tx.captured
}
#[test]
fn a_connected_keyboard_gets_the_report_byte_for_byte() {
let router = DongleRouter::new();
router.link_up();
let req = request(0x01);
let captured = run(&router, VecDeque::from([req.to_vec()]), 0);
assert!(captured.is_empty(), "forwarded requests get no local reply");
let forwarded = router.to_keyboard.try_receive().expect("report routed to the keyboard");
assert_eq!(forwarded, req, "byte-for-byte pass-through");
}
#[test]
fn an_absent_keyboard_answers_the_unhandled_echo() {
let router = DongleRouter::new();
let captured = run(&router, VecDeque::from([request(0x01).to_vec()]), 2);
assert_eq!(captured.len(), VIAL_EP_SIZE, "exactly one reply");
assert_eq!(captured[0], ViaCommand::Unhandled as u8);
assert_eq!(&captured[1..], &request(0x01)[1..], "the request's tail is echoed");
assert!(
router.to_keyboard.try_receive().is_err(),
"and nothing is parked for later"
);
}
#[test]
fn a_forward_waiting_on_a_dying_link_answers_instead_of_replaying() {
let router = DongleRouter::new();
router.link_up();
router.to_keyboard.try_send([0u8; VIAL_EP_SIZE]).unwrap();
block_on(join(router.forward_report(request(0x02)), async {
yield_now().await;
router.link_down();
}));
let reply = router.to_host.try_receive().expect("a reply is queued");
assert_eq!(reply[0], ViaCommand::Unhandled as u8);
assert_eq!(
&reply[1..],
&request(0x02)[1..],
"seqless protocol: the echo is the match"
);
assert!(
router.to_keyboard.try_receive().is_err(),
"and the request is not parked for whichever keyboard reconnects next"
);
}
#[test]
fn a_keyboard_reply_reaches_the_host_intact() {
let router = DongleRouter::new();
let mut reply = [0u8; VIAL_EP_SIZE];
reply[0] = 0x01;
reply[31] = 0xEE;
let mut rx = ChunkRead {
chunks: VecDeque::new(),
idle_reads: 4,
};
let mut tx = VecWrite { captured: Vec::new() };
block_on(join(router.run_session(&mut rx, &mut tx), async {
yield_now().await;
router.link_up();
router.to_host.try_send(reply).unwrap();
yield_now().await;
}));
assert_eq!(tx.captured, reply.to_vec(), "one whole report per write");
}
}