update.py 8.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233
  1. #!/usr/bin/env python3
  2. from flipper.app import App
  3. from flipper.utils.fff import FlipperFormatFile
  4. from flipper.assets.coprobin import CoproBinary, get_stack_type
  5. from flipper.assets.obdata import OptionBytesData, ObReferenceValues
  6. from os.path import basename, join, exists
  7. import os
  8. import shutil
  9. import zlib
  10. import tarfile
  11. import math
  12. class Main(App):
  13. UPDATE_MANIFEST_VERSION = 2
  14. UPDATE_MANIFEST_NAME = "update.fuf"
  15. # No compression, plain tar
  16. RESOURCE_TAR_MODE = "w:"
  17. RESOURCE_TAR_FORMAT = tarfile.USTAR_FORMAT
  18. RESOURCE_FILE_NAME = "resources.tar"
  19. WHITELISTED_STACK_TYPES = set(
  20. map(
  21. get_stack_type,
  22. ["BLE_FULL", "BLE_LIGHT", "BLE_BASIC"],
  23. )
  24. )
  25. FLASH_BASE = 0x8000000
  26. MIN_LFS_PAGES = 6
  27. def init(self):
  28. self.subparsers = self.parser.add_subparsers(help="sub-command help")
  29. # generate
  30. self.parser_generate = self.subparsers.add_parser(
  31. "generate", help="Generate update description file"
  32. )
  33. self.parser_generate.add_argument("-d", dest="directory", required=True)
  34. self.parser_generate.add_argument("-v", dest="version", required=True)
  35. self.parser_generate.add_argument("-t", dest="target", required=True)
  36. self.parser_generate.add_argument(
  37. "--dfu", dest="dfu", default="", required=False
  38. )
  39. self.parser_generate.add_argument("-r", dest="resources", required=False)
  40. self.parser_generate.add_argument("--stage", dest="stage", required=True)
  41. self.parser_generate.add_argument(
  42. "--radio", dest="radiobin", default="", required=False
  43. )
  44. self.parser_generate.add_argument(
  45. "--radioaddr",
  46. dest="radioaddr",
  47. type=lambda x: int(x, 16),
  48. default=0,
  49. required=False,
  50. )
  51. self.parser_generate.add_argument(
  52. "--radiotype", dest="radiotype", required=False
  53. )
  54. self.parser_generate.add_argument("--obdata", dest="obdata", required=False)
  55. self.parser_generate.add_argument(
  56. "--I-understand-what-I-am-doing", dest="disclaimer", required=False
  57. )
  58. self.parser_generate.set_defaults(func=self.generate)
  59. def generate(self):
  60. stage_basename = basename(self.args.stage)
  61. dfu_basename = basename(self.args.dfu)
  62. radiobin_basename = basename(self.args.radiobin)
  63. resources_basename = ""
  64. radio_version = 0
  65. radio_meta = None
  66. radio_addr = self.args.radioaddr
  67. if self.args.radiobin:
  68. if not self.args.radiotype:
  69. raise ValueError("Missing --radiotype")
  70. radio_meta = CoproBinary(self.args.radiobin)
  71. radio_version = self.copro_version_as_int(radio_meta, self.args.radiotype)
  72. if (
  73. get_stack_type(self.args.radiotype) not in self.WHITELISTED_STACK_TYPES
  74. and self.args.disclaimer != "yes"
  75. ):
  76. self.logger.error(
  77. f"You are trying to bundle a non-standard stack type '{self.args.radiotype}'."
  78. )
  79. self.disclaimer()
  80. return 1
  81. if radio_addr == 0:
  82. radio_addr = radio_meta.get_flash_load_addr()
  83. self.logger.info(
  84. f"Using guessed radio address 0x{radio_addr:08X}, verify with Release_Notes"
  85. " or specify --radioaddr"
  86. )
  87. if not exists(self.args.directory):
  88. os.makedirs(self.args.directory)
  89. shutil.copyfile(self.args.stage, join(self.args.directory, stage_basename))
  90. dfu_size = 0
  91. if self.args.dfu:
  92. dfu_size = os.stat(self.args.dfu).st_size
  93. shutil.copyfile(self.args.dfu, join(self.args.directory, dfu_basename))
  94. if radiobin_basename:
  95. shutil.copyfile(
  96. self.args.radiobin, join(self.args.directory, radiobin_basename)
  97. )
  98. if self.args.resources:
  99. resources_basename = self.RESOURCE_FILE_NAME
  100. self.package_resources(
  101. self.args.resources, join(self.args.directory, resources_basename)
  102. )
  103. if not self.layout_check(dfu_size, radio_addr):
  104. self.logger.warn("Memory layout looks suspicious")
  105. if not self.args.disclaimer == "yes":
  106. self.disclaimer()
  107. return 2
  108. file = FlipperFormatFile()
  109. file.setHeader(
  110. "Flipper firmware upgrade configuration", self.UPDATE_MANIFEST_VERSION
  111. )
  112. file.writeKey("Info", self.args.version)
  113. file.writeKey("Target", self.args.target[1:]) # dirty 'f' strip
  114. file.writeKey("Loader", stage_basename)
  115. file.writeComment("little-endian hex!")
  116. file.writeKey("Loader CRC", self.int2ffhex(self.crc(self.args.stage)))
  117. file.writeKey("Firmware", dfu_basename)
  118. file.writeKey("Radio", radiobin_basename or "")
  119. file.writeKey("Radio address", self.int2ffhex(radio_addr))
  120. file.writeKey("Radio version", self.int2ffhex(radio_version, 12))
  121. if radiobin_basename:
  122. file.writeKey("Radio CRC", self.int2ffhex(self.crc(self.args.radiobin)))
  123. else:
  124. file.writeKey("Radio CRC", self.int2ffhex(0))
  125. file.writeKey("Resources", resources_basename)
  126. obvalues = ObReferenceValues((), (), ())
  127. if self.args.obdata:
  128. obd = OptionBytesData(self.args.obdata)
  129. obvalues = obd.gen_values().export()
  130. file.writeComment(
  131. "NEVER EVER MESS WITH THESE VALUES, YOU WILL BRICK YOUR DEVICE"
  132. )
  133. file.writeKey("OB reference", self.bytes2ffhex(obvalues.reference))
  134. file.writeKey("OB mask", self.bytes2ffhex(obvalues.compare_mask))
  135. file.writeKey("OB write mask", self.bytes2ffhex(obvalues.write_mask))
  136. file.save(join(self.args.directory, self.UPDATE_MANIFEST_NAME))
  137. return 0
  138. def layout_check(self, fw_size, radio_addr):
  139. if fw_size == 0 or radio_addr == 0:
  140. self.logger.info("Cannot validate layout for partial package")
  141. return True
  142. lfs_span = radio_addr - self.FLASH_BASE - fw_size
  143. self.logger.debug(f"Expected LFS size: {lfs_span}")
  144. lfs_span_pages = lfs_span / (4 * 1024)
  145. if lfs_span_pages < self.MIN_LFS_PAGES:
  146. self.logger.warn(
  147. f"Expected LFS size is too small (~{int(lfs_span_pages)} pages)"
  148. )
  149. return False
  150. return True
  151. def disclaimer(self):
  152. self.logger.error(
  153. "You might brick you device into a state in which you'd need an SWD programmer to fix it."
  154. )
  155. self.logger.error(
  156. "Please confirm that you REALLY want to do that with --I-understand-what-I-am-doing=yes"
  157. )
  158. def package_resources(self, srcdir: str, dst_name: str):
  159. with tarfile.open(
  160. dst_name, self.RESOURCE_TAR_MODE, format=self.RESOURCE_TAR_FORMAT
  161. ) as tarball:
  162. tarball.add(srcdir, arcname="")
  163. @staticmethod
  164. def copro_version_as_int(coprometa, stacktype):
  165. major = coprometa.img_sig.version_major
  166. minor = coprometa.img_sig.version_minor
  167. sub = coprometa.img_sig.version_sub
  168. branch = coprometa.img_sig.version_branch
  169. release = coprometa.img_sig.version_build
  170. stype = get_stack_type(stacktype)
  171. return (
  172. major
  173. | (minor << 8)
  174. | (sub << 16)
  175. | (branch << 24)
  176. | (release << 32)
  177. | (stype << 40)
  178. )
  179. @staticmethod
  180. def bytes2ffhex(value: bytes):
  181. return " ".join(f"{b:02X}" for b in value)
  182. @staticmethod
  183. def int2ffhex(value: int, n_hex_syms=8):
  184. if value:
  185. n_hex_syms = math.ceil(math.ceil(math.log2(value)) / 8) * 2
  186. fmtstr = f"%0{n_hex_syms}X"
  187. hexstr = fmtstr % value
  188. return " ".join(list(Main.batch(hexstr, 2))[::-1])
  189. @staticmethod
  190. def crc(fileName):
  191. prev = 0
  192. with open(fileName, "rb") as file:
  193. for eachLine in file:
  194. prev = zlib.crc32(eachLine, prev)
  195. return prev & 0xFFFFFFFF
  196. @staticmethod
  197. def batch(iterable, n=1):
  198. l = len(iterable)
  199. for ndx in range(0, l, n):
  200. yield iterable[ndx : min(ndx + n, l)]
  201. if __name__ == "__main__":
  202. Main()()