IO Expander MCP23017で分割したキーボード(Sparrow62v2) でQMK Firmwareに対応させる

tl;dr

  • MCP23017は、I2C IOエキスパンダーで、IOが足りないない所をI2Cプロトコルで拡張できるICだよ。16bitのIOが増やせるよ。ただし、プルダウン機能はないよ。
  • Sparrow62v2では左手側にRaspberry Pi Picoを積み、右手側にMCP23017を積み、その間をI2Cで接続したキーボードだよ。今まで、KMK Firmwareしか対応してなかったよ。
  • MCP23017で利用したプロトコルは、INPUT/OUTPUT方向を決めるIODIRA, IODIRBと、値のゲットセットを行うGPIOA、GPIOBさえ使えば良いよ。
  • QMK Firmwareで、scan_custom_matrix()を実装することで、MCP23017でのマトリックススキャンを実装したよ。

Sparrow62v2

Sparrow62v2は、私の作っている左右分割型自作キーボードキットです。かつては游舎工房でも委託販売しておりましたが、現在は在庫切れとなっております。そろそろv3を作りたいと思っており、再販予定は今のところありません。

左右分割で左右でレイアウトが異なっているのと、トッププレート、ボトムプレート、メインボードも厚さがそれぞれ異なるPCBを利用しているため、結構生産にコストがかかります。1年半で20程度販売できたのですが、再生産コストをかけるならばv3を設計したい気持ちがあり、v2の再生産を予定していません。

一方、利用いただいてる方はいらっしゃり、QMK Firmware対応のリクエストを受けました。MCP23017を利用しており、QMK Firmwareの基本機能に習熟していないこともあり、販売当初からKMK Firmwareのみをサポートさせてもらっていました。SparrowDialを通して、QMK Firmwareについて理解を深め、I2Cデバイスを使うための機能が整備されていることを理解したため、対応をやってみることにしました。

IOエキスパンダーMCP23017を組み込む

IOエキスパンダーMCP23017は、I2Cのシリアル通信で、GPIO 16bit分を操作できるICです。秋月でも190円で購入することができます。半導体品不足の時には、在庫切れを起こしていましたが、現状在庫豊富なようです。

akizukidenshi.com

Sparrow62v2ではスペースを節約したり、MCUを2個用意させるコストに疑問を感じていたため、IOエキスパンダーを利用するようにしました。

ただ、注意点としては、MCP23017には入力に対してプルアップ(フローティング時でHighとする)する機能はあるのですが、プルダウン(フローティング時にLowとする)する機能はありません。COLからROWに電流を流すスイッチマトリックス構成の場合、入力側のROWにプルダウンが必要となります。そのため、プルダウン抵抗を回路に追加しました。

実際のSparrow62v2の右手側MCP23017周りの回路図

Sparrow62v2の構成

Sparrow62v2の内部構成はざっくり以下のような構成です。

左手メインボードではRP2040(Raspberry Pi Pico)上でQMK Firmwareが動作しており、TRRSソケットを通して右手メインボードのMCP23017のI2Cで通信を行っています。

左右のメインボードでそれぞれスイッチマトリックスが組まれており、COLからROWに電流が流れるようになっています。スイッチマトリックスがRP2040、MCP23017に接続された形になっています。

MCP23017のプロトコル

I2Cではマスター(MCU、RP2040)側から、スレイブ(MCP23017)に対して、「特定アドレスのレジスタを読み書きする」ようにプロトコルを組むことが多いです。このレジスタがアドレスごとに機能が異なっており、特定のアドレスで、GPIOの役割をセットできたり、GPIOのアウトプットを設定したり、インプットの値を取得したりします。

MCP23017には、アドレスの振り方に、16ビットモード(IOCON.BANK=0)と、8ビットモード(IOCON.BANK=1)があります。初期状態が16ビットモードになっていて、16ビットモードでもアドレスが違うだけなので、私は16ビットモードで利用しています。本稿も16ビットモードで解説します。

主に使うのは以下の4つレジスタだけです。

