ユーザ用ツール

サイト用ツール


ハイハイスクールアドベンチャー_m5stack_m5cardputer版

差分

このページの2つのバージョン間の差分を表示します。

この比較画面へのリンク

両方とも前のリビジョン前のリビジョン
次のリビジョン
前のリビジョン
ハイハイスクールアドベンチャー_m5stack_m5cardputer版 [2024/10/29 02:27] – [BTキーボードをつなごう] arakiハイハイスクールアドベンチャー_m5stack_m5cardputer版 [2025/04/25 01:56] (現在) – [BTキーボードをつなごう] araki
行 47: 行 47:
 ソースは、GitHubから取得して、Platform IOのプロジェクトとしてビルドする。 ソースは、GitHubから取得して、Platform IOのプロジェクトとしてビルドする。
  
-<code>+<code bash>
 $ git clone https://github.com/wildtree/hhsadv.git $ git clone https://github.com/wildtree/hhsadv.git
 </code> </code>
行 86: 行 86:
 Core/Core2なら 用意されているSPIを使えば動くが、Cardputerはそこもまとめて面倒をみないといけない。 Core/Core2なら 用意されているSPIを使えば動くが、Cardputerはそこもまとめて面倒をみないといけない。
  
-<code>+<code cpp>
     cfg.clear_display = true;     cfg.clear_display = true;
     M5.begin(cfg);     M5.begin(cfg);
行 119: 行 119:
 CoreはWireでINTR=5だが、Core2はWire1でINTR=33だ。 CoreはWireでINTR=5だが、Core2はWire1でINTR=33だ。
  
