use std::sync::Arc;
use hidpp::channel::HidppChannel;
use openlogi_core::device::{BatteryInfo, BatteryStatus};
use super::features::{BatteryProbe, ProbedFeatures, probe_features, read_battery};
pub(super) const REFRESH_TICKS: u64 = 15;
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub(super) enum CacheKey {
Bolt { unit_id: [u8; 4] },
UnifyingSlot { receiver_uid: String, slot: u8 },
Direct(async_hid::DeviceId),
}
pub(super) const CACHE_MISS_GRACE: u8 = 3;
#[derive(Clone)]
pub(super) struct Cached {
pub(super) probe: ProbedFeatures,
pub(super) battery: Option<BatteryProbe>,
pub(super) probed_tick: u64,
}
fn hold_percentage_while_charging(
fresh: BatteryInfo,
prev: Option<&BatteryInfo>,
probe: BatteryProbe,
) -> BatteryInfo {
if !matches!(probe, BatteryProbe::Legacy(_)) {
return fresh;
}
let charging = matches!(
fresh.status,
BatteryStatus::Charging | BatteryStatus::ChargingSlow
);
if charging
&& fresh.percentage == 0
&& let Some(p) = prev.filter(|p| p.percentage > 0)
{
return BatteryInfo {
percentage: p.percentage,
level: p.level,
status: fresh.status,
};
}
fresh
}
pub(super) enum CacheOutcome {
Fresh(CacheKey, Cached),
Update(CacheKey, Cached),
Seen(CacheKey),
Unkeyed,
}
pub(super) fn seen(id: Option<CacheKey>) -> CacheOutcome {
id.map_or(CacheOutcome::Unkeyed, CacheOutcome::Seen)
}
pub(super) fn is_stale(cached: &Cached, tick: u64) -> bool {
tick.wrapping_sub(cached.probed_tick) >= REFRESH_TICKS
}
pub(super) async fn probe_or_reuse(
channel: &Arc<HidppChannel>,
index: u8,
id: Option<CacheKey>,
cached: Option<&Cached>,
online: bool,
tick: u64,
) -> (ProbedFeatures, CacheOutcome) {
if online && cached.is_none_or(|c| is_stale(c, tick)) {
let (mut fresh, battery) = probe_features(channel, index).await;
if let (Some(reading), Some(probe)) = (fresh.battery.take(), battery) {
fresh.battery = Some(hold_percentage_while_charging(
reading,
cached.and_then(|c| c.probe.battery.as_ref()),
probe,
));
}
if fresh.capabilities.is_some() {
if let Some(c) = cached {
backfill_identity(&mut fresh, &c.probe);
}
if fresh.identity_incomplete && cached.is_none() {
return (fresh, seen(id));
}
return match id {
Some(key) => {
let value = Cached {
probe: fresh.clone(),
battery,
probed_tick: tick,
};
(fresh, CacheOutcome::Fresh(key, value))
}
None => (fresh, CacheOutcome::Unkeyed),
};
}
return match cached {
Some(c) => (c.probe.clone(), seen(id)),
None => (fresh, seen(id)),
};
}
match cached {
Some(c) => {
if online
&& let Some(probe) = c.battery
&& let Some(key) = id.clone()
&& let Some(battery) = read_battery(channel, index, probe).await
{
let battery =
hold_percentage_while_charging(battery, c.probe.battery.as_ref(), probe);
let mut entry = c.clone();
entry.probe.battery = Some(battery);
return (entry.probe.clone(), CacheOutcome::Update(key, entry));
}
(c.probe.clone(), seen(id))
}
None => (ProbedFeatures::default(), seen(id)),
}
}
pub(super) fn backfill_identity(fresh: &mut ProbedFeatures, cached: &ProbedFeatures) {
if fresh.kind.is_none() {
fresh.kind = cached.kind;
}
if !fresh.identity_incomplete {
return;
}
match (fresh.model_info.as_mut(), cached.model_info.as_ref()) {
(None, Some(previous)) => {
fresh.model_info = Some(previous.clone());
fresh.identity_incomplete = false;
}
(Some(now), Some(previous))
if now.serial_number.is_none() && previous.serial_number.is_some() =>
{
now.serial_number.clone_from(&previous.serial_number);
fresh.identity_incomplete = false;
}
_ => {}
}
}
#[cfg(test)]
mod hold_tests {
use openlogi_core::device::{BatteryInfo, BatteryLevel, BatteryStatus};
use super::{BatteryProbe, hold_percentage_while_charging};
fn battery(percentage: u8, status: BatteryStatus) -> BatteryInfo {
BatteryInfo {
percentage,
level: BatteryLevel::Good,
status,
}
}
#[test]
fn charging_zero_holds_last_known_percentage() {
let legacy = BatteryProbe::Legacy(0);
let held = hold_percentage_while_charging(
battery(0, BatteryStatus::Charging),
Some(&battery(85, BatteryStatus::Discharging)),
legacy,
);
assert_eq!(held.percentage, 85);
assert_eq!(held.status, BatteryStatus::Charging);
let discharging = hold_percentage_while_charging(
battery(0, BatteryStatus::Discharging),
Some(&battery(85, BatteryStatus::Discharging)),
legacy,
);
assert_eq!(discharging.percentage, 0);
let live = hold_percentage_while_charging(
battery(40, BatteryStatus::Charging),
Some(&battery(85, BatteryStatus::Discharging)),
legacy,
);
assert_eq!(live.percentage, 40);
let cold =
hold_percentage_while_charging(battery(0, BatteryStatus::Charging), None, legacy);
assert_eq!(cold.percentage, 0);
}
#[test]
fn unified_charging_zero_is_not_held() {
let live = hold_percentage_while_charging(
battery(0, BatteryStatus::Charging),
Some(&battery(85, BatteryStatus::Discharging)),
BatteryProbe::Unified(0),
);
assert_eq!(live.percentage, 0);
}
}