python實現(xiàn)FINS協(xié)議的UDP服務端是一件稍微麻煩點的事情。它不像modbusTCP那樣,可以使用現(xiàn)成的pymodbus模塊去實現(xiàn)。但是,我們可以根據(jù)協(xié)議幀進行組包,自己去實現(xiàn)幀的格式,而這一切可以基于socket模塊。本文基于原先?FINS協(xié)議的TCP服務端的文章進行修改。
一、FINS TCP與FINS UDP
1、FINS_UDP與FINS_TCP有什么不同
FINS(Factory Interface Network Service)是歐姆龍(Omron)PLC(可編程邏輯控制器)的通信協(xié)議。FINS支持兩種主要的傳輸方式:FINS over TCP(FINS_TCP)和FINS over UDP(FINS_UDP)。
下面是它們之間的主要區(qū)別:
(1)傳輸層協(xié)議:
FINS_TCP: 使用TCP(Transmission Control Protocol)作為傳輸層協(xié)議。TCP是面向連接的、可靠的協(xié)議,確保數(shù)據(jù)的可靠性和順序傳輸。
FINS_UDP: 使用UDP(User Datagram Protocol)作為傳輸層協(xié)議。UDP是面向無連接的協(xié)議,它不保證數(shù)據(jù)的可靠性和順序傳輸,但通常具有更低的延遲。
(2)連接方式:
FINS_TCP: 建立連接后進行通信,類似于常見的TCP通信方式。
FINS_UDP: 無連接,每個數(shù)據(jù)包獨立發(fā)送,適用于對實時性要求較高的應用場景。
(3)可靠性和順序性:
FINS_TCP: 提供TCP的可靠性和順序性,適用于對數(shù)據(jù)完整性和傳輸順序有要求的應用。
FINS_UDP: 不提供可靠性和順序性的保證,適用于對實時性要求較高,可以容忍一些數(shù)據(jù)丟失的場景。
(4)用途:
FINS_TCP: 適用于對數(shù)據(jù)完整性和傳輸順序要求較高的應用,例如需要確保每個數(shù)據(jù)包都被正確接收的場景。
FINS_UDP: 適用于實時性要求較高,可以容忍一些數(shù)據(jù)丟失的應用,例如對于實時控制要求較高的系統(tǒng)。
選擇使用哪種方式取決于具體的應用場景和對通信特性的要求。
2、FINS_UDP與FINS_TCP的協(xié)議包有什么不同
(1)握手包
FINS_TCP有握手包,而FINS_UDP沒有握手包。
(2)請求頭
FINS_TCP的請求頭是FINS,而FINS_UDP沒有請求頭。
(3)其他部分
一致。相關文檔請查閱我之前寫的“python實現(xiàn)FINS協(xié)議的TCP服務端(篇一)”等文章,現(xiàn)對比如下:
FINS_TCP
46 49 4E 53 00 00 00 1A 00 00 00 02 00 00 00 00 80 00 02 00 01 00 00 01 00 3D 01 01 82 00 64 00 00 01
FINS_UDP
80 00 02 00 FF 00 00 05 00 64 01 01 82 00 64 00 00 01
二、程序實現(xiàn)
1、構建響應幀
def recognition_frame(req_bytes_frame, Trigger):
get_frame = req_bytes_frame.hex().upper()
print("設備請求:", get_frame)
SRC_value = get_frame[22:24] # 判斷讀寫,01為讀,02為寫
Area_value = get_frame[24:26] # 判斷寄存器區(qū)域,82為保持寄存器
# print(SRC_value)
# print(Area_value)
if SRC_value == "01":
if Area_value == "82":
response_1 = "00000000000000000000010100000001" # Trigger位為True
response_0 = "00000000000000000000010100000000" # Trigger位為False
if Trigger == True:
return bytes().fromhex(response_1)
else:
return bytes().fromhex(response_0)
else:
raise ValueError("Area_value is error!")
elif SRC_value == "02":
if Area_value == "82":
print("***************************************")
# 寫保持寄存器的響應
print("掃碼器寫入的結果數(shù)據(jù):", bytes().fromhex(get_frame))
response = "0000000000000000000001020000"
return bytes().fromhex(response)
else:
raise ValueError("Area_value is error!")
else:
raise ValueError("SRC_value is error!")
這個函數(shù)是針對讀取或寫入保持寄存器的請求,以下是對函數(shù)的解釋:
(1)輸入?yún)?shù):
req_bytes_frame
: 一個字節(jié)序列,表示設備請求的原始幀。Trigger
: 一個布爾值,似乎用于確定設備是否處于觸發(fā)狀態(tài)。
(2)函數(shù)操作:
將輸入的字節(jié)序列轉換為十六進制表示,并轉換為大寫形式。從幀中提取 SRC(源)和 Area(寄存器區(qū)域)的值。
根據(jù) SRC 和 Area 的值執(zhí)行相應的邏輯:
如果 SRC 是 "01",表示讀取請求,繼續(xù)判斷 Area 是否為 "82"(保持寄存器)。
如果不是 "82",拋出 ValueError
異常,表示 Area 值錯誤。
如果 SRC 是 "02",表示寫入請求,同樣判斷 Area 是否為 "82"。
如果是,打印掃碼器寫入的結果數(shù)據(jù),構建寫保持寄存器的響應幀。如果不是 "82",同樣拋出 ValueError
異常。
如果 SRC 不是 "01" 或 "02",拋出 ValueError
異常,表示 SRC 值錯誤。
如果是,根據(jù) Trigger 的值構建響應幀,其中 Trigger 為 True 時觸發(fā)位為 1,否則為 0。
(3)返回值:
如果是讀取請求,返回構建的響應幀(True 觸發(fā)位或 False 觸發(fā)位)。
如果是寫入請求,返回構建的寫保持寄存器的響應幀。
(4)異常處理:
如果 Area 值不是 "82",或者 SRC 值不是 "01" 或 "02",都會拋出 ValueError
異常,提示相應的錯誤信息。
總體來說,該函數(shù)是為了處理設備的讀取和寫入請求,并根據(jù)請求類型和條件構建相應的響應幀。
2、服務器實現(xiàn)
if __name__ == "__main__":
DM_start = 1000
# 創(chuàng)建FINS服務端
# 創(chuàng)建一個TCP/IP套接字
server_socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
# 綁定套接字到特定地址和端口
server_address = ('192.168.1.188', 9600) # 服務器地址和端口
server_socket.bind(server_address)
try:
num = 0 # 觸發(fā)標志
Trigger_rec = 0 # Trigger置為True時,對應變?yōu)?,表示觸發(fā)一次
response = "" # 響應
while True:
# 接收客戶端請求
request, client_address = server_socket.recvfrom(1024)
if request:
# 如果收到的不是請求頭
if "800002" in request.hex():
# print(request.hex()[22:24])
# 實現(xiàn)掃碼觸發(fā)
if request.hex()[22:24] == "01": # 判斷讀寫,01為讀觸發(fā)指令,02為寫觸發(fā)結果
if Trigger_rec != 2:
Trigger_rec += 1
if Trigger_rec == 1:
response = recognition_frame(request, Trigger=False) # 先清空觸發(fā)信號
server_socket.sendto(response, client_address)
elif Trigger_rec == 2: # 復位Trigger信號
response = recognition_frame(request, Trigger=True) # 再置位觸發(fā)信號
server_socket.sendto(response, client_address)
# 實現(xiàn)結果接收
elif request.hex()[22:24] == "02":
print(request.hex())
# print("---------------", int(request.hex()[26:30], 16))
if int(request.hex()[26:30], 16) == DM_start + 4:
if any(c != '0' for c in request.hex()[36:]): # 不全為0
print("掃碼結果:", request.hex()[36:])
num += 1
Trigger_rec = 0
else:
response = recognition_frame(request, Trigger=True)
server_socket.sendto(response, client_address)
print("還沒有收到結果,繼續(xù)等待掃碼結果!")
else:
response = recognition_frame(request, Trigger=True)
server_socket.sendto(response, client_address)
# 處理其他請求
else:
response = recognition_frame(request, Trigger=True)
server_socket.sendto(response, client_address)
print("服務響應:", response.hex()) # 可以響應為空
if num == 1:
assert bytes().fromhex(request.hex()[36:]).decode() != "NG", "實際掃碼結果為:{},不符合預期".format(bytes().fromhex(request.hex()[36:]).decode())
break
request = False
finally:
# 清理連接
server_socket.close()
這段代碼是一個服務端程序,是套接字UDP服務端。讓我們分析主要的部分:
(1)服務端設置:
創(chuàng)建一個UDP套接字(socket.AF_INET, socket.SOCK_DGRAM
)用于與客戶端通信。綁定套接字到特定的地址和端口(('192.168.1.188', 9600)
)。
(2)主循環(huán):
在一個無限循環(huán)中,服務端等待從客戶端接收請求。對于收到的請求,根據(jù)請求的內容進行不同的處理。
(3)請求處理:
判斷請求是否包含特定的頭部標識 "800002"。如果是讀觸發(fā)指令("01"
),則處理觸發(fā)邏輯。如果是寫觸發(fā)結果指令("02"
),則處理掃碼結果。如果是其他請求,統(tǒng)一進行處理。
(4)觸發(fā)邏輯:
根據(jù)觸發(fā)標志 (Trigger_rec
) 的狀態(tài),對觸發(fā)指令進行相應的處理。首先清空觸發(fā)信號,然后再置位觸發(fā)信號。
(5)掃碼結果處理:
對于寫觸發(fā)結果指令,檢查是否收到了預期的掃碼結果。如果掃碼結果不全為0,則認為收到有效的掃碼結果,增加計數(shù)。如果結果為0,繼續(xù)等待掃碼結果。
(6)響應處理:
根據(jù)處理后的結果,調用 recognition_frame
函數(shù)構建響應幀。將響應發(fā)送給客戶端。
(7)斷言和終止條件:
當計數(shù) num
達到1時,使用斷言檢查掃碼結果是否符合預期,并終止程序。
(8)清理:
在 finally
塊中關閉套接字,確保程序退出時資源被釋放。
總體來說,這個服務端程序用于處理來自客戶端的請求,其中包括了特定的觸發(fā)指令和掃碼結果指令。在處理這些指令時,它通過 recognition_frame
函數(shù)構建響應,并對結果進行相應的處理。