[C#.NET][TCP Socket] 利用 Socket 控制 Advantech RU25 Reader-Passive Mode
日前,因為在控制這台 Reader 設備的時候短缺原廠的 command set / get 文件,於是利用原廠提供的 Demo software + Smart Sniff 來觀察 TCP Package,我也會用 Smart Sniff 來驗証資料傳遞之間是否有問題。
除了 Smart Sniff 之外,我也力推免費軟體 Wireshark,功能較為強大,相信大家都有聽過它,相當的實用,市面上也有相關的書籍,實戰封包分析-使用 Wireshark (Practical Packet Analysis: Using Wireshark to Solve Real-World Network Problems, 2/e)
工具準備好之後,就可以來開始處理它了。
Step1.Conect:
跟前面幾篇一樣建立一個新的 Socket Class,並且 調用Socket.Connect 方法。
var ipAddress = IPAddress.Parse(this.Config.IpAddress);
var iPEndPoint = new IPEndPoint(ipAddress, this.Config.Port);
//Create a TCP/IP socket.
this._socketCliect = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
EndPoint endPoint = (EndPoint)iPEndPoint;
this._socketCliect.Connect(endPoint);
當連線成功的時候,Reader 不會拋訊息回來,不用寫 Receive,也就是不需要帳號/密碼,在網路的世界裡,欠缺安全性這方面的考量。
完整的 Connect 方法如下,因為設備的特性所以我必須要送出一些參數命令,比如連續兩次的 set readmode 0,才能在某些情況下取得正確的 Receive data(這是實驗得知的特性)
private Socket _socketCliect = null; private Encoding _encode = Encoding.ASCII; public bool Connect() { if (this.Config == null) { return false; } if (this.IsConnected) { return true; } var ipAddress = IPAddress.Parse(this.Config.IpAddress); var iPEndPoint = new IPEndPoint(ipAddress, this.Config.Port); //Create a TCP/IP socket. this._socketCliect = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); EndPoint endPoint = (EndPoint)iPEndPoint; this._socketCliect.Connect(endPoint); if (this._socketCliect.Connected) { SendAndReceive("set readmode 0"); var readmode = SendAndReceive("set readmode 0"); var inventorymode = this.SendAndReceive("set inventorymode 0"); if (readmode == MESSAGE_SUCCESS && inventorymode == MESSAGE_SUCCESS) { this.IsConnected = true; } else { this.IsConnected = false; } } return this.IsConnected; }
Setp2.Send Command Set:
跟設備搭上線後便可以對它送出 ASCII 命令,當我調用 Socket.Send 方法 送出資料 後,就要調用 Socket.Receive 方法去 拿回資料,然後驗證拿到的資料是否正確。
下圖為原廠提供設備回傳的資料格式:
根據文件,程式碼如下,在這裡我只檢查最後6個 byte[] { 49, 49, 48, 50, 13, 10 },也就是 1102\r\n ,若在時間內收到最後這 6 個 byte 則離開迴圈並回傳資料,若在時間內拿不到這最後的 byte[] ,表示網路接收逾時(ReceiveTimeout),則離開迴圈。
public static readonly string MESSAGE_SUCCESS = "1101\r\n701\r\n1102\r\n";
public string SendAndReceive(string Data)
{
if (this.Config == null)
{
return null;
}
if (this._socketCliect == null || !this._socketCliect.Connected)
{
return null;
}
SocketError error = new SocketError();
var command = string.Concat(Data, "\r");
var sentCommand = this.Encode.GetBytes(command);
this._socketCliect.Send(sentCommand, 0, sentCommand.Length, SocketFlags.None, out error);
var tempTimeOut = this._socketCliect.ReceiveTimeout;
this._socketCliect.ReceiveTimeout = 1000;
var bufferArray = new byte[256];
var socketError = new SocketError();
MemoryStream memory = new MemoryStream();
byte[] expect = new byte[] { 49, 49, 48, 50, 13, 10 };
byte[] target = new byte[6];
while (true)
{
var receiveCount = this._socketCliect.Receive(bufferArray, 0, bufferArray.Length, SocketFlags.None, out socketError);
if (receiveCount == 0 || socketError != SocketError.Success)
{
break;
}
memory.Write(bufferArray, 0, receiveCount);
if (memory.Length < expect.Length)
{
continue;
}
var startIndex = memory.Length - expect.Length;
memory.Seek(startIndex, SeekOrigin.Begin);
memory.Read(target, 0, expect.Length);
var compare = expect.SequenceEqual(target);
if (compare)
{
break;
}
}
this._socketCliect.ReceiveTimeout = tempTimeOut;
byte[] response = memory.ToArray();
var result = this.Encode.GetString(response, 0, response.Length);
if (memory != null)
{
memory.Close();
}
return result;
}
下圖為 set 命令的封包傳送結果
Setp3.Send Command Get:
設備除了有 set 之外,當然還會有 get。
下圖為 get command 所回傳的資料格式:
當我調用 SendAndReceive("get readmode") 方法,就能觀察到以下的資料
每一個功能的 get 回傳的資料格式以及 return code 都不太一樣,return code 是用來驗證資料格式是否正確的旗標,所以要 用力的 看文件,若沒文件就觀察 Smart Sniff。
依照文件規則 get command 產生了以下程式碼:
public string getValidData(string data, string returnCode)
{
if (string.IsNullOrEmpty(data) | string.IsNullOrEmpty(returnCode))
{
return null;
}
var split = data.Split(new string[] { "\r\n" }, StringSplitOptions.RemoveEmptyEntri
if (split.Length <= 3)
{
return null;
}
var commandIndex = 0;
var endIndex = split.Length - 1;
var returnIndex = split.Length - 2;
var commandCode = split[commandIndex];
var endCode = split[endIndex];
if (commandCode != "1101" && endCode != "1102" && split[returnIndex] != returnCode)
{
return null;
}
StringBuilder builder = new StringBuilder();
for (int i = 0; i < split.Length; i++)
{
if (i == commandIndex | i == endIndex | i == returnIndex)
{
continue;
}
builder.Append(split[i]);
}
return builder.ToString();
}
inventory command,是我手頭上所擁有的稀有文件資料,正好也算是一個最重要的命令,就以它來當例子,當我調用 SendAndReceive("inventory"),會得到以下的結果,它所回傳的結果是多筆的
如下圖,inventory 所定義的資料格式
針對 inventory 的命令包裝成一個方法,然後調用 getValidData 方法,驗證是否為合法資料。
public IEnumerable<TagStatus> InventoryTag()
{
if (!this.IsConnected)
{
return null;
}
var response = this.SendAndReceive("inventory");
if (string.IsNullOrEmpty(response))
{
return null;
}
var data = getValidData(response, "101");
List<TagStatus> tagStatusList = new List<TagStatus>();
foreach (var d in data)
{
var tag = TagStatus.Parse(d);
tagStatusList.Add(tag);
}
return tagStatusList;
}
用來裝載資料的 TagStatus 類別
public class TagStatus : INotifyPropertyChanged
{
private string _tagEpc;
private string _memory;
private int _antennaNumber;
private double _rssiAver;
private double _rssiMax;
private double _rssiMin;
private string _pc;
public string TagEpc
{
get { return _tagEpc; }
set
{
_tagEpc = value;
OnPropertyChanged("TagEpc");
}
}
public int AntennaNumber
{
get { return _antennaNumber; }
set
{
_antennaNumber = value;
OnPropertyChanged("AntennaNumber");
}
}
public double RSSI_Aver
{
get { return _rssiAver; }
set
{
_rssiAver = value;
OnPropertyChanged("RSSI_Aver");
}
}
public double RSSI_Min
{
get { return _rssiMin; }
set
{
_rssiMin = value;
OnPropertyChanged("RSSI_Min");
}
}
public double RSSI_Max
{
get { return _rssiMax; }
set
{
_rssiMax = value;
OnPropertyChanged("RSSI_Max");
}
}
public string PC
{
get { return _pc; }
set
{
_pc = value;
OnPropertyChanged("PC");
}
}
public string Memory
{
get { return _memory; }
set
{
_memory = value;
OnPropertyChanged("Memory");
}
}
public event PropertyChangedEventHandler PropertyChanged;
protected void OnPropertyChanged(string name)
{
PropertyChangedEventHandler changedEventHandler = this.PropertyChanged;
if (changedEventHandler == null)
return;
changedEventHandler((object)this, new PropertyChangedEventArgs(name));
}
public override string ToString()
{
return string.Format("TagEpc:{0}", this.TagEpc);
}
public static TagStatus Parse(string data)
{
// "3000E2003411B802011618152459,0002,4A,4A,4A";
// "3000E2003411B802011618152459,000000000,0002,4A,4A,4A";
if (string.IsNullOrEmpty(data))
{
return null;
}
var split = data.Split(',');
var index = split.Length;
if (index < 5)
{
return null;
}
TagStatus tagStatus = new TagStatus();
tagStatus.TagEpc = split[0].Substring(4);
tagStatus.PC = split[0].Substring(0, 4);
if (index == 5)
{
// "3000E2003411B802011618152459,0002,4A,4A,4A";
tagStatus.AntennaNumber = int.Parse(split[1]);
tagStatus.RSSI_Min = Convert.ToInt32(split[2], 16);
tagStatus.RSSI_Max = Convert.ToInt32(split[3], 16);
tagStatus.RSSI_Aver = Convert.ToInt32(split[4], 16);
}
else if (index == 6)
{
// "3000E2003411B802011618152459,000000000,0002,4A,4A,4A";
tagStatus.Memory = split[1];
tagStatus.AntennaNumber = int.Parse(split[2]);
tagStatus.RSSI_Min = Convert.ToInt32(split[3], 16);
tagStatus.RSSI_Max = Convert.ToInt32(split[4], 16);
tagStatus.RSSI_Aver = Convert.ToInt32(split[5], 16);
}
return tagStatus;
}
}
若有謬誤,煩請告知,新手發帖請多包涵
Microsoft MVP Award 2010~2017 C# 第四季
Microsoft MVP Award 2018~2022 .NET