shellmatta_transport.py 8.9 KB

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