keyballのOLEDで猫ちゃんを走らせる

# 完成品

![無題の動画 ‐ Clipchampで作成.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/359441/839ea23e-83b8-b226-6c5b-7f2d16d3fcf8.gif)


githubリポジトリはこちらになります。

https://github.com/empelt/keyball/tree/main/qmk_firmware/keyboards/keyball/keyball61/keymaps/cat

# はじめに
keyballをカスタマイズするのは沼です。キーキャップ，キースイッチ，ケーブル，テンティング，キー配列と様々な沼がありますが，今回はその中のOLED沼に浸かっていこうと思います。

OLEDで何をするかを考えたときに最初に思いついたのは，[RunCat](https://kyome.io/runcat/)でした。やはり全人類猫を見ながら生活したいということで，今回はkeyballのOLEDで猫を走らせて行こうと思います。

ベースにしているのはかみだいさんのファームウェアです。

https://twitter.com/d_kamiichi/status/1626523090665734145?ref_src=twsrc%5Etfw%7Ctwcamp%5Etweetembed%7Ctwterm%5E1626523090665734145%7Ctwgr%5E2689d6c407ffe8e547c291b4a80a9358dda6eace%7Ctwcon%5Es1_&ref_url=https%3A%2F%2Fnote.com%2Ftwoboy03%2Fn%2Fn791f11d7f261

# 事前準備
- かみだいさんのファームウェアをforkしてcloneする

https://github.com/kamiichi99/keyball/blob/main/qmk_firmware/keyboards/keyball/readme.md
- QMK firmware環境構築

https://note.com/yinouet1001/n/n54e6d3dc243a

# 画像の用意
今回，画像はRunCatの[githubリポジトリ](https://github.com/Kyome22/RunCat_for_windows)から拝借します。

RunCat_for_windows/RunCat/resources/cat/に猫ちゃんのicoファイルが白黒それぞれ5枚ずつあるのでそれをダウンロードしてきます。(今回は白の5枚を使います)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/359441/796fa20f-a52d-6cb7-347a-83f5eafc0bdc.png)


しかし，この猫ちゃんは0～255のモノクロ画像なので，いい感じのスクリプトを書いて0 or 255のモノクロ画像に変換します。スクリプトも[github](https://github.com/empelt/keyball)においてあります。

```python:conver.ipynb
for i in range(5):
    im = Image.open(f'img/{i}.png')
    im.point(lambda x: 255 if x > 115 else 0).save(f'converted_img/{i}.png')
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/359441/a2837b9c-39dd-385f-4fbc-050a60ab5114.png)

これで，画像の準備ができました。


# 画像を変換
次に画像をファームウェアに書きこめるように，コンバーターでbytesに変換します。

https://javl.github.io/image2cpp/

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/359441/dc736b70-1183-2a60-04c6-a26a20363baf.png)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/359441/3334f1ae-c8ed-737a-282f-172e2d58977c.png)

画像を選択した後，↑の図のように設定を変更します。
- Background color: Black
- Code output format: Plain bytes
- Draw mode: Vertical - 1 bit per pixel

なお設定に関しては，ご自身の環境や図によってよしなにいじってみてください。

Generate codeで出力されたコードをメモっておきます。

# ファームウェアの編集
今回は，キーが押されるごとに猫の画像を切り替えていき，パラパラ漫画の要領で猫を走らせます。

詳しいコードの編集は，[github](https://github.com/empelt/keyball)のcommitの差分を参照してみてください。

また今回編集するのは以下のファイルたちです。
```
keyball/qmk_firmware/keyboards/keyball/keyball61/keymaps/cat/keymap.c (new)
keyball/qmk_firmware/keyboards/keyball/lib/keyball/keyball.c
keyball/qmk_firmware/keyboards/keyball/lib/keyball/keyball.h
```

### かみだいさんのファームウェアのkeymapを複製，名前変更する
上書きしてしまってもいいのですが，わかりにくくなるため自分のkeymapを作成します。

```bash
$ cp -r qmk_firmware/keyboards/keyball/keyball61/keymaps/kamidai/ qmk_firmware/keyboards/keyball/keyball61/keymaps/cat
```
これで，
```
keyball/qmk_firmware/keyboards/keyball/keyball61/keymaps/cat/
    ├── config.h
    ├── keymap.c
    ├── readme.md
    └── rules.mk
```
が生成されていればOKです。

### keyball.cの編集

コンバーターで生成したコードをそれぞれ定数として持たせます。

```c:keyball.c
#ifdef OLED_ENABLE
// clang-format off
    const char PROGMEM cat_0[] = {
        0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 
        0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x80, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 
        0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0x80, 0x80, 0x80, 0x80, 0xc0, 0xc0, 0xe0, 0xe0, 
        0xe0, 0xe0, 0xe0, 0xf0, 0xfc, 0xff, 0xfd, 0xe7, 0xe7, 0xff, 0xe7, 0x70, 0x30, 0x00, 0x00, 0x00, 
        0x06, 0x06, 0x06, 0x06, 0x06, 0x03, 0x03, 0x01, 0x01, 0x00, 0x01, 0x7f, 0xff, 0xff, 0x3f, 0x7f, 
        0xff, 0xdf, 0x0f, 0x0f, 0x0f, 0x7f, 0x3f, 0x1f, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 
        0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x03, 0x00, 
        0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
    };
    const char PROGMEM cat_1[] = {
        0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 
        0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xc0, 0xc0, 0x80, 0xc0, 0xc0, 0x00, 0x00, 0x00, 
        0x00, 0x80, 0xc0, 0xc0, 0xc0, 0x60, 0x60, 0x60, 0x60, 0x60, 0x60, 0xe0, 0xc0, 0xe0, 0xe0, 0xf0, 
        0xf0, 0xf0, 0xf0, 0xf0, 0xf0, 0xf8, 0xfc, 0xff, 0xfe, 0xff, 0xf3, 0xff, 0x7f, 0x32, 0x1c, 0x00, 
        0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x30, 0x38, 0x1c, 0xde, 0xfe, 0x7f, 0x3f, 0x1f, 0x0f, 0x0f, 
        0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x03, 0x03, 0x03, 0x07, 0x07, 0x0f, 0x0f, 0x06, 0x04, 0x00, 
        0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 
        0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
    };
    const char PROGMEM cat_2[] = {
        0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 
        0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 
        0x00, 0x00, 0x04, 0x0c, 0x06, 0x06, 0x06, 0x06, 0x06, 0x0c, 0x8c, 0x9c, 0xf8, 0xf0, 0xf0, 0xf0, 
        0xf0, 0xf0, 0xf0, 0xf0, 0xe0, 0xe0, 0xe0, 0xf0, 0xfc, 0xfe, 0xfb, 0xce, 0xfe, 0xff, 0xcb, 0x60, 
        0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x08, 0x0c, 0x0e, 0x0f, 0x27, 0x3f, 0x3f, 0x1f, 0x0f, 0x07, 
        0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x0f, 0x0f, 0x1f, 0x3f, 0x3f, 0x3e, 0x04, 0x04, 
        0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 
        0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00
    };
    const char PROGMEM cat_3[] = {
        0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 
        0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 
        0x00, 0x00, 0x00, 0x00, 0x03, 0x03, 0x03, 0x03, 0x03, 0x02, 0x06, 0x0c, 0xdc, 0xf8, 0xe0, 0xf0, 
        0xf0, 0xe0, 0xe0, 0xc0, 0x80, 0x80, 0x80, 0xc0, 0xe0, 0xf0, 0x68, 0x7c, 0xf0, 0xf8, 0x78, 0x00, 
        0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x1f, 0xff, 0xff, 0x1f, 0x0f, 
        0x0f, 0x0f, 0x0f, 0x1f, 0x1f, 0x1f, 0x7f, 0xff, 0xff, 0xff, 0x7f, 0x7f, 0x67, 0x66, 0x06, 0x01, 
        0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 
        0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x03, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 
    };
    const char PROGMEM cat_4[] = {
        0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 
        0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 
        0x00, 0xc0, 0xc0, 0x80, 0x80, 0x80, 0x80, 0xc0, 0xc0, 0xe0, 0xe0, 0xe0, 0xf0, 0xf0, 0xf0, 0xf0, 
        0xf0, 0xf0, 0xe0, 0xe0, 0xf0, 0xf8, 0xfc, 0xf6, 0x9e, 0xfc, 0xfe, 0xbe, 0xc0, 0x00, 0x00, 0x00, 
        0x00, 0x00, 0x01, 0x01, 0x01, 0x01, 0x01, 0x00, 0x00, 0x00, 0x00, 0x03, 0x07, 0x3f, 0x3f, 0x3f, 
        0xff, 0xff, 0x7f, 0x3f, 0xff, 0xff, 0xff, 0x07, 0x03, 0x03, 0x03, 0x01, 0x00, 0x00, 0x00, 0x00, 
        0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 
        0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00
    };
// clang-format on
#endif
```

次に，実際に猫をOLEDに描画する部分の処理を追加します。

cat_stateの値によって，表示する猫の画像を切り替えています。

```c:keyball.c
# 何枚目の猫の画像を表示しているかを管理する変数
uint8_t cat_state = 0;

void keyball_oled_render_cat(void)
{
#ifdef OLED_ENABLE
    switch (cat_state) {
        case 0:
            oled_write_raw_P(cat_0, sizeof(cat_0));
            break;
        case 1:
            oled_write_raw_P(cat_1, sizeof(cat_1));
            break;
        case 2:
            oled_write_raw_P(cat_2, sizeof(cat_2));
            break;
        case 3:
            oled_write_raw_P(cat_3, sizeof(cat_3));
            break;
        case 4:
            oled_write_raw_P(cat_4, sizeof(cat_4));
            break;
    }
#endif
}
```

最後に，keyball.cにもともとあるprocess_record_kbという関数(キーが押されたときに発火するやつ)の中に，cat_stateを変更する処理を書きます。

```c:keyball.c
...
bool process_record_kb(uint16_t keycode, keyrecord_t *record) {
    cat_state = (cat_state + 1) % 5; // <- これを追加
    // store last keycode, row, and col for OLED
    keyball.last_kc  = keycode;
    keyball.last_pos = record->event.key;
    ...
```
mod 5を取ってあげることで，cat_stateの値は0 ～ 4の間を無限にループします。

### keyball.hの編集
keyball.cに関数を追加したためヘッダーファイルのkeyball.hも修正します。
```c:keyball.h
...
void keyball_oled_render_cat(void); // <- これを追加
```

### keymap.cの編集

keymap.cにすでにあるoledkit_render_info_userを修正します。

また，keyballのOLEDは縦向きで，猫を横に走らせたいので，OLEDを縦表示にするためにoled_init_user関数を配置しています。(master側のみ回転しているのは，keyballはデフォルトで，スレイブ側のOLEDにkeyballのロゴを表示するようになっているので，そのレイアウトを崩さないようにするためです)
```c:keymap.c
void oledkit_render_info_user(void) {
    // 猫を中央に持ってくるために空行を入れる
    oled_write_ln_P(PSTR(" "), false);
    oled_write_ln_P(PSTR(" "), false);
    oled_write_ln_P(PSTR(" "), false);
    oled_write_ln_P(PSTR(" "), false);
    oled_write_ln_P(PSTR(" "), false);
    keyball_oled_render_cat();
}

// 猫を表示する側のOLEDを縦表示にする
oled_rotation_t oled_init_user(oled_rotation_t rotation) {
    return !is_keyboard_master() ? OLED_ROTATION_180 : OLED_ROTATION_270;
}
```

あとは，ファームウェアをビルドして，keyballに書き込めば完成です！


# おわりに
筆者はC言語なんもわからん人間なので，もし変な実装があったらぜひ教えていただけるとありがたいです！

それでは，良きkeyballライフを！