663 lines
23 KiB
Rust
663 lines
23 KiB
Rust
//! Screen Output
|
|
|
|
use embedded_graphics::prelude::*;
|
|
use embedded_graphics::{geometry, mono_font, pixelcolor, primitives, text};
|
|
|
|
use crate::applogic;
|
|
|
|
pub struct DisplaySize {
|
|
width: u16,
|
|
height: u16,
|
|
offset_top: i32,
|
|
corner_radius: i32,
|
|
}
|
|
|
|
pub struct Handler<D>
|
|
where
|
|
D: DrawTarget<Color = pixelcolor::Rgb565>,
|
|
{
|
|
target: D,
|
|
size: DisplaySize,
|
|
|
|
// internal state
|
|
prev_state: ScreenState,
|
|
hb_state: bool,
|
|
gnss_hb_state: bool,
|
|
}
|
|
|
|
#[derive(Debug, Default, PartialEq, Eq)]
|
|
pub enum ScreenState {
|
|
#[default]
|
|
Uninitialized,
|
|
/// aka. "no GNSS fix" or "splash screen"
|
|
Initial,
|
|
Error(alloc::string::String),
|
|
#[cfg(feature = "spat")]
|
|
GlosaSearching(applogic::GlosaSearching),
|
|
#[cfg(feature = "spat")]
|
|
GlosaFound(applogic::GlosaFound),
|
|
#[cfg(feature = "spat")]
|
|
GlosaLocked(applogic::GlosaSignalInfo),
|
|
}
|
|
|
|
#[cfg(feature = "spat")]
|
|
impl From<applogic::GlosaOutput> for ScreenState {
|
|
fn from(value: applogic::GlosaOutput) -> Self {
|
|
match value {
|
|
applogic::GlosaOutput::Searching(glosa_searching) => {
|
|
Self::GlosaSearching(glosa_searching)
|
|
}
|
|
applogic::GlosaOutput::Found(glosa_found) => Self::GlosaFound(glosa_found),
|
|
applogic::GlosaOutput::Locked(glosa_signal_info) => {
|
|
Self::GlosaLocked(glosa_signal_info)
|
|
}
|
|
applogic::GlosaOutput::Error(err_str) => Self::Error(err_str),
|
|
}
|
|
}
|
|
}
|
|
|
|
impl<D> Handler<D>
|
|
where
|
|
D: DrawTarget<Color = pixelcolor::Rgb565>,
|
|
{
|
|
const COLOR_BG: pixelcolor::Rgb565 = pixelcolor::Rgb565::BLACK;
|
|
const COLOR_FG: pixelcolor::Rgb565 = pixelcolor::Rgb565::WHITE;
|
|
|
|
const BK_STYLE: primitives::PrimitiveStyle<pixelcolor::Rgb565> =
|
|
primitives::PrimitiveStyleBuilder::new()
|
|
.fill_color(Self::COLOR_BG)
|
|
.build();
|
|
const RD_STYLE: primitives::PrimitiveStyle<pixelcolor::Rgb565> =
|
|
primitives::PrimitiveStyleBuilder::new()
|
|
.fill_color(pixelcolor::Rgb565::CSS_RED)
|
|
.build();
|
|
const YE_STYLE: primitives::PrimitiveStyle<pixelcolor::Rgb565> =
|
|
primitives::PrimitiveStyleBuilder::new()
|
|
.fill_color(pixelcolor::Rgb565::new(31, 55, 0))
|
|
.build();
|
|
const GN_STYLE: primitives::PrimitiveStyle<pixelcolor::Rgb565> =
|
|
primitives::PrimitiveStyleBuilder::new()
|
|
.fill_color(pixelcolor::Rgb565::new(10, 50, 0))
|
|
.build();
|
|
const BLANK_SIGNAL_STYLE: primitives::PrimitiveStyle<pixelcolor::Rgb565> =
|
|
primitives::PrimitiveStyleBuilder::new()
|
|
.fill_color(pixelcolor::Rgb565::CSS_GRAY)
|
|
.build();
|
|
|
|
const DEFAULT_TEXT_STYLE: mono_font::MonoTextStyle<'static, pixelcolor::Rgb565> =
|
|
mono_font::MonoTextStyle::new(&profont::PROFONT_24_POINT, Self::COLOR_FG);
|
|
const MID_TEXT_STYLE: mono_font::MonoTextStyle<'static, pixelcolor::Rgb565> =
|
|
mono_font::MonoTextStyle::new(&profont::PROFONT_18_POINT, Self::COLOR_FG);
|
|
const SMALL_TEXT_STYLE: mono_font::MonoTextStyle<'static, pixelcolor::Rgb565> =
|
|
mono_font::MonoTextStyle::new(&profont::PROFONT_14_POINT, Self::COLOR_FG);
|
|
|
|
const BIG_HEIGHT_THLD: u16 = 172;
|
|
|
|
pub fn new(target: D, display_size: DisplaySize) -> Self {
|
|
Self {
|
|
target,
|
|
size: display_size,
|
|
prev_state: ScreenState::Uninitialized,
|
|
hb_state: false,
|
|
gnss_hb_state: false,
|
|
}
|
|
}
|
|
|
|
pub fn update(&mut self, state: ScreenState) -> Result<(), D::Error> {
|
|
if self.prev_state != state {
|
|
match &state {
|
|
ScreenState::Uninitialized | ScreenState::Initial => self.draw_slash_screen()?,
|
|
ScreenState::GlosaSearching(_glosa_searching) => self.draw_glosa_searching()?,
|
|
ScreenState::GlosaFound(glosa_found) => self.draw_glosa_found(glosa_found)?,
|
|
ScreenState::GlosaLocked(glosa_data) => {
|
|
let prev_glosa_data =
|
|
if let ScreenState::GlosaLocked(prev_glosa_data) = &self.prev_state {
|
|
Some(prev_glosa_data.clone())
|
|
} else {
|
|
None
|
|
};
|
|
|
|
self.draw_glosa(glosa_data, prev_glosa_data)?;
|
|
}
|
|
ScreenState::Error(msg) => self.draw_message(msg)?,
|
|
}
|
|
}
|
|
|
|
// Update indicator/ heartbeat
|
|
{
|
|
let update_ind_dia = 10;
|
|
let circle_x_pos =
|
|
i32::from(self.size.width) - update_ind_dia - self.size.corner_radius;
|
|
|
|
text::Text::with_alignment(
|
|
"HB",
|
|
geometry::Point::new(circle_x_pos - 5, 10 + self.size.offset_top),
|
|
Self::SMALL_TEXT_STYLE,
|
|
text::Alignment::Right,
|
|
)
|
|
.draw(&mut self.target)?;
|
|
|
|
primitives::Circle::new(
|
|
geometry::Point::new(circle_x_pos, 2 + self.size.offset_top),
|
|
update_ind_dia.cast_unsigned(),
|
|
)
|
|
.into_styled(if self.hb_state {
|
|
Self::GN_STYLE
|
|
} else {
|
|
Self::BK_STYLE
|
|
})
|
|
.draw(&mut self.target)?;
|
|
}
|
|
|
|
self.prev_state = state;
|
|
self.hb_state = !self.hb_state;
|
|
|
|
Ok(())
|
|
}
|
|
|
|
// on big screns: uses full screen width and height from 250 to 280 pixels
|
|
pub fn update_gnss(
|
|
&mut self,
|
|
pvt: &ublox::nav_pvt::proto27::NavPvtRef,
|
|
) -> Result<(), D::Error> {
|
|
use alloc::string::ToString as _;
|
|
|
|
// this only applies to big screens
|
|
if !self.is_big_height() {
|
|
return Ok(());
|
|
}
|
|
|
|
let num_sat = pvt.num_satellites();
|
|
let fix_str = match pvt.fix_type() {
|
|
ublox::GnssFixType::NoFix
|
|
| ublox::GnssFixType::DeadReckoningOnly
|
|
| ublox::GnssFixType::TimeOnlyFix => {
|
|
alloc::format!("{num_sat:2} sat")
|
|
}
|
|
ublox::GnssFixType::Fix2D => "2D fix".to_string(),
|
|
ublox::GnssFixType::Fix3D | ublox::GnssFixType::GPSPlusDeadReckoning => {
|
|
"3D fix".to_string()
|
|
}
|
|
_ => " ".to_string(),
|
|
};
|
|
|
|
// clear area and write information
|
|
// note: text position is x baseline, not top left corner
|
|
primitives::Rectangle::new(
|
|
geometry::Point::new(0, 250 + self.size.offset_top),
|
|
geometry::Size::new(self.size.width.into(), 20),
|
|
)
|
|
.into_styled(Self::BK_STYLE)
|
|
.draw(&mut self.target)?;
|
|
|
|
let colon = if self.gnss_hb_state { ":" } else { " " };
|
|
let message = alloc::format!(
|
|
"{fix_str}{colon} {:2.0}km/h, {:3.0}°",
|
|
pvt.ground_speed_2d() * 3.6,
|
|
pvt.heading_motion()
|
|
);
|
|
text::Text::with_alignment(
|
|
&message,
|
|
geometry::Point::new(self.size.corner_radius / 2, 265 + self.size.offset_top),
|
|
Self::SMALL_TEXT_STYLE,
|
|
text::Alignment::Left,
|
|
)
|
|
.draw(&mut self.target)?;
|
|
|
|
self.gnss_hb_state = !self.gnss_hb_state;
|
|
|
|
Ok(())
|
|
}
|
|
|
|
fn clear(&mut self) -> Result<(), D::Error>
|
|
where
|
|
D: DrawTarget<Color = pixelcolor::Rgb565>,
|
|
{
|
|
primitives::Rectangle::new(
|
|
geometry::Point::new(0, 0),
|
|
geometry::Size::new(self.size.width.into(), self.size.height.into()),
|
|
)
|
|
.into_styled(Self::BK_STYLE)
|
|
.draw(&mut self.target)?;
|
|
|
|
Ok(())
|
|
}
|
|
|
|
fn is_big_height(&self) -> bool {
|
|
self.size.height > Self::BIG_HEIGHT_THLD
|
|
}
|
|
|
|
#[allow(unused)]
|
|
pub fn test_img(&mut self) -> Result<(), D::Error> {
|
|
mipidsi::TestImage::new().draw(&mut self.target)?;
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[allow(unused)]
|
|
pub fn draw_message(&mut self, msg: &str) -> Result<(), D::Error> {
|
|
// clear whole display and draw text
|
|
self.clear()?;
|
|
|
|
text::Text::with_alignment(
|
|
msg,
|
|
geometry::Point::new(i32::from(self.size.width) / 2, 40 + self.size.offset_top),
|
|
Self::DEFAULT_TEXT_STYLE,
|
|
text::Alignment::Center,
|
|
)
|
|
.draw(&mut self.target)?;
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[allow(unused)]
|
|
fn draw_glosa_searching(&mut self) -> Result<(), D::Error> {
|
|
// clear whole display and draw text
|
|
self.clear()?;
|
|
|
|
text::Text::with_alignment(
|
|
"Searching...",
|
|
geometry::Point::new(i32::from(self.size.width) / 2, 130 + self.size.offset_top),
|
|
Self::DEFAULT_TEXT_STYLE,
|
|
text::Alignment::Center,
|
|
)
|
|
.draw(&mut self.target)?;
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[allow(unused)]
|
|
fn draw_glosa_found(&mut self, data: &applogic::GlosaFound) -> Result<(), D::Error> {
|
|
// clear whole display and draw text
|
|
self.clear()?;
|
|
|
|
let text = if let Some(appr) = data.approach_id {
|
|
alloc::format!("Int. {}\nApproach {appr}", data.intersection_id)
|
|
} else {
|
|
alloc::format!("Int. {}\nno approach ID available", data.intersection_id)
|
|
};
|
|
|
|
text::Text::with_alignment(
|
|
&text,
|
|
geometry::Point::new(i32::from(self.size.width) / 2, 130 + self.size.offset_top),
|
|
Self::DEFAULT_TEXT_STYLE,
|
|
text::Alignment::Center,
|
|
)
|
|
.draw(&mut self.target)?;
|
|
|
|
Ok(())
|
|
}
|
|
|
|
// uses full screen width and height
|
|
// - from 0 to 105+67 = 172 pixels for small screens
|
|
// - from 0 to 105+67+20 = 192 pixels for big screens
|
|
#[allow(unused)]
|
|
fn draw_glosa(
|
|
&mut self,
|
|
data: &applogic::GlosaSignalInfo,
|
|
prev_data: Option<applogic::GlosaSignalInfo>,
|
|
) -> Result<(), D::Error> {
|
|
let num_signals = data.signal_groups.len();
|
|
|
|
let is_big_height = self.is_big_height();
|
|
|
|
if let Some(prev) = prev_data
|
|
&& prev.signal_groups.len() != num_signals
|
|
{
|
|
// clear everything if number of signals has changed
|
|
self.clear();
|
|
} else {
|
|
// only clear maneuver and timing area otherwise
|
|
let sig_total_height = 67 + if is_big_height { 20 } else { 0 };
|
|
primitives::Rectangle::new(
|
|
geometry::Point::new(0, 105 + self.size.offset_top),
|
|
geometry::Size::new(self.size.width.into(), sig_total_height),
|
|
)
|
|
.into_styled(Self::BK_STYLE)
|
|
.draw(&mut self.target)?;
|
|
}
|
|
|
|
// do nothing more when no signals
|
|
if num_signals == 0 {
|
|
return Ok(());
|
|
}
|
|
|
|
// draw intersection ID
|
|
text::Text::new(
|
|
&alloc::format!("#{}", data.intersection_id),
|
|
geometry::Point::new(self.size.corner_radius + 5, 10 + self.size.offset_top),
|
|
Self::SMALL_TEXT_STYLE,
|
|
)
|
|
.draw(&mut self.target)?;
|
|
|
|
#[allow(clippy::cast_possible_truncation, clippy::cast_possible_wrap)]
|
|
let sig_width = i32::from(self.size.width) / (num_signals as i32);
|
|
|
|
for (idx, sig) in data.signal_groups.iter().enumerate() {
|
|
#[allow(clippy::cast_possible_truncation, clippy::cast_possible_wrap)]
|
|
let offset = (idx as i32) * sig_width;
|
|
|
|
let circle_dia = 50;
|
|
let circle_margin = (sig_width - circle_dia) / 2;
|
|
let centerline = offset + (sig_width / 2);
|
|
|
|
// draw phase color
|
|
let style = match sig.phase {
|
|
applogic::SignalPhase::Unknown => Self::BLANK_SIGNAL_STYLE,
|
|
applogic::SignalPhase::Red => Self::RD_STYLE,
|
|
applogic::SignalPhase::Green => Self::GN_STYLE,
|
|
applogic::SignalPhase::RedYellow | applogic::SignalPhase::Yellow => Self::YE_STYLE,
|
|
};
|
|
primitives::Circle::new(
|
|
geometry::Point::new(offset + circle_margin, 15 + self.size.offset_top),
|
|
circle_dia.cast_unsigned(),
|
|
)
|
|
.into_styled(style)
|
|
.draw(&mut self.target)?;
|
|
|
|
// TODO: draw lane type?
|
|
|
|
// draw maneuver
|
|
text::Text::with_alignment(
|
|
&maneuver_to_str(sig.maneuver),
|
|
geometry::Point::new(centerline, 130 + self.size.offset_top),
|
|
Self::DEFAULT_TEXT_STYLE,
|
|
text::Alignment::Center,
|
|
)
|
|
.draw(&mut self.target)?;
|
|
|
|
// draw timing indicator(s)
|
|
if let Some(timing) = &sig.timing {
|
|
if let (Some(likely), Some(max)) = (timing.likely_end_sec, timing.max_end_sec) {
|
|
text::Text::with_alignment(
|
|
&alloc::format!("{likely}s"),
|
|
geometry::Point::new(centerline, 160 + self.size.offset_top),
|
|
Self::DEFAULT_TEXT_STYLE,
|
|
text::Alignment::Center,
|
|
)
|
|
.draw(&mut self.target)?;
|
|
|
|
// only for big screen
|
|
if is_big_height {
|
|
// TODO: show as diff to likely time?
|
|
text::Text::with_alignment(
|
|
&alloc::format!("{}-{max}", timing.min_end_sec),
|
|
geometry::Point::new(centerline, 185 + self.size.offset_top),
|
|
Self::MID_TEXT_STYLE,
|
|
text::Alignment::Center,
|
|
)
|
|
.draw(&mut self.target)?;
|
|
}
|
|
} else {
|
|
text::Text::with_alignment(
|
|
&alloc::format!("{}s", timing.min_end_sec),
|
|
geometry::Point::new(centerline, 160 + self.size.offset_top),
|
|
Self::DEFAULT_TEXT_STYLE,
|
|
text::Alignment::Center,
|
|
)
|
|
.draw(&mut self.target)?;
|
|
}
|
|
}
|
|
}
|
|
|
|
// if is_big_height {
|
|
// // TODO: Add ETA
|
|
// }
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[allow(unused)]
|
|
fn draw_slash_screen(&mut self) -> Result<(), D::Error> {
|
|
let text_style = mono_font::MonoTextStyle::new(&profont::PROFONT_18_POINT, Self::COLOR_FG);
|
|
|
|
self.clear()?;
|
|
|
|
let sig_width = i32::from(self.size.width) / 3;
|
|
let circle_dia = 50;
|
|
let circle_margin = (sig_width - circle_dia) / 2;
|
|
|
|
for (idx, style) in [Self::RD_STYLE, Self::YE_STYLE, Self::GN_STYLE]
|
|
.iter()
|
|
.enumerate()
|
|
{
|
|
#[allow(clippy::cast_possible_truncation, clippy::cast_possible_wrap)]
|
|
let offset = (idx as i32) * sig_width;
|
|
|
|
primitives::Circle::new(
|
|
geometry::Point::new(offset + circle_margin, 15 + self.size.offset_top),
|
|
circle_dia.cast_unsigned(),
|
|
)
|
|
.into_styled(*style)
|
|
.draw(&mut self.target)?;
|
|
}
|
|
|
|
// draw welcome text
|
|
let centerline = i32::from(self.size.width) / 2;
|
|
text::Text::with_alignment(
|
|
"C-ITS Signal Phase\nWaiting for GNSS...",
|
|
geometry::Point::new(centerline, 130 + self.size.offset_top),
|
|
text_style,
|
|
text::Alignment::Center,
|
|
)
|
|
.draw(&mut self.target)?;
|
|
|
|
Ok(())
|
|
}
|
|
}
|
|
|
|
fn maneuver_to_str(value: applogic::v2x::map::Maneuvers) -> alloc::string::String {
|
|
alloc::format!(
|
|
"{}{}{}",
|
|
if value.left_allowed { "<" } else { " " },
|
|
if value.staight_allowed { "^" } else { " " },
|
|
if value.right_allowed { ">" } else { " " },
|
|
)
|
|
}
|
|
|
|
/// Creates an [`mipidsi::Display`], width, height tuple for an ST7789 172*320px screen in landscape orientation
|
|
///
|
|
/// # Errors
|
|
/// Human-readable fatal errors
|
|
#[allow(unused, clippy::too_many_arguments, clippy::type_complexity)]
|
|
pub fn make_small_display<IoScl, IoSda, IoRes, IoDc, IoCs, IoBl, Spi>(
|
|
scl: IoScl,
|
|
sda: IoSda,
|
|
res: IoRes,
|
|
dc: IoDc,
|
|
cs: IoCs,
|
|
bl: IoBl,
|
|
spi: Spi,
|
|
spi_buffer: &mut [u8],
|
|
) -> Result<
|
|
(
|
|
mipidsi::Display<
|
|
mipidsi::interface::SpiInterface<
|
|
'_,
|
|
embedded_hal_bus::spi::ExclusiveDevice<
|
|
esp_hal::spi::master::Spi<'_, esp_hal::Blocking>,
|
|
esp_hal::gpio::Output<'_>,
|
|
embedded_hal_bus::spi::NoDelay,
|
|
>,
|
|
esp_hal::gpio::Output<'_>,
|
|
>,
|
|
mipidsi::models::ST7789,
|
|
esp_hal::gpio::Output<'_>,
|
|
>,
|
|
DisplaySize,
|
|
),
|
|
alloc::string::String,
|
|
>
|
|
where
|
|
IoScl: esp_hal::gpio::interconnect::PeripheralOutput<'static>,
|
|
IoSda: esp_hal::gpio::interconnect::PeripheralOutput<'static>,
|
|
IoRes:
|
|
esp_hal::gpio::interconnect::PeripheralOutput<'static> + esp_hal::gpio::OutputPin + 'static,
|
|
IoDc:
|
|
esp_hal::gpio::interconnect::PeripheralOutput<'static> + esp_hal::gpio::OutputPin + 'static,
|
|
IoCs:
|
|
esp_hal::gpio::interconnect::PeripheralOutput<'static> + esp_hal::gpio::OutputPin + 'static,
|
|
IoBl:
|
|
esp_hal::gpio::interconnect::PeripheralOutput<'static> + esp_hal::gpio::OutputPin + 'static,
|
|
Spi: esp_hal::spi::master::Instance + 'static,
|
|
{
|
|
const DISPLAY_HIDDEN_X: u16 = 34; // somehow this display has 34 px of invisible space on the left (in native orientation)
|
|
|
|
let size = DisplaySize {
|
|
width: 320,
|
|
height: 172,
|
|
offset_top: 0,
|
|
corner_radius: 10,
|
|
};
|
|
|
|
let (spi, cs) = setup_st7789_spi(scl, sda, cs, spi)
|
|
.map_err(|err| alloc::format!("Failed to build SPI config: {err}"))?;
|
|
|
|
let output_config = esp_hal::gpio::OutputConfig::default();
|
|
let dc_output = esp_hal::gpio::Output::new(dc, esp_hal::gpio::Level::Low, output_config);
|
|
let rst_output = esp_hal::gpio::Output::new(res, esp_hal::gpio::Level::High, output_config);
|
|
let _ = esp_hal::gpio::Output::new(bl, esp_hal::gpio::Level::High, output_config); // enable backlight
|
|
|
|
let mut display_delay = esp_hal::delay::Delay::new();
|
|
|
|
let spi_device = embedded_hal_bus::spi::ExclusiveDevice::new_no_delay(spi, cs)
|
|
.map_err(|err| alloc::format!("Failed to initialize SPI device: {err}"))?;
|
|
|
|
// Note: height and width are swapped b/c of 90° display rotation
|
|
let display = mipidsi::Builder::new(
|
|
mipidsi::models::ST7789,
|
|
mipidsi::interface::SpiInterface::new(spi_device, dc_output, spi_buffer),
|
|
)
|
|
.display_size(size.height + DISPLAY_HIDDEN_X, size.width)
|
|
.reset_pin(rst_output)
|
|
.invert_colors(mipidsi::options::ColorInversion::Inverted)
|
|
.orientation(mipidsi::options::Orientation::new().rotate(mipidsi::options::Rotation::Deg90))
|
|
.init(&mut display_delay)
|
|
.map_err(|err| alloc::format!("Failed to initialize display: {err:?}"))?;
|
|
|
|
Ok((display, size))
|
|
}
|
|
|
|
/// Creates an [`mipidsi::Display`], width, height tuple for an ST7789 240*280px screen in portrait orientation
|
|
///
|
|
/// # Errors
|
|
/// Human-readable fatal errors
|
|
#[allow(unused, clippy::too_many_arguments, clippy::type_complexity)]
|
|
pub fn make_large_display<IoScl, IoSda, IoRes, IoDc, IoCs, IoBl, Spi>(
|
|
scl: IoScl,
|
|
sda: IoSda,
|
|
res: IoRes,
|
|
dc: IoDc,
|
|
cs: IoCs,
|
|
bl: IoBl,
|
|
spi: Spi,
|
|
spi_buffer: &mut [u8],
|
|
) -> Result<
|
|
(
|
|
mipidsi::Display<
|
|
mipidsi::interface::SpiInterface<
|
|
'_,
|
|
embedded_hal_bus::spi::ExclusiveDevice<
|
|
esp_hal::spi::master::Spi<'_, esp_hal::Blocking>,
|
|
esp_hal::gpio::Output<'_>,
|
|
embedded_hal_bus::spi::NoDelay,
|
|
>,
|
|
esp_hal::gpio::Output<'_>,
|
|
>,
|
|
mipidsi::models::ST7789,
|
|
esp_hal::gpio::Output<'_>,
|
|
>,
|
|
DisplaySize,
|
|
),
|
|
alloc::string::String,
|
|
>
|
|
where
|
|
IoScl: esp_hal::gpio::interconnect::PeripheralOutput<'static>,
|
|
IoSda: esp_hal::gpio::interconnect::PeripheralOutput<'static>,
|
|
IoRes:
|
|
esp_hal::gpio::interconnect::PeripheralOutput<'static> + esp_hal::gpio::OutputPin + 'static,
|
|
IoDc:
|
|
esp_hal::gpio::interconnect::PeripheralOutput<'static> + esp_hal::gpio::OutputPin + 'static,
|
|
IoCs:
|
|
esp_hal::gpio::interconnect::PeripheralOutput<'static> + esp_hal::gpio::OutputPin + 'static,
|
|
IoBl:
|
|
esp_hal::gpio::interconnect::PeripheralOutput<'static> + esp_hal::gpio::OutputPin + 'static,
|
|
Spi: esp_hal::spi::master::Instance + 'static,
|
|
{
|
|
const DISPLAY_HIDDEN_Y: u16 = 20; // somehow this display has 20 px of invisible space on the top (in native orientation)
|
|
|
|
let size = DisplaySize {
|
|
width: 240,
|
|
height: 280 + DISPLAY_HIDDEN_Y,
|
|
offset_top: DISPLAY_HIDDEN_Y.into(),
|
|
corner_radius: 30,
|
|
};
|
|
|
|
let (spi, cs) = setup_st7789_spi(scl, sda, cs, spi)
|
|
.map_err(|err| alloc::format!("Failed to build SPI config: {err}"))?;
|
|
|
|
let output_config = esp_hal::gpio::OutputConfig::default();
|
|
let dc_output = esp_hal::gpio::Output::new(dc, esp_hal::gpio::Level::Low, output_config);
|
|
let rst_output = esp_hal::gpio::Output::new(res, esp_hal::gpio::Level::High, output_config);
|
|
let _ = esp_hal::gpio::Output::new(bl, esp_hal::gpio::Level::High, output_config); // enable backlight
|
|
|
|
let mut display_delay = esp_hal::delay::Delay::new();
|
|
|
|
let spi_device = embedded_hal_bus::spi::ExclusiveDevice::new_no_delay(spi, cs)
|
|
.map_err(|err| alloc::format!("Failed to initialize SPI device: {err}"))?;
|
|
|
|
let display = mipidsi::Builder::new(
|
|
mipidsi::models::ST7789,
|
|
mipidsi::interface::SpiInterface::new(spi_device, dc_output, spi_buffer),
|
|
)
|
|
.display_size(size.width, size.height + DISPLAY_HIDDEN_Y)
|
|
.reset_pin(rst_output)
|
|
.invert_colors(mipidsi::options::ColorInversion::Inverted)
|
|
.orientation(mipidsi::options::Orientation::new().rotate(mipidsi::options::Rotation::Deg0))
|
|
.init(&mut display_delay)
|
|
.map_err(|err| alloc::format!("Failed to initialize display: {err:?}"))?;
|
|
|
|
Ok((display, size))
|
|
}
|
|
|
|
/// Builds the SPI interface for the ST7789 display
|
|
///
|
|
/// Returns (spi master, chip-select) tuple
|
|
///
|
|
/// # Errors
|
|
/// Returns SPI config error when SPI setup failed
|
|
fn setup_st7789_spi<IoScl, IoSda, IoCs, Spi>(
|
|
scl: IoScl,
|
|
sda: IoSda,
|
|
cs: IoCs,
|
|
spi: Spi,
|
|
) -> Result<
|
|
(
|
|
esp_hal::spi::master::Spi<'static, esp_hal::Blocking>,
|
|
esp_hal::gpio::Output<'static>,
|
|
),
|
|
esp_hal::spi::master::ConfigError,
|
|
>
|
|
where
|
|
IoScl: esp_hal::gpio::interconnect::PeripheralOutput<'static>,
|
|
IoSda: esp_hal::gpio::interconnect::PeripheralOutput<'static>,
|
|
IoCs:
|
|
esp_hal::gpio::interconnect::PeripheralOutput<'static> + esp_hal::gpio::OutputPin + 'static,
|
|
Spi: esp_hal::spi::master::Instance + 'static,
|
|
{
|
|
let spi_config = esp_hal::spi::master::Config::default()
|
|
.with_frequency(esp_hal::time::Rate::from_mhz(40))
|
|
.with_mode(esp_hal::spi::Mode::_0);
|
|
let spi: esp_hal::spi::master::Spi<'_, esp_hal::Blocking> =
|
|
esp_hal::spi::master::Spi::new(spi, spi_config)?
|
|
.with_sck(scl)
|
|
.with_mosi(sda);
|
|
|
|
let cs_output = esp_hal::gpio::Output::new(
|
|
cs,
|
|
esp_hal::gpio::Level::High,
|
|
esp_hal::gpio::OutputConfig::default(),
|
|
);
|
|
|
|
Ok((spi, cs_output))
|
|
}
|