diff --git a/octoprint_mrbeam/__init__.py b/octoprint_mrbeam/__init__.py index 4c0bd532b..e6bb54104 100644 --- a/octoprint_mrbeam/__init__.py +++ b/octoprint_mrbeam/__init__.py @@ -69,18 +69,13 @@ from octoprint_mrbeam.os_health_care import os_health_care from octoprint_mrbeam.rest_handler.docs_handler import DocsRestHandlerMixin from octoprint_mrbeam.model.laser_cutter_mode import LaserCutterModeModel -from octoprint_mrbeam.services import settings_service -from octoprint_mrbeam.services.settings_service import SettingsService -from octoprint_mrbeam.services.burger_menu_service import BurgerMenuService -from octoprint_mrbeam.services.document_service import DocumentService -from octoprint_mrbeam.services.laser_cutter_mode import laser_cutter_mode_service +from octoprint_mrbeam.service import settings_service +from octoprint_mrbeam.service.settings_service import SettingsService +from octoprint_mrbeam.service.burger_menu_service import BurgerMenuService +from octoprint_mrbeam.service.document_service import DocumentService +from octoprint_mrbeam.service.laser_cutter_mode import laser_cutter_mode_service +from octoprint_mrbeam.service.profile.laser_cutter_profile import laser_cutter_profile_service from octoprint_mrbeam.wizard_config import WizardConfig -from octoprint_mrbeam.printing.profile import ( - laserCutterProfileManager, - InvalidProfileError, - CouldNotOverwriteError, - Profile, -) from octoprint_mrbeam.software_update_information import ( get_update_information, switch_software_channel, @@ -111,6 +106,9 @@ from octoprint_mrbeam.util import get_thread from octoprint_mrbeam import camera from octoprint_mrbeam.util.version_comparator import compare_pep440_versions +from octoprint_mrbeam.constant.profile import laser_cutter as laser_cutter_profiles +from octoprint_mrbeam.enums.laser_cutter_mode import LaserCutterModeEnum +from octoprint_mrbeam.enums.device_series import DeviceSeriesEnum # this is a easy&simple way to access the plugin and all injections everywhere within the plugin __builtin__._mrbeam_plugin_implementation = None @@ -169,6 +167,8 @@ class MrBeamPlugin( RESTART_OCTOPRINT_CMD = "sudo systemctl restart octoprint.service" def __init__(self): + self._laser_cutter_mode_service = None + self.laser_cutter_profile_service = None self.mrbeam_plugin_initialized = False self._shutting_down = False self._slicing_commands = dict() @@ -195,12 +195,7 @@ def __init__(self): self._iobeam_connected = False self._laserhead_ready = False - - # Create the ``laserCutterProfileManager`` early to inject into the ``Laser`` - # See ``laser_factory`` - self.laserCutterProfileManager = laserCutterProfileManager( - profile_id=self._device_info.get_type() - ) + self._laser_cutter_profile_initialized = False self._boot_grace_period_counter = 0 @@ -217,9 +212,6 @@ def __init__(self): # Jinja custom filters need to be loaded already on instance creation FilterLoader.load_custom_jinja_filters() - # Initialize the laser cutter mode service attribute - self._laser_cutter_mode_service = None - # inside initialize() OctoPrint is already loaded, not assured during __init__()! def initialize(self): self._plugin_version = __version__ @@ -240,6 +232,7 @@ def initialize(self): self._event_bus.subscribe( MrBeamEvents.LASER_HEAD_READ, self._on_laserhead_ready ) + self._event_bus.subscribe(MrBeamEvents.LASER_CUTTER_PROFILE_INITIALIZED, self._on_laser_cutter_profile_initialized) self.start_time_ntp_timer() @@ -268,6 +261,15 @@ def initialize(self): ) self.analytics_handler = analyticsHandler(self) + self._laser_cutter_mode_service = laser_cutter_mode_service(self) + + # The laser cutter profile has already been initialized by OctoPrint at this point + # This is due to the Laser class hook implementation + # So at this point the laser cutter profile is already initialized with the default profile + self.laser_cutter_profile_service = laser_cutter_profile_service(self) + # That's why we need to update the laser cutter profile with the profile for the current configuration + # and select it in the initialization of the plugin + self.update_laser_cutter_profile(self.get_laser_cutter_profile_for_current_configuration()) self.user_notification_system = user_notification_system(self) self.onebutton_handler = oneButtonHandler(self) self.interlock_handler = interLockHandler(self) @@ -301,48 +303,92 @@ def initialize(self): self._do_initial_log() self._printer.register_user_notification_system(self.user_notification_system) - # Initialize the laser cutter mode service - self._laser_cutter_mode_service = laser_cutter_mode_service(self) + def update_laser_cutter_profile(self, profile): + """ + Updates the laser cutter profile and selects it. + + Args: + profile (dict): The profile to update. + + Returns: + None + """ + if not isinstance(profile, dict): + raise TypeError("The 'profile' parameter must be a dictionary.") + + if 'id' not in profile or not isinstance(profile['id'], str): + raise ValueError("Invalid or missing 'id' in the provided profile.") + + # We save the profile even if it already exists to make sure that it is up-to-date with the latest + # changes in the profiles' implementation + self.laser_cutter_profile_service.save(profile, allow_overwrite=True, make_default=True) + self.laser_cutter_profile_service.select(profile['id']) + + def get_laser_cutter_profile_for_current_configuration(self): + """ + Returns the laser cutter profile for the current configuration. + + Returns: + dict: The laser cutter profile for the current configuration. + """ + laser_cutter_mode = self.get_laser_cutter_mode() + device_series = self._device_info.get_series() + profile = laser_cutter_profiles.default_profile + if laser_cutter_mode == LaserCutterModeEnum.DEFAULT.value: + if device_series == DeviceSeriesEnum.C.value: + profile = laser_cutter_profiles.series_2c_profile + elif laser_cutter_mode == LaserCutterModeEnum.ROTARY.value: + profile = laser_cutter_profiles.rotary_profile + if device_series == DeviceSeriesEnum.C.value: + profile = laser_cutter_profiles.series_2c_rotary_profile + return profile + def get_settings(self): return self._settings def _on_iobeam_connect(self, *args, **kwargs): - """Called when the iobeam socket is connected. - - Args: - *args: - **kwargs: + """ + Called when the iobeam socket is connected. Returns: - + None """ self._logger.info("MrBeamPlugin on_iobeam_connected") self._iobeam_connected = True self._try_to_connect_laser() def _on_laserhead_ready(self, *args, **kwargs): - """Called when the laserhead is ready. - - Args: - *args: - **kwargs: + """ + Called when the laserhead is ready. Returns: - + None """ self._logger.info("MrBeamPlugin on_laserhead_ready") self._laserhead_ready = True self._try_to_connect_laser() + def _on_laser_cutter_profile_initialized(self, *args, **kwargs): + """ + Called when the laser cutter profile is initialized. + + Returns: + None + """ + self._logger.info("MrBeamPlugin on_laser_cutter_profile_initialized") + self._laser_cutter_profile_initialized = True + self._try_to_connect_laser() + def _try_to_connect_laser(self): """Tries to connect the laser if both iobeam and laserhead are ready and the laser is not connected yet.""" if ( self._iobeam_connected and self._laserhead_ready + and self._laser_cutter_profile_initialized and self._printer.is_closed_or_error() ): - self._printer.connect() + self._printer.connect(profile=self.laser_cutter_profile_service.get_current_or_default()) def _init_frontend_logger(self): handler = logging.handlers.RotatingFileHandler( @@ -382,7 +428,7 @@ def _do_initial_log(self): msg = ( "MrBeam Lasercutter Profile: %s" - % self.laserCutterProfileManager.get_current_or_default() + % self.laser_cutter_profile_service.get_current_or_default() ) self._logger.info(msg, terminal=True) self._frontend_logger.info(msg) @@ -596,7 +642,7 @@ def on_settings_load(self): fps=self._settings.get(["leds", "fps"]), ), isFirstRun=self.isFirstRun(), - laser_cutter_mode=self._settings.get(["laser_cutter_mode"]), + laser_cutter_mode=self.get_laser_cutter_mode(), ) def on_settings_save(self, data): @@ -862,9 +908,9 @@ def on_ui_render(self, now, request, render_kwargs): enable_accesscontrol and self._user_manager.hasBeenCustomized() ) - selectedProfile = self.laserCutterProfileManager.get_current_or_default() - enable_focus = selectedProfile["focus"] - safety_glasses = selectedProfile["glasses"] + selected_profile = self.laser_cutter_profile_service.get_current_or_default() + enable_focus = selected_profile["focus"] + safety_glasses = selected_profile["glasses"] # render_kwargs["templates"]["settings"]["entries"]["serial"][1]["template"] = "settings/serialconnection.jinja2" wizard = render_kwargs["templates"] is not None and bool( @@ -1620,7 +1666,7 @@ def printLabel(self): ) @restricted_access_or_calibration_tool_mode def engraveCalibrationMarkers(self, intensity, feedrate): - profile = self.laserCutterProfileManager.get_current_or_default() + profile = self.laser_cutter_profile_service.get_current_or_default() try: i = int(int(intensity) / 100.0 * JobParams.Max.INTENSITY) f = int(feedrate) @@ -1646,7 +1692,7 @@ def engraveCalibrationMarkers(self, intensity, feedrate): return make_response("Laser: Serial not connected", 400) if self._printer.get_state_id() == "LOCKED": - self._printer.home("xy") + self._printer.home() seconds = 0 while ( @@ -1700,31 +1746,16 @@ def engraveCalibrationMarkers(self, intensity, feedrate): # return NO_CONTENT # Laser cutter profiles - @octoprint.plugin.BlueprintPlugin.route("/profiles", methods=["GET"]) - def laserCutterProfilesList(self): - all_profiles = self.laserCutterProfileManager.converted_profiles() - for profile_id, profile in all_profiles.items(): - all_profiles[profile_id]["resource"] = url_for( - ".laserCutterProfilesGet", identifier=profile["id"], _external=True - ) - return jsonify(dict(profiles=all_profiles)) - - @octoprint.plugin.BlueprintPlugin.route( - "/profiles/", methods=["GET"] - ) - def laserCutterProfilesGet(self, identifier): - profile = self.laserCutterProfileManager.get(identifier) - if profile is None: - return make_response("Unknown profile: %s" % identifier, 404) - else: - return jsonify(self._convert_profile(profile)) + @octoprint.plugin.BlueprintPlugin.route("/currentProfile", methods=["GET"]) + def get_current_laser_cutter_profile(self): + return jsonify(self.laser_cutter_profile_service.get_current_or_default()) # ~ Calibration def generateCalibrationMarkersSvg(self): """Used from the calibration screen to engrave the calibration markers.""" # TODO mv this func to other file - profile = self.laserCutterProfileManager.get_current_or_default() + profile = self.laser_cutter_profile_service.get_current_or_default() cm = CalibrationMarker( str(profile["volume"]["width"]), str(profile["volume"]["depth"]) ) @@ -2117,7 +2148,7 @@ def on_api_command(self, command, data): parse_csv( device_model=self.get_model_id(), laserhead_model=self.get_current_laser_head_model(), - laser_cutter_mode=self.get_laser_cutter_mode(), + laser_cutter_mode = self.get_laser_cutter_mode() or LaserCutterModeEnum.DEFAULT.value, ) ), 200, @@ -2604,7 +2635,7 @@ def is_job_cancelled(): is_job_cancelled() # check before conversion started - profile = self.laserCutterProfileManager.get_current_or_default() + profile = self.laser_cutter_profile_service.get_current_or_default() maxWidth = profile["volume"]["width"] maxHeight = profile["volume"]["depth"] @@ -2693,7 +2724,7 @@ def on_event(self, event, payload): if event == MrBeamEvents.BOOT_GRACE_PERIOD_END: if self.calibration_tool_mode: - self._printer.home("Homing before starting calibration tool") + self._printer.home() self.lid_handler.onLensCalibrationStart() if event == OctoPrintEvents.ERROR: @@ -2833,7 +2864,7 @@ def laser_factory(self, components, *args, **kwargs): return Laser( components["file_manager"], components["analysis_queue"], - self.laserCutterProfileManager, + self.laser_cutter_profile_service, ) def laser_filemanager(self, *args, **kwargs): diff --git a/octoprint_mrbeam/analytics/analytics_handler.py b/octoprint_mrbeam/analytics/analytics_handler.py index cff9fcb10..93778ea2d 100644 --- a/octoprint_mrbeam/analytics/analytics_handler.py +++ b/octoprint_mrbeam/analytics/analytics_handler.py @@ -1526,7 +1526,7 @@ def _cleanup_job(self): def _init_new_job(self): self._cleanup_job() self._current_job_id = "j_{}_{}".format(self._snr, time.time()) - self._add_job_event(AnalyticsKeys.Job.Event.LASERJOB_STARTED, payload=payload) + self._add_job_event(AnalyticsKeys.Job.Event.LASERJOB_STARTED, payload=None) # -------- WRITER THREAD (queue --> analytics file) ---------------------------------------------------------------- def _write_queue_to_analytics_file(self): diff --git a/octoprint_mrbeam/services/__init__.py b/octoprint_mrbeam/constant/__init__.py similarity index 100% rename from octoprint_mrbeam/services/__init__.py rename to octoprint_mrbeam/constant/__init__.py diff --git a/tests/services/__init__.py b/octoprint_mrbeam/constant/profile/__init__.py similarity index 100% rename from tests/services/__init__.py rename to octoprint_mrbeam/constant/profile/__init__.py diff --git a/octoprint_mrbeam/constant/profile/laser_cutter/__init__.py b/octoprint_mrbeam/constant/profile/laser_cutter/__init__.py new file mode 100644 index 000000000..fc6a2a627 --- /dev/null +++ b/octoprint_mrbeam/constant/profile/laser_cutter/__init__.py @@ -0,0 +1,16 @@ +from __future__ import absolute_import + +from octoprint.util import dict_merge +from octoprint_mrbeam.constant.profile.laser_cutter import default, series_2c, rotary, series_2c_rotary + +# Default profile for the Mr beam laser cutter in default mode and a non-2C series +default_profile = default.profile + +# Default profile for the Mr beam laser cutter in default mode and a 2C series +series_2c_profile = dict_merge(default_profile, series_2c.profile) + +# Default profile for the Mr beam laser cutter in rotary mode and a non-2C series +rotary_profile = dict_merge(default_profile, rotary.profile) + +# Default profile for the Mr beam laser cutter in rotary mode and a 2C series +series_2c_rotary_profile = dict_merge(default_profile, series_2c_rotary.profile) diff --git a/octoprint_mrbeam/printing/profiles/default.py b/octoprint_mrbeam/constant/profile/laser_cutter/default.py similarity index 95% rename from octoprint_mrbeam/printing/profiles/default.py rename to octoprint_mrbeam/constant/profile/laser_cutter/default.py index 5eb341738..ca4531a1e 100644 --- a/octoprint_mrbeam/printing/profiles/default.py +++ b/octoprint_mrbeam/constant/profile/laser_cutter/default.py @@ -1,9 +1,9 @@ -# Default config for the Mr beam laser cutter +# Default profile for the Mr beam laser cutter in default mode and a non-2C series __all__ = ["profile"] profile = dict( - id="_default", + id="default", name="MrBeam2", model="X", axes=dict( @@ -47,6 +47,9 @@ width=500.0, working_area_shift_x=7.0, working_area_shift_y=0.0, + after_homing_shift_x=0.0, + after_homing_shift_y=0.0, + after_homing_shift_rate=5000, ), grbl=dict( resetOnConnect=True, diff --git a/octoprint_mrbeam/constant/profile/laser_cutter/rotary.py b/octoprint_mrbeam/constant/profile/laser_cutter/rotary.py new file mode 100644 index 000000000..4986b23ab --- /dev/null +++ b/octoprint_mrbeam/constant/profile/laser_cutter/rotary.py @@ -0,0 +1,23 @@ +# Default profile for the Mr beam laser cutter in default mode and a non-2C series + +__all__ = ["profile"] + +profile = dict( + id="rotary", + volume=dict( + depth=390.0, + width=500.0, + after_homing_shift_y=-80.0, # After homing shift in Y direction + after_homing_shift_rate=500, # After homing feed rate mm / min + ), + grbl=dict( + settings={ + 110: 500, # X Max rate, mm / min + 111: 500, # Y Max rate, mm / min + 120: 30, # X Acceleration, mm / sec ^ 2 + 121: 30, # Y Acceleration, mm / sec ^ 2 + 130: 360, # X max travel, mm # !! C-Series: 501.1 + 131: 200, # Y max travel, mm + }, + ), +) diff --git a/octoprint_mrbeam/constant/profile/laser_cutter/series_2c.py b/octoprint_mrbeam/constant/profile/laser_cutter/series_2c.py new file mode 100644 index 000000000..363af7fd5 --- /dev/null +++ b/octoprint_mrbeam/constant/profile/laser_cutter/series_2c.py @@ -0,0 +1,18 @@ +#!/usr/bin/env python3 +from octoprint_mrbeam.enums.device_series import DeviceSeriesEnum + +profile = dict( + id="series_" + DeviceSeriesEnum.C.value, + model="C", + legacy=dict( + job_done_home_position_x=250, + ), + volume=dict( + working_area_shift_x=0.0, + ), + grbl=dict( + settings={ + 130: 501.1, # X max travel, mm + }, + ), +) diff --git a/octoprint_mrbeam/constant/profile/laser_cutter/series_2c_rotary.py b/octoprint_mrbeam/constant/profile/laser_cutter/series_2c_rotary.py new file mode 100644 index 000000000..f41f7498d --- /dev/null +++ b/octoprint_mrbeam/constant/profile/laser_cutter/series_2c_rotary.py @@ -0,0 +1,29 @@ +# Default profile for the Mr beam laser cutter in default mode and a non-2C series +from octoprint_mrbeam.enums.device_series import DeviceSeriesEnum + +__all__ = ["profile"] + +profile = dict( + id="series_" + DeviceSeriesEnum.C.value + "_rotary", + model="C", + legacy=dict( + job_done_home_position_x=250, + ), + volume=dict( + working_area_shift_x=0.0, + depth=390.0, + width=500.0, + after_homing_shift_y=-80.0, # After homing shift in Y direction + after_homing_shift_rate=500, # After homing feed rate mm / min + ), + grbl=dict( + settings={ + 110: 500, # X Max rate, mm / min + 111: 500, # Y Max rate, mm / min + 120: 30, # X Acceleration, mm / sec ^ 2 + 121: 30, # Y Acceleration, mm / sec ^ 2 + 130: 346, # X max travel, mm # !! C-Series: 501.1 + 131: 200, # Y max travel, mm + }, + ), +) diff --git a/octoprint_mrbeam/enums/device_series.py b/octoprint_mrbeam/enums/device_series.py new file mode 100644 index 000000000..c0f7255fe --- /dev/null +++ b/octoprint_mrbeam/enums/device_series.py @@ -0,0 +1,6 @@ +from enum import Enum + + +class DeviceSeriesEnum(Enum): + """In this Enum we collect the different device series that are only relevant to implementations""" + C = "2C" diff --git a/octoprint_mrbeam/filemanager/__init__.py b/octoprint_mrbeam/filemanager/__init__.py index de259c9d9..c4e5957db 100644 --- a/octoprint_mrbeam/filemanager/__init__.py +++ b/octoprint_mrbeam/filemanager/__init__.py @@ -50,7 +50,7 @@ def __init__(self, plugin): self, self._plugin._analysis_queue, self._plugin._slicing_manager, - self._plugin.laserCutterProfileManager, + self._plugin.laser_cutter_profile_service, initial_storage_managers=storage_managers, ) diff --git a/octoprint_mrbeam/iobeam/lid_handler.py b/octoprint_mrbeam/iobeam/lid_handler.py index f553e78a8..3f16256a4 100644 --- a/octoprint_mrbeam/iobeam/lid_handler.py +++ b/octoprint_mrbeam/iobeam/lid_handler.py @@ -93,7 +93,7 @@ def __init__(self, plugin): self._printer = plugin._printer self._plugin_manager = plugin._plugin_manager self._laserCutterProfile = ( - plugin.laserCutterProfileManager.get_current_or_default() + plugin.laser_cutter_profile_service.get_current_or_default() ) self._logger = mrb_logger( "octoprint.plugins.mrbeam.iobeam.lidhandler", logging.INFO diff --git a/octoprint_mrbeam/iobeam/temperature_manager.py b/octoprint_mrbeam/iobeam/temperature_manager.py index 286002275..bc49ebe4c 100644 --- a/octoprint_mrbeam/iobeam/temperature_manager.py +++ b/octoprint_mrbeam/iobeam/temperature_manager.py @@ -55,7 +55,7 @@ def __init__(self, plugin, laser): self._plugin.laserhead_handler.current_laserhead_high_temperature_warn_offset ) self.cooling_duration = ( - plugin.laserCutterProfileManager.get_current_or_default()["laser"][ + plugin.laser_cutter_profile_service.get_current_or_default()["laser"][ "cooling_duration" ] ) @@ -147,7 +147,7 @@ def reset(self, kwargs): self._plugin.laserhead_handler.current_laserhead_high_temperature_warn_offset ) self.cooling_duration = ( - self._plugin.laserCutterProfileManager.get_current_or_default()["laser"][ + self._plugin.laser_cutter_profile_service.get_current_or_default()["laser"][ "cooling_duration" ] ) diff --git a/octoprint_mrbeam/migrate.py b/octoprint_mrbeam/migrate.py index 35d42cf2b..3db2b5ced 100644 --- a/octoprint_mrbeam/migrate.py +++ b/octoprint_mrbeam/migrate.py @@ -13,7 +13,7 @@ from octoprint_mrbeam.mrb_logger import mrb_logger from octoprint_mrbeam.util.cmd_exec import exec_cmd, exec_cmd_output from octoprint_mrbeam.util import logExceptions -from octoprint_mrbeam.printing.profile import laserCutterProfileManager +from octoprint_mrbeam.service.profile.laser_cutter_profile import laser_cutter_profile_service from octoprint_mrbeam.printing.comm_acc2 import MachineCom from octoprint_mrbeam.materials import materials from octoprint_mrbeam.migration import ( @@ -81,9 +81,6 @@ def __init__(self, plugin): def run(self): try: - if not self.is_lasercutterProfile_set(): - self.set_lasercutterProfile() - # must be done outside of is_migration_required()-block. self.delete_egg_dir_leftovers() @@ -496,39 +493,6 @@ def fix_ssh_key_permissions(self): ##### migrations ##### ########################################################## - def migrate_from_0_0_0(self): - self._logger.info("migrate_from_0_0_0() ") - write = False - my_profile = laserCutterProfileManager().get_default() - if ( - not "laser" in my_profile - or not "intensity_factor" in my_profile["laser"] - or not my_profile["laser"]["intensity_factor"] - ): - # this setting was introduce with MrbeamPlugin version 0.1.13 - my_profile["laser"]["intensity_factor"] = 13 - write = True - self._logger.info( - "migrate_from_0_0_0() Set lasercutterProfile ['laser']['intensity_factor'] = 13" - ) - if ( - not "dust" in my_profile - or not "auto_mode_time" in my_profile["dust"] - or not my_profile["dust"]["auto_mode_time"] - ): - # previous default was 300 (5min) - my_profile["dust"]["auto_mode_time"] = 60 - write = True - self._logger.info( - "migrate_from_0_0_0() Set lasercutterProfile ['dust']['auto_mode_time'] = 60" - ) - if write: - laserCutterProfileManager().save( - my_profile, allow_overwrite=True, make_default=True - ) - else: - self._logger.info("migrate_from_0_0_0() nothing to do here.") - def setup_iptables(self): """Creates iptables config file. @@ -576,21 +540,6 @@ def setup_iptables(self): "setup_iptables() Created and loaded iptables conf: '%s'", iptables_file ) - def add_grbl_130_maxTravel(self): - """Since we introduced GRBL settings sync (aka correct_settings), we - have grbl settings in machine profiles So we need to add the old value - for 'x max travel' for C-Series devices there.""" - if self.plugin._device_series == "2C": - default_profile = laserCutterProfileManager().get_default() - default_profile["grbl"]["settings"][130] = 501.1 - laserCutterProfileManager().save( - default_profile, allow_overwrite=True, make_default=True - ) - self._logger.info( - "add_grbl_130_maxTravel() C-Series Device: Added ['grbl']['settings'][130]=501.1 to lasercutterProfile: %s", - default_profile, - ) - def update_change_hostename_apname_scripts(self): self._logger.info("update_change_hostename_apname_scripts() ") src_change_hostname = os.path.join( @@ -680,16 +629,16 @@ def update_mount_manager( def auto_update_grbl(self): self._logger.info("auto_update_grbl() ") - laserCutterProfile = laserCutterProfileManager().get_current_or_default() - if laserCutterProfile: - laserCutterProfile["grbl"][ + laser_cutter_profile = laser_cutter_profile_service().get_current_or_default() + if laser_cutter_profile: + laser_cutter_profile["grbl"][ "auto_update_version" ] = self.GRBL_AUTO_UPDATE_VERSION - laserCutterProfile["grbl"]["auto_update_file"] = self.GRBL_AUTO_UPDATE_FILE - laserCutterProfileManager().save(laserCutterProfile, allow_overwrite=True) + laser_cutter_profile["grbl"]["auto_update_file"] = self.GRBL_AUTO_UPDATE_FILE + laser_cutter_profile_service().save(laser_cutter_profile, allow_overwrite=True) else: raise MigrationException( - "Error while configuring grbl update - no lasercutterProfile", + "Error while configuring grbl update - no laser_cutter_profile", ) def inflate_file_system(self): @@ -826,119 +775,6 @@ def fix_s_series_mount_manager(self): exec_cmd("sudo rm /etc/systemd/system/usb_mount_manager_remove.service") self._logger.info("end fix_s_series_mount_manager") - ########################################################## - ##### lasercutterProfiles ##### - ########################################################## - - def is_lasercutterProfile_set(self): - """Is a non-generic lasercutterProfile set as default profile? - - :return: True if a non-generic lasercutterProfile is set as default - """ - return laserCutterProfileManager().get_default()["id"] != "my_default" - - def set_lasercutterProfile(self): - if laserCutterProfileManager().get_default()["id"] == "my_default": - self._logger.info( - "set_lasercutterPorfile() Setting lasercutterProfile for device '%s'", - self.plugin._device_series, - ) - - if self.plugin._device_series == "2X": - # 2X placeholder value. - self._logger.error( - "set_lasercutterProfile() Can't set lasercutterProfile. device_series is %s", - self.plugin._device_series, - ) - return - elif self.plugin._device_series == "2C": - self.set_lasercutterPorfile_2C() - elif self.plugin._device_series in ("2D", "2E", "2F"): - self.set_lasercutterPorfile_2DEF(series=self.plugin._device_series[1]) - else: - self.set_lasercutterPorfile_2all() - self.save_current_version() - - def set_lasercutterPorfile_2all(self): - profile_id = "MrBeam{}".format(self.plugin._device_series) - if laserCutterProfileManager().exists(profile_id): - laserCutterProfileManager().set_default(profile_id) - self._logger.info( - "set_lasercutterPorfile_2all() Set lasercutterProfile '%s' as default.", - profile_id, - ) - else: - self._logger.warn( - "set_lasercutterPorfile_2all() No lasercutterProfile '%s' found. Keep using generic profile.", - profile_id, - ) - - def set_lasercutterPorfile_2C(self): - """Series C came with no default lasercutterProfile set. - - FYI: the image contained only a profile called 'MrBeam2B' which was never used since it wasn't set as default - """ - profile_id = "MrBeam2C" - model = "C" - - if laserCutterProfileManager().exists(profile_id): - laserCutterProfileManager().set_default(profile_id) - self._logger.info( - "set_lasercutterPorfile_2C() Set lasercutterProfile '%s' as default.", - profile_id, - ) - else: - default_profile = laserCutterProfileManager().get_default() - default_profile["id"] = profile_id - default_profile["name"] = "MrBeam2" - default_profile["model"] = model - default_profile["legacy"] = dict() - default_profile["legacy"]["job_done_home_position_x"] = 250 - default_profile["grbl"]["settings"][130] = 501.1 - laserCutterProfileManager().save( - default_profile, allow_overwrite=True, make_default=True - ) - self._logger.info( - "set_lasercutterPorfile_2C() Created lasercutterProfile '%s' and set as default. Content: %s", - profile_id, - default_profile, - ) - - def set_lasercutterPorfile_2DEF(self, series): - """ - In case lasercutterProfile does not exist - :return: - """ - series = series.upper() - profile_id = "MrBeam2{}".format(series) - model = series - - if laserCutterProfileManager().exists(profile_id): - laserCutterProfileManager().set_default(profile_id) - self._logger.info( - "set_lasercutterPorfile_2DEF() Set lasercutterProfile '%s' as default.", - profile_id, - ) - else: - default_profile = laserCutterProfileManager().get_default() - default_profile["id"] = profile_id - default_profile["name"] = "MrBeam2" - default_profile["model"] = model - laserCutterProfileManager().save( - default_profile, allow_overwrite=True, make_default=True - ) - self._logger.info( - "set_lasercutterPorfile_2DEF() Created lasercutterProfile '%s' and set as default. Content: %s", - profile_id, - default_profile, - ) - - self._logger.info( - "set_lasercutterPorfile_2DEF() Created lasercutterProfile '%s' and set as default. Content: %s", - profile_id, - default_profile, - ) - def rm_camera_calibration_repo(self): """Delete the legacy camera calibration and detection repo.""" from octoprint.settings import settings diff --git a/octoprint_mrbeam/model/laser_cutter_profile.py b/octoprint_mrbeam/model/laser_cutter_profile.py new file mode 100644 index 000000000..051701e43 --- /dev/null +++ b/octoprint_mrbeam/model/laser_cutter_profile.py @@ -0,0 +1,19 @@ +from octoprint_mrbeam.mrb_logger import mrb_logger + +class LaserCutterProfileModel(object): + """Laser cutter profile model.""" + + def __init__(self, profile=None): + """Initialize laser cutter profile. + + If the profile is not found in the defined profiles, it will fall back to default. + + Args: + profile (dict): The profile of the laser cutter. + """ + self._logger = mrb_logger("octoprint.plugins.mrbeam.model.laser_cutter_profile") + self._data = profile + + @property + def data(self): + return self._data diff --git a/octoprint_mrbeam/model/settings_model.py b/octoprint_mrbeam/model/settings_model.py index e605b77fd..61dcd7ea7 100644 --- a/octoprint_mrbeam/model/settings_model.py +++ b/octoprint_mrbeam/model/settings_model.py @@ -43,21 +43,3 @@ def __repr__(self): self.url, self.healthcheck_url, ) - - -class MaterialStoreModel: - """ - Data object containing information corresponding to the material store section to be used on the jinja2 templates - """ - - def __init__(self, enabled=False, url="", healthcheck_url=""): - self.enabled = enabled - self.url = url - self.healthcheck_url = healthcheck_url - - def __repr__(self): - return "MaterialStore(enabled=%s, url=%s, healthcheck_url=%s)" % ( - self.enabled, - self.url, - self.healthcheck_url, - ) diff --git a/octoprint_mrbeam/mrbeam_events.py b/octoprint_mrbeam/mrbeam_events.py index 4844dfcdc..3de1a543a 100644 --- a/octoprint_mrbeam/mrbeam_events.py +++ b/octoprint_mrbeam/mrbeam_events.py @@ -71,6 +71,7 @@ class MrBeamEvents(object): HARDWARE_MALFUNCTION = "HardwareMalfunction" LASER_HEAD_READ = "LaserHeadRead" + LASER_CUTTER_PROFILE_INITIALIZED = "LaserCutterProfileInitialized" # Camera Calibration Screen Events RAW_IMAGE_TAKING_START = "RawImageTakingStart" diff --git a/octoprint_mrbeam/printing/comm_acc2.py b/octoprint_mrbeam/printing/comm_acc2.py index 80e6df29c..8896df733 100644 --- a/octoprint_mrbeam/printing/comm_acc2.py +++ b/octoprint_mrbeam/printing/comm_acc2.py @@ -33,14 +33,14 @@ ) from octoprint_mrbeam.notifications import NotificationIds -from octoprint_mrbeam.printing.profile import laserCutterProfileManager from octoprint_mrbeam.mrb_logger import mrb_logger from octoprint_mrbeam.printing.acc_line_buffer import AccLineBuffer from octoprint_mrbeam.printing.acc_watch_dog import AccWatchDog from octoprint_mrbeam.util import dict_get from octoprint_mrbeam.util.cmd_exec import exec_cmd_output from octoprint_mrbeam.mrbeam_events import MrBeamEvents - +from octoprint_mrbeam.service.profile.laser_cutter_profile import laser_cutter_profile_service +from octoprint_mrbeam.constant.profile import laser_cutter as laser_cutter_profiles ### MachineCom ######################################################################################################### class MachineCom(object): @@ -160,11 +160,16 @@ def __init__( baudrate = settingsBaudrate if callbackObject is None: callbackObject = MachineComPrintCallback() + if printerProfileManager: + laser_cutter_profile = printerProfileManager().get_current_or_default() + else: + laser_cutter_profile = laser_cutter_profiles.default_profile self._port = port self._baudrate = baudrate self._callback = callbackObject - self._laserCutterProfile = laserCutterProfileManager().get_current_or_default() + + self._laserCutterProfile = laser_cutter_profile self._state = self.STATE_NONE self._grbl_state = None @@ -1536,7 +1541,7 @@ def _verify_and_correct_loaded_grbl_settings( ) commands.append("${id}={val}".format(id=id, val=value)) elif my_grbl_settings[id]["value"] != value: - self._logger.error( + self._logger.warn( "GRBL Settings $%s=%s (%s) - Incorrect value! Should be: %s", id, my_grbl_settings[id]["value"], @@ -1948,7 +1953,7 @@ def reset_grbl_auto_update_config(self): try: self._laserCutterProfile["grbl"]["auto_update_file"] = None self._laserCutterProfile["grbl"]["auto_update_version"] = None - laserCutterProfileManager().save( + laser_cutter_profile_service().save( self._laserCutterProfile, allow_overwrite=True ) except Exception: @@ -2847,7 +2852,7 @@ def _set_compressor(self, value): except: self._logger.exception("Exception in _set_air_pressure() ") - def _set_compressor_pause(self, paused): + def _set_compressor_pause(self): try: _mrbeam_plugin_implementation.compressor_handler.set_compressor_pause() except: diff --git a/octoprint_mrbeam/printing/printer.py b/octoprint_mrbeam/printing/printer.py index b0546c1e5..2eae9f135 100644 --- a/octoprint_mrbeam/printing/printer.py +++ b/octoprint_mrbeam/printing/printer.py @@ -8,6 +8,7 @@ from octoprint_mrbeam.filemanager.analysis import beam_analysis_queue_factory from octoprint_mrbeam.util import dict_merge from octoprint_mrbeam.util.errors import ErrorCodes +from octoprint_mrbeam.service.profile.laser_cutter_profile import laser_cutter_profile_service class Laser(Printer): @@ -49,6 +50,9 @@ def __init__(self, fileManager, analysisQueue, printerProfileManager): ) self._user_notification_system = None + # We are overriding this attribute that is used in OctoPrint's Printer class + self._printerProfileManager = laser_cutter_profile_service() + self._event_bus = eventManager() self._event_bus.subscribe( MrBeamEvents.LASER_JOB_ABORT, self._on_laser_job_abort @@ -72,8 +76,9 @@ def connect(self, port=None, baudrate=None, profile=None): if self._comm is not None: self._comm.close() - eventManager().fire(Events.CONNECTING, payload=dict(profile=profile)) - self._printerProfileManager.select(profile) + eventManager().fire(Events.CONNECTING, payload=dict(profile=self._printerProfileManager.get_current_or_default()['id'])) + # TODO: SW-4080: Handle when switching the profiles in realtime + # self._printerProfileManager.select(profile) self._comm = comm.MachineCom( port, baudrate, @@ -92,7 +97,24 @@ def set_colors(self, currentFileName, value): self._comm.setColors(currentFileName, value) # extend commands: home, position, increase_passes, decrease_passes - def home(self, axes): + def home(self, axes="xy"): + """ + Home the printer. + Commands to be sent to the printer in order to home: + Command 1: "$H" = Move to home position + Command 2: command = set coordinate origin + Command 3: "G21" = set units to millimeters + Command 4: "G91" = set relative coordinate mode + Command 5: moving_command = move to after homing position + Command 6: "G90" = set absolute coordinate mode + + Args: + axes: axes to home, default is "xy" + This is not used in the plugin, but kept for compatibility with the original implementation + + Returns: + None + """ printer_profile = self._printerProfileManager.get_current_or_default() params = dict( x=printer_profile["volume"]["width"] @@ -100,10 +122,17 @@ def home(self, axes): y=printer_profile["volume"]["depth"] + printer_profile["volume"]["working_area_shift_y"], z=0, + after_homing_shift_x=printer_profile["volume"]["after_homing_shift_x"], + after_homing_shift_y=printer_profile["volume"]["after_homing_shift_y"], + after_homing_shift_rate=printer_profile["volume"]["after_homing_shift_rate"], ) self._comm.rescue_from_home_pos() command = "G92X{x}Y{y}Z{z}".format(**params) - self.commands(["$H", command, "G90", "G21"]) + + # Moving command after homing + moving_command = "G1X{after_homing_shift_x}Y{after_homing_shift_y}F{after_homing_shift_rate}".format(**params) + + self.commands(["$H", command, "G21", "G91", moving_command, "G90"]) def is_homed(self): return self._stateMonitor._machinePosition == self.HOMING_POSITION @@ -112,7 +141,7 @@ def cancel_print(self): """Cancel the current printjob and do homing.""" super(Laser, self).cancel_print() time.sleep(0.5) - self.home(axes="wtf") + self.home() eventManager().fire(MrBeamEvents.PRINT_CANCELING_DONE) def fail_print(self, error_msg=None): @@ -124,7 +153,7 @@ def fail_print(self, error_msg=None): self._comm.cancelPrint(failed=True, error_msg=error_msg) time.sleep(0.5) - self.home(axes="wtf") + self.home() self._show_job_cancelled_due_to_internal_error() eventManager().fire(MrBeamEvents.PRINT_CANCELING_DONE) @@ -139,7 +168,7 @@ def abort_job(self, event): """ self._comm.abort_lasering(event) time.sleep(0.5) - self.home(axes="wtf") + self.home() self._event_bus.fire(MrBeamEvents.LASER_JOB_ABORTED, {"trigger": event}) def position(self, x, y): diff --git a/octoprint_mrbeam/printing/profile.py b/octoprint_mrbeam/printing/profile.py deleted file mode 100644 index ba8ecf310..000000000 --- a/octoprint_mrbeam/printing/profile.py +++ /dev/null @@ -1,478 +0,0 @@ -# coding=utf-8 -from __future__ import absolute_import - -__author__ = "Gina Häußge " -__license__ = "GNU Affero General Public License http://www.gnu.org/licenses/agpl.html" -__copyright__ = "Copyright (C) 2014 The OctoPrint Project - Released under terms of the AGPLv3 License" - -import os -import copy -from flask import url_for -import re -import collections -from itertools import chain - -from . import profiles - -from octoprint.printer.profile import PrinterProfileManager -from octoprint.util import ( - dict_merge, - dict_clean, - dict_contains_keys, - dict_minimal_mergediff, -) -from octoprint.settings import settings -from octoprint_mrbeam.mrb_logger import mrb_logger -from octoprint_mrbeam.util import dict_get -from octoprint_mrbeam.util.log import logme - - -# singleton -from octoprint_mrbeam.util.device_info import deviceInfo - -_instance = None - - -def laserCutterProfileManager(*a, **kw): - global _instance - if _instance is None: - _instance = LaserCutterProfileManager(*a, **kw) - return _instance - - -defaults = dict( - # general settings - svgDPI=90, - pierce_time=0, - # vector settings - speed=300, - intensity=500, - fill_areas=False, - engrave=False, - set_passes=1, - cut_outlines=True, - cross_fill=False, - fill_angle=0, - fill_spacing=0.25, - # pixel settings - beam_diameter=0.25, - intensity_white=0, - intensity_black=500, - feedrate_white=1500, - feedrate_black=250, - img_contrast=1.0, - img_sharpening=1.0, - img_dithering=False, - multicolor="", -) - - -class SaveError(Exception): - pass - - -class CouldNotOverwriteError(SaveError): - pass - - -class InvalidProfileError(Exception): - pass - - -LASER_PROFILE_DEFAULT = profiles.default.profile -LASER_PROFILE_2C = profiles.mrb2c.profile -LASER_PROFILE_DUMMY = profiles.dummy.profile - -LASER_PROFILES_DERIVED = ( - LASER_PROFILE_2C, - LASER_PROFILE_DUMMY, -) - -# fmt: off -LASER_PROFILES = tuple(chain( - (LASER_PROFILE_DEFAULT,), - (dict_merge(LASER_PROFILE_DEFAULT, profile) for profile in LASER_PROFILES_DERIVED) -)) -# fmt: on - -# /!\ "id" should always be written into a new laser profile -LASER_PROFILE_IDENTIFIERS = tuple(pr["id"] for pr in LASER_PROFILES) - -LASER_PROFILE_MAP = dict( - zip( - LASER_PROFILE_IDENTIFIERS, - LASER_PROFILES, - ) -) - - -class LaserCutterProfileManager(PrinterProfileManager): - - SETTINGS_PATH_PROFILE_DEFAULT_ID = ["lasercutterProfiles", "default"] - SETTINGS_PATH_PROFILE_DEFAULT_PROFILE = ["lasercutterProfiles", "defaultProfile"] - # SETTINGS_PATH_PROFILE_CURRENT_ID = ['lasercutterProfiles', 'current'] - - default = LASER_PROFILE_DEFAULT - - def __init__(self, profile_id=None): - _laser_cutter_profile_folder = ( - settings().getBaseFolder("printerProfiles") + "/lasercutterprofiles" - ) - if not os.path.exists(_laser_cutter_profile_folder): - os.makedirs(_laser_cutter_profile_folder) - PrinterProfileManager.__init__(self) - self._folder = _laser_cutter_profile_folder - self._logger = mrb_logger("octoprint.plugins.mrbeam." + __name__) - # HACK - select the default profile. - # See self.select() - waiting for upstream fix - self.select(profile_id or settings().get(self.SETTINGS_PATH_PROFILE_DEFAULT_ID)) - - def _migrate_old_default_profile(self): - # overwritten to prevent defautl OP migration. - pass - - def _verify_default_available(self): - # Overloaded from OP because of printerProfiles path ``default_id`` and hard-coded profiles - default_id = settings().get(self.SETTINGS_PATH_PROFILE_DEFAULT_ID) - if default_id is None: - default_id = "_default" - - if not self.exists(default_id): - # fmt: off - if not self.exists("_default"): - if default_id == "_default": - self._logger.error("Profile _default does not exist, it should be part of the defined profiles.") - else: - self._logger.error("Selected default profile {} and _default does not exist, _default should be defined in the hard coded profiles.".format(default_id)) - else: - self._logger.error("Selected default profile {} does not exists, resetting to _default".format(default_id)) - settings().set(self.SETTINGS_PATH_PROFILE_DEFAULT_ID, "_default") - settings().save() - # fmt: on - default_id = "_default" - - profile = self.get(default_id) - if profile is None: - # fmt: off - self._logger.error("Selected default profile {} is invalid, resetting to default values".format(default_id)) - # fmt: on - profile = copy.deepcopy(self.__class__.default) - profile["id"] = default_id - self.save(self.__class__.default, allow_overwrite=True, make_default=True) - - # @logme(True) - # fmt: off - def select(self, identifier): - """Overloaded because OctoPrint uses a global - ``PrinterProfileManager``, which on line 612 of - ``OctoPrint/src/octoprint/server/__init__.py`` selects the ``_default`` - printer profile name. - - FIXME - In upstream : create a hook that allows to change ``PrinterProfileManager`` - """ - _current_id = dict_get(self._current, ["id",]) - # self._logger.warning("ID %s, CURR %s", identifier, _current_id) - if (identifier in [None, "_default"]) and self.exists(_current_id): - self._logger.warning("Not selecting the _default profile because of OP default behaviour. See ``octoprint_mrbeam.printing.profile.select()``.") - return True - else: - return PrinterProfileManager.select(self, identifier) - # fmt: on - - def get(self, identifier): - """Extend the file based ``PrinterProfileManager.get`` with the few - hardcoded ones we have.""" - try: - default = self._load_default() - if identifier == "_default": - return default - elif ( - identifier in LASER_PROFILE_IDENTIFIERS - ): # if device series has profile defined use this - file_based_result = PrinterProfileManager.get(self, identifier) or {} - # Update derivated profiles using the default profile. - hard_coded = dict_merge(default, LASER_PROFILE_MAP[identifier]) - return dict_merge(hard_coded, file_based_result) - else: - if identifier is None: - identifier = ( - deviceInfo().get_type() - ) # generate identifier from device type - else: - default["id"] = identifier - default["model"] = identifier[-1] - return dict_merge(default, PrinterProfileManager.get(self, identifier)) - except InvalidProfileError: - return None - - def remove(self, identifier): - # Overloaded from OP because of printerProfiles path (default_id) - if self._current is not None and self._current["id"] == identifier: - return False - elif settings().get(self.SETTINGS_PATH_PROFILE_DEFAULT_ID) == identifier: - return False - return self._remove_from_path(self._get_profile_path(identifier)) - - def is_default_unmodified(self): - # Overloaded because of settings path and barely used by OP - return True - - def get_default(self): - # Overloaded because of settings path - default = settings().get(self.SETTINGS_PATH_PROFILE_DEFAULT_ID) - if default is not None and self.exists(default): - profile = self.get(default) - if profile is not None: - return profile - - return copy.deepcopy(self.__class__.default) - - def set_default(self, identifier): - # Overloaded because of settings path and extended identifiers - file_based_identifiers = self._load_all_identifiers().keys() - if identifier is not None and not ( - identifier in file_based_identifiers - or identifier in LASER_PROFILE_IDENTIFIERS - ): - return - - settings().set(self.SETTINGS_PATH_PROFILE_DEFAULT_ID, identifier, force=True) - settings().save() - - # @logme(output=True) - def get_current_or_default(self): - return PrinterProfileManager.get_current_or_default(self) - - def exists(self, identifier): - # if the regex matches and there is no profile it will use the default and change the id and model - if identifier is not None and ( - identifier in LASER_PROFILE_IDENTIFIERS - or re.match(r"MrBeam[0-9][A-Z]", identifier) - ): - return True - else: - return PrinterProfileManager.exists(self, identifier) - - # @logme(output=True) - def _load_all(self): - """Extend the file based ``PrinterProfileManager._load_all`` with the - few hardcoded ones we have.""" - file_based_profiles = PrinterProfileManager._load_all(self) - device_type = deviceInfo().get_type() - mrbeam_generated_profiles = {device_type: self.get(device_type)} - mrbeam_profiles = dict_merge(LASER_PROFILE_MAP, mrbeam_generated_profiles) - return dict_merge(mrbeam_profiles, file_based_profiles) - - def _load_default(self, defaultModel=None): - # Overloaded because of settings path - default = copy.deepcopy(LASER_PROFILE_DEFAULT) - profile = self._ensure_valid_profile(default) - if not profile: - self._logger.warn("Invalid default profile after applying overrides") - raise InvalidProfileError() - return profile - - def _save_to_path(self, path, profile, allow_overwrite=False): - """Changes the file base PrinterProfileManager._save_to_path so only - the diff between the profile and the default profile will be saved.""" - validated_profile = self._ensure_valid_profile(profile) - - if not validated_profile: - raise InvalidProfileError() - - default = self._load_default() - validated_profile = dict_minimal_mergediff(default, validated_profile) - - if os.path.exists(path) and not allow_overwrite: - raise SaveError( - "Profile %s already exists and not allowed to overwrite" % profile["id"] - ) - - import yaml - - from octoprint.util import atomic_write - - try: - with atomic_write(path, mode="wt", max_permissions=0o666) as f: - yaml.safe_dump( - validated_profile, - f, - default_flow_style=False, - indent=2, - allow_unicode=True, - ) - except Exception as e: - self._logger.exception( - "Error while trying to save profile %s" % validated_profile["id"] - ) - raise SaveError( - "Cannot save profile %s: %s" % (validated_profile["id"], str(e)) - ) - - def _ensure_valid_profile(self, profile): - # Ensuring that all keys are present is the default behaviour of the OP ``PrinterProfileManager`` - # This ``LaserCutterProfileManager`` can use partially declared profiles, as they are - # completed using the default profile. - - # will merge with default config so the minimal saved one won't fail - profile = dict_merge(copy.deepcopy(LASER_PROFILE_DEFAULT), profile) - - # conversion helper - def convert_value(value, path, converter): - for part in path[:-1]: - if not isinstance(value, dict) or not part in value: - raise RuntimeError( - "%s is not contained in profile" % ".".join(path) - ) - value = value[part] - - if not isinstance(value, dict) or not path[-1] in value: - raise RuntimeError("%s is not contained in profile" % ".".join(path)) - - value[path[-1]] = converter(value[path[-1]]) - - # convert ints - for path in ( - ("axes", "x", "speed"), - ("axes", "y", "speed"), - ("axes", "z", "speed"), - ): - try: - convert_value(profile, path, int) - except: - return False - - # convert floats - for path in (("volume", "width"), ("volume", "depth"), ("volume", "height")): - try: - convert_value(profile, path, float) - except: - return False - - # convert booleans - for path in ( - ("axes", "x", "inverted"), - ("axes", "y", "inverted"), - ("axes", "z", "inverted"), - ): - try: - convert_value(profile, path, bool) - except: - return False - - return profile - - # ~ Extra functionality - - # @logme(output=True) - def converted_profiles(self): - ret = {} - - default = self.get_default()["id"] - current = self.get_current_or_default()["id"] - for identifier, profile in self.get_all().items(): - ret[identifier] = copy.deepcopy(profile) - ret[identifier]["default"] = profile["id"] == default - ret[identifier]["current"] = profile["id"] == current - - return ret - - -class Profile(object): - def __init__(self, profile): - self.profile = profile - - # fmt: off - @staticmethod - def merge_profile(profile, overrides=None): - import copy - - result = copy.deepcopy(defaults) - for k in result.keys(): - profile_value = None - override_value = None - - if k in profile: - profile_value = profile[k] - if overrides and k in overrides: - override_value = overrides[k] - - if profile_value is None and override_value is None: - # neither override nor profile, no need to handle this key further - continue - - # just change the result value to the override_value if available, otherwise to the profile_value if - # that is given, else just leave as is - if override_value is not None: - result[k] = override_value - elif profile_value is not None: - result[k] = profile_value - return result - - def get(self, key): - if key in self.profile: - return self.profile[key] - elif key in defaults: - return defaults[key] - else: - return None - - def get_int(self, key, default=None): - value = self.get(key) - if value is None: - return default - - try: - return int(value) - except ValueError: - return default - - def get_float(self, key, default=None): - value = self.get(key) - if value is None: - return default - - if isinstance(value, (str, unicode, basestring)): - value = value.replace(",", ".").strip() - - try: - return float(value) - except ValueError: - return default - - def get_boolean(self, key, default=None): - value = self.get(key) - if value is None: - return default - - if isinstance(value, bool): - return value - elif isinstance(value, (str, unicode, basestring)): - return ( - value.lower() == "true" - or value.lower() == "yes" - or value.lower() == "on" - or value == "1" - ) - elif isinstance(value, (int, float)): - return value > 0 - else: - return value == True - - def convert_to_engine(self): - - settings = { - "--engraving-laser-speed": self.get_int("speed"), - "--laser-intensity": self.get_int("intensity"), - "--beam-diameter": self.get_float("beam_diameter"), - "--img-intensity-white": self.get_int("intensity_white"), - "--img-intensity-black": self.get_int("intensity_black"), - "--img-speed-white": self.get_int("feedrate_white"), - "--img-speed-black": self.get_int("feedrate_black"), - "--pierce-time": self.get_float("pierce_time"), - "--contrast": self.get_float("img_contrast"), - "--sharpening": self.get_float("img_sharpening"), - "--img-dithering": self.get_boolean("img_dithering"), - } - - return settings diff --git a/octoprint_mrbeam/printing/profiles/__init__.py b/octoprint_mrbeam/printing/profiles/__init__.py deleted file mode 100644 index 028b2c8f0..000000000 --- a/octoprint_mrbeam/printing/profiles/__init__.py +++ /dev/null @@ -1,4 +0,0 @@ -# coding=utf-8 -from __future__ import absolute_import - -from . import default, dummy, mrb2c diff --git a/octoprint_mrbeam/printing/profiles/dummy.py b/octoprint_mrbeam/printing/profiles/dummy.py deleted file mode 100644 index 6350aec81..000000000 --- a/octoprint_mrbeam/printing/profiles/dummy.py +++ /dev/null @@ -1,9 +0,0 @@ -# Dummy laser config for the virtual Mr beam laser cutter - -__all__ = ["profile"] - -profile = dict( - id="MrBeam2Dummy", - name="Dummy Laser", - model="X", -) diff --git a/octoprint_mrbeam/printing/profiles/mrb2c.py b/octoprint_mrbeam/printing/profiles/mrb2c.py deleted file mode 100644 index ab876b021..000000000 --- a/octoprint_mrbeam/printing/profiles/mrb2c.py +++ /dev/null @@ -1,12 +0,0 @@ -#!/usr/bin/env python3 - -profile = dict( - id="MrBeam2C", - model="C", - legacy=dict( - job_done_home_position_x=250, - ), - volume=dict( - working_area_shift_x=0.0, - ), -) diff --git a/octoprint_mrbeam/service/__init__.py b/octoprint_mrbeam/service/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/octoprint_mrbeam/services/burger_menu_service.py b/octoprint_mrbeam/service/burger_menu_service.py similarity index 100% rename from octoprint_mrbeam/services/burger_menu_service.py rename to octoprint_mrbeam/service/burger_menu_service.py diff --git a/octoprint_mrbeam/services/document_service.py b/octoprint_mrbeam/service/document_service.py similarity index 100% rename from octoprint_mrbeam/services/document_service.py rename to octoprint_mrbeam/service/document_service.py diff --git a/octoprint_mrbeam/services/laser_cutter_mode.py b/octoprint_mrbeam/service/laser_cutter_mode.py similarity index 97% rename from octoprint_mrbeam/services/laser_cutter_mode.py rename to octoprint_mrbeam/service/laser_cutter_mode.py index 212dd0fad..e7d217d07 100644 --- a/octoprint_mrbeam/services/laser_cutter_mode.py +++ b/octoprint_mrbeam/service/laser_cutter_mode.py @@ -6,7 +6,8 @@ def laser_cutter_mode_service(plugin): - """Get or create a singleton instance of the LaserCutterModeService. + """ + Get or create a singleton instance of the LaserCutterModeService. This function is used to manage a singleton instance of the LaserCutterModeService class. It ensures that only one instance of the service is created and returned @@ -20,7 +21,6 @@ def laser_cutter_mode_service(plugin): Returns: _instance (LaserCutterModeService): The singleton instance of the LaserCutterModeService class. If no instance exists, it creates one and returns it. - """ global _instance if _instance is None: diff --git a/octoprint_mrbeam/service/profile/__init__.py b/octoprint_mrbeam/service/profile/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/octoprint_mrbeam/service/profile/laser_cutter_profile.py b/octoprint_mrbeam/service/profile/laser_cutter_profile.py new file mode 100644 index 000000000..9010eeed1 --- /dev/null +++ b/octoprint_mrbeam/service/profile/laser_cutter_profile.py @@ -0,0 +1,239 @@ +from octoprint_mrbeam.mrb_logger import mrb_logger +from octoprint_mrbeam.service.profile.profile import ProfileService +from octoprint_mrbeam.model.laser_cutter_profile import LaserCutterProfileModel +from octoprint_mrbeam.constant.profile.laser_cutter import default_profile +from octoprint_mrbeam.mrbeam_events import MrBeamEvents +from octoprint_mrbeam.enums.laser_cutter_mode import LaserCutterModeEnum + +# singleton instance of the LaserCutterProfileService class to be used across the application +_instance = None + + +def laser_cutter_profile_service(plugin=None, profile=default_profile): + """ + Get or create a singleton instance of the LaserCutterProfileService. + + This function is used to manage a singleton instance of the LaserCutterProfileService + class. It ensures that only one instance of the service is created and returned + during the program's execution. + + Example Usage: laser_cutter_profile_service = laser_cutter_profile_service(plugin_instance) + + Args: + plugin (object): An object representing the MrBeamPlugin + profile (dict): The default profile of the laser cutter. + + Returns: + _instance (LaserCutterProfileService): The singleton instance of the LaserCutterProfileService + class. If no instance exists, it creates one and returns it. + """ + global _instance + if _instance is None: + _instance = LaserCutterProfileService(id="laser_cutter", profile=LaserCutterProfileModel(profile).data) + + # fire event to notify other plugins that the laser cutter profile is initialized + if plugin is not None: + plugin.fire_event( + MrBeamEvents.LASER_CUTTER_PROFILE_INITIALIZED, + dict(), + ) + + return _instance + + +class LaserCutterProfileService(ProfileService): + """ Service class for laser cutter profile. """ + + DEFAULT_PROFILE_ID = LaserCutterModeEnum.DEFAULT.value + + def __init__(self, id, profile): + """Initialize laser cutter profile service. + + Args: + id (str): The id of the profile. + profile (dict): The profile of the laser cutter. + """ + super(LaserCutterProfileService, self).__init__(id, profile) + self._logger = mrb_logger("octoprint.plugins.mrbeam.services.profile.laser_cutter_profile") + + def _migrate_profile(self, profile): + """Migrate the profile to the latest version. + + Args: + profile (dict): The profile of the laser cutter. + + Returns: + Boolean: True if the profile was migrated, False otherwise. + """ + self._logger.debug("Checking if profile needs to be migrated: %s", profile) + return False + + # Implementation Sample: + + # # make sure profile format is up to date + # modified = False + # + # if "volume" in profile and "formFactor" in profile["volume"] and not "origin" in profile["volume"]: + # profile["volume"]["origin"] = BedOrigin.CENTER if profile["volume"]["formFactor"] == BedTypes.CIRCULAR else BedOrigin.LOWERLEFT + # modified = True + # + # if "volume" in profile and not "custom_box" in profile["volume"]: + # profile["volume"]["custom_box"] = False + # modified = True + # + # if "extruder" in profile and not "sharedNozzle" in profile["extruder"]: + # profile["extruder"]["sharedNozzle"] = False + # modified = True + # + # if "extruder" in profile and "sharedNozzle" in profile["extruder"] and profile["extruder"]["sharedNozzle"]: + # profile["extruder"]["offsets"] = [(0.0, 0.0)] + # modified = True + # + # return modified + + def _ensure_valid_profile(self, profile): + """ + Ensure that the profile is valid. + + Args: + profile (dict): The profile of the laser cutter. + + Returns: + dict: The profile of the laser cutter. + """ + return profile + + # Implementation Sample: + + # # ensure all keys are present + # if not dict_contains_keys(self._default, profile): + # self._logger.warn("Profile invalid, missing keys. Expected: {expected!r}. Actual: {actual!r}".format( + # expected=self._default.keys(), actual=profile.keys())) + # return False + # + # # conversion helper + # def convert_value(profile, path, converter): + # value = profile + # for part in path[:-1]: + # if not isinstance(value, dict) or not part in value: + # raise RuntimeError("%s is not contained in profile" % ".".join(path)) + # value = value[part] + # + # if not isinstance(value, dict) or not path[-1] in value: + # raise RuntimeError("%s is not contained in profile" % ".".join(path)) + # + # value[path[-1]] = converter(value[path[-1]]) + # + # # convert ints + # for path in (("extruder", "count"), ("axes", "x", "speed"), ("axes", "y", "speed"), ("axes", "z", "speed")): + # try: + # convert_value(profile, path, int) + # except Exception as e: + # self._logger.warn( + # "Profile has invalid value for path {path!r}: {msg}".format(path=".".join(path), msg=str(e))) + # return False + # + # # convert floats + # for path in (("volume", "width"), ("volume", "depth"), ("volume", "height"), ("extruder", "nozzleDiameter")): + # try: + # convert_value(profile, path, float) + # except Exception as e: + # self._logger.warn( + # "Profile has invalid value for path {path!r}: {msg}".format(path=".".join(path), msg=str(e))) + # return False + # + # # convert booleans + # for path in ( + # ("axes", "x", "inverted"), ("axes", "y", "inverted"), ("axes", "z", "inverted"), ("extruder", "sharedNozzle")): + # try: + # convert_value(profile, path, bool) + # except Exception as e: + # self._logger.warn( + # "Profile has invalid value for path {path!r}: {msg}".format(path=".".join(path), msg=str(e))) + # return False + # + # # validate form factor + # if not profile["volume"]["formFactor"] in BedTypes.values(): + # self._logger.warn("Profile has invalid value volume.formFactor: {formFactor}".format( + # formFactor=profile["volume"]["formFactor"])) + # return False + # + # # validate origin type + # if not profile["volume"]["origin"] in BedOrigin.values(): + # self._logger.warn( + # "Profile has invalid value in volume.origin: {origin}".format(origin=profile["volume"]["origin"])) + # return False + # + # # ensure origin and form factor combination is legal + # if profile["volume"]["formFactor"] == BedTypes.CIRCULAR and not profile["volume"]["origin"] == BedOrigin.CENTER: + # profile["volume"]["origin"] = BedOrigin.CENTER + # + # # force width and depth of volume to be identical for circular beds, with width being the reference + # if profile["volume"]["formFactor"] == BedTypes.CIRCULAR: + # profile["volume"]["depth"] = profile["volume"]["width"] + # + # # if we have a custom bounding box, validate it + # if profile["volume"]["custom_box"] and isinstance(profile["volume"]["custom_box"], dict): + # if not len(profile["volume"]["custom_box"]): + # profile["volume"]["custom_box"] = False + # + # else: + # default_box = self._default_box_for_volume(profile["volume"]) + # for prop, limiter in (("x_min", min), ("y_min", min), ("z_min", min), + # ("x_max", max), ("y_max", max), ("z_max", max)): + # if prop not in profile["volume"]["custom_box"] or profile["volume"]["custom_box"][prop] is None: + # profile["volume"]["custom_box"][prop] = default_box[prop] + # else: + # value = profile["volume"]["custom_box"][prop] + # try: + # value = limiter(float(value), default_box[prop]) + # profile["volume"]["custom_box"][prop] = value + # except: + # self._logger.warn( + # "Profile has invalid value in volume.custom_box.{}: {!r}".format(prop, value)) + # return False + # + # # make sure we actually do have a custom box and not just the same values as the + # # default box + # for prop in profile["volume"]["custom_box"]: + # if profile["volume"]["custom_box"][prop] != default_box[prop]: + # break + # else: + # # exactly the same as the default box, remove custom box + # profile["volume"]["custom_box"] = False + # + # # validate offsets + # offsets = [] + # for offset in profile["extruder"]["offsets"]: + # if not len(offset) == 2: + # self._logger.warn("Profile has an invalid extruder.offsets entry: {entry!r}".format(entry=offset)) + # return False + # x_offset, y_offset = offset + # try: + # offsets.append((float(x_offset), float(y_offset))) + # except: + # self._logger.warn( + # "Profile has an extruder.offsets entry with non-float values: {entry!r}".format(entry=offset)) + # return False + # profile["extruder"]["offsets"] = offsets + # + # return profile + + # @staticmethod + # def _default_box_for_volume(volume): + # if volume["origin"] == BedOrigin.CENTER: + # half_width = volume["width"] / 2.0 + # half_depth = volume["depth"] / 2.0 + # return dict(x_min=-half_width, + # x_max=half_width, + # y_min=-half_depth, + # y_max=half_depth, + # z_min=0.0, + # z_max=volume["height"]) + # else: + # return dict(x_min=0.0, + # x_max=volume["width"], + # y_min=0.0, + # y_max=volume["depth"], + # z_min=0.0, + # z_max=volume["height"]) diff --git a/octoprint_mrbeam/service/profile/profile.py b/octoprint_mrbeam/service/profile/profile.py new file mode 100644 index 000000000..b27e6d5e5 --- /dev/null +++ b/octoprint_mrbeam/service/profile/profile.py @@ -0,0 +1,337 @@ +# coding=utf-8 +""" +This file is a modified version of the original file from OctoPrint. +The original file is profile.py located in the folder octoprint/printer/profile.py. +The original class was designed to work only with printer profiles and with defined +dictionary structures. This modified version is designed to work with any kind of +profile and with any kind of dictionary structure. The only requirement is that the +profile is a dictionary. +""" + +from __future__ import absolute_import, division, print_function + +import os +import copy +import re +from octoprint_mrbeam.mrb_logger import mrb_logger + +try: + from os import scandir +except ImportError: + from scandir import scandir + +from octoprint.settings import settings +from octoprint.util import dict_merge, dict_sanitize, dict_contains_keys, is_hidden_path + + +class SaveError(Exception): + pass + + +class CouldNotOverwriteError(SaveError): + pass + + +class InvalidProfileError(Exception): + pass + + +class ProfileService(object): + """ + Manager for profiles. Offers methods to select the globally used profile and to list, add, remove, + load and save profiles. + + A profile is a ``dict`` of a certain structure. + + Args: + default (dict): The default profile to use if no other profile is selected. + folder (str): The folder to store the profiles in. + """ + + DEFAULT_PROFILE_ID = "_default" + PROFILE_CONFIGURATION_FILE_ENDING = ".profile" + + def __init__(self, id="", default=None): + self._default = default + self._id = id + self._current = None + self._logger = mrb_logger("octoprint.plugins.mrbeam.service.profile.profile") + + self._folder = os.path.join( + settings().getBaseFolder("base"), + "profiles", + self._id + ) + if not os.path.isdir(self._folder): + os.makedirs(self._folder) + + self._migrate_old_default_profile() + self._verify_default_available() + + def _migrate_old_default_profile(self): + default_overrides = settings().get(["profiles", self._id, "defaultProfile"]) + if not default_overrides: + return + + if self.exists(self.DEFAULT_PROFILE_ID): + return + + if not isinstance(default_overrides, dict): + self._logger.warn( + "Existing default profile from settings is not a valid profile, refusing to migrate: {!r}".format( + default_overrides)) + return + + default_overrides["id"] = self.DEFAULT_PROFILE_ID + self.save(default_overrides) + + settings().set(["profiles", self._id, "defaultProfile"], None, force=True) + settings().save() + + self._logger.info("Migrated default profile from settings to {}.profile: {!r}".format(self.DEFAULT_PROFILE_ID, default_overrides)) + + def _verify_default_available(self): + default_id = settings().get(["profiles", self._id, "default"]) + if default_id is None: + default_id = self.DEFAULT_PROFILE_ID + + if not self.exists(default_id): + if not self.exists(self.DEFAULT_PROFILE_ID): + if default_id == self.DEFAULT_PROFILE_ID: + self._logger.error( + "Profile {} does not exist, creating {} again and setting it as default".format(self.DEFAULT_PROFILE_ID, self.DEFAULT_PROFILE_ID)) + else: + self._logger.error( + "Selected default profile {} and {} do not exist, creating {} again and setting it as default".format( + default_id, self.DEFAULT_PROFILE_ID, self.DEFAULT_PROFILE_ID)) + self.save(self._default, allow_overwrite=True, make_default=True) + else: + self._logger.error( + "Selected default profile {} does not exists, resetting to {}".format(default_id, self.DEFAULT_PROFILE_ID)) + settings().set(["profiles", self._id, "default"], self.DEFAULT_PROFILE_ID, force=True) + settings().save() + default_id = self.DEFAULT_PROFILE_ID + + profile = self.get(default_id) + if profile is None: + self._logger.error("Selected default profile {} is invalid, resetting to default values".format(default_id)) + profile = copy.deepcopy(self._default) + profile["id"] = default_id + self.save(self._default, allow_overwrite=True, make_default=True) + + def select(self, identifier): + if identifier is None or not self.exists(identifier): + self._current = self.get_default() + return False + else: + self._current = self.get(identifier) + if self._current is None: + self._logger.error("Profile {} is invalid, cannot select, falling back to default".format(identifier)) + self._current = self.get_default() + return False + else: + return True + + def deselect(self): + self._current = None + + def get_all(self): + return self._load_all() + + def get(self, identifier): + try: + if self.exists(identifier): + return self._load_from_path(self._get_profile_path(identifier)) + else: + return None + except InvalidProfileError: + return None + + def remove(self, identifier): + if (self._current is not None and self._current["id"] == identifier) or settings().get(["profiles", self._id, "default"]) == identifier: + return False + return self._remove_from_path(self._get_profile_path(identifier)) + + def save(self, profile, allow_overwrite=False, make_default=False): + if "id" in profile: + identifier = profile["id"] + elif "name" in profile: + identifier = profile["name"] + else: + raise InvalidProfileError("profile must contain either id or name") + + identifier = self._sanitize(identifier) + profile["id"] = identifier + + self._migrate_profile(profile) + profile = dict_sanitize(profile, self._default) + profile = dict_merge(self._default, profile) + + self._save_to_path(self._get_profile_path(identifier), profile, allow_overwrite=allow_overwrite) + + if make_default: + settings().set(["profiles", self._id, "default"], identifier, force=True) + settings().save() + + if self._current is not None and self._current["id"] == identifier: + self.select(identifier) + return self.get(identifier) + + def is_default_unmodified(self): + default = settings().get(["profiles", self._id, "default"]) + return default is None or default == self.DEFAULT_PROFILE_ID or not self.exists(self.DEFAULT_PROFILE_ID) + + @property + def profile_count(self): + return len(self._load_all_identifiers()) + + @property + def last_modified(self): + dates = [os.stat(self._folder).st_mtime] + dates += [entry.stat().st_mtime for entry in scandir(self._folder) if entry.name.endswith(self.PROFILE_CONFIGURATION_FILE_ENDING)] + return max(dates) + + def get_default(self): + default = settings().get(["profiles", self._id, "default"]) + if default is not None and self.exists(default): + profile = self.get(default) + if profile is not None: + return profile + else: + self._logger.warn("Default profile {} is invalid, falling back to built-in defaults".format(default)) + + return copy.deepcopy(self._default) + + def set_default(self, identifier): + all_identifiers = self._load_all_identifiers().keys() + if identifier is not None and identifier not in all_identifiers: + return + + settings().set(["profiles", self._id, "default"], identifier, force=True) + settings().save() + + def get_current_or_default(self): + if self._current is not None: + return self._current + else: + return self.get_default() + + def get_current(self): + return self._current + + def exists(self, identifier): + if identifier is None: + return False + else: + path = self._get_profile_path(identifier) + return os.path.exists(path) and os.path.isfile(path) + + def _load_all(self): + all_identifiers = self._load_all_identifiers() + results = dict() + for identifier, path in all_identifiers.items(): + try: + profile = self._load_from_path(path) + except InvalidProfileError: + self._logger.warn("Profile {} is invalid, skipping".format(identifier)) + continue + + if profile is None: + continue + + results[identifier] = dict_merge(self._default, profile) + return results + + def _load_all_identifiers(self): + results = dict() + for entry in scandir(self._folder): + if is_hidden_path(entry.name) or not entry.name.endswith(self.PROFILE_CONFIGURATION_FILE_ENDING): + continue + + if not entry.is_file(): + continue + + identifier = entry.name[:-len(self.PROFILE_CONFIGURATION_FILE_ENDING)] + results[identifier] = entry.path + return results + + def _load_from_path(self, path): + if not os.path.exists(path) or not os.path.isfile(path): + return None + + import yaml + with open(path) as f: + profile = yaml.safe_load(f) + + if profile is None or not isinstance(profile, dict): + raise InvalidProfileError("Profile is None or not a dictionary") + + if self._migrate_profile(profile): + try: + self._save_to_path(path, profile, allow_overwrite=True) + except Exception as e: + self._logger.exception( + "Tried to save profile to {path} after migrating it while loading, ran into exception: {e}".format( + path=path, e=e)) + + profile = self._ensure_valid_profile(profile) + + if not profile: + self._logger.warn("Invalid profile: %s" % path) + raise InvalidProfileError() + return profile + + def _save_to_path(self, path, profile, allow_overwrite=False): + validated_profile = self._ensure_valid_profile(profile) + if not validated_profile: + raise InvalidProfileError() + + if os.path.exists(path) and not allow_overwrite: + raise SaveError("Profile %s already exists and not allowed to overwrite" % profile["id"]) + + from octoprint.util import atomic_write + import yaml + try: + with atomic_write(path, "wb", max_permissions=0o666) as f: + yaml.safe_dump(profile, f, default_flow_style=False, indent=" ", allow_unicode=True) + except Exception as e: + self._logger.exception("Error while trying to save profile %s" % profile["id"]) + raise SaveError("Cannot save profile %s: %s" % (profile["id"], str(e))) + + def _remove_from_path(self, path): + try: + os.remove(path) + return True + except Exception as e: + self._logger.warn( + "Tried to remove profile from {path}, ran into exception: {e}".format( + path=path, e=e)) + return False + + def _get_profile_path(self, identifier): + return os.path.join(self._folder, "%s.profile" % identifier) + + def _sanitize(self, name): + if name is None: + return None + + if "/" in name or "\\" in name: + raise ValueError("name must not contain / or \\") + + import string + valid_chars = "-_.() {ascii}{digits}".format(ascii=string.ascii_letters, digits=string.digits) + sanitized_name = ''.join(c for c in name if c in valid_chars) + sanitized_name = sanitized_name.replace(" ", "_") + return sanitized_name + + def _migrate_profile(self, profile): + """ + Subclasses must override this method. + """ + raise NotImplementedError("Subclasses must implement _migrate_profile") + + def _ensure_valid_profile(self, profile): + """ + Subclasses must override this method. + """ + raise NotImplementedError("Subclasses must implement _ensure_valid_profile.") diff --git a/octoprint_mrbeam/services/settings_service.py b/octoprint_mrbeam/service/settings_service.py similarity index 100% rename from octoprint_mrbeam/services/settings_service.py rename to octoprint_mrbeam/service/settings_service.py diff --git a/octoprint_mrbeam/static/js/app/view-models/convert.js b/octoprint_mrbeam/static/js/app/view-models/convert.js index 66dbaa9d2..9f414632b 100644 --- a/octoprint_mrbeam/static/js/app/view-models/convert.js +++ b/octoprint_mrbeam/static/js/app/view-models/convert.js @@ -1261,9 +1261,7 @@ $(function () { (intensity_black_user - intensity_white_user); var intensity = Math.round( intensity_user * - self.profile - .currentProfileData() - .laser.intensity_factor() + self.profile.currentProfileData().laser.intensity_factor ); var feedrate = Math.round( speed_white + initial_factor * (speed_black - speed_white) @@ -1317,7 +1315,7 @@ $(function () { ); var intensity = intensity_user * - self.profile.currentProfileData().laser.intensity_factor(); + self.profile.currentProfileData().laser.intensity_factor; var feedrate = parseFloat($(job).find(".param_feedrate").val()); var piercetime = parseFloat( $(job).find(".param_piercetime").val() @@ -1411,11 +1409,11 @@ $(function () { intensity_black_user: self.imgIntensityBlack(), intensity_black: self.imgIntensityBlack() * - self.profile.currentProfileData().laser.intensity_factor(), + self.profile.currentProfileData().laser.intensity_factor, intensity_white_user: self.imgIntensityWhite(), intensity_white: self.imgIntensityWhite() * - self.profile.currentProfileData().laser.intensity_factor(), + self.profile.currentProfileData().laser.intensity_factor, speed_black: self.imgFeedrateBlack(), speed_white: self.imgFeedrateWhite(), dithering: self.imgDithering(), diff --git a/octoprint_mrbeam/static/js/app/view-models/lasercutter-profiles.js b/octoprint_mrbeam/static/js/app/view-models/lasercutter-profiles.js index f50e6824e..3e7f3b0f3 100644 --- a/octoprint_mrbeam/static/js/app/view-models/lasercutter-profiles.js +++ b/octoprint_mrbeam/static/js/app/view-models/lasercutter-profiles.js @@ -65,74 +65,15 @@ $(function () { max_travel_z: 132, // Z Max travel, mm }; - self.profiles = new ItemListHelper( - "laserCutterProfiles", - { - name: function (a, b) { - // sorts ascending - if ( - a["name"].toLocaleLowerCase() < - b["name"].toLocaleLowerCase() - ) - return -1; - if ( - a["name"].toLocaleLowerCase() > - b["name"].toLocaleLowerCase() - ) - return 1; - return 0; - }, - }, - {}, - "name", - [], - [], - 10 - ); - self.hasDataLoaded = false; - self.defaultProfile = ko.observable(); self.currentProfile = ko.observable(); - self.currentProfileData = ko.observable( - ko.mapping.fromJS(self._cleanProfile()) - ); - - self.editorNew = ko.observable(false); - - self.editorName = ko.observable(); - self.editorIdentifier = ko.observable(); - self.editorModel = ko.observable(); - - self.editorVolumeWidth = ko.observable(); - self.editorVolumeDepth = ko.observable(); - self.editorVolumeHeight = ko.observable(); - - self.editorZAxis = ko.observable(); - self.editorFocus = ko.observable(); - self.editorGlasses = ko.observable(); - - self.editorAxisXSpeed = ko.observable(); - self.editorAxisYSpeed = ko.observable(); - self.editorAxisZSpeed = ko.observable(); - - self.editorAxisXInverted = ko.observable(false); - self.editorAxisYInverted = ko.observable(false); - self.editorAxisZInverted = ko.observable(false); - - self.makeDefault = function (data) { - var profile = { - id: data.id, - default: true, - }; - - self.updateProfile(profile); - }; + self.currentProfileData = ko.observable(self._cleanProfile()); self.requestData = function () { $.ajax({ - url: BASEURL + "plugin/mrbeam/profiles", + url: BASEURL + "plugin/mrbeam/currentProfile", type: "GET", dataType: "json", success: self.fromResponse, @@ -140,196 +81,41 @@ $(function () { }; self.fromResponse = function (data) { - var items = []; - var defaultProfile = undefined; - var currentProfile = undefined; - var currentProfileData = undefined; - _.each(data.profiles, function (entry) { - if (entry.default) { - defaultProfile = entry.id; - } - if (entry.current) { - currentProfile = entry.id; - currentProfileData = ko.mapping.fromJS( - entry, - self.currentProfileData - ); - } - entry["isdefault"] = ko.observable(entry.default); - entry["iscurrent"] = ko.observable(entry.current); - items.push(entry); - }); - self.profiles.updateItems(items); - self.defaultProfile(defaultProfile); - self.currentProfile(currentProfile); - self.currentProfileData(currentProfileData); - + self.currentProfile(data.id); + self.currentProfileData(data); self.hasDataLoaded = true; //TODO calculate MaxSpeed without Conversion - // var maxSpeed = Math.min(self.currentProfileData().axes.x.speed(), self.currentProfileData().axes.y.speed()); + // var maxSpeed = Math.min(self.currentProfileData().axes.x.speed, self.currentProfileData().axes.y.speed); // self.conversion.maxSpeed(maxSpeed); }; - self.addProfile = function (callback) { - var profile = self._editorData(); - $.ajax({ - url: BASEURL + "plugin/mrbeam/profiles", - type: "POST", - dataType: "json", - contentType: "application/json; charset=UTF-8", - data: JSON.stringify({ profile: profile }), - success: function () { - if (callback !== undefined) { - callback(); - } - self.requestData(); - }, - }); - }; - - self.removeProfile = function (data) { - $.ajax({ - url: data.resource, - type: "DELETE", - dataType: "json", - success: self.requestData, - }); - }; - - self.updateProfile = function (profile, callback) { - if (profile == undefined) { - profile = self._editorData(); - } - - $.ajax({ - url: BASEURL + "plugin/mrbeam/profiles/" + profile.id, - type: "PATCH", - dataType: "json", - contentType: "application/json; charset=UTF-8", - data: JSON.stringify({ profile: profile }), - success: function () { - if (callback !== undefined) { - callback(); - } - self.requestData(); - }, - }); - }; - - self.showEditProfileDialog = function (data) { - var add = false; - if (data == undefined) { - data = self._cleanProfile(); - add = true; - } - - self.editorNew(add); - - self.editorIdentifier(data.id); - self.editorName(data.name); - self.editorModel(data.model); - - self.editorVolumeWidth(data.volume.width); - self.editorVolumeDepth(data.volume.depth); - self.editorVolumeHeight(data.volume.height); - - self.editorZAxis(data.zAxis); - self.editorFocus(data.focus); - self.editorGlasses(data.glasses); - - self.editorAxisXSpeed(data.axes.x.speed); - self.editorAxisXInverted(data.axes.x.inverted); - self.editorAxisYSpeed(data.axes.y.speed); - self.editorAxisYInverted(data.axes.y.inverted); - self.editorAxisZSpeed(data.axes.z.speed); - self.editorAxisZInverted(data.axes.z.inverted); - - var editDialog = $("#settings_laserCutterProfiles_editDialog"); - var confirmButton = $("button.btn-confirm", editDialog); - var dialogTitle = $("h3.modal-title", editDialog); - - dialogTitle.text( - add - ? "Add Profile" - : _.sprintf('Edit Profile "%(name)s"', { name: data.name }) - ); - confirmButton.unbind("click"); - confirmButton.bind("click", function () { - self.confirmEditProfile(add); - }); - editDialog.modal("show"); - }; - - self.confirmEditProfile = function (add) { - var callback = function () { - $("#settings_laserCutterProfiles_editDialog").modal("hide"); - }; - - if (add) { - self.addProfile(callback); - } else { - self.updateProfile(undefined, callback); - } - }; - self.getMechanicalPerformanceData = function () { - const fx = self.currentProfileData().axes.x.speed(); - const fy = self.currentProfileData().axes.y.speed(); + const fx = self.currentProfileData().axes.x.speed; + const fy = self.currentProfileData().axes.y.speed; const maxF = Math.min(fx, fy); - const ax = self - .currentProfileData() - .grbl.settings[self.grblKeys.max_acc_x](); - const ay = self - .currentProfileData() - .grbl.settings[self.grblKeys.max_acc_y](); + const ax = + self.currentProfileData().grbl.settings[ + self.grblKeys.max_acc_x + ]; + const ay = + self.currentProfileData().grbl.settings[ + self.grblKeys.max_acc_y + ]; const maxAcc = Math.min(ax, ay); return { - workingAreaWidth: self.currentProfileData().volume.width(), - workingAreaHeight: self.currentProfileData().volume.depth(), + workingAreaWidth: self.currentProfileData().volume.width, + workingAreaHeight: self.currentProfileData().volume.depth, maxFeedrateXY: maxF, accelerationXY: maxAcc, }; }; - self._editorData = function () { - var profile = { - id: self.editorIdentifier(), - name: self.editorName(), - model: self.editorModel(), - volume: { - width: parseFloat(self.editorVolumeWidth()), - depth: parseFloat(self.editorVolumeDepth()), - height: parseFloat(self.editorVolumeHeight()), - }, - zAxis: self.editorZAxis(), - focus: self.editorFocus(), - glasses: self.editorGlasses(), - axes: { - x: { - speed: parseInt(self.editorAxisXSpeed()), - inverted: self.editorAxisXInverted(), - }, - y: { - speed: parseInt(self.editorAxisYSpeed()), - inverted: self.editorAxisYInverted(), - }, - z: { - speed: parseInt(self.editorAxisZSpeed()), - inverted: self.editorAxisZInverted(), - }, - }, - }; - - return profile; - }; - self.onSettingsShown = self.requestData; self.onStartup = function () { self.requestData(); self.control.showZAxis = ko.computed(function () { - var has = self.currentProfileData()["zAxis"](); - return has; + return self.currentProfileData()["zAxis"]; }); // dependency injection }; } diff --git a/octoprint_mrbeam/static/js/app/view-models/modal/ready-to-laser.js b/octoprint_mrbeam/static/js/app/view-models/modal/ready-to-laser.js index 39003ff23..0f61cbc07 100644 --- a/octoprint_mrbeam/static/js/app/view-models/modal/ready-to-laser.js +++ b/octoprint_mrbeam/static/js/app/view-models/modal/ready-to-laser.js @@ -77,7 +77,7 @@ $(function () { // self.oneButton = // self.laserCutterProfiles.currentProfileData().start_method != // undefined && - // self.laserCutterProfiles.currentProfileData().start_method() == + // self.laserCutterProfiles.currentProfileData().start_method == // "onebutton"; // if (!self.laserCutterProfiles.hasDataLoaded) { // self.oneButton = true; diff --git a/octoprint_mrbeam/static/js/app/view-models/mother.js b/octoprint_mrbeam/static/js/app/view-models/mother.js index fe3398cf4..343e54c82 100644 --- a/octoprint_mrbeam/static/js/app/view-models/mother.js +++ b/octoprint_mrbeam/static/js/app/view-models/mother.js @@ -76,7 +76,7 @@ $(function () { //self.requestData(); self.control.showZAxis = ko.computed(function () { - // var has = self.currentProfileData()['zAxis'](); + // var has = self.currentProfileData()['zAxis']; // return has; return false; }); diff --git a/octoprint_mrbeam/static/js/app/view-models/working-area.js b/octoprint_mrbeam/static/js/app/view-models/working-area.js index f3abb78f0..aba75ffbb 100644 --- a/octoprint_mrbeam/static/js/app/view-models/working-area.js +++ b/octoprint_mrbeam/static/js/app/view-models/working-area.js @@ -77,10 +77,10 @@ $(function () { }); self.workingAreaWidthMM = ko.computed(function () { - return self.profile.currentProfileData().volume.width(); + return self.profile.currentProfileData().volume.width; }, self); self.workingAreaHeightMM = ko.computed(function () { - return self.profile.currentProfileData().volume.depth(); + return self.profile.currentProfileData().volume.depth; }, self); // QuickShape limits @@ -419,7 +419,7 @@ $(function () { self.performHomingCycle = function (source) { let stateString = self.state ? self.state.stateString() : null; OctoPrint.printer - .home(["x", "y"]) + .home() .done(function () { console.log( "Homing call OK (source: " + @@ -4286,8 +4286,8 @@ $(function () { // TODO get from https://github.com/mrbeam/MrBeamPlugin/blob/2682b7a2e97373478e6516a98c8ba766d26ff317/octoprint_mrbeam/static/js/lasercutterprofiles.js#L276 // once this branch feature/SW-244... is merged. - const fx = self.profile.currentProfileData().axes.x.speed(); - const fy = self.profile.currentProfileData().axes.y.speed(); + const fx = self.profile.currentProfileData().axes.x.speed; + const fy = self.profile.currentProfileData().axes.y.speed; const maxSpeed = Math.min(fx, fy); crosshairHandle.mousedown(function (event) { diff --git a/octoprint_mrbeam/templates/hard_refresh_overlay.jinja2 b/octoprint_mrbeam/templates/hard_refresh_overlay.jinja2 index 614a2e70f..6f7e7a28d 100644 --- a/octoprint_mrbeam/templates/hard_refresh_overlay.jinja2 +++ b/octoprint_mrbeam/templates/hard_refresh_overlay.jinja2 @@ -21,9 +21,9 @@
- {{ _("Windows | Ctrl + Shift + R") }} + {{ _("Windows | Ctrl + Shift + R") }}
- {{ _("Mac | Command + Shift + R") }} + {{ _("Mac | Command + Shift + R") }}
diff --git a/octoprint_mrbeam/util/cmd_exec.py b/octoprint_mrbeam/util/cmd_exec.py index ad09b9c8b..538c661a1 100644 --- a/octoprint_mrbeam/util/cmd_exec.py +++ b/octoprint_mrbeam/util/cmd_exec.py @@ -26,7 +26,9 @@ def exec_cmd(cmd, log=True, shell=True, loglvl=DEBUG): e, ) return None - if code != 0 and log: + if code == -15: + _logger.warn("cmd= '{}', return code: {}".format(cmd, code)) + elif code != 0 and log: _logger.error("cmd= '{}', return code: {}".format(cmd, code)) return code == 0 diff --git a/test/test_iobeam_handler.py b/test/test_iobeam_handler.py index 522e02c39..7dc2c1bb3 100644 --- a/test/test_iobeam_handler.py +++ b/test/test_iobeam_handler.py @@ -225,7 +225,6 @@ def sendCommand(self, command): def _send(self, payload): if self.conn is not None: - self._logger.info(" --> " + payload) self.conn.send(payload + self.SOCKET_NEWLINE) else: raise Exception("No Connection, not able to write on socket") diff --git a/tests/conftest.py b/tests/conftest.py index be8e2174c..d3bf84de3 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -9,6 +9,7 @@ from octoprint.settings import settings from octoprint_mrbeam import MrBeamPlugin +from octoprint_mrbeam import deviceInfo, IS_X86 sett = settings(init=True) # Initialize octoprint settings, necessary for MrBeamPlugin @@ -48,7 +49,9 @@ def mrbeam_plugin(): ) mrbeam_plugin._settings.get_boolean = MagicMock() mrbeam_plugin._settings.global_get = MagicMock() + mrbeam_plugin._device_info = deviceInfo(use_dummy_values=IS_X86) mrbeam_plugin._event_bus = event_manager + mrbeam_plugin.laser_cutter_profile_service = MagicMock() mrbeam_plugin.dust_manager = MagicMock() mrbeam_plugin.temperature_manager = MagicMock() mrbeam_plugin.compressor_handler = MagicMock() diff --git a/tests/service/__init__.py b/tests/service/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/service/profile/__init__.py b/tests/service/profile/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/service/profile/test_profile.py b/tests/service/profile/test_profile.py new file mode 100644 index 000000000..bbe0d32d3 --- /dev/null +++ b/tests/service/profile/test_profile.py @@ -0,0 +1,173 @@ +import mock +import os +import pytest +import yaml +from mock.mock import MagicMock, patch +import json +from mock import mock_open + +from octoprint.settings import settings + +from octoprint_mrbeam.constant.profile import laser_cutter as laser_cutter_profiles +from octoprint_mrbeam.service.profile.laser_cutter_profile import laser_cutter_profile_service, \ + LaserCutterProfileService +from octoprint_mrbeam.service.profile.profile import ProfileService, InvalidProfileError, SaveError + + +def test_profile_when_class_instantiation_without_method_implementation(): + with pytest.raises(NotImplementedError): + ProfileService("profile_service_test_id", laser_cutter_profiles.default_profile) + + +class ProfileServiceImplementation(ProfileService): + + def __init__(self, profile_service_id, current_profile): + super(ProfileServiceImplementation, self).__init__(profile_service_id, current_profile) + + def _migrate_profile(self, profile): + return False + + def _ensure_valid_profile(self, profile): + return profile + + +@pytest.mark.parametrize("profile, identifier, profile_file_exists, exists", + [ + # Test case 1: Default profile, profile setting exists, profile file exists + (laser_cutter_profiles.default_profile, laser_cutter_profiles.default_profile["id"], True, + True), + # Test case 2: Rotary profile, profile setting exists, profile file exists + (laser_cutter_profiles.rotary_profile, laser_cutter_profiles.rotary_profile["id"], True, + True), + # Test case 3: Series 2C profile, profile setting exists, profile file exists + ( + laser_cutter_profiles.series_2c_profile, + laser_cutter_profiles.default_profile["id"], True, + True), + # Test case 4: Series 2C Rotary profile, profile setting exists, profile file exists + (laser_cutter_profiles.series_2c_rotary_profile, + laser_cutter_profiles.default_profile["id"], True, True), + # Test case 5: Default profile, profile setting does not exist, profile file does not exist + (laser_cutter_profiles.default_profile, laser_cutter_profiles.default_profile["id"], False, + False), + # Test case 6: Rotary profile, profile setting does not exist, profile file does not exist + (laser_cutter_profiles.rotary_profile, laser_cutter_profiles.rotary_profile["id"], False, + False), + # Test case 7: Series 2C profile, profile setting exists, profile file does not exist + (laser_cutter_profiles.series_2c_profile, laser_cutter_profiles.series_2c_profile["id"], + False, False), + # Test case 8: Series 2C Rotary profile, profile setting exists, profile default_profile does not exist + (laser_cutter_profiles.series_2c_rotary_profile, + laser_cutter_profiles.default_profile["id"], False, False), + ], + ids=["Test case 1: Default profile, profile setting exists, profile file exists", + "Test case 2: Rotary profile, profile setting exists, profile file exists", + "Test case 3: Series 2C profile, profile setting exists, profile file exists", + "Test case 4: Series 2C Rotary profile, profile setting exists, profile file exists", + "Test case 5: Default profile, profile setting does not exist, profile file does not exist", + "Test case 6: Rotary profile, profile setting does not exist, profile file does not exist", + "Test case 7: Series 2C profile, profile setting exists, profile file does not exist", + "Test case 8: Series 2C Rotary profile, profile setting exists, profile file does not exist" + ] + ) +def test_exists_when_profile_is_initiated(profile, identifier, profile_file_exists, exists): + profile_service_instance = ProfileServiceImplementation("profile_service_test_id", profile) + + with patch("os.path.exists", return_value=profile_file_exists), patch("os.path.isfile", + return_value=profile_file_exists): + # Assert + assert profile_service_instance.exists(identifier) == exists + + +def test_save_when_profile_is_empty(): + profile_service_instance = ProfileServiceImplementation("profile_service_test_id", + laser_cutter_profiles.default_profile) + with pytest.raises(InvalidProfileError): + profile_service_instance.save({}) + + +def test_save_when_profile_is_valid_and_path_exists_and_allow_overwrite_is_false(): + profile_service_instance = ProfileServiceImplementation("profile_service_test_id", + laser_cutter_profiles.default_profile) + with patch("os.path.exists", return_value=True), pytest.raises(SaveError): + profile_service_instance.save(laser_cutter_profiles.rotary_profile, False) + + +@pytest.mark.parametrize("profile, profile_file_exists, expected_profile", + [ + (laser_cutter_profiles.default_profile, True, laser_cutter_profiles.default_profile), + (laser_cutter_profiles.rotary_profile, True, laser_cutter_profiles.rotary_profile), + (laser_cutter_profiles.series_2c_profile, True, laser_cutter_profiles.series_2c_profile), + (laser_cutter_profiles.series_2c_rotary_profile, True, + laser_cutter_profiles.series_2c_rotary_profile), + (laser_cutter_profiles.default_profile, False, None), + (laser_cutter_profiles.rotary_profile, False, None), + (laser_cutter_profiles.series_2c_profile, False, None), + (laser_cutter_profiles.series_2c_rotary_profile, False, None), + ], + ids=[ + "Test case 1: Default profile, profile file exists", + "Test case 2: Rotary profile, profile file exists", + "Test case 3: Series 2C profile, profile file exists", + "Test case 4: Series 2C Rotary profile, profile file exists", + "Test case 5: Default profile, profile file does not exist", + "Test case 6: Rotary profile, profile file does not exist", + "Test case 7: Series 2C profile, profile file does not exist", + "Test case 8: Series 2C Rotary profile, profile file does not exist"] + ) +def test_get_when_profile_is_initiated(profile, profile_file_exists, expected_profile, mocker): + profile_service_instance = ProfileServiceImplementation("profile_service_test_id", profile) + + with patch("os.path.exists", return_value=profile_file_exists), patch("os.path.isfile", + return_value=profile_file_exists), patch( + "os.path.exists", + return_value=profile_file_exists), patch( + "os.path.isfile", return_value=profile_file_exists), patch("__builtin__.open", + mock_open(read_data=json.dumps(profile))), patch( + "yaml.safe_load", return_value=profile): + # Assert + assert profile_service_instance.get(profile["id"]) == expected_profile + + +@pytest.mark.parametrize("profile, profile_setting, profile_file_exists, expected_profile", + [ + (laser_cutter_profiles.default_profile, laser_cutter_profiles.default_profile["id"], True, + laser_cutter_profiles.default_profile), + (laser_cutter_profiles.rotary_profile, laser_cutter_profiles.rotary_profile["id"], True, + laser_cutter_profiles.rotary_profile), + (laser_cutter_profiles.series_2c_profile, laser_cutter_profiles.series_2c_profile["id"], + True, laser_cutter_profiles.series_2c_profile), + (laser_cutter_profiles.series_2c_rotary_profile, + laser_cutter_profiles.series_2c_rotary_profile["id"], True, + laser_cutter_profiles.series_2c_rotary_profile), + (laser_cutter_profiles.default_profile, None, False, + laser_cutter_profiles.default_profile), + (laser_cutter_profiles.rotary_profile, None, False, laser_cutter_profiles.rotary_profile), + (laser_cutter_profiles.series_2c_profile, laser_cutter_profiles.series_2c_profile["id"], + False, laser_cutter_profiles.series_2c_profile), + (laser_cutter_profiles.series_2c_rotary_profile, + laser_cutter_profiles.series_2c_rotary_profile["id"], False, + laser_cutter_profiles.series_2c_rotary_profile), + ], + ids=["Test case 1: Default profile, profile setting exists, profile file exists", + "Test case 2: Rotary profile, profile setting exists, profile file exists", + "Test case 3: Series 2C profile, profile setting exists, profile file exists", + "Test case 4: Series 2C Rotary profile, profile setting exists, profile file exists", + "Test case 5: Default profile, profile setting does not exist, profile file does not exist", + "Test case 6: Rotary profile, profile setting does not exist, profile file does not exist", + "Test case 7: Series 2C profile, profile setting exists, profile file does not exist", + "Test case 8: Series 2C Rotary profile, profile setting exists, profile file does not exist"] + ) +def test_get_default_when_profile_is_initiated(profile, profile_setting, profile_file_exists, expected_profile): + profile_service_instance = ProfileServiceImplementation("profile_service_test_id", profile) + + patch.object(settings(), "get", return_value=profile_setting) + with patch("os.path.exists", return_value=profile_file_exists), patch("os.path.isfile", + return_value=profile_file_exists), patch( + "os.path.exists", + return_value=profile_file_exists), patch( + "os.path.isfile", return_value=profile_file_exists), patch("__builtin__.open", + mock_open(read_data=json.dumps(profile))), patch( + "yaml.safe_load", return_value=profile): + # Assert + assert profile_service_instance.get_default() == expected_profile diff --git a/tests/services/test_burger_menu_service.py b/tests/service/test_burger_menu_service.py similarity index 84% rename from tests/services/test_burger_menu_service.py rename to tests/service/test_burger_menu_service.py index 6c8698926..d30d82a0a 100644 --- a/tests/services/test_burger_menu_service.py +++ b/tests/service/test_burger_menu_service.py @@ -7,8 +7,8 @@ from octoprint_mrbeamdoc.enum.supported_languages import SupportedLanguage from octoprint_mrbeamdoc.model.mrbeam_doc_definition import MrBeamDocDefinition -from octoprint_mrbeam.services.document_service import DocumentService -from octoprint_mrbeam.services.burger_menu_service import BurgerMenuService +from octoprint_mrbeam.service.document_service import DocumentService +from octoprint_mrbeam.service.burger_menu_service import BurgerMenuService from tests.logger.test_logger import LoggerMock @@ -23,7 +23,7 @@ def test_get_burger_menu_model__with_none__should_return_empty_burger_menu_model burger_menu_model = self._burger_menu_service.get_burger_menu_model(None) self.assertIs(len(burger_menu_model.documents), 0) - @patch('octoprint_mrbeam.services.burger_menu_service.get_locale') + @patch('octoprint_mrbeam.service.burger_menu_service.get_locale') def test_get_burger_menu_model__with_unsupported_language__should_return_default_to_english(self, get_locale_mock): get_locale_mock.return_value = MagicMock(language='ch') burger_menu_model = self._burger_menu_service.get_burger_menu_model(MrBeamModel.MRBEAM2.value) @@ -31,8 +31,8 @@ def test_get_burger_menu_model__with_unsupported_language__should_return_default for document in burger_menu_model.documents: self.assertEquals(document.document_link.language, SupportedLanguage.ENGLISH) - @patch('octoprint_mrbeam.services.burger_menu_service.MrBeamDocUtils.get_mrbeam_definitions_for') - @patch('octoprint_mrbeam.services.burger_menu_service.get_locale') + @patch('octoprint_mrbeam.service.burger_menu_service.MrBeamDocUtils.get_mrbeam_definitions_for') + @patch('octoprint_mrbeam.service.burger_menu_service.get_locale') def test_get_burger_menu_model__with_language_not_valid_for_definition__should_fallback_to_english(self, get_locale_mock, get_mrbeam_definitions_for_mock): @@ -45,7 +45,7 @@ def test_get_burger_menu_model__with_language_not_valid_for_definition__should_f for document in burger_menu_model.documents: self.assertEquals(document.document_link.language, SupportedLanguage.ENGLISH) - @patch('octoprint_mrbeam.services.burger_menu_service.get_locale') + @patch('octoprint_mrbeam.service.burger_menu_service.get_locale') def test_get_burger_menu_model__with_supported_language__should_return_documents_in_that_language(self, get_locale_mock): get_locale_mock.return_value = MagicMock(language='de') @@ -54,9 +54,9 @@ def test_get_burger_menu_model__with_supported_language__should_return_documents for document in burger_menu_model.documents: self.assertEquals(document.document_link.language, SupportedLanguage.GERMAN) - @patch('octoprint_mrbeam.services.burger_menu_service.MrBeamDocUtils.get_mrbeam_definitions_for') - @patch('octoprint_mrbeam.services.document_service.gettext') - @patch('octoprint_mrbeam.services.burger_menu_service.get_locale') + @patch('octoprint_mrbeam.service.burger_menu_service.MrBeamDocUtils.get_mrbeam_definitions_for') + @patch('octoprint_mrbeam.service.document_service.gettext') + @patch('octoprint_mrbeam.service.burger_menu_service.get_locale') def test_get_burger_menu_model__with_unicode_translation__should_work(self, get_locale_mock, get_text_mock, get_mrbeam_definitions_for_mock): get_locale_mock.return_value = MagicMock(language='es') @@ -69,9 +69,9 @@ def test_get_burger_menu_model__with_unicode_translation__should_work(self, get_ for document in burger_menu_model.documents: self.assertEquals(document.document_link.language, SupportedLanguage.SPANISH) - @patch('octoprint_mrbeam.services.burger_menu_service.MrBeamDocUtils.get_mrbeam_definitions_for') - @patch('octoprint_mrbeam.services.document_service.gettext') - @patch('octoprint_mrbeam.services.burger_menu_service.get_locale') + @patch('octoprint_mrbeam.service.burger_menu_service.MrBeamDocUtils.get_mrbeam_definitions_for') + @patch('octoprint_mrbeam.service.document_service.gettext') + @patch('octoprint_mrbeam.service.burger_menu_service.get_locale') def test_get_burger_menu_model__with_null_translation__should_not_crash_and_return_title_as_None(self, get_locale_mock, get_text_mock, diff --git a/tests/services/test_laser_cutter_mode.py b/tests/service/test_laser_cutter_mode.py similarity index 92% rename from tests/services/test_laser_cutter_mode.py rename to tests/service/test_laser_cutter_mode.py index 1af6d3cfb..c9cc83d63 100644 --- a/tests/services/test_laser_cutter_mode.py +++ b/tests/service/test_laser_cutter_mode.py @@ -1,7 +1,7 @@ import pytest from octoprint_mrbeam.enums.laser_cutter_mode import LaserCutterModeEnum -from octoprint_mrbeam.services.laser_cutter_mode import LaserCutterModeService +from octoprint_mrbeam.service.laser_cutter_mode import LaserCutterModeService @pytest.fixture diff --git a/tests/services/test_settings_service.py b/tests/service/test_settings_service.py similarity index 88% rename from tests/services/test_settings_service.py rename to tests/service/test_settings_service.py index 4570ddc44..32b9876e4 100644 --- a/tests/services/test_settings_service.py +++ b/tests/service/test_settings_service.py @@ -8,8 +8,8 @@ from octoprint_mrbeamdoc.enum.mrbeam_model import MrBeamModel from octoprint_mrbeam import DocumentService, SWUpdateTier -from octoprint_mrbeam.services import settings_service -from octoprint_mrbeam.services.settings_service import SettingsService +from octoprint_mrbeam.service import settings_service +from octoprint_mrbeam.service.settings_service import SettingsService from tests.logger.test_logger import LoggerMock @@ -35,7 +35,7 @@ def test_get_template_settings_model_with_dreamcut__then_return_settings_with_ab settings_model = self._settings_service.get_template_settings_model(MrBeamModel.DREAMCUT_S.value) self._validate_settings_model(settings_model) - @patch('octoprint_mrbeam.services.settings_service.requests.get', + @patch('octoprint_mrbeam.service.settings_service.requests.get', side_effect=requests.exceptions.RequestException()) def test_get_template_settings_model_with_no_internet__then_return_settings_with_empty_material_store_settings( self, requests_mock): @@ -43,16 +43,16 @@ def test_get_template_settings_model_with_no_internet__then_return_settings_with self._validate_settings_model(settings_model) self._validate_empty_material_store_settings(settings_model) - @patch('octoprint_mrbeam.services.settings_service.requests.get') - @patch('octoprint_mrbeam.services.settings_service.yaml.load', side_effect=yaml.YAMLError()) + @patch('octoprint_mrbeam.service.settings_service.requests.get') + @patch('octoprint_mrbeam.service.settings_service.yaml.load', side_effect=yaml.YAMLError()) def test_get_template_settings_model_with_yaml_issue_in_material_store__then_empty_material_store_settings( self, yaml_mock, requests_mock): settings_model = self._settings_service.get_template_settings_model(MrBeamModel.DREAMCUT_S.value) self._validate_settings_model(settings_model) self._validate_empty_material_store_settings(settings_model) - @patch('octoprint_mrbeam.services.settings_service.requests.get') - @patch('octoprint_mrbeam.services.settings_service.yaml.load') + @patch('octoprint_mrbeam.service.settings_service.requests.get') + @patch('octoprint_mrbeam.service.settings_service.yaml.load') def test_get_template_settings_model_with_none_material_store_settings__then_empty_material_store_settings(self, yaml_mock, requests_mock): yaml_mock.return_value = None @@ -60,8 +60,8 @@ def test_get_template_settings_model_with_none_material_store_settings__then_emp self._validate_settings_model(settings_model) self._validate_empty_material_store_settings(settings_model) - @patch('octoprint_mrbeam.services.settings_service.requests.get') - @patch('octoprint_mrbeam.services.settings_service.yaml.load') + @patch('octoprint_mrbeam.service.settings_service.requests.get') + @patch('octoprint_mrbeam.service.settings_service.yaml.load') def test_get_template_settings_model_with_empty_material_store_settings__then_empty_material_store_settings(self, yaml_mock, requests_mock): yaml_mock.return_value = {} @@ -69,8 +69,8 @@ def test_get_template_settings_model_with_empty_material_store_settings__then_em self._validate_settings_model(settings_model) self._validate_empty_material_store_settings(settings_model) - @patch('octoprint_mrbeam.services.settings_service.requests.get') - @patch('octoprint_mrbeam.services.settings_service.yaml.load') + @patch('octoprint_mrbeam.service.settings_service.requests.get') + @patch('octoprint_mrbeam.service.settings_service.yaml.load') def test_get_template_settings_model_with_no_material_store_settings__then_empty_material_store_settings(self, yaml_mock, requests_mock): yaml_mock.return_value = {'material-store': {}} @@ -78,8 +78,8 @@ def test_get_template_settings_model_with_no_material_store_settings__then_empty self._validate_settings_model(settings_model) self._validate_empty_material_store_settings(settings_model) - @patch('octoprint_mrbeam.services.settings_service.requests.get') - @patch('octoprint_mrbeam.services.settings_service.yaml.load') + @patch('octoprint_mrbeam.service.settings_service.requests.get') + @patch('octoprint_mrbeam.service.settings_service.yaml.load') def test_get_template_settings_model_with_no_environment_material_store_settings__then_empty_material_store_settings( self, yaml_mock, requests_mock): yaml_mock.return_value = {'material-store': {'environment': {}}} @@ -87,8 +87,8 @@ def test_get_template_settings_model_with_no_environment_material_store_settings self._validate_settings_model(settings_model) self._validate_empty_material_store_settings(settings_model) - @patch('octoprint_mrbeam.services.settings_service.requests.get') - @patch('octoprint_mrbeam.services.settings_service.yaml.load') + @patch('octoprint_mrbeam.service.settings_service.requests.get') + @patch('octoprint_mrbeam.service.settings_service.yaml.load') def test_get_template_settings_model_with_no_matching_environment_material_store_settings__then_empty_material_store_settings( self, yaml_mock, requests_mock): yaml_mock.return_value = {'material-store': { @@ -97,8 +97,8 @@ def test_get_template_settings_model_with_no_matching_environment_material_store self._validate_settings_model(settings_model) self._validate_empty_material_store_settings(settings_model) - @patch('octoprint_mrbeam.services.settings_service.requests.get') - @patch('octoprint_mrbeam.services.settings_service.yaml.load') + @patch('octoprint_mrbeam.service.settings_service.requests.get') + @patch('octoprint_mrbeam.service.settings_service.yaml.load') def test_get_template_settings_model_with_no_url_material_store_settings__then_empty_material_store_settings(self, yaml_mock, requests_mock): yaml_mock.return_value = {'material-store': { @@ -107,8 +107,8 @@ def test_get_template_settings_model_with_no_url_material_store_settings__then_e self._validate_settings_model(settings_model) self._validate_empty_material_store_settings(settings_model) - @patch('octoprint_mrbeam.services.settings_service.requests.get') - @patch('octoprint_mrbeam.services.settings_service.yaml.load') + @patch('octoprint_mrbeam.service.settings_service.requests.get') + @patch('octoprint_mrbeam.service.settings_service.yaml.load') def test_get_template_settings_model_with_no_enabled_material_store_settings__then_empty_material_store_settings( self, yaml_mock, requests_mock): yaml_mock.return_value = {'material-store': { @@ -117,8 +117,8 @@ def test_get_template_settings_model_with_no_enabled_material_store_settings__th self._validate_settings_model(settings_model) self._validate_empty_material_store_settings(settings_model) - @patch('octoprint_mrbeam.services.settings_service.requests.get') - @patch('octoprint_mrbeam.services.settings_service.yaml.load') + @patch('octoprint_mrbeam.service.settings_service.requests.get') + @patch('octoprint_mrbeam.service.settings_service.yaml.load') def test_get_template_settings_model_with_no_healthcheck_url_material_store_settings__then_empty_material_store_settings( self, yaml_mock, requests_mock): yaml_mock.return_value = {'material-store': { @@ -127,8 +127,8 @@ def test_get_template_settings_model_with_no_healthcheck_url_material_store_sett self._validate_settings_model(settings_model) self._validate_empty_material_store_settings(settings_model) - @patch('octoprint_mrbeam.services.settings_service.requests.get') - @patch('octoprint_mrbeam.services.settings_service.yaml.load') + @patch('octoprint_mrbeam.service.settings_service.requests.get') + @patch('octoprint_mrbeam.service.settings_service.yaml.load') def test_get_template_settings_model_with_correct_material_store_settings__then_valid_settings(self, yaml_mock, requests_mock): yaml_mock.return_value = {'material-store': { 'environment': {'prod': {'url': 'https://test.material.store.mr-beam.org', 'enabled': True, 'healthcheck_url': 'https://test.material.store.mr-beam.org/api/healthcheck'}}}} diff --git a/tests/test___init__.py b/tests/test___init__.py new file mode 100644 index 000000000..8de947c6d --- /dev/null +++ b/tests/test___init__.py @@ -0,0 +1,72 @@ +import pytest +from mock.mock import patch, MagicMock + +from octoprint_mrbeam.enums.laser_cutter_mode import LaserCutterModeEnum +from octoprint_mrbeam.enums.device_series import DeviceSeriesEnum +from octoprint_mrbeam.constant.profile import laser_cutter as laser_cutter_profiles + +def test_update_laser_cutter_profile_when_profile_and_id_are_valid(mrbeam_plugin): + # Arrange + sample_profile = {'id': "default_profile", 'name': 'Default Profile'} + + # Act + mrbeam_plugin.update_laser_cutter_profile(sample_profile) + + # Assert that the save method was called with the correct arguments + mrbeam_plugin.laser_cutter_profile_service.save.assert_called_once_with( + sample_profile, allow_overwrite=True, make_default=True + ) + + # Assert that the select method was called with the correct argument + mrbeam_plugin.laser_cutter_profile_service.select.assert_called_once_with(sample_profile['id']) + +def test_update_laser_cutter_profile_when_profile_is_invalid(mrbeam_plugin): + # Arrange + sample_profile = "not_a_dict" + + # Act and Assert + with pytest.raises(TypeError): + mrbeam_plugin.update_laser_cutter_profile(sample_profile) + + # Act and Assert + with pytest.raises(TypeError): + mrbeam_plugin.update_laser_cutter_profile() + +def test_update_laser_cutter_profile_when_id_is_invalid(mrbeam_plugin): + # Arrange + sample_profile_1 = {'id': 1, 'name': 'Default Profile'} + sample_profile_2 = {'name': 'Default Profile'} + + # Act and Assert + with pytest.raises(ValueError): + mrbeam_plugin.update_laser_cutter_profile(sample_profile_1) + + # Act and Assert + with pytest.raises(ValueError): + mrbeam_plugin.update_laser_cutter_profile(sample_profile_2) + +@pytest.mark.parametrize("laser_cutter_mode, device_series, expected_profile", [ + # Test case 1: Default mode, Series non-C + (LaserCutterModeEnum.DEFAULT.value, None, laser_cutter_profiles.default_profile), + # Test case 2: Default mode, Series C + (LaserCutterModeEnum.DEFAULT.value, DeviceSeriesEnum.C.value , laser_cutter_profiles.series_2c_profile), + # Test case 3: Rotary mode, Series non-C + (LaserCutterModeEnum.ROTARY.value, None, laser_cutter_profiles.rotary_profile), + # Test case 4: Rotary mode, Series C + (LaserCutterModeEnum.ROTARY.value, DeviceSeriesEnum.C.value, laser_cutter_profiles.series_2c_rotary_profile), + # Test case 5: No mode, No series + (None, None, laser_cutter_profiles.default_profile), +],) +def test_get_laser_cutter_profile_for_current_configuration(laser_cutter_mode, device_series, expected_profile, mocker, mrbeam_plugin): + # Arrange + with patch( + "octoprint_mrbeam.util.device_info.DeviceInfo.get_series", + return_value=device_series, + ): + mocker.patch("octoprint_mrbeam.MrBeamPlugin.get_laser_cutter_mode", return_value=laser_cutter_mode) + + # Act + result_profile = mrbeam_plugin.get_laser_cutter_profile_for_current_configuration() + + # Assert + assert result_profile == expected_profile