Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / Languages / C#

HIDAche - An Exercise in Hardware/Software Integration

4.95/5 (211 votes)
12 Mar 2014CPOL22 min read 355.9K   9.5K  
A simple USB hardware/software application to torment others
Image 1

Introduction

The goal of this article is to illustrate the potential for cool applications when software is combined with custom hardware. I will provide a brief introduction to USB and how communication with a custom device can be used to create an annoying gadget.

Before reading this article let me give a little background as to why I would post what might be considered more of a hardware article on a software-centered website. My degree is in electrical engineering and my passion has always been with hardware but I've also enjoyed getting my hardware projects to interact with computers. Levent Saltuklaroglu's article on how to turn on LEDs with the parallel port inspired me. I saw that, even though this is predominantly a software programming site, people really enjoyed a little bit of the hardware side of things. So the objective is to take Levent's idea a little further by providing a USB example. NOTE: I consider myself a fairly capable programmer but expect to take some criticism as to how I implement the Windows programming side of things. Remember that my degree is not in Computer Science although I do enjoy programming quite a bit. I welcome any constructive feedback but ask that you be gentle.

The easy part was deciding to write an article involving hardware and software integration. The hard part was deciding what the hardware would be and how the software would interact with it. About 2 years ago I had the idea to create a USB prank device that would spoof mouse movements and keyboard strokes at predetermined intervals when the device was connected. Well, after about 2 minutes of Google time I found that my idea was not original but I decided to implement it anyway because I felt like I could add a few things to existing implementations and that it would be a good project to familiarize myself with USB. I hope you find the information useful and can have some fun playing with it. I decided to name the device HIDAche.

Prerequisites

I've tried to keep this project as simple as possible but because it involves a software and a hardware side, there are a few things you will need if you decide to recreate the project for yourself. If you just want to play with the software side of the project then you can use Visual C# Express Edition. If you are interested in the hardware side of things then you will also need the following:

  • Basic knowledge of schematics
  • Basic knowledge of circuit construction
  • ANSI C compiler (I use the Microchip C18 free Student Edition)
  • Microcontroller programmer (I use the PIC-MCP-USB)

The list above is for those who are interested in actually recreating the project. If you just want to take a look at the firmware code I recommend using Visual Studio but you can use any text file viewer.

USB Basics

The goal of this article is to provide information, by means of a fairly simple example, about how we can make a USB device that communicates with the computer and how we can create cool applications that utilize that communication. As this article is not meant to be an in-depth look at USB I will only cover the basics. If you want to learn more I suggest the following resources:

Transfer Rates

USB currently comes in three general flavors; High Speed (480 Mbits/s max), Full Speed (12 Mbits/s max) and Low Speed (1.5 Mbits/s max). Of course the USB Implementor's Forum asks us not to use the terms High, Full, and Low speed but recommend "'USB' for slower speed products (1.5 Mb/s and 12Mb/s) and 'Hi-Speed USB' for high-speed products (480Mb/s)." USB 2.0 supports all three speeds. The USB 3.0 spec was recently released and supports a new "SuperSpeed" of 5Gbps. However, at the time of this writing, the spec is too new to really consider. The Microchip PIC 18F2455 microcontroller that I will be using in this project supports Full Speed and Low Speed transmission. We will set it up to use Full Speed communication as a USB 2.0 compatible device.

A great thing about USB is that the cabling is very simple. It’s not like a parallel or even a serial cable that requires you to keep track of a handful of wires. USB cables contain 4 wires; power, ground, and two data wires (D+ and D-). The USB Specification even dictates what color those wires should be so you can count on them being the same from cable to cable (look for the USB logo to be sure).

USB Connectors


Pin NumberColorFunction
1RedPower (+5 volts)
2WhiteD-
3GreenD+
4BlackGround

Power

Power can be supplied to a USB device in several configurations. We can use the power and ground wires as a power source for a project if it is guaranteed to consume less than 100mA of current. This is the low power bus-powered mode. If we need any more power than that our device has to request it (up to 500 mA) from the host upon enumeration (high power bus-powered mode). If we need even more power then we have the option to make the device self-powered meaning it uses little or no power from the host. To keep things simple we will define our device as a low power bus-powered device which means we will get all of the juice we need from the computer and keep it under 100mA.

