2

I bought a Sparkfun USB host shield a while ago and have been using it using my Logitech Extreme 3D Pro Joystick. There is some example code for that one which makes it nice and easy to work with. There is also some example code for a generic USB HID Joystick.

My goal is to be able to use any joystick I want with the shield. All the channels on each of my flight sticks are being detected but they all have issues like they are mapped wrongly and don't have a full range of motion.

I understand I need to use USBHID_desc to get the USB HID Descriptor (unique for each joystick right?) and plug those numbers somewhere into the USBHIDJoystick code, but I don't know where to put this information.

My problem boils down to how should I use the data recovered from the USBHID_desc program.

Any help would be appreciated as I'm at a bit of a loss

Thanks

Report Descriptor for Random Flight Stick

Start
0000: 05 01 09 04 A1 01 09 01 A1 00 05 01 09 30 09 31 
0010: 15 00 26 FF 03 35 00 46 FF 03 65 00 75 0A 95 02 
0020: 81 02 09 35 09 32 15 00 26 FF 01 35 00 46 FF 01 
0030: 65 00 75 09 95 02 81 02 75 01 95 02 81 01 09 39 
0040: 15 01 25 08 35 00 46 3B 01 65 14 75 08 95 01 81 
0050: 02 05 09 19 01 29 06 15 00 25 01 35 00 45 01 75 
0060: 01 95 06 81 02 75 01 95 0A 81 01 C0 C0 
Usage Page Gen Desktop Ctrls(01)
Usage Game Pad
Collection Application
Usage Pointer
Collection Physical
Usage Page Gen Desktop Ctrls(01)
Usage X
Usage Y
Logical Min(00)
Logical Max(FF03)
Physical Min(00)
Physical Max(FF03)
Unit(00)
Report Size(0A)
Report Count(02)
Input(00000010)
Usage Rz
Usage Z
Logical Min(00)
Logical Max(FF01)
Physical Min(00)
Physical Max(FF01)
Unit(00)
Report Size(09)
Report Count(02)
Input(00000010)
Report Size(01)
Report Count(02)
Input(00000001)
Usage Hat Switch
Logical Min(01)
Logical Max(08)
Physical Min(00)
Physical Max(3B01)
Unit(14)
Report Size(08)
Report Count(01)
Input(00000010)
Usage Page Button(09)
Usage Min(01)
Usage Max(06)
Logical Min(00)
Logical Max(01)
Physical Min(00)
Physical Max(01)
Report Size(01)
Report Count(06)
Input(00000010)
Report Size(01)
Report Count(0A)
Input(00000001)
End Collection
End Collection Game Pad Pointer X Y(02)(08)
 Rz Z(00)(E3)
(00)(00)
 Hat Switch(00)
 Btn0001
(00) Btn0002
(01) Btn0003
(00) Btn0004
(00) Btn0005
(00) Btn0006
(00)
(00)(00)(00)(00)(00)(00)(00)(00)(00)(00)

USB HID Joystick .INO

#include <usbhid.h>
#include <hiduniversal.h>
#include <usbhub.h>

// Satisfy IDE, which only needs to see the include statment in the ino.
#ifdef dobogusinclude
#include <spi4teensy3.h>
#endif
#include <SPI.h>

#include "hidjoystickrptparser.h"

USB Usb;
USBHub Hub(&Usb);
HIDUniversal Hid(&Usb);
JoystickEvents JoyEvents;
JoystickReportParser Joy(&JoyEvents);

void setup() {
        Serial.begin(115200);
#if !defined(__MIPSEL__)
        while (!Serial); // Wait for serial port to connect - used on Leonardo, Teensy and other boards with built-in USB CDC serial connection
#endif
        Serial.println("Start");

        if (Usb.Init() == -1)
                Serial.println("OSC did not start.");

        delay(200);

        if (!Hid.SetReportParser(0, &Joy))
                ErrorMessage<uint8_t > (PSTR("SetReportParser"), 1);
}

void loop() {
        Usb.Task();
}

USB HID Joystick .cpp

#include "hidjoystickrptparser.h"

