Pi PICO を circuitpython で HID

![pico+circuitpython.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/496179/6de5cdb2-4dfb-df11-116d-08af71854175.png)

# 1.はじめに
pythonでロータリーエンコーダを上手に読めたので、RPiをPi PICOに乗り換えてHIDを作ってみます。
Pi PICOの開発環境はいくつかありますが、主にスキルの問題でpython環境にします。本気でHIDを作るなら、処理が早そうな C の方が適している気はしますが、1msecの入力タイミングを競うつもりがないならpythonでも良さそうな気がします。
Pi PICO の python環境には micropython と circuitpython がありますが、2021/9/16現在、micropython には HID の良いライブラリが無さそうなので、私にとっては、circuitpython 一択です。私自身 Adafruit 信徒なので否やはありません。

# 2.コンセプト
作るのは、キーボードでもマウスでもゲームパッドでもないあまり売って無さそうなデバイスです。
それぞれの装置から機能をつまみ食いしたキメラHIDを作ってみます。
![kimera.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/496179/37fe1093-5019-1cfa-7917-a5ff2ca09106.png)
ロータリーエンコーダの制御がうまくいったので、使えそうなシーンには迷わず投入していきます。音声ボリューム、矢印左右、上下、+-キー、PageUp/Downなどなど。

## 2-1.想定する使用シーン

・3D CADなどで 3D モデルをぐりぐり回したりズームインアウトしたり。
・動画編集ソフトでコマ送りしてみたり。
・音楽の再生/ストップしてみたり。
・ブラウザを行ったり来たりしてみたり。

実際便利かどうかは作ってみてから考えましょう。
手間をかけずにまず作る！

## 2-2.使いそうな部品たち

・ロータリーエンコーダー・・・適量
~~~・タクトスイッチ（オルタネート/モーメンタリ）・・・適量
・トグルスイッチ（ON-OFF/ON-OFF-ON/(ON)-OFF-(ON)・・・適量
・リニアポテンショメーター・・・もしかしたら使う
・Nポジションロータリースイッチ・・・もしかしたら使う
・タッチパネル（付きディスプレイ）・・・トラックパッド代わりに使いたいので調査中。
・筐体・・・3Dプリンタで作るか、100円ショップの適当なハコを加工するか。~~~


## 2-3.circuitpython のライブラリ達
ざっと調べたところ、次のライブラリの機能をつまみ食いすれば作りたいものが作れそう。
Adafruit様万歳。

