はじめに
LLRP は RFIDリーダのネットワークインターフェースを標準化したプロトコルです.
仕様や詳細については下記のリンクを参照してください.
LLRP Toolkit
LLRP仕様
タグのメモリ構造などは下記を参考にしてください
GS1 EPC/RFID標準
RFIDタグの構造について
似た例(大体, 同じ例)が Impinj Support にもあります.
Hello LLRP (Low-Level Reader Protocol)
環境
名称 | Version | 備考 | |
---|---|---|---|
言語 | C# | 9.0 | |
ライブラリ | libltknet-sdk | 10.46.0 | libltknet-sdk(LLRP) |
タグIDの取得
リーダに対して読込命令を発行し, 周辺にある RFID の EPC を取得します.
リーダへの接続
下記のコードでは, IPv4 アドレス 192.168.100.64
のリーダに対して 5084(LLRP)
で接続を行います.
既に何かしらのプログラムが LLRP に接続している場合は, エラーが発生します.
暗号化された通信を使用する場合は, ポート番号を 5085
, useTLS = true
に変更します.
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);
レポートイベント
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 等操作に関係する設定を初期化します.
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] 間隔の受信感度と検出時のタグの角度を追加するために拡張機能を有効化しています.
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
を宣言.
var msg = new MSG_ADD_ROSPEC();
ROSpec
PARAM_ROSpec
の宣言と優先順位, ROSpecID, ステータスの設定.
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
としている.
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 の場合, タグ検出時にレポートを発行することになります.
下記のコードでは, タグ検出と同時にレポートを発行するように設定しています.
// 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 に続く文字のパラメータを含ませるか指定.
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] 間隔の受信感度とタグ検出時の角度をレポートに含ませる.
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);
このコードで, リーダが使用できるアンテナポートをすべて使用するように指示しています.
アンテナ設定
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
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
の実行.
MSG_ERROR_MESSAGE? err = null;
var resp = client.ADD_ROSPEC(
msg: msg,
msg_err: out err,
time_out: timeout);
LLRPHelper.Check(resp, err);
ROSpec の削除
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 の有効化
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 の無効化
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 クライアントに送信する.
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 を停止することができる.
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 に渡すと正しくない時間が取得されてしまいます.
LLRP ヘルパ
LLRPクライアントから応答があった場合, 毎度 if..else を行うのが大変なの受信したメッセージもしくはエラーメッセージを判断するヘルパを作成.
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();
}
}
}