diff --git a/src/main.rs b/src/main.rs index c8ed644..fbf29f7 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,15 +1,16 @@ -use core::panic; use std::num::NonZeroU16; -use std::{borrow::Cow, num::NonZero}; +use std::{borrow::Cow}; use std::str::FromStr; -use chrono::{format::Fixed, DateTime, Datelike, Days, FixedOffset, NaiveTime}; +use anyhow::Context; +use chrono::{DateTime, Datelike, Days, FixedOffset, NaiveTime, TimeDelta, Timelike, 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}, @@ -35,7 +36,7 @@ type Timetable = [[Option;8]; 5]; static mut WAKEUP_COUNTER: u64 = 0; #[link_section = ".rtc.data"] -static mut WIFI_FAIL_COUNTER: Option = None; +static mut FETCHING_FAIL_COUNT: Option = None; #[link_section = ".rtc.data"] static mut ERROR_MSG: Option<[u8; 2048]> = None; @@ -53,9 +54,9 @@ fn main() -> anyhow::Result<()> { let busy = PinDriver::input(peripherals.pins.gpio17).unwrap(); let rst = PinDriver::output(peripherals.pins.gpio16).unwrap(); - let cs = peripherals.pins.gpio5; // y - let clk = peripherals.pins.gpio18; // y - let mosi = peripherals.pins.gpio23; // y + 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(); @@ -145,73 +146,42 @@ fn main() -> anyhow::Result<()> { } // Create a file called wifi-creds.txt and write SSID and password into it separated by a line break - 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().unwrap(); - let nvs = EspDefaultNvsPartition::take().unwrap(); - let mut wifi = BlockingWifi::wrap( - EspWifi::new(peripherals.modem, sysloop.clone(), Some(nvs)).unwrap(), - sysloop.clone(), - ) - .unwrap(); - wifi.set_configuration(&Configuration::Client( - esp_idf_svc::wifi::ClientConfiguration { - ssid, - password, - auth_method: esp_idf_svc::wifi::AuthMethod::WPA2WPA3Personal, - ..Default::default() - }, - )) - .unwrap(); - let connected = wifi.start() - .and_then(|_| wifi.connect()) - .and_then(|_| wifi.wait_netif_up()) - .and_then(|_| loop {if wifi.is_connected().unwrap() {break Ok(());}}).is_ok(); + 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(); - if !connected{ - let fail_count: u16 = unsafe { - let new_val = match WIFI_FAIL_COUNTER{ - Some(c) => c.saturating_add(1), - None => { - NonZeroU16::new_unchecked(1) - } - }; - WIFI_FAIL_COUNTER = 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); - println!("Failed to connect to wifi for the {}th time", fail_count); - - // Quadratic falloff - let sleep_time = u32::min((fail_count as u32).pow(2u32) * 4, 600); - println!("Going to sleep for {} seconds", sleep_time); - unsafe { - esp_deep_sleep(sleep_time as u64 * 1000_000) + unsafe { + esp_deep_sleep(sleep_time as u64 * 1000_000) + } } - } - - println!("Connected to wifi"); + }; unsafe { - WIFI_FAIL_COUNTER = None; + FETCHING_FAIL_COUNT = None; } - let httpconnection = EspHttpConnection::new(&HttpConfig { - use_global_ca_store: true, - crt_bundle_attach: Some(esp_idf_sys::esp_crt_bundle_attach), - ..Default::default() - })?; - - let (tt, now) = fetch_timetable(httpconnection); + // 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); + 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(); @@ -219,19 +189,86 @@ fn main() -> anyhow::Result<()> { epd7in5.sleep(&mut spi, &mut delay)?; log::info!("Completed"); - wifi.disconnect().unwrap(); - wifi.stop().unwrap(); - drop(epd7in5); - drop(wifi); drop(spi); + let seconds_until_wakeup = next_wakeup_time.timestamp() - now.timestamp(); + unsafe{ - esp_deep_sleep(10 * 60 * 1000_000); + esp_deep_sleep(seconds_until_wakeup as u64 * 1000_000); } } -fn draw_layout(display: &mut epd_waveshare::epd7in5_v2::Display7in5, timetable: &Timetable, now: DateTime){ +fn calculate_next_wakeuptime(now: DateTime) -> Option>{ + + 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, DateTime)>{ + 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, next_wakeup_time: DateTime){ let display_area = display.bounding_box(); let margin = FixedMargin(10); @@ -277,12 +314,16 @@ fn draw_layout(display: &mut epd_waveshare::epd7in5_v2::Display7in5, timetable: 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(); - LinearLayout::vertical(Chain::new(time_table_grid).append(time_text)) + 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() @@ -314,6 +355,15 @@ fn get_school_period(time: NaiveTime) -> 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) -> Box{ @@ -333,8 +383,8 @@ fn transform_lessons(lessons: Vec) -> Box{ table } - -fn fetch_timetable(client: EspHttpConnection) -> (Box, DateTime) { +//TODO: Make this return result +fn fetch_timetable(client: EspHttpConnection) -> anyhow::Result<(Box, DateTime)> { let mut creds = include_str!("../untis-creds.txt").split('\n'); let server = creds.next().unwrap(); @@ -348,12 +398,12 @@ fn fetch_timetable(client: EspHttpConnection) -> (Box, DateTime