shellmatta_transport.py 9.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264
  1. """ Python module to handle the shellmatta transport layer """
  2. from crccheck.crc import Crc32
  3. import struct
  4. class ShellmattaTransport():
  5. """ Class to handle the shellmatta transport layer """
  6. class Packet():
  7. """ A shellmatta packet structure """
  8. HEADER_LENGTH = 8
  9. MAX_PAYLOAD_LENGTH = 255
  10. CRC_LENGTH = 4
  11. SOH = 1
  12. PROTOCOL_VERSION = 1
  13. TYPE_DATA = 0
  14. TYPE_SEQ_CNT_REQUEST = 1
  15. TYPE_SEQ_CNT_RESPOND = 129
  16. TYPE_MAX_BUFFERSIZE_REQUEST = 2
  17. TYPE_MAX_BUFFERSIZE_RESPOND = 130
  18. TYPE_SEARCH_DEVICE_REQUEST = 3
  19. TYPE_SEARCH_DEVICE_RESPOND = 131
  20. TYPE_SET_ADDRESS_REQUEST = 4
  21. TYPE_SET_ADDRESS_RESPOND = 132
  22. def __init__(self,
  23. start_of_header,
  24. protocol_version,
  25. packet_type,
  26. payload_length,
  27. source,
  28. destination,
  29. sequence_h2s,
  30. sequence_s2h,
  31. payload=b'',
  32. crc=0,
  33. crc_fct=Crc32.calc):
  34. """ creates a shellmatta transport packet based on the passed data """
  35. self.start_of_header = start_of_header
  36. self.protocol_version = protocol_version
  37. self.packet_type = packet_type
  38. self.payload_length = payload_length
  39. self.source = source
  40. self.destination = destination
  41. self.sequence_h2s = sequence_h2s
  42. self.sequence_s2h = sequence_s2h
  43. self.payload = payload
  44. self.crc = crc
  45. self.crc_fct = crc_fct
  46. @classmethod
  47. def from_header_data(cls, header_data):
  48. """ create an empty packet based on raw header data """
  49. if not header_data or len(header_data) != cls.HEADER_LENGTH:
  50. raise ValueError("A shellmatta transport packet needs 8 data bytes as a header.")
  51. data = struct.unpack('BBBBBBBB', header_data)
  52. return cls(*data)
  53. def set_payload(self, payload):
  54. """ sets/replaces the complete payload of the packet """
  55. if len(payload) > self.MAX_PAYLOAD_LENGTH:
  56. raise ValueError("Payload size exceeds limit!")
  57. self.payload = payload
  58. self.payload_length = len(self.payload)
  59. def append_payload(self, payload):
  60. """ append passed payload to the packet """
  61. if len(payload) + self.payload_length > self.MAX_PAYLOAD_LENGTH:
  62. raise ValueError("Payload size exceeds limit!")
  63. self.payload += payload
  64. self.payload_length = len(self.payload)
  65. def calc_crc(self):
  66. """ Calculates the crc checksum """
  67. return self.crc_fct(bytes(self)[:-4])
  68. def secure(self):
  69. """ Calculates the crc checksum """
  70. self.crc = self.calc_crc()
  71. def verify(self, crc):
  72. """ Checks the packet agains the passed crc """
  73. return crc == self.calc_crc()
  74. def __bytes__(self):
  75. """ Create binary representation of the packet """
  76. # pack header
  77. raw_buffer = struct.pack('BBBBBBBB',
  78. self.start_of_header,
  79. self.protocol_version,
  80. self.packet_type,
  81. self.payload_length,
  82. self.source,
  83. self.destination,
  84. self.sequence_h2s,
  85. self.sequence_s2h)
  86. raw_buffer += self.payload
  87. raw_buffer += self.crc.to_bytes(4, 'big')
  88. return raw_buffer
  89. def __init__(self, com_obj, mandatory=False, custom_crc=Crc32.calc):
  90. self.com_obj = com_obj
  91. self.mandatory = mandatory
  92. self.custom_crc = custom_crc
  93. self.sequence_counter_h2s = 0
  94. self.sequence_counter_s2h = 0
  95. self.received_raw_buffer = b''
  96. self.received_packet = None
  97. self.received_buffer = b''
  98. def __send(self, packet_type, data, destination=0):
  99. """ Sends data to the shellmatta - splitting at max size
  100. Args:
  101. packet_type (int): type of packet to send
  102. data (bytestring): string of data to send
  103. """
  104. while len(data) > 0:
  105. packet = self.Packet(self.Packet.SOH,
  106. self.Packet.PROTOCOL_VERSION,
  107. packet_type,
  108. min(len(data), self.Packet.MAX_PAYLOAD_LENGTH),
  109. 0,
  110. destination,
  111. self.sequence_counter_h2s,
  112. self.sequence_counter_s2h,
  113. data[:self.Packet.MAX_PAYLOAD_LENGTH],
  114. crc_fct=self.custom_crc)
  115. self.sequence_counter_h2s += 1
  116. packet.secure()
  117. self.com_obj.write(bytes(packet))
  118. data = data[self.Packet.MAX_PAYLOAD_LENGTH:]
  119. def __peek_com(self, size):
  120. """ wraps the read method to be able to peek data - leave data in buffer
  121. Args:
  122. size(integer) : number of bytes to peek"""
  123. if len(self.received_raw_buffer) < size:
  124. received_data = self.com_obj.read(size - len(self.received_raw_buffer))
  125. self.received_raw_buffer += received_data
  126. if len(self.received_raw_buffer) < size:
  127. raise TimeoutError("No response from Shellmatta")
  128. # return the requested data from the buffer
  129. data = self.received_raw_buffer[:size]
  130. return data
  131. def __read_com(self, size):
  132. """ wraps the read method - removes read data from the bufffer
  133. Args:
  134. size(integer) : number of bytes to read"""
  135. # return the requested data from the buffer
  136. data = self.__peek_com(size)
  137. self.received_raw_buffer = self.received_raw_buffer[size:]
  138. return data
  139. def __process_reception(self):
  140. """ try to read a complete telegram from the shellmatta """
  141. data = self.__peek_com(1)
  142. success = False
  143. # start parsing transport layer telegram
  144. if int(data[0]) == self.Packet.SOH:
  145. # process the header
  146. data = self.__peek_com(self.Packet.HEADER_LENGTH)
  147. packet = self.Packet.from_header_data(data)
  148. # read complete packet
  149. packet_size = self.Packet.HEADER_LENGTH + packet.payload_length + self.Packet.CRC_LENGTH
  150. packet_data = self.__peek_com(packet_size)
  151. packet.set_payload(packet_data[self.Packet.HEADER_LENGTH:-self.Packet.CRC_LENGTH])
  152. # verify crc
  153. crc = int.from_bytes(packet_data[-self.Packet.CRC_LENGTH:], 'big', signed=False)
  154. success = packet.verify(crc)
  155. if success:
  156. # remove the packet from the raw buffer
  157. self.__read_com(packet_size)
  158. if packet.packet_type == packet.TYPE_DATA:
  159. self.received_buffer += packet.payload
  160. # process invalid bytes
  161. if not success:
  162. if not self.mandatory:
  163. # append the received SOH directly to the buffer
  164. self.received_buffer += self.__read_com(1)
  165. else:
  166. # throw away the SOH byte
  167. self.__read_com(1)
  168. def write(self, data):
  169. """ Send data using the transport layer
  170. Args:
  171. data (bytes): data to send to shellmatta
  172. """
  173. if not isinstance(data, bytes):
  174. raise ValueError("data must be od type bytes")
  175. self.__send(self.Packet.TYPE_DATA, data)
  176. def write_manual(self, data):
  177. """Send data as if it was written by manual input. Will not use transport layer protocol.
  178. Args:
  179. data (string): String to send to shellmatta
  180. """
  181. if not isinstance(data, bytes):
  182. raise ValueError("data must be od type bytes")
  183. self.com_obj.write(data)
  184. def read(self, size=1):
  185. """ Reads size bytes from the shellmatta transport layer
  186. Args:
  187. size (integer): number of bytes to read
  188. """
  189. try:
  190. while len(self.received_buffer) < size:
  191. self.__process_reception()
  192. except TimeoutError:
  193. pass
  194. data = self.received_buffer[:size]
  195. self.received_buffer = self.received_buffer[size:]
  196. return data
  197. def reset(self):
  198. """ resets all internal states and flush the counterpart """
  199. self.sequence_counter_h2s = 0
  200. self.sequence_counter_s2h = 0
  201. self.received_raw_buffer = b''
  202. self.received_packet = None
  203. self.received_buffer = b''
  204. # flush the buffer and send one cancel
  205. self.com_obj.write(b'\x00' * (self.Packet.MAX_PAYLOAD_LENGTH + 12))
  206. def close(self):
  207. """ Close port """
  208. self.reset()