update.py 9.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272
  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. 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 = "firmware.dfu" # used to be basename(self.args.dfu)
  67. radiobin_basename = "radio.bin" # used to be basename(self.args.radiobin)
  68. resources_basename = ""
  69. radio_version = 0
  70. radio_meta = None
  71. radio_addr = self.args.radioaddr
  72. if self.args.radiobin:
  73. if not self.args.radiotype:
  74. raise ValueError("Missing --radiotype")
  75. radio_meta = CoproBinary(self.args.radiobin)
  76. radio_version = self.copro_version_as_int(radio_meta, self.args.radiotype)
  77. if (
  78. get_stack_type(self.args.radiotype) not in self.WHITELISTED_STACK_TYPES
  79. and self.args.disclaimer != "yes"
  80. ):
  81. self.logger.error(
  82. f"You are trying to bundle a non-standard stack type '{self.args.radiotype}'."
  83. )
  84. self.disclaimer()
  85. return 1
  86. if radio_addr == 0:
  87. radio_addr = radio_meta.get_flash_load_addr()
  88. self.logger.info(
  89. f"Using guessed radio address 0x{radio_addr:08X}, verify with Release_Notes"
  90. " or specify --radioaddr"
  91. )
  92. if not exists(self.args.directory):
  93. os.makedirs(self.args.directory)
  94. shutil.copyfile(self.args.stage, join(self.args.directory, stage_basename))
  95. dfu_size = 0
  96. if self.args.dfu:
  97. dfu_size = os.stat(self.args.dfu).st_size
  98. shutil.copyfile(self.args.dfu, join(self.args.directory, dfu_basename))
  99. if radiobin_basename:
  100. shutil.copyfile(
  101. self.args.radiobin, join(self.args.directory, radiobin_basename)
  102. )
  103. if self.args.resources:
  104. resources_basename = self.RESOURCE_FILE_NAME
  105. if not self.package_resources(
  106. self.args.resources, join(self.args.directory, resources_basename)
  107. ):
  108. return 3
  109. if not self.layout_check(dfu_size, radio_addr):
  110. self.logger.warn("Memory layout looks suspicious")
  111. if not self.args.disclaimer == "yes":
  112. self.disclaimer()
  113. return 2
  114. if self.args.splash:
  115. splash_args = [
  116. "-i",
  117. self.args.splash,
  118. "-o",
  119. join(self.args.directory, self.SPLASH_BIN_NAME),
  120. ]
  121. if splash_code := SlideshowMain(no_exit=True)(splash_args):
  122. self.logger.error(
  123. f"Failed to convert splash screen data: {splash_code}"
  124. )
  125. return splash_code
  126. file = FlipperFormatFile()
  127. file.setHeader(
  128. "Flipper firmware upgrade configuration", self.UPDATE_MANIFEST_VERSION
  129. )
  130. file.writeKey("Info", self.args.version)
  131. file.writeKey("Target", self.args.target[1:]) # dirty 'f' strip
  132. file.writeKey("Loader", stage_basename)
  133. file.writeComment("little-endian hex!")
  134. file.writeKey("Loader CRC", self.int2ffhex(self.crc(self.args.stage)))
  135. file.writeKey("Firmware", dfu_basename)
  136. file.writeKey("Radio", radiobin_basename or "")
  137. file.writeKey("Radio address", self.int2ffhex(radio_addr))
  138. file.writeKey("Radio version", self.int2ffhex(radio_version, 12))
  139. if radiobin_basename:
  140. file.writeKey("Radio CRC", self.int2ffhex(self.crc(self.args.radiobin)))
  141. else:
  142. file.writeKey("Radio CRC", self.int2ffhex(0))
  143. file.writeKey("Resources", resources_basename)
  144. obvalues = ObReferenceValues((), (), ())
  145. if self.args.obdata:
  146. obd = OptionBytesData(self.args.obdata)
  147. obvalues = obd.gen_values().export()
  148. file.writeComment(
  149. "NEVER EVER MESS WITH THESE VALUES, YOU WILL BRICK YOUR DEVICE"
  150. )
  151. file.writeKey("OB reference", self.bytes2ffhex(obvalues.reference))
  152. file.writeKey("OB mask", self.bytes2ffhex(obvalues.compare_mask))
  153. file.writeKey("OB write mask", self.bytes2ffhex(obvalues.write_mask))
  154. file.writeKey("Splashscreen", self.SPLASH_BIN_NAME if self.args.splash else "")
  155. file.save(join(self.args.directory, self.UPDATE_MANIFEST_NAME))
  156. return 0
  157. def layout_check(self, fw_size, radio_addr):
  158. if fw_size == 0 or radio_addr == 0:
  159. self.logger.info("Cannot validate layout for partial package")
  160. return True
  161. lfs_span = radio_addr - self.FLASH_BASE - fw_size
  162. self.logger.debug(f"Expected LFS size: {lfs_span}")
  163. lfs_span_pages = lfs_span / (4 * 1024)
  164. if lfs_span_pages < self.MIN_LFS_PAGES:
  165. self.logger.warn(
  166. f"Expected LFS size is too small (~{int(lfs_span_pages)} pages)"
  167. )
  168. return False
  169. return True
  170. def disclaimer(self):
  171. self.logger.error(
  172. "You might brick you device into a state in which you'd need an SWD programmer to fix it."
  173. )
  174. self.logger.error(
  175. "Please confirm that you REALLY want to do that with --I-understand-what-I-am-doing=yes"
  176. )
  177. def _tar_filter(self, tarinfo: tarfile.TarInfo):
  178. if len(tarinfo.name) > self.RESOURCE_ENTRY_NAME_MAX_LENGTH:
  179. self.logger.error(
  180. f"Cannot package resource: name '{tarinfo.name}' too long"
  181. )
  182. raise ValueError("Resource name too long")
  183. return tarinfo
  184. def package_resources(self, srcdir: str, dst_name: str):
  185. try:
  186. with tarfile.open(
  187. dst_name, self.RESOURCE_TAR_MODE, format=self.RESOURCE_TAR_FORMAT
  188. ) as tarball:
  189. tarball.add(
  190. srcdir,
  191. arcname="",
  192. filter=self._tar_filter,
  193. )
  194. return True
  195. except ValueError as e:
  196. self.logger.error(f"Cannot package resources: {e}")
  197. return False
  198. @staticmethod
  199. def copro_version_as_int(coprometa, stacktype):
  200. major = coprometa.img_sig.version_major
  201. minor = coprometa.img_sig.version_minor
  202. sub = coprometa.img_sig.version_sub
  203. branch = coprometa.img_sig.version_branch
  204. release = coprometa.img_sig.version_build
  205. stype = get_stack_type(stacktype)
  206. return (
  207. major
  208. | (minor << 8)
  209. | (sub << 16)
  210. | (branch << 24)
  211. | (release << 32)
  212. | (stype << 40)
  213. )
  214. @staticmethod
  215. def bytes2ffhex(value: bytes):
  216. return " ".join(f"{b:02X}" for b in value)
  217. @staticmethod
  218. def int2ffhex(value: int, n_hex_syms=8):
  219. if value:
  220. n_hex_syms = max(math.ceil(math.ceil(math.log2(value)) / 8) * 2, n_hex_syms)
  221. fmtstr = f"%0{n_hex_syms}X"
  222. hexstr = fmtstr % value
  223. return " ".join(list(Main.batch(hexstr, 2))[::-1])
  224. @staticmethod
  225. def crc(fileName):
  226. prev = 0
  227. with open(fileName, "rb") as file:
  228. for eachLine in file:
  229. prev = zlib.crc32(eachLine, prev)
  230. return prev & 0xFFFFFFFF
  231. @staticmethod
  232. def batch(iterable, n=1):
  233. l = len(iterable)
  234. for ndx in range(0, l, n):
  235. yield iterable[ndx : min(ndx + n, l)]
  236. if __name__ == "__main__":
  237. Main()()