前回の記事の続きとなります。Githubは以下です
キャラクタリスティックのNotify属性のコールバック処理
上のサイトで解説されている通り、キャラクタリスティックには属性があります。属性というと「?」って感じなんですが、通知と表現するとNotifyと被るので属性と表現されます。感覚としては通知みたいなもんです。
- Read
- Write
- Notify
- Indicate
があります(他にもあるかも)。Read,Writeは読み書きする属性、Notify,Indicateはなんらかの通知の属性です。NotifyとIndicateの違いは接続[ESP32]からの応答を要求するのがIndicate、しないのがNotifyとのことです。HIDサービスでキーコードやマウスの移動などの情報はNotifyで通知されてきます。つまりはNotifyを受けった際にコールバック処理をしてデータをマウス・キーボードパーサーに送ればよいのです。
NimBLEのサンプルスケッチでは
上のライブラリのサンプルでは”DEAD”サービスの”BEEF”キャラクタリスティックからのNotifyを受け取ったときにコールバック処理をしています。このコードでは汎用性が悪く、Moke Nakamuraさんのサイトで、その点を修正した解説とコードが掲載されています。とてもありがたいです。
複数のキャラクタリスティックにNotifyのコールバックを登録できるようになっています。これを用いていきます。
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 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 |
//クライアントのプロビジョニングを処理とサーバーとの接続/インターフェースを行う。 bool connectToServer() { NimBLEClient* pClient = nullptr; //再利用できるクライアントの検索 if(NimBLEDevice::getClientListSize()) { pClient = NimBLEDevice::getClientByPeerAddress(advDevice->getAddress()); if(pClient){ //このデバイスをすでに知っている場合は、connect()の第2引数にfalseセットすることで //サービスデータベースを更新しないようにする。これにより、かなりの時間と電力を節約できる if(!pClient->connect(advDevice, false)) { Serial.println("Reconnect failed"); return false; } Serial.println("Reconnected client"); }else { //このデバイスを知っているクライアントがまだない場合、使用できる切断中のクライアントを調べる pClient = NimBLEDevice::getDisconnectedClient(); } } //再利用するクライアントがない場合、新しいクライアントを作成する if(!pClient) { if(NimBLEDevice::getClientListSize() >= NIMBLE_MAX_CONNECTIONS) { Serial.println("Max clients reached - no more connections available"); return false; } pClient = NimBLEDevice::createClient(); Serial.println("New client created"); pClient->setClientCallbacks(&clientCB, false); //初期接続パラメータの設定: これらの設定は、15ms間隔、0ms待ち時間、120msタイムアウト //これらの設定は、3クライアントが確実に接続できる安全な設定です。タイムアウトはインターバルの倍数で、最小は100ms //最小間隔:12 * 1.25ms = 15ms、最大間隔:12 * 1.25ms = 15ms、待ち時間 0ms、タイムアウト51 * 10ms = 510ms pClient->setConnectionParams(12,12,0,51); //接続が完了するまでの待機時間(秒)を設定。デフォルトは30。 pClient->setConnectTimeout(5); if (!pClient->connect(advDevice)) { //クライアントを作成したが、接続に失敗 NimBLEDevice::deleteClient(pClient); Serial.println("Failed to connect, deleted client"); return false; } } if(!pClient->isConnected()) { if (!pClient->connect(advDevice)) { Serial.println("Failed to connect"); return false; } } //接続完了 Serial.print("Connected to: "); Serial.println(pClient->getPeerAddress().toString().c_str()); Serial.print("RSSI: "); Serial.println(pClient->getRssi()); NimBLERemoteService *pSvc = nullptr; //複数扱うためにvectorを使う std::vector<NimBLERemoteCharacteristic*> *pChrs = nullptr; NimBLERemoteDescriptor *pDsc = nullptr; //HIDサービスを取得する pSvc = pClient->getService(serviceUUID); if (pSvc) { //Serial.println("pSvc : HIDサービス取得成功"); // 複数のCharacterisiticsを取得(リフレッシュtrue) pChrs = pSvc->getCharacteristics(true); }else{ //Serial.println("pSvc : HIDサービス取得失敗"); } if (pChrs) { //Serial.println("pChrs : Characterisitics取得成功"); // 複数のReport Characterisiticsの中からNotify属性を持っているものをCallbackに登録する for (int i = 0; i < pChrs->size(); i++) { // Read Write処理部は省略 //registerForNotify()は非推奨となり、subscribe()/unsubscribe()に置き換えられた // Subscribeパラメータのデフォルトは、notifications=true、notifyCallback=nullptr、response=false //接続機器からのcharacteristic変更を通知する属性はNotifyとIndicateのみでコールバックを登録する //Notify属性 if (pChrs->at(i)->canNotify()) { //Serial.printf("SetNotify UUID: %s\n",pChrs->at(i)->getUUID().toString().c_str()); //if (!pChrs->at(i)->registerForNotify(notifyCB)) { if(!pChrs->at(i)->subscribe(true, notifyCB,true)) { //コールバック登録(サブスクライブ)失敗した場合は切断する pClient->disconnect(); return false; } } else //Indicate属性(Noitfyとの違いは接続元[ESP32]からの応答を要求する) if (pChrs->at(i)->canIndicate()) { if(!pChrs->at(i)->subscribe(false, notifyCB,true)) { pClient->disconnect(); return false; } } } }else{ //Serial.println("pChrs : Characterisitics取得失敗"); } Serial.println("Done with this device!"); return true; } |
ちょっと変更したのが、registerForNotifyがdeprecatedになってsubscribeになった点です。
Notifyの場合はnotifications = trueにして、Indicateの場合はfalseにします。第三引数のresponseはNotifyの場合はfalseかなと思ったんですが、デフォルトのfalseではコールバックが呼ばれないのでtrueとしています。このへんは調べましたがわからずじまいでした。IndicateでもNotifyでもtureを指定しています。
コールバックの本体(NotifyCB)です
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 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 |
//通知(Notify)/表示(Indicate)の受信時コールバック void notifyCB(NimBLERemoteCharacteristic* pRemoteCharacteristic, uint8_t* pData, size_t length, bool isNotify){ #ifdef NOTIFY_DEBUG //ここでは文字列処理・ログ出力などの重い処理は遅延の原因となるため注意 std::string str = (isNotify == true) ? "Notification" : "Indication"; str += " from "; // NimBLEAddress and NimBLEUUID have std::string operators str += std::string(pRemoteCharacteristic->getRemoteService()->getClient()->getPeerAddress()); str += ": Service = " + std::string(pRemoteCharacteristic->getRemoteService()->getUUID()); str += ", Characteristic = " + std::string(pRemoteCharacteristic->getUUID()); char st[20]; sprintf(st, ",length = %zd",length); str += std::string(st); //str += ", Value = " + std::string((char*)pData, length); //Hex出力 std::string buf = ""; for(int i=0; i< length ; i++){ sprintf(st,"%02x ",pData[i]); buf += std::string(st); } str += ", Value = " + buf; Serial.println(str.c_str()); #endif //Notifyのキャラクテリスティクを処理 if(pRemoteCharacteristic->getUUID().equals(charUUID)){ //マウスからの通知 if(pRemoteCharacteristic->getRemoteService()->getClient()->getPeerAddress().equals(Mouse_Ad)){ mouse_rpt_parser.Parse(length,pData); return; } //キーボードからの通知 if(pRemoteCharacteristic->getRemoteService()->getClient()->getPeerAddress().equals(Kb_Ad)){ kb_rpt_parser.Parse(length,pData); return; } } } |
Notifyを受信したら、マウス/キーボードのパーサーにデータを渡すだけです。注意点はマウスの通知は更新が激しいので通知が半端なく来ます。ログ出力、文字列比較などの重い文字列処理をしてしまうと処理が追い付かなくなり、マウスがドリフトしてしまいます。当初デバイスのMacアドレス比較を文字列比較で行って激重になって原因追及に時間を取られてしまいました。デバック以外は軽い処理のみにすべきです。パーサーもその点考えて実装すべきです。僕は気づいた時には実装後でしたw。
データ解析とPC-98とのやりとりは各種パーサーが担当します。記事を改めます。