1
+ # !/usr/bin/env python3
2
+ # Software License Agreement (BSD License)
3
+ #
4
+ # Copyright (c) 2024, UFACTORY, Inc.
5
+ # All rights reserved.
6
+ #
7
+
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