better ui with 3 lines now
This commit is contained in:
@@ -9,7 +9,7 @@ name = "projekt_final"
|
|||||||
path = "./src/bin/main.rs"
|
path = "./src/bin/main.rs"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
pages-tui = { path = "../../../pages-tui", default-features = false }
|
pages-tui = { path = "../../../pages-tui" }
|
||||||
esp-bootloader-esp-idf = { version = "0.2.0", features = ["esp32"] }
|
esp-bootloader-esp-idf = { version = "0.2.0", features = ["esp32"] }
|
||||||
esp-hal = { version = "=1.0.0-rc.0", features = [
|
esp-hal = { version = "=1.0.0-rc.0", features = [
|
||||||
"esp32",
|
"esp32",
|
||||||
|
|||||||
@@ -10,11 +10,8 @@ use pages_tui::input::Key;
|
|||||||
/// IMU sensor reading from MPU6050
|
/// IMU sensor reading from MPU6050
|
||||||
#[derive(Clone, Copy, Default, Debug)]
|
#[derive(Clone, Copy, Default, Debug)]
|
||||||
pub struct ImuReading {
|
pub struct ImuReading {
|
||||||
/// Acceleration in g (earth gravity units)
|
|
||||||
pub accel_g: [f32; 3],
|
pub accel_g: [f32; 3],
|
||||||
/// Angular velocity in degrees per second
|
|
||||||
pub gyro_dps: [f32; 3],
|
pub gyro_dps: [f32; 3],
|
||||||
/// Temperature in Celsius
|
|
||||||
pub temp_c: f32,
|
pub temp_c: f32,
|
||||||
/// Timestamp in milliseconds since boot
|
/// Timestamp in milliseconds since boot
|
||||||
pub timestamp_ms: u64,
|
pub timestamp_ms: u64,
|
||||||
@@ -35,4 +32,5 @@ pub enum DisplayCommand {
|
|||||||
Clear,
|
Clear,
|
||||||
|
|
||||||
PushKey(Key),
|
PushKey(Key),
|
||||||
|
AddChatMessage(HString<24>),
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,5 @@
|
|||||||
// src/display/api.rs
|
// src/display/api.rs
|
||||||
//! Public API for the display feature.
|
//! Public API for the display feature.
|
||||||
//!
|
|
||||||
//! Other parts of the system use this module to send commands to the display.
|
|
||||||
//! The actual rendering happens in `task.rs`.
|
|
||||||
|
|
||||||
use embassy_sync::blocking_mutex::raw::CriticalSectionRawMutex;
|
use embassy_sync::blocking_mutex::raw::CriticalSectionRawMutex;
|
||||||
use embassy_sync::channel::{Channel, Receiver, TrySendError};
|
use embassy_sync::channel::{Channel, Receiver, TrySendError};
|
||||||
@@ -11,72 +8,47 @@ use heapless::String;
|
|||||||
use crate::contracts::{DisplayCommand, ImuReading};
|
use crate::contracts::{DisplayCommand, ImuReading};
|
||||||
use pages_tui::input::Key;
|
use pages_tui::input::Key;
|
||||||
|
|
||||||
/// Queue size for display commands.
|
|
||||||
/// Moderate size to handle bursts without dropping.
|
|
||||||
const QUEUE_SIZE: usize = 8;
|
const QUEUE_SIZE: usize = 8;
|
||||||
|
|
||||||
/// Channel for sending commands to the display task.
|
|
||||||
pub(crate) static DISPLAY_CHANNEL: Channel<CriticalSectionRawMutex, DisplayCommand, QUEUE_SIZE> =
|
pub(crate) static DISPLAY_CHANNEL: Channel<CriticalSectionRawMutex, DisplayCommand, QUEUE_SIZE> =
|
||||||
Channel::new();
|
Channel::new();
|
||||||
|
|
||||||
/// Send a command to the display.
|
|
||||||
///
|
|
||||||
/// This is async and will wait if the queue is full.
|
|
||||||
/// For fire-and-forget, use `try_send`.
|
|
||||||
///
|
|
||||||
/// # Example
|
|
||||||
/// ```ignore
|
|
||||||
/// display::api::send(DisplayCommand::SetStatus("Hello".try_into().unwrap())).await;
|
|
||||||
/// ```
|
|
||||||
pub async fn send(cmd: DisplayCommand) {
|
pub async fn send(cmd: DisplayCommand) {
|
||||||
DISPLAY_CHANNEL.send(cmd).await;
|
DISPLAY_CHANNEL.send(cmd).await;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Try to send a command without waiting.
|
|
||||||
///
|
|
||||||
/// Returns `Err(cmd)` if the queue is full.
|
|
||||||
pub fn try_send(cmd: DisplayCommand) -> Result<(), DisplayCommand> {
|
pub fn try_send(cmd: DisplayCommand) -> Result<(), DisplayCommand> {
|
||||||
DISPLAY_CHANNEL.try_send(cmd).map_err(|e| match e {
|
DISPLAY_CHANNEL.try_send(cmd).map_err(|e| match e {
|
||||||
TrySendError::Full(command) => command,
|
TrySendError::Full(command) => command,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get a receiver for display commands (internal use).
|
|
||||||
///
|
|
||||||
/// Used by the display task to receive commands.
|
|
||||||
pub(crate) fn receiver() -> Receiver<'static, CriticalSectionRawMutex, DisplayCommand, QUEUE_SIZE> {
|
pub(crate) fn receiver() -> Receiver<'static, CriticalSectionRawMutex, DisplayCommand, QUEUE_SIZE> {
|
||||||
DISPLAY_CHANNEL.receiver()
|
DISPLAY_CHANNEL.receiver()
|
||||||
}
|
}
|
||||||
|
|
||||||
// ─────────────────────────────────────────────────────────────────────────────
|
|
||||||
// Convenience functions for common commands
|
|
||||||
// ─────────────────────────────────────────────────────────────────────────────
|
// ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
/// Send IMU data to the display.
|
|
||||||
pub async fn show_imu(reading: ImuReading) {
|
pub async fn show_imu(reading: ImuReading) {
|
||||||
send(DisplayCommand::SetImu(reading)).await;
|
send(DisplayCommand::SetImu(reading)).await;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Set the status line.
|
|
||||||
pub async fn set_status(text: &str) {
|
pub async fn set_status(text: &str) {
|
||||||
let mut s = String::<32>::new();
|
let mut s = String::<32>::new();
|
||||||
let _ = s.push_str(&text[..text.len().min(32)]);
|
let _ = s.push_str(&text[..text.len().min(32)]);
|
||||||
send(DisplayCommand::SetStatus(s)).await;
|
send(DisplayCommand::SetStatus(s)).await;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Show an error message.
|
|
||||||
pub async fn show_error(text: &str) {
|
pub async fn show_error(text: &str) {
|
||||||
let mut s = String::<64>::new();
|
let mut s = String::<64>::new();
|
||||||
let _ = s.push_str(&text[..text.len().min(64)]);
|
let _ = s.push_str(&text[..text.len().min(64)]);
|
||||||
send(DisplayCommand::ShowError(s)).await;
|
send(DisplayCommand::ShowError(s)).await;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Update MQTT status indicator.
|
|
||||||
pub async fn set_mqtt_status(connected: bool, msg_count: u32) {
|
pub async fn set_mqtt_status(connected: bool, msg_count: u32) {
|
||||||
send(DisplayCommand::SetMqttStatus { connected, msg_count }).await;
|
send(DisplayCommand::SetMqttStatus { connected, msg_count }).await;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Clear the display.
|
|
||||||
pub async fn clear() {
|
pub async fn clear() {
|
||||||
send(DisplayCommand::Clear).await;
|
send(DisplayCommand::Clear).await;
|
||||||
}
|
}
|
||||||
@@ -84,3 +56,10 @@ pub async fn clear() {
|
|||||||
pub async fn push_key(key: Key) {
|
pub async fn push_key(key: Key) {
|
||||||
send(DisplayCommand::PushKey(key)).await;
|
send(DisplayCommand::PushKey(key)).await;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Add a chat message (for Chat page)
|
||||||
|
pub async fn add_chat_message(msg: &str) {
|
||||||
|
let mut s = String::<24>::new();
|
||||||
|
let _ = s.push_str(&msg[..msg.len().min(24)]);
|
||||||
|
send(DisplayCommand::AddChatMessage(s)).await;
|
||||||
|
}
|
||||||
|
|||||||
@@ -25,17 +25,13 @@ pub async fn display_task(i2c: I2cDevice) {
|
|||||||
|
|
||||||
if let Err(e) = display.init() {
|
if let Err(e) = display.init() {
|
||||||
error!("Display init failed: {:?}", e);
|
error!("Display init failed: {:?}", e);
|
||||||
loop {
|
loop { Timer::after(Duration::from_secs(60)).await; }
|
||||||
Timer::after(Duration::from_secs(60)).await;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let config = EmbeddedBackendConfig {
|
let config = EmbeddedBackendConfig {
|
||||||
flush_callback: Box::new(
|
flush_callback: Box::new(|d: &mut Ssd1306<_, _, BufferedGraphicsMode<DisplaySize128x32>>| {
|
||||||
|d: &mut Ssd1306<_, _, BufferedGraphicsMode<DisplaySize128x32>>| {
|
|
||||||
let _ = d.flush();
|
let _ = d.flush();
|
||||||
},
|
}),
|
||||||
),
|
|
||||||
..Default::default()
|
..Default::default()
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -46,47 +42,43 @@ pub async fn display_task(i2c: I2cDevice) {
|
|||||||
let mut orchestrator = Orchestrator::<Screen>::new();
|
let mut orchestrator = Orchestrator::<Screen>::new();
|
||||||
let rx = receiver();
|
let rx = receiver();
|
||||||
|
|
||||||
// Register all pages
|
// Register pages
|
||||||
|
orchestrator.register_page("menu".into(), Screen::Menu);
|
||||||
orchestrator.register_page("imu".into(), Screen::Imu);
|
orchestrator.register_page("imu".into(), Screen::Imu);
|
||||||
orchestrator.register_page("mqtt".into(), Screen::Mqtt);
|
orchestrator.register_page("chat".into(), Screen::Chat);
|
||||||
orchestrator.register_page("status".into(), Screen::Status);
|
|
||||||
|
|
||||||
// Key bindings:
|
|
||||||
// - Tab (Next button): cycle focus between Prev/Next buttons
|
|
||||||
// - Enter (Select button): trigger the focused button's action
|
|
||||||
orchestrator.bind(Key::tab(), ComponentAction::Next);
|
orchestrator.bind(Key::tab(), ComponentAction::Next);
|
||||||
orchestrator.bind(Key::enter(), ComponentAction::Select);
|
orchestrator.bind(Key::enter(), ComponentAction::Select);
|
||||||
|
|
||||||
// Navigate to initial page
|
let _ = orchestrator.navigate_to("menu".into());
|
||||||
let _ = orchestrator.navigate_to("imu".into());
|
|
||||||
|
info!("Display ready");
|
||||||
|
|
||||||
loop {
|
loop {
|
||||||
while let Ok(cmd) = rx.try_receive() {
|
while let Ok(cmd) = rx.try_receive() {
|
||||||
match cmd {
|
match cmd {
|
||||||
DisplayCommand::PushKey(key) => {
|
DisplayCommand::PushKey(key) => {
|
||||||
if key == Key::tab() {
|
if key == Key::tab() {
|
||||||
// Cycle focus between buttons
|
|
||||||
orchestrator.focus_manager_mut().wrap_next();
|
orchestrator.focus_manager_mut().wrap_next();
|
||||||
info!("Focus changed to: {:?}", orchestrator.focus_manager().current());
|
|
||||||
} else if key == Key::enter() {
|
} else if key == Key::enter() {
|
||||||
// Process Select - this triggers Component::handle()
|
if let Ok(events) = orchestrator.process_frame(key) {
|
||||||
let events = orchestrator.process_frame(key);
|
|
||||||
|
|
||||||
// Handle navigation events
|
|
||||||
if let Ok(events) = events {
|
|
||||||
for event in events {
|
for event in events {
|
||||||
match event {
|
match event {
|
||||||
|
ScreenEvent::GoToImu => {
|
||||||
|
let _ = orchestrator.navigate_to("imu".into());
|
||||||
|
}
|
||||||
|
ScreenEvent::GoToChat => {
|
||||||
|
let _ = orchestrator.navigate_to("chat".into());
|
||||||
|
}
|
||||||
ScreenEvent::NavigatePrev => {
|
ScreenEvent::NavigatePrev => {
|
||||||
if let Some(current) = orchestrator.current_id() {
|
if let Some(cur) = orchestrator.current_id() {
|
||||||
let prev = prev_page_id(current.as_str());
|
let prev = prev_page_id(cur.as_str());
|
||||||
info!("Navigating to prev: {}", prev);
|
|
||||||
let _ = orchestrator.navigate_to(prev.into());
|
let _ = orchestrator.navigate_to(prev.into());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
ScreenEvent::NavigateNext => {
|
ScreenEvent::NavigateNext => {
|
||||||
if let Some(current) = orchestrator.current_id() {
|
if let Some(cur) = orchestrator.current_id() {
|
||||||
let next = next_page_id(current.as_str());
|
let next = next_page_id(cur.as_str());
|
||||||
info!("Navigating to next: {}", next);
|
|
||||||
let _ = orchestrator.navigate_to(next.into());
|
let _ = orchestrator.navigate_to(next.into());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -99,10 +91,8 @@ pub async fn display_task(i2c: I2cDevice) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Render current state
|
if let Some(screen) = orchestrator.current() {
|
||||||
if let Some(current_screen) = orchestrator.current() {
|
render_frame(&mut terminal, screen, orchestrator.focus_manager().current(), &state);
|
||||||
let focused = orchestrator.focus_manager().current();
|
|
||||||
render_frame(&mut terminal, current_screen, focused, &state);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Timer::after(Duration::from_millis(REFRESH_INTERVAL_MS)).await;
|
Timer::after(Duration::from_millis(REFRESH_INTERVAL_MS)).await;
|
||||||
|
|||||||
@@ -4,32 +4,36 @@ use alloc::format;
|
|||||||
use heapless::String as HString;
|
use heapless::String as HString;
|
||||||
use pages_tui::prelude::*;
|
use pages_tui::prelude::*;
|
||||||
use ratatui::{
|
use ratatui::{
|
||||||
layout::{Constraint, Direction, Layout},
|
layout::Rect,
|
||||||
style::Stylize,
|
style::Stylize,
|
||||||
widgets::Paragraph,
|
widgets::Paragraph,
|
||||||
Terminal,
|
Terminal,
|
||||||
};
|
};
|
||||||
use log::info;
|
use log::info;
|
||||||
|
|
||||||
/// Focusable buttons available on every page
|
/// Focus targets - different per screen
|
||||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
|
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
|
||||||
pub enum PageButton {
|
pub enum PageFocus {
|
||||||
Prev,
|
MenuImu,
|
||||||
Next,
|
MenuChat,
|
||||||
|
NavPrev,
|
||||||
|
NavNext,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl FocusId for PageButton {}
|
impl FocusId for PageFocus {}
|
||||||
|
|
||||||
/// All available buttons for iteration
|
const MENU_TARGETS: &[PageFocus] = &[PageFocus::MenuImu, PageFocus::MenuChat];
|
||||||
const PAGE_BUTTONS: &[PageButton] = &[PageButton::Prev, PageButton::Next];
|
const NAV_TARGETS: &[PageFocus] = &[PageFocus::NavPrev, PageFocus::NavNext];
|
||||||
|
|
||||||
/// Display state shared between pages
|
/// Display state
|
||||||
pub struct DisplayState {
|
pub struct DisplayState {
|
||||||
pub(crate) status: HString<32>,
|
pub(crate) status: HString<32>,
|
||||||
pub(crate) last_imu: Option<ImuReading>,
|
pub(crate) last_imu: Option<ImuReading>,
|
||||||
pub(crate) last_error: Option<HString<64>>,
|
pub(crate) last_error: Option<HString<64>>,
|
||||||
pub(crate) mqtt_connected: bool,
|
pub(crate) mqtt_connected: bool,
|
||||||
pub(crate) mqtt_msg_count: u32,
|
pub(crate) mqtt_msg_count: u32,
|
||||||
|
pub(crate) chat_msg1: HString<24>,
|
||||||
|
pub(crate) chat_msg2: HString<24>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for DisplayState {
|
impl Default for DisplayState {
|
||||||
@@ -40,6 +44,8 @@ impl Default for DisplayState {
|
|||||||
last_error: None,
|
last_error: None,
|
||||||
mqtt_connected: false,
|
mqtt_connected: false,
|
||||||
mqtt_msg_count: 0,
|
mqtt_msg_count: 0,
|
||||||
|
chat_msg1: HString::new(),
|
||||||
|
chat_msg2: HString::new(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -63,20 +69,30 @@ impl DisplayState {
|
|||||||
self.status = HString::new();
|
self.status = HString::new();
|
||||||
}
|
}
|
||||||
DisplayCommand::PushKey(_) => {}
|
DisplayCommand::PushKey(_) => {}
|
||||||
|
DisplayCommand::AddChatMessage(msg) => {
|
||||||
|
self.add_chat_message(msg.as_str());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn add_chat_message(&mut self, msg: &str) {
|
||||||
|
self.chat_msg2 = self.chat_msg1.clone();
|
||||||
|
self.chat_msg1.clear();
|
||||||
|
let _ = self.chat_msg1.push_str(&msg[..msg.len().min(24)]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||||
pub enum Screen {
|
pub enum Screen {
|
||||||
|
Menu,
|
||||||
Imu,
|
Imu,
|
||||||
Mqtt,
|
Chat,
|
||||||
Status,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Events emitted by screens for navigation
|
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
pub enum ScreenEvent {
|
pub enum ScreenEvent {
|
||||||
|
GoToImu,
|
||||||
|
GoToChat,
|
||||||
NavigatePrev,
|
NavigatePrev,
|
||||||
NavigateNext,
|
NavigateNext,
|
||||||
}
|
}
|
||||||
@@ -84,46 +100,45 @@ pub enum ScreenEvent {
|
|||||||
impl Component for Screen {
|
impl Component for Screen {
|
||||||
type Action = ComponentAction;
|
type Action = ComponentAction;
|
||||||
type Event = ScreenEvent;
|
type Event = ScreenEvent;
|
||||||
type Focus = PageButton;
|
type Focus = PageFocus;
|
||||||
|
|
||||||
fn handle(
|
fn handle(
|
||||||
&mut self,
|
&mut self,
|
||||||
focus: &Self::Focus,
|
focus: &Self::Focus,
|
||||||
action: Self::Action,
|
action: Self::Action,
|
||||||
) -> Result<Option<Self::Event>, ComponentError> {
|
) -> Result<Option<Self::Event>, ComponentError> {
|
||||||
match action {
|
if let ComponentAction::Select = action {
|
||||||
ComponentAction::Select => {
|
info!("Select {:?} focus:{:?}", self, focus);
|
||||||
info!("Select on {:?} screen, focused: {:?}", self, focus);
|
return Ok(Some(match focus {
|
||||||
// When Select is pressed, trigger navigation based on focused button
|
PageFocus::MenuImu => ScreenEvent::GoToImu,
|
||||||
match focus {
|
PageFocus::MenuChat => ScreenEvent::GoToChat,
|
||||||
PageButton::Prev => return Ok(Some(ScreenEvent::NavigatePrev)),
|
PageFocus::NavPrev => ScreenEvent::NavigatePrev,
|
||||||
PageButton::Next => return Ok(Some(ScreenEvent::NavigateNext)),
|
PageFocus::NavNext => ScreenEvent::NavigateNext,
|
||||||
}
|
}));
|
||||||
}
|
|
||||||
_ => {}
|
|
||||||
}
|
}
|
||||||
Ok(None)
|
Ok(None)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn targets(&self) -> &[Self::Focus] {
|
fn targets(&self) -> &[Self::Focus] {
|
||||||
// Every page has Prev and Next buttons
|
match self {
|
||||||
PAGE_BUTTONS
|
Screen::Menu => MENU_TARGETS,
|
||||||
|
Screen::Imu | Screen::Chat => NAV_TARGETS,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn on_enter(&mut self) -> Result<(), ComponentError> {
|
fn on_enter(&mut self) -> Result<(), ComponentError> {
|
||||||
info!("Entered screen: {:?}", self);
|
info!("Enter: {:?}", self);
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn on_exit(&mut self) -> Result<(), ComponentError> {
|
fn on_exit(&mut self) -> Result<(), ComponentError> {
|
||||||
info!("Exited screen: {:?}", self);
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============ PAGE ORDER ============
|
// ============ PAGE ORDER ============
|
||||||
|
|
||||||
const PAGE_ORDER: &[&str] = &["imu", "mqtt", "status"];
|
const PAGE_ORDER: &[&str] = &["menu", "imu", "chat"];
|
||||||
|
|
||||||
pub fn next_page_id(current: &str) -> &'static str {
|
pub fn next_page_id(current: &str) -> &'static str {
|
||||||
let idx = PAGE_ORDER.iter().position(|&p| p == current).unwrap_or(0);
|
let idx = PAGE_ORDER.iter().position(|&p| p == current).unwrap_or(0);
|
||||||
@@ -132,102 +147,120 @@ pub fn next_page_id(current: &str) -> &'static str {
|
|||||||
|
|
||||||
pub fn prev_page_id(current: &str) -> &'static str {
|
pub fn prev_page_id(current: &str) -> &'static str {
|
||||||
let idx = PAGE_ORDER.iter().position(|&p| p == current).unwrap_or(0);
|
let idx = PAGE_ORDER.iter().position(|&p| p == current).unwrap_or(0);
|
||||||
if idx == 0 {
|
if idx == 0 { PAGE_ORDER[PAGE_ORDER.len() - 1] } else { PAGE_ORDER[idx - 1] }
|
||||||
PAGE_ORDER[PAGE_ORDER.len() - 1]
|
|
||||||
} else {
|
|
||||||
PAGE_ORDER[idx - 1]
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============ RENDERING ============
|
// ============ RENDERING - NO LAYOUT, DIRECT RECTS ============
|
||||||
|
// 128x32 display = 21 chars x 4 rows (6x8 font)
|
||||||
|
|
||||||
fn render_imu_content(state: &DisplayState) -> alloc::string::String {
|
/// Get row rect (0-3) for 128x32 display
|
||||||
if let Some(ref imu) = state.last_imu {
|
#[inline]
|
||||||
format!(
|
fn row(area: Rect, n: u16) -> Rect {
|
||||||
"G:{:.0} {:.0} {:.0}\nT:{:.1}C",
|
Rect::new(area.x, area.y + n, area.width, 1)
|
||||||
imu.gyro_dps[0], imu.gyro_dps[1], imu.gyro_dps[2], imu.temp_c
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
format!("Waiting IMU...")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn render_mqtt_content(state: &DisplayState) -> alloc::string::String {
|
|
||||||
format!(
|
|
||||||
"MQTT:{}\nMsg:{}",
|
|
||||||
if state.mqtt_connected { "ON" } else { "OFF" },
|
|
||||||
state.mqtt_msg_count
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn render_status_content(state: &DisplayState) -> alloc::string::String {
|
|
||||||
format!("Status:\n{}", state.status.as_str())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn render_button(label: &str, focused: bool) -> Paragraph<'static> {
|
|
||||||
let text = if focused {
|
|
||||||
format!("[>{}]", label)
|
|
||||||
} else {
|
|
||||||
format!(" {} ", label)
|
|
||||||
};
|
|
||||||
|
|
||||||
if focused {
|
|
||||||
Paragraph::new(text).white().on_blue()
|
|
||||||
} else {
|
|
||||||
Paragraph::new(text).gray()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn render_frame<B: ratatui::backend::Backend>(
|
pub fn render_frame<B: ratatui::backend::Backend>(
|
||||||
terminal: &mut Terminal<B>,
|
terminal: &mut Terminal<B>,
|
||||||
current_screen: &Screen,
|
current_screen: &Screen,
|
||||||
focused_button: Option<&PageButton>,
|
focused: Option<&PageFocus>,
|
||||||
state: &DisplayState,
|
state: &DisplayState,
|
||||||
) {
|
) {
|
||||||
let _ = terminal.draw(|f| {
|
let _ = terminal.draw(|f| {
|
||||||
// Handle errors with priority
|
let area = f.area();
|
||||||
|
|
||||||
if let Some(ref err) = state.last_error {
|
if let Some(ref err) = state.last_error {
|
||||||
let content = format!("ERR: {}", err.as_str());
|
f.render_widget(Paragraph::new(format!("ERR:{}", err.as_str())).red().bold(), area);
|
||||||
f.render_widget(Paragraph::new(content).red().bold(), f.area());
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Layout: content area + button bar
|
match current_screen {
|
||||||
let chunks = Layout::default()
|
Screen::Menu => render_menu(f, area, focused, state),
|
||||||
.direction(Direction::Vertical)
|
Screen::Imu => render_imu(f, area, focused, state),
|
||||||
.constraints([
|
Screen::Chat => render_chat(f, area, focused, state),
|
||||||
Constraint::Min(1), // Content
|
}
|
||||||
Constraint::Length(1), // Button bar
|
|
||||||
])
|
|
||||||
.split(f.area());
|
|
||||||
|
|
||||||
// Render page content
|
|
||||||
let content = match current_screen {
|
|
||||||
Screen::Imu => render_imu_content(state),
|
|
||||||
Screen::Mqtt => render_mqtt_content(state),
|
|
||||||
Screen::Status => render_status_content(state),
|
|
||||||
};
|
|
||||||
|
|
||||||
let style = match current_screen {
|
|
||||||
Screen::Imu => Paragraph::new(content).cyan(),
|
|
||||||
Screen::Mqtt => Paragraph::new(content).green(),
|
|
||||||
Screen::Status => Paragraph::new(content).yellow(),
|
|
||||||
};
|
|
||||||
f.render_widget(style, chunks[0]);
|
|
||||||
|
|
||||||
// Render button bar
|
|
||||||
let btn_chunks = Layout::default()
|
|
||||||
.direction(Direction::Horizontal)
|
|
||||||
.constraints([
|
|
||||||
Constraint::Percentage(50),
|
|
||||||
Constraint::Percentage(50),
|
|
||||||
])
|
|
||||||
.split(chunks[1]);
|
|
||||||
|
|
||||||
let prev_focused = focused_button == Some(&PageButton::Prev);
|
|
||||||
let next_focused = focused_button == Some(&PageButton::Next);
|
|
||||||
|
|
||||||
f.render_widget(render_button("<", prev_focused), btn_chunks[0]);
|
|
||||||
f.render_widget(render_button(">", next_focused), btn_chunks[1]);
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn render_menu(f: &mut ratatui::Frame, area: Rect, focused: Option<&PageFocus>, state: &DisplayState) {
|
||||||
|
// Row 0 Title
|
||||||
|
let conn = if state.mqtt_connected { "*" } else { "x" };
|
||||||
|
f.render_widget(
|
||||||
|
Paragraph::new(format!("-- ESP32 Hub {} --", conn)).white().bold(),
|
||||||
|
row(area, 0),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Row 1 Sensors
|
||||||
|
let imu_sel = focused == Some(&PageFocus::MenuImu);
|
||||||
|
f.render_widget(
|
||||||
|
Paragraph::new(if imu_sel { " > Sensors" } else { " Sensors" })
|
||||||
|
.style(if imu_sel { ratatui::style::Style::default().white().bold() } else { ratatui::style::Style::default().white() }),
|
||||||
|
row(area, 1),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Row 2 Messages
|
||||||
|
let chat_sel = focused == Some(&PageFocus::MenuChat);
|
||||||
|
f.render_widget(
|
||||||
|
Paragraph::new(if chat_sel { " > Messages" } else { " Messages" })
|
||||||
|
.style(if chat_sel { ratatui::style::Style::default().white().bold() } else { ratatui::style::Style::default().white() }),
|
||||||
|
row(area, 2),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render_imu(f: &mut ratatui::Frame, area: Rect, focused: Option<&PageFocus>, state: &DisplayState) {
|
||||||
|
if let Some(ref imu) = state.last_imu {
|
||||||
|
// Row 0 Gyro
|
||||||
|
// f.render_widget(
|
||||||
|
// Paragraph::new(format!("G:{:+5.0}{:+5.0}{:+5.0}", imu.gyro_dps[0], imu.gyro_dps[1], imu.gyro_dps[2])).cyan(),
|
||||||
|
// row(area, 0),
|
||||||
|
// );
|
||||||
|
// Row 1 Accel
|
||||||
|
f.render_widget(
|
||||||
|
Paragraph::new(format!("A:{:+.1} {:+.1} {:+.1}", imu.accel_g[0], imu.accel_g[1], imu.accel_g[2])).green(),
|
||||||
|
row(area, 1),
|
||||||
|
);
|
||||||
|
// Row 2 Temp
|
||||||
|
f.render_widget(
|
||||||
|
Paragraph::new(format!("T: {:.1}C", imu.temp_c)).yellow(),
|
||||||
|
row(area, 2),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
f.render_widget(Paragraph::new("Waiting for MPU...").white(), row(area, 1));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Row 3 Nav bar
|
||||||
|
render_nav_bar(f, row(area, 2), focused);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render_chat(f: &mut ratatui::Frame, area: Rect, focused: Option<&PageFocus>, state: &DisplayState) {
|
||||||
|
// Row 0 Message 1
|
||||||
|
if state.chat_msg1.is_empty() {
|
||||||
|
f.render_widget(Paragraph::new("(no messages)").white(), row(area, 0));
|
||||||
|
} else {
|
||||||
|
f.render_widget(Paragraph::new(format!(">{}", state.chat_msg1.as_str())).green(), row(area, 0));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Row 1 Message 2
|
||||||
|
if !state.chat_msg2.is_empty() {
|
||||||
|
f.render_widget(Paragraph::new(format!(">{}", state.chat_msg2.as_str())).white(), row(area, 1));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Row 3 Nav bar
|
||||||
|
render_nav_bar(f, row(area, 3), focused);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Nav bar: --[<]-------[>]--
|
||||||
|
fn render_nav_bar(f: &mut ratatui::Frame, area: Rect, focused: Option<&PageFocus>) {
|
||||||
|
let prev_sel = focused == Some(&PageFocus::NavPrev);
|
||||||
|
let next_sel = focused == Some(&PageFocus::NavNext);
|
||||||
|
|
||||||
|
let prev = if prev_sel { "[<]" } else { " < " };
|
||||||
|
let next = if next_sel { "[>]" } else { " > " };
|
||||||
|
|
||||||
|
// Build the line: --[<]-------[>]--
|
||||||
|
let line = format!("--{}-------{}--", prev, next);
|
||||||
|
|
||||||
|
f.render_widget(
|
||||||
|
Paragraph::new(line).style(ratatui::style::Style::default().white()),
|
||||||
|
area,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user