Skip to content

Add Keyboard class with API to send HID reports; prune constants; change examples #1

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 8 commits into from
Apr 12, 2017
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
78 changes: 46 additions & 32 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,8 @@ Introduction
:target: https://gitter.im/adafruit/circuitpython?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge
:alt: Gitter

This driver provides USB HID related constants. In the future it will include
helper functions and classes as well.
This driver simulates USB HID devices, such as keyboard, mouse, and joystick.
Right now only keyboard is implemented.

Dependencies
=============
Expand All @@ -26,39 +26,53 @@ This is easily achieved by downloading
Usage Example
=============

The current `keyboard` module stores key constants which make it easier to
construct keypress reports for a keyboard device.
The ``keyboard`` module defines keycode constants and a ``Keyboard`` class to
construct and send keypress reports for a USB keyboard device.

.. code-block:: python

import usb_hid
import adafruit_hid.keyboard as kbd
import time

report = bytearray(8) # Keyboard reports are always 8 bytes.

# Devices are initialized earlier so find the one for the keyboard.
keyboard = None
for device in usb_hid.devices:
if device.usage_page is 0x1 and device.usage is 0x06:
keyboard = device
break

# The first byte of the report includes a bitfield indicating which
# modifiers are pressed. Their bit position is their code's difference from
# 0xE0.
report[0] |= 1 << (kbd.LEFT_SHIFT - 0xE0)
# Normal keys are simply their byte code in bytes 2-7. When fewer than six
# keys are pressed then the trailing bytes are zero.
report[2] = kbd.A
keyboard.send_report(report)

time.sleep(0.1)

# Clear the key presses and send another report.
report[0] = 0
report[2] = 0
keyboard.send_report(report)
from adafruit_hid.keyboard import Keyboard

# Find a keyboard device to talk to.
kbd = Keyboard()

# Simulate typing. Press and release the A key (not shifted),
# then the B key, then c, then the Enter key.
# Type "abc" followed by return.
kbd.type("abc\n")

# Type control-x, then "Abc", then backspace.
kbd.type((Keyboard.CONTROL, 'x'), 'Abc', Keyboard.BACKSPACE)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What do you think about splitting the type method out into its own module? It feels to me like a convenience on top of the press and release mechanics. Splitting it would provide a clear boundary where press and release can use keycode constants and type can use strings.

In general, I like many smaller modules so that people can import just the functionality they want. It also helps with RAM usage during load because each file is smaller individually.

Separately, type can then get fancier for those who need it to type something specific. For example, you could actually handle keyboard layouts like type("Quickly", layout=en_US) and have it press the right underlying keys.

Copy link
Collaborator Author

@dhalbert dhalbert Apr 2, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see the package providing three pieces of functionality:

  1. Press and release keycodes.
  2. Mapping from character codes to keycodes.
  3. Convenience methods for ordinary keyboard typing.

If the user is doing #1 but not #3, s/he will still need to do #2 if the program doesn't know until run-time what it is going to type. I like your idea of possible future alternate layouts. The Arduino library is very US-keyboard-centric.

Originally I wrote the _char_to_keycode() method to return a two-element tuple of (modifier, keycode). I took that out because it allocated storage and I wanted to avoid that. The ASCII_TO_KEYCODE table is only 128 bytes because of the bit-encoding for shift, and it can live in flash, but it only works for English.

But the general case is a table of tuples, or similar. A Unicode character may require multiple keypresses and releases with modifiers. For instance, on a Mac, to type è, you can type Option + ` then e. That might be represented as ((OPTION, GRAVE_ACCENT), E) or even ((OPTION, GRAVE_ACCENT), (E,))where each top-level element is a combination to press and release. Or if you used dead keys (as on Windows, I think), it could be (GRAVE_ACCENT, E).

So maybe I should give up on the idea of not generating garbage and not using RAM for the lookup tables. There's an efficiency/generality tradeoff here that's difficult given the tight storage constraints. I would love to have constant tuples and dicts in flash, but that may be a long way off.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think its fine to use RAM for the lookup tables. I just would make sure and only load the lookup table(s) the user's code needs.

Its good to think about efficiency but we shouldn't be hamstrung by it. MicroPython and CircuitPython's strength is ease of use and fast iteration not speed. I think its good to figure about good APIs first and worry about speed second. There are a lot of tricks we can do with a little effort to provide the same API but make it faster or smaller. For example, we could provide a frozendict C implementation that stores the dictionary with a continuous block rather than a bunch of small chunks.

Perhaps each locale should provide a function to do the mapping and then internally decide the most efficient way to do the conversion.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That sounds like the right idea. I'll try to come up with a uniform API.


# Press and hold left-hand Control and right-hand Alt.
kbd.press_keys(Keyboard.CONTROL, Keyboard.ALT)

# Press and hold the A and B keys (lower case, not shifted).
kbd.press_keys('ab')

# Press and hold the A and B keys (same effect as above).
kbd.press_keys('a', 'b')
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Having two ways to do this seems weird to me. I'd rather have them take keycode constants consistently to make the API uniform and reinforce the idea that they are just codes that may result in any letter. type can be used for higher level "type this on my computer".

I also think you can drop the _keys from the method name because its implicit on the class.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I can remove that generality of multiple strings. It is confusing to have too many ways to do something. Also I'll drop the _keys -- you're right.


# Press capital C. This implies pressing left Shift as well,
# because the character is capitalized.
kbd.press_keys('C')

# Release the B key.
kbd.release_keys('b')

# Release all keys.
kbd.release_all()

# Press '5' on the keypad and the F8 key.
kbd.press_keys(Keyboard.KEYPAD_FIVE, Keyboard.F8)

# Press the shifted '1' key to get '!' (exclamation mark).
kbd.press_keys(Keyboard.SHIFT, '1')

# Same effect as above. The '!' implies pressing Shift and '1'.
kbd.press_keys('!').



Contributing
============
Expand Down
Loading