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:
2026-06-18 21:43:59 +02:00
parent 6ad76b98a2
commit 557cceb498
83 changed files with 5844 additions and 1 deletions

View File

@@ -0,0 +1,8 @@
[package]
name = "tcp-client"
version = "0.1.0"
edition = "2024"
[dependencies]
client-domain.workspace = true
protocol.workspace = true

View 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()
}
}