From d94df9dd85ad1b0f5a65eda65a89e389ad25e8d4 Mon Sep 17 00:00:00 2001 From: jneilliii Date: Sat, 26 Oct 2024 23:41:17 -0400 Subject: [PATCH] 0.1.8rc1 (#56) * bring latest pybambu module in from home assistant integration * add onBeforePrintStart callback to prevent starting "local" files and display print options dialog.* add onBeforePrintStart callback to prevent starting "local" files and display print options dialog (with AMS mapping support) * add AMS display in sidebar --- .github/workflows/issue-validator.yml | 21 + octoprint_bambu_printer/bambu_print_plugin.py | 11 +- .../printer/bambu_virtual_printer.py | 36 +- .../printer/pybambu/__init__.py | 4 + .../printer/pybambu/bambu_client.py | 552 +++++++ .../printer/pybambu/bambu_cloud.py | 293 ++++ .../printer/pybambu/commands.py | 24 + .../printer/pybambu/const.py | 1222 ++++++++++++++ .../printer/pybambu/filaments.json | 72 + .../printer/pybambu/models.py | 1424 +++++++++++++++++ .../printer/pybambu/utils.py | 227 +++ .../printer/states/idle_state.py | 2 +- .../printer/states/paused_state.py | 2 +- .../printer/states/printing_state.py | 6 +- .../static/css/bambu_printer.css | 24 + .../static/js/bambu_printer.js | 118 +- .../static/js/jquery-ui.min.js | 8 + .../static/js/knockout-sortable.1.2.0.js | 490 ++++++ .../templates/bambu_printer.jinja2 | 31 + .../templates/bambu_printer_sidebar.jinja2 | 12 + setup.py | 4 +- test/test_gcode_execution.py | 4 +- 22 files changed, 4518 insertions(+), 69 deletions(-) create mode 100644 .github/workflows/issue-validator.yml create mode 100644 octoprint_bambu_printer/printer/pybambu/__init__.py create mode 100644 octoprint_bambu_printer/printer/pybambu/bambu_client.py create mode 100644 octoprint_bambu_printer/printer/pybambu/bambu_cloud.py create mode 100644 octoprint_bambu_printer/printer/pybambu/commands.py create mode 100644 octoprint_bambu_printer/printer/pybambu/const.py create mode 100644 octoprint_bambu_printer/printer/pybambu/filaments.json create mode 100644 octoprint_bambu_printer/printer/pybambu/models.py create mode 100644 octoprint_bambu_printer/printer/pybambu/utils.py create mode 100644 octoprint_bambu_printer/static/css/bambu_printer.css create mode 100644 octoprint_bambu_printer/static/js/jquery-ui.min.js create mode 100644 octoprint_bambu_printer/static/js/knockout-sortable.1.2.0.js create mode 100644 octoprint_bambu_printer/templates/bambu_printer.jinja2 create mode 100644 octoprint_bambu_printer/templates/bambu_printer_sidebar.jinja2 diff --git a/.github/workflows/issue-validator.yml b/.github/workflows/issue-validator.yml new file mode 100644 index 0000000..a806637 --- /dev/null +++ b/.github/workflows/issue-validator.yml @@ -0,0 +1,21 @@ +name: issue validator +on: + workflow_dispatch: + issues: + types: [opened, edited] + +permissions: + issues: write + +jobs: + validate: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: Okabe-Junya/issue-validator@v0.4.1 + with: + body: '/\[(octoprint\.log)\]|\[(plugin_bambu_printer_serial\.log)\]/g' + body-regex-flags: 'true' + is-auto-close: 'true' + issue-type: 'both' + github-token: ${{ secrets.GITHUB_TOKEN }} \ No newline at end of file diff --git a/octoprint_bambu_printer/bambu_print_plugin.py b/octoprint_bambu_printer/bambu_print_plugin.py index dd255f0..aba273f 100644 --- a/octoprint_bambu_printer/bambu_print_plugin.py +++ b/octoprint_bambu_printer/bambu_print_plugin.py @@ -22,7 +22,7 @@ from octoprint.access.permissions import Permissions from octoprint.logging.handlers import CleaningTimedRotatingFileHandler from octoprint_bambu_printer.printer.file_system.cached_file_view import CachedFileView -from pybambu import BambuCloud +from octoprint_bambu_printer.printer.pybambu import BambuCloud from octoprint_bambu_printer.printer.file_system.remote_sd_card_file_list import ( RemoteSDCardFileList, @@ -67,7 +67,9 @@ class BambuPrintPlugin( self._timelapse_files_view.with_filter("timelapse/", ".avi") def get_assets(self): - return {"js": ["js/bambu_printer.js"]} + return {"js": ["js/jquery-ui.min.js", "js/knockout-sortable.1.2.0.js", "js/bambu_printer.js"], + "css": ["css/bambu_printer.css"] + } def get_template_configs(self): return [ @@ -77,7 +79,7 @@ class BambuPrintPlugin( "custom_bindings": True, "template": "bambu_timelapse.jinja2", }, - ] # , {"type": "generic", "custom_bindings": True, "template": "bambu_printer.jinja2"}] + {"type": "generic", "custom_bindings": True, "template": "bambu_printer.jinja2"}] def get_settings_defaults(self): return { @@ -97,6 +99,9 @@ class BambuPrintPlugin( "email": "", "auth_token": "", "always_use_default_options": False, + "ams_data": [], + "ams_mapping": [], + "ams_current_tray": None, } def is_api_adminonly(self): diff --git a/octoprint_bambu_printer/printer/bambu_virtual_printer.py b/octoprint_bambu_printer/printer/bambu_virtual_printer.py index 7fe0185..632ca97 100644 --- a/octoprint_bambu_printer/printer/bambu_virtual_printer.py +++ b/octoprint_bambu_printer/printer/bambu_virtual_printer.py @@ -1,7 +1,7 @@ from __future__ import annotations import collections -from dataclasses import dataclass, field +from dataclasses import dataclass, field, asdict import math from pathlib import Path import queue @@ -11,7 +11,7 @@ import time from octoprint_bambu_printer.printer.file_system.cached_file_view import CachedFileView from octoprint_bambu_printer.printer.file_system.file_info import FileInfo from octoprint_bambu_printer.printer.print_job import PrintJob -from pybambu import BambuClient, commands +from octoprint_bambu_printer.printer.pybambu import BambuClient, commands import logging import logging.handlers @@ -43,6 +43,7 @@ class BambuPrinterTelemetry: lastTempAt: float = time.monotonic() firmwareName: str = "Bambu" extruderCount: int = 1 + ams_current_tray: int = -1 # noinspection PyBroadException @@ -64,6 +65,7 @@ class BambuVirtualPrinter: self._data_folder = data_folder self._last_hms_errors = None self._log = logging.getLogger("octoprint.plugins.bambu_printer.BambuPrinter") + self.ams_data = self._settings.get(["ams_data"]) self._state_idle = IdleState(self) self._state_printing = PrintingState(self) @@ -168,6 +170,22 @@ class BambuVirtualPrinter: def change_state(self, new_state: APrinterState): self._state_change_queue.put(new_state) + def _convert2serialize(self, obj): + if isinstance(obj, dict): + return {k: self._convert2serialize(v) for k, v in obj.items()} + elif hasattr(obj, "_ast"): + return self._convert2serialize(obj._ast()) + elif not isinstance(obj, str) and hasattr(obj, "__iter__"): + return [self._convert2serialize(v) for v in obj] + elif hasattr(obj, "__dict__"): + return { + k: self._convert2serialize(v) + for k, v in obj.__dict__.items() + if not callable(v) and not k.startswith('_') + } + else: + return obj + def new_update(self, event_type): if event_type == "event_hms_errors": self._update_hms_errors() @@ -178,6 +196,13 @@ class BambuVirtualPrinter: device_data = self.bambu_client.get_device() print_job_state = device_data.print_job.gcode_state temperatures = device_data.temperature + ams_data = self._convert2serialize(device_data.ams.data) + + if self.ams_data != ams_data: + self._log.debug(f"Recieveid AMS Update: {ams_data}") + self.ams_data = ams_data + self._settings.set(["ams_data"], ams_data) + self._settings.save(trigger_event=True) self.lastTempAt = time.monotonic() self._telemetry.temp[0] = temperatures.nozzle_temp @@ -185,6 +210,11 @@ class BambuVirtualPrinter: self._telemetry.bedTemp = temperatures.bed_temp self._telemetry.bedTargetTemp = temperatures.target_bed_temp self._telemetry.chamberTemp = temperatures.chamber_temp + self._telemetry.ams_current_tray = device_data.push_all_data["ams"]["tray_now"] or -1 + + if self._telemetry.ams_current_tray != self._settings.get_int(["ams_current_tray"]): + self._settings.set_int(["ams_current_tray"], self._telemetry.ams_current_tray) + self._settings.save(trigger_event=True) self._log.debug(f"Received printer state update: {print_job_state}") if ( @@ -214,6 +244,8 @@ class BambuVirtualPrinter: def on_disconnect(self, on_disconnect): self._log.debug(f"on disconnect called") + self.stop_continuous_status_report() + self.stop_continuous_temp_report() return on_disconnect def on_connect(self, on_connect): diff --git a/octoprint_bambu_printer/printer/pybambu/__init__.py b/octoprint_bambu_printer/printer/pybambu/__init__.py new file mode 100644 index 0000000..cd59e96 --- /dev/null +++ b/octoprint_bambu_printer/printer/pybambu/__init__.py @@ -0,0 +1,4 @@ +"""Initialise the Bambu Client""" +# TODO: Once complete, move pybambu to PyPi +from .bambu_client import BambuClient +from .bambu_cloud import BambuCloud diff --git a/octoprint_bambu_printer/printer/pybambu/bambu_client.py b/octoprint_bambu_printer/printer/pybambu/bambu_client.py new file mode 100644 index 0000000..a0cd9a4 --- /dev/null +++ b/octoprint_bambu_printer/printer/pybambu/bambu_client.py @@ -0,0 +1,552 @@ +from __future__ import annotations + +import queue +import json +import math +import re +import socket +import ssl +import struct +import threading +import time + +from dataclasses import dataclass +from typing import Any + +import paho.mqtt.client as mqtt + +from .bambu_cloud import BambuCloud +from .const import ( + LOGGER, + Features, +) +from .models import Device, SlicerSettings +from .commands import ( + GET_VERSION, + PUSH_ALL, + START_PUSH, +) + + +class WatchdogThread(threading.Thread): + + def __init__(self, client): + self._client = client + self._watchdog_fired = False + self._stop_event = threading.Event() + self._last_received_data = time.time() + super().__init__() + self.setName(f"{self._client._device.info.device_type}-Watchdog-{threading.get_native_id()}") + + def stop(self): + self._stop_event.set() + + def received_data(self): + self._last_received_data = time.time() + + def run(self): + LOGGER.info("Watchdog thread started.") + WATCHDOG_TIMER = 30 + while True: + # Wait out the remainder of the watchdog delay or 1s, whichever is higher. + interval = time.time() - self._last_received_data + wait_time = max(1, WATCHDOG_TIMER - interval) + if self._stop_event.wait(wait_time): + # Stop event has been set. Exit thread. + break + interval = time.time() - self._last_received_data + if not self._watchdog_fired and (interval > WATCHDOG_TIMER): + LOGGER.debug(f"Watchdog fired. No data received for {math.floor(interval)} seconds for {self._client._serial}.") + self._watchdog_fired = True + self._client._on_watchdog_fired() + elif interval < WATCHDOG_TIMER: + self._watchdog_fired = False + + LOGGER.info("Watchdog thread exited.") + + +class ChamberImageThread(threading.Thread): + def __init__(self, client): + self._client = client + self._stop_event = threading.Event() + super().__init__() + self.setName(f"{self._client._device.info.device_type}-Chamber-{threading.get_native_id()}") + + def stop(self): + self._stop_event.set() + + def run(self): + LOGGER.debug("Chamber image thread started.") + + auth_data = bytearray() + + username = 'bblp' + access_code = self._client._access_code + hostname = self._client.host + port = 6000 + MAX_CONNECT_ATTEMPTS = 12 + connect_attempts = 0 + + auth_data += struct.pack(" 0: + img += dr + if len(img) > payload_size: + # We got more data than we expected. + LOGGER.error(f"Unexpected image payload received: {len(img)} > {payload_size}") + # Reset buffer + img = None + elif len(img) == payload_size: + # We should have the full image now. + if img[:4] != jpeg_start: + LOGGER.error("JPEG start magic bytes missing.") + elif img[-2:] != jpeg_end: + LOGGER.error("JPEG end magic bytes missing.") + else: + # Content is as expected. Send it. + self._client.on_jpeg_received(img) + + # Reset buffer + img = None + # else: + # Otherwise we need to continue looping without reseting the buffer to receive the remaining data + # and without delaying. + + elif len(dr) == 16: + # We got the header bytes. Get the expected payload size from it and create the image buffer bytearray. + # Reset connect_attempts now we know the connect was successful. + connect_attempts = 0 + img = bytearray() + payload_size = int.from_bytes(dr[0:3], byteorder='little') + + elif len(dr) == 0: + # This occurs if the wrong access code was provided. + LOGGER.error("Chamber image connection rejected by the printer. Check provided access code and IP address.") + # Sleep for a short while and then re-attempt the connection. + time.sleep(5) + break + + else: + LOGGER.error(f"UNEXPECTED DATA RECEIVED: {len(dr)}") + time.sleep(1) + + except OSError as e: + if e.errno == 113: + LOGGER.debug("Host is unreachable") + else: + LOGGER.error("A Chamber Image thread outer exception occurred:") + LOGGER.error(f"Exception. Type: {type(e)} Args: {e}") + if not self._stop_event.is_set(): + time.sleep(1) # Avoid a tight loop if this is a persistent error. + + except Exception as e: + LOGGER.error(f"A Chamber Image thread outer exception occurred:") + LOGGER.error(f"Exception. Type: {type(e)} Args: {e}") + if not self._stop_event.is_set(): + time.sleep(1) # Avoid a tight loop if this is a persistent error. + + LOGGER.debug("Chamber image thread exited.") + + +class MqttThread(threading.Thread): + def __init__(self, client): + self._client = client + self._stop_event = threading.Event() + super().__init__() + self.setName(f"{self._client._device.info.device_type}-Mqtt-{threading.get_native_id()}") + + def stop(self): + self._stop_event.set() + + def run(self): + LOGGER.info("MQTT listener thread started.") + exceptionSeen = "" + while True: + try: + host = self._client.host if self._client._local_mqtt else self._client.bambu_cloud.cloud_mqtt_host + LOGGER.debug(f"Connect: Attempting Connection to {host}") + self._client.client.connect(host, self._client._port, keepalive=5) + + LOGGER.debug("Starting listen loop") + self._client.client.loop_forever() + LOGGER.debug("Ended listen loop.") + break + except TimeoutError as e: + if exceptionSeen != "TimeoutError": + LOGGER.debug(f"TimeoutError: {e}.") + exceptionSeen = "TimeoutError" + time.sleep(5) + except ConnectionError as e: + if exceptionSeen != "ConnectionError": + LOGGER.debug(f"ConnectionError: {e}.") + exceptionSeen = "ConnectionError" + time.sleep(5) + except OSError as e: + if e.errno == 113: + if exceptionSeen != "OSError113": + LOGGER.debug(f"OSError: {e}.") + exceptionSeen = "OSError113" + time.sleep(5) + else: + LOGGER.error("A listener loop thread exception occurred:") + LOGGER.error(f"Exception. Type: {type(e)} Args: {e}") + time.sleep(1) # Avoid a tight loop if this is a persistent error. + except Exception as e: + LOGGER.error("A listener loop thread exception occurred:") + LOGGER.error(f"Exception. Type: {type(e)} Args: {e}") + time.sleep(1) # Avoid a tight loop if this is a persistent error. + + if self._client.client is None: + break + + self._client.client.disconnect() + + LOGGER.info("MQTT listener thread exited.") + + +@dataclass +class BambuClient: + """Initialize Bambu Client to connect to MQTT Broker""" + _watchdog = None + _camera = None + _usage_hours: float + + def __init__(self, device_type: str, serial: str, host: str, local_mqtt: bool, region: str, email: str, + username: str, auth_token: str, access_code: str, usage_hours: float = 0, manual_refresh_mode: bool = False): + self.callback = None + self.host = host + self._local_mqtt = local_mqtt + self._serial = serial + self._auth_token = auth_token + self._access_code = access_code + self._username = username + self._connected = False + self._device_type = device_type + self._usage_hours = usage_hours + self._port = 1883 + self._refreshed = False + self._manual_refresh_mode = manual_refresh_mode + self._device = Device(self) + self.bambu_cloud = BambuCloud(region, email, username, auth_token) + self.slicer_settings = SlicerSettings(self) + + @property + def connected(self): + """Return if connected to server""" + return self._connected + + @property + def manual_refresh_mode(self): + """Return if the integration is running in poll mode""" + return self._manual_refresh_mode + + async def set_manual_refresh_mode(self, on): + self._manual_refresh_mode = on + if self._manual_refresh_mode: + # Disconnect from the server. User must manually hit the refresh button to connect to refresh and then it will immediately disconnect. + self.disconnect() + else: + # Reconnect normally + self.connect(self.callback) + + def connect(self, callback): + """Connect to the MQTT Broker""" + self.client = mqtt.Client() + self.callback = callback + self.client.on_connect = self.on_connect + self.client.on_disconnect = self.on_disconnect + self.client.on_message = self.on_message + # Set aggressive reconnect polling. + self.client.reconnect_delay_set(min_delay=1, max_delay=1) + + self.client.tls_set(tls_version=ssl.PROTOCOL_TLS, cert_reqs=ssl.CERT_NONE) + self.client.tls_insecure_set(True) + self._port = 8883 + if self._local_mqtt: + self.client.username_pw_set("bblp", password=self._access_code) + else: + self.client.username_pw_set(self._username, password=self._auth_token) + + LOGGER.debug("Starting MQTT listener thread") + self._mqtt = MqttThread(self) + self._mqtt.start() + + def subscribe_and_request_info(self): + LOGGER.debug("Loading slicer settings...") + self.slicer_settings.update() + LOGGER.debug("Now subscribing...") + self.subscribe() + LOGGER.debug("On Connect: Getting version info") + self.publish(GET_VERSION) + LOGGER.debug("On Connect: Request push all") + self.publish(PUSH_ALL) + + def on_connect(self, + client_: mqtt.Client, + userdata: None, + flags: dict[str, Any], + result_code: int, + properties: mqtt.Properties | None = None, ): + """Handle connection""" + LOGGER.info("On Connect: Connected to printer") + self._on_connect() + + def _on_connect(self): + self._connected = True + self.subscribe_and_request_info() + + LOGGER.debug("Starting watchdog thread") + self._watchdog = WatchdogThread(self) + self._watchdog.start() + + if self._device.supports_feature(Features.CAMERA_IMAGE): + LOGGER.debug("Starting Chamber Image thread") + self._camera = ChamberImageThread(self) + self._camera.start() + + def try_on_connect(self, + client_: mqtt.Client, + userdata: None, + flags: dict[str, Any], + result_code: int, + properties: mqtt.Properties | None = None, ): + """Handle connection""" + LOGGER.info("On Connect: Connected to printer") + self._connected = True + LOGGER.debug("Now test subscribing...") + self.subscribe() + # For the initial configuration connection attempt, we just need version info. + LOGGER.debug("On Connect: Getting version info") + self.publish(GET_VERSION) + + def on_disconnect(self, + client_: mqtt.Client, + userdata: None, + result_code: int): + """Called when MQTT Disconnects""" + LOGGER.warn(f"On Disconnect: Printer disconnected with error code: {result_code}") + self._on_disconnect() + + def _on_disconnect(self): + LOGGER.debug("_on_disconnect: Lost connection to the printer") + self._connected = False + self._device.info.set_online(False) + if self._watchdog is not None: + LOGGER.debug("Stopping watchdog thread") + self._watchdog.stop() + self._watchdog.join() + if self._camera is not None: + LOGGER.debug("Stopping camera thread") + self._camera.stop() + self._camera.join() + + def _on_watchdog_fired(self): + LOGGER.info("Watch dog fired") + self._device.info.set_online(False) + self.publish(START_PUSH) + + def on_jpeg_received(self, bytes): + self._device.chamber_image.set_jpeg(bytes) + + def on_message(self, client, userdata, message): + """Return the payload when received""" + try: + # X1 mqtt payload is inconsistent. Adjust it for consistent logging. + clean_msg = re.sub(r"\\n *", "", str(message.payload)) + if self._refreshed: + LOGGER.debug(f"Received data: {clean_msg}") + + json_data = json.loads(message.payload) + if json_data.get("event"): + # These are events from the bambu cloud mqtt feed and allow us to detect when a local + # device has connected/disconnected (e.g. turned on/off) + if json_data.get("event").get("event") == "client.connected": + LOGGER.debug("Client connected event received.") + self._on_disconnect() # We aren't guaranteed to recieve a client.disconnected event. + self._on_connect() + elif json_data.get("event").get("event") == "client.disconnected": + LOGGER.debug("Client disconnected event received.") + self._on_disconnect() + else: + self._device.info.set_online(True) + self._watchdog.received_data() + if json_data.get("print"): + self._device.print_update(data=json_data.get("print")) + # Once we receive data, if in manual refresh mode, we disconnect again. + if self._manual_refresh_mode: + self.disconnect() + if json_data.get("print").get("msg", 0) == 0: + self._refreshed= False + elif json_data.get("info") and json_data.get("info").get("command") == "get_version": + LOGGER.debug("Got Version Data") + self._device.info_update(data=json_data.get("info")) + except Exception as e: + LOGGER.error("An exception occurred processing a message:") + LOGGER.error(f"Exception type: {type(e)}") + LOGGER.error(f"Exception data: {e}") + + def subscribe(self): + """Subscribe to report topic""" + LOGGER.debug(f"Subscribing: device/{self._serial}/report") + self.client.subscribe(f"device/{self._serial}/report") + + def publish(self, msg): + """Publish a custom message""" + result = self.client.publish(f"device/{self._serial}/request", json.dumps(msg)) + status = result[0] + if status == 0: + LOGGER.debug(f"Sent {msg} to topic device/{self._serial}/request") + return True + + LOGGER.error(f"Failed to send message to topic device/{self._serial}/request") + return False + + async def refresh(self): + """Force refresh data""" + + if self._manual_refresh_mode: + self.connect(self.callback) + else: + LOGGER.debug("Force Refresh: Getting Version Info") + self._refreshed = True + self.publish(GET_VERSION) + LOGGER.debug("Force Refresh: Request Push All") + self._refreshed = True + self.publish(PUSH_ALL) + + self.slicer_settings.update() + + def get_device(self): + """Return device""" + return self._device + + def disconnect(self): + """Disconnect the Bambu Client from server""" + LOGGER.debug(" Disconnect: Client Disconnecting") + if self.client is not None: + self.client.disconnect() + self.client = None + + async def try_connection(self): + """Test if we can connect to an MQTT broker.""" + LOGGER.debug("Try Connection") + + result: queue.Queue[bool] = queue.Queue(maxsize=1) + + def on_message(client, userdata, message): + json_data = json.loads(message.payload) + LOGGER.debug(f"Try Connection: Got '{json_data}'") + if json_data.get("info") and json_data.get("info").get("command") == "get_version": + LOGGER.debug("Got Version Command Data") + self._device.info_update(data=json_data.get("info")) + result.put(True) + + self.client = mqtt.Client() + self.client.on_connect = self.try_on_connect + self.client.on_disconnect = self.on_disconnect + self.client.on_message = on_message + + self.client.tls_set(tls_version=ssl.PROTOCOL_TLS, cert_reqs=ssl.CERT_NONE) + self.client.tls_insecure_set(True) + if self._local_mqtt: + self.client.username_pw_set("bblp", password=self._access_code) + else: + self.client.username_pw_set(self._username, password=self._auth_token) + self._port = 8883 + + LOGGER.debug("Test connection: Connecting to %s", self.host) + try: + self.client.connect(self.host, self._port) + self.client.loop_start() + if result.get(timeout=10): + return True + except OSError as e: + return False + except queue.Empty: + return False + finally: + self.disconnect() + + async def __aenter__(self): + """Async enter. + Returns: + The BambuLab object. + """ + return self + + async def __aexit__(self, *_exc_info): + """Async exit. + Args: + _exc_info: Exec type. + """ + self.disconnect() diff --git a/octoprint_bambu_printer/printer/pybambu/bambu_cloud.py b/octoprint_bambu_printer/printer/pybambu/bambu_cloud.py new file mode 100644 index 0000000..0cd2cbb --- /dev/null +++ b/octoprint_bambu_printer/printer/pybambu/bambu_cloud.py @@ -0,0 +1,293 @@ +from __future__ import annotations + +import base64 +import json +import httpx + +from dataclasses import dataclass + +from .const import LOGGER + +@dataclass +class BambuCloud: + + def __init__(self, region: str, email: str, username: str, auth_token: str): + self._region = region + self._email = email + self._username = username + self._auth_token = auth_token + + def _get_authentication_token(self) -> dict: + LOGGER.debug("Getting accessToken from Bambu Cloud") + if self._region == "China": + url = 'https://api.bambulab.cn/v1/user-service/user/login' + else: + url = 'https://api.bambulab.com/v1/user-service/user/login' + headers = {'User-Agent' : "HA Bambulab"} + data = {'account': self._email, 'password': self._password} + with httpx.Client(http2=True) as client: + response = client.post(url, headers=headers, json=data, timeout=10) + if response.status_code >= 400: + LOGGER.debug(f"Received error: {response.status_code}") + raise ValueError(response.status_code) + return response.json()['accessToken'] + + def _get_username_from_authentication_token(self) -> str: + # User name is in 2nd portion of the auth token (delimited with periods) + b64_string = self._auth_token.split(".")[1] + # String must be multiples of 4 chars in length. For decode pad with = character + b64_string += "=" * ((4 - len(b64_string) % 4) % 4) + jsonAuthToken = json.loads(base64.b64decode(b64_string)) + # Gives json payload with "username":"u_" within it + return jsonAuthToken['username'] + + # Retrieves json description of devices in the form: + # { + # 'message': 'success', + # 'code': None, + # 'error': None, + # 'devices': [ + # { + # 'dev_id': 'REDACTED', + # 'name': 'Bambu P1S', + # 'online': True, + # 'print_status': 'SUCCESS', + # 'dev_model_name': 'C12', + # 'dev_product_name': 'P1S', + # 'dev_access_code': 'REDACTED', + # 'nozzle_diameter': 0.4 + # }, + # { + # 'dev_id': 'REDACTED', + # 'name': 'Bambu P1P', + # 'online': True, + # 'print_status': 'RUNNING', + # 'dev_model_name': 'C11', + # 'dev_product_name': 'P1P', + # 'dev_access_code': 'REDACTED', + # 'nozzle_diameter': 0.4 + # }, + # { + # 'dev_id': 'REDACTED', + # 'name': 'Bambu X1C', + # 'online': True, + # 'print_status': 'RUNNING', + # 'dev_model_name': 'BL-P001', + # 'dev_product_name': 'X1 Carbon', + # 'dev_access_code': 'REDACTED', + # 'nozzle_diameter': 0.4 + # } + # ] + # } + + def test_authentication(self, region: str, email: str, username: str, auth_token: str) -> bool: + self._region = region + self._email = email + self._username = username + self._auth_token = auth_token + try: + self.get_device_list() + except: + return False + return True + + def login(self, region: str, email: str, password: str): + self._region = region + self._email = email + self._password = password + + self._auth_token = self._get_authentication_token() + self._username = self._get_username_from_authentication_token() + + def get_device_list(self) -> dict: + LOGGER.debug("Getting device list from Bambu Cloud") + if self._region == "China": + url = 'https://api.bambulab.cn/v1/iot-service/api/user/bind' + else: + url = 'https://api.bambulab.com/v1/iot-service/api/user/bind' + headers = {'Authorization': 'Bearer ' + self._auth_token, 'User-Agent' : "HA Bambulab"} + with httpx.Client(http2=True) as client: + response = client.get(url, headers=headers, timeout=10) + if response.status_code >= 400: + LOGGER.debug(f"Received error: {response.status_code}") + raise ValueError(response.status_code) + return response.json()['devices'] + + # The slicer settings are of the following form: + # + # { + # "message": "success", + # "code": null, + # "error": null, + # "print": { + # "public": [ + # { + # "setting_id": "GP004", + # "version": "01.09.00.15", + # "name": "0.20mm Standard @BBL X1C", + # "update_time": "2024-07-04 11:27:08", + # "nickname": null + # }, + # ... + # } + # "private": [] + # }, + # "printer": { + # "public": [ + # { + # "setting_id": "GM001", + # "version": "01.09.00.15", + # "name": "Bambu Lab X1 Carbon 0.4 nozzle", + # "update_time": "2024-07-04 11:25:07", + # "nickname": null + # }, + # ... + # ], + # "private": [] + # }, + # "filament": { + # "public": [ + # { + # "setting_id": "GFSA01", + # "version": "01.09.00.15", + # "name": "Bambu PLA Matte @BBL X1C", + # "update_time": "2024-07-04 11:29:21", + # "nickname": null, + # "filament_id": "GFA01" + # }, + # ... + # ], + # "private": [ + # { + # "setting_id": "PFUS46ea5c221cabe5", + # "version": "1.9.0.14", + # "name": "Fillamentum PLA Extrafill @Bambu Lab X1 Carbon 0.4 nozzle", + # "update_time": "2024-07-10 06:48:17", + # "base_id": null, + # "filament_id": "Pc628b24", + # "filament_type": "PLA", + # "filament_is_support": "0", + # "nozzle_temperature": [ + # 190, + # 240 + # ], + # "nozzle_hrc": "3", + # "filament_vendor": "Fillamentum" + # }, + # ... + # ] + # }, + # "settings": {} + # } + + def get_slicer_settings(self) -> dict: + LOGGER.debug("Getting slicer settings from Bambu Cloud") + if self._region == "China": + url = 'https://api.bambulab.cn/v1/iot-service/api/slicer/setting?version=undefined' + else: + url = 'https://api.bambulab.com/v1/iot-service/api/slicer/setting?version=undefined' + headers = {'Authorization': 'Bearer ' + self._auth_token, 'User-Agent' : "HA Bambulab"} + with httpx.Client(http2=True) as client: + response = client.get(url, headers=headers, timeout=10) + if response.status_code >= 400: + LOGGER.error(f"Slicer settings load failed: {response.status_code}") + return None + return response.json() + + # The task list is of the following form with a 'hits' array with typical 20 entries. + # + # "total": 531, + # "hits": [ + # { + # "id": 35237965, + # "designId": 0, + # "designTitle": "", + # "instanceId": 0, + # "modelId": "REDACTED", + # "title": "REDACTED", + # "cover": "REDACTED", + # "status": 4, + # "feedbackStatus": 0, + # "startTime": "2023-12-21T19:02:16Z", + # "endTime": "2023-12-21T19:02:35Z", + # "weight": 34.62, + # "length": 1161, + # "costTime": 10346, + # "profileId": 35276233, + # "plateIndex": 1, + # "plateName": "", + # "deviceId": "REDACTED", + # "amsDetailMapping": [ + # { + # "ams": 4, + # "sourceColor": "F4D976FF", + # "targetColor": "F4D976FF", + # "filamentId": "GFL99", + # "filamentType": "PLA", + # "targetFilamentType": "", + # "weight": 34.62 + # } + # ], + # "mode": "cloud_file", + # "isPublicProfile": false, + # "isPrintable": true, + # "deviceModel": "P1P", + # "deviceName": "Bambu P1P", + # "bedType": "textured_plate" + # }, + + def get_tasklist(self) -> dict: + if self._region == "China": + url = 'https://api.bambulab.cn/v1/user-service/my/tasks' + else: + url = 'https://api.bambulab.com/v1/user-service/my/tasks' + headers = {'Authorization': 'Bearer ' + self._auth_token, 'User-Agent' : "HA Bambulab"} + with httpx.Client(http2=True) as client: + response = client.get(url, headers=headers, timeout=10) + if response.status_code >= 400: + LOGGER.debug(f"Received error: {response.status_code}") + raise ValueError(response.status_code) + return response.json() + + def get_latest_task_for_printer(self, deviceId: str) -> dict: + LOGGER.debug(f"Getting latest task from Bambu Cloud for Printer: {deviceId}") + data = self.get_tasklist_for_printer(deviceId) + if len(data) != 0: + return data[0] + LOGGER.debug("No tasks found for printer") + return None + + def get_tasklist_for_printer(self, deviceId: str) -> dict: + LOGGER.debug(f"Getting task list from Bambu Cloud for Printer: {deviceId}") + tasks = [] + data = self.get_tasklist() + for task in data['hits']: + if task['deviceId'] == deviceId: + tasks.append(task) + return tasks + + def get_device_type_from_device_product_name(self, device_product_name: str): + if device_product_name == "X1 Carbon": + return "X1C" + return device_product_name.replace(" ", "") + + def download(self, url: str) -> bytearray: + LOGGER.debug(f"Downloading cover image: {url}") + with httpx.Client(http2=True) as client: + response = client.get(url, timeout=10) + if response.status_code >= 400: + LOGGER.debug(f"Received error: {response.status_code}") + raise ValueError(response.status_code) + return response.content + + @property + def username(self): + return self._username + + @property + def auth_token(self): + return self._auth_token + + @property + def cloud_mqtt_host(self): + return "cn.mqtt.bambulab.com" if self._region == "China" else "us.mqtt.bambulab.com" diff --git a/octoprint_bambu_printer/printer/pybambu/commands.py b/octoprint_bambu_printer/printer/pybambu/commands.py new file mode 100644 index 0000000..96271db --- /dev/null +++ b/octoprint_bambu_printer/printer/pybambu/commands.py @@ -0,0 +1,24 @@ +"""MQTT Commands""" +CHAMBER_LIGHT_ON = { + "system": {"sequence_id": "0", "command": "ledctrl", "led_node": "chamber_light", "led_mode": "on", + "led_on_time": 500, "led_off_time": 500, "loop_times": 0, "interval_time": 0}} +CHAMBER_LIGHT_OFF = { + "system": {"sequence_id": "0", "command": "ledctrl", "led_node": "chamber_light", "led_mode": "off", + "led_on_time": 500, "led_off_time": 500, "loop_times": 0, "interval_time": 0}} + +SPEED_PROFILE_TEMPLATE = {"print": {"sequence_id": "0", "command": "print_speed", "param": ""}} + +GET_VERSION = {"info": {"sequence_id": "0", "command": "get_version"}} + +PAUSE = {"print": {"sequence_id": "0", "command": "pause"}} +RESUME = {"print": {"sequence_id": "0", "command": "resume"}} +STOP = {"print": {"sequence_id": "0", "command": "stop"}} + +PUSH_ALL = {"pushing": {"sequence_id": "0", "command": "pushall"}} + +START_PUSH = { "pushing": {"sequence_id": "0", "command": "start"}} + +SEND_GCODE_TEMPLATE = {"print": {"sequence_id": "0", "command": "gcode_line", "param": ""}} # param = GCODE_EACH_LINE_SEPARATED_BY_\n + +# X1 only currently +GET_ACCESSORIES = {"system": {"sequence_id": "0", "command": "get_accessories", "accessory_type": "none"}} \ No newline at end of file diff --git a/octoprint_bambu_printer/printer/pybambu/const.py b/octoprint_bambu_printer/printer/pybambu/const.py new file mode 100644 index 0000000..c867b14 --- /dev/null +++ b/octoprint_bambu_printer/printer/pybambu/const.py @@ -0,0 +1,1222 @@ +import json +import logging + +from pathlib import Path +from enum import ( + Enum, + IntEnum, +) + +LOGGER = logging.getLogger(__package__) + + +class Features(Enum): + AUX_FAN = 1, + CHAMBER_LIGHT = 2, + CHAMBER_FAN = 3, + CHAMBER_TEMPERATURE = 4, + CURRENT_STAGE = 5, + PRINT_LAYERS = 6, + AMS = 7, + EXTERNAL_SPOOL = 8, + K_VALUE = 9, + START_TIME = 10, + AMS_TEMPERATURE = 11, + CAMERA_RTSP = 13, + START_TIME_GENERATED = 14, + CAMERA_IMAGE = 15, + DOOR_SENSOR = 16, + MANUAL_MODE = 17, + + +class FansEnum(Enum): + PART_COOLING = 1, + AUXILIARY = 2, + CHAMBER = 3, + HEATBREAK = 4, + + +CURRENT_STAGE_IDS = { + "default": "unknown", + 0: "printing", + 1: "auto_bed_leveling", + 2: "heatbed_preheating", + 3: "sweeping_xy_mech_mode", + 4: "changing_filament", + 5: "m400_pause", + 6: "paused_filament_runout", + 7: "heating_hotend", + 8: "calibrating_extrusion", + 9: "scanning_bed_surface", + 10: "inspecting_first_layer", + 11: "identifying_build_plate_type", + 12: "calibrating_micro_lidar", # DUPLICATED? + 13: "homing_toolhead", + 14: "cleaning_nozzle_tip", + 15: "checking_extruder_temperature", + 16: "paused_user", + 17: "paused_front_cover_falling", + 18: "calibrating_micro_lidar", # DUPLICATED? + 19: "calibrating_extrusion_flow", + 20: "paused_nozzle_temperature_malfunction", + 21: "paused_heat_bed_temperature_malfunction", + 22: "filament_unloading", + 23: "paused_skipped_step", + 24: "filament_loading", + 25: "calibrating_motor_noise", + 26: "paused_ams_lost", + 27: "paused_low_fan_speed_heat_break", + 28: "paused_chamber_temperature_control_error", + 29: "cooling_chamber", + 30: "paused_user_gcode", + 31: "motor_noise_showoff", + 32: "paused_nozzle_filament_covered_detected", + 33: "paused_cutter_error", + 34: "paused_first_layer_error", + 35: "paused_nozzle_clog", + # X1 returns -1 for idle + -1: "idle", # DUPLICATED + # P1 returns 255 for idle + 255: "idle", # DUPLICATED +} + +CURRENT_STAGE_OPTIONS = list(set(CURRENT_STAGE_IDS.values())) # Conversion to set first removes the duplicates + +GCODE_STATE_OPTIONS = [ + "failed", + "finish", + "idle", + "init", + "offline", + "pause", + "prepare", + "running", + "slicing", + "unknown" +] + +SPEED_PROFILE = { + 1: "silent", + 2: "standard", + 3: "sport", + 4: "ludicrous" +} + +PRINT_TYPE_OPTIONS = { + "cloud", + "local", + "idle", + "system", + "unknown" +} + + +def load_dict(filename: str) -> dict: + with open(filename) as f: + return json.load(f); + + +FILAMENT_NAMES = load_dict(Path(__file__).with_name('filaments.json')) + +# TODO: Update error lists with data from https://e.bambulab.com/query.php?lang=en +# UNIQUE_ID=dAa5VFRi +HMS_ERRORS = { + "0C00_0300_0002_0010": "Foreign objects detected on hotbed: please check and clean the hotbed.", + "0500_0100_0003_0004": "Not enough space in MicroSD Card; please clear some space.", + "0300_0200_0001_0006": "The nozzle temperature is abnormal; the sensor may have a short circuit. Please check whether the connector is properly plugged in.", + "0500_0300_0001_0021": "Hardware incompatible: please check the laser.", + "0500_0500_0001_0001": "The factory data of AP board is abnormal; please replace the AP board with a new one.", + "0500_0500_0003_0002": "The device is in the engineering state; please pay attention to the information security related matters.", + "0300_1800_0001_0005": "The Z axis motor got stuck while moving, or the extrusion force sensor may have an issue; please check if there is any foreign matter on the Z sliders or Z timing belt wheels.", + "0300_0200_0001_000B": "The nozzle temperature is abnormal. Temperature control system may have an issue.", + "0300_9100_0001_000C": "The chamber heater 1 has been working at full load for a long time. The temperature control system may have an issue.", + "0300_0100_0001_000D": "An issue occurred when heating the heatbed previously. To continue using your printer, please refer to the wiki to troubleshoot.", + "0500_0300_0001_0024": "The current temperature is too low. In order to protect you and your printer. Printing task, moving axis and other operations is disabled. Please move the printer to an environment above 10°C.", + "0500_0200_0002_0008": "Time synchronization failed.", + "0300_1A00_0002_0002": "The nozzle is clogged with filament.", + "0300_0F00_0001_0001": "Abnormal accelerometer data detected. Please try to restart the printer.", + "0300_0100_0001_0008": "An abnormality occurs during the heating process of the heatbed, the heating modules may be broken.", + "0300_0100_0001_000C": "The heatbed has worked at full load for a long time. The temperature control system may be abnormal.", + "0500_0200_0002_0006": "Streaming function error. Please check the network and try again. You can restart or update the printer if the issue persists.", + "0300_0D00_0002_0004": "The build plate is not placed properly. Please adjust it.", + "0300_0D00_0002_0005": "The build plate is not placed properly. Please adjust it.", + "0300_0D00_0002_0006": "The build plate is not placed properly. Please adjust it.", + "0300_0D00_0002_0007": "The build plate is not placed properly. Please adjust it.", + "0300_0D00_0002_0008": "The build plate is not placed properly. Please adjust it.", + "0300_0D00_0002_0009": "The build plate is not placed properly. Please adjust it.", + "0300_0D00_0002_000A": "The build plate is not placed properly. Please adjust it.", + "0300_0D00_0002_0003": "The build plate is not placed properly. Please adjust it.", + "1201_1000_0001_0001": "The AMS2 Slot1 motor has slipped. The extrusion wheel may be malfunctioning, or the filament may be too thin.", + "1202_1000_0001_0001": "The AMS3 Slot1 motor has slipped. The extrusion wheel may be malfunctioning, or the filament may be too thin.", + "1201_1000_0002_0002": "The AMS2 Slot1 motor is overloaded. The filament may be tangled or stuck.", + "1202_1000_0002_0002": "The AMS3 Slot1 motor is overloaded. The filament may be tangled or stuck.", + "1203_1000_0002_0002": "The AMS4 Slot1 motor is overloaded. The filament may be tangled or stuck.", + "1201_1100_0002_0002": "The AMS2 Slot2 motor is overloaded. The filament may be tangled or stuck.", + "1202_1100_0002_0002": "The AMS3 Slot2 motor is overloaded. The filament may be tangled or stuck.", + "1203_1100_0002_0002": "The AMS4 Slot2 motor is overloaded. The filament may be tangled or stuck.", + "1201_1200_0002_0002": "The AMS2 Slot3 motor is overloaded. The filament may be tangled or stuck.", + "1202_1200_0002_0002": "The AMS3 Slot3 motor is overloaded. The filament may be tangled or stuck.", + "1203_1200_0002_0002": "The AMS4 Slot3 motor is overloaded. The filament may be tangled or stuck.", + "1201_1300_0002_0002": "The AMS2 Slot4 motor is overloaded. The filament may be tangled or stuck.", + "1202_1300_0002_0002": "The AMS3 Slot4 motor is overloaded. The filament may be tangled or stuck.", + "1203_1300_0002_0002": "The AMS4 Slot4 motor is overloaded. The filament may be tangled or stuck.", + "1201_1000_0001_0003": "The AMS2 Slot1 motor torque control is malfunctioning. The current sensor may be faulty.", + "1202_1000_0001_0003": "The AMS3 Slot1 motor torque control is malfunctioning. The current sensor may be faulty.", + "1203_1000_0001_0003": "The AMS4 Slot1 motor torque control is malfunctioning. The current sensor may be faulty.", + "1200_1300_0002_0002": "The AMS1 Slot4 motor is overloaded. The filament may be tangled or stuck.", + "1200_4500_0002_0003": "The filament cutter handle has not released. The handle or blade may be stuck.", + "0C00_0300_0002_000F": "Parts skipped before first layer inspection; the inspection will not be supported for the current print.", + "0C00_0100_0002_0008": "Failed to get image from chamber camera. Spaghetti and waste chute pileup detection is not available for now.", + "0C00_0300_0003_0006": "Purged filament may have piled up in the waste chute. Please check and clean the chute.", + "1200_1000_0001_0001": "The AMS1 Slot1 motor has slipped. The extrusion wheel may be malfunctioning, or the filament may be too thin.", + "1203_1000_0001_0001": "The AMS4 Slot1 motor has slipped. The extrusion wheel may be malfunctioning, or the filament may be too thin.", + "1200_1000_0002_0002": "The AMS1 Slot1 motor is overloaded. The filament may be tangled or stuck.", + "1200_1100_0002_0002": "The AMS1 Slot2 motor is overloaded. The filament may be tangled or stuck.", + "1200_1200_0002_0002": "The AMS1 Slot3 motor is overloaded. The filament may be tangled or stuck.", + "1200_1000_0001_0003": "The AMS1 Slot1 motor torque control is malfunctioning. The current sensor may be faulty.", + "0300_1D00_0001_0001": "The position sensor of extrusion motor is abnormal. The sensor signal wire may be not properly plugged in.", + "0300_1C00_0001_0001": "The extrusion motor driver is abnormal. The MOSFET may have a short circuit.", + "0300_0100_0001_000A": "The hotbed temperature control is abnormal, the AC board may be broken.", + "0300_9100_0001_000A": "The temperature of chamber heater 1 is abnormal. The AC board may be broken.", + "0300_9200_0001_000A": "The temperature of chamber heater 2 is abnormal. The AC board may be broken.", + "0300_1700_0001_0001": "The speed of the nozzle fan is too slow or stopped. It may be stuck or the connector may not be plugged in properly.", + "0300_1700_0002_0002": "The speed of the nozzle fan is slow. It may be stuck and need cleaning.", + "0300_1800_0001_0002": "The sensitivity of the extrusion force sensor is low, the nozzle may not installed correctly.", + "0500_0400_0003_0009": "The bed temperature exceeds filament's vitrification temperature, which may cause nozzle clog. Please keep the front door of printer open. Already turning off the door opening detection temporarily.", + "0300_1B00_0001_0001": "The signal of the heatbed acceleration sensor is weak. The sensor may have fallen off or been damaged.", + "0300_1B00_0001_0002": "External disturbance was detected on the heatbed acceleeration sensor. The sensor signal wire may not be fixed.", + "0300_1B00_0001_0003": "The heatbed acceleration sensor detected unexpected continuous force. The sensor may be stuck, or the analog front end may be broken.", + "0300_0D00_0001_000B": "The Z axis motor seems to be stuck when moving. Please check if there is any foreign matter on the Z sliders or Z timing belt wheels .", + "0300_0D00_0002_0001": "Heatbed homing abnormal: there may be a bulge on the heatbed or the nozzle tip may not be clean.", + "0300_1000_0002_0002": "The resonance frequency of the X axis differs greatly from last calibration. Please clean the carbon rod and rerun the machine calibration afterward.", + "0300_1100_0002_0002": "The resonance frequency of the Y axis differs greatly from the last calibration. Please clean the carbon rod and rerun the machine calibration afterward.", + "12FF_2000_0002_0007": "Failed to check the filament location in the tool head; please click for more help.", + "1200_2000_0002_0006": "Failed to extrude AMS1 Slot1 filament; the extruder may be clogged or the filament may be too thin, causing the extruder to slip.", + "1201_2000_0002_0006": "Failed to extrude AMS2 Slot1 filament; the extruder may be clogged or the filament may be too thin, causing the extruder to slip.", + "1202_2000_0002_0006": "Failed to extrude AMS3 Slot1 filament; the extruder may be clogged or the filament may be too thin, causing the extruder to slip.", + "1203_2000_0002_0006": "Failed to extrude AMS4 Slot1 filament; the extruder may be clogged or the filament may be too thin, causing the extruder to slip.", + "1200_2100_0002_0006": "Failed to extrude AMS1 Slot2 filament; the extruder may be clogged or the filament may be too thin, causing the extruder to slip.", + "1201_2100_0002_0006": "Failed to extrude AMS2 Slot2 filament; the extruder may be clogged or the filament may be too thin, causing the extruder to slip.", + "1202_2100_0002_0006": "Failed to extrude AMS3 Slot2 filament; the extruder may be clogged or the filament may be too thin, causing the extruder to slip.", + "1203_2100_0002_0006": "Failed to extrude AMS4 Slot2 filament; the extruder may be clogged or the filament may be too thin, causing the extruder to slip.", + "1200_2200_0002_0006": "Failed to extrude AMS1 Slot3 filament; the extruder may be clogged or the filament may be too thin, causing the extruder to slip.", + "1201_2200_0002_0006": "Failed to extrude AMS2 Slot3 filament; the extruder may be clogged or the filament may be too thin, causing the extruder to slip.", + "1202_2200_0002_0006": "Failed to extrude AMS3 Slot3 filament; the extruder may be clogged or the filament may be too thin, causing the extruder to slip.", + "1203_2200_0002_0006": "Failed to extrude AMS4 Slot3 filament; the extruder may be clogged or the filament may be too thin, causing the extruder to slip.", + "1200_2300_0002_0006": "Failed to extrude AMS1 Slot4 filament; the extruder may be clogged or the filament may be too thin, causing the extruder to slip.", + "1201_2300_0002_0006": "Failed to extrude AMS2 Slot4 filament; the extruder may be clogged or the filament may be too thin, causing the extruder to slip.", + "1202_2300_0002_0006": "Failed to extrude AMS3 Slot4 filament; the extruder may be clogged or the filament may be too thin, causing the extruder to slip.", + "1203_2300_0002_0006": "Failed to extrude AMS4 Slot4 filament; the extruder may be clogged or the filament may be too thin, causing the extruder to slip.", + "0300_0200_0001_0009": "The nozzle temperature control is abnormal; the hot end may not be installed. If you want to heat the hot end without it being installed, please turn on maintenance mode on the settings page.", + "0300_9400_0002_0003": "Chamber failed to reach the desired temperature. The machine will stop waiting for the chamber temperature.", + "0500_0400_0002_0020": "", + "0300_9000_0001_0002": "Chamber heating failed. The chamber may not be enclosed, or the ambient temperature may be too low, or the heat dissipation vent of the power supply may be blocked.", + "0500_0300_0001_0004": "The AHB module is malfunctioning. Please restart the device.", + "0500_0300_0002_0020": "Micro SD Card capacity is insufficient to cache print files.", + "12FF_2000_0003_0007": "Checking the filament location of all AMS slots, please wait.", + "0300_9400_0003_0001": "Chamber cooling may be too slow. You can open the chamber to help cooling if the gas in chamber is non-toxic.", + "0300_1800_0001_0001": "The value of extrusion force sensor is low, the nozzle seems to not be installed.", + "0300_1800_0001_0003": "The extrusion force sensor is not available, the link between the MC and TH may be broken or the sensor is broken.", + "0300_1800_0001_0004": "The data from extrusion force sensor is abnormal, the sensor should be broken.", + "0300_1900_0001_0001": "The eddy current sensor on Y-axis is not available, the wire should be broken.", + "0300_1900_0002_0002": "The sensitivity of Y-axis eddy current sensor is too low.", + "0300_9400_0003_0002": "Chamber temperature setting value exceed the limit, the boundary value will be set.", + "0300_1A00_0002_0001": "The nozzle is covered with filaments, or the build plate is put in crooked.", + "0300_9000_0001_0010": "The communication of chamber temperature controller is abnormal.", + "0300_9100_0001_0008": "Chamber heater 1 failed to rise to target temperature.", + "0300_9300_0001_0006": "Chamber temperature is abnormal. The chamber heater's temperature sensor at the air inlet may have an open circuit.", + "0300_9300_0001_0007": "Chamber temperature is abnormal. The temperature sensor at the power supply may have a short circuit.", + "0300_9300_0001_0008": "Chamber temperature is abnormal. The temperature sensor at power supply may have an open circuit.", + "0300_9300_0001_0002": "Chamber temperature is abnormal. The chamber heater's temperature sensor may have an open circuit.", + "0300_9300_0001_0003": "Chamber temperature is abnormal. The chamber heater's temperature sensor at the air outlet may have a short circuit.", + "0300_9300_0001_0004": "Chamber temperature is abnormal. The chamber heater's temperature sensor at the air outlet may have an open circuit.", + "0300_9300_0001_0005": "Chamber temperature is abnormal. The chamber heater's temperature sensor at the air inlet may have a short circuit.", + "0300_9100_0001_0001": "The temperature of chamber heater 1 is abnormal. The heater may have a short circuit.", + "0300_9100_0001_0002": "The temperature of chamber heater 1 is abnormal. The heater may have an open circuit or the thermal fuse may have taken effect.", + "0300_9100_0001_0006": "The temperature of chamber heater 1 is abnormal. The sensor may have a short circuit.", + "0300_9100_0001_0007": "The temperature of chamber heater 1 is abnormal. The sensor may have an open circuit.", + "0300_9200_0001_0001": "The temperature of chamber heater 2 is abnormal. The heater may have a short circuit.", + "0300_9200_0001_0002": "The temperature of chamber heater 2 is abnormal. The heater may have an open circuit or the thermal fuse may be in effect.", + "0300_9200_0001_0006": "The temperature of chamber heater 2 is abnormal. The sensor may have a short circuit.", + "0300_9200_0001_0007": "The temperature of chamber heater 2 is abnormal. The sensor may have an open circuit.", + "0300_9200_0001_0008": "Chamber heater 2 failed to rise to target temperature.", + "0300_9300_0001_0001": "Chamber temperature is abnormal. The chamber heater's temperature sensor may have a short circuit.", + "0300_9000_0001_0001": "Chamber heating failed. The chamber heater may be failing to blow hot air.", + "0300_9000_0001_0003": "Chamber heating failed. The temperature of power supply may be too high.", + "0300_9000_0001_0004": "Chamber heating failed. The speed of the heating fan is too low.", + "0300_9000_0001_0005": "Chamber heating failed. The thermal resistance is too high.", + "0300_9100_0001_0003": "The temperature of chamber heater 1 is abnormal. The heater is over temperature.", + "0300_9200_0001_0003": "The temperature of chamber heater 2 is abnormal. The heater is over temperature.", + "0500_0300_0001_0023": "The CTC module is malfunctioning. Please restart the device.", + "1200_8000_0002_0001": "AMS1 Slot1 filament may be tangled or stuck.", + "1200_8100_0002_0001": "AMS1 Slot2 filament may be tangled or stuck.", + "1200_8200_0002_0001": "AMS1 Slot3 filament may be tangled or stuck.", + "1200_8300_0002_0001": "AMS1 Slot4 filament may be tangled or stuck.", + "1201_8000_0002_0001": "AMS2 Slot1 filament may be tangled or stuck.", + "1201_8100_0002_0001": "AMS2 Slot2 filament may be tangled or stuck.", + "1201_8200_0002_0001": "AMS2 Slot3 filament may be tangled or stuck.", + "1201_8300_0002_0001": "AMS2 Slot4 filament may be tangled or stuck.", + "1202_8000_0002_0001": "AMS3 Slot1 filament may be tangled or stuck.", + "1202_8100_0002_0001": "AMS3 Slot2 filament may be tangled or stuck.", + "1202_8200_0002_0001": "AMS3 Slot3 filament may be tangled or stuck.", + "1202_8300_0002_0001": "AMS3 Slot4 filament may be tangled or stuck.", + "1203_8000_0002_0001": "AMS4 Slot1 filament may be tangled or stuck.", + "1203_8100_0002_0001": "AMS4 Slot2 filament may be tangled or stuck.", + "1203_8200_0002_0001": "AMS4 Slot3 filament may be tangled or stuck.", + "1203_8300_0002_0001": "AMS4 Slot4 filament may be tangled or stuck.", + "12FF_8000_0002_0001": "The filament on the spool holder may be tangled or stuck.", + "0500_0400_0002_0014": "The RFID-tag on AMS2 Slot1 cannot be identified.", + "0500_0400_0002_0013": "The RFID-tag on AMS1 Slot4 cannot be identified.", + "0500_0400_0002_0010": "The RFID-tag on AMS1 Slot1 cannot be identified.", + "0500_0400_0002_0011": "The RFID-tag on AMS1 Slot2 cannot be identified.", + "0500_0400_0002_0012": "The RFID-tag on AMS1 Slot3 cannot be identified.", + "0500_0400_0002_0015": "The RFID-tag on AMS2 Slot2 cannot be identified.", + "0500_0400_0002_0016": "The RFID-tag on AMS2 Slot3 cannot be identified.", + "0500_0400_0002_0017": "The RFID-tag on AMS2 Slot4 cannot be identified.", + "0500_0400_0002_0018": "The RFID-tag on AMS3 Slot1 cannot be identified.", + "0500_0400_0002_0019": "The RFID-tag on AMS3 Slot2 cannot be identified.", + "0500_0400_0002_001A": "The RFID-tag on AMS3 Slot3 cannot be identified.", + "0500_0400_0002_001B": "The RFID-tag on AMS3 Slot4 cannot be identified.", + "0500_0400_0002_001C": "The RFID-tag on AMS4 Slot1 cannot be identified.", + "0500_0400_0002_001D": "The RFID-tag on AMS4 Slot2 cannot be identified.", + "0500_0400_0002_001E": "The RFID-tag on AMS4 Slot3 cannot be identified.", + "0500_0400_0002_001F": "The RFID-tag on AMS4 Slot4 cannot be identified.", + "0500_0300_0003_0022": "MicroSD Card performance degradation has been detected. It may affect print jobs, logs, and video records. Please try to format or change the MicroSD card.", + "0300_0300_0002_0002": "", + "1203_1100_0001_0001": "The AMS4 Slot2 motor has slipped. The extrusion wheel may be malfunctioning, or the filament may be too thin.", + "1203_1200_0001_0001": "The AMS4 Slot3 motor has slipped. The extrusion wheel may be malfunctioning, or the filament may be too thin.", + "1203_1300_0001_0001": "The AMS4 Slot4 motor has slipped. The extrusion wheel may be malfunctioning, or the filament may be too thin.", + "1203_1100_0001_0003": "The AMS4 Slot2 motor torque control is malfunctioning. The current sensor may be faulty.", + "1203_1200_0001_0003": "The AMS4 Slot3 motor torque control is malfunctioning. The current sensor may be faulty.", + "1203_1300_0001_0003": "The AMS4 Slot4 motor torque control is malfunctioning. The current sensor may be faulty.", + "1201_1100_0001_0001": "The AMS2 Slot2 motor has slipped. The extrusion wheel may be malfunctioning, or the filament may be too thin.", + "1202_1100_0001_0001": "The AMS3 Slot2 motor has slipped. The extrusion wheel may be malfunctioning, or the filament may be too thin.", + "1201_1200_0001_0001": "The AMS2 Slot3 motor has slipped. The extrusion wheel may be malfunctioning, or the filament may be too thin.", + "1202_1200_0001_0001": "The AMS3 Slot3 motor has slipped. The extrusion wheel may be malfunctioning, or the filament may be too thin.", + "1201_1300_0001_0001": "The AMS2 Slot4 motor has slipped. The extrusion wheel may be malfunctioning, or the filament may be too thin.", + "1202_1300_0001_0001": "The AMS3 Slot4 motor has slipped. The extrusion wheel may be malfunctioning, or the filament may be too thin.", + "1201_1100_0001_0003": "The AMS2 Slot2 motor torque control is malfunctioning. The current sensor may be faulty.", + "1202_1100_0001_0003": "The AMS3 Slot2 motor torque control is malfunctioning. The current sensor may be faulty.", + "1201_1200_0001_0003": "The AMS2 Slot3 motor torque control is malfunctioning. The current sensor may be faulty.", + "1202_1200_0001_0003": "The AMS3 Slot3 motor torque control is malfunctioning. The current sensor may be faulty.", + "1201_1300_0001_0003": "The AMS2 Slot4 motor torque control is malfunctioning. The current sensor may be faulty.", + "1202_1300_0001_0003": "The AMS3 Slot4 motor torque control is malfunctioning. The current sensor may be faulty.", + "12FF_2000_0002_0002": "Filament on the spool holder is empty; please insert a new filament.", + "1200_3000_0001_0001": "AMS1 Slot1 RFID coil is broken or the RF hardware circuit has an error.", + "1201_3000_0001_0001": "AMS2 Slot1 RFID coil is broken or the RF hardware circuit has an error.", + "1202_3000_0001_0001": "AMS3 Slot1 RFID coil is broken or the RF hardware circuit has an error.", + "1203_3000_0001_0001": "AMS4 Slot1 RFID coil is broken or the RF hardware circuit has an error.", + "1200_3100_0001_0001": "AMS1 Slot2 RFID coil is broken or the RF hardware circuit has an error.", + "1201_3100_0001_0001": "AMS2 Slot2 RFID coil is broken or the RF hardware circuit has an error.", + "1202_3100_0001_0001": "AMS3 Slot2 RFID coil is broken or the RF hardware circuit has an error.", + "1203_3100_0001_0001": "AMS4 Slot2 RFID coil is broken or the RF hardware circuit has an error.", + "1200_3200_0001_0001": "AMS1 Slot3 RFID coil is broken or the RF hardware circuit has an error.", + "1201_3200_0001_0001": "AMS2 Slot3 RFID coil is broken or the RF hardware circuit has an error.", + "1202_3200_0001_0001": "AMS3 Slot3 RFID coil is broken or the RF hardware circuit has an error.", + "1203_3200_0001_0001": "AMS4 Slot3 RFID coil is broken or the RF hardware circuit has an error.", + "1200_3300_0001_0001": "AMS1 Slot4 RFID coil is broken or the RF hardware circuit has an error.", + "1201_3300_0001_0001": "AMS2 Slot4 RFID coil is broken or the RF hardware circuit has an error.", + "1202_3300_0001_0001": "AMS3 Slot4 RFID coil is broken or the RF hardware circuit has an error.", + "1203_3300_0001_0001": "AMS4 Slot4 RFID coil is broken or the RF hardware circuit has an error.", + "1200_3000_0001_0004": "RFID cannot be read because of an encryption chip failure in AMS1.", + "1201_3000_0001_0004": "RFID cannot be read because of an encryption chip failure in AMS2.", + "1202_3000_0001_0004": "RFID cannot be read because of an encryption chip failure in AMS3.", + "1203_3000_0001_0004": "RFID cannot be read because of an encryption chip failure in AMS4.", + "1200_4500_0002_0001": "The filament cutter sensor is malfunctioning. Please check whether the connector is properly plugged in.", + "1200_5000_0002_0001": "AMS1 communication is abnormal; please check the connection cable.", + "1201_5000_0002_0001": "AMS2 communication is abnormal; please check the connection cable.", + "1202_5000_0002_0001": "AMS3 communication is abnormal; please check the connection cable.", + "1203_5000_0002_0001": "AMS4 communication is abnormal; please check the connection cable.", + "1200_5100_0003_0001": "AMS is disabled; please load filament from spool holder.", + "1201_2000_0002_0001": "AMS2 Slot1 filament has run out; please insert a new filament.", + "1202_2000_0002_0001": "AMS3 Slot1 filament has run out; please insert a new filament.", + "1203_2000_0002_0001": "AMS4 Slot1 filament has run out; please insert a new filament.", + "1200_2100_0002_0001": "AMS1 Slot2 filament has run out; please insert a new filament.", + "1201_2100_0002_0001": "AMS2 Slot2 filament has run out; please insert a new filament.", + "1202_2100_0002_0001": "AMS3 Slot2 filament has run out; please insert a new filament.", + "1203_2100_0002_0001": "AMS4 Slot2 filament has run out; please insert a new filament.", + "1200_2200_0002_0001": "AMS1 Slot3 filament has run out; please insert a new filament.", + "1201_2200_0002_0001": "AMS2 Slot3 filament has run out; please insert a new filament.", + "1202_2200_0002_0001": "AMS3 Slot3 filament has run out; please insert a new filament.", + "1203_2200_0002_0001": "AMS4 Slot3 filament has run out; please insert a new filament.", + "1200_2300_0002_0001": "AMS1 Slot4 filament has run out; please insert a new filament.", + "1201_2300_0002_0001": "AMS2 Slot4 filament has run out; please insert a new filament.", + "1202_2300_0002_0001": "AMS3 Slot4 filament has run out; please insert a new filament.", + "1203_2300_0002_0001": "AMS4 Slot4 filament has run out; please insert a new filament.", + "12FF_2000_0002_0001": "Filament at the spool holder has run out; please insert a new filament.", + "1201_2000_0002_0002": "AMS2 Slot1 is empty; please insert a new filament.", + "1202_2000_0002_0002": "AMS3 Slot1 is empty; please insert a new filament.", + "1203_2000_0002_0002": "AMS4 Slot1 is empty; please insert a new filament.", + "1200_2100_0002_0002": "AMS1 Slot2 is empty; please insert a new filament.", + "1201_2100_0002_0002": "AMS2 Slot2 is empty; please insert a new filament.", + "1202_2100_0002_0002": "AMS3 Slot2 is empty; please insert a new filament.", + "1203_2100_0002_0002": "AMS4 Slot2 is empty; please insert a new filament.", + "1200_2200_0002_0002": "AMS1 Slot3 is empty; please insert a new filament.", + "1201_2200_0002_0002": "AMS2 Slot3 is empty; please insert a new filament.", + "1202_2200_0002_0002": "AMS3 Slot3 is empty; please insert a new filament.", + "1203_2200_0002_0002": "AMS4 Slot3 is empty; please insert a new filament.", + "1200_2300_0002_0002": "AMS1 Slot4 is empty; please insert a new filament.", + "1201_2300_0002_0002": "AMS2 Slot4 is empty; please insert a new filament.", + "1202_2300_0002_0002": "AMS3 Slot4 is empty; please insert a new filament.", + "1203_2300_0002_0002": "AMS4 Slot4 is empty; please insert a new filament.", + "1203_2100_0002_0005": "AMS4 Slot2 filament has run out, and purging the old filament went abnormally; please check to see if filament is stuck in the toolhead.", + "1201_2000_0003_0001": "AMS2 Slot1 filament has run out. Purging the old filament; please wait.", + "1202_2000_0003_0001": "AMS3 Slot1 filament has run out. Purging the old filament; please wait.", + "1203_2000_0003_0001": "AMS4 Slot1 filament has run out. Purging the old filament; please wait.", + "1201_2100_0003_0001": "AMS2 Slot2 filament has run out. Purging the old filament; please wait.", + "1202_2100_0003_0001": "AMS3 Slot2 filament has run out. Purging the old filament; please wait.", + "1203_2100_0003_0001": "AMS4 Slot2 filament has run out. Purging the old filament; please wait.", + "1200_2200_0003_0001": "AMS1 Slot3 filament has run out. Purging the old filament; please wait.", + "1201_2200_0003_0001": "AMS2 Slot3 filament has run out. Purging the old filament; please wait.", + "1202_2200_0003_0001": "AMS3 Slot3 filament has run out. Purging the old filament; please wait.", + "1203_2200_0003_0001": "AMS4 Slot3 filament has run out. Purging the old filament; please wait.", + "1200_2300_0003_0001": "AMS1 Slot4 filament has run out. Purging the old filament; please wait.", + "1201_2300_0003_0001": "AMS2 Slot4 filament has run out. Purging the old filament; please wait.", + "1202_2300_0003_0001": "AMS3 Slot4 filament has run out. Purging the old filament; please wait.", + "1203_2300_0003_0001": "AMS4 Slot4 filament has run out. Purging the old filament; please wait.", + "1200_2000_0003_0001": "AMS1 Slot1 filament has run out. Purging the old filament; please wait.", + "1200_2100_0003_0001": "AMS1 Slot2 filament has run out. Purging the old filament; please wait.", + "1200_2300_0003_0002": "AMS1 Slot4 filament has run out and automatically switched to the slot with the same filament.", + "1201_2300_0003_0002": "AMS2 Slot4 filament has run out and automatically switched to the slot with the same filament.", + "1202_2300_0003_0002": "AMS3 Slot4 filament has run out and automatically switched to the slot with the same filament.", + "1203_2300_0003_0002": "AMS4 Slot4 filament has run out and automatically switched to the slot with the same filament.", + "1201_2000_0003_0002": "AMS2 Slot1 filament has run out and automatically switched to the slot with the same filament.", + "1202_2000_0003_0002": "AMS3 Slot1 filament has run out and automatically switched to the slot with the same filament.", + "1203_2000_0003_0002": "AMS4 Slot1 filament has run out and automatically switched to the slot with the same filament.", + "1200_2100_0003_0002": "AMS1 Slot2 filament has run out and automatically switched to the slot with the same filament.", + "1201_2100_0003_0002": "AMS2 Slot2 filament has run out and automatically switched to the slot with the same filament.", + "1202_2100_0003_0002": "AMS3 Slot2 filament has run out and automatically switched to the slot with the same filament.", + "1203_2100_0003_0002": "AMS4 Slot2 filament has run out and automatically switched to the slot with the same filament.", + "1200_2200_0003_0002": "AMS1 Slot3 filament has run out and automatically switched to the slot with the same filament.", + "1201_2200_0003_0002": "AMS2 Slot3 filament has run out and automatically switched to the slot with the same filament.", + "1202_2200_0003_0002": "AMS3 Slot3 filament has run out and automatically switched to the slot with the same filament.", + "1203_2200_0003_0002": "AMS4 Slot3 filament has run out and automatically switched to the slot with the same filament.", + "1202_2200_0002_0003": "AMS3 Slot3 filament may be broken in the PTFE tube.", + "1203_2200_0002_0003": "AMS4 Slot3 filament may be broken in the PTFE tube.", + "1200_2300_0002_0003": "AMS1 Slot4 filament may be broken in the PTFE tube.", + "1201_2300_0002_0003": "AMS2 Slot4 filament may be broken in the PTFE tube.", + "1202_2300_0002_0003": "AMS3 Slot4 filament may be broken in the PTFE tube.", + "1203_2300_0002_0003": "AMS4 Slot4 filament may be broken in the PTFE tube.", + "1200_2000_0002_0004": "AMS1 Slot1 filament may be broken in the tool head.", + "1201_2000_0002_0004": "AMS2 Slot1 filament may be broken in the tool head.", + "1202_2000_0002_0004": "AMS3 Slot1 filament may be broken in the tool head.", + "1203_2000_0002_0004": "AMS4 Slot1 filament may be broken in the tool head.", + "1200_2100_0002_0004": "AMS1 Slot2 filament may be broken in the tool head.", + "1201_2100_0002_0004": "AMS2 Slot2 filament may be broken in the tool head.", + "1202_2100_0002_0004": "AMS3 Slot2 filament may be broken in the tool head.", + "1203_2100_0002_0004": "AMS4 Slot2 filament may be broken in the tool head.", + "1200_2200_0002_0004": "AMS1 Slot3 filament may be broken in the tool head.", + "1201_2200_0002_0004": "AMS2 Slot3 filament may be broken in the tool head.", + "1202_2200_0002_0004": "AMS3 Slot3 filament may be broken in the tool head.", + "1203_2200_0002_0004": "AMS4 Slot3 filament may be broken in the tool head.", + "1200_2300_0002_0004": "AMS1 Slot4 filament may be broken in the tool head.", + "1201_2300_0002_0004": "AMS2 Slot4 filament may be broken in the tool head.", + "1202_2300_0002_0004": "AMS3 Slot4 filament may be broken in the tool head.", + "1203_2300_0002_0004": "AMS4 Slot4 filament may be broken in the tool head.", + "1200_2000_0003_0002": "AMS1 Slot1 filament has run out and automatically switched to the slot with the same filament.", + "1200_1100_0001_0001": "The AMS1 Slot2 motor has slipped. The extrusion wheel may be malfunctioning, or the filament may be too thin.", + "1200_1200_0001_0001": "The AMS1 Slot3 motor has slipped. The extrusion wheel may be malfunctioning, or the filament may be too thin.", + "1200_1300_0001_0001": "The AMS1 Slot4 motor has slipped. The extrusion wheel may be malfunctioning, or the filament may be too thin.", + "1200_1100_0001_0003": "The AMS1 Slot2 motor torque control is malfunctioning. The current sensor may be faulty.", + "1200_1200_0001_0003": "The AMS1 Slot3 motor torque control is malfunctioning. The current sensor may be faulty.", + "1200_1300_0001_0003": "The AMS1 Slot4 motor torque control is malfunctioning. The current sensor may be faulty.", + "1200_2000_0002_0003": "AMS1 Slot1 filament may be broken in the PTFE tube.", + "1201_2000_0002_0003": "AMS2 Slot1 filament may be broken in the PTFE tube.", + "1202_2000_0002_0003": "AMS3 Slot1 filament may be broken in the PTFE tube.", + "1203_2000_0002_0003": "AMS4 Slot1 filament may be broken in the PTFE tube.", + "1200_2100_0002_0003": "AMS1 Slot2 filament may be broken in the PTFE tube.", + "1201_2100_0002_0003": "AMS2 Slot2 filament may be broken in the PTFE tube.", + "1202_2100_0002_0003": "AMS3 Slot2 filament may be broken in the PTFE tube.", + "1203_2100_0002_0003": "AMS4 Slot2 filament may be broken in the PTFE tube.", + "1200_2200_0002_0003": "AMS1 Slot3 filament may be broken in the PTFE tube.", + "1201_2200_0002_0003": "AMS2 Slot3 filament may be broken in the PTFE tube.", + "1202_2200_0002_0005": "AMS3 Slot3 filament has run out, and purging the old filament went abnormally; please check to see if filament is stuck in the toolhead.", + "1203_2200_0002_0005": "AMS4 Slot3 filament has run out, and purging the old filament went abnormally; please check to see if filament is stuck in the toolhead.", + "1200_2300_0002_0005": "AMS1 Slot4 filament has run out, and purging the old filament went abnormally; please check to see if filament is stuck in the toolhead.", + "1201_2300_0002_0005": "AMS2 Slot4 filament has run out, and purging the old filament went abnormally; please check to see if filament is stuck in the toolhead.", + "1202_2300_0002_0005": "AMS3 Slot4 filament has run out, and purging the old filament went abnormally; please check to see if filament is stuck in the toolhead.", + "1203_2300_0002_0005": "AMS4 Slot4 filament has run out, and purging the old filament went abnormally; please check to see if filament is stuck in the toolhead.", + "1200_2000_0002_0001": "AMS1 Slot1 filament has run out; please insert a new filament.", + "1200_2000_0002_0002": "AMS1 Slot1 is empty; please insert a new filament.", + "1200_2000_0002_0005": "AMS1 Slot1 filament has run out, and purging the old filament went abnormally; please check to see if filament is stuck in the toolhead.", + "1201_2000_0002_0005": "AMS2 Slot1 filament has run out, and purging the old filament went abnormally; please check to see if filament is stuck in the toolhead.", + "1202_2000_0002_0005": "AMS3 Slot1 filament has run out, and purging the old filament went abnormally; please check to see if filament is stuck in the toolhead.", + "1203_2000_0002_0005": "AMS4 Slot1 filament has run out, and purging the old filament went abnormally; please check to see if filament is stuck in the toolhead.", + "1200_2100_0002_0005": "AMS1 Slot2 filament has run out, and purging the old filament went abnormally; please check to see if filament is stuck in the toolhead.", + "1201_2100_0002_0005": "AMS2 Slot2 filament has run out, and purging the old filament went abnormally; please check to see if filament is stuck in the toolhead.", + "1202_2100_0002_0005": "AMS3 Slot2 filament has run out, and purging the old filament went abnormally; please check to see if filament is stuck in the toolhead.", + "1200_2200_0002_0005": "AMS1 Slot3 filament has run out, and purging the old filament went abnormally; please check to see if filament is stuck in the toolhead.", + "1201_2200_0002_0005": "AMS2 Slot3 filament has run out, and purging the old filament went abnormally; please check to see if filament is stuck in the toolhead.", + "12FF_2000_0002_0005": "Filament may be broken in the tool head.", + "12FF_2000_0002_0006": "Failed to extrude the filament and the extruder may be clogged.", + "1200_3000_0002_0002": "The RFID-tag on AMS1 Slot1 is damaged.", + "1201_3000_0002_0002": "The RFID-tag on AMS2 Slot1 is damaged.", + "1202_3000_0002_0002": "The RFID-tag on AMS3 Slot1 is damaged.", + "1203_3000_0002_0002": "The RFID-tag on AMS4 Slot1 is damaged.", + "1200_3100_0002_0002": "The RFID-tag on AMS1 Slot2 is damaged.", + "1201_3100_0002_0002": "The RFID-tag on AMS2 Slot2 is damaged.", + "1202_3100_0002_0002": "The RFID-tag on AMS3 Slot2 is damaged.", + "1203_3100_0002_0002": "The RFID-tag on AMS4 Slot2 is damaged.", + "1200_3200_0002_0002": "The RFID-tag on AMS1 Slot3 is damaged.", + "1201_3200_0002_0002": "The RFID-tag on AMS2 Slot3 is damaged.", + "1202_3200_0002_0002": "The RFID-tag on AMS3 Slot3 is damaged.", + "1203_3200_0002_0002": "The RFID-tag on AMS4 Slot3 is damaged.", + "1200_3300_0002_0002": "The RFID-tag on AMS1 Slot4 is damaged.", + "1201_3300_0002_0002": "The RFID-tag on AMS2 Slot4 is damaged.", + "1202_3300_0002_0002": "The RFID-tag on AMS3 Slot4 is damaged.", + "1203_3300_0002_0002": "The RFID-tag on AMS4 Slot4 is damaged.", + "1200_3000_0003_0003": "AMS1 Slot1 RFID cannot be read because of a structural error.", + "1201_3000_0003_0003": "AMS2 Slot1 RFID cannot be read because of a structural error.", + "1202_3000_0003_0003": "AMS3 Slot1 RFID cannot be read because of a structural error.", + "1203_3000_0003_0003": "AMS4 Slot1 RFID cannot be read because of a structural error.", + "1200_3100_0003_0003": "AMS1 Slot2 RFID cannot be read because of a structural error.", + "1201_3100_0003_0003": "AMS2 Slot2 RFID cannot be read because of a structural error.", + "1202_3100_0003_0003": "AMS3 Slot2 RFID cannot be read because of a structural error.", + "1203_3100_0003_0003": "AMS4 Slot2 RFID cannot be read because of a structural error.", + "1200_3200_0003_0003": "AMS1 Slot3 RFID cannot be read because of a structural error.", + "1201_3200_0003_0003": "AMS2 Slot3 RFID cannot be read because of a structural error.", + "1202_3200_0003_0003": "AMS3 Slot3 RFID cannot be read because of a structural error.", + "1203_3200_0003_0003": "AMS4 Slot3 RFID cannot be read because of a structural error.", + "1200_3300_0003_0003": "AMS1 Slot4 RFID cannot be read because of a structural error.", + "1201_3300_0003_0003": "AMS2 Slot4 RFID cannot be read because of a structural error.", + "1202_3300_0003_0003": "AMS3 Slot4 RFID cannot be read because of a structural error.", + "1203_3300_0003_0003": "AMS4 Slot4 RFID cannot be read because of a structural error.", + "1200_4500_0002_0002": "The filament cutter's cutting distance is too large. The X motor may lose steps.", + "1200_7000_0001_0001": "AMS1 Filament speed and length error: The slot 1 filament odometry may be faulty.", + "1201_7000_0001_0001": "AMS2 Filament speed and length error: The slot 1 filament odometry may be faulty.", + "1202_7000_0001_0001": "AMS3 Filament speed and length error: The slot 1 filament odometry may be faulty.", + "1203_7000_0001_0001": "AMS4 Filament speed and length error: The slot 1 filament odometry may be faulty.", + "1200_7100_0001_0001": "AMS1 Filament speed and length error: The slot 2 filament odometry may be faulty.", + "1201_7100_0001_0001": "AMS2 Filament speed and length error: The slot 2 filament odometry may be faulty.", + "1202_7100_0001_0001": "AMS3 Filament speed and length error: The slot 2 filament odometry may be faulty.", + "1203_7100_0001_0001": "AMS4 Filament speed and length error: The slot 2 filament odometry may be faulty.", + "1200_7200_0001_0001": "AMS1 Filament speed and length error: The slot 3 filament odometry may be faulty.", + "1201_7200_0001_0001": "AMS2 Filament speed and length error: The slot 3 filament odometry may be faulty.", + "1202_7200_0001_0001": "AMS3 Filament speed and length error: The slot 3 filament odometry may be faulty.", + "1203_7200_0001_0001": "AMS4 Filament speed and length error: The slot 3 filament odometry may be faulty.", + "1200_7300_0001_0001": "AMS1 Filament speed and length error: The slot 4 filament odometry may be faulty.", + "1201_7300_0001_0001": "AMS2 Filament speed and length error: The slot 4 filament odometry may be faulty.", + "1202_7300_0001_0001": "AMS3 Filament speed and length error: The slot 4 filament odometry may be faulty.", + "1203_7300_0001_0001": "AMS4 Filament speed and length error: The slot 4 filament odometry may be faulty.", + "12FF_2000_0002_0004": "Please pull out the filament on the spool holder from the extruder.", + "1200_2400_0002_0001": "Filament may be broken in the tool head.", + "1200_2500_0002_0001": "Failed to extrude the filament and the extruder may be clogged.", + "0500_0100_0003_0007": "Unable to record time-lapse photography without MicroSD card inserted.", + "0500_0300_0002_0010": "forward coredump, it is recovering.", + "0500_0300_0002_0011": "upgrade coredump, it is recovering.", + "0500_0300_0002_0012": "ipcam coredump, it is recovering.", + "0500_0300_0002_0016": "device_manager coredump, it is recovering.", + "0500_0300_0002_0017": "recorder coredump, it is recovering.", + "0500_0300_0002_0018": "security coredump, it is recovering.", + "0500_0300_0002_0013": "xcam coredump, it is recovering.", + "0500_0300_0002_0014": "bbl_screen coredump, it is recovering.", + "0500_0300_0002_0015": "device_gate coredump, it is recovering.", + "0500_0400_0003_0008": "The door is detected to be open.", + "0500_0100_0003_0005": "MicroSD Card error: please reinsert, format or replace it.", + "0500_0100_0003_0006": "Unformatted MicroSD Card: please format it.", + "07FF_2000_0002_0001": "External filament has run out; please load a new filament.", + "07FF_2000_0002_0002": "External filament is missing; please load a new filament.", + "07FF_2000_0002_0004": "Please pull the external filament from the extruder.", + "0300_0300_0001_0001": "The speed of the nozzle fan is too slow or stopped. It may be stuck or the connector is not plugged in properly.", + "0300_0100_0003_0008": "The temperature of the heated bed exceeds the limit and automatically adjusts to the limit temperature.", + "0300_2000_0001_0003": "X axis homing abnormal: the timing belt may be loose.", + "0300_2000_0001_0004": "Y axis homing abnormal: the timing belt may be loose.", + "0C00_0300_0002_0004": "First layer inspection is not supported for the current print job.", + "0C00_0300_0003_000D": "Some objects may have fallen down, or the extruder is not extruding normally. Please check and decide if the printing should be stopped.", + "0C00_0300_0003_0008": "Possible spaghetti defects were detected. Please check the print quality and decide if the job should be stopped.", + "0500_0200_0002_0002": "Device login failed; please check your account information.", + "0500_0200_0002_0003": "Failed to connect internet; please check the network connection.", + "0500_0200_0002_0004": "Unauthorized user: please check your account information.", + "0500_0200_0002_0005": "Failed to connect internet; please check the network connection.", + "0500_0200_0002_0007": "Liveview service login failed; please check your network connection.", + "0500_0300_0001_0001": "The MC module is malfunctioning; please restart the device or check device cable connection.", + "0500_0300_0001_000A": "System state is abnormal; please restore factory settings.", + "0500_0300_0001_000B": "The screen is malfunctioning; please restart the device.", + "0300_2000_0001_0001": "X axis homing abnormal: please check if the tool head is stuck or the carbon rod has too much resistance.", + "0300_2000_0001_0002": "Y axis homing abnormal: please check if the tool head is stuck or the Y carriage has too much resistance.", + "0300_4000_0002_0001": "Data transmission over the serial port is abnormal; the software system may be faulty.", + "0300_4100_0001_0001": "The system voltage is unstable; triggering the power failure protection function.", + "0500_0200_0002_0001": "Failed to connect internet. Please check the network connection.", + "0300_0400_0002_0001": "The speed of the part cooling fan is too slow or stopped. It may be stuck or the connector is not plugged in properly.", + "0500_0100_0002_0002": "USB camera is not connected. Please check video camera cable connection.", + "0500_0300_0002_000C": "Wireless hardware error: please turn off/on WiFi or restart the device.", + "0C00_0300_0003_000B": "Inspecting the first layer: please wait a moment.", + "0C00_0200_0002_0004": "Nozzle height seems too low. Please check if the nozzle is worn or tilted. Re-calibrate Lidar if the nozzle has been replaced.", + "0C00_0300_0003_0007": "Possible first layer defects have been detected. Please check the first layer quality and decide if the job should be stopped.", + "0C00_0100_0001_000B": "Failed to calibrate Micro Lidar. Please make sure the calibration chart is clean and not occluded, and run machine calibration again.", + "0C00_0200_0002_0002": "The horizontal laser line is too wide. Please check if the heatbed is dirty.", + "0C00_0200_0002_0008": "The vertical laser line is too wide. Please check if the heatbed is dirty.", + "0300_0A00_0001_0001": "Heatbed force sensor 1 is too sensitive. It may be stuck between the strain arm and heatbed support, or the adjusting screw may be too tight.", + "0300_0A00_0001_0004": "An external disturbance was detected on force sensor 1. The heatbed plate may have touched something outside the heatbed.", + "0300_0A00_0001_0005": "Force sensor 1 detected unexpected continuous force. The heatbed may be stuck, or the analog front end may be broken.", + "0300_0B00_0001_0001": "Heatbed force sensor 2 is too sensitive. It may be stuck between the strain arm and heatbed support, or the adjusting screw may be too tight.", + "0300_0B00_0001_0004": "An external disturbance was detected on force sensor 2. The heatbed plate may have touched something outside the heatbed.", + "0300_0B00_0001_0005": "Force sensor 2 detected unexpected continuous force. The heatbed may be stuck, or the analog front end may be broken.", + "0300_0C00_0001_0001": "Heatbed force sensor 3 is too sensitive. It may be stuck between the strain arm and heatbed support, or the adjusting screw may be too tight.", + "0300_0C00_0001_0004": "An external disturbance was detected on force sensor 3. The heatbed plate may have touched something outside the heatbed.", + "0300_0C00_0001_0005": "Force sensor 3 detected unexpected continuous force. The heatbed may be stuck, or the analog front end may be broken.", + "0C00_0100_0001_000A": "The Micro Lidar LED may be broken.", + "0500_0400_0002_0007": "The bed temperature exceeds the filament's vitrification temperature, which may cause a nozzle clog. Please keep the front door of the printer open or lower the bed temperature.", + "0300_1000_0002_0001": "The resonance frequency of the X axis is low. The timing belt may be loose.", + "0300_1100_0002_0001": "The resonance frequency of the Y axis is low. The timing belt may be loose.", + "0300_0A00_0001_0002": "The signal of heatbed force sensor 1 is weak. The force sensor may be broken or have poor electric connection.", + "0300_0A00_0001_0003": "The signal of heatbed force sensor 1 is too weak. The electronic connection to the sensor may be broken.", + "0300_0B00_0001_0003": "The signal of heatbed force sensor 2 is too weak. The electronic connection to the sensor may be broken.", + "0300_0B00_0001_0002": "The signal of heatbed force sensor 2 is weak. The force sensor may be broken or have poor electric connection.", + "0300_0C00_0001_0002": "The signal of heatbed force sensor 3 is weak. The force sensor may be broken or have poor electric connection.", + "0300_0C00_0001_0003": "The signal of heatbed force sensor 3 is too weak. The electronic connection to the sensor may be broken.", + "0300_0600_0001_0001": "Motor-A has an open-circuit. There may be a loose connection, or the motor may have failed.", + "0300_0600_0001_0002": "Motor-A has a short-circuit. It may have failed.", + "0300_0700_0001_0001": "Motor-B has an open-circuit. The connection may be loose, or the motor may have failed.", + "0300_0700_0001_0002": "Motor-B has a short-circuit. It may have failed.", + "0300_0800_0001_0001": "Motor-Z has an open-circuit. The connection may be loose, or the motor may have failed.", + "0300_0800_0001_0002": "Motor-Z has a short-circuit. It may have failed.", + "0300_0900_0001_0001": "Motor-E has an open-circuit. The connection may be loose, or the motor may have failed.", + "0300_0900_0001_0002": "Motor-E has a short-circuit. It may have failed.", + "0C00_0100_0002_0007": "Micro Lidar laser parameters are drifted. Please re-calibrate your printer.", + "0C00_0300_0002_000C": "The build plate localization marker is not detected. Please check if the build plate is aligned correctly.", + "0300_0600_0001_0003": "The resistance of Motor-A is abnormal, the motor may have failed.", + "0300_0700_0001_0003": "The resistance of Motor-B is abnormal, the motor may have failed.", + "0300_0800_0001_0003": "The resistance of Motor-Z is abnormal, the motor may have failed.", + "0300_0900_0001_0003": "The resistance of Motor-E is abnormal, the motor may have failed.", + "0300_1300_0001_0001": "The current sensor of Motor-A is abnormal. This may be caused by a failure of the hardware sampling circuit.", + "0300_1400_0001_0001": "The current sensor of Motor-B is abnormal. This may be caused by a failure of the hardware sampling circuit.", + "0300_1500_0001_0001": "The current sensor of Motor-Z is abnormal. This may be caused by a failure of the hardware sampling circuit.", + "0300_1600_0001_0001": "The current sensor of Motor-E is abnormal. This may be caused by a failure of the hardware sampling circuit.", + "0C00_0200_0002_0009": "The vertical laser is not bright enough at homing position. Please clean or replace heatbed if this message appears repeatedly.", + "0C00_0300_0003_000E": "Your printer seems to be printing without extruding.", + "0C00_0300_0003_000F": "Your nozzle seems to be covered with jammed or clogged material.", + "0C00_0200_0002_0006": "Nozzle height seems too high. Please check if there is filament residual attached to the nozzle.", + "0C00_0300_0001_0009": "The first layer inspection module rebooted abnormally. The inspection result may be inaccurate.", + "0C00_0300_0001_000A": "Your printer is in factory mode. Please contact Technical Support.", + "0C00_0300_0002_0001": "Filament exposure metering failed because laser reflection is too weak on this material. First layer inspection may be inaccurate.", + "0C00_0300_0002_0002": "First layer inspection terminated due to abnormal Lidar data.", + "0C00_0300_0002_0005": "First layer inspection timed out abnormally, and the current results may be inaccurate.", + "0C00_0200_0002_0007": "The vertical laser is not lit. Please check if it's covered or hardware connection is normal.", + "0C00_0100_0001_0004": "The Micro Lidar camera lens seems to be dirty. Please clean the lens.", + "0C00_0100_0001_0005": "Micro Lidar camera OTP parameter is abnormal. Please contact after-sales.", + "0C00_0100_0001_0009": "The chamber camera lens seems to be dirty. Please clean the lens.", + "0C00_0100_0002_0002": "The Micro Lidar camera is malfunctioning and related functions may fail. Please contact after-sales if this message keeps appearing in multiple prints.", + "0C00_0100_0002_0006": "Micro Lidar camera extrinsic parameters are abnormal. Please enable flowrate calibration in your next print.", + "0C00_0200_0001_0001": "The horizontal laser is not lit. Please check if it's covered or hardware connection is normal.", + "0C00_0200_0001_0005": "A new Micro Lidar was detected. Please calibrate it on Calibration page before use.", + "0C00_0200_0002_0003": "The horizontal laser is not bright enough at homing position. Please clean or replace heatbed if this message appears repeatedly.", + "0C00_0100_0001_0001": "The Micro Lidar camera is offline. Please check the hardware connection.", + "0C00_0100_0001_0003": "Synchronization between the Micro Lidar camera and MCU is abnormal. Please restart your printer.", + "0500_0300_0001_0009": "A system hang occurred. It has been recovered by automatic restart.", + "0500_0300_0002_000D": "The SD Card controller is malfunctioning.", + "0500_0300_0003_0007": "A system panic occurred. It has been recovered by automatic restart.", + "0500_0400_0001_0001": "Failed to download print job. Please check your network connection.", + "0500_0400_0001_0002": "Failed to report print state. Please check your network connection.", + "0500_0400_0001_0003": "The content of print file is unreadable. Please resend the print job.", + "0500_0400_0001_0004": "The print file is unauthorized.", + "0500_0400_0001_0006": "Failed to resume previous print.", + "0500_0300_0001_0002": "The toolhead is malfunctioning. Please restart the device.", + "0500_0300_0001_0003": "The AMS module is malfunctioning. Please restart the device.", + "0500_0300_0001_0005": "Internal service is malfunctioning. Please restart the device.", + "0500_0300_0001_0006": "A system panic occurred. Please restart the device.", + "0500_0300_0001_0008": "A system hang occurred. Please restart the device.", + "0300_1200_0002_0001": "The front cover of the toolhead fell off.", + "0500_0100_0002_0001": "The media pipeline is malfunctioning.", + "0500_0100_0002_0003": "USB camera is malfunctioning.", + "0300_0D00_0001_0002": "Heatbed homing failed. The environmental vibration is too great.", + "0300_0D00_0001_0003": "The build plate is not placed properly. Please adjust it.", + "0300_0D00_0001_0004": "The build plate is not placed properly. Please adjust it.", + "0300_0D00_0001_0005": "The build plate is not placed properly. Please adjust it.", + "0300_0D00_0001_0006": "The build plate is not placed properly. Please adjust it.", + "0300_0D00_0001_0007": "The build plate is not placed properly. Please adjust it.", + "0300_0D00_0001_0008": "The build plate is not placed properly. Please adjust it.", + "0300_0D00_0001_0009": "The build plate is not placed properly. Please adjust it.", + "0300_0D00_0001_000A": "The build plate is not placed properly. Please adjust it.", + "0300_0100_0001_0001": "The heatbed temperature is abnormal; the heater may have a short circuit.", + "0300_0100_0001_0002": "The heatbed temperature is abnormal; the heater may have an open circuit, or the thermal switch may be open.", + "0300_0100_0001_0003": "The heatbed temperature is abnormal; the heater is over temperature.", + "0300_0200_0001_0001": "The nozzle temperature is abnormal; the heater may have a short circuit.", + "0300_0200_0001_0002": "The nozzle temperature is abnormal; the heater may have an open circuit.", + "0300_0200_0001_0003": "The nozzle temperature is abnormal; the heater is over temperature.", + "0300_0500_0001_0001": "The motor driver is overheating. Its radiator may be loose, or its cooling fan may be damaged.", + "0300_0100_0001_0006": "The heatbed temperature is abnormal; the sensor may have a short circuit.", + "0300_0100_0001_0007": "The heatbed temperature is abnormal; the sensor may have an open circuit.", + "0300_0200_0001_0007": "The nozzle temperature is abnormal; the sensor may have an open circuit.", +} +# UNIQUE_ID=wy2WtJ2q + +# These errors cover those that are AMS and/or slot specific. +# 070X_xYxx_xxxx_xxxx = AMS X (0 based index) Slot Y (0 based index) has the error. +# UNIQUE_ID=dxeWW5n6 +HMS_AMS_ERRORS = { + "0700_3000_0002_0002": "The RFID-tag on AMS1 Slot1 is damaged or the it's content cannot be identified.", + "0700_3100_0002_0002": "The RFID-tag on AMS1 Slot2 is damaged or the it's content cannot be identified.", + "0700_3200_0002_0002": "The RFID-tag on AMS1 Slot3 is damaged or the it's content cannot be identified.", + "0700_3300_0002_0002": "The RFID-tag on AMS1 Slot4 is damaged or the it's content cannot be identified.", + "0701_3000_0002_0002": "The RFID-tag on AMS2 Slot1 is damaged or the it's content cannot be identified.", + "0701_3100_0002_0002": "The RFID-tag on AMS2 Slot2 is damaged or the it's content cannot be identified.", + "0701_3200_0002_0002": "The RFID-tag on AMS2 Slot3 is damaged or the it's content cannot be identified.", + "0701_3300_0002_0002": "The RFID-tag on AMS2 Slot4 is damaged or the it's content cannot be identified.", + "0702_3000_0002_0002": "The RFID-tag on AMS3 Slot1 is damaged or the it's content cannot be identified.", + "0702_3100_0002_0002": "The RFID-tag on AMS3 Slot2 is damaged or the it's content cannot be identified.", + "0702_3200_0002_0002": "The RFID-tag on AMS3 Slot3 is damaged or the it's content cannot be identified.", + "0702_3300_0002_0002": "The RFID-tag on AMS3 Slot4 is damaged or the it's content cannot be identified.", + "0703_3000_0002_0002": "The RFID-tag on AMS4 Slot1 is damaged or the it's content cannot be identified.", + "0703_3100_0002_0002": "The RFID-tag on AMS4 Slot2 is damaged or the it's content cannot be identified.", + "0703_3200_0002_0002": "The RFID-tag on AMS4 Slot3 is damaged or the it's content cannot be identified.", + "0703_3300_0002_0002": "The RFID-tag on AMS4 Slot4 is damaged or the it's content cannot be identified.", + "0700_4000_0002_0004": "The filament buffer signal is abnormal; the spring may be stuck or the filament may be tangle.", + "0700_2000_0002_0002": "AMS1 Slot1 is empty; please load a new filament.", + "0700_2100_0002_0002": "AMS1 Slot2 is empty; please load a new filament.", + "0700_2200_0002_0002": "AMS1 Slot3 is empty; please load a new filament.", + "0700_2300_0002_0002": "AMS1 Slot4 is empty; please load a new filament.", + "0701_2000_0002_0002": "AMS2 Slot1 is empty; please load a new filament.", + "0701_2100_0002_0002": "AMS2 Slot2 is empty; please load a new filament.", + "0701_2200_0002_0002": "AMS2 Slot3 is empty; please load a new filament.", + "0701_2300_0002_0002": "AMS2 Slot4 is empty; please load a new filament.", + "0702_2000_0002_0002": "AMS3 Slot1 is empty; please load a new filament.", + "0702_2100_0002_0002": "AMS3 Slot2 is empty; please load a new filament.", + "0702_2200_0002_0002": "AMS3 Slot3 is empty; please load a new filament.", + "0702_2300_0002_0002": "AMS3 Slot4 is empty; please load a new filament.", + "0703_2000_0002_0002": "AMS4 Slot1 is empty; please load a new filament.", + "0703_2100_0002_0002": "AMS4 Slot2 is empty; please load a new filament.", + "0703_2200_0002_0002": "AMS4 Slot3 is empty; please load a new filament.", + "0703_2300_0002_0002": "AMS4 Slot4 is empty; please load a new filament.", + "0700_2000_0003_0001": "AMS1 Slot1 filament has run out. Please wait while old filament is purged.", + "0700_2100_0003_0001": "AMS1 Slot2 filament has run out. Please wait while old filament is purged.", + "0700_2200_0003_0001": "AMS1 Slot3 filament has run out. Please wait while old filament is purged.", + "0700_2000_0003_0002": "AMS1 Slot1 filament has run out and automatically switched to the slot with the same filament.", + "0700_2100_0003_0002": "AMS1 Slot2 filament has run out and automatically switched to the slot with the same filament.", + "0700_2200_0003_0002": "AMS1 Slot3 filament has run out and automatically switched to the slot with the same filament.", + "0702_5000_0002_0001": "AMS3 communication is abnormal; please check the connection cable.", + "0703_5000_0002_0001": "AMS4 communication is abnormal; please check the connection cable.", + "0700_6000_0002_0001": "The AMS1 slot1 is overloaded. The filament may be tangled or the spool may be stuck.", + "0701_6000_0002_0001": "The AMS2 slot1 is overloaded. The filament may be tangled or the spool may be stuck.", + "0702_6000_0002_0001": "The AMS3 slot1 is overloaded. The filament may be tangled or the spool may be stuck.", + "0703_6000_0002_0001": "The AMS4 slot1 is overloaded. The filament may be tangled or the spool may be stuck.", + "0700_6100_0002_0001": "The AMS1 slot2 is overloaded. The filament may be tangled or the spool may be stuck.", + "0701_6100_0002_0001": "The AMS2 slot2 is overloaded. The filament may be tangled or the spool may be stuck.", + "0702_6100_0002_0001": "The AMS3 slot2 is overloaded. The filament may be tangled or the spool may be stuck.", + "0703_6100_0002_0001": "The AMS4 slot2 is overloaded. The filament may be tangled or the spool may be stuck.", + "0700_6200_0002_0001": "The AMS1 slot3 is overloaded. The filament may be tangled or the spool may be stuck.", + "0701_6200_0002_0001": "The AMS2 slot3 is overloaded. The filament may be tangled or the spool may be stuck.", + "0702_6200_0002_0001": "The AMS3 slot3 is overloaded. The filament may be tangled or the spool may be stuck.", + "0703_6200_0002_0001": "The AMS4 slot3 is overloaded. The filament may be tangled or the spool may be stuck.", + "0700_6300_0002_0001": "The AMS1 slot4 is overloaded. The filament may be tangled or the spool may be stuck.", + "0701_6300_0002_0001": "The AMS2 slot4 is overloaded. The filament may be tangled or the spool may be stuck.", + "0702_6300_0002_0001": "The AMS3 slot4 is overloaded. The filament may be tangled or the spool may be stuck.", + "0703_6300_0002_0001": "The AMS4 slot4 is overloaded. The filament may be tangled or the spool may be stuck.", + "0700_2000_0002_0004": "AMS1 Slot1 filament may be broken in the tool head.", + "0702_2000_0002_0004": "AMS3 Slot1 filament may be broken in the tool head.", + "0703_2000_0002_0004": "AMS4 Slot1 filament may be broken in the tool head.", + "0700_2100_0002_0004": "AMS1 Slot2 filament may be broken in the tool head.", + "0701_2100_0002_0004": "AMS2 Slot2 filament may be broken in the tool head.", + "0702_2100_0002_0004": "AMS3 Slot2 filament may be broken in the tool head.", + "0703_2100_0002_0004": "AMS4 Slot2 filament may be broken in the tool head.", + "0700_2200_0002_0004": "AMS1 Slot3 filament may be broken in the tool head.", + "0701_2200_0002_0004": "AMS2 Slot3 filament may be broken in the tool head.", + "0702_2200_0002_0004": "AMS3 Slot3 filament may be broken in the tool head.", + "0703_2200_0002_0004": "AMS4 Slot3 filament may be broken in the tool head.", + "0700_2300_0002_0004": "AMS1 Slot4 filament may be broken in the tool head.", + "0701_2300_0002_0004": "AMS2 Slot4 filament may be broken in the tool head.", + "0702_2300_0002_0004": "AMS3 Slot4 filament may be broken in the tool head.", + "0703_2300_0002_0004": "AMS4 Slot4 filament may be broken in the tool head.", + "0702_2000_0002_0005": "AMS3 Slot1 filament has run out, and purging the old filament went abnormally; please check whether the filament is stuck in the tool head.", + "0703_2000_0002_0005": "AMS4 Slot1 filament has run out, and purging the old filament went abnormally; please check whether the filament is stuck in the tool head.", + "0702_2100_0002_0005": "AMS3 Slot2 filament has run out, and purging the old filament went abnormally; please check whether the filament is stuck in the tool head.", + "0703_2100_0002_0005": "AMS4 Slot2 filament has run out, and purging the old filament went abnormally; please check whether the filament is stuck in the tool head.", + "0702_2200_0002_0005": "AMS3 Slot3 filament has run out, and purging the old filament went abnormally; please check whether the filament is stuck in the tool head.", + "0703_2200_0002_0005": "AMS4 Slot3 filament has run out, and purging the old filament went abnormally; please check whether the filament is stuck in the tool head.", + "0702_2300_0002_0005": "AMS3 Slot4 filament has run out, and purging the old filament went abnormally; please check whether the filament is stuck in the tool head.", + "0703_2300_0002_0005": "AMS4 Slot4 filament has run out, and purging the old filament went abnormally; please check whether the filament is stuck in the tool head.", + "0701_2000_0003_0002": "AMS2 Slot1 filament has run out and automatically switched to the slot with the same filament.", + "0702_2000_0003_0002": "AMS3 Slot1 filament has run out and automatically switched to the slot with the same filament.", + "0703_2000_0003_0002": "AMS4 Slot1 filament has run out and automatically switched to the slot with the same filament.", + "0701_2100_0003_0002": "AMS2 Slot2 filament has run out and automatically switched to the slot with the same filament.", + "0702_2100_0003_0002": "AMS3 Slot2 filament has run out and automatically switched to the slot with the same filament.", + "0703_2100_0003_0002": "AMS4 Slot2 filament has run out and automatically switched to the slot with the same filament.", + "0701_2200_0003_0002": "AMS2 Slot3 filament has run out and automatically switched to the slot with the same filament.", + "0702_2200_0003_0002": "AMS3 Slot3 filament has run out and automatically switched to the slot with the same filament.", + "0703_2200_0003_0002": "AMS4 Slot3 filament has run out and automatically switched to the slot with the same filament.", + "0700_2300_0003_0002": "AMS1 Slot4 filament has run out and automatically switched to the slot with the same filament.", + "0701_2300_0003_0002": "AMS2 Slot4 filament has run out and automatically switched to the slot with the same filament.", + "0702_2300_0003_0002": "AMS3 Slot4 filament has run out and automatically switched to the slot with the same filament.", + "0703_2300_0003_0002": "AMS4 Slot4 filament has run out and automatically switched to the slot with the same filament.", + "0700_4500_0002_0001": "The filament cutter sensor is malfunctioning; please check whether the connector is properly plugged in.", + "0700_5000_0002_0001": "AMS1 communication is abnormal; please check the connection cable.", + "0701_5000_0002_0001": "AMS2 communication is abnormal; please check the connection cable.", + "0701_2000_0002_0004": "AMS2 Slot1 filament may be broken in the tool head.", + "0700_2000_0002_0005": "AMS1 Slot1 filament has run out, and purging the old filament went abnormally; please check whether the filament is stuck in the tool head.", + "0701_2000_0002_0005": "AMS2 Slot1 filament has run out, and purging the old filament went abnormally; please check whether the filament is stuck in the tool head.", + "0700_2100_0002_0005": "AMS1 Slot2 filament has run out, and purging the old filament went abnormally; please check whether the filament is stuck in the tool head.", + "0701_2100_0002_0005": "AMS2 Slot2 filament has run out, and purging the old filament went abnormally; please check whether the filament is stuck in the tool head.", + "0700_2200_0002_0005": "AMS1 Slot3 filament has run out, and purging the old filament went abnormally; please check whether the filament is stuck in the tool head.", + "0701_2200_0002_0005": "AMS2 Slot3 filament has run out, and purging the old filament went abnormally; please check whether the filament is stuck in the tool head.", + "0700_2300_0002_0005": "AMS1 Slot4 filament has run out, and purging the old filament went abnormally; please check whether the filament is stuck in the tool head.", + "0701_2300_0002_0005": "AMS2 Slot4 filament has run out, and purging the old filament went abnormally; please check whether the filament is stuck in the tool head.", + "0700_5100_0003_0001": "The AMS is disabled; please load filament from spool holder.", + "0700_2000_0002_0001": "AMS1 Slot1 filament has run out. Please insert a new filament.", + "0700_2000_0002_0003": "AMS1 Slot1's filament may be broken in AMS.", + "0700_2100_0002_0001": "AMS1 Slot2 filament has run out. Please insert a new filament.", + "0700_2100_0002_0003": "AMS1 Slot2's filament may be broken in AMS.", + "0700_2200_0002_0001": "AMS1 Slot3 filament has run out. Please insert a new filament.", + "0700_2200_0002_0003": "AMS1 Slot3's filament may be broken in AMS.", + "0700_2300_0002_0001": "AMS1 Slot4 filament has run out. Please insert a new filament.", + "0700_2300_0002_0003": "AMS1 Slot4's filament may be broken in AMS.", + "0701_2000_0002_0001": "AMS2 Slot1 filament has run out. Please insert a new filament.", + "0701_2000_0002_0003": "AMS2 Slot1's filament may be broken in AMS.", + "0701_2100_0002_0001": "AMS2 Slot2 filament has run out. Please insert a new filament.", + "0701_2100_0002_0003": "AMS2 Slot2's filament may be broken in AMS.", + "0701_2200_0002_0001": "AMS2 Slot3 filament has run out. Please insert a new filament.", + "0701_2200_0002_0003": "AMS2 Slot3's filament may be broken in AMS.", + "0701_2300_0002_0001": "AMS2 Slot4 filament has run out. Please insert a new filament.", + "0701_2300_0002_0003": "AMS2 Slot4's filament may be broken in AMS.", + "0702_2000_0002_0001": "AMS3 Slot1 filament has run out. Please insert a new filament.", + "0702_2000_0002_0003": "AMS3 Slot1's filament may be broken in AMS.", + "0702_2100_0002_0001": "AMS3 Slot2 filament has run out. Please insert a new filament.", + "0702_2100_0002_0003": "AMS3 Slot2's filament may be broken in AMS.", + "0702_2200_0002_0001": "AMS3 Slot3 filament has run out. Please insert a new filament.", + "0702_2200_0002_0003": "AMS3 Slot3's filament may be broken in AMS.", + "0702_2300_0002_0001": "AMS3 Slot4 filament has run out. Please insert a new filament.", + "0702_2300_0002_0003": "AMS3 Slot4's filament may be broken in AMS.", + "0703_2000_0002_0001": "AMS4 Slot1 filament has run out. Please insert a new filament.", + "0703_2000_0002_0003": "AMS4 Slot1's filament may be broken in AMS.", + "0703_2100_0002_0001": "AMS4 Slot2 filament has run out. Please insert a new filament.", + "0703_2100_0002_0003": "AMS4 Slot2's filament may be broken in AMS.", + "0703_2200_0002_0001": "AMS4 Slot3 filament has run out. Please insert a new filament.", + "0703_2200_0002_0003": "AMS4 Slot3's filament may be broken in AMS.", + "0703_2300_0002_0001": "AMS4 Slot4 filament has run out. Please insert a new filament.", + "0703_2300_0002_0003": "AMS4 Slot4's filament may be broken in AMS.", + "0701_2000_0003_0001": "AMS2 Slot1 filament has run out. Please wait while old filament is purged.", + "0702_2000_0003_0001": "AMS3 Slot1 filament has run out. Please wait while old filament is purged.", + "0703_2000_0003_0001": "AMS4 Slot1 filament has run out. Please wait while old filament is purged.", + "0701_2100_0003_0001": "AMS2 Slot2 filament has run out. Please wait while old filament is purged.", + "0702_2100_0003_0001": "AMS3 Slot2 filament has run out. Please wait while old filament is purged.", + "0703_2100_0003_0001": "AMS4 Slot2 filament has run out. Please wait while old filament is purged.", + "0701_2200_0003_0001": "AMS2 Slot3 filament has run out. Please wait while old filament is purged.", + "0702_2200_0003_0001": "AMS3 Slot3 filament has run out. Please wait while old filament is purged.", + "0703_2200_0003_0001": "AMS4 Slot3 filament has run out. Please wait while old filament is purged.", + "0700_2300_0003_0001": "AMS1 Slot4 filament has run out. Please wait while old filament is purged.", + "0701_2300_0003_0001": "AMS2 Slot4 filament has run out. Please wait while old filament is purged.", + "0702_2300_0003_0001": "AMS3 Slot4 filament has run out. Please wait while old filament is purged.", + "0703_2300_0003_0001": "AMS4 Slot4 filament has run out. Please wait while old filament is purged.", + "0703_3000_0003_0003": "RFID cannot be read because of a hardware or structural error.", + "0703_3100_0001_0001": "The RFID board between AMS4 Slot3 & Slot4 has an error.", + "0703_3100_0001_0004": "Encryption chip failure.", + "0703_3100_0003_0003": "RFID cannot be read because of a hardware or structural error.", + "0703_3500_0001_0001": "The temperature and humidity sensor has an error. The chip may be faulty.", + "0703_1300_0002_0002": "The AMS4 slot4 motor is overloaded. The filament may be tangled or stuck.", + "0703_3000_0001_0001": "The RFID board between AMS4 Slot1 & Slot2 has an error.", + "0703_3000_0001_0004": "Encryption chip failure.", + "0703_0100_0001_0003": "The AMS4 assist motor torque control is malfunctioning. The current sensor may be faulty.", + "0703_0100_0001_0004": "The AMS4 assist motor speed control is malfunctioning. The speed sensor may be faulty.", + "0703_0100_0002_0002": "The AMS4 assist motor is overloaded. The filament may be tangled or stuck.", + "0703_0200_0001_0001": "AMS4 Filament speed and length error: The filament odometry may be faulty.", + "0703_1000_0001_0001": "The AMS4 slot1 motor has slipped. The extrusion wheel may be malfunctioning, or the filament may be too thin.", + "0703_1000_0001_0003": "The AMS4 slot1 motor torque control is malfunctioning. The current sensor may be faulty.", + "0703_1000_0002_0002": "The AMS4 slot1 motor is overloaded. The filament may be tangled or stuck.", + "0703_1100_0001_0001": "The AMS4 slot2 motor has slipped. The extrusion wheel may be malfunctioning, or the filament may be too thin.", + "0703_1100_0001_0003": "The AMS4 slot2 motor torque control is malfunctioning. The current sensor may be faulty.", + "0703_1100_0002_0002": "The AMS4 slot2 motor is overloaded. The filament may be tangled or stuck.", + "0703_1200_0001_0001": "The AMS4 slot3 motor has slipped. The extrusion wheel may be malfunctioning, or the filament may be too thin.", + "0703_1200_0001_0003": "The AMS4 slot3 motor torque control is malfunctioning. The current sensor may be faulty.", + "0703_1200_0002_0002": "The AMS4 slot3 motor is overloaded. The filament may be tangled or stuck.", + "0703_1300_0001_0001": "The AMS4 slot4 motor has slipped. The extrusion wheel may be malfunctioning, or the filament may be too thin.", + "0703_1300_0001_0003": "The AMS4 slot4 motor torque control is malfunctioning. The current sensor may be faulty.", + "0702_3000_0003_0003": "RFID cannot be read because of a hardware or structural error.", + "0702_3100_0001_0001": "The RFID board between AMS3 Slot3 & Slot4 has an error.", + "0702_3100_0001_0004": "Encryption chip failure.", + "0702_3100_0003_0003": "RFID cannot be read because of a hardware or structural error.", + "0702_3500_0001_0001": "The temperature and humidity sensor has an error. The chip may be faulty.", + "0703_0100_0001_0001": "The AMS4 assist motor has slipped. The extrusion wheel may be worn down, or the filament may be too thin.", + "0702_3000_0001_0001": "The RFID board between AMS3 Slot1 & Slot2 has an error.", + "0702_3000_0001_0004": "Encryption chip failure.", + "0702_0100_0002_0002": "The AMS3 assist motor is overloaded. The filament may be tangled or stuck.", + "0702_0200_0001_0001": "AMS3 Filament speed and length error: The filament odometry may be faulty.", + "0702_1000_0001_0001": "The AMS3 slot1 motor has slipped. The extrusion wheel may be malfunctioning, or the filament may be too thin.", + "0702_1000_0001_0003": "The AMS3 slot1 motor torque control is malfunctioning. The current sensor may be faulty.", + "0702_1000_0002_0002": "The AMS3 slot1 motor is overloaded. The filament may be tangled or stuck.", + "0702_1100_0001_0001": "The AMS3 slot2 motor has slipped. The extrusion wheel may be malfunctioning, or the filament may be too thin.", + "0702_1100_0001_0003": "The AMS3 slot2 motor torque control is malfunctioning. The current sensor may be faulty.", + "0702_1100_0002_0002": "The AMS3 slot2 motor is overloaded. The filament may be tangled or stuck.", + "0702_1200_0001_0001": "The AMS3 slot3 motor has slipped. The extrusion wheel may be malfunctioning, or the filament may be too thin.", + "0702_1200_0001_0003": "The AMS3 slot3 motor torque control is malfunctioning. The current sensor may be faulty.", + "0702_1200_0002_0002": "The AMS3 slot3 motor is overloaded. The filament may be tangled or stuck.", + "0702_1300_0001_0001": "The AMS3 slot4 motor has slipped. The extrusion wheel may be malfunctioning, or the filament may be too thin.", + "0702_1300_0001_0003": "The AMS3 slot4 motor torque control is malfunctioning. The current sensor may be faulty.", + "0702_1300_0002_0002": "The AMS3 slot4 motor is overloaded. The filament may be tangled or stuck.", + "0701_3100_0001_0001": "The RFID board between AMS2 Slot3 & Slot4 has an error.", + "0701_3100_0001_0004": "Encryption chip failure.", + "0701_3100_0003_0003": "RFID cannot be read because of a hardware or structural error.", + "0701_3500_0001_0001": "The temperature and humidity sensor has an error. The chip may be faulty.", + "0702_0100_0001_0001": "The AMS3 assist motor has slipped. The extrusion wheel may be worn down, or the filament may be too thin.", + "0702_0100_0001_0003": "The AMS3 assist motor torque control is malfunctioning. The current sensor may be faulty.", + "0702_0100_0001_0004": "The AMS3 assist motor speed control is malfunctioning. The speed sensor may be faulty.", + "0701_3000_0001_0001": "The RFID board between AMS2 Slot1 & Slot2 has an error.", + "0701_3000_0001_0004": "Encryption chip failure.", + "0701_3000_0003_0003": "RFID cannot be read because of a hardware or structural error.", + "0701_0100_0002_0002": "The AMS2 assist motor is overloaded. The filament may be tangled or stuck.", + "0701_0200_0001_0001": "AMS2 Filament speed and length error: The filament odometry may be faulty.", + "0701_1000_0001_0001": "The AMS2 slot1 motor has slipped. The extrusion wheel may be malfunctioning, or the filament may be too thin.", + "0701_1000_0001_0003": "The AMS2 slot1 motor torque control is malfunctioning. The current sensor may be faulty.", + "0701_1000_0002_0002": "The AMS2 slot1 motor is overloaded. The filament may be tangled or stuck.", + "0701_1100_0001_0001": "The AMS2 slot2 motor has slipped. The extrusion wheel may be malfunctioning, or the filament may be too thin.", + "0701_1100_0001_0003": "The AMS2 slot2 motor torque control is malfunctioning. The current sensor may be faulty.", + "0701_1100_0002_0002": "The AMS2 slot2 motor is overloaded. The filament may be tangled or stuck.", + "0701_1200_0001_0001": "The AMS2 slot3 motor has slipped. The extrusion wheel may be malfunctioning, or the filament may be too thin.", + "0701_1200_0001_0003": "The AMS2 slot3 motor torque control is malfunctioning. The current sensor may be faulty.", + "0701_1200_0002_0002": "The AMS2 slot3 motor is overloaded. The filament may be tangled or stuck.", + "0701_1300_0001_0001": "The AMS2 slot4 motor has slipped. The extrusion wheel may be malfunctioning, or the filament may be too thin.", + "0701_1300_0001_0003": "The AMS2 slot4 motor torque control is malfunctioning. The current sensor may be faulty.", + "0701_1300_0002_0002": "The AMS2 slot4 motor is overloaded. The filament may be tangled or stuck.", + "0700_3100_0001_0004": "Encryption chip failure.", + "0700_3100_0003_0003": "RFID cannot be read because of a hardware or structural error.", + "0700_3500_0001_0001": "The temperature and humidity sensor has an error. The chip may be faulty.", + "0700_4000_0002_0001": "The filament buffer position signal lost: the cable or position sensor may be malfunctioning.", + "0700_4000_0002_0002": "The filament buffer position signal error: the position sensor may be malfunctioning.", + "0700_4000_0002_0003": "The AMS Hub communication is abnormal, the cable may be not well connected.", + "0700_4500_0002_0002": "The filament cutter's cutting distance is too large. The XY motor may lose steps.", + "0700_4500_0002_0003": "The filament cutter handle has not released. The handle or blade may be stuck.", + "0701_0100_0001_0001": "The AMS2 assist motor has slipped. The extrusion wheel may be worn down, or the filament may be too thin.", + "0701_0100_0001_0003": "The AMS2 assist motor torque control is malfunctioning. The current sensor may be faulty.", + "0701_0100_0001_0004": "The AMS2 assist motor speed control is malfunctioning. The speed sensor may be faulty.", + "0700_3000_0001_0001": "The RFID board between AMS1 Slot1 & Slot2 has an error.", + "0700_3000_0001_0004": "Encryption chip failure.", + "0700_3000_0003_0003": "RFID cannot be read because of a hardware or structural error.", + "0700_3100_0001_0001": "The RFID board between AMS1 Slot3 & Slot4 has an error.", + "0700_0200_0001_0001": "AMS1 Filament speed and length error: The filament odometry may be faulty.", + "0700_1000_0001_0001": "The AMS1 slot1 motor has slipped. The extrusion wheel may be malfunctioning, or the filament may be too thin.", + "0700_1000_0001_0003": "The AMS1 slot1 motor torque control is malfunctioning. The current sensor may be faulty.", + "0700_1000_0002_0002": "The AMS1 slot1 motor is overloaded. The filament may be tangled or stuck.", + "0700_1100_0001_0001": "The AMS1 slot2 motor has slipped. The extrusion wheel may be malfunctioning, or the filament may be too thin.", + "0700_1100_0001_0003": "The AMS1 slot2 motor torque control is malfunctioning. The current sensor may be faulty.", + "0700_1100_0002_0002": "The AMS1 slot2 motor is overloaded. The filament may be tangled or stuck.", + "0700_1200_0001_0001": "The AMS1 slot3 motor has slipped. The extrusion wheel may be malfunctioning, or the filament may be too thin.", + "0700_1200_0001_0003": "The AMS1 slot3 motor torque control is malfunctioning. The current sensor may be faulty.", + "0700_1200_0002_0002": "The AMS1 slot3 motor is overloaded. The filament may be tangled or stuck.", + "0700_1300_0001_0001": "The AMS1 slot4 motor has slipped. The extrusion wheel may be malfunctioning, or the filament may be too thin.", + "0700_1300_0001_0003": "The AMS1 slot4 motor torque control is malfunctioning. The current sensor may be faulty.", + "0700_1300_0002_0002": "The AMS1 slot4 motor is overloaded. The filament may be tangled or stuck.", + "0700_0100_0001_0001": "The AMS1 assist motor has slipped. The extrusion wheel may be worn down, or the filament may be too thin.", + "0700_0100_0001_0003": "The AMS1 assist motor torque control is malfunctioning. The current sensor may be faulty.", + "0700_0100_0001_0004": "The AMS1 assist motor speed control is malfunctioning. The speed sensor may be faulty.", + "0700_0100_0002_0002": "The AMS1 assist motor is overloaded. The filament may be tangled or stuck.", +} +# UNIQUE_ID=ARxX6kr9 + +# UNIQUE_ID=ZEJTS2b8 +PRINT_ERROR_ERRORS = { + "1201_8007": "Failed to extrude the filament. The extruder may be clogged or the filament may be stuck; please refer to HMS. After troubleshooting, click 'Retry' button.", + "1202_8007": "Failed to extrude the filament. The extruder may be clogged or the filament may be stuck; please refer to HMS. After troubleshooting, click 'Retry' button.", + "1203_8007": "Failed to extrude the filament. The extruder may be clogged or the filament may be stuck; please refer to HMS. After troubleshooting, click 'Retry' button.", + "1201_8014": "Failed to check the filament location in the tool head; please refer to the HMS. After troubleshooting, click the 'Retry' button.", + "1202_8014": "Failed to check the filament location in the tool head; please refer to the HMS. After troubleshooting, click the 'Retry' button.", + "0501_4038": "The region settings do not match the printer; please check the printer's region settings.", + "1201_8016": "The extruder is not extruding normally; please refer to the HMS. After trouble shooting. If the defects are acceptable, please click 'Retry' button.", + "1202_8016": "The extruder is not extruding normally; please refer to the HMS. After trouble shooting. If the defects are acceptable, please click 'Retry' button.", + "1203_8016": "The extruder is not extruding normally; please refer to the HMS. After trouble shooting. If the defects are acceptable, please click 'Retry' button.", + "0300_8015": "The filament has run out; please load new filament. If the filament is loaded, please select 'Resume'.", + "0501_4033": "Your APP region is not matched with your printer; please download the APP in the corresponding region and register your account again.", + "0300_8017": "Foreign objects detected on hotbed. Please check and clean the hotbed, then tap 'Resume' button to resume the print job.", + "0514_039": "Device login has expired; please try to bind again.", + "1000_C001": "High bed temperature may lead to filament clogging in the nozzle. You may open the chamber door.", + "0300_8010": "The hotend fan speed is abnormal.", + "1200_8006": "Unable to feed filament into the extruder. This could be due to entangled filament or a stuck spool. If not, please check if the AMS PTFE tube is connected.", + "1200_8016": "The extruder is not extruding normally. Refer to the Assistant for troubleshooting. There may be defects in this layer, you may resume if the defects are acceptable.", + "0500_403A": "The current temperature is too low. In order to protect you and your printer. Printing task, moving axis and other operations are disabled. Please move the printer to an environment above 10 celsius degree.", + "0700_8006": "Unable to feed filament into the extruder. This could be due to entangled filament or a stuck spool. If not, please check if the AMS PTFE tube is connected.", + "0300_8013": "Printing was paused by the user. You can select 'Resume' to continue printing.", + "0300_4000": "Printing stopped because homing Z axis failed.", + "0500_4003": "Printing stopped because the printer was unable to parse the file. Please resend your print job.", + "0500_4004": "The printer can't receive new print jobs while printing. Resend after the current print finishes.", + "0700_8002": "The cutter is stuck. Please make sure the cutter handle is out.", + "0700_8003": "Failed to pull out the filament from the extruder. This might be caused by clogged extruder or filament broken inside the extruder.", + "0700_8004": "AMS failed to pull back filament. This could be due to a stuck spool or the end of the filament being stuck in the path.", + "0700_8005": "The AMS failed to send out filament. You can clip the end of your filament flat, and reinsert. If this message persists, please check the PTFE tubes in AMS for any signs of wear and tear.", + "0700_8007": "Extruding filament failed. The extruder might be clogged.", + "0700_8010": "The AMS assist motor is overloaded. This could be due to entangled filament or a stuck spool.", + "0701_8003": "Failed to pull out the filament from the extruder. There may be an extruder clog or broken filament inside the extruder.", + "0701_8004": "AMS failed to pull back filament. This could be due to a stuck spool or the end of the filament being stuck in the path.", + "0701_8005": "The AMS failed to send filament. You canclip the end of your filament flat, and reinsert. If this message persists, please check the PTFE tubes in AMS for any signs of wear and tear.", + "0701_8010": "The AMS assist motor is overloaded. This could be due to entangled filament or a stuck spool.", + "0700_4001": "The AMS has been disabled for a print, but it still has filament loaded. Please unload the AMS filament , and switch to the spool holder filament for printing.", + "0500_4014": "Slicing for the print job failed. Please check your settings and restart the print job.", + "1200_8001": "Cutting the filament failed. Please check to see if the cutter is stuck. Refer to the Assistant for solutions.", + "1200_8007": "Failed to extrude the filament. This might be caused by clogged extruder or stuck filament. Refer to the Assistant for solutions.", + "1200_8010": "Filament or spool may be stuck.", + "1200_8014": "The filament location in the toolhead was not found. Refer to the Assistant for solutions.", + "1200_8015": "Failed to pull out the filament from the toolhead. Please check if the filament is stuck, or the filament is broken inside the extruder or PTFE tube.", + "0300_4002": "Printing Stopped because Auto Bed Leveling failed.", + "0300_8002": "First layer defects were detected by the Micro Lidar. Please check the quality of the printed model before continuing your print.", + "0300_8003": "Spaghetti defects were detected by the AI Print Monitoring. Please check the quality of the printed model before continuing your print.", + "0300_8004": "Filament ran out. Please load new filament.", + "0300_8005": "Toolhead front cover fell off. Please remount the front cover and check to make sure your print is going okay.", + "0300_8006": "The build plate marker was not detected. Please confirm the build plate is correctly positioned on the heatbed with all four corners aligned, and the maker is clear.", + "0700_8011": "AMS filament ran out. Please insert a new filament into the same AMS slot.", + "0701_8011": "AMS filament ran out. Please insert a new filament into the same AMS slot.", + "0702_8011": "AMS filament ran out. Please insert a new filament into the same AMS slot.", + "0703_8011": "AMS filament ran out. Please insert a new filament into the same AMS slot.", + "0300_8007": "There was an unfinished print job when the printer lost power. If the model is still adhered to the build plate, you can try resuming the print job.", + "0300_8008": "Printing Stopped because nozzle temperature problem.", + "0300_800B": "The cutter is stuck. Please make sure the cutter handle is out.", + "1200_8011": "AMS filament ran out. Please insert a new filament into the same AMS slot.", + "0300_8011": "Detected build plate is not the same as the Gcode file. Please adjust slicer settings or use the correct plate.", + "0701_8006": "Unable to feed filament into the extruder. This could be due to entangled filament or a stuck spool.", + "0300_800A": "A Filament pile-up was detected by the AI Print Monitoring. Please clean the filament from the waste chute.", + "07FF_8006": "Please feed filament into the PTFE tube until it can not be pushed any farther.", + "07FF_C003": "Please pull out the filament on the spool holder. If this message persists, please check to see if there is filament broken in the extruder or PTFE Tube. (Connect PTFE tube if you are about to use AMS)", + "07FF_C006": "Please feed filament into the PTFE tube until it can not be pushed any farther.", + "12FF_8003": "Please pull out the filament on the spool holder. If this message persists, please check to see if there is filament broken in the extruder or PTFE Tube. (Connect PTFE tube if you are about to use AMS)", + "12FF_C003": "Please pull out the filament on the spool holder. If this message persists, please check to see if there is filament broken in the extruder or PTFE Tube. (Connect PTFE tube if you are about to use AMS)", + "12FF_8006": "Please feed filament into the PTFE tube until it can not be pushed any farther.", + "12FF_C006": "Please feed filament into the PTFE tube until it can not be pushed any farther.", + "12FF_8010": "Please check if the filament or the spool is stuck.", + "07FF_8003": "Please pull out the filament on the spool holder. If this message persists, please check to see if there is filament broken in the extruder. (Connect PTFE tube if you are about to use AMS)", + "1000_C003": "Enabling traditional timelapse might lead to defects. Please enable it as needed?", + "1200_8004": "Failed to pull back the filament from the toolhead. Please check whether the filament is stuck. After troubleshooting, click the 'Retry' button.", + "0C00_8005": "Purged filament has piled up in the waste chute, which may cause a tool head collision.", + "0C00_C006": "Purged filament may have piled up in the waste chute.", + "0500_4015": "There is not enough free storage space for the print job. Please format or clean MicroSD card to release available space.", + "0500_C010": "MicroSD Card read/write exception. please reinsert or replace MicroSD Card .", + "0300_8018": "Chamber temperature malfunction.", + "0300_8019": "No build plate is placed.", + "0300_8014": "The nozzle is covered with filaments, or the build plate is installed incorrectly. Please cancel this printing and clean the nozzle or adjust the build plate according to the actual status, or tap 'Resume' button to resume the print job.", + "0300_8016": "The nozzle is clogged up with filaments. Please cancel this printing and clean the nozzle according to the actual status, or tap 'Resume' button to resume the print job.", + "0300_4005": "The nozzle fan speed is abnormal.", + "0300_400F": "No build plate is placed.", + "0500_4002": "Unsupported print file path or name. Please resend the printing job.", + "0500_8036": "Your sliced file is not consistent with the current printer model. Continue?", + "0500_4037": "Your sliced file is not compatible with current printer model. This file can't be printed on this printer.", + "0500_4038": "The nozzle diameter in sliced file is not consistent with the current nozzle setting. This file can't be printed.", + "1000_C002": "Printing CF material with stainless steel may cause nozzle damage.", + "1001_C001": "Timelapse is not supported because Spiral vase is enabled in slicing presets.", + "1001_C002": "Timelapse is not supported because Print sequence is set to 'By object'.", + "0501_4035": "The device is in the process of binding and cannot respond to new binding requests.", + "0501_4032": "QR code binding is in progress, so device discovery binding cannot be performed. You can scan the QR code on the screen for binding or exit the QR code display page on screen and try device discovery binding.", + "1202_8005": "Failed to feed the filament. Please load the filament, then click the 'Retry' button.", + "1203_8005": "Failed to feed the filament. Please load the filament, then click the 'Retry' button.", + "12FF_8005": "Failed to feed the filament. Please load the filament, then click the 'Retry' button.", + "1202_8006": "Failed to feed the filament into the toolhead. Please check whether the filament is stuck. After troubleshooting, click the 'Retry' button.", + "1203_8006": "Failed to feed the filament into the toolhead. Please check whether the filament is stuck. After troubleshooting, click the 'Retry' button.", + "1202_8010": "Please check if the spool or filament is stuck. After troubleshooting, click the 'Retry' button.", + "1203_8010": "Please check if the spool or filament is stuck. After troubleshooting, click the 'Retry' button.", + "1202_8011": "AMS filament has run out. Please insert a new filament into the AMS and click the 'Retry' button.", + "1203_8011": "AMS filament has run out. Please insert a new filament into the AMS and click the 'Retry' button.", + "12FF_8011": "AMS filament has run out. Please insert a new filament into the AMS and click the 'Retry' button.", + "1202_8012": "Failed to get AMS mapping table; please click the 'Retry' button to continue.", + "1203_8012": "Failed to get AMS mapping table; please click the 'Retry' button to continue.", + "12FF_8012": "Failed to get AMS mapping table; please click the 'Retry' button to continue.", + "1202_8013": "Timeout while purging old filament. Please check if the filament is stuck or the extruder clogged. After troubleshooting, click the 'Retry' button.", + "1203_8013": "Timeout while purging old filament. Please check if the filament is stuck or the extruder clogged. After troubleshooting, click the 'Retry' button.", + "12FF_8013": "Timeout while purging old filament. Please check if the filament is stuck or the extruder clogged. After troubleshooting, click the 'Retry' button.", + "1202_8015": "Failed to pull back the filament from the toolhead. Please check if the filament is stuck or is broken inside the extruder. After troubleshooting, click the 'Retry' button.", + "1203_8015": "Failed to pull back the filament from the toolhead. Please check if the filament is stuck or is broken inside the extruder. After troubleshooting, click the 'Retry' button.", + "1203_8001": "Failed to cut the filament. Please check the cutter. After troubleshooting, click the 'Retry' button.", + "12FF_8001": "Failed to cut the filament. Please check the cutter. After troubleshooting, click the 'Retry' button.", + "1203_8002": "The cutter is stuck. Please pull out the cutter handle and click the 'Retry' button.", + "12FF_8002": "The cutter is stuck. Please pull out the cutter handle and click the 'Retry' button.", + "1203_8003": "Failed to pull out the filament from the extruder. Please check whether the extruder is clogged or whether the filament is broken inside the extruder. After troubleshooting, click the 'Retry' button.", + "1202_8004": "Failed to pull back the filament from the toolhead. Please check whether the filament is stuck. After troubleshooting, click the 'Retry' button.", + "1203_8004": "Failed to pull back the filament from the toolhead. Please check whether the filament is stuck. After troubleshooting, click the 'Retry' button.", + "12FF_8004": "Failed to pull back the filament from the toolhead. Please check whether the filament is stuck. After troubleshooting, click the 'Retry' button.", + "0700_8001": "Failed to cut the filament. Please check the cutter. After troubleshooting, click the 'Retry' button.", + "0701_8001": "Failed to cut the filament. Please check the cutter. After troubleshooting, click the 'Retry' button.", + "0701_8002": "The cutter is stuck. Please pull out the cutter handle and click the 'Retry' button.", + "0701_8007": "Failed to extrude the filament. Please check if the extruder clogged. After troubleshooting, click the 'Retry' button.", + "0702_8001": "Failed to cut the filament. Please check the cutter. After troubleshooting, click the 'Retry' button.", + "0702_8002": "The cutter is stuck. Please pull out the cutter handle and click the 'Retry' button.", + "0702_8003": "Failed to pull out the filament from the extruder. Please check whether the extruder is clogged or whether the filament is broken inside the extruder. After troubleshooting, click the 'Retry' button.", + "0702_8004": "Failed to pull back the filament from the toolhead to AMS. Please check whether the filament or the spool is stuck. After troubleshooting, click the 'Retry' button.", + "0702_8005": "Failed to feed the filament outside the AMS. Please clip the end of the filament flat and check to see if the spool is stuck. After troubleshooting, click the 'Retry' button.", + "0702_8006": "Failed to feed the filament into the toolhead. Please check whether the filament or the spool is stuck. After troubleshooting, click the 'Retry' button.", + "0702_8007": "Failed to extrude the filament. Please check if the extruder clogged. After troubleshooting, click the 'Retry' button.", + "0702_8010": "AMS assist motor is overloaded. Please check if the spool or filament is stuck. After troubleshooting, click the 'Retry' button.", + "0703_8001": "Failed to cut the filament. Please check the cutter. After troubleshooting, click the 'Retry' button.", + "0703_8002": "The cutter is stuck. Please pull out the cutter handle and click the 'Retry' button.", + "0703_8003": "Failed to pull out the filament from the extruder. Please check whether the extruder is clogged or whether the filament is broken inside the extruder. After troubleshooting, click the 'Retry' button.", + "0703_8004": "Failed to pull back the filament from the toolhead to AMS. Please check whether the filament or the spool is stuck. After troubleshooting, click the 'Retry' button.", + "0703_8005": "Failed to feed the filament outside the AMS. Please clip the end of the filament flat and check to see if the spool is stuck. After troubleshooting, click the 'Retry' button.", + "0703_8006": "Failed to feed the filament into the toolhead. Please check whether the filament or the spool is stuck. After troubleshooting, click the 'Retry' button.", + "0703_8007": "Failed to extrude the filament. Please check if the extruder clogged. After troubleshooting, click the 'Retry' button.", + "0703_8010": "AMS assist motor is overloaded. Please check if the spool or filament is stuck. After troubleshooting, click the 'Retry' button.", + "0700_8013": "Timeout purging old filament: Please check if the filament is stuck or the extruder is clogged. After troubleshooting, click the 'Retry' button.", + "0701_8013": "Timeout purging old filament: Please check if the filament is stuck or the extruder is clogged. After troubleshooting, click the 'Retry' button.", + "0702_8013": "Timeout purging old filament: Please check if the filament is stuck or the extruder is clogged. After troubleshooting, click the 'Retry' button.", + "0703_8013": "Timeout purging old filament: Please check if the filament is stuck or the extruder is clogged. After troubleshooting, click the 'Retry' button.", + "07FF_8001": "Failed to cut the filament. Please check the cutter. After troubleshooting, click the 'Retry' button.", + "07FF_8002": "The cutter is stuck. Please pull out the cutter handle and click the 'Retry' button.", + "07FF_8005": "Failed to feed the filament outside the AMS. Please clip the end of the filament flat and check to see if the spool is stuck. After troubleshooting, click the 'Retry' button.", + "07FF_8010": "AMS assist motor is overloaded. Please check if the spool or filament is stuck. After troubleshooting, click the 'Retry' button.", + "07FF_8011": "AMS filament ran out. Please put a new filament into AMS and click the 'Retry' button.", + "07FF_8013": "Timeout purging old filament: Please check if the filament is stuck or the extruder is clogged. After troubleshooting, click the 'Retry' button.", + "1201_8001": "Failed to cut the filament. Please check the cutter. After troubleshooting, click the 'Retry' button.", + "1202_8001": "Failed to cut the filament. Please check the cutter. After troubleshooting, click the 'Retry' button.", + "1200_8002": "The cutter is stuck. Please pull out the cutter handle and click the 'Retry' button.", + "1201_8002": "The cutter is stuck. Please pull out the cutter handle and click the 'Retry' button.", + "1202_8002": "The cutter is stuck. Please pull out the cutter handle and click the 'Retry' button.", + "1200_8003": "Failed to pull out the filament from the extruder. Please check whether the extruder is clogged or whether the filament is broken inside the extruder. After troubleshooting, click the 'Retry' button.", + "1201_8003": "Failed to pull out the filament from the extruder. Please check whether the extruder is clogged or whether the filament is broken inside the extruder. After troubleshooting, click the 'Retry' button.", + "1202_8003": "Failed to pull out the filament from the extruder. Please check whether the extruder is clogged or whether the filament is broken inside the extruder. After troubleshooting, click the 'Retry' button.", + "1201_8004": "Failed to pull back the filament from the toolhead. Please check whether the filament is stuck. After troubleshooting, click the 'Retry' button.", + "1200_8005": "Failed to feed the filament. Please load the filament, then click the 'Retry' button.", + "1201_8005": "Failed to feed the filament. Please load the filament and then click the 'Retry' button.", + "1201_8006": "Failed to feed the filament into the toolhead. Please check whether the filament is stuck. After troubleshooting, click the 'Retry' button.", + "1201_8010": "Please check if the spool or filament is stuck. After troubleshooting, click the 'Retry' button.", + "1201_8011": "AMS filament has run out. Please insert a new filament into the AMS and click the 'Retry' button.", + "1200_8012": "Failed to get AMS mapping table. Please click the 'Retry' button to continue.", + "1201_8012": "Failed to get AMS mapping table; please click the 'Retry' button to continue.", + "1200_8013": "Timeout while purging old filament. Please check if the filament is stuck or the extruder clogged. After troubleshooting, click the 'Retry' button.", + "1201_8013": "Timeout while purging old filament. Please check if the filament is stuck or the extruder clogged. After troubleshooting, click the 'Retry' button.", + "1203_8014": "Failed to check the filament location in the tool head; please refer to the HMS. After troubleshooting, click the 'Retry' button.", + "1201_8015": "Failed to pull back the filament from the toolhead. Please check if the filament is stuck or the filament is broken inside the extruder. After troubleshooting, click the 'Retry' button.", + "0300_800D": "Some objects have fallen down, or the extruder is not extruding normally. If the defects are acceptable, click 'Resume' button to resume the print job.", + "0C00_8001": "First layer defects were detected. If the defects are acceptable, click 'Resume' button to resume the print job.", + "0500_8030": "", + "0500_402E": "The system does not support the file system currently used by the Micro SD card. Please replace the Micro SD card or format the current Micro SD card to FAT32.", + "0500_402F": "The Micro SD card sector data is damaged. Please use the SD card repair tool to repair or format it. If it still cannot be identified, please replace the Micro SD card.", + "0501_4017": "Binding failed. Please retry or restart the printer and retry.", + "0501_4018": "Binding configuration information parsing failed, please try again.", + "0501_4019": "The printer has already been bound. Please unbind it and try again.", + "0501_401A": "Cloud access failed. Possible reasons include network instability caused by interference, inability to access the internet, or router firewall configuration restrictions. You can try moving the printer closer to the router or checking the router configuration and then try again.", + "0501_401B": "Cloud response is invalid. If you have tried multiple times and are still failing, please contact customer service.", + "0501_401C": "Cloud access is rejected. If you have tried multiple times and are still failing, please contact customer service.", + "0501_401D": "Cloud access failed, which may be caused by network instability due to interference. You can try moving the printer closer to the router before you try again.", + "0501_401E": "Cloud response is invalid. If you have tried multiple times and are still failing, please contact customer service.", + "0501_401F": "Authorization timed out. Please make sure that your phone or PC has access to the internet, and ensure that the Bambu Studio/Bambu Handy APP is running in the foreground during the binding operation.", + "0501_4020": "Cloud access rejected. If you have tried multiple times and are still failing, please contact customer service.", + "0501_4021": "Cloud access failed, which may be caused by network instability due to interference. You can try moving the printer closer to the router before you try again.", + "0501_4022": "Cloud response is invalid. If you have tried multiple times and are still failing, please contact customer service.", + "0501_4023": "Cloud access rejected. If you have tried multiple times and are still failing, please contact customer service.", + "0501_4024": "Cloud access failed. Possible reasons include network instability caused by interference, inability to access the internet, or router firewall configuration restrictions. You can try moving the printer closer to the router or checking the router configuration before you try again.", + "0501_4025": "Cloud response is invalid. If you have tried multiple times and are still failing, please contact customer service.", + "0501_4026": "Cloud access rejected. If you have tried multiple times and are still failing, please contact customer service.", + "0501_4027": "Cloud access failed; this may be caused by network instability due to interference. You can try moving the printer closer to the router before you try again.", + "0501_4028": "Cloud response is invalid. If you have tried multiple times and are still failing, please contact customer service.", + "0501_4029": "Cloud access is rejected. If you have tried multiple times and are still failing, please contact customer service.", + "0501_4031": "Device discovery binding is in progress, and the QR code cannot be displayed on the screen. You can wait for the binding to finish or abort the device discovery binding process in the APP/Studio and retry scanning the QR code on the screen for binding.", + "0501_4034": "The slicing progress has not been updated for a long time, and the printing task has exited. Please confirm the parameters and reinitiate printing.", + "07FF_8004": "Failed to pull back the filament from the toolhead to AMS. Please check whether the filament or the spool is stuck. After troubleshooting, click the 'retry' button.", + "0C00_800A": "The detected build plate is not the same as in G-code.", + "12FF_8007": "Check nozzle. Click 'Done' if filament was extruded, otherwise push filament forward slightly and click 'Retry.'", + "0300_8012": "", + "0500_402D": "System exception.", + "0500_4027": "Cloud access failed; this may be caused by network instability due to interference. You can try moving the printer closer to the router before you try again.", + "0500_4028": "Cloud response is invalid. If you have tried multiple times and are still failing, please contact customer service.", + "0500_4029": "Cloud access is rejected. If you have tried multiple times and are still failing, please contact customer service.", + "0500_402A": "Failed to connect to the router, which may be caused by wireless interference or being too far away from the router. Please try again or move the printer closer to the router and try again.", + "0500_402C": "Failed to obtain IP address, which may be caused by wireless interference resulting in data transmission failure or DHCP address pool of the router being full. Please move the printer closer to the router and try again. If the issue persists, please check router settings to see whether the IP addresses have been exhausted.", + "0500_4016": "The MicroSD Card is write-protected. Please replace the MicroSD Card.", + "0500_401A": "Cloud access failed. Possible reasons include network instability caused by interference, inability to access the internet, or router firewall configuration restrictions. You can try moving the printer closer to the router or checking the router configuration and then try again.", + "0500_401B": "Cloud response is invalid. If you have tried multiple times and are still failing, please contact customer service.", + "0500_401C": "Cloud access is rejected. If you have tried multiple times and are still failing, please contact customer service.", + "0500_401D": "Cloud access failed, which may be caused by network instability due to interference. You can try moving the printer closer to the router before you try again.", + "0500_401E": "Cloud response is invalid. If you have tried multiple times and are still failing, please contact customer service.", + "0500_4020": "Cloud access rejected. If you have tried multiple times and are still failing, please contact customer service.", + "0500_4021": "Cloud access failed, which may be caused by network instability due to interference. You can try moving the printer closer to the router before you try again.", + "0500_4022": "Cloud response is invalid. If you have tried multiple times and are still failing, please contact customer service.", + "0500_4023": "Cloud access rejected. If you have tried multiple times and are still failing, please contact customer service.", + "0500_4024": "Cloud access failed. Possible reasons include network instability caused by interference, inability to access the internet, or router firewall configuration restrictions. You can try moving the printer closer to the router or checking the router configuration before you try again.", + "0500_4025": "Cloud response is invalid. If you have tried multiple times and are still failing, please contact customer service.", + "0500_4026": "Cloud access rejected. If you have tried multiple times and are still failing, please contact customer service.", + "0500_401F": "Authorization timed out. Please make sure that your phone or PC has access to the internet, and ensure that the Bambu Studio/Bambu Handy APP is running in the foreground during the binding operation.", + "0500_402B": "Router connection failed due to incorrect password. Please check the password and try again.", + "0500_4017": "Binding failed. Please retry or restart the printer and retry.", + "0500_4018": "Binding configuration information parsing failed, please try again.", + "0500_4019": "The printer has already been bound. Please unbind it and try again.", + "07FF_8007": "Please observe the nozzle. If the filament has been extruded, click 'Done'; if it is not, please push the filament forward slightly and then click 'Retry'.", + "1201_4001": "Filament is still loaded from the AMS when it has been disabled. Please unload AMS filament, load from spool holder, and restart print job.", + "1202_4001": "Filament is still loaded from the AMS when it has been disabled. Please unload AMS filament, load from spool holder, and restart print job.", + "1203_4001": "Filament is still loaded from the AMS when it has been disabled. Please unload AMS filament, load from spool holder, and restart print job.", + "12FF_4001": "Filament is still loaded from the AMS when it has been disabled. Please unload AMS filament, load from spool holder, and restart print job.", + "1200_4001": "Filament is still loaded from the AMS when it has been disabled. Please unload AMS filament, load from spool holder, and restart print job.", + "0300_800E": "The print file is not available. Please check to see if the storage media has been removed.", + "0500_8013": "The print file is not available. Please check to see if the storage media has been removed.", + "0500_4012": "The door seems to be open, so printing was paused.", + "0300_800F": "The door seems to be open, so printing was paused.", + "0500_C011": "", + "0500_400C": "Please insert a MicroSD card and restart the printing job.", + "07FF_8012": "Failed to get AMS mapping table; please click 'Retry' to continue.", + "07FF_4001": "Filament is still loaded from the AMS after it has been disabled. Please unload the filament, load from the spool holder, and restart printing.", + "0300_800C": "Skipping step detected, auto-recover complete; please resume print and check if there are any layer shift problems.", + "0500_400E": "Printing was cancelled.", + "0C00_8009": "Build plate localization marker was not found.", + "0500_400D": "Please run a self-test and restart the printing job.", + "0500_400B": "There was a problem downloading a file. Please check you network connection and resend the printing job.", + "0300_400A": "Mechanical resonance frequency identification failed.", + "0300_8009": "Heatbed temperature malfunction.", + "0300_400E": "The motor self-check failed.", + "0700_8012": "Failed to get AMS mapping table; please click 'Retry' to continue.", + "0701_8012": "Failed to get AMS mapping table; please click 'Retry' to continue.", + "0702_8012": "Failed to get AMS mapping table; please click 'Retry' to continue.", + "0703_8012": "Failed to get AMS mapping table; please click 'Retry' to continue.", + "0701_4001": "Filament is still loaded from the AMS after it has been disabled. Please unload the filament, load from the spool holder, and restart printing.", + "0702_4001": "Filament is still loaded from the AMS after it has been disabled. Please unload the filament, load from the spool holder, and restart printing.", + "0703_4001": "Filament is still loaded from the AMS after it has been disabled. Please unload the filament, load from the spool holder, and restart printing.", + "0C00_8002": "Spaghetti failure was detected.", + "0C00_C003": "Possible defects were detected in the first layer.", + "0C00_C004": "Possible spaghetti failure was detected.", + "0500_4007": "Print jobs are not allowed to be sent while force updating or when repair updating is required.", + "0500_4008": "Starting printing failed. please power cycle the printer and resend the print job.", + "0500_4009": "Print jobs are not allowed to be sent while updating logs.", + "0500_400A": "The file name is not supported. Please rename and restart the printing job.", + "0300_8001": "Printing was paused by the user. You can tap 'Resume' to resume the print job.", + "0500_4001": "Failed to connect to Bambu Cloud. Please check your network connection.", + "0500_4005": "Print jobs are not allowed to be sent while updating firmware.", + "0500_4006": "There is not enough free storage space for the print job. Restoring to factory settings can release available space.", + "0300_4001": "The printer timed out waiting for the nozzle to cool down before homing.", + "0300_4006": "The nozzle is clogged.", + "0300_4008": "The AMS failed to change filament.", + "0300_4009": "Homing XY axis failed.", + "0300_400B": "Internal communication exception.", + "0300_400C": "Printing was cancelled.", + "0300_400D": "Resume failed after power loss.", + "0300_8000": "Printing was paused for unknown reason. You can tap 'Resume' to resume the print job.", + "0300_4003": "Nozzle temperature malfunction.", + "0300_4004": "Heatbed temperature malfunction.", +} +# UNIQUE_ID=Y329g6Nq + +HMS_SEVERITY_LEVELS = { + "default": "unknown", + 1: "fatal", + 2: "serious", + 3: "common", + 4: "info" +} + +HMS_MODULES = { + "default": "unknown", + 0x05: "mainboard", + 0x0C: "xcam", + 0x07: "ams", + 0x08: "toolhead", + 0x03: "mc" +} + +class SdcardState(Enum): + NO_SDCARD = 0x00000000, + HAS_SDCARD_NORMAL = 0x00000100, + HAS_SDCARD_ABNORMAL = 0x00000200, + SDCARD_STATE_NUM = 0x00000300, + +class Home_Flag_Values(IntEnum): + X_AXIS = 0x00000001, + Y_AXIS = 0x00000002, + Z_AXIS = 0x00000004, + VOLTAGE220 = 0x00000008, + XCAM_AUTO_RECOVERY_STEP_LOSS = 0x00000010, + CAMERA_RECORDING = 0x00000020, + # Gap + AMS_CALIBRATE_REMAINING = 0x00000080, + SD_CARD_PRESENT = 0x00000100, + SD_CARD_ABNORMAL = 0x00000200, + AMS_AUTO_SWITCH = 0x00000400, + # Gap + XCAM_ALLOW_PROMPT_SOUND = 0x00020000, + WIRED_NETWORK = 0x00040000, + FILAMENT_TANGLE_DETECT_SUPPORTED = 0x00080000, + FILAMENT_TANGLE_DETECTED = 0x00100000, + SUPPORTS_MOTOR_CALIBRATION = 0x00200000, + # Gap + DOOR_OPEN = 0x00800000, + # Gap + INSTALLED_PLUS = 0x04000000, + SUPPORTED_PLUS = 0x08000000, + # Gap + diff --git a/octoprint_bambu_printer/printer/pybambu/filaments.json b/octoprint_bambu_printer/printer/pybambu/filaments.json new file mode 100644 index 0000000..f174de3 --- /dev/null +++ b/octoprint_bambu_printer/printer/pybambu/filaments.json @@ -0,0 +1,72 @@ +{ + "GFA00": "Bambu PLA Basic", + "GFA01": "Bambu PLA Matte", + "GFA02": "Bambu PLA Metal", + "GFA05": "Bambu PLA Silk", + "GFA07": "Bambu PLA Marble", + "GFA08": "Bambu PLA Sparkle", + "GFA09": "Bambu PLA Tough", + "GFA11": "Bambu PLA Aero", + "GFA12": "Bambu PLA Glow", + "GFA13": "Bambu PLA Dynamic", + "GFA15": "Bambu PLA Galaxy", + "GFA50": "Bambu PLA-CF", + "GFB00": "Bambu ABS", + "GFB01": "Bambu ASA", + "GFB02": "Bambu ASA-Aero", + "GFB50": "Bambu ABS-GF", + "GFB60": "PolyLite ABS", + "GFB61": "PolyLite ASA", + "GFB98": "Generic ASA", + "GFB99": "Generic ABS", + "GFC00": "Bambu PC", + "GFC99": "Generic PC", + "GFG00": "Bambu PETG Basic", + "GFG01": "Bambu PETG Translucent", + "GFG02": "Bambu PETG HF", + "GFG50": "Bambu PETG-CF", + "GFG60": "PolyLite PETG", + "GFG97": "Generic PCTG", + "GFG98": "Generic PETG-CF", + "GFG99": "Generic PETG", + "GFL00": "PolyLite PLA", + "GFL01": "PolyTerra PLA", + "GFL03": "eSUN PLA+", + "GFL04": "Overture PLA", + "GFL05": "Overture Matte PLA", + "GFL95": "Generic PLA High Speed", + "GFL96": "Generic PLA Silk", + "GFL98": "Generic PLA-CF", + "GFL99": "Generic PLA", + "GFN03": "Bambu PA-CF", + "GFN04": "Bambu PAHT-CF", + "GFN05": "Bambu PA6-CF", + "GFN08": "Bambu PA6-GF", + "GFN96": "Generic PPA-GF", + "GFN97": "Generic PPA-CF", + "GFN98": "Generic PA-CF", + "GFN99": "Generic PA", + "GFP95": "Generic PP-GF", + "GFP96": "Generic PP-CF", + "GFP97": "Generic PP", + "GFP98": "Generic PE-CF", + "GFP99": "Generic PE", + "GFR98": "Generic PHA", + "GFR99": "Generic EVA", + "GFS00": "Bambu Support W", + "GFS01": "Bambu Support G", + "GFS02": "Bambu Support For PLA", + "GFS03": "Bambu Support For PA/PET", + "GFS04": "Bambu PVA", + "GFS05": "Bambu Support For PLA/PETG", + "GFS06": "Bambu Support for ABS", + "GFS97": "Generic BVOH", + "GFS98": "Generic HIPS", + "GFS99": "Generic PVA", + "GFT01": "Bambu PET-CF", + "GFT97": "Generic PPS", + "GFT98": "Generic PPS-CF", + "GFU00": "Bambu TPU 95A HF", + "GFU01": "Bambu TPU 95A", + "GFU99": "Generic TPU" +} diff --git a/octoprint_bambu_printer/printer/pybambu/models.py b/octoprint_bambu_printer/printer/pybambu/models.py new file mode 100644 index 0000000..398d0be --- /dev/null +++ b/octoprint_bambu_printer/printer/pybambu/models.py @@ -0,0 +1,1424 @@ +import math + +from dataclasses import dataclass, field +from datetime import datetime +from dateutil import parser, tz +from packaging import version + +from .utils import ( + search, + fan_percentage, + fan_percentage_to_gcode, + get_current_stage, + get_filament_name, + get_printer_type, + get_speed_name, + get_hw_version, + get_sw_version, + get_start_time, + get_end_time, + get_HMS_error_text, + get_print_error_text, + get_generic_AMS_HMS_error_code, + get_HMS_severity, + get_HMS_module, +) +from .const import ( + LOGGER, + Features, + FansEnum, + Home_Flag_Values, + SdcardState, + SPEED_PROFILE, + GCODE_STATE_OPTIONS, + PRINT_TYPE_OPTIONS, +) +from .commands import ( + CHAMBER_LIGHT_ON, + CHAMBER_LIGHT_OFF, + SPEED_PROFILE_TEMPLATE, +) + +class Device: + def __init__(self, client): + self._client = client + self.temperature = Temperature() + self.lights = Lights(client = client) + self.info = Info(client = client) + self.print_job = PrintJob(client = client) + self.fans = Fans(client = client) + self.speed = Speed(client = client) + self.stage = StageAction() + self.ams = AMSList(client = client) + self.external_spool = ExternalSpool(client = client) + self.hms = HMSList(client = client) + self.print_error = PrintErrorList(client = client) + self.camera = Camera() + self.home_flag = HomeFlag(client=client) + self.push_all_data = None + self.get_version_data = None + if self.supports_feature(Features.CAMERA_IMAGE): + self.chamber_image = ChamberImage(client = client) + self.cover_image = CoverImage(client = client) + + def print_update(self, data) -> bool: + send_event = False + send_event = send_event | self.info.print_update(data = data) + send_event = send_event | self.print_job.print_update(data = data) + send_event = send_event | self.temperature.print_update(data = data) + send_event = send_event | self.lights.print_update(data = data) + send_event = send_event | self.fans.print_update(data = data) + send_event = send_event | self.speed.print_update(data = data) + send_event = send_event | self.stage.print_update(data = data) + send_event = send_event | self.ams.print_update(data = data) + send_event = send_event | self.external_spool.print_update(data = data) + send_event = send_event | self.hms.print_update(data = data) + send_event = send_event | self.print_error.print_update(data = data) + send_event = send_event | self.camera.print_update(data = data) + send_event = send_event | self.home_flag.print_update(data = data) + + if send_event and self._client.callback is not None: + LOGGER.debug("event_printer_data_update") + self._client.callback("event_printer_data_update") + + if data.get("msg", 0) == 0: + self.push_all_data = data + + def info_update(self, data): + self.info.info_update(data = data) + self.home_flag.info_update(data = data) + self.ams.info_update(data = data) + if data.get("command") == "get_version": + self.get_version_data = data + + def supports_feature(self, feature): + if feature == Features.AUX_FAN: + return True + elif feature == Features.CHAMBER_LIGHT: + return True + elif feature == Features.CHAMBER_FAN: + return self.info.device_type == "X1" or self.info.device_type == "X1C" or self.info.device_type == "X1E" or self.info.device_type == "P1P" or self.info.device_type == "P1S" + elif feature == Features.CHAMBER_TEMPERATURE: + return self.info.device_type == "X1" or self.info.device_type == "X1C" or self.info.device_type == "X1E" + elif feature == Features.CURRENT_STAGE: + return True + elif feature == Features.PRINT_LAYERS: + return True + elif feature == Features.AMS: + return len(self.ams.data) != 0 + elif feature == Features.EXTERNAL_SPOOL: + return True + elif feature == Features.K_VALUE: + return self.info.device_type == "P1P" or self.info.device_type == "P1S" or self.info.device_type == "A1" or self.info.device_type == "A1MINI" + elif feature == Features.START_TIME: + return False + elif feature == Features.START_TIME_GENERATED: + return True + elif feature == Features.AMS_TEMPERATURE: + return self.info.device_type == "X1" or self.info.device_type == "X1C" or self.info.device_type == "X1E" + elif feature == Features.CAMERA_RTSP: + return self.info.device_type == "X1" or self.info.device_type == "X1C" or self.info.device_type == "X1E" + elif feature == Features.CAMERA_IMAGE: + return (self._client.host != "") and (self._client._access_code != "") and (self.info.device_type == "P1P" or self.info.device_type == "P1S" or self.info.device_type == "A1" or self.info.device_type == "A1MINI") + elif feature == Features.DOOR_SENSOR: + return self.info.device_type == "X1" or self.info.device_type == "X1C" or self.info.device_type == "X1E" + elif feature == Features.MANUAL_MODE: + return self.info.device_type == "P1P" or self.info.device_type == "P1S" or self.info.device_type == "A1" or self.info.device_type == "A1MINI" + + return False + + def get_active_tray(self): + if self.supports_feature(Features.AMS): + if self.ams.tray_now == 255: + return None + if self.ams.tray_now == 254: + return self.external_spool + active_ams = self.ams.data[math.floor(self.ams.tray_now / 4)] + active_tray = self.ams.tray_now % 4 + return None if active_ams is None else active_ams.tray[active_tray] + else: + return self.external_spool + +@dataclass +class Lights: + """Return all light related info""" + chamber_light: str + chamber_light_override: str + work_light: str + + def __init__(self, client): + self._client = client + self.chamber_light = "unknown" + self.work_light = "unknown" + self.chamber_light_override = "" + + def print_update(self, data) -> bool: + old_data = f"{self.__dict__}" + + # "lights_report": [ + # { + # "node": "chamber_light", + # "mode": "on" + # }, + # { + # "node": "work_light", # X1 only + # "mode": "flashing" + # } + # ], + + chamber_light = \ + search(data.get("lights_report", []), lambda x: x.get('node', "") == "chamber_light", + {"mode": self.chamber_light}).get("mode") + if self.chamber_light_override != "": + if self.chamber_light_override == chamber_light: + self.chamber_light_override = "" + else: + self.chamber_light = chamber_light + self.work_light = \ + search(data.get("lights_report", []), lambda x: x.get('node', "") == "work_light", + {"mode": self.work_light}).get("mode") + + return (old_data != f"{self.__dict__}") + + def TurnChamberLightOn(self): + self.chamber_light = "on" + self.chamber_light_override = "on" + if self._client.callback is not None: + self._client.callback("event_light_update") + self._client.publish(CHAMBER_LIGHT_ON) + + def TurnChamberLightOff(self): + self.chamber_light = "off" + self.chamber_light_override = "off" + if self._client.callback is not None: + self._client.callback("event_light_update") + self._client.publish(CHAMBER_LIGHT_OFF) + + +@dataclass +class Camera: + """Return camera related info""" + recording: str + resolution: str + rtsp_url: str + timelapse: str + + def __init__(self): + self.recording = '' + self.resolution = '' + self.rtsp_url = None + self.timelapse = '' + + def print_update(self, data) -> bool: + old_data = f"{self.__dict__}" + + # "ipcam": { + # "ipcam_dev": "1", + # "ipcam_record": "enable", + # "mode_bits": 2, + # "resolution": "1080p", + # "rtsp_url": "rtsps://192.168.1.64/streaming/live/1", + # "timelapse": "disable", + # "tutk_server": "disable" + # } + + self.timelapse = data.get("ipcam", {}).get("timelapse", self.timelapse) + self.recording = data.get("ipcam", {}).get("ipcam_record", self.recording) + self.resolution = data.get("ipcam", {}).get("resolution", self.resolution) + self.rtsp_url = data.get("ipcam", {}).get("rtsp_url", self.rtsp_url) + + return (old_data != f"{self.__dict__}") + +@dataclass +class Temperature: + """Return all temperature related info""" + bed_temp: int + target_bed_temp: int + chamber_temp: int + nozzle_temp: int + target_nozzle_temp: int + + def __init__(self): + self.bed_temp = 0 + self.target_bed_temp = 0 + self.chamber_temp = 0 + self.nozzle_temp = 0 + self.target_nozzle_temp = 0 + + def print_update(self, data) -> bool: + old_data = f"{self.__dict__}" + + self.bed_temp = round(data.get("bed_temper", self.bed_temp)) + self.target_bed_temp = round(data.get("bed_target_temper", self.target_bed_temp)) + self.chamber_temp = round(data.get("chamber_temper", self.chamber_temp)) + self.nozzle_temp = round(data.get("nozzle_temper", self.nozzle_temp)) + self.target_nozzle_temp = round(data.get("nozzle_target_temper", self.target_nozzle_temp)) + + return (old_data != f"{self.__dict__}") + +@dataclass +class Fans: + """Return all fan related info""" + _aux_fan_speed_percentage: int + _aux_fan_speed: int + _aux_fan_speed_override: int + _aux_fan_speed_override_time: datetime + _chamber_fan_speed_percentage: int + _chamber_fan_speed: int + _chamber_fan_speed_override: int + _chamber_fan_speed_override_time: datetime + _cooling_fan_speed_percentage: int + _cooling_fan_speed: int + _cooling_fan_speed_override: int + _cooling_fan_speed_override_time: datetime + _heatbreak_fan_speed_percentage: int + _heatbreak_fan_speed: int + + def __init__(self, client): + self._client = client + self._aux_fan_speed_percentage = 0 + self._aux_fan_speed = 0 + self._aux_fan_speed_override = 0 + self._aux_fan_speed_override_time = None + self._chamber_fan_speed_percentage = 0 + self._chamber_fan_speed = 0 + self._chamber_fan_speed_override = 0 + self._chamber_fan_speed_override_time = None + self._cooling_fan_speed_percentage = 0 + self._cooling_fan_speed = 0 + self._cooling_fan_speed_override = 0 + self._cooling_fan_speed_override_time = None + self._heatbreak_fan_speed_percentage = 0 + self._heatbreak_fan_speed = 0 + + def print_update(self, data) -> bool: + old_data = f"{self.__dict__}" + + self._aux_fan_speed = data.get("big_fan1_speed", self._aux_fan_speed) + self._aux_fan_speed_percentage = fan_percentage(self._aux_fan_speed) + if self._aux_fan_speed_override_time is not None: + delta = datetime.now() - self._aux_fan_speed_override_time + if delta.seconds > 5: + self._aux_fan_speed_override_time = None + self._chamber_fan_speed = data.get("big_fan2_speed", self._chamber_fan_speed) + self._chamber_fan_speed_percentage = fan_percentage(self._chamber_fan_speed) + if self._chamber_fan_speed_override_time is not None: + delta = datetime.now() - self._chamber_fan_speed_override_time + if delta.seconds > 5: + self._chamber_fan_speed_override_time = None + self._cooling_fan_speed = data.get("cooling_fan_speed", self._cooling_fan_speed) + self._cooling_fan_speed_percentage = fan_percentage(self._cooling_fan_speed) + if self._cooling_fan_speed_override_time is not None: + delta = datetime.now() - self._cooling_fan_speed_override_time + if delta.seconds > 5: + self._cooling_fan_speed_override_time = None + self._heatbreak_fan_speed = data.get("heatbreak_fan_speed", self._heatbreak_fan_speed) + self._heatbreak_fan_speed_percentage = fan_percentage(self._heatbreak_fan_speed) + + return (old_data != f"{self.__dict__}") + + def set_fan_speed(self, fan: FansEnum, percentage: int): + """Set fan speed""" + percentage = round(percentage / 10) * 10 + command = fan_percentage_to_gcode(fan, percentage) + + if fan == FansEnum.PART_COOLING: + self._cooling_fan_speed = percentage + self._cooling_fan_speed_override_time = datetime.now() + elif fan == FansEnum.AUXILIARY: + self._aux_fan_speed_override = percentage + self._aux_fan_speed_override_time = datetime.now() + elif fan == FansEnum.CHAMBER: + self._chamber_fan_speed_override = percentage + self._chamber_fan_speed_override_time = datetime.now() + + LOGGER.debug(command) + self._client.publish(command) + + if self._client.callback is not None: + self._client.callback("event_printer_data_update") + + def get_fan_speed(self, fan: FansEnum) -> int: + if fan == FansEnum.PART_COOLING: + if self._cooling_fan_speed_override_time is not None: + return self._cooling_fan_speed_override + else: + return self._cooling_fan_speed_percentage + elif fan == FansEnum.AUXILIARY: + if self._aux_fan_speed_override_time is not None: + return self._aux_fan_speed_override + else: + return self._aux_fan_speed_percentage + elif fan == FansEnum.CHAMBER: + if self._chamber_fan_speed_override_time is not None: + return self._chamber_fan_speed_override + else: + return self._chamber_fan_speed_percentage + elif fan == FansEnum.HEATBREAK: + return self._heatbreak_fan_speed_percentage + +@dataclass +class PrintJob: + """Return all information related content""" + + print_percentage: int + gcode_state: str + file_type_icon: str + gcode_file: str + subtask_name: str + start_time: datetime + end_time: datetime + remaining_time: int + current_layer: int + total_layers: int + print_error: int + print_weight: float + print_length: int + print_bed_type: str + print_type: str + _ams_print_weights: float + _ams_print_lengths: float + + @property + def get_ams_print_weights(self) -> float: + values = {} + for i in range(16): + if self._ams_print_weights[i] != 0: + values[f"AMS Slot {i}"] = self._ams_print_weights[i] + return values + + @property + def get_ams_print_lengths(self) -> float: + values = {} + for i in range(16): + if self._ams_print_lengths[i] != 0: + values[f"AMS Slot {i}"] = self._ams_print_lengths[i] + return values + + def __init__(self, client): + self._client = client + self.print_percentage = 0 + self.gcode_state = "unknown" + self.gcode_file = "" + self.subtask_name = "" + self.start_time = None + self.end_time = None + self.remaining_time = 0 + self.current_layer = 0 + self.total_layers = 0 + self.print_error = 0 + self.print_weight = 0 + self._ams_print_weights = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] + self._ams_print_lengths = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] + self.print_length = 0 + self.print_bed_type = "unknown" + self.file_type_icon = "mdi:file" + self.print_type = "" + + def print_update(self, data) -> bool: + old_data = f"{self.__dict__}" + + # Example payload: + # { + # "print": { + # "gcode_start_time": "1681479206", + # "gcode_state": "IDLE", + # "mc_print_stage": "1", + # "mc_percent": 100, + # "mc_remaining_time": 0, + # "wifi_signal": "-53dBm", + # "print_type": "idle", + # "ipcam": { + # "ipcam_dev": "1", + # "ipcam_record": "enable" + # "resolution": "1080p", # X1 only + # "timelapse": "disable" + # }, + # "layer_num": 0, + # "total_layer_num": 0, + + self.print_percentage = data.get("mc_percent", self.print_percentage) + previous_gcode_state = self.gcode_state + self.gcode_state = data.get("gcode_state", self.gcode_state) + if previous_gcode_state != self.gcode_state: + LOGGER.debug(f"GCODE_STATE: {previous_gcode_state} -> {self.gcode_state}") + if self.gcode_state.lower() not in GCODE_STATE_OPTIONS: + LOGGER.error(f"Unknown gcode_state. Please log an issue : '{self.gcode_state}'") + self.gcode_state = "unknown" + if previous_gcode_state != self.gcode_state: + LOGGER.debug(f"GCODE_STATE: {previous_gcode_state} -> {self.gcode_state}") + self.gcode_file = data.get("gcode_file", self.gcode_file) + self.print_type = data.get("print_type", self.print_type) + if self.print_type.lower() not in PRINT_TYPE_OPTIONS: + LOGGER.debug(f"Unknown print_type. Please log an issue : '{self.print_type}'") + self.print_type = "unknown" + self.subtask_name = data.get("subtask_name", self.subtask_name) + self.file_type_icon = "mdi:file" if self.print_type != "cloud" else "mdi:cloud-outline" + self.current_layer = data.get("layer_num", self.current_layer) + self.total_layers = data.get("total_layer_num", self.total_layers) + + # Initialize task data at startup. + if previous_gcode_state == "unknown" and self.gcode_state != "unknown": + self._update_task_data() + + # Calculate start / end time after we update task data so we don't stomp on prepopulated values while idle on integration start. + if data.get("gcode_start_time") is not None: + if self.start_time != get_start_time(int(data.get("gcode_start_time"))): + LOGGER.debug(f"GCODE START TIME: {self.start_time}") + self.start_time = get_start_time(int(data.get("gcode_start_time"))) + + # Generate the end_time from the remaining_time mqtt payload value if present. + if data.get("mc_remaining_time") is not None: + existing_remaining_time = self.remaining_time + self.remaining_time = data.get("mc_remaining_time") + if self.start_time is None: + self.end_time = None + elif existing_remaining_time != self.remaining_time: + self.end_time = get_end_time(self.remaining_time) + LOGGER.debug(f"END TIME2: {self.end_time}") + + # Handle print start + previously_idle = previous_gcode_state == "IDLE" or previous_gcode_state == "FAILED" or previous_gcode_state == "FINISH" + currently_idle = self.gcode_state == "IDLE" or self.gcode_state == "FAILED" or self.gcode_state == "FINISH" + + if previously_idle and not currently_idle: + if self._client.callback is not None: + self._client.callback("event_print_started") + + # Generate the start_time for P1P/S when printer moves from idle to another state. Original attempt with remaining time + # becoming non-zero didn't work as it never bounced to zero in at least the scenario where a print was canceled. + if self._client._device.supports_feature(Features.START_TIME_GENERATED): + # We can use the existing get_end_time helper to format date.now() as desired by passing 0. + self.start_time = get_end_time(0) + # Make sure we don't keep using a stale end time. + self.end_time = None + LOGGER.debug(f"GENERATED START TIME: {self.start_time}") + + # Update task data if bambu cloud connected + self._update_task_data() + + # When a print is canceled by the user, this is the payload that's sent. A couple of seconds later + # print_error will be reset to zero. + # { + # "print": { + # "print_error": 50348044, + # } + # } + isCanceledPrint = False + if data.get("print_error") == 50348044 and self.print_error == 0: + isCanceledPrint = True + if self._client.callback is not None: + self._client.callback("event_print_canceled") + self.print_error = data.get("print_error", self.print_error) + + # Handle print failed + if previous_gcode_state != "unknown" and previous_gcode_state != "FAILED" and self.gcode_state == "FAILED": + if not isCanceledPrint: + if self._client.callback is not None: + self._client.callback("event_print_failed") + + # Handle print finish + if previous_gcode_state != "unknown" and previous_gcode_state != "FINISH" and self.gcode_state == "FINISH": + if self._client.callback is not None: + self._client.callback("event_print_finished") + + if currently_idle and not previously_idle and previous_gcode_state != "unknown": + if self.start_time != None: + # self.end_time isn't updated if we hit an AMS retract at print end but the printer does count that entire + # paused time as usage hours. So we need to use the current time instead of the last recorded end time in + # our calculation here. + duration = datetime.now() - self.start_time + # Round usage hours to 2 decimal places (about 1/2 a minute accuracy) + new_hours = round((duration.seconds / 60 / 60) * 100) / 100 + LOGGER.debug(f"NEW USAGE HOURS: {new_hours}") + self._client._device.info.usage_hours += new_hours + + return (old_data != f"{self.__dict__}") + + # The task list is of the following form with a 'hits' array with typical 20 entries. + # + # "total": 531, + # "hits": [ + # { + # "id": 35237965, + # "designId": 0, + # "designTitle": "", + # "instanceId": 0, + # "modelId": "REDACTED", + # "title": "REDACTED", + # "cover": "REDACTED", + # "status": 4, + # "feedbackStatus": 0, + # "startTime": "2023-12-21T19:02:16Z", + # "endTime": "2023-12-21T19:02:35Z", + # "weight": 34.62, + # "length": 1161, + # "costTime": 10346, + # "profileId": 35276233, + # "plateIndex": 1, + # "plateName": "", + # "deviceId": "REDACTED", + # "amsDetailMapping": [ + # { + # "ams": 4, + # "sourceColor": "F4D976FF", + # "targetColor": "F4D976FF", + # "filamentId": "GFL99", + # "filamentType": "PLA", + # "targetFilamentType": "", + # "weight": 34.62 + # } + # ], + # "mode": "cloud_file", + # "isPublicProfile": false, + # "isPrintable": true, + # "deviceModel": "P1P", + # "deviceName": "Bambu P1P", + # "bedType": "textured_plate" + # }, + + def _update_task_data(self): + if self._client.bambu_cloud.auth_token != "": + self._task_data = self._client.bambu_cloud.get_latest_task_for_printer(self._client._serial) + if self._task_data is None: + LOGGER.debug("No bambu cloud task data found for printer.") + self._client._device.cover_image.set_jpeg(None) + self.print_weight = 0 + self._ams_print_weights = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] + self._ams_print_lengths = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] + self.print_length = 0 + self.print_bed_type = "unknown" + self.start_time = None + self.end_time = None + else: + LOGGER.debug("Updating bambu cloud task data found for printer.") + url = self._task_data.get('cover', '') + if url != "": + data = self._client.bambu_cloud.download(url) + self._client._device.cover_image.set_jpeg(data) + + self.print_length = self._task_data.get('length', self.print_length * 100) / 100 + self.print_bed_type = self._task_data.get('bedType', self.print_bed_type) + self.print_weight = self._task_data.get('weight', self.print_weight) + ams_print_data = self._task_data.get('amsDetailMapping', []) + self._ams_print_weights = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] + self._ams_print_lengths = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] + if self.print_weight != 0: + for ams_data in ams_print_data: + index = ams_data['ams'] + weight = ams_data['weight'] + self._ams_print_weights[index] = weight + self._ams_print_lengths[index] = self.print_length * weight / self.print_weight + + status = self._task_data['status'] + LOGGER.debug(f"CLOUD PRINT STATUS: {status}") + if self._client._device.supports_feature(Features.START_TIME_GENERATED) and (status == 4): + # If we generate the start time (not X1), then rely more heavily on the cloud task data and + # do so uniformly so we always have matched start/end times. + + # "startTime": "2023-12-21T19:02:16Z" + cloud_time_str = self._task_data.get('startTime', "") + LOGGER.debug(f"CLOUD START TIME1: {self.start_time}") + if cloud_time_str != "": + local_dt = parser.parse(cloud_time_str).astimezone(tz.tzlocal()) + # Convert it to timestamp and back to get rid of timezone in printed output to match datetime objects created from mqtt timestamps. + local_dt = datetime.fromtimestamp(local_dt.timestamp()) + self.start_time = local_dt + LOGGER.debug(f"CLOUD START TIME2: {self.start_time}") + + # "endTime": "2023-12-21T19:02:35Z" + cloud_time_str = self._task_data.get('endTime', "") + LOGGER.debug(f"CLOUD END TIME1: {self.end_time}") + if cloud_time_str != "": + local_dt = parser.parse(cloud_time_str).astimezone(tz.tzlocal()) + # Convert it to timestamp and back to get rid of timezone in printed output to match datetime objects created from mqtt timestamps. + local_dt = datetime.fromtimestamp(local_dt.timestamp()) + self.end_time = local_dt + LOGGER.debug(f"CLOUD END TIME2: {self.end_time}") + + +@dataclass +class Info: + """Return all device related content""" + + # Device state + serial: str + device_type: str + wifi_signal: int + device_type: str + hw_ver: str + sw_ver: str + online: bool + new_version_state: int + mqtt_mode: str + nozzle_diameter: float + nozzle_type: str + usage_hours: float + + def __init__(self, client): + self._client = client + + self.serial = self._client._serial + self.device_type = self._client._device_type.upper() + self.wifi_signal = 0 + self.hw_ver = "unknown" + self.sw_ver = "unknown" + self.online = False + self.new_version_state = 0 + self.mqtt_mode = "local" if self._client._local_mqtt else "bambu_cloud" + self.nozzle_diameter = 0 + self.nozzle_type = "unknown" + self.usage_hours = client._usage_hours + + def set_online(self, online): + if self.online != online: + self.online = online + if self._client.callback is not None: + self._client.callback("event_printer_data_update") + + def info_update(self, data): + + # Example payload: + # { + # "info": { + # "command": "get_version", + # "sequence_id": "20004", + # "module": [ + # { + # "name": "ota", + # "project_name": "C11", + # "sw_ver": "01.02.03.00", + # "hw_ver": "OTA", + # "sn": "..." + # }, + # { + # "name": "esp32", + # "project_name": "C11", + # "sw_ver": "00.03.12.31", + # "hw_ver": "AP04", + # "sn": "..." + # }, + modules = data.get("module", []) + self.device_type = get_printer_type(modules, self.device_type) + LOGGER.debug(f"Device is {self.device_type}") + self.hw_ver = get_hw_version(modules, self.hw_ver) + self.sw_ver = get_sw_version(modules, self.sw_ver) + if self._client.callback is not None: + self._client.callback("event_printer_info_update") + + def print_update(self, data) -> bool: + old_data = f"{self.__dict__}" + + # Example payload: + # { + # "print": { + # "gcode_start_time": "1681479206", + # "gcode_state": "IDLE", + # "mc_print_stage": "1", + # "mc_percent": 100, + # "mc_remaining_time": 0, + # "wifi_signal": "-53dBm", + # "print_type": "idle", + # "ipcam": { + # "ipcam_dev": "1", + # "ipcam_record": "enable" + # "resolution": "1080p", # X1 only + # "timelapse": "disable" + # }, + # "layer_num": 0, + # "total_layer_num": 0, + + self.wifi_signal = int(data.get("wifi_signal", str(self.wifi_signal)).replace("dBm", "")) + + # Version data is provided differently for X1 and P1 + # P1P example: + # "upgrade_state": { + # "sequence_id": 0, + # "progress": "", + # "status": "", + # "consistency_request": false, + # "dis_state": 1, + # "err_code": 0, + # "force_upgrade": false, + # "message": "", + # "module": "", + # "new_version_state": 1, + # "new_ver_list": [ + # { + # "name": "ota", + # "cur_ver": "01.02.03.00", + # "new_ver": "01.03.00.00" + # }, + # { + # "name": "ams/0", + # "cur_ver": "00.00.05.96", + # "new_ver": "00.00.06.32" + # } + # ] + # }, + # X1 example: + # "upgrade_state": { + # "ahb_new_version_number": "", + # "ams_new_version_number": "", + # "consistency_request": false, + # "dis_state": 0, + # "err_code": 0, + # "force_upgrade": false, + # "message": "", + # "module": "null", + # "new_version_state": 2, + # "ota_new_version_number": "", + # "progress": "0", + # "sequence_id": 0, + # "status": "IDLE" + # }, + # The 'new_version_state' value is common to indicate a new upgrade is available. + # Observed values so far are: + # 1 - upgrade available + # 2 - no upgrades available + # And the P1P lists it's versions in new_ver_list as a structured set of data with old + # and new versions provided for each component. While the X1 lists only the new version + # in separate string properties. + + self.new_version_state = data.get("upgrade_state", {}).get("new_version_state", self.new_version_state) + + # "nozzle_diameter": "0.4", + # "nozzle_type": "hardened_steel", + self.nozzle_diameter = float(data.get("nozzle_diameter", self.nozzle_diameter)) + self.nozzle_type = data.get("nozzle_type", self.nozzle_type) + + return (old_data != f"{self.__dict__}") + + @property + def has_bambu_cloud_connection(self) -> bool: + return self._client.bambu_cloud.auth_token != "" + +@dataclass +class AMSInstance: + """Return all AMS instance related info""" + + def __init__(self, client): + self.serial = "" + self.sw_version = "" + self.hw_version = "" + self.humidity_index = 0 + self.temperature = 0 + self.tray = [None] * 4 + self.tray[0] = AMSTray(client) + self.tray[1] = AMSTray(client) + self.tray[2] = AMSTray(client) + self.tray[3] = AMSTray(client) + + +@dataclass +class AMSList: + """Return all AMS related info""" + + def __init__(self, client): + self._client = client + self.tray_now = 0 + self.data = [None] * 4 + + def info_update(self, data): + old_data = f"{self.__dict__}" + + # First determine if this the version info data or the json payload data. We use the version info to determine + # what devices to add to humidity_index assistant and add all the sensors as entities. And then then json payload data + # to populate the values for all those entities. + + # The module entries are of this form (P1/X1): + # { + # "name": "ams/0", + # "project_name": "", + # "sw_ver": "00.00.05.96", + # "loader_ver": "00.00.00.00", + # "ota_ver": "00.00.00.00", + # "hw_ver": "AMS08", + # "sn": "" + # } + # AMS Lite of the form: + # { + # "name": "ams_f1/0", + # "project_name": "", + # "sw_ver": "00.00.07.89", + # "loader_ver": "00.00.00.00", + # "ota_ver": "00.00.00.00", + # "hw_ver": "AMS_F102", + # "sn": "**REDACTED**" + # } + + data_changed = False + module_list = data.get("module", []) + for module in module_list: + name = module["name"] + index = -1 + if name.startswith("ams/"): + index = int(name[4]) + elif name.startswith("ams_f1/"): + index = int(name[7]) + + if index != -1: + # Sometimes we get incomplete version data. We have to skip if that occurs since the serial number is + # requires as part of the home assistant device identity. + if not module['sn'] == '': + # May get data before info so create entries if necessary + if self.data[index] is None: + self.data[index] = AMSInstance(self._client) + + if self.data[index].serial != module['sn']: + data_changed = True + self.data[index].serial = module['sn'] + if self.data[index].sw_version != module['sw_ver']: + data_changed = True + self.data[index].sw_version = module['sw_ver'] + if self.data[index].hw_version != module['hw_ver']: + data_changed = True + self.data[index].hw_version = module['hw_ver'] + + data_changed = data_changed or (old_data != f"{self.__dict__}") + + if data_changed: + if self._client.callback is not None: + self._client.callback("event_ams_info_update") + + def print_update(self, data) -> bool: + old_data = f"{self.__dict__}" + + # AMS json payload is of the form: + # "ams": { + # "ams": [ + # { + # "id": "0", + # "humidity": "4", + # "temp": "0.0", + # "tray": [ + # { + # "id": "0", + # "remain": -1, + # "k": 0.019999999552965164, # P1P only + # "n": 1.399999976158142, # P1P only + # "tag_uid": "0000000000000000", + # "tray_id_name": "", + # "tray_info_idx": "GFL99", + # "tray_type": "PLA", + # "tray_sub_brands": "", + # "tray_color": "FFFF00FF", # RRGGBBAA + # "tray_weight": "0", + # "tray_diameter": "0.00", + # "drying_temp": "0", + # "drying_time": "0", + # "bed_temp_type": "0", + # "bed_temp": "0", + # "nozzle_temp_max": "240", + # "nozzle_temp_min": "190", + # "remain": 100, + # "xcam_info": "000000000000000000000000", + # "tray_uuid": "00000000000000000000000000000000" + # }, + # { + # "id": "1", + # ... + # }, + # { + # "id": "2", + # ... + # }, + # { + # "id": "3", + # ... + # } + # ] + # } + # ], + # "ams_exist_bits": "1", + # "tray_exist_bits": "f", + # "tray_is_bbl_bits": "f", + # "tray_now": "255", + # "tray_read_done_bits": "f", + # "tray_reading_bits": "0", + # "tray_tar": "255", + # "version": 3, + # "insert_flag": true, + # "power_on_flag": false + # }, + + ams_data = data.get("ams", []) + if len(ams_data) != 0: + self.tray_now = int(ams_data.get('tray_now', self.tray_now)) + + ams_list = ams_data.get("ams", []) + for ams in ams_list: + index = int(ams['id']) + # May get data before info so create entry if necessary + if self.data[index] is None: + self.data[index] = AMSInstance(self._client) + + if self.data[index].humidity_index != int(ams['humidity']): + self.data[index].humidity_index = int(ams['humidity']) + if self.data[index].temperature != float(ams['temp']): + self.data[index].temperature = float(ams['temp']) + + tray_list = ams['tray'] + for tray in tray_list: + tray_id = int(tray['id']) + self.data[index].tray[tray_id].print_update(tray) + + data_changed = (old_data != f"{self.__dict__}") + return data_changed + +@dataclass +class AMSTray: + """Return all AMS tray related info""" + + def __init__(self, client): + self._client = client + self.empty = True + self.idx = "" + self.name = "" + self.type = "" + self.sub_brands = "" + self.color = "00000000" # RRGGBBAA + self.nozzle_temp_min = 0 + self.nozzle_temp_max = 0 + self.remain = 0 + self.k = 0 + self.tag_uid = "0000000000000000" + + def print_update(self, data) -> bool: + old_data = f"{self.__dict__}" + + if len(data) == 1: + # If the data is exactly one entry then it's just the ID and the tray is empty. + self.empty = True + self.idx = "" + self.name = "Empty" + self.type = "Empty" + self.sub_brands = "" + self.color = "00000000" # RRGGBBAA + self.nozzle_temp_min = 0 + self.nozzle_temp_max = 0 + self.remain = 0 + self.tag_uid = "0000000000000000" + self.k = 0 + else: + self.empty = False + self.idx = data.get('tray_info_idx', self.idx) + self.name = get_filament_name(self.idx, self._client.slicer_settings.custom_filaments) + self.type = data.get('tray_type', self.type) + self.sub_brands = data.get('tray_sub_brands', self.sub_brands) + self.color = data.get('tray_color', self.color) + self.nozzle_temp_min = data.get('nozzle_temp_min', self.nozzle_temp_min) + self.nozzle_temp_max = data.get('nozzle_temp_max', self.nozzle_temp_max) + self.remain = data.get('remain', self.remain) + self.tag_uid = data.get('tag_uid', self.tag_uid) + self.k = data.get('k', self.k) + + return (old_data != f"{self.__dict__}") + + +@dataclass +class ExternalSpool(AMSTray): + """Return the virtual tray related info""" + + def __init__(self, client): + super().__init__(client) + self._client = client + + def print_update(self, data) -> bool: + + # P1P virtual tray example + # "vt_tray": { + # "id": "254", + # "tag_uid": "0000000000000000", + # "tray_id_name": "", + # "tray_info_idx": "GFB99", + # "tray_type": "ABS", + # "tray_sub_brands": "", + # "tray_color": "000000FF", + # "tray_weight": "0", + # "tray_diameter": "0.00", + # "tray_temp": "0", + # "tray_time": "0", + # "bed_temp_type": "0", + # "bed_temp": "0", + # "nozzle_temp_max": "280", + # "nozzle_temp_min": "240", + # "remain": 100, + # "xcam_info": "000000000000000000000000", + # "tray_uuid": "00000000000000000000000000000000", + # "remain": 0, + # "k": 0.029999999329447746, + # "n": 1.399999976158142 + # }, + # + # This is exact same data as the AMS exposes so we can just defer to the AMSTray object + # to parse this json. + + received_virtual_tray_data = False + tray_data = data.get("vt_tray", {}) + if len(tray_data) != 0: + received_virtual_tray_data = super().print_update(tray_data) + + return received_virtual_tray_data + + +@dataclass +class Speed: + """Return speed profile information""" + _id: int + name: str + modifier: int + + def __init__(self, client): + self._client = client + self._id = 2 + self.name = get_speed_name(2) + self.modifier = 100 + + def print_update(self, data) -> bool: + old_data = f"{self.__dict__}" + + self._id = int(data.get("spd_lvl", self._id)) + self.name = get_speed_name(self._id) + self.modifier = int(data.get("spd_mag", self.modifier)) + + return (old_data != f"{self.__dict__}") + + def SetSpeed(self, option: str): + for id, speed in SPEED_PROFILE.items(): + if option == speed: + self._id = id + self.name = speed + command = SPEED_PROFILE_TEMPLATE + command['print']['param'] = f"{id}" + self._client.publish(command) + if self._client.callback is not None: + self._client.callback("event_speed_update") + + +@dataclass +class StageAction: + """Return Stage Action information""" + _id: int + _print_type: str + description: str + + def __init__(self): + self._id = 255 + self._print_type = "" + self.description = get_current_stage(self._id) + + def print_update(self, data) -> bool: + old_data = f"{self.__dict__}" + + self._print_type = data.get("print_type", self._print_type) + if self._print_type.lower() not in PRINT_TYPE_OPTIONS: + self._print_type = "unknown" + self._id = int(data.get("stg_cur", self._id)) + if (self._print_type == "idle") and (self._id == 0): + # On boot the printer reports stg_cur == 0 incorrectly instead of 255. Attempt to correct for this. + self._id = 255 + self.description = get_current_stage(self._id) + + return (old_data != f"{self.__dict__}") + +@dataclass +class HMSList: + """Return all HMS related info""" + _count: int + _errors: dict + + def __init__(self, client): + self._client = client + self._count = 0 + self._errors = {} + self._errors["Count"] = 0 + + def print_update(self, data) -> bool: + # Example payload: + # "hms": [ + # { + # "attr": 50331904, # In hex this is 0300 0100 + # "code": 65543 # In hex this is 0001 0007 + # } + # ], + # So this is HMS_0300_0100_0001_0007: + # https://wiki.bambulab.com/en/x1/troubleshooting/hmscode/0300_0100_0001_0007 + # 'The heatbed temperature is abnormal; the sensor may have an open circuit.' + + if 'hms' in data.keys(): + hmsList = data.get('hms', []) + self._count = len(hmsList) + errors = {} + errors["Count"] = self._count + + index: int = 0 + for hms in hmsList: + index = index + 1 + attr = int(hms['attr']) + code = int(hms['code']) + hms_notif = HMSNotification(attr=attr, code=code) + errors[f"{index}-Error"] = f"HMS_{hms_notif.hms_code}: {get_HMS_error_text(hms_notif.hms_code)}" + errors[f"{index}-Wiki"] = hms_notif.wiki_url + errors[f"{index}-Severity"] = hms_notif.severity + #LOGGER.debug(f"HMS error for '{hms_notif.module}' and severity '{hms_notif.severity}': HMS_{hms_notif.hms_code}") + #errors[f"{index}-Module"] = hms_notif.module # commented out to avoid bloat with current structure + + if self._errors != errors: + LOGGER.debug("Updating HMS error list.") + self._errors = errors + if self._count != 0: + LOGGER.warning(f"HMS ERRORS: {errors}") + if self._client.callback is not None: + self._client.callback("event_hms_errors") + return True + + return False + + @property + def errors(self) -> dict: + #LOGGER.debug(f"PROPERTYCALL: get_hms_errors") + return self._errors + + @property + def error_count(self) -> int: + return self._count + +@dataclass +class PrintErrorList: + """Return all print_error related info""" + _error: dict + _count: int + + def __init__(self, client): + self._client = client + self._error = {} + + def print_update(self, data) -> bool: + # Example payload: + # "print_error": 117473286 + # So this is 07008006 which we make more human readable to 0700-8006 + # https://e.bambulab.com/query.php?lang=en + # 'Unable to feed filament into the extruder. This could be due to entangled filament or a stuck spool. If not, please check if the AMS PTFE tube is connected.' + + if 'print_error' in data.keys(): + errors = {} + print_error_code = data.get('print_error') + if print_error_code != 0: + hex_conversion = f'0{int(print_error_code):x}' + print_error_code_hex = hex_conversion[slice(0,4,1)] + "_" + hex_conversion[slice(4,8,1)] + errors = {} + errors[f"Code"] = f"{print_error_code_hex.upper()}" + errors[f"Error"] = f"{print_error_code_hex.upper()}: {get_print_error_text(print_error_code)}" + # LOGGER.warning(f"PRINT ERRORS: {errors}") # This will emit a message to home assistant log every 1 second if enabled + + if self._error != errors: + self._error = errors + if self._client.callback is not None: + self._client.callback("event_print_error") + + # We send the error event directly so always return False for the general data event. + return False + + @property + def error(self) -> dict: + return self._error + + @property + def on(self) -> int: + return self._error is not None + + +@dataclass +class HMSNotification: + """Return an HMS object and all associated details""" + + def __init__(self, attr: int = 0, code: int = 0): + self.attr = attr + self.code = code + + @property + def severity(self): + return get_HMS_severity(self.code) + + @property + def module(self): + return get_HMS_module(self.attr) + + @property + def hms_code(self): + if self.attr > 0 and self.code > 0: + return f'{int(self.attr / 0x10000):0>4X}_{self.attr & 0xFFFF:0>4X}_{int(self.code / 0x10000):0>4X}_{self.code & 0xFFFF:0>4X}' # 0300_0100_0001_0007 + return "" + + @property + def wiki_url(self): + if self.attr > 0 and self.code > 0: + return f"https://wiki.bambulab.com/en/x1/troubleshooting/hmscode/{get_generic_AMS_HMS_error_code(self.hms_code)}" + return "" + + +@dataclass +class ChamberImage: + """Returns the latest jpeg data from the P1P camera""" + def __init__(self, client): + self._client = client + self._bytes = bytearray() + + def set_jpeg(self, bytes): + self._bytes = bytes + + def get_jpeg(self) -> bytearray: + return self._bytes.copy() + +@dataclass +class CoverImage: + """Returns the cover image from the Bambu API""" + + def __init__(self, client): + self._client = client + self._bytes = bytearray() + self._image_last_updated = datetime.now() + if self._client.callback is not None: + self._client.callback("event_printer_cover_image_update") + + def set_jpeg(self, bytes): + self._bytes = bytes + self._image_last_updated = datetime.now() + + def get_jpeg(self) -> bytearray: + return self._bytes + + def get_last_update_time(self) -> datetime: + return self._image_last_updated + + +@dataclass +class HomeFlag: + """Contains parsed _values from the homeflag sensor""" + _value: int + _sw_ver: str + _device_type: str + + def __init__(self, client): + self._value = 0 + self._client = client + self._sw_ver = "" + self._device_type = "" + + def info_update(self, data): + modules = data.get("module", []) + self._device_type = get_printer_type(modules, self._device_type) + self._sw_ver = get_sw_version(modules, self._sw_ver) + + def print_update(self, data: dict) -> bool: + old_data = f"{self.__dict__}" + self._value = int(data.get("home_flag", str(self._value))) + return (old_data != f"{self.__dict__}") + + @property + def door_open(self) -> bool or None: + if not self.door_open_available: + return None + + return (self._value & Home_Flag_Values.DOOR_OPEN) != 0 + + @property + def door_open_available(self) -> bool: + if not self._client._device.supports_feature(Features.DOOR_SENSOR): + return False + + if (self._device_type in ["X1", "X1C"] and version.parse(self._sw_ver) < version.parse("01.07.00.00")): + return False + + return True + + @property + def x_axis_homed(self) -> bool: + return (self._value & Home_Flag_Values.X_AXIS) != 0 + + @property + def y_axis_homed(self) -> bool: + return (self._value & Home_Flag_Values.Y_AXIS) != 0 + + @property + def z_axis_homed(self) -> bool: + return (self._value & Home_Flag_Values.Z_AXIS) != 0 + + @property + def homed(self) -> bool: + return self.x_axis_homed and self.y_axis_homed and self.z_axis_homed + + @property + def is_220V(self) -> bool: + return (self._value & Home_Flag_Values.VOLTAGE220) != 0 + + @property + def xcam_autorecovery_steploss(self) -> bool: + return (self._value & Home_Flag_Values.XCAM_AUTO_RECOVERY_STEP_LOSS) != 0 + + @property + def camera_recording(self) -> bool: + return (self._value & Home_Flag_Values.CAMERA_RECORDING) != 0 + + @property + def ams_calibrate_remaining(self) -> bool: + return (self._value & Home_Flag_Values.AMS_CALIBRATE_REMAINING) != 0 + + @property + def sdcard_present(self) -> bool: + return (self._value & Home_Flag_Values.SD_CARD_STATE) != SdcardState.NO_SDCARD + + @property + def sdcard_normal(self) -> bool: + return self.sdcard_present and (self._value & Home_Flag_Values.HAS_SDCARD_ABNORMAL) != SdcardState.HAS_SDCARD_ABNORMAL + + @property + def ams_auto_switch_filament(self) -> bool: + return (self._value & Home_Flag_Values.AMS_AUTO_SWITCH) != 0 + + @property + def wired_network_connection(self): + return (self._value & Home_Flag_Values.WIRED_NETWORK) != 0 + + @property + def xcam_prompt_sound(self) -> bool: + return (self._value & Home_Flag_Values.XCAM_ALLOW_PROMPT_SOUND) != 0 + + @property + def supports_motor_noise_calibration(self) -> bool: + return (self._value & Home_Flag_Values.SUPPORTS_MOTOR_CALIBRATION) != 0 + + @property + def p1s_upgrade_supported(self) -> bool: + return (self._value & Home_Flag_Values.SUPPORTED_PLUS) != 0 + + @property + def p1s_upgrade_installed(self) -> bool: + return (self._value & Home_Flag_Values.INSTALLED_PLUS) != 0 + + +class SlicerSettings: + custom_filaments: dict = field(default_factory=dict) + + def __init__(self, client): + self._client = client + self.custom_filaments = {} + + def _load_custom_filaments(self, slicer_settings: dict): + if 'private' in slicer_settings["filament"]: + for filament in slicer_settings['filament']['private']: + name = filament["name"] + if " @" in name: + name = name[:name.index(" @")] + if filament.get("filament_id", "") != "": + self.custom_filaments[filament["filament_id"]] = name + LOGGER.debug("Got custom filaments: %s", self.custom_filaments) + + def update(self): + self.custom_filaments = {} + if self._client.bambu_cloud.auth_token != "": + LOGGER.debug("Loading slicer settings") + slicer_settings = self._client.bambu_cloud.get_slicer_settings() + if slicer_settings is not None: + self._load_custom_filaments(slicer_settings) diff --git a/octoprint_bambu_printer/printer/pybambu/utils.py b/octoprint_bambu_printer/printer/pybambu/utils.py new file mode 100644 index 0000000..4b71824 --- /dev/null +++ b/octoprint_bambu_printer/printer/pybambu/utils.py @@ -0,0 +1,227 @@ +import math +from datetime import datetime, timedelta + +from .const import ( + CURRENT_STAGE_IDS, + SPEED_PROFILE, + FILAMENT_NAMES, + HMS_ERRORS, + HMS_AMS_ERRORS, + PRINT_ERROR_ERRORS, + HMS_SEVERITY_LEVELS, + HMS_MODULES, + LOGGER, + FansEnum, +) +from .commands import SEND_GCODE_TEMPLATE + + +def search(lst, predicate, default={}): + """Search an array for a string""" + for item in lst: + if predicate(item): + return item + return default + + +def fan_percentage(speed): + """Converts a fan speed to percentage""" + if not speed: + return 0 + percentage = (int(speed) / 15) * 100 + return round(percentage / 10) * 10 + + +def fan_percentage_to_gcode(fan: FansEnum, percentage: int): + """Converts a fan speed percentage to the gcode command to set that""" + if fan == FansEnum.PART_COOLING: + fanString = "P1" + elif fan == FansEnum.AUXILIARY: + fanString = "P2" + elif fan == FansEnum.CHAMBER: + fanString = "P3" + + percentage = round(percentage / 10) * 10 + speed = math.ceil(255 * percentage / 100) + command = SEND_GCODE_TEMPLATE + command['print']['param'] = f"M106 {fanString} S{speed}\n" + return command + + +def to_whole(number): + if not number: + return 0 + return round(number) + + +def get_filament_name(idx, custom_filaments: dict): + """Converts a filament idx to a human-readable name""" + result = FILAMENT_NAMES.get(idx, "unknown") + if result == "unknown" and idx != "": + result = custom_filaments.get(idx, "unknown") + if result == "unknown" and idx != "": + LOGGER.debug(f"UNKNOWN FILAMENT IDX: '{idx}'") + return result + + +def get_speed_name(id): + """Return the human-readable name for a speed id""" + return SPEED_PROFILE.get(int(id), "standard") + + +def get_current_stage(id) -> str: + """Return the human-readable description for a stage action""" + return CURRENT_STAGE_IDS.get(int(id), "unknown") + + +def get_HMS_error_text(hms_code: str): + """Return the human-readable description for an HMS error""" + + ams_code = get_generic_AMS_HMS_error_code(hms_code) + ams_error = HMS_AMS_ERRORS.get(ams_code, "") + if ams_error != "": + # 070X_xYxx_xxxx_xxxx = AMS X (0 based index) Slot Y (0 based index) has the error + ams_index = int(hms_code[3:4], 16) + 1 + ams_slot = int(hms_code[6:7], 16) + 1 + ams_error = ams_error.replace('AMS1', f"AMS{ams_index}") + ams_error = ams_error.replace('slot 1', f"slot {ams_slot}") + return ams_error + + return HMS_ERRORS.get(hms_code, "unknown") + + +def get_print_error_text(print_error_code: str): + """Return the human-readable description for a print error""" + + hex_conversion = f'0{int(print_error_code):x}' + print_error_code = hex_conversion[slice(0,4,1)] + "_" + hex_conversion[slice(4,8,1)] + print_error = PRINT_ERROR_ERRORS.get(print_error_code.upper(), "") + if print_error != "": + return print_error + + return PRINT_ERROR_ERRORS.get(print_error_code, "unknown") + + +def get_HMS_severity(code: int) -> str: + uint_code = code >> 16 + if code > 0 and uint_code in HMS_SEVERITY_LEVELS: + return HMS_SEVERITY_LEVELS[uint_code] + return HMS_SEVERITY_LEVELS["default"] + + +def get_HMS_module(attr: int) -> str: + uint_attr = (attr >> 24) & 0xFF + if attr > 0 and uint_attr in HMS_MODULES: + return HMS_MODULES[uint_attr] + return HMS_MODULES["default"] + + +def get_generic_AMS_HMS_error_code(hms_code: str): + code1 = int(hms_code[0:4], 16) + code2 = int(hms_code[5:9], 16) + code3 = int(hms_code[10:14], 16) + code4 = int(hms_code[15:19], 16) + + # 070X_xYxx_xxxx_xxxx = AMS X (0 based index) Slot Y (0 based index) has the error + ams_code = f"{code1 & 0xFFF8:0>4X}_{code2 & 0xF8FF:0>4X}_{code3:0>4X}_{code4:0>4X}" + ams_error = HMS_AMS_ERRORS.get(ams_code, "") + if ams_error != "": + return ams_code + + return f"{code1:0>4X}_{code2:0>4X}_{code3:0>4X}_{code4:0>4X}" + + +def get_printer_type(modules, default): + # Known possible values: + # + # A1/P1 printers are of the form: + # { + # "name": "esp32", + # "project_name": "C11", + # "sw_ver": "01.07.23.47", + # "hw_ver": "AP04", + # "sn": "**REDACTED**", + # "flag": 0 + # }, + # P1P = AP04 / C11 + # P1S = AP04 / C12 + # A1Mini = AP05 / N1 or AP04 / N1 or AP07 / N1 + # A1 = AP05 / N2S + # + # X1C printers are of the form: + # { + # "hw_ver": "AP05", + # "name": "rv1126", + # "sn": "**REDACTED**", + # "sw_ver": "00.00.28.55" + # }, + # X1C = AP05 + # + # X1E printers are of the form: + # { + # "flag": 0, + # "hw_ver": "AP02", + # "name": "ap", + # "sn": "**REDACTED**", + # "sw_ver": "00.00.32.14" + # } + # X1E = AP02 + + apNode = search(modules, lambda x: x.get('hw_ver', "").find("AP0") == 0) + if len(apNode.keys()) > 1: + hw_ver = apNode['hw_ver'] + project_name = apNode.get('project_name', '') + if hw_ver == 'AP02': + return 'X1E' + elif project_name == 'N1': + return 'A1MINI' + elif hw_ver == 'AP04': + if project_name == 'C11': + return 'P1P' + if project_name == 'C12': + return 'P1S' + elif hw_ver == 'AP05': + if project_name == 'N2S': + return 'A1' + if project_name == '': + return 'X1C' + LOGGER.debug(f"UNKNOWN DEVICE: hw_ver='{hw_ver}' / project_name='{project_name}'") + return default + + +def get_hw_version(modules, default): + """Retrieve hardware version of printer""" + apNode = search(modules, lambda x: x.get('hw_ver', "").find("AP0") == 0) + if len(apNode.keys()) > 1: + return apNode.get("hw_ver") + return default + + +def get_sw_version(modules, default): + """Retrieve software version of printer""" + ota = search(modules, lambda x: x.get('name', "") == "ota") + if len(ota.keys()) > 1: + return ota.get("sw_ver") + return default + + +def get_start_time(timestamp): + """Return start time of a print""" + if timestamp == 0: + return None + return datetime.fromtimestamp(timestamp) + + +def get_end_time(remaining_time): + """Calculate the end time of a print""" + end_time = round_minute(datetime.now() + timedelta(minutes=remaining_time)) + return end_time + + +def round_minute(date: datetime = None, round_to: int = 1): + """ Round datetime object to minutes""" + if not date: + date = datetime.now() + date = date.replace(second=0, microsecond=0) + delta = date.minute % round_to + return date.replace(minute=date.minute - delta) diff --git a/octoprint_bambu_printer/printer/states/idle_state.py b/octoprint_bambu_printer/printer/states/idle_state.py index 9780f02..9954f03 100644 --- a/octoprint_bambu_printer/printer/states/idle_state.py +++ b/octoprint_bambu_printer/printer/states/idle_state.py @@ -49,7 +49,7 @@ class IdleState(APrinterState): ), "layer_inspect": self._printer._settings.get_boolean(["layer_inspect"]), "use_ams": self._printer._settings.get_boolean(["use_ams"]), - "ams_mapping": "", + "ams_mapping": self._printer._settings.get(["ams_mapping"]), } } diff --git a/octoprint_bambu_printer/printer/states/paused_state.py b/octoprint_bambu_printer/printer/states/paused_state.py index 79d5d54..7c79d2e 100644 --- a/octoprint_bambu_printer/printer/states/paused_state.py +++ b/octoprint_bambu_printer/printer/states/paused_state.py @@ -8,7 +8,7 @@ if TYPE_CHECKING: import threading -import pybambu.commands +import octoprint_bambu_printer.printer.pybambu.commands from octoprint.util import RepeatedTimer from octoprint_bambu_printer.printer.states.a_printer_state import APrinterState diff --git a/octoprint_bambu_printer/printer/states/printing_state.py b/octoprint_bambu_printer/printer/states/printing_state.py index 0fb3fc1..52922c6 100644 --- a/octoprint_bambu_printer/printer/states/printing_state.py +++ b/octoprint_bambu_printer/printer/states/printing_state.py @@ -10,9 +10,9 @@ if TYPE_CHECKING: import threading -import pybambu -import pybambu.models -import pybambu.commands +import octoprint_bambu_printer.printer.pybambu +import octoprint_bambu_printer.printer.pybambu.models +import octoprint_bambu_printer.printer.pybambu.commands from octoprint_bambu_printer.printer.print_job import PrintJob from octoprint_bambu_printer.printer.states.a_printer_state import APrinterState diff --git a/octoprint_bambu_printer/static/css/bambu_printer.css b/octoprint_bambu_printer/static/css/bambu_printer.css new file mode 100644 index 0000000..d28dce0 --- /dev/null +++ b/octoprint_bambu_printer/static/css/bambu_printer.css @@ -0,0 +1,24 @@ +#sidebar_plugin_bambu_printer div.well { + min-height: 70px; +} + +#sidebar_plugin_bambu_printer div.well div.span3.text-center div.row-fluid { + padding-top: 10px; +} + +#sidebar_plugin_bambu_printer div.well div.span3.text-center div.row-fluid.active { + border: 2px solid; + -webkit-border-radius: 4px; + -moz-border-radius: 4px; + border-radius: 4px; +} + +#bambu_printer_print_options div.well { + min-height: 60px; +} + +#bambu_printer_print_options div.modal-body { + overflow: inherit !important; +} + + diff --git a/octoprint_bambu_printer/static/js/bambu_printer.js b/octoprint_bambu_printer/static/js/bambu_printer.js index 73afe44..f985b5b 100644 --- a/octoprint_bambu_printer/static/js/bambu_printer.js +++ b/octoprint_bambu_printer/static/js/bambu_printer.js @@ -15,6 +15,27 @@ $(function () { self.accessViewModel = parameters[3]; self.timelapseViewModel = parameters[4]; + self.use_ams = true; + self.ams_mapping = ko.observableArray([]); + + self.ams_mapping_computed = function(){ + var output_list = []; + var index = 0; + + ko.utils.arrayForEach(self.settingsViewModel.settings.plugins.bambu_printer.ams_data(), function(item){ + if(item){ + output_list = output_list.concat(item.tray()); + } + }); + + ko.utils.arrayForEach(output_list, function(item){ + item["index"] = ko.observable(index); + index++; + }); + + return output_list; + }; + self.getAuthToken = function (data) { self.settingsViewModel.settings.plugins.bambu_printer.auth_token(""); OctoPrint.simpleApiCommand("bambu_printer", "register", { @@ -68,7 +89,6 @@ $(function () { } if (data.files !== undefined) { - console.log(data.files); self.listHelper.updateItems(data.files); self.listHelper.resetPage(); } @@ -78,71 +98,59 @@ $(function () { $('#bambu_timelapse').appendTo("#timelapse"); }; + self.onAfterBinding = function () { + console.log(self.ams_mapping_computed()); + }; + self.showTimelapseThumbnail = function(data) { $("#bambu_printer_timelapse_thumbnail").attr("src", data.thumbnail); $("#bambu_printer_timelapse_preview").modal('show'); }; - /*$('#files div.upload-buttons > span.fileinput-button:first, #files div.folder-button').remove(); - $('#files div.upload-buttons > span.fileinput-button:first').removeClass('span6').addClass('input-block-level'); - - self.onBeforePrintStart = function(start_print_command) { - let confirmation_html = '' + - '
\n' + - '
\n' + - ' \n' + - '
\n' + - ' \n' + - '
\n' + - '
\n' + - '
'; - - if(!self.settingsViewModel.settings.plugins.bambu_printer.always_use_default_options()){ - confirmation_html += '\n' + - '
\n' + - '
\n' + - ' \n' + - ' \n' + - ' \n' + - '
\n' + - '
\n' + - ' \n' + - ' \n' + - ' \n' + - '
\n' + - '
\n'; + self.onBeforePrintStart = function(start_print_command, data) { + self.ams_mapping(self.ams_mapping_computed()); + self.start_print_command = start_print_command; + self.use_ams = self.settingsViewModel.settings.plugins.bambu_printer.use_ams(); + // prevent starting locally stored files, once data is added to core OctoPrint this + // could be adjusted to include additional processing like get sliced file's + // spool assignments and colors from plate_#.json inside 3mf file. + if(data && data.origin !== "sdcard") { + return false; } - - showConfirmationDialog({ - title: "Bambu Print Options", - html: confirmation_html, - cancel: gettext("Cancel"), - proceed: [gettext("Print"), gettext("Always")], - onproceed: function (idx) { - if(idx === 1){ - self.settingsViewModel.settings.plugins.bambu_printer.timelapse($('#bambu_printer_timelapse').is(':checked')); - self.settingsViewModel.settings.plugins.bambu_printer.bed_leveling($('#bambu_printer_bed_leveling').is(':checked')); - self.settingsViewModel.settings.plugins.bambu_printer.flow_cali($('#bambu_printer_flow_cali').is(':checked')); - self.settingsViewModel.settings.plugins.bambu_printer.vibration_cali($('#bambu_printer_vibration_cali').is(':checked')); - self.settingsViewModel.settings.plugins.bambu_printer.layer_inspect($('#bambu_printer_layer_inspect').is(':checked')); - self.settingsViewModel.settings.plugins.bambu_printer.use_ams($('#bambu_printer_use_ams').is(':checked')); - self.settingsViewModel.settings.plugins.bambu_printer.always_use_default_options(true); - self.settingsViewModel.saveData(); - } - // replace this with our own print command API call? - start_print_command(); - }, - nofade: true - }); + $("#bambu_printer_print_options").modal('show'); return false; - };*/ + }; + + self.toggle_spool_active = function(data) { + if(data.index() >= 0){ + data.original_index = ko.observable(data.index()); + data.index(-1); + } else { + data.index(data.original_index()); + } + }; + + self.cancel_print_options = function() { + self.settingsViewModel.settings.plugins.bambu_printer.use_ams(self.use_ams); + $("#bambu_printer_print_options").modal('hide'); + }; + + self.accept_print_options = function() { + console.log("starting print!!!!"); + console.log(self.ams_mapping()); + $("#bambu_printer_print_options").modal('hide'); + var flattened_ams_mapping = ko.utils.arrayMap(self.ams_mapping(), function(item) { + return item.index(); + }); + self.settingsViewModel.settings.plugins.bambu_printer.ams_mapping(flattened_ams_mapping); + self.settingsViewModel.saveData(undefined, self.start_print_command); + // self.settingsViewModel.saveData(); + }; } OCTOPRINT_VIEWMODELS.push({ construct: Bambu_printerViewModel, - // ViewModels your plugin depends on, e.g. loginStateViewModel, settingsViewModel, ... dependencies: ["settingsViewModel", "filesViewModel", "loginStateViewModel", "accessViewModel", "timelapseViewModel"], - // Elements to bind to, e.g. #settings_plugin_bambu_printer, #tab_plugin_bambu_printer, ... - elements: ["#bambu_printer_print_options", "#settings_plugin_bambu_printer", "#bambu_timelapse"] + elements: ["#bambu_printer_print_options", "#settings_plugin_bambu_printer", "#bambu_timelapse", "#sidebar_plugin_bambu_printer"] }); }); diff --git a/octoprint_bambu_printer/static/js/jquery-ui.min.js b/octoprint_bambu_printer/static/js/jquery-ui.min.js new file mode 100644 index 0000000..d3e8f68 --- /dev/null +++ b/octoprint_bambu_printer/static/js/jquery-ui.min.js @@ -0,0 +1,8 @@ +/*! jQuery UI - v1.12.1 - 2018-11-18 +* http://jqueryui.com +* Includes: widget.js, data.js, disable-selection.js, scroll-parent.js, widgets/draggable.js, widgets/droppable.js, widgets/resizable.js, widgets/selectable.js, widgets/sortable.js, widgets/mouse.js +* Copyright jQuery Foundation and other contributors; Licensed MIT */ + +(function(t){"function"==typeof define&&define.amd?define(["jquery"],t):t(jQuery)})(function(t){t.ui=t.ui||{},t.ui.version="1.12.1";var e=0,i=Array.prototype.slice;t.cleanData=function(e){return function(i){var s,n,o;for(o=0;null!=(n=i[o]);o++)try{s=t._data(n,"events"),s&&s.remove&&t(n).triggerHandler("remove")}catch(a){}e(i)}}(t.cleanData),t.widget=function(e,i,s){var n,o,a,r={},l=e.split(".")[0];e=e.split(".")[1];var h=l+"-"+e;return s||(s=i,i=t.Widget),t.isArray(s)&&(s=t.extend.apply(null,[{}].concat(s))),t.expr[":"][h.toLowerCase()]=function(e){return!!t.data(e,h)},t[l]=t[l]||{},n=t[l][e],o=t[l][e]=function(t,e){return this._createWidget?(arguments.length&&this._createWidget(t,e),void 0):new o(t,e)},t.extend(o,n,{version:s.version,_proto:t.extend({},s),_childConstructors:[]}),a=new i,a.options=t.widget.extend({},a.options),t.each(s,function(e,s){return t.isFunction(s)?(r[e]=function(){function t(){return i.prototype[e].apply(this,arguments)}function n(t){return i.prototype[e].apply(this,t)}return function(){var e,i=this._super,o=this._superApply;return this._super=t,this._superApply=n,e=s.apply(this,arguments),this._super=i,this._superApply=o,e}}(),void 0):(r[e]=s,void 0)}),o.prototype=t.widget.extend(a,{widgetEventPrefix:n?a.widgetEventPrefix||e:e},r,{constructor:o,namespace:l,widgetName:e,widgetFullName:h}),n?(t.each(n._childConstructors,function(e,i){var s=i.prototype;t.widget(s.namespace+"."+s.widgetName,o,i._proto)}),delete n._childConstructors):i._childConstructors.push(o),t.widget.bridge(e,o),o},t.widget.extend=function(e){for(var s,n,o=i.call(arguments,1),a=0,r=o.length;r>a;a++)for(s in o[a])n=o[a][s],o[a].hasOwnProperty(s)&&void 0!==n&&(e[s]=t.isPlainObject(n)?t.isPlainObject(e[s])?t.widget.extend({},e[s],n):t.widget.extend({},n):n);return e},t.widget.bridge=function(e,s){var n=s.prototype.widgetFullName||e;t.fn[e]=function(o){var a="string"==typeof o,r=i.call(arguments,1),l=this;return a?this.length||"instance"!==o?this.each(function(){var i,s=t.data(this,n);return"instance"===o?(l=s,!1):s?t.isFunction(s[o])&&"_"!==o.charAt(0)?(i=s[o].apply(s,r),i!==s&&void 0!==i?(l=i&&i.jquery?l.pushStack(i.get()):i,!1):void 0):t.error("no such method '"+o+"' for "+e+" widget instance"):t.error("cannot call methods on "+e+" prior to initialization; "+"attempted to call method '"+o+"'")}):l=void 0:(r.length&&(o=t.widget.extend.apply(null,[o].concat(r))),this.each(function(){var e=t.data(this,n);e?(e.option(o||{}),e._init&&e._init()):t.data(this,n,new s(o,this))})),l}},t.Widget=function(){},t.Widget._childConstructors=[],t.Widget.prototype={widgetName:"widget",widgetEventPrefix:"",defaultElement:"
",options:{classes:{},disabled:!1,create:null},_createWidget:function(i,s){s=t(s||this.defaultElement||this)[0],this.element=t(s),this.uuid=e++,this.eventNamespace="."+this.widgetName+this.uuid,this.bindings=t(),this.hoverable=t(),this.focusable=t(),this.classesElementLookup={},s!==this&&(t.data(s,this.widgetFullName,this),this._on(!0,this.element,{remove:function(t){t.target===s&&this.destroy()}}),this.document=t(s.style?s.ownerDocument:s.document||s),this.window=t(this.document[0].defaultView||this.document[0].parentWindow)),this.options=t.widget.extend({},this.options,this._getCreateOptions(),i),this._create(),this.options.disabled&&this._setOptionDisabled(this.options.disabled),this._trigger("create",null,this._getCreateEventData()),this._init()},_getCreateOptions:function(){return{}},_getCreateEventData:t.noop,_create:t.noop,_init:t.noop,destroy:function(){var e=this;this._destroy(),t.each(this.classesElementLookup,function(t,i){e._removeClass(i,t)}),this.element.off(this.eventNamespace).removeData(this.widgetFullName),this.widget().off(this.eventNamespace).removeAttr("aria-disabled"),this.bindings.off(this.eventNamespace)},_destroy:t.noop,widget:function(){return this.element},option:function(e,i){var s,n,o,a=e;if(0===arguments.length)return t.widget.extend({},this.options);if("string"==typeof e)if(a={},s=e.split("."),e=s.shift(),s.length){for(n=a[e]=t.widget.extend({},this.options[e]),o=0;s.length-1>o;o++)n[s[o]]=n[s[o]]||{},n=n[s[o]];if(e=s.pop(),1===arguments.length)return void 0===n[e]?null:n[e];n[e]=i}else{if(1===arguments.length)return void 0===this.options[e]?null:this.options[e];a[e]=i}return this._setOptions(a),this},_setOptions:function(t){var e;for(e in t)this._setOption(e,t[e]);return this},_setOption:function(t,e){return"classes"===t&&this._setOptionClasses(e),this.options[t]=e,"disabled"===t&&this._setOptionDisabled(e),this},_setOptionClasses:function(e){var i,s,n;for(i in e)n=this.classesElementLookup[i],e[i]!==this.options.classes[i]&&n&&n.length&&(s=t(n.get()),this._removeClass(n,i),s.addClass(this._classes({element:s,keys:i,classes:e,add:!0})))},_setOptionDisabled:function(t){this._toggleClass(this.widget(),this.widgetFullName+"-disabled",null,!!t),t&&(this._removeClass(this.hoverable,null,"ui-state-hover"),this._removeClass(this.focusable,null,"ui-state-focus"))},enable:function(){return this._setOptions({disabled:!1})},disable:function(){return this._setOptions({disabled:!0})},_classes:function(e){function i(i,o){var a,r;for(r=0;i.length>r;r++)a=n.classesElementLookup[i[r]]||t(),a=e.add?t(t.unique(a.get().concat(e.element.get()))):t(a.not(e.element).get()),n.classesElementLookup[i[r]]=a,s.push(i[r]),o&&e.classes[i[r]]&&s.push(e.classes[i[r]])}var s=[],n=this;return e=t.extend({element:this.element,classes:this.options.classes||{}},e),this._on(e.element,{remove:"_untrackClassesElement"}),e.keys&&i(e.keys.match(/\S+/g)||[],!0),e.extra&&i(e.extra.match(/\S+/g)||[]),s.join(" ")},_untrackClassesElement:function(e){var i=this;t.each(i.classesElementLookup,function(s,n){-1!==t.inArray(e.target,n)&&(i.classesElementLookup[s]=t(n.not(e.target).get()))})},_removeClass:function(t,e,i){return this._toggleClass(t,e,i,!1)},_addClass:function(t,e,i){return this._toggleClass(t,e,i,!0)},_toggleClass:function(t,e,i,s){s="boolean"==typeof s?s:i;var n="string"==typeof t||null===t,o={extra:n?e:i,keys:n?t:e,element:n?this.element:t,add:s};return o.element.toggleClass(this._classes(o),s),this},_on:function(e,i,s){var n,o=this;"boolean"!=typeof e&&(s=i,i=e,e=!1),s?(i=n=t(i),this.bindings=this.bindings.add(i)):(s=i,i=this.element,n=this.widget()),t.each(s,function(s,a){function r(){return e||o.options.disabled!==!0&&!t(this).hasClass("ui-state-disabled")?("string"==typeof a?o[a]:a).apply(o,arguments):void 0}"string"!=typeof a&&(r.guid=a.guid=a.guid||r.guid||t.guid++);var l=s.match(/^([\w:-]*)\s*(.*)$/),h=l[1]+o.eventNamespace,c=l[2];c?n.on(h,c,r):i.on(h,r)})},_off:function(e,i){i=(i||"").split(" ").join(this.eventNamespace+" ")+this.eventNamespace,e.off(i).off(i),this.bindings=t(this.bindings.not(e).get()),this.focusable=t(this.focusable.not(e).get()),this.hoverable=t(this.hoverable.not(e).get())},_delay:function(t,e){function i(){return("string"==typeof t?s[t]:t).apply(s,arguments)}var s=this;return setTimeout(i,e||0)},_hoverable:function(e){this.hoverable=this.hoverable.add(e),this._on(e,{mouseenter:function(e){this._addClass(t(e.currentTarget),null,"ui-state-hover")},mouseleave:function(e){this._removeClass(t(e.currentTarget),null,"ui-state-hover")}})},_focusable:function(e){this.focusable=this.focusable.add(e),this._on(e,{focusin:function(e){this._addClass(t(e.currentTarget),null,"ui-state-focus")},focusout:function(e){this._removeClass(t(e.currentTarget),null,"ui-state-focus")}})},_trigger:function(e,i,s){var n,o,a=this.options[e];if(s=s||{},i=t.Event(i),i.type=(e===this.widgetEventPrefix?e:this.widgetEventPrefix+e).toLowerCase(),i.target=this.element[0],o=i.originalEvent)for(n in o)n in i||(i[n]=o[n]);return this.element.trigger(i,s),!(t.isFunction(a)&&a.apply(this.element[0],[i].concat(s))===!1||i.isDefaultPrevented())}},t.each({show:"fadeIn",hide:"fadeOut"},function(e,i){t.Widget.prototype["_"+e]=function(s,n,o){"string"==typeof n&&(n={effect:n});var a,r=n?n===!0||"number"==typeof n?i:n.effect||i:e;n=n||{},"number"==typeof n&&(n={duration:n}),a=!t.isEmptyObject(n),n.complete=o,n.delay&&s.delay(n.delay),a&&t.effects&&t.effects.effect[r]?s[e](n):r!==e&&s[r]?s[r](n.duration,n.easing,o):s.queue(function(i){t(this)[e](),o&&o.call(s[0]),i()})}}),t.widget,t.extend(t.expr[":"],{data:t.expr.createPseudo?t.expr.createPseudo(function(e){return function(i){return!!t.data(i,e)}}):function(e,i,s){return!!t.data(e,s[3])}}),t.fn.extend({disableSelection:function(){var t="onselectstart"in document.createElement("div")?"selectstart":"mousedown";return function(){return this.on(t+".ui-disableSelection",function(t){t.preventDefault()})}}(),enableSelection:function(){return this.off(".ui-disableSelection")}}),t.fn.scrollParent=function(e){var i=this.css("position"),s="absolute"===i,n=e?/(auto|scroll|hidden)/:/(auto|scroll)/,o=this.parents().filter(function(){var e=t(this);return s&&"static"===e.css("position")?!1:n.test(e.css("overflow")+e.css("overflow-y")+e.css("overflow-x"))}).eq(0);return"fixed"!==i&&o.length?o:t(this[0].ownerDocument||document)},t.ui.ie=!!/msie [\w.]+/.exec(navigator.userAgent.toLowerCase());var s=!1;t(document).on("mouseup",function(){s=!1}),t.widget("ui.mouse",{version:"1.12.1",options:{cancel:"input, textarea, button, select, option",distance:1,delay:0},_mouseInit:function(){var e=this;this.element.on("mousedown."+this.widgetName,function(t){return e._mouseDown(t)}).on("click."+this.widgetName,function(i){return!0===t.data(i.target,e.widgetName+".preventClickEvent")?(t.removeData(i.target,e.widgetName+".preventClickEvent"),i.stopImmediatePropagation(),!1):void 0}),this.started=!1},_mouseDestroy:function(){this.element.off("."+this.widgetName),this._mouseMoveDelegate&&this.document.off("mousemove."+this.widgetName,this._mouseMoveDelegate).off("mouseup."+this.widgetName,this._mouseUpDelegate)},_mouseDown:function(e){if(!s){this._mouseMoved=!1,this._mouseStarted&&this._mouseUp(e),this._mouseDownEvent=e;var i=this,n=1===e.which,o="string"==typeof this.options.cancel&&e.target.nodeName?t(e.target).closest(this.options.cancel).length:!1;return n&&!o&&this._mouseCapture(e)?(this.mouseDelayMet=!this.options.delay,this.mouseDelayMet||(this._mouseDelayTimer=setTimeout(function(){i.mouseDelayMet=!0},this.options.delay)),this._mouseDistanceMet(e)&&this._mouseDelayMet(e)&&(this._mouseStarted=this._mouseStart(e)!==!1,!this._mouseStarted)?(e.preventDefault(),!0):(!0===t.data(e.target,this.widgetName+".preventClickEvent")&&t.removeData(e.target,this.widgetName+".preventClickEvent"),this._mouseMoveDelegate=function(t){return i._mouseMove(t)},this._mouseUpDelegate=function(t){return i._mouseUp(t)},this.document.on("mousemove."+this.widgetName,this._mouseMoveDelegate).on("mouseup."+this.widgetName,this._mouseUpDelegate),e.preventDefault(),s=!0,!0)):!0}},_mouseMove:function(e){if(this._mouseMoved){if(t.ui.ie&&(!document.documentMode||9>document.documentMode)&&!e.button)return this._mouseUp(e);if(!e.which)if(e.originalEvent.altKey||e.originalEvent.ctrlKey||e.originalEvent.metaKey||e.originalEvent.shiftKey)this.ignoreMissingWhich=!0;else if(!this.ignoreMissingWhich)return this._mouseUp(e)}return(e.which||e.button)&&(this._mouseMoved=!0),this._mouseStarted?(this._mouseDrag(e),e.preventDefault()):(this._mouseDistanceMet(e)&&this._mouseDelayMet(e)&&(this._mouseStarted=this._mouseStart(this._mouseDownEvent,e)!==!1,this._mouseStarted?this._mouseDrag(e):this._mouseUp(e)),!this._mouseStarted)},_mouseUp:function(e){this.document.off("mousemove."+this.widgetName,this._mouseMoveDelegate).off("mouseup."+this.widgetName,this._mouseUpDelegate),this._mouseStarted&&(this._mouseStarted=!1,e.target===this._mouseDownEvent.target&&t.data(e.target,this.widgetName+".preventClickEvent",!0),this._mouseStop(e)),this._mouseDelayTimer&&(clearTimeout(this._mouseDelayTimer),delete this._mouseDelayTimer),this.ignoreMissingWhich=!1,s=!1,e.preventDefault()},_mouseDistanceMet:function(t){return Math.max(Math.abs(this._mouseDownEvent.pageX-t.pageX),Math.abs(this._mouseDownEvent.pageY-t.pageY))>=this.options.distance},_mouseDelayMet:function(){return this.mouseDelayMet},_mouseStart:function(){},_mouseDrag:function(){},_mouseStop:function(){},_mouseCapture:function(){return!0}}),t.ui.plugin={add:function(e,i,s){var n,o=t.ui[e].prototype;for(n in s)o.plugins[n]=o.plugins[n]||[],o.plugins[n].push([i,s[n]])},call:function(t,e,i,s){var n,o=t.plugins[e];if(o&&(s||t.element[0].parentNode&&11!==t.element[0].parentNode.nodeType))for(n=0;o.length>n;n++)t.options[o[n][0]]&&o[n][1].apply(t.element,i)}},t.ui.safeActiveElement=function(t){var e;try{e=t.activeElement}catch(i){e=t.body}return e||(e=t.body),e.nodeName||(e=t.body),e},t.ui.safeBlur=function(e){e&&"body"!==e.nodeName.toLowerCase()&&t(e).trigger("blur")},t.widget("ui.draggable",t.ui.mouse,{version:"1.12.1",widgetEventPrefix:"drag",options:{addClasses:!0,appendTo:"parent",axis:!1,connectToSortable:!1,containment:!1,cursor:"auto",cursorAt:!1,grid:!1,handle:!1,helper:"original",iframeFix:!1,opacity:!1,refreshPositions:!1,revert:!1,revertDuration:500,scope:"default",scroll:!0,scrollSensitivity:20,scrollSpeed:20,snap:!1,snapMode:"both",snapTolerance:20,stack:!1,zIndex:!1,drag:null,start:null,stop:null},_create:function(){"original"===this.options.helper&&this._setPositionRelative(),this.options.addClasses&&this._addClass("ui-draggable"),this._setHandleClassName(),this._mouseInit()},_setOption:function(t,e){this._super(t,e),"handle"===t&&(this._removeHandleClassName(),this._setHandleClassName())},_destroy:function(){return(this.helper||this.element).is(".ui-draggable-dragging")?(this.destroyOnClear=!0,void 0):(this._removeHandleClassName(),this._mouseDestroy(),void 0)},_mouseCapture:function(e){var i=this.options;return this.helper||i.disabled||t(e.target).closest(".ui-resizable-handle").length>0?!1:(this.handle=this._getHandle(e),this.handle?(this._blurActiveElement(e),this._blockFrames(i.iframeFix===!0?"iframe":i.iframeFix),!0):!1)},_blockFrames:function(e){this.iframeBlocks=this.document.find(e).map(function(){var e=t(this);return t("
").css("position","absolute").appendTo(e.parent()).outerWidth(e.outerWidth()).outerHeight(e.outerHeight()).offset(e.offset())[0]})},_unblockFrames:function(){this.iframeBlocks&&(this.iframeBlocks.remove(),delete this.iframeBlocks)},_blurActiveElement:function(e){var i=t.ui.safeActiveElement(this.document[0]),s=t(e.target);s.closest(i).length||t.ui.safeBlur(i)},_mouseStart:function(e){var i=this.options;return this.helper=this._createHelper(e),this._addClass(this.helper,"ui-draggable-dragging"),this._cacheHelperProportions(),t.ui.ddmanager&&(t.ui.ddmanager.current=this),this._cacheMargins(),this.cssPosition=this.helper.css("position"),this.scrollParent=this.helper.scrollParent(!0),this.offsetParent=this.helper.offsetParent(),this.hasFixedAncestor=this.helper.parents().filter(function(){return"fixed"===t(this).css("position")}).length>0,this.positionAbs=this.element.offset(),this._refreshOffsets(e),this.originalPosition=this.position=this._generatePosition(e,!1),this.originalPageX=e.pageX,this.originalPageY=e.pageY,i.cursorAt&&this._adjustOffsetFromHelper(i.cursorAt),this._setContainment(),this._trigger("start",e)===!1?(this._clear(),!1):(this._cacheHelperProportions(),t.ui.ddmanager&&!i.dropBehaviour&&t.ui.ddmanager.prepareOffsets(this,e),this._mouseDrag(e,!0),t.ui.ddmanager&&t.ui.ddmanager.dragStart(this,e),!0)},_refreshOffsets:function(t){this.offset={top:this.positionAbs.top-this.margins.top,left:this.positionAbs.left-this.margins.left,scroll:!1,parent:this._getParentOffset(),relative:this._getRelativeOffset()},this.offset.click={left:t.pageX-this.offset.left,top:t.pageY-this.offset.top}},_mouseDrag:function(e,i){if(this.hasFixedAncestor&&(this.offset.parent=this._getParentOffset()),this.position=this._generatePosition(e,!0),this.positionAbs=this._convertPositionTo("absolute"),!i){var s=this._uiHash();if(this._trigger("drag",e,s)===!1)return this._mouseUp(new t.Event("mouseup",e)),!1;this.position=s.position}return this.helper[0].style.left=this.position.left+"px",this.helper[0].style.top=this.position.top+"px",t.ui.ddmanager&&t.ui.ddmanager.drag(this,e),!1},_mouseStop:function(e){var i=this,s=!1;return t.ui.ddmanager&&!this.options.dropBehaviour&&(s=t.ui.ddmanager.drop(this,e)),this.dropped&&(s=this.dropped,this.dropped=!1),"invalid"===this.options.revert&&!s||"valid"===this.options.revert&&s||this.options.revert===!0||t.isFunction(this.options.revert)&&this.options.revert.call(this.element,s)?t(this.helper).animate(this.originalPosition,parseInt(this.options.revertDuration,10),function(){i._trigger("stop",e)!==!1&&i._clear()}):this._trigger("stop",e)!==!1&&this._clear(),!1},_mouseUp:function(e){return this._unblockFrames(),t.ui.ddmanager&&t.ui.ddmanager.dragStop(this,e),this.handleElement.is(e.target)&&this.element.trigger("focus"),t.ui.mouse.prototype._mouseUp.call(this,e)},cancel:function(){return this.helper.is(".ui-draggable-dragging")?this._mouseUp(new t.Event("mouseup",{target:this.element[0]})):this._clear(),this},_getHandle:function(e){return this.options.handle?!!t(e.target).closest(this.element.find(this.options.handle)).length:!0},_setHandleClassName:function(){this.handleElement=this.options.handle?this.element.find(this.options.handle):this.element,this._addClass(this.handleElement,"ui-draggable-handle")},_removeHandleClassName:function(){this._removeClass(this.handleElement,"ui-draggable-handle")},_createHelper:function(e){var i=this.options,s=t.isFunction(i.helper),n=s?t(i.helper.apply(this.element[0],[e])):"clone"===i.helper?this.element.clone().removeAttr("id"):this.element;return n.parents("body").length||n.appendTo("parent"===i.appendTo?this.element[0].parentNode:i.appendTo),s&&n[0]===this.element[0]&&this._setPositionRelative(),n[0]===this.element[0]||/(fixed|absolute)/.test(n.css("position"))||n.css("position","absolute"),n},_setPositionRelative:function(){/^(?:r|a|f)/.test(this.element.css("position"))||(this.element[0].style.position="relative")},_adjustOffsetFromHelper:function(e){"string"==typeof e&&(e=e.split(" ")),t.isArray(e)&&(e={left:+e[0],top:+e[1]||0}),"left"in e&&(this.offset.click.left=e.left+this.margins.left),"right"in e&&(this.offset.click.left=this.helperProportions.width-e.right+this.margins.left),"top"in e&&(this.offset.click.top=e.top+this.margins.top),"bottom"in e&&(this.offset.click.top=this.helperProportions.height-e.bottom+this.margins.top)},_isRootNode:function(t){return/(html|body)/i.test(t.tagName)||t===this.document[0]},_getParentOffset:function(){var e=this.offsetParent.offset(),i=this.document[0];return"absolute"===this.cssPosition&&this.scrollParent[0]!==i&&t.contains(this.scrollParent[0],this.offsetParent[0])&&(e.left+=this.scrollParent.scrollLeft(),e.top+=this.scrollParent.scrollTop()),this._isRootNode(this.offsetParent[0])&&(e={top:0,left:0}),{top:e.top+(parseInt(this.offsetParent.css("borderTopWidth"),10)||0),left:e.left+(parseInt(this.offsetParent.css("borderLeftWidth"),10)||0)}},_getRelativeOffset:function(){if("relative"!==this.cssPosition)return{top:0,left:0};var t=this.element.position(),e=this._isRootNode(this.scrollParent[0]);return{top:t.top-(parseInt(this.helper.css("top"),10)||0)+(e?0:this.scrollParent.scrollTop()),left:t.left-(parseInt(this.helper.css("left"),10)||0)+(e?0:this.scrollParent.scrollLeft())}},_cacheMargins:function(){this.margins={left:parseInt(this.element.css("marginLeft"),10)||0,top:parseInt(this.element.css("marginTop"),10)||0,right:parseInt(this.element.css("marginRight"),10)||0,bottom:parseInt(this.element.css("marginBottom"),10)||0}},_cacheHelperProportions:function(){this.helperProportions={width:this.helper.outerWidth(),height:this.helper.outerHeight()}},_setContainment:function(){var e,i,s,n=this.options,o=this.document[0];return this.relativeContainer=null,n.containment?"window"===n.containment?(this.containment=[t(window).scrollLeft()-this.offset.relative.left-this.offset.parent.left,t(window).scrollTop()-this.offset.relative.top-this.offset.parent.top,t(window).scrollLeft()+t(window).width()-this.helperProportions.width-this.margins.left,t(window).scrollTop()+(t(window).height()||o.body.parentNode.scrollHeight)-this.helperProportions.height-this.margins.top],void 0):"document"===n.containment?(this.containment=[0,0,t(o).width()-this.helperProportions.width-this.margins.left,(t(o).height()||o.body.parentNode.scrollHeight)-this.helperProportions.height-this.margins.top],void 0):n.containment.constructor===Array?(this.containment=n.containment,void 0):("parent"===n.containment&&(n.containment=this.helper[0].parentNode),i=t(n.containment),s=i[0],s&&(e=/(scroll|auto)/.test(i.css("overflow")),this.containment=[(parseInt(i.css("borderLeftWidth"),10)||0)+(parseInt(i.css("paddingLeft"),10)||0),(parseInt(i.css("borderTopWidth"),10)||0)+(parseInt(i.css("paddingTop"),10)||0),(e?Math.max(s.scrollWidth,s.offsetWidth):s.offsetWidth)-(parseInt(i.css("borderRightWidth"),10)||0)-(parseInt(i.css("paddingRight"),10)||0)-this.helperProportions.width-this.margins.left-this.margins.right,(e?Math.max(s.scrollHeight,s.offsetHeight):s.offsetHeight)-(parseInt(i.css("borderBottomWidth"),10)||0)-(parseInt(i.css("paddingBottom"),10)||0)-this.helperProportions.height-this.margins.top-this.margins.bottom],this.relativeContainer=i),void 0):(this.containment=null,void 0)},_convertPositionTo:function(t,e){e||(e=this.position);var i="absolute"===t?1:-1,s=this._isRootNode(this.scrollParent[0]);return{top:e.top+this.offset.relative.top*i+this.offset.parent.top*i-("fixed"===this.cssPosition?-this.offset.scroll.top:s?0:this.offset.scroll.top)*i,left:e.left+this.offset.relative.left*i+this.offset.parent.left*i-("fixed"===this.cssPosition?-this.offset.scroll.left:s?0:this.offset.scroll.left)*i}},_generatePosition:function(t,e){var i,s,n,o,a=this.options,r=this._isRootNode(this.scrollParent[0]),l=t.pageX,h=t.pageY;return r&&this.offset.scroll||(this.offset.scroll={top:this.scrollParent.scrollTop(),left:this.scrollParent.scrollLeft()}),e&&(this.containment&&(this.relativeContainer?(s=this.relativeContainer.offset(),i=[this.containment[0]+s.left,this.containment[1]+s.top,this.containment[2]+s.left,this.containment[3]+s.top]):i=this.containment,t.pageX-this.offset.click.lefti[2]&&(l=i[2]+this.offset.click.left),t.pageY-this.offset.click.top>i[3]&&(h=i[3]+this.offset.click.top)),a.grid&&(n=a.grid[1]?this.originalPageY+Math.round((h-this.originalPageY)/a.grid[1])*a.grid[1]:this.originalPageY,h=i?n-this.offset.click.top>=i[1]||n-this.offset.click.top>i[3]?n:n-this.offset.click.top>=i[1]?n-a.grid[1]:n+a.grid[1]:n,o=a.grid[0]?this.originalPageX+Math.round((l-this.originalPageX)/a.grid[0])*a.grid[0]:this.originalPageX,l=i?o-this.offset.click.left>=i[0]||o-this.offset.click.left>i[2]?o:o-this.offset.click.left>=i[0]?o-a.grid[0]:o+a.grid[0]:o),"y"===a.axis&&(l=this.originalPageX),"x"===a.axis&&(h=this.originalPageY)),{top:h-this.offset.click.top-this.offset.relative.top-this.offset.parent.top+("fixed"===this.cssPosition?-this.offset.scroll.top:r?0:this.offset.scroll.top),left:l-this.offset.click.left-this.offset.relative.left-this.offset.parent.left+("fixed"===this.cssPosition?-this.offset.scroll.left:r?0:this.offset.scroll.left)}},_clear:function(){this._removeClass(this.helper,"ui-draggable-dragging"),this.helper[0]===this.element[0]||this.cancelHelperRemoval||this.helper.remove(),this.helper=null,this.cancelHelperRemoval=!1,this.destroyOnClear&&this.destroy()},_trigger:function(e,i,s){return s=s||this._uiHash(),t.ui.plugin.call(this,e,[i,s,this],!0),/^(drag|start|stop)/.test(e)&&(this.positionAbs=this._convertPositionTo("absolute"),s.offset=this.positionAbs),t.Widget.prototype._trigger.call(this,e,i,s)},plugins:{},_uiHash:function(){return{helper:this.helper,position:this.position,originalPosition:this.originalPosition,offset:this.positionAbs}}}),t.ui.plugin.add("draggable","connectToSortable",{start:function(e,i,s){var n=t.extend({},i,{item:s.element});s.sortables=[],t(s.options.connectToSortable).each(function(){var i=t(this).sortable("instance");i&&!i.options.disabled&&(s.sortables.push(i),i.refreshPositions(),i._trigger("activate",e,n))})},stop:function(e,i,s){var n=t.extend({},i,{item:s.element});s.cancelHelperRemoval=!1,t.each(s.sortables,function(){var t=this;t.isOver?(t.isOver=0,s.cancelHelperRemoval=!0,t.cancelHelperRemoval=!1,t._storedCSS={position:t.placeholder.css("position"),top:t.placeholder.css("top"),left:t.placeholder.css("left")},t._mouseStop(e),t.options.helper=t.options._helper):(t.cancelHelperRemoval=!0,t._trigger("deactivate",e,n))})},drag:function(e,i,s){t.each(s.sortables,function(){var n=!1,o=this;o.positionAbs=s.positionAbs,o.helperProportions=s.helperProportions,o.offset.click=s.offset.click,o._intersectsWith(o.containerCache)&&(n=!0,t.each(s.sortables,function(){return this.positionAbs=s.positionAbs,this.helperProportions=s.helperProportions,this.offset.click=s.offset.click,this!==o&&this._intersectsWith(this.containerCache)&&t.contains(o.element[0],this.element[0])&&(n=!1),n})),n?(o.isOver||(o.isOver=1,s._parent=i.helper.parent(),o.currentItem=i.helper.appendTo(o.element).data("ui-sortable-item",!0),o.options._helper=o.options.helper,o.options.helper=function(){return i.helper[0]},e.target=o.currentItem[0],o._mouseCapture(e,!0),o._mouseStart(e,!0,!0),o.offset.click.top=s.offset.click.top,o.offset.click.left=s.offset.click.left,o.offset.parent.left-=s.offset.parent.left-o.offset.parent.left,o.offset.parent.top-=s.offset.parent.top-o.offset.parent.top,s._trigger("toSortable",e),s.dropped=o.element,t.each(s.sortables,function(){this.refreshPositions()}),s.currentItem=s.element,o.fromOutside=s),o.currentItem&&(o._mouseDrag(e),i.position=o.position)):o.isOver&&(o.isOver=0,o.cancelHelperRemoval=!0,o.options._revert=o.options.revert,o.options.revert=!1,o._trigger("out",e,o._uiHash(o)),o._mouseStop(e,!0),o.options.revert=o.options._revert,o.options.helper=o.options._helper,o.placeholder&&o.placeholder.remove(),i.helper.appendTo(s._parent),s._refreshOffsets(e),i.position=s._generatePosition(e,!0),s._trigger("fromSortable",e),s.dropped=!1,t.each(s.sortables,function(){this.refreshPositions()}))})}}),t.ui.plugin.add("draggable","cursor",{start:function(e,i,s){var n=t("body"),o=s.options;n.css("cursor")&&(o._cursor=n.css("cursor")),n.css("cursor",o.cursor)},stop:function(e,i,s){var n=s.options;n._cursor&&t("body").css("cursor",n._cursor)}}),t.ui.plugin.add("draggable","opacity",{start:function(e,i,s){var n=t(i.helper),o=s.options;n.css("opacity")&&(o._opacity=n.css("opacity")),n.css("opacity",o.opacity)},stop:function(e,i,s){var n=s.options;n._opacity&&t(i.helper).css("opacity",n._opacity)}}),t.ui.plugin.add("draggable","scroll",{start:function(t,e,i){i.scrollParentNotHidden||(i.scrollParentNotHidden=i.helper.scrollParent(!1)),i.scrollParentNotHidden[0]!==i.document[0]&&"HTML"!==i.scrollParentNotHidden[0].tagName&&(i.overflowOffset=i.scrollParentNotHidden.offset())},drag:function(e,i,s){var n=s.options,o=!1,a=s.scrollParentNotHidden[0],r=s.document[0];a!==r&&"HTML"!==a.tagName?(n.axis&&"x"===n.axis||(s.overflowOffset.top+a.offsetHeight-e.pageY=0;d--)l=s.snapElements[d].left-s.margins.left,h=l+s.snapElements[d].width,c=s.snapElements[d].top-s.margins.top,u=c+s.snapElements[d].height,l-g>_||m>h+g||c-g>b||v>u+g||!t.contains(s.snapElements[d].item.ownerDocument,s.snapElements[d].item)?(s.snapElements[d].snapping&&s.options.snap.release&&s.options.snap.release.call(s.element,e,t.extend(s._uiHash(),{snapItem:s.snapElements[d].item})),s.snapElements[d].snapping=!1):("inner"!==f.snapMode&&(n=g>=Math.abs(c-b),o=g>=Math.abs(u-v),a=g>=Math.abs(l-_),r=g>=Math.abs(h-m),n&&(i.position.top=s._convertPositionTo("relative",{top:c-s.helperProportions.height,left:0}).top),o&&(i.position.top=s._convertPositionTo("relative",{top:u,left:0}).top),a&&(i.position.left=s._convertPositionTo("relative",{top:0,left:l-s.helperProportions.width}).left),r&&(i.position.left=s._convertPositionTo("relative",{top:0,left:h}).left)),p=n||o||a||r,"outer"!==f.snapMode&&(n=g>=Math.abs(c-v),o=g>=Math.abs(u-b),a=g>=Math.abs(l-m),r=g>=Math.abs(h-_),n&&(i.position.top=s._convertPositionTo("relative",{top:c,left:0}).top),o&&(i.position.top=s._convertPositionTo("relative",{top:u-s.helperProportions.height,left:0}).top),a&&(i.position.left=s._convertPositionTo("relative",{top:0,left:l}).left),r&&(i.position.left=s._convertPositionTo("relative",{top:0,left:h-s.helperProportions.width}).left)),!s.snapElements[d].snapping&&(n||o||a||r||p)&&s.options.snap.snap&&s.options.snap.snap.call(s.element,e,t.extend(s._uiHash(),{snapItem:s.snapElements[d].item})),s.snapElements[d].snapping=n||o||a||r||p)}}),t.ui.plugin.add("draggable","stack",{start:function(e,i,s){var n,o=s.options,a=t.makeArray(t(o.stack)).sort(function(e,i){return(parseInt(t(e).css("zIndex"),10)||0)-(parseInt(t(i).css("zIndex"),10)||0)});a.length&&(n=parseInt(t(a[0]).css("zIndex"),10)||0,t(a).each(function(e){t(this).css("zIndex",n+e)}),this.css("zIndex",n+a.length))}}),t.ui.plugin.add("draggable","zIndex",{start:function(e,i,s){var n=t(i.helper),o=s.options;n.css("zIndex")&&(o._zIndex=n.css("zIndex")),n.css("zIndex",o.zIndex)},stop:function(e,i,s){var n=s.options;n._zIndex&&t(i.helper).css("zIndex",n._zIndex)}}),t.ui.draggable,t.widget("ui.droppable",{version:"1.12.1",widgetEventPrefix:"drop",options:{accept:"*",addClasses:!0,greedy:!1,scope:"default",tolerance:"intersect",activate:null,deactivate:null,drop:null,out:null,over:null},_create:function(){var e,i=this.options,s=i.accept;this.isover=!1,this.isout=!0,this.accept=t.isFunction(s)?s:function(t){return t.is(s)},this.proportions=function(){return arguments.length?(e=arguments[0],void 0):e?e:e={width:this.element[0].offsetWidth,height:this.element[0].offsetHeight}},this._addToManager(i.scope),i.addClasses&&this._addClass("ui-droppable")},_addToManager:function(e){t.ui.ddmanager.droppables[e]=t.ui.ddmanager.droppables[e]||[],t.ui.ddmanager.droppables[e].push(this)},_splice:function(t){for(var e=0;t.length>e;e++)t[e]===this&&t.splice(e,1)},_destroy:function(){var e=t.ui.ddmanager.droppables[this.options.scope];this._splice(e)},_setOption:function(e,i){if("accept"===e)this.accept=t.isFunction(i)?i:function(t){return t.is(i)};else if("scope"===e){var s=t.ui.ddmanager.droppables[this.options.scope];this._splice(s),this._addToManager(i)}this._super(e,i)},_activate:function(e){var i=t.ui.ddmanager.current;this._addActiveClass(),i&&this._trigger("activate",e,this.ui(i)) +},_deactivate:function(e){var i=t.ui.ddmanager.current;this._removeActiveClass(),i&&this._trigger("deactivate",e,this.ui(i))},_over:function(e){var i=t.ui.ddmanager.current;i&&(i.currentItem||i.element)[0]!==this.element[0]&&this.accept.call(this.element[0],i.currentItem||i.element)&&(this._addHoverClass(),this._trigger("over",e,this.ui(i)))},_out:function(e){var i=t.ui.ddmanager.current;i&&(i.currentItem||i.element)[0]!==this.element[0]&&this.accept.call(this.element[0],i.currentItem||i.element)&&(this._removeHoverClass(),this._trigger("out",e,this.ui(i)))},_drop:function(e,i){var s=i||t.ui.ddmanager.current,o=!1;return s&&(s.currentItem||s.element)[0]!==this.element[0]?(this.element.find(":data(ui-droppable)").not(".ui-draggable-dragging").each(function(){var i=t(this).droppable("instance");return i.options.greedy&&!i.options.disabled&&i.options.scope===s.options.scope&&i.accept.call(i.element[0],s.currentItem||s.element)&&n(s,t.extend(i,{offset:i.element.offset()}),i.options.tolerance,e)?(o=!0,!1):void 0}),o?!1:this.accept.call(this.element[0],s.currentItem||s.element)?(this._removeActiveClass(),this._removeHoverClass(),this._trigger("drop",e,this.ui(s)),this.element):!1):!1},ui:function(t){return{draggable:t.currentItem||t.element,helper:t.helper,position:t.position,offset:t.positionAbs}},_addHoverClass:function(){this._addClass("ui-droppable-hover")},_removeHoverClass:function(){this._removeClass("ui-droppable-hover")},_addActiveClass:function(){this._addClass("ui-droppable-active")},_removeActiveClass:function(){this._removeClass("ui-droppable-active")}});var n=t.ui.intersect=function(){function t(t,e,i){return t>=e&&e+i>t}return function(e,i,s,n){if(!i.offset)return!1;var o=(e.positionAbs||e.position.absolute).left+e.margins.left,a=(e.positionAbs||e.position.absolute).top+e.margins.top,r=o+e.helperProportions.width,l=a+e.helperProportions.height,h=i.offset.left,c=i.offset.top,u=h+i.proportions().width,d=c+i.proportions().height;switch(s){case"fit":return o>=h&&u>=r&&a>=c&&d>=l;case"intersect":return o+e.helperProportions.width/2>h&&u>r-e.helperProportions.width/2&&a+e.helperProportions.height/2>c&&d>l-e.helperProportions.height/2;case"pointer":return t(n.pageY,c,i.proportions().height)&&t(n.pageX,h,i.proportions().width);case"touch":return(a>=c&&d>=a||l>=c&&d>=l||c>a&&l>d)&&(o>=h&&u>=o||r>=h&&u>=r||h>o&&r>u);default:return!1}}}();t.ui.ddmanager={current:null,droppables:{"default":[]},prepareOffsets:function(e,i){var s,n,o=t.ui.ddmanager.droppables[e.options.scope]||[],a=i?i.type:null,r=(e.currentItem||e.element).find(":data(ui-droppable)").addBack();t:for(s=0;o.length>s;s++)if(!(o[s].options.disabled||e&&!o[s].accept.call(o[s].element[0],e.currentItem||e.element))){for(n=0;r.length>n;n++)if(r[n]===o[s].element[0]){o[s].proportions().height=0;continue t}o[s].visible="none"!==o[s].element.css("display"),o[s].visible&&("mousedown"===a&&o[s]._activate.call(o[s],i),o[s].offset=o[s].element.offset(),o[s].proportions({width:o[s].element[0].offsetWidth,height:o[s].element[0].offsetHeight}))}},drop:function(e,i){var s=!1;return t.each((t.ui.ddmanager.droppables[e.options.scope]||[]).slice(),function(){this.options&&(!this.options.disabled&&this.visible&&n(e,this,this.options.tolerance,i)&&(s=this._drop.call(this,i)||s),!this.options.disabled&&this.visible&&this.accept.call(this.element[0],e.currentItem||e.element)&&(this.isout=!0,this.isover=!1,this._deactivate.call(this,i)))}),s},dragStart:function(e,i){e.element.parentsUntil("body").on("scroll.droppable",function(){e.options.refreshPositions||t.ui.ddmanager.prepareOffsets(e,i)})},drag:function(e,i){e.options.refreshPositions&&t.ui.ddmanager.prepareOffsets(e,i),t.each(t.ui.ddmanager.droppables[e.options.scope]||[],function(){if(!this.options.disabled&&!this.greedyChild&&this.visible){var s,o,a,r=n(e,this,this.options.tolerance,i),l=!r&&this.isover?"isout":r&&!this.isover?"isover":null;l&&(this.options.greedy&&(o=this.options.scope,a=this.element.parents(":data(ui-droppable)").filter(function(){return t(this).droppable("instance").options.scope===o}),a.length&&(s=t(a[0]).droppable("instance"),s.greedyChild="isover"===l)),s&&"isover"===l&&(s.isover=!1,s.isout=!0,s._out.call(s,i)),this[l]=!0,this["isout"===l?"isover":"isout"]=!1,this["isover"===l?"_over":"_out"].call(this,i),s&&"isout"===l&&(s.isout=!1,s.isover=!0,s._over.call(s,i)))}})},dragStop:function(e,i){e.element.parentsUntil("body").off("scroll.droppable"),e.options.refreshPositions||t.ui.ddmanager.prepareOffsets(e,i)}},t.uiBackCompat!==!1&&t.widget("ui.droppable",t.ui.droppable,{options:{hoverClass:!1,activeClass:!1},_addActiveClass:function(){this._super(),this.options.activeClass&&this.element.addClass(this.options.activeClass)},_removeActiveClass:function(){this._super(),this.options.activeClass&&this.element.removeClass(this.options.activeClass)},_addHoverClass:function(){this._super(),this.options.hoverClass&&this.element.addClass(this.options.hoverClass)},_removeHoverClass:function(){this._super(),this.options.hoverClass&&this.element.removeClass(this.options.hoverClass)}}),t.ui.droppable,t.widget("ui.resizable",t.ui.mouse,{version:"1.12.1",widgetEventPrefix:"resize",options:{alsoResize:!1,animate:!1,animateDuration:"slow",animateEasing:"swing",aspectRatio:!1,autoHide:!1,classes:{"ui-resizable-se":"ui-icon ui-icon-gripsmall-diagonal-se"},containment:!1,ghost:!1,grid:!1,handles:"e,s,se",helper:!1,maxHeight:null,maxWidth:null,minHeight:10,minWidth:10,zIndex:90,resize:null,start:null,stop:null},_num:function(t){return parseFloat(t)||0},_isNumber:function(t){return!isNaN(parseFloat(t))},_hasScroll:function(e,i){if("hidden"===t(e).css("overflow"))return!1;var s=i&&"left"===i?"scrollLeft":"scrollTop",n=!1;return e[s]>0?!0:(e[s]=1,n=e[s]>0,e[s]=0,n)},_create:function(){var e,i=this.options,s=this;this._addClass("ui-resizable"),t.extend(this,{_aspectRatio:!!i.aspectRatio,aspectRatio:i.aspectRatio,originalElement:this.element,_proportionallyResizeElements:[],_helper:i.helper||i.ghost||i.animate?i.helper||"ui-resizable-helper":null}),this.element[0].nodeName.match(/^(canvas|textarea|input|select|button|img)$/i)&&(this.element.wrap(t("
").css({position:this.element.css("position"),width:this.element.outerWidth(),height:this.element.outerHeight(),top:this.element.css("top"),left:this.element.css("left")})),this.element=this.element.parent().data("ui-resizable",this.element.resizable("instance")),this.elementIsWrapper=!0,e={marginTop:this.originalElement.css("marginTop"),marginRight:this.originalElement.css("marginRight"),marginBottom:this.originalElement.css("marginBottom"),marginLeft:this.originalElement.css("marginLeft")},this.element.css(e),this.originalElement.css("margin",0),this.originalResizeStyle=this.originalElement.css("resize"),this.originalElement.css("resize","none"),this._proportionallyResizeElements.push(this.originalElement.css({position:"static",zoom:1,display:"block"})),this.originalElement.css(e),this._proportionallyResize()),this._setupHandles(),i.autoHide&&t(this.element).on("mouseenter",function(){i.disabled||(s._removeClass("ui-resizable-autohide"),s._handles.show())}).on("mouseleave",function(){i.disabled||s.resizing||(s._addClass("ui-resizable-autohide"),s._handles.hide())}),this._mouseInit()},_destroy:function(){this._mouseDestroy();var e,i=function(e){t(e).removeData("resizable").removeData("ui-resizable").off(".resizable").find(".ui-resizable-handle").remove()};return this.elementIsWrapper&&(i(this.element),e=this.element,this.originalElement.css({position:e.css("position"),width:e.outerWidth(),height:e.outerHeight(),top:e.css("top"),left:e.css("left")}).insertAfter(e),e.remove()),this.originalElement.css("resize",this.originalResizeStyle),i(this.originalElement),this},_setOption:function(t,e){switch(this._super(t,e),t){case"handles":this._removeHandles(),this._setupHandles();break;default:}},_setupHandles:function(){var e,i,s,n,o,a=this.options,r=this;if(this.handles=a.handles||(t(".ui-resizable-handle",this.element).length?{n:".ui-resizable-n",e:".ui-resizable-e",s:".ui-resizable-s",w:".ui-resizable-w",se:".ui-resizable-se",sw:".ui-resizable-sw",ne:".ui-resizable-ne",nw:".ui-resizable-nw"}:"e,s,se"),this._handles=t(),this.handles.constructor===String)for("all"===this.handles&&(this.handles="n,e,s,w,se,sw,ne,nw"),s=this.handles.split(","),this.handles={},i=0;s.length>i;i++)e=t.trim(s[i]),n="ui-resizable-"+e,o=t("
"),this._addClass(o,"ui-resizable-handle "+n),o.css({zIndex:a.zIndex}),this.handles[e]=".ui-resizable-"+e,this.element.append(o);this._renderAxis=function(e){var i,s,n,o;e=e||this.element;for(i in this.handles)this.handles[i].constructor===String?this.handles[i]=this.element.children(this.handles[i]).first().show():(this.handles[i].jquery||this.handles[i].nodeType)&&(this.handles[i]=t(this.handles[i]),this._on(this.handles[i],{mousedown:r._mouseDown})),this.elementIsWrapper&&this.originalElement[0].nodeName.match(/^(textarea|input|select|button)$/i)&&(s=t(this.handles[i],this.element),o=/sw|ne|nw|se|n|s/.test(i)?s.outerHeight():s.outerWidth(),n=["padding",/ne|nw|n/.test(i)?"Top":/se|sw|s/.test(i)?"Bottom":/^e$/.test(i)?"Right":"Left"].join(""),e.css(n,o),this._proportionallyResize()),this._handles=this._handles.add(this.handles[i])},this._renderAxis(this.element),this._handles=this._handles.add(this.element.find(".ui-resizable-handle")),this._handles.disableSelection(),this._handles.on("mouseover",function(){r.resizing||(this.className&&(o=this.className.match(/ui-resizable-(se|sw|ne|nw|n|e|s|w)/i)),r.axis=o&&o[1]?o[1]:"se")}),a.autoHide&&(this._handles.hide(),this._addClass("ui-resizable-autohide"))},_removeHandles:function(){this._handles.remove()},_mouseCapture:function(e){var i,s,n=!1;for(i in this.handles)s=t(this.handles[i])[0],(s===e.target||t.contains(s,e.target))&&(n=!0);return!this.options.disabled&&n},_mouseStart:function(e){var i,s,n,o=this.options,a=this.element;return this.resizing=!0,this._renderProxy(),i=this._num(this.helper.css("left")),s=this._num(this.helper.css("top")),o.containment&&(i+=t(o.containment).scrollLeft()||0,s+=t(o.containment).scrollTop()||0),this.offset=this.helper.offset(),this.position={left:i,top:s},this.size=this._helper?{width:this.helper.width(),height:this.helper.height()}:{width:a.width(),height:a.height()},this.originalSize=this._helper?{width:a.outerWidth(),height:a.outerHeight()}:{width:a.width(),height:a.height()},this.sizeDiff={width:a.outerWidth()-a.width(),height:a.outerHeight()-a.height()},this.originalPosition={left:i,top:s},this.originalMousePosition={left:e.pageX,top:e.pageY},this.aspectRatio="number"==typeof o.aspectRatio?o.aspectRatio:this.originalSize.width/this.originalSize.height||1,n=t(".ui-resizable-"+this.axis).css("cursor"),t("body").css("cursor","auto"===n?this.axis+"-resize":n),this._addClass("ui-resizable-resizing"),this._propagate("start",e),!0},_mouseDrag:function(e){var i,s,n=this.originalMousePosition,o=this.axis,a=e.pageX-n.left||0,r=e.pageY-n.top||0,l=this._change[o];return this._updatePrevProperties(),l?(i=l.apply(this,[e,a,r]),this._updateVirtualBoundaries(e.shiftKey),(this._aspectRatio||e.shiftKey)&&(i=this._updateRatio(i,e)),i=this._respectSize(i,e),this._updateCache(i),this._propagate("resize",e),s=this._applyChanges(),!this._helper&&this._proportionallyResizeElements.length&&this._proportionallyResize(),t.isEmptyObject(s)||(this._updatePrevProperties(),this._trigger("resize",e,this.ui()),this._applyChanges()),!1):!1},_mouseStop:function(e){this.resizing=!1;var i,s,n,o,a,r,l,h=this.options,c=this;return this._helper&&(i=this._proportionallyResizeElements,s=i.length&&/textarea/i.test(i[0].nodeName),n=s&&this._hasScroll(i[0],"left")?0:c.sizeDiff.height,o=s?0:c.sizeDiff.width,a={width:c.helper.width()-o,height:c.helper.height()-n},r=parseFloat(c.element.css("left"))+(c.position.left-c.originalPosition.left)||null,l=parseFloat(c.element.css("top"))+(c.position.top-c.originalPosition.top)||null,h.animate||this.element.css(t.extend(a,{top:l,left:r})),c.helper.height(c.size.height),c.helper.width(c.size.width),this._helper&&!h.animate&&this._proportionallyResize()),t("body").css("cursor","auto"),this._removeClass("ui-resizable-resizing"),this._propagate("stop",e),this._helper&&this.helper.remove(),!1},_updatePrevProperties:function(){this.prevPosition={top:this.position.top,left:this.position.left},this.prevSize={width:this.size.width,height:this.size.height}},_applyChanges:function(){var t={};return this.position.top!==this.prevPosition.top&&(t.top=this.position.top+"px"),this.position.left!==this.prevPosition.left&&(t.left=this.position.left+"px"),this.size.width!==this.prevSize.width&&(t.width=this.size.width+"px"),this.size.height!==this.prevSize.height&&(t.height=this.size.height+"px"),this.helper.css(t),t},_updateVirtualBoundaries:function(t){var e,i,s,n,o,a=this.options;o={minWidth:this._isNumber(a.minWidth)?a.minWidth:0,maxWidth:this._isNumber(a.maxWidth)?a.maxWidth:1/0,minHeight:this._isNumber(a.minHeight)?a.minHeight:0,maxHeight:this._isNumber(a.maxHeight)?a.maxHeight:1/0},(this._aspectRatio||t)&&(e=o.minHeight*this.aspectRatio,s=o.minWidth/this.aspectRatio,i=o.maxHeight*this.aspectRatio,n=o.maxWidth/this.aspectRatio,e>o.minWidth&&(o.minWidth=e),s>o.minHeight&&(o.minHeight=s),o.maxWidth>i&&(o.maxWidth=i),o.maxHeight>n&&(o.maxHeight=n)),this._vBoundaries=o},_updateCache:function(t){this.offset=this.helper.offset(),this._isNumber(t.left)&&(this.position.left=t.left),this._isNumber(t.top)&&(this.position.top=t.top),this._isNumber(t.height)&&(this.size.height=t.height),this._isNumber(t.width)&&(this.size.width=t.width)},_updateRatio:function(t){var e=this.position,i=this.size,s=this.axis;return this._isNumber(t.height)?t.width=t.height*this.aspectRatio:this._isNumber(t.width)&&(t.height=t.width/this.aspectRatio),"sw"===s&&(t.left=e.left+(i.width-t.width),t.top=null),"nw"===s&&(t.top=e.top+(i.height-t.height),t.left=e.left+(i.width-t.width)),t},_respectSize:function(t){var e=this._vBoundaries,i=this.axis,s=this._isNumber(t.width)&&e.maxWidth&&e.maxWidtht.width,a=this._isNumber(t.height)&&e.minHeight&&e.minHeight>t.height,r=this.originalPosition.left+this.originalSize.width,l=this.originalPosition.top+this.originalSize.height,h=/sw|nw|w/.test(i),c=/nw|ne|n/.test(i);return o&&(t.width=e.minWidth),a&&(t.height=e.minHeight),s&&(t.width=e.maxWidth),n&&(t.height=e.maxHeight),o&&h&&(t.left=r-e.minWidth),s&&h&&(t.left=r-e.maxWidth),a&&c&&(t.top=l-e.minHeight),n&&c&&(t.top=l-e.maxHeight),t.width||t.height||t.left||!t.top?t.width||t.height||t.top||!t.left||(t.left=null):t.top=null,t},_getPaddingPlusBorderDimensions:function(t){for(var e=0,i=[],s=[t.css("borderTopWidth"),t.css("borderRightWidth"),t.css("borderBottomWidth"),t.css("borderLeftWidth")],n=[t.css("paddingTop"),t.css("paddingRight"),t.css("paddingBottom"),t.css("paddingLeft")];4>e;e++)i[e]=parseFloat(s[e])||0,i[e]+=parseFloat(n[e])||0;return{height:i[0]+i[2],width:i[1]+i[3]}},_proportionallyResize:function(){if(this._proportionallyResizeElements.length)for(var t,e=0,i=this.helper||this.element;this._proportionallyResizeElements.length>e;e++)t=this._proportionallyResizeElements[e],this.outerDimensions||(this.outerDimensions=this._getPaddingPlusBorderDimensions(t)),t.css({height:i.height()-this.outerDimensions.height||0,width:i.width()-this.outerDimensions.width||0})},_renderProxy:function(){var e=this.element,i=this.options;this.elementOffset=e.offset(),this._helper?(this.helper=this.helper||t("
"),this._addClass(this.helper,this._helper),this.helper.css({width:this.element.outerWidth(),height:this.element.outerHeight(),position:"absolute",left:this.elementOffset.left+"px",top:this.elementOffset.top+"px",zIndex:++i.zIndex}),this.helper.appendTo("body").disableSelection()):this.helper=this.element},_change:{e:function(t,e){return{width:this.originalSize.width+e}},w:function(t,e){var i=this.originalSize,s=this.originalPosition;return{left:s.left+e,width:i.width-e}},n:function(t,e,i){var s=this.originalSize,n=this.originalPosition;return{top:n.top+i,height:s.height-i}},s:function(t,e,i){return{height:this.originalSize.height+i}},se:function(e,i,s){return t.extend(this._change.s.apply(this,arguments),this._change.e.apply(this,[e,i,s]))},sw:function(e,i,s){return t.extend(this._change.s.apply(this,arguments),this._change.w.apply(this,[e,i,s]))},ne:function(e,i,s){return t.extend(this._change.n.apply(this,arguments),this._change.e.apply(this,[e,i,s]))},nw:function(e,i,s){return t.extend(this._change.n.apply(this,arguments),this._change.w.apply(this,[e,i,s]))}},_propagate:function(e,i){t.ui.plugin.call(this,e,[i,this.ui()]),"resize"!==e&&this._trigger(e,i,this.ui())},plugins:{},ui:function(){return{originalElement:this.originalElement,element:this.element,helper:this.helper,position:this.position,size:this.size,originalSize:this.originalSize,originalPosition:this.originalPosition}}}),t.ui.plugin.add("resizable","animate",{stop:function(e){var i=t(this).resizable("instance"),s=i.options,n=i._proportionallyResizeElements,o=n.length&&/textarea/i.test(n[0].nodeName),a=o&&i._hasScroll(n[0],"left")?0:i.sizeDiff.height,r=o?0:i.sizeDiff.width,l={width:i.size.width-r,height:i.size.height-a},h=parseFloat(i.element.css("left"))+(i.position.left-i.originalPosition.left)||null,c=parseFloat(i.element.css("top"))+(i.position.top-i.originalPosition.top)||null;i.element.animate(t.extend(l,c&&h?{top:c,left:h}:{}),{duration:s.animateDuration,easing:s.animateEasing,step:function(){var s={width:parseFloat(i.element.css("width")),height:parseFloat(i.element.css("height")),top:parseFloat(i.element.css("top")),left:parseFloat(i.element.css("left"))};n&&n.length&&t(n[0]).css({width:s.width,height:s.height}),i._updateCache(s),i._propagate("resize",e)}})}}),t.ui.plugin.add("resizable","containment",{start:function(){var e,i,s,n,o,a,r,l=t(this).resizable("instance"),h=l.options,c=l.element,u=h.containment,d=u instanceof t?u.get(0):/parent/.test(u)?c.parent().get(0):u;d&&(l.containerElement=t(d),/document/.test(u)||u===document?(l.containerOffset={left:0,top:0},l.containerPosition={left:0,top:0},l.parentData={element:t(document),left:0,top:0,width:t(document).width(),height:t(document).height()||document.body.parentNode.scrollHeight}):(e=t(d),i=[],t(["Top","Right","Left","Bottom"]).each(function(t,s){i[t]=l._num(e.css("padding"+s))}),l.containerOffset=e.offset(),l.containerPosition=e.position(),l.containerSize={height:e.innerHeight()-i[3],width:e.innerWidth()-i[1]},s=l.containerOffset,n=l.containerSize.height,o=l.containerSize.width,a=l._hasScroll(d,"left")?d.scrollWidth:o,r=l._hasScroll(d)?d.scrollHeight:n,l.parentData={element:d,left:s.left,top:s.top,width:a,height:r}))},resize:function(e){var i,s,n,o,a=t(this).resizable("instance"),r=a.options,l=a.containerOffset,h=a.position,c=a._aspectRatio||e.shiftKey,u={top:0,left:0},d=a.containerElement,p=!0;d[0]!==document&&/static/.test(d.css("position"))&&(u=l),h.left<(a._helper?l.left:0)&&(a.size.width=a.size.width+(a._helper?a.position.left-l.left:a.position.left-u.left),c&&(a.size.height=a.size.width/a.aspectRatio,p=!1),a.position.left=r.helper?l.left:0),h.top<(a._helper?l.top:0)&&(a.size.height=a.size.height+(a._helper?a.position.top-l.top:a.position.top),c&&(a.size.width=a.size.height*a.aspectRatio,p=!1),a.position.top=a._helper?l.top:0),n=a.containerElement.get(0)===a.element.parent().get(0),o=/relative|absolute/.test(a.containerElement.css("position")),n&&o?(a.offset.left=a.parentData.left+a.position.left,a.offset.top=a.parentData.top+a.position.top):(a.offset.left=a.element.offset().left,a.offset.top=a.element.offset().top),i=Math.abs(a.sizeDiff.width+(a._helper?a.offset.left-u.left:a.offset.left-l.left)),s=Math.abs(a.sizeDiff.height+(a._helper?a.offset.top-u.top:a.offset.top-l.top)),i+a.size.width>=a.parentData.width&&(a.size.width=a.parentData.width-i,c&&(a.size.height=a.size.width/a.aspectRatio,p=!1)),s+a.size.height>=a.parentData.height&&(a.size.height=a.parentData.height-s,c&&(a.size.width=a.size.height*a.aspectRatio,p=!1)),p||(a.position.left=a.prevPosition.left,a.position.top=a.prevPosition.top,a.size.width=a.prevSize.width,a.size.height=a.prevSize.height)},stop:function(){var e=t(this).resizable("instance"),i=e.options,s=e.containerOffset,n=e.containerPosition,o=e.containerElement,a=t(e.helper),r=a.offset(),l=a.outerWidth()-e.sizeDiff.width,h=a.outerHeight()-e.sizeDiff.height;e._helper&&!i.animate&&/relative/.test(o.css("position"))&&t(this).css({left:r.left-n.left-s.left,width:l,height:h}),e._helper&&!i.animate&&/static/.test(o.css("position"))&&t(this).css({left:r.left-n.left-s.left,width:l,height:h})}}),t.ui.plugin.add("resizable","alsoResize",{start:function(){var e=t(this).resizable("instance"),i=e.options;t(i.alsoResize).each(function(){var e=t(this);e.data("ui-resizable-alsoresize",{width:parseFloat(e.width()),height:parseFloat(e.height()),left:parseFloat(e.css("left")),top:parseFloat(e.css("top"))})})},resize:function(e,i){var s=t(this).resizable("instance"),n=s.options,o=s.originalSize,a=s.originalPosition,r={height:s.size.height-o.height||0,width:s.size.width-o.width||0,top:s.position.top-a.top||0,left:s.position.left-a.left||0};t(n.alsoResize).each(function(){var e=t(this),s=t(this).data("ui-resizable-alsoresize"),n={},o=e.parents(i.originalElement[0]).length?["width","height"]:["width","height","top","left"];t.each(o,function(t,e){var i=(s[e]||0)+(r[e]||0);i&&i>=0&&(n[e]=i||null)}),e.css(n)})},stop:function(){t(this).removeData("ui-resizable-alsoresize")}}),t.ui.plugin.add("resizable","ghost",{start:function(){var e=t(this).resizable("instance"),i=e.size;e.ghost=e.originalElement.clone(),e.ghost.css({opacity:.25,display:"block",position:"relative",height:i.height,width:i.width,margin:0,left:0,top:0}),e._addClass(e.ghost,"ui-resizable-ghost"),t.uiBackCompat!==!1&&"string"==typeof e.options.ghost&&e.ghost.addClass(this.options.ghost),e.ghost.appendTo(e.helper)},resize:function(){var e=t(this).resizable("instance");e.ghost&&e.ghost.css({position:"relative",height:e.size.height,width:e.size.width})},stop:function(){var e=t(this).resizable("instance");e.ghost&&e.helper&&e.helper.get(0).removeChild(e.ghost.get(0))}}),t.ui.plugin.add("resizable","grid",{resize:function(){var e,i=t(this).resizable("instance"),s=i.options,n=i.size,o=i.originalSize,a=i.originalPosition,r=i.axis,l="number"==typeof s.grid?[s.grid,s.grid]:s.grid,h=l[0]||1,c=l[1]||1,u=Math.round((n.width-o.width)/h)*h,d=Math.round((n.height-o.height)/c)*c,p=o.width+u,f=o.height+d,g=s.maxWidth&&p>s.maxWidth,m=s.maxHeight&&f>s.maxHeight,_=s.minWidth&&s.minWidth>p,v=s.minHeight&&s.minHeight>f;s.grid=l,_&&(p+=h),v&&(f+=c),g&&(p-=h),m&&(f-=c),/^(se|s|e)$/.test(r)?(i.size.width=p,i.size.height=f):/^(ne)$/.test(r)?(i.size.width=p,i.size.height=f,i.position.top=a.top-d):/^(sw)$/.test(r)?(i.size.width=p,i.size.height=f,i.position.left=a.left-u):((0>=f-c||0>=p-h)&&(e=i._getPaddingPlusBorderDimensions(this)),f-c>0?(i.size.height=f,i.position.top=a.top-d):(f=c-e.height,i.size.height=f,i.position.top=a.top+o.height-f),p-h>0?(i.size.width=p,i.position.left=a.left-u):(p=h-e.width,i.size.width=p,i.position.left=a.left+o.width-p))}}),t.ui.resizable,t.widget("ui.selectable",t.ui.mouse,{version:"1.12.1",options:{appendTo:"body",autoRefresh:!0,distance:0,filter:"*",tolerance:"touch",selected:null,selecting:null,start:null,stop:null,unselected:null,unselecting:null},_create:function(){var e=this;this._addClass("ui-selectable"),this.dragged=!1,this.refresh=function(){e.elementPos=t(e.element[0]).offset(),e.selectees=t(e.options.filter,e.element[0]),e._addClass(e.selectees,"ui-selectee"),e.selectees.each(function(){var i=t(this),s=i.offset(),n={left:s.left-e.elementPos.left,top:s.top-e.elementPos.top};t.data(this,"selectable-item",{element:this,$element:i,left:n.left,top:n.top,right:n.left+i.outerWidth(),bottom:n.top+i.outerHeight(),startselected:!1,selected:i.hasClass("ui-selected"),selecting:i.hasClass("ui-selecting"),unselecting:i.hasClass("ui-unselecting")})})},this.refresh(),this._mouseInit(),this.helper=t("
"),this._addClass(this.helper,"ui-selectable-helper")},_destroy:function(){this.selectees.removeData("selectable-item"),this._mouseDestroy()},_mouseStart:function(e){var i=this,s=this.options;this.opos=[e.pageX,e.pageY],this.elementPos=t(this.element[0]).offset(),this.options.disabled||(this.selectees=t(s.filter,this.element[0]),this._trigger("start",e),t(s.appendTo).append(this.helper),this.helper.css({left:e.pageX,top:e.pageY,width:0,height:0}),s.autoRefresh&&this.refresh(),this.selectees.filter(".ui-selected").each(function(){var s=t.data(this,"selectable-item");s.startselected=!0,e.metaKey||e.ctrlKey||(i._removeClass(s.$element,"ui-selected"),s.selected=!1,i._addClass(s.$element,"ui-unselecting"),s.unselecting=!0,i._trigger("unselecting",e,{unselecting:s.element}))}),t(e.target).parents().addBack().each(function(){var s,n=t.data(this,"selectable-item");return n?(s=!e.metaKey&&!e.ctrlKey||!n.$element.hasClass("ui-selected"),i._removeClass(n.$element,s?"ui-unselecting":"ui-selected")._addClass(n.$element,s?"ui-selecting":"ui-unselecting"),n.unselecting=!s,n.selecting=s,n.selected=s,s?i._trigger("selecting",e,{selecting:n.element}):i._trigger("unselecting",e,{unselecting:n.element}),!1):void 0}))},_mouseDrag:function(e){if(this.dragged=!0,!this.options.disabled){var i,s=this,n=this.options,o=this.opos[0],a=this.opos[1],r=e.pageX,l=e.pageY;return o>r&&(i=r,r=o,o=i),a>l&&(i=l,l=a,a=i),this.helper.css({left:o,top:a,width:r-o,height:l-a}),this.selectees.each(function(){var i=t.data(this,"selectable-item"),h=!1,c={};i&&i.element!==s.element[0]&&(c.left=i.left+s.elementPos.left,c.right=i.right+s.elementPos.left,c.top=i.top+s.elementPos.top,c.bottom=i.bottom+s.elementPos.top,"touch"===n.tolerance?h=!(c.left>r||o>c.right||c.top>l||a>c.bottom):"fit"===n.tolerance&&(h=c.left>o&&r>c.right&&c.top>a&&l>c.bottom),h?(i.selected&&(s._removeClass(i.$element,"ui-selected"),i.selected=!1),i.unselecting&&(s._removeClass(i.$element,"ui-unselecting"),i.unselecting=!1),i.selecting||(s._addClass(i.$element,"ui-selecting"),i.selecting=!0,s._trigger("selecting",e,{selecting:i.element}))):(i.selecting&&((e.metaKey||e.ctrlKey)&&i.startselected?(s._removeClass(i.$element,"ui-selecting"),i.selecting=!1,s._addClass(i.$element,"ui-selected"),i.selected=!0):(s._removeClass(i.$element,"ui-selecting"),i.selecting=!1,i.startselected&&(s._addClass(i.$element,"ui-unselecting"),i.unselecting=!0),s._trigger("unselecting",e,{unselecting:i.element}))),i.selected&&(e.metaKey||e.ctrlKey||i.startselected||(s._removeClass(i.$element,"ui-selected"),i.selected=!1,s._addClass(i.$element,"ui-unselecting"),i.unselecting=!0,s._trigger("unselecting",e,{unselecting:i.element})))))}),!1}},_mouseStop:function(e){var i=this;return this.dragged=!1,t(".ui-unselecting",this.element[0]).each(function(){var s=t.data(this,"selectable-item");i._removeClass(s.$element,"ui-unselecting"),s.unselecting=!1,s.startselected=!1,i._trigger("unselected",e,{unselected:s.element})}),t(".ui-selecting",this.element[0]).each(function(){var s=t.data(this,"selectable-item");i._removeClass(s.$element,"ui-selecting")._addClass(s.$element,"ui-selected"),s.selecting=!1,s.selected=!0,s.startselected=!0,i._trigger("selected",e,{selected:s.element})}),this._trigger("stop",e),this.helper.remove(),!1}}),t.widget("ui.sortable",t.ui.mouse,{version:"1.12.1",widgetEventPrefix:"sort",ready:!1,options:{appendTo:"parent",axis:!1,connectWith:!1,containment:!1,cursor:"auto",cursorAt:!1,dropOnEmpty:!0,forcePlaceholderSize:!1,forceHelperSize:!1,grid:!1,handle:!1,helper:"original",items:"> *",opacity:!1,placeholder:!1,revert:!1,scroll:!0,scrollSensitivity:20,scrollSpeed:20,scope:"default",tolerance:"intersect",zIndex:1e3,activate:null,beforeStop:null,change:null,deactivate:null,out:null,over:null,receive:null,remove:null,sort:null,start:null,stop:null,update:null},_isOverAxis:function(t,e,i){return t>=e&&e+i>t},_isFloating:function(t){return/left|right/.test(t.css("float"))||/inline|table-cell/.test(t.css("display"))},_create:function(){this.containerCache={},this._addClass("ui-sortable"),this.refresh(),this.offset=this.element.offset(),this._mouseInit(),this._setHandleClassName(),this.ready=!0},_setOption:function(t,e){this._super(t,e),"handle"===t&&this._setHandleClassName()},_setHandleClassName:function(){var e=this;this._removeClass(this.element.find(".ui-sortable-handle"),"ui-sortable-handle"),t.each(this.items,function(){e._addClass(this.instance.options.handle?this.item.find(this.instance.options.handle):this.item,"ui-sortable-handle")})},_destroy:function(){this._mouseDestroy();for(var t=this.items.length-1;t>=0;t--)this.items[t].item.removeData(this.widgetName+"-item");return this},_mouseCapture:function(e,i){var s=null,n=!1,o=this;return this.reverting?!1:this.options.disabled||"static"===this.options.type?!1:(this._refreshItems(e),t(e.target).parents().each(function(){return t.data(this,o.widgetName+"-item")===o?(s=t(this),!1):void 0}),t.data(e.target,o.widgetName+"-item")===o&&(s=t(e.target)),s?!this.options.handle||i||(t(this.options.handle,s).find("*").addBack().each(function(){this===e.target&&(n=!0)}),n)?(this.currentItem=s,this._removeCurrentsFromItems(),!0):!1:!1)},_mouseStart:function(e,i,s){var n,o,a=this.options;if(this.currentContainer=this,this.refreshPositions(),this.helper=this._createHelper(e),this._cacheHelperProportions(),this._cacheMargins(),this.scrollParent=this.helper.scrollParent(),this.offset=this.currentItem.offset(),this.offset={top:this.offset.top-this.margins.top,left:this.offset.left-this.margins.left},t.extend(this.offset,{click:{left:e.pageX-this.offset.left,top:e.pageY-this.offset.top},parent:this._getParentOffset(),relative:this._getRelativeOffset()}),this.helper.css("position","absolute"),this.cssPosition=this.helper.css("position"),this.originalPosition=this._generatePosition(e),this.originalPageX=e.pageX,this.originalPageY=e.pageY,a.cursorAt&&this._adjustOffsetFromHelper(a.cursorAt),this.domPosition={prev:this.currentItem.prev()[0],parent:this.currentItem.parent()[0]},this.helper[0]!==this.currentItem[0]&&this.currentItem.hide(),this._createPlaceholder(),a.containment&&this._setContainment(),a.cursor&&"auto"!==a.cursor&&(o=this.document.find("body"),this.storedCursor=o.css("cursor"),o.css("cursor",a.cursor),this.storedStylesheet=t("").appendTo(o)),a.opacity&&(this.helper.css("opacity")&&(this._storedOpacity=this.helper.css("opacity")),this.helper.css("opacity",a.opacity)),a.zIndex&&(this.helper.css("zIndex")&&(this._storedZIndex=this.helper.css("zIndex")),this.helper.css("zIndex",a.zIndex)),this.scrollParent[0]!==this.document[0]&&"HTML"!==this.scrollParent[0].tagName&&(this.overflowOffset=this.scrollParent.offset()),this._trigger("start",e,this._uiHash()),this._preserveHelperProportions||this._cacheHelperProportions(),!s)for(n=this.containers.length-1;n>=0;n--)this.containers[n]._trigger("activate",e,this._uiHash(this));return t.ui.ddmanager&&(t.ui.ddmanager.current=this),t.ui.ddmanager&&!a.dropBehaviour&&t.ui.ddmanager.prepareOffsets(this,e),this.dragging=!0,this._addClass(this.helper,"ui-sortable-helper"),this._mouseDrag(e),!0},_mouseDrag:function(e){var i,s,n,o,a=this.options,r=!1;for(this.position=this._generatePosition(e),this.positionAbs=this._convertPositionTo("absolute"),this.lastPositionAbs||(this.lastPositionAbs=this.positionAbs),this.options.scroll&&(this.scrollParent[0]!==this.document[0]&&"HTML"!==this.scrollParent[0].tagName?(this.overflowOffset.top+this.scrollParent[0].offsetHeight-e.pageY=0;i--)if(s=this.items[i],n=s.item[0],o=this._intersectsWithPointer(s),o&&s.instance===this.currentContainer&&n!==this.currentItem[0]&&this.placeholder[1===o?"next":"prev"]()[0]!==n&&!t.contains(this.placeholder[0],n)&&("semi-dynamic"===this.options.type?!t.contains(this.element[0],n):!0)){if(this.direction=1===o?"down":"up","pointer"!==this.options.tolerance&&!this._intersectsWithSides(s))break; +this._rearrange(e,s),this._trigger("change",e,this._uiHash());break}return this._contactContainers(e),t.ui.ddmanager&&t.ui.ddmanager.drag(this,e),this._trigger("sort",e,this._uiHash()),this.lastPositionAbs=this.positionAbs,!1},_mouseStop:function(e,i){if(e){if(t.ui.ddmanager&&!this.options.dropBehaviour&&t.ui.ddmanager.drop(this,e),this.options.revert){var s=this,n=this.placeholder.offset(),o=this.options.axis,a={};o&&"x"!==o||(a.left=n.left-this.offset.parent.left-this.margins.left+(this.offsetParent[0]===this.document[0].body?0:this.offsetParent[0].scrollLeft)),o&&"y"!==o||(a.top=n.top-this.offset.parent.top-this.margins.top+(this.offsetParent[0]===this.document[0].body?0:this.offsetParent[0].scrollTop)),this.reverting=!0,t(this.helper).animate(a,parseInt(this.options.revert,10)||500,function(){s._clear(e)})}else this._clear(e,i);return!1}},cancel:function(){if(this.dragging){this._mouseUp(new t.Event("mouseup",{target:null})),"original"===this.options.helper?(this.currentItem.css(this._storedCSS),this._removeClass(this.currentItem,"ui-sortable-helper")):this.currentItem.show();for(var e=this.containers.length-1;e>=0;e--)this.containers[e]._trigger("deactivate",null,this._uiHash(this)),this.containers[e].containerCache.over&&(this.containers[e]._trigger("out",null,this._uiHash(this)),this.containers[e].containerCache.over=0)}return this.placeholder&&(this.placeholder[0].parentNode&&this.placeholder[0].parentNode.removeChild(this.placeholder[0]),"original"!==this.options.helper&&this.helper&&this.helper[0].parentNode&&this.helper.remove(),t.extend(this,{helper:null,dragging:!1,reverting:!1,_noFinalSort:null}),this.domPosition.prev?t(this.domPosition.prev).after(this.currentItem):t(this.domPosition.parent).prepend(this.currentItem)),this},serialize:function(e){var i=this._getItemsAsjQuery(e&&e.connected),s=[];return e=e||{},t(i).each(function(){var i=(t(e.item||this).attr(e.attribute||"id")||"").match(e.expression||/(.+)[\-=_](.+)/);i&&s.push((e.key||i[1]+"[]")+"="+(e.key&&e.expression?i[1]:i[2]))}),!s.length&&e.key&&s.push(e.key+"="),s.join("&")},toArray:function(e){var i=this._getItemsAsjQuery(e&&e.connected),s=[];return e=e||{},i.each(function(){s.push(t(e.item||this).attr(e.attribute||"id")||"")}),s},_intersectsWith:function(t){var e=this.positionAbs.left,i=e+this.helperProportions.width,s=this.positionAbs.top,n=s+this.helperProportions.height,o=t.left,a=o+t.width,r=t.top,l=r+t.height,h=this.offset.click.top,c=this.offset.click.left,u="x"===this.options.axis||s+h>r&&l>s+h,d="y"===this.options.axis||e+c>o&&a>e+c,p=u&&d;return"pointer"===this.options.tolerance||this.options.forcePointerForContainers||"pointer"!==this.options.tolerance&&this.helperProportions[this.floating?"width":"height"]>t[this.floating?"width":"height"]?p:e+this.helperProportions.width/2>o&&a>i-this.helperProportions.width/2&&s+this.helperProportions.height/2>r&&l>n-this.helperProportions.height/2},_intersectsWithPointer:function(t){var e,i,s="x"===this.options.axis||this._isOverAxis(this.positionAbs.top+this.offset.click.top,t.top,t.height),n="y"===this.options.axis||this._isOverAxis(this.positionAbs.left+this.offset.click.left,t.left,t.width),o=s&&n;return o?(e=this._getDragVerticalDirection(),i=this._getDragHorizontalDirection(),this.floating?"right"===i||"down"===e?2:1:e&&("down"===e?2:1)):!1},_intersectsWithSides:function(t){var e=this._isOverAxis(this.positionAbs.top+this.offset.click.top,t.top+t.height/2,t.height),i=this._isOverAxis(this.positionAbs.left+this.offset.click.left,t.left+t.width/2,t.width),s=this._getDragVerticalDirection(),n=this._getDragHorizontalDirection();return this.floating&&n?"right"===n&&i||"left"===n&&!i:s&&("down"===s&&e||"up"===s&&!e)},_getDragVerticalDirection:function(){var t=this.positionAbs.top-this.lastPositionAbs.top;return 0!==t&&(t>0?"down":"up")},_getDragHorizontalDirection:function(){var t=this.positionAbs.left-this.lastPositionAbs.left;return 0!==t&&(t>0?"right":"left")},refresh:function(t){return this._refreshItems(t),this._setHandleClassName(),this.refreshPositions(),this},_connectWith:function(){var t=this.options;return t.connectWith.constructor===String?[t.connectWith]:t.connectWith},_getItemsAsjQuery:function(e){function i(){r.push(this)}var s,n,o,a,r=[],l=[],h=this._connectWith();if(h&&e)for(s=h.length-1;s>=0;s--)for(o=t(h[s],this.document[0]),n=o.length-1;n>=0;n--)a=t.data(o[n],this.widgetFullName),a&&a!==this&&!a.options.disabled&&l.push([t.isFunction(a.options.items)?a.options.items.call(a.element):t(a.options.items,a.element).not(".ui-sortable-helper").not(".ui-sortable-placeholder"),a]);for(l.push([t.isFunction(this.options.items)?this.options.items.call(this.element,null,{options:this.options,item:this.currentItem}):t(this.options.items,this.element).not(".ui-sortable-helper").not(".ui-sortable-placeholder"),this]),s=l.length-1;s>=0;s--)l[s][0].each(i);return t(r)},_removeCurrentsFromItems:function(){var e=this.currentItem.find(":data("+this.widgetName+"-item)");this.items=t.grep(this.items,function(t){for(var i=0;e.length>i;i++)if(e[i]===t.item[0])return!1;return!0})},_refreshItems:function(e){this.items=[],this.containers=[this];var i,s,n,o,a,r,l,h,c=this.items,u=[[t.isFunction(this.options.items)?this.options.items.call(this.element[0],e,{item:this.currentItem}):t(this.options.items,this.element),this]],d=this._connectWith();if(d&&this.ready)for(i=d.length-1;i>=0;i--)for(n=t(d[i],this.document[0]),s=n.length-1;s>=0;s--)o=t.data(n[s],this.widgetFullName),o&&o!==this&&!o.options.disabled&&(u.push([t.isFunction(o.options.items)?o.options.items.call(o.element[0],e,{item:this.currentItem}):t(o.options.items,o.element),o]),this.containers.push(o));for(i=u.length-1;i>=0;i--)for(a=u[i][1],r=u[i][0],s=0,h=r.length;h>s;s++)l=t(r[s]),l.data(this.widgetName+"-item",a),c.push({item:l,instance:a,width:0,height:0,left:0,top:0})},refreshPositions:function(e){this.floating=this.items.length?"x"===this.options.axis||this._isFloating(this.items[0].item):!1,this.offsetParent&&this.helper&&(this.offset.parent=this._getParentOffset());var i,s,n,o;for(i=this.items.length-1;i>=0;i--)s=this.items[i],s.instance!==this.currentContainer&&this.currentContainer&&s.item[0]!==this.currentItem[0]||(n=this.options.toleranceElement?t(this.options.toleranceElement,s.item):s.item,e||(s.width=n.outerWidth(),s.height=n.outerHeight()),o=n.offset(),s.left=o.left,s.top=o.top);if(this.options.custom&&this.options.custom.refreshContainers)this.options.custom.refreshContainers.call(this);else for(i=this.containers.length-1;i>=0;i--)o=this.containers[i].element.offset(),this.containers[i].containerCache.left=o.left,this.containers[i].containerCache.top=o.top,this.containers[i].containerCache.width=this.containers[i].element.outerWidth(),this.containers[i].containerCache.height=this.containers[i].element.outerHeight();return this},_createPlaceholder:function(e){e=e||this;var i,s=e.options;s.placeholder&&s.placeholder.constructor!==String||(i=s.placeholder,s.placeholder={element:function(){var s=e.currentItem[0].nodeName.toLowerCase(),n=t("<"+s+">",e.document[0]);return e._addClass(n,"ui-sortable-placeholder",i||e.currentItem[0].className)._removeClass(n,"ui-sortable-helper"),"tbody"===s?e._createTrPlaceholder(e.currentItem.find("tr").eq(0),t("",e.document[0]).appendTo(n)):"tr"===s?e._createTrPlaceholder(e.currentItem,n):"img"===s&&n.attr("src",e.currentItem.attr("src")),i||n.css("visibility","hidden"),n},update:function(t,n){(!i||s.forcePlaceholderSize)&&(n.height()||n.height(e.currentItem.innerHeight()-parseInt(e.currentItem.css("paddingTop")||0,10)-parseInt(e.currentItem.css("paddingBottom")||0,10)),n.width()||n.width(e.currentItem.innerWidth()-parseInt(e.currentItem.css("paddingLeft")||0,10)-parseInt(e.currentItem.css("paddingRight")||0,10)))}}),e.placeholder=t(s.placeholder.element.call(e.element,e.currentItem)),e.currentItem.after(e.placeholder),s.placeholder.update(e,e.placeholder)},_createTrPlaceholder:function(e,i){var s=this;e.children().each(function(){t(" ",s.document[0]).attr("colspan",t(this).attr("colspan")||1).appendTo(i)})},_contactContainers:function(e){var i,s,n,o,a,r,l,h,c,u,d=null,p=null;for(i=this.containers.length-1;i>=0;i--)if(!t.contains(this.currentItem[0],this.containers[i].element[0]))if(this._intersectsWith(this.containers[i].containerCache)){if(d&&t.contains(this.containers[i].element[0],d.element[0]))continue;d=this.containers[i],p=i}else this.containers[i].containerCache.over&&(this.containers[i]._trigger("out",e,this._uiHash(this)),this.containers[i].containerCache.over=0);if(d)if(1===this.containers.length)this.containers[p].containerCache.over||(this.containers[p]._trigger("over",e,this._uiHash(this)),this.containers[p].containerCache.over=1);else{for(n=1e4,o=null,c=d.floating||this._isFloating(this.currentItem),a=c?"left":"top",r=c?"width":"height",u=c?"pageX":"pageY",s=this.items.length-1;s>=0;s--)t.contains(this.containers[p].element[0],this.items[s].item[0])&&this.items[s].item[0]!==this.currentItem[0]&&(l=this.items[s].item.offset()[a],h=!1,e[u]-l>this.items[s][r]/2&&(h=!0),n>Math.abs(e[u]-l)&&(n=Math.abs(e[u]-l),o=this.items[s],this.direction=h?"up":"down"));if(!o&&!this.options.dropOnEmpty)return;if(this.currentContainer===this.containers[p])return this.currentContainer.containerCache.over||(this.containers[p]._trigger("over",e,this._uiHash()),this.currentContainer.containerCache.over=1),void 0;o?this._rearrange(e,o,null,!0):this._rearrange(e,null,this.containers[p].element,!0),this._trigger("change",e,this._uiHash()),this.containers[p]._trigger("change",e,this._uiHash(this)),this.currentContainer=this.containers[p],this.options.placeholder.update(this.currentContainer,this.placeholder),this.containers[p]._trigger("over",e,this._uiHash(this)),this.containers[p].containerCache.over=1}},_createHelper:function(e){var i=this.options,s=t.isFunction(i.helper)?t(i.helper.apply(this.element[0],[e,this.currentItem])):"clone"===i.helper?this.currentItem.clone():this.currentItem;return s.parents("body").length||t("parent"!==i.appendTo?i.appendTo:this.currentItem[0].parentNode)[0].appendChild(s[0]),s[0]===this.currentItem[0]&&(this._storedCSS={width:this.currentItem[0].style.width,height:this.currentItem[0].style.height,position:this.currentItem.css("position"),top:this.currentItem.css("top"),left:this.currentItem.css("left")}),(!s[0].style.width||i.forceHelperSize)&&s.width(this.currentItem.width()),(!s[0].style.height||i.forceHelperSize)&&s.height(this.currentItem.height()),s},_adjustOffsetFromHelper:function(e){"string"==typeof e&&(e=e.split(" ")),t.isArray(e)&&(e={left:+e[0],top:+e[1]||0}),"left"in e&&(this.offset.click.left=e.left+this.margins.left),"right"in e&&(this.offset.click.left=this.helperProportions.width-e.right+this.margins.left),"top"in e&&(this.offset.click.top=e.top+this.margins.top),"bottom"in e&&(this.offset.click.top=this.helperProportions.height-e.bottom+this.margins.top)},_getParentOffset:function(){this.offsetParent=this.helper.offsetParent();var e=this.offsetParent.offset();return"absolute"===this.cssPosition&&this.scrollParent[0]!==this.document[0]&&t.contains(this.scrollParent[0],this.offsetParent[0])&&(e.left+=this.scrollParent.scrollLeft(),e.top+=this.scrollParent.scrollTop()),(this.offsetParent[0]===this.document[0].body||this.offsetParent[0].tagName&&"html"===this.offsetParent[0].tagName.toLowerCase()&&t.ui.ie)&&(e={top:0,left:0}),{top:e.top+(parseInt(this.offsetParent.css("borderTopWidth"),10)||0),left:e.left+(parseInt(this.offsetParent.css("borderLeftWidth"),10)||0)}},_getRelativeOffset:function(){if("relative"===this.cssPosition){var t=this.currentItem.position();return{top:t.top-(parseInt(this.helper.css("top"),10)||0)+this.scrollParent.scrollTop(),left:t.left-(parseInt(this.helper.css("left"),10)||0)+this.scrollParent.scrollLeft()}}return{top:0,left:0}},_cacheMargins:function(){this.margins={left:parseInt(this.currentItem.css("marginLeft"),10)||0,top:parseInt(this.currentItem.css("marginTop"),10)||0}},_cacheHelperProportions:function(){this.helperProportions={width:this.helper.outerWidth(),height:this.helper.outerHeight()}},_setContainment:function(){var e,i,s,n=this.options;"parent"===n.containment&&(n.containment=this.helper[0].parentNode),("document"===n.containment||"window"===n.containment)&&(this.containment=[0-this.offset.relative.left-this.offset.parent.left,0-this.offset.relative.top-this.offset.parent.top,"document"===n.containment?this.document.width():this.window.width()-this.helperProportions.width-this.margins.left,("document"===n.containment?this.document.height()||document.body.parentNode.scrollHeight:this.window.height()||this.document[0].body.parentNode.scrollHeight)-this.helperProportions.height-this.margins.top]),/^(document|window|parent)$/.test(n.containment)||(e=t(n.containment)[0],i=t(n.containment).offset(),s="hidden"!==t(e).css("overflow"),this.containment=[i.left+(parseInt(t(e).css("borderLeftWidth"),10)||0)+(parseInt(t(e).css("paddingLeft"),10)||0)-this.margins.left,i.top+(parseInt(t(e).css("borderTopWidth"),10)||0)+(parseInt(t(e).css("paddingTop"),10)||0)-this.margins.top,i.left+(s?Math.max(e.scrollWidth,e.offsetWidth):e.offsetWidth)-(parseInt(t(e).css("borderLeftWidth"),10)||0)-(parseInt(t(e).css("paddingRight"),10)||0)-this.helperProportions.width-this.margins.left,i.top+(s?Math.max(e.scrollHeight,e.offsetHeight):e.offsetHeight)-(parseInt(t(e).css("borderTopWidth"),10)||0)-(parseInt(t(e).css("paddingBottom"),10)||0)-this.helperProportions.height-this.margins.top])},_convertPositionTo:function(e,i){i||(i=this.position);var s="absolute"===e?1:-1,n="absolute"!==this.cssPosition||this.scrollParent[0]!==this.document[0]&&t.contains(this.scrollParent[0],this.offsetParent[0])?this.scrollParent:this.offsetParent,o=/(html|body)/i.test(n[0].tagName);return{top:i.top+this.offset.relative.top*s+this.offset.parent.top*s-("fixed"===this.cssPosition?-this.scrollParent.scrollTop():o?0:n.scrollTop())*s,left:i.left+this.offset.relative.left*s+this.offset.parent.left*s-("fixed"===this.cssPosition?-this.scrollParent.scrollLeft():o?0:n.scrollLeft())*s}},_generatePosition:function(e){var i,s,n=this.options,o=e.pageX,a=e.pageY,r="absolute"!==this.cssPosition||this.scrollParent[0]!==this.document[0]&&t.contains(this.scrollParent[0],this.offsetParent[0])?this.scrollParent:this.offsetParent,l=/(html|body)/i.test(r[0].tagName);return"relative"!==this.cssPosition||this.scrollParent[0]!==this.document[0]&&this.scrollParent[0]!==this.offsetParent[0]||(this.offset.relative=this._getRelativeOffset()),this.originalPosition&&(this.containment&&(e.pageX-this.offset.click.leftthis.containment[2]&&(o=this.containment[2]+this.offset.click.left),e.pageY-this.offset.click.top>this.containment[3]&&(a=this.containment[3]+this.offset.click.top)),n.grid&&(i=this.originalPageY+Math.round((a-this.originalPageY)/n.grid[1])*n.grid[1],a=this.containment?i-this.offset.click.top>=this.containment[1]&&i-this.offset.click.top<=this.containment[3]?i:i-this.offset.click.top>=this.containment[1]?i-n.grid[1]:i+n.grid[1]:i,s=this.originalPageX+Math.round((o-this.originalPageX)/n.grid[0])*n.grid[0],o=this.containment?s-this.offset.click.left>=this.containment[0]&&s-this.offset.click.left<=this.containment[2]?s:s-this.offset.click.left>=this.containment[0]?s-n.grid[0]:s+n.grid[0]:s)),{top:a-this.offset.click.top-this.offset.relative.top-this.offset.parent.top+("fixed"===this.cssPosition?-this.scrollParent.scrollTop():l?0:r.scrollTop()),left:o-this.offset.click.left-this.offset.relative.left-this.offset.parent.left+("fixed"===this.cssPosition?-this.scrollParent.scrollLeft():l?0:r.scrollLeft())}},_rearrange:function(t,e,i,s){i?i[0].appendChild(this.placeholder[0]):e.item[0].parentNode.insertBefore(this.placeholder[0],"down"===this.direction?e.item[0]:e.item[0].nextSibling),this.counter=this.counter?++this.counter:1;var n=this.counter;this._delay(function(){n===this.counter&&this.refreshPositions(!s)})},_clear:function(t,e){function i(t,e,i){return function(s){i._trigger(t,s,e._uiHash(e))}}this.reverting=!1;var s,n=[];if(!this._noFinalSort&&this.currentItem.parent().length&&this.placeholder.before(this.currentItem),this._noFinalSort=null,this.helper[0]===this.currentItem[0]){for(s in this._storedCSS)("auto"===this._storedCSS[s]||"static"===this._storedCSS[s])&&(this._storedCSS[s]="");this.currentItem.css(this._storedCSS),this._removeClass(this.currentItem,"ui-sortable-helper")}else this.currentItem.show();for(this.fromOutside&&!e&&n.push(function(t){this._trigger("receive",t,this._uiHash(this.fromOutside))}),!this.fromOutside&&this.domPosition.prev===this.currentItem.prev().not(".ui-sortable-helper")[0]&&this.domPosition.parent===this.currentItem.parent()[0]||e||n.push(function(t){this._trigger("update",t,this._uiHash())}),this!==this.currentContainer&&(e||(n.push(function(t){this._trigger("remove",t,this._uiHash())}),n.push(function(t){return function(e){t._trigger("receive",e,this._uiHash(this))}}.call(this,this.currentContainer)),n.push(function(t){return function(e){t._trigger("update",e,this._uiHash(this))}}.call(this,this.currentContainer)))),s=this.containers.length-1;s>=0;s--)e||n.push(i("deactivate",this,this.containers[s])),this.containers[s].containerCache.over&&(n.push(i("out",this,this.containers[s])),this.containers[s].containerCache.over=0);if(this.storedCursor&&(this.document.find("body").css("cursor",this.storedCursor),this.storedStylesheet.remove()),this._storedOpacity&&this.helper.css("opacity",this._storedOpacity),this._storedZIndex&&this.helper.css("zIndex","auto"===this._storedZIndex?"":this._storedZIndex),this.dragging=!1,e||this._trigger("beforeStop",t,this._uiHash()),this.placeholder[0].parentNode.removeChild(this.placeholder[0]),this.cancelHelperRemoval||(this.helper[0]!==this.currentItem[0]&&this.helper.remove(),this.helper=null),!e){for(s=0;n.length>s;s++)n[s].call(this,t);this._trigger("stop",t,this._uiHash())}return this.fromOutside=!1,!this.cancelHelperRemoval},_trigger:function(){t.Widget.prototype._trigger.apply(this,arguments)===!1&&this.cancel()},_uiHash:function(e){var i=e||this;return{helper:i.helper,placeholder:i.placeholder||t([]),position:i.position,originalPosition:i.originalPosition,offset:i.positionAbs,item:i.currentItem,sender:e?e.element:null}}})}); \ No newline at end of file diff --git a/octoprint_bambu_printer/static/js/knockout-sortable.1.2.0.js b/octoprint_bambu_printer/static/js/knockout-sortable.1.2.0.js new file mode 100644 index 0000000..dff62ce --- /dev/null +++ b/octoprint_bambu_printer/static/js/knockout-sortable.1.2.0.js @@ -0,0 +1,490 @@ +// knockout-sortable 1.2.0 | (c) 2019 Ryan Niemeyer | http://www.opensource.org/licenses/mit-license +;(function(factory) { + if (typeof define === "function" && define.amd) { + // AMD anonymous module + define(["knockout", "jquery", "jquery-ui/ui/widgets/sortable", "jquery-ui/ui/widgets/draggable", "jquery-ui/ui/widgets/droppable"], factory); + } else if (typeof require === "function" && typeof exports === "object" && typeof module === "object") { + // CommonJS module + var ko = require("knockout"), + jQuery = require("jquery"); + require("jquery-ui/ui/widgets/sortable"); + require("jquery-ui/ui/widgets/draggable"); + require("jquery-ui/ui/widgets/droppable"); + factory(ko, jQuery); + } else { + // No module loader (plain