Composite Devices - Part 3

In our last installment you learned, step by step, how to build up a USB Composite Device in HIDmaker FS . Along the way, you saw that the set of data items that is used by one of the USB Interfaces in your Composite device (which we call an Interface Data Set) can be either made from scratch, read from a file that was saved from another HIDmaker project, or some mixture of the two.

You also read about why both the Name and the Usage (= the combination of Usage Page and Usage ID) for each USB transfer variable must be unique across your whole project, so both your compiler and the PC's HID device drivers can always distinguish each variable from all other variables in your project.

This time we'll talk about how HIDmaker's generated PC code talks to multiple Interaces in a USB Composite Device, and how (and why) you can modify that behavior.

{readmorelink}Read More Of Composite Devices Tutorial Part 3...>>{/readmorelink}

 

How HIDmaker's Generated PC Program Normally Works with a USB Composite Device

In the usual situation, the PC program that HIDmaker generates for a USB Composite Device expects to find more than one single USB Interface for the peripheral device it is matched to.  When Windows finds tells the program that your device is attached, then the PC program will open all of the USB Interfaces in this Composite device, and find all the Reports and variables that it expects to find in each Interface.

For the 2-Interface BInOut project we created last time (which is one of the sample projects in your HIDmaker FS installation), the class that represents your whole device is called TBothInOut.  The file that defines the TBothInOut class has a filename that is always BInOut_L.*.  The extension (which we showed as "*" here) depends on which PC compiler it was generated for: it would be .vb for a Visual Basic .NET project, .cs for a C# .NET project, .pas for a Delphi project, and so on.  The "BInOut" part of the file name is just the project name.  The "_L" part tells us that this is the "library" for all the variables and Reports that are in the entire device.

The class that is defined in this file contains properties, methods, and events.  Among the properties of this class you will find a separate object that represents each USB transfer variable.  

In fact, a custom generated comment block at the top of this file tells us what variables and reports are in this class for this particular project.  Here's what it looks like for a Delphi file that was generated for the BInOut project:

<snip>

// This HID has the following Reports, which contain the following variables:

//

//  OutputRptA :

//    OutData1  - 5 bit simple variable

//    OutArray1  - array variable, 9 elements, each 8 bits long

// You may write these variables by first setting the properties OutData1 etc.,

// then calling function WriteOutputRptA to send the data to the device.

// For array variables, see the special instructions below.

//

//  InputRptB :

//    InData1  - 3 bit simple variable

//    InArray1  - array variable, 9 elements, each 8 bits long

// You may read simple variables by first calling function ReadInputRptB to get

// the report from the device, then accessing properties like OutData1.UnscaledValue etc.

// For array variables, see the special instructions below.

//

//  OutputRptC :

//    OutData2  - 7 bit simple variable

//    OutArray2  - array variable, 10 elements, each 8 bits long

// You may write these variables by first setting the properties OutData1 etc.,

// then calling function WriteOutputRptC to send the data to the device.

// For array variables, see the special instructions below.

//

//  InputRptD :

//    InData2  - 4 bit simple variable

//    InArray2  - array variable, 16 elements, each 8 bits long

// You may read simple variables by first calling function ReadInputRptD to get

// the report from the device, then accessing properties like OutData1.UnscaledValue etc.

// For array variables, see the special instructions below.

//

//

// This class also provides methods like ReadAllInputRpts and

// WriteAllOutputRpts to simultaneously update all reports of each time

// with a single function call.

//

//*****************************************************************************

 

</snip>

Note that this device has two USB Interfaces, each with an Input Report and an Output Report.  When the PC program is notified that the device it is matched to is connected to the PC via USB, then the program will (normally) tell the operating system to open each one of those Interfaces in the device.

If you only want to communicate with one USB Interface at times, HIDmaker's generated source code provides a separate routine to read the Input Report from each USB Interface : for this particular project, these routines are named ReadInputRptB() and ReadInputRptD().  Similarly, the generated code provides a separate routine to write the Output Report to each individual USB Interface : these are named WriteOutputRptA() and WriteOutputRptC() in this particular project.

Alternatively, you can call ReadAllInputReports() to read the data from all USB Interfaces in the device at the same time if you prefer, and you can call WriteAllOutputReports() to send data to both Interfaces at the same time if you want.

 

Special Considerations for Mouse and Keyboard Interfaces

We mentioned that the example project we created last time did not contain any USB Interfaces that identify themselves as either an official Keyboard or an official Mouse, to avoid certain complications.

The complications we are speaking of have to do with the fact that whenever Windows finds a USB Interface that is a keyboard or a mouse, Windows will automatically open that Interface for exclusive use by the operating system.  That means no normal "User Mode" PC application (as opposed to a "Kernal Mode" device driver) can read from or write to a keyboard or a mouse directly.  Any attempt to open a keyboard or mouse device for direct I/O will produce an error message from the operating system.

This affects us because the PC program that HIDmaker creates for a USB Composite Device tries to open each of the USB Interfaces for I/O.  If at least one of these

