You can use this .NET library to communicate with Nintendo Switch Joy-Con or Pro controllers. It's based on the HidSharp library, which is a cross-platform .NET library for USB HID devices, and it's compatible with Windows, Linux and macOS. So, this library should work on all these platforms too.
- Read input data: buttons, sticks, gyroscope and accelerometer.
- Read factory and user calibration data and use it to get calibrated values.
- Control HD Rumble (frequency and amplitude).
- Set input report mode.
- Set player LEDs.
- Set Home LED pattern.
- Set IMU sensor sensitivity, filter and perfomance parameters.
- Get battery level.
- Get base device info (type, firmware version, etc).
- Get the controller's colors.
- Read/write SPI flash memory.
- Read/write IMU sensor registers directly.
And what you can't do (yet?):
- MCU stuff: NFC, IR camera, Ringfit, etc.
It's pretty simple. First, you need to connect your controller via Bluetooth to the computer. Then you need get the controller's HidDevice object using the HidSharp library, create a JoyCon
object, subscribe to the events and call the Start()
method. Here's an example:
using HidSharp;
using System.Text;
using wtf.cluster.JoyCon;
using wtf.cluster.JoyCon.Calibration;
using wtf.cluster.JoyCon.ExtraData;
using wtf.cluster.JoyCon.HomeLed;
using wtf.cluster.JoyCon.InputReports;
using wtf.cluster.JoyCon.Rumble;
Console.OutputEncoding = Encoding.UTF8;
HidDevice? device = null;
// Get a list of all HID devices
DeviceList list = DeviceList.Local;
if (OperatingSystem.IsWindows())
{
// Get all devices developed by Nintendo by vendor ID
var nintendos = list.GetHidDevices(0x057e);
// Get the first Nintendo controller
device = nintendos.FirstOrDefault();
// It works fine for Windows, but...
}
else
{
// Linux has more complicated HID device management, we can't get device's vendor ID,
// so let's filter devices by their report descriptor
// It should work in Windows too, so it's more multiplatform solution
var hidDevices = list.GetHidDevices();
foreach (var d in hidDevices)
{
var rd = d.GetReportDescriptor();
if (rd != null)
{
if (
(rd.OutputReports.Count() == 4)
&& (rd.OutputReports.Where(r => r.ReportID == 0x01).Count() == 1)
&& (rd.OutputReports.Where(r => r.ReportID == 0x10).Count() == 1)
&& (rd.OutputReports.Where(r => r.ReportID == 0x11).Count() == 1)
&& (rd.OutputReports.Where(r => r.ReportID == 0x12).Count() == 1)
&& (rd.InputReports.Count() == 6)
&& (rd.InputReports.Where(r => r.ReportID == 0x21).Count() == 1)
&& (rd.InputReports.Where(r => r.ReportID == 0x30).Count() == 1)
&& (rd.InputReports.Where(r => r.ReportID == 0x31).Count() == 1)
&& (rd.InputReports.Where(r => r.ReportID == 0x32).Count() == 1)
&& (rd.InputReports.Where(r => r.ReportID == 0x33).Count() == 1)
&& (rd.InputReports.Where(r => r.ReportID == 0x3F).Count() == 1)
)
{
device = d;
break;
}
}
}
}
if (device == null)
{
Console.WriteLine("No controller. Please connect Joy-Con or Pro controller via Bluetooth.");
return;
}
// Create a new JoyCon instance based on the HID device
var joycon = new JoyCon(device);
// Start controller polling
joycon.Start();
// First of all, we need to set format of the input reports,
// most of the time you will need to use InputReportMode.Full mode
await joycon.SetInputReportModeAsync(JoyCon.InputReportType.Full);
// Get some information about the controller
DeviceInfo deviceInfo = await joycon.GetDeviceInfoAsync();
Console.WriteLine($"Type: {deviceInfo.ControllerType}, Firmware: {deviceInfo.FirmwareVersionMajor}.{deviceInfo.FirmwareVersionMinor}");
var serial = await joycon.GetSerialNumberAsync();
Console.WriteLine($"Serial number: {serial ?? "<none>"}");
ControllerColors? colors = await joycon.GetColorsAsync();
if (colors != null)
{
Console.WriteLine($"Body color: {colors.BodyColor}, buttons color: {colors.ButtonsColor}");
}
else
{
Console.WriteLine("Colors not specified, seems like the controller is grey.");
}
// Enable IMU (accelerometer and gyroscope)
await joycon.EnableImuAsync(true);
// Enable rumble feature (it's enabled by default, actually)
await joycon.EnableRumbleAsync(true);
// You can control LEDs on the controller
await joycon.SetPlayerLedsAsync(JoyCon.LedState.Off, JoyCon.LedState.On, JoyCon.LedState.Off, JoyCon.LedState.Blinking);
// Get factory calibration data
CalibrationData facCal = await joycon.GetFactoryCalibrationAsync();
// Get user calibration data
CalibrationData userCal = await joycon.GetUserCalibrationAsync();
// Combine both calibrations, e.g. user calibration has higher priority
CalibrationData calibration = facCal + userCal;
// Get some parameters for the sticks
StickParametersSet sticksParameters = await joycon.GetStickParametersAsync();
// Home LED dimming pattern demo. Useless, but fun.
await joycon.SetHomeLedDimmingPatternAsync(new HomeLedDimmingPattern
{
StepDurationBase = 4, // base duration is 40ms
StartLedBrightness = 0, // 0%, off
FullCyclesNumber = 0, // infinite
HomeLedDimmingSteps =
{
new HomeLedDimmingStep
{
LedBrightness = 0x0F, // 100%
TransitionDuration = 2, // base * 2 = 40ms * 2 = 80ms
PauseDuration = 4, // base * 4 = 40ms * 4 = 160ms
},
new HomeLedDimmingStep
{
LedBrightness = 0x00, // LED off
TransitionDuration = 2, // base * 2 = 40ms * 2 = 80ms
PauseDuration = 4, // base * 4 = 40ms * 4 = 160ms
},
new HomeLedDimmingStep { LedBrightness = 0x0F, TransitionDuration = 2, PauseDuration = 4 },
new HomeLedDimmingStep { LedBrightness = 0x00, TransitionDuration = 2, PauseDuration = 4 },
new HomeLedDimmingStep { LedBrightness = 0x0F, TransitionDuration = 2, PauseDuration = 4 },
new HomeLedDimmingStep { LedBrightness = 0x00, TransitionDuration = 2, PauseDuration = 4 },
new HomeLedDimmingStep
{
LedBrightness = 0x08, // 50%
TransitionDuration = 8, // base * 8 = 40ms * 8 = 320ms
PauseDuration = 4, // base * 8 = 40ms * 8 = 320ms
},
new HomeLedDimmingStep
{
LedBrightness = 0x00, // LED off
TransitionDuration = 8, // base * 8 = 40ms * 8 = 320ms
PauseDuration = 15, // base * 15 = 40ms * 15 = 600ms
}
},
});
// Save the current cursor position
(var cX, var cY) = (Console.CursorLeft, Console.CursorTop);
// Subscribe to the input reports
joycon.ReportReceived += (sender, input) =>
{
Console.SetCursorPosition(cX, cY);
// Check the type of the input report, most likely it will be InputWithImu
if (input is InputFullWithImu j)
{
// Some base data from the controller
Console.WriteLine($"LeftStick: ({j.LeftStick}), RightStick: ({j.RightStick}), Buttons: ({j.Buttons}), Battery: {j.Battery}, Charging: {j.Charging} ");
// But we need calibrated data
StickPositionCalibrated leftStickCalibrated = j.LeftStick.GetCalibrated(calibration.LeftStickCalibration!, sticksParameters.LeftStickParameters.DeadZone);
StickPositionCalibrated rightStickCalibrated = j.RightStick.GetCalibrated(calibration.RightStickCalibration!, sticksParameters.RightStickParameters.DeadZone);
Console.WriteLine($"Calibrated LeftStick: (({leftStickCalibrated}), RightStick: ({rightStickCalibrated})) ");
// And accelerometer and gyroscope data
ImuFrameCalibrated calibratedImu = j.Imu.Frames[0].GetCalibrated(calibration.ImuCalibration!);
Console.WriteLine($"Calibrated IMU: ({calibratedImu}) ");
}
else
{
Console.WriteLine($"Invalid input report type: {input.GetType()}");
}
return Task.CompletedTask;
};
// Error handling
joycon.StoppedOnError += new JoyCon.StoppedOnErrorHandler((_, ex) =>
{
Console.WriteLine();
Console.WriteLine($"Critical error: {ex.Message}");
Console.WriteLine("Controller polling stopped.");
Environment.Exit(1);
return Task.CompletedTask;
});
// Periodically send rumble command to the controller
var rumbleTimer = new Timer(async _ =>
{
try
{
await joycon.WriteRumble(new RumbleSet(new RumbleData(40.9, 1), new RumbleData(65, 1)));
}
catch (Exception e)
{
Console.WriteLine($"Rumble error: {e.Message}");
}
}, null, 0, 1000);
Console.ReadKey();
await rumbleTimer.DisposeAsync();
joycon.Stop();
Console.WriteLine();
Console.WriteLine("Stopped.");
You should call the SetInputReportModeAsync()
method to set the input report format. Most of the time you'll need to use:
- The
InputReportType.Full
mode, in this mode you'll receiveInputFull
objects, which contains all the data: buttons, sticks, gyroscope, accelerometer, battery level, etc. You'll receive reports at a rate of 60 Hz. This is the most common mode. - The
InputReportType.Simple
mode, in this mode you'll receiveInputSimple
objects, which contains the base data only. You'll receive reports with every data change.
ReportReceived
event is fired when a new input report is received from the controller. It will be the object that implements the IJoyConReport
interface. Possible objects are:
InputSimple
, used in theInputReportType.Simple
mode, contains the base data only.InputFull
, used in theInputReportType.Full
mode, contains all the data: buttons and sticks.InputFullWithImu
- derived fromInputFull
, contains the base data and IMU data.InputFullWithSubCmdReply
- derived fromInputFull
, contains the base data and subcommand reply data. You should handle subcommand replies only if you send subcommands withnoWaitAck = true
parameter. Otherwise, the library will handle it for you.InputFullWithImuMcuFw
- derived fromInputFull
, contains the base data, IMU data and some additional MCU data. This feature in not implemented in this library.InputFullWithMcuFw
- derived fromInputFull
, contains the base data and MCU firmware update data. This feature in not implemented in this library.
Most of the time you'll need just InputReportType.Full
mode and handle only InputFullWithImu
or InputFull
objects. You should handle the ReportReceived
event as fast as possible, because the next report will not be received until the current one is processed. If you need to do some heavy calculations, you should use a separate thread or a timer.
StoppedOnError
event is fired when an critical error occurs during the controller polling than causes the polling to stop, i.e. the controller is disconnected. You should handle this event to stop the application properly or to try to reconnect the controller.
You can find full documentation here: https://clusterm.github.io/joycon/
- Both Joy-Con controllers have both sticks and HD Rumble actuators registers. So, input reports contain data for both sticks and actuators. But you can't use the right stick data for the left controller and vice versa. Missing stick's data is invalid. The same is for the HD Rumble: you can write rumble data to the both actuators but right Joy-Con will handle only the right actuator data and the left Joy-Con will handle only the left actuator data. Pro controller has all the sticks and actuators, so you can read and write data for all of them.
- You'll receive input reports with a raw uncalibrated data. You can get the calibration data using the
GetFactoryCalibrationAsync()
,GetUserCalibrationAsync()
andGetStickParametersAsync()
methods, then useIStickPosition.GetCalibrated()
andImuFrame.GetCalibrated()
methods to get calibrated data. User calibration can be set in the Switch's system settings. - Don't forget to call the
EnableImuAsync()
method if you need to use the accelerometer and gyroscope data, it's disabled by default. - You can also call the
EnableRumbleAsync()
method but rumble is enabled by default. - Rumble amlitude is limited by this library to avoid damaging the controller.
dotnet add package JoyCon.NET
https://www.nuget.org/packages/JoyCon.NET
- HidSharp library by Zer.
- Nintendo Switch Reverse Engineering by dekuNukem.
- Me (Alexey Cluster)
- Buy Me A Coffee
- Become a sponsor on GitHub
- Donation Alerts
- Boosty
- BTC: 1MBYsGczwCypXhMBocoDQWxx7KZT2iiwzJ
- PayPal is not available in Armenia :(