名称 レジスタアドレス 機能
IODIRA 0x00 GPIOAの各IO0~8のインプット・アウトプットの方向を8bit(0:アプトプット、1: インプット)で設定する
IODIRB 0x01 GPIOBの各IO0~8のインプット・アウトプットの方向を8bit(0:アプトプット、1: インプット)で設定する
GPIOA 0x12 GPIOAの各IO0~8のインプット・アウトプットの値を8bit(0: Low、1:High)で取得、設定する
GPIOB 0x13 GPIOBの各IO0~8のインプット・アウトプットの値を8bit(0: Low、1:High)で取得、設定する

IODIRAをROWのインプット、IODIRBをCOLのアウトプットとして使うため、以下を設定すれば良いとわかります。

  • GPIOAを全てインプットにするため、IODIRAに0xffをセット
  • GPIOBを全てアウトプットにするため、IODIRBに0x00をセット

そしてCOLからROWのマトリックススキャンをするため、以下を行います。

  • 読み取りたいCOLのGPIOのみをHighにするため、GPIOBに該当bitのみをHigh(例: 0b00000010)をセット
  • COL設定後に、ROWのGPIOを読み取る(対象COLにおいて、スイッチが押されたキーのROWのみがHighになる)ため、GPIOAを読み取り

キーマトリックスの制御については、Google検索すると記事が見つかるため、そちらをご覧ください。

MCP23017については詳しくは、日本語のデータシートがあるので、こちらを参照ください。

MCP23017データシート

https://ww1.microchip.com/downloads/jp/DeviceDoc/20001952C_JP.pdf

QMK Firmwareでのカスタムキーマトリックスをスキャンのインターフェイス

QMK Firmwareにおいては、カスタムのキーマトリックスを実装するための機能が用意されています。このうち、CUSTOM_MATRIX=liteの機能があり、こちらを利用することにしました。

docs.qmk.fm

liteでは以下の関数のみ実装します。

  • void matrix_init_custom(void)
    • キーマトリックの初期化を行う関数
    • MCP23017や、RP2040のGPIOの設定を行う。
  • bool matrix_scan_custom(matrix_row_t current_matrix)
    • キーマトリックスのスキャンを行う関数
    • 引数current_matrix に行ごとの16bitのビット列が渡される。この各行値を、各列のbitを1(押されいる)/0(押されいない)に設定することで、どのキーが押されているかを渡す。
    • 戻り値に、current_matrix[]に変更がある場合、trueを返す。

一点注意点としては、通常はkeyboard.jsonに"matrix_pins"を設定すると思いますが、カスタムマトリックスliteを有効にすると、この値は無視されます。そのため、MCP23017を使わないRP2040側で行うマトリックススキャンも実装する必要があります。

カスタムマトリックスliteと、I2C通信の有効化

カスタムマトリックスlite

カスタムマトリックスを有効にするには、keyboards/{キーボード名}/rules.mkに以下を記述します。

CUSTOM_MATRIX = lite
SRC += matrix.c

keyboard.jsonの"matrix_pins"で通常は生成される、MATRIX_ROWS、MATRIX_COLSの値をkeyboards/{キーボード名}/config.hに設定します。

#pragma once

#define MATRIX_ROWS 5
#define MATRIX_COLS 14

最後に、スキャンの実装を行うkeyboars/{キーボード名}/matrix.cを追加します。

void matrix_init_custom(void) {
}

bool matrix_scan_custom(matrix_row_t current_matrix[]) {
}

I2C通信の有効化

QMK FirmwareにはI2C通信のための機能があり、こちらを有効化します。

くわしくはQMK Firmwareのドキュメントを参照ください。

docs.qmk.fm

keyboard/{キーボード名}/config.hに以下を追加します。RP2040では、I2C0とI2C1の2つがあります。I2C_DRIVERでどちらを利用するのか指定します(下記はI2C0の例)。さらに、I2Cで利用するピンを設定します。I2C0であったとしてもI2C1_SDA/SCL_PINに設定します。F_SCLはI2Cの通信速度ですが、電子工作レベルでは100kHzくらいが使われます。うまく通信ができない場合、この値を落としてみたり、スキャン時の速度が遅い場合は上げてみるのも手です。ただ、MCUによって使える速度の種類に制限があります。

#define I2C1_SCL_PIN GP13
#define I2C1_SDA_PIN GP12
#define I2C_DRIVER I2CD0
#define F_SCL 100000

keyboard/{キーボード名}/halconf.hを作成し、以下を追加します。

