Skip to content

Commit 0e9f379

Browse files
committed
feat(firmware): add bluetooth hid personas (2026.6.20.4-1886)
1 parent 4a6a820 commit 0e9f379

23 files changed

Lines changed: 1111 additions & 65 deletions

bridge/src/cmd_home.rs

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -860,6 +860,10 @@ async fn choose_input_mode() -> Result<InputModeChoice> {
860860
"DInput / PlayStation",
861861
"choose PS3, generic HID, or PS4 mode",
862862
),
863+
menu_item(
864+
"Bluetooth",
865+
"choose Bluetooth HID target layout for wireless receivers",
866+
),
863867
menu_item(
864868
"Maple",
865869
"Xbox-compatible mode labelled for Dreamcast adapters",
@@ -871,8 +875,9 @@ async fn choose_input_mode() -> Result<InputModeChoice> {
871875
0 => Ok(InputModeChoice::Auto),
872876
1 => choose_xbox_input_mode().await,
873877
2 => choose_playstation_input_mode().await,
874-
3 => Ok(InputModeChoice::Persona(protocol::Persona::Maple)),
875-
4 => Ok(InputModeChoice::Persona(protocol::Persona::Keyboard)),
878+
3 => choose_bluetooth_input_mode().await,
879+
4 => Ok(InputModeChoice::Persona(protocol::Persona::Maple)),
880+
5 => Ok(InputModeChoice::Persona(protocol::Persona::Keyboard)),
876881
_ => Ok(InputModeChoice::Persona(protocol::Persona::Debug)),
877882
}
878883
}
@@ -905,6 +910,24 @@ async fn choose_playstation_input_mode() -> Result<InputModeChoice> {
905910
}
906911
}
907912

