update.py 9.0 KB

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