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については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; };
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の約束事として、接続するものの片方をサーバ、残りをクライアントと呼ぶ。 さて、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, }, };
手抜きの極みのキー処理である。 キーボードからの通知が来るたびに、これが呼び出される。 Handle == 41は modifiers + 10文字分のキーバッファだが Handle == 29は modifiers + res + 6文字分のキーバッファとなっている。 そもそも Handle で見分けるのが正しいのかさえよくわかっていないがとにかく試して動いたキーボードはこのどちらかだった。
動作は単純で、前回渡されたデータと、今回渡ってきたデータとでキーバッファ部分を比べて、新しいのがあったらそれが今回入力されたキーだとみなして、keybufに積んでいく。 これは、例えば、A, B, Cのキーを押しっぱなしにして、新しく D を押したときに、バッファには A, B, C, Dの四文字が入ってくるからである。古いバッファは A, B, Cのみだったので Dが追加で押されたことがわかる。
データは keybufに格納され、あとで、BTKeyBoard::update()が呼び出されるときに処理される。
typedef union { struct __attribute__((__packed__)) { uint8_t modifiers; uint8_t keys[10]; } k1; struct __attribute__((__packed__)) { uint8_t modifiers; uint8_t reserved; uint8_t keys[6]; uint8_t padding[3]; } k2; uint8_t raw[11]; } keyboard_t; static keyboard_t keyboardReport; static std::queue<uint16_t> keybuf; static void notifyCallback(NimBLERemoteCharacteristic *pRemoteCharacteristic, uint8_t *pData, size_t length, bool isNotify) { // handle: 41 -- key / 51 -- media key switch (pRemoteCharacteristic->getHandle()) { case 22: case 29: case 41: if (length != 8 && length != 11) return; // not key data interface (maybe) keyboard_t *newKeyReport = (keyboard_t*)pData; int buflen = 6; uint8_t *buf = keyboardReport.k2.keys; uint8_t *input = newKeyReport->k2.keys; uint8_t mod = newKeyReport->k2.modifiers; if (length == 11) { buflen = 10; buf = keyboardReport.k1.keys; input = newKeyReport->k1.keys; mod = newKeyReport->k1.modifiers; } for (int i = 0 ; i < buflen ; i++) { uint8_t c = input[i]; if (c == 0) continue; if (memchr(buf, c, buflen) == NULL) keybuf.push(((uint16_t)mod << 8)|c); } memcpy(&keyboardReport, pData, length); break; } }
BLEキーボードっていっているのにつながるやつとつながらないやつがありまして、なんでだろうと思っていたのです。 思えば、最初にテストに使った、バッファローのBSKBB335がつながる方のやつだったので、深く考えないで、かつさっさと実装を終えられたのですが。
それでもつながらないのがあるというのが気になります。 BLEの実装に関しては、ペリフェラル側6)に多く、セントラル側7)については、ほぼ、NimBLEのサンプルとその亜種しかないのです。
ハイハイスクールアドベンチャーについても、もちろん、亜種の一つです。
通知可能のCharacteristicは端から notfy()をかけてみても、まったく一つも callbackを返してきません。 悶々とします。
で、gatttool を使ってもう少し掘ってみることにしました。
$ gatttool -I -b xx:xx:xx:xx:xx:xx ... [xx:xx:xx:xx:xx:xx][LE]> char-read-uuid 2a4d Error: Read characteristics by UUID failed: Encryption required before read/write [xx:xx:xx:xx:xx:xx][LE]>
え、これって、セキュアな接続をはらないとダメってことですか?
なんで、コードにごそごそと追加を行います。
pClient = NimBLEDevice::createClient(); Serial.println("A new client created."); pClient->setClientCallbacks(new ClientCallbacks(), false); pClient->setConnectionParams(12, 12, 0, 51); pClient->setConnectTimeout(5); // 5sec if (!pClient->connect(advDevice)) { NimBLEDevice::deleteClient(pClient); Serial.println("Failed to connect."); return false; } if (!pClient->secureConnection()) { Serial.println("Failed to establish secure connection."); return false; }
そうそう。 初期化もちょいと変えてやります。
NimBLEDevice::init(""); NimBLEDevice::setSecurityAuth(true, true, true); // all true if requires secure connection. NimBLEDevice::setPower(ESP_PWR_LVL_P9); NimBLEScan *pScan = NimBLEDevice::getScan(); pScan->setAdvertisedDeviceCallbacks(new AdvertisedDeviceCallbacks()); pScan->setInterval(45); pScan->setWindow(15); pScan->setActiveScan(true); pScan->start(scanTime);
NimBLEDevice::setSecurityAuth()は全部trueにしてやります。 これで無事につながらなかったキーボードがつながるようになりました。
BLEキーボードを接続した状態でしばらく何もしないで放置していると、キーボード側がパワーセーブモードに入って接続が切れます。 クライアント側には onDisconnect() の通知が送られて、接続が切れたことが認識されます。
ここで、デバイスの再検索を始めて、打鍵するなどして復帰したキーボードから Advertiseがやってくればめでたく再接続になります。
NimBLE のサンプルやそれを参考にした多くのコードが、次のような実装をしていたので私もそうしていました。
void ClientCallbacks::onDisconnect(NimBLEClient* pClient) { Serial.print(pClient->getPeerAddress().toString().c_str()); Serial.println(" disconnected - Starting scan"); NimBLEDevice::getScan()->start(BTKeyBoard::scanTime); }
一見すると何の問題もありません。 実際、シリアルコンソールにも disconnected - Starting scan と表示されて、いかにも動いているように見えますが実は全然動いていません。 一向にデバイスが再接続されないのです。 困った。 これでは、一気通貫にハイハイスクールアドベンチャーを最後までプレイするのでなければ、ゲームの途中でにっちもさっちもいかなくなってしまいます。
さて、原因はなんでしょう? もしかすると、一度接続したデバイスからの Advertiseを無視しているのかもしれません。 ならば
NimBLEDevice::getScan()->setDuplicateFilter(true);
とかすればいいのかもしれないと思って試してみました。 再接続はうまくいかないし、そもそも、最初の接続が完了するまで何度も Advertiseのコールバックがかかってきてしまいました。 筋違いのようです。
じゃあ、なんなんだろう?
ふと、callbackの中から NimBLEDevice::getScan()→start(0); とか呼んじゃダメなんじゃないか?ということに気づきました。 これ、つまりコールバック処理が完了しなくなって、おかしなことになっている可能性があるんじゃないかと。
で、次のようにしました。
static volatile bool connected = false; void ClientCallbacks::onDisconnect(NimBLEClient* pClient) { Serial.print(pClient->getPeerAddress().toString().c_str()); Serial.println(" disconnected"); connected = false; }
で、メイン処理の中で、
if (!connected) { Serial.println("Start scan."); NimBLEDevice::getScan()->start(scanTime); }
という処理を足してやりました。
結論。
あっさり再接続しました_ノ乙(、ン、)_
Connect to BLE Keybord. Advertised HID Device found: Name: Notepad8, Address: XX:XX:XX:XX:XX:XX, appearance: 961, manufacturer data: YYYYYYYYYYYYYYYYYYYYYY, serviceUUID: 0x1812 Start connecting to server... A new client created. BLE Device connected. Connected to XX:XX:XX:XX:XX:XX RSSI: -60 Done. XX:XX:XX:XX:XX:XX disconnected Start scan. Advertised HID Device found: Name: Notepad8, Address: XX:XX:XX:XX:XX:XX, appearance: 961, manufacturer data: YYYYYYYYYYYYYYYYYYYYYY, serviceUUID: 0x1812 Start connecting to server... BLE Device connected. Reconnected. Connected to XX:XX:XX:XX:XX:XX RSSI: -62 Done.
キーバッファの仕様は何種類か存在しているようである。
最も多い実装は8bytesで、modifiers + padding + key buffer (6bytes) という構成で、手元ではバッファローのBSKBB335だけが11bytes で modifiers + key buffer (10bytes)というものである。
キーバッファのサイズだとか、多分アラインメントを考慮したであろうパディングの有無や全体長などの差異はあれど、渡ってくる情報(modifiersやキーコード)に違いはない。
が、キーバッファーの部分の扱いには、解釈違いのものがあるようである。
当初、試したキーボード8)では、キーが押されたときには先頭からキーコードが入って送られてきた。 なので、常にキーが押されればキーバッファの先頭にはキーコードがあるという想定だった。
他のキーボードでもそれで問題は特にはなかった。
が、ちょっと色気(?)を出して、シフトキーを押したりコントロールキーを押したりしたときにそれは起きた。
文字が入力されない。
仕方がないので、デバッグです。 調べてみたら、キーは渡ってきてます。 但し、なぜか二文字目で先頭は0です。 どうやら、キーボードによっては、modifiers が押されたのもキーバッファを消費する9)ようです。 なので、0ならそこで打ち切りとしていた処理を、0なら飛ばすように変更しました。
Cardputerは別として、基本的に、M5シリーズ向けハイハイスクールアドベンチャーは320×240の表示装置を前提にデザインしている。 が、しかるのちさんは M5Atomに 240×240のパネルをつないでミニチュアをつくってらっしゃる。
このため表示装置にあわせたスケーリングを行いたい。
基本的に、M5GFXライブラリは、スプライトやバッファを使った画像データをアフィン変換して表示する機能があるので、これを使えばスケーリングできる。
が、問題は、画像バッファはメモリを食うということだ。
最初、ハイハイスクールアドベンチャーはRGB565の256×152のバッファをもって、ここに描画をして表示していた。ほかの部分についてはダイレクトに画面に書き込んで、画面のデータを使って、文字のスクロールなどを実装していた。利点はメモリを使わないで済むこと。
ところがスケーリングをするためにはバッファを持たなければならない。
単純に、文字表示領域を片っ端から createSprite()を使ってバッファリングしていくとあっという間にヒープを使い切ってしまい動作しなくなった。
そこで、画像データをRGB332にしてバッファを半分にし、各スプライトも M5Canvas::setColorDepth(8)でRGB332にしてけちけち実装した。
最後に、ダイアログを同じように8bitのバッファにしたら、ダイアログが表示されなくなってしまった。 どうやら、ヒープ不足でバッファをとれない様子。
もともとダイアログは白黒10)なので、思い切ってM5Canvas::setColorDepth(1)にしたらあっさり動いた。グレーは白になってしまい全く表示されなくなったがまあご愛敬、ということで。
画像用のバッファをRGB332にしたために、幽霊先生のジャケットがMAROONからピンクになってしまった、まるで第二期から第三期に代わったときのルパン三世のように。
これは、ペイント処理の関係で、RGB565–>RGB332–>RGB565と変換して、もとと同じ値に戻る組み合わせでない色はペイントに使えなくなってしまったためだ。この条件を満たしていないと、ペイント処理で境界色や塗った領域の検出ができなくなり移乗動作してしまうためである。
しかるのちさん(@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