sensor_core/
lib.rs

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/// Indicates the current type of message to be sent to the display.
16/// Either a message to prepares static assets, by sending them to the display, and then be stored on the fs.
17/// Or the actual render loop, where the prev. stored asses will be used to render the image.
18/// The type is used to deserialize the data to the correct struct.
19/// The data is a vector of bytes, which will be deserialized to the correct struct, depending on the type.
20#[derive(Serialize, Deserialize, PartialEq, Debug, Clone)]
21pub struct TransportMessage {
22    pub transport_type: TransportType,
23    pub data: Vec<u8>,
24}
25
26/// Represents the type of the message to be sent to the display.
27/// Either a message to prepares static assets, by sending them to the display, and then be stored on the fs.
28/// Or the actual render loop, where the prev. stored asses will be used to render the image.
29/// The type is used to deserialize the data to the correct struct.
30#[derive(Serialize, Deserialize, PartialEq, Eq, Debug, Clone)]
31pub enum TransportType {
32    /// De/Serialize to PrepareTextData
33    PrepareText,
34    /// De/Serialize to PrepareStaticImageData
35    PrepareStaticImage,
36    /// De/Serialize to PrepareConditionalImageData
37    PrepareConditionalImage,
38    /// De/Serialize to RenderData
39    RenderImage,
40}
41
42/// Represents the data to be rendered on a display.
43/// It holds the display config and the sensor values.
44#[derive(Serialize, Deserialize, PartialEq, Debug, Default, Clone)]
45pub struct RenderData {
46    pub display_config: DisplayConfig,
47    pub sensor_values: Vec<SensorValue>,
48}
49
50/// Represents the preparation data for the render process.
51/// It holds all static assets to be rendered.
52/// This is done once before the loop starts.
53/// Each asset will be stored on the display locally, and load during the render process by its
54/// asset id / element id
55#[derive(Serialize, Deserialize, PartialEq, Debug, Clone)]
56pub struct PrepareTextData {
57    /// Key is the element id
58    /// Value is the font data
59    pub font_data: HashMap<String, Vec<u8>>,
60}
61
62/// Represents the preparation data for the render process.
63/// It holds all static assets to be rendered.
64/// This is done once before the loop starts.
65/// Each asset will be stored on the display locally, and load during the render process by its
66/// asset id / element id
67#[derive(Serialize, Deserialize, PartialEq, Debug, Clone)]
68pub struct PrepareStaticImageData {
69    /// Key is the element id
70    /// Value is the element data
71    pub images_data: HashMap<String, Vec<u8>>,
72}
73
74/// Represents the preparation data for the render process.
75/// It holds all static assets to be rendered.
76/// This is done once before the loop starts.
77/// Each asset will be stored on the display locally, and load during the render process by its
78/// asset id / element id
79#[derive(Serialize, Deserialize, PartialEq, Debug, Clone)]
80pub struct PrepareConditionalImageData {
81    /// Key is the element id
82    /// Value is the element data
83    pub images_data: HashMap<String, HashMap<String, Vec<u8>>>,
84}
85
86/// Represents the display config.
87/// It holds the resolution and the elements to be rendered.
88#[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/// Represents a single element to be rendered on a display.
99#[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/// Represents a text element on a display.
122#[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/// Represents the text alignment of a text element.
145#[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/// Represents a static image element on a display.
157#[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/// Represents the type of a graph element on a display.
168#[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/// Represents a graph element on a display.
178#[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/// Represents a conditional image element on a display.
205#[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/// Represents the type of an element on a display.
224#[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/// Provides a single SensorValue
238#[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/// Represents the modifier of a sensor value.
253/// This is used to modify the value before rendering.
254/// For example to output the average or max value of a sensor.
255#[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/// Represents the type of a sensor value.
269/// This is used to determine how to render the value.
270/// For example a text value will be rendered as text, while a number value can be rendered as a graph.
271#[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
280/// Render the image
281/// The image will be a RGB8 png image
282pub 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    // Get the resolution from the lcd config
290    let image_width = display_config.resolution_width;
291    let image_height = display_config.resolution_height;
292
293    // Create a new ImageBuffer with the specified resolution
294    let mut image = ImageBuffer::new(image_width, image_height);
295
296    // Iterate over lcd elements and draw them on the image
297    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
306/// Draws a single element on the image.
307/// The element will be drawn on the given image buffer.
308/// Distinguishes between the different element types and calls the corresponding draw function.
309fn 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    // diff between type
320    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
360/// Draws a static image on the image buffer.
361fn 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    // Read image into memory
373    // We heavily assume that this is already png encoded to skip the expensive png decoding
374    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
393/// Draws a conditional image on the image buffer.
394fn 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
426/// Draws a text element on the image buffer.
427fn 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
462/// Converts a hex string to a Rgba<u8>
463/// The hex string must be in the format #RRGGBBAA
464/// Example: #FF0000CC
465/// Returns a Rgba<u8> struct
466pub 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
476/// Extracts the historical values from the sensor_value_history and reverses the order
477pub 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
496/// Checks if the given DirEntry is an image
497pub 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
504/// Get the cache directory for the given element
505pub 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
518/// Get the base cache directory
519pub fn get_cache_base_dir() -> PathBuf {
520    dirs::cache_dir()
521        .unwrap()
522        .join(std::env::var("SENSOR_BRIDGE_APP_NAME").unwrap())
523}
524
525/// Get the application config dir
526pub fn get_config_dir() -> PathBuf {
527    dirs::config_dir()
528        .unwrap()
529        .join(std::env::var("SENSOR_BRIDGE_APP_NAME").unwrap())
530}