Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ Beyond device emulation, VIIPER can proxy real USB devices for traffic inspectio
- HID Keyboard with N-key rollover and LED feedback; see [Devices › Keyboard](docs/devices/keyboard.md)
- HID Mouse with 5 buttons and horizontal/vertical wheel; see [Devices › Mouse](docs/devices/mouse.md)
- PS4 controller emulation; see [Devices › DualShock 4 Controller](docs/devices/dualshock4.md)
- Nintendo Switch 2 Pro Controller emulation; see [Devices › Switch 2 Pro Controller](docs/devices/ns2pro.md)
- 🔜 Future plugin system allows for more device types (other gamepads, specialized HID)

## 🔌 Requirements
Expand Down
131 changes: 131 additions & 0 deletions device/ns2pro/commands.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
package ns2pro

import "encoding/binary"

func (d *NS2Pro) handleBulkOut(out []byte) {
if len(out) < 8 {
return
}
cmd := out[0]
seq := out[2]
sub := out[3]

switch cmd {
case 0x02:
d.handleFlashCommand(seq, sub, out)
case 0x03:
d.handleUSBCommand(seq, sub, out)
case 0x0C:
d.handleFeatureCommand(seq, sub, out)
case 0x09:
d.handlePlayerLEDCommand(seq, sub, out)
default:
d.enqueueResponse(commandHeader(cmd, seq, sub))
}
}

func (d *NS2Pro) handlePlayerLEDCommand(seq, sub uint8, out []byte) {
if sub == 0x07 && len(out) >= 9 {
d.emitOutput(OutputState{
Flags: OutputFlagLED,
PlayerLedMask: out[8],
})
}
d.enqueueResponse(commandHeader(0x09, seq, sub))
}

func (d *NS2Pro) handleFlashCommand(seq, sub uint8, out []byte) {
if sub != 0x01 || len(out) < 16 {
d.enqueueResponse(commandHeader(0x02, seq, sub))
return
}

address := binary.LittleEndian.Uint32(out[12:16])
resp := make([]byte, 0x50)
copy(resp[0:8], commandHeader(0x02, seq, sub))
resp[8] = 0x40
binary.LittleEndian.PutUint32(resp[12:16], address)
copy(resp[16:], minimalFlashBlock(address))
d.enqueueResponse(resp)
}

func (d *NS2Pro) handleUSBCommand(seq, sub uint8, out []byte) {
switch sub {
case 0x03:
if len(out) >= 9 {
d.usbReportsEnabled = out[8] != 0
}
d.enqueueResponse(append(commandHeader(0x03, seq, sub), 0x01, 0x00, 0x00, 0x00))
case 0x0A:
if len(out) >= 9 {
switch out[8] {
case ReportIDCommon, ReportIDPro:
d.activeReportID = out[8]
}
}
d.enqueueResponse(commandHeader(0x03, seq, sub))
case 0x0D:
d.usbReportsEnabled = true
d.enqueueResponse(append(commandHeader(0x03, seq, sub), 0x01, 0x00, 0x00, 0x00))
default:
d.enqueueResponse(commandHeader(0x03, seq, sub))
}
}

func (d *NS2Pro) handleFeatureCommand(seq, sub uint8, out []byte) {
flags := uint8(0)
if len(out) >= 9 {
flags = out[8]
}

switch sub {
case 0x01:
payload := make([]byte, 12)
copy(payload[4:], featureInfo(flags))
d.enqueueResponse(append(commandHeader(0x0C, seq, sub), payload...))
case 0x02:
d.featureMask = flags
d.enqueueResponse(append(commandHeader(0x0C, seq, sub), 0x00, 0x00, 0x00, 0x00))
case 0x03:
d.featureMask = 0
d.featureFlags = 0
d.enqueueResponse(append(commandHeader(0x0C, seq, sub), 0x00, 0x00, 0x00, 0x00))
case 0x04:
d.featureFlags |= d.maskedFeatures(flags)
d.enqueueResponse(append(commandHeader(0x0C, seq, sub), 0x00, 0x00, 0x00, 0x00))
case 0x05:
d.featureFlags &^= d.maskedFeatures(flags)
d.enqueueResponse(append(commandHeader(0x0C, seq, sub), 0x00, 0x00, 0x00, 0x00))
default:
d.enqueueResponse(append(commandHeader(0x0C, seq, sub), 0x00, 0x00, 0x00, 0x00))
}
}

func (d *NS2Pro) maskedFeatures(flags uint8) uint8 {
if d.featureMask == 0 {
return flags
}
return flags & d.featureMask
}

func featureInfo(flags uint8) []byte {
out := make([]byte, 8)
for _, entry := range featureInfoMap {
if flags&entry.feature != 0 {
out[entry.index] = entry.value
}
}
return out
}