### 2-3-1.adafruit_hid.keyboard.Keyboard
[キーボードエミュレーション用API](https://circuitpython.readthedocs.io/projects/hid/en/latest/api.html#)と[ソースコード](https://circuitpython.readthedocs.io/projects/hid/en/3.1.3/_modules/adafruit_hid/keyboard.html)

### 2-3-2.adafruit_hid.keycode.Keycode
[キーボードのキーコード](https://circuitpython.readthedocs.io/projects/hid/en/latest/api.html#adafruit-hid-keycode-keycode)

### 2-3-3.adafruit_hid.mouse.Mouse
[マウスエミュレーション用API](https://circuitpython.readthedocs.io/projects/hid/en/latest/api.html#adafruit-hid-mouse-mouse)と[ソースコード](https://circuitpython.readthedocs.io/projects/hid/en/3.1.3/_modules/adafruit_hid/mouse.html)

### 2-3-4.adafruit_hid.consumer_control.ConsumerControl
キーボード上でボリュームコントロールとか再生、停止とかオーディオ系のコントロールができる[アレのAPI](https://circuitpython.readthedocs.io/projects/hid/en/latest/api.html#adafruit-hid-consumer-control-consumercontrol)と[ソースコード](https://circuitpython.readthedocs.io/projects/hid/en/3.1.3/_modules/adafruit_hid/consumer_control.html)

### 2-3-5.adafruit_hid.consumer_control_code.ConsumerControlCode
[アレのキーコード](https://circuitpython.readthedocs.io/projects/hid/en/latest/api.html#adafruit-hid-consumer-control-code-consumercontrolcode)

### 2-3-6.adafruit_hid.gamepad.Gamepad
[ゲームパッドエミュレーション用API](https://circuitpython.readthedocs.io/projects/hid/en/3.1.3/api.html#adafruit-hid-gamepad-gamepad)と[ソースコード](https://circuitpython.readthedocs.io/projects/hid/en/3.1.3/_modules/adafruit_hid/gamepad.html)

※試してみたところ、うまく動かせませんでした。

# 3.ライブラリのテスト

HIDのライブラリはこちらの[GIT](https://github.com/adafruit/Adafruit_CircuitPython_HID)からZIPダウンロードして、解凍。
解凍したフォルダの中のadafruit_hidフォルダをRaspberry Pi PICOのlibフォルダの中にコピーしてください。
Raspberry Pi PICO はエクスプローラー上ではCIRCUITPYというドライブ名で表示されています。

## 3-1.ロータリーエンコーダーのテスト
まず、ロータリーエンコーダのテストを行います。

pin接続は次の通り。

![68747470733a2f2f71696974612d696d6167652d73746f72652e73332e61702d6e6f727468656173742d312e616d617a6f6e6177732e636f6d2f302f3439363137392f38643934303233662d643232342d633330622d626362362d3364353031373363326435662e706e67.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/496179/5a9d9c06-5a59-5d3a-95e4-3566faabe913.png)


|エンコーダ|RPi PICO|
| ---- |-----|
|GND|GND|
|SIA|GPIO2|
|SIB|GPIO3|
|SW|GPIO4|
|VCC|3V3|

pull UPの時はVCCは接続不要です。

ロータリーエンコーダーの動作確認用コードは次のコードです。
RPi PICOにはcircuitpython用のファームを入れています。
ファームのインストールはPi PICOのbootボタンを押しながらUSBを刺し、見えたドライブに[公式サイトからダウンロードしたファーム](https://circuitpython.org/board/raspberry_pi_pico/)(.UF2ファイル)を保存するだけです。

```python
import time
import board
import digitalio

rotaryA = board.GP2
rotaryB = board.GP3
rotarySW = board.GP4

counter = 0
times = 1
AstateOld = 1
BstateOld = 1
rotaryBtnOld = 1

Astate = digitalio.DigitalInOut(rotaryA)
Astate.switch_to_input(pull=digitalio.Pull.UP)
Bstate = digitalio.DigitalInOut(rotaryB)
Bstate.switch_to_input(pull=digitalio.Pull.UP)
rotaryBtn = digitalio.DigitalInOut(rotarySW)
rotaryBtn.switch_to_input(pull=digitalio.Pull.UP)


while True:
    AstateCurrent = Astate.value
    BstateCurrent = Bstate.value
    rotaryBtnCurrent = rotaryBtn.value

    if AstateCurrent == 1 and AstateOld == 0:
        print("AstateOld = ",AstateOld)
        print("BsatteOld = ",BstateOld)
        
        if AstateOld == 0 and BstateOld == 0:
            counter += 1
        elif AstateOld == 0 and BstateOld == 1:
            counter -= 1
        times += 1
        
        print("counter = ", counter)
        print("times   = ", times)

    if rotaryBtnCurrent == 0 and rotaryBtnOld == 1:
        print("rotaryBtn was pressed")
    if rotaryBtnCurrent == 1 and rotaryBtnOld == 0:
        print("rotaryBtn was released")
        
    AstateOld = AstateCurrent
    BstateOld = BstateCurrent
    rotaryBtnOld = rotaryBtnCurrent
    
#    time.sleep(0.001)
        

```
RPiのコードをほぼそのまま移植しています。
circuitpythonはまだ割り込み処理に対応していませんので、メインループが重くなると取りこぼしが出るかもしれません。
RPiで必要だったチャタリング防止のsleep処理は不要でした。RPiよりも処理速度が遅いためと思われます。



# 3-2.キーボードのテスト
キーボードのテストとして、ロータリーエンコーダーの左右回転で矢印上下、プッシュスイッチで、[Alt + Print Screen]を割り付けます。

```python
import time
import board
import digitalio

import usb_hid
from adafruit_hid.keyboard import Keyboard
from adafruit_hid.keycode import Keycode

kbd = Keyboard(usb_hid.devices)
rotaryA = board.GP2
rotaryB = board.GP3
rotarySW = board.GP4

counter = 0
times = 1
AstateOld = 1
BstateOld = 1
rotaryBtnOld = 1

Astate = digitalio.DigitalInOut(rotaryA)
Astate.switch_to_input(pull=digitalio.Pull.UP)
Bstate = digitalio.DigitalInOut(rotaryB)
Bstate.switch_to_input(pull=digitalio.Pull.UP)
rotaryBtn = digitalio.DigitalInOut(rotarySW)
rotaryBtn.switch_to_input(pull=digitalio.Pull.UP)


while True:
    AstateCurrent = Astate.value
    BstateCurrent = Bstate.value
    rotaryBtnCurrent = rotaryBtn.value

    if AstateCurrent == 1 and AstateOld == 0:
        print("AstateOld = ",AstateOld)
        print("BsatteOld = ",BstateOld)
        
        if AstateOld == 0 and BstateOld == 0:
            counter += 1
            kbd.send(Keycode.UP_ARROW)

        elif AstateOld == 0 and BstateOld == 1:
            counter -= 1
            kbd.send(Keycode.DOWN_ARROW)

        times += 1
        
        print("counter = ", counter)
        print("times   = ", times)

    if rotaryBtnCurrent == 0 and rotaryBtnOld == 1:
        print("rotaryBtn was pressed")
        kbd.send(Keycode.ALT,Keycode.PRINT_SCREEN)
        time.sleep(0.001)

#    if rotaryBtnCurrent == 1 and rotaryBtnOld == 0:
#        print("rotaryBtn was released")


    AstateOld = AstateCurrent
    BstateOld = BstateCurrent
    rotaryBtnOld = rotaryBtnCurrent
```

エンコーダーで上下スクロール、プッシュボタンでアクティブウィンドウをスナップショットできるデバイスになりました。
スクリーンショットは連打する必要がないため、kbd.send()でプッシュスイッチ押し込み一回につき一回のスクリーンショットを撮っています。
プッシュスイッチの押し込み検出時にkbd.press()、スイッチ解放検出時にkbd.release()とすると、長押し中の連打が可能になります。

# 3-3.マウスのテスト
マウスのテストでは、ロータリーエンコーダーをホイール、プッシュスイッチを左クリックに割り付けます。

```python
import time
import board
import digitalio

import usb_hid
from adafruit_hid.mouse import Mouse

m = Mouse(usb_hid.devices)

rotaryA = board.GP2
rotaryB = board.GP3
rotarySW = board.GP4

counter = 0
times = 1
AstateOld = 1
BstateOld = 1
rotaryBtnOld = 1

Astate = digitalio.DigitalInOut(rotaryA)
Astate.switch_to_input(pull=digitalio.Pull.UP)
Bstate = digitalio.DigitalInOut(rotaryB)
Bstate.switch_to_input(pull=digitalio.Pull.UP)
rotaryBtn = digitalio.DigitalInOut(rotarySW)
rotaryBtn.switch_to_input(pull=digitalio.Pull.UP)


while True:
    AstateCurrent = Astate.value
    BstateCurrent = Bstate.value
    rotaryBtnCurrent = rotaryBtn.value

    if AstateCurrent == 1 and AstateOld == 0:
        print("AstateOld = ",AstateOld)
        print("BsatteOld = ",BstateOld)
        
        if AstateOld == 0 and BstateOld == 0:
            counter += 1
            m.move(wheel = 1)
        elif AstateOld == 0 and BstateOld == 1:
            counter -= 1
            m.move(wheel = -1)
        times += 1
        
        print("counter = ", counter)
        print("times   = ", times)

    if rotaryBtnCurrent == 0 and rotaryBtnOld == 1:
        print("rotaryBtn was pressed")
        m.press(Mouse.LEFT_BUTTON)

    if rotaryBtnCurrent == 1 and rotaryBtnOld == 0:
        print("rotaryBtn was released")
        m.release(Mouse.LEFT_BUTTON)
        
    AstateOld = AstateCurrent
    BstateOld = BstateCurrent
    rotaryBtnOld = rotaryBtnCurrent

```

左クリックの部分は、m.press()とm.releaseとすることで、押し込みと解放の両方を検出しているため、ドラッグ操作ができます。
m.click()とすると、プッシュスイッチの押し込み一回でpressとreleaseを行いますので、ボタン解放の検出は不要になります。


# 3-4.コンシューマーコントロールのテスト
ロータリーエンコーダーをボリューム、プッシュスイッチを再生/一時停止キーに割り付けます。

```python
import time
import board
import digitalio

import usb_hid
from adafruit_hid.consumer_control import ConsumerControl
from adafruit_hid.consumer_control_code import ConsumerControlCode

consumer_control = ConsumerControl(usb_hid.devices)
rotaryA = board.GP2
rotaryB = board.GP3
rotarySW = board.GP4

counter = 0
times = 1
AstateOld = 1
BstateOld = 1
rotaryBtnOld = 1

Astate = digitalio.DigitalInOut(rotaryA)
Astate.switch_to_input(pull=digitalio.Pull.UP)
Bstate = digitalio.DigitalInOut(rotaryB)
Bstate.switch_to_input(pull=digitalio.Pull.UP)
rotaryBtn = digitalio.DigitalInOut(rotarySW)
rotaryBtn.switch_to_input(pull=digitalio.Pull.UP)


while True:
    AstateCurrent = Astate.value
    BstateCurrent = Bstate.value
    rotaryBtnCurrent = rotaryBtn.value

    if AstateCurrent == 1 and AstateOld == 0:
        print("AstateOld = ",AstateOld)
        print("BsatteOld = ",BstateOld)
        
        if AstateOld == 0 and BstateOld == 0:
            counter += 1
            consumer_control.send(ConsumerControlCode.VOLUME_INCREMENT)

        elif AstateOld == 0 and BstateOld == 1:
            counter -= 1
            consumer_control.send(ConsumerControlCode.VOLUME_DECREMENT)

        times += 1
        
        print("counter = ", counter)
        print("times   = ", times)

    if rotaryBtnCurrent == 0 and rotaryBtnOld == 1:
        print("rotaryBtn was pressed")
        consumer_control.send(ConsumerControlCode.PLAY_PAUSE)
        time.sleep(0.001)

    AstateOld = AstateCurrent
    BstateOld = BstateCurrent
    rotaryBtnOld = rotaryBtnCurrent
```

# 3-5.ゲームパッドのテスト
こちらに[ライブラリのソースが公開されている](https://circuitpython.readthedocs.io/projects/hid/en/3.1.3/_modules/adafruit_hid/gamepad.html#Gamepad)のですが、うまく動きません。
デバイスの認識のあたりがうまく動いてない様ですが、私の知識ではよくわかりませんでした。
aidafruit_HIDライブラリにも含まれていないので、まだ未完成なのかもしれません。

というわけで、残念ながらゲームパッドは今回はパスします。

# 3-6.キーボードとマウスのキメラ
ゲームパッドのライブラリが動かないのでアナログ入力はできませんでしたが、キーボードとマウスのキメラデバイスをテストしてみます。

エンコーダーでマウスホイール、プッシュボタンで[ALT + PRINT SCREEN]をエミュレートしてみます。

```python
import time
import board
import digitalio

import usb_hid
from adafruit_hid.mouse import Mouse
from adafruit_hid.keyboard import Keyboard
from adafruit_hid.keycode import Keycode

m = Mouse(usb_hid.devices)
kbd = Keyboard(usb_hid.devices)

rotaryA = board.GP2
rotaryB = board.GP3
rotarySW = board.GP4

counter = 0
times = 1
AstateOld = 1
BstateOld = 1
rotaryBtnOld = 1

Astate = digitalio.DigitalInOut(rotaryA)
Astate.switch_to_input(pull=digitalio.Pull.UP)
Bstate = digitalio.DigitalInOut(rotaryB)
Bstate.switch_to_input(pull=digitalio.Pull.UP)
rotaryBtn = digitalio.DigitalInOut(rotarySW)
rotaryBtn.switch_to_input(pull=digitalio.Pull.UP)


while True:
    AstateCurrent = Astate.value
    BstateCurrent = Bstate.value
    rotaryBtnCurrent = rotaryBtn.value

    if AstateCurrent == 1 and AstateOld == 0:
        print("AstateOld = ",AstateOld)
        print("BsatteOld = ",BstateOld)

        if AstateOld == 0 and BstateOld == 0:
            counter += 1
            m.move(wheel = 1)
        elif AstateOld == 0 and BstateOld == 1:
            counter -= 1
            m.move(wheel = -1)
        times += 1

        print("counter = ", counter)
        print("times   = ", times)

    if rotaryBtnCurrent == 0 and rotaryBtnOld == 1:
        print("rotaryBtn was pressed")
        kbd.send(Keycode.ALT,Keycode.PRINT_SCREEN)
        time.sleep(0.001)

    AstateOld = AstateCurrent
    BstateOld = BstateCurrent
    rotaryBtnOld = rotaryBtnCurrent
```

# 4.実装（延期）
今回は動作テストまでとします。

# 5.まとめ
ゲームパッドが動かなかったので少しテンションが下がってしまいました。
デバイスづくりはなるべく近い将来に延期します。
ゲームパッドは使えなかったもののその他のライブラリは簡単に使うことができました。

想定外だったのはcircuitpythonが割り込み処理に対応してなかったことでが、特に時間のかかる処理を行うわけでもないため、入力の取りこぼしなく動作できている様です。