update.py 6.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178
  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
  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. def init(self):
  20. self.subparsers = self.parser.add_subparsers(help="sub-command help")
  21. # generate
  22. self.parser_generate = self.subparsers.add_parser(
  23. "generate", help="Generate update description file"
  24. )
  25. self.parser_generate.add_argument("-d", dest="directory", required=True)
  26. self.parser_generate.add_argument("-v", dest="version", required=True)
  27. self.parser_generate.add_argument("-t", dest="target", required=True)
  28. self.parser_generate.add_argument(
  29. "--dfu", dest="dfu", default="", required=False
  30. )
  31. self.parser_generate.add_argument("-r", dest="resources", required=False)
  32. self.parser_generate.add_argument("--stage", dest="stage", required=True)
  33. self.parser_generate.add_argument(
  34. "--radio", dest="radiobin", default="", required=False
  35. )
  36. self.parser_generate.add_argument(
  37. "--radioaddr",
  38. dest="radioaddr",
  39. type=lambda x: int(x, 16),
  40. default=0,
  41. required=False,
  42. )
  43. self.parser_generate.add_argument(
  44. "--radiotype", dest="radiotype", required=False
  45. )
  46. self.parser_generate.add_argument("--obdata", dest="obdata", required=False)
  47. self.parser_generate.set_defaults(func=self.generate)
  48. def generate(self):
  49. stage_basename = basename(self.args.stage)
  50. dfu_basename = basename(self.args.dfu)
  51. radiobin_basename = basename(self.args.radiobin)
  52. resources_basename = ""
  53. radio_version = 0
  54. radio_meta = None
  55. radio_addr = self.args.radioaddr
  56. if self.args.radiobin:
  57. if not self.args.radiotype:
  58. raise ValueError("Missing --radiotype")
  59. radio_meta = CoproBinary(self.args.radiobin)
  60. radio_version = self.copro_version_as_int(radio_meta, self.args.radiotype)
  61. if radio_addr == 0:
  62. radio_addr = radio_meta.get_flash_load_addr()
  63. self.logger.info(
  64. f"Using guessed radio address 0x{radio_addr:X}, verify with Release_Notes"
  65. " or specify --radioaddr"
  66. )
  67. if not exists(self.args.directory):
  68. os.makedirs(self.args.directory)
  69. shutil.copyfile(self.args.stage, join(self.args.directory, stage_basename))
  70. if self.args.dfu:
  71. shutil.copyfile(self.args.dfu, join(self.args.directory, dfu_basename))
  72. if radiobin_basename:
  73. shutil.copyfile(
  74. self.args.radiobin, join(self.args.directory, radiobin_basename)
  75. )
  76. if self.args.resources:
  77. resources_basename = self.RESOURCE_FILE_NAME
  78. self.package_resources(
  79. self.args.resources, join(self.args.directory, resources_basename)
  80. )
  81. file = FlipperFormatFile()
  82. file.setHeader(
  83. "Flipper firmware upgrade configuration", self.UPDATE_MANIFEST_VERSION
  84. )
  85. file.writeKey("Info", self.args.version)
  86. file.writeKey("Target", self.args.target[1:]) # dirty 'f' strip
  87. file.writeKey("Loader", stage_basename)
  88. file.writeComment("little-endian hex!")
  89. file.writeKey("Loader CRC", self.int2ffhex(self.crc(self.args.stage)))
  90. file.writeKey("Firmware", dfu_basename)
  91. file.writeKey("Radio", radiobin_basename or "")
  92. file.writeKey("Radio address", self.int2ffhex(radio_addr))
  93. file.writeKey("Radio version", self.int2ffhex(radio_version, 12))
  94. if radiobin_basename:
  95. file.writeKey("Radio CRC", self.int2ffhex(self.crc(self.args.radiobin)))
  96. else:
  97. file.writeKey("Radio CRC", self.int2ffhex(0))
  98. file.writeKey("Resources", resources_basename)
  99. file.writeComment(
  100. "NEVER EVER MESS WITH THESE VALUES, YOU WILL BRICK YOUR DEVICE"
  101. )
  102. if self.args.obdata:
  103. obd = OptionBytesData(self.args.obdata)
  104. obvalues = obd.gen_values().export()
  105. file.writeKey("OB reference", self.bytes2ffhex(obvalues.reference))
  106. file.writeKey("OB mask", self.bytes2ffhex(obvalues.compare_mask))
  107. file.writeKey("OB write mask", self.bytes2ffhex(obvalues.write_mask))
  108. file.save(join(self.args.directory, self.UPDATE_MANIFEST_NAME))
  109. return 0
  110. def package_resources(self, srcdir: str, dst_name: str):
  111. with tarfile.open(
  112. dst_name, self.RESOURCE_TAR_MODE, format=self.RESOURCE_TAR_FORMAT
  113. ) as tarball:
  114. tarball.add(srcdir, arcname="")
  115. @staticmethod
  116. def copro_version_as_int(coprometa, stacktype):
  117. major = coprometa.img_sig.version_major
  118. minor = coprometa.img_sig.version_minor
  119. sub = coprometa.img_sig.version_sub
  120. branch = coprometa.img_sig.version_branch
  121. release = coprometa.img_sig.version_build
  122. stype = get_stack_type(stacktype)
  123. return (
  124. major
  125. | (minor << 8)
  126. | (sub << 16)
  127. | (branch << 24)
  128. | (release << 32)
  129. | (stype << 40)
  130. )
  131. @staticmethod
  132. def bytes2ffhex(value: bytes):
  133. return " ".join(f"{b:02X}" for b in value)
  134. @staticmethod
  135. def int2ffhex(value: int, n_hex_syms=8):
  136. if value:
  137. n_hex_syms = math.ceil(math.ceil(math.log2(value)) / 8) * 2
  138. fmtstr = f"%0{n_hex_syms}X"
  139. hexstr = fmtstr % value
  140. return " ".join(list(Main.batch(hexstr, 2))[::-1])
  141. @staticmethod
  142. def crc(fileName):
  143. prev = 0
  144. with open(fileName, "rb") as file:
  145. for eachLine in file:
  146. prev = zlib.crc32(eachLine, prev)
  147. return prev & 0xFFFFFFFF
  148. @staticmethod
  149. def batch(iterable, n=1):
  150. l = len(iterable)
  151. for ndx in range(0, l, n):
  152. yield iterable[ndx : min(ndx + n, l)]
  153. if __name__ == "__main__":
  154. Main()()