feat(input-server): add InputServer<D: InputDevice> with protocol, mock + UART adapters, and tests
This commit is contained in:
@@ -4,3 +4,5 @@ version = "0.1.0"
|
|||||||
edition = "2024"
|
edition = "2024"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
|
types = { workspace = true }
|
||||||
|
server-framework = { workspace = true }
|
||||||
|
|||||||
38
crates/servers/input-server/src/adapters/mock.rs
Normal file
38
crates/servers/input-server/src/adapters/mock.rs
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
use crate::input_device::InputDevice;
|
||||||
|
use crate::protocol::InputEvent;
|
||||||
|
|
||||||
|
pub struct MockInputDevice {
|
||||||
|
events: [Option<InputEvent>; 16],
|
||||||
|
head: usize,
|
||||||
|
tail: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl MockInputDevice {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self {
|
||||||
|
events: [None; 16],
|
||||||
|
head: 0,
|
||||||
|
tail: 0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn push_event(&mut self, event: InputEvent) {
|
||||||
|
self.events[self.tail] = Some(event);
|
||||||
|
self.tail = (self.tail + 1) % self.events.len();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl InputDevice for MockInputDevice {
|
||||||
|
fn poll(&mut self) -> Option<InputEvent> {
|
||||||
|
if self.head == self.tail {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let event = self.events[self.head].take();
|
||||||
|
self.head = (self.head + 1) % self.events.len();
|
||||||
|
event
|
||||||
|
}
|
||||||
|
|
||||||
|
fn has_event(&self) -> bool {
|
||||||
|
self.head != self.tail
|
||||||
|
}
|
||||||
|
}
|
||||||
2
crates/servers/input-server/src/adapters/mod.rs
Normal file
2
crates/servers/input-server/src/adapters/mod.rs
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
pub mod mock;
|
||||||
|
pub mod qemu_uart;
|
||||||
48
crates/servers/input-server/src/adapters/qemu_uart.rs
Normal file
48
crates/servers/input-server/src/adapters/qemu_uart.rs
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
use crate::input_device::InputDevice;
|
||||||
|
use crate::protocol::{InputEvent, Key};
|
||||||
|
|
||||||
|
const UART_BASE: usize = 0x10000000;
|
||||||
|
const UART_LSR: usize = 5;
|
||||||
|
|
||||||
|
pub struct UartInputDevice {
|
||||||
|
base: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl UartInputDevice {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self { base: UART_BASE }
|
||||||
|
}
|
||||||
|
|
||||||
|
fn read_byte(&self) -> Option<u8> {
|
||||||
|
let lsr = unsafe { core::ptr::read_volatile((self.base + UART_LSR) as *const u8) };
|
||||||
|
if lsr & 1 != 0 {
|
||||||
|
Some(unsafe { core::ptr::read_volatile(self.base as *const u8) })
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn byte_to_key(byte: u8) -> Key {
|
||||||
|
match byte {
|
||||||
|
b'w' | b'W' => Key::Up,
|
||||||
|
b's' | b'S' => Key::Down,
|
||||||
|
b'a' | b'A' => Key::Left,
|
||||||
|
b'd' | b'D' => Key::Right,
|
||||||
|
b'\r' | b'\n' => Key::Enter,
|
||||||
|
0x1B => Key::Escape,
|
||||||
|
c => Key::Char(c),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl InputDevice for UartInputDevice {
|
||||||
|
fn poll(&mut self) -> Option<InputEvent> {
|
||||||
|
let byte = self.read_byte()?;
|
||||||
|
Some(InputEvent::KeyDown(Self::byte_to_key(byte)))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn has_event(&self) -> bool {
|
||||||
|
let lsr = unsafe { core::ptr::read_volatile((self.base + UART_LSR) as *const u8) };
|
||||||
|
lsr & 1 != 0
|
||||||
|
}
|
||||||
|
}
|
||||||
6
crates/servers/input-server/src/input_device.rs
Normal file
6
crates/servers/input-server/src/input_device.rs
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
use crate::protocol::InputEvent;
|
||||||
|
|
||||||
|
pub trait InputDevice {
|
||||||
|
fn poll(&mut self) -> Option<InputEvent>;
|
||||||
|
fn has_event(&self) -> bool;
|
||||||
|
}
|
||||||
@@ -1,14 +1,10 @@
|
|||||||
pub fn add(left: u64, right: u64) -> u64 {
|
#![no_std]
|
||||||
left + right
|
|
||||||
}
|
pub mod adapters;
|
||||||
|
pub mod input_device;
|
||||||
|
pub mod protocol;
|
||||||
|
pub mod server;
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
#[path = "tests/mod.rs"]
|
||||||
use super::*;
|
mod tests;
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn it_works() {
|
|
||||||
let result = add(2, 2);
|
|
||||||
assert_eq!(result, 4);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
107
crates/servers/input-server/src/protocol.rs
Normal file
107
crates/servers/input-server/src/protocol.rs
Normal file
@@ -0,0 +1,107 @@
|
|||||||
|
use server_framework::{Decode, Encode};
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
pub enum Key {
|
||||||
|
Up,
|
||||||
|
Down,
|
||||||
|
Left,
|
||||||
|
Right,
|
||||||
|
Enter,
|
||||||
|
Escape,
|
||||||
|
Char(u8),
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
pub enum InputEvent {
|
||||||
|
KeyDown(Key),
|
||||||
|
KeyUp(Key),
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, PartialEq)]
|
||||||
|
pub enum InputRequest {
|
||||||
|
PollEvent,
|
||||||
|
WaitEvent,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, PartialEq)]
|
||||||
|
pub enum InputResponse {
|
||||||
|
Event(InputEvent),
|
||||||
|
NoEvent,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Encode for InputRequest {
|
||||||
|
fn encode(&self) -> [usize; 4] {
|
||||||
|
match self {
|
||||||
|
InputRequest::PollEvent => [0, 0, 0, 0],
|
||||||
|
InputRequest::WaitEvent => [1, 0, 0, 0],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Decode for InputRequest {
|
||||||
|
fn decode(words: [usize; 4]) -> Option<Self> {
|
||||||
|
match words[0] {
|
||||||
|
0 => Some(InputRequest::PollEvent),
|
||||||
|
1 => Some(InputRequest::WaitEvent),
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn encode_key(key: Key) -> (usize, usize) {
|
||||||
|
match key {
|
||||||
|
Key::Up => (0, 0),
|
||||||
|
Key::Down => (1, 0),
|
||||||
|
Key::Left => (2, 0),
|
||||||
|
Key::Right => (3, 0),
|
||||||
|
Key::Enter => (4, 0),
|
||||||
|
Key::Escape => (5, 0),
|
||||||
|
Key::Char(c) => (6, c as usize),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn decode_key(kind: usize, data: usize) -> Option<Key> {
|
||||||
|
match kind {
|
||||||
|
0 => Some(Key::Up),
|
||||||
|
1 => Some(Key::Down),
|
||||||
|
2 => Some(Key::Left),
|
||||||
|
3 => Some(Key::Right),
|
||||||
|
4 => Some(Key::Enter),
|
||||||
|
5 => Some(Key::Escape),
|
||||||
|
6 => Some(Key::Char(data as u8)),
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Encode for InputResponse {
|
||||||
|
fn encode(&self) -> [usize; 4] {
|
||||||
|
match self {
|
||||||
|
InputResponse::NoEvent => [0, 0, 0, 0],
|
||||||
|
InputResponse::Event(InputEvent::KeyDown(key)) => {
|
||||||
|
let (kind, data) = encode_key(*key);
|
||||||
|
[1, kind, data, 0]
|
||||||
|
}
|
||||||
|
InputResponse::Event(InputEvent::KeyUp(key)) => {
|
||||||
|
let (kind, data) = encode_key(*key);
|
||||||
|
[2, kind, data, 0]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Decode for InputResponse {
|
||||||
|
fn decode(words: [usize; 4]) -> Option<Self> {
|
||||||
|
match words[0] {
|
||||||
|
0 => Some(InputResponse::NoEvent),
|
||||||
|
1 => {
|
||||||
|
let key = decode_key(words[1], words[2])?;
|
||||||
|
Some(InputResponse::Event(InputEvent::KeyDown(key)))
|
||||||
|
}
|
||||||
|
2 => {
|
||||||
|
let key = decode_key(words[1], words[2])?;
|
||||||
|
Some(InputResponse::Event(InputEvent::KeyUp(key)))
|
||||||
|
}
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
36
crates/servers/input-server/src/server.rs
Normal file
36
crates/servers/input-server/src/server.rs
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
use server_framework::Server;
|
||||||
|
|
||||||
|
use crate::input_device::InputDevice;
|
||||||
|
use crate::protocol::{InputRequest, InputResponse};
|
||||||
|
|
||||||
|
pub struct InputServer<D: InputDevice> {
|
||||||
|
device: D,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<D: InputDevice> InputServer<D> {
|
||||||
|
pub fn new(device: D) -> Self {
|
||||||
|
Self { device }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn device(&self) -> &D {
|
||||||
|
&self.device
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<D: InputDevice> Server for InputServer<D> {
|
||||||
|
type Request = InputRequest;
|
||||||
|
type Response = InputResponse;
|
||||||
|
|
||||||
|
fn handle(&mut self, request: InputRequest) -> InputResponse {
|
||||||
|
match request {
|
||||||
|
InputRequest::PollEvent => match self.device.poll() {
|
||||||
|
Some(event) => InputResponse::Event(event),
|
||||||
|
None => InputResponse::NoEvent,
|
||||||
|
},
|
||||||
|
InputRequest::WaitEvent => match self.device.poll() {
|
||||||
|
Some(event) => InputResponse::Event(event),
|
||||||
|
None => InputResponse::NoEvent,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
2
crates/servers/input-server/src/tests/mod.rs
Normal file
2
crates/servers/input-server/src/tests/mod.rs
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
mod protocol;
|
||||||
|
mod server;
|
||||||
46
crates/servers/input-server/src/tests/protocol.rs
Normal file
46
crates/servers/input-server/src/tests/protocol.rs
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
use crate::protocol::*;
|
||||||
|
use server_framework::{Decode, Encode};
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn poll_request_roundtrip() {
|
||||||
|
let words = InputRequest::PollEvent.encode();
|
||||||
|
assert_eq!(InputRequest::decode(words), Some(InputRequest::PollEvent));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn wait_request_roundtrip() {
|
||||||
|
let words = InputRequest::WaitEvent.encode();
|
||||||
|
assert_eq!(InputRequest::decode(words), Some(InputRequest::WaitEvent));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn key_down_response_roundtrip() {
|
||||||
|
let resp = InputResponse::Event(InputEvent::KeyDown(Key::Up));
|
||||||
|
let words = resp.encode();
|
||||||
|
assert_eq!(InputResponse::decode(words), Some(resp));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn key_up_response_roundtrip() {
|
||||||
|
let resp = InputResponse::Event(InputEvent::KeyUp(Key::Left));
|
||||||
|
let words = resp.encode();
|
||||||
|
assert_eq!(InputResponse::decode(words), Some(resp));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn char_key_roundtrip() {
|
||||||
|
let resp = InputResponse::Event(InputEvent::KeyDown(Key::Char(b'x')));
|
||||||
|
let words = resp.encode();
|
||||||
|
assert_eq!(InputResponse::decode(words), Some(resp));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn no_event_roundtrip() {
|
||||||
|
let words = InputResponse::NoEvent.encode();
|
||||||
|
assert_eq!(InputResponse::decode(words), Some(InputResponse::NoEvent));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn invalid_request_returns_none() {
|
||||||
|
assert!(InputRequest::decode([99, 0, 0, 0]).is_none());
|
||||||
|
}
|
||||||
51
crates/servers/input-server/src/tests/server.rs
Normal file
51
crates/servers/input-server/src/tests/server.rs
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
use crate::adapters::mock::MockInputDevice;
|
||||||
|
use crate::protocol::*;
|
||||||
|
use crate::server::InputServer;
|
||||||
|
use server_framework::Server;
|
||||||
|
|
||||||
|
fn test_server() -> InputServer<MockInputDevice> {
|
||||||
|
InputServer::new(MockInputDevice::new())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn poll_with_no_events_returns_no_event() {
|
||||||
|
let mut server = test_server();
|
||||||
|
let resp = server.handle(InputRequest::PollEvent);
|
||||||
|
assert_eq!(resp, InputResponse::NoEvent);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn poll_returns_queued_event() {
|
||||||
|
let mut device = MockInputDevice::new();
|
||||||
|
device.push_event(InputEvent::KeyDown(Key::Up));
|
||||||
|
let mut server = InputServer::new(device);
|
||||||
|
|
||||||
|
let resp = server.handle(InputRequest::PollEvent);
|
||||||
|
assert_eq!(resp, InputResponse::Event(InputEvent::KeyDown(Key::Up)));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn poll_drains_events_in_order() {
|
||||||
|
let mut device = MockInputDevice::new();
|
||||||
|
device.push_event(InputEvent::KeyDown(Key::Left));
|
||||||
|
device.push_event(InputEvent::KeyDown(Key::Right));
|
||||||
|
let mut server = InputServer::new(device);
|
||||||
|
|
||||||
|
let r1 = server.handle(InputRequest::PollEvent);
|
||||||
|
let r2 = server.handle(InputRequest::PollEvent);
|
||||||
|
let r3 = server.handle(InputRequest::PollEvent);
|
||||||
|
|
||||||
|
assert_eq!(r1, InputResponse::Event(InputEvent::KeyDown(Key::Left)));
|
||||||
|
assert_eq!(r2, InputResponse::Event(InputEvent::KeyDown(Key::Right)));
|
||||||
|
assert_eq!(r3, InputResponse::NoEvent);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn char_event_preserved() {
|
||||||
|
let mut device = MockInputDevice::new();
|
||||||
|
device.push_event(InputEvent::KeyDown(Key::Char(b'z')));
|
||||||
|
let mut server = InputServer::new(device);
|
||||||
|
|
||||||
|
let resp = server.handle(InputRequest::PollEvent);
|
||||||
|
assert_eq!(resp, InputResponse::Event(InputEvent::KeyDown(Key::Char(b'z'))));
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user