文書の過去の版を表示しています。
目次
ハイハイスクールアドベンチャー M5Stack/M5Cardputer版
あらすじ
2019年神奈山県立ハイ高等学校は 地盤が弱く校舎の老朽化も進んだため、 とうとう廃校にする以外方法がなく なってしまった。
ところで大変な情報を手に入れた。 それは、
「ハイ高校にATOMIC BOMBが仕掛けられている。」
と、いうものだ。 どうやらハイ高が廃校になった時、 気が狂った理科の先生がATOMIC BOMBを 学校のどこかに仕掛けてしまったらしい。
お願いだ。我が母校のコナゴナになった 姿を見たくはない。 早くATOMIC BOMBを取り除いてくれ……!!
行動は英語で、“<動詞>” 或いは、“<動詞>”+“<目的語>“のように入れていただきたい。 例えば、”look room”と入れれば部屋の様子を見ることが出来るという訳だ。
それでは Good Luck!!!…………
概要
M5Faces を思い付きで買ったので、キーボードを使う何かを作りたくなって移植を開始しました。 途中でメモリ周りでエラーが多発したため開発を中断1)したものの、M5Unifiedライブラリなる素敵なライブラリがあるらしいことをしって熱が再燃。 今度は最後まで作り切った。 作っている最中に M5Cardputerもリリースされたため、これにも対応を行った。
M5Stack Core (BASIC, Grey, Fire)および Core2、また M5Cardputerに対応しているが、M5Stack Core/Core2については FACESのキーボードモジュールが存在していることが前提であるか、あるいはBLEキーボードが必要である。
FACESに関しては入手困難なようなので、見かけたらゲットされたい。
また、データをmicroSDカードにストアして利用するためmicroSDカードも必須である。
ビルド
本アプリケーションはGitHubにてソースを、こちらにてデータファイルを公開している。
データファイルはmicroSDカードに /HHSAdv というディレクトリを作成し、そこに展開したファイルすべてをコピーする。
ソースは、GitHubから取得して、Platform IOのプロジェクトとしてビルドする。
$ git clone https://github.com/wildtree/hhsadv.git
ビルドにはM5Unified 0.1.12, M5GFX 0.1.12, および M5Cardputer 1.0.3以降が必要になる。 それぞれ、platformio.ini に明記されているので、プロジェクトとして開いたら良しなにやってもらえるはずだ。
ターゲットは、M5Stack-grey, M5Stack-core2, M5Cardputerが用意してあるが、M5Stack-core2はPSRAMを使用するケースを想定して定義してあるものなので使う必要がないため、このターゲットは無視して構わない。2) M5Stack Core2であっても M5Stack-grey でビルドしたバイナリを使用可能である。 M5 Cardputer用は M5Cardputerをターゲットとしてビルドしたものを使用する。 Ver 1.6より m5atomExtDisplay というターゲットが追加されており、M5Atom + 240×240 SPI液晶モジュール + BLEキーボードでのプレイが可能になっている。M5Atom + SPI液晶に関してはしかるのちさん (@shikarunoci)からコードの提供をいただいた。
あれこれ
M5Cardputerについて
M5Cardputerについては240×135ピクセルの1.14インチ液晶を持つが、これはハイハイスクールアドベンチャーの画像データが想定してる256×152ピクセルよりも小さい。
なので、当初は移植対象から除外していたのだが、GFXライブラリーは、バッファの画像をアフィン変換してLCDに転送する機能を持っていると知ったため、後から対象にした。
256×152の画面を縮小しているが、画像についてはおおむね問題ないレベルで表示できていると思う。
問題は、メッセージエリアである。 勿論ここも8×16/16×16のフォントで描画したものをアフィン変換して転送しているのだが、視力に挑戦といったレベルになっている。 頑張れば読めるというレベルで、これを初見でプレイするのは厳しいだろう。
キーボードもSIOで通信しているFACESのキーボードと違い、キーマトリックスがGPIOにもろに露出した形になっているため、GPIOから得たキーマップをキーコードに変換してやらないと使い物にならない。 幸い、このあたりも M5Cardputerのライブラリがまとめて面倒を見てくれているので、アプリを書くにあたっては困らないが、コードに差異が生じるので理解しておかないといけない。
ピンアサインなど
M5シリーズは、ぽいぽいピンアサインが変わるので、このあたりも留意していなければならない。 SDカードやキーボードの割り当てはちゃんと機種を見て動作を変えないといけない部分だ。
例えば、SDカードのマウントは以下のようにしている。 Core/Core2なら 用意されているSPIを使えば動くが、Cardputerはそこもまとめて面倒をみないといけない。
cfg.clear_display = true; M5.begin(cfg); uint8_t ssPin = M5.getPin(m5::pin_name_t::sd_spi_ss); if (M5.getBoard() == m5::board_t::board_M5Cardputer) { M5Cardputer.begin(cfg); spi.begin( M5.getPin(m5::pin_name_t::sd_spi_sclk), M5.getPin(m5::pin_name_t::sd_spi_miso), M5.getPin(m5::pin_name_t::sd_spi_mosi), M5.getPin(m5::pin_name_t::sd_spi_ss) ); } else { spi = SPI; } M5.Display.setRotation(1); Serial.printf("Free heap size: %6d\r\n", esp_get_free_heap_size()); // mount SD (need for M5Unified library) while (false == SD.begin(ssPin /*GPIO_NUM_4*/, spi, 25000000)) { M5.Display.println("SD Wait ..."); delay(500); }
キーボードも Cardputerはさておき、CoreとCore2とではピンアサインが違うため、コードを変えないといけない。 こっちは、KeyBoardという仮想クラスを作っておいてボードによってインスタンスを変えることで対応している。
CoreはWireでINTR=5だが、Core2はWire1でINTR=33だ。
class M5StackKeyBoard : public KeyBoard { protected: public: M5StackKeyBoard() : KeyBoard(m5::board_t::board_M5Stack) { Wire.begin(); pinMode(INTR, INPUT); digitalWrite(INTR, HIGH); } virtual ~M5StackKeyBoard() { Wire.end(); } virtual bool wait_any_key() override; virtual bool fetch_key(uint8_t &c) override; static const int INTR = 5; }; class M5Core2KeyBoard : public KeyBoard { protected: public: M5Core2KeyBoard() : KeyBoard(m5::board_t::board_M5StackCore2) { Wire1.begin(); pinMode(INTR, INPUT); digitalWrite(INTR, HIGH); } virtual ~M5Core2KeyBoard() { Wire1.end(); } virtual bool wait_any_key() override; virtual bool fetch_key(uint8_t &c) override; static const int INTR = 33; };
BTキーボードをつなごう
ESP32を使っているのだから、BTキーボードをつなげられたら、FACESがなくてもハイハイスクールアドベンチャーを遊べる。 BTキーボードをM5 Stackに繋いだ話はしかるのちさんのところにあったので、多分つながる。
問題は、上記の話以外はESP32とBTキーボードで探すと、USBキーボードをESP32を使ってBTキーボードに仕立てる話ばかりで、肝心のBTキーボードをESP32に接続する話は見当たらない。 需要がないのか?
しかるのちさんによれば、esp_8_bit ではBLE3)キーボードがつながらないらしいが、今更作るなら BLEキーボードがつながればいい。 大体が、PlatformIOで引っかかってくるライブラリは NimBLEなので、BLEで行く方がいい。 他人のプロジェクトをぺろっと自分のプロジェクトに張り付けるのはなんともいえない座りの悪さもあるし、あと、esp_8_bitのコードはちょっと気になる部分もあったので、使いたくなかったのだ。
BT(BLE)のお約束
BTの約束事として、接続するものの片方をサーバ、残りをクライアントと呼ぶ。 さて、M5 StackとBLEキーボード、どっちがサーバでどっちがクライアントでしょう?
答えはM5 Stackがクライアントで、BLEキーボード側がサーバになります。 最初、サーバ側のサンプル読んで始めようとしていたので、全然間違ってたわけです。 esp_8_bitで気になるところというのは、hid_serverとかhci_serverとか、サーバ側であるかのような名前を使っているところが気になったのです。
クライアント側はサーバー側の Advertise を拾って、クライアントとして接続をかけに行きます。
接続が成立したら、サーバ側から、Characteristic の一覧をもらって、キーボードの場合なら、キーボード側からの通知を拾ってキーを受け取るという動作になります。
HID_DEVICEのUUIDは 0x1812と決まっています。 これを見つけて接続していきます。
バッテリーの残量やらなんやらを渡してくる Characteristic なんかもあるんですが、それはここでは関係ないので割愛します。 簡単。
キーボードからのデータは HID_REPORT_DATA == 0x2A4D で渡されます。
キーボードの挙動
キーボードが持っている Characteristic については、結構たくさんあるのですが、要は、キーを拾えればいいのです。 調べたところ、手元のキーボードではHandle == 41 で通常のキーが、 Handle == 51でメディアキー4)を受け取るようになっています。
キーボードが返してくる通常キーについては11バイトのデータ構成で、先頭が modifiers 5)で、残り10バイトがキーコードになります。全部使えば10キーロールオーバーになるんでしょうが、多分その辺はキーボードによって違うようです。
キーが押されたり離されたりすると、そのたびに通知がやってきます。
キーが押されたのか離されたのかをどっかのフラグに持っていてくれるとよかったのですがそうではないようです。
キーが modifiers 以外には一文字ずつしか押されないと仮定すると、キーが押されたときにはキーコードが渡され、キーが離されると0 がデータとしてわたってきます。
Nキーが同時に押されているなら、押されている全部のキーのキーコードが入ってくるのですが、離されると離されたキーの値が0になって残りのキーコードは入ったままになってきます。 この時、0でないキーコードは先頭側に詰められてきます。
真面目にドライバーを書くなら、いちいちキーコードがどう変化したのかを記録してやらないとダメそうですが、基本的に複数キーが押されることはないだろうハイハイスクールアドベンチャーなら、あんまり真剣に考えないでも良さそうです。
0じゃない文字数を数えて、前回より増えていたら新しくキーが押されたとして扱います。 本当はこれだと色々具合が悪いのですが、今回はよしとします。 まあキーリピートもききませんが、今回はまあいいでしょう。
新たにキーが押されたと判定したら、渡されてきたバッファのキーコードを modifiersを考慮して、対応するASCIIコードに変換してキューに格納し、ゲーム側から求められたら先頭から順に渡していきます。
この辺も手抜きで、modifiersについてはCTRLが押されていたらそれを優先してあとはSHIFTしか考慮していません。AltやCmdは無視しています。
とりあえずこれでBTキーボードが使えるようになりました。 ゲームには支障はない感じですが、アクションげーんとかを考えてる人はもう少し真剣に考えないといけないと思います。
コード片
ハイハイスクールアドベンチャーはキーボード周りを KeyBoardクラスの派生クラスとして扱っている。 M5 Stackグレー, M5 Stack Core2, Cardputer 用のキーボードクラスがそれぞれ定義されているが、ここにBTキーボードクラスも追加する。
但し、NimBLEのAPIは通知を受け取る notify callback 関数を渡さないといけないので、完全にこのクラス内に閉じることができない。
なので、keyboard.cpp 内に閉じた関数や変数にそこそこ依存している。
変換テーブル
キーコードの変換テーブルは英字配列を想定している。 0~95の96種類のキーコードを扱えるが、抜けているところは手元のキーボードにはないキーである。 三組定義されていて、modifiersなし、CTRL、SHIFTの組み合わせになっている。 JIS配列に対応させたい場合はこれを書き換えることになる。
const uint8_t BTKeyBoard::_keymap[][96] = { { 0, 0, 0, 0, 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', '1', '2', '3', '4', '5', '6', '7', '8', '9', '0', 13, 27, 8, 9, ' ', '-', '=', '[', ']','\\', 0, ';','\'', '`', ',', '.', '/', 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 127, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, }, { 0, 0, 0, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, }, { 0, 0, 0, 0, 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', '!', '@', '#', '$', '%', '^', '&', '*', '(', ')', 13, 27, 8, 9, ' ', '_', '+', '{', '}', '|', 0, ':', '"', '~', '<', '>', '?', 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 127, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, }, };
通知処理
手抜きの極みのキー処理である。 キーボードからの通知が来るたびに、これが呼び出される。
データは keyboardReport に格納され、あとで、BTKeyBoard::update()が呼び出されるときに処理される。
typedef struct __attribute__((__packed__)) { uint8_t modifiers; uint8_t keys[10]; } keyboard_t; static keyboard_t keyboardReport; static bool newKeyData = false; static void notifyCallback(NimBLERemoteCharacteristic *pRemoteCharacteristic, uint8_t *pData, size_t length, bool isNotify) { // handle: 41 -- key / 51 -- media key if (pRemoteCharacteristic->getHandle() == 41) { keyboard_t *newKeyReport = (keyboard_t*)pData; for (int i = 0 ; i < 10 ; i++) { uint8_t c = newKeyReport->keys[i]; if (c == 0) break; if (memchr(&keyboardReport, c, 10) == NULL) keybuf.push((((uint16_t)newKeyReport->modifiers) << 8)|c); } memcpy(&keyboardReport, pData, sizeof(keyboardReport)); } }
画面のスケーリング
Cardputerは別として、基本的に、M5シリーズ向けハイハイスクールアドベンチャーは320×240の表示装置を前提にデザインしている。 が、しかるのちさんは M5Atomに 240×240のパネルをつないでミニチュアをつくってらっしゃる。
このため表示装置にあわせたスケーリングを行いたい。
基本的に、M5GFXライブラリは、スプライトやバッファを使った画像データをアフィン変換して表示する機能があるので、これを使えばスケーリングできる。
が、問題は、画像バッファはメモリを食うということだ。
最初、ハイハイスクールアドベンチャーはRGB565の256×152のバッファをもって、ここに描画をして表示していた。ほかの部分についてはダイレクトに画面に書き込んで、画面のデータを使って、文字のスクロールなどを実装していた。利点はメモリを使わないで済むこと。
ところがスケーリングをするためにはバッファを持たなければならない。
単純に、文字表示領域を片っ端から createSprite()を使ってバッファリングしていくとあっという間にヒープを使い切ってしまい動作しなくなった。
そこで、画像データをRGB332にしてバッファを半分にし、各スプライトも M5Canvas::setColorDepth(8)でRGB332にしてけちけち実装した。
最後に、ダイアログを同じように8bitのバッファにしたら、ダイアログが表示されなくなってしまった。 どうやら、ヒープ不足でバッファをとれない様子。
もともとダイアログは白黒6)なので、思い切ってM5Canvas::setColorDepth(1)にしたらあっさり動いた。グレーは白になってしまい全く表示されなくなったがまあご愛敬、ということで。
副作用
画像用のバッファをRGB332にしたために、幽霊先生のジャケットがMAROONからピンクになってしまった、まるで第二期から第三期に代わったときのルパン三世のように。
これは、ペイント処理の関係で、RGB565–>RGB332–>RGB565と変換して、もとと同じ値に戻る組み合わせでない色はペイントに使えなくなってしまったためだ。この条件を満たしていないと、ペイント処理で境界色や塗った領域の検出ができなくなり移乗動作してしまうためである。
M5ATOM + 外部ディスプレイ
しかるのちさん(@shikarunoci)から M5ATOMに、240×240のSPI液晶モジュールを接続して遊ぶためのソースの変更分をいただき、これを本体にマージしました。
液晶は、SPI接続で、env:m5atomExtDisplay でビルドする。 接続は次の通り。
液晶 | M5ATOM |
---|---|
VCC | 3V3 |
GND | GND |
SLC | G23 |
SDA | G33 |
RES | G19 |
DC | G22 |
CS | - |
データファイルはPlatformIOのUpload Filesystem Image メニューでSPIFFSに転送しておく。 この時、ビルド環境をARM64上に構築していると、mkspiffs コマンドがないため失敗する。 その場合自力でビルドして置き換えておく。
$ git clone --recursive https://github.com/igrr/mkspiffs.git $ cd mkspiffs $ make clean $ make dist BUILD_CONFIG_NAME="-arduino-esp32" CPPFLAGS="-DSPIFFS_OBJ_META_LEN=4" $ cp mkspiffs ~/.platformio/packages/tool-mkspiffs/mkspiffs_espressif32_arduino