-<code>+<code cpp>
 class M5StackKeyBoard : public KeyBoard class M5StackKeyBoard : public KeyBoard
 { {
行 233: 行 233:
 JIS配列に対応させたい場合はこれを書き換えることになる。 JIS配列に対応させたい場合はこれを書き換えることになる。
  
-<code>+<code cpp>
 const uint8_t BTKeyBoard::_keymap[][96] = { const uint8_t BTKeyBoard::_keymap[][96] = {
     {    0,   0,   0,   0, 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l',     {    0,   0,   0,   0, 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l',
行 267: 行 267:
 キーボードからの通知が来るたびに、これが呼び出される。 キーボードからの通知が来るたびに、これが呼び出される。
 Handle == 41は modifiers + 10文字分のキーバッファだが Handle == 29は modifiers + res + 6文字分のキーバッファとなっている。 Handle == 41は modifiers + 10文字分のキーバッファだが Handle == 29は modifiers + res + 6文字分のキーバッファとなっている。
-そもそも Handle で見分けるのが正しいのかさえよくわかっていないがとにかく試して動いたキーボードはこのどちらかだった。+<del>そもそも Handle で見分けるのが正しいのかさえよくわかっていないがとにかく試して動いたキーボードはこのどちらかだった。</del> 
 +実はキーボードからのキー入力の通知についてはハンドルで見分けることができる。 
 + 
 +btstack でBLEキーボードをつないだ時に知ったのだけれど、Notification で渡されるパケットには実は二種類のハンドルが入っている。 
 +一つが attribute handle で、もう一つが value handleだ。 
 + 
 +私が調べたところ、attribute handle == 0x0040 のものがキーボードからのキー入力の通知になる。 
 +なので、btstack ではそのようにしてふるっているし、NimBLEでもそうするべきだろうと思ったのだが、NimBLERemoteCharacteristic クラスは、value_handleしか取得できない。 
 + 
 +value_handleの方はキーボードによってまちまちの値を返すので、使い物にならない。 
 +  
 +どうにかして attribute handle を取り出せないものかと NimBLEのコードを掘っていったら、あろうことか、attribute_handleの方は中で捨ててた。 
 + 
 +よって、NimBLEでは Notification Event Handlerの中でハンドルで処理を分けるやり方はできない、というのが結論である
  
 動作は単純で、前回渡されたデータと、今回渡ってきたデータとでキーバッファ部分を比べて、新しいのがあったらそれが今回入力されたキーだとみなして、keybufに積んでいく。 動作は単純で、前回渡されたデータと、今回渡ってきたデータとでキーバッファ部分を比べて、新しいのがあったらそれが今回入力されたキーだとみなして、keybufに積んでいく。
行 275: 行 288:
  
  
-<code>+<code cpp>
 typedef union { typedef union {
     struct __attribute__((__packed__))     struct __attribute__((__packed__))
行 300: 行 313:
 { {
     // handle: 41 -- key / 51 -- media key     // handle: 41 -- key / 51 -- media key
-    if (pRemoteCharacteristic->getHandle() == 41)+    switch (pRemoteCharacteristic->getHandle())
     {     {
-        keyboard_t *newKeyReport = (keyboard_t*)pData; +        case 22: 
-        for (int i = 0 ; i < 10 ; i++)+        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; 
 +    } 
 +
 +</code> 
 + 
 +== つながるキーボードつながらないキーボード == 
 + 
 +BLEキーボードっていっているのにつながるやつとつながらないやつがありまして、なんでだろうと思っていたのです。 
 +思えば、最初にテストに使った、バッファローのBSKBB335がつながる方のやつだったので、深く考えないで、かつさっさと実装を終えられたのですが。 
 + 
 +それでもつながらないのがあるというのが気になります。 
 +BLEの実装に関しては、ペリフェラル側((サーバー側))に多く、セントラル側((クライアント側))については、ほぼ、NimBLEのサンプルとその亜種しかないのです。 
 + 
 +ハイハイスクールアドベンチャーについても、もちろん、亜種の一つです。 
 + 
 +通知可能のCharacteristicは端から notfy()をかけてみても、まったく一つも callbackを返してきません。 
 +悶々とします。 
 + 
 +で、gatttool を使ってもう少し掘ってみることにしました。 
 + 
 +<code bash> 
 +$ 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]> 
 +</code> 
 + 
 +え、これって、セキュアな接続をはらないとダメってことですか? 
 + 
 +なんで、コードにごそごそと追加を行います。 
 + 
 +<code cpp> 
 +        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))
         {         {
-            uint8_t c = newKeyReport->k1.keys[i]; +            NimBLEDevice::deleteClient(pClient); 
-            if (c == 0break+            Serial.println("Failed to connect.")
-            if (memchr(&keyboardReport, c, 10) == NULL) keybuf.push((((uint16_t)newKeyReport->k1.modifiers) << 8)|c);+            return false;
         }         }
-        memcpy(&keyboardReport, pData, sizeof(keyboardReport)); +        if (!pClient->secureConnection())
-    } +
-    else if (pRemoteCharacteristic->getHandle() == 29) +
-    { +
-        keyboard_t *newKeyReport = (keyboard_t*)pData; +
-        if(newKeyReport->k2.modifiers == 3) return; +
-        for (int i = 0 ; i < 6 ; i++)+
         {         {
-            uint8_t c = newKeyReport->k2.keys[i]; +            Serial.println("Failed to establish secure connection."); 
-            if (c == 0break+            return false;
-            if (memchr(&keyboardReport, c, 10) == NULL) keybuf.push((((uint16_t)newKeyReport->k2.modifiers) << 8)|c);+
         }         }
-        memcpy(&keyboardReportpDatasizeof(keyboardReport));        +</code> 
 + 
 +そうそう。 
 +初期化もちょいと変えてやります。 
 + 
 +<code cpp> 
 +    NimBLEDevice::init(""); 
 +    NimBLEDevice::setSecurityAuth(truetruetrue); // all true if requires secure connection. 
 +    NimBLEDevice::setPower(ESP_PWR_LVL_P9)
 +    NimBLEScan *pScan = NimBLEDevice::getScan(); 
 +    pScan->setAdvertisedDeviceCallbacks(new AdvertisedDeviceCallbacks()); 
 +    pScan->setInterval(48); 
 +    pScan->setWindow(48); 
 +    pScan->setActiveScan(true); 
 +    pScan->start(scanTime); 
 +    while (pScan->isScanning())  // NimBLE 2.xからは start()が非同期になった様子。 
 +    { 
 +        sleep(100);
     }     }
 +</code>
 +
 +NimBLEDevice::setSecurityAuth()は全部trueにしてやります。
 +これで無事につながらなかったキーボードがつながるようになりました。
 +
 +== キーボードが再接続できない ==
 +
 +BLEキーボードを接続した状態でしばらく何もしないで放置していると、キーボード側がパワーセーブモードに入って接続が切れます。
 +クライアント側には onDisconnect() の通知が送られて、接続が切れたことが認識されます。
 +
 +ここで、デバイスの再検索を始めて、打鍵するなどして復帰したキーボードから Advertiseがやってくればめでたく再接続になります。
 +
 +NimBLE のサンプルやそれを参考にした多くのコードが、次のような実装をしていたので私もそうしていました。
 +
 +<code cpp>
 +void
 +ClientCallbacks::onDisconnect(NimBLEClient* pClient)
 +{
 +    Serial.print(pClient->getPeerAddress().toString().c_str());
 +    Serial.println(" disconnected - Starting scan");
 +    NimBLEDevice::getScan()->start(BTKeyBoard::scanTime);
 } }
 +</code>
  
 +一見すると何の問題もありません。
 +実際、シリアルコンソールにも  disconnected - Starting scan と表示されて、いかにも動いているように見えますが実は全然動いていません。
 +一向にデバイスが再接続されないのです。
 +困った。
 +これでは、一気通貫にハイハイスクールアドベンチャーを最後までプレイするのでなければ、ゲームの途中でにっちもさっちもいかなくなってしまいます。
 +
 +さて、原因はなんでしょう?
 +もしかすると、一度接続したデバイスからの Advertiseを無視しているのかもしれません。
 +ならば <code cpp>NimBLEDevice::getScan()->setDuplicateFilter(true);</code>とかすればいいのかもしれないと思って試してみました。
 +再接続はうまくいかないし、そもそも、最初の接続が完了するまで何度も Advertiseのコールバックがかかってきてしまいました。
 +筋違いのようです。
 +
 +じゃあ、なんなんだろう?
 +
 +ふと、callbackの中から NimBLEDevice::getScan()->start(0); とか呼んじゃダメなんじゃないか?ということに気づきました。
 +これ、つまりコールバック処理が完了しなくなって、おかしなことになっている可能性があるんじゃないかと。
 +
 +で、次のようにしました。
 +
 +<code cpp>
 +static volatile bool connected = false;
 +
 +void
 +ClientCallbacks::onDisconnect(NimBLEClient* pClient)
 +{
 +    Serial.print(pClient->getPeerAddress().toString().c_str());
 +    Serial.println(" disconnected");
 +    connected = false;
 +}
 </code> </code>
 +
 +で、メイン処理の中で、
 +
 +<code cpp>
 +    if (!connected)
 +    {
 +        Serial.println("Start scan.");
 +        NimBLEDevice::getScan()->start(scanTime);
 +    }
 +</code>
 +
 +という処理を足してやりました。
 +
 +結論。
 +
 +あっさり再接続しました_ノ乙(、ン、)_
 +
 +<code>
 +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.
 +</code>
 +
 +== キーバッファ ==
 +
 +キーバッファの仕様は何種類か存在しているようである。
 +
 +最も多い実装は8bytesで、modifiers + padding + key buffer (6bytes) という構成で、手元ではバッファローのBSKBB335だけが11bytes で modifiers + key buffer (10bytes)というものである。
 +
 +キーバッファのサイズだとか、多分アラインメントを考慮したであろうパディングの有無や全体長などの差異はあれど、渡ってくる情報(modifiersやキーコード)に違いはない。
 +
 +が、キーバッファーの部分の扱いには、解釈違いのものがあるようである。
 +
 +当初、試したキーボード((BSKBB335/NimBLE版 Cardputer BT キーボード))では、キーが押されたときには先頭からキーコードが入って送られてきた。
 +なので、常にキーが押されればキーバッファの先頭にはキーコードがあるという想定だった。
 +
 +他のキーボードでもそれで問題は特にはなかった。
 +
 +が、ちょっと色気(?)を出して、シフトキーを押したりコントロールキーを押したりしたときにそれは起きた。
 +
 +文字が入力されない。
 +
 +仕方がないので、デバッグです。
 +調べてみたら、キーは渡ってきてます。
 +但し、なぜか二文字目で先頭は0です。
 +どうやら、キーボードによっては、modifiers が押されたのもキーバッファを消費する((ただしよりにもよってキーコードは0))ようです。
 +なので、0ならそこで打ち切りとしていた処理を、0なら飛ばすように変更しました。
 +
 +== NimBLE 2.2.3 ==
 +
 +NimBLEが1.x系から2.x系になりました。
 +メモリの使用量などが改善しているという触れ込みなので、メモリが余裕というわけではないESP32環境では乗り換えないという選択肢はないでしょう。
 +
 +移行の大筋は[[https://github.com/h2zero/NimBLE-Arduino/blob/master/docs/1.x_to2.x_migration_guide.md|Migrating from 1.x to 2]]あたりを参照しながらやればいいでしょう。
 +
 +大きな変更としては、ポインタ渡しだったものが参照渡しや値渡しになっていたり、Advertiseの Callbackを受けるクラスが NimBLEScanCallbacksになっていたりするのと、NimBLEClientCallbacsのイベントハンドラの名前や引数がちょいちょい変わっているのと、あとは時間に関する指定が、sec だったり msだったりしてたのが msに統一されたというあたりでしょうか。
 +
 +まあ、intellisenseが効いている VSCode で編集していれば、変更はほぼ機械的に、赤くなっているエラー行を入れ替えていって、あとは時間に関する値の見直しを入れていくくらいでしょうか?
 +
 +わたしは、NimBLEClient::setConnectTimeout()の値を修正し忘れて((1.xではsec指定だったのが 2.xで ms指定に変わっているので1000倍しないとダメ。))接続、即切断を繰り返して動かないというのをやらかしました。
 +
 +ただ、本当に厄介なのはそういう部分じゃなくて、同じな態でいるのに、実は挙動が違うという奴です。
 +具体的には **NimBLEDevice::getScan()->start(scanTime)**です。
 +
 +1.xでは、この関数を呼び出すと、Advertiseを拾って、stopがかかるまでここでブロックされていました。((だったはずです。キーボードが接続待ちになるまでここで止まっていたので。))
 +
 +ところが、2.xではこの関数は即座に戻ってきて処理を続行し、裏で待ち受け処理を行うようになっていました。
 +
 +何が問題なのかというと、Scan Stopがかかって、接続可能な状態になっている前提で組まれているコードが、Scan中なのにやってきて、しかもまだデバイスが見つかっていないので、管理用のステータスが「未接続」となっているものだから、別のところから再び scan をかけようとするのです。
 +
 +1.xでどうだったかは知りませんが、2.xではこの状況になるとデフォルトでは先行するスキャンでやりかかっていた処理やデータはクリアされて最初からやり直しをするようになっています。
 +
 +この時、先行するスキャンがデバイスを見つけて、データに触り始めている状態だと、触っているデータがクリアされてしまい例外を吐いて落ちてしまうのです。
 +
 +当然、この時どこを触っているのかはその時々で違うので、backtrace を手繰ってもその都度違うところで例外が出るので最初原因が何なのかわからずに大変困惑しました。
 +
 +対処として、start()をかけうるコードの前に、NimBLEDevice::getScan()->isScanning() のチェックを入れて、スキャン中に再スキャンを開始しないようにしてあります。
 +
 +また、初期化部分では、スキャンが終わるまでループするようにも修正しました。
 +
 +
  
 ==== 画面のスケーリング ==== ==== 画面のスケーリング ====
行 379: 行 608:
 その場合自力でビルドして置き換えておく。 その場合自力でビルドして置き換えておく。
  
-<code>+<code bash>
 $ git clone --recursive https://github.com/igrr/mkspiffs.git $ git clone --recursive https://github.com/igrr/mkspiffs.git
 $ cd mkspiffs $ cd mkspiffs
ハイハイスクールアドベンチャー_m5stack_m5cardputer版.1730168827.txt.gz · 最終更新: 2024/10/29 02:27 by araki