not logged in | [Login]
I ordered a pyboard because I couldn't resist trying out Python on a microcontroller, but also because I had a specific use case in mind, involving its USB HID capabilities.
However I was suprised to find that there was nothing in the documentation about the HID Keyboard mode, only an example for the mouse mode.
I didn't know where to start, and didn't really understand the only code snippet I found in the forums. So I opened my own thread in the forum.
Fortunately I got a lot of useful hints from helpful members there, and after a while was able to achieve my original goal (many thanks to dhylands, pythoncoder & deshipu !).
So I'm attempting to help others in turn, by documenting the steps I followed to get USB HID keyboard mode working, and adapt it to my use case.
For now I'm putting this in the Wiki, as I don't really know how Git works ; if anyone feels like moving this inside the official documentation (alongside the HID mouse example maybe), feel free :-)
I wanted to turn the pyboard into an USB dongle to hold and emit some of the lenghty logins and passwords I have to use daily at work.
I had already done this with an Arduino, similar to this.
I didn't know anything about Arduinos, and very little of C, but it was actually very easy thanks to the tutorial, the Arduino IDE and all the ready-made libraries inside.
It worked well, although I didn't have to understand any of it, but I wanted something easier to manage (like changing passwords), and that also felt like I'd done it myself...
Python notwithstanding, this was actually harder because I had to start from scratch without any directions :-)
The code examples below will show the various blocks that are needed, and how they work together.
This is well documented, but worth mentioning:
The boot.py
file needs to be modified like this:
#boot.py import machine import pyb #pyb.main('main.py') # main script to run after this one #pyb.usb_mode('CDC+MSC') # act as a serial and a storage device pyb.usb_mode('CDC+HID',hid=pyb.hid_keyboard)
Note: In this mode there is no USB storage.
To access the onboard files for editing, you need to press the Reset button while pressing down the User button.
The LEDs start cycling, when the orange LED is lit alone, release the User button.
This is the trickiest, low-level part, which needs digging into the USB reference docs.
The device communicates with the host using 8-byte arrays of hex codes, called "reports".
All bytes are initialized to 0x00.
Byte 0 is for a modifier key, or combination thereof. It is used as a bitmap, each bit mapped to a modifier:
Examples: 0x02 for Shift, 0x05 for Control+Alt
Byte 1 is "reserved" (unused, actually)
Bytes 2-7 are for the actual key scancode(s) - up to 6 at a time ("chording").
In most usual cases, "press key" reports will be sent using just bytes 0 and 2.
To "release" the key(s), send another report with all bytes at 0x00.
Here is the minimal code in main.py
that will print 'T' to your host screen when you reset the pyboard :
(the focused window on your host needs to be writable :-)
import pyb kb = pyb.USB_HID() buf = bytearray(8) # Sending T # Do the key down buf[0] = 0x02 # LEFT_SHIFT buf[2] = 0x17 # keycode for 't/T' kb.send(buf) pyb.delay(5) # Do the key up buf[0] = 0x00 buf[2] = 0x00 kb.send(buf)(of course this will become a function later)
Seeing the above working for the first time is pure joy, but it quickly brings questions:
The first question is answered by another USB reference document (chapter 10, page 53).
This gives the hex scan code for each key on a standard US QWERTY keyboard.
What we need then is a reverse mapping of literal characters to their scan codes and modifier keys (only the ones we'll actually use, as it's quite tedious).
I chose to do this as a dict with chars as keys and scancode/modifier tuples as values:
PLAIN = 0x00 SHIFT = 0x02 kbmap = dict() # part of second row example kbmap['t'] = (0x17,PLAIN) kbmap['T'] = (0x17,SHIFT) kbmap['y'] = (0x1c,PLAIN) kbmap['Y'] = (0x1c,SHIFT) kbmap['u'] = (0x18,PLAIN) kbmap['U'] = (0x18,SHIFT) kbmap['i'] = (0x0c,PLAIN) kbmap['I'] = (0x0c,SHIFT) kbmap['o'] = (0x12,PLAIN) kbmap['O'] = (0x12,SHIFT) kbmap['p'] = (0x13,PLAIN) kbmap['P'] = (0x13,SHIFT) # ...and so on
I put this in a separate kb_map.py
module as it can grow large, and import it from main.py
.
From there on we can have a function that sends literal chars:
import pyb from kb_map import kbmap kb = pyb.USB_HID() buf = bytearray(8) def sendchr(char) : if not char in kbmap.keys() : print("Unknow char") ; return # key down buf[2], buf[0] = kbmap[char] kb.send(buf) pyb.delay(5) # key up buf[2], buf[0] = 0x00, 0x00 kb.send(buf) pyb.delay(5) send('t') send('T')
Note: the USB reference scancodes are for physical positions of keys on a QWERTY-US keyboard.
The actual character(s) sent for a given key will vary wildly between local non-QWERTY-US layouts.
Thus each variant will need its own kb_map.py mapping, which is why I don't include a full version.
Once we have all of the above it becomes trivial to send a string:
def sendstr(str) : for c in str : sendchr(c)
For easier testing we can use the pyboard as a one-trick pony with the single User button by adding this code:
led = pyb.LED(1) sw = pyb.Switch() mypw = "My P4ssw0rd" while 1 : if sw() : led.toggle() kb.sendstr(mypw) # Wait for switch to be released while sw(): pyb.delay(2) pass led.toggle() pyb.delay(1)It's easy to adapt this to use a list of strings, and cycle through it with each button press.
This is all well and good, but what we need is a device that will store and output several logins et passwords, which means several buttons.
Also, a button should be able to output several strings in sequence, such as "login, Tab, password, Enter".
Again I put these strings in a separate store.py
module, as a list of tuples :
# store.py pwds = [ ("C.A.D", "login1", "\t", "pwd1", "\n"), ("pwd1", "\n"), ("my.email@ddress.com", "\t"), ("pwd2", "\n"), ("pwd3", "\n"), ("pwd4", "\n"), ("abbreviation1", "\n"), ("abbreviation2",) ]
The first line is a special case, as it sends the "C.A.D" string, which is a moniker for Control-Alt-Del, to automate a Windows login sequence.
In the code below you will see that there is a separate sendCAD() function to send that code, which is used instead of sendstr() when the string matches the moniker (I could have used a special char in the kbmap table, but I find it clearer this way).
Of course you can send any string you want, not just logins and passwords.
Note the trailing comma in the last tuple, which holds only one string.
I used an 8-button (2x4) keypad that fits nicely on the back of the pyboard (actually, everything fits nicely inside the black plastic case it was shipped in :-).
It needs 8 pins plus Ground, and a simple-minded debouncing class I found at McHobby's (there are much more elaborate ones, such as pythoncoder's, but it works OK).
With the pyboard, editing the strings in store.py
, as logins/passwords change, becomes a breeze.
You just put it back in USB storage mode (as described in section 1), then access store.py
from any host, and change it with any available editor (yes, even Notepad if you have no choice :-).
Then eject the device and press the Reset button to return to HID Keyboard mode.
Below is a working version of the above bits, put together.
Obviously the buttons and strings lists should be adapted to the actual number of buttons and pins used.
You must supply your own kb_map.py
and store.py
.
Questions/comments/suggestions should go to the thread in the forum. Enjoy!
# main.py import pyb from kb_map import kbmap from store import pwds kb = pyb.USB_HID() buf = bytearray(8) def sendchr(char) : if not char in kbmap.keys() : # print warning to REPL if active print("\nUnknow char\n") ; return # key down with modifiers buf[2], buf[0] = kbfr[char] kb.send(buf) pyb.delay(10) # key up buf[2], buf[0] = 0x00, 0x00 kb.send(buf) pyb.delay(10) def sendCAD() : # send Control-Alt-DEL sendchr((0x4c,0x05)) # DEL with Alt+Ctl pyb.delay(500) def sendstr(str) : for c in str : sendchr(c) class PullUpButton: pin = None # Pin object state = None # Last known state def __init__( self, button_pin ): self.pin = pyb.Pin( button_pin, pyb.Pin.IN, pull=pyb.Pin.PULL_UP ) self.state = self.pin.value() def is_pressed(self): val = self.pin.value() result = False if val != self.state: pyb.delay( 10 ) # retry in 10 ms (debouncing) val2 = self.pin.value() if val == val2: # value is stable :) self.state = val result = (val == 0) # Is pressed return result btns = [ PullUpButton( pyb.Pin.board.Y12 ), PullUpButton( pyb.Pin.board.Y11 ), PullUpButton( pyb.Pin.board.Y10 ), PullUpButton( pyb.Pin.board.Y9 ), PullUpButton( pyb.Pin.board.X8 ), PullUpButton( pyb.Pin.board.X7 ), PullUpButton( pyb.Pin.board.X6 ), PullUpButton( pyb.Pin.board.X5 ) ] while True: pyb.delay( 2 ) for i in range(8) : if btns[i].is_pressed(): print('button pressed : ', i+1) for s in pwds[i] : print( s ) if s == "C.A.D" : sendCAD() else : sendstr(s) ; pyb.delay( 20 )
Oh, and don't worry about the pyb.delay(n)
calls all over the place: that's just me being paranoid :-)
(The only one that's definitely needed is in PullUpButton
)
Last edited by fpp, 2017-06-16 17:31:36