A clone of: https://github.com/nutechsoftware/alarmdecoder This is requires as they dropped support for older firmware releases w/o building in backward compatibility code, and they had previously hardcoded pyserial to a python2 only version.

224 lines
6.8 KiB

  1. """
  2. Provides utility classes for the `AlarmDecoder`_ (AD2) devices.
  3. .. _AlarmDecoder: http://www.alarmdecoder.com
  4. .. moduleauthor:: Scott Petersen <scott@nutech.com>
  5. """
  6. import time
  7. import threading
  8. import select
  9. import alarmdecoder
  10. from io import open
  11. from collections import deque
  12. class NoDeviceError(Exception):
  13. """
  14. No devices found.
  15. """
  16. pass
  17. class CommError(Exception):
  18. """
  19. There was an error communicating with the device.
  20. """
  21. pass
  22. class TimeoutError(Exception):
  23. """
  24. There was a timeout while trying to communicate with the device.
  25. """
  26. pass
  27. class InvalidMessageError(Exception):
  28. """
  29. The format of the panel message was invalid.
  30. """
  31. pass
  32. class UploadError(Exception):
  33. """
  34. Generic firmware upload error.
  35. """
  36. pass
  37. class UploadChecksumError(UploadError):
  38. """
  39. The firmware upload failed due to a checksum error.
  40. """
  41. pass
  42. def bytes_available(device):
  43. bytes_avail = 0
  44. if isinstance(device, alarmdecoder.devices.SerialDevice):
  45. if hasattr(device._device, "in_waiting"):
  46. bytes_avail = device._device.in_waiting
  47. else:
  48. bytes_avail = device._device.inWaiting()
  49. elif isinstance(device, alarmdecoder.devices.SocketDevice):
  50. bytes_avail = 4096
  51. return bytes_avail
  52. def read_firmware_file(file_path):
  53. data_queue = deque()
  54. with open(file_path) as firmware_handle:
  55. for line in firmware_handle:
  56. line = line.rstrip()
  57. if line != '' and line[0] == ':':
  58. data_queue.append(line + "\r")
  59. return data_queue
  60. class Firmware(object):
  61. """
  62. Represents firmware for the `AlarmDecoder`_ devices.
  63. """
  64. # Constants
  65. STAGE_START = 0
  66. STAGE_WAITING = 1
  67. STAGE_BOOT = 2
  68. STAGE_WAITING_ON_LOADER = 2.5
  69. STAGE_LOAD = 3
  70. STAGE_UPLOADING = 4
  71. STAGE_DONE = 5
  72. STAGE_ERROR = 98
  73. STAGE_DEBUG = 99
  74. @staticmethod
  75. def read(device):
  76. response = None
  77. bytes_avail = bytes_available(device)
  78. if isinstance(device, alarmdecoder.devices.SerialDevice):
  79. response = device._device.read(bytes_avail)
  80. elif isinstance(device, alarmdecoder.devices.SocketDevice):
  81. response = device._device.recv(bytes_avail)
  82. return response
  83. @staticmethod
  84. def upload(device, file_path, progress_callback=None, debug=False):
  85. """
  86. Uploads firmware to an `AlarmDecoder`_ device.
  87. :param file_path: firmware file path
  88. :type file_path: string
  89. :param progress_callback: callback function used to report progress
  90. :type progress_callback: function
  91. :raises: :py:class:`~alarmdecoder.util.NoDeviceError`, :py:class:`~alarmdecoder.util.TimeoutError`
  92. """
  93. def progress_stage(stage, **kwargs):
  94. """Callback to update progress for the specified stage."""
  95. if progress_callback is not None:
  96. progress_callback(stage, **kwargs)
  97. return stage
  98. if device is None:
  99. raise NoDeviceError('No device specified for firmware upload.')
  100. fds = [device._device.fileno()]
  101. # Read firmware file into memory
  102. try:
  103. write_queue = read_firmware_file(file_path)
  104. except IOError as err:
  105. stage = progress_stage(Firmware.STAGE_ERROR, error=str(err))
  106. return
  107. data_read = ''
  108. got_response = False
  109. running = True
  110. stage = progress_stage(Firmware.STAGE_START)
  111. if device.is_reader_alive():
  112. # Close the reader thread and wait for it to die, otherwise
  113. # it interferes with our reading.
  114. device.stop_reader()
  115. while device._read_thread.is_alive():
  116. stage = progress_stage(Firmware.STAGE_WAITING)
  117. time.sleep(0.5)
  118. time.sleep(3)
  119. try:
  120. while running:
  121. rr, wr, _ = select.select(fds, fds, [], 0.5)
  122. if len(rr) != 0:
  123. response = Firmware.read(device)
  124. for c in response:
  125. # HACK: Python 3 / PySerial hack.
  126. if isinstance(c, int):
  127. c = chr(c)
  128. if c == '\xff' or c == '\r': # HACK: odd case for our mystery \xff byte.
  129. # Boot started, start looking for the !boot message
  130. if data_read.startswith("!sn"):
  131. stage = progress_stage(Firmware.STAGE_BOOT)
  132. # Entered bootloader upload mode, start uploading
  133. elif data_read.startswith("!load"):
  134. got_response = True
  135. stage = progress_stage(Firmware.STAGE_UPLOADING)
  136. # Checksum error
  137. elif data_read == '!ce':
  138. running = False
  139. raise UploadChecksumError("Checksum error in {0}".format(file_path))
  140. # Bad data
  141. elif data_read == '!no':
  142. running = False
  143. raise UploadError("Incorrect data sent to bootloader.")
  144. # Firmware upload complete
  145. elif data_read == '!ok':
  146. running = False
  147. stage = progress_stage(Firmware.STAGE_DONE)
  148. # All other responses are valid during upload.
  149. else:
  150. got_response = True
  151. if stage == Firmware.STAGE_UPLOADING:
  152. progress_stage(stage)
  153. data_read = ''
  154. elif c == '\n':
  155. pass
  156. else:
  157. data_read += c
  158. if len(wr) != 0:
  159. # Reboot device
  160. if stage in [Firmware.STAGE_START, Firmware.STAGE_WAITING]:
  161. device.write('=')
  162. stage = progress_stage(Firmware.STAGE_WAITING_ON_LOADER)
  163. # Enter bootloader
  164. elif stage == Firmware.STAGE_BOOT:
  165. device.write('=')
  166. stage = progress_stage(Firmware.STAGE_LOAD)
  167. # Upload firmware
  168. elif stage == Firmware.STAGE_UPLOADING:
  169. if len(write_queue) > 0 and got_response == True:
  170. got_response = False
  171. device.write(write_queue.popleft())
  172. except UploadError as err:
  173. stage = progress_stage(Firmware.STAGE_ERROR, error=str(err))
  174. else:
  175. stage = progress_stage(Firmware.STAGE_DONE)