arch: push wire types out of ClientApp, extract event_service, cleanup dead code
- ClientApp stores domain types, RepaintCommand carries DisplayHint + Vec<(String,Value)> - adapters no longer convert Wire→Domain (eliminated duplication in esp32 + desktop) - event_service in application layer handles LayoutChanged/WebhookDataReceived/ThemeChanged - bootstrap event_handler reduced to 10-line dispatcher - polling_service reuses event_service::apply_and_broadcast (deduplicated broadcast pattern) - AppState.config_service() replaces 11 inline ConfigService::new() calls - delete unused poll_interval_secs parameter chain - delete unused StoragePort/ClientConfig (zero implementations)
This commit is contained in:
@@ -2,7 +2,6 @@
|
|||||||
KFRAME_DATABASE_URL=sqlite:kframe.db?mode=rwc
|
KFRAME_DATABASE_URL=sqlite:kframe.db?mode=rwc
|
||||||
KFRAME_TCP_ADDR=0.0.0.0:2699
|
KFRAME_TCP_ADDR=0.0.0.0:2699
|
||||||
KFRAME_HTTP_ADDR=0.0.0.0:3000
|
KFRAME_HTTP_ADDR=0.0.0.0:3000
|
||||||
KFRAME_POLL_INTERVAL_SECS=5
|
|
||||||
|
|
||||||
# Auth (required)
|
# Auth (required)
|
||||||
JWT_SECRET=change-me-to-a-random-secret
|
JWT_SECRET=change-me-to-a-random-secret
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
pub mod extractors;
|
pub mod extractors;
|
||||||
mod routes;
|
mod routes;
|
||||||
|
|
||||||
|
use application::ConfigService;
|
||||||
use axum::Router;
|
use axum::Router;
|
||||||
use domain::{
|
use domain::{
|
||||||
AuthPort, BroadcastPort, ClientRegistry, ConfigRepository, EventPublisher, PasswordHashPort,
|
AuthPort, BroadcastPort, ClientRegistry, ConfigRepository, EventPublisher, PasswordHashPort,
|
||||||
@@ -36,6 +37,18 @@ impl<C, E, W, B, R, A, H> Clone for AppState<C, E, W, B, R, A, H> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl<C, E, W, B, R, A, H> AppState<C, E, W, B, R, A, H>
|
||||||
|
where
|
||||||
|
C: ConfigRepository,
|
||||||
|
C::Error: std::fmt::Debug,
|
||||||
|
E: EventPublisher,
|
||||||
|
E::Error: std::fmt::Debug,
|
||||||
|
{
|
||||||
|
pub fn config_service(&self) -> ConfigService<'_, C, E> {
|
||||||
|
ConfigService::new(self.config.as_ref(), self.events.as_ref())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub fn router<C, E, W, B, R, A, H>(state: AppState<C, E, W, B, R, A, H>) -> Router
|
pub fn router<C, E, W, B, R, A, H>(state: AppState<C, E, W, B, R, A, H>) -> Router
|
||||||
where
|
where
|
||||||
C: ConfigRepository + UserRepository + WidgetStateCache + Send + Sync + 'static,
|
C: ConfigRepository + UserRepository + WidgetStateCache + Send + Sync + 'static,
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
use crate::AppState;
|
use crate::AppState;
|
||||||
use crate::extractors::AuthUser;
|
use crate::extractors::AuthUser;
|
||||||
use api_types::DataSourceDto;
|
use api_types::DataSourceDto;
|
||||||
use application::ConfigService;
|
|
||||||
use axum::{
|
use axum::{
|
||||||
extract::{Path, State},
|
extract::{Path, State},
|
||||||
http::StatusCode,
|
http::StatusCode,
|
||||||
@@ -65,7 +65,7 @@ where
|
|||||||
let source = body
|
let source = body
|
||||||
.into_domain()
|
.into_domain()
|
||||||
.map_err(|e| (StatusCode::BAD_REQUEST, e))?;
|
.map_err(|e| (StatusCode::BAD_REQUEST, e))?;
|
||||||
let svc = ConfigService::new(state.config.as_ref(), state.events.as_ref());
|
let svc = state.config_service();
|
||||||
svc.create_data_source(source)
|
svc.create_data_source(source)
|
||||||
.await
|
.await
|
||||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("{e}")))?;
|
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("{e}")))?;
|
||||||
@@ -87,7 +87,7 @@ where
|
|||||||
let source = body
|
let source = body
|
||||||
.into_domain()
|
.into_domain()
|
||||||
.map_err(|e| (StatusCode::BAD_REQUEST, e))?;
|
.map_err(|e| (StatusCode::BAD_REQUEST, e))?;
|
||||||
let svc = ConfigService::new(state.config.as_ref(), state.events.as_ref());
|
let svc = state.config_service();
|
||||||
svc.update_data_source(source)
|
svc.update_data_source(source)
|
||||||
.await
|
.await
|
||||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("{e}")))?;
|
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("{e}")))?;
|
||||||
@@ -105,7 +105,7 @@ where
|
|||||||
E: EventPublisher,
|
E: EventPublisher,
|
||||||
E::Error: std::fmt::Debug,
|
E::Error: std::fmt::Debug,
|
||||||
{
|
{
|
||||||
let svc = ConfigService::new(state.config.as_ref(), state.events.as_ref());
|
let svc = state.config_service();
|
||||||
svc.delete_data_source(id)
|
svc.delete_data_source(id)
|
||||||
.await
|
.await
|
||||||
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
|
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
use crate::AppState;
|
use crate::AppState;
|
||||||
use crate::extractors::AuthUser;
|
use crate::extractors::AuthUser;
|
||||||
use api_types::LayoutDto;
|
use api_types::LayoutDto;
|
||||||
use application::ConfigService;
|
|
||||||
use axum::{extract::State, http::StatusCode, response::Json};
|
use axum::{extract::State, http::StatusCode, response::Json};
|
||||||
use domain::{ConfigRepository, EventPublisher};
|
use domain::{ConfigRepository, EventPublisher};
|
||||||
|
|
||||||
@@ -39,7 +39,7 @@ where
|
|||||||
let layout = body
|
let layout = body
|
||||||
.into_domain()
|
.into_domain()
|
||||||
.map_err(|e| (StatusCode::BAD_REQUEST, e))?;
|
.map_err(|e| (StatusCode::BAD_REQUEST, e))?;
|
||||||
let svc = ConfigService::new(state.config.as_ref(), state.events.as_ref());
|
let svc = state.config_service();
|
||||||
svc.update_layout(layout)
|
svc.update_layout(layout)
|
||||||
.await
|
.await
|
||||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("{e}")))?;
|
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("{e}")))?;
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
use crate::AppState;
|
use crate::AppState;
|
||||||
use crate::extractors::AuthUser;
|
use crate::extractors::AuthUser;
|
||||||
use api_types::{CreatePresetDto, PresetDto};
|
use api_types::{CreatePresetDto, PresetDto};
|
||||||
use application::ConfigService;
|
|
||||||
use axum::{
|
use axum::{
|
||||||
extract::{Path, State},
|
extract::{Path, State},
|
||||||
http::StatusCode,
|
http::StatusCode,
|
||||||
@@ -65,7 +65,7 @@ where
|
|||||||
let preset = body
|
let preset = body
|
||||||
.into_domain()
|
.into_domain()
|
||||||
.map_err(|e| (StatusCode::BAD_REQUEST, e))?;
|
.map_err(|e| (StatusCode::BAD_REQUEST, e))?;
|
||||||
let svc = ConfigService::new(state.config.as_ref(), state.events.as_ref());
|
let svc = state.config_service();
|
||||||
svc.save_preset(preset)
|
svc.save_preset(preset)
|
||||||
.await
|
.await
|
||||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("{e}")))?;
|
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("{e}")))?;
|
||||||
@@ -83,7 +83,7 @@ where
|
|||||||
E: EventPublisher,
|
E: EventPublisher,
|
||||||
E::Error: std::fmt::Debug,
|
E::Error: std::fmt::Debug,
|
||||||
{
|
{
|
||||||
let svc = ConfigService::new(state.config.as_ref(), state.events.as_ref());
|
let svc = state.config_service();
|
||||||
svc.delete_preset(id)
|
svc.delete_preset(id)
|
||||||
.await
|
.await
|
||||||
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
|
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
|
||||||
@@ -101,7 +101,7 @@ where
|
|||||||
E: EventPublisher,
|
E: EventPublisher,
|
||||||
E::Error: std::fmt::Debug,
|
E::Error: std::fmt::Debug,
|
||||||
{
|
{
|
||||||
let svc = ConfigService::new(state.config.as_ref(), state.events.as_ref());
|
let svc = state.config_service();
|
||||||
svc.load_preset(id)
|
svc.load_preset(id)
|
||||||
.await
|
.await
|
||||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("{e}")))?;
|
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("{e}")))?;
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
use crate::AppState;
|
use crate::AppState;
|
||||||
use crate::extractors::AuthUser;
|
use crate::extractors::AuthUser;
|
||||||
use api_types::ThemeDto;
|
use api_types::ThemeDto;
|
||||||
use application::ConfigService;
|
|
||||||
use axum::{extract::State, http::StatusCode, response::Json};
|
use axum::{extract::State, http::StatusCode, response::Json};
|
||||||
use domain::{ConfigRepository, EventPublisher};
|
use domain::{ConfigRepository, EventPublisher};
|
||||||
|
|
||||||
@@ -38,7 +38,7 @@ where
|
|||||||
E::Error: std::fmt::Debug,
|
E::Error: std::fmt::Debug,
|
||||||
{
|
{
|
||||||
let theme = body.into_domain();
|
let theme = body.into_domain();
|
||||||
let svc = ConfigService::new(state.config.as_ref(), state.events.as_ref());
|
let svc = state.config_service();
|
||||||
svc.update_theme(theme)
|
svc.update_theme(theme)
|
||||||
.await
|
.await
|
||||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("{e}")))?;
|
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("{e}")))?;
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
use crate::AppState;
|
use crate::AppState;
|
||||||
use crate::extractors::AuthUser;
|
use crate::extractors::AuthUser;
|
||||||
use api_types::{CreateWidgetDto, WidgetDto};
|
use api_types::{CreateWidgetDto, WidgetDto};
|
||||||
use application::ConfigService;
|
|
||||||
use axum::{
|
use axum::{
|
||||||
extract::{Path, State},
|
extract::{Path, State},
|
||||||
http::StatusCode,
|
http::StatusCode,
|
||||||
@@ -65,7 +65,7 @@ where
|
|||||||
let widget = body
|
let widget = body
|
||||||
.into_domain()
|
.into_domain()
|
||||||
.map_err(|e| (StatusCode::BAD_REQUEST, e))?;
|
.map_err(|e| (StatusCode::BAD_REQUEST, e))?;
|
||||||
let svc = ConfigService::new(state.config.as_ref(), state.events.as_ref());
|
let svc = state.config_service();
|
||||||
svc.create_widget(widget)
|
svc.create_widget(widget)
|
||||||
.await
|
.await
|
||||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("{e}")))?;
|
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("{e}")))?;
|
||||||
@@ -87,7 +87,7 @@ where
|
|||||||
let widget = body
|
let widget = body
|
||||||
.into_domain()
|
.into_domain()
|
||||||
.map_err(|e| (StatusCode::BAD_REQUEST, e))?;
|
.map_err(|e| (StatusCode::BAD_REQUEST, e))?;
|
||||||
let svc = ConfigService::new(state.config.as_ref(), state.events.as_ref());
|
let svc = state.config_service();
|
||||||
svc.update_widget(widget)
|
svc.update_widget(widget)
|
||||||
.await
|
.await
|
||||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("{e}")))?;
|
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("{e}")))?;
|
||||||
@@ -105,7 +105,7 @@ where
|
|||||||
E: EventPublisher,
|
E: EventPublisher,
|
||||||
E::Error: std::fmt::Debug,
|
E::Error: std::fmt::Debug,
|
||||||
{
|
{
|
||||||
let svc = ConfigService::new(state.config.as_ref(), state.events.as_ref());
|
let svc = state.config_service();
|
||||||
svc.delete_widget(id)
|
svc.delete_widget(id)
|
||||||
.await
|
.await
|
||||||
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
|
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
|
||||||
|
|||||||
123
crates/application/src/event_service.rs
Normal file
123
crates/application/src/event_service.rs
Normal file
@@ -0,0 +1,123 @@
|
|||||||
|
use crate::DataProjection;
|
||||||
|
use domain::{
|
||||||
|
BroadcastPort, ConfigRepository, DomainEvent, Layout, Value, WidgetConfig, WidgetState,
|
||||||
|
};
|
||||||
|
use std::sync::Arc;
|
||||||
|
use tracing::{error, info, warn};
|
||||||
|
|
||||||
|
pub async fn handle_event<C, B>(
|
||||||
|
event: DomainEvent,
|
||||||
|
config: &Arc<C>,
|
||||||
|
broadcaster: &Arc<B>,
|
||||||
|
projection: &Arc<DataProjection>,
|
||||||
|
) where
|
||||||
|
C: ConfigRepository,
|
||||||
|
C::Error: std::fmt::Display,
|
||||||
|
B: BroadcastPort,
|
||||||
|
B::Error: std::fmt::Display,
|
||||||
|
{
|
||||||
|
match event {
|
||||||
|
DomainEvent::LayoutChanged { layout } => {
|
||||||
|
handle_layout_changed(&layout, config, broadcaster, projection).await;
|
||||||
|
}
|
||||||
|
DomainEvent::WebhookDataReceived { source_id, data } => {
|
||||||
|
handle_webhook_data(source_id, &data, config, broadcaster, projection).await;
|
||||||
|
}
|
||||||
|
DomainEvent::ThemeChanged { theme } => {
|
||||||
|
if let Err(e) = broadcaster.push_theme_update(&theme).await {
|
||||||
|
error!(error = %e, "failed to push theme update");
|
||||||
|
}
|
||||||
|
info!("theme changed, pushed update to clients");
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn handle_layout_changed<C, B>(
|
||||||
|
layout: &Layout,
|
||||||
|
config: &Arc<C>,
|
||||||
|
broadcaster: &Arc<B>,
|
||||||
|
projection: &Arc<DataProjection>,
|
||||||
|
) where
|
||||||
|
C: ConfigRepository,
|
||||||
|
C::Error: std::fmt::Display,
|
||||||
|
B: BroadcastPort,
|
||||||
|
B::Error: std::fmt::Display,
|
||||||
|
{
|
||||||
|
let widgets = match config.list_widgets().await {
|
||||||
|
Ok(w) => w,
|
||||||
|
Err(e) => {
|
||||||
|
error!(error = %e, "failed to fetch widgets for screen update");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut widget_states = Vec::new();
|
||||||
|
for w in &widgets {
|
||||||
|
if let Some(s) = projection.get_state(w.id).await {
|
||||||
|
widget_states.push((w.id, w.display_hint.clone(), s));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Err(e) = broadcaster.push_screen_update(layout, &widget_states).await {
|
||||||
|
error!(error = %e, "failed to push screen update");
|
||||||
|
}
|
||||||
|
|
||||||
|
info!("layout changed, pushed screen update to clients");
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn handle_webhook_data<C, B>(
|
||||||
|
source_id: u16,
|
||||||
|
data: &Value,
|
||||||
|
config: &Arc<C>,
|
||||||
|
broadcaster: &Arc<B>,
|
||||||
|
projection: &Arc<DataProjection>,
|
||||||
|
) where
|
||||||
|
C: ConfigRepository,
|
||||||
|
C::Error: std::fmt::Display,
|
||||||
|
B: BroadcastPort,
|
||||||
|
B::Error: std::fmt::Display,
|
||||||
|
{
|
||||||
|
let widgets = match config.list_widgets().await {
|
||||||
|
Ok(w) => w,
|
||||||
|
Err(e) => {
|
||||||
|
error!(error = %e, "failed to fetch widgets for webhook");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let changed = apply_and_broadcast(source_id, data, &widgets, broadcaster, projection).await;
|
||||||
|
if !changed.is_empty() {
|
||||||
|
info!(source_id, count = changed.len(), "webhook data pushed");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn apply_and_broadcast<B>(
|
||||||
|
source_id: u16,
|
||||||
|
data: &Value,
|
||||||
|
widgets: &[WidgetConfig],
|
||||||
|
broadcaster: &Arc<B>,
|
||||||
|
projection: &Arc<DataProjection>,
|
||||||
|
) -> Vec<(u16, WidgetState)>
|
||||||
|
where
|
||||||
|
B: BroadcastPort,
|
||||||
|
B::Error: std::fmt::Display,
|
||||||
|
{
|
||||||
|
let changed: Vec<(u16, WidgetState)> =
|
||||||
|
projection.apply_poll_result(source_id, data, widgets).await;
|
||||||
|
|
||||||
|
if !changed.is_empty() {
|
||||||
|
let with_hints: Vec<_> = changed
|
||||||
|
.iter()
|
||||||
|
.filter_map(|(id, state)| {
|
||||||
|
let hint = widgets.iter().find(|w| w.id == *id)?.display_hint.clone();
|
||||||
|
Some((*id, hint, state.clone()))
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
if let Err(e) = broadcaster.push_data_update(&with_hints).await {
|
||||||
|
warn!(error = %e, "failed to push update");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
changed
|
||||||
|
}
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
pub mod auth_service;
|
pub mod auth_service;
|
||||||
mod config_service;
|
mod config_service;
|
||||||
mod data_projection;
|
mod data_projection;
|
||||||
|
pub mod event_service;
|
||||||
pub mod polling_service;
|
pub mod polling_service;
|
||||||
|
|
||||||
pub use config_service::ConfigService;
|
pub use config_service::ConfigService;
|
||||||
|
|||||||
@@ -130,7 +130,20 @@ async fn poll_and_broadcast<C, B, P, F>(
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
broadcast_changes(source, &result, &widgets, broadcaster, projection, config).await;
|
let changed = crate::event_service::apply_and_broadcast(
|
||||||
|
source.id,
|
||||||
|
&result,
|
||||||
|
&widgets,
|
||||||
|
broadcaster,
|
||||||
|
projection,
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
if !changed.is_empty() {
|
||||||
|
if let Err(e) = config.save_widget_states(&changed).await {
|
||||||
|
warn!(error = %e, "failed to cache widget states");
|
||||||
|
}
|
||||||
|
info!(source = %source.name, count = changed.len(), "pushed widget updates");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn poll_loop<C, B, P, F>(
|
async fn poll_loop<C, B, P, F>(
|
||||||
@@ -176,55 +189,25 @@ async fn poll_loop<C, B, P, F>(
|
|||||||
last_refresh = tokio::time::Instant::now();
|
last_refresh = tokio::time::Instant::now();
|
||||||
}
|
}
|
||||||
|
|
||||||
broadcast_changes(
|
let changed = crate::event_service::apply_and_broadcast(
|
||||||
&source,
|
source.id,
|
||||||
&result,
|
&result,
|
||||||
&widgets,
|
&widgets,
|
||||||
&broadcaster,
|
&broadcaster,
|
||||||
&projection,
|
&projection,
|
||||||
&config,
|
|
||||||
)
|
)
|
||||||
.await;
|
.await;
|
||||||
|
if !changed.is_empty() {
|
||||||
|
if let Err(e) = config.save_widget_states(&changed).await {
|
||||||
|
warn!(error = %e, "failed to cache widget states");
|
||||||
|
}
|
||||||
|
info!(source = %source.name, count = changed.len(), "pushed widget updates");
|
||||||
|
}
|
||||||
|
|
||||||
tokio::time::sleep(interval).await;
|
tokio::time::sleep(interval).await;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn broadcast_changes<C, B>(
|
|
||||||
source: &DataSource,
|
|
||||||
result: &Value,
|
|
||||||
widgets: &[WidgetConfig],
|
|
||||||
broadcaster: &Arc<B>,
|
|
||||||
projection: &Arc<DataProjection>,
|
|
||||||
config: &Arc<C>,
|
|
||||||
) where
|
|
||||||
C: WidgetStateCache,
|
|
||||||
C::Error: std::fmt::Display,
|
|
||||||
B: BroadcastPort,
|
|
||||||
B::Error: std::fmt::Display,
|
|
||||||
{
|
|
||||||
let changed: Vec<(u16, WidgetState)> = projection
|
|
||||||
.apply_poll_result(source.id, result, widgets)
|
|
||||||
.await;
|
|
||||||
|
|
||||||
if !changed.is_empty() {
|
|
||||||
let with_hints: Vec<_> = changed
|
|
||||||
.iter()
|
|
||||||
.filter_map(|(id, state)| {
|
|
||||||
let hint = widgets.iter().find(|w| w.id == *id)?.display_hint.clone();
|
|
||||||
Some((*id, hint, state.clone()))
|
|
||||||
})
|
|
||||||
.collect();
|
|
||||||
if let Err(e) = broadcaster.push_data_update(&with_hints).await {
|
|
||||||
warn!(error = %e, "failed to push update");
|
|
||||||
}
|
|
||||||
if let Err(e) = config.save_widget_states(&changed).await {
|
|
||||||
warn!(error = %e, "failed to cache widget states");
|
|
||||||
}
|
|
||||||
info!(source = %source.name, count = changed.len(), "pushed widget updates");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn broadcast_errors<B>(
|
async fn broadcast_errors<B>(
|
||||||
source: &DataSource,
|
source: &DataSource,
|
||||||
widgets: &[WidgetConfig],
|
widgets: &[WidgetConfig],
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ pub struct ServerConfig {
|
|||||||
pub database_url: String,
|
pub database_url: String,
|
||||||
pub tcp_addr: String,
|
pub tcp_addr: String,
|
||||||
pub http_addr: String,
|
pub http_addr: String,
|
||||||
pub poll_interval_secs: u64,
|
|
||||||
pub spa_dir: Option<String>,
|
pub spa_dir: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -15,10 +14,6 @@ impl ServerConfig {
|
|||||||
.unwrap_or_else(|_| "sqlite:kframe.db?mode=rwc".into()),
|
.unwrap_or_else(|_| "sqlite:kframe.db?mode=rwc".into()),
|
||||||
tcp_addr: env::var("KFRAME_TCP_ADDR").unwrap_or_else(|_| "0.0.0.0:2699".into()),
|
tcp_addr: env::var("KFRAME_TCP_ADDR").unwrap_or_else(|_| "0.0.0.0:2699".into()),
|
||||||
http_addr: env::var("KFRAME_HTTP_ADDR").unwrap_or_else(|_| "0.0.0.0:3000".into()),
|
http_addr: env::var("KFRAME_HTTP_ADDR").unwrap_or_else(|_| "0.0.0.0:3000".into()),
|
||||||
poll_interval_secs: env::var("KFRAME_POLL_INTERVAL_SECS")
|
|
||||||
.ok()
|
|
||||||
.and_then(|v| v.parse().ok())
|
|
||||||
.unwrap_or(5),
|
|
||||||
spa_dir: env::var("KFRAME_SPA_DIR").ok(),
|
spa_dir: env::var("KFRAME_SPA_DIR").ok(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,8 @@
|
|||||||
use application::DataProjection;
|
use application::DataProjection;
|
||||||
use config_sqlite::SqliteConfigStore;
|
use config_sqlite::SqliteConfigStore;
|
||||||
use domain::{BroadcastPort, ConfigRepository, DomainEvent};
|
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use tcp_server::{TcpBroadcaster, TcpEventBus};
|
use tcp_server::{TcpBroadcaster, TcpEventBus};
|
||||||
use tracing::{error, info, warn};
|
use tracing::{error, warn};
|
||||||
|
|
||||||
pub async fn run(
|
pub async fn run(
|
||||||
event_bus: Arc<TcpEventBus>,
|
event_bus: Arc<TcpEventBus>,
|
||||||
@@ -15,69 +14,10 @@ pub async fn run(
|
|||||||
|
|
||||||
loop {
|
loop {
|
||||||
match rx.recv().await {
|
match rx.recv().await {
|
||||||
Ok(DomainEvent::LayoutChanged { layout }) => {
|
Ok(event) => {
|
||||||
let widgets = match config.list_widgets().await {
|
application::event_service::handle_event(event, &config, &broadcaster, &projection)
|
||||||
Ok(w) => w,
|
|
||||||
Err(e) => {
|
|
||||||
error!(error = %e, "failed to fetch widgets for screen update");
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
let mut widget_states = Vec::new();
|
|
||||||
for w in &widgets {
|
|
||||||
if let Some(s) = projection.get_state(w.id).await {
|
|
||||||
widget_states.push((w.id, w.display_hint.clone(), s));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Err(e) = broadcaster
|
|
||||||
.push_screen_update(&layout, &widget_states)
|
|
||||||
.await
|
|
||||||
{
|
|
||||||
error!(error = %e, "failed to push screen update");
|
|
||||||
}
|
|
||||||
|
|
||||||
info!("layout changed, pushed screen update to clients");
|
|
||||||
}
|
|
||||||
Ok(DomainEvent::WebhookDataReceived { source_id, data }) => {
|
|
||||||
let widgets = match config.list_widgets().await {
|
|
||||||
Ok(w) => w,
|
|
||||||
Err(e) => {
|
|
||||||
error!(error = %e, "failed to fetch widgets for webhook");
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
let changed = projection
|
|
||||||
.apply_poll_result(source_id, &data, &widgets)
|
|
||||||
.await;
|
.await;
|
||||||
|
|
||||||
if !changed.is_empty() {
|
|
||||||
let with_hints: Vec<_> = changed
|
|
||||||
.iter()
|
|
||||||
.filter_map(|(id, state)| {
|
|
||||||
let hint = widgets.iter().find(|w| w.id == *id)?.display_hint.clone();
|
|
||||||
Some((*id, hint, state.clone()))
|
|
||||||
})
|
|
||||||
.collect();
|
|
||||||
if let Err(e) = broadcaster.push_data_update(&with_hints).await {
|
|
||||||
error!(error = %e, "failed to push webhook data update");
|
|
||||||
}
|
|
||||||
info!(
|
|
||||||
source_id,
|
|
||||||
count = changed.len(),
|
|
||||||
"webhook data received, pushed update"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
Ok(DomainEvent::ThemeChanged { theme }) => {
|
|
||||||
if let Err(e) = broadcaster.push_theme_update(&theme).await {
|
|
||||||
error!(error = %e, "failed to push theme update");
|
|
||||||
}
|
|
||||||
info!("theme changed, pushed update to clients");
|
|
||||||
}
|
|
||||||
Ok(_) => {}
|
|
||||||
Err(tokio::sync::broadcast::error::RecvError::Lagged(n)) => {
|
Err(tokio::sync::broadcast::error::RecvError::Lagged(n)) => {
|
||||||
warn!(skipped = n, "event handler lagged, missed events");
|
warn!(skipped = n, "event handler lagged, missed events");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -90,11 +90,5 @@ async fn main() -> Result<()> {
|
|||||||
event_handler::run(ev_bus, ev_config, ev_bc, ev_proj).await;
|
event_handler::run(ev_bus, ev_config, ev_bc, ev_proj).await;
|
||||||
});
|
});
|
||||||
|
|
||||||
polling::run(
|
polling::run(config_store, broadcaster, projection).await
|
||||||
config_store,
|
|
||||||
broadcaster,
|
|
||||||
projection,
|
|
||||||
cfg.poll_interval_secs,
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -57,7 +57,6 @@ pub async fn run(
|
|||||||
config: Arc<SqliteConfigStore>,
|
config: Arc<SqliteConfigStore>,
|
||||||
broadcaster: Arc<TcpBroadcaster>,
|
broadcaster: Arc<TcpBroadcaster>,
|
||||||
projection: Arc<DataProjection>,
|
projection: Arc<DataProjection>,
|
||||||
_poll_interval_secs: u64,
|
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
let adapters = Adapters {
|
let adapters = Adapters {
|
||||||
http: Arc::new(HttpJsonAdapter::new()),
|
http: Arc::new(HttpJsonAdapter::new()),
|
||||||
|
|||||||
@@ -1,24 +1,24 @@
|
|||||||
use crate::conversions::wire_to_layout;
|
use crate::conversions::{wire_to_display_hint, wire_to_layout, wire_to_widget_state};
|
||||||
use client_domain::{BoundingBox, Color, LayoutEngine, RenderTree, ThemeConfig};
|
use client_domain::{BoundingBox, Color, LayoutEngine, RenderTree, ThemeConfig};
|
||||||
use protocol::{
|
use domain::{DisplayHint, Value, WidgetError, WidgetState};
|
||||||
ServerMessage, WidgetDescriptor, WireColor, WireDisplayHint, WireLayoutNode, WireWidgetState,
|
use protocol::{ServerMessage, WidgetDescriptor, WireColor, WireLayoutNode};
|
||||||
};
|
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
|
|
||||||
pub struct ClientApp {
|
pub struct ClientApp {
|
||||||
screen: BoundingBox,
|
screen: BoundingBox,
|
||||||
render_tree: Option<RenderTree>,
|
render_tree: Option<RenderTree>,
|
||||||
widget_states: HashMap<u16, (WireDisplayHint, WireWidgetState)>,
|
widget_states: HashMap<u16, (DisplayHint, WidgetState)>,
|
||||||
theme: ThemeConfig,
|
theme: ThemeConfig,
|
||||||
theme_changed: bool,
|
theme_changed: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct RepaintCommand {
|
pub struct RepaintCommand {
|
||||||
pub widget_id: u16,
|
pub widget_id: u16,
|
||||||
pub bounds: BoundingBox,
|
pub bounds: BoundingBox,
|
||||||
pub display_hint: WireDisplayHint,
|
pub display_hint: DisplayHint,
|
||||||
pub state: WireWidgetState,
|
pub data: Vec<(String, Value)>,
|
||||||
|
pub error: Option<WidgetError>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ClientApp {
|
impl ClientApp {
|
||||||
@@ -77,9 +77,10 @@ impl ClientApp {
|
|||||||
let new_tree = LayoutEngine::compute(&layout, self.screen);
|
let new_tree = LayoutEngine::compute(&layout, self.screen);
|
||||||
|
|
||||||
self.widget_states.clear();
|
self.widget_states.clear();
|
||||||
for w in &widgets {
|
for w in widgets {
|
||||||
self.widget_states
|
let hint = wire_to_display_hint(w.display_hint);
|
||||||
.insert(w.id, (w.display_hint.clone(), w.state.clone()));
|
let state = wire_to_widget_state(w.state);
|
||||||
|
self.widget_states.insert(w.id, (hint, state));
|
||||||
}
|
}
|
||||||
|
|
||||||
let repaints = self.build_repaints_for_all(&new_tree);
|
let repaints = self.build_repaints_for_all(&new_tree);
|
||||||
@@ -96,21 +97,19 @@ impl ClientApp {
|
|||||||
let mut repaints = Vec::new();
|
let mut repaints = Vec::new();
|
||||||
|
|
||||||
for w in widgets {
|
for w in widgets {
|
||||||
|
let hint = wire_to_display_hint(w.display_hint);
|
||||||
|
let state = wire_to_widget_state(w.state);
|
||||||
|
|
||||||
let changed = self
|
let changed = self
|
||||||
.widget_states
|
.widget_states
|
||||||
.get(&w.id)
|
.get(&w.id)
|
||||||
.is_none_or(|(_, prev_state)| *prev_state != w.state);
|
.is_none_or(|(_, prev)| *prev != state);
|
||||||
|
|
||||||
if changed {
|
if changed {
|
||||||
if let Some(bounds) = tree.get_widget_bounds(w.id) {
|
if let Some(bounds) = tree.get_widget_bounds(w.id) {
|
||||||
repaints.push(RepaintCommand {
|
repaints.push(Self::make_repaint(w.id, *bounds, &hint, &state));
|
||||||
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));
|
self.widget_states.insert(w.id, (hint, state));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -122,18 +121,32 @@ impl ClientApp {
|
|||||||
|
|
||||||
for (id, (hint, state)) in &self.widget_states {
|
for (id, (hint, state)) in &self.widget_states {
|
||||||
if let Some(bounds) = tree.get_widget_bounds(*id) {
|
if let Some(bounds) = tree.get_widget_bounds(*id) {
|
||||||
repaints.push(RepaintCommand {
|
repaints.push(Self::make_repaint(*id, *bounds, hint, state));
|
||||||
widget_id: *id,
|
|
||||||
bounds: *bounds,
|
|
||||||
display_hint: hint.clone(),
|
|
||||||
state: state.clone(),
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
repaints.sort_by_key(|r| r.widget_id);
|
repaints.sort_by_key(|r| r.widget_id);
|
||||||
repaints
|
repaints
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn make_repaint(
|
||||||
|
id: u16,
|
||||||
|
bounds: BoundingBox,
|
||||||
|
hint: &DisplayHint,
|
||||||
|
state: &WidgetState,
|
||||||
|
) -> RepaintCommand {
|
||||||
|
RepaintCommand {
|
||||||
|
widget_id: id,
|
||||||
|
bounds,
|
||||||
|
display_hint: hint.clone(),
|
||||||
|
data: state
|
||||||
|
.data
|
||||||
|
.iter()
|
||||||
|
.map(|(k, v)| (k.clone(), v.clone()))
|
||||||
|
.collect(),
|
||||||
|
error: state.error.clone(),
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn wire_color(c: WireColor) -> Color {
|
fn wire_color(c: WireColor) -> Color {
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
use client_application::{ClientApp, RepaintCommand};
|
use client_application::ClientApp;
|
||||||
use client_domain::BoundingBox;
|
use client_domain::BoundingBox;
|
||||||
use protocol::{
|
use protocol::{
|
||||||
ServerMessage, WidgetDescriptor, WireAlignItems, WireContainerNode, WireDirection,
|
ServerMessage, WidgetDescriptor, WireAlignItems, WireContainerNode, WireDirection,
|
||||||
@@ -84,8 +84,8 @@ fn data_update_only_repaints_changed_widgets() {
|
|||||||
assert_eq!(repaints.len(), 1);
|
assert_eq!(repaints.len(), 1);
|
||||||
assert_eq!(repaints[0].widget_id, 1);
|
assert_eq!(repaints[0].widget_id, 1);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
repaints[0].state.data[0].value,
|
repaints[0].data[0],
|
||||||
WireValue::String("6.1°C".into())
|
("temperature".into(), domain::Value::String("6.1°C".into()))
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
use client_application::{ClientApp, conversions, run_connection_loop};
|
use client_application::{ClientApp, RepaintCommand, run_connection_loop};
|
||||||
use client_domain::{
|
use client_domain::{
|
||||||
BoundingBox, DisplayPort, FontMetrics, RenderEngine, RepaintRequest, ThemeConfig,
|
BoundingBox, DisplayPort, FontMetrics, RenderEngine, RepaintRequest, ThemeConfig,
|
||||||
WidgetRenderer,
|
WidgetRenderer,
|
||||||
@@ -10,6 +10,16 @@ use std::thread;
|
|||||||
use std::time::{Duration, Instant};
|
use std::time::{Duration, Instant};
|
||||||
use tcp_client::StdTcpClient;
|
use tcp_client::StdTcpClient;
|
||||||
|
|
||||||
|
fn to_request(cmd: &RepaintCommand) -> RepaintRequest {
|
||||||
|
RepaintRequest {
|
||||||
|
widget_id: cmd.widget_id,
|
||||||
|
bounds: cmd.bounds,
|
||||||
|
display_hint: cmd.display_hint.clone(),
|
||||||
|
data: cmd.data.clone(),
|
||||||
|
error: cmd.error.clone(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
let screen = BoundingBox::screen(240, 320);
|
let screen = BoundingBox::screen(240, 320);
|
||||||
let mut app = ClientApp::new(screen);
|
let mut app = ClientApp::new(screen);
|
||||||
@@ -56,30 +66,7 @@ fn main() {
|
|||||||
if !repaints.is_empty() {
|
if !repaints.is_empty() {
|
||||||
println!("\n--- Repaint ({} widgets) ---", repaints.len());
|
println!("\n--- Repaint ({} widgets) ---", repaints.len());
|
||||||
|
|
||||||
let requests: Vec<RepaintRequest> = repaints
|
let requests: Vec<_> = repaints.iter().map(to_request).collect();
|
||||||
.iter()
|
|
||||||
.map(|cmd| RepaintRequest {
|
|
||||||
widget_id: cmd.widget_id,
|
|
||||||
bounds: cmd.bounds,
|
|
||||||
display_hint: conversions::wire_to_display_hint(
|
|
||||||
cmd.display_hint.clone(),
|
|
||||||
),
|
|
||||||
data: cmd
|
|
||||||
.state
|
|
||||||
.data
|
|
||||||
.iter()
|
|
||||||
.map(|kv| {
|
|
||||||
(kv.key.clone(), conversions::wire_to_value(kv.value.clone()))
|
|
||||||
})
|
|
||||||
.collect(),
|
|
||||||
error: cmd
|
|
||||||
.state
|
|
||||||
.error
|
|
||||||
.as_ref()
|
|
||||||
.map(|e| conversions::wire_to_widget_error(e.clone())),
|
|
||||||
})
|
|
||||||
.collect();
|
|
||||||
|
|
||||||
let bg = engine.theme().background;
|
let bg = engine.theme().background;
|
||||||
let updates = renderer.apply_repaints(&engine, requests);
|
let updates = renderer.apply_repaints(&engine, requests);
|
||||||
for update in &updates {
|
for update in &updates {
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ pub use domain::{AlignItems, DisplayHintKind, HAlign, JustifyContent, VAlign};
|
|||||||
pub use font::{FontMetrics, FontSize};
|
pub use font::{FontMetrics, FontSize};
|
||||||
pub use layout_engine::LayoutEngine;
|
pub use layout_engine::LayoutEngine;
|
||||||
pub use markup::{TextSpan, parse_markup};
|
pub use markup::{TextSpan, parse_markup};
|
||||||
pub use ports::{ClientConfig, DisplayPort, NetworkPort, StoragePort};
|
pub use ports::{DisplayPort, NetworkPort};
|
||||||
pub use render_engine::{DrawCommand, RenderEngine};
|
pub use render_engine::{DrawCommand, RenderEngine};
|
||||||
pub use render_tree::RenderTree;
|
pub use render_tree::RenderTree;
|
||||||
pub use scroll::ScrollState;
|
pub use scroll::ScrollState;
|
||||||
|
|||||||
@@ -1,7 +1,5 @@
|
|||||||
mod display;
|
mod display;
|
||||||
mod network;
|
mod network;
|
||||||
mod storage;
|
|
||||||
|
|
||||||
pub use display::DisplayPort;
|
pub use display::DisplayPort;
|
||||||
pub use network::NetworkPort;
|
pub use network::NetworkPort;
|
||||||
pub use storage::{ClientConfig, StoragePort};
|
|
||||||
|
|||||||
@@ -1,11 +0,0 @@
|
|||||||
pub struct ClientConfig {
|
|
||||||
pub wifi_ssid: String,
|
|
||||||
pub wifi_password: String,
|
|
||||||
pub server_addr: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
pub trait StoragePort {
|
|
||||||
type Error;
|
|
||||||
|
|
||||||
fn load_config(&self) -> Result<ClientConfig, Self::Error>;
|
|
||||||
}
|
|
||||||
@@ -1,10 +1,12 @@
|
|||||||
use std::sync::mpsc;
|
use std::sync::mpsc;
|
||||||
use std::time::{Duration, Instant};
|
use std::time::{Duration, Instant};
|
||||||
|
|
||||||
|
use client_application::RepaintCommand;
|
||||||
use client_domain::{
|
use client_domain::{
|
||||||
BoundingBox, Color, DisplayPort, FontMetrics, RenderEngine, RepaintRequest, ThemeConfig,
|
BoundingBox, Color, DisplayPort, FontMetrics, RenderEngine, RepaintRequest, ThemeConfig,
|
||||||
WidgetRenderer,
|
WidgetRenderer,
|
||||||
};
|
};
|
||||||
use client_application::{ClientApp, RepaintCommand, conversions};
|
use client_application::ClientApp;
|
||||||
use protocol::ServerMessage;
|
use protocol::ServerMessage;
|
||||||
use super::RenderEvent;
|
use super::RenderEvent;
|
||||||
use crate::config::RENDER_POLL_INTERVAL;
|
use crate::config::RENDER_POLL_INTERVAL;
|
||||||
@@ -17,17 +19,14 @@ const INDICATOR_MARGIN: u16 = 4;
|
|||||||
const COLOR_CONNECTED: Color = Color(0, 200, 0);
|
const COLOR_CONNECTED: Color = Color(0, 200, 0);
|
||||||
const COLOR_DISCONNECTED: Color = Color(200, 0, 0);
|
const COLOR_DISCONNECTED: Color = Color(200, 0, 0);
|
||||||
|
|
||||||
fn to_repaint_requests(repaints: &[RepaintCommand]) -> Vec<RepaintRequest> {
|
fn to_request(cmd: &RepaintCommand) -> RepaintRequest {
|
||||||
repaints
|
RepaintRequest {
|
||||||
.iter()
|
widget_id: cmd.widget_id,
|
||||||
.map(|cmd| RepaintRequest {
|
bounds: cmd.bounds,
|
||||||
widget_id: cmd.widget_id,
|
display_hint: cmd.display_hint.clone(),
|
||||||
bounds: cmd.bounds,
|
data: cmd.data.clone(),
|
||||||
display_hint: conversions::wire_to_display_hint(cmd.display_hint.clone()),
|
error: cmd.error.clone(),
|
||||||
data: cmd.state.data.iter().map(|kv| (kv.key.clone(), conversions::wire_to_value(kv.value.clone()))).collect(),
|
}
|
||||||
error: cmd.state.error.as_ref().map(|e| conversions::wire_to_widget_error(e.clone())),
|
|
||||||
})
|
|
||||||
.collect()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn run(
|
pub fn run(
|
||||||
@@ -52,7 +51,11 @@ pub fn run(
|
|||||||
|
|
||||||
loop {
|
loop {
|
||||||
let has_scrollers = renderer.has_active_scrollers();
|
let has_scrollers = renderer.has_active_scrollers();
|
||||||
let timeout = if has_scrollers { SCROLL_TICK } else { RENDER_POLL_INTERVAL };
|
let timeout = if has_scrollers {
|
||||||
|
SCROLL_TICK
|
||||||
|
} else {
|
||||||
|
RENDER_POLL_INTERVAL
|
||||||
|
};
|
||||||
match rx.recv_timeout(timeout) {
|
match rx.recv_timeout(timeout) {
|
||||||
Ok(RenderEvent::ConnectionStatus(status)) => {
|
Ok(RenderEvent::ConnectionStatus(status)) => {
|
||||||
if status != connected {
|
if status != connected {
|
||||||
@@ -76,12 +79,14 @@ pub fn run(
|
|||||||
first_update = false;
|
first_update = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
let requests = to_repaint_requests(&repaints);
|
let requests: Vec<_> = repaints.iter().map(to_request).collect();
|
||||||
let updates = renderer.apply_repaints(&engine, requests);
|
let updates = renderer.apply_repaints(&engine, requests);
|
||||||
for update in &updates {
|
for update in &updates {
|
||||||
display.fill_rect(update.bounds, bg).unwrap();
|
display.fill_rect(update.bounds, bg).unwrap();
|
||||||
for dc in &update.commands {
|
for dc in &update.commands {
|
||||||
display.draw_text_span(&dc.text, dc.x, dc.y, dc.color, dc.font).unwrap();
|
display
|
||||||
|
.draw_text_span(&dc.text, dc.x, dc.y, dc.color, dc.font)
|
||||||
|
.unwrap();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -107,7 +112,9 @@ pub fn run(
|
|||||||
for update in &scroll_updates {
|
for update in &scroll_updates {
|
||||||
display.fill_rect(update.bounds, bg).unwrap();
|
display.fill_rect(update.bounds, bg).unwrap();
|
||||||
for dc in &update.commands {
|
for dc in &update.commands {
|
||||||
display.draw_text_span(&dc.text, dc.x, dc.y, dc.color, dc.font).unwrap();
|
display
|
||||||
|
.draw_text_span(&dc.text, dc.x, dc.y, dc.color, dc.font)
|
||||||
|
.unwrap();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
draw_indicator(&mut display, screen, connected);
|
draw_indicator(&mut display, screen, connected);
|
||||||
@@ -117,8 +124,14 @@ pub fn run(
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn draw_indicator(display: &mut Esp32DisplayAdapter, screen: BoundingBox, connected: bool) {
|
fn draw_indicator(display: &mut Esp32DisplayAdapter, screen: BoundingBox, connected: bool) {
|
||||||
let color = if connected { COLOR_CONNECTED } else { COLOR_DISCONNECTED };
|
let color = if connected {
|
||||||
|
COLOR_CONNECTED
|
||||||
|
} else {
|
||||||
|
COLOR_DISCONNECTED
|
||||||
|
};
|
||||||
let x = screen.x + screen.width - INDICATOR_DIAMETER - INDICATOR_MARGIN;
|
let x = screen.x + screen.width - INDICATOR_DIAMETER - INDICATOR_MARGIN;
|
||||||
let y = screen.y + screen.height - INDICATOR_DIAMETER - INDICATOR_MARGIN;
|
let y = screen.y + screen.height - INDICATOR_DIAMETER - INDICATOR_MARGIN;
|
||||||
display.fill_circle(x, y, INDICATOR_DIAMETER, color).unwrap();
|
display
|
||||||
|
.fill_circle(x, y, INDICATOR_DIAMETER, color)
|
||||||
|
.unwrap();
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user