USB Interfaces is a keyboard or a mouse, then the generated program wouldn't work.  That's OK, we have a really easy workaround, which is made possible by HIDmaker's flexible architecture, the Interface Data Set concept, and the .rdi data files that HIDmaker uses. Here's the trick:

To make a PC program that will ignore a keyboard or mouse Interface, follow these simple steps:

 

  1. Make a new HIDmaker project that contains all the Interfaces in your device except the keyboard or mouse Interface that you want the PC program to ignore.
  2. For each USB Interface that you leave in this new project, load the .rdi file from the corresponding USB Interface in the original (full) project
  3. Generate code as usual
  4. Add a few lines of code to the generated PC program, which will narrow down the search criteria for the PC program to determine which Interface (or Interfaces) it should open (see below).  You set up this code so it opens
  5. Then, use the generated PC program, from the new "partial" project, to talk to just some of the Interfaces in the peripheral device that contains the PIC code from the "full" project!

This Opens Up Interesting Possibilities...

Even if you don't have a keyboard or mouse Interface in your device, you can generate a separate PC program that talks to each one of your device's Interfaces individually.  Then, you can run all those separate programs at the same time if you want, or you can run each program at a different time.

Let's see how we can do that with our BInOut example program, which contains 2 USB Interfaces.  In creating this project, we have followed the rules and have given each Interface a unique Top Level Usage, like so:

 

  • Interface 0
    • Usage Page:  0xFF00  (= 65280)  // One of the "Vendor Defined" values
    • Usage ID:  0xFFF1 (=65521) // One of the "Vendor Defined" values
  • Interface 1
    • Usage Page:  0xFF00  (= 65280)  // One of the "Vendor Defined" values
    • Usage ID:  0xFF09 (=65289) // One of the "Vendor Defined" values

 

Remember that the Top Level Usage of each USB Interface is the combination of Usage Page and Usage ID of the top level Application Collection. Even though the Usage Page is the same for both Interfaces, it is enough that the Usage IDs of these Interfaces are different.

Now we have HIDmaker FS generate two separate PC programs, one for each Interface.

The code that HIDmaker FS has generated for each of these two projects is almost good enough, but not quite. Here's the catch:

Suppose we try to use the PC program that only knows how to talk to the first USB Interface in the device, Interface 0.  If you compile the code exactly the way HIDmaker generated it, the program will try to open all matching USB Interfaces - that is, of Interfaces, in all HID devices that are currently connected to the PC, that have the expected Vendor ID and Product ID.  The problem is, in a USB Composite Device, both Interfaces are part of the same device, so they both have the same Vendor ID and Product ID.

If you try this, the PC program will mistakenly open both Interfaces, because it was only told to look any Interface that has the right Vendor ID and Product ID.  It will open both Interfaces successfully, but it will fail when it tries to read the data from the second Interface in the device - the data will have a different format, and probably a different length, than it expected. (Remember that this is the PC program that only knows about the data in the first Interface, right?)

Luckily, there is a very simple fix.  Our HIDagent object can be told to narrow down its search criteria, to match other factors in addition to Vendor ID and Product ID.  We just need to add a few lines of code to ALSO look for the right Top Level Usage.  We can do that by adding the following 4 lines of code (shown in red) to the OpenOurHID routine in the generated code:

 

 VB .NET sample code:

<snip>

 Private Function OpenOurHid() As Boolean

    Dim Success As Boolean

    Dim GiveUp As Boolean

    Dim Choice As Integer

    Dim CrLf As String

    Dim Prompt As String

    '

    Do

      HIDagent.VID_To_Match = MY_VID

      HIDagent.PID_to_Match = MY_PID

      HIDagent.SearchOnPID = True

      HIDagent.SearchOnVID = True

      ' Added by hand

      HIDagent.SearchOnUsagePage = True

      HIDagent.UsagePage_To_Match = &HFF00S   '  &HFF00 or 65280 decimal

      HIDagent.SearchOnUsage = True

      HIDagent.Usage_To_Match = &HFFF1S  ' &HFFF1I or 65521

      GiveUp = False

      Success = HIDagent.OpenAllMatchingIntfs

      If Not Success Then

        Beep()

        CrLf = Chr(13) & Chr(10)

        Prompt = "Unable to open USB device: " & CrLf & CrLf & MY_PROD & CrLf & CrLf & "Please attach the device and click OK," & CrLf & "  or click Cancel to exit"

        Choice = MsgBox(Prompt, MsgBoxStyle.OKCancel + MsgBoxStyle.Information + MsgBoxStyle.ApplicationModal)

            ' etc...

 

</snip>

 

The 4 lines we showed above are for a Visual Basic .NET version of the PC program.  If you prefer to use a different one of the many PC compilers we support, the lines you add will be very similar to those shown above. 

For example, here are the same 4 lines (shown in red) added to a Delphi version of the same generated program:

 

Delphi sample code:

<snip>

