Add N64 two handle mode

This commit is contained in:
Marc Riera 2025-05-11 17:31:37 +02:00
parent 32ed9a0226
commit 274ceab091
3 changed files with 393 additions and 0 deletions

View file

@ -37,6 +37,7 @@ Connect the Plug & Play to a PC or console using the data cable. Press one of th
| One handle controller (Nintendo Switch) | UP | SELECT+START=HOME, SELECT+LEFT=L, SELECT+RIGHT=R | | One handle controller (Nintendo Switch) | UP | SELECT+START=HOME, SELECT+LEFT=L, SELECT+RIGHT=R |
| Two handle controller (PC) | RIGHT | D-Pad is mapped to SELECT+ABCD | | Two handle controller (PC) | RIGHT | D-Pad is mapped to SELECT+ABCD |
| Two handle controller (PS1) | DOWN + Power handle at 0 | Hold D to disable handles and enable D-Pad | | Two handle controller (PS1) | DOWN + Power handle at 0 | Hold D to disable handles and enable D-Pad |
| Two handle controller (N64) | DOWN + Power handle at 1 | |
| Two handle controller "Type 2" (PS2) | D | | | Two handle controller "Type 2" (PS2) | D | |
| Shinkansen controller (PS2) | B | Power notches are mapped to P2-P4-P7-P10-P13 | | Shinkansen controller (PS2) | B | Power notches are mapped to P2-P4-P7-P10-P13 |
| Multi Train Controller (PS2) - P4/B7 | C + Power handle at 0 | SELECT+A=A2, SELECT+D=ATS, SELECT+D-Pad=Reverser | | Multi Train Controller (PS2) - P4/B7 | C + Power handle at 0 | SELECT+A=A2, SELECT+D=ATS, SELECT+D-Pad=Reverser |
@ -49,6 +50,20 @@ Hold the buttons until the controller vibrates to confirm selection. If no butto
If you need more information regarding each controller and supported software, please check the [Densha de GO! controller documentation](https://marcriera.github.io/ddgo-controller-docs). If you need more information regarding each controller and supported software, please check the [Densha de GO! controller documentation](https://marcriera.github.io/ddgo-controller-docs).
## Usage with emulators
### Nintendo 64
Use mode *Two handle controller (N64)* In the emulator's settings, assign the controller to **port 3**. The controller should map automatically. Make sure to enable the setting **Independent C-Buttons controls**.
### PlayStation
Use mode *Two handle controller (PS1)*. In the emulator's settings, configure a regular analog/digital controller (**not a Densha de GO! controller**). The controller should map automatically. If the handles become unresponsive at some point., press **D**.
### PlayStation 2
Use mode *Generic Train Controller*. In the emulator's settings, configure a USB Densha de GO! controller and map the buttons/axes manually.
## RNDIS access (advanced users) ## RNDIS access (advanced users)
When no controller is selected, RNDIS access is enabled in the device. You can access SSH on the Plug & Play at 169.254.215.100. SFTP is not supported out of the box, but SCP is available. Keep in mind the root filesystem is mounted read-only by default. When no controller is selected, RNDIS access is enabled in the device. You can access SSH on the Plug & Play at 169.254.215.100. SFTP is not supported out of the box, but SCP is available. Keep in mind the root filesystem is mounted read-only by default.

View file

@ -15,6 +15,7 @@ mod sotp031201_p4b2b7;
mod sotp031201_p4b7; mod sotp031201_p4b7;
mod sotp031201_p5b5; mod sotp031201_p5b5;
mod sotp031201_p5b7; mod sotp031201_p5b7;
mod tcpp20003;
mod tcpp20009; mod tcpp20009;
mod tcpp20011; mod tcpp20011;
mod zkns001; mod zkns001;
@ -28,6 +29,7 @@ const ANDROID_GADGET: &str = "/sys/class/android_usb/android0";
#[derive(PartialEq, Debug, Clone, Copy)] #[derive(PartialEq, Debug, Clone, Copy)]
pub enum ControllerModel { pub enum ControllerModel {
DGOC44U, DGOC44U,
TCPP20003,
TCPP20009, TCPP20009,
TCPP20011, TCPP20011,
SOTP031201P4B7, SOTP031201P4B7,
@ -78,6 +80,14 @@ pub fn set_model(state: &ControllerState) -> Option<ControllerModel> {
&slph00051::DESCRIPTORS, &slph00051::DESCRIPTORS,
&slph00051::STRINGS, &slph00051::STRINGS,
); );
} else if state.button_down && state.power == 1 {
model_name = "TCPP-20004";
model = ControllerModel::TCPP20003;
descriptors = (
&tcpp20003::DEVICE_DESCRIPTOR,
&tcpp20003::DESCRIPTORS,
&tcpp20003::STRINGS,
);
} else if state.button_d { } else if state.button_d {
model_name = "TCPP-20009"; model_name = "TCPP-20009";
model = ControllerModel::TCPP20009; model = ControllerModel::TCPP20009;
@ -149,6 +159,9 @@ pub fn set_state(state: &mut ControllerState, model: &ControllerModel) {
ControllerModel::DGOC44U => { ControllerModel::DGOC44U => {
dgoc44u::update_gadget(state); dgoc44u::update_gadget(state);
} }
ControllerModel::TCPP20003 => {
tcpp20003::update_gadget(state);
}
ControllerModel::TCPP20009 => { ControllerModel::TCPP20009 => {
tcpp20009::update_gadget(state); tcpp20009::update_gadget(state);
} }
@ -194,6 +207,9 @@ pub fn handle_ctrl_transfer(model: ControllerModel, data: &[u8]) {
ControllerModel::SLPH00051 => { ControllerModel::SLPH00051 => {
report = Some(&slph00051::HID_REPORT_DESCRIPTOR); report = Some(&slph00051::HID_REPORT_DESCRIPTOR);
} }
ControllerModel::TCPP20003 => {
report = Some(&tcpp20003::HID_REPORT_DESCRIPTOR);
}
ControllerModel::GENERIC => { ControllerModel::GENERIC => {
report = Some(&generic::HID_REPORT_DESCRIPTOR); report = Some(&generic::HID_REPORT_DESCRIPTOR);
} }
@ -214,6 +230,9 @@ pub fn handle_ctrl_transfer(model: ControllerModel, data: &[u8]) {
ControllerModel::SLPH00051 => { ControllerModel::SLPH00051 => {
slph00051::handle_ctrl_transfer(data); slph00051::handle_ctrl_transfer(data);
} }
ControllerModel::TCPP20003 => {
tcpp20003::handle_ctrl_transfer(data);
}
ControllerModel::GENERIC => { ControllerModel::GENERIC => {
generic::handle_ctrl_transfer(data); generic::handle_ctrl_transfer(data);
} }

View file

@ -0,0 +1,359 @@
use crate::controller::emulated::{DeviceDescriptor, ENDPOINT0, ENDPOINT1};
use crate::controller::physical::ControllerState;
use bitflags::bitflags;
use std::fs::File;
use std::io::Write;
pub const DESCRIPTORS: [u8; 80] = [
0x01, 0x00, 0x00, 0x00, 0x50, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00,
0x09, 0x04, 0x00, 0x00, 0x02, 0x03, 0x00, 0x00, 0x00, 0x09, 0x21, 0x11, 0x01, 0x00, 0x01, 0x22,
0x94, 0x00, 0x07, 0x05, 0x02, 0x03, 0x40, 0x00, 0x05, 0x07, 0x05, 0x81, 0x03, 0x40, 0x00, 0x05,
0x09, 0x04, 0x00, 0x00, 0x02, 0x03, 0x00, 0x00, 0x00, 0x09, 0x21, 0x11, 0x01, 0x00, 0x01, 0x22,
0x94, 0x00, 0x07, 0x05, 0x02, 0x03, 0x40, 0x00, 0x05, 0x07, 0x05, 0x81, 0x03, 0x40, 0x00, 0x05,
];
pub const STRINGS: [u8; 16] = [
0x02, 0x00, 0x00, 0x00, 0x10, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
];
pub const DEVICE_DESCRIPTOR: DeviceDescriptor = DeviceDescriptor {
b_device_class: 0x0,
b_device_sub_class: 0x0,
id_vendor: 0x054C,
id_product: 0x0268,
bcd_device: 0x0100,
i_manufacturer: "TAITO",
i_product: "Densha de Go! Plug & Play (N64 Two Handle mode)",
i_serial_number: "TCPP-20004",
};
pub const HID_REPORT_DESCRIPTOR: [u8; 148] = [
0x05, 0x01, // Usage Page (Generic Desktop Ctrls)
0x09, 0x04, // Usage (Joystick)
0xA1, 0x01, // Collection (Physical)
0xA1, 0x02, // Collection (Application)
0x85, 0x01, // Report ID (1)
0x75, 0x08, // Report Size (8)
0x95, 0x01, // Report Count (1)
0x15, 0x00, // Logical Minimum (0)
0x26, 0xFF, 0x00, // Logical Maximum (255)
0x81, 0x03, // Input (Const,Var,Abs,No Wrap,Linear,Preferred State,No Null Position)
// NOTE: reserved byte
0x75, 0x01, // Report Size (1)
0x95, 0x13, // Report Count (19)
0x15, 0x00, // Logical Minimum (0)
0x25, 0x01, // Logical Maximum (1)
0x35, 0x00, // Physical Minimum (0)
0x45, 0x01, // Physical Maximum (1)
0x05, 0x09, // Usage Page (Button)
0x19, 0x01, // Usage Minimum (0x01)
0x29, 0x13, // Usage Maximum (0x13)
0x81, 0x02, // Input (Data,Var,Abs,No Wrap,Linear,Preferred State,No Null Position)
0x75, 0x01, // Report Size (1)
0x95, 0x0D, // Report Count (13)
0x06, 0x00, 0xFF, // Usage Page (Vendor Defined 0xFF00)
0x81, 0x03, // Input (Const,Var,Abs,No Wrap,Linear,Preferred State,No Null Position)
// NOTE: 32 bit integer, where 0:18 are buttons and 19:31 are reserved
0x15, 0x00, // Logical Minimum (0)
0x26, 0xFF, 0x00, // Logical Maximum (255)
0x05, 0x01, // Usage Page (Generic Desktop Ctrls)
0x09, 0x01, // Usage (Pointer)
0xA1, 0x00, // Collection (Undefined)
0x75, 0x08, // Report Size (8)
0x95, 0x04, // Report Count (4)
0x35, 0x00, // Physical Minimum (0)
0x46, 0xFF, 0x00, // Physical Maximum (255)
0x09, 0x30, // Usage (X)
0x09, 0x31, // Usage (Y)
0x09, 0x32, // Usage (Z)
0x09, 0x35, // Usage (Rz)
0x81, 0x02, // Input (Data,Var,Abs,No Wrap,Linear,Preferred State,No Null Position)
// NOTE: four joysticks
0xC0, // End Collection
0x05, 0x01, // Usage Page (Generic Desktop Ctrls)
0x75, 0x08, // Report Size (8)
0x95, 0x27, // Report Count (39)
0x09, 0x01, // Usage (Pointer)
0x81, 0x02, // Input (Data,Var,Abs,No Wrap,Linear,Preferred State,No Null Position)
0x75, 0x08, // Report Size (8)
0x95, 0x30, // Report Count (48)
0x09, 0x01, // Usage (Pointer)
0x91,
0x02, // Output (Data,Var,Abs,No Wrap,Linear,Preferred State,No Null Position,Non-volatile)
0x75, 0x08, // Report Size (8)
0x95, 0x30, // Report Count (48)
0x09, 0x01, // Usage (Pointer)
0xB1,
0x02, // Feature (Data,Var,Abs,No Wrap,Linear,Preferred State,No Null Position,Non-volatile)
0xC0, // End Collection
0xA1, 0x02, // Collection (Application)
0x85, 0x02, // Report ID (2)
0x75, 0x08, // Report Size (8)
0x95, 0x30, // Report Count (48)
0x09, 0x01, // Usage (Pointer)
0xB1,
0x02, // Feature (Data,Var,Abs,No Wrap,Linear,Preferred State,No Null Position,Non-volatile)
0xC0, // End Collection
0xA1, 0x02, // Collection (Application)
0x85, 0xEE, // Report ID (238)
0x75, 0x08, // Report Size (8)
0x95, 0x30, // Report Count (48)
0x09, 0x01, // Usage (Pointer)
0xB1,
0x02, // Feature (Data,Var,Abs,No Wrap,Linear,Preferred State,No Null Position,Non-volatile)
0xC0, // End Collection
0xA1, 0x02, // Collection (Application)
0x85, 0xEF, // Report ID (239)
0x75, 0x08, // Report Size (8)
0x95, 0x30, // Report Count (48)
0x09, 0x01, // Usage (Pointer)
0xB1,
0x02, // Feature (Data,Var,Abs,No Wrap,Linear,Preferred State,No Null Position,Non-volatile)
0xC0, // End Collection
0xC0, // End Collection
];
const F2_REPORT: [u8; 64] = [
0xF2, 0xFF, 0xFF, 0x0, 0x0, 0x6, 0xF5, 0x48, 0xE2, 0x49, 0x0, 0x3, 0x50, 0x81, 0xD8, 0x1, 0x8A,
0x13, 0x0, 0x0, 0x0, 0x0, 0x4, 0x0, 0x2, 0x2, 0x2, 0x2, 0x0, 0x0, 0x0, 0x4, 0x4, 0x4, 0x4, 0x0,
0x0, 0x4, 0x0, 0x1, 0x2, 0x7, 0x0, 0x17, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
];
const F5_REPORT: [u8; 64] = [
0x1, 0x0, 0x0, 0x23, 0x6, 0x7C, 0xB9, 0xB, 0xE2, 0x49, 0x0, 0x3, 0x50, 0x81, 0xD8, 0x1, 0x8A,
0x13, 0x0, 0x0, 0x0, 0x0, 0x4, 0x0, 0x2, 0x2, 0x2, 0x2, 0x0, 0x0, 0x0, 0x4, 0x4, 0x4, 0x4, 0x0,
0x0, 0x4, 0x0, 0x1, 0x2, 0x7, 0x0, 0x17, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
];
bitflags! {
struct Buttons1: u8 {
const NONE = 0;
const SELECT = 1;
const L3 = 2;
const R3 = 4;
const START = 8;
const UP = 16;
const RIGHT = 32;
const DOWN = 64;
const LEFT = 128;
}
struct Buttons2: u8 {
const NONE = 0;
const L2 = 1;
const R2 = 2;
const L1 = 4;
const R1 = 8;
const TRIANGLE = 16;
const CIRCLE = 32;
const CROSS = 64;
const SQUARE = 128;
}
}
pub fn update_gadget(state: &mut ControllerState) {
let mut buttons1 = Buttons1::NONE;
let mut buttons2 = Buttons2::NONE;
// Calculate data for handles
match state.power {
0 => {
buttons1.insert(Buttons1::UP);
buttons2.insert(Buttons2::L2);
}
1 => {
buttons1.insert(Buttons1::RIGHT);
buttons2.insert(Buttons2::L2);
}
2 => {
buttons2.insert(Buttons2::L2);
}
3 => {
buttons1.insert(Buttons1::UP | Buttons1::RIGHT);
}
4 => {
buttons1.insert(Buttons1::UP);
}
_ => {
buttons1.insert(Buttons1::RIGHT);
}
}
match state.brake {
0 => {
buttons2.insert(Buttons2::L1 | Buttons2::CIRCLE | Buttons2::TRIANGLE);
}
1 => {
buttons2.insert(Buttons2::R1 | Buttons2::CIRCLE | Buttons2::TRIANGLE);
}
2 => {
buttons2.insert(Buttons2::CIRCLE | Buttons2::TRIANGLE);
}
3 => {
buttons2.insert(Buttons2::L1 | Buttons2::R1 | Buttons2::TRIANGLE);
}
4 => {
buttons2.insert(Buttons2::L1 | Buttons2::TRIANGLE);
}
5 => {
buttons2.insert(Buttons2::R1 | Buttons2::TRIANGLE);
}
6 => {
buttons2.insert(Buttons2::TRIANGLE);
}
7 => {
buttons2.insert(Buttons2::L1 | Buttons2::R1 | Buttons2::CIRCLE);
}
8 => {
buttons2.insert(Buttons2::L1 | Buttons2::CIRCLE);
}
_ => (),
}
// Calculate data for buttons
if state.button_a {
buttons2.insert(Buttons2::SQUARE)
}
if state.button_b {
buttons2.insert(Buttons2::CROSS)
}
if state.button_c {
buttons1.insert(Buttons1::SELECT)
}
if state.button_start {
buttons1.insert(Buttons1::START)
}
if state.button_select {
buttons2.insert(Buttons2::R2)
}
let btn_up = if buttons1.contains(Buttons1::UP) {
0xFF
} else {
0x0
};
let btn_right = if buttons1.contains(Buttons1::RIGHT) {
0xFF
} else {
0x0
};
let btn_down = if buttons1.contains(Buttons1::DOWN) {
0xFF
} else {
0x0
};
let btn_left = if buttons1.contains(Buttons1::LEFT) {
0xFF
} else {
0x0
};
let btn_l2 = if buttons2.contains(Buttons2::L2) {
0xFF
} else {
0x0
};
let btn_r2 = if buttons2.contains(Buttons2::R2) {
0xFF
} else {
0x0
};
let btn_l1 = if buttons2.contains(Buttons2::L1) {
0xFF
} else {
0x0
};
let btn_r1 = if buttons2.contains(Buttons2::R1) {
0xFF
} else {
0x0
};
let btn_triangle = if buttons2.contains(Buttons2::TRIANGLE) {
0xFF
} else {
0x0
};
let btn_circle = if buttons2.contains(Buttons2::CIRCLE) {
0xFF
} else {
0x0
};
let btn_cross = if buttons2.contains(Buttons2::CROSS) {
0xFF
} else {
0x0
};
let btn_square = if buttons2.contains(Buttons2::SQUARE) {
0xFF
} else {
0x0
};
// Assemble data and send it to gadget
let data = [
0x1,
0x0,
buttons1.bits,
buttons2.bits,
0x0,
0x0,
0x80,
0x80,
0x80,
0x80,
0x0,
0x0,
0x0,
0x0,
btn_up,
btn_right,
btn_down,
btn_left,
btn_l2,
btn_r2,
btn_l1,
btn_r1,
btn_triangle,
btn_circle,
btn_cross,
btn_square,
0x0,
0x0,
0x0,
0x3,
0xEF,
0x14,
0x0,
0x0,
0x0,
0x0,
0x23,
0x1A,
0x77,
0x1,
0x81,
0x1,
0xFE,
0x1,
0xFE,
0x1,
0xFE,
0x1,
0xFE,
];
if let Ok(mut file) = File::create(ENDPOINT1) {
file.write(&data).ok();
}
}
pub fn handle_ctrl_transfer(data: &[u8]) {
if data[1] == 1 && data[2] == 0xF2 {
// Init 1
if let Ok(mut file) = File::create(ENDPOINT0) {
file.write(&F2_REPORT).unwrap();
}
} else if data[1] == 1 && data[2] == 0xF5 {
// Init 2
if let Ok(mut file) = File::create(ENDPOINT0) {
file.write(&F5_REPORT).unwrap();
}
}
}