JoystickReportParser::JoystickReportParser(JoystickEvents *evt) :
joyEvents(evt),
oldHat(0xDE),
oldButtons(0) {
        for (uint8_t i = 0; i < RPT_GEMEPAD_LEN; i++)
                oldPad[i] = 0xD;
}

void JoystickReportParser::Parse(USBHID *hid, bool is_rpt_id, uint8_t len, uint8_t *buf) {
        bool match = true;

        // Checking if there are changes in report since the method was last called
        for (uint8_t i = 0; i < RPT_GEMEPAD_LEN; i++)
                if (buf[i] != oldPad[i]) {
                        match = false;
                        break;
                }

        // Calling Game Pad event handler
        if (!match && joyEvents) {
                joyEvents->OnGamePadChanged((const GamePadEventData*)buf);

                for (uint8_t i = 0; i < RPT_GEMEPAD_LEN; i++) oldPad[i] = buf[i];
        }

        uint8_t hat = (buf[5] & 0xF);

        // Calling Hat Switch event handler
        if (hat != oldHat && joyEvents) {
                joyEvents->OnHatSwitch(hat);
                oldHat = hat;
        }

        uint16_t buttons = (0x0000 | buf[6]);
        buttons <<= 4;
        buttons |= (buf[5] >> 4);
        uint16_t changes = (buttons ^ oldButtons);

        // Calling Button Event Handler for every button changed
        if (changes) {
                for (uint8_t i = 0; i < 0x0C; i++) {
                        uint16_t mask = (0x0001 << i);

                        if (((mask & changes) > 0) && joyEvents) {
                                if ((buttons & mask) > 0)
                                        joyEvents->OnButtonDn(i + 1);
                                else
                                        joyEvents->OnButtonUp(i + 1);
                        }
                }
                oldButtons = buttons;
        }
}

void JoystickEvents::OnGamePadChanged(const GamePadEventData *evt) {
        Serial.print("X1: ");
        PrintHex<uint8_t > (evt->X, 0x80);
        Serial.print("\tY1: ");
        PrintHex<uint8_t > (evt->Y, 0x80);
        Serial.print("\tX2: ");
        PrintHex<uint8_t > (evt->Z1, 0x80);
        Serial.print("\tY2: ");
        PrintHex<uint8_t > (evt->Z2, 0x80);
        Serial.print("\tRz: ");
        PrintHex<uint8_t > (evt->Rz, 0x80);
        Serial.println("");
}

void JoystickEvents::OnHatSwitch(uint8_t hat) {
        Serial.print("Hat Switch: ");
        PrintHex<uint8_t > (hat, 0x80);
        Serial.println("");
}

void JoystickEvents::OnButtonUp(uint8_t but_id) {
        Serial.print("Up: ");
        Serial.println(but_id, DEC);
}

void JoystickEvents::OnButtonDn(uint8_t but_id) {
        Serial.print("Dn: ");
        Serial.println(but_id, DEC);
}

USB HID Joystick .h

#if !defined(__HIDJOYSTICKRPTPARSER_H__)
#define __HIDJOYSTICKRPTPARSER_H__

#include <usbhid.h>

struct GamePadEventData {
        uint8_t X, Y, Z1, Z2, Rz;
};

class JoystickEvents {
public:
        virtual void OnGamePadChanged(const GamePadEventData *evt);
        virtual void OnHatSwitch(uint8_t hat);
        virtual void OnButtonUp(uint8_t but_id);
        virtual void OnButtonDn(uint8_t but_id);
};

#define RPT_GEMEPAD_LEN     5

class JoystickReportParser : public HIDReportParser {
        JoystickEvents *joyEvents;

        uint8_t oldPad[RPT_GEMEPAD_LEN];
        uint8_t oldHat;
        uint16_t oldButtons;

public:
        JoystickReportParser(JoystickEvents *evt);

        virtual void Parse(USBHID *hid, bool is_rpt_id, uint8_t len, uint8_t *buf);
};

#endif // __HIDJOYSTICKRPTPARSER_H__

Edit of the current problem: (This was fixed with a bit of trial and error in the packed struct)

