update.py 9.9 KB

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