#define HAL_USE_I2C TRUE

keyboard/{キーボード名}/mcuconf.hを作成し、以下を追加します。

#pragma once

#include_next <mcuconf.h>

#undef RP_I2C_USE_I2C0
#define RP_I2C_USE_I2C0 TRUE

#undef RP_I2C_USE_I2C1
#define RP_I2C_USE_I2C1 FALSE

デバッグコンソールを有効にする

QMK Firmwareのロジック実装時は、プリント文によるデバッグをできるようにしておきます。

まず、ログを出力するコンソールを有効化するには、keyboard.jsonのfeaturesにconsoleをtrueに設定します。

{
    // 略
    "features": {
        "bootmagic": true,
        "command": false,
        "console": true, // これ!
        "extrakey": true,
        "mousekey": true,
        "nkro": false
    },
    // 略
}

これで、各コードで#include <print.h>をして、printf()を呼ぶとコンソールに出力されるようになります。

デバッグ用のdprintf()も用意されており、デバッグ時はこちらを利用しておけば、デバッグ終了後もprintf()を削除する必要がなくなります。

dprintf()では、keymap.cにて、以下のフックを実装し、デバッグを有効化しておきます。

void keyboard_post_init_user(void) {
  // Customise these values to desired behaviour
  debug_enable = true;
  debug_matrix = false;
  //debug_keyboard=true;
  //debug_mouse=true;
}

このコンソールに接続して見るには、qmkコマンドを使います。

qmk console

MCP23017の初期化を実装する

matrix.cの実装に入っていきます。

ポイントとして以下の部分です。

  • i2cの初期化として、i2c_init();を呼ぶ。
  • i2c_write_register()で、IODIRA、IODIRBで、各GPIOのインプット・アウトぷっとの方向を設定する
#include "timer.h"
#include "matrix.h"
#include "debug.h"
#include "wait.h"
#include <print.h>
#include "platforms/chibios/gpio.h"
#include "i2c_master.h"

#define MCP23017_I2C_ADDRESS 0x20
#define MCP23017_IODIR_A 0x00
#define MCP23017_IODIR_B 0x01

#ifndef MCP23017_I2C_TIMEOUT
#    define MCP23017_I2C_TIMEOUT 100
#endif

void matrix_init_custom(void) {
    i2c_init();
    wait_ms(10);

    // MCU側のキーマトリックスの方向設定
    // 略

    // GPIOAをInputに設定
    uint8_t      iodir_a  = 0xff;
    status = i2c_write_register(MCP23017_I2C_ADDRESS << 1, MCP23017_IODIR_A, &iodir_a, 1, MCP23017_I2C_TIMEOUT);
    dprintf("set I2C IODIR_A i2c_status_t:%d\n", status);

    // GPIOBをOutputに設定
    uint8_t      iodir_b  = 0x00;
    status = i2c_write_register(MCP23017_I2C_ADDRESS << 1, MCP23017_IODIR_B, &iodir_b, 1, MCP23017_I2C_TIMEOUT);
    dprintf("set I2C IODIR_B i2c_status_t:%d\n", status);
}

i2c_write_registerが、レジスタに書き込む関数になります。

MCP23017のI2Cアドレス(同じI2Cバスに繋がった複数のI2Cスレーブデバイスを、マスター側から指示しわけるためのアドレス)が0x20となっています。ただし、i2c_write_registerにおいては1bitシフトして設定する必要があります。

MCP23017からキーマトリックスのスキャンを実装する

COL2ROWのキーマトリックスのスキャンでは以下を行う必要があります。

  1. COL0をHighにし、それ以外のCOLをLowにする
  2. ROWを読み取る
  3. 1〜2を各COLで繰り返す

各COL/ROWを設定するためのbit値を示す配列として、RIGHT_COL_BITS、RIGHT_ROW_BITSを指定しました。COL0のときはRIGHT_COL_BITS[0]を使う形になります。

uint8_t RIGHT_COL_BITS[] = {1, 1 << 1, 1 << 2, 1 << 3, 1 << 4, 1 << 5, 1 << 6, 1 << 7};
uint8_t RIGHT_ROW_BITS[] = {1, 1 << 1, 1 << 2, 1 << 3, 1 << 4};

