dfu_transport_ble.py 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327
  1. # Copyright (c) 2015, Nordic Semiconductor
  2. # All rights reserved.
  3. #
  4. # Redistribution and use in source and binary forms, with or without
  5. # modification, are permitted provided that the following conditions are met:
  6. #
  7. # * Redistributions of source code must retain the above copyright notice, this
  8. # list of conditions and the following disclaimer.
  9. #
  10. # * Redistributions in binary form must reproduce the above copyright notice,
  11. # this list of conditions and the following disclaimer in the documentation
  12. # and/or other materials provided with the distribution.
  13. #
  14. # * Neither the name of Nordic Semiconductor ASA nor the names of its
  15. # contributors may be used to endorse or promote products derived from
  16. # this software without specific prior written permission.
  17. #
  18. # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
  19. # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
  20. # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
  21. # DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
  22. # FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
  23. # DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
  24. # SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
  25. # CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
  26. # OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
  27. # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
  28. # Python standard library
  29. from time import sleep
  30. from datetime import datetime, timedelta
  31. import abc
  32. import logging
  33. # Nordic libraries
  34. from nordicsemi.exceptions import NordicSemiException, IllegalStateException
  35. from nordicsemi.dfu.util import int16_to_bytes
  36. from nordicsemi.dfu.dfu_transport import DfuTransport, DfuEvent
  37. logger = logging.getLogger(__name__)
  38. # BLE DFU OpCodes :
  39. class DfuOpcodesBle(object):
  40. """ DFU opcodes used during DFU communication with bootloader
  41. See http://developer.nordicsemi.com/nRF51_SDK/doc/7.2.0/s110/html/a00949.html#gafa9a52a3e6c43ccf00cf680f944d67a3
  42. for further information
  43. """
  44. INVALID_OPCODE = 0
  45. START_DFU = 1
  46. INITIALIZE_DFU = 2
  47. RECEIVE_FIRMWARE_IMAGE = 3
  48. VALIDATE_FIRMWARE_IMAGE = 4
  49. ACTIVATE_FIRMWARE_AND_RESET = 5
  50. SYSTEM_RESET = 6
  51. REQ_PKT_RCPT_NOTIFICATION = 8
  52. RESPONSE = 16
  53. PKT_RCPT_NOTIF = 17
  54. class DfuErrorCodeBle(object):
  55. """ DFU error code used during DFU communication with bootloader
  56. See http://developer.nordicsemi.com/nRF51_SDK/doc/7.2.0/s110/html/a00949.html#gafa9a52a3e6c43ccf00cf680f944d67a3
  57. for further information
  58. """
  59. SUCCESS = 1
  60. INVALID_STATE = 2
  61. NOT_SUPPORTED = 3
  62. DATA_SIZE_EXCEEDS_LIMIT = 4
  63. CRC_ERROR = 5
  64. OPERATION_FAILED = 6
  65. @staticmethod
  66. def error_code_lookup(error_code):
  67. """
  68. Returns a description lookup table for error codes received from peer.
  69. :param int error_code: Error code to parse
  70. :return str: Textual description of the error code
  71. """
  72. code_lookup = {DfuErrorCodeBle.SUCCESS: "SUCCESS",
  73. DfuErrorCodeBle.INVALID_STATE: "Invalid State",
  74. DfuErrorCodeBle.NOT_SUPPORTED: "Not Supported",
  75. DfuErrorCodeBle.DATA_SIZE_EXCEEDS_LIMIT: "Data Size Exceeds Limit",
  76. DfuErrorCodeBle.CRC_ERROR: "CRC Error",
  77. DfuErrorCodeBle.OPERATION_FAILED: "Operation Failed"}
  78. return code_lookup.get(error_code, "UNKOWN ERROR CODE")
  79. # Service UUID. For further information, look at the nRF51 SDK documentation V7.2.0:
  80. # http://developer.nordicsemi.com/nRF51_SDK/doc/7.2.0/s110/html/a00071.html#ota_spec_number
  81. UUID_DFU_SERVICE = '000015301212EFDE1523785FEABCD123'
  82. # Characteristic UUID
  83. UUID_DFU_PACKET_CHARACTERISTIC = '000015321212EFDE1523785FEABCD123'
  84. UUID_DFU_CONTROL_STATE_CHARACTERISTIC = '000015311212EFDE1523785FEABCD123'
  85. # Descriptor UUID
  86. UUID_CLIENT_CHARACTERISTIC_CONFIGURATION_DESCRIPTOR = 0x2902
  87. # NOTE: If packet receipt notification is enabled, a packet receipt
  88. # notification will be received for each 'num_of_packets_between_notif'
  89. # number of packets.
  90. #
  91. # Configuration tip: Increase this to get lesser notifications from the DFU
  92. # Target about packet receipts. Make it 0 to disable the packet receipt
  93. # notification
  94. NUM_OF_PACKETS_BETWEEN_NOTIF = 10
  95. DATA_PACKET_SIZE = 20
  96. class DfuTransportBle(DfuTransport):
  97. def __init__(self):
  98. super(DfuTransportBle, self).__init__()
  99. def open(self):
  100. super(DfuTransportBle, self).open()
  101. def is_open(self):
  102. return super(DfuTransportBle, self).is_open()
  103. def close(self):
  104. super(DfuTransportBle, self).close()
  105. def _wait_for_condition(self, condition_function, expected_condition_value=True, timeout=10,
  106. waiting_for="condition"):
  107. """
  108. Waits for condition_function to be true
  109. Will timeout after 60 seconds
  110. :param function condition_function: The function we are waiting for to return true
  111. :param str timeout_message: Message that should be logged
  112. :return:
  113. """
  114. start_time = datetime.now()
  115. while condition_function() != expected_condition_value:
  116. timeout_message = "Timeout while waiting for {0}.".format(waiting_for)
  117. timed_out = datetime.now() - start_time > timedelta(0, timeout)
  118. if timed_out:
  119. self._send_event(DfuEvent.TIMEOUT_EVENT, log_message=timeout_message)
  120. raise NordicSemiException(timeout_message)
  121. if not self.is_open():
  122. log_message = "Disconnected from device while waiting for {0}.".format(waiting_for)
  123. raise IllegalStateException(log_message)
  124. sleep(0.1)
  125. if self.get_last_error() != DfuErrorCodeBle.SUCCESS:
  126. error_message = "Error occoured while waiting for {0}. Error response {1}."
  127. error_code = DfuErrorCodeBle.error_code_lookup(self.get_last_error())
  128. error_message = error_message.format(waiting_for, error_code)
  129. self._send_event(DfuEvent.ERROR_EVENT, log_message=error_message)
  130. raise NordicSemiException(error_message)
  131. @abc.abstractmethod
  132. def send_packet_data(self, data):
  133. """
  134. Send data to the packet characteristic
  135. :param str data: The data to be sent
  136. :return:
  137. """
  138. pass
  139. @abc.abstractmethod
  140. def send_control_data(self, opcode, data=""):
  141. """
  142. Send data to the control characteristic
  143. :param int opcode: The opcode for the operation command sent to the control characteristic
  144. :param str data: The data to be sent
  145. :return:
  146. """
  147. pass
  148. @abc.abstractmethod
  149. def get_received_response(self):
  150. """
  151. Returns True if the transport layer has received a response it expected
  152. :return: bool
  153. """
  154. pass
  155. def clear_received_response(self):
  156. """
  157. Clears the received response status, sets it to False.
  158. :return:
  159. """
  160. pass
  161. @abc.abstractmethod
  162. def is_waiting_for_notification(self):
  163. """
  164. Returns True if the transport layer is waiting for a notification
  165. :return: bool
  166. """
  167. pass
  168. def set_waiting_for_notification(self):
  169. """
  170. Notifies the transport layer that it should wait for notification
  171. :return:
  172. """
  173. pass
  174. @abc.abstractmethod
  175. def get_last_error(self):
  176. """
  177. Returns the last error code
  178. :return: DfuErrorCodeBle
  179. """
  180. pass
  181. def _start_dfu(self, program_mode, image_size_packet):
  182. logger.debug("Sending 'START DFU' command")
  183. self.send_control_data(DfuOpcodesBle.START_DFU, chr(program_mode))
  184. logger.debug("Sending image size")
  185. self.send_packet_data(image_size_packet)
  186. self._wait_for_condition(self.get_received_response, waiting_for="response for START DFU")
  187. self.clear_received_response()
  188. def send_start_dfu(self, program_mode, softdevice_size=0, bootloader_size=0, app_size=0):
  189. super(DfuTransportBle, self).send_start_dfu(program_mode, softdevice_size, bootloader_size, app_size)
  190. image_size_packet = DfuTransport.create_image_size_packet(softdevice_size, bootloader_size, app_size)
  191. self._send_event(DfuEvent.PROGRESS_EVENT, progress=0, log_message="Setting up transfer...")
  192. try:
  193. self._start_dfu(program_mode, image_size_packet)
  194. except IllegalStateException:
  195. # We got disconnected. Try to send Start DFU again in case of buttonless dfu.
  196. self.close()
  197. self.open()
  198. if not self.is_open():
  199. raise IllegalStateException("Failed to reopen transport backend.")
  200. self._start_dfu(program_mode, image_size_packet)
  201. def send_init_packet(self, init_packet):
  202. super(DfuTransportBle, self).send_init_packet(init_packet)
  203. init_packet_start = chr(0x00)
  204. init_packet_end = chr(0x01)
  205. logger.debug("Sending 'INIT DFU' command")
  206. self.send_control_data(DfuOpcodesBle.INITIALIZE_DFU, init_packet_start)
  207. logger.debug("Sending init data")
  208. for i in range(0, len(init_packet), DATA_PACKET_SIZE):
  209. data_to_send = init_packet[i:i + DATA_PACKET_SIZE]
  210. self.send_packet_data(data_to_send)
  211. logger.debug("Sending 'Init Packet Complete' command")
  212. self.send_control_data(DfuOpcodesBle.INITIALIZE_DFU, init_packet_end)
  213. self._wait_for_condition(self.get_received_response, timeout=60, waiting_for="response for INITIALIZE DFU")
  214. self.clear_received_response()
  215. if NUM_OF_PACKETS_BETWEEN_NOTIF:
  216. packet = int16_to_bytes(NUM_OF_PACKETS_BETWEEN_NOTIF)
  217. logger.debug("Send number of packets before device sends notification")
  218. self.send_control_data(DfuOpcodesBle.REQ_PKT_RCPT_NOTIFICATION, packet)
  219. def send_firmware(self, firmware):
  220. def progress_percentage(part, complete):
  221. """
  222. Calculate progress percentage
  223. :param int part: Part value
  224. :param int complete: Completed value
  225. :return: int: Percentage complete
  226. """
  227. return min(100, (part + DATA_PACKET_SIZE) * 100 / complete)
  228. super(DfuTransportBle, self).send_firmware(firmware)
  229. packets_sent = 0
  230. last_progress_update = -1 # Last packet sequence number when an update was fired to the event system
  231. bin_size = len(firmware)
  232. logger.debug("Send 'RECEIVE FIRMWARE IMAGE' command")
  233. self.send_control_data(DfuOpcodesBle.RECEIVE_FIRMWARE_IMAGE)
  234. for i in range(0, bin_size, DATA_PACKET_SIZE):
  235. progress = progress_percentage(i, bin_size)
  236. if progress != last_progress_update:
  237. self._send_event(DfuEvent.PROGRESS_EVENT, progress=progress, log_message="Uploading firmware")
  238. last_progress_update = progress
  239. self._wait_for_condition(self.is_waiting_for_notification, expected_condition_value=False,
  240. waiting_for="notification from device")
  241. data_to_send = firmware[i:i + DATA_PACKET_SIZE]
  242. log_message = "Sending Firmware bytes [{0}, {1}]".format(i, i + len(data_to_send))
  243. logger.debug(log_message)
  244. packets_sent += 1
  245. if NUM_OF_PACKETS_BETWEEN_NOTIF != 0:
  246. if (packets_sent % NUM_OF_PACKETS_BETWEEN_NOTIF) == 0:
  247. self.set_waiting_for_notification()
  248. self.send_packet_data(data_to_send)
  249. self._wait_for_condition(self.get_received_response, waiting_for="response for RECEIVE FIRMWARE IMAGE")
  250. self.clear_received_response()
  251. def send_validate_firmware(self):
  252. super(DfuTransportBle, self).send_validate_firmware()
  253. logger.debug("Sending 'VALIDATE FIRMWARE IMAGE' command")
  254. self.send_control_data(DfuOpcodesBle.VALIDATE_FIRMWARE_IMAGE)
  255. self._wait_for_condition(self.get_received_response, waiting_for="response for VALIDATE FIRMWARE IMAGE")
  256. self.clear_received_response()
  257. logger.info("Firmware validated OK.")
  258. def send_activate_firmware(self):
  259. super(DfuTransportBle, self).send_activate_firmware()
  260. logger.debug("Sending 'ACTIVATE FIRMWARE AND RESET' command")
  261. self.send_control_data(DfuOpcodesBle.ACTIVATE_FIRMWARE_AND_RESET)