update.py 6.3 KB

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