USB HID Support on Pico!

Posted on
Page
of 4
/ 4
Last Next
  • I've finally managed to add USB HID support on the Pico. An experimental binary for it should be here in an hour or two.

    It's taken most of last and this week to work my way around ST's USB drivers, and they now don't bare much relation to the originals at all :(

    So... How do you use it?

    // Simple mouse
    E.setUSBHID({
      reportDescriptor : [
      0x05,   0x01,
      0x09,   0x02,
      0xA1,   0x01,
      0x09,   0x01,
    
      0xA1,   0x00,
      0x05,   0x09,
      0x19,   0x01,
      0x29,   0x03,
    
      0x15,   0x00,
      0x25,   0x01,
      0x95,   0x03,
      0x75,   0x01,
    
      0x81,   0x02,
      0x95,   0x01,
      0x75,   0x05,
      0x81,   0x01,
    
      0x05,   0x01,
      0x09,   0x30,
      0x09,   0x31,
      0x09,   0x38,
    
      0x15,   0x81,
      0x25,   0x7F,
      0x75,   0x08,
      0x95,   0x03,
    
      0x81,   0x06,
      0xC0,   0x09,
      0x3c,   0x05,
      0xff,   0x09,
    
      0x01,   0x15,
      0x00,   0x25,
      0x01,   0x75,
      0x01,   0x95,
    
      0x02,   0xb1,
      0x22,   0x75,
      0x06,   0x95,
      0x01,   0xb1,
    
      0x01,   0xc0 ]
    });
    
    function mouse(x,y,b) {
      E.sendUSBHID([b&7,x,y,0]);
    }
    
    // Now save, unplug USB and re-plug it
    
    mouse(20,20,0); // moves mouse diagonally down
    
    // Simple Keyboard
    
    E.setUSBHID({
      reportDescriptor : [
            0x05, 0x01,          // Usage Page (Generic Desktop),
            0x09, 0x06,          // Usage (Keyboard),
            0xA1, 0x01,          // Collection (Application),
            0x75, 0x01,          //   Report Size (1),
            0x95, 0x08,          //   Report Count (8),
            0x05, 0x07,          //   Usage Page (Key Codes),
            0x19, 0xE0,          //   Usage Minimum (224),
            0x29, 0xE7,          //   Usage Maximum (231),
            0x15, 0x00,          //   Logical Minimum (0),
            0x25, 0x01,          //   Logical Maximum (1),
            0x81, 0x02,          //   Input (Data, Variable, Absolute), ;Modifier byte
            0x95, 0x01,          //   Report Count (1),
            0x75, 0x08,          //   Report Size (8),
            0x81, 0x03,          //   Input (Constant),                 ;Reserved byte
            0x95, 0x05,          //   Report Count (5),
            0x75, 0x01,          //   Report Size (1),
            0x05, 0x08,          //   Usage Page (LEDs),
            0x19, 0x01,          //   Usage Minimum (1),
            0x29, 0x05,          //   Usage Maximum (5),
            0x91, 0x02,          //   Output (Data, Variable, Absolute), ;LED report
            0x95, 0x01,          //   Report Count (1),
            0x75, 0x03,          //   Report Size (3),
            0x91, 0x03,          //   Output (Constant),                 ;LED report padding
            0x95, 0x06,          //   Report Count (6),
            0x75, 0x08,          //   Report Size (8),
            0x15, 0x00,          //   Logical Minimum (0),
            0x25, 0x68,          //   Logical Maximum(104),
            0x05, 0x07,          //   Usage Page (Key Codes),
            0x19, 0x00,          //   Usage Minimum (0),
            0x29, 0x68,          //   Usage Maximum (104),
            0x81, 0x00,          //   Input (Data, Array),
            0xc0                 // End Collection
      ]
    });
    
      // 1 = modifiers
      // 2 = ?
      // 3..8 = key codes currently down
    
    var KEY = {
      A           : 4 ,
      B           : 5 ,
      C           : 6 ,
      D           : 7 ,
      E           : 8 ,
      F           : 9 ,
      G           : 10,
      H           : 11,
      I           : 12,
      J           : 13,
      K           : 14,
      L           : 15,
      M           : 16,
      N           : 17,
      O           : 18,
      P           : 19,
      Q           : 20,
      R           : 21,
      S           : 22,
      T           : 23,
      U           : 24,
      V           : 25,
      W           : 26,
      X           : 27,
      Y           : 28,
      Z           : 29
    };
    
    function tap(key) {
      E.sendUSBHID([0,0,key,0,0,0,0,0]);
      setTimeout(function() { E.sendUSBHID([0,0,0,0,0,0,0,0]); }, 10);
    }
    
    function type(txt, callback) {
      if (!txt.length) return;
      var int = setInterval(function() {
        tap(KEY[txt[0]]);
        txt = txt.substr(1);
        if (!txt.length) clearInterval(int);
      }, 20);
    }
    
    // Now save, unplug USB and re-plug it
    
    type("HELLOWORLD");
    

    I'll make these all into libraries when it's ready for a proper release, but it's quite usable even now :)

  • Just to add, the keyboard stuff is shamefully ripped off the Teensy 3 USB library - there must be a better source of USB report descriptors, but I can't find one :(

  • Just to add, at the moment this will only work if you power the pico from a battery, and then plug into USB after. It's an easy fix, I just forgot to do it before posting this

  • Great to see this coming along and to be here now!

  • Awesome news, looking forward to getting my Pico to try this on!

  • That's pretty neat.

    Since I am missing the critical USB know-how: we have to enter some kind of HID mode which then will be reset when plugged in without power from the battery, or is this just another USB channel in parallel to the serial port?

    Can this already be revoked in software or is the current way to toggle it by having it started up before plugging (HID settings active) or not (pure console mode)?

    As I said, no idea on the "mode" model right now, but eager to try out.

    Great addition to the Pico's dongle form factor.

  • With USB, when you plug a device in it's 'enumerated' - the host computer asks it what it is and what it can do. So to tell the PC it's a mouse, Espruino has to know to do that at the time it's plugged in.

    When HID is enabled, Espruino actually enumerates as a HID device and a COM port, so you can still communicate with it just like before - including changing the USB HID code on the fly. Only if you want to change what it enumerates as, you have to unplug and re-plug it.

    I've already tweaked the code so that Espruino can appear as a USB HID device even when it's plugged straight in without a battery, but there are some really strange compilation issues. Sometimes it works fine, and sometimes it only receives the first 2 characters of anything you send it - so right now it's not something I can release!

  • Ok, that would be cool. Though the hick-ups seem to get closer to "on every char" :)

    "While you're at it"(TM), is there a way to have an absolute pointer device, too. Don't know if that's a mode/param of the mouse mode or a separate one.

    Cheers
    Stev

  • :) Looks like it's as easy as googling for it: https://forums.obdev.at/viewtopic.php?t=­2559

    In case the link goes down:

    /* declare a digitzer device generated with HID Descriptor tool */
    /* 4 buttons (reports only 3 for compatibility), absolute pointer , normally generates X/Y valus of max. 15 bits    
     * this represents an absolute pointer device with a resolution of 0.1 mm (2972 dots on a 29,7 cm tablet)
     * to the OS . 
    */
    PROGMEM char usbHidReportDescriptor[60] = {
        0x05, 0x01,                    // USAGE_PAGE (Generic Desktop)
        0x09, 0x02,                    // USAGE (Mouse)
        0xa1, 0x01,                    // COLLECTION (Application)
        0x09, 0x01,                    //   USAGE (Pointer)
        0xa1, 0x00,                    //   COLLECTION (Physical)
        0x05, 0x09,                    //     USAGE_PAGE (Button)
        0x19, 0x01,                    //     USAGE_MINIMUM (Button 1)
        0x29, 0x03,                    //     USAGE_MAXIMUM (Button 3)
        0x15, 0x00,                    //     LOGICAL_MINIMUM (0)
        0x25, 0x01,                    //     LOGICAL_MAXIMUM (1)
        0x95, 0x03,                    //     REPORT_COUNT (3)
        0x75, 0x01,                    //     REPORT_SIZE (1)
        0x81, 0x02,                    //     INPUT (Data,Var,Abs)
        0x95, 0x01,                    //     REPORT_COUNT (1)
        0x75, 0x05,                    //     REPORT_SIZE (5)
        0x81, 0x03,                    //     INPUT (Cnst,Var,Abs)
        0x05, 0x01,                    //     USAGE_PAGE (Generic Desktop)
        0x09, 0x30,                    //     USAGE (X)
        0x09, 0x31,                    //     USAGE (Y)
        0x35, 0x00,                    //     PHYSICAL_MINIMUM (0)
        0x46, 0x9d, 0x0b,              //     PHYSICAL_MAXIMUM (2973)
        0x15, 0x00,                    //     LOGICAL_MINIMUM (0)
        0x26, 0x9d, 0x0b,              //     LOGICAL_MAXIMUM (2973)
        0x65, 0x11,                    //     UNIT (SI Lin:Distance)
        0x55, 0x0e,                    //     UNIT_EXPONENT (-2)
        0x75, 0x10,                    //     REPORT_SIZE (16)
        0x95, 0x02,                    //     REPORT_COUNT (2)
        0x81, 0x02,                    //     INPUT (Data,Var,Abs)
        0xc0,                          //   END_COLLECTION
        0xc0                           // END_COLLECTION
    };
    

    ... but yes, I'll try and remember to put that in as another module.

  • Hoped it would be more work :)

    To make up for that:

    What's b (or first array param) in sendUSBHID() in the mouse example (or the general content of that data stream array for certain HID modes)? Or just: can this be zero for the absolute pointing device as well?

  • Yes, it can be zero - it's just whether buttons are pressed or not. I'll put definitions in the modules, but it's 1=left, 2=right, 4=middle - a bit like dealing with low-level GUI events tends to be :)

    I think at some point the modules will need a bit of tidying - for instance making sure you can actually print a line of text to the Keyboard.

    As a fun aside, you can now do something like this:

    var kb = require("USBKeyboard");
    
    var data = [];
    
    setInterval(function() {
      if (data.length>=40) data = data.slice(-39);
      data.push(E.getTemperature());
    }, 1000);
    
    function outputData() {
      var i = 0;
      function outputLine() {
        if (i>=data.length) return;
        var s = data[i++].toFixed(2)+",\n";
        kb.type(s, outputLine);
      }
      outputLine();
    }
    
    setWatch(outputData, BTN, {debounce:100,repeat:true, edge:"rising"});
    

    I'll post a link to the latest version in a bit

  • Ok, updated firmware is here now

    USB HID documentation and examples at http://www.espruino.com/USB

  • Ah, so good. Especially with the module and tablet. This build still needs external Battery or just the save()?

    Nice link at the end. Lots of tinkering options :)

    As a backchannel, eg for a software on the host, reporting certain state values, can one grab and use the console's serial port on the Espruino side for that so that data sent to the USB modem dev on the host would end up in some serial buffer on the Pico? I remember switching console from back then, but I don't know if that's still possible. I could of course set vars via the console via JS, but it would nice to define limited protocol on the board side.

  • The tablet example keeps logging me out of my Mac :) Probably kills's the UI.
    From a quick search it seems that the values in the HID setup should be signed numbers while the module uses unsigned ones for the physical and logical limits. Those might just not be looked at in the relative mouse example (which works fine :).

    The kb example triggers the context menu. Haven't looked at that module, though, yet.

  • Hm, lost my prepared comment in the editor ;/ Again...

    I changed the report descriptor in the USBTablet module to signed values (0xFF, 0x7f) for the logical maximum and changed the send function accordingly. Which seems to work.

    However I see some offset of my monitors origin (about 2500..2600 logical units) for top and left, resulting in a clipped circle. This only occurs on one of our Macs, on another with less clipping, on another without (just the vertical values are clipped due to the difference in height vs width in pixels). Just to make clear that this might not be the fault of the Pico when it occurs. Might be some kind of touch pad or multi monitor setup that triggers that. Can't say.

    However, the physical minimum/maximum value only seem to be information on the theoretical input device behind it. They don't seem to have any effect (like a movable and scalable value window, as one might expect). At least on the mac without any tablet driver they seem not to be taken into account.

  • This build still needs external Battery or just the save()?

    Just save() this time, but I think you figured that out? :)

    can one grab and use the console's serial port on the Espruino side for that so that data sent to the USB modem dev on the host would end up in some serial buffer on the Pico?

    Yes, you just need to move the console out the way and then you can do what you want:

    LoopbackA.setConsole(); // or you can put it on Serial1 if you want?
    USB.on('data',function(d) { ... });
    

    USBTablet

    Ok, I'll update the module. Very interesting that a plug in device can crash the UI! :o

    As you say, I doubt the clipping/offset is anything Pico-related - the data sent from the Pico obviously doesn't have anything done to it (it works fine on Linux), so I wonder whether your Macs have some kind of 'calibration' that's applied?

    Very strange about the keyboard example and the context menu - I actually tried that on a Mac and it worked great.

    the physical minimum/maximum value ... don't seem to have any effect

    Interesting - so it's just the logical values? To be honest I didn't read it properly and assumed they were X and Y.

  • Just to add - I'm interested how people get on in Windows. I don't know if it's the ST VCP drivers grabbing the device to themselves, but I didn't have any luck with it.

  • Ok, I'll update the module. Very interesting that a plug in device can
    crash the UI! :o

    Yes, indeed. OTH, it could do all kind of weird things. Have to look at the logs to see if someone complained.

    As you say, I doubt the clipping/offset is anything Pico-related - the
    data sent from the Pico obviously doesn't have anything done to it (it
    works fine on Linux), so I wonder whether your Macs have some kind of
    'calibration' that's applied?

    Probably. The irritating thing is, that I have no idea what and where. The other Macs work fine. The "less offset" things was due to a param change. I have just one strange Mac in the group :)

    Very strange about the keyboard example and the context menu - I
    actually tried that on a Mac and it worked great.

    Low sample count issue :)

  • Excellent work. Is it possible to configure this to do "RawHID"?
    It would be neat to be able to write/read raw hid reports. Kind of what teensy example
    https://www.pjrc.com/teensy/rawhid.html

  • Excellent work. Is it possible to configure this to do "RawHID"?
    It would be neat to be able to write/read raw hid reports. Kind of what teensy example

    That would be awesome.

  • Looking pretty good, having mixed results w/ the keyboard libraries on various Windows 8.1 boxes but mouse doesn't appear to work at all on any of the 3 test boxes.

  • Is it possible to configure this to do "RawHID"?

    It should be possible to send 'RawHID' now - I think you can basically just copy the device descriptor from the Teensy's code. Receiving would require a bit more work though - it's possible in the future, but right now I want to get the current HID implementation working well first.

    mouse doesn't appear to work at all on any of the 3 test boxes.

    Are these all Windows? Do you get any errors reported, or any devices shown in the device manager?

  • Hey,

    any progress/plans in hiding the STM serial port or making the composite device look more like known ones?

    -Stev

  • What do you mean about 'looking more like known ones'? If you can get the HID Descriptor of a device you want to emulate, you should just be able to copy it... Or do you mean changing the device name that gets reported?

    I don't think anyone asked about hiding the serial port so far? It wasn't on my list of things to do. Is it causing you problems having it there, or are you looking at putting it inside something and want to hide the fact that it's actually an Espruino?

  • Hm, I had a CDC matching warning under OS X 10.8 which seems to be gone with 10.9, but I wondered if the needed USB drivers for Windows are needed since the matching of the composite device doesn't work and whether this would improve by having more control about what's visible and what's not or how it will present itself.

    Just exploring the different approaches of (USB) driver architectures as I have problems with another (Odroid C1) device that won't be matched correctly.

    Frankly I'm a bit annoyed by the vast amount of information on a problem space that I would rather skip in favor of the application, so I was curious if there's any way cheat a bit on the device side to get there.

  • Post a reply
    • Bold
    • Italics
    • Link
    • Image
    • List
    • Quote
    • code
    • Preview
About

USB HID Support on Pico!

Posted by Avatar for Gordon @Gordon

Actions