WIP: functional checkpoint!
This commit is contained in:
1 parent
bb588c93ce
commit
38684482e8
19 files changed
+601
-151
No files matched your search
@@ -1,5 +1,6 @@
|
||||
//! OLED display driver with radio status dashboard.
|
||||
|
||||
mod render;
|
||||
pub mod sh1106;
|
||||
mod task;
|
||||
pub use task::display_task;
|
||||
+15
-14
@@ -69,10 +69,13 @@ pub fn dashboard(
|
||||
match status.state {
|
||||
RadioState::Receiving => {
|
||||
// "RX" inverted on row 0
|
||||
Rectangle::new(Point::new(0, 0), Size::new(MODE_BOX_W as u32, FONT_H as u32))
|
||||
.into_styled(fill)
|
||||
.draw(target)
|
||||
.ok();
|
||||
Rectangle::new(
|
||||
Point::new(0, 0),
|
||||
Size::new(MODE_BOX_W as u32, FONT_H as u32),
|
||||
)
|
||||
.into_styled(fill)
|
||||
.draw(target)
|
||||
.ok();
|
||||
Text::new("RX", Point::new(1, FONT_H - 1), inv_style)
|
||||
.draw(target)
|
||||
.ok();
|
||||
@@ -217,10 +220,7 @@ pub fn dashboard(
|
||||
}
|
||||
|
||||
/// Render the splash/waiting screen (shown when idle or no config).
|
||||
pub fn splash(
|
||||
target: &mut impl DrawTarget<Color = BinaryColor>,
|
||||
board: &BoardInfo,
|
||||
) {
|
||||
pub fn splash(target: &mut impl DrawTarget<Color = BinaryColor>, board: &BoardInfo) {
|
||||
let bb = target.bounding_box();
|
||||
let w = bb.size.width as i32;
|
||||
|
||||
@@ -324,11 +324,8 @@ fn rssi_sparkline(
|
||||
i + committed.saturating_sub(effective_bars)
|
||||
} else {
|
||||
let start = count - RSSI_HISTORY_LEN;
|
||||
let skip = RSSI_HISTORY_LEN.saturating_sub(if live {
|
||||
visible_bars - 1
|
||||
} else {
|
||||
visible_bars
|
||||
});
|
||||
let skip =
|
||||
RSSI_HISTORY_LEN.saturating_sub(if live { visible_bars - 1 } else { visible_bars });
|
||||
(start + skip + i) % RSSI_HISTORY_LEN
|
||||
};
|
||||
let is_tx = tx_history[idx];
|
||||
@@ -361,7 +358,11 @@ fn bar_height(rssi: i16, is_tx: bool, spark_h: i32) -> Option<i32> {
|
||||
let clamped = rssi.clamp(RSSI_MIN, RSSI_MAX);
|
||||
((clamped - RSSI_MIN) as i32 * spark_h) / (RSSI_MAX - RSSI_MIN) as i32
|
||||
};
|
||||
if h == 0 { None } else { Some(h) }
|
||||
if h == 0 {
|
||||
None
|
||||
} else {
|
||||
Some(h)
|
||||
}
|
||||
}
|
||||
|
||||
/// Draw a single sparkline bar (solid for RX, dotted for TX).
|
||||
|
||||
@@ -0,0 +1,136 @@
|
||||
//! Minimal async SH1106 I2C OLED driver (128x64, page addressing).
|
||||
//!
|
||||
//! The SH1106 is very similar to the SSD1306 but only supports page-mode
|
||||
//! addressing (no horizontal/vertical auto-advance). This driver sends
|
||||
//! framebuffer data page-by-page with explicit page/column commands.
|
||||
|
||||
use embedded_graphics::draw_target::DrawTarget;
|
||||
use embedded_graphics::geometry::{OriginDimensions, Size};
|
||||
use embedded_graphics::pixelcolor::BinaryColor;
|
||||
use embedded_graphics::Pixel;
|
||||
use embedded_hal_async::i2c::I2c;
|
||||
|
||||
const WIDTH: usize = 128;
|
||||
const HEIGHT: usize = 64;
|
||||
const PAGES: usize = HEIGHT / 8;
|
||||
const BUF_SIZE: usize = WIDTH * PAGES; // 1024
|
||||
|
||||
/// SH1106 has a 132-column RAM; visible 128 columns are centered with a
|
||||
/// 2-column offset on each side.
|
||||
const COL_OFFSET: u8 = 2;
|
||||
|
||||
pub struct Sh1106<I> {
|
||||
i2c: I,
|
||||
addr: u8,
|
||||
buffer: [u8; BUF_SIZE],
|
||||
}
|
||||
|
||||
impl<I: I2c> Sh1106<I> {
|
||||
pub fn new(i2c: I, addr: u8) -> Self {
|
||||
Self {
|
||||
i2c,
|
||||
addr,
|
||||
buffer: [0u8; BUF_SIZE],
|
||||
}
|
||||
}
|
||||
|
||||
/// Send a sequence of commands (each prefixed with the I2C command byte 0x00).
|
||||
async fn cmd(&mut self, commands: &[u8]) -> Result<(), I::Error> {
|
||||
for &c in commands {
|
||||
self.i2c.write(self.addr, &[0x00, c]).await?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Initialize the display with sensible defaults.
|
||||
pub async fn init(&mut self) -> Result<(), I::Error> {
|
||||
self.cmd(&[
|
||||
0xAE, // display off
|
||||
0xD5, 0x80, // clock div: default
|
||||
0xA8, 0x3F, // multiplex ratio: 64
|
||||
0xD3, 0x00, // display offset: 0
|
||||
0x40, // start line: 0
|
||||
0x8D, 0x14, // charge pump: enable
|
||||
0xA1, // segment remap: column 127 mapped to SEG0
|
||||
0xC8, // COM scan direction: remapped
|
||||
0xDA, 0x12, // COM pin config: alternative, no L/R remap
|
||||
0x81, 0xCF, // contrast: 207
|
||||
0xD9, 0xF1, // pre-charge: phase1=1, phase2=15
|
||||
0xDB, 0x40, // VCOMH deselect: ~0.77×Vcc
|
||||
0xA4, // entire display ON (follow RAM)
|
||||
0xA6, // normal display (not inverted)
|
||||
0xAF, // display on
|
||||
])
|
||||
.await
|
||||
}
|
||||
|
||||
/// Flush the entire framebuffer to the display, page by page.
|
||||
pub async fn flush(&mut self) -> Result<(), I::Error> {
|
||||
// Each I2C data write is prefixed with 0x40 (data continuation byte).
|
||||
let mut page_buf = [0u8; 1 + WIDTH]; // 0x40 + 128 data bytes
|
||||
page_buf[0] = 0x40;
|
||||
|
||||
for page in 0..PAGES {
|
||||
// Set page address and column start (with SH1106 2-column offset).
|
||||
self.cmd(&[
|
||||
0xB0 | page as u8, // page address
|
||||
0x00 | (COL_OFFSET & 0x0F), // column low nibble
|
||||
0x10 | (COL_OFFSET >> 4), // column high nibble
|
||||
])
|
||||
.await?;
|
||||
|
||||
let start = page * WIDTH;
|
||||
page_buf[1..].copy_from_slice(&self.buffer[start..start + WIDTH]);
|
||||
self.i2c.write(self.addr, &page_buf).await?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Set display brightness (contrast).
|
||||
pub async fn set_brightness(&mut self, value: u8) -> Result<(), I::Error> {
|
||||
self.cmd(&[0x81, value]).await
|
||||
}
|
||||
|
||||
/// Clear the framebuffer (call flush() to update the display).
|
||||
pub fn clear_buffer(&mut self) {
|
||||
self.buffer.fill(0);
|
||||
}
|
||||
}
|
||||
|
||||
impl<I: I2c> DrawTarget for Sh1106<I> {
|
||||
type Color = BinaryColor;
|
||||
type Error = core::convert::Infallible;
|
||||
|
||||
fn draw_iter<T>(&mut self, pixels: T) -> Result<(), Self::Error>
|
||||
where
|
||||
T: IntoIterator<Item = Pixel<Self::Color>>,
|
||||
{
|
||||
for Pixel(pos, color) in pixels {
|
||||
let x = pos.x;
|
||||
let y = pos.y;
|
||||
if x >= 0 && x < WIDTH as i32 && y >= 0 && y < HEIGHT as i32 {
|
||||
let x = x as usize;
|
||||
let y = y as usize;
|
||||
let idx = (y / 8) * WIDTH + x;
|
||||
let bit = (y % 8) as u8;
|
||||
if color.is_on() {
|
||||
self.buffer[idx] |= 1 << bit;
|
||||
} else {
|
||||
self.buffer[idx] &= !(1 << bit);
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn clear(&mut self, color: Self::Color) -> Result<(), Self::Error> {
|
||||
self.buffer.fill(if color.is_on() { 0xFF } else { 0x00 });
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl<I: I2c> OriginDimensions for Sh1106<I> {
|
||||
fn size(&self) -> Size {
|
||||
Size::new(WIDTH as u32, HEIGHT as u32)
|
||||
}
|
||||
}
|
||||
+79
-78
@@ -1,15 +1,11 @@
|
||||
use embassy_executor::task;
|
||||
use embassy_futures::select::{select3, Either3};
|
||||
use embassy_time::Timer;
|
||||
use ssd1306::mode::DisplayConfigAsync;
|
||||
use ssd1306::prelude::DisplayRotation;
|
||||
use ssd1306::size::DisplaySize128x64;
|
||||
use ssd1306::{I2CDisplayInterface, Ssd1306Async};
|
||||
use embedded_graphics::draw_target::DrawTarget;
|
||||
use embedded_graphics::pixelcolor::BinaryColor;
|
||||
|
||||
use crate::board::DisplayParts;
|
||||
use crate::channel::{
|
||||
DisplayCommand, DisplayCommandChannel, RadioState, RadioStatus, StatusWatch,
|
||||
};
|
||||
use crate::channel::{DisplayCommand, DisplayCommandChannel, RadioState, RadioStatus, StatusWatch};
|
||||
|
||||
use crate::board::{Board, LoRaBoard};
|
||||
|
||||
@@ -80,6 +76,28 @@ impl DisplayState {
|
||||
}
|
||||
}
|
||||
|
||||
/// Render the appropriate screen for the current state into the display buffer.
|
||||
fn render_current(
|
||||
display: &mut impl DrawTarget<Color = BinaryColor>,
|
||||
state: &DisplayState,
|
||||
board: &render::BoardInfo<'_>,
|
||||
) {
|
||||
if state.is_active() {
|
||||
render::dashboard(
|
||||
display,
|
||||
&state.last_status,
|
||||
&state.rssi_history,
|
||||
&state.tx_history,
|
||||
state.rssi_count,
|
||||
state.current_slot_rssi,
|
||||
state.current_slot_tx,
|
||||
board,
|
||||
);
|
||||
} else {
|
||||
render::splash(display, board);
|
||||
}
|
||||
}
|
||||
|
||||
#[task]
|
||||
pub async fn display_task(
|
||||
parts: DisplayParts,
|
||||
@@ -103,20 +121,51 @@ pub async fn display_task(
|
||||
mac: &mac_str,
|
||||
};
|
||||
|
||||
let interface = I2CDisplayInterface::new(parts.i2c);
|
||||
let mut display = Ssd1306Async::new(interface, DisplaySize128x64, DisplayRotation::Rotate0)
|
||||
.into_buffered_graphics_mode();
|
||||
cfg_if::cfg_if! {
|
||||
if #[cfg(feature = "wio_tracker_l1")] {
|
||||
let mut display = super::sh1106::Sh1106::new(parts.i2c, 0x3D);
|
||||
|
||||
if display.init().await.is_err() {
|
||||
defmt::error!("SSD1306 init failed");
|
||||
return;
|
||||
}
|
||||
if display
|
||||
.set_brightness(ssd1306::prelude::Brightness::BRIGHTEST)
|
||||
.await
|
||||
.is_err()
|
||||
{
|
||||
defmt::warn!("display brightness set failed");
|
||||
Timer::after_millis(100).await;
|
||||
|
||||
if display.init().await.is_err() {
|
||||
defmt::error!("SH1106 init failed — retrying");
|
||||
Timer::after_millis(500).await;
|
||||
if display.init().await.is_err() {
|
||||
defmt::error!("SH1106 init failed twice, giving up");
|
||||
return;
|
||||
}
|
||||
}
|
||||
if display.set_brightness(0xFF).await.is_err() {
|
||||
defmt::warn!("display brightness set failed");
|
||||
}
|
||||
} else {
|
||||
use ssd1306::mode::DisplayConfigAsync;
|
||||
use ssd1306::prelude::DisplayRotation;
|
||||
use ssd1306::size::DisplaySize128x64;
|
||||
use ssd1306::{I2CDisplayInterface, Ssd1306Async};
|
||||
|
||||
let interface = I2CDisplayInterface::new(parts.i2c);
|
||||
let mut display = Ssd1306Async::new(interface, DisplaySize128x64, DisplayRotation::Rotate0)
|
||||
.into_buffered_graphics_mode();
|
||||
|
||||
Timer::after_millis(100).await;
|
||||
|
||||
if display.init().await.is_err() {
|
||||
defmt::error!("SSD1306 init failed — retrying");
|
||||
Timer::after_millis(500).await;
|
||||
if display.init().await.is_err() {
|
||||
defmt::error!("SSD1306 init failed twice, giving up");
|
||||
return;
|
||||
}
|
||||
}
|
||||
if display
|
||||
.set_brightness(ssd1306::prelude::Brightness::BRIGHTEST)
|
||||
.await
|
||||
.is_err()
|
||||
{
|
||||
defmt::warn!("display brightness set failed");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let mut state = DisplayState::new();
|
||||
@@ -126,7 +175,8 @@ pub async fn display_task(
|
||||
};
|
||||
|
||||
// Show splash/waiting screen
|
||||
render_splash(&mut display, &board_info).await;
|
||||
render::splash(&mut display, &board_info);
|
||||
let _ = display.flush().await;
|
||||
|
||||
loop {
|
||||
match select3(
|
||||
@@ -151,7 +201,8 @@ pub async fn display_task(
|
||||
state.last_status = radio_status;
|
||||
|
||||
if state.display_on {
|
||||
render_current(&mut display, &state, &board_info).await;
|
||||
render_current(&mut display, &state, &board_info);
|
||||
let _ = display.flush().await;
|
||||
}
|
||||
}
|
||||
Either3::Second(cmd) => match cmd {
|
||||
@@ -167,73 +218,23 @@ pub async fn display_task(
|
||||
if let Some(s) = receiver.try_get() {
|
||||
state.last_status = s;
|
||||
}
|
||||
render_current(&mut display, &state, &board_info).await;
|
||||
render_current(&mut display, &state, &board_info);
|
||||
let _ = display.flush().await;
|
||||
}
|
||||
DisplayCommand::Reset => {
|
||||
state = DisplayState::new();
|
||||
render_splash(&mut display, &board_info).await;
|
||||
render::splash(&mut display, &board_info);
|
||||
let _ = display.flush().await;
|
||||
}
|
||||
},
|
||||
Either3::Third(()) => {
|
||||
// Timer tick: advance sparkline slot
|
||||
state.advance_slot();
|
||||
if state.display_on && state.is_active() {
|
||||
render_dashboard(&mut display, &state, &board_info).await;
|
||||
render_current(&mut display, &state, &board_info);
|
||||
let _ = display.flush().await;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// The concrete display type is verbose but avoids making this generic
|
||||
// over both DrawTarget and the SSD1306-specific flush method.
|
||||
type Display<I> = ssd1306::Ssd1306Async<
|
||||
I,
|
||||
DisplaySize128x64,
|
||||
ssd1306::mode::BufferedGraphicsModeAsync<DisplaySize128x64>,
|
||||
>;
|
||||
|
||||
/// Render the appropriate screen (active dashboard or splash) and flush.
|
||||
async fn render_current<I>(
|
||||
display: &mut Display<I>,
|
||||
state: &DisplayState,
|
||||
board: &render::BoardInfo<'_>,
|
||||
) where
|
||||
I: display_interface::AsyncWriteOnlyDataCommand,
|
||||
{
|
||||
if state.is_active() {
|
||||
render_dashboard(display, state, board).await;
|
||||
} else {
|
||||
render_splash(display, board).await;
|
||||
}
|
||||
}
|
||||
|
||||
/// Render the active dashboard and flush.
|
||||
async fn render_dashboard<I>(
|
||||
display: &mut Display<I>,
|
||||
state: &DisplayState,
|
||||
board: &render::BoardInfo<'_>,
|
||||
) where
|
||||
I: display_interface::AsyncWriteOnlyDataCommand,
|
||||
{
|
||||
render::dashboard(
|
||||
display,
|
||||
&state.last_status,
|
||||
&state.rssi_history,
|
||||
&state.tx_history,
|
||||
state.rssi_count,
|
||||
state.current_slot_rssi,
|
||||
state.current_slot_tx,
|
||||
board,
|
||||
);
|
||||
let _ = display.flush().await;
|
||||
}
|
||||
|
||||
/// Render the splash/waiting screen and flush.
|
||||
async fn render_splash<I>(display: &mut Display<I>, board: &render::BoardInfo<'_>)
|
||||
where
|
||||
I: display_interface::AsyncWriteOnlyDataCommand,
|
||||
{
|
||||
render::splash(display, board);
|
||||
let _ = display.flush().await;
|
||||
}
|
||||
Reference in new issue
Block a user