Communication

USB uses differential data signals on D- and D+ for its communication. Differential means it doesn’t define ones and zeroes with on and off voltage states or even positive and negative voltage states. A differential 1 is defined as D+ being 200mV greater than D-. A 0 is defined as D+ being at least 200mV less than D-. In this configuration it doesn’t matter what the voltage is on either line at a particular time but what the voltage 'difference is between the two lines at any given time. Seems difficult to implement but our microcontroller will take care of those low level details for us.

Device Drivers

One of the major stumbling blocks for USB hardware development is the device driver. A quick look at Toby Opferman's 6 part series on this website gives you an idea of how involved and dangerous (lots of blue screens) driver development can be. Fortunately, if we design things right, we can use drivers that others have already written. Windows has several preinstalled drivers for Operating System use from which we can build our application. This is where the idea of USB device classes comes into play. USB classes are a way to define a standard implementation for a group of devices that perform similar tasks. A few examples of classes that have their own definition under the USB Specification are:

  • Human Interface Devices (HID)
  • Audio Class
  • Communications Device Class (CDC)
  • Imaging Class
  • Mass Storage Class

The classes are so well defined that different devices created within them can often share a driver. For example, in Windows XP most devices that fall within the Audio Class will use usbaudio.sys. HID devices will use hidusb.sys. This will prove useful a little later.

Microcontrollers

