ちっこいキーボードを愛でた話

ちっ○いはステータスだ！！

これは、ちっこいキーボードです
![スクリーンショット 2024-05-22 20.58.57.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2733720/5074254a-7c3b-d82a-2aa1-92b9aef7b9a3.jpeg)

# はじめに
- ラズパイのヘッドレス化作業をしているとき、有線キーボードとマウスがないと不便だと思ってたところ、M5Stackが[こんなキーボード](https://docs.m5stack.com/ja/unit/cardkb_1.1)を作っていたのを思い出して買ってみた。
- なんとか、有線キーボードおよび有線マウスに化けさせたいと思う。
- `USBHIDKeyboard.h`と`USBHIDMouse.h`というライブラリを使えば余裕らしいから、1日で出来るかな？

# サンプルスケッチ（M5AtomS3用に改変）
- [サンプルスケッチ](https://github.com/m5stack/M5Stack/blob/master/examples/Unit/CardKB/CardKB.ino)はこんな感じ。動かすと、AtomS3の画面とシリアルモニタに打ち込んだ文字が表示された。
```cpp
//M5AtomS3用に、若干改変
#include <M5Unified.h> // 変更：M5Stack.h ⇒ M5Unified.h
#include <Wire.h> // 追加
#define CARDKB_ADDR 0x5F

void setup() {
    M5.begin();             
    // M5.Power.begin();    // 削除
    Wire.begin(); //追加
    M5.Lcd.setTextSize(2);
    M5.Lcd.printf("CardKBtest\n"); //スペースを削って10文字にした
    M5.Lcd.printf(">>");
}
void loop() {
    Wire.requestFrom(CARDKB_ADDR,1);
    while (Wire.available())
    {
        char c = Wire.read();
        if (c != 0) {
            M5.Lcd.printf("%c", c);
            Serial.printf("%c <-HEX:", c); //　追加            
            Serial.println(c, HEX);
        }
    }
}
```
- キーボードのキーとvalue（上記コードのc）の対応表は以下の通り。なお、「a」のvalueは0x61となっているが、これは文字の[asciiコード](https://ja.wikipedia.org/wiki/ASCII)であり、キーボードの[スキャンコード](https://ja.wikipedia.org/wiki/%E3%82%B9%E3%82%AD%E3%83%A3%E3%83%B3%E3%82%B3%E3%83%BC%E3%83%89)ではないことに注意。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2733720/1fdd8040-bc0f-7f88-8e0d-a8c049d39e53.png)
- 大文字のAを打つときは、「Shift + a」の同時押しでなく、「Shift」⇒（LEDが赤く点滅）⇒「a」の順番で打つ。「Sym」(緑点滅)、「Fn」（青点滅）も同じ。

# usbキーボード化
- `USB.h`と`USBHIDKeyboard.h`を導入して、`USBHIDKeyboard Keyboard;`のKeyboardを出力先にするだけでよさそう。
- つまり、サンプルスケッチの`M5.Lcd.printf("%c", c);`の「M5.Lcd」を「Keyboard」に入れ替えればOKだ！余裕〜〜
```cpp
//M5AtomS3の場合
#include <M5Unified.h>       // 変更：M5Stack.h ⇒ M5Unified.h
#include <Wire.h>            // 追加
#include <USB.h>             // さらに追加
#include <USBHIDKeyboard.h>  // さらに追加
USBHIDKeyboard Keyboard;     // さらに追加

#define CARDKB_ADDR 0x5F

void setup() {
  M5.begin();
  // M5.Power.begin();    // 削除
  Wire.begin();      //追加
  USB.begin();       //さらに追加
  Keyboard.begin();  //さらに追加
  M5.Lcd.setTextSize(2);
  M5.Lcd.printf("CardKBtest\n");  //スペースを削って10文字にした
  M5.Lcd.printf(">>");
}
void loop() {
  Wire.requestFrom(CARDKB_ADDR, 1);
  while (Wire.available()) {
    char c = Wire.read();
    if (c != 0) {
      M5.Lcd.printf("%c", c);
      M5.Lcd.printf("(%x) ", c);  //　変更
      // Serial.println(c, HEX); // 削除
      Keyboard.print(c);  // さらに追加
    }
  }
}
```
- よし、macに接続されて、キーボードとして認識された！
- 「abc」と入力すると反応する！バックスペースで文字が消える！成功だ！
- と思ったら、enterキーを押しても改行しない...左矢印キーを押しても、カーソルが右に行く...ボスケテ...
- 調べてみると、対応表のEnterの0x0dはascii制御文字「CR(\r)」に相当するけど、macの改行は「LF(\n)」(0x0a)であるため、それが悪さをしてるんだろうか。でも、左矢印なんて、そもそもascii制御文字にそのようなものはないし、ascii制御文字は関係ないのかな？
- 基本に立ち返って、[USBHIDKeyboard.h](USBHIDKeyboard.h)のコードを見てみると、Returnの割り当ては0xB0でascii制御文字とは全然関係ないね。それに、上下左右キーの割り当ても設定されている。これを使えばいいのかな？
```cpp
#define KEY_UP_ARROW     0xDA
#define KEY_DOWN_ARROW   0xD9
#define KEY_LEFT_ARROW   0xD8
#define KEY_RIGHT_ARROW  0xD7
#define KEY_MENU         0xFE
#define KEY_SPACE        0x20
#define KEY_BACKSPACE    0xB2
#define KEY_TAB          0xB3
#define KEY_RETURN       0xB0
#define KEY_ESC          0xB1
```
- よって、試しに、
- ```cpp
    Keyboard.print(c);
 　を

- ```cpp
      if (c == 0x0d) {
        Keyboard.print(KEY_RETURN);
      } else {
        Keyboard.print(c);
      }

　　に書き換えて、リターンを押したところ、「176」(=0xb0)が表示された。
- 改めて`USBHIDKeyboard.h`をみると、そもそも標準のメソッド名はwriteなので、printをwriteに書き換えたところ、うまく改行できた。これを踏まえると、abcのような単純な文字以外は、`USBHIDKeyboard.h`の割り当て表に従って、変換すべきと分かる。
- よって、制御系のコードを変換することとした。
```cpp
      char c_key;
      switch (c) {
        case 0x0d: c_key = KEY_RETURN; break;
        case 0xb5: c_key = KEY_UP_ARROW; break;
        case 0xb6: c_key = KEY_DOWN_ARROW; break;
        case 0xb4: c_key = KEY_LEFT_ARROW; break;
        case 0xb7: c_key = KEY_RIGHT_ARROW; break;
        case 0x09: c_key = KEY_TAB; break;
        case 0x1b: c_key = KEY_ESC; break;
        default: c_key = c;
      }
      Keyboard.write(c_key);
```
- 上記により、上下左右の動きと、リターン、tab、escのキーを割り当てることが出来た。
- とりあえず、緊急避難的な使用目的なら、これらのキーで十分でしょ。

# usbマウス化
- 次は、usbマウス化を探る。
- ライブラリ[USBHIDMouse.h](https://github.com/espressif/arduino-esp32/blob/master/libraries/USB/src/USBHIDMouse.h)や、これを使った[サンプルスケッチ](https://github.com/espressif/arduino-esp32/blob/master/libraries/USB/examples/Mouse/ButtonMouseControl/ButtonMouseControl.ino)を見てみると、ボタンは、press,release,clickの動作を使い分けることが出来るみたい。緊急避難的な使用としては、clickだけで十分かな。デフォは左ボタンだけど、右ボタンと中央ボタンの指定も出来る。
- 動作としては、4つの引数を持つmoveメソッドがあり、(x軸移動、y軸移動、ホイール、パン)に割り当てられている。
- こっちの[サンプルスケッチ](https://github.com/espressif/arduino-esp32/blob/master/libraries/USB/examples/KeyboardAndMouseControl/KeyboardAndMouseControl.ino)をみると、とりあえずの移動は20刻みで様子をみたほうが良さそう。
- で、作ってみたコードがこれ。
```cpp
#include <M5Unified.h>
#include <Wire.h>
#include <USB.h>
#include <USBHIDMouse.h>
USBHIDMouse Mouse;

#define CARDKB_ADDR 0x5F

void setup() {
  M5.begin();
  Wire.begin();
  Mouse.begin();
  USB.begin();
  M5.Lcd.setTextSize(2);
  M5.Lcd.print("USB_mouse");
}

int8_t x = 0;
int8_t y = 0;
int8_t dx = 1;
int8_t dy = 1;

void loop() {
  Wire.requestFrom(CARDKB_ADDR, 1);
  while (Wire.available()) {
    char c = Wire.read();
    if (c != 0) {

      //クリック動作
      switch (c) {
        case 0x2c:  //, 左クリック
          Mouse.click();
          x = y = 0;
          break;
        case 0x2e:  //. 右クリック
          Mouse.click(MOUSE_RIGHT);
          x = y = 0;
          break;
        case 0x0d:  //returnキーでダブルクリック
          Mouse.click();
          delay(50);
          Mouse.click();
          x = y = 0;
          break;
      }

      //距離設定
      if (c > 0x30 && c < 0x40)  //数字キー 0-9の場合
      {
        dx = dy = 2 * (c - 0x30);
        x = y = 0;
      }

      //移動動作
      switch (c) {
        //可変距離
        case 0xb5: y = max(min(y - 20,-20), -120); break;  //up(画面座標は左上が原点だから、yを減少させる方向)
        case 0xb6: y = min(max(y + 20,+20), +120); break;  //down
        case 0xb4: x = max(min(x - 20,-20), -120); break;  //left
        case 0xb7: x = min(max(x + 20,+20), +120); break;  //right
        case 0x20: x = y = 0                     ; break;  //spaceキーでリセット

        //固定距離(sボタンを中心の上下左右) -- 微調整用に距離短め
        case 0x65: x =   0 ; y = -dy ; break;  //e 上
        case 0x78: x =   0 ; y = +dy ; break;  //x 下
        case 0x61: x = -dx ; y =   0 ; break;  //a 左
        case 0x64: x = +dx ; y =   0 ; break;  //d 右
        case 0x77: x = -dx ; y = -dy ; break;  //w 左上
        case 0x7a: x = -dx ; y = +dy ; break;  //z 左下
        case 0x72: x = +dx ; y = -dy ; break;  //r 右上
        case 0x63: x = +dx ; y = +dy ; break;  //c 右下
      }
      Mouse.move(x, y);

      M5.Lcd.clearDisplay();
      M5.Lcd.setCursor(0, 0);
      M5.Lcd.printf("(x,y)=\n (%d,%d)\n\n", x, y);
      M5.Lcd.printf("(dx,dy)=\n (%d,%d)", dx, dy);
    }
  }
}
```
- 使い勝手は改善の余地がありそうだけど、取りあえず動いた。
- 上下左右のキーを押すと、一度の押下で移動する距離が20→40→...→100→120と増えていく。
- sの上下左右に有るキー(w,e,r,a,d,z,x,c)を押すと、常に固定幅の距離で動く。移動距離は数字キー(1-9)で設定可能(初期値:1)。9に設定すると移動距離は18(数字キーの倍)。

# usbキーボードマウス
- 二つを合体させるけど、当然同時に使用は出来ないから、モード切替ボタン操作を決める。「fn + return」(=0xa3)でいいかな。
- で、合体させたのがこれ。
```cpp
#include <M5Unified.h>
#include <Wire.h>
#include <USB.h>
#include <USBHIDMouse.h>
#include <USBHIDKeyboard.h>
USBHIDKeyboard Keyboard;
USBHIDMouse Mouse;

#define CARDKB_ADDR 0x5F


enum mskbmode { MS,
                KB };  //マウス[MS]とキーボード[KB]の2モード設定
mskbmode mode = KB;    //初期値はキーボードモード（緊急避難的にはキーボードの方が使用頻度高そう）

void setup() {
  M5.begin();
  Wire.begin();
  Mouse.begin();
  Keyboard.begin();
  USB.begin();
  M5.Lcd.setTextSize(2);
  M5.Lcd.print("USBmouseKB");
}

int8_t x = 0;
int8_t y = 0;
int8_t dx = 1;
int8_t dy = 1;

void loop() {
  Wire.requestFrom(CARDKB_ADDR, 1);
  while (Wire.available()) {
    char c = Wire.read();

    if (c == 0xa3) {  //モード切替
      switch (mode) {
        case MS:
          mode = KB;
          c = 0;
          M5.Lcd.clearDisplay();
          M5.Lcd.setCursor(0, 0);
          M5.Lcd.println("KEYBOARD");
          break;
        case KB:
          mode = MS;
          c = 0;
          M5.Lcd.clearDisplay();
          M5.Lcd.setCursor(0, 0);
          M5.Lcd.println("MOUSE");
          break;
      }
    }

    // キーボードモード
    if (c != 0 && mode == KB) {

      char c_key;
      switch (c) {
        case 0x0d: c_key = KEY_RETURN; break;
        case 0xb5: c_key = KEY_UP_ARROW; break;
        case 0xb6: c_key = KEY_DOWN_ARROW; break;
        case 0xb4: c_key = KEY_LEFT_ARROW; break;
        case 0xb7: c_key = KEY_RIGHT_ARROW; break;
        case 0x09: c_key = KEY_TAB; break;
        case 0x1b: c_key = KEY_ESC; break;
        default: c_key = c;
      }
      Keyboard.write(c_key);

      M5.Lcd.clearDisplay();
      M5.Lcd.setCursor(0, 0);
      M5.Lcd.println("KEYBOARD");
      M5.Lcd.printf("%c", c);
      M5.Lcd.printf("(%x) ", c);
    }

    // マウスモード
    if (c != 0 && mode == MS) {

      //クリック動作
      switch (c) {
        case 0x2c:  //, 左クリック
          Mouse.click();
          x = y = 0;
          break;
        case 0x2e:  //. 右クリック
          Mouse.click(MOUSE_RIGHT);
          x = y = 0;
          break;
        case 0x0d:  //returnキーでダブルクリック
          Mouse.click();
          delay(50);
          Mouse.click();
          x = y = 0;
          break;

        //追加
        case 0x6b:  //k 左ボタンpress
          Mouse.press();
          x = y = 0;
          break;
        case 0x6c:  //l 左ボタンrelease
          Mouse.release();
          x = y = 0;
          break;
      }

      //距離設定
      if (c > 0x30 && c < 0x40)  //数字キー 0-9の場合
      {
        dx = dy = 2 * (c - 0x30);
        x = y = 0;
      }

      //移動動作
      switch (c) {
        //可変距離
        case 0xb5: y = max(min(y - 20,-20), -120); break;  //up(画面座標は左上が原点だから、yを減少させる方向)
        case 0xb6: y = min(max(y + 20,+20), +120); break;  //down
        case 0xb4: x = max(min(x - 20,-20), -120); break;  //left
        case 0xb7: x = min(max(x + 20,+20), +120); break;  //right
        case 0x20: x = y = 0                     ; break;  //spaceキーでリセット

        //固定距離(sボタンを中心の上下左右) -- 微調整用に距離短め
        case 0x65: x =   0 ; y = -dy ; break;  //e 上
        case 0x78: x =   0 ; y = +dy ; break;  //x 下
        case 0x61: x = -dx ; y =   0 ; break;  //a 左
        case 0x64: x = +dx ; y =   0 ; break;  //d 右
        case 0x77: x = -dx ; y = -dy ; break;  //w 左上
        case 0x7a: x = -dx ; y = +dy ; break;  //z 左下
        case 0x72: x = +dx ; y = -dy ; break;  //r 右上
        case 0x63: x = +dx ; y = +dy ; break;  //c 右下
      }
      Mouse.move(x, y);

      M5.Lcd.clearDisplay();
      M5.Lcd.setCursor(0, 0);
      M5.Lcd.println("MOUSE");
      M5.Lcd.printf("(x,y)=\n (%d,%d)\n", x, y);
      M5.Lcd.printf("(dx,dy)=\n (%d,%d)\n", dx, dy);
      M5.Lcd.print(Mouse.isPressed() ? "pressed" : "released");
    }
  }
}
```
- (後日、追記)実際に使ってみて、`press()`と`release()`が有った方が便利だったので、合体版で追加した。写真のsbcは[radxa_zero3w](http://radxa.com/products/zeros/zero3w/#techspec)で、ラズパイzeroと違ってusb-cが使えるのが良い。メモリ2g+eMMc16gbの40pin付きで8千円。とりあえず、出先でusbキーボードマウスを使ってsbcのbluetooth設定を行えた。目的は達成された。
![スクリーンショット 2024-05-25 10.13.18.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2733720/f2c337a2-972f-6ede-c1d3-650f1f88b97f.jpeg)


# おわりに
- 会社から帰って夜に作業を始めてから、翌日の午前中に完成できて一安心。
- それにしても、ちっこいキーボードで作業すると、指が痛くなるな。
- 次は、旧atomをusb-cでラズパイと接続して、PCとはbluetoothシリアル接続させることで、普通のキーボードからラズパイを操作させようかな。

# おまけ
- 後日、さらにコンパクトにしようとして、atomS3のかわりに[nanoC6](https://docs.m5stack.com/ja/core/M5NanoC6)を使おうと思ったら、nanoC6に使われるESPC6はUSB-OTG機能(必要に応じてUSBホストにもUSBペリフェラルにもなれる)がないらしく、nanoC6はUSBHIDとしてのキーボードやマウスにはなれなかった。残念。