This repository has been archived on 2026-08-31. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
donglora/vendor/ssd1306/examples/rtic_brightness.rs
T
swaitsandClaude Opus 4.6 3102fc747a feat: add display UI with splash screen, dashboard, and RSSI sparkline
Implement full SSD1306 OLED display support:
- Splash screen on boot (1.5s) with project name, version, board
- Dashboard showing radio state, config, counters, RSSI/SNR
- RSSI history sparkline (64-sample ring buffer, 2px-wide bars)
- DisplayOn/DisplayOff host commands routed via dedicated channel
- Pure render functions against generic DrawTarget (no hardware coupling)
- Display on/off control bypasses radio task entirely

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-29 20:42:54 -06:00

187 lines
6.0 KiB
Rust

//! Cycle the screen brightness through 5 predefined brightness levels when the "DVD" logo hits one
//! of the sides of the display.
//!
//! For best results, run with the `--release` flag.
#![no_std]
#![no_main]
pub mod pac {
pub use embassy_stm32::pac::Interrupt as interrupt;
pub use embassy_stm32::pac::*;
}
#[rtic::app(device = crate::pac, peripherals= false, dispatchers = [EXTI0])]
mod app {
use defmt_rtt as _;
use display_interface_spi::SPIInterface;
use embassy_stm32::{
gpio,
mode::Blocking,
spi::{self, Spi},
time::Hertz,
timer::low_level::Timer,
Config,
};
use embedded_graphics::{
geometry::Point,
image::Image,
pixelcolor::{BinaryColor, Rgb565},
prelude::*,
primitives::{PrimitiveStyle, Rectangle},
};
use panic_probe as _;
use ssd1306::{mode::BufferedGraphicsMode, prelude::*, Ssd1306};
use tinybmp::Bmp;
type Display = Ssd1306<
SPIInterface<
embedded_hal_bus::spi::ExclusiveDevice<
Spi<'static, Blocking>,
gpio::Output<'static>,
embedded_hal_bus::spi::NoDelay,
>,
gpio::Output<'static>,
>,
DisplaySize128x64,
BufferedGraphicsMode<DisplaySize128x64>,
>;
#[shared]
struct SharedResources {}
#[local]
struct Resources {
display: Display,
timer: Timer<'static, embassy_stm32::peripherals::TIM1>,
top_left: Point,
velocity: Point,
bmp: Bmp<Rgb565, 'static>,
brightness: Brightness,
}
#[init]
fn init(_cx: init::Context) -> (SharedResources, Resources, init::Monotonics) {
let mut config: Config = Default::default();
config.rcc.hse = Some(embassy_stm32::rcc::Hse {
freq: Hertz::mhz(8),
mode: embassy_stm32::rcc::HseMode::Oscillator,
});
config.rcc.sys = embassy_stm32::rcc::Sysclk::PLL1_P;
config.rcc.pll = Some(embassy_stm32::rcc::Pll {
src: embassy_stm32::rcc::PllSource::HSE,
prediv: embassy_stm32::rcc::PllPreDiv::DIV1,
mul: embassy_stm32::rcc::PllMul::MUL9, // 8 * 9 = 72Mhz
});
// Scale down to 36Mhz (maximum allowed)
config.rcc.apb1_pre = embassy_stm32::rcc::APBPrescaler::DIV2;
let p = embassy_stm32::init(config);
let mut config = spi::Config::default();
config.frequency = Hertz::mhz(8);
let spi = Spi::new_blocking_txonly(p.SPI1, p.PA5, p.PA7, config);
let mut rst = gpio::Output::new(p.PB0, gpio::Level::Low, gpio::Speed::Low);
let dc = gpio::Output::new(p.PB1, gpio::Level::Low, gpio::Speed::Low);
let cs = gpio::Output::new(p.PB10, gpio::Level::Low, gpio::Speed::Low);
let spi = embedded_hal_bus::spi::ExclusiveDevice::new_no_delay(spi, cs).unwrap();
let interface = display_interface_spi::SPIInterface::new(spi, dc);
let mut display = Ssd1306::new(interface, DisplaySize128x64, DisplayRotation::Rotate180)
.into_buffered_graphics_mode();
display
.reset(&mut rst, &mut embassy_time::Delay {})
.unwrap();
display.init().unwrap();
// Forget the RST pin to keep the display out of reset
core::mem::forget(rst);
// Update framerate
let timer = Timer::new(p.TIM1);
timer.set_frequency(Hertz(20)); // 20 FPS
timer.enable_update_interrupt(true);
timer.start();
let bmp = Bmp::from_slice(include_bytes!("dvd.bmp")).unwrap();
// Init the static resources to use them later through RTIC
(
SharedResources {},
Resources {
timer,
display,
top_left: Point::new(5, 3),
velocity: Point::new(1, 1),
bmp,
brightness: Brightness::default(),
},
init::Monotonics(),
)
}
#[task(binds = TIM1_UP, local = [display, top_left, velocity, timer, bmp, brightness])]
fn update(cx: update::Context) {
let update::LocalResources {
display,
top_left,
velocity,
timer,
bmp,
brightness,
..
} = cx.local;
let bottom_right = *top_left + bmp.bounding_box().size;
// Erase previous image position with a filled black rectangle
Rectangle::with_corners(*top_left, bottom_right)
.into_styled(PrimitiveStyle::with_fill(BinaryColor::Off))
.draw(display)
.unwrap();
// Check if the image collided with a screen edge
{
let mut collision = false;
if bottom_right.x > display.size().width as i32 || top_left.x < 0 {
velocity.x = -velocity.x;
collision = true;
}
if bottom_right.y > display.size().height as i32 || top_left.y < 0 {
velocity.y = -velocity.y;
collision = true;
}
if collision {
// Change the brightness
*brightness = match *brightness {
Brightness::DIMMEST => Brightness::DIM,
Brightness::DIM => Brightness::NORMAL,
Brightness::NORMAL => Brightness::BRIGHT,
Brightness::BRIGHT => Brightness::BRIGHTEST,
Brightness::BRIGHTEST => Brightness::DIMMEST, // restart cycle
_ => Brightness::NORMAL, // Brightness is not an enum, cover rest of patterns
};
// Send the new brightness value to the display
display.set_brightness(*brightness).unwrap();
}
}
// Move the image
*top_left += *velocity;
// Draw image at new position
Image::new(bmp, *top_left)
.draw(&mut display.color_converted())
.unwrap();
// Write changes to the display
display.flush().unwrap();
// Clears the update flag
timer.clear_update_interrupt();
}
}