LeapMotion SDK スタートアップ (C API / v4)

昔買って忘れていたLeapMotionが最近発掘されまして、せっかくなので利用手順を記録しつつ使い始めてみようと思います。購入～開封～接続は略。
ここでは素のC言語 SDKである `Leap C API` を扱います。何も考えずに最新バージョン、v4系を使います。

# SDK セットアップ
本家SDKのページ https://developer.leapmotion.com/get-started からダウンロードして解凍、適当なところに置きます。

## サンプルを動かしてみる
SDK内 `LeapSDK/samples` にサンプルコードがあります。`CMakeLists.txt` があるってことはcmakeで一発ですね。UNIXであれば cmake をパッケージマネージャーでインストールなので簡単。
sampleディレクトリ以下で `cmake .` とすれば、MakefileあるいはVisual Studioソリューションが出来上がってくれます。

### Windows環境の場合
Windowsの人は、Visual Studioのプロンプトを開けばcmakeが使えるんではないかと思います。VS 2019用では現時点ではエラーが出たので、VS 2017用のものを以下では利用しています。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/12150/d1c08a80-31d1-0030-3a7b-e91e0b9461f7.png)
(Win+Sで検索開いて x64 って打てば出てくることでしょう)

```
D:\dev\Leap_Motion_Developer_Kit_4.0.0+52173\LeapSDK\samples>cmake .
-- The C compiler identification is MSVC 19.16.27025.1
-- The CXX compiler identification is MSVC 19.16.27025.1
(以下略)
```

Visual Studio 2017 (v141) 用に出来上がっているので、2019で使いたい場合は、ソリューションを開いたあとに、各プロジェクトのプロパティ→全般→ターゲットプラットフォームを v142 に変更するとビルドできます。
実行ファイルは `samples/Debug` あるいは `samples/Release` の下にできます。ソリューションから実行ボタン押すとエラーが出るので焦りますが、ALL_BUILD をターゲットにデバッグ実行しようとしてもできません。対象のサンプルプロジェクトをスタートアッププロジェクトに指定すればいけます。プロジェクトを右クリックすると出てくる下の画像のようなでかいコンテキストメニューの、真ん中辺り。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/12150/fae09386-04c7-b3ec-b43a-07ef44bcacdc.png)

### PollingSample
最低限の機能が動いているのを一番簡単にみられるのが、PollingSampleです。現在の状態をprintし続けるだけのものです。実行して、LeapMotionの上に手をかざすと、検出した手に個別にIDが振られ・3次元座標が取得でき・手の左右が判別できている、という基本能力が確認できます。

```
Frame 332263 with 1 hands.
    Hand id 23 is a left hand with position (-197.825180, 198.359665, 108.724442).
Frame 332264 with 2 hands.
    Hand id 23 is a left hand with position (-192.487244, 194.556442, 106.263969).
    Hand id 24 is a right hand with position (-34.713543, 205.542984, -39.841156).
```

メイン関数のある `PollingSample.c` は、`OpenConnection()` というたった一行のセットアップコードのあと、`GetFrame()`してそれを表示するだけで簡単。

```
    LEAP_TRACKING_EVENT *frame = GetFrame();
    if(frame && (frame->tracking_frame_id > lastFrameID)){
      lastFrameID = frame->tracking_frame_id;
      printf("Frame %lli with %i hands.\n", (long long int)frame->tracking_frame_id, frame->nHands);
      for(uint32_t h = 0; h < frame->nHands; h++){
        LEAP_HAND* hand = &frame->pHands[h];
        printf("    Hand id %i is a %s hand with position (%f, %f, %f).\n",
                    hand->id,
                    (hand->type == eLeapHandType_Left ? "left" : "right"),
                    hand->palm.position.x,
                    hand->palm.position.y,
                    hand->palm.position.z);
      }
    }
```

楽勝！かと思いや、`OpenConnection()`も`GetFrame()`も、`ExampleConnection.c`に記述されている部分です。**これコピペして使いまわせば十分なのでは？**という気もしますが、基本を疎かにすると後で苦労しそうなので、ちゃんと見ておきますか…

## 情報取得スレッド
`GetFrame()`の周りを見てみます。
別スレッドで色々処理した結果を受け渡し用の構造体 `currentFrame ` に突っ込み、GetFrame()では最新の情報が取れるようにしてあります。

