Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Панель (Dashboard)

В этой главе мы напишем основную логику. Идея в том, чтобы обновлять панель через определённые промежутки времени, скажем каждые 10 минут. Для этого мы будем получать свежие данные о погоде, вызывая функцию access_website из модуля weather, и затем обновлять каждую часть панели новой информацией.

Псевдонимы типов

Создадим псевдонимы типов для SPI-устройства и e-paper дисплея, чтобы код было легче читать и использовать.

#![allow(unused)]
fn main() {
type SpiDevice = ExclusiveDevice<Spi<'static, esp_hal::Blocking>, Output<'static>, Delay>;
type EPD = Epd1in54<SpiDevice, Input<'static>, Output<'static>, Output<'static>, Delay>;
}

Структура Dashboard

Определим структуру Dashboard, которая принимает Wi-Fi-стек, драйвер e-paper и SPI-устройство. Функция new инициализирует структуру и задаёт дисплей e-paper по умолчанию.

#![allow(unused)]
fn main() {
pub struct Dashboard {
    display: Display1in54,
    wifi: Stack<'static>,
    epd: EPD,
    spi_dev: SpiDevice,
}

impl Dashboard {
    pub fn new(wifi: Stack<'static>, epd: EPD, spi_dev: SpiDevice) -> Self {
        Self {
            display: Display1in54::default(),
            wifi,
            epd,
            spi_dev,
        }
    }
}
}

Примечание: следующие функции, которые принимают self первым параметром, тоже должны находиться внутри блока impl.

Запуск панели

Это стартовая функция, которую мы вызовем далее в файле main.rs. Здесь же создаём экземпляр WeatherApi.

Я повернул дисплей на 90 градусов. Технически в этом нет необходимости, так как наш дисплей имеет размер 200x200 пикселей. Но если вы используете прямоугольный дисплей, поворот имеет больше смысла.

В цикле каждые 10 минут мы вызываем функцию refresh, которая получает свежие данные и обновляет дисплей.

#![allow(unused)]
fn main() {
pub async fn start(&mut self, tls_seed: u64) {
    self.display.set_rotation(DisplayRotation::Rotate90);

    let api = WeatherApi::new(self.wifi, tls_seed);
    loop {
        self.refresh(&api).await;

        Timer::after(Duration::from_secs(60 * 10)).await;
    }
}
}

Обновление дисплея свежими данными о погоде

Сначала получаем свежие данные о погоде, вызывая access_website у WeatherApi. Затем выводим e-paper дисплей из режима сна, так как в конце мы снова его усыпим. После этого очищаем предыдущий кадр и заливаем дисплей белым цветом.

Далее выводим обновлённую информацию о погоде: сначала дату, затем иконку погоды и температуру. После этого добавляем влажность, скорость ветра и, наконец, подпись внизу (небольшой текст «implRust»). Когда всё нарисовано, обновляем дисплей новым кадром, ждём 5 секунд и переводим дисплей в спящий режим.

#![allow(unused)]
fn main() {
pub async fn refresh(&mut self, api: &WeatherApi) {
    info!("Getting weather data");
    let weather_data = api.access_website().await;
    info!("Got weather data");

    self.epd.wake_up(&mut self.spi_dev, &mut Delay).unwrap();
    Timer::after(Duration::from_secs(5)).await;

    // Очищаем имеющееся изображение
    self.epd.clear_frame(&mut self.spi_dev, &mut Delay).unwrap();
    self.display.clear(Color::White).unwrap();
    self.epd
        .update_and_display_frame(&mut self.spi_dev, self.display.buffer(), &mut Delay)
        .unwrap();
    Timer::after(Duration::from_secs(5)).await;

    self.draw_date(weather_data.dt);

    self.draw_icon(weather_data.weather[0].id.icon(), Point::new(20, 50));
    self.draw_temperature(weather_data.main.temp, Point::new(20 + 70, 60));

    self.draw_humidity(weather_data.main.humidity);
    self.draw_wind(weather_data.wind.speed);

    self.draw_signature();

    self.epd
        .update_and_display_frame(&mut self.spi_dev, self.display.buffer(), &mut Delay)
        .unwrap();
    Timer::after(Duration::from_secs(5)).await;

    self.epd.sleep(&mut self.spi_dev, &mut Delay).unwrap();
}
}

Вспомогательная функция получения иконки

Эта простая функция принимает имя иконки и позицию, в которой её нужно нарисовать. Она сопоставляет имя иконки с соответствующими байтами изображения, затем с помощью tinybmp и embedded_graphics превращает байты в изображение и выводит его на дисплей.