なお、私のキーボードではRP2040側のキーマトリックスのGPIOの定義もしています。

uint8_t LEFT_COLS[]      = {GP5, GP6, GP7, GP8, GP9, GP10, GP11};
uint8_t LEFT_ROWS[]      = {GP0, GP1, GP2, GP3, GP4};

わかりにくいのですが、QMK Firmwareにとっては以下となるように私のキーボードでは実装することになります。なので、キーボードにとってのCOLと、MCP23017として処理するCOLがずれることに注意してください。

スキャンは以下のように実装しました。RP2040側のキーマトリックスの処理についてはブログ中では省略します。コードは公開していますので、そちらを参照ください。

#define MCP23017_I2C_ADDRESS 0x20
#define MCP23017_GPIO_A 0x12
#define MCP23017_GPIO_B 0x13

bool matrix_scan_custom(matrix_row_t current_matrix[]) {
    // スキャン後のマトリックスの状態を入れておく
    matrix_row_t scaned_matrix[MATRIX_ROWS];
    memset(scaned_matrix, 0, sizeof(scaned_matrix));

    for (int col = 0; col < MATRIX_COLS; col++) {
        if (col < sizeof(LEFT_COLS)) {
            // MCU側のキーマトリックススキャン
            // 略
        } else {
            // MCP23017側のMatrix

            // COL側、特定のCOLだけHIGHにして、他はLOWにする
            uint8_t      write_buf = RIGHT_COL_BITS[col - sizeof(LEFT_COLS)];
            i2c_status_t status    = i2c_write_register(MCP23017_I2C_ADDRESS << 1, MCP23017_GPIO_B, &write_buf, 1, MCP23017_I2C_TIMEOUT);
            dprintf("write I2C GPIOB i2c_status_t:%d value:0x%02X col:%d\n", status, write_buf, col);

            if (status != I2C_STATUS_SUCCESS) {
                return 0;
            }

            // ROW側、読み取り
            uint8_t read_buf;
            status = i2c_read_register(MCP23017_I2C_ADDRESS << 1, MCP23017_GPIO_A, &read_buf, 1, MCP23017_I2C_TIMEOUT);

            dprintf("read I2C GPIOA i2c_status_t:%d value:0x%02X col:%d\n", status, read_buf, col);


            if (status != I2C_STATUS_SUCCESS) {
                return 0;
            }

            for (int row = 0; row < sizeof(RIGHT_ROW_BITS); row++) {
                // ROWごとに1(ボタンが押された行)があるか確認
                if (read_buf & RIGHT_ROW_BITS[row]) {
                    // このループのCOLのbitを立てる。
                    scaned_matrix[row] |= 1 << col;
                }
            }
        }
    }

    // 更新があったかチェックして、QMK Firmwareにスキャン結果を戻す配列current_matrixを更新する
    bool updated = false;
    for (int row = 0; row < MATRIX_ROWS; row++) {
        if (current_matrix[row] != scaned_matrix[row]) {
            current_matrix[row] = scaned_matrix[row];
            updated             = true;
        }
    }

    return updated;
}

ポイントは以下です。

1 COLごとにループする。 2 COLごとのループで、対象のCOLのみHigh、それ以外はLowとなるように、レジスタGPIOBに書き込む。 3. 2.のあとにROWの各値を読み込むように、レジスタGPIOAを読み込む。読み込んでHighだった場合、スキャン結果の変数scaned_matrix[row]に書き込む。 4. 最後にscanned_matrixと引数current_matrixを比較して、引数current_matrixを更新する。また、更新された場合、戻り値にtrueを返す。

キーマトリクスの処理自体は非常に簡単なため、理屈がわかればそれほど実装量も多くありません。

このコードは以下で公開しています。

github.com

デアッグのポイント

上記コードにもdpritfがたくさん含まれるとおり、プリントデバッグを繰り返しました。

キーマトリックススキャンの度にログを出していると、毎秒100回近く動いているのもあり、結構ログの流量が多くなり、目で追えません。

そのため、私はデバッグログの出力を500msに1度しか行わないように工夫しました。

# ログの出力インターバル
#ifndef CUSTOM_MATRIX_DEBUG_INTERVAL
#    define CUSTOM_MATRIX_DEBUG_INTERVAL 500
#endif

static uint16_t d_timer = 0;

