Skip to content

Commit 92a8af3

Browse files
committed
Show media on Qube display
1 parent 5e22209 commit 92a8af3

6 files changed

Lines changed: 267 additions & 47 deletions

File tree

common/rmk-main-upstream/rmk/src/host/via/mod.rs

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,11 +32,28 @@ fn process_host_data_packet(data: &[u8; 32]) -> bool {
3232
crate::host_data::update_layout(data[1]);
3333
true
3434
}
35-
HOST_DATA_VOLUME | HOST_DATA_MEDIA_ARTIST | HOST_DATA_MEDIA_TITLE => true,
35+
HOST_DATA_MEDIA_ARTIST => {
36+
crate::host_data::update_media_artist(host_data_text(data));
37+
true
38+
}
39+
HOST_DATA_MEDIA_TITLE => {
40+
crate::host_data::update_media_title(host_data_text(data));
41+
true
42+
}
43+
HOST_DATA_VOLUME => true,
3644
_ => false,
3745
}
3846
}
3947

48+
fn host_data_text(data: &[u8; 32]) -> &str {
49+
let len = (data[1] as usize).min(30);
50+
let bytes = &data[2..2 + len];
51+
match core::str::from_utf8(bytes) {
52+
Ok(text) => text,
53+
Err(err) => core::str::from_utf8(&bytes[..err.valid_up_to()]).unwrap_or(""),
54+
}
55+
}
56+
4057
pub struct VialService<'a> {
4158
ctx: &'a KeyboardContext<'a>,
4259
vial_config: VialConfig<'static>,

common/rmk-main-upstream/rmk/src/host_data.rs

Lines changed: 41 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,30 @@
1-
use core::sync::atomic::{AtomicU8, Ordering};
1+
use core::{
2+
cell::RefCell,
3+
sync::atomic::{AtomicU8, Ordering},
4+
};
5+
6+
use embassy_sync::blocking_mutex::Mutex;
7+
8+
use crate::RawMutex;
29

310
const UNKNOWN: u8 = u8::MAX;
11+
const HOST_TEXT_LEN: usize = 32;
412

513
static HOST_HOUR: AtomicU8 = AtomicU8::new(UNKNOWN);
614
static HOST_MINUTE: AtomicU8 = AtomicU8::new(UNKNOWN);
715
static HOST_LAYOUT: AtomicU8 = AtomicU8::new(UNKNOWN);
16+
static HOST_MEDIA_ARTIST: Mutex<RawMutex, RefCell<heapless::String<HOST_TEXT_LEN>>> =
17+
Mutex::new(RefCell::new(heapless::String::new()));
18+
static HOST_MEDIA_TITLE: Mutex<RawMutex, RefCell<heapless::String<HOST_TEXT_LEN>>> =
19+
Mutex::new(RefCell::new(heapless::String::new()));
820

9-
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
21+
#[derive(Clone, Debug, Default, PartialEq, Eq)]
1022
pub struct HostData {
1123
pub hour: Option<u8>,
1224
pub minute: Option<u8>,
1325
pub layout: Option<u8>,
26+
pub media_artist: heapless::String<HOST_TEXT_LEN>,
27+
pub media_title: heapless::String<HOST_TEXT_LEN>,
1428
}
1529