function TMainForm.OpenOurHID: Boolean;

var

  Success, GiveUp : Boolean;

  Choice : Integer;

begin

  repeat

    HIDagent.VID_To_Match := MY_VID;

    HIDagent.PID_To_Match := MY_PID;

    HIDagent.SearchOnPID := True;

    HIDagent.SearchOnVID := True;

    // Added by hand

    HIDagent.SearchOnUsagePage := True;

    HIDagent.UsagePage_To_Match := 65280;  

    HIDagent.SearchOnUsage := True;

    HIDagent.Usage_To_Match := 65521;   

    GiveUp := False;

    Success := HIDagent.OpenAllMatchingIntfs;

    if not Success then

      begin

        Beep;

        Choice := MessageDlg('Unable to open USB device: '+#13+#10

                 +#13+#10

 

    // etc...

</snip>

 

Here's how this looks in action:

 

Thumbnail image

 

The screen shot below shows 3 different PC programs (all generated by HIDmaker FS ) that are talking to the same USB Composite Device at the same time. 

  1. The program in the middle is the full program that was generated by HIDmaker FS for the BInOut example project we have been describing. It has not been modified in any way. It talks to both of the two USB Interfaces in the device.
  2. The program on the left only talks to the first USB Interface (Interface 0) in the same BInOut device.  It was generated as a "Normal" (= single Interface) device, using the .rdi data file that was saved from Interface 0 of the BInOut Composite device project.  Then before compiling, the appropriate version of the 4 lines of extra code shown above were added to make the PC program only open an Interface that has the right Vendor ID, Product ID, and Top Level Usage.  That will cause it to open Interface 0 of any BInOut devices that happen to be connected to the PC.
  3. The program on the right only talks to the second USB Interface (Interface 1) in the same BInOut device.  It was generated using the .rdi file saved from Interface 1 of our BInOut Composite device project.   Before compiling, the appropriate version of the 4 lines of extra code shown above were added to make it only open Interface 1 of any matching devices.

It is important to understand several things:

  • These three programs shown in the picture are running simultaneously, and are able to talk to different Interfaces of the the same device at the same time.  This is how USB devices are supposed to work, but unfortunately, few of them actually do. This is very easy to do with HIDmaker FS , but it is NOT easy to do with those free demos that you get from the web or from the semiconductor manufacturer.
  • Notice that the data items in each USB Interface consist of a simple variable whose size is less than 8 bits, followed by an array of 8-bit elements.  Real USB HID class rules require your device to be able to handle a mixture of odd sized data items, that are not limited to 8, 16, 24, and 32 bit sizes.  Since the data items must be tightly packed in a real HID class device, this means that NONE of the 8-bit array elements is aligned to a byte boundary in the actual packets that get sent over the USB cable. Yet this works perfectly.  This is so trivial to do in HIDmaker FS that you don't have to think about it, or even be aware of it.  But those free demo programs you get from the web don't seem to have any idea of how to do this.
  • You could program the BInOut hex file into a second device, and plug that new device in to another USB port on the same PC, while these 3 programs are still talking to the first BInOut device.  Each program would automatically recognize the second device, and start talking to it, in addition to the first device.  Each program will have a separate conversation with each device (or with a specific Interface of each device).  You don't have to write or modify a single line of HIDmaker's generated code to get this to happen. 

 

How To Use This Technique To Make More Profits

Think about what this kind of capability can let you do.  Here are some possibilities:

 

  1. You can use this to have an after market for your device. You can make a device that does 3 different kinds of functionality, but you can sell the customer just the software for one Interface (i.e. one kind of functionality) if he wants to save money.  Later on, you can sell him a second PC program to let him access the functionality in one of the other Interfaces. Still later, you can sell him a third PC program that lets him use the third Interface.
  2. Another way to do the same thing would be to provide a PC program that can talk to all 3 USB Interfaces, but provide an unlock code for only the parts of functionality that he paid for.  Later, he can purchase another unlock code to get more capability from his device.

We recognize that these revenue enhancing approaches might not appeal to everybody. You might be inclined to think that you are doing something sneaky - trying to force customers to pay extra to be able to use capabilities that are already there, in the device he already purchased.  Yes, it might seem unfair if you did it this way.  The applicability of these techniques to your product will depend on your customers and your market, and how you phrase your offer.  If you can offer to sell them a separate PC program that talks to that second USB Interface , and that separate PC program truly adds value, then there is nothing wrong with asking them to pay for that extra capability.

Look at it this way.  Your development time has value.  You can offer customers a product with less functionality at a lower price, which will increase your sales by making your product affordable to more people.  Then later, if a customer decides he wants the extra capability, he can pay for it then.  This is just a way of letting your customers pay for only the functions that they need.

It's kind of like cable television.  Every subscriber has the same cable box, and gets the same signals coming in on the cable.  If you want to view premium channels, though, you have to pay for them.

 

CommentsWanted

Got a comment on what you just read?  Let us know - just add a comment below.
We'd love to hear from you!

Save