feat(display-server): add DisplayServer<F: Framebuffer> with protocol, mock adapter, 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 }
|
||||||
|
|||||||
66
crates/servers/display-server/src/adapters/mock.rs
Normal file
66
crates/servers/display-server/src/adapters/mock.rs
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
use crate::framebuffer::Framebuffer;
|
||||||
|
|
||||||
|
pub struct MockFramebuffer {
|
||||||
|
width: u16,
|
||||||
|
height: u16,
|
||||||
|
pixels: [(u16, u16, u32); 4096],
|
||||||
|
pixel_count: usize,
|
||||||
|
flushed: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl MockFramebuffer {
|
||||||
|
pub fn new(width: u16, height: u16) -> Self {
|
||||||
|
Self {
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
pixels: [(0, 0, 0); 4096],
|
||||||
|
pixel_count: 0,
|
||||||
|
flushed: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn pixel_count(&self) -> usize {
|
||||||
|
self.pixel_count
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn was_flushed(&self) -> bool {
|
||||||
|
self.flushed
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn pixel_at(&self, index: usize) -> (u16, u16, u32) {
|
||||||
|
self.pixels[index]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Framebuffer for MockFramebuffer {
|
||||||
|
fn width(&self) -> u16 {
|
||||||
|
self.width
|
||||||
|
}
|
||||||
|
|
||||||
|
fn height(&self) -> u16 {
|
||||||
|
self.height
|
||||||
|
}
|
||||||
|
|
||||||
|
fn write_pixel(&mut self, x: u16, y: u16, color: u32) {
|
||||||
|
if self.pixel_count < self.pixels.len() {
|
||||||
|
self.pixels[self.pixel_count] = (x, y, color);
|
||||||
|
self.pixel_count += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn fill_rect(&mut self, x: u16, y: u16, w: u16, h: u16, color: u32) {
|
||||||
|
for dy in 0..h {
|
||||||
|
for dx in 0..w {
|
||||||
|
self.write_pixel(x + dx, y + dy, color);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn clear(&mut self, _color: u32) {
|
||||||
|
self.pixel_count = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn flush(&mut self) {
|
||||||
|
self.flushed = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
1
crates/servers/display-server/src/adapters/mod.rs
Normal file
1
crates/servers/display-server/src/adapters/mod.rs
Normal file
@@ -0,0 +1 @@
|
|||||||
|
pub mod mock;
|
||||||
8
crates/servers/display-server/src/framebuffer.rs
Normal file
8
crates/servers/display-server/src/framebuffer.rs
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
pub trait Framebuffer {
|
||||||
|
fn width(&self) -> u16;
|
||||||
|
fn height(&self) -> u16;
|
||||||
|
fn write_pixel(&mut self, x: u16, y: u16, color: u32);
|
||||||
|
fn fill_rect(&mut self, x: u16, y: u16, w: u16, h: u16, color: u32);
|
||||||
|
fn clear(&mut self, color: u32);
|
||||||
|
fn flush(&mut self);
|
||||||
|
}
|
||||||
@@ -1,14 +1,10 @@
|
|||||||
pub fn add(left: u64, right: u64) -> u64 {
|
#![no_std]
|
||||||
left + right
|
|
||||||
}
|
pub mod adapters;
|
||||||
|
pub mod framebuffer;
|
||||||
|
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
76
crates/servers/display-server/src/protocol.rs
Normal file
76
crates/servers/display-server/src/protocol.rs
Normal file
@@ -0,0 +1,76 @@
|
|||||||
|
use server_framework::{Decode, Encode};
|
||||||
|
|
||||||
|
#[derive(Debug, PartialEq)]
|
||||||
|
pub enum DisplayRequest {
|
||||||
|
DrawRect { x: u16, y: u16, w: u16, h: u16, color: u32 },
|
||||||
|
Clear { color: u32 },
|
||||||
|
Flush,
|
||||||
|
GetDimensions,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, PartialEq)]
|
||||||
|
pub enum DisplayResponse {
|
||||||
|
Ok,
|
||||||
|
Dimensions { width: u16, height: u16 },
|
||||||
|
Error,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Encode for DisplayRequest {
|
||||||
|
fn encode(&self) -> [usize; 4] {
|
||||||
|
match self {
|
||||||
|
DisplayRequest::DrawRect { x, y, w, h, color } => {
|
||||||
|
let xy = (*x as usize) | ((*y as usize) << 16);
|
||||||
|
let wh = (*w as usize) | ((*h as usize) << 16);
|
||||||
|
[0, xy, wh, *color as usize]
|
||||||
|
}
|
||||||
|
DisplayRequest::Clear { color } => [1, *color as usize, 0, 0],
|
||||||
|
DisplayRequest::Flush => [2, 0, 0, 0],
|
||||||
|
DisplayRequest::GetDimensions => [3, 0, 0, 0],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Decode for DisplayRequest {
|
||||||
|
fn decode(words: [usize; 4]) -> Option<Self> {
|
||||||
|
match words[0] {
|
||||||
|
0 => {
|
||||||
|
let x = (words[1] & 0xFFFF) as u16;
|
||||||
|
let y = ((words[1] >> 16) & 0xFFFF) as u16;
|
||||||
|
let w = (words[2] & 0xFFFF) as u16;
|
||||||
|
let h = ((words[2] >> 16) & 0xFFFF) as u16;
|
||||||
|
let color = words[3] as u32;
|
||||||
|
Some(DisplayRequest::DrawRect { x, y, w, h, color })
|
||||||
|
}
|
||||||
|
1 => Some(DisplayRequest::Clear { color: words[1] as u32 }),
|
||||||
|
2 => Some(DisplayRequest::Flush),
|
||||||
|
3 => Some(DisplayRequest::GetDimensions),
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Encode for DisplayResponse {
|
||||||
|
fn encode(&self) -> [usize; 4] {
|
||||||
|
match self {
|
||||||
|
DisplayResponse::Ok => [0, 0, 0, 0],
|
||||||
|
DisplayResponse::Dimensions { width, height } => {
|
||||||
|
[1, *width as usize, *height as usize, 0]
|
||||||
|
}
|
||||||
|
DisplayResponse::Error => [2, 0, 0, 0],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Decode for DisplayResponse {
|
||||||
|
fn decode(words: [usize; 4]) -> Option<Self> {
|
||||||
|
match words[0] {
|
||||||
|
0 => Some(DisplayResponse::Ok),
|
||||||
|
1 => Some(DisplayResponse::Dimensions {
|
||||||
|
width: words[1] as u16,
|
||||||
|
height: words[2] as u16,
|
||||||
|
}),
|
||||||
|
2 => Some(DisplayResponse::Error),
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
44
crates/servers/display-server/src/server.rs
Normal file
44
crates/servers/display-server/src/server.rs
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
use server_framework::Server;
|
||||||
|
|
||||||
|
use crate::framebuffer::Framebuffer;
|
||||||
|
use crate::protocol::{DisplayRequest, DisplayResponse};
|
||||||
|
|
||||||
|
pub struct DisplayServer<F: Framebuffer> {
|
||||||
|
fb: F,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<F: Framebuffer> DisplayServer<F> {
|
||||||
|
pub fn new(fb: F) -> Self {
|
||||||
|
Self { fb }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn framebuffer(&self) -> &F {
|
||||||
|
&self.fb
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<F: Framebuffer> Server for DisplayServer<F> {
|
||||||
|
type Request = DisplayRequest;
|
||||||
|
type Response = DisplayResponse;
|
||||||
|
|
||||||
|
fn handle(&mut self, request: DisplayRequest) -> DisplayResponse {
|
||||||
|
match request {
|
||||||
|
DisplayRequest::DrawRect { x, y, w, h, color } => {
|
||||||
|
self.fb.fill_rect(x, y, w, h, color);
|
||||||
|
DisplayResponse::Ok
|
||||||
|
}
|
||||||
|
DisplayRequest::Clear { color } => {
|
||||||
|
self.fb.clear(color);
|
||||||
|
DisplayResponse::Ok
|
||||||
|
}
|
||||||
|
DisplayRequest::Flush => {
|
||||||
|
self.fb.flush();
|
||||||
|
DisplayResponse::Ok
|
||||||
|
}
|
||||||
|
DisplayRequest::GetDimensions => DisplayResponse::Dimensions {
|
||||||
|
width: self.fb.width(),
|
||||||
|
height: self.fb.height(),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
2
crates/servers/display-server/src/tests/mod.rs
Normal file
2
crates/servers/display-server/src/tests/mod.rs
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
mod protocol;
|
||||||
|
mod server;
|
||||||
54
crates/servers/display-server/src/tests/protocol.rs
Normal file
54
crates/servers/display-server/src/tests/protocol.rs
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
use crate::protocol::{DisplayRequest, DisplayResponse};
|
||||||
|
use server_framework::{Decode, Encode};
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn draw_rect_roundtrip() {
|
||||||
|
let req = DisplayRequest::DrawRect {
|
||||||
|
x: 10,
|
||||||
|
y: 20,
|
||||||
|
w: 100,
|
||||||
|
h: 50,
|
||||||
|
color: 0xFF0000,
|
||||||
|
};
|
||||||
|
let words = req.encode();
|
||||||
|
let decoded = DisplayRequest::decode(words).unwrap();
|
||||||
|
assert_eq!(decoded, req);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn clear_roundtrip() {
|
||||||
|
let req = DisplayRequest::Clear { color: 0x00FF00 };
|
||||||
|
let words = req.encode();
|
||||||
|
let decoded = DisplayRequest::decode(words).unwrap();
|
||||||
|
assert_eq!(decoded, req);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn flush_roundtrip() {
|
||||||
|
let words = DisplayRequest::Flush.encode();
|
||||||
|
let decoded = DisplayRequest::decode(words).unwrap();
|
||||||
|
assert_eq!(decoded, DisplayRequest::Flush);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn get_dimensions_roundtrip() {
|
||||||
|
let words = DisplayRequest::GetDimensions.encode();
|
||||||
|
let decoded = DisplayRequest::decode(words).unwrap();
|
||||||
|
assert_eq!(decoded, DisplayRequest::GetDimensions);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn dimensions_response_roundtrip() {
|
||||||
|
let resp = DisplayResponse::Dimensions {
|
||||||
|
width: 320,
|
||||||
|
height: 240,
|
||||||
|
};
|
||||||
|
let words = resp.encode();
|
||||||
|
let decoded = DisplayResponse::decode(words).unwrap();
|
||||||
|
assert_eq!(decoded, resp);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn invalid_request_returns_none() {
|
||||||
|
assert!(DisplayRequest::decode([99, 0, 0, 0]).is_none());
|
||||||
|
}
|
||||||
75
crates/servers/display-server/src/tests/server.rs
Normal file
75
crates/servers/display-server/src/tests/server.rs
Normal file
@@ -0,0 +1,75 @@
|
|||||||
|
use crate::adapters::mock::MockFramebuffer;
|
||||||
|
use crate::protocol::{DisplayRequest, DisplayResponse};
|
||||||
|
use crate::server::DisplayServer;
|
||||||
|
use server_framework::Server;
|
||||||
|
|
||||||
|
fn test_server() -> DisplayServer<MockFramebuffer> {
|
||||||
|
DisplayServer::new(MockFramebuffer::new(320, 240))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn draw_rect_writes_pixels() {
|
||||||
|
let mut server = test_server();
|
||||||
|
let resp = server.handle(DisplayRequest::DrawRect {
|
||||||
|
x: 0,
|
||||||
|
y: 0,
|
||||||
|
w: 4,
|
||||||
|
h: 3,
|
||||||
|
color: 0xFF,
|
||||||
|
});
|
||||||
|
assert_eq!(resp, DisplayResponse::Ok);
|
||||||
|
assert_eq!(server.framebuffer().pixel_count(), 12);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn clear_resets_pixels() {
|
||||||
|
let mut server = test_server();
|
||||||
|
server.handle(DisplayRequest::DrawRect {
|
||||||
|
x: 0,
|
||||||
|
y: 0,
|
||||||
|
w: 10,
|
||||||
|
h: 10,
|
||||||
|
color: 0xFF,
|
||||||
|
});
|
||||||
|
server.handle(DisplayRequest::Clear { color: 0 });
|
||||||
|
assert_eq!(server.framebuffer().pixel_count(), 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn flush_sets_flag() {
|
||||||
|
let mut server = test_server();
|
||||||
|
assert!(!server.framebuffer().was_flushed());
|
||||||
|
server.handle(DisplayRequest::Flush);
|
||||||
|
assert!(server.framebuffer().was_flushed());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn get_dimensions_returns_size() {
|
||||||
|
let mut server = test_server();
|
||||||
|
let resp = server.handle(DisplayRequest::GetDimensions);
|
||||||
|
assert_eq!(
|
||||||
|
resp,
|
||||||
|
DisplayResponse::Dimensions {
|
||||||
|
width: 320,
|
||||||
|
height: 240
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn draw_rect_correct_pixel_positions() {
|
||||||
|
let mut server = test_server();
|
||||||
|
server.handle(DisplayRequest::DrawRect {
|
||||||
|
x: 5,
|
||||||
|
y: 10,
|
||||||
|
w: 2,
|
||||||
|
h: 2,
|
||||||
|
color: 0xAB,
|
||||||
|
});
|
||||||
|
let fb = server.framebuffer();
|
||||||
|
assert_eq!(fb.pixel_count(), 4);
|
||||||
|
assert_eq!(fb.pixel_at(0), (5, 10, 0xAB));
|
||||||
|
assert_eq!(fb.pixel_at(1), (6, 10, 0xAB));
|
||||||
|
assert_eq!(fb.pixel_at(2), (5, 11, 0xAB));
|
||||||
|
assert_eq!(fb.pixel_at(3), (6, 11, 0xAB));
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user