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,110 @@
use std::collections::HashMap;
use domain::LayoutNode;
use client_domain::{BoundingBox, LayoutEngine, RenderTree};
use protocol::{
ServerMessage, WidgetDescriptor, WireDisplayHint, WireWidgetState, WireLayoutNode,
};
pub struct ClientApp {
screen: BoundingBox,
render_tree: Option<RenderTree>,
widget_states: HashMap<u16, (WireDisplayHint, WireWidgetState)>,
}
#[derive(Debug, Clone, PartialEq)]
pub struct RepaintCommand {
pub widget_id: u16,
pub bounds: BoundingBox,
pub display_hint: WireDisplayHint,
pub state: WireWidgetState,
}
impl ClientApp {
pub fn new(screen: BoundingBox) -> Self {
Self {
screen,
render_tree: None,
widget_states: HashMap::new(),
}
}
pub fn handle_message(&mut self, msg: ServerMessage) -> Vec<RepaintCommand> {
match msg {
ServerMessage::ScreenUpdate { layout, widgets } => {
self.handle_screen_update(layout, widgets)
}
ServerMessage::DataUpdate { widgets } => {
self.handle_data_update(widgets)
}
ServerMessage::Heartbeat => Vec::new(),
}
}
fn handle_screen_update(
&mut self,
wire_layout: WireLayoutNode,
widgets: Vec<WidgetDescriptor>,
) -> Vec<RepaintCommand> {
let layout: LayoutNode = wire_layout.into();
let new_tree = LayoutEngine::compute(&layout, self.screen);
self.widget_states.clear();
for w in &widgets {
self.widget_states.insert(w.id, (w.display_hint.clone(), w.state.clone()));
}
let repaints = self.build_repaints_for_all(&new_tree);
self.render_tree = Some(new_tree);
repaints
}
fn handle_data_update(
&mut self,
widgets: Vec<WidgetDescriptor>,
) -> Vec<RepaintCommand> {
let tree = match &self.render_tree {
Some(t) => t,
None => return Vec::new(),
};
let mut repaints = Vec::new();
for w in widgets {
let changed = self.widget_states
.get(&w.id)
.map_or(true, |(_, prev_state)| *prev_state != w.state);
if changed {
if let Some(bounds) = tree.get_widget_bounds(w.id) {
repaints.push(RepaintCommand {
widget_id: w.id,
bounds: *bounds,
display_hint: w.display_hint.clone(),
state: w.state.clone(),
});
}
self.widget_states.insert(w.id, (w.display_hint, w.state));
}
}
repaints
}
fn build_repaints_for_all(&self, tree: &RenderTree) -> Vec<RepaintCommand> {
let mut repaints = Vec::new();
for (id, (hint, state)) in &self.widget_states {
if let Some(bounds) = tree.get_widget_bounds(*id) {
repaints.push(RepaintCommand {
widget_id: *id,
bounds: *bounds,
display_hint: hint.clone(),
state: state.clone(),
});
}
}
repaints.sort_by_key(|r| r.widget_id);
repaints
}
}

View File

@@ -0,0 +1,3 @@
mod client_app;
pub use client_app::{ClientApp, RepaintCommand};