X: 512 Y: 512 Hat Switch: 0 Twist: 768 Slider: 108 Buttons A: 0 Buttons B: 0
X: 512 Y: 512 Hat Switch: 0 Twist: 256 Slider: 108 Buttons A: 0 Buttons B: 0
X: 512 Y: 512 Hat Switch: 0 Twist: 768 Slider: 108 Buttons A: 0 Buttons B: 0
X: 512 Y: 512 Hat Switch: 0 Twist: 256 Slider: 108 Buttons A: 0 Buttons B: 0
X: 512 Y: 512 Hat Switch: 0 Twist: 768 Slider: 108 Buttons A: 0 Buttons B: 0
X: 512 Y: 512 Hat Switch: 0 Twist: 256 Slider: 108 Buttons A: 0 Buttons B: 0
X: 512 Y: 512 Hat Switch: 0 Twist: 768 Slider: 108 Buttons A: 0 Buttons B: 0
X: 512 Y: 512 Hat Switch: 0 Twist: 256 Slider: 108 Buttons A: 0 Buttons B: 0
X: 512 Y: 512 Hat Switch: 0 Twist: 768 Slider: 108 Buttons A: 0 Buttons B: 0
X: 512 Y: 512 Hat Switch: 0 Twist: 256 Slider: 108 Buttons A: 0 Buttons B: 0
X: 512 Y: 512 Hat Switch: 0 Twist: 768 Slider: 108 Buttons A: 0 Buttons B: 0

Edit for if statement issue: Below is a section of the what my issue pretty much goes down to. Now, I am assuming here that structs act in a similar way to dictionaries in python which I think is a massive simplification as they also seem to work as a class? Anyway, I was working on a way to have a first struct with the data for a specific joystick in, then using the data from that first to fill the second that will be used multiple times in the code but I think that is a worse method then what I have below and I couldn't get it to work anyway.

.cpp

#include "le3dp_rptparser.h"
#define __HIDJOYSTICKRPTPARSER_H__

#include <usbhid.h>

struct LogitechGamePro
{
  union { //axes and hut switch
    uint32_t axes;
    struct {
      uint32_t x : 10;
      uint32_t y : 10;
      uint32_t twist : 9;
      uint32_t slider : 10;   
    };
  };
  uint8_t hat;
  uint8_t buttons_a;
  uint8_t buttons_b;
};



struct GamePadEventData
{
  union { //axes and hut switch
    uint32_t axes;
    struct {
      uint32_t x : 10;
      uint32_t y : 10;
      uint32_t twist : 9;
      uint32_t slider : 10;   
    };
  };
  uint8_t hat;
  uint8_t buttons_a;
  uint8_t buttons_b;
};





JoystickReportParser::JoystickReportParser(JoystickEvents *evt) :
    joyEvents(evt)
{}

void JoystickReportParser::Parse(USBHID *hid, bool is_rpt_id, uint8_t len, uint8_t *buf)
{

  
    
    bool match = true;

    // Checking if there are changes in report since the method was last called
    for (uint8_t i=0; i<RPT_GAMEPAD_LEN; i++) {
        if( buf[i] != oldPad[i] ) {
            match = false;
            break;
        }
  }
    // Calling Game Pad event handler
    if (!match && joyEvents) {
        joyEvents->OnGamePadChanged((const GamePadEventData*)buf);

        for (uint8_t i=0; i<RPT_GAMEPAD_LEN; i++) oldPad[i] = buf[i];
    }
 
}



void JoystickEvents::OnGamePadChanged(const GamePadEventData *evt)
{
    Serial.print("X: ");
    PrintHex<uint16_t>(evt->x, 0x80);
    Serial.print(" Y: ");
    PrintHex<uint16_t>(evt->y, 0x80);
    Serial.print(" Hat Switch: ");
    PrintHex<uint8_t>(evt->hat, 0x80);
    Serial.print(" Twist: ");
    PrintHex<uint8_t>(evt->twist, 0x80);
    Serial.print(" Slider: ");
    PrintHex<uint8_t>(evt->slider, 0x80);
  Serial.print(" Buttons A: ");
    PrintHex<uint8_t>(evt->buttons_a, 0x80);
    Serial.print(" Buttons B: ");
    PrintHex<uint8_t>(evt->buttons_b, 0x80);
    Serial.println("");
}

.h

