2020-08-25 21:02:02 -04:00
|
|
|
#!/usr/bin/env python
|
2020-09-11 19:33:56 -04:00
|
|
|
import argparse
|
|
|
|
|
import hashlib
|
2022-03-03 11:42:05 -05:00
|
|
|
import subprocess
|
2020-09-11 19:33:56 -04:00
|
|
|
import shutil
|
2020-12-03 16:56:01 -05:00
|
|
|
import shlex
|
2020-09-11 19:33:56 -04:00
|
|
|
import time
|
|
|
|
|
import sys
|
|
|
|
|
import re
|
|
|
|
|
import os
|
2020-08-25 21:02:02 -04:00
|
|
|
|
2022-03-03 11:42:05 -05:00
|
|
|
try:
|
|
|
|
|
sys.path.append(os.path.join(os.environ["IDF_PATH"], "components", "partition_table"))
|
2022-03-14 00:31:13 -04:00
|
|
|
import serial, parttool
|
2022-03-03 11:42:05 -05:00
|
|
|
except:
|
|
|
|
|
pass
|
|
|
|
|
|
2021-09-24 16:50:27 -04:00
|
|
|
DEFAULT_TARGET = os.getenv("RG_TOOL_TARGET", "odroid-go")
|
|
|
|
|
DEFAULT_BAUD = os.getenv("RG_TOOL_BAUD", "1152000")
|
|
|
|
|
DEFAULT_PORT = os.getenv("RG_TOOL_PORT", "COM3")
|
2022-03-16 18:16:54 -04:00
|
|
|
PROJECT_NAME = os.getenv("PROJECT_NAME", "Retro-Go") # os.path.basename(os.getcwd()).title()
|
2022-03-14 00:19:22 -04:00
|
|
|
PROJECT_ICON = os.getenv("PROJECT_ICON", "icon.raw")
|
|
|
|
|
PROJECT_APPS = os.getenv("PROJECT_APPS", "partitions.csv")
|
2022-01-31 07:53:00 -05:00
|
|
|
try:
|
2022-03-14 00:19:22 -04:00
|
|
|
PROJECT_VER = os.getenv("PROJECT_VER") or subprocess.check_output(
|
|
|
|
|
"git describe --tags --abbrev=5 --dirty --always", shell=True
|
|
|
|
|
).decode().rstrip()
|
2022-01-31 07:53:00 -05:00
|
|
|
except:
|
2022-03-14 00:19:22 -04:00
|
|
|
PROJECT_VER = "unknown"
|
2020-08-25 21:02:02 -04:00
|
|
|
|
2022-03-14 00:19:22 -04:00
|
|
|
if type(PROJECT_APPS) is str: # Assume it's a partitions.csv, we must then parse it
|
|
|
|
|
filename = PROJECT_APPS
|
|
|
|
|
PROJECT_APPS = {}
|
|
|
|
|
try:
|
|
|
|
|
with open(filename, "r") as f:
|
|
|
|
|
for line in f:
|
2022-03-14 00:31:13 -04:00
|
|
|
m = re.match(r"^\s*([^#]+)\s*,\s*(.+)\s*,\s*(.+)\s*,\s*(.+)\s*,\s*([^#]+)", line)
|
|
|
|
|
if m and m[2] in ["app", "0"]:
|
2022-03-14 00:19:22 -04:00
|
|
|
PROJECT_APPS[m[1]] = [0, int(m[5], base=0), m[2], m[3]]
|
|
|
|
|
except:
|
|
|
|
|
exit("Failed reading partitions from '%s' (PROJECT_APPS)." % filename)
|
|
|
|
|
|
|
|
|
|
if not PROJECT_APPS:
|
|
|
|
|
exit("No subprojects defined. Are you running from the project's directory?")
|
|
|
|
|
|
|
|
|
|
if not os.getenv("IDF_PATH"):
|
|
|
|
|
exit("IDF_PATH is not defined. Are you running inside esp-idf environment?")
|
2020-09-11 19:33:56 -04:00
|
|
|
|
2022-01-31 07:53:00 -05:00
|
|
|
|
2020-09-11 19:33:56 -04:00
|
|
|
class Symbol:
|
|
|
|
|
def __init__(self, address, name, source="??:?", inlined=None):
|
|
|
|
|
self.address = int(str(address), 0)
|
|
|
|
|
self.name = name
|
|
|
|
|
self.source = os.path.normpath(source)
|
|
|
|
|
self.inlined = inlined
|
|
|
|
|
self.hash = hashlib.sha1(bytes(self.source + self.name, "UTF-8")).hexdigest()
|
|
|
|
|
|
|
|
|
|
def __str__(self):
|
|
|
|
|
text = "0x%x: %s at %s" % (self.address, self.name, self.source)
|
|
|
|
|
if self.inlined:
|
|
|
|
|
text += "\ninlined by %s" % str(self.inlined)
|
|
|
|
|
return text.replace("\n", "\n ")
|
|
|
|
|
|
2020-09-11 21:35:57 -04:00
|
|
|
|
|
|
|
|
class CallBranch:
|
|
|
|
|
def __init__(self, name, parent=None):
|
|
|
|
|
self.name = name
|
|
|
|
|
self.parent = parent
|
|
|
|
|
self.run_time = 0;
|
|
|
|
|
self.children = dict()
|
|
|
|
|
|
|
|
|
|
def add_frame(self, caller, callee, num_calls, run_time):
|
|
|
|
|
if callee.hash not in self.children:
|
|
|
|
|
self.children[callee.hash] = [caller, callee, num_calls, run_time]
|
|
|
|
|
else:
|
|
|
|
|
self.children[callee.hash][2] += num_calls
|
|
|
|
|
self.children[callee.hash][3] += run_time
|
|
|
|
|
self.run_time += run_time
|
|
|
|
|
|
|
|
|
|
|
2022-01-31 07:53:00 -05:00
|
|
|
def debug_print(text):
|
|
|
|
|
print("\033[0;33m%s\033[0m" % text)
|
|
|
|
|
|
|
|
|
|
|
2020-09-11 19:33:56 -04:00
|
|
|
def find_symbol(elf_file, addr):
|
|
|
|
|
try:
|
|
|
|
|
if addr not in symbols_cache:
|
|
|
|
|
symbols_cache[addr] = Symbol(0, "??")
|
2022-01-31 07:53:00 -05:00
|
|
|
out = subprocess.check_output(["xtensa-esp32-elf-addr2line", "-ifCe", elf_file, addr], shell=True)
|
|
|
|
|
lines = out.decode().rstrip().splitlines()
|
2020-09-11 19:33:56 -04:00
|
|
|
if len(lines) > 2:
|
|
|
|
|
symbols_cache[addr] = Symbol(addr, lines[0], lines[1], Symbol(0, lines[2], lines[3]))
|
|
|
|
|
elif len(lines) == 2:
|
|
|
|
|
symbols_cache[addr] = Symbol(addr, lines[0], lines[1])
|
|
|
|
|
except:
|
|
|
|
|
pass
|
|
|
|
|
return symbols_cache[addr]
|
2022-03-14 00:19:22 -04:00
|
|
|
symbols_cache = dict()
|
2020-09-11 19:33:56 -04:00
|
|
|
|
|
|
|
|
|
|
|
|
|
def analyze_profile(frames):
|
2020-09-11 21:35:57 -04:00
|
|
|
flatten = True # False is currently not working correctly
|
2020-09-11 19:33:56 -04:00
|
|
|
tree = dict()
|
|
|
|
|
|
|
|
|
|
for caller, callee, num_calls, run_time in frames:
|
2020-09-11 21:35:57 -04:00
|
|
|
branch = '*' if flatten else caller.name + "@" + os.path.basename(caller.source)
|
2020-09-11 19:33:56 -04:00
|
|
|
if branch not in tree:
|
2020-09-11 21:35:57 -04:00
|
|
|
tree[branch] = CallBranch(branch, caller)
|
|
|
|
|
tree[branch].add_frame(caller, callee, num_calls, run_time)
|
|
|
|
|
|
|
|
|
|
tree_sorted = sorted(tree.values(), key=lambda x: x.run_time, reverse=True)
|
|
|
|
|
|
|
|
|
|
for branch in tree_sorted:
|
|
|
|
|
if branch.run_time < 100_000:
|
|
|
|
|
continue
|
|
|
|
|
|
|
|
|
|
debug_print("%-68s %dms" % (branch.name, branch.run_time / 1000))
|
|
|
|
|
children = sorted(branch.children.values(), key=lambda x: x[3], reverse=True)
|
|
|
|
|
|
|
|
|
|
for caller, callee, num_calls, run_time in children:
|
|
|
|
|
if run_time < 10_000:
|
|
|
|
|
continue
|
|
|
|
|
debug_print(" %-32s %-20s %-10d %dms"
|
|
|
|
|
% (callee.name, os.path.basename(callee.source), num_calls, run_time / 1000))
|
|
|
|
|
|
|
|
|
|
debug_print("")
|
2020-09-11 19:33:56 -04:00
|
|
|
|
|
|
|
|
|
2022-03-19 21:17:44 -04:00
|
|
|
def build_firmware(targets, device_type):
|
2020-08-27 16:40:45 -04:00
|
|
|
args = [
|
|
|
|
|
sys.executable,
|
|
|
|
|
"tools/mkfw.py",
|
2021-12-17 13:25:35 -05:00
|
|
|
("%s_%s_%s.fw" % (PROJECT_NAME, PROJECT_VER, device_type)).lower(),
|
2020-08-27 16:40:45 -04:00
|
|
|
("%s %s" % (PROJECT_NAME, PROJECT_VER)),
|
2022-03-14 00:19:22 -04:00
|
|
|
PROJECT_ICON
|
2020-08-27 16:40:45 -04:00
|
|
|
]
|
2021-09-13 20:38:10 -04:00
|
|
|
|
|
|
|
|
if device_type in ["mrgc-g32", "esplay"]:
|
|
|
|
|
args.append("--esplay")
|
|
|
|
|
|
2020-08-27 16:40:45 -04:00
|
|
|
for target in targets:
|
|
|
|
|
part = PROJECT_APPS[target]
|
2022-03-19 21:17:44 -04:00
|
|
|
args += [str(0), str(part[0]), str(part[1]), target, os.path.join(target, "build", target + ".bin")]
|
2020-08-25 21:02:02 -04:00
|
|
|
|
2021-03-12 05:17:37 -05:00
|
|
|
commandline = ' '.join(shlex.quote(arg) for arg in args[1:]) # shlex.join()
|
|
|
|
|
print("Building firmware: %s\n" % commandline)
|
2022-03-14 00:19:22 -04:00
|
|
|
subprocess.run(args, check=True)
|
2020-08-25 21:02:02 -04:00
|
|
|
|
|
|
|
|
|
2021-09-13 20:38:10 -04:00
|
|
|
def clean_app(target):
|
2020-08-25 21:02:02 -04:00
|
|
|
print("Cleaning up app '%s'..." % target)
|
2021-06-19 11:33:11 -04:00
|
|
|
try:
|
2022-01-31 07:53:00 -05:00
|
|
|
os.unlink(os.path.join(target, "sdkconfig"))
|
|
|
|
|
os.unlink(os.path.join(target, "sdkconfig.old"))
|
2021-09-13 20:38:10 -04:00
|
|
|
except:
|
|
|
|
|
pass
|
|
|
|
|
try:
|
2022-01-31 07:53:00 -05:00
|
|
|
shutil.rmtree(os.path.join(target, "build"))
|
2021-06-19 11:33:11 -04:00
|
|
|
except:
|
|
|
|
|
pass
|
|
|
|
|
print("Done.\n")
|
2020-08-25 21:02:02 -04:00
|
|
|
|
|
|
|
|
|
2022-03-19 21:17:44 -04:00
|
|
|
def build_app(target, device_type, with_profiling=False, with_netplay=False):
|
2020-09-11 19:33:56 -04:00
|
|
|
# To do: clean up if any of the flags changed since last build
|
2020-08-25 21:02:02 -04:00
|
|
|
print("Building app '%s'" % target)
|
2022-03-19 21:17:44 -04:00
|
|
|
os.putenv("ENABLE_PROFILING", "1" if with_profiling else "0")
|
2020-09-16 14:35:38 -04:00
|
|
|
os.putenv("ENABLE_NETPLAY", "1" if with_netplay else "0")
|
2021-06-19 11:33:11 -04:00
|
|
|
os.putenv("PROJECT_VER", PROJECT_VER)
|
2022-03-19 21:17:44 -04:00
|
|
|
os.putenv("RG_TARGET", re.sub(r'[^A-Z0-9]', '_', device_type.upper()))
|
2022-03-14 00:19:22 -04:00
|
|
|
subprocess.run("idf.py app", shell=True, check=True, cwd=os.path.join(os.getcwd(), target))
|
2020-09-12 01:44:55 -04:00
|
|
|
|
2021-11-24 15:46:54 -05:00
|
|
|
try:
|
|
|
|
|
print("\nPatching esp_image_header_t to skip sha256 on boot... ", end="")
|
2022-02-02 16:39:25 -05:00
|
|
|
with open(os.path.join(target, "build", target + ".bin"), "r+b") as fp:
|
2021-11-24 15:46:54 -05:00
|
|
|
fp.seek(23)
|
|
|
|
|
fp.write(b"\0")
|
|
|
|
|
print("done!\n")
|
|
|
|
|
except: # don't really care if that fails
|
|
|
|
|
print("failed!\n")
|
|
|
|
|
pass
|
2020-08-25 21:02:02 -04:00
|
|
|
|
|
|
|
|
|
2020-09-11 19:33:56 -04:00
|
|
|
def monitor_app(target, port, baudrate=115200):
|
2020-08-25 21:02:02 -04:00
|
|
|
print("Starting monitor for app '%s'" % target)
|
2020-09-11 19:33:56 -04:00
|
|
|
mon = serial.Serial(port, baudrate=baudrate, timeout=0)
|
2022-01-31 07:53:00 -05:00
|
|
|
elf = os.path.join(target, "build", target + ".elf")
|
2020-09-11 19:33:56 -04:00
|
|
|
|
|
|
|
|
mon.setDTR(False)
|
|
|
|
|
mon.setRTS(False)
|
|
|
|
|
|
|
|
|
|
# To do: detect ctrl+r ctrl+c etc
|
|
|
|
|
|
|
|
|
|
profile_frames = list()
|
|
|
|
|
|
|
|
|
|
line_bytes = b''
|
|
|
|
|
while 1:
|
2020-09-13 16:01:33 -04:00
|
|
|
if mon.in_waiting == 0:
|
2020-09-11 19:33:56 -04:00
|
|
|
sys.stdout.flush()
|
|
|
|
|
time.sleep(0.010)
|
2020-09-13 16:01:33 -04:00
|
|
|
continue
|
|
|
|
|
|
|
|
|
|
byte = mon.read()
|
|
|
|
|
|
|
|
|
|
if byte != b"\n":
|
|
|
|
|
sys.stdout.buffer.write(byte) # byte.decode()
|
|
|
|
|
line_bytes += byte
|
|
|
|
|
else:
|
|
|
|
|
line = line_bytes.decode(errors="ignore").rstrip()
|
|
|
|
|
line_bytes = b''
|
|
|
|
|
|
|
|
|
|
# check for debug data meant to be analyzed, not displayed
|
|
|
|
|
m = re.match(r"^RGD:([A-Z0-9]+):([A-Z0-9]+)\s*(.*)$", line)
|
|
|
|
|
if m:
|
|
|
|
|
rg_debug_ns = m.group(1)
|
|
|
|
|
rg_debug_cmd = m.group(2)
|
|
|
|
|
rg_debug_arg = m.group(3)
|
|
|
|
|
|
|
|
|
|
sys.stdout.buffer.write(b"\r") # Clear the line
|
|
|
|
|
|
|
|
|
|
if rg_debug_ns == "PROF":
|
|
|
|
|
if rg_debug_cmd == "BEGIN":
|
|
|
|
|
profile_frames.clear()
|
|
|
|
|
if rg_debug_cmd == "END":
|
|
|
|
|
analyze_profile(profile_frames)
|
|
|
|
|
if rg_debug_cmd == "DATA":
|
|
|
|
|
m = re.match(r"([x0-9a-f]+)\s([x0-9a-f]+)\s(\d+)\s(\d+)", rg_debug_arg)
|
|
|
|
|
if m:
|
|
|
|
|
profile_frames.append([
|
|
|
|
|
find_symbol(elf, m.group(1)),
|
|
|
|
|
find_symbol(elf, m.group(2)),
|
|
|
|
|
int(m.group(3)),
|
|
|
|
|
int(m.group(4)),
|
|
|
|
|
])
|
|
|
|
|
continue
|
|
|
|
|
|
|
|
|
|
sys.stdout.buffer.write(b"\n")
|
|
|
|
|
|
|
|
|
|
# check for symbol addresses
|
|
|
|
|
for addr in re.findall(r"0x4[0-9a-fA-F]{7}", line):
|
|
|
|
|
symbol = find_symbol(elf, addr)
|
|
|
|
|
if symbol and "??:" not in symbol.source:
|
|
|
|
|
debug_print(symbol)
|
2020-08-25 21:02:02 -04:00
|
|
|
|
|
|
|
|
|
2020-08-27 16:40:45 -04:00
|
|
|
parser = argparse.ArgumentParser(description="Retro-Go build tool")
|
|
|
|
|
parser.add_argument(
|
2020-09-11 19:41:51 -04:00
|
|
|
# To do: Learn to use subcommands instead...
|
2022-03-19 21:17:44 -04:00
|
|
|
"command", choices=["build-fw", "build-img", "release", "build", "clean", "flash", "monitor", "run", "profile"],
|
2020-08-27 16:40:45 -04:00
|
|
|
)
|
|
|
|
|
parser.add_argument(
|
2021-06-19 11:33:11 -04:00
|
|
|
"apps", nargs="*", default="all", choices=["all"] + list(PROJECT_APPS.keys())
|
2020-08-27 16:40:45 -04:00
|
|
|
)
|
2020-09-11 19:33:56 -04:00
|
|
|
parser.add_argument(
|
2022-01-22 17:36:26 -05:00
|
|
|
"--target", default=DEFAULT_TARGET, choices=["odroid-go", "esp32s2", "mrgc-g32", "qtpy-gamer"], help="Device to target"
|
2020-09-11 19:33:56 -04:00
|
|
|
)
|
2020-09-09 16:43:17 -04:00
|
|
|
parser.add_argument(
|
2021-06-19 11:33:11 -04:00
|
|
|
"--with-netplay", action="store_const", const=True, help="Build with netplay enabled"
|
2020-09-09 16:43:17 -04:00
|
|
|
)
|
2020-08-27 16:40:45 -04:00
|
|
|
parser.add_argument(
|
2021-09-24 12:26:08 -04:00
|
|
|
"--port", default=DEFAULT_PORT, help="Serial port to use for flash and monitor"
|
|
|
|
|
)
|
|
|
|
|
parser.add_argument(
|
|
|
|
|
"--baud", default=DEFAULT_BAUD, help="Serial baudrate to use for flashing"
|
2020-08-27 16:40:45 -04:00
|
|
|
)
|
|
|
|
|
args = parser.parse_args()
|
|
|
|
|
|
2020-09-09 16:43:17 -04:00
|
|
|
|
2020-08-27 16:40:45 -04:00
|
|
|
command = args.command
|
2021-06-19 11:33:11 -04:00
|
|
|
apps = args.apps if "all" not in args.apps else PROJECT_APPS.keys()
|
2020-08-27 16:40:45 -04:00
|
|
|
|
2020-09-09 16:43:17 -04:00
|
|
|
|
2021-12-06 18:39:18 -05:00
|
|
|
if command in ["clean", "release"]:
|
2021-09-13 20:38:10 -04:00
|
|
|
print("=== Step: Cleaning ===\n")
|
|
|
|
|
for app in apps:
|
|
|
|
|
clean_app(app)
|
2021-06-19 11:33:11 -04:00
|
|
|
|
2022-03-19 21:17:44 -04:00
|
|
|
if command in ["build", "build-fw", "build-img", "release", "run", "profile"]:
|
2021-09-13 20:38:10 -04:00
|
|
|
print("=== Step: Building ===\n")
|
|
|
|
|
for app in apps:
|
2022-03-19 21:17:44 -04:00
|
|
|
build_app(app, args.target, command == "profile", args.with_netplay)
|
2020-08-25 21:02:02 -04:00
|
|
|
|
2021-12-06 18:39:18 -05:00
|
|
|
if command in ["build-fw", "release"]:
|
2021-09-13 20:38:10 -04:00
|
|
|
print("=== Step: Packing ===\n")
|
2022-03-19 21:17:44 -04:00
|
|
|
build_firmware(apps, args.target)
|
|
|
|
|
|
|
|
|
|
# if command in ["build-img", "release"]:
|
|
|
|
|
# print("=== Step: Packing ===\n")
|
|
|
|
|
# build_image(apps, args.target)
|
2020-08-25 21:02:02 -04:00
|
|
|
|
2022-03-19 21:17:44 -04:00
|
|
|
if command in ["flash", "run", "profile"]:
|
2021-09-13 20:38:10 -04:00
|
|
|
print("=== Step: Flashing ===\n")
|
2022-03-14 00:31:13 -04:00
|
|
|
if "parttool" not in globals():
|
|
|
|
|
exit("Failed to load the parttool module from your esp-idf framework.")
|
2022-03-03 11:42:05 -05:00
|
|
|
try:
|
|
|
|
|
pt = parttool.ParttoolTarget(args.port, args.baud)
|
|
|
|
|
except:
|
|
|
|
|
exit("Failed to read device's partition table!")
|
|
|
|
|
try:
|
|
|
|
|
for app in apps:
|
|
|
|
|
print("Flashing app '%s'" % app)
|
|
|
|
|
pt.write_partition(parttool.PartitionName(app), os.path.join(app, "build", app + ".bin"))
|
|
|
|
|
except Exception as e:
|
|
|
|
|
print("Error: {}".format(e))
|
|
|
|
|
if "does not exist" in str(e):
|
|
|
|
|
print("This indicates that the partition table on your device is incorrect.")
|
|
|
|
|
print("Make sure you've installed a recent retro-go-*.fw!")
|
|
|
|
|
exit("Task failed.")
|
2020-09-09 16:43:17 -04:00
|
|
|
|
2022-03-19 21:17:44 -04:00
|
|
|
if command in ["monitor", "run", "profile"]:
|
2021-09-13 20:38:10 -04:00
|
|
|
print("=== Step: Monitoring ===\n")
|
2022-03-14 00:31:13 -04:00
|
|
|
if "serial" not in globals():
|
|
|
|
|
exit("Failed to load the serial module. You can try running 'pip install pyserial'.")
|
2021-06-19 11:33:11 -04:00
|
|
|
if len(apps) == 1:
|
|
|
|
|
monitor_app(apps[0], args.port)
|
2021-02-06 15:56:11 -05:00
|
|
|
else:
|
|
|
|
|
monitor_app("dummy", args.port)
|
2020-08-25 21:02:02 -04:00
|
|
|
|
|
|
|
|
print("All done!")
|