build_ota_image.py 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582
  1. #!/usr/bin/env python3
  2. #
  3. # Build Actions SoC OTA firmware
  4. #
  5. # Copyright (c) 2019 Actions Semiconductor Co., Ltd
  6. #
  7. # SPDX-License-Identifier: Apache-2.0
  8. #
  9. import os
  10. import sys
  11. import time
  12. import struct
  13. import argparse
  14. import platform
  15. import subprocess
  16. import array
  17. import hashlib
  18. import shutil
  19. import zipfile
  20. import xml.etree.ElementTree as ET
  21. import zlib
  22. import lzma
  23. # lzma compress for ota and temp bin
  24. LZMA_OTA_BIN = 1
  25. LZMA_TEMP_BIN = 1
  26. script_path = os.path.split(os.path.realpath(__file__))[0]
  27. def align_down(data, alignment):
  28. return data & ~(alignment - 1)
  29. def align_up(data, alignment):
  30. return align_down(data + alignment - 1, alignment)
  31. def calc_pad_length(length, alignment):
  32. return align_up(length, alignment) - length
  33. def crc32_file(filename):
  34. if os.path.isfile(filename):
  35. with open(filename, 'rb') as f:
  36. crc = zlib.crc32(f.read(), 0) & 0xffffffff
  37. return crc
  38. return 0
  39. def panic(err_msg):
  40. print('\033[1;31;40m')
  41. print('FW: Error: %s\n' %err_msg)
  42. print('\033[0m')
  43. sys.exit(1)
  44. def print_notice(msg):
  45. print('\033[1;32;40m%s\033[0m' %msg)
  46. '''
  47. /* OTA image */
  48. typedef struct
  49. {
  50. uint8 filename[12];
  51. uint8 reserved1[4];
  52. uint32 offset;
  53. uint32 length;
  54. uint8 reserved2[4];
  55. uint32 checksum;
  56. }atf_dir_t;
  57. struct ota_fw_hdr {
  58. u32_t magic;
  59. u32_t header_checksum;
  60. u16_t header_version;
  61. u16_t header_size;
  62. u16_t file_cnt;
  63. u16_t flag;
  64. u16_t dir_offset;
  65. u16_t data_offset;
  66. u32_t data_size;
  67. u32_t data_checksum;
  68. u8_t reserved[8];
  69. } __attribute__((packed));
  70. struct fw_version {
  71. u32_t magic;
  72. u32_t version_code;
  73. u32_t version_res;
  74. u32_t system_version_code;
  75. char version_name[64];
  76. char board_name[32];
  77. u8_t reserved1[12];
  78. u32_t checksum;
  79. };
  80. struct ota_fw_head {
  81. struct ota_fw_hdr hdr;
  82. u8_t reserved1[32];
  83. /* offset: 0x40 */
  84. struct ota_fw_ver new_ver;
  85. /* offset: 0xa0 */
  86. struct ota_fw_ver old_ver;
  87. u8_t reserved2[32];
  88. /* offset: 0x200 */
  89. struct ota_fw_dir dir;
  90. };
  91. #define LZMA_MAGIC 0x414d5a4c
  92. typedef struct lzma_head {
  93. uint32_t ih_magic;
  94. uint32_t ih_hdr_size; /* Size of image header (bytes). */
  95. uint32_t ih_img_size; /* Size of image body (bytes). */
  96. uint32_t ih_org_size; /* Size of origin data (bytes) */
  97. }lzma_head_t;
  98. '''
  99. class fw_version(object):
  100. def __init__(self, xml_file, tag_name):
  101. self.valid = False
  102. print('FWVER: Parse xml file: %s tag %s' %(xml_file, tag_name))
  103. tree = ET.ElementTree(file=xml_file)
  104. root = tree.getroot()
  105. if (root.tag != 'ota_firmware'):
  106. panic('OTA: invalid OTA xml file')
  107. ver = root.find(tag_name)
  108. if ver == None:
  109. return
  110. self.version_name = ver.find('version_name').text.strip()
  111. self.version_code = int(ver.find('version_code').text.strip(), 0)
  112. self.version_res = int(ver.find('version_res').text.strip(), 0)
  113. self.board_name = ver.find('board_name').text.strip()
  114. self.valid = True
  115. def get_data(self):
  116. return struct.pack('<32s24s4xIxI', bytearray(self.version_name, 'utf8'), \
  117. bytearray(self.board_name, 'utf8'), self.version_code, self.version_res)
  118. def dump_info(self):
  119. print('\t' + 'version name: ' + self.version_code)
  120. print('\t' + 'version name: ' + self.version_res)
  121. print('\t' + 'version code: ' + self.version_name)
  122. print('\t' + ' board name: ' + self.board_name)
  123. class OTAFile(object):
  124. def __init__(self, fpath):
  125. if (not os.path.isfile(fpath)):
  126. panic('invalid file: ' + fpath)
  127. self.file_path = fpath
  128. self.file_name = bytearray(os.path.basename(fpath), 'utf8')
  129. if len(self.file_name) > 12:
  130. print('OTA: file %s name is too long' %(self.file_name))
  131. return
  132. self.length = os.path.getsize(fpath)
  133. self.checksum = int(crc32_file(fpath))
  134. self.data = bytearray(0)
  135. with open(fpath, 'rb') as f:
  136. self.data = f.read()
  137. def dump_info(self):
  138. print(' name: ' + self.file_name.decode('utf8').rstrip('\0'))
  139. print(' length: ' + str(self.length))
  140. print(' checksum: ' + str(self.checksum))
  141. FIRMWARE_VERSION_MAGIC = 0x52455646 #FVER
  142. OTA_FIRMWARE_MAGIC = b'AOTA'
  143. OTA_FIRMWARE_DIR_OFFSET = 0x200
  144. OTA_FIRMWARE_DIR_ENTRY_SIZE = 0x20
  145. OTA_FIRMWARE_HEADER_SIZE = 0x400
  146. OTA_FIRMWARE_DATA_OFFSET = 0x400
  147. OTA_FIRMWARE_VERSION = 0x0100
  148. LZMA_HDR_SIZE = 0x10
  149. LZMA_MAGIC = 0x414d5a4c #LZMA
  150. class ota_fw(object):
  151. def __init__(self):
  152. self.old_version = None
  153. self.new_version = None
  154. self.disk_size = 0
  155. self.fw_version = {}
  156. self.partitions = []
  157. def _get_partition_info(self, keyword, keystr):
  158. if self.partitions:
  159. for info_dict in self.partitions:
  160. if keyword in info_dict and keystr == info_dict[keyword]:
  161. return info_dict
  162. return {}
  163. def parse_config(self, cfg_file, board_name, output_file):
  164. #print('OTA: Parse config file: %s' %cfg_file)
  165. tree = ET.ElementTree(file=cfg_file)
  166. root = tree.getroot()
  167. if (root.tag != 'firmware'):
  168. sys.stderr.write('error: invalid firmware config file')
  169. sys.exit(1)
  170. disk_size_prop = root.find('disk_size')
  171. if disk_size_prop is not None:
  172. self.disk_size = int(disk_size_prop.text.strip(), 0)
  173. firmware_version = root.find('firmware_version')
  174. for prop in firmware_version:
  175. self.fw_version[prop.tag] = prop.text.strip()
  176. if 'version_name' in self.fw_version.keys():
  177. file_name = os.path.basename(output_file)
  178. cur_time = time.strftime('%y%m%d%H%M',time.localtime(time.time()))
  179. version_name = self.fw_version['version_name'].replace('$(build_time)', cur_time)
  180. self.fw_version['version_name'] = version_name
  181. self.fw_version['board_name'] = board_name
  182. part_list = root.find('partitions').findall('partition')
  183. for part in part_list:
  184. part_prop = {}
  185. for prop in part:
  186. part_prop[prop.tag] = prop.text.strip()
  187. self.partitions.append(part_prop)
  188. def dump_info(self, ota_files):
  189. i = 0
  190. for file in ota_files:
  191. print('[%d]' %(i))
  192. i = i + 1
  193. file.dump_info()
  194. def generate_ota_xml(self, ota_file_list, ota_xml):
  195. root = ET.Element('ota_firmware')
  196. root.text = '\n\t'
  197. root.tail = '\n'
  198. fw_ver = ET.SubElement(root, 'firmware_version')
  199. fw_ver.text = '\n\t\t'
  200. fw_ver.tail = '\n'
  201. for v in self.fw_version:
  202. type = ET.SubElement(fw_ver, v)
  203. type.text = self.fw_version[v]
  204. type.tail = '\n\t\t'
  205. parts = ET.SubElement(root, 'partitions')
  206. parts.text = '\n\t'
  207. parts.tail = '\n\t'
  208. # write part_num firstly for fast search
  209. part_num = ET.SubElement(parts, 'partitionsNum')
  210. part_num.text=str(len(ota_file_list))
  211. part_num.tail = '\n\t'
  212. for partfile in ota_file_list:
  213. orig_partfile = partfile + ".orig"
  214. if not os.path.exists(orig_partfile):
  215. orig_partfile = partfile
  216. part = self._get_partition_info('file_name', os.path.basename(partfile))
  217. if not part:
  218. continue
  219. part_node = ET.SubElement(parts, 'partition')
  220. part_node.text = '\n\t\t'
  221. part_node.tail = '\n\t'
  222. type = ET.SubElement(part_node, 'type')
  223. type.text=part['type']
  224. type.tail = '\n\t\t'
  225. type = ET.SubElement(part_node, 'name')
  226. type.text=part['name']
  227. type.tail = '\n\t\t'
  228. type = ET.SubElement(part_node, 'file_id')
  229. type.text=part['file_id']
  230. type.tail = '\n\t\t'
  231. type = ET.SubElement(part_node, 'storage_id')
  232. type.text=part['storage_id']
  233. type.tail = '\n\t\t'
  234. url = ET.SubElement(part_node, 'file_name')
  235. url.text = part['file_name']
  236. url.tail = '\n\t\t'
  237. type = ET.SubElement(part_node, 'file_size')
  238. type.text=str(hex(os.path.getsize(partfile)))
  239. type.tail = '\n\t\t'
  240. type = ET.SubElement(part_node, 'orig_size')
  241. type.text=str(hex(os.path.getsize(orig_partfile)))
  242. type.tail = '\n\t\t'
  243. crc = ET.SubElement(part_node, 'checksum')
  244. crc.text = str(hex(crc32_file(orig_partfile)))
  245. crc.tail = '\n\t'
  246. tree = ET.ElementTree(root)
  247. tree.write(ota_xml, xml_declaration=True, method="xml", encoding='UTF-8')
  248. def ota_get_file_seq(self, file_name):
  249. seq = 0
  250. file_name = os.path.basename(file_name)
  251. #for part in self.partitions:
  252. for i, part in enumerate(self.partitions):
  253. if ('file_name' in part.keys()) and ('true' == part['enable_ota']):
  254. if file_name == part['file_name']:
  255. seq = i + 1;
  256. # boot is the last file in ota firmware
  257. if 'BOOT' == part['type']:
  258. seq = 0x10000
  259. if 'SYS_PARAM' == part['type']:
  260. seq = 0x10001
  261. return seq
  262. def pack(self, ota_file_list, output_file):
  263. if len(ota_file_list) > 14:
  264. panic('OTA: too much input files')
  265. if len(ota_file_list) == 1 and os.path.isdir(ota_file_list[0]):
  266. files = [name for name in os.listdir(ota_file_list[0]) \
  267. if os.path.isfile(os.path.join(ota_file_list[0], name))]
  268. else:
  269. files = ota_file_list
  270. ota_files = []
  271. for file in files:
  272. ota_files.append(OTAFile(file))
  273. self.dump_info(ota_files)
  274. file_data = bytearray(0)
  275. head_data = bytearray(0)
  276. dir_data = bytearray(0)
  277. data_offset = OTA_FIRMWARE_DATA_OFFSET
  278. dir_entry = bytearray(0)
  279. for file in ota_files:
  280. dir_entry = struct.pack("<12s4xII4xI", file.file_name, data_offset, file.length, file.checksum)
  281. pad_len = calc_pad_length(file.length, 512)
  282. data_offset = data_offset + file.length + pad_len
  283. file_data = file_data + file.data + bytearray(pad_len)
  284. dir_data = dir_data + dir_entry
  285. data_crc = zlib.crc32(file_data, 0) & 0xffffffff
  286. pad_len = calc_pad_length(len(dir_data), 512)
  287. dir_data = dir_data + bytearray(pad_len)
  288. head_data = struct.pack("<4sIHHHHHHII36x", OTA_FIRMWARE_MAGIC, 0, \
  289. OTA_FIRMWARE_VERSION, OTA_FIRMWARE_HEADER_SIZE, \
  290. len(ota_files), 0, \
  291. OTA_FIRMWARE_DIR_OFFSET, OTA_FIRMWARE_DATA_OFFSET, \
  292. data_offset, data_crc)
  293. # add fw version
  294. xml_fpath = ''
  295. for file in ota_files:
  296. if file.file_name == b'ota.xml':
  297. xml_fpath = file.file_path
  298. if xml_fpath == '':
  299. panic('cannot found ota.xml file')
  300. fw_ver = fw_version(xml_fpath, 'firmware_version')
  301. if not fw_ver.valid:
  302. panic('cannot found ota.xml file')
  303. head_data = head_data + fw_ver.get_data()
  304. old_fw_ver = fw_version(xml_fpath, 'old_firmware_version')
  305. if old_fw_ver.valid:
  306. head_data = head_data + old_fw_ver.get_data()
  307. head_data = head_data + bytearray(calc_pad_length(len(head_data), 512))
  308. header_crc = zlib.crc32(head_data[8:] + dir_data, 0) & 0xffffffff
  309. head_data = head_data[0:4] + struct.pack('<I', header_crc) + \
  310. head_data[8:]
  311. with open(output_file, 'wb') as f:
  312. f.write(head_data)
  313. f.write(dir_data)
  314. f.write(file_data)
  315. def __build_ota_image(self, ota_file_list, image_name, lzma=0):
  316. if not ota_file_list:
  317. panic('NO OTA files input')
  318. if lzma > 0:
  319. for partfile in ota_file_list:
  320. if os.path.basename(partfile) != 'TEMP.bin':
  321. self.__build_lzma_image(partfile, 0x8000, 1) #32K
  322. ota_file_list.sort(key=self.ota_get_file_seq)
  323. ota_dir = os.path.dirname(ota_file_list[0])
  324. ota_xml = os.path.join(ota_dir, 'ota.xml')
  325. self.generate_ota_xml(ota_file_list, ota_xml)
  326. ota_file_list.insert(0, ota_xml)
  327. self.pack(ota_file_list, image_name)
  328. if not os.path.exists(image_name):
  329. panic('Failed to generate OTA image %s' %image_name)
  330. def __build_lzma_image(self, image_name, blk_size, backup=0):
  331. if not os.path.exists(image_name):
  332. return
  333. orig_image = image_name + ".orig"
  334. if os.path.exists(orig_image):
  335. os.remove(orig_image)
  336. os.rename(image_name,orig_image)
  337. with open(image_name, 'wb') as f1, open(orig_image, 'rb') as f2:
  338. while True:
  339. raw_data = f2.read(blk_size)
  340. if len(raw_data) == 0:
  341. break
  342. xz_data = lzma.compress(raw_data)
  343. hdr_data = struct.pack("<IIII", LZMA_MAGIC, LZMA_HDR_SIZE, len(xz_data), len(raw_data))
  344. f1.write(hdr_data)
  345. f1.write(xz_data)
  346. if backup == 0:
  347. os.remove(orig_image)
  348. def __get_psram_size(self, zephyr_cfg):
  349. bret = 0
  350. with open(zephyr_cfg, "r") as f:
  351. lines = f.readlines()
  352. for elem in lines:
  353. _c = elem.strip().split("=")
  354. if _c[0] == "CONFIG_PSRAM_SIZE":
  355. bret = int(_c[1])
  356. break;
  357. return bret
  358. def __build_temp_image(self, zephyr_cfg, temp_bin, ota_app_bin, lzma=0):
  359. if not os.path.exists(temp_bin):
  360. return
  361. if lzma > 0:
  362. psram_size = self.__get_psram_size(zephyr_cfg)
  363. if psram_size >= 4096:
  364. blk_size = 0x200000 #2048K
  365. elif psram_size >= 1024:
  366. blk_size = 0x40000 #256K
  367. else:
  368. blk_size = 0x8000 #32K
  369. self.__build_lzma_image(temp_bin, blk_size, 0)
  370. if os.path.exists(ota_app_bin):
  371. with open(ota_app_bin, 'rb') as f1, open(temp_bin, 'rb') as f2:
  372. app_data = f1.read()
  373. temp_data = f2.read()
  374. with open(temp_bin, 'wb') as f1:
  375. f1.write(app_data)
  376. f1.write(temp_data)
  377. def generate_ota_image_internal(self, ota_file, ota_dir = '', ota_xml = ''):
  378. if os.path.exists(ota_file):
  379. os.remove(ota_file)
  380. files = []
  381. # embed ota file info: embed type: embed file list
  382. embed_ota_image_dict = {}
  383. for part in self.partitions:
  384. if 'file_name' in part and ('true' == part['enable_ota']):
  385. file_name = os.path.join(ota_dir, part['file_name'])
  386. if 'ota_embed' in part:
  387. ota_embed_type = part['ota_embed']
  388. if ota_embed_type in embed_ota_image_dict:
  389. embed_ota_image_dict[ota_embed_type].append(file_name)
  390. else:
  391. embed_ota_image_dict[ota_embed_type] = [file_name]
  392. else:
  393. files.append(file_name)
  394. for ota_embed_type in embed_ota_image_dict:
  395. embed_file_list = embed_ota_image_dict[ota_embed_type]
  396. embed_image = os.path.join(ota_dir, "%s.bin" %ota_embed_type)
  397. self.__build_ota_image(embed_file_list, embed_image, 0)
  398. files.append(embed_image)
  399. part = self._get_partition_info('type', ota_embed_type)
  400. part['file_name'] = os.path.basename(embed_image)
  401. temp_bin = os.path.join(ota_dir, 'TEMP.bin')
  402. ota_app_bin = os.path.join(ota_dir, 'ota_app.bin')
  403. out_dir = os.path.dirname(os.path.dirname(ota_dir))
  404. zephyr_cfg = os.path.join(out_dir, 'zephyr', ".config")
  405. self.__build_temp_image(zephyr_cfg,temp_bin,ota_app_bin, LZMA_TEMP_BIN)
  406. self.__build_ota_image(files, ota_file, LZMA_OTA_BIN)
  407. if not os.path.exists(ota_file):
  408. panic('Failed to generate OTA image')
  409. def copy_ota_files(self, bin_dir, ota_dir):
  410. ota_app_bin = os.path.join(bin_dir, 'ota_app.bin')
  411. if os.path.exists(ota_app_bin):
  412. shutil.copyfile(ota_app_bin, os.path.join(ota_dir, 'ota_app.bin'))
  413. for part in self.partitions:
  414. if ('file_name' in part.keys()) and ('true' == part['enable_ota']):
  415. shutil.copyfile(os.path.join(bin_dir, part['file_name']), \
  416. os.path.join(ota_dir, part['file_name']))
  417. def generate_ota_image(self, input_file_dir, ota_filename):
  418. print('FW: Build OTA image \'' + ota_filename + '\'')
  419. ota_file_dir = os.path.join(os.path.dirname(ota_filename),
  420. os.path.splitext(os.path.basename(ota_filename))[0])
  421. if os.path.isdir(ota_file_dir):
  422. shutil.rmtree(ota_file_dir)
  423. os.mkdir(ota_file_dir)
  424. # generate OTA firmware with crc/randomizer
  425. self.copy_ota_files(input_file_dir, ota_file_dir)
  426. self.generate_ota_image_internal(ota_filename, ota_file_dir)
  427. def unpack_fw(fpath, out_dir):
  428. f = open(fpath, 'rb')
  429. if f == None:
  430. panic('cannot open file')
  431. ota_data = f.read()
  432. magic, checksum, header_version, header_size, file_cnt, header_flag, \
  433. dir_offs, data_offs, data_len, data_crc \
  434. = struct.unpack("<4sIHHHHHHII36x", ota_data[0:64])
  435. if magic != OTA_FIRMWARE_MAGIC:
  436. panic('invalid magic')
  437. if not os.path.exists(out_dir):
  438. os.makedirs(out_dir)
  439. for i in range(file_cnt):
  440. entry_offs = dir_offs + i * OTA_FIRMWARE_DIR_ENTRY_SIZE
  441. name, offset, file_length, checksum = \
  442. struct.unpack("<12s4xII4xI", ota_data[entry_offs : entry_offs + OTA_FIRMWARE_DIR_ENTRY_SIZE])
  443. if name[0] != 0:
  444. with open(os.path.join(out_dir, name.decode('utf8').rstrip('\0')), 'wb') as wf:
  445. wf.write(ota_data[offset : offset + file_length])
  446. def main(argv):
  447. parser = argparse.ArgumentParser(
  448. description='Build OTA firmware image',
  449. )
  450. parser.add_argument('-b', dest = 'board_name')
  451. parser.add_argument('-c', dest = 'fw_cfgfile')
  452. parser.add_argument('-i', dest = 'input_file_dir')
  453. parser.add_argument('-o', dest = 'output_file')
  454. parser.add_argument('-x', dest = 'extract_out_dir')
  455. args = parser.parse_args();
  456. print('ATF: Build OTA firmware image: %s' %args.output_file)
  457. if args.extract_out_dir:
  458. image_file = args.input_file_dir
  459. unpack_fw(image_file, args.extract_out_dir)
  460. else:
  461. if (not args.output_file) :
  462. panic('no file')
  463. fw = ota_fw()
  464. fw.parse_config(args.fw_cfgfile, args.board_name, args.output_file)
  465. fw.generate_ota_image(args.input_file_dir, args.output_file)
  466. return 0
  467. if __name__ == '__main__':
  468. main(sys.argv[1:])