# encoding: utf-8 """ Waf tool for ChibiOS build """ from waflib import Errors, Logs, Task, Utils, Context from waflib.TaskGen import after_method, before_method, feature import os import shutil import sys import re import pickle import struct import base64 _dynamic_env_data = {} def _load_dynamic_env_data(bld): bldnode = bld.bldnode.make_node('modules/ChibiOS') tmp_str = bldnode.find_node('include_dirs').read() tmp_str = tmp_str.replace(';\n','') tmp_str = tmp_str.replace('-I','') #remove existing -I flags # split, coping with separator idirs = re.split('; ', tmp_str) # create unique list, coping with relative paths idirs2 = [] for d in idirs: if d.startswith('../'): # relative paths from the make build are relative to BUILDROOT d = os.path.join(bld.env.BUILDROOT, d) d = os.path.normpath(d) if not d in idirs2: idirs2.append(d) _dynamic_env_data['include_dirs'] = idirs2 @feature('ch_ap_library', 'ch_ap_program') @before_method('process_source') def ch_dynamic_env(self): # The generated files from configuration possibly don't exist if it's just # a list command (TODO: figure out a better way to address that). if self.bld.cmd == 'list': return if not _dynamic_env_data: _load_dynamic_env_data(self.bld) self.use += ' ch' self.env.append_value('INCLUDES', _dynamic_env_data['include_dirs']) class upload_fw(Task.Task): color='BLUE' always_run = True def run(self): import platform upload_tools = self.env.get_flat('UPLOAD_TOOLS') upload_port = self.generator.bld.options.upload_port src = self.inputs[0] # Refer Tools/scripts/macos_remote_upload.sh for details if 'AP_OVERRIDE_UPLOAD_CMD' in os.environ: cmd = "{} '{}'".format(os.environ['AP_OVERRIDE_UPLOAD_CMD'], src.abspath()) elif "microsoft-standard-WSL2" in platform.release(): if not self.wsl2_prereq_checks(): return print("If this takes takes too long here, try power-cycling your hardware\n") cmd = "{} '{}/uploader.py' '{}'".format('python.exe', upload_tools, src.abspath()) else: cmd = "{} '{}/uploader.py' '{}'".format(self.env.get_flat('PYTHON'), upload_tools, src.abspath()) if upload_port is not None: cmd += " '--port' '%s'" % upload_port return self.exec_command(cmd) def wsl2_prereq_checks(self): # As of July 2022 WSL2 does not support native USB support. The workaround from Microsoft # using 'usbipd' does not work due to the following workflow: # # 1) connect USB device to Windows computer running WSL2 # 2) device boots into app # 3) use 'usbipd' from Windows Cmd/PowerShell to determine busid, this is very hard to automate on Windows # 4) use 'usbipd' from Windows Cmd/PowerShell to attach, this is very hard to automate on Windows # -- device is now viewable via 'lsusb' but you need sudo to read from it. # either run 'chmod666 /dev/ttyACM*' or use udev to automate chmod on device connect # 5) uploader.py detects device, sends reboot command which disconnects the USB port and reboots into # bootloader (different USB device) # 6) manually repeat steps 3 & 4 # 7) doing steps 3 and 4 will most likely take several seconds and in many cases the bootloader has # moved on into the app # # Solution: simply call "python.exe" instead of 'python' which magically calls it from the windows # system using the same absolute path back into the WSL2's user's directory # Requirements: Windows must have Python3.9.x (NTO 3.10.x) installed and a few packages. import subprocess try: where_python = subprocess.check_output('where.exe python.exe', shell=True, text=True) except subprocess.CalledProcessError: #if where.exe can't find the file it returns a non-zero result which throws this exception where_python = "" if not where_python or not "\Python\Python" in where_python or "python.exe" not in where_python: print(self.get_full_wsl2_error_msg("Windows python.exe not found")) return False python_version = subprocess.check_output('python.exe --version', shell=True, text=True) if "3.10." in python_version: print(self.get_full_wsl2_error_msg("Your Windows %s version is not compatible" % python_version.strip())) return False return True def get_full_wsl2_error_msg(self, error_msg): return (""" **************************************** **************************************** WSL2 firmware uploads use the host's Windows Python.exe so it has access to the COM ports. %s Please download Windows Installer 3.9.x (not 3.10) from https://www.python.org/downloads/ and make sure to add it to your path during the installation. Once installed, run this command in Powershell or Command Prompt to install some packages: pip.exe install empy pyserial **************************************** **************************************** """ % error_msg) def exec_command(self, cmd, **kw): kw['stdout'] = sys.stdout return super(upload_fw, self).exec_command(cmd, **kw) def keyword(self): return "Uploading" class set_default_parameters(Task.Task): color='CYAN' always_run = True def keyword(self): return "apj_tool" def run(self): rel_default_parameters = self.env.get_flat('DEFAULT_PARAMETERS').replace("'", "") abs_default_parameters = os.path.join(self.env.SRCROOT, rel_default_parameters) apj_tool = self.env.APJ_TOOL sys.path.append(os.path.dirname(apj_tool)) from apj_tool import embedded_defaults defaults = embedded_defaults(self.inputs[0].abspath()) if defaults.find(): defaults.set_file(abs_default_parameters) defaults.save() class generate_bin(Task.Task): color='CYAN' # run_str="${OBJCOPY} -O binary ${SRC} ${TGT}" always_run = True EXTF_MEMORY_START = 0x90000000 EXTF_MEMORY_END = 0x90FFFFFF INTF_MEMORY_START = 0x08000000 INTF_MEMORY_END = 0x08FFFFFF def keyword(self): return "Generating" def run(self): if self.env.HAS_EXTERNAL_FLASH_SECTIONS: ret = self.split_sections() if (ret < 0): return ret return ret else: cmd = [self.env.get_flat('OBJCOPY'), '-O', 'binary', self.inputs[0].relpath(), self.outputs[0].relpath()] self.exec_command(cmd) '''list sections and split into two binaries based on section's location in internal, external or in ram''' def split_sections(self): # get a list of sections cmd = "'{}' -A -x {}".format(self.env.get_flat('SIZE'), self.inputs[0].relpath()) out = self.generator.bld.cmd_and_log(cmd, quiet=Context.BOTH, cwd=self.env.get_flat('BUILDROOT')) extf_sections = [] intf_sections = [] is_text_in_extf = False found_text_section = False ramsections = [] for line in out.splitlines(): section_line = line.split() if (len(section_line) < 3): continue try: if int(section_line[2], 0) == 0: continue else: addr = int(section_line[2], 0) except ValueError: continue if (addr >= self.EXTF_MEMORY_START) and (addr <= self.EXTF_MEMORY_END): extf_sections.append("--only-section=%s" % section_line[0]) if section_line[0] == '.text': is_text_in_extf = True found_text_section = True elif (addr >= self.INTF_MEMORY_START) and (addr <= self.INTF_MEMORY_END): intf_sections.append("--only-section=%s" % section_line[0]) if section_line[0] == '.text': is_text_in_extf = False found_text_section = True else: # most likely RAM data, we place it in the same bin as text ramsections.append(section_line[0]) if found_text_section: for section in ramsections: if is_text_in_extf: extf_sections.append("--only-section=%s" % section) else: intf_sections.append("--only-section=%s" % section) else: Logs.error("Couldn't find .text section") # create intf binary if len(intf_sections): cmd = "'{}' {} -O binary {} {}".format(self.env.get_flat('OBJCOPY'), ' '.join(intf_sections), self.inputs[0].relpath(), self.outputs[0].relpath()) else: cmd = "cp /dev/null {}".format(self.outputs[0].relpath()) ret = self.exec_command(cmd) if (ret < 0): return ret # create extf binary cmd = "'{}' {} -O binary {} {}".format(self.env.get_flat('OBJCOPY'), ' '.join(extf_sections), self.inputs[0].relpath(), self.outputs[1].relpath()) return self.exec_command(cmd) def __str__(self): return self.outputs[0].path_from(self.generator.bld.bldnode) def to_unsigned(i): '''convert a possibly signed integer to unsigned''' if i < 0: i += 2**32 return i def sign_firmware(image, private_keyfile): '''sign firmware with private key''' try: import monocypher except ImportError: Logs.error("Please install monocypher with: python3 -m pip install pymonocypher") return None try: key = open(private_keyfile, 'r').read() except Exception as ex: Logs.error("Failed to open %s" % private_keyfile) return None keytype = "PRIVATE_KEYV1:" if not key.startswith(keytype): Logs.error("Bad private key file %s" % private_keyfile) return None key = base64.b64decode(key[len(keytype):]) sig = monocypher.signature_sign(key, image) sig_len = len(sig) sig_version = 30437 return struct.pack("