class AvrdudeApp(ZeroApp): menu_name = "Avrdude" default_config = '{"default_part":"m328p", "default_programmer":"usbasp", "last_write_file":"/tmp/tmp.hex", "last_read_filename":"tmp", "last_read_dir":"/tmp", "last_write_fuse_params":[], "filter_programmers":true, "last_bitclock":5, "bootloader_config_filename":"bootloaders.json"}' config_filename = "config.json" # These programmers are selected on whether they: # 1. don't require a bootloader (otherwise pyavrdude.detect_chip breaks) # So, "arduino" and similar are out # Especially since "arduino" requires to use a serial port # and we don't yet have a way to set it # 2. make sense on ZeroPhone (LPT and COM programmers are out) supported_programmers = [ "usbasp", "avrisp2", "usbasp-clone", "usbtiny", "pickit2" ] # These are the fuse types that will, by default, be offered for read/ # autodetected for write fuse_types = ["hfuse", "lfuse", "efuse"] flash_format = 'i' fuse_format = 'i' def __init__(self, *args, **kwargs): ZeroApp.__init__(self, *args, **kwargs) # Initializing variables from config file self.config = read_or_create_config(local_path(self.config_filename), self.default_config, self.menu_name + " app") self.save_config = save_config_method_gen( self, local_path(self.config_filename)) self.current_chip = self.config["default_part"] self.current_programmer = self.config["default_programmer"] self.filter_programmers = self.config["filter_programmers"] self.bitclock = self.config["last_bitclock"] self.read_filename = self.config["last_read_filename"] self.read_dir = self.config["last_read_dir"] self.write_file = self.config["last_write_file"] self.write_fuse_params = self.config["last_write_fuse_params"] self.bootloader_config_filename = self.config[ "bootloader_config_filename"] # Creating UI elements to be used in run_and_monitor_process() # they're reusable anyway, and we don't have to init them each time # that we enter run_and_monitor_process() . self.erase_restore_indicator = LoadingIndicator(self.i, self.o, message="Erasing") self.read_write_bar = ProgressBar(self.i, self.o) def set_context(self, context): """ A ZPUI-specific function to get the context. For now, is used to avoid polling the chip if the app not the one active. """ self.context = context # Avrdude application flow: # 1. Main menu - select action (read/write/erase) # 2. Parameters are set up and checked # 3. A process is created # 4. Files/options are added to the process' commandline # 5. Chip is detected # 6. Process is started and monitored # 1. Main menu def check_avrdude_available(self): try: with open(os.devnull, "w") as f: assert (call(['avrdude'], stdout=f, stderr=f) == 0) return True except (AssertionError, OSError): return False def on_start(self): if not self.check_avrdude_available(): PrettyPrinter("Avrdude not available!", self.i, self.o, 3) return mc = [["Read chip", self.read_menu], ["Write chip", self.write_menu], ["Erase chip", self.erase_menu], ["Pick programmer", self.pick_programmer], ["Pick chip", self.pick_chip], ["Pinouts", lambda: graphics.show_pinouts(self.i, self.o)], ["Settings", self.settings_menu]] Menu(mc, self.i, self.o, name="Avrdude app main menu").activate() # 2. Initial read/write/erase menus and safety checks def read_menu(self): def verify_and_run(): """ If either directory that we need to read into does not exist, the filename is invalid or there is already an invalid file in that location (character device or something like that), we should warn the user and abort. If there's already a file in that location, we should make the user confirm the overwrite. """ full_path = os.path.join(self.read_dir, self.read_filename) + ".hex" if not os.path.isdir(self.read_dir): PrettyPrinter("Wrong directory!", self.i, self.o, 3) return if '/' in self.read_filename: PrettyPrinter("Filename can't contain a slash!", self.i, self.o, 3) return if os.path.exists(full_path): if os.path.isdir(full_path) or not os.path.isfile(full_path): PrettyPrinter( "Trying to overwrite something that is not a file!", self.i, self.o, 5) return choice = DialogBox( 'ync', self.i, self.o, message="Overwrite?", name="Avrdude write overwrite confirmation").activate() if not choice: return self.create_process() if self.read_checklist(): self.detect_and_run() # Needs to be autogenerated so that filename/path update after changing them def get_contents(): contents = [[ "Filename: {}".format(self.read_filename), lambda: self.set_filename("read_filename", "last_read_filename", message="Read filename:") ], [ "Folder: {}".format(self.read_dir), lambda: self.set_dir("read_dir", "last_read_dir") ], ["Continue", verify_and_run]] return contents Menu([], self.i, self.o, contents_hook=get_contents, name="Avrdude read parameter menu", append_exit=False).activate() def write_menu(self): def verify_and_run(): """ If file that we need to write to the chip does not exist, we should warn the user and abort. """ if not (os.path.exists(self.write_file) and os.path.isfile(self.write_file)): PrettyPrinter("File does not exist/invalid!", self.i, self.o, 2) return self.create_process() if self.write_checklist(): self.detect_and_run() # Needs to be autogenerated so that filename updates after changing them def get_contents(): if self.write_file: dir, filename = os.path.split(self.write_file) else: dir, filename = '/', 'None' contents = [[ "File: {}".format(filename), lambda x=dir: self.set_write_file(x) ], ["Use bootloader", self.pick_bootloader], ["Use last read file", self.set_write_as_last_read], ["Continue", verify_and_run]] return contents Menu([], self.i, self.o, contents_hook=get_contents, name="Avrdude write parameter menu", append_exit=False).activate() def erase_menu(self): """ This function is only named this way for naming consistency. There's no menu - at least, not yet; it simply checks whether the user presses "Yes" in a DialogBox. """ self.create_process() if self.erase_checklist(): self.detect_and_run() # 3. Process creation def create_process(self): """Creates the avrdude process.""" self.p = pyavrdude.AvrdudeProcess( self.current_chip, self.current_programmer, parameters=self.get_avrdude_parameters()) def get_avrdude_parameters(self): """ Returns the additional parameters for avrdude to use. For now, only works with bitclock; other parameters (including programmer-specific ones) can be added here later. Is used both by ``detect_chip()`` and ``create_process()``. """ return ['-B', str(self.bitclock)] # 4. Read/write/erase setup functions concerning file types and fuses def read_checklist(self): """ Asks about types of memory that you'd like to read - for now, hardcoded to ["flash", "hfuse", "lfuse", "efuse"]. Doesn't yet check if the MCU actually has the type of memory requested. This version only supports reading fuses using the 'i' mode - for now. """ types = ["flash"] + self.fuse_types choices = [[c.capitalize(), c] for c in types] choices = Checkbox( choices, self.i, self.o, default_state=True, name="Avrdude read memory selection checkbox").activate() base_filename = self.read_filename if not choices: return False for choice in choices: if choices[choice]: # Flash is read by default extension = "hex" if choice == "flash" else choice format = self.flash_format if choice == "flash" else self.fuse_format file_path = os.path.join( self.read_dir, "{}.{}".format(base_filename, extension)) self.p.setup_read(file_path, memtype=choice, format=format) return True def write_checklist(self): """ Asks about types of memory that you'd like to write. When writing a bootloader, it adds the fuses to avrdude CLI parameters as text. When writing a .hex file or a backup created by read_checklist(), checks if there are additional fuse files present and adds them to avrdude CLI parameters. """ if self.write_fuse_params: # Text fuse parameters detected, formatting them properly memory_params = [["flash", self.write_file, self.flash_format]] memory_params += copy(self.write_fuse_params) else: # No text fuse parameters found, looking for files memory_params = self.autodetect_memory_files(self.write_file) if memory_params: # If there are additional fuse files/settings present, # we give the user a chance to opt-out choices = [[type.capitalize(), type] for type, _, _ in memory_params] # Adding the "Flash" choice in front, as we certainly have that # but the user might want to only flash fuses #choices = [["Flash", "flash"]] + choices choices = Checkbox( choices, self.i, self.o, default_state=True, name="Avrdude write memory selection checkbox").activate() if not choices: # user exited the listbox return False # they likely don't want to continue for type, value, format in memory_params: if choices.get(type, False): # type selected self.p.setup_write(value, memtype=type, format=format) else: # No fuse files/settings found # Add the flash file and proceed without a checkbox, no fuses self.p.setup_write(self.write_file, memtype="flash", format=self.flash_format) return True def erase_checklist(self): """ Asks user if they really want to erase the chip. """ answer = DialogBox( 'yn', self.i, self.o, message="Are you sure?", name="Avrdude app erase verify dialogbox").activate() if answer: self.p.setup_erase() return answer # 5. "Chip detection" screen functions ## Status image functions def detect_and_run(self): """ Runs the chip detection loop which stops once the chip is detected (or user exits). If a chip is detected, proceeds to launch the avrdude process. """ Printer("Detecting...", self.i, self.o, 0) self.detect_loop() # Refresher has exited by now - either because of RefresherExitException # or because the user pressed LEFT # if a chip is found, it's likely the former status = self.get_status() if not heuristics.chip_is_found(status): return # Likely a LEFT press hrs = heuristics.get_human_readable_status(status) self.display_status(hrs) self.run_and_monitor_process() def detect_loop(self): """ The detect loop. Will exit on RefresherExitException (raised by ``show_chip_status`` or on KEY_LEFT from the user. """ r = Refresher(self.get_current_status_data, self.i, self.o, name="Avrdude chip detect loop") r.activate() def get_current_status_data(self): """ A callback for the status Refresher. In future, if compatibility with text-based screens is needed, can fall back to ``show_chip_status()``-provided data. """ status = self.show_chip_status() return graphics.make_image_from_status(self.o, status, success_message="Found chip!") def display_status(self, hrs, success_message="Found chip!", delay=1): """ Simply shows the provided status on the screen using a GraphicsPrinter. """ image = graphics.make_image_from_status( self.o, hrs, success_message=success_message) GraphicsPrinter(image, self.i, self.o, delay, invert=False) ## Status text functions def show_chip_status(self): """ A callback for the Refresher to get human-readable status data. Raises the RefresherExitException once a chip is found; is context-aware and will not poll status (thereby polling the hardware) when the app is not the one active. """ if self.context.is_active(): status = self.get_status() readable_status = heuristics.get_human_readable_status(status) if heuristics.chip_is_found(status): raise RefresherExitException return readable_status else: return ["Not probing", "App inactive"] def get_status(self): """ Simply gets the status (non-human-readable) from the pyavrdude library. """ return pyavrdude.detect_chip(self.current_chip, self.current_programmer, *self.get_avrdude_parameters()) # 6. Avrdude process monitor state machine def run_and_monitor_process(self): """ Starts and monitors the avrdude process; updating the user on its status. Activates/deactivates and updates loading indicators, then shows a status image once process is completed. In the future, will also allow sending bugreports about yet-unknown failures during the process. """ self.p.run() previous_s = {"status": "not received yet"} while previous_s['status'] not in ["success", "failure"]: self.p.poll() s = self.p.get_interactive_status() if s != previous_s: # Status changed, updating if s["status"] == "started": self.read_write_bar.pause() self.erase_restore_indicator.background_if_inactive() self.erase_restore_indicator.message = "Started" elif s["status"] == "in progress": if s["operation"] in ["reading", "writing", "verifying"]: self.erase_restore_indicator.pause() self.read_write_bar.background_if_inactive() self.read_write_bar.message = s[ "operation"].capitalize() self.read_write_bar.message += " " + s["time"] self.read_write_bar.progress = s["progress"] elif s["operation"] in ["erasing", "restoring fuses"]: self.read_write_bar.pause() self.erase_restore_indicator.message = s[ "operation"].capitalize() elif s["status"] in ["success", "failure"]: pass # Is going to fail/succeed anyway once it goes through all the lines previous_s = s # Process is over, showing the result self.read_write_bar.stop() self.erase_restore_indicator.stop() status = self.p.get_status() hrs = heuristics.get_human_readable_status(status) self.display_status(hrs, success_message="Done!", delay=1) #if hrs == ['Failure', 'Unknown error']: # # Unknown error # # Once bugreport library is ready, we can offer the user to send a bugreport # PathPickers and Inputs for editing read/write paths - also save config variables def set_write_file(self, dir): """ A function to set the writing file specifically. Calls ``self.set_file``, then resets the ``write_fuse_params`` if a file was chosen. """ if self.set_file(dir, "write_file", "last_write_file"): # A file was picked manually, resetting fuse write params self.write_fuse_params = [] self.config["last_write_fuse_params"] = self.write_fuse_params self.save_config() def set_file(self, dir, attr_name, config_option_name): """ A function for selecting files (specifically, a full path to an existing file. For now, is only used for "write_file". """ original_file = getattr(self, attr_name) dir, filename = os.path.split(original_file) # The original dir might not exist anymore, we might need to go through directories # until we find a working one while dir and not (os.path.exists(dir) and os.path.isdir(dir)): dir = os.path.split(dir)[0] if not dir: dir = '/' file = PathPicker(dir, self.i, self.o, file=filename).activate() if file and os.path.isfile(file): setattr(self, attr_name, file) self.config[config_option_name] = file self.save_config() return True def set_dir(self, attr_name, config_option_name): """ A convenience wrapper for setting directories. For now, is only used for "read_dir". """ original_dir = getattr(self, attr_name) dir = original_dir if original_dir else "/" dir = PathPicker(dir, self.i, self.o, dirs_only=True).activate() if dir: setattr(self, attr_name, dir) self.config[config_option_name] = dir self.save_config() return True def set_filename(self, attr_name, config_option_name, message="Filename:"): """ A convenience wrapper for setting filenames. For now, is only used for "read_filename". """ original_value = getattr(self, attr_name) value = original_value if original_value else "" filename = UniversalInput(self.i, self.o, message=message, value=value).activate() if filename: setattr(self, attr_name, filename) self.config[config_option_name] = filename self.save_config() return True # Various settings def pick_bootloader(self): """ A menu to pick the bootloader from bootloaders.json. Also records fuse information in self.write_fuse_params, where it stays until user selects another bootloader or manually selects a file by path. """ bootloader_dir = local_path("bootloaders/") config = read_config( os.path.join(bootloader_dir, self.bootloader_config_filename)) bootloader_choices = [[bootloader["name"], bootloader] for bootloader in config["bootloaders"]] if not bootloader_choices: PrettyPrinter("No bootloaders found!", self.i, self.o, 3) return choice = Listbox(bootloader_choices, self.i, self.o, name="Avrdude bootloader picker").activate() if choice: self.write_file = os.path.join(bootloader_dir, choice["file"]) self.write_fuse_params = [] for type in self.fuse_types: if type in choice: self.write_fuse_params.append( [type, choice[type], config["fuse_format"]]) self.config["last_write_file"] = self.write_file self.config["last_write_fuse_params"] = self.write_fuse_params self.save_config() def pick_chip(self): """ A menu to pick the chip from a list of available chips provided by avrdude. """ chips = pyavrdude.get_parts() lc = [[name, alias] for alias, name in chips.items()] choice = Listbox(lc, self.i, self.o, "Avrdude part picker listbox", selected=self.current_chip).activate() if choice: self.current_chip = choice self.config["default_part"] = self.current_chip self.save_config() def pick_programmer(self): """ A menu to pick the programmer from a list of available programmers provided by avrdude. """ programmers = pyavrdude.get_programmers() if self.filter_programmers: programmers = self.get_filtered_programmers(programmers) lc = [[name, alias] for alias, name in programmers.items()] choice = Listbox(lc, self.i, self.o, "Avrdude programmer picker listbox", selected=self.current_programmer).activate() if choice: self.current_programmer = choice self.config["default_programmer"] = self.current_programmer self.save_config() def set_bitclock(self): """ Allows adjustments of avrdude bitclock, allowing to work with slowly-clocked microcontrollers. """ bitclock = IntegerAdjustInput(self.bitclock, self.i, self.o, message="Bitclock:", name="Avrdude app bitclock adjust", min=1, max=50).activate() if bitclock: self.bitclock = bitclock self.config["last_bitclock"] = bitclock self.save_config() def toggle_filter_programmers(self): """ Toggles whether the programmers in the programmer list are filtered (for user convenience). """ self.filter_programmers = not self.filter_programmers self.config["filter_programmers"] = self.filter_programmers self.save_config() def settings_menu(self): def get_contents(): return [ ["Set bitclock (-B)", self.set_bitclock], #["Report last error", self.report_last_error], [ "Unfilter programmers" if self.filter_programmers else "Filter programmers", self.toggle_filter_programmers ] ] Menu([], self.i, self.o, contents_hook=get_contents, name="Avrdude app settings menu").activate() # User-friendliness def get_filtered_programmers(self, programmers): """ A filtering function to only allow selecting programmers from a whitelist. """ return { alias: name for alias, name in programmers.items() if alias in self.supported_programmers } def set_write_as_last_read(self): """ A convenience function to set the write file as the target for read functions (to allow for convenient copying of microcontrollers). """ self.write_file = os.path.join(self.read_dir, self.read_filename + ".hex") self.write_params = [] self.config["last_write_file"] = self.write_file self.config["last_write_fuse_params"] = self.write_fuse_params self.save_config() # Bugreport functions - TODO #def report_last_error(self): # #Describe the report procedure # #Ask for sending confirmation # self.send_bugreport() #def send_bugreport(self): # # use bugreport library # bugreport = BugReport() # if not hasattr(self, 'p') or not self.p: # PrettyPrinter("No process found - nothing to send!", self.i, self.o, 3) # return # status = self.p.get_status() # bugreport.add_text() # bugreport.send() # Fuse file detection/format functions for write_checklist def autodetect_memory_files(self, write_file): """ This function autodetects files that could've been written by app's read functions. They have the same filename as the write file, but a different extension (denoting their type). Returns [type, value, format] lists ready to be inserted into the avrdude commandline template. """ files = [["flash", write_file, self.flash_format]] write_dir, write_filename = os.path.split(write_file) base_filename = write_filename.rsplit('.', 1)[0] # Get all files from the write directory wp_files = [file for file in os.listdir(write_dir) \ if os.path.isfile(os.path.join(write_dir, file))] # Split filenames and extensions wp_files_exts = [file.rsplit('.', 1) for file in wp_files] # Get all the files where filename matches the base filename and extension is supported suitable_files = [file for file in wp_files_exts if file[0] == base_filename \ and file[-1] in self.fuse_types \ and len(file) == 2 ] # Get the extensions from that list available_exts = [file[-1] for file in suitable_files] # "available_exts" is a subset of "self.fuse_types" now # building [type, value, format] lists for type in available_exts: value = os.path.join(write_dir, base_filename + '.' + type) files.append([type, value, self.fuse_format]) return files
def update(self, suggest_restart=True, skip_steps=None): logger.info("Starting update process") pb = ProgressBar(i, o, message="Updating ZPUI") pb.run_in_background() progress_per_step = 100 / len(self.steps) skip_steps = skip_steps if skip_steps else [] completed_steps = [] try: for step in self.steps: if step in skip_steps: continue pb.set_message( self.progressbar_messages.get(step, "Loading...")) sleep(0.5) # The user needs some time to read the message self.run_step(step) completed_steps.append(step) pb.progress += progress_per_step except UpdateUnnecessary: logger.info("Update is unnecessary!") pb.stop() PrettyPrinter("ZPUI already up-to-date!", i, o, 2) return True except: # Name of the failed step is contained in `step` variable failed_step = step logger.exception("Failed on step {}".format(failed_step)) failed_message = self.failed_messages.get( failed_step, "Failed on step '{}'".format(failed_step)) with pb.paused: PrettyPrinter(failed_message, i, o, 2) pb.set_message("Reverting update") try: logger.info( "Reverting the failed step: {}".format(failed_step)) self.revert_step(failed_step) except: logger.exception( "Can't revert failed step {}".format(failed_step)) with pb.paused: PrettyPrinter("Can't revert failed step '{}'".format(step), i, o, 2) logger.info("Reverting the previous steps") for step in completed_steps: try: self.revert_step(step) except: logger.exception( "Failed to revert step {}".format(failed_step)) with pb.paused: PrettyPrinter( "Failed to revert step '{}'".format(step), i, o, 2) pb.progress -= progress_per_step sleep( 1 ) # Needed here so that 1) the progressbar goes to 0 2) run_in_background launches the thread before the final stop() call #TODO: add a way to pause the Refresher pb.stop() logger.info("Update failed") PrettyPrinter("Update failed, try again later?", i, o, 3) return False else: logger.info("Update successful!") sleep(0.5) # showing the completed progressbar pb.stop() PrettyPrinter("Update successful!", i, o, 3) if suggest_restart: self.suggest_restart() return True