The hardware can be the scariest part of a project like this but once you get a grasp on things it's really not too hard to implement. At the heart of many hardware projects nowadays is the microcontroller. It's basically a computer on a chip. In this project I used the PIC 18F2455 from Microchip but other microcontrollers from different manufacturers could have also worked. When I say “computer on a chip” I mean it. For just a few US dollars the 18F2455 has 23 possible I/O pins (great for turning things on like LEDs), an on-board serial port, Full Speed USB transceiver and can perform up to 12 million instructions per second. It can hold 24 kilobytes of program code, has 2KB of on-board RAM, and 256 bytes of EEPROM (the microcontroller's hard drive). These numbers seem small since the standard today is to talk in gigabytes but for small hardware projects it's more than enough. I know what you may be thinking; “Yeah, sounds great but who wants to program in assembly?” Well the good news is that you don't have to. You can do it in C (still not your favorite, I know, but better than assembly). I think what made Levent Saltuklaroglu's article so popular was that it allowed anybody to go home, splice open their parallel cable and start lighting up LEDs. I'll admit, microcontrollers are a little more complex but not impossible. Working with them is very similar to writing a C program in Visual Studio.

Bootloader

I could dedicate an entire article to hardware bootloaders. This article is long enough as it is so I will just give a brief description. Pulling the microcontroller off of your breadboard every time you need to reprogram it gets old really fast. You never get firmware right the first time so there will be plenty of reprograms with each project. To save time and frustration I use a bootloader. Basically, a bootloader is a segment of code that sits at the beginning of the microcontroller's program memory. On power up, that code checks for a button to be pressed. If it's pressed it goes into bootloader mode. If it's not pressed then it jumps to the user program block and begins execution. In bootloader mode, a GUI application can read a compiled hex file and send programming commands to the microcontroller via a USB connection. The bootloader interprets those commands and reprograms the device without the need of a hardware programmer. This allows us to use a microcontroller programmer to program the device a single time with the bootloader code. After that we can reprogram the device with our application code as many times as we like by using the USB connection and a simple GUI application.

The Good Stuff

Ok, that's enough background information. Let's get to the project and the code. So to create HIDAche we first need to know what functionality we will need from a hardware perspective. Pretty simple. We need to know the packet format for a HID mouse and keyboard so we can imitate them, USB communication to get that information to the computer, and a way to store our prank settings. Some features of the device I wanted to implement were as follows:

  • Be able to precisely specifiy the time between keystrokes or mouse movements down to the minute
  • Have just the mouse move, just the keyboard type, or both simultaneously
  • GUI to program prank settings
  • Install it in a thumb drive enclosure for stealth factor

Communication

To get our information to the computer we definitely want to use the principle of USB classes to simplify things. Another reason to choose this route is for stealth. If you use a custom driver the user will get the "Found New Hardware" dialog and have to complete a bunch of steps. That just won't do. For many of the classes described above, the OS will likely have drivers and autoinstall them like when you plug in a new USB keyboard. Since mouse and keyboard functions fall within the HID realm, I decided to go that route. By making the device HID compliant the OS should automatically recognize it and load up the drivers for it. Fortunately, Microchip has example firmware on their website for creating a mouse. I used this as a starting point for the firmware development. HID devices communicate by sending and receiving reports. This helps standardize all of the communication. This is what the definition of the report looks like in the firmware code:

//class specific descriptor - HID mouse and keyboard
ROM struct{BYTE report[HID_RPT01_SIZE];}hid_rpt01={
    {0x05, 0x01, /* Usage Page (Generic Desktop)        */
    0x09, 0x02, /* Usage (Mouse)                        */
    0xA1, 0x01, /* Collection (Application)             */
    0x85, 0x4D, /*  REPORT_ID (77) for mouse            */
    0x09, 0x01, /*  Usage (Pointer)                     */
    0xA1, 0x00, /*  Collection (Physical)               */
    0x05, 0x09, /*  Usage Page (Buttons)                */
    0x19, 0x01, /*  Usage Minimum (01)                  */
    0x29, 0x03, /*  Usage Maximum (03)                  */
    0x15, 0x00, /*  Logical Minimum (0)                 */
    0x25, 0x01, /*  Logical Maximum (0)                 */
    0x95, 0x03, /*  Report Count (3)                    */
    0x75, 0x01, /*  Report Size (1)                     */
    0x81, 0x02, /*  Input (Data, Variable, Absolute)    */
    0x95, 0x01, /*  Report Count (1)                    */
    0x75, 0x05, /*  Report Size (5)                     */
    0x81, 0x01, /*  Input (Constant)    ;5 bit padding  */
    0x05, 0x01, /*  Usage Page (Generic Desktop)        */
    0x09, 0x30, /*  Usage (X)                           */
    0x09, 0x31, /*  Usage (Y)                           */
    0x15, 0x81, /*  Logical Minimum (-127)              */
    0x25, 0x7F, /*  Logical Maximum (127)               */
    0x75, 0x08, /*  Report Size (8)                     */
    0x95, 0x02, /*  Report Count (2)                    */
    0x81, 0x06, /*  Input (Data, Variable, Relative)    */
    0xC0, 0xC0, /*  Double End Collection               */
    0x09, 0x06, /*  Usage (Keyboard)                    */
    0xA1, 0x01, /*  Collection (Application)            */
    0x85, 0x4B, /*  REPORT_ID (75) for keyboard         */
    0x05, 0x07, /*  Usage Page (Keyboard)               */
    0x19, 0xE0, /*  Usage Min (Keyboard LeftControl     */
    0x29, 0xE7, /*  Usage Max (Keyboard Right GUI)      */
    0x15, 0x00, /*  Logical Min (0)                     */
    0x25, 0x01, /*  Logical Max (1)                     */
    0x75, 0x01, /*  Report Size (1)                     */
    0x95, 0x08, /*  Report Count (8)                    */
    0x81, 0x02, /*  Input (Data, Variable, Absolute)    */
    0x95, 0x01, /*  Report Count (1)                    */
    0x75, 0x08, /*  Report Size (8)                     */
    0x81, 0x01, /*  Input (Constant, Array, Absolute)   */
    0x95, 0x05, /*  Report Count (5)                    */
    0x75, 0x01, /*  Report Size (1)                     */
    0x05, 0x08, /*  Usage Page (LEDs)                   */
    0x19, 0x01, /*  Usage Min (Num Lock)                */
    0x29, 0x05, /*  Usage Max                           */
    0x91, 0x02, /*  Output (Data, Variable, Absolute)   */
    0x95, 0x01, /*  Report Count (1)                    */
    0x75, 0x03, /*  Report Size (3)                     */
    0x91, 0x01, /*  Output (Constant, Array, Absolute)  */
    0x95, 0x06, /*  Report Count (6)                    */
    0x75, 0x08, /*  Report Size (8)                     */
    0x15, 0x00, /*  Logical Min (0)                     */
    0x25, 0x65, /*  Logical Max (101)                   */
    0x05, 0x07, /*  Usage Page (Keyboard)               */
    0x19, 0x00, /*  Usage Min (Reserved)                */
    0x29, 0x65, /*  Usage Max (Keyboard App)            */
    0x81, 0x00, /*  Input (Data, Array, Absolute)       */
    0xC0}       /*  End Collection                      */
};/* End Collection

Looks complicated but it's not too bad once you know how to read them (creating them is a whole different beast). The "Report Count" items are telling you the number of fields and the "Report Size" is the number of bits in each field. So for the buttons under Usage Page (Buttons) we see that there are three fields of 1 bit each for button state. This is followed by one field of 5 bits to pad the button information out to a byte. After that we define bytes for X and Y with "Usage (X)" and "Usage (Y)." They are both 8 bits wide. Following the mouse definition is the keyboard definition. Actual functioning reports are pretty difficult to put together and I'm no expert. I bring this up as a point. This sort of thing may seem daunting at first. It was for me but there is documentation all over the web for this sort of thing and if you have a little patience and determination you'll be up and running in no time.

Something important to note in the reports is the report ID for both the keyboard and mouse functionality. Our device isn't really a mouse or keyboard, it's a combination of the two. This is alright as long as the computer knows what kind of information we are sending in each packet. The ID in the report allows us to specify this. I chose a report ID of 75 for keyboard commands and 77 for mouse commands. These were arbitrary numbers and could have been different as long as they are not the same. What that does is, upon enumeration, the report tells the OS "When I want to send you a mouse packet it will have an ID of 77 and if I want to send a keyboard packet it will have an ID of 75." Now, when the computer gets a USB packet from our device with an ID of 77 it will know to translate the contents of the packet into the appropriate OS mouse movements or clicks. NOTE: The report ID is always the first byte in the packet.

With the reports all done we know exactly what our packets should look like going to the computer. The following tables show what the mouse and keyboard packets look like based on how they were defined in the reports.

Mouse

Bit 7Bit 6Bit 5Bit 4Bit 3Bit 2Bit 1Bit 0
Byte 00x4D (Mouse report ID of 77)
Byte 1Y overflowX overflowY sign X sign1Middle ButtonRight ButtonLeft Button
Byte 1X movement
Byte 2Y movement

Keyboard

Value
Byte 00x4B (Keyboard report ID of 75)
Byte 1Control Byte 0
Byte 2Control Byte 1
Byte 3HID Usage ID
Byte 4HID Usage ID
Byte 5HID Usage ID
Byte 6HID Usage ID
Byte 7HID Usage ID
Byte 8HID Usage ID
Byte 9HID Usage ID

The control bytes in the keyboard packet are used to "press" control keys like SHIFT, ALT, CTRL, etc. The rest of the bytes labeled HID Usage ID are for sending keystrokes. Since we are HID compliant we can't just send ASCII characters here. More on this later. That's really all there is to it. The operating system will interpret these packets and simulate mouse movements, clicks, and keystrokes appropriately. So how do we get values into our byte array to send to the computer?

Putting It All Together

So now we know how to communicate with the computer and we know what we want to communicate. We just put it together. But first, when we plug in the device it needs to know what to do. To accomplish this we'll store the prank settings in the microcontroller's EEPROM. This is like the hard drive of the PIC because data persists even when power is removed. I've decided to organize the prank information in the 256 bytes of EEPROM as follows:

 // EEPROM MEMORY LAYOUT ********************************************
#define DEVICE_STRUCT_LOC    0x00
#define INTERVAL_LOC    0x0A
#define DEV_MODE_LOC    0x0B
#define MOUSE_MODE_LOC    0x0C
#define KEYS_LENGTH_LOC    0x0D
#define PHRASE_START_LOC    0x0E

The keystrokes can occupy the rest of the EEPROM after 0x0E. The DEVICE_STRUCT_LOC will contain a unique ID so our programming software knows that a HIDAche device has been connected. When the device is plugged in, the firmware will read all of that data from the EEPROM.

// Read in the hidache device info
// Read in the prank interval
ReadEEPROMData(INTERVAL_LOC, (char*)&prankInterval, 1);
// Read in the device mode
ReadEEPROMData(DEV_MODE_LOC, (char*)&deviceMode, 1);
// Read in the mouse mode
ReadEEPROMData(MOUSE_MODE_LOC, (char*)&mouseMode, 1);
// Read in the number of keystrokes for keyboard mode
ReadEEPROMData(KEYS_LENGTH_LOC, (char*)&phraseLength, 1);

// Now read in the phrase just once so we don't have to read it every time the
// prank interval hits
ReadEEPROMData(PHRASE_START_LOC, phraseArray, phraseLength);

Another feature we need is the ability to move the mouse or send keystrokes on a specified time interval. To implement the delay between mouse movements or keystrokes I used another handy feature of microcontrollers; an internal timer. You can look in the code for details but essentially I did some fine tuning and got the timer to trigger an interrupt every second. I use that interval to perform the correct actions on the minute interval specified when you program the device.

USB communication has very strict timings. To satisfy the demands of that communication we can't go very long in our firmware code without servicing the USB tasks. This is accomplished with a call to USBDeviceTasks(). This does things like check for bus activity and certain control packets. Since this has to be called quite frequently, the entire firmware runs in an infinite while loop. In that while loop we service the USB and then handle our prank tasks with a call to ProcessIO(). In ProcessIO() we check to see if the timer interrupt has been triggered and if the prank interval has expired. If it has we make a call to PerformEmulation() where we decide what to emulate based on our settings.

C#
switch(deviceMode)
{        case MODE_MOUSE:
        // For mouse only mode we can go ahead and set the keyboard
                  // finished variable up front.
        IsKeyboardFinished = TRUE;

        // Load the next value in the buffer
        FillMouseBuffer();
        break;
    case MODE_KEYBOARD:
        // For keyboard only mode we can go ahead and set the mouse
                  // finished variable up front;
        IsMouseFinished = TRUE;
        
        // Load the next value in the buffer
        FillKeyboardBuffer();
        break;
    case MODE_BOTH:
        // Switch back and forth between the two modes until they both
                  // complete
        if(sendType == KEYSTROKE)
            FillKeyboardBuffer();
        else
            FillMouseBuffer();
        break;
}

// Send the buffer to the computer
TransmitBuffer();

To not make the article more lengthy than it already is I'll refer you to the code to see what happens in the fill methods rather than put the code here. For mouse mode I essentially populate the movement portions of the packet to simulate the particular movement setting. Keyboard mode is a little more complicated but not too much. As I stated earlier, we can't just send ASCII characters to the computer since the device is enumerating as a HID USB device. We have to send the HID Usage code for particular keys. You can get these from the USB spec. I store the keystrokes as ASCII characters in EEPROM and then just translate them on the fly in firmware.

C#
BOOL TranslateAsciiToHID(char asciiKey, char *hidKey, BOOL *needShiftKey)
{
    short i = 0;

    // Lets initialize this to false and then just set it if we need it
    *needShiftKey = FALSE;
    
    // Lowercase ASCII characters
    if((asciiKey >= 97) && (asciiKey <= 122))
    {
        *hidKey = asciiKey - 93;
        return TRUE;
    }
    // Uppercase ASCII characters
    else if((asciiKey >= 65) && (asciiKey <= 90))
    {
        *needShiftKey = TRUE;
        *hidKey = asciiKey - 61;
        return TRUE;
    }
    // Numbers
    else if((asciiKey >= 49) && (asciiKey <= 57))
    {
        *hidKey = asciiKey - 19;
        return TRUE;
    }
    // Spacebar.  I decided not to include this in the table because it could be
    // a regular occurance and looking for it in the table will take more time
    else if(asciiKey == 32)
    {
        *hidKey = 44;
        return TRUE;
    }
    // Custom Sequences
    else if(asciiKey > 200)
    {
        switch(asciiKey)
        {
            case 201:
                *hidKey = 42; //Delete
                break;
            case 202:
                *hidKey = 0;
                *needShiftKey = TRUE;
                break;
            case 203:
                *hidKey = 102;  // Keyboard Power Button if supported
                break;
            default:
                *hidKey = 0;
        }
        return TRUE;
    }
    // Look for the character in the key table defined above
    else
    {
        // Look through the table to see if we can find it there
        for(i = 0; i < TABLE_LEN; i++)
        {
            if(KeyTable[i][0] == asciiKey) // We've found the entry
            {
                *hidKey = KeyTable[i][1];
                *needShiftKey = KeyTable[i][2];
                return TRUE;
            }
        }

        // Send a z to indicate an error
        *hidKey = 29;
        return FALSE;
    }
}

KeyTable is defined in the file TranslateAsciiToHID.c and assists in the translation. Once our transmission buffer is filled we're ready to send the information off to the computer with a call to TransmitBuffer(). At that point we'll either see mouse movement or keyboard strokes. If you look at the firmware you'll see that there are a lot of files and code to support the USB protocol. That code was written by Microchip and while it is nice to understand what is going on in servicing USB tasks it is not necessary. For our purposes they expose some methods for us to call to transmit data and that's all we really need to know. We can now move the mouse or send keystrokes based on settings that we program into our device. If you're feeling overwhelmed it's understandable. It's a lot to take in at first but is really pretty straight forward after you've looked at the code a bit. Just remember that I did not have to write a lot of the code. All of the USB code was provided by Microchip (no sense in reinventing the wheel). I've tried to heavily comment my portions of the code and explain what is going on. If you have questions, feel free to post them or email me. Most of my code is in hidache.c, TranslateAsciiToHID.c, HardwareProfile.h, and usb_descriptors.c.

That pretty much does it for the hardware programming. I've tried to intelligently comment the code that I changed from Microchip's original mouse example so that it is easy to understand. The only thing left from a hardware perspective is the schematic. As you can see it is very simple.

HIDAche/hidache3small.JPG

The PIC can read settings out of its EEPROM and perform prank actions based on those settings. Now we need a handy way to allow the devious prankster to get his/her desired prank settings onto the device. That's where the software comes in.

GUI HIDAche Programmer

When I began working on the Windows side of things for this project I had just read Josh Smith's article Creating the Same Program in Windows Forms and WPF which is what led me to do things the way I did with data binding. I realize the implementation is not perfect and would love to hear constructive feedback if you have it. The idea behind the GUI programmer is quite simple; provide a method to get prank settings onto the HIDAche device. Since I am using a bootloader on the prank device an easy solution presented itself. I know that the bootloader has the ability to read and write to the EEPROM on the PIC which happens to be where the prank settings are stored. Since I have the source code for the bootloader I also know what the command packets need to look like to access the EEPROM. So the simple solution is to have the device enter the bootloader mode and read and write that information just as a bootloader GUI program would. Most of the code is very straight forward so I'll just focus on a few key points. Let's see how it works.

Device Connection

If you remember from above to enter bootload mode we simply hold down the device button and plug it in. So if the GUI programmer is open and we plug in our device a couple of things need to happen. First, we need to be able to detect the connection of a USB device, and second, we need to be able to determine if that device is our prank device. Well, there are plenty of articles on this site about how to detect USB device connections so I won't go too much into it here. Basically all we do is override the WndProc method in our form and register for USB device notifications with a call to RegisterForHIDDeviceNotifications() which is located in the USBComm library. That overriden function looks like this:

C#
protected override void WndProc(ref Message m)
{
    switch (m.Msg)
    {
        case WM_DEVICECHANGE:
            // The WParam value identifies what is occurring.
            // n = (int)m.WParam;
            if ((HIDAcheControl)this.flowLayoutPanel1.Controls["hidCtrl"] != null)
            {
                // Update the connection status graphic and ask the user if they want
                // to load the configuration if this change is a connection
                ((HIDAcheControl)this.flowLayoutPanel1.Controls[
                    "hidCtrl"]).HidAcheDevice.CheckConnectionStatus();

                if ((int)m.WParam == DBT_DEVICEARRIVAL)
                {
                    HidAcheDevice dev = ((HIDAcheControl)this.flowLayoutPanel1.Controls[
                        "hidCtrl"]).HidAcheDevice;
                    if (dev.IsConnected)
                    {
                        if (MessageBox.Show(this, "A HIDAche device has been connected." +
                            "Would you like to read the configuration from the device?",
                            "Device Connected", MessageBoxButtons.YesNo, 
                            MessageBoxIcon.Question) == DialogResult.Yes)
                            dev.ReadDevice();
                    }
                }
            }
            break;
    }
    base.WndProc(ref m);
}

WM_DEVICECHANGE is the message that will be sent on a connection or removal of a USB device. So, if that is the message we receive we check the connection status to display whether a device is connected or disconnected. In our CheckConnectionStatus() method we verify that the VID/PID match what our device should have so we don't show a connected state when some random USB device is attached. If it is a HIDAche device we offer the option of reading in the current settings. Pretty simple. NOTE: I realize that there is a design flaw here. You'll notice that at the end of the overrided function there is a call to base.WndProc() which passes our message along. The fact that there is a MessageBox.Show() in the function means the message could be held up indefinitely which could cause a problem when connecting other USB hardware. Any suggestions on how to get around this would be appreciated. This is only a problem when programming our settings and isn't a problem when the device is attached in prank mode.

Databinding to a HIDacheDevice Object

In my best attempt to follow Josh Smith's example of databinding I use a BindingSource object to connect the elements of the HIDAcheControl to a HIDacheDevice object. It is a very straightforward way to separate the object from the UI. Every setting on the control is bound to an object member and will update it accordingly when you change things with the user interface. I really like this approach since it simplifies the code. When settings change we don't have to write code to actually change the values in the object, the binding takes care of it. Perhaps the most interesting part is binding the connection status image to the IsConnected public member. This took very little extra code. The following was added to the constructor of the HIDAcheControl.

C#
Binding connectionBinding = this.lblIsConnected.DataBindings[0];
connectionBinding.Format += this.ConvertIsConnectedToString;

All this does is force a call to ConvertIsConnectedToString() when the label binds. In that method we handle setting the correct text and the image based on the connection status.

USBComm Library

The last really interesting piece of this project is the software connection between the device and the computer used for programming prank settings. It's one thing to have the prank device send USB commands to the OS but it is another thing to get an application to send USB packets to our device. For this we use the USBComm library. I firmly believe in giving credit where credit is due. The majority of this library was written by Jan Axelson and can be downloaded from USB Central. Essentially the library simplifies the task of sending packets to HID devices like the prank device. I made a few changes and additions to the library to make things a little easier for this device like handling the fact that the packets come back left zero-padded in a 65 byte buffer. To handle the commands to read and write to the device EEPROM I created the DeviceCommand object which greatly simplifies the process of constructing the packets. As I mentioned above since I have the bootloader code I know what the commands are to perform a read and write of EEPROM. To create a packet to read 10 bytes from the EEPROM staring at address 0x0A the code would look like this.

C#
// Library needs to know the VID/PID of our device. We set those in the firmware.
USBComm.HIDCommObject comm = new USBComm.HIDCommObject(0x003C, 0x04D8);

// Read command is 0x07
// Write command is 0x05
// The address of the DeviceCommand is 4 bytes. The F0 tells the bootloader code
// that we are going to write to EEPROM and the 0A at the end says we will start
// at address 0x0A.

USBComm.DeviceCommand command = new DeviceCommand(0x07, (Int32)0x00F0000A, 10,
    new byte[10]);

// Now we send it and get our response

comm.SendCommandAndGetResponseFromDevice(ref command);

Because we pass our DeviceCommand by reference SendCommandAndGetResponseFromDevice() returns the object to us with the requested data in the Data member of the DeviceCommand. That's pretty much it. The code is pretty easy to navigate so you can get more in-depth details by looking there. If you have any specific questions feel free to post them.

Points of Interest

Well, that wraps up my first CodeProject article. Hopefully it wasn't too painful to read. This is just one way to make use of hardware/software integration. The possibilities are only limited by your imagination. Once we have a way to communicate between the computer and a device we can do any number of things; turn on lights, display sensor readings, turn servos, etc. I'd like to write another article involving hardware/software integration with USB capability but will only do so if there is an interest so if you've enjoyed the article and would like to see more please let me know.

History

  • Added bootloader code to project files - 3/12/2014
  • Original Article - 1/21/2009

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)