From dfb9426c18f64f13f551da7e97db07e1c45e6147 Mon Sep 17 00:00:00 2001 From: Marcus Ramse Date: Sun, 13 Oct 2024 11:08:50 +0000 Subject: [PATCH] app: headset connection notifications --- ggoled_app/assets/headset_connected.png | Bin 0 -> 172 bytes ggoled_app/assets/headset_disconnected.png | Bin 0 -> 177 bytes ggoled_app/src/main.rs | 55 ++++++++++++++++++--- ggoled_cli/src/main.rs | 5 +- ggoled_draw/src/lib.rs | 51 ++++++++++--------- ggoled_lib/src/lib.rs | 11 +++-- 6 files changed, 84 insertions(+), 38 deletions(-) create mode 100644 ggoled_app/assets/headset_connected.png create mode 100644 ggoled_app/assets/headset_disconnected.png diff --git a/ggoled_app/assets/headset_connected.png b/ggoled_app/assets/headset_connected.png new file mode 100644 index 0000000000000000000000000000000000000000..0e939b01eaaf1866269fe58f9ed79dad1d2fe8bf GIT binary patch literal 172 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`jKx9jP7LeL$-D$|(mY)pLo9la zPIlyDP~>3dzWKj?=R6+{rX&j;_od02feZr8mpUaF?CKq^wYjLUPq2|%#GcciCO(z< zbLlzh3UFjr!7; TXC4d(+Q{JP>gTe~DWM4fN1Zt% literal 0 HcmV?d00001 diff --git a/ggoled_app/assets/headset_disconnected.png b/ggoled_app/assets/headset_disconnected.png new file mode 100644 index 0000000000000000000000000000000000000000..36d7e0667c0bb3f367f878a73f91ad1f13a12479 GIT binary patch literal 177 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`jKx9jP7LeL$-D$|vOQfKLo9la zPB!E_5WwMd@yoyPWx>LWA3O>;zTVF9Gc!YB#~u^r1DEAb#0I!jFq{lrz;TI~iXHWpCc3p<8ld+x$W|)|=C2-)B*;+?=9Ya*|Jp@!b066WTi_B>ed+ a9lztNodU~)vV}lv89ZJ6T-G@yGywqd5J9d0 literal 0 HcmV?d00001 diff --git a/ggoled_app/src/main.rs b/ggoled_app/src/main.rs index 0c2ed3e..0982e48 100644 --- a/ggoled_app/src/main.rs +++ b/ggoled_app/src/main.rs @@ -3,18 +3,19 @@ mod os; use chrono::{Local, TimeDelta, Timelike}; -use ggoled_draw::{DrawDevice, DrawEvent, LayerId, ShiftMode, TextRenderer}; +use ggoled_draw::{bitmap_from_memory, DrawDevice, DrawEvent, LayerId, ShiftMode, TextRenderer}; use ggoled_lib::Device; use os::{dispatch_system_events, get_idle_seconds, Media, MediaControl}; use rfd::{MessageDialog, MessageLevel}; use serde::{Deserialize, Serialize}; -use std::{fmt::Debug, path::PathBuf, thread::sleep, time::Duration}; +use std::{fmt::Debug, path::PathBuf, sync::Arc, thread::sleep, time::Duration}; use tray_icon::{ menu::{CheckMenuItem, Menu, MenuEvent, MenuItem, PredefinedMenuItem, Submenu}, Icon, TrayIconBuilder, }; const IDLE_TIMEOUT_SECS: usize = 60; +const NOTIF_DUR: Duration = Duration::from_secs(5); #[derive(Serialize, Deserialize, Default, Clone, Copy)] enum ConfigShiftMode { @@ -40,20 +41,22 @@ struct ConfigFont { #[derive(Serialize, Deserialize)] #[serde(default)] struct Config { + font: Option, show_time: bool, show_media: bool, idle_timeout: bool, oled_shift: ConfigShiftMode, - font: Option, + show_notifications: bool, } impl Default for Config { fn default() -> Self { Self { + font: None, show_time: true, show_media: true, idle_timeout: true, oled_shift: ConfigShiftMode::default(), - font: None, + show_notifications: true, } } } @@ -113,6 +116,7 @@ fn main() { // Create tray icon with menu let tm_time_check = CheckMenuItem::new("Show time", true, config.show_time, None); let tm_media_check = CheckMenuItem::new("Show playing media", true, config.show_media, None); + let tm_notif_check = CheckMenuItem::new("Show notifications", true, config.show_notifications, None); let tm_idle_check = CheckMenuItem::new("Screensaver when idle", true, config.idle_timeout, None); let tm_oledshift_off = CheckMenuItem::new("Off", true, false, None); let tm_oledshift_simple = CheckMenuItem::new("Simple", true, false, None); @@ -127,6 +131,7 @@ fn main() { &PredefinedMenuItem::separator(), &tm_time_check, &tm_media_check, + &tm_notif_check, &tm_idle_check, &Submenu::with_items("OLED screen shift", true, &[&tm_oledshift_off, &tm_oledshift_simple]).unwrap(), &PredefinedMenuItem::separator(), @@ -156,17 +161,26 @@ fn main() { .unwrap(); }; update_connection(true); - update_oledshift(&mut dev, config.oled_shift); - dev.play(); - let mgr = MediaControl::new(); + // Load icons + let icon_hs_connect = + Arc::new(bitmap_from_memory(include_bytes!("../assets/headset_connected.png"), 0x80).unwrap()); + let icon_hs_disconnect = + Arc::new(bitmap_from_memory(include_bytes!("../assets/headset_disconnected.png"), 0x80).unwrap()); + // State + let mgr = MediaControl::new(); let menu_channel = MenuEvent::receiver(); let mut last_time = Local::now() - TimeDelta::seconds(1); let mut last_media: Option = None; let mut time_layers: Vec = vec![]; let mut media_layers: Vec = vec![]; + let mut notif_layer: Option = None; + let mut notif_expiry = Local::now(); + + // Go! + dev.play(); 'main: loop { // Window event loop is required to get tray-icon working dispatch_system_events(); @@ -178,6 +192,8 @@ fn main() { config.show_time = tm_time_check.is_checked(); } else if event.id == tm_media_check.id() { config.show_media = tm_media_check.is_checked(); + } else if event.id == tm_notif_check.id() { + config.show_notifications = tm_notif_check.is_checked(); } else if event.id == tm_idle_check.id() { config.idle_timeout = tm_idle_check.is_checked(); } else if event.id == tm_oledshift_off.id() { @@ -204,6 +220,23 @@ fn main() { DrawEvent::DeviceDisconnected => update_connection(false), DrawEvent::DeviceReconnected => update_connection(true), DrawEvent::DeviceEvent(event) => match event { + ggoled_lib::DeviceEvent::HeadsetConnection { connected } => { + if config.show_notifications { + notif_layer = Some( + dev.add_layer(ggoled_draw::DrawLayer::Image { + bitmap: (if connected { + &icon_hs_connect + } else { + &icon_hs_disconnect + }) + .clone(), + x: 8, + y: 8, + }), + ); + notif_expiry = Local::now() + NOTIF_DUR; + } + } _ => {} }, } @@ -214,6 +247,14 @@ fn main() { if time.second() != last_time.second() || config_updated { last_time = time; + // Remove expired notifications + if let Some(id) = notif_layer { + if time >= notif_expiry { + dev.remove_layer(id); + notif_layer = None; + } + } + // Check if idle let idle_seconds = get_idle_seconds(); if config.idle_timeout && idle_seconds >= IDLE_TIMEOUT_SECS { diff --git a/ggoled_cli/src/main.rs b/ggoled_cli/src/main.rs index 73dd918..9a33e31 100644 --- a/ggoled_cli/src/main.rs +++ b/ggoled_cli/src/main.rs @@ -6,6 +6,7 @@ use ggoled_draw::DrawDevice; use ggoled_lib::Bitmap; use ggoled_lib::Device; use spin_sleep::sleep; +use std::sync::Arc; use std::time::Instant; use std::{ io::{stdin, Read}, @@ -198,7 +199,7 @@ fn main() { let bitmap = if path == "-" { let mut buf = Vec::::new(); stdin().read_to_end(&mut buf).expect("Failed to read from stdin"); - bitmap_from_memory(&buf, image_args.threshold).expect("Failed to read image from stdin") + Arc::new(bitmap_from_memory(&buf, image_args.threshold).expect("Failed to read image from stdin")) } else { let mut frames = decode_frames(&path, image_args.threshold); if frames.len() != 1 { @@ -220,7 +221,7 @@ fn main() { panic!("No image paths"); } let period = framerate.map(|f| Duration::from_secs(1).div(f)); - let bitmaps: Vec<(Bitmap, Duration)> = paths + let bitmaps: Vec<(Arc, Duration)> = paths .iter() .flat_map(|path| { decode_frames(path, image_args.threshold).into_iter().map(|frame| { diff --git a/ggoled_draw/src/lib.rs b/ggoled_draw/src/lib.rs index efeee05..4ccead1 100644 --- a/ggoled_draw/src/lib.rs +++ b/ggoled_draw/src/lib.rs @@ -94,8 +94,9 @@ pub fn bitmap_from_memory(buf: &[u8], threshold: u8) -> anyhow::Result { Ok(bitmap_from_dynimage(&img, threshold)) } +#[derive(Clone)] pub struct Frame { - pub bitmap: Bitmap, + pub bitmap: Arc, pub delay: Option, } @@ -107,7 +108,7 @@ pub fn decode_frames(path: &str, threshold: u8) -> Vec { frames .map(|frame| { let frame = frame.expect("Failed to decode gif frame"); - let bitmap = bitmap_from_image(frame.buffer(), threshold); + let bitmap = Arc::new(bitmap_from_image(frame.buffer(), threshold)); Frame { bitmap, delay: Some(Duration::from_millis(frame.delay().numer_denom_ms().0 as u64)), @@ -116,7 +117,7 @@ pub fn decode_frames(path: &str, threshold: u8) -> Vec { .collect() } else { let img = reader.decode().expect("Failed to decode image"); - let bitmap = bitmap_from_dynimage(&img, threshold); + let bitmap = Arc::new(bitmap_from_dynimage(&img, threshold)); vec![Frame { bitmap, delay: None }] } } @@ -128,23 +129,21 @@ impl LayerId { LayerId(0) } } -pub struct Pos { - pub x: isize, - pub y: isize, -} pub enum DrawLayer { Image { - bitmap: Bitmap, - pos: Pos, + bitmap: Arc, + x: isize, + y: isize, }, Animation { frames: Vec, - pos: Pos, + x: isize, + y: isize, follow_fps: bool, }, Scroll { - bitmap: Bitmap, + bitmap: Arc, y: isize, }, } @@ -252,15 +251,16 @@ fn run_draw_device_thread( let mut layers = layers.lock().unwrap(); for (_, state) in layers.iter_mut() { match &state.layer { - DrawLayer::Image { bitmap, pos } => screen.blit(bitmap, pos.x + shift_x, pos.y + shift_y, false), + DrawLayer::Image { bitmap, x, y } => screen.blit(bitmap, x + shift_x, y + shift_y, false), DrawLayer::Animation { frames, - pos, + x, + y, follow_fps, } => { if !frames.is_empty() { let frame = &frames[state.anim.ticks % frames.len()]; - screen.blit(&frame.bitmap, pos.x + shift_x, pos.y + shift_y, false); + screen.blit(&frame.bitmap, x + shift_x, y + shift_y, false); if *follow_fps { state.anim.ticks += 1; } else if time >= state.anim.next_update { @@ -369,11 +369,11 @@ impl DrawDevice { pub fn poll_event(&mut self) -> DrawEvent { self.event_receiver.recv().unwrap() } - pub fn center_bitmap(&self, bitmap: &Bitmap) -> Pos { - Pos { - x: (self.width as isize - bitmap.w as isize) / 2, - y: (self.height as isize - bitmap.h as isize) / 2, - } + pub fn center_bitmap(&self, bitmap: &Bitmap) -> (isize, isize) { + ( + (self.width as isize - bitmap.w as isize) / 2, + (self.height as isize - bitmap.h as isize) / 2, + ) } fn add_layer_locked(&mut self, layers: &mut MutexGuard<'_, LayerMap>, layer: DrawLayer) -> LayerId { self.layer_counter += 1; @@ -412,7 +412,12 @@ impl DrawDevice { pub fn add_text(&mut self, text: &str, x: Option, y: Option) -> Vec { let layers = self.layers.clone(); let mut layers = layers.lock().unwrap(); - let bitmaps = self.texter.render_lines(text); + let bitmaps: Vec<_> = self + .texter + .render_lines(text) + .into_iter() + .map(|b| Arc::new(b)) + .collect(); let line_height = self.texter.line_height(); let center_y: isize = (self.height as isize - (line_height * bitmaps.len()) as isize) / 2; bitmaps @@ -428,10 +433,8 @@ impl DrawDevice { &mut layers, DrawLayer::Image { bitmap, - pos: Pos { - x: x.unwrap_or(center.x), - y, - }, + x: x.unwrap_or(center.0), + y, }, ) } diff --git a/ggoled_lib/src/lib.rs b/ggoled_lib/src/lib.rs index 2ff667f..4374cfa 100644 --- a/ggoled_lib/src/lib.rs +++ b/ggoled_lib/src/lib.rs @@ -22,8 +22,9 @@ struct ReportDrawable<'a> { #[derive(Debug)] pub enum DeviceEvent { - VolumeChanged { volume: u8 }, - BatteryChanged { headset: u8, charging: u8 }, + Volume { volume: u8 }, + Battery { headset: u8, charging: u8 }, + HeadsetConnection { connected: bool }, } pub struct Device { @@ -206,11 +207,11 @@ impl Device { return None; } Some(match buf[1] { - 0x25 => DeviceEvent::VolumeChanged { + 0x25 => DeviceEvent::Volume { volume: 0x38u8.saturating_sub(buf[2]), }, - // Connection/Disconnection: 0xb5 => {} - 0xb7 => DeviceEvent::BatteryChanged { + 0xb5 => DeviceEvent::HeadsetConnection { connected: buf[4] == 8 }, + 0xb7 => DeviceEvent::Battery { headset: buf[2], charging: buf[3], // NOTE: there's a chance `buf[4]` represents the max value, but i don't have any other devices to test with