mirror of
https://git.mirrors.martin98.com/https://github.com/Ultimaker/Cura
synced 2025-06-04 11:14:21 +08:00
Put the firmware-update meta-data in the 'normal' printer definitions and make the code handle that.
This commit is contained in:
parent
931143ceaa
commit
2e3abbc904
@ -10,15 +10,12 @@ from typing import Set
|
|||||||
from UM.Extension import Extension
|
from UM.Extension import Extension
|
||||||
from UM.Application import Application
|
from UM.Application import Application
|
||||||
from UM.Logger import Logger
|
from UM.Logger import Logger
|
||||||
from UM.PluginRegistry import PluginRegistry
|
|
||||||
from UM.Qt.QtApplication import QtApplication
|
|
||||||
from UM.i18n import i18nCatalog
|
from UM.i18n import i18nCatalog
|
||||||
from UM.Settings.ContainerRegistry import ContainerRegistry
|
from UM.Settings.ContainerRegistry import ContainerRegistry
|
||||||
|
|
||||||
from cura.Settings.GlobalStack import GlobalStack
|
from cura.Settings.GlobalStack import GlobalStack
|
||||||
|
|
||||||
from .FirmwareUpdateCheckerJob import FirmwareUpdateCheckerJob
|
from .FirmwareUpdateCheckerJob import FirmwareUpdateCheckerJob
|
||||||
from .FirmwareUpdateCheckerLookup import FirmwareUpdateCheckerLookup, getSettingsKeyForMachine
|
|
||||||
from .FirmwareUpdateCheckerMessage import FirmwareUpdateCheckerMessage
|
from .FirmwareUpdateCheckerMessage import FirmwareUpdateCheckerMessage
|
||||||
|
|
||||||
i18n_catalog = i18nCatalog("cura")
|
i18n_catalog = i18nCatalog("cura")
|
||||||
@ -38,18 +35,14 @@ class FirmwareUpdateChecker(Extension):
|
|||||||
if Application.getInstance().getPreferences().getValue("info/automatic_update_check"):
|
if Application.getInstance().getPreferences().getValue("info/automatic_update_check"):
|
||||||
ContainerRegistry.getInstance().containerAdded.connect(self._onContainerAdded)
|
ContainerRegistry.getInstance().containerAdded.connect(self._onContainerAdded)
|
||||||
|
|
||||||
# Partly initialize after creation, since we need our own path from the plugin-manager.
|
|
||||||
self._download_url = None
|
|
||||||
self._check_job = None
|
self._check_job = None
|
||||||
self._checked_printer_names = set() # type: Set[str]
|
self._checked_printer_names = set() # type: Set[str]
|
||||||
self._lookups = None
|
|
||||||
QtApplication.pluginsLoaded.connect(self._onPluginsLoaded)
|
|
||||||
|
|
||||||
## Callback for the message that is spawned when there is a new version.
|
## Callback for the message that is spawned when there is a new version.
|
||||||
def _onActionTriggered(self, message, action):
|
def _onActionTriggered(self, message, action):
|
||||||
if action == FirmwareUpdateCheckerMessage.STR_ACTION_DOWNLOAD:
|
if action == FirmwareUpdateCheckerMessage.STR_ACTION_DOWNLOAD:
|
||||||
machine_id = message.getMachineId()
|
machine_id = message.getMachineId()
|
||||||
download_url = self._lookups.getRedirectUserFor(machine_id)
|
download_url = message.getDownloadUrl()
|
||||||
if download_url is not None:
|
if download_url is not None:
|
||||||
if QDesktopServices.openUrl(QUrl(download_url)):
|
if QDesktopServices.openUrl(QUrl(download_url)):
|
||||||
Logger.log("i", "Redirected browser to {0} to show newly available firmware.".format(download_url))
|
Logger.log("i", "Redirected browser to {0} to show newly available firmware.".format(download_url))
|
||||||
@ -66,18 +59,6 @@ class FirmwareUpdateChecker(Extension):
|
|||||||
def _onJobFinished(self, *args, **kwargs):
|
def _onJobFinished(self, *args, **kwargs):
|
||||||
self._check_job = None
|
self._check_job = None
|
||||||
|
|
||||||
def _onPluginsLoaded(self):
|
|
||||||
if self._lookups is not None:
|
|
||||||
return
|
|
||||||
|
|
||||||
self._lookups = FirmwareUpdateCheckerLookup(os.path.join(PluginRegistry.getInstance().getPluginPath(
|
|
||||||
"FirmwareUpdateChecker"), "resources/machines.json"))
|
|
||||||
|
|
||||||
# Initialize the Preference called `latest_checked_firmware` that stores the last version
|
|
||||||
# checked for each printer.
|
|
||||||
for machine_id in self._lookups.getMachineIds():
|
|
||||||
Application.getInstance().getPreferences().addPreference(getSettingsKeyForMachine(machine_id), "")
|
|
||||||
|
|
||||||
## Connect with software.ultimaker.com, load latest.version and check version info.
|
## Connect with software.ultimaker.com, load latest.version and check version info.
|
||||||
# If the version info is different from the current version, spawn a message to
|
# If the version info is different from the current version, spawn a message to
|
||||||
# allow the user to download it.
|
# allow the user to download it.
|
||||||
@ -85,16 +66,18 @@ class FirmwareUpdateChecker(Extension):
|
|||||||
# \param silent type(boolean) Suppresses messages other than "new version found" messages.
|
# \param silent type(boolean) Suppresses messages other than "new version found" messages.
|
||||||
# This is used when checking for a new firmware version at startup.
|
# This is used when checking for a new firmware version at startup.
|
||||||
def checkFirmwareVersion(self, container = None, silent = False):
|
def checkFirmwareVersion(self, container = None, silent = False):
|
||||||
if self._lookups is None:
|
|
||||||
self._onPluginsLoaded()
|
|
||||||
|
|
||||||
container_name = container.definition.getName()
|
container_name = container.definition.getName()
|
||||||
if container_name in self._checked_printer_names:
|
if container_name in self._checked_printer_names:
|
||||||
return
|
return
|
||||||
self._checked_printer_names.add(container_name)
|
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,
|
self._check_job = FirmwareUpdateCheckerJob(container = container, silent = silent,
|
||||||
lookups = self._lookups,
|
machine_name = container_name, metadata = metadata,
|
||||||
callback = self._onActionTriggered)
|
callback = self._onActionTriggered)
|
||||||
self._check_job.start()
|
self._check_job.start()
|
||||||
self._check_job.finished.connect(self._onJobFinished)
|
self._check_job.finished.connect(self._onJobFinished)
|
||||||
|
@ -9,7 +9,7 @@ from UM.Version import Version
|
|||||||
|
|
||||||
import urllib.request
|
import urllib.request
|
||||||
from urllib.error import URLError
|
from urllib.error import URLError
|
||||||
from typing import Dict
|
from typing import Dict, Optional
|
||||||
|
|
||||||
from .FirmwareUpdateCheckerLookup import FirmwareUpdateCheckerLookup, getSettingsKeyForMachine
|
from .FirmwareUpdateCheckerLookup import FirmwareUpdateCheckerLookup, getSettingsKeyForMachine
|
||||||
from .FirmwareUpdateCheckerMessage import FirmwareUpdateCheckerMessage
|
from .FirmwareUpdateCheckerMessage import FirmwareUpdateCheckerMessage
|
||||||
@ -25,13 +25,15 @@ class FirmwareUpdateCheckerJob(Job):
|
|||||||
ZERO_VERSION = Version(STRING_ZERO_VERSION)
|
ZERO_VERSION = Version(STRING_ZERO_VERSION)
|
||||||
EPSILON_VERSION = Version(STRING_EPSILON_VERSION)
|
EPSILON_VERSION = Version(STRING_EPSILON_VERSION)
|
||||||
|
|
||||||
def __init__(self, container, silent, lookups: FirmwareUpdateCheckerLookup, callback) -> None:
|
def __init__(self, container, silent, machine_name, metadata, callback) -> None:
|
||||||
super().__init__()
|
super().__init__()
|
||||||
self._container = container
|
self._container = container
|
||||||
self.silent = silent
|
self.silent = silent
|
||||||
self._callback = callback
|
self._callback = callback
|
||||||
|
|
||||||
self._lookups = lookups
|
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.
|
self._headers = {} # type:Dict[str, str] # Don't set headers yet.
|
||||||
|
|
||||||
def getUrlResponse(self, url: str) -> str:
|
def getUrlResponse(self, url: str) -> str:
|
||||||
@ -50,10 +52,12 @@ class FirmwareUpdateCheckerJob(Job):
|
|||||||
raw_str = response.split("\n", 1)[0].rstrip()
|
raw_str = response.split("\n", 1)[0].rstrip()
|
||||||
return Version(raw_str)
|
return Version(raw_str)
|
||||||
|
|
||||||
def getCurrentVersionForMachine(self, machine_id: int) -> Version:
|
def getCurrentVersion(self) -> Version:
|
||||||
max_version = self.ZERO_VERSION
|
max_version = self.ZERO_VERSION
|
||||||
|
if self._lookups is None:
|
||||||
|
return max_version
|
||||||
|
|
||||||
machine_urls = self._lookups.getCheckUrlsFor(machine_id)
|
machine_urls = self._lookups.getCheckUrls()
|
||||||
if machine_urls is not None:
|
if machine_urls is not None:
|
||||||
for url in machine_urls:
|
for url in machine_urls:
|
||||||
version = self.parseVersionResponse(self.getUrlResponse(url))
|
version = self.parseVersionResponse(self.getUrlResponse(url))
|
||||||
@ -61,16 +65,20 @@ class FirmwareUpdateCheckerJob(Job):
|
|||||||
max_version = version
|
max_version = version
|
||||||
|
|
||||||
if max_version < self.EPSILON_VERSION:
|
if max_version < self.EPSILON_VERSION:
|
||||||
Logger.log("w", "MachineID {0} not handled!".format(repr(machine_id)))
|
Logger.log("w", "MachineID {0} not handled!".format(self._lookups.getMachineName()))
|
||||||
|
|
||||||
return max_version
|
return max_version
|
||||||
|
|
||||||
def run(self):
|
def run(self):
|
||||||
if self._lookups is None:
|
if self._lookups is None:
|
||||||
Logger.log("e", "Can not check for a new release. URL not set!")
|
self._lookups = FirmwareUpdateCheckerLookup(self._machine_name, self._metadata)
|
||||||
return
|
|
||||||
|
|
||||||
try:
|
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()
|
application_name = Application.getInstance().getApplicationName()
|
||||||
application_version = Application.getInstance().getVersion()
|
application_version = Application.getInstance().getVersion()
|
||||||
self._headers = {"User-Agent": "%s - %s" % (application_name, application_version)}
|
self._headers = {"User-Agent": "%s - %s" % (application_name, application_version)}
|
||||||
@ -79,11 +87,11 @@ class FirmwareUpdateCheckerJob(Job):
|
|||||||
machine_name = self._container.definition.getName()
|
machine_name = self._container.definition.getName()
|
||||||
|
|
||||||
# If it is not None, then we compare between the checked_version and the current_version
|
# If it is not None, then we compare between the checked_version and the current_version
|
||||||
machine_id = self._lookups.getMachineByName(machine_name.lower())
|
machine_id = self._lookups.getMachineId()
|
||||||
if machine_id is not None:
|
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))
|
Logger.log("i", "You have a(n) {0} in the printer list. Let's check the firmware!".format(machine_name))
|
||||||
|
|
||||||
current_version = self.getCurrentVersionForMachine(machine_id)
|
current_version = self.getCurrentVersion()
|
||||||
|
|
||||||
# If it is the first time the version is checked, the checked_version is ""
|
# If it is the first time the version is checked, the checked_version is ""
|
||||||
setting_key_str = getSettingsKeyForMachine(machine_id)
|
setting_key_str = getSettingsKeyForMachine(machine_id)
|
||||||
@ -99,7 +107,7 @@ class FirmwareUpdateCheckerJob(Job):
|
|||||||
# notify the user when no new firmware version is available.
|
# notify the user when no new firmware version is available.
|
||||||
if (checked_version != "") and (checked_version != current_version):
|
if (checked_version != "") and (checked_version != current_version):
|
||||||
Logger.log("i", "SHOWING FIRMWARE UPDATE MESSAGE")
|
Logger.log("i", "SHOWING FIRMWARE UPDATE MESSAGE")
|
||||||
message = FirmwareUpdateCheckerMessage(machine_id, machine_name)
|
message = FirmwareUpdateCheckerMessage(machine_id, machine_name, self._lookups.getRedirectUserUrl())
|
||||||
message.actionTriggered.connect(self._callback)
|
message.actionTriggered.connect(self._callback)
|
||||||
message.show()
|
message.show()
|
||||||
else:
|
else:
|
||||||
|
@ -1,11 +1,7 @@
|
|||||||
# Copyright (c) 2018 Ultimaker B.V.
|
# Copyright (c) 2018 Ultimaker B.V.
|
||||||
# Cura is released under the terms of the LGPLv3 or higher.
|
# Cura is released under the terms of the LGPLv3 or higher.
|
||||||
|
|
||||||
import json
|
from typing import List, Optional
|
||||||
|
|
||||||
from typing import Callable, Dict, List, Optional
|
|
||||||
|
|
||||||
from UM.Logger import Logger
|
|
||||||
|
|
||||||
from UM.i18n import i18nCatalog
|
from UM.i18n import i18nCatalog
|
||||||
i18n_catalog = i18nCatalog("cura")
|
i18n_catalog = i18nCatalog("cura")
|
||||||
@ -17,44 +13,23 @@ def getSettingsKeyForMachine(machine_id: int) -> str:
|
|||||||
|
|
||||||
class FirmwareUpdateCheckerLookup:
|
class FirmwareUpdateCheckerLookup:
|
||||||
|
|
||||||
def __init__(self, json_path) -> None:
|
def __init__(self, machine_name, machine_json) -> None:
|
||||||
# Open the .json file with the needed lookup-lists for each machine(/model) and retrieve "raw" json.
|
|
||||||
with open(json_path, "r", encoding = "utf-8") as json_file:
|
|
||||||
machines_json = json.load(json_file).get("machines")
|
|
||||||
if machines_json is None:
|
|
||||||
Logger.log("e", "Missing or inaccessible: {0}".format(json_path))
|
|
||||||
return
|
|
||||||
|
|
||||||
# Parse all the needed lookup-tables from the ".json" file(s) in the resources folder.
|
# Parse all the needed lookup-tables from the ".json" file(s) in the resources folder.
|
||||||
self._machine_ids = [] # type:List[int]
|
self._machine_id = machine_json.get("id")
|
||||||
self._machine_per_name = {} # type:Dict[str, int]
|
self._machine_name = machine_name.lower() # Lower in-case upper-case chars are added to the original json.
|
||||||
self._parse_version_url_per_machine = {} # type:Dict[int, Callable]
|
self._check_urls = [] # type:List[str]
|
||||||
self._check_urls_per_machine = {} # type:Dict[int, List[str]]
|
for check_url in machine_json.get("check_urls"):
|
||||||
self._redirect_user_per_machine = {} # type:Dict[int, str]
|
self._check_urls.append(check_url)
|
||||||
try:
|
self._redirect_user = machine_json.get("update_url")
|
||||||
for machine_json in machines_json:
|
|
||||||
machine_id = machine_json.get("id")
|
|
||||||
machine_name = machine_json.get("name").lower() # Lower in case upper-case char are added to the json.
|
|
||||||
self._machine_ids.append(machine_id)
|
|
||||||
self._machine_per_name[machine_name] = machine_id
|
|
||||||
self._check_urls_per_machine[machine_id] = [] # Multiple check-urls: see "_comment" in the .json file.
|
|
||||||
for check_url in machine_json.get("check_urls"):
|
|
||||||
self._check_urls_per_machine[machine_id].append(check_url)
|
|
||||||
self._redirect_user_per_machine[machine_id] = machine_json.get("update_url")
|
|
||||||
except Exception as ex:
|
|
||||||
Logger.log("e", "Couldn't parse firmware-update-check lookup-lists from file because {0}.".format(ex))
|
|
||||||
|
|
||||||
def getMachineIds(self) -> List[int]:
|
def getMachineId(self) -> Optional[int]:
|
||||||
return self._machine_ids
|
return self._machine_id
|
||||||
|
|
||||||
def getMachineByName(self, machine_name: str) -> Optional[int]:
|
def getMachineName(self) -> Optional[int]:
|
||||||
return self._machine_per_name.get(machine_name)
|
return self._machine_name
|
||||||
|
|
||||||
def getParseVersionUrlFor(self, machine_id: int) -> Optional[Callable]:
|
def getCheckUrls(self) -> Optional[List[str]]:
|
||||||
return self._parse_version_url_per_machine.get(machine_id)
|
return self._check_urls
|
||||||
|
|
||||||
def getCheckUrlsFor(self, machine_id: int) -> Optional[List[str]]:
|
def getRedirectUserUrl(self) -> Optional[str]:
|
||||||
return self._check_urls_per_machine.get(machine_id)
|
return self._redirect_user
|
||||||
|
|
||||||
def getRedirectUserFor(self, machine_id: int) -> Optional[str]:
|
|
||||||
return self._redirect_user_per_machine.get(machine_id)
|
|
||||||
|
@ -9,7 +9,7 @@ i18n_catalog = i18nCatalog("cura")
|
|||||||
class FirmwareUpdateCheckerMessage(Message):
|
class FirmwareUpdateCheckerMessage(Message):
|
||||||
STR_ACTION_DOWNLOAD = "download"
|
STR_ACTION_DOWNLOAD = "download"
|
||||||
|
|
||||||
def __init__(self, machine_id: int, machine_name: str) -> None:
|
def __init__(self, machine_id: int, machine_name: str, download_url: str) -> None:
|
||||||
super().__init__(i18n_catalog.i18nc(
|
super().__init__(i18n_catalog.i18nc(
|
||||||
"@info Don't translate {machine_name}, since it gets replaced by a printer name!",
|
"@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(
|
"New features are available for your {machine_name}! It is recommended to update the firmware on your printer.").format(
|
||||||
@ -19,6 +19,7 @@ class FirmwareUpdateCheckerMessage(Message):
|
|||||||
"New %s firmware available") % machine_name)
|
"New %s firmware available") % machine_name)
|
||||||
|
|
||||||
self._machine_id = machine_id
|
self._machine_id = machine_id
|
||||||
|
self._download_url = download_url
|
||||||
|
|
||||||
self.addAction(self.STR_ACTION_DOWNLOAD,
|
self.addAction(self.STR_ACTION_DOWNLOAD,
|
||||||
i18n_catalog.i18nc("@action:button", "How to update"),
|
i18n_catalog.i18nc("@action:button", "How to update"),
|
||||||
@ -29,3 +30,6 @@ class FirmwareUpdateCheckerMessage(Message):
|
|||||||
|
|
||||||
def getMachineId(self) -> int:
|
def getMachineId(self) -> int:
|
||||||
return self._machine_id
|
return self._machine_id
|
||||||
|
|
||||||
|
def getDownloadUrl(self) -> str:
|
||||||
|
return self._download_url
|
||||||
|
@ -1,33 +0,0 @@
|
|||||||
{
|
|
||||||
"_comment": "There are multiple 'check_urls', because sometimes an URL is about to be phased out, and it's useful to have a new 'future-proof' one at the ready.",
|
|
||||||
|
|
||||||
"machines":
|
|
||||||
[
|
|
||||||
{
|
|
||||||
"id": 9066,
|
|
||||||
"name": "ultimaker 3",
|
|
||||||
"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/en/resources/20500-upgrade-firmware"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": 9511,
|
|
||||||
"name": "ultimaker 3 extended",
|
|
||||||
"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/en/resources/20500-upgrade-firmware"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": 9051,
|
|
||||||
"name": "ultimaker s5",
|
|
||||||
"check_urls": ["http://software.ultimaker.com/releases/firmware/9051/stable/version.txt"],
|
|
||||||
"update_url": "https://ultimaker.com/en/resources/20500-upgrade-firmware"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
@ -24,7 +24,16 @@
|
|||||||
},
|
},
|
||||||
"first_start_actions": [ "DiscoverUM3Action" ],
|
"first_start_actions": [ "DiscoverUM3Action" ],
|
||||||
"supported_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/en/resources/20500-upgrade-firmware"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
||||||
|
@ -23,7 +23,16 @@
|
|||||||
"1": "ultimaker3_extended_extruder_right"
|
"1": "ultimaker3_extended_extruder_right"
|
||||||
},
|
},
|
||||||
"first_start_actions": [ "DiscoverUM3Action" ],
|
"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/en/resources/20500-upgrade-firmware"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
"overrides": {
|
"overrides": {
|
||||||
|
@ -30,7 +30,12 @@
|
|||||||
"first_start_actions": [ "DiscoverUM3Action" ],
|
"first_start_actions": [ "DiscoverUM3Action" ],
|
||||||
"supported_actions": [ "DiscoverUM3Action" ],
|
"supported_actions": [ "DiscoverUM3Action" ],
|
||||||
"supports_usb_connection": false,
|
"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/en/resources/20500-upgrade-firmware"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
"overrides": {
|
"overrides": {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user