Files
Momentum-Firmware/scripts/update.py
2023-03-25 22:30:50 +00:00

284 lines
10 KiB
Python
Executable File

#!/usr/bin/env python3
from flipper.app import App
from flipper.utils.fff import FlipperFormatFile
from flipper.assets.coprobin import CoproBinary, get_stack_type
from flipper.assets.obdata import OptionBytesData, ObReferenceValues
from os.path import basename, join, exists
import os
import shutil
import zlib
import tarfile
import math
import pathlib
from slideshow import Main as SlideshowMain
class Main(App):
UPDATE_MANIFEST_VERSION = 2
UPDATE_MANIFEST_NAME = "update.fuf"
# No compression, plain tar
RESOURCE_TAR_MODE = "w:"
RESOURCE_TAR_FORMAT = tarfile.USTAR_FORMAT
RESOURCE_FILE_NAME = "resources.tar"
RESOURCE_ENTRY_NAME_MAX_LENGTH = 100
WHITELISTED_STACK_TYPES = set(
map(
get_stack_type,
["BLE_FULL", "BLE_LIGHT", "BLE_BASIC"],
)
)
FLASH_BASE = 0x8000000
MIN_LFS_PAGES = 6
# Post-update slideshow
SPLASH_BIN_NAME = "splash.bin"
def init(self):
self.subparsers = self.parser.add_subparsers(help="sub-command help")
# generate
self.parser_generate = self.subparsers.add_parser(
"generate", help="Generate update description file"
)
self.parser_generate.add_argument("-d", dest="directory", required=True)
self.parser_generate.add_argument("-v", dest="version", required=True)
self.parser_generate.add_argument("-t", dest="target", required=True)
self.parser_generate.add_argument(
"--dfu", dest="dfu", default="", required=False
)
self.parser_generate.add_argument("-r", dest="resources", required=False)
self.parser_generate.add_argument("--stage", dest="stage", required=True)
self.parser_generate.add_argument(
"--radio", dest="radiobin", default="", required=False
)
self.parser_generate.add_argument(
"--radioaddr",
dest="radioaddr",
type=lambda x: int(x, 16),
default=0,
required=False,
)
self.parser_generate.add_argument(
"--radiotype", dest="radiotype", required=False
)
self.parser_generate.add_argument("--obdata", dest="obdata", required=False)
self.parser_generate.add_argument("--splash", dest="splash", required=False)
self.parser_generate.add_argument(
"--I-understand-what-I-am-doing", dest="disclaimer", required=False
)
self.parser_generate.set_defaults(func=self.generate)
def generate(self):
stage_basename = "updater.bin" # used to be basename(self.args.stage)
dfu_basename = (
"firmware.dfu" if self.args.dfu else ""
) # used to be basename(self.args.dfu)
radiobin_basename = (
"radio.bin" if self.args.radiobin else ""
) # used to be basename(self.args.radiobin)
resources_basename = ""
radio_version = 0
radio_meta = None
radio_addr = self.args.radioaddr
if self.args.radiobin:
if not self.args.radiotype:
raise ValueError("Missing --radiotype")
radio_meta = CoproBinary(self.args.radiobin)
radio_version = self.copro_version_as_int(radio_meta, self.args.radiotype)
if (
get_stack_type(self.args.radiotype) not in self.WHITELISTED_STACK_TYPES
and self.args.disclaimer != "yes"
):
self.logger.error(
f"You are trying to bundle a non-standard stack type '{self.args.radiotype}'."
)
self.disclaimer()
return 1
if radio_addr == 0:
radio_addr = radio_meta.get_flash_load_addr()
self.logger.info(
f"Using guessed radio address 0x{radio_addr:08X}, verify with Release_Notes"
" or specify --radioaddr"
)
if not exists(self.args.directory):
os.makedirs(self.args.directory)
shutil.copyfile(self.args.stage, join(self.args.directory, stage_basename))
dfu_size = 0
if self.args.dfu:
dfu_size = os.stat(self.args.dfu).st_size
shutil.copyfile(self.args.dfu, join(self.args.directory, dfu_basename))
if radiobin_basename:
shutil.copyfile(
self.args.radiobin, join(self.args.directory, radiobin_basename)
)
if self.args.resources:
resources_basename = self.RESOURCE_FILE_NAME
if not self.package_resources(
self.args.resources, join(self.args.directory, resources_basename)
):
return 3
if not self.layout_check(dfu_size, radio_addr):
self.logger.warn("Memory layout looks suspicious")
if not self.args.disclaimer == "yes":
self.disclaimer()
return 2
if self.args.splash:
SlideshowMain(no_exit=True)([
"-i",
str(pathlib.Path(self.args.splash).parent / "xfwfirstboot"),
"-o",
join(self.args.directory, "xfwfirstboot.bin"),
])
splash_args = [
"-i",
self.args.splash,
"-o",
join(self.args.directory, self.SPLASH_BIN_NAME),
]
if splash_code := SlideshowMain(no_exit=True)(splash_args):
self.logger.error(
f"Failed to convert splash screen data: {splash_code}"
)
return splash_code
file = FlipperFormatFile()
file.setHeader(
"Flipper firmware upgrade configuration", self.UPDATE_MANIFEST_VERSION
)
file.writeKey("Info", self.args.version)
file.writeKey("Target", self.args.target[1:]) # dirty 'f' strip
file.writeKey("Loader", stage_basename)
file.writeComment("little-endian hex!")
file.writeKey("Loader CRC", self.int2ffhex(self.crc(self.args.stage)))
file.writeKey("Firmware", dfu_basename)
file.writeKey("Radio", radiobin_basename or "")
file.writeKey("Radio address", self.int2ffhex(radio_addr))
file.writeKey("Radio version", self.int2ffhex(radio_version, 12))
if radiobin_basename:
file.writeKey("Radio CRC", self.int2ffhex(self.crc(self.args.radiobin)))
else:
file.writeKey("Radio CRC", self.int2ffhex(0))
file.writeKey("Resources", resources_basename)
obvalues = ObReferenceValues((), (), ())
if self.args.obdata:
obd = OptionBytesData(self.args.obdata)
obvalues = obd.gen_values().export()
file.writeComment(
"NEVER EVER MESS WITH THESE VALUES, YOU WILL BRICK YOUR DEVICE"
)
file.writeKey("OB reference", self.bytes2ffhex(obvalues.reference))
file.writeKey("OB mask", self.bytes2ffhex(obvalues.compare_mask))
file.writeKey("OB write mask", self.bytes2ffhex(obvalues.write_mask))
file.writeKey("Splashscreen", self.SPLASH_BIN_NAME if self.args.splash else "")
file.save(join(self.args.directory, self.UPDATE_MANIFEST_NAME))
return 0
def layout_check(self, fw_size, radio_addr):
if fw_size == 0 or radio_addr == 0:
self.logger.info("Cannot validate layout for partial package")
return True
lfs_span = radio_addr - self.FLASH_BASE - fw_size
self.logger.debug(f"Expected LFS size: {lfs_span}")
lfs_span_pages = lfs_span / (4 * 1024)
if lfs_span_pages < self.MIN_LFS_PAGES:
self.logger.warn(
f"Expected LFS size is too small (~{int(lfs_span_pages)} pages)"
)
return False
return True
def disclaimer(self):
self.logger.error(
"You might brick your device into a state in which you'd need an SWD programmer to fix it."
)
self.logger.error(
"Please confirm that you REALLY want to do that with --I-understand-what-I-am-doing=yes"
)
def _tar_filter(self, tarinfo: tarfile.TarInfo):
if len(tarinfo.name) > self.RESOURCE_ENTRY_NAME_MAX_LENGTH:
self.logger.error(
f"Cannot package resource: name '{tarinfo.name}' too long"
)
raise ValueError("Resource name too long")
return tarinfo
def package_resources(self, srcdir: str, dst_name: str):
try:
with tarfile.open(
dst_name, self.RESOURCE_TAR_MODE, format=self.RESOURCE_TAR_FORMAT
) as tarball:
tarball.add(
srcdir,
arcname="",
filter=self._tar_filter,
)
return True
except ValueError as e:
self.logger.error(f"Cannot package resources: {e}")
return False
@staticmethod
def copro_version_as_int(coprometa, stacktype):
major = coprometa.img_sig.version_major
minor = coprometa.img_sig.version_minor
sub = coprometa.img_sig.version_sub
branch = coprometa.img_sig.version_branch
release = coprometa.img_sig.version_build
stype = get_stack_type(stacktype)
return (
major
| (minor << 8)
| (sub << 16)
| (branch << 24)
| (release << 32)
| (stype << 40)
)
@staticmethod
def bytes2ffhex(value: bytes):
return " ".join(f"{b:02X}" for b in value)
@staticmethod
def int2ffhex(value: int, n_hex_syms=8):
if value:
n_hex_syms = max(math.ceil(math.ceil(math.log2(value)) / 8) * 2, n_hex_syms)
fmtstr = f"%0{n_hex_syms}X"
hexstr = fmtstr % value
return " ".join(list(Main.batch(hexstr, 2))[::-1])
@staticmethod
def crc(fileName):
prev = 0
with open(fileName, "rb") as file:
for eachLine in file:
prev = zlib.crc32(eachLine, prev)
return prev & 0xFFFFFFFF
@staticmethod
def batch(iterable, n=1):
l = len(iterable)
for ndx in range(0, l, n):
yield iterable[ndx : min(ndx + n, l)]
if __name__ == "__main__":
Main()()