1630
pub fn update_time(hour: u8, minute: u8) {
@@ -24,14 +38,39 @@ pub fn update_layout(layout: u8) {
2438
HOST_LAYOUT.store(layout, Ordering::Relaxed);
2539
}
2640

41+
pub fn update_media_artist(value: &str) {
42+
update_media_text(&HOST_MEDIA_ARTIST, value);
43+
}
44+
45+
pub fn update_media_title(value: &str) {
46+
update_media_text(&HOST_MEDIA_TITLE, value);
47+
}
48+
2749
pub fn snapshot() -> HostData {
2850
HostData {
2951
hour: known(HOST_HOUR.load(Ordering::Relaxed)),
3052
minute: known(HOST_MINUTE.load(Ordering::Relaxed)),
3153
layout: known(HOST_LAYOUT.load(Ordering::Relaxed)),
54+
media_artist: HOST_MEDIA_ARTIST.lock(|cell| cell.borrow().clone()),
55+
media_title: HOST_MEDIA_TITLE.lock(|cell| cell.borrow().clone()),
3256
}
3357
}
3458

3559
fn known(value: u8) -> Option<u8> {
3660
(value != UNKNOWN).then_some(value)
3761
}
62+
63+
fn update_media_text(
64+
slot: &Mutex<RawMutex, RefCell<heapless::String<HOST_TEXT_LEN>>>,
65+
value: &str,
66+
) {
67+
slot.lock(|cell| {
68+
let mut text = cell.borrow_mut();
69+
text.clear();
70+
for ch in value.chars() {
71+
if text.push(ch).is_err() {
72+
break;
73+
}
74+
}
75+
});
76+
}

keyboards/k04_qube/src/qube_display.rs

Lines changed: 103 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,8 @@ const STRIPE_BYTES: usize = SCREEN_W * STRIPE_H * 2;
6161
const BACKLIGHT_ACTIVE_HIGH: bool = true;
6262
const SAFE_X: i32 = 18;
6363
const SAFE_W: u32 = SCREEN_W as u32 - (SAFE_X as u32 * 2);
64+
const MEDIA_VISIBLE_CHARS: usize = 26;
65+
const MEDIA_GAP_CHARS: usize = 3;
6466
const PANEL_RADIUS: u32 = 14;
6567
const CHIP_RADIUS: u32 = 7;
6668
const BAR_RADIUS: u32 = 5;
@@ -380,7 +382,9 @@ where
380382
}),
381383
irq,
382384
},
383-
renderer: QubeStatusRenderer { host_data },
385+
renderer: QubeStatusRenderer {
386+
host_data: host_data.clone(),
387+
},
384388
ctx: RenderContext::default(),
385389
last_host_data: host_data,
386390
last_render: Instant::from_ticks(0),
@@ -417,7 +421,7 @@ where
417421
fn sync_host_data(&mut self) {
418422
let host_data = rmk::host_data::snapshot();
419423
if host_data != self.last_host_data {
420-
self.last_host_data = host_data;
424+
self.last_host_data = host_data.clone();
421425
self.renderer.host_data = host_data;
422426
self.request_redraw();
423427
}
@@ -578,7 +582,7 @@ where
578582
central: &mut impl EventSubscriber<Event = CentralConnectedEvent>,
579583
) -> UiEv {
580584
match select(
581-
Timer::after(Duration::from_secs(1)),
585+
Timer::after(Duration::from_millis(250)),
582586
Self::next_any(
583587
layer, wpm, led, mods, key, sleep, bat, conn, peri_conn, peri_bat, central,
584588
),
@@ -667,6 +671,9 @@ where
667671
UiEv::Central(e) => self.ctx.central_connected = e.connected,
668672
UiEv::HostDataTick => {
669673
self.sync_host_data();
674+
if self.renderer.media_needs_marquee() {
675+
self.request_redraw();
676+
}
670677
need_redraw = false;
671678
}
672679
}
@@ -708,16 +715,24 @@ pub struct QubeStatusRenderer {
708715
host_data: rmk::host_data::HostData,
709716
}
710717

718+
impl QubeStatusRenderer {
719+
fn media_needs_marquee(&self) -> bool {
720+
let mut media: heapless::String<72> = heapless::String::new();
721+
push_media_label(&mut media, &self.host_data);
722+
media.len() > MEDIA_VISIBLE_CHARS
723+
}
724+
}
725+
711726
impl DisplayRenderer<Rgb565> for QubeStatusRenderer {
712727
fn render<D: DrawTarget<Color = Rgb565>>(&mut self, ctx: &RenderContext, display: &mut D) {
713728
let _ = display.clear(COL_BG);
714729

715730
let layer_meta = MonoTextStyle::new(&FONT_6X10, COL_LABEL);
731+
let header_media = MonoTextStyle::new(&FONT_6X10, COL_FG);
732+
let header_fallback = MonoTextStyle::new(&FONT_8X13, COL_ACCENT);
716733
let body = MonoTextStyle::new(&FONT_8X13, COL_FG);
717-
let body_accent = MonoTextStyle::new(&FONT_8X13, COL_ACCENT);
718734
let title_shadow = MonoTextStyle::new(&FONT_10X20, COL_ACCENT_DIM);
719735
let title = MonoTextStyle::new(&FONT_10X20, COL_FG);
720-
let top = TextStyleBuilder::new().baseline(Baseline::Top).build();
721736
let tc = TextStyleBuilder::new()
722737
.alignment(Alignment::Center)
723738
.baseline(Baseline::Top)
@@ -740,13 +755,13 @@ impl DisplayRenderer<Rgb565> for QubeStatusRenderer {
740755
draw_panel(display, SAFE_X, 14, SAFE_W, 28, COL_PANEL, COL_BORDER_DIM);
741756
draw_round_fill(display, SAFE_X + 11, 23, 3, 10, 2, COL_ACCENT);
742757
let mut s: heapless::String<16> = heapless::String::new();
743-
let _ = s.push_str(host_layout_label(self.host_data.layout));
744-
let _ =
745-
Text::with_text_style(&s, Point::new(SAFE_X + 22, 21), body_accent, top).draw(display);
746-
s.clear();
747-
push_host_time(&mut s, self.host_data.hour, self.host_data.minute);
748-
let _ = Text::with_text_style(&s, Point::new(SAFE_X + SAFE_W as i32 - 14, 21), body, tr)
749-
.draw(display);
758+
draw_media_or_fallback(display, &self.host_data, header_media, header_fallback);
759+
if host_time_available(&self.host_data) {
760+
push_host_time(&mut s, self.host_data.hour, self.host_data.minute);
761+
let _ =
762+
Text::with_text_style(&s, Point::new(SAFE_X + SAFE_W as i32 - 14, 21), body, tr)
763+
.draw(display);
764+
}
750765

751766
// Layer panel.
752767
draw_panel(
@@ -997,15 +1012,6 @@ fn layer_name(layer: u8) -> &'static str {
9971012
}
9981013
}
9991014

1000-
fn host_layout_label(layout: Option<u8>) -> &'static str {
1001-
match layout {
1002-
Some(0) => "EN",
1003-
Some(1) => "RU",
1004-
Some(_) => "??",
1005-
None => "--",
1006-
}
1007-
}
1008-
10091015
fn push_host_time(buffer: &mut heapless::String<16>, hour: Option<u8>, minute: Option<u8>) {
10101016
match (hour, minute) {
10111017
(Some(hour), Some(minute)) => {
@@ -1016,3 +1022,79 @@ fn push_host_time(buffer: &mut heapless::String<16>, hour: Option<u8>, minute: O
10161022
}
10171023
}
10181024
}
1025+
1026+
fn draw_media_or_fallback<D: DrawTarget<Color = Rgb565>>(
1027+
display: &mut D,
1028+
host_data: &rmk::host_data::HostData,
1029+
media_style: MonoTextStyle<'_, Rgb565>,
1030+
fallback_style: MonoTextStyle<'_, Rgb565>,
1031+
) {
1032+
let top = TextStyleBuilder::new().baseline(Baseline::Top).build();
1033+
let mut media: heapless::String<72> = heapless::String::new();
1034+
push_media_label(&mut media, host_data);
1035+
1036+
if media.is_empty() {
1037+
if !host_time_available(host_data) {
1038+
let _ = Text::with_text_style("QUBE", Point::new(SAFE_X + 22, 21), fallback_style, top)
1039+
.draw(display);
1040+
}
1041+
return;
1042+
}
1043+
1044+
let mut visible: heapless::String<32> = heapless::String::new();
1045+
if media.len() <= MEDIA_VISIBLE_CHARS {
1046+
let _ = visible.push_str(&media);
1047+
} else {
1048+
let elapsed = Instant::now()
1049+
.duration_since(Instant::from_ticks(0))
1050+
.as_millis() as usize;
1051+
let offset = (elapsed / 300) % (media.len() + MEDIA_GAP_CHARS);
1052+
push_marquee_slice(&mut visible, &media, offset);
1053+
}
1054+
1055+
let _ = Text::with_text_style(&visible, Point::new(SAFE_X + 22, 22), media_style, top)
1056+
.draw(display);
1057+
}
1058+
1059+
fn push_media_label(buffer: &mut heapless::String<72>, host_data: &rmk::host_data::HostData) {
1060+
if !host_data.media_artist.is_empty() {
1061+
push_ascii_text(buffer, &host_data.media_artist);
1062+
}
1063+
if !host_data.media_title.is_empty() {
1064+
if !buffer.is_empty() {
1065+
let _ = buffer.push_str(" - ");
1066+
}
1067+
push_ascii_text(buffer, &host_data.media_title);
1068+
}
1069+
}
1070+
1071+
fn push_ascii_text<const N: usize>(buffer: &mut heapless::String<N>, value: &str) {
1072+
for ch in value.chars() {
1073+
let ch = if ch.is_ascii_graphic() || ch == ' ' {
1074+
ch
1075+
} else {
1076+
'?'
1077+
};
1078+
if buffer.push(ch).is_err() {
1079+
break;
1080+
}
1081+
}
1082+
}
1083+
1084+
fn push_marquee_slice(buffer: &mut heapless::String<32>, text: &str, offset: usize) {
1085+
let bytes = text.as_bytes();
1086+
let cycle_len = bytes.len() + MEDIA_GAP_CHARS;
1087+
for i in 0..MEDIA_VISIBLE_CHARS {
1088+
let idx = (offset + i) % cycle_len;
1089+
let ch = if idx < bytes.len() {
1090+
bytes[idx] as char
1091+
} else {
1092+
' '
1093+
};
1094+
let _ = buffer.push(ch);
1095+
}
1096+
}
1097+
1098+
fn host_time_available(host_data: &rmk::host_data::HostData) -> bool {
1099+
host_data.hour.is_some() && host_data.minute.is_some()
1100+
}

keyboards/k04_qube/vial.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
"vendorId": "0xE126",
44
"productId": "0x0071",
55
"entropy": {
6-
"liveFeatures": ["time", "layout"]
6+
"liveFeatures": ["time", "media"]
77
},
88
"lighting": "none",
99
"matrix": {

0 commit comments

Comments
 (0)