Add pollen indicator
This commit is contained in:
parent
0381b8d14c
commit
426d5b4453
9 changed files with 463 additions and 157 deletions
12
Cargo.toml
12
Cargo.toml
|
@ -8,10 +8,10 @@ authors = ["Felix Suchert <dev@felixsuchert.de>"]
|
|||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
embedded-graphics = "0.7.1"
|
||||
embedded-plots = "0.2.0"
|
||||
tinybmp = "0.3.3"
|
||||
profont = "0.5.0"
|
||||
embedded-graphics = "0.8"
|
||||
embedded-plots = "0.2"
|
||||
tinybmp = "0.5"
|
||||
profont = "0.6"
|
||||
ssd1675 = { git = "https://github.com/Feliix42/ssd1675" }
|
||||
#ssd1675 = { path = "../ssd1675" }
|
||||
|
||||
|
@ -21,9 +21,11 @@ serde_json = "1.0"
|
|||
|
||||
# for retrieving weather data from the API
|
||||
ureq = { version = "2.6", features = ["json"] }
|
||||
# for parsing timestamps from APIs
|
||||
time = { version = "0.3", features = ["macros", "parsing", "local-offset"] }
|
||||
|
||||
linux-embedded-hal = { version = "0.3.2", optional = true }
|
||||
embedded-graphics-simulator = { version = "0.3.0", optional = true }
|
||||
embedded-graphics-simulator = { version = "0.5", optional = true }
|
||||
|
||||
|
||||
[features]
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
use embedded_graphics::prelude::*;
|
||||
use embedded_graphics_simulator::{OutputSettings, SimulatorDisplay, Window};
|
||||
use ssd1675::Color;
|
||||
use inky_ticker::ApplicationState;
|
||||
|
||||
fn main() -> Result<(), core::convert::Infallible> {
|
||||
println!("Initialize Display");
|
||||
|
@ -9,18 +10,25 @@ fn main() -> Result<(), core::convert::Infallible> {
|
|||
println!("Creating Window");
|
||||
let mut window = Window::new("Hello World", &OutputSettings::default());
|
||||
|
||||
//loop {
|
||||
//println!("Populate display...");
|
||||
//inky_ticker::populate(&mut display);
|
||||
let mut state = ApplicationState::default();
|
||||
|
||||
//println!("Updating window");
|
||||
//window.update(&display);
|
||||
// switch between states every few minutes
|
||||
let minute = std::time::Duration::from_secs(60);
|
||||
|
||||
//std::thread::sleep(std::time::Duration::from_secs(1));
|
||||
//}
|
||||
loop {
|
||||
display.clear(Color::White).expect("failed to clear screen");
|
||||
|
||||
inky_ticker::populate(&mut display);
|
||||
window.show_static(&display);
|
||||
println!("Populating screen");
|
||||
|
||||
Ok(())
|
||||
inky_ticker::populate(&mut display, &mut state);
|
||||
|
||||
println!("Updating window");
|
||||
window.update(&display);
|
||||
|
||||
|
||||
println!("Going to sleep");
|
||||
|
||||
state.advance_screen();
|
||||
std::thread::sleep(minute);
|
||||
}
|
||||
}
|
||||
|
|
38
src/error.rs
Normal file
38
src/error.rs
Normal file
|
@ -0,0 +1,38 @@
|
|||
use embedded_graphics::{
|
||||
draw_target::DrawTarget,
|
||||
mono_font::MonoTextStyle,
|
||||
prelude::*,
|
||||
text::{Alignment, Baseline, Text},
|
||||
};
|
||||
use profont::{PROFONT_12_POINT, PROFONT_24_POINT};
|
||||
use ssd1675::Color;
|
||||
use std::error::Error;
|
||||
use std::fmt::Debug;
|
||||
|
||||
pub fn display_no_data<D, E>(display: &mut D, err: E)
|
||||
where
|
||||
D: DrawTarget<Color = Color>,
|
||||
D::Error: Debug,
|
||||
E: Error,
|
||||
{
|
||||
Text::with_alignment(
|
||||
"KEINE DATEN",
|
||||
Point::new(106, 28),
|
||||
MonoTextStyle::new(&PROFONT_24_POINT, Color::Red),
|
||||
Alignment::Center,
|
||||
)
|
||||
.draw(display)
|
||||
.expect("error drawing text");
|
||||
|
||||
eprintln!("[error] Failed to get/parse JSON:\n{}", err);
|
||||
|
||||
// TODO(feliix42): As soon as there is a custom text renderer for full texts, use it here!
|
||||
Text::with_baseline(
|
||||
&err.to_string(),
|
||||
Point::new(5, 32),
|
||||
MonoTextStyle::new(&PROFONT_12_POINT, Color::Black),
|
||||
Baseline::Top,
|
||||
)
|
||||
.draw(display)
|
||||
.expect("error drawing text");
|
||||
}
|
149
src/lib.rs
149
src/lib.rs
|
@ -1,121 +1,50 @@
|
|||
use embedded_graphics::{
|
||||
draw_target::DrawTarget, image::Image, mono_font::MonoTextStyle, prelude::*, text::Text,
|
||||
};
|
||||
use profont::{PROFONT_12_POINT, PROFONT_14_POINT, PROFONT_24_POINT, PROFONT_9_POINT};
|
||||
use embedded_graphics::draw_target::DrawTarget;
|
||||
use ssd1675::Color;
|
||||
use std::fmt::Debug;
|
||||
use std::process::Command;
|
||||
use std::{fs, io};
|
||||
use tinybmp::Bmp;
|
||||
|
||||
mod error;
|
||||
mod pollen;
|
||||
mod weather;
|
||||
|
||||
pub fn populate<D>(display: &mut D)
|
||||
/// Represents the different screens that can be shown on the ticker display.
|
||||
#[derive(Clone, Copy, PartialEq, Eq)]
|
||||
pub enum Screen {
|
||||
Weather,
|
||||
PollenRadar,
|
||||
}
|
||||
|
||||
pub struct ApplicationState {
|
||||
/// Current copy of the pollen data, if any is available.
|
||||
pub pollen_data: Option<pollen::data::PollenData>,
|
||||
/// Currently shown screen
|
||||
pub current_screen: Screen,
|
||||
}
|
||||
|
||||
impl Default for ApplicationState {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
pollen_data: None,
|
||||
current_screen: Screen::Weather,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ApplicationState {
|
||||
pub fn advance_screen(&mut self) {
|
||||
self.current_screen = match self.current_screen {
|
||||
Screen::Weather => Screen::PollenRadar,
|
||||
Screen::PollenRadar => Screen::Weather,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn populate<D>(display: &mut D, state: &mut ApplicationState)
|
||||
where
|
||||
D: DrawTarget<Color = Color>,
|
||||
D::Error: Debug,
|
||||
{
|
||||
weather::get_weather(display)
|
||||
match state.current_screen {
|
||||
Screen::Weather => weather::get_weather(display),
|
||||
Screen::PollenRadar => pollen::get_pollen(display, state),
|
||||
}
|
||||
|
||||
// TODO(feliix42): Remove dead code here
|
||||
|
||||
pub fn populate_default<D>(display: &mut D)
|
||||
where
|
||||
D: DrawTarget<Color = Color>,
|
||||
D::Error: Debug,
|
||||
{
|
||||
Text::new(
|
||||
"Raspberry Pi",
|
||||
Point::new(1, 20),
|
||||
MonoTextStyle::new(&PROFONT_24_POINT, Color::Red),
|
||||
)
|
||||
.draw(display)
|
||||
.expect("error drawing text");
|
||||
|
||||
if let Ok(cpu_temp) = read_cpu_temp() {
|
||||
Text::new(
|
||||
"CPU Temp:",
|
||||
Point::new(1, 40),
|
||||
MonoTextStyle::new(&PROFONT_14_POINT, Color::Black),
|
||||
)
|
||||
.draw(display)
|
||||
.expect("error drawing text");
|
||||
Text::new(
|
||||
&format!("{:.1}°C", cpu_temp),
|
||||
Point::new(95, 40),
|
||||
MonoTextStyle::new(&PROFONT_12_POINT, Color::Red),
|
||||
)
|
||||
.draw(display)
|
||||
.expect("error drawing text");
|
||||
}
|
||||
|
||||
if let Some(uptime) = read_uptime() {
|
||||
Text::new(
|
||||
uptime.trim(),
|
||||
Point::new(1, 93),
|
||||
MonoTextStyle::new(&PROFONT_9_POINT, Color::Black),
|
||||
)
|
||||
.draw(display)
|
||||
.expect("error drawing text");
|
||||
}
|
||||
|
||||
if let Some(uname) = read_uname() {
|
||||
Text::new(
|
||||
uname.trim(),
|
||||
Point::new(1, 84),
|
||||
MonoTextStyle::new(&PROFONT_9_POINT, Color::Black),
|
||||
)
|
||||
.draw(display)
|
||||
.expect("error drawing text");
|
||||
}
|
||||
|
||||
let bmp_data = include_bytes!("../assets/rain.bmp");
|
||||
|
||||
// Load 16 BPP 8x8px image.
|
||||
// Note: The color type is specified explicitly to match the format used by the BMP image.
|
||||
let bmp = Bmp::<Color>::from_slice(bmp_data).unwrap();
|
||||
//let bmp: Bmp<Color> = bmp.into();
|
||||
|
||||
// Draw the image with the top left corner at (10, 20) by wrapping it in
|
||||
// an embedded-graphics `Image`.
|
||||
Image::new(&bmp, Point::new(10, 20))
|
||||
.draw(display)
|
||||
.expect("Failed to draw image");
|
||||
}
|
||||
|
||||
pub fn read_cpu_temp() -> Result<f64, io::Error> {
|
||||
fs::read_to_string("/sys/class/thermal/thermal_zone0/temp")?
|
||||
.trim()
|
||||
.parse::<i32>()
|
||||
.map(|temp| temp as f64 / 1000.)
|
||||
.map_err(|err| io::Error::new(io::ErrorKind::Other, err))
|
||||
}
|
||||
|
||||
pub fn read_uptime() -> Option<String> {
|
||||
Command::new("uptime")
|
||||
.arg("-p")
|
||||
.output()
|
||||
.ok()
|
||||
.and_then(|output| {
|
||||
if output.status.success() {
|
||||
String::from_utf8(output.stdout).ok()
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
pub fn read_uname() -> Option<String> {
|
||||
Command::new("uname")
|
||||
.arg("-smr")
|
||||
.output()
|
||||
.ok()
|
||||
.and_then(|output| {
|
||||
if output.status.success() {
|
||||
String::from_utf8(output.stdout).ok()
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
}
|
||||
|
|
7
src/pollen/api.rs
Normal file
7
src/pollen/api.rs
Normal file
|
@ -0,0 +1,7 @@
|
|||
use super::data::RawPollenData;
|
||||
|
||||
pub fn get_pollen_data() -> Result<RawPollenData, ureq::Error> {
|
||||
let api_url = "https://opendata.dwd.de/climate_environment/health/alerts/s31fg.json";
|
||||
|
||||
Ok(ureq::get(api_url).call()?.into_json()?)
|
||||
}
|
204
src/pollen/data.rs
Normal file
204
src/pollen/data.rs
Normal file
|
@ -0,0 +1,204 @@
|
|||
//! Rust data types representing the return values of the [DWD Pollen
|
||||
//! API](https://opendata.dwd.de/climate_environment/health/alerts/Beschreibung_pollen_s31fg.pdf).
|
||||
//!
|
||||
//! Individual region keys and and other data is explained in the OpenData portal documentation
|
||||
//! linked above.
|
||||
|
||||
use serde::Deserialize;
|
||||
use std::collections::HashMap;
|
||||
use std::str::FromStr;
|
||||
use time::{macros::format_description, OffsetDateTime, PrimitiveDateTime, UtcOffset};
|
||||
|
||||
// TODO(feliix42): Transparent error handling: remove `unwrap`!
|
||||
|
||||
/// Raw Response Data from the DWD Pollen API with both current Pollen data and forecast information for the
|
||||
/// next two days.
|
||||
///
|
||||
/// Not intended to be used as is. It must be converted to `PollenData` first using
|
||||
/// [PollenData::from].
|
||||
#[derive(Deserialize)]
|
||||
pub struct RawPollenData {
|
||||
// meta fields
|
||||
/// Name of the Data retrieved (i.e., the title of the forecast)
|
||||
//pub name: String,
|
||||
/// Creator of the data
|
||||
//pub sender: String,
|
||||
// TODO(feliix42): Write a custom parser stripping the `Uhr` string at the end.
|
||||
/// Date and time when this report was created
|
||||
last_update: String,
|
||||
/// Next update to the data
|
||||
next_update: String,
|
||||
///// Explanations for the individual hazard levels
|
||||
//pub legend: HashMap<String, String>,
|
||||
|
||||
// actual data
|
||||
/// A list of all data for Germany
|
||||
content: Vec<RawRegionPollenData>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct RawRegionPollenData {
|
||||
pub region_name: String,
|
||||
#[serde(rename = "partregion_name")]
|
||||
pub part_of_region: String,
|
||||
pub region_id: u32,
|
||||
#[serde(rename = "partregion_id")]
|
||||
pub part_of_region_id: i32,
|
||||
#[serde(rename = "Pollen")]
|
||||
pub pollen: HashMap<Allergene, RawAllergenDetails>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct RawAllergenDetails {
|
||||
pub today: String,
|
||||
pub tomorrow: String,
|
||||
pub dayafter_to: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Eq, PartialEq)]
|
||||
pub enum ParseError {
|
||||
// UnknownAllergen,
|
||||
UnknownIntensity,
|
||||
}
|
||||
|
||||
/// Response from the DWD Pollen API with both current Pollen data and forecast information for the
|
||||
/// next two days.
|
||||
pub struct PollenData {
|
||||
/// Date and time when this report was created
|
||||
pub last_update: OffsetDateTime,
|
||||
/// Next update to the data
|
||||
pub next_update: OffsetDateTime,
|
||||
|
||||
/// A list of all data for Germany
|
||||
pub content: Vec<RegionPollenData>,
|
||||
}
|
||||
|
||||
impl PollenData {
|
||||
pub fn get_region_by_ids<'a>(&'a self, id: u32, part: i32) -> Option<&'a RegionPollenData> {
|
||||
self.content.iter().find(|&x| x.region_id == id && x.part_of_region_id == part)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<RawPollenData> for PollenData {
|
||||
fn from(value: RawPollenData) -> Self {
|
||||
let format = format_description!("[year]-[month]-[day] [hour]:[minute] Uhr");
|
||||
|
||||
// we assume the DWD server has the same time zone offset as our Pi.
|
||||
let local_offset = UtcOffset::current_local_offset().unwrap();
|
||||
|
||||
let last = PrimitiveDateTime::parse(&value.last_update, format)
|
||||
.unwrap()
|
||||
.assume_offset(local_offset);
|
||||
let next = PrimitiveDateTime::parse(&value.next_update, format)
|
||||
.unwrap()
|
||||
.assume_offset(local_offset);
|
||||
|
||||
Self {
|
||||
last_update: last,
|
||||
next_update: next,
|
||||
|
||||
content: value
|
||||
.content
|
||||
.into_iter()
|
||||
.map(RegionPollenData::from)
|
||||
.collect(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct RegionPollenData {
|
||||
pub region_name: String,
|
||||
pub part_of_region: String,
|
||||
pub region_id: u32,
|
||||
pub part_of_region_id: i32,
|
||||
pub pollen: HashMap<Allergene, AllergenDetails>,
|
||||
}
|
||||
|
||||
impl From<RawRegionPollenData> for RegionPollenData {
|
||||
fn from(value: RawRegionPollenData) -> Self {
|
||||
Self {
|
||||
region_name: value.region_name,
|
||||
part_of_region: value.part_of_region,
|
||||
region_id: value.region_id,
|
||||
part_of_region_id: value.part_of_region_id,
|
||||
pollen: value
|
||||
.pollen
|
||||
.into_iter()
|
||||
.map(|(k, v)| (k, AllergenDetails::from(v)))
|
||||
.collect(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Details about a specific allergen
|
||||
pub struct AllergenDetails {
|
||||
pub today: Intensity,
|
||||
pub tomorrow: Intensity,
|
||||
pub dayafter_to: Intensity,
|
||||
}
|
||||
|
||||
impl From<RawAllergenDetails> for AllergenDetails {
|
||||
fn from(value: RawAllergenDetails) -> Self {
|
||||
Self {
|
||||
today: Intensity::from_str(&value.today).unwrap(),
|
||||
tomorrow: Intensity::from_str(&value.tomorrow).unwrap(),
|
||||
dayafter_to: Intensity::from_str(&value.dayafter_to).unwrap(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Individual allergenes
|
||||
#[derive(Deserialize, Eq, Hash, PartialEq)]
|
||||
pub enum Allergene {
|
||||
Ambrosia,
|
||||
Roggen,
|
||||
Birke,
|
||||
Erle,
|
||||
Beifuss,
|
||||
Esche,
|
||||
Graeser,
|
||||
Hasel,
|
||||
}
|
||||
|
||||
/// Intensity of an allergen
|
||||
#[derive(Copy, Clone, PartialOrd, Ord, PartialEq, Eq)]
|
||||
pub enum Intensity {
|
||||
Keine,
|
||||
KeinBisGering,
|
||||
Gering,
|
||||
GeringBisMittel,
|
||||
Mittel,
|
||||
MittelBisHoch,
|
||||
Hoch,
|
||||
}
|
||||
|
||||
impl Intensity {
|
||||
pub fn as_fraction(&self) -> f32 {
|
||||
match *self {
|
||||
Intensity::Keine => 0_f32,
|
||||
Intensity::KeinBisGering => 0.1666,
|
||||
Intensity::Gering => 0.3333,
|
||||
Intensity::GeringBisMittel => 0.5,
|
||||
Intensity::Mittel => 0.6666,
|
||||
Intensity::MittelBisHoch => 0.8333,
|
||||
Intensity::Hoch => 1_f32,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl FromStr for Intensity {
|
||||
type Err = ParseError;
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
match s {
|
||||
"0" => Ok(Intensity::Keine),
|
||||
"0-1" => Ok(Intensity::KeinBisGering),
|
||||
"1" => Ok(Intensity::Gering),
|
||||
"1-2" => Ok(Intensity::GeringBisMittel),
|
||||
"2" => Ok(Intensity::Mittel),
|
||||
"2-3" => Ok(Intensity::MittelBisHoch),
|
||||
"3" => Ok(Intensity::Hoch),
|
||||
_ => Err(ParseError::UnknownIntensity),
|
||||
}
|
||||
}
|
||||
}
|
145
src/pollen/mod.rs
Normal file
145
src/pollen/mod.rs
Normal file
|
@ -0,0 +1,145 @@
|
|||
use embedded_graphics::{
|
||||
draw_target::DrawTarget,
|
||||
mono_font::MonoTextStyle,
|
||||
prelude::*,
|
||||
primitives::{Rectangle, RoundedRectangle, PrimitiveStyleBuilder},
|
||||
text::{Alignment, Text},
|
||||
};
|
||||
use profont::{
|
||||
PROFONT_12_POINT, PROFONT_18_POINT,
|
||||
};
|
||||
use ssd1675::Color;
|
||||
use std::fmt::Debug;
|
||||
use crate::error::display_no_data;
|
||||
use crate::ApplicationState;
|
||||
use time::OffsetDateTime;
|
||||
use data::{Allergene, Intensity};
|
||||
|
||||
mod api;
|
||||
pub mod data;
|
||||
|
||||
pub fn get_pollen<D>(display: &mut D, state: &mut ApplicationState)
|
||||
where
|
||||
D: DrawTarget<Color = Color>,
|
||||
D::Error: Debug,
|
||||
{
|
||||
// determine whether an update to the state is necessary
|
||||
let update_needed = if let Some(data) = &state.pollen_data {
|
||||
let now = OffsetDateTime::now_local().unwrap();
|
||||
(data.next_update - now).is_negative()
|
||||
} else {
|
||||
true
|
||||
};
|
||||
|
||||
let pollen_data = if update_needed {
|
||||
match api::get_pollen_data() {
|
||||
Ok(d) => data::PollenData::from(d),
|
||||
Err(e) => {
|
||||
state.pollen_data = None;
|
||||
display_no_data(display, e);
|
||||
return;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
std::mem::take(&mut state.pollen_data).unwrap()
|
||||
};
|
||||
|
||||
// 80: Sachsen
|
||||
// 81: Tiefland
|
||||
let local_data = pollen_data.get_region_by_ids(80, 81).unwrap();
|
||||
|
||||
// draw results
|
||||
Text::with_alignment(
|
||||
"Pollenbelastung",
|
||||
Point::new(106, 21),
|
||||
MonoTextStyle::new(&PROFONT_18_POINT, Color::Black),
|
||||
Alignment::Center,
|
||||
)
|
||||
.draw(display)
|
||||
.expect("error drawing text");
|
||||
|
||||
Text::new(
|
||||
"Roggen",
|
||||
Point::new(30, 49),
|
||||
MonoTextStyle::new(&PROFONT_12_POINT, Color::Black),
|
||||
)
|
||||
.draw(display)
|
||||
.expect("error drawing text");
|
||||
|
||||
let rye_intensity = local_data.pollen[&Allergene::Roggen].today;
|
||||
let rye_value = (78_f32 * rye_intensity.as_fraction()).round() as u32;
|
||||
let highlight_color = if rye_intensity >= Intensity::GeringBisMittel {
|
||||
Color::Red
|
||||
} else {
|
||||
Color::Black
|
||||
};
|
||||
|
||||
let outer_style = PrimitiveStyleBuilder::new()
|
||||
.stroke_width(1)
|
||||
.stroke_color(highlight_color)
|
||||
.fill_color(Color::White)
|
||||
.build();
|
||||
let inner_style = PrimitiveStyleBuilder::new()
|
||||
.stroke_width(0)
|
||||
.stroke_color(highlight_color)
|
||||
.fill_color(highlight_color)
|
||||
.build();
|
||||
|
||||
RoundedRectangle::with_equal_corners(
|
||||
Rectangle::new(Point::new(105, 39), Size::new(80, 12)),
|
||||
Size::new(6, 6)
|
||||
)
|
||||
.into_styled(outer_style)
|
||||
.draw(display)
|
||||
.expect("error drawing rectangle");
|
||||
|
||||
RoundedRectangle::with_equal_corners(
|
||||
Rectangle::new(Point::new(106, 40), Size::new(rye_value, 10)),
|
||||
Size::new(5, 5)
|
||||
)
|
||||
.into_styled(inner_style)
|
||||
.draw(display)
|
||||
.expect("error drawing rectangle");
|
||||
|
||||
Text::new(
|
||||
"Gräser",
|
||||
Point::new(30, 69),
|
||||
MonoTextStyle::new(&PROFONT_12_POINT, Color::Black),
|
||||
)
|
||||
.draw(display)
|
||||
.expect("error drawing text");
|
||||
|
||||
let outer_style = PrimitiveStyleBuilder::new()
|
||||
.stroke_width(1)
|
||||
.stroke_color(Color::Black)
|
||||
.fill_color(Color::White)
|
||||
.build();
|
||||
let inner_style = PrimitiveStyleBuilder::new()
|
||||
.stroke_width(0)
|
||||
.stroke_color(Color::Black)
|
||||
.fill_color(Color::Black)
|
||||
.build();
|
||||
|
||||
RoundedRectangle::with_equal_corners(
|
||||
Rectangle::new(Point::new(105, 59), Size::new(80, 12)),
|
||||
Size::new(6, 6)
|
||||
)
|
||||
.into_styled(outer_style)
|
||||
.draw(display)
|
||||
.expect("error drawing rectangle");
|
||||
|
||||
let graeser_intensity = local_data.pollen[&Allergene::Graeser].today;
|
||||
let graeser_value = (78_f32 * graeser_intensity.as_fraction()).round() as u32;
|
||||
|
||||
RoundedRectangle::with_equal_corners(
|
||||
Rectangle::new(Point::new(106, 60), Size::new(graeser_value, 10)),
|
||||
Size::new(5, 5)
|
||||
)
|
||||
.into_styled(inner_style)
|
||||
.draw(display)
|
||||
.expect("error drawing rectangle");
|
||||
|
||||
|
||||
// write back current status
|
||||
state.pollen_data = Some(pollen_data);
|
||||
}
|
|
@ -59,7 +59,7 @@ impl WeatherData {
|
|||
}
|
||||
|
||||
pub fn current_condition(&self) -> Bmp<'static, Color> {
|
||||
unimplemented!()
|
||||
self.current.data.weather[0].condition.clone().into()
|
||||
}
|
||||
|
||||
pub fn forecast_plot_data(&self) -> Vec<PlotPoint> {
|
||||
|
@ -175,7 +175,7 @@ pub struct WeatherDetails {
|
|||
/// specification](https://openweathermap.org/weather-conditions#Weather-Condition-Codes-2).
|
||||
/// It ignores the fine details within weather conditions (e.g. heavy thunderstorm and light
|
||||
/// thunderstorm both map to `Thunderstorm`).
|
||||
#[derive(Deserialize)] //Debug)]
|
||||
#[derive(Clone, Deserialize)] //Debug)]
|
||||
#[serde(from = "u16")]
|
||||
pub enum WeatherCondition {
|
||||
Thunderstorm,
|
||||
|
|
|
@ -3,15 +3,15 @@ use embedded_graphics::{
|
|||
image::Image,
|
||||
mono_font::MonoTextStyle,
|
||||
prelude::*,
|
||||
text::{Alignment, Baseline, Text},
|
||||
text::Text
|
||||
};
|
||||
use embedded_plots::{axis::Scale, curve::Curve, single_plot::SinglePlot};
|
||||
use profont::{
|
||||
PROFONT_12_POINT, PROFONT_14_POINT, PROFONT_24_POINT, PROFONT_7_POINT, PROFONT_9_POINT,
|
||||
PROFONT_14_POINT, PROFONT_24_POINT, PROFONT_7_POINT, PROFONT_9_POINT,
|
||||
};
|
||||
use ssd1675::Color;
|
||||
use std::error::Error;
|
||||
use std::fmt::Debug;
|
||||
use crate::error::display_no_data;
|
||||
|
||||
mod api;
|
||||
mod data;
|
||||
|
@ -119,30 +119,3 @@ where
|
|||
.expect("Failed to draw the temperature plot");
|
||||
}
|
||||
|
||||
fn display_no_data<D, E>(display: &mut D, err: E)
|
||||
where
|
||||
D: DrawTarget<Color = Color>,
|
||||
D::Error: Debug,
|
||||
E: Error,
|
||||
{
|
||||
Text::with_alignment(
|
||||
"KEINE DATEN",
|
||||
Point::new(106, 28),
|
||||
MonoTextStyle::new(&PROFONT_24_POINT, Color::Red),
|
||||
Alignment::Center,
|
||||
)
|
||||
.draw(display)
|
||||
.expect("error drawing text");
|
||||
|
||||
eprintln!("[error] Failed to get/parse JSON:\n{}", err);
|
||||
|
||||
// TODO(feliix42): As soon as there is a custom text renderer for full texts, use it here!
|
||||
Text::with_baseline(
|
||||
&err.to_string(),
|
||||
Point::new(5, 32),
|
||||
MonoTextStyle::new(&PROFONT_12_POINT, Color::Black),
|
||||
Baseline::Top,
|
||||
)
|
||||
.draw(display)
|
||||
.expect("error drawing text");
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue