Amazonで買ったガジェットを魔改造したソースコード

Amazonで買ったガジェットを魔改造したソースコード

Amazonで売っている中華ガジェット、ぶっちゃけハズレも多いのですが、なんかそそられるものがありますよね。
今回は中華ガジェットを買ったらハズレだったので、自分で中身を入れ替えて使えるようにした、そんな話です。

と、話を書いても良いのですが、動画にまとめてニコニコ動画にアップしたので、それを見てやってください。

ソースコードの解説

解説は動画を見ていただいた前提で書かせていただきます。まだ見ていない方はご視聴頂ますようお願いします。

まずはソースコード全体です。

ソースコードのダウンロードはこちらから→code.py

import board
import rotaryio
import busio
import digitalio
import time
import usb_hid
from adafruit_hid.keyboard import Keyboard
from adafruit_hid.keycode import Keycode
import adafruit_debouncer
import adafruit_ssd1306

BUTTON_IO = [board.GP26, board.GP27, board.GP28]
BUTTON_KEYCODE = [Keycode.KEYPAD_ZERO, Keycode.KEYPAD_PERIOD, Keycode.KEYPAD_ENTER]
FF_KEY = Keycode.PERIOD
RW_KEY = Keycode.COMMA
SCL = board.GP7
SDA = board.GP6
ENC_A = board.GP2
ENC_B = board.GP3

class logic_key:
    """Logic Pro専用キーボードのクラス
    """
    def __init__(self) -> None:
        """コンストラクタ
        """
        self.keyboard = Keyboard(usb_hid.devices)
        self.encoder = rotaryio.IncrementalEncoder(ENC_A, ENC_B)
        self.prepos = self.encoder.position
        try:
            self.i2c = busio.I2C(SCL, SDA)
            self.display = adafruit_ssd1306.SSD1306_I2C(128, 64, self.i2c, addr=0x3C)
        except RuntimeError as e:
            # I2Cがつながっていないとプルアップされていない例外が出るので
            # それを拾ってself.displayを使わないようにする。
            print(e)
            print("ディスプレイが見つからないので使わないことにします。")
            self.display = None
        self.shows = [self.show_stop, self.show_pause, self.show_play]
        self.buttons = []
        self.init_buttons()
        print("初期化完了")
    
    def init_buttons(self) -> None:
        """ボタンの初期化メソッド
        """
        for b in BUTTON_IO:
            tmp_pin = digitalio.DigitalInOut(b)
            tmp_pin.direction = digitalio.Direction.INPUT
            tmp_pin.pull = digitalio.Pull.UP
            self.buttons.append(
                adafruit_debouncer.Debouncer(tmp_pin)
            )

    def main_loop(self) -> None:
        """メインループメソッド
        """
        while True:
            self.encoder_detect()
            self.button_detect()

    def encoder_detect(self) -> None:
        """ロータリーエンコーダーを検知するメソッド
        """
        pos = self.encoder.position
        # if pos != self.prepos:
        #     print(pos)
        if pos > self.prepos:
            self.keyboard.send(FF_KEY)
        if pos < self.prepos:
            self.keyboard.send(RW_KEY)
        self.prepos = pos

    def button_detect(self) -> None:
        """ボタンを検知するメソッド
        """
        for i, btn in enumerate(self.buttons):
            btn.update()
            if btn.fell:
                self.send_key(i, True)
            if btn.rose:
                self.send_key(i, False)

    def send_key(self, btn_num: int, is_pressed: bool):
        """ボタンに応じたキーボード情報を送るメソッド

        Args:
            btn_num (int): ボタンの番号
            is_pressed (bool): 押したとき->True / 離したとき->False
        """
        if is_pressed:
            # print("btn", btn_num, "pressed.")
            self.keyboard.press(BUTTON_KEYCODE[btn_num])
            self.shows[btn_num]()
        else:
            # print("btn", btn_num, "released.")
            self.keyboard.release(BUTTON_KEYCODE[btn_num])

    def show_play(self) -> None:
        """再生ボタン時のディスプレイ表示
        """
        if self.display is None:
            return
        self.display.fill(0)
        self.display.text("PLAY", 20, 0, 1, size=3)
        for i in range(16):
            self.display.hline(40, 32 + i, i * 2, 1)
            self.display.hline(40, 63 - i, i * 2, 1)
        self.display.show()

    def show_stop(self) -> None:
        """停止ボタン時のディスプレイ表示
        """
        if self.display is None:
            return
        self.display.fill(0)
        self.display.text("STOP", 20, 0, 1, size=3)
        self.display.fill_rect(40, 32, 32, 32, 1)
        self.display.show()

    def show_pause(self) -> None:
        """一時停止ボタン時のディスプレイ表示
        """
        if self.display is None:
            return
        self.display.fill(0)
        self.display.text("PAUSE", 20, 0, 1, size=3)
        self.display.fill_rect(40, 32, 11, 32, 1)
        self.display.fill_rect(61, 32, 11, 32, 1)
        self.display.show()

