WIP: Adapt to new screen
TODO: Make better use of the bigger screen TODO: Show main loop HB different from GNSS updates?
This commit is contained in:
parent
666499c240
commit
52f1b65e30
3 changed files with 133 additions and 35 deletions
|
|
@ -8,7 +8,7 @@ The code is currently tailored to:
|
||||||
- U-Blox M10Q GNSS (connected on GPIO12 (RX), GPIO11 (TX), and GPIO25 (reset))
|
- U-Blox M10Q GNSS (connected on GPIO12 (RX), GPIO11 (TX), and GPIO25 (reset))
|
||||||
or Seeed XIAO L67K (connected on GPIO12 (RX), GPIO11 (TX), GPIO1 (wakeup) and GPIO25 (reset))
|
or Seeed XIAO L67K (connected on GPIO12 (RX), GPIO11 (TX), GPIO1 (wakeup) and GPIO25 (reset))
|
||||||
only available with `gnss_ubx` or `gnss_l67k` feature (which is included in `spat` feature)
|
only available with `gnss_ubx` or `gnss_l67k` feature (which is included in `spat` feature)
|
||||||
- Some ST7789 172*320px screen (connected on GPIO8 (SCL/ SCK), GPIO10 (SDA/ MOSI), GPIO7 (reset), GPIO23 (DC), GPIO24 (CS), TODO GPIO0 (BL))
|
- Some ST7789 240*280px screen (connected on GPIO8 (SCL/ SCK), GPIO10 (SDA/ MOSI), GPIO7 (reset), GPIO23 (DC), GPIO24 (CS), GPIO0 (BL))
|
||||||
only available with `screen` feature (which is included in `spat` feature)
|
only available with `screen` feature (which is included in `spat` feature)
|
||||||
|
|
||||||
## Features
|
## Features
|
||||||
|
|
|
||||||
|
|
@ -127,8 +127,8 @@ async fn main(spawner: Spawner) -> ! {
|
||||||
let mut spi_buffer = [0_u8; 512];
|
let mut spi_buffer = [0_u8; 512];
|
||||||
#[cfg(feature = "screen")]
|
#[cfg(feature = "screen")]
|
||||||
let mut screen = {
|
let mut screen = {
|
||||||
// 172*320px in landscape orientation
|
// 240*280px in portrait orientation
|
||||||
let (display, width, height) = screen::make_small_display(
|
let (display, size) = screen::make_large_display(
|
||||||
peripherals.GPIO8,
|
peripherals.GPIO8,
|
||||||
peripherals.GPIO10,
|
peripherals.GPIO10,
|
||||||
peripherals.GPIO7,
|
peripherals.GPIO7,
|
||||||
|
|
@ -140,7 +140,7 @@ async fn main(spawner: Spawner) -> ! {
|
||||||
)
|
)
|
||||||
.expect("Fatal error building screen");
|
.expect("Fatal error building screen");
|
||||||
|
|
||||||
let mut screen = screen::Handler::new(display, width, height);
|
let mut screen = screen::Handler::new(display, size);
|
||||||
|
|
||||||
let _ = screen
|
let _ = screen
|
||||||
.update(screen::ScreenState::Initial)
|
.update(screen::ScreenState::Initial)
|
||||||
|
|
|
||||||
162
src/screen.rs
162
src/screen.rs
|
|
@ -5,13 +5,19 @@ use embedded_graphics::{geometry, mono_font, pixelcolor, primitives, text};
|
||||||
|
|
||||||
use crate::applogic;
|
use crate::applogic;
|
||||||
|
|
||||||
|
pub struct DisplaySize {
|
||||||
|
width: u16,
|
||||||
|
height: u16,
|
||||||
|
offset_top: i32,
|
||||||
|
corner_radius: i32,
|
||||||
|
}
|
||||||
|
|
||||||
pub struct Handler<D>
|
pub struct Handler<D>
|
||||||
where
|
where
|
||||||
D: DrawTarget<Color = pixelcolor::Rgb565>,
|
D: DrawTarget<Color = pixelcolor::Rgb565>,
|
||||||
{
|
{
|
||||||
target: D,
|
target: D,
|
||||||
width: u16,
|
size: DisplaySize,
|
||||||
height: u16,
|
|
||||||
|
|
||||||
// internal state
|
// internal state
|
||||||
prev_state: ScreenState,
|
prev_state: ScreenState,
|
||||||
|
|
@ -82,11 +88,10 @@ where
|
||||||
const SMALL_TEXT_STYLE: mono_font::MonoTextStyle<'static, pixelcolor::Rgb565> =
|
const SMALL_TEXT_STYLE: mono_font::MonoTextStyle<'static, pixelcolor::Rgb565> =
|
||||||
mono_font::MonoTextStyle::new(&profont::PROFONT_14_POINT, Self::COLOR_FG);
|
mono_font::MonoTextStyle::new(&profont::PROFONT_14_POINT, Self::COLOR_FG);
|
||||||
|
|
||||||
pub fn new(target: D, width: u16, height: u16) -> Self {
|
pub fn new(target: D, display_size: DisplaySize) -> Self {
|
||||||
Self {
|
Self {
|
||||||
target,
|
target,
|
||||||
width,
|
size: display_size,
|
||||||
height,
|
|
||||||
prev_state: ScreenState::Uninitialized,
|
prev_state: ScreenState::Uninitialized,
|
||||||
hb_state: false,
|
hb_state: false,
|
||||||
}
|
}
|
||||||
|
|
@ -115,18 +120,19 @@ where
|
||||||
// Update indicator/ heartbeat
|
// Update indicator/ heartbeat
|
||||||
{
|
{
|
||||||
let update_ind_dia = 10;
|
let update_ind_dia = 10;
|
||||||
let circle_x_pos = i32::from(self.width) - update_ind_dia - 10;
|
let circle_x_pos =
|
||||||
|
i32::from(self.size.width) - update_ind_dia - self.size.corner_radius;
|
||||||
|
|
||||||
text::Text::with_alignment(
|
text::Text::with_alignment(
|
||||||
"HB",
|
"HB",
|
||||||
geometry::Point::new(circle_x_pos - 5, 10),
|
geometry::Point::new(circle_x_pos - 5, 10 + self.size.offset_top),
|
||||||
Self::SMALL_TEXT_STYLE,
|
Self::SMALL_TEXT_STYLE,
|
||||||
text::Alignment::Right,
|
text::Alignment::Right,
|
||||||
)
|
)
|
||||||
.draw(&mut self.target)?;
|
.draw(&mut self.target)?;
|
||||||
|
|
||||||
primitives::Circle::new(
|
primitives::Circle::new(
|
||||||
geometry::Point::new(circle_x_pos, 2),
|
geometry::Point::new(circle_x_pos, 2 + self.size.offset_top),
|
||||||
update_ind_dia.cast_unsigned(),
|
update_ind_dia.cast_unsigned(),
|
||||||
)
|
)
|
||||||
.into_styled(if self.hb_state {
|
.into_styled(if self.hb_state {
|
||||||
|
|
@ -149,7 +155,7 @@ where
|
||||||
{
|
{
|
||||||
primitives::Rectangle::new(
|
primitives::Rectangle::new(
|
||||||
geometry::Point::new(0, 0),
|
geometry::Point::new(0, 0),
|
||||||
geometry::Size::new(self.width.into(), self.height.into()),
|
geometry::Size::new(self.size.width.into(), self.size.height.into()),
|
||||||
)
|
)
|
||||||
.into_styled(Self::BK_STYLE)
|
.into_styled(Self::BK_STYLE)
|
||||||
.draw(&mut self.target)?;
|
.draw(&mut self.target)?;
|
||||||
|
|
@ -171,7 +177,7 @@ where
|
||||||
|
|
||||||
text::Text::with_alignment(
|
text::Text::with_alignment(
|
||||||
msg,
|
msg,
|
||||||
geometry::Point::new(i32::from(self.width) / 2, 40),
|
geometry::Point::new(i32::from(self.size.width) / 2, 40 + self.size.offset_top),
|
||||||
Self::DEFAULT_TEXT_STYLE,
|
Self::DEFAULT_TEXT_STYLE,
|
||||||
text::Alignment::Center,
|
text::Alignment::Center,
|
||||||
)
|
)
|
||||||
|
|
@ -186,8 +192,8 @@ where
|
||||||
self.clear()?;
|
self.clear()?;
|
||||||
|
|
||||||
text::Text::with_alignment(
|
text::Text::with_alignment(
|
||||||
"No upcoming SPAT",
|
"Searching...",
|
||||||
geometry::Point::new(i32::from(self.width) / 2, 130),
|
geometry::Point::new(i32::from(self.size.width) / 2, 130 + self.size.offset_top),
|
||||||
Self::DEFAULT_TEXT_STYLE,
|
Self::DEFAULT_TEXT_STYLE,
|
||||||
text::Alignment::Center,
|
text::Alignment::Center,
|
||||||
)
|
)
|
||||||
|
|
@ -209,7 +215,7 @@ where
|
||||||
|
|
||||||
text::Text::with_alignment(
|
text::Text::with_alignment(
|
||||||
&text,
|
&text,
|
||||||
geometry::Point::new(i32::from(self.width) / 2, 130),
|
geometry::Point::new(i32::from(self.size.width) / 2, 130 + self.size.offset_top),
|
||||||
Self::DEFAULT_TEXT_STYLE,
|
Self::DEFAULT_TEXT_STYLE,
|
||||||
text::Alignment::Center,
|
text::Alignment::Center,
|
||||||
)
|
)
|
||||||
|
|
@ -235,23 +241,28 @@ where
|
||||||
// only clear maneuver and timing area otherwise
|
// only clear maneuver and timing area otherwise
|
||||||
|
|
||||||
primitives::Rectangle::new(
|
primitives::Rectangle::new(
|
||||||
geometry::Point::new(0, 105),
|
geometry::Point::new(0, 105 + self.size.offset_top),
|
||||||
geometry::Size::new(self.width.into(), 67),
|
geometry::Size::new(self.size.width.into(), 67),
|
||||||
)
|
)
|
||||||
.into_styled(Self::BK_STYLE)
|
.into_styled(Self::BK_STYLE)
|
||||||
.draw(&mut self.target)?;
|
.draw(&mut self.target)?;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// do nothing more when no signals
|
||||||
|
if num_signals == 0 {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
// draw intersection ID
|
// draw intersection ID
|
||||||
text::Text::new(
|
text::Text::new(
|
||||||
&alloc::format!("#{}", data.intersection_id),
|
&alloc::format!("#{}", data.intersection_id),
|
||||||
geometry::Point::new(15, 10),
|
geometry::Point::new(self.size.corner_radius + 5, 10 + self.size.offset_top),
|
||||||
Self::SMALL_TEXT_STYLE,
|
Self::SMALL_TEXT_STYLE,
|
||||||
)
|
)
|
||||||
.draw(&mut self.target)?;
|
.draw(&mut self.target)?;
|
||||||
|
|
||||||
#[allow(clippy::cast_possible_truncation, clippy::cast_possible_wrap)]
|
#[allow(clippy::cast_possible_truncation, clippy::cast_possible_wrap)]
|
||||||
let sig_width = i32::from(self.width) / (num_signals as i32);
|
let sig_width = i32::from(self.size.width) / (num_signals as i32);
|
||||||
|
|
||||||
for (idx, sig) in data.signal_groups.iter().enumerate() {
|
for (idx, sig) in data.signal_groups.iter().enumerate() {
|
||||||
#[allow(clippy::cast_possible_truncation, clippy::cast_possible_wrap)]
|
#[allow(clippy::cast_possible_truncation, clippy::cast_possible_wrap)]
|
||||||
|
|
@ -269,7 +280,7 @@ where
|
||||||
applogic::SignalPhase::RedYellow | applogic::SignalPhase::Yellow => Self::YE_STYLE,
|
applogic::SignalPhase::RedYellow | applogic::SignalPhase::Yellow => Self::YE_STYLE,
|
||||||
};
|
};
|
||||||
primitives::Circle::new(
|
primitives::Circle::new(
|
||||||
geometry::Point::new(offset + circle_margin, 15),
|
geometry::Point::new(offset + circle_margin, 15 + self.size.offset_top),
|
||||||
circle_dia.cast_unsigned(),
|
circle_dia.cast_unsigned(),
|
||||||
)
|
)
|
||||||
.into_styled(style)
|
.into_styled(style)
|
||||||
|
|
@ -280,7 +291,7 @@ where
|
||||||
// draw maneuver
|
// draw maneuver
|
||||||
text::Text::with_alignment(
|
text::Text::with_alignment(
|
||||||
&maneuver_to_str(sig.maneuver),
|
&maneuver_to_str(sig.maneuver),
|
||||||
geometry::Point::new(centerline, 130),
|
geometry::Point::new(centerline, 130 + self.size.offset_top),
|
||||||
Self::DEFAULT_TEXT_STYLE,
|
Self::DEFAULT_TEXT_STYLE,
|
||||||
text::Alignment::Center,
|
text::Alignment::Center,
|
||||||
)
|
)
|
||||||
|
|
@ -290,7 +301,7 @@ where
|
||||||
if let Some(end_sec) = sig.end_sec {
|
if let Some(end_sec) = sig.end_sec {
|
||||||
text::Text::with_alignment(
|
text::Text::with_alignment(
|
||||||
&alloc::format!("{end_sec}s"),
|
&alloc::format!("{end_sec}s"),
|
||||||
geometry::Point::new(centerline, 160),
|
geometry::Point::new(centerline, 160 + self.size.offset_top),
|
||||||
Self::DEFAULT_TEXT_STYLE,
|
Self::DEFAULT_TEXT_STYLE,
|
||||||
text::Alignment::Center,
|
text::Alignment::Center,
|
||||||
)
|
)
|
||||||
|
|
@ -307,7 +318,7 @@ where
|
||||||
|
|
||||||
self.clear()?;
|
self.clear()?;
|
||||||
|
|
||||||
let sig_width = i32::from(self.width) / 3;
|
let sig_width = i32::from(self.size.width) / 3;
|
||||||
let circle_dia = 50;
|
let circle_dia = 50;
|
||||||
let circle_margin = (sig_width - circle_dia) / 2;
|
let circle_margin = (sig_width - circle_dia) / 2;
|
||||||
|
|
||||||
|
|
@ -319,7 +330,7 @@ where
|
||||||
let offset = (idx as i32) * sig_width;
|
let offset = (idx as i32) * sig_width;
|
||||||
|
|
||||||
primitives::Circle::new(
|
primitives::Circle::new(
|
||||||
geometry::Point::new(offset + circle_margin, 15),
|
geometry::Point::new(offset + circle_margin, 15 + self.size.offset_top),
|
||||||
circle_dia.cast_unsigned(),
|
circle_dia.cast_unsigned(),
|
||||||
)
|
)
|
||||||
.into_styled(*style)
|
.into_styled(*style)
|
||||||
|
|
@ -327,10 +338,10 @@ where
|
||||||
}
|
}
|
||||||
|
|
||||||
// draw welcome text
|
// draw welcome text
|
||||||
let centerline = i32::from(self.width) / 2;
|
let centerline = i32::from(self.size.width) / 2;
|
||||||
text::Text::with_alignment(
|
text::Text::with_alignment(
|
||||||
"C-ITS Signal Phase Display\nWaiting for GNSS fix...",
|
"C-ITS Signal Phase\nWaiting for GNSS...",
|
||||||
geometry::Point::new(centerline, 130),
|
geometry::Point::new(centerline, 130 + self.size.offset_top),
|
||||||
text_style,
|
text_style,
|
||||||
text::Alignment::Center,
|
text::Alignment::Center,
|
||||||
)
|
)
|
||||||
|
|
@ -378,8 +389,7 @@ pub fn make_small_display<IoScl, IoSda, IoRes, IoDc, IoCs, IoBl, Spi>(
|
||||||
mipidsi::models::ST7789,
|
mipidsi::models::ST7789,
|
||||||
esp_hal::gpio::Output<'_>,
|
esp_hal::gpio::Output<'_>,
|
||||||
>,
|
>,
|
||||||
u16,
|
DisplaySize,
|
||||||
u16,
|
|
||||||
),
|
),
|
||||||
alloc::string::String,
|
alloc::string::String,
|
||||||
>
|
>
|
||||||
|
|
@ -396,10 +406,98 @@ where
|
||||||
esp_hal::gpio::interconnect::PeripheralOutput<'static> + esp_hal::gpio::OutputPin + 'static,
|
esp_hal::gpio::interconnect::PeripheralOutput<'static> + esp_hal::gpio::OutputPin + 'static,
|
||||||
Spi: esp_hal::spi::master::Instance + 'static,
|
Spi: esp_hal::spi::master::Instance + 'static,
|
||||||
{
|
{
|
||||||
const DISPLAY_WIDTH: u16 = 320;
|
|
||||||
const DISPLAY_HEIGHT: u16 = 172;
|
|
||||||
const DISPLAY_HIDDEN_X: u16 = 34; // somehow this display has 34 px of invisible space on the left (in native orientation)
|
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(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)
|
let (spi, cs) = setup_st7789_spi(scl, sda, cs, spi)
|
||||||
.map_err(|err| alloc::format!("Failed to build SPI config: {err}"))?;
|
.map_err(|err| alloc::format!("Failed to build SPI config: {err}"))?;
|
||||||
|
|
||||||
|
|
@ -417,14 +515,14 @@ where
|
||||||
mipidsi::models::ST7789,
|
mipidsi::models::ST7789,
|
||||||
mipidsi::interface::SpiInterface::new(spi_device, dc_output, spi_buffer),
|
mipidsi::interface::SpiInterface::new(spi_device, dc_output, spi_buffer),
|
||||||
)
|
)
|
||||||
.display_size(DISPLAY_HEIGHT + DISPLAY_HIDDEN_X, DISPLAY_WIDTH)
|
.display_size(size.width, size.height + DISPLAY_HIDDEN_Y)
|
||||||
.reset_pin(rst_output)
|
.reset_pin(rst_output)
|
||||||
.invert_colors(mipidsi::options::ColorInversion::Inverted)
|
.invert_colors(mipidsi::options::ColorInversion::Inverted)
|
||||||
.orientation(mipidsi::options::Orientation::new().rotate(mipidsi::options::Rotation::Deg90))
|
.orientation(mipidsi::options::Orientation::new().rotate(mipidsi::options::Rotation::Deg0))
|
||||||
.init(&mut display_delay)
|
.init(&mut display_delay)
|
||||||
.map_err(|err| alloc::format!("Failed to initialize display: {err:?}"))?;
|
.map_err(|err| alloc::format!("Failed to initialize display: {err:?}"))?;
|
||||||
|
|
||||||
Ok((display, DISPLAY_WIDTH, DISPLAY_HEIGHT))
|
Ok((display, size))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Builds the SPI interface for the ST7789 display
|
/// Builds the SPI interface for the ST7789 display
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue