Compare commits

..

No commits in common. "9c7dc2a897096ed71e96eb13a73380761b2caac2" and "ad9cea9378e95dd097cf63a29ebadb1eec7ae8da" have entirely different histories.

14 changed files with 483 additions and 1375 deletions

1332
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -11,8 +11,8 @@ embedded-graphics = "0.8.1"
linux-embedded-hal = "0.3.0"
embedded-hal = "0.2.5"
libc = "0.2.98"
rusqlite = "0.32.1"
time = { version = "0.3.9", features = ["parsing", "formatting"] }
rusqlite = "0.27.0"
time = { version = "0.3.9", features = ["parsing"] }
time-tz = "2"
image = { version = "0.24.1", optional = true }
serde_json = "1.0.79"
@ -21,20 +21,19 @@ serde = "1.0.136"
rppal = { version = "0.14.1", features = ["hal"] }
ssd1351 = { git = "https://github.com/FliegendeWurst/ssd1351-rust", rev = "3de5be50bd9a59391c669aec8357923a56d121f6" }
display-interface-spi = "0.4.1"
ureq = { version = "=3.0.0-rc2", default-features = false, features = ["rustls"] }
ureq = { version = "2.4.0", default-features = false }
winit = { version = "0.28.7", optional = true }
softbuffer = { version = "0.3.1", optional = true }
rand_xoshiro = "0.6.0"
gpiocdev = "0.7.2"
gpiocdev = "0.6.0"
rpassword = "7.2.0"
andotp-import = "0.1.0"
totp-rs = "5.4.0"
color_space = "0.5.4"
#gpio-am2302-rs = { git = "https://github.com/FliegendeWurst/gpio-am2302-rs" }
[features]
pc = ["winit", "softbuffer", "image"]
default = ["pc"]
default = [ "pc" ]
[profile.release]
codegen-units = 1

View File

