Skip to content

Commit 62e9dce

Browse files
committed
fix(bluetooth): actively reconnect paired HID receivers (2026.6.23.1-4240)
1 parent 67f0bd6 commit 62e9dce

18 files changed

Lines changed: 1097 additions & 33 deletions

bridge/src/cdc/bt_status.rs

Lines changed: 108 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,12 @@ use anyhow::{bail, Result};
33
use super::{
44
BT_STATUS_FIXED_LEN, BT_STATUS_FLAG_CONNECTED, BT_STATUS_FLAG_SEND_REQUESTED,
55
BT_STATUS_FLAG_STARTED, BT_STATUS_V1_FIXED_LEN, BT_STATUS_V1_VERSION, BT_STATUS_V2_FIXED_LEN,
6-
BT_STATUS_V2_VERSION, BT_STATUS_VERSION,
6+
BT_STATUS_V2_VERSION, BT_STATUS_V3_FIXED_LEN, BT_STATUS_V3_VERSION, BT_STATUS_VERSION,
77
};
88
#[derive(Clone, Debug, PartialEq, Eq)]
99
pub struct BtStatus {
10+
pub status_version: u8,
11+
pub decoded_status_version: u8,
1012
pub flags: u8,
1113
pub target: u8,
1214
pub last_status: u8,
@@ -57,6 +59,26 @@ pub struct BtStatus {
5759
pub last_encryption_enabled: u8,
5860
pub last_disconnection_reason: u8,
5961
pub last_hid_open_status: u8,
62+
pub reconnect_state: u8,
63+
pub reconnect_cycle_attempts: u8,
64+
pub last_reconnect_status: u8,
65+
pub last_reconnect_reason: u8,
66+
pub reconnect_schedule_count: u32,
67+
pub reconnect_attempt_count: u32,
68+
pub reconnect_success_count: u32,
69+
pub reconnect_failed_count: u32,
70+
pub reconnect_blocked_count: u32,
71+
pub last_reconnect_ms: u32,
72+
pub connection_complete_count: u32,
73+
pub last_connection_complete_status: u8,
74+
pub last_connection_complete_link_type: u8,
75+
pub last_connection_complete_ms: u32,
76+
pub incoming_l2cap_connection_count: u32,
77+
pub incoming_l2cap_hid_control_count: u32,
78+
pub incoming_l2cap_hid_interrupt_count: u32,
79+
pub last_incoming_l2cap_psm: u16,
80+
pub last_incoming_l2cap_local_cid: u16,
81+
pub last_incoming_l2cap_ms: u32,
6082
pub local_name: String,
6183
}
6284

@@ -85,6 +107,42 @@ impl BtStatus {
85107
|| self.disconnection_complete_count > 0
86108
|| self.last_security_event_ms > 0
87109
}
110+
111+
pub fn newer_status_version(&self) -> bool {
112+
self.status_version > BT_STATUS_VERSION
113+
}
114+
115+
pub fn reconnect_peer_known(&self) -> bool {
116+
self.reconnect_state & 0x01 != 0
117+
}
118+
119+
pub fn reconnect_pending(&self) -> bool {
120+
self.reconnect_state & 0x02 != 0
121+
}
122+
123+
pub fn reconnect_in_progress(&self) -> bool {
124+
self.reconnect_state & 0x04 != 0
125+
}
126+
127+
pub fn reconnect_maxed(&self) -> bool {
128+
self.reconnect_state & 0x08 != 0
129+
}
130+
131+
pub fn reconnect_activity_seen(&self) -> bool {
132+
self.reconnect_schedule_count > 0
133+
|| self.reconnect_attempt_count > 0
134+
|| self.reconnect_success_count > 0
135+
|| self.reconnect_failed_count > 0
136+
|| self.reconnect_blocked_count > 0
137+
|| self.last_reconnect_ms > 0
138+
}
139+
140+
pub fn incoming_hid_l2cap_seen(&self) -> bool {
141+
self.incoming_l2cap_connection_count > 0
142+
|| self.incoming_l2cap_hid_control_count > 0
143+
|| self.incoming_l2cap_hid_interrupt_count > 0
144+
|| self.last_incoming_l2cap_psm != 0
145+
}
88146
}
89147

