modbus_rtu.py 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313
  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 struct
  10. import time
  11. from modbus_tk import LOGGER
  12. from modbus_tk.modbus import (
  13. Databank, Query, Master, Server,
  14. InvalidArgumentError, ModbusInvalidResponseError, ModbusInvalidRequestError
  15. )
  16. from modbus_tk.hooks import call_hooks
  17. from modbus_tk import utils
  18. class RtuQuery(Query):
  19. """Subclass of a Query. Adds the Modbus RTU specific part of the protocol"""
  20. def __init__(self):
  21. """Constructor"""
  22. super(RtuQuery, self).__init__()
  23. self._request_address = 0
  24. self._response_address = 0
  25. def build_request(self, pdu, slave):
  26. """Add the Modbus RTU part to the request"""
  27. self._request_address = slave
  28. if (self._request_address < 0) or (self._request_address > 255):
  29. raise InvalidArgumentError("Invalid address {0}".format(self._request_address))
  30. data = struct.pack(">B", self._request_address) + pdu
  31. crc = struct.pack(">H", utils.calculate_crc(data))
  32. return data + crc
  33. def parse_response(self, response):
  34. """Extract the pdu from the Modbus RTU response"""
  35. if len(response) < 3:
  36. raise ModbusInvalidResponseError("Response length is invalid {0}".format(len(response)))
  37. (self._response_address, ) = struct.unpack(">B", response[0:1])
  38. if self._request_address != self._response_address:
  39. raise ModbusInvalidResponseError(
  40. "Response address {0} is different from request address {1}".format(
  41. self._response_address, self._request_address
  42. )
  43. )
  44. (crc, ) = struct.unpack(">H", response[-2:])
  45. if crc != utils.calculate_crc(response[:-2]):
  46. raise ModbusInvalidResponseError("Invalid CRC in response")
  47. return response[1:-2]
  48. def parse_request(self, request):
  49. """Extract the pdu from the Modbus RTU request"""
  50. if len(request) < 3:
  51. raise ModbusInvalidRequestError("Request length is invalid {0}".format(len(request)))
  52. (self._request_address, ) = struct.unpack(">B", request[0:1])
  53. (crc, ) = struct.unpack(">H", request[-2:])
  54. if crc != utils.calculate_crc(request[:-2]):
  55. raise ModbusInvalidRequestError("Invalid CRC in request")
  56. return self._request_address, request[1:-2]
  57. def build_response(self, response_pdu):
  58. """Build the response"""
  59. self._response_address = self._request_address
  60. data = struct.pack(">B", self._response_address) + response_pdu
  61. crc = struct.pack(">H", utils.calculate_crc(data))
  62. return data + crc
  63. class RtuMaster(Master):
  64. """Subclass of Master. Implements the Modbus RTU MAC layer"""
  65. def __init__(self, serial, interchar_multiplier=1.5, interframe_multiplier=3.5, t0=None):
  66. """Constructor. Pass the pyserial.Serial object"""
  67. self._serial = serial
  68. self.use_sw_timeout = False
  69. LOGGER.info("RtuMaster %s is %s", self._serial.name, "opened" if self._serial.is_open else "closed")
  70. super(RtuMaster, self).__init__(self._serial.timeout)
  71. if t0:
  72. self._t0 = t0
  73. else:
  74. self._t0 = utils.calculate_rtu_inter_char(self._serial.baudrate)
  75. #self._serial.inter_byte_timeout = interchar_multiplier * self._t0
  76. #self.set_timeout(interframe_multiplier * self._t0)
  77. # For some RS-485 adapters, the sent data(echo data) appears before modbus response.
  78. # So read echo data and discard it. By yush0602@gmail.com
  79. self.handle_local_echo = False
  80. def _do_open(self):
  81. """Open the given serial port if not already opened"""
  82. if not self._serial.is_open:
  83. call_hooks("modbus_rtu.RtuMaster.before_open", (self, ))
  84. self._serial.open()
  85. def _do_close(self):
  86. """Close the serial port if still opened"""
  87. if self._serial.is_open:
  88. self._serial.close()
  89. call_hooks("modbus_rtu.RtuMaster.after_close", (self, ))
  90. return True
  91. def set_timeout(self, timeout_in_sec, use_sw_timeout=False):
  92. """Change the timeout value"""
  93. Master.set_timeout(self, timeout_in_sec)
  94. self._serial.timeout = timeout_in_sec
  95. # Use software based timeout in case the timeout functionality provided by the serial port is unreliable
  96. self.use_sw_timeout = use_sw_timeout
  97. def _handle(self, request, expected_length):
  98. self._send(request)
  99. expected_length = int(expected_length)
  100. response = self._serial.read(expected_length)
  101. return response
  102. def _send(self, request):
  103. """Send request to the slave"""
  104. retval = call_hooks("modbus_rtu.RtuMaster.before_send", (self, request))
  105. if retval is not None:
  106. request = retval
  107. self._serial.reset_input_buffer()
  108. self._serial.reset_output_buffer()
  109. self._serial.write(request)
  110. self._serial.flush()
  111. # Read the echo data, and discard it
  112. if self.handle_local_echo:
  113. self._serial.read(len(request))
  114. def _recv(self, expected_length=-1):
  115. """Receive the response from the slave"""
  116. response = utils.to_data("")
  117. start_time = time.time() if self.use_sw_timeout else 0
  118. while True:
  119. read_bytes = self._serial.read(expected_length if expected_length > 0 else 1)
  120. if self.use_sw_timeout:
  121. read_duration = time.time() - start_time
  122. else:
  123. read_duration = 0
  124. if (not read_bytes) or (read_duration > self._serial.timeout):
  125. break
  126. response += read_bytes
  127. if expected_length >= 0 and len(response) >= expected_length:
  128. # if the expected number of byte is received consider that the response is done
  129. # improve performance by avoiding end-of-response detection by timeout
  130. break
  131. retval = call_hooks("modbus_rtu.RtuMaster.after_recv", (self, response))
  132. if retval is not None:
  133. return retval
  134. return response
  135. def _make_query(self):
  136. """Returns an instance of a Query subclass implementing the modbus RTU protocol"""
  137. return RtuQuery()
  138. class RtuServer(Server):
  139. """This class implements a simple and mono-threaded modbus rtu server"""
  140. _timeout = 0
  141. def __init__(self, serial, databank=None, error_on_missing_slave=True, **kwargs):
  142. """
  143. Constructor: initializes the server settings
  144. serial: a pyserial object
  145. databank: the data to access
  146. interframe_multiplier: 3.5 by default
  147. interchar_multiplier: 1.5 by default
  148. """
  149. interframe_multiplier = kwargs.pop('interframe_multiplier', 3.5)
  150. interchar_multiplier = kwargs.pop('interchar_multiplier', 1.5)
  151. databank = databank if databank else Databank(error_on_missing_slave=error_on_missing_slave)
  152. super(RtuServer, self).__init__(databank)
  153. self._serial = serial
  154. LOGGER.info("RtuServer %s is %s", self._serial.name, "opened" if self._serial.is_open else "closed")
  155. self._t0 = utils.calculate_rtu_inter_char(self._serial.baudrate)
  156. self._serial.inter_byte_timeout = interchar_multiplier * self._t0
  157. self.set_timeout(interframe_multiplier * self._t0)
  158. self._block_on_first_byte = False
  159. def close(self):
  160. """close the serial communication"""
  161. if self._serial.is_open:
  162. call_hooks("modbus_rtu.RtuServer.before_close", (self, ))
  163. self._serial.close()
  164. call_hooks("modbus_rtu.RtuServer.after_close", (self, ))
  165. def set_timeout(self, timeout):
  166. self._timeout = timeout
  167. self._serial.timeout = timeout
  168. def get_timeout(self):
  169. return self._timeout
  170. def __del__(self):
  171. """Destructor"""
  172. self.close()
  173. def _make_query(self):
  174. """Returns an instance of a Query subclass implementing the modbus RTU protocol"""
  175. return RtuQuery()
  176. def start(self):
  177. """Allow the server thread to block on first byte"""
  178. self._block_on_first_byte = True
  179. super(RtuServer, self).start()
  180. def stop(self):
  181. """Force the server thread to exit"""
  182. # Prevent blocking on first byte in server thread.
  183. # Without the _block_on_first_byte following problem could happen:
  184. # 1. Current blocking read(1) is cancelled
  185. # 2. Server thread resumes and start next read(1)
  186. # 3. RtuServer clears go event and waits for thread to finish
  187. # 4. Server thread finishes only when a byte is received
  188. # Thanks to _block_on_first_byte, if server thread does start new read
  189. # it will timeout as it won't be blocking.
  190. self._block_on_first_byte = False
  191. if self._serial.is_open:
  192. # cancel any pending read from server thread, it most likely is
  193. # blocking read(1) call
  194. self._serial.cancel_read()
  195. super(RtuServer, self).stop()
  196. def _do_init(self):
  197. """initialize the serial connection"""
  198. if not self._serial.is_open:
  199. call_hooks("modbus_rtu.RtuServer.before_open", (self, ))
  200. self._serial.open()
  201. call_hooks("modbus_rtu.RtuServer.after_open", (self, ))
  202. def _do_exit(self):
  203. """close the serial connection"""
  204. self.close()
  205. def _do_run(self):
  206. """main function of the server"""
  207. try:
  208. # check the status of every socket
  209. request = utils.to_data('')
  210. if self._block_on_first_byte:
  211. # do a blocking read for first byte
  212. self._serial.timeout = None
  213. try:
  214. read_bytes = self._serial.read(1)
  215. request += read_bytes
  216. except Exception as e:
  217. self._serial.close()
  218. self._serial.open()
  219. self._serial.timeout = self._timeout
  220. # Read rest of the request
  221. while True:
  222. try:
  223. read_bytes = self._serial.read(128)
  224. if not read_bytes:
  225. break
  226. except Exception as e:
  227. self._serial.close()
  228. self._serial.open()
  229. break
  230. request += read_bytes
  231. # parse the request
  232. if request:
  233. retval = call_hooks("modbus_rtu.RtuServer.after_read", (self, request))
  234. if retval is not None:
  235. request = retval
  236. response = self._handle(request)
  237. # send back the response
  238. retval = call_hooks("modbus_rtu.RtuServer.before_write", (self, response))
  239. if retval is not None:
  240. response = retval
  241. if response:
  242. if self._serial.in_waiting > 0:
  243. # Most likely master timed out on this request and started a new one
  244. # for which we already received atleast 1 byte
  245. LOGGER.warning("Not sending response because there is new request pending")
  246. else:
  247. self._serial.write(response)
  248. self._serial.flush()
  249. time.sleep(self.get_timeout())
  250. call_hooks("modbus_rtu.RtuServer.after_write", (self, response))
  251. except Exception as excpt:
  252. LOGGER.error("Error while handling request, Exception occurred: %s", excpt)
  253. call_hooks("modbus_rtu.RtuServer.on_error", (self, excpt))