1use std::collections::HashMap;
2use std::fs;
3use std::fs::DirEntry;
4use std::path::{Path, PathBuf};
5use std::time::Instant;
6
7use image::{ImageBuffer, ImageFormat, Rgba};
8use log::{debug, error};
9use serde::{Deserialize, Serialize};
10
11pub mod conditional_image_renderer;
12pub mod graph_renderer;
13pub mod text_renderer;
14
15#[derive(Serialize, Deserialize, PartialEq, Debug, Clone)]
21pub struct TransportMessage {
22 pub transport_type: TransportType,
23 pub data: Vec<u8>,
24}
25
26#[derive(Serialize, Deserialize, PartialEq, Eq, Debug, Clone)]
31pub enum TransportType {
32 PrepareText,
34 PrepareStaticImage,
36 PrepareConditionalImage,
38 RenderImage,
40}
41
42#[derive(Serialize, Deserialize, PartialEq, Debug, Default, Clone)]
45pub struct RenderData {
46 pub display_config: DisplayConfig,
47 pub sensor_values: Vec<SensorValue>,
48}
49
50#[derive(Serialize, Deserialize, PartialEq, Debug, Clone)]
56pub struct PrepareTextData {
57 pub font_data: HashMap<String, Vec<u8>>,
60}
61
62#[derive(Serialize, Deserialize, PartialEq, Debug, Clone)]
68pub struct PrepareStaticImageData {
69 pub images_data: HashMap<String, Vec<u8>>,
72}
73
74#[derive(Serialize, Deserialize, PartialEq, Debug, Clone)]
80pub struct PrepareConditionalImageData {
81 pub images_data: HashMap<String, HashMap<String, Vec<u8>>>,
84}
85
86#[derive(Serialize, Deserialize, PartialEq, Debug, Default, Clone)]
89pub struct DisplayConfig {
90 #[serde(default)]
91 pub resolution_height: u32,
92 #[serde(default)]
93 pub resolution_width: u32,
94 #[serde(default)]
95 pub elements: Vec<ElementConfig>,
96}
97
98#[derive(Serialize, Deserialize, PartialEq, Debug, Default, Clone)]
100pub struct ElementConfig {
101 #[serde(default)]
102 pub id: String,
103 #[serde(default)]
104 pub name: String,
105 #[serde(default)]
106 pub element_type: ElementType,
107 #[serde(default)]
108 pub x: i32,
109 #[serde(default)]
110 pub y: i32,
111 #[serde(default)]
112 pub text_config: Option<TextConfig>,
113 #[serde(default)]
114 pub image_config: Option<ImageConfig>,
115 #[serde(default)]
116 pub graph_config: Option<GraphConfig>,
117 #[serde(default)]
118 pub conditional_image_config: Option<ConditionalImageConfig>,
119}
120
121#[derive(Serialize, Deserialize, PartialEq, Debug, Default, Clone)]
123pub struct TextConfig {
124 #[serde(default)]
125 pub sensor_id: String,
126 #[serde(default)]
127 pub value_modifier: SensorValueModifier,
128 #[serde(default)]
129 pub format: String,
130 #[serde(default)]
131 pub font_family: String,
132 #[serde(default)]
133 pub font_size: u32,
134 #[serde(default)]
135 pub font_color: String,
136 #[serde(default)]
137 pub width: u32,
138 #[serde(default)]
139 pub height: u32,
140 #[serde(default)]
141 pub alignment: TextAlign,
142}
143
144#[derive(Serialize, Deserialize, PartialEq, Eq, Debug, Default, Clone)]
146pub enum TextAlign {
147 #[default]
148 #[serde(rename = "left")]
149 Left,
150 #[serde(rename = "center")]
151 Center,
152 #[serde(rename = "right")]
153 Right,
154}
155
156#[derive(Serialize, Deserialize, PartialEq, Debug, Default, Clone)]
158pub struct ImageConfig {
159 #[serde(default)]
160 pub width: u32,
161 #[serde(default)]
162 pub height: u32,
163 #[serde(default)]
164 pub image_path: String,
165}
166
167#[derive(Serialize, Deserialize, PartialEq, Debug, Default, Clone)]
169pub enum GraphType {
170 #[default]
171 #[serde(rename = "line")]
172 Line,
173 #[serde(rename = "line-fill")]
174 LineFill,
175}
176
177#[derive(Serialize, Deserialize, PartialEq, Debug, Default, Clone)]
179pub struct GraphConfig {
180 #[serde(default)]
181 pub sensor_id: String,
182 #[serde(default)]
183 pub sensor_values: Vec<f64>,
184 #[serde(default)]
185 pub min_sensor_value: Option<f64>,
186 #[serde(default)]
187 pub max_sensor_value: Option<f64>,
188 #[serde(default)]
189 pub width: u32,
190 #[serde(default)]
191 pub height: u32,
192 #[serde(default)]
193 pub graph_type: GraphType,
194 #[serde(default)]
195 pub graph_color: String,
196 #[serde(default)]
197 pub graph_stroke_width: i32,
198 #[serde(default)]
199 pub background_color: String,
200 #[serde(default)]
201 pub border_color: String,
202}
203
204#[derive(Serialize, Deserialize, PartialEq, Debug, Default, Clone)]
206pub struct ConditionalImageConfig {
207 #[serde(default)]
208 pub sensor_id: String,
209 #[serde(default)]
210 pub sensor_value: String,
211 #[serde(default)]
212 pub images_path: String,
213 #[serde(default)]
214 pub min_sensor_value: f64,
215 #[serde(default)]
216 pub max_sensor_value: f64,
217 #[serde(default)]
218 pub width: u32,
219 #[serde(default)]
220 pub height: u32,
221}
222
223#[derive(Serialize, Deserialize, PartialEq, Eq, Debug, Default, Clone)]
225pub enum ElementType {
226 #[default]
227 #[serde(rename = "text")]
228 Text,
229 #[serde(rename = "static-image")]
230 StaticImage,
231 #[serde(rename = "graph")]
232 Graph,
233 #[serde(rename = "conditional-image")]
234 ConditionalImage,
235}
236
237#[derive(Serialize, Deserialize, PartialEq, Debug, Clone, Default)]
239pub struct SensorValue {
240 #[serde(default)]
241 pub id: String,
242 #[serde(default)]
243 pub value: String,
244 #[serde(default)]
245 pub unit: String,
246 #[serde(default)]
247 pub label: String,
248 #[serde(default)]
249 pub sensor_type: SensorType,
250}
251
252#[derive(Serialize, Deserialize, PartialEq, Eq, Debug, Default, Clone)]
256pub enum SensorValueModifier {
257 #[default]
258 #[serde(rename = "none")]
259 None,
260 #[serde(rename = "min")]
261 Min,
262 #[serde(rename = "max")]
263 Max,
264 #[serde(rename = "avg")]
265 Avg,
266}
267
268#[derive(Serialize, Deserialize, PartialEq, Eq, Debug, Default, Clone)]
272pub enum SensorType {
273 #[default]
274 #[serde(rename = "text")]
275 Text,
276 #[serde(rename = "number")]
277 Number,
278}
279
280pub fn render_lcd_image(
283 display_config: DisplayConfig,
284 sensor_value_history: &[Vec<SensorValue>],
285 fonts_data: &HashMap<String, Vec<u8>>,
286) -> ImageBuffer<Rgba<u8>, Vec<u8>> {
287 let start_time = Instant::now();
288
289 let image_width = display_config.resolution_width;
291 let image_height = display_config.resolution_height;
292
293 let mut image = ImageBuffer::new(image_width, image_height);
295
296 for lcd_element in display_config.elements {
298 draw_element(&mut image, lcd_element, sensor_value_history, fonts_data);
299 }
300
301 debug!(" = Total frame render duration: {:?}", start_time.elapsed());
302
303 image
304}
305
306fn draw_element(
310 image: &mut ImageBuffer<Rgba<u8>, Vec<u8>>,
311 lcd_element: ElementConfig,
312 sensor_value_history: &[Vec<SensorValue>],
313 fonts_data: &HashMap<String, Vec<u8>>,
314) {
315 let x = lcd_element.x;
316 let y = lcd_element.y;
317 let element_id = lcd_element.id.as_str();
318
319 match lcd_element.element_type {
321 ElementType::Text => {
322 let text_config = lcd_element.text_config.unwrap();
323 draw_text(
324 image,
325 &lcd_element.id,
326 text_config,
327 x,
328 y,
329 sensor_value_history,
330 fonts_data,
331 );
332 }
333 ElementType::StaticImage => {
334 draw_static_image(image, &lcd_element.id, x, y);
335 }
336 ElementType::Graph => {
337 let mut graph_config = lcd_element.graph_config.unwrap();
338 graph_config.sensor_values =
339 extract_value_sequence(sensor_value_history, &graph_config.sensor_id);
340
341 draw_graph(image, x, y, graph_config);
342 }
343 ElementType::ConditionalImage => {
344 let conditional_image_config = lcd_element.conditional_image_config.unwrap();
345 let sensor_value = sensor_value_history[0]
346 .iter()
347 .find(|&s| s.id == conditional_image_config.sensor_id);
348 draw_conditional_image(
349 image,
350 x,
351 y,
352 element_id,
353 conditional_image_config,
354 sensor_value,
355 )
356 }
357 }
358}
359
360fn draw_static_image(image: &mut ImageBuffer<Rgba<u8>, Vec<u8>>, element_id: &str, x: i32, y: i32) {
362 let start_time = Instant::now();
363
364 let cache_dir = get_cache_dir(element_id, &ElementType::StaticImage).join(element_id);
365 let file_path = cache_dir.to_str().unwrap();
366
367 if !Path::new(&file_path).exists() {
368 error!("File {} does not exist", file_path);
369 return;
370 }
371
372 let img_data = fs::read(file_path).unwrap();
375 let overlay_image = image::load_from_memory(&img_data).unwrap();
376
377 image::imageops::overlay(image, &overlay_image, x as i64, y as i64);
378
379 debug!(" - Image render duration: {:?}", start_time.elapsed());
380}
381
382fn draw_graph(image: &mut ImageBuffer<Rgba<u8>, Vec<u8>>, x: i32, y: i32, config: GraphConfig) {
383 let start_time = Instant::now();
384
385 let img_data = graph_renderer::render(&config);
386 let graph_image = image::load_from_memory(&img_data).unwrap();
387
388 image::imageops::overlay(image, &graph_image, x as i64, y as i64);
389
390 debug!(" - Graph render duration: {:?}", start_time.elapsed());
391}
392
393fn draw_conditional_image(
395 image: &mut ImageBuffer<Rgba<u8>, Vec<u8>>,
396 x: i32,
397 y: i32,
398 element_id: &str,
399 mut config: ConditionalImageConfig,
400 sensor_value: Option<&SensorValue>,
401) {
402 let start_time = Instant::now();
403
404 let sensor_value = match sensor_value {
405 None => {
406 return;
407 }
408 Some(sensor_value) => sensor_value,
409 };
410
411 config.sensor_value = sensor_value.value.clone();
412 let img_data =
413 conditional_image_renderer::render(element_id, &sensor_value.sensor_type, &config);
414
415 if let Some(img_data) = img_data {
416 let conditional_image = image::load_from_memory(&img_data).unwrap();
417 image::imageops::overlay(image, &conditional_image, x as i64, y as i64);
418 }
419
420 debug!(
421 " - Conditional image render duration: {:?}",
422 start_time.elapsed()
423 );
424}
425
426fn draw_text(
428 image: &mut ImageBuffer<Rgba<u8>, Vec<u8>>,
429 _element_id: &str,
430 text_config: TextConfig,
431 x: i32,
432 y: i32,
433 sensor_value_history: &[Vec<SensorValue>],
434 fonts_data: &HashMap<String, Vec<u8>>,
435) {
436 let start_time = Instant::now();
437
438 let font_data = match fonts_data.get(&text_config.font_family) {
439 Some(font_data) => font_data,
440 None => {
441 error!(
442 "Font data for font family {} not found",
443 text_config.font_family
444 );
445 return;
446 }
447 };
448 let font = rusttype::Font::try_from_bytes(font_data).unwrap();
449
450 let text_image = text_renderer::render(
451 image.width(),
452 image.height(),
453 &text_config,
454 sensor_value_history,
455 &font,
456 );
457 image::imageops::overlay(image, &text_image, x as i64, y as i64);
458
459 debug!(" - Text render duration: {:?}", start_time.elapsed());
460}
461
462pub fn hex_to_rgba(hex_string: &str) -> Rgba<u8> {
467 let hex_string = hex_string.trim_start_matches('#');
468 let hex = u32::from_str_radix(hex_string, 16).unwrap();
469 let r = ((hex >> 24) & 0xff) as u8;
470 let g = ((hex >> 16) & 0xff) as u8;
471 let b = ((hex >> 8) & 0xff) as u8;
472 let a = (hex & 0xff) as u8;
473 Rgba([r, g, b, a])
474}
475
476pub fn extract_value_sequence(
478 sensor_value_history: &[Vec<SensorValue>],
479 sensor_id: &str,
480) -> Vec<f64> {
481 let mut sensor_values: Vec<f64> = sensor_value_history
482 .iter()
483 .flat_map(|history_entry| {
484 history_entry.iter().find_map(|entry| {
485 if entry.id.eq(sensor_id) {
486 return entry.value.parse().ok();
487 }
488 None
489 })
490 })
491 .collect();
492 sensor_values.reverse();
493 sensor_values
494}
495
496pub fn is_image(dir_entry: &DirEntry) -> bool {
498 let entry_path = dir_entry.path();
499 let extension_string = entry_path.extension().map(|ext| ext.to_str().unwrap());
500 let image_format = extension_string.and_then(ImageFormat::from_extension);
501 image_format.map(|x| x.can_read()).unwrap_or(false)
502}
503
504pub fn get_cache_dir(element_id: &str, element_type: &ElementType) -> PathBuf {
506 let element_type_folder_name = match element_type {
507 ElementType::Text => "text",
508 ElementType::StaticImage => "static-image",
509 ElementType::Graph => "graph",
510 ElementType::ConditionalImage => "conditional-image",
511 };
512
513 get_cache_base_dir()
514 .join(element_type_folder_name)
515 .join(element_id)
516}
517
518pub fn get_cache_base_dir() -> PathBuf {
520 dirs::cache_dir()
521 .unwrap()
522 .join(std::env::var("SENSOR_BRIDGE_APP_NAME").unwrap())
523}
524
525pub fn get_config_dir() -> PathBuf {
527 dirs::config_dir()
528 .unwrap()
529 .join(std::env::var("SENSOR_BRIDGE_APP_NAME").unwrap())
530}