Esempio n. 1
0
class UpdateView(HasTraits):
    piksi_hw_rev = String('piksi_multi')
    is_v2 = Bool(False)

    piksi_stm_vers = String('Waiting for Piksi to send settings...',
                            width=COLUMN_WIDTH)
    newest_stm_vers = String('Downloading Latest Firmware info...')
    piksi_nap_vers = String('Waiting for Piksi to send settings...')
    newest_nap_vers = String('Downloading Latest Firmware info...')
    local_console_vers = String('v' + CONSOLE_VERSION)
    newest_console_vers = String('Downloading Latest Console info...')

    erase_stm = Bool(True)
    erase_en = Bool(True)

    update_stm_firmware = Button(label='Update FW')
    update_nap_firmware = Button(label='Update NAP')
    update_full_firmware = Button(label='Update Piksi STM and NAP Firmware')

    updating = Bool(False)
    update_stm_en = Bool(False)
    update_nap_en = Bool(False)
    update_en = Bool(False)
    serial_upgrade = Bool(False)
    upgrade_steps = String("Firmware upgrade steps:")

    download_firmware = Button(label='Download Latest Firmware')
    download_directory = Directory(
        "  Please choose a directory for downloaded firmware files...")
    download_stm = Button(label='Download', height=HT)
    download_nap = Button(label='Download', height=HT)
    downloading = Bool(False)
    download_fw_en = Bool(True)

    stm_fw = Instance(FirmwareFileDialog)
    nap_fw = Instance(FirmwareFileDialog)

    stream = Instance(OutputStream)

    view = View(
        VGroup(
            Item('piksi_hw_rev',
                 label='Hardware Revision',
                 editor_args={'enabled': False},
                 resizable=True),
            HGroup(
                VGroup(Item('piksi_stm_vers',
                            label='Current',
                            resizable=True,
                            editor_args={'enabled': False}),
                       Item('newest_stm_vers',
                            label='Latest',
                            resizable=True,
                            editor_args={
                                'enabled': False,
                                'readonly_allow_selection': True
                            }),
                       Item('stm_fw',
                            style='custom',
                            show_label=True,
                            label="Local File",
                            enabled_when='download_fw_en',
                            visible_when='serial_upgrade',
                            editor_args={'enabled': False}),
                       HGroup(
                           Item('update_stm_firmware',
                                show_label=False,
                                enabled_when='update_stm_en',
                                visible_when='serial_upgrade'),
                           Item('erase_stm',
                                label='Erase STM flash\n(recommended)',
                                enabled_when='erase_en',
                                show_label=True,
                                visible_when='is_v2')),
                       show_border=True,
                       label="Firmware Version"),
                VGroup(Item('piksi_nap_vers',
                            label='Current',
                            resizable=True,
                            editor_args={'enabled': False}),
                       Item('newest_nap_vers',
                            label='Latest',
                            resizable=True,
                            editor_args={'enabled': False}),
                       Item('nap_fw',
                            style='custom',
                            show_label=True,
                            label="Local File",
                            enabled_when='download_fw_en',
                            editor_args={'enabled': False}),
                       HGroup(
                           Item('update_nap_firmware',
                                show_label=False,
                                enabled_when='update_nap_en',
                                visible_when='serial_upgrade'),
                           Item(width=50, label="                  ")),
                       show_border=True,
                       label="NAP Version",
                       visible_when='is_v2'),
                VGroup(Item('local_console_vers',
                            label='Current',
                            resizable=True,
                            editor_args={'enabled': False}),
                       Item('newest_console_vers',
                            label='Latest',
                            editor_args={'enabled': False}),
                       label="Swift Console Version",
                       show_border=True),
            ), UItem('download_directory', enabled_when='download_fw_en'),
            UItem('download_firmware', enabled_when='download_fw_en'),
            UItem('update_full_firmware',
                  enabled_when='update_en',
                  visible_when='is_v2'),
            VGroup(
                UItem('upgrade_steps',
                      visible_when='not serial_upgrade',
                      style='readonly'),
                Item(
                    'stream',
                    style='custom',
                    editor=InstanceEditor(),
                    show_label=False,
                ),
                show_border=True,
            )))

    def __init__(self,
                 link,
                 download_dir=None,
                 prompt=True,
                 serial_upgrade=False):
        """
        Traits tab with UI for updating Piksi firmware.

        Parameters
        ----------
        link : sbp.client.handler.Handler
          Link for SBP transfer to/from Piksi.
        prompt : bool
          Prompt user to update console/firmware if out of date.
        """
        self.link = link
        self.settings = {}
        self.prompt = prompt
        self.python_console_cmds = {'update': self}
        try:
            self.update_dl = UpdateDownloader()
            if download_dir:
                self.update_dl.set_root_path(download_dir)
        except URLError:
            self.update_dl = None
        self.erase_en = True
        self.stm_fw = FirmwareFileDialog('bin')
        self.stm_fw.on_trait_change(self._manage_enables, 'status')
        self.nap_fw = FirmwareFileDialog('M25')
        self.nap_fw.on_trait_change(self._manage_enables, 'status')
        self.stream = OutputStream()
        self.serial_upgrade = serial_upgrade
        self.last_call_fw_version = None
        if not self.serial_upgrade:
            self._write(
                "1. Insert the USB flash drive provided with your Piki Multi into "
                "your computer.  Select the flash drive root directory as the "
                "firmware download destination using the \"Please "
                "choose a directory for downloaded firmware files\" directory "
                "chooser above.  Press the \"Download Latest Firmware\" button.  "
                "This will download the latest Piksi Multi firmware file onto the "
                "USB flashdrive.\n"
                "2. Eject the drive from your computer and plug it "
                "into the Piksi Multi evaluation board.\n"
                "3. Reset your Piksi Multi and it will upgrade to the version "
                "on the USB flash drive. This should take less than 5 minutes.\n"
                "4. When the upgrade completes you will be prompted to remove the "
                "USB flash drive and reset your Piksi Multi.\n"
                "5. Verify that the firmware version has upgraded via inspection "
                "of the Current Firmware Version box on the Firmware Update Tab "
                "of the Swift Console.\n")

    def _manage_enables(self):
        """ Manages whether traits widgets are enabled in the UI or not. """
        if self.updating or self.downloading:
            self.update_stm_en = False
            self.update_nap_en = False
            self.update_en = False
            self.download_fw_en = False
            self.erase_en = False
        else:
            self.download_fw_en = True
            self.erase_en = True
            if self.stm_fw.ihx is not None or self.stm_fw.blob is not None:
                self.update_stm_en = True
            else:
                self.update_stm_en = False
                self.update_en = False
            if self.nap_fw.ihx is not None:
                self.update_nap_en = True
            else:
                self.update_nap_en = False
                self.update_en = False
            if self.nap_fw.ihx is not None and self.stm_fw.ihx is not None:
                self.update_en = True

    def _download_directory_changed(self):
        if self.update_dl:
            self.update_dl.set_root_path(self.download_directory)

    def _updating_changed(self):
        """ Handles self.updating trait being changed. """
        self._manage_enables()

    def _downloading_changed(self):
        """ Handles self.downloading trait being changed. """
        self._manage_enables()

    def _write(self, text):
        """
        Stream style write function. Allows flashing debugging messages to be
        routed to embedded text console.

        Parameters
        ----------
        text : string
          Text to be written to screen.
        """
        self.stream.write(text)
        self.stream.write('\n')
        self.stream.flush()

    def _update_stm_firmware_fired(self):
        """
        Handle update_stm_firmware button. Starts thread so as not to block the GUI
        thread.
        """
        try:
            if self._firmware_update_thread.is_alive():
                return
        except AttributeError:
            pass

        self._firmware_update_thread = Thread(
            target=self.manage_firmware_updates, args=("STM", ))
        self._firmware_update_thread.start()

    def _update_nap_firmware_fired(self):
        """
        Handle update_nap_firmware button. Starts thread so as not to block the GUI
        thread.
        """
        try:
            if self._firmware_update_thread.is_alive():
                return
        except AttributeError:
            pass

        self._firmware_update_thread = Thread(
            target=self.manage_firmware_updates, args=("M25", ))
        self._firmware_update_thread.start()

    def _update_full_firmware_fired(self):
        """
        Handle update_full_firmware button. Starts thread so as not to block the GUI
        thread.
        """
        try:
            if self._firmware_update_thread.is_alive():
                return
        except AttributeError:
            pass

        self._firmware_update_thread = Thread(
            target=self.manage_firmware_updates, args=("ALL", ))
        self._firmware_update_thread.start()

    def _download_firmware(self):
        """ Download latest firmware from swiftnav.com. """
        self._write('')

        # Check that we received the index file from the website.
        if self.update_dl is None:
            self._write("Error: Can't download firmware files")
            return

        self.downloading = True
        status = 'Downloading Latest Firmware...'
        self.nap_fw.clear(status)
        self.stm_fw.clear(status)
        self._write(status)

        # Get firmware files from Swift Nav's website, save to disk, and load.
        if 'fw' in self.update_dl.index[self.piksi_hw_rev]:
            try:
                self._write('Downloading Latest Multi firmware')
                filepath = self.update_dl.download_multi_firmware(
                    self.piksi_hw_rev)
                self._write('Saved file to %s' % filepath)
                self.stm_fw.load_bin(filepath)
            except AttributeError:
                self.nap_fw.clear("Error downloading firmware")
                self._write(
                    "Error downloading firmware: index file not downloaded yet"
                )
            except IOError:
                self.nap_fw.clear(
                    "IOError: unable to write to path %s. "
                    "Verify that the path exists and is writable." %
                    self.download_directory)
                self._write("IOError: unable to write to path %s. "
                            "Verify that the path exists and is writable." %
                            self.download_directory)
            except KeyError:
                self.nap_fw.clear("Error downloading firmware")
                self._write(
                    "Error downloading firmware: URL not present in index")
            except URLError:
                self.nap_fw.clear("Error downloading firmware")
                self._write(
                    "Error: Failed to download latest NAP firmware from Swift Navigation's website"
                )
            self.downloading = False
            return

    def _download_firmware_fired(self):
        """
        Handle download_firmware button. Starts thread so as not to block the GUI
        thread.
        """
        try:
            if self._download_firmware_thread.is_alive():
                return
        except AttributeError:
            pass

        self._download_firmware_thread = Thread(target=self._download_firmware)
        self._download_firmware_thread.start()

    def compare_versions(self):
        """
        To be called after latest Piksi firmware info has been received from
        device, to decide if current firmware on Piksi is out of date. Also informs
        user if the firmware was successfully upgraded. Starts a thread so as not
        to block GUI thread.
        """
        try:
            if self._compare_versions_thread.is_alive():
                return
        except AttributeError:
            pass

        self._compare_versions_thread = Thread(target=self._compare_versions)
        self._compare_versions_thread.start()

    def _compare_versions(self):
        """
        Compares version info between received firmware version / current console
        and firmware / console info from website to decide if current firmware or
        console is out of date. Prompt user to update if so. Informs user if
        firmware successfully upgraded.
        """
        # Check that settings received from Piksi contain FW versions.
        try:
            self.piksi_hw_rev = \
                HW_REV_LOOKUP[self.settings['system_info']
                              ['hw_revision'].value]
            self.piksi_stm_vers = \
                self.settings['system_info']['firmware_version'].value
        except KeyError:
            self._write(
                "\nError: Settings received from Piksi don't contain firmware version keys. Please contact Swift Navigation.\n"
            )
            return

        self.is_v2 = self.piksi_hw_rev.startswith('piksi_v2')
        if self.is_v2:
            self.stm_fw.set_flash_type('STM')
            self.serial_upgrade = True
        else:
            self.stm_fw.set_flash_type('bin')

        self._get_latest_version_info()

        # Check that we received the index file from the website.
        if self.update_dl is None:
            self._write(
                "Error: No website index to use to compare versions with local firmware"
            )
            return
        # Get local stm version
        local_stm_version = None
        try:
            local_stm_version = self.settings['system_info'][
                'firmware_version'].value
        except:
            pass
        # Check if console is out of date and notify user if so.
        if self.prompt:
            local_console_version = parse_version(CONSOLE_VERSION)
            remote_console_version = parse_version(self.newest_console_vers)
            self.console_outdated = remote_console_version > local_console_version

            # we want to warn users using v2 regardless of version logic
            if self.console_outdated or self.is_v2:
                if not self.is_v2:
                    console_outdated_prompt = \
                        prompt.CallbackPrompt(
                            title="Swift Console Outdated",
                            actions=[prompt.close_button],
                        )
                    console_outdated_prompt.text = \
                        "Your console is out of date and may be incompatible\n" + \
                        "with current firmware. We highly recommend upgrading to\n" + \
                        "ensure proper behavior.\n\n" + \
                        "Please visit http://support.swiftnav.com to\n" + \
                        "download the latest version.\n\n" + \
                        "Local Console Version :\n\t" + \
                        "v" + CONSOLE_VERSION + \
                        "\nLatest Console Version :\n\t" + \
                        self.update_dl.index[self.piksi_hw_rev]['console']['version'] + "\n"
                else:
                    console_outdated_prompt = \
                        prompt.CallbackPrompt(
                            title="Swift Console Incompatible",
                            actions=[prompt.close_button],
                        )
                    console_outdated_prompt.text = \
                        "Your console is incompatible with your hardware revision.\n" + \
                        "We highly recommend using a compatible console version\n" + \
                        "to ensure proper behavior.\n\n" + \
                        "Please visit http://support.swiftnav.com to\n" + \
                        "download the latest compatible version.\n\n" + \
                        "Current Hardware revision :\n\t" + \
                        self.piksi_hw_rev + \
                        "\nLast supported Console Version: \n\t" + \
                        self.update_dl.index[self.piksi_hw_rev]['console']['version'] + "\n"

                console_outdated_prompt.run()

            # For timing aesthetics between windows popping up.
            sleep(0.5)

            # Check if firmware is out of date and notify user if so.
            remote_stm_version = self.newest_stm_vers

            self.fw_outdated = remote_stm_version > local_stm_version
            if local_stm_version.startswith('DEV'):
                self.fw_outdated = False

            if self.fw_outdated:
                fw_update_prompt = \
                    prompt.CallbackPrompt(
                        title='Firmware Update',
                        actions=[prompt.close_button]
                    )

                if 'fw' in self.update_dl.index[self.piksi_hw_rev]:
                    fw_update_prompt.text = \
                        "New Piksi firmware available.\n\n" + \
                        "Please use the Firmware Update tab to update.\n\n" + \
                        "Newest Firmware Version :\n\t%s\n\n" % \
                        self.update_dl.index[self.piksi_hw_rev]['fw']['version']
                else:
                    fw_update_prompt.text = \
                        "New Piksi firmware available.\n\n" + \
                        "Please use the Firmware Update tab to update.\n\n" + \
                        "Newest STM Version :\n\t%s\n\n" % \
                        self.update_dl.index[self.piksi_hw_rev]['stm_fw']['version'] + \
                        "Newest SwiftNAP Version :\n\t%s\n\n" % \
                        self.update_dl.index[self.piksi_hw_rev]['nap_fw']['version']

                fw_update_prompt.run()

        # Check if firmware successfully upgraded and notify user if so.
        if self.last_call_fw_version is not None and \
                self.last_call_fw_version != local_stm_version:
            fw_success_str = "Firmware successfully upgraded from %s to %s." % \
                             (self.last_call_fw_version, local_stm_version)
            print(fw_success_str)
            self._write(fw_success_str)

        # Record firmware version reported each time this callback is called.
        self.last_call_fw_version = local_stm_version

    def _get_latest_version_info(self):
        """ Get latest firmware / console version from website. """
        try:
            self.update_dl = UpdateDownloader()
        except URLError:
            self._write(
                "\nError: Failed to download latest file index from Swift Navigation's website. Please visit our website to check that you're running the latest Piksi firmware and Piksi console.\n"
            )
            return

        # Make sure index contains all keys we are interested in.
        try:
            if 'fw' in self.update_dl.index[self.piksi_hw_rev]:
                self.newest_stm_vers = self.update_dl.index[
                    self.piksi_hw_rev]['fw']['version']
            else:
                self.newest_stm_vers = self.update_dl.index[
                    self.piksi_hw_rev]['stm_fw']['version']
                self.newest_nap_vers = self.update_dl.index[
                    self.piksi_hw_rev]['nap_fw']['version']
            self.newest_console_vers = self.update_dl.index[
                self.piksi_hw_rev]['console']['version']
        except KeyError:
            self._write(
                "\nError: Index downloaded from Swift Navigation's website (%s) doesn't contain all keys. Please contact Swift Navigation.\n"
                % INDEX_URL)
            return

    def manage_stm_firmware_update(self):
        # Erase all of STM's flash (other than bootloader) if box is checked.
        if self.erase_stm:
            text = "Erasing STM"
            self._write(text)
            self.create_flash("STM")
            sectors_to_erase = set(range(self.pk_flash.n_sectors)).difference(
                set(self.pk_flash.restricted_sectors))
            progress_dialog = PulsableProgressDialog(len(sectors_to_erase),
                                                     False)
            progress_dialog.title = text
            GUI.invoke_later(progress_dialog.open)
            erase_count = 0
            for s in sorted(sectors_to_erase):
                progress_dialog.progress(erase_count)
                self._write('Erasing %s sector %d' %
                            (self.pk_flash.flash_type, s))
                self.pk_flash.erase_sector(s)
                erase_count += 1
            self.stop_flash()
            self._write("")
            try:
                progress_dialog.close()
            except AttributeError:
                pass
        # Flash STM.
        text = "Updating STM"
        self._write(text)
        self.create_flash("STM")
        stm_n_ops = self.pk_flash.ihx_n_ops(self.stm_fw.ihx,
                                            erase=not self.erase_stm)
        progress_dialog = PulsableProgressDialog(stm_n_ops, True)
        progress_dialog.title = text
        GUI.invoke_later(progress_dialog.open)
        # Don't erase sectors if we've already done so above.
        self.pk_flash.write_ihx(self.stm_fw.ihx,
                                self.stream,
                                mod_print=0x40,
                                elapsed_ops_cb=progress_dialog.progress,
                                erase=not self.erase_stm)
        self.stop_flash()
        self._write("")
        try:
            progress_dialog.close()
        except AttributeError:
            pass

    def manage_nap_firmware_update(self, check_version=False):
        # Flash NAP if out of date.
        try:
            local_nap_version = parse_version(
                self.settings['system_info']['nap_version'].value)
            remote_nap_version = parse_version(self.newest_nap_vers)
            nap_out_of_date = local_nap_version != remote_nap_version
        except KeyError:
            nap_out_of_date = True
        if nap_out_of_date or not check_version:
            text = "Updating NAP"
            self._write(text)
            self.create_flash("M25")
            nap_n_ops = self.pk_flash.ihx_n_ops(self.nap_fw.ihx)
            progress_dialog = PulsableProgressDialog(nap_n_ops, True)
            progress_dialog.title = text
            GUI.invoke_later(progress_dialog.open)
            self.pk_flash.write_ihx(self.nap_fw.ihx,
                                    self.stream,
                                    mod_print=0x40,
                                    elapsed_ops_cb=progress_dialog.progress)
            self.stop_flash()
            self._write("")
            try:
                progress_dialog.close()
            except AttributeError:
                pass
            return True
        else:
            text = "NAP is already to latest version, not updating!"
            self._write(text)
            self._write("")
            return False

    def manage_multi_firmware_update(self):
        # Set up progress dialog and transfer file to Piksi using SBP FileIO
        progress_dialog = PulsableProgressDialog(len(self.stm_fw.blob))
        progress_dialog.title = "Transferring image file"
        GUI.invoke_later(progress_dialog.open)
        self._write("Transferring image file...")
        try:
            FileIO(self.link).write("upgrade.image_set.bin",
                                    self.stm_fw.blob,
                                    progress_cb=progress_dialog.progress)
        except Exception as e:
            self._write("Failed to transfer image file to Piksi: %s\n" % e)
            progress_dialog.close()
            return
        try:
            progress_dialog.close()
        except AttributeError:
            pass

        # Setup up pulsed progress dialog and commit to flash
        progress_dialog = PulsableProgressDialog(100, True)
        progress_dialog.title = "Committing to flash"
        GUI.invoke_later(progress_dialog.open)
        self._write("Committing file to flash...")

        def log_cb(msg, **kwargs):
            self._write(msg.text)

        self.link.add_callback(log_cb, SBP_MSG_LOG)
        code = shell_command(self.link,
                             "upgrade_tool upgrade.image_set.bin",
                             600,
                             progress_cb=progress_dialog.progress)
        self.link.remove_callback(log_cb, SBP_MSG_LOG)
        progress_dialog.close()

        if code != 0:
            self._write('Failed to perform upgrade (code = %d)' % code)
            if code == -255:
                self._write('Shell command timed out.  Please try again.')
            return
        self._write('Resetting Piksi...')
        self.link(MsgReset(flags=0))

    # Executed in GUI thread, called from Handler.
    def manage_firmware_updates(self, device):
        """
        Update Piksi firmware. Erase entire STM flash (other than bootloader)
        if so directed. Flash NAP only if new firmware is available.
        """
        self.updating = True
        update_nap = False
        self._write('')
        if not self.is_v2:
            self.manage_multi_firmware_update()
            self.updating = False
            return
        elif device == "STM":
            self.manage_stm_firmware_update()
        elif device == "M25":
            update_nap = self.manage_nap_firmware_update()
        else:
            self.manage_stm_firmware_update()
            update_nap = self.manage_nap_firmware_update(check_version=True)

        # Must tell Piksi to jump to application after updating firmware.
        if device == "STM" or update_nap:
            self.link(MsgBootloaderJumpToApp(jump=0))
            self._write("Firmware update finished.")
            self._write("")

        self.updating = False

    def create_flash(self, flash_type):
        """
        Create flash.Flash instance and set Piksi into bootloader mode, prompting
        user to reset if necessary.

        Parameter
        ---------
        flash_type : string
          Either "STM" or "M25".
        """
        # Reset device if the application is running to put into bootloader mode.
        self.link(MsgReset(flags=0))

        self.pk_boot = bootload.Bootloader(self.link)

        self._write("Waiting for bootloader handshake message from Piksi ...")
        reset_prompt = None
        handshake_received = self.pk_boot.handshake(1)

        # Prompt user to reset Piksi if we don't receive the handshake message
        # within a reasonable amount of tiime (firmware might be corrupted).
        while not handshake_received:
            reset_prompt = \
                prompt.CallbackPrompt(
                    title="Please Reset Piksi",
                    actions=[prompt.close_button],
                )

            reset_prompt.text = \
                "You must press the reset button on your Piksi in order\n" + \
                "to update your firmware.\n\n" + \
                "Please press it now.\n\n"

            reset_prompt.run(block=False)

            while not reset_prompt.closed and not handshake_received:
                handshake_received = self.pk_boot.handshake(1)

            reset_prompt.kill()
            reset_prompt.wait()

        self._write("received bootloader handshake message.")
        self._write("Piksi Onboard Bootloader Version: " +
                    self.pk_boot.version)

        self.pk_flash = flash.Flash(self.link, flash_type,
                                    self.pk_boot.sbp_version)

    def stop_flash(self):
        """
        Stop Flash and Bootloader instances (removes callback from SerialLink).
        """
        self.pk_flash.stop()
        self.pk_boot.stop()