bool matrix_scan_custom(matrix_row_t current_matrix[]) {
    bool debug = false;
    if (timer_elapsed(d_timer) > CUSTOM_MATRIX_DEBUG_INTERVAL) {
        debug   = true;
        d_timer = timer_read();
    }

    if (debug) {
        dprintf("-- matrix_scan_custom start --\n");
    }

    // 略
}

ちゃんと動くか?

デバッグを繰り返し、ちゃんと動くようになりました!! 最近はSparrowDialなど、QMK Firmwareで作り込みをしていたため、その資産がそのまま使えるようになり、久しぶりに分割キーボードを楽しんでいます。

ただ、ファームウェア更新のためにRP2040側にリセットをかけたとき、I2C通信がそれ以降成功しない(ログ上もi2c_status=-1が返っている)ことが3回に1回くらい発生しました。MCP23017側が悪いのか、RP2040側が悪いのかまでは判別できていません。ただ、USBを再接続すると直ります。

RP2040から、I2CのVBUSを瞬断してMCP23017を再起動できるように、MOSFETを追加しても良かったなと思っています。

終わりに

ここのところはSparrow60C、SparrowDialと割れてないキーボードを使っていたのですが、やはり分割キーボードの良さとはまた別のものです。久しぶりに分割キーボードをQMK Firmwareで楽しめています。

Sparrow62v2ではMCP23017を利用していましたが、最近はSparrowTVでもIOエキスパンダとして、CH32V003F4P6にそのような実装をして利用しています。今後は、Sparrow62v3を作る際にはCH32V003F4P6を左右分割機能の要として活用したいと思っています。

builderscon 2024がすごく面白かった

buildersconとは

2024/08/10にbuilderscon2024が5年ぶり開催されました。私の中で、最も好きだったテックカンファレンスだったので、rebootに非常に楽しみにしていました。

buildersconとは、『「知らなかった、を聞く」をテーマとした技術を愛する全てのギーク達のお祭り』とあります。特定の技術にフォーカスせず、しかし特定の技術に深掘りした方々の深淵の世界を覗かせてもらえる「私には知る機会がなかった技術を聞く」時間となっており、非常に面白く思っています。

誰が作成しても 1 つの構造になるモデリング技術

経験に依存しないモデリングを求めてたどり着いた、『Theory of Models(TM)』を解説する講演。

私自身、ソフトウェアアーキテクトを自称していて、事業課題やサービスを様々な形でモデルに落として実装することを日々やっていて、「何をモデル化すると良いのか」や「そのモデルが正しいのか」は非常に議論が難しく、えてして押しつけがましくなったりするので、非常に興味がそそられました。

各プロセスで「経験に依存しないように考慮しているポイント」を拾っているのを聴きながら、自分の開発は複雑さに戦うためにより戦略的DDD目指しているので、ちょっとこれだけでは戦えないよなとか、今までやってたアレはむしろやり過ぎ?とか、考えていました。

紹介したTMを補完する位置づけとして、多数の書籍を紹介していた、それもただの紹介ではなくこの書籍が解説しようとしていることを咀嚼した内容になっていたのも良かったです。モデリング銀の弾丸はなく様々な考えを適応していくことと捉えているのがよくわかって、その一次情報に触れさせてもらえるのは聴者に勉強を促す形になっていて、読まねば!という気持ちになりました。

法律に準拠した本人確認システムを0から作った話

タイトルの通り、B/43というサービスを作った際の、必要とされるドメイン知識、利用した技術とそのドメイン特有の課題、そして実装された運用と、サービス開発の全てが詰まっていた講演。

私自身も、過去マイナンバーが登場時にシステム開発をやっていたことがあり、政府サイトのQ&Aに実務に落とし込んだ設計が公開されているのをウォッチし続けていたのを思い出したりしていて、非常に面白く聞けました。

Amazon Rekognitionで写真の人間の同一性は結構正確に出るとか、今利用できる技術面の話も参考になりました。「今の時代はこれらが使える前提でサービス開発ができる」と認識するのはこういう講演でちゃんとキャッチしたいことです。

また、最後に残る「人による全数チェック運用」の部分も、本人認証システムのドメインからわかるように理由が解説され、どのように運用しているのかを説明されていて、聞き応えがありました。