var featureInfoMap = []struct {
feature uint8
index int
value byte
}{
{FeatureButtons, 0, 0x07},
{FeatureSticks, 1, 0x07},
{FeatureIMU, 2, 0x01},
{FeatureMouse, 4, 0x03},
{FeatureRumble, 5, 0x03},
}
75 changes: 75 additions & 0 deletions device/ns2pro/const.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
package ns2pro

const (
DefaultVID = 0x057E
DefaultPID = 0x2069
DefaultSerialEnding = "00"
DefaultSerial = "VIIPER-NS2PRO-" + DefaultSerialEnding
DefaultBatteryVolts uint16 = 3800
)

const (
EndpointHIDIn = 0x81
EndpointHIDOut = 0x01
EndpointBulkOut = 0x02
EndpointBulkIn = 0x82
)

const (
ReportIDCommon = 0x05
ReportIDPro = 0x09
ReportIDOutput = 0x02
)

const (
InputReportSize = 64
OutputReportSize = 64
InputWireSize = 24
OutputRumbleSize = 32
OutputWireSize = 34
)

const (
OutputFlagRumble = 0x01
OutputFlagLED = 0x02
)

const (
StickMin uint16 = 0
StickCenter uint16 = 0x0800
StickMax uint16 = 0x0FFF
BatteryMax uint8 = 9
)

const (
FeatureButtons = 0x01
FeatureSticks = 0x02
FeatureIMU = 0x04
FeatureMouse = 0x10
FeatureRumble = 0x20
)

const (
ButtonB uint32 = 1 << iota
ButtonA
ButtonY
ButtonX
ButtonR
ButtonZR
ButtonPlus
ButtonRightStick
ButtonDown
ButtonRight
ButtonLeft
ButtonUp
ButtonL
ButtonZL
ButtonMinus
ButtonLeftStick
ButtonHome
ButtonCapture
ButtonGR
ButtonGL
ButtonC
ButtonHeadset
)
149 changes: 149 additions & 0 deletions device/ns2pro/descriptor.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
package ns2pro

import (
"github.com/Alia5/VIIPER/usb"
"github.com/Alia5/VIIPER/usb/hid"
)

const microsoftOS10VendorCode = 0x20

const microsoftOS10DeviceInterfaceGUID = "{7D8F1E5C-9D89-4E0D-A2F0-6D10F1F89A8F}"

func MakeDescriptor() usb.Descriptor {
return usb.Descriptor{
Device: usb.DeviceDescriptor{
BcdUSB: 0x0200,
BDeviceClass: 0xEF,
BDeviceSubClass: 0x02,
BDeviceProtocol: 0x01,
BMaxPacketSize0: 0x40,
IDVendor: DefaultVID,
IDProduct: DefaultPID,
BcdDevice: 0x0200,
IManufacturer: 0x01,
IProduct: 0x02,
ISerialNumber: 0x03,
BNumConfigurations: 0x01,
Speed: 2,
},
Configuration: usb.ConfigurationDescriptor{
BConfigurationValue: 0x01,
IConfiguration: 0x04,
BMAttributes: 0xC0,
BMaxPower: 0xFA,
},
MicrosoftOS10: &usb.MicrosoftOS10Descriptor{
VendorCode: microsoftOS10VendorCode,
InterfaceNumber: 0x01,
CompatibleID: "WINUSB",
DeviceInterfaceGUID: microsoftOS10DeviceInterfaceGUID,
},
Interfaces: []usb.InterfaceConfig{
{
Descriptor: usb.InterfaceDescriptor{
BInterfaceNumber: 0x00,
BAlternateSetting: 0x00,
BNumEndpoints: 0x02,
BInterfaceClass: 0x03,
BInterfaceSubClass: 0x00,
BInterfaceProtocol: 0x00,
IInterface: 0x05,
},
HID: &usb.HIDFunction{
Descriptor: usb.HIDDescriptor{
BcdHID: 0x0111,
BCountryCode: 0x00,
Descriptors: []usb.HIDSubDescriptor{
{Type: usb.ReportDescType},
},
},
ReportDescriptor: reportDescriptor,
},
Endpoints: []usb.EndpointDescriptor{
{BEndpointAddress: EndpointHIDIn, BMAttributes: 0x03, WMaxPacketSize: 64, BInterval: 4},
{BEndpointAddress: EndpointHIDOut, BMAttributes: 0x03, WMaxPacketSize: 64, BInterval: 4},
},
},
{
Descriptor: usb.InterfaceDescriptor{
BInterfaceNumber: 0x01,
BAlternateSetting: 0x00,
BNumEndpoints: 0x02,
BInterfaceClass: 0xFF,
BInterfaceSubClass: 0x00,
BInterfaceProtocol: 0x00,
IInterface: 0x06,
},
Endpoints: []usb.EndpointDescriptor{
{BEndpointAddress: EndpointBulkOut, BMAttributes: 0x02, WMaxPacketSize: 64, BInterval: 0},
{BEndpointAddress: EndpointBulkIn, BMAttributes: 0x02, WMaxPacketSize: 64, BInterval: 0},
},
},
},
Strings: map[uint8]string{
0: "\u0409",
1: "Nintendo",
2: "Switch 2 Pro Controller",
3: DefaultSerialEnding,
4: "Nintendo Switch 2 Pro Controller",
5: "Nintendo Switch 2 Pro Controller",
6: "Pro Controller",
},
}
}