90148
fn read_u16_le(payload: &[u8], offset: usize) -> u16 {
@@ -108,35 +166,58 @@ pub fn decode_bt_status_payload(payload: &[u8]) -> Result<BtStatus> {
108166
let fixed_len = match version {
109167
BT_STATUS_V1_VERSION => BT_STATUS_V1_FIXED_LEN,
110168
BT_STATUS_V2_VERSION => BT_STATUS_V2_FIXED_LEN,
169+
BT_STATUS_V3_VERSION => BT_STATUS_V3_FIXED_LEN,
111170
BT_STATUS_VERSION => BT_STATUS_FIXED_LEN,
171+
newer if newer > BT_STATUS_VERSION => BT_STATUS_V3_FIXED_LEN,
112172
_ => bail!(
113-
"BT_STATUS version mismatch (got {}, want {}, {}, or {})",
173+
"BT_STATUS version mismatch (got {}, want {}, {}, {}, or {})",
114174
payload[0],
115175
BT_STATUS_V1_VERSION,
116176
BT_STATUS_V2_VERSION,
177+
BT_STATUS_V3_VERSION,
117178
BT_STATUS_VERSION
118179
),
119180
};
120181
if payload.len() < fixed_len {
121182
bail!("BT_STATUS response truncated ({} bytes)", payload.len());
122183
}
184+
let decode_name = version <= BT_STATUS_VERSION;
123185
let name_len_offset = match version {
124186
BT_STATUS_V1_VERSION => BT_STATUS_V1_FIXED_LEN - 1,
125187
BT_STATUS_V2_VERSION => BT_STATUS_V2_FIXED_LEN - 1,
126-
_ => BT_STATUS_FIXED_LEN - 1,
188+
BT_STATUS_V3_VERSION => BT_STATUS_V3_FIXED_LEN - 1,
189+
BT_STATUS_VERSION => BT_STATUS_FIXED_LEN - 1,
190+
_ => payload.len(),
127191
};
128192
let name_start = fixed_len;
129-
let name_len = payload[name_len_offset] as usize;
130-
let need = name_start + name_len;
193+
let name_len = if decode_name {
194+
payload[name_len_offset] as usize
195+
} else {
196+
0
197+
};
198+
let need = if decode_name {
199+
name_start + name_len
200+
} else {
201+
name_start
202+
};
131203
if payload.len() < need {
132204
bail!(
133205
"BT_STATUS local name truncated (need {need} bytes, have {})",
134206
payload.len()
135207
);
136208
}
137-
let has_v2 = version == BT_STATUS_V2_VERSION || version == BT_STATUS_VERSION;
138-
let has_v3 = version == BT_STATUS_VERSION;
209+
let has_v2 = version == BT_STATUS_V2_VERSION
210+
|| version == BT_STATUS_V3_VERSION
211+
|| version >= BT_STATUS_VERSION;
212+
let has_v3 = version == BT_STATUS_V3_VERSION || version >= BT_STATUS_VERSION;
213+
let has_v4 = version == BT_STATUS_VERSION;
139214
Ok(BtStatus {
215+
status_version: version,
216+
decoded_status_version: if version > BT_STATUS_VERSION {
217+
BT_STATUS_V3_VERSION
218+
} else {
219+
version
220+
},
140221
flags: payload[1],
141222
target: payload[2],
142223
last_status: payload[3],
@@ -187,6 +268,26 @@ pub fn decode_bt_status_payload(payload: &[u8]) -> Result<BtStatus> {
187268
last_encryption_enabled: if has_v3 { payload[145] } else { 0 },
188269
last_disconnection_reason: if has_v3 { payload[146] } else { 0 },
189270
last_hid_open_status: if has_v3 { payload[147] } else { 0 },
271+
reconnect_state: if has_v4 { payload[152] } else { 0 },
272+
reconnect_cycle_attempts: if has_v4 { payload[153] } else { 0 },
273+
last_reconnect_status: if has_v4 { payload[154] } else { 0 },
274+
last_reconnect_reason: if has_v4 { payload[155] } else { 0 },
275+
reconnect_schedule_count: if has_v4 { read_u32_le(payload, 156) } else { 0 },
276+
reconnect_attempt_count: if has_v4 { read_u32_le(payload, 160) } else { 0 },
277+
reconnect_success_count: if has_v4 { read_u32_le(payload, 164) } else { 0 },
278+
reconnect_failed_count: if has_v4 { read_u32_le(payload, 168) } else { 0 },
279+
reconnect_blocked_count: if has_v4 { read_u32_le(payload, 172) } else { 0 },
280+
last_reconnect_ms: if has_v4 { read_u32_le(payload, 176) } else { 0 },
281+
connection_complete_count: if has_v4 { read_u32_le(payload, 180) } else { 0 },
282+
last_connection_complete_status: if has_v4 { payload[184] } else { 0 },
283+
last_connection_complete_link_type: if has_v4 { payload[185] } else { 0 },
284+
last_connection_complete_ms: if has_v4 { read_u32_le(payload, 188) } else { 0 },
285+
incoming_l2cap_connection_count: if has_v4 { read_u32_le(payload, 192) } else { 0 },
286+
incoming_l2cap_hid_control_count: if has_v4 { read_u32_le(payload, 196) } else { 0 },
287+
incoming_l2cap_hid_interrupt_count: if has_v4 { read_u32_le(payload, 200) } else { 0 },
288+
last_incoming_l2cap_psm: if has_v4 { read_u16_le(payload, 204) } else { 0 },
289+
last_incoming_l2cap_local_cid: if has_v4 { read_u16_le(payload, 206) } else { 0 },
290+
last_incoming_l2cap_ms: if has_v4 { read_u32_le(payload, 208) } else { 0 },
190291
local_name: String::from_utf8_lossy(&payload[name_start..need]).into_owned(),
191292
})
192293
}

bridge/src/cdc/frame.rs

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -77,12 +77,14 @@ pub const HELLO_FLAG_WIFI_JOINED: u8 = 0x02;
7777
pub const HELLO_FLAG_RUN_MODE_OK: u8 = 0x04;
7878
pub const HELLO_FLAG_RUN_MODE_ACTIVE: u8 = 0x08;
7979

80-
pub const BT_STATUS_VERSION: u8 = 3;
80+
pub const BT_STATUS_VERSION: u8 = 4;
8181
pub const BT_STATUS_V1_VERSION: u8 = 1;
8282
pub const BT_STATUS_V1_FIXED_LEN: usize = 49;
8383
pub const BT_STATUS_V2_VERSION: u8 = 2;
8484
pub const BT_STATUS_V2_FIXED_LEN: usize = 99;
85-
pub const BT_STATUS_FIXED_LEN: usize = 153;
85+
pub const BT_STATUS_V3_VERSION: u8 = 3;
86+
pub const BT_STATUS_V3_FIXED_LEN: usize = 153;
87+
pub const BT_STATUS_FIXED_LEN: usize = 213;
8688
pub const BT_STATUS_FLAG_STARTED: u8 = 1 << 0;
8789
pub const BT_STATUS_FLAG_CONNECTED: u8 = 1 << 1;
8890
pub const BT_STATUS_FLAG_SEND_REQUESTED: u8 = 1 << 2;

bridge/src/cdc/tests.rs

Lines changed: 108 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -87,8 +87,28 @@ fn bt_status_payload_decodes_wire_shape() {
8787
payload[145] = 1;
8888
payload[146] = 0x13;
8989
payload[147] = 0x44;
90-
payload[152] = name.len() as u8;
91-
payload[153..].copy_from_slice(name);
90+
payload[152] = 0x07;
91+
payload[153] = 2;
92+
payload[154] = 0x21;
93+
payload[155] = 4;
94+
payload[156..160].copy_from_slice(&28u32.to_le_bytes());
95+
payload[160..164].copy_from_slice(&29u32.to_le_bytes());
96+
payload[164..168].copy_from_slice(&30u32.to_le_bytes());
97+
payload[168..172].copy_from_slice(&31u32.to_le_bytes());
98+
payload[172..176].copy_from_slice(&32u32.to_le_bytes());
99+
payload[176..180].copy_from_slice(&0x1020_3040u32.to_le_bytes());
100+
payload[180..184].copy_from_slice(&33u32.to_le_bytes());
101+
payload[184] = 0x35;
102+
payload[185] = 1;
103+
payload[188..192].copy_from_slice(&0x4050_6070u32.to_le_bytes());
104+
payload[192..196].copy_from_slice(&34u32.to_le_bytes());
105+
payload[196..200].copy_from_slice(&35u32.to_le_bytes());
106+
payload[200..204].copy_from_slice(&36u32.to_le_bytes());
107+
payload[204..206].copy_from_slice(&0x0013u16.to_le_bytes());
108+
payload[206..208].copy_from_slice(&0x0041u16.to_le_bytes());
109+
payload[208..212].copy_from_slice(&0x5060_7080u32.to_le_bytes());
110+
payload[212] = name.len() as u8;
111+
payload[213..].copy_from_slice(name);
92112

93113
let status = decode_bt_status_payload(&payload).unwrap();
94114

@@ -144,10 +164,67 @@ fn bt_status_payload_decodes_wire_shape() {
144164
assert_eq!(status.last_encryption_enabled, 1);
145165
assert_eq!(status.last_disconnection_reason, 0x13);
146166
assert_eq!(status.last_hid_open_status, 0x44);
167+
assert_eq!(status.reconnect_state, 0x07);
168+
assert!(status.reconnect_peer_known());
169+
assert!(status.reconnect_pending());
170+
assert!(status.reconnect_in_progress());
171+
assert!(!status.reconnect_maxed());
172+
assert_eq!(status.reconnect_cycle_attempts, 2);
173+
assert_eq!(status.last_reconnect_status, 0x21);
174+
assert_eq!(status.last_reconnect_reason, 4);
175+
assert_eq!(status.reconnect_schedule_count, 28);
176+
assert_eq!(status.reconnect_attempt_count, 29);
177+
assert_eq!(status.reconnect_success_count, 30);
178+
assert_eq!(status.reconnect_failed_count, 31);
179+
assert_eq!(status.reconnect_blocked_count, 32);
180+
assert_eq!(status.last_reconnect_ms, 0x10203040);
181+
assert!(status.reconnect_activity_seen());
182+
assert_eq!(status.connection_complete_count, 33);
183+
assert_eq!(status.last_connection_complete_status, 0x35);
184+
assert_eq!(status.last_connection_complete_link_type, 1);
185+
assert_eq!(status.last_connection_complete_ms, 0x40506070);
186+
assert_eq!(status.incoming_l2cap_connection_count, 34);
187+
assert_eq!(status.incoming_l2cap_hid_control_count, 35);
188+
assert_eq!(status.incoming_l2cap_hid_interrupt_count, 36);
189+
assert_eq!(status.last_incoming_l2cap_psm, 0x0013);
190+
assert_eq!(status.last_incoming_l2cap_local_cid, 0x0041);
191+
assert_eq!(status.last_incoming_l2cap_ms, 0x50607080);
192+
assert!(status.incoming_hid_l2cap_seen());
147193
assert!(status.pairing_security_contact_seen());
148194
assert_eq!(status.local_name, "CouchLink BT HID");
149195
}
150196

197+
#[test]
198+
fn bt_status_payload_decodes_v3_wire_shape() {
199+
let name = b"Security BT";
200+
let mut payload = vec![0u8; BT_STATUS_V3_FIXED_LEN + name.len()];
201+
payload[0] = BT_STATUS_V3_VERSION;
202+
payload[1] = BT_STATUS_FLAG_STARTED;
203+
payload[2] = 1;
204+
payload[106..110].copy_from_slice(&4u32.to_le_bytes());
205+
payload[110..114].copy_from_slice(&4u32.to_le_bytes());
206+
payload[122..126].copy_from_slice(&1u32.to_le_bytes());
207+
payload[146] = 0x16;
208+
payload[152] = name.len() as u8;
209+
payload[153..].copy_from_slice(name);
210+
211+
let status = decode_bt_status_payload(&payload).unwrap();
212+
213+
assert_eq!(status.status_version, BT_STATUS_V3_VERSION);
214+
assert_eq!(status.decoded_status_version, BT_STATUS_V3_VERSION);
215+
assert!(status.started());
216+
assert_eq!(status.target, 1);
217+
assert_eq!(status.user_confirmation_request_count, 4);
218+
assert_eq!(status.user_confirmation_response_count, 4);
219+
assert_eq!(status.link_key_notification_count, 1);
220+
assert_eq!(status.last_disconnection_reason, 0x16);
221+
assert!(status.pairing_security_contact_seen());
222+
assert_eq!(status.reconnect_attempt_count, 0);
223+
assert_eq!(status.connection_complete_count, 0);
224+
assert_eq!(status.incoming_l2cap_connection_count, 0);
225+
assert_eq!(status.local_name, "Security BT");
226+
}
227+
151228
#[test]
152229
fn bt_status_payload_decodes_v2_wire_shape() {
153230
let name = b"Previous BT";
@@ -168,6 +245,9 @@ fn bt_status_payload_decodes_v2_wire_shape() {
168245
assert_eq!(status.pin_code_request_count, 0);
169246
assert_eq!(status.user_confirmation_request_count, 0);
170247
assert_eq!(status.hid_open_failed_count, 0);
248+
assert_eq!(status.reconnect_attempt_count, 0);
249+
assert_eq!(status.connection_complete_count, 0);
250+
assert_eq!(status.incoming_l2cap_connection_count, 0);
171251
assert!(!status.pairing_security_contact_seen());
172252
assert_eq!(status.local_name, "Previous BT");
173253
}
@@ -195,6 +275,9 @@ fn bt_status_payload_decodes_v1_wire_shape() {
195275
assert_eq!(status.pin_code_request_count, 0);
196276
assert_eq!(status.user_confirmation_request_count, 0);
197277
assert_eq!(status.hid_open_failed_count, 0);
278+
assert_eq!(status.reconnect_attempt_count, 0);
279+
assert_eq!(status.connection_complete_count, 0);
280+
assert_eq!(status.incoming_l2cap_connection_count, 0);
198281
assert!(!status.pairing_security_contact_seen());
199282
assert_eq!(status.local_name, "Legacy BT");
200283
}
@@ -203,10 +286,31 @@ fn bt_status_payload_decodes_v1_wire_shape() {
203286
fn bt_status_payload_rejects_bad_shape() {
204287
assert!(decode_bt_status_payload(&[0; BT_STATUS_FIXED_LEN - 1]).is_err());
205288
let mut bad_version = vec![0u8; BT_STATUS_FIXED_LEN];
206-
bad_version[0] = BT_STATUS_VERSION + 1;
289+
bad_version[0] = 0;
207290
assert!(decode_bt_status_payload(&bad_version).is_err());
208291
let mut bad_name = vec![0u8; BT_STATUS_FIXED_LEN];
209292
bad_name[0] = BT_STATUS_VERSION;
210-
bad_name[152] = 1;
293+
bad_name[212] = 1;
211294
assert!(decode_bt_status_payload(&bad_name).is_err());
212295
}
296+
297+
#[test]
298+
fn bt_status_payload_decodes_future_version_prefix() {
299+
let mut payload = vec![0u8; BT_STATUS_V3_FIXED_LEN + 8];
300+
payload[0] = BT_STATUS_VERSION + 1;
301+
payload[1] = BT_STATUS_FLAG_STARTED;
302+
payload[2] = 1;
303+
payload[106..110].copy_from_slice(&2u32.to_le_bytes());
304+
payload[146] = 0x16;
305+
306+
let status = decode_bt_status_payload(&payload).unwrap();
307+
308+
assert_eq!(status.status_version, BT_STATUS_VERSION + 1);
309+
assert_eq!(status.decoded_status_version, BT_STATUS_V3_VERSION);
310+
assert!(status.newer_status_version());
311+
assert!(status.started());
312+
assert_eq!(status.user_confirmation_request_count, 2);
313+
assert_eq!(status.last_disconnection_reason, 0x16);
314+
assert_eq!(status.reconnect_attempt_count, 0);
315+
assert_eq!(status.local_name, "");
316+
}

0 commit comments

Comments
 (0)