add all crates: domain, protocol, application, client, adapters, ESP32 firmware
Server: domain (entities, value objects, ports), protocol (postcard wire types), application (config service, data projection), adapters (config-memory, tcp-server), bootstrap (composition root with fake data). Client: client-domain (layout engine, render tree, HAL ports), client-application (message handling, repaint commands), adapters (tcp-client, display-terminal), client-desktop (end-to-end working). ESP32: client-esp32 firmware with ILI9341 display over SPI, WiFi networking. Display test verified on hardware — landscape orientation, text rendering works. 60 workspace tests, all passing.
This commit is contained in:
82
crates/adapters/tcp-client/src/lib.rs
Normal file
82
crates/adapters/tcp-client/src/lib.rs
Normal file
@@ -0,0 +1,82 @@
|
||||
use std::io::{Read, Write};
|
||||
use std::net::TcpStream;
|
||||
use std::time::Duration;
|
||||
use client_domain::NetworkPort;
|
||||
use protocol::MAX_FRAME_SIZE;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum TcpClientError {
|
||||
Io(std::io::Error),
|
||||
NotConnected,
|
||||
FrameTooLarge(usize),
|
||||
}
|
||||
|
||||
impl std::fmt::Display for TcpClientError {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
TcpClientError::Io(e) => write!(f, "io: {e}"),
|
||||
TcpClientError::NotConnected => write!(f, "not connected"),
|
||||
TcpClientError::FrameTooLarge(n) => write!(f, "frame too large: {n}"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct StdTcpClient {
|
||||
stream: Option<TcpStream>,
|
||||
}
|
||||
|
||||
impl StdTcpClient {
|
||||
pub fn new() -> Self {
|
||||
Self { stream: None }
|
||||
}
|
||||
}
|
||||
|
||||
impl NetworkPort for StdTcpClient {
|
||||
type Error = TcpClientError;
|
||||
|
||||
fn connect(&mut self, addr: &str) -> Result<(), Self::Error> {
|
||||
let stream = TcpStream::connect(addr).map_err(TcpClientError::Io)?;
|
||||
stream.set_nonblocking(true).map_err(TcpClientError::Io)?;
|
||||
stream.set_read_timeout(Some(Duration::from_millis(10))).map_err(TcpClientError::Io)?;
|
||||
self.stream = Some(stream);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn disconnect(&mut self) -> Result<(), Self::Error> {
|
||||
self.stream = None;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn send(&mut self, data: &[u8]) -> Result<(), Self::Error> {
|
||||
let stream = self.stream.as_mut().ok_or(TcpClientError::NotConnected)?;
|
||||
stream.write_all(data).map_err(TcpClientError::Io)
|
||||
}
|
||||
|
||||
fn receive(&mut self) -> Result<Option<Vec<u8>>, Self::Error> {
|
||||
let stream = self.stream.as_mut().ok_or(TcpClientError::NotConnected)?;
|
||||
|
||||
let mut len_buf = [0u8; 4];
|
||||
match stream.read_exact(&mut len_buf) {
|
||||
Ok(()) => {}
|
||||
Err(e) if e.kind() == std::io::ErrorKind::WouldBlock => return Ok(None),
|
||||
Err(e) if e.kind() == std::io::ErrorKind::TimedOut => return Ok(None),
|
||||
Err(e) => return Err(TcpClientError::Io(e)),
|
||||
}
|
||||
|
||||
let len = u32::from_be_bytes(len_buf) as usize;
|
||||
if len > MAX_FRAME_SIZE {
|
||||
return Err(TcpClientError::FrameTooLarge(len));
|
||||
}
|
||||
|
||||
let mut payload = vec![0u8; len];
|
||||
stream.set_nonblocking(false).map_err(TcpClientError::Io)?;
|
||||
stream.read_exact(&mut payload).map_err(TcpClientError::Io)?;
|
||||
stream.set_nonblocking(true).map_err(TcpClientError::Io)?;
|
||||
|
||||
Ok(Some(payload))
|
||||
}
|
||||
|
||||
fn is_connected(&self) -> bool {
|
||||
self.stream.is_some()
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user