#![allow(unused)]
fn main() {
fn draw_icon(&mut self, icon_name: &'static str, pos: Point) {
    let img_bytes = self.get_icon(icon_name).unwrap();

    let bmp = Bmp::from_slice(img_bytes).unwrap();
    let image = Image::new(&bmp, pos);
    image.draw(&mut self.display).unwrap();
}

pub fn get_icon(&self, icon_name: &'static str) -> Option<&'static [u8]> {
    ICONS
        .iter()
        .find(|(name, _)| *name == icon_name)
        .map(|(_, img_bytes)| *img_bytes)
}
}

Вывод температуры

Выведем температуру на экран, добавим к ней суффикс «°C» и нарисуем под ней горизонтальную линию.

#![allow(unused)]
fn main() {
fn draw_temperature(&mut self, temperature: f64, pos: Point) {
    let text_style = MonoTextStyle::new(&PROFONT_24_POINT, Color::Black);

    info!("Drawing temperature");
    let mut text: String<20> = String::new();
    write!(&mut text, "{}°C", temperature).unwrap();

    Text::with_baseline(&text, pos, text_style, Baseline::Top)
        .draw(&mut self.display)
        .unwrap();

    Line::new(Point::new(0, 105), Point::new(200, 105))
        .into_styled(PrimitiveStyle::with_stroke(Color::Black, 5))
        .draw(&mut self.display)
        .unwrap();
}
}

Вывод влажности

Покажем на экране иконку влажности, затем выведем рядом значение влажности и вертикальную линию для разделения.

#![allow(unused)]
fn main() {
fn draw_humidity(&mut self, humidity: i32) {
    self.draw_icon("humidity_percentage.bmp", Point::new(5, 110));

    let text_style = MonoTextStyle::new(&PROFONT_18_POINT, Color::Black);

    let mut text: String<10> = String::new();
    write!(&mut text, "{}", humidity).unwrap();

    Text::with_baseline(&text, Point::new(5 + 50, 120), text_style, Baseline::Top)
        .draw(&mut self.display)
        .unwrap();

    Line::new(Point::new(5 + 85, 120), Point::new(5 + 85, 120 + 30))
        .into_styled(PrimitiveStyle::with_stroke(Color::Black, 5))
        .draw(&mut self.display)
        .unwrap();
}
}

Вывод ветра

Выведем скорость ветра на экран: сначала нарисуем иконку ветра, затем значение скорости и единицу измерения m/s (м/с).

#![allow(unused)]
fn main() {
fn draw_wind(&mut self, wind_speed: f64) {
    self.draw_icon("air.bmp", Point::new(100, 110));

    let text_style = MonoTextStyle::new(&PROFONT_18_POINT, Color::Black);

    let mut text: String<10> = String::new();
    write!(&mut text, "{}", wind_speed).unwrap();

    Text::with_baseline(&text, Point::new(100 + 50, 120), text_style, Baseline::Top)
        .draw(&mut self.display)
        .unwrap();

    let text_style = MonoTextStyleBuilder::new()
        .font(&FONT_10X20)
        .text_color(Color::Black)
        .build();

    Text::with_baseline("m/s", Point::new(100 + 50, 140), text_style, Baseline::Top)
        .draw(&mut self.display)
        .unwrap();
}
}

Вывод даты

Выведем текущую дату на экран, отформатировав её с днём, месяцем и годом. Затем выведем текст в заданной позиции и нарисуем под ним горизонтальную линию для разделения.

#![allow(unused)]
fn main() {
fn draw_date(&mut self, dt: DateTime<Utc>) {
    let text_style = MonoTextStyle::new(&PROFONT_24_POINT, Color::Black);

    let mut text: String<24> = String::new();
    write!(
        &mut text,
        "{} {} {}",
        dt.day(),
        month_name(dt.month()),
        dt.year()
    )
    .unwrap();

    Text::with_baseline(&text, Point::new(20, 10), text_style, Baseline::Top)
        .draw(&mut self.display)
        .unwrap();

    Line::new(Point::new(0, 45), Point::new(200, 45))
        .into_styled(PrimitiveStyle::with_stroke(Color::Black, 5))
        .draw(&mut self.display)
        .unwrap();
}
}

Вывод подписи

Это не связано с погодой: мы просто выводим «implRust» для удовольствия. Вычисляем центральную позицию, рисуем в центре чёрный прямоугольник и помещаем текст ровно посередине.

#![allow(unused)]
fn main() {
fn draw_signature(&mut self) {
    let display_width = epd1in54_v2::WIDTH as i32;
    let rect_padding = 20;

    let rect_width = display_width - 2 * rect_padding;
    let rect_height = 40;
    let rect_x = rect_padding;
    let rect_y = 170;

    let style = PrimitiveStyleBuilder::new()
        .stroke_color(Color::Black)
        .stroke_width(3)
        .fill_color(Color::Black)
        .build();

    Rectangle::new(
        Point::new(rect_x, rect_y),
        Size::new(rect_width as u32, rect_height as u32),
    )
    .into_styled(style)
    .draw(&mut self.display)
    .unwrap();

    let text = "implRust";
    let text_style = MonoTextStyle::new(&PROFONT_24_POINT, Color::White);

    let char_width = PROFONT_24_POINT.character_size.width as i32;
    let text_width = text.len() as i32 * char_width;
    let text_x = rect_x + (rect_width - text_width) / 2;

    Text::with_baseline(
        text,
        Point::new(text_x as i32, rect_y),
        text_style,
        Baseline::Top,
    )
    .draw(&mut self.display)
    .unwrap();
}
}

Вспомогательная функция для названия месяца

Мы создали вспомогательную функцию, которая возвращает сокращённое название месяца по его номеру.

#![allow(unused)]
fn main() {
fn month_name(month: u32) -> &'static str {
    match month {
        1 => "Jan",
        2 => "Feb",
        3 => "Mar",
        4 => "Apr",
        5 => "May",
        6 => "Jun",
        7 => "Jul",
        8 => "Aug",
        9 => "Sep",
        10 => "Oct",
        11 => "Nov",
        12 => "Dec",
        _ => "Err",
    }
}
}