913+
async fn choose_bluetooth_input_mode() -> Result<InputModeChoice> {
914+
let choices = vec![
915+
menu_item("Bluetooth HID", "generic Bluetooth HID gamepad"),
916+
menu_item("Bluetooth Xbox", "Bluetooth HID with Xbox button ordering"),
917+
menu_item(
918+
"Bluetooth PlayStation",
919+
"Bluetooth HID with PlayStation button ordering",
920+
),
921+
];
922+
match select("Bluetooth input mode", &choices, 0).await? {
923+
0 => Ok(InputModeChoice::Persona(protocol::Persona::BluetoothHid)),
924+
1 => Ok(InputModeChoice::Persona(protocol::Persona::BluetoothXbox)),
925+
_ => Ok(InputModeChoice::Persona(
926+
protocol::Persona::BluetoothPlaystation,
927+
)),
928+
}
929+
}
930+
908931
async fn prepare_routes_for_input_mode(
909932
routes: Vec<cmd_run::StreamRoute>,
910933
mode: InputModeChoice,

bridge/src/cmd_run.rs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,9 @@ impl StreamRoute {
126126
| Persona::Ps4
127127
| Persona::XboxOne
128128
| Persona::GenericHid
129+
| Persona::BluetoothHid
130+
| Persona::BluetoothXbox
131+
| Persona::BluetoothPlaystation
129132
| Persona::Debug => xinput::user_slot_label(self.source_slot),
130133
}
131134
}
@@ -1153,6 +1156,9 @@ impl RouteRuntime {
11531156
| Persona::Ps4
11541157
| Persona::XboxOne
11551158
| Persona::GenericHid
1159+
| Persona::BluetoothHid
1160+
| Persona::BluetoothXbox
1161+
| Persona::BluetoothPlaystation
11561162
| Persona::Debug => self.next_controller_packet(),
11571163
Persona::Keyboard => self.next_keyboard_packet(),
11581164
};
@@ -1272,6 +1278,9 @@ fn print_status(routes: &mut [RouteRuntime]) {
12721278
| Persona::Ps4
12731279
| Persona::XboxOne
12741280
| Persona::GenericHid
1281+
| Persona::BluetoothHid
1282+
| Persona::BluetoothXbox
1283+
| Persona::BluetoothPlaystation
12751284
| Persona::Debug => format!(
12761285
"buttons=0x{:04X} lt={} rt={} lx={} ly={} rx={} ry={}",
12771286
route.last_state.buttons,

bridge/src/cmd_usb_diag.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -189,6 +189,11 @@ pub fn format_usb_diag(diag: &protocol::UsbDiag, persona: protocol::Persona) ->
189189
protocol::Persona::XboxOne => "Xbox One XGIP",
190190
protocol::Persona::Debug => "Debug XInput packet capture",
191191
protocol::Persona::GenericHid => "Generic HID gamepad",
192+
protocol::Persona::BluetoothHid => "Bluetooth HID gamepad (no USB output)",
193+
protocol::Persona::BluetoothXbox => "Bluetooth HID Xbox order (no USB output)",
194+
protocol::Persona::BluetoothPlaystation => {
195+
"Bluetooth HID PlayStation order (no USB output)"
196+
}
192197
};
193198
let mut out = String::new();
194199
let _ = writeln!(out, " {}", usb_verdict(diag, device_label));

bridge/src/main.rs

Lines changed: 114 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -276,6 +276,62 @@ enum Command {
276276
#[arg(long)]
277277
no_stream: bool,
278278
},
279+
/// Switch a Pico to Bluetooth HID gamepad mode.
280+
#[command(
281+
name = "bluetooth-hid",
282+
alias = "bluetooth",
283+
alias = "bt-hid",
284+
alias = "blueretro",
285+
alias = "n64-bluetooth"
286+
)]
287+
BluetoothHid {
288+
/// Select a Pico by UID, IP, or board name. Repeat to select more than one.
289+
#[arg(long = "pico")]
290+
picos: Vec<String>,
291+
292+
/// Switch every Pico currently visible on Wi-Fi.
293+
#[arg(long)]
294+
all: bool,
295+
296+
/// Switch the persona but don't start streaming afterwards.
297+
#[arg(long)]
298+
no_stream: bool,
299+
},
300+
/// Switch a Pico to Bluetooth HID with Xbox button ordering.
301+
#[command(name = "bluetooth-xbox", alias = "bt-xbox", alias = "blueretro-xbox")]
302+
BluetoothXbox {
303+
/// Select a Pico by UID, IP, or board name. Repeat to select more than one.
304+
#[arg(long = "pico")]
305+
picos: Vec<String>,
306+
307+
/// Switch every Pico currently visible on Wi-Fi.
308+
#[arg(long)]
309+
all: bool,
310+
311+
/// Switch the persona but don't start streaming afterwards.
312+
#[arg(long)]
313+
no_stream: bool,
314+
},
315+
/// Switch a Pico to Bluetooth HID with PlayStation button ordering.
316+
#[command(
317+
name = "bluetooth-playstation",
318+
alias = "bt-playstation",
319+
alias = "bt-ps",
320+
alias = "blueretro-playstation"
321+
)]
322+
BluetoothPlaystation {
323+
/// Select a Pico by UID, IP, or board name. Repeat to select more than one.
324+
#[arg(long = "pico")]
325+
picos: Vec<String>,
326+
327+
/// Switch every Pico currently visible on Wi-Fi.
328+
#[arg(long)]
329+
all: bool,
330+
331+
/// Switch the persona but don't start streaming afterwards.
332+
#[arg(long)]
333+
no_stream: bool,
334+
},
279335
/// First-time setup wizard: flash the Pico, provision Wi-Fi, and check LAN discovery.
280336
Setup {
281337
/// Path to the .uf2 firmware to flash.
@@ -559,6 +615,29 @@ fn main() {
559615
all,
560616
no_stream,
561617
}) => cmd_persona::run(protocol::Persona::GenericHid, picos, all, !no_stream).await,
618+
Some(Command::BluetoothHid {
619+
picos,
620+
all,
621+
no_stream,
622+
}) => cmd_persona::run(protocol::Persona::BluetoothHid, picos, all, !no_stream).await,
623+
Some(Command::BluetoothXbox {
624+
picos,
625+
all,
626+
no_stream,
627+
}) => cmd_persona::run(protocol::Persona::BluetoothXbox, picos, all, !no_stream).await,
628+
Some(Command::BluetoothPlaystation {
629+
picos,
630+
all,
631+
no_stream,
632+
}) => {
633+
cmd_persona::run(
634+
protocol::Persona::BluetoothPlaystation,
635+
picos,
636+
all,
637+
!no_stream,
638+
)
639+
.await
640+
}
562641
Some(Command::Setup { uf2 }) => cmd_setup::run(uf2).await,
563642
Some(Command::Doctor) => cmd_doctor::run().await,
564643
Some(Command::Flash { uf2, all, from_usb }) => cmd_flash::run(uf2, all, from_usb).await,
@@ -944,7 +1023,17 @@ mod tests {
9441023

9451024
#[test]
9461025
fn specific_gamepad_persona_commands_parse_target_and_no_stream() {
947-
for command in ["xbox", "xbox360", "xboxone", "ps3", "ps4", "generic-hid"] {
1026+
for command in [
1027+
"xbox",
1028+
"xbox360",
1029+
"xboxone",
1030+
"ps3",
1031+
"ps4",
1032+
"generic-hid",
1033+
"bluetooth-hid",
1034+
"bluetooth-xbox",
1035+
"bluetooth-playstation",
1036+
] {
9481037
let cli =
9491038
Cli::try_parse_from(["couchlink", command, "--pico", "07D37EB6", "--no-stream"])
9501039
.unwrap();
@@ -996,6 +1085,30 @@ mod tests {
9961085
all,
9971086
no_stream,
9981087
}),
1088+
)
1089+
| (
1090+
"bluetooth-hid",
1091+
Some(Command::BluetoothHid {
1092+
picos,
1093+
all,
1094+
no_stream,
1095+
}),
1096+
)
1097+
| (
1098+
"bluetooth-xbox",
1099+
Some(Command::BluetoothXbox {
1100+
picos,
1101+
all,
1102+
no_stream,
1103+
}),
1104+
)
1105+
| (
1106+
"bluetooth-playstation",
1107+
Some(Command::BluetoothPlaystation {
1108+
picos,
1109+
all,
1110+
no_stream,
1111+
}),
9991112
) => {
10001113
assert_eq!(picos, vec!["07D37EB6"]);
10011114
assert!(!all);

0 commit comments

Comments
 (0)