React Server Component の疑問を解き明かす

今までのSSRなどから続く課題と発展の解説して、React Server Componentの概念にたどり着く講演。

ただ、Server Componentと言われるだけだと、わからない部分を丁寧に解説されていて、理解することができました。

このServer Componentのユースケースがあるのか、既にサーバサイドとして構築している部分のどれだけをServer Componentに移せばいいのか、悩みはつきないなと思いました。

個人的には、React、Next.jsは難しく扱いやすいとは言えないように思っていますが、フロントエンド技術の『概念』の最前線であり、「今実験的ながらもこの『概念』を受け入れるか」と言われると「大規模開発でなければ、常に受け入れるように考えるべき」と考えた結果、Vue.jsよりも積極的に使いたいと思っています。なので、今回『概念』にフォーカスしたセッションは非常にありがたいです。

健やかなサービス運営のための PWG

プロダクト開発チームとインフラチームの共有会 Performance Working Group(PWG)でやっている活動を紹介する講演。

具体的に「今」このような運用をしています、というのを紹介してくれて、課題感があるんでやってみねば!という気にさせられました。

ホー〇ページ・ビルダー 2024

エモ成分多めの、個人Webサイトやるモチベーションや技術に関する講演。

私も近い意識があるので、共感しっぱなしでした。

私のエモ部分を中心に紹介すると、

私も自分のアイデンティティ強化に100円ショップ推しグッズコーナーにあるキーホルダーキットを使って鞄に付けてたりとか。

自分が保守する限りつかる自前短縮URLをつくってたりとか。

自分のやってきたことのコンテンツを確立するために、この"@74thの製作ログ"をつくってるとか。はてなブログいいよねとか。

自分のドメインのWebサイトを、つたないレイアウトながら運営していることに意義を感じているとか。

https://74th.tech/

それでも、アイデンティティを示すページとしては最近は github.com に移行した(つまり、この辺にパッションもって今でもアップデートしている)とか。

github.com

SNSは交流であり、自分の話としてレポートが書きたい気持ちがあるので、ブログ、個人サイトに必要性を感じているとか。

100年コンテンツが残るの意味だと、最近はgithub.comにおいとけば残らないかなー、とか考えてることとか。

CLI ジェネレータ「cyamli」によるコンソールアプリのスキーマ駆動開発

CLIツールを作るときの引数やフラグを、YAMLで定義して、型付きコードを出力させて、型付きで実装でき、かつマルチ言語対応のツール、cyamliの紹介。

4つの自分の要求を満たすツールがなく、作ったという話だけれど、その4つの要求のモチベーションから解説されていたのは良かった。

YAMLでの定義から、型付きコードを生成し、コード補完を活用しながら実装できるのを見せるデモも、実際に型があると言われつつも実際にコード補完が効いているのを見せられると、もうこれのない世界を見たくないという気にさせる魅力があった。

個人的には、話の最初のうちは、「プログラムとユーザのインターフェイスである引数、フラグでやる複雑性はそこまで大きいことは行わず、結局構造的な記述ができるconfig.jsonを定義して渡す形になるのだから、引数、フラグでそんなに頑張ってもなー、言語間に仕様の違いも今やCopilotくんがいい感じに出力してくれるしなー」と思って聞いていたのですが、「サブコマンドに対応している」「実際に定義通りに型補完効いている」というのを見せられて、やられました。CLIツールが許容できる複雑性の意味では「サブコマンド」でかなり満たされるけれど、作るには面倒すぎるところが大きく、それをやってくれるなら使いたくなるじゃないか、と心を動かされました。

1000行で実現する Linux on Browser

1000行で動くRISC-V Linuxを、RISC-VエミュレータをRustで実装することで動かす講演。

40分のコンパクトな講演で、RISC-VのISAとか、それを実際に実装したところとか、Linux起動やコンソール表示のところをやったところとか、一通りが詰まっているのがすごく良く、面白かったです。これ面白いんですよが伝わるパッション多めに話されていたのも、ノリよく聞くことができました。

自作CPUエミュレータに憧れはありましたが、大変そうというイメージが大きくトライしようと思っていませんでしたが、この山を登ってみたい!!!!と心動かされました。

その他感想

