Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 6 additions & 6 deletions crates/openlogi-cli/src/cmd/diag/dpi.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,10 @@ pub struct DpiArgs {
}

pub async fn run(args: DpiArgs) -> Result<()> {
let (uid, slot, name) = first_online_device().await?;
println!("device: {name} (slot {slot}, receiver {uid})");
let (route, name) = first_online_device().await?;
println!("device: {name} ({route})");

let before = openlogi_hid::get_dpi(Some(&uid), slot)
let before = openlogi_hid::get_dpi(&route)
.await
.context("read current DPI")?;
println!(" current DPI: {before}");
Expand All @@ -37,11 +37,11 @@ pub async fn run(args: DpiArgs) -> Result<()> {
}

println!(" writing DPI: {target}");
openlogi_hid::set_dpi(Some(&uid), slot, target)
openlogi_hid::set_dpi(&route, target)
.await
.context("write DPI")?;

let after = openlogi_hid::get_dpi(Some(&uid), slot)
let after = openlogi_hid::get_dpi(&route)
.await
.context("read DPI after write")?;
println!(" read-back DPI: {after}");
Expand All @@ -54,7 +54,7 @@ pub async fn run(args: DpiArgs) -> Result<()> {
}

println!(" restoring DPI: {before}");
openlogi_hid::set_dpi(Some(&uid), slot, before)
openlogi_hid::set_dpi(&route, before)
.await
.context("restore DPI")?;

Expand Down
6 changes: 3 additions & 3 deletions crates/openlogi-cli/src/cmd/diag/features.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,10 @@ use crate::cmd::diag::first_online_device;
pub struct FeaturesArgs {}

pub async fn run(_args: FeaturesArgs) -> Result<()> {
let (uid, slot, name) = first_online_device().await?;
println!("device: {name} (slot {slot}, receiver {uid})");
let (route, name) = first_online_device().await?;
println!("device: {name} ({route})");

let entries = openlogi_hid::dump_features(Some(&uid), slot)
let entries = openlogi_hid::dump_features(&route)
.await
.context("dump features")?;

Expand Down
23 changes: 17 additions & 6 deletions crates/openlogi-cli/src/cmd/diag/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@

use anyhow::Result;
use clap::Subcommand;
use openlogi_hid::DeviceRoute;

pub mod dpi;
pub mod features;
Expand All @@ -33,21 +34,31 @@ impl DiagCmd {
}
}

/// Shared device picker: enumerate inventories, return the first online
/// paired device with a receiver `unique_id` (i.e. the same selection rule
/// the GUI uses for its initial DPI target).
pub(crate) async fn first_online_device() -> Result<(String, u8, String)> {
/// Shared device picker: enumerate inventories, return the [`DeviceRoute`] +
/// display name of the first online paired device (the same selection rule the
/// GUI uses for its initial target). Builds a Bolt route when the device is
/// behind a receiver, a direct route otherwise (USB cable / Bluetooth).
pub(crate) async fn first_online_device() -> Result<(DeviceRoute, String)> {
use anyhow::anyhow;
let inventories = openlogi_hid::enumerate().await?;
inventories
.into_iter()
.find_map(|inv| {
let uid = inv.receiver.unique_id?;
let paired = inv.paired.into_iter().find(|p| p.online)?;
let route = match inv.receiver.unique_id {
Some(receiver_uid) => DeviceRoute::Bolt {
receiver_uid,
slot: paired.slot,
},
None => DeviceRoute::Direct {
vendor_id: inv.receiver.vendor_id,
product_id: inv.receiver.product_id,
},
};
let name = paired
.codename
.unwrap_or_else(|| format!("Slot {}", paired.slot));
Some((uid, paired.slot, name))
Some((route, name))
})
.ok_or_else(|| anyhow!("no online HID++ device found — is a Logi mouse paired?"))
}
12 changes: 6 additions & 6 deletions crates/openlogi-cli/src/cmd/diag/smartshift.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,23 +14,23 @@ pub struct SmartshiftArgs {
}

pub async fn run(args: SmartshiftArgs) -> Result<()> {
let (uid, slot, name) = first_online_device().await?;
println!("device: {name} (slot {slot}, receiver {uid})");
let (route, name) = first_online_device().await?;
println!("device: {name} ({route})");

let before = openlogi_hid::get_smartshift_status(Some(&uid), slot)
let before = openlogi_hid::get_smartshift_status(&route)
.await
.context("read SmartShift status")?;
println!(
" current: mode={:?} sensitivity={}",
before.mode, before.sensitivity
);

let new_mode = openlogi_hid::toggle_smartshift(Some(&uid), slot)
let new_mode = openlogi_hid::toggle_smartshift(&route)
.await
.context("toggle SmartShift")?;
println!(" toggled to: {new_mode:?}");

let after = openlogi_hid::get_smartshift_status(Some(&uid), slot)
let after = openlogi_hid::get_smartshift_status(&route)
.await
.context("read SmartShift after toggle")?;
println!(
Expand All @@ -51,7 +51,7 @@ pub async fn run(args: SmartshiftArgs) -> Result<()> {
}

println!(" restoring mode: {:?}", before.mode);
openlogi_hid::toggle_smartshift(Some(&uid), slot)
openlogi_hid::toggle_smartshift(&route)
.await
.context("restore SmartShift")?;

Expand Down
4 changes: 2 additions & 2 deletions crates/openlogi-gui/src/components/dpi_panel.rs
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ impl DpiPanel {
// when the panel was constructed.
let target = cx
.try_global::<AppState>()
.and_then(|s| s.current_record().and_then(|r| r.dpi_target.clone()));
.and_then(|s| s.current_record().and_then(|r| r.route.clone()));
write_dpi_in_background(None, target, dpi);
}
},
Expand Down Expand Up @@ -186,7 +186,7 @@ fn preset_chip(idx: usize, value: u32, active: bool, presets: &[u32], pal: Palet
.on_click(move |_event, _window, cx| {
let target = cx
.try_global::<AppState>()
.and_then(|s| s.current_record().and_then(|r| r.dpi_target.clone()));
.and_then(|s| s.current_record().and_then(|r| r.route.clone()));
cx.update_global::<AppState, _>(|state, _| state.dpi = value);
write_dpi_in_background(None, target, value);
cx.refresh_windows();
Expand Down
42 changes: 16 additions & 26 deletions crates/openlogi-gui/src/hardware.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,28 +12,21 @@
//! transient open is kept as a fallback for callers (e.g. the CGEventTap hook)
//! firing while no session is connected.

use openlogi_hid::{CaptureChannel, SharedChannel};
use openlogi_hid::{CaptureChannel, DeviceRoute, SharedChannel};
use tracing::{debug, warn};

/// Clone out the capture session's channel when it points at `target`. `None`
/// when no slot is supplied (e.g. the DPI slider, where a transient open is
/// fine) or the open channel targets a different device.
fn reusable_channel(capture: Option<&CaptureChannel>, target: &DpiTarget) -> Option<SharedChannel> {
/// Clone out the capture session's channel when it reaches `route`. `None` when
/// no capture session is connected or the open channel points at a different
/// device.
fn reusable_channel(
capture: Option<&CaptureChannel>,
route: &DeviceRoute,
) -> Option<SharedChannel> {
capture?
.read()
.ok()
.and_then(|slot| (*slot).clone())
.filter(|chan| chan.matches(Some(&target.receiver_uid), target.slot))
}

/// Identifies which physical device hardware-side writes should target.
/// `receiver_uid` is the Bolt receiver's unique id (so writes route
/// correctly when more than one receiver is plugged in); `slot` is the
/// device's pairing slot on that receiver.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct DpiTarget {
pub receiver_uid: String,
pub slot: u8,
.filter(|chan| chan.matches(route))
}

/// Spawn an OS thread that toggles SmartShift (free ↔ ratchet) on the
Expand All @@ -42,7 +35,7 @@ pub struct DpiTarget {
/// logged.
pub fn toggle_smartshift_in_background(
capture: Option<&CaptureChannel>,
target: Option<DpiTarget>,
target: Option<DeviceRoute>,
) {
let Some(target) = target else {
debug!("no target device — SmartShift toggle skipped");
Expand All @@ -64,13 +57,12 @@ pub fn toggle_smartshift_in_background(
let result = rt.block_on(async {
match &shared {
Some(shared) => openlogi_hid::toggle_smartshift_on(shared).await,
None => {
openlogi_hid::toggle_smartshift(Some(&target.receiver_uid), target.slot).await
}
None => openlogi_hid::toggle_smartshift(&target).await,
}
});
let index = target.device_index();
match result {
Ok(mode) => debug!(slot = target.slot, ?mode, reused, "SmartShift toggled"),
Ok(mode) => debug!(index, ?mode, reused, "SmartShift toggled"),
Err(e) => warn!(error = ?e, "SmartShift toggle failed"),
}
});
Expand All @@ -82,7 +74,7 @@ pub fn toggle_smartshift_in_background(
/// `target == None` is a no-op (dev environment without a real device).
pub fn write_dpi_in_background(
capture: Option<&CaptureChannel>,
target: Option<DpiTarget>,
target: Option<DeviceRoute>,
dpi: u32,
) {
let Some(target) = target else {
Expand All @@ -109,14 +101,12 @@ pub fn write_dpi_in_background(
let result = rt.block_on(async {
match &shared {
Some(shared) => openlogi_hid::set_dpi_on(shared, dpi_u16).await,
None => {
openlogi_hid::set_dpi(Some(&target.receiver_uid), target.slot, dpi_u16).await
}
None => openlogi_hid::set_dpi(&target, dpi_u16).await,
}
});
match result {
Ok(()) => debug!(
slot = target.slot,
index = target.device_index(),
dpi = dpi_u16,
reused,
"DPI written to device"
Expand Down
4 changes: 2 additions & 2 deletions crates/openlogi-gui/src/state.rs
Original file line number Diff line number Diff line change
Expand Up @@ -170,7 +170,7 @@ impl AppState {
let presets = record
.map(|r| config.dpi_presets(&r.config_key))
.unwrap_or_default();
let target = record.and_then(|r| r.dpi_target.clone());
let target = record.and_then(|r| r.route.clone());
(
bindings,
gesture_bindings,
Expand Down Expand Up @@ -517,7 +517,7 @@ impl AppState {
.current_record()
.map(|r| self.config.dpi_presets(&r.config_key))
.unwrap_or_default();
let target = self.current_record().and_then(|r| r.dpi_target.clone());
let target = self.current_record().and_then(|r| r.route.clone());
match self.dpi_cycle.write() {
Ok(mut guard) => {
*guard = DpiCycleState {
Expand Down
35 changes: 26 additions & 9 deletions crates/openlogi-gui/src/state/devices.rs
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
//! Device-list construction and selection helpers for [`super::AppState`].

use openlogi_core::device::{BatteryInfo, DeviceInventory, DeviceKind};
use openlogi_hid::{DIRECT_DEVICE_INDEX, DeviceRoute};

use crate::asset::{AssetResolver, ResolvedAsset};
use crate::hardware::DpiTarget;

/// One paired device with everything the UI needs to switch to it in O(1):
/// the config key (for bindings/DPI persistence), a display name, the
/// resolved asset (PNG + metadata, or `None` for the synthetic fallback),
/// and the routing target for HID++ DPI writes.
/// and the [`DeviceRoute`] HID++ writes / capture target.
///
/// The `kind` / `slot` / `online` / `battery` fields mirror the source
/// [`PairedDevice`](openlogi_core::device::PairedDevice) so the header
Expand All @@ -20,7 +20,7 @@ pub struct DeviceRecord {
pub config_key: String,
pub display_name: String,
pub asset: Option<ResolvedAsset>,
pub dpi_target: Option<DpiTarget>,
pub route: Option<DeviceRoute>,
pub kind: DeviceKind,
pub slot: u8,
pub online: bool,
Expand All @@ -33,7 +33,6 @@ pub(super) fn build_device_list(
) -> Vec<DeviceRecord> {
let mut list = Vec::new();
for inv in inventories {
let receiver_uid = inv.receiver.unique_id.clone();
for paired in &inv.paired {
let Some(model) = paired.model_info.as_ref() else {
continue;
Expand All @@ -45,15 +44,11 @@ pub(super) fn build_device_list(
.map(|a| a.display_name.clone())
.or_else(|| paired.codename.clone())
.unwrap_or_else(|| format!("Slot {}", paired.slot));
let dpi_target = receiver_uid.as_ref().map(|uid| DpiTarget {
receiver_uid: uid.clone(),
slot: paired.slot,
});
list.push(DeviceRecord {
config_key,
display_name,
asset,
dpi_target,
route: device_route(inv, paired.slot),
kind: paired.kind,
slot: paired.slot,
online: paired.online,
Expand All @@ -64,6 +59,28 @@ pub(super) fn build_device_list(
list
}

/// Build the [`DeviceRoute`] HID++ writes use to reach a device.
///
/// A Bolt-paired device routes through its receiver UID + slot. A directly
/// attached one (USB cable / Bluetooth) carries no receiver UID and sits at
/// [`DIRECT_DEVICE_INDEX`] — it routes by the HID node's vendor/product id
/// instead. A Bolt device whose receiver UID couldn't be read gets no route
/// (`None`), so hardware writes are skipped rather than mis-routed to the
/// receiver's own pid.
fn device_route(inv: &DeviceInventory, slot: u8) -> Option<DeviceRoute> {
match &inv.receiver.unique_id {
Some(receiver_uid) => Some(DeviceRoute::Bolt {
receiver_uid: receiver_uid.clone(),
slot,
}),
None if slot == DIRECT_DEVICE_INDEX => Some(DeviceRoute::Direct {
vendor_id: inv.receiver.vendor_id,
product_id: inv.receiver.product_id,
}),
None => None,
}
}

pub(super) fn pick_initial_device(list: &[DeviceRecord], saved: Option<&str>) -> usize {
saved
.and_then(|key| list.iter().position(|r| r.config_key == key))
Expand Down
8 changes: 4 additions & 4 deletions crates/openlogi-gui/src/state/dpi.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
//! DPI-cycle state shared with background action dispatch.

use crate::hardware::DpiTarget;
use openlogi_hid::DeviceRoute;

/// Shared state consumed by the OS hook thread and the DPI panel UI to
/// implement DPI preset cycling and direct preset selection actions.
Expand All @@ -11,14 +11,14 @@ use crate::hardware::DpiTarget;
pub struct DpiCycleState {
pub presets: Vec<u32>,
pub index: usize,
pub target: Option<DpiTarget>,
pub target: Option<DeviceRoute>,
}

impl DpiCycleState {
/// Advance to the next preset (wrapping last → first) and return the new
/// DPI + the device target to write to. Returns `None` if `presets` is
/// empty.
pub fn cycle(&mut self) -> Option<(u32, Option<DpiTarget>)> {
pub fn cycle(&mut self) -> Option<(u32, Option<DeviceRoute>)> {
if self.presets.is_empty() {
return None;
}
Expand All @@ -28,7 +28,7 @@ impl DpiCycleState {

/// Jump to preset `i`, clamping to the list length. Returns the DPI +
/// target, or `None` if `presets` is empty.
pub fn set(&mut self, i: usize) -> Option<(u32, Option<DpiTarget>)> {
pub fn set(&mut self, i: usize) -> Option<(u32, Option<DeviceRoute>)> {
if self.presets.is_empty() {
return None;
}
Expand Down
Loading
Loading