Ability to use media keys with Pico over USB HID

Posted on
  • Hi all!

    I've just been following the docs to try out the USB HID keyboard module, it's great!
    I was wondering if this module is able to send mediakey commands (vol up/down, play, pause)

    My use case is, that I've created a IR Reciever Hat for my Pico which is plugged in to my computer via a USB port that has IR line-of-sight to my remote control, on my desk

    I have used the IR communication Espruino examples to determine which button was pressed on my IR remote control, works perfectly!

    Now, I want to be able to send volume/media commands to my PC by simply using the remote control.

  • Looks like I'd need to register the Pico as a Consumer Device (but not too sure what's involved with that)

    Compromise for now is to use a AutoHotkey script to control multimedia functions:

    [#NoEnv](https://forum.espruino.com/search/?q=%23NoEnv)  ; Recommended for performance and compatibility with future AutoHotkey releases.
    ; [#Warn](https://forum.espruino.com/search/?q=%23Warn)  ; Enable warnings to assist with detecting common errors.
    SendMode Input  ; Recommended for new scripts due to its superior speed and reliability.
    SetWorkingDir %A_ScriptDir%  ; Ensures a consistent starting directory.
    
    ; AutoHotkey Media Keys
    !M::Send   {Media_Play_Pause}
    ^!Left::Send        {Media_Prev}
    ^!Right::Send       {Media_Next}
    ^!NumpadMult::Send  {Volume_Mute}
    ^!NumpadAdd::Send   {Volume_Up}
    ^!NumpadSub::Send   {Volume_Down}  
    

    Along with my Pico script:

    let kb = require("USBKeyboard");
    let debug = 0;
    let code = 0;
    let timeout;
    let lastTime;
    // function to do something with the code when we get it
    function handleCode() {
      timeout = undefined;
      if (debug) print(code);
      LED2.write(true);
      switch(code) {
        case 16712445: {
          console.log('Volume +');
          kb.setModifiers(kb.MODIFY.CTRL, function() {
            kb.tap(kb.KEY.F12);
          });
          break;
        }
        case 16750695: {
          console.log('Volume -');
          kb.setModifiers(kb.MODIFY.CTRL, function() {
            kb.tap(kb.KEY.F11);
          });
          break;
        }
        case 16754775: {
          console.log('Play/Pause');
          kb.setModifiers(kb.MODIFY.ALT, function() {
            kb.tap(kb.KEY.M);
          });
          break;
        }
        case 16720605: {
          console.log('Mute');
          kb.setModifiers(kb.MODIFY.CTRL, function() {
            kb.tap(kb.KEY.F10);
          });
          break;
        }
        case 16748655: {
          console.log('Skip');
          kb.setModifiers(kb.MODIFY.SHIFT, function() {
            kb.tap(kb.KEY.N);
          });
          break;
        }
        case 16738455: {
          console.log('Prev desktop');
          kb.setModifiers(kb.MODIFY.SHIFT, function() {
            kb.setModifiers(kb.MODIFY.GUI, function() {
              kb.tap(kb.KEY.LEFT);
            });
          });
          break;
        }
        case 16756815: {
          console.log('Next desktop');
          kb.setModifiers(kb.MODIFY.GUI, function() {
            kb.setModifiers(kb.MODIFY.SHIFT, function() {
              kb.tap(kb.KEY.RIGHT);
            });
          });
          break;
        }
        case 16753245: {
          console.log('Lock');
          kb.setModifiers(kb.MODIFY.GUI, function() {
            kb.tap(kb.KEY.L);
          });
          break;
        }
        default: {
          //console.log('default');
        }
        kb.setModifiers(0, function() {});
      }
      code = 0;
      LED2.write(false);
    }
    
    function onPulseOn(e) {
      code = (code*2) | ((e.time - lastTime) > 0.0008);
      if (timeout!==undefined) clearTimeout(timeout);
      timeout = setTimeout(handleCode, 20);
      lastTime = e.time;
    }
    
    function onPulseOff(e) {
      lastTime = e.time;
    }
    
    setWatch(onPulseOff, A5, { repeat:true, edge:"rising" });
    setWatch(onPulseOn, A5, { repeat:true, edge:"falling" });
    
    setWatch(function(e) {
      debug = !debug;
      console.log(`Debug mode ${debug ? 'on' : 'off' }`);
    }, BTN, { repeat: true });
    
    
  • Hi! While we don't have a library for this via USB, there is one for Bluetooth HID (http://www.espruino.com/BLE+Keyboard). I haven't tried this out, but just mashing the two bits of code together may work:

    E.setUSBHID({
      reportDescriptor : [
      0x05, 0x0c,                    // USAGE_PAGE (Consumer Devices)
      0x09, 0x01,                    // USAGE (Consumer Control)
      0xa1, 0x01,                    // COLLECTION (Application)
                                     // -------------------- common global items
      0x15, 0x00,                    //   LOGICAL_MINIMUM (0)
      0x25, 0x01,                    //   LOGICAL_MAXIMUM (1)
      0x75, 0x01,                    //   REPORT_SIZE (1)    - each field occupies 1 bit
                                     // -------------------- misc bits
      0x95, 0x05,                    //   REPORT_COUNT (5)
      0x09, 0xb5,                    //   USAGE (Scan Next Track)
      0x09, 0xb6,                    //   USAGE (Scan Previous Track)
      0x09, 0xb7,                    //   USAGE (Stop)
      0x09, 0xcd,                    //   USAGE (Play/Pause)
      0x09, 0xe2,                    //   USAGE (Mute)
      0x81, 0x06,                    //   INPUT (Data,Var,Rel)  - relative inputs
                                     // -------------------- volume up/down bits
      0x95, 0x02,                    //   REPORT_COUNT (2)
      0x09, 0xe9,                    //   USAGE (Volume Up)
      0x09, 0xea,                    //   USAGE (Volume Down)
      0x81, 0x02,                    //   INPUT (Data,Var,Abs)  - absolute inputs
                                     // -------------------- padding bit
      0x95, 0x01,                    //   REPORT_COUNT (1)
      0x81, 0x01,                    //   INPUT (Cnst,Ary,Abs)
      0xc0                           // END_COLLECTION
      ]
    });
    
    function p(c,cb) { E.sendUSBHID([c]);cb(); }
    next = function(cb) { p(0x1,cb) };
    prev = function(cb) { p(0x2,cb) };
    stop = function(cb) { p(0x4,cb) };
    playpause = function(cb) { p(0x8,cb) };
    mute = function(cb) { p(0x10,cb) };
    volumeUp = function(cb) { p(0x20,cb) };
    volumeDown = function(cb) { p(0x40,cb) };
    
  • Holy smokes, just tried swapping this in (just playpause for now) and it works!

    Will try replacing the rest later - see what happens and report back.

    Many thanks for the suggestion, Gordon!

  • Here's the latest incarnation! Working pretty well.

    Sometimes the mute and playpause commands don't work the 2nd time you press them, but with subsequent presses work fine

    let debug = 0;
    let code = 0;
    let timeout;
    let lastTime;
    
    const VOLUME_UP    = 16712445;
    const VOLUME_DOWN  = 16750695;
    const PLAY_PAUSE   = 16754775;
    const MUTE         = 16720605;
    const NEXT         = 16748655;
    const PREV         = 16769055;
    const STOP         = 16756815;
    
    E.setUSBHID({
      reportDescriptor : [
      0x05, 0x0c,                    // USAGE_PAGE (Consumer Devices)
      0x09, 0x01,                    // USAGE (Consumer Control)
      0xa1, 0x01,                    // COLLECTION (Application)
                                     // -------------------- common global items
      0x15, 0x00,                    //   LOGICAL_MINIMUM (0)
      0x25, 0x01,                    //   LOGICAL_MAXIMUM (1)
      0x75, 0x01,                    //   REPORT_SIZE (1)    - each field occupies 1 bit
                                     // -------------------- misc bits
      0x95, 0x05,                    //   REPORT_COUNT (5)
      0x09, 0xb5,                    //   USAGE (Scan Next Track)
      0x09, 0xb6,                    //   USAGE (Scan Previous Track)
      0x09, 0xb7,                    //   USAGE (Stop)
      0x09, 0xcd,                    //   USAGE (Play/Pause)
      0x09, 0xe2,                    //   USAGE (Mute)
      0x81, 0x06,                    //   INPUT (Data,Var,Rel)  - relative inputs
                                     // -------------------- volume up/down bits
      0x95, 0x02,                    //   REPORT_COUNT (2)
      0x09, 0xe9,                    //   USAGE (Volume Up)
      0x09, 0xea,                    //   USAGE (Volume Down)
      0x81, 0x02,                    //   INPUT (Data,Var,Abs)  - absolute inputs
                                     // -------------------- padding bit
      0x95, 0x01,                    //   REPORT_COUNT (1)
      0x81, 0x01,                    //   INPUT (Cnst,Ary,Abs)
      0xc0                           // END_COLLECTION
      ]
    });
    
    function p(c, cb) {
      E.sendUSBHID([c]);
      return cb();
    }
    
    next = function (cb) {
      p(0x1, cb);
      console.log('next');
    };
    prev = function (cb) {
      p(0x2, cb);
      console.log('prev');
    };
    stop = function (cb) {
      p(0x4, cb);
      console.log('stop');
    };
    playpause = function (cb) {
      p(0x8, cb);
      console.log('playpause');
    };
    mute = function (cb) {
      p(0x10, cb);
      console.log('mute');
    };
    volumeUp = function (cb) {
      p(0x20, cb);
      console.log('volumeUp');
    };
    volumeDown = function (cb) {
      p(0x40, cb);
      console.log('volumeDown');
    };
    
    const cb = () => {};
    
    function handleCode() {
      timeout = undefined;
      if (debug) print(code);
      LED2.write(true);
    
      if (code === PLAY_PAUSE)  playpause(cb);
      if (code === VOLUME_UP)   volumeUp(cb);
      if (code === VOLUME_DOWN) volumeDown(cb);
      if (code === MUTE)        mute(cb);
      if (code === NEXT)        next(cb);
      if (code === PREV)        prev(cb);
      if (code === STOP)        stop(cb);
    
      code = 0;
      LED2.write(false);
    }
    
    // When an IR pulse is detected
    function onPulseOn(e) {
      code = (code * 2) | (e.time - lastTime > 0.0008);
      if (timeout !== undefined) clearTimeout(timeout);
      timeout = setTimeout(handleCode, 20);
      lastTime = e.time;
    }
    
    // When IR pulse stops
    function onPulseOff(e) {
      lastTime = e.time;
    }
    
    setWatch(onPulseOff, A5, { repeat: true, edge: "rising" });
    setWatch(onPulseOn, A5, { repeat: true, edge: "falling" });
    
    setWatch(
      // Press the button on the pico to enable debug mode and
      // see the IR code for your own remote
      function (e) {
        debug = !debug;
        console.log(`Debug mode ${debug ? "on" : "off"}`);
      },
      BTN,
      { repeat: true }
    );
    
    
  • Interestingly when hitting vol up/down, the command gets sent repeatedly until you press it again too

  • Hmm - when pressing keys normally on the keyboard, you send one HID report for keydown, and another to show the key has been lifted. It didn't seem to be the case on the BLE version but maybe you need to do that here?

    function p(c, cb) {
      E.sendUSBHID([c]);
      setTimeout(function() {
        E.sendUSBHID([0]);
        cb();
      },100);
    }
    

    Thanks for posting up your code though - that's great! Maybe I should turn this into a module?

  • Hmm - when pressing keys normally on the keyboard, you send one HID report for keydown, and another to show the key has been lifted. It didn't seem to be the case on the BLE version but maybe you need to do that here?

    Yep that was exactly it!

    Thanks for the snippet Gordon, simply dropped it in and now it's working brilliantly.

    I think a module for this would have definitely helped me in the first instance.

  • Final working code:

    let debug = 0;
    let code = 0;
    let timeout;
    let lastTime;
    
    const VOLUME_UP    = 16712445;
    const VOLUME_DOWN  = 16750695;
    const PLAY_PAUSE   = 16754775;
    const MUTE         = 16720605;
    const NEXT         = 16748655;
    const PREV         = 16769055;
    const STOP         = 16756815;
    
    E.setUSBHID({
      reportDescriptor : [
      0x05, 0x0c,  // USAGE_PAGE (Consumer Devices)
      0x09, 0x01,  // USAGE (Consumer Control)
      0xa1, 0x01,  // COLLECTION (Application)
                   // -------------------- common global items
      0x15, 0x00,  //   LOGICAL_MINIMUM (0)
      0x25, 0x01,  //   LOGICAL_MAXIMUM (1)
      0x75, 0x01,  //   REPORT_SIZE (1)    - each field occupies 1 bit
                   // -------------------- misc bits
      0x95, 0x05,  //   REPORT_COUNT (5)
      0x09, 0xb5,  //   USAGE (Scan Next Track)
      0x09, 0xb6,  //   USAGE (Scan Previous Track)
      0x09, 0xb7,  //   USAGE (Stop)
      0x09, 0xcd,  //   USAGE (Play/Pause)
      0x09, 0xe2,  //   USAGE (Mute)
      0x81, 0x06,  //   INPUT (Data,Var,Rel)  - relative inputs
                   // -------------------- volume up/down bits
      0x95, 0x02,  //   REPORT_COUNT (2)
      0x09, 0xe9,  //   USAGE (Volume Up)
      0x09, 0xea,  //   USAGE (Volume Down)
      0x81, 0x02,  //   INPUT (Data,Var,Abs)  - absolute inputs
                   // -------------------- padding bit
      0x95, 0x01,  //   REPORT_COUNT (1)
      0x81, 0x01,  //   INPUT (Cnst,Ary,Abs)
      0xc0         // END_COLLECTION
      ]
    });
    
    function p(c, cb) {
      E.sendUSBHID([c]);
      setTimeout(function() {
        E.sendUSBHID([0]);
        cb();
      },100);
    }
    
    next = function (cb) {
      p(0x1, cb);
      console.log('next');
    };
    prev = function (cb) {
      p(0x2, cb);
      console.log('prev');
    };
    stop = function (cb) {
      p(0x4, cb);
      console.log('stop');
    };
    playpause = function (cb) {
      p(0x8, cb);
      console.log('playpause');
    };
    mute = function (cb) {
      p(0x10, cb);
      console.log('mute');
    };
    volumeUp = function (cb) {
      p(0x20, cb);
      console.log('volumeUp');
    };
    volumeDown = function (cb) {
      p(0x40, cb);
      console.log('volumeDown');
    };
    
    const cb = () => {};
    
    function handleCode() {
      timeout = undefined;
      if (debug) print(code);
      LED2.write(true);
    
      if (code === PLAY_PAUSE)  playpause(cb);
      if (code === VOLUME_UP)   volumeUp(cb);
      if (code === VOLUME_DOWN) volumeDown(cb);
      if (code === MUTE)        mute(cb);
      if (code === NEXT)        next(cb);
      if (code === PREV)        prev(cb);
      if (code === STOP)        stop(cb);
    
      code = 0;
      LED2.write(false);
    }
    
    // When an IR pulse is detected
    function onPulseOn(e) {
      code = (code * 2) | (e.time - lastTime > 0.0008);
      if (timeout !== undefined) clearTimeout(timeout);
      timeout = setTimeout(handleCode, 20);
      lastTime = e.time;
    }
    
    // When IR pulse stops
    function onPulseOff(e) {
      lastTime = e.time;
    }
    
    setWatch(onPulseOff, A5, { repeat: true, edge: "rising" });
    setWatch(onPulseOn, A5, { repeat: true, edge: "falling" });
    
    setWatch(
      // Press the button on the pico to enable debug mode and
      // see the IR code for your own remote
      function (e) {
        debug = !debug;
        console.log(`Debug mode ${debug ? "on" : "off"}`);
      },
      BTN,
      { repeat: true }
    );
    
    
  • So happy I dug my Pico out to do this! Love it, thanks for all the help.

    It's so useful. Also have the Adafruit Rotary Trinkey programmed to control media which inspired me to try this (having completed your IR tutorial about 1y ago successfully!)

  • Great - thanks! Glad it's working so well! I'll try and get this added as a module :)

    edit: just added a module and it should go live this week

  • Oh, that's awesome!

    Is this the one? Or am I getting ahead of myself? 😛


    edit: I did get ahead of myself - found it https://github.com/espruino/EspruinoDocs/blob/master/modules/USBMedia.js

  • Thanks for the change in the docs! I think that was you?

  • Yes indeed it was 🙂 no problem Gordon, thanks for the module!

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

Ability to use media keys with Pico over USB HID

Posted by Avatar for Stoaty @Stoaty

Actions