```
/** Returns a pointer to the cached tracking frame. */
LEAP_TRACKING_EVENT* GetFrame(){
  LEAP_TRACKING_EVENT *currentFrame;

  LockMutex(&dataLock);
  currentFrame = lastFrame;
  UnlockMutex(&dataLock);

  return currentFrame;
}
```

別スレッドでやっているので同期しない(スレッドの処理1回とmainのループ回数が一致しない)けれども、main側の表示ループでは `frame->tracking_frame_id > lastFrameID` とチェックして、更新されていない場合は多重表示しないようにされていますね。

### メッセージ処理
別スレッドで実行される `serviceMessageLoop` を見ると、`LeapPollConnection` で取得成功した場合はその内容でswitchして、それぞれ処理用の関数に回しているだけです。この`LeapPollConnection`はSDKの関数。ようやくSDKまでたどり着きました。(サンプルコードとしてはSDKの関数をもっと近い位置呼んでほしい気もちょっとする)
処理移譲先の `handleなんとかEvent` いう関数のうち、一番長い `handleDeviceEvent` だけ見ます。ここで、`LeapGetDeviceInfo()`で得られた情報を、 `GetFrame()` で読める場所にコピーしています。ただ、初回必ず失敗したあとリトライするコードになっていますね。(以下コメント意訳を付加)

```
// Start with a length of 1 (pretending we don't know a priori what the length is).
// Currently device serial numbers are all the same length, but that could change in the future
// データの長さがわからんので最初は1をセット
  deviceProperties.serial_length = 1;
  deviceProperties.serial = malloc(deviceProperties.serial_length);
//This will fail since the serial buffer is only 1 character long
// But deviceProperties is updated to contain the required buffer length
// まあ失敗するよね。ただ必要な実際の長さが deviceProperties に得られているので…
  result = LeapGetDeviceInfo(deviceHandle, &deviceProperties);
  if(result == eLeapRS_InsufficientBuffer){
    //try again with correct buffer size
    //実データサイズのバッファとバッファ長をセットしてリトライ
    deviceProperties.serial = realloc(deviceProperties.serial, deviceProperties.serial_length);
    result = LeapGetDeviceInfo(deviceHandle, &deviceProperties);
    // エラー処理省略
  }
```

## セットアップ段階
main関数の最初にある `OpenConnection()` を見ます。`LeapCreateConnection` → `LeapOpenConnection` と呼べばいいだけでした。周りの処理はスレッド作るところ。

```
  if(connectionHandle || LeapCreateConnection(NULL, &connectionHandle) == eLeapRS_Success){
    eLeapRS result = LeapOpenConnection(connectionHandle);
    if(result == eLeapRS_Success){
      _isRunning = true;
#if defined(_MSC_VER)
      InitializeCriticalSection(&dataLock);
      pollingThread = (HANDLE)_beginthread(serviceMessageLoop, 0, NULL);
#else
      pthread_create(&pollingThread, NULL, serviceMessageLoop, NULL);
#endif
    }
  }
```

## 手順まとめ
- `LeapCreateConnection` でデバイスと接続
- `LeapGetDeviceInfo` で情報取得

もうちょっと簡単なサンプルコードに出来るのでは？という気が多少しましたが、`ExampleConnection.c` は全サンプルで共通化されているので、サンプルによっては使わない処理も入っているのですね。


## Gesture ？？？
[Leap C API のドキュメント](https://developer.leapmotion.com/documentation/v4/index.html)を `gesture` とか `circle` とかで検索しても出てきませんね…。なんだか以前の別言語APIドキュメント見ると deprecated なんて書いてあるし。どういうこと。

[Gestures deprecated? · Issue #11 · leapmotion/LeapUnreal](https://github.com/leapmotion/LeapUnreal/issues/11) より

>Yes gestures are fully deprecated with v4 SDK. It is generally recommended to directly interact with objects in the scene instead of using generic recognizers, but if you want implement a swipe recognizer it is fairly easy to do.
>E.g. if you wish to make a right/left swipe recognizer you'd first take the dot product between your view (camera) rotation Right vector and your normalized hand velocity. Divide this by the magnitude of both vectors multiplied together. (以下略)
(意訳：gestureはオワコン。三角関数駆使して自力でやってね。簡単でしょう？)

正気か。SDK側で無難なクオリティが担保されたものを持っていて欲しいし、それが平均的なアプリケーションの質＝LeapMotionの評価向上にも繋がると思うけど。どうしてこうなった。