pages-tui working

This commit is contained in:
Priec
2026-01-18 15:23:04 +01:00
parent 273bf2f946
commit c5fef061f4
8 changed files with 343 additions and 117 deletions

View File

@@ -1,84 +1,24 @@
// src/display/task.rs
//! SSD1306 display rendering task optimized for 0.91" 128x32 OLED.
use embassy_time::{Duration, Timer};
use log::{error, info};
use alloc::boxed::Box;
use alloc::format;
use heapless::String;
use mousefood::{EmbeddedBackend, EmbeddedBackendConfig};
use ratatui::{
layout::{Constraint, Direction, Layout},
style::{Style, Stylize},
widgets::Paragraph,
Terminal,
};
use ssd1306::{
mode::BufferedGraphicsMode, prelude::*, I2CDisplayInterface, Ssd1306,
};
use ratatui::Terminal;
use ssd1306::{mode::BufferedGraphicsMode, prelude::*, I2CDisplayInterface, Ssd1306};
use crate::bus::I2cDevice;
use crate::contracts::{DisplayCommand, ImuReading};
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;
/// 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]
pub async fn display_task(i2c: I2cDevice) {
info!("Display task starting...");
// Initialize SSD1306 display for 128x32 variant
let interface = I2CDisplayInterface::new(i2c);
let mut display = Ssd1306::new(interface, DisplaySize128x32, DisplayRotation::Rotate0)
.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 {
flush_callback: Box::new(
|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 state = DisplayState::default();
let mut orchestrator = Orchestrator::<Screen>::new();
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 {
// Process all pending commands (non-blocking)
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_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;
}
}
/// 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]);
});
}