はじめに
この度、マウス/キーボードの操作にTinyUSB Mouse and Keyboard libraryを使うマイコンに出会った。今回もJPキーボードに対応したのでここにまとめておく。
1 JPキーボード対応
USBプロトコルスタックをサポートするAdafruit TinyUSB Library for Arduinoは、HIDキーコード(Usage IDが本来の呼称)を扱うため、キーボードの種類に依存しない。USキーボードとかJPキーボードのキーコードをHIDキーコードに変換しているのは、APIライブラリであるTinyUSB Mouse and Keyboard libraryだ。
1.1 HIDキーコード変換
さっそくコードを眺めてみる。
size_t TinyKeyboard_::press(uint8_t k)
{
uint8_t i;
if (k >= 136) { // it's a non-printing key (not a modifier)
k = k - 136;
} else if (k >= 128) { // it's a modifier key
_keyReport.modifiers |= (1<<(k-128));
k = 0;
} else { // it's a printing key
k = pgm_read_byte(_asciimap + k);
if (!k) {
setWriteError();
return 0;
}
if (k & 0x80) { // it's a capital letter or other character reached with shift
_keyReport.modifiers |= 0x02; // the left shift modifier
k &= 0x7F;
}
}
// Add k to the key report only if it's not already present
// and if there is an empty slot.
if (_keyReport.keys[0] != k && _keyReport.keys[1] != k &&
_keyReport.keys[2] != k && _keyReport.keys[3] != k &&
_keyReport.keys[4] != k && _keyReport.keys[5] != k) {
for (i=0; i<6; i++) {
if (_keyReport.keys[i] == 0x00) {
_keyReport.keys[i] = k;
break;
}
}
if (i == 6) {
setWriteError();
return 0;
}
}
sendReport(&_keyReport);
return 1;
}
最初のif文で引数kのコードを3つ振り分けている。コメントにもあるように次のような振り分けである。
# | 条件 | 意味 | キーの例 |
---|---|---|---|
1 | 136以上のコード (k ≥ 0x88) |
非印字文字 | 矢印↑キーやF1キーなど |
2 | 128以上のコード (k ≥ 0x80) |
修飾キー | Shift,CtrlやAltキーなど |
3 | それ以外のコード | 印字文字 | ASCII文字(A,b,&など) |
上表の#1,2のコードがTinyUSB_Mouse_and_Keyboard.h
に定義されている。
// Keyboard codes
// Note these are different in some respects to the TinyUSB codes but
// are compatible with Arduino Keyboard.h API
# define KEY_LEFT_CTRL 0x80
# define KEY_LEFT_SHIFT 0x81
# define KEY_LEFT_ALT 0x82
# define KEY_LEFT_GUI 0x83
# define KEY_RIGHT_CTRL 0x84
# define KEY_RIGHT_SHIFT 0x85
# define KEY_RIGHT_ALT 0x86
# define KEY_RIGHT_GUI 0x87
# define KEY_UP_ARROW 0xDA
# define KEY_DOWN_ARROW 0xD9
# define KEY_LEFT_ARROW 0xD8
# define KEY_RIGHT_ARROW 0xD7
# define KEY_BACKSPACE 0xB2
# define KEY_TAB 0xB3
# define KEY_RETURN 0xB0
# define KEY_ESC 0xB1
# define KEY_INSERT 0xD1
# define KEY_DELETE 0xD4
# define KEY_PAGE_UP 0xD3
# define KEY_PAGE_DOWN 0xD6
# define KEY_HOME 0xD2
# define KEY_END 0xD5
# define KEY_CAPS_LOCK 0xC1
# define KEY_F1 0xC2
# define KEY_F2 0xC3
# define KEY_F3 0xC4
# define KEY_F4 0xC5
# define KEY_F5 0xC6
# define KEY_F6 0xC7
# define KEY_F7 0xC8
# define KEY_F8 0xC9
# define KEY_F9 0xCA
# define KEY_F10 0xCB
# define KEY_F11 0xCC
# define KEY_F12 0xCD
# define KEY_F13 0xF0
# define KEY_F14 0xF1
# define KEY_F15 0xF2
# define KEY_F16 0xF3
# define KEY_F17 0xF4
# define KEY_F18 0xF5
# define KEY_F19 0xF6
# define KEY_F20 0xF7
# define KEY_F21 0xF8
# define KEY_F22 0xF9
# define KEY_F23 0xFA
# define KEY_F24 0xFB
キーボードコードとして、コメントにある通り「HIDキーコードと異なるがArduinoキーボードAPIと互換がある」コードが定義されている。例えば上記のKEY_F1
は0xC2
で定義されているがHIDキーコードは0x3A
、この値の差が確かに136である(0xC2ー0x3A=0x88)。136を引いてHIDキーコードに変換するということだ。
0x80〜0x87はキーボードの左右にあるCtrl/Shift/Alt/⌘キーに対応しており、HIDレポートディスクリプタのmodifierビットをセットしている。
残るコード値128未満の印字可能文字(ASCII文字)のHIDキーコードへの変換は、変換テーブルを介して行っている。_asciimap
変換テーブルは TinyUSB_Mouse_and_Keyboard.cpp
に定義されている。
#define SHIFT 0x80
const uint8_t _asciimap[128] =
{
0x00, // NUL
0x00, // SOH
0x00, // STX
0x00, // ETX
0x00, // EOT
0x00, // ENQ
0x00, // ACK
0x00, // BEL
0x2a, // BS Backspace
0x2b, // TAB Tab
0x28, // LF Enter
0x00, // VT
0x00, // FF
0x00, // CR
0x00, // SO
0x00, // SI
0x00, // DEL
0x00, // DC1
0x00, // DC2
0x00, // DC3
0x00, // DC4
0x00, // NAK
0x00, // SYN
0x00, // ETB
0x00, // CAN
0x00, // EM
0x00, // SUB
0x00, // ESC
0x00, // FS
0x00, // GS
0x00, // RS
0x00, // US
0x2c, // ' '
0x1e|SHIFT, // !
0x34|SHIFT, // "
0x20|SHIFT, // #
0x21|SHIFT, // $
0x22|SHIFT, // %
0x24|SHIFT, // &
0x34, // '
0x26|SHIFT, // (
0x27|SHIFT, // )
0x25|SHIFT, // *
0x2e|SHIFT, // +
0x36, // ,
0x2d, // -
0x37, // .
0x38, // /
0x27, // 0
0x1e, // 1
0x1f, // 2
0x20, // 3
0x21, // 4
0x22, // 5
0x23, // 6
0x24, // 7
0x25, // 8
0x26, // 9
0x33|SHIFT, // :
0x33, // ;
0x36|SHIFT, // <
0x2e, // =
0x37|SHIFT, // >
0x38|SHIFT, // ?
0x1f|SHIFT, // @
0x04|SHIFT, // A
0x05|SHIFT, // B
0x06|SHIFT, // C
0x07|SHIFT, // D
0x08|SHIFT, // E
0x09|SHIFT, // F
0x0a|SHIFT, // G
0x0b|SHIFT, // H
0x0c|SHIFT, // I
0x0d|SHIFT, // J
0x0e|SHIFT, // K
0x0f|SHIFT, // L
0x10|SHIFT, // M
0x11|SHIFT, // N
0x12|SHIFT, // O
0x13|SHIFT, // P
0x14|SHIFT, // Q
0x15|SHIFT, // R
0x16|SHIFT, // S
0x17|SHIFT, // T
0x18|SHIFT, // U
0x19|SHIFT, // V
0x1a|SHIFT, // W
0x1b|SHIFT, // X
0x1c|SHIFT, // Y
0x1d|SHIFT, // Z
0x2f, // [
0x31, // bslash
0x30, // ]
0x23|SHIFT, // ^
0x2d|SHIFT, // _
0x35, // `
0x04, // a
0x05, // b
0x06, // c
0x07, // d
0x08, // e
0x09, // f
0x0a, // g
0x0b, // h
0x0c, // i
0x0d, // j
0x0e, // k
0x0f, // l
0x10, // m
0x11, // n
0x12, // o
0x13, // p
0x14, // q
0x15, // r
0x16, // s
0x17, // t
0x18, // u
0x19, // v
0x1a, // w
0x1b, // x
0x1c, // y
0x1d, // z
0x2f|SHIFT, // {
0x31|SHIFT, // |
0x30|SHIFT, // }
0x35|SHIFT, // ~
0 // DEL
};
記号文字の多くがUSキーボードとJPキーボードでキー位置の割り当てが異なるので、下記のように、ここの定義をJPキーボードに合わせて変更すれば良いはずである。
# define SHIFT 0x80
const uint8_t _asciimap[128] =
{
0x00, // NUL
0x00, // SOH
0x00, // STX
0x00, // ETX
0x00, // EOT
0x00, // ENQ
0x00, // ACK
0x00, // BEL
0x2a, // BS Backspace
0x2b, // TAB Tab
0x28, // LF Enter
0x00, // VT
0x00, // FF
0x00, // CR
0x00, // SO
0x00, // SI
0x00, // DEL
0x00, // DC1
0x00, // DC2
0x00, // DC3
0x00, // DC4
0x00, // NAK
0x00, // SYN
0x00, // ETB
0x00, // CAN
0x00, // EM
0x00, // SUB
0x00, // ESC
0x00, // FS
0x00, // GS
0x00, // RS
0x00, // US
0x2c, // ' '
0x1e|SHIFT, // !
- 0x34|SHIFT, // "
+ 0x1f|SHIFT, // " 34 0x22
0x20|SHIFT, // #
0x21|SHIFT, // $
0x22|SHIFT, // %
- 0x24|SHIFT, // &
+ 0x23|SHIFT, // & 38 0x26
- 0x34, // '
+ 0x24|SHIFT, // ' 39 0x27
- 0x26|SHIFT, // (
+ 0x25|SHIFT, // ( 40 0x28
- 0x27|SHIFT, // )
+ 0x26|SHIFT, // ) 41 0x29
- 0x25|SHIFT, // *
+ 0x34|SHIFT, // * 42 0x2A
- 0x2e|SHIFT, // +
+ 0x33|SHIFT, // + 43 0x2B
0x36, // ,
0x2d, // -
0x37, // .
0x38, // /
0x27, // 0
0x1e, // 1
0x1f, // 2
0x20, // 3
0x21, // 4
0x22, // 5
0x23, // 6
0x24, // 7
0x25, // 8
0x26, // 9
- 0x33|SHIFT, // :
+ 0x34, // :
0x33, // ;
0x36|SHIFT, // <
- 0x2e, // =
+ 0x2d|SHIFT, // =
0x37|SHIFT, // >
0x38|SHIFT, // ?
- 0x1f|SHIFT, // @
+ 0x2f, // @
0x04|SHIFT, // A
0x05|SHIFT, // B
0x06|SHIFT, // C
0x07|SHIFT, // D
0x08|SHIFT, // E
0x09|SHIFT, // F
0x0a|SHIFT, // G
0x0b|SHIFT, // H
0x0c|SHIFT, // I
0x0d|SHIFT, // J
0x0e|SHIFT, // K
0x0f|SHIFT, // L
0x10|SHIFT, // M
0x11|SHIFT, // N
0x12|SHIFT, // O
0x13|SHIFT, // P
0x14|SHIFT, // Q
0x15|SHIFT, // R
0x16|SHIFT, // S
0x17|SHIFT, // T
0x18|SHIFT, // U
0x19|SHIFT, // V
0x1a|SHIFT, // W
0x1b|SHIFT, // X
0x1c|SHIFT, // Y
0x1d|SHIFT, // Z
- 0x2f, // [
+ 0x30, // [
- 0x31, // bslash
+ 0x89, // ¥
- 0x30, // ]
+ 0x32, // ]
- 0x23|SHIFT, // ^
+ 0x2e, // ^
- 0x2d|SHIFT, // _
+ 0x87|SHIFT, // _
- 0x35, // `
+ 0x2f|SHIFT, // `
0x04, // a
0x05, // b
0x06, // c
0x07, // d
0x08, // e
0x09, // f
0x0a, // g
0x0b, // h
0x0c, // i
0x0d, // j
0x0e, // k
0x0f, // l
0x10, // m
0x11, // n
0x12, // o
0x13, // p
0x14, // q
0x15, // r
0x16, // s
0x17, // t
0x18, // u
0x19, // v
0x1a, // w
0x1b, // x
0x1c, // y
0x1d, // z
- 0x2f|SHIFT, // {
+ 0x30|SHIFT, // {
- 0x31|SHIFT, // |
+ 0x89|SHIFT, // |
- 0x30|SHIFT, // }
+ 0x32|SHIFT, // }
- 0x35|SHIFT, // ~
+ 0x2e|SHIFT, // ~
0 // DEL
};
ところが、この定義では上手く変換できない文字がある。問題となる文字は¥
_
|
の3つ。この3文字のキーコードを見ていただきたい。
0x89, // ¥
0x87|SHIFT, // _
0x89|SHIFT, // |
この3文字だけ0x80以上で、Shiftキーを必要とする文字を示すSHIFT(0x80)
とかぶってしまっているのが原因である。前出の処理コードに従うと「0x80ビットが立っているなら左Shiftキーのmodifierビットをセットして、0x7Fと論理積を取った値をHIDキーコードとしてセットする」ため、期待する文字にならないのだ。
解決には、1文字2バイト構成とする_asciimap
変換テーブル自体の変更と、それに見合った処理コードの変更が必要である。
_asciimap
テーブルは、以下のように2バイト構成とし、SHIFTキーの要否を1バイト目、HIDキーコードを2バイト目とする。
1.1 HIDキーコード変換テーブルの変更
-#define SHIFT 0x80
+#define SHIFT 0x100
-const uint8_t _asciimap[128] =
+const uint16_t _asciimap[128] = {
0x00, // NUL 0 0x00
0x00, // SOH 1 0x01
0x00, // STX 2 0x02
0x00, // ETX 3 0x03
0x00, // EOT 4 0x04
0x00, // ENQ 5 0x05
0x00, // ACK 6 0x06
0x00, // BEL 7 0x07
0x2A, // BS 8 0x08
0x2B, // TAB 9 0x09
0x28, // LF 10 0x0A
0x00, // VT 11 0x0B
0x00, // FF 12 0x0C
0x00, // CR 13 0x0D
0x00, // SO 14 0x0E
0x00, // SI 15 0x0F
0x00, // DEL 16 0x10
0x00, // DC1 17 0x11
0x00, // DC2 18 0x12
0x00, // DC3 19 0x13
0x00, // DC4 20 0x14
0x00, // NAK 21 0x15
0x00, // SYN 22 0x16
0x00, // ETB 23 0x17
0x00, // CAN 24 0x18
0x00, // EM 25 0x19
0x00, // SUB 26 0x1A
0x00, // ESC 27 0x1B
0x00, // FS 28 0x1C
0x00, // GS 29 0x1D
0x00, // RS 30 0x1E
0x00, // US 31 0x1F
0x2C, // Space 32 0x20
0x1E | SHIFT, // ! 33 0x21
0x1F | SHIFT, // " 34 0x22
0x20 | SHIFT, // # 35 0x23
0x21 | SHIFT, // $ 36 0x24
0x22 | SHIFT, // % 37 0x25
0x23 | SHIFT, // & 38 0x26
0x24 | SHIFT, // ' 39 0x27
0x25 | SHIFT, // ( 40 0x28
0x26 | SHIFT, // ) 41 0x29
0x34 | SHIFT, // * 42 0x2A
0x33 | SHIFT, // + 43 0x2B
0x36, // , 44 0x2C
0x2D, // - 45 0x2D
0x37, // . 46 0x2E
0x38, // / 47 0x2F
0x27, // 0 48 0x30
0x1E, // 1 49 0x31
0x1F, // 2 50 0x32
0x20, // 3 51 0x33
0x21, // 4 52 0x34
0x22, // 5 53 0x35
0x23, // 6 54 0x36
0x24, // 7 55 0x37
0x25, // 8 56 0x38
0x26, // 9 57 0x39
0x34, // : 58 0x3A
0x33, // ; 59 0x3B
0x36 | SHIFT, // < 60 0x3C
0x2D | SHIFT, // = 61 0x3D
0x37 | SHIFT, // > 62 0x3E
0x38 | SHIFT, // ? 63 0x3F
0x2F, // @ 64 0x40
0x04 | SHIFT, // A 65 0x41
0x05 | SHIFT, // B 66 0x42
0x06 | SHIFT, // C 67 0x43
0x07 | SHIFT, // D 68 0x44
0x08 | SHIFT, // E 69 0x45
0x09 | SHIFT, // F 70 0x46
0x0A | SHIFT, // G 71 0x47
0x0B | SHIFT, // H 72 0x48
0x0C | SHIFT, // I 73 0x49
0x0D | SHIFT, // J 74 0x4A
0x0E | SHIFT, // K 75 0x4B
0x0F | SHIFT, // L 76 0x4C
0x10 | SHIFT, // M 77 0x4D
0x11 | SHIFT, // N 78 0x4E
0x12 | SHIFT, // O 79 0x4F
0x13 | SHIFT, // P 80 0x50
0x14 | SHIFT, // Q 81 0x51
0x15 | SHIFT, // R 82 0x52
0x16 | SHIFT, // S 83 0x53
0x17 | SHIFT, // T 84 0x54
0x18 | SHIFT, // U 85 0x55
0x19 | SHIFT, // V 86 0x56
0x1A | SHIFT, // W 87 0x57
0x1B | SHIFT, // X 88 0x58
0x1C | SHIFT, // Y 89 0x59
0x1D | SHIFT, // Z 90 0x5A
0x30, // [ 91 0x5B
0x89, // ¥ 92 0x5C
0x32, // ] 93 0x5D
0x2E, // ^ 94 0x5E
0x87 | SHIFT, // _ 95 0x5F
0x2F | SHIFT, // ` 96 0x60
0x04, // a 97 0x61
0x05, // b 98 0x62
0x06, // c 99 0x63
0x07, // d 100 0x64
0x08, // e 101 0x65
0x09, // f 102 0x66
0x0A, // g 103 0x67
0x0B, // h 104 0x68
0x0C, // i 105 0x69
0x0D, // j 106 0x6A
0x0E, // k 107 0x6B
0x0F, // l 108 0x6C
0x10, // m 109 0x6D
0x11, // n 110 0x6E
0x12, // o 111 0x6F
0x13, // p 112 0x70
0x14, // q 113 0x71
0x15, // r 114 0x72
0x16, // s 115 0x73
0x17, // t 116 0x74
0x18, // u 117 0x75
0x19, // v 118 0x76
0x1A, // w 119 0x77
0x1B, // x 120 0x78
0x1C, // y 121 0x79
0x1D, // z 122 0x7A
0x30 | SHIFT, // { 123 0x7B
0x89 | SHIFT, // | 124 0x7C
0x32 | SHIFT, // } 125 0x7D
0x2E | SHIFT, // ~ 126 0x7E
0x00, // DEL 127 0x7F
};
1.2 press/releaseメソッドの対応
次に処理コード部分を次のように変更する。
size_t TinyKeyboard_::press(uint8_t k) {
uint8_t i;
if (k >= 136) { // it's a non-printing key (not a modifier)
k = k - 136;
} else if (k >= 128) { // it's a modifier key
_keyReport.modifiers |= (1<<(k-128));
k = 0;
} else { // it's a printing key
- k = pgm_read_byte(_asciimap + k);
+ uint16_t c = _asciimap[k];
- if (!k) {
+ if (!c) {
setWriteError();
return 0;
}
- if (k & 0x80) { // it's a capital letter or other character reached with shift
+ if (c & SHIFT) { // it's a capital letter or other character reached with shift
_keyReport.modifiers |= 0x02; // the left shift modifier
- k &= 0x7F;
}
+ k = (uint8_t)(c & 0xFF);
}
// Add k to the key report only if it's not already present
// and if there is an empty slot.
if (_keyReport.keys[0] != k && _keyReport.keys[1] != k &&
_keyReport.keys[2] != k && _keyReport.keys[3] != k &&
_keyReport.keys[4] != k && _keyReport.keys[5] != k) {
for (i=0; i<6; i++) {
if (_keyReport.keys[i] == 0x00) {
_keyReport.keys[i] = k;
break;
}
}
if (i == 6) {
setWriteError();
return 0;
}
}
sendReport(&_keyReport);
return 1;
}
TinyKeyboard_::release
メソッドも上記と同様に変更が必要である。
なお、_asciimap
テーブルがTinyUSB_Mouse_and_Keyboard.cpp
に定義されているため、USキーボードとJPキーボードを#ifdef
で分ける共存ができない。これを可能にしたいならば、_asciimap
テーブルをTinyUSB_Mouse_and_Keyboard.h
に移し、#ifdef
でUSキーボード変換テーブルとJPキーボード変換テーブルをコンパイル時に選択するように変更が必要である。JPキーボード専用であれば、このままで良い。
以上で、JPキーボードに対応する方法は完了。次にマウスの水平ホイール操作を追加する方法を説明する。
2. マウスの水平ホイールの追加
そもそもUSBプロトコルスタックをサポートするAdafruit TinyUSB Library for Arduinoには、最初からマウスの水平ホイール(横スクロール)操作は用意されている。しかし、Arduino標準マウスAPIに準拠させたためか、TinyUSB Mouse and Keyboard libraryにはAPIが無い。
このため、move
メソッドをコピーしてちょっと変更を加えれば使えるようになる。
TinyMouse_::move
メソッドの4番目の引数panがそれだ。
class TinyMouse_
{
private:
uint8_t _buttons;
void buttons(uint8_t b);
public:
TinyMouse_(void);
void begin(void);
void end(void);
void click(uint8_t b = MOUSE_LEFT);
void move(signed char x, signed char y, signed char wheel = 0);
+ void move(signed char x, signed char y, signed char wheel, signed char pan = 0);
void press(uint8_t b = MOUSE_LEFT); // press LEFT by default
void release(uint8_t b = MOUSE_LEFT); // release LEFT by default
bool isPressed(uint8_t b = MOUSE_LEFT); // check LEFT by default
};
void TinyMouse_::move (int8_t x, int8_t y, int8_t wheel) {
if ( USBDevice.suspended() ) {
USBDevice.remoteWakeup();
}
while(!usb_hid.ready()) delay(1);
usb_hid.mouseReport(RID_MOUSE,_buttons,x,y,wheel,0);
}
+ void TinyMouse_::move (signed char x, signed char y, signed char wheel, signed char pan) {
+ if ( USBDevice.suspended() ) {
+ USBDevice.remoteWakeup();
+ }
+ while(!usb_hid.ready()) delay(1);
+ usb_hid.mouseReport(RID_MOUSE,_buttons,x,y,wheel,pan);
+ }
上のコードを見れば、使い方は自明だろう。
Arduino標準マウスAPIコンパチといえ上位互換に当たるので最初から用意されていてもおかしくないAPIだ。
以上の対応で快適になったと思われる。
おわりに
新しいAPIに出会う度に、なんで最初から対応してないの?と思いつつ、いつもJPキーボード対応とマウスの水平ホイール追加をやっている自分がいる。
今回、TinyUSB関連のソースファイルを色々読んだが、HIDレポートディスクリプタ自体の変更が不要だったので、比較的に楽に実現できた。なお、オリジナルのソースファイルに手を加える行為は自己責任とします。
そういえば、Arduino標準APIはまだ対応してないな。使ってないから。
以上