use std::collections::HashMap;
use std::fs;
use std::fs::DirEntry;
use std::path::{Path, PathBuf};
use std::time::Instant;
use image::{ImageBuffer, ImageFormat, Rgba};
use log::{debug, error};
use serde::{Deserialize, Serialize};
pub mod conditional_image_renderer;
pub mod graph_renderer;
pub mod text_renderer;
#[derive(Serialize, Deserialize, PartialEq, Debug, Clone)]
pub struct TransportMessage {
pub transport_type: TransportType,
pub data: Vec<u8>,
}
#[derive(Serialize, Deserialize, PartialEq, Eq, Debug, Clone)]
pub enum TransportType {
PrepareText,
PrepareStaticImage,
PrepareConditionalImage,
RenderImage,
}
#[derive(Serialize, Deserialize, PartialEq, Debug, Default, Clone)]
pub struct RenderData {
pub display_config: DisplayConfig,
pub sensor_values: Vec<SensorValue>,
}
#[derive(Serialize, Deserialize, PartialEq, Debug, Clone)]
pub struct PrepareTextData {
pub font_data: HashMap<String, Vec<u8>>,
}
#[derive(Serialize, Deserialize, PartialEq, Debug, Clone)]
pub struct PrepareStaticImageData {
pub images_data: HashMap<String, Vec<u8>>,
}
#[derive(Serialize, Deserialize, PartialEq, Debug, Clone)]
pub struct PrepareConditionalImageData {
pub images_data: HashMap<String, HashMap<String, Vec<u8>>>,
}
#[derive(Serialize, Deserialize, PartialEq, Debug, Default, Clone)]
pub struct DisplayConfig {
#[serde(default)]
pub resolution_height: u32,
#[serde(default)]
pub resolution_width: u32,
#[serde(default)]
pub elements: Vec<ElementConfig>,
}
#[derive(Serialize, Deserialize, PartialEq, Debug, Default, Clone)]
pub struct ElementConfig {
#[serde(default)]
pub id: String,
#[serde(default)]
pub name: String,
#[serde(default)]
pub element_type: ElementType,
#[serde(default)]
pub x: i32,
#[serde(default)]
pub y: i32,
#[serde(default)]
pub text_config: Option<TextConfig>,
#[serde(default)]
pub image_config: Option<ImageConfig>,
#[serde(default)]
pub graph_config: Option<GraphConfig>,
#[serde(default)]
pub conditional_image_config: Option<ConditionalImageConfig>,
}
#[derive(Serialize, Deserialize, PartialEq, Debug, Default, Clone)]
pub struct TextConfig {
#[serde(default)]
pub sensor_id: String,
#[serde(default)]
pub value_modifier: SensorValueModifier,
#[serde(default)]
pub format: String,
#[serde(default)]
pub font_family: String,
#[serde(default)]
pub font_size: u32,
#[serde(default)]
pub font_color: String,
#[serde(default)]
pub width: u32,
#[serde(default)]
pub height: u32,
#[serde(default)]
pub alignment: TextAlign,
}
#[derive(Serialize, Deserialize, PartialEq, Eq, Debug, Default, Clone)]
pub enum TextAlign {
#[default]
#[serde(rename = "left")]
Left,
#[serde(rename = "center")]
Center,
#[serde(rename = "right")]
Right,
}
#[derive(Serialize, Deserialize, PartialEq, Debug, Default, Clone)]
pub struct ImageConfig {
#[serde(default)]
pub width: u32,
#[serde(default)]
pub height: u32,
#[serde(default)]
pub image_path: String,
}
#[derive(Serialize, Deserialize, PartialEq, Debug, Default, Clone)]
pub enum GraphType {
#[default]
#[serde(rename = "line")]
Line,
#[serde(rename = "line-fill")]
LineFill,
}
#[derive(Serialize, Deserialize, PartialEq, Debug, Default, Clone)]
pub struct GraphConfig {
#[serde(default)]
pub sensor_id: String,
#[serde(default)]
pub sensor_values: Vec<f64>,
#[serde(default)]
pub min_sensor_value: Option<f64>,
#[serde(default)]
pub max_sensor_value: Option<f64>,
#[serde(default)]
pub width: u32,
#[serde(default)]
pub height: u32,
#[serde(default)]
pub graph_type: GraphType,
#[serde(default)]
pub graph_color: String,
#[serde(default)]
pub graph_stroke_width: i32,
#[serde(default)]
pub background_color: String,
#[serde(default)]
pub border_color: String,
}
#[derive(Serialize, Deserialize, PartialEq, Debug, Default, Clone)]
pub struct ConditionalImageConfig {
#[serde(default)]
pub sensor_id: String,
#[serde(default)]
pub sensor_value: String,
#[serde(default)]
pub images_path: String,
#[serde(default)]
pub min_sensor_value: f64,
#[serde(default)]
pub max_sensor_value: f64,
#[serde(default)]
pub width: u32,
#[serde(default)]
pub height: u32,
}
#[derive(Serialize, Deserialize, PartialEq, Eq, Debug, Default, Clone)]
pub enum ElementType {
#[default]
#[serde(rename = "text")]
Text,
#[serde(rename = "static-image")]
StaticImage,
#[serde(rename = "graph")]
Graph,
#[serde(rename = "conditional-image")]
ConditionalImage,
}
#[derive(Serialize, Deserialize, PartialEq, Debug, Clone, Default)]
pub struct SensorValue {
#[serde(default)]
pub id: String,
#[serde(default)]
pub value: String,
#[serde(default)]
pub unit: String,
#[serde(default)]
pub label: String,
#[serde(default)]
pub sensor_type: SensorType,
}
#[derive(Serialize, Deserialize, PartialEq, Eq, Debug, Default, Clone)]
pub enum SensorValueModifier {
#[default]
#[serde(rename = "none")]
None,
#[serde(rename = "min")]
Min,
#[serde(rename = "max")]
Max,
#[serde(rename = "avg")]
Avg,
}
#[derive(Serialize, Deserialize, PartialEq, Eq, Debug, Default, Clone)]
pub enum SensorType {
#[default]
#[serde(rename = "text")]
Text,
#[serde(rename = "number")]
Number,
}
pub fn render_lcd_image(
display_config: DisplayConfig,
sensor_value_history: &[Vec<SensorValue>],
fonts_data: &HashMap<String, Vec<u8>>,
) -> ImageBuffer<Rgba<u8>, Vec<u8>> {
let start_time = Instant::now();
let image_width = display_config.resolution_width;
let image_height = display_config.resolution_height;
let mut image = ImageBuffer::new(image_width, image_height);
for lcd_element in display_config.elements {
draw_element(&mut image, lcd_element, sensor_value_history, fonts_data);
}
debug!(" = Total frame render duration: {:?}", start_time.elapsed());
image
}
fn draw_element(
image: &mut ImageBuffer<Rgba<u8>, Vec<u8>>,
lcd_element: ElementConfig,
sensor_value_history: &[Vec<SensorValue>],
fonts_data: &HashMap<String, Vec<u8>>,
) {
let x = lcd_element.x;
let y = lcd_element.y;
let element_id = lcd_element.id.as_str();
match lcd_element.element_type {
ElementType::Text => {
let text_config = lcd_element.text_config.unwrap();
draw_text(
image,
&lcd_element.id,
text_config,
x,
y,
sensor_value_history,
fonts_data,
);
}
ElementType::StaticImage => {
draw_static_image(image, &lcd_element.id, x, y);
}
ElementType::Graph => {
let mut graph_config = lcd_element.graph_config.unwrap();
graph_config.sensor_values =
extract_value_sequence(sensor_value_history, &graph_config.sensor_id);
draw_graph(image, x, y, graph_config);
}
ElementType::ConditionalImage => {
let conditional_image_config = lcd_element.conditional_image_config.unwrap();
let sensor_value = sensor_value_history[0]
.iter()
.find(|&s| s.id == conditional_image_config.sensor_id);
draw_conditional_image(
image,
x,
y,
element_id,
conditional_image_config,
sensor_value,
)
}
}
}
fn draw_static_image(image: &mut ImageBuffer<Rgba<u8>, Vec<u8>>, element_id: &str, x: i32, y: i32) {
let start_time = Instant::now();
let cache_dir = get_cache_dir(element_id, &ElementType::StaticImage).join(element_id);
let file_path = cache_dir.to_str().unwrap();
if !Path::new(&file_path).exists() {
error!("File {} does not exist", file_path);
return;
}
let img_data = fs::read(file_path).unwrap();
let overlay_image = image::load_from_memory(&img_data).unwrap();
image::imageops::overlay(image, &overlay_image, x as i64, y as i64);
debug!(" - Image render duration: {:?}", start_time.elapsed());
}
fn draw_graph(image: &mut ImageBuffer<Rgba<u8>, Vec<u8>>, x: i32, y: i32, config: GraphConfig) {
let start_time = Instant::now();
let img_data = graph_renderer::render(&config);
let graph_image = image::load_from_memory(&img_data).unwrap();
image::imageops::overlay(image, &graph_image, x as i64, y as i64);
debug!(" - Graph render duration: {:?}", start_time.elapsed());
}
fn draw_conditional_image(
image: &mut ImageBuffer<Rgba<u8>, Vec<u8>>,
x: i32,
y: i32,
element_id: &str,
mut config: ConditionalImageConfig,
sensor_value: Option<&SensorValue>,
) {
let start_time = Instant::now();
let sensor_value = match sensor_value {
None => {
return;
}
Some(sensor_value) => sensor_value,
};
config.sensor_value = sensor_value.value.clone();
let img_data =
conditional_image_renderer::render(element_id, &sensor_value.sensor_type, &config);
if let Some(img_data) = img_data {
let conditional_image = image::load_from_memory(&img_data).unwrap();
image::imageops::overlay(image, &conditional_image, x as i64, y as i64);
}
debug!(
" - Conditional image render duration: {:?}",
start_time.elapsed()
);
}
fn draw_text(
image: &mut ImageBuffer<Rgba<u8>, Vec<u8>>,
_element_id: &str,
text_config: TextConfig,
x: i32,
y: i32,
sensor_value_history: &[Vec<SensorValue>],
fonts_data: &HashMap<String, Vec<u8>>,
) {
let start_time = Instant::now();
let font_data = match fonts_data.get(&text_config.font_family) {
Some(font_data) => font_data,
None => {
error!(
"Font data for font family {} not found",
text_config.font_family
);
return;
}
};
let font = rusttype::Font::try_from_bytes(font_data).unwrap();
let text_image = text_renderer::render(
image.width(),
image.height(),
&text_config,
sensor_value_history,
&font,
);
image::imageops::overlay(image, &text_image, x as i64, y as i64);
debug!(" - Text render duration: {:?}", start_time.elapsed());
}
pub fn hex_to_rgba(hex_string: &str) -> Rgba<u8> {
let hex_string = hex_string.trim_start_matches('#');
let hex = u32::from_str_radix(hex_string, 16).unwrap();
let r = ((hex >> 24) & 0xff) as u8;
let g = ((hex >> 16) & 0xff) as u8;
let b = ((hex >> 8) & 0xff) as u8;
let a = (hex & 0xff) as u8;
Rgba([r, g, b, a])
}
pub fn extract_value_sequence(
sensor_value_history: &[Vec<SensorValue>],
sensor_id: &str,
) -> Vec<f64> {
let mut sensor_values: Vec<f64> = sensor_value_history
.iter()
.flat_map(|history_entry| {
history_entry.iter().find_map(|entry| {
if entry.id.eq(sensor_id) {
return entry.value.parse().ok();
}
None
})
})
.collect();
sensor_values.reverse();
sensor_values
}
pub fn is_image(dir_entry: &DirEntry) -> bool {
let entry_path = dir_entry.path();
let extension_string = entry_path.extension().map(|ext| ext.to_str().unwrap());
let image_format = extension_string.and_then(ImageFormat::from_extension);
image_format.map(|x| x.can_read()).unwrap_or(false)
}
pub fn get_cache_dir(element_id: &str, element_type: &ElementType) -> PathBuf {
let element_type_folder_name = match element_type {
ElementType::Text => "text",
ElementType::StaticImage => "static-image",
ElementType::Graph => "graph",
ElementType::ConditionalImage => "conditional-image",
};
get_cache_base_dir()
.join(element_type_folder_name)
.join(element_id)
}
pub fn get_cache_base_dir() -> PathBuf {
dirs::cache_dir()
.unwrap()
.join(std::env::var("SENSOR_BRIDGE_APP_NAME").unwrap())
}
pub fn get_config_dir() -> PathBuf {
dirs::config_dir()
.unwrap()
.join(std::env::var("SENSOR_BRIDGE_APP_NAME").unwrap())
}