RFID リーダを制御する Low Level Reader Protocol(LLRP) について

# はじめに
LLRP は RFIDリーダのネットワークインターフェースを標準化したプロトコルです.
仕様や詳細については下記のリンクを参照してください.
[LLRP Toolkit](http://llrp.org/)
[LLRP仕様](https://www.gs1.org/standards/epc-rfid/llrp/1-1-0)

タグのメモリ構造などは下記を参考にしてください
[GS1 EPC/RFID標準](https://www.dsri.jp/standard/epc/epcrfidstandards.html)
[RFIDタグの構造について](https://asreader.jp/blog/rf%E3%82%BF%E3%82%B0%E3%81%AE%E6%A7%8B%E9%80%A0%E3%81%AB%E3%81%A4%E3%81%84%E3%81%A6/)

似た例(大体, 同じ例)が　Impinj Support にもあります.
[Hello LLRP (Low-Level Reader Protocol)](https://support.impinj.com/hc/en-us/articles/202756168-Hello-LLRP-Low-Level-Reader-Protocol-)
## 環境
| | 名称 | Version | 備考 |
| --- | --- | --- | --- |
| 言語 | C# | 9.0 | |
| ライブラリ | libltknet-sdk | 10.46.0 | [libltknet-sdk](https://www.nuget.org/packages/libltknet-sdk/)(LLRP) |


# タグIDの取得
リーダに対して読込命令を発行し, 周辺にある RFID の EPC を取得します.

## リーダへの接続
下記のコードでは, IPv4 アドレス `192.168.100.64` のリーダに対して `5084(LLRP)` で接続を行います.
既に何かしらのプログラムが LLRP に接続している場合は, エラーが発生します.

暗号化された通信を使用する場合は, ポート番号を `5085`, `useTLS = true` に変更します.

```C#
using var client = new LLRPClient(port: 5084);
ENUM_ConnectionAttemptStatusType status =
  ENUM_ConnectionAttemptStatusType.Success;

var isSuccessful = client.Open(
    llrp_reader_name: "192.168.64.101",
    useTLS:            false,
    timeout:           3000,
    status:            out status);
if(! isSuccessful ||
    status != ENUM_ConnectionAttemptStatusType.Success) {
  return;
}

try { DeleteROSpec(client); } catch { }

client.OnRoAccessReportReceived += OnLLRPClientRoAccessReportReceived;

ResetToFactoryDefault(client);
EnableImpinjExtensions(client);

uint roSpecId = 1000;
AddROSpec(client, roSpecId);
EnableROSpec(client, roSpecId);
StartROSpec(client, roSpecId);

await Task.Delay(3000);

StopROSpec(client, roSpecId);
DisableROSpec(client, roSpecId);
DeleteROSpec(client, roSpecId);
```

### レポートイベント

```C#
private static void OnLLRPClientRoAccessReportReceived(MSG_RO_ACCESS_REPORT? report) {
  if(report == null ||
      report.TagReportData == null ||
      report.TagReportData.Length == 0) {
    return;
  }

  foreach(var data in report.TagReportData) {
    // CRC, PC Bits
    ushort? crc = null, pcBits = null;
    for(var idx = 0; idx < data.AirProtocolTagData.Length; ++idx) {
      switch(data.AirProtocolTagData[idx]) {
        case PARAM_C1G2_CRC _crc:
          crc = _crc.CRC;
          break;
        case PARAM_C1G2_PC _pc:
          pcBits = _pc.PC_Bits;
          break;
      }
    }

    // 受信感度, 検出角度
    double? peakRssi = null, phaseAngle = null;
    for(var idx = 0; idx < data.Custom.Length; ++idx) {
      switch(data.Custom[idx]) {
        case PARAM_ImpinjPeakRSSI _peakRssi:
          peakRssi = (double)_peakRssi.RSSI / 100d;
          break;

        case PARAM_ImpinjRFPhaseAngle _phaseAngle:
          phaseAngle = ((double)_phaseAngle.PhaseAngle / 4096d) * 360d;
          break;
      }
    }
    if(peakRssi == null) {
      peakRssi = (double?)data.PeakRSSI?.PeakRSSI;
    }

    ushort antennaId = data.AntennaID.AntennaID;
    // EPC
    var epc = data.EPCParameter[0] switch {
      PARAM_EPCData _epc => _epc.EPC.ToHexString(),
      PARAM_EPC_96  _epc => _epc.EPC.ToHexString(),
      _ => throw new NotSupportedException(),
    };

    Console.Error.WriteLine($"{crc:X4} {pcBits:X4} {antennaId} {epc} {peakRssi:f2} {phaseAngle:f2}");
  }
}
```

### 任意: リーダの初期化
リーダのROSpec, AccessSpec 等操作に関係する設定を初期化します.

```C#
private static void ResetToFactoryDefault(
    LLRPClient client,
    int        timeout = 3000) {
  var msg = new MSG_SET_READER_CONFIG() {
    ResetToFactoryDefault = true
  };

  MSG_ERROR_MESSAGE? err = null;
  var resp = client.SET_READER_CONFIG(
      msg:      msg,
      msg_err:  out err,
      time_out: timeout);

  LLRPHelper.Check(resp, err);
}
```

### 任意: Impinj 拡張機能の有効化
接続対象のリーダが Impinj 社製の場合にのみ有効です. 下記コードを実行することで, リーダの拡張機能の設定が行なえます.
ここで示している例では, タグ検出時のレポートに, 0.5[dBm] 間隔の受信感度と検出時のタグの角度を追加するために拡張機能を有効化しています.

```C#
private static void EnableImpinjExtensions(
    LLRPClient client,
    int        timeout = 3000) {
  var msg = new MSG_IMPINJ_ENABLE_EXTENSIONS();
  MSG_ERROR_MESSAGE? err = null;

  var resp = client.CUSTOM_MESSAGE(
      msg:      msg,
      msg_err:  out err,
      time_out: timeout);

  LLRPHelper.Check(resp, err);
}
```

## Reader Operation (ROSpec) の追加
Impinj 社製のリーダは 1つの ROSpec に対応していますが, リーダによっては複数の ROSpec を追加できるかもしれません.(そのようなリーダに対応したことがないため)
そのため, 追加する ROSpec のみを対象に操作を行い場合, ROSpec を削除をすることをおすすめします. 削除方法については後述します.

`MSG_ADD_ROSPEC` を宣言.

```C#
var msg = new MSG_ADD_ROSPEC();
```

### ROSpec
`PARAM_ROSpec` の宣言と優先順位, ROSpecID, ステータスの設定.

```C#
var pROSpec = new PARAM_ROSpec();
msg.ROSpec = pROSpec;

pROSpec.Priority     = 0;
pROSpec.ROSpecID     = roSpecId;
pROSpec.CurrentState = ENUM_ROSpecState.Disabled;
```

### ROBoundarySpec
#### ROSpec 開始トリガ
ROSpec の開始方法として下記の種類があります.

* GPI  
リーダが GPIO を備えている場合, 対象のポートが ON または OFF (電圧の印加もしくは -v[V] に接地)した際に ROSpec を開始させます.
* Immediate  
ROSpec を有効化した時に, ROSpec を開始します.
* Null  
ROSpec を明示的に開始する必要があります.
* Periodic  
指定した秒数後に ROSpec を開始します. LLRP の使用を見る限りでは, 開始時間を設定することができるようです.(未検証)

### ROSpec 停止トリガ

* Duration  
ROSpec が開始した後, 指定した秒数後に ROSpec を停止します.
* GPI_With_Timeout  
GPIO の対象のポートが ON または OFF した際に, ROSpec を停止します. Timeout で秒数も指定することが可能です.
* Null  
ROSpec を明示的に停止する必要があります.

リーダへの接続で記したコードでは, 明示的に 読み込み(`Inventory`)開始, 停止を指示しているため, トリガを `Null` としている.

```C#
var pBoundary = new PARAM_ROBoundarySpec();
pROSpec.ROBoundarySpec = pBoundary;

// 開始トリガ
var pStartTrigger = new PARAM_ROSpecStartTrigger();
pBoundary.ROSpecStartTrigger = pStartTrigger;
pStartTrigger.ROSpecStartTriggerType = ENUM_ROSpecStartTriggerType.Null;

// 停止トリガ
var pStopTrigger = new PARAM_ROSpecStopTrigger();
pBoundary.ROSpecStopTrigger = pStopTrigger;
pStopTrigger.ROSpecStopTriggerType = ENUM_ROSpecStopTriggerType.Null;
```

### ROReport Spec
リーダから発行するレポート(ROSpec のみの場合は, 検出したタグの情報)を設定します.
レポートを発行する方法として,

* Upon_N_Tags_Or_End_Of_AISpec  
N 個のタグを検出もしくは AISpec の終了
* Upon_N_Tags_Or_End_Of_ROSpec  
N 個のタグの検出もしくは, ROSpec の終了

の 2つのどちらかを指定することができます.
タグの個数 N は 0 を指定すると, タグの個数についての終了条件が無視されることになります. そのため, ROSpec もしくは AISpec が終了した際にレポートの発行されることになります.
タグの個数 N が 1 の場合, タグ検出時にレポートを発行することになります.
下記のコードでは, タグ検出と同時にレポートを発行するように設定しています.

```C#
// Report Spec
var pReport = new PARAM_ROReportSpec();
pROSpec.ROReportSpec = pReport;
pReport.N               = 1;
pReport.ROReportTrigger = ENUM_ROReportTriggerType.Upon_N_Tags_Or_End_Of_ROSpec;
```

#### TagReportContentSelector
タグ検出時に発行されるレポートに含まれる内容の設定を行います.

* `AirProtocolEPCMemorySelector`, `PARAM_C1G2EPCMemorySelector`
このパラメータは, EPC 領域の先頭 2word[32bit] の内容である, CRC, PC Bits を含ませるか否かを指定しています.
一般的に取得する必要性は低いですが, PC Bits に含まれるフラグによって処理を変更したい場合等で使用します.

* `Enable`~
Enable に続く文字のパラメータを含ませるか指定.

```C#
var pContentSelector = new PARAM_TagReportContentSelector();
pReport.TagReportContentSelector = pContentSelector;
pContentSelector.AirProtocolEPCMemorySelector = new ();
pContentSelector.AirProtocolEPCMemorySelector
  .Add(new PARAM_C1G2EPCMemorySelector() { EnablePCBits = true, EnableCRC = true });

pContentSelector.EnableAccessSpecID             = false;
pContentSelector.EnableAntennaID                = true;
pContentSelector.EnableChannelIndex             = false;
pContentSelector.EnableFirstSeenTimestamp       = false;
pContentSelector.EnableInventoryParameterSpecID = false;
pContentSelector.EnablePeakRSSI                 = true;
pContentSelector.EnableROSpecID                 = true;
pContentSelector.EnableLastSeenTimestamp        = false;
pContentSelector.EnableSpecIndex                = false;
pContentSelector.EnableTagSeenCount             = false;
```

Impinj の拡張機能を有効にしている場合を想定. 0.5[dBm] 間隔の受信感度とタグ検出時の角度をレポートに含ませる.

```C#
var pImpinjContentSelector = new PARAM_ImpinjTagReportContentSelector();
pReport.Custom.Add(pImpinjContentSelector);
pImpinjContentSelector.ImpinjEnablePeakRSSI = new () { PeakRSSIMode = ENUM_ImpinjPeakRSSIMode.Enabled };
pImpinjContentSelector.ImpinjEnableRFPhaseAngle = new () { RFPhaseAngleMode = ENUM_ImpinjRFPhaseAngleMode.Enabled };
```

`Inventory Spec`, `AISpec` の設定
`pAI.AntennaIDs.Add(0);` このコードで, リーダが使用できるアンテナポートをすべて使用するように指示しています.

アンテナ設定

```C#
pROSpec.SpecParameter = new ();

// AI Spec
var pAI = new PARAM_AISpec();
pROSpec.SpecParameter.Add(pAI);

pAI.AntennaIDs = new ();
pAI.AntennaIDs.Add(0);

var pAiSpecStopTrigger = new PARAM_AISpecStopTrigger();
pAI.AISpecStopTrigger = pAiSpecStopTrigger;
pAiSpecStopTrigger.AISpecStopTriggerType = ENUM_AISpecStopTriggerType.Null;

pAI.InventoryParameterSpec = new PARAM_InventoryParameterSpec[1];

// Inventory Spec
var pInventory = new PARAM_InventoryParameterSpec();
pAI.InventoryParameterSpec[0] = pInventory;

pInventory.InventoryParameterSpecID = 123;
pInventory.ProtocolID = ENUM_AirProtocols.EPCGlobalClass1Gen2;
pInventory.AntennaConfiguration = new PARAM_AntennaConfiguration[4];
```

`AntennaConfiguration` の設定.
`pControl.ModeIndex = 1002;` ここでリーダのモードを指定しています.
この値はメーカにより指定されています. 一例として Impinj 社が指定しているモード値のリンクを下記に記します.
[Reader Modes (RF Modes) Made Easy – Impinj Support Portal](https://support.impinj.com/hc/en-us/articles/360000046899-Reader-Modes-RF-Modes-Made-Easy)

```C#
for(var idx = 0; idx < 4; ++idx) {
  var aid = (ushort)(idx + 1);
  var pAc = new PARAM_AntennaConfiguration();
  pInventory.AntennaConfiguration[idx] = pAc;

  pAc.AntennaID = aid;
  pAc.RFTransmitter = new () {
    ChannelIndex  = 1,
    HopTableID    = 1,
    TransmitPower = 81
  };

  pAc.RFReceiver = new () { ReceiverSensitivity = 1 };

  pAc.AirProtocolInventoryCommandSettings = new ();
  var pCommand = new PARAM_C1G2InventoryCommand();
  pAc.AirProtocolInventoryCommandSettings.Add(pCommand);
  pCommand.TagInventoryStateAware = false;

  var pControl = new PARAM_C1G2RFControl();
  pCommand.C1G2RFControl = pControl;
  pControl.Tari      = 0;
  pControl.ModeIndex = 1002;

  var pSingulation = new PARAM_C1G2SingulationControl();
  pCommand.C1G2SingulationControl = pSingulation;
  pSingulation.TagPopulation  = 32;
  pSingulation.TagTransitTime = 0;
  pSingulation.Session        = new TwoBits((ushort)1);

  var pSearchMode = new PARAM_ImpinjInventorySearchMode();
  pCommand.Custom.Add(pSearchMode);
  pSearchMode.InventorySearchMode = ENUM_ImpinjInventorySearchType.Single_Target;
}
```

`ADD_ROSPEC` の実行.

```C#
MSG_ERROR_MESSAGE? err = null;
var resp = client.ADD_ROSPEC(
    msg:      msg,
    msg_err:  out err,
    time_out: timeout);

LLRPHelper.Check(resp, err);
```

## ROSpec の削除

```C#
private static void DeleteROSpec(
    LLRPClient client,
    uint       roSpecId = 0,
    int        timeout  = 3000) {
  var msg = new MSG_DELETE_ROSPEC() { ROSpecID = roSpecId };
  MSG_ERROR_MESSAGE? err = null;

  var resp = client.DELETE_ROSPEC(
      msg:      msg,
      msg_err:  out err,
      time_out: timeout);

  LLRPHelper.Check(resp, err);
}
```

## ROSpec の有効化

```C#
private static void EnableROSpec(
    LLRPClient client,
    uint       roSpecId,
    int        timeout = 3000) {
  var msg = new MSG_ENABLE_ROSPEC() { ROSpecID = roSpecId };
  MSG_ERROR_MESSAGE? err = null;

  var resp = client.ENABLE_ROSPEC(
      msg:      msg,
      msg_err:  out err,
      time_out: timeout);

  LLRPHelper.Check(resp, err);
}
```

## ROSpec の無効化

```C#
private static void DisableROSpec(
    LLRPClient client,
    uint       roSpecId = 0,
    int        timeout  = 3000) {
  var msg = new MSG_DISABLE_ROSPEC() { ROSpecID = roSpecId };
  MSG_ERROR_MESSAGE? err = null;

  var resp = client.DISABLE_ROSPEC(
      msg:      msg,
      msg_err:  out err,
      time_out: timeout);

  LLRPHelper.Check(resp, err);
}
```

## ROSpec の開始
ROSpec を開始するには予め有効にした ROSpecID を適応した MSG_START_ROSPEC メッセージを LLRP クライアントに送信する.

```C#
private static void StartROSpec(
    LLRPClient client,
    uint       roSpecId,
    int        timeout = 3000) {
  var msg = new MSG_START_ROSPEC() { ROSpecID = roSpecId };
  MSG_ERROR_MESSAGE? err = null;

  var resp = client.START_ROSPEC(
      msg:      msg,
      msg_err:  out err,
      time_out: timeout);

  LLRPHelper.Check(resp, err);
}
```

## ROSpec  の停止
ROSpec の停止は, LLRP クライアントに対して MSG_STOP_ROSPEC を送信する.
停止する ROSpec の ID を 0 に指定することで, LLRP クライアントに設定されているすべての ROSpec を停止することができる.

```C#
private static void StopROSpec(
    LLRPClient client,
    uint       roSpecId = 0,
    int        timeout = 3000) {
  var msg = new MSG_STOP_ROSPEC() { ROSpecID = roSpecId };
  MSG_ERROR_MESSAGE? err = null;

  var resp = client.STOP_ROSPEC(
      msg:      msg,
      msg_err:  out err,
      time_out: timeout);

  LLRPHelper.Check(resp, err);
}
```

## リーダの切断
`LLRPClient.Close` 内で `MSG_CLOSE_CONNECTION` を使用して切断処理を行っています.
コンソールで使用する場合や `Task.Delay` 等で特定の時間リーダを動作させる場合は, `using` ステートメントを使用することを推奨します.

## 実行結果
上述した設定に対応したリーダであれば, 下図のような結果になると思います. 
この結果の例の `FirstSeenTimestampUTC` `LastSeenTimestampUTC` で取得された値は, Unix時間となっているため, そのままの値を DateTime に渡すと正しくない時間が取得されてしまいます.
 
![スクリーンショット_2021-10-28_23-43-01.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/33399/50c09ce2-1c7f-4cb7-a4fe-f4d2b3cd51d8.png)

## LLRP ヘルパ
LLRPクライアントから応答があった場合, 毎度 if..else を行うのが大変なの受信したメッセージもしくはエラーメッセージを判断するヘルパを作成.

```c#
internal static class LLRPHelper {
  public static void Check(
      Message?           message,
      MSG_ERROR_MESSAGE? err) {
    if(message == null && err == null) {
      throw new NullReferenceException();
    }

    var status = message?.GetType()
      .GetField("LLRPStatus")?
      .GetValue(message) as PARAM_LLRPStatus;
    if(status == null) {
      status = err?.LLRPStatus;
    }
    if(status == null) {
      // TODO Custom Exception
      throw new Exception();
    }

    if(status.StatusCode != ENUM_StatusCode.M_Success) {
      // TODO Custom Exception
      throw new Exception();
    }
  }
}
```