Stable Point - AV/K/M working

This commit is contained in:
Brad Stein 2025-06-30 15:45:37 -05:00
parent 981f1d1fa3
commit f60736990b
11 changed files with 37 additions and 53 deletions

View File

@ -149,7 +149,7 @@ impl LesavkaClientApp {
/*──────────────── keyboard stream ───────────────*/ /*──────────────── keyboard stream ───────────────*/
async fn stream_loop_keyboard(&self, ep: Channel) { async fn stream_loop_keyboard(&self, ep: Channel) {
loop { loop {
info!("⌨️ dial {}", self.server_addr); // LESAVKA-client info!("⌨️🤙 dial {}", self.server_addr); // LESAVKA-client
let mut cli = RelayClient::new(ep.clone()); let mut cli = RelayClient::new(ep.clone());
// ✅ use kbd_tx here - fixes E0271 // ✅ use kbd_tx here - fixes E0271
@ -171,7 +171,7 @@ impl LesavkaClientApp {
/*──────────────── mouse stream ──────────────────*/ /*──────────────── mouse stream ──────────────────*/
async fn stream_loop_mouse(&self, ep: Channel) { async fn stream_loop_mouse(&self, ep: Channel) {
loop { loop {
info!("🖱️ dial {}", self.server_addr); info!("🖱️🤙 dial {}", self.server_addr);
let mut cli = RelayClient::new(ep.clone()); let mut cli = RelayClient::new(ep.clone());
let outbound = BroadcastStream::new(self.mou_tx.subscribe()) let outbound = BroadcastStream::new(self.mou_tx.subscribe())

View File

@ -64,7 +64,7 @@ impl InputAggregator {
match classify_device(&dev) { match classify_device(&dev) {
DeviceKind::Keyboard => { DeviceKind::Keyboard => {
dev.grab().with_context(|| format!("grabbing keyboard {path:?}"))?; dev.grab().with_context(|| format!("grabbing keyboard {path:?}"))?;
info!("Grabbed keyboard {:?}", dev.name().unwrap_or("UNKNOWN")); info!("🤏🖱️ Grabbed keyboard {:?}", dev.name().unwrap_or("UNKNOWN"));
// pass dev_mode to aggregator // pass dev_mode to aggregator
// let kbd_agg = KeyboardAggregator::new(dev, self.dev_mode); // let kbd_agg = KeyboardAggregator::new(dev, self.dev_mode);
@ -75,7 +75,7 @@ impl InputAggregator {
} }
DeviceKind::Mouse => { DeviceKind::Mouse => {
dev.grab().with_context(|| format!("grabbing mouse {path:?}"))?; dev.grab().with_context(|| format!("grabbing mouse {path:?}"))?;
info!("Grabbed mouse {:?}", dev.name().unwrap_or("UNKNOWN")); info!("🤏⌨️ Grabbed mouse {:?}", dev.name().unwrap_or("UNKNOWN"));
// let mouse_agg = MouseAggregator::new(dev); // let mouse_agg = MouseAggregator::new(dev);
let mouse_agg = MouseAggregator::new(dev, self.dev_mode, self.mou_tx.clone()); let mouse_agg = MouseAggregator::new(dev, self.dev_mode, self.mou_tx.clone());

View File

@ -1,4 +1,4 @@
// client/src/layout.rs Wayland-only window placement utilities // client/src/layout.rs - Wayland-only window placement utilities
#![forbid(unsafe_code)] #![forbid(unsafe_code)]
use std::process::Command; use std::process::Command;

View File

@ -69,7 +69,7 @@ async fn main() -> Result<()> {
.with(file_layer) .with(file_layer)
.init(); .init();
tracing::info!("lesavka-client running in DEV mode → {}", log_path.display()); tracing::info!("📜 lesavka-client running in DEV mode → {}", log_path.display());
} else { } else {
tracing_subscriber::registry() tracing_subscriber::registry()
.with(env_filter) .with(env_filter)

View File

@ -4,6 +4,7 @@ use anyhow::{Context, Result};
use gstreamer as gst; use gstreamer as gst;
use gstreamer_app as gst_app; use gstreamer_app as gst_app;
use gst::prelude::*; use gst::prelude::*;
use gst::MessageView::*;
use tracing::{error, info, warn, debug}; use tracing::{error, info, warn, debug};
use lesavka_common::lesavka::AudioPacket; use lesavka_common::lesavka::AudioPacket;
@ -69,7 +70,6 @@ impl AudioOut {
// ── 4. Log *all* warnings/errors from the bus ────────────────────── // ── 4. Log *all* warnings/errors from the bus ──────────────────────
let bus = pipeline.bus().unwrap(); let bus = pipeline.bus().unwrap();
std::thread::spawn(move || { std::thread::spawn(move || {
use gst::MessageView::*;
for msg in bus.iter_timed(gst::ClockTime::NONE) { for msg in bus.iter_timed(gst::ClockTime::NONE) {
match msg.view() { match msg.view() {
Error(e) => error!("💥 gst error from {:?}: {} ({})", Error(e) => error!("💥 gst error from {:?}: {} ({})",
@ -82,8 +82,18 @@ impl AudioOut {
.structure() .structure()
.map(|s| s.to_string()) .map(|s| s.to_string())
.unwrap_or_default()), .unwrap_or_default()),
StateChanged(s) if s.current() == gst::State::Playing => StateChanged(s) if s.current() == gst::State::Playing => {
info!("🔊 audio pipeline PLAYING (sink='{}')", sink), if msg
.src()
.map(|s| s.is::<gst::Pipeline>())
.unwrap_or(false)
{
info!("🔊 audio pipeline PLAYING (sink='{}')", sink);
} else {
debug!("🔊 element {} now PLAYING",
msg.src().map(|s| s.name()).unwrap_or_default());
}
},
_ => {} _ => {}
} }
} }
@ -116,12 +126,11 @@ impl Drop for AudioOut {
fn pick_sink_element() -> Result<String> { fn pick_sink_element() -> Result<String> {
// 1. Operator override // 1. Operator override
if let Ok(s) = std::env::var("LESAVKA_AUDIO_SINK") { if let Ok(s) = std::env::var("LESAVKA_AUDIO_SINK") {
info!("🎛️ sink overridden via LESAVKA_AUDIO_SINK={}", s); info!("💪 sink overridden via LESAVKA_AUDIO_SINK={}", s);
return Ok(s); return Ok(s);
} }
// 2. Query PipeWire for default & running sinks // 2. Query PipeWire for default & running sinks
// (works even if PulseAudio is present because PipeWire mimics it)
let sinks = list_pw_sinks(); // Vec<(name,state)> let sinks = list_pw_sinks(); // Vec<(name,state)>
for (n, st) in &sinks { for (n, st) in &sinks {
if *st == "RUNNING" { if *st == "RUNNING" {
@ -132,45 +141,23 @@ fn pick_sink_element() -> Result<String> {
// 3. First RUNNING sink // 3. First RUNNING sink
if let Some((n, _)) = sinks.iter().find(|(_, st)| *st == "RUNNING") { if let Some((n, _)) = sinks.iter().find(|(_, st)| *st == "RUNNING") {
warn!("🪄 picking first RUNNING sink '{}'", n); warn!("🏃 picking first RUNNING sink '{}'", n);
return Ok(format!("pulsesink device={}", n));
}
// 4. Anything
if let Some((n, _)) = sinks.first() {
warn!("🪄 picking first sink '{}'", n);
return Ok(format!("pulsesink device={}", n)); return Ok(format!("pulsesink device={}", n));
} }
// Fallback let autoaudiosink try its luck // 4. Anything
warn!("😬 no PipeWire sinks readable falling back to autoaudiosink"); if let Some((n, _)) = sinks.first() {
warn!("🎲 picking first sink '{}'", n);
return Ok(format!("pulsesink device={}", n));
}
// Fallback - let autoaudiosink try its luck
warn!("🫣 no PipeWire sinks readable - falling back to autoaudiosink");
Ok("autoaudiosink".to_string()) Ok("autoaudiosink".to_string())
} }
/// Minimal PipeWire sink enumerator (no extra crate required).
fn list_pw_sinks() -> Vec<(String, String)> { fn list_pw_sinks() -> Vec<(String, String)> {
let mut out = Vec::new(); let mut out = Vec::new();
// if let Ok(lines) = std::process::Command::new("pw-cli")
// .args(["ls", "Node"])
// .output()
// .map(|o| String::from_utf8_lossy(&o.stdout).to_string())
// {
// for l in lines.lines() {
// // Example: " 36 │ node.alive = true │ alsa_output.pci-0000_2f_00.4.iec958-stereo │ state: SUSPENDED ..."
// if let Some(pos) = l.find("│") {
// let parts: Vec<_> = l[pos..].split('│').map(|s| s.trim()).collect();
// if parts.len() >= 3 && parts[2].starts_with("alsa_output.") {
// let name = parts[2].to_string();
// // try to parse state, else UNKNOWN
// let state = parts.get(3)
// .and_then(|s| s.split_whitespace().nth(1))
// .unwrap_or("UNKNOWN")
// .to_string();
// out.push((name, state));
// }
// }
// }
// }
if out.is_empty() { if out.is_empty() {
// ── PulseAudio / pactl fallback ──────────────────────────────── // ── PulseAudio / pactl fallback ────────────────────────────────
if let Ok(info) = std::process::Command::new("pactl") if let Ok(info) = std::process::Command::new("pactl")

View File

@ -15,7 +15,7 @@ pub struct MonitorInfo {
/// Enumerate monitors sorted by our desired priority. /// Enumerate monitors sorted by our desired priority.
pub fn enumerate_monitors() -> Vec<MonitorInfo> { pub fn enumerate_monitors() -> Vec<MonitorInfo> {
let Some(display) = gdk::Display::default() else { let Some(display) = gdk::Display::default() else {
tracing::warn!("⚠️ no GDK display falling back to single-monitor 0,0"); tracing::warn!("⚠️ no GDK display - falling back to single-monitor 0,0");
return vec![MonitorInfo { return vec![MonitorInfo {
geometry: gdk::Rectangle::new(0, 0, 1920, 1080), geometry: gdk::Rectangle::new(0, 0, 1920, 1080),
scale_factor: 1, scale_factor: 1,

View File

@ -124,7 +124,7 @@ impl MonitorWindow {
y = r.y, y = r.y,
); );
// Retry in a detached thread avoids blocking GStreamer // Retry in a detached thread - avoids blocking GStreamer
let placename = placer.name; let placename = placer.name;
let runner = placer.run.clone(); let runner = placer.run.clone();
thread::spawn(move || { thread::spawn(move || {

View File

@ -1,5 +1,5 @@
#!/usr/bin/env bash #!/usr/bin/env bash
# lesavkacore.sh oneshot USBgadget bringup (Pi5 / ArchARM) # lesavkacore.sh - oneshot USBgadget bringup (Pi5 / ArchARM)
# Presents: • Bootprotocol keyboard (hidg0) # Presents: • Bootprotocol keyboard (hidg0)
# • Bootprotocol mouse (hidg1) # • Bootprotocol mouse (hidg1)
# • Stereo UAC2 speaker + microphone # • Stereo UAC2 speaker + microphone
@ -98,7 +98,7 @@ printf '\x05\x01\x09\x02\xa1\x01\x09\x01\xa1\x00'\
'\x05\x01\x09\x30\x09\x31\x09\x38\x15\x81\x25\x7f\x75\x08\x95\x03\x81\x06'\ '\x05\x01\x09\x30\x09\x31\x09\x38\x15\x81\x25\x7f\x75\x08\x95\x03\x81\x06'\
'\xc0\xc0' >"$G/functions/hid.usb1/report_desc" '\xc0\xc0' >"$G/functions/hid.usb1/report_desc"
# ---------- UAC2 function speaker + mic, 2×48kHz stereo --------- # ---------- UAC2 function - speaker + mic, 2×48kHz stereo ---------
mkdir -p "$G/functions/uac2.usb0" mkdir -p "$G/functions/uac2.usb0"
U="$G/functions/uac2.usb0" U="$G/functions/uac2.usb0"
# Playback (speaker) # Playback (speaker)

View File

@ -122,8 +122,6 @@ ExecStart=/usr/local/bin/lesavka-server
Restart=always Restart=always
Environment=RUST_LOG=lesavka_server=info,lesavka_server::audio=info,lesavka_server::video=info,lesavka_server::gadget=info Environment=RUST_LOG=lesavka_server=info,lesavka_server::audio=info,lesavka_server::video=info,lesavka_server::gadget=info
Environment=RUST_BACKTRACE=1 Environment=RUST_BACKTRACE=1
Environment=GST_DEBUG=3,v4l2src:6,tsdemux:6,parsebin:5
Environment=GST_DEBUG_DUMP_DOT_DIR=/tmp
Restart=always Restart=always
RestartSec=5 RestartSec=5
StandardError=append:/tmp/lesavka-server.stderr StandardError=append:/tmp/lesavka-server.stderr

View File

@ -39,7 +39,7 @@ impl Drop for AudioStream {
} }
/*───────────────────────────────────────────────────────────────────────────*/ /*───────────────────────────────────────────────────────────────────────────*/
/* ear() capture from ALSA (“speaker”) and push AAC AUs via gRPC */ /* ear() - capture from ALSA (“speaker”) and push AAC AUs via gRPC */
/*───────────────────────────────────────────────────────────────────────────*/ /*───────────────────────────────────────────────────────────────────────────*/
pub async fn ear(alsa_dev: &str, id: u32) -> anyhow::Result<AudioStream> { pub async fn ear(alsa_dev: &str, id: u32) -> anyhow::Result<AudioStream> {
@ -151,7 +151,6 @@ fn build_pipeline_desc(dev: &str) -> anyhow::Result<String> {
}) })
.ok_or_else(|| anyhow!("no AAC encoder plugin available"))?; .ok_or_else(|| anyhow!("no AAC encoder plugin available"))?;
// one long literal assembled with `concat!` so Rust sees *one* string
Ok(format!( Ok(format!(
concat!( concat!(
"alsasrc device=\"{dev}\" do-timestamp=true ! ", "alsasrc device=\"{dev}\" do-timestamp=true ! ",

View File

@ -32,7 +32,7 @@ impl Stream for VideoStream {
impl Drop for VideoStream { impl Drop for VideoStream {
fn drop(&mut self) { fn drop(&mut self) {
// shut down nicely avoids the “dispose element … READY/PLAYING …” spam // shut down nicely - avoids the “dispose element … READY/PLAYING …” spam
let _ = self._pipeline.set_state(gst::State::Null); let _ = self._pipeline.set_state(gst::State::Null);
} }
} }
@ -93,7 +93,7 @@ pub async fn eye_ball(
} else { } else {
warn!(target:"lesavka_server::video", warn!(target:"lesavka_server::video",
eye = %eye, eye = %eye,
"🍪 cam_{eye} not found skipping pad-probe"); "🍪 cam_{eye} not found - skipping pad-probe");
} }
let eye_clone = eye.to_owned(); let eye_clone = eye.to_owned();
@ -191,7 +191,7 @@ pub async fn eye_ball(
debug!(target:"lesavka_server::video", debug!(target:"lesavka_server::video",
eye = %eye, eye = %eye,
dropped = c, dropped = c,
"⏳ channel full dropping frames"); "⏳ channel full - dropping frames");
} }
} }
Err(e) => error!("mpsc send err: {e}"), Err(e) => error!("mpsc send err: {e}"),