var reportDescriptor = hid.ReportDescriptor{
Items: []hid.Item{
hidShort(hid.ItemTypeGlobal, 0x0, 0x01),
hidShort(hid.ItemTypeLocal, 0x0, 0x05),
hidShort(hid.ItemTypeMain, 0xA, 0x01),
hidShort(hid.ItemTypeGlobal, 0x8, ReportIDCommon),
hidShort(hid.ItemTypeGlobal, 0x0, 0xFF),
hidShort(hid.ItemTypeLocal, 0x0, 0x01),
hidShort(hid.ItemTypeGlobal, 0x1, 0x00),
hidShort(hid.ItemTypeGlobal, 0x2, 0xFF, 0x00),
hidShort(hid.ItemTypeGlobal, 0x9, 0x3F),
hidShort(hid.ItemTypeGlobal, 0x7, 0x08),
hidShort(hid.ItemTypeMain, 0x8, 0x02),
hidShort(hid.ItemTypeGlobal, 0x8, ReportIDPro),
hidShort(hid.ItemTypeLocal, 0x0, 0x01),
hidShort(hid.ItemTypeGlobal, 0x9, 0x02),
hidShort(hid.ItemTypeMain, 0x8, 0x02),
hidShort(hid.ItemTypeGlobal, 0x0, 0x09),
hidShort(hid.ItemTypeLocal, 0x1, 0x01),
hidShort(hid.ItemTypeLocal, 0x2, 0x15),
hidShort(hid.ItemTypeGlobal, 0x2, 0x01),
hidShort(hid.ItemTypeGlobal, 0x9, 0x15),
hidShort(hid.ItemTypeGlobal, 0x7, 0x01),
hidShort(hid.ItemTypeMain, 0x8, 0x02),
hidShort(hid.ItemTypeGlobal, 0x9, 0x01),
hidShort(hid.ItemTypeGlobal, 0x7, 0x03),
hidShort(hid.ItemTypeMain, 0x8, 0x03),
hidShort(hid.ItemTypeGlobal, 0x0, 0x01),
hidShort(hid.ItemTypeLocal, 0x0, 0x01),
hidShort(hid.ItemTypeMain, 0xA, 0x00),
hidShort(hid.ItemTypeLocal, 0x0, 0x30),
hidShort(hid.ItemTypeLocal, 0x0, 0x31),
hidShort(hid.ItemTypeLocal, 0x0, 0x33),
hidShort(hid.ItemTypeLocal, 0x0, 0x35),
hidShort(hid.ItemTypeGlobal, 0x2, 0xFF, 0x0F),
hidShort(hid.ItemTypeGlobal, 0x9, 0x04),
hidShort(hid.ItemTypeGlobal, 0x7, 0x0C),
hidShort(hid.ItemTypeMain, 0x8, 0x02),
hidShort(hid.ItemTypeMain, 0xC),
hidShort(hid.ItemTypeGlobal, 0x0, 0xFF),
hidShort(hid.ItemTypeLocal, 0x0, 0x02),
hidShort(hid.ItemTypeGlobal, 0x2, 0xFF, 0x00),
hidShort(hid.ItemTypeGlobal, 0x9, 0x34),
hidShort(hid.ItemTypeGlobal, 0x7, 0x08),
hidShort(hid.ItemTypeMain, 0x8, 0x02),
hidShort(hid.ItemTypeGlobal, 0x8, ReportIDOutput),
hidShort(hid.ItemTypeLocal, 0x0, 0x01),
hidShort(hid.ItemTypeGlobal, 0x9, 0x3F),
hidShort(hid.ItemTypeMain, 0x9, 0x02),
hidShort(hid.ItemTypeMain, 0xC),
}}

func hidShort(itemType hid.ItemType, tag uint8, data ...uint8) hid.AnyItem {
return hid.AnyItem{Type: itemType, Tag: tag, Data: hid.Data(data)}
}
Loading
Loading