use std::{collections::HashMap, sync::Arc, time::Duration};
use futures_concurrency::future::Join as _;
use hidpp::{
channel::HidppChannel,
receiver::{
NotificationFlags,
bolt::{
DeviceConnection as BoltDeviceConnection, Event as BoltEvent, Receiver as BoltReceiver,
},
},
};
use openlogi_core::device::{DeviceInventory, DeviceKind, PairedDevice, ReceiverInfo};
use tokio::time::timeout;
use tracing::{debug, warn};
use super::{MAX_BOLT_SLOTS, NodeProbe, PassContext, ProbeVerdict};
use crate::backend::NodeInfo;
use crate::host_lock::ReceiverRegisterPhase;
use crate::inventory::cache::{CacheKey, CacheOutcome, probe_or_reuse, seen};
use crate::inventory::events::EventSubscriptionHandle;
use crate::inventory::features::ProbedFeatures;
use crate::inventory::mappings::{map_kind, resolve_device_kind};
pub(super) async fn probe_bolt_receiver(
channel: Arc<HidppChannel>,
info: NodeInfo,
bolt: BoltReceiver,
registers: ReceiverRegisterPhase,
pass: PassContext<'_>,
) -> NodeProbe {
let unique_id = bolt.get_unique_id().await.ok();
let pairing_count = bolt.count_pairings().await.ok();
debug!(?pairing_count, "receiver reports pairing count");
let connections =
drain_device_arrival(&bolt, pass.subscriptions, pass.timeouts.arrival_drain).await;
debug!(events = connections.len(), "drained device-arrival events");
let by_slot: HashMap<u8, BoltDeviceConnection> =
connections.into_iter().map(|c| (c.index, c)).collect();
let mut identities = Vec::new();
for slot in 1u8..=MAX_BOLT_SLOTS {
if let Some(identity) =
read_bolt_slot_identity(&bolt, &channel, by_slot.get(&slot), slot).await
{
identities.push(identity);
}
}
drop(registers);
let slot_results = identities
.iter()
.map(|identity| walk_bolt_slot(&channel, identity, pass))
.collect::<Vec<_>>()
.join()
.await;
let receiver = ReceiverInfo {
name: "Logi Bolt Receiver".to_string(),
vendor_id: info.vendor_id,
product_id: info.product_id,
unique_id,
};
assemble_bolt_probe(receiver, pairing_count, slot_results)
}
pub(in crate::inventory) fn assemble_bolt_probe(
receiver: ReceiverInfo,
pairing_count: Option<u8>,
slot_results: Vec<(PairedDevice, CacheOutcome)>,
) -> NodeProbe {
let (paired, outcomes): (Vec<_>, Vec<_>) = slot_results.into_iter().unzip();
if let Some(count) = pairing_count
&& paired.len() != usize::from(count)
{
warn!(
expected = count,
found = paired.len(),
"paired-device count mismatch — some slots may be unreadable"
);
}
let answered_in_full = pairing_count.is_some_and(|count| paired.len() == usize::from(count));
NodeProbe {
inventory: Some(DeviceInventory { receiver, paired }),
verdict: ProbeVerdict::healthy_when(answered_in_full),
outcomes,
}
}
struct BoltSlotIdentity {
slot: u8,
codename: Option<String>,
id: Option<CacheKey>,
online: bool,
register_kind: DeviceKind,
wpid: Option<u16>,
}
async fn read_bolt_slot_identity(
bolt: &BoltReceiver,
channel: &Arc<HidppChannel>,
event: Option<&BoltDeviceConnection>,
slot: u8,
) -> Option<BoltSlotIdentity> {
let pairing = match bolt.get_device_pairing_information(slot).await {
Ok(p) => p,
Err(e) => {
debug!(slot, error = ?e, "slot empty or unreadable");
return None;
}
};
let codename = read_codename(channel, slot).await;
let online = event.map_or(pairing.online, |c| c.online);
let bolt_kind = event.map_or(pairing.kind, |c| c.kind);
let wpid = event.map(|c| c.wpid);
debug!(
slot,
online,
?wpid,
?bolt_kind,
has_event = event.is_some(),
codename = ?codename,
"paired slot"
);
let id = (pairing.unit_id != [0u8; 4]).then_some(CacheKey::Bolt {
unit_id: pairing.unit_id,
});
Some(BoltSlotIdentity {
slot,
codename,
id,
online,
register_kind: map_kind(bolt_kind),
wpid,
})
}
async fn walk_bolt_slot(
channel: &Arc<HidppChannel>,
identity: &BoltSlotIdentity,
pass: PassContext<'_>,
) -> (PairedDevice, CacheOutcome) {
let &BoltSlotIdentity {
slot,
online,
register_kind,
wpid,
..
} = identity;
let id = identity.id.clone();
let cached = id.as_ref().and_then(|i| pass.cache.get(i));
let slot_budget = pass.timeouts.bolt_slot_probe;
let probe_result = timeout(
slot_budget,
probe_or_reuse(
channel,
slot,
id.clone(),
cached,
online,
pass.now,
pass.subscriptions,
),
)
.await;
let (probe, outcome) = if let Ok(r) = probe_result {
r
} else {
debug!(slot, budget = ?slot_budget,
"Bolt slot probe timed out; using cached data if available");
let probe = cached.map_or_else(ProbedFeatures::default, |c| c.probe.clone());
(probe, seen(id))
};
if matches!(outcome, CacheOutcome::Fresh(..))
&& let Some(probed) = probe.kind
&& probed != DeviceKind::Unknown
&& register_kind != DeviceKind::Unknown
&& probed != register_kind
{
debug!(
slot,
?register_kind,
?probed,
"device-kind sources disagree — trusting 0x0005"
);
}
let device = PairedDevice {
slot,
codename: identity.codename.clone(),
wpid,
kind: resolve_device_kind(probe.kind, register_kind),
online,
battery: probe.battery,
model_info: probe.model_info,
capabilities: probe.capabilities,
settings_revision: pass.settings_revision(slot),
};
(device, outcome)
}
async fn drain_device_arrival(
bolt: &BoltReceiver,
subscriptions: Option<&EventSubscriptionHandle>,
idle_timeout: Duration,
) -> Vec<BoltDeviceConnection> {
let rx = bolt.listen();
let _receiver_snapshot = subscriptions.map(EventSubscriptionHandle::begin_receiver_snapshot);
match bolt.get_notification_flags().await {
Ok(flags) if !flags.contains(NotificationFlags::WIRELESS) => {
if let Err(error) = bolt
.set_notification_flags(flags | NotificationFlags::WIRELESS)
.await
{
debug!(?error, "enable Bolt wireless notifications failed");
}
}
Ok(_) => {}
Err(error) => debug!(?error, "read Bolt notification state failed"),
}
if let Err(e) = bolt.trigger_device_arrival().await {
debug!(error = ?e, "trigger_device_arrival failed; receiver may report no devices");
return Vec::new();
}
let mut out = Vec::new();
loop {
match timeout(idle_timeout, rx.recv()).await {
Ok(Ok(BoltEvent::DeviceConnection(c))) => out.push(c),
Ok(Ok(_)) => {} Ok(Err(_)) | Err(_) => break,
}
}
out
}
async fn read_codename(channel: &HidppChannel, slot: u8) -> Option<String> {
let response = channel
.read_long_sub_register(0xFF, 0xB5, 0x60 + slot, [0x01, 0x00])
.await
.ok()?;
let len = usize::from(response[2]).min(13);
core::str::from_utf8(&response[3..3 + len])
.ok()
.map(str::to_string)
}