modbus_tcp.py 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360
  1. #!/usr/bin/env python
  2. # -*- coding: utf-8 -*-
  3. """
  4. Modbus TestKit: Implementation of Modbus protocol in python
  5. (C)2009 - Luc Jean - luc.jean@gmail.com
  6. (C)2009 - Apidev - http://www.apidev.fr
  7. This is distributed under GNU LGPL license, see license.txt
  8. """
  9. import socket
  10. import select
  11. import struct
  12. from modbus_tk import LOGGER
  13. from modbus_tk.hooks import call_hooks
  14. from modbus_tk.modbus import (
  15. Databank, Master, Query, Server,
  16. InvalidArgumentError, ModbusInvalidResponseError, ModbusInvalidRequestError
  17. )
  18. from modbus_tk.utils import threadsafe_function, flush_socket, to_data
  19. #-------------------------------------------------------------------------------
  20. class ModbusInvalidMbapError(Exception):
  21. """Exception raised when the modbus TCP header doesn't correspond to what is expected"""
  22. def __init__(self, value):
  23. Exception.__init__(self, value)
  24. #-------------------------------------------------------------------------------
  25. class TcpMbap(object):
  26. """Defines the information added by the Modbus TCP layer"""
  27. def __init__(self):
  28. """Constructor: initializes with 0"""
  29. self.transaction_id = 0
  30. self.protocol_id = 0
  31. self.length = 0
  32. self.unit_id = 0
  33. def clone(self, mbap):
  34. """Set the value of each fields from another TcpMbap instance"""
  35. self.transaction_id = mbap.transaction_id
  36. self.protocol_id = mbap.protocol_id
  37. self.length = mbap.length
  38. self.unit_id = mbap.unit_id
  39. def _check_ids(self, request_mbap):
  40. """
  41. Check that the ids in the request and the response are similar.
  42. if not returns a string describing the error
  43. """
  44. error_str = ""
  45. if request_mbap.transaction_id != self.transaction_id:
  46. error_str += "Invalid transaction id: request={0} - response={1}. ".format(
  47. request_mbap.transaction_id, self.transaction_id)
  48. if request_mbap.protocol_id != self.protocol_id:
  49. error_str += "Invalid protocol id: request={0} - response={1}. ".format(
  50. request_mbap.protocol_id, self.protocol_id
  51. )
  52. if request_mbap.unit_id != self.unit_id:
  53. error_str += "Invalid unit id: request={0} - response={1}. ".format(request_mbap.unit_id, self.unit_id)
  54. return error_str
  55. def check_length(self, pdu_length):
  56. """Check the length field is valid. If not raise an exception"""
  57. following_bytes_length = pdu_length+1
  58. if self.length != following_bytes_length:
  59. return "Response length is {0} while receiving {1} bytes. ".format(self.length, following_bytes_length)
  60. return ""
  61. def check_response(self, request_mbap, response_pdu_length):
  62. """Check that the MBAP of the response is valid. If not raise an exception"""
  63. error_str = self._check_ids(request_mbap)
  64. error_str += self.check_length(response_pdu_length)
  65. if len(error_str) > 0:
  66. raise ModbusInvalidMbapError(error_str)
  67. def pack(self):
  68. """convert the TCP mbap into a string"""
  69. return struct.pack(">HHHB", self.transaction_id, self.protocol_id, self.length, self.unit_id)
  70. def unpack(self, value):
  71. """extract the TCP mbap from a string"""
  72. (self.transaction_id, self.protocol_id, self.length, self.unit_id) = struct.unpack(">HHHB", value)
  73. class TcpQuery(Query):
  74. """Subclass of a Query. Adds the Modbus TCP specific part of the protocol"""
  75. #static variable for giving a unique id to each query
  76. _last_transaction_id = 0
  77. def __init__(self):
  78. """Constructor"""
  79. super(TcpQuery, self).__init__()
  80. self._request_mbap = TcpMbap()
  81. self._response_mbap = TcpMbap()
  82. @threadsafe_function
  83. def _get_transaction_id(self):
  84. """returns an identifier for the query"""
  85. if TcpQuery._last_transaction_id < 0xffff:
  86. TcpQuery._last_transaction_id += 1
  87. else:
  88. TcpQuery._last_transaction_id = 0
  89. return TcpQuery._last_transaction_id
  90. def build_request(self, pdu, slave):
  91. """Add the Modbus TCP part to the request"""
  92. if (slave < 0) or (slave > 255):
  93. raise InvalidArgumentError("{0} Invalid value for slave id".format(slave))
  94. self._request_mbap.length = len(pdu) + 1
  95. self._request_mbap.transaction_id = self._get_transaction_id()
  96. self._request_mbap.unit_id = slave
  97. mbap = self._request_mbap.pack()
  98. return mbap + pdu
  99. def parse_response(self, response):
  100. """Extract the pdu from the Modbus TCP response"""
  101. if len(response) > 6:
  102. mbap, pdu = response[:7], response[7:]
  103. self._response_mbap.unpack(mbap)
  104. self._response_mbap.check_response(self._request_mbap, len(pdu))
  105. return pdu
  106. else:
  107. raise ModbusInvalidResponseError("Response length is only {0} bytes. ".format(len(response)))
  108. def parse_request(self, request):
  109. """Extract the pdu from a modbus request"""
  110. if len(request) > 6:
  111. mbap, pdu = request[:7], request[7:]
  112. self._request_mbap.unpack(mbap)
  113. error_str = self._request_mbap.check_length(len(pdu))
  114. if len(error_str) > 0:
  115. raise ModbusInvalidMbapError(error_str)
  116. return self._request_mbap.unit_id, pdu
  117. else:
  118. raise ModbusInvalidRequestError("Request length is only {0} bytes. ".format(len(request)))
  119. def build_response(self, response_pdu):
  120. """Build the response"""
  121. self._response_mbap.clone(self._request_mbap)
  122. self._response_mbap.length = len(response_pdu) + 1
  123. return self._response_mbap.pack() + response_pdu
  124. class TcpMaster(Master):
  125. """Subclass of Master. Implements the Modbus TCP MAC layer"""
  126. def __init__(self, host="127.0.0.1", port=502, timeout_in_sec=5.0):
  127. """Constructor. Set the communication settings"""
  128. super(TcpMaster, self).__init__(timeout_in_sec)
  129. self._host = host
  130. self._port = port
  131. self._sock = None
  132. def _do_open(self):
  133. """Connect to the Modbus slave"""
  134. if self._sock:
  135. self._sock.close()
  136. self._sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
  137. self.set_timeout(self.get_timeout())
  138. self._sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
  139. call_hooks("modbus_tcp.TcpMaster.before_connect", (self, ))
  140. self._sock.connect((self._host, self._port))
  141. call_hooks("modbus_tcp.TcpMaster.after_connect", (self, ))
  142. def _do_close(self):
  143. """Close the connection with the Modbus Slave"""
  144. if self._sock:
  145. call_hooks("modbus_tcp.TcpMaster.before_close", (self, ))
  146. self._sock.close()
  147. call_hooks("modbus_tcp.TcpMaster.after_close", (self, ))
  148. self._sock = None
  149. return True
  150. def set_timeout(self, timeout_in_sec):
  151. """Change the timeout value"""
  152. super(TcpMaster, self).set_timeout(timeout_in_sec)
  153. if self._sock:
  154. self._sock.setblocking(timeout_in_sec > 0)
  155. if timeout_in_sec:
  156. self._sock.settimeout(timeout_in_sec)
  157. def _send(self, request):
  158. """Send request to the slave"""
  159. retval = call_hooks("modbus_tcp.TcpMaster.before_send", (self, request))
  160. if retval is not None:
  161. request = retval
  162. try:
  163. flush_socket(self._sock, 3)
  164. except Exception as msg:
  165. #if we can't flush the socket successfully: a disconnection may happened
  166. #try to reconnect
  167. LOGGER.error('Error while flushing the socket: {0}'.format(msg))
  168. self._do_open()
  169. self._sock.send(request)
  170. def _recv(self, expected_length=-1):
  171. """
  172. Receive the response from the slave
  173. Do not take expected_length into account because the length of the response is
  174. written in the mbap. Used for RTU only
  175. """
  176. response = to_data('')
  177. length = 255
  178. while len(response) < length:
  179. rcv_byte = self._sock.recv(1)
  180. if rcv_byte:
  181. response += rcv_byte
  182. if len(response) == 6:
  183. to_be_recv_length = struct.unpack(">HHH", response)[2]
  184. length = to_be_recv_length + 6
  185. else:
  186. break
  187. retval = call_hooks("modbus_tcp.TcpMaster.after_recv", (self, response))
  188. if retval is not None:
  189. return retval
  190. return response
  191. def _make_query(self):
  192. """Returns an instance of a Query subclass implementing the modbus TCP protocol"""
  193. return TcpQuery()
  194. class TcpServer(Server):
  195. """
  196. This class implements a simple and mono-threaded modbus tcp server
  197. !! Change in 0.5.0: By default the TcpServer is not bound to a specific address
  198. for example: You must set address to 'loaclhost', if youjust want to accept local connections
  199. """
  200. def __init__(self, port=502, address='', timeout_in_sec=1, databank=None, error_on_missing_slave=True):
  201. """Constructor: initializes the server settings"""
  202. databank = databank if databank else Databank(error_on_missing_slave=error_on_missing_slave)
  203. super(TcpServer, self).__init__(databank)
  204. self._sock = None
  205. self._sa = (address, port)
  206. self._timeout_in_sec = timeout_in_sec
  207. self._sockets = []
  208. def _make_query(self):
  209. """Returns an instance of a Query subclass implementing the modbus TCP protocol"""
  210. return TcpQuery()
  211. def _get_request_length(self, mbap):
  212. """Parse the mbap and returns the number of bytes to be read"""
  213. if len(mbap) < 6:
  214. raise ModbusInvalidRequestError("The mbap is only %d bytes long", len(mbap))
  215. length = struct.unpack(">HHH", mbap[:6])[2]
  216. return length
  217. def _do_init(self):
  218. """initialize server"""
  219. self._sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
  220. self._sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
  221. if self._timeout_in_sec:
  222. self._sock.settimeout(self._timeout_in_sec)
  223. self._sock.setblocking(0)
  224. self._sock.bind(self._sa)
  225. self._sock.listen(10)
  226. self._sockets.append(self._sock)
  227. def _do_exit(self):
  228. """clean the server tasks"""
  229. #close the sockets
  230. for sock in self._sockets:
  231. try:
  232. sock.close()
  233. self._sockets.remove(sock)
  234. except Exception as msg:
  235. LOGGER.warning("Error while closing socket, Exception occurred: %s", msg)
  236. self._sock.close()
  237. self._sock = None
  238. def _do_run(self):
  239. """called in a almost-for-ever loop by the server"""
  240. # check the status of every socket
  241. inputready = select.select(self._sockets, [], [], 1.0)[0]
  242. # handle data on each a socket
  243. for sock in inputready:
  244. try:
  245. if sock == self._sock:
  246. # handle the server socket
  247. client, address = self._sock.accept()
  248. client.setblocking(0)
  249. LOGGER.info("%s is connected with socket %d...", str(address), client.fileno())
  250. self._sockets.append(client)
  251. call_hooks("modbus_tcp.TcpServer.on_connect", (self, client, address))
  252. else:
  253. if len(sock.recv(1, socket.MSG_PEEK)) == 0:
  254. # socket is disconnected
  255. LOGGER.info("%d is disconnected" % (sock.fileno()))
  256. call_hooks("modbus_tcp.TcpServer.on_disconnect", (self, sock))
  257. sock.close()
  258. self._sockets.remove(sock)
  259. break
  260. # handle all other sockets
  261. sock.settimeout(1.0)
  262. request = to_data("")
  263. is_ok = True
  264. # read the 7 bytes of the mbap
  265. while (len(request) < 7) and is_ok:
  266. new_byte = sock.recv(1)
  267. if len(new_byte) == 0:
  268. is_ok = False
  269. else:
  270. request += new_byte
  271. retval = call_hooks("modbus_tcp.TcpServer.after_recv", (self, sock, request))
  272. if retval is not None:
  273. request = retval
  274. if is_ok:
  275. # read the rest of the request
  276. length = self._get_request_length(request)
  277. while (len(request) < (length + 6)) and is_ok:
  278. new_byte = sock.recv(1)
  279. if len(new_byte) == 0:
  280. is_ok = False
  281. else:
  282. request += new_byte
  283. if is_ok:
  284. response = ""
  285. # parse the request
  286. try:
  287. response = self._handle(request)
  288. except Exception as msg:
  289. LOGGER.error("Error while handling a request, Exception occurred: %s", msg)
  290. # send back the response
  291. if response:
  292. try:
  293. retval = call_hooks("modbus_tcp.TcpServer.before_send", (self, sock, response))
  294. if retval is not None:
  295. response = retval
  296. sock.send(response)
  297. call_hooks("modbus_tcp.TcpServer.after_send", (self, sock, response))
  298. except Exception as msg:
  299. is_ok = False
  300. LOGGER.error(
  301. "Error while sending on socket %d, Exception occurred: %s", sock.fileno(), msg
  302. )
  303. except Exception as excpt:
  304. LOGGER.warning("Error while processing data on socket %d: %s", sock.fileno(), excpt)
  305. call_hooks("modbus_tcp.TcpServer.on_error", (self, sock, excpt))
  306. sock.close()
  307. self._sockets.remove(sock)