1+ # !/usr/bin/env python3
2+ # Software License Agreement (BSD License)
3+ #
4+ # Copyright (c) 2024, UFACTORY, Inc.
5+ # All rights reserved.
6+ #
7+ # Author: Vinman <vinman.wen@ufactory.cc> <vinman.cub@gmail.com>
8+
9+ import sys
10+ import time
11+ import socket
12+ import struct
13+ import logging
14+ import threading
15+
16+ logger = logging .Logger ('modbus_tcp' )
17+ logger_fmt = '[%(levelname)s][%(asctime)s][%(filename)s:%(lineno)d] - - %(message)s'
18+ logger_date_fmt = '%Y-%m-%d %H:%M:%S'
19+ stream_handler = logging .StreamHandler (sys .stdout )
20+ stream_handler .setLevel (logging .DEBUG )
21+ stream_handler .setFormatter (logging .Formatter (logger_fmt , logger_date_fmt ))
22+ logger .addHandler (stream_handler )
23+ logger .setLevel (logging .INFO )
24+
25+
26+ class ModbusTcpClient (object ):
27+ def __init__ (self , ip , port = 502 , unit_id = 0x01 ):
28+ self .sock = socket .socket (socket .AF_INET , socket .SOCK_STREAM )
29+ self .sock .setsockopt (socket .SOL_SOCKET , socket .SO_REUSEADDR , 1 )
30+ self .sock .setblocking (True )
31+ self .sock .connect ((ip , port ))
32+ self ._transaction_id = 0
33+ self ._protocol_id = 0x00
34+ self ._unit_id = unit_id
35+ self ._func_code = 0x00
36+ self ._lock = threading .Lock ()
37+
38+ def __wait_to_response (self , transaction_id = None , unit_id = None , func_code = None , timeout = 3 ):
39+ expired = time .monotonic () + timeout
40+ recv_data = b''
41+ length = 0
42+ code = - 3 # TIMEOUT
43+ send_transaction_id = transaction_id if transaction_id is not None else self ._transaction_id
44+ send_unit_id = unit_id if unit_id is not None else self ._unit_id
45+ send_func_code = func_code if func_code is not None else self ._func_code
46+ while time .monotonic () < expired :
47+ if len (recv_data ) < 7 :
48+ recv_data += self .sock .recv (7 - len (recv_data ))
49+ if len (recv_data ) < 7 :
50+ continue
51+ if length == 0 :
52+ length = struct .unpack ('>H' , recv_data [4 :6 ])[0 ]
53+ if len (recv_data ) < length + 6 :
54+ recv_data += self .sock .recv (length + 6 - len (recv_data ))
55+ if len (recv_data ) < length + 6 :
56+ continue
57+ transaction_id = struct .unpack ('>H' , recv_data [0 :2 ])[0 ]
58+ protocol_id = struct .unpack ('>H' , recv_data [2 :4 ])[0 ]
59+ unit_id = recv_data [6 ]
60+ func_code = recv_data [7 ]
61+ if transaction_id != send_transaction_id :
62+ logger .warning ('Receive a reply with a mismatched transaction id (S: {}, R: {}), discard it and continue waiting.' .format (send_transaction_id , transaction_id ))
63+ length = 0
64+ recv_data = b''
65+ continue
66+ elif protocol_id != self ._protocol_id :
67+ logger .warning ('Receive a reply with a mismatched protocol id (S: {}, R: {}), discard it and continue waiting.' .format (self ._protocol_id , protocol_id ))
68+ length = 0
69+ recv_data = b''
70+ continue
71+ elif unit_id != send_unit_id :
72+ logger .warning ('Receive a reply with a mismatched unit id (S: {}, R: {}), discard it and continue waiting.' .format (send_unit_id , unit_id ))
73+ length = 0
74+ recv_data = b''
75+ continue
76+ elif func_code != send_func_code and func_code != send_func_code + 0x80 :
77+ logger .warning ('Receive a reply with a mismatched func code (S: {}, R: {}), discard it and continue waiting.' .format (send_func_code , func_code ))
78+ length = 0
79+ recv_data = b''
80+ continue
81+ else :
82+ code = 0
83+ break
84+ if code == 0 and len (recv_data ) == 9 :
85+ logger .error ('modbus tcp data exception, exp={}, res={}' .format (recv_data [8 ], recv_data ))
86+ return recv_data [8 ], recv_data
87+ elif code != 0 :
88+ logger .error ('recv timeout, len={}, res={}' .format (len (recv_data ), recv_data ))
89+ return code , recv_data
90+
91+ def __pack_to_send (self , pdu_data , unit_id = None ):
92+ self ._transaction_id = self ._transaction_id % 65535 + 1
93+ unit_id = unit_id if unit_id is not None else self ._unit_id
94+ data = struct .pack ('>HHHB' , self ._transaction_id , self ._protocol_id , len (pdu_data ) + 1 , unit_id )
95+ data += pdu_data
96+ self .sock .send (data )
97+
98+ def __request (self , pdu , unit_id = None ):
99+ with self ._lock :
100+ self ._func_code = pdu [0 ]
101+ self .__pack_to_send (pdu )
102+ return self .__wait_to_response (unit_id = unit_id , func_code = pdu [0 ])
103+
104+ def __read_bits (self , addr , quantity , func_code = 0x01 ):
105+ assert func_code == 0x01 or func_code == 0x02
106+ pdu = struct .pack ('>BHH' , func_code , addr , quantity )
107+ code , res_data = self .__request (pdu )
108+ if code == 0 and len (res_data ) == 9 + (quantity + 7 ) // 8 :
109+ return code , [(res_data [9 + i // 8 ] >> (i % 8 ) & 0x01 ) for i in range (quantity )]
110+ else :
111+ return code , res_data
112+
113+ def __read_registers (self , addr , quantity , func_code = 0x03 , signed = False ):
114+ assert func_code == 0x03 or func_code == 0x04
115+ pdu = struct .pack ('>BHH' , func_code , addr , quantity )
116+ code , res_data = self .__request (pdu )
117+ if code == 0 and len (res_data ) == 9 + quantity * 2 :
118+ return 0 , list (struct .unpack ('>{}{}' .format (quantity , 'h' if signed else 'H' ), res_data [9 :]))
119+ else :
120+ return code , res_data
121+
122+ def read_coil_bits (self , addr , quantity ):
123+ """
124+ func_code: 0x01
125+ """
126+ return self .__read_bits (addr , quantity , func_code = 0x01 )
127+
128+ def read_input_bits (self , addr , quantity ):
129+ """
130+ func_code: 0x02
131+ """
132+ return self .__read_bits (addr , quantity , func_code = 0x02 )
133+
134+ def read_holding_registers (self , addr , quantity , signed = False ):
135+ """
136+ func_code: 0x03
137+ """
138+ return self .__read_registers (addr , quantity , func_code = 0x03 , signed = signed )
139+
140+ def read_input_registers (self , addr , quantity , signed = False ):
141+ """
142+ func_code: 0x04
143+ """
144+ return self .__read_registers (addr , quantity , func_code = 0x04 , signed = signed )
145+
146+ def write_single_coil_bit (self , addr , on ):
147+ """
148+ func_code: 0x05
149+ """
150+ pdu = struct .pack ('>BHH' , 0x05 , addr , 0xFF00 if on else 0x0000 )
151+ return self .__request (pdu )[0 ]
152+
153+ def write_single_holding_register (self , addr , reg_val ):
154+ """
155+ func_code: 0x06
156+ """
157+ pdu = struct .pack ('>BHH' , 0x06 , addr , reg_val )
158+ return self .__request (pdu )[0 ]
159+
160+ def write_multiple_coil_bits (self , addr , bits ):
161+ """
162+ func_code: 0x0F
163+ """
164+ datas = [0 ] * ((len (bits ) + 7 ) // 8 )
165+ for i in range (len (bits )):
166+ if bits [i ]:
167+ datas [i // 8 ] |= (1 << (i % 8 ))
168+ pdu = struct .pack ('>BHHB{}B' .format (len (datas )), 0x0F , addr , len (bits ), len (datas ), * datas )
169+ return self .__request (pdu )[0 ]
170+
171+ def write_multiple_holding_registers (self , addr , regs ):
172+ """
173+ func_code: 0x10
174+ """
175+ pdu = struct .pack ('>BHHB{}H' .format (len (regs )), 0x10 , addr , len (regs ), len (regs ) * 2 , * regs )
176+ return self .__request (pdu )[0 ]
177+
178+ def mask_write_holding_register (self , addr , and_mask , or_mask ):
179+ """
180+ func_code: 0x16
181+ """
182+ pdu = struct .pack ('>BHHH' , 0x16 , addr , and_mask , or_mask )
183+ return self .__request (pdu )[0 ]
184+
185+ def write_and_read_holding_registers (self , r_addr , r_quantity , w_addr , w_regs , r_signed = False , w_signed = False ):
186+ """
187+ func_code: 0x17
188+ """
189+ pdu = struct .pack ('>BHHHHB{}{}' .format (len (w_regs ), 'h' if w_signed else 'H' ), 0x17 , r_addr , r_quantity , w_addr , len (w_regs ), len (w_regs ) * 2 , * w_regs )
190+ code , res_data = self .__request (pdu )
191+ if code == 0 and len (res_data ) == 9 + r_quantity * 2 :
192+ return 0 , struct .unpack ('>{}{}' .format (r_quantity , 'h' if r_signed else 'H' ), res_data [9 :])
193+ else :
194+ return code , res_data
0 commit comments