pages-tui working
This commit is contained in:
14
mqtt_display/Cargo.lock
generated
14
mqtt_display/Cargo.lock
generated
@@ -1073,9 +1073,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "heapless"
|
name = "heapless"
|
||||||
version = "0.9.1"
|
version = "0.9.2"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "b1edcd5a338e64688fbdcb7531a846cfd3476a54784dcb918a0844682bc7ada5"
|
checksum = "2af2455f757db2b292a9b1768c4b70186d443bcb3b316252d6b540aec1cd89ed"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"hash32",
|
"hash32",
|
||||||
"stable_deref_trait",
|
"stable_deref_trait",
|
||||||
@@ -1305,6 +1305,13 @@ dependencies = [
|
|||||||
"autocfg",
|
"autocfg",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pages-tui"
|
||||||
|
version = "0.1.0"
|
||||||
|
dependencies = [
|
||||||
|
"heapless 0.9.2",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "paste"
|
name = "paste"
|
||||||
version = "1.0.15"
|
version = "1.0.15"
|
||||||
@@ -1452,9 +1459,10 @@ dependencies = [
|
|||||||
"esp-hal-embassy",
|
"esp-hal-embassy",
|
||||||
"esp-println",
|
"esp-println",
|
||||||
"esp-wifi",
|
"esp-wifi",
|
||||||
"heapless 0.9.1",
|
"heapless 0.9.2",
|
||||||
"log",
|
"log",
|
||||||
"mousefood",
|
"mousefood",
|
||||||
|
"pages-tui",
|
||||||
"ratatui",
|
"ratatui",
|
||||||
"rust-mqtt",
|
"rust-mqtt",
|
||||||
"smoltcp",
|
"smoltcp",
|
||||||
|
|||||||
@@ -9,6 +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 }
|
||||||
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",
|
||||||
|
|||||||
@@ -9,7 +9,7 @@
|
|||||||
// TODO WARNING core 1 should be logic, core 0 wifi, its flipped now
|
// TODO WARNING core 1 should be logic, core 0 wifi, its flipped now
|
||||||
|
|
||||||
use embassy_executor::Spawner;
|
use embassy_executor::Spawner;
|
||||||
use embassy_futures::select::{select3, Either3};
|
use embassy_futures::select::{select, Either, select3, Either3};
|
||||||
use embassy_net::{Runner, StackResources};
|
use embassy_net::{Runner, StackResources};
|
||||||
use embassy_sync::signal::Signal;
|
use embassy_sync::signal::Signal;
|
||||||
use embassy_sync::blocking_mutex::raw::CriticalSectionRawMutex;
|
use embassy_sync::blocking_mutex::raw::CriticalSectionRawMutex;
|
||||||
@@ -20,7 +20,9 @@ use esp_alloc as _;
|
|||||||
use esp_backtrace as _;
|
use esp_backtrace as _;
|
||||||
|
|
||||||
use esp_hal::{
|
use esp_hal::{
|
||||||
|
gpio::InputConfig,
|
||||||
clock::CpuClock,
|
clock::CpuClock,
|
||||||
|
gpio::{Input, Pull},
|
||||||
i2c::master::{Config as I2cConfig, I2c},
|
i2c::master::{Config as I2cConfig, I2c},
|
||||||
rng::Rng,
|
rng::Rng,
|
||||||
system::{CpuControl, Stack},
|
system::{CpuControl, Stack},
|
||||||
@@ -31,6 +33,7 @@ use esp_wifi::{
|
|||||||
EspWifiController,
|
EspWifiController,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
use pages_tui::input::Key;
|
||||||
use log::info;
|
use log::info;
|
||||||
use rust_mqtt::packet::v5::publish_packet::QualityOfService;
|
use rust_mqtt::packet::v5::publish_packet::QualityOfService;
|
||||||
use static_cell::StaticCell;
|
use static_cell::StaticCell;
|
||||||
@@ -122,6 +125,11 @@ async fn main(spawner: Spawner) -> ! {
|
|||||||
NETWORK_READY.wait().await;
|
NETWORK_READY.wait().await;
|
||||||
info!("Network ready, starting core 0 tasks");
|
info!("Network ready, starting core 0 tasks");
|
||||||
|
|
||||||
|
let config = InputConfig::default().with_pull(Pull::Down);
|
||||||
|
let button_select = Input::new(peripherals.GPIO32, config);
|
||||||
|
let button_next = Input::new(peripherals.GPIO35, config);
|
||||||
|
spawner.spawn(button_detection_task(button_select, button_next)).unwrap();
|
||||||
|
|
||||||
// Core 0: display and MPU tasks
|
// Core 0: display and MPU tasks
|
||||||
spawner.spawn(display::task::display_task(display_i2c)).expect("spawn display_task");
|
spawner.spawn(display::task::display_task(display_i2c)).expect("spawn display_task");
|
||||||
spawner.spawn(mpu::task::mpu_task(mpu_i2c)).expect("spawn mpu_task");
|
spawner.spawn(mpu::task::mpu_task(mpu_i2c)).expect("spawn mpu_task");
|
||||||
@@ -248,3 +256,25 @@ async fn connection_task(mut controller: WifiController<'static>) {
|
|||||||
async fn net_task(mut runner: Runner<'static, WifiDevice<'static>>) {
|
async fn net_task(mut runner: Runner<'static, WifiDevice<'static>>) {
|
||||||
runner.run().await
|
runner.run().await
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[embassy_executor::task]
|
||||||
|
async fn button_detection_task(mut select_btn: Input<'static>, mut next_btn: Input<'static>) {
|
||||||
|
use embassy_futures::select::Either;
|
||||||
|
loop {
|
||||||
|
match select(
|
||||||
|
select_btn.wait_for_rising_edge(),
|
||||||
|
next_btn.wait_for_rising_edge(),
|
||||||
|
).await {
|
||||||
|
Either::First(_) => {
|
||||||
|
info!("Detection: GPIO 32 (Select) triggered!");
|
||||||
|
display::api::push_key(Key::enter()).await;
|
||||||
|
}
|
||||||
|
Either::Second(_) => {
|
||||||
|
info!("Detection: GPIO 35 (Next) triggered!");
|
||||||
|
display::api::push_key(Key::tab()).await
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Debounce: prevent mechanical bouncing from double-triggering
|
||||||
|
embassy_time::Timer::after(embassy_time::Duration::from_millis(200)).await;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -5,6 +5,7 @@
|
|||||||
//! Features depend on these types, not on each other.
|
//! Features depend on these types, not on each other.
|
||||||
|
|
||||||
use heapless::String as HString;
|
use heapless::String as HString;
|
||||||
|
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)]
|
||||||
@@ -32,4 +33,6 @@ pub enum DisplayCommand {
|
|||||||
SetMqttStatus { connected: bool, msg_count: u32 },
|
SetMqttStatus { connected: bool, msg_count: u32 },
|
||||||
/// Clear the display to default state
|
/// Clear the display to default state
|
||||||
Clear,
|
Clear,
|
||||||
|
|
||||||
|
PushKey(Key),
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ use embassy_sync::channel::{Channel, Receiver, TrySendError};
|
|||||||
use heapless::String;
|
use heapless::String;
|
||||||
|
|
||||||
use crate::contracts::{DisplayCommand, ImuReading};
|
use crate::contracts::{DisplayCommand, ImuReading};
|
||||||
|
use pages_tui::input::Key;
|
||||||
|
|
||||||
/// Queue size for display commands.
|
/// Queue size for display commands.
|
||||||
/// Moderate size to handle bursts without dropping.
|
/// Moderate size to handle bursts without dropping.
|
||||||
@@ -79,3 +80,7 @@ pub async fn set_mqtt_status(connected: bool, msg_count: u32) {
|
|||||||
pub async fn clear() {
|
pub async fn clear() {
|
||||||
send(DisplayCommand::Clear).await;
|
send(DisplayCommand::Clear).await;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn push_key(key: Key) {
|
||||||
|
send(DisplayCommand::PushKey(key)).await;
|
||||||
|
}
|
||||||
|
|||||||
@@ -3,3 +3,4 @@
|
|||||||
|
|
||||||
pub mod api;
|
pub mod api;
|
||||||
pub mod task;
|
pub mod task;
|
||||||
|
pub mod tui;
|
||||||
|
|||||||
@@ -1,84 +1,24 @@
|
|||||||
// src/display/task.rs
|
// src/display/task.rs
|
||||||
//! SSD1306 display rendering task optimized for 0.91" 128x32 OLED.
|
|
||||||
|
|
||||||
use embassy_time::{Duration, Timer};
|
use embassy_time::{Duration, Timer};
|
||||||
use log::{error, info};
|
use log::{error, info};
|
||||||
|
|
||||||
use alloc::boxed::Box;
|
use alloc::boxed::Box;
|
||||||
use alloc::format;
|
|
||||||
use heapless::String;
|
|
||||||
use mousefood::{EmbeddedBackend, EmbeddedBackendConfig};
|
use mousefood::{EmbeddedBackend, EmbeddedBackendConfig};
|
||||||
use ratatui::{
|
use ratatui::Terminal;
|
||||||
layout::{Constraint, Direction, Layout},
|
use ssd1306::{mode::BufferedGraphicsMode, prelude::*, I2CDisplayInterface, Ssd1306};
|
||||||
style::{Style, Stylize},
|
|
||||||
widgets::Paragraph,
|
|
||||||
Terminal,
|
|
||||||
};
|
|
||||||
use ssd1306::{
|
|
||||||
mode::BufferedGraphicsMode, prelude::*, I2CDisplayInterface, Ssd1306,
|
|
||||||
};
|
|
||||||
|
|
||||||
use crate::bus::I2cDevice;
|
use crate::bus::I2cDevice;
|
||||||
use crate::contracts::{DisplayCommand, ImuReading};
|
|
||||||
use crate::display::api::receiver;
|
use crate::display::api::receiver;
|
||||||
|
use crate::display::tui::{render_frame, next_page_id, prev_page_id, DisplayState, Screen, ScreenEvent};
|
||||||
|
use crate::contracts::DisplayCommand;
|
||||||
|
use pages_tui::prelude::*;
|
||||||
|
|
||||||
/// Display refresh interval in milliseconds.
|
|
||||||
const REFRESH_INTERVAL_MS: u64 = 100;
|
const REFRESH_INTERVAL_MS: u64 = 100;
|
||||||
|
|
||||||
/// Internal state for what to render.
|
|
||||||
struct DisplayState {
|
|
||||||
status: String<32>,
|
|
||||||
last_imu: Option<ImuReading>,
|
|
||||||
last_error: Option<String<64>>,
|
|
||||||
mqtt_connected: bool,
|
|
||||||
mqtt_msg_count: u32,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Default for DisplayState {
|
|
||||||
fn default() -> Self {
|
|
||||||
Self {
|
|
||||||
status: String::new(),
|
|
||||||
last_imu: None,
|
|
||||||
last_error: None,
|
|
||||||
mqtt_connected: false,
|
|
||||||
mqtt_msg_count: 0,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl DisplayState {
|
|
||||||
fn apply_command(&mut self, cmd: DisplayCommand) {
|
|
||||||
match cmd {
|
|
||||||
DisplayCommand::SetImu(reading) => {
|
|
||||||
self.last_imu = Some(reading);
|
|
||||||
self.last_error = None;
|
|
||||||
}
|
|
||||||
DisplayCommand::SetStatus(s) => {
|
|
||||||
self.status = s;
|
|
||||||
}
|
|
||||||
DisplayCommand::ShowError(e) => {
|
|
||||||
self.last_error = Some(e);
|
|
||||||
}
|
|
||||||
DisplayCommand::SetMqttStatus { connected, msg_count } => {
|
|
||||||
self.mqtt_connected = connected;
|
|
||||||
self.mqtt_msg_count = msg_count;
|
|
||||||
}
|
|
||||||
DisplayCommand::Clear => {
|
|
||||||
self.last_imu = None;
|
|
||||||
self.last_error = None;
|
|
||||||
self.status = String::new();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// The display rendering task.
|
|
||||||
/// Designed for 0.91" 128x32 slim OLED screen.
|
|
||||||
#[embassy_executor::task]
|
#[embassy_executor::task]
|
||||||
pub async fn display_task(i2c: I2cDevice) {
|
pub async fn display_task(i2c: I2cDevice) {
|
||||||
info!("Display task starting...");
|
info!("Display task starting...");
|
||||||
|
|
||||||
// Initialize SSD1306 display for 128x32 variant
|
|
||||||
let interface = I2CDisplayInterface::new(i2c);
|
let interface = I2CDisplayInterface::new(i2c);
|
||||||
let mut display = Ssd1306::new(interface, DisplaySize128x32, DisplayRotation::Rotate0)
|
let mut display = Ssd1306::new(interface, DisplaySize128x32, DisplayRotation::Rotate0)
|
||||||
.into_buffered_graphics_mode();
|
.into_buffered_graphics_mode();
|
||||||
@@ -90,9 +30,6 @@ pub async fn display_task(i2c: I2cDevice) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
info!("SSD1306 display initialized (128x32)");
|
|
||||||
|
|
||||||
// Configure mousefood backend for ratatui
|
|
||||||
let config = EmbeddedBackendConfig {
|
let config = EmbeddedBackendConfig {
|
||||||
flush_callback: Box::new(
|
flush_callback: Box::new(
|
||||||
|d: &mut Ssd1306<_, _, BufferedGraphicsMode<DisplaySize128x32>>| {
|
|d: &mut Ssd1306<_, _, BufferedGraphicsMode<DisplaySize128x32>>| {
|
||||||
@@ -106,60 +43,68 @@ pub async fn display_task(i2c: I2cDevice) {
|
|||||||
let mut terminal = Terminal::new(backend).expect("terminal init failed");
|
let mut terminal = Terminal::new(backend).expect("terminal init failed");
|
||||||
|
|
||||||
let mut state = DisplayState::default();
|
let mut state = DisplayState::default();
|
||||||
|
let mut orchestrator = Orchestrator::<Screen>::new();
|
||||||
let rx = receiver();
|
let rx = receiver();
|
||||||
|
|
||||||
info!("Display task entering render loop");
|
// Register all pages
|
||||||
|
orchestrator.register_page("imu".into(), Screen::Imu);
|
||||||
|
orchestrator.register_page("mqtt".into(), Screen::Mqtt);
|
||||||
|
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::enter(), ComponentAction::Select);
|
||||||
|
|
||||||
|
// Navigate to initial page
|
||||||
|
let _ = orchestrator.navigate_to("imu".into());
|
||||||
|
|
||||||
loop {
|
loop {
|
||||||
// Process all pending commands (non-blocking)
|
|
||||||
while let Ok(cmd) = rx.try_receive() {
|
while let Ok(cmd) = rx.try_receive() {
|
||||||
state.apply_command(cmd);
|
match cmd {
|
||||||
|
DisplayCommand::PushKey(key) => {
|
||||||
|
if key == Key::tab() {
|
||||||
|
// Cycle focus between buttons
|
||||||
|
orchestrator.focus_manager_mut().wrap_next();
|
||||||
|
info!("Focus changed to: {:?}", orchestrator.focus_manager().current());
|
||||||
|
} else if key == Key::enter() {
|
||||||
|
// Process Select - this triggers Component::handle()
|
||||||
|
let events = orchestrator.process_frame(key);
|
||||||
|
|
||||||
|
// Handle navigation events
|
||||||
|
if let Ok(events) = events {
|
||||||
|
for event in events {
|
||||||
|
match event {
|
||||||
|
ScreenEvent::NavigatePrev => {
|
||||||
|
if let Some(current) = orchestrator.current_id() {
|
||||||
|
let prev = prev_page_id(current.as_str());
|
||||||
|
info!("Navigating to prev: {}", prev);
|
||||||
|
let _ = orchestrator.navigate_to(prev.into());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ScreenEvent::NavigateNext => {
|
||||||
|
if let Some(current) = orchestrator.current_id() {
|
||||||
|
let next = next_page_id(current.as_str());
|
||||||
|
info!("Navigating to next: {}", next);
|
||||||
|
let _ = orchestrator.navigate_to(next.into());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => state.apply_command(cmd),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Render current state
|
// Render current state
|
||||||
render_frame(&mut terminal, &state);
|
if let Some(current_screen) = orchestrator.current() {
|
||||||
|
let focused = orchestrator.focus_manager().current();
|
||||||
|
render_frame(&mut terminal, current_screen, focused, &state);
|
||||||
|
}
|
||||||
|
|
||||||
// Wait before next refresh
|
|
||||||
Timer::after(Duration::from_millis(REFRESH_INTERVAL_MS)).await;
|
Timer::after(Duration::from_millis(REFRESH_INTERVAL_MS)).await;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Render a single frame compactly (for 128x32 OLED)
|
|
||||||
fn render_frame<B: ratatui::backend::Backend>(terminal: &mut Terminal<B>, state: &DisplayState) {
|
|
||||||
let _ = terminal.draw(|f| {
|
|
||||||
let chunks = Layout::default()
|
|
||||||
.direction(Direction::Vertical)
|
|
||||||
.constraints([
|
|
||||||
Constraint::Length(1),
|
|
||||||
Constraint::Min(0),
|
|
||||||
])
|
|
||||||
.split(f.area());
|
|
||||||
|
|
||||||
// Header: condensed status + MQTT indicator
|
|
||||||
let mqtt_indicator = if state.mqtt_connected { "M" } else { "m" };
|
|
||||||
let header_title = format!(
|
|
||||||
"[{}] {} #{}",
|
|
||||||
mqtt_indicator,
|
|
||||||
state.status.as_str(),
|
|
||||||
state.mqtt_msg_count
|
|
||||||
);
|
|
||||||
|
|
||||||
f.render_widget(Paragraph::new(header_title).style(Style::default().reversed()), chunks[0]);
|
|
||||||
|
|
||||||
// Body: minimal content (no borders, short text)
|
|
||||||
let body_content = if let Some(ref err) = state.last_error {
|
|
||||||
format!("ERR: {}", err.as_str())
|
|
||||||
} else if let Some(ref imu) = state.last_imu {
|
|
||||||
format!(
|
|
||||||
"A:{:.1} {:.1} {:.1}\nG:{:.0} {:.0} {:.0}\nT:{:.1}C",
|
|
||||||
imu.accel_g[0], imu.accel_g[1], imu.accel_g[2],
|
|
||||||
imu.gyro_dps[0], imu.gyro_dps[1], imu.gyro_dps[2],
|
|
||||||
imu.temp_c
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
format!("Waiting for data...")
|
|
||||||
};
|
|
||||||
|
|
||||||
f.render_widget(Paragraph::new(body_content), chunks[1]);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|||||||
233
mqtt_display/src/display/tui.rs
Normal file
233
mqtt_display/src/display/tui.rs
Normal file
@@ -0,0 +1,233 @@
|
|||||||
|
// src/display/tui.rs
|
||||||
|
use crate::contracts::{DisplayCommand, ImuReading};
|
||||||
|
use alloc::format;
|
||||||
|
use heapless::String as HString;
|
||||||
|
use pages_tui::prelude::*;
|
||||||
|
use ratatui::{
|
||||||
|
layout::{Constraint, Direction, Layout},
|
||||||
|
style::Stylize,
|
||||||
|
widgets::Paragraph,
|
||||||
|
Terminal,
|
||||||
|
};
|
||||||
|
use log::info;
|
||||||
|
|
||||||
|
/// Focusable buttons available on every page
|
||||||
|
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
|
||||||
|
pub enum PageButton {
|
||||||
|
Prev,
|
||||||
|
Next,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FocusId for PageButton {}
|
||||||
|
|
||||||
|
/// All available buttons for iteration
|
||||||
|
const PAGE_BUTTONS: &[PageButton] = &[PageButton::Prev, PageButton::Next];
|
||||||
|
|
||||||
|
/// Display state shared between pages
|
||||||
|
pub struct DisplayState {
|
||||||
|
pub(crate) status: HString<32>,
|
||||||
|
pub(crate) last_imu: Option<ImuReading>,
|
||||||
|
pub(crate) last_error: Option<HString<64>>,
|
||||||
|
pub(crate) mqtt_connected: bool,
|
||||||
|
pub(crate) mqtt_msg_count: u32,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for DisplayState {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
status: HString::new(),
|
||||||
|
last_imu: None,
|
||||||
|
last_error: None,
|
||||||
|
mqtt_connected: false,
|
||||||
|
mqtt_msg_count: 0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl DisplayState {
|
||||||
|
pub fn apply_command(&mut self, cmd: DisplayCommand) {
|
||||||
|
match cmd {
|
||||||
|
DisplayCommand::SetImu(reading) => {
|
||||||
|
self.last_imu = Some(reading);
|
||||||
|
self.last_error = None;
|
||||||
|
}
|
||||||
|
DisplayCommand::SetStatus(s) => self.status = s,
|
||||||
|
DisplayCommand::ShowError(e) => self.last_error = Some(e),
|
||||||
|
DisplayCommand::SetMqttStatus { connected, msg_count } => {
|
||||||
|
self.mqtt_connected = connected;
|
||||||
|
self.mqtt_msg_count = msg_count;
|
||||||
|
}
|
||||||
|
DisplayCommand::Clear => {
|
||||||
|
self.last_imu = None;
|
||||||
|
self.last_error = None;
|
||||||
|
self.status = HString::new();
|
||||||
|
}
|
||||||
|
DisplayCommand::PushKey(_) => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||||
|
pub enum Screen {
|
||||||
|
Imu,
|
||||||
|
Mqtt,
|
||||||
|
Status,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Events emitted by screens for navigation
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub enum ScreenEvent {
|
||||||
|
NavigatePrev,
|
||||||
|
NavigateNext,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Component for Screen {
|
||||||
|
type Action = ComponentAction;
|
||||||
|
type Event = ScreenEvent;
|
||||||
|
type Focus = PageButton;
|
||||||
|
|
||||||
|
fn handle(
|
||||||
|
&mut self,
|
||||||
|
focus: &Self::Focus,
|
||||||
|
action: Self::Action,
|
||||||
|
) -> Result<Option<Self::Event>, ComponentError> {
|
||||||
|
match action {
|
||||||
|
ComponentAction::Select => {
|
||||||
|
info!("Select on {:?} screen, focused: {:?}", self, focus);
|
||||||
|
// When Select is pressed, trigger navigation based on focused button
|
||||||
|
match focus {
|
||||||
|
PageButton::Prev => return Ok(Some(ScreenEvent::NavigatePrev)),
|
||||||
|
PageButton::Next => return Ok(Some(ScreenEvent::NavigateNext)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
Ok(None)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn targets(&self) -> &[Self::Focus] {
|
||||||
|
// Every page has Prev and Next buttons
|
||||||
|
PAGE_BUTTONS
|
||||||
|
}
|
||||||
|
|
||||||
|
fn on_enter(&mut self) -> Result<(), ComponentError> {
|
||||||
|
info!("Entered screen: {:?}", self);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn on_exit(&mut self) -> Result<(), ComponentError> {
|
||||||
|
info!("Exited screen: {:?}", self);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============ PAGE ORDER ============
|
||||||
|
|
||||||
|
const PAGE_ORDER: &[&str] = &["imu", "mqtt", "status"];
|
||||||
|
|
||||||
|
pub fn next_page_id(current: &str) -> &'static str {
|
||||||
|
let idx = PAGE_ORDER.iter().position(|&p| p == current).unwrap_or(0);
|
||||||
|
PAGE_ORDER[(idx + 1) % PAGE_ORDER.len()]
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn prev_page_id(current: &str) -> &'static str {
|
||||||
|
let idx = PAGE_ORDER.iter().position(|&p| p == current).unwrap_or(0);
|
||||||
|
if idx == 0 {
|
||||||
|
PAGE_ORDER[PAGE_ORDER.len() - 1]
|
||||||
|
} else {
|
||||||
|
PAGE_ORDER[idx - 1]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============ RENDERING ============
|
||||||
|
|
||||||
|
fn render_imu_content(state: &DisplayState) -> alloc::string::String {
|
||||||
|
if let Some(ref imu) = state.last_imu {
|
||||||
|
format!(
|
||||||
|
"G:{:.0} {:.0} {:.0}\nT:{:.1}C",
|
||||||
|
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>(
|
||||||
|
terminal: &mut Terminal<B>,
|
||||||
|
current_screen: &Screen,
|
||||||
|
focused_button: Option<&PageButton>,
|
||||||
|
state: &DisplayState,
|
||||||
|
) {
|
||||||
|
let _ = terminal.draw(|f| {
|
||||||
|
// Handle errors with priority
|
||||||
|
if let Some(ref err) = state.last_error {
|
||||||
|
let content = format!("ERR: {}", err.as_str());
|
||||||
|
f.render_widget(Paragraph::new(content).red().bold(), f.area());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Layout: content area + button bar
|
||||||
|
let chunks = Layout::default()
|
||||||
|
.direction(Direction::Vertical)
|
||||||
|
.constraints([
|
||||||
|
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]);
|
||||||
|
});
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user