#![allow(dead_code, unused_imports)]
use anyhow::{Context, Result};
use ht32_panel_hw::{
lcd::{Framebuffer, LcdDevice},
led::{LedDevice, LedTheme},
Orientation,
};
use serde::{Deserialize, Serialize};
use std::path::{Path, PathBuf};
use std::sync::{Mutex, RwLock};
use tracing::{debug, info, warn};
use crate::config::Config;
use crate::faces::{self, EnabledComplications, Face, Theme};
use crate::rendering::Canvas;
use crate::sensors::{
data::{IpDisplayPreference, SystemData},
CpuSensor, DiskSensor, MemorySensor, NetworkSensor, Sensor, SystemInfo, TemperatureSensor,
};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DisplaySettings {
#[serde(default = "default_face")]
pub face: String,
#[serde(default)]
pub orientation: String,
#[serde(default = "default_theme")]
pub theme: String,
#[serde(default = "default_led_theme")]
pub led_theme: u8,
#[serde(default = "default_led_value")]
pub led_intensity: u8,
#[serde(default = "default_led_value")]
pub led_speed: u8,
#[serde(default = "default_refresh_interval")]
pub refresh_interval: u32,
#[serde(default, skip_serializing)]
pub network_interface: Option<String>,
#[serde(default, skip_serializing)]
pub ip_display: Option<String>,
#[serde(default)]
pub complications: EnabledComplications,
}
fn default_face() -> String {
"professional".to_string()
}
fn default_theme() -> String {
"default".to_string()
}
fn default_led_theme() -> u8 {
2 }
fn default_led_value() -> u8 {
3
}
fn default_refresh_interval() -> u32 {
2500 }
impl Default for DisplaySettings {
fn default() -> Self {
Self {
face: default_face(),
orientation: "landscape".to_string(),
theme: default_theme(),
led_theme: default_led_theme(),
led_intensity: default_led_value(),
led_speed: default_led_value(),
refresh_interval: default_refresh_interval(),
network_interface: None,
ip_display: None,
complications: EnabledComplications::new(),
}
}
}
struct Sensors {
cpu: CpuSensor,
temperature: TemperatureSensor,
memory: MemorySensor,
network: NetworkSensor,
disk: DiskSensor,
system: SystemInfo,
}
impl Sensors {
fn new(network_interface: &str) -> Self {
Self {
cpu: CpuSensor::new(),
temperature: TemperatureSensor::new(),
memory: MemorySensor::new(),
network: NetworkSensor::new(network_interface),
disk: DiskSensor::auto(),
system: SystemInfo::new(),
}
}
fn new_auto() -> Self {
Self {
cpu: CpuSensor::new(),
temperature: TemperatureSensor::new(),
memory: MemorySensor::new(),
network: NetworkSensor::auto(),
disk: DiskSensor::auto(),
system: SystemInfo::new(),
}
}
fn sample(&mut self, ip_preference: IpDisplayPreference) -> SystemData {
let cpu_percent = self.cpu.sample();
let _ = self.temperature.sample(); let cpu_temp = self.temperature.temperature();
let ram_percent = self.memory.sample();
let _ = self.network.sample(); let _ = self.disk.sample();
let display_ip = match ip_preference {
IpDisplayPreference::Ipv6Gua => self.network.ipv6_gua(),
IpDisplayPreference::Ipv6Lla => self.network.ipv6_lla(),
IpDisplayPreference::Ipv6Ula => self.network.ipv6_ula(),
IpDisplayPreference::Ipv4 => self.network.ipv4_address(),
};
let (hour, minute, day, month, year, day_of_week, _) = self.system.time_components();
SystemData {
hostname: self.system.hostname(),
time: self.system.time(),
hour,
minute,
day,
month,
year,
day_of_week,
uptime: self.system.uptime(),
cpu_percent,
cpu_temp,
ram_percent,
disk_read_rate: self.disk.read_rate(),
disk_write_rate: self.disk.write_rate(),
disk_history: self.disk.history().clone(),
disk_read_history: self.disk.read_history().clone(),
disk_write_history: self.disk.write_history().clone(),
net_interface: self.network.interface_name().to_string(),
net_rx_rate: self.network.rx_rate(),
net_tx_rate: self.network.tx_rate(),
net_history: self.network.history().clone(),
net_rx_history: self.network.rx_history().clone(),
net_tx_history: self.network.tx_history().clone(),
display_ip,
}
}
}
pub struct AppState {
config: RwLock<Config>,
state_dir: PathBuf,
lcd: Option<Mutex<LcdDevice>>,
led_device_path: String,
orientation: RwLock<Orientation>,
canvas: RwLock<Canvas>,
framebuffer: RwLock<Framebuffer>,
needs_redraw: RwLock<bool>,
led_theme: RwLock<u8>,
led_intensity: RwLock<u8>,
led_speed: RwLock<u8>,
needs_led_update: RwLock<bool>,
sensors: Mutex<Sensors>,
face: RwLock<Box<dyn Face>>,
theme_name: RwLock<String>,
refresh_interval: RwLock<u32>,
complications: RwLock<EnabledComplications>,
}
impl AppState {
pub fn new(config: Config) -> Result<Self> {
let state_dir = PathBuf::from(&config.state_dir);
if let Err(e) = std::fs::create_dir_all(&state_dir) {
warn!("Failed to create state directory {:?}: {}", state_dir, e);
}
let settings = Self::load_display_settings(&state_dir);
let orientation: Orientation = settings.orientation.parse().unwrap_or_default();
let lcd = match LcdDevice::open() {
Ok(device) => {
if let Err(e) = device.heartbeat() {
warn!("Failed to send initial heartbeat: {}", e);
}
if let Err(e) = device.set_orientation(Orientation::Landscape) {
warn!("Failed to set initial orientation: {}", e);
}
info!("LCD device opened successfully");
Some(Mutex::new(device))
}
Err(e) => {
warn!("LCD device not found: {}. Running in headless mode.", e);
None
}
};
let (canvas_w, canvas_h) = orientation.dimensions();
let mut canvas = Canvas::new(canvas_w as u32, canvas_h as u32);
let framebuffer = Framebuffer::new();
let face = faces::create_face(&settings.face).unwrap_or_else(|| {
warn!(
"Unknown face '{}', falling back to 'professional'",
settings.face
);
faces::create_face("professional").unwrap()
});
info!("Using display face: {}", face.name());
let mut complications = settings.complications.clone();
complications.init_from_defaults(face.as_ref());
if let Some(ref ip_display) = settings.ip_display {
let face_name = face.name();
complications.set_option(
face_name,
faces::complication_names::IP_ADDRESS,
faces::complication_options::IP_TYPE,
ip_display.clone(),
);
info!("Migrated legacy ip_display setting: {}", ip_display);
}
if let Some(ref network_interface) = settings.network_interface {
let face_name = face.name();
complications.set_option(
face_name,
faces::complication_names::NETWORK,
faces::complication_options::INTERFACE,
network_interface.clone(),
);
info!(
"Migrated legacy network_interface setting: {}",
network_interface
);
}
let network_interface_value = complications
.get_option(
face.name(),
faces::complication_names::NETWORK,
faces::complication_options::INTERFACE,
)
.cloned();
let sensors = match network_interface_value.as_ref() {
Some(iface) if iface != "auto" && !iface.is_empty() => Sensors::new(iface),
_ => Sensors::new_auto(),
};
let theme = Theme::from_preset(&settings.theme);
canvas.set_background(theme.background);
info!("Display orientation: {}", orientation);
info!("Theme: {}", settings.theme);
Ok(Self {
led_device_path: config.devices.led.clone(),
led_theme: RwLock::new(settings.led_theme),
led_intensity: RwLock::new(settings.led_intensity),
led_speed: RwLock::new(settings.led_speed),
state_dir,
config: RwLock::new(config),
lcd,
orientation: RwLock::new(orientation),
canvas: RwLock::new(canvas),
framebuffer: RwLock::new(framebuffer),
needs_redraw: RwLock::new(true),
needs_led_update: RwLock::new(true),
sensors: Mutex::new(sensors),
face: RwLock::new(face),
theme_name: RwLock::new(settings.theme),
refresh_interval: RwLock::new(settings.refresh_interval),
complications: RwLock::new(complications),
})
}
fn load_display_settings(state_dir: &Path) -> DisplaySettings {
let settings_file = state_dir.join("display.toml");
if let Ok(content) = std::fs::read_to_string(&settings_file) {
if let Ok(settings) = toml::from_str(&content) {
return settings;
}
}
DisplaySettings::default()
}
fn save_display_settings(&self) {
let settings = DisplaySettings {
face: self.face.read().unwrap().name().to_string(),
orientation: self.orientation.read().unwrap().to_string(),
theme: self.theme_name.read().unwrap().clone(),
led_theme: *self.led_theme.read().unwrap(),
led_intensity: *self.led_intensity.read().unwrap(),
led_speed: *self.led_speed.read().unwrap(),
refresh_interval: *self.refresh_interval.read().unwrap(),
network_interface: None, ip_display: None, complications: self.complications.read().unwrap().clone(),
};
let settings_file = self.state_dir.join("display.toml");
match toml::to_string_pretty(&settings) {
Ok(content) => {
if let Err(e) = std::fs::write(&settings_file, content) {
warn!("Failed to save display settings: {}", e);
}
}
Err(e) => {
warn!("Failed to serialize display settings: {}", e);
}
}
}
pub fn config(&self) -> Config {
self.config.read().unwrap().clone()
}
pub fn update_config<F>(&self, f: F)
where
F: FnOnce(&mut Config),
{
let mut config = self.config.write().unwrap();
f(&mut config);
}
pub fn orientation(&self) -> Orientation {
*self.orientation.read().unwrap()
}
pub fn is_lcd_connected(&self) -> bool {
self.lcd.is_some()
}
pub fn is_web_enabled(&self) -> bool {
self.config.read().unwrap().web.enable
}
pub fn set_orientation(&self, orientation: Orientation) -> Result<()> {
if let Some(ref lcd) = self.lcd {
let device = lcd.lock().unwrap();
device.set_orientation(Orientation::Landscape)?;
}
*self.orientation.write().unwrap() = orientation;
let (width, height) = orientation.dimensions();
{
let mut canvas = self.canvas.write().unwrap();
canvas.resize(width as u32, height as u32);
canvas.clear(); }
{
let mut fb = self.framebuffer.write().unwrap();
fb.resize(320, 170);
fb.clear(0); }
*self.needs_redraw.write().unwrap() = true;
self.save_display_settings();
info!("Orientation set to: {}", orientation);
Ok(())
}
pub fn refresh_interval(&self) -> u32 {
*self.refresh_interval.read().unwrap()
}
pub fn refresh_interval_ms(&self) -> u32 {
*self.refresh_interval.read().unwrap()
}
pub fn set_refresh_interval(&self, ms: u32) {
let clamped = ms.clamp(500, 10000);
*self.refresh_interval.write().unwrap() = clamped;
self.save_display_settings();
info!("Refresh interval set to {}ms", clamped);
}
pub fn led_settings(&self) -> (u8, u8, u8) {
(
*self.led_theme.read().unwrap(),
*self.led_intensity.read().unwrap(),
*self.led_speed.read().unwrap(),
)
}
pub async fn set_led(&self, theme: u8, intensity: u8, speed: u8) -> Result<()> {
*self.led_theme.write().unwrap() = theme;
*self.led_intensity.write().unwrap() = intensity;
*self.led_speed.write().unwrap() = speed;
self.save_display_settings();
let led = LedDevice::new(&self.led_device_path);
let led_theme = LedTheme::from_byte(theme)?;
if let Err(e) = led.set_theme(led_theme, intensity, speed).await {
warn!(
"Failed to send LED command to {}: {}",
self.led_device_path, e
);
return Err(e.into());
}
info!(
"LED set to theme {} (intensity: {}, speed: {})",
theme, intensity, speed
);
Ok(())
}
pub async fn led_off(&self) -> Result<()> {
let led = LedDevice::new(&self.led_device_path);
led.set_off().await?;
*self.led_theme.write().unwrap() = 4; self.save_display_settings();
info!("LED turned off");
Ok(())
}
pub fn send_heartbeat(&self) -> Result<()> {
if let Some(ref lcd) = self.lcd {
let device = lcd.lock().unwrap();
device.heartbeat()?;
debug!("Heartbeat sent");
}
Ok(())
}
fn sample_sensors(&self) -> SystemData {
let mut sensors = self.sensors.lock().unwrap();
let ip_preference = self.get_ip_display_from_complications();
sensors.sample(ip_preference)
}
fn get_ip_display_from_complications(&self) -> IpDisplayPreference {
let face_name = self.face.read().unwrap().name().to_string();
let complications = self.complications.read().unwrap();
complications
.get_option(
&face_name,
faces::complication_names::IP_ADDRESS,
faces::complication_options::IP_TYPE,
)
.and_then(|s| s.parse().ok())
.unwrap_or(IpDisplayPreference::Ipv6Gua)
}
fn get_network_interface_from_complications(&self) -> Option<String> {
let face_name = self.face.read().unwrap().name().to_string();
let complications = self.complications.read().unwrap();
complications
.get_option(
&face_name,
faces::complication_names::NETWORK,
faces::complication_options::INTERFACE,
)
.filter(|s| *s != "auto" && !s.is_empty())
.cloned()
}
pub async fn render_frame(&self) -> Result<()> {
let system_data = self.sample_sensors();
let theme = Theme::from_preset(&self.theme_name.read().unwrap());
{
let mut canvas = self.canvas.write().unwrap();
let face = self.face.read().unwrap();
let complications = self.complications.read().unwrap();
canvas.clear();
face.render(&mut canvas, &system_data, &theme, &complications);
}
{
let canvas = self.canvas.read().unwrap();
let mut framebuffer = self.framebuffer.write().unwrap();
let orientation = *self.orientation.read().unwrap();
self.render_with_orientation(&canvas, &mut framebuffer, orientation)?;
if let Some(ref lcd) = self.lcd {
let device = lcd.lock().unwrap();
device.redraw(&framebuffer)?;
}
}
let needs_led = *self.needs_led_update.read().unwrap();
if needs_led {
let (theme, intensity, speed) = self.led_settings();
if let Err(e) = self.set_led(theme, intensity, speed).await {
tracing::warn!("LED update failed: {}", e);
}
*self.needs_led_update.write().unwrap() = false;
}
Ok(())
}
fn render_with_orientation(
&self,
canvas: &Canvas,
framebuffer: &mut Framebuffer,
orientation: Orientation,
) -> Result<()> {
use ht32_panel_hw::lcd::rgb888_to_rgb565;
let pixels = canvas.pixmap_pixels();
let fb_data = framebuffer.data_mut();
let (cw, ch) = canvas.dimensions();
match orientation {
Orientation::Landscape => {
for (i, pixel) in pixels.iter().enumerate() {
if i < fb_data.len() {
fb_data[i] = rgb888_to_rgb565(pixel.red(), pixel.green(), pixel.blue());
}
}
}
Orientation::LandscapeUpsideDown => {
let len = fb_data.len();
for (i, pixel) in pixels.iter().enumerate() {
if i < len {
fb_data[len - 1 - i] =
rgb888_to_rgb565(pixel.red(), pixel.green(), pixel.blue());
}
}
}
Orientation::Portrait => {
for y in 0..ch {
for x in 0..cw {
let src_idx = (y * cw + x) as usize;
let dst_x = ch - 1 - y;
let dst_y = x;
let dst_idx = (dst_y * 320 + dst_x) as usize;
if src_idx < pixels.len() && dst_idx < fb_data.len() {
let pixel = &pixels[src_idx];
fb_data[dst_idx] =
rgb888_to_rgb565(pixel.red(), pixel.green(), pixel.blue());
}
}
}
}
Orientation::PortraitUpsideDown => {
for y in 0..ch {
for x in 0..cw {
let src_idx = (y * cw + x) as usize;
let dst_x = y;
let dst_y = cw - 1 - x;
let dst_idx = (dst_y * 320 + dst_x) as usize;
if src_idx < pixels.len() && dst_idx < fb_data.len() {
let pixel = &pixels[src_idx];
fb_data[dst_idx] =
rgb888_to_rgb565(pixel.red(), pixel.green(), pixel.blue());
}
}
}
}
}
Ok(())
}
pub fn force_redraw(&self) {
*self.needs_redraw.write().unwrap() = true;
}
pub fn get_screen_png(&self) -> Result<Vec<u8>> {
let canvas = self.canvas.read().unwrap();
let (width, height) = canvas.dimensions();
let rgba = canvas.pixels();
let mut png_data = Vec::new();
{
let mut encoder = png::Encoder::new(&mut png_data, width, height);
encoder.set_color(png::ColorType::Rgba);
encoder.set_depth(png::BitDepth::Eight);
let mut writer = encoder.write_header()?;
writer.write_image_data(rgba)?;
}
Ok(png_data)
}
pub fn clear_display(&self, color: u16) -> Result<()> {
{
let mut fb = self.framebuffer.write().unwrap();
fb.clear(color);
}
self.force_redraw();
Ok(())
}
pub fn with_canvas<F, R>(&self, f: F) -> R
where
F: FnOnce(&mut Canvas) -> R,
{
let mut canvas = self.canvas.write().unwrap();
let result = f(&mut canvas);
*self.needs_redraw.write().unwrap() = true;
result
}
pub fn read_canvas<F, R>(&self, f: F) -> R
where
F: FnOnce(&Canvas) -> R,
{
let canvas = self.canvas.read().unwrap();
f(&canvas)
}
pub fn set_face(&self, name: &str) -> Result<()> {
if let Some(new_face) = faces::create_face(name) {
{
let mut complications = self.complications.write().unwrap();
complications.init_from_defaults(new_face.as_ref());
}
*self.face.write().unwrap() = new_face;
self.save_display_settings();
info!("Display face changed to: {}", name);
Ok(())
} else {
Err(anyhow::anyhow!("Unknown face: {}", name))
}
}
pub fn face_name(&self) -> String {
self.face.read().unwrap().name().to_string()
}
pub fn available_complications(&self) -> Vec<faces::Complication> {
self.face.read().unwrap().available_complications()
}
pub fn enabled_complications(&self) -> std::collections::HashSet<String> {
let face_name = self.face.read().unwrap().name().to_string();
self.complications.read().unwrap().get_enabled(&face_name)
}
pub fn set_complication_enabled(&self, complication_id: &str, enabled: bool) -> Result<()> {
let face_name = self.face.read().unwrap().name().to_string();
let available: Vec<_> = self.face.read().unwrap().available_complications();
if !available.iter().any(|c| c.id == complication_id) {
return Err(anyhow::anyhow!(
"Unknown complication '{}' for face '{}'",
complication_id,
face_name
));
}
self.complications
.write()
.unwrap()
.set_enabled(&face_name, complication_id, enabled);
self.save_display_settings();
*self.needs_redraw.write().unwrap() = true;
info!(
"Complication '{}' {} for face '{}'",
complication_id,
if enabled { "enabled" } else { "disabled" },
face_name
);
Ok(())
}
pub fn theme_name(&self) -> String {
self.theme_name.read().unwrap().clone()
}
pub fn set_theme(&self, name: &str) -> Result<()> {
if !faces::available_themes().iter().any(|t| t.id == name) {
return Err(anyhow::anyhow!("Unknown theme: {}", name));
}
*self.theme_name.write().unwrap() = name.to_string();
let theme = Theme::from_preset(name);
self.canvas
.write()
.unwrap()
.set_background(theme.background);
*self.needs_redraw.write().unwrap() = true;
self.save_display_settings();
info!("Theme set to: {}", name);
Ok(())
}
pub fn available_themes(&self) -> Vec<faces::ThemeInfo> {
faces::available_themes()
}
pub fn display_settings(&self) -> DisplaySettings {
DisplaySettings {
face: self.face.read().unwrap().name().to_string(),
orientation: self.orientation.read().unwrap().to_string(),
theme: self.theme_name.read().unwrap().clone(),
led_theme: *self.led_theme.read().unwrap(),
led_intensity: *self.led_intensity.read().unwrap(),
led_speed: *self.led_speed.read().unwrap(),
refresh_interval: *self.refresh_interval.read().unwrap(),
network_interface: None,
ip_display: None,
complications: self.complications.read().unwrap().clone(),
}
}
pub fn ip_display(&self) -> IpDisplayPreference {
self.get_ip_display_from_complications()
}
pub fn set_ip_display(&self, preference: IpDisplayPreference) {
let face_name = self.face.read().unwrap().name().to_string();
self.complications.write().unwrap().set_option(
&face_name,
faces::complication_names::IP_ADDRESS,
faces::complication_options::IP_TYPE,
preference.to_string(),
);
self.save_display_settings();
info!("IP display preference set to: {}", preference);
}
pub fn network_interface(&self) -> Option<String> {
self.get_network_interface_from_complications()
}
pub fn network_interface_config(&self) -> String {
let sensors = self.sensors.lock().unwrap();
sensors.network.interface_name().to_string()
}
pub fn set_network_interface(&self, interface: Option<String>) {
let face_name = self.face.read().unwrap().name().to_string();
let value = interface.clone().unwrap_or_else(|| "auto".to_string());
self.complications.write().unwrap().set_option(
&face_name,
faces::complication_names::NETWORK,
faces::complication_options::INTERFACE,
value.clone(),
);
let mut sensors = self.sensors.lock().unwrap();
if value == "auto" || value.is_empty() {
sensors.network.set_auto();
} else {
sensors.network.set_interface(&value);
}
self.save_display_settings();
}
pub fn list_network_interfaces(&self) -> Vec<String> {
NetworkSensor::list_interfaces()
}
pub fn get_complication_option(
&self,
complication_id: &str,
option_id: &str,
) -> Option<String> {
let face_name = self.face.read().unwrap().name().to_string();
self.complications
.read()
.unwrap()
.get_option(&face_name, complication_id, option_id)
.cloned()
}
pub fn set_complication_option(
&self,
complication_id: &str,
option_id: &str,
value: &str,
) -> anyhow::Result<()> {
let face_name = self.face.read().unwrap().name().to_string();
let available = self.face.read().unwrap().available_complications();
let complication = available
.iter()
.find(|c| c.id == complication_id)
.ok_or_else(|| {
anyhow::anyhow!(
"Unknown complication '{}' for face '{}'",
complication_id,
face_name
)
})?;
let option = complication
.options
.iter()
.find(|o| o.id == option_id)
.ok_or_else(|| {
anyhow::anyhow!(
"Unknown option '{}' for complication '{}'",
option_id,
complication_id
)
})?;
if let faces::ComplicationOptionType::Choice(choices) = &option.option_type {
if !choices.iter().any(|c| c.value == value) {
if complication_id == faces::complication_names::NETWORK
&& option_id == faces::complication_options::INTERFACE
{
let interfaces = NetworkSensor::list_interfaces();
if value != "auto" && !interfaces.contains(&value.to_string()) {
return Err(anyhow::anyhow!(
"Unknown interface '{}'. Available: auto, {:?}",
value,
interfaces
));
}
} else {
let valid_values: Vec<_> = choices.iter().map(|c| c.value.as_str()).collect();
return Err(anyhow::anyhow!(
"Invalid value '{}' for option '{}'. Valid values: {:?}",
value,
option_id,
valid_values
));
}
}
}
self.complications.write().unwrap().set_option(
&face_name,
complication_id,
option_id,
value.to_string(),
);
if complication_id == faces::complication_names::NETWORK
&& option_id == faces::complication_options::INTERFACE
{
let mut sensors = self.sensors.lock().unwrap();
if value == "auto" || value.is_empty() {
sensors.network.set_auto();
} else {
sensors.network.set_interface(value);
}
}
self.save_display_settings();
*self.needs_redraw.write().unwrap() = true;
info!(
"Complication option '{}.{}' set to '{}' for face '{}'",
complication_id, option_id, value, face_name
);
Ok(())
}
}