家族で一定のコロナピーク時は会食への参加を避けるように決めたため、申し訳ないですが懇親会は参加しませんでした。

今回5年ぶりのrebootで、小規模の1トラックながら、それでも濃い内容の世界ですごく楽しめました!

机のある会場、会場の音響、混み具合も非常に快適で、PCでメモやツイートをみながら、セッションに集中することができました。

過去の参加ログ

過去の参加ブログはこちら(2019書いてなかったことに気づきました、もったいない…)。

74th.hateblo.jp

74th.hateblo.jp

74th.hateblo.jp

最後に

いつかBuildersconに登壇するのが夢なので、来年もあれば必ず参加し、builderを名乗れるネタを温めておきます!

IR、温湿度計などのGroveデバイスをREST風API化して、活用するESP32C3-IoT-Server-Boardを作った

tl;dr

  • 複数のGroveデバイスを接続可能なESP32-C3基板を作って、REST風API化したよ
  • お家のインテリアに合うようにケースも作ったよ
  • IR、温湿度計を付けて、お家の複数箇所にデプロイしたよ
  • Streamlitから便利に制御して使っているよ、マイコンでやるよりも楽そうだよ
  • Boothで販売中、つくまたにも展示するよ

ESP32-C3が安くて便利で使いまくっている

ESP32は非常に安価なWiFi、BLE付きマイコンモジュールですが、その中でも特に安いシリーズであるESP32-C3があります。秋月では310円で販売されています。安価ながらArduinoフレームワークに対応し、IOの数やメモリ、Flashが若干少ないですが、小さい用途であれば十分です。

akizukidenshi.com

加えて、汎用的なUSBデバイスコントローラ機能はありませんが、USB Serialのコントローラを付属しています。これにより、別のUSB Serialを用意しなくても、書き込みを行うことができます。非常に手軽です。

基本戦略、制御処理の多くは自宅サーバ(SBC)に持って行く

我が家には、OrangePi5+を自宅サーバとして使っています(かつてはミニPC(MacMini)を使っていましたが、ラズパイ4の頃からSBCに移りました)。そこではメディアサーバ等と動かしており、いつでもスマホから利用できるサーバリソースとして活用しています。

この背景から、確かに全ての処理をマイコン上のファームウェアで実現すると便利ですが、慣れているサーバサイドプログラミングで行った方が自在にクラウド連携や制御を記述できると思い、マイコン側はシンプルなHTTPサーバとして実現することを思いました。

Firmware設計: ArduinoでREST風APIを実現する

ESP32のArduinoを使って、HTTPサーバを立てるのは非常に簡単できます。このあたりは「アナログ電圧計を使って、ヘッドレスPCのCPU使用率表示を作る」という記事で軽く紹介しました。この記事ではArduinoJsonというライブラリを使って、JSONのパースの処理もしています。

74th.hateblo.jp

これと同じ要領で、REST風APIを設計しました。

  • POST /ir/send : 赤外線リモコンの送信
  • GET /sht31 : 温湿度センサの読み取り

実際に以下のようなデータを/ir/send にPOSTで送ります。

{
  "type": "MITSUBISHI_AC",
  "hex": "0x23CB260100205808164000000000080000F3"
}

すると、ESP32-C3に繋がれたM5 IRモジュールからエアコンの電源を制御できるようになりました。

今回のコード自体は、以下のリポジトリにMITライセンスで置いてあります。

github.com

基板設計: Grove3ポートと、インジケータがほしい

GroveデバイスをつなぐIoTを作るのであればM5Stack製品を用いるのが簡単です。 しかし、画面等を必要としない、電池も使う予定はないため、もっと安価に家中にばら撒きたく、専用の基板を設計することにしました。

  • Groveは3ポート用意し、I2C用のプルアップもできるようにする
  • Groveはデバイスに応じて、5V/3.3Vの電源を接続可能にする
  • インジケータ用のRGB LEDを付ける
  • おまけでボタンを追加しておく
  • 取り付け用の穴を開けておく

以下のものができました。

基板自体は、OSSとして以下で公開しています。

github.com

特に、注意したところとしては以下があります。

  • ESP32のガイドの通り、アンテナエリアの左右のベタを一定以上取り除く

