@@ -593,15 +593,11 @@ fn selected_pico_xinput_source_error(instance_ids: &[String]) -> String {
593593}
594594
595595async fn wait_for_bluetooth_source_slots ( routes : & mut [ StreamRoute ] , quiet : bool ) -> Result < ( ) > {
596- let bluetooth_route_count = routes
597- . iter ( )
598- . filter ( |route| route. pico . persona . is_bluetooth ( ) )
599- . count ( ) ;
600- if bluetooth_route_count == 0 {
596+ if !routes. iter ( ) . any ( |route| route. pico . persona . is_bluetooth ( ) ) {
601597 return Ok ( ( ) ) ;
602598 }
603599
604- let allow_auto_switch = bluetooth_route_count == 1 ;
600+ let allow_auto_switch = should_auto_switch_bluetooth_source ( routes ) ;
605601 let deadline = Instant :: now ( ) + BLUETOOTH_SOURCE_WAIT ;
606602 let mut announced_wait = false ;
607603 loop {
@@ -723,6 +719,10 @@ fn bluetooth_source_slot_decision(
723719 BluetoothSourceSlotDecision :: Missing
724720}
725721
722+ fn should_auto_switch_bluetooth_source ( routes : & [ StreamRoute ] ) -> bool {
723+ routes. len ( ) == 1 && routes[ 0 ] . pico . persona . is_bluetooth ( )
724+ }
725+
726726#[ derive( Clone , Debug ) ]
727727struct MissingBluetoothSource {
728728 pico_uid : String ,
@@ -822,11 +822,12 @@ pub async fn stream_routes(routes: Vec<StreamRoute>, options: StreamOptions) ->
822822 print_stream_intro ( & routes, socket. local_addr ( ) ?) ;
823823 }
824824
825+ let allow_bluetooth_source_auto_switch = should_auto_switch_bluetooth_source ( & routes) ;
825826 let mut runtime: Vec < RouteRuntime > = routes
826827 . into_iter ( )
827828 . map ( |route| {
828829 let bluetooth_usb = bluetooth_usb_links. remove ( & route. pico . info . unique_id_short ) ;
829- RouteRuntime :: new ( route, bluetooth_usb)
830+ RouteRuntime :: new ( route, bluetooth_usb, allow_bluetooth_source_auto_switch )
830831 } )
831832 . collect ( ) ;
832833 // Bring the injected-input keyboard hook up before the first tick so a
@@ -972,14 +973,21 @@ struct RouteRuntime {
972973 last_key : protocol:: KeyboardReport ,
973974 last_packet_number : Option < u32 > ,
974975 source_connected : bool ,
976+ allow_source_auto_switch : bool ,
977+ source_auto_switch_count : u32 ,
978+ last_source_auto_switch : Option < String > ,
975979 last_send_type : & ' static str ,
976980 last_recovery_attempt : Option < Instant > ,
977981 recovery_hint_printed : bool ,
978982 bluetooth_pairing_hint_printed : bool ,
979983}
980984
981985impl RouteRuntime {
982- fn new ( route : StreamRoute , bluetooth_usb : Option < cdc:: PicoSetup > ) -> Self {
986+ fn new (
987+ route : StreamRoute ,
988+ bluetooth_usb : Option < cdc:: PicoSetup > ,
989+ allow_source_auto_switch : bool ,
990+ ) -> Self {
983991 Self {
984992 route,
985993 bluetooth_usb,
@@ -996,6 +1004,9 @@ impl RouteRuntime {
9961004 last_key : protocol:: KeyboardReport :: default ( ) ,
9971005 last_packet_number : None ,
9981006 source_connected : false ,
1007+ allow_source_auto_switch,
1008+ source_auto_switch_count : 0 ,
1009+ last_source_auto_switch : None ,
9991010 last_send_type : "heartbeat" ,
10001011 last_recovery_attempt : None ,
10011012 recovery_hint_printed : false ,
@@ -1069,7 +1080,8 @@ impl RouteRuntime {
10691080 }
10701081
10711082 fn next_controller_packet ( & mut self ) -> Packet {
1072- let source = xinput:: read_slot ( self . route . source_slot ) ;
1083+ let source = xinput:: read_slot ( self . route . source_slot )
1084+ . or_else ( || self . try_auto_switch_bluetooth_source ( ) ) ;
10731085 let ( state, packet_number, connected) = match source {
10741086 Some ( snapshot) => ( snapshot. state , Some ( snapshot. packet_number ) , true ) ,
10751087 None => ( GamepadState :: default ( ) , None , false ) ,
@@ -1091,6 +1103,40 @@ impl RouteRuntime {
10911103 packet
10921104 }
10931105
1106+ fn try_auto_switch_bluetooth_source ( & mut self ) -> Option < xinput:: SlotSnapshot > {
1107+ if !self . allow_source_auto_switch || !self . route . pico . persona . is_bluetooth ( ) {
1108+ return None ;
1109+ }
1110+ let connected = xinput:: connected_slots ( ) ;
1111+ let BluetoothSourceSlotDecision :: AutoSwitch { from, to } =
1112+ bluetooth_source_slot_decision ( self . route . source_slot , & connected, true )
1113+ else {
1114+ return None ;
1115+ } ;
1116+ let snapshot = connected
1117+ . iter ( )
1118+ . find ( |snapshot| snapshot. slot == to)
1119+ . copied ( ) ?;
1120+ self . route . source_slot = to;
1121+ self . source_auto_switch_count = self . source_auto_switch_count . saturating_add ( 1 ) ;
1122+ self . last_source_auto_switch = Some ( format ! (
1123+ "{} -> {}" ,
1124+ xinput:: user_slot_label( from) ,
1125+ xinput:: user_slot_label( to)
1126+ ) ) ;
1127+ self . last_packet_number = None ;
1128+ self . source_connected = false ;
1129+ tracing:: warn!(
1130+ "stream: Bluetooth source auto-switched during stream pico={} from {} to {} packet={} {}" ,
1131+ self . route. pico. uid_hex( ) ,
1132+ xinput:: user_slot_label( from) ,
1133+ xinput:: user_slot_label( to) ,
1134+ snapshot. packet_number,
1135+ format_xinput_snapshot( & snapshot)
1136+ ) ;
1137+ Some ( snapshot)
1138+ }
1139+
10941140 fn next_keyboard_packet ( & mut self ) -> Packet {
10951141 let report = keyboard:: read_keyboard ( ) ;
10961142 // The host keyboard is always present in the Parsec model, so the
@@ -1269,16 +1315,27 @@ fn print_status(routes: &mut [RouteRuntime]) {
12691315 sent_delta : Some ( sent_delta) ,
12701316 last_inbound_ms_ago,
12711317 source_connected : Some ( route. source_connected ) ,
1318+ source_auto_switch_count : Some ( route. source_auto_switch_count ) ,
1319+ last_source_auto_switch : route. last_source_auto_switch . clone ( ) ,
12721320 last_send_type : Some ( route. last_send_type . to_string ( ) ) ,
12731321 } ) ,
12741322 ) ;
1323+ let source_switch_note = match (
1324+ route. source_auto_switch_count ,
1325+ route. last_source_auto_switch . as_deref ( ) ,
1326+ ) {
1327+ ( 0 , _) => String :: new ( ) ,
1328+ ( count, Some ( last) ) => format ! ( " | source auto-switches {count} last {last}" ) ,
1329+ ( count, None ) => format ! ( " | source auto-switches {count}" ) ,
1330+ } ;
12751331 if bluetooth_route {
12761332 println ! (
1277- " {} -> {} ({}) | {} | PC USB input +{} total {} | Bluetooth output {} | {} {}" ,
1333+ " {} -> {} ({}) | {}{} | PC USB input +{} total {} | Bluetooth output {} | {} {}" ,
12781334 route. route. source_label( ) ,
12791335 route. route. pico. uid_hex( ) ,
12801336 route. route. pico. persona. label( ) ,
12811337 source_state,
1338+ source_switch_note,
12821339 sent_delta,
12831340 route. sent_total,
12841341 peer_state,
@@ -1287,10 +1344,11 @@ fn print_status(routes: &mut [RouteRuntime]) {
12871344 ) ;
12881345 } else {
12891346 println ! (
1290- " {} -> {} | {} | out +{} total {} | in {} ({}) | {} {}" ,
1347+ " {} -> {} | {}{} | out +{} total {} | in {} ({}) | {} {}" ,
12911348 route. route. source_label( ) ,
12921349 route. route. pico. uid_hex( ) ,
12931350 source_state,
1351+ source_switch_note,
12941352 sent_delta,
12951353 route. sent_total,
12961354 route. inbound_total,
0 commit comments