def main() -> None:
    """メイン関数
    """
    lk = logic_key()
    lk.main_loop()


if __name__ == "__main__":
    main()

行うこと

やっていることは単純です。USBキーボードとして動かして、

  • ボタン入力に対してキーボードの状態を送る
  • ロータリーエンコーダーの変化に対してキーボードの状態を送る
  • オマケ機能としてI2CインターフェイスでSSD1306のディスプレイに出力を出す。

この3つになります。

定数関連

BUTTON_IO = [board.GP26, board.GP27, board.GP28]
BUTTON_KEYCODE = [Keycode.KEYPAD_ZERO, Keycode.KEYPAD_PERIOD, Keycode.KEYPAD_ENTER]
FF_KEY = Keycode.PERIOD
RW_KEY = Keycode.COMMA
SCL = board.GP7
SDA = board.GP6
ENC_A = board.GP2
ENC_B = board.GP3

定数のキモは、ボタンのIO配列とキーコードのリストです。この両方のリストのインデックスでボタンと送るキーコードを紐づけています。

他の定数は、まぁ、定数名そのままって感じです。

コンストラクタ

    def __init__(self) -> None:
        """コンストラクタ
        """
        self.keyboard = Keyboard(usb_hid.devices)
        self.encoder = rotaryio.IncrementalEncoder(ENC_A, ENC_B)
        self.prepos = self.encoder.position
        try:
            self.i2c = busio.I2C(SCL, SDA)
            self.display = adafruit_ssd1306.SSD1306_I2C(128, 64, self.i2c, addr=0x3C)
        except RuntimeError as e:
            # I2Cがつながっていないとプルアップされていない例外が出るので
            # それを拾ってself.displayを使わないようにする。
            print(e)
            print("ディスプレイが見つからないので使わないことにします。")
            self.display = None
        self.shows = [self.show_stop, self.show_pause, self.show_play]
        self.buttons = []
        self.init_buttons()
        print("初期化完了")

動画中でも言ってますが、今回の作例では、ネット上にある作例ではあまり使われていないクラスを作ってます。

クラスのコンストラクタでクラスの初期化とハードウェアの初期化もしています。

ロータリーエンコーダーを見る部分を自分でつくろうと思ったら、エンコーダーのA相とB相の関係を色々見てあげないといけないのですが、CircuitPythonではrotaryio.IncrementalEncoderと宣言するだけで、ロータリーエンコーダーの現在位置をカウントしてくれます。非常に便利です。

例外処理が入っているのはI2Cの部分で、ディスプレイが接続されてI2CのI/Oがプルアップされていないと、RuntimeErrorがraiseしてくるので、それを捕まえてself.displayをNoneにしています。ディスプレイを使うメソッドなどでself.displayをチェックをしていてNoneならば処理をしないようにしています。