#if !defined(__HIDJOYSTICKRPTPARSER_H__)
#define __HIDJOYSTICKRPTPARSER_H__

#include <usbhid.h>

struct GamePadEventData {};


struct ST290 {};
struct LogitechGamePro {};


class JoystickEvents
{
public:
    virtual void OnGamePadChanged(const GamePadEventData *evt);
};

#define RPT_GAMEPAD_LEN sizeof(GamePadEventData)/sizeof(uint8_t)

class JoystickReportParser : public HIDReportParser
{
    JoystickEvents      *joyEvents;

  uint8_t oldPad[RPT_GAMEPAD_LEN];

public:
    JoystickReportParser(JoystickEvents *evt);

    virtual void Parse(USBHID *hid, bool is_rpt_id, uint8_t len, uint8_t *buf);
};

#endif // __HIDJOYSTICKRPTPARSER_H__

Error from Example code 2:

sketch\le3dp_rptparser.cpp: In member function 'virtual void JoystickReportParser::Parse(USBHID*, bool, uint8_t, uint8_t*)':
le3dp_rptparser.cpp:36:54: error: no matching function for call to 'JoystickEvents::OnGamePadChanged(GamePadEventData&)'
             joyEvents->OnGamePadChanged(currentValues);
                                                      ^
In file included from sketch\le3dp_rptparser.cpp:1:0:
sketch\le3dp_rptparser.h:43:15: note: candidate: virtual void JoystickEvents::OnGamePadChanged(const GamePadEventData*)
  virtual void OnGamePadChanged(const GamePadEventData *evt);
               ^~~~~~~~~~~~~~~~
sketch\le3dp_rptparser.h:43:15: note:   no known conversion for argument 1 from 'GamePadEventData' to 'const GamePadEventData*'
sketch\le3dp_rptparser.cpp: At global scope:
le3dp_rptparser.cpp:46:6: error: prototype for 'void JoystickEvents::OnGamePadChanged(GamePadEventData*)' does not match any in class 'JoystickEvents'
 void JoystickEvents::OnGamePadChanged(GamePadEventData *evt)
      ^~~~~~~~~~~~~~
In file included from sketch\le3dp_rptparser.cpp:1:0:
le3dp_rptparser.h:43:15: error: candidate is: virtual void JoystickEvents::OnGamePadChanged(const GamePadEventData*)
  virtual void OnGamePadChanged(const GamePadEventData *evt);
               ^~~~~~~~~~~~~~~~
exit status 1
no matching function for call to 'JoystickEvents::OnGamePadChanged(GamePadEventData&)'

1 Answer 1

3

Reading and understanding a report descriptor can be a bit of a black art at times. They're quite cryptic when you first look at them, but actually they make perfect sense.

If you think of each entry (except "input" or "output") as setting some configuration value, and the "input" and "output" entries as using those configuration values, it makes more sense.

Since you are only working with gamepads and joysticks (essentially the same thing) much of the descriptor can be ignored (however you sill have to parse it to get to the other stuff). So let's take your example descriptor a line (or group of lines) at a time and see what they all mean, and what they mean to you.

Usage Page Gen Desktop Ctrls(01)
Usage Game Pad
Collection Application
Usage Pointer
Collection Physical
Usage Page Gen Desktop Ctrls(01)

You don't much care about these from a data perspective, but they do define what the following data is interpreted as. This tells you that "The following data is for configuring the pointer aspect (direction, not mouse pointer) of a game pad.

Usage X
Usage Y

These are the parameters that we are now configuring (joystick X and Y movement). We group them together because they have common settings and parameters. Those parameters are coming up.

Logical Min(00)
Logical Max(FF03)
Physical Min(00)
Physical Max(FF03)

This is the range of the values that we are going to be reporting, and what that range means. In this case there is a 1:1 mapping between the values the joystick reports (Logical) and what those values map to (Physical). We see a better usage of this later on. The numbers are "little endian", which means that "FF03" is actually "0x03FF", so the joystick reports values between 0 and 1023.

Unit(00)

This defines the physical units of the value being reported. In this case there are no units. Again we see a better usage of this later on.

Report Size(0A)
Report Count(02)

