|
1 | | -use std::collections::HashSet; |
| 1 | +use std::{collections::HashSet, error::Error, io, sync::Arc}; |
2 | 2 |
|
| 3 | +use hidpp::channel::{HidppChannel, RawHidChannel}; |
3 | 4 | use openlogi_core::device::{ |
4 | 5 | Capabilities, DeviceInventory, DeviceKind, PairedDevice, ReceiverInfo, |
5 | 6 | }; |
| 7 | +use tokio::sync::{Mutex, mpsc}; |
6 | 8 |
|
7 | 9 | use super::cache::{CACHE_MISS_GRACE, CacheKey, CacheOutcome, Cached, REFRESH_TICKS, is_stale}; |
8 | 10 | use super::probe::{ |
9 | 11 | NodeProbe, assemble_bolt_probe, assemble_unifying_device, parse_codename_unifying, |
| 12 | + probe_unifying_features, |
10 | 13 | }; |
11 | 14 | use super::{Enumerator, ONESHOT_ATTEMPTS, one_shot_should_stop}; |
12 | 15 | use crate::inventory::features::ProbedFeatures; |
@@ -105,6 +108,101 @@ fn unifying_cached_features_do_not_override_current_liveness() { |
105 | 108 | ); |
106 | 109 | } |
107 | 110 |
|
| 111 | +struct BatteryErrorPingChannel { |
| 112 | + incoming_tx: mpsc::UnboundedSender<Vec<u8>>, |
| 113 | + incoming_rx: Mutex<mpsc::UnboundedReceiver<Vec<u8>>>, |
| 114 | +} |
| 115 | + |
| 116 | +impl BatteryErrorPingChannel { |
| 117 | + fn new() -> Self { |
| 118 | + let (incoming_tx, incoming_rx) = mpsc::unbounded_channel(); |
| 119 | + Self { |
| 120 | + incoming_tx, |
| 121 | + incoming_rx: Mutex::new(incoming_rx), |
| 122 | + } |
| 123 | + } |
| 124 | +} |
| 125 | + |
| 126 | +#[hidpp::async_trait] |
| 127 | +impl RawHidChannel for BatteryErrorPingChannel { |
| 128 | + fn vendor_id(&self) -> u16 { |
| 129 | + 0x046d |
| 130 | + } |
| 131 | + |
| 132 | + fn product_id(&self) -> u16 { |
| 133 | + 0xc52b |
| 134 | + } |
| 135 | + |
| 136 | + async fn write_report(&self, src: &[u8]) -> Result<usize, Box<dyn Error + Send + Sync>> { |
| 137 | + let mut response = src.to_vec(); |
| 138 | + if src.get(2).copied() != Some(0) { |
| 139 | + if response.len() < 7 { |
| 140 | + return Err(Box::new(io::Error::other("short mock HID++ report"))); |
| 141 | + } |
| 142 | + response[2] = 0xff; |
| 143 | + response[3] = src[2]; |
| 144 | + response[4] = src[3]; |
| 145 | + response[5] = 0x08; |
| 146 | + response[6] = 0; |
| 147 | + } |
| 148 | + self.incoming_tx.send(response).map_err(|_| { |
| 149 | + Box::new(io::Error::other("mock HID++ response receiver closed")) |
| 150 | + as Box<dyn Error + Send + Sync> |
| 151 | + })?; |
| 152 | + Ok(src.len()) |
| 153 | + } |
| 154 | + |
| 155 | + async fn read_report(&self, buf: &mut [u8]) -> Result<usize, Box<dyn Error + Send + Sync>> { |
| 156 | + let Some(report) = self.incoming_rx.lock().await.recv().await else { |
| 157 | + return Err(Box::new(io::Error::other( |
| 158 | + "mock HID++ response sender closed", |
| 159 | + ))); |
| 160 | + }; |
| 161 | + let len = report.len().min(buf.len()); |
| 162 | + buf[..len].copy_from_slice(&report[..len]); |
| 163 | + Ok(len) |
| 164 | + } |
| 165 | + |
| 166 | + fn supports_short_long_hidpp(&self) -> Option<(bool, bool)> { |
| 167 | + Some((true, true)) |
| 168 | + } |
| 169 | + |
| 170 | + async fn get_report_descriptor( |
| 171 | + &self, |
| 172 | + _buf: &mut [u8], |
| 173 | + ) -> Result<usize, Box<dyn Error + Send + Sync>> { |
| 174 | + unreachable!("mock declares HID++ support") |
| 175 | + } |
| 176 | +} |
| 177 | + |
| 178 | +#[tokio::test] |
| 179 | +async fn unifying_battery_failure_uses_root_ping_before_marking_offline() { |
| 180 | + let channel = Arc::new( |
| 181 | + HidppChannel::from_raw_channel(BatteryErrorPingChannel::new()) |
| 182 | + .await |
| 183 | + .unwrap_or_else(|e| panic!("mock HID++ channel should open: {e}")), |
| 184 | + ); |
| 185 | + let id = CacheKey::UnifyingSlot { |
| 186 | + receiver_uid: "receiver".to_string(), |
| 187 | + slot: 1, |
| 188 | + }; |
| 189 | + let cached = Cached { |
| 190 | + probe: ProbedFeatures { |
| 191 | + capabilities: Some(Capabilities::default()), |
| 192 | + ..ProbedFeatures::default() |
| 193 | + }, |
| 194 | + battery_index: Some(4), |
| 195 | + probed_tick: 10, |
| 196 | + }; |
| 197 | + |
| 198 | + let (_, _, online) = probe_unifying_features(&channel, 1, &id, Some(&cached), 11).await; |
| 199 | + |
| 200 | + assert!( |
| 201 | + online, |
| 202 | + "a failed battery refresh is inconclusive when the root ping still answers" |
| 203 | + ); |
| 204 | +} |
| 205 | + |
108 | 206 | fn inventory(slots: &[u8]) -> Vec<DeviceInventory> { |
109 | 207 | vec![DeviceInventory { |
110 | 208 | receiver: ReceiverInfo { |
|
0 commit comments