もう一つのポイントは、self.showのリストです。これはメソッドの配列で、ボタンの番号とメソッド配列の番号が紐付けられるようにしています。

ボタンの初期化メソッド

    def init_buttons(self) -> None:
        """ボタンの初期化メソッド
        """
        for b in BUTTON_IO:
            tmp_pin = digitalio.DigitalInOut(b)
            tmp_pin.direction = digitalio.Direction.INPUT
            tmp_pin.pull = digitalio.Pull.UP
            self.buttons.append(
                adafruit_debouncer.Debouncer(tmp_pin)
            )

定数の配列で宣言されていたボタンのGPIOに対して初期化を行いつつ、self.buttonsというリストにadafruit_debouncer.Debouncerを割り当ててます。

Debouncerはいわゆるチャタリング対策をして、その上で立ち上がり/立ち下がりを検出してくれるものです。
実はadafruit_debouncer.Buttonというのもあるのですが、今回はDebouncerのほうが使いやすいです。

詳細はadafruitのドキュメントを見てください。

メインループ

    def main_loop(self) -> None:
        """メインループメソッド
        """
        while True:
            self.encoder_detect()
            self.button_detect()

2つのメソッドを無限に回すメインループです。
情報系の人が見たらこんな無限ループはビビっちゃいますけど、組み込み系だと当たり前に使うやつですね。

encoder_detect()でロータリーエンコーダーの動きを見て、button_detect()でボタンの動きを見てます。

ロータリーエンコーダーのメソッド

    def encoder_detect(self) -> None:
        """ロータリーエンコーダーを検知するメソッド
        """
        pos = self.encoder.position
        # if pos != self.prepos:
        #     print(pos)
        if pos > self.prepos:
            self.keyboard.send(FF_KEY)
        if pos < self.prepos:
            self.keyboard.send(RW_KEY)
        self.prepos = pos

self.encoder.positionで現在のロータリーエンコーダーの位置がint型で得られます。楽ですねぇ。
前回の位置と比較して、大きければ早送りのキーを、小さければ巻き戻しのキーを送ってます。
同じならば何もしないで、最後に前回の位置self.preposに現在位置を入れてこのメソッドは終了です。

ボタン検出メソッドとそれに対応する処理のメソッド

    def button_detect(self) -> None:
        """ボタンを検知するメソッド
        """
        for i, btn in enumerate(self.buttons):
            btn.update()
            if btn.fell:
                self.send_key(i, True)
            if btn.rose:
                self.send_key(i, False)

    def send_key(self, btn_num: int, is_pressed: bool):
        """ボタンに応じたキーボード情報を送るメソッド

        Args:
            btn_num (int): ボタンの番号
            is_pressed (bool): 押したとき->True / 離したとき->False
        """
        if is_pressed:
            # print("btn", btn_num, "pressed.")
            self.keyboard.press(BUTTON_KEYCODE[btn_num])
            self.shows[btn_num]()
        else:
            # print("btn", btn_num, "released.")
            self.keyboard.release(BUTTON_KEYCODE[btn_num])

button_detect()では、ボタンリストのenumerateでforループで回します。btnをupdate()すると現在の状況と前回との差が得られます。ボタンは押されると立ち下がり離されると立ち上がります。それぞれbtn.fellとbtn.roseで検出できます。そうして、ボタンの番号と押されたのか離されたのかをis_pressedとしてsend_key()メソッドに送っています。

send_key()ではis_pressedに応じてボタン番号で指定されたキーコードをキーボードに送っています。

また、ボタン番号に応じてメソッドのリストshowsを実行しています。

ディスプレイ出力のメソッド

これらはまぁ、見たまんまなので解説することもないでしょうか。
三角形を描くメソッドがないのでいろいろな長さの線をいっぱい描く力技ぐらいでしょうねw

解説は以上です。
ざっくり解説なのでわかりにくいところもあるかとは思いますが、あしからず。