Files
esp-epd/src/main.rs

434 lines
15 KiB
Rust

use std::collections::HashMap;
use std::num::NonZeroU16;
use std::{borrow::Cow};
use std::str::FromStr;
use anyhow::Context;
use chrono::{DateTime, Datelike, Days, FixedOffset, NaiveTime, TimeDelta, Weekday};
use embedded_graphics::{
prelude::{Dimensions, Point},
text::{Text},
Drawable,
};
use epd_waveshare::{color::Color, epd7in5_v2::Epd7in5, prelude::WaveshareDisplay};
use esp_idf_hal::modem::Modem;
use esp_idf_hal::{
delay::{Ets},
gpio::{Gpio19, PinDriver},
spi::{config, SpiDriverConfig},
};
use esp_idf_svc::http::client::{Configuration as HttpConfig, EspHttpConnection};
use esp_idf_svc::wifi::{BlockingWifi, EspWifi};
use esp_idf_svc::{
eventloop::EspSystemEventLoop,
hal::{prelude::*, spi::SpiDeviceDriver},
nvs::EspDefaultNvsPartition,
wifi::Configuration,
};
use esp_idf_sys::{esp_deep_sleep, StaticSemaphore_t};
use u8g2_fonts::U8g2TextStyle;
use untis::{Date, Lesson, LessonCode};
use embedded_layout::{layout::linear::{spacing::DistributeFill, FixedMargin, LinearLayout}, prelude::*};
type Timetable = [[Option<Lesson>;8]; 5];
#[unsafe(link_section = ".rtc.data")]
static mut WAKEUP_COUNTER: u64 = 0;
#[unsafe(link_section = ".rtc.data")]
static mut FETCHING_FAIL_COUNT: Option<NonZeroU16> = None;
#[unsafe(link_section = ".rtc.data")]
static mut ERROR_MSG: Option<[u8; 2048]> = None;
fn main() -> anyhow::Result<()> {
// It is necessary to call this function once. Otherwise some patches to the runtime
// implemented by esp-idf-sys might not link properly. See https://github.com/esp-rs/esp-idf-template/issues/71
esp_idf_svc::sys::link_patches();
// Bind the log crate to the ESP Logging facilities
esp_idf_svc::log::EspLogger::initialize_default();
let peripherals = Peripherals::take().unwrap();
log::info!("Took peripherals");
let busy = PinDriver::input(peripherals.pins.gpio17).unwrap();
let rst = PinDriver::output(peripherals.pins.gpio16).unwrap();
let cs = peripherals.pins.gpio5;
let clk = peripherals.pins.gpio18;
let mosi = peripherals.pins.gpio23;
let miso = peripherals.pins.gpio19; // aka. dc
let dc = PinDriver::output(miso).unwrap();
std::panic::set_hook(Box::new(|panic|{
let msg: String = format!("{}", panic);/* if let Some(s) = panic.payload().downcast_ref::<&str>() {
format!("{s:?}")
} else if let Some(s) = panic.payload().downcast_ref::<String>() {
format!("{s:?}")
} else {
format!("Failed to downcast panic msg")
}; */
let msg_bytes = msg.into_bytes();
// Copy the string buffer into the array
let mut buffer = [0u8; 2048];
for i in 0..(buffer.len().min(msg_bytes.len())){
buffer[i] = msg_bytes[i];
}
unsafe{
ERROR_MSG = Some(buffer);
// Sleep for a second
esp_deep_sleep(1_000_000)
}
}));
log::info!("Initialized pins");
let config = config::Config::new();
let mut spi = SpiDeviceDriver::new_single(
peripherals.spi2, // 2 oder 3?
clk,
mosi,
Option::<Gpio19>::None,
Some(cs),
&SpiDriverConfig::default(),
&config,
)
.unwrap();
log::info!("Initialized spi interface");
// Initialize the draw buffer
// Allocating on the stack makes the chip crash. We gotta put it on the heap
let mut display = Box::new(epd_waveshare::epd7in5_v2::Display7in5::default());
display.set_rotation(epd_waveshare::prelude::DisplayRotation::Rotate0);
unsafe {
WAKEUP_COUNTER += 1;
}
let mut delay = Ets;
unsafe{
if let Some(error) = ERROR_MSG{
// Draw to the display
let text_style = U8g2TextStyle::new(u8g2_fonts::fonts::u8g2_font_7x14_mr, Color::White);
let error_label = Text::new("An error occured:", Point::zero(), &text_style);
let error_message = String::from_utf8_lossy(&error);
let error_message = match &error_message{
Cow::Borrowed(C) => *C,
Cow::Owned(C) => &C
} ;
let error_message_label = Text::new(error_message, Point::zero(), &text_style);
let mut vg = [error_label, error_message_label];
LinearLayout::vertical(Views::new(&mut vg))
.arrange()
.draw(&mut *display).unwrap();
let mut epd7in5 = Epd7in5::new(&mut spi, busy, dc, rst, &mut delay, None).unwrap();
epd7in5.update_and_display_frame(&mut spi, &*display.buffer(), &mut delay)?;
epd7in5.sleep(&mut spi, &mut delay)?;
// Sleep for 12 hours
esp_deep_sleep(12*60*60*60*1000_000)
}
}
// Create a file called wifi-creds.txt and write SSID and password into it separated by a line break
let (tt, now) = match fetch_data(peripherals.modem){
Ok(d) => d,
Err(e) => {
let fail_count: u16 = unsafe {
let new_val = match FETCHING_FAIL_COUNT{
Some(C) => C.saturating_add(1),
None => {
NonZeroU16::new_unchecked(1)
}
};
FETCHING_FAIL_COUNT = Some(new_val);
new_val
}.into();
println!("Failed to fetch timetable for the {}th time. Error: {}", fail_count, e);
// Quadratic falloff maxing out at 1 hours intervals
let sleep_time = u32::min((fail_count as u32).pow(2u32) * 4, 3600);
println!("Going to sleep for {} seconds", sleep_time);
unsafe {
esp_deep_sleep(sleep_time as u64 * 1000_000)
}
}
};
unsafe {
FETCHING_FAIL_COUNT = None;
}
// We wanna panic here if this fails because retrying this in a couple of seconds is stupid
let next_wakeup_time = calculate_next_wakeuptime(now).context(format!("Failed to calculate next wakeup time for time {now}")).unwrap();
// Render the layout
draw_layout(&mut display, &tt, now, next_wakeup_time);
// Actually push changes to the display
let mut epd7in5 = Epd7in5::new(&mut spi, busy, dc, rst, &mut delay, None).unwrap();
epd7in5.update_and_display_frame(&mut spi, &*display.buffer(), &mut delay)?;
epd7in5.sleep(&mut spi, &mut delay)?;
log::info!("Completed");
drop(epd7in5);
drop(spi);
let seconds_until_wakeup = next_wakeup_time.timestamp() - now.timestamp();
unsafe{
esp_deep_sleep(seconds_until_wakeup as u64 * 1000_000);
}
}
fn get_full_subject_name(shortname: &str) -> Option<&'static str>{
if shortname.len() < 2{
return None;
}
let shortname = shortname.to_lowercase();
Some(match shortname[..2].as_ref() {
"in" => "Informatik",
"en" => "Englisch",
"de" => "Deutsch",
"ds" => "Theater",
"ph" => "Physik",
"ma" => "Mathematik",
"ch" => "Chemie",
"ge" => "Geschichte",
"sp" => "Sport",
_ => {
return None;
}
})
}
fn calculate_next_wakeuptime(now: DateTime<FixedOffset>) -> Option<DateTime<FixedOffset>>{
if now.weekday().num_days_from_monday() > 4 || (now.weekday() == Weekday::Fri && now.time() > NaiveTime::from_hms_opt(15, 15, 0)?){
// On weekends, wait until 5am on the next day to update
let d = now.checked_add_signed(TimeDelta::days(1))?;
let d = d.with_time(NaiveTime::from_hms_opt(5, 0, 0).unwrap()).single()?;
Some(d)
}else if now.time() > NaiveTime::from_hms_opt(15, 15, 0)? || now.time() < NaiveTime::from_hms_opt(5, 0, 0)?{
// If it's a weekday but it's after 15:15 or before 5am, update after an hour
Some(now.checked_add_signed(TimeDelta::minutes(60))?)
}else{
// In any other case, update 5 minutes before the end of a lesson but at least 10 minutes from now
let (_, end_time) = get_school_period_and_end_time(now.time());
let before_lesson_end = now.with_time(end_time.overflowing_sub_signed(TimeDelta::minutes(5)).0).earliest()?;
let in_10_minutes = now.checked_add_signed(TimeDelta::minutes(10))?;
Some(before_lesson_end.max(in_10_minutes))
}
}
fn fetch_data(modem: Modem) -> anyhow::Result<(Box<Timetable>, DateTime<FixedOffset>)>{
let mut creds = include_str!("../wifi-creds.txt").split('\n');
let ssid: heapless::String<32> =
heapless::String::<32>::from_str(creds.next().unwrap()).unwrap();
let password: heapless::String<64> =
heapless::String::<64>::from_str(creds.next().unwrap()).unwrap();
// Connecting to wifi
let sysloop = EspSystemEventLoop::take()?;
let nvs = EspDefaultNvsPartition::take()?;
let mut wifi = BlockingWifi::wrap(
EspWifi::new(modem, sysloop.clone(), Some(nvs))?,
sysloop.clone(),
)?;
wifi.set_configuration(&Configuration::Client(
esp_idf_svc::wifi::ClientConfiguration {
ssid,
password,
auth_method: esp_idf_svc::wifi::AuthMethod::WPA2WPA3Personal,
..Default::default()
},
))?;
wifi.start()
.and_then(|_| wifi.connect())
.and_then(|_| wifi.wait_netif_up())
.and_then(|_| loop {if wifi.is_connected().unwrap() {break Ok(());}})?;
println!("Connected to wifi");
let httpconnection = EspHttpConnection::new(&HttpConfig {
use_global_ca_store: true,
crt_bundle_attach: Some(esp_idf_sys::esp_crt_bundle_attach),
..Default::default()
})?;
let res = fetch_timetable(httpconnection);
wifi.disconnect().unwrap();
wifi.stop().unwrap();
res
}
fn draw_layout(display: &mut epd_waveshare::epd7in5_v2::Display7in5, timetable: &Timetable, now: DateTime<FixedOffset>, next_wakeup_time: DateTime<FixedOffset>){
let display_area = display.bounding_box();
let margin = FixedMargin(15);
// let text_style = MonoTextStyle::new(&embedded_graphics::mono_font::ascii::FONT_10X20, Color::White);
let text_style = U8g2TextStyle::new(u8g2_fonts::fonts::u8g2_font_profont22_mr, Color::White);
let mut inverted_text_style = U8g2TextStyle::new(u8g2_fonts::fonts::u8g2_font_profont22_mr, Color::Black);
inverted_text_style.background_color = Some(Color::White);
let text_style_small = U8g2TextStyle::new(u8g2_fonts::fonts::u8g2_font_7x14_mr, Color::White);
let empty_str = String::from(" ");
let day_of_week = now.weekday().num_days_from_monday(); // zero-indexed
let current_period = get_school_period(now.time());
let mut texts = timetable.iter().enumerate().map(|(day_index, day)| day.iter().enumerate().map(|(period, l)| {
let s = if let Some(l) = l && l.code != LessonCode::Cancelled{
Some(l.subjects.first().unwrap().name.as_str())
}else{
None
}.and_then(get_full_subject_name);
let text_style = if period as u8 == current_period && day_index as u32 == day_of_week{
&inverted_text_style
}else{
&text_style
};
//LinearLayout::horizontal(Chain::new(Text::new()))
Text::new(s.unwrap_or_else(|| &empty_str), Point::zero(), text_style)
}).collect::<Vec<_>>()).collect::<Vec<_>>();
let mut columns = texts.iter_mut().map(|day|
LinearLayout::vertical(Views::new(day))
.align_to(&display_area, horizontal::Center, vertical::Top)
.with_spacing(margin)
.arrange()
).collect::<Vec<_>>();
let columns = Views::new(&mut columns);
// Unsafe is needed to access static mutable variable
let time_str = format!("{} ({})", now.format("%Y-%m-%d %H:%M"), unsafe {WAKEUP_COUNTER});
let time_text = Text::new(&time_str, Point::zero(), &text_style_small);
let next_wakeup_time_str = format!("Next wakup: {}", next_wakeup_time.naive_local());
let time_table_grid = LinearLayout::horizontal(columns)
.with_alignment(vertical::Center)
.with_spacing(margin)
.arrange();
let subtext = LinearLayout::vertical(Chain::new(time_text).append(Text::new(&next_wakeup_time_str, Point::zero(), &text_style_small))).with_alignment(horizontal::Center).with_spacing(FixedMargin(2)).arrange();
LinearLayout::vertical(Chain::new(time_table_grid).append(subtext))
.with_alignment(horizontal::Center)
.with_spacing(DistributeFill(display_area.size.height-20))
.arrange()
.align_to(&display_area, horizontal::Center, vertical::Center)
.draw(display)
.unwrap();
}
const PERIOD_LOOKUP: [NaiveTime; 8] = [
(NaiveTime::from_hms_opt(8, 45, 0).unwrap()),
(NaiveTime::from_hms_opt(9, 35, 0).unwrap()),
(NaiveTime::from_hms_opt(10, 40, 0).unwrap()),
(NaiveTime::from_hms_opt(11, 30, 0).unwrap()),
(NaiveTime::from_hms_opt(12, 40, 0).unwrap()),
(NaiveTime::from_hms_opt(13, 30, 0).unwrap()),
(NaiveTime::from_hms_opt(14, 25, 0).unwrap()),
(NaiveTime::from_hms_opt(15, 15, 0).unwrap()),
];
fn get_school_period(time: NaiveTime) -> u8{
for (index, period_time) in PERIOD_LOOKUP.iter().enumerate(){
if time < *period_time{
return index as u8;
}
}
7u8
}
fn get_school_period_and_end_time(time: NaiveTime) -> (u8, NaiveTime){
for (index, period_time) in PERIOD_LOOKUP.iter().enumerate(){
if time < *period_time{
return (index as u8, *period_time);
}
}
(7u8, PERIOD_LOOKUP[7])
}
fn transform_lessons(lessons: Vec<Lesson>) -> Box<Timetable>{
let mut table: Box<[[Option<Lesson>; 8]; 5]> = Box::new([0u8; 5].map(|_| [0u8; 8].map(|_| Option::<Lesson>::None)));
for lesson in lessons {
let day_idx = lesson.date.weekday().num_days_from_monday() as usize;
let mut l = Cow::<Lesson>::Owned(lesson);
for (period_idx, time) in PERIOD_LOOKUP.iter().enumerate(){
if l.start_time.0 < *time && &l.end_time.0 >= time{
table[day_idx][period_idx] = Some(l.into_owned());
l = Cow::Borrowed(&table[day_idx][period_idx].as_ref().unwrap());
}
}
}
table
}
//TODO: Make this return result
fn fetch_timetable(client: EspHttpConnection) -> anyhow::Result<(Box<Timetable>, DateTime<FixedOffset>)> {
let mut creds = include_str!("../untis-creds.txt").split('\n');
let server = creds.next().unwrap();
let school_id = creds.next().unwrap();
let usr = creds.next().unwrap();
let password = creds.next().unwrap();
let mut s = untis::Client::login(
server,
school_id,
usr,
password,
client
).or_else(|e| None.context(format!("{:?}", e)))?;
let cest = chrono::FixedOffset::east_opt(2 * 3600).unwrap();
let now = s.date().context("Untis client provided no time but time is necessary for the requesting the timetable")?.with_timezone(&cest);
println!("Current time: {}", now);
let date = now.date_naive().checked_add_days(Days::new(2)).unwrap();
let lessons = s.own_timetable_for_week(&Date(date)).or_else(|e| None.context(format!("{:?}", e)))?;
Ok((transform_lessons(lessons), now))
}