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>
187 lines
6.0 KiB
Rust
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();
|
|
}
|
|
}
|