JoystickInfo Class

A JoystickInfo object describes a joystick device, and lets you read the current button status and axis values from the device. You get these objects from mainWindow.getJoystickInfo().

The best way to handle button presses and axis value changes is via events (see below). However, in some cases it might be necessary (or just more convenient) to poll the current status, such as within an interval timer or another type of event handler. The JoystickInfo object makes that possible through the button() method and the various axis value reader methods. If you are going to use polling, and you're going to poll the device with any frequency, it's best to retrieve the JoystickInfo object once (from mainWindow.getJoystickInfo()), and keep a reference to it in a global variable or other location, so that you can reuse the same object each time you need to access the buttons or axis values. Each call to getJoystickInfo() has to retrieve the internal joystick record and construct a new Javascript object with the descriptor information, so it's a little inefficient to create a new object every on every polling cycle.

If you want to store settings related to a joystick across sessions, the best way to identify the joystick is via its Instance GUID. That's a unique identifier that Windows assigns to each input device specifically for this purpose. You can also identify the type of a device with the combination of its Vendor ID (VID), Product ID (PID), and product name string, but that only tells you the type; it can't distinguish among multiple devices of the same type all plugged in at once.

Joystick events

Button presses and axis value changes can be handled through PinballY's event system. Joystick events are handled through the mainWindow object.

Refer to these event classes for full details on joystick event handling:

Joystick axes

An "axis" represents one direction of motion of one of the moving controls on a joystick, such as the main stick, a wheel, a dial, or a slider. Some controls, like dials and sliders, have only one dimension of motion - they can only move left/right or left/right or clockwise/counterclockwise. These controls only have one axis each, since you can describe their position with a single number saying where they are along that single dimension of motion. The main stick, on the other hand, can move both left/right and forward/back, so it needs (at least) two axes to describe those two dimensions of motion. The stick might also rotate, in which case you need another axis for each rotational degree of freedom, and it might move up and down as well, requiring a third "Z" axis for the vertical motion.

Each axis has a "current value" that tells you the current position of the associated control along that axis. This is simply an integer value, within a range that the device itself defines, called the logical range. Each device defines its own logical range for each axis. JoystickInfo lets you determine the device's range for each axis.

The USB specifications lay out a list of all of the possible axis types that joysticks can have. All USB joysticks have to work within that standard list, so they have to describe their control positions to the computer in terms of the standard axis types. This provides a common frame of reference so that game software can work with a wide variety of physical devices without having to be programmed specially for every individual device; software can just work in terms of the USB-defined abstractions, like the "X axis" and the "slider control".

Here are the axes from the USB specifications that PinballY recognizes:

NameUSB HID Usage CodeDescription
X0x30Main stick X position (left/right)
Y0x31Main stick Y position (forward/back)
Z0x32Main stick Z position (up/down)
RX0x33Main stick X rotation (usually pitch)
RY0x34Main stick Y rotation (usually roll)
RZ0x35Main stick Z rotation (usually yaw)
Slider0x36Slider control position
Dial0x37Dial control position
Wheel0x38Wheel control position
Hat0x39Hat switch position

Most joysticks only have a subset of the types of axes defined in the specs. The JoystickInfo object provides information on which axes are supported by a given device.

The "Usage Code" in the table above is a USB HID concept, which we include purely for reference purposes. It's just an integer identifier assigned in the USB specs to uniquely identify the axis in communications between the device and the PC.

"HID" defined

We use the term "HID" a few times in this chapter. That stands for Human Interface Device, and it's the technical term that the USB specifications use to encompass all sorts of USB hardware devices that involve user input, including keyboards, mice, and joysticks. As far as Windows is concerned, joysticks are just generic HIDs, so programs like PinballY that access them must do so in terms of the Windows HID programming interfaces. PinballY's Javascript interface to the joysticks tries to make things a little more straightforward by working in terms of more concrete, joystick-specific concepts. Even so, the generic HID foundation is still lurking beneath, so you'll see a few mentions of "HID" in the notes below.

Properties and methods

The JoystickInfo object has the following properties and methods:

axes: An object describing all of the axes and controls that the joystick provides. This doesn't tell you the current values of the axes; it just describes which axes are present and what their ranges of motion are. The possible axes are the standard axis and control elements defined in the USB HID specification, as listed above.

The returned object has a property for each axis that exists on the device, named with the Name listed above. For example, if the device has an X axis, then joystickInfo.axes.X will contain a descriptor for the X axis. Each axis descriptor object has the following properties and methods:

The axes value is only accurate if the device is currently attached, and it's only a snapshot of the axis list at the time the JoystickInfo object was created. If the device is currently unplugged or disabled, and you plug it in at some point after retrieving a JoystickInfo object for the device, that JoystickInfo object won't be updated to reflect the actual axis list of the live device. You'd need to make a new call to mainWindow.getJoystickInfo() to create a fresh JoystickInfo object to get the updated axis information once the device is plugged in.

button(index): This method returns the current button press status for the given button number. This should be one of the numbers that appears in the buttons array, since that contains a full list of all of the available buttons on the device. The return value is true if the button is currently pressed, false if not. If the button isn't one of the valid buttons for the device, the method returns undefined.

buttons: An array of numbers, listing all of the buttons that the joystick has. Joystick buttons are simply numbered, usually starting at 1.

The button array is only accurate if the device is currently attached, and it's only a snapshot of the button list at the time the JoystickInfo object was created. If the device is currently unplugged or disabled, and you plug it in at some point after retrieving a JoystickInfo object for the device, that JoystickInfo object won't be updated to reflect the actual button list of the live device. You'd need to make a new call to mainWindow.getJoystickInfo() to create a fresh JoystickInfo object to get the updated button list once the device is plugged in.

enableAxisEvents(options): Enable or disable JoystickAxisEvent for some or all of this joystick's control axes. This fills in the options object with the joystick's unit number, and then calls mainWindow.enableJoystickAxisEvents(), passing along the other options values you provide. options can be omitted entirely, in which case defaults are used for all properties.

guid: A string giving the DirectInput "Instance GUID" for the device, if available. This is formatted in the standard Windows GUID style (without braces), "00000000-0000-0000-0000-000000000000". This is set to undefined if no GUID is available for the device.

The Instance GUID is meant to uniquely identify the device across sessions and system reboots, so it's suitable to use for storing configuration settings and other persistent data related to the device.

isPresent: A boolean (true/false) indicating whether or not this joystick is physically connected to the computer and enabled in Windows Device Manager. The reason this property is useful is that PinballY keeps track of joysticks that have been configured in the settings with commands assigned to their buttons, and lists such joysticks in the array of all joysticks, even if they aren't currently plugged in. That lets PinballY restore joystick button mappings across sessions even if a joystick is temporarily unplugged. This property lets you determine if the descriptor refers to such a saved device record rather than to a device that's actually present in the system right now. (This is a read-only "getter" that retrieves the live status each time you evaluate it, so it's always up-to-date.)

This property is only a snapshot of the status as of the time the JoystickInfo object was created. It's not "live" - it's not updated if the device is later plugged in or unplugged. To get the current status, you need to create a new JoystickInfo object for the device by calling mainWindow.getJoystickInfo() again.

path: A string giving the Windows file system path that represents the device in the file system. This is superficially similar to a regular filename, but it represents the device object rather than a disk file. You can use this to access the device via lower-level Windows API functions such as CreateFile(). This can also serve as a stable identifier for the device, although the Instance GUID is better for that purposes, since it's specifically designed to be stable across system reboots. The device's file path sometimes contains information on the physical bus location of the device, such as which USB port it's plugged into, so it can change if something about the USB bus layout changes or you plug the device into a different USB port.

productID: A 16-bit integer (0 to 65535) giving the USB PID (product ID). This can be used in combination with the vendor ID to uniquely identify the make and model of the device. The product ID is an arbitrary code assigned by the manufacturer, and is meant to be unique for each device they make.

productName: A string giving the product name, as provided by the device's firmware. This is usually a human-readable name describing the make and model, but it's up to the device's manufacturer to specify this string and embed it in the device's firmware. It's even possible for a device to omit this information, in which case PinballY synthesizes a placeholder name based on the VID and PID values.

serial: A string giving the USB device's serial number. This is information reported by the device's firmware, meant to help distinguish multiple instances of the same type of device (e.g., two joysticks of the same make and model). This is up to the device's firmware to provide, so there's no guarantee that every device will have a unique serial number.

setAxisRange(axisName, min, max): Sets a normalized value range for the named axis. axisName is a string containing one of the standard axis names listed earlier, such as "X" or "Slider". min and max are numbers giving new, normalized range you wish to use for the axis. After calling this method, the axis reader function will return values in the range from min to max inclusive. For example, here's how you can set the X axis to return values from -1000 to +1000:

js.setAxisRange("X", -1000, 1000); let x = js.X(); // X() now returns values from -1000 to +1000

You can change the range for an axis at any time simply by making a new call to setAxisRange(). You can also reset to the original underlying "logical" range that the device returns natively by calling setAxisRange() again and omitting the min and max arguments, as in setAxisRange("X").

This setting is "local" to the JoystickInfo object that you call the method on. It doesn't affect the underlying joystick device, and it doesn't even affect any other JoystickInfo objects that refer to the same physical device. The range setting is just a view of the data for this individual JoystickInfo object.

unit: The PinballY logical unit number for the joystick. This is an arbitrary internal ID that PinballY uses to identify the joystick during the current program session. It corresponds to the unit property passed in joystick event objects. This ID doesn't mean anything to Windows or to any other programs (it's purely internal to PinballY), and it's only valid for the duration of the current program session (the same physical device might be assigned a different logical unit number the next time you run PinballY).

