flash_imaging.py 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318
  1. # Copyright 2024 Google LLC
  2. #
  3. # Licensed under the Apache License, Version 2.0 (the "License");
  4. # you may not use this file except in compliance with the License.
  5. # You may obtain a copy of the License at
  6. #
  7. # http://www.apache.org/licenses/LICENSE-2.0
  8. #
  9. # Unless required by applicable law or agreed to in writing, software
  10. # distributed under the License is distributed on an "AS IS" BASIS,
  11. # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  12. # See the License for the specific language governing permissions and
  13. # limitations under the License.
  14. from __future__ import absolute_import
  15. import collections
  16. import struct
  17. import time
  18. import pebble.pulse2.exceptions
  19. from .. import exceptions
  20. class EraseCommand(object):
  21. command_type = 1
  22. command_struct = struct.Struct('<BII')
  23. response_type = 128
  24. response_struct = struct.Struct('<xII?')
  25. Response = collections.namedtuple(
  26. 'EraseResponse', 'address length complete')
  27. def __init__(self, address, length):
  28. self.address = address
  29. self.length = length
  30. @property
  31. def packet(self):
  32. return self.command_struct.pack(
  33. self.command_type, self.address, self.length)
  34. def parse_response(self, response):
  35. if response[0] != self.response_type:
  36. raise exceptions.ResponseParseError(
  37. 'Unexpected response: %r' % response)
  38. unpacked = self.Response._make(self.response_struct.unpack(response))
  39. if unpacked.address != self.address or unpacked.length != self.length:
  40. raise exceptions.ResponseParseError(
  41. 'Response does not match command: '
  42. 'address=%#.08x legnth=%d (expected %#.08x, %d)' % (
  43. unpacked.address, unpacked.length, self.address,
  44. self.length))
  45. return unpacked
  46. class WriteCommand(object):
  47. command_type = 2
  48. command_struct = struct.Struct('<BI')
  49. header_len = command_struct.size
  50. def __init__(self, address, data):
  51. self.address = address
  52. self.data = data
  53. @property
  54. def packet(self):
  55. header = self.command_struct.pack(self.command_type, self.address)
  56. return header + self.data
  57. class WriteResponse(object):
  58. response_type = 129
  59. response_struct = struct.Struct('<xII?')
  60. Response = collections.namedtuple(
  61. 'WriteResponse', 'address length complete')
  62. @classmethod
  63. def parse(cls, response):
  64. if response[0] != cls.response_type:
  65. raise exceptions.ResponseParseError(
  66. 'Unexpected response: %r' % response)
  67. return cls.Response._make(cls.response_struct.unpack(response))
  68. class CrcCommand(object):
  69. command_type = 3
  70. command_struct = struct.Struct('<BII')
  71. response_type = 130
  72. response_struct = struct.Struct('<xIII')
  73. Response = collections.namedtuple('CrcResponse', 'address length crc')
  74. def __init__(self, address, length):
  75. self.address = address
  76. self.length = length
  77. @property
  78. def packet(self):
  79. return self.command_struct.pack(self.command_type, self.address,
  80. self.length)
  81. def parse_response(self, response):
  82. if response[0] != self.response_type:
  83. raise exceptions.ResponseParseError(
  84. 'Unexpected response: %r' % response)
  85. unpacked = self.Response._make(self.response_struct.unpack(response))
  86. if unpacked.address != self.address or unpacked.length != self.length:
  87. raise exceptions.ResponseParseError(
  88. 'Response does not match command: '
  89. 'address=%#.08x legnth=%d (expected %#.08x, %d)' % (
  90. unpacked.address, unpacked.length, self.address,
  91. self.length))
  92. return unpacked
  93. class QueryFlashRegionCommand(object):
  94. command_type = 4
  95. command_struct = struct.Struct('<BB')
  96. REGION_PRF = 1
  97. REGION_SYSTEM_RESOURCES = 2
  98. response_type = 131
  99. response_struct = struct.Struct('<xBII')
  100. Response = collections.namedtuple(
  101. 'FlashRegionGeometry', 'region address length')
  102. def __init__(self, region):
  103. self.region = region
  104. @property
  105. def packet(self):
  106. return self.command_struct.pack(self.command_type, self.region)
  107. def parse_response(self, response):
  108. if response[0] != self.response_type:
  109. raise exceptions.ResponseParseError(
  110. 'Unexpected response: %r' % response)
  111. unpacked = self.Response._make(self.response_struct.unpack(response))
  112. if unpacked.address == 0 and unpacked.length == 0:
  113. raise exceptions.RegionDoesNotExist(self.region)
  114. return unpacked
  115. class FinalizeFlashRegionCommand(object):
  116. command_type = 5
  117. command_struct = struct.Struct('<BB')
  118. response_type = 132
  119. response_struct = struct.Struct('<xB')
  120. def __init__(self, region):
  121. self.region = region
  122. @property
  123. def packet(self):
  124. return self.command_struct.pack(self.command_type, self.region)
  125. def parse_response(self, response):
  126. if response[0] != self.response_type:
  127. raise exceptions.ResponseParseError(
  128. 'Unexpected response: %r' % response)
  129. region, = self.response_struct.unpack(response)
  130. if region != self.region:
  131. raise exceptions.ResponseParseError(
  132. 'Response does not match command: '
  133. 'response is for region %d (expected %d)' % (
  134. region, self.region))
  135. class FlashImaging(object):
  136. PORT_NUMBER = 0x0002
  137. RESP_BAD_CMD = 192
  138. RESP_INTERNAL_ERROR = 193
  139. REGION_PRF = QueryFlashRegionCommand.REGION_PRF
  140. REGION_SYSTEM_RESOURCES = QueryFlashRegionCommand.REGION_SYSTEM_RESOURCES
  141. def __init__(self, link):
  142. self.socket = link.open_socket('best-effort', self.PORT_NUMBER)
  143. def close(self):
  144. self.socket.close()
  145. def erase(self, address, length):
  146. cmd = EraseCommand(address, length)
  147. ack_received = False
  148. retries = 0
  149. while retries < 10:
  150. if not ack_received:
  151. self.socket.send(cmd.packet)
  152. try:
  153. packet = self.socket.receive(timeout=5 if ack_received else 1.5)
  154. response = cmd.parse_response(packet)
  155. ack_received = True
  156. if response.complete:
  157. return
  158. except pebble.pulse2.exceptions.ReceiveQueueEmpty:
  159. ack_received = False
  160. retries += 1
  161. continue
  162. raise exceptions.CommandTimedOut
  163. def write(self, address, data, max_retries=5, max_in_flight=5,
  164. progress_cb=None):
  165. mtu = self.socket.mtu - WriteCommand.header_len
  166. assert(mtu > 0)
  167. unsent = collections.deque()
  168. for offset in range(0, len(data), mtu):
  169. segment = data[offset:offset+mtu]
  170. assert(len(segment))
  171. seg_address = address + offset
  172. unsent.appendleft(
  173. (seg_address, WriteCommand(seg_address, segment), 0))
  174. in_flight = collections.OrderedDict()
  175. retries = 0
  176. while unsent or in_flight:
  177. try:
  178. while True:
  179. # Process ACKs (if any)
  180. ack = WriteResponse.parse(
  181. self.socket.receive(block=False))
  182. try:
  183. cmd, _, _ = in_flight[ack.address]
  184. del in_flight[ack.address]
  185. except KeyError:
  186. for seg_address, cmd, retry_count in unsent:
  187. if seg_address == ack.address:
  188. if retry_count == 0:
  189. # ACK for a segment we never sent?!
  190. raise exceptions.WriteError(
  191. 'Received ACK for an unsent segment: '
  192. '%#.08x' % ack.address)
  193. # Got an ACK for a sent (but timed out) segment
  194. unsent.remove((seg_address, cmd, retry_count))
  195. break
  196. else:
  197. raise exceptions.WriteError(
  198. 'Received ACK for an unknown segment: '
  199. '%#.08x' % ack.address)
  200. if len(cmd.data) != ack.length:
  201. raise exceptions.WriteError(
  202. 'ACK length %d != data length %d' % (
  203. ack.length, len(cmd.data)))
  204. assert(ack.complete)
  205. if progress_cb:
  206. progress_cb(True)
  207. except pebble.pulse2.exceptions.ReceiveQueueEmpty:
  208. pass
  209. # Retry any in_flight writes where the ACK has timed out
  210. to_retry = []
  211. timeout_time = time.time() - 0.5
  212. for (seg_address,
  213. (cmd, send_time, retry_count)) in in_flight.copy().items():
  214. if send_time > timeout_time:
  215. # in_flight is an OrderedDict so iteration is in
  216. # chronological order.
  217. break
  218. if retry_count >= max_retries:
  219. raise exceptions.WriteError(
  220. 'Segment %#.08x exceeded the max retry count (%d)' % (
  221. seg_address, max_retries))
  222. # Enqueue the packet again to resend later.
  223. del in_flight[seg_address]
  224. unsent.appendleft((seg_address, cmd, retry_count+1))
  225. retries += 1
  226. if progress_cb:
  227. progress_cb(False)
  228. # Send out fresh segments
  229. try:
  230. while len(in_flight) < max_in_flight:
  231. seg_address, cmd, retry_count = unsent.pop()
  232. self.socket.send(cmd.packet)
  233. in_flight[cmd.address] = (cmd, time.time(), retry_count)
  234. except IndexError:
  235. pass
  236. # Give other threads a chance to run
  237. time.sleep(0)
  238. return retries
  239. def _command_and_response(self, cmd, timeout=0.5):
  240. for attempt in range(5):
  241. self.socket.send(cmd.packet)
  242. try:
  243. packet = self.socket.receive(timeout=timeout)
  244. return cmd.parse_response(packet)
  245. except pebble.pulse2.exceptions.ReceiveQueueEmpty:
  246. pass
  247. raise exceptions.CommandTimedOut
  248. def crc(self, address, length):
  249. cmd = CrcCommand(address, length)
  250. return self._command_and_response(cmd, timeout=1).crc
  251. def query_region_geometry(self, region):
  252. cmd = QueryFlashRegionCommand(region)
  253. return self._command_and_response(cmd)
  254. def finalize_region(self, region):
  255. cmd = FinalizeFlashRegionCommand(region)
  256. return self._command_and_response(cmd)