assets.py 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274
  1. #!/usr/bin/env python3
  2. from flipper.app import App
  3. from flipper.assets.icon import file2image
  4. import logging
  5. import argparse
  6. import subprocess
  7. import io
  8. import os
  9. import sys
  10. ICONS_SUPPORTED_FORMATS = ["png"]
  11. ICONS_TEMPLATE_H_HEADER = """#pragma once
  12. #include <gui/icon.h>
  13. """
  14. ICONS_TEMPLATE_H_ICON_NAME = "extern const Icon {name};\n"
  15. ICONS_TEMPLATE_C_HEADER = """#include \"assets_icons.h\"
  16. #include <gui/icon_i.h>
  17. """
  18. ICONS_TEMPLATE_C_FRAME = "const uint8_t {name}[] = {data};\n"
  19. ICONS_TEMPLATE_C_DATA = "const uint8_t* const {name}[] = {data};\n"
  20. ICONS_TEMPLATE_C_ICONS = "const Icon {name} = {{.width={width},.height={height},.frame_count={frame_count},.frame_rate={frame_rate},.frames=_{name}}};\n"
  21. class Main(App):
  22. def init(self):
  23. # command args
  24. self.subparsers = self.parser.add_subparsers(help="sub-command help")
  25. self.parser_icons = self.subparsers.add_parser(
  26. "icons", help="Process icons and build icon registry"
  27. )
  28. self.parser_icons.add_argument("input_directory", help="Source directory")
  29. self.parser_icons.add_argument("output_directory", help="Output directory")
  30. self.parser_icons.set_defaults(func=self.icons)
  31. self.parser_manifest = self.subparsers.add_parser(
  32. "manifest", help="Create directory Manifest"
  33. )
  34. self.parser_manifest.add_argument("local_path", help="local_path")
  35. self.parser_manifest.set_defaults(func=self.manifest)
  36. self.parser_copro = self.subparsers.add_parser(
  37. "copro", help="Gather copro binaries for packaging"
  38. )
  39. self.parser_copro.add_argument("cube_dir", help="Path to Cube folder")
  40. self.parser_copro.add_argument("output_dir", help="Path to output folder")
  41. self.parser_copro.add_argument("mcu", help="MCU series as in copro folder")
  42. self.parser_copro.add_argument(
  43. "--cube_ver", dest="cube_ver", help="Cube version", required=True
  44. )
  45. self.parser_copro.add_argument(
  46. "--stack_type", dest="stack_type", help="Stack type", required=True
  47. )
  48. self.parser_copro.add_argument(
  49. "--stack_file",
  50. dest="stack_file",
  51. help="Stack file name in copro folder",
  52. required=True,
  53. )
  54. self.parser_copro.add_argument(
  55. "--stack_addr",
  56. dest="stack_addr",
  57. help="Stack flash address, as per release_notes",
  58. type=lambda x: int(x, 16),
  59. default=0,
  60. required=False,
  61. )
  62. self.parser_copro.set_defaults(func=self.copro)
  63. self.parser_dolphin = self.subparsers.add_parser(
  64. "dolphin", help="Assemble dolphin resources"
  65. )
  66. self.parser_dolphin.add_argument(
  67. "-s",
  68. "--symbol-name",
  69. help="Symbol and file name in dolphin output directory",
  70. default=None,
  71. )
  72. self.parser_dolphin.add_argument(
  73. "input_directory", help="Dolphin source directory"
  74. )
  75. self.parser_dolphin.add_argument(
  76. "output_directory", help="Dolphin output directory"
  77. )
  78. self.parser_dolphin.set_defaults(func=self.dolphin)
  79. def _icon2header(self, file):
  80. image = file2image(file)
  81. return image.width, image.height, image.data_as_carray()
  82. def _iconIsSupported(self, filename):
  83. extension = filename.lower().split(".")[-1]
  84. return extension in ICONS_SUPPORTED_FORMATS
  85. def icons(self):
  86. self.logger.debug(f"Converting icons")
  87. icons_c = open(
  88. os.path.join(self.args.output_directory, "assets_icons.c"),
  89. "w",
  90. newline="\n",
  91. )
  92. icons_c.write(ICONS_TEMPLATE_C_HEADER)
  93. icons = []
  94. # Traverse icons tree, append image data to source file
  95. for dirpath, dirnames, filenames in os.walk(self.args.input_directory):
  96. self.logger.debug(f"Processing directory {dirpath}")
  97. dirnames.sort()
  98. filenames.sort()
  99. if not filenames:
  100. continue
  101. if "frame_rate" in filenames:
  102. self.logger.debug(f"Folder contatins animation")
  103. icon_name = "A_" + os.path.split(dirpath)[1].replace("-", "_")
  104. width = height = None
  105. frame_count = 0
  106. frame_rate = 0
  107. frame_names = []
  108. for filename in sorted(filenames):
  109. fullfilename = os.path.join(dirpath, filename)
  110. if filename == "frame_rate":
  111. frame_rate = int(open(fullfilename, "r").read().strip())
  112. continue
  113. elif not self._iconIsSupported(filename):
  114. continue
  115. self.logger.debug(f"Processing animation frame {filename}")
  116. temp_width, temp_height, data = self._icon2header(fullfilename)
  117. if width is None:
  118. width = temp_width
  119. if height is None:
  120. height = temp_height
  121. assert width == temp_width
  122. assert height == temp_height
  123. frame_name = f"_{icon_name}_{frame_count}"
  124. frame_names.append(frame_name)
  125. icons_c.write(
  126. ICONS_TEMPLATE_C_FRAME.format(name=frame_name, data=data)
  127. )
  128. frame_count += 1
  129. assert frame_rate > 0
  130. assert frame_count > 0
  131. icons_c.write(
  132. ICONS_TEMPLATE_C_DATA.format(
  133. name=f"_{icon_name}", data=f'{{{",".join(frame_names)}}}'
  134. )
  135. )
  136. icons_c.write("\n")
  137. icons.append((icon_name, width, height, frame_rate, frame_count))
  138. else:
  139. # process icons
  140. for filename in filenames:
  141. if not self._iconIsSupported(filename):
  142. continue
  143. self.logger.debug(f"Processing icon {filename}")
  144. icon_name = "I_" + "_".join(filename.split(".")[:-1]).replace(
  145. "-", "_"
  146. )
  147. fullfilename = os.path.join(dirpath, filename)
  148. width, height, data = self._icon2header(fullfilename)
  149. frame_name = f"_{icon_name}_0"
  150. icons_c.write(
  151. ICONS_TEMPLATE_C_FRAME.format(name=frame_name, data=data)
  152. )
  153. icons_c.write(
  154. ICONS_TEMPLATE_C_DATA.format(
  155. name=f"_{icon_name}", data=f"{{{frame_name}}}"
  156. )
  157. )
  158. icons_c.write("\n")
  159. icons.append((icon_name, width, height, 0, 1))
  160. # Create array of images:
  161. self.logger.debug(f"Finalizing source file")
  162. for name, width, height, frame_rate, frame_count in icons:
  163. icons_c.write(
  164. ICONS_TEMPLATE_C_ICONS.format(
  165. name=name,
  166. width=width,
  167. height=height,
  168. frame_rate=frame_rate,
  169. frame_count=frame_count,
  170. )
  171. )
  172. icons_c.write("\n")
  173. icons_c.close()
  174. # Create Public Header
  175. self.logger.debug(f"Creating header")
  176. icons_h = open(
  177. os.path.join(self.args.output_directory, "assets_icons.h"),
  178. "w",
  179. newline="\n",
  180. )
  181. icons_h.write(ICONS_TEMPLATE_H_HEADER)
  182. for name, width, height, frame_rate, frame_count in icons:
  183. icons_h.write(ICONS_TEMPLATE_H_ICON_NAME.format(name=name))
  184. icons_h.close()
  185. self.logger.debug(f"Done")
  186. return 0
  187. def manifest(self):
  188. from flipper.assets.manifest import Manifest
  189. directory_path = os.path.normpath(self.args.local_path)
  190. if not os.path.isdir(directory_path):
  191. self.logger.error(f'"{directory_path}" is not a directory')
  192. exit(255)
  193. manifest_file = os.path.join(directory_path, "Manifest")
  194. old_manifest = Manifest()
  195. if os.path.exists(manifest_file):
  196. self.logger.info("Manifest is present, loading to compare")
  197. old_manifest.load(manifest_file)
  198. self.logger.info(
  199. f'Creating temporary Manifest for directory "{directory_path}"'
  200. )
  201. new_manifest = Manifest()
  202. new_manifest.create(directory_path)
  203. self.logger.info(f"Comparing new manifest with existing")
  204. only_in_old, changed, only_in_new = Manifest.compare(old_manifest, new_manifest)
  205. for record in only_in_old:
  206. self.logger.info(f"Only in old: {record}")
  207. for record in changed:
  208. self.logger.info(f"Changed: {record}")
  209. for record in only_in_new:
  210. self.logger.info(f"Only in new: {record}")
  211. if any((only_in_old, changed, only_in_new)):
  212. self.logger.warning("Manifests are different, updating")
  213. new_manifest.save(manifest_file)
  214. else:
  215. self.logger.info("Manifest is up-to-date!")
  216. self.logger.info(f"Complete")
  217. return 0
  218. def copro(self):
  219. from flipper.assets.copro import Copro
  220. self.logger.info(f"Bundling coprocessor binaries")
  221. copro = Copro(self.args.mcu)
  222. self.logger.info(f"Loading CUBE info")
  223. copro.loadCubeInfo(self.args.cube_dir, self.args.cube_ver)
  224. self.logger.info(f"Bundling")
  225. copro.bundle(
  226. self.args.output_dir,
  227. self.args.stack_file,
  228. self.args.stack_type,
  229. self.args.stack_addr,
  230. )
  231. self.logger.info(f"Complete")
  232. return 0
  233. def dolphin(self):
  234. from flipper.assets.dolphin import Dolphin
  235. self.logger.info(f"Processing Dolphin sources")
  236. dolphin = Dolphin()
  237. self.logger.info(f"Loading data")
  238. dolphin.load(self.args.input_directory)
  239. self.logger.info(f"Packing")
  240. dolphin.pack(self.args.output_directory, self.args.symbol_name)
  241. self.logger.info(f"Complete")
  242. return 0
  243. if __name__ == "__main__":
  244. Main()()