The unit number for a given device remains the same throughout a PinballY program session (that is, from the time that you launch PinballY to the time you quit out of that session). The unit number stays the same even if you unplug the joystick and plug it back in during the session (as long as Windows recognizes it as the same device when you plug it back in, anyway). The unit number assignments for specific devices can change each time you run PinballY, though, so you shouldn't use the unit number to identify the device persistently (in an external file, for example, of in the program settings).

vendorID: A 16-bit integer (0 to 65535) giving the USB VID (vendor ID). This can be used in combination with the product ID to uniquely identify the make and model of the device.

A VID is meant to uniquely identify the device's manufacturer. Officially, VIDs are assigned by a central authority, USB-IF, which is the industry trade group that controls the USB specifications. VIDs should therefore never be ambiguous. In practice, though, some device makers use ad hoc, self-assigned VIDs, because a hefty license fee is required to obtain a official VID assignment. (As far as I can tell, for example, the LedWiz's VID is unofficial.) As a result, you can't absolutely count on the VID as a unique manufacturer identification, in that multiple device makers could accidentally self-assign themselves the same VID. If you need to identify an exact device, you might want to check other properties in addition to the VID/PID, such as the product name string and serial number, to make sure they match the expected values and formats for the target device.

Axis/control value reader methods

In addition to the properties and methods listed above, the JoystickInfo object also has a method for each axis/control value listed above. Each axis reader method has the name listed under the Name column in the axis list. For example, the main X axis can be read from the method joystickInfo.X().

Calling one of the axis reader methods retrieves the current, live value for that axis. This returns a number, which is in the "logical" value range for the axis. You can determine the range from the logicalMinimum and logicalMaximum properties of the descriptor object for the axis or control found in the axes array.

If a given axis doesn't exist on the device, calling that axis reader method returns undefined.

For example, here's how you'd retrieve the current X and Y axis values for the joystick at unit 0:

let js = mainWindow.getJoystickInfo(0); let x = js.X(); let y = js.Y();

Normalizing an axis's value range

By default, each axis returns values in the logical value range for the axis. The logical range is defined by the device - there's no generic standard range. Some joysticks might report X values from 0 to +1000, while others might report values from -65535 to +65535. The only way to determine the range is to look at the axis information in the device descriptor.

It's almost always more convenient to work in terms of a range you select, though. The easiest way to do this is to call the setAxisRange() method to set up the desired range on each axis. That method sets up the corresponding axis reader method so that it automatically maps the logical range reported by the device to a custom min-to-max range you define. The mapping is simply linear.

You can also normalize axis ranges yourself, with a little arithmetic. Here's a general formula you can use to convert from the logical range to an abstract range of 0.0 to 1.0:

let xNorm = (x - js.axes.X.logicalMinimum) / (js.axes.X.logicalMaximum - js.axes.X.logicalMinimum);

From there, you can convert to any desired new min-max range:

let xMin = -1000; let xMax = 1000; let xNewRange = xNorm*(xMax - xMin) + xMin;

Rather than writing this out every time we want to perform the normalization, we can create a method on the joystick descriptor object that does this work under the covers for us.

let xMin = -1000; let xMax = 1000; js.myX = function() { // get the original X value from the device let x = this.X(); // normalize it to 0.0 to 1.0 let xNorm = (x - this.axes.X.logicalMinimum) / (this.axes.X.logicalMaximum - this.axes.X.logicalMinimum); // and finally adjust to our desired scale from xMin to xMax return xNorm*(xMax - xMin) + xMin; };

That's actually all that the system's setAxisRange() does. That method just gets a little tricker by replacing the normal X() method with the adjustment calculation, so that you can call X() directly to get the adjusted value.