use homie_controller::{Datatype, Device, HomieController, Node, State};
use log::{debug, error, trace};
use rainbow_hat_rs::{
alphanum4::Alphanum4,
apa102::{APA102, NUM_PIXELS},
touch::Buttons,
};
use std::{
collections::HashMap,
sync::{Arc, Mutex},
time::Duration,
};
use tokio::{
task::{self, JoinHandle},
time::sleep,
};
const TEMPERATURE_PROPERTY_ID: &str = "temperature";
const HUMIDITY_PROPERTY_ID: &str = "humidity";
const NAME_ID: &str = "name";
const PROPERTY_IDS: [&str; 3] = [TEMPERATURE_PROPERTY_ID, HUMIDITY_PROPERTY_ID, NAME_ID];
const BRIGHTNESS_LEVELS: [u8; 4] = [10, 3, 0, 31];
const BUTTON_POLL_PERIOD: Duration = Duration::from_millis(100);
#[derive(Debug)]
pub struct UiState {
controller: Arc<HomieController>,
alphanum: Alphanum4,
pixels: APA102,
selected_device_id: Option<String>,
selected_node_id: Option<String>,
selected_property_id: String,
selected_brightness: u8,
button_state: [bool; 3],
}
impl UiState {
pub fn new(controller: Arc<HomieController>, alphanum: Alphanum4, pixels: APA102) -> Self {
Self {
controller,
alphanum,
pixels,
selected_device_id: None,
selected_node_id: None,
selected_property_id: TEMPERATURE_PROPERTY_ID.to_string(),
selected_brightness: BRIGHTNESS_LEVELS[0],
button_state: Default::default(),
}
}
pub fn update_display(&mut self) {
let devices = self.controller.devices();
let nodes = find_nodes(&devices);
for i in 0..NUM_PIXELS {
let (r, g, b) = if let Some((device_id, node_id, node)) = nodes.get(i) {
let selected = Some(*device_id) == self.selected_device_id.as_deref()
&& Some(*node_id) == self.selected_node_id.as_deref();
trace!("Showing node {node:?}");
colour_for_node(node, selected)
} else {
(0, 0, 0)
};
self.pixels.pixels[NUM_PIXELS - 1 - i] = [r, g, b, self.selected_brightness];
}
if let Err(e) = self.pixels.show() {
error!("Error setting RGB LEDs: {e}");
}
if self.selected_device_id.is_none() || self.selected_node_id.is_none() {
if let Some((device_id, node_id, _)) = nodes.first() {
self.selected_device_id = Some(device_id.to_string());
self.selected_node_id = Some(node_id.to_string());
}
}
if self.selected_brightness == 0 {
self.alphanum.print_str(" ", false);
} else if let (Some(selected_device_id), Some(selected_node_id)) =
(&self.selected_device_id, &self.selected_node_id)
{
if let Some(value) = get_property(
&devices,
selected_device_id,
selected_node_id,
&self.selected_property_id,
) {
print_str_decimal(
&mut self.alphanum,
value,
if self.selected_property_id == HUMIDITY_PROPERTY_ID {
Some('%')
} else {
None
},
);
} else {
self.alphanum.print_str("gone", false);
}
} else {
self.alphanum.print_str(" ", false);
}
if let Err(e) = self.alphanum.show() {
error!("Error displaying: {e}");
}
}
fn button_pressed(&mut self, button_index: usize) {
debug!("Button {button_index} pressed.");
match button_index {
0 => {
let devices = self.controller.devices();
let nodes = find_nodes(&devices);
if !nodes.is_empty() {
let new_index = if let Some(selected_node_id) = &self.selected_node_id {
if let Some(current_index) = nodes
.iter()
.position(|(_, node_id, _)| node_id == selected_node_id)
{
(current_index + 1) % nodes.len()
} else {
0
}
} else {
0
};
self.selected_node_id = Some(nodes[new_index].1.to_string());
}
}
1 => {
let current_index = PROPERTY_IDS
.iter()
.position(|x| x == &self.selected_property_id)
.unwrap_or(0);
self.selected_property_id =
PROPERTY_IDS[(current_index + 1) % PROPERTY_IDS.len()].to_string();
}
2 => {
let current_index = BRIGHTNESS_LEVELS
.iter()
.position(|&level| level == self.selected_brightness)
.unwrap_or(0);
self.selected_brightness =
BRIGHTNESS_LEVELS[(current_index + 1) % BRIGHTNESS_LEVELS.len()];
}
_ => {}
}
self.update_display();
}
fn update_button_state(&mut self, new_state: [bool; 3]) {
for (i, &new_state) in new_state.iter().enumerate() {
if new_state && !self.button_state[i] {
self.button_pressed(i);
}
}
self.button_state = new_state;
}
}
pub fn spawn_button_poll_loop(
mut buttons: Buttons,
ui_state: Arc<Mutex<UiState>>,
) -> JoinHandle<()> {
task::spawn(async move {
loop {
let new_state = [
buttons.a.is_pressed(),
buttons.b.is_pressed(),
buttons.c.is_pressed(),
];
ui_state.lock().unwrap().update_button_state(new_state);
sleep(BUTTON_POLL_PERIOD).await;
}
})
}
fn get_property<'a>(
devices: &'a HashMap<String, Device>,
device_id: &str,
node_id: &str,
property_id: &str,
) -> Option<&'a str> {
let node = devices.get(device_id)?.nodes.get(node_id)?;
if property_id == NAME_ID {
node.name.as_deref()
} else {
node.properties.get(property_id)?.value.as_deref()
}
}
fn print_str_decimal(alphanum: &mut Alphanum4, s: &str, unit: Option<char>) {
let number_width = if unit.is_some() { 3usize } else { 4 };
let padding = number_width.saturating_sub(if s.contains('.') {
s.len() - 1
} else {
s.len()
});
for position in 0..padding {
alphanum.set_digit(position, ' ', false);
}
let mut position = padding;
for c in s.chars() {
if c == '.' {
if position == 0 {
alphanum.set_digit(position, '0', true);
position += 1;
} else {
alphanum.set_decimal(position - 1, true);
}
} else {
alphanum.set_digit(position, c, false);
position += 1;
}
if position >= number_width {
break;
}
}
if let Some(unit) = unit {
alphanum.set_digit(3, unit, false);
}
}
fn find_nodes(devices: &HashMap<String, Device>) -> Vec<(&str, &str, &Node)> {
let mut nodes: Vec<(&str, &str, &Node)> = vec![];
for (device_id, device) in devices {
if device.state == State::Ready {
for (node_id, node) in &device.nodes {
if let (Some(temperature_node), Some(humidity_node)) = (
node.properties.get(TEMPERATURE_PROPERTY_ID),
node.properties.get(HUMIDITY_PROPERTY_ID),
) {
if temperature_node.datatype == Some(Datatype::Float)
&& humidity_node.datatype == Some(Datatype::Integer)
{
nodes.push((device_id, node_id, node));
}
}
}
}
}
nodes
}
fn colour_for_node(node: &Node, selected: bool) -> (u8, u8, u8) {
let temperature: f64 = node
.properties
.get(TEMPERATURE_PROPERTY_ID)
.unwrap()
.value()
.unwrap();
let humidity: i64 = node
.properties
.get(HUMIDITY_PROPERTY_ID)
.unwrap()
.value()
.unwrap();
(
scale_to_u8(temperature, 0.0, 40.0),
scale_to_u8(humidity as f64, 30.0, 100.0),
if selected { 128 } else { 0 },
)
}
fn scale_to_u8(value: f64, low: f64, high: f64) -> u8 {
(255.0 * (value - low) / (high - low)) as u8
}