Repository navigation
Controller Records
This page explains how to add a controller that OpenJoystickDriver does not know, or change how it drives a known one, with your own controller record.
Note: This page applies to OpenJoystickDriver 0.6.0-alpha.1 and later. Version 0.5.0-beta.4 and earlier do not have all of the features on this page.
- How Records Work
- Choose Who Drives the Controller
- Describe a Controller's Input Reports
- Add Rumble To a Controller
- Send Fixed Reports At Startup
- Tune Timings and the Stick Deadzone
- Select a Model With Protocol Parameters
- Map HID Usages
- Control Names and Other Vocabularies
- Offer Alternative Modes Chosen By a Probe
- Connect a Switch 2 Controller Over Bluetooth LE
- Draft a Record From a Connected Controller
- Install a Record
- Check Which Records Apply
- Remove a Record
- Schema Versions and Compatibility
- Further Reading
A controller record tells OpenJoystickDriver which protocol family drives one controller model, identified by its VID:PID. The app contains a bundled record for each supported model. Your records are JSON files in ~/Library/Application Support/OpenJoystickDriver/Controllers. Each file is named after its model in lowercase hexadecimal, for example 045e-028e.json.
A file in that folder is one of two kinds:
-
Controller: a complete record for a model that has no bundled record. Name itVVVV-PPPP.json. -
ControllerPatch: new values for a model that already has a record. Name itVVVV-PPPP.patch.json. Itsselectornames the model byvendorIDandproductID, and itspatchholds the fields to change.
A patch follows RFC 7396 JSON Merge Patch. An object in patch merges into the record key by key, a value you give replaces the record's value, an array replaces the whole array, and null deletes the key. Deleting the last key of a section leaves an empty section, which a record does not accept, so set the whole section (tuning, protocol.parameters) to null instead. A patch that changes protocol.family must also set to null every protocol key of the old family that the new family does not read.
{
"apiVersion": "openjoystickdriver/v1alpha1",
"kind": "ControllerPatch",
"selector": { "vendorID": 1118, "productID": 746 },
"patch": { "tuning": { "stickDeadzone": 0.08 }, "protocol": { "parameters": { "shareButton": null } } }
}Several patches can apply to one model. The layers apply from the weakest to the strongest: the bundled record, a bundled patch, a patch from an installed mod package, then your patch. Inside one layer, a patch with "selector": { "all": true } applies first, a vendorID and productID patch next, and a patch with "selector": { "model": "VVVV-PPPP" } last. A Controller file of yours replaces a package's Controller file for the same model. A ControllerPatch of yours applies over the package record instead of replacing it. If you remove the package later, OJD lists your patch as a problem and skips it.
A record for a model that uses raw USB works only when macOS lets OpenJoystickDriver open the device directly. The Xbox USB part of the OJD driver extension claims only the Xbox models in its signed product list, and your record cannot add a model to that list. ojd record validate says when this applies.
macOS serves some controllers natively through the Game Controller framework. OpenJoystickDriver leaves those to macOS by default and only reads their input. The record's server field changes this:
-
system: macOS drives the controller when it can. This is the default. -
ojd: OpenJoystickDriver opens the controller exclusively even when macOS supports it, and drives it like a controller macOS does not know.
A raw-USB family, such as xbox.gip, has no server field, because macOS cannot serve those controllers. A new server value applies to a connected controller as soon as the running service reloads the records. You do not need to unplug the controller.
To change server without editing the file, run ojd controller claim CONTROLLER or ojd controller release CONTROLLER. claim sets ojd, and release sets system. The command writes the value to your record for the model, and creates a ControllerPatch file when you have none and the bundled catalog or an installed package has a record of the model. It refuses a raw-USB family, a controller that macOS does not serve natively, and a model that has no record at all, with the code E2023. For a model with no record, run ojd record draft to start one. ojd controller show names the choice and the layer it comes from, and the serving route and persona. In the app, the controller detail has the same choice as the Served by picker, shown for a controller that macOS serves natively or that a record gives to OpenJoystickDriver, and never for a raw-USB family.
Some controllers send fixed input reports that their HID descriptor describes wrongly. A record with the protocol family hid.report-layout names where each control sits in the report, in its input field. The family requires input, and no other family takes it in a record. The bundled xbox.xid protocol description declares its own input layout as data, in Resources/Protocols/xbox.xid.json, with the same fields.
"protocol": { "family": "hid.report-layout" },
"input": {
"report": { "length": 19 },
"buttons": [
{ "control": "South", "byte": 13, "mask": 128 },
{ "control": "Back", "byte": 1, "mask": 1 }
],
"axes": [
{ "control": "LeftStickX", "byte": 3 },
{ "control": "LeftStickY", "byte": 4 }
],
"hat": [
{
"encoding": "directions",
"up": { "byte": 9, "mask": 128 },
"right": { "byte": 7, "mask": 128 },
"down": { "byte": 10, "mask": 128 },
"left": { "byte": 8, "mask": 128 }
}
],
"leftTrigger": { "byte": 17 },
"rightTrigger": { "byte": 18 }
}Offsets count from the first byte of the report as macOS delivers it.
-
report:lengthis the shortest report OpenJoystickDriver reads, from 1 to 64 bytes. Withid, from 1 to 255, byte 0 must hold that report ID, andlengthcounts it. OpenJoystickDriver ignores shorter reports and reports with another ID. -
buttons: each entry names a control, abyte, and amask. The button is pressed while any masked bit is set. Repeat a control to read it from several places. A button cannot bedpad, a stick axis,left-trigger, orright-trigger. -
axes: each entry names a stick axis and itsbyte.bitsis 8, the default, or 16 for a little-endian value inbyteand the next byte.signedreads the value as two's complement.minandmaxset the raw range, which defaults to the full range of the width.invertedflips the direction.symmetricreads a signed value as the raw value divided bymax, so the lowest raw value reads as full negative; it requiressigned;minandmaxdefault to the full range, and if given must be negative and positive. X grows right and Y grows down, as in HID. -
hat: a list of sources; the first that reads a direction wins. An8-waysource reads the masked bits as 0 for north, clockwise to 7 for north-west, and any other value as centered. WithneutralUntilNonzero, 0 also reads centered until the bits have been nonzero once, for a controller that sends 0 before its hat is live. Adirectionssource names one bit for each direction; opposing directions cancel. Atablesource takes abyte, amask, andvalues, which maps each masked number, written in decimal without leading zeros, to a direction name fromnorth,northEast,east,southEast,south,southWest,west, andnorthWest; any other number reads as centered. -
leftTriggerandrightTrigger: abyteread from 0 to 255, abuttonthat reads as fully pulled, or both.
Name at least one control. ojd record validate checks that every field lies inside the report.
A hid.report-layout or vendor.ps3-third-party controller is input-only unless its record names its rumble report in output.rumble:
"output": {
"rumble": {
"report": { "kind": "output", "id": 2, "length": 8 },
"leftMain": { "byte": 3 },
"rightMain": { "byte": 2 }
}
}-
reportnames the HID report:kindisoutputorfeature,idis the report ID, andlengthcounts every byte, including the report ID. -
leftMain,rightMain,leftTrigger, andrightTriggereach give the byte offset of that motor's intensity, from 0 to 255. Name at least one.
OpenJoystickDriver sets the report ID in byte 0 when the ID is not 0, writes each motor's intensity, and sends 0 in every other byte. It sends this report even when macOS drives the controller, because macOS does not drive these motors.
A HID controller that needs a fixed report before it sends input can name it in output.startup. The Sixaxis records use one to enable a DualShock 3 over Bluetooth:
"output": {
"startup": [
{
"report": { "kind": "feature", "id": 244, "length": 5 },
"bytes": [66, 3, 0, 0],
"transport": "bluetooth-classic"
}
]
}-
reportnames the HID report as inoutput.rumble.lengthis 2 to 128 bytes. -
byteslists every byte after the report ID, or every byte of the report whenidis 0, from 0 to 255. -
delayMillisecondswaits before this write, from 0 to 1000. Without it, astartup-phase write waits the family's startup interval and anactivation-phase write does not wait. -
phaseisactivation, the default, orstartup. Anactivationwrite is sent after the protocol driver's own startup, when the controller opens; astartupwrite is sent with the driver's startup writes. -
crc32Seed(0 to 255) makes OpenJoystickDriver write the Sony Bluetooth CRC-32 of the report, with this seed, in its last 4 bytes.lengththen counts those 4 bytes and must exceed 4. The DualShock 4 Bluetooth report uses it. -
transportlimits the write tousb,bluetooth-classic, orbluetooth-le. Without it, OpenJoystickDriver sends the write on every transport.
List 1 to 16 writes. OpenJoystickDriver sends them in order after the protocol driver's own startup, only when it opens the controller. A failed write is logged and the next one is still sent. A family's description may also carry startup writes, which a record's writes of the same format replace. In a description a HID write may add variants (the family variants it applies to) and when ({"parameter": "layout", "is": ["Pro"]} or isNot, naming a declared enumeration parameter). A record may not use variants or when.
A raw-USB family that carries startup packets in its description (xbox.xusb, xbox.gip, nintendo.switch2) takes them in output.startup too, in place of the description's. A raw-USB write has no report: bytes (1 to 64), endpoint (1 to 127, default the family's output endpoint), timeoutMilliseconds (1 to 60000, default the family's timeout), delayMilliseconds (0 to 1000), sessionByte (the index of a byte the driver fills with its per-session value, such as the GIP sequence), and toleratesRejection. One list holds one format only, and a record that names protocol.initialization takes no raw-USB writes.
A record's tuning field changes a value that the protocol driver otherwise sets. Name at least one value. A value you leave out keeps the family's default, which its protocol description holds (see Family Defaults).
"tuning": {
"stickDeadzone": 0.02,
"hidStartupRecoveryRounds": 3
}| Field | Range | Applies to | What it sets |
|---|---|---|---|
stickDeadzone |
more than 0, less than 1 | every family | The radial deadzone of both sticks on the virtual gamepad, as a fraction of full deflection. Movement past the deadzone is rescaled to the full range. |
inputLivenessTimeoutMs |
1 to 60000 | sony.dualshock4 |
How long the controller can send no input before OpenJoystickDriver releases its held controls. The default is 1000. |
hidStartupIntervalMs |
0 to 60000 | HID families | The gap between the startup writes. |
minimumHIDOutputIntervalMs |
0 to 60000 | HID families | The shortest gap between rumble and lighting reports. 0 sends them without a limit. |
hidStartupRecoveryIntervalMs |
1 to 60000 | nintendo.switch1 |
The gap between startup recovery rounds. The family's description gives the default (200). |
hidStartupRecoveryRounds |
1 to 10 | nintendo.switch1 |
The startup recovery rounds before OpenJoystickDriver gives up. The family's description gives the default (2). |
lizardResendTicks |
1 to 100000 | valve.steam-triton |
The keep-alive ticks between resends of the lizard-mode-off request. The family's description gives the default (75). |
usbCommandTimeoutMs |
1 to 60000 |
nintendo.switch2, vendor.gamesir (USB) |
The timeout of one USB command transfer. The description gives the default: 1000 for nintendo.switch2, 2000 for vendor.gamesir. |
presenceResendReports |
1 to 1000 |
xbox.gip, xbox.xusb
|
The input reports between resends of the request that makes a controller announce itself. The description gives the default: 4 for xbox.gip, 16 for xbox.xusb. |
adapterDisconnectDelayMs |
1 to 60000 |
sony.dualshock4, sony.dualsense
|
How long an adapter or dongle can stay silent before its controller counts as disconnected. The default is 500. |
usbStartupIntervalMs |
0 to 60000 | xbox.gip |
The gap between the raw-USB startup writes. The default is 50. |
usbStartupRetryDelayMs |
1 to 60000 | xbox.gip |
The wait before the first startup retry; each later retry waits twice as long. The default is 1000. |
usbTransferTimeoutMs |
1 to 60000 | xbox.gip |
The timeout of each raw-USB transfer, startup packets and rumble. The default is 2000. |
frameQueueCapacity |
1 to 1024 | every family | How many virtual gamepad reports may wait behind the one being sent. The default is 64. When the queue is full, the newest state replaces the oldest waiting state, so a slow consumer never grows memory and always receives the newest state. A reply to an app's output report is never dropped. |
The default for frameQueueCapacity is in each protocol description's defaults.tuning (Resources/Protocols/<family>.json), so a record, a patch, or all.patch.json overrides it per model or for every controller. Reports queue only while the system takes a long time to accept the previous one; button changes that arrive in the same moment still reach the virtual gamepad in order until the queue is full.
Raw-USB families, such as xbox.gip, do not take the two HID timings. The two recovery values apply only to nintendo.switch1, because the nintendo.switch2 and nintendo.switch-input-only drivers have no startup recovery. ojd record validate rejects a value outside its range or its families.
A tuning in a ControllerPatch merges per value into the bundled tuning: a value you leave out keeps its bundled value. Timings apply when the running service connects the controller again after the record changes.
A family's default timings and control set are data in Resources/Protocols/<family>.json, not Swift constants. protocol-description.schema.json defines the file, and it holds these members:
-
endpoints: the USB endpoint addresses the driver uses. -
rawUSB:true, or the variants that talk raw USB. -
recordSections: the record sections the driver reads. -
tuningKeys: thetuningkeys a record may set for the family.rawUSBTuningKeyslists the keys of its raw-USB variants, and a description that is always raw USB takes onlyrawUSBTuningKeys. -
buttonLabels: the labels of the family's buttons. -
macOSOwnedOutput: the output that macOS owns for the family, and arationalethat says why. -
controls: the controls the driver reports.recordCapabilitiesnames the control deltas (present,rumbleAbsent) that a record of the family may declare; the driver adds the record-derived parts. -
defaults.tuning: the default value of each tuning key, such aslizardResendTicks. The keep-alive interval is not a tuning key:defaults.session.keepAlive.intervalMillisecondsis its only source, and a description or record that nameskeepAliveIntervalMsanywhere is rejected.defaults.variants.<variant>.tuninggives another value for a transport, for example the Switch 1hidStartupIntervalMsof 60 over Bluetooth and 20 over USB. -
defaults.session.keepAlive: the family's keep-alive.intervalMilliseconds(1 to 60000) is the period, and the optionalwrites(1 to 8) are the fixed packets of each run, in the formats ofoutput.startup(a HID report withreportandbytes, or a raw-USB packet withbytes,endpoint,timeoutMillisecondsandsessionByte) withoutphase,delayMilliseconds,transport,crc32Seed,toleratesRejectionandwhen. Unlike startup, one list may mix both formats, each limited withvariants(vendor.gamesirsends a HID report onenhanced-hidand a raw-USB packet onusb).resendTuning: "lizardResendTicks"sends a write only every that-many runs (valve.steam-triton); the family must read the key and default it indefaults.tuning. A record carries no keep-alive writes, so nothing replaces them; a record cannot change the period either, because the interval has no record key. The writes that follow driver state, such as a running rumble, stay in the driver (see Wire Protocols). A HID write needs a family with HID variants, and a raw-USB write needs one with raw-USB variants. -
defaults.session.subcommands(nintendo.switch1only, and it must have it): the subcommand report.reportIdis the output report that carries a subcommand,counterModulus(2 to 256) is the rule of the rolling packet counter (it starts at 0, the next value is(n + 1)modulo the modulus, and the rumble-only report shares the counter), andrequests(1 to 16) are sent in order at startup. A request has asubcommandbyte and exactly one ofdata(1 to 16 bytes) orspiRead(address,lengthof 1 to 29, and apurposeoffactoryMotionCalibrationoruserMotionCalibration, each at most once). An SPI read sends the address as four little-endian bytes and then the length. A startup recovery round re-sends the reads whose reply has not arrived. The rumble bytes in the header and the player-indicator subcommand stay in the driver (see Wire Protocols).
Change a default by editing the description: every record of the family follows it, and a record's or patch's own tuning still wins. Then run ./Scripts/ojd catalog regenerate --write, which copies the descriptions into the Kit bundle.
~/Library/Application Support/OpenJoystickDriver/Controllers/all.patch.json sets a tuning default for every controller. It is a ControllerPatch with "selector": { "all": true }, and controller-patch.schema.json describes it. Only tuning may appear in its patch.
{
"apiVersion": "openjoystickdriver/v1alpha1",
"kind": "ControllerPatch",
"selector": { "all": true },
"patch": { "tuning": { "stickDeadzone": 0.05 } }
}Each value takes the first layer that sets it, from the strongest down: a patch of yours for the model, your all.patch.json, the package, the bundled record, then the driver's own default. A value in all.patch.json applies only to the families that read it, as in the table above. The file is optional. An invalid file is ignored whole, and ojd config show, ojd status, and the config-files check of ojd diagnose report why. ojd config show prints each value and the layer that set it.
The record's stickDeadzone shapes the virtual gamepad's stick report. A profile's innerDeadzone calibrates a stick that the profile maps to aim, flick, pointer, scroll, or steering output. The two do not inherit from each other: a deadzone in a record or in all.patch.json does not change what a profile mapping does. ojd config show --controller lists the active profile's innerDeadzone values in the profile layer next to the record's keys, when the service runs.
A VirtualDevice file in ~/Library/Application Support/OpenJoystickDriver/Personas chooses the virtual gamepad that games see for one controller model, or for one controller unit. virtual-device.schema.json describes it.
{
"apiVersion": "openjoystickdriver/v1alpha1",
"kind": "VirtualDevice",
"match": { "vendorID": 1356, "productID": 3302 },
"persona": "SdlCompatible"
}match names the model by VID:PID in decimal. Add unit with a U- ID from ojd controller show to match one controller. A unit file wins over a model file. persona is Generic or SdlCompatible. Generic is the default (1209:4A4F, pid.codes), and every controller publishes it until you pick another. SdlCompatible publishes the Xbox One S Bluetooth identity (045E:02FD) over an approximation of its report layout. SDL 3.4.x does not list it with the default hints. An optional identity gives a vendor ID, product ID, product name, manufacturer, and version, and an optional labels gives the glyph family (xbox or generic). OpenJoystickDriver publishes the virtual gamepad with that identity over the persona's report layout. The transport, the report layout, and the virtual LocationID stay those of the persona, and version defaults to the persona's. While the service runs, it watches the Personas folder. When a file is added, edited, or removed and a connected controller's identity or persona changes, OpenJoystickDriver creates that controller's virtual gamepad again with the new identity, so you do not reconnect the controller. Games and other apps see the old virtual gamepad disappear and a new one appear. A change that leaves a controller's identity and persona the same, or that concerns another controller, does not touch its virtual gamepad. A file with a wrong apiVersion or kind fails with E5001. Any other file that fails validation is skipped, and ojd status and the config-files check of ojd diagnose name it and the reason. The old VirtualHIDProfileOverrides setting is gone and is not migrated, so recreate your overrides as VirtualDevice files. The persona names hid-generic and hid-xbox-one-s-bt and the Persona kind are gone and are not accepted.
Some families drive several models that differ in one detail. A typed parameter in the record's protocol.parameters object selects the detail. Each family declares its own parameters, and a record with an unknown parameter, a parameter another family declares, or a value of the wrong type is rejected. A flag parameter is true or left out.
-
shareButton(xbox.gip):{"fromEnd": N}reads the Share button N bytes back from the end of the packet (Linux xpadMAP_SHARE_OFFSETis 26). -
layout(nintendo.switch1,nintendo.switch2):Pro,JoyConLeftorJoyConRight, andGameCubeonnintendo.switch2, the NSO GameCube controller.usbProtocol(nintendo.switch1):ChargingOnlyfor a pad whose USB link only charges it, which binds the Bluetooth Classic link only; it excludeslayout. -
innerGripsandlightingMemory(vendor.gamesir): an enhanced HID record names exactly one.innerGripsreports the inner grips;lightingMemory: "ProfileSlots"makes lighting memory slot-based. A GameSir USB record names neither. -
hatSource: "DpadPressure"(vendor.ps3-third-party): any nonzero D-pad pressure byte holds its direction. -
motionCalibration: "Factory"(sony.dualshock4): read the motion calibration from the controller. Without it, OpenJoystickDriver uses nominal calibration, as for third-party DualShock 4 controllers. -
role: "WirelessAdapter"(sony.dualshock4): the Sony DUALSHOCK 4 USB wireless adapter. Output waits until a controller connects to the adapter. -
motionScale: "StrikePad"(sony.dualshock4): a controller whose motion reports half the acceleration in the opposite direction on all axes. -
reportRevision: "Shield2015"(vendor.nvidia-shield): the 2015 SHIELD controller, which has rumble. Without it, the driver treats the controller as the 2017 model, which is input-only on macOS. -
maker: "ThirdParty"(sony.dualsense): the controller is not a Sony DualSense. The driver reads feature report0x03for its features and limits output to them, as SDL'sHIDAPI_DriverPS5does. -
unprobedSensorsandunprobedTouchpad(sony.dualsense): a third-party DualSense that does not answer the capability probe still has motion sensors or a touchpad, and either parameter reads its input in the alternate report layout. -
forcedVibration(sony.dualsense): a third-party DualSense keeps vibration when its probe reply omits it. -
role: "Receiver"(sony.dualsense): a third-party DualSense wireless receiver, which reports with no controller paired. OJD treats the controller as connected only while the packet sequence advances.
A DualShock 4 record takes at most one of role and motionScale. A sony.dualsense record runs as a third-party controller only with maker: "ThirdParty", whatever its vendor ID, and the other DualSense parameters describe that model and need it; a record without parameters is a Sony DualSense. The catalog gives maker: "ThirdParty" to every sony.dualsense record whose vendor ID is not Sony's (054C). A patch merges its parameters key by key into the bundled ones and cannot remove one, but it can set a bundled parameter to another value.
A hid.descriptor record maps the usages of its report descriptor to controls in input.hidUsageMap. Without one, the record uses the standard map in Resources/Protocols/hid.descriptor.json. Each entry names a usage by decimal page and usage and one control, as a kind:
-
button: the control is pressed while the value is nonzero. It cannot be a D-pad id or a stick axis. -
axis: the control is a stick axis (LeftStickX,LeftStickY,RightStickX,RightStickY) orLeftTriggerorRightTrigger. A stick reads -1 to 1 and a trigger 0 to 1 across the element's logical range.range({"min": 0, "max": 100},minbelowmax) replaces that range,invertedreverses the axis, anddeadZone(above 0 and below 1) reads 0 within that fraction of full travel and rescales the rest. -
hat: the entry has nocontrol; it drivesDpadUp,DpadDown,DpadLeftandDpadRight.rangeis{"min": 0, "max": 7}or{"min": 1, "max": 8}and names the value of north, then clockwise; any other value is neutral. Withoutrange, the element's logical minimum is north.
reportID limits the map to one report. A usage the map does not name is ignored, and both the app and ./Scripts/ojd check profiles reject a usage named twice or an axis range whose min is not below its max. The description may also list descriptorShapes that choose a map from the usages a controller's report descriptor declares: the shape in hid.descriptor.json gives a controller with Z, Rz, a Brake or Accelerator and no Rx or Ry the Z/Rz layout, with Brake as LT and Accelerator as RT. The records for the WR007 (11C1:5600), SCUF Envision (2E95:434D), 8BitDo Ultimate 2C (2DC8:301B, 301C) and DragonRise (0079:0006, by patch) show complete maps.
Every document, profile, endpoint message and record names a control with one UpperCamelCase id. Resources/Schemas/vocabulary.schema.json defines the ids, and Resources/Vocabulary/crosswalk.json holds the names the same control has elsewhere. The crosswalk is documentation: it is not a schema alias, and a document that uses another spelling is refused. A - means the other vocabulary has no name for the control. The W3C index is the buttons index of the standard gamepad mapping. Capture and Microphone share the SDL3 name MISC1. The JSON file also lists the HID page and usage of each control. A null usage means the device descriptor assigns it.
| Control | W3C index | SDL3 | evdev | Apple GameController |
|---|---|---|---|---|
South |
0 | SOUTH |
BTN_SOUTH |
ButtonA |
East |
1 | EAST |
BTN_EAST |
ButtonB |
West |
2 | WEST |
BTN_WEST |
ButtonX |
North |
3 | NORTH |
BTN_NORTH |
ButtonY |
LeftShoulder |
4 | LEFT_SHOULDER |
BTN_TL |
LeftShoulder |
RightShoulder |
5 | RIGHT_SHOULDER |
BTN_TR |
RightShoulder |
LeftTrigger |
6 | LEFT_TRIGGER |
ABS_Z |
LeftTrigger |
RightTrigger |
7 | RIGHT_TRIGGER |
ABS_RZ |
RightTrigger |
Back |
8 | BACK |
BTN_SELECT |
ButtonOptions |
Start |
9 | START |
BTN_START |
ButtonMenu |
LeftStickClick |
10 | LEFT_STICK |
BTN_THUMBL |
LeftThumbstickButton |
RightStickClick |
11 | RIGHT_STICK |
BTN_THUMBR |
RightThumbstickButton |
DpadUp |
12 | DPAD_UP |
BTN_DPAD_UP |
DirectionPad |
DpadDown |
13 | DPAD_DOWN |
BTN_DPAD_DOWN |
DirectionPad |
DpadLeft |
14 | DPAD_LEFT |
BTN_DPAD_LEFT |
DirectionPad |
DpadRight |
15 | DPAD_RIGHT |
BTN_DPAD_RIGHT |
DirectionPad |
Guide |
16 | GUIDE |
BTN_MODE |
ButtonHome |
Capture |
- | MISC1 |
KEY_RECORD |
ButtonShare |
Microphone |
- | MISC1 |
- | - |
TouchpadClick |
- | TOUCHPAD |
BTN_TOUCH |
TouchpadButton |
LeftPaddle1 |
- | LEFT_PADDLE1 |
BTN_GRIPL |
XboxPaddleOne |
LeftPaddle2 |
- | LEFT_PADDLE2 |
BTN_GRIPL2 |
XboxPaddleTwo |
RightPaddle1 |
- | RIGHT_PADDLE1 |
BTN_GRIPR |
XboxPaddleThree |
RightPaddle2 |
- | RIGHT_PADDLE2 |
BTN_GRIPR2 |
XboxPaddleFour |
Auxiliary1 |
- | MISC2 |
BTN_TRIGGER_HAPPY1 |
- |
Auxiliary2 |
- | MISC3 |
BTN_TRIGGER_HAPPY2 |
- |
Auxiliary3 |
- | MISC4 |
BTN_TRIGGER_HAPPY3 |
- |
Auxiliary4 |
- | MISC5 |
BTN_TRIGGER_HAPPY4 |
- |
PlayStation Share (Create) and Xbox View are Back. PlayStation Options, Xbox Menu and Switch Plus are Start. Xbox Share and Switch Capture are Capture. The names Share and Options are not controls.
Some controllers can run one of several protocols, such as XInput, DInput, or Switch mode. A HID record can list the alternatives in modes. The record's own protocol is the default mode and has no probes:
"modes": [
{
"id": "feature-ef-answers",
"protocol": { "family": "sony.sixaxis" },
"match": {
"probes": [{ "report": 239, "length": 49, "expect": "answers" }]
}
}
]-
idis lowercase words joined by hyphens, and is notdefault. IDs are unique in a record. -
protocolnames the protocol that mode runs, as the record's ownprotocoldoes. -
match.probeslists 1 or more probes. A mode is selected only when all of its probes hold. - A probe reads one feature report (
report0 to 255,length1 to 65535) and expectsanswers,stalls, orbytes.bytesalso takesoffsetandbytes, uppercase hex pairs the reply must hold at that offset; the other expectations take neither.
A probe reads a feature report from the controller and selects a mode by the reply. OpenJoystickDriver never switches a controller's mode. It tries the modes in record order and runs the first whose probes all hold. When no mode matches, the default mode runs. A report that several probes name is read once.
A probe only reads. It cannot write to the controller. Each read gives up after 2 seconds. When a probe gets no usable reply, the selection stops, the controller stays enabled in its default mode, and OpenJoystickDriver logs a line that names the report. No usable reply means a timeout, an unavailable route, or any failure other than the controller's own refusal, such as a device that is not open, gone, or held exclusively. Only a stalled pipe (0xE000404F) or an unsupported request (kIOReturnUnsupported) is a refusal. It satisfies stalls and fails the other expectations, so a stall where a mode expects answers leaves the default mode running. Probes read HID feature reports, so a controller on another route, such as a Bluetooth LE GATT peripheral, always runs its default mode.
Modes apply to HID controllers only. A record with a raw-USB family cannot use modes, and a mode cannot name one. Every mode follows the record-level rules of the default protocol for input.layout, output.rumble, and capabilities. A patch can replace modes.
The DualShock 3 record carries one mode that probes feature report 0xEF. It is a candidate discriminator only: the mode runs the same protocol as the default, and the record does not say which unit is genuine. The length of 49 is a chosen length, the size of a Sixaxis input report, not a size recorded from a DualShock 3 reply.
OpenJoystickDriver connects a Nintendo Switch 2 controller over Bluetooth LE only when its record names the GATT characteristic that carries vibration:
"bluetoothLE": {
"vibrationCharacteristic": "FA19B0FB-CD1F-46A7-84A1-BBB09E00C149"
}Write the UUID in uppercase. Only a nintendo.switch2 record takes bluetoothLE. The service looks for Switch 2 advertisements with the Nintendo vendor ID (057E) only.
A Switch 2 record without bluetoothLE connects over USB only, and ojd record validate and ojd record show say so. The bundled Switch 2 records have the section, and a ControllerPatch that leaves bluetoothLE out keeps the bundled value. A Controller for a new Switch 2 model needs its own bluetoothLE.
ojd record draft CONTROLLER builds a starting record for a connected controller. The service must be running.
- Run
ojd record draft CONTROLLER > record.json, whereCONTROLLERis an ID fromojd controller listor the controller's VID:PID. - For 10 seconds, press every button and move each stick, trigger, and the D-pad.
--duration SECONDSchanges the time. - Read the bytes that changed. For each report byte that took more than one value, the command prints its offset and its lowest and highest value on stderr.
The record maps what the HID report descriptor states plainly: buttons 1 to 11 in the order the hid.descriptor family reads them, X, Y, Rx, and Ry sticks, Z and Rz triggers, and the hat, with the hid.report-layout family. When nothing maps, it names hid.descriptor instead. Check every control against the changed bytes and correct the record before you install it. The draft is a ControllerPatch for a model with a bundled record and a Controller otherwise.
- Find the VID:PID of your controller. For more information, see Finding your controller ID.
- Write the record file. Set
apiVersiontoopenjoystickdriver/v1alpha1andkindtoControllerorControllerPatch. - Check the file. Run
ojd record validate FILE. - Install the file. Run
ojd record install FILE.
The running service applies the record when the folder changes. It connects each connected controller of that model again with the new record, and leaves other controllers connected. A controller that you disconnected stays disconnected. You do not need to restart the service or unplug the controller.
Run ojd record list. Each line shows a model, its protocol family, and its layer: bundled, package, or user.
Run ojd record show VVVV:PPPP to see one model's effective record and the layer of each field.
OpenJoystickDriver skips a file that is not valid and uses the bundled record for that model. ojd record list, ojd status, and ojd diagnose name each skipped file and the reason.
- Run
ojd record remove VVVV:PPPP. - Confirm the deletion.
The command removes the Controller file and the patch file of that model. A bundled model then goes back to its bundled record.
Each file that OJD reads declares apiVersion: openjoystickdriver/v1alpha1 and its kind, such as Controller, ControllerPatch, Persona, Profile, or ModPackage. Files do not declare $schema. OJD accepts only the apiVersion it supports and rejects every other one, a missing one, and a kind it does not read there. The error names the file, the version it found, and the supported one. It does not convert files. The schemas are flat files in Resources/Schemas/, and each $id is https://xsyetopz.github.io/OpenJoystickDriver/schemas/v1alpha1/<kind>.schema.json. Nothing publishes those IDs yet.
- Before 1.0, a
v1alpha1schema may change incompatibly. The release notes list each such change and how to update a file. - At 1.0 the schemas are promoted to
v1. Av1schema takes only additive changes: a new optional field, or a new value where a reader can ignore it. - A change that is not additive needs a new version,
v2. OJD names the version it accepts, so a file for an unknown version is rejected rather than half read.
The --json output schema has its own release check: within a major version, or a minor version before 1.0, it only gains keys and values. See Using the command line.
- Home
- Start here
- Controllers
- Games
- Remapping
- Your Mac
- Command line
- Help
- Advanced