diff --git a/plugins/FirmwareUpdateChecker/FirmwareUpdateChecker.py b/plugins/FirmwareUpdateChecker/FirmwareUpdateChecker.py index f01e8cb276..415931b7ec 100644 --- a/plugins/FirmwareUpdateChecker/FirmwareUpdateChecker.py +++ b/plugins/FirmwareUpdateChecker/FirmwareUpdateChecker.py @@ -1,9 +1,12 @@ -# Copyright (c) 2017 Ultimaker B.V. +# Copyright (c) 2018 Ultimaker B.V. # Cura is released under the terms of the LGPLv3 or higher. +import os from PyQt5.QtCore import QUrl from PyQt5.QtGui import QDesktopServices +from typing import Set + from UM.Extension import Extension from UM.Application import Application from UM.Logger import Logger @@ -13,6 +16,7 @@ from UM.Settings.ContainerRegistry import ContainerRegistry from cura.Settings.GlobalStack import GlobalStack from .FirmwareUpdateCheckerJob import FirmwareUpdateCheckerJob +from .FirmwareUpdateCheckerMessage import FirmwareUpdateCheckerMessage i18n_catalog = i18nCatalog("cura") @@ -21,32 +25,31 @@ i18n_catalog = i18nCatalog("cura") # The plugin is currently only usable for applications maintained by Ultimaker. But it should be relatively easy # to change it to work for other applications. class FirmwareUpdateChecker(Extension): - JEDI_VERSION_URL = "http://software.ultimaker.com/jedi/releases/latest.version?utm_source=cura&utm_medium=software&utm_campaign=resources" - def __init__(self): + def __init__(self) -> None: super().__init__() - # Initialize the Preference called `latest_checked_firmware` that stores the last version - # checked for the UM3. In the future if we need to check other printers' firmware - Application.getInstance().getPreferences().addPreference("info/latest_checked_firmware", "") - # Listen to a Signal that indicates a change in the list of printers, just if the user has enabled the - # 'check for updates' option + # "check for updates" option Application.getInstance().getPreferences().addPreference("info/automatic_update_check", True) if Application.getInstance().getPreferences().getValue("info/automatic_update_check"): ContainerRegistry.getInstance().containerAdded.connect(self._onContainerAdded) - self._download_url = None self._check_job = None + self._checked_printer_names = set() # type: Set[str] ## Callback for the message that is spawned when there is a new version. def _onActionTriggered(self, message, action): - if action == "download": - if self._download_url is not None: - QDesktopServices.openUrl(QUrl(self._download_url)) - - def _onSetDownloadUrl(self, download_url): - self._download_url = download_url + if action == FirmwareUpdateCheckerMessage.STR_ACTION_DOWNLOAD: + machine_id = message.getMachineId() + download_url = message.getDownloadUrl() + if download_url is not None: + if QDesktopServices.openUrl(QUrl(download_url)): + Logger.log("i", "Redirected browser to {0} to show newly available firmware.".format(download_url)) + else: + Logger.log("e", "Can't reach URL: {0}".format(download_url)) + else: + Logger.log("e", "Can't find URL for {0}".format(machine_id)) def _onContainerAdded(self, container): # Only take care when a new GlobalStack was added @@ -63,13 +66,18 @@ class FirmwareUpdateChecker(Extension): # \param silent type(boolean) Suppresses messages other than "new version found" messages. # This is used when checking for a new firmware version at startup. def checkFirmwareVersion(self, container = None, silent = False): - # Do not run multiple check jobs in parallel - if self._check_job is not None: - Logger.log("i", "A firmware update check is already running, do nothing.") + container_name = container.definition.getName() + if container_name in self._checked_printer_names: + return + self._checked_printer_names.add(container_name) + + metadata = container.definition.getMetaData().get("firmware_update_info") + if metadata is None: + Logger.log("i", "No machine with name {0} in list of firmware to check.".format(container_name)) return - self._check_job = FirmwareUpdateCheckerJob(container = container, silent = silent, url = self.JEDI_VERSION_URL, - callback = self._onActionTriggered, - set_download_url_callback = self._onSetDownloadUrl) + self._check_job = FirmwareUpdateCheckerJob(container = container, silent = silent, + machine_name = container_name, metadata = metadata, + callback = self._onActionTriggered) self._check_job.start() self._check_job.finished.connect(self._onJobFinished) diff --git a/plugins/FirmwareUpdateChecker/FirmwareUpdateCheckerJob.py b/plugins/FirmwareUpdateChecker/FirmwareUpdateCheckerJob.py index eadacf2c02..4c60b95824 100644 --- a/plugins/FirmwareUpdateChecker/FirmwareUpdateCheckerJob.py +++ b/plugins/FirmwareUpdateChecker/FirmwareUpdateCheckerJob.py @@ -1,13 +1,18 @@ -# Copyright (c) 2017 Ultimaker B.V. +# Copyright (c) 2018 Ultimaker B.V. # Cura is released under the terms of the LGPLv3 or higher. from UM.Application import Application from UM.Message import Message from UM.Logger import Logger from UM.Job import Job +from UM.Version import Version import urllib.request -import codecs +from urllib.error import URLError +from typing import Dict, Optional + +from .FirmwareUpdateCheckerLookup import FirmwareUpdateCheckerLookup, getSettingsKeyForMachine +from .FirmwareUpdateCheckerMessage import FirmwareUpdateCheckerMessage from UM.i18n import i18nCatalog i18n_catalog = i18nCatalog("cura") @@ -15,46 +20,86 @@ i18n_catalog = i18nCatalog("cura") ## This job checks if there is an update available on the provided URL. class FirmwareUpdateCheckerJob(Job): - def __init__(self, container = None, silent = False, url = None, callback = None, set_download_url_callback = None): + STRING_ZERO_VERSION = "0.0.0" + STRING_EPSILON_VERSION = "0.0.1" + ZERO_VERSION = Version(STRING_ZERO_VERSION) + EPSILON_VERSION = Version(STRING_EPSILON_VERSION) + + def __init__(self, container, silent, machine_name, metadata, callback) -> None: super().__init__() self._container = container self.silent = silent - self._url = url self._callback = callback - self._set_download_url_callback = set_download_url_callback - def run(self): - if not self._url: - Logger.log("e", "Can not check for a new release. URL not set!") - return + self._machine_name = machine_name + self._metadata = metadata + self._lookups = None # type:Optional[FirmwareUpdateCheckerLookup] + self._headers = {} # type:Dict[str, str] # Don't set headers yet. + + def getUrlResponse(self, url: str) -> str: + result = self.STRING_ZERO_VERSION try: + request = urllib.request.Request(url, headers = self._headers) + response = urllib.request.urlopen(request) + result = response.read().decode("utf-8") + except URLError: + Logger.log("w", "Could not reach '{0}', if this URL is old, consider removal.".format(url)) + + return result + + def parseVersionResponse(self, response: str) -> Version: + raw_str = response.split("\n", 1)[0].rstrip() + return Version(raw_str) + + def getCurrentVersion(self) -> Version: + max_version = self.ZERO_VERSION + if self._lookups is None: + return max_version + + machine_urls = self._lookups.getCheckUrls() + if machine_urls is not None: + for url in machine_urls: + version = self.parseVersionResponse(self.getUrlResponse(url)) + if version > max_version: + max_version = version + + if max_version < self.EPSILON_VERSION: + Logger.log("w", "MachineID {0} not handled!".format(self._lookups.getMachineName())) + + return max_version + + def run(self): + if self._lookups is None: + self._lookups = FirmwareUpdateCheckerLookup(self._machine_name, self._metadata) + + try: + # Initialize a Preference that stores the last version checked for this printer. + Application.getInstance().getPreferences().addPreference( + getSettingsKeyForMachine(self._lookups.getMachineId()), "") + + # Get headers application_name = Application.getInstance().getApplicationName() - headers = {"User-Agent": "%s - %s" % (application_name, Application.getInstance().getVersion())} - request = urllib.request.Request(self._url, headers = headers) - current_version_file = urllib.request.urlopen(request) - reader = codecs.getreader("utf-8") + application_version = Application.getInstance().getVersion() + self._headers = {"User-Agent": "%s - %s" % (application_name, application_version)} # get machine name from the definition container machine_name = self._container.definition.getName() - machine_name_parts = machine_name.lower().split(" ") # If it is not None, then we compare between the checked_version and the current_version - # Now we just do that if the active printer is Ultimaker 3 or Ultimaker 3 Extended or any - # other Ultimaker 3 that will come in the future - if len(machine_name_parts) >= 2 and machine_name_parts[:2] == ["ultimaker", "3"]: - Logger.log("i", "You have a UM3 in printer list. Let's check the firmware!") + machine_id = self._lookups.getMachineId() + if machine_id is not None: + Logger.log("i", "You have a(n) {0} in the printer list. Let's check the firmware!".format(machine_name)) - # Nothing to parse, just get the string - # TODO: In the future may be done by parsing a JSON file with diferent version for each printer model - current_version = reader(current_version_file).readline().rstrip() + current_version = self.getCurrentVersion() - # If it is the first time the version is checked, the checked_version is '' - checked_version = Application.getInstance().getPreferences().getValue("info/latest_checked_firmware") + # If it is the first time the version is checked, the checked_version is "" + setting_key_str = getSettingsKeyForMachine(machine_id) + checked_version = Version(Application.getInstance().getPreferences().getValue(setting_key_str)) - # If the checked_version is '', it's because is the first time we check firmware and in this case + # If the checked_version is "", it's because is the first time we check firmware and in this case # we will not show the notification, but we will store it for the next time - Application.getInstance().getPreferences().setValue("info/latest_checked_firmware", current_version) + Application.getInstance().getPreferences().setValue(setting_key_str, current_version) Logger.log("i", "Reading firmware version of %s: checked = %s - latest = %s", machine_name, checked_version, current_version) # The first time we want to store the current version, the notification will not be shown, @@ -62,28 +107,11 @@ class FirmwareUpdateCheckerJob(Job): # notify the user when no new firmware version is available. if (checked_version != "") and (checked_version != current_version): Logger.log("i", "SHOWING FIRMWARE UPDATE MESSAGE") - - message = Message(i18n_catalog.i18nc( - "@info Don't translate {machine_name}, since it gets replaced by a printer name!", - "New features are available for your {machine_name}! It is recommended to update the firmware on your printer.").format( - machine_name=machine_name), - title=i18n_catalog.i18nc( - "@info:title The %s gets replaced with the printer name.", - "New %s firmware available") % machine_name) - - message.addAction("download", - i18n_catalog.i18nc("@action:button", "How to update"), - "[no_icon]", - "[no_description]", - button_style=Message.ActionButtonStyle.LINK, - button_align=Message.ActionButtonStyle.BUTTON_ALIGN_LEFT) - - - # If we do this in a cool way, the download url should be available in the JSON file - if self._set_download_url_callback: - self._set_download_url_callback("https://ultimaker.com/en/resources/20500-upgrade-firmware") + message = FirmwareUpdateCheckerMessage(machine_id, machine_name, self._lookups.getRedirectUserUrl()) message.actionTriggered.connect(self._callback) message.show() + else: + Logger.log("i", "No machine with name {0} in list of firmware to check.".format(machine_name)) except Exception as e: Logger.log("w", "Failed to check for new version: %s", e) diff --git a/plugins/FirmwareUpdateChecker/FirmwareUpdateCheckerLookup.py b/plugins/FirmwareUpdateChecker/FirmwareUpdateCheckerLookup.py new file mode 100644 index 0000000000..a21ad3f0e5 --- /dev/null +++ b/plugins/FirmwareUpdateChecker/FirmwareUpdateCheckerLookup.py @@ -0,0 +1,35 @@ +# Copyright (c) 2018 Ultimaker B.V. +# Cura is released under the terms of the LGPLv3 or higher. + +from typing import List, Optional + +from UM.i18n import i18nCatalog +i18n_catalog = i18nCatalog("cura") + + +def getSettingsKeyForMachine(machine_id: int) -> str: + return "info/latest_checked_firmware_for_{0}".format(machine_id) + + +class FirmwareUpdateCheckerLookup: + + def __init__(self, machine_name, machine_json) -> None: + # Parse all the needed lookup-tables from the ".json" file(s) in the resources folder. + self._machine_id = machine_json.get("id") + self._machine_name = machine_name.lower() # Lower in-case upper-case chars are added to the original json. + self._check_urls = [] # type:List[str] + for check_url in machine_json.get("check_urls"): + self._check_urls.append(check_url) + self._redirect_user = machine_json.get("update_url") + + def getMachineId(self) -> Optional[int]: + return self._machine_id + + def getMachineName(self) -> Optional[int]: + return self._machine_name + + def getCheckUrls(self) -> Optional[List[str]]: + return self._check_urls + + def getRedirectUserUrl(self) -> Optional[str]: + return self._redirect_user diff --git a/plugins/FirmwareUpdateChecker/FirmwareUpdateCheckerMessage.py b/plugins/FirmwareUpdateChecker/FirmwareUpdateCheckerMessage.py new file mode 100644 index 0000000000..fd56c101a0 --- /dev/null +++ b/plugins/FirmwareUpdateChecker/FirmwareUpdateCheckerMessage.py @@ -0,0 +1,37 @@ +# Copyright (c) 2018 Ultimaker B.V. +# Cura is released under the terms of the LGPLv3 or higher. + +from UM.i18n import i18nCatalog +from UM.Message import Message + +i18n_catalog = i18nCatalog("cura") + + +# Make a separate class, since we need an extra field: The machine-id that this message is about. +class FirmwareUpdateCheckerMessage(Message): + STR_ACTION_DOWNLOAD = "download" + + def __init__(self, machine_id: int, machine_name: str, download_url: str) -> None: + super().__init__(i18n_catalog.i18nc( + "@info Don't translate {machine_name}, since it gets replaced by a printer name!", + "New features are available for your {machine_name}! It is recommended to update the firmware on your printer.").format( + machine_name = machine_name), + title = i18n_catalog.i18nc( + "@info:title The %s gets replaced with the printer name.", + "New %s firmware available") % machine_name) + + self._machine_id = machine_id + self._download_url = download_url + + self.addAction(self.STR_ACTION_DOWNLOAD, + i18n_catalog.i18nc("@action:button", "How to update"), + "[no_icon]", + "[no_description]", + button_style = Message.ActionButtonStyle.LINK, + button_align = Message.ActionButtonStyle.BUTTON_ALIGN_LEFT) + + def getMachineId(self) -> int: + return self._machine_id + + def getDownloadUrl(self) -> str: + return self._download_url diff --git a/resources/definitions/ultimaker3.def.json b/resources/definitions/ultimaker3.def.json index b1daa6b780..72756de2a5 100644 --- a/resources/definitions/ultimaker3.def.json +++ b/resources/definitions/ultimaker3.def.json @@ -24,7 +24,16 @@ }, "first_start_actions": [ "DiscoverUM3Action" ], "supported_actions": [ "DiscoverUM3Action" ], - "supports_usb_connection": false + "supports_usb_connection": false, + "firmware_update_info": { + "id": 9066, + "check_urls": + [ + "http://software.ultimaker.com/jedi/releases/latest.version?utm_source=cura&utm_medium=software&utm_campaign=resources", + "http://software.ultimaker.com/releases/firmware/9066/stable/version.txt" + ], + "update_url": "https://ultimaker.com/firmware" + } }, diff --git a/resources/definitions/ultimaker3_extended.def.json b/resources/definitions/ultimaker3_extended.def.json index eb3cda9320..68f26969b7 100644 --- a/resources/definitions/ultimaker3_extended.def.json +++ b/resources/definitions/ultimaker3_extended.def.json @@ -23,7 +23,16 @@ "1": "ultimaker3_extended_extruder_right" }, "first_start_actions": [ "DiscoverUM3Action" ], - "supported_actions": [ "DiscoverUM3Action" ] + "supported_actions": [ "DiscoverUM3Action" ], + "firmware_update_info": { + "id": 9511, + "check_urls": + [ + "http://software.ultimaker.com/jedi/releases/latest.version?utm_source=cura&utm_medium=software&utm_campaign=resources", + "http://software.ultimaker.com/releases/firmware/9511/stable/version.txt" + ], + "update_url": "https://ultimaker.com/firmware" + } }, "overrides": { diff --git a/resources/definitions/ultimaker_s5.def.json b/resources/definitions/ultimaker_s5.def.json index 2e634787af..310765dbc3 100644 --- a/resources/definitions/ultimaker_s5.def.json +++ b/resources/definitions/ultimaker_s5.def.json @@ -30,7 +30,12 @@ "first_start_actions": [ "DiscoverUM3Action" ], "supported_actions": [ "DiscoverUM3Action" ], "supports_usb_connection": false, - "weight": -1 + "weight": -1, + "firmware_update_info": { + "id": 9051, + "check_urls": ["http://software.ultimaker.com/releases/firmware/9051/stable/version.txt"], + "update_url": "https://ultimaker.com/firmware" + } }, "overrides": {