DataSourceConfig refactored to enum: External/Clock/StaticText. Clock generates formatted time via chrono, static text emits configured string. ESP32: connection status indicator (green/red dot bottom-right), per-widget clear before redraw, RenderEvent enum for local + server messages. Polling uses DataUpdate instead of ScreenUpdate to avoid wiping widget state. Empty mappings passthrough raw source data for internal sources.
81 lines
2.3 KiB
Rust
81 lines
2.3 KiB
Rust
use crate::value_objects::{DisplayHint, KeyMapping, Value, WidgetState};
|
|
use std::collections::BTreeMap;
|
|
|
|
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 has_mappings = self.mappings.iter().any(|m| !m.source_path.is_empty());
|
|
if !has_mappings {
|
|
let data = match raw {
|
|
Value::Object(map) => map.clone(),
|
|
_ => BTreeMap::new(),
|
|
};
|
|
return WidgetState { data, error: None };
|
|
}
|
|
|
|
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,
|
|
}
|
|
}
|
|
}
|