1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
|
//! Status protocol for the MMX 150 wireless dongle (reverse-engineered).
//!
//! The dongle rejects `GET_FEATURE`/`GET_INPUT` control transfers, so status
//! queries are write-then-read exchanges over the vendor output reports.
//!
//! Link state: writing output report `0x3a` (`3a 09 22 01 00...`) elicits an
//! input report `0x3c` (`3c 01 00 00 00 01 ...`) on the interrupt IN endpoint
//! if and only if the headset is linked to the dongle. No response within the
//! timeout means the headset is powered off or out of range.
/// Vendor output report IDs available on the dongle.
pub mod reports {
/// 60-byte output/feature report.
pub const OUT_60_A: u8 = 0x2a;
/// 60-byte feature report.
pub const FEAT_60_B: u8 = 0x2b;
/// 60-byte output/feature report.
pub const OUT_60_C: u8 = 0x3a;
/// 60-byte feature report.
pub const FEAT_60_D: u8 = 0x3b;
/// 16-byte output report.
pub const OUT_16_A: u8 = 0x2d;
/// 16-byte output report.
pub const OUT_16_B: u8 = 0x3d;
}
use std::io;
use std::time::{Duration, Instant};
use crate::hid::Hidraw;
/// Consecutive missed polls required before declaring the headset
/// disconnected (guards against dropped RF packets).
pub const LINK_DEBOUNCE_MISSES: u32 = 2;
/// How long to wait for a link response after sending the query.
const LINK_RESPONSE_TIMEOUT: Duration = Duration::from_millis(400);
/// Headset link state as seen through the dongle.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum LinkState {
Connected,
Disconnected,
}
/// A confirmed change of [`LinkState`], with `initial` set when the
/// transition is the daemon's first determination of the state (rather than
/// a change from a previously confirmed state).
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Transition {
ToConnected { initial: bool },
ToDisconnected { initial: bool },
}
/// Builds the 61-byte link query output report (report id `0x3a` + payload).
pub fn link_query_frame() -> Vec<u8> {
let mut frame = vec![0u8; 61];
frame[0] = reports::OUT_60_C;
frame[1] = 0x09;
frame[2] = 0x22;
frame[3] = 0x01;
frame
}
/// True if `data` is a link-status response report (`0x3c` echoing the
/// query selector).
pub fn is_link_response(data: &[u8]) -> bool {
data.len() >= 2 && data[0] == 0x3c && data[1] == 0x01
}
/// Queries the dongle once for the headset link state. Returns
/// `Ok(Disconnected)` if no response arrives within the timeout window.
pub fn link_status(hid: &mut Hidraw) -> io::Result<LinkState> {
hid.write(&link_query_frame())?;
let deadline = Instant::now() + LINK_RESPONSE_TIMEOUT;
while let Some(remaining) = deadline.checked_duration_since(Instant::now()) {
match hid.read_with_timeout(remaining)? {
Some(data) if is_link_response(&data) => return Ok(LinkState::Connected),
Some(_) => continue,
None => break,
}
}
Ok(LinkState::Disconnected)
}
/// Tracks link state across polls, debouncing disconnects and reporting
/// transitions. State starts unknown (`None`) and is pinned by the first
/// confirmed determination; `fire_initial` controls whether that first
/// determination is reported as a transition.
#[derive(Debug)]
pub struct StateTracker {
state: Option<LinkState>,
misses: u32,
fire_initial: bool,
}
impl StateTracker {
pub fn new(fire_initial: bool) -> StateTracker {
StateTracker {
state: None,
misses: 0,
fire_initial,
}
}
/// Current confirmed state, `None` until the first determination.
pub fn state(&self) -> Option<LinkState> {
self.state
}
/// Feed one poll result (`true` = link response seen) and get any
/// confirmed transition.
pub fn update(&mut self, link_up: bool) -> Option<Transition> {
if link_up {
self.misses = 0;
if self.state == Some(LinkState::Connected) {
return None;
}
let initial = self.state.is_none();
self.state = Some(LinkState::Connected);
if initial && !self.fire_initial {
None
} else {
Some(Transition::ToConnected { initial })
}
} else {
self.misses += 1;
if self.misses < LINK_DEBOUNCE_MISSES || self.state == Some(LinkState::Disconnected) {
return None;
}
let initial = self.state.is_none();
self.state = Some(LinkState::Disconnected);
if initial && !self.fire_initial {
None
} else {
Some(Transition::ToDisconnected { initial })
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn link_query_frame_shape() {
let f = link_query_frame();
assert_eq!(f.len(), 61);
assert_eq!(&f[..4], &[0x3a, 0x09, 0x22, 0x01]);
assert!(f[4..].iter().all(|&b| b == 0));
}
#[test]
fn link_response_accepted() {
let r = [0x3c, 0x01, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00];
assert!(is_link_response(&r));
}
#[test]
fn link_response_rejects_other_report_ids() {
let mut r = vec![0x3c, 0x01, 0x00, 0x00, 0x00, 0x01];
r[0] = 0x2c;
assert!(!is_link_response(&r));
assert!(!is_link_response(&[0x3d, 0x01]));
}
#[test]
fn link_response_rejects_wrong_echo() {
let r = [0x3c, 0x09, 0x00, 0x00, 0x00, 0x01];
assert!(!is_link_response(&r));
}
#[test]
fn link_response_rejects_short_frames() {
assert!(!is_link_response(&[]));
assert!(!is_link_response(&[0x3c]));
}
#[test]
fn initial_connect_fires() {
let mut t = StateTracker::new(true);
assert_eq!(
t.update(true),
Some(Transition::ToConnected { initial: true })
);
assert_eq!(t.state(), Some(LinkState::Connected));
}
#[test]
fn initial_disconnect_fires_after_debounce() {
let mut t = StateTracker::new(true);
assert_eq!(t.update(false), None);
assert_eq!(t.state(), None);
assert_eq!(
t.update(false),
Some(Transition::ToDisconnected { initial: true })
);
assert_eq!(t.state(), Some(LinkState::Disconnected));
}
#[test]
fn single_miss_does_not_disconnect() {
let mut t = StateTracker::new(true);
assert_eq!(
t.update(true),
Some(Transition::ToConnected { initial: true })
);
assert_eq!(t.update(false), None);
assert_eq!(t.update(true), None);
assert_eq!(t.state(), Some(LinkState::Connected));
}
#[test]
fn misses_reset_on_hit() {
let mut t = StateTracker::new(true);
assert_eq!(
t.update(true),
Some(Transition::ToConnected { initial: true })
);
assert_eq!(t.update(false), None);
assert_eq!(t.update(true), None);
assert_eq!(t.update(false), None);
assert_eq!(t.update(true), None);
assert_eq!(t.state(), Some(LinkState::Connected));
}
#[test]
fn two_misses_disconnect_then_reconnect() {
let mut t = StateTracker::new(true);
assert_eq!(
t.update(true),
Some(Transition::ToConnected { initial: true })
);
assert_eq!(t.update(false), None);
assert_eq!(
t.update(false),
Some(Transition::ToDisconnected { initial: false })
);
assert_eq!(t.update(false), None);
assert_eq!(
t.update(true),
Some(Transition::ToConnected { initial: false })
);
}
#[test]
fn no_initial_fires_suppressed_but_later_fire() {
let mut t = StateTracker::new(false);
assert_eq!(t.update(true), None);
assert_eq!(t.state(), Some(LinkState::Connected));
assert_eq!(t.update(false), None);
assert_eq!(
t.update(false),
Some(Transition::ToDisconnected { initial: false })
);
assert_eq!(
t.update(true),
Some(Transition::ToConnected { initial: false })
);
}
#[test]
fn suppressed_initial_still_tracks_state() {
let mut t = StateTracker::new(false);
assert_eq!(t.update(false), None);
assert_eq!(t.update(false), None);
assert_eq!(t.state(), Some(LinkState::Disconnected));
assert_eq!(
t.update(true),
Some(Transition::ToConnected { initial: false })
);
}
}
|