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.
71 lines
1.9 KiB
Rust
71 lines
1.9 KiB
Rust
use std::collections::BTreeMap;
|
|
use crate::value_objects::{DisplayHint, KeyMapping, Value, WidgetState};
|
|
|
|
pub type WidgetId = u16;
|
|
pub type DataSourceId = u16;
|
|
|
|
const DEFAULT_MAX_DATA_SIZE: u16 = 2048;
|
|
|
|
#[derive(Debug, Clone)]
|
|
pub struct WidgetConfig {
|
|
pub id: WidgetId,
|
|
pub name: String,
|
|
pub display_hint: DisplayHint,
|
|
pub data_source_id: DataSourceId,
|
|
pub mappings: Vec<KeyMapping>,
|
|
pub max_data_size: u16,
|
|
}
|
|
|
|
impl WidgetConfig {
|
|
pub fn new(
|
|
id: WidgetId,
|
|
name: String,
|
|
display_hint: DisplayHint,
|
|
data_source_id: DataSourceId,
|
|
mappings: Vec<KeyMapping>,
|
|
) -> Self {
|
|
Self {
|
|
id,
|
|
name,
|
|
display_hint,
|
|
data_source_id,
|
|
mappings,
|
|
max_data_size: DEFAULT_MAX_DATA_SIZE,
|
|
}
|
|
}
|
|
|
|
pub fn extract(&self, raw: &Value) -> WidgetState {
|
|
let budget = self.max_data_size as usize;
|
|
let mut used = 0usize;
|
|
let mut data = BTreeMap::new();
|
|
|
|
for mapping in &self.mappings {
|
|
if let Some((key, value)) = mapping.extract(raw) {
|
|
let key_cost = key.len();
|
|
let remaining = budget.saturating_sub(used + key_cost);
|
|
let value = Self::truncate_value(value, remaining);
|
|
used += key_cost + value.estimated_size();
|
|
data.insert(key, value);
|
|
if used >= budget {
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
WidgetState { data, error: None }
|
|
}
|
|
|
|
fn truncate_value(value: Value, max_bytes: usize) -> Value {
|
|
match value {
|
|
Value::String(s) if s.len() > max_bytes => {
|
|
let truncated: String = s.char_indices()
|
|
.take_while(|(i, _)| *i < max_bytes)
|
|
.map(|(_, c)| c)
|
|
.collect();
|
|
Value::String(truncated)
|
|
}
|
|
other => other,
|
|
}
|
|
}
|
|
}
|