mirror of
https://github.com/FliegendeWurst/raspi-oled.git
synced 2024-11-21 18:04:58 +00:00
Compare commits
6 Commits
ad9cea9378
...
9c7dc2a897
Author | SHA1 | Date | |
---|---|---|---|
|
9c7dc2a897 | ||
|
9293d327e0 | ||
|
a2eaa5bd52 | ||
|
6e0dbcf006 | ||
|
13325554a4 | ||
|
efac4f49a7 |
1340
Cargo.lock
generated
1340
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
11
Cargo.toml
11
Cargo.toml
@ -11,8 +11,8 @@ embedded-graphics = "0.8.1"
|
|||||||
linux-embedded-hal = "0.3.0"
|
linux-embedded-hal = "0.3.0"
|
||||||
embedded-hal = "0.2.5"
|
embedded-hal = "0.2.5"
|
||||||
libc = "0.2.98"
|
libc = "0.2.98"
|
||||||
rusqlite = "0.27.0"
|
rusqlite = "0.32.1"
|
||||||
time = { version = "0.3.9", features = ["parsing"] }
|
time = { version = "0.3.9", features = ["parsing", "formatting"] }
|
||||||
time-tz = "2"
|
time-tz = "2"
|
||||||
image = { version = "0.24.1", optional = true }
|
image = { version = "0.24.1", optional = true }
|
||||||
serde_json = "1.0.79"
|
serde_json = "1.0.79"
|
||||||
@ -21,19 +21,20 @@ serde = "1.0.136"
|
|||||||
rppal = { version = "0.14.1", features = ["hal"] }
|
rppal = { version = "0.14.1", features = ["hal"] }
|
||||||
ssd1351 = { git = "https://github.com/FliegendeWurst/ssd1351-rust", rev = "3de5be50bd9a59391c669aec8357923a56d121f6" }
|
ssd1351 = { git = "https://github.com/FliegendeWurst/ssd1351-rust", rev = "3de5be50bd9a59391c669aec8357923a56d121f6" }
|
||||||
display-interface-spi = "0.4.1"
|
display-interface-spi = "0.4.1"
|
||||||
ureq = { version = "2.4.0", default-features = false }
|
ureq = { version = "=3.0.0-rc2", default-features = false, features = ["rustls"] }
|
||||||
winit = { version = "0.28.7", optional = true }
|
winit = { version = "0.28.7", optional = true }
|
||||||
softbuffer = { version = "0.3.1", optional = true }
|
softbuffer = { version = "0.3.1", optional = true }
|
||||||
rand_xoshiro = "0.6.0"
|
rand_xoshiro = "0.6.0"
|
||||||
gpiocdev = "0.6.0"
|
gpiocdev = "0.7.2"
|
||||||
rpassword = "7.2.0"
|
rpassword = "7.2.0"
|
||||||
andotp-import = "0.1.0"
|
andotp-import = "0.1.0"
|
||||||
totp-rs = "5.4.0"
|
totp-rs = "5.4.0"
|
||||||
|
color_space = "0.5.4"
|
||||||
#gpio-am2302-rs = { git = "https://github.com/FliegendeWurst/gpio-am2302-rs" }
|
#gpio-am2302-rs = { git = "https://github.com/FliegendeWurst/gpio-am2302-rs" }
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
pc = ["winit", "softbuffer", "image"]
|
pc = ["winit", "softbuffer", "image"]
|
||||||
default = [ "pc" ]
|
default = ["pc"]
|
||||||
|
|
||||||
[profile.release]
|
[profile.release]
|
||||||
codegen-units = 1
|
codegen-units = 1
|
||||||
|
@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
rustPlatform.buildRustPackage {
|
rustPlatform.buildRustPackage {
|
||||||
pname = "raspi-oled";
|
pname = "raspi-oled";
|
||||||
version = "unstable-infdev-4";
|
version = "unstable-infdev-7";
|
||||||
|
|
||||||
src = ./.;
|
src = ./.;
|
||||||
|
|
||||||
@ -16,7 +16,7 @@ rustPlatform.buildRustPackage {
|
|||||||
|
|
||||||
nativeBuildInputs = [ pkg-config ];
|
nativeBuildInputs = [ pkg-config ];
|
||||||
|
|
||||||
cargoBuildFlags = [ "--no-default-features" "--bin" "take_measurement" ];
|
cargoBuildFlags = [ "--no-default-features" "--bin" "main_loop" ];
|
||||||
|
|
||||||
buildInputs = [ sqlite ];
|
buildInputs = [ sqlite ];
|
||||||
|
|
||||||
|
@ -321,8 +321,8 @@ impl<D: DrawTarget<Color = Rgb565>> Draw<D> for Measurements {
|
|||||||
}
|
}
|
||||||
if time_until_first.is_none()
|
if time_until_first.is_none()
|
||||||
&& (i > 0
|
&& (i > 0
|
||||||
|| event.1 > time.hour() as i32 || (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.2 >= time.minute() as i32))
|
||||||
{
|
{
|
||||||
time_until_first = Some(
|
time_until_first = Some(
|
||||||
((i * 24 + event.1) * 60 + event.2) * 60
|
((i * 24 + event.1) * 60 + event.2) * 60
|
||||||
|
@ -22,8 +22,8 @@ use rppal::{
|
|||||||
spi::{Bus, Mode, SlaveSelect, Spi},
|
spi::{Bus, Mode, SlaveSelect, Spi},
|
||||||
};
|
};
|
||||||
use rusqlite::Connection;
|
use rusqlite::Connection;
|
||||||
use schedule::Schedule;
|
use schedule::{github_notifications::GithubNotifications, Schedule};
|
||||||
use screensaver::{Screensaver, TimeDisplay};
|
use screensaver::{BearReminder, Screensaver, TimeDisplay};
|
||||||
use ssd1351::display::display::Ssd1351;
|
use ssd1351::display::display::Ssd1351;
|
||||||
use time::OffsetDateTime;
|
use time::OffsetDateTime;
|
||||||
use time_tz::{timezones::db::europe::BERLIN, OffsetDateTimeExt};
|
use time_tz::{timezones::db::europe::BERLIN, OffsetDateTimeExt};
|
||||||
@ -37,6 +37,8 @@ pub type Oled = Ssd1351<SPIInterfaceNoCS<Spi, OutputPin>>;
|
|||||||
pub type Rng = Xoroshiro128StarStar;
|
pub type Rng = Xoroshiro128StarStar;
|
||||||
|
|
||||||
static BLACK: Rgb565 = Rgb565::new(0, 0, 0);
|
static BLACK: Rgb565 = Rgb565::new(0, 0, 0);
|
||||||
|
/// Delay after drawing a frame in milliseconds.
|
||||||
|
const FRAME_INTERVAL: u64 = 66;
|
||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
if rppal::system::DeviceInfo::new().is_ok() {
|
if rppal::system::DeviceInfo::new().is_ok() {
|
||||||
@ -46,7 +48,9 @@ fn main() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub trait Context {
|
pub trait Context<D: DrawTarget<Color = Rgb565>> {
|
||||||
|
fn do_draw(&self, drawable: Box<dyn Draw<D>>);
|
||||||
|
|
||||||
fn do_action(&self, action: Action);
|
fn do_action(&self, action: Action);
|
||||||
|
|
||||||
fn active_count(&self) -> usize;
|
fn active_count(&self) -> usize;
|
||||||
@ -58,7 +62,7 @@ pub trait Context {
|
|||||||
|
|
||||||
pub struct ContextDefault<D: DrawTarget<Color = Rgb565>> {
|
pub struct ContextDefault<D: DrawTarget<Color = Rgb565>> {
|
||||||
screensavers: Vec<Box<dyn Screensaver<D>>>,
|
screensavers: Vec<Box<dyn Screensaver<D>>>,
|
||||||
scheduled: Vec<Box<dyn Schedule>>,
|
scheduled: Vec<Box<dyn Schedule<D>>>,
|
||||||
active: RefCell<Vec<Box<dyn Draw<D>>>>,
|
active: RefCell<Vec<Box<dyn Draw<D>>>>,
|
||||||
database: Rc<RefCell<Connection>>,
|
database: Rc<RefCell<Connection>>,
|
||||||
}
|
}
|
||||||
@ -70,10 +74,17 @@ impl<D: DrawTarget<Color = Rgb565>> ContextDefault<D> {
|
|||||||
screensavers.push(Box::new(draw::Measurements::temps()));
|
screensavers.push(Box::new(draw::Measurements::temps()));
|
||||||
screensavers.push(Box::new(draw::Measurements::events()));
|
screensavers.push(Box::new(draw::Measurements::events()));
|
||||||
let database = Connection::open("sensors.db").expect("failed to open database");
|
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 {
|
ContextDefault {
|
||||||
database: Rc::new(RefCell::new(database)),
|
database: Rc::new(RefCell::new(database)),
|
||||||
screensavers,
|
screensavers,
|
||||||
scheduled: schedule::reminders(),
|
scheduled,
|
||||||
active: RefCell::new(vec![Box::new(TimeDisplay::new())]),
|
active: RefCell::new(vec![Box::new(TimeDisplay::new())]),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -112,7 +123,11 @@ impl<D: DrawTarget<Color = Rgb565>> ContextDefault<D> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<D: DrawTarget<Color = Rgb565>> Context for 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);
|
||||||
|
}
|
||||||
|
|
||||||
fn do_action(&self, action: Action) {
|
fn do_action(&self, action: Action) {
|
||||||
match action {
|
match action {
|
||||||
Action::Screensaver(id) => {
|
Action::Screensaver(id) => {
|
||||||
@ -226,7 +241,7 @@ fn pc_main() {
|
|||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
// redraw
|
// redraw
|
||||||
if Instant::now().duration_since(start) > Duration::from_millis(iters * 66) {
|
if Instant::now().duration_since(start) > Duration::from_millis(iters * FRAME_INTERVAL) {
|
||||||
iters += 1;
|
iters += 1;
|
||||||
buffer_dirty = ctx.loop_iter(&mut disp, &mut rng);
|
buffer_dirty = ctx.loop_iter(&mut disp, &mut rng);
|
||||||
}
|
}
|
||||||
@ -428,6 +443,6 @@ fn main_loop(mut disp: Oled, mut ctx: ContextDefault<Oled>) {
|
|||||||
if dirty {
|
if dirty {
|
||||||
let _ = disp.flush(); // ignore bus write errors, they are harmless
|
let _ = disp.flush(); // ignore bus write errors, they are harmless
|
||||||
}
|
}
|
||||||
thread::sleep(Duration::from_millis(66));
|
thread::sleep(Duration::from_millis(FRAME_INTERVAL));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -23,5 +23,5 @@ fn main() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn get_json(url: &str) -> Result<String, Box<dyn Error>> {
|
fn get_json(url: &str) -> Result<String, Box<dyn Error>> {
|
||||||
Ok(ureq::get(url).call()?.into_string()?)
|
Ok(ureq::get(url).call()?.into_body().read_to_string()?)
|
||||||
}
|
}
|
||||||
|
126
src/bin/schedule/github_notifications.rs
Normal file
126
src/bin/schedule/github_notifications.rs
Normal file
@ -0,0 +1,126 @@
|
|||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
@ -1,18 +1,24 @@
|
|||||||
|
use embedded_graphics::{pixelcolor::Rgb565, prelude::DrawTarget};
|
||||||
use time::OffsetDateTime;
|
use time::OffsetDateTime;
|
||||||
|
|
||||||
use crate::{action::Action, Context};
|
use crate::{action::Action, Context};
|
||||||
|
|
||||||
|
pub mod github_notifications;
|
||||||
|
|
||||||
/// Task to be executed at certain times.
|
/// Task to be executed at certain times.
|
||||||
/// Guaranteed to be checked at least once every minute.
|
/// Guaranteed to be checked at least once every minute.
|
||||||
pub trait Schedule {
|
pub trait Schedule<D>
|
||||||
fn check_and_do(&self, ctx: &dyn Context, time: OffsetDateTime) {
|
where
|
||||||
|
D: DrawTarget<Color = Rgb565>,
|
||||||
|
{
|
||||||
|
fn check_and_do(&self, ctx: &dyn Context<D>, time: OffsetDateTime) {
|
||||||
if self.check(ctx, time) {
|
if self.check(ctx, time) {
|
||||||
self.execute(ctx, time);
|
self.execute(ctx, time);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn check(&self, ctx: &dyn Context, time: OffsetDateTime) -> bool;
|
fn check(&self, ctx: &dyn Context<D>, time: OffsetDateTime) -> bool;
|
||||||
fn execute(&self, ctx: &dyn Context, time: OffsetDateTime);
|
fn execute(&self, ctx: &dyn Context<D>, time: OffsetDateTime);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy)]
|
#[derive(Debug, Clone, Copy)]
|
||||||
@ -23,12 +29,12 @@ pub struct Reminder {
|
|||||||
should_beep: bool,
|
should_beep: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Schedule for Reminder {
|
impl<D: DrawTarget<Color = Rgb565>> Schedule<D> for Reminder {
|
||||||
fn check(&self, ctx: &dyn Context, time: OffsetDateTime) -> bool {
|
fn check(&self, ctx: &dyn Context<D>, time: OffsetDateTime) -> bool {
|
||||||
time.hour() == self.hour && time.minute() == self.minute && ctx.active_count() == 1
|
time.hour() == self.hour && time.minute() == self.minute && ctx.active_count() == 1
|
||||||
}
|
}
|
||||||
|
|
||||||
fn execute(&self, ctx: &dyn Context, _time: OffsetDateTime) {
|
fn execute(&self, ctx: &dyn Context<D>, _time: OffsetDateTime) {
|
||||||
if self.should_beep {
|
if self.should_beep {
|
||||||
ctx.enable_pwm();
|
ctx.enable_pwm();
|
||||||
}
|
}
|
||||||
@ -51,6 +57,6 @@ static DUOLINGO: Reminder = Reminder::new(11, 40, Action::Screensaver("duolingo"
|
|||||||
static DUOLINGO_NIGHT: Reminder = Reminder::new(23, 40, Action::Screensaver("duolingo"), false);
|
static DUOLINGO_NIGHT: Reminder = Reminder::new(23, 40, Action::Screensaver("duolingo"), false);
|
||||||
static FOOD: Reminder = Reminder::new(13, 15, Action::Screensaver("plate"), false);
|
static FOOD: Reminder = Reminder::new(13, 15, Action::Screensaver("plate"), false);
|
||||||
|
|
||||||
pub fn reminders() -> Vec<Box<dyn Schedule>> {
|
pub fn reminders<D: DrawTarget<Color = Rgb565>>() -> Vec<Box<dyn Schedule<D>>> {
|
||||||
vec![Box::new(DUOLINGO), Box::new(DUOLINGO_NIGHT), Box::new(FOOD)]
|
vec![Box::new(DUOLINGO), Box::new(DUOLINGO_NIGHT), Box::new(FOOD)]
|
||||||
}
|
}
|
||||||
|
BIN
src/bin/screensaver/github.raw
Normal file
BIN
src/bin/screensaver/github.raw
Normal file
Binary file not shown.
@ -3,6 +3,7 @@ use std::cell::RefCell;
|
|||||||
use std::sync::atomic::{AtomicU32, AtomicU64};
|
use std::sync::atomic::{AtomicU32, AtomicU64};
|
||||||
|
|
||||||
use embedded_graphics::mono_font::ascii::FONT_10X20;
|
use embedded_graphics::mono_font::ascii::FONT_10X20;
|
||||||
|
use embedded_graphics::prelude::RgbColor;
|
||||||
use embedded_graphics::{
|
use embedded_graphics::{
|
||||||
mono_font::MonoTextStyleBuilder,
|
mono_font::MonoTextStyleBuilder,
|
||||||
pixelcolor::Rgb565,
|
pixelcolor::Rgb565,
|
||||||
@ -15,6 +16,7 @@ use rand_xoshiro::rand_core::RngCore;
|
|||||||
use time::{Duration, OffsetDateTime};
|
use time::{Duration, OffsetDateTime};
|
||||||
use time_tz::{timezones::db::europe::BERLIN, OffsetDateTimeExt};
|
use time_tz::{timezones::db::europe::BERLIN, OffsetDateTimeExt};
|
||||||
|
|
||||||
|
use crate::schedule::Schedule;
|
||||||
use crate::{Draw, Rng};
|
use crate::{Draw, Rng};
|
||||||
|
|
||||||
pub static SPEED: AtomicU64 = AtomicU64::new(32);
|
pub static SPEED: AtomicU64 = AtomicU64::new(32);
|
||||||
@ -102,6 +104,44 @@ impl SimpleScreensaver {
|
|||||||
iters: AtomicU32::new(0),
|
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);
|
static TIME_COLOR: Rgb565 = Rgb565::new(0b01_111, 0b011_111, 0b01_111);
|
||||||
@ -195,6 +235,8 @@ pub static RPI: SimpleScreensaver = SimpleScreensaver::new("rpi", include_bytes!
|
|||||||
pub static DUOLINGO: SimpleScreensaver = SimpleScreensaver::new("duolingo", include_bytes!("./duolingo.raw"));
|
pub static DUOLINGO: SimpleScreensaver = SimpleScreensaver::new("duolingo", include_bytes!("./duolingo.raw"));
|
||||||
pub static SPAGHETTI: SimpleScreensaver = SimpleScreensaver::new("spaghetti", include_bytes!("./spaghetti.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 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>>> {
|
pub fn screensavers<D: DrawTarget<Color = Rgb565>>() -> Vec<Box<dyn Screensaver<D>>> {
|
||||||
vec![
|
vec![
|
||||||
@ -205,3 +247,52 @@ pub fn screensavers<D: DrawTarget<Color = Rgb565>>() -> Vec<Box<dyn Screensaver<
|
|||||||
Box::new(PLATE.clone()),
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
5
src/bin/screensaver/teddy_bear.raw
Normal file
5
src/bin/screensaver/teddy_bear.raw
Normal file
File diff suppressed because one or more lines are too long
@ -64,7 +64,7 @@ fn main() {
|
|||||||
.success();
|
.success();
|
||||||
let sync_good = if nixos_up {
|
let sync_good = if nixos_up {
|
||||||
if let Ok(x) = ureq::get("http://nixos.fritz.box:12783/").call() {
|
if let Ok(x) = ureq::get("http://nixos.fritz.box:12783/").call() {
|
||||||
x.status() < 400
|
x.status().as_u16() < 400
|
||||||
} else {
|
} else {
|
||||||
false
|
false
|
||||||
}
|
}
|
||||||
|
222
src/github.rs
Normal file
222
src/github.rs
Normal file
@ -0,0 +1,222 @@
|
|||||||
|
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))
|
||||||
|
}
|
@ -4,6 +4,7 @@ use std::{
|
|||||||
time::{self, Duration},
|
time::{self, Duration},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
#[cfg(feature = "pc")]
|
||||||
use embedded_graphics::{
|
use embedded_graphics::{
|
||||||
draw_target::DrawTarget,
|
draw_target::DrawTarget,
|
||||||
pixelcolor::Rgb565,
|
pixelcolor::Rgb565,
|
||||||
@ -17,6 +18,8 @@ use gpiocdev::{
|
|||||||
#[cfg(feature = "pc")]
|
#[cfg(feature = "pc")]
|
||||||
use image::{ImageBuffer, Rgb};
|
use image::{ImageBuffer, Rgb};
|
||||||
|
|
||||||
|
pub mod github;
|
||||||
|
|
||||||
#[cfg(feature = "pc")]
|
#[cfg(feature = "pc")]
|
||||||
pub struct FrameOutput {
|
pub struct FrameOutput {
|
||||||
pub buffer: ImageBuffer<Rgb<u8>, Vec<u8>>,
|
pub buffer: ImageBuffer<Rgb<u8>, Vec<u8>>,
|
||||||
@ -43,7 +46,8 @@ impl DrawTarget for FrameOutput {
|
|||||||
{
|
{
|
||||||
for pos in pixels {
|
for pos in pixels {
|
||||||
if pos.0.x < 0
|
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()
|
|| pos.0.y as u32 >= self.buffer.height()
|
||||||
{
|
{
|
||||||
continue;
|
continue;
|
||||||
|
Loading…
Reference in New Issue
Block a user