working everything properly well
This commit is contained in:
81
mqtt_display/src/display/api.rs
Normal file
81
mqtt_display/src/display/api.rs
Normal file
@@ -0,0 +1,81 @@
|
||||
// src/display/api.rs
|
||||
//! 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::channel::{Channel, Receiver, TrySendError};
|
||||
use heapless::String as HString;
|
||||
|
||||
use crate::contracts::{DisplayCommand, ImuReading};
|
||||
|
||||
/// Queue size for display commands.
|
||||
/// Moderate size to handle bursts without dropping.
|
||||
const QUEUE_SIZE: usize = 8;
|
||||
|
||||
/// Channel for sending commands to the display task.
|
||||
pub(crate) static DISPLAY_CHANNEL: Channel<CriticalSectionRawMutex, DisplayCommand, QUEUE_SIZE> =
|
||||
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) {
|
||||
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> {
|
||||
DISPLAY_CHANNEL.try_send(cmd).map_err(|e| match e {
|
||||
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> {
|
||||
DISPLAY_CHANNEL.receiver()
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
// Convenience functions for common commands
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
/// Send IMU data to the display.
|
||||
pub async fn show_imu(reading: ImuReading) {
|
||||
send(DisplayCommand::SetImu(reading)).await;
|
||||
}
|
||||
|
||||
/// Set the status line.
|
||||
pub async fn set_status(text: &str) {
|
||||
let mut s = HString::<32>::new();
|
||||
let _ = s.push_str(&text[..text.len().min(32)]);
|
||||
send(DisplayCommand::SetStatus(s)).await;
|
||||
}
|
||||
|
||||
/// Show an error message.
|
||||
pub async fn show_error(text: &str) {
|
||||
let mut s = HString::<64>::new();
|
||||
let _ = s.push_str(&text[..text.len().min(64)]);
|
||||
send(DisplayCommand::ShowError(s)).await;
|
||||
}
|
||||
|
||||
/// Update MQTT status indicator.
|
||||
pub async fn set_mqtt_status(connected: bool, msg_count: u32) {
|
||||
send(DisplayCommand::SetMqttStatus { connected, msg_count }).await;
|
||||
}
|
||||
|
||||
/// Clear the display.
|
||||
pub async fn clear() {
|
||||
send(DisplayCommand::Clear).await;
|
||||
}
|
||||
5
mqtt_display/src/display/mod.rs
Normal file
5
mqtt_display/src/display/mod.rs
Normal file
@@ -0,0 +1,5 @@
|
||||
// src/display/mod.rs
|
||||
//! SSD1306 OLED display
|
||||
|
||||
pub mod api;
|
||||
pub mod task;
|
||||
165
mqtt_display/src/display/task.rs
Normal file
165
mqtt_display/src/display/task.rs
Normal file
@@ -0,0 +1,165 @@
|
||||
// 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 as HString;
|
||||
use mousefood::{EmbeddedBackend, EmbeddedBackendConfig};
|
||||
use ratatui::{
|
||||
layout::{Constraint, Direction, Layout},
|
||||
style::{Style, Stylize},
|
||||
widgets::Paragraph,
|
||||
Terminal,
|
||||
};
|
||||
use ssd1306::{
|
||||
mode::BufferedGraphicsMode, prelude::*, I2CDisplayInterface, Ssd1306,
|
||||
};
|
||||
|
||||
use crate::bus::I2cDevice;
|
||||
use crate::contracts::{DisplayCommand, ImuReading};
|
||||
use crate::display::api::receiver;
|
||||
|
||||
/// Display refresh interval in milliseconds.
|
||||
const REFRESH_INTERVAL_MS: u64 = 100;
|
||||
|
||||
/// Internal state for what to render.
|
||||
struct DisplayState {
|
||||
status: HString<32>,
|
||||
last_imu: Option<ImuReading>,
|
||||
last_error: Option<HString<64>>,
|
||||
mqtt_connected: bool,
|
||||
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 {
|
||||
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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 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();
|
||||
|
||||
if let Err(e) = display.init() {
|
||||
error!("Display init failed: {:?}", e);
|
||||
loop {
|
||||
Timer::after(Duration::from_secs(60)).await;
|
||||
}
|
||||
}
|
||||
|
||||
info!("SSD1306 display initialized (128x32)");
|
||||
|
||||
// Configure mousefood backend for ratatui
|
||||
let config = EmbeddedBackendConfig {
|
||||
flush_callback: Box::new(
|
||||
|d: &mut Ssd1306<_, _, BufferedGraphicsMode<DisplaySize128x32>>| {
|
||||
let _ = d.flush();
|
||||
},
|
||||
),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let backend = EmbeddedBackend::new(&mut display, config);
|
||||
let mut terminal = Terminal::new(backend).expect("terminal init failed");
|
||||
|
||||
let mut state = DisplayState::default();
|
||||
let rx = receiver();
|
||||
|
||||
info!("Display task entering render loop");
|
||||
|
||||
loop {
|
||||
// Process all pending commands (non-blocking)
|
||||
while let Ok(cmd) = rx.try_receive() {
|
||||
state.apply_command(cmd);
|
||||
}
|
||||
|
||||
// Render current state
|
||||
render_frame(&mut terminal, &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]);
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user