@ -2,7 +2,7 @@
rustPlatform.buildRustPackage {
pname = "raspi-oled";
version = "unstable-infdev-7";
version = "unstable-infdev-4";
src = ./.;
@ -16,7 +16,7 @@ rustPlatform.buildRustPackage {
nativeBuildInputs = [ pkg-config ];
cargoBuildFlags = [ "--no-default-features" "--bin" "main_loop" ];
cargoBuildFlags = [ "--no-default-features" "--bin" "take_measurement" ];
buildInputs = [ sqlite ];

View File

@ -321,8 +321,8 @@ impl<D: DrawTarget<Color = Rgb565>> Draw<D> for Measurements {
}
if time_until_first.is_none()
&& (i > 0
|| event.1 > time.hour() as i32
|| (event.1 == time.hour() as i32 && event.2 >= time.minute() as i32))
|| event.1 > time.hour() as i32 || (event.1 == time.hour() as i32
&& event.2 >= time.minute() as i32))
{
time_until_first = Some(
((i * 24 + event.1) * 60 + event.2) * 60

View File

@ -22,8 +22,8 @@ use rppal::{
spi::{Bus, Mode, SlaveSelect, Spi},
};
use rusqlite::Connection;
use schedule::{github_notifications::GithubNotifications, Schedule};
use screensaver::{BearReminder, Screensaver, TimeDisplay};
use schedule::Schedule;
use screensaver::{Screensaver, TimeDisplay};
use ssd1351::display::display::Ssd1351;
use time::OffsetDateTime;
use time_tz::{timezones::db::europe::BERLIN, OffsetDateTimeExt};
@ -37,8 +37,6 @@ pub type Oled = Ssd1351<SPIInterfaceNoCS<Spi, OutputPin>>;
pub type Rng = Xoroshiro128StarStar;
static BLACK: Rgb565 = Rgb565::new(0, 0, 0);
/// Delay after drawing a frame in milliseconds.
const FRAME_INTERVAL: u64 = 66;
fn main() {
if rppal::system::DeviceInfo::new().is_ok() {
@ -48,9 +46,7 @@ fn main() {
}
}
pub trait Context<D: DrawTarget<Color = Rgb565>> {
fn do_draw(&self, drawable: Box<dyn Draw<D>>);
pub trait Context {
fn do_action(&self, action: Action);
fn active_count(&self) -> usize;
@ -62,7 +58,7 @@ pub trait Context<D: DrawTarget<Color = Rgb565>> {
pub struct ContextDefault<D: DrawTarget<Color = Rgb565>> {
screensavers: Vec<Box<dyn Screensaver<D>>>,
scheduled: Vec<Box<dyn Schedule<D>>>,
scheduled: Vec<Box<dyn Schedule>>,
active: RefCell<Vec<Box<dyn Draw<D>>>>,
database: Rc<RefCell<Connection>>,
}
@ -74,17 +70,10 @@ impl<D: DrawTarget<Color = Rgb565>> ContextDefault<D> {
screensavers.push(Box::new(draw::Measurements::temps()));
screensavers.push(Box::new(draw::Measurements::events()));
let database = Connection::open("sensors.db").expect("failed to open database");
let mut scheduled = schedule::reminders();
scheduled.push(Box::new(GithubNotifications {
pat: env::var("GITHUB_PAT").expect("no env var GITHUB_PAT set"),
last_modified: RefCell::new(None),
last_call: RefCell::new(OffsetDateTime::now_utc().to_timezone(BERLIN) - time::Duration::seconds(50)),
}));
scheduled.push(Box::new(BearReminder::default()));
ContextDefault {
database: Rc::new(RefCell::new(database)),
screensavers,
scheduled,
scheduled: schedule::reminders(),
active: RefCell::new(vec![Box::new(TimeDisplay::new())]),
}
}
@ -123,11 +112,7 @@ impl<D: DrawTarget<Color = Rgb565>> ContextDefault<D> {
}
}
impl<D: DrawTarget<Color = Rgb565>> Context<D> for ContextDefault<D> {
fn do_draw(&self, drawable: Box<dyn Draw<D>>) {
self.active.borrow_mut().push(drawable);
}
impl<D: DrawTarget<Color = Rgb565>> Context for ContextDefault<D> {
fn do_action(&self, action: Action) {
match action {
Action::Screensaver(id) => {
@ -241,7 +226,7 @@ fn pc_main() {
.unwrap();
// redraw
if Instant::now().duration_since(start) > Duration::from_millis(iters * FRAME_INTERVAL) {
if Instant::now().duration_since(start) > Duration::from_millis(iters * 66) {
iters += 1;
buffer_dirty = ctx.loop_iter(&mut disp, &mut rng);
}
@ -443,6 +428,6 @@ fn main_loop(mut disp: Oled, mut ctx: ContextDefault<Oled>) {
if dirty {
let _ = disp.flush(); // ignore bus write errors, they are harmless
}
thread::sleep(Duration::from_millis(FRAME_INTERVAL));
thread::sleep(Duration::from_millis(66));
}
}

View File

@ -23,5 +23,5 @@ fn main() {
}
fn get_json(url: &str) -> Result<String, Box<dyn Error>> {
Ok(ureq::get(url).call()?.into_body().read_to_string()?)
Ok(ureq::get(url).call()?.into_string()?)
}

View File

@ -1,126 +0,0 @@
use std::cell::RefCell;
use color_space::{Hsv, ToRgb};
use embedded_graphics::{
mono_font::{iso_8859_10::FONT_8X13, MonoTextStyleBuilder},
pixelcolor::Rgb565,
prelude::{DrawTarget, Point, RgbColor},
text::Text,
Drawable,
};
use raspi_oled::github::get_new_notifications;
use crate::{
screensaver::{SimpleScreensaver, GITHUB},
Draw,
};
use super::Schedule;
pub struct GithubNotifications {
pub pat: String,
pub last_modified: RefCell<Option<String>>,
pub last_call: RefCell<time::OffsetDateTime>,
}
impl<D: DrawTarget<Color = Rgb565>> Schedule<D> for GithubNotifications {
fn check(&self, _ctx: &dyn crate::Context<D>, time: time::OffsetDateTime) -> bool {
let time_since_last = time - *self.last_call.borrow();
time_since_last.whole_minutes() >= 1
}
fn execute(&self, ctx: &dyn crate::Context<D>, time: time::OffsetDateTime) {
*self.last_call.borrow_mut() = time;
let last_modified = self.last_modified.borrow().clone();
if let Ok((notifications, last_modified)) = get_new_notifications(&self.pat, last_modified.as_deref()) {
*self.last_modified.borrow_mut() = last_modified;
let relevant: Vec<_> = notifications
.into_iter()
.filter(|x| x.reason != "state_change" && x.unread)
.collect();
if relevant.is_empty() {
return;
}
let max_lines = 8;
let max_line_length = 16;
let mut lines = vec![];
let mut relevant = relevant.into_iter();
while lines.len() < max_lines {
if let Some(x) = relevant.next() {
let url = x.subject.url;
let Some(url) = url else {
lines.push("no url".to_owned());
continue;
};
let parts: Vec<_> = url.split('/').collect();
if parts.len() < 8 {
lines.push("too few url parts".to_owned());
continue;
}
lines.push(format!("{} #{}", parts[5], parts[7]));
if lines.len() < max_lines {
let mut desc = format!(" {}", x.subject.title);
desc.truncate(desc.floor_char_boundary(max_line_length));
lines.push(desc);
}
} else {
break;
}
}
let remaining = relevant.count();
if remaining != 0 {
lines.push(format!("... {} more", remaining));
}
ctx.do_draw(Box::new(GithubNotificationsDraw {
calls: RefCell::new(0),
screen: &GITHUB,
lines,
}));
}
}
}
struct GithubNotificationsDraw {
calls: RefCell<usize>,
screen: &'static SimpleScreensaver,
lines: Vec<String>,
}
impl<D: DrawTarget<Color = Rgb565>> Draw<D> for GithubNotificationsDraw {
fn draw(&self, disp: &mut D, _rng: &mut crate::Rng) -> Result<bool, D::Error> {
let calls = *self.calls.borrow();
if calls < 40 {
let hue = calls as f64 / 40.0 * 360.0;
let hsv = Hsv::new(hue, 1.0, 1.0);
let rgb = hsv.to_rgb();
let r = rgb.r as u8 >> 3;
let g = rgb.g as u8 >> 2;
let b = rgb.b as u8 >> 3;
self.screen.draw_all_colored(disp, Rgb565::new(r, g, b))?;
} else {
disp.clear(Rgb565::BLACK)?;
// fit 9 lines
let text_style_clock = MonoTextStyleBuilder::new()
.font(&FONT_8X13)
.text_color(Rgb565::WHITE)
.build();
for (y, line) in self.lines.iter().enumerate() {
Text::new(line, Point::new(0, (12 + y * 14) as _), text_style_clock).draw(disp)?;
}
}
*self.calls.borrow_mut() += 1;
Ok(calls < 90)
}
fn expired(&self) -> bool {
*self.calls.borrow() > 90
}
fn as_any(&self) -> &dyn std::any::Any {
&*self
}
fn as_any_mut(&mut self) -> &mut dyn std::any::Any {
&mut *self
}
}

View File

@ -1,24 +1,18 @@
use embedded_graphics::{pixelcolor::Rgb565, prelude::DrawTarget};
use time::OffsetDateTime;
use crate::{action::Action, Context};
pub mod github_notifications;
/// Task to be executed at certain times.
/// Guaranteed to be checked at least once every minute.
pub trait Schedule<D>
where
D: DrawTarget<Color = Rgb565>,
{
fn check_and_do(&self, ctx: &dyn Context<D>, time: OffsetDateTime) {
pub trait Schedule {
fn check_and_do(&self, ctx: &dyn Context, time: OffsetDateTime) {
if self.check(ctx, time) {
self.execute(ctx, time);
}
}
fn check(&self, ctx: &dyn Context<D>, time: OffsetDateTime) -> bool;
fn execute(&self, ctx: &dyn Context<D>, time: OffsetDateTime);
fn check(&self, ctx: &dyn Context, time: OffsetDateTime) -> bool;
fn execute(&self, ctx: &dyn Context, time: OffsetDateTime);
}
#[derive(Debug, Clone, Copy)]
@ -29,12 +23,12 @@ pub struct Reminder {
should_beep: bool,
}
impl<D: DrawTarget<Color = Rgb565>> Schedule<D> for Reminder {
fn check(&self, ctx: &dyn Context<D>, time: OffsetDateTime) -> bool {
impl Schedule for Reminder {
fn check(&self, ctx: &dyn Context, time: OffsetDateTime) -> bool {
time.hour() == self.hour && time.minute() == self.minute && ctx.active_count() == 1
}
fn execute(&self, ctx: &dyn Context<D>, _time: OffsetDateTime) {
fn execute(&self, ctx: &dyn Context, _time: OffsetDateTime) {
if self.should_beep {
ctx.enable_pwm();
}
@ -57,6 +51,6 @@ static DUOLINGO: Reminder = Reminder::new(11, 40, Action::Screensaver("duolingo"
static DUOLINGO_NIGHT: Reminder = Reminder::new(23, 40, Action::Screensaver("duolingo"), false);
static FOOD: Reminder = Reminder::new(13, 15, Action::Screensaver("plate"), false);
pub fn reminders<D: DrawTarget<Color = Rgb565>>() -> Vec<Box<dyn Schedule<D>>> {
pub fn reminders() -> Vec<Box<dyn Schedule>> {
vec![Box::new(DUOLINGO), Box::new(DUOLINGO_NIGHT), Box::new(FOOD)]
}

Binary file not shown.

View File

@ -3,7 +3,6 @@ use std::cell::RefCell;
use std::sync::atomic::{AtomicU32, AtomicU64};
use embedded_graphics::mono_font::ascii::FONT_10X20;
use embedded_graphics::prelude::RgbColor;
use embedded_graphics::{
mono_font::MonoTextStyleBuilder,
pixelcolor::Rgb565,
@ -16,7 +15,6 @@ use rand_xoshiro::rand_core::RngCore;
use time::{Duration, OffsetDateTime};
use time_tz::{timezones::db::europe::BERLIN, OffsetDateTimeExt};
use crate::schedule::Schedule;
use crate::{Draw, Rng};
pub static SPEED: AtomicU64 = AtomicU64::new(32);
@ -104,44 +102,6 @@ impl SimpleScreensaver {
iters: AtomicU32::new(0),
}
}
pub fn draw_all_colored<D: DrawTarget<Color = Rgb565>>(&self, disp: &mut D, color: Rgb565) -> Result<(), D::Error> {
disp.fill_contiguous(
&Rectangle::new((0, 0).into(), (128, 128).into()),
(0..128 * 128).map(|idx| {
let (red, green, blue) = (self.data[3 * idx], self.data[3 * idx + 1], self.data[3 * idx + 2]);
let r = red >> 3;
let g = green >> 2;
let b = blue >> 3;
if (r, g, b) != (0, 0, 0) {
color
} else {
Rgb565::BLACK
}
}),
)?;
Ok(())
}
pub fn draw_all<D: DrawTarget<Color = Rgb565>>(&self, disp: &mut D, flipped: bool) -> Result<(), D::Error> {
disp.fill_contiguous(
&Rectangle::new((0, 0).into(), (128, 128).into()),
(0..128 * 128).map(|idx| {
let (mut red, mut green, mut blue) =
(self.data[3 * idx], self.data[3 * idx + 1], self.data[3 * idx + 2]);
if flipped {
red = 255 - red;
green = 255 - green;
blue = 255 - blue;
}
let r = red >> 3;
let g = green >> 2;
let b = blue >> 3;
Rgb565::new(r, g, b)
}),
)?;
Ok(())
}
}
static TIME_COLOR: Rgb565 = Rgb565::new(0b01_111, 0b011_111, 0b01_111);
@ -235,8 +195,6 @@ pub static RPI: SimpleScreensaver = SimpleScreensaver::new("rpi", include_bytes!
pub static DUOLINGO: SimpleScreensaver = SimpleScreensaver::new("duolingo", include_bytes!("./duolingo.raw"));
pub static SPAGHETTI: SimpleScreensaver = SimpleScreensaver::new("spaghetti", include_bytes!("./spaghetti.raw"));
pub static PLATE: SimpleScreensaver = SimpleScreensaver::new("plate", include_bytes!("./plate.raw"));
pub static GITHUB: SimpleScreensaver = SimpleScreensaver::new("github", include_bytes!("./github.raw"));
pub static TEDDY_BEAR: SimpleScreensaver = SimpleScreensaver::new("teddy_bear", include_bytes!("./teddy_bear.raw"));
pub fn screensavers<D: DrawTarget<Color = Rgb565>>() -> Vec<Box<dyn Screensaver<D>>> {
vec![
@ -247,52 +205,3 @@ pub fn screensavers<D: DrawTarget<Color = Rgb565>>() -> Vec<Box<dyn Screensaver<
Box::new(PLATE.clone()),
]
}
pub struct BearReminder;
impl Default for BearReminder {
fn default() -> Self {
Self {}
}
}
impl<D: DrawTarget<Color = Rgb565>> Schedule<D> for BearReminder {
fn check(&self, _ctx: &dyn crate::Context<D>, time: OffsetDateTime) -> bool {
time.hour() == 21 && time.minute() == 30 && time.to_julian_day() % 2 == 1
}
fn execute(&self, ctx: &dyn crate::Context<D>, _time: OffsetDateTime) {
ctx.do_draw(Box::new(BearDraw { calls: RefCell::new(0) }));
}
}
struct BearDraw {
calls: RefCell<usize>,
}
impl<D: DrawTarget<Color = Rgb565>> Draw<D> for BearDraw {
fn draw(&self, disp: &mut D, _rng: &mut Rng) -> Result<bool, <D as DrawTarget>::Error> {
let mut calls = self.calls.borrow_mut();
*calls += 1;
if *calls > 73 {
return Ok(false);
}
TEDDY_BEAR.draw_all(disp, *calls % 8 >= 4)?;
Ok(true)
}
fn expired(&self) -> bool {
*self.calls.borrow() > 110
}
fn as_any(&self) -> &dyn Any {
&*self
}
fn as_any_mut(&mut self) -> &mut dyn Any {
&mut *self
}
}

File diff suppressed because one or more lines are too long

View File

@ -64,7 +64,7 @@ fn main() {
.success();
let sync_good = if nixos_up {
if let Ok(x) = ureq::get("http://nixos.fritz.box:12783/").call() {
x.status().as_u16() < 400
x.status() < 400
} else {
false
}

View File

@ -1,222 +0,0 @@
use std::error::Error;
use serde::Deserialize;
use time::{macros::format_description, PrimitiveDateTime};
#[derive(Debug, Clone, PartialEq, Deserialize)]
#[non_exhaustive]
pub struct Notification {
pub id: String,
pub repository: Repository,
pub subject: Subject,
pub reason: String,
pub unread: bool,
pub updated_at: String,
pub last_read_at: Option<String>,
pub url: String,
}
#[derive(Debug, Clone, Eq, PartialEq, Deserialize)]
#[non_exhaustive]
pub struct Repository {
pub id: u64,
pub node_id: Option<String>,
pub name: String,
pub full_name: Option<String>,
pub owner: Option<serde_json::Value>,
pub private: Option<bool>,
pub html_url: Option<String>,
pub description: Option<String>,
pub fork: Option<bool>,
pub url: String,
pub archive_url: Option<String>,
pub assignees_url: Option<String>,
pub blobs_url: Option<String>,
pub branches_url: Option<String>,
pub collaborators_url: Option<String>,
pub comments_url: Option<String>,
pub commits_url: Option<String>,
pub compare_url: Option<String>,
pub contents_url: Option<String>,
pub contributors_url: Option<String>,
pub deployments_url: Option<String>,
pub downloads_url: Option<String>,
pub events_url: Option<String>,
pub forks_url: Option<String>,
pub git_commits_url: Option<String>,
pub git_refs_url: Option<String>,
pub git_tags_url: Option<String>,
pub git_url: Option<String>,
pub issue_comment_url: Option<String>,
pub issue_events_url: Option<String>,
pub issues_url: Option<String>,
pub keys_url: Option<String>,
pub labels_url: Option<String>,
pub languages_url: Option<String>,
pub merges_url: Option<String>,
pub milestones_url: Option<String>,
pub notifications_url: Option<String>,
pub pulls_url: Option<String>,
pub releases_url: Option<String>,
pub ssh_url: Option<String>,
pub stargazers_url: Option<String>,
pub statuses_url: Option<String>,
pub subscribers_url: Option<String>,
pub subscription_url: Option<String>,
pub tags_url: Option<String>,
pub teams_url: Option<String>,
pub trees_url: Option<String>,
pub clone_url: Option<String>,
pub mirror_url: Option<String>,
pub hooks_url: Option<String>,
pub svn_url: Option<String>,
pub homepage: Option<String>,
pub language: Option<::serde_json::Value>,
pub forks_count: Option<u32>,
pub stargazers_count: Option<u32>,
pub watchers_count: Option<u32>,
pub size: Option<u32>,
pub default_branch: Option<String>,
pub open_issues_count: Option<u32>,
pub is_template: Option<bool>,
pub topics: Option<Vec<String>>,
pub has_issues: Option<bool>,
pub has_projects: Option<bool>,
pub has_wiki: Option<bool>,
pub has_pages: Option<bool>,
pub has_downloads: Option<bool>,
pub archived: Option<bool>,
pub disabled: Option<bool>,
pub visibility: Option<String>,
pub pushed_at: Option<String>,
pub created_at: Option<String>,
pub updated_at: Option<String>,
pub permissions: Option<serde_json::Value>,
pub allow_rebase_merge: Option<bool>,
pub template_repository: Option<Box<Repository>>,
pub allow_squash_merge: Option<bool>,
pub allow_merge_commit: Option<bool>,
pub allow_update_branch: Option<bool>,
pub allow_forking: Option<bool>,
pub subscribers_count: Option<i64>,
pub network_count: Option<i64>,
pub license: Option<serde_json::Value>,
pub allow_auto_merge: Option<bool>,
pub delete_branch_on_merge: Option<bool>,
pub parent: Option<Box<Repository>>,
pub source: Option<Box<Repository>>,
}
#[derive(Debug, Clone, PartialEq, Deserialize)]
#[non_exhaustive]
pub struct Subject {
pub title: String,
pub url: Option<String>,
pub latest_comment_url: Option<String>,
pub r#type: String,
}
/// Get new notifications.
/// Returns: notifications and new last-modified value.
pub fn get_new_notifications(
pat: &str,
last_modified: Option<&str>,
) -> Result<(Vec<Notification>, Option<String>), Box<dyn Error>> {
let mut resp = ureq::get("https://api.github.com/notifications").header("Authorization", &format!("Bearer {pat}"));
if let Some(val) = last_modified {
resp = resp.header("If-Modified-Since", val);
}
let json = resp.call()?.into_body().read_to_string()?;
let items: Vec<Notification> = serde_json::from_str(&json)?;
let new_last_modified = items.get(0).map(|x| x.updated_at.clone());
let last_modified = if let Some(lm) = new_last_modified {
// parse and increase by five seconds
let format = format_description!("[year]-[month]-[day]T[hour]:[minute]:[second]Z");
let mut dt = PrimitiveDateTime::parse(&lm, format)?;
dt += time::Duration::seconds(5);
Some(dt.format(&format)?)
} else {
last_modified.map(|x| x.to_owned())
};
Ok((items, last_modified))
}

View File

@ -4,7 +4,6 @@ use std::{
time::{self, Duration},
};
#[cfg(feature = "pc")]
use embedded_graphics::{
draw_target::DrawTarget,
pixelcolor::Rgb565,
@ -18,8 +17,6 @@ use gpiocdev::{
#[cfg(feature = "pc")]
use image::{ImageBuffer, Rgb};
pub mod github;
#[cfg(feature = "pc")]
pub struct FrameOutput {
pub buffer: ImageBuffer<Rgb<u8>, Vec<u8>>,
@ -46,8 +43,7 @@ impl DrawTarget for FrameOutput {
{
for pos in pixels {
if pos.0.x < 0
|| pos.0.y < 0
|| pos.0.x as u32 >= self.buffer.width()
|| pos.0.y < 0 || pos.0.x as u32 >= self.buffer.width()
|| pos.0.y as u32 >= self.buffer.height()
{
continue;