[FL-1970, FL-1965, FL-1872, FL-1689] Python framework, Scripts and fixes (#779)

* Scripts: add flipper lib, migrate ob to flipper lib, update ob.data
* Makefile: speedup build with phony target for .d files
* FuriHal,U8G2: full MGG display support and ERC contrast tuning.
* Desktop: fix dolphin rename artifact.
* Scripts: port otp.py to flipper scripting lib.
* Scripts: add wipe and core1 flashing to flash.py, remove obsolete shell scripts
* Scripts: replace core1 flashing script with global makefile.
* Scripts: final touches and migration to python. Root Makefile for everything.
This commit is contained in:
あく
2021-10-21 15:24:34 +03:00
committed by GitHub
parent 4997b56498
commit 2751440193
29 changed files with 1246 additions and 295 deletions

View File

@@ -1,6 +1,6 @@
# About
This folder contains differnt scripts that automates routine actions.
This folder contains supplementary scripts that automates routine actions.
Flashing scripts are based on cli version of [STM32CubeProgrammer](https://www.st.com/en/development-tools/stm32cubeprog.html).
You will need to add STM32_Programmer_CLI to your path to use them.
@@ -9,29 +9,26 @@ You will need to add STM32_Programmer_CLI to your path to use them.
Always flash your device in the folllowing sequence:
- OTP (Only on empty MCU)
- Core2 firmware
- Core1 firmware
- Core1 and Core2 firmware flashing
- Option Bytes
## Otp flashing
!!! Flashing incorrect OTP may permanently brick your device !!!
Normally OTP data generated and flashed at factory.
Normally OTP data generated and flashed at the factory.
In case if MCU was replaced you'll need correct OTP data to be able to use companion applications.
Use `otp.py` to generate OTP data and `flash_otp_version_*` to flash OTP zone.
Use `otp.py` to generate and flash OTP data.
You will need exact main board revision to genrate OTP data. It can be found on main PCB.
Also display type, region and etc...
!!! Flashing incorrect OTP may permanently brick your device !!!
## Core2 flashing
## Core1 and Core2 firmware flashing
Script blindly updates FUS and Radiostack. This operation is going to corrupt bootloader and firmware.
Reflash Core1 after Core2.
## Core1 flashing
Script compiles and flashes both bootloader and firmware.
Main flashing sequence can be found in root `Makefile`.
Core2 goes first, then Core1.
Never flash FUS or you will loose your job, girlfriend and keys in secure enclave.
## Option Bytes

152
scripts/flash.py Executable file
View File

@@ -0,0 +1,152 @@
#!/usr/bin/env python3
import logging
import argparse
import sys
import os
from flipper.app import App
from flipper.cube import CubeProgrammer
STATEMENT = "AGREE_TO_LOOSE_FLIPPER_FEATURES_THAT_USES_CRYPTO_ENCLAVE"
class Main(App):
def init(self):
self.subparsers = self.parser.add_subparsers(help="sub-command help")
# Wipe
self.parser_wipe = self.subparsers.add_parser("wipe", help="Wipe MCU Flash")
self.parser_wipe.set_defaults(func=self.wipe)
# Core 1 boot
self.parser_core1boot = self.subparsers.add_parser(
"core1boot", help="Flash Core1 Bootloader"
)
self._addArgsSWD(self.parser_core1boot)
self.parser_core1boot.add_argument(
"bootloader", type=str, help="Bootloader binary"
)
self.parser_core1boot.set_defaults(func=self.core1boot)
# Core 1 firmware
self.parser_core1firmware = self.subparsers.add_parser(
"core1firmware", help="Flash Core1 Firmware"
)
self._addArgsSWD(self.parser_core1firmware)
self.parser_core1firmware.add_argument(
"firmware", type=str, help="Firmware binary"
)
self.parser_core1firmware.set_defaults(func=self.core1firmware)
# Core 1 all
self.parser_core1 = self.subparsers.add_parser(
"core1", help="Flash Core1 Boot and Firmware"
)
self._addArgsSWD(self.parser_core1)
self.parser_core1.add_argument("bootloader", type=str, help="Bootloader binary")
self.parser_core1.add_argument("firmware", type=str, help="Firmware binary")
self.parser_core1.set_defaults(func=self.core1)
# Core 2 fus
self.parser_core2fus = self.subparsers.add_parser(
"core2fus", help="Flash Core2 Firmware Update Service"
)
self._addArgsSWD(self.parser_core2fus)
self.parser_core2fus.add_argument(
"--statement",
type=str,
help="NEVER FLASH FUS, IT WILL ERASE CRYPTO ENCLAVE",
required=True,
)
self.parser_core2fus.add_argument(
"fus_address", type=str, help="Firmware Update Service Address"
)
self.parser_core2fus.add_argument(
"fus", type=str, help="Firmware Update Service Binary"
)
self.parser_core2fus.set_defaults(func=self.core2fus)
# Core 2 radio stack
self.parser_core2radio = self.subparsers.add_parser(
"core2radio", help="Flash Core2 Radio stack"
)
self._addArgsSWD(self.parser_core2radio)
self.parser_core2radio.add_argument(
"radio_address", type=str, help="Radio Stack Binary Address"
)
self.parser_core2radio.add_argument(
"radio", type=str, help="Radio Stack Binary"
)
self.parser_core2radio.set_defaults(func=self.core2radio)
def _addArgsSWD(self, parser):
parser.add_argument(
"--port", type=str, help="Port to connect: swd or usb1", default="swd"
)
def wipe(self):
self.logger.info(f"Wiping flash")
cp = CubeProgrammer("swd")
self.logger.info(f"Setting RDP to 0xBB")
cp.setOptionBytes({"RDP": ("0xBB", "rw")})
self.logger.info(f"Verifying RDP")
r = cp.checkOptionBytes({"RDP": ("0xBB", "rw")})
assert r == True
self.logger.info(f"Result: {r}")
self.logger.info(f"Setting RDP to 0xAA")
cp.setOptionBytes({"RDP": ("0xAA", "rw")})
self.logger.info(f"Verifying RDP")
r = cp.checkOptionBytes({"RDP": ("0xAA", "rw")})
assert r == True
self.logger.info(f"Result: {r}")
self.logger.info(f"Complete")
return 0
def core1boot(self):
self.logger.info(f"Flashing bootloader")
cp = CubeProgrammer(self.args.port)
cp.flashBin("0x08000000", self.args.bootloader)
self.logger.info(f"Complete")
cp.resetTarget()
return 0
def core1firmware(self):
self.logger.info(f"Flashing firmware")
cp = CubeProgrammer(self.args.port)
cp.flashBin("0x08008000", self.args.firmware)
self.logger.info(f"Complete")
cp.resetTarget()
return 0
def core1(self):
self.logger.info(f"Flashing bootloader")
cp = CubeProgrammer(self.args.port)
cp.flashBin("0x08000000", self.args.bootloader)
self.logger.info(f"Flashing firmware")
cp.flashBin("0x08008000", self.args.firmware)
cp.resetTarget()
self.logger.info(f"Complete")
return 0
def core2fus(self):
if self.args.statement != STATEMENT:
self.logger.error(
f"PLEASE DON'T. THIS FEATURE INTENDED ONLY FOR FACTORY FLASHING"
)
return 1
self.logger.info(f"Flashing Firmware Update Service")
cp = CubeProgrammer(self.args.port)
cp.flashCore2(self.args.fus_address, self.args.fus)
self.logger.info(f"Complete")
return 0
def core2radio(self):
if int(self.args.radio_address, 16) > 0x080E0000:
self.logger.error(f"I KNOW WHAT YOU DID LAST SUMMER")
return 1
cp = CubeProgrammer(self.args.port)
self.logger.info(f"Removing Current Radio Stack")
cp.deleteCore2RadioStack()
self.logger.info(f"Flashing Radio Stack")
cp.flashCore2(self.args.radio_address, self.args.radio)
self.logger.info(f"Complete")
return 0
if __name__ == "__main__":
Main()()

View File

@@ -1,12 +0,0 @@
#!/bin/bash
set -x -e
SCRIPT_DIR="$( cd -- "$(dirname "$0")" >/dev/null 2>&1 ; pwd -P )"
PROJECT_DIR="$(dirname "$SCRIPT_DIR")"
rm "$PROJECT_DIR"/bootloader/.obj/f*/flash || true
make -C "$PROJECT_DIR"/bootloader -j9 flash
rm "$PROJECT_DIR"/firmware/.obj/f*/flash || true
make -C "$PROJECT_DIR"/firmware -j9 flash

View File

@@ -1,20 +0,0 @@
#!/bin/bash
set -x -e
SCRIPT_DIR="$( cd -- "$(dirname "$0")" >/dev/null 2>&1 ; pwd -P )"
PROJECT_DIR="$(dirname "$SCRIPT_DIR")"
COPRO_DIR="$PROJECT_DIR/lib/STM32CubeWB/Projects/STM32WB_Copro_Wireless_Binaries/STM32WB5x"
STM32_Programmer_CLI -c port=swd -fwupgrade "$COPRO_DIR"/stm32wb5x_FUS_fw_for_fus_0_5_3.bin 0x080EC000 || true
STM32_Programmer_CLI -c port=swd
STM32_Programmer_CLI -c port=swd -fwupgrade "$COPRO_DIR"/stm32wb5x_FUS_fw.bin 0x080EC000 || true
STM32_Programmer_CLI -c port=swd
STM32_Programmer_CLI -c port=swd -fwdelete
STM32_Programmer_CLI -c port=swd -fwupgrade "$COPRO_DIR"/stm32wb5x_BLE_Stack_full_fw.bin 0x080CA000 firstinstall=0
STM32_Programmer_CLI -c port=swd -ob nSWBOOT0=1 nBOOT0=1

View File

@@ -1,17 +0,0 @@
#!/bin/bash
set -x -e
if [ "$#" -ne 1 ]; then
echo "OTP file required"
exit
fi
if [ ! -f "$1" ]; then
echo "Unable to open OTP file"
exit
fi
STM32_Programmer_CLI -c port=usb1 -d "$1" 0x1FFF7000
STM32_Programmer_CLI -c port=usb1 -r8 0x1FFF7000 8

View File

@@ -1,17 +0,0 @@
#!/bin/bash
set -x -e
if [ "$#" -ne 1 ]; then
echo "OTP file required"
exit
fi
if [ ! -f "$1" ]; then
echo "Unable to open OTP file"
exit
fi
STM32_Programmer_CLI -c port=swd -d "$1" 0x1FFF7000
STM32_Programmer_CLI -c port=swd -r8 0x1FFF7000 8

View File

@@ -1,11 +0,0 @@
#!/bin/bash
set -x -e
STM32_Programmer_CLI -c port=swd -ob RDP=0xBB
STM32_Programmer_CLI -c port=swd -ob displ
STM32_Programmer_CLI -c port=swd --readunprotect
STM32_Programmer_CLI -c port=swd -ob displ

47
scripts/flipper/app.py Normal file
View File

@@ -0,0 +1,47 @@
import logging
import argparse
import sys
import os
class App:
def __init__(self):
# Argument Parser
self.parser = argparse.ArgumentParser()
self.parser.add_argument("-d", "--debug", action="store_true", help="Debug")
# Logging
self.logger = logging.getLogger()
# Application specific initialization
self.init()
def __call__(self):
self.args = self.parser.parse_args()
if "func" not in self.args:
self.parser.error("Choose something to do")
# configure log output
self.log_level = logging.DEBUG if self.args.debug else logging.INFO
self.logger.setLevel(self.log_level)
self.handler = logging.StreamHandler(sys.stdout)
self.handler.setLevel(self.log_level)
self.formatter = logging.Formatter("%(asctime)s [%(levelname)s] %(message)s")
self.handler.setFormatter(self.formatter)
self.logger.addHandler(self.handler)
# execute requested function
self.before()
return_code = self.args.func()
self.after()
if isinstance(return_code, int):
exit(return_code)
else:
self.logger.error(f"Missing return code")
exit(255)
def init(self):
raise Exception("init() is not implemented")
def before(self):
pass
def after(self):
pass

91
scripts/flipper/cube.py Normal file
View File

@@ -0,0 +1,91 @@
import logging
import subprocess
class CubeProgrammer:
"""STM32 Cube Programmer cli wrapper"""
def __init__(self, port, params=[]):
self.port = port
self.params = params
# logging
self.logger = logging.getLogger()
def _execute(self, args):
try:
output = subprocess.check_output(
[
"STM32_Programmer_CLI",
"-q",
f"-c port={self.port}",
*self.params,
*args,
]
)
except subprocess.CalledProcessError as e:
if e.output:
print("Process output:\n", e.output.decode())
print("Process return code:", e.returncode)
raise e
assert output
return output.decode()
def getVersion(self):
output = self._execute(["--version"])
def checkOptionBytes(self, option_bytes):
output = self._execute(["-ob displ"])
ob_correct = True
for line in output.split("\n"):
line = line.strip()
if not ":" in line:
self.logger.debug(f"Skipping line: {line}")
continue
key, data = line.split(":", 1)
key = key.strip()
data = data.strip()
if not key in option_bytes.keys():
self.logger.debug(f"Skipping key: {key}")
continue
self.logger.debug(f"Processing key: {key} {data}")
value, comment = data.split(" ", 1)
value = value.strip()
comment = comment.strip()
if option_bytes[key][0] != value:
self.logger.error(
f"Invalid OB: {key} {value}, expected: {option_bytes[key][0]}"
)
ob_correct = False
return ob_correct
def setOptionBytes(self, option_bytes):
options = []
for key, (value, attr) in option_bytes.items():
if "w" in attr:
options.append(f"{key}={value}")
self._execute(["-ob", *options])
return True
def flashBin(self, address, filename):
self._execute(
[
"-d",
filename,
f"{address}",
]
)
def flashCore2(self, address, filename):
self._execute(
[
"-fwupgrade",
filename,
f"{address}",
]
)
def deleteCore2RadioStack(self):
self._execute(["-fwdelete"])
def resetTarget(self):
self._execute([])

View File

@@ -25,7 +25,7 @@ SBRSA:0xA:r
SBRV:0x32800:r
PCROP1A_STRT:0x1FF:r
PCROP1A_END:0x0:r
PCROP_RDP:0x1:r
PCROP_RDP:0x1:rw
PCROP1B_STRT:0x1FF:r
PCROP1B_END:0x0:r
WRP1A_STRT:0xFF:r

View File

@@ -6,12 +6,12 @@ import subprocess
import sys
import os
from flipper.app import App
from flipper.cube import CubeProgrammer
class Main:
def __init__(self):
# command args
self.parser = argparse.ArgumentParser()
self.parser.add_argument("-d", "--debug", action="store_true", help="Debug")
class Main(App):
def init(self):
self.subparsers = self.parser.add_subparsers(help="sub-command help")
self.parser_check = self.subparsers.add_parser(
"check", help="Check Option Bytes"
@@ -20,39 +20,16 @@ class Main:
"--port", type=str, help="Port to connect: swd or usb1", default="swd"
)
self.parser_check.set_defaults(func=self.check)
# Set command
self.parser_set = self.subparsers.add_parser("set", help="Set Option Bytes")
self.parser_set.add_argument(
"--port", type=str, help="Port to connect: swd or usb1", default="swd"
)
self.parser_set.set_defaults(func=self.set)
# logging
self.logger = logging.getLogger()
# OB
self.ob = {}
def __call__(self):
self.args = self.parser.parse_args()
if "func" not in self.args:
self.parser.error("Choose something to do")
# configure log output
self.log_level = logging.DEBUG if self.args.debug else logging.INFO
self.logger.setLevel(self.log_level)
self.handler = logging.StreamHandler(sys.stdout)
self.handler.setLevel(self.log_level)
self.formatter = logging.Formatter("%(asctime)s [%(levelname)s] %(message)s")
self.handler.setFormatter(self.formatter)
self.logger.addHandler(self.handler)
# execute requested function
self.loadOB()
return_code = self.args.func()
if isinstance(return_code, int):
return return_code
else:
self.logger.error(f"Forgotten return code")
return 255
def loadOB(self):
def before(self):
self.logger.info(f"Loading Option Bytes data")
file_path = os.path.join(os.path.dirname(sys.argv[0]), "ob.data")
file = open(file_path, "r")
@@ -62,47 +39,8 @@ class Main:
def check(self):
self.logger.info(f"Checking Option Bytes")
try:
output = subprocess.check_output(
[
"STM32_Programmer_CLI",
"-q",
"-c",
f"port={self.args.port}",
"-ob displ",
]
)
assert output
except subprocess.CalledProcessError as e:
self.logger.error(e.output.decode())
self.logger.error(f"Failed to call STM32_Programmer_CLI")
return 127
except Exception as e:
self.logger.error(f"Failed to call STM32_Programmer_CLI")
self.logger.exception(e)
return 126
ob_correct = True
for line in output.decode().split("\n"):
line = line.strip()
if not ":" in line:
self.logger.debug(f"Skipping line: {line}")
continue
key, data = line.split(":", 1)
key = key.strip()
data = data.strip()
if not key in self.ob.keys():
self.logger.debug(f"Skipping key: {key}")
continue
self.logger.debug(f"Processing key: {key} {data}")
value, comment = data.split(" ", 1)
value = value.strip()
comment = comment.strip()
if self.ob[key][0] != value:
self.logger.error(
f"Invalid OB: {key} {value}, expected: {self.ob[key][0]}"
)
ob_correct = False
if ob_correct:
cp = CubeProgrammer(self.args.port)
if cp.checkOptionBytes(self.ob):
self.logger.info(f"OB Check OK")
return 0
else:
@@ -111,34 +49,14 @@ class Main:
def set(self):
self.logger.info(f"Setting Option Bytes")
options = []
for key, (value, attr) in self.ob.items():
if "w" in attr:
options.append(f"{key}={value}")
try:
output = subprocess.check_output(
[
"STM32_Programmer_CLI",
"-q",
"-c",
f"port={self.args.port}",
"-ob",
*options,
]
)
assert output
self.logger.info(f"Success")
except subprocess.CalledProcessError as e:
self.logger.error(e.output.decode())
self.logger.error(f"Failed to call STM32_Programmer_CLI")
return 125
except Exception as e:
self.logger.error(f"Failed to call STM32_Programmer_CLI")
self.logger.exception(e)
return 124
return 0
cp = CubeProgrammer(self.args.port)
if cp.setOptionBytes(self.ob):
self.logger.info(f"OB Set OK")
return 0
else:
self.logger.error(f"OB Set FAIL")
return 255
if __name__ == "__main__":
return_code = Main()()
exit(return_code)
Main()()

View File

@@ -32,12 +32,13 @@ OTP_DISPLAYS = {
"mgg": 0x02,
}
from flipper.app import App
from flipper.cube import CubeProgrammer
class Main:
def __init__(self):
# command args
self.parser = argparse.ArgumentParser()
self.parser.add_argument("-d", "--debug", action="store_true", help="Debug")
class Main(App):
def init(self):
# SubParsers
self.subparsers = self.parser.add_subparsers(help="sub-command help")
# Generate All
self.parser_generate_all = self.subparsers.add_parser(
@@ -73,21 +74,6 @@ class Main:
self.logger = logging.getLogger()
self.timestamp = datetime.datetime.now().timestamp()
def __call__(self):
self.args = self.parser.parse_args()
if "func" not in self.args:
self.parser.error("Choose something to do")
# configure log output
self.log_level = logging.DEBUG if self.args.debug else logging.INFO
self.logger.setLevel(self.log_level)
self.handler = logging.StreamHandler(sys.stdout)
self.handler.setLevel(self.log_level)
self.formatter = logging.Formatter("%(asctime)s [%(levelname)s] %(message)s")
self.handler.setFormatter(self.formatter)
self.logger.addHandler(self.handler)
# execute requested function
self.args.func()
def _add_swd_args(self, parser):
parser.add_argument(
"--port", type=str, help="Port to connect: swd or usb1", default="swd"
@@ -153,89 +139,90 @@ class Main:
)
def generate_all(self):
self.logger.debug(f"Generating OTP")
self.logger.info(f"Generating OTP")
self._process_first_args()
self._process_second_args()
open(f"{self.args.file}_first.bin", "wb").write(self._pack_first())
open(f"{self.args.file}_second.bin", "wb").write(self._pack_second())
self.logger.info(
f"Generated files: {self.args.file}_first.bin and {self.args.file}_second.bin"
)
return 0
def flash_first(self):
self.logger.debug(f"Flashing first block of OTP")
self.logger.info(f"Flashing first block of OTP")
self._process_first_args()
filename = f"otp_unknown_first_{self.timestamp}.bin"
file = open(filename, "wb")
file.write(self._pack_first())
file.close()
self._flash_bin("0x1FFF7000", filename)
try:
self.logger.info(f"Packing binary data")
file = open(filename, "wb")
file.write(self._pack_first())
file.close()
self.logger.info(f"Flashing OTP")
cp = CubeProgrammer(self.args.port)
cp.flashBin("0x1FFF7000", filename)
cp.resetTarget()
self.logger.info(f"Flashed Successfully")
os.remove(filename)
except Exception as e:
self.logger.exception(e)
return 0
os.remove(filename)
return 1
def flash_second(self):
self.logger.debug(f"Flashing second block of OTP")
self.logger.info(f"Flashing second block of OTP")
self._process_second_args()
filename = f"otp_{self.args.name}_second_{self.timestamp}.bin"
file = open(filename, "wb")
file.write(self._pack_second())
file.close()
self._flash_bin("0x1FFF7010", filename)
try:
self.logger.info(f"Packing binary data")
file = open(filename, "wb")
file.write(self._pack_second())
file.close()
self.logger.info(f"Flashing OTP")
cp = CubeProgrammer(self.args.port)
cp.flashBin("0x1FFF7010", filename)
cp.resetTarget()
self.logger.info(f"Flashed Successfully")
os.remove(filename)
except Exception as e:
self.logger.exception(e)
return 1
os.remove(filename)
return 0
def flash_all(self):
self.logger.debug(f"Flashing OTP")
self.logger.info(f"Flashing OTP")
self._process_first_args()
self._process_second_args()
filename = f"otp_{self.args.name}_whole_{self.timestamp}.bin"
file = open(filename, "wb")
file.write(self._pack_first())
file.write(self._pack_second())
file.close()
self._flash_bin("0x1FFF7000", filename)
os.remove(filename)
def _flash_bin(self, address, filename):
self.logger.debug(f"Programming {filename} at {address}")
try:
output = subprocess.check_output(
[
"STM32_Programmer_CLI",
"-q",
"-c",
f"port={self.args.port}",
"-d",
filename,
f"{address}",
]
)
assert output
self.logger.info(f"Success")
except subprocess.CalledProcessError as e:
self.logger.error(e.output.decode())
self.logger.error(f"Failed to call STM32_Programmer_CLI")
return
self.logger.info(f"Packing binary data")
file = open(filename, "wb")
file.write(self._pack_first())
file.write(self._pack_second())
file.close()
self.logger.info(f"Flashing OTP")
cp = CubeProgrammer(self.args.port)
cp.flashBin("0x1FFF7000", filename)
cp.resetTarget()
self.logger.info(f"Flashed Successfully")
os.remove(filename)
except Exception as e:
self.logger.error(f"Failed to call STM32_Programmer_CLI")
self.logger.exception(e)
return
# reboot
subprocess.check_output(
[
"STM32_Programmer_CLI",
"-q",
"-c",
f"port={self.args.port}",
]
)
return 1
return 0
if __name__ == "__main__":