設計に当たり、躓いたところは以下がありました。最終バージョンでは全て解決済みです。

  • USB電源保護ICのCH213Kを入れてみたのですが、電流過多でリセッタブルフィーズが働いてしまうのか期待通り動作せず、CH217Kに変更しました。
  • USB DM/DPのピンをマイコンに直結させていたのですが、安定しませんでした。ダンピング抵抗10Ωを付けたところ、安定しました。データシートにもその指摘がありました。
  • SK6812-MINI-Eに、データシート通りVDDに5Vで接続していたのですが、3.3Vの信号では制御できませんでした。VDDに3.3Vを繋いだところ制御できました。

インテリアになじませるようにケースを作る

このプロダクトは、「お家のそこら中に実際にデプロイする」ことを目的にしていました。そのため、家族に怒られないようにインテリアになじむケースに収めることにしました。

このケースの製作については、別途既に記事にしています。

74th.hateblo.jp

Streamlitから制御する

Streamlitはデータ可視化Webツール作成フレームワークです。本業はデータエンジニアであり、本業でとても活用しているものです。

streamlit.io

StreamlitはPythonで記述し、Pythonのコードで取得したデータをst.write(df)とか書くとそれを表示したり、st.button("OK")とか書くとPythonプログラムを実行するためのボタンが簡単に記述できます。以下は実際に私の家で動作するコードです。

# pages/sample.py
import requests

import streamlit as st

sht31 = requests.get("http://192.168.1.109/sht31").json()
st.text(f"""温度: {sht31["data"]["temperature"]:.1f} ℃""")
st.text(f"""湿度: {sht31["data"]["humidity"]:.1f} %""")

if st.button("TV ON"):
    requests.post("http://192.168.1.104/ir/send", json={"type": "SONY", "hex": "0xA90"})
    st.success("TV ON 信号を送信しました")

基本はこのようなコードを、使いやすいように作り込んでいきます。

エアコン操作画面

Pythonマイコンの外でできることにより、より複雑な命令を記述することができます。たとえば、 以下のようなユースケースを考えています。

  • 室温と一緒に、外気温や、天気をWebAPIから取得して表示する
  • 出かけるので、全てのエアコンをオフにする
  • TVの画面を、TV横PCに変更するため、「チャンネル1」「入力切り替え」「入力切り替え」を順に送る。現在何番目の入力が表示されているかわからないため、「チャンネル1」を介して、入力切り替えで確実に期待の画面を表示できるようにする。

これを構築していきました。

なお、Streamlitは家の外からもアクセスできるように公開サーバになっていますが、Streamlit自体には認証の仕組みはありません。そのため、Google認証がかかるOAuth2 Proxyを介するようにしました。ちなみにこのStreamlit自体はOCIコンテナ化してあり、OrangePi5+に構築したKubernetes(k3s)上で動かしています。このOAuth2 ProxyとKubernetesについては、これを構築したときにQiita記事にしています。

qiita.com

ESP32C3-IoT-Server-Boardがデプロイされた姿

実際に寝室のエアコンにデプロイされている様子を写真に撮りました。

インジケータLED用のケース窓の裏には紙がはってあるため、LEDが眩しすぎることはありません。

既に以下の箇所にデプロイしています。

  • リビングのエアコン
  • 寝室のエアコン
  • 客間のエアコン
  • リビングのTV前

超便利! 応答速度はGoogle Homeには勝てない…

リモコンを探す手間もなく、スマホから操作で対応した家電が操作でき、非常に便利になりました。

Streamlitによって、マイコンだけでは難しかった(面倒だった)制御もすることができました。

ただ、よくよく考えてみると、今まで家電の電源を消すだけであれば「OK Google、リビングのエアコン消して!」と指示していました。この便利さにはなかなか勝てませんね。

このESP32C3-IoT-ServerとケースはBoothにて販売中! 『つくまた』にて展示、販売予定

今回作成したESP32C3-IoT-Serverのキットと、対応ケースはBoothにて販売中です。

74th.booth.pm

また、2024-08-03に開催される『つくまた』でも、展示、販売します!以下のような使い方を想起させる展示も行います。ぜひ寄っていただければと思います。

w.atwiki.jp

次の展望

組み合わせたくなる使いたくなるGroveデバイスを増やしていきたいと思います!

できればGoogle Homeから動かせるようにMatterデバイス化したいですね。