Skip to content

Commit

Permalink
app: headset connection notifications
Browse files Browse the repository at this point in the history
  • Loading branch information
JerwuQu committed Oct 13, 2024
1 parent 0c385ab commit dfb9426
Show file tree
Hide file tree
Showing 6 changed files with 84 additions and 38 deletions.
Binary file added ggoled_app/assets/headset_connected.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added ggoled_app/assets/headset_disconnected.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
55 changes: 48 additions & 7 deletions ggoled_app/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -40,20 +41,22 @@ struct ConfigFont {
#[derive(Serialize, Deserialize)]
#[serde(default)]
struct Config {
font: Option<ConfigFont>,
show_time: bool,
show_media: bool,
idle_timeout: bool,
oled_shift: ConfigShiftMode,
font: Option<ConfigFont>,
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,
}
}
}
Expand Down Expand Up @@ -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);
Expand All @@ -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(),
Expand Down Expand Up @@ -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<Media> = None;
let mut time_layers: Vec<LayerId> = vec![];
let mut media_layers: Vec<LayerId> = vec![];
let mut notif_layer: Option<LayerId> = 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();
Expand All @@ -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() {
Expand All @@ -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;
}
}
_ => {}
},
}
Expand All @@ -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 {
Expand Down
5 changes: 3 additions & 2 deletions ggoled_cli/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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},
Expand Down Expand Up @@ -198,7 +199,7 @@ fn main() {
let bitmap = if path == "-" {
let mut buf = Vec::<u8>::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 {
Expand All @@ -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<Bitmap>, Duration)> = paths
.iter()
.flat_map(|path| {
decode_frames(path, image_args.threshold).into_iter().map(|frame| {
Expand Down
51 changes: 27 additions & 24 deletions ggoled_draw/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -94,8 +94,9 @@ pub fn bitmap_from_memory(buf: &[u8], threshold: u8) -> anyhow::Result<Bitmap> {
Ok(bitmap_from_dynimage(&img, threshold))
}

#[derive(Clone)]
pub struct Frame {
pub bitmap: Bitmap,
pub bitmap: Arc<Bitmap>,
pub delay: Option<Duration>,
}

Expand All @@ -107,7 +108,7 @@ pub fn decode_frames(path: &str, threshold: u8) -> Vec<Frame> {
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)),
Expand All @@ -116,7 +117,7 @@ pub fn decode_frames(path: &str, threshold: u8) -> Vec<Frame> {
.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 }]
}
}
Expand All @@ -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<Bitmap>,
x: isize,
y: isize,
},
Animation {
frames: Vec<Frame>,
pos: Pos,
x: isize,
y: isize,
follow_fps: bool,
},
Scroll {
bitmap: Bitmap,
bitmap: Arc<Bitmap>,
y: isize,
},
}
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -412,7 +412,12 @@ impl DrawDevice {
pub fn add_text(&mut self, text: &str, x: Option<isize>, y: Option<isize>) -> Vec<LayerId> {
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
Expand All @@ -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,
},
)
}
Expand Down
11 changes: 6 additions & 5 deletions ggoled_lib/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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
Expand Down

0 comments on commit dfb9426

Please sign in to comment.