Now we get into the nitty gritty of the data being sent. This tells us that each report value is 0x0A (10) bits in size, and there are 2 of them. That's 1 report for the X value of 10 bits, and 1 report for the Y value of 10 bits (in that order, since that is the order of the "Usage" lines above).

So so far we have 20 bits of data in our report that are laid out in bytes as:

XXXXXXXX
YYYYYYXX
....YYYY

There's 4 bits not filled in our bytes yet, but don't worry, they're coming soon. Next comes the "Input" entry:

Input(00000010)

This is where it actually creates the data in the report according to the settings we have set above. The 00000010 tells us that it's a variable data value that is being sent.

Now we have another similar chunk for the Z (throttle) and Rz (rotational Z) values:

Usage Rz
Usage Z
Logical Min(00)
Logical Max(FF01)
Physical Min(00)
Physical Max(FF01)
Unit(00)
Report Size(09)
Report Count(02)
Input(00000010)

This is almost the same as above, but the numbers are different. We have 2 usages of Rz and Z (in that order), and they report values between 0 and 0x01FF (511). Again there are no units to these numbers. The report size is 9 bits (by the way, 29-1 is 511), and there are two reports, one for Rz and one for Z.

So lets feed these into our bytes, 9 bits of z (I'll use "z" for Rz) and 9 for Z:

XXXXXXXX
YYYYYYXX
zzzzYYYY
ZZZzzzzz
..ZZZZZZ

You see we still have two bits unused in our bytes. HID reports don't like unused bits, so we need to pad those out before moving on to a new area of interest:

Report Size(01)
Report Count(02)
Input(00000001)

Here we're defining a report size of 1 bit and there's two reports. That's 2 bits. These are our padding bits to fill out those extra two bits above. You see the Input entry has a different number with it. 00000001 tells us that this is constant data. It's nothing that will ever change - just padding.

So now our HID report looks like:

XXXXXXXX
YYYYYYXX
zzzzYYYY
ZZZzzzzz
00ZZZZZZ

Now we move on to the "Hat switch", which is somewhat similar, but not quite:

Usage Hat Switch
Logical Min(01)
Logical Max(08)
Physical Min(00)
Physical Max(3B01)
Unit(14)
Report Size(08)
Report Count(01)
Input(00000010)

Here we have 1 report of 8 bits. We're sending a value between 1 and 8 for the 8 directions of the hat switch (you can only have 1 direction at a time), and we're using an entire byte to send that value in. It sounds wasteful, given that we only need 4 bits to represent the number 8, but remember we would have to pad the other 4 bits with a constant value to bring it up to a round byte. So it's simpler to just use the whole byte in this instance and save having to have a padding section in the descriptor.

You notice that the logical and physical min and max differ in this section. That shows you what you would map the logical values to in a "real world" situation. In this case the values 1-8 map to the real values 0-315 (0x01b3). It's up to you if you actually do that mapping or not. What are those numbers though? Well, if you divide 315 by 8 (the number of hat positions) you get 45. And each hat position is a 45° increment. So they must map to degrees. And look, "Unit" hs a value of 0x14 in it. If we look that up we see that 0x14 corresponds to "English Rotational Angular Position", or "degrees" in normal speech.

We can now add that to our report format:

XXXXXXXX
YYYYYYXX
zzzzYYYY
ZZZzzzzz
00ZZZZZZ
HHHHHHHH

And finally we have 6 buttons:

Usage Page Button(09)
Usage Min(01)
Usage Max(06)
Logical Min(00)
Logical Max(01)
Physical Min(00)
Physical Max(01)
Report Size(01)
Report Count(06)
Input(00000010)

There is no "Button 1" etc usage in HID, but you can define a range for the usage, which is what has been done here. Basically since we're in the "buttons" usage page we define a range of 1-6 for the buttons - so there's button 1 to button 6 inclusive.

Each button is either 0 or 1 (not pressed, or pressed), and that's the same "In the real world". Each button has a 1 bit report, since that's all you need, and there are 6 reports, one for each button.

Again we're only using 6 out of the 8 bits of a byte for this, so we pad it out with two more constant bits:

Report Size(01)
Report Count(0A)
Input(00000001)

So if we now add those buttons we get our final report format:

XXXXXXXX
YYYYYYXX
zzzzYYYY
ZZZzzzzz
00ZZZZZZ
HHHHHHHH
00654321

So you have 7 bytes of data that you then have to split up and recombine in the right way with all sorts of bit shifting and masking to extract the real values. And of course this varies from joystick to joystick.

As you can guess writing a parser not only for the HID descriptor itself, but to extract the data in accordance with that descriptor, will be quite a task. Some joysticks will have the values all packed together, some won't. Some will need to have things read in one order, and some in another. Some will have more buttons, or more axes of motion than others.

You can read the HID 1.11 specification here which will tell you all about how the raw numbers map to the different things.

The existing parser code is looking for things in hard coded positions in the data coming in and is doing it in very basic ways. It doesn't honour the report descriptor in any way, and instead just takes each byte as an axis.

You would need to re-write that entire Parse() function (or create a child class that overrides it) to use the information you have parsed from the report descriptor to extract the values from the data bytes in the right way. Given the myriad ways it could be done you will have quite a task ahead of you. It would be easier to just support specific joysticks and hard-code the options for them, maybe make a separate subclass for each joystick each with its own overridden Parse() function. It's up to you to then use the different classes in your code as you need.


To address your other problems:

  • In the header file have the definitions for your structs:
struct LogitechGamePro
{
  union { //axes and hut switch
    uint32_t axes;
    struct {
      uint32_t x : 10;
      uint32_t y : 10;
      uint32_t twist : 9;
      uint32_t slider : 10;   
    };
  };
  uint8_t hat;
  uint8_t buttons_a;
  uint8_t buttons_b;
};



struct GamePadEventData
{
  union { //axes and hut switch
    uint32_t axes;
    struct {
      uint32_t x : 10;
      uint32_t y : 10;
      uint32_t twist : 9;
      uint32_t slider : 10;   
    };
  };
  uint8_t hat;
  uint8_t buttons_a;
  uint8_t buttons_b;
};

In the .cpp file use those for casting and population:



void JoystickReportParser::Parse(USBHID *hid, bool is_rpt_id, uint8_t len, uint8_t *buf)
{

    struct GamePadEventData currentValues;
  
    
    bool match = true;

    // Checking if there are changes in report since the method was last called
    for (uint8_t i=0; i<RPT_GAMEPAD_LEN; i++) {
        if( buf[i] != oldPad[i] ) {
            match = false;
            break;
        }
    }
    // Calling Game Pad event handler
    if (!match && joyEvents) {
        // if (joystick is the logitech game pro) {
            // Overlay the struct on top of the data by casting it to
            // a new variable
            struct LogitechGamePro *jsdata = (LogitechGamePro *)buf;
 
            // Copy values from the temporary overlaid struct into
            // a real variable of the right type for the callback
            currentValues.x = jsdata->x;
            currentValues.y = jsdata->y;
            currentValues.z = jsdata->z;
            // etc

            // Call the callback with that real variable
            joyEvents->OnGamePadChanged(&currentValues);
        // } else if (joystick is some other joystick) {
        //     do the same as above with a different joystick struct
        // } ... etc ...

        for (uint8_t i=0; i<RPT_GAMEPAD_LEN; i++) oldPad[i] = buf[i];
    }
 
}
31
  • 2
    Yep, using a packed struct like that is a good way of extracting the data.
    – Majenko
    Jun 19, 2020 at 17:46
  • 1
    Okay, I sorted the problem out. For anyone wondering I had the packed struct a bit wrong. Trial and error for about a minute sorted it for me but you could do it with the report descriptor instead. Jun 20, 2020 at 13:59
  • 1
    You would probably want a big if-then-else (or a switch/case) to overlay the struct over the byte array of the incoming data (cast it), then copy the values from the struct into some standard other struct / array to use later in your program.
    – Majenko
    Jun 20, 2020 at 14:18
  • 1
    Not without seeing your code.
    – Majenko
    Jun 25, 2020 at 19:31
  • 1
    Your empty structs in the .h should be the full structs that are currently in the .cpp. The structs in the .cpp should not exist.
    – Majenko
    Jun 27, 2020 at 13:15

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.

Not the answer you're looking for? Browse other questions tagged or ask your own question.